Merge branch 'b-3.6.1' of ../kivitendo-erp_20220811
authorMichael Wagner <michael@wagnertech.de>
Fri, 12 Aug 2022 11:53:09 +0000 (13:53 +0200)
committerMichael Wagner <michael@wagnertech.de>
Fri, 12 Aug 2022 11:53:09 +0000 (13:53 +0200)
Conflicts:
SL/Controller/Mebil.pm
VERSION
locale/de/all
menus/user/00-erp.yaml

2102 files changed:
.eslintrc.js [new file with mode: 0644]
.gitignore
.htaccess
.jshintrc [new file with mode: 0644]
.mailmap [new file with mode: 0644]
Devel/REPL/Plugin/AutoloadModules.pm [new file with mode: 0644]
Devel/REPL/Plugin/PermanentHistory.pm [new file with mode: 0644]
SL/.htaccess
SL/AM.pm
SL/AP.pm
SL/AR.pm
SL/ARAP.pm
SL/AccTransCorrections.pm
SL/ArchiveZipFixes.pm [deleted file]
SL/Auth.pm
SL/Auth/ColumnInformation.pm
SL/Auth/DB.pm
SL/Auth/LDAP.pm
SL/Auth/Password.pm
SL/Auth/SessionValue.pm
SL/BP.pm
SL/BackgroundJob/ALL.pm [deleted file]
SL/BackgroundJob/CloseProjectsBelongingToClosedSalesOrders.pm [new file with mode: 0644]
SL/BackgroundJob/ConvertTimeRecordings.pm [new file with mode: 0644]
SL/BackgroundJob/CreateOrUpdateFileFullTexts.pm [new file with mode: 0644]
SL/BackgroundJob/CreatePeriodicInvoices.pm
SL/BackgroundJob/CsvImport.pm
SL/BackgroundJob/FailedBackgroundJobsReport.pm
SL/BackgroundJob/MassDeliveryOrderPrinting.pm [new file with mode: 0644]
SL/BackgroundJob/MassRecordCreationAndPrinting.pm
SL/BackgroundJob/SelfTest.pm
SL/BackgroundJob/SelfTest/Base.pm
SL/BackgroundJob/SelfTest/Transactions.pm
SL/BackgroundJob/SetNumberRange.pm [new file with mode: 0644]
SL/BackgroundJob/ShopOrderMassTransfer.pm [new file with mode: 0644]
SL/BackgroundJob/ShopPartMassUpload.pm [new file with mode: 0644]
SL/BackgroundJob/Test.pm
SL/CA.pm
SL/CP.pm
SL/CT.pm
SL/CTI.pm
SL/CVar.pm
SL/ClientJS.pm
SL/Common.pm
SL/Controller/AccTrans.pm
SL/Controller/Admin.pm
SL/Controller/BackgroundJob.pm
SL/Controller/BackgroundJobHistory.pm
SL/Controller/BankAccount.pm [deleted file]
SL/Controller/BankImport.pm
SL/Controller/BankTransaction.pm
SL/Controller/Base.pm
SL/Controller/Buchungsgruppen.pm
SL/Controller/Business.pm [deleted file]
SL/Controller/Chart.pm
SL/Controller/ClientConfig.pm
SL/Controller/CsvImport.pm
SL/Controller/CsvImport/ARTransaction.pm [new file with mode: 0644]
SL/Controller/CsvImport/AdditionalBillingAddress.pm [new file with mode: 0644]
SL/Controller/CsvImport/BankTransaction.pm
SL/Controller/CsvImport/Base.pm
SL/Controller/CsvImport/BaseMulti.pm
SL/Controller/CsvImport/Contact.pm
SL/Controller/CsvImport/CustomerVendor.pm
SL/Controller/CsvImport/DeliveryOrder.pm [new file with mode: 0644]
SL/Controller/CsvImport/Helper/Consistency.pm
SL/Controller/CsvImport/Inventory.pm
SL/Controller/CsvImport/Order.pm
SL/Controller/CsvImport/Part.pm
SL/Controller/CsvImport/Project.pm
SL/Controller/CsvImport/Shipto.pm
SL/Controller/CustomDataExport.pm [new file with mode: 0644]
SL/Controller/CustomDataExportDesigner.pm [new file with mode: 0644]
SL/Controller/CustomVariableConfig.pm
SL/Controller/Customer.pm
SL/Controller/CustomerVendor.pm
SL/Controller/CustomerVendorTurnover.pm [new file with mode: 0644]
SL/Controller/DeliveryOrder.pm [new file with mode: 0644]
SL/Controller/DeliveryOrder/TypeData.pm [new file with mode: 0644]
SL/Controller/DeliveryPlan.pm
SL/Controller/DeliveryTerm.pm
SL/Controller/DeliveryValueReport.pm
SL/Controller/Department.pm [deleted file]
SL/Controller/DownloadZip.pm [new file with mode: 0644]
SL/Controller/Draft.pm [new file with mode: 0644]
SL/Controller/EmailJournal.pm
SL/Controller/Employee.pm
SL/Controller/File.pm [new file with mode: 0644]
SL/Controller/FinancialControllingReport.pm
SL/Controller/FinancialOverview.pm
SL/Controller/GL.pm [deleted file]
SL/Controller/GoBD.pm [new file with mode: 0644]
SL/Controller/Helper/GetModels/Paginated.pm
SL/Controller/Helper/ParseFilter.pm
SL/Controller/Helper/ReportGenerator.pm
SL/Controller/Helper/ReportGenerator/ControlRow.pm [new file with mode: 0644]
SL/Controller/Helper/ReportGenerator/ControlRow/ALL.pm [new file with mode: 0644]
SL/Controller/Helper/ReportGenerator/ControlRow/Base.pm [new file with mode: 0644]
SL/Controller/Helper/ReportGenerator/ControlRow/Data.pm [new file with mode: 0644]
SL/Controller/Helper/ReportGenerator/ControlRow/Separator.pm [new file with mode: 0644]
SL/Controller/Helper/ReportGenerator/ControlRow/SimpleData.pm [new file with mode: 0644]
SL/Controller/Helper/ThumbnailCreator.pm [new file with mode: 0644]
SL/Controller/ImageUpload.pm [new file with mode: 0644]
SL/Controller/Inventory.pm
SL/Controller/Letter.pm [new file with mode: 0644]
SL/Controller/LiquidityProjection.pm
SL/Controller/LoginScreen.pm
SL/Controller/MassDeliveryOrderPrint.pm [new file with mode: 0644]
SL/Controller/MassInvoiceCreatePrint.pm
SL/Controller/MaterializeTest.pm [new file with mode: 0644]
SL/Controller/Mebil.pm
SL/Controller/ODGeierlein.pm [new file with mode: 0644]
SL/Controller/Order.pm [new file with mode: 0644]
SL/Controller/OrderItem.pm [new file with mode: 0644]
SL/Controller/Part.pm
SL/Controller/PartsPriceHistory.pm [new file with mode: 0644]
SL/Controller/PartsPriceUpdate.pm [new file with mode: 0644]
SL/Controller/PayPostingImport.pm [new file with mode: 0644]
SL/Controller/PaymentTerm.pm
SL/Controller/PhoneNumber.pm [new file with mode: 0644]
SL/Controller/PriceRule.pm
SL/Controller/PriceSource.pm
SL/Controller/Project.pm
SL/Controller/ProjectStatus.pm [deleted file]
SL/Controller/ProjectType.pm [deleted file]
SL/Controller/Reconciliation.pm
SL/Controller/RecordLinks.pm
SL/Controller/RecordTemplate.pm [new file with mode: 0644]
SL/Controller/RequirementSpec.pm
SL/Controller/RequirementSpecAcceptanceStatus.pm [deleted file]
SL/Controller/RequirementSpecComplexity.pm [deleted file]
SL/Controller/RequirementSpecItem.pm
SL/Controller/RequirementSpecOrder.pm
SL/Controller/RequirementSpecPart.pm
SL/Controller/RequirementSpecPredefinedText.pm [deleted file]
SL/Controller/RequirementSpecRisk.pm [deleted file]
SL/Controller/RequirementSpecStatus.pm [deleted file]
SL/Controller/RequirementSpecTextBlock.pm
SL/Controller/RequirementSpecType.pm [deleted file]
SL/Controller/SalesPurchase.pm [new file with mode: 0644]
SL/Controller/SellPriceInformation.pm
SL/Controller/Shop.pm [new file with mode: 0644]
SL/Controller/ShopOrder.pm [new file with mode: 0644]
SL/Controller/ShopPart.pm [new file with mode: 0644]
SL/Controller/SimpleSystemSetting.pm [new file with mode: 0644]
SL/Controller/TaskServer.pm
SL/Controller/Taxzones.pm
SL/Controller/TimeRecording.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/Article.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/Assembly.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/Assortment.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/Base.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/Contact.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/Customer.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/CustomerVendor.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/DeliveryOrder.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/GLTransaction.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/OERecord.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/Part.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/PhoneNumber.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/PurchaseDeliveryOrder.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/PurchaseOrder.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/RequestForQuotation.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/SalesDeliveryOrder.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/SalesOrder.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/SalesQuotation.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/Service.pm [new file with mode: 0644]
SL/Controller/TopQuickSearch/Vendor.pm [new file with mode: 0644]
SL/Controller/YearEndTransactions.pm [new file with mode: 0644]
SL/Controller/ZUGFeRD.pm [new file with mode: 0644]
SL/DATEV.pm
SL/DATEV/CSV.pm [new file with mode: 0644]
SL/DATEV/KNEFile.pm [deleted file]
SL/DB.pm
SL/DB/AdditionalBillingAddress.pm [new file with mode: 0644]
SL/DB/ApGl.pm [new file with mode: 0644]
SL/DB/Assembly.pm
SL/DB/AssortmentItem.pm [new file with mode: 0644]
SL/DB/AuthMasterRight.pm [new file with mode: 0644]
SL/DB/AuthSchemaInfo.pm [new file with mode: 0644]
SL/DB/AuthSession.pm [new file with mode: 0644]
SL/DB/AuthSessionContent.pm [new file with mode: 0644]
SL/DB/AuthUser.pm
SL/DB/BackgroundJob.pm
SL/DB/BankAccount.pm
SL/DB/BankTransaction.pm
SL/DB/BankTransactionAccTrans.pm [new file with mode: 0644]
SL/DB/Buchungsgruppe.pm
SL/DB/Business.pm
SL/DB/Chart.pm
SL/DB/Contact.pm
SL/DB/ContactDepartment.pm [new file with mode: 0644]
SL/DB/ContactTitle.pm [new file with mode: 0644]
SL/DB/CsvImportProfile.pm
SL/DB/CsvImportReport.pm
SL/DB/CustomDataExportQuery.pm [new file with mode: 0644]
SL/DB/CustomDataExportQueryParameter.pm [new file with mode: 0644]
SL/DB/CustomVariable.pm
SL/DB/CustomVariableConfig.pm
SL/DB/Customer.pm
SL/DB/Default.pm
SL/DB/DeliveryOrder.pm
SL/DB/DeliveryOrder/TypeData.pm [new file with mode: 0644]
SL/DB/DeliveryOrderItem.pm
SL/DB/DeliveryOrderItemsStock.pm
SL/DB/Dunning.pm
SL/DB/EmailJournal.pm
SL/DB/Employee.pm
SL/DB/EmployeeProjectInvoices.pm [new file with mode: 0644]
SL/DB/File.pm [new file with mode: 0644]
SL/DB/FileFullText.pm [new file with mode: 0644]
SL/DB/GLTransaction.pm
SL/DB/Greeting.pm [new file with mode: 0644]
SL/DB/Helper/ALL.pm
SL/DB/Helper/AccountingPeriod.pm
SL/DB/Helper/ActsAsList.pm
SL/DB/Helper/Attr.pm
SL/DB/Helper/AttrDuration.pm
SL/DB/Helper/AttrHTML.pm
SL/DB/Helper/AttrSorted.pm
SL/DB/Helper/Cache.pm [new file with mode: 0644]
SL/DB/Helper/CustomVariables.pm
SL/DB/Helper/DisplayableNamePreferences.pm [new file with mode: 0644]
SL/DB/Helper/FlattenToForm.pm
SL/DB/Helper/IBANValidation.pm [new file with mode: 0644]
SL/DB/Helper/LinkedRecords.pm
SL/DB/Helper/Manager.pm
SL/DB/Helper/Mappings.pm
SL/DB/Helper/Metadata.pm
SL/DB/Helper/PDF_A.pm [new file with mode: 0644]
SL/DB/Helper/Payment.pm
SL/DB/Helper/Presenter.pm [new file with mode: 0644]
SL/DB/Helper/PriceTaxCalculator.pm
SL/DB/Helper/SalesPurchaseInvoice.pm [new file with mode: 0644]
SL/DB/Helper/Sorted.pm
SL/DB/Helper/TransNumberGenerator.pm
SL/DB/Helper/VATIDNrValidation.pm [new file with mode: 0644]
SL/DB/Helper/ValidateAssembly.pm [new file with mode: 0644]
SL/DB/Helper/ZUGFeRD.pm [new file with mode: 0644]
SL/DB/History.pm
SL/DB/Inventory.pm
SL/DB/Invoice.pm
SL/DB/Letter.pm
SL/DB/LetterDraft.pm
SL/DB/MakeModel.pm
SL/DB/Manager/AdditionalBillingAddress.pm [new file with mode: 0644]
SL/DB/Manager/ApGl.pm [new file with mode: 0644]
SL/DB/Manager/AssortmentItem.pm [new file with mode: 0644]
SL/DB/Manager/AuthMasterRight.pm [new file with mode: 0644]
SL/DB/Manager/AuthSchemaInfo.pm [new file with mode: 0644]
SL/DB/Manager/AuthSession.pm [new file with mode: 0644]
SL/DB/Manager/AuthSessionContent.pm [new file with mode: 0644]
SL/DB/Manager/BackgroundJob.pm
SL/DB/Manager/BankTransactionAccTrans.pm [new file with mode: 0644]
SL/DB/Manager/Chart.pm
SL/DB/Manager/ContactDepartment.pm [new file with mode: 0644]
SL/DB/Manager/ContactTitle.pm [new file with mode: 0644]
SL/DB/Manager/CsvImportReport.pm
SL/DB/Manager/CustomDataExportQuery.pm [new file with mode: 0644]
SL/DB/Manager/CustomDataExportQueryParameter.pm [new file with mode: 0644]
SL/DB/Manager/Customer.pm
SL/DB/Manager/DeliveryOrder.pm
SL/DB/Manager/Employee.pm
SL/DB/Manager/EmployeeProjectInvoices.pm [new file with mode: 0644]
SL/DB/Manager/File.pm [new file with mode: 0644]
SL/DB/Manager/FileFullText.pm [new file with mode: 0644]
SL/DB/Manager/Greeting.pm [new file with mode: 0644]
SL/DB/Manager/Inventory.pm [new file with mode: 0644]
SL/DB/Manager/Letter.pm [new file with mode: 0644]
SL/DB/Manager/MakeModel.pm [new file with mode: 0644]
SL/DB/Manager/MebilMapping.pm [new file with mode: 0644]
SL/DB/Manager/Order.pm
SL/DB/Manager/OrderItem.pm
SL/DB/Manager/Part.pm
SL/DB/Manager/PartClassification.pm [new file with mode: 0644]
SL/DB/Manager/PartCustomerPrice.pm [new file with mode: 0644]
SL/DB/Manager/PartsGroup.pm [new file with mode: 0644]
SL/DB/Manager/PartsPriceHistory.pm [new file with mode: 0644]
SL/DB/Manager/PaymentTerm.pm
SL/DB/Manager/PriceFactor.pm [new file with mode: 0644]
SL/DB/Manager/PriceRule.pm
SL/DB/Manager/PriceRuleItem.pm
SL/DB/Manager/Pricegroup.pm
SL/DB/Manager/ReconciliationLink.pm
SL/DB/Manager/RecordTemplate.pm [new file with mode: 0644]
SL/DB/Manager/RecordTemplateItem.pm [new file with mode: 0644]
SL/DB/Manager/Shop.pm [new file with mode: 0644]
SL/DB/Manager/ShopImage.pm [new file with mode: 0644]
SL/DB/Manager/ShopOrder.pm [new file with mode: 0644]
SL/DB/Manager/ShopOrderItem.pm [new file with mode: 0644]
SL/DB/Manager/ShopPart.pm [new file with mode: 0644]
SL/DB/Manager/Stocktaking.pm [new file with mode: 0644]
SL/DB/Manager/TimeRecording.pm [new file with mode: 0644]
SL/DB/Manager/TimeRecordingArticle.pm [new file with mode: 0644]
SL/DB/Manager/UserPreference.pm [new file with mode: 0644]
SL/DB/Manager/Vendor.pm
SL/DB/MebilMapping.pm [new file with mode: 0644]
SL/DB/MetaSetup/AdditionalBillingAddress.pm [new file with mode: 0644]
SL/DB/MetaSetup/ApGl.pm [new file with mode: 0644]
SL/DB/MetaSetup/Assembly.pm
SL/DB/MetaSetup/AssortmentItem.pm [new file with mode: 0644]
SL/DB/MetaSetup/AuthClient.pm
SL/DB/MetaSetup/AuthClientGroup.pm
SL/DB/MetaSetup/AuthClientUser.pm
SL/DB/MetaSetup/AuthGroup.pm
SL/DB/MetaSetup/AuthGroupRight.pm
SL/DB/MetaSetup/AuthMasterRight.pm [new file with mode: 0644]
SL/DB/MetaSetup/AuthSchemaInfo.pm [new file with mode: 0644]
SL/DB/MetaSetup/AuthSession.pm [new file with mode: 0644]
SL/DB/MetaSetup/AuthSessionContent.pm [new file with mode: 0644]
SL/DB/MetaSetup/AuthUser.pm
SL/DB/MetaSetup/AuthUserConfig.pm
SL/DB/MetaSetup/AuthUserGroup.pm
SL/DB/MetaSetup/BackgroundJob.pm
SL/DB/MetaSetup/BankAccount.pm
SL/DB/MetaSetup/BankTransaction.pm
SL/DB/MetaSetup/BankTransactionAccTrans.pm [new file with mode: 0644]
SL/DB/MetaSetup/Buchungsgruppe.pm
SL/DB/MetaSetup/Business.pm
SL/DB/MetaSetup/Chart.pm
SL/DB/MetaSetup/Contact.pm
SL/DB/MetaSetup/ContactDepartment.pm [new file with mode: 0644]
SL/DB/MetaSetup/ContactTitle.pm [new file with mode: 0644]
SL/DB/MetaSetup/CsvImportReport.pm
SL/DB/MetaSetup/CustomDataExportQuery.pm [new file with mode: 0644]
SL/DB/MetaSetup/CustomDataExportQueryParameter.pm [new file with mode: 0644]
SL/DB/MetaSetup/CustomVariableConfig.pm
SL/DB/MetaSetup/Customer.pm
SL/DB/MetaSetup/Default.pm
SL/DB/MetaSetup/DeliveryOrder.pm
SL/DB/MetaSetup/DeliveryOrderItem.pm
SL/DB/MetaSetup/Dunning.pm
SL/DB/MetaSetup/DunningConfig.pm
SL/DB/MetaSetup/EmailJournalAttachment.pm
SL/DB/MetaSetup/EmployeeProjectInvoices.pm [new file with mode: 0644]
SL/DB/MetaSetup/File.pm [new file with mode: 0644]
SL/DB/MetaSetup/FileFullText.pm [new file with mode: 0644]
SL/DB/MetaSetup/FollowUp.pm
SL/DB/MetaSetup/GLTransaction.pm
SL/DB/MetaSetup/Greeting.pm [new file with mode: 0644]
SL/DB/MetaSetup/Inventory.pm
SL/DB/MetaSetup/Invoice.pm
SL/DB/MetaSetup/InvoiceItem.pm
SL/DB/MetaSetup/Language.pm
SL/DB/MetaSetup/Letter.pm
SL/DB/MetaSetup/LetterDraft.pm
SL/DB/MetaSetup/MakeModel.pm
SL/DB/MetaSetup/MebilMapping.pm [new file with mode: 0644]
SL/DB/MetaSetup/Order.pm
SL/DB/MetaSetup/OrderItem.pm
SL/DB/MetaSetup/Part.pm
SL/DB/MetaSetup/PartClassification.pm [new file with mode: 0644]
SL/DB/MetaSetup/PartCustomerPrice.pm [new file with mode: 0644]
SL/DB/MetaSetup/PartsGroup.pm
SL/DB/MetaSetup/PartsPriceHistory.pm [new file with mode: 0644]
SL/DB/MetaSetup/PaymentTerm.pm
SL/DB/MetaSetup/PeriodicInvoicesConfig.pm
SL/DB/MetaSetup/Price.pm
SL/DB/MetaSetup/Pricegroup.pm
SL/DB/MetaSetup/PurchaseInvoice.pm
SL/DB/MetaSetup/RecordTemplate.pm [new file with mode: 0644]
SL/DB/MetaSetup/RecordTemplateItem.pm [new file with mode: 0644]
SL/DB/MetaSetup/RequirementSpecItem.pm
SL/DB/MetaSetup/SepaExportItem.pm
SL/DB/MetaSetup/Shipto.pm
SL/DB/MetaSetup/Shop.pm [new file with mode: 0644]
SL/DB/MetaSetup/ShopImage.pm [new file with mode: 0644]
SL/DB/MetaSetup/ShopOrder.pm [new file with mode: 0644]
SL/DB/MetaSetup/ShopOrderItem.pm [new file with mode: 0644]
SL/DB/MetaSetup/ShopPart.pm [new file with mode: 0644]
SL/DB/MetaSetup/Stocktaking.pm [new file with mode: 0644]
SL/DB/MetaSetup/Tax.pm
SL/DB/MetaSetup/TimeRecording.pm [new file with mode: 0644]
SL/DB/MetaSetup/TimeRecordingArticle.pm [new file with mode: 0644]
SL/DB/MetaSetup/UserPreference.pm [new file with mode: 0644]
SL/DB/MetaSetup/Vendor.pm
SL/DB/Note.pm
SL/DB/Object.pm
SL/DB/Object/Hooks.pm
SL/DB/Order.pm
SL/DB/OrderItem.pm
SL/DB/Part.pm
SL/DB/PartClassification.pm [new file with mode: 0644]
SL/DB/PartCustomerPrice.pm [new file with mode: 0644]
SL/DB/PartsGroup.pm
SL/DB/PartsPriceHistory.pm [new file with mode: 0644]
SL/DB/PaymentTerm.pm
SL/DB/PeriodicInvoicesConfig.pm
SL/DB/Price.pm
SL/DB/PriceFactor.pm
SL/DB/PriceRule.pm
SL/DB/PriceRuleItem.pm
SL/DB/Pricegroup.pm
SL/DB/Printer.pm
SL/DB/Project.pm
SL/DB/PurchaseInvoice.pm
SL/DB/RecordTemplate.pm [new file with mode: 0644]
SL/DB/RecordTemplateItem.pm [new file with mode: 0644]
SL/DB/RequirementSpec.pm
SL/DB/SepaExportItem.pm
SL/DB/Shipto.pm
SL/DB/Shop.pm [new file with mode: 0644]
SL/DB/ShopImage.pm [new file with mode: 0644]
SL/DB/ShopOrder.pm [new file with mode: 0644]
SL/DB/ShopOrderItem.pm [new file with mode: 0644]
SL/DB/ShopPart.pm [new file with mode: 0644]
SL/DB/Stocktaking.pm [new file with mode: 0644]
SL/DB/TimeRecording.pm [new file with mode: 0644]
SL/DB/TimeRecordingArticle.pm [new file with mode: 0644]
SL/DB/Unit.pm
SL/DB/UserPreference.pm [new file with mode: 0644]
SL/DB/VC.pm
SL/DB/Vendor.pm
SL/DBConnect.pm
SL/DBConnect/Cache.pm
SL/DBUpgrade2.pm
SL/DBUpgrade2/Base.pm
SL/DBUtils.pm
SL/DN.pm
SL/DO.pm
SL/DefaultManager.pm [new file with mode: 0644]
SL/DefaultManager/German.pm [new file with mode: 0644]
SL/DefaultManager/Swiss.pm [new file with mode: 0644]
SL/Dev/ALL.pm [new file with mode: 0644]
SL/Dev/CustomerVendor.pm [new file with mode: 0644]
SL/Dev/File.pm [new file with mode: 0644]
SL/Dev/Inventory.pm [new file with mode: 0644]
SL/Dev/Part.pm [new file with mode: 0644]
SL/Dev/Payment.pm [new file with mode: 0644]
SL/Dev/Record.pm [new file with mode: 0644]
SL/Dev/Shop.pm [new file with mode: 0644]
SL/Dev/TimeRecording.pm [new file with mode: 0644]
SL/Dispatcher.pm
SL/Dispatcher/AuthHandler/User.pm
SL/Drafts.pm [deleted file]
SL/FU.pm
SL/File.pm [new file with mode: 0644]
SL/File/Backend.pm [new file with mode: 0644]
SL/File/Backend/Filesystem.pm [new file with mode: 0644]
SL/File/Backend/Webdav.pm [new file with mode: 0644]
SL/File/Object.pm [new file with mode: 0644]
SL/Form.pm
SL/GL.pm
SL/GenericTranslations.pm
SL/GoBD.pm [new file with mode: 0644]
SL/HTML/Util.pm
SL/Helper/CreatePDF.pm
SL/Helper/Csv.pm
SL/Helper/Csv/Dispatcher.pm
SL/Helper/DateTime.pm
SL/Helper/File.pm [new file with mode: 0644]
SL/Helper/GlAttachments.pm [new file with mode: 0644]
SL/Helper/ISO3166.pm [new file with mode: 0644]
SL/Helper/ISO4217.pm [new file with mode: 0644]
SL/Helper/Inventory.pm [new file with mode: 0644]
SL/Helper/Inventory/Allocation.pm [new file with mode: 0644]
SL/Helper/MT940.pm [deleted file]
SL/Helper/MassPrintCreatePDF.pm [new file with mode: 0644]
SL/Helper/Number.pm [new file with mode: 0644]
SL/Helper/Object.pm [new file with mode: 0644]
SL/Helper/PrintOptions.pm [new file with mode: 0644]
SL/Helper/QrBill.pm [new file with mode: 0644]
SL/Helper/ShippedQty.pm [new file with mode: 0644]
SL/Helper/UNECERecommendation20.pm [new file with mode: 0644]
SL/Helper/UserPreferences.pm [new file with mode: 0644]
SL/Helper/UserPreferences/DisplayPreferences.pm [new file with mode: 0644]
SL/Helper/UserPreferences/DisplayableName.pm [new file with mode: 0644]
SL/Helper/UserPreferences/PartPickerSearch.pm [new file with mode: 0644]
SL/Helper/UserPreferences/PositionsScrollbar.pm [new file with mode: 0644]
SL/Helper/UserPreferences/TimeRecording.pm [new file with mode: 0644]
SL/Helper/UserPreferences/UpdatePositions.pm [new file with mode: 0644]
SL/IC.pm
SL/IO.pm
SL/IR.pm
SL/IS.pm
SL/Inifile.pm
SL/InstallationCheck.pm
SL/InstanceConfiguration.pm
SL/JSON.pm
SL/LXDebug.pm
SL/Layout/ActionBar.pm [new file with mode: 0644]
SL/Layout/ActionBar/Action.pm [new file with mode: 0644]
SL/Layout/ActionBar/ComboBox.pm [new file with mode: 0644]
SL/Layout/ActionBar/Link.pm [new file with mode: 0644]
SL/Layout/ActionBar/Separator.pm [new file with mode: 0644]
SL/Layout/ActionBar/Submit.pm [new file with mode: 0644]
SL/Layout/Base.pm
SL/Layout/Classic.pm
SL/Layout/Content.pm [new file with mode: 0644]
SL/Layout/CssMenu.pm
SL/Layout/DHTMLMenu.pm [new file with mode: 0644]
SL/Layout/Dispatcher.pm
SL/Layout/Javascript.pm
SL/Layout/Material.pm [new file with mode: 0644]
SL/Layout/MaterialMenu.pm [new file with mode: 0644]
SL/Layout/MaterialStyle.pm [new file with mode: 0644]
SL/Layout/MenuLeft.pm
SL/Layout/MobileLogin.pm [new file with mode: 0644]
SL/Layout/None.pm
SL/Layout/Split.pm [new file with mode: 0644]
SL/Layout/Top.pm
SL/Layout/V3.pm
SL/Letter.pm [deleted file]
SL/LiquidityProjection.pm
SL/Locale.pm
SL/Locale/String.pm
SL/LxOfficeConf.pm
SL/MT940.pm [new file with mode: 0644]
SL/Mailer.pm
SL/Menu.pm
SL/MoreCommon.pm
SL/Notes.pm
SL/Num2text.pm
SL/OE.pm
SL/PE.pm [deleted file]
SL/Presenter.pm
SL/Presenter/ALL.pm [new file with mode: 0644]
SL/Presenter/BankAccount.pm
SL/Presenter/Chart.pm
SL/Presenter/CustomerVendor.pm
SL/Presenter/DeliveryOrder.pm
SL/Presenter/Dunning.pm [new file with mode: 0644]
SL/Presenter/EmailJournal.pm [new file with mode: 0644]
SL/Presenter/EscapedText.pm
SL/Presenter/FileObject.pm [new file with mode: 0644]
SL/Presenter/GL.pm
SL/Presenter/Invoice.pm
SL/Presenter/JavascriptMenu.pm [new file with mode: 0644]
SL/Presenter/Letter.pm [new file with mode: 0644]
SL/Presenter/MaterialComponents.pm [new file with mode: 0644]
SL/Presenter/Order.pm
SL/Presenter/Part.pm
SL/Presenter/Project.pm
SL/Presenter/Record.pm
SL/Presenter/RequirementSpec.pm
SL/Presenter/RequirementSpecItem.pm
SL/Presenter/RequirementSpecTextBlock.pm
SL/Presenter/SepaExport.pm
SL/Presenter/ShopOrder.pm [new file with mode: 0644]
SL/Presenter/Simple.pm [new file with mode: 0644]
SL/Presenter/Tag.pm
SL/Presenter/Text.pm
SL/Presenter/WebdavObject.pm [new file with mode: 0644]
SL/PriceSource.pm
SL/PriceSource/ALL.pm
SL/PriceSource/Base.pm
SL/PriceSource/Customer.pm
SL/PriceSource/CustomerPrice.pm [new file with mode: 0644]
SL/PriceSource/Discount.pm
SL/PriceSource/MasterData.pm
SL/PriceSource/Price.pm
SL/PriceSource/Pricegroup.pm
SL/PriceSource/Vendor.pm
SL/RC.pm
SL/RP.pm
SL/RecordLinks.pm
SL/ReportGenerator.pm
SL/Request.pm
SL/SEPA.pm
SL/SEPA/XML.pm
SL/SessionFile.pm
SL/Shop.pm [new file with mode: 0644]
SL/ShopConnector/ALL.pm [new file with mode: 0644]
SL/ShopConnector/Base.pm [new file with mode: 0644]
SL/ShopConnector/Shopware.pm [new file with mode: 0644]
SL/ShopConnector/Shopware6.pm [new file with mode: 0644]
SL/ShopConnector/WooCommerce.pm [new file with mode: 0644]
SL/System/Process.pm
SL/System/ResourceCache.pm [new file with mode: 0644]
SL/System/TaskServer.pm
SL/TODO.pm
SL/Taxkeys.pm
SL/Template.pm
SL/Template/LaTeX.pm
SL/Template/OpenDocument.pm
SL/Template/Plugin/KiviLatex.pm
SL/Template/Plugin/L.pm
SL/Template/Plugin/LxERP.pm
SL/Template/Plugin/P.pm
SL/Template/XML.pm [deleted file]
SL/TransNumber.pm
SL/USTVA.pm
SL/User.pm
SL/Util.pm
SL/VATIDNr.pm [new file with mode: 0644]
SL/VK.pm
SL/Version.pm [new file with mode: 0644]
SL/WH.pm
SL/Webdav.pm
SL/Webdav/File.pm
SL/Webdav/Object.pm
SL/X.pm
SL/X/Base.pm [new file with mode: 0644]
SL/YAML.pm [new file with mode: 0644]
SL/ZUGFeRD.pm [new file with mode: 0644]
VERSION
bin/mozilla/.htaccess
bin/mozilla/acctranscorrections.pl
bin/mozilla/am.pl
bin/mozilla/amtemplates.pl
bin/mozilla/ap.pl
bin/mozilla/ar.pl
bin/mozilla/arap.pl [deleted file]
bin/mozilla/bp.pl
bin/mozilla/ca.pl
bin/mozilla/common.pl
bin/mozilla/cp.pl
bin/mozilla/ct.pl
bin/mozilla/datev.pl
bin/mozilla/dn.pl
bin/mozilla/do.pl
bin/mozilla/drafts.pl [deleted file]
bin/mozilla/fu.pl
bin/mozilla/generictranslations.pl
bin/mozilla/gl.pl
bin/mozilla/ic.pl
bin/mozilla/installationcheck.pl
bin/mozilla/invoice_io.pl [deleted file]
bin/mozilla/io.pl
bin/mozilla/ir.pl
bin/mozilla/is.pl
bin/mozilla/letter.pl [deleted file]
bin/mozilla/login.pl
bin/mozilla/oe.pl
bin/mozilla/pe.pl [deleted file]
bin/mozilla/rc.pl
bin/mozilla/reportgenerator.pl
bin/mozilla/rp.pl
bin/mozilla/sepa.pl
bin/mozilla/todo.pl
bin/mozilla/ustva.pl
bin/mozilla/vk.pl
bin/mozilla/wh.pl
config/.htaccess
config/kivitendo.conf.default
css/bwa.css
css/common.css
css/kivitendo/dhtmlsuite/menu-bar.css
css/kivitendo/dhtmlsuite/menu-item.css
css/kivitendo/frame_header/header.css
css/kivitendo/jquery-ui.custom.css
css/kivitendo/main.css
css/kivitendo/menu.css
css/lx-office-erp/dhtmlsuite/menu-bar.css
css/lx-office-erp/frame_header/header.css
css/lx-office-erp/jquery-ui.custom.css
css/lx-office-erp/list_accounts.css
css/lx-office-erp/main.css
css/lx-office-erp/menu.css
css/material/icons.css [new file with mode: 0644]
css/material/icons.ttf [new file with mode: 0644]
css/material/materialize.css [new symlink]
css/material/materialize.min.css [new file with mode: 0644]
css/tooltipster.css
css/ui-lightness/jquery-ui-1.10.3.custom.css
css/webshop.css [new file with mode: 0644]
debian/.dummy [new file with mode: 0644]
debian/mkivitendo.changelog [new file with mode: 0644]
debian/mkivitendo.control [new file with mode: 0644]
debian/mkivitendo.cp [new file with mode: 0755]
debian/mkivitendo.postinst [new file with mode: 0755]
debian/mkivitendo.prepare [new file with mode: 0755]
dispatcher.fpl
dispatcher.pl
doc/DATEV-2015/EXTF_Anlag-Buchungen.csv [new file with mode: 0644]
doc/DATEV-2015/EXTF_Anlag-Filialen.csv [new file with mode: 0644]
doc/DATEV-2015/EXTF_Buchungsstapel.csv [new file with mode: 0644]
doc/DATEV-2015/EXTF_Buchungstextkonstanten.csv [new file with mode: 0644]
doc/DATEV-2015/EXTF_Div-Adressen.csv [new file with mode: 0644]
doc/DATEV-2015/EXTF_Sachkontobeschriftungen.csv [new file with mode: 0644]
doc/DATEV-2015/EXTF_Stammdaten-Deb-Kred.csv [new file with mode: 0644]
doc/DATEV-2015/EXTF_Textschluessel.csv [new file with mode: 0644]
doc/DATEV-2015/EXTF_Wiederkehrende-Buchungen.csv [new file with mode: 0644]
doc/DATEV-2015/EXTF_Zahlungsbedingungen.csv [new file with mode: 0644]
doc/UPGRADE
doc/changelog
doc/copyright
doc/dokumentation.xml
doc/html/ch01.html
doc/html/ch02.html
doc/html/ch02s02.html
doc/html/ch02s03.html
doc/html/ch02s04.html
doc/html/ch02s05.html
doc/html/ch02s06.html
doc/html/ch02s07.html
doc/html/ch02s08.html
doc/html/ch02s09.html
doc/html/ch02s10.html
doc/html/ch02s11.html
doc/html/ch02s12.html
doc/html/ch02s13.html
doc/html/ch02s14.html
doc/html/ch02s15.html
doc/html/ch02s16.html
doc/html/ch02s17.html
doc/html/ch02s18.html
doc/html/ch02s19.html [new file with mode: 0644]
doc/html/ch02s20.html [new file with mode: 0644]
doc/html/ch02s21.html [new file with mode: 0644]
doc/html/ch03.html
doc/html/ch03s02.html
doc/html/ch03s03.html
doc/html/ch03s04.html
doc/html/ch03s05.html
doc/html/ch03s06.html [new file with mode: 0644]
doc/html/ch03s07.html [new file with mode: 0644]
doc/html/ch03s08.html [new file with mode: 0644]
doc/html/ch03s09.html [new file with mode: 0644]
doc/html/ch03s10.html [new file with mode: 0644]
doc/html/ch04.html
doc/html/ch04s02.html
doc/html/ch04s03.html
doc/html/ch04s04.html
doc/html/ch04s05.html
doc/html/ch04s06.html
doc/html/ch04s07.html
doc/html/ch04s08.html [new file with mode: 0644]
doc/html/images/DMS-Allgemeine-Dokumentenanhaenge.png [new file with mode: 0644]
doc/html/images/DMS-Anhaenge-hochladen.png [new file with mode: 0644]
doc/html/images/DMS-Anhaenge.png [new file with mode: 0644]
doc/html/images/DMS-ClientConfig.png [new file with mode: 0644]
doc/html/images/DMS-Dokumente-Scanner.png [new file with mode: 0644]
doc/html/images/DMS-Dokumente.png [new file with mode: 0644]
doc/html/images/DMS-Overview.png [new file with mode: 0644]
doc/html/images/Einzahlungsschein_Makro.png [new file with mode: 0644]
doc/html/images/Shop_Artikel.png [new file with mode: 0644]
doc/html/images/Shop_Artikel_Listing.png [new file with mode: 0644]
doc/html/images/Shop_Bestell.png [new file with mode: 0644]
doc/html/images/Shop_Config.png [new file with mode: 0644]
doc/html/images/Shop_Listing.png [new file with mode: 0644]
doc/html/index.html
doc/html/system/docbook-xsl/images/tip.png [new file with mode: 0644]
doc/images/DMS-Allgemeine-Dokumentenanhaenge.png [new file with mode: 0644]
doc/images/DMS-Anhaenge-hochladen.png [new file with mode: 0644]
doc/images/DMS-Anhaenge.png [new file with mode: 0644]
doc/images/DMS-ClientConfig.png [new file with mode: 0644]
doc/images/DMS-Dokumente-Scanner.png [new file with mode: 0644]
doc/images/DMS-Dokumente.png [new file with mode: 0644]
doc/images/DMS-Overview.png [new file with mode: 0644]
doc/images/Einzahlungsschein_Makro.png [new file with mode: 0644]
doc/images/Shop_Artikel.png [new file with mode: 0644]
doc/images/Shop_Artikel_Listing.png [new file with mode: 0644]
doc/images/Shop_Bestell.png [new file with mode: 0644]
doc/images/Shop_Config.png [new file with mode: 0644]
doc/images/Shop_Listing.png [new file with mode: 0644]
doc/kivitendo-Dokumentation.pdf
doc/modules/LICENSE.CGI-Ajax [deleted file]
doc/modules/LICENSE.Email-Address [deleted file]
doc/modules/LICENSE.List-MoreUtils [deleted file]
doc/modules/LICENSE.List-UtilsBy [deleted file]
doc/modules/README.CGI-Ajax [deleted file]
doc/modules/README.File-Slurp [deleted file]
doc/modules/README.List-UtilsBy [deleted file]
doc/modules/README.PDF-Table
doc/modules/README.Sort-Naturally [deleted file]
doc/modules/README.YAML [deleted file]
doc/release_management.txt
image/CH-Kreuz_7mm.png [new file with mode: 0644]
image/collapse.svg [new file with mode: 0644]
image/collapse3.gif [new file with mode: 0644]
image/edit-entry.png [new file with mode: 0644]
image/expand.svg [new file with mode: 0644]
image/glass14x14.png [new file with mode: 0644]
image/gruener_punkt.gif [new file with mode: 0644]
image/icons/16x16/wtg.png [new file with mode: 0644]
image/icons/svg/gobd.svg [new file with mode: 0644]
image/icons/svg/mail_journal.svg [new file with mode: 0644]
image/kivitendo_mir.png [new file with mode: 0644]
image/kivitendo_xmas.png [new file with mode: 0644]
image/rotate_cw.svg [new file with mode: 0644]
image/roter_punkt.gif [new file with mode: 0644]
image/search.svg [new file with mode: 0644]
image/select-down.png [new file with mode: 0644]
js/autocomplete_chart.js
js/autocomplete_customer.js [deleted file]
js/autocomplete_part.js [deleted file]
js/autocomplete_project.js
js/calculate_qty.js
js/ckeditor/CHANGES.md [new file with mode: 0644]
js/ckeditor/LICENSE.md [new file with mode: 0644]
js/ckeditor/README.md [new file with mode: 0644]
js/ckeditor/adapters/jquery.js
js/ckeditor/build-config.js
js/ckeditor/ckeditor.js
js/ckeditor/config.js
js/ckeditor/contents.css
js/ckeditor/lang/de.js
js/ckeditor/lang/en.js
js/ckeditor/plugins/about/dialogs/about.js [deleted file]
js/ckeditor/plugins/about/dialogs/hidpi/logo_ckeditor.png [deleted file]
js/ckeditor/plugins/about/dialogs/logo_ckeditor.png [deleted file]
js/ckeditor/plugins/clipboard/dialogs/paste.js [deleted file]
js/ckeditor/plugins/codemirror/css/codemirror.min.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/js/beautify.min.js [new file with mode: 0644]
js/ckeditor/plugins/codemirror/js/codemirror.addons.min.js [new file with mode: 0644]
js/ckeditor/plugins/codemirror/js/codemirror.addons.search.min.js [new file with mode: 0644]
js/ckeditor/plugins/codemirror/js/codemirror.min.js [new file with mode: 0644]
js/ckeditor/plugins/codemirror/js/codemirror.mode.bbcode.min.js [new file with mode: 0644]
js/ckeditor/plugins/codemirror/js/codemirror.mode.bbcodemixed.min.js [new file with mode: 0644]
js/ckeditor/plugins/codemirror/js/codemirror.mode.htmlmixed.min.js [new file with mode: 0644]
js/ckeditor/plugins/codemirror/js/codemirror.mode.javascript.min.js [new file with mode: 0644]
js/ckeditor/plugins/codemirror/js/codemirror.mode.php.min.js [new file with mode: 0644]
js/ckeditor/plugins/codemirror/js/codemirror.mode.twig.min.js [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/3024-day.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/3024-night.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/abcdef.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/ambiance-mobile.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/ambiance.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/base16-dark.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/base16-light.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/bespin.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/blackboard.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/cobalt.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/colorforth.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/dracula.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/duotone-dark.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/duotone-light.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/eclipse.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/elegant.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/erlang-dark.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/hopscotch.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/icecoder.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/isotope.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/lesser-dark.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/liquibyte.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/material.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/mbo.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/mdn-like.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/midnight.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/monokai.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/neat.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/neo.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/night.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/panda-syntax.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/paraiso-dark.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/paraiso-light.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/pastel-on-dark.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/railscasts.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/rubyblue.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/seti.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/solarized.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/the-matrix.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/tomorrow-night-bright.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/tomorrow-night-eighties.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/ttcn.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/twilight.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/vibrant-ink.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/xq-dark.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/xq-light.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/yeti.css [new file with mode: 0644]
js/ckeditor/plugins/codemirror/theme/zenburn.css [new file with mode: 0644]
js/ckeditor/plugins/dialog/dialogDefinition.js
js/ckeditor/plugins/fakeobjects/images/spacer.gif [deleted file]
js/ckeditor/plugins/icons.png
js/ckeditor/plugins/icons_hidpi.png
js/ckeditor/plugins/inline_resize/plugin.js [new file with mode: 0644]
js/ckeditor/plugins/link/dialogs/anchor.js
js/ckeditor/plugins/link/dialogs/link.js
js/ckeditor/plugins/link/images/anchor.png
js/ckeditor/plugins/link/images/hidpi/anchor.png
js/ckeditor/plugins/sourcedialog/dialogs/sourcedialog.js [new file with mode: 0644]
js/ckeditor/samples/css/samples.css [new file with mode: 0644]
js/ckeditor/samples/img/github-top.png [new file with mode: 0644]
js/ckeditor/samples/img/header-bg.png [new file with mode: 0644]
js/ckeditor/samples/img/header-separator.png [new file with mode: 0644]
js/ckeditor/samples/img/logo.png [new file with mode: 0644]
js/ckeditor/samples/img/navigation-tip.png [new file with mode: 0644]
js/ckeditor/samples/index.html [new file with mode: 0644]
js/ckeditor/samples/js/sample.js [new file with mode: 0644]
js/ckeditor/samples/js/sf.js [new file with mode: 0644]
js/ckeditor/samples/old/ajax.html [new file with mode: 0644]
js/ckeditor/samples/old/api.html [new file with mode: 0644]
js/ckeditor/samples/old/appendto.html [new file with mode: 0644]
js/ckeditor/samples/old/assets/inlineall/logo.png [new file with mode: 0644]
js/ckeditor/samples/old/assets/outputxhtml/outputxhtml.css [new file with mode: 0644]
js/ckeditor/samples/old/assets/posteddata.php [new file with mode: 0644]
js/ckeditor/samples/old/assets/sample.jpg [new file with mode: 0644]
js/ckeditor/samples/old/assets/uilanguages/languages.js [new file with mode: 0644]
js/ckeditor/samples/old/datafiltering.html [new file with mode: 0644]
js/ckeditor/samples/old/dialog/assets/my_dialog.js [new file with mode: 0644]
js/ckeditor/samples/old/dialog/dialog.html [new file with mode: 0644]
js/ckeditor/samples/old/divreplace.html [new file with mode: 0644]
js/ckeditor/samples/old/enterkey/enterkey.html [new file with mode: 0644]
js/ckeditor/samples/old/index.html [new file with mode: 0644]
js/ckeditor/samples/old/inlineall.html [new file with mode: 0644]
js/ckeditor/samples/old/inlinebycode.html [new file with mode: 0644]
js/ckeditor/samples/old/inlinetextarea.html [new file with mode: 0644]
js/ckeditor/samples/old/jquery.html [new file with mode: 0644]
js/ckeditor/samples/old/readonly.html [new file with mode: 0644]
js/ckeditor/samples/old/replacebyclass.html [new file with mode: 0644]
js/ckeditor/samples/old/replacebycode.html [new file with mode: 0644]
js/ckeditor/samples/old/sample.css [new file with mode: 0644]
js/ckeditor/samples/old/sample.js [new file with mode: 0644]
js/ckeditor/samples/old/sample_posteddata.php [new file with mode: 0644]
js/ckeditor/samples/old/sourcedialog/sourcedialog.html [new file with mode: 0644]
js/ckeditor/samples/old/tabindex.html [new file with mode: 0644]
js/ckeditor/samples/old/toolbar/toolbar.html [new file with mode: 0644]
js/ckeditor/samples/old/uicolor.html [new file with mode: 0644]
js/ckeditor/samples/old/uilanguages.html [new file with mode: 0644]
js/ckeditor/samples/old/wysiwygarea/fullpage.html [new file with mode: 0644]
js/ckeditor/samples/old/xhtmlstyle.html [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/css/fontello.css [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/font/LICENSE.txt [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/font/config.json [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/font/fontello.eot [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/font/fontello.svg [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/font/fontello.ttf [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/font/fontello.woff [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/index.html [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/js/abstracttoolbarmodifier.js [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/js/fulltoolbareditor.js [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/js/toolbarmodifier.js [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/js/toolbartextmodifier.js [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/lib/codemirror/LICENSE [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/lib/codemirror/codemirror.css [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/lib/codemirror/codemirror.js [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/lib/codemirror/javascript.js [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/lib/codemirror/neo.css [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/lib/codemirror/show-hint.css [new file with mode: 0644]
js/ckeditor/samples/toolbarconfigurator/lib/codemirror/show-hint.js [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/dialog.css [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/dialog_ie.css [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/dialog_ie8.css [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/dialog_iequirks.css [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/editor.css [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/editor_gecko.css [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/editor_ie.css [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/editor_ie8.css [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/editor_iequirks.css [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/icons.png [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/icons_hidpi.png [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/images/arrow.png [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/images/close.png [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/images/hidpi/close.png [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/images/hidpi/lock-open.png [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/images/hidpi/lock.png [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/images/hidpi/refresh.png [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/images/lock-open.png [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/images/lock.png [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/images/refresh.png [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/images/spinner.gif [new file with mode: 0644]
js/ckeditor/skins/moono-lisa/readme.md [new file with mode: 0644]
js/ckeditor/skins/moono/dialog.css [deleted file]
js/ckeditor/skins/moono/dialog_ie.css [deleted file]
js/ckeditor/skins/moono/dialog_ie7.css [deleted file]
js/ckeditor/skins/moono/dialog_ie8.css [deleted file]
js/ckeditor/skins/moono/dialog_iequirks.css [deleted file]
js/ckeditor/skins/moono/dialog_opera.css [deleted file]
js/ckeditor/skins/moono/editor.css [deleted file]
js/ckeditor/skins/moono/editor_gecko.css [deleted file]
js/ckeditor/skins/moono/editor_ie.css [deleted file]
js/ckeditor/skins/moono/editor_ie7.css [deleted file]
js/ckeditor/skins/moono/editor_ie8.css [deleted file]
js/ckeditor/skins/moono/editor_iequirks.css [deleted file]
js/ckeditor/skins/moono/icons.png [deleted file]
js/ckeditor/skins/moono/icons_hidpi.png [deleted file]
js/ckeditor/skins/moono/images/arrow.png [deleted file]
js/ckeditor/skins/moono/images/close.png [deleted file]
js/ckeditor/skins/moono/images/hidpi/close.png [deleted file]
js/ckeditor/skins/moono/images/hidpi/lock-open.png [deleted file]
js/ckeditor/skins/moono/images/hidpi/lock.png [deleted file]
js/ckeditor/skins/moono/images/hidpi/refresh.png [deleted file]
js/ckeditor/skins/moono/images/lock-open.png [deleted file]
js/ckeditor/skins/moono/images/lock.png [deleted file]
js/ckeditor/skins/moono/images/refresh.png [deleted file]
js/ckeditor/skins/moono/readme.md [deleted file]
js/ckeditor/styles.js
js/client_js.js
js/common.js
js/customer_or_vendor_selection.js [deleted file]
js/edit_periodic_invoices_config.js
js/follow_up.js
js/glquicksearch.js [deleted file]
js/jquery.jstree.js
js/kivi.AP.js [new file with mode: 0644]
js/kivi.AR.js [new file with mode: 0644]
js/kivi.ActionBar.js [new file with mode: 0644]
js/kivi.BankTransaction.js [new file with mode: 0644]
js/kivi.CustomDataExportDesigner.js [new file with mode: 0644]
js/kivi.CustomerVendor.js
js/kivi.CustomerVendorTurnover.js [new file with mode: 0644]
js/kivi.DeliveryOrder.js [new file with mode: 0644]
js/kivi.Draft.js [new file with mode: 0644]
js/kivi.Dunning.js [new file with mode: 0644]
js/kivi.File.js [new file with mode: 0644]
js/kivi.FileDB.js [new file with mode: 0644]
js/kivi.GL.js [new file with mode: 0644]
js/kivi.GoBD.js [new file with mode: 0644]
js/kivi.ImageUpload.js [new file with mode: 0644]
js/kivi.Inventory.js [new file with mode: 0644]
js/kivi.LeftMenu.js [new file with mode: 0644]
js/kivi.Letter.js [new file with mode: 0644]
js/kivi.MassDeliveryOrderPrint.js [new file with mode: 0644]
js/kivi.MassInvoiceCreatePrint.js
js/kivi.Materialize.js [new file with mode: 0644]
js/kivi.Order.js [new file with mode: 0644]
js/kivi.Part.js [new file with mode: 0644]
js/kivi.PriceRule.js
js/kivi.QuickSearch.js [new file with mode: 0644]
js/kivi.RecordTemplate.js [new file with mode: 0644]
js/kivi.SalesPurchase.js
js/kivi.Shop.js [new file with mode: 0644]
js/kivi.ShopOrder.js [new file with mode: 0644]
js/kivi.ShopPart.js [new file with mode: 0644]
js/kivi.TimeRecording.js [new file with mode: 0644]
js/kivi.Validator.js [new file with mode: 0644]
js/kivi.js
js/locale/de.js
js/locale/en.js [new file with mode: 0644]
js/materialize.js [new symlink]
js/materialize/materialize.min.js [new file with mode: 0644]
js/part_selection.js [deleted file]
js/quicksearch_input.js [deleted file]
js/requirement_spec.js
js/show_history.js
js/switchmenuframe.js [deleted file]
js/t/kivi/format_amount.js
js/t/kivi/parse_amount.js
js/t/kivi/parse_format_date.js
js/t/kivi/parse_format_time.js [new file with mode: 0644]
locale/.htaccess
locale/de/COPYING
locale/de/Num2text
locale/de/all
locale/de/more/all [new file with mode: 0644]
locale/de/special_chars
locale/en/COPYING
locale/en/all
locale/en/special_chars
menus/.dummy [new file with mode: 0644]
menus/admin/.dummy [new file with mode: 0644]
menus/mobile/.dummy [new file with mode: 0644]
menus/mobile/00-erp.yaml [new file with mode: 0644]
menus/user/.dummy [new file with mode: 0644]
menus/user/00-erp.yaml
menus/user/10-custom-data-export.yaml [new file with mode: 0644]
menus/user/10-order-controller.yaml [new file with mode: 0644]
menus/user/10-time-recording.yaml [new file with mode: 0644]
menus/user/20-invoice-for-advance-payment.yaml [new file with mode: 0644]
modules/fallback/Daemon/Generic.pm [deleted file]
modules/fallback/Daemon/Generic/Event.pm [deleted file]
modules/fallback/Daemon/Generic/While1.pm [deleted file]
modules/fallback/DateTime/Event/Cron.pm [deleted file]
modules/fallback/DateTime/Set.pm [deleted file]
modules/fallback/DateTime/Span.pm [deleted file]
modules/fallback/DateTime/SpanSet.pm [deleted file]
modules/fallback/Email/Address.pm [deleted file]
modules/fallback/Exception/Lite.pm [deleted file]
modules/fallback/Exception/Lite.pod [deleted file]
modules/fallback/File/Flock.pm [deleted file]
modules/fallback/File/Slurp.pm [deleted file]
modules/fallback/List/MoreUtils.pm [deleted file]
modules/fallback/List/UtilsBy.pm [deleted file]
modules/fallback/Regexp/IPv6.pm [deleted file]
modules/fallback/Set/Crontab.pm [deleted file]
modules/fallback/Set/Infinite.pm [deleted file]
modules/fallback/Set/Infinite/Arithmetic.pm [deleted file]
modules/fallback/Set/Infinite/Basic.pm [deleted file]
modules/fallback/Set/Infinite/_recurrence.pm [deleted file]
modules/fallback/Sort/Naturally.pm [deleted file]
modules/fallback/String/ShellQuote.pm [deleted file]
modules/fallback/parent.pm [deleted file]
modules/override/Algorithm/CheckDigits/M97_001.pm [new file with mode: 0644]
modules/override/Devel/REPL/Plugin/AutoloadModules.pm [deleted file]
modules/override/Devel/REPL/Plugin/PermanentHistory.pm [deleted file]
modules/override/PDF/Table.pm [changed mode: 0644->0755]
modules/override/Rose/DBx/Cache/Anywhere.pm [deleted file]
modules/override/YAML.pm [deleted file]
modules/override/YAML/Any.pm [deleted file]
modules/override/YAML/Dumper.pm [deleted file]
modules/override/YAML/Dumper/Base.pm [deleted file]
modules/override/YAML/Error.pm [deleted file]
modules/override/YAML/Loader.pm [deleted file]
modules/override/YAML/Loader/Base.pm [deleted file]
modules/override/YAML/Marshall.pm [deleted file]
modules/override/YAML/Mo.pm [deleted file]
modules/override/YAML/Node.pm [deleted file]
modules/override/YAML/Tag.pm [deleted file]
modules/override/YAML/Types.pm [deleted file]
scripts/.htaccess
scripts/boot/systemd/kivitendo-task-server.service
scripts/build_doc.sh
scripts/console
scripts/create_tags_file.pl [deleted file]
scripts/csv-import-from-shell.sh
scripts/dbconnect.pl
scripts/dbupgrade2_tool.pl
scripts/find-use.pl
scripts/generate_client_js_actions.pl
scripts/generate_client_js_actions.tpl
scripts/image_maps.pl
scripts/installation_check.pl
scripts/locales.pl
scripts/make_docs.pl [changed mode: 0644->0755]
scripts/pl2tmpl.pl [deleted file]
scripts/rose_auto_create_model.pl
scripts/spawn_oo.pl [deleted file]
scripts/sync_files_from_backend.pl [new file with mode: 0755]
scripts/task_server.pl
scripts/templ2t8.pl [deleted file]
sql/Pg-upgrade2-auth/add_api_token.sql
sql/Pg-upgrade2-auth/add_batch_printing_to_full_access.sql
sql/Pg-upgrade2-auth/add_master_rights.sql
sql/Pg-upgrade2-auth/all_drafts_edit.pl [new file with mode: 0644]
sql/Pg-upgrade2-auth/assembly_edit_right.pl [new file with mode: 0644]
sql/Pg-upgrade2-auth/auth_schema_normalization_1.pl
sql/Pg-upgrade2-auth/bank_transaction_rights.pl
sql/Pg-upgrade2-auth/client_task_server.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/clients.pl
sql/Pg-upgrade2-auth/clients_webdav.pl
sql/Pg-upgrade2-auth/convert_columns_to_html_for_sending_html_emails.pl [new file with mode: 0644]
sql/Pg-upgrade2-auth/custom_data_export_rights.pl [new file with mode: 0644]
sql/Pg-upgrade2-auth/customer_vendor_record_extra_tab_rights.pl [new file with mode: 0644]
sql/Pg-upgrade2-auth/delivery_plan_rights.pl
sql/Pg-upgrade2-auth/delivery_process_value.pl
sql/Pg-upgrade2-auth/details_and_report_of_parts.pl
sql/Pg-upgrade2-auth/foreign_key_constraints_on_delete.pl
sql/Pg-upgrade2-auth/mail_journal_rights.pl [new file with mode: 0644]
sql/Pg-upgrade2-auth/master_rights_position_gaps.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/master_rights_positions_fix.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/move_shop_part_edit_right.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/other_file_sources.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/other_file_sources2.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/password_hashing.sql
sql/Pg-upgrade2-auth/productivity_rights.pl
sql/Pg-upgrade2-auth/purchase_letter_rights.pl [new file with mode: 0644]
sql/Pg-upgrade2-auth/record_links_rights.pl [new file with mode: 0644]
sql/Pg-upgrade2-auth/release_3_0_0.sql
sql/Pg-upgrade2-auth/release_3_2_0.sql
sql/Pg-upgrade2-auth/release_3_3_0.sql
sql/Pg-upgrade2-auth/release_3_4_0.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/release_3_5_0.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/release_3_5_1.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/release_3_5_2.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/release_3_5_3.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/release_3_5_4.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/release_3_5_5.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/release_3_5_6.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/release_3_5_6_1.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/release_3_5_7.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/release_3_5_8.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/release_3_6_0.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/release_3_6_1.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/remove_insecurely_hashed_passwords.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/remove_menustyle_v4.sql
sql/Pg-upgrade2-auth/remove_menustyle_xml.sql
sql/Pg-upgrade2-auth/rename_general_ledger_rights.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/requirement_spec_rights.pl
sql/Pg-upgrade2-auth/right_assortment_edit.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/right_develop.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/right_productivity_as_category.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/right_purchase_all_edit.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/right_time_recording.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/rights_for_showing_ar_and_ap_transactions.pl
sql/Pg-upgrade2-auth/rights_for_viewing_project_specific_invoices.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/rights_sales_purchase_edit_prices.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/rights_time_recording_show_edit_all.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/rights_view_docs.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/sales_letter_rights.pl
sql/Pg-upgrade2-auth/session_content_auto_restore.sql
sql/Pg-upgrade2-auth/session_content_primary_key.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/split_transaction_rights.pl [new file with mode: 0644]
sql/Pg-upgrade2-auth/webshop_api_rights.pl [new file with mode: 0644]
sql/Pg-upgrade2-auth/webshop_api_rights_2.pl [new file with mode: 0644]
sql/Pg-upgrade2/SKR04-3804-addition.pl
sql/Pg-upgrade2/acc_trans_without_oid.sql
sql/Pg-upgrade2/accounts_tax_office_leonberg.sql [new file with mode: 0644]
sql/Pg-upgrade2/add_emloyee_project_assignment_for_viewing_invoices.sql [new file with mode: 0644]
sql/Pg-upgrade2/add_gl_imported.sql [new file with mode: 0644]
sql/Pg-upgrade2/add_gl_transaction_description.sql [new file with mode: 0644]
sql/Pg-upgrade2/add_node_id_to_background_jobs.sql [new file with mode: 0644]
sql/Pg-upgrade2/add_parts_price_history.sql [new file with mode: 0644]
sql/Pg-upgrade2/add_parts_price_history2.sql [new file with mode: 0644]
sql/Pg-upgrade2/add_record_templates_transaction_description.sql [new file with mode: 0644]
sql/Pg-upgrade2/add_stocktaking_preselects_client_config_default.sql [new file with mode: 0644]
sql/Pg-upgrade2/add_stocktaking_qty_threshold_client_config_default.sql [new file with mode: 0644]
sql/Pg-upgrade2/add_test_mode_to_csv_import_report.sql [new file with mode: 0644]
sql/Pg-upgrade2/add_transfer_doc_interval.sql [new file with mode: 0644]
sql/Pg-upgrade2/add_warehouse_for_assembly.sql [new file with mode: 0644]
sql/Pg-upgrade2/alter_default_shipped_qty.sql [new file with mode: 0644]
sql/Pg-upgrade2/alter_record_template_tables.sql [new file with mode: 0644]
sql/Pg-upgrade2/ap_gl.sql [new file with mode: 0644]
sql/Pg-upgrade2/ap_set_payment_term_from_vendor.sql [new file with mode: 0644]
sql/Pg-upgrade2/ar_add_qrbill_without_amount.sql [new file with mode: 0644]
sql/Pg-upgrade2/assembly_parts_foreign_key.sql [new file with mode: 0644]
sql/Pg-upgrade2/assembly_parts_foreign_key2.sql [new file with mode: 0644]
sql/Pg-upgrade2/assembly_position.sql [new file with mode: 0644]
sql/Pg-upgrade2/assortment.sql [new file with mode: 0644]
sql/Pg-upgrade2/assortment_charge.sql [new file with mode: 0644]
sql/Pg-upgrade2/auto_delete_reconciliation_links_on_acc_trans_deletion.pl [new file with mode: 0644]
sql/Pg-upgrade2/auto_delete_sepa_export_items_on_ap_ar_deletion.pl [new file with mode: 0644]
sql/Pg-upgrade2/background_job_change_create_periodic_invoices_to_daily.pl [deleted file]
sql/Pg-upgrade2/background_job_change_create_periodic_invoices_to_daily.sql [new file with mode: 0644]
sql/Pg-upgrade2/background_jobs_3.pl [deleted file]
sql/Pg-upgrade2/background_jobs_3.sql [new file with mode: 0644]
sql/Pg-upgrade2/background_jobs_clean_auth_sessions.pl [deleted file]
sql/Pg-upgrade2/background_jobs_clean_auth_sessions.sql [new file with mode: 0644]
sql/Pg-upgrade2/bank_account_flag_for_zugferd_usage.sql [new file with mode: 0644]
sql/Pg-upgrade2/bank_account_informations_for_swiss_qrbill.sql [new file with mode: 0644]
sql/Pg-upgrade2/bank_accounts_unique_chart_constraint.sql
sql/Pg-upgrade2/bank_transaction_acc_trans_remove_wrong_primary_key.sql [new file with mode: 0644]
sql/Pg-upgrade2/bank_transactions_check_constraint_invoice_amount.sql [new file with mode: 0644]
sql/Pg-upgrade2/bank_transactions_nuke_trailing_spaces_in_purpose.sql [new file with mode: 0644]
sql/Pg-upgrade2/bank_transactions_type.sql [new file with mode: 0644]
sql/Pg-upgrade2/bank_transactions_type2.sql [new file with mode: 0644]
sql/Pg-upgrade2/bankaccounts_reconciliation.sql
sql/Pg-upgrade2/bankaccounts_sortkey_and_obsolete.sql
sql/Pg-upgrade2/change_warehouse_client_config_default.sql [new file with mode: 0644]
sql/Pg-upgrade2/chart_pos_er.sql [new file with mode: 0644]
sql/Pg-upgrade2/charts_without_taxkey.pl
sql/Pg-upgrade2/check_bin_belongs_to_wh_trigger.sql
sql/Pg-upgrade2/clean_tax_18_19.pl [new file with mode: 0644]
sql/Pg-upgrade2/contact_departments_own_table.sql [new file with mode: 0644]
sql/Pg-upgrade2/contact_titles_own_table.sql [new file with mode: 0644]
sql/Pg-upgrade2/contacts_add_main_contact.pl [new file with mode: 0644]
sql/Pg-upgrade2/convert_columns_to_html_for_sending_html_emails.pl [new file with mode: 0644]
sql/Pg-upgrade2/convert_columns_to_html_for_sending_html_emails2.pl [new file with mode: 0644]
sql/Pg-upgrade2/convert_drafts_to_record_templates.pl [new file with mode: 0644]
sql/Pg-upgrade2/convert_real_qty.sql [new file with mode: 0644]
sql/Pg-upgrade2/create_part_customerprices.sql [new file with mode: 0644]
sql/Pg-upgrade2/create_part_if_not_found.sql [new file with mode: 0644]
sql/Pg-upgrade2/create_record_template_tables.sql [new file with mode: 0644]
sql/Pg-upgrade2/csv_import_reports_add_numheaders.sql
sql/Pg-upgrade2/csv_mt940_add_profile.sql [new file with mode: 0644]
sql/Pg-upgrade2/custom_data_export.sql [new file with mode: 0644]
sql/Pg-upgrade2/custom_data_export_default_values_for_parameters.sql [new file with mode: 0644]
sql/Pg-upgrade2/custom_variable_partsgroups.sql
sql/Pg-upgrade2/custom_variables_add_edit_position.sql [new file with mode: 0644]
sql/Pg-upgrade2/custom_variables_convert_width_height_to_pixels.pl [new file with mode: 0644]
sql/Pg-upgrade2/customer_add_commercial_court.sql [new file with mode: 0644]
sql/Pg-upgrade2/customer_add_fields.sql [new file with mode: 0644]
sql/Pg-upgrade2/customer_add_generic_mail_delivery.sql [new file with mode: 0644]
sql/Pg-upgrade2/customer_add_postal_invoice.sql [new file with mode: 0644]
sql/Pg-upgrade2/customer_additional_billing_addresses.sql [new file with mode: 0644]
sql/Pg-upgrade2/customer_create_zugferd_invoices.sql [new file with mode: 0644]
sql/Pg-upgrade2/customer_klass_rename_to_pricegroup_id_and_foreign_key.sql [new file with mode: 0644]
sql/Pg-upgrade2/customer_orderlock.sql [new file with mode: 0644]
sql/Pg-upgrade2/customer_remove_empty_additional_billing_addresses.sql [new file with mode: 0644]
sql/Pg-upgrade2/customer_vendor_add_natural_person.sql [new file with mode: 0644]
sql/Pg-upgrade2/customer_vendor_routing_id.sql [new file with mode: 0644]
sql/Pg-upgrade2/customer_vendor_shipto_add_gln.sql [new file with mode: 0644]
sql/Pg-upgrade2/cvars_remove_dublicate_entries.pl [new file with mode: 0644]
sql/Pg-upgrade2/datev_export_format.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_add_feature_experimental.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_add_feature_experimental2.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_add_features.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_add_finanzamt_data.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_add_precision.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_add_quick_search_modules.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_add_rnd_accno_ids.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_advance_payment_clearing_chart_id.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_advance_payment_transfer_charts.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_bcc_to_login.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_contact_departments_use_textfield.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_contact_titles_use_textfield.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_create_qrbill_data.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_create_zugferd_data.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_customer_vendor_ustid_taxnummer_unique.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_delivery_date_interval.pl [new file with mode: 0644]
sql/Pg-upgrade2/defaults_delivery_orders_check_stocked.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_doc_email_attachment.pl [new file with mode: 0644]
sql/Pg-upgrade2/defaults_drop_delivery_plan_calculate_transferred_do.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_filemanagement_remove_doc_database.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_invoice_mail_priority.pl [new file with mode: 0644]
sql/Pg-upgrade2/defaults_invoice_prevent_browser_back.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_invoice_warn_no_delivery_order.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_order_controller.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_order_warn_duplicate_parts.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_order_warn_no_cusordnumber.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_order_warn_no_deliverydate.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_partsgroup_required.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_posting_records_add.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_posting_records_default_false.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_print_interpolate_variables_in_positions.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_produce_assembly_transfer_service.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_qrbill_variants.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_req_delivery_date.pl [new file with mode: 0644]
sql/Pg-upgrade2/defaults_sales_purchase_record_numbers_changeable.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_set_dunning_creator.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_show_longdescription_select_item.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_split_address.pl [new file with mode: 0644]
sql/Pg-upgrade2/defaults_transfer_settings.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_vc_greetings_use_textfield.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_view_record_links.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_workflow_po_ap_chart_id.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_year_end_charts.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_zugferd_test_mode.sql [new file with mode: 0644]
sql/Pg-upgrade2/delete_cvars_on_trans_deletion_add_shipto.sql [new file with mode: 0644]
sql/Pg-upgrade2/delete_from_generic_translations_on_language_deletion.pl [new file with mode: 0644]
sql/Pg-upgrade2/delete_translations_on_delivery_term_delete.sql
sql/Pg-upgrade2/delete_translations_on_payment_term_delete.sql
sql/Pg-upgrade2/delete_translations_on_tax_delete.sql
sql/Pg-upgrade2/delete_warehouse_for_assembly.sql [new file with mode: 0644]
sql/Pg-upgrade2/delete_wrong_charts_for_taxkeys.pl [new file with mode: 0644]
sql/Pg-upgrade2/delete_wrong_charts_for_taxkeys_04.pl [new file with mode: 0644]
sql/Pg-upgrade2/delivery_orders.sql
sql/Pg-upgrade2/delivery_terms.sql
sql/Pg-upgrade2/deliveryorder_transnumbers.sql [new file with mode: 0644]
sql/Pg-upgrade2/deliveryorder_type.sql [new file with mode: 0644]
sql/Pg-upgrade2/displayable_name_prefs_defaults.sql [new file with mode: 0644]
sql/Pg-upgrade2/drop_gifi_2.sql
sql/Pg-upgrade2/drop_payment_terms_ranking.sql [new file with mode: 0644]
sql/Pg-upgrade2/drop_shipped_qty_config.sql [new file with mode: 0644]
sql/Pg-upgrade2/dunning_config_print_original_invoice.sql [new file with mode: 0644]
sql/Pg-upgrade2/dunning_foreign_key_for_trans_id.sql [new file with mode: 0644]
sql/Pg-upgrade2/dunning_original_invoice_printed.sql [new file with mode: 0644]
sql/Pg-upgrade2/email_journal_attachments_add_fileid.sql [new file with mode: 0644]
sql/Pg-upgrade2/emmvee_background_jobs_2.pl [deleted file]
sql/Pg-upgrade2/emmvee_background_jobs_2.sql [new file with mode: 0644]
sql/Pg-upgrade2/employee_drop_columns.sql
sql/Pg-upgrade2/erzeugnisnummern.pl
sql/Pg-upgrade2/eur_bwa_category_views.sql [new file with mode: 0644]
sql/Pg-upgrade2/exchangerate_in_oe.sql [new file with mode: 0644]
sql/Pg-upgrade2/file_full_texts.sql [new file with mode: 0644]
sql/Pg-upgrade2/file_storage_dunning_documents.sql [new file with mode: 0644]
sql/Pg-upgrade2/file_storage_dunning_invoice.sql [new file with mode: 0644]
sql/Pg-upgrade2/file_storage_partial_invoices.sql [new file with mode: 0644]
sql/Pg-upgrade2/file_storage_project.sql [new file with mode: 0644]
sql/Pg-upgrade2/file_storage_type_dunning_orig_invoice.sql [new file with mode: 0644]
sql/Pg-upgrade2/file_storage_type_letter.sql [new file with mode: 0644]
sql/Pg-upgrade2/filemanagement_feature.sql [new file with mode: 0644]
sql/Pg-upgrade2/files.sql [new file with mode: 0644]
sql/Pg-upgrade2/files_add_variant.sql [new file with mode: 0644]
sql/Pg-upgrade2/first_aggregator.sql
sql/Pg-upgrade2/full_texts_background_job.sql [new file with mode: 0644]
sql/Pg-upgrade2/get_shipped_qty_config.sql [new file with mode: 0644]
sql/Pg-upgrade2/gl_add_deliverydate.sql [new file with mode: 0644]
sql/Pg-upgrade2/greetings_own_table.sql [new file with mode: 0644]
sql/Pg-upgrade2/inventory_fix_shippingdate_assemblies.sql [new file with mode: 0644]
sql/Pg-upgrade2/inventory_itime_parts_id_index.sql [new file with mode: 0644]
sql/Pg-upgrade2/inventory_parts_id_index.sql [new file with mode: 0644]
sql/Pg-upgrade2/inventory_shippingdate_not_null.sql [new file with mode: 0644]
sql/Pg-upgrade2/invoice_positions.pl
sql/Pg-upgrade2/invoices_amount_paid_not_null.sql
sql/Pg-upgrade2/konjunkturpaket_2020.sql [new file with mode: 0644]
sql/Pg-upgrade2/konjunkturpaket_2020_SKR03-korrekturen.sql [new file with mode: 0644]
sql/Pg-upgrade2/konjunkturpaket_2020_SKR03.sql [new file with mode: 0644]
sql/Pg-upgrade2/konjunkturpaket_2020_SKR04-korrekturen.sql [new file with mode: 0644]
sql/Pg-upgrade2/konjunkturpaket_2020_SKR04.sql [new file with mode: 0644]
sql/Pg-upgrade2/language_obsolete.sql [new file with mode: 0644]
sql/Pg-upgrade2/letter_cleanup.sql [new file with mode: 0644]
sql/Pg-upgrade2/letter_date_type.sql
sql/Pg-upgrade2/letter_vendorletter.sql [new file with mode: 0644]
sql/Pg-upgrade2/link_requirement_spec_to_orders_created_from_quotations_created_from_requirement_spec.sql [new file with mode: 0644]
sql/Pg-upgrade2/makemodel_add_vendor_foreign_key.sql [new file with mode: 0644]
sql/Pg-upgrade2/mebil_v1.sql [new file with mode: 0644]
sql/Pg-upgrade2/new_chart_1593_1495.sql [new file with mode: 0644]
sql/Pg-upgrade2/new_chart_3260_1711.sql [new file with mode: 0644]
sql/Pg-upgrade2/new_chart_3272_1718.sql [new file with mode: 0644]
sql/Pg-upgrade2/oe_ar_ap_delivery_orders_edit_notes_as_html.pl
sql/Pg-upgrade2/oe_customer_vendor_fkeys.sql
sql/Pg-upgrade2/orderitems_delivery_order_items_positions.pl
sql/Pg-upgrade2/orderitems_optional.sql [new file with mode: 0644]
sql/Pg-upgrade2/part_classification_report_separate.sql [new file with mode: 0644]
sql/Pg-upgrade2/part_classifications.sql [new file with mode: 0644]
sql/Pg-upgrade2/part_type_enum.sql [new file with mode: 0644]
sql/Pg-upgrade2/parts_remove_unneeded_fields.sql [new file with mode: 0644]
sql/Pg-upgrade2/partsgroup_sortkey_obsolete.sql [new file with mode: 0644]
sql/Pg-upgrade2/payment_terms_for_invoices.sql [new file with mode: 0644]
sql/Pg-upgrade2/payment_terms_obsolete.sql [new file with mode: 0644]
sql/Pg-upgrade2/periodic_invoices_background_job.pl [deleted file]
sql/Pg-upgrade2/periodic_invoices_background_job.sql [new file with mode: 0644]
sql/Pg-upgrade2/periodic_invoices_first_billing_date.sql
sql/Pg-upgrade2/periodic_invoices_order_value_periodicity2.sql [new file with mode: 0644]
sql/Pg-upgrade2/periodic_invoices_send_email.sql [new file with mode: 0644]
sql/Pg-upgrade2/price_rules.sql
sql/Pg-upgrade2/price_source_client_config.sql
sql/Pg-upgrade2/pricegroup_sortkey_obsolete.sql [new file with mode: 0644]
sql/Pg-upgrade2/prices_delete_cascade.pl [new file with mode: 0644]
sql/Pg-upgrade2/prices_unique.sql [new file with mode: 0644]
sql/Pg-upgrade2/project_defaults.sql [new file with mode: 0644]
sql/Pg-upgrade2/project_mtime_trigger.sql [new file with mode: 0644]
sql/Pg-upgrade2/re_add_sepa_export_items_foreign_keys.sql [new file with mode: 0644]
sql/Pg-upgrade2/receivable_payable_default_accounts.sql [new file with mode: 0644]
sql/Pg-upgrade2/record_links_bt_acc_trans.pl [new file with mode: 0644]
sql/Pg-upgrade2/record_links_dunning_post_delete_trigger.sql [new file with mode: 0644]
sql/Pg-upgrade2/record_links_post_delete_triggers_gl.sql [new file with mode: 0644]
sql/Pg-upgrade2/record_links_remove_to_quotation.pl [new file with mode: 0644]
sql/Pg-upgrade2/record_template_payment_id.sql [new file with mode: 0644]
sql/Pg-upgrade2/recorditem_active_dicount_source.sql
sql/Pg-upgrade2/recorditem_active_price_source.sql
sql/Pg-upgrade2/release_3_4_0.sql [new file with mode: 0644]
sql/Pg-upgrade2/release_3_4_1.sql [new file with mode: 0644]
sql/Pg-upgrade2/release_3_5_0.sql [new file with mode: 0644]
sql/Pg-upgrade2/release_3_5_1.sql [new file with mode: 0644]
sql/Pg-upgrade2/release_3_5_2.sql [new file with mode: 0644]
sql/Pg-upgrade2/release_3_5_3.sql [new file with mode: 0644]
sql/Pg-upgrade2/release_3_5_4.sql [new file with mode: 0644]
sql/Pg-upgrade2/release_3_5_5.sql [new file with mode: 0644]
sql/Pg-upgrade2/release_3_5_6.sql [new file with mode: 0644]
sql/Pg-upgrade2/release_3_5_6_1.sql [new file with mode: 0644]
sql/Pg-upgrade2/release_3_5_7.sql [new file with mode: 0644]
sql/Pg-upgrade2/release_3_5_8.sql [new file with mode: 0644]
sql/Pg-upgrade2/release_3_6_0.sql [new file with mode: 0644]
sql/Pg-upgrade2/release_3_6_1.sql [new file with mode: 0644]
sql/Pg-upgrade2/remove_alternate_from_parts.sql [new file with mode: 0644]
sql/Pg-upgrade2/remove_comma_aggregate_functions.sql [new file with mode: 0644]
sql/Pg-upgrade2/remove_double_tax_entries_skr04.pl [new file with mode: 0644]
sql/Pg-upgrade2/remove_oids.sql [new file with mode: 0644]
sql/Pg-upgrade2/remove_redundant_customer_vendor_delete_triggers.sql
sql/Pg-upgrade2/remove_redundant_cvar_delete_triggers.sql
sql/Pg-upgrade2/remove_taxkey_15_17_skr04.sql [new file with mode: 0644]
sql/Pg-upgrade2/requirement_spec_edit_html.pl
sql/Pg-upgrade2/requirement_spec_items_price_factor.sql [new file with mode: 0644]
sql/Pg-upgrade2/sales_quotation_order_probability_expected_billing_date.sql
sql/Pg-upgrade2/self_test_background_job.pl [deleted file]
sql/Pg-upgrade2/self_test_background_job.sql [new file with mode: 0644]
sql/Pg-upgrade2/sepa_export_items.sql [new file with mode: 0644]
sql/Pg-upgrade2/sepa_recommended_execution_date.sql [new file with mode: 0644]
sql/Pg-upgrade2/sepa_reference_add_vc_vc_id.sql [new file with mode: 0644]
sql/Pg-upgrade2/shop_orders.sql [new file with mode: 0644]
sql/Pg-upgrade2/shop_orders_add_active_pricesource.sql [new file with mode: 0644]
sql/Pg-upgrade2/shop_orders_update_1.sql [new file with mode: 0644]
sql/Pg-upgrade2/shop_orders_update_2.sql [new file with mode: 0644]
sql/Pg-upgrade2/shop_orders_update_3.sql [new file with mode: 0644]
sql/Pg-upgrade2/shop_orders_update_4.sql [new file with mode: 0644]
sql/Pg-upgrade2/shop_parts.sql [new file with mode: 0644]
sql/Pg-upgrade2/shopimages.sql [new file with mode: 0644]
sql/Pg-upgrade2/shopimages_2.sql [new file with mode: 0644]
sql/Pg-upgrade2/shopimages_3.sql [new file with mode: 0644]
sql/Pg-upgrade2/shops.sql [new file with mode: 0644]
sql/Pg-upgrade2/shops_1.sql [new file with mode: 0644]
sql/Pg-upgrade2/shops_2.sql [new file with mode: 0644]
sql/Pg-upgrade2/shops_3.sql [new file with mode: 0644]
sql/Pg-upgrade2/shops_4.sql [new file with mode: 0644]
sql/Pg-upgrade2/shops_5.sql [new file with mode: 0644]
sql/Pg-upgrade2/shops_6.sql [new file with mode: 0644]
sql/Pg-upgrade2/steuerfilterung.pl
sql/Pg-upgrade2/stocktakings.sql [new file with mode: 0644]
sql/Pg-upgrade2/tax_point.sql [new file with mode: 0644]
sql/Pg-upgrade2/tax_point2.sql [new file with mode: 0644]
sql/Pg-upgrade2/tax_removed_taxnumber.sql [new file with mode: 0644]
sql/Pg-upgrade2/tax_reverse_charge.sql [new file with mode: 0644]
sql/Pg-upgrade2/tax_reverse_charge_key_18.sql [new file with mode: 0644]
sql/Pg-upgrade2/tax_reverse_charge_key_19.sql [new file with mode: 0644]
sql/Pg-upgrade2/time_recordings.sql [new file with mode: 0644]
sql/Pg-upgrade2/time_recordings2.sql [new file with mode: 0644]
sql/Pg-upgrade2/time_recordings_add_order.sql [new file with mode: 0644]
sql/Pg-upgrade2/time_recordings_articles.sql [new file with mode: 0644]
sql/Pg-upgrade2/time_recordings_date_duration.sql [new file with mode: 0644]
sql/Pg-upgrade2/time_recordings_remove_type.sql [new file with mode: 0644]
sql/Pg-upgrade2/transfer_out_sales_invoice.sql
sql/Pg-upgrade2/transfer_out_serial_charge_number.sql [new file with mode: 0644]
sql/Pg-upgrade2/transfer_type_assembled.sql [new file with mode: 0644]
sql/Pg-upgrade2/transfer_type_stocktaking.sql [new file with mode: 0644]
sql/Pg-upgrade2/trigram_extension.sql [new file with mode: 0644]
sql/Pg-upgrade2/trigram_indices.sql [new file with mode: 0644]
sql/Pg-upgrade2/trigram_indices_webshop.sql [new file with mode: 0644]
sql/Pg-upgrade2/use_html_in_letter.pl [new file with mode: 0644]
sql/Pg-upgrade2/user_preferences.sql [new file with mode: 0644]
sql/Swiss-German-chart.sql [deleted file]
sql/Switzerland-deutsch-MWST-2014-chart.sql [new file with mode: 0644]
sql/Switzerland-deutsch-Verein-2017-chart.sql [new file with mode: 0644]
sql/Switzerland-deutsch-ohneMWST-2014-chart.sql [new file with mode: 0644]
sql/lx-office.sql
t/.htaccess
t/000setup_database.t
t/001compile.t
t/002goodperl.t
t/003safesys.t
t/006spellcheck.t
t/Support/Files.pm
t/Support/TestSetup.pm
t/ar/ar.t [new file with mode: 0644]
t/auth/evaluate_rights_ary.t [new file with mode: 0644]
t/background_job/convert_time_recordings.t [new file with mode: 0644]
t/background_job/create_periodic_invoices.t
t/background_job/known_jobs.t [deleted file]
t/bank/bank_transactions.t [new file with mode: 0644]
t/controllers/base/render.t
t/controllers/csvimport/artransactions.t [new file with mode: 0644]
t/controllers/csvimport/customervendor.t [new file with mode: 0644]
t/controllers/csvimport/delivery_orders.t [new file with mode: 0644]
t/controllers/csvimport/inventory.t [new file with mode: 0644]
t/controllers/csvimport/parts.t [new file with mode: 0644]
t/controllers/financial_controlling/sales_order_with_periodic_invoices_config.t
t/controllers/financial_overview/sales_orders.t
t/controllers/helpers/parse_filter.t
t/controllers/project/project_linked_records.t [new file with mode: 0644]
t/cti/call_link.t
t/datev/datev_format_2018.t [new file with mode: 0644]
t/datev/encoding.t [new file with mode: 0644]
t/datev/invoices.t [new file with mode: 0644]
t/db/delivery_order.t [new file with mode: 0644]
t/db/order.t [new file with mode: 0644]
t/db/time_recordig.t [new file with mode: 0644]
t/db_helper/attr_duration.t
t/db_helper/convert_invoice.t
t/db_helper/payment.t
t/db_helper/price_tax_calculator.t
t/db_helper/record_links.t
t/db_helper/with_transaction.t [new file with mode: 0644]
t/file/filesystem.t [new file with mode: 0644]
t/gl/gl.t [new file with mode: 0644]
t/helper/attr.t
t/helper/csv.t
t/helper/datetime.t
t/helper/number.t [new file with mode: 0644]
t/helper/object.t [new file with mode: 0644]
t/helper/shipped_qty.t [new file with mode: 0644]
t/helper/trim.t [new file with mode: 0644]
t/helper/user_preferencess.t [new file with mode: 0644]
t/menu/parse_access_string.t [new file with mode: 0644]
t/part/assembly.t [new file with mode: 0644]
t/part/assortment.t [new file with mode: 0644]
t/part/stock.t [new file with mode: 0644]
t/pay_posting_import/datev.csv [new file with mode: 0644]
t/pay_posting_import/datev_import.t [new file with mode: 0644]
t/presenter/base/render.t
t/request/flatten.t
t/request/post_multipart.t
t/request/post_multipart_1
t/run.sh [new file with mode: 0755]
t/shop/json_ok.json [new file with mode: 0644]
t/shop/shop_order.t [new file with mode: 0644]
t/shop/shopware.t [new file with mode: 0644]
t/shop/woocommerce.t [new file with mode: 0644]
t/structure/common_errors.t [changed mode: 0644->0755]
t/structure/double_colon_interpolation.t [new file with mode: 0644]
t/structure/instance_conf_method_names.t
t/structure/no_indirect_object_notation.t
t/structure/no_lexicals_in_postif.t
t/tax/tax.t [new file with mode: 0644]
t/template_syntax.t
t/test.pl
t/wh/inventory.t [new file with mode: 0644]
t/wh/journal.t [new file with mode: 0644]
t/wh/transfer.t
t/x/exceptions.t [new file with mode: 0644]
t/year_end/year_end.t [new file with mode: 0644]
templates/.htaccess
templates/mail/self_test/status_mail.txt
templates/mobile_webpages/file/list.html [new file with mode: 0644]
templates/mobile_webpages/file/upload_dialog.html [new file with mode: 0644]
templates/mobile_webpages/generic/error.html [new file with mode: 0644]
templates/mobile_webpages/generic/exception.html [new file with mode: 0644]
templates/mobile_webpages/generic/information.html [new file with mode: 0644]
templates/mobile_webpages/image_upload/local_list.html [new file with mode: 0644]
templates/mobile_webpages/layout/javascript_setup.js [new file with mode: 0644]
templates/mobile_webpages/login/company_logo.html [new file with mode: 0644]
templates/mobile_webpages/login_screen/user_login.html [new file with mode: 0644]
templates/mobile_webpages/menu/menu.html [new file with mode: 0644]
templates/mobile_webpages/test/components.html [new file with mode: 0644]
templates/mobile_webpages/test/modal.html [new file with mode: 0644]
templates/pdf/pdf_a_metadata.xmp [new file with mode: 0644]
templates/print/RB/Readme.tex
templates/print/RB/credit_note.tex
templates/print/RB/deutsch.tex
templates/print/RB/emptyPage.pdf [new file with mode: 0644]
templates/print/RB/english.tex
templates/print/RB/firma/briefkopf.png
templates/print/RB/firma/chf_account.tex [new file with mode: 0644]
templates/print/RB/firma/ident.tex
templates/print/RB/ic_supply.tex [new file with mode: 0644]
templates/print/RB/ic_supply_EN.tex [new file with mode: 0644]
templates/print/RB/insettings.tex
templates/print/RB/invoice.html
templates/print/RB/invoice.tex
templates/print/RB/letter.tex
templates/print/RB/proforma.tex
templates/print/RB/purchase_delivery_order.tex
templates/print/RB/purchase_order.tex
templates/print/RB/request_quotation.tex
templates/print/RB/requirement_spec.tex
templates/print/RB/sales_delivery_order.tex
templates/print/RB/sales_order.html
templates/print/RB/sales_order.tex
templates/print/RB/sales_quotation.html
templates/print/RB/sales_quotation.tex
templates/print/RB/statement.tex
templates/print/RB/zahlungserinnerung.tex
templates/print/RB/zahlungserinnerung_invoice.tex
templates/print/Standard
templates/print/f-tex/bin_list.html [deleted file]
templates/print/f-tex/default.tex [deleted file]
templates/print/f-tex/letter.lco [deleted symlink]
templates/print/f-tex/letter_head.pdf [deleted symlink]
templates/print/f-tex/mydata.tex [deleted symlink]
templates/print/f-tex/mydata.tex.example [deleted file]
templates/print/f-tex/sample.lco [deleted file]
templates/print/f-tex/sample_head.pdf [deleted file]
templates/print/f-tex/statement.html [deleted file]
templates/print/f-tex/translations.tex [deleted file]
templates/print/f-tex/zwischensumme.sty [deleted file]
templates/print/marei/Readme.md [new file with mode: 0644]
templates/print/marei/bin_list.html [new file with mode: 0644]
templates/print/marei/bin_list.tex [new file with mode: 0644]
templates/print/marei/check.tex [new file with mode: 0644]
templates/print/marei/credit_note.tex [new file with mode: 0644]
templates/print/marei/deutsch.tex [new file with mode: 0644]
templates/print/marei/emptyPage.pdf [new file with mode: 0644]
templates/print/marei/english.tex [new file with mode: 0644]
templates/print/marei/final_invoice.tex [new symlink]
templates/print/marei/firma/briefkopf.png [new file with mode: 0644]
templates/print/marei/firma/chf_account.tex [new file with mode: 0644]
templates/print/marei/firma/euro_account.tex [new file with mode: 0644]
templates/print/marei/firma/ident.tex [new file with mode: 0644]
templates/print/marei/firma/kivitendo.png [new file with mode: 0644]
templates/print/marei/firma/steigmann.png [new file with mode: 0644]
templates/print/marei/firma/usd_account.tex [new file with mode: 0644]
templates/print/marei/ic_supply.tex [new file with mode: 0644]
templates/print/marei/ic_supply_EN.tex [new file with mode: 0644]
templates/print/marei/images/draft.png [new file with mode: 0644]
templates/print/marei/images/hintergrund_seite1.png [new file with mode: 0644]
templates/print/marei/images/hintergrund_seite2.png [new file with mode: 0644]
templates/print/marei/images/schachfiguren.jpg [new file with mode: 0644]
templates/print/marei/inheaders.tex [new file with mode: 0644]
templates/print/marei/insettings.tex [new file with mode: 0644]
templates/print/marei/invoice.html [new file with mode: 0644]
templates/print/marei/invoice.tex [new file with mode: 0644]
templates/print/marei/invoice_copy.tex [new symlink]
templates/print/marei/invoice_for_advance_payment.tex [new symlink]
templates/print/marei/kiviletter.sty [new file with mode: 0644]
templates/print/marei/kivitendo.sty [new file with mode: 0644]
templates/print/marei/letter.tex [new file with mode: 0644]
templates/print/marei/pick_list.html [new file with mode: 0644]
templates/print/marei/pick_list.tex [new file with mode: 0644]
templates/print/marei/proforma.tex [new file with mode: 0644]
templates/print/marei/purchase_delivery_order.tex [new file with mode: 0644]
templates/print/marei/purchase_order.html [new file with mode: 0644]
templates/print/marei/purchase_order.tex [new file with mode: 0644]
templates/print/marei/receipt.tex [new file with mode: 0644]
templates/print/marei/request_quotation.html [new file with mode: 0644]
templates/print/marei/request_quotation.tex [new file with mode: 0644]
templates/print/marei/requirement_spec.tex [new file with mode: 0644]
templates/print/marei/sales_delivery_order.tex [new file with mode: 0644]
templates/print/marei/sales_order.html [new file with mode: 0644]
templates/print/marei/sales_order.tex [new file with mode: 0644]
templates/print/marei/sales_quotation.html [new file with mode: 0644]
templates/print/marei/sales_quotation.tex [new file with mode: 0644]
templates/print/marei/statement.html [new file with mode: 0644]
templates/print/marei/statement.tex [new file with mode: 0644]
templates/print/marei/supplier_delivery_order.tex [new symlink]
templates/print/marei/zahlungserinnerung.tex [new file with mode: 0644]
templates/print/marei/zahlungserinnerung_invoice.tex [new file with mode: 0644]
templates/print/rev-odt/credit_note.odt
templates/print/rev-odt/invoice.odt
templates/print/rev-odt/invoice_besr.odt [new file with mode: 0644]
templates/print/rev-odt/invoice_qr.odt [new file with mode: 0644]
templates/print/rev-odt/mahnung.odt [new file with mode: 0644]
templates/print/rev-odt/mahnung_invoice.odt [new file with mode: 0644]
templates/print/rev-odt/proforma.odt
templates/print/rev-odt/readme.txt
templates/print/rev-odt/sales_order.odt
templates/print/rev-odt/sales_order_besr.odt [new file with mode: 0644]
templates/print/rev-odt/sales_quotation.odt
templates/print/rev-odt/zahlungserinnerung.odt [new file with mode: 0644]
templates/webpages/acc_trans/_mini_ledger.html
templates/webpages/acctranscorrections/analyze_filter.html
templates/webpages/acctranscorrections/assistant_for_wrong_taxkeys.html
templates/webpages/admin/adminlogin.html
templates/webpages/admin/create_dataset.html
templates/webpages/admin/edit_client.html
templates/webpages/admin/edit_user.html
templates/webpages/admin/show.html
templates/webpages/am/_units_header_info.html [new file with mode: 0644]
templates/webpages/am/add_unit.html [new file with mode: 0644]
templates/webpages/am/audit_control.html
templates/webpages/am/config.html
templates/webpages/am/confirm_delete_warehouse.html [deleted file]
templates/webpages/am/edit_accounts.html
templates/webpages/am/edit_bins.html [new file with mode: 0644]
templates/webpages/am/edit_price_factor.html [deleted file]
templates/webpages/am/edit_tax.html
templates/webpages/am/edit_templates.html
templates/webpages/am/edit_units.html
templates/webpages/am/edit_warehouse.html
templates/webpages/am/form_footer.html [deleted file]
templates/webpages/am/language_header.html [deleted file]
templates/webpages/am/language_list.html [deleted file]
templates/webpages/am/lead_header.html [deleted file]
templates/webpages/am/lead_list.html [deleted file]
templates/webpages/am/list_account_details.html
templates/webpages/am/list_price_factors.html [deleted file]
templates/webpages/am/list_tax.html
templates/webpages/am/list_warehouses.html
templates/webpages/amcvar/render_inputs.html
templates/webpages/amcvar/render_inputs_block.html
templates/webpages/amcvar/search_filter.html
templates/webpages/ap/ap_transactions_bottom.html [deleted file]
templates/webpages/ap/form_footer.html
templates/webpages/ap/form_header.html
templates/webpages/ap/search.html
templates/webpages/ar/ar_transactions_bottom.html
templates/webpages/ar/ar_transactions_header.html [new file with mode: 0644]
templates/webpages/ar/form_footer.html
templates/webpages/ar/form_header.html
templates/webpages/ar/search.html
templates/webpages/arap/select_project.html [deleted file]
templates/webpages/background_job/form.html
templates/webpages/background_job/list.html
templates/webpages/background_job_history/_filter.html
templates/webpages/background_job_history/list.html
templates/webpages/background_job_history/show.html
templates/webpages/bank_import/import_mt940.html [new file with mode: 0644]
templates/webpages/bank_import/upload_mt940.html [new file with mode: 0644]
templates/webpages/bank_transactions/_filter.html
templates/webpages/bank_transactions/_payment_suggestion.html [new file with mode: 0644]
templates/webpages/bank_transactions/_problems.html [new file with mode: 0644]
templates/webpages/bank_transactions/_template_list.html [new file with mode: 0644]
templates/webpages/bank_transactions/add_list.html
templates/webpages/bank_transactions/assign_invoice.html
templates/webpages/bank_transactions/create_invoice.html
templates/webpages/bank_transactions/filter_drafts.html [deleted file]
templates/webpages/bank_transactions/invoices.html
templates/webpages/bank_transactions/list.html
templates/webpages/bank_transactions/report_bottom.html
templates/webpages/bank_transactions/report_top.html
templates/webpages/bank_transactions/search.html
templates/webpages/bank_transactions/tabs/all.html
templates/webpages/bank_transactions/tabs/automatic.html
templates/webpages/bankaccounts/form.html [deleted file]
templates/webpages/bankaccounts/list.html [deleted file]
templates/webpages/bankimport/form.html [deleted file]
templates/webpages/bp/list_spool.html
templates/webpages/bp/search.html
templates/webpages/buchungsgruppen/form.html
templates/webpages/buchungsgruppen/list.html
templates/webpages/business/form.html [deleted file]
templates/webpages/business/list.html [deleted file]
templates/webpages/ca/list.html
templates/webpages/chart/report_configuration_overview.html [new file with mode: 0644]
templates/webpages/chart/test_page.html
templates/webpages/client_config/_attachments.html [new file with mode: 0644]
templates/webpages/client_config/_datev_check_configuration.html
templates/webpages/client_config/_default_accounts.html
templates/webpages/client_config/_features.html
templates/webpages/client_config/_miscellaneous.html
templates/webpages/client_config/_posting_configuration.html
templates/webpages/client_config/_ranges_of_numbers.html
templates/webpages/client_config/_record_links.html [new file with mode: 0644]
templates/webpages/client_config/_stocktaking.html [new file with mode: 0644]
templates/webpages/client_config/_warehouse.html
templates/webpages/client_config/form.html
templates/webpages/common/_print_dialog.html [new file with mode: 0644]
templates/webpages/common/_send_email_dialog.html [new file with mode: 0644]
templates/webpages/common/_ship_to_dialog.html [new file with mode: 0644]
templates/webpages/common/flash.html
templates/webpages/common/render_cvar_filter_input.html
templates/webpages/common/render_cvar_input.html
templates/webpages/common/search_history.html
templates/webpages/common/select_warehouse_bin.html
templates/webpages/common/show_history.html
templates/webpages/common/show_vc_details.html
templates/webpages/cp/form_footer.html
templates/webpages/cp/form_header.html
templates/webpages/csv_import/_deferred_report.html
templates/webpages/csv_import/_deferred_results.html
templates/webpages/csv_import/_errors.html [deleted file]
templates/webpages/csv_import/_form_artransactions.html [new file with mode: 0644]
templates/webpages/csv_import/_form_delivery_orders.html [new file with mode: 0644]
templates/webpages/csv_import/_form_parts.html
templates/webpages/csv_import/_mapping_item.html [new file with mode: 0644]
templates/webpages/csv_import/_preview.html [deleted file]
templates/webpages/csv_import/_result.html [deleted file]
templates/webpages/csv_import/_results.html [deleted file]
templates/webpages/csv_import/form.html
templates/webpages/csv_import/report.html
templates/webpages/ct/list_names_bottom.html
templates/webpages/ct/search.html
templates/webpages/ct/search_contact.html
templates/webpages/cti/calling.html
templates/webpages/custom_data_export/empty_result_set.html [new file with mode: 0644]
templates/webpages/custom_data_export/export.html [new file with mode: 0644]
templates/webpages/custom_data_export/list.html [new file with mode: 0644]
templates/webpages/custom_data_export_designer/edit.html [new file with mode: 0644]
templates/webpages/custom_data_export_designer/edit_parameters.html [new file with mode: 0644]
templates/webpages/custom_data_export_designer/list.html [new file with mode: 0644]
templates/webpages/custom_variable_config/form.html
templates/webpages/custom_variable_config/list.html
templates/webpages/customer_vendor/form.html
templates/webpages/customer_vendor/get_delivery.html
templates/webpages/customer_vendor/tabs/additional_billing_addresses.html [new file with mode: 0644]
templates/webpages/customer_vendor/tabs/billing.html
templates/webpages/customer_vendor/tabs/contacts.html
templates/webpages/customer_vendor/tabs/custom_variables.html
templates/webpages/customer_vendor/tabs/price_list.html [new file with mode: 0644]
templates/webpages/customer_vendor/tabs/shipto.html
templates/webpages/customer_vendor/tabs/vcnotes.html
templates/webpages/customer_vendor/test_page.html
templates/webpages/customer_vendor_turnover/_list_open_items.html [new file with mode: 0644]
templates/webpages/customer_vendor_turnover/_list_open_orders.html [new file with mode: 0644]
templates/webpages/customer_vendor_turnover/_statistic_tabs.html [new file with mode: 0644]
templates/webpages/customer_vendor_turnover/count_open_items_by_year.html [new file with mode: 0644]
templates/webpages/customer_vendor_turnover/count_turnover.html [new file with mode: 0644]
templates/webpages/customer_vendor_turnover/dun_statistic.html [new file with mode: 0644]
templates/webpages/customer_vendor_turnover/email_statistic.html [new file with mode: 0644]
templates/webpages/customer_vendor_turnover/invoices_statistic.html [new file with mode: 0644]
templates/webpages/customer_vendor_turnover/letter_statistic.html [new file with mode: 0644]
templates/webpages/customer_vendor_turnover/order_statistic.html [new file with mode: 0644]
templates/webpages/customer_vendor_turnover/quotation_statistic.html [new file with mode: 0644]
templates/webpages/customer_vendor_turnover/turnover.html [new file with mode: 0644]
templates/webpages/customer_vendor_turnover/turnover_statistic.html [new file with mode: 0644]
templates/webpages/datev/export.html
templates/webpages/datev/export3.html
templates/webpages/datev/export_bewegungsdaten.html
templates/webpages/datev/export_stammdaten.html [deleted file]
templates/webpages/datev/net_gross_difference.html
templates/webpages/dbupgrade/acc_trans_constraints.html
templates/webpages/dbupgrade/warning.html
templates/webpages/delivery_order/form.html [new file with mode: 0644]
templates/webpages/delivery_order/stock_dialog.html [new file with mode: 0644]
templates/webpages/delivery_order/tabs/_business_info_row.html [new file with mode: 0644]
templates/webpages/delivery_order/tabs/_item_input.html [new file with mode: 0644]
templates/webpages/delivery_order/tabs/_row.html [new file with mode: 0644]
templates/webpages/delivery_order/tabs/_second_row.html [new file with mode: 0644]
templates/webpages/delivery_order/tabs/basic_data.html [new file with mode: 0644]
templates/webpages/delivery_plan/_filter.html
templates/webpages/delivery_plan/report_top.html
templates/webpages/delivery_term/form.html
templates/webpages/delivery_term/list.html
templates/webpages/delivery_value_report/_filter.html
templates/webpages/department/form.html [deleted file]
templates/webpages/department/list.html [deleted file]
templates/webpages/do/form_footer.html
templates/webpages/do/form_header.html
templates/webpages/do/orders_bottom.html
templates/webpages/do/orders_top.html
templates/webpages/do/search.html
templates/webpages/do/stock_in_form.html
templates/webpages/drafts/form.html [new file with mode: 0644]
templates/webpages/drafts/load.html [deleted file]
templates/webpages/drafts/save_new.html [deleted file]
templates/webpages/dunning/add.html
templates/webpages/dunning/edit_config.html
templates/webpages/dunning/search.html
templates/webpages/dunning/set_email.html
templates/webpages/dunning/show_dunning_bottom.html
templates/webpages/dunning/show_dunning_top.html
templates/webpages/dunning/show_invoices.html
templates/webpages/dunning/status.html [new file with mode: 0644]
templates/webpages/email_journal/_filter.html
templates/webpages/email_journal/show.html
templates/webpages/employee/_form.html [deleted file]
templates/webpages/employee/edit.html
templates/webpages/employee/list.html
templates/webpages/file/import_dialog.html [new file with mode: 0644]
templates/webpages/file/list.html [new file with mode: 0644]
templates/webpages/file/rename_dialog.html [new file with mode: 0644]
templates/webpages/file/upload_dialog.html [new file with mode: 0644]
templates/webpages/financial_controlling_report/_filter.html
templates/webpages/fu/add_edit.html
templates/webpages/fu/edit_access_rights.html
templates/webpages/fu/report_bottom.html
templates/webpages/fu/report_top.html
templates/webpages/fu/search.html
templates/webpages/generic/autocomplete.html [deleted file]
templates/webpages/generic/calculate_qty.html
templates/webpages/generic/cov_selection.html [deleted file]
templates/webpages/generic/edit_email.html [deleted file]
templates/webpages/generic/error.html
templates/webpages/generic/exception.html
templates/webpages/generic/multibox.html [deleted file]
templates/webpages/generic/new_item.html
templates/webpages/generic/part_selection.html [deleted file]
templates/webpages/generic/print_options.html
templates/webpages/generic/select_part.html [deleted file]
templates/webpages/generic/set_longdescription.html
templates/webpages/generictranslations/edit_email_strings.html [new file with mode: 0644]
templates/webpages/generictranslations/edit_greetings.html
templates/webpages/generictranslations/edit_sepa_strings.html
templates/webpages/generictranslations/edit_zugferd_notes.html [new file with mode: 0644]
templates/webpages/gl/form_footer.html
templates/webpages/gl/form_header.html
templates/webpages/gl/form_header_chart_balances_js.html [deleted file]
templates/webpages/gl/generate_report_bottom.html
templates/webpages/gl/search.html
templates/webpages/gl/update_tax_accounts.html
templates/webpages/gobd/filter.html [new file with mode: 0644]
templates/webpages/ic/ajax_autocomplete.html [deleted file]
templates/webpages/ic/assembly_row.html [deleted file]
templates/webpages/ic/choice.html [deleted file]
templates/webpages/ic/confirm_price_update.html
templates/webpages/ic/form_footer.html [deleted file]
templates/webpages/ic/form_header.html [deleted file]
templates/webpages/ic/generate_report_bottom.html
templates/webpages/ic/generate_report_top.html
templates/webpages/ic/makemodel.html [deleted file]
templates/webpages/ic/price_row.html [deleted file]
templates/webpages/ic/sales_price_information.html [deleted file]
templates/webpages/ic/search.html
templates/webpages/ic/search_update_prices.html
templates/webpages/ic/tabs/_edit_translations.html [deleted file]
templates/webpages/inventory/_stock.html
templates/webpages/inventory/report_bottom.html [new file with mode: 0644]
templates/webpages/inventory/stocktaking/_already_counted_dialog.html [new file with mode: 0644]
templates/webpages/inventory/stocktaking/_filter.html [new file with mode: 0644]
templates/webpages/inventory/stocktaking/form.html [new file with mode: 0644]
templates/webpages/inventory/stocktaking/full_report_top.html [new file with mode: 0644]
templates/webpages/inventory/stocktaking/report_bottom.html [new file with mode: 0644]
templates/webpages/inventory/warehouse_selection_stock.html
templates/webpages/inventory/warehouse_usage.html [new file with mode: 0644]
templates/webpages/io/select_item.html
templates/webpages/io/ship_to.html [deleted file]
templates/webpages/ir/_payments.html
templates/webpages/ir/form_footer.html
templates/webpages/ir/form_header.html
templates/webpages/is/_payments.html
templates/webpages/is/form_footer.html
templates/webpages/is/form_header.html
templates/webpages/layout/javascript_setup.js
templates/webpages/letter/edit.html
templates/webpages/letter/load_drafts.html
templates/webpages/letter/report_bottom.html
templates/webpages/letter/report_top.html
templates/webpages/letter/search.html
templates/webpages/liquidity_projection/_filter.html
templates/webpages/liquidity_projection/_result.html
templates/webpages/login/company_logo.html
templates/webpages/login_screen/user_login.html
templates/webpages/mass_delivery_order_print/_filter.html [new file with mode: 0644]
templates/webpages/mass_delivery_order_print/_print_status.html [new file with mode: 0644]
templates/webpages/mass_delivery_order_print/list_delivery_orders.html [new file with mode: 0644]
templates/webpages/mass_invoice_create_print_from_do/_create_print_all_step_1.html
templates/webpages/mass_invoice_create_print_from_do/_filter.html
templates/webpages/mass_invoice_create_print_from_do/list_invoices.html
templates/webpages/mass_invoice_create_print_from_do/list_sales_delivery_orders.html
templates/webpages/menu/header.html
templates/webpages/menu/menu.html [deleted file]
templates/webpages/menu/menunew.html [deleted file]
templates/webpages/oe/check_for_direct_delivery.html
templates/webpages/oe/edit_periodic_invoices_config.html
templates/webpages/oe/form_footer.html
templates/webpages/oe/form_header.html
templates/webpages/oe/orders_bottom.html
templates/webpages/oe/orders_top.html
templates/webpages/oe/periodic_invoices_email.txt
templates/webpages/oe/price_sources_dialog.html
templates/webpages/oe/sales_order.html
templates/webpages/oe/search.html
templates/webpages/order/form.html [new file with mode: 0644]
templates/webpages/order/tabs/_business_info_row.html [new file with mode: 0644]
templates/webpages/order/tabs/_item_input.html [new file with mode: 0644]
templates/webpages/order/tabs/_price_sources_dialog.html [new file with mode: 0644]
templates/webpages/order/tabs/_row.html [new file with mode: 0644]
templates/webpages/order/tabs/_second_row.html [new file with mode: 0644]
templates/webpages/order/tabs/_tax_row.html [new file with mode: 0644]
templates/webpages/order/tabs/basic_data.html [new file with mode: 0644]
templates/webpages/order/tabs/phone_notes.html [new file with mode: 0644]
templates/webpages/order_items_search/_order_item_list.html [new file with mode: 0644]
templates/webpages/order_items_search/order_items.html [new file with mode: 0644]
templates/webpages/part/_assembly.html [new file with mode: 0644]
templates/webpages/part/_assembly_row.html [new file with mode: 0644]
templates/webpages/part/_assortment.html [new file with mode: 0644]
templates/webpages/part/_assortment_row.html [new file with mode: 0644]
templates/webpages/part/_basic_data.html [new file with mode: 0644]
templates/webpages/part/_customerprice_row.html [new file with mode: 0644]
templates/webpages/part/_customerprices.html [new file with mode: 0644]
templates/webpages/part/_cvars.html [new file with mode: 0644]
templates/webpages/part/_edit_translations.html [new file with mode: 0644]
templates/webpages/part/_inventory.html [new file with mode: 0644]
templates/webpages/part/_inventory_data.html [new file with mode: 0644]
templates/webpages/part/_makemodel.html [new file with mode: 0644]
templates/webpages/part/_makemodel_row.html [new file with mode: 0644]
templates/webpages/part/_multi_items_dialog.html [new file with mode: 0644]
templates/webpages/part/_multi_items_result.html [new file with mode: 0644]
templates/webpages/part/_part_picker_result.html
templates/webpages/part/_pricegroup_prices.html [new file with mode: 0644]
templates/webpages/part/_sales_price_information.html [new file with mode: 0644]
templates/webpages/part/_shop.html [new file with mode: 0644]
templates/webpages/part/form.html [new file with mode: 0644]
templates/webpages/part/history.html [new file with mode: 0644]
templates/webpages/part/part_picker_search.html
templates/webpages/part/test_page.html
templates/webpages/parts_price_history/report_bottom.html [new file with mode: 0644]
templates/webpages/pay_posting_import/form.html [new file with mode: 0644]
templates/webpages/payment_term/form.html
templates/webpages/payment_term/list.html
templates/webpages/pe/partsgroup_form.html [deleted file]
templates/webpages/pe/partsgroup_report.html [deleted file]
templates/webpages/pe/pricegroup_form.html [deleted file]
templates/webpages/pe/pricegroup_report.html [deleted file]
templates/webpages/pe/search.html [deleted file]
templates/webpages/price_rule/_filter.html
templates/webpages/price_rule/form.html
templates/webpages/price_rule/item.html
templates/webpages/price_rule/report_bottom.html
templates/webpages/price_rule/report_top.html
templates/webpages/project/_basic_data.html
templates/webpages/project/_filter.html
templates/webpages/project/_invoice_permissions.html [new file with mode: 0644]
templates/webpages/project/_linked_records.html
templates/webpages/project/_project_picker_result.html [new file with mode: 0644]
templates/webpages/project/form.html
templates/webpages/project/project_picker_search.html [new file with mode: 0644]
templates/webpages/project/report_top.html
templates/webpages/project/search.html [deleted file]
templates/webpages/project/test_page.html
templates/webpages/project_status/form.html [deleted file]
templates/webpages/project_status/list.html [deleted file]
templates/webpages/project_type/form.html [deleted file]
templates/webpages/project_type/list.html [deleted file]
templates/webpages/rc/step1.html
templates/webpages/rc/step2.html
templates/webpages/reconciliation/assigning_table.html
templates/webpages/reconciliation/form.html
templates/webpages/reconciliation/search.html
templates/webpages/record_links/add_filter.html
templates/webpages/record_links/add_list.html
templates/webpages/record_template/dialog.html [new file with mode: 0644]
templates/webpages/report_generator/csv_export_options.html
templates/webpages/report_generator/html_report.html
templates/webpages/report_generator/pdf_export_options.html
templates/webpages/requirement_spec/_filter.html
templates/webpages/requirement_spec/_form.html
templates/webpages/requirement_spec/_show_basic_settings.html
templates/webpages/requirement_spec/_show_time_and_cost_estimate.html
templates/webpages/requirement_spec/_show_time_and_cost_estimate_item.html
templates/webpages/requirement_spec/show.html
templates/webpages/requirement_spec_acceptance_status/form.html [deleted file]
templates/webpages/requirement_spec_acceptance_status/list.html [deleted file]
templates/webpages/requirement_spec_complexity/form.html [deleted file]
templates/webpages/requirement_spec_complexity/list.html [deleted file]
templates/webpages/requirement_spec_item/_function_block_content_bottom.html
templates/webpages/requirement_spec_item/_section_form.html
templates/webpages/requirement_spec_item/_section_header.html
templates/webpages/requirement_spec_order/_assignment_form.html
templates/webpages/requirement_spec_order/list.html
templates/webpages/requirement_spec_part/_edit.html
templates/webpages/requirement_spec_predefined_text/form.html [deleted file]
templates/webpages/requirement_spec_predefined_text/list.html [deleted file]
templates/webpages/requirement_spec_risk/form.html [deleted file]
templates/webpages/requirement_spec_risk/list.html [deleted file]
templates/webpages/requirement_spec_status/form.html [deleted file]
templates/webpages/requirement_spec_status/list.html [deleted file]
templates/webpages/requirement_spec_type/form.html [deleted file]
templates/webpages/requirement_spec_type/list.html [deleted file]
templates/webpages/rp/aging_ar_bottom.html
templates/webpages/rp/aging_ar_top.html
templates/webpages/rp/bwa.html
templates/webpages/rp/e_mail.html [deleted file]
templates/webpages/rp/erfolgsrechnung.html [new file with mode: 0644]
templates/webpages/rp/html_report_susa.html
templates/webpages/rp/income_statement.html
templates/webpages/rp/print_options.html
templates/webpages/rp/report.html
templates/webpages/sepa/bank_transfer_add.html
templates/webpages/sepa/bank_transfer_create.html
templates/webpages/sepa/bank_transfer_edit.html
templates/webpages/sepa/bank_transfer_list_bottom.html
templates/webpages/sepa/bank_transfer_list_top.html
templates/webpages/sepa/bank_transfer_mark_as_closed_step1.html [deleted file]
templates/webpages/sepa/bank_transfer_search.html
templates/webpages/shop_order/_filter.html [new file with mode: 0644]
templates/webpages/shop_order/_get_one.html [new file with mode: 0644]
templates/webpages/shop_order/_transfer_status.html [new file with mode: 0644]
templates/webpages/shop_order/list.html [new file with mode: 0644]
templates/webpages/shop_order/show.html [new file with mode: 0644]
templates/webpages/shop_part/_filter.html [new file with mode: 0644]
templates/webpages/shop_part/_list_articles.html [new file with mode: 0644]
templates/webpages/shop_part/_list_images.html [new file with mode: 0644]
templates/webpages/shop_part/_upload_status.html [new file with mode: 0644]
templates/webpages/shop_part/categories.html [new file with mode: 0644]
templates/webpages/shop_part/edit.html [new file with mode: 0644]
templates/webpages/shops/form.html [new file with mode: 0644]
templates/webpages/shops/list.html [new file with mode: 0644]
templates/webpages/shops/test_shop_connection.html [new file with mode: 0644]
templates/webpages/simple_system_setting/_bank_account_form.html [new file with mode: 0644]
templates/webpages/simple_system_setting/_business_form.html [new file with mode: 0644]
templates/webpages/simple_system_setting/_default_form.html [new file with mode: 0644]
templates/webpages/simple_system_setting/_language_form.html [new file with mode: 0644]
templates/webpages/simple_system_setting/_part_classification_form.html [new file with mode: 0644]
templates/webpages/simple_system_setting/_parts_group_form.html [new file with mode: 0644]
templates/webpages/simple_system_setting/_price_factor_form.html [new file with mode: 0644]
templates/webpages/simple_system_setting/_pricegroup_form.html [new file with mode: 0644]
templates/webpages/simple_system_setting/_requirement_spec_acceptance_status_form.html [new file with mode: 0644]
templates/webpages/simple_system_setting/_requirement_spec_predefined_text_form.html [new file with mode: 0644]
templates/webpages/simple_system_setting/_requirement_spec_status_form.html [new file with mode: 0644]
templates/webpages/simple_system_setting/_requirement_spec_type_form.html [new file with mode: 0644]
templates/webpages/simple_system_setting/_time_recording_article_form.html [new file with mode: 0644]
templates/webpages/simple_system_setting/form.html [new file with mode: 0644]
templates/webpages/simple_system_setting/list.html [new file with mode: 0644]
templates/webpages/task_server/show.html
templates/webpages/taxzones/form.html
templates/webpages/taxzones/list.html
templates/webpages/time_recording/_filter.html [new file with mode: 0644]
templates/webpages/time_recording/form.html [new file with mode: 0644]
templates/webpages/time_recording/report_bottom.html [new file with mode: 0644]
templates/webpages/time_recording/report_top.html [new file with mode: 0644]
templates/webpages/ustva/config_step1.html
templates/webpages/ustva/config_step2.html
templates/webpages/ustva/generic_taxreport.html [deleted file]
templates/webpages/ustva/report.html
templates/webpages/ustva/ustva.html
templates/webpages/vk/search_invoice.html
templates/webpages/webdav/_list.html
templates/webpages/wh/journal_filter.html
templates/webpages/wh/removal_parts_selection.html
templates/webpages/wh/report_bottom.html [new file with mode: 0644]
templates/webpages/wh/report_filter.html
templates/webpages/wh/report_top.html [new file with mode: 0644]
templates/webpages/wh/transfer_parts_selection.html
templates/webpages/wh/warehouse_selection.html
templates/webpages/wh/warehouse_selection_assembly.html
templates/webpages/wh/warehouse_selection_stock.html [deleted file]
templates/webpages/yearend/_charts.html [new file with mode: 0644]
templates/webpages/yearend/form.html [new file with mode: 0644]
templates/webpages/zugferd/form.html [new file with mode: 0644]
texmf/embedfile.sty [new file with mode: 0644]
users/gdpdu-01-08-2002.dtd [new file with mode: 0644]

diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644 (file)
index 0000000..0ba85d8
--- /dev/null
@@ -0,0 +1,39 @@
+module.exports = {
+    "env": {
+        "browser": true,
+        "es6": true,
+        "jquery": true
+    },
+    "extends": "eslint:recommended",
+    "parserOptions": {
+        "ecmaVersion": 2015
+    },
+    "rules": {
+        "indent": [
+            "error",
+            2
+        ],
+        "linebreak-style": [
+            "error",
+            "unix"
+        ],
+        "semi": [
+            "error",
+            "always"
+        ],
+        "no-console": [
+            "error",
+            {
+                "allow": [
+                    "warn",
+                    "error"
+                ],
+            }
+        ]
+    },
+    "globals": {
+      "namespace": true,
+      "kivi": true
+    },
+};
+
index 7ade8b1..5c8f7d8 100644 (file)
@@ -21,6 +21,7 @@
 /users/pid/
 /users/session_files/
 /users/templates-cache/
+/users/templates-cache-for-tests/
 /users/xvfb_display
 /webdav/*
 crm
index 6686227..106aeb4 100644 (file)
--- a/.htaccess
+++ b/.htaccess
@@ -1,11 +1,16 @@
-### Choose a character set (just in case you like to change it here)
-### uncommit the line you wish to activate
-#AddDefaultCharset ISO-8859-15
+# Should always be the default
 #AddDefaultCharset UTF-8
 
 ### simple access control by client ip
-### uncomment the lines starting with Order ..., Deny ... and Allow ...
-### examples: "Allow from 192.168" or "Allow from 192.168.1" or "Allow from 192.168.178" or "Allow from 217.84.201.2"
-#Order deny,allow
-#Deny from all
-#Allow from 192.168
+### uncomment the lines starting with <IfModule ...> until last </IfModule>
+### examples for Apache >= 2.4: "Require ip 192.168" or "Require ip 192.168.1" or "Require ip 192.168.178" or "Require ip 217.84.201.2"
+#<IfModule mod_authz_core.c>
+#  # Apache 2.4
+#  Require ip 192.168
+#</IfModule>
+
+<IfModule mod_rewrite.c>
+  RewriteEngine On
+  RewriteRule .*/(\.git|config)/.*$ - [F,NC]
+</IfModule>
+
diff --git a/.jshintrc b/.jshintrc
new file mode 100644 (file)
index 0000000..bff88db
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,12 @@
+{
+  "laxcomma" : true,   // tolerate "," at the beginning of lines
+  "laxbreak" : true,   // tolerate "+" at the beginning of lines
+  "jquery": true,      // assume jquery is loaded
+  "asi" : true,        // tolerate satements without ";" yet
+  "eqeqeq" : false,    // don't require === for comparisons yet
+ // "strict" : true
+
+  "globals" : {
+    "predef": [ "kivi" ]
+  }
+}
diff --git a/.mailmap b/.mailmap
new file mode 100644 (file)
index 0000000..bfb19e5
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,42 @@
+Bernd Bleßmann <bernd@kivitendo-premium.de> <bb@it-entwicklung.de>
+Bernd Bleßmann <bernd@kivitendo-premium.de> <bibi@online.de>
+Bernd Bleßmann <bernd@kivitendo-premium.de> bernd <bernd@lxbug.(none)>
+Christian Wittmer <chris@computersalat.de> ChrisWi <chris@computersalat.de>
+Geoffrey Richardson <information@kivitendo-premium.de>
+Geoffrey Richardson <information@kivitendo-premium.de> G. Richardson <grichardson@kivitec.de>
+Geoffrey Richardson <information@kivitendo-premium.de> <information@lx-office-hosting.de>
+Geoffrey Richardson <information@kivitendo-premium.de> <information@richardson-bueren.de>
+Geoffrey Richardson <information@kivitendo-premium.de> grichardson <gr@richardson-bueren.de>
+Holger Lindemann <hli@lx-system.de> <hli@debian7.lx-system.de>
+Holger Lindemann <hli@lx-system.de> <hli@lenny.hoch.ul>
+Jan Büren <jan@kivitendo-premium.de> <jan@baobab.intranet.xplace.de>
+Jan Büren <jan@kivitendo-premium.de> <jan@circa-support.eu>
+Jan Büren <jan@kivitendo-premium.de> <jan@echinacea.es>
+Jan Büren <jan@kivitendo-premium.de> <jan@kivitendo.de>
+Jan Büren <jan@kivitendo-premium.de> <jan@lx-office-hosting.de>
+Jan Büren <jan@kivitendo-premium.de> <jan@lx-office-premium.de>
+Jan Büren <jan@kivitendo-premium.de> <jan@richardson-bueren.de>
+Jan Büren <jan@kivitendo-premium.de> <jan@weitan.org>
+Jan Büren <jan@kivitendo-premium.de> <root@vc-kivi.vitracom.org>
+Joachim Zach <joachim@lx-office-hosting.de> <info@ceos-gmbh.de>
+Marei Peischl <marei@peitex.de> Marei (peiTeX) <marei@peitex.de>
+Marei Peischl <marei@peitex.de> Marei Peischl (peiTeX) <marei@peitex.de>
+Martin Helmling <martin.helmling@octosoft.eu> <MartinHelmling@octo-soft.de>
+Martin Helmling <martin.helmling@octosoft.eu> <mh@waldpark.octosoft.eu>
+Martin Helmling <martin.helmling@octosoft.eu> Martin Helmling martin.helmling@octosoft.eu
+Martin Helmling <martin.helmling@octosoft.eu> Martin Helmling mh@waldpark.octosoft.eu <martin.helmling@octosoft.eu>
+Moritz Bunkus <m.bunkus@linet.de> <moritz@bunkus.org>
+Moritz Bunkus <m.bunkus@linet.de> <m.bunkus@linet-services.de>
+Niclas Zimmermann <niclas@kivitendo-premium.de> <niclas@lx-office-hosting.de>
+Rolf Eike Beer <dakon@users.sf.net> Rolf Eike Beer <eike@sf-mail.de>
+Roman Karuschka <karuschka@ok-it-services.de> R. Karuschka <r.karuschka@ok-it-services.de>
+Roman Karuschka <karuschka@ok-it-services.de> Roman Karushka <karuschka@ok-it-services.de>
+Roman Karuschka <karuschka@ok-it-services.de> roman <roman@omega.ok-it-services.de>
+Sven Schöling <s.schoeling@googlemail.com> <s.schoeling@linet-services.de>
+Sven Schöling <s.schoeling@googlemail.com> <sven.schoeling@opendynamic.de>
+Timo Eickmeyer <timo@kivitendo-premium.de> T. Eickmeyer <timo@kivitendo-premium.de>
+Waldemar Toews <waldemar.toews@opendynamic.de> <toews@erp-d300.opendynamic.local>
+Wulf Coulmann <wulf@coulmann.de> Wulf <wulf@coulmann.de>
+Wulf Coulmann <wulf@coulmann.de> root <root@coulmann.de>
+Wulf Coulmann <wulf@coulmann.de> wulf@coulmann.de <root@coulmann.de>
+Wulf Coulmann <wulf@coulmann.de> wulf@coulmann.de <wulf@coulmann.de>
diff --git a/Devel/REPL/Plugin/AutoloadModules.pm b/Devel/REPL/Plugin/AutoloadModules.pm
new file mode 100644 (file)
index 0000000..e36ee96
--- /dev/null
@@ -0,0 +1,29 @@
+package Devel::REPL::Plugin::AutoloadModules;
+
+use Moose::Role;
+use namespace::clean -except => [ 'meta' ];
+use Data::Dumper;
+
+has 'autoloaded' => ( is => 'rw', isa => 'HashRef', default => sub { {} } );
+
+my $re = qr/Runtime error: Can.t locate object method "\w+" via package "\w+" \(perhaps you forgot to load "(\w+)"\?\)/;
+around 'execute' => sub {
+  my $orig = shift;
+  my $self = shift;
+
+  my @re = $self->$orig(@_);                           # original call
+
+  return @re unless defined $re[0] && $re[0] =~ /$re/; # if there is no "perhaps you forgot" error, just return
+  my $module = $1;                                     # save the missing package name
+
+  return @re if $self->autoloaded->{$module};          # if we tried to load it before, give up and return the error
+
+  $self->autoloaded->{$module} = 1;                    # make sure we don't try this again
+  $self->eval("use SL::$module");                      # try to load the missing module
+
+  @re = $self->$orig(@_);                              # try again
+
+  return @re;
+};
+
+1;
diff --git a/Devel/REPL/Plugin/PermanentHistory.pm b/Devel/REPL/Plugin/PermanentHistory.pm
new file mode 100644 (file)
index 0000000..3a46b56
--- /dev/null
@@ -0,0 +1,39 @@
+package Devel::REPL::Plugin::PermanentHistory;
+
+use Moose::Role;
+use namespace::clean -except => [ 'meta' ];
+use File::Slurp;
+use Data::Dumper;
+
+has 'history_file' => ( is => 'rw' );
+
+sub load_history {
+  my $self = shift;
+  my $file = shift;
+
+  $self->history_file( $file );
+
+  return unless $self->history_file && -f $self->history_file;
+
+  my @history =
+    map { chomp; $_ }
+    read_file($self->history_file);
+#  print  Dumper(\@history);
+  $self->history( \@history );
+  $self->term->addhistory($_) for @history;
+}
+
+before 'DESTROY' => sub {
+  my $self = shift;
+
+  return unless $self->history_file;
+
+  write_file $self->history_file,
+    map { $_, $/ }
+    grep $_,
+    grep { !/^quit\b/ }
+    @{ $self->history };
+};
+
+1;
+
index 0a9a047..cde5b44 100644 (file)
@@ -1,2 +1,9 @@
-Order Allow,Deny
-Deny from all
+<IfModule mod_authz_core.c>
+  # Apache 2.4
+  Require all denied
+</IfModule>
+<IfModule !mod_authz_core.c>
+  # Apache 2.2
+  Order deny,allow
+  Deny from all
+</IfModule>
index cbbfae0..2d8ed4c 100644 (file)
--- a/SL/AM.pm
+++ b/SL/AM.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Administration module
@@ -46,172 +47,107 @@ use SL::DB::AuthUser;
 use SL::DB::Default;
 use SL::DB::Employee;
 use SL::DB::Chart;
+use SL::DB::Customer;
+use SL::DB::Part;
+use SL::DB::Vendor;
+use SL::DB;
 use SL::GenericTranslations;
+use SL::Helper::UserPreferences::DisplayPreferences;
+use SL::Helper::UserPreferences::PositionsScrollbar;
+use SL::Helper::UserPreferences::PartPickerSearch;
+use SL::Helper::UserPreferences::TimeRecording;
+use SL::Helper::UserPreferences::UpdatePositions;
 
 use strict;
 
 sub get_account {
   $main::lxdebug->enter_sub();
 
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-  my $query = qq{
-    SELECT c.accno, c.description, c.charttype, c.category,
-      c.link, c.pos_bilanz, c.pos_eur, c.new_chart_id, c.valid_from,
-      c.pos_bwa, datevautomatik,
-      tk.taxkey_id, tk.pos_ustva, tk.tax_id,
-      tk.tax_id || '--' || tk.taxkey_id AS tax, tk.startdate
-    FROM chart c
-    LEFT JOIN taxkeys tk
-    ON (c.id=tk.chart_id AND tk.id =
-      (SELECT id FROM taxkeys
-       WHERE taxkeys.chart_id = c.id AND startdate <= current_date
-       ORDER BY startdate DESC LIMIT 1))
-    WHERE c.id = ?
-    };
-
-
-  $main::lxdebug->message(LXDebug->QUERY(), "\$query=\n $query");
-  my $sth = $dbh->prepare($query);
-  $sth->execute($form->{id}) || $form->dberror($query . " ($form->{id})");
-
-  my $ref = $sth->fetchrow_hashref("NAME_lc");
+  # fetch chart-related data and set form fields
+  # get_account is called by add_account in am.pl
+  # always sets $form->{TAXKEY} and default_accounts
+  # loads chart data when $form->{id} is passed
 
-  foreach my $key (keys %$ref) {
-    $form->{"$key"} = $ref->{"$key"};
-  }
-
-  $sth->finish;
+  my ($self, $myconfig, $form) = @_;
 
   # get default accounts
-  $query = qq|SELECT inventory_accno_id, income_accno_id, expense_accno_id
-              FROM defaults|;
-  $main::lxdebug->message(LXDebug->QUERY(), "\$query=\n $query");
-  $sth = $dbh->prepare($query);
-  $sth->execute || $form->dberror($query);
-
-  $ref = $sth->fetchrow_hashref("NAME_lc");
-
-  map { $form->{$_} = $ref->{$_} } keys %{ $ref };
-
-  $sth->finish;
-
-
-
-  # get taxkeys and description
-  $query = qq{
-    SELECT
-      id,
-      (SELECT accno FROM chart WHERE id=tax.chart_id) AS chart_accno,
-      taxkey,
-      id||'--'||taxkey AS tax,
-      taxdescription,
-      rate
-    FROM tax ORDER BY taxkey
-  };
-  $main::lxdebug->message(LXDebug->QUERY(), "\$query=\n $query");
-  $sth = $dbh->prepare($query);
-  $sth->execute || $form->dberror($query);
+  map { $form->{$_} = $::instance_conf->{$_} } qw(inventory_accno_id income_accno_id expense_accno_id);
 
+  require SL::DB::Tax;
+  my $taxes = SL::DB::Manager::Tax->get_all( with_objects => ['chart'] , sort_by => 'taxkey' );
   $form->{TAXKEY} = [];
+  foreach my $tk ( @{$taxes} ) {
+    push @{ $form->{TAXKEY} },  { id          => $tk->id,
+                                  chart_accno => $tk->chart_id ? $tk->chart->accno : undef,
+                                  taxkey      => $tk->taxkey,
+                                  tax         => $tk->id . '--' . $tk->taxkey,
+                                  rate        => $tk->rate
+                                };
+  };
 
-  while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
-    push @{ $form->{TAXKEY} }, $ref;
-  }
-
-  $sth->finish;
   if ($form->{id}) {
-    # get new accounts
-    $query = qq|SELECT id, accno,description
-                FROM chart
-                WHERE link = ?
-                ORDER BY accno|;
-    $main::lxdebug->message(LXDebug->QUERY(), "\$query=\n $query");
-    $sth = $dbh->prepare($query);
-    $sth->execute($form->{link}) || $form->dberror($query . " ($form->{link})");
 
-    $form->{NEWACCOUNT} = [];
-    while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
-      push @{ $form->{NEWACCOUNT} }, $ref;
+    my $chart_obj = SL::DB::Manager::Chart->find_by(id => $form->{id}) || die "Can't open chart";
+
+    my @chart_fields = qw(accno description charttype category link pos_bilanz
+                          pos_eur pos_er new_chart_id valid_from pos_bwa datevautomatik);
+    foreach my $cf ( @chart_fields ) {
+      $form->{"$cf"} = $chart_obj->$cf;
     }
 
-    $sth->finish;
+    my $active_taxkey = $chart_obj->get_active_taxkey;
+    $form->{$_}  = $active_taxkey->$_ foreach qw(taxkey_id pos_ustva tax_id startdate);
+    $form->{tax} = $active_taxkey->tax_id . '--' . $active_taxkey->taxkey_id;
 
-    # get the taxkeys of account
-
-    $query = qq{
-      SELECT
-        tk.id,
-        tk.chart_id,
-        c.accno,
-        tk.tax_id,
-        t.taxdescription,
-        t.rate,
-        tk.taxkey_id,
-        tk.pos_ustva,
-        tk.startdate
-      FROM taxkeys tk
-      LEFT JOIN   tax t ON (t.id = tk.tax_id)
-      LEFT JOIN chart c ON (c.id = t.chart_id)
-
-      WHERE tk.chart_id = ?
-      ORDER BY startdate DESC
-    };
-    $main::lxdebug->message(LXDebug->QUERY(), "\$query=\n $query");
-    $sth = $dbh->prepare($query);
+    # check if there are any transactions for this chart
+    $form->{orphaned} = $chart_obj->has_transaction ? 0 : 1;
 
-    $sth->execute($form->{id}) || $form->dberror($query . " ($form->{id})");
+    # check if new account is active
+    # The old sql query was broken since at least 2006 and always returned 0
+    $form->{new_chart_valid} = $chart_obj->new_chart_valid;
 
+    # get the taxkeys of the account
     $form->{ACCOUNT_TAXKEYS} = [];
-
-    while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
-      push @{ $form->{ACCOUNT_TAXKEYS} }, $ref;
+    foreach my $taxkey ( sort { $b->startdate <=> $a->startdate } @{ $chart_obj->taxkeys } ) {
+      push @{ $form->{ACCOUNT_TAXKEYS} }, { id             => $taxkey->id,
+                                            chart_id       => $taxkey->chart_id,
+                                            tax_id         => $taxkey->tax_id,
+                                            taxkey_id      => $taxkey->taxkey_id,
+                                            pos_ustva      => $taxkey->pos_ustva,
+                                            startdate      => $taxkey->startdate->to_kivitendo,
+                                            taxdescription => $taxkey->tax->taxdescription,
+                                            rate           => $taxkey->tax->rate,
+                                            accno          => defined $taxkey->tax->chart_id ? $taxkey->tax->chart->accno : undef,
+                                          };
     }
 
-    $sth->finish;
-
-  }
-  # check if we have any transactions
-  $query = qq|SELECT a.trans_id FROM acc_trans a
-              WHERE a.chart_id = ?|;
-  $main::lxdebug->message(LXDebug->QUERY(), "\$query=\n $query");
-  $sth = $dbh->prepare($query);
-  $sth->execute($form->{id}) || $form->dberror($query . " ($form->{id})");
-
-  ($form->{orphaned}) = $sth->fetchrow_array;
-  $form->{orphaned} = !$form->{orphaned};
-  $sth->finish;
-
-  # check if new account is active
-  $form->{new_chart_valid} = 0;
-  if ($form->{new_chart_id}) {
-    $query = qq|SELECT current_date-valid_from FROM chart
-                WHERE id = ?|;
-    $main::lxdebug->message(LXDebug->QUERY(), "\$query=\n $query");
-    my ($count) = selectrow_query($form, $dbh, $query, $form->{id});
-    if ($count >=0) {
-      $form->{new_chart_valid} = 1;
-    }
-    $sth->finish;
-  }
+    # get new accounts (Folgekonto). Find all charts with the same link
+    $form->{NEWACCOUNT} = $chart_obj->db->dbh->selectall_arrayref('select id, accno,description from chart where link = ? and id != ? order by accno', {Slice => {}}, $chart_obj->link, $form->{id});
 
-  $dbh->disconnect;
+  } else { # set to orphaned for new charts, so chart_type can be changed (needed by $AccountIsPosted)
+    $form->{orphaned} = 1;
+  };
 
   $main::lxdebug->leave_sub();
 }
 
 sub save_account {
+  my ($self, $myconfig, $form) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_save_account, $self, $myconfig, $form);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _save_account {
   # TODO: it should be forbidden to change an account to a heading if there
   # have been bookings to this account in the past
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database, turn off AutoCommit
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   for (qw(AR_include_in_dropdown AP_include_in_dropdown summary_account)) {
     $form->{$form->{$_}} = $form->{$_} if $form->{$_};
@@ -224,23 +160,15 @@ sub save_account {
     }
   }
 
-  $form->{link} = "";
-  foreach my $item ($form->{AR},            $form->{AR_amount},
-                    $form->{AR_tax},        $form->{AR_paid},
-                    $form->{AP},            $form->{AP_amount},
-                    $form->{AP_tax},        $form->{AP_paid},
-                    $form->{IC},            $form->{IC_sale},
-                    $form->{IC_cogs},       $form->{IC_taxpart},
-                    $form->{IC_income},     $form->{IC_expense},
-                    $form->{IC_taxservice}
-    ) {
-    $form->{link} .= "${item}:" if ($item);
-  }
-  chop $form->{link};
+  my @link_order = qw(AR AR_amount AR_tax AR_paid AP AP_amount AP_tax AP_paid IC IC_sale IC_cogs IC_taxpart IC_income IC_expense IC_taxservice);
+  $form->{link} = join ':', grep $_, map $form->{$_}, @link_order;
 
   # strip blanks from accno
   map { $form->{$_} =~ s/ //g; } qw(accno);
 
+  # collapse multiple (horizontal) whitespace in chart description (Ticket 148)
+  map { $form->{$_} =~ s/\h+/ /g } qw(description);
+
   my ($query, $sth);
 
   if ($form->{id} eq "NULL") {
@@ -301,6 +229,7 @@ sub save_account {
                   pos_bwa   = ?,
                   pos_bilanz = ?,
                   pos_eur = ?,
+                  pos_er = ?,
                   new_chart_id = ?,
                   valid_from = ?,
                   datevautomatik = ?
@@ -315,6 +244,7 @@ sub save_account {
                   conv_i($form->{pos_bwa}),
                   conv_i($form->{pos_bilanz}),
                   conv_i($form->{pos_eur}),
+                  conv_i($form->{pos_er}),
                   conv_i($form->{new_chart_id}),
                   conv_date($form->{valid_from}),
                   ($form->{datevautomatik} eq 'T') ? 'true':'false',
@@ -443,42 +373,32 @@ SQL
 
   do_query($form, $dbh, $query, $form->{id});
 
-  # commit
-  my $rc = $dbh->commit;
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-
-  return $rc;
+  return 1;
 }
 
 sub delete_account {
+  my ($self, $myconfig, $form) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_delete_account, $self, $myconfig, $form);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _delete_account {
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database, turn off AutoCommit
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $query = qq|SELECT count(*) FROM acc_trans a
                  WHERE a.chart_id = ?|;
   my ($count) = selectrow_query($form, $dbh, $query, $form->{id});
 
   if ($count) {
-    $dbh->disconnect;
-    $main::lxdebug->leave_sub();
     return;
   }
 
-  # set inventory_accno_id, income_accno_id, expense_accno_id to defaults
-  foreach my $type (qw(inventory income expense)) {
-    $query =
-      qq|UPDATE parts | .
-      qq|SET ${type}_accno_id = (SELECT ${type}_accno_id FROM defaults) | .
-      qq|WHERE ${type}_accno_id = ?|;
-    do_query($form, $dbh, $query, $form->{id});
-  }
-
   $query = qq|DELETE FROM tax
               WHERE chart_id = ?|;
   do_query($form, $dbh, $query, $form->{id});
@@ -495,170 +415,7 @@ sub delete_account {
               WHERE id = ?|;
   do_query($form, $dbh, $query, $form->{id});
 
-  # commit and redirect
-  my $rc = $dbh->commit;
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-
-  return $rc;
-}
-
-sub lead {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-
-  my $query = qq|SELECT id, lead
-                 FROM leads
-                 ORDER BY 2|;
-
-  my $sth = $dbh->prepare($query);
-  $sth->execute || $form->dberror($query);
-
-  while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
-    push @{ $form->{ALL} }, $ref;
-  }
-
-  $sth->finish;
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-}
-
-sub get_lead {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-
-  my $query =
-    qq|SELECT l.id, l.lead | .
-    qq|FROM leads l | .
-    qq|WHERE l.id = ?|;
-  my $sth = $dbh->prepare($query);
-  $sth->execute($form->{id}) || $form->dberror($query . " ($form->{id})");
-
-  my $ref = $sth->fetchrow_hashref("NAME_lc");
-
-  map { $form->{$_} = $ref->{$_} } keys %$ref;
-
-  $sth->finish;
-
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-}
-
-sub save_lead {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-  my ($query);
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-
-  my @values = ($form->{description});
-  # id is the old record
-  if ($form->{id}) {
-    $query = qq|UPDATE leads SET
-                lead = ?
-                WHERE id = ?|;
-    push(@values, $form->{id});
-  } else {
-    $query = qq|INSERT INTO leads
-                (lead)
-                VALUES (?)|;
-  }
-  do_query($form, $dbh, $query, @values);
-
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-}
-
-sub delete_lead {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-  my ($query);
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-
-  $query = qq|DELETE FROM leads
-              WHERE id = ?|;
-  do_query($form, $dbh, $query, $form->{id});
-
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-}
-
-sub language {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form, $return_list) = @_;
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-
-  my $query =
-    "SELECT id, description, template_code, article_code, " .
-    "  output_numberformat, output_dateformat, output_longdates " .
-    "FROM language ORDER BY description";
-
-  my $sth = $dbh->prepare($query);
-  $sth->execute || $form->dberror($query);
-
-  my $ary = [];
-
-  while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
-    push(@{ $ary }, $ref);
-  }
-
-  $sth->finish;
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-
-  if ($return_list) {
-    return @{$ary};
-  } else {
-    $form->{ALL} = $ary;
-  }
-}
-
-sub get_language {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-
-  my $query =
-    "SELECT description, template_code, article_code, " .
-    "  output_numberformat, output_dateformat, output_longdates " .
-    "FROM language WHERE id = ?";
-  my $sth = $dbh->prepare($query);
-  $sth->execute($form->{"id"}) || $form->dberror($query . " ($form->{id})");
-
-  my $ref = $sth->fetchrow_hashref("NAME_lc");
-
-  map { $form->{$_} = $ref->{$_} } keys %$ref;
-
-  $sth->finish;
-
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
+  return 1;
 }
 
 sub get_language_details {
@@ -666,80 +423,19 @@ sub get_language_details {
 
   my ($self, $myconfig, $form, $id) = @_;
 
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $query =
     "SELECT template_code, " .
     "  output_numberformat, output_dateformat, output_longdates " .
     "FROM language WHERE id = ?";
   my @res = selectrow_query($form, $dbh, $query, $id);
-  $dbh->disconnect;
 
   $main::lxdebug->leave_sub();
 
   return @res;
 }
 
-sub save_language {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-  my (@values, $query);
-
-  map({ push(@values, $form->{$_}); }
-      qw(description template_code article_code
-         output_numberformat output_dateformat output_longdates));
-
-  # id is the old record
-  if ($form->{id}) {
-    $query =
-      "UPDATE language SET " .
-      "  description = ?, template_code = ?, article_code = ?, " .
-      "  output_numberformat = ?, output_dateformat = ?, " .
-      "  output_longdates = ? " .
-      "WHERE id = ?";
-    push(@values, $form->{id});
-  } else {
-    $query =
-      "INSERT INTO language (" .
-      "  description, template_code, article_code, " .
-      "  output_numberformat, output_dateformat, output_longdates" .
-      ") VALUES (?, ?, ?, ?, ?, ?)";
-  }
-  do_query($form, $dbh, $query, @values);
-
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-}
-
-sub delete_language {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-  my $query;
-
-  # connect to database
-  my $dbh = $form->dbconnect_noauto($myconfig);
-
-  foreach my $table (qw(generic_translations units_language)) {
-    $query = qq|DELETE FROM $table WHERE language_id = ?|;
-    do_query($form, $dbh, $query, $form->{"id"});
-  }
-
-  $query = "DELETE FROM language WHERE id = ?";
-  do_query($form, $dbh, $query, $form->{"id"});
-
-  $dbh->commit();
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-}
-
 sub prepare_template_filename {
   $main::lxdebug->enter_sub();
 
@@ -747,30 +443,24 @@ sub prepare_template_filename {
 
   my ($filename, $display_filename);
 
-  if ($form->{type} eq "stylesheet") {
-    $filename = "css/$myconfig->{stylesheet}";
-    $display_filename = $myconfig->{stylesheet};
-
-  } else {
-    $filename = $form->{formname};
+  $filename = $form->{formname};
 
-    if ($form->{language}) {
-      my ($id, $template_code) = split(/--/, $form->{language});
-      $filename .= "_${template_code}";
-    }
+  if ($form->{language}) {
+    my ($id, $template_code) = split(/--/, $form->{language});
+    $filename .= "_${template_code}";
+  }
 
-    if ($form->{printer}) {
-      my ($id, $template_code) = split(/--/, $form->{printer});
-      $filename .= "_${template_code}";
-    }
+  if ($form->{printer}) {
+    my ($id, $template_code) = split(/--/, $form->{printer});
+    $filename .= "_${template_code}";
+  }
 
-    $filename .= "." . ($form->{format} eq "html" ? "html" : "tex");
-    if ($form->{"formname"} =~ m|\.\.| || $form->{"formname"} =~ m|^/|) {
-      $filename =~ s|.*/||;
-    }
-    $display_filename = $filename;
-    $filename = SL::DB::Default->get->templates . "/$filename";
+  $filename .= "." . ($form->{format} eq "html" ? "html" : "tex");
+  if ($form->{"formname"} =~ m|\.\.| || $form->{"formname"} =~ m|^/|) {
+    $filename =~ s|.*/||;
   }
+  $display_filename = $filename;
+  $filename = SL::DB::Default->get->templates . "/$filename";
 
   $main::lxdebug->leave_sub();
 
@@ -825,12 +515,53 @@ sub save_template {
   return $error;
 }
 
+sub displayable_name_specs_by_module {
+  +{
+     'SL::DB::Customer' => {
+       specs => SL::DB::Customer->displayable_name_specs,
+       prefs => SL::DB::Customer->displayable_name_prefs,
+     },
+     'SL::DB::Vendor' => {
+       specs => SL::DB::Vendor->displayable_name_specs,
+       prefs => SL::DB::Vendor->displayable_name_prefs,
+     },
+     'SL::DB::Part' => {
+       specs => SL::DB::Part->displayable_name_specs,
+       prefs => SL::DB::Part->displayable_name_prefs,
+     },
+  };
+}
+
+sub positions_scrollbar_height {
+  SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
+}
+
+sub purchase_search_makemodel {
+  SL::Helper::UserPreferences::PartPickerSearch->new()->get_purchase_search_makemodel();
+}
+
+sub sales_search_customer_partnumber {
+  SL::Helper::UserPreferences::PartPickerSearch->new()->get_sales_search_customer_partnumber();
+}
+
+sub positions_show_update_button {
+  SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
+}
+
+sub time_recording_use_duration {
+  SL::Helper::UserPreferences::TimeRecording->new()->get_use_duration();
+}
+
+sub longdescription_dialog_size_percentage {
+  SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
+}
+
 sub save_preferences {
   $main::lxdebug->enter_sub();
 
   my ($self, $form) = @_;
 
-  my $employee = SL::DB::Manager::Employee->find_by(login => $::myconfig{login});
+  my $employee = SL::DB::Manager::Employee->current;
   $employee->update_attributes(name => $form->{name});
 
   my $user = SL::DB::Manager::AuthUser->find_by(login => $::myconfig{login});
@@ -840,6 +571,35 @@ sub save_preferences {
       map { ($_ => $form->{$_}) } SL::DB::AuthUser::CONFIG_VARS(),
     });
 
+  # Displayable name preferences
+  my $displayable_name_specs_by_module = displayable_name_specs_by_module();
+  foreach my $specs (@{ $form->{displayable_name_specs} }) {
+    if (!$specs->{value} || $specs->{value} eq $displayable_name_specs_by_module->{$specs->{module}}->{prefs}->get_default()) {
+      $displayable_name_specs_by_module->{$specs->{module}}->{prefs}->delete($specs->{value});
+    } else {
+      $displayable_name_specs_by_module->{$specs->{module}}->{prefs}->store_value($specs->{value});
+    }
+  }
+
+  if (exists $form->{positions_scrollbar_height}) {
+    SL::Helper::UserPreferences::PositionsScrollbar->new()->store_height($form->{positions_scrollbar_height})
+  }
+  if (exists $form->{purchase_search_makemodel}) {
+    SL::Helper::UserPreferences::PartPickerSearch->new()->store_purchase_search_makemodel($form->{purchase_search_makemodel})
+  }
+  if (exists $form->{sales_search_customer_partnumber}) {
+    SL::Helper::UserPreferences::PartPickerSearch->new()->store_sales_search_customer_partnumber($form->{sales_search_customer_partnumber})
+  }
+  if (exists $form->{positions_show_update_button}) {
+    SL::Helper::UserPreferences::UpdatePositions->new()->store_show_update_button($form->{positions_show_update_button})
+  }
+  if (exists $form->{time_recording_use_duration}) {
+    SL::Helper::UserPreferences::TimeRecording->new()->store_use_duration($form->{time_recording_use_duration})
+  }
+  if (exists $form->{longdescription_dialog_size_percentage}) {
+    SL::Helper::UserPreferences::DisplayPreferences->new()->store_longdescription_dialog_size_percentage($form->{longdescription_dialog_size_percentage})
+  }
+
   $main::lxdebug->leave_sub();
 
   return 1;
@@ -854,7 +614,7 @@ sub get_defaults {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
+  my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
   my $defaults = selectfirst_hashref_query($form, $dbh, qq|SELECT * FROM defaults|) || {};
 
@@ -870,7 +630,7 @@ sub closedto {
 
   my ($self, $myconfig, $form) = @_;
 
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $query = qq|SELECT closedto, max_future_booking_interval, revtrans FROM defaults|;
   my $sth   = $dbh->prepare($query);
@@ -880,8 +640,6 @@ sub closedto {
 
   $sth->finish;
 
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
@@ -890,23 +648,24 @@ sub closebooks {
 
   my ($self, $myconfig, $form) = @_;
 
-  my $dbh = $form->dbconnect($myconfig);
+  SL::DB->client->with_transaction(sub {
+    my $dbh = SL::DB->client->dbh;
 
-  my ($query, @values);
+    my ($query, @values);
 
-  # is currently NEVER trueish (no more hidden revtrans in $form)
-  # if ($form->{revtrans}) {
-  #   $query = qq|UPDATE defaults SET closedto = NULL, revtrans = '1'|;
-  # -> therefore you can only set this to false (which is already the default)
-  # and this flag is currently only checked in gl.pl. TOOD Can probably be removed
+    # is currently NEVER trueish (no more hidden revtrans in $form)
+    # if ($form->{revtrans}) {
+    #   $query = qq|UPDATE defaults SET closedto = NULL, revtrans = '1'|;
+    # -> therefore you can only set this to false (which is already the default)
+    # and this flag is currently only checked in gl.pl. TOOD Can probably be removed
 
-    $query = qq|UPDATE defaults SET closedto = ?, max_future_booking_interval = ?, revtrans = '0'|;
-    @values = (conv_date($form->{closedto}), conv_i($form->{max_future_booking_interval}));
+      $query = qq|UPDATE defaults SET closedto = ?, max_future_booking_interval = ?, revtrans = '0'|;
+      @values = (conv_date($form->{closedto}), conv_i($form->{max_future_booking_interval}));
 
-  # set close in defaults
-  do_query($form, $dbh, $query, @values);
-
-  $dbh->disconnect;
+    # set close in defaults
+    do_query($form, $dbh, $query, @values);
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
@@ -932,7 +691,7 @@ sub retrieve_units {
   my ($self, $myconfig, $form, $prefix) = @_;
   $prefix ||= '';
 
-  my $dbh = $form->get_standard_dbh;
+  my $dbh = SL::DB->client->dbh;
 
   my $query = "SELECT *, base_unit AS original_base_unit FROM units";
 
@@ -1021,7 +780,7 @@ sub units_in_use {
 
   my ($self, $myconfig, $form, $units) = @_;
 
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   map({ $_->{"in_use"} = 0; } values(%{$units}));
 
@@ -1058,8 +817,6 @@ sub units_in_use {
     }
   }
 
-  $dbh->disconnect();
-
   $main::lxdebug->leave_sub();
 }
 
@@ -1190,37 +947,45 @@ sub add_unit {
 
   my ($self, $myconfig, $form, $name, $base_unit, $factor, $languages) = @_;
 
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  SL::DB->client->with_transaction(sub {
+    my $dbh = SL::DB->client->dbh;
 
-  my $query = qq|SELECT COALESCE(MAX(sortkey), 0) + 1 FROM units|;
-  my ($sortkey) = selectrow_query($form, $dbh, $query);
+    my $query = qq|SELECT COALESCE(MAX(sortkey), 0) + 1 FROM units|;
+    my ($sortkey) = selectrow_query($form, $dbh, $query);
 
-  $query = "INSERT INTO units (name, base_unit, factor, sortkey) " .
-    "VALUES (?, ?, ?, ?)";
-  do_query($form, $dbh, $query, $name, $base_unit, $factor, $sortkey);
+    $query = "INSERT INTO units (name, base_unit, factor, sortkey) " .
+      "VALUES (?, ?, ?, ?)";
+    do_query($form, $dbh, $query, $name, $base_unit, $factor, $sortkey);
 
-  if ($languages) {
-    $query = "INSERT INTO units_language (unit, language_id, localized, localized_plural) VALUES (?, ?, ?, ?)";
-    my $sth = $dbh->prepare($query);
-    foreach my $lang (@{$languages}) {
-      my @values = ($name, $lang->{"id"}, $lang->{"localized"}, $lang->{"localized_plural"});
-      $sth->execute(@values) || $form->dberror($query . " (" . join(", ", @values) . ")");
+    if ($languages) {
+      $query = "INSERT INTO units_language (unit, language_id, localized, localized_plural) VALUES (?, ?, ?, ?)";
+      my $sth = $dbh->prepare($query);
+      foreach my $lang (@{$languages}) {
+        my @values = ($name, $lang->{"id"}, $lang->{"localized"}, $lang->{"localized_plural"});
+        $sth->execute(@values) || $form->dberror($query . " (" . join(", ", @values) . ")");
+      }
+      $sth->finish();
     }
-    $sth->finish();
-  }
-
-  $dbh->commit();
-  $dbh->disconnect();
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
 
 sub save_units {
+  my ($self, $myconfig, $form, $units, $delete_units) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_save_units, $self, $myconfig, $form, $units, $delete_units);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _save_units {
   my ($self, $myconfig, $form, $units, $delete_units) = @_;
 
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my ($base_unit, $unit, $sth, $query);
 
@@ -1267,10 +1032,8 @@ sub save_units {
 
   $sth->finish();
   $sth_lang->finish();
-  $dbh->commit();
-  $dbh->disconnect();
 
-  $main::lxdebug->leave_sub();
+  return 1;
 }
 
 sub taxes {
@@ -1278,21 +1041,23 @@ sub taxes {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $query = qq|SELECT
                    t.id,
                    t.taxkey,
                    t.taxdescription,
                    round(t.rate * 100, 2) AS rate,
-                   (SELECT accno FROM chart WHERE id = chart_id) AS taxnumber,
-                   (SELECT description FROM chart WHERE id = chart_id) AS account_description,
-                   (SELECT accno FROM chart WHERE id = skonto_sales_chart_id) AS skonto_chart_accno,
-                   (SELECT description FROM chart WHERE id = skonto_sales_chart_id) AS skonto_chart_description,
-                   (SELECT accno FROM chart WHERE id = skonto_purchase_chart_id) AS skonto_chart_purchase_accno,
-                   (SELECT description FROM chart WHERE id = skonto_purchase_chart_id) AS skonto_chart_purchase_description
+                   tc.accno               AS taxnumber,
+                   tc.description         AS account_description,
+                   ssc.accno              AS skonto_chart_accno,
+                   ssc.description        AS skonto_chart_description,
+                   spc.accno              AS skonto_chart_purchase_accno,
+                   spc.description        AS skonto_chart_purchase_description
                  FROM tax t
+                 LEFT JOIN chart tc  ON (tc.id = t.chart_id)
+                 LEFT JOIN chart ssc ON (ssc.id = t.skonto_sales_chart_id)
+                 LEFT JOIN chart spc ON (spc.id = t.skonto_purchase_chart_id)
                  ORDER BY taxkey, rate|;
 
   my $sth = $dbh->prepare($query);
@@ -1304,7 +1069,6 @@ sub taxes {
   }
 
   $sth->finish;
-  $dbh->disconnect;
 
   $main::lxdebug->leave_sub();
 }
@@ -1314,7 +1078,7 @@ sub get_tax_accounts {
 
   my ($self, $myconfig, $form) = @_;
 
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   # get Accounts from chart
   my $query = qq{ SELECT
@@ -1346,8 +1110,6 @@ sub get_tax_accounts {
 
   $sth->finish;
 
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
@@ -1356,8 +1118,7 @@ sub get_tax {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $query = qq|SELECT
                    taxkey,
@@ -1408,19 +1169,24 @@ sub get_tax {
     $sth->finish;
   }
 
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
 sub save_tax {
+  my ($self, $myconfig, $form) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_save_tax, $self, $myconfig, $form);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _save_tax {
   my ($self, $myconfig, $form) = @_;
   my $query;
 
-  # connect to database
-  my $dbh = $form->get_standard_dbh($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   $form->{rate} = $form->{rate} / 100;
 
@@ -1432,14 +1198,13 @@ sub save_tax {
   $chart_categories .= 'E' if $form->{expense};
   $chart_categories .= 'C' if $form->{costs};
 
-  my @values = ($form->{taxkey}, $form->{taxdescription}, $form->{rate}, conv_i($form->{chart_id}), conv_i($form->{chart_id}), conv_i($form->{skonto_sales_chart_id}), conv_i($form->{skonto_purchase_chart_id}), $chart_categories);
+  my @values = ($form->{taxkey}, $form->{taxdescription}, $form->{rate}, conv_i($form->{chart_id}), conv_i($form->{skonto_sales_chart_id}), conv_i($form->{skonto_purchase_chart_id}), $chart_categories);
   if ($form->{id} ne "") {
     $query = qq|UPDATE tax SET
                   taxkey                   = ?,
                   taxdescription           = ?,
                   rate                     = ?,
                   chart_id                 = ?,
-                  taxnumber                = (SELECT accno FROM chart WHERE id = ? ),
                   skonto_sales_chart_id    = ?,
                   skonto_purchase_chart_id = ?,
                   chart_categories         = ?
@@ -1453,13 +1218,12 @@ sub save_tax {
                   taxdescription,
                   rate,
                   chart_id,
-                  taxnumber,
                   skonto_sales_chart_id,
                   skonto_purchase_chart_id,
                   chart_categories,
                   id
                 )
-                VALUES (?, ?, ?, ?, (SELECT accno FROM chart WHERE id = ?), ?, ?,  ?, ?)|;
+                VALUES (?, ?, ?, ?, ?, ?,  ?, ?)|;
   }
   push(@values, $form->{id});
   do_query($form, $dbh, $query, @values);
@@ -1471,10 +1235,6 @@ sub save_tax {
                               'language_id'      => $language_id,
                               'translation'      => $form->{translations}->{$language_id});
   }
-
-  $dbh->commit();
-
-  $main::lxdebug->leave_sub();
 }
 
 sub delete_tax {
@@ -1483,86 +1243,11 @@ sub delete_tax {
   my ($self, $myconfig, $form) = @_;
   my $query;
 
-  # connect to database
-  my $dbh = $form->get_standard_dbh($myconfig);
-
-  $query = qq|DELETE FROM tax
-              WHERE id = ?|;
-  do_query($form, $dbh, $query, $form->{id});
-
-  $dbh->commit();
-
-  $main::lxdebug->leave_sub();
-}
-
-sub save_price_factor {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->get_standard_dbh($myconfig);
-
-  my $query;
-  my @values = ($form->{description}, conv_i($form->{factor}));
-
-  if ($form->{id}) {
-    $query = qq|UPDATE price_factors SET description = ?, factor = ? WHERE id = ?|;
-    push @values, conv_i($form->{id});
-
-  } else {
-    $query = qq|INSERT INTO price_factors (description, factor, sortkey) VALUES (?, ?, (SELECT COALESCE(MAX(sortkey), 0) + 1 FROM price_factors))|;
-  }
-
-  do_query($form, $dbh, $query, @values);
-
-  $dbh->commit();
-
-  $main::lxdebug->leave_sub();
-}
-
-sub get_all_price_factors {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->get_standard_dbh($myconfig);
-
-  $form->{PRICE_FACTORS} = selectall_hashref_query($form, $dbh, qq|SELECT * FROM price_factors ORDER BY sortkey|);
-
-  $main::lxdebug->leave_sub();
-}
-
-sub get_price_factor {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->get_standard_dbh($myconfig);
-
-  my $query = qq|SELECT description, factor,
-                   ((SELECT COUNT(*) FROM parts      WHERE price_factor_id = ?) +
-                    (SELECT COUNT(*) FROM invoice    WHERE price_factor_id = ?) +
-                    (SELECT COUNT(*) FROM orderitems WHERE price_factor_id = ?)) = 0 AS orphaned
-                 FROM price_factors WHERE id = ?|;
-
-  ($form->{description}, $form->{factor}, $form->{orphaned}) = selectrow_query($form, $dbh, $query, (conv_i($form->{id})) x 4);
-
-  $main::lxdebug->leave_sub();
-}
-
-sub delete_price_factor {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->get_standard_dbh($myconfig);
-
-  do_query($form, $dbh, qq|DELETE FROM price_factors WHERE id = ?|, conv_i($form->{id}));
-  $dbh->commit();
+  SL::DB->client->with_transaction(sub {
+    $query = qq|DELETE FROM tax WHERE id = ?|;
+    do_query($form, SL::DB->client->dbh, $query, $form->{id});
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
@@ -1572,35 +1257,37 @@ sub save_warehouse {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->get_standard_dbh($myconfig);
+  croak('Need at least one new bin') unless $form->{number_of_new_bins} > 0;
 
-  my ($query, @values, $sth);
+  SL::DB->client->with_transaction(sub {
+    my $dbh = SL::DB->client->dbh;
 
-  if (!$form->{id}) {
-    $query        = qq|SELECT nextval('id')|;
-    ($form->{id}) = selectrow_query($form, $dbh, $query);
+    my ($query, @values, $sth);
 
-    $query        = qq|INSERT INTO warehouse (id, sortkey) VALUES (?, (SELECT COALESCE(MAX(sortkey), 0) + 1 FROM warehouse))|;
-    do_query($form, $dbh, $query, $form->{id});
-  }
+    if (!$form->{id}) {
+      $query        = qq|SELECT nextval('id')|;
+      ($form->{id}) = selectrow_query($form, $dbh, $query);
 
-  do_query($form, $dbh, qq|UPDATE warehouse SET description = ?, invalid = ? WHERE id = ?|,
-           $form->{description}, $form->{invalid} ? 't' : 'f', conv_i($form->{id}));
+      $query        = qq|INSERT INTO warehouse (id, sortkey) VALUES (?, (SELECT COALESCE(MAX(sortkey), 0) + 1 FROM warehouse))|;
+      do_query($form, $dbh, $query, $form->{id});
+    }
 
-  if (0 < $form->{number_of_new_bins}) {
-    my ($num_existing_bins) = selectfirst_array_query($form, $dbh, qq|SELECT COUNT(*) FROM bin WHERE warehouse_id = ?|, $form->{id});
-    $query = qq|INSERT INTO bin (warehouse_id, description) VALUES (?, ?)|;
-    $sth   = prepare_query($form, $dbh, $query);
+    do_query($form, $dbh, qq|UPDATE warehouse SET description = ?, invalid = ? WHERE id = ?|,
+             $form->{description}, $form->{invalid} ? 't' : 'f', conv_i($form->{id}));
 
-    foreach my $i (1..$form->{number_of_new_bins}) {
-      do_statement($form, $sth, $query, conv_i($form->{id}), "$form->{prefix}" . ($i + $num_existing_bins));
-    }
+    if (0 < $form->{number_of_new_bins}) {
+      my ($num_existing_bins) = selectfirst_array_query($form, $dbh, qq|SELECT COUNT(*) FROM bin WHERE warehouse_id = ?|, $form->{id});
+      $query = qq|INSERT INTO bin (warehouse_id, description) VALUES (?, ?)|;
+      $sth   = prepare_query($form, $dbh, $query);
 
-    $sth->finish();
-  }
+      foreach my $i (1..$form->{number_of_new_bins}) {
+        do_statement($form, $sth, $query, conv_i($form->{id}), "$form->{prefix}" . ($i + $num_existing_bins));
+      }
 
-  $dbh->commit();
+      $sth->finish();
+    }
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
@@ -1610,34 +1297,30 @@ sub save_bins {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->get_standard_dbh($myconfig);
+  SL::DB->client->with_transaction(sub {
+    my $dbh = SL::DB->client->dbh;
 
-  my ($query, @values, $commit_necessary, $sth);
-
-  @values = map { $form->{"id_${_}"} } grep { $form->{"delete_${_}"} } (1..$form->{rowcount});
-
-  if (@values) {
-    $query = qq|DELETE FROM bin WHERE id IN (| . join(', ', ('?') x scalar(@values)) . qq|)|;
-    do_query($form, $dbh, $query, @values);
-
-    $commit_necessary = 1;
-  }
+    my ($query, @values, $sth);
 
-  $query = qq|UPDATE bin SET description = ? WHERE id = ?|;
-  $sth   = prepare_query($form, $dbh, $query);
+    @values = map { $form->{"id_${_}"} } grep { $form->{"delete_${_}"} } (1..$form->{rowcount});
 
-  foreach my $row (1..$form->{rowcount}) {
-    next if ($form->{"delete_${row}"});
+    if (@values) {
+      $query = qq|DELETE FROM bin WHERE id IN (| . join(', ', ('?') x scalar(@values)) . qq|)|;
+      do_query($form, $dbh, $query, @values);
+    }
 
-    do_statement($form, $sth, $query, $form->{"description_${row}"}, conv_i($form->{"id_${row}"}));
+    $query = qq|UPDATE bin SET description = ? WHERE id = ?|;
+    $sth   = prepare_query($form, $dbh, $query);
 
-    $commit_necessary = 1;
-  }
+    foreach my $row (1..$form->{rowcount}) {
+      next if ($form->{"delete_${row}"});
 
-  $sth->finish();
+      do_statement($form, $sth, $query, $form->{"description_${row}"}, conv_i($form->{"id_${row}"}));
+    }
 
-  $dbh->commit() if ($commit_necessary);
+    $sth->finish();
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
@@ -1647,26 +1330,26 @@ sub delete_warehouse {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->get_standard_dbh($myconfig);
+  my $rc = SL::DB->client->with_transaction(sub {
+    my $dbh = SL::DB->client->dbh;
 
-  my $id      = conv_i($form->{id});
-  my $query   = qq|SELECT i.bin_id FROM inventory i WHERE i.bin_id IN (SELECT b.id FROM bin b WHERE b.warehouse_id = ?) LIMIT 1|;
-  my ($count) = selectrow_query($form, $dbh, $query, $id);
+    my $id      = conv_i($form->{id});
+    my $query   = qq|SELECT i.bin_id FROM inventory i WHERE i.bin_id IN (SELECT b.id FROM bin b WHERE b.warehouse_id = ?) LIMIT 1|;
+    my ($count) = selectrow_query($form, $dbh, $query, $id);
 
-  if ($count) {
-    $main::lxdebug->leave_sub();
-    return 0;
-  }
+    if ($count) {
+      return 0;
+    }
 
-  do_query($form, $dbh, qq|DELETE FROM bin       WHERE warehouse_id = ?|, conv_i($form->{id}));
-  do_query($form, $dbh, qq|DELETE FROM warehouse WHERE id           = ?|, conv_i($form->{id}));
+    do_query($form, $dbh, qq|DELETE FROM bin       WHERE warehouse_id = ?|, conv_i($form->{id}));
+    do_query($form, $dbh, qq|DELETE FROM warehouse WHERE id           = ?|, conv_i($form->{id}));
 
-  $dbh->commit();
+    return 1;
+  });
 
   $main::lxdebug->leave_sub();
 
-  return 1;
+  return $rc;
 }
 
 sub get_all_warehouses {
@@ -1674,8 +1357,7 @@ sub get_all_warehouses {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->get_standard_dbh($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $query = qq|SELECT w.id, w.description, w.invalid,
                    (SELECT COUNT(b.description) FROM bin b WHERE b.warehouse_id = w.id) AS number_of_bins
@@ -1692,8 +1374,7 @@ sub get_warehouse {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->get_standard_dbh($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $id    = conv_i($form->{id});
   my $query = qq|SELECT w.description, w.invalid
@@ -1705,12 +1386,14 @@ sub get_warehouse {
   map { $form->{$_} = $ref->{$_} } keys %{ $ref };
 
   $query = <<SQL;
-    SELECT b.*,
-      (   EXISTS(SELECT i.bin_id FROM inventory i WHERE i.bin_id = b.id LIMIT 1)
-       OR EXISTS(SELECT p.bin_id FROM parts     p WHERE p.bin_id = b.id LIMIT 1))
-      AS in_use
-    FROM bin b
-    WHERE b.warehouse_id = ?
+   SELECT b.*, use.in_use
+     FROM bin b
+     LEFT JOIN (
+       SELECT DISTINCT bin_id, TRUE AS in_use FROM inventory
+       UNION
+       SELECT DISTINCT bin_id, TRUE AS in_use FROM parts
+     ) use ON use.bin_id = b.id
+     WHERE b.warehouse_id = ?;
 SQL
 
   $form->{BINS} = selectall_hashref_query($form, $dbh, $query, conv_i($form->{id}));
@@ -1718,4 +1401,22 @@ SQL
   $main::lxdebug->leave_sub();
 }
 
+sub get_eur_categories {
+  my ($self, $myconfig, $form) = @_;
+
+  my $dbh = SL::DB->client->dbh;
+  my %eur_categories = selectall_as_map($form, $dbh, "select * from eur_categories order by id", 'id', 'description');
+
+  return \%eur_categories;
+}
+
+sub get_bwa_categories {
+  my ($self, $myconfig, $form) = @_;
+
+  my $dbh = SL::DB->client->dbh;
+  my %bwa_categories = selectall_as_map($form, $dbh, "select * from bwa_categories order by id", 'id', 'description');
+
+  return \%bwa_categories;
+}
+
 1;
index f551b34..d399b7c 100644 (file)
--- a/SL/AP.pm
+++ b/SL/AP.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Accounts Payables database backend routines
@@ -38,18 +39,32 @@ use SL::DATEV qw(:CONSTANTS);
 use SL::DBUtils;
 use SL::IO;
 use SL::MoreCommon;
+use SL::DB::ApGl;
 use SL::DB::Default;
+use SL::DB::Draft;
+use SL::DB::Order;
+use SL::DB::PurchaseInvoice;
+use SL::Util qw(trim);
+use SL::DB;
 use Data::Dumper;
-
+use List::Util qw(sum0);
 use strict;
 
 sub post_transaction {
+  my ($self, $myconfig, $form, $provided_dbh, %params) = @_;
   $main::lxdebug->enter_sub();
 
-  my ($self, $myconfig, $form, $provided_dbh, $payments_only) = @_;
-  my $rc = 0; # return code auf false setzen
-  # connect to database
-  my $dbh = $provided_dbh ? $provided_dbh : $form->dbconnect_noauto($myconfig);
+  my $rc = SL::DB->client->with_transaction(\&_post_transaction, $self, $myconfig, $form, $provided_dbh, %params);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _post_transaction {
+  my ($self, $myconfig, $form, $provided_dbh, %params) = @_;
+
+  my $payments_only = $params{payments_only};
+  my $dbh = $provided_dbh || SL::DB->client->dbh;
 
   my ($null, $taxrate, $amount);
   my $exchangerate = 0;
@@ -57,8 +72,6 @@ sub post_transaction {
   $form->{defaultcurrency} = $form->get_default_currency($myconfig);
   $form->{taxincluded} = 0 unless $form->{taxincluded};
 
-  ($null, $form->{department_id}) = split(/--/, $form->{department});
-
   if ($form->{currency} eq $form->{defaultcurrency}) {
     $form->{exchangerate} = 1;
   } else {
@@ -66,13 +79,8 @@ sub post_transaction {
     $form->{exchangerate} = $exchangerate || $form->parse_amount($myconfig, $form->{exchangerate});
   }
 
-  for my $i (1 .. $form->{rowcount}) {
-    $form->{AP_amounts}{"amount_$i"} =
-      (split(/--/, $form->{"AP_amount_$i"}))[0];
-  }
-
-  ($form->{AP_amounts}{payables}) = split(/--/, $form->{APselected});
-  ($form->{AP_payables})          = split(/--/, $form->{APselected});
+  # get the charts selected
+  $form->{AP_amounts}{"amount_$_"} = $form->{"AP_amount_chart_id_$_"} for (1 .. $form->{rowcount});
 
   # calculate the totals while calculating and reformatting the $amount_$i and $tax_$i
   ($form->{netamount},$form->{total_tax},$form->{invtotal}) = $form->calculate_arap('buy',$form->{taxincluded}, $form->{exchangerate});
@@ -132,22 +140,50 @@ sub post_transaction {
 
     $query = qq|UPDATE ap SET invnumber = ?,
                 transdate = ?, ordnumber = ?, vendor_id = ?, taxincluded = ?,
-                amount = ?, duedate = ?, paid = ?, netamount = ?,
+                amount = ?, duedate = ?, deliverydate = ?, tax_point = ?, paid = ?, netamount = ?,
                 currency_id = (SELECT id FROM currencies WHERE name = ?), notes = ?, department_id = ?, storno = ?, storno_id = ?,
-                globalproject_id = ?, direct_debit = ?
+                globalproject_id = ?, direct_debit = ?, payment_id = ?, transaction_description = ?
                WHERE id = ?|;
     @values = ($form->{invnumber}, conv_date($form->{transdate}),
                   $form->{ordnumber}, conv_i($form->{vendor_id}),
                   $form->{taxincluded} ? 't' : 'f', $form->{invtotal},
-                  conv_date($form->{duedate}), $form->{invpaid},
-                  $form->{netamount},
+                  conv_date($form->{duedate}), conv_date($form->{deliverydate}), conv_date($form->{tax_point}),
+                  $form->{invpaid}, $form->{netamount},
                   $form->{currency}, $form->{notes},
                   conv_i($form->{department_id}), $form->{storno},
                   $form->{storno_id}, conv_i($form->{globalproject_id}),
                   $form->{direct_debit} ? 't' : 'f',
+                  conv_i($form->{payment_id}), $form->{transaction_description},
                   $form->{id});
     do_query($form, $dbh, $query, @values);
 
+    $form->new_lastmtime('ap');
+
+    # Link this record to the record it was created from.
+    my $convert_from_oe_id = delete $form->{convert_from_oe_id};
+    if (!$form->{postasnew} && $convert_from_oe_id) {
+      RecordLinks->create_links('dbh'        => $dbh,
+                                'mode'       => 'ids',
+                                'from_table' => 'oe',
+                                'from_ids'   => $convert_from_oe_id,
+                                'to_table'   => 'ap',
+                                'to_id'      => $form->{id},
+      );
+
+      # Close the record it was created from if the amount of
+      # all APs create from this record equals the records amount.
+      my @links = RecordLinks->get_links('dbh'        => $dbh,
+                                         'from_table' => 'oe',
+                                         'from_id'    => $convert_from_oe_id,
+                                         'to_table'   => 'ap',
+      );
+
+      my $amount_sum = sum0 map { SL::DB::PurchaseInvoice->new(id => $_->{to_id})->load->amount } @links;
+      my $order      = SL::DB::Order->new(id => $convert_from_oe_id)->load;
+
+      $order->update_attributes(closed => 1) if ($amount_sum - $order->amount) == 0;
+    }
+
     # add individual transactions
     for my $i (1 .. $form->{rowcount}) {
       if ($form->{"amount_$i"} != 0) {
@@ -158,13 +194,11 @@ sub post_transaction {
         $query =
           qq|INSERT INTO acc_trans | .
           qq|  (trans_id, chart_id, amount, transdate, project_id, taxkey, tax_id, chart_link)| .
-          qq|VALUES (?, (SELECT c.id FROM chart c WHERE c.accno = ?), | .
-          qq|  ?, ?, ?, ?, ?,| .
-          qq| (SELECT c.link FROM chart c WHERE c.accno = ?))|;
-        @values = ($form->{id}, $form->{AP_amounts}{"amount_$i"},
+          qq|VALUES (?, ?,   ?, ?, ?, ?, ?, (SELECT c.link FROM chart c WHERE c.id = ?))|;
+        @values = ($form->{id}, $form->{"AP_amount_chart_id_$i"},
                    $form->{"amount_$i"}, conv_date($form->{transdate}),
                    $project_id, $form->{"taxkey_$i"}, conv_i($form->{"tax_id_$i"}),
-                   $form->{AP_amounts}{"amount_$i"});
+                   $form->{"AP_amount_chart_id_$i"});
         do_query($form, $dbh, $query, @values);
 
         if ($form->{"tax_$i"} != 0) {
@@ -188,19 +222,17 @@ sub post_transaction {
     # add payables
     $query =
       qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, taxkey, tax_id, chart_link) | .
-      qq|VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?, | .
-      qq|        (SELECT taxkey_id FROM chart WHERE accno = ?),| .
+      qq|VALUES (?, ?, ?, ?, | .
+      qq|        (SELECT taxkey_id FROM chart WHERE id = ?),| .
       qq|        (SELECT tax_id| .
       qq|         FROM taxkeys| .
-      qq|         WHERE chart_id= (SELECT id | .
-      qq|                          FROM chart| .
-      qq|                          WHERE accno = ?)| .
+      qq|         WHERE chart_id = ?| .
       qq|         AND startdate <= ?| .
       qq|         ORDER BY startdate DESC LIMIT 1),| .
-      qq|        (SELECT c.link FROM chart c WHERE c.accno = ?))|;
-    @values = ($form->{id}, $form->{AP_amounts}{payables}, $form->{payables},
-               conv_date($form->{transdate}), $form->{AP_amounts}{payables}, $form->{AP_amounts}{payables}, conv_date($form->{transdate}),
-               $form->{AP_amounts}{payables});
+      qq|        (SELECT c.link FROM chart c WHERE c.id = ?))|;
+    @values = ($form->{id}, $form->{AP_chart_id}, $form->{payables},
+               conv_date($form->{transdate}), $form->{AP_chart_id}, $form->{AP_chart_id}, conv_date($form->{transdate}),
+               $form->{AP_chart_id});
     do_query($form, $dbh, $query, @values);
   }
 
@@ -209,6 +241,8 @@ sub post_transaction {
     $form->{payables} = $form->{invpaid};
   }
 
+  my %already_cleared = %{ $params{already_cleared} // {} };
+
   # add paid transactions
   for my $i (1 .. $form->{paidaccounts}) {
 
@@ -242,23 +276,29 @@ sub post_transaction {
       $amount =
         $form->round_amount($form->{"paid_$i"} * $form->{exchangerate} * -1,
                             2);
+
+      my $new_cleared = !$form->{"acc_trans_id_$i"}                                                             ? 'f'
+                      : !$already_cleared{$form->{"acc_trans_id_$i"}}                                           ? 'f'
+                      : $already_cleared{$form->{"acc_trans_id_$i"}}->{amount} != $amount * -1                  ? 'f'
+                      : $already_cleared{$form->{"acc_trans_id_$i"}}->{accno}  != $form->{"AP_paid_account_$i"} ? 'f'
+                      : $already_cleared{$form->{"acc_trans_id_$i"}}->{cleared}                                 ? 't'
+                      :                                                                                           'f';
+
       if ($form->{payables}) {
         $query =
-          qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, project_id, taxkey, tax_id, chart_link) | .
-          qq|VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?, ?, | .
-          qq|        (SELECT taxkey_id FROM chart WHERE accno = ?),| .
+          qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, project_id, cleared, taxkey, tax_id, chart_link) | .
+          qq|VALUES (?, ?, ?, ?, ?, ?, | .
+          qq|        (SELECT taxkey_id FROM chart WHERE id = ?),| .
           qq|        (SELECT tax_id| .
           qq|         FROM taxkeys| .
-          qq|         WHERE chart_id= (SELECT id | .
-          qq|                          FROM chart| .
-          qq|                          WHERE accno = ?)| .
+          qq|         WHERE chart_id = ?| .
           qq|         AND startdate <= ?| .
           qq|         ORDER BY startdate DESC LIMIT 1),| .
-          qq|        (SELECT c.link FROM chart c WHERE c.accno = ?))|;
-        @values = ($form->{id}, $form->{AP_payables}, $amount,
-                   conv_date($form->{"datepaid_$i"}), $project_id,
-                   $form->{AP_payables}, $form->{AP_payables}, conv_date($form->{"datepaid_$i"}),
-                   $form->{AP_payables});
+          qq|        (SELECT c.link FROM chart c WHERE c.id = ?))|;
+        @values = ($form->{id}, $form->{AP_chart_id}, $amount,
+                   conv_date($form->{"datepaid_$i"}), $project_id, $new_cleared,
+                   $form->{AP_chart_id}, $form->{AP_chart_id}, conv_date($form->{"datepaid_$i"}),
+                   $form->{AP_chart_id});
         do_query($form, $dbh, $query, @values);
       }
       $form->{payables} = $amount;
@@ -266,8 +306,8 @@ sub post_transaction {
       # add payment
       my $gldate = (conv_date($form->{"gldate_$i"}))? conv_date($form->{"gldate_$i"}) : conv_date($form->current_date($myconfig));
       $query =
-        qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, gldate, source, memo, project_id, taxkey, tax_id, chart_link) | .
-        qq|VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?, ?, ?, ?, ?, | .
+        qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, gldate, source, memo, project_id, cleared, taxkey, tax_id, chart_link) | .
+        qq|VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?, ?, ?, ?, ?, ?, | .
         qq|        (SELECT taxkey_id FROM chart WHERE accno = ?), | .
         qq|        (SELECT tax_id| .
         qq|         FROM taxkeys| .
@@ -279,7 +319,7 @@ sub post_transaction {
         qq|        (SELECT c.link FROM chart c WHERE c.accno = ?))|;
       @values = ($form->{id}, $form->{"AP_paid_account_$i"}, $form->{"paid_$i"},
                  conv_date($form->{"datepaid_$i"}), $gldate, $form->{"source_$i"},
-                 $form->{"memo_$i"}, $project_id, $form->{"AP_paid_account_$i"},
+                 $form->{"memo_$i"}, $project_id, $new_cleared, $form->{"AP_paid_account_$i"},
                  $form->{"AP_paid_account_$i"}, conv_date($form->{"datepaid_$i"}),
                  $form->{"AP_paid_account_$i"});
       do_query($form, $dbh, $query, @values);
@@ -316,6 +356,15 @@ sub post_transaction {
                              $form->{"exchangerate_$i"}), 2);
 
       if ($amount != 0) {
+        # fetch fxgain and fxloss chart info from defaults if charts aren't already filled in form
+        if ( !$form->{fxgain_accno} && $::instance_conf->get_fxgain_accno_id ) {
+          $form->{fxgain_accno} = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_fxgain_accno_id)->accno;
+        };
+        if ( !$form->{fxloss_accno} && $::instance_conf->get_fxloss_accno_id ) {
+          $form->{fxloss_accno} = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_fxloss_accno_id)->accno;
+        };
+        die "fxloss_accno missing" if $amount < 0 and not $form->{fxloss_accno};
+        die "fxgain_accno missing" if $amount > 0 and not $form->{fxgain_accno};
         $query =
           qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, fx_transaction, cleared, project_id, taxkey, tax_id, chart_link) | .
           qq|VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?, 't', 'f', ?, | .
@@ -349,40 +398,94 @@ sub post_transaction {
   if ($payments_only) {
     $query = qq|UPDATE ap SET paid = ?, datepaid = ? WHERE id = ?|;
     do_query($form, $dbh, $query,  $form->{invpaid}, $form->{invpaid} ? conv_date($form->{datepaid}) : undef, conv_i($form->{id}));
+    $form->new_lastmtime('ap');
   }
 
   IO->set_datepaid(table => 'ap', id => $form->{id}, dbh => $dbh);
 
+  if ($form->{draft_id}) {
+    SL::DB::Manager::Draft->delete_all(where => [ id => delete($form->{draft_id}) ]);
+  }
+
+  # hook for taxkey 94
+  $self->_reverse_charge($myconfig, $form);
   # safety check datev export
   if ($::instance_conf->get_datev_check_on_ap_transaction) {
-    my $transdate = $::form->{transdate} ? DateTime->from_lxoffice($::form->{transdate}) : undef;
-    $transdate  ||= DateTime->today;
-
     my $datev = SL::DATEV->new(
-      exporttype => DATEV_ET_BUCHUNGEN,
-      format     => DATEV_FORMAT_KNE,
       dbh        => $dbh,
       trans_id   => $form->{id},
     );
-
-    $datev->export;
+    $datev->generate_datev_data;
 
     if ($datev->errors) {
-      $dbh->rollback;
       die join "\n", $::locale->text('DATEV check returned errors:'), $datev->errors;
     }
   }
 
-  if (!$provided_dbh) {
-    $dbh->commit();
-    $dbh->disconnect();
-  }
-
-  $rc = 1; #  Den return-code auf true setzen, aber nur falls beim commit alles i.O. ist
+  return 1;
+}
 
-  $main::lxdebug->leave_sub();
+sub _reverse_charge {
+  my ($self, $myconfig, $form) = @_;
 
-  return $rc;
+  # delete previous bookings, if they exists (repost)
+  my $ap_gl = SL::DB::Manager::ApGl->get_first(where => [ ap_id => $form->{id} ]);
+  my $gl_id = ref $ap_gl eq 'SL::DB::ApGl' ? $ap_gl->gl_id : undef;
+
+  SL::DB::Manager::GLTransaction->delete_all(where => [ id    => $gl_id ])       if $gl_id;
+  SL::DB::Manager::ApGl->         delete_all(where => [ ap_id => $form->{id} ])  if $gl_id;
+  SL::DB::Manager::RecordLink->   delete_all(where => [ from_table => 'ap', to_table => 'gl', from_id => $form->{id} ]);
+
+  my ($i, $current_transaction);
+
+  for $i (1 .. $form->{rowcount}) {
+
+    my $tax = SL::DB::Manager::Tax->get_first( where => [id => $form->{"tax_id_$i"}, '!reverse_charge_chart_id' => undef ]);
+    next unless ref $tax eq 'SL::DB::Tax';
+
+    # gl booking
+    my ($credit, $debit);
+    $credit   = SL::DB::Manager::Chart->find_by(id => $tax->chart_id);
+    $debit    = SL::DB::Manager::Chart->find_by(id => $tax->reverse_charge_chart_id);
+
+    croak("No such Chart ID" . $tax->chart_id)          unless ref $credit eq 'SL::DB::Chart';
+    croak("No such Chart ID" . $tax->reverse_chart_id)  unless ref $debit  eq 'SL::DB::Chart';
+
+    my ($tmpnetamount, $tmptaxamount) = $form->calculate_tax($form->{"amount_$i"}, $tax->rate, $form->{taxincluded}, 2);
+    $current_transaction = SL::DB::GLTransaction->new(
+          employee_id    => $form->{employee_id},
+          transdate      => $form->{transdate},
+          description    => $form->{notes} || $form->{invnumber},
+          reference      => $form->{invnumber},
+          department_id  => $form->{department_id} ? $form->{department_id} : undef,
+          imported       => 0, # not imported
+          taxincluded    => 0,
+        )->add_chart_booking(
+          chart  => $tmptaxamount > 0 ? $debit : $credit,
+          debit  => abs($tmptaxamount),
+          source => "Reverse Charge for " . $form->{invnumber},
+          tax_id => 0,
+        )->add_chart_booking(
+          chart  => $tmptaxamount > 0 ? $credit : $debit,
+          credit => abs($tmptaxamount),
+          source => "Reverse Charge for " . $form->{invnumber},
+          tax_id => 0,
+      )->post;
+    # add a stable link from ap to gl
+    my %props_gl = (
+        ap_id => $form->{id},
+        gl_id => $current_transaction->id,
+      );
+    SL::DB::ApGl->new(%props_gl)->save;
+    # Record a record link from ap to gl
+    my %props_rl = (
+        from_table => 'ap',
+        from_id    => $form->{id},
+        to_table   => 'gl',
+        to_id      => $current_transaction->id,
+      );
+    SL::DB::RecordLink->new(%props_rl)->save;
+  }
 }
 
 sub delete_transaction {
@@ -390,19 +493,26 @@ sub delete_transaction {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  SL::DB->client->with_transaction(sub {
 
-  # acc_trans entries are deleted by database triggers.
-  my $query = qq|DELETE FROM ap WHERE id = ?|;
-  do_query($form, $dbh, $query, $form->{id});
+    # if tax 94 reverse charge, clear all GL bookings and links
+    my $ap_gl = SL::DB::Manager::ApGl->get_first(where => [ ap_id => $form->{id} ]);
+    my $gl_id = ref $ap_gl eq 'SL::DB::ApGl' ? $ap_gl->gl_id : undef;
 
-  my $rc = $dbh->commit;
-  $dbh->disconnect;
+    SL::DB::Manager::GLTransaction->delete_all(where => [ id    => $gl_id ])       if $gl_id;
+    SL::DB::Manager::ApGl->         delete_all(where => [ ap_id => $form->{id} ])  if $gl_id;
+    SL::DB::Manager::RecordLink->   delete_all(where => [ from_table => 'ap', to_table => 'gl', from_id => $form->{id} ]);
+    # done gl delete for tax 94 case
+
+    # begin ap delete
+    my $query = qq|DELETE FROM ap WHERE id = ?|;
+    do_query($form, SL::DB->client->dbh, $query, $form->{id});
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 
-  return $rc;
+  return 1;
 }
 
 sub ap_transactions {
@@ -417,68 +527,112 @@ sub ap_transactions {
     qq|SELECT a.id, a.invnumber, a.transdate, a.duedate, a.amount, a.paid, | .
     qq|  a.ordnumber, v.name, a.invoice, a.netamount, a.datepaid, a.notes, | .
     qq|  a.globalproject_id, a.storno, a.storno_id, a.direct_debit, | .
+    qq|  a.transaction_description, a.itime::DATE AS insertdate, | .
     qq|  pr.projectnumber AS globalprojectnumber, | .
     qq|  e.name AS employee, | .
     qq|  v.vendornumber, v.country, v.ustid, | .
     qq|  tz.description AS taxzone, | .
     qq|  pt.description AS payment_terms, | .
+    qq|  department.description AS department, | .
     qq{  ( SELECT ch.accno || ' -- ' || ch.description
            FROM acc_trans at
            LEFT JOIN chart ch ON ch.id = at.chart_id
            WHERE ch.link ~ 'AP[[:>:]]'
             AND at.trans_id = a.id
             LIMIT 1
-          ) AS charts } .
+          ) AS charts, } .
+    qq{  ( SELECT ch.accno || ' -- ' || ch.description
+           FROM acc_trans at
+           LEFT JOIN chart ch ON ch.id = at.chart_id
+           WHERE ch.link ~ 'AP_amount'
+            AND at.trans_id = a.id
+            LIMIT 1
+          ) AS debit_chart } .
     qq|FROM ap a | .
     qq|JOIN vendor v ON (a.vendor_id = v.id) | .
     qq|LEFT JOIN contacts cp ON (a.cp_id = cp.cp_id) | .
     qq|LEFT JOIN employee e ON (a.employee_id = e.id) | .
     qq|LEFT JOIN project pr ON (a.globalproject_id = pr.id) | .
     qq|LEFT JOIN tax_zones tz ON (tz.id = a.taxzone_id)| .
-    qq|LEFT JOIN payment_terms pt ON (pt.id = a.payment_id)|;
+    qq|LEFT JOIN payment_terms pt ON (pt.id = a.payment_id)| .
+    qq|LEFT JOIN department ON (department.id = a.department_id)|;
 
   my $where = '';
 
-  unless ( $::auth->assert('show_ap_transactions', 1) ) {
-    $where .= " AND NOT invoice = 'f' ";  # remove ap transactions from Sales -> Reports -> Invoices
-  };
-
   my @values;
 
-  if ($form->{vendor_id}) {
-    $where .= " AND a.vendor_id = ?";
-    push(@values, $form->{vendor_id});
-  } elsif ($form->{vendor}) {
+  # Permissions:
+  # - Always return invoices & AP transactions for projects the employee has "view invoices" permissions for, no matter what the other rules say.
+  # - Exclude AP transactions if no permissions for them exist.
+  # - Limit to own invoices unless may edit all invoices or view invoices is allowed.
+  # - If may edit all or view invoices is allowed, allow filtering by employee.
+  my (@permission_where, @permission_values);
+
+  if ($::auth->assert('vendor_invoice_edit', 1) || $::auth->assert('purchase_invoice_view', 1)) {
+    if (!$::auth->assert('show_ap_transactions', 1)) {
+      push @permission_where, "NOT invoice = 'f'"; # remove ap transactions from Purchase -> Reports -> Invoices
+    }
+
+    if (!$::auth->assert('purchase_all_edit', 1) && !$::auth->assert('purchase_invoice_view', 1)) {
+      # only show own invoices
+      push @permission_where,  "a.employee_id = ?";
+      push @permission_values, SL::DB::Manager::Employee->current->id;
+
+    } else {
+      if ($form->{employee_id}) {
+        push @permission_where,  "a.employee_id = ?";
+        push @permission_values, conv_i($form->{employee_id});
+      }
+    }
+  }
+
+  if (@permission_where || (!$::auth->assert('vendor_invoice_edit', 1) && !$::auth->assert('purchase_invoice_view', 1))) {
+    my $permission_where_str = @permission_where ? "OR (" . join(" AND ", map { "($_)" } @permission_where) . ")" : "";
+    $where .= qq|
+      AND (   (a.globalproject_id IN (
+               SELECT epi.project_id
+               FROM employee_project_invoices epi
+               WHERE epi.employee_id = ?))
+           $permission_where_str)
+    |;
+    push @values, SL::DB::Manager::Employee->current->id, @permission_values;
+  }
+
+  if ($form->{vendor}) {
     $where .= " AND v.name ILIKE ?";
-    push(@values, $form->like($form->{vendor}));
+    push(@values, like($form->{vendor}));
   }
   if ($form->{"cp_name"}) {
     $where .= " AND (cp.cp_name ILIKE ? OR cp.cp_givenname ILIKE ?)";
-    push(@values, ('%' . $form->{"cp_name"} . '%')x2);
+    push(@values, (like($form->{"cp_name"}))x2);
   }
-  if ($form->{department}) {
-    # ähnlich wie commit 0bbfb33b6aa8e38bb6c81d1684ab7d08e5b5c5af abteilung
-    # wird so nicht mehr als zeichenkette zusammengebaut
-    # hätte zu ee9f9f9aa4c3b9d5d20ab10a45c12bcaa6aa78d0 auffallen können ;-) jan
-    #my ($null, $department_id) = split /--/, $form->{department};
+  if ($form->{department_id}) {
     $where .= " AND a.department_id = ?";
-    push(@values, $form->{department});
+    push(@values, $form->{department_id});
   }
   if ($form->{invnumber}) {
     $where .= " AND a.invnumber ILIKE ?";
-    push(@values, $form->like($form->{invnumber}));
+    push(@values, like($form->{invnumber}));
   }
   if ($form->{ordnumber}) {
     $where .= " AND a.ordnumber ILIKE ?";
-    push(@values, $form->like($form->{ordnumber}));
+    push(@values, like($form->{ordnumber}));
+  }
+  if ($form->{taxzone_id}) {
+    $where .= " AND a.taxzone_id = ?";
+    push(@values, $form->{taxzone_id});
+  }
+  if ($form->{transaction_description}) {
+    $where .= " AND a.transaction_description ILIKE ?";
+    push(@values, like($form->{transaction_description}));
   }
   if ($form->{notes}) {
-    $where .= " AND lower(a.notes) LIKE ?";
-    push(@values, $form->like($form->{notes}));
+    $where .= " AND a.notes ILIKE ?";
+    push(@values, like($form->{notes}));
   }
   if ($form->{project_id}) {
     $where .=
-      qq|AND ((a.globalproject_id = ?) OR EXISTS | .
+      qq| AND ((a.globalproject_id = ?) OR EXISTS | .
       qq|  (SELECT * FROM invoice i | .
       qq|   WHERE i.project_id = ? AND i.trans_id = a.id) | .
       qq| OR EXISTS | .
@@ -490,11 +644,19 @@ sub ap_transactions {
 
   if ($form->{transdatefrom}) {
     $where .= " AND a.transdate >= ?";
-    push(@values, $form->{transdatefrom});
+    push(@values, trim($form->{transdatefrom}));
   }
   if ($form->{transdateto}) {
     $where .= " AND a.transdate <= ?";
-    push(@values, $form->{transdateto});
+    push(@values, trim($form->{transdateto}));
+  }
+  if ($form->{duedatefrom}) {
+    $where .= " AND a.duedate >= ?";
+    push(@values, trim($form->{duedatefrom}));
+  }
+  if ($form->{duedateto}) {
+    $where .= " AND a.duedate <= ?";
+    push(@values, trim($form->{duedateto}));
   }
   if ($form->{open} || $form->{closed}) {
     unless ($form->{open} && $form->{closed}) {
@@ -503,8 +665,35 @@ sub ap_transactions {
     }
   }
 
+  if ($form->{parts_partnumber}) {
+    $where .= <<SQL;
+ AND EXISTS (
+        SELECT invoice.trans_id
+        FROM invoice
+        LEFT JOIN parts ON (invoice.parts_id = parts.id)
+        WHERE (invoice.trans_id = a.id)
+          AND (parts.partnumber ILIKE ?)
+        LIMIT 1
+      )
+SQL
+    push @values, like($form->{parts_partnumber});
+  }
+
+  if ($form->{parts_description}) {
+    $where .= <<SQL;
+ AND EXISTS (
+        SELECT invoice.trans_id
+        FROM invoice
+        WHERE (invoice.trans_id = a.id)
+          AND (invoice.description ILIKE ?)
+        LIMIT 1
+      )
+SQL
+    push @values, like($form->{parts_description});
+  }
+
   if ($where) {
-    substr($where, 0, 4, " WHERE ");
+    $where  =~ s{\s*AND\s*}{ WHERE };
     $query .= $where;
   }
 
@@ -513,7 +702,7 @@ sub ap_transactions {
   my $sortdir   = !defined $form->{sortdir} ? 'ASC' : $form->{sortdir} ? 'ASC' : 'DESC';
   my $sortorder = join(', ', map { "$_ $sortdir" } @a);
 
-  if (grep({ $_ eq $form->{sort} } qw(transdate id invnumber ordnumber name netamount tax amount paid datepaid due duedate notes employee transaction_description direct_debit))) {
+  if (grep({ $_ eq $form->{sort} } qw(transdate id invnumber ordnumber name netamount tax amount paid datepaid due duedate notes employee transaction_description direct_debit department taxzone))) {
     $sortorder = $form->{sort} . " $sortdir";
   }
 
@@ -532,7 +721,7 @@ sub get_transdate {
   my ($self, $myconfig, $form) = @_;
 
   # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $query =
     "SELECT COALESCE(" .
@@ -541,8 +730,6 @@ sub get_transdate {
     "  current_date)";
   ($form->{transdate}) = $dbh->selectrow_array($query);
 
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
@@ -586,17 +773,33 @@ sub _delete_payments {
 }
 
 sub post_payment {
+  my ($self, $myconfig, $form, $locale) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_post_payment, $self, $myconfig, $form, $locale);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _post_payment {
   my ($self, $myconfig, $form, $locale) = @_;
 
-  # connect to database, turn off autocommit
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my (%payments, $old_form, $row, $item, $query, %keep_vars);
 
   $old_form = save_form();
 
+  $query = <<SQL;
+    SELECT at.acc_trans_id, at.amount, at.cleared, c.accno
+    FROM acc_trans at
+    LEFT JOIN chart c ON (at.chart_id = c.id)
+    WHERE (at.trans_id = ?)
+SQL
+
+  my %already_cleared = selectall_as_map($form, $dbh, $query, 'acc_trans_id', [ qw(amount cleared accno) ], $form->{id});
+
   # Delete all entries in acc_trans from prior payments.
   if (SL::DB::Default->get->payments_changeable != 0) {
     $self->_delete_payments($form, $dbh);
@@ -625,7 +828,7 @@ sub post_payment {
 
   # Get the AP accno.
   $query =
-    qq|SELECT c.accno
+    qq|SELECT c.id
        FROM acc_trans at
        LEFT JOIN chart c ON (at.chart_id = c.id)
        WHERE (trans_id = ?)
@@ -633,19 +836,14 @@ sub post_payment {
        ORDER BY at.acc_trans_id
        LIMIT 1|;
 
-  ($form->{APselected}) = selectfirst_array_query($form, $dbh, $query, conv_i($form->{id}));
+  ($form->{AP_chart_id}) = selectfirst_array_query($form, $dbh, $query, conv_i($form->{id}));
 
   # Post the new payments.
-  $self->post_transaction($myconfig, $form, $dbh, 1);
+  $self->post_transaction($myconfig, $form, $dbh, payments_only => 1, already_cleared => \%already_cleared);
 
   restore_form($old_form);
 
-  my $rc = $dbh->commit();
-  $dbh->disconnect();
-
-  $main::lxdebug->leave_sub();
-
-  return $rc;
+  return 1;
 }
 
 sub setup_form {
@@ -715,7 +913,7 @@ sub setup_form {
           }
 
           $index                 = $form->{acc_trans}{$key}->[$i - 1]->{index};
-          $form->{"tax_$index"}  = $form->{acc_trans}{$key}->[$i - 1]->{amount} * -1;
+          $form->{"tax_$index"}  = $form->round_amount($form->{acc_trans}{$key}->[$i - 1]->{amount} * -1 / $exchangerate, 2);
           $totaltax             += $form->{"tax_$index"};
 
         } else {
@@ -731,26 +929,7 @@ sub setup_form {
             $form->{"projectnumber_$k"}    = "$form->{acc_trans}{$key}->[$i-1]->{projectnumber}";
             $form->{"oldprojectnumber_$k"} = $form->{"projectnumber_$k"};
             $form->{"project_id_$k"}       = "$form->{acc_trans}{$key}->[$i-1]->{project_id}";
-          }
-
-          $form->{"${key}_$k"} = "$form->{acc_trans}{$key}->[$i-1]->{accno}--$form->{acc_trans}{$key}->[$i-1]->{description}";
-
-          my $q_description    = quotemeta($form->{acc_trans}{$key}->[$i-1]->{description});
-          $form->{"select${key}"} =~
-            m/<option value=\"
-                ($form->{acc_trans}{$key}->[$i-1]->{accno}--[^\"]*)
-              \">
-              $form->{acc_trans}{$key}->[$i-1]->{accno}
-              --
-              ${q_description}
-              <\/option>\n/x;
-          $form->{"${key}_$k"} = $1;
-
-          if ($akey eq "AP") {
-            $form->{APselected} = $form->{acc_trans}{$key}->[$i-1]->{accno};
-
-          } elsif ($akey eq 'amount') {
-            $form->{"${key}_$k"}   = $form->{acc_trans}{$key}->[$i-1]->{accno} . "--" . $form->{acc_trans}{$key}->[$i-1]->{id};
+            $form->{"${key}_chart_id_$k"}  = $form->{acc_trans}{$key}->[$i-1]->{chart_id};
             $form->{"taxchart_$k"} = $form->{acc_trans}{$key}->[$i-1]->{id}    . "--" . $form->{acc_trans}{$key}->[$i-1]->{rate};
           }
         }
@@ -779,12 +958,20 @@ sub setup_form {
 }
 
 sub storno {
+  my ($self, $form, $myconfig, $id) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_storno, $self, $form, $myconfig, $id);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _storno {
   my ($self, $form, $myconfig, $id) = @_;
 
   my ($query, $new_id, $storno_row, $acc_trans_rows);
-  my $dbh = $form->get_standard_dbh($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   $query = qq|SELECT nextval('glid')|;
   ($new_id) = selectrow_query($form, $dbh, $query);
@@ -800,7 +987,7 @@ sub storno {
   $storno_row->{netamount} *= -1;
   $storno_row->{paid}       = $storno_row->{amount};
 
-  delete @$storno_row{qw(itime mtime)};
+  delete @$storno_row{qw(itime mtime gldate)};
 
   $query = sprintf 'INSERT INTO ap (%s) VALUES (%s)', join(', ', keys %$storno_row), join(', ', map '?', values %$storno_row);
   do_query($form, $dbh, $query, (values %$storno_row));
@@ -808,6 +995,8 @@ sub storno {
   $query = qq|UPDATE ap SET paid = amount + paid, storno = 't' WHERE id = ?|;
   do_query($form, $dbh, $query, $id);
 
+  $form->new_lastmtime('ap') if $id == $form->{id};
+
   # now copy acc_trans entries
   $query = qq|SELECT a.*, c.link FROM acc_trans a LEFT JOIN chart c ON a.chart_id = c.id WHERE a.trans_id = ? ORDER BY a.acc_trans_id|;
   my $rowref = selectall_hashref_query($form, $dbh, $query, $id);
@@ -818,7 +1007,7 @@ sub storno {
   }
 
   for my $row (@$rowref) {
-    delete @$row{qw(itime mtime link acc_trans_id)};
+    delete @$row{qw(itime mtime link acc_trans_id gldate)};
     $query = sprintf 'INSERT INTO acc_trans (%s) VALUES (%s)', join(', ', keys %$row), join(', ', map '?', values %$row);
     $row->{trans_id}   = $new_id;
     $row->{amount}    *= -1;
@@ -827,9 +1016,7 @@ sub storno {
 
   map { IO->set_datepaid(table => 'ap', id => $_, dbh => $dbh) } ($id, $new_id);
 
-  $dbh->commit;
-
-  $main::lxdebug->leave_sub();
+  return 1;
 }
 
 1;
index 2841743..db8a4ac 100644 (file)
--- a/SL/AR.pm
+++ b/SL/AR.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Accounts Receivable module backend routines
@@ -37,17 +38,30 @@ package AR;
 use Data::Dumper;
 use SL::DATEV qw(:CONSTANTS);
 use SL::DBUtils;
+use SL::DB::Draft;
 use SL::IO;
 use SL::MoreCommon;
 use SL::DB::Default;
 use SL::TransNumber;
+use SL::Util qw(trim);
+use SL::DB;
 
 use strict;
 
 sub post_transaction {
+  my ($self, $myconfig, $form, $provided_dbh, %params) = @_;
   $main::lxdebug->enter_sub();
 
-  my ($self, $myconfig, $form, $provided_dbh, $payments_only) = @_;
+  my $rc = SL::DB->client->with_transaction(\&_post_transaction, $self, $myconfig, $form, $provided_dbh, %params);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _post_transaction {
+  my ($self, $myconfig, $form, $provided_dbh, %params) = @_;
+
+  my $payments_only = $params{payments_only};
 
   my ($query, $sth, $null, $taxrate, $amount, $tax);
   my $exchangerate = 0;
@@ -55,7 +69,7 @@ sub post_transaction {
 
   my @values;
 
-  my $dbh = $provided_dbh ? $provided_dbh : $form->dbconnect_noauto($myconfig);
+  my $dbh = $provided_dbh || SL::DB->client->dbh;
   $form->{defaultcurrency} = $form->get_default_currency($myconfig);
 
   # set exchangerate
@@ -64,10 +78,7 @@ sub post_transaction {
         $form->parse_amount($myconfig, $form->{exchangerate}) );
 
   # get the charts selected
-  map { ($form->{AR_amounts}{"amount_$_"}) = split /--/, $form->{"AR_amount_$_"} } 1 .. $form->{rowcount};
-
-  $form->{AR_amounts}{receivables} = $form->{ARselected};
-  $form->{AR}{receivables}         = $form->{ARselected};
+  $form->{AR_amounts}{"amount_$_"} = $form->{"AR_amount_chart_id_$_"} for (1 .. $form->{rowcount});
 
   $form->{tax}       = 0; # is this still needed?
 
@@ -91,8 +102,6 @@ sub post_transaction {
   }
   $form->{paid}   = $form->round_amount($form->{paid} * ($form->{exchangerate} || 1), 2);
 
-  ($null, $form->{employee_id}) = split /--/, $form->{employee};
-
   $form->get_employee($dbh) unless $form->{employee_id};
 
   # if we have an id delete old records else make one
@@ -114,9 +123,6 @@ sub post_transaction {
     }
   }
 
-  # update department
-  ($null, $form->{department_id}) = split(/--/, $form->{department});
-
   # amount for AR account
   $form->{receivables} = $form->round_amount($form->{amount}, 2) * -1;
 
@@ -128,15 +134,19 @@ sub post_transaction {
     $query =
       qq|UPDATE ar set
            invnumber = ?, ordnumber = ?, transdate = ?, customer_id = ?,
-           taxincluded = ?, amount = ?, duedate = ?, paid = ?,
+           taxincluded = ?, amount = ?, duedate = ?, deliverydate = ?, tax_point = ?, paid = ?,
+           currency_id = (SELECT id FROM currencies WHERE name = ?),
            netamount = ?, notes = ?, department_id = ?,
            employee_id = ?, storno = ?, storno_id = ?, globalproject_id = ?,
-           direct_debit = ?
+           direct_debit = ?, transaction_description = ?
          WHERE id = ?|;
     my @values = ($form->{invnumber}, $form->{ordnumber}, conv_date($form->{transdate}), conv_i($form->{customer_id}), $form->{taxincluded} ? 't' : 'f', $form->{amount},
-                  conv_date($form->{duedate}), $form->{paid}, $form->{netamount}, $form->{notes}, conv_i($form->{department_id}),
+                  conv_date($form->{duedate}), conv_date($form->{deliverydate}), conv_date($form->{tax_point}), $form->{paid},
+                  $form->{currency},
+                  $form->{netamount}, $form->{notes}, conv_i($form->{department_id}),
                   conv_i($form->{employee_id}), $form->{storno} ? 't' : 'f', $form->{storno_id},
-                  conv_i($form->{globalproject_id}), $form->{direct_debit} ? 't' : 'f', conv_i($form->{id}));
+                  conv_i($form->{globalproject_id}), $form->{direct_debit} ? 't' : 'f', $form->{transaction_description},
+                  conv_i($form->{id}));
     do_query($form, $dbh, $query, @values);
 
     # add individual transactions for AR, amount and taxes
@@ -146,7 +156,7 @@ sub post_transaction {
 
         # insert detail records in acc_trans
         $query = qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, project_id, taxkey, tax_id, chart_link)
-                     VALUES (?, (SELECT c.id FROM chart c WHERE c.accno = ?), ?, ?, ?, ?, ?, (SELECT c.link FROM chart c WHERE c.accno = ?))|;
+                     VALUES (?, ?, ?, ?, ?, ?, ?, (SELECT c.link FROM chart c WHERE c.id = ?))|;
         @values = (conv_i($form->{id}), $form->{AR_amounts}{"amount_$i"}, conv_i($form->{"amount_$i"}), conv_date($form->{transdate}), $project_id,
                    conv_i($form->{"taxkey_$i"}), conv_i($form->{"tax_id_$i"}), $form->{AR_amounts}{"amount_$i"});
         do_query($form, $dbh, $query, @values);
@@ -164,17 +174,15 @@ sub post_transaction {
 
     # add recievables
     $query = qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, taxkey, tax_id, chart_link)
-                 VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?, (SELECT taxkey_id FROM chart WHERE accno = ?),
+                 VALUES (?, ?, ?, ?, (SELECT taxkey_id FROM chart WHERE id = ?),
                  (SELECT tax_id
                   FROM taxkeys
-                  WHERE chart_id= (SELECT id
-                                   FROM chart
-                                   WHERE accno = ?)
+                  WHERE chart_id = ?
                   AND startdate <= ?
                   ORDER BY startdate DESC LIMIT 1),
-                 (SELECT c.link FROM chart c WHERE c.accno = ?))|;
-    @values = (conv_i($form->{id}), $form->{AR_amounts}{receivables}, conv_i($form->{receivables}), conv_date($form->{transdate}),
-                $form->{AR_amounts}{receivables}, $form->{AR_amounts}{receivables}, conv_date($form->{transdate}), $form->{AR_amounts}{receivables});
+                 (SELECT c.link FROM chart c WHERE c.id = ?))|;
+    @values = (conv_i($form->{id}), $form->{AR_chart_id}, conv_i($form->{receivables}), conv_date($form->{transdate}),
+                $form->{AR_chart_id}, $form->{AR_chart_id}, conv_date($form->{transdate}), $form->{AR_chart_id});
     do_query($form, $dbh, $query, @values);
 
   } else {
@@ -183,6 +191,10 @@ sub post_transaction {
     do_query($form, $dbh, $query,  $form->{paid}, $form->{paid} ? conv_date($form->{datepaid}) : undef, conv_i($form->{id}));
   }
 
+  $form->new_lastmtime('ar');
+
+  my %already_cleared = %{ $params{already_cleared} // {} };
+
   # add paid transactions
   for my $i (1 .. $form->{paidaccounts}) {
 
@@ -206,23 +218,28 @@ sub post_transaction {
       $form->{exchangerate} = $form->{"exchangerate_$i"}
         if ($form->{amount} == 0 && $form->{netamount} == 0);
 
+      my $new_cleared = !$form->{"acc_trans_id_$i"}                                                       ? 'f'
+                      : !$already_cleared{$form->{"acc_trans_id_$i"}}                                     ? 'f'
+                      : $already_cleared{$form->{"acc_trans_id_$i"}}->{amount} != $form->{"paid_$i"} * -1 ? 'f'
+                      : $already_cleared{$form->{"acc_trans_id_$i"}}->{accno}  != $form->{AR}{"paid_$i"}  ? 'f'
+                      : $already_cleared{$form->{"acc_trans_id_$i"}}->{cleared}                           ? 't'
+                      :                                                                                     'f';
+
       # receivables amount
       $amount = $form->round_amount($form->{"paid_$i"} * $form->{exchangerate}, 2);
 
       if ($amount != 0) {
         # add receivable
-        $query = qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, project_id, taxkey, tax_id, chart_link)
-                     VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?, ?, (SELECT taxkey_id FROM chart WHERE accno = ?),
+        $query = qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, project_id, cleared, taxkey, tax_id, chart_link)
+                     VALUES (?, ?, ?, ?, ?, ?, (SELECT taxkey_id FROM chart WHERE id = ?),
                      (SELECT tax_id
                       FROM taxkeys
-                      WHERE chart_id= (SELECT id
-                                       FROM chart
-                                       WHERE accno = ?)
+                      WHERE chart_id = ?
                       AND startdate <= ?
                       ORDER BY startdate DESC LIMIT 1),
-                     (SELECT c.link FROM chart c WHERE c.accno = ?))|;
-        @values = (conv_i($form->{id}), $form->{AR}{receivables}, $amount, conv_date($form->{"datepaid_$i"}), $project_id, $form->{AR}{receivables}, $form->{AR}{receivables}, conv_date($form->{"datepaid_$i"}),
-        $form->{AR}{receivables});
+                     (SELECT c.link FROM chart c WHERE c.id = ?))|;
+        @values = (conv_i($form->{id}), $form->{AR_chart_id}, $amount, conv_date($form->{"datepaid_$i"}), $project_id, $new_cleared,
+                   $form->{AR_chart_id}, $form->{AR_chart_id}, conv_date($form->{"datepaid_$i"}), $form->{AR_chart_id});
 
         do_query($form, $dbh, $query, @values);
       }
@@ -232,8 +249,8 @@ sub post_transaction {
         my $project_id = conv_i($form->{"paid_project_id_$i"});
         my $gldate = (conv_date($form->{"gldate_$i"}))? conv_date($form->{"gldate_$i"}) : conv_date($form->current_date($myconfig));
         $amount = $form->{"paid_$i"} * -1;
-        $query  = qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, gldate, source, memo, project_id, taxkey, tax_id, chart_link)
-                     VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?, ?, ?, ?, ?, (SELECT taxkey_id FROM chart WHERE accno = ?),
+        $query  = qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, gldate, source, memo, project_id, cleared, taxkey, tax_id, chart_link)
+                     VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?, ?, ?, ?, ?, ?, (SELECT taxkey_id FROM chart WHERE accno = ?),
                      (SELECT tax_id
                       FROM taxkeys
                       WHERE chart_id= (SELECT id
@@ -242,7 +259,7 @@ sub post_transaction {
                       AND startdate <= ?
                       ORDER BY startdate DESC LIMIT 1),
                      (SELECT c.link FROM chart c WHERE c.accno = ?))|;
-        @values = (conv_i($form->{id}), $form->{AR}{"paid_$i"}, $amount, conv_date($form->{"datepaid_$i"}), $gldate, $form->{"source_$i"}, $form->{"memo_$i"}, $project_id, $form->{AR}{"paid_$i"},
+        @values = (conv_i($form->{id}), $form->{AR}{"paid_$i"}, $amount, conv_date($form->{"datepaid_$i"}), $gldate, $form->{"source_$i"}, $form->{"memo_$i"}, $project_id, $new_cleared, $form->{AR}{"paid_$i"},
                     $form->{AR}{"paid_$i"}, conv_date($form->{"datepaid_$i"}), $form->{AR}{"paid_$i"});
         do_query($form, $dbh, $query, @values);
 
@@ -269,6 +286,15 @@ sub post_transaction {
         $amount = $form->round_amount( $form->{"paid_$i"} * ($form->{exchangerate} - $form->{"exchangerate_$i"}) * -1, 2);
 
         if ($amount != 0) {
+          # fetch fxgain and fxloss chart info from defaults if charts aren't already filled in form
+          if ( !$form->{fxgain_accno} && $::instance_conf->get_fxgain_accno_id ) {
+            $form->{fxgain_accno} = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_fxgain_accno_id)->accno;
+          };
+          if ( !$form->{fxloss_accno} && $::instance_conf->get_fxloss_accno_id ) {
+            $form->{fxloss_accno} = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_fxloss_accno_id)->accno;
+          };
+          die "fxloss_accno missing" if $amount < 0 and not $form->{fxloss_accno};
+          die "fxgain_accno missing" if $amount > 0 and not $form->{fxgain_accno};
           my $accno = ($amount > 0) ? $form->{fxgain_accno} : $form->{fxloss_accno};
           $query = qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, fx_transaction, cleared, project_id, taxkey, tax_id, chart_link)
                        VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?, 't', 'f', ?, (SELECT taxkey_id FROM chart WHERE accno = ?),
@@ -293,33 +319,25 @@ sub post_transaction {
 
   IO->set_datepaid(table => 'ar', id => $form->{id}, dbh => $dbh);
 
+  if ($form->{draft_id}) {
+    SL::DB::Manager::Draft->delete_all(where => [ id => delete($form->{draft_id}) ]);
+  }
+
   # safety check datev export
   if ($::instance_conf->get_datev_check_on_ar_transaction) {
-    my $transdate = $::form->{transdate} ? DateTime->from_lxoffice($::form->{transdate}) : undef;
-    $transdate  ||= DateTime->today;
-
     my $datev = SL::DATEV->new(
-      exporttype => DATEV_ET_BUCHUNGEN,
-      format     => DATEV_FORMAT_KNE,
       dbh        => $dbh,
       trans_id   => $form->{id},
     );
 
-    $datev->export;
+    $datev->generate_datev_data;
 
     if ($datev->errors) {
-      $dbh->rollback;
       die join "\n", $::locale->text('DATEV check returned errors:'), $datev->errors;
     }
   }
 
-  my $rc = 1;
-  if (!$provided_dbh) {
-    $rc = $dbh->commit();
-    $dbh->disconnect();
-  }
-
-  $main::lxdebug->leave_sub() and return $rc;
+  return 1;
 }
 
 sub _delete_payments {
@@ -362,17 +380,33 @@ sub _delete_payments {
 }
 
 sub post_payment {
+  my ($self, $myconfig, $form, $locale) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_post_payment, $self, $myconfig, $form, $locale);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _post_payment {
   my ($self, $myconfig, $form, $locale) = @_;
 
-  # connect to database, turn off autocommit
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my (%payments, $old_form, $row, $item, $query, %keep_vars);
 
   $old_form = save_form();
 
+  $query = <<SQL;
+    SELECT at.acc_trans_id, at.amount, at.cleared, c.accno
+    FROM acc_trans at
+    LEFT JOIN chart c ON (at.chart_id = c.id)
+    WHERE (at.trans_id = ?)
+SQL
+
+  my %already_cleared = selectall_as_map($form, $dbh, $query, 'acc_trans_id', [ qw(amount cleared accno) ], $form->{id});
+
   # Delete all entries in acc_trans from prior payments.
   if (SL::DB::Default->get->payments_changeable != 0) {
     $self->_delete_payments($form, $dbh);
@@ -399,9 +433,9 @@ sub post_payment {
   $form->{exchangerate}    = $form->format_amount($myconfig, $form->{exchangerate});
   $form->{defaultcurrency} = $form->get_default_currency($myconfig);
 
-  # Get the AR accno (which is normally done by Form::create_links()).
+  # Get the AR chart ID (which is normally done by Form::create_links()).
   $query =
-    qq|SELECT c.accno
+    qq|SELECT c.id
        FROM acc_trans at
        LEFT JOIN chart c ON (at.chart_id = c.id)
        WHERE (trans_id = ?)
@@ -409,19 +443,14 @@ sub post_payment {
        ORDER BY at.acc_trans_id
        LIMIT 1|;
 
-  ($form->{ARselected}) = selectfirst_array_query($form, $dbh, $query, conv_i($form->{id}));
+  ($form->{AR_chart_id}) = selectfirst_array_query($form, $dbh, $query, conv_i($form->{id}));
 
   # Post the new payments.
-  $self->post_transaction($myconfig, $form, $dbh, 1);
+  $self->post_transaction($myconfig, $form, $dbh, payments_only => 1, already_cleared => \%already_cleared);
 
   restore_form($old_form);
 
-  my $rc = $dbh->commit();
-  $dbh->disconnect();
-
-  $main::lxdebug->leave_sub();
-
-  return $rc;
+  return 1;
 }
 
 sub delete_transaction {
@@ -429,20 +458,16 @@ sub delete_transaction {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database, turn AutoCommit off
-  my $dbh = $form->dbconnect_noauto($myconfig);
-
-  # acc_trans entries are deleted by database triggers.
-  my $query = qq|DELETE FROM ar WHERE id = ?|;
-  do_query($form, $dbh, $query, $form->{id});
-
-  # commit
-  my $rc = $dbh->commit;
-  $dbh->disconnect;
+  SL::DB->client->with_transaction(sub {
+    # acc_trans entries are deleted by database triggers.
+    my $query = qq|DELETE FROM ar WHERE id = ?|;
+    do_query($form, SL::DB->client->dbh, $query, $form->{id});
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 
-  return $rc;
+  return 1;
 }
 
 sub ar_transactions {
@@ -457,11 +482,13 @@ sub ar_transactions {
 
   my $query =
     qq|SELECT DISTINCT a.id, a.invnumber, a.ordnumber, a.cusordnumber, a.transdate, | .
+    qq|  a.donumber, a.deliverydate, | .
     qq|  a.duedate, a.netamount, a.amount, a.paid, | .
     qq|  a.invoice, a.datepaid, a.notes, a.shipvia, | .
     qq|  a.shippingpoint, a.storno, a.storno_id, a.globalproject_id, | .
     qq|  a.marge_total, a.marge_percent, | .
     qq|  a.transaction_description, a.direct_debit, | .
+    qq|  a.type, | .
     qq|  pr.projectnumber AS globalprojectnumber, | .
     qq|  c.name, c.customernumber, c.country, c.ustid, b.description as customertype, | .
     qq|  c.id as customer_id, | .
@@ -470,6 +497,7 @@ sub ar_transactions {
     qq|  dc.dunning_description, | .
     qq|  tz.description AS taxzone, | .
     qq|  pt.description AS payment_terms, | .
+    qq|  d.description AS department, | .
     qq{  ( SELECT ch.accno || ' -- ' || ch.description
            FROM acc_trans at
            LEFT JOIN chart ch ON ch.id = at.chart_id
@@ -491,44 +519,68 @@ sub ar_transactions {
 
   my $where = "1 = 1";
 
-  unless ( $::auth->assert('show_ar_transactions', 1) ) {
-    $where .= " AND NOT invoice = 'f' ";  # remove ar transactions from Sales -> Reports -> Invoices
-  };
+  # Permissions:
+  # - Always return invoices & AR transactions for projects the employee has "view invoices" permissions for, no matter what the other rules say.
+  # - Exclude AR transactions if no permissions for them exist.
+  # - Limit to own invoices unless may edit all invoices or view invoices is allowed.
+  # - If may edit all or view invoices is allowed, allow filtering by employee/salesman.
+  my (@permission_where, @permission_values);
 
-  if ($form->{customernumber}) {
-    $where .= " AND c.customernumber = ?";
-    push(@values, $form->{customernumber});
+  if ($::auth->assert('invoice_edit', 1) || $::auth->assert('sales_invoice_view', 1)) {
+    if (!$::auth->assert('show_ar_transactions', 1) ) {
+      push @permission_where, "NOT invoice = 'f'";  # remove ar transactions from Sales -> Reports -> Invoices
+    }
+
+    if (!$::auth->assert('sales_all_edit', 1) && !$::auth->assert('sales_invoice_view', 1)) {
+      # only show own invoices
+      push @permission_where,  "a.employee_id = ?";
+      push @permission_values, SL::DB::Manager::Employee->current->id;
+
+    } else {
+      if ($form->{employee_id}) {
+        push @permission_where,  "a.employee_id = ?";
+        push @permission_values, conv_i($form->{employee_id});
+      }
+      if ($form->{salesman_id}) {
+        push @permission_where,  "a.salesman_id = ?";
+        push @permission_values, conv_i($form->{salesman_id});
+      }
+    }
+  }
+
+  if (@permission_where || (!$::auth->assert('invoice_edit', 1) && !$::auth->assert('sales_invoice_view', 1))) {
+    my $permission_where_str = @permission_where ? "OR (" . join(" AND ", map { "($_)" } @permission_where) . ")" : "";
+    $where .= qq|
+      AND (   (a.globalproject_id IN (
+               SELECT epi.project_id
+               FROM employee_project_invoices epi
+               WHERE epi.employee_id = ?))
+           $permission_where_str)
+    |;
+    push @values, SL::DB::Manager::Employee->current->id, @permission_values;
   }
-  if ($form->{customer_id}) {
-    $where .= " AND a.customer_id = ?";
-    push(@values, $form->{customer_id});
-  } elsif ($form->{customer}) {
+
+  if ($form->{customer}) {
     $where .= " AND c.name ILIKE ?";
-    push(@values, $form->like($form->{customer}));
+    push(@values, like($form->{customer}));
   }
   if ($form->{"cp_name"}) {
     $where .= " AND (cp.cp_name ILIKE ? OR cp.cp_givenname ILIKE ?)";
-    push(@values, ('%' . $form->{"cp_name"} . '%')x2);
+    push(@values, (like($form->{"cp_name"}))x2);
   }
   if ($form->{business_id}) {
     my $business_id = $form->{business_id};
     $where .= " AND c.business_id = ?";
     push(@values, $business_id);
   }
-  if ($form->{department_id}) {
-    my $department_id = $form->{department_id};
-    $where .= " AND a.department_id = ?";
-    push(@values, $department_id);
-  }
-  if ($form->{department}) {
-    my $department = "%" . $form->{department} . "%";
-    $where .= " AND d.description ILIKE ?";
-    push(@values, $department);
+  if ($form->{taxzone_id}) {
+    $where .= " AND a.taxzone_id = ?";
+    push(@values, $form->{taxzone_id});
   }
-  foreach my $column (qw(invnumber ordnumber cusordnumber notes transaction_description)) {
+  foreach my $column (qw(invnumber ordnumber cusordnumber notes transaction_description shipvia shippingpoint)) {
     if ($form->{$column}) {
       $where .= " AND a.$column ILIKE ?";
-      push(@values, $form->like($form->{$column}));
+      push(@values, like($form->{$column}));
     }
   }
   if ($form->{"project_id"}) {
@@ -545,19 +597,19 @@ sub ar_transactions {
 
   if ($form->{transdatefrom}) {
     $where .= " AND a.transdate >= ?";
-    push(@values, $form->{transdatefrom});
+    push(@values, trim($form->{transdatefrom}));
   }
   if ($form->{transdateto}) {
     $where .= " AND a.transdate <= ?";
-    push(@values, $form->{transdateto});
+    push(@values, trim($form->{transdateto}));
   }
   if ($form->{duedatefrom}) {
     $where .= " AND a.duedate >= ?";
-    push(@values, $form->{duedatefrom});
+    push(@values, trim($form->{duedatefrom}));
   }
   if ($form->{duedateto}) {
     $where .= " AND a.duedate <= ?";
-    push(@values, $form->{duedateto});
+    push(@values, trim($form->{duedateto}));
   }
   if ($form->{open} || $form->{closed}) {
     unless ($form->{open} && $form->{closed}) {
@@ -566,20 +618,58 @@ sub ar_transactions {
     }
   }
 
-  if (!$main::auth->assert('sales_all_edit', 1)) {
-    # only show own invoices
-    $where .= " AND a.employee_id = (select id from employee where login= ?)";
-    push (@values, $::myconfig{login});
-  } else {
-    if ($form->{employee_id}) {
-      $where .= " AND a.employee_id = ?";
-      push @values, conv_i($form->{employee_id});
-    }
-    if ($form->{salesman_id}) {
-      $where .= " AND a.salesman_id = ?";
-      push @values, conv_i($form->{salesman_id});
-    }
-  };
+  if ($form->{parts_partnumber}) {
+    $where .= <<SQL;
+      AND EXISTS (
+        SELECT invoice.trans_id
+        FROM invoice
+        LEFT JOIN parts ON (invoice.parts_id = parts.id)
+        WHERE (invoice.trans_id = a.id)
+          AND (parts.partnumber ILIKE ?)
+        LIMIT 1
+      )
+SQL
+    push @values, like($form->{parts_partnumber});
+  }
+
+  if ($form->{parts_description}) {
+    $where .= <<SQL;
+      AND EXISTS (
+        SELECT invoice.trans_id
+        FROM invoice
+        WHERE (invoice.trans_id = a.id)
+          AND (invoice.description ILIKE ?)
+        LIMIT 1
+      )
+SQL
+    push @values, like($form->{parts_description});
+  }
+
+  if ($form->{show_not_mailed}) {
+    $where .= <<SQL;
+      AND NOT EXISTS (
+        SELECT rl.to_id
+        FROM record_links rl
+        WHERE (rl.from_id = a.id)
+          AND (rl.to_table = 'email_journal')
+        LIMIT 1
+      )
+SQL
+  }
+
+  if ($form->{show_marked_as_closed}) {
+    $query .= '
+      LEFT JOIN (
+              SELECT SUM(acc_trans.amount) AS amount, trans_id
+              FROM acc_trans
+              LEFT JOIN chart ON chart.id = chart_id
+              WHERE chart.link ILIKE ?
+              GROUP BY trans_id
+      ) AS paid_difference ON (paid_difference.trans_id = a.id)
+    ';
+    unshift @values, '%AR_paid%';
+    $where .= ' AND COALESCE(paid_difference.amount, 0) + a.paid != 0';
+  }
 
   my ($cvar_where, @cvar_values) = CVar->build_filter_query('module'         => 'CT',
                                                             'trans_id_field' => 'c.id',
@@ -595,7 +685,7 @@ sub ar_transactions {
   my $sortdir   = !defined $form->{sortdir} ? 'ASC' : $form->{sortdir} ? 'ASC' : 'DESC';
   my $sortorder = join(', ', map { "$_ $sortdir" } @a);
 
-  if (grep({ $_ eq $form->{sort} } qw(id transdate duedate invnumber ordnumber cusordnumber name datepaid employee shippingpoint shipvia transaction_description))) {
+  if (grep({ $_ eq $form->{sort} } qw(id transdate duedate invnumber ordnumber cusordnumber donumber deliverydate name datepaid employee shippingpoint shipvia transaction_description department taxzone))) {
     $sortorder = $form->{sort} . " $sortdir";
   }
 
@@ -614,7 +704,7 @@ sub get_transdate {
   my ($self, $myconfig, $form) = @_;
 
   # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $query =
     "SELECT COALESCE(" .
@@ -623,8 +713,6 @@ sub get_transdate {
     "  current_date)";
   ($form->{transdate}) = $dbh->selectrow_array($query);
 
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
@@ -640,6 +728,7 @@ sub setup_form {
   $form->{forex} = $form->{exchangerate};
   $exchangerate  = $form->{exchangerate} ? $form->{exchangerate} : 1;
 
+  # expected keys: AR, AR_paid, AR_tax, AR_amount
   foreach my $key (keys %{ $form->{AR_links} }) {
     $j = 0;
     $k = 0;
@@ -668,15 +757,17 @@ sub setup_form {
         $form->{"paid_project_id_$j"} = $form->{acc_trans}{$key}->[$i - 1]->{project_id};
         $form->{paidaccounts}++;
 
-      } else {
+      } else { # e.g. AR_amount, AR, AR_tax
 
         $akey = $key;
-        $akey =~ s/AR_//;
+        $akey =~ s/AR_//; # e.g. tax, amount, AR, used to store form key tax_$i, amount_$i, ...
 
-        if ($key eq "AR_tax" || $key eq "AP_tax") {
+        if ($key eq "AR_tax" || $key eq "AP_tax") { # AR_tax
           $form->{"${key}_$form->{acc_trans}{$key}->[$i-1]->{accno}"}  = "$form->{acc_trans}{$key}->[$i-1]->{accno}--$form->{acc_trans}{$key}->[$i-1]->{description}";
+          # determine the rounded tax amounts for each account, e.g. tax_1776
           $form->{"${akey}_$form->{acc_trans}{$key}->[$i-1]->{accno}"} = $form->round_amount($form->{acc_trans}{$key}->[$i - 1]->{amount} / $exchangerate, 2);
 
+          # check e.g. $form->{1776_rate}, does this make sense for AR_tax charts? Is this ever valid? If it was, totaltax would be calculated twice
           if ($form->{"$form->{acc_trans}{$key}->[$i-1]->{accno}_rate"} > 0) {
             $totaltax += $form->{"${akey}_$form->{acc_trans}{$key}->[$i-1]->{accno}"};
             $taxrate  += $form->{"$form->{acc_trans}{$key}->[$i-1]->{accno}_rate"};
@@ -687,10 +778,11 @@ sub setup_form {
           }
 
           $index                 = $form->{acc_trans}{$key}->[$i - 1]->{index};
-          $form->{"tax_$index"}  = $form->{acc_trans}{$key}->[$i - 1]->{amount};
+          $form->{"tax_$index"}  = $form->round_amount($form->{acc_trans}{$key}->[$i - 1]->{amount} / $exchangerate, 2); # convert the tax_$i amounts
+          # currently totaltax is the sum of rounded tax amounts, is this correct?
           $totaltax             += $form->{"tax_$index"};
 
-        } else {
+        } else { # e.g. AR_amount, AR
           $k++;
           $form->{"${akey}_$k"} = $form->round_amount($form->{acc_trans}{$key}->[$i - 1]->{amount} / $exchangerate, 2);
 
@@ -702,15 +794,8 @@ sub setup_form {
             $form->{"projectnumber_$k"}    = $form->{acc_trans}{$key}->[$i-1]->{projectnumber};
             $form->{taxrate}               = $form->{acc_trans}{$key}->[$i - 1]->{rate};
             $form->{"project_id_$k"}       = $form->{acc_trans}{$key}->[$i-1]->{project_id};
-          }
 
-          $form->{"${key}_$i"} = "$form->{acc_trans}{$key}->[$i-1]->{accno}--$form->{acc_trans}{$key}->[$i-1]->{description}";
-
-          if ($akey eq "AR") {
-            $form->{ARselected} = $form->{acc_trans}{$key}->[$i-1]->{accno};
-
-          } elsif ($akey eq "amount") {
-            $form->{"${key}_$k"}   = $form->{acc_trans}{$key}->[$i-1]->{accno} . "--" . $form->{acc_trans}{$key}->[$i-1]->{id};
+            $form->{"${key}_chart_id_$k"} = $form->{acc_trans}{$key}->[$i-1]->{chart_id};
             $form->{"taxchart_$k"} = $form->{acc_trans}{$key}->[$i-1]->{id}    . "--" . $form->{acc_trans}{$key}->[$i-1]->{rate};
           }
         }
@@ -741,12 +826,21 @@ sub setup_form {
 }
 
 sub storno {
+  my ($self, $form, $myconfig, $id) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_storno, $self, $form, $myconfig, $id);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+
+sub _storno {
   my ($self, $form, $myconfig, $id) = @_;
 
   my ($query, $new_id, $storno_row, $acc_trans_rows);
-  my $dbh = $form->get_standard_dbh($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   $query = qq|SELECT nextval('glid')|;
   ($new_id) = selectrow_query($form, $dbh, $query);
@@ -770,6 +864,8 @@ sub storno {
   $query = qq|UPDATE ar SET paid = amount + paid, storno = 't' WHERE id = ?|;
   do_query($form, $dbh, $query, $id);
 
+  $form->new_lastmtime('ar') if $id == $form->{id};
+
   # now copy acc_trans entries
   $query = qq|SELECT a.*, c.link FROM acc_trans a LEFT JOIN chart c ON a.chart_id = c.id WHERE a.trans_id = ? ORDER BY a.acc_trans_id|;
   my $rowref = selectall_hashref_query($form, $dbh, $query, $id);
@@ -789,9 +885,7 @@ sub storno {
 
   map { IO->set_datepaid(table => 'ar', id => $_, dbh => $dbh) } ($id, $new_id);
 
-  $dbh->commit;
-
-  $main::lxdebug->leave_sub();
+  return 1;
 }
 
 
index dd2bd8e..a2d7864 100644 (file)
@@ -4,6 +4,7 @@ use SL::AM;
 use SL::Common;
 use SL::DBUtils;
 use SL::MoreCommon;
+use SL::DB;
 use Data::Dumper;
 
 use strict;
@@ -19,7 +20,7 @@ sub close_orders_if_billed {
   my $myconfig  = \%main::myconfig;
   my $form      = $main::form;
 
-  my $dbh       = $params{dbh} || $form->get_standard_dbh($myconfig);
+  my $dbh       = $params{dbh} || SL::DB->client->dbh;
 
   # First, find all order IDs from which this invoice has been
   # created. Either directly by a conversion from an order to this invoice
@@ -60,13 +61,14 @@ sub close_orders_if_billed {
   my $q_billed  = qq|SELECT i.parts_id, i.qty ${qtyfactor} AS qty, i.unit, p.unit AS partunit
                      FROM invoice i
                      LEFT JOIN parts p ON (i.parts_id = p.id)
-                     WHERE i.trans_id = ?|;
+                     WHERE i.trans_id = ? AND i.assemblyitem is false|;
   my $h_billed  = prepare_query($form, $dbh, $q_billed);
 
   my $q_ordered = qq|SELECT oi.parts_id, oi.qty, oi.unit, p.unit AS partunit
                       FROM orderitems oi
                       LEFT JOIN parts p ON (oi.parts_id = p.id)
-                      WHERE oi.trans_id = ?|;
+                      WHERE oi.trans_id = ?
+                      AND not oi.optional|;
   my $h_ordered = prepare_query($form, $dbh, $q_ordered);
 
   my @close_oe_ids;
@@ -75,6 +77,10 @@ sub close_orders_if_billed {
   # said order. Again consider both direct conversions and indirect
   # conversions via delivery orders.
   foreach my $oe_id (@oe_ids) {
+
+    # Dont close orders with periodic invoice
+    next if SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $oe_id);
+
     # Direct conversions "order -> invoice":
     @links          = RecordLinks->get_links('dbh'        => $dbh,
                                              'from_table' => 'oe',
@@ -150,10 +156,11 @@ sub close_orders_if_billed {
 
   # Close orders that have been billed fully.
   if (scalar @close_oe_ids) {
-    my $query = qq|UPDATE oe SET closed = TRUE WHERE id IN (| . join(', ', ('?') x scalar @close_oe_ids) . qq|)|;
-    do_query($form, $dbh, $query, @close_oe_ids);
-
-    $dbh->commit unless $params{dbh};
+    SL::DB->client->with_transaction(sub {
+      my $query = qq|UPDATE oe SET closed = TRUE WHERE id IN (| . join(', ', ('?') x scalar @close_oe_ids) . qq|)|;
+      do_query($form, $dbh, $query, @close_oe_ids);
+      1;
+    }) or do { die SL::DB->client->error };
   }
 
   $main::lxdebug->leave_sub();
index ea7ca53..2ecad9f 100644 (file)
@@ -7,6 +7,7 @@ use List::Util qw(first);
 
 use SL::DBUtils;
 use SL::Taxkeys;
+use SL::DB;
 
 sub new {
   my $type = shift;
@@ -262,6 +263,10 @@ sub _check_trans_invoices_inventory_with_taxkeys {
   my $self   = shift;
   my %params = @_;
 
+  # ist nur für bestandsmethode notwendig. bei der Aufwandsmethode
+  # können Warenkonten mit Steuerschlüssel sein (5400 in SKR04)
+  return 0 if $::instance_conf->get_inventory_system eq 'periodic';
+
   if (!$params{transaction}->[0]->{invoice}) {
     $main::lxdebug->leave_sub();
     return 0;
@@ -694,7 +699,7 @@ sub fix_ap_ar_wrong_taxkeys {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
+  my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
   my $query    = qq|SELECT 'ap' AS module,
                       at.acc_trans_id, at.trans_id, at.chart_id, at.amount, at.taxkey, at.transdate,
@@ -774,24 +779,25 @@ sub fix_ap_ar_wrong_taxkeys {
   }
 
   if (scalar @corrections) {
-    my $q_taxkey_only     = qq|UPDATE acc_trans SET taxkey = ? WHERE acc_trans_id = ?|;
-    my $h_taxkey_only     = prepare_query($form, $dbh, $q_taxkey_only);
-
-    my $q_taxkey_chart_id = qq|UPDATE acc_trans SET taxkey = ?, chart_id = ? WHERE acc_trans_id = ?|;
-    my $h_taxkey_chart_id = prepare_query($form, $dbh, $q_taxkey_chart_id);
-
-    foreach my $entry (@corrections) {
-      if ($entry->{chart_id}) {
-        do_statement($form, $h_taxkey_chart_id, $q_taxkey_chart_id, $entry->{taxkey}, $entry->{chart_id}, $entry->{acc_trans_id});
-      } else {
-        do_statement($form, $h_taxkey_only, $q_taxkey_only, $entry->{taxkey}, $entry->{acc_trans_id});
+    SL::DB->client->with_transaction(sub {
+      my $q_taxkey_only     = qq|UPDATE acc_trans SET taxkey = ? WHERE acc_trans_id = ?|;
+      my $h_taxkey_only     = prepare_query($form, $dbh, $q_taxkey_only);
+
+      my $q_taxkey_chart_id = qq|UPDATE acc_trans SET taxkey = ?, chart_id = ? WHERE acc_trans_id = ?|;
+      my $h_taxkey_chart_id = prepare_query($form, $dbh, $q_taxkey_chart_id);
+
+      foreach my $entry (@corrections) {
+        if ($entry->{chart_id}) {
+          do_statement($form, $h_taxkey_chart_id, $q_taxkey_chart_id, $entry->{taxkey}, $entry->{chart_id}, $entry->{acc_trans_id});
+        } else {
+          do_statement($form, $h_taxkey_only, $q_taxkey_only, $entry->{taxkey}, $entry->{acc_trans_id});
+        }
       }
-    }
-
-    $h_taxkey_only->finish();
-    $h_taxkey_chart_id->finish();
 
-    $dbh->commit() unless ($params{dbh});
+      $h_taxkey_only->finish();
+      $h_taxkey_chart_id->finish();
+      1;
+    }) or do { die SL::DB->client->error };
   }
 
   $main::lxdebug->leave_sub();
@@ -803,10 +809,14 @@ sub fix_invoice_inventory_with_taxkeys {
   my $self     = shift;
   my %params   = @_;
 
+  # ist nur für bestandsmethode notwendig. bei der Aufwandsmethode
+  # können Warenkonten mit Steuerschlüssel sein (5400 in SKR04)
+  return 0 if $::instance_conf->get_inventory_system eq 'periodic';
+
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
+  my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
   my $query    = qq|SELECT at.*, c.link
                     FROM acc_trans at
@@ -854,17 +864,17 @@ sub fix_invoice_inventory_with_taxkeys {
   }
 
   if (@corrections) {
-    $query = qq|UPDATE acc_trans SET taxkey = 0 WHERE acc_trans_id = ?|;
-    $sth   = prepare_query($form, $dbh, $query);
+    SL::DB->client->with_transaction(sub {
+      $query = qq|UPDATE acc_trans SET taxkey = 0 WHERE acc_trans_id = ?|;
+      $sth   = prepare_query($form, $dbh, $query);
 
-    foreach my $acc_trans_id (@corrections) {
-      do_statement($form, $sth, $query, $acc_trans_id);
-    }
-
-    $sth->finish();
+      foreach my $acc_trans_id (@corrections) {
+        do_statement($form, $sth, $query, $acc_trans_id);
+      }
 
-    $dbh->commit() unless ($params{dbh});
-#     $dbh->rollback();
+      $sth->finish();
+      1;
+    }) or do { die SL::DB->client->error };
   }
 
   $main::lxdebug->leave_sub();
@@ -881,41 +891,41 @@ sub fix_wrong_taxkeys {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
-
-  my $q_taxkey_only  = qq|UPDATE acc_trans SET taxkey = ? WHERE acc_trans_id = ?|;
-  my $h_taxkey_only  = prepare_query($form, $dbh, $q_taxkey_only);
+  my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
-  my $q_taxkey_chart = qq|UPDATE acc_trans SET taxkey = ?, chart_id = ? WHERE acc_trans_id = ?|;
-  my $h_taxkey_chart = prepare_query($form, $dbh, $q_taxkey_chart);
+  SL::DB->client->with_transaction(sub {
+    my $q_taxkey_only  = qq|UPDATE acc_trans SET taxkey = ? WHERE acc_trans_id = ?|;
+    my $h_taxkey_only  = prepare_query($form, $dbh, $q_taxkey_only);
 
-  my $q_transdate    = qq|SELECT transdate FROM acc_trans WHERE acc_trans_id = ?|;
-  my $h_transdate    = prepare_query($form, $dbh, $q_transdate);
+    my $q_taxkey_chart = qq|UPDATE acc_trans SET taxkey = ?, chart_id = ? WHERE acc_trans_id = ?|;
+    my $h_taxkey_chart = prepare_query($form, $dbh, $q_taxkey_chart);
 
-  foreach my $fix (@{ $params{fixes} }) {
-    next unless ($fix->{acc_trans_id});
+    my $q_transdate    = qq|SELECT transdate FROM acc_trans WHERE acc_trans_id = ?|;
+    my $h_transdate    = prepare_query($form, $dbh, $q_transdate);
 
-    do_statement($form, $h_taxkey_only, $q_taxkey_only, conv_i($fix->{taxkey}), conv_i($fix->{acc_trans_id}));
+    foreach my $fix (@{ $params{fixes} }) {
+      next unless ($fix->{acc_trans_id});
 
-    next unless ($fix->{tax_entry_acc_trans_id});
+      do_statement($form, $h_taxkey_only, $q_taxkey_only, conv_i($fix->{taxkey}), conv_i($fix->{acc_trans_id}));
 
-    do_statement($form, $h_transdate, $q_transdate, conv_i($fix->{tax_entry_acc_trans_id}));
-    my ($transdate) = $h_transdate->fetchrow_array();
+      next unless ($fix->{tax_entry_acc_trans_id});
 
-    my %all_taxes = $self->{taxkeys}->get_full_tax_info('transdate' => $transdate);
-    my $tax_info  = $all_taxes{taxkeys}->{ $fix->{taxkey} };
+      do_statement($form, $h_transdate, $q_transdate, conv_i($fix->{tax_entry_acc_trans_id}));
+      my ($transdate) = $h_transdate->fetchrow_array();
 
-    next unless ($tax_info);
+      my %all_taxes = $self->{taxkeys}->get_full_tax_info('transdate' => $transdate);
+      my $tax_info  = $all_taxes{taxkeys}->{ $fix->{taxkey} };
 
-    do_statement($form, $h_taxkey_chart, $q_taxkey_chart, conv_i($fix->{taxkey}), conv_i($tax_info->{taxchart_id}), conv_i($fix->{tax_entry_acc_trans_id}));
-  }
+      next unless ($tax_info);
 
-  $h_taxkey_only->finish();
-  $h_taxkey_chart->finish();
-  $h_transdate->finish();
+      do_statement($form, $h_taxkey_chart, $q_taxkey_chart, conv_i($fix->{taxkey}), conv_i($tax_info->{taxchart_id}), conv_i($fix->{tax_entry_acc_trans_id}));
+    }
 
-#   $dbh->rollback();
-  $dbh->commit() unless ($params{dbh});
+    $h_taxkey_only->finish();
+    $h_taxkey_chart->finish();
+    $h_transdate->finish();
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
@@ -931,19 +941,19 @@ sub delete_transaction {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
-
-  do_query($form, $dbh, qq|UPDATE ar SET storno_id = NULL WHERE storno_id = ?|, conv_i($params{trans_id}));
-  do_query($form, $dbh, qq|UPDATE ap SET storno_id = NULL WHERE storno_id = ?|, conv_i($params{trans_id}));
-  do_query($form, $dbh, qq|UPDATE gl SET storno_id = NULL WHERE storno_id = ?|, conv_i($params{trans_id}));
+  my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
-  do_query($form, $dbh, qq|DELETE FROM ar        WHERE id       = ?|, conv_i($params{trans_id}));
-  do_query($form, $dbh, qq|DELETE FROM ap        WHERE id       = ?|, conv_i($params{trans_id}));
-  do_query($form, $dbh, qq|DELETE FROM gl        WHERE id       = ?|, conv_i($params{trans_id}));
-  do_query($form, $dbh, qq|DELETE FROM acc_trans WHERE trans_id = ?|, conv_i($params{trans_id}));
+  SL::DB->client->with_transaction(sub {
+    do_query($form, $dbh, qq|UPDATE ar SET storno_id = NULL WHERE storno_id = ?|, conv_i($params{trans_id}));
+    do_query($form, $dbh, qq|UPDATE ap SET storno_id = NULL WHERE storno_id = ?|, conv_i($params{trans_id}));
+    do_query($form, $dbh, qq|UPDATE gl SET storno_id = NULL WHERE storno_id = ?|, conv_i($params{trans_id}));
 
-#   $dbh->rollback();
-  $dbh->commit() unless ($params{dbh});
+    do_query($form, $dbh, qq|DELETE FROM ar        WHERE id       = ?|, conv_i($params{trans_id}));
+    do_query($form, $dbh, qq|DELETE FROM ap        WHERE id       = ?|, conv_i($params{trans_id}));
+    do_query($form, $dbh, qq|DELETE FROM gl        WHERE id       = ?|, conv_i($params{trans_id}));
+    do_query($form, $dbh, qq|DELETE FROM acc_trans WHERE trans_id = ?|, conv_i($params{trans_id}));
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
diff --git a/SL/ArchiveZipFixes.pm b/SL/ArchiveZipFixes.pm
deleted file mode 100644 (file)
index ee50579..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-package SL::ArchiveZipFixes;
-
-use strict;
-
-use Archive::Zip;
-use Archive::Zip::Member;
-use version;
-
-# Archive::Zip contains a bug starting with 1.31_04 which prohibits
-# re-writing Zips produced by LibreOffice (.odt). See
-# https://rt.cpan.org/Public/Bug/Display.html?id=92205
-
-sub _member_writeToFileHandle {
-    my $self         = shift;
-    my $fh           = shift;
-    my $fhIsSeekable = shift;
-    my $offset       = shift;
-
-    return _error("no member name given for $self")
-      if $self->fileName() eq '';
-
-    $self->{'writeLocalHeaderRelativeOffset'} = $offset;
-    $self->{'wasWritten'}                     = 0;
-
-    # Determine if I need to write a data descriptor
-    # I need to do this if I can't refresh the header
-    # and I don't know compressed size or crc32 fields.
-    my $headerFieldsUnknown = (
-        ( $self->uncompressedSize() > 0 )
-          and ($self->compressionMethod() == Archive::Zip::COMPRESSION_STORED
-            or $self->desiredCompressionMethod() == Archive::Zip::COMPRESSION_DEFLATED )
-    );
-
-    my $shouldWriteDataDescriptor =
-      ( $headerFieldsUnknown and not $fhIsSeekable );
-
-    $self->hasDataDescriptor(1)
-      if ($shouldWriteDataDescriptor);
-
-    $self->{'writeOffset'} = 0;
-
-    my $status = $self->rewindData();
-    ( $status = $self->_writeLocalFileHeader($fh) )
-      if $status == Archive::Zip::AZ_OK;
-    ( $status = $self->_writeData($fh) )
-      if $status == Archive::Zip::AZ_OK;
-    if ( $status == Archive::Zip::AZ_OK ) {
-        $self->{'wasWritten'} = 1;
-        if ( $self->hasDataDescriptor() ) {
-            $status = $self->_writeDataDescriptor($fh);
-        }
-        elsif ($headerFieldsUnknown) {
-            $status = $self->_refreshLocalFileHeader($fh);
-        }
-    }
-
-    return $status;
-}
-
-sub fix_write_to_file_handle_1_30 {
-  return if version->new("$Archive::Zip::VERSION")->numify <= version->new("1.30")->numify;
-
-  no warnings 'redefine';
-
-  *Archive::Zip::Member::_writeToFileHandle = \&_member_writeToFileHandle;
-}
-
-sub apply_fixes {
-  fix_write_to_file_handle_1_30();
-}
-
-1;
index 788154c..6be6933 100644 (file)
@@ -5,7 +5,7 @@ use DBI;
 use Digest::MD5 qw(md5_hex);
 use IO::File;
 use Time::HiRes qw(gettimeofday);
-use List::MoreUtils qw(uniq);
+use List::MoreUtils qw(any uniq);
 use YAML;
 use Regexp::IPv6 qw($IPv6_re);
 
@@ -20,7 +20,7 @@ use SL::SessionFile;
 use SL::User;
 use SL::DBConnect;
 use SL::DBUpgrade2;
-use SL::DBUtils;
+use SL::DBUtils qw(do_query do_statement prepare_execute_query prepare_query selectall_array_query selectrow_query selectall_ids);
 
 use strict;
 
@@ -37,12 +37,12 @@ sub new {
   my $self            = bless {}, $type;
 
   $self->_read_auth_config(%params);
-  $self->reset;
+  $self->init;
 
   return $self;
 }
 
-sub reset {
+sub init {
   my ($self, %params) = @_;
 
   $self->{SESSION}            = { };
@@ -50,7 +50,29 @@ sub reset {
   $self->{RIGHTS}             = { };
   $self->{unique_counter}     = 0;
   $self->{column_information} = SL::Auth::ColumnInformation->new(auth => $self);
-  $self->{authenticator}->reset;
+}
+
+sub reset {
+  my ($self, %params) = @_;
+
+  $self->{SESSION}        = { };
+  $self->{FULL_RIGHTS}    = { };
+  $self->{RIGHTS}         = { };
+  $self->{unique_counter} = 0;
+
+  if ($self->is_db_connected) {
+    # reset is called during request shutdown already. In case of a
+    # completely new auth DB this would fail and generate an error
+    # message even if the user is currently trying to create said auth
+    # DB. Therefore only fetch the column information if a connection
+    # has been established.
+    $self->{column_information} = SL::Auth::ColumnInformation->new(auth => $self);
+    $self->{column_information}->_fetch;
+  } else {
+    delete $self->{column_information};
+  }
+
+  $_->reset for @{ $self->{authenticators} };
 
   $self->client(undef);
 }
@@ -72,6 +94,18 @@ sub set_client {
   return $self->client;
 }
 
+sub get_default_client_id {
+  my ($self) = @_;
+
+  my $dbh    = $self->dbconnect;
+
+  return unless $dbh;
+
+  my $row = $dbh->selectrow_hashref(qq|SELECT id FROM auth.clients WHERE is_default = TRUE LIMIT 1|);
+
+  return $row->{id} if $row;
+}
+
 sub DESTROY {
   my $self = shift;
 
@@ -84,12 +118,15 @@ sub mini_error {
 
   my ($self, @msg) = @_;
   if ($ENV{HTTP_USER_AGENT}) {
-    print Form->create_http_response(content_type => 'text/html');
+    # $::form might not be initialized yet at this point — therefore
+    # we cannot use "create_http_response" yet.
+    my $cgi = CGI->new('');
+    print $cgi->header('-type' => 'text/html', '-charset' => 'UTF-8');
     print "<pre>", join ('<br>', @msg), "</pre>";
   } else {
     print STDERR "Error: @msg\n";
   }
-  ::end_of_request();
+  $::dispatcher->end_request;
 }
 
 sub _read_auth_config {
@@ -106,19 +143,33 @@ sub _read_auth_config {
 
   } else {
     $self->{DB_config}   = $::lx_office_conf{'authentication/database'};
-    $self->{LDAP_config} = $::lx_office_conf{'authentication/ldap'};
   }
 
-  if ($self->{module} eq 'DB') {
-    $self->{authenticator} = SL::Auth::DB->new($self);
+  $self->{authenticators} =  [];
+  $self->{module}       ||=  'DB';
+  $self->{module}         =~ s{^ +| +$}{}g;
 
-  } elsif ($self->{module} eq 'LDAP') {
-    $self->{authenticator} = SL::Auth::LDAP->new($self);
-  }
+  foreach my $module (split m{ +}, $self->{module}) {
+    my $config_name;
+    ($module, $config_name) = split m{:}, $module, 2;
+    $config_name          ||= $module eq 'DB' ? 'database' : lc($module);
+    my $config              = $::lx_office_conf{'authentication/' . $config_name};
 
-  if (!$self->{authenticator}) {
-    my $locale = Locale->new('en');
-    $self->mini_error($locale->text('No or an unknown authenticantion module specified in "config/kivitendo.conf".'));
+    if (!$config) {
+      my $locale = Locale->new('en');
+      $self->mini_error($locale->text('Missing configuration section "authentication/#1" in "config/kivitendo.conf".', $config_name));
+    }
+
+    if ($module eq 'DB') {
+      push @{ $self->{authenticators} }, SL::Auth::DB->new($self);
+
+    } elsif ($module eq 'LDAP') {
+      push @{ $self->{authenticators} }, SL::Auth::LDAP->new($config);
+
+    } else {
+      my $locale = Locale->new('en');
+      $self->mini_error($locale->text('Unknown authenticantion module #1 specified in "config/kivitendo.conf".', $module));
+    }
   }
 
   my $cfg = $self->{DB_config};
@@ -133,7 +184,7 @@ sub _read_auth_config {
     $self->mini_error($locale->text('config/kivitendo.conf: Missing parameters in "authentication/database". Required parameters are "host", "db" and "user".'));
   }
 
-  $self->{authenticator}->verify_config();
+  $_->verify_config for @{ $self->{authenticators} };
 
   $self->{session_timeout} *= 1;
   $self->{session_timeout}  = 8 * 60 if (!$self->{session_timeout});
@@ -168,8 +219,8 @@ sub authenticate_root {
     return ERR_PASSWORD;
   }
 
-  $password             = SL::Auth::Password->hash(login => 'root', password => $password);
   my $admin_password    = SL::Auth::Password->hash_if_unhashed(login => 'root', password => $self->{admin_password}->());
+  $password             = SL::Auth::Password->hash(login => 'root', password => $password, stored_password => $admin_password);
 
   my $result = $password eq $admin_password ? OK : ERR_PASSWORD;
   $self->set_session_value(SESSION_KEY_ROOT_AUTH() => $result);
@@ -193,7 +244,14 @@ sub authenticate {
     return ERR_PASSWORD;
   }
 
-  my $result = $login ? $self->{authenticator}->authenticate($login, $password) : ERR_USER;
+  my $result = ERR_USER;
+  if ($login) {
+    foreach my $authenticator (@{ $self->{authenticators} }) {
+      $result = $authenticator->authenticate($login, $password);
+      last if $result == OK;
+    }
+  }
+
   $self->set_session_value(SESSION_KEY_USER_AUTH() => $result, login => $login, client_id => $self->client->{id});
   return $result;
 }
@@ -236,6 +294,7 @@ sub dbconnect {
   $self->{dbh} = SL::DBConnect->connect($dsn, $cfg->{user}, $cfg->{password}, { pg_enable_utf8 => 1, AutoCommit => 1 });
 
   if (!$may_fail && !$self->{dbh}) {
+    delete $self->{dbh};
     $main::form->error($main::locale->text('The connection to the authentication database failed:') . "\n" . $DBI::errstr);
   }
 
@@ -251,6 +310,11 @@ sub dbdisconnect {
   }
 }
 
+sub is_db_connected {
+  my ($self) = @_;
+  return !!$self->{dbh};
+}
+
 sub check_tables {
   my ($self, $dbh)    = @_;
 
@@ -372,15 +436,22 @@ sub save_user {
 sub can_change_password {
   my $self = shift;
 
-  return $self->{authenticator}->can_change_password();
+  return any { $_->can_change_password } @{ $self->{authenticators} };
 }
 
 sub change_password {
   my ($self, $login, $new_password) = @_;
 
-  my $result = $self->{authenticator}->change_password($login, $new_password);
+  my $overall_result = OK;
 
-  return $result;
+  foreach my $authenticator (@{ $self->{authenticators} }) {
+    next unless $authenticator->can_change_password;
+
+    my $result = $authenticator->change_password($login, $new_password);
+    $overall_result = $result if $result != OK;
+  }
+
+  return $overall_result;
 }
 
 sub read_all_users {
@@ -516,7 +587,7 @@ sub restore_session {
 
   $form   = $main::form;
 
-  # Don't fail if the auth DB doesn't yet.
+  # Don't fail if the auth DB doesn't exist yet.
   if (!( $dbh = $self->dbconnect(1) )) {
     return $self->session_restore_result(SESSION_NONE());
   }
@@ -537,12 +608,10 @@ sub restore_session {
   #  1. session ID exists in the database
   #  2. hasn't expired yet
   #  3. if cookie for the API token is given: the cookie's value equal database column 'auth.session.api_token' for the session ID
-  #  4. if cookie for the API token is NOT given then: the requestee's IP address must match the stored IP address
   $self->{api_token}   = $cookie->{api_token} if $cookie;
   my $api_token_cookie = $self->get_api_token_cookie;
   my $cookie_is_bad    = !$cookie || $cookie->{is_expired};
   $cookie_is_bad     ||= $api_token_cookie && ($api_token_cookie ne $cookie->{api_token}) if  $api_token_cookie;
-  $cookie_is_bad     ||= $cookie->{ip_address} ne $ENV{REMOTE_ADDR}                       if !$api_token_cookie && $ENV{REMOTE_ADDR} !~ /^$IPv6_re$/;
   if ($cookie_is_bad) {
     $self->destroy_session();
     return $self->session_restore_result($cookie ? SESSION_EXPIRED() : SESSION_NONE());
@@ -592,18 +661,18 @@ SQL
 sub _load_with_auto_restore_column {
   my ($self, $dbh, $session_id) = @_;
 
-  my $auto_restore_keys = join ', ', map { "'${_}'" } qw(login password rpw);
+  my %auto_restore_keys = map { $_ => 1 } qw(login password rpw client_id), SESSION_KEY_ROOT_AUTH, SESSION_KEY_USER_AUTH;
 
   my $query = <<SQL;
     SELECT sess_key, sess_value, auto_restore
     FROM auth.session_content
-    WHERE (session_id = ?)
-      AND (   auto_restore
-           OR sess_key IN (${auto_restore_keys}))
+    WHERE (session_id = ?) AND (auto_restore OR sess_key IN (@{[ join ',', ("?") x keys %auto_restore_keys ]}))
 SQL
-  my $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
+  my $sth = prepare_execute_query($::form, $dbh, $query, $session_id, keys %auto_restore_keys);
 
+  my $need_delete;
   while (my $ref = $sth->fetchrow_hashref) {
+    $need_delete = 1 if $ref->{auto_restore};
     my $value = SL::Auth::SessionValue->new(auth         => $self,
                                             key          => $ref->{sess_key},
                                             value        => $ref->{sess_value},
@@ -619,19 +688,8 @@ SQL
 
   $sth->finish;
 
-  $query = <<SQL;
-    SELECT sess_key
-    FROM auth.session_content
-    WHERE (session_id = ?)
-      AND NOT COALESCE(auto_restore, FALSE)
-      AND (sess_key NOT IN (${auto_restore_keys}))
-SQL
-  $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
-
-  while (my $ref = $sth->fetchrow_hashref) {
-    my $value = SL::Auth::SessionValue->new(auth => $self,
-                                            key  => $ref->{sess_key});
-    $self->{SESSION}->{ $ref->{sess_key} } = $value;
+  if ($need_delete) {
+    do_query($::form, $dbh, 'DELETE FROM auth.session_content WHERE auto_restore AND session_id = ?', $session_id);
   }
 }
 
@@ -726,16 +784,6 @@ sub save_session {
     return;
   }
 
-  my @unfetched_keys = map     { $_->{key}        }
-                       grep    { ! $_->{fetched}  }
-                       values %{ $self->{SESSION} };
-  # $::lxdebug->dump(0, "unfetched_keys", [ sort @unfetched_keys ]);
-  # $::lxdebug->dump(0, "all keys", [ sort map { $_->{key} } values %{ $self->{SESSION} } ]);
-  my $query          = qq|DELETE FROM auth.session_content WHERE (session_id = ?)|;
-  $query            .= qq| AND (sess_key NOT IN (| . join(', ', ('?') x scalar @unfetched_keys) . qq|))| if @unfetched_keys;
-
-  do_query($::form, $dbh, $query, $session_id, @unfetched_keys);
-
   my ($id) = selectrow_query($::form, $dbh, qq|SELECT id FROM auth.session WHERE id = ?|, $session_id);
 
   if ($id) {
@@ -749,28 +797,40 @@ sub save_session {
     do_query($::form, $dbh, qq|UPDATE auth.session SET api_token = ? WHERE id = ?|, $self->_create_session_id, $session_id) unless $stored_api_token;
   }
 
-  my @values_to_save = grep    { $_->{fetched} }
+  my @values_to_save = grep    { $_->{modified} }
                        values %{ $self->{SESSION} };
   if (@values_to_save) {
-    my ($columns, $placeholders) = ('', '');
+    my %known_keys = map { $_ => 1 }
+      selectall_ids($::form, $dbh, qq|SELECT sess_key FROM auth.session_content WHERE session_id = ?|, 'sess_key', $session_id);
     my $auto_restore             = $self->{column_information}->has('auto_restore');
 
-    if ($auto_restore) {
-      $columns      .= ', auto_restore';
-      $placeholders .= ', ?';
-    }
+    my $insert_query  = $auto_restore
+      ? "INSERT INTO auth.session_content (session_id, sess_key, sess_value, auto_restore) VALUES (?, ?, ?, ?)"
+      : "INSERT INTO auth.session_content (session_id, sess_key, sess_value) VALUES (?, ?, ?)";
+    my $insert_sth = prepare_query($::form, $dbh, $insert_query);
 
-    $query  = qq|INSERT INTO auth.session_content (session_id, sess_key, sess_value ${columns}) VALUES (?, ?, ? ${placeholders})|;
-    my $sth = prepare_query($::form, $dbh, $query);
+    my $update_query  = $auto_restore
+      ? "UPDATE auth.session_content SET sess_value = ?, auto_restore = ? WHERE session_id = ? AND sess_key = ?"
+      : "UPDATE auth.session_content SET sess_value = ? WHERE session_id = ? AND sess_key = ?";
+    my $update_sth = prepare_query($::form, $dbh, $update_query);
 
     foreach my $value (@values_to_save) {
       my @values = ($value->{key}, $value->get_dumped);
       push @values, $value->{auto_restore} if $auto_restore;
 
-      do_statement($::form, $sth, $query, $session_id, @values);
+      if ($known_keys{$value->{key}}) {
+        do_statement($::form, $update_sth, $update_query,
+          $value->get_dumped, ( $value->{auto_restore} )x!!$auto_restore, $session_id, $value->{key}
+        );
+      } else {
+        do_statement($::form, $insert_sth, $insert_query,
+          $session_id, $value->{key}, $value->get_dumped, ( $value->{auto_restore} )x!!$auto_restore
+        );
+      }
     }
 
-    $sth->finish();
+    $insert_sth->finish;
+    $update_sth->finish;
   }
 
   $dbh->commit() unless $provided_dbh;
@@ -788,12 +848,14 @@ sub set_session_value {
     if (ref $key eq 'HASH') {
       $self->{SESSION}->{ $key->{key} } = SL::Auth::SessionValue->new(key          => $key->{key},
                                                                       value        => $key->{value},
+                                                                      modified     => 1,
                                                                       auto_restore => $key->{auto_restore});
 
     } else {
       my $value = shift @params;
       $self->{SESSION}->{ $key } = SL::Auth::SessionValue->new(key   => $key,
-                                                               value => $value);
+                                                               value => $value,
+                                                               modified => 1);
     }
   }
 
@@ -810,13 +872,14 @@ sub delete_session_value {
 }
 
 sub get_session_value {
-  my $self = shift;
-  my $data = $self->{SESSION} && $self->{SESSION}->{ $_[0] } ? $self->{SESSION}->{ $_[0] }->get : undef;
+  my ($self, $key) = @_;
+
+  return if !$self->{SESSION};
 
-  return $data;
+  ($self->{SESSION}{$key} //= SL::Auth::SessionValue->new(auth => $self, key => $key))->get
 }
 
-sub create_unique_sesion_value {
+sub create_unique_session_value {
   my ($self, $value, %params) = @_;
 
   $self->{SESSION} ||= { };
@@ -849,7 +912,7 @@ sub save_form_in_session {
     $data->{$key} = $form->{$key} if !ref($form->{$key}) || $non_scalars;
   }
 
-  return $self->create_unique_sesion_value($data, %params);
+  return $self->create_unique_session_value($data, %params);
 }
 
 sub restore_form_from_session {
@@ -918,7 +981,7 @@ sub _tables_present {
 
     my ($count) = selectrow_query($main::form, $dbh, $query, @tables);
 
-    return scalar @tables == $count;
+    scalar @tables == $count;
   }
 }
 
@@ -936,7 +999,7 @@ sub all_rights_full {
   my ($self) = @_;
 
   @{ $self->{master_rights} ||= do {
-      $self->dbconnect->selectall_arrayref("SELECT name, description, category FROM auth.master_rights ORDER BY id");
+      $self->dbconnect->selectall_arrayref("SELECT name, description, category FROM auth.master_rights ORDER BY position");
     }
   }
 }
@@ -1060,23 +1123,38 @@ sub evaluate_rights_ary {
 
   my $value  = 0;
   my $action = '|';
+  my $negate = 0;
 
   foreach my $el (@{$ary}) {
+    next unless defined $el;
+
     if (ref $el eq "ARRAY") {
+      my $val = evaluate_rights_ary($el);
+      $val    = !$val if $negate;
+      $negate = 0;
       if ($action eq '|') {
-        $value |= evaluate_rights_ary($el);
+        $value |= $val;
       } else {
-        $value &= evaluate_rights_ary($el);
+        $value &= $val;
       }
 
     } elsif (($el eq '&') || ($el eq '|')) {
       $action = $el;
 
+    } elsif ($el eq '!') {
+      $negate = !$negate;
+
     } elsif ($action eq '|') {
-      $value |= $el;
+      my $val = $el;
+      $val    = !$val if $negate;
+      $negate = 0;
+      $value |= $val;
 
     } else {
-      $value &= $el;
+      my $val = $el;
+      $val    = !$val if $negate;
+      $negate = 0;
+      $value &= $val;
 
     }
   }
@@ -1151,6 +1229,15 @@ sub check_right {
   return $granted;
 }
 
+sub deny_access {
+  my ($self) = @_;
+
+  $::dispatcher->reply_with_json_error(error => 'access') if $::request->type eq 'json';
+
+  delete $::form->{title};
+  $::form->show_generic_error($::locale->text("You do not have the permissions to access this function."));
+}
+
 sub assert {
   my ($self, $right, $dont_abort) = @_;
 
@@ -1159,8 +1246,7 @@ sub assert {
   }
 
   if (!$dont_abort) {
-    delete $::form->{title};
-    $::form->show_generic_error($::locale->text("You do not have the permissions to access this function."));
+    $self->deny_access;
   }
 
   return 0;
@@ -1246,7 +1332,7 @@ The values can be any Perl structure. They are stored as YAML dumps.
 Retrieve a value from the session. Returns C<undef> if the value
 doesn't exist.
 
-=item C<create_unique_sesion_value $value, %params>
+=item C<create_unique_session_value $value, %params>
 
 Create a unique key in the session and store C<$value>
 there.
@@ -1262,7 +1348,7 @@ setters nor the deleter access the database.
 =item C<save_form_in_session %params>
 
 Stores the content of C<$params{form}> (default: C<$::form>) in the
-session using L</create_unique_sesion_value>.
+session using L</create_unique_session_value>.
 
 If C<$params{non_scalars}> is trueish then non-scalar values will be
 stored as well. Default is to only store scalar values.
@@ -1292,6 +1378,11 @@ close the database connection.
 Creating a new database handle on each request can take up to 30% of the
 pre-request startup time, so we want to avoid that for fast ajax calls.
 
+=item C<assert, $right, $dont_abort>
+
+Checks if current user has the C<$right>. If C<$dont_abort> is falsish
+the request dies with a access denied error, otherwise returns true or false.
+
 =back
 
 =head1 BUGS
index 64b600d..024b0cf 100644 (file)
@@ -27,16 +27,14 @@ sub _fetch {
 
   foreach my $table (qw(session session_content)) {
     my $query = <<SQL;
-      SELECT a.attname, format_type(a.atttypid, a.atttypmod) AS format_type, d.adsrc, a.attnotnull
-      FROM pg_attribute a
-      LEFT JOIN pg_attrdef d ON (a.attrelid = d.adrelid) AND (a.attnum = d.adnum)
-      WHERE (a.attrelid = 'auth.${table}'::regclass)
-        AND (a.attnum > 0)
-        AND NOT a.attisdropped
-      ORDER BY a.attnum
+      SELECT attname
+      FROM pg_attribute
+      WHERE (attrelid = 'auth.${table}'::regclass)
+        AND (attnum > 0)
+        AND NOT attisdropped
 SQL
 
-    $self->{info}->{$table} = { selectall_as_map($::form, $self->{auth}->dbconnect, $query, 'attname', [ qw(format_type adsrc attnotnull) ]) };
+    $self->{info}->{$table} = { selectall_as_map($::form, $self->{auth}->dbconnect, $query, 'attname', [ qw(attname) ]) };
   }
 
   return $self;
index 0bbc050..93e5cc0 100644 (file)
@@ -38,17 +38,15 @@ sub authenticate {
 
   my $stored_password = $self->{auth}->get_stored_password($login);
 
-  my ($algorithm, $algorithm2);
-
   # Empty password hashes in the database mean just that -- empty
   # passwords. Hash it for easier comparison.
-  $stored_password               = SL::Auth::Password->hash(password => $stored_password) unless $stored_password;
-  ($algorithm, $stored_password) = SL::Auth::Password->parse($stored_password);
-  ($algorithm2, $password)       = SL::Auth::Password->parse(SL::Auth::Password->hash(password => $password, algorithm => $algorithm, login => $login));
+  $stored_password    = SL::Auth::Password->hash(password => $stored_password) unless $stored_password;
+  my ($algorithm)     = SL::Auth::Password->parse($stored_password);
+  my $hashed_password = SL::Auth::Password->hash(password => $password, algorithm => $algorithm, login => $login, stored_password => $stored_password);
 
   $main::lxdebug->leave_sub();
 
-  return $password eq $stored_password ? OK : ERR_PASSWORD;
+  return $hashed_password eq $stored_password ? OK : ERR_PASSWORD;
 }
 
 sub can_change_password {
index 18c395e..2f651b3 100644 (file)
@@ -2,28 +2,21 @@ package SL::Auth::LDAP;
 
 use English '-no_match_vars';
 
-use Scalar::Util qw(weaken);
 use SL::Auth::Constants qw(:all);
 
 use strict;
 
 sub new {
-  $main::lxdebug->enter_sub();
-
   if (!defined eval "require Net::LDAP;") {
     die 'The module "Net::LDAP" is not installed.';
   }
 
-  my $type = shift;
-  my $self = {};
-
-  $self->{auth} = shift;
-  weaken $self->{auth};
+  my $type        = shift;
+  my $self        = {};
+  $self->{config} = shift;
 
   bless $self, $type;
 
-  $main::lxdebug->leave_sub();
-
   return $self;
 }
 
@@ -34,52 +27,47 @@ sub reset {
 }
 
 sub _connect {
-  $main::lxdebug->enter_sub();
-
   my $self = shift;
-  my $cfg  = $self->{auth}->{LDAP_config};
-
-  if ($self->{ldap}) {
-    $main::lxdebug->leave_sub();
+  my $cfg  = $self->{config};
 
-    return $self->{ldap};
-  }
+  return $self->{ldap} if $self->{ldap};
 
-  my $port      = $cfg->{port} || 389;
-  $self->{ldap} = Net::LDAP->new($cfg->{host}, 'port' => $port);
+  my $port = $cfg->{port} || 389;
+  my $ldap = Net::LDAP->new($cfg->{host}, port => $port, timeout => $cfg->{timeout} || 10);
 
-  if (!$self->{ldap}) {
-    $main::form->error($main::locale->text('The LDAP server "#1:#2" is unreachable. Please check config/kivitendo.conf.', $cfg->{host}, $port));
+  if (!$ldap) {
+    $::lxdebug->warn($main::locale->text('The LDAP server "#1:#2" is unreachable. Please check config/kivitendo.conf.', $cfg->{host}, $port));
+    return undef;
   }
 
   if ($cfg->{tls}) {
-    my $mesg = $self->{ldap}->start_tls('verify' => 'none');
+    my $mesg = $ldap->start_tls(verify => $cfg->{verify} // 'require');
     if ($mesg->is_error()) {
-      $main::form->error($main::locale->text('The connection to the LDAP server cannot be encrypted (SSL/TLS startup failure). Please check config/kivitendo.conf.'));
+      $::lxdebug->warn($main::locale->text('The connection to the LDAP server cannot be encrypted (SSL/TLS startup failure). Please check config/kivitendo.conf.'));
+      return undef;
     }
   }
 
   if ($cfg->{bind_dn}) {
-    my $mesg = $self->{ldap}->bind($cfg->{bind_dn}, 'password' => $cfg->{bind_password});
+    my $mesg = $ldap->bind($cfg->{bind_dn}, 'password' => $cfg->{bind_password});
     if ($mesg->is_error()) {
-      $main::form->error($main::locale->text('Binding to the LDAP server as "#1" failed. Please check config/kivitendo.conf.', $cfg->{bind_dn}));
+      $::lxdebug->warn($main::locale->text('Binding to the LDAP server as "#1" failed. Please check config/kivitendo.conf.', $cfg->{bind_dn}));
+      return undef;
     }
   }
 
-  $main::lxdebug->leave_sub();
+  $self->{ldap} = $ldap;
 
   return $self->{ldap};
 }
 
 sub _get_filter {
-  $main::lxdebug->enter_sub();
-
   my $self   = shift;
   my $login  = shift;
 
   my ($cfg, $filter);
 
-  $cfg    =  $self->{auth}->{LDAP_config};
+  $cfg    =  $self->{config};
 
   $filter =  "$cfg->{filter}";
   $filter =~ s|^\s+||;
@@ -106,79 +94,54 @@ sub _get_filter {
 
   }
 
-  $main::lxdebug->leave_sub();
-
   return $filter;
 }
 
 sub _get_user_dn {
-  $main::lxdebug->enter_sub();
-
   my $self   = shift;
   my $ldap   = shift;
   my $login  = shift;
 
   $self->{dn_cache} ||= { };
 
-  if ($self->{dn_cache}->{$login}) {
-    $main::lxdebug->leave_sub();
-    return $self->{dn_cache}->{$login};
-  }
+  return $self->{dn_cache}->{$login} if $self->{dn_cache}->{$login};
 
-  my $cfg    = $self->{auth}->{LDAP_config};
+  my $cfg    = $self->{config};
 
   my $filter = $self->_get_filter($login);
 
   my $mesg   = $ldap->search('base' => $cfg->{base_dn}, 'scope' => 'sub', 'filter' => $filter);
 
-  if ($mesg->is_error() || (0 == $mesg->count())) {
-    $main::lxdebug->leave_sub();
-    return undef;
-  }
+  return undef if $mesg->is_error || !$mesg->count();
 
   my $entry                   = $mesg->entry(0);
   $self->{dn_cache}->{$login} = $entry->dn();
 
-  $main::lxdebug->leave_sub();
-
   return $self->{dn_cache}->{$login};
 }
 
 sub authenticate {
-  $main::lxdebug->enter_sub();
-
   my $self       = shift;
   my $login      = shift;
   my $password   = shift;
   my $is_crypted = shift;
 
-  if ($is_crypted) {
-    $main::lxdebug->leave_sub();
-    return ERR_BACKEND;
-  }
+  return ERR_BACKEND if $is_crypted;
 
   my $ldap = $self->_connect();
 
-  if (!$ldap) {
-    $main::lxdebug->leave_sub();
-    return ERR_BACKEND;
-  }
+  return ERR_BACKEND if !$ldap;
 
   my $dn = $self->_get_user_dn($ldap, $login);
 
   $main::lxdebug->message(LXDebug->DEBUG2(), "LDAP authenticate: dn $dn");
 
-  if (!$dn) {
-    $main::lxdebug->leave_sub();
-    return ERR_BACKEND;
-  }
+  return ERR_BACKEND if !$dn;
 
   my $mesg = $ldap->bind($dn, 'password' => $password);
 
   $main::lxdebug->message(LXDebug->DEBUG2(), "LDAP authenticate: bind mesg " . $mesg->error());
 
-  $main::lxdebug->leave_sub();
-
   return $mesg->is_error() ? ERR_PASSWORD : OK;
 }
 
@@ -195,13 +158,11 @@ sub change_password {
 }
 
 sub verify_config {
-  $main::lxdebug->enter_sub();
-
   my $form   = $main::form;
   my $locale = $main::locale;
 
   my $self = shift;
-  my $cfg  = $self->{auth}->{LDAP_config};
+  my $cfg  = $self->{config};
 
   if (!$cfg) {
     $form->error($locale->text('config/kivitendo.conf: Key "authentication/ldap" is missing.'));
@@ -210,8 +171,6 @@ sub verify_config {
   if (!$cfg->{host} || !$cfg->{attribute} || !$cfg->{base_dn}) {
     $form->error($locale->text('config/kivitendo.conf: Missing parameters in "authentication/ldap". Required parameters are "host", "attribute" and "base_dn".'));
   }
-
-  $main::lxdebug->leave_sub();
 }
 
 1;
index 5ae75ea..20a9ed5 100644 (file)
@@ -3,27 +3,53 @@ package SL::Auth::Password;
 use strict;
 
 use Carp;
-use Digest::MD5 ();
 use Digest::SHA ();
+use Encode ();
+use PBKDF2::Tiny ();
+
+sub hash_pkkdf2 {
+  my ($class, %params) = @_;
+
+  # PBKDF2::Tiny expects data to be in octets. Therefore we must
+  # encode everything we hand over (login, password) to UTF-8.
+
+  # This hash method uses a random hash and not just the user's login
+  # for its salt. This is due to the official recommendation that at
+  # least eight octets of random data should be used. Therefore we
+  # must store the salt together with the hashed password. The format
+  # in the database is:
+
+  # {PBKDF2}salt-in-hex:hash-in-hex
+
+  my $salt;
+
+  if ((defined $params{stored_password}) && ($params{stored_password} =~ m/^\{PBKDF2\} ([0-9a-f]+) :/x)) {
+    $salt = (split m{:}, Encode::encode('utf-8', $1), 2)[0];
+
+  } else {
+    my @login  = map { ord } split m{}, Encode::encode('utf-8', $params{login});
+    my @random = map { int(rand(256)) } (0..16);
+
+    $salt      = join '', map { sprintf '%02x', $_ } @login, @random;
+  }
+
+  my $hashed = "{PBKDF2}${salt}:" . join('', map { sprintf '%02x', ord } split m{}, PBKDF2::Tiny::derive('SHA-256', $salt, Encode::encode('utf-8', $params{password})));
+
+  return $hashed;
+}
 
 sub hash {
   my ($class, %params) = @_;
 
-  $params{algorithm} ||= 'SHA256S';
+  $params{algorithm} ||= 'PBKDF2';
 
   my $salt = $params{algorithm} =~ m/S$/ ? $params{login} : '';
 
   if ($params{algorithm} =~ m/^SHA256/) {
     return '{' . $params{algorithm} . '}' . Digest::SHA::sha256_hex($salt . $params{password});
 
-  } elsif ($params{algorithm} =~ m/^SHA1/) {
-    return '{' . $params{algorithm} . '}' . Digest::SHA::sha1_hex($salt . $params{password});
-
-  } elsif ($params{algorithm} =~ m/^MD5/) {
-    return '{' . $params{algorithm} . '}' . Digest::MD5::md5_hex($salt . $params{password});
-
-  } elsif ($params{algorithm} eq 'CRYPT') {
-    return '{CRYPT}' . crypt($params{password}, substr($params{login}, 0, 2));
+  } elsif ($params{algorithm} =~ m/^PBKDF2/) {
+    return $class->hash_pkkdf2(password => $params{password}, stored_password => $params{stored_password});
 
   } else {
     croak 'Unsupported hash algorithm ' . $params{algorithm};
@@ -50,7 +76,7 @@ sub parse {
   my ($class, $password, $default_algorithm) = @_;
 
   return ($1, $2) if $password =~ m/^\{ ([^\}]+) \} (.+)/x;
-  return ($default_algorithm || 'CRYPT', $password);
+  return ($default_algorithm || 'PBKDF2', $password);
 }
 
 1;
index cfaa624..0950a46 100644 (file)
@@ -7,16 +7,16 @@ use strict;
 use SL::Locale::String ();
 
 use Scalar::Util qw(weaken);
-use YAML;
 
 use SL::DBUtils;
+use SL::YAML;
 
 sub new {
   my ($class, %params) = @_;
 
   my $self = bless {}, $class;
 
-  map { $self->{$_} = $params{$_} } qw(auth key value auto_restore);
+  map { $self->{$_} = $params{$_} } qw(auth key value auto_restore modified);
 
   $self->{fetched} =                  exists $params{value};
   $self->{parsed}  = !$params{raw} && exists $params{value};
@@ -39,13 +39,14 @@ sub get_dumped {
   my ($self) = @_;
   no warnings 'once';
   local $YAML::Stringify = 1;
-  return YAML::Dump($self->get);
+  return SL::YAML::Dump($self->get);
 }
 
 sub _fetch {
   my ($self) = @_;
 
   return $self if $self->{fetched};
+  return $self if !$self->{auth}->session_tables_present;
 
   my $dbh          = $self->{auth}->dbconnect;
   my $query        = qq|SELECT sess_value FROM auth.session_content WHERE (session_id = ?) AND (sess_key = ?)|;
@@ -58,7 +59,7 @@ sub _fetch {
 sub _parse {
   my ($self) = @_;
 
-  $self->{value}  = YAML::Load($self->{value}) unless $self->{parsed};
+  $self->{value}  = SL::YAML::Load($self->{value}) unless $self->{parsed};
   $self->{parsed} = 1;
 
   return $self;
@@ -71,7 +72,7 @@ sub _load_value {
 
   my %params = ( simple => 1 );
   eval {
-    my $data = YAML::Load($value);
+    my $data = SL::YAML::Load($value);
 
     if (ref $data eq 'HASH') {
       map { $params{$_} = $data->{$_} } keys %{ $data };
index f7eaf2b..4489b8d 100644 (file)
--- a/SL/BP.pm
+++ b/SL/BP.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Batch printing module backend routines
 package BP;
 
 use SL::DBUtils;
+use SL::DB;
 
 use strict;
 
-sub get_vc {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-
-  my %arap = (invoice           => 'ar',
-              sales_order       => 'oe',
-              purchase_order    => 'oe',
-              sales_quotation   => 'oe',
-              request_quotation => 'oe',
-              check             => 'ap',
-              receipt           => 'ar');
-
-  my $vc = $form->{vc} eq "customer" ? "customer" : "vendor";
-  my $arap_type = defined($arap{$form->{type}}) ? $arap{$form->{type}} : 'ar';
-
-  my $query =
-    qq|SELECT count(*) | .
-    qq|FROM (SELECT DISTINCT ON (vc.id) vc.id FROM $vc vc, $arap_type a, status s | .
-    qq|  WHERE a.${vc}_id = vc.id  AND s.trans_id = a.id AND s.formname = ? | .
-    qq|    AND s.spoolfile IS NOT NULL) AS total|;
-
-  my ($count) = selectrow_query($form, $dbh, $query, $form->{type});
-
-  # build selection list
-  if ($count < $myconfig->{vclimit}) {
-    $query =
-      qq|SELECT DISTINCT ON (vc.id) vc.id, vc.name | .
-      qq|FROM $vc vc, $arap_type a, status s | .
-      qq|WHERE a.${vc}_id = vc.id AND s.trans_id = a.id AND s.formname = ? | .
-      qq|  AND s.spoolfile IS NOT NULL|;
-
-    my $sth = $dbh->prepare($query);
-    $sth->execute($form->{type}) || $form->dberror($query . " ($form->{type})");
-
-    $form->{"all_${vc}"} = [];
-    while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
-      push @{ $form->{"all_${vc}"} }, $ref;
-    }
-    $sth->finish;
-  }
-
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-}
-
 sub payment_accounts {
   $main::lxdebug->enter_sub();
 
   my ($self, $myconfig, $form) = @_;
 
   # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $query =
     qq|SELECT DISTINCT ON (s.chart_id) c.accno, c.description | .
@@ -109,7 +61,6 @@ sub payment_accounts {
   }
 
   $sth->finish;
-  $dbh->disconnect;
 
   $main::lxdebug->leave_sub();
 }
@@ -119,8 +70,7 @@ sub get_spoolfiles {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my ($query, $arap, @values);
   my $invnumber = "invnumber";
@@ -187,12 +137,12 @@ sub get_spoolfiles {
     push(@values, conv_i($form->{"${vc}_id"}));
   } elsif ($form->{ $vc }) {
     $query .= " AND vc.name ILIKE ?";
-    push(@values, $form->like($form->{ $vc }));
+    push(@values, like($form->{ $vc }));
   }
   foreach my $column (qw(invnumber ordnumber quonumber donumber)) {
     if ($form->{$column}) {
       $query .= " AND a.$column ILIKE ?";
-      push(@values, $form->like($form->{$column}));
+      push(@values, like($form->{$column}));
     }
   }
 
@@ -227,7 +177,6 @@ sub get_spoolfiles {
   }
 
   $sth->finish;
-  $dbh->disconnect;
 
   $main::lxdebug->leave_sub();
 }
@@ -239,42 +188,37 @@ sub delete_spool {
 
   my $spool = $::lx_office_conf{paths}->{spool};
 
-  # connect to database, turn AutoCommit off
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  SL::DB->client->with_transaction(sub {
+    my $dbh = SL::DB->client->dbh;
 
-  my $query;
-
-  if ($form->{type} =~ /(check|receipt)/) {
-    $query = qq|DELETE FROM status WHERE spoolfile = ?|;
-  } else {
-    $query =
-      qq|UPDATE status SET spoolfile = NULL, printed = '1' | .
-      qq|WHERE spoolfile = ?|;
-  }
-  my $sth = $dbh->prepare($query) || $form->dberror($query);
+    my $query;
 
-  foreach my $i (1 .. $form->{rowcount}) {
-    if ($form->{"checked_$i"}) {
-      $sth->execute($form->{"spoolfile_$i"}) || $form->dberror($query);
-      $sth->finish;
+    if ($form->{type} =~ /(check|receipt)/) {
+      $query = qq|DELETE FROM status WHERE spoolfile = ?|;
+    } else {
+      $query =
+        qq|UPDATE status SET spoolfile = NULL, printed = '1' | .
+        qq|WHERE spoolfile = ?|;
     }
-  }
+    my $sth = $dbh->prepare($query) || $form->dberror($query);
 
-  # commit
-  my $rc = $dbh->commit;
-  $dbh->disconnect;
+    foreach my $i (1 .. $form->{rowcount}) {
+      if ($form->{"checked_$i"}) {
+        $sth->execute($form->{"spoolfile_$i"}) || $form->dberror($query);
+        $sth->finish;
+      }
+    }
 
-  if ($rc) {
     foreach my $i (1 .. $form->{rowcount}) {
       if ($form->{"checked_$i"}) {
         unlink(qq|$spool/$form->{"spoolfile_$i"}|);
       }
     }
-  }
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
-
-  return $rc;
+  return 1;
 }
 
 sub print_spool {
@@ -285,7 +229,7 @@ sub print_spool {
   my $spool = $::lx_office_conf{paths}->{spool};
 
   # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $query =
     qq|UPDATE status SET printed = '1' | .
@@ -316,10 +260,7 @@ sub print_spool {
     }
   }
 
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
 1;
-
diff --git a/SL/BackgroundJob/ALL.pm b/SL/BackgroundJob/ALL.pm
deleted file mode 100644 (file)
index ba9ce83..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-package SL::BackgroundJob::ALL;
-
-use strict;
-
-use SL::BackgroundJob::Base;
-use SL::BackgroundJob::BackgroundJobCleanup;
-use SL::BackgroundJob::CleanBackgroundJobHistory;
-use SL::BackgroundJob::CreatePeriodicInvoices;
-use SL::BackgroundJob::FailedBackgroundJobsReport;
-
-1;
-
diff --git a/SL/BackgroundJob/CloseProjectsBelongingToClosedSalesOrders.pm b/SL/BackgroundJob/CloseProjectsBelongingToClosedSalesOrders.pm
new file mode 100644 (file)
index 0000000..feb887a
--- /dev/null
@@ -0,0 +1,75 @@
+package SL::BackgroundJob::CloseProjectsBelongingToClosedSalesOrders;
+
+use strict;
+
+use parent qw(SL::BackgroundJob::Base);
+
+use SL::DB::Project;
+use SL::DB::ProjectStatus;
+
+sub run {
+  my ($self, $db_obj)     = @_;
+
+  my $data                = $db_obj->data_as_hash;
+  $data->{new_status}   ||= 'done';
+  $data->{set_inactive}   = 1 if !exists $data->{set_inactive};
+
+  my $new_status          = SL::DB::Manager::ProjectStatus->find_by(name => $data->{new_status}) || die "No project status named '$data->{new_status}' found!";
+
+  my %attributes          = (project_status_id => $new_status->id);
+  $attributes{active}     = 0 if $data->{set_inactive};
+
+  my $sql                 = <<EOSQL;
+    id IN (
+      SELECT oe.globalproject_id
+      FROM oe
+      WHERE (oe.globalproject_id IS NOT NULL)
+        AND (oe.customer_id      IS NOT NULL)
+        AND NOT COALESCE(oe.quotation, FALSE)
+        AND     COALESCE(oe.closed,    FALSE)
+    )
+EOSQL
+
+  SL::DB::Manager::Project->update_all(
+    set   => \%attributes,
+    where => [
+      '!project_status_id' => $new_status->id,
+      \$sql,
+    ],
+  );
+
+  return 1;
+}
+
+1;
+
+__END__
+
+=encoding utf8
+
+=head1 NAME
+
+SL::BackgroundJob::CloseProjectsBelongingToClosedSalesOrders —
+Background job for closing all projects which are linked to a closed
+sales order (via C<oe.globalproject_id>)
+
+=head1 SYNOPSIS
+
+This background job searches all closed sales orders for linked
+projects. Those projects whose status is not C<done> will be modified:
+their status will be set to C<done> and their C<active> flag will be
+set to C<false>.
+
+Both of these can be configured via the job's data hash: C<new_status>
+is the new status' name (defaults to C<done>), and C<set_inactive>
+determines whether or not the project will be set to inactive
+(defaults to 1).
+
+The job is deactivated by default. Administrators of installations
+where such a feature is wanted have to create a job entry manually.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
diff --git a/SL/BackgroundJob/ConvertTimeRecordings.pm b/SL/BackgroundJob/ConvertTimeRecordings.pm
new file mode 100644 (file)
index 0000000..45ded55
--- /dev/null
@@ -0,0 +1,511 @@
+package SL::BackgroundJob::ConvertTimeRecordings;
+
+use strict;
+
+use parent qw(SL::BackgroundJob::Base);
+
+use SL::DB::DeliveryOrder;
+use SL::DB::Part;
+use SL::DB::Project;
+use SL::DB::TimeRecording;
+use SL::Helper::ShippedQty;
+use SL::Locale::String qw(t8);
+
+use DateTime;
+use List::Util qw(any);
+use Try::Tiny;
+sub create_job {
+  $_[0]->create_standard_job('7 3 1 * *'); # every first day of month at 03:07
+}
+use Rose::Object::MakeMethods::Generic (
+ 'scalar'                => [ qw(params) ],
+);
+
+#
+# If job does not throw an error,
+# success in background_job_histories is 'success'.
+# It is 'failure' otherwise.
+#
+# Return value goes to result in background_job_histories.
+#
+sub run {
+  my ($self, $db_obj) = @_;
+
+  $self->initialize_params($db_obj->data_as_hash) if $db_obj;
+
+  $self->{$_} = [] for qw(job_errors);
+
+  my %customer_where;
+  %customer_where = ('customer_id' => $self->params->{customer_ids}) if scalar @{ $self->params->{customer_ids} };
+
+  my $time_recordings = SL::DB::Manager::TimeRecording->get_all(where => [date        => { ge_lt => [ $self->params->{from_date}, $self->params->{to_date} ]},
+                                                                          or          => [booked => 0, booked => undef],
+                                                                          '!duration' => 0,
+                                                                          '!duration' => undef,
+                                                                          %customer_where]);
+
+  return t8('No time recordings to convert') if scalar @$time_recordings == 0;
+
+  my @donumbers;
+
+  if ($self->params->{link_order}) {
+    my %time_recordings_by_order_id;
+    my %orders_by_order_id;
+    foreach my $tr (@$time_recordings) {
+      my $order = $self->get_order_for_time_recording($tr);
+      next if !$order;
+      push @{ $time_recordings_by_order_id{$order->id} }, $tr;
+      $orders_by_order_id{$order->id} ||= $order;
+    }
+    @donumbers = $self->convert_with_linking(\%time_recordings_by_order_id, \%orders_by_order_id);
+
+  } else {
+    @donumbers = $self->convert_without_linking($time_recordings);
+  }
+
+  my $msg  = t8('Number of delivery orders created:');
+  $msg    .= ' ';
+  $msg    .= scalar @donumbers;
+  $msg    .= ' (';
+  $msg    .= join ', ', @donumbers;
+  $msg    .= ').';
+  # die if errors exists
+  if (@{ $self->{job_errors} }) {
+    $msg  .= ' ' . t8('The following errors occurred:');
+    $msg  .= ' ';
+    $msg  .= join "\n", @{ $self->{job_errors} };
+    die $msg . "\n";
+  }
+  return $msg;
+}
+
+# helper
+sub initialize_params {
+  my ($self, $data) = @_;
+
+  # valid parameters with default values
+  my %valid_params = (
+    from_date           => DateTime->new( day => 1,    month => DateTime->today_local->month, year => DateTime->today_local->year)->subtract(months => 1)->to_kivitendo,
+    to_date             => DateTime->last_day_of_month(month => DateTime->today_local->month, year => DateTime->today_local->year)->subtract(months => 1)->to_kivitendo,
+    customernumbers     => [],
+    override_part_id    => undef,
+    default_part_id     => undef,
+    override_project_id => undef,
+    default_project_id  => undef,
+    rounding            => 1,
+    link_order          => 0,
+  );
+
+
+  # check user input param names
+  foreach my $param (keys %$data) {
+    die "Not a valid parameter: $param" unless exists $valid_params{$param};
+  }
+
+  # set defaults
+  $self->params(
+    { map { ($_ => $data->{$_} // $valid_params{$_}) } keys %valid_params }
+  );
+
+
+  # convert date from string to object
+  my ($from_date, $to_date);
+  try {
+    if ($self->params->{from_date}) {
+      $from_date = DateTime->from_kivitendo($self->params->{from_date});
+      # no undef and no other type.
+      die unless ref $from_date eq 'DateTime';
+    }
+    if ($self->params->{to_date}) {
+      $to_date = DateTime->from_kivitendo($self->params->{to_date});
+      # no undef and no other type.
+      die unless ref $to_date eq 'DateTime';
+    }
+  } catch {
+    die t8("Cannot convert date.") ."\n" .
+        t8("Input from string: #1", $self->params->{from_date}) . "\n" .
+        t8("Input to string: #1", $self->params->{to_date}) . "\n" .
+        t8("Details: #1", $_);
+  };
+
+  $to_date->add(days => 1); # to get all from the to_date, because of the time part (15.12.2020 23.59 > 15.12.2020)
+
+  $self->params->{from_date} = $from_date;
+  $self->params->{to_date}   = $to_date;
+
+
+  # check if customernumbers are valid
+  die 'Customer numbers must be given in an array' if 'ARRAY' ne ref $self->params->{customernumbers};
+
+  my $customers = [];
+  if (scalar @{ $self->params->{customernumbers} }) {
+    $customers = SL::DB::Manager::Customer->get_all(where => [ customernumber => $self->params->{customernumbers},
+                                                               or             => [obsolete => undef, obsolete => 0] ]);
+  }
+  die 'Not all customer numbers are valid' if scalar @$customers != scalar @{ $self->params->{customernumbers} };
+
+  # return customer ids
+  $self->params->{customer_ids} = [ map { $_->id } @$customers ];
+
+
+  # check part
+  if ($self->params->{override_part_id} && !SL::DB::Manager::Part->find_by(id => $self->params->{override_part_id},
+                                                                           or => [obsolete => undef, obsolete => 0])) {
+    die 'No valid part found by given override part id';
+  }
+  if ($self->params->{default_part_id} && !SL::DB::Manager::Part->find_by(id => $self->params->{default_part_id},
+                                                                           or => [obsolete => undef, obsolete => 0])) {
+    die 'No valid part found by given default part id';
+  }
+
+
+  # check project
+  if ($self->params->{override_project_id} && !SL::DB::Manager::Project->find_by(id => $self->params->{override_project_id},
+                                                                                 active => 1, valid => 1)) {
+    die 'No valid project found by given override project id';
+  }
+  if ($self->params->{default_project_id} && !SL::DB::Manager::Project->find_by(id => $self->params->{default_project_id},
+                                                                                active => 1, valid => 1)) {
+    die 'No valid project found by given default project id';
+  }
+
+  return $self->params;
+}
+
+sub convert_without_linking {
+  my ($self, $time_recordings) = @_;
+
+  my %time_recordings_by_customer_id;
+  push @{ $time_recordings_by_customer_id{$_->customer_id} }, $_ for @$time_recordings;
+
+  my %convert_params = (
+    rounding         => $self->params->{rounding},
+    override_part_id => $self->params->{override_part_id},
+    default_part_id  => $self->params->{default_part_id},
+  );
+
+  my @donumbers;
+  foreach my $customer_id (keys %time_recordings_by_customer_id) {
+    my $do;
+    if (!eval {
+      $do = SL::DB::DeliveryOrder->new_from_time_recordings($time_recordings_by_customer_id{$customer_id}, %convert_params);
+      1;
+    }) {
+      $self->log_error("creating delivery order failed ($@) for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_customer_id{$customer_id}});
+    }
+
+    if ($do) {
+      if (!SL::DB->client->with_transaction(sub {
+        $do->save;
+        $_->update_attributes(booked => 1) for @{$time_recordings_by_customer_id{$customer_id}};
+        1;
+      })) {
+        $self->log_error('saving delivery order failed for time recording ids ' . join ', ', map { $_->id } @{$time_recordings_by_customer_id{$customer_id}});
+      } else {
+        push @donumbers, $do->donumber;
+      }
+    }
+  }
+
+  return @donumbers;
+}
+
+sub convert_with_linking {
+  my ($self, $time_recordings_by_order_id, $orders_by_order_id) = @_;
+
+  my %convert_params = (
+    rounding        => $self->params->{rounding},
+    override_part_id => $self->params->{override_part_id},
+    default_part_id  => $self->params->{default_part_id},
+  );
+
+  my @donumbers;
+  foreach my $related_order_id (keys %$time_recordings_by_order_id) {
+    my $related_order = $orders_by_order_id->{$related_order_id};
+    my $do;
+    if (!eval {
+      $do = SL::DB::DeliveryOrder->new_from_time_recordings($time_recordings_by_order_id->{$related_order_id}, related_order => $related_order, %convert_params);
+      1;
+    }) {
+      $self->log_error("creating delivery order failed ($@) for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_order_id->{$related_order_id}});
+    }
+
+    if ($do) {
+      if (!SL::DB->client->with_transaction(sub {
+        $do->save;
+        $_->update_attributes(booked => 1) for @{$time_recordings_by_order_id->{$related_order_id}};
+
+        $related_order->link_to_record($do);
+
+        # TODO extend link_to_record for items, otherwise long-term no d.r.y.
+        foreach my $item (@{ $do->items }) {
+          foreach (qw(orderitems)) {
+            if ($item->{"converted_from_${_}_id"}) {
+              die unless $item->{id};
+              RecordLinks->create_links('mode'       => 'ids',
+                                        'from_table' => $_,
+                                        'from_ids'   => $item->{"converted_from_${_}_id"},
+                                        'to_table'   => 'delivery_order_items',
+                                        'to_id'      => $item->{id},
+              ) || die;
+              delete $item->{"converted_from_${_}_id"};
+            }
+          }
+        }
+
+        # update delivered and item's ship for related order
+        my $helper = SL::Helper::ShippedQty->new->calculate($related_order)->write_to_objects;
+        $related_order->delivered($related_order->{delivered});
+        $_->ship($_->{shipped_qty}) for @{$related_order->items};
+        $related_order->save(cascade => 1);
+
+        1;
+      })) {
+        $self->log_error('saving delivery order failed for time recording ids ' . join ', ', map { $_->id } @{$time_recordings_by_order_id->{$related_order_id}});
+
+      } else {
+        push @donumbers, $do->donumber;
+      }
+    }
+  }
+
+  return @donumbers;
+}
+
+sub get_order_for_time_recording {
+  my ($self, $tr) = @_;
+
+  my $orders;
+
+  if (!$tr->order_id) {
+    # check project
+    my $project_id;
+    $project_id   = $self->params->{override_project_id};
+    $project_id ||= $tr->project_id;
+    $project_id ||= $self->params->{default_project_id};
+
+    if (!$project_id) {
+      $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : no project id');
+      return;
+    }
+
+    my $project = SL::DB::Project->load_cached($project_id);
+
+    if (!$project) {
+      $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : project not found');
+      return;
+    }
+    if (!$project->active || !$project->valid) {
+      $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : project not active or not valid');
+      return;
+    }
+    if ($project->customer_id && $project->customer_id != $tr->customer_id) {
+      $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : project customer does not match customer of time recording');
+      return;
+    }
+
+    $orders = SL::DB::Manager::Order->get_all(where        => [customer_id      => $tr->customer_id,
+                                                               or               => [quotation => undef, quotation => 0],
+                                                               globalproject_id => $project_id, ],
+                                              with_objects => ['orderitems']);
+
+  } else {
+    # order_id given
+    my $order = SL::DB::Manager::Order->find_by(id => $tr->order_id);
+    push @$orders, $order if $order;
+  }
+
+  if (!scalar @$orders) {
+    $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : no order found');
+    return;
+  }
+
+  # check part
+  my $part_id;
+  $part_id   = $self->params->{override_part_id};
+  $part_id ||= $tr->part_id;
+  $part_id ||= $self->params->{default_part_id};
+
+  if (!$part_id) {
+    $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : no part id');
+    return;
+  }
+  my $part = SL::DB::Part->load_cached($part_id);
+  if (!$part->unit_obj->is_time_based) {
+    $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : part unit is not time based');
+    return;
+  }
+
+  my @matching_orders;
+  foreach my $order (@$orders) {
+    if (any { $_->parts_id == $part_id } @{ $order->items_sorted }) {
+      push @matching_orders, $order;
+    }
+  }
+
+  if (1 != scalar @matching_orders) {
+    $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : no or more than one orders do match');
+    return;
+  }
+
+  my $matching_order = $matching_orders[0];
+
+  if (!$matching_order->is_sales) {
+    $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : found order is not a sales order');
+    return;
+  }
+
+  if ($matching_order->customer_id != $tr->customer_id) {
+    $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : customer of order does not match customer of time recording');
+    return;
+  }
+
+  if ($tr->project_id && !$self->params->{override_project_id} && $tr->project_id != ($matching_order->globalproject_id || 0)) {
+    $self->log_error('searching related order failed for time recording id ' . $tr->id . ' : project of order does not match project of time recording');
+    return;
+  }
+
+  return $matching_order;
+}
+
+sub log_error {
+  my ($self, $msg) = @_;
+
+  my $dbg = 0;
+
+  push @{ $self->{job_errors} }, $msg;
+  $::lxdebug->message(LXDebug->WARN(), 'ConvertTimeRecordings: ' . $msg) if $dbg;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::BackgroundJob::ConvertTimeRecordings - Convert time recording
+entries into delivery orders
+
+=head1 SYNOPSIS
+
+Get all time recording entries for the given period and customer numbers
+and create delivery ordes out of that (using
+C<SL::DB::DeliveryOrder-E<gt>new_from_time_recordings>).
+
+=head1 CONFIGURATION
+
+Some data can be provided to configure this backgroung job.
+If there is user data and it cannot be validated the background job
+fails.
+
+Example:
+
+  from_date: 01.12.2020
+  to_date: 15.12.2020
+  customernumbers: [1,2,3]
+
+=over 4
+
+=item C<from_date>
+
+The date from which on time recordings should be collected. It defaults
+to the first day of the previous month.
+
+Example (format depends on your settings):
+
+from_date: 01.12.2020
+
+=item C<to_date>
+
+The date till which time recordings should be collected. It defaults
+to the last day of the previous month.
+
+Example (format depends on your settings):
+
+to_date: 15.12.2020
+
+=item C<customernumbers>
+
+An array with the customer numbers for which time recordings should
+be collected. If not given, time recordings for all customers are
+collected.
+
+customernumbers: [c1,22332,334343]
+
+=item C<override_part_id>
+
+The part id of a time based service which should be used to
+book the times instead of the parts which are set in the time
+recordings.
+
+=item C<default_part_id>
+
+The part id of a time based service which should be used to
+book the times if no part is set in the time recording entry.
+
+=item C<rounding>
+
+If set the 0 no rounding of the times will be done otherwise
+the times will be rounded up to the full quarters of an hour,
+ie. 0.25h 0.5h 0.75h 1.25h ...
+Defaults to rounding true (1).
+
+=item C<link_order>
+
+If set the job links the created delivery order with the order
+given in the time recording entry. If there is no order given, then
+it tries to find an order with the current customer and project
+number. It tries to do as much automatic workflow processing as the
+UI.
+Defaults to off. If set to true (1) the job will fail if there
+is no sales order which qualifies as a predecessor.
+Conditions for a predeccesor:
+
+ * Order given in time recording entry OR
+ * Global project_id must match time_recording.project_id OR data.project_id
+ * Customer must match customer in time recording entry
+ * The sales order must have at least one or more time related services
+ * The Project needs to be valid and active
+
+The job doesn't care if the sales order is already delivered or closed.
+If the sales order is overdelivered some organisational stuff needs to be done.
+The sales order may also already be closed, ie the amount is fully billed, but
+the services are not yet fully delivered (simple case: 'Payment in advance').
+
+Hint: take a look or extend the job CloseProjectsBelongingToClosedSalesOrder for
+further automatisation of your organisational needs.
+
+=item C<override_project_id>
+
+Use this project id instead of the project id in the time recordings to find
+a related order. This is only used if C<link_order> is true.
+
+=item C<default_project_id>
+
+Use this project id if no project id is set in the time recording
+entry. This is only used if C<link_order> is true.
+
+=back
+
+=head1 TODO
+
+=over 4
+
+=item * part and project parameters as numbers
+
+Add parameters to give part and project not with their ids, but with their
+numbers. E.g. (default_/override_)part_number,
+(default_/override_)project_number.
+
+
+=back
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/BackgroundJob/CreateOrUpdateFileFullTexts.pm b/SL/BackgroundJob/CreateOrUpdateFileFullTexts.pm
new file mode 100644 (file)
index 0000000..055accd
--- /dev/null
@@ -0,0 +1,144 @@
+package SL::BackgroundJob::CreateOrUpdateFileFullTexts;
+
+use strict;
+
+use parent qw(SL::BackgroundJob::Base);
+
+use Encode qw(decode);
+use English qw( -no_match_vars );
+use File::Slurp qw(read_file);
+use List::MoreUtils qw(uniq);
+use IPC::Run qw();
+use Unicode::Normalize qw();
+
+use SL::DB::File;
+use SL::DB::FileFullText;
+use SL::HTML::Util;
+
+my %extractor_by_mime_type = (
+  'application/pdf' => \&_pdf_to_strings,
+  'text/html'       => \&_html_to_strings,
+  'text/plain'      => \&_text_to_strings,
+);
+
+sub create_job {
+  $_[0]->create_standard_job('20 3 * * *'); # # every day at 3:20 am
+}
+
+#
+# If job does not throw an error,
+# success in background_job_histories is 'success'.
+# It is 'failure' otherwise.
+#
+# return value goes to result in background_job_histories
+#
+sub run {
+  my $self    = shift;
+  my $db_obj  = shift;
+
+  my $all_dbfiles = SL::DB::Manager::File->get_all;
+
+  foreach my $dbfile (@$all_dbfiles) {
+    next if $dbfile->full_text && (($dbfile->mtime || $dbfile->itime) <= ($dbfile->full_text->mtime || $dbfile->full_text->itime));
+    next if !defined $extractor_by_mime_type{$dbfile->mime_type};
+
+    my $file_name;
+    if (!eval { $file_name = SL::File->get(dbfile => $dbfile)->get_file(); 1; }) {
+      $::lxdebug->message(LXDebug::WARN(), "CreateOrUpdateFileFullTexts::run: get_file failed: " . $EVAL_ERROR);
+      next;
+    }
+
+    my $text = $extractor_by_mime_type{$dbfile->mime_type}->($file_name);
+
+    if ($dbfile->full_text) {
+      $dbfile->full_text->update_attributes(full_text => $text);
+    } else {
+      SL::DB::FileFullText->new(file => $dbfile, full_text => $text)->save;
+    }
+  }
+
+  return 'ok';
+}
+
+sub _pdf_to_strings {
+  my ($file_name) = @_;
+
+  my   @cmd = qw(pdftotext -enc UTF-8);
+  push @cmd,  $file_name;
+  push @cmd,  '-';
+
+  my ($txt, $err);
+
+  IPC::Run::run \@cmd, \undef, \$txt, \$err;
+
+  if ($CHILD_ERROR) {
+    $::lxdebug->message(LXDebug::WARN(), "CreateOrUpdateFileFullTexts::_pdf_to_text failed for '$file_name': " . ($CHILD_ERROR >> 8) . ": " . $err);
+    return '';
+  }
+
+  $txt = Encode::decode('utf-8-strict', $txt);
+  $txt =~ s{\r}{ }g;
+  $txt =~ s{\p{WSpace}+}{ }g;
+  $txt = Unicode::Normalize::normalize('C', $txt);
+  $txt = join ' ' , uniq(split(' ', $txt));
+
+  return $txt;
+}
+
+sub _html_to_strings {
+  my ($file_name) = @_;
+
+  my $txt = read_file($file_name);
+
+  $txt = Encode::decode('utf-8-strict', $txt);
+  $txt = SL::HTML::Util::strip($txt);
+  $txt =~ s{\r}{ }g;
+  $txt =~ s{\p{WSpace}+}{ }g;
+  $txt = Unicode::Normalize::normalize('C', $txt);
+  $txt = join ' ' , uniq(split(' ', $txt));
+
+  return $txt;
+}
+
+sub _text_to_strings {
+  my ($file_name) = @_;
+
+  my $txt = read_file($file_name);
+
+  $txt = Encode::decode('utf-8-strict', $txt);
+  $txt =~ s{\r}{ }g;
+  $txt =~ s{\p{WSpace}+}{ }g;
+  $txt = Unicode::Normalize::normalize('C', $txt);
+  $txt = join ' ' , uniq(split(' ', $txt));
+
+  return $txt;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::BackgroundJob::CreateOrUpdateFileFullTexts - Extract text strings/words from
+files in the DMS for full text search.
+
+=head1 SYNOPSIS
+
+Search all documents in the files table and try to extract strings from them
+and store the strings in the database.
+
+Duplicate strings/words in one text are removed.
+
+Strings are updated if the change or creation time of the document is newer than
+the old entry.
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
index 2df2a30..4e32063 100644 (file)
@@ -7,14 +7,20 @@ use parent qw(SL::BackgroundJob::Base);
 use Config::Std;
 use DateTime::Format::Strptime;
 use English qw(-no_match_vars);
+use List::MoreUtils qw(uniq);
 
+use SL::Common;
 use SL::DB::AuthUser;
 use SL::DB::Default;
 use SL::DB::Order;
 use SL::DB::Invoice;
 use SL::DB::PeriodicInvoice;
 use SL::DB::PeriodicInvoicesConfig;
+use SL::File;
+use SL::Helper::CreatePDF qw(create_pdf find_template);
 use SL::Mailer;
+use SL::Util qw(trim);
+use SL::System::Process;
 
 sub create_job {
   $_[0]->create_standard_job('0 3 1 * *'); # first day of month at 3:00 am
@@ -24,43 +30,69 @@ sub run {
   my $self        = shift;
   $self->{db_obj} = shift;
 
-  my $configs = SL::DB::Manager::PeriodicInvoicesConfig->get_all(query => [ active => 1 ]);
+  $self->{$_} = [] for qw(job_errors posted_invoices printed_invoices printed_failed emailed_invoices emailed_failed disabled_orders);
 
-  foreach my $config (@{ $configs }) {
-    my $new_end_date = $config->handle_automatic_extension;
-    _log_msg("Periodic invoice configuration ID " . $config->id . " extended through " . $new_end_date->strftime('%d.%m.%Y') . "\n") if $new_end_date;
-  }
+  if (!$self->{db_obj}->db->with_transaction(sub {
+    1;                          # make Emacs happy
+
+    my $configs = SL::DB::Manager::PeriodicInvoicesConfig->get_all(query => [ active => 1 ]);
+
+    foreach my $config (@{ $configs }) {
+      my $new_end_date = $config->handle_automatic_extension;
+      _log_msg("Periodic invoice configuration ID " . $config->id . " extended through " . $new_end_date->strftime('%d.%m.%Y') . "\n") if $new_end_date;
+    }
 
-  my (@new_invoices, @invoices_to_print);
+    my (@invoices_to_print, @invoices_to_email);
 
-  _log_msg("Number of configs: " . scalar(@{ $configs}));
+    _log_msg("Number of configs: " . scalar(@{ $configs}));
 
-  foreach my $config (@{ $configs }) {
-    # A configuration can be set to inactive by
-    # $config->handle_automatic_extension. Therefore the check in
-    # ...->get_all() does not suffice.
-    _log_msg("Config " . $config->id . " active " . $config->active);
-    next unless $config->active;
+    foreach my $config (@{ $configs }) {
+      # A configuration can be set to inactive by
+      # $config->handle_automatic_extension. Therefore the check in
+      # ...->get_all() does not suffice.
+      _log_msg("Config " . $config->id . " active " . $config->active);
+      next unless $config->active;
 
-    my @dates = _calculate_dates($config);
+      my @dates = _calculate_dates($config);
 
-    _log_msg("Dates: " . join(' ', map { $_->to_lxoffice } @dates));
+      _log_msg("Dates: " . join(' ', map { $_->to_lxoffice } @dates));
 
-    foreach my $date (@dates) {
-      my $invoice = $self->_create_periodic_invoice($config, $date);
-      next unless $invoice;
+      foreach my $date (@dates) {
+        my $data = $self->_create_periodic_invoice($config, $date);
+        next unless $data;
 
-      _log_msg("Invoice " . $invoice->invnumber . " posted for config ID " . $config->id . ", period start date " . $::locale->format_date(\%::myconfig, $date) . "\n");
-      push @new_invoices,      $invoice;
-      push @invoices_to_print, [ $invoice, $config ] if $config->print;
+        _log_msg("Invoice " . $data->{invoice}->invnumber . " posted for config ID " . $config->id . ", period start date " . $::locale->format_date(\%::myconfig, $date) . "\n");
 
-      # last;
+        push @{ $self->{posted_invoices} }, $data->{invoice};
+        push @invoices_to_print, $data if $config->print;
+        push @invoices_to_email, $data if $config->send_email;
+
+        my $inactive_ordnumber = $config->disable_one_time_config;
+        if ($inactive_ordnumber) {
+          # disable one time configs and skip eventual invoices
+          _log_msg("Order " . $inactive_ordnumber . " deavtivated \n");
+          push @{ $self->{disabled_orders} }, $inactive_ordnumber;
+          last;
+        }
+      }
     }
-  }
 
-  _print_invoice(@{ $_ }) for @invoices_to_print;
+    foreach my $inv ( @invoices_to_print ) { $self->_print_invoice($inv); }
+    foreach my $inv ( @invoices_to_email ) { $self->_email_invoice($inv); }
 
-  _send_email(\@new_invoices, [ map { $_->[0] } @invoices_to_print ]) if @new_invoices;
+    $self->_send_summary_email;
+
+      1;
+    })) {
+      $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
+      return undef;
+    }
+
+    if (@{ $self->{job_errors} }) {
+      my $msg = join "\n", @{ $self->{job_errors} };
+      _log_msg("Errors: $msg");
+      die $msg;
+    }
 
   return 1;
 }
@@ -74,7 +106,7 @@ sub _log_msg {
 sub _generate_time_period_variables {
   my $config            = shift;
   my $period_start_date = shift;
-  my $period_end_date   = $period_start_date->clone->truncate(to => 'month')->add(months => $config->get_billing_period_length)->subtract(days => 1);
+  my $period_end_date   = $period_start_date->clone->add(months => $config->get_billing_period_length)->subtract(days => 1);
 
   my @month_names       = ('',
                            $::locale->text('January'), $::locale->text('February'), $::locale->text('March'),     $::locale->text('April'),   $::locale->text('May'),      $::locale->text('June'),
@@ -97,8 +129,8 @@ sub _generate_time_period_variables {
     previous_year       => [ $period_start_date->clone->truncate(to => 'year')->subtract(years => 1),   sub { $_[0]->year } ],
     next_year           => [ $period_start_date->clone->truncate(to => 'year')->add(     years => 1),   sub { $_[0]->year } ],
 
-    period_start_date   => [ $period_start_date->clone->truncate(to => 'month'), sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
-    period_end_date     => [ $period_end_date,                                   sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
+    period_start_date   => [ $period_start_date->clone, sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
+    period_end_date     => [ $period_end_date,          sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
   };
 
   return $vars;
@@ -111,16 +143,15 @@ sub _replace_vars {
   my $sub_fmt  = lc($params{attribute_format} // 'text');
 
   my ($start_tag, $end_tag) = $sub_fmt eq 'html' ? ('&lt;%', '%&gt;') : ('<%', '%>');
+  my @invoice_keys          = $params{invoice} ? (map { $_->name } $params{invoice}->meta->columns) : ();
+  my $key_name_re           = join '|', map { quotemeta } (@invoice_keys, keys %{ $params{vars} });
 
-  $str =~ s{ ${start_tag} ([a-z0-9_]+) ( \s+ format \s*=\s* (.*?) \s* )? ${end_tag} }{
+  $str =~ s{ ${start_tag} ($key_name_re) ( \s+ format \s*=\s* (.*?) \s* )? ${end_tag} }{
     my ($key, $format) = ($1, $3);
     $key               = $::locale->unquote_special_chars('html', $key) if $sub_fmt eq 'html';
     my $new_value;
 
-    if (!$params{vars}->{$key}) {
-      $new_value = '';
-
-    } elsif ($format) {
+    if ($params{vars}->{$key} && $format) {
       $format    = $::locale->unquote_special_chars('html', $format) if $sub_fmt eq 'html';
 
       $new_value = DateTime::Format::Strptime->new(
@@ -129,11 +160,15 @@ sub _replace_vars {
         time_zone   => 'local',
       )->format_datetime($params{vars}->{$key}->[0]);
 
-    } else {
+    } elsif ($params{vars}->{$key}) {
       $new_value = $params{vars}->{$1}->[1]->($params{vars}->{$1}->[0]);
+
+    } elsif ($params{invoice} && $params{invoice}->can($key)) {
+      $new_value = $params{invoice}->$key;
     }
 
-    $new_value = $::locale->quote_special_chars('html', $new_value) if $sub_fmt eq 'html';
+    $new_value //= '';
+    $new_value   = $::locale->quote_special_chars('html', $new_value) if $sub_fmt eq 'html';
 
     $new_value;
 
@@ -145,6 +180,8 @@ sub _replace_vars {
 sub _adjust_sellprices_for_period_lengths {
   my (%params) = @_;
 
+  return if $params{config}->periodicity eq 'o';
+
   my $billing_len     = $params{config}->get_billing_period_length;
   my $order_value_len = $params{config}->get_order_value_period_length;
 
@@ -177,8 +214,6 @@ sub _adjust_sellprices_for_period_lengths {
 }
 
 sub _create_periodic_invoice {
-  $main::lxdebug->enter_sub();
-
   my $self              = shift;
   my $config            = shift;
   my $period_start_date = shift;
@@ -189,15 +224,22 @@ sub _create_periodic_invoice {
 
   my $order   = $config->order;
   my $invoice;
-  if (!$self->{db_obj}->db->do_transaction(sub {
+  if (!$self->{db_obj}->db->with_transaction(sub {
     1;                          # make Emacs happy
 
     $invoice = SL::DB::Invoice->new_from($order);
 
+    my $tax_point = ($invoice->tax_point // $time_period_vars->{period_end_date}->[0])->clone;
+
+    while ($tax_point < $period_start_date) {
+      $tax_point->add(months => $config->get_billing_period_length);
+    }
+
     my $intnotes  = $invoice->intnotes ? $invoice->intnotes . "\n\n" : '';
     $intnotes    .= "Automatisch am " . $invdate->to_lxoffice . " erzeugte Rechnung";
 
     $invoice->assign_attributes(deliverydate => $period_start_date,
+                                tax_point    => $tax_point,
                                 intnotes     => $intnotes,
                                 employee     => $order->employee, # new_from sets employee to import user
                                 direct_debit => $config->direct_debit,
@@ -213,18 +255,6 @@ sub _create_periodic_invoice {
 
     $invoice->post(ar_id => $config->ar_chart_id) || die;
 
-    # like $form->add_shipto, but we don't need to check for a manual exception,
-    # because we can already assume this (otherwise no shipto_id from order)
-    if ($order->shipto_id) {
-
-      my $shipto_oe = SL::DB::Manager::Shipto->find_by(shipto_id => $order->shipto_id);
-      my $shipto_ar = $shipto_oe->clone_and_reset;
-
-      $shipto_ar->module('AR');            # alter module OE -> AR
-      $shipto_ar->trans_id($invoice->id);  # alter trans_id -> new id from invoice
-      $shipto_ar->save;
-    }
-
     $order->link_to_record($invoice);
 
     foreach my $item (@{ $invoice->items }) {
@@ -250,12 +280,19 @@ sub _create_periodic_invoice {
     _log_msg("_create_invoice created for period start date $period_start_date id " . $invoice->id . " number " . $invoice->invnumber . " netamount " . $invoice->netamount . " amount " . $invoice->amount);
 
     # die $invoice->transaction_description;
+
+    1;
   })) {
     $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
     return undef;
   }
-  $main::lxdebug->leave_sub();
-  return $invoice;
+
+  return {
+    config            => $config,
+    period_start_date => $period_start_date,
+    invoice           => $invoice,
+    time_period_vars  => $time_period_vars,
+  };
 }
 
 sub _calculate_dates {
@@ -263,15 +300,21 @@ sub _calculate_dates {
   return $config->calculate_invoice_dates(end_date => DateTime->today_local);
 }
 
-sub _send_email {
-  my ($posted_invoices, $printed_invoices) = @_;
-
+sub _send_summary_email {
+  my ($self) = @_;
   my %config = %::lx_office_conf;
 
-  return if !$config{periodic_invoices} || !$config{periodic_invoices}->{send_email_to} || !scalar @{ $posted_invoices };
+  return if !$config{periodic_invoices} || !$config{periodic_invoices}->{send_email_to} || !scalar @{ $self->{posted_invoices} };
+
+  return if $config{periodic_invoices}->{send_for_errors_only} && !@{ $self->{printed_failed} } && !@{ $self->{emailed_failed} };
 
-  my $user  = SL::DB::Manager::AuthUser->find_by(login => $config{periodic_invoices}->{send_email_to});
-  my $email = $user ? $user->get_config_value('email') : undef;
+  my $email = $config{periodic_invoices}->{send_email_to};
+  if ($email !~ m{\@}) {
+    my $user = SL::DB::Manager::AuthUser->find_by(login => $email);
+    $email   = $user ? $user->get_config_value('email') : undef;
+  }
+
+  _log_msg("_send_summary_email: about to send to '" . ($email || '') . "'");
 
   return unless $email;
 
@@ -285,11 +328,10 @@ sub _send_email {
 
   my $email_template = $config{periodic_invoices}->{email_template};
   my $filename       = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/oe/periodic_invoices_email.txt" );
-  my %params         = ( POSTED_INVOICES  => $posted_invoices,
-                         PRINTED_INVOICES => $printed_invoices );
+  my %params         = map { (uc($_) => $self->{$_}) } qw(posted_invoices printed_invoices printed_failed emailed_invoices emailed_failed disabled_orders);
 
   my $output;
-  $template->process($filename, \%params, \$output);
+  $template->process($filename, \%params, \$output) || die $template->error;
 
   my $mail              = Mailer->new;
   $mail->{from}         = $config{periodic_invoices}->{email_from};
@@ -301,8 +343,54 @@ sub _send_email {
   $mail->send;
 }
 
+sub _store_pdf_in_webdav {
+  my ($self, $pdf_file_name, $invoice) = @_;
+
+  return unless $::instance_conf->get_webdav_documents;
+
+  my $form = Form->new('');
+
+  $form->{cwd}              = SL::System::Process->exe_dir;
+  $form->{tmpdir}           = ($pdf_file_name =~ m{(.+)/})[0];
+  $form->{tmpfile}          = ($pdf_file_name =~ m{.+/(.+)})[0];
+  $form->{format}           = 'pdf';
+  $form->{formname}         = 'invoice';
+  $form->{type}             = 'invoice';
+  $form->{vc}               = 'customer';
+  $form->{invnumber}        = $invoice->invnumber;
+  $form->{recipient_locale} = $invoice->language ? $invoice->language->template_code : '';
+
+  Common::copy_file_to_webdav_folder($form);
+}
+
+sub _store_pdf_in_filemanagement {
+  my ($self, $pdf_file, $invoice) = @_;
+
+  return unless $::instance_conf->get_doc_storage;
+
+  # create a form for generate_attachment_filename
+  my $form = Form->new('');
+  $form->{invnumber} = $invoice->invnumber;
+  $form->{type}      = 'invoice';
+  $form->{format}    = 'pdf';
+  $form->{formname}  = 'invoice';
+  $form->{language}  = '_' . $invoice->language->template_code if $invoice->language;
+  my $doc_name       = $form->generate_attachment_filename();
+
+  SL::File->save(object_id   => $invoice->id,
+                 object_type => 'invoice',
+                 mime_type   => 'application/pdf',
+                 source      => 'created',
+                 file_type   => 'document',
+                 file_name   => $doc_name,
+                 file_path   => $pdf_file);
+}
+
 sub _print_invoice {
-  my ($invoice, $config) = @_;
+  my ($self, $data) = @_;
+
+  my $invoice       = $data->{invoice};
+  my $config        = $data->{config};
 
   return unless $config->print && $config->printer_id && $config->printer->printer_command;
 
@@ -317,24 +405,118 @@ sub _print_invoice {
   $form->{OUT}          = $config->printer->printer_command;
   $form->{OUT_MODE}     = '|-';
 
-  $form->{TEMPLATE_DRIVER_OPTIONS} = {
-    variable_content_types => {
-      longdescription => 'html',
-      partnotes       => 'html',
-      notes           => 'html',
-    },
-  };
+  $form->{TEMPLATE_DRIVER_OPTIONS} = { };
+  $form->{TEMPLATE_DRIVER_OPTIONS}->{variable_content_types} = $form->get_variable_content_types();
 
   $form->prepare_for_printing;
 
   $form->throw_on_error(sub {
     eval {
       $form->parse_template(\%::myconfig);
+      push @{ $self->{printed_invoices} }, $invoice;
       1;
-    } || die $EVAL_ERROR->getMessage;
+    } or do {
+      push @{ $self->{job_errors} }, $EVAL_ERROR->error;
+      push @{ $self->{printed_failed} }, [ $invoice, $EVAL_ERROR->error ];
+    };
   });
 }
 
+sub _email_invoice {
+  my ($self, $data) = @_;
+
+  $data->{config}->load;
+
+  return unless $data->{config}->send_email;
+
+  my @recipients =
+    uniq
+    map  { lc       }
+    grep { $_       }
+    map  { trim($_) }
+    (split(m{,}, $data->{config}->email_recipient_address),
+     $data->{config}->email_recipient_contact   ? ($data->{config}->email_recipient_contact->cp_email) : (),
+     $data->{invoice}->{customer}->invoice_mail ? ($data->{invoice}->{customer}->invoice_mail) : ()
+    );
+
+  return unless @recipients;
+
+  my $language      = $data->{invoice}->language ? $data->{invoice}->language->template_code : undef;
+  my %create_params = (
+    template               => scalar($self->find_template(name => 'invoice', language => $language)),
+    variables              => Form->new(''),
+    return                 => 'file_name',
+    record                 => $data->{invoice},
+    variable_content_types => {
+      longdescription => 'html',
+      partnotes       => 'html',
+      notes           => 'html',
+      $::form->get_variable_content_types_for_cvars,
+    },
+  );
+
+  $data->{invoice}->flatten_to_form($create_params{variables}, format_amounts => 1);
+  $create_params{variables}->prepare_for_printing;
+
+  my $pdf_file_name;
+  my $label = $language && Locale::is_supported($language) ? Locale->new($language)->text('Invoice') : $::locale->text('Invoice');
+
+  eval {
+    $pdf_file_name = $self->create_pdf(%create_params);
+
+    $self->_store_pdf_in_webdav        ($pdf_file_name, $data->{invoice});
+    $self->_store_pdf_in_filemanagement($pdf_file_name, $data->{invoice});
+
+    for (qw(email_subject email_body)) {
+      _replace_vars(
+        object           => $data->{config},
+        invoice          => $data->{invoice},
+        vars             => $data->{time_period_vars},
+        attribute        => $_,
+        attribute_format => ($_ eq 'email_body' ? 'html' : 'text')
+      );
+    }
+
+    my $global_bcc = SL::DB::Default->get->global_bcc;
+    my $overall_error;
+
+    for my $recipient (@recipients) {
+      my $mail             = Mailer->new;
+      $mail->{record_id}   = $data->{invoice}->id,
+      $mail->{record_type} = 'invoice',
+      $mail->{from}        = $data->{config}->email_sender || $::lx_office_conf{periodic_invoices}->{email_from};
+      $mail->{to}          = $recipient;
+      $mail->{bcc}         = $global_bcc;
+      $mail->{subject}     = $data->{config}->email_subject;
+      $mail->{message}     = $data->{config}->email_body;
+      $mail->{message}    .= SL::DB::Default->get->signature;
+      $mail->{content_type} = 'text/html';
+      $mail->{attachments} = [{
+        path     => $pdf_file_name,
+        name     => sprintf('%s %s.pdf', $label, $data->{invoice}->invnumber),
+      }];
+
+      my $error        = $mail->send;
+
+      if ($error) {
+        push @{ $self->{job_errors} }, $error;
+        push @{ $self->{emailed_failed} }, [ $data->{invoice}, $error ];
+        $overall_error = 1;
+      }
+    }
+
+    push @{ $self->{emailed_invoices} }, $data->{invoice} unless $overall_error;
+
+    1;
+
+  } or do {
+    push @{ $self->{job_errors} }, $EVAL_ERROR;
+    push @{ $self->{emailed_failed} }, [ $data->{invoice}, $EVAL_ERROR ];
+  };
+
+  unlink $pdf_file_name if $pdf_file_name;
+}
+
 1;
 
 __END__
index b5e6f29..00f76d5 100644 (file)
@@ -4,9 +4,9 @@ use strict;
 
 use parent qw(SL::BackgroundJob::Base);
 
-use YAML ();
+use SL::JSON;
+use SL::YAML;
 use SL::DB::CsvImportProfile;
-use SL::SessionFile::Random;
 
 sub create_job {
   my ($self_or_class, %params) = @_;
@@ -14,13 +14,8 @@ sub create_job {
   my $package       = ref($self_or_class) || $self_or_class;
   $package          =~ s/SL::BackgroundJob:://;
 
-  my $profile = delete $params{profile} || SL::DB::CsvImportProfile->new;
-  my $new_profile = $profile->clone_and_reset_deep;
-  $new_profile->save;
-
   my %data = (
     %params,
-    profile_id => $new_profile->id,
     session_id => $::auth->get_session_id,
   );
 
@@ -28,7 +23,7 @@ sub create_job {
     type         => 'once',
     active       => 1,
     package_name => $package,
-    data         => YAML::Dump(\%data),
+    data         => SL::YAML::Dump(\%data),
   );
 
   return $job;
@@ -38,7 +33,7 @@ sub profile {
   my ($self) = @_;
 
   if (!$self->{profile}) {
-    my $data = YAML::Load($self->{db_obj}->data);
+    my $data = SL::YAML::Load($self->{db_obj}->data);
     $self->{profile} = SL::DB::Manager::CsvImportProfile->find_by(id => $data->{profile_id});
   }
 
@@ -60,6 +55,7 @@ sub do_import {
   my $job = $self->{db_obj};
 
   $c->profile($self->profile);
+  $c->mappings(SL::JSON::from_json($self->profile->get('json_mappings'))) if $self->profile->get('json_mappings');
   $c->type($job->data_as_hash->{type});
   $c->{employee_id} = $job->data_as_hash->{employee_id};
 
@@ -85,18 +81,22 @@ sub do_import {
   my $session_id = $job->data_as_hash->{session_id};
 
   $c->test_and_import(test => $test, session_id => $session_id);
-
+  my $result;
   if ($c->errors) {
     $job->set_data(
       errors   => $c->errors,
     )->save;
+    $result = $::locale->text('Import finished with errors.');
   } else {
 
-    my $report_id = $c->save_report(session_id => $session_id);
+    my $report_id = $c->save_report(session_id => $session_id, test => $test);
     $job->set_data(report_id => $report_id)->save;
 
     $c->track_progress(finished => 1);
+    $result = $::locale->text('Import finished without errors.');
   }
+
+  return $result;
 }
 
 sub track_progress {
index e0bea8b..a29aea5 100644 (file)
@@ -39,12 +39,12 @@ sub send_email {
     EVAL_PERL   => 0,
     ABSOLUTE    => 1,
     CACHE_SIZE  => 0,
+    ENCODING    => 'utf8',
   }) || die("Could not create Template instance");
 
   my $file_name = $self->data->{template} || 'templates/webpages/failed_background_jobs_report/email.txt';
   my $body;
   $template->process($file_name, { SELF => $self }, \$body);
-  $body = Encode::decode('utf-8', $body);
 
   Mailer->new(
     from         => $self->data->{from},
diff --git a/SL/BackgroundJob/MassDeliveryOrderPrinting.pm b/SL/BackgroundJob/MassDeliveryOrderPrinting.pm
new file mode 100644 (file)
index 0000000..772d566
--- /dev/null
@@ -0,0 +1,104 @@
+package SL::BackgroundJob::MassDeliveryOrderPrinting;
+
+use strict;
+use warnings;
+
+use parent qw(SL::BackgroundJob::Base);
+
+use SL::DB::DeliveryOrder;
+use SL::DB::Order;  # origin order to delivery_order
+use SL::DB::Printer;
+use SL::SessionFile;
+use SL::Template;
+use SL::Helper::MassPrintCreatePDF qw(:all);
+use SL::Helper::CreatePDF qw(:all);
+use SL::Helper::File qw(store_pdf append_general_pdf_attachments doc_storage_enabled);
+
+use constant WAITING_FOR_EXECUTION       => 0;
+use constant PRINTING_DELIVERY_ORDERS    => 1;
+use constant DONE                        => 2;
+
+# Data format:
+# my $data             = {
+#   record_ids          => [ 123, 124, 127, ],
+#   printer_id         => 4711,
+#   num_created        => 0,
+#   num_printed        => 0,
+#   printed_ids        => [ 234, 235, ],
+#   conversion_errors  => [ { id => 124, number => 'A981723', message => "Stuff went boom" }, ],
+#   print_errors       => [ { id => 234, number => 'L87123123', message => "Printer is out of coffee" }, ],
+#   pdf_file_name      => 'qweqwe.pdf',
+#   session_id         => $::auth->get_session_id,
+# };
+
+
+sub convert_deliveryorders_to_pdf {
+  my ($self) = @_;
+
+  my $job_obj = $self->{job_obj};
+  my $db      = $job_obj->db;
+
+  $job_obj->set_data(status => PRINTING_DELIVERY_ORDERS())->save;
+  my $data   = $job_obj->data_as_hash;
+
+  my $printer_id = $data->{printer_id};
+  if ( $data->{media} ne 'printer' ) {
+      undef $printer_id;
+      $data->{media} = 'file';
+  }
+  my %variables  = (
+    type         => 'delivery_order',
+    formname     =>  $data->{formname},
+    format       =>  $data->{format},
+    media        =>  $data->{media},
+    printer_id   =>  $printer_id,
+    copies       =>  $data->{copies},
+  );
+
+  my @pdf_file_names;
+  foreach my $delivery_order_id (@{ $data->{record_ids} }) {
+    my $number = $delivery_order_id;
+    my $delivery_order = SL::DB::DeliveryOrder->new(id => $delivery_order_id)->load;
+
+    eval {
+      $number = $delivery_order->donumber;
+
+      my %params = (
+        variables  => \%variables,
+        document   => $delivery_order,
+        return     => 'file_name',
+       );
+
+      push @pdf_file_names, $self->create_massprint_pdf(%params);
+
+      $data->{num_created}++;
+
+      1;
+
+    } or do {
+      push @{ $data->{conversion_errors} }, { id => $delivery_order->id, number => $number, message => $@ };
+    };
+
+    $job_obj->update_attributes(data_as_hash => $data);
+  }
+
+  $self->merge_massprint_pdf(file_names => \@pdf_file_names, type => 'delivery_order' ) if scalar(@pdf_file_names) > 0;
+}
+
+sub run {
+  my ($self, $job_obj) = @_;
+
+  $self->{job_obj}         = $job_obj;
+
+  $self->convert_deliveryorders_to_pdf;
+  $self->print_pdfs;
+
+  my $data       = $job_obj->data_as_hash;
+  $data->{num_printed} =  $data->{num_created};
+  $job_obj->update_attributes(data_as_hash => $data);
+  $job_obj->set_data(status => DONE())->save;
+
+  return 1;
+}
+
+1;
index 7c6f558..4cc2519 100644 (file)
@@ -11,13 +11,17 @@ use SL::DB::Invoice;
 use SL::DB::Printer;
 use SL::SessionFile;
 use SL::Template;
+use SL::ARAP;
 use SL::Locale::String qw(t8);
-use SL::Webdav;
+use SL::Helper::MassPrintCreatePDF qw(:all);
+use SL::Helper::CreatePDF qw(:all);
+use SL::Helper::File qw(store_pdf append_general_pdf_attachments doc_storage_enabled);
 
 use constant WAITING_FOR_EXECUTION       => 0;
 use constant CONVERTING_DELIVERY_ORDERS  => 1;
 use constant PRINTING_INVOICES           => 2;
 use constant DONE                        => 3;
+
 # Data format:
 # my $data             = {
 #   record_ids          => [ 123, 124, 127, ],
@@ -30,6 +34,7 @@ use constant DONE                        => 3;
 #   conversion_errors  => [ { id => 124, number => 'A981723', message => "Stuff went boom" }, ],
 #   print_errors       => [ { id => 234, number => 'L87123123', message => "Printer is out of coffee" }, ],
 #   pdf_file_name      => 'qweqwe.pdf',
+#   session_id         => $::auth->get_session_id,
 # };
 
 sub create_invoices {
@@ -37,6 +42,7 @@ sub create_invoices {
 
   my $job_obj = $self->{job_obj};
   my $db      = $job_obj->db;
+  my $dbh     = $db->dbh;
 
   $job_obj->set_data(status => CONVERTING_DELIVERY_ORDERS())->save;
 
@@ -45,16 +51,30 @@ sub create_invoices {
     my $data   = $job_obj->data_as_hash;
 
     eval {
-      my $invoice;
       my $sales_delivery_order = SL::DB::DeliveryOrder->new(id => $delivery_order_id)->load;
       $number                  = $sales_delivery_order->donumber;
+      my %conversion_params    = $data->{transdate} ? ('attributes' => { transdate => $data->{transdate} }) : ();
+      my $invoice              = $sales_delivery_order->convert_to_invoice(%conversion_params);
+
+      die $db->error if !$invoice;
 
-      if (!$db->do_transaction(sub {
-        $invoice = $sales_delivery_order->convert_to_invoice(sub { $data->{transdate} ? ('attributes' => { transdate => $data->{transdate} }) :
-                                                                         undef }->() ) || die $db->error;
-        1;
-      })) {
-        die $db->error;
+      ARAP->close_orders_if_billed('dbh'     => $dbh,
+                                   'arap_id' => $invoice->id,
+                                   'table'   => 'ar',);
+
+      # update shop status
+      my @linked_shop_orders = $invoice->linked_records(
+        from      => 'ShopOrder',
+        via       => [ 'DeliveryOrder', 'Order' ],
+      );
+      #if (scalar @linked_shop_orders[0][0] >= 1){
+        #do update
+      my $shop_order = $linked_shop_orders[0][0];
+      if ($shop_order){
+      require SL::Shop;
+        my $shop_config = SL::DB::Manager::Shop->get_first( query => [ id => $shop_order->shop_id ] );
+        my $shop = SL::Shop->new( config => $shop_config );
+        $shop->connector->set_orderstatus($shop_order->shop_trans_id, "completed");
       }
 
       $data->{num_created}++;
@@ -79,123 +99,50 @@ sub convert_invoices_to_pdf {
   my $db      = $job_obj->db;
 
   $job_obj->set_data(status => PRINTING_INVOICES())->save;
+  my $data = $job_obj->data_as_hash;
 
-  require SL::Controller::MassInvoiceCreatePrint;
-
-  my $printer_id = $job_obj->data_as_hash->{printer_id};
-  my $ctrl       = SL::Controller::MassInvoiceCreatePrint->new;
+  my $printer_id = $data->{printer_id};
+  if ( $data->{media} ne 'printer' ) {
+      undef $printer_id;
+      $data->{media} = 'file';
+  }
   my %variables  = (
     type         => 'invoice',
     formname     => 'invoice',
     format       => 'pdf',
     media        => $printer_id ? 'printer' : 'file',
+    printer_id   => $printer_id,
   );
 
   my @pdf_file_names;
 
   foreach my $invoice (@{ $self->{invoices} }) {
-    my $data = $job_obj->data_as_hash;
 
     eval {
-      my %create_params = (
-        template  => $ctrl->find_template(name => 'invoice', printer_id => $printer_id),
-        variables => Form->new(''),
+      my @errors = ();
+      my %params = (
+        variables => \%variables,
         return    => 'file_name',
-        variable_content_types => { longdescription => 'html',
-                                    partnotes       => 'html',
-                                    notes           => 'html',}
+        document  => $invoice,
+        errors    => \@errors,
       );
-
-
-
-      $create_params{variables}->{$_} = $variables{$_} for keys %variables;
-
-      $invoice->flatten_to_form($create_params{variables}, format_amounts => 1);
-      $create_params{variables}->prepare_for_printing;
-
-      push @pdf_file_names, $ctrl->create_pdf(%create_params);
-
-      # copy file to webdav folder
-      if ($::instance_conf->get_webdav_documents) {
-        my $webdav = SL::Webdav->new(
-          type     => 'invoice',
-          number   => $invoice->invnumber,
-        );
-        my $webdav_file = SL::Webdav::File->new(
-          webdav   => $webdav,
-          filename => t8('Invoice') . '_' . $invoice->invnumber . '.pdf',
-        );
-        eval {
-          $webdav_file->store(file => $pdf_file_names[-1]);
-          1;
-        } or do {
-          push @{ $data->{print_errors} }, { id => $invoice->id, number => $invoice->invnumber, message => $@ };
-        }
-      }
-
+      push @pdf_file_names, $self->create_massprint_pdf(%params);
       $data->{num_printed}++;
 
-      1;
-
-    } or do {
-      push @{ $data->{print_errors} }, { id => $invoice->id, number => $invoice->invnumber, message => $@ };
-    };
-
-    $job_obj->update_attributes(data_as_hash => $data);
-  }
-
-  if (@pdf_file_names) {
-    my $data = $job_obj->data_as_hash;
-
-    eval {
-      $self->{merged_pdf} = $ctrl->merge_pdfs(file_names => \@pdf_file_names);
-      unlink @pdf_file_names;
-
-      if (!$printer_id) {
-        my $file_name = 'mass_invoice' . $job_obj->id . '.pdf';
-        my $sfile     = SL::SessionFile->new($file_name, mode => 'w');
-        $sfile->fh->print($self->{merged_pdf});
-        $sfile->fh->close;
-
-        $data->{pdf_file_name} = $file_name;
+      if (scalar @errors) {
+        push @{ $data->{print_errors} }, { id => $invoice->id, number => $invoice->invnumber, message => join(', ', @errors) };
       }
 
       1;
 
     } or do {
-      push @{ $data->{print_errors} }, { message => $@ };
+      push @{ $data->{print_errors} }, { id => $invoice->id, number => $invoice->invnumber, message => $@ };
     };
 
     $job_obj->update_attributes(data_as_hash => $data);
   }
-}
-
-sub print_pdfs {
-  my ($self)     = @_;
-
-  my $job_obj         = $self->{job_obj};
-  my $data            = $job_obj->data_as_hash;
-  my $printer_id      = $data->{printer_id};
-  my $copy_printer_id = $data->{copy_printer_id};
-
-  return if !$printer_id;
-
-  my $out;
-
-  foreach  my $local_printer_id ($printer_id, $copy_printer_id) {
-    next unless $local_printer_id;
-    my $printer = SL::DB::Printer->new(id => $local_printer_id)->load;
-    my $command = SL::Template::create(type => 'ShellCommand', form => Form->new(''))->parse($printer->printer_command);
-    if (!open $out, '|-', $command) {
-      push @{ $data->{print_errors} }, { message => $::locale->text('Could not execute printer command: #1', $!) };
-      $job_obj->update_attributes(data_as_hash => $data);
-      return;
-    }
-    binmode $out;
-    print $out $self->{merged_pdf};
-    close $out;
-  }
 
+  $self->merge_massprint_pdf(file_names => \@pdf_file_names, type => 'invoice' ) if scalar(@pdf_file_names) > 0;
 }
 
 sub run {
index 6137ced..b715f9e 100644 (file)
@@ -13,6 +13,8 @@ use FindBin;
 use SL::DB::AuthUser;
 use SL::DB::Default;
 use SL::Common;
+use SL::Locale::String qw(t8);
+use Carp;
 
 use Rose::Object::MakeMethods::Generic (
   array => [
@@ -24,7 +26,7 @@ use Rose::Object::MakeMethods::Generic (
    'add_full_diag'  => { interface => 'add', hash_key => 'full_diag' },
   ],
   scalar => [
-   qw(diag tester config aggreg),
+   qw(diag tester config aggreg module_nr),
   ],
 );
 
@@ -61,11 +63,13 @@ sub run {
              $self->aggreg->failed,
              $self->aggreg->todo_passed,
   );
-
-  if (!$self->aggreg->all_passed || $self->config->{send_email_on_success}) {
+  # if (!$self->aggreg->all_passed || $self->config->{send_email_on_success}) {
+  # all_passed is not set or calculated (anymore). it is safe to check only for probs or errors
+  if ($self->aggreg->failed || $self->config->{send_email_on_success}) {
     $self->_send_email;
   }
 
+  croak t8("Unsuccessfully executed:" . join ("\n", $self->errors)) if $self->errors;
   return 1;
 }
 
@@ -84,6 +88,9 @@ sub run_module {
   $module =~ s/[^\w:]//g;
   $module = "SL::BackgroundJob::SelfTest::$module";
 
+  # increase module nr
+  $self->module_nr(($self->module_nr || 0) + 1);
+
   # try to load module;
   (my $file = $module) =~ s|::|/|g;
   eval {
@@ -92,15 +99,14 @@ sub run_module {
   } or $self->add_errors($::locale->text('Could not load class #1 (#2): "#3"', $module, $file, $@)) && return;
 
   eval {
-    my $worker = $module->new;
-    $worker->tester($self->tester);
-
-    $worker->run;
-    1;
+    $self->tester->subtest($module => sub {
+      $module->new->run;
+    });
+  1
   } or $self->add_errors($::locale->text('Could not load class #1, #2', $module, $@)) && return;
 
   $self->add_full_diag($output);
-  $self->{diag_per_module}{$module} = $output;
+  $self->{diag_per_module}{$self->module_nr . ': ' . $module} = $output;
 
   my $parser = TAP::Parser->new({ tap => $output});
   $parser->run;
@@ -131,13 +137,14 @@ sub _send_email {
   $mail->{content_type} = $content_type;
   $mail->{message}      = $$output;
 
-  $mail->send;
+  my $err = $mail->send;
+  $self->add_errors('Mailer error #1', $err) if $err;
+
 }
 
 sub _prepare_report {
   my ($self) = @_;
 
-  my $user = $self->_email_user;
   my $template = Template->new({ 'INTERPOLATE' => 0,
                                  'EVAL_PERL'   => 0,
                                  'ABSOLUTE'    => 1,
@@ -153,8 +160,10 @@ sub _prepare_report {
   my %params = (
     SELF     => $self,
     host     => hostname,
-    database => $::myconfig{dbname},
+    database => $::auth->client->{dbname},
+    client   => $::auth->client->{name},
     path     => $FindBin::Bin,
+    errors   => $self->errors,
   );
 
   my $output;
@@ -182,14 +191,4 @@ SL::BackgroundJob::SelfTest - pluggable self testing
   use SL::BackgroundJob::SelfTest;
   SL::BackgroundJob::SelfTest->new->run;;
 
-=head1 DESCRIPTION
-
-
-
-=head1 FUNCTIONS
-
-=head1 BUGS
-
-=head1 AUTHOR
-
 =cut
index 857de8a..6926a32 100644 (file)
@@ -73,11 +73,11 @@ of your tester object will be collected and processed.
 
 =over 4
 
-=item E<tester>
+=item C<tester>
 
-=item E<init_tester>
+=item C<init_tester>
 
-If you don't bother overriding E<init_tester>, your test will use a
+If you don't bother overriding C<init_tester>, your test will use a
 L<Test::More> object by default. Any other L<Test::Builder> object will do.
 
 The TAP output of your builder will be collected and processed for further handling.
index 6307ea9..ab145ce 100644 (file)
@@ -15,7 +15,7 @@ sub run {
 
   $self->_setup;
 
-  $self->tester->plan(tests => 18);
+  $self->tester->plan(tests => 34);
 
   $self->check_konten_mit_saldo_nicht_in_guv;
   $self->check_bilanzkonten_mit_pos_eur;
@@ -23,6 +23,7 @@ sub run {
   $self->check_verwaiste_acc_trans_eintraege;
   $self->check_verwaiste_invoice_eintraege;
   $self->check_ar_acc_trans_amount;
+  $self->check_ap_acc_trans_amount;
   $self->check_netamount_laut_invoice_ar;
   $self->check_invnumbers_unique;
   $self->check_summe_stornobuchungen;
@@ -35,6 +36,15 @@ sub run {
   $self->check_overpayments;
   $self->check_every_account_with_taxkey;
   $self->calc_saldenvortraege;
+  $self->check_missing_tax_bookings;
+  $self->check_bank_transactions_overpayments;
+  $self->check_ar_paid_acc_trans;
+  $self->check_ap_paid_acc_trans;
+  $self->check_zero_amount_paid_but_datepaid_exists;
+  $self->check_orphaned_reconciliated_links;
+  $self->check_recommended_client_settings;
+  $self->check_orphaned_bank_transaction_acc_trans_links;
+  $self->check_consistent_itimes;
 }
 
 sub _setup {
@@ -43,7 +53,6 @@ sub _setup {
   # TODO FIXME calc dates better, unless this is wanted
   $self->fromdate(DateTime->new(day => 1, month => 1, year => DateTime->today->year));
   $self->todate($self->fromdate->clone->add(years => 1)->add(days => -1));
-
   $self->dbh($::form->get_standard_dbh);
 }
 
@@ -135,9 +144,15 @@ sub check_verwaiste_invoice_eintraege {
   my ($self) = @_;
   my $query = qq|
      select * from invoice i
-      where trans_id not in (select id from ar union select id from ap order by id) |;
+      where trans_id not in (select id from ar WHERE ar.transdate >=? AND ar.transdate <=?
+                             UNION
+                             select id from ap WHERE ap.transdate >= ? and ap.transdate <= ?)
+      AND i.transdate >=? AND i.transdate <=?|;
+
+  my $verwaiste_invoice = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate,
+                                                  $self->fromdate, $self->todate, $self->fromdate, $self->todate);
+
 
-  my $verwaiste_invoice = selectall_hashref_query($::form, $self->dbh, $query);
   if (@$verwaiste_invoice) {
      $self->tester->ok(0, "Es gibt verwaiste invoice Einträge! (wo ar/ap-Eintrag fehlt)");
      for my $invoice ( @{ $verwaiste_invoice }) {
@@ -156,10 +171,13 @@ sub check_netamount_laut_invoice_ar {
     where a.transdate >= ? and a.transdate <= ?;|;
   my ($netamount_laut_invoice) =  selectfirst_array_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
 
-  $query = qq| select sum(netamount) from ar where transdate >= ? and transdate <= ?; |;
+  $query = qq| select sum(netamount) from ar where transdate >= ? and transdate <= ? AND invoice; |;
   my ($netamount_laut_ar) =  selectfirst_array_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
 
-  my $correct = $netamount_laut_invoice - $netamount_laut_ar == 0;
+  # should be enough to get a diff below 1. We have currently the following issues:
+  # verkaufsbericht berücksichtigt keinen rabatt
+  # fxsellprice ist mit mwst-inklusive
+  my $correct = abs($netamount_laut_invoice - $netamount_laut_ar) < 1;
 
   $self->tester->ok($correct, "Summe laut Verkaufsbericht sollte gleich Summe aus Verkauf -> Berichte -> Rechnungen sein");
   if (!$correct) {
@@ -190,22 +208,35 @@ sub check_invnumbers_unique {
 sub check_summe_stornobuchungen {
   my ($self) = @_;
 
-  my $query = qq|
-    select sum(amount) from ar a JOIN customer c ON (a.customer_id = c.id)
-    WHERE storno is true
-      AND a.transdate >= ? and a.transdate <= ?|;
-  my ($summe_stornobuchungen_ar) = selectfirst_array_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
-
-  $query = qq|
-    select sum(amount) from ap a JOIN vendor c ON (a.vendor_id = c.id)
-    WHERE storno is true
-      AND a.transdate >= ? and a.transdate <= ?|;
-  my ($summe_stornobuchungen_ap) = selectfirst_array_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
-
-  $self->tester->ok($summe_stornobuchungen_ap == 0, 'Summe aller Einkaufsrechnungen (stornos + stornierte) soll 0 sein');
-  $self->tester->ok($summe_stornobuchungen_ar == 0, 'Summe aller Verkaufsrechnungen (stornos + stornierte) soll 0 sein');
-  $self->tester->diag("Summe Einkaufsrechnungen (ar): $summe_stornobuchungen_ar") if $summe_stornobuchungen_ar;
-  $self->tester->diag("Summe Einkaufsrechnungen (ap): $summe_stornobuchungen_ap") if $summe_stornobuchungen_ap;
+  my %sums_canceled;
+  my %sums_storno;
+  foreach my $table (qw(ar ap)) {
+    # check invoices canceled (stornoed) in consideration period (corresponding stornos do not have to be in this period)
+    my $query = qq|
+      SELECT sum(amount) FROM $table WHERE id IN (
+        SELECT id FROM $table WHERE storno IS TRUE AND storno_id IS NULL AND transdate >= ? AND transdate <= ?
+        UNION
+        SELECT id FROM $table WHERE storno IS TRUE AND storno_id IS NOT NULL AND storno_id IN
+          (SELECT id FROM $table WHERE storno IS TRUE AND storno_id IS NULL AND transdate >= ? AND transdate <= ?)
+      )|;
+    ($sums_canceled{$table}) = selectfirst_array_query($::form, $self->dbh, $query, $self->fromdate, $self->todate, $self->fromdate, $self->todate);
+
+    # check storno invoices in consideration period (corresponding canceled (stornoed) invoices do not have to be in this period)
+    $query = qq|
+      SELECT sum(amount) FROM $table WHERE id IN (
+        SELECT storno_id FROM $table WHERE storno IS TRUE AND storno_id IS NOT NULL AND transdate >= ? AND transdate <= ?
+        UNION
+        SELECT id FROM $table WHERE storno IS TRUE AND storno_id IS NOT NULL AND transdate >= ? AND transdate <= ?
+      )|;
+    ($sums_storno{$table}) = selectfirst_array_query($::form, $self->dbh, $query, $self->fromdate, $self->todate, $self->fromdate, $self->todate);
+
+    my $text_rg = ($table eq 'ar') ? 'Verkaufsrechnungen' : 'Einkaufsrechnungen';
+
+    $self->tester->ok($sums_canceled{$table} == 0, "Summe aller $text_rg (stornos + stornierte) soll 0 sein (für stornierte Rechnungen)");
+    $self->tester->ok($sums_storno  {$table} == 0, "Summe aller $text_rg (stornos + stornierte) soll 0 sein (für Storno-Rechnungen)");
+    $self->tester->diag("Summe $text_rg ($table) (für stornierte Rechnungen) : " . $sums_canceled{$table}) if $sums_canceled{$table} != 0;
+    $self->tester->diag("Summe $text_rg ($table) (für Storno-Rechnungen)     : " . $sums_storno  {$table}) if $sums_storno  {$table} != 0;
+  }
 }
 
 sub check_ar_paid {
@@ -219,7 +250,7 @@ sub check_ar_paid {
     where
           (select sum(amount) from acc_trans a left join chart c on (c.id = a.chart_id) where trans_id = ar.id and c.link like '%AR_paid%') is not null
             AND storno is false
-      AND transdate >= ? and transdate <= ?
+      AND ar.id in (SELECT id from ar where transdate >= ? and transdate <= ?)
     order by diff |;
 
   my $paid_diffs_ar = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
@@ -238,13 +269,13 @@ sub check_ap_paid {
   my ($self) = @_;
 
   my $query = qq|
-      select invnumber,paid,
+      select invnumber,paid,id,
             (select sum(amount) from acc_trans a left join chart c on (c.id = a.chart_id) where trans_id = ap.id and c.link like '%AP_paid%') as accpaid ,
             paid-(select sum(amount) from acc_trans a left join chart c on (c.id = a.chart_id) where trans_id = ap.id and c.link like '%AP_paid%') as diff
      from ap
      where
            (select sum(amount) from acc_trans a left join chart c on (c.id = a.chart_id) where trans_id = ap.id and c.link like '%AP_paid%') is not null
-       AND transdate >= ? and transdate <= ?
+      AND ap.id in (SELECT id from ap where transdate >= ? and transdate <= ?)
      order by diff |;
 
   my $paid_diffs_ap = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
@@ -254,7 +285,7 @@ sub check_ap_paid {
   $self->tester->ok(!$errors, "Vergleich ap paid mit acc_trans AP_paid");
   for my $paid_diff_ap (@{ $paid_diffs_ap }) {
      next if $paid_diff_ap->{diff} == 0;
-     $self->tester->diag("ap invnumber: $paid_diff_ap->{invnumber} : paid: $paid_diff_ap->{paid}    acc_paid= $paid_diff_ap->{accpaid}    diff: $paid_diff_ap->{diff}");
+     $self->tester->diag("ap invnumber: $paid_diff_ap->{invnumber} : ID :: ID :  $paid_diff_ap->{id}  : paid: $paid_diff_ap->{paid}    acc_paid= $paid_diff_ap->{accpaid}    diff: $paid_diff_ap->{diff}");
   }
 }
 
@@ -263,9 +294,9 @@ sub check_ar_overpayments {
 
   my $query = qq|
        select invnumber,paid,amount,transdate,c.customernumber,c.name from ar left join customer c on (ar.customer_id = c.id)
-     where abs(paid) > abs(amount)
+       where abs(paid) > abs(amount)
        AND transdate >= ? and transdate <= ?
-         order by invnumber;|;
+       order by invnumber;|;
 
   my $overpaids_ar =  selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
 
@@ -282,9 +313,9 @@ sub check_ap_overpayments {
 
   my $query = qq|
       select invnumber,paid,amount,transdate,vc.vendornumber,vc.name from ap left join vendor vc on (ap.vendor_id = vc.id)
-    where abs(paid) > abs(amount)
+      where abs(paid) > abs(amount)
       AND transdate >= ? and transdate <= ?
-        order by invnumber;|;
+      order by invnumber;|;
 
   my $overpaids_ap =  selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
 
@@ -300,13 +331,15 @@ sub check_paid_stornos {
   my ($self) = @_;
 
   my $query = qq|
-    SELECT ar.invnumber,sum(amount - COALESCE((SELECT sum(amount)*-1 FROM acc_trans LEFT JOIN chart ON (acc_trans.chart_id=chart.id) WHERE link ilike '%paid%' AND acc_trans.trans_id=ar.id ),0)) as "open"
+    SELECT ar.invnumber,sum(amount - COALESCE((SELECT sum(amount)*-1
+                            FROM acc_trans LEFT JOIN chart ON (acc_trans.chart_id=chart.id)
+                            WHERE link ilike '%paid%' AND acc_trans.trans_id=ar.id ),0)) as "open"
     FROM ar, customer
     WHERE paid != amount
       AND ar.storno
       AND (ar.customer_id = customer.id)
       AND ar.transdate >= ? and ar.transdate <= ?
-    GROUP BY ar.invnumber;|;
+    GROUP BY ar.invnumber|;
   my $paid_stornos = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
 
   $self->tester->ok(0 == @$paid_stornos, "Keine bezahlten Stornos");
@@ -323,19 +356,20 @@ sub check_stornos_ohne_partner {
     FROM ar
     LEFT JOIN customer c on (c.id = ar.customer_id)
     WHERE storno_id is null AND storno is true AND ar.id not in (SELECT storno_id FROM ar WHERE storno_id is not null AND storno is true)
+    AND ar.transdate >= ? and ar.transdate <= ?
     UNION
     SELECT (SELECT cast ('ap' as text)) as invoice,ap.id,invnumber,storno,amount,transdate,type,vendornumber as cv_number
     FROM ap
     LEFT JOIN vendor v on (v.id = ap.vendor_id)
-    WHERE storno_id is null AND storno is true AND ap.id not in (SELECT storno_id FROM ap WHERE storno_id is not null AND storno is true);
-  |;
+    WHERE storno_id is null AND storno is true AND ap.id not in (SELECT storno_id FROM ap WHERE storno_id is not null AND storno is true)
+    AND ap.transdate >= ? and ap.transdate <= ?|;
 
-  my $stornos_ohne_partner =  selectall_hashref_query($::form, $self->dbh, $query);
+  my $stornos_ohne_partner =  selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate,
+                                                                                   $self->fromdate, $self->todate);
 
   $self->tester->ok(@$stornos_ohne_partner == 0, 'Es sollte keine Stornos ohne Partner geben');
   if (@$stornos_ohne_partner) {
-    $self->tester->diag("Stornos ohne Partner:   (kann passieren wenn Stornorechnung außerhalb Zeitraum liegt)");
-    $self->tester->diag("gilt aber trotzdem als paid zu dem Zeitpunkt, oder?");
+    $self->tester->diag("Stornos ohne Partner, oder Storno über Jahreswechsel hinaus");
   }
   my $stornoheader = 0;
   for my $storno (@{ $stornos_ohne_partner }) {
@@ -351,22 +385,23 @@ sub check_overpayments {
 
   # Vergleich ar.paid und das was laut acc_trans bezahlt wurde
   # "als bezahlt markieren" ohne sauberes Ausbuchen führt zu Differenzen bei offenen Forderungen
-  # geht nur auf wenn acc_trans Zahlungseingänge auch im Untersuchungszeitraum lagen
-  # Stornos werden rausgefiltert
+  # Berücksichtigt Zahlungseingänge im Untersuchungszeitraums und
+  # prüft weitere Zahlungen und Buchungen über trans_id (kein Zeitfilter)
+
   my $query = qq|
-SELECT
-invnumber,customernumber,name,ar.transdate,ar.datepaid,
-amount,
-amount-paid as "open via ar",
-paid as "paid via ar",
-coalesce((SELECT sum(amount)*-1 FROM acc_trans LEFT JOIN chart ON (acc_trans.chart_id=chart.id) WHERE link ilike '%paid%' AND acc_trans.trans_id=ar.id AND acc_trans.transdate <= ?),0) as "paid via acc_trans"
-FROM ar left join customer c on (c.id = ar.customer_id)
-WHERE
- (ar.storno IS FALSE)
- AND (transdate <= ? )
-;|;
-
-  my $invoices = selectall_hashref_query($::form, $self->dbh, $query, $self->todate, $self->todate);
+    SELECT
+    invnumber,customernumber,name,ar.transdate,ar.datepaid,
+    amount,
+    amount-paid as "open via ar",
+    paid as "paid via ar",
+    coalesce((SELECT sum(amount)*-1 FROM acc_trans
+      WHERE chart_link ilike '%paid%' AND acc_trans.trans_id=ar.id),0) as "paid via acc_trans"
+    FROM ar left join customer c on (c.id = ar.customer_id)
+    WHERE
+     ar.storno IS FALSE
+     AND ar.id in (SELECT trans_id from acc_trans where transdate >= ? AND transdate <= ? AND chart_link ilike '%paid%')|;
+
+  my $invoices = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
 
   my $count_overpayments = scalar grep {
        $_->{"paid via ar"} != $_->{"paid via acc_trans"}
@@ -379,14 +414,14 @@ WHERE
   if ($count_overpayments) {
     for my $invoice (@{ $invoices }) {
       if ($invoice->{"paid via ar"} != $invoice->{"paid via acc_trans"}) {
-        $self->tester->diag("paid via ar (@{[ $invoice->{'paid via ar'} * 1 ]}) !=   paid via acc_trans  (@{[ $invoice->{'paid via acc_trans'} * 1 ]}) (at least until transdate!)");
+        $self->tester->diag("Rechnung: $invoice->{invnumber}, Kunde $invoice->{name}  Nebenbuch-Bezahlwert: (@{[ $invoice->{'paid via ar'} * 1 ]}) !=   Hauptbuch-Bezahlwert:  (@{[ $invoice->{'paid via acc_trans'} * 1 ]}) (at least until transdate!)");
         if (defined $invoice->{datepaid}) {
           $self->tester->diag("datepaid = $invoice->{datepaid})");
         }
-        $self->tester->diag("Überzahlung!") if $invoice->{"paid via acc_trans"} > $invoice->{amount};
+        $self->tester->diag("Überzahlung bei Rechnung: $invoice->{invnumber}") if $invoice->{"paid via acc_trans"} > $invoice->{amount};
       } elsif ( $invoice->{"amount"} - $invoice->{"paid via acc_trans"} != $invoice->{"open via ar"} && $invoice->{"paid via ar"} != $invoice->{"paid via acc_trans"}) {
         $self->tester->diag("amount - paid_via_acc_trans !=  open_via_ar");
-        $self->tester->diag("Überzahlung!") if $invoice->{"paid via acc_trans"} > $invoice->{amount};
+        $self->tester->diag("Überzahlung bei Rechnung: $invoice->{invnumber}") if $invoice->{"paid via acc_trans"} > $invoice->{amount};
       } else {
         # nothing wrong
       }
@@ -433,10 +468,14 @@ sub check_ar_acc_trans_amount {
   my ($self) = @_;
 
   my $query = qq|
-          select ar.invnumber, ar.netamount, ac.amount
-           from ar left join acc_trans ac on (ac.trans_id = ar.id) where ac.chart_link like 'AR_amount%' AND ac.amount <> ar.netamount|;
+          select sum(ac.amount) as amount, ar.invnumber,ar.netamount
+          from acc_trans ac left join ar on (ac.trans_id = ar.id)
+          WHERE ac.chart_link like 'AR_amount%'
+          AND ac.transdate >= ? AND ac.transdate <= ?
+          AND ar.type       = 'invoice'
+          group by invnumber,netamount having sum(ac.amount) <> ar.netamount|;
 
-  my $ar_amount_not_ac_amount = selectall_hashref_query($::form, $self->dbh, $query);
+  my $ar_amount_not_ac_amount = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
 
   if ( scalar @{ $ar_amount_not_ac_amount } > 0 ) {
     $self->tester->ok(0, "Folgende Ausgangsrechnungen haben einen falschen Netto-Wert im Nebenbuch:");
@@ -445,10 +484,321 @@ sub check_ar_acc_trans_amount {
       $self->tester->diag("Rechnungsnummer: $ar_ac_amount_nok->{invnumber} Hauptbuch-Wert: $ar_ac_amount_nok->{amount}
                             Nebenbuch-Wert: $ar_ac_amount_nok->{netamount}");
     }
+  } else {
+    $self->tester->ok(1, "Hauptbuch-Nettowert und Debitoren-Nebenbuch-Nettowert  stimmen überein.");
+  }
+
+}
+
+sub check_ap_acc_trans_amount {
+  my ($self) = @_;
+
+  my $query = qq|
+          select sum(ac.amount) as amount, ap.invnumber,ap.netamount
+          from acc_trans ac left join ap on (ac.trans_id = ap.id)
+          WHERE (ac.chart_link like '%AP_amount%' OR ac.chart_link like '%IC_cogs%')
+          AND ac.transdate >= ? AND ac.transdate <= ?
+          group by invnumber,trans_id,netamount having sum(ac.amount) <> ap.netamount*-1|;
+
+  my $ap_amount_not_ac_amount = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
+
+  if ( scalar @{ $ap_amount_not_ac_amount } > 0 ) {
+    $self->tester->ok(0, "Folgende Eingangsrechnungen haben einen falschen Netto-Wert im Nebenbuch:");
+
+    for my $ap_ac_amount_nok (@{ $ap_amount_not_ac_amount } ) {
+      $self->tester->diag("Rechnungsnummer: $ap_ac_amount_nok->{invnumber} Hauptbuch-Wert: $ap_ac_amount_nok->{amount}
+                            Nebenbuch-Wert: $ap_ac_amount_nok->{netamount}");
+    }
+  } else {
+    $self->tester->ok(1, "Hauptbuch-Nettowert und Kreditoren-Nebenbuch-Nettowert stimmen überein.");
+  }
+
+}
+
+
+sub check_missing_tax_bookings {
+
+  my ($self) = @_;
+
+  # check tax bookings. all taxkey <> 0 should have tax bookings in acc_trans
+
+  my $query = qq| select trans_id, chart.accno,transdate from acc_trans left join chart on (chart.id = acc_trans.chart_id)
+                    WHERE taxkey NOT IN (SELECT taxkey from tax where rate=0) AND trans_id NOT IN
+                    (select trans_id from acc_trans where chart_link ilike '%tax%' and trans_id IN
+                    (SELECT trans_id from acc_trans where taxkey NOT IN (SELECT taxkey from tax where rate=0)))
+                    AND transdate >= ? AND transdate <= ?|;
+
+  my $missing_tax_bookings = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
+
+  if ( scalar @{ $missing_tax_bookings } > 0 ) {
+    $self->tester->ok(0, "Folgende Konten weisen Buchungen ohne Steuerverknüpfung auf:");
+
+    for my $acc_trans_nok (@{ $missing_tax_bookings } ) {
+      $self->tester->diag("Kontonummer: $acc_trans_nok->{accno} Belegdatum: $acc_trans_nok->{transdate} Trans-ID: $acc_trans_nok->{trans_id}.
+                           Kann über System -> Korrekturen im Hauptbuch bereinigt werden. Falls es ein Zahlungskonto ist, wurde
+                           ggf. ein Brutto-Skonto-Konto mit einer Netto-Rechnung verknüpft. Kann nur per SQL geändert werden.");
+    }
   } else {
     $self->tester->ok(1, "Hauptbuch-Nettowert und Nebenbuch-Nettowert stimmen überein.");
   }
+}
+
+sub check_bank_transactions_overpayments {
+  my ($self) = @_;
+
+  my $query = qq|
+       select id,amount,invoice_amount, purpose,transdate from bank_transactions where abs(invoice_amount) > abs(amount)
+         AND transdate >= ? AND transdate <= ? order by transdate|;
 
+  my $overpaids_bank_transactions =  selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
+
+  my $correct = 0 == @$overpaids_bank_transactions;
+
+  $self->tester->ok($correct, "Keine überbuchte Banktransaktion (der zugeordnete Betrag ist nicht höher, als der Überweisungsbetrag).");
+  for my $overpaid_bank_transaction (@{ $overpaids_bank_transactions }) {
+    $self->tester->diag("Überbuchte Bankbewegung!
+                         Verwendungszweck: $overpaid_bank_transaction->{purpose}
+                         Transaktionsdatum: $overpaid_bank_transaction->{transdate}
+                         Betrag= $overpaid_bank_transaction->{amount}  Zugeordneter Betrag = $overpaid_bank_transaction->{invoice_amount}
+                         Bitte kontaktieren Sie Ihren kivitendo-DB-Admin, der die Überweisung wieder zurücksetzt (Table: bank_transactions Column: invoice_amount).");
+  }
+}
+
+sub check_ar_paid_acc_trans {
+  my ($self) = @_;
+
+  my $query = qq|
+          select sum(ac.amount) as paid_amount, ar.invnumber,ar.paid
+          from acc_trans ac left join ar on (ac.trans_id = ar.id)
+          WHERE ac.chart_link like '%AR_paid%'
+          AND ac.trans_id in (SELECT trans_id from acc_trans ac where ac.transdate >= ? AND ac.transdate <= ?)
+          group by invnumber, paid having sum(ac.amount) <> ar.paid*-1|;
+
+  my $ar_amount_not_ac_amount = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
+
+  if ( scalar @{ $ar_amount_not_ac_amount } > 0 ) {
+    $self->tester->ok(0, "Folgende Ausgangsrechnungen haben einen falschen Bezahl-Wert im Nebenbuch:");
+
+    for my $ar_ac_amount_nok (@{ $ar_amount_not_ac_amount } ) {
+      $self->tester->diag("Rechnungsnummer: $ar_ac_amount_nok->{invnumber} Hauptbuch-Wert: $ar_ac_amount_nok->{paid_amount}
+                            Nebenbuch-Wert: $ar_ac_amount_nok->{paid}");
+    }
+  } else {
+    $self->tester->ok(1, "Hauptbuch-Bezahlwert und Debitoren-Nebenbuch-Bezahlwert stimmen überein.");
+  }
+}
+
+sub check_ap_paid_acc_trans {
+  my ($self) = @_;
+
+  my $query = qq|
+          select sum(ac.amount) as paid_amount, ap.invnumber,ap.paid
+          from acc_trans ac left join ap on (ac.trans_id = ap.id)
+          WHERE ac.chart_link like '%AP_paid%'
+          AND ac.trans_id in (SELECT trans_id from acc_trans ac where ac.transdate >= ? AND ac.transdate <= ?)
+          group by trans_id,invnumber,paid having sum(ac.amount) <> ap.paid|;
+
+  my $ap_amount_not_ac_amount = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
+
+  if ( scalar @{ $ap_amount_not_ac_amount } > 0 ) {
+    $self->tester->ok(0, "Folgende Eingangsrechnungen haben einen falschen Bezahl-Wert im Nebenbuch:");
+
+    for my $ap_ac_amount_nok (@{ $ap_amount_not_ac_amount } ) {
+      $self->tester->diag("Rechnungsnummer: $ap_ac_amount_nok->{invnumber} Hauptbuch-Wert: $ap_ac_amount_nok->{paid_amount}
+                            Nebenbuch-Wert: $ap_ac_amount_nok->{paid}");
+    }
+  } else {
+    $self->tester->ok(1, "Hauptbuch Bezahl-Wert und Kreditoren-Nebenbuch-Bezahlwert stimmen überein.");
+  }
+}
+
+sub check_zero_amount_paid_but_datepaid_exists {
+  my ($self) = @_;
+
+  my $query = qq|(SELECT invnumber,datepaid from ar where datepaid is NOT NULL AND paid = 0
+                    AND id not IN (select trans_id from acc_trans WHERE chart_link like '%paid%' AND acc_trans.trans_id = ar.id)
+                    AND datepaid >= ? AND datepaid <= ?)
+                  UNION
+                 (SELECT invnumber,datepaid from ap where datepaid is NOT NULL AND paid = 0
+                    AND id not IN (select trans_id from acc_trans WHERE chart_link like '%paid%' AND acc_trans.trans_id = ap.id)
+                    AND datepaid >= ? AND datepaid <= ?)|;
+
+  my $datepaid_should_be_null = selectall_hashref_query($::form, $self->dbh, $query,
+                                                         $self->fromdate, $self->todate,
+                                                         $self->fromdate, $self->todate);
+
+  if ( scalar @{ $datepaid_should_be_null } > 0 ) {
+    $self->tester->ok(0, "Folgende Rechnungen haben ein Bezahl-Datum, aber keinen Bezahl-Wert im Nebenbuch:");
+
+    for my $datepaid_should_be_null_nok (@{ $datepaid_should_be_null } ) {
+      $self->tester->diag("Rechnungsnummer: $datepaid_should_be_null_nok->{invnumber}
+                           Bezahl-Datum: $datepaid_should_be_null_nok->{datepaid}");
+    }
+  } else {
+    $self->tester->ok(1, "Kein Bezahl-Datum ohne Bezahl-Wert und ohne wirkliche Zahlungen gefunden (arap.datepaid, arap.paid konsistent).");
+  }
+}
+
+sub check_orphaned_reconciliated_links {
+  my ($self) = @_;
+
+  my $query = qq|
+          SELECT purpose from bank_transactions
+          WHERE cleared is true
+          AND NOT EXISTS (SELECT bank_transaction_id from reconciliation_links WHERE bank_transaction_id = bank_transactions.id)
+          AND transdate >= ? AND transdate <= ?|;
+
+  my $bt_cleared_no_link = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
+
+  if ( scalar @{ $bt_cleared_no_link } > 0 ) {
+    $self->tester->ok(0, "Verwaiste abgeglichene Bankbewegungen gefunden. Bei folgenden Bankbewegungen ist die abgleichende Verknüpfung gelöscht worden:");
+
+    for my $bt_orphaned (@{ $bt_cleared_no_link }) {
+      $self->tester->diag("Verwendungszweck: $bt_orphaned->{purpose}");
+    }
+  } else {
+    $self->tester->ok(1, "Keine verwaisten Einträge in abgeglichenen Bankbewegungen.");
+  }
+}
+
+sub check_recommended_client_settings {
+  my ($self) = @_;
+
+  my $all_ok = 1;
+
+  # expand: check datev && check mark_as_paid
+  my %settings_values_nok = (
+                              SL::DB::Default->get->is_changeable => 1,
+                              SL::DB::Default->get->ar_changeable => 1,
+                              SL::DB::Default->get->ap_changeable => 1,
+                              SL::DB::Default->get->ir_changeable => 1,
+                              SL::DB::Default->get->gl_changeable => 1,
+                             );
+
+  foreach (keys %settings_values_nok) {
+    if ($_ == $settings_values_nok{$_}) {
+      $self->tester->ok(0, "Buchungskonfiguration: Mindestens ein Belegtyp ist immer änderbar.");
+      undef $all_ok;
+    }
+  }
+
+  # payments more strict (avoid losing payments acc_trans_ids)
+  my $payments_ok = SL::DB::Default->get->payments_changeable == 0 ? 1 : 0;
+  $self->tester->ok(0, "Manuelle Zahlungen sind zu lange änderbar (Empfehlung: niemals).") unless $payments_ok;
+
+  $self->tester->ok(1, "Mandantenkonfiguration optimal eingestellt.") if ($payments_ok && $all_ok);
+}
+
+sub check_orphaned_bank_transaction_acc_trans_links {
+  my ($self) = @_;
+
+  my $query = qq|
+          SELECT purpose from bank_transactions
+          WHERE invoice_amount <> 0
+          AND NOT EXISTS (SELECT bank_transaction_id FROM bank_transaction_acc_trans WHERE bank_transaction_id = bank_transactions.id)
+          AND itime > (SELECT min(itime) from bank_transaction_acc_trans)
+          AND transdate >= ? AND transdate <= ?|;
+
+  my $bt_assigned_no_link = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
+
+  if ( scalar @{ $bt_assigned_no_link } > 0 ) {
+    $self->tester->ok(0, "Verwaiste Verknüpfungen zu Bankbewegungen gefunden. Bei folgenden Bankbewegungen ist eine interne Verknüpfung gelöscht worden:");
+
+    for my $bt_orphaned (@{ $bt_assigned_no_link }) {
+      $self->tester->diag("Verwendungszweck: $bt_orphaned->{purpose}");
+    }
+  } else {
+    $self->tester->ok(1, "Keine verwaisten Einträge in verknüpften Bankbewegungen (Richtung Bank).");
+  }
+  # check for deleted acc_trans_ids
+  $query = qq|
+          SELECT purpose from bank_transactions
+          WHERE id in
+          (SELECT bank_transaction_id from bank_transaction_acc_trans
+           WHERE NOT EXISTS (SELECT acc_trans.acc_trans_id FROM acc_trans WHERE acc_trans.acc_trans_id = bank_transaction_acc_trans.acc_trans_id)
+           AND transdate >= ? AND transdate <= ?)|;
+
+  my $bt_assigned_no_acc_trans = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
+
+  if ( scalar @{ $bt_assigned_no_acc_trans } > 0 ) {
+    $self->tester->ok(0, "Verwaiste Verknüpfungen zu Bankbewegungen gefunden. Bei folgenden Bankbewegungen ist eine interne Verknüpfung gelöscht worden:");
+
+    for my $bt_orphaned (@{ $bt_assigned_no_acc_trans }) {
+      $self->tester->diag("Verwendungszweck: $bt_orphaned->{purpose}");
+    }
+  } else {
+    $self->tester->ok(1, "Keine verwaisten Einträge in verknüpften Bankbewegungen (Richtung Buchung (Richtung Buchung)).");
+  }
+}
+
+sub check_consistent_itimes {
+  my ($self) = @_;
+  my $query;
+
+  $query = qq|
+    SELECT mtime, itime,gldate, acc_trans_id, trans_id
+    FROM  acc_trans a
+    WHERE itime::date <> gldate::date
+    AND a.transdate >= ? and a.transdate <= ?|;
+
+  my $itimes_ac = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
+
+  if ( scalar @{ $itimes_ac } > 0 ) {
+    $self->tester->ok(0, "Inkonsistente Zeitstempel in der acc_trans gefunden. Bei folgenden ids:");
+    for my $bogus_time (@{ $itimes_ac }) {
+      $self->tester->diag("ID: $bogus_time->{trans_id} acc_trans_id: $bogus_time->{acc_trans_id} ");
+    }
+  } else {
+    $self->tester->ok(1, "Keine inkonsistenten Zeitstempel in der acc_trans.");
+  }
+  $query = qq|
+    SELECT amount, itime, gldate, id
+    FROM ap a
+    WHERE itime::date <> gldate::date
+    AND a.transdate >= ? and a.transdate <= ?|;
+
+  my $itimes_ap = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
+
+  if ( scalar @{ $itimes_ap } > 0 ) {
+    $self->tester->ok(0, "Inkonsistente Zeitstempel in ap gefunden. Bei folgenden ids:");
+    for my $bogus_time (@{ $itimes_ap }) {
+      $self->tester->diag("ID: $bogus_time->{id} itime: $bogus_time->{itime} mtime: $bogus_time->{mtime} ");
+    }
+  } else {
+    $self->tester->ok(1, "Keine inkonsistenten Zeitstempel in ap.");
+  }
+  $query = qq|
+    SELECT amount, itime, gldate, id
+    FROM ar a
+    WHERE itime::date <> gldate::date
+    AND a.transdate >= ? and a.transdate <= ?|;
+
+  my $itimes_ar = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
+
+  if ( scalar @{ $itimes_ap } > 0 ) {
+    $self->tester->ok(0, "Inkonsistente Zeitstempel in ar gefunden. Bei folgenden ids:");
+    for my $bogus_time (@{ $itimes_ar }) {
+      $self->tester->diag("ID: $bogus_time->{id} itime: $bogus_time->{itime} mtime: $bogus_time->{mtime} ");
+    }
+  } else {
+    $self->tester->ok(1, "Keine inkonsistenten Zeitstempel in ar.");
+  }
+  $query = qq|
+    SELECT itime, gldate, id, mtime
+    FROM gl a
+    WHERE itime::date <> gldate::date
+    AND a.transdate >= ? and a.transdate <= ?|;
+
+  my $itimes_gl = selectall_hashref_query($::form, $self->dbh, $query, $self->fromdate, $self->todate);
+
+  if ( scalar @{ $itimes_gl } > 0 ) {
+    $self->tester->ok(0, "Inkonsistente Zeitstempel in gl gefunden. Bei folgenden ids:");
+    for my $bogus_time (@{ $itimes_ar }) {
+      $self->tester->diag("ID: $bogus_time->{id} itime: $bogus_time->{itime} mtime: $bogus_time->{mtime} ");
+    }
+  } else {
+    $self->tester->ok(1, "Keine inkonsistenten Zeitstempel in gl.");
+  }
 }
 
 1;
@@ -465,10 +815,6 @@ SL::BackgroundJob::SelfTest::Transactions - base tests
 
 Several tests for data integrity.
 
-=head1 FUNCTIONS
-
-=head1 BUGS
-
 =head1 AUTHOR
 
 G. Richardson E<lt>information@richardson-bueren.deE<gt>
@@ -476,4 +822,3 @@ Jan Büren E<lt>information@richardson-bueren.deE<gt>
 Sven Schoeling E<lt>s.schoeling@linet-services.deE<gt>
 
 =cut
-
diff --git a/SL/BackgroundJob/SetNumberRange.pm b/SL/BackgroundJob/SetNumberRange.pm
new file mode 100644 (file)
index 0000000..a7976f4
--- /dev/null
@@ -0,0 +1,42 @@
+package SL::BackgroundJob::SetNumberRange;
+
+use strict;
+
+use parent qw(SL::BackgroundJob::Base);
+
+use SL::PrefixedNumber;
+
+use DateTime::Format::Strptime;
+
+sub create_job {
+  $_[0]->create_standard_job('59 23 31 12 *'); # one minute before new year
+}
+
+
+sub run {
+  my ($self, $db_obj) = @_;
+  my $data       = $db_obj->data_as_hash;
+
+  if ($data->{digits_year} && !($data->{digits_year} == 2 || $data->{digits_year} == 4)) {
+    die "No valid input for digits_year should be 2 or 4.";
+  }
+  if ($data->{multiplier}  && !($data->{multiplier} % 10 == 0)) {
+    die "No valid input for multiplier should be 10, 100, .., 1000000";
+  }
+  my $next_year  = DateTime->today_local->truncate(to => 'year')->add(years => 1)->year();
+  $next_year     = ($data->{digits_year} == 2) ? substr($next_year, 2, 2) : $next_year;
+  my $multiplier = $data->{multiplier} || 100;
+
+  my $defaults   = SL::DB::Default->get;
+
+  foreach (qw(invnumber cnnumber sonumber ponumber sqnumber rfqnumber sdonumber pdonumber)) {
+    my $current_number = SL::PrefixedNumber->new(number => $defaults->{$_});
+    $current_number->set_to($next_year * $multiplier);
+    $defaults->{$_} = $current_number->get_current;
+  }
+  $defaults->save() || die "Could not change number ranges";
+
+  return exists $data->{result} ? $data->{result} : 1;
+}
+
+1;
diff --git a/SL/BackgroundJob/ShopOrderMassTransfer.pm b/SL/BackgroundJob/ShopOrderMassTransfer.pm
new file mode 100644 (file)
index 0000000..62437a7
--- /dev/null
@@ -0,0 +1,104 @@
+package SL::BackgroundJob::ShopOrderMassTransfer;
+
+use strict;
+use warnings;
+
+use parent qw(SL::BackgroundJob::Base);
+
+use SL::DBUtils;
+use SL::DB::ShopOrder;
+use SL::DB::ShopOrderItem;
+use SL::DB::Order;
+use SL::DB::History;
+use SL::DB::DeliveryOrder;
+use SL::DB::Inventory;
+use Sort::Naturally ();
+use SL::Locale::String qw(t8);
+
+use constant WAITING_FOR_EXECUTION        => 0;
+use constant CONVERTING_TO_ORDER          => 1;
+use constant DONE                         => 2;
+
+# Data format:
+# my $data                  = {
+#     shop_order_record_ids       => [ 603, 604, 605],
+#     num_order_created           => 0,
+#     orders_ids                  => [1,2,3]
+#     conversion_errors         => [ { id => 603 , item => 2, message => "Out of stock"}, ],
+# };
+#
+
+sub create_order {
+  my ( $self ) = @_;
+  my $job_obj = $self->{job_obj};
+  my $db      = $job_obj->db;
+  $job_obj->set_data(CONVERTING_TO_ORDER())->save;
+
+  my $data = $job_obj->data_as_hash;
+  foreach my $shop_order_id (@{ $data->{shop_order_record_ids} }) {
+    my $shop_order = SL::DB::ShopOrder->new(id => $shop_order_id)->load;
+    unless($shop_order){
+      push @{ $data->{conversion_errors} }, { id => $shop_order->id, number => $shop_order->shop_ordernumber, message => t8('Shoporder not found') };
+      $job_obj->update_attributes(data_as_hash => $data);
+    }
+    my $customer = SL::DB::Manager::Customer->find_by(id => $shop_order->{kivi_customer_id});
+    unless($customer){
+      push @{ $data->{conversion_errors} }, { id => $shop_order->id, number => $shop_order->shop_ordernumber, message => t8('Customer not found') };
+      $job_obj->update_attributes(data_as_hash => $data);
+    }
+    my $employee = SL::DB::Manager::Employee->current;
+    my $items = SL::DB::Manager::ShopOrderItem->get_all( where => [shop_order_id => $shop_order_id], );
+
+    if ($customer->{order_lock} == 0) {
+      $shop_order->{shop_order_items} = $items;
+
+      $db->with_transaction( sub {
+        my $order = $shop_order->convert_to_sales_order(customer => $customer, employee => $employee);
+
+        if ($order->{error}){
+          push @{ $data->{conversion_errors} }, { id => $shop_order->id, number => $shop_order->shop_ordernumber, message => \@{$order->{errors}} };
+          $job_obj->update_attributes(data_as_hash => $data);
+        }else{
+          $order->save;
+          $order->calculate_prices_and_taxes;
+          my $snumbers = "ordernumber_" . $order->ordnumber;
+          SL::DB::History->new(
+                            trans_id    => $order->id,
+                            snumbers    => $snumbers,
+                            employee_id => SL::DB::Manager::Employee->current->id,
+                            addition    => 'SAVED',
+                            what_done   => 'Shopimport->Order(MassTransfer)',
+                          )->save();
+          $shop_order->transferred(1);
+          $shop_order->transfer_date(DateTime->now_local);
+          $shop_order->save;
+          $shop_order->link_to_record($order);
+          $data->{num_order_created} ++;
+          push @{ $data->{orders_ids} }, $order->id;
+          push @{ $data->{shop_orders_ids} }, $shop_order->id;
+
+          $job_obj->update_attributes(data_as_hash => $data);
+        }
+        1;
+      })or do {
+        push @{ $data->{conversion_errors} }, { id => $shop_order->id, number => $shop_order->shop_ordernumber, message => $@ };
+        $job_obj->update_attributes(data_as_hash => $data);
+      }
+    }else{
+      push @{ $data->{conversion_errors} }, { id => $shop_order->id, number => $shop_order->shop_ordernumber, message => t8('Customerorderlock') };
+      $job_obj->update_attributes(data_as_hash => $data);
+    }
+  }
+}
+
+sub run {
+  my ($self, $job_obj) = @_;
+
+  $self->{job_obj}         = $job_obj;
+  $self->create_order;
+
+  $job_obj->set_data(status => DONE())->save;
+
+  return 1;
+}
+1;
diff --git a/SL/BackgroundJob/ShopPartMassUpload.pm b/SL/BackgroundJob/ShopPartMassUpload.pm
new file mode 100644 (file)
index 0000000..ddaf57f
--- /dev/null
@@ -0,0 +1,73 @@
+package SL::BackgroundJob::ShopPartMassUpload;
+
+use strict;
+use warnings;
+
+use parent qw(SL::BackgroundJob::Base);
+
+use SL::DBUtils;
+use SL::DB::ShopPart;
+use SL::Shop;
+
+use constant WAITING_FOR_EXECUTION        => 0;
+use constant UPLOAD_TO_WEBSHOP            => 1;
+use constant DONE                         => 2;
+
+# Data format:
+# my $data                  = {
+#     shop_part_record_ids         => [ 603, 604, 605 ],
+#     todo                         => $::form->{upload_todo},
+#     status                       => SL::BackgroundJob::ShopPartMassUpload->WAITING_FOR_EXECUTION(),
+#     num_uploaded                 => 0,
+#     conversation                 => [ { id => 603 , number => 2, message => "Ok" or $@ }, ],
+# };
+
+sub update_webarticles {
+  my ( $self ) = @_;
+
+  my $job_obj = $self->{job_obj};
+  my $db      = $job_obj->db;
+
+  $job_obj->set_data(UPLOAD_TO_WEBSHOP())->save;
+  my $num_uploaded = 0;
+  foreach my $shop_part_id (@{ $job_obj->data_as_hash->{shop_part_record_ids} }) {
+    my $data  = $job_obj->data_as_hash;
+    eval {
+      my $shop_part = SL::DB::Manager::ShopPart->find_by(id => $shop_part_id);
+      unless($shop_part){
+        push @{ $data->{conversion} }, { id => $shop_part_id, number => '', message => 'Shoppart not found' };
+      }
+
+      my $shop = SL::Shop->new( config => $shop_part->shop );
+
+      my $return    = $shop->connector->update_part($shop_part, $data->{todo});
+      if ( $return == 1 ) {
+        my $now = DateTime->now;
+        my $attributes->{last_update} = $now;
+        $shop_part->assign_attributes(%{ $attributes });
+        $shop_part->save;
+        $data->{num_uploaded} = $num_uploaded++;
+        push @{ $data->{conversion} }, { id => $shop_part_id, number => $shop_part->part->partnumber, message => 'uploaded' };
+      }else{
+      push @{ $data->{conversion} }, { id => $shop_part_id, number => $shop_part->part->partnumber, message => $return };
+      }
+      1;
+    } or do {
+      push @{ $data->{conversion} }, { id => $shop_part_id, number => '', message => $@ };
+    };
+
+    $job_obj->update_attributes(data_as_hash => $data);
+  }
+}
+
+sub run {
+  my ($self, $job_obj) = @_;
+
+  $self->{job_obj}         = $job_obj;
+  $self->update_webarticles;
+
+  $job_obj->set_data(status => DONE())->save;
+
+  return 1;
+}
+1;
index c8c76db..a70b7a3 100644 (file)
@@ -4,11 +4,13 @@ use strict;
 
 use parent qw(SL::BackgroundJob::Base);
 
+use SL::System::TaskServer;
+
 sub run {
   my ($self, $db_obj) = @_;
   my $data            = $db_obj->data_as_hash;
 
-  $::lxdebug->message(0, "Test job is being executed.");
+  $::lxdebug->message(0, "Test job ID " . $db_obj->id . " is being executed on node " . SL::System::TaskServer::node_id() . ".");
 
   die "Oh cruel world: " . $data->{exception} if $data->{exception};
 
index 2c4c221..019d44f 100644 (file)
--- a/SL/CA.pm
+++ b/SL/CA.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 # chart of accounts
 #
@@ -40,6 +41,7 @@ use strict;
 package CA;
 use Data::Dumper;
 use SL::DBUtils;
+use SL::DB;
 
 sub all_accounts {
   $main::lxdebug->enter_sub();
@@ -49,7 +51,7 @@ sub all_accounts {
   my (%amount, $acc_cash_where);
 
   # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   # bug 1071 Warum sollte bei Erreichen eines neuen Jahres die Kontenübersicht nur noch die
   # bereits bebuchten Konten anzeigen?
@@ -99,11 +101,11 @@ sub all_accounts {
       c.pos_eur,
       c.valid_from,
       c.datevautomatik,
-      comma(tk.startdate::text) AS startdate,
-      comma(tk.taxkey_id::text) AS taxkey,
-      comma(tx.taxdescription || to_char (tx.rate, '99V99' ) || '%') AS taxdescription,
-      comma(tx.taxnumber::text) AS taxaccount,
-      comma(tk.pos_ustva::text) AS tk_ustva,
+      array_agg(tk.startdate) AS startdates,
+      array_agg(tk.taxkey_id) AS taxkeys,
+      array_agg(tx.taxdescription || to_char (tx.rate, '99V99' ) || '%') AS taxdescriptions,
+      array_agg(taxchart.accno) AS taxaccounts,
+      array_agg(tk.pos_ustva) AS pos_ustvas,
       ( SELECT accno
       FROM chart c2
       WHERE c2.id = c.id
@@ -111,6 +113,7 @@ sub all_accounts {
     FROM chart c
     LEFT JOIN taxkeys tk ON (c.id = tk.chart_id)
     LEFT JOIN tax tx ON (tk.tax_id = tx.id)
+    LEFT JOIN chart taxchart ON (taxchart.id = tx.chart_id)
     WHERE 1=1
     $where
     GROUP BY c.accno, c.id, c.description, c.charttype,
@@ -134,7 +137,6 @@ sub all_accounts {
   }
 
   $sth->finish;
-  $dbh->disconnect;
 
   $main::lxdebug->leave_sub();
 }
@@ -144,8 +146,7 @@ sub all_transactions {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   # get chart_id
   my $query = qq|SELECT id FROM chart WHERE accno = ?|;
@@ -264,7 +265,7 @@ sub all_transactions {
 
     # get all transactions
     $query =
-      qq|SELECT a.id, a.reference, a.description, ac.transdate, ac.chart_id, | .
+      qq|SELECT ac.itime, a.id, a.reference, a.description, ac.transdate, ac.chart_id, | .
       qq|  FALSE AS invoice, ac.amount, 'gl' as module, | .
       qq§(SELECT accno||'--'||rate FROM tax LEFT JOIN chart ON (tax.chart_id=chart.id) WHERE tax.id = (SELECT tax_id FROM taxkeys WHERE taxkey_id = ac.taxkey AND taxkeys.startdate <= ac.transdate ORDER BY taxkeys.startdate DESC LIMIT 1)) AS taxinfo, ac.source || ' ' || ac.memo AS memo § .
       qq|FROM acc_trans ac, gl a | .
@@ -276,7 +277,7 @@ sub all_transactions {
 
       qq|UNION ALL | .
 
-      qq|SELECT a.id, a.invnumber, c.name, ac.transdate, ac.chart_id, | .
+      qq|SELECT ac.itime, a.id, a.invnumber, c.name, ac.transdate, ac.chart_id, | .
       qq|  a.invoice, ac.amount, 'ar' as module, | .
       qq§(SELECT accno||'--'||rate FROM tax LEFT JOIN chart ON (tax.chart_id=chart.id) WHERE tax.id = (SELECT tax_id FROM taxkeys WHERE taxkey_id = ac.taxkey AND taxkeys.startdate <= ac.transdate ORDER BY taxkeys.startdate DESC LIMIT 1)) AS taxinfo, ac.source || ' ' || ac.memo AS memo  § .
       qq|FROM acc_trans ac, customer c, ar a | .
@@ -289,7 +290,7 @@ sub all_transactions {
 
       qq|UNION ALL | .
 
-      qq|SELECT a.id, a.invnumber, v.name, ac.transdate, ac.chart_id, | .
+      qq|SELECT ac.itime, a.id, a.invnumber, v.name, ac.transdate, ac.chart_id, | .
       qq|  a.invoice, ac.amount, 'ap' as module, | .
       qq§(SELECT accno||'--'||rate FROM tax LEFT JOIN chart ON (tax.chart_id=chart.id) WHERE tax.id = (SELECT tax_id FROM taxkeys WHERE taxkey_id = ac.taxkey AND taxkeys.startdate <= ac.transdate ORDER BY taxkeys.startdate DESC LIMIT 1)) AS taxinfo, ac.source || ' ' || ac.memo AS memo  § .
       qq|FROM acc_trans ac, vendor v, ap a | .
@@ -325,7 +326,7 @@ sub all_transactions {
       $query .=
         qq|UNION ALL | .
 
-        qq|SELECT a.id, a.invnumber, c.name, a.transdate, | .
+        qq|SELECT ac.itime, a.id, a.invnumber, c.name, a.transdate, | .
         qq|  a.invoice, ac.qty * ac.sellprice AS sellprice, 'ar' as module, | .
         qq§(SELECT accno||'--'||rate FROM tax LEFT JOIN chart ON (tax.chart_id=chart.id) WHERE tax.id = (SELECT tax_id FROM taxkeys WHERE taxkey_id = ac.taxkey AND taxkeys.startdate <= ac.transdate ORDER BY taxkeys.startdate DESC LIMIT 1)) AS taxinfo § .
         qq|FROM ar a | .
@@ -340,7 +341,7 @@ sub all_transactions {
         $project .
         qq|UNION ALL | .
 
-        qq|SELECT a.id, a.invnumber, v.name, a.transdate, | .
+        qq|SELECT ac.itime, a.id, a.invnumber, v.name, a.transdate, | .
         qq|  a.invoice, ac.qty * ac.sellprice AS sellprice, 'ap' as module, | .
         qq§(SELECT accno||'--'||rate FROM tax LEFT JOIN chart ON (tax.chart_id=chart.id) WHERE tax.id = (SELECT tax_id FROM taxkeys WHERE taxkey_id = ac.taxkey AND taxkeys.startdate <= ac.transdate ORDER BY taxkeys.startdate DESC LIMIT 1)) AS taxinfo § .
         qq|FROM ap a | .
@@ -366,7 +367,8 @@ sub all_transactions {
   }
 
   my $sort = grep({ $form->{sort} eq $_ } qw(transdate reference description)) ? $form->{sort} : 'transdate';
-  my $sort2 = ($sort eq 'reference')?'transdate':'reference';
+  $sort = ($sort eq 'transdate') ? 'transdate, itime' : $sort;
+  my $sort2 = ($sort eq 'reference') ? 'transdate, itime' : 'reference';
   $query .= qq|ORDER BY $sort , $sort2 |;
   my $sth = prepare_execute_query($form, $dbh, $query, @values);
 
@@ -424,7 +426,6 @@ sub all_transactions {
   }
 
   $sth->finish;
-  $dbh->disconnect;
 
   $main::lxdebug->leave_sub();
 }
index a2a2476..75e753e 100644 (file)
--- a/SL/CP.pm
+++ b/SL/CP.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Check and receipt printing payment module backend routines
@@ -36,6 +37,7 @@
 
 package CP;
 use SL::DBUtils;
+use SL::DB;
 
 use strict;
 
@@ -66,8 +68,7 @@ sub paymentaccounts {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $ARAP = $form->{ARAP} eq "AR" ? "AR" : "AP";
 
@@ -76,7 +77,7 @@ sub paymentaccounts {
     qq|FROM chart | .
     qq|WHERE link LIKE ? |.
     qq|ORDER BY accno|;
-  my $sth = prepare_execute_query($form, $dbh, $query, '%' . $ARAP . '%');
+  my $sth = prepare_execute_query($form, $dbh, $query, like($ARAP));
 
   $form->{PR}{ $form->{ARAP} } = ();
   $form->{PR}{"$form->{ARAP}_paid"} = ();
@@ -97,41 +98,6 @@ sub paymentaccounts {
   $query = qq|SELECT closedto FROM defaults|;
   ($form->{closedto}) = selectrow_query($form, $dbh, $query);
 
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-}
-
-sub get_openvc {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  my $dbh = $form->dbconnect($myconfig);
-
-  my $arap = ($form->{vc} eq 'customer') ? 'ar' : 'ap';
-  my $vc = $form->{vc} eq "customer" ? "customer" : "vendor";
-  my $query =
-    qq|SELECT count(*) | .
-    qq|FROM $vc ct, $arap a | .
-    qq|WHERE (a.${vc}_id = ct.id) AND (a.amount != a.paid)|;
-  my ($count) = selectrow_query($form, $dbh, $query);
-
-  # build selection list
-  if ($count < $myconfig->{vclimit}) {
-    $query =
-      qq|SELECT DISTINCT ct.id, ct.name | .
-      qq|FROM $vc ct, $arap a | .
-      qq|WHERE (a.${vc}_id = ct.id) AND (a.amount != a.paid) | .
-      qq|ORDER BY ct.name|;
-    $form->{"all_$form->{vc}"} = selectall_hashref_query($form, $dbh, $query);
-  }
-
-  # aufruf für all_deparments rausgenommen, da die abteilungen nur
-  # beim buchen der belege (rechnung, fibu) geändert werden und danach
-  # NICHT mehr überschrieben werden
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
@@ -141,7 +107,7 @@ sub get_openinvoices {
   my ($self, $myconfig, $form) = @_;
 
   # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $vc = $form->{vc} eq "customer" ? "customer" : "vendor";
 
@@ -186,19 +152,24 @@ sub get_openinvoices {
 SQL
   ($form->{openinvoices_other_currencies}) = selectfirst_array_query($form, $dbh, $query, conv_i($form->{"${vc}_id"}), "$form->{currency}");
 
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
 sub process_payment {
+  my ($self, $myconfig, $form) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_process_payment, $self, $myconfig, $form);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _process_payment {
   my ($self, $myconfig, $form) = @_;
   my $amount;
 
-  # connect to database, turn AutoCommit off
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my ($paymentaccno) = split /--/, $form->{account};
 
@@ -356,18 +327,8 @@ sub process_payment {
       # /saving the history
     }
   }
-  my $rc;
-  # Hier wurden negativen Zahlungseingänge abgefangen
-  # da Zahlungsein- und ausgänge immer positiv sind
-  # Besser: in Oberfläche schon prüfen erledigt jb 10.2010
-    $rc = $dbh->commit;
-
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
 
-  return $rc;
+  return 1;
 }
 
 1;
-
index cdf274b..b52c879 100644 (file)
--- a/SL/CT.pm
+++ b/SL/CT.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # backend code for customers and vendors
@@ -40,6 +41,9 @@ package CT;
 use SL::Common;
 use SL::CVar;
 use SL::DBUtils;
+use SL::DB;
+use SL::Util qw(trim);
+use Text::ParseWords;
 
 use strict;
 
@@ -75,11 +79,15 @@ sub search {
       "zipcode"            => "ct.zipcode",
       "city"               => "ct.city",
       "country"            => "ct.country",
+      "gln"                => "ct.gln",
       "discount"           => "ct.discount",
       "insertdate"         => "ct.itime",
       "salesman"           => "e.name",
       "payment"            => "pt.description",
       "pricegroup"         => "pg.pricegroup",
+      "ustid"              => "ct.ustid",
+      "creditlimit"        => "ct.creditlimit",
+      "commercial_court"   => "ct.commercial_court",
     );
 
   $form->{sort} ||= "name";
@@ -94,7 +102,7 @@ sub search {
   }
   my $sortdir   = !defined $form->{sortdir} ? 'ASC' : $form->{sortdir} ? 'ASC' : 'DESC';
 
-  if ($sortorder !~ /(business|id|discount|itime)/ && !$join_records) {
+  if ($sortorder !~ /(business|creditlimit|id|discount|itime)/ && !$join_records) {
     $sortorder  = "lower($sortorder) ${sortdir}";
   } else {
     $sortorder .= " ${sortdir}";
@@ -102,19 +110,41 @@ sub search {
 
   if ($form->{"${cv}number"}) {
     $where .= " AND ct.${cv}number ILIKE ?";
-    push(@values, '%' . $form->{"${cv}number"} . '%');
+    push(@values, like($form->{"${cv}number"}));
   }
 
   foreach my $key (qw(name contact email)) {
     if ($form->{$key}) {
       $where .= " AND ct.$key ILIKE ?";
-      push(@values, '%' . $form->{$key} . '%');
+      push(@values, like($form->{$key}));
     }
   }
 
   if ($form->{cp_name}) {
     $where .= " AND ct.id IN (SELECT cp_cv_id FROM contacts WHERE lower(cp_name) LIKE lower(?))";
-    push @values, '%' . $form->{cp_name} . '%';
+    push @values, like($form->{cp_name});
+  }
+
+  if ($form->{addr_street}) {
+    $where .= qq| AND ((ct.street ILIKE ?) | .
+              qq|      OR | .
+              qq|      (ct.id IN ( | .
+              qq|         SELECT sc.trans_id FROM shipto sc | .
+              qq|         WHERE (sc.module = 'CT') | .
+              qq|           AND (sc.shiptostreet ILIKE ?) | .
+              qq|      ))) |;
+    push @values, (like($form->{addr_street})) x 2;
+  }
+
+  if ($form->{addr_zipcode}) {
+    $where .= qq| AND ((ct.zipcode ILIKE ?) | .
+              qq|      OR | .
+              qq|      (ct.id IN ( | .
+              qq|         SELECT sc.trans_id FROM shipto sc | .
+              qq|         WHERE (sc.module = 'CT') | .
+              qq|           AND (sc.shiptozipcode ILIKE ?) | .
+              qq|      ))) |;
+    push @values, (like($form->{addr_zipcode})) x 2;
   }
 
   if ($form->{addr_city}) {
@@ -127,7 +157,7 @@ sub search {
                           AND (lower(sc.shiptocity) LIKE lower(?))
                       ))
                      )";
-    push @values, ('%' . $form->{addr_city} . '%') x 2;
+    push @values, (like($form->{addr_city})) x 2;
   }
 
   if ($form->{addr_country}) {
@@ -140,7 +170,20 @@ sub search {
                           AND (lower(so.shiptocountry) LIKE lower(?))
                       ))
                      )";
-    push @values, ('%' . $form->{addr_country} . '%') x 2;
+    push @values, (like($form->{addr_country})) x 2;
+  }
+
+  if ($form->{addr_gln}) {
+    $where .= " AND ((lower(ct.gln) LIKE lower(?))
+                     OR
+                     (ct.id IN (
+                        SELECT so.trans_id
+                        FROM shipto so
+                        WHERE (so.module = 'CT')
+                          AND (lower(so.shiptogln) LIKE lower(?))
+                      ))
+                     )";
+    push @values, (like($form->{addr_gln})) x 2;
   }
 
   if ( $form->{status} eq 'orphaned' ) {
@@ -188,11 +231,40 @@ sub search {
     push @values, conv_date($form->{insertdateto});
   }
 
-  # Nur Kunden finden, bei denen ich selber der Verkäufer bin
-  # Gilt nicht für Lieferanten
-  if ($cv eq 'customer' &&   !$main::auth->assert('customer_vendor_all_edit', 1)) {
-    $where .= qq| AND ct.salesman_id = (select em.id from employee em where em.login = ?)|;
-    push(@values, $::myconfig{login});
+  if ($form->{all}) {
+    my @tokens = parse_line('\s+', 0, $form->{all});
+      $where .= qq| AND (
+          ct.${cv}number ILIKE ? OR
+          ct.name        ILIKE ?
+          )| for @tokens;
+    push @values, ("%$_%")x2 for @tokens;
+  }
+
+  if (($form->{create_zugferd_invoices} // '') ne '') {
+    $where .= qq| AND (ct.create_zugferd_invoices = ?)|;
+    push @values, $form->{create_zugferd_invoices};
+  }
+
+  if ($form->{all_phonenumbers}) {
+    my $search_term = trim($form->{all_phonenumbers});
+    $search_term    =~ s{\p{WSpace}+}{}g;
+    $search_term    = join ' *', split(//, $search_term);
+
+    $where .= qq| AND (ct.phone ~* ? OR
+                       ct.fax   ~* ? OR
+                       ct.id    IN
+                         (SELECT cp_cv_id FROM contacts
+                          WHERE cp_phone1      ~* ? OR
+                                cp_phone2      ~* ? OR
+                                cp_fax         ~* ? OR
+                                cp_mobile1     ~* ? OR
+                                cp_mobile2     ~* ? OR
+                                cp_satphone    ~* ? OR
+                                cp_satfax      ~* ? OR
+                                cp_privatphone ~* ?
+                         )
+    )|;
+    push @values, ($search_term)x10;
   }
 
   my ($cvar_where, @cvar_values) = CVar->build_filter_query('module'         => 'CT',
@@ -204,22 +276,20 @@ sub search {
     push @values, @cvar_values;
   }
 
-  if ($form->{addr_street}) {
-    $where .= qq| AND (ct.street ILIKE ?)|;
-    push @values, '%' . $form->{addr_street} . '%';
-  }
+  my $pg_select = $form->{l_pricegroup} ? qq|, pg.pricegroup as pricegroup | : '';
+  my $pg_join   = $form->{l_pricegroup} ? qq|LEFT JOIN pricegroup pg ON (ct.pricegroup_id = pg.id) | : '';
 
-  if ($form->{addr_zipcode}) {
-    $where .= qq| AND (ct.zipcode ILIKE ?)|;
-    push @values, $form->{addr_zipcode} . '%';
+  my $main_cp_select = '';
+  if ($form->{l_main_contact_person}) {
+    $main_cp_select =  qq/, (SELECT concat(cp.cp_givenname, ' ', cp.cp_name, ' | ', cp.cp_email, ' | ', cp.cp_phone1)
+                              FROM contacts cp WHERE ct.id=cp.cp_cv_id AND cp.cp_main LIMIT 1)
+                              AS main_contact_person /;
   }
-
-  my $pg_select = $form->{l_pricegroup} ? qq|, pg.pricegroup as pricegroup | : '';
-  my $pg_join   = $form->{l_pricegroup} ? qq|LEFT JOIN pricegroup pg ON (ct.klass = pg.id) | : '';
   my $query =
     qq|SELECT ct.*, ct.itime::DATE AS insertdate, b.description AS business, e.name as salesman, | .
     qq|  pt.description as payment | .
     $pg_select .
+    $main_cp_select .
     (qq|, NULL AS invnumber, NULL AS ordnumber, NULL AS quonumber, NULL AS invid, NULL AS module, NULL AS formtype, NULL AS closed | x!! $join_records) .
     qq|FROM $cv ct | .
     qq|LEFT JOIN business b ON (ct.business_id = b.id) | .
@@ -242,6 +312,7 @@ sub search {
         qq|SELECT ct.*, ct.itime::DATE AS insertdate, b.description AS business, e.name as salesman, | .
         qq|  pt.description as payment | .
         $pg_select .
+        $main_cp_select .
         qq|, a.invnumber, a.ordnumber, a.quonumber, a.id AS invid, | .
         qq|  '$module' AS module, 'invoice' AS formtype, | .
         qq|  (a.amount = a.paid) AS closed | .
@@ -261,6 +332,7 @@ sub search {
         qq|SELECT ct.*, ct.itime::DATE AS insertdate, b.description AS business, e.name as salesman, | .
         qq|  pt.description as payment | .
         $pg_select .
+        $main_cp_select .
         qq|, ' ' AS invnumber, o.ordnumber, o.quonumber, o.id AS invid, | .
         qq|  'oe' AS module, 'order' AS formtype, o.closed | .
         qq|FROM $cv ct | .
@@ -279,6 +351,7 @@ sub search {
         qq|SELECT ct.*, ct.itime::DATE AS insertdate, b.description AS business, e.name as salesman, | .
         qq|  pt.description as payment | .
         $pg_select .
+        $main_cp_select .
         qq|, ' ' AS invnumber, o.ordnumber, o.quonumber, o.id AS invid, | .
         qq|  'oe' AS module, 'quotation' AS formtype, o.closed | .
         qq|FROM $cv ct | .
@@ -305,7 +378,7 @@ sub get_contact {
 
   die 'Missing argument: cp_id' unless $::form->{cp_id};
 
-  my $dbh   = $form->dbconnect($myconfig);
+  my $dbh   = SL::DB->client->dbh;
   my $query =
     qq|SELECT * FROM contacts c | .
     qq|WHERE cp_id = ? ORDER BY cp_id limit 1|;
@@ -323,7 +396,6 @@ sub get_contact {
   ($form->{cp_used}) = selectfirst_array_query($form, $dbh, $query, ($form->{cp_id})x2);
 
   $sth->finish;
-  $dbh->disconnect;
 
   $main::lxdebug->leave_sub();
 }
@@ -395,7 +467,7 @@ sub search_contacts {
       'cp.cp_name      ILIKE ?',
       'cp.cp_givenname ILIKE ?',
       'cp.cp_email     ILIKE ?';
-    push @values, ('%' . $params{search_term} . '%') x 3;
+    push @values, (like($params{search_term})) x 3;
 
     if (($params{search_term} =~ m/\d/) && ($params{search_term} !~ m/[^\d \(\)+\-]/)) {
       my $number =  $params{search_term};
index 3bc1386..6989e7e 100644 (file)
--- a/SL/CTI.pm
+++ b/SL/CTI.pm
@@ -30,7 +30,13 @@ sub call {
 sub call_link {
   my ($class, %params) = @_;
 
-  return "controller.pl?action=CTI/call&number=" . uri_encode($class->sanitize_number(number => $params{number})) . ($params{internal} ? '&internal=1' : '');
+  my $config           = $::lx_office_conf{cti} || {};
+
+  if ($config->{dial_command}) {
+    return "controller.pl?action=CTI/call&number=" . uri_encode($class->sanitize_number(number => $params{number})) . ($params{internal} ? '&internal=1' : '');
+  } else {
+    return 'callto://' . uri_encode($class->sanitize_number(number => $params{number}));
+  }
 }
 
 sub sanitize_number {
index 0ace069..637f129 100644 (file)
@@ -2,6 +2,7 @@ package CVar;
 
 use strict;
 
+use Carp;
 use List::MoreUtils qw(any);
 use List::Util qw(first);
 use Scalar::Util qw(blessed);
@@ -9,6 +10,9 @@ use Data::Dumper;
 
 use SL::DBUtils;
 use SL::MoreCommon qw(listify);
+use SL::Presenter::Text;
+use SL::Util qw(trim);
+use SL::DB;
 
 sub get_configs {
   $main::lxdebug->enter_sub();
@@ -43,7 +47,7 @@ SQL
       } elsif ($config->{type} eq 'number') {
         $config->{precision} = $1 if ($config->{options} =~ m/precision=(\d+)/i);
 
-      } elsif ($config->{type} eq 'textfield') {
+      } elsif ($config->{type} =~ m{^(?:html|text)field$}) {
         $config->{width}  = 30;
         $config->{height} =  5;
         $config->{width}  = $1 if ($config->{options} =~ m/width=(\d+)/i);
@@ -109,7 +113,7 @@ sub get_custom_variables {
   my $custom_variables = $self->get_configs(module => $params{module});
 
   foreach my $cvar (@{ $custom_variables }) {
-    if ($cvar->{type} eq 'textfield') {
+    if ($cvar->{type} =~ m{^(?:html|text)field}) {
       $cvar->{width}  = 30;
       $cvar->{height} =  5;
 
@@ -133,7 +137,7 @@ sub get_custom_variables {
       do_statement($form, $h_var, $q_var, @values);
       $act_var = $h_var->fetchrow_hashref();
 
-      $valid = $self->get_custom_variables_validity(config_id => $cvar->{id}, trans_id => $params{trans_id});
+      $valid = $self->get_custom_variables_validity(config_id => $cvar->{id}, trans_id => $params{trans_id}, sub_module => $params{sub_module});
     } else {
       $valid = !$cvar->{flag_defaults_to_invalid};
     }
@@ -198,8 +202,16 @@ sub get_custom_variables {
 }
 
 sub save_custom_variables {
+  my ($self, %params) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_save_custom_variables, $self, %params);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _save_custom_variables {
   my $self     = shift;
   my %params   = @_;
 
@@ -208,7 +220,7 @@ sub save_custom_variables {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
+  my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
   my @configs  = $params{configs} ? @{ $params{configs} } : grep { $_->{module} eq $params{module} } @{ CVar->get_configs() };
 
@@ -233,11 +245,34 @@ sub save_custom_variables {
   my $sth = prepare_query($form, $dbh, $query);
 
   foreach my $config (@configs) {
+    if ($params{save_validity}) {
+      my $valid_index = "$params{name_prefix}cvar_$config->{name}$params{name_postfix}_valid";
+      my $new_valid   = $params{variables}{$valid_index} || $params{always_valid} ? 1 : 0;
+      my $old_valid   = $self->get_custom_variables_validity(trans_id => $params{trans_id}, config_id => $config->{id});
+
+      $self->save_custom_variables_validity(trans_id  => $params{trans_id},
+                                            config_id => $config->{id},
+                                            validity  => $new_valid,
+                                           );
+
+      if (!$new_valid || !$old_valid) {
+        # When activating a cvar (old_valid == 0 && new_valid == 1)
+        # the input to hold the variable's value wasn't actually
+        # rendered, meaning saving the value now would only save an
+        # empty value/the value 0. This means that the next time the
+        # form is rendered, an existing value is found and used
+        # instead of the variable's default value from the
+        # configuration. Therefore don't save the values in such
+        # cases.
+        next;
+      }
+    }
+
     my @values = (conv_i($config->{id}), "$params{sub_module}", conv_i($params{trans_id}));
 
     my $value  = $params{variables}->{"$params{name_prefix}cvar_$config->{name}$params{name_postfix}"};
 
-    if (($config->{type} eq 'text') || ($config->{type} eq 'textfield') || ($config->{type} eq 'select')) {
+    if (any { $config->{type} eq $_ } qw(text textfield htmlfield select)) {
       push @values, undef, undef, $value, undef;
 
     } elsif (($config->{type} eq 'date') || ($config->{type} eq 'timestamp')) {
@@ -253,21 +288,11 @@ sub save_custom_variables {
     }
 
     do_statement($form, $sth, $query, @values);
-
-    if ($params{save_validity}) {
-      my $valid_index = "$params{name_prefix}cvar_$config->{name}$params{name_postfix}_valid";
-      $self->save_custom_variables_validity(trans_id  => $params{trans_id},
-                                            config_id => $config->{id},
-                                            validity  => ($params{variables}{$valid_index} || $params{always_valid} ? 1 : 0)
-                                           );
-    }
   }
 
   $sth->finish();
 
-  $dbh->commit() unless $params{dbh};
-
-  $main::lxdebug->leave_sub();
+  return 1;
 }
 
 sub render_inputs {
@@ -351,11 +376,11 @@ sub build_filter_query {
 
     my (@sub_values, @sub_where, $not);
 
-    if (($config->{type} eq 'text') || ($config->{type} eq 'textfield')) {
+    if (any { $config->{type} eq $_ } qw(text textfield htmlfield)) {
       next unless ($params{filter}->{$name});
 
       push @sub_where,  qq|cvar.text_value ILIKE ?|;
-      push @sub_values, '%' . $params{filter}->{$name} . '%'
+      push @sub_values, like($params{filter}->{$name});
 
     } elsif ($config->{type} eq 'select') {
       next unless ($params{filter}->{$name});
@@ -406,7 +431,7 @@ sub build_filter_query {
       }
 
       push @sub_where,  qq|cvar.number_value $op ?|;
-      push @sub_values, $form->parse_amount($myconfig, $params{filter}->{$name});
+      push @sub_values, $form->parse_amount($myconfig, trim($params{filter}->{$name}));
 
     } elsif ($config->{type} eq 'bool') {
       next unless ($params{filter}->{$name});
@@ -418,12 +443,12 @@ sub build_filter_query {
 
       my $table = $config->{type};
       push @sub_where, qq|cvar.number_value * 1 IN (SELECT id FROM $table WHERE name ILIKE ?)|;
-      push @sub_values, "%$params{filter}->{$name}%";
+      push @sub_values, like($params{filter}->{$name});
     } elsif ($config->{type} eq 'part') {
       next unless $params{filter}->{$name};
 
       push @sub_where, qq|cvar.number_value * 1 IN (SELECT id FROM parts WHERE partnumber ILIKE ?)|;
-      push @sub_values, "%$params{filter}->{$name}%";
+      push @sub_values, like($params{filter}->{$name});
     }
 
     if (@sub_where) {
@@ -501,6 +526,7 @@ sub add_custom_variables_to_report {
         : $cfg->{type} eq 'vendor'    ? (SL::DB::Manager::Vendor->find_by(id => 1*$ref->{number_value})   || SL::DB::Vendor->new)->name
         : $cfg->{type} eq 'part'      ? (SL::DB::Manager::Part->find_by(id => 1*$ref->{number_value})     || SL::DB::Part->new)->partnumber
         : $cfg->{type} eq 'bool'      ? ($ref->{bool_value} ? $locale->text('Yes') : $locale->text('No'))
+        : $cfg->{type} eq 'htmlfield' ? SL::Presenter::Text::stripped_html($ref->{text_value})
         :                               $ref->{text_value};
     }
   }
@@ -546,8 +572,16 @@ sub get_field_format_list {
 }
 
 sub save_custom_variables_validity {
+  my ($self, %params) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_save_custom_variables_validity, $self, %params);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _save_custom_variables_validity {
   my $self     = shift;
   my %params   = @_;
 
@@ -556,7 +590,7 @@ sub save_custom_variables_validity {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
+  my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
   my (@where, @values);
   add_token(\@where, \@values, col => "config_id", val => $params{config_id}, esc => \&conv_i);
@@ -582,14 +616,16 @@ sub save_custom_variables_validity {
 
   $sth->finish();
 
-  $dbh->commit() unless $params{dbh};
-
-  $main::lxdebug->leave_sub();
+  return 1;
 }
 
-sub get_custom_variables_validity {
-  $main::lxdebug->enter_sub(2);
+my %_validity_sub_module_mapping = (
+  orderitems           => { table => 'orderitems',           result_column => 'parts_id', trans_id_column => 'id', },
+  delivery_order_items => { table => 'delivery_order_items', result_column => 'parts_id', trans_id_column => 'id', },
+  invoice              => { table => 'invoice',              result_column => 'parts_id', trans_id_column => 'id', },
+);
 
+sub get_custom_variables_validity {
   my $self     = shift;
   my %params   = @_;
 
@@ -600,11 +636,29 @@ sub get_custom_variables_validity {
 
   my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
 
-  my $query    = qq|SELECT id FROM custom_variables_validity WHERE config_id = ? AND trans_id = ? LIMIT 1|;
+  my $query;
 
-  my ($invalid) = selectfirst_array_query($form, $dbh, $query, conv_i($params{config_id}), conv_i($params{trans_id}));
+  if ($params{sub_module}) {
+    my %mapping = %{ $_validity_sub_module_mapping{ $params{sub_module} } || croak("Invalid sub_module '" . $params{sub_module} . "'") };
+    $query = <<SQL;
+      SELECT cvv.id
+      FROM $mapping{table} mt
+      LEFT JOIN custom_variables_validity cvv ON (cvv.trans_id = mt.$mapping{result_column})
+      WHERE (cvv.config_id                = ?)
+        AND (mt.$mapping{trans_id_column} = ?)
+      LIMIT 1
+SQL
+  } else {
+    $query = <<SQL;
+      SELECT id
+      FROM custom_variables_validity
+      WHERE (config_id = ?)
+        AND (trans_id  = ?)
+      LIMIT 1
+SQL
+  }
 
-  $main::lxdebug->leave_sub(2);
+  my ($invalid) = selectfirst_array_query($form, $dbh, $query, conv_i($params{config_id}), conv_i($params{trans_id}));
 
   return !$invalid;
 }
@@ -724,7 +778,7 @@ SL::CVar.pm - Custom Variables module
 
 Suppose the following scenario:
 
-You have a lot of parts in your database, and a set of properties cofigured. Now not every part has every of these properties, some combinations will just make no sense. In order to clean up your inputs a bit, you want to mark certain combinations as invalid, blocking them from modification and possibly display.
+You have a lot of parts in your database, and a set of properties configured. Now not every part has every of these properties, some combinations will just make no sense. In order to clean up your inputs a bit, you want to mark certain combinations as invalid, blocking them from modification and possibly display.
 
 Validity is assumed. If you modify validity, you actually save B<invalidity>.
 Invalidity is saved as a function of config_id, and the trans_id
index 335bf81..49bde80 100644 (file)
@@ -9,8 +9,8 @@ use SL::JSON ();
 
 use Rose::Object::MakeMethods::Generic
 (
-  scalar                  => [ qw(controller) ],
-  'scalar --get_set_init' => [ qw(_actions _flash _error) ],
+  scalar                  => [ qw() ],
+  'scalar --get_set_init' => [ qw(controller _actions _flash _flash_detail _no_flash_clear _error) ],
 );
 
 my %supported_methods = (
@@ -75,12 +75,12 @@ my %supported_methods = (
 
   # ## jQuery UI dialog plugin ## pattern: $(<TARGET>).dialog('<FUNCTION>')
 
-  # Opening and closing and closing a popup
+  # Opening and closing a popup
   'dialog:open'          => 1, # kivi.popup_dialog(<TARGET>)
   'dialog:close'         => 1,
 
   # ## jQuery Form plugin ##
-  'ajaxForm'             => 1, # pattern: $(<TARGET>).ajaxForm({ success: eval_json_result })
+  'ajaxForm'             => 1, # $(<TARGET>).ajaxForm({ success: eval_json_result })
 
   # ## jstree plugin ## pattern: $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>)
 
@@ -113,13 +113,18 @@ my %supported_methods = (
 
   # ## other stuff ##
   redirect_to            => 1,  # window.location.href = <TARGET>
+  save_file              => 4,  # kivi.save_file(<TARGET>, <ARGS>)
 
   flash                  => 2,  # kivi.display_flash(<TARGET>, <ARGS>)
+  flash_detail           => 2,  # kivi.display_flash_detail(<TARGET>, <ARGS>)
+  clear_flash            => 2,  # kivi.clear_flash(<TARGET>, <ARGS>)
   reinit_widgets         => 0,  # kivi.reinit_widgets()
   run                    => -1, # kivi.run(<TARGET>, <ARGS>)
   run_once_for           => 3,  # kivi.run_once_for(<TARGET>, <ARGS>)
 
   scroll_into_view       => 1,  # $(<TARGET>)[0].scrollIntoView()
+
+  set_cursor_position    => 2,  # kivi.set_cursor_position(<TARGET>, <ARGS>)
 );
 
 my %trim_target_for = map { ($_ => 1) } qw(insertAfter insertBefore appendTo prependTo);
@@ -180,15 +185,23 @@ sub init__flash {
   return {};
 }
 
+sub init__flash_detail {
+  return {};
+}
+
 sub init__error {
   return '';
 }
 
+sub init__no_flash_clear {
+  return '';
+}
+
 sub to_json {
   my ($self) = @_;
 
-  return SL::JSON::to_json({ error        => $self->_error   }) if $self->_error;
-  return SL::JSON::to_json({ eval_actions => $self->_actions });
+  return SL::JSON::to_json({ error          => $self->_error   }) if $self->_error;
+  return SL::JSON::to_json({ no_flash_clear => $self->_no_flash_clear, eval_actions => $self->_actions });
 }
 
 sub to_array {
@@ -236,6 +249,27 @@ sub flash {
   return $self;
 }
 
+sub flash_detail {
+  my ($self, $type, @messages) = @_;
+
+  my $message = join '<br>', grep { $_ } @messages;
+
+  if (!$self->_flash_detail->{$type}) {
+    $self->_flash_detail->{$type} = [ 'flash_detail', $type, $message ];
+    push @{ $self->_actions }, $self->_flash_detail->{$type};
+  } else {
+    $self->_flash_detail->{$type}->[-1] .= ' ' . $message;
+  }
+
+  return $self;
+}
+
+sub no_flash_clear{
+  my ($self) = @_;
+  $self->_no_flash_clear('1');
+  return $self;
+}
+
 sub error {
   my ($self, @messages) = @_;
 
@@ -244,6 +278,12 @@ sub error {
   return $self;
 }
 
+sub init_controller {
+  # fallback
+  require SL::Controller::Base;
+  SL::Controller::Base->new;
+}
+
 1;
 __END__
 
@@ -263,20 +303,15 @@ First some JavaScript code:
   // In the client generate an AJAX request whose 'success' handler
   // calls "eval_json_result(data)":
   var data = {
-    action: "SomeController/the_action",
+    action: "SomeController/my_personal_action",
     id:     $('#some_input_field').val()
   };
   $.post("controller.pl", data, eval_json_result);
 
-Now some Perl code:
-
-  # In the controller itself. First, make sure that the "client_js.js"
-  # is loaded. This must be done when the whole side is loaded, so
-  # it's not in the action called by the AJAX request shown above.
-  $::request->layout->use_javascript('client_js.js');
+Now some Controller (perl) code for my personal action:
 
-  # Now in that action called via AJAX:
-  sub action_the_action {
+  # my personal action
+  sub action_my_personal_action {
     my ($self) = @_;
 
     # Create a new client-side JS object and do stuff with it!
@@ -315,7 +350,7 @@ This module enables the generation of jQuery-using JavaScript code on
 the server side. That code is then evaluated in a safe way on the
 client side.
 
-The workflow is usally that the client creates an AJAX request, the
+The workflow is usually that the client creates an AJAX request, the
 server creates some actions and sends them back, and the client then
 implements each of these actions.
 
@@ -457,6 +492,17 @@ C<flash> on the same C<$self> will be merged by type.
 
 On the client side the flashes of all types will be cleared after each
 successful ClientJS call that did not end with C<$js-E<gt>error(...)>.
+This clearing can be switched of by the function C<no_flash_clear>
+
+=item C<flash_detail $type, $message>
+
+Display a detailed message C<$message> in the flash of type C<$type>. Multiple calls of
+C<flash_detail> on the same C<$self> will be merged by type.
+So the flash message can be hold short and the visibility of details can toggled by the user.
+
+=item C<no_flash_clear>
+
+No automatic clearing of flash after successful ClientJS call
 
 =item C<error $message>
 
@@ -614,7 +660,7 @@ C<select_node>, C<deselect_node>, C<deselect_all>
 
 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
 
-In order not having to maintain two files (this one and
+In order to not have to maintain two files (this one and
 C<js/client_js.js>) there's a script that can parse this file's
 C<%supported_methods> definition and generate the file
 C<js/client_js.js> accordingly. The steps are:
index bcaeb62..126a30e 100644 (file)
@@ -24,6 +24,8 @@ use POSIX ();
 use Encode qw(decode);
 
 use SL::DBUtils;
+use SL::DB;
+use SL::HTML::Util;
 
 sub unique_id {
   my ($a, $b) = gettimeofday();
@@ -54,7 +56,7 @@ sub retrieve_parts {
 
   my ($self, $myconfig, $form, $order_by, $order_dir) = @_;
 
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my (@filter_values, $filter);
 
@@ -62,18 +64,18 @@ sub retrieve_parts {
     next unless $form->{$_};
 
     $filter .= qq| AND ($_ ILIKE ?)|;
-    push @filter_values, '%' . $form->{$_} . '%';
+    push @filter_values, like($form->{$_});
   }
 
   if ($form->{no_assemblies}) {
-    $filter .= qq| AND (NOT COALESCE(assembly, FALSE))|;
+    $filter .= qq| AND (NOT part_type = 'assembly')|;
   }
   if ($form->{assemblies}) {
-    $filter .= qq| AND assembly=TRUE|;
+    $filter .= qq| AND part_type = 'assembly'|;
   }
 
   if ($form->{no_services}) {
-    $filter .= qq| AND (inventory_accno_id is not NULL or assembly=TRUE)|;
+    $filter .= qq| AND NOT (part_type = 'service' OR part_type = 'assembly')|;
   }
 
   substr($filter, 1, 3) = "WHERE" if ($filter);
@@ -93,98 +95,23 @@ sub retrieve_parts {
     push(@{$parts}, $ref);
   }
   $sth->finish();
-  $dbh->disconnect();
 
   $main::lxdebug->leave_sub();
 
   return $parts;
 }
 
-sub retrieve_projects {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form, $order_by, $order_dir) = @_;
-
-  my $dbh = $form->dbconnect($myconfig);
-
-  my (@filter_values, $filter);
-  if ($form->{"projectnumber"}) {
-    $filter .= qq| AND (projectnumber ILIKE ?)|;
-    push(@filter_values, '%' . $form->{"projectnumber"} . '%');
-  }
-  if ($form->{"description"}) {
-    $filter .= qq| AND (description ILIKE ?)|;
-    push(@filter_values, '%' . $form->{"description"} . '%');
-  }
-  substr($filter, 1, 3) = "WHERE" if ($filter);
-
-  $order_by =~ s/[^a-zA-Z_]//g;
-  $order_dir = $order_dir ? "ASC" : "DESC";
-
-  my $query =
-    qq|SELECT id, projectnumber, description | .
-    qq|FROM project $filter | .
-    qq|ORDER BY $order_by $order_dir|;
-  my $sth = $dbh->prepare($query);
-  $sth->execute(@filter_values) || $form->dberror($query . " (" . join(", ", @filter_values) . ")");
-  my $projects = [];
-  while (my $ref = $sth->fetchrow_hashref()) {
-    push(@{$projects}, $ref);
-  }
-  $sth->finish();
-  $dbh->disconnect();
-
-  $main::lxdebug->leave_sub();
-
-  return $projects;
-}
-
-sub retrieve_employees {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form, $order_by, $order_dir) = @_;
-
-  my $dbh = $form->dbconnect($myconfig);
-
-  my (@filter_values, $filter);
-  if ($form->{"name"}) {
-    $filter .= qq| AND (name ILIKE ?)|;
-    push(@filter_values, '%' . $form->{"name"} . '%');
-  }
-  substr($filter, 1, 3) = "WHERE" if ($filter);
-
-  $order_by =~ s/[^a-zA-Z_]//g;
-  $order_dir = $order_dir ? "ASC" : "DESC";
-
-  my $query =
-    qq|SELECT id, name | .
-    qq|FROM employee $filter | .
-    qq|ORDER BY $order_by $order_dir|;
-  my $sth = $dbh->prepare($query);
-  $sth->execute(@filter_values) || $form->dberror($query . " (" . join(", ", @filter_values) . ")");
-  my $employees = [];
-  while (my $ref = $sth->fetchrow_hashref()) {
-    push(@{$employees}, $ref);
-  }
-  $sth->finish();
-  $dbh->disconnect();
-
-  $main::lxdebug->leave_sub();
-
-  return $employees;
-}
-
 sub retrieve_customers_or_vendors {
   $main::lxdebug->enter_sub();
 
   my ($self, $myconfig, $form, $order_by, $order_dir, $is_vendor, $allow_both) = @_;
 
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my (@filter_values, $filter);
   if ($form->{"name"}) {
     $filter .= " AND (TABLE.name ILIKE ?)";
-    push(@filter_values, '%' . $form->{"name"} . '%');
+    push(@filter_values, like($form->{"name"}));
   }
   if (!$form->{"obsolete"}) {
     $filter .= " AND NOT TABLE.obsolete";
@@ -230,7 +157,6 @@ sub retrieve_customers_or_vendors {
     push(@{$customers}, $ref);
   }
   $sth->finish();
-  $dbh->disconnect();
 
   $main::lxdebug->leave_sub();
 
@@ -242,12 +168,12 @@ sub retrieve_delivery_customer {
 
   my ($self, $myconfig, $form, $order_by, $order_dir) = @_;
 
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my (@filter_values, $filter);
   if ($form->{"name"}) {
     $filter .= qq| (name ILIKE ?) AND|;
-    push(@filter_values, '%' . $form->{"name"} . '%');
+    push(@filter_values, like($form->{"name"}));
   }
 
   $order_by =~ s/[^a-zA-Z_]//g;
@@ -266,7 +192,6 @@ sub retrieve_delivery_customer {
     push(@{$delivery_customers}, $ref);
   }
   $sth->finish();
-  $dbh->disconnect();
 
   $main::lxdebug->leave_sub();
 
@@ -278,12 +203,12 @@ sub retrieve_vendor {
 
   my ($self, $myconfig, $form, $order_by, $order_dir) = @_;
 
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my (@filter_values, $filter);
   if ($form->{"name"}) {
     $filter .= qq| (name ILIKE ?) AND|;
-    push(@filter_values, '%' . $form->{"name"} . '%');
+    push(@filter_values, like($form->{"name"}));
   }
 
   $order_by =~ s/[^a-zA-Z_]//g;
@@ -302,7 +227,6 @@ sub retrieve_vendor {
     push(@{$vendors}, $ref);
   }
   $sth->finish();
-  $dbh->disconnect();
 
   $main::lxdebug->leave_sub();
 
@@ -388,7 +312,7 @@ sub get_vc_details {
 
   $vc = $vc eq "customer" ? "customer" : "vendor";
 
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $query;
 
@@ -408,7 +332,6 @@ sub get_vc_details {
   my $ref = selectfirst_hashref_query($form, $dbh, $query, $vc_id);
 
   if (!$ref) {
-    $dbh->disconnect();
     $main::lxdebug->leave_sub();
     return 0;
   }
@@ -420,13 +343,16 @@ sub get_vc_details {
   $query = qq|SELECT * FROM shipto WHERE (trans_id = ?)|;
   $form->{SHIPTO} = selectall_hashref_query($form, $dbh, $query, $vc_id);
 
+  if ($vc eq 'customer') {
+    $query = qq|SELECT * FROM additional_billing_addresses WHERE (customer_id = ?)|;
+    $form->{ADDITIONAL_BILLING_ADDRESSES} = selectall_hashref_query($form, $dbh, $query, $vc_id);
+  }
+
   $query = qq|SELECT * FROM contacts WHERE (cp_cv_id = ?)|;
   $form->{CONTACTS} = selectall_hashref_query($form, $dbh, $query, $vc_id);
 
   # Only show default pricegroup for customer, not vendor, which is why this is outside the main query
-  ($form->{pricegroup}) = selectrow_query($form, $dbh, qq|SELECT pricegroup FROM pricegroup WHERE id = ?|, $form->{klass});
-
-  $dbh->disconnect();
+  ($form->{pricegroup}) = selectrow_query($form, $dbh, qq|SELECT pricegroup FROM pricegroup WHERE id = ?|, $form->{pricegroup_id});
 
   $main::lxdebug->leave_sub();
 
@@ -440,14 +366,19 @@ sub get_shipto_by_id {
 
   $prefix ||= "";
 
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $query = qq|SELECT * FROM shipto WHERE shipto_id = ?|;
   my $ref   = selectfirst_hashref_query($form, $dbh, $query, $shipto_id);
 
   map { $form->{"${prefix}${_}"} = $ref->{$_} } keys %{ $ref } if $ref;
 
-  $dbh->disconnect();
+  my $cvars = CVar->get_custom_variables(
+    dbh      => $dbh,
+    module   => 'ShipTo',
+    trans_id => $shipto_id,
+  );
+  $form->{"${prefix}shiptocvar_$_->{name}"} = $_->{value} for @{ $cvars };
 
   $main::lxdebug->leave_sub();
 }
@@ -457,6 +388,8 @@ sub save_email_status {
 
   my ($self, $myconfig, $form) = @_;
 
+  return unless ($::instance_conf->get_email_journal);
+
   my ($table, $query, $dbh);
 
   if ($form->{script} eq 'oe.pl') {
@@ -474,33 +407,34 @@ sub save_email_status {
 
   return $main::lxdebug->leave_sub() if (!$form->{id} || !$table || !$form->{formname});
 
-  $dbh = $form->get_standard_dbh($myconfig);
-
-  my ($intnotes) = selectrow_query($form, $dbh, qq|SELECT intnotes FROM $table WHERE id = ?|, $form->{id});
+  SL::DB->client->with_transaction(sub {
+    $dbh = SL::DB->client->dbh;
 
-  $intnotes =~ s|\r||g;
-  $intnotes =~ s|\n$||;
+    my ($intnotes) = selectrow_query($form, $dbh, qq|SELECT intnotes FROM $table WHERE id = ?|, $form->{id});
 
-  $intnotes .= "\n\n" if ($intnotes);
+    $intnotes =~ s|\r||g;
+    $intnotes =~ s|\n$||;
 
-  my $cc  = $form->{cc}  ? $main::locale->text('Cc') . ": $form->{cc}\n"   : '';
-  my $bcc = $form->{bcc} ? $main::locale->text('Bcc') . ": $form->{bcc}\n" : '';
-  my $now = scalar localtime;
+    $intnotes .= "\n\n" if ($intnotes);
 
-  $intnotes .= $main::locale->text('[email]') . "\n"
-    . $main::locale->text('Date') . ": $now\n"
-    . $main::locale->text('To (email)') . ": $form->{email}\n"
-    . "${cc}${bcc}"
-    . $main::locale->text('Subject') . ": $form->{subject}\n\n"
-    . $main::locale->text('Message') . ": $form->{message}";
+    my $cc  = $form->{cc}  ? $main::locale->text('Cc') . ": $form->{cc}\n"   : '';
+    my $bcc = $form->{bcc} ? $main::locale->text('Bcc') . ": $form->{bcc}\n" : '';
+    my $now = scalar localtime;
 
-  $intnotes =~ s|\r||g;
+    $intnotes .= $main::locale->text('[email]') . "\n"
+      . $main::locale->text('Date') . ": $now\n"
+      . $main::locale->text('To (email)') . ": $form->{email}\n"
+      . "${cc}${bcc}"
+      . $main::locale->text('Subject') . ": $form->{subject}\n\n"
+      . $main::locale->text('Message') . ": " . SL::HTML::Util->strip($form->{message});
 
-  do_query($form, $dbh, qq|UPDATE $table SET intnotes = ? WHERE id = ?|, $intnotes, $form->{id});
+    $intnotes =~ s|\r||g;
 
-  $form->save_status($dbh);
+    do_query($form, $dbh, qq|UPDATE $table SET intnotes = ? WHERE id = ?|, $intnotes, $form->{id});
 
-  $dbh->commit();
+    $form->save_status($dbh);
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
@@ -613,16 +547,16 @@ sub copy_file_to_webdav_folder {
   foreach my $item (qw(tmpdir tmpfile type)){
     next if $form->{$item};
     $::lxdebug->message(LXDebug::WARN(), 'Missing parameter:' . $item);
-    $::form->error($::locale->text("Missing parameter for WebDAV file copy"));
+    $::lxdebug->leave_sub();
+    return $::locale->text("Missing parameter for WebDAV file copy");
   }
 
   my ($webdav_folder, $document_name) =  get_webdav_folder($form);
 
   if (! $webdav_folder){
-    $::lxdebug->leave_sub();
     $::lxdebug->message(LXDebug::WARN(), 'Cannot check correct WebDAV folder');
-    $::form->error($::locale->text("Cannot check correct WebDAV folder"));
-    return undef;
+    $::lxdebug->leave_sub();
+    return $::locale->text("Cannot check correct WebDAV folder")
   }
 
   $complete_path =  File::Spec->catfile($form->{cwd},  $webdav_folder);
@@ -636,7 +570,11 @@ sub copy_file_to_webdav_folder {
     chdir($current_dir);
   }
 
-  opendir my $dh, $complete_path or die "Could not open $complete_path: $!";
+  my $dh;
+  if (!opendir $dh, $complete_path) {
+    $::lxdebug->leave_sub();
+    return "Could not open $complete_path: $!";
+  }
 
   my ($newest_name, $newest_time);
   while ( defined( my $file = readdir( $dh ) ) ) {
@@ -658,15 +596,19 @@ sub copy_file_to_webdav_folder {
     return;
   }
 
+  $form->{attachment_filename} ||= $form->generate_attachment_filename;
+
   my $timestamp =  get_current_formatted_time();
-  my $new_file  =  File::Spec->catfile($form->{cwd}, $webdav_folder, $form->generate_attachment_filename());
+  my $new_file  =  File::Spec->catfile($form->{cwd}, $webdav_folder, $form->{attachment_filename});
   $new_file =~ s{(.*)\.}{$1$timestamp\.};
 
   if (!File::Copy::copy($current_file, $new_file)) {
     $::lxdebug->message(LXDebug::WARN(), "Copy file from $current_file to $new_file failed: $ERRNO");
-    $::form->error($::locale->text("Copy file from #1 to #2 failed: #3", $current_file, $new_file, $ERRNO));
+    $::lxdebug->leave_sub();
+    return $::locale->text("Copy file from #1 to #2 failed: #3", $current_file, $new_file, $ERRNO);
   }
 
+  return;
   $::lxdebug->leave_sub();
 }
 
@@ -706,6 +648,11 @@ C<full> (replace consecutive line feed/carriage return characters in
 the middle by a single space and remove tailing line feed/carriage
 return characters).
 
+=item C<save_email_status>
+
+Adds sending information to internal notes.
+Does nothing if the client config email_journal is enabled.
+
 =back
 
 =head1 BUGS
index 6c58061..80e37b7 100644 (file)
@@ -23,7 +23,7 @@ sub action_list_transactions {
 sub _mini_ledger {
   my ($self, $transactions) = @_;
 
-  $::auth->assert('general_ledger');
+  $::auth->assert('invoice_edit');
 
   my $debit_sum  = 0;
   my $credit_sum = 0;
@@ -39,7 +39,7 @@ sub _mini_ledger {
 sub _mini_trial_balance {
   my ($self, $transactions) = @_;
 
-  $::auth->assert('general_ledger');
+  $::auth->assert('invoice_edit');
 
   my $rec = {};
   foreach my $t ( @{ $transactions } ) {
@@ -57,7 +57,7 @@ sub _mini_trial_balance {
 }
 
 sub check_auth {
-  $::auth->assert('general_ledger');
+  $::auth->assert('invoice_edit');
 }
 
 1;
@@ -91,7 +91,7 @@ SL::Controller::AccTrans - module to list all transactions and balances of an in
   SL::Controller::AccTrans->action_list_transactions();
 
   The HTML blob can also be opened directly as a url:
-  controller.pl?action=AccTrans/print_table&trans_id=7
+  controller.pl?action=AccTrans/list_transactions&trans_id=7
 
 =head1 TODO
 
index d0082b1..c22fdca 100644 (file)
@@ -6,15 +6,18 @@ use parent qw(SL::Controller::Base);
 
 use IO::Dir;
 use List::Util qw(first);
+use List::UtilsBy qw(sort_by);
 
 use SL::Common ();
 use SL::DB::AuthUser;
 use SL::DB::AuthGroup;
 use SL::DB::Printer;
+use SL::DBUtils ();
 use SL::Helper::Flash;
 use SL::Locale::String qw(t8);
 use SL::System::InstallationLock;
 use SL::User;
+use SL::Version;
 use SL::Layout::AdminLogin;
 
 use Rose::Object::MakeMethods::Generic
@@ -28,7 +31,7 @@ __PACKAGE__->run_before(\&setup_layout);
 __PACKAGE__->run_before(\&setup_client, only => [ qw(list_printers new_printer edit_printer save_printer delete_printer) ]);
 
 sub get_auth_level { "admin" };
-sub keep_auth_vars {
+sub keep_auth_vars_in_form {
   my ($class, %params) = @_;
   return $params{action} eq 'login';
 }
@@ -81,10 +84,9 @@ sub action_create_auth_tables {
   $::auth->set_session_value('admin_password', $::lx_office_conf{authentication}->{admin_password});
   $::auth->create_or_refresh_session;
 
-  return if $self->apply_dbupgrade_scripts;
+  my $scripts_applied = $self->apply_dbupgrade_scripts;
 
-  my $group = (SL::DB::Manager::AuthGroup->get_all(limit => 1))[0];
-  if (!$group) {
+  if (! SL::DB::Manager::AuthGroup->get_all_count) {
     SL::DB::AuthGroup->new(
       name        => t8('Full Access'),
       description => t8('Full access to all functions'),
@@ -92,7 +94,7 @@ sub action_create_auth_tables {
     )->save;
   }
 
-  $self->action_login;
+  $self->action_login unless $scripts_applied;
 }
 
 #
@@ -111,12 +113,12 @@ sub action_show {
 sub action_new_user {
   my ($self) = @_;
 
+  my $defaults = SL::DefaultManager->new($::lx_office_conf{system}->{default_manager});
   $self->user(SL::DB::AuthUser->new(
     config_values => {
-      vclimit      => 200,
-      countrycode  => "de",
-      numberformat => "1.000,00",
-      dateformat   => "dd.mm.yy",
+      countrycode  => $defaults->language('de'),
+      numberformat => $defaults->numberformat('1.000,00'),
+      dateformat   => $defaults->dateformat('dd.mm.yy'),
       stylesheet   => "kivitendo.css",
       menustyle    => "neu",
     },
@@ -400,19 +402,24 @@ sub action_create_dataset_login {
 
 sub action_create_dataset {
   my ($self) = @_;
-  $self->create_dataset_form;
+
+  my %superuser = $self->check_database_superuser_privileges(no_credentials_not_an_error => 1);
+  $self->create_dataset_form(superuser => \%superuser);
 }
 
 sub action_do_create_dataset {
   my ($self) = @_;
 
+  my %superuser = $self->check_database_superuser_privileges;
+
   my @errors;
   push @errors, t8("Dataset missing!")          if !$::form->{db};
   push @errors, t8("Default currency missing!") if !$::form->{defaultcurrency};
+  push @errors, $superuser{error}               if !$superuser{have_privileges} && $superuser{error};
 
   if (@errors) {
     flash('error', @errors);
-    return $self->create_dataset_form;
+    return $self->create_dataset_form(superuser => \%superuser);
   }
 
   $::form->{encoding} = 'UNICODE';
@@ -444,7 +451,7 @@ sub action_do_delete_dataset {
 
   if (@errors) {
     flash('error', @errors);
-    return $self->create_dataset_form;
+    return $self->delete_dataset_form;
   }
 
   User->new->dbdelete($::form);
@@ -488,7 +495,7 @@ sub action_lock_system {
 
 sub init_db_cfg            { $::lx_office_conf{'authentication/database'}                                                    }
 sub init_is_locked         { SL::System::InstallationLock->is_locked                                                         }
-sub init_client            { SL::DB::Manager::AuthClient->find_by(id => ($::form->{id} || ($::form->{client}  || {})->{id})) }
+sub init_client            { SL::DB::Manager::AuthClient->find_by(id => (($::form->{client} || {})->{id} || $::form->{id}))  }
 sub init_user              { SL::DB::AuthUser  ->new(id => ($::form->{id} || ($::form->{user}    || {})->{id}))->load        }
 sub init_group             { SL::DB::AuthGroup ->new(id => ($::form->{id} || ($::form->{group}   || {})->{id}))->load        }
 sub init_printer           { SL::DB::Printer   ->new(id => ($::form->{id} || ($::form->{printer} || {})->{id}))->load        }
@@ -497,8 +504,8 @@ sub init_all_users         { SL::DB::Manager::AuthUser  ->get_all_sorted
 sub init_all_groups        { SL::DB::Manager::AuthGroup ->get_all_sorted                                                     }
 sub init_all_printers      { SL::DB::Manager::Printer   ->get_all_sorted                                                     }
 sub init_all_dateformats   { [ qw(mm/dd/yy dd/mm/yy dd.mm.yy yyyy-mm-dd)      ]                                              }
-sub init_all_numberformats { [ '1,000.00', '1000.00', '1.000,00', '1000,00'   ]                                              }
-sub init_all_stylesheets   { [ qw(lx-office-erp.css kivitendo.css) ]                                                         }
+sub init_all_numberformats { [ '1,000.00', '1000.00', '1.000,00', '1000,00', "1'000.00" ]                                    }
+sub init_all_stylesheets   { [ qw(lx-office-erp.css Mobile.css kivitendo.css) ]                                              }
 sub init_all_dbsources             { [ sort User->dbsources($::form)                               ] }
 sub init_all_used_dbsources        { { map { (join(':', $_->dbhost || 'localhost', $_->dbport || 5432, $_->dbname) => $_->name) } @{ $_[0]->all_clients }  } }
 sub init_all_accounting_methods    { [ { id => 'accrual',   name => t8('Accrual accounting')  }, { id => 'cash',     name => t8('Cash accounting')       } ] }
@@ -557,12 +564,13 @@ sub init_all_countrycodes {
 sub setup_layout {
   my ($self, $action) = @_;
 
+  my $defaults = SL::DefaultManager->new($::lx_office_conf{system}->{default_manager});
   $::request->layout(SL::Layout::Dispatcher->new(style => 'admin'));
   $::form->{favicon} = "favicon.ico";
   %::myconfig        = (
-    countrycode      => 'de',
-    numberformat     => '1.000,00',
-    dateformat       => 'dd.mm.yy',
+    countrycode      => $defaults->language('de'),
+    numberformat     => $defaults->numberformat('1.000,00'),
+    dateformat       => $defaults->dateformat('dd.mm.yy'),
   ) if !%::myconfig;
 }
 
@@ -570,7 +578,7 @@ sub setup_client {
   my ($self) = @_;
 
   $self->client(SL::DB::Manager::AuthClient->get_default || $self->all_clients->[0]) if !$self->client;
-  $::auth->set_client($self->client->id);
+  $::auth->set_client($self->client->id) if $self->client;
 }
 
 #
@@ -584,16 +592,23 @@ sub use_multiselect_js {
   return $self;
 }
 
+sub use_ckeditor_js {
+  my ($self) = @_;
+
+  $::request->{layout}->use_javascript("${_}.js") for qw(ckeditor/ckeditor ckeditor/adapters/jquery);
+  return $self;
+}
+
 sub login_form {
   my ($self, %params) = @_;
   $::request->layout(SL::Layout::AdminLogin->new);
-  my $version         = $::form->read_version;
-  $self->render('admin/adminlogin', title => t8('kivitendo v#1 administration', $version), %params, version => $version);
+  my $version         = SL::Version->get_version;
+  $self->render('admin/adminlogin', title => t8('kivitendo v#1 administration', $version), %params, version => $version );
 }
 
 sub edit_user_form {
   my ($self, %params) = @_;
-  $self->use_multiselect_js->render('admin/edit_user', %params);
+  $self->use_multiselect_js->use_ckeditor_js->render('admin/edit_user', %params);
 }
 
 sub edit_client_form {
@@ -627,7 +642,23 @@ sub database_administration_login_form {
 
 sub create_dataset_form {
   my ($self, %params) = @_;
-  $self->render('admin/create_dataset', title => (t8('Database Administration') . " / " . t8('Create Dataset')));
+
+  my $defaults = SL::DefaultManager->new($::lx_office_conf{system}->{default_manager});
+  $::form->{favicon} = "favicon.ico";
+  $::form->{countrymode}             = $defaults->country('DE');
+  $::form->{chart}                   = $defaults->chart_of_accounts('Germany-DATEV-SKR03EU');
+  $::form->{defaultcurrency}         = $defaults->currency('EUR');
+  $::form->{precision}               = $defaults->precision(0.01);
+  $::form->{accounting_method}       = $defaults->accounting_method('cash');
+  $::form->{inventory_system}        = $defaults->inventory_system('periodic');
+  $::form->{profit_determination}    = $defaults->profit_determination('balance');
+  $::form->{feature_balance}         = $defaults->feature_balance(1);
+  $::form->{feature_datev}           = $defaults->feature_datev(1);
+  $::form->{feature_erfolgsrechnung} = $defaults->feature_erfolgsrechnung(0);
+  $::form->{feature_eurechnung}      = $defaults->feature_eurechnung(1);
+  $::form->{feature_ustva}           = $defaults->feature_ustva(1);
+
+  $self->render('admin/create_dataset', title => (t8('Database Administration') . " / " . t8('Create Dataset')), superuser => $params{superuser});
 }
 
 sub delete_dataset_form {
@@ -672,4 +703,63 @@ sub authenticate_root {
   return undef;
 }
 
+sub is_user_used_for_task_server {
+  my ($self, $user) = @_;
+
+  return undef if !$user;
+  return join ', ', sort_by { lc } map { $_->name } @{ SL::DB::Manager::AuthClient->get_all(where => [ task_server_user_id => $user->id ]) };
+}
+
+sub check_database_superuser_privileges {
+  my ($self, %params) = @_;
+
+  my %dbconnect_form = %{ $::form };
+  my %result         = (
+    username => $dbconnect_form{dbuser},
+    password => $dbconnect_form{dbpasswd},
+  );
+
+  my $check_privileges = sub {
+    my $dbh = SL::DBConnect->connect($dbconnect_form{dbconnect}, $result{username}, $result{password}, SL::DBConnect->get_options);
+    return (error => $::locale->text('The credentials (username & password) for connecting database are wrong.')) if !$dbh;
+
+    my $is_superuser = SL::DBUtils::role_is_superuser($dbh, $result{username});
+
+    $dbh->disconnect;
+
+    return (have_privileges => $is_superuser);
+  };
+
+  User::dbconnect_vars(\%dbconnect_form, $dbconnect_form{dbdefault});
+
+  %result = (
+    %result,
+    $check_privileges->(),
+  );
+
+  if (!$result{have_privileges}) {
+    $result{username} = $::form->{database_superuser_user};
+    $result{password} = $::form->{database_superuser_password};
+
+    if ($::form->{database_superuser_user}) {
+      %result = (
+        %result,
+        $check_privileges->(),
+      );
+    }
+  }
+
+  if ($result{have_privileges}) {
+    $::auth->set_session_value(database_superuser_username => $result{username}, database_superuser_password => $result{password});
+    return %result;
+  }
+
+  $::auth->delete_session_value(qw(database_superuser_username database_superuser_password));
+
+  return ()                                                                            if !$::form->{database_superuser_user} && $params{no_credentials_not_an_error};
+  return (%result, error => $::locale->text('No superuser credentials were entered.')) if !$::form->{database_superuser_user};
+  return %result                                                                       if $result{error};
+  return (%result, error => $::locale->text('The database user \'#1\' does not have superuser privileges.', $result{username}));
+}
+
 1;
index 3e89ecc..27a1c69 100644 (file)
@@ -8,18 +8,17 @@ use SL::BackgroundJob::Base;
 use SL::Controller::Helper::GetModels;
 use SL::DB::BackgroundJob;
 use SL::Helper::Flash;
+use SL::JSON;
 use SL::Locale::String;
 use SL::System::TaskServer;
 
 use Rose::Object::MakeMethods::Generic
 (
-  scalar                  => [ qw(background_job) ],
-  'scalar --get_set_init' => [ qw(task_server back_to models) ],
+  'scalar --get_set_init' => [ qw(task_server back_to models background_job) ],
 );
 
 __PACKAGE__->run_before('check_auth');
 __PACKAGE__->run_before('check_task_server');
-__PACKAGE__->run_before('load_background_job', only => [ qw(edit update destroy execute show) ]);
 
 #
 # actions
@@ -28,6 +27,7 @@ __PACKAGE__->run_before('load_background_job', only => [ qw(edit update destroy
 sub action_list {
   my ($self) = @_;
 
+  $self->setup_list_action_bar;
   $self->render('background_job/list',
                 title           => $::locale->text('Background jobs'),
                 BACKGROUND_JOBS => $self->models->get,
@@ -37,7 +37,8 @@ sub action_list {
 sub action_new {
   my ($self) = @_;
 
-  $self->background_job(SL::DB::BackgroundJob->new(cron_spec => '* * * * *',  package_name => 'Test'));
+  $self->background_job(SL::DB::BackgroundJob->new(cron_spec => '* * * * *',  package_name => 'Test')) unless $self->background_job;
+  $self->setup_form_action_bar;
   $self->render('background_job/form',
                 title       => $::locale->text('Create a new background job'),
                 JOB_CLASSES => [ SL::BackgroundJob::Base->get_known_job_classes ]);
@@ -46,11 +47,20 @@ sub action_new {
 sub action_edit {
   my ($self) = @_;
 
+  $self->setup_form_action_bar;
   $self->render('background_job/form',
                 title       => $::locale->text('Edit background job'),
                 JOB_CLASSES => [ SL::BackgroundJob::Base->get_known_job_classes ]);
 }
 
+sub action_edit_as_new {
+  my ($self) = @_;
+
+  delete $::form->{background_job}->{id};
+  $self->background_job(SL::DB::BackgroundJob->new(%{ $::form->{background_job} }));
+  $self->action_new;
+}
+
 sub action_show {
   my ($self) = @_;
 
@@ -109,6 +119,33 @@ sub action_execute {
                      back_to    => $self->url_for(action => 'edit', id => $self->background_job->id));
 }
 
+sub action_execute_class {
+  my ($self) = @_;
+
+  my $result;
+
+  my $ok = eval {
+    die "no class name given in parameter 'class'" if !$::form->{class} || ($::form->{class} =~ m{[^a-z0-9]}i);
+    die "invalid class"                            if ! -f "SL/BackgroundJob/" . $::form->{class} . ".pm";
+
+    my $package = "SL::BackgroundJob::" . $::form->{class};
+
+    eval "require $package" or die $@;
+    my $job = SL::DB::BackgroundJob->new(data => $::form->{data});
+    $job->data(decode_json($::form->{json_data})) if $::form->{json_data};
+    $result = $package->new->run($job);
+
+    1;
+  };
+
+  my $reply = {
+    status => $ok ? 'succeeded' : 'failed',
+    result => $ok ? $result     : $@,
+  };
+
+  $self->render(\to_json($reply), { type => 'json' });
+}
+
 #
 # filters
 #
@@ -146,9 +183,8 @@ sub create_or_update {
   $self->redirect_to($self->back_to);
 }
 
-sub load_background_job {
-  my ($self) = @_;
-  $self->background_job(SL::DB::BackgroundJob->new(id => $::form->{id})->load);
+sub init_background_job {
+  return $::form->{id} ? SL::DB::BackgroundJob->new(id => $::form->{id})->load : undef;
 }
 
 sub init_task_server {
@@ -183,4 +219,71 @@ sub init_models {
   );
 }
 
+sub setup_list_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      link => [
+        t8('Add'),
+        link      => $self->url_for(action => 'new'),
+        accesskey => 'enter',
+      ],
+      link => [
+        t8('Server control'),
+        link => $self->url_for(controller => 'TaskServer', action => 'show'),
+      ],
+      link => [
+        t8('Job history'),
+        link => $self->url_for(controller => 'BackgroundJobHistory', action => 'list'),
+      ],
+    );
+  }
+}
+
+sub setup_form_action_bar {
+  my ($self) = @_;
+
+  my $is_new = !$self->background_job->id;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [
+          t8('Save'),
+          submit    => [ '#form', { action => 'BackgroundJob/' . ($is_new ? 'create' : 'update') } ],
+          accesskey => 'enter',
+        ],
+        action => [
+          t8('Save and execute'),
+          submit => [ '#form', { action => 'BackgroundJob/save_and_execute' } ],
+        ],
+        action => [
+          t8('Use as new'),
+          submit   => [ '#form', { action => 'BackgroundJob/edit_as_new' } ],
+          disabled => $is_new ? t8('The object has not been saved yet.') : undef,
+        ],
+      ], # end of combobox "Save"
+
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => 'BackgroundJob/destroy' } ],
+        confirm  => t8('Do you really want to delete this object?'),
+        disabled => $is_new ? t8('This object has not been saved yet.') : undef,
+      ],
+
+      link => [
+        t8('Abort'),
+        link => $self->url_for(action => 'list'),
+      ],
+
+      link => [
+        t8('Job history'),
+        link     => $self->url_for(controller => 'BackgroundJobHistory', action => 'list', 'filter.package_name:substr::ilike' => $self->background_job->package_name),
+        disabled => $is_new ? t8('This object has not been saved yet.') : undef,
+      ],
+    );
+  }
+}
+
 1;
index fae8706..4fbd7ec 100644 (file)
@@ -29,6 +29,7 @@ sub action_list {
 
   $self->make_filter_summary;
 
+  $self->setup_list_action_bar;
   $self->render('background_job_history/list',
                 title   => $::locale->text('Background job history'),
                 ENTRIES => $self->models->get,
@@ -41,6 +42,7 @@ sub action_show {
   my $back_to = $::form->{back_to} || $self->url_for(action => 'list');
 
   $self->history(SL::DB::BackgroundJobHistory->new(id => $::form->{id})->load);
+  $self->setup_show_action_bar;
   $self->render('background_job_history/show',
                 title   => $::locale->text('View background job execution result'),
                 back_to => $back_to);
@@ -111,4 +113,34 @@ sub init_models {
   );
 }
 
+sub setup_list_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      link => [
+        t8('Server control'),
+        link => $self->url_for(controller => 'TaskServer', action => 'show'),
+      ],
+      link => [
+        t8('List of jobs'),
+        link => $self->url_for(controller => 'BackgroundJob', action => 'list'),
+      ],
+    );
+  }
+}
+
+sub setup_show_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      link => [
+        t8('Back'),
+        link => $self->url_for(action => 'list'),
+      ],
+    );
+  }
+}
+
 1;
diff --git a/SL/Controller/BankAccount.pm b/SL/Controller/BankAccount.pm
deleted file mode 100644 (file)
index dff4312..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-package SL::Controller::BankAccount;
-
-use strict;
-
-use parent qw(SL::Controller::Base);
-
-use SL::Helper::Flash;
-use SL::Locale::String;
-use SL::DB::Default;
-use SL::DB::Manager::BankAccount;
-use SL::DB::Manager::BankTransaction;
-
-use Rose::Object::MakeMethods::Generic (
-  scalar                  => [ qw(bank_account) ],
-);
-
-__PACKAGE__->run_before('check_auth');
-__PACKAGE__->run_before('load_bank_account', only => [ qw(edit update delete) ]);
-
-#
-# actions
-#
-
-sub action_list {
-  my ($self) = @_;
-
-  $self->render('bankaccounts/list',
-                title           => t8('Bank accounts'),
-                BANKACCOUNTS    => SL::DB::Manager::BankAccount->get_all_sorted,
-               );
-}
-
-sub action_new {
-  my ($self) = @_;
-
-  $self->{bank_account} = SL::DB::BankAccount->new;
-  $self->render('bankaccounts/form',
-                 title => t8('Add bank account'));
-}
-
-sub action_edit {
-  my ($self) = @_;
-
-  $self->render('bankaccounts/form', title => t8('Edit bank account'));
-}
-
-sub action_create {
-  my ($self) = @_;
-
-  $self->{bank_account} = SL::DB::BankAccount->new;
-  $self->create_or_update;
-}
-
-sub action_update {
-  my ($self) = @_;
-  $self->create_or_update;
-}
-
-sub action_delete {
-  my ($self) = @_;
-
-  if ( $self->{bank_account}->{number_of_bank_transactions} > 0 ) {
-    flash_later('error', $::locale->text('The bank account has been used and cannot be deleted.'));
-  } elsif ( eval { $self->{bank_account}->delete; 1; } ) {
-    flash_later('info',  $::locale->text('The bank account has been deleted.'));
-  } else {
-    flash_later('error', $::locale->text('The bank account has been used and cannot be deleted.'));
-  };
-  $self->redirect_to(action => 'list');
-
-}
-
-sub action_reorder {
-  my ($self) = @_;
-
-  SL::DB::BankAccount->reorder_list(@{ $::form->{account_id} || [] });
-  $self->render(\'', { type => 'json' });
-}
-
-#
-# filters
-#
-
-sub check_auth {
-  $::auth->assert('config');
-}
-
-sub load_bank_account {
-  my ($self) = @_;
-
-  $self->{bank_account} = SL::DB::BankAccount->new(id => $::form->{id})->load;
-  $self->{bank_account}->{number_of_bank_transactions} = SL::DB::Manager::BankTransaction->get_all_count( query => [ local_bank_account_id => $self->{bank_account}->{id} ] );
-}
-
-#
-# helpers
-#
-
-sub create_or_update {
-  my ($self) = @_;
-  my $is_new = !$self->{bank_account}->id;
-
-  my $params = delete($::form->{bank_account}) || { };
-
-  $self->{bank_account}->assign_attributes(%{ $params });
-
-  my @errors = $self->{bank_account}->validate;
-
-  if (@errors) {
-    flash('error', @errors);
-    $self->render('bankaccounts/form',
-                   title => $is_new ? t8('Add bank account') : t8('Edit bank account'));
-    return;
-  }
-
-  $self->{bank_account}->save;
-
-  flash_later('info', $is_new ? t8('The bank account has been created.') : t8('The bank account has been saved.'));
-  $self->redirect_to(action => 'list');
-}
-
-1;
index 867589f..1b20dec 100644 (file)
 package SL::Controller::BankImport;
+
 use strict;
-use Data::Dumper;
+
 use parent qw(SL::Controller::Base);
 
-use SL::Locale::String qw(t8);
-use SL::DB::CsvImportProfile;
-use SL::Helper::MT940;
+use List::MoreUtils qw(apply);
+use List::Util qw(max min);
+
+use SL::DB::BankAccount;
+use SL::DB::BankTransaction;
+use SL::DB::Default;
+use SL::Helper::Flash;
+use SL::MT940;
+use SL::SessionFile::Random;
+
+use Rose::Object::MakeMethods::Generic
+(
+  scalar                  => [ qw(file_name transactions statistics charset) ],
+  'scalar --get_set_init' => [ qw(bank_accounts) ],
+);
 
 __PACKAGE__->run_before('check_auth');
 
 sub action_upload_mt940 {
   my ($self, %params) = @_;
 
-  my $profile = SL::DB::Manager::CsvImportProfile->find_by(name => 'MT940', login => $::myconfig{login});
-  $self->render('bankimport/form', title => $::locale->text('MT940 import'), profile => $profile ? 1 : 0);
+  $self->setup_upload_mt940_action_bar;
+  $self->render('bank_import/upload_mt940', title => $::locale->text('MT940 import'));
+}
+
+sub action_import_mt940_preview {
+  my ($self, %params) = @_;
+
+  if (!$::form->{file}) {
+    flash_later('error', $::locale->text('You have to upload an MT940 file to import.'));
+    return $self->redirect_to(action => 'upload_mt940');
+  }
 
+  die "missing file for action import_mt940_preview" unless $::form->{file};
+
+  my $file = SL::SessionFile::Random->new(mode => '>');
+  $file->fh->print($::form->{file});
+  $file->fh->close;
+
+  $self->charset($::form->{charset});
+  $self->file_name($file->file_name);
+  $self->parse_and_analyze_transactions;
+
+  $self->setup_upload_mt940_preview_action_bar;
+  $self->render('bank_import/import_mt940', title => $::locale->text('MT940 import preview'), preview => 1);
 }
 
 sub action_import_mt940 {
   my ($self, %params) = @_;
 
-  die "missing file for action import" unless $::form->{file};
+  die "missing file for action import_mt940" unless $::form->{file_name};
 
-  my $converted_data = SL::Helper::MT940::convert_mt940_data($::form->{file});
+  $self->file_name($::form->{file_name});
+  $self->charset($::form->{charset});
+  $self->parse_and_analyze_transactions;
+  $self->import_transactions;
 
-  # store the converted data in a session file with a name expected by the profile type "bank_transactions"
-  my $file = SL::SessionFile->new("csv-import-bank_transactions.csv", mode => '>');
-  $file->fh->print($converted_data);
-  $file->fh->close;
+  $self->render('bank_import/import_mt940', title => $::locale->text('MT940 import result'));
+}
+
+sub parse_and_analyze_transactions {
+  my ($self, %params) = @_;
+
+  my $errors     = 0;
+  my $duplicates = 0;
+  my ($min_date, $max_date);
+
+  my $currency_id = SL::DB::Default->get->currency_id;
+
+  $self->transactions([ sort { $a->{transdate} cmp $b->{transdate} } SL::MT940->parse($self->file_name, charset => $self->charset) ]);
+
+  foreach my $transaction (@{ $self->transactions }) {
+    $transaction->{bank_account}   = $self->bank_accounts->{ make_bank_account_idx($transaction->{local_bank_code}, $transaction->{local_account_number}) };
+    $transaction->{bank_account} //= $self->bank_accounts->{ make_bank_account_idx('IBAN',                          $transaction->{local_account_number}) };
+
+    if (!$transaction->{bank_account}) {
+      $transaction->{error} = $::locale->text('No bank account configured for bank code/BIC #1, account number/IBAN #2.', $transaction->{local_bank_code}, $transaction->{local_account_number});
+      $errors++;
+      next;
+    }
+
+    $transaction->{local_bank_account_id} = $transaction->{bank_account}->id;
+    $transaction->{currency_id}           = $currency_id;
+
+    $min_date = min($min_date // $transaction->{transdate}, $transaction->{transdate});
+    $max_date = max($max_date // $transaction->{transdate}, $transaction->{transdate});
+  }
 
-  my $profile = SL::DB::Manager::CsvImportProfile->find_by(name => 'MT940', login => $::myconfig{login});
-  die t8("The MT940 import needs an import profile called MT940") unless $profile;
+  my %existing_bank_transactions;
 
-  $self->redirect_to(controller => 'controller.pl', action => 'CsvImport/test', 'profile.type' => 'bank_transactions', 'profile.id' => $profile->id, force_profile => 1);
+  if ((scalar(@{ $self->transactions }) - $errors) > 0) {
+    my @entries =
+      @{ SL::DB::Manager::BankTransaction->get_all(
+          where => [
+            transdate => { ge => $min_date },
+            transdate => { lt => $max_date->clone->add(days => 1) },
+          ],
+          inject_results => 1) };
+
+    %existing_bank_transactions = map { (make_transaction_idx($_) => 1) } @entries;
+  }
+
+  foreach my $transaction (@{ $self->transactions }) {
+    next if $transaction->{error};
+
+    if ($existing_bank_transactions{make_transaction_idx($transaction)}) {
+      $transaction->{duplicate} = 1;
+      $duplicates++;
+      next;
+    }
+  }
+
+  $self->statistics({
+    total      => scalar(@{ $self->transactions }),
+    errors     => $errors,
+    duplicates => $duplicates,
+    to_import  => scalar(@{ $self->transactions }) - $errors - $duplicates,
+  });
+}
+
+sub import_transactions {
+  my ($self, %params) = @_;
 
-};
+  my $imported = 0;
+
+  SL::DB::client->with_transaction(sub {
+    # make Emacs happy
+    1;
+
+    foreach my $transaction (@{ $self->transactions }) {
+      next if $transaction->{error} || $transaction->{duplicate};
+
+      SL::DB::BankTransaction->new(
+        map { ($_ => $transaction->{$_}) } qw(amount currency_id local_bank_account_id purpose remote_account_number remote_bank_code remote_name transaction_code transdate valutadate)
+      )->save;
+
+      $imported++;
+    }
+
+    1;
+  });
+
+  $self->statistics->{imported} = $imported;
+}
 
 sub check_auth {
   $::auth->assert('bank_transaction');
 }
 
-1;
+sub make_bank_account_idx {
+  return join '/', map { my $q = $_; $q =~ s{ +}{}g; $q } @_;
+}
+
+sub normalize_text {
+  my ($text) = @_;
+
+  $text = lc($text // '');
+  $text =~ s{ }{}g;
+
+  return $text;
+}
+
+sub make_transaction_idx {
+  my ($transaction) = @_;
+
+  if (ref($transaction) eq 'SL::DB::BankTransaction') {
+    $transaction = { map { ($_ => $transaction->$_) } qw(local_bank_account_id transdate valutadate amount purpose) };
+  }
 
+  return normalize_text(join '/',
+                        map { $_ // '' }
+                        ($transaction->{local_bank_account_id},
+                         $transaction->{transdate}->ymd,
+                         $transaction->{valutadate}->ymd,
+                         (apply { s{0+$}{} } $transaction->{amount} * 1),
+                         $transaction->{purpose}));
+}
+
+sub init_bank_accounts {
+  my ($self) = @_;
+
+  my %bank_accounts;
+
+  foreach my $bank_account (@{ SL::DB::Manager::BankAccount->get_all }) {
+    if ($bank_account->bank_code && $bank_account->account_number) {
+      $bank_accounts{make_bank_account_idx($bank_account->bank_code, $bank_account->account_number)} = $bank_account;
+    }
+    if ($bank_account->iban) {
+      $bank_accounts{make_bank_account_idx('IBAN', $bank_account->iban)} = $bank_account;
+    }
+  }
+
+  return \%bank_accounts;
+}
+
+sub setup_upload_mt940_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        $::locale->text('Preview'),
+        submit    => [ '#form', { action => 'BankImport/import_mt940_preview' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_upload_mt940_preview_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        $::locale->text('Import'),
+        submit    => [ '#form', { action => 'BankImport/import_mt940' } ],
+        accesskey => 'enter',
+        disabled  => $self->statistics->{to_import} ? undef : $::locale->text('No entries can be imported.'),
+      ],
+    );
+  }
+}
+
+1;
index bbe4edf..9d432d5 100644 (file)
@@ -17,18 +17,28 @@ use SL::SEPA;
 use SL::DB::Invoice;
 use SL::DB::PurchaseInvoice;
 use SL::DB::RecordLink;
+use SL::DB::ReconciliationLink;
 use SL::JSON;
 use SL::DB::Chart;
 use SL::DB::AccTransaction;
+use SL::DB::BankTransactionAccTrans;
 use SL::DB::Tax;
-use SL::DB::Draft;
 use SL::DB::BankAccount;
-use SL::Presenter;
+use SL::DB::GLTransaction;
+use SL::DB::RecordTemplate;
+use SL::DB::SepaExportItem;
+use SL::DBUtils qw(like do_query);
+
+use SL::Presenter::Tag qw(checkbox_tag html_tag);
+use Carp;
+use List::UtilsBy qw(partition_by);
+use List::MoreUtils qw(any);
 use List::Util qw(max);
 
 use Rose::Object::MakeMethods::Generic
 (
- 'scalar --get_set_init' => [ qw(models) ],
+  scalar                  => [ qw(callback transaction) ],
+  'scalar --get_set_init' => [ qw(models problems) ],
 );
 
 __PACKAGE__->run_before('check_auth');
@@ -43,6 +53,7 @@ sub action_search {
 
   my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
 
+  $self->setup_search_action_bar;
   $self->render('bank_transactions/search',
                  BANK_ACCOUNTS => $bank_accounts);
 }
@@ -53,58 +64,111 @@ sub action_list_all {
   $self->make_filter_summary;
   $self->prepare_report;
 
+  $self->setup_list_all_action_bar;
   $self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
 }
 
-sub action_list {
-  my ($self) = @_;
+sub gather_bank_transactions_and_proposals {
+  my ($self, %params) = @_;
 
-  if (!$::form->{filter}{bank_account}) {
-    flash('error', t8('No bank account chosen!'));
-    $self->action_search;
-    return;
-  }
-
-  my $sort_by = $::form->{sort_by} || 'transdate';
+  my $sort_by = $params{sort_by} || 'transdate';
   $sort_by = 'transdate' if $sort_by eq 'proposal';
-  $sort_by .= $::form->{sort_dir} ? ' DESC' : ' ASC';
-
-  my $fromdate = $::locale->parse_date_to_object($::form->{filter}->{fromdate});
-  my $todate   = $::locale->parse_date_to_object($::form->{filter}->{todate});
-  $todate->add( days => 1 ) if $todate;
+  $sort_by .= $params{sort_dir} ? ' DESC' : ' ASC';
 
   my @where = ();
-  push @where, (transdate => { ge => $fromdate }) if ($fromdate);
-  push @where, (transdate => { lt => $todate })   if ($todate);
-  my $bank_account = SL::DB::Manager::BankAccount->find_by( id => $::form->{filter}{bank_account} );
+  push @where, (transdate => { ge => $params{fromdate} }) if $params{fromdate};
+  push @where, (transdate => { lt => $params{todate} })   if $params{todate};
   # bank_transactions no younger than starting date,
+  # including starting date (same search behaviour as fromdate)
   # but OPEN invoices to be matched may be from before
-  if ( $bank_account->reconciliation_starting_date ) {
-    push @where, (transdate => { gt => $bank_account->reconciliation_starting_date });
+  if ( $params{bank_account}->reconciliation_starting_date ) {
+    push @where, (transdate => { ge => $params{bank_account}->reconciliation_starting_date });
   };
 
-  my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(where => [ amount => {ne => \'invoice_amount'},
-                                                                               local_bank_account_id => $::form->{filter}{bank_account},
-                                                                               @where ],
-                                                                    with_objects => [ 'local_bank_account', 'currency' ],
-                                                                    sort_by => $sort_by, limit => 10000);
-
-  my $all_open_ar_invoices = SL::DB::Manager::Invoice->get_all(where => [amount => { gt => \'paid' }], with_objects => 'customer');
-  my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { gt => \'paid' }], with_objects => 'vendor');
+  my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(
+    with_objects => [ 'local_bank_account', 'currency' ],
+    sort_by      => $sort_by,
+    limit        => 10000,
+    where        => [
+      amount                => {ne => \'invoice_amount'},      # '} make emacs happy
+      local_bank_account_id => $params{bank_account}->id,
+      cleared               => 0,
+      @where
+    ],
+  );
+  # credit notes have a negative amount, treat differently
+  my $all_open_ar_invoices = SL::DB::Manager::Invoice->get_all(where        => [ or => [ amount => { gt => \'paid' },                 # '} make emacs happy
+                                                                                         and    => [ type    => 'credit_note',
+                                                                                                     amount  => { lt => \'paid' }     # '} make emacs happy
+                                                                                         ],
+                                                                                 ],
+                                                               ],
+                                                               with_objects => ['customer','payment_terms']);
+
+  my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where        => [amount => { ne => \'paid' }],                 #  '}] make emacs happy
+                                                                       with_objects => ['vendor'  ,'payment_terms']);
+  my $all_open_sepa_export_items = SL::DB::Manager::SepaExportItem->get_all(where        => [chart_id               => $params{bank_account}->chart_id ,
+                                                                                             'sepa_export.executed' => 0,
+                                                                                             'sepa_export.closed'   => 0
+                                                                            ],
+                                                                            with_objects => ['sepa_export']);
 
   my @all_open_invoices;
   # filter out invoices with less than 1 cent outstanding
-  push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ar_invoices };
-  push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
+  push @all_open_invoices, map { $_->{is_ar}=1 ; $_ } grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ar_invoices };
+  push @all_open_invoices, map { $_->{is_ar}=0 ; $_ } grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
+
+  my %sepa_exports;
+  my %sepa_export_items_by_id = partition_by { $_->ar_id || $_->ap_id } @$all_open_sepa_export_items;
+
+  # first collect sepa export items to open invoices
+  foreach my $open_invoice (@all_open_invoices){
+    $open_invoice->{realamount}  = $::form->format_amount(\%::myconfig,$open_invoice->amount,2);
+    $open_invoice->{skonto_type} = 'without_skonto';
+    foreach (@{ $sepa_export_items_by_id{ $open_invoice->id } || [] }) {
+      my $factor                   = ($_->ar_id == $open_invoice->id ? 1 : -1);
+      $open_invoice->{realamount}  = $::form->format_amount(\%::myconfig,$open_invoice->amount*$factor,2);
+
+      $open_invoice->{skonto_type} = $_->payment_type;
+      $sepa_exports{$_->sepa_export_id} ||= { count => 0, is_ar => 0, amount => 0, proposed => 0, invoices => [], item => $_ };
+      $sepa_exports{$_->sepa_export_id}->{count}++;
+      $sepa_exports{$_->sepa_export_id}->{is_ar}++ if  $_->ar_id == $open_invoice->id;
+      $sepa_exports{$_->sepa_export_id}->{amount} += $_->amount * $factor;
+      push @{ $sepa_exports{$_->sepa_export_id}->{invoices} }, $open_invoice;
+    }
+  }
 
   # try to match each bank_transaction with each of the possible open invoices
   # by awarding points
+  my @proposals;
 
   foreach my $bt (@{ $bank_transactions }) {
-    next unless $bt->{remote_name};  # bank has no name, usually fees, use create invoice to assign
+    ## 5 Stellen hinter dem Komma auf 2 Stellen reduzieren
+    $bt->amount($bt->amount*1);
+    $bt->invoice_amount($bt->invoice_amount*1);
+
+    $bt->{proposals}    = [];
+    $bt->{rule_matches} = [];
 
     $bt->{remote_name} .= $bt->{remote_name_1} if $bt->{remote_name_1};
 
+    if ( $bt->is_batch_transaction ) {
+      my $found=0;
+      foreach ( keys  %sepa_exports) {
+        if ( abs(($sepa_exports{$_}->{amount} * 1) - ($bt->amount * 1)) < 0.01 ) {
+          ## jupp
+          @{$bt->{proposals}} = @{$sepa_exports{$_}->{invoices}};
+          $bt->{sepa_export_ok} = 1;
+          $sepa_exports{$_}->{proposed}=1;
+          push(@proposals, $bt);
+          $found=1;
+          last;
+        }
+      }
+      next if $found;
+      # batch transaction has no remotename !!
+    }
+
     # try to match the current $bt to each of the open_invoices, saving the
     # results of get_agreement_with_invoice in $open_invoice->{agreement} and
     # $open_invoice->{rule_matches}.
@@ -114,11 +178,13 @@ sub action_list {
     # the arrays $bt->{proposals} and $bt->{rule_matches}, and the agreement
     # score is stored in $bt->{agreement}
 
-    foreach my $open_invoice (@all_open_invoices){
-      ($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice);
-    };
-
-    $bt->{proposals} = [];
+    foreach my $open_invoice (@all_open_invoices) {
+      ($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice,
+        sepa_export_items => $all_open_sepa_export_items,
+      );
+      $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,
+                                      $open_invoice->amount * ($open_invoice->{is_ar} ? 1 : -1), 2);
+    }
 
     my $agreement = 15;
     my $min_agreement = 3; # suggestions must have at least this score
@@ -142,22 +208,53 @@ sub action_list {
   # to qualify as a proposal there has to be
   # * agreement >= 5  TODO: make threshold configurable in configuration
   # * there must be only one exact match
-  # * depending on whether sales or purchase the amount has to have the correct sign (so Gutschriften don't work?)
   my $proposal_threshold = 5;
-  my @proposals = grep { $_->{agreement} >= $proposal_threshold
-                         and 1 == scalar @{ $_->{proposals} }
-                         and (@{ $_->{proposals} }[0]->is_sales ? abs(@{ $_->{proposals} }[0]->amount - $_->amount) < 0.01  : abs(@{ $_->{proposals} }[0]->amount + $_->amount) < 0.01) } @{ $bank_transactions };
+  my @otherproposals = grep {
+       ($_->{agreement} >= $proposal_threshold)
+    && (1 == scalar @{ $_->{proposals} })
+  } @{ $bank_transactions };
+
+  push @proposals, @otherproposals;
 
   # sort bank transaction proposals by quality (score) of proposal
-  $bank_transactions = [ sort { $a->{agreement} <=> $b->{agreement} } @{ $bank_transactions } ] if $::form->{sort_by} eq 'proposal' and $::form->{sort_dir} == 1;
-  $bank_transactions = [ sort { $b->{agreement} <=> $a->{agreement} } @{ $bank_transactions } ] if $::form->{sort_by} eq 'proposal' and $::form->{sort_dir} == 0;
+  if ($params{sort_by} && $params{sort_by} eq 'proposal') {
+    my $dir = $params{sort_dir} ? 1 : -1;
+    $bank_transactions = [ sort { ($a->{agreement} <=> $b->{agreement}) * $dir } @{ $bank_transactions } ];
+  }
+
+  return ( $bank_transactions , \@proposals );
+}
 
+sub action_list {
+  my ($self) = @_;
+
+  if (!$::form->{filter}{bank_account}) {
+    flash('error', t8('No bank account chosen!'));
+    $self->action_search;
+    return;
+  }
+
+  my $bank_account = SL::DB::BankAccount->load_cached($::form->{filter}->{bank_account});
+  my $fromdate     = $::locale->parse_date_to_object($::form->{filter}->{fromdate});
+  my $todate       = $::locale->parse_date_to_object($::form->{filter}->{todate});
+  $todate->add( days => 1 ) if $todate;
 
+  my ($bank_transactions, $proposals) = $self->gather_bank_transactions_and_proposals(
+    bank_account => $bank_account,
+    fromdate     => $fromdate,
+    todate       => $todate,
+    sort_by      => $::form->{sort_by},
+    sort_dir     => $::form->{sort_dir},
+  );
+
+  $::request->layout->add_javascripts("kivi.BankTransaction.js");
   $self->render('bank_transactions/list',
                 title             => t8('Bank transactions MT940'),
                 BANK_TRANSACTIONS => $bank_transactions,
-                PROPOSALS         => \@proposals,
-                bank_account      => $bank_account );
+                PROPOSALS         => $proposals,
+                bank_account      => $bank_account,
+                ui_tab            => scalar(@{ $proposals }) > 0 ? 1 : 0,
+              );
 }
 
 sub action_assign_invoice {
@@ -165,46 +262,58 @@ sub action_assign_invoice {
 
   $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
 
-  $self->render('bank_transactions/assign_invoice', { layout  => 0 },
-                title      => t8('Assign invoice'),);
+  $self->render('bank_transactions/assign_invoice',
+                { layout => 0 },
+                title => t8('Assign invoice'),);
 }
 
 sub action_create_invoice {
   my ($self) = @_;
   my %myconfig = %main::myconfig;
 
-  $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
-  my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(account_number => $self->{transaction}->{remote_account_number});
+  $self->transaction(SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id}));
 
-  my $drafts = SL::DB::Manager::Draft->get_all(where => [ module => 'ap'] , with_objects => 'employee');
+  my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(iban => $self->transaction->{remote_account_number});
+  my $use_vendor_filter     = $self->transaction->{remote_account_number} && $vendor_of_transaction;
 
-  my @filtered_drafts;
+  my $templates_ap = SL::DB::Manager::RecordTemplate->get_all(
+    where        => [ template_type => 'ap_transaction' ],
+    sort_by      => [ qw(template_name) ],
+    with_objects => [ qw(employee vendor) ],
+  );
+  my $templates_gl = SL::DB::Manager::RecordTemplate->get_all(
+    query        => [ template_type => 'gl_transaction',
+                      chart_id      => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id,
+                    ],
+    sort_by      => [ qw(template_name) ],
+    with_objects => [ qw(employee record_template_items) ],
+  );
 
-  foreach my $draft ( @{ $drafts } ) {
-    my $draft_as_object = YAML::Load($draft->form);
-    my $vendor = SL::DB::Manager::Vendor->find_by(id => $draft_as_object->{vendor_id});
-    $draft->{vendor} = $vendor->name;
-    $draft->{vendor_id} = $vendor->id;
-    push @filtered_drafts, $draft;
+  # pre filter templates_ap, if we have a vendor match (IBAN eq IBAN) - show and allow user to edit this via gui!
+  $templates_ap = [ grep { $_->vendor_id == $vendor_of_transaction->id } @{ $templates_ap } ] if $use_vendor_filter;
+
+  $self->callback($self->url_for(
+    action                => 'list',
+    'filter.bank_account' => $::form->{filter}->{bank_account},
+    'filter.todate'       => $::form->{filter}->{todate},
+    'filter.fromdate'     => $::form->{filter}->{fromdate},
+  ));
+
+  # if we have exactly one ap match, use this directly
+  if (1 == scalar @{ $templates_ap }) {
+    $self->redirect_to($self->load_ap_record_template_url($templates_ap->[0]));
+
+  } else {
+    my $dialog_html = $self->render(
+      'bank_transactions/create_invoice',
+      { layout => 0, output => 0 },
+      title        => t8('Create invoice'),
+      TEMPLATES_GL => $use_vendor_filter && @{ $templates_ap } ? undef : $templates_gl,
+      TEMPLATES_AP => $templates_ap,
+      vendor_name  => $use_vendor_filter && @{ $templates_ap } ? $vendor_of_transaction->name : undef,
+    );
+    $self->js->run('kivi.BankTransaction.show_create_invoice_dialog', $dialog_html)->render;
   }
-
-  #Filter drafts
-  @filtered_drafts = grep { $_->{vendor_id} == $vendor_of_transaction->id } @filtered_drafts if $vendor_of_transaction;
-
-  my $all_vendors = SL::DB::Manager::Vendor->get_all();
-
-  $self->render('bank_transactions/create_invoice', { layout  => 0 },
-      title      => t8('Create invoice'),
-      DRAFTS     => \@filtered_drafts,
-      vendor_id  => $vendor_of_transaction ? $vendor_of_transaction->id : undef,
-      vendor_name => $vendor_of_transaction ? $vendor_of_transaction->name : undef,
-      ALL_VENDORS => $all_vendors,
-      limit      => $myconfig{vclimit},
-      callback   => $self->url_for(action                => 'list',
-                                   'filter.bank_account' => $::form->{filter}->{bank_account},
-                                   'filter.todate'       => $::form->{filter}->{todate},
-                                   'filter.fromdate'     => $::form->{filter}->{fromdate}),
-      );
 }
 
 sub action_ajax_payment_suggestion {
@@ -214,60 +323,62 @@ sub action_ajax_payment_suggestion {
   # create an HTML blob to be used by the js function add_invoices in templates/webpages/bank_transactions/list.html
   # and return encoded as JSON
 
-  my $bt = SL::DB::Manager::BankTransaction->find_by( id => $::form->{bt_id} );
-  my $invoice = SL::DB::Manager::Invoice->find_by( id => $::form->{prop_id} );
-  $invoice = SL::DB::Manager::PurchaseInvoice->find_by( id => $::form->{prop_id} ) unless $invoice;
+  croak("Need bt_id") unless $::form->{bt_id};
 
-  die unless $bt and $invoice;
+  my $invoice = SL::DB::Manager::Invoice->find_by( id => $::form->{prop_id} ) || SL::DB::Manager::PurchaseInvoice->find_by( id => $::form->{prop_id} );
 
-  my @select_options = $invoice->get_payment_select_options_for_bank_transaction($::form->{bt_id});
+  croak("No valid invoice found") unless $invoice;
 
-  my $html;
-  $html .= SL::Presenter->input_tag('invoice_ids.' . $::form->{bt_id} . '[]', $::form->{prop_id} , type => 'hidden');
-  $html .= SL::Presenter->escape( $invoice->invnumber );
-  $html .= SL::Presenter->select_tag('invoice_skontos.' . $::form->{bt_id} . '[]', \@select_options,
-                                              value_key => 'payment_type',
-                                              title_key => 'display' ) if @select_options;
-  $html .= '<a href=# onclick="delete_invoice(' . $::form->{bt_id} . ',' . $::form->{prop_id} . ');">x</a>';
-  $html = SL::Presenter->html_tag('div', $html, id => $::form->{bt_id} . '.' . $::form->{prop_id});
+  my $html = $self->render(
+    'bank_transactions/_payment_suggestion', { output => 0 },
+    bt_id          => $::form->{bt_id},
+    invoice        => $invoice,
+  );
 
-  $self->render(\ SL::JSON::to_json( { 'html' => $html } ), { layout => 0, type => 'json', process => 0 });
+  $self->render(\ SL::JSON::to_json( { 'html' => "$html" } ), { layout => 0, type => 'json', process => 0 });
 };
 
-sub action_filter_drafts {
+sub action_filter_templates {
   my ($self) = @_;
 
-  $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
-  my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(account_number => $self->{transaction}->{remote_account_number});
-
-  my $drafts = SL::DB::Manager::Draft->get_all(with_objects => 'employee');
-
-  my @filtered_drafts;
-
-  foreach my $draft ( @{ $drafts } ) {
-    my $draft_as_object = YAML::Load($draft->form);
-    my $vendor = SL::DB::Manager::Vendor->find_by(id => $draft_as_object->{vendor_id});
-    $draft->{vendor} = $vendor->name;
-    $draft->{vendor_id} = $vendor->id;
-    push @filtered_drafts, $draft;
-  }
+  $self->{transaction}      = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
+
+  my (@filter, @filter_ap);
+
+  # filter => gl and ap | filter_ap = ap (i.e. vendorname)
+  push @filter,    ('template_name' => { ilike => '%' . $::form->{template} . '%' })  if $::form->{template};
+  push @filter,    ('reference'     => { ilike => '%' . $::form->{reference} . '%' }) if $::form->{reference};
+  push @filter_ap, ('vendor.name'   => { ilike => '%' . $::form->{vendor} . '%' })    if $::form->{vendor};
+  push @filter_ap, @filter;
+  my $templates_gl = SL::DB::Manager::RecordTemplate->get_all(
+    query        => [ template_type => 'gl_transaction',
+                      chart_id      => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id,
+                      (and => \@filter) x !!@filter
+                    ],
+    with_objects => [ qw(employee record_template_items) ],
+  );
 
-  my $vendor_name = $::form->{vendor};
-  my $vendor_id = $::form->{vendor_id};
+  my $templates_ap = SL::DB::Manager::RecordTemplate->get_all(
+    where        => [ template_type => 'ap_transaction', (and => \@filter_ap) x !!@filter_ap ],
+    with_objects => [ qw(employee vendor) ],
+  );
+  $::form->{filter} //= {};
 
-  #Filter drafts
-  @filtered_drafts = grep { $_->{vendor_id} == $vendor_id } @filtered_drafts if $vendor_id;
-  @filtered_drafts = grep { $_->{vendor} =~ /$vendor_name/i } @filtered_drafts if $vendor_name;
+  $self->callback($self->url_for(
+    action                => 'list',
+    'filter.bank_account' => $::form->{filter}->{bank_account},
+    'filter.todate'       => $::form->{filter}->{todate},
+    'filter.fromdate'     => $::form->{filter}->{fromdate},
+  ));
 
   my $output  = $self->render(
-      'bank_transactions/filter_drafts',
-      { output      => 0 },
-      DRAFTS => \@filtered_drafts,
-      );
-
-  my %result = ( count => 0, html => $output );
+    'bank_transactions/_template_list',
+    { output => 0 },
+    TEMPLATES_AP => $templates_ap,
+    TEMPLATES_GL => $templates_gl,
+  );
 
-  $self->render(\to_json(\%result), { type => 'json', process => 0 });
+  $self->render(\to_json({ html => $output }), { type => 'json', process => 0 });
 }
 
 sub action_ajax_add_list {
@@ -277,8 +388,8 @@ sub action_ajax_add_list {
   my @where_purchase = (amount => { ne => \'paid' });
 
   if ($::form->{invnumber}) {
-    push @where_sale,     (invnumber => { ilike => '%' . $::form->{invnumber} . '%'});
-    push @where_purchase, (invnumber => { ilike => '%' . $::form->{invnumber} . '%'});
+    push @where_sale,     (invnumber => { ilike => like($::form->{invnumber})});
+    push @where_purchase, (invnumber => { ilike => like($::form->{invnumber})});
   }
 
   if ($::form->{amount}) {
@@ -287,13 +398,13 @@ sub action_ajax_add_list {
   }
 
   if ($::form->{vcnumber}) {
-    push @where_sale,     ('customer.customernumber' => { ilike => '%' . $::form->{vcnumber} . '%'});
-    push @where_purchase, ('vendor.vendornumber'     => { ilike => '%' . $::form->{vcnumber} . '%'});
+    push @where_sale,     ('customer.customernumber' => { ilike => like($::form->{vcnumber})});
+    push @where_purchase, ('vendor.vendornumber'     => { ilike => like($::form->{vcnumber})});
   }
 
   if ($::form->{vcname}) {
-    push @where_sale,     ('customer.name' => { ilike => '%' . $::form->{vcname} . '%'});
-    push @where_purchase, ('vendor.name'   => { ilike => '%' . $::form->{vcname} . '%'});
+    push @where_sale,     ('customer.name' => { ilike => like($::form->{vcname})});
+    push @where_purchase, ('vendor.name'   => { ilike => like($::form->{vcname})});
   }
 
   if ($::form->{transdatefrom}) {
@@ -313,7 +424,7 @@ sub action_ajax_add_list {
     };
   }
 
-  my $all_open_ar_invoices = SL::DB::Manager::Invoice->get_all(where => \@where_sale, with_objects => 'customer');
+  my $all_open_ar_invoices = SL::DB::Manager::Invoice        ->get_all(where => \@where_sale,     with_objects => 'customer');
   my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => \@where_purchase, with_objects => 'vendor');
 
   my @all_open_invoices = @{ $all_open_ar_invoices };
@@ -323,10 +434,10 @@ sub action_ajax_add_list {
   @all_open_invoices = sort { $a->id <=> $b->id } @all_open_invoices;
 
   my $output  = $self->render(
-      'bank_transactions/add_list',
-      { output      => 0 },
-      INVOICES => \@all_open_invoices,
-      );
+    'bank_transactions/add_list',
+    { output => 0 },
+    INVOICES => \@all_open_invoices,
+  );
 
   my %result = ( count => 0, html => $output );
 
@@ -338,98 +449,90 @@ sub action_ajax_accept_invoices {
 
   my @selected_invoices;
   foreach my $invoice_id (@{ $::form->{invoice_id} || [] }) {
-    my $invoice_object = SL::DB::Manager::Invoice->find_by(id => $invoice_id);
-    $invoice_object ||= SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
-
+    my $invoice_object = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
     push @selected_invoices, $invoice_object;
   }
 
-  $self->render('bank_transactions/invoices', { layout => 0 },
-                INVOICES => \@selected_invoices,
-                bt_id    => $::form->{bt_id} );
+  $self->render(
+    'bank_transactions/invoices',
+    { layout => 0 },
+    INVOICES => \@selected_invoices,
+    bt_id    => $::form->{bt_id},
+  );
 }
 
-sub action_save_invoices {
+sub save_invoices {
   my ($self) = @_;
 
-  my $invoice_hash = delete $::form->{invoice_ids}; # each key (the bt line with a bt_id) contains an array of invoice_ids
-  my $skonto_hash  = delete $::form->{invoice_skontos} || {}; # array containing the payment type, could be empty
+  return 0 if !$::form->{invoice_ids};
+
+  my %invoice_hash = %{ delete $::form->{invoice_ids} };  # each key (the bt line with a bt_id) contains an array of invoice_ids
+
+  # e.g. three partial payments with bt_ids 54, 55 and 56 for invoice with id 74:
+  # $invoice_hash = {
+  #         '55' => [
+  #                 '74'
+  #               ],
+  #         '54' => [
+  #                 '74'
+  #               ],
+  #         '56' => [
+  #                 '74'
+  #               ]
+  #       };
+  #
+  # or if the payment with bt_id 44 is used to pay invoices with ids 50, 51 and 52
+  # $invoice_hash = {
+  #           '44' => [ '50', '51', 52' ]
+  #         };
+
+  $::form->{invoice_skontos} ||= {}; # hash of arrays containing the payment types, could be empty
 
   # a bank_transaction may be assigned to several invoices, i.e. a customer
   # might pay several open invoices with one transaction
 
-  while ( my ($bt_id, $invoice_ids) = each(%$invoice_hash) ) {
-    my $bank_transaction = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
-    my $sign = $bank_transaction->amount < 0 ? -1 : 1;
-    my $amount_of_transaction = $sign * $bank_transaction->amount;
+  $self->problems([]);
 
-    my @invoices;
-    foreach my $invoice_id (@{ $invoice_ids }) {
-      push @invoices, (SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id));
-    }
-    @invoices = sort { return 1 if ($a->is_sales and $a->amount > 0);
-                          return 1 if (!$a->is_sales and $a->amount < 0);
-                          return -1; } @invoices                if $bank_transaction->amount > 0;
-    @invoices = sort { return -1 if ($a->is_sales and $a->amount > 0);
-                       return -1 if (!$a->is_sales and $a->amount < 0);
-                       return 1; } @invoices                    if $bank_transaction->amount < 0;
-
-    foreach my $invoice (@invoices) {
-
-      # Check if bank_transaction already has a link to the invoice, may only be linked once per invoice
-      # This might be caused by the user reloading a page and resending the form
-      die t8("Bank transaction with id #1 has already been linked to #2.", $bank_transaction->id, $invoice->displayable_name)
-        if _existing_record_link($bank_transaction, $invoice);
-
-      my $payment_type;
-      if ( defined $skonto_hash->{"$bt_id"} ) {
-        $payment_type = shift(@{ $skonto_hash->{"$bt_id"} });
-      } else {
-        $payment_type = 'without_skonto';
-      };
-      if ($amount_of_transaction == 0) {
-        flash('warning',  $::locale->text('There are invoices which could not be paid by bank transaction #1 (Account number: #2, bank code: #3)!',
-                                            $bank_transaction->purpose,
-                                            $bank_transaction->remote_account_number,
-                                            $bank_transaction->remote_bank_code));
-        last;
-      }
-      # pay invoice or go to the next bank transaction if the amount is not sufficiently high
-      if ($invoice->amount <= $amount_of_transaction) {
-        $invoice->pay_invoice(chart_id     => $bank_transaction->local_bank_account->chart_id,
-                              trans_id     => $invoice->id,
-                              amount       => $invoice->amount,
-                              payment_type => $payment_type,
-                              transdate    => $bank_transaction->transdate->to_kivitendo);
-        if ($invoice->is_sales) {
-          $amount_of_transaction -= $sign * $invoice->amount;
-          $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $invoice->amount);
-        } else {
-          $amount_of_transaction += $sign * $invoice->amount if (!$invoice->is_sales);
-          $bank_transaction->invoice_amount($bank_transaction->invoice_amount - $invoice->amount);
-        }
-      } else {
-        $invoice->pay_invoice(chart_id     => $bank_transaction->local_bank_account->chart_id,
-                              trans_id     => $invoice->id,
-                              amount       => $amount_of_transaction,
-                              payment_type => $payment_type,
-                              transdate    => $bank_transaction->transdate->to_kivitendo);
-        $bank_transaction->invoice_amount($bank_transaction->amount) if $invoice->is_sales;
-        $bank_transaction->invoice_amount($bank_transaction->amount) if !$invoice->is_sales;
-        $amount_of_transaction = 0;
-      }
+  my $count = 0;
 
-      # Record a record link from the bank transaction to the invoice
-      my @props = (
-          from_table => 'bank_transactions',
-          from_id    => $bt_id,
-          to_table   => $invoice->is_sales ? 'ar' : 'ap',
-          to_id      => $invoice->id,
-          );
-
-      SL::DB::RecordLink->new(@props)->save;
+  if ( $::form->{proposal_ids} ) {
+    foreach (@{ $::form->{proposal_ids} }) {
+      my  $bank_transaction_id = $_;
+      my  $invoice_ids = $invoice_hash{$_};
+      push @{ $self->problems }, $self->save_single_bank_transaction(
+        bank_transaction_id => $bank_transaction_id,
+        invoice_ids         => $invoice_ids,
+        sources             => ($::form->{sources} // {})->{$_},
+        memos               => ($::form->{memos}   // {})->{$_},
+      );
+      $count += scalar( @{$invoice_ids} );
+    }
+  } else {
+    while ( my ($bank_transaction_id, $invoice_ids) = each(%invoice_hash) ) {
+      push @{ $self->problems }, $self->save_single_bank_transaction(
+        bank_transaction_id => $bank_transaction_id,
+        invoice_ids         => $invoice_ids,
+        sources             => [  map { $::form->{"sources_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ],
+        memos               => [  map { $::form->{"memos_${bank_transaction_id}_${_}"}   } @{ $invoice_ids } ],
+      );
+      $count += scalar( @{$invoice_ids} );
     }
-    $bank_transaction->save;
+  }
+  my $max_count = $count;
+  foreach (@{ $self->problems }) {
+    $count-- if $_->{result} eq 'error';
+  }
+  return ($count, $max_count);
+}
+
+sub action_save_invoices {
+  my ($self) = @_;
+  my ($success_count, $max_count) = $self->save_invoices();
+
+  if ($success_count == $max_count) {
+    flash('ok', t8('#1 invoice(s) saved.', $success_count));
+  } else {
+    flash('error', t8('At least #1 invoice(s) not saved', $max_count - $success_count));
   }
 
   $self->action_list();
@@ -438,44 +541,302 @@ sub action_save_invoices {
 sub action_save_proposals {
   my ($self) = @_;
 
-  foreach my $bt_id (@{ $::form->{proposal_ids} }) {
-    my $bt = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
+  if ( $::form->{proposal_ids} ) {
+    my $propcount = scalar(@{ $::form->{proposal_ids} });
+    if ( $propcount > 0 ) {
+      my $count = $self->save_invoices();
 
-    my $arap = SL::DB::Manager::Invoice->find_by(id => $::form->{"proposed_invoice_$bt_id"});
-    $arap    = SL::DB::Manager::PurchaseInvoice->find_by(id => $::form->{"proposed_invoice_$bt_id"}) if not defined $arap;
+      flash('ok', t8('#1 proposal(s) with #2 invoice(s) saved.',  $propcount, $count));
+    }
+  }
+  $self->action_list();
 
-    # check for existing record_link for that $bt and $arap
-    # do this before any changes to $bt are made
-    die t8("Bank transaction with id #1 has already been linked to #2.", $bt->id, $arap->displayable_name)
-      if _existing_record_link($bt, $arap);
+}
 
-    #mark bt as booked
-    $bt->invoice_amount($bt->amount);
-    $bt->save;
+sub save_single_bank_transaction {
+  my ($self, %params) = @_;
 
-    #pay invoice
-    $arap->pay_invoice(chart_id  => $bt->local_bank_account->chart_id,
-                       trans_id  => $arap->id,
-                       amount    => $arap->amount,
-                       transdate => $bt->transdate->to_kivitendo);
-    $arap->save;
+  my %data = (
+    %params,
+    bank_transaction => SL::DB::Manager::BankTransaction->find_by(id => $params{bank_transaction_id}),
+    invoices         => [],
+  );
+
+  if (!$data{bank_transaction}) {
+    return {
+      %data,
+      result => 'error',
+      message => $::locale->text('The ID #1 is not a valid database ID.', $data{bank_transaction_id}),
+    };
+  }
+
+  my $bank_transaction = $data{bank_transaction};
+
+  if ($bank_transaction->closed_period) {
+    return {
+      %data,
+      result => 'error',
+      message => $::locale->text('Cannot post payment for a closed period!'),
+    };
+  }
+  my (@warnings);
+
+  my $worker = sub {
+    my $bt_id                 = $data{bank_transaction_id};
+    my $sign                  = $bank_transaction->amount < 0 ? -1 : 1;
+    my $payment_received      = $bank_transaction->amount > 0;
+    my $payment_sent          = $bank_transaction->amount < 0;
+
+
+    foreach my $invoice_id (@{ $params{invoice_ids} }) {
+      my $invoice = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
+      if (!$invoice) {
+        return {
+          %data,
+          result  => 'error',
+          message => $::locale->text("The ID #1 is not a valid database ID.", $invoice_id),
+        };
+      }
+      push @{ $data{invoices} }, $invoice;
+    }
 
-    #create record link
-    my @props = (
+    if (   $payment_received
+        && any {    ( $_->is_sales && ($_->amount < 0))
+                 || (!$_->is_sales && ($_->amount > 0))
+               } @{ $data{invoices} }) {
+      return {
+        %data,
+        result  => 'error',
+        message => $::locale->text("Received payments can only be posted for sales invoices and purchase credit notes."),
+      };
+    }
+
+    if (   $payment_sent
+        && any {    ( $_->is_sales && ($_->amount > 0))
+                 || (!$_->is_sales && ($_->amount < 0) && ($_->invoice_type eq 'purchase_invoice'))
+               } @{ $data{invoices} }) {
+      return {
+        %data,
+        result  => 'error',
+        message => $::locale->text("Sent payments can only be posted for purchase invoices and sales credit notes."),
+      };
+    }
+
+    my $max_invoices = scalar(@{ $data{invoices} });
+    my $n_invoices   = 0;
+
+    foreach my $invoice (@{ $data{invoices} }) {
+      my $source = ($data{sources} // [])->[$n_invoices];
+      my $memo   = ($data{memos}   // [])->[$n_invoices];
+
+      $n_invoices++ ;
+      # safety check invoice open
+      croak("Invoice closed. Cannot proceed.") unless ($invoice->open_amount);
+
+      if (   ($payment_sent     && $bank_transaction->not_assigned_amount >= 0)
+          || ($payment_received && $bank_transaction->not_assigned_amount <= 0)) {
+        return {
+          %data,
+          result  => 'error',
+          message => $::locale->text("A payment can only be posted for multiple invoices if the amount to post is equal to or bigger than the sum of the open amounts of the affected invoices."),
+        };
+      }
+
+      my ($payment_type, $free_skonto_amount);
+      if ( defined $::form->{invoice_skontos}->{"$bt_id"} ) {
+        $payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} });
+      } else {
+        $payment_type = 'without_skonto';
+      }
+
+      if ($payment_type eq 'free_skonto') {
+        # parse user input > 0
+        if ($::form->parse_amount(\%::myconfig, $::form->{"free_skonto_amount"}->{"$bt_id"}{$invoice->id}) > 0) {
+          $free_skonto_amount = $::form->parse_amount(\%::myconfig, $::form->{"free_skonto_amount"}->{"$bt_id"}{$invoice->id});
+        } else {
+          return {
+            %data,
+            result  => 'error',
+            message => $::locale->text("Free skonto amount has to be a positive number."),
+          };
+        }
+      }
+    # pay invoice
+    # TODO rewrite this: really booked amount should be a return value of Payment.pm
+    # also this controller shouldnt care about how to calc skonto. we simply delegate the
+    # payment_type to the helper and get the corresponding bank_transaction values back
+    # hotfix to get the signs right - compare absolute values and later set the signs
+    # should be better done elsewhere - changing not_assigned_amount to abs feels seriously bogus
+
+    my $open_amount = $payment_type eq 'with_skonto_pt' ? $invoice->amount_less_skonto : $invoice->open_amount;
+    $open_amount            = abs($open_amount);
+    $open_amount           -= $free_skonto_amount if ($payment_type eq 'free_skonto');
+    my $not_assigned_amount = abs($bank_transaction->not_assigned_amount);
+    my $amount_for_booking  = ($open_amount < $not_assigned_amount) ? $open_amount : $not_assigned_amount;
+    my $amount_for_payment  = $amount_for_booking;
+
+    # get the right direction for the payment bookings (all amounts < 0 are stornos, credit notes or negative ap)
+    $amount_for_payment *= -1 if $invoice->amount < 0;
+    $free_skonto_amount *= -1 if ($free_skonto_amount && $invoice->amount < 0);
+    # get the right direction for the bank transaction
+    $amount_for_booking *= $sign;
+
+    $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $amount_for_booking);
+
+    # ... and then pay the invoice
+    my @acc_ids = $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
+                          trans_id      => $invoice->id,
+                          amount        => $amount_for_payment,
+                          payment_type  => $payment_type,
+                          source        => $source,
+                          memo          => $memo,
+                          skonto_amount => $free_skonto_amount,
+                          bt_id         => $bt_id,
+                          transdate     => $bank_transaction->valutadate->to_kivitendo);
+    # ... and record the origin via BankTransactionAccTrans
+    if (scalar(@acc_ids) < 2) {
+      return {
+        %data,
+        result  => 'error',
+        message => $::locale->text("Unable to book transactions for bank purpose #1", $bank_transaction->purpose),
+      };
+    }
+    foreach my $acc_trans_id (@acc_ids) {
+        my $id_type = $invoice->is_sales ? 'ar' : 'ap';
+        my  %props_acc = (
+          acc_trans_id        => $acc_trans_id,
+          bank_transaction_id => $bank_transaction->id,
+          $id_type            => $invoice->id,
+        );
+        SL::DB::BankTransactionAccTrans->new(%props_acc)->save;
+    }
+      # Record a record link from the bank transaction to the invoice
+      my %props = (
         from_table => 'bank_transactions',
         from_id    => $bt_id,
-        to_table   => $arap->is_sales ? 'ar' : 'ap',
-        to_id      => $arap->id,
-        );
+        to_table   => $invoice->is_sales ? 'ar' : 'ap',
+        to_id      => $invoice->id,
+      );
+      SL::DB::RecordLink->new(%props)->save;
+
+      # "close" a sepa_export_item if it exists
+      # code duplicated in action_save_proposals!
+      # currently only works, if there is only exactly one open sepa_export_item
+      if ( my $seis = $invoice->find_sepa_export_items({ executed => 0 }) ) {
+        if ( scalar @$seis == 1 ) {
+          # moved the execution and the check for sepa_export into a method,
+          # this isn't part of a transaction, though
+          $seis->[0]->set_executed if $invoice->id == $seis->[0]->arap_id;
+        }
+      }
 
-    SL::DB::RecordLink->new(@props)->save;
-  }
+    }
+    $bank_transaction->save;
 
-  flash('ok', t8('#1 proposal(s) saved.', scalar @{ $::form->{proposal_ids} }));
+    # 'undef' means 'no error' here.
+    return undef;
+  };
 
-  $self->action_list();
+  my $error;
+  my $rez = $data{bank_transaction}->db->with_transaction(sub {
+    eval {
+      $error = $worker->();
+      1;
+
+    } or do {
+      $error = {
+        %data,
+        result  => 'error',
+        message => $@,
+      };
+    };
+
+    # Rollback Fehler nicht weiterreichen
+    # die if $error;
+    # aber einen rollback von hand
+    $::lxdebug->message(LXDebug->DEBUG2(),"finish worker with ". ($error ? $error->{result} : '-'));
+    $data{bank_transaction}->db->dbh->rollback if $error && $error->{result} eq 'error';
+  });
+
+  return grep { $_ } ($error, @warnings);
 }
+sub action_unlink_bank_transaction {
+  my ($self, %params) = @_;
+
+  croak("No bank transaction ids") unless scalar @{ $::form->{ids}} > 0;
+
+  my $success_count;
 
+  foreach my $bt_id (@{ $::form->{ids}} )  {
+
+    my $bank_transaction = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
+    croak("No valid bank transaction found") unless (ref($bank_transaction)  eq 'SL::DB::BankTransaction');
+    croak t8('Cannot unlink payment for a closed period!') if $bank_transaction->closed_period;
+
+    # everything in one transaction
+    my $rez = $bank_transaction->db->with_transaction(sub {
+      # 1. remove all reconciliations (due to underlying trigger, this has to be the first step)
+      my $rec_links = SL::DB::Manager::ReconciliationLink->get_all(where => [ bank_transaction_id => $bt_id ]);
+      $_->delete for @{ $rec_links };
+
+      my %trans_ids;
+      foreach my $acc_trans_id_entry (@{ SL::DB::Manager::BankTransactionAccTrans->get_all(where => [bank_transaction_id => $bt_id ] )}) {
+
+        my $acc_trans = SL::DB::Manager::AccTransaction->get_all(where => [acc_trans_id => $acc_trans_id_entry->acc_trans_id]);
+
+        # save trans_id and type
+        die "no type" unless ($acc_trans_id_entry->ar_id || $acc_trans_id_entry->ap_id || $acc_trans_id_entry->gl_id);
+        $trans_ids{$acc_trans_id_entry->ar_id} = 'ar' if $acc_trans_id_entry->ar_id;
+        $trans_ids{$acc_trans_id_entry->ap_id} = 'ap' if $acc_trans_id_entry->ap_id;
+        $trans_ids{$acc_trans_id_entry->gl_id} = 'gl' if $acc_trans_id_entry->gl_id;
+        # 2. all good -> ready to delete acc_trans and bt_acc link
+        $acc_trans_id_entry->delete;
+        $_->delete for @{ $acc_trans };
+      }
+      # 3. update arap.paid (may not be 0, yet)
+      #    or in case of gl, delete whole entry
+      while (my ($trans_id, $type) = each %trans_ids) {
+        if ($type eq 'gl') {
+          SL::DB::Manager::GLTransaction->delete_all(where => [ id => $trans_id ]);
+          next;
+        }
+        die ("invalid type") unless $type =~ m/^(ar|ap)$/;
+
+        # recalc and set paid via database query
+        my $query = qq|UPDATE $type SET paid =
+                        (SELECT COALESCE(abs(sum(amount)),0) FROM acc_trans
+                         WHERE trans_id = ?
+                         AND chart_link ilike '%paid%')
+                       WHERE id = ?|;
+
+        die if (do_query($::form, $bank_transaction->db->dbh, $query, $trans_id, $trans_id) == -1);
+      }
+      # 4. and delete all (if any) record links
+      my $rl = SL::DB::Manager::RecordLink->delete_all(where => [ from_id => $bt_id, from_table => 'bank_transactions' ]);
+
+      # 5. finally reset  this bank transaction
+      $bank_transaction->invoice_amount(0);
+      $bank_transaction->cleared(0);
+      $bank_transaction->save;
+      # 6. and add a log entry in history_erp
+      SL::DB::History->new(
+        trans_id    => $bank_transaction->id,
+        snumbers    => 'bank_transaction_unlink_' . $bank_transaction->id,
+        employee_id => SL::DB::Manager::Employee->current->id,
+        what_done   => 'bank_transaction',
+        addition    => 'UNLINKED',
+      )->save();
+
+      1;
+
+    }) || die t8('error while unlinking payment #1 : ', $bank_transaction->purpose) . $bank_transaction->db->error . "\n";
+
+    $success_count++;
+  }
+
+  flash('ok', t8('#1 bank transaction bookings undone.', $success_count));
+  $self->action_list_all() unless $params{testcase};
+}
 #
 # filters
 #
@@ -495,12 +856,13 @@ sub make_filter_summary {
   my @filter_strings;
 
   my @filters = (
-    [ $filter->{"transdate:date::ge"},  $::locale->text('Transdate')  . " " . $::locale->text('From Date') ],
-    [ $filter->{"transdate:date::le"},  $::locale->text('Transdate')  . " " . $::locale->text('To Date')   ],
-    [ $filter->{"valutadate:date::ge"}, $::locale->text('Valutadate') . " " . $::locale->text('From Date') ],
-    [ $filter->{"valutadate:date::le"}, $::locale->text('Valutadate') . " " . $::locale->text('To Date')   ],
-    [ $filter->{"amount:number"},       $::locale->text('Amount')                                          ],
-    [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account')                          ],
+    [ $filter->{"transdate:date::ge"},      $::locale->text('Transdate')  . " " . $::locale->text('From Date') ],
+    [ $filter->{"transdate:date::le"},      $::locale->text('Transdate')  . " " . $::locale->text('To Date')   ],
+    [ $filter->{"valutadate:date::ge"},     $::locale->text('Valutadate') . " " . $::locale->text('From Date') ],
+    [ $filter->{"valutadate:date::le"},     $::locale->text('Valutadate') . " " . $::locale->text('To Date')   ],
+    [ $filter->{"amount:number"},           $::locale->text('Amount')                                          ],
+    [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account')                              ],
+    [ $filter->{"remote_name:substr::ilike"}, $::locale->text('Remote name')                                   ],
   );
 
   for (@filters) {
@@ -518,25 +880,35 @@ sub prepare_report {
   my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
   $self->{report} = $report;
 
-  my @columns     = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount invoice_amount invoices currency purpose local_account_number local_bank_code id);
+  my @columns     = qw(ids local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount invoice_amount invoices currency purpose local_account_number local_bank_code id);
   my @sortable    = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount                                  purpose local_account_number local_bank_code);
 
   my %column_defs = (
-    transdate             => { sub => sub { $_[0]->transdate_as_date } },
-    valutadate            => { sub => sub { $_[0]->valutadate_as_date } },
+    ids                 => { raw_header_data => checkbox_tag("", id => "check_all", checkall  => "[data-checkall=1]"),
+                             'align'         => 'center',
+                             raw_data        => sub { if (@{ $_[0]->linked_invoices }) {
+                                                        if ($_[0]->closed_period) {
+                                                          html_tag('text', "X"); #, tooltip => t8('Bank Transaction is in a closed period.')),
+                                                        } else {
+                                                          checkbox_tag("ids[]", value => $_[0]->id, "data-checkall" => 1);
+                                                        }
+                                                } } },
+    transdate             => { sub   => sub { $_[0]->transdate_as_date } },
+    valutadate            => { sub   => sub { $_[0]->valutadate_as_date } },
     remote_name           => { },
     remote_account_number => { },
     remote_bank_code      => { },
-    amount                => { sub => sub { $_[0]->amount_as_number },
+    amount                => { sub   => sub { $_[0]->amount_as_number },
                                align => 'right' },
-    invoice_amount        => { sub => sub { $_[0]->invoice_amount_as_number },
+    invoice_amount        => { sub   => sub { $_[0]->invoice_amount_as_number },
                                align => 'right' },
-    invoices              => { sub => sub { $_[0]->linked_invoices } },
-    currency              => { sub => sub { $_[0]->currency->name } },
+    invoices              => { sub   => sub { my @invnumbers; for my $obj (@{ $_[0]->linked_invoices }) {
+                                                                next unless $obj; push @invnumbers, $obj->invnumber } return \@invnumbers } },
+    currency              => { sub   => sub { $_[0]->currency->name } },
     purpose               => { },
-    local_account_number  => { sub => sub { $_[0]->local_bank_account->account_number } },
-    local_bank_code       => { sub => sub { $_[0]->local_bank_account->bank_code } },
-    local_bank_name       => { sub => sub { $_[0]->local_bank_account->name } },
+    local_account_number  => { sub   => sub { $_[0]->local_bank_account->account_number } },
+    local_bank_code       => { sub   => sub { $_[0]->local_bank_account->bank_code } },
+    local_bank_name       => { sub   => sub { $_[0]->local_bank_account->name } },
     id                    => {},
   );
 
@@ -566,31 +938,19 @@ sub prepare_report {
   );
 }
 
-sub _existing_record_link {
-  my ($bt, $invoice) = @_;
-
-  # check whether a record link from banktransaction $bt already exists to
-  # invoice $invoice, returns 1 if that is the case
-
-  die unless $bt->isa("SL::DB::BankTransaction") && ( $invoice->isa("SL::DB::Invoice") || $invoice->isa("SL::DB::PurchaseInvoice") );
-
-  my $linked_record_to_table = $invoice->is_sales ? 'Invoice' : 'PurchaseInvoice';
-  my $linked_records = $bt->linked_records( direction => 'to', to => $linked_record_to_table, query => [ id => $invoice->id ]  );
-
-  return @$linked_records ? 1 : 0;
-};
-
+sub init_problems { [] }
 
 sub init_models {
   my ($self) = @_;
 
   SL::Controller::Helper::GetModels->new(
     controller => $self,
-    sorted => {
+    sorted     => {
       _default => {
-        by    => 'transdate',
-        dir   => 0,   # 1 = ASC, 0 = DESC : default sort is newest at top
+        by  => 'transdate',
+        dir => 0,   # 1 = ASC, 0 = DESC : default sort is newest at top
       },
+      id                    => t8('ID'),
       transdate             => t8('Transdate'),
       remote_name           => t8('Remote name'),
       amount                => t8('Amount'),
@@ -609,4 +969,177 @@ sub init_models {
   );
 }
 
+sub load_ap_record_template_url {
+  my ($self, $template) = @_;
+
+  return $self->url_for(
+    controller                           => 'ap.pl',
+    action                               => 'load_record_template',
+    id                                   => $template->id,
+    'form_defaults.amount_1'             => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
+    'form_defaults.transdate'            => $self->transaction->transdate_as_date,
+    'form_defaults.duedate'              => $self->transaction->transdate_as_date,
+    'form_defaults.no_payment_bookings'  => 1,
+    'form_defaults.paid_1_suggestion'    => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
+    'form_defaults.AP_paid_1_suggestion' => $self->transaction->local_bank_account->chart->accno,
+    'form_defaults.callback'             => $self->callback,
+    'form_defaults.notes'                => $self->convert_purpose_for_template($template, $self->transaction->purpose),
+  );
+}
+
+sub load_gl_record_template_url {
+  my ($self, $template) = @_;
+
+  return $self->url_for(
+    controller                           => 'gl.pl',
+    action                               => 'load_record_template',
+    id                                   => $template->id,
+    'form_defaults.amount_1'             => abs($self->transaction->not_assigned_amount), # always positive
+    'form_defaults.transdate'            => $self->transaction->transdate_as_date,
+    'form_defaults.callback'             => $self->callback,
+    'form_defaults.bt_id'                => $self->transaction->id,
+    'form_defaults.bt_chart_id'          => $self->transaction->local_bank_account->chart->id,
+    'form_defaults.description'          => $self->convert_purpose_for_template($template, $self->transaction->purpose),
+  );
+}
+
+sub convert_purpose_for_template {
+  my ($self, $template, $purpose) = @_;
+
+  # enter custom code here
+
+  return $purpose;
+}
+
+sub setup_search_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Filter'),
+        submit    => [ '#search_form', { action => 'BankTransaction/list' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_list_all_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [ t8('Actions') ],
+        action => [
+          t8('Unlink bank transactions'),
+            submit => [ '#form', { action => 'BankTransaction/unlink_bank_transaction' } ],
+            checks => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ],
+            disabled  => $::instance_conf->get_payments_changeable ? t8('Cannot safely unlink bank transactions, please set the posting configuration for payments to unchangeable.') : undef,
+          ],
+        ],
+        action => [
+          t8('Filter'),
+          submit    => [ '#filter_form', { action => 'BankTransaction/list_all' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
 1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Controller::BankTransaction - Posting payments to invoices from
+bank transactions imported earlier
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<save_single_bank_transaction %params>
+
+Takes a bank transaction ID (as parameter C<bank_transaction_id> and
+tries to post its amount to a certain number of invoices (parameter
+C<invoice_ids>, an array ref of database IDs to purchase or sales
+invoice objects).
+
+This method handles already partly assigned bank transactions.
+
+This method cannot handle already partly assigned bank transactions, i.e.
+a bank transaction that has a invoice_amount <> 0 but not the fully
+transaction amount (invoice_amount == amount).
+
+If the amount of the bank transaction is higher than the sum of
+the assigned invoices (1 .. n) the bank transaction will only be
+partly assigned.
+
+The whole function is wrapped in a database transaction. If an
+exception occurs the bank transaction is not posted at all. The same
+is true if the code detects an error during the execution, e.g. a bank
+transaction that's already been posted earlier. In both cases the
+database transaction will be rolled back.
+
+If warnings but not errors occur the database transaction is still
+committed.
+
+The return value is an error object or C<undef> if the function
+succeeded. The calling function will collect all warnings and errors
+and display them in a nicely formatted table if any occurred.
+
+An error object is a hash reference containing the following members:
+
+=over 2
+
+=item * C<result> — can be either C<warning> or C<error>. Warnings are
+displayed slightly different than errors.
+
+=item * C<message> — a human-readable message included in the list of
+errors meant as the description of why the problem happened
+
+=item * C<bank_transaction_id>, C<invoice_ids> — the same parameters
+that the function was called with
+
+=item * C<bank_transaction> — the database object
+(C<SL::DB::BankTransaction>) corresponding to C<bank_transaction_id>
+
+=item * C<invoices> — an array ref of the database objects (either
+C<SL::DB::Invoice> or C<SL::DB::PurchaseInvoice>) corresponding to
+C<invoice_ids>
+
+=back
+
+=item C<action_unlink_bank_transaction>
+
+Takes one or more bank transaction ID (as parameter C<form::ids>) and
+tries to revert all payment bookings including already cleared bookings.
+
+This method won't undo payments that are in a closed period and assumes
+that payments are not manually changed, i.e. only imported payments.
+
+GL-records will be deleted completely if a bank transaction was the source.
+
+TODO: we still rely on linked_records for the check boxes
+
+=item C<convert_purpose_for_template>
+
+This method can be used to parse, filter and convert the bank transaction's
+purpose string before it will be assigned to the description field of a
+gl transaction or to the notes field of an ap transaction.
+You have to write your own custom code.
+
+=back
+
+=head1 AUTHOR
+
+Niclas Zimmermann E<lt>niclas@kivitendo-premium.deE<gt>,
+Geoffrey Richardson E<lt>information@richardson-bueren.deE<gt>
+
+=cut
index b97f3c5..63f153f 100644 (file)
@@ -7,6 +7,7 @@ use parent qw(Rose::Object);
 use Carp;
 use IO::File;
 use List::Util qw(first);
+use MIME::Base64;
 use SL::Request qw(flatten);
 use SL::MoreCommon qw(uri_encode);
 use SL::Presenter;
@@ -14,7 +15,7 @@ use SL::Presenter;
 use Rose::Object::MakeMethods::Generic
 (
   scalar                  => [ qw(action_name) ],
-  'scalar --get_set_init' => [ qw(js) ],
+  'scalar --get_set_init' => [ qw(js p) ],
 );
 
 #
@@ -29,6 +30,7 @@ sub url_for {
   my %params      = ref($_[0]) eq 'HASH' ? %{ $_[0] } : @_;
   my $controller  = delete($params{controller}) || $self->controller_name;
   my $action      = $params{action}             || 'dispatch';
+  my $fragment    = delete $params{fragment};
 
   my $script;
   if ($controller =~ m/\.pl$/) {
@@ -41,7 +43,7 @@ sub url_for {
 
   my $query       = join '&', map { uri_encode($_->[0]) . '=' . uri_encode($_->[1]) } @{ flatten(\%params) };
 
-  return "${script}?${query}";
+  return "${script}?${query}" . (defined $fragment ? "#$fragment" : '');
 }
 
 sub redirect_to {
@@ -53,7 +55,7 @@ sub redirect_to {
     SL::Helper::Flash::delay_flash();
   }
 
-  return $self->render(SL::ClientJS->new->redirect_to($self->url_for(@_))) if $::request->is_ajax;
+  return $self->render(SL::ClientJS->new->redirect_to($url)) if $::request->is_ajax;
 
   print $::request->{cgi}->redirect($url);
 }
@@ -73,6 +75,7 @@ sub render {
     header     => 1,
     layout     => 1,
     process    => 1,
+    status     => '200 ok',
   );
   $options->{$_} //= $defaults{$_} for keys %defaults;
   $options->{type} = lc $options->{type};
@@ -129,7 +132,8 @@ sub render {
                         :                              'application/json';
 
       print $::form->create_http_response(content_type => $content_type,
-                                          charset      => 'UTF-8');
+                                          charset      => 'UTF-8',
+                                          (status      => $options->{status}) x !!$options->{status});
     }
   }
 
@@ -155,23 +159,35 @@ sub send_file {
   my $attachment_name =  $params{name} || (!ref($file_name_or_content) ? $file_name_or_content : '');
   $attachment_name    =~ s:.*//::g;
 
-  print $::form->create_http_response(content_type        => $content_type,
-                                      content_disposition => 'attachment; filename="' . $attachment_name . '"',
-                                      content_length      => $size);
-
-  if (!ref $file_name_or_content) {
-    $::locale->with_raw_io(\*STDOUT, sub { print while <$file> });
-    $file->close;
-    unlink $file_name_or_content if $params{unlink};
+  if ($::request->is_ajax || $params{ajax}) {
+    my $octets = ref $file_name_or_content ? $file_name_or_content : \ do { local $/ = undef; <$file> };
+    $self->js->save_file(MIME::Base64::encode_base64($$octets), $content_type, $size, $attachment_name);
+    $self->js->render unless $params{js_no_render};
   } else {
-    $::locale->with_raw_io(\*STDOUT, sub { print $$file_name_or_content });
+    print $::form->create_http_response(content_type        => $content_type,
+                                        content_disposition => 'attachment; filename="' . $attachment_name . '"',
+                                        content_length      => $size);
+
+    if (!ref $file_name_or_content) {
+      $::locale->with_raw_io(\*STDOUT, sub { print while <$file> });
+      $file->close;
+      unlink $file_name_or_content if $params{unlink};
+    } else {
+      $::locale->with_raw_io(\*STDOUT, sub { print $$file_name_or_content });
+    }
   }
+
+  return 1;
 }
 
 sub presenter {
   return SL::Presenter->get;
 }
 
+sub init_p {
+  return SL::Presenter->get;
+}
+
 sub controller_name {
   my $class = ref($_[0]) || $_[0];
   $class    =~ s/^SL::Controller:://;
@@ -526,6 +542,9 @@ L</controller_name>.
 The action to call is given by C<$params{action}>. It defaults to
 C<dispatch>.
 
+If C<$params{fragment}> is present, it's used as the fragment of the resulting
+URL.
+
 All other key/value pairs in C<%params> are appended as GET parameters
 to the URL.
 
index 3f93582..a608a6e 100644 (file)
@@ -33,9 +33,10 @@ sub action_list {
       $chartlist{ $gruppe->id } = SL::DB::TaxzoneChart->get_all_accounts_by_buchungsgruppen_id($gruppe->id);
   }
 
+  $self->setup_list_action_bar;
   $::form->header;
   $self->render('buchungsgruppen/list',
-                title           => t8('Buchungsgruppen'),
+                title           => t8('Booking groups'),
                 BUCHUNGSGRUPPEN => $buchungsgruppen,
                 CHARTLIST       => \%chartlist,
                 TAXZONES        => $taxzones);
@@ -45,12 +46,13 @@ sub action_new {
   my ($self) = @_;
 
   $self->config(SL::DB::Buchungsgruppe->new());
-  $self->show_form(title => t8('Add Buchungsgruppe'));
+  $self->show_form(title => t8('Add booking group'));
 }
 
 sub show_form {
   my ($self, %params) = @_;
 
+  $self->setup_show_form_action_bar;
   $self->render('buchungsgruppen/form', %params,
                  TAXZONES       => SL::DB::Manager::TaxZone->get_all_sorted());
 }
@@ -63,7 +65,7 @@ sub action_edit {
   # orphaned method, where an IF-ELSE statement toggles between L.select_tag
   # and text.
 
-  $self->show_form(title     => t8('Edit Buchungsgruppe'),
+  $self->show_form(title     => t8('Edit booking group'),
                    CHARTLIST => SL::DB::TaxzoneChart->get_all_accounts_by_buchungsgruppen_id($self->config->id));
 }
 
@@ -85,13 +87,14 @@ sub action_delete {
   # allow deletion of unused Buchungsgruppen. Will fail, due to database
   # constraint, if Buchungsgruppe is connected to a part
 
-  my $db = $self->{config}->db;
-  $db->do_transaction(sub {
-        my $taxzone_charts = SL::DB::Manager::TaxzoneChart->get_all(where => [ buchungsgruppen_id => $self->config->id ]);
-        foreach my $taxzonechart ( @{$taxzone_charts} ) { $taxzonechart->delete };
-        $self->config->delete();
-        flash_later('info',  $::locale->text('The buchungsgruppe has been deleted.'));
-  }) || flash_later('error', $::locale->text('The buchungsgruppe is in use and cannot be deleted.'));
+  $self->{config}->db->with_transaction(sub {
+    my $taxzone_charts = SL::DB::Manager::TaxzoneChart->get_all(where => [ buchungsgruppen_id => $self->config->id ]);
+    foreach my $taxzonechart ( @{$taxzone_charts} ) { $taxzonechart->delete };
+    $self->config->delete();
+    flash_later('info',  $::locale->text('The booking group has been deleted.'));
+
+    1;
+  }) || flash_later('error', $::locale->text('The booking group is in use and cannot be deleted.'));
 
   $self->redirect_to(action => 'list');
 
@@ -133,7 +136,7 @@ sub create_or_update {
   my @errors;
 
   my $db = $self->config->db;
-  $db->do_transaction( sub {
+  if (!$db->with_transaction(sub {
 
     $self->config->assign_attributes(%{ $params }); # assign description and inventory_accno_id
 
@@ -169,11 +172,15 @@ sub create_or_update {
         $taxzone_chart->save;
       }
     }
-  } ) || die @errors ? join("\n", @errors) . "\n" : $db->error . "\n";
-         # die with rollback of taxzone save if saving of any of the taxzone_charts fails
-         # only show the $db->error if we haven't already identified the likely error ourselves
 
-  flash_later('info', $is_new ? t8('The Buchungsgruppe has been created.') : t8('The Buchungsgruppe has been saved.'));
+    1;
+  })) {
+    die @errors ? join("\n", @errors) . "\n" : $db->error . "\n";
+    # die with rollback of taxzone save if saving of any of the taxzone_charts fails
+    # only show the $db->error if we haven't already identified the likely error ourselves
+  }
+
+  flash_later('info', $is_new ? t8('The booking group has been created.') : t8('The booking group has been saved.'));
   $self->redirect_to(action => 'list');
 }
 
@@ -183,4 +190,53 @@ sub create_or_update {
 
 sub init_defaults        { SL::DB::Default->get }
 
+#
+# helpers
+#
+
+sub setup_show_form_action_bar {
+  my ($self) = @_;
+
+  my $is_new = !$self->config->id;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => 'Buchungsgruppen/' . ($is_new ? 'create' : 'update') } ],
+        checks    => [ 'kivi.validate_form' ],
+        accesskey => 'enter',
+      ],
+
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => 'Buchungsgruppen/delete' } ],
+        confirm  => t8('Do you really want to delete this object?'),
+        disabled => $is_new                  ? t8('This object has not been saved yet.')
+                  : !$self->config->orphaned ? t8('The object is in use and cannot be deleted.')
+                  :                            undef,
+      ],
+
+      link => [
+        t8('Abort'),
+        link => $self->url_for(action => 'list'),
+      ],
+    );
+  }
+  $::request->layout->add_javascripts('kivi.Validator.js');
+}
+
+sub setup_list_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      link => [
+        t8('Add'),
+        link => $self->url_for(action => 'new'),
+      ],
+    );
+  }
+}
+
 1;
diff --git a/SL/Controller/Business.pm b/SL/Controller/Business.pm
deleted file mode 100644 (file)
index f7108bb..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-package SL::Controller::Business;
-
-use strict;
-
-use parent qw(SL::Controller::Base);
-
-use SL::DB::Business;
-use SL::Helper::Flash;
-
-use Rose::Object::MakeMethods::Generic
-(
- scalar => [ qw(business) ],
-);
-
-__PACKAGE__->run_before('check_auth');
-__PACKAGE__->run_before('load_business', only => [ qw(edit update destroy) ]);
-
-#
-# actions
-#
-
-sub action_list {
-  my ($self) = @_;
-
-  $self->render('business/list',
-                title       => $::locale->text('Businesses'),
-                BUSINESSS => SL::DB::Manager::Business->get_all_sorted);
-}
-
-sub action_new {
-  my ($self) = @_;
-
-  $self->{business} = SL::DB::Business->new;
-  $self->render('business/form', title => $::locale->text('Create a new business'));
-}
-
-sub action_edit {
-  my ($self) = @_;
-  $self->render('business/form', title => $::locale->text('Edit business'));
-}
-
-sub action_create {
-  my ($self) = @_;
-
-  $self->{business} = SL::DB::Business->new;
-  $self->create_or_update;
-}
-
-sub action_update {
-  my ($self) = @_;
-  $self->create_or_update;
-}
-
-sub action_destroy {
-  my ($self) = @_;
-
-  if (eval { $self->{business}->delete; 1; }) {
-    flash_later('info',  $::locale->text('The business has been deleted.'));
-  } else {
-    flash_later('error', $::locale->text('The business is in use and cannot be deleted.'));
-  }
-
-  $self->redirect_to(action => 'list');
-}
-
-#
-# filters
-#
-
-sub check_auth {
-  $::auth->assert('config');
-}
-
-#
-# helpers
-#
-
-sub create_or_update {
-  my $self   = shift;
-  my $is_new = !$self->{business}->id;
-  my $params = delete($::form->{business}) || { };
-
-  $self->{business}->assign_attributes(%{ $params });
-
-  my @errors = $self->{business}->validate;
-
-  if (@errors) {
-    flash('error', @errors);
-    $self->render('business/form', title => $is_new ? $::locale->text('Create a new business') : $::locale->text('Edit business'));
-    return;
-  }
-
-  $self->{business}->save;
-
-  flash_later('info', $is_new ? $::locale->text('The business has been created.') : $::locale->text('The business has been saved.'));
-  $self->redirect_to(action => 'list');
-}
-
-sub load_business {
-  my ($self) = @_;
-  $self->{business} = SL::DB::Business->new(id => $::form->{id})->load;
-}
-
-1;
index 624a65b..eb2b473 100644 (file)
@@ -4,6 +4,9 @@ use strict;
 use parent qw(SL::Controller::Base);
 
 use Clone qw(clone);
+use List::UtilsBy qw(partition_by sort_by);
+
+use SL::AM;
 use SL::DB::Chart;
 use SL::Controller::Helper::GetModels;
 use SL::Locale::String qw(t8);
@@ -29,6 +32,7 @@ sub action_ajax_autocomplete {
     if (1 == scalar @{ $exact_matches = SL::DB::Manager::Chart->get_all(
       query => [
         SL::DB::Manager::Chart->type_filter($::form->{filter}{type}),
+        charttype => 'A',
         or => [
           description => { ilike => $::form->{filter}{'all:substr:multi::ilike'} },
           accno       => { ilike => $::form->{filter}{'all:substr:multi::ilike'} },
@@ -54,7 +58,7 @@ sub action_ajax_autocomplete {
 }
 
 sub action_test_page {
-  $_[0]->render('chart/test_page');
+  $_[0]->render('chart/test_page', pre_filled_chart => SL::DB::Manager::Chart->get_first);
 }
 
 sub action_chart_picker_search {
@@ -81,6 +85,42 @@ sub action_show {
   }
 }
 
+sub action_show_report_configuration_overview {
+  my ($self) = @_;
+
+  my @all_charts = sort { $a->accno cmp $b->accno } @{ SL::DB::Manager::Chart->get_all(inject_results => 1) };
+  my @types      = qw(bilanz bwa er eur);
+  my %headings   = (
+    bilanz       => t8('Balance'),
+    bwa          => t8('BWA'),
+    er           => t8('Erfolgsrechnung'),
+    eur          => t8('EUER'),
+  );
+
+  my @data;
+
+  foreach my $type (@types) {
+    my $method = "pos_${type}";
+    my $names  = $type eq 'bwa' ? AM->get_bwa_categories(\%::myconfig, $::form)
+               : $type eq 'eur' ? AM->get_eur_categories(\%::myconfig, $::form)
+               :                  {};
+    my %charts = partition_by { $_->$method // '' } @all_charts;
+    delete $charts{''};
+
+    next if !%charts;
+
+    push @data, {
+      type      => $type,
+      heading   => $headings{$type},
+      charts    => \%charts,
+      positions => [ sort { ($a * 1) <=> ($b * 1) } keys %charts ],
+      names     => $names,
+    };
+  }
+
+  $self->render('chart/report_configuration_overview', DATA => \@data);
+}
+
 sub init_charts {
 
   # disable pagination when hiding chart details = paginate when showing chart details
@@ -108,6 +148,9 @@ sub init_models {
       accno       => t8('Account number'),
       description => t8('Description'),
     },
+    query => [
+      charttype => 'A',
+    ],
   );
 }
 
index ded1a33..bc0f287 100644 (file)
@@ -12,22 +12,31 @@ use SL::DB::Default;
 use SL::DB::Language;
 use SL::DB::Part;
 use SL::DB::Unit;
+use SL::DB::Customer;
 use SL::Helper::Flash;
 use SL::Locale::String qw(t8);
 use SL::PriceSource::ALL;
 use SL::Template;
+use SL::Controller::TopQuickSearch;
+use SL::DB::Helper::AccountingPeriod qw(get_balance_startdate_method_options);
+use SL::Helper::ShippedQty;
+use SL::VATIDNr;
+use SL::ZUGFeRD;
 
 __PACKAGE__->run_before('check_auth');
 
 use Rose::Object::MakeMethods::Generic (
-  'scalar --get_set_init' => [ qw(defaults all_warehouses all_weightunits all_languages all_currencies all_templates all_price_sources h_unit_name
-                                  posting_options payment_options accounting_options inventory_options profit_options balance_startdate_method_options) ],
+  'scalar --get_set_init' => [ qw(defaults all_warehouses all_weightunits all_languages all_currencies all_templates all_price_sources h_unit_name available_quick_search_modules available_shipped_qty_item_identity_fields
+                                  all_project_statuses all_project_types zugferd_settings
+                                  posting_options payment_options accounting_options inventory_options profit_options balance_startdate_method_options
+                                  displayable_name_specs_by_module) ],
 );
 
 sub action_edit {
   my ($self, %params) = @_;
 
   $::form->{use_templates} = $self->defaults->templates ? 'existing' : 'new';
+  $::form->{feature_datev} = $self->defaults->feature_datev;
   $self->edit_form;
 }
 
@@ -92,6 +101,11 @@ sub action_save {
     }
   }
 
+  my $cleaned_ustid = SL::VATIDNr->clean($defaults->{co_ustid});
+  if ($cleaned_ustid && !SL::VATIDNr->validate($cleaned_ustid)) {
+    push @errors, t8("The VAT ID number '#1' is invalid.", $defaults->{co_ustid});
+  }
+
   # Show form again if there were any errors. Nothing's been changed
   # yet in the database.
   if (@errors) {
@@ -127,6 +141,11 @@ sub action_save {
     $self->defaults->templates('templates/' . $::form->{new_templates});
   }
 
+  # Displayable name preferences
+  foreach my $specs (@{ $::form->{displayable_name_specs} }) {
+    $self->displayable_name_specs_by_module->{$specs->{module}}->{prefs}->store_default($specs->{default});
+  }
+
   # Finally save defaults.
   $self->defaults->save;
 
@@ -145,7 +164,10 @@ sub init_all_languages   { SL::DB::Manager::Language->get_all_sorted
 sub init_all_currencies  { SL::DB::Manager::Currency->get_all_sorted                                                     }
 sub init_all_weightunits { my $unit = SL::DB::Manager::Unit->find_by(name => 'kg'); $unit ? $unit->convertible_units : [] }
 sub init_all_templates   { +{ SL::Template->available_templates }                                                        }
-sub init_h_unit_name     { first { SL::DB::Manager::Unit->find_by(name => $_) } qw(Std h Stunde)                         };
+sub init_h_unit_name     { first { SL::DB::Manager::Unit->find_by(name => $_) } qw(Std h Stunde)                         }
+sub init_all_project_types    { SL::DB::Manager::ProjectType->get_all_sorted                                             }
+sub init_all_project_statuses { SL::DB::Manager::ProjectStatus->get_all_sorted                                           }
+sub init_zugferd_settings     { \@SL::ZUGFeRD::customer_settings                                                         }
 
 sub init_posting_options {
   [ { title => t8("never"),           value => 0           },
@@ -175,11 +197,7 @@ sub init_profit_options {
 }
 
 sub init_balance_startdate_method_options {
-  [ { title => t8("After closed period"),                       value => "closed_to"                   },
-    { title => t8("Start of year"),                             value => "start_of_year"               },
-    { title => t8("All transactions"),                          value => "all_transactions"            },
-    { title => t8("Last opening balance or all transactions"),  value => "last_ob_or_all_transactions" },
-    { title => t8("Last opening balance or start of year"),     value => "last_ob_or_start_of_year"    }, ]
+  return SL::DB::Helper::AccountingPeriod::get_balance_startdate_method_options;
 }
 
 sub init_all_price_sources {
@@ -188,6 +206,31 @@ sub init_all_price_sources {
   [ map { [ $_->name, $_->description ] } @classes ];
 }
 
+sub init_available_quick_search_modules {
+  [ SL::Controller::TopQuickSearch->new->available_modules ];
+}
+
+sub init_available_shipped_qty_item_identity_fields {
+  [ SL::Helper::ShippedQty->new->available_item_identity_fields ];
+}
+
+sub init_displayable_name_specs_by_module {
+  +{
+     'SL::DB::Customer' => {
+       specs => SL::DB::Customer->displayable_name_specs,
+       prefs => SL::DB::Customer->displayable_name_prefs,
+     },
+     'SL::DB::Vendor' => {
+       specs => SL::DB::Vendor->displayable_name_specs,
+       prefs => SL::DB::Vendor->displayable_name_prefs,
+     },
+     'SL::DB::Part' => {
+       specs => SL::DB::Part->displayable_name_specs,
+       prefs => SL::DB::Part->displayable_name_prefs,
+     },
+  };
+}
+
 #
 # filters
 #
@@ -203,12 +246,27 @@ sub check_auth {
 sub edit_form {
   my ($self) = @_;
 
-  $::request->layout->use_javascript("${_}.js") for qw(jquery.selectboxes jquery.multiselect2side);
+  $::request->layout->use_javascript("${_}.js") for qw(jquery.selectboxes jquery.multiselect2side kivi.File ckeditor/ckeditor ckeditor/adapters/jquery);
 
+  $self->setup_edit_form_action_bar;
   $self->render('client_config/form', title => t8('Client Configuration'),
                 make_chart_title     => sub { $_[0]->accno . '--' . $_[0]->description },
                 make_templates_value => sub { 'templates/' . $_[0] },
               );
 }
 
+sub setup_edit_form_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => 'ClientConfig/save' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
 1;
index b74b04b..d1ff719 100644 (file)
@@ -2,14 +2,18 @@ package SL::Controller::CsvImport;
 
 use strict;
 
+use SL::DB;
 use SL::DB::Buchungsgruppe;
 use SL::DB::CsvImportProfile;
 use SL::DB::CsvImportReport;
 use SL::DB::Unit;
 use SL::DB::Helper::Paginated ();
+use SL::DBUtils qw(do_statement);
 use SL::Helper::Flash;
 use SL::Locale::String;
 use SL::SessionFile;
+use SL::SessionFile::Random;
+use SL::Controller::CsvImport::AdditionalBillingAddress;
 use SL::Controller::CsvImport::Contact;
 use SL::Controller::CsvImport::CustomerVendor;
 use SL::Controller::CsvImport::Part;
@@ -17,28 +21,30 @@ use SL::Controller::CsvImport::Inventory;
 use SL::Controller::CsvImport::Shipto;
 use SL::Controller::CsvImport::Project;
 use SL::Controller::CsvImport::Order;
+use SL::Controller::CsvImport::DeliveryOrder;
+use SL::Controller::CsvImport::ARTransaction;
 use SL::JSON;
 use SL::Controller::CsvImport::BankTransaction;
 use SL::BackgroundJob::CsvImport;
 use SL::System::TaskServer;
 
-use List::MoreUtils qw(none);
+use List::MoreUtils qw(any none);
 use List::Util qw(min);
 
 use parent qw(SL::Controller::Base);
 
 use Rose::Object::MakeMethods::Generic
 (
- scalar                  => [ qw(type profile file all_profiles all_charsets sep_char all_sep_chars quote_char all_quote_chars escape_char all_escape_chars all_buchungsgruppen all_units
-                                 import_status errors headers raw_data_headers info_headers data num_imported num_importable displayable_columns file all_taxzones) ],
- 'scalar --get_set_init' => [ qw(worker task_server) ],
+ scalar                  => [ qw(type profile all_profiles all_charsets sep_char all_sep_chars quote_char all_quote_chars escape_char all_escape_chars all_buchungsgruppen all_units
+                                 import_status errors headers raw_data_headers info_headers data num_importable displayable_columns file all_taxzones) ],
+ 'scalar --get_set_init' => [ qw(worker task_server num_imported mappings) ],
  'array'                 => [
    progress_tracker     => { },
    add_progress_tracker => {  interface => 'add', hash_key => 'progress_tracker' },
  ],
 );
 
-__PACKAGE__->run_before('check_auth');
+__PACKAGE__->run_before('check_auth', except => [ qw(report) ]);
 __PACKAGE__->run_before('ensure_form_structure');
 __PACKAGE__->run_before('check_type', except => [ qw(report) ]);
 __PACKAGE__->run_before('load_all_profiles');
@@ -96,11 +102,10 @@ sub action_result {
   my $data = $self->{background_job}->data_as_hash;
 
   my $profile = SL::DB::Manager::CsvImportProfile->find_by(id => $data->{profile_id});
-
   $self->profile($profile);
 
   if ($data->{errors} and my $first_error =  $data->{errors}->[0]) {
-    flash('error', $::locale->text('There was an error parsing the csv file: #1 in line #2.', $first_error->[2], $first_error->[0]));
+    flash('error', $::locale->text('There was an error parsing the csv file: #1 in line #2.', $first_error->[2], $first_error->[4]));
   }
 
   if ($data->{progress}{finished} || $data->{errors}) {
@@ -153,78 +158,157 @@ sub action_download_sample {
 sub action_report {
   my ($self, %params) = @_;
 
-  my $report_id = $params{report_id} || $::form->{id};
-
-  $self->{report}      = SL::DB::Manager::CsvImportReport->find_by(id => $report_id);
+  my $report_id   = $params{report_id} || $::form->{id};
+  $self->{report} = SL::DB::Manager::CsvImportReport->find_by(id => $report_id);
 
   if (!$self->{report}) {
     $::form->error(t8('No report with id #1', $report_id));
   }
-  my $num_rows         = $self->{report}->numrows;
-  my $num_cols         = SL::DB::Manager::CsvImportReportRow->get_all_count(query => [ csv_import_report_id => $report_id, row => 0 ]);
+
+  my $show_info_err = ($self->{report}->profile->get('full_preview', 0) == 1);
+  my $show_first_20 = ($self->{report}->profile->get('full_preview', 0) == 2);
+
+  my $num_rows = 0;
+  if ($show_first_20) {
+    $num_rows  = min($self->{report}->numrows, 20);
+  } elsif ($show_info_err) {
+    # count each status row only once
+    $num_rows  = SL::DB::Manager::CsvImportReportStatus->get_all_count(query    => [csv_import_report_id => $report_id],
+                                                                       select   => ['row'],
+                                                                       distinct => 1,);
+  } else {
+    # show all
+    $num_rows  = $self->{report}->numrows;
+  }
 
   # manual paginating, yuck
-  my $page = $::form->{page} || 1;
-  my $pages = {};
-  $pages->{per_page}        = $::form->{per_page} || 20;
-  $pages->{max}             = SL::DB::Helper::Paginated::ceil($num_rows, $pages->{per_page}) || 1;
-  $pages->{page}             = $page < 1 ? 1
-                            : $page > $pages->{max} ? $pages->{max}
-                            : $page;
-  $pages->{common}          = [ grep { $_->{visible} } @{ SL::DB::Helper::Paginated::make_common_pages($pages->{page}, $pages->{max}) } ];
+  my $page                   = $::form->{page} || 1;
+  my $pages                  = {};
+  $pages->{per_page}         = $::form->{per_page} || 20;
+  $pages->{max}              = SL::DB::Helper::Paginated::ceil($num_rows, $pages->{per_page}) || 1;
+  $pages->{page}             = $page < 1             ? 1
+                             : $page > $pages->{max} ? $pages->{max}
+                             : $                       page;
+  $pages->{common}           = [ grep { $_->{visible} } @{ SL::DB::Helper::Paginated::make_common_pages($pages->{page}, $pages->{max}) } ];
 
   $self->{report_numheaders} = $self->{report}->numheaders;
-  my $first_row_header = 0;
-  my $last_row_header  = $self->{report_numheaders} - 1;
-  my $first_row_data   = $pages->{per_page} * ($pages->{page}-1) + $self->{report_numheaders};
-  my $last_row_data    = min($pages->{per_page} * $pages->{page}, $num_rows) + $self->{report_numheaders} - 1;
-  $self->{display_rows} = [
-    $first_row_header
-      ..
-    $last_row_header,
-    $first_row_data
-      ..
-    $last_row_data
-  ];
+  my $first_row_header       = 0;
+  my $last_row_header        = $self->{report_numheaders} - 1;
+  my $first_row_data         = $pages->{per_page} * ($pages->{page}-1) + $self->{report_numheaders};
+  my $last_row_data          = min($pages->{per_page} * $pages->{page}, $num_rows) + $self->{report_numheaders} - 1;
+
+
+  $self->{display_rows} = [];
+  if ($show_info_err) {
+    my $limit    = $last_row_data  - $first_row_data + 1;
+    my $offset   = $first_row_data - $self->{report_numheaders};
+    my @err_rows = map { $_->row } @{SL::DB::Manager::CsvImportReportStatus->get_all(query    => [csv_import_report_id => $report_id],
+                                                                                     distinct => 1,
+                                                                                     select   => ['row'],
+                                                                                     limit    => $limit,
+                                                                                     offset   => $offset,
+                                                                                     sort_by  => 'row')};
+    $self->{display_rows} = [ $first_row_header .. $last_row_header,
+                              @err_rows ];
+
+  } else {
+
+    $self->{display_rows} = [ $first_row_header .. $last_row_header,
+                              $first_row_data   .. $last_row_data ];
+  }
 
   my @query = (
+    row                  => $self->{display_rows},
     csv_import_report_id => $report_id,
-    or => [
-      and => [
-        row => { ge => $first_row_header },
-        row => { le => $last_row_header },
-      ],
-      and => [
-        row => { ge => $first_row_data },
-        row => { le => $last_row_data },
-      ]
-    ]
   );
 
-  my $rows             = SL::DB::Manager::CsvImportReportRow->get_all(query => \@query);
-  my $status           = SL::DB::Manager::CsvImportReportStatus->get_all(query => \@query);
+  my $rows               = SL::DB::Manager::CsvImportReportRow   ->get_all(query => \@query, sort_by => 'row');
+  my $status             = SL::DB::Manager::CsvImportReportStatus->get_all(query => \@query, sort_by => 'row');
+  $self->{num_errors}    = SL::DB::Manager::CsvImportReportStatus->get_all_count(query => [csv_import_report_id => $report_id, type => 'errors']);
 
   $self->{report_rows}   = $self->{report}->folded_rows(rows => $rows);
   $self->{report_status} = $self->{report}->folded_status(status => $status);
-  $self->{pages} = $pages;
-  $self->{base_url} = $self->url_for(action => 'report', id => $report_id, no_layout => $params{no_layout} || $::form->{no_layout} );
+  $self->{pages}         = $pages;
+  $self->{base_url}      = $self->url_for(action => 'report', id => $report_id, no_layout => $params{no_layout} || $::form->{no_layout} );
 
   $self->render('csv_import/report', { layout => !($params{no_layout} || $::form->{no_layout}) });
 }
 
+sub action_add_empty_mapping_line {
+  my ($self) = @_;
+
+  $self->profile_from_form;
+  $self->setup_help;
+
+  $self->js
+    ->append('#csv_import_mappings', $self->render('csv_import/_mapping_item', { layout => 0, output => 0 }))
+    ->hide('#mapping_empty')
+    ->render;
+}
+
+sub action_add_mapping_from_upload {
+  my ($self) = @_;
+
+  if ($::form->{tmp_profile_id}) {
+    $self->profile_from_form(SL::DB::CsvImportProfile->new(id => $::form->{tmp_profile_id})->load);
+  } else {
+    $self->profile_from_form;
+  }
+  $self->setup_help;
+
+  my $file_name;
+  if ($self->profile->get('file_name')) {
+    $file_name = $self->profile->get('file_name');
+  } else {
+    $self->js
+      ->flash('error', t8('No file has been uploaded yet.'))
+      ->render;
+    return;
+  }
+
+  my $file = SL::SessionFile->new($file_name, mode => '<', encoding => $self->profile->get('charset'));
+  if (!$file->fh) {
+    $self->js
+      ->flash('error', t8('No file has been uploaded yet.'))
+      ->render;
+    return;
+  }
+
+  my $csv = SL::Helper::Csv->new(
+    file => $file->file_name,
+    map { $_ => $self->profile->get($_) } qw(sep_char escape_char quote_char),
+  );
+
+  $csv->_open_file;
+  my $header = $csv->check_header;
+
+  for my $field (@$header) {
+    next if $self->mappings_for_profile->{$field};
+    $self->js->append(
+      '#csv_import_mappings',
+      $self->render('csv_import/_mapping_item', { layout => 0, output => 0 }, item => { from => $field }),
+    );
+  }
+
+  $self->js
+    ->hide('#mapping_empty')
+    ->render;
+}
+
 
 #
 # filters
 #
 
 sub check_auth {
-  $::auth->assert('config');
+  $_[0]->check_type;
+  $_[0]->worker->check_auth;
 }
 
 sub check_type {
   my ($self) = @_;
 
-  die "Invalid CSV import type" if none { $_ eq $::form->{profile}->{type} } qw(parts inventories customers_vendors addresses contacts projects orders bank_transactions);
+  die "Invalid CSV import type" if none { $_ eq $::form->{profile}->{type} } qw(parts inventories customers_vendors billing_addresses addresses contacts projects orders delivery_orders bank_transactions ar_transactions);
   $self->type($::form->{profile}->{type});
 }
 
@@ -260,19 +344,24 @@ sub render_inputs {
     $self->$sub(($char_map{$type}->{$char} || [])->[0] || $char);
   }
 
-  $self->file(SL::SessionFile->new($self->csv_file_name));
+  if ($self->profile->get('file_name')) {
+    $self->file(SL::SessionFile->new($self->profile->get('file_name')));
+  }
 
   my $title = $self->type eq 'customers_vendors' ? $::locale->text('CSV import: customers and vendors')
+            : $self->type eq 'billing_addresses' ? $::locale->text('CSV import: additional billing addresses')
             : $self->type eq 'addresses'         ? $::locale->text('CSV import: shipping addresses')
             : $self->type eq 'contacts'          ? $::locale->text('CSV import: contacts')
             : $self->type eq 'parts'             ? $::locale->text('CSV import: parts and services')
             : $self->type eq 'inventories'       ? $::locale->text('CSV import: inventories')
             : $self->type eq 'projects'          ? $::locale->text('CSV import: projects')
             : $self->type eq 'orders'            ? $::locale->text('CSV import: orders')
+            : $self->type eq 'delivery_orders'   ? $::locale->text('CSV import: delivery orders')
             : $self->type eq 'bank_transactions' ? $::locale->text('CSV import: bank transactions')
+            : $self->type eq 'ar_transactions'   ? $::locale->text('CSV import: ar transactions')
             : die;
 
-  if ($self->{type} eq 'customers_vendors' or $self->{type} eq 'orders'  ) {
+  if ( any { $_ eq $self->{type} } qw(customers_vendors orders delivery_orders ar_transactions) ) {
     $self->all_taxzones(SL::DB::Manager::TaxZone->get_all_sorted(query => [ obsolete => 0 ]));
   };
 
@@ -283,33 +372,48 @@ sub render_inputs {
 
   $self->setup_help;
 
+  $self->setup_render_inputs_action_bar;
+
   $self->render('csv_import/form', title => $title);
 }
 
 sub test_and_import_deferred {
   my ($self, %params) = @_;
 
-  if ( $::form->{force_profile} && $::form->{profile}->{id} ) {
+  if ( $::form->{force_profile} && ($::form->{tmp_profile_id} || $::form->{profile}->{id}) ) {
+    $::form->{profile}->{id} = $::form->{tmp_profile_id} if $::form->{tmp_profile_id};
     $self->load_default_profile;
-  }  else {
+  } elsif ($::form->{tmp_profile_id}) {
+    $self->profile_from_form(SL::DB::CsvImportProfile->new(id => $::form->{tmp_profile_id})->load);
+  } else {
     $self->profile_from_form;
   };
 
+  my $file_name;
   if ($::form->{file}) {
-    my $file = SL::SessionFile->new($self->csv_file_name, mode => '>');
+    my $file = SL::SessionFile::Random->new(mode => '>');
     $file->fh->print($::form->{file});
     $file->fh->close;
+    $file_name = $file->file_name;
+    $self->profile->set('file_name', $file_name);
+  } elsif ($self->profile->get('file_name')) {
+    $file_name = $self->profile->get('file_name');
+  } else {
+    flash('error', $::locale->text('No file has been uploaded yet.'));
+    return $self->action_new;
   }
 
-  my $file = SL::SessionFile->new($self->csv_file_name, mode => '<', encoding => $self->profile->get('charset'));
+  my $file = SL::SessionFile->new($file_name, mode => '<', encoding => $self->profile->get('charset'));
   if (!$file->fh) {
     flash('error', $::locale->text('No file has been uploaded yet.'));
     return $self->action_new;
   }
 
+  # save tempory profile
+  $self->profile($self->profile->clone_and_reset_deep)->save;
+
   $self->{background_job} = SL::BackgroundJob::CsvImport->create_job(
-    file        => $self->csv_file_name,
-    profile     => $self->profile,
+    profile_id  => $self->profile->id,
     type        => $self->profile->type,
     test        => $params{test},
     employee_id => SL::DB::Manager::Employee->current->id,
@@ -336,7 +440,7 @@ sub test_and_import {
   my ($self, %params) = @_;
 
   my $file = SL::SessionFile->new(
-    $self->csv_file_name,
+    $self->profile->get('file_name'),
     mode       => '<',
     encoding   => $self->profile->get('charset'),
     session_id => $params{session_id}
@@ -364,12 +468,13 @@ sub load_default_profile {
 
   my $profile;
   if ($::form->{profile}->{id}) {
-    $profile = SL::DB::Manager::CsvImportProfile->find_by(id => $::form->{profile}->{id}, login => $::myconfig{login});
+    $profile = SL::DB::Manager::CsvImportProfile->find_by(id => $::form->{profile}->{id});
   }
   $profile ||= SL::DB::Manager::CsvImportProfile->find_by(type => $self->{type}, is_default => 1, login => $::myconfig{login});
   $profile ||= SL::DB::CsvImportProfile->new(type => $self->{type}, login => $::myconfig{login});
 
   $self->profile($profile);
+  $self->mappings(SL::JSON::from_json($self->profile->get('json_mappings'))) if $self->profile->get('json_mappings');
   $self->worker->set_profile_defaults;
   $self->profile->set_defaults;
 }
@@ -405,15 +510,19 @@ sub profile_from_form {
     $::form->{settings}->{sellprice_adjustment} = $::form->parse_amount(\%::myconfig, $::form->{settings}->{sellprice_adjustment});
   }
 
-  if ($self->type eq 'orders') {
+  if ($self->type eq 'orders' or $self->{type} eq 'ar_transactions') {
     $::form->{settings}->{max_amount_diff} = $::form->parse_amount(\%::myconfig, $::form->{settings}->{max_amount_diff});
   }
 
-  delete $::form->{profile}->{id};
   $self->profile($existing_profile || SL::DB::CsvImportProfile->new(login => $::myconfig{login}));
   $self->profile->assign_attributes(%{ $::form->{profile} });
+
+  # save settings for file_name, as this is not in form, but maybe in existing_profile
+  push @settings, { key => 'file_name', value => $self->profile->get('file_name') } if $self->profile->get('file_name');
+
   $self->profile->settings(map({ { key => $_, value => $::form->{settings}->{$_} } } keys %{ $::form->{settings} }),
                            @settings);
+  $self->profile->set('json_mappings', JSON::to_json($self->mappings));
   $self->profile->set_defaults;
 }
 
@@ -447,67 +556,66 @@ sub save_report_single {
 
   $self->track_progress(phase => 'building report', progress => 0);
 
-  my $clone_profile = $self->profile->clone_and_reset_deep;
-  $clone_profile->save; # weird bug. if this isn't saved before adding it to the report, it will default back to the last profile.
-
   my $report = SL::DB::CsvImportReport->new(
     session_id => $params{session_id},
-    profile    => $clone_profile,
+    profile_id => $self->profile->id,
     type       => $self->type,
     file       => '',
     numrows    => scalar @{ $self->data },
     numheaders => 1,
+    test_mode  => $params{test} ? 1 : 0,
   );
 
   $report->save(cascade => 1) or die $report->db->error;
 
-  my $dbh = $::form->get_standard_dbh;
-  $dbh->begin_work;
+  SL::DB->client->with_transaction(sub {
+    my $dbh = SL::DB->client->dbh;
 
-  my $query  = 'INSERT INTO csv_import_report_rows (csv_import_report_id, col, row, value) VALUES (?, ?, ?, ?)';
-  my $query2 = 'INSERT INTO csv_import_report_status (csv_import_report_id, row, type, value) VALUES (?, ?, ?, ?)';
+    my $query  = 'INSERT INTO csv_import_report_rows (csv_import_report_id, col, row, value) VALUES (?, ?, ?, ?)';
+    my $query2 = 'INSERT INTO csv_import_report_status (csv_import_report_id, row, type, value) VALUES (?, ?, ?, ?)';
 
-  my $sth = $dbh->prepare($query);
-  my $sth2 = $dbh->prepare($query2);
+    my $sth = $dbh->prepare($query);
+    my $sth2 = $dbh->prepare($query2);
 
-  # save headers
-  my (@headers, @info_methods, @raw_methods, @methods);
+    # save headers
+    my (@headers, @info_methods, @raw_methods, @methods);
 
-  for my $i (0 .. $#{ $self->info_headers->{headers} }) {
-    next unless         $self->info_headers->{used}->{ $self->info_headers->{methods}->[$i] };
-    push @headers,      $self->info_headers->{headers}->[$i];
-    push @info_methods, $self->info_headers->{methods}->[$i];
-  }
-  for my $i (0 .. $#{ $self->headers->{headers} }) {
-    next unless         $self->headers->{used}->{ $self->headers->{headers}->[$i] };
-    push @headers,      $self->headers->{headers}->[$i];
-    push @methods,      $self->headers->{methods}->[$i];
-  }
-  for my $i (0 .. $#{ $self->raw_data_headers->{headers} }) {
-    next unless         $self->raw_data_headers->{used}->{ $self->raw_data_headers->{headers}->[$i] };
-    push @headers,      $self->raw_data_headers->{headers}->[$i];
-    push @raw_methods,  $self->raw_data_headers->{headers}->[$i];
-  }
-
-  $sth->execute($report->id, $_, 0, $headers[$_]) for 0 .. $#headers;
+    for my $i (0 .. $#{ $self->info_headers->{headers} }) {
+      next unless         $self->info_headers->{used}->{ $self->info_headers->{methods}->[$i] };
+      push @headers,      $self->info_headers->{headers}->[$i];
+      push @info_methods, $self->info_headers->{methods}->[$i];
+    }
+    for my $i (0 .. $#{ $self->headers->{headers} }) {
+      next unless         $self->headers->{used}->{ $self->headers->{headers}->[$i] };
+      push @headers,      $self->headers->{headers}->[$i];
+      push @methods,      $self->headers->{methods}->[$i];
+    }
+    for my $i (0 .. $#{ $self->raw_data_headers->{headers} }) {
+      next unless         $self->raw_data_headers->{used}->{ $self->raw_data_headers->{headers}->[$i] };
+      push @headers,      $self->raw_data_headers->{headers}->[$i];
+      push @raw_methods,  $self->raw_data_headers->{headers}->[$i];
+    }
 
-  # col offsets
-  my $o1 =       @info_methods;
-  my $o2 = $o1 + @methods;
+    do_statement($::form, $sth, $query, $report->id, $_, 0, $headers[$_]) for 0 .. $#headers;
 
-  for my $row (0 .. $#{ $self->data }) {
-    $self->track_progress(progress => $row / @{ $self->data } * 100) if $row % 1000 == 0;
-    my $data_row = $self->{data}[$row];
+    # col offsets
+    my $o1 =       @info_methods;
+    my $o2 = $o1 + @methods;
 
-    $sth->execute($report->id,       $_, $row + 1, $data_row->{info_data}{ $info_methods[$_] }) for 0 .. $#info_methods;
-    $sth->execute($report->id, $o1 + $_, $row + 1, $data_row->{object}->${ \ $methods[$_] })    for 0 .. $#methods;
-    $sth->execute($report->id, $o2 + $_, $row + 1, $data_row->{raw_data}{ $raw_methods[$_] })   for 0 .. $#raw_methods;
+    for my $row (0 .. $#{ $self->data }) {
+      $self->track_progress(progress => $row / @{ $self->data } * 100) if $row % 1000 == 0;
+      my $data_row = $self->{data}[$row];
 
-    $sth2->execute($report->id, $row + 1, 'information', $_) for @{ $data_row->{information} || [] };
-    $sth2->execute($report->id, $row + 1, 'errors', $_)      for @{ $data_row->{errors}      || [] };
-  }
+      my $object = $data_row->{object_to_save} || $data_row->{object};
+      do_statement($::form, $sth, $query, $report->id,       $_, $row + 1, $data_row->{info_data}{ $info_methods[$_] }) for 0 .. $#info_methods;
+      do_statement($::form, $sth, $query, $report->id, $o1 + $_, $row + 1, $object->${ \ $methods[$_] })                for 0 .. $#methods;
+      do_statement($::form, $sth, $query, $report->id, $o2 + $_, $row + 1, $data_row->{raw_data}{ $raw_methods[$_] })   for 0 .. $#raw_methods;
 
-  $dbh->commit;
+      do_statement($::form, $sth2, $query2, $report->id, $row + 1, 'information', $_) for @{ $data_row->{information} || [] };
+      do_statement($::form, $sth2, $query2, $report->id, $row + 1, 'errors', $_)      for @{ $data_row->{errors}      || [] };
+    }
+    1;
+  }) or do { die SL::DB->client->error };
 
   return $report->id;
 }
@@ -517,98 +625,92 @@ sub save_report_multi {
 
   $self->track_progress(phase => 'building report', progress => 0);
 
-  my $clone_profile = $self->profile->clone_and_reset_deep;
-  $clone_profile->save; # weird bug. if this isn't saved before adding it to the report, it will default back to the last profile.
-
   my $report = SL::DB::CsvImportReport->new(
     session_id => $params{session_id},
-    profile    => $clone_profile,
+    profile_id => $self->profile->id,
     type       => $self->type,
     file       => '',
     numrows    => scalar @{ $self->data },
     numheaders => scalar @{ $self->worker->profile },
+    test_mode  => $params{test} ? 1 : 0,
   );
 
   $report->save(cascade => 1) or die $report->db->error;
 
-  my $dbh = $::form->get_standard_dbh;
-  $dbh->begin_work;
+  SL::DB->client->with_transaction(sub {
+    my $dbh = SL::DB->client->dbh;
 
-  my $query  = 'INSERT INTO csv_import_report_rows (csv_import_report_id, col, row, value) VALUES (?, ?, ?, ?)';
-  my $query2 = 'INSERT INTO csv_import_report_status (csv_import_report_id, row, type, value) VALUES (?, ?, ?, ?)';
+    my $query  = 'INSERT INTO csv_import_report_rows (csv_import_report_id, col, row, value) VALUES (?, ?, ?, ?)';
+    my $query2 = 'INSERT INTO csv_import_report_status (csv_import_report_id, row, type, value) VALUES (?, ?, ?, ?)';
 
-  my $sth = $dbh->prepare($query);
-  my $sth2 = $dbh->prepare($query2);
+    my $sth = $dbh->prepare($query);
+    my $sth2 = $dbh->prepare($query2);
 
-  # save headers
-  my ($headers, $info_methods, $raw_methods, $methods);
+    # save headers
+    my ($headers, $info_methods, $raw_methods, $methods);
 
-  for my $i (0 .. $#{ $self->worker->profile }) {
-    my $row_ident = $self->worker->profile->[$i]->{row_ident};
+    for my $i (0 .. $#{ $self->worker->profile }) {
+      my $row_ident = $self->worker->profile->[$i]->{row_ident};
 
-    for my $i (0 .. $#{ $self->info_headers->{$row_ident}->{headers} }) {
-      next unless                            $self->info_headers->{$row_ident}->{used}->{ $self->info_headers->{$row_ident}->{methods}->[$i] };
-      push @{ $headers->{$row_ident} },      $self->info_headers->{$row_ident}->{headers}->[$i];
-      push @{ $info_methods->{$row_ident} }, $self->info_headers->{$row_ident}->{methods}->[$i];
-    }
-    for my $i (0 .. $#{ $self->headers->{$row_ident}->{headers} }) {
-      next unless                       $self->headers->{$row_ident}->{used}->{ $self->headers->{$row_ident}->{headers}->[$i] };
-      push @{ $headers->{$row_ident} }, $self->headers->{$row_ident}->{headers}->[$i];
-      push @{ $methods->{$row_ident} }, $self->headers->{$row_ident}->{methods}->[$i];
-    }
+      for my $i (0 .. $#{ $self->info_headers->{$row_ident}->{headers} }) {
+        next unless                            $self->info_headers->{$row_ident}->{used}->{ $self->info_headers->{$row_ident}->{methods}->[$i] };
+        push @{ $headers->{$row_ident} },      $self->info_headers->{$row_ident}->{headers}->[$i];
+        push @{ $info_methods->{$row_ident} }, $self->info_headers->{$row_ident}->{methods}->[$i];
+      }
+      for my $i (0 .. $#{ $self->headers->{$row_ident}->{headers} }) {
+        next unless                       $self->headers->{$row_ident}->{used}->{ $self->headers->{$row_ident}->{headers}->[$i] };
+        push @{ $headers->{$row_ident} }, $self->headers->{$row_ident}->{headers}->[$i];
+        push @{ $methods->{$row_ident} }, $self->headers->{$row_ident}->{methods}->[$i];
+      }
 
-    for my $i (0 .. $#{ $self->raw_data_headers->{$row_ident}->{headers} }) {
-    next unless                           $self->raw_data_headers->{$row_ident}->{used}->{ $self->raw_data_headers->{$row_ident}->{headers}->[$i] };
-    push @{ $headers->{$row_ident} },     $self->raw_data_headers->{$row_ident}->{headers}->[$i];
-    push @{ $raw_methods->{$row_ident} }, $self->raw_data_headers->{$row_ident}->{headers}->[$i];
-  }
-
-  }
+      for my $i (0 .. $#{ $self->raw_data_headers->{$row_ident}->{headers} }) {
+      next unless                           $self->raw_data_headers->{$row_ident}->{used}->{ $self->raw_data_headers->{$row_ident}->{headers}->[$i] };
+      push @{ $headers->{$row_ident} },     $self->raw_data_headers->{$row_ident}->{headers}->[$i];
+      push @{ $raw_methods->{$row_ident} }, $self->raw_data_headers->{$row_ident}->{headers}->[$i];
+    }
 
-  for my $i (0 .. $#{ $self->worker->profile }) {
-    my $row_ident = $self->worker->profile->[$i]->{row_ident};
-    $sth->execute($report->id, $_, $i, $headers->{$row_ident}->[$_]) for 0 .. $#{ $headers->{$row_ident} };
-  }
+    }
 
-  # col offsets
-  my ($off1, $off2);
-  for my $i (0 .. $#{ $self->worker->profile }) {
-    my $row_ident = $self->worker->profile->[$i]->{row_ident};
-    my $n_info_methods = $info_methods->{$row_ident} ? scalar @{ $info_methods->{$row_ident} } : 0;
-    my $n_methods      = $methods->{$row_ident} ?      scalar @{ $methods->{$row_ident} }      : 0;
+    for my $i (0 .. $#{ $self->worker->profile }) {
+      my $row_ident = $self->worker->profile->[$i]->{row_ident};
+      do_statement($::form, $sth, $query, $report->id, $_, $i, $headers->{$row_ident}->[$_]) for 0 .. $#{ $headers->{$row_ident} };
+    }
 
-    $off1->{$row_ident} = $n_info_methods;
-    $off2->{$row_ident} = $off1->{$row_ident} + $n_methods;
-  }
+    # col offsets
+    my ($off1, $off2);
+    for my $i (0 .. $#{ $self->worker->profile }) {
+      my $row_ident = $self->worker->profile->[$i]->{row_ident};
+      my $n_info_methods = $info_methods->{$row_ident} ? scalar @{ $info_methods->{$row_ident} } : 0;
+      my $n_methods      = $methods->{$row_ident} ?      scalar @{ $methods->{$row_ident} }      : 0;
 
-  my $n_header_rows = scalar @{ $self->worker->profile };
+      $off1->{$row_ident} = $n_info_methods;
+      $off2->{$row_ident} = $off1->{$row_ident} + $n_methods;
+    }
 
-  for my $row (0 .. $#{ $self->data }) {
-    $self->track_progress(progress => $row / @{ $self->data } * 100) if $row % 1000 == 0;
-    my $data_row = $self->{data}[$row];
-    my $row_ident = $data_row->{raw_data}{datatype};
+    my $n_header_rows = scalar @{ $self->worker->profile };
 
-    my $o1 = $off1->{$row_ident};
-    my $o2 = $off2->{$row_ident};
+    for my $row (0 .. $#{ $self->data }) {
+      $self->track_progress(progress => $row / @{ $self->data } * 100) if $row % 1000 == 0;
+      my $data_row = $self->{data}[$row];
+      my $row_ident = $data_row->{raw_data}{datatype};
 
-    $sth->execute($report->id,       $_, $row + $n_header_rows, $data_row->{info_data}{ $info_methods->{$row_ident}->[$_] }) for 0 .. $#{ $info_methods->{$row_ident} };
-    $sth->execute($report->id, $o1 + $_, $row + $n_header_rows, $data_row->{object}->${ \ $methods->{$row_ident}->[$_] })    for 0 .. $#{ $methods->{$row_ident} };
-    $sth->execute($report->id, $o2 + $_, $row + $n_header_rows, $data_row->{raw_data}{ $raw_methods->{$row_ident}->[$_] })   for 0 .. $#{ $raw_methods->{$row_ident} };
+      my $o1 = $off1->{$row_ident};
+      my $o2 = $off2->{$row_ident};
 
-    $sth2->execute($report->id, $row + $n_header_rows, 'information', $_) for @{ $data_row->{information} || [] };
-    $sth2->execute($report->id, $row + $n_header_rows, 'errors', $_)      for @{ $data_row->{errors}      || [] };
-  }
+      my $object = $data_row->{object_to_save} || $data_row->{object};
+      do_statement($::form, $sth, $query, $report->id,       $_, $row + $n_header_rows, $data_row->{info_data}{ $info_methods->{$row_ident}->[$_] }) for 0 .. $#{ $info_methods->{$row_ident} };
+      do_statement($::form, $sth, $query, $report->id, $o1 + $_, $row + $n_header_rows, $object->${ \ $methods->{$row_ident}->[$_] })                for 0 .. $#{ $methods->{$row_ident} };
+      do_statement($::form, $sth, $query, $report->id, $o2 + $_, $row + $n_header_rows, $data_row->{raw_data}{ $raw_methods->{$row_ident}->[$_] })   for 0 .. $#{ $raw_methods->{$row_ident} };
 
-  $dbh->commit;
+      do_statement($::form, $sth2, $query2, $report->id, $row + $n_header_rows, 'information', $_) for @{ $data_row->{information} || [] };
+      do_statement($::form, $sth2, $query2, $report->id, $row + $n_header_rows, 'errors', $_)      for @{ $data_row->{errors}      || [] };
+    }
+    1;
+  }) or do { die SL::DB->client->error };
 
   return $report->id;
 }
 
-sub csv_file_name {
-  my ($self) = @_;
-  return "csv-import-" . $self->type . ".csv";
-}
-
 sub init_worker {
   my $self = shift;
 
@@ -620,15 +722,20 @@ sub init_worker {
 
   return $self->{type} eq 'customers_vendors' ? SL::Controller::CsvImport::CustomerVendor->new(@args)
        : $self->{type} eq 'contacts'          ? SL::Controller::CsvImport::Contact->new(@args)
+       : $self->{type} eq 'billing_addresses' ? SL::Controller::CsvImport::AdditionalBillingAddress->new(@args)
        : $self->{type} eq 'addresses'         ? SL::Controller::CsvImport::Shipto->new(@args)
        : $self->{type} eq 'parts'             ? SL::Controller::CsvImport::Part->new(@args)
        : $self->{type} eq 'inventories'       ? SL::Controller::CsvImport::Inventory->new(@args)
        : $self->{type} eq 'projects'          ? SL::Controller::CsvImport::Project->new(@args)
        : $self->{type} eq 'orders'            ? SL::Controller::CsvImport::Order->new(@args)
+       : $self->{type} eq 'delivery_orders'   ? SL::Controller::CsvImport::DeliveryOrder->new(@args)
        : $self->{type} eq 'bank_transactions' ? SL::Controller::CsvImport::BankTransaction->new(@args)
+       : $self->{type} eq 'ar_transactions'   ? SL::Controller::CsvImport::ARTransaction->new(@args)
        :                                        die "Program logic error";
 }
 
+sub init_num_imported { 0 }
+
 sub setup_help {
   my ($self) = @_;
 
@@ -652,11 +759,44 @@ sub cleanup_reports {
 }
 
 sub check_task_server {
+  if (!$::auth->client->{task_server_user_id}) {
+    flash('error', t8('The task server is required for this module but not enabled for the current client. Please enable it for the client "#1" in the administration section.', $::auth->client->{name}));
+  }
+
   return 1 if $_[0]->task_server->is_running;
 
-  flash('info', t8('The task server is not running at the moment but needed for this module'));
+  flash('warning', t8('The task server is not running at the moment but needed for this module'));
 
   1;
 }
 
+sub mappings_for_profile {
+  +{ map { $_->{from} => $_->{to} } @{ $_[0]->mappings } }
+}
+
+sub init_mappings {
+  [ grep { $_->{from} } @{ $::form->{mappings} || [] } ]
+}
+
+sub setup_render_inputs_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Preview'),
+        submit    => [ '#form', { action => 'CsvImport/test' } ],
+        accesskey => 'enter',
+        not_if    => ($self->profile && $self->profile->get('dont_edit_profile')),
+      ],
+      action => [
+        t8('Import'),
+        submit    => [ '#form', { action => 'CsvImport/import' } ],
+        disabled  => t8('The test import has not been executed yet.'),
+        id        => 'action_import',
+      ],
+    );
+  }
+}
+
 1;
diff --git a/SL/Controller/CsvImport/ARTransaction.pm b/SL/Controller/CsvImport/ARTransaction.pm
new file mode 100644 (file)
index 0000000..36760fe
--- /dev/null
@@ -0,0 +1,605 @@
+package SL::Controller::CsvImport::ARTransaction;
+
+use strict;
+
+use List::MoreUtils qw(any);
+
+use Data::Dumper;
+use SL::Helper::Csv;
+use SL::Controller::CsvImport::Helper::Consistency;
+use SL::DB::Invoice;
+use SL::DB::AccTransaction;
+use SL::DB::Department;
+use SL::DB::Project;
+use SL::DB::TaxZone;
+use SL::DB::Chart;
+use SL::TransNumber;
+use DateTime;
+
+use parent qw(SL::Controller::CsvImport::BaseMulti);
+
+use Rose::Object::MakeMethods::Generic
+(
+ 'scalar --get_set_init' => [ qw(settings charts_by taxkeys_by) ],
+);
+
+
+sub init_class {
+  my ($self) = @_;
+  $self->class(['SL::DB::Invoice', 'SL::DB::AccTransaction']);
+}
+
+sub set_profile_defaults {
+  my ($self) = @_;
+
+  $self->controller->profile->_set_defaults(
+                       ar_column          => $::locale->text('Invoice'),
+                       transaction_column => $::locale->text('AccTransaction'),
+                       max_amount_diff    => 0.02,
+                      );
+};
+
+
+sub init_settings {
+  my ($self) = @_;
+
+  return { map { ( $_ => $self->controller->profile->get($_) ) } qw(ar_column transaction_column max_amount_diff) };
+}
+
+sub init_profile {
+  my ($self) = @_;
+
+  my $profile = $self->SUPER::init_profile;
+
+  # SUPER::init_profile sets row_ident to the translated class name
+  # overwrite it with the user specified settings
+# TODO: remove hardcoded row_idents
+  foreach my $p (@{ $profile }) {
+    if ($p->{class} eq 'SL::DB::Invoice') {
+      $p->{row_ident} = $self->_ar_column;
+    }
+    if ($p->{class} eq 'SL::DB::AccTransaction') {
+      $p->{row_ident} = $self->_transaction_column;
+    }
+  }
+
+  foreach my $p (@{ $profile }) {
+    my $prof = $p->{profile};
+    if ($p->{row_ident} eq $self->_ar_column) {
+      # no need to handle
+      delete @{$prof}{qw(delivery_customer_id delivery_vendor_id )};
+    }
+    if ($p->{row_ident} eq $self->_transaction_column) {
+      # no need to handle
+      delete @{$prof}{qw(trans_id)};
+    }
+  }
+
+  return $profile;
+}
+
+
+sub setup_displayable_columns {
+  my ($self) = @_;
+
+  $self->SUPER::setup_displayable_columns;
+
+  $self->add_displayable_columns($self->_ar_column,
+                                 { name => 'datatype',                description => $self->_ar_column . ' [1]'                               },
+                                 { name => 'currency',                description => $::locale->text('Currency')                              },
+                                 { name => 'cusordnumber',            description => $::locale->text('Customer Order Number')                 },
+                                 { name => 'direct_debit',            description => $::locale->text('direct debit')                          },
+                                 { name => 'donumber',                description => $::locale->text('Delivery Order Number')                 },
+                                 { name => 'duedate',                 description => $::locale->text('Due Date')                              },
+                                 { name => 'delivery_term_id',        description => $::locale->text('Delivery terms (database ID)')          },
+                                 { name => 'delivery_term',           description => $::locale->text('Delivery terms (name)')                 },
+                                 { name => 'deliverydate',            description => $::locale->text('Delivery Date')                         },
+                                 { name => 'employee_id',             description => $::locale->text('Employee (database ID)')                },
+                                 { name => 'intnotes',                description => $::locale->text('Internal Notes')                        },
+                                 { name => 'notes',                   description => $::locale->text('Notes')                                 },
+                                 { name => 'invnumber',               description => $::locale->text('Invoice Number')                        },
+                                 { name => 'quonumber',               description => $::locale->text('Quotation Number')                      },
+                                 { name => 'reqdate',                 description => $::locale->text('Reqdate')                               },
+                                 { name => 'salesman_id',             description => $::locale->text('Salesman (database ID)')                },
+                                 { name => 'transaction_description', description => $::locale->text('Transaction description')               },
+                                 { name => 'transdate',               description => $::locale->text('Invoice Date')                          },
+                                 { name => 'verify_amount',           description => $::locale->text('Amount (for verification)') . ' [2]'    },
+                                 { name => 'verify_netamount',        description => $::locale->text('Net amount (for verification)') . ' [2]'},
+                                 { name => 'taxincluded',             description => $::locale->text('Tax Included')                          },
+                                 { name => 'customer',                description => $::locale->text('Customer (name)')                       },
+                                 { name => 'customernumber',          description => $::locale->text('Customer Number')                       },
+                                 { name => 'customer_gln',            description => $::locale->text('Customer GLN')                          },
+                                 { name => 'customer_id',             description => $::locale->text('Customer (database ID)')                },
+                                 { name => 'language_id',             description => $::locale->text('Language (database ID)')                },
+                                 { name => 'language',                description => $::locale->text('Language (name)')                       },
+                                 { name => 'payment_id',              description => $::locale->text('Payment terms (database ID)')           },
+                                 { name => 'payment',                 description => $::locale->text('Payment terms (name)')                  },
+                                 { name => 'taxzone_id',              description => $::locale->text('Tax zone (database ID)')                },
+                                 { name => 'taxzone',                 description => $::locale->text('Tax zone (description)')                },
+                                 { name => 'department_id',           description => $::locale->text('Department (database ID)')              },
+                                 { name => 'department',              description => $::locale->text('Department (description)')              },
+                                 { name => 'globalproject_id',        description => $::locale->text('Document Project (database ID)')        },
+                                 { name => 'globalprojectnumber',     description => $::locale->text('Document Project (number)')             },
+                                 { name => 'globalproject',           description => $::locale->text('Document Project (description)')        },
+                                 { name => 'archart',                 description => $::locale->text('Receivables account (account number)')  },
+                                 { name => 'orddate',                 description => $::locale->text('Order Date')                            },
+                                 { name => 'ordnumber',               description => $::locale->text('Order Number')                          },
+                                 { name => 'quonumber',               description => $::locale->text('Quotation Number')                      },
+                                 { name => 'quodate',                 description => $::locale->text('Quotation Date')                        },
+                                );
+
+  $self->add_displayable_columns($self->_transaction_column,
+                                 { name => 'datatype',      description => $self->_transaction_column . ' [1]'       },
+                                 { name => 'projectnumber', description => $::locale->text('Project (number)')       },
+                                 { name => 'project',       description => $::locale->text('Project (description)')  },
+                                 { name => 'amount',        description => $::locale->text('Amount')                 },
+                                 { name => 'accno',         description => $::locale->text('Account number')         },
+                                 { name => 'taxkey',        description => $::locale->text('Taxkey')                 },
+                                );
+}
+
+sub init_taxkeys_by {
+  my ($self) = @_;
+
+  my $all_taxes = SL::DB::Manager::Tax->get_all;
+  return { map { $_->taxkey => $_->id } @{ $all_taxes } };
+}
+
+
+sub init_charts_by {
+  my ($self) = @_;
+
+  my $all_charts = SL::DB::Manager::Chart->get_all;
+  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_charts } } ) } qw(id accno) };
+}
+
+sub check_objects {
+  my ($self) = @_;
+
+  $self->controller->track_progress(phase => 'building data', progress => 0);
+
+  my $i = 0;
+  my $num_data = scalar @{ $self->controller->data };
+  my $invoice_entry;
+
+  foreach my $entry (@{ $self->controller->data }) {
+    $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
+
+    if ($entry->{raw_data}->{datatype} eq $self->_ar_column) {
+      $self->handle_invoice($entry);
+      $invoice_entry = $entry;
+    } elsif ($entry->{raw_data}->{datatype} eq $self->_transaction_column ) {
+      die "Cannot process transaction row without an invoice row" if !$invoice_entry;
+      $self->handle_transaction($entry, $invoice_entry);
+    } else {
+      die "unknown datatype";
+    };
+
+  } continue {
+    $i++;
+  } # finished data parsing
+
+  $self->add_transactions_to_ar(); # go through all data entries again, adding receivable entry to ar lines while calculating amount and netamount
+
+  foreach my $entry (@{ $self->controller->data }) {
+    next unless ($entry->{raw_data}->{datatype} eq $self->_ar_column);
+    $self->check_verify_amounts($entry->{object});
+  };
+
+  foreach my $entry (@{ $self->controller->data }) {
+    next unless ($entry->{raw_data}->{datatype} eq $self->_ar_column);
+    unless ( $entry->{object}->validate_acc_trans ) {
+      push @{ $entry->{errors} }, $::locale->text('Error: ar transaction doesn\'t validate');
+    };
+  };
+
+  # add info columns that aren't directly part of the object to be imported
+  # but are always determined or should always be shown because they are mandatory
+  $self->add_info_columns($self->_ar_column,
+                          { header => $::locale->text('Customer/Vendor'),     method => 'vc_name'   },
+                          { header => $::locale->text('Receivables account'), method => 'archart'   },
+                          { header => $::locale->text('Amount'),              method => 'amount'    },
+                          { header => $::locale->text('Net amount'),          method => 'netamount' },
+                          { header => $::locale->text('Tax zone'),            method => 'taxzone'   });
+
+  # Adding info_header this way only works, if the first invoice $self->controller->data->[0]
+
+  # Todo: access via ->[0] ok? Better: search first order column and use this
+  $self->add_info_columns($self->_ar_column, { header => $::locale->text('Department'),    method => 'department' }) if $self->controller->data->[0]->{info_data}->{department} or $self->controller->data->[0]->{raw_data}->{department};
+
+  $self->add_info_columns($self->_ar_column, { header => $::locale->text('Project Number'), method => 'globalprojectnumber' }) if $self->controller->data->[0]->{info_data}->{globalprojectnumber};
+
+  $self->add_columns($self->_ar_column,
+                     map { "${_}_id" } grep { exists $self->controller->data->[0]->{raw_data}->{$_} } qw(payment department globalproject taxzone cp currency));
+  $self->add_columns($self->_ar_column, 'globalproject_id') if exists $self->controller->data->[0]->{raw_data}->{globalprojectnumber};
+  $self->add_columns($self->_ar_column, 'notes')            if exists $self->controller->data->[0]->{raw_data}->{notes};
+
+  # Todo: access via ->[1] ok? Better: search first item column and use this
+  $self->add_info_columns($self->_transaction_column, { header => $::locale->text('Chart'), method => 'accno' });
+  $self->add_columns($self->_transaction_column, 'amount');
+
+  $self->add_info_columns($self->_transaction_column, { header => $::locale->text('Project Number'), method => 'projectnumber' }) if $self->controller->data->[1]->{info_data}->{projectnumber};
+
+  # $self->add_columns($self->_transaction_column,
+  #                    map { "${_}_id" } grep { exists $self->controller->data->[1]->{raw_data}->{$_} } qw(project price_factor pricegroup));
+  # $self->add_columns($self->_transaction_column,
+  #                    map { "${_}_id" } grep { exists $self->controller->data->[2]->{raw_data}->{$_} } qw(project price_factor pricegroup));
+  # $self->add_columns($self->_transaction_column, 'project_id') if exists $self->controller->data->[1]->{raw_data}->{projectnumber};
+  # $self->add_columns($self->_transaction_column, 'taxkey') if exists $self->controller->data->[1]->{raw_data}->{taxkey};
+
+  # If invoice has errors, add error for acc_trans items
+  # If acc_trans item has an error, add an error to the invoice item
+  my $ar_entry;
+  foreach my $entry (@{ $self->controller->data }) {
+    # Search first order
+    if ($entry->{raw_data}->{datatype} eq $self->_ar_column) {
+      $ar_entry = $entry;
+    } elsif ( defined $ar_entry
+              && $entry->{raw_data}->{datatype} eq $self->_transaction_column
+              && scalar @{ $ar_entry->{errors} } > 0 ) {
+      push @{ $entry->{errors} }, $::locale->text('Error: invalid ar row for this transaction');
+    } elsif ( defined $ar_entry
+              && $entry->{raw_data}->{datatype} eq $self->_transaction_column
+              && scalar @{ $entry->{errors} } > 0 ) {
+      push @{ $ar_entry->{errors} }, $::locale->text('Error: invalid acc transactions for this ar row');
+    }
+  }
+}
+
+sub handle_invoice {
+
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  $object->transactions( [] ); # initialise transactions for ar object so methods work on unsaved transactions
+
+  my $vc_obj;
+  if (any { $entry->{raw_data}->{$_} } qw(customer customernumber customer_gln customer_id)) {
+    $self->check_vc($entry, 'customer_id');
+    # check_vc only sets customer_id, but we need vc_obj later for customer defaults
+    $vc_obj = SL::DB::Customer->new(id => $object->customer_id)->load if $object->customer_id;
+  } elsif (any { $entry->{raw_data}->{$_} } qw(vendor vendornumber vendor_gln vendor_id)) {
+    $self->check_vc($entry, 'vendor_id');
+    $vc_obj = SL::DB::Vendor->new(id => $object->vendor_id)->load if $object->vendor_id;
+  } else {
+    push @{ $entry->{errors} }, $::locale->text('Error: Customer/vendor missing');
+  }
+
+  # check for duplicate invnumbers already in database
+  if ( SL::DB::Manager::Invoice->get_all_count( where => [ invnumber => $object->invnumber ] ) ) {
+    push @{ $entry->{errors} }, $::locale->text('Error: invnumber already exists');
+  }
+
+  $self->check_archart($entry); # checks for receivable account
+  # $self->check_amounts($entry); # checks and sets amount and netamount, use verify_amount and verify_netamount instead
+  $self->check_payment($entry); # currency default from customer used below
+  $self->check_department($entry);
+  $self->check_taxincluded($entry);
+  $self->check_project($entry, global => 1);
+  $self->check_taxzone($entry); # taxzone default from customer used below
+  $self->check_currency($entry); # currency default from customer used below
+  $self->handle_salesman($entry);
+  $self->handle_employee($entry);
+
+  if ($vc_obj ) {
+    # copy defaults from customer if not specified in import file
+    foreach (qw(payment_id language_id taxzone_id currency_id)) {
+      $object->$_($vc_obj->$_) unless $object->$_;
+    }
+  }
+}
+
+sub check_taxkey {
+  my ($self, $entry, $invoice_entry, $chart) = @_;
+
+  die "check_taxkey needs chart object as an argument" unless ref($chart) eq 'SL::DB::Chart';
+  # problem: taxkey is not unique in table tax, normally one of those entries is chosen directly from a dropdown
+  # so we check if the chart has an active taxkey, and if it matches the taxkey from the import, use the active taxkey
+  # if the chart doesn't have an active taxkey, use the first entry from Tax that matches the taxkey
+
+  my $object         = $entry->{object};
+  my $invoice_object = $invoice_entry->{object};
+
+  unless ( defined $entry->{raw_data}->{taxkey} ) {
+    push @{ $entry->{errors} }, $::locale->text('Error: taxkey missing'); # don't just assume 0, force taxkey in import
+    return 0;
+  };
+
+  my $tax = $chart->get_active_taxkey($invoice_object->deliverydate // $invoice_object->transdate // DateTime->today_local)->tax;
+  if ( $entry->{raw_data}->{taxkey} != $tax->taxkey ) {
+   # assume there is only one tax entry with that taxkey, can't guess
+    $tax = SL::DB::Manager::Tax->get_first( where => [ taxkey => $entry->{raw_data}->{taxkey} ]);
+  };
+
+  unless ( $tax ) {
+    push @{ $entry->{errors} }, $::locale->text('Error: invalid taxkey');
+    return 0;
+  };
+
+  $object->taxkey($tax->taxkey);
+  $object->tax_id($tax->id);
+  return 1;
+};
+
+sub check_amounts {
+  my ($self, $entry) = @_;
+  # currently not used in favour of verify_amount and verify_netamount
+
+  my $object = $entry->{object};
+
+  unless ($entry->{raw_data}->{amount} && $entry->{raw_data}->{netamount}) {
+    push @{ $entry->{errors} }, $::locale->text('Error: need amount and netamount');
+    return 0;
+  };
+  unless ($entry->{raw_data}->{amount} * 1 && $entry->{raw_data}->{netamount} * 1) {
+    push @{ $entry->{errors} }, $::locale->text('Error: amount and netamount need to be numeric');
+    return 0;
+  };
+
+  $object->amount( $entry->{raw_data}->{amount} );
+  $object->netamount( $entry->{raw_data}->{netamount} );
+};
+
+sub handle_transaction {
+  my ($self, $entry, $invoice_entry) = @_;
+
+  # Prepare acc_trans data. amount is dealt with in add_transactions_to_ar
+
+  my $object = $entry->{object};
+
+  $self->check_project($entry, global => 0);
+  if ( $self->check_chart($entry) ) {
+    my $chart_obj = SL::DB::Manager::Chart->find_by(id => $object->chart_id);
+
+    unless ( $chart_obj->link =~ /AR_amount/ ) {
+      push @{ $entry->{errors} }, $::locale->text('Error: chart isn\'t an ar_amount chart');
+      return 0;
+    };
+
+    if ( $self->check_taxkey($entry, $invoice_entry, $chart_obj) ) {
+      # do nothing, taxkey was assigned, just continue
+    } else {
+      # missing taxkey, don't do anything
+      return 0;
+    };
+  } else {
+    return 0;
+  };
+
+  # check whether taxkey and automatic taxkey match
+  # die sprintf("taxkeys don't match: %s not equal default taxkey for chart %s: %s", $object->taxkey, $chart_obj->accno, $active_tax_for_chart->tax->taxkey) unless $object->taxkey == $active_tax_for_chart->tax->taxkey;
+
+  die "no taxkey for transaction object" unless $object->taxkey or $object->taxkey == 0;
+
+}
+
+sub check_chart {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  if (any { $entry->{raw_data}->{$_} } qw(accno chart_id)) {
+
+    # Check whether or not chart ID is valid.
+    if ($object->chart_id && !$self->charts_by->{id}->{ $object->chart_id }) {
+      push @{ $entry->{errors} }, $::locale->text('Error: invalid chart_id');
+      return 0;
+    }
+
+    # Map number to ID if given.
+    if (!$object->chart_id && $entry->{raw_data}->{accno}) {
+      my $chart = $self->charts_by->{accno}->{ $entry->{raw_data}->{accno} };
+      if (!$chart) {
+        push @{ $entry->{errors} }, $::locale->text('Error: invalid chart (accno)');
+        return 0;
+      }
+
+      $object->chart_id($chart->id);
+    }
+
+    # Map description to ID if given.
+    if (!$object->chart_id && $entry->{raw_data}->{description}) {
+      my $chart = $self->charts_by->{description}->{ $entry->{raw_data}->{description} };
+      if (!$chart) {
+        push @{ $entry->{errors} }, $::locale->text('Error: invalid chart');
+        return 0;
+      }
+
+      $object->chart_id($chart->id);
+    }
+
+    if ($object->chart_id) {
+      # add account number to preview
+      $entry->{info_data}->{accno} = $self->charts_by->{id}->{ $object->chart_id }->accno;
+    } else {
+      push @{ $entry->{errors} }, $::locale->text('Error: chart not found');
+      return 0;
+    }
+  } else {
+    push @{ $entry->{errors} }, $::locale->text('Error: chart missing');
+    return 0;
+  }
+
+  return 1;
+}
+
+sub check_archart {
+  my ($self, $entry) = @_;
+
+  my $chart;
+
+  if ( $entry->{raw_data}->{archart} ) {
+    my $archart = $entry->{raw_data}->{archart};
+    $chart = SL::DB::Manager::Chart->find_by(accno => $archart);
+    unless ($chart) {
+      push @{ $entry->{errors} }, $::locale->text("Error: can't find ar chart with accno #1", $archart);
+      return 0;
+    };
+  } elsif ( $::instance_conf->get_ar_chart_id ) {
+    $chart = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_ar_chart_id);
+  } else {
+    push @{ $entry->{errors} }, $::locale->text("Error: neither archart passed, no default receivables chart configured");
+    return 0;
+  };
+
+  unless ($chart->link eq 'AR') {
+    push @{ $entry->{errors} }, $::locale->text('Error: archart isn\'t an AR chart');
+    return 0;
+  };
+
+  $entry->{info_data}->{archart} = $chart->accno;
+  $entry->{object}->{archart} = $chart;
+  return 1;
+};
+
+sub check_taxincluded {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  if ( $entry->{raw_data}->{taxincluded} ) {
+    if ( $entry->{raw_data}->{taxincluded} eq 'f' or $entry->{raw_data}->{taxincluded} eq '0' ) {
+      $object->taxincluded('0');
+    } elsif ( $entry->{raw_data}->{taxincluded} eq 't' or $entry->{raw_data}->{taxincluded} eq '1' ) {
+      $object->taxincluded('1');
+    } else {
+      push @{ $entry->{errors} }, $::locale->text('Error: taxincluded has to be t or f');
+      return 0;
+    };
+  } else {
+    push @{ $entry->{errors} }, $::locale->text('Error: taxincluded wasn\'t set');
+    return 0;
+  };
+  return 1;
+};
+
+sub check_verify_amounts {
+  my ($self) = @_;
+
+  # If amounts are given, show calculated amounts as info and given amounts (verify_xxx).
+  # And throw an error if the differences are too big.
+  my @to_verify = ( { column      => 'amount',
+                      raw_column  => 'verify_amount',
+                      info_header => 'Calc. Amount',
+                      info_method => 'calc_amount',
+                      err_msg     => $::locale->text('Amounts differ too much'),
+                    },
+                    { column      => 'netamount',
+                      raw_column  => 'verify_netamount',
+                      info_header => 'Calc. Net amount',
+                      info_method => 'calc_netamount',
+                      err_msg     => $::locale->text('Net amounts differ too much'),
+                    } );
+
+  foreach my $tv (@to_verify) {
+    if (exists $self->controller->data->[0]->{raw_data}->{ $tv->{raw_column} }) {
+      $self->add_raw_data_columns($self->_ar_column, $tv->{raw_column});
+      $self->add_info_columns($self->_ar_column,
+                              { header => $::locale->text($tv->{info_header}), method => $tv->{info_method} });
+    }
+
+    # check differences
+    foreach my $entry (@{ $self->controller->data }) {
+      if ( @{ $entry->{errors} } ) {
+        push @{ $entry->{errors} }, $::locale->text($tv->{err_msg});
+        return 0;
+      };
+
+      if ($entry->{raw_data}->{datatype} eq $self->_ar_column) {
+        next if !$entry->{raw_data}->{ $tv->{raw_column} };
+        my $parsed_value = $::form->parse_amount(\%::myconfig, $entry->{raw_data}->{ $tv->{raw_column} });
+        # round $abs_diff, otherwise it might trigger for 0.020000000000021
+        my $abs_diff = $::form->round_amount(abs($entry->{object}->${ \$tv->{column} } - $parsed_value),2);
+        if ( $abs_diff > $self->settings->{'max_amount_diff'}) {
+          push @{ $entry->{errors} }, $::locale->text($tv->{err_msg});
+        }
+      }
+    }
+  }
+};
+
+sub add_transactions_to_ar {
+  my ($self) = @_;
+
+  # go through all verified ar and acc_trans rows in import, adding acc_trans objects to ar objects
+
+  my $ar_entry;  # the current ar row
+
+  foreach my $entry (@{ $self->controller->data }) {
+    # when we reach an ar_column for the first time, don't do anything, just store in $ar_entry
+    # when we reach an ar_column for the second time, save it
+    if ($entry->{raw_data}->{datatype} eq $self->_ar_column) {
+      if ( $ar_entry && $ar_entry->{object} ) { # won't trigger the first time, finishes the last object
+        if ( $ar_entry->{object}->{archart} && $ar_entry->{object}->{archart}->isa('SL::DB::Chart') ) {
+          $ar_entry->{object}->recalculate_amounts; # determine and set amount and netamount for ar
+          $ar_entry->{object}->create_ar_row(chart => $ar_entry->{object}->{archart});
+          $ar_entry->{info_data}->{amount}    = $ar_entry->{object}->amount;
+          $ar_entry->{info_data}->{netamount} = $ar_entry->{object}->netamount;
+        } else {
+          push @{ $entry->{errors} }, $::locale->text("ar_chart isn't a valid chart");
+        };
+      };
+      $ar_entry = $entry; # remember as last ar_entry
+
+    } elsif ( defined $ar_entry && $entry->{raw_data}->{datatype} eq $self->_transaction_column ) {
+      push @{ $entry->{errors} }, $::locale->text('no tax_id in acc_trans') if !defined $entry->{object}->tax_id;
+      next if @{ $entry->{errors} };
+
+      my $acc_trans_objects = $ar_entry->{object}->add_ar_amount_row(
+        amount      => $entry->{object}->amount,
+        chart       => SL::DB::Manager::Chart->find_by(id => $entry->{object}->chart_id), # add_ar_amount takes chart obj. as argument
+        tax_id      => $entry->{object}->tax_id,
+        project_id  => $entry->{object}->project_id,
+        debug       => 0,
+      );
+
+    } else {
+      die "This should never happen\n";
+    };
+  }
+
+  # finish the final object
+  if ( $ar_entry->{object} ) {
+    if ( $ar_entry->{object}->{archart} && $ar_entry->{object}->{archart}->isa('SL::DB::Chart') ) {
+      $ar_entry->{object}->recalculate_amounts;
+      $ar_entry->{info_data}->{amount}    = $ar_entry->{object}->amount;
+      $ar_entry->{info_data}->{netamount} = $ar_entry->{object}->netamount;
+
+      $ar_entry->{object}->create_ar_row(chart => $ar_entry->{object}->{archart});
+    } else {
+      push @{ $ar_entry->{errors} }, $::locale->text("The receivables chart isn't a valid chart.");
+      return 0;
+    };
+  } else {
+    die "There was no final ar_entry object";
+  };
+}
+
+sub save_objects {
+  my ($self, %params) = @_;
+
+  # save all the Invoice objects
+  my $objects_to_save;
+  foreach my $entry (@{ $self->controller->data }) {
+    # only push the invoice objects that don't have an error
+    next if $entry->{raw_data}->{datatype} ne $self->_ar_column;
+    next if @{ $entry->{errors} };
+
+    die unless $entry->{object}->validate_acc_trans;
+
+    push @{ $objects_to_save }, $entry;
+  }
+
+  $self->SUPER::save_objects(data => $objects_to_save);
+}
+
+sub _ar_column {
+  $_[0]->settings->{'ar_column'}
+}
+
+sub _transaction_column {
+  $_[0]->settings->{'transaction_column'}
+}
+
+1;
diff --git a/SL/Controller/CsvImport/AdditionalBillingAddress.pm b/SL/Controller/CsvImport/AdditionalBillingAddress.pm
new file mode 100644 (file)
index 0000000..6d2d8ea
--- /dev/null
@@ -0,0 +1,90 @@
+package SL::Controller::CsvImport::AdditionalBillingAddress;
+
+use strict;
+
+use SL::Helper::Csv;
+
+use parent qw(SL::Controller::CsvImport::Base);
+
+use Rose::Object::MakeMethods::Generic
+(
+ scalar => [ qw(table) ],
+);
+
+sub set_profile_defaults {
+};
+
+sub init_class {
+  my ($self) = @_;
+  $self->class('SL::DB::AdditionalBillingAddress');
+}
+
+sub _hash_object {
+  my ($o) = @_;
+  return join('--', map({ s/[\s,\.\-]//g; $_ } ($o->name, $o->street)));
+}
+
+sub check_objects {
+  my ($self) = @_;
+
+  $self->controller->track_progress(phase => 'building data', progress => 0);
+
+  my %existing_by_id_name_street = map { (_hash_object($_) => $_) } @{ $self->existing_objects };
+  my $methods                    = $self->controller->headers->{methods};
+
+  my $i = 0;
+  my $num_data = scalar @{ $self->controller->data };
+  foreach my $entry (@{ $self->controller->data }) {
+    $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
+
+    $self->check_vc($entry, 'customer_id');
+
+    next if @{ $entry->{errors} };
+
+    my $object   = $entry->{object};
+    my $idx      = _hash_object($object);
+    my $existing = $existing_by_id_name_street{$idx};
+
+    if (!$existing) {
+      $existing_by_id_name_street{$idx} = $object;
+    } else {
+      $entry->{object_to_save} = $existing;
+
+      $existing->$_( $object->$_ ) for @{ $methods };
+
+      push @{ $entry->{information} }, $::locale->text('Updating existing entry in database');
+    }
+
+  } continue {
+    $i++;
+  }
+
+  $self->add_info_columns({ header => $::locale->text('Customer/Vendor'), method => 'vc_name' });
+}
+
+sub setup_displayable_columns {
+  my ($self) = @_;
+
+  $self->SUPER::setup_displayable_columns;
+
+  $self->add_displayable_columns(
+    { name => 'default_address', description => $::locale->text('Default address flag') },
+    { name => 'name',            description => $::locale->text('Name')                 },
+    { name => 'department_1',    description => $::locale->text('Department 1')         },
+    { name => 'department_2',    description => $::locale->text('Department 2')         },
+    { name => 'street',          description => $::locale->text('Street')               },
+    { name => 'zipcode',         description => $::locale->text('Zipcode')              },
+    { name => 'city',            description => $::locale->text('City')                 },
+    { name => 'country',         description => $::locale->text('Country')              },
+    { name => 'contact',         description => $::locale->text('Contact')              },
+    { name => 'email',           description => $::locale->text('E-mail')               },
+    { name => 'fax',             description => $::locale->text('Fax')                  },
+    { name => 'gln',             description => $::locale->text('GLN')                  },
+    { name => 'phone',           description => $::locale->text('Phone')                },
+    { name => 'customer_id',     description => $::locale->text('Customer')             },
+    { name => 'customer',        description => $::locale->text('Customer (name)')      },
+    { name => 'customernumber',  description => $::locale->text('Customer Number')      },
+  );
+}
+
+1;
index addcb01..7706373 100644 (file)
@@ -41,7 +41,7 @@ sub check_objects {
   $self->controller->track_progress(phase => 'building data', progress => 0);
   my $update_policy  = $self->controller->profile->get('update_policy') || 'skip';
 
-  my $i;
+  my $i = 0;
   my $num_data = scalar @{ $self->controller->data };
   foreach my $entry (@{ $self->controller->data }) {
     $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
@@ -56,6 +56,7 @@ sub check_objects {
   }
 
   $self->add_info_columns({ header => $::locale->text('Bank account'), method => 'local_bank_name' });
+  $self->add_raw_data_columns("currency", "currency_id") if grep { /^currency(?:_id)?$/ } @{ $self->csv->header };
 }
 
 sub check_existing {
@@ -73,8 +74,9 @@ sub check_existing {
     # * transdate
     # * remote_account_number  (may be empty for records of our own bank)
     # * amount
+    # * local_bank_account_id (case flatrate bank charges for two accounts in one bank: same purpose, transdate, remote_account_number(empty), amount. Just different local_bank_account_id)
     my $num;
-    if ( $num = SL::DB::Manager::BankTransaction->get_all_count(query =>[ remote_account_number => $object->remote_account_number, transdate => $object->transdate, purpose => $object->purpose, amount => $object->amount] ) ) {
+    if ( $num = SL::DB::Manager::BankTransaction->get_all_count(query =>[ remote_account_number => $object->remote_account_number, transdate => $object->transdate, purpose => $object->purpose, amount => $object->amount, local_bank_account_id => $object->local_bank_account_id] ) ) {
       push(@{$entry->{errors}}, $::locale->text('Skipping due to existing bank transaction in database'));
     };
   } else {
@@ -82,25 +84,43 @@ sub check_existing {
   };
 }
 
+sub _displayable_columns {
+ (
+   { name => 'local_bank_code',       description => $::locale->text('Own bank code') },
+   { name => 'local_account_number',  description => $::locale->text('Own bank account number or IBAN') },
+   { name => 'local_bank_account_id', description => $::locale->text('ID of own bank account') },
+   { name => 'remote_bank_code',      description => $::locale->text('Bank code of the goal/source') },
+   { name => 'remote_account_number', description => $::locale->text('Account number of the goal/source') },
+   { name => 'transdate',             description => $::locale->text('Transdate') },
+   { name => 'valutadate',            description => $::locale->text('Valutadate') },
+   { name => 'amount',                description => $::locale->text('Amount') },
+   { name => 'currency',              description => $::locale->text('Currency') },
+   { name => 'currency_id',           description => $::locale->text('Currency (database ID)')          },
+   { name => 'remote_name',           description => $::locale->text('Name of the goal/source (if field names remote_name and remote_name_1 exist they will be combined into field "remote_name")') },
+   { name => 'remote_name_1',          description => $::locale->text('Name of the goal/source (if field names remote_name and remote_name_1 exist they will be combined into field "remote_name")') },
+   { name => 'purpose',               description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') },
+   { name => 'purpose1',              description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') },
+   { name => 'purpose2',              description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') },
+   { name => 'purpose3',              description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') },
+   { name => 'purpose4',              description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') },
+   { name => 'purpose5',              description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') },
+   { name => 'purpose6',              description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') },
+   { name => 'purpose7',              description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') },
+   { name => 'purpose8',              description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') },
+   { name => 'purpose9',              description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') },
+   { name => 'purpose10',             description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') },
+   { name => 'purpose11',             description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') },
+   { name => 'purpose12',             description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') },
+   { name => 'purpose13',             description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') }
+ );
+}
+
 sub setup_displayable_columns {
   my ($self) = @_;
 
   $self->SUPER::setup_displayable_columns;
 
-  # TODO: don't show fields cleared, invoice_amount and transaction_id in the help text, as these should not be imported
-  $self->add_displayable_columns({ name => 'local_bank_code',       description => $::locale->text('Own bank code') },
-                                 { name => 'local_account_number',  description => $::locale->text('Own bank account number or IBAN') },
-                                 { name => 'local_bank_account_id', description => $::locale->text('ID of own bank account') },
-                                 { name => 'remote_bank_code',      description => $::locale->text('Bank code of the goal/source') },
-                                 { name => 'remote_account_number', description => $::locale->text('Account number of the goal/source') },
-                                 { name => 'transdate',             description => $::locale->text('Date of transaction') },
-                                 { name => 'valutadate',            description => $::locale->text('Valuta date') },
-                                 { name => 'amount',                description => $::locale->text('Amount') },
-                                 { name => 'currency',              description => $::locale->text('Currency') },
-                                 { name => 'currency_id',           description => $::locale->text('Currency (database ID)')          },
-                                 { name => 'remote_name',           description => $::locale->text('Name of the goal/source (if field names remote_name and remote_name_1 exist they will be combined into field "remote_name")') },
-                                 { name => 'purpose',               description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') },
-                                 );
+  $self->add_displayable_columns($self->_displayable_columns);
 }
 
 sub check_bank_account {
@@ -147,7 +167,6 @@ sub check_bank_account {
     $object->local_bank_account_id($bank_account->id);
     $entry->{info_data}->{local_bank_name} = $bank_account->name;
   }
-
   return $object->local_bank_account_id ? 1 : 0;
 }
 
@@ -156,18 +175,12 @@ sub join_purposes {
 
   my $object = $entry->{object};
 
-  my $purpose = join('', $entry->{raw_data}->{purpose},
-                         $entry->{raw_data}->{purpose1},
-                         $entry->{raw_data}->{purpose2},
-                         $entry->{raw_data}->{purpose3},
-                         $entry->{raw_data}->{purpose4},
-                         $entry->{raw_data}->{purpose5},
-                         $entry->{raw_data}->{purpose6},
-                         $entry->{raw_data}->{purpose7},
-                         $entry->{raw_data}->{purpose8},
-                         $entry->{raw_data}->{purpose9},
-                         $entry->{raw_data}->{purpose10},
-                         $entry->{raw_data}->{purpose11} );
+  my $purpose =
+    join ' ',
+    grep { ($_ // '') !~ m{^ *$} }
+    map  { $entry->{raw_data}->{"purpose$_"} }
+    ('', 1..13);
+
   $object->purpose($purpose);
 
 }
@@ -177,9 +190,13 @@ sub join_remote_names {
 
   my $object = $entry->{object};
 
-  my $remote_name = join('', $entry->{raw_data}->{remote_name},
+  my $remote_name = join(' ', $entry->{raw_data}->{remote_name},
                              $entry->{raw_data}->{remote_name_1} );
   $object->remote_name($remote_name);
 }
 
+sub check_auth {
+  $::auth->assert('config') if ! $::auth->assert('bank_transaction',1);
+}
+
 1;
index 3090182..1aaec79 100644 (file)
@@ -3,10 +3,12 @@ package SL::Controller::CsvImport::Base;
 use strict;
 
 use English qw(-no_match_vars);
+use List::Util qw(min);
 use List::MoreUtils qw(pairwise any);
 
 use SL::Helper::Csv;
 
+use SL::DB;
 use SL::DB::BankAccount;
 use SL::DB::Customer;
 use SL::DB::Language;
@@ -21,7 +23,7 @@ use parent qw(Rose::Object);
 use Rose::Object::MakeMethods::Generic
 (
  scalar                  => [ qw(controller file csv test_run save_with_cascade) ],
- 'scalar --get_set_init' => [ qw(profile displayable_columns existing_objects class manager_class cvar_columns all_cvar_configs all_languages payment_terms_by delivery_terms_by all_bank_accounts all_vc vc_by clone_methods) ],
+ 'scalar --get_set_init' => [ qw(profile displayable_columns existing_objects class manager_class cvar_columns all_cvar_configs all_languages payment_terms_by delivery_terms_by all_bank_accounts all_vc vc_by vc_counts_by clone_methods) ],
 );
 
 sub run {
@@ -32,9 +34,9 @@ sub run {
   $self->controller->track_progress(phase => 'parsing csv', progress => 0);
 
   my $profile = $self->profile;
-  $self->csv(SL::Helper::Csv->new(file                   => $self->file->file_name,
+  $self->csv(SL::Helper::Csv->new(file                   => ('SCALAR' eq ref $self->file)? $self->file: $self->file->file_name,
                                   encoding               => $self->controller->profile->get('charset'),
-                                  profile                => [{ profile => $profile, class => $self->class }],
+                                  profile                => [{ profile => $profile, class => $self->class, mapping => $self->controller->mappings_for_profile }],
                                   ignore_unknown_columns => 1,
                                   strict_profile         => 1,
                                   case_insensitive_header => 1,
@@ -43,8 +45,8 @@ sub run {
 
   $self->controller->track_progress(progress => 10);
 
-  my $old_numberformat      = $::myconfig{numberformat};
-  $::myconfig{numberformat} = $self->controller->profile->get('numberformat');
+  local $::myconfig{numberformat} = $self->controller->profile->get('numberformat');
+  local $::myconfig{dateformat}   = $self->controller->profile->get('dateformat');
 
   $self->csv->parse;
 
@@ -54,9 +56,9 @@ sub run {
 
   return if ( !$self->csv->header || $self->csv->errors );
 
-  my $headers         = { headers => [ grep { $profile->{$_} } @{ $self->csv->header } ] };
-  $headers->{methods} = [ map { $profile->{$_} } @{ $headers->{headers} } ];
-  $headers->{used}    = { map { ($_ => 1) }      @{ $headers->{headers} } };
+  my $headers         = { headers => [ grep { $self->csv->dispatcher->is_known($_, 0) } @{ $self->csv->header } ] };
+  $headers->{methods} = [ map { $_->{path} } @{ $self->csv->specs->[0] } ];
+  $headers->{used}    = { map { ($_ => 1) }  @{ $headers->{headers} } };
   $self->controller->headers($headers);
   $self->controller->raw_data_headers({ used => { }, headers => [ ] });
   $self->controller->info_headers({ used => { }, headers => [ ] });
@@ -81,8 +83,6 @@ sub run {
   $self->fix_field_lengths;
 
   $self->controller->track_progress(progress => 100);
-
-  $::myconfig{numberformat} = $old_numberformat;
 }
 
 sub add_columns {
@@ -186,10 +186,28 @@ sub init_vc_by {
                     vendors   => { map { ( $_->vendornumber   => $_ ) } @{ $self->all_vc->{vendors}   } } );
   my %by_name   = ( customers => { map { ( $_->name           => $_ ) } @{ $self->all_vc->{customers} } },
                     vendors   => { map { ( $_->name           => $_ ) } @{ $self->all_vc->{vendors}   } } );
+  my %by_gln    = ( customers => { map { ( $_->gln            => $_ ) } grep $_->gln, @{ $self->all_vc->{customers} } },
+                    vendors   => { map { ( $_->gln            => $_ ) } grep $_->gln, @{ $self->all_vc->{vendors}   } } );
 
   return { id     => \%by_id,
            number => \%by_number,
-           name   => \%by_name,   };
+           name   => \%by_name,
+           gln    => \%by_gln };
+}
+
+sub init_vc_counts_by {
+  my ($self) = @_;
+
+  my $vc_counts_by = {};
+
+  $vc_counts_by->{number}->{customers}->{$_->customernumber}++ for @{ $self->all_vc->{customers} };
+  $vc_counts_by->{number}->{vendors}->  {$_->vendornumber}++   for @{ $self->all_vc->{vendors} };
+  $vc_counts_by->{name}->  {customers}->{$_->name}++           for @{ $self->all_vc->{customers} };
+  $vc_counts_by->{name}->  {vendors}->  {$_->name}++           for @{ $self->all_vc->{vendors} };
+  $vc_counts_by->{gln}->   {customers}->{$_->gln}++            for grep $_->gln, @{ $self->all_vc->{customers} };
+  $vc_counts_by->{gln}->   {vendors}->  {$_->gln}++            for grep $_->gln, @{ $self->all_vc->{vendors} };
+
+  return $vc_counts_by;
 }
 
 sub check_vc {
@@ -199,36 +217,90 @@ sub check_vc {
     $entry->{object}->$id_column(undef) if !$self->vc_by->{id}->{ $entry->{object}->$id_column };
   }
 
+  my $is_ambiguous;
   if (!$entry->{object}->$id_column) {
-    my $vc = $self->vc_by->{number}->{customers}->{ $entry->{raw_data}->{customernumber} }
-          || $self->vc_by->{number}->{vendors}->{   $entry->{raw_data}->{vendornumber}   };
+    my $vc;
+    if ($entry->{raw_data}->{customernumber}) {
+      $vc = $self->vc_by->{number}->{customers}->{ $entry->{raw_data}->{customernumber} };
+      if ($vc && $self->vc_counts_by->{number}->{customers}->{ $entry->{raw_data}->{customernumber} } > 1) {
+        $vc = undef;
+        $is_ambiguous = 1;
+      }
+    } elsif ($entry->{raw_data}->{vendornumber}) {
+      $vc = $self->vc_by->{number}->{vendors}->{ $entry->{raw_data}->{vendornumber} };
+      if ($vc && $self->vc_counts_by->{number}->{vendors}->{ $entry->{raw_data}->{vendornumber} } > 1) {
+        $vc = undef;
+        $is_ambiguous = 1;
+      }
+    }
+
     $entry->{object}->$id_column($vc->id) if $vc;
   }
 
   if (!$entry->{object}->$id_column) {
-    my $vc = $self->vc_by->{name}->{customers}->{ $entry->{raw_data}->{customer} }
-          || $self->vc_by->{name}->{vendors}->{   $entry->{raw_data}->{vendor}   };
+    my $vc;
+    if ($entry->{raw_data}->{customer}) {
+      $vc = $self->vc_by->{name}->{customers}->{ $entry->{raw_data}->{customer} };
+      if ($vc && $self->vc_counts_by->{name}->{customers}->{ $entry->{raw_data}->{customer} } > 1) {
+        $vc = undef;
+        $is_ambiguous = 1;
+      }
+    } elsif ($entry->{raw_data}->{vendor}) {
+      $vc = $self->vc_by->{name}->{vendors}->{ $entry->{raw_data}->{vendor} };
+      if ($vc && $self->vc_counts_by->{name}->{vendors}->{ $entry->{raw_data}->{vendor} } > 1) {
+        $vc = undef;
+        $is_ambiguous = 1;
+      }
+    }
+
+    $entry->{object}->$id_column($vc->id) if $vc;
+  }
+
+  if (!$entry->{object}->$id_column) {
+    my $vc;
+    if ($entry->{raw_data}->{customer_gln}) {
+      $vc = $self->vc_by->{gln}->{customers}->{ $entry->{raw_data}->{customer_gln} };
+      if ($vc && $self->vc_counts_by->{gln}->{customers}->{ $entry->{raw_data}->{customer_gln} } > 1) {
+        $vc = undef;
+        $is_ambiguous = 1;
+      }
+    } elsif ($entry->{raw_data}->{vendor_gln}) {
+      $vc = $self->vc_by->{gln}->{vendors}->{ $entry->{raw_data}->{vendor_gln} };
+      if ($vc && $self->vc_counts_by->{gln}->{vendors}->{ $entry->{raw_data}->{vendor_gln} } > 1) {
+        $vc = undef;
+        $is_ambiguous = 1;
+      }
+    }
     $entry->{object}->$id_column($vc->id) if $vc;
   }
 
   if ($entry->{object}->$id_column) {
     $entry->{info_data}->{vc_name} = $self->vc_by->{id}->{ $entry->{object}->$id_column }->name;
   } else {
-    push @{ $entry->{errors} }, $::locale->text('Error: Customer/vendor not found');
+    if ($is_ambiguous) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Customer/vendor is ambiguous');
+    } else {
+      push @{ $entry->{errors} }, $::locale->text('Error: Customer/vendor not found');
+    }
   }
 }
 
 sub handle_cvars {
   my ($self, $entry) = @_;
 
+  my $object = $entry->{object_to_save} || $entry->{object};
+  return unless $object->can('cvars_by_config');
+
   my %type_to_column = ( text      => 'text_value',
                          textfield => 'text_value',
+                         htmlfield => 'text_value',
                          select    => 'text_value',
                          date      => 'timestamp_value_as_date',
                          timestamp => 'timestamp_value_as_date',
                          number    => 'number_value_as_number',
                          bool      => 'bool_value' );
 
+  # autovivify all cvars (cvars_by_config will do that for us)
   my @cvars;
   my %changed_cvars;
   foreach my $config (@{ $self->all_cvar_configs }) {
@@ -243,16 +315,13 @@ sub handle_cvars {
   }
 
   # merge existing with new cvars. swap every existing with the imported one, push the rest
-  if (@cvars) {
-    my @orig_cvars = ($entry->{object_to_save} || $entry->{object})->custom_variables;
-    for (@orig_cvars) {
-      $_ = $changed_cvars{ $_->config->name } if $changed_cvars{ $_->config->name };
-      delete $changed_cvars{ $_->config->name };
-    }
-    push @orig_cvars, values %changed_cvars;
-
-    $entry->{object}->custom_variables(\@orig_cvars);
+  my @orig_cvars = @{ $object->cvars_by_config };
+  for (@orig_cvars) {
+    $_ = $changed_cvars{ $_->config->name } if $changed_cvars{ $_->config->name };
+    delete $changed_cvars{ $_->config->name };
   }
+  push @orig_cvars, values %changed_cvars;
+  $object->custom_variables(\@orig_cvars);
 }
 
 sub init_profile {
@@ -337,6 +406,10 @@ sub check_objects {
 sub check_duplicates {
 }
 
+sub check_auth {
+  $::auth->assert('config');
+}
+
 sub check_std_duplicates {
   my $self = shift;
 
@@ -459,32 +532,35 @@ sub save_objects {
 
   $self->controller->track_progress(phase => 'saving data', progress => 0); # scale from 45..95%;
 
-  my $dbh = $data->[0]{object}->db;
-
-  $dbh->begin_work;
-  foreach my $entry_index (0 .. $#$data) {
-    my $entry = $data->[$entry_index];
-    next if @{ $entry->{errors} };
-
-    my $object = $entry->{object_to_save} || $entry->{object};
-
-    my $ret;
-    if (!eval { $ret = $object->save(cascade => !!$self->save_with_cascade()); 1 }) {
-      push @{ $entry->{errors} }, $::locale->text('Error when saving: #1', $EVAL_ERROR);
-    } elsif ( !$ret ) {
-      push @{ $entry->{errors} }, $::locale->text('Error when saving: #1', $object->db->error);
-    } else {
-      $self->_save_history($object);
-      $self->controller->num_imported($self->controller->num_imported + 1);
-    }
-  } continue {
-    if ($entry_index % 100 == 0) {
-      $dbh->commit;
-      $self->controller->track_progress(progress => $entry_index/scalar(@$data) * 100); # scale from 45..95%;
-      $dbh->begin_work;
-    }
+  my $last_index = $#$data;
+  my $chunk_size = 100;      # one transaction and progress update every 100 objects
+
+  for my $chunk (0 .. $last_index / $chunk_size) {
+    $self->controller->track_progress(progress => ($chunk_size * $chunk)/scalar(@$data) * 100); # scale from 45..95%;
+    SL::DB->client->with_transaction(sub {
+      foreach my $entry_index ($chunk_size * $chunk .. min( $last_index, $chunk_size * ($chunk + 1) - 1 )) {
+        my $entry = $data->[$entry_index];
+
+        my $object = $entry->{object_to_save} || $entry->{object};
+        $self->save_additions_always($object);
+
+        next if @{ $entry->{errors} };
+
+        my $ret;
+        if (!eval { $ret = $object->save(cascade => !!$self->save_with_cascade()); 1 }) {
+          push @{ $entry->{errors} }, $::locale->text('Error when saving: #1', $EVAL_ERROR);
+        } elsif ( !$ret ) {
+          push @{ $entry->{errors} }, $::locale->text('Error when saving: #1', $object->db->error);
+        } else {
+          $self->_save_history($object);
+          $self->save_additions($object);
+          $self->controller->num_imported($self->controller->num_imported + 1);
+        }
+      }
+      1;
+    }) or do { die SL::DB->client->error };
   }
-  $dbh->commit;
+  $self->controller->track_progress(progress => 100);
 }
 
 sub field_lengths {
@@ -519,18 +595,47 @@ sub clean_fields {
   return @cleaned_fields;
 }
 
+sub save_additions {
+  my ($self, $object) = @_;
+
+  # Can be overridden by derived specialized importer classes to save
+  # additional tables (e.g. record links).
+  # This sub is called after the object is saved successfully in an transaction.
+
+  return;
+}
+
+sub save_additions_always {
+  my ($self, $object) = @_;
+
+  # Can be overridden by derived specialized importer classes to save
+  # additional tables always.
+  # This sub is called before the object is saved. Therefore this
+  # hook will always be executed whether or not the import entry can be saved successfully.
+
+  return;
+}
+
+
 sub _save_history {
   my ($self, $object) = @_;
 
-  if (any { $_ eq $self->controller->{type} } qw(parts customers_vendors orders)) {
+  if (any { $self->controller->{type} && $_ eq $self->controller->{type} } qw(parts customers_vendors orders delivery_orders ar_transactions)) {
     my $snumbers = $self->controller->{type} eq 'parts'             ? 'partnumber_' . $object->partnumber
                  : $self->controller->{type} eq 'customers_vendors' ?
                      ($self->table eq 'customer' ? 'customernumber_' . $object->customernumber : 'vendornumber_' . $object->vendornumber)
                  : $self->controller->{type} eq 'orders'            ? 'ordnumber_' . $object->ordnumber
+                 : $self->controller->{type} eq 'delivery_orders'   ? 'donumber_'  . $object->donumber
+                 : $self->controller->{type} eq 'ar_transactions'   ? 'invnumber_' . $object->invnumber
                  : '';
 
-    my $what_done = $self->controller->{type} eq 'orders' ? 'sales_order'
-                  : '';
+    my $what_done = '';
+    if ($self->controller->{type} eq 'orders') {
+      $what_done = $object->customer_id ? 'sales_order' : 'purchase_order';
+    }
+    if ($self->controller->{type} eq 'delivery_orders') {
+      $what_done = $object->customer_id ? 'sales_delivery_order' : 'purchase_delivery_order';
+    }
 
     SL::DB::History->new(
       trans_id    => $object->id,
index 973a4ae..9ddb86b 100644 (file)
@@ -2,7 +2,7 @@ package SL::Controller::CsvImport::BaseMulti;
 
 use strict;
 
-use List::MoreUtils qw(pairwise);
+use List::MoreUtils qw(pairwise firstidx);
 
 use SL::Helper::Csv;
 
@@ -22,7 +22,7 @@ sub run {
 
   my $profile = $self->profile;
 
-  $self->csv(SL::Helper::Csv->new(file                    => $self->file->file_name,
+  $self->csv(SL::Helper::Csv->new(file                   => ('SCALAR' eq ref $self->file)? $self->file: $self->file->file_name,
                                   encoding                => $self->controller->profile->get('charset'),
                                   profile                 => $profile,
                                   ignore_unknown_columns  => 1,
@@ -94,6 +94,17 @@ sub run {
   $::myconfig{numberformat} = $old_numberformat;
 }
 
+sub init_manager_class {
+  my ($self) = @_;
+
+  my @manager_classes;
+  foreach my $class (@{ $self->class }) {
+    $class =~ m/^SL::DB::(.+)/;
+    push @manager_classes, "SL::DB::Manager::" . $1;
+  }
+  $self->manager_class(\@manager_classes);
+}
+
 sub add_columns {
   my ($self, $row_ident, @columns) = @_;
 
@@ -158,8 +169,12 @@ sub init_cvar_columns_by {
 sub handle_cvars {
   my ($self, $entry, %params) = @_;
 
+  return if @{ $entry->{errors} };
+  return unless $entry->{object}->can('cvars_by_config');
+
   my %type_to_column = ( text      => 'text_value',
                          textfield => 'text_value',
+                         htmlfield => 'text_value',
                          select    => 'text_value',
                          date      => 'timestamp_value_as_date',
                          timestamp => 'timestamp_value_as_date',
@@ -167,13 +182,21 @@ sub handle_cvars {
                          bool      => 'bool_value' );
 
   $params{sub_module} ||= '';
+
+  # autovivify all cvars (cvars_by_config will do that for us)
   my @cvars;
+  @cvars = @{ $entry->{object}->cvars_by_config };
+
   foreach my $config (@{ $self->cvar_configs_by->{row_ident}->{$entry->{raw_data}->{datatype}} }) {
     next unless exists $entry->{raw_data}->{ "cvar_" . $config->name };
     my $value  = $entry->{raw_data}->{ "cvar_" . $config->name };
     my $column = $type_to_column{ $config->type } || die "Program logic error: unknown custom variable storage type";
 
-    push @cvars, SL::DB::CustomVariable->new(config_id => $config->id, $column => $value, sub_module => $params{sub_module});
+    my $cvar = SL::DB::CustomVariable->new(config_id => $config->id, $column => $value, sub_module => $params{sub_module});
+
+    # replace autovivified cvar by new one
+    my $idx = firstidx { $_->config_id == $config->id } @cvars;
+    $cvars[$idx] = $cvar if -1 != $idx;
   }
 
   $entry->{object}->custom_variables(\@cvars) if @cvars;
@@ -187,6 +210,18 @@ sub init_profile {
     eval "require " . $class;
 
     my %unwanted = map { ( $_ => 1 ) } (qw(itime mtime), map { $_->name } @{ $class->meta->primary_key_columns });
+
+    # TODO: exceptions for AccTransaction and Invoice wh
+    if ( $class =~ m/^SL::DB::AccTransaction/ ) {
+      my %unwanted_acc_trans = map { ( $_ => 1 ) } (qw(acc_trans_id trans_id cleared fx_transaction ob_transaction cb_transaction itime mtime chart_link tax_id description gldate memo source transdate), map { $_->name } @{ $class->meta->primary_key_columns });
+      @unwanted{keys %unwanted_acc_trans} = values %unwanted_acc_trans;
+    };
+    if ( $class =~ m/^SL::DB::Invoice/ ) {
+      # remove fields that aren't needed / shouldn't be set for ar transaction
+      my %unwanted_ar = map { ( $_ => 1 ) } (qw(closed currency currency_id datepaid dunning_config_id gldate invnumber_for_credit_note invoice marge_percent marge_total amount netamount paid shippingpoint shipto_id shipvia storno storno_id type cp_id), map { $_->name } @{ $class->meta->primary_key_columns });
+      @unwanted{keys %unwanted_ar} = values %unwanted_ar;
+    };
+
     my %prof;
     $prof{datatype} = '';
     for my $col ($class->meta->columns) {
@@ -268,7 +303,7 @@ sub fix_field_lengths {
 
   my %field_lengths_by_ri = $self->field_lengths;
   foreach my $entry (@{ $self->controller->data }) {
-    next unless @{ $entry->{errors} };
+    next unless defined $entry->{errors} && @{ $entry->{errors} };
     my %field_lengths = %{ $field_lengths_by_ri{ $entry->{raw_data}->{datatype} } };
     map { $entry->{object}->$_(substr($entry->{object}->$_, 0, $field_lengths{$_})) if $entry->{object}->$_ } keys %field_lengths;
   }
@@ -279,4 +314,3 @@ sub fix_field_lengths {
 sub is_multiplexed { 1 }
 
 1;
-
index adaa85c..81fa0e8 100644 (file)
@@ -158,8 +158,10 @@ sub setup_displayable_columns {
 
                                  { name => 'customer',       description => $::locale->text('Customer (name)')               },
                                  { name => 'customernumber', description => $::locale->text('Customer Number')               },
+                                 { name => 'customer_gln',   description => $::locale->text('Customer GLN')                  },
                                  { name => 'vendor',         description => $::locale->text('Vendor (name)')                 },
                                  { name => 'vendornumber',   description => $::locale->text('Vendor Number')                 },
+                                 { name => 'vendor_gln',     description => $::locale->text('Vendor GLN')                    },
                                 );
 }
 
index 4f78721..2f8b15c 100644 (file)
@@ -7,6 +7,7 @@ use SL::Controller::CsvImport::Helper::Consistency;
 use SL::DB::Business;
 use SL::DB::CustomVariable;
 use SL::DB::CustomVariableConfig;
+use SL::DB::Employee;
 use SL::DB::PaymentTerm;
 use SL::TransNumber;
 
@@ -53,7 +54,7 @@ sub init_languages_by {
 sub init_salesmen_by {
   my ($self) = @_;
 
-  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ SL::DB::Manager::Employee->get_all } } ) } qw(id name) };
+  return { map { my $col = $_; ( $col => { map { ( lc($_->$col) => $_ ) } @{ SL::DB::Manager::Employee->get_all } } ) } qw(id name login) };
 }
 
 sub check_objects {
@@ -67,7 +68,7 @@ sub check_objects {
   my %vcs_by_number = map { ( $_->$numbercolumn => $_ ) } @{ $self->existing_objects };
   my $methods       = $self->controller->headers->{methods};
 
-  my $i;
+  my $i = 0;
   my $num_data = scalar @{ $self->controller->data };
   foreach my $entry (@{ $self->controller->data }) {
     $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
@@ -81,17 +82,16 @@ sub check_objects {
     $self->check_taxzone($entry,  take_default => 1);
     $self->check_currency($entry, take_default => 1);
     $self->check_salesman($entry);
-    $self->handle_cvars($entry);
 
     next if @{ $entry->{errors} };
 
-    my @cleaned_fields = $self->clean_fields(qr{[\r\n]}, $object, qw(name department_1 department_2 street zipcode city country contact phone fax homepage email cc bcc
+    my @cleaned_fields = $self->clean_fields(qr{[\r\n]}, $object, qw(name department_1 department_2 street zipcode city country gln contact phone fax homepage email cc bcc
                                                                      taxnumber account_number bank_code bank username greeting taxzone));
 
     push @{ $entry->{information} }, $::locale->text('Illegal characters have been removed from the following fields: #1', join(', ', @cleaned_fields))
       if @cleaned_fields;
 
-    my $existing_vc = $vcs_by_number{ $object->$numbercolumn };
+    my $existing_vc = $object->$numbercolumn ? $vcs_by_number{ $object->$numbercolumn } : undef;
     if (!$existing_vc) {
       $vcs_by_number{ $object->$numbercolumn } = $object if $object->$numbercolumn;
 
@@ -109,6 +109,9 @@ sub check_objects {
     } else {
       $object->$numbercolumn('####');
     }
+
+    $self->handle_cvars($entry);
+
   } continue {
     $i++;
   }
@@ -215,8 +218,9 @@ sub check_salesman {
   }
 
   # Map name to ID if given.
-  if (!$object->salesman_id && $entry->{raw_data}->{salesman}) {
-    my $salesman = $self->salesmen_by->{name}->{ $entry->{raw_data}->{salesman} };
+  if (!$object->salesman_id && ($entry->{raw_data}->{salesman} || $entry->{raw_data}->{salesman_login})) {
+    my $salesman = $self->salesmen_by->{name} ->{ lc($entry->{raw_data}->{salesman}) }
+                // $self->salesmen_by->{login}->{ lc($entry->{raw_data}->{salesman_login}) };
 
     if (!$salesman) {
       push @{ $entry->{errors} }, $::locale->text('Error: Invalid salesman');
@@ -236,21 +240,10 @@ sub save_objects {
   my ($self, %params) = @_;
 
   my $numbercolumn   = $self->table . 'number';
-  my $with_number    = [ grep { $_->{object}->$numbercolumn ne '####' } @{ $self->controller->data } ];
-  my $without_number = [ grep { $_->{object}->$numbercolumn eq '####' } @{ $self->controller->data } ];
-
-  foreach my $entry (@{$with_number}, @{$without_number}) {
-    my $object = $entry->{object};
+  my $with_number    = [ grep { ($_->{object}->$numbercolumn || '') ne '####' } @{ $self->controller->data } ];
+  my $without_number = [ grep { ($_->{object}->$numbercolumn || '') eq '####' } @{ $self->controller->data } ];
 
-    my $number = SL::TransNumber->new(type        => $self->table(),
-                                      number      => $object->$numbercolumn(),
-                                      business_id => $object->business_id(),
-                                      save        => 1);
-
-    if ( $object->$numbercolumn eq '####' || !$number->is_unique() ) {
-      $object->$numbercolumn($number->create_unique());
-    }
-  }
+  $_->{object}->$numbercolumn('') for @{ $without_number };
 
   $self->SUPER::save_objects(data => $with_number);
   $self->SUPER::save_objects(data => $without_number);
@@ -261,7 +254,7 @@ sub init_profile {
 
   my $profile = $self->SUPER::init_profile;
   delete @{$profile}{qw(business datevexport language payment delivery_term taxincluded terms)};
-  delete @{$profile}{qw(salesman salesman_id)}    if $::instance_conf->get_vertreter;
+  delete @{$profile}{qw(salesman salesman_id salesman_login)} if $::instance_conf->get_vertreter;
 
   return $profile;
 }
@@ -296,10 +289,11 @@ sub setup_displayable_columns {
                                  { name => 'discount',          description => $::locale->text('Discount')                        },
                                  { name => 'email',             description => $::locale->text('E-mail')                          },
                                  { name => 'fax',               description => $::locale->text('Fax')                             },
+                                 { name => 'gln',               description => $::locale->text('GLN')                             },
                                  { name => 'greeting',          description => $::locale->text('Greeting')                        },
                                  { name => 'homepage',          description => $::locale->text('Homepage')                        },
                                  { name => 'iban',              description => $::locale->text('IBAN')                            },
-                                 { name => 'klass',             description => $::locale->text('Preisklasse')                     },
+                                 { name => 'pricegroup_id',     description => $::locale->text('Price group (database ID)')       },
                                  { name => 'language_id',       description => $::locale->text('Language (database ID)')          },
                                  { name => 'language',          description => $::locale->text('Language (name)')                 },
                                  { name => 'name',              description => $::locale->text('Name')                            },
@@ -319,10 +313,10 @@ sub setup_displayable_columns {
                                 );
 
   if (!$::instance_conf->get_vertreter) {
-    $self->add_displayable_columns({ name => 'salesman_id', description => $::locale->text('Salesman (database ID)') });
-    $self->add_displayable_columns({ name => 'salesman',    description => $::locale->text('Salesman') });
+    $self->add_displayable_columns({ name => 'salesman',       description => $::locale->text('Salesman') },
+                                   { name => 'salesman_id',    description => $::locale->text('Salesman (database ID)') },
+                                   { name => 'salesman_login', description => $::locale->text('Salesman (login)') });
   }
-
 }
 
 # TODO:
diff --git a/SL/Controller/CsvImport/DeliveryOrder.pm b/SL/Controller/CsvImport/DeliveryOrder.pm
new file mode 100644 (file)
index 0000000..2e5a9e9
--- /dev/null
@@ -0,0 +1,1259 @@
+package SL::Controller::CsvImport::DeliveryOrder;
+
+
+use strict;
+
+use List::Util qw(first);
+use List::MoreUtils qw(any none uniq);
+use DateTime;
+
+use SL::Controller::CsvImport::Helper::Consistency;
+use SL::DB::DeliveryOrder;
+use SL::DB::DeliveryOrder::TypeData qw(:types);
+use SL::DB::DeliveryOrderItem;
+use SL::DB::DeliveryOrderItemsStock;
+use SL::DB::Part;
+use SL::DB::PaymentTerm;
+use SL::DB::Contact;
+use SL::DB::PriceFactor;
+use SL::DB::Pricegroup;
+use SL::DB::Shipto;
+use SL::DB::Unit;
+use SL::DB::Inventory;
+use SL::DB::TransferType;
+use SL::DBUtils;
+use SL::Helper::ShippedQty;
+use SL::PriceSource;
+use SL::TransNumber;
+use SL::Util qw(trim);
+
+use parent qw(SL::Controller::CsvImport::BaseMulti);
+
+
+use Rose::Object::MakeMethods::Generic
+(
+ 'scalar --get_set_init' => [ qw(settings languages_by all_parts parts_by part_counts_by
+                                 contacts_by ct_shiptos_by
+                                 price_factors_by pricegroups_by units_by
+                                 warehouses_by bins_by transfer_types_by) ],
+);
+
+
+sub init_class {
+  my ($self) = @_;
+  $self->class(['SL::DB::DeliveryOrder', 'SL::DB::DeliveryOrderItem', 'SL::DB::DeliveryOrderItemsStock']);
+}
+
+sub set_profile_defaults {
+  my ($self) = @_;
+
+  $self->controller->profile->_set_defaults(
+    order_column         => $::locale->text('DeliveryOrder'),
+    item_column          => $::locale->text('OrderItem'),
+    stock_column         => $::locale->text('StockInfo'),
+    ignore_faulty_positions => 0,
+  );
+};
+
+sub init_settings {
+  my ($self) = @_;
+
+  return { map { ( $_ => $self->controller->profile->get($_) ) } qw(order_column item_column stock_column ignore_faulty_positions) };
+}
+
+sub init_cvar_configs_by {
+  my ($self) = @_;
+
+  my $item_cvar_configs = SL::DB::Manager::CustomVariableConfig->get_all(where => [ module => 'IC' ]);
+  $item_cvar_configs = [grep { $_->has_flag('editable') } @{ $item_cvar_configs }];
+
+  my $ccb;
+  $ccb->{class}->{$self->class->[0]}        = [];
+  $ccb->{class}->{$self->class->[1]}        = $item_cvar_configs;
+  $ccb->{class}->{$self->class->[2]}        = [];
+  $ccb->{row_ident}->{$self->_order_column} = [];
+  $ccb->{row_ident}->{$self->_item_column}  = $item_cvar_configs;
+  $ccb->{row_ident}->{$self->_stock_column} = [];
+
+  return $ccb;
+}
+
+sub init_profile {
+  my ($self) = @_;
+
+  my $profile = $self->SUPER::init_profile;
+
+  # SUPER::init_profile sets row_ident to the translated class name
+  # overwrite it with the user specified settings
+  foreach my $p (@{ $profile }) {
+    $p->{row_ident} = $self->_order_column if $p->{class} eq $self->class->[0];
+    $p->{row_ident} = $self->_item_column  if $p->{class} eq $self->class->[1];
+    $p->{row_ident} = $self->_stock_column if $p->{class} eq $self->class->[2];
+  }
+
+  foreach my $p (@{ $profile }) {
+    my $prof = $p->{profile};
+    if ($p->{row_ident} eq $self->_order_column) {
+      # no need to handle
+      delete @{$prof}{qw(oreqnumber)};
+    }
+    if ($p->{row_ident} eq $self->_item_column) {
+      # no need to handle
+      delete @{$prof}{qw(delivery_order_id)};
+    }
+    if ($p->{row_ident} eq $self->_stock_column) {
+      # no need to handle
+      delete @{$prof}{qw(delivery_order_item_id)};
+      delete @{$prof}{qw(bestbefore)} if !$::instance_conf->get_show_bestbefore;
+    }
+  }
+
+  return $profile;
+}
+
+sub init_existing_objects {
+  my ($self) = @_;
+
+  # only use objects of main class (the first one)
+  eval "require " . $self->class->[0];
+  $self->existing_objects($self->manager_class->[0]->get_all);
+}
+
+sub get_duplicate_check_fields {
+  return {
+    donumber => {
+      label     => $::locale->text('Delivery Order Number'),
+      default   => 1,
+      std_check => 1,
+      maker     => sub {
+        my ($object, $worker) = @_;
+        return if ref $object ne $worker->class->[0];
+        return $object->donumber;
+      },
+    },
+  };
+}
+
+sub check_std_duplicates {
+  my $self = shift;
+
+  my $duplicates = {};
+
+  my $all_fields = $self->get_duplicate_check_fields();
+
+  foreach my $key (keys(%{ $all_fields })) {
+    if ( $self->controller->profile->get('duplicates_'. $key) && (!exists($all_fields->{$key}->{std_check}) || $all_fields->{$key}->{std_check} )  ) {
+      $duplicates->{$key} = {};
+    }
+  }
+
+  my @duplicates_keys = keys(%{ $duplicates });
+
+  if ( !scalar(@duplicates_keys) ) {
+    return;
+  }
+
+  if ( $self->controller->profile->get('duplicates') eq 'check_db' ) {
+    foreach my $object (@{ $self->existing_objects }) {
+      foreach my $key (@duplicates_keys) {
+        my $value = exists($all_fields->{$key}->{maker}) ? $all_fields->{$key}->{maker}->($object, $self) : $object->$key;
+        $duplicates->{$key}->{$value} = 'db';
+      }
+    }
+  }
+
+  # only check order rows
+  foreach my $entry (@{ $self->controller->data }) {
+    if ($entry->{raw_data}->{datatype} ne $self->_order_column) {
+      next;
+    }
+    if ( @{ $entry->{errors} } ) {
+      next;
+    }
+
+    my $object = $entry->{object};
+
+    foreach my $key (@duplicates_keys) {
+      my $value = exists($all_fields->{$key}->{maker}) ? $all_fields->{$key}->{maker}->($object, $self) : $object->$key;
+
+      if ( exists($duplicates->{$key}->{$value}) ) {
+        push(@{ $entry->{errors} }, $duplicates->{$key}->{$value} eq 'db' ? $::locale->text('Duplicate in database') : $::locale->text('Duplicate in CSV file'));
+        last;
+      } else {
+        $duplicates->{$key}->{$value} = 'csv';
+      }
+
+    }
+  }
+}
+
+sub setup_displayable_columns {
+  my ($self) = @_;
+
+  $self->SUPER::setup_displayable_columns;
+
+  $self->add_cvar_columns_to_displayable_columns($self->_order_column);
+
+  $self->add_displayable_columns($self->_order_column,
+                                 { name => 'datatype',                description => $self->_order_column . ' [1]'                            },
+                                 { name => 'closed',                  description => $::locale->text('Closed')                                },
+                                 { name => 'contact',                 description => $::locale->text('Contact Person (name)')                 },
+                                 { name => 'cp_id',                   description => $::locale->text('Contact Person (database ID)')          },
+                                 { name => 'currency',                description => $::locale->text('Currency')                              },
+                                 { name => 'currency_id',             description => $::locale->text('Currency (database ID)')                },
+                                 { name => 'customer',                description => $::locale->text('Customer (name)')                       },
+                                 { name => 'customernumber',          description => $::locale->text('Customer Number')                       },
+                                 { name => 'customer_id',             description => $::locale->text('Customer (database ID)')                },
+                                 { name => 'cusordnumber',            description => $::locale->text('Customer Order Number')                 },
+                                 { name => 'delivered',               description => $::locale->text('Delivered')                             },
+                                 { name => 'delivery_term',           description => $::locale->text('Delivery terms (name)')                 },
+                                 { name => 'delivery_term_id',        description => $::locale->text('Delivery terms (database ID)')          },
+                                 { name => 'department_id',           description => $::locale->text('Department (database ID)')              },
+                                 { name => 'department',              description => $::locale->text('Department (description)')              },
+                                 { name => 'donumber',                description => $::locale->text('Delivery Order Number')                 },
+                                 { name => 'employee_id',             description => $::locale->text('Employee (database ID)')                },
+                                 { name => 'globalproject',           description => $::locale->text('Document Project (description)')        },
+                                 { name => 'globalprojectnumber',     description => $::locale->text('Document Project (number)')             },
+                                 { name => 'globalproject_id',        description => $::locale->text('Document Project (database ID)')        },
+                                 { name => 'intnotes',                description => $::locale->text('Internal Notes')                        },
+                                 { name => 'language',                description => $::locale->text('Language (name)')                       },
+                                 { name => 'language_id',             description => $::locale->text('Language (database ID)')                },
+                                 { name => 'notes',                   description => $::locale->text('Notes')                                 },
+                                 { name => 'order_type',              description => $::locale->text('Delivery Order Type')                   },
+                                 { name => 'ordnumber',               description => $::locale->text('Order Number')                          },
+                                 { name => 'payment',                 description => $::locale->text('Payment terms (name)')                  },
+                                 { name => 'payment_id',              description => $::locale->text('Payment terms (database ID)')           },
+                                 { name => 'reqdate',                 description => $::locale->text('Reqdate')                               },
+                                 { name => 'salesman_id',             description => $::locale->text('Salesman (database ID)')                },
+                                 { name => 'shippingpoint',           description => $::locale->text('Shipping Point')                        },
+                                 { name => 'shipvia',                 description => $::locale->text('Ship via')                              },
+                                 { name => 'shipto_id',               description => $::locale->text('Ship to (database ID)')                 },
+                                 { name => 'taxincluded',             description => $::locale->text('Tax Included')                          },
+                                 { name => 'taxzone',                 description => $::locale->text('Tax zone (description)')                },
+                                 { name => 'taxzone_id',              description => $::locale->text('Tax zone (database ID)')                },
+                                 { name => 'transaction_description', description => $::locale->text('Transaction description')               },
+                                 { name => 'transdate',               description => $::locale->text('Order Date')                            },
+                                 { name => 'vendor',                  description => $::locale->text('Vendor (name)')                         },
+                                 { name => 'vendornumber',            description => $::locale->text('Vendor Number')                         },
+                                 { name => 'vendor_id',               description => $::locale->text('Vendor (database ID)')                  },
+                                );
+
+  $self->add_cvar_columns_to_displayable_columns($self->_item_column);
+
+  $self->add_displayable_columns($self->_item_column,
+                                 { name => 'datatype',        description => $self->_item_column . ' [1]'                  },
+                                 { name => 'cusordnumber',    description => $::locale->text('Customer Order Number')      },
+                                 { name => 'description',     description => $::locale->text('Description')                },
+                                 { name => 'discount',        description => $::locale->text('Discount')                   },
+                                 { name => 'lastcost',        description => $::locale->text('Lastcost')                   },
+                                 { name => 'longdescription', description => $::locale->text('Long Description')           },
+                                 { name => 'ordnumber',       description => $::locale->text('Order Number')               },
+                                 { name => 'partnumber',      description => $::locale->text('Part Number')                },
+                                 { name => 'parts_id',        description => $::locale->text('Part (database ID)')         },
+                                 { name => 'position',        description => $::locale->text('position')                   },
+                                 { name => 'price_factor',    description => $::locale->text('Price factor (name)')        },
+                                 { name => 'price_factor_id', description => $::locale->text('Price factor (database ID)') },
+                                 { name => 'pricegroup',      description => $::locale->text('Price group (name)')         },
+                                 { name => 'pricegroup_id',   description => $::locale->text('Price group (database ID)')  },
+                                 { name => 'project',         description => $::locale->text('Project (description)')      },
+                                 { name => 'projectnumber',   description => $::locale->text('Project (number)')           },
+                                 { name => 'project_id',      description => $::locale->text('Project (database ID)')      },
+                                 { name => 'qty',             description => $::locale->text('Quantity')                   },
+                                 { name => 'reqdate',         description => $::locale->text('Reqdate')                    },
+                                 { name => 'sellprice',       description => $::locale->text('Sellprice')                  },
+                                 { name => 'serialnumber',    description => $::locale->text('Serial No.')                 },
+                                 { name => 'transdate',       description => $::locale->text('Order Date')                 },
+                                 { name => 'unit',            description => $::locale->text('Unit')                       },
+                                );
+
+  $self->add_cvar_columns_to_displayable_columns($self->_stock_column);
+
+  $self->add_displayable_columns($self->_stock_column,
+                                 { name => 'datatype',     description => $self->_stock_column . ' [1]'              },
+                                 { name => 'warehouse',    description => $::locale->text('Warehouse')               },
+                                 { name => 'warehouse_id', description => $::locale->text('Warehouse (database ID)') },
+                                 { name => 'bin',          description => $::locale->text('Bin')                     },
+                                 { name => 'bin_id',       description => $::locale->text('Bin (database ID)')       },
+                                 { name => 'chargenumber', description => $::locale->text('Charge number')           },
+                                 { name => 'qty',          description => $::locale->text('Quantity')                },
+                                 { name => 'unit',         description => $::locale->text('Unit')                    },
+                                );
+  if ($::instance_conf->get_show_bestbefore) {
+    $self->add_displayable_columns($self->_stock_column,
+                                   { name => 'bestbefore', description => $::locale->text('Best Before') });
+  }
+}
+
+
+sub init_languages_by {
+  my ($self) = @_;
+
+  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_languages } } ) } qw(id description article_code) };
+}
+
+sub init_all_parts {
+  my ($self) = @_;
+
+  return SL::DB::Manager::Part->get_all(where => [or => [ obsolete => 0, obsolete => undef ]]);
+}
+
+sub init_parts_by {
+  my ($self) = @_;
+
+  return { map { my $col = $_; ( $col => { map { ( trim($_->$col) => $_ ) } @{ $self->all_parts } } ) } qw(id partnumber ean description) };
+}
+
+sub init_part_counts_by {
+  my ($self) = @_;
+
+  my $part_counts_by;
+
+  $part_counts_by->{ean}->        {trim($_->ean)}++         for @{ $self->all_parts };
+  $part_counts_by->{description}->{trim($_->description)}++ for @{ $self->all_parts };
+
+  return $part_counts_by;
+}
+
+sub init_contacts_by {
+  my ($self) = @_;
+
+  my $all_contacts = SL::DB::Manager::Contact->get_all;
+
+  my $cby;
+  # by customer/vendor id  _and_  contact person id
+  $cby->{'cp_cv_id+cp_id'}   = { map { ( $_->cp_cv_id . '+' . $_->cp_id   => $_ ) } @{ $all_contacts } };
+  # by customer/vendor id  _and_  contact person name
+  $cby->{'cp_cv_id+cp_name'} = { map { ( $_->cp_cv_id . '+' . $_->cp_name => $_ ) } @{ $all_contacts } };
+
+  return $cby;
+}
+
+sub init_ct_shiptos_by {
+  my ($self) = @_;
+
+  my $all_ct_shiptos = SL::DB::Manager::Shipto->get_all(query => [module => 'CT']);
+
+  my $sby;
+  # by trans_id  _and_  shipto_id
+  $sby->{'trans_id+shipto_id'} = { map { ( $_->trans_id . '+' . $_->shipto_id => $_ ) } @{ $all_ct_shiptos } };
+
+  return $sby;
+}
+
+sub init_price_factors_by {
+  my ($self) = @_;
+
+  my $all_price_factors = SL::DB::Manager::PriceFactor->get_all;
+  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_price_factors } } ) } qw(id description) };
+}
+
+sub init_pricegroups_by {
+  my ($self) = @_;
+
+  my $all_pricegroups = SL::DB::Manager::Pricegroup->get_all;
+  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_pricegroups } } ) } qw(id pricegroup) };
+}
+
+sub init_units_by {
+  my ($self) = @_;
+
+  my $all_units = SL::DB::Manager::Unit->get_all;
+  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_units } } ) } qw(name) };
+}
+
+sub init_warehouses_by {
+  my ($self) = @_;
+
+  my $all_warehouses = SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef ]]);
+  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_warehouses } } ) } qw(id description) };
+}
+
+sub init_bins_by {
+  my ($self) = @_;
+
+  my $all_bins = SL::DB::Manager::Bin->get_all();
+  my $bins_by;
+  $bins_by->{_wh_id_and_id_ident()}          = { map { ( _wh_id_and_id_maker($_->warehouse_id, $_->id)                   => $_ ) } @{ $all_bins } };
+  $bins_by->{_wh_id_and_description_ident()} = { map { ( _wh_id_and_description_maker($_->warehouse_id, $_->description) => $_ ) } @{ $all_bins } };
+
+  return $bins_by;
+}
+
+sub init_transfer_types_by {
+  my ($self) = @_;
+
+  my $all_transfer_types = SL::DB::Manager::TransferType->get_all();
+  my $transfer_types_by;
+  $transfer_types_by->{_transfer_type_dir_and_description_ident()} = {
+    map { ( _transfer_type_dir_and_description_maker($_->direction, $_->description) => $_ ) } @{ $all_transfer_types }
+  };
+
+  return $transfer_types_by;
+}
+
+sub check_objects {
+  my ($self) = @_;
+
+  $self->controller->track_progress(phase => 'building data', progress => 0);
+
+  my $i = 0;
+  my $num_data = scalar @{ $self->controller->data };
+  my $order_entry;
+  my $item_entry;
+  foreach my $entry (@{ $self->controller->data }) {
+    $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
+
+    $entry->{info_data}->{datatype} = $entry->{raw_data}->{datatype};
+
+    if ($entry->{raw_data}->{datatype} eq $self->_order_column) {
+      $self->handle_order($entry);
+      $order_entry = $entry;
+    } elsif ($entry->{raw_data}->{datatype} eq $self->_item_column && $entry->{object}->can('part')) {
+      $self->handle_item($entry, $order_entry);
+      $item_entry = $entry;
+    } elsif ($entry->{raw_data}->{datatype} eq $self->_stock_column) {
+      $self->handle_stock($entry, $item_entry, $order_entry);
+      push @{ $order_entry->{errors} }, $::locale->text('Error: Stock problem') if scalar(@{$entry->{errors}}) > 0;
+    } else {
+      $order_entry = undef;
+      $item_entry  = undef;
+    }
+
+    $self->handle_cvars($entry, sub_module => 'delivery_order_items');
+
+  } continue {
+    $i++;
+  }
+
+  $self->add_info_columns($self->_order_column,
+                          { header => $::locale->text('Data type'), method => 'datatype' });
+  $self->add_info_columns($self->_item_column,
+                          { header => $::locale->text('Data type'), method => 'datatype' });
+  $self->add_info_columns($self->_stock_column,
+                          { header => $::locale->text('Data type'), method => 'datatype' });
+
+  $self->add_info_columns($self->_order_column,
+                          { header => $::locale->text('Customer/Vendor'), method => 'vc_name' });
+  # Todo: access via ->[0] ok? Better: search first order column and use this
+  $self->add_columns($self->_order_column,
+                     map { "${_}_id" } grep { exists $self->controller->data->[0]->{raw_data}->{$_} } qw(payment delivery_term language department globalproject taxzone cp currency));
+  $self->add_columns($self->_order_column, 'globalproject_id') if exists $self->controller->data->[0]->{raw_data}->{globalprojectnumber};
+  $self->add_columns($self->_order_column, 'cp_id')            if exists $self->controller->data->[0]->{raw_data}->{contact};
+
+  $self->add_info_columns($self->_item_column,
+                          { header => $::locale->text('Part Number'), method => 'partnumber' });
+  # Todo: access via ->[1] ok? Better: search first item column and use this
+  $self->add_columns($self->_item_column,
+                     map { "${_}_id" } grep { exists $self->controller->data->[1]->{raw_data}->{$_} } qw(project price_factor pricegroup));
+  $self->add_columns($self->_item_column, 'project_id') if exists $self->controller->data->[1]->{raw_data}->{projectnumber};
+
+  $self->add_cvar_raw_data_columns();
+
+
+  # Check overall qtys for sales delivery orders, because they are
+  # stocked out in the end and a stock underrun can occure.
+  # Todo: let it work even with bestbefore turned off.
+  $order_entry = undef;
+  $item_entry  = undef;
+  my %wanted_qtys_by_part_wh_bin_charge_bestbefore;
+  my %stock_entries_with_part_wh_bin_charge_bestbefore;
+  my %order_entries_with_part_wh_bin_charge_bestbefore;
+  foreach my $entry (@{ $self->controller->data }) {
+    if ($entry->{raw_data}->{datatype} eq $self->_order_column) {
+      if (scalar(@{ $entry->{errors} }) || !$entry->{object}->is_sales) {
+        $order_entry = undef;
+        $item_entry  = undef;
+        next;
+      }
+      $order_entry = $entry;
+
+    } elsif (defined $order_entry && $entry->{raw_data}->{datatype} eq $self->_item_column) {
+      if (scalar(@{ $entry->{errors} })) {
+        $item_entry = undef;
+        next;
+      }
+      $item_entry = $entry;
+
+    } elsif (defined $item_entry && $entry->{raw_data}->{datatype} eq $self->_stock_column) {
+      my $object = $entry->{object};
+      my $key = join('+',
+                     $item_entry->{object}->parts_id,
+                     $object->warehouse_id,
+                     $object->bin_id,
+                     $object->chargenumber,
+                     $object->bestbefore);
+      $wanted_qtys_by_part_wh_bin_charge_bestbefore{$key} += $object->qty;
+      push @{$order_entries_with_part_wh_bin_charge_bestbefore{$key}}, $order_entry;
+      push @{$stock_entries_with_part_wh_bin_charge_bestbefore{$key}}, $entry;
+    }
+  }
+
+  foreach my $key (keys %wanted_qtys_by_part_wh_bin_charge_bestbefore) {
+    my ($parts_id, $wh_id, $bin_id, $chargenumber, $bestbefore) = split '\+', $key;
+    my $qty = $self->get_stocked_qty($parts_id, $wh_id, $bin_id, $chargenumber, $bestbefore);
+    if ($wanted_qtys_by_part_wh_bin_charge_bestbefore{$key} > $qty) {
+
+      foreach my $stock_entry (@{ $stock_entries_with_part_wh_bin_charge_bestbefore{$key} }) {
+        push @{ $stock_entry->{errors} }, $::locale->text('Error: Stocking out would result in stock underrun');
+      }
+
+      foreach my $order_entry (uniq @{ $order_entries_with_part_wh_bin_charge_bestbefore{$key} }) {
+        my $part            = $self->parts_by->{id}->{$parts_id}->displayable_name;
+        my $stock           = $self->bins_by->{_wh_id_and_id_ident()}->{_wh_id_and_id_maker($wh_id, $bin_id)}->full_description;
+        my $bestbefore_obj  = $::locale->parse_date_to_object($bestbefore, dateformat=>'yyyy-mm-dd');
+        my $bestbefore_text = $bestbefore_obj? $::locale->parse_date_to_object($bestbefore_obj, dateformat=>'yyyy-mm-dd')->to_kivitendo: '-';
+        my $wanted_qty      = $wanted_qtys_by_part_wh_bin_charge_bestbefore{$key};
+        my $details_text    = sprintf('%s (%s / %s / %s): %s > %s',
+                                      $part,
+                                      $stock,
+                                      $chargenumber,
+                                      $bestbefore_text,
+                                      $::form->format_amount(\%::myconfig, $wanted_qty,  2),
+                                      $::form->format_amount(\%::myconfig, $qty, 2));
+        push @{ $order_entry->{errors} }, $::locale->text('Error: Stocking out would result in stock underrun: #1', $details_text);
+      }
+
+    }
+  }
+
+}
+
+sub handle_order {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  $object->orderitems([]);
+
+  $self->handle_order_sources($entry);
+  my $first_source_order = $object->{source_orders}->[0];
+
+  my $vc_obj;
+  if (any { $entry->{raw_data}->{$_} } qw(customer customernumber customer_id)) {
+    $self->check_vc($entry, 'customer_id');
+    $vc_obj = SL::DB::Customer->new(id => $object->customer_id)->load if $object->customer_id;
+
+  } elsif (any { $entry->{raw_data}->{$_} } qw(vendor vendornumber vendor_id)) {
+    $self->check_vc($entry, 'vendor_id');
+    $vc_obj = SL::DB::Vendor->new(id => $object->vendor_id)->load if $object->vendor_id;
+
+  } else {
+    # customer / vendor from (first) source order if not given
+    if ($first_source_order) {
+      if ($first_source_order->customer) {
+        $vc_obj = $first_source_order->customer;
+        $object->customer($first_source_order->customer);
+      } elsif ($first_source_order->vendor) {
+        $vc_obj = $first_source_order->vendor;
+        $object->vendor($first_source_order->vendor);
+      }
+    }
+  }
+
+  if (!$vc_obj) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Customer/vendor missing');
+  }
+
+  $self->handle_type($entry);
+  $self->check_contact($entry);
+  $self->check_language($entry);
+  $self->check_payment($entry);
+  $self->check_delivery_term($entry);
+  $self->check_department($entry);
+  $self->check_project($entry, global => 1);
+  $self->check_ct_shipto($entry);
+  $self->check_taxzone($entry);
+  $self->check_currency($entry, take_default => 0);
+
+  # copy from (first) source order if not given
+  # if no source order, then copy some values from customer/vendor
+  if ($first_source_order) {
+    foreach (qw(cusordnumber notes intnotes shippingpoint shipvia
+                transaction_description currency_id delivery_term_id
+                department_id language_id payment_id globalproject_id shipto_id
+                taxzone_id)) {
+      $object->$_($first_source_order->$_) unless $object->$_;
+    }
+  } elsif ($vc_obj) {
+    foreach (qw(currency_id delivery_term_id language_id payment_id taxzone_id)) {
+      $object->$_($vc_obj->$_) unless $object->$_;
+    }
+    $object->intnotes($vc_obj->notes) unless $object->intnotes;
+  }
+
+  $self->handle_salesman($entry);
+  $self->handle_employee($entry);
+}
+
+sub handle_item {
+  my ($self, $entry, $order_entry) = @_;
+
+  return unless $order_entry;
+
+  my $order_obj = $order_entry->{object};
+  my $object    = $entry->{object};
+  $object->delivery_order_stock_entries([]);
+
+  if (!$self->check_part($entry)) {
+    if ($self->controller->profile->get('ignore_faulty_positions')) {
+      push @{ $order_entry->{information} }, $::locale->text('Warning: Faulty position ignored');
+    } else {
+      push @{ $order_entry->{errors} }, $::locale->text('Error: Faulty position in this delivery order');
+    }
+    return;
+  }
+
+  $order_obj->add_items($object);
+
+  my $part_obj = SL::DB::Part->new(id => $object->parts_id)->load;
+
+  $self->handle_item_source($entry, $order_entry);
+  $object->position($object->{source_item}->position) if $object->{source_item};
+
+  $self->handle_unit($entry);
+
+  # copy from part if not given
+  $object->description($part_obj->description) unless $object->description;
+  $object->longdescription($part_obj->notes)   unless $object->longdescription;
+  $object->lastcost($part_obj->lastcost)       unless defined $object->lastcost;
+
+  $self->check_project($entry, global => 0);
+  $self->check_price_factor($entry);
+  $self->check_pricegroup($entry);
+
+  $self->handle_sellprice($entry, $order_entry);
+  $self->handle_discount($entry, $order_entry);
+
+  push @{ $order_entry->{errors} }, $::locale->text('Error: Faulty position in this delivery order') if scalar(@{$entry->{errors}}) > 0;
+}
+
+sub handle_stock {
+  my ($self, $entry, $item_entry, $order_entry) = @_;
+
+  return unless $item_entry;
+
+  my $item_obj  = $item_entry->{object};
+  return unless $item_obj->part;
+
+  my $order_obj = $order_entry->{object};
+  my $object    = $entry->{object};
+
+  $item_obj->add_delivery_order_stock_entries($object);
+
+  $self->check_warehouse($entry);
+  $self->check_bin($entry);
+
+  $self->handle_unit($entry, $item_obj->part);
+
+  # check if enough is stocked
+  # not necessary, because overall stock underrun is checked later
+  # if ($order_obj->is_sales) {
+  #   my $stocked_qty = $self->get_stocked_qty($item_obj->parts_id,
+  #                                            $object->warehouse_id,
+  #                                            $object->bin_id,
+  #                                            $object->chargenumber,
+  #                                            $object->bestbefore);
+  #   if ($stocked_qty < $object->qty) {
+  #     push @{ $entry->{errors} }, $::locale->text('Error: Not enough parts in stock');
+  #   }
+  # }
+
+  my ($stock_info_entry, $part) = @_;
+
+  # Todo: option: should stock?
+  if (1) {
+    my $tt_key = $order_obj->is_sales
+               ? _transfer_type_dir_and_description_maker('out', 'shipped')
+               : _transfer_type_dir_and_description_maker('in', 'stock');
+    my $trans_type_id = $self->transfer_types_by->{_transfer_type_dir_and_description_ident()}{$tt_key}->id;
+
+    my $qty = $order_obj->is_sales ? -1*($object->qty) : $object->qty;
+    my $inventory = SL::DB::Inventory->new(
+      parts_id      => $item_obj->parts_id,
+      warehouse_id  => $object->warehouse_id,
+      bin_id        => $object->bin_id,
+      trans_type_id => $trans_type_id,
+      qty           => $qty,
+      chargenumber  => $object->chargenumber,
+      employee_id   => $order_obj->employee_id,
+      shippingdate  => ($order_obj->reqdate || DateTime->today_local),
+      comment       => $order_obj->transaction_description,
+      project_id    => ($order_obj->globalproject_id || $item_obj->project_id),
+    );
+    $inventory->bestbefore($object->bestbefore) if $::instance_conf->get_show_bestbefore;
+    $object->{inventory_obj} = $inventory;
+    $order_obj->delivered(1);
+  }
+}
+
+sub handle_type {
+  my ($self, $entry) = @_;
+
+  if (!exists $entry->{raw_data}->{order_type}) {
+    # if no type is present - set to sales delivery order or purchase delivery
+    # order depending on is_sales or customer/vendor
+
+    $entry->{object}->order_type(
+      $entry->{object}->customer_id  ? SALES_DELIVERY_ORDER_TYPE :
+      $entry->{object}->vendor_id    ? PURCHASE_DELIVERY_ORDER_TYPE :
+      $entry->{raw_data}->{is_sales} ? SALES_DELIVERY_ORDER_TYPE :
+                                       PURCHASE_DELIVERY_ORDER_TYPE
+    );
+  }
+}
+
+sub handle_order_sources {
+  my ($self, $entry) = @_;
+
+  my $record = $entry->{object};
+
+  $record->{source_orders} = [];
+  return $record->{source_orders} if !$record->ordnumber;
+
+  my @order_numbers = split ' ', $record->ordnumber;
+
+  my $orders = SL::DB::Manager::Order->get_all(where => [ordnumber => \@order_numbers]);
+
+  if (scalar @$orders == 0) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Source order not found');
+  } elsif (scalar @$orders > 1) {
+    push @{ $entry->{errors} }, $::locale->text('Error: More than one source order found');
+  }
+
+  foreach my $order (@$orders) {
+    $self->{remaining_source_qtys_by_item_id} = { map { $_->id => $_->qty } @{ $order->items } };
+  }
+
+  $record->{source_orders} = $orders;
+}
+
+sub handle_item_source {
+  my ($self, $entry, $record_entry) = @_;
+
+  my $item   = $entry->{object};
+  my $record = $record_entry->{object};
+
+  return if !@{ $record->{source_orders} };
+
+  # Todo: units?
+
+  foreach my $order (@{ $record->{source_orders} }) {
+    # First: Excact matches and source order position is still complete.
+    $item->{source_item} = first {
+         $item->parts_id                                     == $_->parts_id
+      && $item->qty                                          == $_->qty
+      && $self->{remaining_source_qtys_by_item_id}->{$_->id} == $_->qty
+    } @{ $order->items_sorted };
+    if ($item->{source_item}) {
+      $self->{remaining_source_qtys_by_item_id}->{$item->{source_item}->id} -= $item->qty;
+      last;
+    }
+
+    # Second: Smallest remaining order qty greater or equal delivery order qty.
+    $item->{source_item} = first {
+         $item->parts_id                                     == $_->parts_id
+      && $self->{remaining_source_qtys_by_item_id}->{$_->id} >= $item->qty
+    } sort { $self->{remaining_source_qtys_by_item_id}->{$a->id} <=> $self->{remaining_source_qtys_by_item_id}->{$b->id} } @{ $order->items_sorted };
+    if ($item->{source_item}) {
+      $self->{remaining_source_qtys_by_item_id}->{$item->{source_item}->id} -= $item->qty;
+      last;
+    }
+
+    # Last: Overdelivery?
+    # $item->{source_item} = first {
+    #      $item->parts_id == $_->parts_id
+    # } @{ $order->items_sorted };
+    # if ($item->{source_item}) {
+    #   $self->{remaining_source_qtys_by_item_id}->{$item->{source_item}->id} -= $item->qty;
+    #   last;
+    # }
+  }
+}
+
+sub handle_unit {
+  my ($self, $entry, $part) = @_;
+
+  my $object = $entry->{object};
+
+  $part ||= $object->part;
+
+  # Set unit from part if not given.
+  if (!$object->unit) {
+    $object->unit($part->unit);
+    return 1;
+  }
+
+  # Check whether or not unit is valid.
+  if ($object->unit && !$self->units_by->{name}->{ $object->unit }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid unit');
+    return 0;
+  }
+
+  # Check whether unit is convertible to parts unit
+  if (none { $object->unit eq $_ } map { $_->name } @{ $part->unit_obj->convertible_units }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid unit');
+    return 0;
+  }
+
+  return 1;
+}
+
+sub handle_sellprice {
+  my ($self, $entry, $record_entry) = @_;
+
+  my $item   = $entry->{object};
+  my $record = $record_entry->{object};
+
+  return if !$record->customervendor;
+
+  # If sellprice is given, set price source to pricegroup if given or to none.
+  if (exists $entry->{raw_data}->{sellprice}) {
+    my $price_source      = SL::PriceSource->new(record_item => $item, record => $record);
+    my $price_source_spec = $item->pricegroup_id ? 'pricegroup' . '/' . $item->pricegroup_id : '';
+    my $price             = $price_source->price_from_source($price_source_spec);
+    $item->active_price_source($price->source);
+
+  } else {
+
+    if ($item->{source_item}) {
+      # Set sellprice from source order item if not given. Convert with respect to unit.
+      my $sellprice = $item->{source_item}->sellprice;
+      if ($item->unit ne $item->{source_item}->unit) {
+        $sellprice = $item->unit_obj->convert_to($sellprice, $item->{source_item}->unit_obj);
+      }
+      $item->sellprice($sellprice);
+      $item->active_price_source($item->{source_item}->active_price_source);
+
+    } else {
+      # Set sellprice the best price of price source
+      my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
+      my $price        = $price_source->best_price;
+      if ($price) {
+        $item->sellprice($price->price);
+        $item->active_price_source($price->source);
+      } else {
+        $item->sellprice(0);
+        $item->active_price_source($price_source->price_from_source('')->source);
+      }
+    }
+  }
+}
+
+sub handle_discount {
+  my ($self, $entry, $record_entry) = @_;
+
+  my $item   = $entry->{object};
+  my $record = $record_entry->{object};
+
+  return if !$record->customervendor;
+
+  # If discount is given, set discount to none.
+  if (exists $entry->{raw_data}->{discount}) {
+    my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
+    my $discount     = $price_source->price_from_source('');
+    $item->active_discount_source($discount->source);
+
+  } else {
+
+    if ($item->{source_item}) {
+      # Set discount from source order item if not given.
+      $item->discount($item->{source_item}->discount);
+      $item->active_discount_source($item->{source_item}->active_discount_source);
+
+    } else {
+      # Set discount the best discount of price source
+      my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
+      my $discount     = $price_source->best_discount;
+      if ($discount) {
+        $item->discount($discount->discount);
+        $item->active_discount_source($discount->source);
+      } else {
+        $item->discount(0);
+        $item->active_discount_source($price_source->discount_from_source('')->source);
+      }
+    }
+  }
+}
+
+sub check_contact {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  my $cp_cv_id = $object->customer_id || $object->vendor_id;
+  return 0 unless $cp_cv_id;
+
+  # Check whether or not contact ID is valid.
+  if ($object->cp_id && !$self->contacts_by->{'cp_cv_id+cp_id'}->{ $cp_cv_id . '+' . $object->cp_id }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid contact');
+    return 0;
+  }
+
+  # Map name to ID if given.
+  if (!$object->cp_id && $entry->{raw_data}->{contact}) {
+    my $cp = $self->contacts_by->{'cp_cv_id+cp_name'}->{ $cp_cv_id . '+' . $entry->{raw_data}->{contact} };
+    if (!$cp) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid contact');
+      return 0;
+    }
+
+    $object->cp_id($cp->cp_id);
+  }
+
+  if ($object->cp_id) {
+    $entry->{info_data}->{contact} = $self->contacts_by->{'cp_cv_id+cp_id'}->{ $cp_cv_id . '+' . $object->cp_id }->cp_name;
+  }
+
+  return 1;
+}
+
+sub check_language {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  # Check whether or not language ID is valid.
+  if ($object->language_id && !$self->languages_by->{id}->{ $object->language_id }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid language');
+    return 0;
+  }
+
+  # Map name to ID if given.
+  if (!$object->language_id && $entry->{raw_data}->{language}) {
+    my $language = $self->languages_by->{description}->{  $entry->{raw_data}->{language} }
+                || $self->languages_by->{article_code}->{ $entry->{raw_data}->{language} };
+
+    if (!$language) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid language');
+      return 0;
+    }
+
+    $object->language_id($language->id);
+  }
+
+  if ($object->language_id) {
+    $entry->{info_data}->{language} = $self->languages_by->{id}->{ $object->language_id }->description;
+  }
+
+  return 1;
+}
+
+sub check_ct_shipto {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  my $trans_id = $object->customer_id || $object->vendor_id;
+  return 0 unless $trans_id;
+
+  # Check whether or not shipto ID is valid.
+  if ($object->shipto_id && !$self->ct_shiptos_by->{'trans_id+shipto_id'}->{ $trans_id . '+' . $object->shipto_id }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid shipto');
+    return 0;
+  }
+
+  return 1;
+}
+
+sub check_part {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+  my $is_ambiguous;
+
+  # Check whether or not part ID is valid.
+  if ($object->parts_id && !$self->parts_by->{id}->{ $object->parts_id }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
+    return 0;
+  }
+
+  # Map number to ID if given.
+  if (!$object->parts_id && $entry->{raw_data}->{partnumber}) {
+    my $part = $self->parts_by->{partnumber}->{ trim($entry->{raw_data}->{partnumber}) };
+    if (!$part) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
+      return 0;
+    }
+
+    $object->parts_id($part->id);
+  }
+
+  # Map description to ID if given.
+  if (!$object->parts_id && $entry->{raw_data}->{description}) {
+    my $part = $self->parts_by->{description}->{ trim($entry->{raw_data}->{description}) };
+    if (!$part) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
+      return 0;
+    }
+
+    if ($self->part_counts_by->{description}->{ trim($entry->{raw_data}->{description}) } > 1) {
+      $is_ambiguous = 1;
+    } else {
+      $object->parts_id($part->id);
+    }
+  }
+
+  # Map ean to ID if given.
+  if (!$object->parts_id && $entry->{raw_data}->{ean}) {
+    my $part = $self->parts_by->{ean}->{ trim($entry->{raw_data}->{ean}) };
+    if (!$part) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
+      return 0;
+    }
+
+    if ($self->part_counts_by->{ean}->{ trim($entry->{raw_data}->{ean}) } > 1) {
+      $is_ambiguous = 1;
+    } else {
+      $object->parts_id($part->id);
+    }
+  }
+
+  if ($object->parts_id) {
+    $entry->{info_data}->{partnumber} = $self->parts_by->{id}->{ $object->parts_id }->partnumber;
+  } else {
+    if ($is_ambiguous) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Part is ambiguous');
+    } else {
+      push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
+    }
+    return 0;
+  }
+
+  return 1;
+}
+
+sub check_price_factor {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  # Check whether or not price_factor ID is valid.
+  if ($object->price_factor_id && !$self->price_factors_by->{id}->{ $object->price_factor_id }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid price factor');
+    return 0;
+  }
+
+  # Map description to ID if given.
+  if (!$object->price_factor_id && $entry->{raw_data}->{price_factor}) {
+    my $price_factor = $self->price_factors_by->{description}->{ $entry->{raw_data}->{price_factor} };
+    if (!$price_factor) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid price factor');
+      return 0;
+    }
+
+    $object->price_factor_id($price_factor->id);
+  }
+
+  return 1;
+}
+
+sub check_pricegroup {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  # Check whether or not pricegroup ID is valid.
+  if ($object->pricegroup_id && !$self->pricegroups_by->{id}->{ $object->pricegroup_id }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid price group');
+    return 0;
+  }
+
+  # Map pricegroup to ID if given.
+  if (!$object->pricegroup_id && $entry->{raw_data}->{pricegroup}) {
+    my $pricegroup = $self->pricegroups_by->{pricegroup}->{ $entry->{raw_data}->{pricegroup} };
+    if (!$pricegroup) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid price group');
+      return 0;
+    }
+
+    $object->pricegroup_id($pricegroup->id);
+  }
+
+  return 1;
+}
+
+sub check_warehouse {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  # Check whether or not warehouse ID is valid.
+  if ($object->warehouse_id && !$self->warehouses_by->{id}->{ $object->warehouse_id }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
+    return 0;
+  }
+
+  # Map description to ID if given.
+  if (!$object->warehouse_id && $entry->{raw_data}->{warehouse}) {
+    my $wh = $self->warehouses_by->{description}->{ $entry->{raw_data}->{warehouse} };
+    if (!$wh) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
+      return 0;
+    }
+
+    $object->warehouse_id($wh->id);
+  }
+
+  if ($object->warehouse_id) {
+    $entry->{info_data}->{warehouse} = $self->warehouses_by->{id}->{ $object->warehouse_id }->description;
+  } else {
+    push @{ $entry->{errors} }, $::locale->text('Error: Warehouse not found');
+    return 0;
+  }
+
+  return 1;
+}
+
+# Check bin for given warehouse, so check_warehouse must be called first.
+sub check_bin {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  # Check whether or not bin ID is valid.
+  if ($object->bin_id && !$self->bins_by->{_wh_id_and_id_ident()}->{ _wh_id_and_id_maker($object->warehouse_id, $object->bin_id) }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
+    return 0;
+  }
+
+  # Map description to ID if given.
+  if (!$object->bin_id && $entry->{raw_data}->{bin}) {
+    my $bin = $self->bins_by->{_wh_id_and_description_ident()}->{ _wh_id_and_description_maker($object->warehouse_id, $entry->{raw_data}->{bin}) };
+    if (!$bin) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
+      return 0;
+    }
+
+    $object->bin_id($bin->id);
+  }
+
+  if ($object->bin_id) {
+    $entry->{info_data}->{bin} = $self->bins_by->{_wh_id_and_id_ident()}->{ _wh_id_and_id_maker($object->warehouse_id, $object->bin_id) }->description;
+  } else {
+    push @{ $entry->{errors} }, $::locale->text('Error: Bin not found');
+    return 0;
+  }
+
+  return 1;
+}
+
+sub save_additions {
+  my ($self, $object) = @_;
+
+  # record links
+  my $orders = delete $object->{source_orders};
+
+  if (scalar(@$orders)) {
+
+    $_->link_to_record($object) for @$orders;
+
+    foreach my $item (@{ $object->items }) {
+      my $orderitem = delete $item->{source_item};
+      $orderitem->link_to_record($item) if $orderitem;
+    }
+  }
+
+  # delivery order for all positions created?
+  if (scalar(@$orders)) {
+    SL::Helper::ShippedQty->new->calculate($orders)->write_to_objects;
+    $_->update_attributes(delivered => $_->delivered) for @{ $orders };
+  }
+
+  # inventory (or use WH->transfer?)
+  foreach my $item (@{ $object->items }) {
+    foreach my $stock_info (@{ $item->delivery_order_stock_entries }) {
+      my $inventory  = delete $stock_info->{inventory_obj};
+      next if !$inventory;
+      my ($trans_id) = selectrow_query($::form, $object->db->dbh, qq|SELECT nextval('id')|);
+      $inventory->trans_id($trans_id);
+      $inventory->oe_id($object->id);
+      $inventory->delivery_order_items_stock_id($stock_info->id);
+      $inventory->save;
+    }
+  }
+}
+
+sub save_objects {
+  my ($self, %params) = @_;
+
+  # Collect orders without errors to save.
+  my $entries_to_save = [];
+  foreach my $entry (@{ $self->controller->data }) {
+    next if $entry->{raw_data}->{datatype} ne $self->_order_column;
+    next if @{ $entry->{errors} };
+
+    push @{ $entries_to_save }, $entry;
+  }
+
+  $self->SUPER::save_objects(data => $entries_to_save);
+}
+
+sub get_stocked_qty {
+  my ($self, $parts_id, $wh_id, $bin_id, $chargenumber, $bestbefore) = @_;
+
+  my $key = join '+', $parts_id, $wh_id, $bin_id, $chargenumber, $bestbefore;
+  return $self->{stocked_qty}->{$key} if exists $self->{stocked_qty}->{$key};
+
+  my $bestbefore_filter  = '';
+  my $bestbefore_val_cnt = 0;
+  if ($::instance_conf->get_show_bestbefore) {
+    $bestbefore_filter  = ($bestbefore) ? 'AND bestbefore = ?' : 'AND bestbefore IS NULL';
+    $bestbefore_val_cnt = ($bestbefore) ? 1                    : 0;
+  }
+
+  my $query = <<SQL;
+    SELECT sum(qty) FROM inventory
+      WHERE parts_id = ? AND warehouse_id = ? AND bin_id = ? AND chargenumber = ? $bestbefore_filter
+      GROUP BY warehouse_id, bin_id, chargenumber
+SQL
+
+  my @values = ($parts_id,
+                $wh_id,
+                $bin_id,
+                $chargenumber);
+  push @values, $bestbefore if $bestbefore_val_cnt;
+
+  my $dbh = $self->controller->data->[0]{object}->db->dbh;
+  my ($stocked_qty) = selectrow_query($::form, $dbh, $query, @values);
+
+  $self->{stocked_qty}->{$key} = $stocked_qty;
+  return $stocked_qty;
+}
+
+sub _wh_id_and_description_ident {
+  return 'wh_id+description';
+}
+
+sub _wh_id_and_description_maker {
+  return join '+', $_[0], $_[1]
+}
+
+sub _wh_id_and_id_ident {
+  return 'wh_id+id';
+}
+
+sub _wh_id_and_id_maker {
+  return join '+', $_[0], $_[1]
+}
+
+sub _transfer_type_dir_and_description_ident {
+  return 'dir+description';
+}
+
+sub _transfer_type_dir_and_description_maker {
+  return join '+', $_[0], $_[1]
+}
+
+sub _order_column {
+  $_[0]->settings->{'order_column'}
+}
+
+sub _item_column {
+  $_[0]->settings->{'item_column'}
+}
+
+sub _stock_column {
+  $_[0]->settings->{'stock_column'}
+}
+
+1;
index 82ec563..5538a73 100644 (file)
@@ -2,14 +2,17 @@ package SL::Controller::CsvImport::Helper::Consistency;
 
 use strict;
 
+use Data::Dumper;
 use SL::DB::Default;
 use SL::DB::Currency;
 use SL::DB::TaxZone;
+use SL::DB::Project;
+use SL::DB::Department;
 
 use SL::Helper::Csv::Error;
 
 use parent qw(Exporter);
-our @EXPORT = qw(check_currency check_taxzone);
+our @EXPORT = qw(check_currency check_taxzone check_project check_department check_customer_vendor handle_salesman handle_employee);
 
 #
 # public functions
@@ -92,20 +95,142 @@ sub check_taxzone {
   if (!$object->taxzone_id && $params{take_default}) {
     # my $default_id = $self->settings->{'default_taxzone'};
     my $default_id = $self->controller->profile->get('default_taxzone');
+    $default_id ||= _default_taxzone_id($self);
     $object->taxzone_id($default_id);
     # check if default taxzone_id is valid just to be sure
     if (! _taxzones_by($self)->{id}->{ $object->taxzone_id }) {
       push @{ $entry->{errors} }, $::locale->text('Error with default taxzone');
       return 0;
-    };
+    }
   };
 
   # for the order import at this stage $object->taxzone_id may still not be
-  # defined, in this case the customer/vendor taxzone will be used.
+  # defined, in this case the customer/vendor taxzone will be used later
+  if ( defined $object->taxzone_id ) {
+    $entry->{info_data}->{taxzone} = _taxzones_by($self)->{id}->{ $object->taxzone_id }->description;
+  };
+
+  return 1;
+}
+
+sub check_project {
+  my ($self, $entry, %params) = @_;
+
+  my $id_column          = ($params{global} ? 'global' : '') . 'project_id';
+  my $number_column      = ($params{global} ? 'global' : '') . 'projectnumber';
+  my $description_column = ($params{global} ? 'global' : '') . 'project';
+
+  my $object = $entry->{object};
+
+  # Check whether or not project ID is valid.
+  if ($object->$id_column) {
+    if (! _projects_by($self)->{id}->{ $object->$id_column }) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid project');
+      return 0;
+    } else {
+      $entry->{info_data}->{$number_column} = _projects_by($self)->{id}->{ $object->$id_column }->description;
+    };
+  }
+
+  my $proj;
+  # Map number to ID if given.
+  if (!$object->$id_column && $entry->{raw_data}->{$number_column}) {
+    $proj = _projects_by($self)->{projectnumber}->{ $entry->{raw_data}->{$number_column} };
+    if (!$proj) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid project');
+      return 0;
+    }
+
+    $object->$id_column($proj->id);
+  }
+
+  # Map description to ID if given.
+  if (!$object->$id_column && $entry->{raw_data}->{$description_column}) {
+    $proj = _projects_by($self)->{description}->{ $entry->{raw_data}->{$description_column} };
+    if (!$proj) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid project');
+      return 0;
+    }
+
+    $object->$id_column($proj->id);
+  }
+
+  if ( $proj ) {
+    $entry->{info_data}->{"$description_column"} = $proj->description;
+    $entry->{info_data}->{"$number_column"}      = $proj->projectnumber;
+  };
+
+  return 1;
+}
+
+sub check_department {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  # Check whether or not department ID was assigned and is valid.
+  if ($object->department_id) {
+    if (!_departments_by($self)->{id}->{ $object->department_id }) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid department');
+      return 0;
+    } else {
+      # add department description as well, more feedback for user
+      $entry->{info_data}->{department} = _departments_by($self)->{id}->{ $object->department_id }->description;
+    };
+  }
+
+  # Map department description to ID if given.
+  if (!$object->department_id && $entry->{raw_data}->{department}) {
+    $entry->{info_data}->{department} = $entry->{raw_data}->{department};
+    my $dep = _departments_by($self)->{description}->{ $entry->{raw_data}->{department} };
+    if (!$dep) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid department');
+      return 0;
+    }
+    $entry->{info_data}->{department} = $dep->description;
+    $object->department_id($dep->id);
+  }
 
   return 1;
 }
 
+# ToDo: salesman by name
+sub handle_salesman {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+  my $vc_obj;
+  $vc_obj    = SL::DB::Customer->new(id => $object->customer_id)->load if $object->can('customer') && $object->customer_id;
+  $vc_obj    = SL::DB::Vendor->new(id   => $object->vendor_id)->load   if (!$vc_obj && $object->can('vendor') && $object->vendor_id);
+
+  # salesman from customer/vendor or login if not given
+  if (!$object->salesman) {
+    if ($vc_obj && $vc_obj->salesman_id) {
+      $object->salesman(SL::DB::Manager::Employee->find_by(id => $vc_obj->salesman_id));
+    } else {
+      $object->salesman(SL::DB::Manager::Employee->current);
+    }
+  }
+}
+
+# ToDo: employee by name
+sub handle_employee {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  # employee from front end if not given
+  if (!$object->employee_id) {
+    $object->employee_id($self->controller->{employee_id});
+  }
+  # employee from login if not given
+  if (!$object->employee_id) {
+    $object->employee_id(SL::DB::Manager::Employee->current->id) if SL::DB::Manager::Employee->current;
+  }
+}
+
+
+
 #
 # private functions
 #
@@ -128,6 +253,26 @@ sub _default_currency_id {
   return SL::DB::Default->get->currency_id;
 }
 
+sub _default_taxzone_id {
+  my ($self) = @_;
+
+  return SL::DB::Manager::TaxZone->get_all_sorted(query => [ obsolete => 0 ])->[0]->id;
+}
+
+sub _departments_by {
+  my ($self) = @_;
+
+  my $all_departments = SL::DB::Manager::Department->get_all;
+  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_departments } } ) } qw(id description) };
+}
+
+sub _projects_by {
+  my ($self) = @_;
+
+  my $all_projects = SL::DB::Manager::Project->get_all;
+  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_projects } } ) } qw(id projectnumber description) };
+}
+
 sub _taxzones_by {
   my ($self) = @_;
 
index a6e9001..e6f6794 100644 (file)
@@ -53,7 +53,8 @@ sub init_parts_by {
   my ($self) = @_;
 
   my $all_parts = SL::DB::Manager::Part->get_all;
-  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_parts } } ) } qw(id partnumber ean description) };
+  return { map { my $col = $_; ( $col =>
+         { map { ( $_->$col => $_ ) } grep { defined $_->$col } @{ $all_parts } } ) } qw(id partnumber ean description) };
 }
 
 sub init_warehouses_by {
@@ -70,7 +71,6 @@ sub init_bins_by {
   my $bins_by;
   $bins_by->{_wh_id_and_id_ident()}          = { map { ( _wh_id_and_id_maker($_->warehouse_id, $_->id)                   => $_ ) } @{ $all_bins } };
   $bins_by->{_wh_id_and_description_ident()} = { map { ( _wh_id_and_description_maker($_->warehouse_id, $_->description) => $_ ) } @{ $all_bins } };
-
   return $bins_by;
 }
 
@@ -79,7 +79,7 @@ sub check_objects {
 
   $self->controller->track_progress(phase => 'building data', progress => 0);
 
-  my $i;
+  my $i = 0;
   my $num_data = scalar @{ $self->controller->data };
   foreach my $entry (@{ $self->controller->data }) {
     $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
@@ -127,6 +127,8 @@ sub check_warehouse {
 
   my $object = $entry->{object};
 
+  $self->settings->{apply_warehouse} ||= '';  # avoid warnings if undefined
+
   # If warehouse from front-end is enforced for all transfers, use this, if valid.
   if ($self->settings->{apply_warehouse} eq 'all') {
     $object->warehouse_id(undef);
@@ -185,6 +187,8 @@ sub check_bin {
 
   my $object = $entry->{object};
 
+  $self->settings->{apply_bin} ||= '';  # avoid warnings if undefined
+
   # If bin from front-end is enforced for all transfers, use this, if valid.
   if ($self->settings->{apply_bin} eq 'all') {
     $object->bin_id(undef);
@@ -217,7 +221,7 @@ sub check_bin {
   }
 
   # Map description to ID if given.
-  if (!$object->bin_id && $entry->{raw_data}->{bin}) {
+  if (!$object->bin_id && $entry->{raw_data}->{bin} && $object->warehouse_id) {
     my $bin = $self->bins_by->{_wh_id_and_description_ident()}->{ _wh_id_and_description_maker($object->warehouse_id, $entry->{raw_data}->{bin}) };
     if (!$bin) {
       push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
@@ -272,11 +276,20 @@ sub check_part {
 # This imports inventories when target_qty is given, transfers else.
 # So we get the actual qty in stock and transfer the difference in case of
 # a given target_qty
-sub check_qty{
+sub check_qty {
   my ($self, $entry) = @_;
 
   my $object = $entry->{object};
 
+  # parse qty (may be float values)
+  if (exists $entry->{raw_data}->{target_qty}) {
+    $entry->{raw_data}->{target_qty} = $::form->parse_amount(\%::myconfig, $entry->{raw_data}->{target_qty});
+    # $object->target_qty($entry->{raw_data}->{target_qty});
+  }
+  if (exists $entry->{raw_data}->{qty}) {
+    $entry->{raw_data}->{qty}        = $::form->parse_amount(\%::myconfig, $entry->{raw_data}->{qty});
+    $object->qty($entry->{raw_data}->{qty});
+  }
   if (! exists $entry->{raw_data}->{target_qty} && ! exists $entry->{raw_data}->{qty}) {
     push @{ $entry->{errors} }, $::locale->text('Error: A quantity or a target quantity must be given.');
     return 0;
@@ -370,7 +383,7 @@ sub handle_employee {
 
   # employee from login if not given
   if (!$object->employee_id) {
-    $object->employee_id(SL::DB::Manager::Employee->find_by(login => $::myconfig{login})->id);
+    $object->employee_id(SL::DB::Manager::Employee->current->id) if SL::DB::Manager::Employee->current;
   }
 
   if ($object->employee_id) {
@@ -395,7 +408,7 @@ sub save_objects {
   my $data = $params{data} || $self->controller->data;
 
   foreach my $entry (@{ $data }) {
-    my ($trans_id) = selectrow_query($::form, $::form->get_standard_dbh, qq|SELECT nextval('id')|);
+    my ($trans_id) = selectrow_query($::form,$entry->{object}->db->dbh, qq|SELECT nextval('id')|);
     $entry->{object}->trans_id($trans_id);
   }
 
@@ -424,7 +437,7 @@ SQL
                 $object->chargenumber);
   push @values, $object->bestbefore if $bestbefore_val_cnt;
 
-  my ($stocked_qty) = selectrow_query($::form, $::form->get_standard_dbh, $query, @values);
+  my ($stocked_qty) = selectrow_query($::form, $object->db->dbh, $query, @values);
 
   return $stocked_qty;
 }
index e0935c0..503b4d1 100644 (file)
@@ -3,7 +3,7 @@ package SL::Controller::CsvImport::Order;
 
 use strict;
 
-use List::MoreUtils qw(any);
+use List::MoreUtils qw(any none);
 
 use SL::Helper::Csv;
 use SL::Controller::CsvImport::Helper::Consistency;
@@ -18,14 +18,15 @@ use SL::DB::Pricegroup;
 use SL::DB::Project;
 use SL::DB::Shipto;
 use SL::DB::TaxZone;
-use SL::TransNumber;
+use SL::DB::Unit;
+use SL::PriceSource;
 
 use parent qw(SL::Controller::CsvImport::BaseMulti);
 
 
 use Rose::Object::MakeMethods::Generic
 (
- 'scalar --get_set_init' => [ qw(settings languages_by parts_by contacts_by departments_by projects_by ct_shiptos_by price_factors_by pricegroups_by) ],
+ 'scalar --get_set_init' => [ qw(settings languages_by all_parts parts_by part_counts_by contacts_by ct_shiptos_by price_factors_by pricegroups_by units_by) ],
 );
 
 
@@ -132,9 +133,11 @@ sub setup_displayable_columns {
                                  { name => 'taxincluded',             description => $::locale->text('Tax Included')                          },
                                  { name => 'customer',                description => $::locale->text('Customer (name)')                       },
                                  { name => 'customernumber',          description => $::locale->text('Customer Number')                       },
+                                 { name => 'customer_gln',            description => $::locale->text('Customer GLN')                          },
                                  { name => 'customer_id',             description => $::locale->text('Customer (database ID)')                },
                                  { name => 'vendor',                  description => $::locale->text('Vendor (name)')                         },
                                  { name => 'vendornumber',            description => $::locale->text('Vendor Number')                         },
+                                 { name => 'vendor_gln',              description => $::locale->text('Vendor GLN')                            },
                                  { name => 'vendor_id',               description => $::locale->text('Vendor (database ID)')                  },
                                  { name => 'language_id',             description => $::locale->text('Language (database ID)')                },
                                  { name => 'language',                description => $::locale->text('Language (name)')                       },
@@ -159,6 +162,7 @@ sub setup_displayable_columns {
                                  { name => 'cusordnumber',    description => $::locale->text('Customer Order Number')      },
                                  { name => 'description',     description => $::locale->text('Description')                },
                                  { name => 'discount',        description => $::locale->text('Discount')                   },
+                                 { name => 'ean',             description => $::locale->text('EAN')                        },
                                  { name => 'lastcost',        description => $::locale->text('Lastcost')                   },
                                  { name => 'longdescription', description => $::locale->text('Long Description')           },
                                  { name => 'marge_percent',   description => $::locale->text('Margepercent')               },
@@ -190,11 +194,27 @@ sub init_languages_by {
   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_languages } } ) } qw(id description article_code) };
 }
 
+sub init_all_parts {
+  my ($self) = @_;
+
+  return SL::DB::Manager::Part->get_all;
+}
+
 sub init_parts_by {
   my ($self) = @_;
 
-  my $all_parts = SL::DB::Manager::Part->get_all;
-  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_parts } } ) } qw(id partnumber ean description) };
+  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_parts } } ) } qw(id partnumber ean description) };
+}
+
+sub init_part_counts_by {
+  my ($self) = @_;
+
+  my $part_counts_by;
+
+  $part_counts_by->{ean}->        {$_->ean}++         for @{ $self->all_parts };
+  $part_counts_by->{description}->{$_->description}++ for @{ $self->all_parts };
+
+  return $part_counts_by;
 }
 
 sub init_contacts_by {
@@ -211,20 +231,6 @@ sub init_contacts_by {
   return $cby;
 }
 
-sub init_departments_by {
-  my ($self) = @_;
-
-  my $all_departments = SL::DB::Manager::Department->get_all;
-  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_departments } } ) } qw(id description) };
-}
-
-sub init_projects_by {
-  my ($self) = @_;
-
-  my $all_projects = SL::DB::Manager::Project->get_all;
-  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_projects } } ) } qw(id projectnumber description) };
-}
-
 sub init_ct_shiptos_by {
   my ($self) = @_;
 
@@ -251,6 +257,13 @@ sub init_pricegroups_by {
   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_pricegroups } } ) } qw(id pricegroup) };
 }
 
+sub init_units_by {
+  my ($self) = @_;
+
+  my $all_units = SL::DB::Manager::Unit->get_all;
+  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_units } } ) } qw(name) };
+}
+
 sub check_objects {
   my ($self) = @_;
 
@@ -258,20 +271,32 @@ sub check_objects {
 
   my $i = 0;
   my $num_data = scalar @{ $self->controller->data };
+  my $order_entry;
   foreach my $entry (@{ $self->controller->data }) {
     $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
 
+    $entry->{info_data}->{datatype} = $entry->{raw_data}->{datatype};
+
     if ($entry->{raw_data}->{datatype} eq $self->_order_column) {
       $self->handle_order($entry);
+      $order_entry = $entry;
     } elsif ($entry->{raw_data}->{datatype} eq $self->_item_column && $entry->{object}->can('part')) {
-      $self->handle_item($entry);
+      $self->handle_item($entry, $order_entry);
+    } else {
+      $order_entry = undef;
     }
+
     $self->handle_cvars($entry, sub_module => 'orderitems');
 
   } continue {
     $i++;
   }
 
+  $self->add_info_columns($self->_order_column,
+                          { header => $::locale->text('Data type'), method => 'datatype' });
+  $self->add_info_columns($self->_item_column,
+                          { header => $::locale->text('Data type'), method => 'datatype' });
+
   $self->add_info_columns($self->_order_column,
                           { header => $::locale->text('Customer/Vendor'), method => 'vc_name' });
   # Todo: access via ->[0] ok? Better: search first order column and use this
@@ -291,21 +316,6 @@ sub check_objects {
 
   $self->add_items_to_order();
   $self->handle_prices_and_taxes();
-
-
-  # If order has errors set error for orderitems as well
-  my $order_entry;
-  foreach my $entry (@{ $self->controller->data }) {
-    # Search first order
-    if ($entry->{raw_data}->{datatype} eq $self->_order_column) {
-      $order_entry = $entry;
-    } elsif ( defined $order_entry
-              && $entry->{raw_data}->{datatype} eq $self->_item_column
-              && scalar @{ $order_entry->{errors} } > 0 ) {
-      push @{ $entry->{errors} }, $::locale->text('Error: Invalid order for this order item');
-    }
-  }
-
 }
 
 sub handle_order {
@@ -314,10 +324,10 @@ sub handle_order {
   my $object = $entry->{object};
 
   my $vc_obj;
-  if (any { $entry->{raw_data}->{$_} } qw(customer customernumber customer_id)) {
+  if (any { $entry->{raw_data}->{$_} } qw(customer customernumber customer_gln customer_id)) {
     $self->check_vc($entry, 'customer_id');
     $vc_obj = SL::DB::Customer->new(id => $object->customer_id)->load if $object->customer_id;
-  } elsif (any { $entry->{raw_data}->{$_} } qw(vendor vendornumber vendor_id)) {
+  } elsif (any { $entry->{raw_data}->{$_} } qw(vendor vendornumber vendor_gln vendor_id)) {
     $self->check_vc($entry, 'vendor_id');
     $vc_obj = SL::DB::Vendor->new(id => $object->vendor_id)->load if $object->vendor_id;
   } else {
@@ -336,46 +346,16 @@ sub handle_order {
 
   if ($vc_obj) {
     # copy from customer if not given
-    foreach (qw(payment_id language_id taxzone_id currency_id)) {
+    foreach (qw(payment_id delivery_term_id language_id taxzone_id currency_id)) {
       $object->$_($vc_obj->$_) unless $object->$_;
     }
+    $object->intnotes($vc_obj->notes) unless $object->intnotes;
   }
 
   $self->handle_salesman($entry);
   $self->handle_employee($entry);
 }
 
-# ToDo: salesman by name
-sub handle_salesman {
-  my ($self, $entry) = @_;
-
-  my $object = $entry->{object};
-  my $vc_obj;
-  $vc_obj    = SL::DB::Customer->new(id => $object->customer_id)->load if $object->customer_id;
-  $vc_obj    = SL::DB::Vendor->new(id   => $object->vendor_id)->load   if (!$vc_obj && $object->vendor_id);
-
-  # salesman from customer/vendor or login if not given
-  if (!$object->salesman) {
-    if ($vc_obj && $vc_obj->salesman_id) {
-      $object->salesman(SL::DB::Manager::Employee->find_by(id => $vc_obj->salesman_id));
-    } else {
-      $object->salesman(SL::DB::Manager::Employee->find_by(login => $::myconfig{login}));
-    }
-  }
-}
-
-# ToDo: employee by name
-sub handle_employee {
-  my ($self, $entry) = @_;
-
-  my $object = $entry->{object};
-
-  # employee from login if not given
-  if (!$object->employee_id) {
-    $object->employee_id(SL::DB::Manager::Employee->find_by(login => $::myconfig{login})->id);
-  }
-}
-
 sub check_language {
   my ($self, $entry) = @_;
 
@@ -408,33 +388,125 @@ sub check_language {
 }
 
 sub handle_item {
-  my ($self, $entry) = @_;
+  my ($self, $entry, $order_entry) = @_;
+
+  return unless $order_entry;
 
   my $object = $entry->{object};
+
   return unless $self->check_part($entry);
 
   my $part_obj = SL::DB::Part->new(id => $object->parts_id)->load;
 
+  $self->handle_unit($entry);
+
   # copy from part if not given
   $object->description($part_obj->description) unless $object->description;
   $object->longdescription($part_obj->notes)   unless $object->longdescription;
-  $object->unit($part_obj->unit)               unless $object->unit;
-  $object->sellprice($part_obj->sellprice)     unless defined $object->sellprice;
   $object->lastcost($part_obj->lastcost)       unless defined $object->lastcost;
 
   # set to 0 if not given
-  $object->discount(0) unless $object->discount;
   $object->ship(0)     unless $object->ship;
 
   $self->check_project($entry, global => 0);
   $self->check_price_factor($entry);
   $self->check_pricegroup($entry);
+
+  $self->handle_sellprice($entry, $order_entry);
+  $self->handle_discount($entry, $order_entry);
+}
+
+sub handle_unit {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  # Set unit from part if not given.
+  if (!$object->unit) {
+    $object->unit($object->part->unit);
+    return 1;
+  }
+
+  # Check whether or not unit is valid.
+  if ($object->unit && !$self->units_by->{name}->{ $object->unit }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid unit');
+    return 0;
+  }
+
+  # Check whether unit is convertible to parts unit
+  if (none { $object->unit eq $_ } map { $_->name } @{ $object->part->unit_obj->convertible_units }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid unit');
+    return 0;
+  }
+
+  return 1;
+}
+
+sub handle_sellprice {
+  my ($self, $entry, $record_entry) = @_;
+
+  my $item   = $entry->{object};
+  my $record = $record_entry->{object};
+
+  return if !$record->customervendor;
+
+  # If sellprice is given, set price source to pricegroup if given or to none.
+  if (exists $entry->{raw_data}->{sellprice}) {
+    my $price_source      = SL::PriceSource->new(record_item => $item, record => $record);
+    my $price_source_spec = $item->pricegroup_id ? 'pricegroup' . '/' . $item->pricegroup_id : '';
+    my $price             = $price_source->price_from_source($price_source_spec);
+    $item->active_price_source($price->source);
+
+  } else {
+    # Set sellprice the best price of price source
+    my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
+    my $price        = $price_source->best_price;
+    if ($price) {
+      $item->sellprice($price->price);
+      $item->active_price_source($price->source);
+    } else {
+      $item->sellprice(0);
+      $item->active_price_source($price_source->price_from_source('')->source);
+    }
+  }
+}
+
+sub handle_discount {
+  my ($self, $entry, $record_entry) = @_;
+
+  my $item   = $entry->{object};
+  my $record = $record_entry->{object};
+
+  return if !$record->customervendor;
+
+  # If discount is given, set discount source to none.
+  if (exists $entry->{raw_data}->{discount}) {
+    $item->discount($item->discount/100.0)      if $item->discount;
+    $item->discount(0)                      unless $item->discount;
+
+    my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
+    my $discount     = $price_source->price_from_source('');
+    $item->active_discount_source($discount->source);
+
+  } else {
+    # Set discount the best discount of price source
+    my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
+    my $discount     = $price_source->best_discount;
+    if ($discount) {
+      $item->discount($discount->discount);
+      $item->active_discount_source($discount->source);
+    } else {
+      $item->discount(0);
+      $item->active_discount_source($price_source->discount_from_source('')->source);
+    }
+  }
 }
 
 sub check_part {
   my ($self, $entry) = @_;
 
   my $object = $entry->{object};
+  my $is_ambiguous;
 
   # Check whether or not part ID is valid.
   if ($object->parts_id && !$self->parts_by->{id}->{ $object->parts_id }) {
@@ -461,13 +533,41 @@ sub check_part {
       return 0;
     }
 
-    $object->parts_id($part->id);
+    if ($self->part_counts_by->{description}->{ $entry->{raw_data}->{description} } > 1) {
+      $is_ambiguous = 1;
+    } else {
+      $object->parts_id($part->id);
+    }
+  }
+
+  # Map ean to ID if given.
+  if (!$object->parts_id && $entry->{raw_data}->{ean}) {
+    my $part = $self->parts_by->{ean}->{ $entry->{raw_data}->{ean} };
+    if (!$part) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid part');
+      return 0;
+    }
+
+    if ($self->part_counts_by->{ean}->{ $entry->{raw_data}->{ean} } > 1) {
+      $is_ambiguous = 1;
+    } else {
+      $object->parts_id($part->id);
+    }
   }
 
   if ($object->parts_id) {
     $entry->{info_data}->{partnumber} = $self->parts_by->{id}->{ $object->parts_id }->partnumber;
   } else {
-    push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
+    if ($is_ambiguous) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Part is ambiguous');
+    } else {
+      push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
+    }
+    return 0;
+  }
+
+  if ($self->parts_by->{id}->{ $object->parts_id }->obsolete) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Part is obsolete');
     return 0;
   }
 
@@ -506,71 +606,6 @@ sub check_contact {
   return 1;
 }
 
-sub check_department {
-  my ($self, $entry) = @_;
-
-  my $object = $entry->{object};
-
-  # Check whether or not department ID is valid.
-  if ($object->department_id && !$self->departments_by->{id}->{ $object->department_id }) {
-    push @{ $entry->{errors} }, $::locale->text('Error: Invalid department');
-    return 0;
-  }
-
-  # Map description to ID if given.
-  if (!$object->department_id && $entry->{raw_data}->{department}) {
-    my $dep = $self->departments_by->{description}->{ $entry->{raw_data}->{department} };
-    if (!$dep) {
-      push @{ $entry->{errors} }, $::locale->text('Error: Invalid department');
-      return 0;
-    }
-
-    $object->department_id($dep->id);
-  }
-
-  return 1;
-}
-
-sub check_project {
-  my ($self, $entry, %params) = @_;
-
-  my $id_column          = ($params{global} ? 'global' : '') . 'project_id';
-  my $number_column      = ($params{global} ? 'global' : '') . 'projectnumber';
-  my $description_column = ($params{global} ? 'global' : '') . 'project';
-
-  my $object = $entry->{object};
-
-  # Check whether or not projetc ID is valid.
-  if ($object->$id_column && !$self->projects_by->{id}->{ $object->$id_column }) {
-    push @{ $entry->{errors} }, $::locale->text('Error: Invalid project');
-    return 0;
-  }
-
-  # Map number to ID if given.
-  if (!$object->$id_column && $entry->{raw_data}->{$number_column}) {
-    my $proj = $self->projects_by->{projectnumber}->{ $entry->{raw_data}->{$number_column} };
-    if (!$proj) {
-      push @{ $entry->{errors} }, $::locale->text('Error: Invalid project');
-      return 0;
-    }
-
-    $object->$id_column($proj->id);
-  }
-
-  # Map description to ID if given.
-  if (!$object->$id_column && $entry->{raw_data}->{$description_column}) {
-    my $proj = $self->projects_by->{description}->{ $entry->{raw_data}->{$description_column} };
-    if (!$proj) {
-      push @{ $entry->{errors} }, $::locale->text('Error: Invalid project');
-      return 0;
-    }
-
-    $object->$id_column($proj->id);
-  }
-
-  return 1;
-}
-
 sub check_ct_shipto {
   my ($self, $entry) = @_;
 
@@ -725,22 +760,16 @@ sub handle_prices_and_taxes() {
 sub save_objects {
   my ($self, %params) = @_;
 
-  # set order number and collect to save
-  my $objects_to_save;
+  # Collect orders without errors to save.
+  my $entries_to_save = [];
   foreach my $entry (@{ $self->controller->data }) {
     next if $entry->{raw_data}->{datatype} ne $self->_order_column;
     next if @{ $entry->{errors} };
 
-    if (!$entry->{object}->ordnumber) {
-      my $number = SL::TransNumber->new(type => 'sales_order',
-                                        save => 1);
-      $entry->{object}->ordnumber($number->create_unique());
-    }
-
-    push @{ $objects_to_save }, $entry;
+    push @{ $entries_to_save }, $entry;
   }
 
-  $self->SUPER::save_objects(data => $objects_to_save);
+  $self->SUPER::save_objects(data => $entries_to_save);
 }
 
 sub _order_column {
index be989ee..9afaf63 100644 (file)
@@ -23,7 +23,8 @@ use parent qw(SL::Controller::CsvImport::Base);
 use Rose::Object::MakeMethods::Generic
 (
  scalar                  => [ qw(table makemodel_columns) ],
- 'scalar --get_set_init' => [ qw(bg_by settings parts_by price_factors_by units_by partsgroups_by
+ 'scalar --get_set_init' => [ qw(bg_by settings parts_by price_factors_by classification_by units_by partsgroups_by
+                                 warehouses_by bins_by
                                  translation_columns all_pricegroups) ],
 );
 
@@ -38,7 +39,8 @@ sub set_profile_defaults {
                        sellprice_adjustment_type => 'percent',
                        article_number_policy     => 'update_prices',
                        shoparticle_if_missing    => '0',
-                       parts_type                => 'part',
+                       part_type                 => 'part',
+                       part_classification       => 0,
                        default_buchungsgruppe    => ($bugru ? $bugru->id : undef),
                        apply_buchungsgruppe      => 'all',
                       );
@@ -57,6 +59,13 @@ sub init_bg_by {
   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_bg } } ) } qw(id description) };
 }
 
+sub init_classification_by {
+  my ($self) = @_;
+  my $all_classifications = SL::DB::Manager::PartClassification->get_all;
+  $_->abbreviation($::locale->text($_->abbreviation)) for @{ $all_classifications };
+  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_classifications } } ) } qw(id abbreviation) };
+}
+
 sub init_price_factors_by {
   my ($self) = @_;
 
@@ -78,20 +87,35 @@ sub init_units_by {
   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_units } } ) } qw(name) };
 }
 
+sub init_bins_by {
+  my ($self) = @_;
+
+  my $all_bins = SL::DB::Manager::Bin->get_all;
+  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_bins } } ) } qw(id description) };
+}
+
+sub init_warehouses_by {
+  my ($self) = @_;
+
+  my $all_warehouses = SL::DB::Manager::Warehouse->get_all;
+  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_warehouses } } ) } qw(id description) };
+}
+
+
 sub init_parts_by {
   my ($self) = @_;
 
-#  my $parts_by = { id         => { map { ( $_->id => $_ ) } grep { !$_->assembly } @{ $self->existing_objects } },
+#  my $parts_by = { id         => { map { ( $_->id => $_ ) } grep { !$_->part_type = 'assembly' } @{ $self->existing_objects } },
 #                   partnumber => { part    => { },
 #                                   service => { } } };
 #
 #  foreach my $part (@{ $self->existing_objects }) {
-#    next if $part->assembly;
+#    next if $part->part_type eq 'assembly';
 #    $parts_by->{partnumber}->{ $part->type }->{ $part->partnumber } = $part;
 #  }
 
   my $parts_by = {};
-  my $sth = prepare_execute_query($::form, $::form->get_standard_dbh, 'SELECT partnumber FROM parts');
+  my $sth = prepare_execute_query($::form, SL::DB::Object->new->db->dbh, 'SELECT partnumber FROM parts');
   while (my ($partnumber) = $sth->fetchrow_array()) {
     $parts_by->{partnumber}{$partnumber} = 1;
   }
@@ -102,7 +126,7 @@ sub init_parts_by {
 sub init_all_pricegroups {
   my ($self) = @_;
 
-  return SL::DB::Manager::Pricegroup->get_all(sort => 'id');
+  return SL::DB::Manager::Pricegroup->get_all_sorted;
 }
 
 sub init_settings {
@@ -110,7 +134,7 @@ sub init_settings {
 
   return { map { ( $_ => $self->controller->profile->get($_) ) } qw(apply_buchungsgruppe default_buchungsgruppe article_number_policy
                                                                     sellprice_places sellprice_adjustment sellprice_adjustment_type
-                                                                    shoparticle_if_missing parts_type default_unit) };
+                                                                    shoparticle_if_missing part_type classification_id default_unit) };
 }
 
 sub init_all_cvar_configs {
@@ -134,32 +158,33 @@ sub check_objects {
 
   $self->makemodel_columns({});
 
-  my $i;
+  my $i = 0;
   my $num_data = scalar @{ $self->controller->data };
   foreach my $entry (@{ $self->controller->data }) {
     $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
 
     $self->check_buchungsgruppe($entry);
-    $self->check_type($entry);
+    $self->check_part_type($entry);
     $self->check_unit($entry);
     $self->check_price_factor($entry);
     $self->check_payment($entry);
     $self->check_partsgroup($entry);
+    $self->check_warehouse_and_bin($entry);
     $self->handle_pricegroups($entry);
     $self->check_existing($entry) unless @{ $entry->{errors} };
     $self->handle_prices($entry) if $self->settings->{sellprice_adjustment};
     $self->handle_shoparticle($entry);
     $self->handle_translations($entry);
-    $self->handle_cvars($entry);
+    $self->handle_cvars($entry) unless $entry->{dont_handle_cvars};
     $self->handle_makemodel($entry);
     $self->set_various_fields($entry);
   } continue {
     $i++;
   }
 
-  $self->add_columns(qw(type)) if $self->settings->{parts_type} eq 'mixed';
+  $self->add_columns(qw(part_type classification_id)) if $self->settings->{part_type} eq 'mixed';
   $self->add_columns(qw(buchungsgruppen_id unit));
-  $self->add_columns(map { "${_}_id" } grep { exists $self->controller->data->[0]->{raw_data}->{$_} } qw (price_factor payment partsgroup));
+  $self->add_columns(map { "${_}_id" } grep { exists $self->controller->data->[0]->{raw_data}->{$_} } qw (price_factor payment partsgroup warehouse bin));
   $self->add_columns(qw(shop)) if $self->settings->{shoparticle_if_missing};
   $self->add_cvar_raw_data_columns;
   map { $self->add_raw_data_columns("pricegroup_${_}") if exists $self->controller->data->[0]->{raw_data}->{"pricegroup_$_"} } (1..scalar(@{ $self->all_pricegroups }));
@@ -198,23 +223,43 @@ sub check_buchungsgruppe {
   $default_id    = undef unless $self->bg_by->{id}->{ $default_id };
 
   # 1. Use default ID if enforced.
-  $object->buchungsgruppen_id($default_id) if $default_id && ($self->settings->{apply_buchungsgruppe} eq 'all');
+  if ($default_id && ($self->settings->{apply_buchungsgruppe} eq 'all')) {
+    $object->buchungsgruppen_id($default_id);
+    push @{ $entry->{information} }, $::locale->text('Use default booking group because setting is \'all\'');
+  }
 
   # 2. Use supplied ID if valid
   $object->buchungsgruppen_id(undef) if $object->buchungsgruppen_id && !$self->bg_by->{id}->{ $object->buchungsgruppen_id };
 
   # 3. Look up name if supplied.
-  if (!$object->buchungsgruppen_id) {
+  if (!$object->buchungsgruppen_id && $entry->{raw_data}->{buchungsgruppe}) {
     my $bg = $self->bg_by->{description}->{ $entry->{raw_data}->{buchungsgruppe} };
     $object->buchungsgruppen_id($bg->id) if $bg;
   }
 
   # 4. Use default ID if not valid.
-  $object->buchungsgruppen_id($default_id) if !$object->buchungsgruppen_id && $default_id && ($self->settings->{apply_buchungsgruppe} eq 'missing');
-
+  if (!$object->buchungsgruppen_id && $default_id && ($self->settings->{apply_buchungsgruppe} eq 'missing')) {
+    $object->buchungsgruppen_id($default_id) ;
+    $entry->{buch_information} = $::locale->text('Use default booking group because wanted is missing');
+  }
   return 1 if $object->buchungsgruppen_id;
+  $entry->{buch_error} =  $::locale->text('Error: booking group missing or invalid');
+  return 0;
+}
 
-  push @{ $entry->{errors} }, $::locale->text('Error: Buchungsgruppe missing or invalid');
+sub _part_is_used {
+  my ($self, $part) = @_;
+
+  my $query =
+      qq|SELECT COUNT(parts_id) FROM invoice where parts_id = ?
+         UNION
+         SELECT COUNT(parts_id) FROM assembly where parts_id = ?
+         UNION
+         SELECT COUNT(parts_id) FROM orderitems where parts_id = ?
+        |;
+  foreach my $ref (selectall_hashref_query($::form, $part->db->dbh, $query, $part->id, $part->id, $part->id)) {
+    return 1 if $ref->{count} != 0;
+  }
   return 0;
 }
 
@@ -222,34 +267,112 @@ sub check_existing {
   my ($self, $entry) = @_;
 
   my $object = $entry->{object};
+  my $raw = $entry->{raw_data};
 
   if ($object->partnumber && $self->parts_by->{partnumber}{$object->partnumber}) {
-    $entry->{part} = SL::DB::Manager::Part->find_by(partnumber => $object->partnumber);
+    $entry->{part} = SL::DB::Manager::Part->get_first( query => [ partnumber => $object->partnumber ], limit => 1,
+      with_objects => [ 'translations', 'custom_variables' ], multi_many_ok => 1
+    );
+    if ( !$entry->{part} ) {
+        $entry->{part} = SL::DB::Manager::Part->get_first( query => [ partnumber => $object->partnumber ], limit => 1,
+          with_objects => [ 'translations' ], multi_many_ok => 1
+        );
+    }
   }
 
   if ($entry->{part}) {
-    if ($self->settings->{article_number_policy} eq 'update_prices') {
-      if ($self->settings->{parts_type} eq 'mixed' && $entry->{part}->type ne $object->type) {
-        push(@{$entry->{errors}}, $::locale->text('Skipping due to existing entry in database with different type'));
-      } else {
-        map { $entry->{part}->$_( $object->$_ ) if defined $object->$_ } qw(sellprice listprice lastcost);
+    if ($entry->{part}->part_type ne $object->part_type ) {
+      push(@{$entry->{errors}}, $::locale->text('Skipping due to existing entry in database with different type'));
+      return;
+    }
+    if ( $entry->{part}->unit ne $object->unit ) {
+      if ( $entry->{part}->onhand != 0 || $self->_part_is_used($entry->{part})) {
+        push(@{$entry->{errors}}, $::locale->text('Skipping due to existing entry with different unit'));
+        return;
+      }
+    }
+  }
+
+  if ($self->settings->{article_number_policy} eq 'update_prices_sn' || $self->settings->{article_number_policy} eq 'update_parts_sn') {
+    if (!$entry->{part}) {
+      push(@{$entry->{errors}}, $::locale->text('Skipping non-existent article'));
+      return;
+    }
+  }
+
+  ## checking also doubles in csv !!
+  foreach my $csventry (@{ $self->controller->data }) {
+    if ( $entry != $csventry && $object->partnumber eq $csventry->{object}->partnumber ) {
+      if ( $csventry->{doublechecked} ) {
+        push(@{$entry->{errors}}, $::locale->text('Skipping due to same partnumber in csv file'));
+        return;
+      }
+    }
+  }
+  $entry->{doublechecked} = 1;
+
+  if ($entry->{part}) {
+    if ($self->settings->{article_number_policy} eq 'update_prices' || $self->settings->{article_number_policy} eq 'update_prices_sn') {
+      map { $entry->{part}->$_( $object->$_ ) if defined $object->$_ } qw(sellprice listprice lastcost);
+
+      # merge prices
+      my %prices_by_pricegroup_id = map { $_->pricegroup->id => $_ } $entry->{part}->prices, $object->prices;
+      $entry->{part}->prices(grep { $_ } map { $prices_by_pricegroup_id{$_->id} } @{ $self->all_pricegroups });
+
+      push @{ $entry->{information} }, $::locale->text('Updating prices of existing entry in database');
+      $entry->{object_to_save}    = $entry->{part};
+      $entry->{dont_handle_cvars} = 1;
+    } elsif ( $self->settings->{article_number_policy} eq 'update_parts' || $self->settings->{article_number_policy} eq 'update_parts_sn') {
 
+      # Update parts table
+      # copy only the data which is not explicit copied by  "methods"
+
+      map { $entry->{part}->$_( $object->$_ ) if defined $object->$_ }  qw(description notes weight ean rop image
+                                                                           drawing ve gv
+                                                                           unit
+                                                                           has_sernumber not_discountable obsolete
+                                                                           payment_id
+                                                                           sellprice listprice lastcost);
+
+      if (defined $raw->{"sellprice"} || defined $raw->{"listprice"} || defined $raw->{"lastcost"}) {
         # merge prices
         my %prices_by_pricegroup_id = map { $_->pricegroup->id => $_ } $entry->{part}->prices, $object->prices;
         $entry->{part}->prices(grep { $_ } map { $prices_by_pricegroup_id{$_->id} } @{ $self->all_pricegroups });
+      }
 
-        push @{ $entry->{information} }, $::locale->text('Updating prices of existing entry in database');
-        $entry->{object_to_save} = $entry->{part};
+      # Update translation
+      my @translations;
+      push @translations, $entry->{part}->translations;
+      foreach my $language (@{ $self->all_languages }) {
+        my $desc;
+        $desc = $raw->{"description_". $language->article_code}  if defined $raw->{"description_". $language->article_code};
+        my $notes;
+        $notes = $raw->{"notes_". $language->article_code}  if defined $raw->{"notes_". $language->article_code};
+        next unless $desc || $notes;
+
+        push @translations, SL::DB::Translation->new(language_id     => $language->id,
+                                                     translation     => $desc,
+                                                     longdescription => $notes);
       }
-    } elsif ( $self->settings->{article_number_policy} eq 'skip' ) {
-      push(@{$entry->{errors}}, $::locale->text('Skipping due to existing entry in database'));
+      $entry->{part}->translations(\@translations) if @translations;
+
+      # save Part Update
+      push @{ $entry->{information} }, $::locale->text('Updating data of existing entry in database');
 
+      $entry->{object_to_save} = $entry->{part};
+      # copy all other data via "methods"
+      my $methods        = $self->controller->headers->{methods};
+      $entry->{object_to_save}->$_( $entry->{object}->$_ ) for @{ $methods }, keys %{ $self->clone_methods };
+
+    } elsif ( $self->settings->{article_number_policy} eq 'skip' ) {
+      push(@{$entry->{errors}}, $::locale->text('Skipping due to existing entry in database')) if ( $entry->{part} );
     } else {
-      $object->partnumber('####');
-      push(@{$entry->{errors}}, $::locale->text('Skipping, for assemblies are not importable (yet)')) if $object->type eq 'assembly';
+      #$object->partnumber('####');
     }
   } else {
-    push(@{$entry->{errors}}, $::locale->text('Skipping, for assemblies are not importable (yet)')) if $object->type eq 'assembly';
+    # set error or info from buch if part not exists
+    push @{ $entry->{information} }, $entry->{buch_information} if $entry->{buch_information};
+    push @{ $entry->{errors} }, $entry->{buch_error} if $entry->{buch_error};
   }
 }
 
@@ -272,42 +395,65 @@ sub handle_shoparticle {
   $entry->{object}->shop(1) if $self->settings->{shoparticle_if_missing} && !$self->controller->headers->{used}->{shop};
 }
 
-sub check_type {
+sub check_part_type {
   my ($self, $entry) = @_;
 
-  my $bg = $self->bg_by->{id}->{ $entry->{object}->buchungsgruppen_id };
-  $bg  ||= SL::DB::Buchungsgruppe->new(inventory_accno_id => 1); # does this case ever occur?
+  # TODO: assemblies or assortments can't be imported
+
+  # my $bg = $self->bg_by->{id}->{ $entry->{object}->buchungsgruppen_id };
+  # $bg  ||= SL::DB::Buchungsgruppe->new(inventory_accno_id => 1); # does this case ever occur?
 
-  my $type = $self->settings->{parts_type};
-  if ($type eq 'mixed') {
-    $type = $entry->{raw_data}->{type} =~ m/^p/i ? 'part'
-          : $entry->{raw_data}->{type} =~ m/^s/i ? 'service'
-          : $entry->{raw_data}->{type} =~ m/^a/i ? 'assembly'
-          :                                        undef;
+  my $part_type = $self->settings->{part_type};
+  if ($part_type eq 'mixed' && $entry->{raw_data}->{part_type}) {
+    $part_type = $entry->{raw_data}->{part_type} =~ m/^p/i     ? 'part'
+               : $entry->{raw_data}->{part_type} =~ m/^s/i     ? 'service'
+               : $entry->{raw_data}->{part_type} =~ m/^assem/i ? 'assembly'
+               : $entry->{raw_data}->{part_type} =~ m/^assor/i ? 'assortment'
+               :                                                 $self->settings->{part_type};
   }
+  my $classification_id = $self->settings->{classification_id};
 
-  $entry->{object}->assembly($type eq 'assembly');
+  if ( $entry->{raw_data}->{pclass} && length($entry->{raw_data}->{pclass}) >= 2 ) {
+    my $abbr1 = substr($entry->{raw_data}->{pclass},0,1);
+    my $abbr2 = substr($entry->{raw_data}->{pclass},1);
+
+    if ( $self->classification_by->{abbreviation}->{$abbr2} ) {
+      my $tmp_classification_id = $self->classification_by->{abbreviation}->{$abbr2}->id;
+      $classification_id = $tmp_classification_id if $tmp_classification_id;
+    }
+    if ($part_type eq 'mixed') {
+      $part_type = $abbr1 eq $::locale->text('Part (typeabbreviation)') ? 'part'
+        : $abbr1 eq $::locale->text('Service (typeabbreviation)')       ? 'service'
+        : $abbr1 eq $::locale->text('Assembly (typeabbreviation)')      ? 'assembly'
+        : $abbr1 eq $::locale->text('Assortment (typeabbreviation)')    ? 'assortment'
+        :                                                                 undef;
+    }
+  }
 
   # when saving income_accno_id or expense_accno_id use ids from the selected
   # $bg according to the default tax_zone (the one with the highest sort
   # order).  Alternatively one could use the ids from defaults, but they might
   # not all be set.
+  # Only use existing bg
 
-  $entry->{object}->income_accno_id( $bg->income_accno_id( SL::DB::Manager::TaxZone->get_default->id ) );
+  $entry->{object}->income_accno_id( $bg->income_accno_id( SL::DB::Manager::TaxZone->get_default->id ) );
 
-  if ($type eq 'part' || $type eq 'service') {
-    $entry->{object}->expense_accno_id( $bg->expense_accno_id( SL::DB::Manager::TaxZone->get_default->id ) );
-  }
+  # if ($part_type eq 'part' || $part_type eq 'service') {
+    $entry->{object}->expense_accno_id( $bg->expense_accno_id( SL::DB::Manager::TaxZone->get_default->id ) );
+  }
 
-  if ($type eq 'part') {
-    $entry->{object}->inventory_accno_id( $bg->inventory_accno_id );
-  }
+  # if ($part_type eq 'part') {
+    $entry->{object}->inventory_accno_id( $bg->inventory_accno_id );
+  }
 
-  if (none { $_ eq $type } qw(part service assembly)) {
+  if (none { $_ eq $part_type } qw(part service assembly assortment)) {
     push @{ $entry->{errors} }, $::locale->text('Error: Invalid part type');
     return 0;
   }
 
+  $entry->{object}->part_type($part_type);
+  $entry->{object}->classification_id( $classification_id );
+
   return 1;
 }
 
@@ -332,11 +478,65 @@ sub check_price_factor {
     }
 
     $object->price_factor_id($pf->id);
+    $self->clone_methods->{price_factor_id} = 1;
   }
 
   return 1;
 }
 
+sub check_warehouse_and_bin {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  # Check whether or not warehouse id is valid.
+  if ($object->warehouse_id && !$self->warehouses_by->{id}->{ $object->warehouse_id }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse id');
+    return 0;
+  }
+  # Map name to ID if given.
+  if (!$object->warehouse_id && $entry->{raw_data}->{warehouse}) {
+    my $wh = $self->warehouses_by->{description}->{ $entry->{raw_data}->{warehouse} };
+
+    if (!$wh) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse name #1',$entry->{raw_data}->{warehouse});
+      return 0;
+    }
+
+    $object->warehouse_id($wh->id);
+  }
+  $self->clone_methods->{warehouse_id} = 1;
+
+  # Check whether or not bin id is valid.
+  if ($object->bin_id && !$self->bins_by->{id}->{ $object->bin_id }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin id');
+    return 0;
+  }
+  # Map name to ID if given.
+  if ($object->warehouse_id && !$object->bin_id && $entry->{raw_data}->{bin}) {
+    my $bin = $self->bins_by->{description}->{ $entry->{raw_data}->{bin} };
+
+    if (!$bin) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin name #1',$entry->{raw_data}->{bin});
+      return 0;
+    }
+
+    $object->bin_id($bin->id);
+  }
+  $self->clone_methods->{bin_id} = 1;
+
+  if ($object->warehouse_id && $object->bin_id ) {
+    my $bin = $self->bins_by->{id}->{ $object->bin_id };
+    if ( $bin->warehouse_id != $object->warehouse_id ) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Bin #1 is not from warehouse #2',
+                                                  $self->bins_by->{id}->{$object->bin_id}->description,
+                                                  $self->warehouses_by->{id}->{ $object->warehouse_id }->description);
+      return 0;
+    }
+  }
+  return 1;
+}
+
 sub check_partsgroup {
   my ($self, $entry) = @_;
 
@@ -344,7 +544,7 @@ sub check_partsgroup {
 
   # Check whether or not part group ID is valid.
   if ($object->partsgroup_id && !$self->partsgroups_by->{id}->{ $object->partsgroup_id }) {
-    push @{ $entry->{errors} }, $::locale->text('Error: Invalid parts group');
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid parts group id #1', $object->partsgroup_id);
     return 0;
   }
 
@@ -353,12 +553,14 @@ sub check_partsgroup {
     my $pg = $self->partsgroups_by->{partsgroup}->{ $entry->{raw_data}->{partsgroup} };
 
     if (!$pg) {
-      push @{ $entry->{errors} }, $::locale->text('Error: Invalid parts group');
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid parts group name #1',  $entry->{raw_data}->{partsgroup});
       return 0;
     }
 
     $object->partsgroup_id($pg->id);
   }
+  # register payment_id for method copying later
+  $self->clone_methods->{partsgroup_id} = 1;
 
   return 1;
 }
@@ -403,7 +605,7 @@ sub handle_pricegroups {
   foreach my $pricegroup (@{ $self->all_pricegroups }) {
     $idx++;
     my $sellprice = $entry->{raw_data}->{"pricegroup_${idx}"};
-    next if $sellprice eq '';
+    next if ($sellprice // '') eq '';
 
     push @prices, SL::DB::Price->new(pricegroup_id => $pricegroup->id,
                                      price         => $::form->parse_amount(\%::myconfig, $sellprice));
@@ -448,7 +650,7 @@ sub handle_makemodel {
   my %old_makemodels_by_mm = map { $_->make . $; . $_->model => $_ } $entry->{part}->makemodels;
   my @new_makemodels;
 
-  foreach my $makemodel ($object->makemodels()) {
+  foreach my $makemodel (@{ $object->makemodels_sorted }) {
     my $makemodel_orig = $old_makemodels_by_mm{$makemodel->make,$makemodel->model};
     $found_any = 1;
 
@@ -461,11 +663,11 @@ sub handle_makemodel {
     }
   }
 
-  $entry->{part}->makemodels([ $entry->{part}->makemodels, @new_makemodels ]) if @new_makemodels;
+  $entry->{part}->makemodels([ @{$entry->{part}->makemodels_sorted}, @new_makemodels ]) if @new_makemodels;
 
   # reindex makemodels
   my $i = 0;
-  $_->sortorder(++$i) for @{ $entry->{part}->makemodels };
+  $_->sortorder(++$i) for @{ $entry->{part}->makemodels_sorted };
 
   $self->save_with_cascade(1) if $found_any;
 }
@@ -473,14 +675,16 @@ sub handle_makemodel {
 sub set_various_fields {
   my ($self, $entry) = @_;
 
-  $entry->{object}->priceupdate(DateTime->now_local);
+  my $object = $entry->{object_to_save} || $entry->{object};
+
+  $object->priceupdate(DateTime->now_local);
 }
 
 sub init_profile {
   my ($self) = @_;
 
   my $profile = $self->SUPER::init_profile;
-  delete @{$profile}{qw(alternate assembly bom expense_accno_id income_accno_id inventory_accno_id makemodel priceupdate stockable type)};
+  delete @{$profile}{qw(bom makemodel priceupdate stockable type)};
 
   $profile->{"pricegroup_$_"} = '' for 1 .. scalar @{ $_[0]->all_pricegroups };
 
@@ -494,7 +698,6 @@ sub save_objects {
   my $without_number = [ grep { $_->{object}->partnumber eq '####' } @{ $self->controller->data } ];
 
   map { $_->{object}->partnumber('') } @{ $without_number };
-
   $self->SUPER::save_objects(data => $with_number);
   $self->SUPER::save_objects(data => $without_number);
 }
@@ -505,9 +708,10 @@ sub setup_displayable_columns {
   $self->SUPER::setup_displayable_columns;
   $self->add_cvar_columns_to_displayable_columns;
 
-  $self->add_displayable_columns({ name => 'bin',                description => $::locale->text('Bin')                                                  },
-                                 { name => 'buchungsgruppen_id', description => $::locale->text('Buchungsgruppe (database ID)')                         },
-                                 { name => 'buchungsgruppe',     description => $::locale->text('Buchungsgruppe (name)')                                },
+  $self->add_displayable_columns({ name => 'bin_id',             description => $::locale->text('Bin (database ID)')                                    },
+                                 { name => 'bin',                description => $::locale->text('Bin (name)')                                           },
+                                 { name => 'buchungsgruppen_id', description => $::locale->text('Booking group (database ID)')                          },
+                                 { name => 'buchungsgruppe',     description => $::locale->text('Booking group (name)')                                 },
                                  { name => 'description',        description => $::locale->text('Description')                                          },
                                  { name => 'drawing',            description => $::locale->text('Drawing')                                              },
                                  { name => 'ean',                description => $::locale->text('EAN')                                                  },
@@ -528,16 +732,19 @@ sub setup_displayable_columns {
                                  { name => 'partnumber',         description => $::locale->text('Part Number')                                          },
                                  { name => 'partsgroup_id',      description => $::locale->text('Partsgroup (database ID)')                             },
                                  { name => 'partsgroup',         description => $::locale->text('Partsgroup (name)')                                    },
+                                 { name => 'part_classification',description => $::locale->text('Article classification')  . ' [3]'                     },
                                  { name => 'payment_id',         description => $::locale->text('Payment terms (database ID)')                          },
                                  { name => 'payment',            description => $::locale->text('Payment terms (name)')                                 },
                                  { name => 'price_factor_id',    description => $::locale->text('Price factor (database ID)')                           },
                                  { name => 'price_factor',       description => $::locale->text('Price factor (name)')                                  },
                                  { name => 'rop',                description => $::locale->text('ROP')                                                  },
                                  { name => 'sellprice',          description => $::locale->text('Sellprice')                                            },
-                                 { name => 'shop',               description => $::locale->text('Shopartikel')                                          },
-                                 { name => 'type',               description => $::locale->text('Article type')  . ' [3]'                             },
+                                 { name => 'shop',               description => $::locale->text('Shop article')                                         },
+                                 { name => 'type',               description => $::locale->text('Article type')  . ' [3]'                               },
                                  { name => 'unit',               description => $::locale->text('Unit (if missing or empty default unit will be used)') },
                                  { name => 've',                 description => $::locale->text('Verrechnungseinheit')                                  },
+                                 { name => 'warehouse_id',       description => $::locale->text('Warehouse (database ID)')                              },
+                                 { name => 'warehouse',          description => $::locale->text('Warehouse (name)')                                     },
                                  { name => 'weight',             description => $::locale->text('Weight')                                               },
                                 );
 
index 4b846dc..4fea748 100644 (file)
@@ -32,7 +32,7 @@ sub check_objects {
 
   $self->controller->track_progress(phase => 'building data', progress => 0);
 
-  my $i;
+  my $i = 0;
   my $num_data = scalar @{ $self->controller->data };
   foreach my $entry (@{ $self->controller->data }) {
     $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
index e32da46..1ca25ad 100644 (file)
@@ -24,7 +24,7 @@ sub check_objects {
 
   $self->controller->track_progress(phase => 'building data', progress => 0);
 
-  my $i;
+  my $i = 0;
   my $num_data = scalar @{ $self->controller->data };
   foreach my $entry (@{ $self->controller->data }) {
     $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
@@ -112,6 +112,7 @@ sub setup_displayable_columns {
                                  { name => 'shiptodepartment_2', description => $::locale->text('Department 2')                  },
                                  { name => 'shiptoemail',        description => $::locale->text('E-mail')                        },
                                  { name => 'shiptofax',          description => $::locale->text('Fax')                           },
+                                 { name => 'shiptogln',          description => $::locale->text('GLN')                           },
                                  { name => 'shiptoname',         description => $::locale->text('Name')                          },
                                  { name => 'shiptophone',        description => $::locale->text('Phone')                         },
                                  { name => 'shiptostreet',       description => $::locale->text('Street')                        },
@@ -119,8 +120,10 @@ sub setup_displayable_columns {
                                  { name => 'trans_id',           description => $::locale->text('Customer/Vendor (database ID)') },
                                  { name => 'customer',           description => $::locale->text('Customer (name)')               },
                                  { name => 'customernumber',     description => $::locale->text('Customer Number')               },
+                                 { name => 'customer_gln',       description => $::locale->text('Customer GLN')                  },
                                  { name => 'vendor',             description => $::locale->text('Vendor (name)')                 },
                                  { name => 'vendornumber',       description => $::locale->text('Vendor Number')                 },
+                                 { name => 'vendor_gln',         description => $::locale->text('Vendor GLN')                    },
                                 );
 }
 
diff --git a/SL/Controller/CustomDataExport.pm b/SL/Controller/CustomDataExport.pm
new file mode 100644 (file)
index 0000000..7d5edde
--- /dev/null
@@ -0,0 +1,201 @@
+package SL::Controller::CustomDataExport;
+
+use strict;
+use utf8;
+
+use parent qw(SL::Controller::Base);
+
+use DBI qw(:sql_types);
+use File::Temp ();
+use List::UtilsBy qw(sort_by);
+use POSIX qw(strftime);
+use Text::CSV_XS;
+
+use SL::DB::CustomDataExportQuery;
+use SL::Locale::String qw(t8);
+
+use Rose::Object::MakeMethods::Generic
+(
+  scalar                  => [ qw(rows) ],
+  'scalar --get_set_init' => [ qw(query queries parameters) ],
+);
+
+__PACKAGE__->run_before('check_auth');
+__PACKAGE__->run_before('setup_javascripts');
+
+#
+# actions
+#
+
+sub action_list {
+  my ($self) = @_;
+
+  $self->render('custom_data_export/list', title => $::locale->text('Execute a custom data export query'));
+}
+
+sub action_export {
+  my ($self) = @_;
+
+  if (!$::form->{format}) {
+    $self->setup_export_action_bar;
+    return $self->render('custom_data_export/export', title => t8("Execute custom data export '#1'", $self->query->name));
+  }
+
+  $self->execute_query;
+
+  if (scalar(@{ $self->rows // [] }) == 1) {
+    $self->setup_empty_result_set_action_bar;
+    return $self->render('custom_data_export/empty_result_set', title => t8("Execute custom data export '#1'", $self->query->name));
+  }
+
+
+  my $method = "export_as_" . $::form->{format};
+  $self->$method;
+}
+
+#
+# filters
+#
+
+sub check_auth {
+  my ($self) = @_;
+  $::auth->assert($self->query->access_right) if $self->query->access_right;
+}
+
+sub setup_javascripts {
+  $::request->layout->add_javascripts('kivi.Validator.js');
+}
+
+#
+# helpers
+#
+
+sub init_query      { $::form->{id} ? SL::DB::CustomDataExportQuery->new(id => $::form->{id})->load : SL::DB::CustomDataExportQuery->new }
+sub init_parameters { [ sort_by { lc $_->name } @{ $_[0]->query->parameters // [] } ] }
+
+sub init_queries {
+  my %rights_map     = %{ $::auth->load_rights_for_user($::form->{login}) };
+  my @granted_rights = grep { $rights_map{$_} } keys %rights_map;
+
+  return scalar SL::DB::Manager::CustomDataExportQuery->get_all_sorted(
+    where => [
+      or => [
+        access_right => undef,
+        access_right => '',
+        (access_right => \@granted_rights) x !!@granted_rights,
+      ],
+    ],
+  )
+}
+
+sub setup_export_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Export'),
+        submit    => [ '#form', { action => 'CustomDataExport/export' } ],
+        checks    => [ 'kivi.validate_form' ],
+        accesskey => 'enter',
+      ],
+      action => [
+        t8('Back'),
+        call => [ 'kivi.history_back' ],
+      ],
+    );
+  }
+}
+
+sub setup_empty_result_set_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Back'),
+        call => [ 'kivi.history_back' ],
+      ],
+    );
+  }
+}
+
+sub prepare_query {
+  my ($self) = @_;
+
+  my $sql_query = $self->query->sql_query;
+  my @values;
+
+  my %values_by_name;
+
+  foreach my $parameter (@{ $self->query->parameters // [] }) {
+    my $value                           = ($::form->{parameters} // {})->{ $parameter->name };
+    $values_by_name{ $parameter->name } = $parameter->parameter_type eq 'number' ? $::form->parse_amount(\%::myconfig, $value) : $value;
+  }
+
+  while ($sql_query =~ m{<\%(.+?)\%>}) {
+    push @values, $values_by_name{$1};
+    substr($sql_query, $-[0], $+[0] - $-[0], '?');
+  }
+
+  return ($sql_query, @values);
+}
+
+sub execute_query {
+  my ($self) = @_;
+
+  my ($sql_query, @values) = $self->prepare_query;
+  my $sth                  = $self->query->db->dbh->prepare($sql_query) || $::form->dberror;
+  $sth->execute(@values)                                                || $::form->dberror;
+
+  my @names = @{ $sth->{NAME} };
+  my @types = @{ $sth->{TYPE} };
+  my @data  = @{ $sth->fetchall_arrayref };
+
+  $sth->finish;
+
+  foreach my $row (@data) {
+    foreach my $col (0..$#types) {
+      my $type = $types[$col];
+
+      if ($type == SQL_NUMERIC) {
+        $row->[$col] = $::form->format_amount(\%::myconfig, $row->[$col]);
+      }
+    }
+  }
+
+  $self->rows([
+    \@names,
+    @data,
+  ]);
+}
+
+sub export_as_csv {
+  my ($self) = @_;
+
+  my $csv = Text::CSV_XS->new({
+    binary   => 1,
+    sep_char => ';',
+    eol      => "\n",
+  });
+
+  my ($file_handle, $file_name) = File::Temp::tempfile;
+
+  binmode $file_handle, ":encoding(utf8)";
+
+  $csv->print($file_handle, $_) for @{ $self->rows };
+
+  $file_handle->close;
+
+  my $report_name =  $self->query->name;
+  $report_name    =~ s{[^[:word:]]+}{_}ig;
+  $report_name   .=  strftime('_%Y-%m-%d_%H-%M-%S.csv', localtime());
+
+  $self->send_file(
+    $file_name,
+    content_type => 'text/csv',
+    name         => $report_name,
+  );
+}
+
+1;
diff --git a/SL/Controller/CustomDataExportDesigner.pm b/SL/Controller/CustomDataExportDesigner.pm
new file mode 100644 (file)
index 0000000..1726632
--- /dev/null
@@ -0,0 +1,196 @@
+package SL::Controller::CustomDataExportDesigner;
+
+use strict;
+use utf8;
+
+use parent qw(SL::Controller::Base);
+
+use List::UtilsBy qw(sort_by);
+
+use SL::DB::CustomDataExportQuery;
+use SL::Helper::Flash qw(flash_later);
+use SL::Locale::String qw(t8);
+
+use Rose::Object::MakeMethods::Generic
+(
+  'scalar --get_set_init' => [ qw(query queries access_rights) ],
+);
+
+__PACKAGE__->run_before('check_auth');
+__PACKAGE__->run_before('setup_javascripts');
+
+#
+# actions
+#
+
+sub action_list {
+  my ($self) = @_;
+
+  $self->setup_list_action_bar;
+  $self->render('custom_data_export_designer/list', title => $::locale->text('Design custom data export queries'));
+}
+
+sub action_edit {
+  my ($self) = @_;
+
+  my $title = $self->query->id ? t8('Edit custom data export query') : t8('Add custom data export query');
+
+  $self->setup_edit_action_bar;
+  $self->render('custom_data_export_designer/edit', title => $title);
+}
+
+sub action_edit_parameters {
+  my ($self) = @_;
+
+  my $title     = $self->query->id ? t8('Edit custom data export query') : t8('Add custom data export query');
+  my @parameters = $self->gather_query_data;
+
+  $self->setup_edit_parameters_action_bar;
+  $self->render('custom_data_export_designer/edit_parameters', title => $title, PARAMETERS => \@parameters);
+}
+
+sub action_save {
+  my ($self) = @_;
+
+  my @parameters = $self->gather_query_data;
+
+  $self->query->parameters(\@parameters);
+
+  $self->query->save;
+
+  flash_later('info', t8('The custom data export has been saved.'));
+
+  $self->redirect_to($self->url_for(action => 'list'));
+}
+
+sub action_delete {
+  my ($self) = @_;
+
+  $self->query->delete;
+
+  flash_later('info', t8('The custom data export has been deleted.'));
+
+  $self->redirect_to($self->url_for(action => 'list'));
+}
+
+#
+# filters
+#
+
+sub check_auth {
+  $::auth->assert('custom_data_export_designer');
+}
+
+sub setup_javascripts {
+  $::request->layout->add_javascripts('kivi.Validator.js', 'kivi.CustomDataExportDesigner.js');
+}
+
+#
+# helpers
+#
+
+sub init_query   { $::form->{id} ? SL::DB::CustomDataExportQuery->new(id => $::form->{id})->load : SL::DB::CustomDataExportQuery->new }
+sub init_queries { scalar SL::DB::Manager::CustomDataExportQuery->get_all_sorted }
+
+sub init_access_rights {
+  my @rights = ([ '', t8('Available to all users') ]);
+  my $category;
+
+  foreach my $right ($::auth->all_rights_full) {
+    # name, description, category
+
+    if ($right->[2]) {
+      $category = t8($right->[1]);
+    } elsif ($category) {
+      push @rights, [ $right->[0], sprintf('%s → %s [%s]', $category, t8($right->[1]), $right->[0]) ];
+    }
+  }
+
+  return \@rights;
+}
+
+sub setup_list_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      link => [
+        t8('Add'),
+        link      => $self->url_for(action => 'edit'),
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_edit_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Continue'),
+        submit    => [ '#form', { action => 'CustomDataExportDesigner/edit_parameters' } ],
+        checks    => [ 'kivi.validate_form' ],
+        accesskey => 'enter',
+      ],
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => 'CustomDataExportDesigner/delete' } ],
+        confirm  => t8('Do you really want to delete this object?'),
+        disabled => !$self->query->id ? t8('This object has not been saved yet.')
+                  :                      undef,
+      ],
+      action => [
+        t8('Back'),
+        call => [ 'kivi.history_back' ],
+      ],
+    );
+  }
+}
+
+sub setup_edit_parameters_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => 'CustomDataExportDesigner/save' } ],
+        checks    => [ 'kivi.validate_form' ],
+        accesskey => 'enter',
+      ],
+      action => [
+        t8('Back'),
+        call => [ 'kivi.history_back' ],
+      ],
+    );
+  }
+}
+
+sub gather_query_data {
+  my ($self) = @_;
+
+  $self->query->$_($::form->{query}->{$_}) for qw(name description sql_query access_right);
+  return $self->gather_query_parameters;
+}
+
+sub gather_query_parameters {
+  my ($self) = @_;
+
+  my %used_parameter_names  = map  { ($_ => 1) }                       $self->query->used_parameter_names;
+  my @existing_parameters   = grep { $used_parameter_names{$_->name} } @{ $self->query->parameters // [] };
+  my %parameters_by_name    = map  { ($_->name => $_) }                @existing_parameters;
+  $parameters_by_name{$_} //= SL::DB::CustomDataExportQueryParameter->new(name => $_, parameter_type => 'text', default_value_type => 'none') for keys %used_parameter_names;
+
+  foreach my $parameter_data (@{ $::form->{parameters} // [] }) {
+    my $parameter_obj = $parameters_by_name{ $parameter_data->{name} };
+    next unless $parameter_obj;
+
+    $parameter_obj->$_($parameter_data->{$_}) for qw(parameter_type description default_value_type default_value);
+  }
+
+  return sort_by { lc $_->name } values %parameters_by_name;
+}
+
+1;
index 456fe71..193523a 100644 (file)
@@ -25,6 +25,7 @@ __PACKAGE__->run_before('load_config', only => [ qw(edit update destroy) ]);
 our %translations = (
   text      => t8('Free-form text'),
   textfield => t8('Text field'),
+  htmlfield => t8('HTML field'),
   number    => t8('Number'),
   date      => t8('Date'),
   timestamp => t8('Timestamp'),
@@ -35,7 +36,7 @@ our %translations = (
   part      => t8('Part'),
 );
 
-our @types = qw(text textfield number date bool select customer vendor part); # timestamp
+our @types = qw(text textfield htmlfield number date bool select customer vendor part); # timestamp
 
 #
 # actions
@@ -46,6 +47,7 @@ sub action_list {
 
   my $configs = SL::DB::Manager::CustomVariableConfig->get_all_sorted(where => [ module => $self->module ]);
 
+  $self->setup_list_action_bar;
   $::form->header;
   $self->render('custom_variable_config/list',
                 title   => t8('List of custom variables'),
@@ -70,6 +72,7 @@ sub show_form {
   $params{all_partsgroups} = SL::DB::Manager::PartsGroup->get_all();
 
   $::request->layout->use_javascript("${_}.js") for qw(jquery.selectboxes jquery.multiselect2side);
+  $self->setup_form_action_bar;
   $self->render('custom_variable_config/form', %params);
 }
 
@@ -164,6 +167,7 @@ sub init_modules {
     { module => 'IC',               description => t8('Parts, services and assemblies') },
     { module => 'Projects',         description => t8('Projects')                       },
     { module => 'RequirementSpecs', description => t8('Requirement Specs')              },
+    { module => 'ShipTo',           description => t8('Shipping Address')               },
   )];
 }
 
@@ -196,13 +200,13 @@ sub create_or_update {
     return;
   }
 
-  my $dbh = $self->config->db;
-  $dbh->begin_work;
+  SL::DB->client->with_transaction(sub {
+    my $dbh = SL::DB->client->dbh;
 
-  $self->config->save;
-  $self->_set_cvar_validity() if $is_new;
-
-  $dbh->commit;
+    $self->config->save;
+    $self->_set_cvar_validity() if $is_new;
+    1;
+  }) or do { die SL::DB->client->error };
 
   flash_later('info', $is_new ? t8('The custom variable has been created.') : t8('The custom variable has been saved.'));
   $self->redirect_to(action => 'list', module => $self->module);
@@ -225,4 +229,57 @@ sub _set_cvar_validity {
   }
 }
 
+sub setup_list_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Add'),
+        link => $self->url_for(action => 'new', module => $self->module),
+      ],
+    );
+  }
+}
+
+sub setup_form_action_bar {
+  my ($self) = @_;
+
+  my $is_new = !$self->config->id;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [
+          t8('Save'),
+          submit    => [ '#form', { action => 'CustomVariableConfig/' . ($is_new ? 'create' : 'update') } ],
+          checks    => [ 'check_prerequisites' ],
+          accesskey => 'enter',
+        ],
+
+        action => [
+          t8('Save as new'),
+          submit => [ '#form', { action => 'CustomVariableConfig/create'} ],
+          checks => [ 'check_prerequisites' ],
+          not_if => $is_new,
+        ],
+      ], # end of combobox "Save"
+
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => 'CustomVariableConfig/destroy' } ],
+        confirm  => t8('Do you really want to delete this object?'),
+        disabled => $is_new ? t8('This object has not been saved yet.') : undef,
+      ],
+
+      'separator',
+
+      link => [
+        t8('Abort'),
+        link => $self->url_for(action => 'list', module => $self->module),
+      ],
+    );
+  }
+}
+
 1;
index d6ba13d..34c3fe7 100644 (file)
@@ -6,9 +6,6 @@ use parent qw(SL::Controller::Base);
 use SL::DB::Customer;
 use SL::JSON;
 
-# safety
-__PACKAGE__->run_before(sub { $::auth->assert('customer_vendor_edit') });
-
 sub action_get_hourly_rate {
   my ($self, %params) = @_;
 
index dbe1827..f9e2515 100644 (file)
@@ -3,37 +3,50 @@ package SL::Controller::CustomerVendor;
 use strict;
 use parent qw(SL::Controller::Base);
 
+use List::MoreUtils qw(any);
+
 use SL::JSON;
 use SL::DBUtils;
 use SL::Helper::Flash;
 use SL::Locale::String;
+use SL::Util qw(trim);
+use SL::VATIDNr;
+use SL::Webdav;
+use SL::ZUGFeRD;
 use SL::Controller::Helper::GetModels;
+use SL::Controller::Helper::ReportGenerator;
+use SL::Controller::Helper::ParseFilter;
 
 use SL::DB::Customer;
 use SL::DB::Vendor;
 use SL::DB::Business;
+use SL::DB::ContactDepartment;
+use SL::DB::ContactTitle;
 use SL::DB::Employee;
+use SL::DB::Greeting;
 use SL::DB::Language;
 use SL::DB::TaxZone;
 use SL::DB::Note;
 use SL::DB::PaymentTerm;
 use SL::DB::Pricegroup;
+use SL::DB::Price;
 use SL::DB::Contact;
 use SL::DB::FollowUp;
 use SL::DB::FollowUpLink;
 use SL::DB::History;
 use SL::DB::Currency;
+use SL::DB::Invoice;
+use SL::DB::PurchaseInvoice;
+use SL::DB::Order;
+
+use Data::Dumper;
 
 use Rose::Object::MakeMethods::Generic (
-  'scalar --get_set_init' => [ qw(customer_models vendor_models) ],
+  scalar                  => [ qw(user_has_edit_rights) ],
+  'scalar --get_set_init' => [ qw(customer_models vendor_models zugferd_settings) ],
 );
 
 # safety
-__PACKAGE__->run_before(
-  sub {
-    $::auth->assert('customer_vendor_edit');
-  }
-);
 __PACKAGE__->run_before(
   '_instantiate_args',
   only => [
@@ -48,6 +61,7 @@ __PACKAGE__->run_before(
     'delete',
     'delete_contact',
     'delete_shipto',
+    'delete_additional_billing_address',
   ]
 );
 
@@ -58,9 +72,15 @@ __PACKAGE__->run_before(
     'show',
     'update',
     'ajaj_get_shipto',
+    'ajaj_get_additional_billing_address',
     'ajaj_get_contact',
+    'ajax_list_prices',
   ]
 );
+
+# make sure this comes after _load_customer_vendor
+__PACKAGE__->run_before('_check_auth');
+
 __PACKAGE__->run_before(
   '_create_customer_vendor',
   only => [
@@ -70,12 +90,18 @@ __PACKAGE__->run_before(
 
 __PACKAGE__->run_before('normalize_name');
 
+my @ADDITIONAL_BILLING_ADDRESS_COLUMNS = qw(name department_1 department_2 contact street zipcode city country gln email phone fax default_address);
 
 sub action_add {
   my ($self) = @_;
 
   $self->_pre_render();
-  $self->{cv}->assign_attributes(hourly_rate => $::instance_conf->get_customer_hourly_rate) if $self->{cv}->is_customer;
+
+  if ($self->{cv}->is_customer) {
+    $self->{cv}->assign_attributes(hourly_rate => $::instance_conf->get_customer_hourly_rate);
+    $self->{cv}->salesman_id(SL::DB::Manager::Employee->current->id) if !$::auth->assert('customer_vendor_all_edit', 1);
+  }
+
   $self->render(
     'customer_vendor/form',
     title => ($self->is_vendor() ? $::locale->text('Add Vendor') : $::locale->text('Add Customer')),
@@ -110,6 +136,62 @@ sub action_show {
   }
 }
 
+sub _check_ustid_taxnumber_unique {
+  my ($self) = @_;
+
+  my %cfg;
+  if ($self->is_vendor()) {
+    %cfg = (should_check  => $::instance_conf->get_vendor_ustid_taxnummer_unique,
+            manager_class => 'SL::DB::Manager::Vendor',
+            err_ustid     => t8('A vendor with the same VAT ID already exists.'),
+            err_taxnumber => t8('A vendor with the same taxnumber already exists.'),
+    );
+
+  } elsif ($self->is_customer()) {
+    %cfg = (should_check  => $::instance_conf->get_customer_ustid_taxnummer_unique,
+            manager_class => 'SL::DB::Manager::Customer',
+            err_ustid     => t8('A customer with the same VAT ID already exists.'),
+            err_taxnumber => t8('A customer with the same taxnumber already exists.'),
+    );
+
+  } else {
+    return;
+  }
+
+  my @errors;
+
+  if ($cfg{should_check}) {
+    my $do_clean_taxnumber = sub { my $n = $_[0]; $n //= ''; $n =~ s{[[:space:].-]+}{}g; return $n};
+
+    my $clean_ustid     = SL::VATIDNr->clean($self->{cv}->ustid);
+    my $clean_taxnumber = $do_clean_taxnumber->($self->{cv}->taxnumber);
+
+    if (!($clean_ustid || $clean_taxnumber)) {
+      return t8('VAT ID and/or taxnumber must be given.');
+
+    } else {
+      my $clean_number = $clean_ustid;
+      if ($clean_number) {
+        my $entries = $cfg{manager_class}->get_all(query => ['!id' => $self->{cv}->id, '!ustid' => undef, '!ustid' => ''], select => ['ustid'], distinct => 1);
+        if (any { $clean_number eq SL::VATIDNr->clean($_->ustid) } @$entries) {
+          push @errors, $cfg{err_ustid};
+        }
+      }
+
+      $clean_number = $clean_taxnumber;
+      if ($clean_number) {
+        my $entries = $cfg{manager_class}->get_all(query => ['!id' => $self->{cv}->id, '!taxnumber' => undef, '!taxnumber' => ''], select => ['taxnumber'], distinct => 1);
+        if (any { $clean_number eq $do_clean_taxnumber->($_->taxnumber) } @$entries) {
+          push @errors, $cfg{err_taxnumber};
+        }
+      }
+    }
+  }
+
+  return join "\n", @errors if @errors;
+  return;
+}
+
 sub _save {
   my ($self) = @_;
 
@@ -122,12 +204,27 @@ sub _save {
       title => ($self->is_vendor() ? t8('Edit Vendor') : t8('Edit Customer')),
       %{$self->{template_args}}
     );
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
 
+  $self->{cv}->greeting(trim $self->{cv}->greeting);
+  my $save_greeting           = $self->{cv}->greeting
+    && $::instance_conf->get_vc_greetings_use_textfield
+    && SL::DB::Manager::Greeting->get_all_count(where => [description => $self->{cv}->greeting]) == 0;
+
+  $self->{contact}->cp_title(trim($self->{contact}->cp_title));
+  my $save_contact_title      = $self->{contact}->cp_title
+    && $::instance_conf->get_contact_titles_use_textfield
+    && SL::DB::Manager::ContactTitle->get_all_count(where => [description => $self->{contact}->cp_title]) == 0;
+
+  $self->{contact}->cp_abteilung(trim($self->{contact}->cp_abteilung));
+  my $save_contact_department = $self->{contact}->cp_abteilung
+    && $::instance_conf->get_contact_departments_use_textfield
+    && SL::DB::Manager::ContactDepartment->get_all_count(where => [description => $self->{contact}->cp_abteilung]) == 0;
+
   my $db = $self->{cv}->db;
 
-  $db->do_transaction(sub {
+  $db->with_transaction(sub {
     my $cvs_by_nr;
     if ( $self->is_vendor() ) {
       if ( $self->{cv}->vendornumber ) {
@@ -148,10 +245,18 @@ sub _save {
       }
     }
 
+    my $ustid_taxnumber_error = $self->_check_ustid_taxnumber_unique;
+    $::form->error($ustid_taxnumber_error) if $ustid_taxnumber_error;
+
     $self->{cv}->save(cascade => 1);
 
+    SL::DB::Greeting->new(description => $self->{cv}->greeting)->save if $save_greeting;
+
     $self->{contact}->cp_cv_id($self->{cv}->id);
     if( $self->{contact}->cp_name ne '' || $self->{contact}->cp_givenname ne '' ) {
+      SL::DB::ContactTitle     ->new(description => $self->{contact}->cp_title)    ->save if $save_contact_title;
+      SL::DB::ContactDepartment->new(description => $self->{contact}->cp_abteilung)->save if $save_contact_department;
+
       $self->{contact}->save(cascade => 1);
     }
 
@@ -174,8 +279,23 @@ sub _save {
     }
 
     $self->{shipto}->trans_id($self->{cv}->id);
-    if( $self->{shipto}->shiptoname ne '' ) {
-      $self->{shipto}->save();
+    if(any { $self->{shipto}->$_ ne '' } qw(shiptoname shiptodepartment_1 shiptodepartment_2 shiptostreet shiptozipcode shiptocity shiptocountry shiptogln shiptocontact shiptophone shiptofax shiptoemail)) {
+      $self->{shipto}->save(cascade => 1);
+    }
+
+    if ($self->is_customer && any { $self->{additional_billing_address}->$_ ne '' } grep { $_ ne 'default_address' } @ADDITIONAL_BILLING_ADDRESS_COLUMNS) {
+      $self->{additional_billing_address}->customer_id($self->{cv}->id);
+      $self->{additional_billing_address}->save(cascade => 1);
+
+      # Make sure only one address per customer has "default address" set.
+      if ($self->{additional_billing_address}->default_address) {
+        SL::DB::Manager::AdditionalBillingAddress->update_all(
+          set   => { default_address => 0, },
+          where => [
+            customer_id => $self->{cv}->id,
+            '!id'       => $self->{additional_billing_address}->id,
+          ]);
+      }
     }
 
     my $snumbers = $self->is_vendor() ? 'vendornumber_'. $self->{cv}->vendornumber : 'customernumber_'. $self->{cv}->customernumber;
@@ -198,6 +318,8 @@ sub _save {
         $note->delete(cascade => 'delete');
       }
     }
+
+    1;
   }) || die($db->error);
 
 }
@@ -221,6 +343,10 @@ sub action_save {
     push(@redirect_params, shipto_id => $self->{shipto}->shipto_id);
   }
 
+  if ( $self->is_customer && $self->{additional_billing_address}->id ) {
+    push(@redirect_params, additional_billing_address_id => $self->{additional_billing_address}->id);
+  }
+
   $self->redirect_to(@redirect_params);
 }
 
@@ -236,23 +362,29 @@ sub action_save_and_close {
 sub _transaction {
   my ($self, $script) = @_;
 
-  $::auth->assert('general_ledger         | invoice_edit         | vendor_invoice_edit | ' .
+  $::auth->assert('gl_transactions | ap_transactions | ar_transactions'.
+                    '| invoice_edit         | vendor_invoice_edit | ' .
                  ' request_quotation_edit | sales_quotation_edit | sales_order_edit    | purchase_order_edit');
 
   $self->_save();
 
-  my $callback = $::form->escape($::form->{callback}, 1);
   my $name = $::form->escape($self->{cv}->name, 1);
   my $db = $self->is_vendor() ? 'vendor' : 'customer';
+  my $action = 'add';
+
+  if ($::instance_conf->get_feature_experimental_order && 'oe.pl' eq $script) {
+    $script = 'controller.pl';
+    $action = 'Order/' . $action;
+  }
 
   my $url = $self->url_for(
     controller => $script,
-    action     => 'add',
+    action     => $action,
     vc         => $db,
     $db .'_id' => $self->{cv}->id,
     $db        => $name,
     type       => $::form->{type},
-    callback   => $callback,
+    callback   => $::form->{callback},
   );
 
   print $::form->redirect_header($url);
@@ -261,7 +393,7 @@ sub _transaction {
 sub action_save_and_ar_transaction {
   my ($self) = @_;
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ar_transactions');
 
   $self->_transaction('ar.pl');
 }
@@ -269,7 +401,7 @@ sub action_save_and_ar_transaction {
 sub action_save_and_ap_transaction {
   my ($self) = @_;
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ap_transactions');
 
   $self->_transaction('ap.pl');
 }
@@ -327,7 +459,7 @@ sub action_delete {
     $self->action_edit();
   } else {
 
-    $db->do_transaction(sub {
+    $db->with_transaction(sub {
       $self->{cv}->delete(cascade => 1);
 
       my $snumbers = $self->is_vendor() ? 'vendornumber_'. $self->{cv}->vendornumber : 'customernumber_'. $self->{cv}->customernumber;
@@ -355,7 +487,7 @@ sub action_delete_contact {
     SL::Helper::Flash::flash('error', $::locale->text('No contact selected to delete'));
   } else {
 
-    $db->do_transaction(sub {
+    $db->with_transaction(sub {
       if ( $self->{contact}->used ) {
         $self->{contact}->detach();
         $self->{contact}->save();
@@ -364,6 +496,8 @@ sub action_delete_contact {
         $self->{contact}->delete(cascade => 1);
         SL::Helper::Flash::flash('info', $::locale->text('Contact deleted.'));
       }
+
+      1;
     }) || die($db->error);
 
     $self->{contact} = $self->_new_contact_object;
@@ -381,7 +515,7 @@ sub action_delete_shipto {
     SL::Helper::Flash::flash('error', $::locale->text('No shipto selected to delete'));
   } else {
 
-    $db->do_transaction(sub {
+    $db->with_transaction(sub {
       if ( $self->{shipto}->used ) {
         $self->{shipto}->detach();
         $self->{shipto}->save(cascade => 1);
@@ -390,6 +524,8 @@ sub action_delete_shipto {
         $self->{shipto}->delete(cascade => 1);
         SL::Helper::Flash::flash('info', $::locale->text('Shipto deleted.'));
       }
+
+      1;
     }) || die($db->error);
 
     $self->{shipto} = SL::DB::Shipto->new();
@@ -398,6 +534,32 @@ sub action_delete_shipto {
   $self->action_edit();
 }
 
+sub action_delete_additional_billing_address {
+  my ($self) = @_;
+
+  my $db = $self->{additional_billing_address}->db;
+
+  if ( !$self->{additional_billing_address}->id ) {
+    SL::Helper::Flash::flash('error', $::locale->text('No address selected to delete'));
+  } else {
+    $db->with_transaction(sub {
+      if ( $self->{additional_billing_address}->used ) {
+        $self->{additional_billing_address}->detach;
+        $self->{additional_billing_address}->save(cascade => 1);
+        SL::Helper::Flash::flash('info', $::locale->text('Address is in use and was flagged invalid.'));
+      } else {
+        $self->{additional_billing_address}->delete(cascade => 1);
+        SL::Helper::Flash::flash('info', $::locale->text('Address deleted.'));
+      }
+
+      1;
+    }) || die($db->error);
+
+    $self->{additional_billing_address} = SL::DB::AdditionalBillingAddress->new;
+  }
+
+  $self->action_edit;
+}
 
 sub action_search {
   my ($self) = @_;
@@ -428,11 +590,11 @@ sub action_search_contact {
   print $::form->redirect_header($url);
 }
 
-
 sub action_get_delivery {
   my ($self) = @_;
 
-  $::auth->assert('sales_all_edit');
+  $::auth->assert('sales_all_edit')    if $self->is_customer();
+  $::auth->assert('purchase_all_edit') if $self->is_vendor();
 
   my $dbh = $::form->get_standard_dbh();
 
@@ -507,16 +669,31 @@ sub action_get_delivery {
 sub action_ajaj_get_shipto {
   my ($self) = @_;
 
-  my $data = {
+  my $data = {};
+  $data->{shipto} = {
     map(
       {
         my $name = 'shipto'. $_;
         $name => $self->{shipto}->$name;
       }
-      qw(_id name department_1 department_2 street zipcode city country contact phone fax email)
+      qw(_id name department_1 department_2 street zipcode city gln country contact phone fax email)
     )
   };
 
+  $data->{shipto_cvars} = $self->_prepare_cvar_configs_for_ajaj($self->{shipto}->cvars_by_config);
+
+  $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
+}
+
+sub action_ajaj_get_additional_billing_address {
+  my ($self) = @_;
+
+  my $data = {
+    additional_billing_address => {
+      map { ($_ => $self->{additional_billing_address}->$_) } ('id', @ADDITIONAL_BILLING_ADDRESS_COLUMNS)
+    },
+  };
+
   $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
 }
 
@@ -538,34 +715,16 @@ sub action_ajaj_get_contact {
       }
       qw(
         id gender abteilung title position givenname name email phone1 phone2 fax mobile1 mobile2
-        satphone satfax project street zipcode city privatphone privatemail birthday
+        satphone satfax project street zipcode city privatphone privatemail birthday main
       )
     )
   };
 
-  $data->{contact_cvars} = {
-    map {
-      my $cvar   = $_;
-      my $result = { type => $cvar->config->type };
-
-      if ($cvar->config->type eq 'number') {
-        $result->{value} = $::form->format_amount(\%::myconfig, $cvar->value, -2);
+  $data->{contact_cvars} = $self->_prepare_cvar_configs_for_ajaj($self->{contact}->cvars_by_config);
 
-      } elsif ($result->{type} =~ m{customer|vendor|part}) {
-        my $object       = $cvar->value;
-        my $method       = $result->{type} eq 'part' ? 'description' : 'name';
-
-        $result->{id}    = int($cvar->number_value) || undef;
-        $result->{value} = $object ? $object->$method // '' : '';
-
-      } else {
-        $result->{value} = $cvar->value;
-      }
-
-      ( $cvar->config->name => $result )
-
-    } grep { $_->is_valid } @{ $self->{contact}->cvars_by_config }
-  };
+  # avoid two or more main_cp
+  my $has_main_cp = grep { $_->cp_main == 1 } @{ $self->{cv}->contacts };
+  $data->{contact}->{disable_cp_main} = 1 if ($has_main_cp && !$data->{contact}->{cp_main});
 
   $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
 }
@@ -589,8 +748,8 @@ sub action_ajaj_autocomplete {
   }
 
   # if someone types something, and hits enter, assume he entered the full name.
-  # if something matches, treat that as sole match
-  # unfortunately get_models can't do more than one per package atm, so we d it
+  # if something matches, treat that as the sole match
+  # unfortunately get_models can't do more than one per package atm, so we do it
   # the oldfashioned way.
   if ($::form->{prefer_exact}) {
     my $exact_matches;
@@ -626,10 +785,65 @@ sub action_ajaj_autocomplete {
 }
 
 sub action_test_page {
-  $::request->{layout}->add_javascripts('autocomplete_customer.js');
   $_[0]->render('customer_vendor/test_page');
 }
 
+sub action_ajax_list_prices {
+  my ($self, %params) = @_;
+
+  my $report   = SL::ReportGenerator->new(\%::myconfig, $::form);
+  my @columns  = qw(partnumber description price);
+  my @visible  = qw(partnumber description price);
+  my @sortable = qw(partnumber description price);
+
+  my %column_defs = (
+    partnumber  => { text => $::locale->text('Part Number'),      sub => sub { $_[0]->parts->partnumber  } },
+    description => { text => $::locale->text('Part Description'), sub => sub { $_[0]->parts->description } },
+    price       => { text => $::locale->text('Price'),            sub => sub { $::form->format_amount(\%::myconfig, $_[0]->price, 2) }, align => 'right' },
+  );
+
+  $::form->{sort_by}  ||= 'partnumber';
+  $::form->{sort_dir} //= 1;
+
+  for my $col (@sortable) {
+    $column_defs{$col}{link} = $self->url_for(
+      action   => 'ajax_list_prices',
+      callback => $::form->{callback},
+      db       => $::form->{db},
+      id       => $self->{cv}->id,
+      sort_by  => $col,
+      sort_dir => ($::form->{sort_by} eq $col ? 1 - $::form->{sort_dir} : $::form->{sort_dir})
+    );
+  }
+
+  map { $column_defs{$_}{visible} = 1 } @visible;
+
+  my $pricegroup;
+  $pricegroup = $self->{cv}->pricegroup->pricegroup if $self->{cv}->pricegroup;
+
+  $report->set_columns(%column_defs);
+  $report->set_column_order(@columns);
+  $report->set_options(allow_pdf_export => 0, allow_csv_export => 0);
+  $report->set_sort_indicator($::form->{sort_by}, $::form->{sort_dir});
+  $report->set_export_options(@{ $params{report_generator_export_options} || [] });
+  $report->set_options(
+    %{ $params{report_generator_options} || {} },
+    output_format        => 'HTML',
+    top_info_text        => $::locale->text('Pricegroup') . ': ' . $pricegroup,
+    title                => $::locale->text('Price List'),
+  );
+
+  my $sort_param = $::form->{sort_by} eq 'price'       ? 'price'             :
+                   $::form->{sort_by} eq 'description' ? 'parts.description' :
+                   'parts.partnumber';
+  $sort_param .= ' ' . ($::form->{sort_dir} ? 'ASC' : 'DESC');
+  my $prices = SL::DB::Manager::Price->get_all(where        => [ pricegroup_id => $self->{cv}->pricegroup_id ],
+                                               sort_by      => $sort_param,
+                                               with_objects => 'parts');
+
+  $self->report_generator_list_objects(report => $report, objects => $prices, layout => 0, header => 0);
+}
+
 sub is_vendor {
   return $::form->{db} eq 'vendor';
 }
@@ -683,6 +897,17 @@ sub is_orphaned {
   return $self->{_is_orphaned} = !$dummy;
 }
 
+sub _copy_form_to_cvars {
+  my ($self, %params) = @_;
+
+  foreach my $cvar (@{ $params{target}->cvars_by_config }) {
+    my $value = $params{source}->{$cvar->config->name};
+    $value    = $::form->parse_amount(\%::myconfig, $value) if $cvar->config->type eq 'number';
+
+    $cvar->value($value);
+  }
+}
+
 sub _instantiate_args {
   my ($self) = @_;
 
@@ -705,16 +930,6 @@ sub _instantiate_args {
 
   $self->{cv}->hourly_rate($::instance_conf->get_customer_hourly_rate) if $self->is_customer && !$self->{cv}->hourly_rate;
 
-  foreach my $cvar (@{$self->{cv}->cvars_by_config()}) {
-    my $value = $::form->{cv_cvars}->{$cvar->config->name};
-
-    if ( $cvar->config->type eq 'number' ) {
-      $value = $::form->parse_amount(\%::myconfig, $value);
-    }
-
-    $cvar->value($value);
-  }
-
   if ( $::form->{note}->{id} ) {
     $self->{note} = SL::DB::Note->new(id => $::form->{note}->{id})->load();
     $self->{note_followup} = $self->{note}->follow_up;
@@ -744,6 +959,15 @@ sub _instantiate_args {
   $self->{shipto}->assign_attributes(%{$::form->{shipto}});
   $self->{shipto}->module('CT');
 
+  if ($self->is_customer) {
+    if ( $::form->{additional_billing_address}->{id} ) {
+      $self->{additional_billing_address} = SL::DB::AdditionalBillingAddress->new(id => $::form->{additional_billing_address}->{id})->load;
+    } else {
+      $self->{additional_billing_address} = SL::DB::AdditionalBillingAddress->new;
+    }
+    $self->{additional_billing_address}->assign_attributes(%{ $::form->{additional_billing_address} });
+  }
+
   if ( $::form->{contact}->{cp_id} ) {
     $self->{contact} = SL::DB::Contact->new(cp_id => $::form->{contact}->{cp_id})->load();
   } else {
@@ -751,15 +975,9 @@ sub _instantiate_args {
   }
   $self->{contact}->assign_attributes(%{$::form->{contact}});
 
-  foreach my $cvar (@{$self->{contact}->cvars_by_config()}) {
-    my $value = $::form->{contact_cvars}->{$cvar->config->name};
-
-    if ( $cvar->config->type eq 'number' ) {
-      $value = $::form->parse_amount(\%::myconfig, $value);
-    }
-
-    $cvar->value($value);
-  }
+  $self->_copy_form_to_cvars(target => $self->{cv},      source => $::form->{cv_cvars});
+  $self->_copy_form_to_cvars(target => $self->{contact}, source => $::form->{contact_cvars});
+  $self->_copy_form_to_cvars(target => $self->{shipto},  source => $::form->{shipto_cvars});
 }
 
 sub _load_customer_vendor {
@@ -791,6 +1009,16 @@ sub _load_customer_vendor {
     $self->{shipto} = SL::DB::Shipto->new();
   }
 
+  if ($self->is_customer) {
+    if ( $::form->{additional_billing_address_id} ) {
+      $self->{additional_billing_address} = SL::DB::AdditionalBillingAddress->new(id => $::form->{additional_billing_address_id})->load;
+      die($::locale->text('Error')) if $self->{additional_billing_address}->customer_id != $self->{cv}->id;
+
+    } else {
+      $self->{additional_billing_address} = SL::DB::AdditionalBillingAddress->new;
+    }
+  }
+
   if ( $::form->{contact_id} ) {
     $self->{contact} = SL::DB::Contact->new(cp_id => $::form->{contact_id})->load();
 
@@ -802,6 +1030,32 @@ sub _load_customer_vendor {
   }
 }
 
+sub _may_access_action {
+  my ($self, $action)   = @_;
+
+  my $is_new            = !$self->{cv} || !$self->{cv}->id;
+  my $is_own_customer   = !$is_new
+                       && $self->{cv}->is_customer
+                       && (SL::DB::Manager::Employee->current->id == $self->{cv}->salesman_id);
+  my $has_edit_rights   = $::auth->assert('customer_vendor_all_edit', 1);
+  $has_edit_rights    ||= $::auth->assert('customer_vendor_edit',     1) && ($is_new || $is_own_customer);
+  my $needs_edit_rights = $action =~ m{^(?:add|save|delete|update)};
+
+  $self->user_has_edit_rights($has_edit_rights);
+
+  return 1 if $has_edit_rights;
+  return 0 if $needs_edit_rights;
+  return 1;
+}
+
+sub _check_auth {
+  my ($self, $action) = @_;
+
+  if (!$self->_may_access_action($action)) {
+    $::auth->deny_access;
+  }
+}
+
 sub _create_customer_vendor {
   my ($self) = @_;
 
@@ -813,6 +1067,7 @@ sub _create_customer_vendor {
   $self->{note_followup} = SL::DB::FollowUp->new();
 
   $self->{shipto} = SL::DB::Shipto->new();
+  $self->{additional_billing_address} = SL::DB::AdditionalBillingAddress->new if $self->is_customer;
 
   $self->{contact} = $self->_new_contact_object;
 }
@@ -828,33 +1083,24 @@ sub _pre_render {
 
   $self->{all_employees} = SL::DB::Manager::Employee->get_all(query => [ deleted => 0 ]);
 
-  $query =
-    'SELECT DISTINCT(greeting)
-     FROM customer
-     WHERE greeting IS NOT NULL AND greeting != \'\'
-     UNION
-       SELECT DISTINCT(greeting)
-       FROM vendor
-       WHERE greeting IS NOT NULL AND greeting != \'\'
-     ORDER BY greeting';
-  $self->{all_greetings} = [
-    map(
-      { $_->{greeting}; }
-      selectall_hashref_query($::form, $dbh, $query)
-    )
-  ];
-
-  $query =
-    'SELECT DISTINCT(cp_title) AS title
-     FROM contacts
-     WHERE cp_title IS NOT NULL AND cp_title != \'\'
-     ORDER BY cp_title';
-  $self->{all_titles} = [
-    map(
-      { $_->{title}; }
-      selectall_hashref_query($::form, $dbh, $query)
-    )
-  ];
+  $self->{all_greetings} = SL::DB::Manager::Greeting->get_all_sorted();
+  if ($self->{cv}->id && $self->{cv}->greeting && !grep {$self->{cv}->greeting eq $_->description} @{$self->{all_greetings}}) {
+    unshift @{$self->{all_greetings}}, (SL::DB::Greeting->new(description => $self->{cv}->greeting));
+  }
+
+  $self->{all_contact_titles} = SL::DB::Manager::ContactTitle->get_all_sorted();
+  foreach my $contact (@{ $self->{cv}->contacts }) {
+    if ($contact->cp_title && !grep {$contact->cp_title eq $_->description} @{$self->{all_contact_titles}}) {
+      unshift @{$self->{all_contact_titles}}, (SL::DB::ContactTitle->new(description => $contact->cp_title));
+    }
+  }
+
+  $self->{all_contact_departments} = SL::DB::Manager::ContactDepartment->get_all_sorted();
+  foreach my $contact (@{ $self->{cv}->contacts }) {
+    if ($contact->cp_abteilung && !grep {$contact->cp_abteilung eq $_->description} @{$self->{all_contact_departments}}) {
+      unshift @{$self->{all_contact_departments}}, (SL::DB::ContactDepartment->new(description => $contact->cp_abteilung));
+    }
+  }
 
   $self->{all_currencies} = SL::DB::Manager::Currency->get_all();
 
@@ -883,23 +1129,14 @@ sub _pre_render {
     $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(query => [ or => [ id => $self->{cv}->salesman_id,  deleted => 0 ] ]);
   }
 
-  $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all();
+  $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id       => $self->{cv}->payment_id,
+                                                                                               obsolete => 0 ] ]);
 
   $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all();
 
-  $self->{all_pricegroups} = SL::DB::Manager::Pricegroup->get_all();
-
-  $query =
-    'SELECT DISTINCT(cp_abteilung) AS department
-     FROM contacts
-     WHERE cp_abteilung IS NOT NULL AND cp_abteilung != \'\'
-     ORDER BY cp_abteilung';
-  $self->{all_departments} = [
-    map(
-      { $_->{department}; }
-      selectall_hashref_query($::form, $dbh, $query)
-    )
-  ];
+  if ($self->{cv}->is_customer) {
+    $self->{all_pricegroups} = SL::DB::Manager::Pricegroup->get_all_sorted(query => [ or => [ id => $self->{cv}->pricegroup_id, obsolete => 0 ] ]);
+  }
 
   $self->{contacts} = $self->{cv}->contacts;
   $self->{contacts} ||= [];
@@ -907,6 +1144,11 @@ sub _pre_render {
   $self->{shiptos} = $self->{cv}->shipto;
   $self->{shiptos} ||= [];
 
+  if ($self->is_customer) {
+    $self->{additional_billing_addresses} = $self->{cv}->additional_billing_addresses;
+    $self->{additional_billing_addresses} ||= [];
+  }
+
   $self->{notes} = SL::DB::Manager::Note->get_all(
     query => [
       trans_id => $self->{cv}->id,
@@ -915,10 +1157,173 @@ sub _pre_render {
     with_objects => ['follow_up'],
   );
 
+  if ( $self->is_vendor()) {
+    $self->{open_items} = SL::DB::Manager::PurchaseInvoice->get_all_count(
+      query => [
+        vendor_id => $self->{cv}->id,
+        paid => {lt_sql => 'amount'},
+      ],
+    );
+  } else {
+    $self->{open_items} = SL::DB::Manager::Invoice->get_all_count(
+      query => [
+        customer_id => $self->{cv}->id,
+        paid => {lt_sql => 'amount'},
+      ],
+    );
+  }
+
+  if ( $self->is_vendor() ) {
+    $self->{open_orders} = SL::DB::Manager::Order->get_all_count(
+      query => [
+        vendor_id => $self->{cv}->id,
+        closed => 'F',
+      ],
+    );
+  } else {
+    $self->{open_orders} = SL::DB::Manager::Order->get_all_count(
+      query => [
+        customer_id => $self->{cv}->id,
+        closed => 'F',
+      ],
+    );
+  }
+
+  if ($self->{cv}->number && $::instance_conf->get_webdav) {
+    my $webdav = SL::Webdav->new(
+      type     => $self->is_customer ? 'customer'
+                : $self->is_vendor   ? 'vendor'
+                : undef,
+      number   => $self->{cv}->number,
+    );
+    my @all_objects = $webdav->get_all_objects;
+    @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
+                                                    type => t8('File'),
+                                                    link => File::Spec->catfile($_->full_filedescriptor),
+                                                } } @all_objects;
+  }
+
   $self->{template_args} ||= {};
 
-  $::request->{layout}->add_javascripts('autocomplete_customer.js');
-  $::request->{layout}->add_javascripts('kivi.CustomerVendor.js');
+  $::request->{layout}->add_javascripts("$_.js") for qw (kivi.CustomerVendor kivi.File kivi.CustomerVendorTurnover ckeditor/ckeditor ckeditor/adapters/jquery);
+
+  $self->_setup_form_action_bar;
+}
+
+sub _setup_form_action_bar {
+  my ($self) = @_;
+
+  my $no_rights = $self->user_has_edit_rights ? undef
+                : $self->{cv}->is_customer    ? t8("You don't have the rights to edit this customer.")
+                :                               t8("You don't have the rights to edit this vendor.");
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [
+          t8('Save'),
+          submit    => [ '#form', { action => "CustomerVendor/save" } ],
+          checks    => [ 'check_taxzone_and_ustid' ],
+          accesskey => 'enter',
+          disabled  => $no_rights,
+        ],
+        action => [
+          t8('Save and Close'),
+          submit => [ '#form', { action => "CustomerVendor/save_and_close" } ],
+          checks => [ 'check_taxzone_and_ustid' ],
+          disabled => $no_rights,
+        ],
+      ], # end of combobox "Save"
+
+      combobox => [
+        action => [ t8('Workflow') ],
+        (action => [
+          t8('Save and AP Transaction'),
+          submit => [ '#form', { action => "CustomerVendor/save_and_ap_transaction" } ],
+          checks => [ 'check_taxzone_and_ustid' ],
+          disabled => $no_rights,
+        ]) x !!$self->is_vendor,
+        (action => [
+          t8('Save and AR Transaction'),
+          submit => [ '#form', { action => "CustomerVendor/save_and_ar_transaction" } ],
+          checks => [ 'check_taxzone_and_ustid' ],
+          disabled => $no_rights,
+        ]) x !$self->is_vendor,
+        action => [
+          t8('Save and Invoice'),
+          submit => [ '#form', { action => "CustomerVendor/save_and_invoice" } ],
+          checks => [ 'check_taxzone_and_ustid' ],
+          disabled => $no_rights,
+        ],
+        action => [
+          t8('Save and Order'),
+          submit => [ '#form', { action => "CustomerVendor/save_and_order" } ],
+          checks => [ 'check_taxzone_and_ustid' ],
+          disabled => $no_rights,
+        ],
+        (action => [
+          t8('Save and RFQ'),
+          submit => [ '#form', { action => "CustomerVendor/save_and_rfq" } ],
+          checks => [ 'check_taxzone_and_ustid' ],
+          disabled => $no_rights,
+        ]) x !!$self->is_vendor,
+        (action => [
+          t8('Save and Quotation'),
+          submit => [ '#form', { action => "CustomerVendor/save_and_quotation" } ],
+          checks => [ 'check_taxzone_and_ustid' ],
+          disabled => $no_rights,
+        ]) x !$self->is_vendor,
+      ], # end of combobox "Workflow"
+
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => "CustomerVendor/delete" } ],
+        confirm  => t8('Do you really want to delete this object?'),
+        disabled => !$self->{cv}->id    ? t8('This object has not been saved yet.')
+                  : !$self->is_orphaned ? t8('This object has already been used.')
+                  :                       $no_rights,
+      ],
+
+      'separator',
+
+      action => [
+        t8('History'),
+        call     => [ 'kivi.CustomerVendor.showHistoryWindow', $self->{cv}->id ],
+        disabled => !$self->{cv}->id ? t8('This object has not been saved yet.') : undef,
+      ],
+    );
+  }
+}
+
+sub _prepare_cvar_configs_for_ajaj {
+  my ($self, $cvars) = @_;
+
+  return {
+    map {
+      my $cvar   = $_;
+      my $result = { type => $cvar->config->type };
+
+      if ($cvar->config->type eq 'number') {
+        $result->{value} = $::form->format_amount(\%::myconfig, $cvar->value, -2);
+
+      } elsif ($result->{type} eq 'date') {
+        $result->{value} = $cvar->value ? $cvar->value->to_kivitendo : undef;
+
+      } elsif ($result->{type} =~ m{customer|vendor|part}) {
+        my $object       = $cvar->value;
+        my $method       = $result->{type} eq 'part' ? 'description' : 'name';
+
+        $result->{id}    = int($cvar->number_value) || undef;
+        $result->{value} = $object ? $object->$method // '' : '';
+
+      } else {
+        $result->{value} = $cvar->value;
+      }
+
+      ( $cvar->config->name => $result )
+
+    } grep { $_->is_valid } @{ $cvars }
+  };
 }
 
 sub normalize_name {
@@ -978,15 +1383,26 @@ sub init_vendor_models {
   );
 }
 
+sub init_zugferd_settings {
+  return [
+    [ -1, t8('Use settings from client configuration') ],
+    @SL::ZUGFeRD::customer_settings,
+  ],
+}
+
 sub _new_customer_vendor_object {
   my ($self) = @_;
 
   my $class  = 'SL::DB::' . ($self->is_vendor ? 'Vendor' : 'Customer');
-  return $class->new(
+  my $object = $class->new(
     contacts         => [],
     shipto           => [],
     custom_variables => [],
   );
+
+  $object->additional_billing_addresses([]) if $self->is_customer;
+
+  return $object;
 }
 
 sub _new_contact_object {
diff --git a/SL/Controller/CustomerVendorTurnover.pm b/SL/Controller/CustomerVendorTurnover.pm
new file mode 100644 (file)
index 0000000..5a59f1f
--- /dev/null
@@ -0,0 +1,528 @@
+package SL::Controller::CustomerVendorTurnover;
+use strict;
+use parent qw(SL::Controller::Base);
+use SL::DBUtils;
+use SL::DB::AccTransaction;
+use SL::DB::Invoice;
+use SL::DB::Order;
+use SL::DB::EmailJournal;
+use SL::DB::Letter;
+use SL::DB;
+
+__PACKAGE__->run_before('check_auth');
+
+sub action_list_turnover {
+  my ($self) = @_;
+
+  return $self->render('generic/error', { layout => 0 }, label_error => "list_transactions needs a trans_id") unless $::form->{id};
+
+  my $cv = $::form->{id};
+  my $open_invoices;
+  if ( $::form->{db} eq 'customer' ) {
+    $open_invoices = SL::DB::Manager::Invoice->get_all(
+      query        => [
+                        customer_id => $cv,
+                        or          => [
+                                         amount => { gt => \'paid'},
+                                         amount => { lt => \'paid'},
+                                       ],
+                      ],
+      sort_by      => 'transdate DESC',
+      with_objects => [ 'dunnings' ],
+    );
+  } else {
+    $open_invoices = SL::DB::Manager::PurchaseInvoice->get_all(
+      query   => [
+                   vendor_id => $cv,
+                   or        => [
+                                  amount => { gt => \'paid'},
+                                  amount => { lt => \'paid'},
+                                ],
+                 ],
+      sort_by => 'transdate DESC',
+    );
+  }
+  my $open_items;
+  if (@{$open_invoices}) {
+    $open_items = $self->_list_open_items($open_invoices);
+  }
+  my $open_orders = $self->_get_open_orders;
+  return $self->render('customer_vendor_turnover/turnover', { header => 0 },
+                       open_orders => $open_orders,
+                       open_items  => $open_items,
+                       id          => $cv,
+                      );
+}
+
+sub _list_open_items {
+  my ($self, $open_items) = @_;
+
+  return $self->render('customer_vendor_turnover/_list_open_items', { output => 0 },
+                        OPEN_ITEMS => $open_items,
+                        title      => $::locale->text('Open Items'),
+                      );
+}
+
+sub action_count_open_items_by_year {
+  my ($self) = @_;
+
+  return $self->render('generic/error', { layout => 0 }, label_error => "list_transactions needs a trans_id") unless $::form->{id};
+  my $dbh = SL::DB->client->dbh;
+
+  my $cv = $::form->{id};
+
+  my $query = <<SQL;
+   SELECT EXTRACT (YEAR FROM d.transdate),
+          count(d.id),
+          max(d.dunning_level)
+     FROM dunning d
+LEFT JOIN ar a ON a.id = d.trans_id
+LEFT JOIN customer c ON a.customer_id = c.id
+    WHERE c.id = ?
+ GROUP BY EXTRACT (YEAR FROM d.transdate), c.id
+ ORDER BY date_part DESC
+SQL
+
+  $self->{dun_statistic} = selectall_hashref_query($::form, $dbh, $query, $cv);
+  $self->render('customer_vendor_turnover/count_open_items_by_year', { layout => 0 });
+}
+
+sub action_count_open_items_by_month {
+
+  my ($self) = @_;
+
+  return $self->render('generic/error', { layout => 0 }, label_error => "list_transactions needs a trans_id") unless $::form->{id};
+  my $dbh = SL::DB->client->dbh;
+
+  my $cv = $::form->{id};
+
+  my $query = <<SQL;
+   SELECT CONCAT(EXTRACT (MONTH FROM d.transdate),'/',EXTRACT (YEAR FROM d.transdate)) AS date_part,
+          count(d.id),
+          max(d.dunning_level)
+     FROM dunning d
+LEFT JOIN ar a ON a.id = d.trans_id
+LEFT JOIN customer c ON a.customer_id = c.id
+    WHERE c.id = ?
+ GROUP BY EXTRACT (YEAR FROM d.transdate), EXTRACT (MONTH FROM d.transdate), c.id
+ ORDER BY EXTRACT (YEAR FROM d.transdate) DESC
+SQL
+
+   $self->{dun_statistic} = selectall_hashref_query($::form, $dbh, $query, $cv);
+   $self->render('customer_vendor_turnover/count_open_items_by_year', { layout => 0 });
+}
+
+sub action_turnover_by_month {
+
+  my ($self) = @_;
+
+  return $self->render('generic/error', { layout => 0 }, label_error => "list_transactions needs a trans_id") unless $::form->{id};
+
+  my $dbh = SL::DB->client->dbh;
+  my $cv = $::form->{id};
+  my ($db, $cv_type);
+  if ($::form->{db} eq 'customer') {
+    $db      = "ar";
+    $cv_type = "customer_id";
+  } else {
+    $db      = "ap";
+    $cv_type = "vendor_id";
+  }
+  my $query = <<SQL;
+  SELECT CONCAT(EXTRACT (MONTH FROM transdate),'/',EXTRACT (YEAR FROM transdate)) as date_part,
+         count(id)                                                                as count,
+         sum(amount)                                                              as amount,
+         sum(netamount)                                                           as netamount,
+         sum(paid)                                                                as paid
+    FROM $db WHERE $cv_type = ?
+GROUP BY EXTRACT (YEAR FROM transdate), EXTRACT (MONTH FROM transdate)
+ORDER BY EXTRACT (YEAR FROM transdate) DESC, EXTRACT (MONTH FROM transdate) DESC
+SQL
+   $self->{turnover_statistic} = selectall_hashref_query($::form, $dbh, $query, $cv);
+   $self->render('customer_vendor_turnover/count_turnover', { layout => 0 });
+}
+
+sub action_turnover_by_year {
+  my ($self) = @_;
+
+  return $self->render('generic/error', { layout => 0 }, label_error => "list_transactions needs a trans_id") unless $::form->{id};
+
+  my $dbh = SL::DB->client->dbh;
+  my $cv = $::form->{id};
+  my ($db, $cv_type);
+  if ($::form->{db} eq 'customer') {
+    $db      = "ar";
+    $cv_type = "customer_id";
+  } else {
+    $db      = "ap";
+    $cv_type = "vendor_id";
+  }
+  my $query = <<SQL;
+  SELECT EXTRACT (YEAR FROM transdate) as date_part,
+         count(id)                     as count,
+         sum(amount)                   as amount,
+         sum(netamount)                as netamount,
+         sum(paid)                     as paid
+    FROM $db WHERE $cv_type = ?
+GROUP BY date_part
+ORDER BY date_part DESC
+SQL
+   $self->{turnover_statistic} = selectall_hashref_query($::form, $dbh, $query, $cv);
+   $self->render('customer_vendor_turnover/count_turnover', { layout => 0 });
+}
+
+sub action_get_invoices {
+  my ($self) = @_;
+
+  return $self->render('generic/error', { layout => 0 }, label_error => "list_transactions needs a trans_id") unless $::form->{id};
+
+  my $cv = $::form->{id};
+  my $invoices;
+  if ( $::form->{db} eq 'customer' ) {
+    $invoices = SL::DB::Manager::Invoice->get_all(
+      query   => [ customer_id => $cv, ],
+      sort_by => 'transdate DESC',
+    );
+  } else {
+    $invoices = SL::DB::Manager::PurchaseInvoice->get_all(
+      query   => [ vendor_id => $cv, ],
+      sort_by => 'transdate DESC',
+    );
+  }
+  $self->render('customer_vendor_turnover/invoices_statistic', { layout => 0 }, invoices => $invoices);
+}
+
+sub action_get_orders {
+  my ($self) = @_;
+
+  return $self->render('generic/error', { layout => 0 }, label_error => "list_transactions needs a trans_id") unless $::form->{id};
+
+  my $cv = $::form->{id};
+  my $orders;
+  my $type = $::form->{type};
+  if ( $::form->{db} eq 'customer' ) {
+    $orders = SL::DB::Manager::Order->get_all(
+      query   => [
+                   customer_id => $cv,
+                   quotation   => ($type eq 'quotation' ? 'T' : 'F')
+                 ],
+      sort_by => 'transdate DESC',
+    );
+  } else {
+    $orders = SL::DB::Manager::Order->get_all(
+      query   => [
+                   vendor_id => $cv,
+                   quotation => ($type eq 'quotation' ? 'T' : 'F')
+                 ],
+      sort_by => 'transdate DESC',
+    );
+  }
+  if ( $type eq 'order') {
+    $self->render('customer_vendor_turnover/order_statistic', { layout => 0 }, orders => $orders);
+  } else {
+    $self->render('customer_vendor_turnover/quotation_statistic', { layout => 0 }, orders => $orders);
+  }
+}
+
+sub _get_open_orders {
+  my ( $self ) = @_;
+
+  return $self->render('generic/error', { layout => 0 }, label_error => "list_transactions needs a trans_id") unless $::form->{id};
+  my $open_orders;
+  my $cv = $::form->{id};
+
+  if ( $::form->{db} eq 'customer' ) {
+    $open_orders = SL::DB::Manager::Order->get_all(
+      query   => [
+                   customer_id => $cv,
+                   closed      => 'F',
+                 ],
+      sort_by => 'transdate DESC',
+    );
+  } else {
+    $open_orders = SL::DB::Manager::Order->get_all(
+      query   => [
+                   vendor_id => $cv,
+                   closed    => 'F',
+                 ],
+      sort_by => 'transdate DESC',
+    );
+  }
+
+  return 0 unless scalar @{$open_orders};
+  return $self->render('customer_vendor_turnover/_list_open_orders', { output => 0 },
+                        orders => $open_orders,
+                        title  => $::locale->text('Open Orders'),
+                      );
+}
+
+sub action_get_mails {
+  my ( $self ) = @_;
+
+  return $self->render('generic/error', { layout => 0 }, label_error => "list_transactions needs a trans_id") unless $::form->{id};
+  my $dbh = SL::DB->client->dbh;
+  my $query;
+  my $cv = $::form->{id};
+
+  if ( $::form->{db} eq 'customer') {
+    $query = <<SQL;
+WITH
+oe_emails_customer
+       AS (SELECT rc.to_id, rc.from_id, oe.quotation, oe.quonumber, oe.ordnumber, c.id
+     FROM record_links rc
+LEFT JOIN oe oe      ON rc.from_id = oe.id
+LEFT JOIN customer c ON oe.customer_id = c.id
+    WHERE rc.to_table = 'email_journal'
+      AND rc.from_table ='oe'),
+
+do_emails_customer
+       AS (SELECT rc.to_id, rc.from_id, o.donumber, c.id
+     FROM record_links rc
+LEFT JOIN delivery_orders o ON rc.from_id = o.id
+LEFT JOIN customer c ON o.customer_id = c.id
+    WHERE rc.to_table = 'email_journal'
+      AND rc.from_table = 'delivery_orders'),
+
+inv_emails_customer
+       AS (SELECT rc.to_id, rc.from_id, inv.type, inv.invnumber, c.id
+     FROM record_links rc
+LEFT JOIN ar inv ON rc.from_id = inv.id
+LEFT JOIN customer c ON inv.customer_id = c.id
+    WHERE rc.to_table = 'email_journal'
+      AND rc.from_table = 'ar'),
+
+letter_emails_customer
+       AS (SELECT rc.to_id, rc.from_id, l.letternumber, c.id
+     FROM record_links rc
+LEFT JOIN letter l ON rc.from_id = l.id
+LEFT JOIN customer c ON l.customer_id = c.id
+    WHERE rc.to_table = 'email_journal'
+      AND rc.from_table = 'letter')
+
+SELECT ej.*,
+ CASE
+  oec.quotation WHEN 'F' THEN 'Sales Order'
+                ELSE 'Quotation'
+ END AS type,
+ CASE
+  oec.quotation WHEN 'F' THEN oec.ordnumber
+                ELSE oec.quonumber
+ END    AS recordnumber,
+ oec.id AS record_id
+     FROM email_journal ej
+LEFT JOIN oe_emails_customer oec ON ej.id = oec.to_id
+    WHERE oec.id = ?
+
+UNION
+
+SELECT ej.*, 'Delivery Order' AS type, dec.donumber AS recordnumber,dec.id AS record_id
+     FROM email_journal ej
+LEFT JOIN do_emails_customer dec ON ej.id = dec.to_id
+    WHERE dec.id = ?
+
+UNION
+
+SELECT ej.*,
+ CASE
+  iec.type WHEN 'credit_note' THEN 'Credit Note'
+           WHEN 'invoice' THEN 'Invoice'
+           ELSE 'N/A'
+ END           AS type,
+ iec.invnumber AS recordnumber,
+        iec.id AS record_id
+     FROM email_journal ej
+LEFT JOIN inv_emails_customer iec ON ej.id = iec.to_id
+    WHERE iec.id = ?
+
+UNION
+
+SELECT ej.*, 'Letter' AS type, lec.letternumber AS recordnumber,lec.id AS record_id
+     FROM email_journal ej
+LEFT JOIN letter_emails_customer lec ON ej.id = lec.to_id
+    WHERE lec.id = ?
+ ORDER BY sent_on DESC
+SQL
+  }
+  else {
+    $query = <<SQL;
+WITH
+oe_emails_vendor
+       AS (SELECT rc.to_id, rc.from_id, oe.quotation, oe.quonumber, oe.ordnumber, c.id
+     FROM record_links rc
+LEFT JOIN oe oe ON rc.from_id = oe.id
+LEFT JOIN vendor c ON oe.vendor_id = c.id
+    WHERE rc.to_table = 'email_journal'
+      AND rc.from_table ='oe'),
+
+do_emails_vendor
+       AS (SELECT rc.to_id, rc.from_id, o.donumber, c.id
+     FROM record_links rc
+LEFT JOIN delivery_orders o ON rc.from_id = o.id
+LEFT JOIN vendor c ON o.vendor_id = c.id
+    WHERE rc.to_table = 'email_journal'
+      AND rc.from_table = 'delivery_orders'),
+
+inv_emails_vendor
+       AS (SELECT rc.to_id, rc.from_id, inv.type, inv.invnumber, c.id
+     FROM record_links rc
+LEFT JOIN ap inv ON rc.from_id = inv.id
+LEFT JOIN vendor c ON inv.vendor_id = c.id
+    WHERE rc.to_table = 'email_journal'
+      AND rc.from_table = 'ar'),
+
+letter_emails_vendor
+       AS (SELECT rc.to_id, rc.from_id, l.letternumber, c.id
+     FROM record_links rc
+LEFT JOIN letter l ON rc.from_id = l.id
+LEFT JOIN vendor c ON l.vendor_id = c.id
+    WHERE rc.to_table = 'email_journal'
+      AND rc.from_table = 'letter')
+
+SELECT ej.*,
+ CASE
+  oec.quotation WHEN 'F' THEN 'Purchase Order'
+                ELSE 'Request quotation'
+ END AS type,
+ CASE
+  oec.quotation WHEN 'F' THEN oec.ordnumber
+                ELSE oec.quonumber
+ END   AS recordnumber,
+oec.id AS record_id
+     FROM email_journal ej
+LEFT JOIN oe_emails_vendor oec ON ej.id = oec.to_id
+    WHERE oec.id = ?
+
+UNION
+
+SELECT ej.*, 'Purchase Delivery Order' AS type, dec.donumber AS recordnumber, dec.id AS record_id
+     FROM email_journal ej
+LEFT JOIN do_emails_vendor dec ON ej.id = dec.to_id
+    WHERE dec.id = ?
+
+UNION
+
+SELECT ej.*, iec.type AS type, iec.invnumber AS recordnumber, iec.id AS record_id
+     FROM email_journal ej
+LEFT JOIN inv_emails_vendor iec ON ej.id = iec.to_id
+    WHERE iec.id = ?
+
+UNION
+
+SELECT ej.*, 'Letter' AS type, lec.letternumber AS recordnumber, lec.id AS record_id
+     FROM email_journal ej
+LEFT JOIN letter_emails_vendor lec ON ej.id = lec.to_id
+    WHERE lec.id = ?
+ ORDER BY sent_on DESC
+SQL
+  }
+  my $emails = selectall_hashref_query($::form, $dbh, $query, $cv, $cv, $cv, $cv);
+  $self->render('customer_vendor_turnover/email_statistic', { layout => 0 }, emails => $emails);
+}
+
+sub action_get_letters {
+  my ($self) = @_;
+
+  return $self->render('generic/error', { layout => 0 }, label_error => "list_transactions needs a trans_id") unless $::form->{id};
+
+  my $cv = $::form->{id};
+  my $letters;
+  my $type = $::form->{type};
+  if ( $::form->{db} eq 'customer' ) {
+    $letters = SL::DB::Manager::Letter->get_all(
+      query   => [ customer_id => $cv, ],
+      sort_by => 'date DESC',
+    );
+  } else {
+    $letters = SL::DB::Manager::Letter->get_all(
+      query   => [ vendor_id => $cv, ],
+      sort_by => 'date DESC',
+    );
+  }
+    $self->render('customer_vendor_turnover/letter_statistic', { layout => 0 }, letters => $letters);
+}
+
+sub check_auth {
+  $::auth->assert('show_extra_record_tab_customer | show_extra_record_tab_vendor');
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::CustomerVendorTurnover
+
+=head1 DESCRIPTION
+
+Gets all kinds of records like orders, request orders, quotations, invoices, emails, letters
+
+wich belong to customer/vendor and displays them in an extra tab "Records".
+
+=head1 URL ACTIONS
+
+=over 4
+
+=item C<action_list_turnover>
+
+Basic action wich displays open invoices and open orders if there are any and shows the tab menu for the other actions
+
+=item C<action_count_open_items_by_month>
+
+gets and shows a dunning statistic of the customer by month
+
+=item C<action_count_open_items_by_year>
+
+gets and shows a dunning statistic of the customer by year
+
+=item C<action_turnover_by_month>
+
+gets and shows an invoice statistic of customer/vendor by month
+
+=item C<action_turnover_by_year>
+
+gets and shows an invoice statistic of customer/vendor by year
+
+=item C<action_get_invoices>
+
+get and shows all invoices from the customer/vendor in an extra tab
+
+=item C<action_get_orders>
+
+get and shows all orders from the customer/vendor in an extra tab
+
+=item C<action_get_letters>
+
+get and shows all letters from the customer/vendor in an extra tab
+
+=item C<action_get_mails>
+
+get and shows all mails from the customer/vendor in an extra tab
+
+=back
+
+=head1 Functions
+
+=over 4
+
+=item C<_get_open_orders>
+
+retrieves the open orders for customer/vendor to display them
+
+=item C<_list_open_items>
+
+retrieves open invoices with their dunnings to display them
+
+=back
+
+=head1 BUGS
+
+None yet. :)
+
+=head1 AUTHOR
+
+W. Hahn E<lt>wh@futureworldsearch.netE<gt>
+
+=cut
diff --git a/SL/Controller/DeliveryOrder.pm b/SL/Controller/DeliveryOrder.pm
new file mode 100644 (file)
index 0000000..7c4a99b
--- /dev/null
@@ -0,0 +1,2419 @@
+package SL::Controller::DeliveryOrder;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+use SL::Helper::Flash qw(flash_later);
+use SL::Helper::Number qw(_format_number _parse_number);
+use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
+use SL::Presenter::DeliveryOrder qw(delivery_order_status_line);
+use SL::Locale::String qw(t8);
+use SL::SessionFile::Random;
+use SL::PriceSource;
+use SL::Webdav;
+use SL::File;
+use SL::MIME;
+use SL::Util qw(trim);
+use SL::YAML;
+use SL::DB::History;
+use SL::DB::Order;
+use SL::DB::Default;
+use SL::DB::Unit;
+use SL::DB::Order;
+use SL::DB::Part;
+use SL::DB::PartClassification;
+use SL::DB::PartsGroup;
+use SL::DB::Printer;
+use SL::DB::Language;
+use SL::DB::RecordLink;
+use SL::DB::Shipto;
+use SL::DB::Translation;
+use SL::DB::TransferType;
+
+use SL::Helper::CreatePDF qw(:all);
+use SL::Helper::PrintOptions;
+use SL::Helper::ShippedQty;
+use SL::Helper::UserPreferences::DisplayPreferences;
+use SL::Helper::UserPreferences::PositionsScrollbar;
+use SL::Helper::UserPreferences::UpdatePositions;
+
+use SL::Controller::Helper::GetModels;
+use SL::Controller::DeliveryOrder::TypeData qw(:types);
+
+use List::Util qw(first sum0);
+use List::UtilsBy qw(sort_by uniq_by);
+use List::MoreUtils qw(any none pairwise first_index);
+use English qw(-no_match_vars);
+use File::Spec;
+use Cwd;
+use Sort::Naturally;
+
+use Rose::Object::MakeMethods::Generic
+(
+ scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
+ 'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids type_data) ],
+);
+
+
+# safety
+__PACKAGE__->run_before('check_auth',
+                        except => [ qw(update_stock_information) ]);
+
+__PACKAGE__->run_before('check_auth_for_edit',
+                        except => [ qw(update_stock_information edit show_customer_vendor_details_dialog price_popup stock_in_out_dialog load_second_rows) ]);
+
+__PACKAGE__->run_before('get_unalterable_data',
+                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
+                                     print send_email) ]);
+
+#
+# actions
+#
+
+# add a new order
+sub action_add {
+  my ($self) = @_;
+
+  $self->order->transdate(DateTime->now_local());
+  $self->type_data->set_reqdate_by_type;
+
+
+  $self->pre_render();
+  $self->render(
+    'delivery_order/form',
+    title => $self->get_title_for('add'),
+    %{$self->{template_args}}
+  );
+}
+
+sub action_add_from_order {
+  my ($self) = @_;
+  # this interfers with init_order
+  $self->{converted_from_oe_id} = delete $::form->{id};
+
+  $self->type_data->validate($::form->{type});
+
+  my $order = SL::DB::Order->new(id => $self->{converted_from_oe_id})->load;
+
+  $self->order(SL::DB::DeliveryOrder->new_from($order, type => $::form->{type}));
+
+  $self->action_add;
+}
+
+# edit an existing order
+sub action_edit {
+  my ($self) = @_;
+
+  if ($::form->{id}) {
+    $self->load_order;
+
+  } else {
+    # this is to edit an order from an unsaved order object
+
+    # set item ids to new fake id, to identify them as new items
+    foreach my $item (@{$self->order->items_sorted}) {
+      $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+    }
+    # trigger rendering values for second row as hidden, because they
+    # are loaded only on demand. So we need to keep the values from
+    # the source.
+    $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
+  }
+
+  $self->pre_render();
+  $self->render(
+    'delivery_order/form',
+    title => $self->get_title_for('edit'),
+    %{$self->{template_args}}
+  );
+}
+
+# edit a collective order (consisting of one or more existing orders)
+sub action_edit_collective {
+  my ($self) = @_;
+
+  # collect order ids
+  my @multi_ids = map {
+    $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
+  } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
+
+  # fall back to add if no ids are given
+  if (scalar @multi_ids == 0) {
+    $self->action_add();
+    return;
+  }
+
+  # fall back to save as new if only one id is given
+  if (scalar @multi_ids == 1) {
+    $self->order(SL::DB::DeliveryOrder->new(id => $multi_ids[0])->load);
+    $self->action_save_as_new();
+    return;
+  }
+
+  # make new order from given orders
+  my @multi_orders = map { SL::DB::DeliveryOrder->new(id => $_)->load } @multi_ids;
+  $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
+  $self->order(SL::DB::DeliveryOrder->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
+
+  $self->action_edit();
+}
+
+# delete the order
+sub action_delete {
+  my ($self) = @_;
+
+  my $errors = $self->delete();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  flash_later('info', $self->type_data->text("delete"));
+
+  my @redirect_params = (
+    action => 'add',
+    type   => $self->type,
+  );
+
+  $self->redirect_to(@redirect_params);
+}
+
+# save the order
+sub action_save {
+  my ($self) = @_;
+
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  flash_later('info', $self->type_data->text("saved"));
+
+  my @redirect_params = (
+    action => 'edit',
+    type   => $self->type,
+    id     => $self->order->id,
+  );
+
+  $self->redirect_to(@redirect_params);
+}
+
+# save the order as new document an open it for edit
+sub action_save_as_new {
+  my ($self) = @_;
+
+  my $order = $self->order;
+
+  if (!$order->id) {
+    $self->js->flash('error', t8('This object has not been saved yet.'));
+    return $self->js->render();
+  }
+
+  # load order from db to check if values changed
+  my $saved_order = SL::DB::DeliveryOrder->new(id => $order->id)->load;
+
+  my %new_attrs;
+  # Lets assign a new number if the user hasn't changed the previous one.
+  # If it has been changed manually then use it as-is.
+  $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
+                        ? ''
+                        : trim($order->number);
+
+  # Clear transdate unless changed
+  $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
+                        ? DateTime->today_local
+                        : $order->transdate;
+
+  # Set new reqdate unless changed if it is enabled in client config
+  $new_attrs{reqdate} = $self->type_data->get_reqdate_by_type($order->reqdate, $saved_order->reqdate);
+
+  # Update employee
+  $new_attrs{employee}  = SL::DB::Manager::Employee->current;
+
+  # Create new record from current one
+  $self->order(SL::DB::DeliveryOrder->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
+
+  # no linked records on save as new
+  delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
+
+  # save
+  $self->action_save();
+}
+
+# print the order
+#
+# This is called if "print" is pressed in the print dialog.
+# If PDF creation was requested and succeeded, the pdf is offered for download
+# via send_file (which uses ajax in this case).
+sub action_print {
+  my ($self) = @_;
+
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  $self->js_reset_order_and_item_ids_after_save;
+
+  my $format      = $::form->{print_options}->{format};
+  my $media       = $::form->{print_options}->{media};
+  my $formname    = $::form->{print_options}->{formname};
+  my $copies      = $::form->{print_options}->{copies};
+  my $groupitems  = $::form->{print_options}->{groupitems};
+  my $printer_id  = $::form->{print_options}->{printer_id};
+
+  # only pdf and opendocument by now
+  if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
+    return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
+  }
+
+  # only screen or printer by now
+  if (none { $media eq $_ } qw(screen printer)) {
+    return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
+  }
+
+  # create a form for generate_attachment_filename
+  my $form   = Form->new;
+  $form->{$self->nr_key()}  = $self->order->number;
+  $form->{type}             = $self->type;
+  $form->{format}           = $format;
+  $form->{formname}         = $formname;
+  $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
+  my $pdf_filename          = $form->generate_attachment_filename();
+
+  my $pdf;
+  my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
+                                                   formname   => $formname,
+                                                   language   => $self->order->language,
+                                                   printer_id => $printer_id,
+                                                   groupitems => $groupitems });
+  if (scalar @errors) {
+    return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
+  }
+
+  if ($media eq 'screen') {
+    # screen/download
+    $self->js->flash('info', t8('The PDF has been created'));
+    $self->send_file(
+      \$pdf,
+      type         => SL::MIME->mime_type_from_ext($pdf_filename),
+      name         => $pdf_filename,
+      js_no_render => 1,
+    );
+
+  } elsif ($media eq 'printer') {
+    # printer
+    my $printer_id = $::form->{print_options}->{printer_id};
+    SL::DB::Printer->new(id => $printer_id)->load->print_document(
+      copies  => $copies,
+      content => $pdf,
+    );
+
+    $self->js->flash('info', t8('The PDF has been printed'));
+  }
+
+  my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $pdf_filename);
+  if (scalar @warnings) {
+    $self->js->flash('warning', $_) for @warnings;
+  }
+
+  $self->save_history('PRINTED');
+
+  $self->js
+    ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
+    ->render;
+}
+sub action_preview_pdf {
+  my ($self) = @_;
+
+  my $errors = $self->save();
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  $self->js_reset_order_and_item_ids_after_save;
+
+  my $format      = 'pdf';
+  my $media       = 'screen';
+  my $formname    = $self->type;
+
+  # only pdf
+  # create a form for generate_attachment_filename
+  my $form   = Form->new;
+  $form->{$self->nr_key()}  = $self->order->number;
+  $form->{type}             = $self->type;
+  $form->{format}           = $format;
+  $form->{formname}         = $formname;
+  $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
+  my $pdf_filename          = $form->generate_attachment_filename();
+
+  my $pdf;
+  my @errors = generate_pdf($self->order, \$pdf, { format     => $format,
+                                                   formname   => $formname,
+                                                   language   => $self->order->language,
+                                                 });
+  if (scalar @errors) {
+    return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
+  }
+  $self->save_history('PREVIEWED');
+  $self->js->flash('info', t8('The PDF has been previewed'));
+  # screen/download
+  $self->send_file(
+    \$pdf,
+    type         => SL::MIME->mime_type_from_ext($pdf_filename),
+    name         => $pdf_filename,
+    js_no_render => 0,
+  );
+}
+
+# open the email dialog
+sub action_save_and_show_email_dialog {
+  my ($self) = @_;
+
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  my $cv_method = $self->cv;
+
+  if (!$self->order->$cv_method) {
+    return $self->js->flash('error', $self->cv eq 'customer' ? t8('Cannot send E-mail without customer given') : t8('Cannot send E-mail without vendor given'))
+                    ->render($self);
+  }
+
+  my $email_form;
+  $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
+  $email_form->{to} ||= $self->order->$cv_method->email;
+  $email_form->{cc}   = $self->order->$cv_method->cc;
+  $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
+  # Todo: get addresses from shipto, if any
+
+  my $form = Form->new;
+  $form->{$self->nr_key()}  = $self->order->number;
+  $form->{cusordnumber}     = $self->order->cusordnumber;
+  $form->{formname}         = $self->type;
+  $form->{type}             = $self->type;
+  $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
+  $form->{language_id}      = $self->order->language->id                  if $self->order->language;
+  $form->{format}           = 'pdf';
+  $form->{cp_id}            = $self->order->contact->cp_id if $self->order->contact;
+
+  $email_form->{subject}             = $form->generate_email_subject();
+  $email_form->{attachment_filename} = $form->generate_attachment_filename();
+  $email_form->{message}             = $form->generate_email_body();
+  $email_form->{js_send_function}    = 'kivi.DeliveryOrder.send_email()';
+
+  my %files = $self->get_files_for_email_dialog();
+  $self->{all_employees} = SL::DB::Manager::Employee->get_all(query => [ deleted => 0 ]);
+  my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
+                                  email_form  => $email_form,
+                                  show_bcc    => $::auth->assert('email_bcc', 'may fail'),
+                                  FILES       => \%files,
+                                  is_customer => $self->type_data->is_customer,
+                                  ALL_EMPLOYEES => $self->{all_employees},
+  );
+
+  $self->js
+      ->run('kivi.DeliveryOrder.show_email_dialog', $dialog_html)
+      ->reinit_widgets
+      ->render($self);
+}
+
+# send email
+#
+# Todo: handling error messages: flash is not displayed in dialog, but in the main form
+sub action_send_email {
+  my ($self) = @_;
+
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->run('kivi.DeliveryOrder.close_email_dialog');
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  $self->js_reset_order_and_item_ids_after_save;
+
+  my $email_form  = delete $::form->{email_form};
+  my %field_names = (to => 'email');
+
+  $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
+
+  # for Form::cleanup which may be called in Form::send_email
+  $::form->{cwd}    = getcwd();
+  $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
+
+  $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
+  $::form->{media}  = 'email';
+
+  if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
+    my $pdf;
+    my @errors = generate_pdf($self->order, \$pdf, {media      => $::form->{media},
+                                                    format     => $::form->{print_options}->{format},
+                                                    formname   => $::form->{print_options}->{formname},
+                                                    language   => $self->order->language,
+                                                    printer_id => $::form->{print_options}->{printer_id},
+                                                    groupitems => $::form->{print_options}->{groupitems}});
+    if (scalar @errors) {
+      return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
+    }
+
+    my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $::form->{attachment_filename});
+    if (scalar @warnings) {
+      flash_later('warning', $_) for @warnings;
+    }
+
+    my $sfile = SL::SessionFile::Random->new(mode => "w");
+    $sfile->fh->print($pdf);
+    $sfile->fh->close;
+
+    $::form->{tmpfile} = $sfile->file_name;
+    $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
+  }
+
+  $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
+  $::form->send_email(\%::myconfig, 'pdf');
+
+  # internal notes unless no email journal
+  unless ($::instance_conf->get_email_journal) {
+
+    my $intnotes = $self->order->intnotes;
+    $intnotes   .= "\n\n" if $self->order->intnotes;
+    $intnotes   .= t8('[email]')                                                                                        . "\n";
+    $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
+    $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
+    $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
+    $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
+    $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
+    $intnotes   .= t8('Message')    . ": " . $::form->{message};
+
+    $self->order->update_attributes(intnotes => $intnotes);
+  }
+
+  $self->save_history('MAILED');
+
+  flash_later('info', t8('The email has been sent.'));
+
+  my @redirect_params = (
+    action => 'edit',
+    type   => $self->type,
+    id     => $self->order->id,
+  );
+
+  $self->redirect_to(@redirect_params);
+}
+
+# save the order and redirect to the frontend subroutine for a new
+# delivery order
+sub action_save_and_delivery_order {
+  my ($self) = @_;
+
+  $self->save_and_redirect_to(
+    controller => 'oe.pl',
+    action     => 'oe_delivery_order_from_order',
+  );
+}
+
+# save the order and redirect to the frontend subroutine for a new
+# invoice
+sub action_save_and_invoice {
+  my ($self) = @_;
+
+  $self->save_and_redirect_to(
+    controller => 'oe.pl',
+    action     => 'oe_invoice_from_order',
+  );
+}
+
+# workflow from sales order to sales quotation
+sub action_sales_quotation {
+  $_[0]->workflow_sales_or_request_for_quotation();
+}
+
+# workflow from sales order to sales quotation
+sub action_request_for_quotation {
+  $_[0]->workflow_sales_or_request_for_quotation();
+}
+
+# workflow from sales quotation to sales order
+sub action_sales_order {
+  $_[0]->workflow_sales_or_purchase_order();
+}
+
+# workflow from rfq to purchase order
+sub action_purchase_order {
+  $_[0]->workflow_sales_or_purchase_order();
+}
+
+# workflow from purchase order to ap transaction
+sub action_save_and_ap_transaction {
+  my ($self) = @_;
+
+  $self->save_and_redirect_to(
+    controller => 'ap.pl',
+    action     => 'add_from_purchase_order',
+  );
+}
+
+# set form elements in respect to a changed customer or vendor
+#
+# This action is called on an change of the customer/vendor picker.
+sub action_customer_vendor_changed {
+  my ($self) = @_;
+
+  setup_order_from_cv($self->order);
+
+  my $cv_method = $self->cv;
+
+  if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
+    $self->js->show('#cp_row');
+  } else {
+    $self->js->hide('#cp_row');
+  }
+
+  if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
+    $self->js->show('#shipto_selection');
+  } else {
+    $self->js->hide('#shipto_selection');
+  }
+
+  $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
+
+  $self->js
+    ->replaceWith('#order_cp_id',            $self->build_contact_select)
+    ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
+    ->replaceWith('#shipto_inputs  ',        $self->build_shipto_inputs)
+    ->replaceWith('#business_info_row',      $self->build_business_info_row)
+    ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
+    ->val(        '#order_taxincluded',      $self->order->taxincluded)
+    ->val(        '#order_currency_id',      $self->order->currency_id)
+    ->val(        '#order_payment_id',       $self->order->payment_id)
+    ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
+    ->val(        '#order_intnotes',         $self->order->intnotes)
+    ->val(        '#order_language_id',      $self->order->$cv_method->language_id)
+    ->focus(      '#order_' . $self->cv . '_id')
+    ->run('kivi.DeliveryOrder.update_exchangerate');
+
+  $self->js_redisplay_cvpartnumbers;
+  $self->js->render();
+}
+
+# open the dialog for customer/vendor details
+sub action_show_customer_vendor_details_dialog {
+  my ($self) = @_;
+
+  my $is_customer = 'customer' eq $::form->{vc};
+  my $cv;
+  if ($is_customer) {
+    $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
+  } else {
+    $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
+  }
+
+  my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
+  $details{discount_as_percent} = $cv->discount_as_percent;
+  $details{creditlimt}          = $cv->creditlimit_as_number;
+  $details{business}            = $cv->business->description      if $cv->business;
+  $details{language}            = $cv->language_obj->description  if $cv->language_obj;
+  $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
+  $details{payment_terms}       = $cv->payment->description       if $cv->payment;
+  $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
+
+  foreach my $entry (@{ $cv->shipto }) {
+    push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
+  }
+  foreach my $entry (@{ $cv->contacts }) {
+    push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
+  }
+
+  $_[0]->render('common/show_vc_details', { layout => 0 },
+                is_customer => $is_customer,
+                %details);
+
+}
+
+# called if a unit in an existing item row is changed
+sub action_unit_changed {
+  my ($self) = @_;
+
+  my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
+  my $item = $self->order->items_sorted->[$idx];
+
+  my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
+  $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
+
+  $self->js
+    ->run('kivi.DeliveryOrder.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
+  $self->js_redisplay_line_values;
+  $self->js->render();
+}
+
+# add an item row for a new item entered in the input row
+sub action_add_item {
+  my ($self) = @_;
+
+  delete $::form->{add_item}->{create_part_type};
+
+  my $form_attr = $::form->{add_item};
+
+  return unless $form_attr->{parts_id};
+
+  my $item = new_item($self->order, $form_attr);
+
+  $self->order->add_items($item);
+
+  $self->get_item_cvpartnumber($item);
+
+  my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+  my $row_as_html = $self->p->render('delivery_order/tabs/_row',
+                                     ITEM => $item,
+                                     ID   => $item_id,
+                                     SELF => $self,
+                                     in_out => $self->type_data->transfer,
+  );
+
+  if ($::form->{insert_before_item_id}) {
+    $self->js
+      ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
+  } else {
+    $self->js
+      ->append('#row_table_id', $row_as_html);
+  }
+
+  if ( $item->part->is_assortment ) {
+    $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
+    foreach my $assortment_item ( @{$item->part->assortment_items} ) {
+      my $attr = { parts_id => $assortment_item->parts_id,
+                   qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
+                   unit     => $assortment_item->unit,
+                   description => $assortment_item->part->description,
+                 };
+      my $item = new_item($self->order, $attr);
+
+      # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
+      $item->discount(1) unless $assortment_item->charge;
+
+      $self->order->add_items( $item );
+      $self->get_item_cvpartnumber($item);
+      my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+      my $row_as_html = $self->p->render('delivery_order/tabs/_row',
+                                         ITEM => $item,
+                                         ID   => $item_id,
+                                         SELF => $self,
+      );
+      if ($::form->{insert_before_item_id}) {
+        $self->js
+          ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
+      } else {
+        $self->js
+          ->append('#row_table_id', $row_as_html);
+      }
+    };
+  };
+
+  $self->js
+    ->val('.add_item_input', '')
+    ->run('kivi.DeliveryOrder.init_row_handlers')
+    ->run('kivi.DeliveryOrder.renumber_positions')
+    ->focus('#add_item_parts_id_name');
+
+  $self->js->run('kivi.DeliveryOrder.row_table_scroll_down') if !$::form->{insert_before_item_id};
+
+  $self->js->render();
+}
+
+# add item rows for multiple items at once
+sub action_add_multi_items {
+  my ($self) = @_;
+
+  my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
+  return $self->js->render() unless scalar @form_attr;
+
+  my @items;
+  foreach my $attr (@form_attr) {
+    my $item = new_item($self->order, $attr);
+    push @items, $item;
+    if ( $item->part->is_assortment ) {
+      foreach my $assortment_item ( @{$item->part->assortment_items} ) {
+        my $attr = { parts_id => $assortment_item->parts_id,
+                     qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
+                     unit     => $assortment_item->unit,
+                     description => $assortment_item->part->description,
+                   };
+        my $item = new_item($self->order, $attr);
+
+        # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
+        $item->discount(1) unless $assortment_item->charge;
+        push @items, $item;
+      }
+    }
+  }
+  $self->order->add_items(@items);
+
+  foreach my $item (@items) {
+    $self->get_item_cvpartnumber($item);
+    my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+    my $row_as_html = $self->p->render('delivery_order/tabs/_row',
+                                       ITEM => $item,
+                                       ID   => $item_id,
+                                       SELF => $self,
+                                       in_out => $self->type_data->transfer,
+    );
+
+    if ($::form->{insert_before_item_id}) {
+      $self->js
+        ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
+    } else {
+      $self->js
+        ->append('#row_table_id', $row_as_html);
+    }
+  }
+
+  $self->js
+    ->run('kivi.Part.close_picker_dialogs')
+    ->run('kivi.DeliveryOrder.init_row_handlers')
+    ->run('kivi.DeliveryOrder.renumber_positions')
+    ->focus('#add_item_parts_id_name');
+
+  $self->js->run('kivi.DeliveryOrder.row_table_scroll_down') if !$::form->{insert_before_item_id};
+
+  $self->js->render();
+}
+
+sub action_update_exchangerate {
+  my ($self) = @_;
+
+  my $data = {
+    is_standard   => $self->order->currency_id == $::instance_conf->get_currency_id,
+    currency_name => $self->order->currency->name,
+  };
+
+  $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
+}
+
+# redisplay item rows if they are sorted by an attribute
+sub action_reorder_items {
+  my ($self) = @_;
+
+  my %sort_keys = (
+    partnumber   => sub { $_[0]->part->partnumber },
+    description  => sub { $_[0]->description },
+    qty          => sub { $_[0]->qty },
+    sellprice    => sub { $_[0]->sellprice },
+    discount     => sub { $_[0]->discount },
+    cvpartnumber => sub { $_[0]->{cvpartnumber} },
+  );
+
+  $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
+
+  my $method = $sort_keys{$::form->{order_by}};
+  my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
+  if ($::form->{sort_dir}) {
+    if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
+      @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
+    } else {
+      @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
+    }
+  } else {
+    if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
+      @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
+    } else {
+      @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
+    }
+  }
+  $self->js
+    ->run('kivi.DeliveryOrder.redisplay_items', \@to_sort)
+    ->render;
+}
+
+# show the popup to choose a price/discount source
+sub action_price_popup {
+  my ($self) = @_;
+
+  my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
+  my $item = $self->order->items_sorted->[$idx];
+
+  $self->render_price_dialog($item);
+}
+
+# save the order in a session variable and redirect to the part controller
+sub action_create_part {
+  my ($self) = @_;
+
+  my $previousform = $::auth->save_form_in_session(non_scalars => 1);
+
+  my $callback     = $self->url_for(
+    action       => 'return_from_create_part',
+    type         => $self->type, # type is needed for check_auth on return
+    previousform => $previousform,
+  );
+
+  flash_later('info', t8('You are adding a new part while you are editing another document. You will be redirected to your document when saving the new part or aborting this form.'));
+
+  my @redirect_params = (
+    controller    => 'Part',
+    action        => 'add',
+    part_type     => $::form->{add_item}->{create_part_type},
+    callback      => $callback,
+    inline_create => 1,
+  );
+
+  $self->redirect_to(@redirect_params);
+}
+
+sub action_return_from_create_part {
+  my ($self) = @_;
+
+  $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
+
+  $::auth->restore_form_from_session(delete $::form->{previousform});
+
+  # set item ids to new fake id, to identify them as new items
+  foreach my $item (@{$self->order->items_sorted}) {
+    $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+  }
+
+  $self->get_unalterable_data();
+  $self->pre_render();
+
+  # trigger rendering values for second row/longdescription as hidden,
+  # because they are loaded only on demand. So we need to keep the values
+  # from the source.
+  $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
+  $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
+
+  $self->render(
+    'delivery_order/form',
+    title => $self->get_title_for('edit'),
+    %{$self->{template_args}}
+  );
+
+}
+
+sub action_stock_in_out_dialog {
+  my ($self) = @_;
+
+  my $part    = SL::DB::Part->load_cached($::form->{parts_id}) or die "need parts_id";
+  my $unit    = SL::DB::Unit->load_cached($::form->{unit}) or die "need unit";
+  my $stock   = $::form->{stock};
+  my $row     = $::form->{row};
+  my $item_id = $::form->{item_id};
+  my $qty     = _parse_number($::form->{qty_as_number});
+
+  my $inout = $self->type_data->transfer;
+
+  my @contents   = DO->get_item_availability(parts_id => $part->id);
+  my $stock_info = DO->unpack_stock_information(packed => $stock);
+
+  $self->merge_stock_data($stock_info, \@contents, $part, $unit);
+
+  $self->render("delivery_order/stock_dialog", { layout => 0 },
+    WHCONTENTS => $self->order->delivered ? $stock_info : \@contents,
+    part       => $part,
+    do_qty     => $qty,
+    do_unit    => $unit->unit,
+    delivered  => $self->order->delivered,
+    row        => $row,
+    item_id    => $item_id,
+  );
+}
+
+sub action_update_stock_information {
+  my ($self) = @_;
+
+  my $stock_info = $::form->{stock_info};
+  my $unit = $::form->{unit};
+  my $yaml = SL::YAML::Dump($stock_info);
+  my $stock_qty = $self->calculate_stock_in_out_from_stock_info($unit, $stock_info);
+
+  my $response = {
+    stock_info => $yaml,
+    stock_qty => $stock_qty,
+  };
+  $self->render(\ SL::JSON::to_json($response), { layout => 0, type => 'json', process => 0 });
+}
+
+sub merge_stock_data {
+  my ($self, $stock_info, $contents, $part, $unit) = @_;
+  # TODO rewrite to mapping
+
+  if (!$self->order->delivered) {
+    for my $row (@$contents) {
+      # row here is in parts units. stock is in item units
+      $row->{available_qty} = _format_number($part->unit_obj->convert_to($row->{qty}, $unit));
+
+      for my $sinfo (@{ $stock_info }) {
+        next if $row->{bin_id}       != $sinfo->{bin_id} ||
+                $row->{warehouse_id} != $sinfo->{warehouse_id} ||
+                $row->{chargenumber} ne $sinfo->{chargenumber} ||
+                $row->{bestbefore}   ne $sinfo->{bestbefore};
+
+        $row->{"stock_$_"} = $sinfo->{$_}
+          for qw(qty unit error delivery_order_items_stock_id);
+      }
+    }
+
+  } else {
+    for my $sinfo (@{ $stock_info }) {
+      my $bin = SL::DB::Bin->load_cached($sinfo->{bin_id});
+      $sinfo->{warehousedescription} = $bin->warehouse->description;
+      $sinfo->{bindescription}       = $bin->description;
+      map { $sinfo->{"stock_$_"}      = $sinfo->{$_} } qw(qty unit);
+    }
+  }
+}
+
+# load the second row for one or more items
+#
+# This action gets the html code for all items second rows by rendering a template for
+# the second row and sets the html code via client js.
+sub action_load_second_rows {
+  my ($self) = @_;
+
+  foreach my $item_id (@{ $::form->{item_ids} }) {
+    my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
+    my $item = $self->order->items_sorted->[$idx];
+
+    $self->js_load_second_row($item, $item_id, 0);
+  }
+
+  $self->js->run('kivi.DeliveryOrder.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
+
+  $self->js->render();
+}
+
+# update description, notes and sellprice from master data
+sub action_update_row_from_master_data {
+  my ($self) = @_;
+
+  foreach my $item_id (@{ $::form->{item_ids} }) {
+    my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
+    my $item  = $self->order->items_sorted->[$idx];
+    my $texts = get_part_texts($item->part, $self->order->language_id);
+
+    $item->description($texts->{description});
+    $item->longdescription($texts->{longdescription});
+
+    my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
+
+    my $price_src;
+    if ($item->part->is_assortment) {
+    # add assortment items with price 0, as the components carry the price
+      $price_src = $price_source->price_from_source("");
+      $price_src->price(0);
+    } else {
+      $price_src = $price_source->best_price
+                 ? $price_source->best_price
+                 : $price_source->price_from_source("");
+      $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
+      $price_src->price(0) if !$price_source->best_price;
+    }
+
+
+    $item->sellprice($price_src->price);
+    $item->active_price_source($price_src);
+
+    $self->js
+      ->run('kivi.DeliveryOrder.update_sellprice', $item_id, $item->sellprice_as_number)
+      ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
+      ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
+      ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
+
+    if ($self->search_cvpartnumber) {
+      $self->get_item_cvpartnumber($item);
+      $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
+    }
+  }
+
+  $self->js_redisplay_line_values;
+
+  $self->js->render();
+}
+
+sub action_transfer_stock {
+  my ($self) = @_;
+
+  if ($self->order->delivered) {
+    return $self->js->flash("error", t8('The parts for this order have already been transferred'))->render;
+  }
+
+  my $inout = $self->type_data->properties('transfer');
+
+  my $errors = $self->save;
+
+  if (@$errors) {
+    $self->js->flash('error', $_) for @$errors;
+    return $self->js->render;
+  }
+
+  my $order = $self->order;
+
+  # TODO move to type data
+  my $trans_type = $inout eq 'in'
+    ? SL::DB::Manager::TransferType->find_by(direction => "id", description => "stock")
+    : SL::DB::Manager::TransferType->find_by(direction => "out", description => "shipped");
+
+  my @transfer_requests;
+
+  for my $item (@{ $order->items_sorted }) {
+    for my $stock (@{ $item->delivery_order_stock_entries }) {
+      my $transfer = SL::DB::Inventory->new_from($stock);
+      $transfer->trans_type($trans_type);
+      $transfer->qty($transfer->qty * -1) if $inout eq 'out';
+
+      push @transfer_requests, $transfer if defined $transfer->qty && $transfer->qty != 0;
+    };
+  }
+
+  if (!@transfer_requests) {
+    return $self->js->flash("error", t8("No stock to transfer"))->render;
+  }
+
+  SL::DB->client->with_transaction(sub {
+    $_->save for @transfer_requests;
+    $self->order->update_attributes(delivered => 1);
+  });
+
+  $self->js
+    ->flash("info", t8("Stock transfered"))
+    ->run('kivi.ActionBar.setDisabled', '#transfer_out_action', t8('The parts for this order have already been transferred'))
+    ->run('kivi.ActionBar.setDisabled', '#transfer_in_action', t8('The parts for this order have already been transferred'))
+    ->run('kivi.ActionBar.setDisabled', '#delete_action', t8('The parts for this order have already been transferred'))
+    ->replaceWith('#data-status-line', delivery_order_status_line($self->order))
+    ->render;
+
+}
+
+sub js_load_second_row {
+  my ($self, $item, $item_id, $do_parse) = @_;
+
+  if ($do_parse) {
+    # Parse values from form (they are formated while rendering (template)).
+    # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
+    # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
+    foreach my $var (@{ $item->cvars_by_config }) {
+      $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
+    }
+    $item->parse_custom_variable_values;
+  }
+
+  my $row_as_html = $self->p->render('delivery_order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
+
+  $self->js
+    ->html('#second_row_' . $item_id, $row_as_html)
+    ->data('#second_row_' . $item_id, 'loaded', 1);
+}
+
+sub js_redisplay_line_values {
+  my ($self) = @_;
+
+  my $is_sales = $self->order->is_sales;
+
+  # sales orders with margins
+  my @data;
+  if ($is_sales) {
+    @data = map {
+      [
+       $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
+       $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
+       $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
+      ]} @{ $self->order->items_sorted };
+  } else {
+    @data = map {
+      [
+       $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
+      ]} @{ $self->order->items_sorted };
+  }
+
+  $self->js
+    ->run('kivi.DeliveryOrder.redisplay_line_values', $is_sales, \@data);
+}
+
+sub js_redisplay_cvpartnumbers {
+  my ($self) = @_;
+
+  $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
+
+  my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
+
+  $self->js
+    ->run('kivi.DeliveryOrder.redisplay_cvpartnumbers', \@data);
+}
+
+sub js_reset_order_and_item_ids_after_save {
+  my ($self) = @_;
+
+  $self->js
+    ->val('#id', $self->order->id)
+    ->val('#converted_from_oe_id', '')
+    ->val('#order_' . $self->nr_key(), $self->order->number);
+
+  my $idx = 0;
+  foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
+    next if !$self->order->items_sorted->[$idx]->id;
+    next if $form_item_id !~ m{^new};
+    $self->js
+      ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
+      ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
+      ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
+  } continue {
+    $idx++;
+  }
+  $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
+}
+
+#
+# helpers
+#
+
+sub init_type {
+  my ($self) = @_;
+
+  if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
+    die "Not a valid type for delivery order";
+  }
+
+  $self->type($::form->{type});
+}
+
+sub init_cv {
+  my ($self) = @_;
+
+  return $self->type_data->customervendor;
+}
+
+sub init_search_cvpartnumber {
+  my ($self) = @_;
+
+  my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
+  my $search_cvpartnumber;
+  $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
+  $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
+
+  return $search_cvpartnumber;
+}
+
+sub init_show_update_button {
+  my ($self) = @_;
+
+  !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
+}
+
+sub init_p {
+  SL::Presenter->get;
+}
+
+sub init_order {
+  $_[0]->make_order;
+}
+
+sub init_all_price_factors {
+  SL::DB::Manager::PriceFactor->get_all;
+}
+
+sub init_part_picker_classification_ids {
+  my ($self)    = @_;
+
+  return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => $self->type_data->part_classification_query) } ];
+}
+
+sub check_auth {
+  my ($self) = @_;
+
+  $::auth->assert($self->type_data->access('view') || 'DOES_NOT_EXIST');
+}
+
+sub check_auth_for_edit {
+  my ($self) = @_;
+
+  $::auth->assert($self->type_data->access('edit') || 'DOES_NOT_EXIST');
+}
+
+# build the selection box for contacts
+#
+# Needed, if customer/vendor changed.
+sub build_contact_select {
+  my ($self) = @_;
+
+  select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
+    value_key  => 'cp_id',
+    title_key  => 'full_name_dep',
+    default    => $self->order->cp_id,
+    with_empty => 1,
+    style      => 'width: 300px',
+  );
+}
+
+# build the selection box for shiptos
+#
+# Needed, if customer/vendor changed.
+sub build_shipto_select {
+  my ($self) = @_;
+
+  select_tag('order.shipto_id',
+             [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
+             value_key  => 'shipto_id',
+             title_key  => 'displayable_id',
+             default    => $self->order->shipto_id,
+             with_empty => 0,
+             style      => 'width: 300px',
+  );
+}
+
+# build the inputs for the cusom shipto dialog
+#
+# Needed, if customer/vendor changed.
+sub build_shipto_inputs {
+  my ($self) = @_;
+
+  my $content = $self->p->render('common/_ship_to_dialog',
+                                 vc_obj      => $self->order->customervendor,
+                                 cs_obj      => $self->order->custom_shipto,
+                                 cvars       => $self->order->custom_shipto->cvars_by_config,
+                                 id_selector => '#order_shipto_id');
+
+  div_tag($content, id => 'shipto_inputs');
+}
+
+# render the info line for business
+#
+# Needed, if customer/vendor changed.
+sub build_business_info_row
+{
+  $_[0]->p->render('delivery_order/tabs/_business_info_row', SELF => $_[0]);
+}
+
+
+sub load_order {
+  my ($self) = @_;
+
+  return if !$::form->{id};
+
+  $self->order(SL::DB::DeliveryOrder->new(id => $::form->{id})->load);
+
+  # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
+  # You need a custom shipto object to call cvars_by_config to get the cvars.
+  $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
+
+  $self->prepare_stock_info($_) for $self->order->items;
+
+  return $self->order;
+}
+
+# load or create a new order object
+#
+# And assign changes from the form to this object.
+# If the order is loaded from db, check if items are deleted in the form,
+# remove them form the object and collect them for removing from db on saving.
+# Then create/update items from form (via make_item) and add them.
+sub make_order {
+  my ($self) = @_;
+
+  # add_items adds items to an order with no items for saving, but they cannot
+  # be retrieved via items until the order is saved. Adding empty items to new
+  # order here solves this problem.
+  my $order;
+  $order   = SL::DB::DeliveryOrder->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
+  $order ||= SL::DB::DeliveryOrder->new(orderitems  => [], currency_id => $::instance_conf->get_currency_id(), order_type => $self->type_data->validate($::form->{type}));
+
+  my $cv_id_method = $self->cv . '_id';
+  if (!$::form->{id} && $::form->{$cv_id_method}) {
+    $order->$cv_id_method($::form->{$cv_id_method});
+    setup_order_from_cv($order);
+  }
+
+  my $form_orderitems                  = delete $::form->{order}->{orderitems};
+
+  $order->assign_attributes(%{$::form->{order}});
+
+  $self->setup_custom_shipto_from_form($order, $::form);
+
+  # remove deleted items
+  $self->item_ids_to_delete([]);
+  foreach my $idx (reverse 0..$#{$order->orderitems}) {
+    my $item = $order->orderitems->[$idx];
+    if (none { $item->id == $_->{id} } @{$form_orderitems}) {
+      splice @{$order->orderitems}, $idx, 1;
+      push @{$self->item_ids_to_delete}, $item->id;
+    }
+  }
+
+  my @items;
+  my $pos = 1;
+  foreach my $form_attr (@{$form_orderitems}) {
+    my $item = make_item($order, $form_attr);
+    $item->position($pos);
+    push @items, $item;
+    $pos++;
+  }
+
+  $self->prepare_stock_info($_) for $order->items, @items;
+
+  $order->add_items(grep {!$_->id} @items);
+
+  return $order;
+}
+
+# create or update items from form
+#
+# Make item objects from form values. For items already existing read from db.
+# Create a new item else. And assign attributes.
+sub make_item {
+  my ($record, $attr) = @_;
+
+  my $item;
+  $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
+
+  my $is_new = !$item;
+
+  # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
+  # they cannot be retrieved via custom_variables until the order/orderitem is
+  # saved. Adding empty custom_variables to new orderitem here solves this problem.
+  $item ||= SL::DB::DeliveryOrderItem->new(custom_variables => []);
+
+  # handle stock info
+  if (my $stock_info = delete $attr->{stock_info}) {
+    my %existing = map { $_->id => $_ } $item->delivery_order_stock_entries;
+    my @save;
+
+    for my $line (@{ DO->unpack_stock_information(packed => $stock_info) }) {
+      # lookup existing or make new
+      my $obj = delete $existing{$line->{delivery_order_items_stock_id}}
+             // SL::DB::DeliveryOrderItemsStock->new;
+
+      # assign attributes
+      $obj->$_($line->{$_}) for qw(bin_id warehouse_id chargenumber qty unit);
+      $obj->bestbefore_as_date($line->{bestfbefore})
+        if $line->{bestbefore} && $::instance_conf->get_show_bestbefore;
+      push @save, $obj if $obj->qty;
+    }
+
+    $item->delivery_order_stock_entries(@save);
+  }
+
+  $item->assign_attributes(%$attr);
+
+  if ($is_new) {
+    my $texts = get_part_texts($item->part, $record->language_id);
+    $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
+    $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
+    $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
+  }
+
+  return $item;
+}
+
+# create a new item
+#
+# This is used to add one item
+sub new_item {
+  my ($record, $attr) = @_;
+
+  my $item = SL::DB::DeliveryOrderItem->new;
+
+  # Remove attributes where the user left or set the inputs empty.
+  # So these attributes will be undefined and we can distinguish them
+  # from zero later on.
+  for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
+    delete $attr->{$_} if $attr->{$_} eq '';
+  }
+
+  $item->assign_attributes(%$attr);
+
+  my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
+  my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
+
+  $item->unit($part->unit) if !$item->unit;
+
+  my $price_src;
+  if ( $part->is_assortment ) {
+    # add assortment items with price 0, as the components carry the price
+    $price_src = $price_source->price_from_source("");
+    $price_src->price(0);
+  } elsif (defined $item->sellprice) {
+    $price_src = $price_source->price_from_source("");
+    $price_src->price($item->sellprice);
+  } else {
+    $price_src = $price_source->best_price
+               ? $price_source->best_price
+               : $price_source->price_from_source("");
+    $price_src->price(0) if !$price_source->best_price;
+  }
+
+  my $discount_src;
+  if (defined $item->discount) {
+    $discount_src = $price_source->discount_from_source("");
+    $discount_src->discount($item->discount);
+  } else {
+    $discount_src = $price_source->best_discount
+                  ? $price_source->best_discount
+                  : $price_source->discount_from_source("");
+    $discount_src->discount(0) if !$price_source->best_discount;
+  }
+
+  my %new_attr;
+  $new_attr{part}                   = $part;
+  $new_attr{description}            = $part->description     if ! $item->description;
+  $new_attr{qty}                    = 1.0                    if ! $item->qty;
+  $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
+  $new_attr{sellprice}              = $price_src->price;
+  $new_attr{discount}               = $discount_src->discount;
+  $new_attr{active_price_source}    = $price_src;
+  $new_attr{active_discount_source} = $discount_src;
+  $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
+  $new_attr{project_id}             = $record->globalproject_id;
+  $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
+
+  # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
+  # they cannot be retrieved via custom_variables until the order/orderitem is
+  # saved. Adding empty custom_variables to new orderitem here solves this problem.
+  $new_attr{custom_variables} = [];
+
+  my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
+
+  $item->assign_attributes(%new_attr, %{ $texts });
+
+  return $item;
+}
+
+sub prepare_stock_info {
+  my ($self, $item) = @_;
+
+  $item->{stock_info} = SL::YAML::Dump([
+    map +{
+      delivery_order_items_stock_id => $_->id,
+      qty                           => $_->qty,
+      warehouse_id                  => $_->warehouse_id,
+      bin_id                        => $_->bin_id,
+      chargenumber                  => $_->chargenumber,
+      unit                          => $_->unit,
+    }, $item->delivery_order_stock_entries
+  ]);
+}
+
+sub setup_order_from_cv {
+  my ($order) = @_;
+
+  $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
+
+  $order->intnotes($order->customervendor->notes);
+
+  if ($order->is_sales) {
+    $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
+    $order->taxincluded(defined($order->customer->taxincluded_checked)
+                        ? $order->customer->taxincluded_checked
+                        : $::myconfig{taxincluded_checked});
+  }
+
+}
+
+# setup custom shipto from form
+#
+# The dialog returns form variables starting with 'shipto' and cvars starting
+# with 'shiptocvar_'.
+# Mark it to be deleted if a shipto from master data is selected
+# (i.e. order has a shipto).
+# Else, update or create a new custom shipto. If the fields are empty, it
+# will not be saved on save.
+sub setup_custom_shipto_from_form {
+  my ($self, $order, $form) = @_;
+
+  if ($order->shipto) {
+    $self->is_custom_shipto_to_delete(1);
+  } else {
+    my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
+
+    my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
+    my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
+
+    $custom_shipto->assign_attributes(%$shipto_attrs);
+    $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
+  }
+}
+
+# get data for saving, printing, ..., that is not changed in the form
+#
+# Only cvars for now.
+sub get_unalterable_data {
+  my ($self) = @_;
+
+  foreach my $item (@{ $self->order->items }) {
+    # autovivify all cvars that are not in the form (cvars_by_config can do it).
+    # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
+    foreach my $var (@{ $item->cvars_by_config }) {
+      $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
+    }
+    $item->parse_custom_variable_values;
+  }
+}
+
+# delete the order
+#
+# And remove related files in the spool directory
+sub delete {
+  my ($self) = @_;
+
+  my $errors = [];
+  my $db     = $self->order->db;
+
+  $db->with_transaction(
+    sub {
+      my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
+      $self->order->delete;
+      my $spool = $::lx_office_conf{paths}->{spool};
+      unlink map { "$spool/$_" } @spoolfiles if $spool;
+
+      $self->save_history('DELETED');
+
+      1;
+  }) || push(@{$errors}, $db->error);
+
+  return $errors;
+}
+
+# save the order
+#
+# And delete items that are deleted in the form.
+sub save {
+  my ($self) = @_;
+
+  my $errors = [];
+  my $db     = $self->order->db;
+
+  $db->with_transaction(sub {
+    # delete custom shipto if it is to be deleted or if it is empty
+    if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
+      $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
+      $self->order->custom_shipto(undef);
+    }
+
+    SL::DB::DeliveryOrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
+    $self->order->save(cascade => 1);
+
+    # link records
+    if ($::form->{converted_from_oe_id}) {
+      my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
+      foreach my $converted_from_oe_id (@converted_from_oe_ids) {
+        my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
+        $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/ && $self->order->is_type(PURCHASE_DELIVERY_ORDER_TYPE);
+        $src->link_to_record($self->order);
+      }
+      if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
+        my $idx = 0;
+        foreach (@{ $self->order->items_sorted }) {
+          my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
+          next if !$from_id;
+          SL::DB::RecordLink->new(from_table => 'orderitems',
+                                  from_id    => $from_id,
+                                  to_table   => 'orderitems',
+                                  to_id      => $_->id
+          )->save;
+          $idx++;
+        }
+      }
+    }
+
+    $self->save_history('SAVED');
+
+    1;
+  }) || push(@{$errors}, $db->error);
+
+  return $errors;
+}
+
+sub workflow_sales_or_request_for_quotation {
+  my ($self) = @_;
+
+  # always save
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) for @{ $errors };
+    return $self->js->render();
+  }
+
+  my $destination_type = $self->type_data->workflow("to_quotation_type");
+
+  $self->order(SL::DB::DeliveryOrder->new_from($self->order, destination_type => $destination_type));
+  $self->{converted_from_oe_id} = delete $::form->{id};
+
+  # set item ids to new fake id, to identify them as new items
+  foreach my $item (@{$self->order->items_sorted}) {
+    $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+  }
+
+  # change form type
+  $::form->{type} = $destination_type;
+  $self->type($self->init_type);
+  $self->cv  ($self->init_cv);
+  $self->check_auth;
+
+  $self->get_unalterable_data();
+  $self->pre_render();
+
+  # trigger rendering values for second row as hidden, because they
+  # are loaded only on demand. So we need to keep the values from the
+  # source.
+  $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
+
+  $self->render(
+    'delivery_order/form',
+    title => $self->get_title_for('edit'),
+    %{$self->{template_args}}
+  );
+}
+
+sub workflow_sales_or_purchase_order {
+  my ($self) = @_;
+
+  # always save
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  my $destination_type = $self->type_data->workflow("to_order_type");
+
+  # check for direct delivery
+  # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
+  my $custom_shipto;
+  if ($self->type_data->workflow("to_order_copy_shipto") && $::form->{use_shipto} && $self->order->shipto) {
+    $custom_shipto = $self->order->shipto->clone('SL::DB::DeliveryOrder');
+  }
+
+  $self->order(SL::DB::DeliveryOrder->new_from($self->order, destination_type => $destination_type));
+  $self->{converted_from_oe_id} = delete $::form->{id};
+
+  # set item ids to new fake id, to identify them as new items
+  foreach my $item (@{$self->order->items_sorted}) {
+    $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+  }
+
+  if ($self->type_data->workflow("to_order_copy_shipto")) {
+    if ($::form->{use_shipto}) {
+      $self->order->custom_shipto($custom_shipto) if $custom_shipto;
+    } else {
+      # remove any custom shipto if not wanted
+      $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
+    }
+  }
+
+  # change form type
+  $::form->{type} = $destination_type;
+  $self->type($self->init_type);
+  $self->cv  ($self->init_cv);
+  $self->check_auth;
+
+  $self->get_unalterable_data();
+  $self->pre_render();
+
+  # trigger rendering values for second row as hidden, because they
+  # are loaded only on demand. So we need to keep the values from the
+  # source.
+  $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
+
+  $self->render(
+    'delivery_order/form',
+    title => $self->get_title_for('edit'),
+    %{$self->{template_args}}
+  );
+}
+
+sub pre_render {
+  my ($self) = @_;
+
+  $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
+  $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
+  $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
+  $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->order->language_id ] ] );
+  $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
+                                                                                              deleted => 0 ] ],
+                                                                           sort_by => 'name');
+  $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
+                                                                                              deleted => 0 ] ],
+                                                                           sort_by => 'name');
+  $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
+                                                                                                        obsolete => 0 ] ]);
+  $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
+  $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
+  $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
+  $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
+
+  my $print_form = Form->new('');
+  $print_form->{type}        = $self->type;
+  $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
+  $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
+    form => $print_form,
+    options => {dialog_name_prefix => 'print_options.',
+                show_headers       => 1,
+                no_queue           => 1,
+                no_postscript      => 1,
+                no_opendocument    => 0,
+                no_html            => 1},
+  );
+
+  foreach my $item (@{$self->order->orderitems}) {
+    my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
+    $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
+    $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
+  }
+
+  if ($self->order->${\ $self->type_data->nr_key } && $::instance_conf->get_webdav) {
+    my $webdav = SL::Webdav->new(
+      type     => $self->type,
+      number   => $self->order->number,
+    );
+    my @all_objects = $webdav->get_all_objects;
+    @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
+                                                    type => t8('File'),
+                                                    link => File::Spec->catfile($_->full_filedescriptor),
+                                                } } @all_objects;
+  }
+
+  $self->{template_args}{in_out}                                 = $self->type_data->transfer;
+  $self->{template_args}{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
+
+  $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
+
+  $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.DeliveryOrder kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
+                                                         calculate_qty kivi.Validator follow_up show_history);
+  $self->setup_edit_action_bar;
+}
+
+sub setup_edit_action_bar {
+  my ($self, %params) = @_;
+
+  my $deletion_allowed = $self->type_data->show_menu("delete");
+  my $may_edit_create  = $::auth->assert($self->type_data->access('edit') || 'DOES_NOT_EXIST', 1);
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [
+          t8('Save'),
+          call     => [ 'kivi.DeliveryOrder.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
+                                                           $::instance_conf->get_order_warn_no_deliverydate,
+          ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save as new'),
+          call     => [ 'kivi.DeliveryOrder.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
+          disabled => !$may_edit_create                        ? t8('You do not have the permissions to access this function.')
+                    : $self->type eq 'supplier_delivery_order' ? t8('Need a workflow for Supplier Delivery Order')
+                    : !$self->order->id                        ? t8('This object has not been saved yet.')
+                    :                                            undef,
+        ],
+      ], # end of combobox "Save"
+
+      combobox => [
+        action => [
+          t8('Workflow'),
+        ],
+        action => [
+          t8('Save and Quotation'),
+          submit   => [ '#order_form', { action => "DeliveryOrder/sales_quotation" } ],
+          only_if  => $self->type_data->show_menu("save_and_quotation"),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and RFQ'),
+          submit   => [ '#order_form', { action => "DeliveryOrder/request_for_quotation" } ],
+          only_if  => $self->type_data->show_menu("save_and_rfq"),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Sales Order'),
+          submit   => [ '#order_form', { action => "DeliveryOrder/sales_order" } ],
+          only_if  => $self->type_data->show_menu("save_and_sales_order"),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Purchase Order'),
+          call     => [ 'kivi.DeliveryOrder.purchase_order_check_for_direct_delivery' ],
+          only_if  => $self->type_data->show_menu("save_and_purchase_order"),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Delivery Order'),
+          call     => [ 'kivi.DeliveryOrder.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
+                                                                              $::instance_conf->get_order_warn_no_deliverydate,
+          ],
+          only_if  => $self->type_data->show_menu("save_and_delivery_order"),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Invoice'),
+          call     => [ 'kivi.DeliveryOrder.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
+          only_if  => $self->type_data->show_menu("save_and_invoice"),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and AP Transaction'),
+          call     => [ 'kivi.DeliveryOrder.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
+          only_if  => $self->type_data->show_menu("save_and_ap_transaction"),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+
+      ], # end of combobox "Workflow"
+
+      combobox => [
+        action => [
+          t8('Export'),
+        ],
+        action => [
+          t8('Save and preview PDF'),
+           call    => [ 'kivi.DeliveryOrder.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
+                                                                  $::instance_conf->get_order_warn_no_deliverydate,
+          ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and print'),
+          call     => [ 'kivi.DeliveryOrder.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
+                                                                 $::instance_conf->get_order_warn_no_deliverydate,
+          ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and E-mail'),
+          id       => 'save_and_email_action',
+          call     => [ 'kivi.DeliveryOrder.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
+                                                                                 $::instance_conf->get_order_warn_no_deliverydate,
+          ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
+                    : !$self->order->id ? t8('This object has not been saved yet.')
+                    :                     undef,
+        ],
+        action => [
+          t8('Download attachments of all parts'),
+          call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
+                    : !$self->order->id ? t8('This object has not been saved yet.')
+                    :                     undef,
+          only_if  => $::instance_conf->get_doc_storage,
+        ],
+      ], # end of combobox "Export"
+
+      action => [
+        t8('Delete'),
+        id       => 'delete_action',
+        call     => [ 'kivi.DeliveryOrder.delete_order' ],
+        confirm  => $::locale->text('Do you really want to delete this object?'),
+        disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
+                  : !$self->order->id       ? t8('This object has not been saved yet.')
+                  : $self->order->delivered ? t8('The parts for this order have already been transferred')
+                  :                           undef,
+        only_if  => $self->type_data->show_menu("delete"),
+      ],
+
+      combobox => [
+        action => [
+          t8('Transfer out'),
+          id       => 'transfer_out_action',
+          call     => [ 'kivi.DeliveryOrder.save', 'transfer_stock' ],
+          disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
+                    : !$self->order->id       ? t8('This object has not been saved yet.')
+                    : $self->order->delivered ? t8('The parts for this order have already been transferred')
+                    :                           undef,
+          only_if  => $self->type_data->properties('transfer') eq 'out',
+          confirm  => t8('Do you really want to transfer the stock and set this order to delivered?'),
+        ],
+        action => [
+          t8('Transfer in'),
+          id       => 'transfer_in_action',
+          call     => [ 'kivi.DeliveryOrder.save', 'transfer_stock' ],
+          disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
+                    : !$self->order->id       ? t8('This object has not been saved yet.')
+                    : $self->order->delivered ? t8('The parts for this order have already been transferred')
+                    :                           undef,
+          only_if  => $self->type_data->properties('transfer') eq 'in',
+          confirm  => t8('Do you really want to transfer the stock and set this order to delivered?'),
+        ],
+      ],
+
+      combobox => [
+        action => [
+          t8('more')
+        ],
+        action => [
+          t8('Follow-Up'),
+          call     => [ 'kivi.DeliveryOrder.follow_up_window' ],
+          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+          only_if  => $::auth->assert('productivity', 1),
+        ],
+        action => [
+          t8('History'),
+          call     => [ 'set_history_window', $self->order->id, 'id' ],
+          disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
+        ],
+      ], # end of combobox "more"
+    );
+  }
+}
+
+sub generate_pdf {
+  my ($order, $pdf_ref, $params) = @_;
+
+  my @errors = ();
+
+  my $print_form = Form->new('');
+  $print_form->{type}        = $order->type;
+  $print_form->{formname}    = $params->{formname} || $order->type;
+  $print_form->{format}      = $params->{format}   || 'pdf';
+  $print_form->{media}       = $params->{media}    || 'file';
+  $print_form->{groupitems}  = $params->{groupitems};
+  $print_form->{printer_id}  = $params->{printer_id};
+  $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
+
+  $order->language($params->{language});
+  $order->flatten_to_form($print_form, format_amounts => 1);
+
+  my $template_ext;
+  my $template_type;
+  if ($print_form->{format} =~ /(opendocument|oasis)/i) {
+    $template_ext  = 'odt';
+    $template_type = 'OpenDocument';
+  }
+
+  # search for the template
+  my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
+    name        => $print_form->{formname},
+    extension   => $template_ext,
+    email       => $print_form->{media} eq 'email',
+    language    => $params->{language},
+    printer_id  => $print_form->{printer_id},
+  );
+
+  if (!defined $template_file) {
+    push @errors, $::locale->text('Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.', join ', ', map { "'$_'"} @template_files);
+  }
+
+  return @errors if scalar @errors;
+
+  $print_form->throw_on_error(sub {
+    eval {
+      $print_form->prepare_for_printing;
+
+      $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
+        format        => $print_form->{format},
+        template_type => $template_type,
+        template      => $template_file,
+        variables     => $print_form,
+        variable_content_types => {
+          longdescription => 'html',
+          partnotes       => 'html',
+          notes           => 'html',
+        },
+      );
+      1;
+    } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
+  });
+
+  return @errors;
+}
+
+sub get_files_for_email_dialog {
+  my ($self) = @_;
+
+  my %files = map { ($_ => []) } qw(versions files vc_files part_files);
+
+  return %files if !$::instance_conf->get_doc_storage;
+
+  if ($self->order->id) {
+    $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
+    $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
+    $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
+    $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
+  }
+
+  my @parts =
+    uniq_by { $_->{id} }
+    map {
+      +{ id         => $_->part->id,
+         partnumber => $_->part->partnumber }
+    } @{$self->order->items_sorted};
+
+  foreach my $part (@parts) {
+    my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
+    push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
+  }
+
+  foreach my $key (keys %files) {
+    $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
+  }
+
+  return %files;
+}
+
+sub get_title_for {
+  my ($self, $action) = @_;
+
+  return '' if none { lc($action)} qw(add edit);
+  return $self->type_data->text($action);
+}
+
+sub get_item_cvpartnumber {
+  my ($self, $item) = @_;
+
+  return if !$self->search_cvpartnumber;
+  return if !$self->order->customervendor;
+
+  if ($self->cv eq 'vendor') {
+    my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
+    $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
+  } elsif ($self->cv eq 'customer') {
+    my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
+    $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
+  }
+}
+
+sub get_part_texts {
+  my ($part_or_id, $language_or_id, %defaults) = @_;
+
+  my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
+  my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
+  my $texts       = {
+    description     => $defaults{description}     // $part->description,
+    longdescription => $defaults{longdescription} // $part->notes,
+  };
+
+  return $texts unless $language_id;
+
+  my $translation = SL::DB::Manager::Translation->get_first(
+    where => [
+      parts_id    => $part->id,
+      language_id => $language_id,
+    ]);
+
+  $texts->{description}     = $translation->translation     if $translation && $translation->translation;
+  $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
+
+  return $texts;
+}
+
+sub nr_key {
+  return $_[0]->type_data->nr_key;
+}
+
+sub save_and_redirect_to {
+  my ($self, %params) = @_;
+
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  flash_later('info', $self->type_data->text("saved"));
+
+  $self->redirect_to(%params, id => $self->order->id);
+}
+
+sub save_history {
+  my ($self, $addition) = @_;
+
+  my $number_type = $self->nr_key;
+  my $snumbers    = $number_type . '_' . $self->order->$number_type;
+
+  SL::DB::History->new(
+    trans_id    => $self->order->id,
+    employee_id => SL::DB::Manager::Employee->current->id,
+    what_done   => $self->order->type,
+    snumbers    => $snumbers,
+    addition    => $addition,
+  )->save;
+}
+
+sub store_pdf_to_webdav_and_filemanagement {
+  my($order, $content, $filename) = @_;
+
+  my @errors;
+
+  # copy file to webdav folder
+  if ($order->number && $::instance_conf->get_webdav_documents) {
+    my $webdav = SL::Webdav->new(
+      type     => $order->type,
+      number   => $order->number,
+    );
+    my $webdav_file = SL::Webdav::File->new(
+      webdav   => $webdav,
+      filename => $filename,
+    );
+    eval {
+      $webdav_file->store(data => \$content);
+      1;
+    } or do {
+      push @errors, t8('Storing PDF to webdav folder failed: #1', $@);
+    };
+  }
+  if ($order->id && $::instance_conf->get_doc_storage) {
+    eval {
+      SL::File->save(object_id     => $order->id,
+                     object_type   => $order->type,
+                     mime_type     => 'application/pdf',
+                     source        => 'created',
+                     file_type     => 'document',
+                     file_name     => $filename,
+                     file_contents => $content);
+      1;
+    } or do {
+      push @errors, t8('Storing PDF in storage backend failed: #1', $@);
+    };
+  }
+
+  return @errors;
+}
+
+sub calculate_stock_in_out_from_stock_info {
+  my ($self, $unit, $stock_info) = @_;
+
+  return "" if !$unit;
+
+  my %units_by_name = map { $_->name => $_ } @{ SL::DB::Manager::Unit->get_all };
+
+  my $sum      = sum0 map {
+    $units_by_name{$_->{unit}}->convert_to($_->{qty}, $units_by_name{$unit})
+  } @$stock_info;
+
+  my $content  = _format_number($sum, 2) . ' ' . $unit;
+
+  return $content;
+}
+
+sub calculate_stock_in_out {
+  my ($self, $item, $stock_info) = @_;
+
+  return "" if !$item->part || !$item->part->unit || !$item->unit;
+
+  my $sum      = sum0 map {
+    $_->unit_obj->convert_to($_->qty, $item->unit_obj)
+  } $item->delivery_order_stock_entries;
+
+  my $content  = _format_number($sum, 2);
+
+  return $content;
+}
+
+sub init_type_data {
+  SL::Controller::DeliveryOrder::TypeData->new($_[0]);
+}
+
+sub init_valid_types {
+  $_[0]->type_data->valid_types;
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::Order - controller for orders
+
+=head1 SYNOPSIS
+
+This is a new form to enter orders, completely rewritten with the use
+of controller and java script techniques.
+
+The aim is to provide the user a better experience and a faster workflow. Also
+the code should be more readable, more reliable and better to maintain.
+
+=head2 Key Features
+
+=over 4
+
+=item *
+
+One input row, so that input happens every time at the same place.
+
+=item *
+
+Use of pickers where possible.
+
+=item *
+
+Possibility to enter more than one item at once.
+
+=item *
+
+Item list in a scrollable area, so that the workflow buttons stay at
+the bottom.
+
+=item *
+
+Reordering item rows with drag and drop is possible. Sorting item rows is
+possible (by partnumber, description, qty, sellprice and discount for now).
+
+=item *
+
+No C<update> is necessary. All entries and calculations are managed
+with ajax-calls and the page only reloads on C<save>.
+
+=item *
+
+User can see changes immediately, because of the use of java script
+and ajax.
+
+=back
+
+=head1 CODE
+
+=head2 Layout
+
+=over 4
+
+=item * C<SL/Controller/Order.pm>
+
+the controller
+
+=item * C<template/webpages/delivery_order/form.html>
+
+main form
+
+=item * C<template/webpages/delivery_order/tabs/basic_data.html>
+
+Main tab for basic_data.
+
+This is the only tab here for now. "linked records" and "webdav" tabs are
+reused from generic code.
+
+=over 4
+
+=item * C<template/webpages/delivery_order/tabs/_business_info_row.html>
+
+For displaying information on business type
+
+=item * C<template/webpages/delivery_order/tabs/_item_input.html>
+
+The input line for items
+
+=item * C<template/webpages/delivery_order/tabs/_row.html>
+
+One row for already entered items
+
+=item * C<template/webpages/delivery_order/tabs/_tax_row.html>
+
+Displaying tax information
+
+=back
+
+=item * C<js/kivi.DeliveryOrder.js>
+
+java script functions
+
+=back
+
+=head1 TODO
+
+=over 4
+
+=item * testing
+
+=item * price sources: little symbols showing better price / better discount
+
+=item * select units in input row?
+
+=item * check for direct delivery (workflow sales order -> purchase order)
+
+=item * access rights
+
+=item * display weights
+
+=item * mtime check
+
+=item * optional client/user behaviour
+
+(transactions has to be set - department has to be set -
+ force project if enabled in client config - transport cost reminder)
+
+=back
+
+=head1 KNOWN BUGS AND CAVEATS
+
+=over 4
+
+=item *
+
+Customer discount is not displayed as a valid discount in price source popup
+(this might be a bug in price sources)
+
+(I cannot reproduce this (Bernd))
+
+=item *
+
+No indication that <shift>-up/down expands/collapses second row.
+
+=item *
+
+Inline creation of parts is not currently supported
+
+=item *
+
+Table header is not sticky in the scrolling area.
+
+=item *
+
+Sorting does not include C<position>, neither does reordering.
+
+This behavior was implemented intentionally. But we can discuss, which behavior
+should be implemented.
+
+=back
+
+=head1 To discuss / Nice to have
+
+=over 4
+
+=item *
+
+How to expand/collapse second row. Now it can be done clicking the icon or
+<shift>-up/down.
+
+=item *
+
+Possibility to select PriceSources in input row?
+
+=item *
+
+This controller uses a (changed) copy of the template for the PriceSource
+dialog. Maybe there could be used one code source.
+
+=item *
+
+Rounding-differences between this controller (PriceTaxCalculator) and the old
+form. This is not only a problem here, but also in all parts using the PTC.
+There exists a ticket and a patch. This patch should be testet.
+
+=item *
+
+An indicator, if the actual inputs are saved (like in an
+editor or on text processing application).
+
+=item *
+
+A warning when leaving the page without saveing unchanged inputs.
+
+
+=back
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Controller/DeliveryOrder/TypeData.pm b/SL/Controller/DeliveryOrder/TypeData.pm
new file mode 100644 (file)
index 0000000..0650e08
--- /dev/null
@@ -0,0 +1,101 @@
+package SL::Controller::DeliveryOrder::TypeData;
+
+use strict;
+use Exporter qw(import);
+use Scalar::Util qw(weaken);
+use SL::Locale::String qw(t8);
+use SL::DB::DeliveryOrder::TypeData qw(:types :subs);
+
+my @export_types = qw(SALES_DELIVERY_ORDER_TYPE PURCHASE_DELIVERY_ORDER_TYPE SUPPLIER_DELIVERY_ORDER_TYPE RMA_DELIVERY_ORDER_TYPE);
+
+our @EXPORT_OK = (@export_types);
+our %EXPORT_TAGS = (types => \@export_types);
+
+use Rose::Object::MakeMethods::Generic scalar => [ qw(c) ];
+
+sub new {
+  my ($class, $controller) = @_;
+  my $o = bless {}, $class;
+
+  if ($controller) {
+    $o->c($controller);
+    weaken($o->{c});
+  }
+
+  return $o;
+}
+
+sub validate {
+  my ($self, $string) = @_;
+  validate_type($string);
+}
+
+sub text {
+  my ($self, $string) = @_;
+  get3($self->c->type, "text", $string);
+}
+
+sub show_menu {
+  my ($self, $string) = @_;
+  get3($self->c->type, "show_menu", $string);
+}
+
+sub workflow {
+  my ($self, $string) = @_;
+  get3($self->c->type, "workflow", $string);
+}
+
+sub properties {
+  my ($self, $string) = @_;
+  get3($self->c->type, "properties", $string);
+}
+
+sub access {
+  my ($self, $string) = @_;
+  get3($_[0]->c->type, "rights", $string);
+}
+
+sub is_quotation {
+  get3($_[0]->c->type, "properties", "is_quotation");
+}
+
+sub customervendor {
+  get3($_[0]->c->type, "properties", "customervendor");
+}
+
+sub is_customer {
+  get3($_[0]->c->type, "properties", "is_customer");
+}
+
+sub nr_key {
+  get3($_[0]->c->type, "properties", "nr_key");
+}
+
+sub transfer {
+  get3($_[0]->c->type, "properties", "transfer");
+}
+
+sub part_classification_query {
+  my ($self, $string) = @_;
+  get($self->c->type, "part_classification_query");
+}
+
+sub set_reqdate_by_type {
+  my ($self) = @_;
+
+  if (!$self->c->order->reqdate) {
+    $self->c->order->reqdate(DateTime->today_local->next_workday(extra_days => 1));
+  }
+}
+
+sub get_reqdate_by_type {
+  my ($self, $reqdate, $saved_reqdate) = @_;
+
+  if ($reqdate == $saved_reqdate) {
+    return DateTime->today_local->next_workday(extra_days => 1);
+  } else {
+    return $reqdate;
+  }
+}
+
+1;
index 98f168b..6d7aaa9 100644 (file)
@@ -9,13 +9,13 @@ use SL::DB::Business;
 use SL::Controller::Helper::GetModels;
 use SL::Controller::Helper::ReportGenerator;
 use SL::Locale::String;
+use SL::Helper::ShippedQty;
 use SL::AM;
 use SL::DBUtils ();
 use Carp;
 
 use Rose::Object::MakeMethods::Generic (
-  scalar => [ qw(db_args flat_filter) ],
-  'scalar --get_set_init' => [ qw(models all_edit_right vc all_employees all_businesses) ],
+  'scalar --get_set_init' => [ qw(models all_edit_right vc all_employees all_businesses all_departments) ],
 );
 
 __PACKAGE__->run_before(sub { $::auth->assert('delivery_plan'); });
@@ -26,7 +26,6 @@ my %sort_columns = (
   partnumber        => t8('Part Number'),
   qty               => t8('Qty'),
   shipped_qty       => t8('shipped'),
-  delivered_qty     => t8('transferred in / out'),
   not_shipped_qty   => t8('not shipped'),
   ordnumber         => t8('Order'),
   customer          => t8('Customer'),
@@ -41,6 +40,7 @@ sub action_list {
 
   my $orderitems = $self->models->get;
   $self->calc_qtys($orderitems);
+  $self->setup_list_action_bar;
   $self->report_generator_list_objects(report => $self->{report}, objects => $orderitems);
 }
 
@@ -53,7 +53,7 @@ sub prepare_report {
   my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
   $self->{report} = $report;
 
-  my @columns     = qw(reqdate customer vendor ordnumber partnumber description qty shipped_qty not_shipped_qty delivered_qty);
+  my @columns     = qw(reqdate customer vendor ordnumber partnumber description qty shipped_qty not_shipped_qty);
 
   my @sortable    = qw(reqdate customer vendor ordnumber partnumber description);
 
@@ -66,7 +66,6 @@ sub prepare_report {
     qty               => {      sub => sub { $_[0]->qty_as_number . ' ' . $_[0]->unit                                        } },
     shipped_qty       => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]{shipped_qty}, 2) . ' ' . $_[0]->unit } },
     not_shipped_qty   => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]->qty - $_[0]{shipped_qty}, 2) . ' ' . $_[0]->unit } },
-    delivered_qty     => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]{delivered_qty}, 2) .' ' . $_[0]->unit } },
     ordnumber         => {      sub => sub { $_[0]->order->ordnumber                                                         },
                            obj_link => sub { $self->link_to($_[0]->order)                                                    } },
     vendor            => {      sub => sub { $_[0]->order->vendor->name                                                      },
@@ -104,46 +103,26 @@ sub prepare_report {
 
 sub calc_qtys {
   my ($self, $orderitems) = @_;
-  # using $orderitem->shipped_qty 40 times is far too slow. need to do it manually
-  #
 
   return unless scalar @$orderitems;
 
-  my %orderitems_by_id = map { $_->id => $_ } @$orderitems;
-
-  my $query = <<SQL;
-    SELECT oi.id, doi.qty, doi.unit, doe.delivered
-    FROM record_links rl
-    INNER JOIN delivery_order_items doi ON (doi.delivery_order_id = rl.to_id)
-    INNER JOIN delivery_orders doe ON (doe.id = rl.to_id)
-    INNER JOIN orderitems oi ON (oi.trans_id = rl.from_id)
-    WHERE rl.from_table = 'oe'
-      AND rl.to_table   = 'delivery_orders'
-      AND oi.parts_id   = doi.parts_id
-      AND oi.id IN (@{[ join ', ', ("?")x @$orderitems ]})
-SQL
-
-  my $result = SL::DBUtils::selectall_hashref_query($::form, $::form->get_standard_dbh, $query, map { $_->id } @$orderitems);
-
-  for my $row (@$result) {
-    my $item = $orderitems_by_id{ $row->{id} };
-    $item->{shipped_qty}   ||= 0;
-    $item->{delivered_qty} ||= 0;
-    $item->{shipped_qty}    += AM->convert_unit($row->{unit} => $item->unit) * $row->{qty};
-    $item->{delivered_qty}  += AM->convert_unit($row->{unit} => $item->unit) * $row->{qty} if $row->{delivered};
-  }
+  SL::Helper::ShippedQty
+    ->new()
+    ->calculate($orderitems)
+    ->write_to_objects;
 }
 
 sub make_filter_summary {
   my ($self) = @_;
   my $vc     = $self->vc;
-  my ($business, $employee);
+  my ($business, $employee, $department);
 
   my $filter = $::form->{filter} || {};
   my @filter_strings;
 
   $business = SL::DB::Business->new(id => $filter->{order}{customer}{"business_id"})->load->description if $filter->{order}{customer}{"business_id"};
   $employee = SL::DB::Employee->new(id => $filter->{order}{employee_id})->load->name if $filter->{order}{employee_id};
+  $department = SL::DB::Department->new(id => $filter->{order}{department_id})->load->description if $filter->{order}{department_id};
 
   my @filters = (
     [ $filter->{order}{"ordnumber:substr::ilike"},                    $::locale->text('Number')                                             ],
@@ -158,6 +137,7 @@ sub make_filter_summary {
     [ $filter->{order}{customer}{"name:substr::ilike"},               $::locale->text('Customer')                                           ],
     [ $filter->{order}{customer}{"customernumber:substr::ilike"},     $::locale->text('Customer Number')                                    ],
     [ $business,                                                      $::locale->text('Customer type')                                      ],
+    [ $department,                                                    $::locale->text('Department')                                         ],
     [ $employee,                                                      $::locale->text('Employee')                                           ],
   );
 
@@ -178,14 +158,12 @@ sub make_filter_summary {
   $self->{filter_summary} = join ', ', @filter_strings;
 }
 
-sub delivery_plan_query {
+sub delivery_plan_query_linked_items {
   my ($self) = @_;
   my $vc     = $self->vc;
   my $employee_id = SL::DB::Manager::Employee->current->id;
   my $oe_owner = $_[0]->all_edit_right ? '' : " oe.employee_id = $employee_id AND";
-  # check delivered state for delivery_orders (transferred out) if enabled
-  my $filter_delivered = ($::instance_conf->get_delivery_plan_calculate_transferred_do) ?
-      "AND (SELECT delivered from delivery_orders where id = doi.delivery_order_id)" : '';
+
   [
   "order.${vc}_id" => { gt => 0 },
   'order.closed' => 0,
@@ -193,36 +171,26 @@ sub delivery_plan_query {
 
   # filter by shipped_qty < qty, read from innermost to outermost
   'id' => [ \"
-    -- 3. resolve the desired information about those
-    SELECT oi.id FROM (
-      -- 2. slice only part, orderitem and both quantities from it
-      SELECT parts_id, trans_id, qty, SUM(doi_qty) AS doi_qty FROM (
-        -- 1. join orderitems and deliverorder items via record_links.
-        --    also add customer data to filter for sales_orders
-        SELECT oi.parts_id, oi.trans_id, oi.id, oi.qty, doi.qty AS doi_qty
-        FROM orderitems oi, oe, record_links rl, delivery_order_items doi
-        WHERE
-          oe.id = oi.trans_id AND
-          oe.${vc}_id IS NOT NULL AND
-          (oe.quotation = 'f' OR oe.quotation IS NULL) AND
-          NOT oe.closed AND
-          $oe_owner
-          rl.from_id = oe.id AND
-          rl.from_id = oi.trans_id AND
-          oe.id = oi.trans_id AND
-          rl.from_table = 'oe' AND
-          rl.to_table = 'delivery_orders' AND
-          rl.to_id = doi.delivery_order_id AND
-          oi.parts_id = doi.parts_id
-          $filter_delivered
-      ) tuples GROUP BY parts_id, trans_id, qty
-    ) partials
-    LEFT JOIN orderitems oi ON partials.parts_id = oi.parts_id AND partials.trans_id = oi.trans_id
-    WHERE oi.qty > doi_qty
+    SELECT id FROM (
+      SELECT oi.qty, oi.id, SUM(doi.qty) AS doi_qty
+      FROM orderitems oi, oe, record_links rl, delivery_order_items doi
+      WHERE
+        oe.id = oi.trans_id AND
+        oe.${vc}_id IS NOT NULL AND
+        (oe.quotation = 'f' OR oe.quotation IS NULL) AND
+        NOT oe.closed AND
+        $oe_owner
+        doi.id = rl.to_id AND
+        rl.from_table = 'orderitems'AND
+        rl.to_table   = 'delivery_order_items' AND
+        rl.from_id = oi.id
+      GROUP BY oi.id
+    ) linked
+    WHERE qty > doi_qty
 
     UNION ALL
 
-    -- 4. since the join over record_links fails for sales_orders without any delivery order
+    -- 2. since the join over record_links fails for items not in any delivery order
     --    retrieve those without record_links at all
     SELECT oi.id FROM orderitems oi, oe
     WHERE
@@ -231,58 +199,16 @@ sub delivery_plan_query {
       (oe.quotation = 'f' OR oe.quotation IS NULL) AND
       NOT oe.closed AND
       $oe_owner
-      oi.trans_id NOT IN (
+      NOT EXISTS (
         SELECT from_id
         FROM record_links rl
         WHERE
-          rl.from_table ='oe' AND
-          rl.to_table = 'delivery_orders'
+          rl.from_table = 'orderitems' AND
+          rl.to_table = 'delivery_order_items' AND
+          rl.from_id = oi.id
       )
 
-    UNION ALL
-
-    -- 5. now for the really nasty cases.
-    --    If someone partially delivered an order in several delivery orders,
-    --    there will be lots of record_links (4 doesn't catch those) but those
-    --    won't have matching part_ids in delivery_order_items, so 1-3 can't
-    --    find anything
-    --    In this case aggreg record_links - delivery_order - delivery_order_items
-    --    slice only oe.id, parts_id and sum of of qty
-    --    left join that onto orderitems to get matching qtys in doi while retaining
-    --    entrys without matches and then throw out those without record_links
-    --    TODO: join this and 1-3 into a general case
-                  -- need debug info? uncomment these:
-    SELECT oi.id  -- ,oi.trans_id, oi.parts_id, coalesce(sum, 0), agg.parts_id
-    FROM orderitems oi LEFT JOIN (
-      SELECT rl.from_id as oid, doi.parts_id, sum(doi.qty) FROM (
-        SELECT from_id, to_id
-        FROM record_links rl
-        LEFT JOIN oe ON oe.id = from_id
-        WHERE
-          rl.from_table = 'oe' AND
-          rl.to_table = 'delivery_orders' AND
-
-          oe.${vc}_id IS NOT NULL AND
-          $oe_owner
-          (oe.quotation = 'f' OR oe.quotation IS NULL) AND NOT oe.closed
-      ) rl
-      LEFT JOIN delivery_order_items doi ON (rl.to_id = doi.delivery_order_id)
-      WHERE 1 = 1
-      $filter_delivered
-      GROUP BY rl.from_id, doi.parts_id
-    ) agg ON (agg.oid = oi.trans_id AND agg.parts_id = oi.parts_id)
-    LEFT JOIN oe ON oe.id = oi.trans_id
-    WHERE
-      EXISTS (
-        SELECT to_id
-        FROM record_links rl
-        WHERE oi.trans_id = rl.from_id AND rl.from_table = 'oe' AND rl.to_table = 'delivery_orders'
-      ) AND
-      coalesce(sum, 0) < oi.qty AND
-      oe.${vc}_id IS NOT NULL AND
-      $oe_owner
-      (oe.quotation = 'f' OR oe.quotation IS NULL) AND NOT oe.closed
-  " ],
+  " ], # make emacs happy again: " ]
   ]
 }
 
@@ -290,6 +216,8 @@ sub init_models {
   my ($self) = @_;
   my $vc     = $self->vc;
 
+  my $query = $self->delivery_plan_query_linked_items;
+
   SL::Controller::Helper::GetModels->new(
     controller   => $self,
     model        => 'OrderItem',
@@ -300,14 +228,14 @@ sub init_models {
       },
       %sort_columns,
     },
-    query        => $self->delivery_plan_query,
+    query        => $query,
     with_objects => [ 'order', "order.$vc", 'part' ],
-    additional_url_params => { vc => $vc},
+    additional_url_params => { vc => $vc },
   );
 }
 
 sub init_all_edit_right {
-  $::auth->assert('sales_all_edit', 1)
+  return $_[0]->vc eq 'customer' ? $::auth->assert('sales_all_edit', 1) : $::auth->assert('purchase_all_edit', 1);
 }
 sub init_vc {
   return $::form->{vc} if ($::form->{vc} eq 'customer' || $::form->{vc} eq 'vendor') || croak "self (DeliveryPlan) has no vc defined";
@@ -319,6 +247,9 @@ sub init_all_employees {
 sub init_all_businesses {
   return SL::DB::Manager::Business->get_all_sorted;
 }
+sub init_all_departments {
+  return SL::DB::Manager::Department->get_all_sorted;
+}
 sub link_to {
   my ($self, $object, %params) = @_;
 
@@ -330,11 +261,15 @@ sub link_to {
     my $vc     = $object->is_sales ? 'customer' : 'vendor';
     my $id     = $object->id;
 
-    return "oe.pl?action=$action&type=$type&vc=$vc&id=$id";
+    if ($::instance_conf->get_feature_experimental_order) {
+      return "controller.pl?action=Order/$action&type=$type&id=$id";
+    } else {
+      return "oe.pl?action=$action&type=$type&vc=$vc&id=$id";
+    }
   }
   if ($object->isa('SL::DB::Part')) {
     my $id     = $object->id;
-    return "ic.pl?action=$action&id=$id";
+    return "controller.pl?action=Part/$action&part.id=$id";
   }
   if ($object->isa('SL::DB::Customer')) {
     my $id     = $object->id;
@@ -342,4 +277,18 @@ sub link_to {
   }
 }
 
+sub setup_list_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#filter_form', { action => 'DeliveryPlan/list' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
 1;
index 4810d21..d566b5c 100644 (file)
@@ -7,6 +7,7 @@ use parent qw(SL::Controller::Base);
 use SL::DB::DeliveryTerm;
 use SL::DB::Language;
 use SL::Helper::Flash;
+use SL::Locale::String qw(t8);
 
 use Rose::Object::MakeMethods::Generic
 (
@@ -25,6 +26,7 @@ __PACKAGE__->run_before('load_languages',     only => [ qw(new list edit create
 sub action_list {
   my ($self) = @_;
 
+  $self->setup_list_action_bar;
   $self->render('delivery_term/list',
                 title          => $::locale->text('Delivery terms'),
                 DELIVERY_TERMS => SL::DB::Manager::DeliveryTerm->get_all_sorted);
@@ -34,11 +36,13 @@ sub action_new {
   my ($self) = @_;
 
   $self->{delivery_term} = SL::DB::DeliveryTerm->new;
+  $self->setup_form_action_bar;
   $self->render('delivery_term/form', title => $::locale->text('Create a new delivery term'));
 }
 
 sub action_edit {
   my ($self) = @_;
+  $self->setup_form_action_bar;
   $self->render('delivery_term/form', title => $::locale->text('Edit delivery term'));
 }
 
@@ -120,4 +124,49 @@ sub load_languages {
   $self->{languages} = SL::DB::Manager::Language->get_all_sorted;
 }
 
+sub setup_list_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      link => [
+        t8('Add'),
+        link => $self->url_for(action => 'new'),
+      ],
+    );
+  }
+}
+
+sub setup_form_action_bar {
+  my ($self) = @_;
+
+  my $is_new = !$self->delivery_term->id;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => 'DeliveryTerm/' . ($is_new ? 'create' : 'update') } ],
+        checks    => [ 'kivi.validate_form' ],
+        accesskey => 'enter',
+      ],
+
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => 'DeliveryTerm/destroy' } ],
+        confirm  => t8('Do you really want to delete this object?'),
+        disabled => $is_new ? t8('This object has not been saved yet.') : undef,
+      ],
+
+      'separator',
+
+      link => [
+        t8('Abort'),
+        link => $self->url_for(action => 'list'),
+      ],
+    );
+  }
+  $::request->layout->add_javascripts('kivi.Validator.js');
+}
+
 1;
index 858472b..ae2032f 100644 (file)
@@ -9,14 +9,15 @@ use SL::DB::Business;
 use SL::Controller::Helper::GetModels;
 use SL::Controller::Helper::ReportGenerator;
 use SL::Locale::String;
+use SL::Helper::ShippedQty;
 use SL::AM;
-use SL::DBUtils ();
+use SL::DBUtils qw(selectall_as_map);
+use List::MoreUtils qw(uniq);
 use Carp;
 use Data::Dumper;
 
 use Rose::Object::MakeMethods::Generic (
-  scalar => [ qw(db_args flat_filter) ],
-  'scalar --get_set_init' => [ qw(models vc all_employees all_businesses) ],
+  'scalar --get_set_init' => [ qw(models vc all_employees all_businesses all_partsgroups) ],
 );
 
 __PACKAGE__->run_before(sub { $::auth->assert('delivery_value_report'); });
@@ -38,7 +39,7 @@ my %sort_columns = (
   delivered_qty           => t8('transferred in / out'),
   netto_delivered_qty     => t8('Net value transferred in / out'),
   do_closed_qty           => t8('Qty in closed delivery orders'),
-  netto_do_closed_qty     => t8('Qty in closed delivery orders')
+  netto_do_closed_qty     => t8('Net value in closed delivery orders'),
 );
 
 
@@ -55,6 +56,7 @@ sub action_list {
 
   my $orderitems = $self->models->get;
   $self->calc_qtys_price($orderitems);
+  $self->setup_list_action_bar;
   $self->report_generator_list_objects(report => $self->{report}, objects => $orderitems);
 }
 
@@ -82,29 +84,18 @@ sub prepare_report {
                            obj_link => sub { $self->link_to($_[0]->part)                                      } },
     partnumber        => {      sub => sub { $_[0]->part->partnumber                                          },
                            obj_link => sub { $self->link_to($_[0]->part)                                      } },
-    qty               => {      sub => sub { $_[0]->qty_as_number .
-                                             ($rp_csv_mod ? '' : ' ' .  $_[0]->unit)                           } },
-    netto_qty         => {      sub => sub { $::form->format_amount(\%::myconfig,
-                                              ($_[0]->qty * $_[0]->sellprice * (1 - $_[0]->discount) /
-                                                                         ($_[0]->price_factor || 1), 2))       },},
-    unit              => {      sub => sub {  $_[0]->unit                                                      },
-                            visible => $rp_csv_mod                                                             },
-    shipped_qty       => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]{shipped_qty}, 2) .
-                                             ($rp_csv_mod ? '' : ' ' .  $_[0]->unit)                           } },
-    netto_shipped_qty => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]{netto_shipped_qty}, 2) },},
-    not_shipped_qty   => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]->qty - $_[0]{shipped_qty}
-                                               - $_[0]{delivered_qty} - $_[0]{do_closed_qty}, 2) .
-                                             ($rp_csv_mod ? '' : ' ' .  $_[0]->unit)                           } },
-    delivered_qty     => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]{delivered_qty}, 2) .
-                                             ($rp_csv_mod ? '' : ' ' .  $_[0]->unit)                           } },
-    netto_delivered_qty => {    sub => sub { $::form->format_amount(\%::myconfig, $_[0]{netto_delivered_qty}, 2) },},
-    netto_not_shipped_qty => {  sub => sub { $::form->format_amount(\%::myconfig,(($_[0]->qty -
-                                             $_[0]{shipped_qty} - $_[0]{delivered_qty} - $_[0]{do_closed_qty})
-                                             * ($_[0]->sellprice * (1 - $_[0]->discount) /
-                                                                             ($_[0]->price_factor || 1)), 2))  },},
-    do_closed_qty     => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]{do_closed_qty}, 2) .
-                                             ($rp_csv_mod ? '' : ' ' .  $_[0]->unit)                           },},
-    netto_do_closed_qty => {    sub => sub { $::form->format_amount(\%::myconfig, $_[0]{netto_do_closed_qty}, 2) },},
+    qty               => {      sub => sub { _format_qty($_[0], 'qty', $rp_csv_mod)                           } },
+    netto_qty         => {      sub => sub { _format_val($_[0], 'qty')                                        },},
+    unit              => {      sub => sub {  $_[0]->unit                                                     },
+                            visible => $rp_csv_mod                                                              },
+    shipped_qty       => {      sub => sub { _format_qty($_[0], 'shipped_qty', $rp_csv_mod)                   } },
+    netto_shipped_qty => {      sub => sub { _format_val($_[0], 'shipped_qty')                                },},
+    not_shipped_qty   => {      sub => sub { _format_qty($_[0], 'not_shipped_qty', $rp_csv_mod)               } },
+    netto_not_shipped_qty => {  sub => sub { _format_val($_[0], 'not_shipped_qty')                            },},
+    delivered_qty     => {      sub => sub { _format_qty($_[0], 'delivered_qty', $rp_csv_mod)                 } },
+    netto_delivered_qty => {    sub => sub { _format_val($_[0], 'delivered_qty')                              },},
+    do_closed_qty     => {      sub => sub { _format_qty($_[0], 'do_closed_qty', $rp_csv_mod)                 },},
+    netto_do_closed_qty => {    sub => sub { _format_val($_[0], 'do_closed_qty')                              },},
     ordnumber         => {      sub => sub { $_[0]->order->ordnumber                                           },
                            obj_link => sub { $self->link_to($_[0]->order)                                      } },
     vendor            => {      sub => sub { $_[0]->order->vendor->name                                        },
@@ -150,13 +141,14 @@ sub prepare_report {
 sub make_filter_summary {
   my ($self) = @_;
   my $vc     = $self->vc;
-  my ($business, $employee);
+  my ($business, $employee, $partsgroup);
 
   my $filter = $::form->{filter} || {};
   my @filter_strings;
 
-  $business = SL::DB::Business->new(id => $filter->{order}{customer}{"business_id"})->load->description if $filter->{order}{customer}{"business_id"};
-  $employee = SL::DB::Employee->new(id => $filter->{order}{employee_id})->load->name if $filter->{order}{employee_id};
+  $business   = SL::DB::Business->new(id => $filter->{order}{customer}{"business_id"})->load->description if $filter->{order}{customer}{"business_id"};
+  $employee   = SL::DB::Employee->new(id => $filter->{order}{employee_id})->load->name                    if $filter->{order}{employee_id};
+  $partsgroup = SL::DB::PartsGroup->new(id => $filter->{part}{partsgroup_id})->load->partsgroup           if $filter->{part}{partsgroup_id};
 
   my @filters = (
     [ $filter->{order}{"ordnumber:substr::ilike"},                    $::locale->text('Number')                                             ],
@@ -172,6 +164,7 @@ sub make_filter_summary {
     [ $filter->{order}{customer}{"customernumber:substr::ilike"},     $::locale->text('Customer Number')                                    ],
     [ $business,                                                      $::locale->text('Customer type')                                      ],
     [ $employee,                                                      $::locale->text('Employee')                                           ],
+    [ $partsgroup,                                                    $::locale->text('Partsgroup')                                         ],
   );
 
   # flags for with_object 'part'
@@ -226,6 +219,9 @@ sub init_all_employees {
 sub init_all_businesses {
   return SL::DB::Manager::Business->get_all_sorted;
 }
+sub init_all_partsgroups {
+  return SL::DB::Manager::PartsGroup->get_all_sorted;
+}
 
 
 sub link_to {
@@ -239,11 +235,15 @@ sub link_to {
     my $vc     = $object->is_sales ? 'customer' : 'vendor';
     my $id     = $object->id;
 
-    return "oe.pl?action=$action&type=$type&vc=$vc&id=$id";
+    if ($::instance_conf->get_feature_experimental_order) {
+      return "controller.pl?action=Order/$action&type=$type&id=$id";
+    } else {
+      return "oe.pl?action=$action&type=$type&vc=$vc&id=$id";
+    }
   }
   if ($object->isa('SL::DB::Part')) {
     my $id     = $object->id;
-    return "ic.pl?action=$action&id=$id";
+    return "controller.pl?action=Part/$action&part.id=$id";
   }
   if ($object->isa('SL::DB::Customer')) {
     my $id     = $object->id;
@@ -251,50 +251,72 @@ sub link_to {
   }
 }
 
+sub _format_qty {
+  my ($item, $col, $csv_mod) = @_;
+
+  $::form->format_amount(\%::myconfig, $item->{$col}, 2) .  ($csv_mod ? '' : ' ' .  $item->unit)
+}
+
+sub _format_val {
+  my ($item, $col) = @_;
+
+  $::form->format_amount(\%::myconfig, $item->{$col} * $item->sellprice * (1 - $item->discount) / ($item->price_factor || 1), 2)
+}
+
 
 sub calc_qtys_price {
   my ($self, $orderitems) = @_;
-  # using $orderitem->shipped_qty 40 times is far too slow. need to do it manually
-  # also for calc net values
 
   return unless scalar @$orderitems;
 
-  my %orderitems_by_id = map { $_->id => $_ } @$orderitems;
+  SL::Helper::ShippedQty
+    ->new(require_stock_out => 1)
+    ->calculate($orderitems)
+    ->write_to_objects;
 
-  my $query = <<SQL;
-    SELECT oi.id, doi.qty, doi.unit, doe.delivered, doe.closed,
-           oi.sellprice, oi.discount, oi.price_factor
-    FROM record_links rl
-    INNER JOIN delivery_order_items doi ON (doi.id = rl.to_id)
-    INNER JOIN orderitems oi            ON (oi.id  = rl.from_id)
-    INNER JOIN delivery_orders doe      ON (doe.id = doi.delivery_order_id)
-    WHERE rl.from_table = 'orderitems'
-      AND rl.to_table   = 'delivery_order_items'
-      AND oi.id IN (@{[ join ', ', ("?")x @$orderitems ]})
-SQL
-
-  my $result = SL::DBUtils::selectall_hashref_query($::form, $::form->get_standard_dbh, $query, map { $_->id } @$orderitems);
-
-  for my $row (@$result) {
-    my $item = $orderitems_by_id{ $row->{id} };
-    $item->{shipped_qty}   ||= 0;
-    $item->{delivered_qty} ||= 0;
-    $item->{do_closed_qty} ||= 0;
-    $item->{shipped_qty}    += AM->convert_unit($row->{unit} => $item->unit) * $row->{qty}  unless ($row->{delivered} || $row->{closed});
-    $item->{delivered_qty}  += AM->convert_unit($row->{unit} => $item->unit) * $row->{qty}  if ($row->{delivered} && !$row->{closed});
-    $item->{do_closed_qty}  += AM->convert_unit($row->{unit} => $item->unit) * $row->{qty}  if ($row->{closed});
-    $item->{not_shipped_qty} += AM->convert_unit($row->{unit} => $item->unit) * $row->{qty} unless ($row->{delivered});
-
-    my $price_factor = $row->{price_factor} || 1;
-    $item->{netto_shipped_qty}   = $item->{shipped_qty} * $row->{sellprice} * (1 - $row->{discount} ) / $price_factor;
-    $item->{netto_delivered_qty} = $item->{delivered_qty} * $row->{sellprice} * (1 - $row->{discount} ) / $price_factor;
-    $item->{netto_do_closed_qty} = $item->{do_closed_qty} * $row->{sellprice} * (1 - $row->{discount} ) / $price_factor;
+  $_->{delivered_qty} = delete $_->{shipped_qty} for @$orderitems;
+
+  my $helper = SL::Helper::ShippedQty
+    ->new(require_stock_out => 0, keep_matches => 1)
+    ->calculate($orderitems)
+    ->write_to_objects;
 
+  for my $item (@$orderitems) {
+    $item->{not_shipped_qty} = $item->qty - $item->{shipped_qty};
+    $item->{do_closed_qty}   = 0;
+
+    my $price_factor = $item->price_factor || 1;
   }
-}
 
+  if (my @all_doi_ids = uniq map { $_->[1] } @{ $helper->matches }) {
+    my %oi_by_id = map { $_->id => $_ } @$orderitems;
+    my $query    = sprintf <<'', join ', ', ("?")x@all_doi_ids;
+      SELECT DISTINCT doi.id, closed FROM delivery_orders
+      LEFT JOIN delivery_order_items doi ON (doi.delivery_order_id = delivery_orders.id)
+      WHERE doi.id IN (%s)
 
+    my %doi_is_closed = selectall_as_map($::form, SL::DB->client->dbh, $query, (id => 'closed'), @all_doi_ids);
 
+    for my $match (@{ $helper->matches }) {
+      next unless $doi_is_closed{$match->[1]};
+      $oi_by_id{$match->[0]}->{do_closed_qty} += $match->[2];
+    }
+  }
+}
+
+sub setup_list_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#filter_form', { action => 'DeliveryValueReport/list' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
 
 1;
 
diff --git a/SL/Controller/Department.pm b/SL/Controller/Department.pm
deleted file mode 100644 (file)
index 03d3fb6..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-package SL::Controller::Department;
-
-use strict;
-
-use parent qw(SL::Controller::Base);
-
-use SL::DB::Department;
-use SL::Helper::Flash;
-
-use Rose::Object::MakeMethods::Generic
-(
- scalar => [ qw(department) ],
-);
-
-__PACKAGE__->run_before('check_auth');
-__PACKAGE__->run_before('load_department', only => [ qw(edit update destroy) ]);
-
-#
-# actions
-#
-
-sub action_list {
-  my ($self) = @_;
-
-  $self->render('department/list',
-                title       => $::locale->text('Departments'),
-                DEPARTMENTS => SL::DB::Manager::Department->get_all_sorted);
-}
-
-sub action_new {
-  my ($self) = @_;
-
-  $self->render('department/form', title => $::locale->text('Create a new department'));
-}
-
-sub action_edit {
-  my ($self) = @_;
-  $self->render('department/form', title => $::locale->text('Edit department'));
-}
-
-sub action_create {
-  my ($self) = @_;
-
-  $self->{department} = SL::DB::Department->new;
-  $self->create_or_update;
-}
-
-sub action_update {
-  my ($self) = @_;
-  $self->create_or_update;
-}
-
-sub action_destroy {
-  my ($self) = @_;
-
-  if (eval { $self->{department}->delete; 1; }) {
-    flash_later('info',  $::locale->text('The department has been deleted.'));
-  } else {
-    flash_later('error', $::locale->text('The department is in use and cannot be deleted.'));
-  }
-
-  $self->redirect_to(action => 'list');
-}
-
-#
-# filters
-#
-
-sub check_auth {
-  $::auth->assert('config');
-}
-
-#
-# helpers
-#
-
-sub create_or_update {
-  my $self   = shift;
-  my $is_new = !$self->{department}->id;
-  my $params = delete($::form->{department}) || { };
-
-  $self->{department}->assign_attributes(%{ $params });
-
-  my @errors = $self->{department}->validate;
-
-  if (@errors) {
-    flash('error', @errors);
-    $self->render('department/form', title => $is_new ? $::locale->text('Create a new department') : $::locale->text('Edit department'));
-    return;
-  }
-  $self->{department}->save;
-
-  flash_later('info', $is_new ? $::locale->text('The department has been created.') : $::locale->text('The department has been saved.'));
-  $self->redirect_to(action => 'list');
-}
-
-sub load_department {
-  my ($self) = @_;
-  $self->{department} = SL::DB::Department->new(id => $::form->{id})->load;
-}
-
-1;
diff --git a/SL/Controller/DownloadZip.pm b/SL/Controller/DownloadZip.pm
new file mode 100644 (file)
index 0000000..33514c4
--- /dev/null
@@ -0,0 +1,131 @@
+package SL::Controller::DownloadZip;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use List::Util qw(first max);
+
+use utf8;
+use Encode qw(decode encode);
+use Archive::Zip;
+use SL::File;
+use SL::SessionFile::Random;
+
+sub action_download_orderitems_files {
+  my ($self) = @_;
+
+  #
+  # special case for customer which want to have not all
+  # in kivitendo.conf some regex may be defined:
+  # For no values just let it commented out
+  # PA = Produktionsauftrag, L = Lieferschein, ML = Materialliste
+  # If you want several options, please separate the letter with '|'. Example: '^(PA|L).*'
+  #set_sales_documenttype_for_delivered_quantity = '^(LS).*'
+  #set_purchase_documenttype_for_delivered_quantity = '^(EL).*'
+  #
+  # enbale this perl code:
+  #  my $doctype = $::lx_office_conf{system}->{"set_documenttype_for_part_zip_download"};
+  #  if ( $doctype ) {
+  #    # eliminate first and last char (are quotes)
+  #    $doctype =~ s/^.//;
+  #    $doctype =~ s/.$//;
+  #  }
+
+  #$Archive::Zip::UNICODE = 1;
+
+  my $object_id    = $::form->{object_id};
+  my $object_type  = $::form->{object_type};
+  my $element_type = $::form->{element_type};
+  my $sfile = SL::SessionFile::Random->new(mode => "w");
+  my $zip = Archive::Zip->new();
+  #TODO Check client encoding !!
+  #my $name_encoding = 'cp850';
+  my $name_encoding = 'UTF-8';
+
+  if (   $object_id
+      && ($object_type =~ m{^(?:sales_order|purchase_order|sales_quotation|request_quotation)$})
+      && ($element_type eq 'part')) {
+    my $orderitems = SL::DB::Manager::OrderItem->get_all(query => ['order.id' => $object_id ],
+                                                         with_objects => [ 'order', 'part' ],
+                                                         sort_by => 'part.partnumber ASC');
+    foreach my $item ( @{$orderitems} ) {
+      my @files = SL::File->get_all(object_id   => $item->parts_id,
+                                    object_type => $element_type,
+                                  );
+      next unless @files;
+
+      $zip->addDirectory($item->part->partnumber);
+      $zip->addFile($_->get_file, Encode::encode($name_encoding, $item->part->partnumber . '/' . $_->db_file->file_name)) for @files;
+    }
+  }
+  unless ( $zip->writeToFileNamed($sfile->file_name) == Archive::Zip::AZ_OK ) {
+    die 'zipfile write error';
+  }
+  $sfile->fh->close;
+
+  return $self->send_file(
+    $sfile->file_name,
+    type => 'application/zip',
+    name => $::form->{zipname}.'.zip',
+  );
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::DownloadZip - controller for download all files from parts of an order in one zip file
+
+=head2  C<action_download_zip FORMPARAMS>
+
+Some customer want all attached files for the parts of an sales order or sales delivery order in one zip to download.
+This is a special method for one customer, so it is moved into an extra controller.
+The $Archive::Zip::UNICODE = 1; doesnt work ok
+So today the filenames in cp850/DOS format for legacy windows.
+To ues it for Linux Clients an additinal effort must be done,
+for ex. a link to the same file with an utf-8 name.
+
+There is also a special javascript method necessary which calles this controller method.
+THis method must be inserted into the customer branch:
+
+=begin text
+
+  ns.downloadOrderitemsAtt = function(type,id) {
+    var rowcount  = $('input[name=rowcount]').val() - 1;
+    var data = {
+        action:     'FileManagement/download_zip',
+        type:       type,
+        object_id:  id,
+        rowcount:   rowcount
+    };
+    if ( rowcount == 0 ) {
+        kivi.display_flash('error', kivi.t8('No articles have been added yet.'));
+        return false;
+    }
+    for (var i = 1; i <= rowcount; i++) {
+        data['parts_id_'+i] =  $('#id_' + i).val();
+    };
+    $.download("controller.pl", data);
+    return false;
+  }
+
+=end text
+
+See also L<SL::Controller::FileManagement>
+
+=head1 DISCUSSION
+
+Is this method needed in the master branch ?
+
+=head1 AUTHOR
+
+Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
+
+=cut
diff --git a/SL/Controller/Draft.pm b/SL/Controller/Draft.pm
new file mode 100644 (file)
index 0000000..02e74f5
--- /dev/null
@@ -0,0 +1,202 @@
+package SL::Controller::Draft;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use SL::Helper::Flash qw(flash);
+use SL::Locale::String qw(t8);
+use SL::Request;
+use SL::DB::Draft;
+use SL::DBUtils qw(selectall_hashref_query);
+use SL::YAML;
+use List::Util qw(max);
+
+use Rose::Object::MakeMethods::Generic (
+ scalar => [ qw() ],
+ 'scalar --get_set_init' => [ qw(module submodule draft) ],
+);
+
+__PACKAGE__->run_before('check_auth');
+
+my %allowed_modules = map { $_ => "bin/mozilla/$_.pl" } qw(is ir ar ap gl);
+
+#
+# actions
+#
+
+sub action_draft_dialog {
+  my ($self) = @_;
+  $self->js
+    ->dialog->open({
+      html   => $self->dialog_html,
+      id     => 'save_draft',
+      dialog => {
+        title => t8('Drafts'),
+      },
+    })
+    ->render;
+}
+
+sub action_save {
+  my ($self) = @_;
+
+  my $id          = $::form->{id};
+  my $description = $::form->{description} or die 'need description';
+  my $form        = $self->_build_form;
+
+  my $draft = SL::DB::Manager::Draft->find_by_or_create(id => $id);
+
+  $draft->id($self->module . '-' . $self->submodule . '-' . Common::unique_id()) unless $draft->id;
+
+  $draft->assign_attributes(
+    module      => $self->module,
+    submodule   => $self->submodule,
+    description => $description,
+    form        => SL::YAML::Dump($form),
+    employee_id => SL::DB::Manager::Employee->current->id,
+  );
+
+  $self->draft($draft);
+
+  if (!$draft->save) {
+    flash('error', t8('There was an error saving the draft'));
+    $self->js
+      ->html('#save_draft', $self->dialog_html)
+      ->render;
+  } else {
+    $self->js
+      ->flash('info', t8("Draft saved."))
+      ->dialog->close('#save_draft')
+      ->val('#draft_id', $draft->id)
+      ->render;
+  }
+}
+
+sub action_load {
+  my ($self) = @_;
+
+  if (!$allowed_modules{ $self->draft->module }) {
+    $::form->error(t8('Unknown module: #1', $self->draft->module));
+  } else {
+    package main;
+    require $allowed_modules{ $self->draft->module };
+  }
+  my $params = delete $::form->{form};
+  my $new_form = SL::YAML::Load($self->draft->form);
+  $::form->{$_} = $new_form->{$_} for keys %$new_form;
+  $::form->{"draft_$_"} = $self->draft->$_ for qw(id description);
+
+  if ($params && 'HASH' eq ref $params) {
+    $::form->{$_} = $params->{$_} for keys %$params;
+  }
+  $::form->{script} = $self->draft->module . '.pl';
+  ::show_draft();
+}
+
+sub action_delete {
+  my ($self) = @_;
+
+  $self->module($self->draft->module);
+  $self->submodule($self->draft->submodule);
+
+  if (!$self->draft->delete) {
+    flash('error', t8('There was an error deleting the draft'));
+    $self->js
+      ->html('#save_draft', $self->dialog_html)
+      ->render;
+  } else {
+    flash('info', t8('Draft deleted'));
+
+    $self->js
+      ->html('#save_draft', $self->dialog_html)
+      ->render;
+  }
+}
+
+#
+# helpers
+#
+
+sub _build_form {
+  my $last_index = max map { /form\[(\d+)\]/ ? $1 : 0 } keys %$::form;
+  my $new_form = {};
+
+  for my $i (0..$last_index) {
+    SL::Request::_store_value($new_form, $::form->{"form[$i][name]"}, $::form->{"form[$i][value]"});
+  }
+
+  return $new_form;
+}
+
+sub draft_list {
+  my ($self) = @_;
+
+  if ($::auth->assert('all_drafts_edit', 1)) {
+   my $result = selectall_hashref_query($::form, $::form->get_standard_dbh, <<SQL, $self->module, $self->submodule);
+    SELECT d.*, date(d.itime) AS date
+    FROM drafts d
+    WHERE (d.module      = ?)
+      AND (d.submodule   = ?)
+    ORDER BY d.itime
+SQL
+  } else {
+    my $result = selectall_hashref_query($::form, $::form->get_standard_dbh, <<SQL, $self->module, $self->submodule, SL::DB::Manager::Employee->current->id);
+    SELECT d.*, date(d.itime) AS date
+    FROM drafts d
+    WHERE (d.module      = ?)
+      AND (d.submodule   = ?)
+      AND (d.employee_id = ?)
+    ORDER BY d.itime
+SQL
+  }
+}
+
+sub dialog_html {
+  my ($self) = @_;
+
+  $self->render('drafts/form', { layout => 0, output => 0 },
+    drafts_list => $self->draft_list
+  )
+}
+
+sub init_module {
+  $::form->{module}      or die 'need module';
+}
+
+sub init_submodule {
+  $::form->{submodule}   or die 'need submodule';
+}
+
+sub init_draft {
+  SL::DB::Manager::Draft->find_by(id => $::form->{id}) or die t8('Could not load this draft');
+}
+
+sub check_auth {
+  $::auth->assert('vendor_invoice_edit | invoice_edit | ap_transactions | ar_transactions');
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::Draft
+
+=head1 DESCRIPTION
+
+Encapsulates the old draft mechanism. Use and improvement are discuraged as
+long as the storage is not upgrade safe.
+
+=head1 TODO
+
+  - optional popup on entry
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut
index 551214e..376dfb3 100644 (file)
@@ -27,9 +27,12 @@ __PACKAGE__->run_before('add_stylesheet');
 sub action_list {
   my ($self) = @_;
 
+  $::auth->assert('email_journal');
+
   if ( $::instance_conf->get_email_journal == 0 ) {
     flash('info',  $::locale->text('Storing the emails in the journal is currently disabled in the client configuration.'));
   }
+  $self->setup_list_action_bar;
   $self->render('email_journal/list',
                 title   => $::locale->text('Email journal'),
                 ENTRIES => $self->models->get,
@@ -39,6 +42,8 @@ sub action_list {
 sub action_show {
   my ($self) = @_;
 
+  $::auth->assert('email_journal');
+
   my $back_to = $::form->{back_to} || $self->url_for(action => 'list');
 
   $self->entry(SL::DB::EmailJournal->new(id => $::form->{id})->load);
@@ -47,6 +52,7 @@ sub action_show {
     $::form->error(t8('You do not have permission to access this entry.'));
   }
 
+  $self->setup_show_action_bar;
   $self->render('email_journal/show',
                 title   => $::locale->text('View sent email'),
                 back_to => $back_to);
@@ -55,13 +61,19 @@ sub action_show {
 sub action_download_attachment {
   my ($self) = @_;
 
+  $::auth->assert('email_journal');
+
   my $attachment = SL::DB::EmailJournalAttachment->new(id => $::form->{id})->load;
 
   if (!$self->can_view_all && ($attachment->email_journal->sender_id != SL::DB::Manager::Employee->current->id)) {
     $::form->error(t8('You do not have permission to access this entry.'));
   }
-
-  $self->send_file(\$attachment->content, name => $attachment->name, type => $attachment->mime_type);
+  my $ref = \$attachment->content;
+  if ( $attachment->file_id > 0 ) {
+    my $file = SL::File->get(id => $attachment->file_id );
+    $ref = $file->get_content if $file;
+  }
+  $self->send_file($ref, name => $attachment->name, type => $attachment->mime_type);
 }
 
 #
@@ -76,7 +88,7 @@ sub add_stylesheet {
 # helpers
 #
 
-sub init_can_view_all { $::auth->assert('admin', 1) }
+sub init_can_view_all { $::auth->assert('email_employee_readall', 1) }
 
 sub init_models {
   my ($self) = @_;
@@ -124,4 +136,31 @@ sub init_filter_summary {
   return join ', ', @filter_strings;
 }
 
+sub setup_list_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Filter'),
+        submit    => [ '#filter_form', { action => 'EmailJournal/list' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_show_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Back'),
+        call => [ 'kivi.history_back' ],
+      ],
+    );
+  }
+}
+
 1;
index 265d1db..431c8b7 100644 (file)
@@ -5,6 +5,7 @@ use parent qw(SL::Controller::Base);
 
 use SL::DB::Employee;
 use SL::Helper::Flash;
+use SL::Locale::String qw(t8);
 
 __PACKAGE__->run_before('check_auth');
 __PACKAGE__->run_before('load_all');
@@ -23,9 +24,10 @@ sub action_edit {
   my ($self, %params) = @_;
 
   if ($self->{employee}) {
+    $self->setup_edit_action_bar;
     $self->render('employee/edit', title => $::locale->text('Edit Employee #1', $self->{employee}->safe_name));
   } else {
-    flash('error', $::locale->text('Could not load employee'));
+    flash_later('error', $::locale->text('Could not load employee'));
     $self->redirect_to(action => 'list');
   }
 }
@@ -33,9 +35,26 @@ sub action_edit {
 sub action_save {
   my ($self, %params) = @_;
 
-  $self->{employee}->save;
+  SL::DB->client->with_transaction(sub {
+    1;
 
-  flash('info', $::locale->text('Employee #1 saved!'));
+    $self->{employee}->save;
+
+    if ($self->{employee}->deleted) {
+      my $auth_user = SL::DB::Manager::AuthUser->get_first(login => $self->{employee}->login);
+      if ($auth_user) {
+        SL::DB::Manager::AuthClientUser->delete_all(
+          where => [
+            client_id => $::auth->client->{id},
+            user_id   => $auth_user->id,
+          ]);
+      }
+    }
+
+    1;
+  });
+
+  flash('info', $::locale->text('Employee #1 saved!', $self->{employee}->safe_name));
 
   $self->redirect_to(action => 'edit', 'employee.id' => $self->{employee}->id);
 }
@@ -63,6 +82,26 @@ sub assign_from_form {
   return 1;
 }
 
+sub setup_edit_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => 'Employee/save' } ],
+        accesskey => 'enter',
+      ],
+
+      'separator',
+
+      link => [
+        t8('Abort'),
+        link => $self->url_for(action => 'list'),
+      ],
+    );
+  }
+}
 
 ######################## behaviour ##########################
 
diff --git a/SL/Controller/File.pm b/SL/Controller/File.pm
new file mode 100644 (file)
index 0000000..8dacec2
--- /dev/null
@@ -0,0 +1,892 @@
+package SL::Controller::File;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use List::Util qw(first max);
+
+use utf8;
+use Encode qw(decode);
+use English qw( -no_match_vars );
+use URI::Escape;
+use Cwd;
+use DateTime;
+use File::stat;
+use File::Slurp qw(slurp);
+use File::Spec::Unix;
+use File::Spec::Win32;
+use File::MimeInfo::Magic;
+use MIME::Base64;
+use SL::DB::Helper::Mappings;
+use SL::DB::Order;
+use SL::DB::DeliveryOrder;
+use SL::DB::Invoice;
+
+use SL::DB::PurchaseInvoice;
+use SL::DB::Part;
+use SL::DB::GLTransaction;
+use SL::DB::Draft;
+use SL::DB::History;
+use SL::JSON;
+use SL::Helper::CreatePDF qw(:all);
+use SL::Locale::String;
+use SL::SessionFile;
+use SL::SessionFile::Random;
+use SL::File;
+use SL::Controller::Helper::ThumbnailCreator qw(file_probe_image_type file_probe_type);
+
+use constant DO_DELETE   => 0;
+use constant DO_UNIMPORT => 1;
+
+use Rose::Object::MakeMethods::Generic
+(
+    'scalar --get_set_init' => [ qw() ],
+    'scalar' => [ qw(object object_type object_model object_id object_right file_type files is_global existing) ],
+);
+
+__PACKAGE__->run_before('check_object_params', only => [ qw(list ajax_delete ajax_importdialog ajax_import ajax_unimport ajax_upload ajax_files_uploaded) ]);
+
+# gen:    bitmask: bit 1 (value is 1, 3, 5 or 7) => file created
+#                  bit 2 (value is 2, 3, 6 or 7) => file from other source (e.g. directory for scanned documents)
+#                  bit 3 (value is 4, 5, 6 or 7) => upload as other source
+# gltype: is this used somewhere?
+# dir:    is this used somewhere?
+# model:  base name of the rose model
+# right:  access right used for import
+my %file_types = (
+  'sales_quotation'             => { gen => 1, gltype => '',   dir =>'SalesQuotation',       model => 'Order',          right => 'import_ar'  },
+  'sales_order'                 => { gen => 5, gltype => '',   dir =>'SalesOrder',           model => 'Order',          right => 'import_ar'  },
+  'sales_delivery_order'        => { gen => 1, gltype => '',   dir =>'SalesDeliveryOrder',   model => 'DeliveryOrder',  right => 'import_ar'  },
+  'invoice'                     => { gen => 1, gltype => 'ar', dir =>'SalesInvoice',         model => 'Invoice',        right => 'import_ar'  },
+  'invoice_for_advance_payment' => { gen => 1, gltype => 'ar', dir =>'SalesInvoice',         model => 'Invoice',        right => 'import_ar'  },
+  'final_invoice'               => { gen => 1, gltype => 'ar', dir =>'SalesInvoice',         model => 'Invoice',        right => 'import_ar'  },
+  'credit_note'                 => { gen => 1, gltype => '',   dir =>'CreditNote',           model => 'Invoice',        right => 'import_ar'  },
+  'request_quotation'           => { gen => 7, gltype => '',   dir =>'RequestForQuotation',  model => 'Order',          right => 'import_ap'  },
+  'purchase_order'              => { gen => 7, gltype => '',   dir =>'PurchaseOrder',        model => 'Order',          right => 'import_ap'  },
+  'purchase_delivery_order'     => { gen => 7, gltype => '',   dir =>'PurchaseDeliveryOrder',model => 'DeliveryOrder',  right => 'import_ap'  },
+  'purchase_invoice'            => { gen => 6, gltype => 'ap', dir =>'PurchaseInvoice',      model => 'PurchaseInvoice',right => 'import_ap'  },
+  'vendor'                      => { gen => 0, gltype => '',   dir =>'Vendor',               model => 'Vendor',         right => 'xx'         },
+  'customer'                    => { gen => 1, gltype => '',   dir =>'Customer',             model => 'Customer',       right => 'xx'         },
+  'project'                     => { gen => 0, gltype => '',   dir =>'Project',              model => 'Project',        right => 'xx'         },
+  'part'                        => { gen => 0, gltype => '',   dir =>'Part',                 model => 'Part',           right => 'xx'         },
+  'gl_transaction'              => { gen => 6, gltype => 'gl', dir =>'GeneralLedger',        model => 'GLTransaction',  right => 'import_ap'  },
+  'draft'                       => { gen => 0, gltype => '',   dir =>'Draft',                model => 'Draft',          right => 'xx'         },
+  'csv_customer'                => { gen => 1, gltype => '',   dir =>'Reports',              model => 'Customer',       right => 'xx'         },
+  'csv_vendor'                  => { gen => 1, gltype => '',   dir =>'Reports',              model => 'Vendor',         right => 'xx'         },
+  'shop_image'                  => { gen => 0, gltype => '',   dir =>'ShopImages',           model => 'Part',           right => 'xx'         },
+  'letter'                      => { gen => 7, gltype => '',   dir =>'Letter',               model => 'Letter',         right => 'sales_letter_edit | purchase_letter_edit' },
+);
+
+#--- 4 locale ---#
+# $main::locale->text('imported')
+
+#
+# actions
+#
+
+sub action_list {
+  my ($self) = @_;
+
+  my $is_json = 0;
+  $is_json = 1 if $::form->{json};
+
+  $self->_do_list($is_json);
+}
+
+sub action_ajax_importdialog {
+  my ($self) = @_;
+  $::auth->assert($self->object_right);
+  my $path   = $::form->{path};
+  my @files  = $self->_get_from_import($path);
+  my $source = {
+    'name'         => $::form->{source},
+    'path'         => $path ,
+    'chk_action'   => $::form->{source}.'_import',
+    'chk_title'    => $main::locale->text('Import scanned documents'),
+    'chkall_title' => $main::locale->text('Import all'),
+    'files'        => \@files
+  };
+  $self->render('file/import_dialog',
+                { layout => 0
+                },
+                source => $source
+  );
+}
+
+sub action_ajax_import {
+  my ($self) = @_;
+  $::auth->assert($self->object_right);
+  my $ids    = $::form->{ids};
+  my $source = $::form->{source};
+  my $path   = $::form->{path};
+  my @files  = $self->_get_from_import($path);
+  foreach my $filename (@{ $::form->{$ids} || [] }) {
+    my ($file, undef) = grep { $_->{name} eq $filename } @files;
+    if ( $file ) {
+      my $obj = SL::File->save(object_id   => $self->object_id,
+                               object_type => $self->object_type,
+                               mime_type   => 'application/pdf',
+                               source      => $source,
+                               file_type   => 'document',
+                               file_name   => $file->{filename},
+                               file_path   => $file->{path}
+                             );
+      unlink($file->{path}) if $obj;
+    }
+  }
+  $self->_do_list(1);
+}
+
+sub action_ajax_delete {
+  my ($self) = @_;
+  $self->_delete_all(DO_DELETE, $::locale->text('Following files are deleted:'));
+}
+
+sub action_ajax_unimport {
+  my ($self) = @_;
+  $self->_delete_all(DO_UNIMPORT, $::locale->text('Following files are unimported:'));
+}
+
+sub action_ajax_rename {
+  my ($self) = @_;
+  my ($id, $version) = split /_/, $::form->{id};
+  my $file = SL::File->get(id => $id);
+  if ( ! $file ) {
+    $self->js->flash('error', $::locale->text('File not exists !'))->render();
+    return;
+  }
+  my $sessionfile = $::form->{sessionfile};
+  if ( $sessionfile && -f $sessionfile ) {
+    # new uploaded file
+    if ( $::form->{to} eq $file->file_name ) {
+      # no rename so use as new version
+      $file->save_file($sessionfile);
+      $self->js->flash('warning', $::locale->text('File \'#1\' is used as new Version !', $file->file_name));
+
+    } else {
+      # new filename, so it is a new file with the same attributes as the old file
+      eval {
+        SL::File->save(object_id   => $file->object_id,
+                       object_type => $file->object_type,
+                       mime_type   => $file->mime_type,
+                       source      => $file->source,
+                       file_type   => $file->file_type,
+                       file_name   => $::form->{to},
+                       file_path   => $sessionfile
+                     );
+        unlink($sessionfile);
+        1;
+      } or do {
+        $self->js->flash(       'error', t8('internal error (see details)'))
+                 ->flash_detail('error', $@)->render;
+        return;
+      }
+    }
+
+  } else {
+    # normal rename
+    my $result;
+
+    eval {
+      $result = $file->rename($::form->{to});
+      1;
+    } or do {
+      $self->js->flash(       'error', t8('internal error (see details)'))
+               ->flash_detail('error', $@)->render;
+      return;
+    };
+
+    if ($result != SL::File::RENAME_OK) {
+      $self->js->flash('error',
+                         $result == SL::File::RENAME_EXISTS ? $::locale->text('File still exists !')
+                       : $result == SL::File::RENAME_SAME   ? $::locale->text('Same Filename !')
+                       :                                      $::locale->text('File not exists !'))
+        ->render;
+      return;
+    }
+  }
+  $self->is_global($::form->{is_global});
+  $self->file_type(  $file->file_type);
+  $self->object_type($file->object_type);
+  $self->object_id(  $file->object_id);
+  #$self->object_model($file_types{$file->module}->{model});
+  #$self->object_right($file_types{$file->module}->{right});
+  if ( $::form->{next_ids} ) {
+    my @existing = split(/,/, $::form->{next_ids});
+    $self->existing(\@existing);
+  }
+  $self->_do_list(1);
+}
+
+sub action_ajax_upload {
+  my ($self) = @_;
+  $self->{maxsize} = $::instance_conf->get_doc_max_filesize;
+  $self->{accept_types} = '';
+  $self->{accept_types} = 'image/png,image/gif,image/jpeg,image/tiff,*png,*gif,*.jpg,*.tif' if $self->{file_type} eq 'image';
+  $self->render('file/upload_dialog',
+                { layout => 0
+                },
+  );
+}
+
+sub action_ajax_files_uploaded {
+  my ($self) = @_;
+
+  my $source = 'uploaded';
+  my @existing;
+  if ( $::form->{ATTACHMENTS}->{uploadfiles} ) {
+    my @upfiles = @{ $::form->{ATTACHMENTS}->{uploadfiles} };
+    foreach my $idx (0 .. scalar(@upfiles) - 1) {
+      eval {
+        my $fname = uri_unescape($upfiles[$idx]->{filename});
+        # normalize and find basename
+        # first split with unix rules
+        # after that split with windows rules
+        my ($volume, $directories, $basefile) = File::Spec::Unix->splitpath($fname);
+        ($volume, $directories, $basefile) = File::Spec::Win32->splitpath($basefile);
+
+        # to find real mime_type by magic we must save the filedata
+
+        my $sess_fname = "file_upload_" . $self->object_type . "_" . $self->object_id . "_" . $idx;
+        my $sfile      = SL::SessionFile->new($sess_fname, mode => 'w');
+
+        $sfile->fh->print(${$upfiles[$idx]->{data}});
+        $sfile->fh->close;
+        my $mime_type = File::MimeInfo::Magic::magic($sfile->file_name);
+
+        if (! $mime_type) {
+          # if filename has the suffix "pdf", but isn't really a pdf, set mimetype for no suffix
+          $mime_type = File::MimeInfo::Magic::mimetype($basefile);
+          $mime_type = 'application/octet-stream' if $mime_type eq 'application/pdf' || !$mime_type;
+        }
+        if ( $self->file_type eq 'image' && $self->file_probe_image_type($mime_type, $basefile)) {
+          next;
+        }
+        my ($existobj) = SL::File->get_all(object_id   => $self->object_id,
+                                           object_type => $self->object_type,
+                                           mime_type   => $mime_type,
+                                           source      => $source,
+                                           file_type   => $self->file_type,
+                                           file_name   => $basefile,
+                                      );
+
+        if ($existobj) {
+          push @existing, $existobj->id.'_'.$sfile->file_name;
+        } else {
+          my $fileobj = SL::File->save(object_id        => $self->object_id,
+                                       object_type      => $self->object_type,
+                                       mime_type        => $mime_type,
+                                       source           => $source,
+                                       file_type        => $self->file_type,
+                                       file_name        => $basefile,
+                                       title            => $::form->{title},
+                                       description      => $::form->{description},
+                                       ## two possibilities: what is better ? content or sessionfile ??
+                                       file_contents    => ${$upfiles[$idx]->{data}},
+                                       file_path        => $sfile->file_name
+                                     );
+          unlink($sfile->file_name);
+        }
+        1;
+      } or do {
+        $self->js->flash(       'error', t8('internal error (see details)'))
+                 ->flash_detail('error', $@)->render;
+        return;
+      }
+    }
+  }
+  $self->existing(\@existing);
+  $self->_do_list(1);
+}
+
+sub action_download {
+  my ($self) = @_;
+
+  my $id      = $::form->{id};
+  my $version = $::form->{version};
+
+  my $file = SL::File->get(id => $id );
+  $file->version($version) if $version;
+  my $ref  = $file->get_content;
+  if ( $file && $ref ) {
+    return $self->send_file($ref,
+      type => $file->mime_type,
+      name => $file->file_name,
+    );
+  }
+}
+
+sub action_ajax_get_thumbnail {
+  my ($self) = @_;
+
+  my $id      = $::form->{file_id};
+  my $version = $::form->{file_version};
+  my $file    = SL::File->get(id => $id);
+
+  $file->version($version) if $version;
+
+  my $thumbnail = _create_thumbnail($file, $::form->{size});
+
+  my $overlay_selector  = '#enlarged_thumb_' . $id;
+  $overlay_selector    .= '_' . $version            if $version;
+  $self->js
+    ->attr($overlay_selector, 'src', 'data:' . $thumbnail->{thumbnail_img_content_type} . ';base64,' . MIME::Base64::encode_base64($thumbnail->{thumbnail_img_content}))
+    ->data($overlay_selector, 'is-overlay-loaded', '1')
+    ->render;
+}
+
+
+#
+# filters
+#
+
+sub check_object_params {
+  my ($self) = @_;
+
+  my $id      = ($::form->{object_id} // 0) * 1;
+  my $draftid = ($::form->{draft_id}  // 0) * 1;
+  my $gldoc   = 0;
+  my $type    = undef;
+
+  if ( $draftid == 0 && $id == 0 && $::form->{is_global} ) {
+    $gldoc = 1;
+    $type  = $::form->{object_type};
+  }
+  elsif ( $id == 0 ) {
+    $id   = $::form->{draft_id};
+    $type = 'draft';
+  } elsif ( $::form->{object_type} ) {
+    $type = $::form->{object_type};
+  }
+  die "No object type"      unless $type;
+  die "No file type"        unless $::form->{file_type};
+  die "Unknown object type" unless $file_types{$type};
+
+  $self->is_global($gldoc);
+  $self->file_type($::form->{file_type});
+  $self->object_type($type);
+  $self->object_id($id);
+  $self->object_model($file_types{$type}->{model});
+  $self->object_right($file_types{$type}->{right});
+
+ # $::auth->assert($self->object_right);
+
+ # my $model = 'SL::DB::' . $self->object_model;
+ # $self->object($model->new(id => $self->object_id)->load || die "Record not found");
+
+  return 1;
+}
+
+#
+# private methods
+#
+
+sub _delete_all {
+  my ($self, $do_unimport, $infotext) = @_;
+  my $files = '';
+  my $ids = $::form->{ids};
+  foreach my $id_version (@{ $::form->{$ids} || [] }) {
+    my ($id, $version) = split /_/, $id_version;
+    my $dbfile = SL::File->get(id => $id);
+    if ( $dbfile ) {
+      if ( $version ) {
+        $dbfile->version($version);
+        $files .= ' ' . $dbfile->file_name if $dbfile->delete_version;
+      } else {
+        $files .= ' ' . $dbfile->file_name if $dbfile->delete;
+      }
+    }
+  }
+  $self->js->flash('info', $infotext . $files) if $files;
+  $self->_do_list(1);
+}
+
+sub _do_list {
+  my ($self, $json) = @_;
+
+  my @files;
+  my @object_types = ($self->object_type);
+  if ( $self->file_type eq 'document' ) {
+    push @object_types, qw(dunning1 dunning2 dunning3 dunning_invoice dunning_orig_invoice) if $self->object_type eq 'invoice'; # hardcoded object types?
+  }
+  @files = SL::File->get_all_versions(object_id   => $self->object_id,
+                                      object_type => \@object_types,
+                                      file_type   => $self->file_type,
+                                     );
+
+  $self->files(\@files);
+
+  $_->{thumbnail}     = _create_thumbnail($_)                     for @files;
+  $_->{version_count} = SL::File->get_version_count(id => $_->id) for @files;
+
+  if($self->object_type eq 'shop_image'){
+    $self->js
+      ->run('kivi.ShopPart.show_images', $self->object_id)
+      ->render();
+  }else{
+    $self->_mk_render('file/list', 1, 0, $json);
+  }
+}
+
+sub _get_from_import {
+  my ($self, $path) = @_;
+  my @foundfiles ;
+
+  my $language = $::lx_office_conf{system}->{language};
+  my $timezone = $::locale->get_local_time_zone()->name;
+  if (opendir my $dir, $path) {
+    my @files = (readdir $dir);
+    foreach my $file ( @files) {
+      next if (($file eq '.') || ($file eq '..'));
+      $file = Encode::decode('utf-8', $file);
+
+      next if ( -d "$path/$file" );
+
+      my $tmppath = File::Spec->catfile( $path, $file );
+      next if( ! -f $tmppath );
+
+      my $st = stat($tmppath);
+      my $dt = DateTime->from_epoch( epoch => $st->mtime, time_zone => $timezone, locale => $language );
+      my $sname = $main::locale->quote_special_chars('HTML', $file);
+      push @foundfiles, {
+        'name'     => $file,
+        'filename' => $sname,
+        'path'     => $tmppath,
+        'mtime'    => $st->mtime,
+        'date'     => $dt->dmy('.') . " " . $dt->hms,
+      };
+
+    }
+    closedir($dir);
+
+  } else {
+    $::lxdebug->message(LXDebug::WARN(), "SL::File::_get_from_import opendir failed to open dir " . $path);
+  }
+
+  return @foundfiles;
+}
+
+sub _mk_render {
+  my ($self, $template, $edit, $scanner, $json) = @_;
+  my $err;
+  eval {
+    ##TODO make code configurable
+
+    my $title;
+    my @sources = $self->_get_sources();
+    foreach my $source ( @sources ) {
+      @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files };
+    }
+    if ( $self->file_type eq 'document' ) {
+      $title = $main::locale->text('Documents');
+    } elsif ( $self->file_type eq 'attachment' ) {
+      $title = $main::locale->text('Attachments');
+    } elsif ( $self->file_type eq 'image' ) {
+      $title = $main::locale->text('Images');
+    }
+
+    my $output         = SL::Presenter->get->render(
+      $template,
+      title            => $title,
+      SOURCES          => \@sources,
+      edit_attachments => $edit,
+      object_type      => $self->object_type,
+      object_id        => $self->object_id,
+      file_type        => $self->file_type,
+      is_global        => $self->is_global,
+      json             => $json,
+    );
+    if ( $json ) {
+      $self->js->html('#'.$self->file_type.'_list_'.$self->object_type, $output);
+      if ( $self->existing && scalar(@{$self->existing}) > 0) {
+        my $first = shift @{$self->existing};
+        my ($first_id, $sfile) = split('_', $first, 2);
+        my $file = SL::File->get(id => $first_id );
+        $self->js->run('kivi.File.askForRename', $first_id, $file->file_type, $file->file_name, $sfile, join (',', @{$self->existing}), $self->is_global);
+      }
+      $self->js->render();
+    } else {
+        $self->render(\$output, { layout => 0, process => 0 });
+    }
+    1;
+  } or do {
+    if ($json ){
+      $self->js->flash(       'error', t8('internal error (see details)'))
+               ->flash_detail('error', $@)->render;
+    } else {
+      $self->render('generic/error', { layout => 0 }, label_error => $@);
+    }
+  };
+}
+
+
+sub _get_sources {
+  my ($self) = @_;
+  my @sources;
+  if ( $self->file_type eq 'document' ) {
+    # TODO statt gen neue attribute in filetypes :
+    if (($file_types{$self->object_type}->{gen}*1 & 4)==4) {
+      # bit 3 is set => means upload
+      my $source = {
+        'name'         => 'uploaded',
+        'title'        => $main::locale->text('uploaded Documents'),
+        'chk_action'   => 'uploaded_documents_delete',
+        'chk_title'    => $main::locale->text('Delete Documents'),
+        'chkall_title' => $main::locale->text('Delete all'),
+        'file_title'   => $main::locale->text('filename'),
+        'confirm_text' => $main::locale->text('delete'),
+        'can_rename'   => 1,
+        'are_existing' => $self->existing ? 1 : 0,
+        'rename_title' => $main::locale->text('Rename Attachments'),
+        'can_upload'   => 1,
+        'can_delete'   => 1,
+        'upload_title' => $main::locale->text('Upload Documents'),
+        'done_text'    => $main::locale->text('deleted')
+      };
+      push @sources , $source;
+    }
+
+    if (($file_types{$self->object_type}->{gen}*1 & 1)==1) {
+      my $gendata = {
+        'name'         => 'created',
+        'title'        => $main::locale->text('generated Files'),
+        'chk_action'   => 'documents_delete',
+        'chk_title'    => $main::locale->text('Delete Documents'),
+        'chkall_title' => $main::locale->text('Delete all'),
+        'file_title'   => $main::locale->text('filename'),
+        'confirm_text' => $main::locale->text('delete'),
+        'can_delete'   => $::instance_conf->get_doc_delete_printfiles,
+        'can_rename'   => $::instance_conf->get_doc_delete_printfiles,
+        'rename_title' => $main::locale->text('Rename Documents'),
+        'done_text'    => $main::locale->text('deleted')
+      };
+      push @sources , $gendata;
+    }
+
+    if (($file_types{$self->object_type}->{gen}*1 & 2)==2) {
+      my @others =  SL::File->get_other_sources();
+      foreach my $scanner_or_mailrx (@others) {
+        my $other = {
+          'name'         => $scanner_or_mailrx->{name},
+          'title'        => $main::locale->text('from \'#1\' imported Files', $scanner_or_mailrx->{description}),
+          'chk_action'   => $scanner_or_mailrx->{name}.'_unimport',
+          'chk_title'    => $main::locale->text('Unimport documents'),
+          'chkall_title' => $main::locale->text('Unimport all'),
+          'file_title'   => $main::locale->text('filename'),
+          'confirm_text' => $main::locale->text('unimport'),
+          'can_rename'   => 1,
+          'rename_title' => $main::locale->text('Rename Documents'),
+          'can_import'   => 1,
+          'can_delete'   => 0,
+          'import_title' => $main::locale->text('Add Document from \'#1\'', $scanner_or_mailrx->{name}),
+          'path'         => $scanner_or_mailrx->{directory},
+          'done_text'    => $main::locale->text('unimported')
+        };
+        push @sources , $other;
+      }
+    }
+  }
+  elsif ( $self->file_type eq 'attachment' ) {
+    my $attdata = {
+      'name'         => 'uploaded',
+      'title'        => $main::locale->text(''),
+      'chk_action'   => 'attachments_delete',
+      'chk_title'    => $main::locale->text('Delete Attachments'),
+      'chkall_title' => $main::locale->text('Delete all'),
+      'file_title'   => $main::locale->text('filename'),
+      'confirm_text' => $main::locale->text('delete'),
+      'can_rename'   => 1,
+      'are_existing' => $self->existing ? 1 : 0,
+      'rename_title' => $main::locale->text('Rename Attachments'),
+      'can_upload'   => 1,
+      'can_delete'   => 1,
+      'upload_title' => $main::locale->text('Upload Attachments'),
+      'done_text'    => $main::locale->text('deleted')
+    };
+    push @sources , $attdata;
+  }
+  elsif ( $self->file_type eq 'image' ) {
+    my $attdata = {
+      'name'         => 'uploaded',
+      'title'        => $main::locale->text(''),
+      'chk_action'   => 'images_delete',
+      'chk_title'    => $main::locale->text('Delete Images'),
+      'chkall_title' => $main::locale->text('Delete all'),
+      'file_title'   => $main::locale->text('filename'),
+      'confirm_text' => $main::locale->text('delete'),
+      'can_rename'   => 1,
+      'are_existing' => $self->existing ? 1 : 0,
+      'rename_title' => $main::locale->text('Rename Images'),
+      'can_upload'   => 1,
+      'can_delete'   => 1,
+      'upload_title' => $main::locale->text('Upload Images'),
+      'done_text'    => $main::locale->text('deleted')
+    };
+    push @sources , $attdata;
+  }
+  return @sources;
+}
+
+# ignores all errros
+# todo: cache thumbs?
+sub _create_thumbnail {
+  my ($file, $size) = @_;
+
+  $size //= 64;
+
+  my $filename;
+  if (!eval { $filename = $file->get_file(); 1; }) {
+    $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail get_file failed: " . $EVAL_ERROR);
+    return;
+  }
+
+  # Workaround for pfds which are not handled by file_probe_type.
+  # Maybe use mime info stored in db?
+  my $mime_type = File::MimeInfo::Magic::magic($filename);
+  if ($mime_type =~ m{pdf}) {
+    $filename = _convert_pdf_to_png($filename, size => $size);
+  }
+  return if !$filename;
+
+  my $content;
+  if (!eval { $content = slurp $filename; 1; }) {
+    $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail slurp failed: " . $EVAL_ERROR);
+    return;
+  }
+
+  my $ret;
+  if (!eval { $ret = file_probe_type($content, size => $size); 1; }) {
+    $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail file_probe_type failed: " . $EVAL_ERROR);
+    return;
+  }
+
+  # file_probe_type returns a hash ref with thumbnail info and content
+  # or an error message
+  if ('HASH' ne ref $ret) {
+    $::lxdebug->message(LXDebug::WARN(), "SL::File::_create_thumbnail file_probe_type returned an error: " . $ret);
+    return;
+  }
+
+  return $ret;
+}
+
+sub _convert_pdf_to_png {
+  my ($filename, %params) = @_;
+
+  my $size    = $params{size} // 64;
+  my $sfile   = SL::SessionFile::Random->new();
+  unless (-f $filename) {
+    $::lxdebug->message(LXDebug::WARN(), "_convert_pdf_to_png failed, no file found: $filename");
+    return;
+  }
+  # quotemeta for storno case "storno\ zu\ 1020" *nix only
+  my $command = 'pdftoppm -singlefile -scale-to ' . $size . ' -png' . ' ' . quotemeta($filename) . ' ' . $sfile->file_name;
+
+  if (system($command) == -1) {
+    $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: system call failed: " . $ERRNO);
+    return;
+  }
+  if ($CHILD_ERROR) {
+    $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: pdftoppm failed with error code: " . ($CHILD_ERROR >> 8));
+    $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: File: $filename");
+    return;
+  }
+
+  return $sfile->file_name . '.png';
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::File - Controller for managing files
+
+=head1 SYNOPSIS
+
+The Controller is called directly from the webpages
+
+    <a href="controller.pl?action=File/list&file_type=document\
+       &object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">
+
+
+or indirectly via javascript functions from js/kivi.File.js
+
+    kivi.popup_dialog({ url:     'controller.pl',
+                        data:    { action     : 'File/ajax_upload',
+                                   file_type  : 'uploaded',
+                                   object_type: type,
+                                   object_id  : id
+                                 }
+                           ...
+
+=head1 DESCRIPTION
+
+This is a controller for handling files in a storage independent way.
+The storage may be a Filesystem,a WebDAV, a Database or DMS.
+These backends must be configered in ClientConfig.
+This Controller use as intermediate layer for storage C<SL::File>.
+
+The Controller is responsible to display forms for displaying the files at the ERP-objects and
+for uploading and downloading the files.
+
+More description of the intermediate layer see L<SL::File>.
+
+=head1 METHODS
+
+=head2 C<action_list>
+
+This loads a list of files on a webpage. This can be done with a normal submit or via an ajax/json call.
+Dependent of file_type different sources are available.
+
+For documents there are the 'created' source and the imports from scanners or email.
+For attachments and images only the 'uploaded' source available.
+
+Available C<FORM PARAMS>:
+
+=over 4
+
+=item C<form.object_id>
+
+The Id of the ERP-object.
+
+=item C<form.object_type>
+
+The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
+
+=item C<form.file_type>
+
+For one ERP-object may exists different type of documents the type may be "documents","attachments" or "images".
+This file_type is a filter for the list.
+
+=item C<form.json>
+
+The method can be used as normal HTTP-Request (json=0) or as AJAX-JSON call to refresh the list if the parameter is set to 1.
+
+=back
+
+
+=head2 C<action_ajax_upload>
+
+
+A new file or more files can selected by a dialog and insert into the system.
+
+
+Available C<FORM PARAMS>:
+
+=over 4
+
+=item C<form.file_type>
+
+This parameter describe here the source for a new file :
+"attachments" and "images"
+
+This is a normal upload selection, which may be more then one file to upload.
+
+=item C<form.object_id>
+
+and
+
+=item C<form.object_type>
+
+are the same as at C<action_list>
+
+=back
+
+=head2  C<action_ajax_files_uploaded>
+
+The Upload of selected Files. The "multipart_formdata" is parsed in SL::Request into the formsvariable "form.ATTACHMENTS".
+The filepaths are checked about Unix and Windows paths. Also the MIME type of the files are verified ( IS the contents of a *.pdf real PDF?).
+If the same filename still exists at this object after the download for each existing filename a rename dialog will be opened.
+
+If the filename is not changed the new uploaded file is a new version of the file, if the name is changed it is a new file.
+
+Available C<FORM PARAMS>:
+
+=over 4
+
+=item C<form.ATTACHMENTS.uploadfiles>
+
+This is an array of elements which have {filename} for the name and {data} for the contents.
+
+Also object_id, object_type and file_type
+
+=back
+
+=head2 C<action_download>
+
+This is the real download of a file normally called via javascript "$.download("controller.pl", data);"
+
+Available C<FORM PARAMS>:
+
+=over 4
+
+Also object_id, object_type and file_type
+
+=back
+
+=head2 C<action_ajax_importdialog>
+
+A Dialog with all available and not imported files to import is open.
+More then one file can be selected.
+
+Available C<FORM PARAMS>:
+
+=over 4
+
+=item C<form.source>
+
+The name of the source like "scanner1" or "email"
+
+=item C<form.path>
+
+The full path to the directory on the server, where the files to import can found
+
+Also object_id, object_type and file_type
+
+=back
+
+=head2 C<action_ajax_delete>
+
+Some files can be deleted
+
+Available C<FORM PARAMS>:
+
+=over 4
+
+=item C<form.ids>
+
+The ids of the files to delete. Only this files are deleted not all versions of a file if the exists
+
+=back
+
+=head2 C<action_ajax_unimport>
+
+Some files can be unimported, dependent of the source of the file. This means they are moved
+back to the directory of the source
+
+Available C<FORM PARAMS>:
+
+=over 4
+
+=item C<form.ids>
+
+The ids of the files to unimport. Only these files are unimported not all versions of a file if the exists
+
+=back
+
+=head2 C<action_ajax_rename>
+
+One file can be renamed. There can be some checks if the same filename still exists at one object.
+
+=head1 AUTHOR
+
+Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
+
+=cut
index 125a8a8..f4f09ff 100644 (file)
@@ -37,6 +37,7 @@ sub action_list {
 
   $self->calculate_data;
 
+  $self->setup_list_action_bar;
   $self->list_objects;
 }
 
@@ -243,6 +244,7 @@ sub init_models {
     query => [
       SL::DB::Manager::Order->type_filter('sales_order'),
       '!closed' => 1,
+      (salesman_id => SL::DB::Manager::Employee->current->id) x !$::auth->assert('sales_all_edit', 1),
       or        => [
         globalproject_id => undef,
         and              => [
@@ -276,7 +278,11 @@ sub link_to {
     my $type = $object->type;
     my $id   = $object->id;
 
-    return "oe.pl?action=$action&type=$type&vc=customer&id=$id";
+    if ($::instance_conf->get_feature_experimental_order) {
+      return "controller.pl?action=Order/$action&type=$type&id=$id";
+    } else {
+      return "oe.pl?action=$action&type=$type&vc=customer&id=$id";
+    }
   }
   if ($object->isa('SL::DB::Customer')) {
     my $id     = $object->id;
@@ -288,4 +294,18 @@ sub link_to {
   }
 }
 
+sub setup_list_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#filter_form', { action => 'FinancialControllingReport/list' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
 1;
index 19ae441..c0c7d18 100644 (file)
@@ -11,12 +11,13 @@ use SL::DB::Invoice;
 use SL::DB::Order;
 use SL::DB::PeriodicInvoicesConfig;
 use SL::DB::PurchaseInvoice;
+use SL::DBUtils;
 use SL::Controller::Helper::ReportGenerator;
 use SL::Locale::String;
 
 use Rose::Object::MakeMethods::Generic (
   scalar                  => [ qw(report number_columns year current_year objects subtotals_per_quarter salesman_id) ],
-  'scalar --get_set_init' => [ qw(employees types data) ],
+  'scalar --get_set_init' => [ qw(employees types data show_costs) ],
 );
 
 __PACKAGE__->run_before(sub { $::auth->assert('report'); });
@@ -29,6 +30,7 @@ sub action_list {
   $self->get_objects;
   $self->calculate_one_time_data;
   $self->calculate_periodic_invoices;
+  $self->calculate_costs if $self->show_costs;
   $self->prepare_report;
   $self->list_data;
 }
@@ -55,6 +57,7 @@ sub prepare_report {
     requests_for_quotation => { text => t8('Requests for Quotation') },
     purchase_orders        => { text => t8('Purchase Orders')        },
     purchase_invoices      => { text => t8('Purchase Invoices')      },
+    costs                  => { text => t8('Costs')                  },
   );
 
   $column_defs{$_}->{align} = 'right' for @columns;
@@ -100,7 +103,15 @@ sub get_objects {
   $self->objects->{sales_orders} = [ grep { !$_->periodic_invoices_config || !$_->periodic_invoices_config->active } @{ $self->objects->{sales_orders} } ];
 }
 
-sub init_types { [ qw(sales_quotations sales_orders sales_orders_per_inv sales_invoices requests_for_quotation purchase_orders purchase_invoices) ] }
+sub init_show_costs { $::instance_conf->get_profit_determination eq 'balance' }
+
+sub init_types {
+  my ($self) = @_;
+  my @types  = qw(sales_quotations sales_orders sales_orders_per_inv sales_invoices requests_for_quotation purchase_orders purchase_invoices);
+  push @types, 'costs' if $self->show_costs;
+
+  return \@types;
+}
 
 sub init_data {
   my ($self) = @_;
@@ -126,7 +137,7 @@ sub calculate_one_time_data {
 
   foreach my $type (@{ $self->types }) {
     my $src_object_type = $type eq 'sales_orders_per_inv' ? 'sales_orders' : $type;
-    foreach my $object (@{ $self->objects->{ $src_object_type } }) {
+    foreach my $object (@{ $self->objects->{ $src_object_type } || [] }) {
       my $month                              = $object->transdate->month - 1;
       my $tdata                              = $self->data->{$type};
 
@@ -170,6 +181,48 @@ sub calculate_one_periodic_invoice {
   $sord->{year}                             += $net;
 }
 
+sub calculate_costs {
+  my ($self) = @_;
+
+  # Relevante BWA-Positionen für Kosten:
+  #  4 – Mat./Wareneinkauf
+  # 10 – Personalkosten
+  # 11 – Raumkosten
+  # 12 – Betriebl.Steuern
+  # 13 – Versicherungsbeiträge
+  # 14 – KFZ-Kosten ohne Steuern
+  # 15 – Werbe-/Reisekosten
+  # 16 – Kosten Warenabgabe
+  # 17 – Abschreibungen
+  # 18 – Reparatur/Instandhaltung
+  # 20 – Sonstige Kosten
+  my $query = <<SQL;
+    SELECT SUM(ac.amount * chart_category_to_sgn(c.category)) AS amount,
+      EXTRACT(month FROM ac.transdate) AS month
+    FROM acc_trans ac
+    LEFT JOIN chart c ON (c.id = ac.chart_id)
+    WHERE (c.pos_bwa IN (4, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20))
+      AND (ac.transdate >= ?)
+      AND (ac.transdate <  ?)
+    GROUP BY month
+SQL
+
+  my @args = (
+    DateTime->new_local(day => 1, month => 1, year => $self->year)->to_kivitendo,
+    DateTime->new_local(day => 1, month => 1, year => $self->year + 1)->to_kivitendo,
+  );
+
+  my @results = selectall_hashref_query($::form, SL::DB::AccTransaction->new->db->dbh, $query, @args);
+  foreach my $row (@results) {
+    my $month                              = $row->{month} - 1;
+    my $tdata                              = $self->data->{costs};
+
+    $tdata->{months}->[$month]            += $row->{amount};
+    $tdata->{quarters}->[int($month / 3)] += $row->{amount};
+    $tdata->{year}                        += $row->{amount};
+  }
+}
+
 sub list_data {
   my ($self)           = @_;
 
diff --git a/SL/Controller/GL.pm b/SL/Controller/GL.pm
deleted file mode 100644 (file)
index e79f304..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-package SL::Controller::GL;
-
-use strict;
-use parent qw(SL::Controller::Base);
-
-use SL::DB::GLTransaction;
-use SL::DB::Invoice;
-use SL::DB::PurchaseInvoice;
-use SL::DB::AccTransaction;
-use SL::Locale::String qw(t8);
-use List::Util qw(sum);
-
-__PACKAGE__->run_before('check_auth');
-
-sub action_quicksearch {
-
-  my ($self, %params) = @_;
-
-  my $limit = $::form->{limit} || 40; # max number of results per type (AR/AP/GL)
-  my $term  = $::form->{term}  || '';
-
-  my $descriptionquery = { ilike => '%' . $term . '%' };
-  my $referencequery   = { ilike => '%' . $term . '%' };
-  my $apinvnumberquery = { ilike => '%' . $term . '%' };
-  my $namequery        = { ilike => '%' . $term . '%' };
-  my $arinvnumberquery = { ilike => '%' . $term       };
-  # ar match is more restrictive. Left fuzzy beginning so it also matches "Storno zu $INVNUMBER"
-  # and numbers like 000123 if you only enter 123.
-  # When used in quicksearch short numbers like 1 or 11 won't match because of the
-  # ajax autocomplete minlimit of 3 characters
-
-  my (@glfilter, @arfilter, @apfilter);
-
-  push( @glfilter, (or => [ description => $descriptionquery, reference => $referencequery ] ) );
-  push( @arfilter, (or => [ invnumber   => $arinvnumberquery, name      => $namequery ] ) );
-  push( @apfilter, (or => [ invnumber   => $apinvnumberquery, name      => $namequery ] ) );
-
-  my $gls = SL::DB::Manager::GLTransaction->get_all(  query => [ @glfilter ], limit => $limit, sort_by => 'transdate DESC');
-  my $ars = SL::DB::Manager::Invoice->get_all(        query => [ @arfilter ], limit => $limit, sort_by => 'transdate DESC', with_objects => [ 'customer' ]);
-  my $aps = SL::DB::Manager::PurchaseInvoice->get_all(query => [ @apfilter ], limit => $limit, sort_by => 'transdate DESC', with_objects => [ 'vendor' ]);
-
-  # use the sum of all credit amounts as the "amount" of the gl transaction
-  foreach my $gl ( @$gls ) {
-    $gl->{'amount'} = sum map { $_->amount if $_->amount > 0 } @{$gl->transactions};
-  };
-
-  my $gldata = [
-    map(
-      {
-        {
-           transdate => DateTime->from_object(object => $_->transdate)->ymd(),
-           label     => $_->abbreviation. ": " . $_->description . " " . $_->reference . " " . $::form->format_amount(\%::myconfig, $_->{'amount'},2). " (" . $_->transdate->to_lxoffice . ")" ,
-           value     => '',
-           url       => 'gl.pl?action=edit&id=' . $_->id,
-        }
-      }
-      @{$gls}
-    ),
-  ];
-
-  my $ardata = [
-    map(
-      {
-        {
-           transdate => DateTime->from_object(object => $_->transdate)->ymd(),
-           label     => $_->abbreviation . ": " . $_->invnumber . "   " . $_->customer->name . " " . $::form->format_amount(\%::myconfig, $_->amount,2)  . " (" . $_->transdate->to_lxoffice . ")" ,
-           value     => "",
-           url       => ($_->invoice ? "is" : "ar" ) . '.pl?action=edit&id=' . $_->id,
-        }
-      }
-      @{$ars}
-    ),
-  ];
-
-  my $apdata = [
-    map(
-      {
-        {
-           transdate => DateTime->from_object(object => $_->transdate)->ymd(),
-           label     => $_->abbreviation . ": " . $_->invnumber . " " . $_->vendor->name . " " . $::form->format_amount(\%::myconfig, $_->amount,2)  . " (" . $_->transdate->to_lxoffice . ")" ,
-           value     => "",
-           url       => ($_->invoice ? "ir" : "ap" ) . '.pl?action=edit&id=' . $_->id,
-        }
-      }
-      @{$aps}
-    ),
-  ];
-
-  my $data;
-  push(@{$data},@{$gldata});
-  push(@{$data},@{$ardata});
-  push(@{$data},@{$apdata});
-
-  @$data = reverse sort { $a->{'transdate'} cmp $b->{'transdate'} } @$data;
-
-  $self->render(\SL::JSON::to_json($data), { layout => 0, type => 'json' });
-}
-
-sub check_auth {
-  $::auth->assert('general_ledger');
-}
-
-1;
diff --git a/SL/Controller/GoBD.pm b/SL/Controller/GoBD.pm
new file mode 100644 (file)
index 0000000..182d8b2
--- /dev/null
@@ -0,0 +1,120 @@
+package SL::Controller::GoBD;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use DateTime;
+use SL::GoBD;
+use SL::Locale::String qw(t8);
+use SL::Helper::Flash;
+
+use SL::DB::AccTransaction;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(from to) ],
+);
+
+__PACKAGE__->run_before('check_auth');
+
+sub action_filter {
+  my ($self) = @_;
+
+  $self->from(DateTime->today->add(years => -1)->add(days => 1)) if !$self->from;
+  $self->to(DateTime->today)                                     if !$self->to;
+
+  $::request->layout->add_javascripts('kivi.GoBD.js');
+  $self->setup_filter_action_bar;
+  $self->render('gobd/filter', current_year => DateTime->today->year, title => t8('GoBD Export'));
+}
+
+sub action_export {
+  my ($self) = @_;
+
+  if (!$self->check_inputs) {
+    $self->action_filter;
+    return;
+  }
+
+  my $filename;
+  my $gobd = SL::GoBD->new(
+    company    => $::instance_conf->get_company,
+    location   => $::instance_conf->get_address,
+    from       => $self->from,
+    to         => $self->to,
+  );
+
+  eval {
+    $filename = $gobd->generate_export;
+  } or do {
+    my $errors = $@;
+    flash('error', t8('The export failed because of malformed transactions. Please fix those before exporting.'));
+    flash('error', $_) for @$errors;
+
+    $self->action_filter;
+    return;
+  };
+
+  $self->send_file($filename, name => t8('gobd-#1-#2.zip', $self->from->ymd, $self->to->ymd), unlink => 1);
+}
+
+#--- other stuff
+
+sub check_auth { $::auth->assert('report') }
+
+sub check_inputs {
+  my ($self) = @_;
+
+  my $error = 0;
+
+  if ($::form->{method} eq 'year') {
+    if ($::form->{year}) {
+      $self->from(DateTime->new(year => $::form->{year}, month => 1,  day => 1));
+      $self->to(  DateTime->new(year => $::form->{year}, month => 12, day => 31));
+    } else {
+      $error = 1;
+      flash('error', t8('No year given for method year'));
+    }
+  } else {
+    if (!$::form->{from}) {
+      my $epoch = DateTime->new(day => 1, month => 1, year => 1900);
+      flash('info', t8('No start date given, setting to #1', $epoch->to_kivitendo));
+      $self->from($epoch);
+    }
+
+    if (!$::form->{to}) {
+      flash('info', t8('No end date given, setting to today'));
+      $self->to(DateTime->today);
+    }
+  }
+
+  !$error;
+}
+
+sub available_years {
+  my ($self) = @_;
+
+  my $first_trans = SL::DB::Manager::AccTransaction->get_first(sort_by => 'transdate', limit => 1);
+
+  return [] unless $first_trans;
+  return [ reverse $first_trans->transdate->year .. DateTime->today->year ];
+}
+
+sub init_from { DateTime->from_kivitendo($::form->{from}) }
+sub init_to { DateTime->from_kivitendo($::form->{to}) }
+
+sub setup_filter_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Export'),
+        submit    => [ '#filter_form', { action => 'GoBD/export' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+1;
index d900c18..86b248c 100644 (file)
@@ -57,8 +57,6 @@ sub finalize {
     # try to use Filtered if available and nothing else is configured, but don't
     # blow up if the controller does not use Filtered
     my %paginate_args     = ref($self->paginate_args) eq 'CODE'       ? %{ $self->paginate_args->($self) }
-                          :     $self->paginate_args  eq '__FILTER__'
-                             && $self->get_models->filtered ? $self->get_models->filtered->read_params
                           :     $self->paginate_args  ne '__FILTER__' ? do { my $sub = $self->paginate_args; %{ $self->get_models->controller->$sub() } }
                           :                                               ();
 
index 2ff9798..0f73141 100644 (file)
@@ -8,6 +8,7 @@ our @EXPORT = qw(parse_filter);
 use DateTime;
 use SL::Helper::DateTime;
 use List::MoreUtils qw(uniq);
+use SL::Util qw(trim);
 use SL::MoreCommon qw(listify);
 use Data::Dumper;
 use Text::ParseWords;
@@ -24,9 +25,10 @@ my %filters = (
   date    => sub { DateTime->from_lxoffice($_[0]) },
   number  => sub { $::form->parse_amount(\%::myconfig, $_[0]) },
   percent => sub { $::form->parse_amount(\%::myconfig, $_[0]) / 100 },
-  head    => sub { $_[0] . '%' },
-  tail    => sub { '%' . $_[0] },
-  substr  => sub { '%' . $_[0] . '%' },
+  head    => sub { trim($_[0]) . '%' },
+  tail    => sub { '%' . trim($_[0]) },
+  substr  => sub { '%' . trim($_[0]) . '%' },
+  trim    => sub { trim($_[0]) },
 );
 
 my %methods = (
@@ -123,7 +125,8 @@ sub _parse_filter {
     my ($type, $op)   = $key =~ m{:(.+)::(.+)};
 
     my $is_multi      = $key =~ s/:multi//;
-    my @value_tokens  = $is_multi ? parse_line('\s+', 0, $value) : ($value);
+    my $is_any        = $key =~ s/:any//;
+    my @value_tokens  = $is_multi || $is_any ? parse_line('\s+', 0, $value) : ($value);
 
     ($key, $method)   = split m{::}, $key, 2;
     ($key, @filters)  = split m{:},  $key;
@@ -144,7 +147,7 @@ sub _parse_filter {
 
     next unless defined $key;
 
-    push @result, $is_multi ? (and => [ @args ]) : @args;
+    push @result, $is_multi ? (and => [ @args ]) : $is_any ? (or => [ @args ]) : @args;
   }
   return \@result;
 }
@@ -270,6 +273,10 @@ sub _apply_complex {
 
 __END__
 
+=pod
+
+=encoding utf8
+
 =head1 NAME
 
 SL::Controller::Helper::ParseFilter - Convert a form filter spec into a RDBO get_all filter
@@ -277,10 +284,10 @@ SL::Controller::Helper::ParseFilter - Convert a form filter spec into a RDBO get
 =head1 SYNOPSIS
 
   use SL::Controller::Helper::ParseFilter;
-  SL::DB::Object->get_all(parse_filter($::form->{filter}));
+  SL::DB::Manager::Object->get_all(parse_filter($::form->{filter}));
 
   # or more complex
-  SL::DB::Object->get_all(parse_filter($::form->{filter},
+  SL::DB::Manager::Object->get_all(parse_filter($::form->{filter},
     with_objects => [ qw(part customer) ]));
 
 =head1 DESCRIPTION
@@ -291,8 +298,8 @@ customer. L<Rose::DB::Object> allows you to search for these by filtering them p
 
   query => [
     'customer.name'          => 'John Doe',
-    'department.description' => [ ilike => '%Sales%' ],
-    'orddate'                => [ lt    => DateTime->today ],
+    'department.description' => { ilike => '%Sales%' },
+    'orddate'                => { lt    => DateTime->today },
   ]
 
 Unfortunately, if you specify them in your form as these strings, the form
@@ -432,22 +439,22 @@ Pasres the input string with C<< Form->parse_amount >>
 
 Parses the input string with C<< Form->parse_amount / 100 >>
 
+=item trim
+
+Removes whitespace characters (to be precice, characters with the \p{WSpace}
+property from beginning and end of the value.
+
 =item head
 
-Adds "%" at the end of the string.
+Adds "%" at the end of the string and applies C<trim>.
 
 =item tail
 
-Adds "%" at the end of the string.
+Adds "%" at the end of the string and applies C<trim>.
 
 =item substr
 
-Adds "% .. %" around the search string.
-
-=item eq_ignore_empty
-
-Ignores this item if it's empty. Otherwise compares it with the
-standard SQL C<=> operator.
+Adds "% .. %" around the search string and applies C<trim>.
 
 =back
 
@@ -465,13 +472,18 @@ standard SQL C<=> operator.
 
 All these are recognized like the L<Rose::DB::Object> methods.
 
-=item lazu_bool_eq
+=item lazy_bool_eq
 
 If the value is undefined or an empty string then this parameter will
 be completely removed from the query. Otherwise a falsish filter value
 will match for C<NULL> and C<FALSE>; trueish values will only match
 C<TRUE>.
 
+=item eq_ignore_empty
+
+Ignores this item if it's empty. Otherwise compares it with the
+standard SQL C<=> operator.
+
 =back
 
 =head1 BUGS AND CAVEATS
index 5faa541..be9b830 100644 (file)
@@ -4,8 +4,8 @@ use strict;
 
 use Carp;
 use List::Util qw(max);
+use Scalar::Util qw(blessed);
 
-use SL::Form;
 use SL::Common;
 use SL::MoreCommon;
 use SL::ReportGenerator;
@@ -17,8 +17,31 @@ our @EXPORT = qw(
   report_generator_list_objects
 );
 
+sub _setup_action_bar {
+  my ($self, $type) = @_;
+
+  my $key   = $::form->{CONTROLLER_DISPATCH} ? 'action'                             : 'report_generator_form.report_generator_dispatch_to';
+  my $value = $::form->{CONTROLLER_DISPATCH} ? $::form->{CONTROLLER_DISPATCH} . "/" : '';
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        $type eq 'pdf' ? $::locale->text('PDF export') : $::locale->text('CSV export'),
+        submit => [ '#report_generator_form', { $key => "${value}report_generator_export_as_${type}" } ],
+      ],
+      action => [
+        $::locale->text('Back'),
+        submit => [ '#report_generator_form', { $key => "${value}report_generator_back" } ],
+      ],
+    );
+  }
+}
+
 sub action_report_generator_export_as_pdf {
   my ($self) = @_;
+
+  delete $::form->{action_report_generator_export_as_pdf};
+
   if ($::form->{report_generator_pdf_options_set}) {
     my $saved_form = save_form();
 
@@ -40,6 +63,9 @@ sub action_report_generator_export_as_pdf {
 
   $::form->{copies} = max $::myconfig{copies} * 1, 1;
   $::form->{title} = $::locale->text('PDF export -- options');
+
+  _setup_action_bar($self, 'pdf'); # Sub not exported, therefore don't call via object.
+
   $::form->header;
   print $::form->parse_html_template('report_generator/pdf_export_options', {
     'HIDDEN'               => \@form_values,
@@ -48,6 +74,9 @@ sub action_report_generator_export_as_pdf {
 
 sub action_report_generator_export_as_csv {
   my ($self) = @_;
+
+  delete $::form->{action_report_generator_export_as_csv};
+
   if ($::form->{report_generator_csv_options_set}) {
     $self->report_generator_do('CSV');
     return;
@@ -56,6 +85,9 @@ sub action_report_generator_export_as_csv {
   my @form_values = $::form->flatten_variables(grep { ($_ ne 'login') && ($_ ne 'password') } keys %{ $::form });
 
   $::form->{title} = $::locale->text('CSV export -- options');
+
+  _setup_action_bar($self, 'csv'); # Sub not exported, therefore don't call via object.
+
   $::form->header;
   print $::form->parse_html_template('report_generator/csv_export_options', { 'HIDDEN' => \@form_values });
 }
@@ -95,26 +127,37 @@ sub report_generator_list_objects {
   my @columns     = $params{report}->get_visible_columns('HTML');
 
   for my $obj (@{ $params{objects} || [] }) {
-    my %data = map {
-      my $def = $column_defs->{$_};
-      $_ => {
-        raw_data => $def->{raw_data} ? $def->{raw_data}->($obj) : '',
-        data     => $def->{sub}      ? $def->{sub}->($obj)
-                  : $obj->can($_)    ? $obj->$_
-                  :                    $obj->{$_},
-        link     => $def->{obj_link} ? $def->{obj_link}->($obj) : '',
-      },
-    } @columns;
+    my %data;
+
+    if (blessed($obj) && $obj->isa('SL::Controller::Helper::ReportGenerator::ControlRow::Base')) {
+      $obj->set_data($params{report});
+      next;
+
+    } else {
+      %data = map {
+        my $def = $column_defs->{$_};
+        my $tmp;
+        $tmp->{raw_data} = $def->{raw_data} ? $def->{raw_data}->($obj) : '';
+        $tmp->{data}     = $def->{sub}      ? $def->{sub}->($obj)
+                         : $obj->can($_)    ? $obj->$_
+                         :                    $obj->{$_};
+        $tmp->{link}     = $def->{obj_link} ? $def->{obj_link}->($obj) : '';
+        $_ => $tmp;
+      } @columns;
+    }
 
     $params{data_callback}->(\%data) if $params{data_callback};
 
     $params{report}->add_data(\%data);
   }
 
+  my %options            = %{ $params{options} || {} };
+  $options{action_bar} //= $params{action_bar} // 1;
+
   if ($params{layout}) {
-    return $params{report}->generate_with_headers(%{ $params{options} || {}});
+    return $params{report}->generate_with_headers(%options);
   } else {
-    my $html = $params{report}->generate_html_content(%{ $params{options} || {}});
+    my $html = $params{report}->generate_html_content(action_bar => 0, %options);
     $self->render(\$html , { layout => 0, process => 0 });
   }
 }
@@ -217,6 +260,14 @@ already (column definitions, title, sort handling etc).
 
 Mandatory. An array reference of RDBO models to output.
 
+An element of the array can also be an instance of a control row, i.e.
+an instance of a class derived from
+C<SL::Controller::Helper::ReportGenerator::ControlRow::Base>.
+See also:
+
+L<SL::Controller::Helper::ReportGenerator::ControlRow>
+L<SL::Controller::Helper::ReportGenerator::ControlRow::*>
+
 =item C<data_callback>
 
 Optional. A callback handler (code reference) that gets called for
@@ -230,6 +281,15 @@ L<SL::ReportGenrator/add_data>.
 An optional hash reference that's passed verbatim to the function
 L<SL::ReportGenerator/generate_with_headers>.
 
+=item C<action_bar>
+
+If the buttons for exporting PDF and/or CSV variants are included in
+the action bar. Otherwise they're rendered at the bottom of the page.
+
+The value can be either a specific action bar instance or simply 1 in
+which case the default action bar is used:
+C<$::request-E<gt>layout-E<gt>get('actionbar')>.
+
 =back
 
 =back
diff --git a/SL/Controller/Helper/ReportGenerator/ControlRow.pm b/SL/Controller/Helper/ReportGenerator/ControlRow.pm
new file mode 100644 (file)
index 0000000..536dd34
--- /dev/null
@@ -0,0 +1,103 @@
+package SL::Controller::Helper::ReportGenerator::ControlRow;
+
+use strict;
+use Carp;
+
+use SL::Controller::Helper::ReportGenerator::ControlRow::ALL;
+
+use Exporter 'import';
+our @EXPORT = qw(
+  make_control_row
+);
+
+
+sub make_control_row {
+  my ($type, %args) = @_;
+
+  my $class  = $SL::Controller::Helper::ReportGenerator::ControlRow::ALL::type_to_class{$type} // croak "unknown type $type";
+  my $obj    = $class->new(params => \%args);
+  my @errors = $obj->validate_params;
+  croak join("\n", @errors) if @errors;
+
+  return $obj;
+}
+
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::Helper::ReportGenerator::ControlRow - an interface for
+report generator control rows
+
+=head1 DESCRIPTION
+
+ControlRow is an interface that allows generic control rows to be added
+to objects for the C<SL::Controller::Helper::ReportGenerator>.
+
+Each control row implementation can access the report and add data for a row.
+
+=head1 SYNOPSIS
+
+  package SL::Controller::TimeRecording;
+
+  use SL::Controller::Helper::ReportGenerator;
+  use SL::Controller::Helper::ReportGenerator::ControlRow qw(make_control_row);
+
+  sub action_list {
+    my ($self) = @_;
+
+    # Set up the report generator instance. In this example this is
+    # hidden in "prepare_report".
+    my $report = $self->prepare_report;
+
+    # Get objects from database.
+    my $objects = SL::DB::Manager::TimeRecording->get_all(...);
+
+    # Add a separator
+    push @$objects, make_control_row("separator");
+
+    # And a simple total
+    my $total = sum0 map { _round_total($_->duration_in_hours) } @$objects;
+    push @$objects, make_control_row("simple_data", data => {duration => $total});
+
+    # Let report generator create the output.
+    $self->report_generator_list_objects(
+      report  => $report,
+      objects => $objects,
+    );
+  }
+
+
+=head1 WRITING OWN CONTROL ROW CLASSES
+
+See C<SL::Controller::Helper::ReportGenerator::ControlRow::Base>.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<make_control_row TYPE %PARAMS>
+
+Returns an instance of the control row class for the given type. This
+object can be used as an element of objects to the report generator helper
+(see C<SL::Controller::Helper::ReportGenerator>).
+
+Available types are 'separator', 'data, 'simple_data' for now.
+
+C<%PARAMS> depends on the type. See also:
+
+L<SL::Controller::Helper::ReportGenerator::ControlRow::ALL>
+L<SL::Controller::Helper::ReportGenerator::ControlRow::*>
+
+=back
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Controller/Helper/ReportGenerator/ControlRow/ALL.pm b/SL/Controller/Helper/ReportGenerator/ControlRow/ALL.pm
new file mode 100644 (file)
index 0000000..6ac367a
--- /dev/null
@@ -0,0 +1,15 @@
+package SL::Controller::Helper::ReportGenerator::ControlRow::ALL;
+
+use strict;
+
+use SL::Controller::Helper::ReportGenerator::ControlRow::Data;
+use SL::Controller::Helper::ReportGenerator::ControlRow::Separator;
+use SL::Controller::Helper::ReportGenerator::ControlRow::SimpleData;
+
+our %type_to_class = (
+  data        => 'SL::Controller::Helper::ReportGenerator::ControlRow::Data',
+  separator   => 'SL::Controller::Helper::ReportGenerator::ControlRow::Separator',
+  simple_data => 'SL::Controller::Helper::ReportGenerator::ControlRow::SimpleData',
+);
+
+1;
diff --git a/SL/Controller/Helper/ReportGenerator/ControlRow/Base.pm b/SL/Controller/Helper/ReportGenerator/ControlRow/Base.pm
new file mode 100644 (file)
index 0000000..ea11e97
--- /dev/null
@@ -0,0 +1,98 @@
+package SL::Controller::Helper::ReportGenerator::ControlRow::Base;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+use Rose::Object::MakeMethods::Generic (
+  scalar => [ qw(params) ],
+);
+
+
+sub validate_params { die 'name needs to be implemented' }
+sub set_data        { die 'name needs to be implemented' }
+
+
+1;
+
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::Helper::ReportGenerator::ControlRow::Base - a base class
+for report generator control row classes
+
+=head1 DESCRIPTION
+
+ControlRow is an interface that allows generic control rows to be added
+to objects for the C<SL::Controller::Helper::ReportGenerator>. This is a
+base class from which all control row classes are derived.
+
+=head1 SYNOPSIS
+
+Adding your own new control row of the type "only_dashes":
+
+  package SL::Controller::Helper::ReportGenerator::ControlRow::OnlyDashes;
+
+  use parent qw(SL::Controller::Helper::ReportGenerator::ControlRow::Base);
+
+  sub validate_params { return; } # no params
+
+  sub set_data {
+    my ($self, $report) = @_;
+
+    my %data = map { $_ => {data => '---'} } keys %{ $report->{columns} };
+
+    $report->add_data(\%data);
+  }
+
+After that, you have to register your new class in
+C<SL::Controller::Helper::ReportGenerator::ControlRow::ALL>:
+
+  use SL::Controller::Helper::ReportGenerator::ControlRow::OnlyDashes;
+
+  our %type_to_class = (
+    ...,
+    only_dashes => 'SL::Controller::Helper::ReportGenerator::ControlRow::OnlyDashes',
+  );
+
+
+=head1 WRITING OWN CONTROL ROW CLASSES
+
+You can use C<SL::Controller::Helper::ReportGenerator::ControlRow::Base>
+as parent of your module. You have to provide two methods:
+
+=over 4
+
+=item C<validate_params>
+
+This method is used to validate any params used for your module.
+You can access the params through the method C<params> which contains all
+remaining params after the type of the call to make_control_row (see
+C<SL::Controller::Helper::ReportGenerator::ControlRow>).
+
+The method should return an array of error messages if there are any
+errors. Otherwise it should return C<undef>.
+
+=item C<set_data REPORT>
+
+This method sould set the data for the report generator, which is handeled
+over as argument.
+
+=back
+
+=head1 REGISTERING OWN CONTROL ROW CLASSES
+
+See C<SL::Controller::Helper::ReportGenerator::ControlRow::ALL>. Here your
+class should be included with C<use> and entered in the map C<%type_to_class>
+with an appropiate name for it's type.
+
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Controller/Helper/ReportGenerator/ControlRow/Data.pm b/SL/Controller/Helper/ReportGenerator/ControlRow/Data.pm
new file mode 100644 (file)
index 0000000..159249d
--- /dev/null
@@ -0,0 +1,94 @@
+package SL::Controller::Helper::ReportGenerator::ControlRow::Data;
+
+use strict;
+
+use parent qw(SL::Controller::Helper::ReportGenerator::ControlRow::Base);
+
+
+sub validate_params {
+  my ($self) = @_;
+
+  my @errors;
+  push @errors, 'type "data" needs a parameter "row" as hash ref' if !$self->params->{row} || ('HASH' ne ref $self->params->{row});
+
+  return @errors;;
+}
+
+sub set_data {
+  my ($self, $report) = @_;
+
+  my %data;
+  %data = map {
+    my $def = $self->params->{row}->{$_};
+    my $tmp;
+
+    foreach my $attr (qw(raw_data data link class align)) {
+      $tmp->{$attr} = $def->{$attr} if defined $def->{$attr};
+    }
+    $_ => $tmp;
+  } keys %{ $self->params->{row} };
+
+  $report->add_data(\%data);
+}
+
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::Helper::ReportGenerator::ControlRow::Data - an
+implementaion of a control row class to display data
+
+=head1 DESCRIPTION
+
+This class implements a control row for the report generator helper to display
+data. You can configure the way the data is displayed.
+
+=head1 SYNOPSIS
+
+  use SL::Controller::Helper::ReportGenerator;
+  use SL::Controller::Helper::ReportGenerator::ControlRow qw(make_control_row);
+
+  sub action_list {
+    my ($self) = @_;
+
+    # Set up the report generator instance. In this example this is
+    # hidden in "prepare_report".
+    my $report = $self->prepare_report;
+
+    # Get objects from database.
+    my $objects = SL::DB::Manager::TimeRecording->get_all(...);
+
+    # Add a simple data
+    my $total = $self->get_total($objects);
+    push @$objects, make_control_row(
+      "data",
+      row => { duration => { data  => $total,
+                             class => 'listtotal',
+                             link  => '#info_for_total' } }
+    );
+
+    # Let report generator create the output.
+    $self->report_generator_list_objects(
+      report  => $report,
+      objects => $objects,
+    );
+  }
+
+=head1 PARAMETERS
+
+This control row gets the paramter C<row>, which must a hash ref.
+The keys are the column names for the fields you want to show your
+data. The values are hash refs itself and can contain the keys
+C<raw_data>, C<data>, C<link>, C<class> and C<align> which are passed
+in the data added to the report.
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Controller/Helper/ReportGenerator/ControlRow/Separator.pm b/SL/Controller/Helper/ReportGenerator/ControlRow/Separator.pm
new file mode 100644 (file)
index 0000000..a7e7bf9
--- /dev/null
@@ -0,0 +1,69 @@
+package SL::Controller::Helper::ReportGenerator::ControlRow::Separator;
+
+use strict;
+
+use parent qw(SL::Controller::Helper::ReportGenerator::ControlRow::Base);
+
+
+sub validate_params {
+  return;
+}
+
+sub set_data {
+  my ($self, $report) = @_;
+
+  $report->add_separator();
+}
+
+
+1;
+
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::Helper::ReportGenerator::ControlRow::Separator - an
+implementaion of a control row class to display a separator
+
+=head1 DESCRIPTION
+
+This class implements a control row for the report generator helper to display
+a separator.
+
+=head1 SYNOPSIS
+
+  use SL::Controller::Helper::ReportGenerator;
+  use SL::Controller::Helper::ReportGenerator::ControlRow qw(make_control_row);
+
+  sub action_list {
+    my ($self) = @_;
+
+    # Set up the report generator instance. In this example this is
+    # hidden in "prepare_report".
+    my $report = $self->prepare_report;
+
+    # Get objects from database.
+    my $objects = SL::DB::Manager::TimeRecording->get_all(...);
+
+    # Add a separator
+    push @$objects, make_control_row("separator");
+
+    # Let report generator create the output.
+    $self->report_generator_list_objects(
+      report  => $report,
+      objects => $objects,
+    );
+  }
+
+=head1 PARAMETERS
+
+This control row does not use any parameters.
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Controller/Helper/ReportGenerator/ControlRow/SimpleData.pm b/SL/Controller/Helper/ReportGenerator/ControlRow/SimpleData.pm
new file mode 100644 (file)
index 0000000..7d625d3
--- /dev/null
@@ -0,0 +1,86 @@
+package SL::Controller::Helper::ReportGenerator::ControlRow::SimpleData;
+
+use strict;
+
+use parent qw(SL::Controller::Helper::ReportGenerator::ControlRow::Base);
+
+
+sub validate_params {
+  my ($self) = @_;
+
+  my @errors;
+  push @errors, 'type "simple_data" needs a parameter "data" as hash ref' if !$self->params->{data} || ('HASH' ne ref $self->params->{data});
+
+  return @errors;
+}
+
+sub set_data {
+  my ($self, $report) = @_;
+
+  my %data = map {
+    my $tmp;
+    $tmp->{data} = $self->params->{data}->{$_};
+    $_ => $tmp;
+  } keys %{ $self->params->{data} };
+
+  $report->add_data(\%data);
+}
+
+
+1;
+
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::Helper::ReportGenerator::ControlRow::SimpleData - an
+implementaion of a control row class to display simple data
+
+=head1 DESCRIPTION
+
+This class implements a control row for the report generator helper to display
+simple data. C<Simple> because you only have to provide the column and your data
+as a string.
+
+=head1 SYNOPSIS
+
+  use SL::Controller::Helper::ReportGenerator;
+  use SL::Controller::Helper::ReportGenerator::ControlRow qw(make_control_row);
+
+  sub action_list {
+    my ($self) = @_;
+
+    # Set up the report generator instance. In this example this is
+    # hidden in "prepare_report".
+    my $report = $self->prepare_report;
+
+    # Get objects from database.
+    my $objects = SL::DB::Manager::TimeRecording->get_all(...);
+
+    # Add a simple data
+    push @$objects, make_control_row(
+      "simple_data",
+      data => { duration => 'Total sum of duration is not implemeted yet' }
+    );
+
+    # Let report generator create the output.
+    $self->report_generator_list_objects(
+      report  => $report,
+      objects => $objects,
+    );
+  }
+
+=head1 PARAMETERS
+
+This control row gets the paramter C<data>, which must a hash ref.
+The keys are the column names for the fields you want to show your
+data. The values are the data.
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Controller/Helper/ThumbnailCreator.pm b/SL/Controller/Helper/ThumbnailCreator.pm
new file mode 100644 (file)
index 0000000..cc262d1
--- /dev/null
@@ -0,0 +1,124 @@
+package SL::Controller::Helper::ThumbnailCreator;
+
+use strict;
+
+use SL::Locale::String qw(t8);
+use Carp;
+use GD;
+use Image::Info;
+use File::MimeInfo::Magic;
+use List::MoreUtils qw(apply);
+use List::Util qw(max);
+use Rose::DB::Object::Util;
+
+require Exporter;
+our @ISA      = qw(Exporter);
+our @EXPORT   = qw(file_create_thumbnail file_update_thumbnail file_probe_type file_probe_image_type file_update_type_and_dimensions);
+
+our %supported_mime_types = (
+  'image/gif'  => { extension => 'gif', convert_to_png => 1, },
+  'image/png'  => { extension => 'png' },
+  'image/jpeg' => { extension => 'jpg' },
+  'image/tiff' => { extension => 'tif'},
+);
+
+sub file_create_thumbnail {
+  my ($thumb, %params) = @_;
+
+  croak "No picture set yet" if !$thumb->{content};
+
+  my $image            = GD::Image->new($thumb->{content});
+  my ($width, $height) = $image->getBounds;
+  my $max_dim          = $params{size} // 64;
+  my $curr_max         = max $width, $height, 1;
+  my $factor           = $curr_max <= $max_dim ? 1 : $curr_max / $max_dim;
+  my $new_width        = int($width  / $factor + 0.5);
+  my $new_height       = int($height / $factor + 0.5);
+  my $thumbnail        = GD::Image->new($new_width, $new_height);
+
+  $thumbnail->copyResized($image, 0, 0, 0, 0, $new_width, $new_height, $width, $height);
+
+  $thumb->{thumbnail_img_content} = $thumbnail->png;
+  $thumb->{thumbnail_img_content_type} = "image/png";
+  $thumb->{thumbnail_img_width} = $new_width;
+  $thumb->{thumbnail_img_height} = $new_height;
+  return $thumb;
+
+}
+
+sub file_update_thumbnail {
+  my ($self) = @_;
+
+  return 1 if !$self->file_content || !$self->file_content_type || !Rose::DB::Object::Util::get_column_value_modified($self, 'file_content');
+  $self->file_create_thumbnail;
+  return 1;
+}
+
+sub file_probe_image_type {
+  my ($self, $mime_type, $basefile) = @_;
+
+  if ( !$supported_mime_types{ $mime_type } ) {
+    $self->js->flash('error',t8('file \'#1\' has unsupported image type \'#2\' (supported types: #3)',
+                                $basefile, $mime_type, join(' ', sort keys %supported_mime_types)));
+    return 1;
+  }
+  return 0;
+}
+
+sub file_probe_type {
+  my ($content, %params) = @_;
+  return (t8("No file uploaded yet")) if !$content;
+  my $info = Image::Info::image_info(\$content);
+  if (!$info || $info->{error} || !$info->{file_media_type} || !$supported_mime_types{ $info->{file_media_type} }) {
+    $::lxdebug->warn("Image::Info error: " . $info->{error}) if $info && $info->{error};
+    return (t8('Unsupported image type (supported types: #1)', join(' ', sort keys %supported_mime_types)));
+  }
+
+  my $thumbnail;
+  $thumbnail->{file_content_type} = $info->{file_media_type};
+  $thumbnail->{file_image_width} = $info->{width};
+  $thumbnail->{file_image_height} = $info->{height};
+  $thumbnail->{content} = $content;
+
+  $thumbnail = &file_create_thumbnail($thumbnail, %params);
+
+  return $thumbnail;
+}
+
+sub file_update_type_and_dimensions {
+  my ($self) = @_;
+
+  return () if !$self->file_content;
+  return () if $self->file_content_type && $self->files_img_width && $self->files_img_height && !Rose::DB::Object::Util::get_column_value_modified($self, 'file_content');
+
+  my @errors = $self->file_probe_type;
+  return @errors if @errors;
+
+  my $info = $supported_mime_types{ $self->file_content_type };
+  if ($info->{convert_to_png}) {
+    $self->file_content(GD::Image->new($self->file_content)->png);
+    $self->file_content_type('image/png');
+    $self->filename(apply { s/\.[^\.]+$//;  $_ .= '.png'; } $self->filename);
+  }
+  return ();
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Controller::Helper::ThumbnailCreator - Helper for Fileuploads
+
+=head1 SYNOPSIS
+
+use SL::Controller::Helper::ThumbnailCreator;
+
+=head1 AUTHOR
+
+Werner Hahn E<lt>wh@futureworldsearch.netE<gt>
diff --git a/SL/Controller/ImageUpload.pm b/SL/Controller/ImageUpload.pm
new file mode 100644 (file)
index 0000000..a2497f5
--- /dev/null
@@ -0,0 +1,76 @@
+package SL::Controller::ImageUpload;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+use JSON qw(to_json);
+
+use SL::DB::Part;
+use SL::DB::Order;
+use SL::DB::DeliveryOrder;
+
+use Rose::Object::MakeMethods::Generic
+(
+  scalar => [ qw() ],
+  'scalar --get_set_init' => [ qw(object_type object) ],
+);
+
+my %object_loader = (
+  part            => [ "SL::DB::Part" ],
+  sales_order     => [ "SL::DB::Order", [ sales => 1, quotation => 0 ] ],
+  sales_quotation => [ "SL::DB::Order", [ sales => 1, quotation => 1 ] ],
+  purchase_order  => [ "SL::DB::Order", [ sales => 0, quotation => 1 ] ],
+  sales_delivery_order => [ "SL::DB::DeliveryOrder", [ order_type => 'sales_delivery_order' ] ],
+);
+
+
+################ actions #################
+
+sub action_upload_image {
+  my ($self) = @_;
+
+  $::request->layout->add_javascripts('kivi.File.js');
+  $::request->layout->add_javascripts('kivi.FileDB.js');
+  $::request->layout->add_javascripts('kivi.ImageUpload.js');
+
+  $self->render('image_upload/local_list');
+}
+
+sub action_resolve_object_by_number {
+  my ($self) = @_;
+
+  my $result = {
+    id          => $self->object->id,
+    description => $self->object->displayable_name,
+  };
+
+  $self->render(\ to_json($result), { process => 0, type => 'json' });
+}
+
+################# internal ###############
+
+sub accept_types {
+  "image/*"
+}
+
+sub init_object_type {
+  $::form->{object_type} or die "need object type"
+}
+
+sub init_object {
+  my ($self) = @_;
+
+  return unless $self->object_type;
+
+  my $loader = $object_loader{ $self->object_type } or die "unknown object type";
+  my $manager = $loader->[0]->_get_manager_class;
+
+  return $manager->find_by(id => $::form->{object_id}*1) if $::form->{object_id};
+
+  return $manager->find_by(donumber => $::form->{object_number}, closed => 0, @{ $loader->[1] // [] }) if $::form->{object_number};
+}
+
+
+1;
+
+
index a0764ab..a4c11f6 100644 (file)
@@ -2,33 +2,42 @@ package SL::Controller::Inventory;
 
 use strict;
 use warnings;
+use POSIX qw(strftime);
 
 use parent qw(SL::Controller::Base);
 
 use SL::DB::Inventory;
+use SL::DB::Stocktaking;
 use SL::DB::Part;
 use SL::DB::Warehouse;
 use SL::DB::Unit;
+use SL::DB::Default;
 use SL::WH;
+use SL::ReportGenerator;
 use SL::Locale::String qw(t8);
-use SL::Presenter;
+use SL::Presenter::Tag qw(select_tag);
 use SL::DBUtils;
 use SL::Helper::Flash;
+use SL::Controller::Helper::ReportGenerator;
+use SL::Controller::Helper::GetModels;
+use List::MoreUtils qw(uniq);
+
+use English qw(-no_match_vars);
 
 use Rose::Object::MakeMethods::Generic (
-  'scalar --get_set_init' => [ qw(warehouses units p) ],
+  'scalar --get_set_init' => [ qw(warehouses units is_stocktaking stocktaking_models stocktaking_cutoff_date) ],
   'scalar'                => [ qw(warehouse bin unit part) ],
 );
 
 __PACKAGE__->run_before('_check_auth');
 __PACKAGE__->run_before('_check_warehouses');
-__PACKAGE__->run_before('load_part_from_form',   only => [ qw(stock_in part_changed mini_stock stock) ]);
-__PACKAGE__->run_before('load_unit_from_form',   only => [ qw(stock_in part_changed mini_stock stock) ]);
-__PACKAGE__->run_before('load_wh_from_form',     only => [ qw(stock_in warehouse_changed stock) ]);
-__PACKAGE__->run_before('load_bin_from_form',    only => [ qw(stock_in stock) ]);
+__PACKAGE__->run_before('load_part_from_form',   only => [ qw(stock_in part_changed mini_stock stock stocktaking_part_changed stocktaking_get_warn_qty_threshold save_stocktaking) ]);
+__PACKAGE__->run_before('load_unit_from_form',   only => [ qw(stock_in part_changed mini_stock stock stocktaking_part_changed stocktaking_get_warn_qty_threshold save_stocktaking) ]);
+__PACKAGE__->run_before('load_wh_from_form',     only => [ qw(stock_in warehouse_changed stock stocktaking stocktaking_get_warn_qty_threshold save_stocktaking) ]);
+__PACKAGE__->run_before('load_bin_from_form',    only => [ qw(stock_in stock stocktaking stocktaking_get_warn_qty_threshold save_stocktaking) ]);
 __PACKAGE__->run_before('set_target_from_part',  only => [ qw(part_changed) ]);
 __PACKAGE__->run_before('mini_stock',            only => [ qw(stock_in mini_stock) ]);
-__PACKAGE__->run_before('sanitize_target',       only => [ qw(stock_in warehouse_changed part_changed) ]);
+__PACKAGE__->run_before('sanitize_target',       only => [ qw(stock_usage stock_in warehouse_changed part_changed stocktaking stocktaking_part_changed stocktaking_get_warn_qty_threshold save_stocktaking) ]);
 __PACKAGE__->run_before('set_layout');
 
 sub action_stock_in {
@@ -36,41 +45,404 @@ sub action_stock_in {
 
   $::form->{title}   = t8('Stock');
 
+  # Sometimes we want to open stock_in with a part already selected, but only
+  # the parts_id is passed in the url (and not also warehouse, bin and unit).
+  # Setting select_default_bin in the form will make sure the default warehouse
+  # and bin of that part will already be preselected, as normally
+  # set_target_from_part is only called when a part is changed.
+  $self->set_target_from_part if $::form->{select_default_bin};
   $::request->layout->focus('#part_id_name');
-  $_[0]->render('inventory/warehouse_selection_stock', title => $::form->{title});
+  my $transfer_types = WH->retrieve_transfer_types('in');
+  map { $_->{description} = $main::locale->text($_->{description}) } @{ $transfer_types };
+  $self->setup_stock_in_action_bar;
+  $self->render('inventory/warehouse_selection_stock', title => $::form->{title}, TRANSFER_TYPES => $transfer_types );
+}
+
+sub action_stock_usage {
+  my ($self) = @_;
+
+  $::form->{title}   = t8('UsageE');
+
+  $::form->get_lists('warehouses' => { 'key'    => 'WAREHOUSES',
+                                       'bins'   => 'BINS', });
+
+  $self->setup_stock_usage_action_bar;
+  $self->render('inventory/warehouse_usage',
+                title => $::form->{title},
+                year => DateTime->today->year,
+                WAREHOUSES => $::form->{WAREHOUSES},
+                WAREHOUSE_FILTER => 1,
+                warehouse_id => 0,
+                bin_id => 0
+      );
+
+}
+
+sub getnumcolumns {
+  my ($self) = @_;
+  return qw(stock incorrection found insum back outcorrection disposed
+                     missing shipped used outsum consumed averconsumed);
+}
+
+sub action_usage {
+  my ($self) = @_;
+
+  $main::lxdebug->enter_sub();
+
+  my $form     = $main::form;
+  my %myconfig = %main::myconfig;
+  my $locale   = $main::locale;
+
+  $form->{title}   = t8('UsageE');
+  $form->{report_generator_output_format} = 'HTML' if !$form->{report_generator_output_format};
+
+  my $report = SL::ReportGenerator->new(\%myconfig, $form);
+
+  my @columns = qw(partnumber partdescription);
+
+  push @columns , qw(ptype unit) if $form->{report_generator_output_format} eq 'HTML';
+
+  my @numcolumns = qw(stock incorrection found insum back outcorrection disposed
+                     missing shipped used outsum consumed averconsumed);
+
+  push @columns , $self->getnumcolumns();
+
+  my @hidden_variables = qw(reporttype year duetyp fromdate todate
+                            warehouse_id bin_id partnumber description bestbefore chargenumber partstypes_id);
+  my %column_defs = (
+    'partnumber'      => { 'text' => $locale->text('Part Number'), },
+    'partdescription' => { 'text' => $locale->text('Part_br_Description'), },
+    'unit'            => { 'text' => $locale->text('Unit'), },
+    'stock'           => { 'text' => $locale->text('stock_br'), },
+    'incorrection'    => { 'text' => $locale->text('correction_br'), },
+    'found'           => { 'text' => $locale->text('found_br'), },
+    'insum'           => { 'text' => $locale->text('sum'), },
+    'back'            => { 'text' => $locale->text('back_br'), },
+    'outcorrection'   => { 'text' => $locale->text('correction_br'), },
+    'disposed'        => { 'text' => $locale->text('disposed_br'), },
+    'missing'         => { 'text' => $locale->text('missing_br'), },
+    'shipped'         => { 'text' => $locale->text('shipped_br'), },
+    'used'            => { 'text' => $locale->text('used_br'), },
+    'outsum'          => { 'text' => $locale->text('sum'), },
+    'consumed'        => { 'text' => $locale->text('consumed'), },
+    'averconsumed'    => { 'text' => $locale->text('averconsumed_br'), },
+  );
+
+
+  map { $column_defs{$_}->{visible} = 1 } @columns;
+  #map { $column_defs{$_}->{visible} = $form->{"l_${_}"} ? 1 : 0 } @columns;
+  map { $column_defs{$_}->{align} = 'right' } @numcolumns;
+
+  my @custom_headers = ();
+  # Zeile 1:
+  push @custom_headers, [
+      { 'text' => $locale->text('Part'),
+        'colspan' => ($form->{report_generator_output_format} eq 'HTML'?4:2), 'align' => 'center'},
+      { 'text' => $locale->text('Into bin'), 'colspan' => 4, 'align' => 'center'},
+      { 'text' => $locale->text('From bin'), 'colspan' => 7, 'align' => 'center'},
+      { 'text' => $locale->text('UsageWithout'),    'colspan' => 2, 'align' => 'center'},
+  ];
+
+  # Zeile 2:
+  my @line_2 = ();
+  map { push @line_2 , $column_defs{$_} } @columns;
+  push @custom_headers, [ @line_2 ];
+
+  $report->set_custom_headers(@custom_headers);
+  $report->set_columns( %column_defs );
+  $report->set_column_order(@columns);
+
+  $report->set_export_options('usage', @hidden_variables );
+
+  $report->set_sort_indicator($form->{sort}, $form->{order});
+  $report->set_options('output_format'        => 'HTML',
+                       'controller_class'     => 'Inventory',
+                       'title'                => $form->{title},
+#                      'html_template'        => 'inventory/usage_report',
+                       'attachment_basename'  => strftime($locale->text('warehouse_usage_list') . '_%Y%m%d', localtime time));
+  $report->set_options_from_form;
+
+  my %searchparams ;
+# form vars
+#   reporttype = custom
+#   year = 2014
+#   duetyp = 7
+
+  my $start       = DateTime->now_local;
+  my $end         = DateTime->now_local;
+  my $actualepoch = $end->epoch();
+  my $days = 365;
+  my $mdays=30;
+  $searchparams{reporttype} = $form->{reporttype};
+  if ($form->{reporttype} eq "custom") {
+    my $smon = 1;
+    my $emon = 12;
+    my $sday = 1;
+    my $eday = 31;
+    #forgotten the year --> thisyear
+    if ($form->{year} !~ m/^\d\d\d\d$/) {
+      $locale->date(\%myconfig, $form->current_date(\%myconfig), 0) =~
+        /(\d\d\d\d)/;
+      $form->{year} = $1;
+    }
+    my $leapday = ($form->{year} % 4 == 0) ? 1:0;
+    #yearly report
+    if ($form->{duetyp} eq "13") {
+        $days += $leapday;
+    }
+
+    #Quater reports
+    if ($form->{duetyp} eq "A") {
+      $emon = 3;
+      $days = 90 + $leapday;
+    }
+    if ($form->{duetyp} eq "B") {
+      $smon = 4;
+      $emon = 6;
+      $eday = 30;
+      $days = 91;
+    }
+    if ($form->{duetyp} eq "C") {
+      $smon = 7;
+      $emon = 9;
+      $eday = 30;
+      $days = 92;
+    }
+    if ($form->{duetyp} eq "D") {
+      $smon = 10;
+      $days = 92;
+    }
+    #Monthly reports
+    if ($form->{duetyp} eq "1" || $form->{duetyp} eq "3" || $form->{duetyp} eq "5" ||
+        $form->{duetyp} eq "7" || $form->{duetyp} eq "8" || $form->{duetyp} eq "10" ||
+        $form->{duetyp} eq "12") {
+        $smon = $emon = $form->{duetyp}*1;
+        $mdays=$days = 31;
+    }
+    if ($form->{duetyp} eq "2" || $form->{duetyp} eq "4" || $form->{duetyp} eq "6" ||
+        $form->{duetyp} eq "9" || $form->{duetyp} eq "11" ) {
+        $smon = $emon = $form->{duetyp}*1;
+        $eday = 30;
+        if ($form->{duetyp} eq "2" ) {
+            #this works from 1901 to 2099, 1900 and 2100 fail.
+            $eday = ($form->{year} % 4 == 0) ? 29 : 28;
+        }
+        $mdays=$days = $eday;
+    }
+    $searchparams{year} = $form->{year};
+    $searchparams{duetyp} = $form->{duetyp};
+    $start->set_month($smon);
+    $start->set_day($sday);
+    $start->set_year($form->{year}*1);
+    $end->set_month($emon);
+    $end->set_day($eday);
+    $end->set_year($form->{year}*1);
+  }  else {
+    $searchparams{fromdate} = $form->{fromdate};
+    $searchparams{todate} = $form->{todate};
+#   reporttype = free
+#   fromdate = 01.01.2014
+#   todate = 31.05.2014
+    my ($yy, $mm, $dd) = $locale->parse_date(\%myconfig,$form->{fromdate});
+    $start->set_year($yy);
+    $start->set_month($mm);
+    $start->set_day($dd);
+    ($yy, $mm, $dd) = $locale->parse_date(\%myconfig,$form->{todate});
+    $end->set_year($yy);
+    $end->set_month($mm);
+    $end->set_day($dd);
+    my $dur = $start->delta_md($end);
+    $days = $dur->delta_months()*30 + $dur->delta_days() ;
+  }
+  $start->set_second(0);
+  $start->set_minute(0);
+  $start->set_hour(0);
+  $end->set_second(59);
+  $end->set_minute(59);
+  $end->set_hour(23);
+  if ( $end->epoch() > $actualepoch ) {
+      $end = DateTime->now_local;
+      my $dur = $start->delta_md($end);
+      $days = $dur->delta_months()*30 + $dur->delta_days() ;
+  }
+  if ( $start->epoch() > $end->epoch() ) { $start = $end;$days = 1;}
+  $days = $mdays if $days < $mdays;
+  #$main::lxdebug->message(LXDebug->DEBUG2(), "start=".$start->epoch());
+  #$main::lxdebug->message(LXDebug->DEBUG2(), "  end=".$end->epoch());
+  #$main::lxdebug->message(LXDebug->DEBUG2(), " days=".$days);
+  my @andfilter = (shippingdate => { ge => $start }, shippingdate => { le => $end } );
+  if ( $form->{warehouse_id} ) {
+      push @andfilter , ( warehouse_id => $form->{warehouse_id});
+      $searchparams{warehouse_id} = $form->{warehouse_id};
+      if ( $form->{bin_id} ) {
+          push @andfilter , ( bin_id => $form->{bin_id});
+          $searchparams{bin_id} = $form->{bin_id};
+      }
+  }
+  # alias class t2 entspricht parts
+  if ( $form->{partnumber} ) {
+      push @andfilter , ( 't2.partnumber' => { ilike => '%'. $form->{partnumber} .'%' });
+      $searchparams{partnumber} = $form->{partnumber};
+  }
+  if ( $form->{description} ) {
+      push @andfilter , ( 't2.description' => { ilike => '%'. $form->{description} .'%'  });
+      $searchparams{description} = $form->{description};
+  }
+  if ( $form->{bestbefore} ) {
+    push @andfilter , ( bestbefore => { eq => $form->{bestbefore} });
+      $searchparams{bestbefore} = $form->{bestbefore};
+  }
+  if ( $form->{chargenumber} ) {
+      push @andfilter , ( chargenumber => { ilike => '%'.$form->{chargenumber}.'%' });
+      $searchparams{chargenumber} = $form->{chargenumber};
+  }
+  if ( $form->{partstypes_id} ) {
+      push @andfilter , ( 't2.partstypes_id' => $form->{partstypes_id} );
+      $searchparams{partstypes_id} = $form->{partstypes_id};
+  }
+
+  my @filter = (and => [ @andfilter ] );
+
+  my $objs = SL::DB::Manager::Inventory->get_all(with_objects => ['parts'], where => [ @filter ] , sort_by => 'parts.partnumber ASC');
+  #my $objs = SL::DB::Inventory->_get_manager_class->get_all(...);
+
+  # manual paginating, yuck
+  my $page = $::form->{page} || 1;
+  my $pages = {};
+  $pages->{per_page}        = $::form->{per_page} || 20;
+  my $first_nr = ($page - 1) * $pages->{per_page};
+  my $last_nr  = $first_nr + $pages->{per_page};
+
+  my $last_partid = 0;
+  my $last_row = { };
+  my $row_ind = 0;
+  my $allrows = 0;
+  $allrows = 1 if $form->{report_generator_output_format} ne 'HTML' ;
+  #$main::lxdebug->message(LXDebug->DEBUG2(), "first_nr=".$first_nr." last_nr=".$last_nr);
+  foreach my $entry (@{ $objs } ) {
+      if ( $entry->parts_id != $last_partid ) {
+          if ( $last_partid > 0 ) {
+              if ( $allrows || ($row_ind >= $first_nr && $row_ind < $last_nr )) {
+                  $self->make_row_result($last_row,$days,$last_partid);
+                  $report->add_data($last_row);
+              }
+              $row_ind++ ;
+          }
+          $last_partid = $entry->parts_id;
+          $last_row = { };
+          $last_row->{partnumber}->{data} = $entry->part->partnumber;
+          $last_row->{partdescription}->{data} = $entry->part->description;
+          $last_row->{unit}->{data} = $entry->part->unit;
+          $last_row->{stock}->{data} = 0;
+          $last_row->{incorrection}->{data} = 0;
+          $last_row->{found}->{data} = 0;
+          $last_row->{back}->{data} = 0;
+          $last_row->{outcorrection}->{data} = 0;
+          $last_row->{disposed}->{data} = 0;
+          $last_row->{missing}->{data} = 0;
+          $last_row->{shipped}->{data} = 0;
+          $last_row->{used}->{data} = 0;
+          $last_row->{insum}->{data} = 0;
+          $last_row->{outsum}->{data} = 0;
+          $last_row->{consumed}->{data} = 0;
+          $last_row->{averconsumed}->{data} = 0;
+      }
+      if ( !$allrows && $row_ind >= $last_nr ) {
+          next;
+      }
+      my $prefix='';
+      if ( $entry->trans_type->description eq 'correction' ) {
+          $prefix = $entry->trans_type->direction;
+      }
+      $last_row->{$prefix.$entry->trans_type->description}->{data} +=
+          ( $entry->trans_type->direction eq 'out' ? -$entry->qty : $entry->qty );
+  }
+  if ( $last_partid > 0 && ( $allrows || ($row_ind >= $first_nr && $row_ind < $last_nr ))) {
+      $self->make_row_result($last_row,$days,$last_partid);
+      $report->add_data($last_row);
+      $row_ind++ ;
+  }
+  my $num_rows = @{ $report->{data} } ;
+  #$main::lxdebug->message(LXDebug->DEBUG2(), "count=".$row_ind." rows=".$num_rows);
+
+  if ( ! $allrows ) {
+      $pages->{max}  = SL::DB::Helper::Paginated::ceil($row_ind, $pages->{per_page}) || 1;
+      $pages->{page} = $page < 1 ? 1: $page > $pages->{max} ? $pages->{max}: $page;
+      $pages->{common} = [ grep { $_->{visible} } @{ SL::DB::Helper::Paginated::make_common_pages($pages->{page}, $pages->{max}) } ];
+      $self->{pages} = $pages;
+      $searchparams{action} = "usage";
+      $self->{base_url} = $self->url_for(\%searchparams );
+      #$main::lxdebug->message(LXDebug->DEBUG2(), "page=".$pages->{page}." url=".$self->{base_url});
+
+      $report->set_options('raw_bottom_info_text' => $self->render('inventory/report_bottom', { output => 0 }) );
+  }
+  $report->generate_with_headers();
+
+  $main::lxdebug->leave_sub();
+
+}
+
+sub make_row_result {
+  my ($self,$row,$days,$partid) = @_;
+  my $form     = $main::form;
+  my $myconfig = \%main::myconfig;
+
+  $row->{insum}->{data}  = $row->{stock}->{data} + $row->{incorrection}->{data} + $row->{found}->{data};
+  $row->{outsum}->{data} = $row->{back}->{data} + $row->{outcorrection}->{data} + $row->{disposed}->{data} +
+       $row->{missing}->{data} + $row->{shipped}->{data} + $row->{used}->{data};
+  $row->{consumed}->{data} = $row->{outsum}->{data} -
+       $row->{outcorrection}->{data} - $row->{incorrection}->{data};
+  $row->{averconsumed}->{data} = $row->{consumed}->{data}*30/$days ;
+  map { $row->{$_}->{data} = $form->format_amount($myconfig,$row->{$_}->{data},2); } $self->getnumcolumns();
+  $row->{partnumber}->{link} = 'controller.pl?action=Part/edit&part.id=' . $partid;
 }
 
 sub action_stock {
   my ($self) = @_;
 
+  my $transfer_error;
   my $qty = $::form->parse_amount(\%::myconfig, $::form->{qty});
   if (!$qty) {
-    flash_later('error', t8('Cannot stock without amount'));
+    $transfer_error = t8('Cannot stock without amount');
   } elsif ($qty < 0) {
-    flash_later('error', t8('Cannot stock negative amounts'));
+    $transfer_error = t8('Cannot stock negative amounts');
   } else {
     # do stock
-    WH->transfer({
-      parts         => $self->part,
-      dst_bin       => $self->bin,
-      dst_wh        => $self->warehouse,
-      qty           => $qty,
-      unit          => $self->unit,
-      transfer_type => 'stock',
-      chargenumber  => $::form->{chargenumber},
-      bestbefore    => $::form->{bestbefore},
-      ean           => $::form->{ean},
-      comment       => $::form->{comment},
+    $::form->throw_on_error(sub {
+      eval {
+        WH->transfer({
+          parts         => $self->part,
+          dst_bin       => $self->bin,
+          dst_wh        => $self->warehouse,
+          qty           => $qty,
+          unit          => $self->unit,
+          transfer_type => 'stock',
+          transfer_type_id => $::form->{transfer_type_id},
+          chargenumber  => $::form->{chargenumber},
+          bestbefore    => $::form->{bestbefore},
+          comment       => $::form->{comment},
+        });
+        1;
+      } or do { $transfer_error = $EVAL_ERROR->error; }
     });
 
-    if ($::form->{write_default_bin}) {
-      $self->part->load;   # onhand is calculated in between. don't mess that up
-      $self->part->bin($self->bin);
-      $self->part->warehouse($self->warehouse);
-      $self->part->save;
+    if (!$transfer_error) {
+      if ($::form->{write_default_bin}) {
+        $self->part->load;   # onhand is calculated in between. don't mess that up
+        $self->part->bin($self->bin);
+        $self->part->warehouse($self->warehouse);
+        $self->part->save;
+      }
+
+      flash_later('info', t8('Transfer successful'));
     }
+  }
 
-    flash_later('info', t8('Transfer successful'));
+  my %additional_redirect_params = ();
+  if ($transfer_error) {
+    flash_later('error', $transfer_error);
+    $additional_redirect_params{$_}  = $::form->{$_} for qw(qty chargenumber bestbefore ean comment);
+    $additional_redirect_params{qty} = $qty;
   }
 
   # redirect
@@ -80,6 +452,7 @@ sub action_stock {
     bin_id       => $self->bin->id,
     warehouse_id => $self->warehouse->id,
     unit_id      => $self->unit->id,
+    %additional_redirect_params,
   );
 }
 
@@ -119,6 +492,155 @@ sub action_mini_stock {
     ->render;
 }
 
+sub action_stocktaking {
+  my ($self) = @_;
+
+  $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Inventory);
+  $::request->layout->focus('#part_id_name');
+  $self->setup_stock_stocktaking_action_bar;
+  $self->render('inventory/stocktaking/form', title => t8('Stocktaking'));
+}
+
+sub action_save_stocktaking {
+  my ($self) = @_;
+
+  return $self->js->flash('error', t8('Please choose a part.'))->render()
+    if !$::form->{part_id};
+
+  return $self->js->flash('error', t8('A target quantitiy has to be given'))->render()
+    if $::form->{target_qty} eq '';
+
+  my $target_qty = $::form->parse_amount(\%::myconfig, $::form->{target_qty});
+
+  return $self->js->flash('error', t8('Error: A negative target quantity is not allowed.'))->render()
+    if $target_qty < 0;
+
+  my $stocked_qty  = _get_stocked_qty($self->part,
+                                      warehouse_id => $self->warehouse->id,
+                                      bin_id       => $self->bin->id,
+                                      chargenumber => $::form->{chargenumber},
+                                      bestbefore   => $::form->{bestbefore},);
+
+  my $stocked_qty_in_form_units = $self->part->unit_obj->convert_to($stocked_qty, $self->unit);
+
+  if (!$::form->{dont_check_already_counted}) {
+    my $already_counted = _already_counted($self->part,
+                                           warehouse_id => $self->warehouse->id,
+                                           bin_id       => $self->bin->id,
+                                           cutoff_date  => $::form->{cutoff_date_as_date},
+                                           chargenumber => $::form->{chargenumber},
+                                           bestbefore   => $::form->{bestbefore});
+    if (scalar @$already_counted) {
+      my $reply = $self->js->dialog->open({
+        html   => $self->render('inventory/stocktaking/_already_counted_dialog',
+                                { output => 0 },
+                                already_counted           => $already_counted,
+                                stocked_qty               => $stocked_qty,
+                                stocked_qty_in_form_units => $stocked_qty_in_form_units),
+        id     => 'already_counted_dialog',
+        dialog => {
+          title => t8('Already counted'),
+        },
+      })->render;
+
+      return $reply;
+    }
+  }
+
+  # - target_qty is in units given in form ($self->unit)
+  # - WH->transfer expects qtys in given unit (here: unit from form (unit -> $self->unit))
+  # Therefore use stocked_qty in form units for calculation.
+  my $qty        = $target_qty - $stocked_qty_in_form_units;
+  my $src_or_dst = $qty < 0? 'src' : 'dst';
+  $qty           = abs($qty);
+
+  my $transfer_error;
+  # do stock
+  $::form->throw_on_error(sub {
+    eval {
+      WH->transfer({
+        parts                   => $self->part,
+        $src_or_dst.'_bin'      => $self->bin,
+        $src_or_dst.'_wh'       => $self->warehouse,
+        qty                     => $qty,
+        unit                    => $self->unit,
+        transfer_type           => 'stocktaking',
+        chargenumber            => $::form->{chargenumber},
+        bestbefore              => $::form->{bestbefore},
+        ean                     => $::form->{ean},
+        comment                 => $::form->{comment},
+        record_stocktaking      => 1,
+        stocktaking_qty         => $target_qty,
+        stocktaking_cutoff_date => $::form->{cutoff_date_as_date},
+      });
+      1;
+    } or do { $transfer_error = $EVAL_ERROR->error; }
+  });
+
+  return $self->js->flash('error', $transfer_error)->render()
+    if $transfer_error;
+
+  flash_later('info', $::locale->text('Part successful counted'));
+  $self->redirect_to(action              => 'stocktaking',
+                     warehouse_id        => $self->warehouse->id,
+                     bin_id              => $self->bin->id,
+                     cutoff_date_as_date => $self->stocktaking_cutoff_date->to_kivitendo);
+}
+
+sub action_reload_stocktaking_history {
+  my ($self) = @_;
+
+  $::form->{filter}{'cutoff_date:date'} = $self->stocktaking_cutoff_date->to_kivitendo;
+  $::form->{filter}{'employee_id'}      = SL::DB::Manager::Employee->current->id;
+
+  $self->prepare_stocktaking_report;
+  $self->report_generator_list_objects(report => $self->{report}, objects => $self->stocktaking_models->get, layout => 0, header => 0);
+}
+
+sub action_stocktaking_part_changed {
+  my ($self) = @_;
+
+  $self->js
+    ->replaceWith('#unit_id', $self->build_unit_select)
+    ->focus('#target_qty')
+    ->render;
+}
+
+sub action_stocktaking_journal {
+  my ($self) = @_;
+
+  $self->prepare_stocktaking_report(full => 1);
+  $self->report_generator_list_objects(report => $self->{report}, objects => $self->stocktaking_models->get);
+}
+
+sub action_stocktaking_get_warn_qty_threshold {
+  my ($self) = @_;
+
+  return $_[0]->render(\ !!0, { type => 'text' }) if !$::form->{part_id};
+  return $_[0]->render(\ !!0, { type => 'text' }) if $::form->{target_qty} eq '';
+  return $_[0]->render(\ !!0, { type => 'text' }) if 0 == $::instance_conf->get_stocktaking_qty_threshold;
+
+  my $target_qty  = $::form->parse_amount(\%::myconfig, $::form->{target_qty});
+  my $stocked_qty = _get_stocked_qty($self->part,
+                                     warehouse_id => $self->warehouse->id,
+                                     bin_id       => $self->bin->id,
+                                     chargenumber => $::form->{chargenumber},
+                                     bestbefore   => $::form->{bestbefore},);
+  my $stocked_qty_in_form_units = $self->part->unit_obj->convert_to($stocked_qty, $self->unit);
+  my $qty        = $target_qty - $stocked_qty_in_form_units;
+  $qty           = abs($qty);
+
+  my $warn;
+  if ($qty > $::instance_conf->get_stocktaking_qty_threshold) {
+    $warn  = t8('The target quantity of #1 differs more than the threshold quantity of #2.',
+                $::form->{target_qty} . " " . $self->unit->name,
+                $::form->format_amount(\%::myconfig, $::instance_conf->get_stocktaking_qty_threshold, 2));
+    $warn .= "\n";
+    $warn .= t8('Choose "continue" if you want to use this value. Choose "cancel" otherwise.');
+  }
+  return $_[0]->render(\ $warn, { type => 'text' });
+}
+
 #================================================================
 
 sub _check_auth {
@@ -133,12 +655,58 @@ sub init_warehouses {
   SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef ]]);
 }
 
+#sub init_bins {
+#  SL::DB::Manager::Bin->get_all();
+#}
+
 sub init_units {
   SL::DB::Manager::Unit->get_all;
 }
 
-sub init_p {
-  SL::Presenter->get;
+sub init_is_stocktaking {
+  return $_[0]->action_name =~ m{stocktaking};
+}
+
+sub init_stocktaking_models {
+  my ($self) = @_;
+
+  SL::Controller::Helper::GetModels->new(
+    controller   => $self,
+    model        => 'Stocktaking',
+    sorted       => {
+      _default => {
+        by    => 'itime',
+        dir   => 0,
+      },
+      itime        => t8('Insert Date'),
+      qty          => t8('Target Qty'),
+      chargenumber => t8('Charge Number'),
+      comment      => t8('Comment'),
+      employee     => t8('Employee'),
+      ean          => t8('EAN'),
+      partnumber   => t8('Part Number'),
+      part         => t8('Part Description'),
+      bin          => t8('Bin'),
+      cutoff_date  => t8('Cutoff Date'),
+    },
+    with_objects => ['employee', 'parts', 'warehouse', 'bin'],
+  );
+}
+
+sub init_stocktaking_cutoff_date {
+  my ($self) = @_;
+
+  return DateTime->from_kivitendo($::form->{cutoff_date_as_date}) if $::form->{cutoff_date_as_date};
+  return SL::DB::Default->get->stocktaking_cutoff_date if SL::DB::Default->get->stocktaking_cutoff_date;
+
+  # Default cutoff date is last day of current year, but if current month
+  # is janurary, it is the last day of the last year.
+  my $now    = DateTime->now_local;
+  my $cutoff = DateTime->new(year => $now->year, month => 12, day => 31);
+  if ($now->month < 1) {
+    $cutoff->subtract(years => 1);
+  }
+  return $cutoff;
 }
 
 sub set_target_from_part {
@@ -155,10 +723,18 @@ sub sanitize_target {
 
   $self->warehouse($self->warehouses->[0])       if !$self->warehouse || !$self->warehouse->id;
   $self->bin      ($self->warehouse->bins->[0])  if !$self->bin       || !$self->bin->id;
+#  foreach my $warehouse ( $self->warehouses ) {
+#      $warehouse->{BINS} = [];
+#      foreach my $bin ( $self->bins ) {
+#         if ( $bin->warehouse_id == $warehouse->id ) {
+#             push @{ $warehouse->{BINS} }, $bin;
+#         }
+#      }
+#  }
 }
 
 sub load_part_from_form {
-  $_[0]->part(SL::DB::Manager::Part->find_by_or_create(id => $::form->{part_id}));
+  $_[0]->part(SL::DB::Manager::Part->find_by_or_create(id => $::form->{part_id}||undef));
 }
 
 sub load_unit_from_form {
@@ -166,11 +742,17 @@ sub load_unit_from_form {
 }
 
 sub load_wh_from_form {
-  $_[0]->warehouse(SL::DB::Manager::Warehouse->find_by_or_create(id => $::form->{warehouse_id}));
+  my $preselected;
+  $preselected = SL::DB::Default->get->stocktaking_warehouse_id if $_[0]->is_stocktaking;
+
+  $_[0]->warehouse(SL::DB::Manager::Warehouse->find_by_or_create(id => ($::form->{warehouse_id} || $preselected)));
 }
 
 sub load_bin_from_form {
-  $_[0]->bin(SL::DB::Manager::Bin->find_by_or_create(id => $::form->{bin_id}));
+  my $preselected;
+  $preselected = SL::DB::Default->get->stocktaking_bin_id if $_[0]->is_stocktaking;
+
+  $_[0]->bin(SL::DB::Manager::Bin->find_by_or_create(id => ($::form->{bin_id} || $preselected)));
 }
 
 sub set_layout {
@@ -178,7 +760,7 @@ sub set_layout {
 }
 
 sub build_warehouse_select {
$_[0]->p->select_tag('warehouse_id', $_[0]->warehouses,
 select_tag('warehouse_id', $_[0]->warehouses,
    title_key => 'description',
    default   => $_[0]->warehouse->id,
    onchange  => 'reload_bin_selection()',
@@ -186,7 +768,7 @@ sub build_warehouse_select {
 }
 
 sub build_bin_select {
-  $_[0]->p->select_tag('bin_id', [ $_[0]->warehouse->bins ],
+  select_tag('bin_id', [ $_[0]->warehouse->bins ],
     title_key => 'description',
     default   => $_[0]->bin->id,
   );
@@ -194,11 +776,11 @@ sub build_bin_select {
 
 sub build_unit_select {
   $_[0]->part->id
-    ? $_[0]->p->select_tag('unit_id', $_[0]->part->available_units,
+    ? select_tag('unit_id', $_[0]->part->available_units,
         title_key => 'name',
         default   => $_[0]->part->unit_obj->id,
       )
-    : $_[0]->p->select_tag('unit_id', $_[0]->units,
+    : select_tag('unit_id', $_[0]->units,
         title_key => 'name',
       )
 }
@@ -206,22 +788,55 @@ sub build_unit_select {
 sub mini_journal {
   my ($self) = @_;
 
-  # get last 10 transaction ids
-  my $query = 'SELECT trans_id, max(itime) FROM inventory GROUP BY trans_id ORDER BY max(itime) DESC LIMIT 10';
-  my @ids = selectall_array_query($::form, $::form->get_standard_dbh, $query);
+  # We want to fetch the last 10 inventory events (inventory rows with the same trans_id)
+  # To prevent a Seq Scan on inventory set an index on inventory.itime
+  # Each event may have one (transfer_in/out) or two (transfer) inventory rows
+  # So fetch the last 20, group by trans_id, limit to the last 10 trans_ids,
+  # and then extract the inventory ids from those 10 trans_ids
+  # By querying Inventory->get_all via the id instead of trans_id we can make
+  # use of the existing index on id
 
-  my $objs;
-  $objs = SL::DB::Manager::Inventory->get_all(query => [ trans_id => \@ids ]) if @ids;
+  # inventory ids of the most recent 10 inventory trans_ids
+  my $query = <<SQL;
+with last_inventories as (
+   select id,
+          trans_id,
+          itime
+     from inventory
+ order by itime desc
+    limit 20
+),
+grouped_ids as (
+   select trans_id,
+          array_agg(id) as ids
+     from last_inventories
+ group by trans_id
+ order by max(itime)
+     desc limit 10
+)
+select unnest(ids)
+  from grouped_ids
+ limit 20  -- so the planner knows how many ids to expect, the cte is an optimisation fence
+SQL
 
-  # at most 2 of them belong to a transaction and the qty determins in or out.
-  # sort them for display
+  my $objs  = SL::DB::Manager::Inventory->get_all(
+    query        => [ id => [ \"$query" ] ],                           # " make emacs happy
+    with_objects => [ 'parts', 'trans_type', 'bin', 'bin.warehouse' ], # prevent lazy loading in template
+    sort_by      => 'itime DESC',
+  );
+  # remember order of trans_ids from query, for ordering hash later
+  my @sorted_trans_ids = uniq map { $_->trans_id } @$objs;
+
+  # at most 2 of them belong to a transaction and the qty determines in or out.
   my %transactions;
   for (@$objs) {
     $transactions{ $_->trans_id }{ $_->qty > 0 ? 'in' : 'out' } = $_;
     $transactions{ $_->trans_id }{base} = $_;
   }
-  # and get them into order again
-  my @sorted = map { $transactions{$_} } @ids;
+
+  # because the inventory transactions were built in a hash, we need to sort the
+  # hash by using the original sort order of the trans_ids
+  my @sorted = map { $transactions{$_} } @sorted_trans_ids;
 
   return \@sorted;
 }
@@ -247,4 +862,269 @@ sub show_no_warehouses_error {
   $::form->show_generic_error($msg);
 }
 
+sub prepare_stocktaking_report {
+  my ($self, %params) = @_;
+
+  my $callback    = $self->stocktaking_models->get_callback;
+
+  my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
+  $self->{report} = $report;
+
+  my @columns     = qw(itime employee ean partnumber part qty unit bin chargenumber comment cutoff_date);
+  my @sortable    = qw(itime employee ean partnumber part qty bin chargenumber comment cutoff_date);
+
+  my %column_defs = (
+    itime           => { sub   => sub { $_[0]->itime_as_timestamp },
+                         text  => t8('Insert Date'), },
+    employee        => { sub   => sub { $_[0]->employee->safe_name },
+                         text  => t8('Employee'), },
+    ean             => { sub   => sub { $_[0]->part->ean },
+                         text  => t8('EAN'), },
+    partnumber      => { sub   => sub { $_[0]->part->partnumber },
+                         text  => t8('Part Number'), },
+    part            => { sub   => sub { $_[0]->part->description },
+                         text  => t8('Part Description'), },
+    qty             => { sub   => sub { $_[0]->qty_as_number },
+                         text  => t8('Target Qty'),
+                         align => 'right', },
+    unit            => { sub   => sub { $_[0]->part->unit },
+                         text  => t8('Unit'), },
+    bin             => { sub   => sub { $_[0]->bin->full_description },
+                         text  => t8('Bin'), },
+    chargenumber    => { text  => t8('Charge Number'), },
+    comment         => { text  => t8('Comment'), },
+    cutoff_date     => { sub   => sub { $_[0]->cutoff_date_as_date },
+                         text  => t8('Cutoff Date'), },
+  );
+
+  $report->set_options(
+    std_column_visibility => 1,
+    controller_class      => 'Inventory',
+    output_format         => 'HTML',
+    title                 => (!!$params{full})? $::locale->text('Stocktaking Journal') : $::locale->text('Stocktaking History'),
+    allow_pdf_export      => !!$params{full},
+    allow_csv_export      => !!$params{full},
+  );
+  $report->set_columns(%column_defs);
+  $report->set_column_order(@columns);
+  $report->set_export_options(qw(stocktaking_journal filter));
+  $report->set_options_from_form;
+  $self->stocktaking_models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
+  $self->stocktaking_models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable) if !!$params{full};
+  if (!!$params{full}) {
+    $report->set_options(
+      raw_top_info_text    => $self->render('inventory/stocktaking/full_report_top', { output => 0 }),
+    );
+  }
+  $report->set_options(
+    raw_bottom_info_text => $self->render('inventory/stocktaking/report_bottom',   { output => 0 }),
+  );
+}
+
+sub _get_stocked_qty {
+  my ($part, %params) = @_;
+
+  my $bestbefore_filter  = '';
+  my $bestbefore_val_cnt = 0;
+  if ($::instance_conf->get_show_bestbefore) {
+    $bestbefore_filter  = ($params{bestbefore}) ? 'AND bestbefore = ?' : 'AND bestbefore IS NULL';
+    $bestbefore_val_cnt = ($params{bestbefore}) ? 1                    : 0;
+  }
+
+  my $query = <<SQL;
+    SELECT sum(qty) FROM inventory
+      WHERE parts_id = ? AND warehouse_id = ? AND bin_id = ? AND chargenumber = ? $bestbefore_filter
+      GROUP BY warehouse_id, bin_id, chargenumber
+SQL
+
+  my @values = ($part->id,
+                $params{warehouse_id},
+                $params{bin_id},
+                $params{chargenumber});
+  push @values, $params{bestbefore} if $bestbefore_val_cnt;
+
+  my ($stocked_qty) = selectrow_query($::form, $::form->get_standard_dbh, $query, @values);
+
+  return 1*($stocked_qty || 0);
+}
+
+sub _already_counted {
+  my ($part, %params) = @_;
+
+  my %bestbefore_filter;
+  if ($::instance_conf->get_show_bestbefore) {
+    %bestbefore_filter = (bestbefore => ($params{bestbefore} || undef));
+  }
+
+  SL::DB::Manager::Stocktaking->get_all(query => [and => [parts_id     => $part->id,
+                                                          warehouse_id => $params{warehouse_id},
+                                                          bin_id       => $params{bin_id},
+                                                          cutoff_date  => $params{cutoff_date},
+                                                          chargenumber => $params{chargenumber},
+                                                          %bestbefore_filter]],
+                                        sort_by => ['itime DESC']);
+}
+
+sub setup_stock_in_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Stock'),
+        submit    => [ '#form', { action => 'Inventory/stock' } ],
+        checks    => [ 'check_part_selection_before_stocking' ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_stock_usage_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Show'),
+        submit    => [ '#form', { action => 'Inventory/usage' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_stock_stocktaking_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        checks    => [ 'kivi.Inventory.check_stocktaking_qty_threshold' ],
+        call      => [ 'kivi.Inventory.save_stocktaking' ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
 1;
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::Inventory - Controller for inventory
+
+=head1 DESCRIPTION
+
+This controller handles stock in, stocktaking and reports about inventory
+in warehouses/stocks
+
+- warehouse content
+
+- warehouse journal
+
+- warehouse withdrawal
+
+- stocktaking
+
+=head2 Stocktaking
+
+Stocktaking allows to document the counted quantities of parts during
+stocktaking for a certain cutoff date. Differences between counted and stocked
+quantities are corrected in the stock. The transfer type 'stocktacking' is set
+here.
+
+After picking a part, the mini stock for this part is displayed. At the bottom
+of the form a history of already counted parts for the current employee and the
+choosen cutoff date is shown.
+
+Warehouse, bin and cutoff date canbe preselected in the client configuration.
+
+If a part was already counted for this cutoff date, warehouse and bin, a warning
+is displayed, allowing the user to choose to add the counted quantity to the
+stocked one or to take his counted quantity as the new stocked quantity.
+
+There is also a journal of stocktakings.
+
+Templates are located under C<templates/webpages/inventory/stocktaking>.
+JavaScript functions can be found in C<js/kivi.Inventory.js>.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<action_stock_usage>
+
+Create a search form for stock withdrawal.
+The search parameter for report are made like the reports in bin/mozilla/rp.pl
+
+=item C<action_usage>
+
+Make a report about stock withdrawal.
+
+The manual pagination is implemented like the pagination in SL::Controller::CsvImport.
+
+=item C<action_stocktaking>
+
+This action renders the input form for stocktaking.
+
+=item C<action_save_stocktaking>
+
+This action saves the stocktaking values and corrects the stock after checking
+if the part is already counted for this warehouse, bin and cutoff date.
+For saving SL::WH->transfer is called.
+
+=item C<action_reload_stocktaking_history>
+
+This action is responsible for displaying the stocktaking history at the bottom
+of the form. It uses the stocktaking journal with fixed filters for cutoff date
+and the current employee. The history is displayed via javascript.
+
+=item C<action_stocktaking_part_changed>
+
+This action is called after the user selected or changed the part.
+
+=item C<action_stocktaking_get_warn_qty_threshold>
+
+This action checks if a warning should be shown and returns the warning text via
+ajax. The warning will be shown if the given target value is greater than the
+threshold given in the client configuration.
+
+=item C<is_stocktaking>
+
+This is a method to check if actions are called from stocktaking form.
+This actions should contain "stocktaking" in their name.
+
+=back
+
+=head1 SPECIAL CASES
+
+Because of the PFD-Table Formatter some parameters for PDF must be different to the HTML parameters.
+So in german language there are some tries to use a HTML Break in the second heading line
+to produce two line heading inside table. The actual version has some abbreviations for the header texts.
+
+=head1 BUGS
+
+The PDF-Table library has some limits (doesn't display all if the line is to large) so
+the format is adapted to this
+
+
+=head1 AUTHOR
+
+=over 4
+
+=item only for C<action_stock_usage> and C<action_usage>:
+
+Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
+
+=item for stocktaking:
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=back
+
+=cut
diff --git a/SL/Controller/Letter.pm b/SL/Controller/Letter.pm
new file mode 100644 (file)
index 0000000..4faf452
--- /dev/null
@@ -0,0 +1,702 @@
+package SL::Controller::Letter;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+use Carp;
+use File::Basename;
+use POSIX qw(strftime);
+use SL::Controller::Helper::GetModels;
+use SL::Controller::Helper::ReportGenerator;
+use SL::CT;
+use SL::DB::Employee;
+use SL::DB::Language;
+use SL::DB::Letter;
+use SL::DB::LetterDraft;
+use SL::DB::Printer;
+use SL::File;
+use SL::Helper::Flash qw(flash flash_later);
+use SL::Helper::CreatePDF;
+use SL::Helper::PrintOptions;
+use SL::Locale::String qw(t8);
+use SL::Mailer;
+use SL::IS;
+use SL::Presenter::Tag qw(select_tag);
+use SL::ReportGenerator;
+use SL::Webdav;
+use SL::Webdav::File;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(letter all_employees models webdav_objects is_sales) ],
+);
+
+__PACKAGE__->run_before('check_auth_edit');
+__PACKAGE__->run_before('check_auth_report', only => [ qw(list) ]);
+
+use constant TEXT_CREATED_FOR_VALUES => (qw(presskit fax letter));
+use constant PAGE_CREATED_FOR_VALUES => (qw(sketch 1 2));
+
+my %sort_columns = (
+  date                  => t8('Date'),
+  subject               => t8('Subject'),
+  letternumber          => t8('Letternumber'),
+  customer_id           => t8('Customer'),
+  vendor_id             => t8('Vendor'),
+  contact               => t8('Contact'),
+);
+
+### actions
+
+sub action_add {
+  my ($self, %params) = @_;
+
+  return if $self->load_letter_draft(%params);
+
+  $self->letter->employee_id(SL::DB::Manager::Employee->current->id);
+  $self->letter->salesman_id(SL::DB::Manager::Employee->current->id);
+
+  $self->_display(
+    title       => t8('Add Letter'),
+    language_id => $params{language_id},
+  );
+}
+
+sub action_edit {
+  my ($self, %params) = @_;
+
+  return $self->action_add
+    unless $::form->{letter} || $::form->{draft};
+
+  if ($::form->{draft}) {
+    $self->letter(SL::DB::Letter->new_from_draft($::form->{draft}{id}));
+    $self->is_sales($self->letter->is_sales);
+  }
+
+  $self->_display(
+    title  => t8('Edit Letter'),
+  );
+}
+
+sub action_save {
+  my ($self, %params) = @_;
+
+  my $letter = $self->_update;
+
+  if (!$self->check_letter($letter)) {
+    return $self->_display;
+  }
+
+  $self->check_number;
+
+  if (!$letter->save) {
+    flash('error', t8('There was an error saving the letter'));
+    return $self->_display;
+  }
+
+  flash('info', t8('Letter saved!'));
+
+  $self->_display;
+}
+
+sub action_update_contacts {
+  my ($self) = @_;
+
+  my $letter = $self->letter;
+
+  if (!$self->letter->has_customer_vendor) {
+    return $self->js
+      ->replaceWith(
+        '#letter_cp_id',
+        select_tag('letter.cp_id', [], value_key => 'cp_id', title_key => 'full_name')
+      )
+      ->render;
+  }
+
+  my $contacts = $letter->customer_vendor->contacts;
+
+  my $default;
+  if (   $letter->contact
+      && $letter->contact->cp_cv_id
+      && $letter->contact->cp_cv_id == $letter->customer_vendor_id) {
+    $default = $letter->contact->cp_id;
+  } else {
+    $default = '';
+  }
+
+  $self->js
+    ->replaceWith(
+      '#letter_cp_id',
+      select_tag('letter.cp_id', $contacts, default => $default, value_key => 'cp_id', title_key => 'full_name')
+    )
+    ->render;
+}
+
+sub action_save_letter_draft {
+  my ($self, %params) = @_;
+
+  $self->check_letter;
+
+  my $letter_draft = SL::DB::LetterDraft->new_from_letter($self->_update);
+
+  if (!$letter_draft->save) {
+    flash('error', t8('There was an error saving the letter draft'));
+    return $self->_display;
+  }
+
+  flash('info', t8('Draft for this Letter saved!'));
+
+  $self->_display;
+}
+
+sub action_delete {
+  my ($self, %params) = @_;
+
+  if (!$self->letter->delete) {
+    flash('error', t8('An error occurred. Letter could not be deleted.'));
+    return $self->action_update;
+  }
+
+  flash_later('info', t8('Letter deleted'));
+  $self->redirect_to(action => 'list');
+}
+
+sub action_delete_letter_drafts {
+  my ($self, %params) = @_;
+
+  my @ids =  grep { /^checked_(.*)/ && $::form->{$_} } keys %$::form;
+
+  SL::DB::Manager::LetterDraft->delete_all(query => [ ids => \@ids ]) if @ids;
+
+  $self->redirect_to(action => 'add');
+}
+
+sub action_list {
+  my ($self, %params) = @_;
+
+  $self->setup_list_action_bar;
+  $self->make_filter_summary;
+  $self->prepare_report;
+
+  my $letters = $self->models->get;
+  $self->report_generator_list_objects(report => $self->{report}, objects => $letters);
+
+}
+
+sub action_print_letter {
+  my ($self, %params) = @_;
+
+  my $letter = $self->_update;
+
+  my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
+    name        => 'letter',
+    printer_id  => $::form->{printer_id},
+    language_id => $::form->{language_id},
+    formname    => 'letter',
+    format      => 'pdf',
+  );
+
+  if (!defined $template_file) {
+    $::form->error($::locale->text('Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.', join ', ', map { "'$_'"} @template_files));
+  }
+
+  my %result;
+  eval {
+    %result = SL::Template::LaTeX->parse_and_create_pdf(
+      $template_file,
+      SELF          => $self,
+      FORM          => $::form,
+      letter        => $letter,
+      template_meta => {
+        formname  => 'letter',
+        language  => SL::DB::Manager::Language->find_by_or_create(id => $::form->{language_id}*1),
+        extension => 'pdf',
+        format    => $::form->{format},
+        media     => $::form->{media},
+        printer   => SL::DB::Manager::Printer->find_by_or_create(id => $::form->{printer_id} || undef),
+        today     => DateTime->today,
+      },
+    );
+
+    die $result{error} if $result{error};
+
+    $::form->{type}         = 'letter';
+    $::form->{formname}     = 'letter';
+    $::form->{letternumber} = $letter->letternumber;
+    my $attachment_name     = $::form->generate_attachment_filename;
+
+    if ($::instance_conf->get_webdav_documents) {
+      my $webdav_file = SL::Webdav::File->new(
+        filename => $attachment_name,
+        webdav   => SL::Webdav->new(
+          type   => 'letter',
+          number => $letter->letternumber,
+        ),
+      );
+
+      $webdav_file->store(file => $result{file_name});
+    }
+
+    if ($::instance_conf->get_doc_storage) {
+      my %save_params = (object_id    => $letter->id,
+                         object_type  => 'letter',
+                         mime_type    => 'application/pdf',
+                         source       => 'created',
+                         file_type    => 'document',
+                         file_name    => $attachment_name,
+                         file_path    => $result{file_name});
+      SL::File->save(%save_params);
+    }
+
+    # set some form defaults for printing webdav copy variables
+    if ( $::form->{media} eq 'email') {
+      my $mail             = Mailer->new;
+      my $signature        = $::myconfig{signature};
+      $mail->{$_}          = $params{email}->{$_} for qw(to cc subject message bcc);
+      $mail->{from}        = qq|"$::myconfig{name}" <$::myconfig{email}>|;
+      $mail->{attachments} = [{ path => $result{file_name},
+                                name => $params{email}->{attachment_filename} }];
+      $mail->{message}    .=  "\n-- \n$signature";
+      $mail->{message}     =~ s/\r//g;
+      $mail->{record_id}   =  $letter->id;
+      $mail->send;
+      unlink $result{file_name};
+
+      flash_later('info', t8('The email has been sent.'));
+      $self->redirect_to(action => 'edit', 'letter.id' => $letter->id);
+
+      return 1;
+    }
+
+    if (!$::form->{printer_id} || $::form->{media} eq 'screen') {
+      $self->send_file($result{file_name}, name => $attachment_name);
+      unlink $result{file_name};
+
+      return 1;
+    }
+
+    my $printer = SL::DB::Printer->new(id => $::form->{printer_id})->load;
+    $printer->print_document(
+      copies    => $::form->{copies},
+      file_name => $result{file_name},
+    );
+
+    unlink $result{file_name};
+
+    flash_later('info', t8('The documents have been sent to the printer \'#1\'.', $printer->printer_description));
+    $self->redirect_to(action => 'edit', 'letter.id' => $letter->id, media => 'printer', printer_id => $::form->{printer_id});
+    1;
+  } or do {
+    unlink $result{file_name} if $result{file_name};
+    $::form->error(t8("Creating the PDF failed:") . " " . $@);
+  };
+}
+
+sub action_update {
+  my ($self, $name_selected) = @_;
+
+  $self->_display(
+    letter => $self->_update,
+  );
+}
+
+sub action_skip_draft {
+  my ($self) = @_;
+  $self->action_add(skip_drafts => 1);
+}
+
+sub action_delete_drafts {
+  my ($self) = @_;
+
+  my @ids = @{ $::form->{ids} || [] };
+  SL::DB::Manager::LetterDraft->delete_all(where => [ id => \@ids ]) if @ids;
+
+  $self->action_add(skip_drafts => 1);
+}
+
+sub action_send_email {
+  my ($self) = @_;
+
+  $::form->{media} = 'email';
+  $self->action_print_letter(email => $::form->{email_form});
+}
+
+### internal methods
+
+sub _display {
+  my ($self, %params) = @_;
+
+  $::request->{layout}->use_javascript("${_}.js") for qw(ckeditor/ckeditor ckeditor/adapters/jquery kivi.Letter kivi.SalesPurchase kivi.File);
+
+  my $letter = $self->letter;
+
+ $params{title} ||= t8('Edit Letter');
+
+  $::form->{type}             = 'letter';   # needed for print_options
+  $::form->{vc}               = $letter->is_sales ? 'customer' : 'vendor'; # needs to be for _get_contacts...
+
+  $::form->{language_id} ||= $params{language_id};
+  $::form->{languages}   ||= SL::DB::Manager::Language->get_all_sorted;
+  $::form->{printers}      = SL::DB::Manager::Printer->get_all_sorted;
+
+  $self->setup_display_action_bar;
+  $self->render('letter/edit',
+    %params,
+    TCF           => [ map { key => $_, value => t8(ucfirst $_) }, TEXT_CREATED_FOR_VALUES() ],
+    PCF           => [ map { key => $_, value => t8(ucfirst $_) }, PAGE_CREATED_FOR_VALUES() ],
+    letter        => $letter,
+    employees     => $self->all_employees,
+    print_options => SL::Helper::PrintOptions->get_print_options (
+      options => { no_postscript   => 1,
+                   no_opendocument => 1,
+                   no_html         => 1,
+                   no_queue        => 1,
+                   show_headers    => 1,
+                 }),
+
+  );
+}
+
+sub _update {
+  my ($self, %params) = @_;
+
+  my $letter = $self->letter;
+
+  $self->check_date;
+  $self->set_greetings;
+
+  return $letter;
+}
+
+sub prepare_report {
+  my ($self) = @_;
+
+  my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
+  $self->{report} = $report;
+
+  my @columns  = qw(date subject letternumber customer_id vendor_id contact date);
+  my @sortable = qw(date subject letternumber customer_id vendor_id contact date);
+
+  my %column_defs = (
+    date                  => { text => t8('Date'),         sub => sub { $_[0]->date_as_date } },
+    subject               => { text => t8('Subject'),      sub => sub { $_[0]->subject },
+                               obj_link => sub { $self->url_for(action => 'edit', 'letter.id' => $_[0]->id, callback => $self->models->get_callback) }  },
+    letternumber          => { text => t8('Letternumber'), sub => sub { $_[0]->letternumber },
+                               obj_link => sub { $self->url_for(action => 'edit', 'letter.id' => $_[0]->id, callback => $self->models->get_callback) }  },
+    customer_id           => { text => t8('Customer'),      sub => sub { SL::DB::Manager::Customer->find_by_or_create(id => $_[0]->customer_id)->displayable_name }, visible => $self->is_sales },
+    vendor_id             => { text => t8('Vendor'),        sub => sub { SL::DB::Manager::Vendor->find_by_or_create(id => $_[0]->vendor_id)->displayable_name }, visible => !$self->is_sales},
+    contact               => { text => t8('Contact'),       sub => sub { $_[0]->contact ? $_[0]->contact->full_name : '' } },
+  );
+
+  $column_defs{$_}{text} = $sort_columns{$_} for keys %column_defs;
+
+  $report->set_options(
+    std_column_visibility => 1,
+    controller_class      => 'Letter',
+    output_format         => 'HTML',
+    title                 => t8('Letters'),
+    allow_pdf_export      => 1,
+    allow_csv_export      => 1,
+  );
+
+  $report->set_columns(%column_defs);
+  $report->set_column_order(@columns);
+  $report->set_export_options(qw(list filter is_sales));
+  $report->set_options_from_form;
+
+  $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
+  $self->models->add_additional_url_params(is_sales => $self->is_sales);
+  $self->models->finalize;
+  $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
+
+  $report->set_options(
+    raw_top_info_text    => $self->render('letter/report_top',    { output => 0 }),
+    raw_bottom_info_text => $self->render('letter/report_bottom', { output => 0 }, models => $self->models),
+    attachment_basename  => t8('letters_list') . strftime('_%Y%m%d', localtime time),
+  );
+}
+
+sub make_filter_summary {
+  my ($self) = @_;
+
+  my $filter = $::form->{filter} || {};
+  my @filter_strings;
+
+  my $employee = $filter->{employee_id} ? SL::DB::Employee->new(id => $filter->{employee_id})->load->name : '';
+  my $salesman = $filter->{salesman_id} ? SL::DB::Employee->new(id => $filter->{salesman_id})->load->name : '';
+
+  my @filters = (
+    [ $filter->{"letternumber:substr::ilike"},  t8('Number')     ],
+    [ $filter->{"subject:substr::ilike"},       t8('Subject')    ],
+    [ $filter->{"body:substr::ilike"},          t8('Body')       ],
+    [ $filter->{"date:date::ge"},               t8('From Date')  ],
+    [ $filter->{"date:date::le"},               t8('To Date')    ],
+    [ $employee,                                t8('Employee')   ],
+    [ $salesman,                                t8('Salesman')   ],
+  );
+
+  my %flags = (
+  );
+  my @flags = map { $flags{$_} } @{ $filter->{part}{type} || [] };
+
+  for (@flags) {
+    push @filter_strings, $_ if $_;
+  }
+  for (@filters) {
+    push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
+  }
+
+  $self->{filter_summary} = join ', ', @filter_strings;
+}
+
+sub load_letter_draft {
+  my ($self, %params) = @_;
+
+  return 0 if $params{skip_drafts};
+
+  my $letter_drafts = SL::DB::Manager::LetterDraft->get_all(
+    query => [
+      SL::DB::Manager::Letter->is_sales_filter($self->is_sales),
+    ]
+  );
+
+  return unless @$letter_drafts;
+
+  $self->setup_load_letter_draft_action_bar;
+  $self->render('letter/load_drafts',
+    title         => t8('Letter Draft'),
+    LETTER_DRAFTS => $letter_drafts,
+  );
+
+  return 1;
+}
+
+sub check_date {
+  my ($self) = @_;
+  my $letter = $self->letter;
+
+  return unless $letter;
+  return if $letter->date;
+
+  $letter->date(DateTime->today)
+}
+
+sub check_letter {
+  my ($self, $letter) = @_;
+
+  $letter ||= $self->letter;
+
+  my $error;
+
+  if (!$letter->subject) {
+    flash('error', t8('The subject is missing.'));
+    $error = 1;
+  }
+  if (!$letter->body) {
+    flash('error', t8('The body is missing.'));
+    $error = 1;
+  }
+  if (!$letter->employee_id) {
+    flash('error', t8('The employee is missing.'));
+    $error = 1;
+  }
+
+  return !$error;
+}
+
+sub check_number {
+  my ($self, $letter) = @_;
+
+  $letter ||= $self->letter;
+
+  return if $letter->letternumber;
+
+  $letter->letternumber(SL::TransNumber->new(type => 'letter', id => $self->{id}, number => $self->{letternumber})->create_unique);
+}
+
+sub set_greetings {
+  my ($self) = @_;
+  my $letter = $self->letter;
+
+  return unless $letter;
+  return if $letter->greeting;
+
+  $letter->greeting(t8('Dear Sir or Madam,'));
+}
+
+sub init_letter {
+  my ($self) = @_;
+
+  my $letter      = SL::DB::Manager::Letter->find_by_or_create(id => $::form->{letter}{id} || 0)
+                                           ->assign_attributes(%{ $::form->{letter} });
+
+  if ($letter->cp_id) {
+#     $letter->customer_vendor_id($letter->contact->cp_cv_id);
+      # contacts don't have language_id yet
+#     $letter->greeting(GenericTranslations->get(
+#       translation_type => 'greetings::' . ($letter->contact->cp_gender eq 'f' ? 'female' : 'male'),
+#       language_id      => $letter->contact->language_id,
+#       allow_fallback   => 1
+#     ));
+  }
+
+  $self->is_sales($letter->is_sales) if $letter->id;
+
+  $letter;
+}
+
+sub init_models {
+  my ($self) = @_;
+
+  SL::Controller::Helper::GetModels->new(
+    controller   => $self,
+    model        => 'Letter',
+    query        => [
+      SL::DB::Manager::Letter->is_sales_filter($self->is_sales),
+    ],
+    sorted       => \%sort_columns,
+    with_objects => [ 'contact', 'salesman', 'employee' ],
+  );
+}
+
+sub init_all_employees {
+  SL::DB::Manager::Employee->get_all(query => [ deleted => 0 ]);
+}
+
+sub init_webdav_objects {
+  my ($self) = @_;
+
+  return [] if !$self->letter || !$self->letter->letternumber || !$::instance_conf->get_webdav;
+
+  my $webdav = SL::Webdav->new(
+    type     => 'letter',
+    number   => $self->letter->letternumber,
+  );
+
+  my @all_objects = $webdav->get_all_objects;
+
+  return [ map {
+    +{ name => $_->filename,
+       type => t8('File'),
+       link => File::Spec->catfile($_->full_filedescriptor),
+     }
+  } @all_objects ];
+}
+
+sub init_is_sales {
+  die 'is_sales must be set' unless defined $::form->{is_sales};
+  $::form->{is_sales};
+}
+
+sub check_auth_edit {
+  $::form->{is_sales} ? $::auth->assert('sales_letter_edit')
+                      : $::auth->assert('purchase_letter_edit');
+}
+
+sub check_auth_report {
+  $::form->{is_sales} ? $::auth->assert('sales_letter_report')
+                      : $::auth->assert('purchase_letter_report');
+}
+
+sub setup_load_letter_draft_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      link => [
+        t8('Skip'),
+        link      => $self->url_for(action => 'skip_draft', is_sales => $self->is_sales),
+        accesskey => 'enter',
+      ],
+      action => [
+        t8('Delete'),
+        submit  => [ '#form', { action => 'delete_drafts' } ],
+        checks  => [ [ 'kivi.check_if_entries_selected', '[name="ids[+]"]' ] ],
+        confirm => t8('Do you really want to delete this draft?'),
+      ],
+    );
+  }
+}
+
+sub setup_display_action_bar {
+  my ($self, %params) = @_;
+
+  my $vc = $self->is_sales ? 'customer' : 'vendor'; # needed for show_email_dialog
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#form', { action => 'Letter/update' } ],
+        accesskey => 'enter',
+      ],
+
+      combobox => [
+        action => [
+          t8('Save'),
+          submit => [ '#form', { action => 'Letter/save' } ],
+        ],
+        action => [
+          t8('Save Draft'),
+          submit => [ '#form', { action => 'Letter/save_letter_draft' } ],
+        ],
+      ], # end of combobox "Save"
+
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => 'Letter/delete' } ],
+        confirm  => t8('Are you sure you want to delete this letter?'),
+        disabled => !$self->letter->id ? t8('The object has not been saved yet.') : undef,
+      ],
+
+      combobox => [
+        action => [ t8('Export') ],
+        action => [
+          t8('Print'),
+          call     => [ 'kivi.SalesPurchase.show_print_dialog', 'Letter/print_letter' ],
+          disabled => !$self->letter->id ? t8('The object has not been saved yet.') : undef,
+        ],
+        action => [
+          t8('E-mail'),
+          call     => [ 'kivi.SalesPurchase.show_email_dialog', 'Letter/send_email', $vc, '#letter_' . $vc . '_id' ],
+          disabled => !$self->letter->id ? t8('The object has not been saved yet.') : undef,
+        ],
+      ],
+    );
+  }
+}
+
+sub setup_list_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#search_form', { action => 'Letter/list' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::Letter - Letters CRUD and printing
+
+=head1 DESCRIPTION
+
+Simple letter CRUD controller with drafting capabilities.
+
+copy to webdav is crap
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut
index 24413d7..1c18f34 100644 (file)
@@ -32,6 +32,7 @@ sub action_show {
     buchungsgruppe    => 1,
   };
 
+  $self->setup_show_action_bar;
   $self->render('liquidity_projection/show', title => t8('Liquidity projection'));
 }
 
@@ -78,4 +79,18 @@ sub iso_to_display {
   $::locale->reformat_date({ dateformat => 'yyyy-mm-dd' }, $date, $::myconfig{dateformat});
 }
 
+sub setup_show_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Show'),
+        submit    => [ '#filter_form', { action => 'LiquidityProjection/show' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
 1;
index da2c0bf..adcc8f0 100644 (file)
@@ -13,6 +13,7 @@ use SL::DB::AuthUser;
 use SL::DB::Employee;
 use SL::Locale::String qw(t8);
 use SL::User;
+use SL::Version;
 
 use Rose::Object::MakeMethods::Generic (
   'scalar --get_set_init' => [ qw(clients default_client_id) ],
@@ -57,7 +58,9 @@ sub action_login {
 
   %::myconfig      = $login ? $::auth->read_user(login => $login) : ();
   $::locale        = Locale->new($::myconfig{countrycode}) if $::myconfig{countrycode};
-  SL::Dispatcher::AuthHandler::User->new->handle(countrycode => $::myconfig{countrycode});
+  my $auth_result  = SL::Dispatcher::AuthHandler::User->new->handle(callback => $::form->{callback});
+
+  $::dispatcher->end_request unless $auth_result;
 
   $::request->layout(SL::Layout::Dispatcher->new(style => $::myconfig{menustyle}));
 
@@ -79,7 +82,7 @@ sub action_login {
   }
 
   # Database update available?
-  ::end_of_request() if User::LOGIN_DBUPDATE_AVAILABLE() == $result;
+  $::dispatcher->end_request if User::LOGIN_DBUPDATE_AVAILABLE() == $result;
 
   # Other login errors.
   if (User::LOGIN_OK() != $result) {
@@ -162,7 +165,9 @@ sub error_state {
 }
 
 sub set_layout {
-  $::request->{layout} = SL::Layout::Dispatcher->new(style => 'login');
+  $::request->{layout} = $::request->is_mobile
+    ? SL::Layout::Dispatcher->new(style => 'mobile_login')
+    : SL::Layout::Dispatcher->new(style => 'login');
 }
 
 sub init_clients {
@@ -178,7 +183,7 @@ sub init_default_client_id {
 sub show_login_form {
   my ($self, %params) = @_;
 
-  $self->render('login_screen/user_login', %params, version => $::form->read_version);
+  $self->render('login_screen/user_login', %params, version => SL::Version->get_version, callback => $::form->{callback});
 }
 
 1;
diff --git a/SL/Controller/MassDeliveryOrderPrint.pm b/SL/Controller/MassDeliveryOrderPrint.pm
new file mode 100644 (file)
index 0000000..5cb86e1
--- /dev/null
@@ -0,0 +1,284 @@
+package SL::Controller::MassDeliveryOrderPrint;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use File::Slurp ();
+use File::Copy;
+use List::MoreUtils qw(uniq);
+use List::Util qw(first);
+
+use SL::Controller::Helper::GetModels;
+use SL::BackgroundJob::MassDeliveryOrderPrinting;
+use SL::DB::Customer;
+use SL::DB::DeliveryOrder;
+use SL::DB::Order;
+use SL::DB::Part;
+use SL::DB::Printer;
+use SL::Helper::MassPrintCreatePDF qw(:all);
+use SL::Helper::CreatePDF qw(:all);
+use SL::Helper::File qw(store_pdf append_general_pdf_attachments doc_storage_enabled);
+use SL::Helper::PrintOptions;
+use SL::Helper::Flash;
+use SL::Locale::String;
+use SL::SessionFile;
+use SL::System::TaskServer;
+
+use Rose::Object::MakeMethods::Generic
+(
+  'scalar --get_set_init' => [ qw(delivery_order_models delivery_order_ids printers filter_summary temp_files) ],
+);
+
+__PACKAGE__->run_before('setup');
+
+#
+# actions
+#
+sub action_list_delivery_orders {
+  my ($self) = @_;
+
+  my $show = ($::form->{noshow}?0:1);
+  delete $::form->{noshow};
+
+  if ($::form->{ids}) {
+    my $key = 'MassDeliveryOrderPrint::ids-' . $::form->{ids};
+    $self->delivery_order_ids($::auth->get_session_value($key) || []);
+    $self->delivery_order_models->add_additional_url_params(ids => $::form->{ids});
+  }
+
+  my %selected_ids = map { +($_ => 1) } @{ $self->delivery_order_ids };
+
+  my $pr = SL::DB::Manager::Printer->find_by(
+      printer_description => $::locale->text("sales_delivery_order_printer"));
+  if ($pr ) {
+      $::form->{printer_id} = $pr->id;
+  }
+  $self->render('mass_delivery_order_print/list_delivery_orders',
+                title        => $::locale->text('Print delivery orders'),
+                nowshow      => $show,
+                print_opt    => $self->print_options(hide_language_id => 1),
+                selected_ids => \%selected_ids);
+}
+
+sub action_mass_mdo_download {
+  my ($self) = @_;
+  my $job    = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
+
+  my $sfile  = SL::SessionFile->new($job->data_as_hash->{pdf_file_name}, mode => 'r');
+  die $! if !$sfile->fh;
+
+  my $merged_pdf = do { local $/; my $fh = $sfile->fh; <$fh> };
+  $sfile->fh->close;
+
+  my $file_name =  t8('Sales Delivery Orders') . '-' . DateTime->now_local->strftime('%Y%m%d%H%M%S') . '.pdf';
+  $file_name    =~ s{[^\w\.]+}{_}g;
+
+  return $self->send_file(
+    \$merged_pdf,
+    type => 'application/pdf',
+    name => $file_name,
+  );
+}
+
+sub action_mass_mdo_status {
+  my ($self) = @_;
+  $::lxdebug->enter_sub();
+  eval {
+    my $job = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
+    my $html = $self->render('mass_delivery_order_print/_print_status', { output => 0 }, job => $job);
+
+    $self->js->html('#mass_print_dialog', $html);
+    if ( $job->data_as_hash->{status} == SL::BackgroundJob::MassDeliveryOrderPrinting->DONE() ) {
+      foreach my $dorder_id (@{$job->data_as_hash->{record_ids}}) {
+        $self->js->prop('#multi_id_id_'.$dorder_id,'checked',0);
+      }
+      $self->js->prop('#multi_all','checked',0);
+      $self->js->run('kivi.MassDeliveryOrderPrint.massConversionFinished');
+    }
+    1;
+  } or do {
+    $self->js->run('kivi.MassDeliveryOrderPrint.massConversionFinished')
+      ->run('kivi.MassDeliveryOrderPrint.massConversionFinishProcess')
+      ->flash('error', t8('No such job #1 in the database.',$::form->{job_id}));
+  };
+  $self->js->render;
+
+  $::lxdebug->leave_sub();
+}
+
+sub action_mass_mdo_print {
+  my ($self) = @_;
+  $::lxdebug->enter_sub();
+
+  eval {
+    my @do_ids = @{ $::form->{id} || [] };
+    push @do_ids, map { $::form->{"trans_id_$_"} } grep { $::form->{"multi_id_$_"} } (1..$::form->{rowcount});
+
+    my @delivery_orders = map { SL::DB::DeliveryOrder->new(id => $_)->load } @do_ids;
+
+    if (!@delivery_orders) {
+      $self->js->flash('error', t8('No delivery orders have been selected.'));
+    } else {
+      my $job              = SL::DB::BackgroundJob->new(
+        type               => 'once',
+        active             => 1,
+        package_name       => 'MassDeliveryOrderPrinting',
+
+      )->set_data(
+        record_ids         => [ @do_ids ],
+        printer_id         => $::form->{printer_id},
+        formname           => $::form->{formname},
+        format             => $::form->{format},
+        media              => $::form->{media},
+        bothsided          => ($::form->{bothsided}?1:0),
+        copies             => $::form->{copies},
+        status             => SL::BackgroundJob::MassDeliveryOrderPrinting->WAITING_FOR_EXECUTION(),
+        num_created        => 0,
+        num_printed        => 0,
+        printed_ids        => [ ],
+        conversion_errors  => [ ],
+        print_errors       => [ ],
+        session_id         => $::auth->get_session_id,
+
+      )->update_next_run_at;
+
+      SL::System::TaskServer->new->wake_up;
+      my $html = $self->render('mass_delivery_order_print/_print_status', { output => 0 }, job => $job);
+
+      $self->js
+        ->html('#mass_print_dialog', $html)
+        ->run('kivi.MassDeliveryOrderPrint.massConversionPopup')
+        ->run('kivi.MassDeliveryOrderPrint.massConversionStarted');
+    }
+    1;
+  } or do {
+    my $errstr = $@;
+    my $htmlstr = $errstr;
+    $htmlstr =~ s/\n/<br>/g;
+    $self->js->flash('error', t8('Document generating failed. Please check Templates an LateX !'));
+    $self->js->flash_detail('error', $htmlstr);
+  };
+  $self->js->render;
+  $::lxdebug->leave_sub();
+}
+
+sub action_downloadpdf {
+  my ($self) = @_;
+  $::lxdebug->enter_sub();
+  if ( $::form->{filename} ) {
+    my $content = scalar File::Slurp::read_file($::form->{filename});
+    my $file_name = $::form->get_formname_translation($::form->{formname}) .
+      '-' . DateTime->now_local->strftime('%Y%m%d%H%M%S') . '.pdf';
+    $file_name    =~ s{[^\w\.]+}{_}g;
+
+    unlink($::form->{filename});
+
+    return $self->send_file(
+      \$content,
+      type => 'application/pdf',
+      name => $file_name,
+    );
+  } else {
+    flash('error', t8('No filename exists!'));
+  }
+  $::lxdebug->leave_sub();
+}
+
+#
+# filters
+#
+
+sub init_printers { SL::DB::Manager::Printer->get_all_sorted }
+sub init_delivery_order_ids { [] }
+sub init_temp_files { [] }
+
+sub init_delivery_order_models {
+  my ($self)             = @_;
+  my @delivery_order_ids = @{ $self->delivery_order_ids };
+
+  SL::Controller::Helper::GetModels->new(
+    controller   => $_[0],
+    model        => 'DeliveryOrder',
+    (paginated   => 0,) x !!@delivery_order_ids,
+    sorted       => {
+      _default     => {
+        by           => 'reqdate',
+        dir          => 0,
+      },
+      customer     => t8('Customer'),
+      donumber     => t8('Delivery Order Number'),
+      employee     => t8('Employee'),
+      ordnumber    => t8('Order Number'),
+      reqdate      => t8('Delivery Date'),
+      transdate    => t8('Date'),
+    },
+    with_objects => [ qw(customer employee) ],
+    query        => [
+      '!customer_id' => undef,
+      or             => [ closed    => undef, closed    => 0 ],
+      (id            => \@delivery_order_ids) x !!@delivery_order_ids,
+    ],
+  );
+}
+
+sub init_filter_summary {
+  my ($self) =@_;
+  my $filter = $::form->{filter} || { customer => {}, shipto => {}, };
+
+  my @filters;
+  push @filters, t8('Customer')                              . ' ' . $filter->{customer}->{'name:substr::ilike'}     if $filter->{customer}->{'name:substr::ilike'};
+  push @filters, t8('Shipping address (name)')               . ' ' . $filter->{shipto}->{'shiptoname:substr::ilike'} if $filter->{shipto}->{'shiptoname:substr::ilike'};
+  push @filters, t8('Delivery Date') . ' ' . t8('From Date') . ' ' . $filter->{'reqdate:date::ge'}                   if $filter->{'reqdate:date::ge'};
+  push @filters, t8('Delivery Date') . ' ' . t8('To Date')   . ' ' . $filter->{'reqdate:date::le'}                   if $filter->{'reqdate:date::le'};
+
+  return join ', ', @filters;
+}
+
+sub setup {
+  my ($self) = @_;
+  $::auth->assert('sales_delivery_order_edit');
+  $::request->layout->use_javascript("${_}.js")  for qw(kivi.MassDeliveryOrderPrint);
+}
+
+
+sub generate_documents {
+  my ($self, @delivery_orders) = @_;
+
+  my %pdf_params = (
+    'documents'       => \@delivery_orders ,
+    'variables'       => {
+      'type'            => $::form->{type},
+      'formname'        => $::form->{formname},
+      'language_id'     => '',
+      'format'          => 'pdf',
+      'media'           => 'file',
+      'printer_id'      => $::form->{printer_id},
+    });
+
+  my ($temp_fh, $outname) = File::Temp::tempfile(
+    'kivitendo-outfileXXXXXX',
+    SUFFIX => '.pdf',
+    DIR    => $::lx_office_conf{paths}->{userspath},
+    UNLINK => 0,
+  );
+  close $temp_fh;
+
+  my @pdf_file_names = $self->create_pdfs(%pdf_params);
+  my $fcount = scalar(@pdf_file_names);
+  if ( $fcount < 2 ) {
+    copy($pdf_file_names[0],$outname);
+  } else {
+    if ( !$self->merge_pdfs(file_names => \@pdf_file_names, out_path => $outname, bothsided => $::form->{bothsided} )) {
+      $::lxdebug->leave_sub();
+      return 0;
+    }
+  }
+  foreach my $dorder (@delivery_orders) {
+    $self->js->prop('#multi_id_id_'.$dorder->id,'checked',0);
+  }
+  $self->js->prop('#multi_all','checked',0);
+  return $outname;
+}
+
+1;
index 7fc3388..626a663 100644 (file)
@@ -13,15 +13,17 @@ use SL::Controller::Helper::GetModels;
 use SL::DB::DeliveryOrder;
 use SL::DB::Order;
 use SL::DB::Printer;
+use SL::Helper::MassPrintCreatePDF qw(:all);
 use SL::Helper::CreatePDF qw(:all);
+use SL::Helper::File qw(store_pdf append_general_pdf_attachments doc_storage_enabled);
 use SL::Helper::Flash;
 use SL::Locale::String;
 use SL::SessionFile;
+use SL::ARAP;
 use SL::System::TaskServer;
-
 use Rose::Object::MakeMethods::Generic
 (
-  'scalar --get_set_init' => [ qw(invoice_models invoice_ids sales_delivery_order_models printers default_printer_id today) ],
+  'scalar --get_set_init' => [ qw(invoice_models invoice_ids sales_delivery_order_models printers default_printer_id today all_businesses) ],
 );
 
 __PACKAGE__->run_before('setup');
@@ -39,7 +41,7 @@ sub action_list_sales_delivery_orders {
 
   # if a filter is choosen, the filter info should be visible
   $self->make_filter_summary;
-  $self->sales_delivery_order_models->get;
+  $self->setup_list_sales_delivery_orders_action_bar(show_creation_buttons => $show, num_rows => scalar(@{ $self->sales_delivery_order_models->get }));
   $self->render('mass_invoice_create_print_from_do/list_sales_delivery_orders',
                 noshow  => $show,
                 title   => $::locale->text('Open sales delivery orders'));
@@ -56,27 +58,66 @@ sub action_create_invoices {
   }
 
   my $db = SL::DB::Invoice->new->db;
+  my $dbh = $db->dbh;
+  my @invoices;
+  my @already_closed_delivery_orders;
 
-  if (!$db->do_transaction(sub {
-    my @invoices;
+  if (!$db->with_transaction(sub {
     foreach my $id (@sales_delivery_order_ids) {
       my $delivery_order    = SL::DB::DeliveryOrder->new(id => $id)->load;
 
-      my $invoice = $delivery_order->convert_to_invoice() || die $db->error;
-      push @invoices, $invoice;
-    }
+      # Only process open delivery orders. In this list should only be open
+      # delivery orders, but if the user clicked browser back, a new creation
+      # of invoices for delivery orders which are closed now can be triggered.
+      # Prevent this.
+      if ($delivery_order->closed) {
+        push @already_closed_delivery_orders, $delivery_order;
+
+      } else {
+        my $invoice = $delivery_order->convert_to_invoice() || die $db->error;
 
-    my $key = sprintf('%d-%d', Time::HiRes::gettimeofday());
-    $::auth->set_session_value("MassInvoiceCreatePrint::ids-${key}" => [ map { $_->id } @invoices ]);
+        ARAP->close_orders_if_billed('dbh'     => $dbh,
+                                     'arap_id' => $invoice->id,
+                                     'table'   => 'ar',);
 
-    flash_later('info', t8('The invoices have been created. They\'re pre-selected below.'));
-    $self->redirect_to(action => 'list_invoices', ids => $key);
+        push @invoices, $invoice;
+      }
+    }
 
     1;
   })) {
     $::lxdebug->message(LXDebug::WARN(), "Error: " . $db->error);
     $::form->error($db->error);
   }
+
+  foreach my $invoice( @invoices ) {
+    # update shop status
+    my @linked_shop_orders = $invoice->linked_records(
+      from      => 'ShopOrder',
+      via       => [ 'DeliveryOrder', 'Order' ],
+    );
+    #if (scalar @linked_shop_orders[0][0] >= 1){
+      #do update
+    my $shop_order = $linked_shop_orders[0][0];
+    if ($shop_order){
+    require SL::Shop;
+      my $shop_config = SL::DB::Manager::Shop->get_first( query => [ id => $shop_order->shop_id ] );
+      my $shop = SL::Shop->new( config => $shop_config );
+      $shop->connector->set_orderstatus($shop_order->shop_trans_id, "completed");
+    }
+  }
+
+  my $key = sprintf('%d-%d', Time::HiRes::gettimeofday());
+  $::auth->set_session_value("MassInvoiceCreatePrint::ids-${key}" => [ map { $_->id } @invoices ]);
+
+  if (@already_closed_delivery_orders) {
+    my $dos_list = join ' ', map { $_->donumber } @already_closed_delivery_orders;
+    flash_later('error', t8('The following delivery orders could not be processed because they are already closed: #1', $dos_list));
+  }
+
+  flash_later('info', t8('The invoices have been created. They\'re pre-selected below.')) if @invoices;
+
+  $self->redirect_to(action => 'list_invoices', ids => $key);
 }
 
 sub action_list_invoices {
@@ -88,6 +129,11 @@ sub action_list_invoices {
   if ($::form->{ids}) {
     my $key = 'MassInvoiceCreatePrint::ids-' . $::form->{ids};
     $self->invoice_ids($::auth->get_session_value($key) || []);
+
+    # Prevent models->get to retrieve any invoices if session key is there
+    # but no ids are given.
+    $self->invoice_ids([0]) if !@{$self->invoice_ids};
+
     $self->invoice_models->add_additional_url_params(ids => $::form->{ids});
   }
 
@@ -95,6 +141,8 @@ sub action_list_invoices {
 
   $::form->{printer_id} ||= $self->default_printer_id;
 
+  $self->setup_list_invoices_action_bar(num_rows => scalar(@{ $self->invoice_models->get }));
+
   $self->render('mass_invoice_create_print_from_do/list_invoices',
                 title        => $::locale->text('Open invoice'),
                 noshow       => $show,
@@ -110,7 +158,7 @@ sub action_print {
     return $self->redirect_to(action => 'list_invoices');
   }
 
-  $self->download_or_print_documents(printer_id => $::form->{printer_id}, invoices => \@invoices);
+  $self->download_or_print_documents(printer_id => $::form->{printer_id}, invoices => \@invoices, bothsided => $::form->{bothsided});
 }
 
 sub action_create_print_all_start {
@@ -130,6 +178,7 @@ sub action_create_print_all_start {
     record_ids         => [ map { $_->id } @records[0..$num - 1] ],
     printer_id         => $::form->{printer_id},
     copy_printer_id    => $::form->{copy_printer_id},
+    bothsided          => ($::form->{bothsided}?1:0),
     transdate          => $::form->{transdate},
     status             => SL::BackgroundJob::MassRecordCreationAndPrinting->WAITING_FOR_EXECUTION(),
     num_created        => 0,
@@ -137,6 +186,7 @@ sub action_create_print_all_start {
     invoice_ids        => [ ],
     conversion_errors  => [ ],
     print_errors       => [ ],
+    session_id         => $::auth->get_session_id,
 
   )->update_next_run_at;
 
@@ -171,7 +221,7 @@ sub action_create_print_all_download {
   $sfile->fh->close;
 
   my $type      = 'Invoices';
-  my $file_name =  t8($type) . '-' . DateTime->today_local->strftime('%Y%m%d%H%M%S') . '.pdf';
+  my $file_name =  t8($type) . '-' . DateTime->now_local->strftime('%Y%m%d%H%M%S') . '.pdf';
   $file_name    =~ s{[^\w\.]+}{_}g;
 
   return $self->send_file(
@@ -186,6 +236,7 @@ sub action_create_print_all_download {
 #
 
 sub init_printers { SL::DB::Manager::Printer->get_all_sorted }
+#sub init_att      { require SL::Controller::Attachments; SL::Controller::Attachments->new() }
 sub init_invoice_ids { [] }
 sub init_today         { DateTime->today_local }
 
@@ -208,7 +259,7 @@ sub _init_sales_delivery_order_models {
       },
       customer     => t8('Customer'),
       employee     => t8('Employee'),
-      transdate    => t8('Date'),
+      transdate    => t8('Delivery Order Date'),
       donumber     => t8('Delivery Order Number'),
       ordnumber     => t8('Order Number'),
     },
@@ -256,6 +307,10 @@ sub init_default_printer_id {
   return $pr ? $pr->id : undef;
 }
 
+sub init_all_businesses {
+  return SL::DB::Manager::Business->get_all_sorted;
+}
+
 sub setup {
   my ($self) = @_;
   $::auth->assert('invoice_edit');
@@ -267,30 +322,6 @@ sub setup {
 # helpers
 #
 
-sub create_pdfs {
-  my ($self, %params) = @_;
-
-  my @pdf_file_names;
-  foreach my $invoice (@{ $params{invoices} }) {
-    my %create_params = (
-      template  => $self->find_template(name => 'invoice', printer_id => $params{printer_id}),
-      variables => Form->new(''),
-      return    => 'file_name',
-      variable_content_types => { longdescription => 'html',
-                                  partnotes       => 'html',
-                                  notes           => 'html',}
-    );
-
-    $create_params{variables}->{$_} = $params{variables}->{$_} for keys %{ $params{variables} };
-
-    $invoice->flatten_to_form($create_params{variables}, format_amounts => 1);
-    $create_params{variables}->prepare_for_printing;
-
-    push @pdf_file_names, $self->create_pdf(%create_params);
-  }
-
-  return @pdf_file_names;
-}
 
 sub download_or_print_documents {
   my ($self, %params) = @_;
@@ -299,21 +330,21 @@ sub download_or_print_documents {
 
   eval {
     my %pdf_params = (
-      invoices        => $params{invoices},
-      printer_id      => $params{printer_id},
+      documents       => $params{invoices},
       variables       => {
         type        => 'invoice',
         formname    => 'invoice',
         format      => 'pdf',
         media       => $params{printer_id} ? 'printer' : 'file',
+        printer_id  => $params{printer_id},
       });
 
     @pdf_file_names = $self->create_pdfs(%pdf_params);
-    my $merged_pdf  = $self->merge_pdfs(file_names => \@pdf_file_names);
+    my $merged_pdf  = $self->merge_pdfs(file_names => \@pdf_file_names, bothsided => $params{bothsided});
     unlink @pdf_file_names;
 
     if (!$params{printer_id}) {
-      my $file_name =  t8("Invoices") . '-' . DateTime->today_local->strftime('%Y%m%d%H%M%S') . '.pdf';
+      my $file_name =  t8("Invoices") . '-' . DateTime->now_local->strftime('%Y%m%d%H%M%S') . '.pdf';
       $file_name    =~ s{[^\w\.]+}{_}g;
 
       return $self->send_file(
@@ -324,12 +355,7 @@ sub download_or_print_documents {
     }
 
     my $printer = SL::DB::Printer->new(id => $params{printer_id})->load;
-    my $command = SL::Template::create(type => 'ShellCommand', form => Form->new(''))->parse($printer->printer_command);
-
-    open my $out, '|-', $command or die $!;
-    binmode $out;
-    print $out $merged_pdf;
-    close $out;
+    $printer->print_document(content => $merged_pdf);
 
     flash_later('info', t8('The documents have been sent to the printer \'#1\'.', $printer->printer_description));
     return $self->redirect_to(action => 'list_invoices', printer_id => $params{printer_id});
@@ -348,8 +374,8 @@ sub make_filter_summary {
 
   my @filters = (
     [ $filter->{customer}{"name:substr::ilike"}, t8('Customer') ],
-    [ $filter->{"transdate:date::ge"},           t8('Transdate') . " " . t8('From Date') ],
-    [ $filter->{"transdate:date::le"},           t8('Transdate') . " " . t8('To Date')   ],
+    [ $filter->{"transdate:date::ge"},           t8('Delivery Order Date') . " " . t8('From Date') ],
+    [ $filter->{"transdate:date::le"},           t8('Delivery Order Date') . " " . t8('To Date')   ],
   );
 
   for (@filters) {
@@ -358,6 +384,62 @@ sub make_filter_summary {
 
   $self->{filter_summary} = join ', ', @filter_strings;
 }
+
+sub setup_list_invoices_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#search_form', { action => 'MassInvoiceCreatePrint/list_invoices' } ],
+        accesskey => 'enter',
+      ],
+      action => [
+        $::locale->text('Print'),
+        call     => [ 'kivi.MassInvoiceCreatePrint.showMassPrintOptionsOrDownloadDirectly' ],
+        disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
+      ],
+    );
+  }
+}
+
+sub setup_list_sales_delivery_orders_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        $params{show_creation_buttons} ? t8('Update') : t8('Search'),
+        submit    => [ '#search_form', { action => 'MassInvoiceCreatePrint/list_sales_delivery_orders' } ],
+        accesskey => 'enter',
+      ],
+
+      combobox => [
+        action => [
+          t8('Invoices'),
+          tooltip => t8("Create and print invoices")
+        ],
+        action => [
+          t8("Create and print invoices for all selected delivery orders"),
+          submit    => [ 'form', { action => 'MassInvoiceCreatePrint/create_invoices' } ],
+          disabled  => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
+          only_if   => $params{show_creation_buttons},
+          checks    => [ 'kivi.MassInvoiceCreatePrint.checkDeliveryOrderSelection' ],
+          only_once => 1,
+        ],
+
+        action => [
+          t8("Create and print invoices for all delivery orders matching the filter"),
+          call     => [ 'kivi.MassInvoiceCreatePrint.createPrintAllInitialize' ],
+          disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
+          only_if  => $params{show_creation_buttons},
+        ],
+      ],
+    );
+  }
+}
+
 1;
 
 __END__
@@ -483,7 +565,7 @@ supported: Customer and date from/to of the Delivery Order (database field trans
 
 =head1 TODO
 
-Should be more generalized. Right now just one conversion (delivery order to invoice) is supported.
+pShould be more generalized. Right now just one conversion (delivery order to invoice) is supported.
 Using BackgroundJobs to mass create / transfer stuff is the way to do it. The original idea
 was taken from one client project (mosu) with some extra (maybe not standard compliant) customized
 stuff (using cvars for extra filters and a very compressed Controller for linking (ODSalesOrder.pm)).
diff --git a/SL/Controller/MaterializeTest.pm b/SL/Controller/MaterializeTest.pm
new file mode 100644 (file)
index 0000000..4cfceb3
--- /dev/null
@@ -0,0 +1,16 @@
+package SL::Controller::MaterializeTest;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+__PACKAGE__->run_before(sub { $::auth->assert('developer') });
+
+sub action_components {
+  $_[0]->render("test/components");
+}
+
+sub action_modal {
+  $_[0]->render("test/modal");
+}
+
+1;
index db4a84b..99e20ff 100644 (file)
@@ -51,6 +51,9 @@ sub action_showmap {
        $::lxdebug->enter_sub;
        my ($self) = @_;
        
+       # call model -> diese Zeile ist fraglich, war ein Konflikt
+       $self->{data} = DB::MebilMapping::getMappings($::form->get_standard_dbh);
+       
        $::form->{title} = $::locale->text('Mebil Map');
 
        my $sql = "SELECT fromacc,typ,toacc from mebil_mapping order by ordering";
diff --git a/SL/Controller/ODGeierlein.pm b/SL/Controller/ODGeierlein.pm
new file mode 100644 (file)
index 0000000..749941d
--- /dev/null
@@ -0,0 +1,129 @@
+package SL::Controller::ODGeierlein;
+
+use strict;
+use utf8;
+use List::Util qw(first);
+
+use parent qw(SL::Controller::Base);
+
+use SL::USTVA;
+
+use Rose::Object::MakeMethods::Generic;
+
+#
+# actions
+#
+
+sub action_send {
+  $::lxdebug->enter_sub();
+  my ($self) = @_;
+  my $err = '';
+
+  # Aufruf von get_config zum Einlesen der Daten aus Finanzamt und Defaults
+
+  my $ustva = USTVA->new();
+  $ustva->get_config();
+  $ustva->get_finanzamt();
+  $ustva->set_FromTo(\%$::form);
+  $::lxdebug->message($LXDebug::DEBUG2,"fromdate=".$::form->{fromdate}." todate=".$::form->{todate}." meth=".$::form->{method});
+
+  my $tax_office     = first { $_->{id} eq $::form->{fa_land_nr} } @{ $ustva->{tax_office_information} };
+
+  if ( !$::form->{co_zip} ) {
+    $::form->{co_zip} = $::form->{co_city};
+    $::form->{co_zip} =~ s/\D//g;
+    $::form->{co_city} =~ s/\d//g;
+    $::form->{co_city} =~ s/^\s//g;
+  }
+  $::form->{period}=~ s/^0//;
+
+  # Aufbau der Geierlein Parameter
+  my $params=
+    "name = "  .$::form->{company}."\nstrasse = ".$::form->{co_street}.
+    "\nplz = "    .$::form->{co_zip}."\nort = "  .$::form->{co_city}.
+    "\ntelefon = ".$::form->{co_tel}."\nemail = ".$::form->{co_email}.
+    "\nland = ".$tax_office->{taxbird_nr}."\nsteuernummer = ".$::form->{taxnumber}."\njahr = ".$::form->{year}.
+    "\nzeitraum = ".$::form->{period}."\n";
+
+  $::lxdebug->message($LXDebug::DEBUG2,"param1=".$params );
+
+  # USTVA Daten erzeugen
+  # benötigt $form->{fromdate}, $form->{todate} $form->{method}
+  $ustva->ustva(\%::myconfig, \%$::form);
+
+  my @category_cent = $ustva->report_variables({
+    myconfig    => \%::myconfig,
+    form        => $::form,
+    type        => '',
+    attribute   => 'position',
+    dec_places  => '2',
+  });
+
+  #push @category_cent, qw(Z43  Z45  Z53  Z54  Z62  Z65  Z67);
+
+  my @category_euro = $ustva->report_variables({
+    myconfig    => \%::myconfig,
+    form        => $::form,
+    type        => '',
+    attribute   => 'position',
+    dec_places  => '0',
+  });
+
+  # Numberformatting for Geierlein
+  my $temp_numberformat = $::myconfig{numberformat};
+  # Numberformat must be '1000,00' ?!
+  $::myconfig{numberformat} = '1000,00';
+  foreach my $number (@{ $::form->{category_cent} }) {
+    $::form->{$number} = ($::form->{$number} !=0) ? $::form->format_amount(\%::myconfig, $::form->{$number},'2',''):'';
+  }
+
+  foreach my $number (@{ $::form->{category_euro} }) {
+    $::form->{$number} = ($::form->{$number} !=0) ? $::form->format_amount(\%::myconfig, $::form->{$number},'0',''):'';
+  }
+  # Re-set Numberformat
+  $::myconfig{numberformat} = $temp_numberformat;
+
+  # Berichtigte Anmeldung
+  $params .= "kz10 = 1\n" if $::form->{FA_10};
+
+  # Belege (Verträge, Rechnungen, Erläuterungen usw.) werden gesondert eingereicht
+  $params .= "kz22 = 1\n" if $::form->{FA_22};
+
+  # Verrechnung des Erstattungsbetrags erwünscht / Erstattungsbetrag ist abgetreten
+  $params .= "kz29 = 1\n" if $::form->{FA_29};
+
+  # Die Einzugsermächtigung wird ausnahmsweise (z.B. wegen Verrechnungswünschen) für diesen Voranmeldungszeitraum widerrufen.
+  #  Ein ggf. verbleibender Restbetrag ist gesondert zu entrichten.
+  $params .= "kz26 = 1\n" if $::form->{FA_26};
+
+  my @unused_ids = qw(511 861 971 931 Z43 811 891 Z43 Z45 Z53 Z54 Z62 Z65 Z67 83);
+
+  for my $kennziffer (@{$::form->{category_cent}}, @{$::form->{category_euro}}) {
+    $::lxdebug->message($LXDebug::DEBUG2,"kennziffer ".$kennziffer."=".$::form->{$kennziffer});
+
+    next if first { $_ eq $kennziffer } @unused_ids;
+
+    if ($::form->{$kennziffer} != 0) {
+      $params .= "kz".$kennziffer." = ".$::form->{$kennziffer}."\n";
+    }
+  }
+
+  $::lxdebug->message($LXDebug::DEBUG2,"param2=".$params );
+
+
+  $self->js->flash($err?'error':'info',
+                   $err?$err:
+                   $::locale->text('USTVA Data sent to geierlein'));
+  $self->js->run('openGeierlein',$params) if !$err;
+  $::lxdebug->leave_sub();
+  $self->js->render;
+}
+
+
+
+#
+# filters / helpers
+#
+
+
+1;
diff --git a/SL/Controller/Order.pm b/SL/Controller/Order.pm
new file mode 100644 (file)
index 0000000..faf2ffa
--- /dev/null
@@ -0,0 +1,2892 @@
+package SL::Controller::Order;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+use SL::Helper::Flash qw(flash_later);
+use SL::HTML::Util;
+use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
+use SL::Locale::String qw(t8);
+use SL::SessionFile::Random;
+use SL::PriceSource;
+use SL::Webdav;
+use SL::File;
+use SL::MIME;
+use SL::Util qw(trim);
+use SL::YAML;
+use SL::DB::AdditionalBillingAddress;
+use SL::DB::AuthUser;
+use SL::DB::History;
+use SL::DB::Order;
+use SL::DB::Default;
+use SL::DB::Unit;
+use SL::DB::Part;
+use SL::DB::PartClassification;
+use SL::DB::PartsGroup;
+use SL::DB::Printer;
+use SL::DB::Note;
+use SL::DB::Language;
+use SL::DB::RecordLink;
+use SL::DB::RequirementSpec;
+use SL::DB::Shipto;
+use SL::DB::Translation;
+
+use SL::Helper::CreatePDF qw(:all);
+use SL::Helper::PrintOptions;
+use SL::Helper::ShippedQty;
+use SL::Helper::UserPreferences::DisplayPreferences;
+use SL::Helper::UserPreferences::PositionsScrollbar;
+use SL::Helper::UserPreferences::UpdatePositions;
+
+use SL::Controller::Helper::GetModels;
+
+use List::Util qw(first sum0);
+use List::UtilsBy qw(sort_by uniq_by);
+use List::MoreUtils qw(any none pairwise first_index);
+use English qw(-no_match_vars);
+use File::Spec;
+use Cwd;
+use Sort::Naturally;
+
+use Rose::Object::MakeMethods::Generic
+(
+ scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
+ 'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids) ],
+);
+
+
+# safety
+__PACKAGE__->run_before('check_auth');
+
+__PACKAGE__->run_before('check_auth_for_edit',
+                        except => [ qw(edit show_customer_vendor_details_dialog price_popup load_second_rows) ]);
+
+__PACKAGE__->run_before('recalc',
+                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
+                                     print send_email) ]);
+
+__PACKAGE__->run_before('get_unalterable_data',
+                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_invoice_for_advance_payment save_and_final_invoice save_and_ap_transaction
+                                     print send_email) ]);
+
+#
+# actions
+#
+
+# add a new order
+sub action_add {
+  my ($self) = @_;
+
+  $self->order->transdate(DateTime->now_local());
+  my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
+                   $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
+
+  if (   ($self->type eq sales_order_type()     &&  $::instance_conf->get_deliverydate_on)
+      || ($self->type eq sales_quotation_type() &&  $::instance_conf->get_reqdate_on)
+      && (!$self->order->reqdate)) {
+    $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days));
+  }
+
+
+  $self->pre_render();
+  $self->render(
+    'order/form',
+    title => $self->get_title_for('add'),
+    %{$self->{template_args}}
+  );
+}
+
+# edit an existing order
+sub action_edit {
+  my ($self) = @_;
+
+  if ($::form->{id}) {
+    $self->load_order;
+
+  } else {
+    # this is to edit an order from an unsaved order object
+
+    # set item ids to new fake id, to identify them as new items
+    foreach my $item (@{$self->order->items_sorted}) {
+      $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+    }
+    # trigger rendering values for second row as hidden, because they
+    # are loaded only on demand. So we need to keep the values from
+    # the source.
+    $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
+  }
+
+  $self->recalc();
+  $self->pre_render();
+  $self->render(
+    'order/form',
+    title => $self->get_title_for('edit'),
+    %{$self->{template_args}}
+  );
+}
+
+# edit a collective order (consisting of one or more existing orders)
+sub action_edit_collective {
+  my ($self) = @_;
+
+  # collect order ids
+  my @multi_ids = map {
+    $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
+  } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
+
+  # fall back to add if no ids are given
+  if (scalar @multi_ids == 0) {
+    $self->action_add();
+    return;
+  }
+
+  # fall back to save as new if only one id is given
+  if (scalar @multi_ids == 1) {
+    $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
+    $self->action_save_as_new();
+    return;
+  }
+
+  # make new order from given orders
+  my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
+  $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
+  $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
+
+  $self->action_edit();
+}
+
+# delete the order
+sub action_delete {
+  my ($self) = @_;
+
+  my $errors = $self->delete();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been deleted')
+           : $self->type eq purchase_order_type()    ? $::locale->text('The order has been deleted')
+           : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been deleted')
+           : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
+           : '';
+  flash_later('info', $text);
+
+  my @redirect_params = (
+    action => 'add',
+    type   => $self->type,
+  );
+
+  $self->redirect_to(@redirect_params);
+}
+
+# save the order
+sub action_save {
+  my ($self) = @_;
+
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
+           : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
+           : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
+           : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
+           : '';
+  flash_later('info', $text);
+
+  my @redirect_params;
+  if ($::form->{back_to_caller}) {
+    @redirect_params = $::form->{callback} ? ($::form->{callback})
+                                           : (controller => 'LoginScreen', action => 'user_login');
+
+  } else {
+    @redirect_params = (
+      action   => 'edit',
+      type     => $self->type,
+      id       => $self->order->id,
+      callback => $::form->{callback},
+    );
+  }
+
+  $self->redirect_to(@redirect_params);
+}
+
+# save the order as new document an open it for edit
+sub action_save_as_new {
+  my ($self) = @_;
+
+  my $order = $self->order;
+
+  if (!$order->id) {
+    $self->js->flash('error', t8('This object has not been saved yet.'));
+    return $self->js->render();
+  }
+
+  # load order from db to check if values changed
+  my $saved_order = SL::DB::Order->new(id => $order->id)->load;
+
+  my %new_attrs;
+  # Lets assign a new number if the user hasn't changed the previous one.
+  # If it has been changed manually then use it as-is.
+  $new_attrs{number}    = (trim($order->number) eq $saved_order->number)
+                        ? ''
+                        : trim($order->number);
+
+  # Clear transdate unless changed
+  $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
+                        ? DateTime->today_local
+                        : $order->transdate;
+
+  # Set new reqdate unless changed if it is enabled in client config
+  if ($order->reqdate == $saved_order->reqdate) {
+    my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval       :
+                     $self->type eq sales_order_type()     ? $::instance_conf->get_delivery_date_interval : 1;
+
+    if (   ($self->type eq sales_order_type()     &&  !$::instance_conf->get_deliverydate_on)
+        || ($self->type eq sales_quotation_type() &&  !$::instance_conf->get_reqdate_on)) {
+      $new_attrs{reqdate} = '';
+    } else {
+      $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
+    }
+  } else {
+    $new_attrs{reqdate} = $order->reqdate;
+  }
+
+  # Update employee
+  $new_attrs{employee}  = SL::DB::Manager::Employee->current;
+
+  # Warn on obsolete items
+  my @obsolete_positions = map { $_->position } grep { $_->part->obsolete } @{ $order->items_sorted };
+  flash_later('warning', t8('This record containts obsolete items at position #1', join ', ', @obsolete_positions)) if @obsolete_positions;
+
+  # Create new record from current one
+  $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
+
+  # no linked records on save as new
+  delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
+
+  # save
+  $self->action_save();
+}
+
+# print the order
+#
+# This is called if "print" is pressed in the print dialog.
+# If PDF creation was requested and succeeded, the pdf is offered for download
+# via send_file (which uses ajax in this case).
+sub action_print {
+  my ($self) = @_;
+
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  $self->js_reset_order_and_item_ids_after_save;
+
+  my $redirect_url = $self->url_for(
+    action => 'edit',
+    type   => $self->type,
+    id     => $self->order->id,
+  );
+
+  my $format      = $::form->{print_options}->{format};
+  my $media       = $::form->{print_options}->{media};
+  my $formname    = $::form->{print_options}->{formname};
+  my $copies      = $::form->{print_options}->{copies};
+  my $groupitems  = $::form->{print_options}->{groupitems};
+  my $printer_id  = $::form->{print_options}->{printer_id};
+
+  # only PDF, OpenDocument & HTML for now
+  if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf html)) {
+    flash_later('error', t8('Format \'#1\' is not supported yet/anymore.', $format));
+    return $self->js->redirect_to($redirect_url)->render;
+  }
+
+  # only screen or printer by now
+  if (none { $media eq $_ } qw(screen printer)) {
+    flash_later('error', t8('Media \'#1\' is not supported yet/anymore.', $media));
+    return $self->js->redirect_to($redirect_url)->render;
+  }
+
+  # create a form for generate_attachment_filename
+  my $form   = Form->new;
+  $form->{$self->nr_key()}  = $self->order->number;
+  $form->{type}             = $self->type;
+  $form->{format}           = $format;
+  $form->{formname}         = $formname;
+  $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
+  my $doc_filename          = $form->generate_attachment_filename();
+
+  my $doc;
+  my @errors = $self->generate_doc(\$doc, { media      => $media,
+                                            format     => $format,
+                                            formname   => $formname,
+                                            language   => $self->order->language,
+                                            printer_id => $printer_id,
+                                            groupitems => $groupitems });
+  if (scalar @errors) {
+    flash_later('error', t8('Generating the document failed: #1', $errors[0]));
+    return $self->js->redirect_to($redirect_url)->render;
+  }
+
+  if ($media eq 'screen') {
+    # screen/download
+    flash_later('info', t8('The document has been created.'));
+    $self->send_file(
+      \$doc,
+      type         => SL::MIME->mime_type_from_ext($doc_filename),
+      name         => $doc_filename,
+      js_no_render => 1,
+    );
+
+  } elsif ($media eq 'printer') {
+    # printer
+    my $printer_id = $::form->{print_options}->{printer_id};
+    SL::DB::Printer->new(id => $printer_id)->load->print_document(
+      copies  => $copies,
+      content => $doc,
+    );
+
+    flash_later('info', t8('The document has been printed.'));
+  }
+
+  my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $doc_filename, $formname);
+  if (scalar @warnings) {
+    flash_later('warning', $_) for @warnings;
+  }
+
+  $self->save_history('PRINTED');
+
+  $self->js->redirect_to($redirect_url)->render;
+}
+
+sub action_preview_pdf {
+  my ($self) = @_;
+
+  my $errors = $self->save();
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  $self->js_reset_order_and_item_ids_after_save;
+
+  my $redirect_url = $self->url_for(
+    action => 'edit',
+    type   => $self->type,
+    id     => $self->order->id,
+  );
+
+  my $format      = 'pdf';
+  my $media       = 'screen';
+  my $formname    = $self->type;
+
+  # only pdf
+  # create a form for generate_attachment_filename
+  my $form   = Form->new;
+  $form->{$self->nr_key()}  = $self->order->number;
+  $form->{type}             = $self->type;
+  $form->{format}           = $format;
+  $form->{formname}         = $formname;
+  $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
+  my $pdf_filename          = $form->generate_attachment_filename();
+
+  my $pdf;
+  my @errors = $self->generate_doc(\$pdf, { media      => $media,
+                                            format     => $format,
+                                            formname   => $formname,
+                                            language   => $self->order->language,
+                                          });
+  if (scalar @errors) {
+    flash_later('error', t8('Conversion to PDF failed: #1', $errors[0]));
+    return $self->js->redirect_to($redirect_url)->render;
+  }
+
+  $self->save_history('PREVIEWED');
+
+  flash_later('info', t8('The PDF has been previewed'));
+
+  # screen/download
+  $self->send_file(
+    \$pdf,
+    type         => SL::MIME->mime_type_from_ext($pdf_filename),
+    name         => $pdf_filename,
+    js_no_render => 1,
+  );
+
+  $self->js->redirect_to($redirect_url)->render;
+}
+
+# open the email dialog
+sub action_save_and_show_email_dialog {
+  my ($self) = @_;
+
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  $self->js_reset_order_and_item_ids_after_save;
+
+  my $cv_method = $self->cv;
+
+  if (!$self->order->$cv_method) {
+    return $self->js->flash('error', $self->cv eq 'customer' ? t8('Cannot send E-mail without customer given') : t8('Cannot send E-mail without vendor given'))
+                    ->render($self);
+  }
+
+  my $email_form;
+  $email_form->{to}   = $self->order->contact->cp_email if $self->order->contact;
+  $email_form->{to} ||= $self->order->$cv_method->email;
+  $email_form->{cc}   = $self->order->$cv_method->cc;
+  $email_form->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
+  # Todo: get addresses from shipto, if any
+
+  my $form = Form->new;
+  $form->{$self->nr_key()}  = $self->order->number;
+  $form->{cusordnumber}     = $self->order->cusordnumber;
+  $form->{formname}         = $self->type;
+  $form->{type}             = $self->type;
+  $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
+  $form->{language_id}      = $self->order->language->id                  if $self->order->language;
+  $form->{format}           = 'pdf';
+  $form->{cp_id}            = $self->order->contact->cp_id if $self->order->contact;
+
+  $email_form->{subject}             = $form->generate_email_subject();
+  $email_form->{attachment_filename} = $form->generate_attachment_filename();
+  $email_form->{message}             = $form->generate_email_body();
+  $email_form->{js_send_function}    = 'kivi.Order.send_email()';
+
+  my %files = $self->get_files_for_email_dialog();
+
+  my @employees_with_email = grep {
+    my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
+    $user && !!trim($user->get_config_value('email'));
+  } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
+
+
+  my $all_partner_email_addresses = $self->order->customervendor->get_all_email_addresses();
+
+  my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
+                                  email_form    => $email_form,
+                                  show_bcc      => $::auth->assert('email_bcc', 'may fail'),
+                                  FILES         => \%files,
+                                  is_customer   => $self->cv eq 'customer',
+                                  ALL_EMPLOYEES => \@employees_with_email,
+                                  ALL_PARTNER_EMAIL_ADDRESSES => $all_partner_email_addresses,
+  );
+
+  $self->js
+      ->run('kivi.Order.show_email_dialog', $dialog_html)
+      ->reinit_widgets
+      ->render($self);
+}
+
+# send email
+#
+# Todo: handling error messages: flash is not displayed in dialog, but in the main form
+sub action_send_email {
+  my ($self) = @_;
+
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->run('kivi.Order.close_email_dialog');
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  $self->js_reset_order_and_item_ids_after_save;
+
+  my $email_form  = delete $::form->{email_form};
+
+  if ($email_form->{additional_to}) {
+    $email_form->{to} = join ', ', grep { $_ } $email_form->{to}, @{$email_form->{additional_to}};
+    delete $email_form->{additional_to};
+  }
+
+  my %field_names = (to => 'email');
+
+  $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
+
+  # for Form::cleanup which may be called in Form::send_email
+  $::form->{cwd}    = getcwd();
+  $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
+
+  $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
+  $::form->{media}  = 'email';
+
+  $::form->{attachment_policy} //= '';
+
+  # Is an old file version available?
+  my $attfile;
+  if ($::form->{attachment_policy} eq 'old_file') {
+    $attfile = SL::File->get_all(object_id     => $self->order->id,
+                                 object_type   => $self->type,
+                                 file_type     => 'document',
+                                 print_variant => $::form->{formname});
+  }
+
+  if ($::form->{attachment_policy} ne 'no_file' && !($::form->{attachment_policy} eq 'old_file' && $attfile)) {
+    my $doc;
+    my @errors = $self->generate_doc(\$doc, {media      => $::form->{media},
+                                             format     => $::form->{print_options}->{format},
+                                             formname   => $::form->{print_options}->{formname},
+                                             language   => $self->order->language,
+                                             printer_id => $::form->{print_options}->{printer_id},
+                                             groupitems => $::form->{print_options}->{groupitems}});
+    if (scalar @errors) {
+      return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render($self);
+    }
+
+    my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $::form->{attachment_filename}, $::form->{formname});
+    if (scalar @warnings) {
+      flash_later('warning', $_) for @warnings;
+    }
+
+    my $sfile = SL::SessionFile::Random->new(mode => "w");
+    $sfile->fh->print($doc);
+    $sfile->fh->close;
+
+    $::form->{tmpfile} = $sfile->file_name;
+    $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
+  }
+
+  $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
+  $::form->send_email(\%::myconfig, $::form->{print_options}->{format});
+
+  # internal notes unless no email journal
+  unless ($::instance_conf->get_email_journal) {
+    my $intnotes = $self->order->intnotes;
+    $intnotes   .= "\n\n" if $self->order->intnotes;
+    $intnotes   .= t8('[email]')                                                                                        . "\n";
+    $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
+    $intnotes   .= t8('To (email)') . ": " . $::form->{email}                                                           . "\n";
+    $intnotes   .= t8('Cc')         . ": " . $::form->{cc}                                                              . "\n"    if $::form->{cc};
+    $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}                                                             . "\n"    if $::form->{bcc};
+    $intnotes   .= t8('Subject')    . ": " . $::form->{subject}                                                         . "\n\n";
+    $intnotes   .= t8('Message')    . ": " . SL::HTML::Util->strip($::form->{message});
+
+    $self->order->update_attributes(intnotes => $intnotes);
+  }
+
+  $self->save_history('MAILED');
+
+  flash_later('info', t8('The email has been sent.'));
+
+  my @redirect_params = (
+    action => 'edit',
+    type   => $self->type,
+    id     => $self->order->id,
+  );
+
+  $self->redirect_to(@redirect_params);
+}
+
+# open the periodic invoices config dialog
+#
+# If there are values in the form (i.e. dialog was opened before),
+# then use this values. Create new ones, else.
+sub action_show_periodic_invoices_config_dialog {
+  my ($self) = @_;
+
+  my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
+  $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
+  $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
+                                                   order_value_periodicity => 'p', # = same as periodicity
+                                                   start_date_as_date      => $::form->{transdate_as_date} || $::form->current_date,
+                                                   extend_automatically_by => 12,
+                                                   active                  => 1,
+                                                   email_subject           => GenericTranslations->get(
+                                                                                language_id      => $::form->{language_id},
+                                                                                translation_type =>"preset_text_periodic_invoices_email_subject"),
+                                                   email_body              => GenericTranslations->get(
+                                                                                language_id      => $::form->{language_id},
+                                                                                translation_type => "salutation_general")
+                                                                            . GenericTranslations->get(
+                                                                                language_id      => $::form->{language_id},
+                                                                                translation_type => "salutation_punctuation_mark") . "\n\n"
+                                                                            . GenericTranslations->get(
+                                                                                language_id      => $::form->{language_id},
+                                                                                translation_type =>"preset_text_periodic_invoices_email_body"),
+  );
+  # for older configs, replace email preset text if not yet set.
+  $config->email_subject(GenericTranslations->get(
+                                              language_id      => $::form->{language_id},
+                                              translation_type =>"preset_text_periodic_invoices_email_subject")
+                        ) unless $config->email_subject;
+
+  $config->email_body(GenericTranslations->get(
+                                              language_id      => $::form->{language_id},
+                                              translation_type => "salutation_general")
+                    . GenericTranslations->get(
+                                              language_id      => $::form->{language_id},
+                                              translation_type => "salutation_punctuation_mark") . "\n\n"
+                    . GenericTranslations->get(
+                                              language_id      => $::form->{language_id},
+                                              translation_type =>"preset_text_periodic_invoices_email_body")
+                     ) unless $config->email_body;
+
+  $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
+  $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
+
+  $::form->get_lists(printers => "ALL_PRINTERS",
+                     charts   => { key       => 'ALL_CHARTS',
+                                   transdate => 'current_date' });
+
+  $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
+
+  if ($::form->{customer_id}) {
+    $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
+    my $customer_object = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
+    $::form->{postal_invoice}                  = $customer_object->postal_invoice;
+    $::form->{email_recipient_invoice_address} = $::form->{postal_invoice} ? '' : $customer_object->invoice_mail;
+    $config->send_email(0) if $::form->{postal_invoice};
+  }
+
+  $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
+                popup_dialog             => 1,
+                popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
+                popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
+                config                   => $config,
+                %$::form);
+}
+
+# assign the values of the periodic invoices config dialog
+# as yaml in the hidden tag and set the status.
+sub action_assign_periodic_invoices_config {
+  my ($self) = @_;
+
+  $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
+
+  my $config = { active                     => $::form->{active}       ? 1 : 0,
+                 terminated                 => $::form->{terminated}   ? 1 : 0,
+                 direct_debit               => $::form->{direct_debit} ? 1 : 0,
+                 periodicity                => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
+                 order_value_periodicity    => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
+                 start_date_as_date         => $::form->{start_date_as_date},
+                 end_date_as_date           => $::form->{end_date_as_date},
+                 first_billing_date_as_date => $::form->{first_billing_date_as_date},
+                 print                      => $::form->{print}      ? 1                         : 0,
+                 printer_id                 => $::form->{print}      ? $::form->{printer_id} * 1 : undef,
+                 copies                     => $::form->{copies} * 1 ? $::form->{copies}         : 1,
+                 extend_automatically_by    => $::form->{extend_automatically_by}    * 1 || undef,
+                 ar_chart_id                => $::form->{ar_chart_id} * 1,
+                 send_email                 => $::form->{send_email} ? 1 : 0,
+                 email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
+                 email_recipient_address    => $::form->{email_recipient_address},
+                 email_sender               => $::form->{email_sender},
+                 email_subject              => $::form->{email_subject},
+                 email_body                 => $::form->{email_body},
+               };
+
+  my $periodic_invoices_config = SL::YAML::Dump($config);
+
+  my $status = $self->get_periodic_invoices_status($config);
+
+  $self->js
+    ->remove('#order_periodic_invoices_config')
+    ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
+    ->run('kivi.Order.close_periodic_invoices_config_dialog')
+    ->html('#periodic_invoices_status', $status)
+    ->flash('info', t8('The periodic invoices config has been assigned.'))
+    ->render($self);
+}
+
+sub action_get_has_active_periodic_invoices {
+  my ($self) = @_;
+
+  my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
+  $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
+
+  my $has_active_periodic_invoices =
+       $self->type eq sales_order_type()
+    && $config
+    && $config->active
+    && (!$config->end_date || ($config->end_date > DateTime->today_local))
+    && $config->get_previous_billed_period_start_date;
+
+  $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
+}
+
+# save the order and redirect to the frontend subroutine for a new
+# delivery order
+sub action_save_and_delivery_order {
+  my ($self) = @_;
+
+  $self->save_and_redirect_to(
+    controller => 'oe.pl',
+    action     => 'oe_delivery_order_from_order',
+  );
+}
+
+sub action_save_and_supplier_delivery_order {
+  my ($self) = @_;
+
+  $self->save_and_redirect_to(
+    controller => 'controller.pl',
+    action     => 'DeliveryOrder/add_from_order',
+    type       => 'supplier_delivery_order',
+  );
+}
+
+# save the order and redirect to the frontend subroutine for a new
+# invoice
+sub action_save_and_invoice {
+  my ($self) = @_;
+
+  $self->save_and_redirect_to(
+    controller => 'oe.pl',
+    action     => 'oe_invoice_from_order',
+  );
+}
+
+sub action_save_and_invoice_for_advance_payment {
+  my ($self) = @_;
+
+  $self->save_and_redirect_to(
+    controller       => 'oe.pl',
+    action           => 'oe_invoice_from_order',
+    new_invoice_type => 'invoice_for_advance_payment',
+  );
+}
+
+sub action_save_and_final_invoice {
+  my ($self) = @_;
+
+  $self->save_and_redirect_to(
+    controller       => 'oe.pl',
+    action           => 'oe_invoice_from_order',
+    new_invoice_type => 'final_invoice',
+  );
+}
+
+# workflow from sales order to sales quotation
+sub action_sales_quotation {
+  $_[0]->workflow_sales_or_request_for_quotation();
+}
+
+# workflow from sales order to sales quotation
+sub action_request_for_quotation {
+  $_[0]->workflow_sales_or_request_for_quotation();
+}
+
+# workflow from sales quotation to sales order
+sub action_sales_order {
+  $_[0]->workflow_sales_or_purchase_order();
+}
+
+# workflow from rfq to purchase order
+sub action_purchase_order {
+  $_[0]->workflow_sales_or_purchase_order();
+}
+
+# workflow from purchase order to ap transaction
+sub action_save_and_ap_transaction {
+  my ($self) = @_;
+
+  $self->save_and_redirect_to(
+    controller => 'ap.pl',
+    action     => 'add_from_purchase_order',
+  );
+}
+
+# set form elements in respect to a changed customer or vendor
+#
+# This action is called on an change of the customer/vendor picker.
+sub action_customer_vendor_changed {
+  my ($self) = @_;
+
+  setup_order_from_cv($self->order);
+  $self->recalc();
+
+  my $cv_method = $self->cv;
+
+  if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
+    $self->js->show('#cp_row');
+  } else {
+    $self->js->hide('#cp_row');
+  }
+
+  if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
+    $self->js->show('#shipto_selection');
+  } else {
+    $self->js->hide('#shipto_selection');
+  }
+
+  if ($cv_method eq 'customer') {
+    my $show_hide = scalar @{ $self->order->customer->additional_billing_addresses } > 0 ? 'show' : 'hide';
+    $self->js->$show_hide('#billing_address_row');
+  }
+
+  $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
+
+  $self->js
+    ->replaceWith('#order_cp_id',              $self->build_contact_select)
+    ->replaceWith('#order_shipto_id',          $self->build_shipto_select)
+    ->replaceWith('#shipto_inputs  ',          $self->build_shipto_inputs)
+    ->replaceWith('#order_billing_address_id', $self->build_billing_address_select)
+    ->replaceWith('#business_info_row',        $self->build_business_info_row)
+    ->val(        '#order_taxzone_id',         $self->order->taxzone_id)
+    ->val(        '#order_taxincluded',        $self->order->taxincluded)
+    ->val(        '#order_currency_id',        $self->order->currency_id)
+    ->val(        '#order_payment_id',         $self->order->payment_id)
+    ->val(        '#order_delivery_term_id',   $self->order->delivery_term_id)
+    ->val(        '#order_intnotes',           $self->order->intnotes)
+    ->val(        '#order_language_id',        $self->order->$cv_method->language_id)
+    ->focus(      '#order_' . $self->cv . '_id')
+    ->run('kivi.Order.update_exchangerate');
+
+  $self->js_redisplay_amounts_and_taxes;
+  $self->js_redisplay_cvpartnumbers;
+  $self->js->render();
+}
+
+# open the dialog for customer/vendor details
+sub action_show_customer_vendor_details_dialog {
+  my ($self) = @_;
+
+  my $is_customer = 'customer' eq $::form->{vc};
+  my $cv;
+  if ($is_customer) {
+    $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
+  } else {
+    $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
+  }
+
+  my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
+  $details{discount_as_percent} = $cv->discount_as_percent;
+  $details{creditlimt}          = $cv->creditlimit_as_number;
+  $details{business}            = $cv->business->description      if $cv->business;
+  $details{language}            = $cv->language_obj->description  if $cv->language_obj;
+  $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
+  $details{payment_terms}       = $cv->payment->description       if $cv->payment;
+  $details{pricegroup}          = $cv->pricegroup->pricegroup     if $is_customer && $cv->pricegroup;
+
+  if ($is_customer) {
+    foreach my $entry (@{ $cv->additional_billing_addresses }) {
+      push @{ $details{ADDITIONAL_BILLING_ADDRESSES} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
+    }
+  }
+  foreach my $entry (@{ $cv->shipto }) {
+    push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
+  }
+  foreach my $entry (@{ $cv->contacts }) {
+    push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
+  }
+
+  $_[0]->render('common/show_vc_details', { layout => 0 },
+                is_customer => $is_customer,
+                %details);
+
+}
+
+# called if a unit in an existing item row is changed
+sub action_unit_changed {
+  my ($self) = @_;
+
+  my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
+  my $item = $self->order->items_sorted->[$idx];
+
+  my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
+  $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
+
+  $self->recalc();
+
+  $self->js
+    ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
+  $self->js_redisplay_line_values;
+  $self->js_redisplay_amounts_and_taxes;
+  $self->js->render();
+}
+
+# update item input row when a part ist picked
+sub action_update_item_input_row {
+  my ($self) = @_;
+
+  delete $::form->{add_item}->{$_} for qw(create_part_type sellprice_as_number discount_as_percent);
+
+  my $form_attr = $::form->{add_item};
+
+  return unless $form_attr->{parts_id};
+
+  my $record       = $self->order;
+  my $item         = SL::DB::OrderItem->new(%$form_attr);
+  $item->unit($item->part->unit);
+
+  my ($price_src, $discount_src) = get_best_price_and_discount_source($record, $item, 0);
+
+  $self->js
+    ->val     ('#add_item_unit',                $item->unit)
+    ->val     ('#add_item_description',         $item->part->description)
+    ->val     ('#add_item_sellprice_as_number', '')
+    ->attr    ('#add_item_sellprice_as_number', 'placeholder', $price_src->price_as_number)
+    ->attr    ('#add_item_sellprice_as_number', 'title',       $price_src->source_description)
+    ->val     ('#add_item_discount_as_percent', '')
+    ->attr    ('#add_item_discount_as_percent', 'placeholder', $discount_src->discount_as_percent)
+    ->attr    ('#add_item_discount_as_percent', 'title',       $discount_src->source_description)
+    ->render;
+}
+
+# add an item row for a new item entered in the input row
+sub action_add_item {
+  my ($self) = @_;
+
+  delete $::form->{add_item}->{create_part_type};
+
+  my $form_attr = $::form->{add_item};
+
+  return unless $form_attr->{parts_id};
+
+  my $item = new_item($self->order, $form_attr);
+
+  $self->order->add_items($item);
+
+  $self->recalc();
+
+  $self->get_item_cvpartnumber($item);
+
+  my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+  my $row_as_html = $self->p->render('order/tabs/_row',
+                                     ITEM => $item,
+                                     ID   => $item_id,
+                                     SELF => $self,
+  );
+
+  if ($::form->{insert_before_item_id}) {
+    $self->js
+      ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
+  } else {
+    $self->js
+      ->append('#row_table_id', $row_as_html);
+  }
+
+  if ( $item->part->is_assortment ) {
+    $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
+    foreach my $assortment_item ( @{$item->part->assortment_items} ) {
+      my $attr = { parts_id => $assortment_item->parts_id,
+                   qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
+                   unit     => $assortment_item->unit,
+                   description => $assortment_item->part->description,
+                 };
+      my $item = new_item($self->order, $attr);
+
+      # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
+      $item->discount(1) unless $assortment_item->charge;
+
+      $self->order->add_items( $item );
+      $self->recalc();
+      $self->get_item_cvpartnumber($item);
+      my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+      my $row_as_html = $self->p->render('order/tabs/_row',
+                                         ITEM => $item,
+                                         ID   => $item_id,
+                                         SELF => $self,
+      );
+      if ($::form->{insert_before_item_id}) {
+        $self->js
+          ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
+      } else {
+        $self->js
+          ->append('#row_table_id', $row_as_html);
+      }
+    };
+  };
+
+  $self->js
+    ->val('.add_item_input', '')
+    ->attr('.add_item_input', 'placeholder', '')
+    ->attr('.add_item_input', 'title', '')
+    ->run('kivi.Order.init_row_handlers')
+    ->run('kivi.Order.renumber_positions')
+    ->focus('#add_item_parts_id_name');
+
+  $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
+
+  $self->js_redisplay_amounts_and_taxes;
+  $self->js->render();
+}
+
+# add item rows for multiple items at once
+sub action_add_multi_items {
+  my ($self) = @_;
+
+  my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
+  return $self->js->render() unless scalar @form_attr;
+
+  my @items;
+  foreach my $attr (@form_attr) {
+    my $item = new_item($self->order, $attr);
+    push @items, $item;
+    if ( $item->part->is_assortment ) {
+      foreach my $assortment_item ( @{$item->part->assortment_items} ) {
+        my $attr = { parts_id => $assortment_item->parts_id,
+                     qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
+                     unit     => $assortment_item->unit,
+                     description => $assortment_item->part->description,
+                   };
+        my $item = new_item($self->order, $attr);
+
+        # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
+        $item->discount(1) unless $assortment_item->charge;
+        push @items, $item;
+      }
+    }
+  }
+  $self->order->add_items(@items);
+
+  $self->recalc();
+
+  foreach my $item (@items) {
+    $self->get_item_cvpartnumber($item);
+    my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+    my $row_as_html = $self->p->render('order/tabs/_row',
+                                       ITEM => $item,
+                                       ID   => $item_id,
+                                       SELF => $self,
+    );
+
+    if ($::form->{insert_before_item_id}) {
+      $self->js
+        ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
+    } else {
+      $self->js
+        ->append('#row_table_id', $row_as_html);
+    }
+  }
+
+  $self->js
+    ->run('kivi.Part.close_picker_dialogs')
+    ->run('kivi.Order.init_row_handlers')
+    ->run('kivi.Order.renumber_positions')
+    ->focus('#add_item_parts_id_name');
+
+  $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
+
+  $self->js_redisplay_amounts_and_taxes;
+  $self->js->render();
+}
+
+# recalculate all linetotals, amounts and taxes and redisplay them
+sub action_recalc_amounts_and_taxes {
+  my ($self) = @_;
+
+  $self->recalc();
+
+  $self->js_redisplay_line_values;
+  $self->js_redisplay_amounts_and_taxes;
+  $self->js->render();
+}
+
+sub action_update_exchangerate {
+  my ($self) = @_;
+
+  my $data = {
+    is_standard   => $self->order->currency_id == $::instance_conf->get_currency_id,
+    currency_name => $self->order->currency->name,
+    exchangerate  => $self->order->daily_exchangerate_as_null_number,
+  };
+
+  $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
+}
+
+# redisplay item rows if they are sorted by an attribute
+sub action_reorder_items {
+  my ($self) = @_;
+
+  my %sort_keys = (
+    partnumber   => sub { $_[0]->part->partnumber },
+    description  => sub { $_[0]->description },
+    qty          => sub { $_[0]->qty },
+    sellprice    => sub { $_[0]->sellprice },
+    discount     => sub { $_[0]->discount },
+    cvpartnumber => sub { $_[0]->{cvpartnumber} },
+  );
+
+  $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
+
+  my $method = $sort_keys{$::form->{order_by}};
+  my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
+  if ($::form->{sort_dir}) {
+    if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
+      @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
+    } else {
+      @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
+    }
+  } else {
+    if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
+      @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
+    } else {
+      @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
+    }
+  }
+  $self->js
+    ->run('kivi.Order.redisplay_items', \@to_sort)
+    ->render;
+}
+
+# show the popup to choose a price/discount source
+sub action_price_popup {
+  my ($self) = @_;
+
+  my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
+  my $item = $self->order->items_sorted->[$idx];
+
+  $self->render_price_dialog($item);
+}
+
+# save the order in a session variable and redirect to the part controller
+sub action_create_part {
+  my ($self) = @_;
+
+  my $previousform = $::auth->save_form_in_session(non_scalars => 1);
+
+  my $callback     = $self->url_for(
+    action       => 'return_from_create_part',
+    type         => $self->type, # type is needed for check_auth on return
+    previousform => $previousform,
+  );
+
+  flash_later('info', t8('You are adding a new part while you are editing another document. You will be redirected to your document when saving the new part or aborting this form.'));
+
+  my @redirect_params = (
+    controller    => 'Part',
+    action        => 'add',
+    part_type     => $::form->{add_item}->{create_part_type},
+    callback      => $callback,
+    inline_create => 1,
+  );
+
+  $self->redirect_to(@redirect_params);
+}
+
+sub action_return_from_create_part {
+  my ($self) = @_;
+
+  $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
+
+  $::auth->restore_form_from_session(delete $::form->{previousform});
+
+  # set item ids to new fake id, to identify them as new items
+  foreach my $item (@{$self->order->items_sorted}) {
+    $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+  }
+
+  $self->recalc();
+  $self->get_unalterable_data();
+  $self->pre_render();
+
+  # trigger rendering values for second row/longdescription as hidden,
+  # because they are loaded only on demand. So we need to keep the values
+  # from the source.
+  $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
+  $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
+
+  $self->render(
+    'order/form',
+    title => $self->get_title_for('edit'),
+    %{$self->{template_args}}
+  );
+
+}
+
+# load the second row for one or more items
+#
+# This action gets the html code for all items second rows by rendering a template for
+# the second row and sets the html code via client js.
+sub action_load_second_rows {
+  my ($self) = @_;
+
+  $self->recalc() if $self->order->is_sales; # for margin calculation
+
+  foreach my $item_id (@{ $::form->{item_ids} }) {
+    my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
+    my $item = $self->order->items_sorted->[$idx];
+
+    $self->js_load_second_row($item, $item_id, 0);
+  }
+
+  $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
+
+  $self->js->render();
+}
+
+# update description, notes and sellprice from master data
+sub action_update_row_from_master_data {
+  my ($self) = @_;
+
+  foreach my $item_id (@{ $::form->{item_ids} }) {
+    my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
+    my $item  = $self->order->items_sorted->[$idx];
+    my $texts = get_part_texts($item->part, $self->order->language_id);
+
+    $item->description($texts->{description});
+    $item->longdescription($texts->{longdescription});
+
+    my ($price_src, $discount_src) = get_best_price_and_discount_source($self->order, $item, 1);
+
+    $item->sellprice($price_src->price);
+    $item->active_price_source($price_src);
+    $item->discount($discount_src->discount);
+    $item->active_discount_source($discount_src);
+
+    my $price_editable = $self->order->is_sales ? $::auth->assert('sales_edit_prices', 1) : $::auth->assert('purchase_edit_prices', 1);
+
+    $self->js
+      ->run('kivi.Order.set_price_and_source_text',    $item_id, $price_src   ->source, $price_src   ->source_description, $item->sellprice_as_number, $price_editable)
+      ->run('kivi.Order.set_discount_and_source_text', $item_id, $discount_src->source, $discount_src->source_description, $item->discount_as_percent, $price_editable)
+      ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
+      ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
+      ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
+
+    if ($self->search_cvpartnumber) {
+      $self->get_item_cvpartnumber($item);
+      $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
+    }
+  }
+
+  $self->recalc();
+  $self->js_redisplay_line_values;
+  $self->js_redisplay_amounts_and_taxes;
+
+  $self->js->render();
+}
+
+sub action_save_phone_note {
+  my ($self) = @_;
+
+  if (!$::form->{phone_note}->{subject} || !$::form->{phone_note}->{body}) {
+    return $self->js->flash('error', t8('Phone note needs a subject and a body.'))->render;
+  }
+
+  my $phone_note;
+  if ($::form->{phone_note}->{id}) {
+    $phone_note = first { $_->id == $::form->{phone_note}->{id} } @{$self->order->phone_notes};
+    return $self->js->flash('error', t8('Phone note not found for this order.'))->render if !$phone_note;
+  }
+
+  $phone_note = SL::DB::Note->new() if !$phone_note;
+  my $is_new  = !$phone_note->id;
+
+  $phone_note->assign_attributes(%{ $::form->{phone_note} },
+                                 trans_id     => $self->order->id,
+                                 trans_module => 'oe',
+                                 employee     => SL::DB::Manager::Employee->current);
+
+  $phone_note->save;
+  $self->order(SL::DB::Order->new(id => $self->order->id)->load);
+
+  my $tab_as_html = $self->p->render('order/tabs/phone_notes', SELF => $self);
+
+  return $self->js
+    ->replaceWith('#phone-notes', $tab_as_html)
+    ->html('#num_phone_notes', (scalar @{$self->order->phone_notes}) ? ' (' . scalar @{$self->order->phone_notes} . ')' : '')
+    ->flash('info', $is_new ? t8('Phone note has been created.') : t8('Phone note has been updated.'))
+    ->render;
+}
+
+sub action_delete_phone_note {
+  my ($self) = @_;
+
+  my $phone_note = first { $_->id == $::form->{phone_note}->{id} } @{$self->order->phone_notes};
+
+  return $self->js->flash('error', t8('Phone note not found for this order.'))->render if !$phone_note;
+
+  $phone_note->delete;
+  $self->order(SL::DB::Order->new(id => $self->order->id)->load);
+
+  my $tab_as_html = $self->p->render('order/tabs/phone_notes', SELF => $self);
+
+  return $self->js
+    ->replaceWith('#phone-notes', $tab_as_html)
+    ->html('#num_phone_notes', (scalar @{$self->order->phone_notes}) ? ' (' . scalar @{$self->order->phone_notes} . ')' : '')
+    ->flash('info', t8('Phone note has been deleted.'))
+    ->render;
+}
+
+sub js_load_second_row {
+  my ($self, $item, $item_id, $do_parse) = @_;
+
+  if ($do_parse) {
+    # Parse values from form (they are formated while rendering (template)).
+    # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
+    # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
+    foreach my $var (@{ $item->cvars_by_config }) {
+      $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
+    }
+    $item->parse_custom_variable_values;
+  }
+
+  my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
+
+  $self->js
+    ->html('#second_row_' . $item_id, $row_as_html)
+    ->data('#second_row_' . $item_id, 'loaded', 1);
+}
+
+sub js_redisplay_line_values {
+  my ($self) = @_;
+
+  my $is_sales = $self->order->is_sales;
+
+  # sales orders with margins
+  my @data;
+  if ($is_sales) {
+    @data = map {
+      [
+       $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
+       $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
+       $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
+      ]} @{ $self->order->items_sorted };
+  } else {
+    @data = map {
+      [
+       $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
+      ]} @{ $self->order->items_sorted };
+  }
+
+  $self->js
+    ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
+}
+
+sub js_redisplay_amounts_and_taxes {
+  my ($self) = @_;
+
+  if (scalar @{ $self->{taxes} }) {
+    $self->js->show('#taxincluded_row_id');
+  } else {
+    $self->js->hide('#taxincluded_row_id');
+  }
+
+  if ($self->order->taxincluded) {
+    $self->js->hide('#subtotal_row_id');
+  } else {
+    $self->js->show('#subtotal_row_id');
+  }
+
+  if ($self->order->is_sales) {
+    my $is_neg = $self->order->marge_total < 0;
+    $self->js
+      ->html('#marge_total_id',   $::form->format_amount(\%::myconfig, $self->order->marge_total,   2))
+      ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
+      ->action_if( $is_neg, 'addClass',    '#marge_total_id',        'plus0')
+      ->action_if( $is_neg, 'addClass',    '#marge_percent_id',      'plus0')
+      ->action_if( $is_neg, 'addClass',    '#marge_percent_sign_id', 'plus0')
+      ->action_if(!$is_neg, 'removeClass', '#marge_total_id',        'plus0')
+      ->action_if(!$is_neg, 'removeClass', '#marge_percent_id',      'plus0')
+      ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
+  }
+
+  $self->js
+    ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
+    ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
+    ->remove('.tax_row')
+    ->insertBefore($self->build_tax_rows, '#amount_row_id');
+}
+
+sub js_redisplay_cvpartnumbers {
+  my ($self) = @_;
+
+  $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
+
+  my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
+
+  $self->js
+    ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
+}
+
+sub js_reset_order_and_item_ids_after_save {
+  my ($self) = @_;
+
+  $self->js
+    ->val('#id', $self->order->id)
+    ->val('#converted_from_oe_id', '')
+    ->val('#order_' . $self->nr_key(), $self->order->number);
+
+  my $idx = 0;
+  foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
+    next if !$self->order->items_sorted->[$idx]->id;
+    next if $form_item_id !~ m{^new};
+    $self->js
+      ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
+      ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
+      ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
+  } continue {
+    $idx++;
+  }
+  $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
+}
+
+#
+# helpers
+#
+
+sub init_valid_types {
+  [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
+}
+
+sub init_type {
+  my ($self) = @_;
+
+  if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
+    die "Not a valid type for order";
+  }
+
+  $self->type($::form->{type});
+}
+
+sub init_cv {
+  my ($self) = @_;
+
+  my $cv = (any { $self->type eq $_ } (sales_order_type(),    sales_quotation_type()))   ? 'customer'
+         : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
+         : die "Not a valid type for order";
+
+  return $cv;
+}
+
+sub init_search_cvpartnumber {
+  my ($self) = @_;
+
+  my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
+  my $search_cvpartnumber;
+  $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
+  $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
+
+  return $search_cvpartnumber;
+}
+
+sub init_show_update_button {
+  my ($self) = @_;
+
+  !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
+}
+
+sub init_p {
+  SL::Presenter->get;
+}
+
+sub init_order {
+  $_[0]->make_order;
+}
+
+sub init_all_price_factors {
+  SL::DB::Manager::PriceFactor->get_all;
+}
+
+sub init_part_picker_classification_ids {
+  my ($self)    = @_;
+  my $attribute = 'used_for_' . ($self->type =~ m{sales} ? 'sale' : 'purchase');
+
+  return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => [ $attribute => 1 ]) } ];
+}
+
+sub check_auth {
+  my ($self) = @_;
+
+  my $right_for = { map { $_ => $_.'_edit' . ' | ' . $_.'_view' } @{$self->valid_types} };
+
+  my $right   = $right_for->{ $self->type };
+  $right    ||= 'DOES_NOT_EXIST';
+
+  $::auth->assert($right);
+}
+
+sub check_auth_for_edit {
+  my ($self) = @_;
+
+  my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
+
+  my $right   = $right_for->{ $self->type };
+  $right    ||= 'DOES_NOT_EXIST';
+
+  $::auth->assert($right);
+}
+
+# build the selection box for contacts
+#
+# Needed, if customer/vendor changed.
+sub build_contact_select {
+  my ($self) = @_;
+
+  select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
+    value_key  => 'cp_id',
+    title_key  => 'full_name_dep',
+    default    => $self->order->cp_id,
+    with_empty => 1,
+    style      => 'width: 300px',
+  );
+}
+
+# build the selection box for the additional billing address
+#
+# Needed, if customer/vendor changed.
+sub build_billing_address_select {
+  my ($self) = @_;
+
+  return '' if $self->cv ne 'customer';
+
+  select_tag('order.billing_address_id',
+             [ {displayable_id => '', id => ''}, $self->order->{$self->cv}->additional_billing_addresses ],
+             value_key  => 'id',
+             title_key  => 'displayable_id',
+             default    => $self->order->billing_address_id,
+             with_empty => 0,
+             style      => 'width: 300px',
+  );
+}
+
+# build the selection box for shiptos
+#
+# Needed, if customer/vendor changed.
+sub build_shipto_select {
+  my ($self) = @_;
+
+  select_tag('order.shipto_id',
+             [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
+             value_key  => 'shipto_id',
+             title_key  => 'displayable_id',
+             default    => $self->order->shipto_id,
+             with_empty => 0,
+             style      => 'width: 300px',
+  );
+}
+
+# build the inputs for the cusom shipto dialog
+#
+# Needed, if customer/vendor changed.
+sub build_shipto_inputs {
+  my ($self) = @_;
+
+  my $content = $self->p->render('common/_ship_to_dialog',
+                                 vc_obj      => $self->order->customervendor,
+                                 cs_obj      => $self->order->custom_shipto,
+                                 cvars       => $self->order->custom_shipto->cvars_by_config,
+                                 id_selector => '#order_shipto_id');
+
+  div_tag($content, id => 'shipto_inputs');
+}
+
+# render the info line for business
+#
+# Needed, if customer/vendor changed.
+sub build_business_info_row
+{
+  $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
+}
+
+# build the rows for displaying taxes
+#
+# Called if amounts where recalculated and redisplayed.
+sub build_tax_rows {
+  my ($self) = @_;
+
+  my $rows_as_html;
+  foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
+    $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
+  }
+  return $rows_as_html;
+}
+
+
+sub render_price_dialog {
+  my ($self, $record_item) = @_;
+
+  my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
+
+  $self->js
+    ->run(
+      'kivi.io.price_chooser_dialog',
+      t8('Available Prices'),
+      $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
+    )
+    ->reinit_widgets;
+
+#   if (@errors) {
+#     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
+#     $self->js->show('#dialog_flash_error');
+#   }
+
+  $self->js->render;
+}
+
+sub load_order {
+  my ($self) = @_;
+
+  return if !$::form->{id};
+
+  $self->order(SL::DB::Order->new(id => $::form->{id})->load);
+
+  # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
+  # You need a custom shipto object to call cvars_by_config to get the cvars.
+  $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
+
+  return $self->order;
+}
+
+# load or create a new order object
+#
+# And assign changes from the form to this object.
+# If the order is loaded from db, check if items are deleted in the form,
+# remove them form the object and collect them for removing from db on saving.
+# Then create/update items from form (via make_item) and add them.
+sub make_order {
+  my ($self) = @_;
+
+  # add_items adds items to an order with no items for saving, but they cannot
+  # be retrieved via items until the order is saved. Adding empty items to new
+  # order here solves this problem.
+  my $order;
+  $order   = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
+  $order ||= SL::DB::Order->new(orderitems  => [],
+                                quotation   => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())),
+                                currency_id => $::instance_conf->get_currency_id(),);
+
+  my $cv_id_method = $self->cv . '_id';
+  if (!$::form->{id} && $::form->{$cv_id_method}) {
+    $order->$cv_id_method($::form->{$cv_id_method});
+    setup_order_from_cv($order);
+  }
+
+  my $form_orderitems                  = delete $::form->{order}->{orderitems};
+  my $form_periodic_invoices_config    = delete $::form->{order}->{periodic_invoices_config};
+
+  $order->assign_attributes(%{$::form->{order}});
+
+  $self->setup_custom_shipto_from_form($order, $::form);
+
+  if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) {
+    my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
+    $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
+  }
+
+  # remove deleted items
+  $self->item_ids_to_delete([]);
+  foreach my $idx (reverse 0..$#{$order->orderitems}) {
+    my $item = $order->orderitems->[$idx];
+    if (none { $item->id == $_->{id} } @{$form_orderitems}) {
+      splice @{$order->orderitems}, $idx, 1;
+      push @{$self->item_ids_to_delete}, $item->id;
+    }
+  }
+
+  my @items;
+  my $pos = 1;
+  foreach my $form_attr (@{$form_orderitems}) {
+    my $item = make_item($order, $form_attr);
+    $item->position($pos);
+    push @items, $item;
+    $pos++;
+  }
+  $order->add_items(grep {!$_->id} @items);
+
+  return $order;
+}
+
+# create or update items from form
+#
+# Make item objects from form values. For items already existing read from db.
+# Create a new item else. And assign attributes.
+sub make_item {
+  my ($record, $attr) = @_;
+
+  my $item;
+  $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
+
+  my $is_new = !$item;
+
+  # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
+  # they cannot be retrieved via custom_variables until the order/orderitem is
+  # saved. Adding empty custom_variables to new orderitem here solves this problem.
+  $item ||= SL::DB::OrderItem->new(custom_variables => []);
+
+  $item->assign_attributes(%$attr);
+
+  if ($is_new) {
+    my $texts = get_part_texts($item->part, $record->language_id);
+    $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
+    $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
+    $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
+  }
+
+  return $item;
+}
+
+# create a new item
+#
+# This is used to add one item
+sub new_item {
+  my ($record, $attr) = @_;
+
+  my $item = SL::DB::OrderItem->new;
+
+  # Remove attributes where the user left or set the inputs empty.
+  # So these attributes will be undefined and we can distinguish them
+  # from zero later on.
+  for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
+    delete $attr->{$_} if $attr->{$_} eq '';
+  }
+
+  $item->assign_attributes(%$attr);
+  $item->qty(1.0)                   if !$item->qty;
+  $item->unit($item->part->unit)    if !$item->unit;
+
+  my ($price_src, $discount_src) = get_best_price_and_discount_source($record, $item, 0);
+
+  my %new_attr;
+  $new_attr{description}            = $item->part->description     if ! $item->description;
+  $new_attr{qty}                    = 1.0                          if ! $item->qty;
+  $new_attr{price_factor_id}        = $item->part->price_factor_id if ! $item->price_factor_id;
+  $new_attr{sellprice}              = $price_src->price;
+  $new_attr{discount}               = $discount_src->discount;
+  $new_attr{active_price_source}    = $price_src;
+  $new_attr{active_discount_source} = $discount_src;
+  $new_attr{longdescription}        = $item->part->notes           if ! defined $attr->{longdescription};
+  $new_attr{project_id}             = $record->globalproject_id;
+  $new_attr{lastcost}               = $record->is_sales ? $item->part->lastcost : 0;
+
+  # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
+  # they cannot be retrieved via custom_variables until the order/orderitem is
+  # saved. Adding empty custom_variables to new orderitem here solves this problem.
+  $new_attr{custom_variables} = [];
+
+  my $texts = get_part_texts($item->part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
+
+  $item->assign_attributes(%new_attr, %{ $texts });
+
+  return $item;
+}
+
+sub setup_order_from_cv {
+  my ($order) = @_;
+
+  $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id language_id));
+
+  $order->intnotes($order->customervendor->notes);
+
+  return if !$order->is_sales;
+
+  $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
+  $order->taxincluded(defined($order->customer->taxincluded_checked)
+                      ? $order->customer->taxincluded_checked
+                      : $::myconfig{taxincluded_checked});
+
+  my $address = $order->customer->default_billing_address;;
+  $order->billing_address_id($address ? $address->id : undef);
+}
+
+# setup custom shipto from form
+#
+# The dialog returns form variables starting with 'shipto' and cvars starting
+# with 'shiptocvar_'.
+# Mark it to be deleted if a shipto from master data is selected
+# (i.e. order has a shipto).
+# Else, update or create a new custom shipto. If the fields are empty, it
+# will not be saved on save.
+sub setup_custom_shipto_from_form {
+  my ($self, $order, $form) = @_;
+
+  if ($order->shipto) {
+    $self->is_custom_shipto_to_delete(1);
+  } else {
+    my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
+
+    my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
+    my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
+
+    $custom_shipto->assign_attributes(%$shipto_attrs);
+    $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
+  }
+}
+
+# recalculate prices and taxes
+#
+# Using the PriceTaxCalculator. Store linetotals in the item objects.
+sub recalc {
+  my ($self) = @_;
+
+  my %pat = $self->order->calculate_prices_and_taxes();
+
+  $self->{taxes} = [];
+  foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
+    my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
+
+    push(@{ $self->{taxes} }, { amount    => $pat{taxes_by_tax_id}->{$tax_id},
+                                netamount => $netamount,
+                                tax       => SL::DB::Tax->new(id => $tax_id)->load });
+  }
+  pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}};
+}
+
+# get data for saving, printing, ..., that is not changed in the form
+#
+# Only cvars for now.
+sub get_unalterable_data {
+  my ($self) = @_;
+
+  foreach my $item (@{ $self->order->items }) {
+    # autovivify all cvars that are not in the form (cvars_by_config can do it).
+    # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
+    foreach my $var (@{ $item->cvars_by_config }) {
+      $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
+    }
+    $item->parse_custom_variable_values;
+  }
+}
+
+# delete the order
+#
+# And remove related files in the spool directory
+sub delete {
+  my ($self) = @_;
+
+  my $errors = [];
+  my $db     = $self->order->db;
+
+  $db->with_transaction(
+    sub {
+      my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
+      $self->order->delete;
+      my $spool = $::lx_office_conf{paths}->{spool};
+      unlink map { "$spool/$_" } @spoolfiles if $spool;
+
+      $self->save_history('DELETED');
+
+      1;
+  }) || push(@{$errors}, $db->error);
+
+  return $errors;
+}
+
+# save the order
+#
+# And delete items that are deleted in the form.
+sub save {
+  my ($self) = @_;
+
+  my $errors = [];
+  my $db     = $self->order->db;
+
+  # check for new or updated phone note
+  if ($::form->{phone_note}->{subject} || $::form->{phone_note}->{body}) {
+    if (!$::form->{phone_note}->{subject} || !$::form->{phone_note}->{body}) {
+      return [t8('Phone note needs a subject and a body.')];
+    }
+
+    my $phone_note;
+    if ($::form->{phone_note}->{id}) {
+      $phone_note = first { $_->id == $::form->{phone_note}->{id} } @{$self->order->phone_notes};
+      return [t8('Phone note not found for this order.')] if !$phone_note;
+    }
+
+    $phone_note = SL::DB::Note->new() if !$phone_note;
+    my $is_new  = !$phone_note->id;
+
+    $phone_note->assign_attributes(%{ $::form->{phone_note} },
+                                   trans_id     => $self->order->id,
+                                   trans_module => 'oe',
+                                   employee     => SL::DB::Manager::Employee->current);
+
+    $self->order->add_phone_notes($phone_note) if $is_new;
+  }
+
+  $db->with_transaction(sub {
+    # delete custom shipto if it is to be deleted or if it is empty
+    if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
+      $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
+      $self->order->custom_shipto(undef);
+    }
+
+    SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
+    $self->order->save(cascade => 1);
+
+    # link records
+    if ($::form->{converted_from_oe_id}) {
+      my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
+
+      foreach my $converted_from_oe_id (@converted_from_oe_ids) {
+        my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
+        $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
+        $src->link_to_record($self->order);
+      }
+      if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
+        my $idx = 0;
+        foreach (@{ $self->order->items_sorted }) {
+          my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
+          next if !$from_id;
+          SL::DB::RecordLink->new(from_table => 'orderitems',
+                                  from_id    => $from_id,
+                                  to_table   => 'orderitems',
+                                  to_id      => $_->id
+          )->save;
+          $idx++;
+        }
+      }
+
+      $self->link_requirement_specs_linking_to_created_from_objects(@converted_from_oe_ids);
+    }
+
+    $self->set_project_in_linked_requirement_specs if $self->order->globalproject_id;
+
+    $self->save_history('SAVED');
+
+    1;
+  }) || push(@{$errors}, $db->error);
+
+  return $errors;
+}
+
+sub workflow_sales_or_request_for_quotation {
+  my ($self) = @_;
+
+  # always save
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) for @{ $errors };
+    return $self->js->render();
+  }
+
+  my $destination_type = $::form->{type} eq sales_order_type() ? sales_quotation_type() : request_quotation_type();
+
+  $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
+  delete $::form->{id};
+
+  # no linked records from order to quotations
+  delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
+
+  # set item ids to new fake id, to identify them as new items
+  foreach my $item (@{$self->order->items_sorted}) {
+    $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+  }
+
+  # change form type
+  $::form->{type} = $destination_type;
+  $self->type($self->init_type);
+  $self->cv  ($self->init_cv);
+  $self->check_auth;
+
+  $self->recalc();
+  $self->get_unalterable_data();
+  $self->pre_render();
+
+  # trigger rendering values for second row as hidden, because they
+  # are loaded only on demand. So we need to keep the values from the
+  # source.
+  $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
+
+  $self->render(
+    'order/form',
+    title => $self->get_title_for('edit'),
+    %{$self->{template_args}}
+  );
+}
+
+sub workflow_sales_or_purchase_order {
+  my ($self) = @_;
+
+  # always save
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  my $destination_type = $::form->{type} eq sales_quotation_type()   ? sales_order_type()
+                       : $::form->{type} eq request_quotation_type() ? purchase_order_type()
+                       : $::form->{type} eq purchase_order_type()    ? sales_order_type()
+                       : $::form->{type} eq sales_order_type()       ? purchase_order_type()
+                       : '';
+
+  # check for direct delivery
+  # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
+  my $custom_shipto;
+  if (   $::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()
+      && $::form->{use_shipto} && $self->order->shipto) {
+    $custom_shipto = $self->order->shipto->clone('SL::DB::Order');
+  }
+
+  $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
+  $self->{converted_from_oe_id} = delete $::form->{id};
+
+  # set item ids to new fake id, to identify them as new items
+  foreach my $item (@{$self->order->items_sorted}) {
+    $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+  }
+
+  if ($::form->{type} eq sales_order_type() && $destination_type eq purchase_order_type()) {
+    if ($::form->{use_shipto}) {
+      $self->order->custom_shipto($custom_shipto) if $custom_shipto;
+    } else {
+      # remove any custom shipto if not wanted
+      $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
+    }
+  }
+
+  # change form type
+  $::form->{type} = $destination_type;
+  $self->type($self->init_type);
+  $self->cv  ($self->init_cv);
+  $self->check_auth;
+
+  $self->recalc();
+  $self->get_unalterable_data();
+  $self->pre_render();
+
+  # trigger rendering values for second row as hidden, because they
+  # are loaded only on demand. So we need to keep the values from the
+  # source.
+  $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
+
+  $self->render(
+    'order/form',
+    title => $self->get_title_for('edit'),
+    %{$self->{template_args}}
+  );
+}
+
+
+sub pre_render {
+  my ($self) = @_;
+
+  $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
+  $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
+  $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
+  $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->order->language_id ] ] );
+  $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
+                                                                                              deleted => 0 ] ],
+                                                                           sort_by => 'name');
+  $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
+                                                                                              deleted => 0 ] ],
+                                                                           sort_by => 'name');
+  $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
+                                                                                                        obsolete => 0 ] ]);
+  $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
+  $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
+  $self->{periodic_invoices_status}   = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
+  $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
+  $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
+
+  my $print_form = Form->new('');
+  $print_form->{type}        = $self->type;
+  $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
+  $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
+    form => $print_form,
+    options => {dialog_name_prefix => 'print_options.',
+                show_headers       => 1,
+                no_queue           => 1,
+                no_postscript      => 1,
+                no_opendocument    => 0,
+                no_html            => 0},
+  );
+
+  foreach my $item (@{$self->order->orderitems}) {
+    my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
+    $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
+    $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
+  }
+
+  if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
+    # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
+    # Do not use write_to_objects to prevent order->delivered to be set, because this should be
+    # the value from db, which can be set manually or is set when linked delivery orders are saved.
+    SL::Helper::ShippedQty->new->calculate($self->order)->write_to(\@{$self->order->items});
+  }
+
+  if ($self->order->number && $::instance_conf->get_webdav) {
+    my $webdav = SL::Webdav->new(
+      type     => $self->type,
+      number   => $self->order->number,
+    );
+    my @all_objects = $webdav->get_all_objects;
+    @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
+                                                    type => t8('File'),
+                                                    link => File::Spec->catfile($_->full_filedescriptor),
+                                                } } @all_objects;
+  }
+
+  if (   (any { $self->type eq $_ } (sales_quotation_type(), sales_order_type()))
+      && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
+    $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
+  }
+  $self->{template_args}->{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
+
+  $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
+
+  $self->{template_args}->{num_phone_notes} = scalar @{ $self->order->phone_notes || [] };
+
+  $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
+                                                         edit_periodic_invoices_config calculate_qty follow_up show_history);
+  $self->setup_edit_action_bar;
+}
+
+sub setup_edit_action_bar {
+  my ($self, %params) = @_;
+
+  my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
+                      || (($self->type eq sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
+                      || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
+
+  my @req_trans_cost_art = qw(kivi.Order.check_transport_cost_article_presence) x!!$::instance_conf->get_transport_cost_reminder_article_number_id;
+  my @req_cusordnumber   = qw(kivi.Order.check_cusordnumber_presence)           x($self->type eq sales_order_type() && $::instance_conf->get_order_warn_no_cusordnumber);
+
+  my $has_invoice_for_advance_payment;
+  if ($self->order->id && $self->type eq sales_order_type()) {
+    my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
+    $has_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
+  }
+
+  my $has_final_invoice;
+  if ($self->order->id && $self->type eq sales_order_type()) {
+    my $lr = $self->order->linked_records(direction => 'to', to => ['Invoice']);
+    $has_final_invoice               = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->type} @$lr;
+  }
+
+  my $right_for         = { map { $_ => $_.'_edit' } @{$self->valid_types} };
+  my $right             = $right_for->{ $self->type };
+  $right              ||= 'DOES_NOT_EXIST';
+  my $may_edit_create   = $::auth->assert($right, 'may fail');
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [
+          t8('Save'),
+          call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
+                                                    $::instance_conf->get_order_warn_no_deliverydate,
+          ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Close'),
+          call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
+                                                    $::instance_conf->get_order_warn_no_deliverydate,
+                                                    1
+          ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'],
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save as new'),
+          call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.')
+                     : !$self->order->id ? t8('This object has not been saved yet.')
+                     :                     undef,
+        ],
+      ], # end of combobox "Save"
+
+      combobox => [
+        action => [
+          t8('Workflow'),
+        ],
+        action => [
+          t8('Save and Quotation'),
+          submit   => [ '#order_form', { action => "Order/sales_quotation" } ],
+          checks   => [ @req_trans_cost_art, @req_cusordnumber ],
+          only_if  => (any { $self->type eq $_ } (sales_order_type())),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and RFQ'),
+          submit   => [ '#order_form', { action => "Order/request_for_quotation" } ],
+          only_if  => (any { $self->type eq $_ } (purchase_order_type())),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Sales Order'),
+          submit   => [ '#order_form', { action => "Order/sales_order" } ],
+          checks   => [ @req_trans_cost_art ],
+          only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Purchase Order'),
+          call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
+          checks    => [ @req_trans_cost_art, @req_cusordnumber ],
+          only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
+          disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Delivery Order'),
+          call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
+                                                                       $::instance_conf->get_order_warn_no_deliverydate,
+                                                                                                                        ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          only_if   => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())),
+          disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Supplier Delivery Order'),
+          call      => [ 'kivi.Order.save', 'save_and_supplier_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
+                                                                       $::instance_conf->get_order_warn_no_deliverydate,
+                                                                                                                        ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          only_if   => (any { $self->type eq $_ } (purchase_order_type())),
+          disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and Invoice'),
+          call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          ($has_invoice_for_advance_payment ? t8('Save and Further Invoice for Advance Payment') : t8('Save and Invoice for Advance Payment')),
+          call      => [ 'kivi.Order.save', 'save_and_invoice_for_advance_payment', $::instance_conf->get_order_warn_duplicate_parts ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
+                     : $has_final_invoice ? t8('This order has already a final invoice.')
+                     :                      undef,
+          only_if   => (any { $self->type eq $_ } (sales_order_type())),
+        ],
+        action => [
+          t8('Save and Final Invoice'),
+          call      => [ 'kivi.Order.save', 'save_and_final_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices',
+                         @req_trans_cost_art, @req_cusordnumber,
+          ],
+          disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
+                     : $has_final_invoice ? t8('This order has already a final invoice.')
+                     :                      undef,
+          only_if   => (any { $self->type eq $_ } (sales_order_type())) && $has_invoice_for_advance_payment,
+        ],
+        action => [
+          t8('Save and AP Transaction'),
+          call      => [ 'kivi.Order.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
+          only_if   => (any { $self->type eq $_ } (purchase_order_type())),
+          disabled  => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+
+      ], # end of combobox "Workflow"
+
+      combobox => [
+        action => [
+          t8('Export'),
+        ],
+        action => [
+          t8('Save and preview PDF'),
+          call     => [ 'kivi.Order.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
+                                                          $::instance_conf->get_order_warn_no_deliverydate,
+                      ],
+          checks   => [ @req_trans_cost_art, @req_cusordnumber ],
+          disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and print'),
+          call     => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
+                                                         $::instance_conf->get_order_warn_no_deliverydate,
+                      ],
+          checks   => [ @req_trans_cost_art, @req_cusordnumber ],
+          disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('Save and E-mail'),
+          id       => 'save_and_email_action',
+          call     => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
+                                                                         $::instance_conf->get_order_warn_no_deliverydate,
+                      ],
+          disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
+                    : !$self->order->id  ? t8('This object has not been saved yet.')
+                    :                      undef,
+        ],
+        action => [
+          t8('Download attachments of all parts'),
+          call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
+          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+          only_if  => $::instance_conf->get_doc_storage,
+        ],
+      ], # end of combobox "Export"
+
+      action => [
+        t8('Delete'),
+        call     => [ 'kivi.Order.delete_order' ],
+        confirm  => $::locale->text('Do you really want to delete this object?'),
+        disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
+                  : !$self->order->id  ? t8('This object has not been saved yet.')
+                  :                      undef,
+        only_if  => $deletion_allowed,
+      ],
+
+      combobox => [
+        action => [
+          t8('more')
+        ],
+        action => [
+          t8('History'),
+          call     => [ 'set_history_window', $self->order->id, 'id' ],
+          disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
+        ],
+        action => [
+          t8('Follow-Up'),
+          call     => [ 'kivi.Order.follow_up_window' ],
+          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+          only_if  => $::auth->assert('productivity', 1),
+        ],
+      ], # end of combobox "more"
+    );
+  }
+}
+
+sub generate_doc {
+  my ($self, $doc_ref, $params) = @_;
+
+  my $order  = $self->order;
+  my @errors = ();
+
+  my $print_form = Form->new('');
+  $print_form->{type}        = $order->type;
+  $print_form->{formname}    = $params->{formname} || $order->type;
+  $print_form->{format}      = $params->{format}   || 'pdf';
+  $print_form->{media}       = $params->{media}    || 'file';
+  $print_form->{groupitems}  = $params->{groupitems};
+  $print_form->{printer_id}  = $params->{printer_id};
+  $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
+
+  $order->language($params->{language});
+  $order->flatten_to_form($print_form, format_amounts => 1);
+
+  my $template_ext;
+  my $template_type;
+  if ($print_form->{format} =~ /(opendocument|oasis)/i) {
+    $template_ext  = 'odt';
+    $template_type = 'OpenDocument';
+  } elsif ($print_form->{format} =~ m{html}i) {
+    $template_ext  = 'html';
+    $template_type = 'HTML';
+  }
+
+  # search for the template
+  my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
+    name        => $print_form->{formname},
+    extension   => $template_ext,
+    email       => $print_form->{media} eq 'email',
+    language    => $params->{language},
+    printer_id  => $print_form->{printer_id},
+  );
+
+  if (!defined $template_file) {
+    push @errors, $::locale->text('Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.', join ', ', map { "'$_'"} @template_files);
+  }
+
+  return @errors if scalar @errors;
+
+  $print_form->throw_on_error(sub {
+    eval {
+      $print_form->prepare_for_printing;
+
+      $$doc_ref = SL::Helper::CreatePDF->create_pdf(
+        format        => $print_form->{format},
+        template_type => $template_type,
+        template      => $template_file,
+        variables     => $print_form,
+        variable_content_types => {
+          longdescription => 'html',
+          partnotes       => 'html',
+          notes           => 'html',
+          $::form->get_variable_content_types_for_cvars,
+        },
+      );
+      1;
+    } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
+  });
+
+  return @errors;
+}
+
+sub get_files_for_email_dialog {
+  my ($self) = @_;
+
+  my %files = map { ($_ => []) } qw(versions files vc_files part_files);
+
+  return %files if !$::instance_conf->get_doc_storage;
+
+  if ($self->order->id) {
+    $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
+    $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
+    $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
+    $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
+  }
+
+  my @parts =
+    uniq_by { $_->{id} }
+    map {
+      +{ id         => $_->part->id,
+         partnumber => $_->part->partnumber }
+    } @{$self->order->items_sorted};
+
+  foreach my $part (@parts) {
+    my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
+    push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
+  }
+
+  foreach my $key (keys %files) {
+    $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
+  }
+
+  return %files;
+}
+
+sub make_periodic_invoices_config_from_yaml {
+  my ($yaml_config) = @_;
+
+  return if !$yaml_config;
+  my $attr = SL::YAML::Load($yaml_config);
+  return if 'HASH' ne ref $attr;
+  return SL::DB::PeriodicInvoicesConfig->new(%$attr);
+}
+
+
+sub get_periodic_invoices_status {
+  my ($self, $config) = @_;
+
+  return                      if $self->type ne sales_order_type();
+  return t8('not configured') if !$config;
+
+  my $active = ('HASH' eq ref $config)                           ? $config->{active}
+             : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
+             :                                                     die "Cannot get status of periodic invoices config";
+
+  return $active ? t8('active') : t8('inactive');
+}
+
+sub get_title_for {
+  my ($self, $action) = @_;
+
+  return '' if none { lc($action)} qw(add edit);
+
+  # for locales:
+  # $::locale->text("Add Sales Order");
+  # $::locale->text("Add Purchase Order");
+  # $::locale->text("Add Quotation");
+  # $::locale->text("Add Request for Quotation");
+  # $::locale->text("Edit Sales Order");
+  # $::locale->text("Edit Purchase Order");
+  # $::locale->text("Edit Quotation");
+  # $::locale->text("Edit Request for Quotation");
+
+  $action = ucfirst(lc($action));
+  return $self->type eq sales_order_type()       ? $::locale->text("$action Sales Order")
+       : $self->type eq purchase_order_type()    ? $::locale->text("$action Purchase Order")
+       : $self->type eq sales_quotation_type()   ? $::locale->text("$action Quotation")
+       : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
+       : '';
+}
+
+sub get_item_cvpartnumber {
+  my ($self, $item) = @_;
+
+  return if !$self->search_cvpartnumber;
+  return if !$self->order->customervendor;
+
+  if ($self->cv eq 'vendor') {
+    my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
+    $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
+  } elsif ($self->cv eq 'customer') {
+    my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
+    $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
+  }
+}
+
+sub get_part_texts {
+  my ($part_or_id, $language_or_id, %defaults) = @_;
+
+  my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
+  my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
+  my $texts       = {
+    description     => $defaults{description}     // $part->description,
+    longdescription => $defaults{longdescription} // $part->notes,
+  };
+
+  return $texts unless $language_id;
+
+  my $translation = SL::DB::Manager::Translation->get_first(
+    where => [
+      parts_id    => $part->id,
+      language_id => $language_id,
+    ]);
+
+  $texts->{description}     = $translation->translation     if $translation && $translation->translation;
+  $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
+
+  return $texts;
+}
+
+sub get_best_price_and_discount_source {
+  my ($record, $item, $ignore_given) = @_;
+
+  my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
+
+  my $price_src;
+  if ( $item->part->is_assortment ) {
+    # add assortment items with price 0, as the components carry the price
+    $price_src = $price_source->price_from_source("");
+    $price_src->price(0);
+  } elsif (!$ignore_given && defined $item->sellprice) {
+    $price_src = $price_source->price_from_source("");
+    $price_src->price($item->sellprice);
+  } else {
+    $price_src = $price_source->best_price
+               ? $price_source->best_price
+               : $price_source->price_from_source("");
+    $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->exchangerate;
+    $price_src->price(0) if !$price_source->best_price;
+  }
+
+  my $discount_src;
+  if (!$ignore_given && defined $item->discount) {
+    $discount_src = $price_source->discount_from_source("");
+    $discount_src->discount($item->discount);
+  } else {
+    $discount_src = $price_source->best_discount
+                  ? $price_source->best_discount
+                  : $price_source->discount_from_source("");
+    $discount_src->discount(0) if !$price_source->best_discount;
+  }
+
+  return ($price_src, $discount_src);
+}
+
+sub sales_order_type {
+  'sales_order';
+}
+
+sub purchase_order_type {
+  'purchase_order';
+}
+
+sub sales_quotation_type {
+  'sales_quotation';
+}
+
+sub request_quotation_type {
+  'request_quotation';
+}
+
+sub nr_key {
+  return $_[0]->type eq sales_order_type()       ? 'ordnumber'
+       : $_[0]->type eq purchase_order_type()    ? 'ordnumber'
+       : $_[0]->type eq sales_quotation_type()   ? 'quonumber'
+       : $_[0]->type eq request_quotation_type() ? 'quonumber'
+       : '';
+}
+
+sub save_and_redirect_to {
+  my ($self, %params) = @_;
+
+  my $errors = $self->save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
+
+  my $text = $self->type eq sales_order_type()       ? $::locale->text('The order has been saved')
+           : $self->type eq purchase_order_type()    ? $::locale->text('The order has been saved')
+           : $self->type eq sales_quotation_type()   ? $::locale->text('The quotation has been saved')
+           : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
+           : '';
+  flash_later('info', $text);
+
+  $self->redirect_to(%params, id => $self->order->id);
+}
+
+sub save_history {
+  my ($self, $addition) = @_;
+
+  my $number_type = $self->order->type =~ m{order} ? 'ordnumber' : 'quonumber';
+  my $snumbers    = $number_type . '_' . $self->order->$number_type;
+
+  SL::DB::History->new(
+    trans_id    => $self->order->id,
+    employee_id => SL::DB::Manager::Employee->current->id,
+    what_done   => $self->order->type,
+    snumbers    => $snumbers,
+    addition    => $addition,
+  )->save;
+}
+
+sub store_doc_to_webdav_and_filemanagement {
+  my ($self, $content, $filename, $variant) = @_;
+
+  my $order = $self->order;
+  my @errors;
+
+  # copy file to webdav folder
+  if ($order->number && $::instance_conf->get_webdav_documents) {
+    my $webdav = SL::Webdav->new(
+      type     => $order->type,
+      number   => $order->number,
+    );
+    my $webdav_file = SL::Webdav::File->new(
+      webdav   => $webdav,
+      filename => $filename,
+    );
+    eval {
+      $webdav_file->store(data => \$content);
+      1;
+    } or do {
+      push @errors, t8('Storing the document to the WebDAV folder failed: #1', $@);
+    };
+  }
+  if ($order->id && $::instance_conf->get_doc_storage) {
+    eval {
+      SL::File->save(object_id     => $order->id,
+                     object_type   => $order->type,
+                     mime_type     => SL::MIME->mime_type_from_ext($filename),
+                     source        => 'created',
+                     file_type     => 'document',
+                     file_name     => $filename,
+                     file_contents => $content,
+                     print_variant => $variant);
+      1;
+    } or do {
+      push @errors, t8('Storing the document in the storage backend failed: #1', $@);
+    };
+  }
+
+  return @errors;
+}
+
+sub link_requirement_specs_linking_to_created_from_objects {
+  my ($self, @converted_from_oe_ids) = @_;
+
+  return unless @converted_from_oe_ids;
+
+  my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => \@converted_from_oe_ids ]);
+  foreach my $rs_order (@{ $rs_orders }) {
+    SL::DB::RequirementSpecOrder->new(
+      order_id            => $self->order->id,
+      requirement_spec_id => $rs_order->requirement_spec_id,
+      version_id          => $rs_order->version_id,
+    )->save;
+  }
+}
+
+sub set_project_in_linked_requirement_specs {
+  my ($self) = @_;
+
+  my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $self->order->id ]);
+  foreach my $rs_order (@{ $rs_orders }) {
+    next if $rs_order->requirement_spec->project_id == $self->order->globalproject_id;
+
+    $rs_order->requirement_spec->update_attributes(project_id => $self->order->globalproject_id);
+  }
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::Order - controller for orders
+
+=head1 SYNOPSIS
+
+This is a new form to enter orders, completely rewritten with the use
+of controller and java script techniques.
+
+The aim is to provide the user a better experience and a faster workflow. Also
+the code should be more readable, more reliable and better to maintain.
+
+=head2 Key Features
+
+=over 4
+
+=item *
+
+One input row, so that input happens every time at the same place.
+
+=item *
+
+Use of pickers where possible.
+
+=item *
+
+Possibility to enter more than one item at once.
+
+=item *
+
+Item list in a scrollable area, so that the workflow buttons stay at
+the bottom.
+
+=item *
+
+Reordering item rows with drag and drop is possible. Sorting item rows is
+possible (by partnumber, description, qty, sellprice and discount for now).
+
+=item *
+
+No C<update> is necessary. All entries and calculations are managed
+with ajax-calls and the page only reloads on C<save>.
+
+=item *
+
+User can see changes immediately, because of the use of java script
+and ajax.
+
+=back
+
+=head1 CODE
+
+=head2 Layout
+
+=over 4
+
+=item * C<SL/Controller/Order.pm>
+
+the controller
+
+=item * C<template/webpages/order/form.html>
+
+main form
+
+=item * C<template/webpages/order/tabs/basic_data.html>
+
+Main tab for basic_data.
+
+This is the only tab here for now. "linked records" and "webdav" tabs are
+reused from generic code.
+
+=over 4
+
+=item * C<template/webpages/order/tabs/_business_info_row.html>
+
+For displaying information on business type
+
+=item * C<template/webpages/order/tabs/_item_input.html>
+
+The input line for items
+
+=item * C<template/webpages/order/tabs/_row.html>
+
+One row for already entered items
+
+=item * C<template/webpages/order/tabs/_tax_row.html>
+
+Displaying tax information
+
+=item * C<template/webpages/order/tabs/_price_sources_dialog.html>
+
+Dialog for selecting price and discount sources
+
+=back
+
+=item * C<js/kivi.Order.js>
+
+java script functions
+
+=back
+
+=head1 TODO
+
+=over 4
+
+=item * testing
+
+=item * price sources: little symbols showing better price / better discount
+
+=item * select units in input row?
+
+=item * check for direct delivery (workflow sales order -> purchase order)
+
+=item * access rights
+
+=item * display weights
+
+=item * mtime check
+
+=item * optional client/user behaviour
+
+(transactions has to be set - department has to be set -
+ force project if enabled in client config)
+
+=back
+
+=head1 KNOWN BUGS AND CAVEATS
+
+=over 4
+
+=item *
+
+No indication that <shift>-up/down expands/collapses second row.
+
+=item *
+
+Table header is not sticky in the scrolling area.
+
+=item *
+
+Sorting does not include C<position>, neither does reordering.
+
+This behavior was implemented intentionally. But we can discuss, which behavior
+should be implemented.
+
+=back
+
+=head1 To discuss / Nice to have
+
+=over 4
+
+=item *
+
+How to expand/collapse second row. Now it can be done clicking the icon or
+<shift>-up/down.
+
+=item *
+
+This controller uses a (changed) copy of the template for the PriceSource
+dialog. Maybe there could be used one code source.
+
+=item *
+
+Rounding-differences between this controller (PriceTaxCalculator) and the old
+form. This is not only a problem here, but also in all parts using the PTC.
+There exists a ticket and a patch. This patch should be testet.
+
+=item *
+
+An indicator, if the actual inputs are saved (like in an
+editor or on text processing application).
+
+=item *
+
+A warning when leaving the page without saveing unchanged inputs.
+
+
+=back
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Controller/OrderItem.pm b/SL/Controller/OrderItem.pm
new file mode 100644 (file)
index 0000000..7cdfb35
--- /dev/null
@@ -0,0 +1,125 @@
+package SL::Controller::OrderItem;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+use SL::DB::Order;
+use SL::DB::OrderItem;
+use SL::DB::Customer;
+use SL::DB::Part;
+use SL::Controller::Helper::GetModels;
+use SL::Controller::Helper::ParseFilter;
+use SL::Locale::String qw(t8);
+
+__PACKAGE__->run_before('check_auth');
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar'                => [ qw(orderitems) ],
+  'scalar --get_set_init' => [ qw(model) ],
+);
+
+my %sort_columns = (
+  partnumber        => t8('Part Number'),
+  ordnumber         => t8('Order'),
+  customer          => t8('Customer'),
+  transdate         => t8('Date'),
+);
+
+sub action_search {
+
+  my ($self, %params) = @_;
+
+  my $title = t8("Sold order items");
+
+  $::request->layout->use_javascript('client_js.js');
+
+  # The actual loading of orderitems happens in action_order_item_list_dynamic_table
+  # which is processed inside this template and automatically called upon
+  # loading. This causes all filtered orderitems to be displayed,
+  # there is no paginate mechanism or export
+  $self->render('order_items_search/order_items', { layout => 1, process => 1 },
+                                                  title         => $title,
+               );
+}
+
+
+sub action_order_item_list_dynamic_table {
+  my ($self) = @_;
+
+  $self->orderitems( $self->model->get );
+
+
+  $self->add_linked_delivery_order_items;
+
+  $self->render('order_items_search/_order_item_list', { layout  => 0 , process => 1 });
+}
+
+sub add_linked_delivery_order_items {
+  my ($self) = @_;
+
+  my $qty_round = 2;
+
+  foreach my $orderitem ( @{ $self->orderitems } ) {
+    my $dois = $orderitem->linked_delivery_order_items;
+    $orderitem->{deliveryorders} = join('<br>', map { $_->displayable_delivery_order_info($qty_round) } @{$dois});
+  };
+};
+
+sub init_model {
+  my ($self) = @_;
+
+  SL::Controller::Helper::GetModels->new(
+    controller => $self,
+    model      => 'OrderItem',
+    query      => [ SL::DB::Manager::Order->type_filter('sales_order') ],
+    sorted       => {
+      _default     => {
+        by           => 'transdate',
+        dir          => 0,
+      },
+      %sort_columns,
+    } ,
+    with_objects    => [ 'order', 'order.customer', 'part' ],
+  );
+}
+
+sub check_auth {
+  $::auth->assert('sales_order_edit');
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::OrderItem - Controller for OrderItems
+
+=head2 OVERVIEW
+
+Controller for quickly finding orderitems in sales orders. For example the
+customer phones you, saying he would like to order another one of the green
+thingies he ordered 2 years ago. You have no idea what he is referring to, but
+you can quickly filter by customer (a customerpicker) and e.g. part description
+or partnumber or order date, successively narrowing down the search. The
+resulting list is updated dynamically after keypresses.
+
+=head1 Usage
+
+Certain fields can be preset by passing them as get parameters in the URL, so
+you could create links to this report:
+
+ controller.pl?action=OrderItem/search&ordnumber=24
+ controller.pl?action=OrderItem/search&customer_id=3455
+
+=head1 TODO AND CAVEATS
+
+=over 4
+
+=item * amount of results is limited
+
+=back
+
+=cut
index bd5fe6a..aae4e53 100644 (file)
@@ -5,38 +5,621 @@ use parent qw(SL::Controller::Base);
 
 use Clone qw(clone);
 use SL::DB::Part;
+use SL::DB::PartsGroup;
+use SL::DB::PriceRuleItem;
+use SL::DB::Shop;
 use SL::Controller::Helper::GetModels;
 use SL::Locale::String qw(t8);
 use SL::JSON;
+use List::Util qw(sum);
+use List::UtilsBy qw(extract_by);
+use SL::Helper::Flash;
+use Data::Dumper;
+use DateTime;
+use SL::DB::History;
+use SL::DB::Helper::ValidateAssembly qw(validate_assembly);
+use SL::CVar;
+use SL::MoreCommon qw(save_form);
+use Carp;
+use SL::Presenter::EscapedText qw(escape is_escaped);
+use SL::Presenter::Tag qw(select_tag);
 
 use Rose::Object::MakeMethods::Generic (
-  'scalar --get_set_init' => [ qw(parts models part) ],
+  'scalar --get_set_init' => [ qw(parts models part p warehouses multi_items_models
+                                  makemodels shops_not_assigned
+                                  customerprices
+                                  orphaned
+                                  assortment assortment_items assembly assembly_items
+                                  all_pricegroups all_translations all_partsgroups all_units
+                                  all_buchungsgruppen all_payment_terms all_warehouses
+                                  parts_classification_filter
+                                  all_languages all_units all_price_factors) ],
+  'scalar'                => [ qw(warehouse bin stock_amounts journal) ],
 );
 
 # safety
 __PACKAGE__->run_before(sub { $::auth->assert('part_service_assembly_edit') },
                         except => [ qw(ajax_autocomplete part_picker_search part_picker_result) ]);
 
+__PACKAGE__->run_before(sub { $::auth->assert('developer') },
+                        only => [ qw(test_page) ]);
+
+__PACKAGE__->run_before('check_part_id', only   => [ qw(edit delete) ]);
+
+# actions for editing parts
+#
+sub action_add_part {
+  my ($self, %params) = @_;
+
+  $self->part( SL::DB::Part->new_part );
+  $self->add;
+};
+
+sub action_add_service {
+  my ($self, %params) = @_;
+
+  $self->part( SL::DB::Part->new_service );
+  $self->add;
+};
+
+sub action_add_assembly {
+  my ($self, %params) = @_;
+
+  $self->part( SL::DB::Part->new_assembly );
+  $self->add;
+};
+
+sub action_add_assortment {
+  my ($self, %params) = @_;
+
+  $self->part( SL::DB::Part->new_assortment );
+  $self->add;
+};
+
+sub action_add_from_record {
+  my ($self) = @_;
+
+  check_has_valid_part_type($::form->{part}{part_type});
+
+  die 'parts_classification_type must be "sales" or "purchases"'
+    unless $::form->{parts_classification_type} =~ m/^(sales|purchases)$/;
+
+  $self->parse_form;
+  $self->add;
+}
+
+sub action_add {
+  my ($self) = @_;
+
+  check_has_valid_part_type($::form->{part_type});
+
+  $self->action_add_part       if $::form->{part_type} eq 'part';
+  $self->action_add_service    if $::form->{part_type} eq 'service';
+  $self->action_add_assembly   if $::form->{part_type} eq 'assembly';
+  $self->action_add_assortment if $::form->{part_type} eq 'assortment';
+};
+
+sub action_save {
+  my ($self, %params) = @_;
+
+  # checks that depend only on submitted $::form
+  $self->check_form or return $self->js->render;
+
+  my $is_new = !$self->part->id; # $ part gets loaded here
+
+  # check that the part hasn't been modified
+  unless ( $is_new ) {
+    $self->check_part_not_modified or
+      return $self->js->error(t8('The document has been changed by another user. Please reopen it in another window and copy the changes to the new window'))->render;
+  }
+
+  if (    $is_new
+       && $::form->{part}{partnumber}
+       && SL::DB::Manager::Part->find_by(partnumber => $::form->{part}{partnumber})
+     ) {
+    return $self->js->error(t8('The partnumber is already being used'))->render;
+  }
+
+  $self->parse_form;
+
+  my @errors = $self->part->validate;
+  return $self->js->error(@errors)->render if @errors;
+
+  # $self->part has been loaded, parsed and validated without errors and is ready to be saved
+  $self->part->db->with_transaction(sub {
+
+    $self->part->save(cascade => 1);
+
+    SL::DB::History->new(
+      trans_id    => $self->part->id,
+      snumbers    => 'partnumber_' . $self->part->partnumber,
+      employee_id => SL::DB::Manager::Employee->current->id,
+      what_done   => 'part',
+      addition    => 'SAVED',
+    )->save();
+
+    CVar->save_custom_variables(
+      dbh           => $self->part->db->dbh,
+      module        => 'IC',
+      trans_id      => $self->part->id,
+      variables     => $::form, # $::form->{cvar} would be nicer
+      save_validity => 1,
+    );
+
+    1;
+  }) or return $self->js->error(t8('The item couldn\'t be saved!') . " " . $self->part->db->error )->render;
+
+  flash_later('info', $is_new ? t8('The item has been created.') . " " . $self->part->displayable_name : t8('The item has been saved.'));
+
+  if ( $::form->{callback} ) {
+    $self->redirect_to($::form->unescape($::form->{callback}) . '&new_parts_id=' . $self->part->id);
+
+  } else {
+    # default behaviour after save: reload item, this also resets last_modification!
+    $self->redirect_to(controller => 'Part', action => 'edit', 'part.id' => $self->part->id);
+  }
+}
+
+sub action_save_and_purchase_order {
+  my ($self) = @_;
+
+  my $session_value;
+  if (1 == scalar @{$self->part->makemodels}) {
+    my $prepared_form           = Form->new('');
+    $prepared_form->{vendor_id} = $self->part->makemodels->[0]->make;
+    $session_value              = $::auth->save_form_in_session(form => $prepared_form);
+  }
+
+  $::form->{callback} = $self->url_for(
+    controller   => 'Order',
+    action       => 'return_from_create_part',
+    type         => 'purchase_order',
+    previousform => $session_value,
+  );
+
+  $self->_run_action('save');
+}
+
+sub action_abort {
+  my ($self) = @_;
+
+  if ( $::form->{callback} ) {
+    $self->redirect_to($::form->unescape($::form->{callback}));
+  }
+}
+
+sub action_delete {
+  my ($self) = @_;
+
+  my $db = $self->part->db; # $self->part has a get_set_init on $::form
+
+  my $partnumber = $self->part->partnumber; # remember for history log
+
+  $db->do_transaction(
+    sub {
+
+      # delete part, together with relationships that don't already
+      # have an ON DELETE CASCADE, e.g. makemodel and translation.
+      $self->part->delete(cascade => 1);
+
+      SL::DB::History->new(
+        trans_id    => $self->part->id,
+        snumbers    => 'partnumber_' . $partnumber,
+        employee_id => SL::DB::Manager::Employee->current->id,
+        what_done   => 'part',
+        addition    => 'DELETED',
+      )->save();
+      1;
+  }) or return $self->js->error(t8('The item couldn\'t be deleted!') . " " . $self->part->db->error)->render;
+
+  flash_later('info', t8('The item has been deleted.'));
+  if ( $::form->{callback} ) {
+    $self->redirect_to($::form->unescape($::form->{callback}));
+  } else {
+    $self->redirect_to(controller => 'ic.pl', action => 'search', searchitems => 'article');
+  }
+}
+
+sub action_use_as_new {
+  my ($self, %params) = @_;
+
+  my $oldpart = SL::DB::Manager::Part->find_by( id => $::form->{old_id}) or die "can't find old part";
+  $::form->{oldpartnumber} = $oldpart->partnumber;
+
+  $self->part($oldpart->clone_and_reset_deep);
+  $self->parse_form;
+  $self->part->partnumber(undef);
+
+  $self->render_form;
+}
+
+sub action_edit {
+  my ($self, %params) = @_;
+
+  $self->render_form;
+}
+
+sub render_form {
+  my ($self, %params) = @_;
+
+  $self->_set_javascript;
+  $self->_setup_form_action_bar;
+
+  my (%assortment_vars, %assembly_vars);
+  %assortment_vars = %{ $self->prepare_assortment_render_vars } if $self->part->is_assortment;
+  %assembly_vars   = %{ $self->prepare_assembly_render_vars   } if $self->part->is_assembly;
+
+  $params{CUSTOM_VARIABLES}  = CVar->get_custom_variables(module => 'IC', trans_id => $self->part->id);
+
+  if (scalar @{ $params{CUSTOM_VARIABLES} }) {
+    CVar->render_inputs('variables' => $params{CUSTOM_VARIABLES}, show_disabled_message => 1, partsgroup_id => $self->part->partsgroup_id);
+    $params{CUSTOM_VARIABLES_FIRST_TAB}       = [];
+    @{ $params{CUSTOM_VARIABLES_FIRST_TAB} }  = extract_by { $_->{first_tab} == 1 } @{ $params{CUSTOM_VARIABLES} };
+  }
+
+  my %title_hash = ( part       => t8('Edit Part'),
+                     assembly   => t8('Edit Assembly'),
+                     service    => t8('Edit Service'),
+                     assortment => t8('Edit Assortment'),
+                   );
+
+  $self->part->prices([])       unless $self->part->prices;
+  $self->part->translations([]) unless $self->part->translations;
+
+  $self->render(
+    'part/form',
+    title             => $title_hash{$self->part->part_type},
+    %assortment_vars,
+    %assembly_vars,
+    translations_map  => { map { ($_->language_id   => $_) } @{$self->part->translations} },
+    prices_map        => { map { ($_->pricegroup_id => $_) } @{$self->part->prices      } },
+    oldpartnumber     => $::form->{oldpartnumber},
+    old_id            => $::form->{old_id},
+    %params,
+  );
+}
+
+sub action_history {
+  my ($self) = @_;
+
+  my $history_entries = SL::DB::Part->new(id => $::form->{part}{id})->history_entries;
+  $_[0]->render('part/history', { layout => 0 },
+                                  history_entries => $history_entries);
+}
+
+sub action_inventory {
+  my ($self) = @_;
+
+  $::auth->assert('warehouse_contents');
+
+  $self->stock_amounts($self->part->get_simple_stock_sql);
+  $self->journal($self->part->get_mini_journal);
+
+  $_[0]->render('part/_inventory_data', { layout => 0 });
+};
+
+sub action_update_item_totals {
+  my ($self) = @_;
+
+  my $part_type = $::form->{part_type};
+  die unless $part_type =~ /^(assortment|assembly)$/;
+
+  my $sellprice_sum    = $self->recalc_item_totals(part_type => $part_type, price_type => 'sellcost');
+  my $lastcost_sum     = $self->recalc_item_totals(part_type => $part_type, price_type => 'lastcost');
+  my $items_weight_sum = $self->recalc_item_totals(part_type => $part_type, price_type => 'weight');
+
+  my $sum_diff      = $sellprice_sum-$lastcost_sum;
+
+  $self->js
+    ->html('#items_sellprice_sum',       $::form->format_amount(\%::myconfig, $sellprice_sum, 2, 0))
+    ->html('#items_lastcost_sum',        $::form->format_amount(\%::myconfig, $lastcost_sum,  2, 0))
+    ->html('#items_sum_diff',            $::form->format_amount(\%::myconfig, $sum_diff,      2, 0))
+    ->html('#items_sellprice_sum_basic', $::form->format_amount(\%::myconfig, $sellprice_sum, 2, 0))
+    ->html('#items_lastcost_sum_basic',  $::form->format_amount(\%::myconfig, $lastcost_sum,  2, 0))
+    ->html('#items_weight_sum_basic'   , $::form->format_amount(\%::myconfig, $items_weight_sum))
+    ->no_flash_clear->render();
+}
+
+sub action_add_multi_assortment_items {
+  my ($self) = @_;
+
+  my $item_objects = $self->parse_add_items_to_objects(part_type => 'assortment');
+  my $html         = $self->render_assortment_items_to_html($item_objects);
+
+  $self->js->run('kivi.Part.close_picker_dialogs')
+           ->append('#assortment_rows', $html)
+           ->run('kivi.Part.renumber_positions')
+           ->run('kivi.Part.assortment_recalc')
+           ->render();
+}
+
+sub action_add_multi_assembly_items {
+  my ($self) = @_;
+
+  my $item_objects = $self->parse_add_items_to_objects(part_type => 'assembly');
+  my @checked_objects;
+  foreach my $item (@{$item_objects}) {
+    my $errstr = validate_assembly($item->part,$self->part);
+    $self->js->flash('error',$errstr) if     $errstr;
+    push (@checked_objects,$item)     unless $errstr;
+  }
+
+  my $html = $self->render_assembly_items_to_html(\@checked_objects);
+
+  $self->js->run('kivi.Part.close_picker_dialogs')
+           ->append('#assembly_rows', $html)
+           ->run('kivi.Part.renumber_positions')
+           ->run('kivi.Part.assembly_recalc')
+           ->render();
+}
+
+sub action_add_assortment_item {
+  my ($self, %params) = @_;
+
+  validate_add_items() or return $self->js->error(t8("No part was selected."))->render;
+
+  carp('Too many objects passed to add_assortment_item') if @{$::form->{add_items}} > 1;
+
+  my $add_item_id = $::form->{add_items}->[0]->{parts_id};
+  if ( $add_item_id && grep { $add_item_id == $_->parts_id } @{ $self->assortment_items } ) {
+    return $self->js->flash('error', t8("This part has already been added."))->render;
+  };
+
+  my $number_of_items = scalar @{$self->assortment_items};
+  my $item_objects    = $self->parse_add_items_to_objects(part_type => 'assortment');
+  my $html            = $self->render_assortment_items_to_html($item_objects, $number_of_items);
+
+  push(@{$self->assortment_items}, @{$item_objects});
+  my $part = SL::DB::Part->new(part_type => 'assortment');
+  $part->assortment_items(@{$self->assortment_items});
+  my $items_sellprice_sum = $part->items_sellprice_sum;
+  my $items_lastcost_sum  = $part->items_lastcost_sum;
+  my $items_sum_diff      = $items_sellprice_sum - $items_lastcost_sum;
+
+  $self->js
+    ->append('#assortment_rows'        , $html)  # append in tbody
+    ->val('.add_assortment_item_input' , '')
+    ->run('kivi.Part.focus_last_assortment_input')
+    ->html("#items_sellprice_sum", $::form->format_amount(\%::myconfig, $items_sellprice_sum, 2, 0))
+    ->html("#items_lastcost_sum",  $::form->format_amount(\%::myconfig, $items_lastcost_sum,  2, 0))
+    ->html("#items_sum_diff",      $::form->format_amount(\%::myconfig, $items_sum_diff,      2, 0))
+    ->html('#items_sellprice_sum_basic', $::form->format_amount(\%::myconfig, $items_sellprice_sum, 2, 0))
+    ->html('#items_lastcost_sum_basic',  $::form->format_amount(\%::myconfig, $items_lastcost_sum,  2, 0))
+    ->render;
+}
+
+sub action_add_assembly_item {
+  my ($self) = @_;
+
+  validate_add_items() or return $self->js->error(t8("No part was selected."))->render;
+
+  carp('Too many objects passed to add_assembly_item') if @{$::form->{add_items}} > 1;
+
+  my $add_item_id = $::form->{add_items}->[0]->{parts_id};
+
+  my $duplicate_warning = 0; # duplicates are allowed, just warn
+  if ( $add_item_id && grep { $add_item_id == $_->parts_id } @{ $self->assembly_items } ) {
+    $duplicate_warning++;
+  };
+
+  my $number_of_items = scalar @{$self->assembly_items};
+  my $item_objects    = $self->parse_add_items_to_objects(part_type => 'assembly');
+  if ($add_item_id ) {
+    foreach my $item (@{$item_objects}) {
+      my $errstr = validate_assembly($item->part,$self->part);
+      return $self->js->flash('error',$errstr)->render if $errstr;
+    }
+  }
+
+
+  my $html            = $self->render_assembly_items_to_html($item_objects, $number_of_items);
+
+  $self->js->flash('info', t8("This part has already been added.")) if $duplicate_warning;
+
+  push(@{$self->assembly_items}, @{$item_objects});
+  my $part = SL::DB::Part->new(part_type => 'assembly');
+  $part->assemblies(@{$self->assembly_items});
+  my $items_sellprice_sum = $part->items_sellprice_sum;
+  my $items_lastcost_sum  = $part->items_lastcost_sum;
+  my $items_sum_diff      = $items_sellprice_sum - $items_lastcost_sum;
+  my $items_weight_sum    = $part->items_weight_sum;
+
+  $self->js
+    ->append('#assembly_rows', $html)  # append in tbody
+    ->val('.add_assembly_item_input' , '')
+    ->run('kivi.Part.focus_last_assembly_input')
+    ->html('#items_sellprice_sum', $::form->format_amount(\%::myconfig, $items_sellprice_sum, 2, 0))
+    ->html('#items_lastcost_sum' , $::form->format_amount(\%::myconfig, $items_lastcost_sum , 2, 0))
+    ->html('#items_sum_diff',      $::form->format_amount(\%::myconfig, $items_sum_diff     , 2, 0))
+    ->html('#items_sellprice_sum_basic', $::form->format_amount(\%::myconfig, $items_sellprice_sum, 2, 0))
+    ->html('#items_lastcost_sum_basic' , $::form->format_amount(\%::myconfig, $items_lastcost_sum , 2, 0))
+    ->html('#items_weight_sum_basic'   , $::form->format_amount(\%::myconfig, $items_weight_sum))
+    ->render;
+}
+
+sub action_show_multi_items_dialog {
+  my ($self) = @_;
+
+  my $search_term = $self->models->filtered->laundered->{all_substr_multi__ilike};
+  $search_term  ||= $self->models->filtered->laundered->{all_with_makemodel_substr_multi__ilike};
+  $search_term  ||= $self->models->filtered->laundered->{all_with_customer_partnumber_substr_multi__ilike};
+
+  $_[0]->render('part/_multi_items_dialog', { layout => 0 },
+                all_partsgroups => SL::DB::Manager::PartsGroup->get_all,
+                search_term     => $search_term
+  );
+}
+
+sub action_multi_items_update_result {
+  my $max_count = $::form->{limit};
+
+  my $count = $_[0]->multi_items_models->count;
+
+  if ($count == 0) {
+    my $text = escape($::locale->text('No results.'));
+    $_[0]->render($text, { layout => 0 });
+  } elsif ($max_count && $count > $max_count) {
+    my $text = escape($::locale->text('Too many results (#1 from #2).', $count, $max_count));
+    $_[0]->render($text, { layout => 0 });
+  } else {
+    my $multi_items = $_[0]->multi_items_models->get;
+    $_[0]->render('part/_multi_items_result', { layout => 0 },
+                  multi_items => $multi_items);
+  }
+}
+
+sub action_add_makemodel_row {
+  my ($self) = @_;
+
+  my $vendor_id = $::form->{add_makemodel};
+
+  my $vendor = SL::DB::Manager::Vendor->find_by(id => $vendor_id) or
+    return $self->js->error(t8("No vendor selected or found!"))->render;
+
+  if ( grep { $vendor_id == $_->make } @{ $self->makemodels } ) {
+    $self->js->flash('info', t8("This vendor has already been added."));
+  };
+
+  my $position = scalar @{$self->makemodels} + 1;
+
+  my $mm = SL::DB::MakeModel->new(# parts_id    => $::form->{part}->{id},
+                                  make        => $vendor->id,
+                                  model       => '',
+                                  lastcost    => 0,
+                                  sortorder    => $position,
+                                 ) or die "Can't create MakeModel object";
+
+  my $row_as_html = $self->p->render('part/_makemodel_row',
+                                     makemodel => $mm,
+                                     listrow   => $position % 2 ? 0 : 1,
+  );
+
+  # after selection focus on the model field in the row that was just added
+  $self->js
+    ->append('#makemodel_rows', $row_as_html)  # append in tbody
+    ->val('.add_makemodel_input', '')
+    ->run('kivi.Part.focus_last_makemodel_input')
+    ->render;
+}
+
+sub action_add_customerprice_row {
+  my ($self) = @_;
+
+  my $customer_id = $::form->{add_customerprice};
+
+  my $customer = SL::DB::Manager::Customer->find_by(id => $customer_id)
+    or return $self->js->error(t8("No customer selected or found!"))->render;
+
+  if (grep { $customer_id == $_->customer_id } @{ $self->customerprices }) {
+    $self->js->flash('info', t8("This customer has already been added."));
+  }
+
+  my $position = scalar @{ $self->customerprices } + 1;
+
+  my $cu = SL::DB::PartCustomerPrice->new(
+                      customer_id         => $customer->id,
+                      customer_partnumber => '',
+                      price               => 0,
+                      sortorder           => $position,
+  ) or die "Can't create Customerprice object";
+
+  my $row_as_html = $self->p->render(
+                                     'part/_customerprice_row',
+                                      customerprice => $cu,
+                                      listrow       => $position % 2 ? 0
+                                                                     : 1,
+  );
+
+  $self->js->append('#customerprice_rows', $row_as_html)    # append in tbody
+           ->val('.add_customerprice_input', '')
+           ->run('kivi.Part.focus_last_customerprice_input')->render;
+}
+
+sub action_reorder_items {
+  my ($self) = @_;
+
+  my $part_type = $::form->{part_type};
+
+  my %sort_keys = (
+    partnumber  => sub { $_[0]->part->partnumber },
+    description => sub { $_[0]->part->description },
+    qty         => sub { $_[0]->qty },
+    sellprice   => sub { $_[0]->part->sellprice },
+    lastcost    => sub { $_[0]->part->lastcost },
+    partsgroup  => sub { $_[0]->part->partsgroup_id ? $_[0]->part->partsgroup->partsgroup : '' },
+  );
+
+  my $method = $sort_keys{$::form->{order_by}};
+
+  my @items;
+  if ($part_type eq 'assortment') {
+    @items = @{ $self->assortment_items };
+  } else {
+    @items = @{ $self->assembly_items };
+  };
+
+  my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @items;
+  if ($::form->{order_by} =~ /^(qty|sellprice|lastcost)$/) {
+    if ($::form->{sort_dir}) {
+      @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
+    } else {
+      @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
+    }
+  } else {
+    if ($::form->{sort_dir}) {
+      @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
+    } else {
+      @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
+    }
+  };
+
+  $self->js->run('kivi.Part.redisplay_items', \@to_sort)->render;
+}
+
+sub action_warehouse_changed {
+  my ($self) = @_;
+
+  if ($::form->{warehouse_id} ) {
+    $self->warehouse(SL::DB::Manager::Warehouse->find_by_or_create(id => $::form->{warehouse_id}));
+    die unless ref($self->warehouse) eq 'SL::DB::Warehouse';
+
+    if ( $self->warehouse->id and @{$self->warehouse->bins} ) {
+      $self->bin($self->warehouse->bins_sorted->[0]);
+      $self->js
+        ->html('#bin', $self->build_bin_select)
+        ->focus('#part_bin_id');
+      return $self->js->render;
+    }
+  }
+
+  # no warehouse was selected, empty the bin field and reset the id
+  $self->js
+       ->val('#part_bin_id', undef)
+       ->html('#bin', '');
+
+  return $self->js->render;
+}
+
 sub action_ajax_autocomplete {
   my ($self, %params) = @_;
 
   # if someone types something, and hits enter, assume he entered the full name.
   # if something matches, treat that as sole match
-  # unfortunately get_models can't do more than one per package atm, so we d it
-  # the oldfashioned way.
+  # since we need a second get models instance with different filters for that,
+  # we only modify the original filter temporarily in place
   if ($::form->{prefer_exact}) {
+    local $::form->{filter}{'all::ilike'}                          = delete local $::form->{filter}{'all:substr:multi::ilike'};
+    local $::form->{filter}{'all_with_makemodel::ilike'}           = delete local $::form->{filter}{'all_with_makemodel:substr:multi::ilike'};
+    local $::form->{filter}{'all_with_customer_partnumber::ilike'} = delete local $::form->{filter}{'all_with_customer_partnumber:substr:multi::ilike'};
+
+    my $exact_models = SL::Controller::Helper::GetModels->new(
+      controller   => $self,
+      sorted       => 0,
+      paginated    => { per_page => 2 },
+      with_objects => [ qw(unit_obj classification) ],
+    );
     my $exact_matches;
-    if (1 == scalar @{ $exact_matches = SL::DB::Manager::Part->get_all(
-      query => [
-        obsolete => 0,
-        SL::DB::Manager::Part->type_filter($::form->{filter}{type}),
-        or => [
-          description => { ilike => $::form->{filter}{'all:substr:multi::ilike'} },
-          partnumber  => { ilike => $::form->{filter}{'all:substr:multi::ilike'} },
-        ]
-      ],
-      limit => 2,
-    ) }) {
+    if (1 == scalar @{ $exact_matches = $exact_models->get }) {
       $self->parts($exact_matches);
     }
   }
@@ -48,7 +631,8 @@ sub action_ajax_autocomplete {
      id          => $_->id,
      partnumber  => $_->partnumber,
      description => $_->description,
-     type        => $_->type,
+     ean         => $_->ean,
+     part_type   => $_->part_type,
      unit        => $_->unit,
      cvars       => { map { ($_->config->name => { value => $_->value_as_text, is_valid => $_->is_valid }) } @{ $_->cvars_by_config } },
     }
@@ -58,15 +642,21 @@ sub action_ajax_autocomplete {
 }
 
 sub action_test_page {
-  $_[0]->render('part/test_page');
+  $_[0]->render('part/test_page', pre_filled_part => SL::DB::Manager::Part->get_first);
 }
 
 sub action_part_picker_search {
-  $_[0]->render('part/part_picker_search', { layout => 0 }, parts => $_[0]->parts);
+  my ($self) = @_;
+
+  my $search_term = $self->models->filtered->laundered->{all_substr_multi__ilike};
+  $search_term  ||= $self->models->filtered->laundered->{all_with_makemodel_substr_multi__ilike};
+  $search_term  ||= $self->models->filtered->laundered->{all_with_customer_partnumber_substr_multi__ilike};
+
+  $_[0]->render('part/part_picker_search', { layout => 0 }, search_term => $search_term);
 }
 
 sub action_part_picker_result {
-  $_[0]->render('part/_part_picker_result', { layout => 0 });
+  $_[0]->render('part/_part_picker_result', { layout => 0 }, parts => $_[0]->parts);
 }
 
 sub action_show {
@@ -85,33 +675,1140 @@ sub action_show {
   }
 }
 
-sub init_parts {
-  if ($::form->{no_paginate}) {
-    $_[0]->models->disable_plugin('paginated');
-  }
+# helper functions
+sub validate_add_items {
+  scalar @{$::form->{add_items}};
+}
 
-  $_[0]->models->get;
+sub prepare_assortment_render_vars {
+  my ($self) = @_;
+
+  my %vars = ( items_sellprice_sum => $self->part->items_sellprice_sum,
+               items_lastcost_sum  => $self->part->items_lastcost_sum,
+               assortment_html     => $self->render_assortment_items_to_html( \@{$self->part->items} ),
+             );
+  $vars{items_sum_diff} = $vars{items_sellprice_sum} - $vars{items_lastcost_sum};
+
+  return \%vars;
 }
 
-sub init_part {
-  SL::DB::Part->new(id => $::form->{id} || $::form->{part}{id})->load;
+sub prepare_assembly_render_vars {
+  my ($self) = @_;
+
+  croak("Need assembly item(s) to create a 'save as new' assembly.") unless $self->part->items;
+
+  my %vars = ( items_sellprice_sum => $self->part->items_sellprice_sum,
+               items_lastcost_sum  => $self->part->items_lastcost_sum,
+               assembly_html       => $self->render_assembly_items_to_html( \@{ $self->part->items } ),
+             );
+  $vars{items_sum_diff} = $vars{items_sellprice_sum} - $vars{items_lastcost_sum};
+
+  return \%vars;
 }
 
-sub init_models {
+sub add {
   my ($self) = @_;
 
-  SL::Controller::Helper::GetModels->new(
-    controller => $self,
-    sorted => {
-      _default  => {
-        by => 'partnumber',
-        dir  => 1,
-      },
-      partnumber  => t8('Partnumber'),
-      description  => t8('Description'),
-    },
-    with_objects => [ qw(unit_obj) ],
+  check_has_valid_part_type($self->part->part_type);
+
+  $self->_set_javascript;
+  $self->_setup_form_action_bar;
+
+  my %title_hash = ( part       => t8('Add Part'),
+                     assembly   => t8('Add Assembly'),
+                     service    => t8('Add Service'),
+                     assortment => t8('Add Assortment'),
+                   );
+
+  $self->render(
+    'part/form',
+    title => $title_hash{$self->part->part_type},
   );
 }
 
-1;
+
+sub _set_javascript {
+  my ($self) = @_;
+  $::request->layout->use_javascript("${_}.js")  for qw(kivi.Part kivi.File kivi.PriceRule ckeditor/ckeditor ckeditor/adapters/jquery kivi.ShopPart kivi.Validator);
+  $::request->layout->add_javascripts_inline("\$(function(){kivi.PriceRule.load_price_rules_for_part(@{[ $self->part->id ]})});") if $self->part->id;
+}
+
+sub recalc_item_totals {
+  my ($self, %params) = @_;
+
+  if ( $params{part_type} eq 'assortment' ) {
+    return 0 unless scalar @{$self->assortment_items};
+  } elsif ( $params{part_type} eq 'assembly' ) {
+    return 0 unless scalar @{$self->assembly_items};
+  } else {
+    carp "can only calculate sum for assortments and assemblies";
+  };
+
+  my $part = SL::DB::Part->new(part_type => $params{part_type});
+  if ( $part->is_assortment ) {
+    $part->assortment_items( @{$self->assortment_items} );
+    if ( $params{price_type} eq 'lastcost' ) {
+      return $part->items_lastcost_sum;
+    } else {
+      if ( $params{pricegroup_id} ) {
+        return $part->items_sellprice_sum(pricegroup_id => $params{pricegroup_id});
+      } else {
+        return $part->items_sellprice_sum;
+      };
+    }
+  } elsif ( $part->is_assembly ) {
+    $part->assemblies( @{$self->assembly_items} );
+    if ( $params{price_type} eq 'weight' ) {
+      return $part->items_weight_sum;
+    } elsif ( $params{price_type} eq 'lastcost' ) {
+      return $part->items_lastcost_sum;
+    } else {
+      return $part->items_sellprice_sum;
+    }
+  }
+}
+
+sub check_part_not_modified {
+  my ($self) = @_;
+
+  return !($::form->{last_modification} && ($self->part->last_modification ne $::form->{last_modification}));
+
+}
+
+sub parse_form {
+  my ($self) = @_;
+
+  my $is_new = !$self->part->id;
+
+  my $params = delete($::form->{part}) || { };
+
+  delete $params->{id};
+  $self->part->assign_attributes(%{ $params});
+  $self->part->bin_id(undef) unless $self->part->warehouse_id;
+
+  $self->normalize_text_blocks;
+
+  # Only reset items ([]) and rewrite from form if $::form->{assortment_items} isn't empty. This
+  # will be the case for used assortments when saving, or when a used assortment
+  # is "used as new"
+  if ( $self->part->is_assortment and $::form->{assortment_items} and scalar @{$::form->{assortment_items}}) {
+    $self->part->assortment_items([]);
+    $self->part->add_assortment_items(@{$self->assortment_items}); # assortment_items has a get_set_init
+  };
+
+  if ( $self->part->is_assembly and $::form->{assembly_items} and @{$::form->{assembly_items}} ) {
+    $self->part->assemblies([]); # completely rewrite assortments each time
+    $self->part->add_assemblies( @{ $self->assembly_items } );
+  };
+
+  $self->part->translations([]);
+  $self->parse_form_translations;
+
+  $self->part->prices([]);
+  $self->parse_form_prices;
+
+  $self->parse_form_customerprices;
+  $self->parse_form_makemodels;
+}
+
+sub parse_form_prices {
+  my ($self) = @_;
+  # only save prices > 0
+  my $prices = delete($::form->{prices}) || [];
+  foreach my $price ( @{$prices} ) {
+    my $sellprice = $::form->parse_amount(\%::myconfig, $price->{price});
+    next unless $sellprice > 0; # skip negative prices as well
+    my $p = SL::DB::Price->new(parts_id      => $self->part->id,
+                               pricegroup_id => $price->{pricegroup_id},
+                               price         => $sellprice,
+                              );
+    $self->part->add_prices($p);
+  };
+}
+
+sub parse_form_translations {
+  my ($self) = @_;
+  # don't add empty translations
+  my $translations = delete($::form->{translations}) || [];
+  foreach my $translation ( @{$translations} ) {
+    next unless $translation->{translation};
+    my $t = SL::DB::Translation->new( %{$translation} ) or die "Can't create translation";
+    $self->part->add_translations( $translation );
+  };
+}
+
+sub parse_form_makemodels {
+  my ($self) = @_;
+
+  my $makemodels_map;
+  if ( $self->part->makemodels ) { # check for new parts or parts without makemodels
+    $makemodels_map = { map { $_->id => Rose::DB::Object::Helpers::clone($_) } @{$self->part->makemodels} };
+  };
+
+  $self->part->makemodels([]);
+
+  my $position = 0;
+  my $makemodels = delete($::form->{makemodels}) || [];
+  foreach my $makemodel ( @{$makemodels} ) {
+    next unless $makemodel->{make};
+    $position++;
+    my $vendor = SL::DB::Manager::Vendor->find_by(id => $makemodel->{make}) || die "Can't find vendor from make";
+
+    my $id = $makemodels_map->{$makemodel->{id}} ? $makemodels_map->{$makemodel->{id}}->id : undef;
+    my $mm = SL::DB::MakeModel->new( # parts_id   => $self->part->id, # will be assigned by row add_makemodels
+                                     id         => $id,
+                                     make       => $makemodel->{make},
+                                     model      => $makemodel->{model} || '',
+                                     lastcost   => $::form->parse_amount(\%::myconfig, $makemodel->{lastcost_as_number}),
+                                     sortorder  => $position,
+                                   );
+    if ($makemodels_map->{$mm->id} && !$makemodels_map->{$mm->id}->lastupdate && $makemodels_map->{$mm->id}->lastcost == 0 && $mm->lastcost == 0) {
+      # lastupdate isn't set, original lastcost is 0 and new lastcost is 0
+      # don't change lastupdate
+    } elsif ( !$makemodels_map->{$mm->id} && $mm->lastcost == 0 ) {
+      # new makemodel, no lastcost entered, leave lastupdate empty
+    } elsif ($makemodels_map->{$mm->id} && $makemodels_map->{$mm->id}->lastcost == $mm->lastcost) {
+      # lastcost hasn't changed, use original lastupdate
+      $mm->lastupdate($makemodels_map->{$mm->id}->lastupdate);
+    } else {
+      $mm->lastupdate(DateTime->now);
+    };
+    $self->part->makemodel( scalar @{$self->part->makemodels} ? 1 : 0 ); # do we need this boolean anymore?
+    $self->part->add_makemodels($mm);
+  };
+}
+
+sub parse_form_customerprices {
+  my ($self) = @_;
+
+  my $customerprices_map;
+  if ( $self->part->customerprices ) { # check for new parts or parts without customerprices
+    $customerprices_map = { map { $_->id => Rose::DB::Object::Helpers::clone($_) } @{$self->part->customerprices} };
+  };
+
+  $self->part->customerprices([]);
+
+  my $position = 0;
+  my $customerprices = delete($::form->{customerprices}) || [];
+  foreach my $customerprice ( @{$customerprices} ) {
+    next unless $customerprice->{customer_id};
+    $position++;
+    my $customer = SL::DB::Manager::Customer->find_by(id => $customerprice->{customer_id}) || die "Can't find customer from id";
+
+    my $id = $customerprices_map->{$customerprice->{id}} ? $customerprices_map->{$customerprice->{id}}->id : undef;
+    my $cu = SL::DB::PartCustomerPrice->new( # parts_id   => $self->part->id, # will be assigned by row add_customerprices
+                                     id                   => $id,
+                                     customer_id          => $customerprice->{customer_id},
+                                     customer_partnumber  => $customerprice->{customer_partnumber} || '',
+                                     price                => $::form->parse_amount(\%::myconfig, $customerprice->{price_as_number}),
+                                     sortorder            => $position,
+                                   );
+    if ($customerprices_map->{$cu->id} && !$customerprices_map->{$cu->id}->lastupdate && $customerprices_map->{$cu->id}->price == 0 && $cu->price == 0) {
+      # lastupdate isn't set, original price is 0 and new lastcost is 0
+      # don't change lastupdate
+    } elsif ( !$customerprices_map->{$cu->id} && $cu->price == 0 ) {
+      # new customerprice, no lastcost entered, leave lastupdate empty
+    } elsif ($customerprices_map->{$cu->id} && $customerprices_map->{$cu->id}->price == $cu->price) {
+      # price hasn't changed, use original lastupdate
+      $cu->lastupdate($customerprices_map->{$cu->id}->lastupdate);
+    } else {
+      $cu->lastupdate(DateTime->now);
+    };
+    $self->part->add_customerprices($cu);
+  };
+}
+
+sub build_bin_select {
+  select_tag('part.bin_id', [ @{ $_[0]->warehouse->bins_sorted } ],
+    title_key => 'description',
+    default   => $_[0]->bin->id,
+  );
+}
+
+
+# get_set_inits for partpicker
+
+sub init_parts {
+  if ($::form->{no_paginate}) {
+    $_[0]->models->disable_plugin('paginated');
+  }
+
+  $_[0]->models->get;
+}
+
+# get_set_inits for part controller
+sub init_part {
+  my ($self) = @_;
+
+  # used by edit, save, delete and add
+
+  if ( $::form->{part}{id} ) {
+    return SL::DB::Part->new(id => $::form->{part}{id})->load(with => [ qw(makemodels customerprices prices translations partsgroup shop_parts shop_parts.shop) ]);
+  } elsif ( $::form->{id} ) {
+    return SL::DB::Part->new(id => $::form->{id})->load; # used by inventory tab
+  } else {
+    die "part_type missing" unless $::form->{part}{part_type};
+    return SL::DB::Part->new(part_type => $::form->{part}{part_type});
+  };
+}
+
+sub init_orphaned {
+  my ($self) = @_;
+  return $self->part->orphaned;
+}
+
+sub init_models {
+  my ($self) = @_;
+
+  SL::Controller::Helper::GetModels->new(
+    controller => $self,
+    sorted => {
+      _default  => {
+        by => 'partnumber',
+        dir  => 1,
+      },
+      partnumber  => t8('Partnumber'),
+      description  => t8('Description'),
+    },
+    with_objects => [ qw(unit_obj classification) ],
+  );
+}
+
+sub init_p {
+  SL::Presenter->get;
+}
+
+
+sub init_assortment_items {
+  # this init is used while saving and whenever assortments change dynamically
+  my ($self) = @_;
+  my $position = 0;
+  my @array;
+  my $assortment_items = delete($::form->{assortment_items}) || [];
+  foreach my $assortment_item ( @{$assortment_items} ) {
+    next unless $assortment_item->{parts_id};
+    $position++;
+    my $part = SL::DB::Manager::Part->find_by(id => $assortment_item->{parts_id}) || die "Can't determine item to be added";
+    my $ai = SL::DB::AssortmentItem->new( parts_id      => $part->id,
+                                          qty           => $::form->parse_amount(\%::myconfig, $assortment_item->{qty_as_number}),
+                                          charge        => $assortment_item->{charge},
+                                          unit          => $assortment_item->{unit} || $part->unit,
+                                          position      => $position,
+    );
+
+    push(@array, $ai);
+  };
+  return \@array;
+}
+
+sub init_makemodels {
+  my ($self) = @_;
+
+  my $position = 0;
+  my @makemodel_array = ();
+  my $makemodels = delete($::form->{makemodels}) || [];
+
+  foreach my $makemodel ( @{$makemodels} ) {
+    next unless $makemodel->{make};
+    $position++;
+    my $mm = SL::DB::MakeModel->new( # parts_id   => $self->part->id, # will be assigned by row add_makemodels
+                                    id        => $makemodel->{id},
+                                    make      => $makemodel->{make},
+                                    model     => $makemodel->{model} || '',
+                                    lastcost  => $::form->parse_amount(\%::myconfig, $makemodel->{lastcost_as_number} || 0),
+                                    sortorder => $position,
+                                  ) or die "Can't create mm";
+    # $mm->id($makemodel->{id}) if $makemodel->{id};
+    push(@makemodel_array, $mm);
+  };
+  return \@makemodel_array;
+}
+
+sub init_customerprices {
+  my ($self) = @_;
+
+  my $position = 0;
+  my @customerprice_array = ();
+  my $customerprices = delete($::form->{customerprices}) || [];
+
+  foreach my $customerprice ( @{$customerprices} ) {
+    next unless $customerprice->{customer_id};
+    $position++;
+    my $cu = SL::DB::PartCustomerPrice->new( # parts_id   => $self->part->id, # will be assigned by row add_customerprices
+                                    id                  => $customerprice->{id},
+                                    customer_partnumber => $customerprice->{customer_partnumber},
+                                    customer_id         => $customerprice->{customer_id} || '',
+                                    price               => $::form->parse_amount(\%::myconfig, $customerprice->{price_as_number} || 0),
+                                    sortorder           => $position,
+                                  ) or die "Can't create cu";
+    # $cu->id($customerprice->{id}) if $customerprice->{id};
+    push(@customerprice_array, $cu);
+  };
+  return \@customerprice_array;
+}
+
+sub init_assembly_items {
+  my ($self) = @_;
+  my $position = 0;
+  my @array;
+  my $assembly_items = delete($::form->{assembly_items}) || [];
+  foreach my $assembly_item ( @{$assembly_items} ) {
+    next unless $assembly_item->{parts_id};
+    $position++;
+    my $part = SL::DB::Manager::Part->find_by(id => $assembly_item->{parts_id}) || die "Can't determine item to be added";
+    my $ai = SL::DB::Assembly->new(parts_id    => $part->id,
+                                   bom         => $assembly_item->{bom},
+                                   qty         => $::form->parse_amount(\%::myconfig, $assembly_item->{qty_as_number}),
+                                   position    => $position,
+                                  );
+    push(@array, $ai);
+  };
+  return \@array;
+}
+
+sub init_all_warehouses {
+  my ($self) = @_;
+  SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef, id => $self->part->warehouse_id ] ]);
+}
+
+sub init_all_languages {
+  SL::DB::Manager::Language->get_all_sorted;
+}
+
+sub init_all_partsgroups {
+  my ($self) = @_;
+  SL::DB::Manager::PartsGroup->get_all_sorted(query => [ or => [ id => $self->part->partsgroup_id, obsolete => 0 ] ]);
+}
+
+sub init_all_buchungsgruppen {
+  my ($self) = @_;
+  if ( $self->part->orphaned ) {
+    return SL::DB::Manager::Buchungsgruppe->get_all_sorted;
+  } else {
+    return SL::DB::Manager::Buchungsgruppe->get_all_sorted(where => [ id => $self->part->buchungsgruppen_id ]);
+  }
+}
+
+sub init_shops_not_assigned {
+  my ($self) = @_;
+
+  my @used_shop_ids = map { $_->shop->id } @{ $self->part->shop_parts };
+  if ( @used_shop_ids ) {
+    return SL::DB::Manager::Shop->get_all( query => [ obsolete => 0, '!id' => \@used_shop_ids ], sort_by => 'sortkey' );
+  }
+  else {
+    return SL::DB::Manager::Shop->get_all( query => [ obsolete => 0 ], sort_by => 'sortkey' );
+  }
+}
+
+sub init_all_units {
+  my ($self) = @_;
+  if ( $self->part->orphaned ) {
+    return SL::DB::Manager::Unit->get_all_sorted;
+  } else {
+    return SL::DB::Manager::Unit->get_all(where => [ unit => $self->part->unit ]);
+  }
+}
+
+sub init_all_payment_terms {
+  my ($self) = @_;
+  SL::DB::Manager::PaymentTerm->get_all_sorted(query => [ or => [ id => $self->part->payment_id, obsolete => 0 ] ]);
+}
+
+sub init_all_price_factors {
+  SL::DB::Manager::PriceFactor->get_all_sorted;
+}
+
+sub init_all_pricegroups {
+  SL::DB::Manager::Pricegroup->get_all_sorted(query => [ obsolete => 0 ]);
+}
+
+# model used to filter/display the parts in the multi-items dialog
+sub init_multi_items_models {
+  SL::Controller::Helper::GetModels->new(
+    controller     => $_[0],
+    model          => 'Part',
+    with_objects   => [ qw(unit_obj partsgroup classification) ],
+    disable_plugin => 'paginated',
+    source         => $::form->{multi_items},
+    sorted         => {
+      _default    => {
+        by  => 'partnumber',
+        dir => 1,
+      },
+      partnumber  => t8('Partnumber'),
+      description => t8('Description')}
+  );
+}
+
+sub init_parts_classification_filter {
+  return [] unless $::form->{parts_classification_type};
+
+  return [ used_for_sale     => 't' ] if $::form->{parts_classification_type} eq 'sales';
+  return [ used_for_purchase => 't' ] if $::form->{parts_classification_type} eq 'purchases';
+
+  die "no query rules for parts_classification_type " . $::form->{parts_classification_type};
+}
+
+# simple checks to run on $::form before saving
+
+sub form_check_part_description_exists {
+  my ($self) = @_;
+
+  return 1 if $::form->{part}{description};
+
+  $self->js->flash('error', t8('Part Description missing!'))
+           ->run('kivi.Part.set_tab_active_by_name', 'basic_data')
+           ->focus('#part_description');
+  return 0;
+}
+
+sub form_check_assortment_items_exist {
+  my ($self) = @_;
+
+  return 1 unless $::form->{part}{part_type} eq 'assortment';
+  # skip item check for existing assortments that have been used
+  return 1 if ($self->part->id and !$self->part->orphaned);
+
+  # new or orphaned parts must have items in $::form->{assortment_items}
+  unless ( $::form->{assortment_items} and scalar @{$::form->{assortment_items}} ) {
+    $self->js->run('kivi.Part.set_tab_active_by_name', 'assortment_tab')
+             ->focus('#add_assortment_item_name')
+             ->flash('error', t8('The assortment doesn\'t have any items.'));
+    return 0;
+  };
+  return 1;
+}
+
+sub form_check_assortment_items_unique {
+  my ($self) = @_;
+
+  return 1 unless $::form->{part}{part_type} eq 'assortment';
+
+  my %duplicate_elements;
+  my %count;
+  for (map { $_->{parts_id} } @{$::form->{assortment_items}}) {
+    $duplicate_elements{$_}++ if $count{$_}++;
+  };
+
+  if ( keys %duplicate_elements ) {
+    $self->js->run('kivi.Part.set_tab_active_by_name', 'assortment_tab')
+             ->flash('error', t8('There are duplicate assortment items'));
+    return 0;
+  };
+  return 1;
+}
+
+sub form_check_assembly_items_exist {
+  my ($self) = @_;
+
+  return 1 unless $::form->{part}->{part_type} eq 'assembly';
+
+  # skip item check for existing assembly that have been used
+  return 1 if ($self->part->id and !$self->part->orphaned);
+
+  unless ( $::form->{assembly_items} and scalar @{$::form->{assembly_items}} ) {
+    $self->js->run('kivi.Part.set_tab_active_by_name', 'assembly_tab')
+             ->focus('#add_assembly_item_name')
+             ->flash('error', t8('The assembly doesn\'t have any items.'));
+    return 0;
+  };
+  return 1;
+}
+
+sub form_check_partnumber_is_unique {
+  my ($self) = @_;
+
+  if ( !$::form->{part}{id} and $::form->{part}{partnumber} ) {
+    my $count = SL::DB::Manager::Part->get_all_count(where => [ partnumber => $::form->{part}{partnumber} ]);
+    if ( $count ) {
+      $self->js->flash('error', t8('The partnumber already exists!'))
+               ->focus('#part_description');
+      return 0;
+    };
+  };
+  return 1;
+}
+
+# general checking functions
+
+sub check_part_id {
+  die t8("Can't load item without a valid part.id") . "\n" unless $::form->{part}{id};
+}
+
+sub check_form {
+  my ($self) = @_;
+
+  $self->form_check_part_description_exists || return 0;
+  $self->form_check_assortment_items_exist  || return 0;
+  $self->form_check_assortment_items_unique || return 0;
+  $self->form_check_assembly_items_exist    || return 0;
+  $self->form_check_partnumber_is_unique    || return 0;
+
+  return 1;
+}
+
+sub check_has_valid_part_type {
+  die "invalid part_type" unless $_[0] =~ /^(part|service|assembly|assortment)$/;
+}
+
+
+sub normalize_text_blocks {
+  my ($self) = @_;
+
+  # check if feature is enabled (select normalize_part_descriptions from defaults)
+  return unless ($::instance_conf->get_normalize_part_descriptions);
+
+  # text block
+  foreach (qw(description)) {
+    $self->part->{$_} =~ s/\s+$//s;
+    $self->part->{$_} =~ s/^\s+//s;
+    $self->part->{$_} =~ s/ {2,}/ /g;
+  }
+  # html block (caveat: can be circumvented by using bold or italics)
+  $self->part->{notes} =~ s/^<p>(&nbsp;)+\s+/<p>/s;
+  $self->part->{notes} =~ s/(&nbsp;)+<\/p>$/<\/p>/s;
+
+}
+
+sub render_assortment_items_to_html {
+  my ($self, $assortment_items, $number_of_items) = @_;
+
+  my $position = $number_of_items + 1;
+  my $html;
+  foreach my $ai (@$assortment_items) {
+    $html .= $self->p->render('part/_assortment_row',
+                              PART     => $self->part,
+                              orphaned => $self->orphaned,
+                              ITEM     => $ai,
+                              listrow  => $position % 2 ? 1 : 0,
+                              position => $position, # for legacy assemblies
+                             );
+    $position++;
+  };
+  return $html;
+}
+
+sub render_assembly_items_to_html {
+  my ($self, $assembly_items, $number_of_items) = @_;
+
+  my $position = $number_of_items + 1;
+  my $html;
+  foreach my $ai (@{$assembly_items}) {
+    $html .= $self->p->render('part/_assembly_row',
+                              PART     => $self->part,
+                              orphaned => $self->orphaned,
+                              ITEM     => $ai,
+                              listrow  => $position % 2 ? 1 : 0,
+                              position => $position, # for legacy assemblies
+                             );
+    $position++;
+  };
+  return $html;
+}
+
+sub parse_add_items_to_objects {
+  my ($self, %params) = @_;
+  my $part_type = $params{part_type};
+  die unless $params{part_type} =~ /^(assortment|assembly)$/;
+  my $position = $params{position} || 1;
+
+  my @add_items = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
+
+  my @item_objects;
+  foreach my $item ( @add_items ) {
+    my $part = SL::DB::Manager::Part->find_by(id => $item->{parts_id}) || die "Can't load part";
+    my $ai;
+    if ( $part_type eq 'assortment' ) {
+       $ai = SL::DB::AssortmentItem->new(part          => $part,
+                                         qty           => $::form->parse_amount(\%::myconfig, $item->{qty_as_number}),
+                                         unit          => $part->unit, # TODO: $item->{unit} || $part->unit
+                                         position      => $position,
+                                        ) or die "Can't create AssortmentItem from item";
+    } elsif ( $part_type eq 'assembly' ) {
+      $ai = SL::DB::Assembly->new(parts_id    => $part->id,
+                                 # id          => $self->assembly->id, # will be set on save
+                                 qty         => $::form->parse_amount(\%::myconfig, $item->{qty_as_number}),
+                                 bom         => 0, # default when adding: no bom
+                                 position    => $position,
+                                );
+    } else {
+      die "part_type must be assortment or assembly";
+    }
+    push(@item_objects, $ai);
+    $position++;
+  };
+
+  return \@item_objects;
+}
+
+sub _setup_form_action_bar {
+  my ($self) = @_;
+
+  my $may_edit           = $::auth->assert('part_service_assembly_edit', 'may fail');
+  my $used_in_pricerules = !!SL::DB::Manager::PriceRuleItem->get_all_count(where => [type => 'part', value_int => $self->part->id]);
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [
+          t8('Save'),
+          call      => [ 'kivi.Part.save' ],
+          disabled  => !$may_edit ? t8('You do not have the permissions to access this function.') : undef,
+          checks    => ['kivi.validate_form'],
+        ],
+        action => [
+          t8('Use as new'),
+          call     => [ 'kivi.Part.use_as_new' ],
+          disabled => !$self->part->id ? t8('The object has not been saved yet.')
+                    : !$may_edit       ? t8('You do not have the permissions to access this function.')
+                    :                    undef,
+        ],
+      ], # end of combobox "Save"
+
+      combobox => [
+        action => [ t8('Workflow') ],
+        action => [
+          t8('Save and Purchase Order'),
+          submit   => [ '#ic', { action => "Part/save_and_purchase_order" } ],
+          checks   => ['kivi.validate_form'],
+          disabled => !$self->part->id                                    ? t8('The object has not been saved yet.')
+                    : !$may_edit                                          ? t8('You do not have the permissions to access this function.')
+                    : !$::auth->assert('purchase_order_edit', 'may fail') ? t8('You do not have the permissions to access this function.')
+                    :                                                       undef,
+          only_if  => !$::form->{inline_create},
+        ],
+      ],
+
+      action => [
+        t8('Abort'),
+        submit   => [ '#ic', { action => "Part/abort" } ],
+        only_if  => !!$::form->{inline_create},
+      ],
+
+      action => [
+        t8('Delete'),
+        call     => [ 'kivi.Part.delete' ],
+        confirm  => t8('Do you really want to delete this object?'),
+        disabled => !$self->part->id       ? t8('This object has not been saved yet.')
+                  : !$may_edit             ? t8('You do not have the permissions to access this function.')
+                  : !$self->part->orphaned ? t8('This object has already been used.')
+                  : $used_in_pricerules    ? t8('This object is used in price rules.')
+                  :                          undef,
+      ],
+
+      'separator',
+
+      action => [
+        t8('History'),
+        call     => [ 'kivi.Part.open_history_popup' ],
+        disabled => !$self->part->id ? t8('This object has not been saved yet.')
+                  : !$may_edit       ? t8('You do not have the permissions to access this function.')
+                  :                    undef,
+      ],
+    );
+  }
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::Part - Part CRUD controller
+
+=head1 DESCRIPTION
+
+Controller for adding/editing/saving/deleting parts.
+
+All the relations are loaded at once and saving the part, adding a history
+entry and saving CVars happens inside one transaction.  When saving the old
+relations are deleted and written as new to the database.
+
+Relations for parts:
+
+=over 2
+
+=item makemodels
+
+=item translations
+
+=item assembly items
+
+=item assortment items
+
+=item prices
+
+=back
+
+=head1 PART_TYPES
+
+There are 4 different part types:
+
+=over 4
+
+=item C<part>
+
+The "default" part type.
+
+inventory_accno_id is set.
+
+=item C<service>
+
+Services can't be stocked.
+
+inventory_accno_id isn't set.
+
+=item C<assembly>
+
+Assemblies consist of other parts, services, assemblies or assortments. They
+aren't meant to be bought, only sold. To add assemblies to stock you typically
+have to make them, which reduces the stock by its respective components. Once
+an assembly item has been created there is currently no way to "disassemble" it
+again. An assembly item can appear several times in one assembly. An assmbly is
+sold as one item with a defined sellprice and lastcost. If the component prices
+change the assortment price remains the same. The assembly items may be printed
+in a record if the item's "bom" is set.
+
+=item C<assortment>
+
+Similar to assembly, but each assortment item may only appear once per
+assortment. When selling an assortment the assortment items are added to the
+record together with the assortment, which is added with sellprice 0.
+
+Technically an assortment doesn't have a sellprice, but rather the sellprice is
+determined by the sum of the current assortment item prices when the assortment
+is added to a record. This also means that price rules and customer discounts
+will be applied to the assortment items.
+
+Once the assortment items have been added they may be modified or deleted, just
+as if they had been added manually, the individual assortment items aren't
+linked to the assortment or the other assortment items in any way.
+
+=back
+
+=head1 URL ACTIONS
+
+=over 4
+
+=item C<action_add_part>
+
+=item C<action_add_service>
+
+=item C<action_add_assembly>
+
+=item C<action_add_assortment>
+
+=item C<action_add PART_TYPE>
+
+An alternative to the action_add_$PART_TYPE actions, takes the mandatory
+parameter part_type as an action. Example:
+
+  controller.pl?action=Part/add&part_type=service
+
+=item C<action_add_from_record>
+
+When adding new items to records they can be created on the fly if the entered
+partnumber or description doesn't exist yet. After being asked what part type
+the new item should have the user is redirected to the correct edit page.
+
+Depending on whether the item was added from a sales or a purchase record, only
+the relevant part classifications should be selectable for new item, so this
+parameter is passed on via a hidden parts_classification_type in the new_item
+template.
+
+=item C<action_save>
+
+Saves the current part and then reloads the edit page for the part.
+
+=item C<action_use_as_new>
+
+Takes the information from the current part, plus any modifications made on the
+page, and creates a new edit page that is ready to be saved. The partnumber is
+set empty, so a new partnumber from the number range will be used if the user
+doesn't enter one manually.
+
+Unsaved changes to the original part aren't updated.
+
+The part type cannot be changed in this way.
+
+=item C<action_delete>
+
+Deletes the current part and then redirects to the main page, there is no
+callback.
+
+The delete button only appears if the part is 'orphaned', according to
+SL::DB::Part orphaned.
+
+The part can't be deleted if it appears in invoices, orders, delivery orders,
+the inventory, or is part of an assembly or assortment.
+
+If the part is deleted its relations prices, makdemodel, assembly,
+assortment_items and translation are are also deleted via DELETE ON CASCADE.
+
+Before this controller items that appeared in inventory didn't count as
+orphaned and could be deleted and the inventory entries were also deleted, this
+"feature" hasn't been implemented.
+
+=item C<action_edit part.id>
+
+Load and display a part for editing.
+
+  controller.pl?action=Part/edit&part.id=12345
+
+Passing the part id is mandatory, and the parameter is "part.id", not "id".
+
+=back
+
+=head1 BUTTON ACTIONS
+
+=over 4
+
+=item C<history>
+
+Opens a popup displaying all the history entries. Once a new history controller
+is written the button could link there instead, with the part already selected.
+
+=back
+
+=head1 AJAX ACTIONS
+
+=over 4
+
+=item C<action_update_item_totals>
+
+Is called whenever an element with the .recalc class loses focus, e.g. the qty
+amount of an item changes. The sum of all sellprices and lastcosts is
+calculated and the totals updated. Uses C<recalc_item_totals>.
+
+=item C<action_add_assortment_item>
+
+Adds a new assortment item from a part picker seleciton to the assortment item list
+
+If the item already exists in the assortment the item isn't added and a Flash
+error shown.
+
+Rather than running kivi.Part.renumber_positions and kivi.Part.assembly_recalc
+after adding each new item, add the new object to the item objects that were
+already parsed, calculate totals via a dummy part then update the row and the
+totals.
+
+=item C<action_add_assembly_item>
+
+Adds a new assembly item from a part picker seleciton to the assembly item list
+
+If the item already exists in the assembly a flash info is generated, but the
+item is added.
+
+Rather than running kivi.Part.renumber_positions and kivi.Part.assembly_recalc
+after adding each new item, add the new object to the item objects that were
+already parsed, calculate totals via a dummy part then update the row and the
+totals.
+
+=item C<action_add_multi_assortment_items>
+
+Parses the items to be added from the form generated by the multi input and
+appends the html of the tr-rows to the assortment item table. Afterwards all
+assortment items are renumbered and the sums recalculated via
+kivi.Part.renumber_positions and kivi.Part.assortment_recalc.
+
+=item C<action_add_multi_assembly_items>
+
+Parses the items to be added from the form generated by the multi input and
+appends the html of the tr-rows to the assembly item table. Afterwards all
+assembly items are renumbered and the sums recalculated via
+kivi.Part.renumber_positions and kivi.Part.assembly_recalc.
+
+=item C<action_show_multi_items_dialog>
+
+=item C<action_multi_items_update_result>
+
+=item C<action_add_makemodel_row>
+
+Add a new makemodel row with the vendor that was selected via the vendor
+picker.
+
+Checks the already existing makemodels and warns if a row with that vendor
+already exists. Currently it is possible to have duplicate vendor rows.
+
+=item C<action_reorder_items>
+
+Sorts the item table for assembly or assortment items.
+
+=item C<action_warehouse_changed>
+
+=back
+
+=head1 ACTIONS part picker
+
+=over 4
+
+=item C<action_ajax_autocomplete>
+
+=item C<action_test_page>
+
+=item C<action_part_picker_search>
+
+=item C<action_part_picker_result>
+
+=item C<action_show>
+
+=back
+
+=head1 FORM CHECKS
+
+=over 2
+
+=item C<check_form>
+
+Calls some simple checks that test the submitted $::form for obvious errors.
+Return 1 if all the tests were successfull, 0 as soon as one test fails.
+
+Errors from the failed tests are stored as ClientJS actions in $self->js. In
+some cases extra actions are taken, e.g. if the part description is missing the
+basic data tab is selected and the description input field is focussed.
+
+=back
+
+=over 4
+
+=item C<form_check_part_description_exists>
+
+=item C<form_check_assortment_items_exist>
+
+=item C<form_check_assortment_items_unique>
+
+=item C<form_check_assembly_items_exist>
+
+=item C<form_check_partnumber_is_unique>
+
+=back
+
+=head1 HELPER FUNCTIONS
+
+=over 4
+
+=item C<parse_form>
+
+When submitting the form for saving, parses the transmitted form. Expects the
+following data:
+
+ $::form->{part}
+ $::form->{makemodels}
+ $::form->{translations}
+ $::form->{prices}
+ $::form->{assemblies}
+ $::form->{assortments}
+
+CVar data is currently stored directly in $::form, e.g. $::form->{cvar_size}.
+
+=item C<recalc_item_totals %params>
+
+Helper function for calculating the total lastcost and sellprice for assemblies
+or assortments according to their items, which are parsed from the current
+$::form.
+
+Is called whenever the qty of an item is changed or items are deleted.
+
+Takes two params:
+
+* part_type : 'assortment' or 'assembly' (mandatory)
+
+* price_type: 'lastcost' or 'sellprice', default is 'sellprice'
+
+Depending on the price_type the lastcost sum or sellprice sum is returned.
+
+Doesn't work for recursive items.
+
+=back
+
+=head1 GET SET INITS
+
+There are get_set_inits for
+
+* assembly items
+
+* assortment items
+
+* makemodels
+
+which parse $::form and automatically create an array of objects.
+
+These inits are used during saving and each time a new element is added.
+
+=over 4
+
+=item C<init_makemodels>
+
+Parses $::form->{makemodels}, creates an array of makemodel objects and stores them in
+$self->part->makemodels, ready to be saved.
+
+Used for saving parts and adding new makemodel rows.
+
+=item C<parse_add_items_to_objects PART_TYPE>
+
+Parses the resulting form from either the part-picker submit or the multi-item
+submit, and creates an arrayref of assortment_item or assembly objects, that
+can be rendered via C<render_assortment_items_to_html> or
+C<render_assembly_items_to_html>.
+
+Mandatory param: part_type: assortment or assembly (the resulting html will differ)
+Optional param: position (used for numbering and listrow class)
+
+=item C<render_assortment_items_to_html ITEM_OBJECTS>
+
+Takes an array_ref of assortment_items, and generates tables rows ready for
+adding to the assortment table.  Is used when a part is loaded, or whenever new
+assortment items are added.
+
+=item C<parse_form_makemodels>
+
+Makemodels can't just be overwritten, because of the field "lastupdate", that
+remembers when the lastcost for that vendor changed the last time.
+
+So the original values are cloned and remembered, so we can compare if lastcost
+was changed in $::form, and keep or update lastupdate.
+
+lastcost isn't updated until the first time it was saved with a value, until
+then it is empty.
+
+Also a boolean "makemodel" needs to be written in parts, depending on whether
+makemodel entries exist or not.
+
+We still need init_makemodels for when we open the part for editing.
+
+=back
+
+=head1 TODO
+
+=over 4
+
+=item *
+
+It should be possible to jump to the edit page in a specific tab
+
+=item *
+
+Support callbacks, e.g. creating a new part from within an order, and jumping
+back to the order again afterwards.
+
+=item *
+
+Support units when adding assembly items or assortment items. Currently the
+default unit of the item is always used.
+
+=item *
+
+Calculate sellprice and lastcost totals recursively, in case e.g. an assembly
+consists of other assemblies.
+
+=back
+
+=head1 AUTHOR
+
+G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Controller/PartsPriceHistory.pm b/SL/Controller/PartsPriceHistory.pm
new file mode 100644 (file)
index 0000000..82263b8
--- /dev/null
@@ -0,0 +1,144 @@
+package SL::Controller::PartsPriceHistory;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+use Clone qw(clone);
+use SL::DB::PartsPriceHistory;
+use SL::Controller::Helper::ParseFilter;
+use SL::Controller::Helper::ReportGenerator;
+
+sub action_list {
+  my ($self) = @_;
+  $self->{action} = 'list';
+
+  my %list_params = (
+    sort_by  => $::form->{sort_by} || 'valid_from',
+    sort_dir => $::form->{sort_dir},
+    filter   => $::form->{filter},
+    page     => $::form->{page},
+  );
+
+  my $db_args    = $self->setup_for_list(%list_params);
+  $self->{pages} = SL::DB::Manager::PartsPriceHistory->paginate(%list_params, args => $db_args, per_page => 5);
+
+  my $bottom     = $::form->parse_html_template('parts_price_history/report_bottom', { SELF => $self });
+
+  $self->prepare_report(
+    db_args                  => $db_args,
+    report_generator_options => {
+      raw_bottom_info_text => $bottom,
+      controller_class     => 'PartsPriceHistory',
+    },
+  );
+
+  my $history = SL::DB::Manager::PartsPriceHistory->get_all(%{ $db_args });
+
+  $self->report_generator_list_objects(
+    report  => $self->{report},
+    objects => $history,
+    layout  => 0,
+    header  => 0,
+  );
+}
+
+# private functions
+
+sub setup_for_list {
+  my ($self, %params) = @_;
+
+  $self->{filter} = clone($params{filter});
+
+  my %args = (
+    parse_filter($self->{filter}),
+    sort_by => $self->set_sort_params(%params),
+    page    => $params{page},
+  );
+
+  return \%args;
+}
+
+sub set_sort_params {
+  my ($self, %params) = @_;
+
+  my $sort_str;
+  ($self->{sort_by}, $self->{sort_dir}, $sort_str) = SL::DB::Manager::PartsPriceHistory->make_sort_string(%params);
+  return $sort_str;
+}
+
+sub column_defs {
+  my ($self) = @_;
+
+  return {
+    valid_from => { text => $::locale->text('Date'),       sub => sub { $_[0]->valid_from_as_timestamp }},
+    lastcost   => { text => $::locale->text('Lastcost'),   sub => sub { $_[0]->lastcost_as_number }},
+    listprice  => { text => $::locale->text('List Price'), sub => sub { $_[0]->listprice_as_number }},
+    sellprice  => { text => $::locale->text('Sell Price'), sub => sub { $_[0]->sellprice_as_number }},
+  };
+}
+
+sub prepare_report {
+  my ($self, %params) = @_;
+
+  my $objects     = $params{objects} || [];
+  my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
+  $self->{report} = $report;
+
+  my $title       = $::locale->text('Price history for master data');
+
+  my @columns     = qw(valid_from lastcost listprice sellprice);
+
+  my $column_defs = $self->column_defs;
+
+  for my $col (@columns) {
+    $column_defs->{$col}{link} = $self->self_url(
+      sort_by  => $col,
+      sort_dir => ($self->{sort_by} eq $col ? 1 - $self->{sort_dir} : $self->{sort_dir}),
+      page     => $self->{pages}{cur},
+    );
+  }
+
+  $column_defs->{$_}{visible} = 1 for @columns;
+
+  $report->set_columns(%$column_defs);
+  $report->set_column_order(@columns);
+  $report->set_options(allow_pdf_export => 0, allow_csv_export => 0);
+  $report->set_sort_indicator($self->{sort_by}, $self->{sort_dir});
+  $report->set_export_options(@{ $params{report_generator_export_options} || [] });
+  $report->set_options(
+    %{ $params{report_generator_options} || {} },
+    output_format => 'HTML',
+    top_info_text => $self->displayable_filter($::form->{filter}),
+    title         => $title,
+  );
+  $report->set_options_from_form;
+}
+
+sub displayable_filter {
+  my ($self, $filter) = @_;
+
+  my $column_defs = $self->column_defs;
+  my @texts;
+
+  push @texts, [ $::locale->text('Sort By'), $column_defs->{$self->{sort_by}}{text}  ] if $column_defs->{$self->{sort_by}}{text};
+  push @texts, [ $::locale->text('Page'),    $::locale->text($self->{pages}{cur})    ] if $self->{pages}{cur} > 1;
+
+  return join '; ', map { "$_->[0]: $_->[1]" } @texts;
+}
+
+sub self_url {
+  my ($self, %params) = @_;
+
+  %params = (
+    action   => $self->{action},
+    sort_by  => $self->{sort_by},
+    sort_dir => $self->{sort_dir},
+    page     => $self->{pages}{cur},
+    filter   => $::form->{filter},
+    %params,
+  );
+
+  return $self->url_for(%params);
+}
+
+1;
diff --git a/SL/Controller/PartsPriceUpdate.pm b/SL/Controller/PartsPriceUpdate.pm
new file mode 100644 (file)
index 0000000..8481998
--- /dev/null
@@ -0,0 +1,316 @@
+package SL::Controller::PartsPriceUpdate;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+use SL::DBUtils qw(prepare_query selectfirst_array_query prepare_query do_statement do_query);
+use SL::JSON;
+use SL::Helper::Flash qw(flash);
+use SL::DB;
+use SL::DB::Part;
+use SL::DB::Pricegroup;
+use SL::Locale::String qw(t8);
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(pricegroups pricegroups_by_id filter) ],
+);
+
+__PACKAGE__->run_before('check_rights');
+
+
+sub action_search_update_prices {
+  my ($self) = @_;
+
+  $self->setup_search_update_prices_action_bar;
+  $self->render('ic/search_update_prices',
+    title => t8('Update Prices'),
+  );
+}
+
+sub action_confirm_price_update {
+  my ($self) = @_;
+
+  my @errors;
+  my $found;
+
+  for my $key (keys %{ $self->filter->{prices} || {} }) {
+    my $row = $self->filter->{prices}{$key};
+
+    next if $row->{price_as_number} eq '';
+
+    my $type   = $row->{type};
+    my $value  = $::form->parse_amount(\%::myconfig, $row->{price_as_number});
+    my $name   = $key =~ /^\d+$/      ? $self->pricegroups_by_id->{$key}->pricegroup
+               : $key eq 'sellprice'  ? t8('Sell Price')
+               : $key eq 'listprice'  ? t8('List Price')
+               :                        '';
+
+    if (0 > $value && ($type eq 'percent')) {
+      push @errors, t8('You cannot adjust the price for pricegroup "#1" by a negative percentage.', $name);
+    } elsif (!$value) {
+      push @errors, t8('No valid number entered for pricegroup "#1".', $name);
+    } elsif (0 < $value) {
+      $found = 1;
+    }
+  }
+
+  push @errors, t8('No prices will be updated because no prices have been entered.') if !$found;
+
+  my $num_matches = $self->get_num_matches_for_priceupdate();
+
+  if (@errors) {
+    flash('error', $_) for @errors;
+    return $self->action_search_update_prices;
+  } else {
+
+    my $key = $::auth->create_unique_session_value(SL::JSON::to_json($self->filter));
+
+    $self->setup_confirm_price_update_action_bar;
+    $self->render('ic/confirm_price_update',
+      num_matches => $num_matches,
+      filter_key  => $key,
+    );
+  }
+}
+
+sub action_update_prices {
+  my ($self) = @_;
+
+  my $num_updated = $self->do_update_prices;
+
+  if ($num_updated) {
+    $::form->redirect(t8('#1 prices were updated.', $num_updated));
+  } else {
+    $::form->error(t8('Could not update prices!'));
+  }
+}
+
+sub _create_filter_for_priceupdate {
+  my ($self) = @_;
+  my $filter = $self->filter;
+
+  my @where_values;
+  my $where = '1 = 1';
+
+  for my $item (qw(partnumber drawing microfiche make model pg.partsgroup description serialnumber)) {
+    my $column = $item;
+    $column =~ s/.*\.//;
+    next unless $filter->{$column};
+
+    $where .= qq| AND $item ILIKE ?|;
+    push @where_values, "%$filter->{$column}%";
+  }
+
+  # items which were never bought, sold or on an order
+  if ($filter->{itemstatus} eq 'orphaned') {
+    $where .=
+      qq| AND (p.onhand = 0)
+          AND p.id NOT IN
+            (
+              SELECT DISTINCT parts_id FROM invoice
+              UNION
+              SELECT DISTINCT parts_id FROM assembly
+              UNION
+              SELECT DISTINCT parts_id FROM orderitems
+              UNION
+              SELECT DISTINCT parts_id FROM delivery_order_items
+            )|;
+
+  } elsif ($filter->{itemstatus} eq 'active') {
+    $where .= qq| AND p.obsolete = '0'|;
+
+  } elsif ($filter->{itemstatus} eq 'obsolete') {
+    $where .= qq| AND p.obsolete = '1'|;
+
+  } elsif ($filter->{itemstatus} eq 'onhand') {
+    $where .= qq| AND p.onhand > 0|;
+
+  } elsif ($filter->{itemstatus} eq 'short') {
+    $where .= qq| AND p.onhand < p.rop|;
+
+  }
+
+  for my $column (qw(make model)) {
+    next unless ($filter->{$column});
+    $where .= qq| AND p.id IN (SELECT DISTINCT parts_id FROM makemodel WHERE $column ILIKE ?|;
+    push @where_values, "%$filter->{$column}%";
+  }
+
+  return ($where, @where_values);
+}
+
+sub get_num_matches_for_priceupdate {
+  my ($self)   = @_;
+  my $filter   = $self->filter;
+  my $dbh      = SL::DB->client->dbh;
+  my ($where, @where_values) = $self->_create_filter_for_priceupdate;
+
+  my $num_updated = 0;
+  my $query;
+
+  for my $column (qw(sellprice listprice)) {
+    next if $filter->{prices}{$column}{price_as_number} eq "";
+
+    $query =
+      qq|SELECT COUNT(*)
+         FROM parts
+         WHERE id IN
+           (SELECT p.id
+            FROM parts p
+            LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
+            WHERE $where)|;
+    my ($result)  = selectfirst_array_query($::form, $dbh, $query, @where_values);
+    $num_updated += $result if (0 <= $result);
+  }
+
+  my @ids = grep { $filter->{prices}{$_}{price_as_number} } map { $_->id } @{ $self->pricegroups };
+  if (@ids) {
+    $query =
+      qq|SELECT COUNT(*)
+         FROM prices
+         WHERE parts_id IN
+           (SELECT p.id
+            FROM parts p
+            LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
+            WHERE $where)
+         AND pricegroup_id IN (@{[ join ',', ('?')x@ids ]})|;
+
+    my ($result)  = selectfirst_array_query($::form, $dbh, $query, @where_values, @ids);
+    $num_updated += $result if (0 <= $result);
+  }
+
+  return $num_updated;
+}
+
+sub do_update_prices {
+  SL::DB->client->with_transaction(\&_update_prices, $_[0]);
+}
+
+sub _update_prices {
+  my ($self) = @_;
+  my $filter_json = $::auth->get_session_value($::form->{filter_key});
+  my $filter = SL::JSON::from_json($filter_json);
+  $self->filter($filter);
+  die "missing filter" unless $filter;
+
+  my ($where, @where_values) = $self->_create_filter_for_priceupdate;
+  my $num_updated = 0;
+
+  # connect to database
+  my $dbh = SL::DB->client->dbh;
+
+  for my $column (qw(sellprice listprice)) {
+    my $row = $filter->{prices}{$column};
+    next if ($row->{price_as_number} eq "");
+
+    my $value = $::form->parse_amount(\%::myconfig, $row->{price_as_number});
+    my $operator = '+';
+
+    if ($row->{type} eq "percent") {
+      $value = ($value / 100) + 1;
+      $operator = '*';
+    }
+
+    my $query =
+      qq|UPDATE parts SET $column = $column $operator ?
+         WHERE id IN
+           (SELECT p.id
+            FROM parts p
+            LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
+            WHERE $where)|;
+    my $result    = do_query($::form, $dbh, $query, $value, @where_values);
+    $num_updated += $result if 0 <= $result;
+  }
+
+  my $q_add =
+    qq|UPDATE prices SET price = price + ?
+       WHERE parts_id IN
+         (SELECT p.id
+          FROM parts p
+          LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
+          WHERE $where) AND (pricegroup_id = ?)|;
+  my $sth_add = prepare_query($::form, $dbh, $q_add);
+
+  my $q_multiply =
+    qq|UPDATE prices SET price = price * ?
+       WHERE parts_id IN
+         (SELECT p.id
+          FROM parts p
+          LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
+          WHERE $where) AND (pricegroup_id = ?)|;
+  my $sth_multiply = prepare_query($::form, $dbh, $q_multiply);
+
+  for my $pg (@{ $self->pricegroups }) {
+    my $row = $filter->{prices}{$pg->id};
+    next if $row->{price_as_number} eq "";
+
+    my $value = $::form->parse_amount(\%::myconfig, $row->{price_as_number});
+    my $result;
+
+    if ($row->{type} eq "percent") {
+      $result = do_statement($::form, $sth_multiply, $q_multiply, ($value / 100) + 1, @where_values, $pg->id);
+    } else {
+      $result = do_statement($::form, $sth_add, $q_add, $value, @where_values, $pg->id);
+    }
+
+    $num_updated += $result if (0 <= $result);
+  }
+
+  $sth_add->finish;
+  $sth_multiply->finish;
+
+  1;
+}
+
+sub init_pricegroups {
+  SL::DB::Manager::Pricegroup->get_all_sorted(query => [
+    obsolete => 0,
+  ]);
+}
+
+sub init_pricegroups_by_id {
+  +{ map { $_->id => $_ } @{ $_[0]->pricegroups } }
+}
+
+sub check_rights {
+  $::auth->assert('part_service_assembly_edit');
+}
+
+sub init_filter {
+  $::form->{filter} || {};
+}
+
+sub setup_search_update_prices_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Continue'),
+        submit    => [ '#form', { action => 'PartsPriceUpdate/confirm_price_update' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_confirm_price_update_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Continue'),
+        submit    => [ '#form', { action => 'PartsPriceUpdate/update_prices' } ],
+        accesskey => 'enter',
+      ],
+
+      action => [
+        t8('Back'),
+        call => [ 'kivi.history_back' ],
+      ],
+    );
+  }
+}
+
+1;
diff --git a/SL/Controller/PayPostingImport.pm b/SL/Controller/PayPostingImport.pm
new file mode 100644 (file)
index 0000000..c8d3909
--- /dev/null
@@ -0,0 +1,186 @@
+package SL::Controller::PayPostingImport;
+use strict;
+use parent qw(SL::Controller::Base);
+
+use SL::File;
+use SL::Helper::DateTime;
+use SL::Helper::Flash qw(flash_later);
+use SL::Locale::String qw(t8);
+
+use Carp;
+use Text::CSV_XS;
+
+__PACKAGE__->run_before('check_auth');
+
+
+sub action_upload_pay_postings {
+  my ($self, %params) = @_;
+
+  $self->setup_pay_posting_action_bar;
+  $self->render('pay_posting_import/form', title => $::locale->text('Import Pay Postings'));
+}
+
+sub action_import_datev_pay_postings {
+  my ($self, %params) = @_;
+
+  die t8("missing file for action import") unless ($::form->{file});
+
+  my $filename= $::form->{ATTACHMENTS}{file}{filename};
+  # check name and first fields of CSV data
+  die t8("Wrong file name, expects name like: DTVF_*_LOHNBUCHUNG*.csv") unless $filename =~ /^DTVF_.*_LOHNBUCHUNGEN_LUG.*\.csv$/;
+  die t8("not a valid DTVF file, expected first field in A1 'DTVF'")    unless ($::form->{file} =~ m/^('|")?DTVF/);
+  die t8("not a valid DTVF file, expected field header start with 'Umsatz; (..) ;Konto;Gegenkonto'")
+    unless ($::form->{file} =~ m/Umsatz;S\/H;;;;;Konto;Gegenkonto.*;;Belegdatum;Belegfeld 1;Belegfeld 2;;Buchungstext/);
+
+  # check if file is already imported
+  my $acc_trans_doc = SL::DB::Manager::AccTransaction->get_first(query => [ source => $filename ]);
+  die t8("Already imported: ") . $acc_trans_doc->source if ref $acc_trans_doc eq 'SL::DB::AccTransaction';
+
+  if (parse_and_import($self)) {
+    flash_later('info', t8("All pay postings successfully imported."));
+  }
+  $self->setup_pay_posting_action_bar;
+  $self->render('pay_posting_import/form', title => $::locale->text('Imported Pay Postings'));
+}
+
+sub parse_and_import {
+  my $self     = shift;
+
+  my $csv = Text::CSV_XS->new ({ binary => 0, auto_diag => 1, sep_char => ";" });
+  open (my $fh, "<:encoding(cp1252)", \$::form->{file}) or die "cannot open $::form->{file} $!";
+  # Read/parse CSV
+  # Umsatz S/H Konto Gegenkonto (ohne BU-Schlüssel) Belegdatum Belegfeld 1 Belegfeld 2 Buchungstext
+  my $year = substr($csv->getline($fh)->[12], 0, 4);
+
+  # whole import or nothing
+  my $current_transaction;
+  SL::DB->client->with_transaction(sub {
+    while (my $row = $csv->getline($fh)) {
+      next unless $row->[0] =~ m/\d/;
+      my ($credit, $debit, $dt_to_kivi, $length, $accno_credit, $accno_debit,
+          $department_name, $department);
+
+      # check valid soll/haben kennzeichen
+      croak("No valid debit/credit sign") unless $row->[1] =~ m/^(S|H)$/;
+
+      # check transaction date can be 4 or 3 digit (leading 0 omitted)
+      $length = length $row->[9] == 4 ? 2 : 1;
+      $dt_to_kivi = DateTime->new(year  => $year,
+                                  month => substr ($row->[9], -2),
+                                  day   => substr($row->[9],0, $length))->to_kivitendo;
+
+      croak("Something wrong with date conversion") unless $dt_to_kivi;
+
+      $accno_credit = $row->[1] eq 'S' ? $row->[7] : $row->[6];
+      $accno_debit  = $row->[1] eq 'S' ? $row->[6] : $row->[7];
+      $credit   = SL::DB::Manager::Chart->find_by(accno => $accno_credit);
+      $debit    = SL::DB::Manager::Chart->find_by(accno => $accno_debit);
+
+      croak("No such Chart $accno_credit") unless ref $credit eq 'SL::DB::Chart';
+      croak("No such Chart $accno_debit")  unless ref $debit  eq 'SL::DB::Chart';
+
+      # optional KOST1 - KOST2 ?
+      $department_name = $row->[36];
+      if ($department_name) {
+        $department    = SL::DB::Manager::Department->get_first(where => [ description => { ilike =>  $department_name . '%' } ]);
+      }
+
+      my $amount = $::form->parse_amount({ numberformat => '1000,00' }, $row->[0]);
+
+      $current_transaction = SL::DB::GLTransaction->new(
+          employee_id    => $::form->{employee_id},
+          transdate      => $dt_to_kivi,
+          description    => $row->[13],
+          reference      => $row->[13],
+          department_id  => ref $department eq 'SL::DB::Department' ?  $department->id : undef,
+          imported       => 1,
+          taxincluded    => 1,
+        )->add_chart_booking(
+          chart  => $credit,
+          credit => $amount,
+          source => $::form->{ATTACHMENTS}{file}{filename},
+        )->add_chart_booking(
+          chart  => $debit,
+          debit  => $amount,
+          source => $::form->{ATTACHMENTS}{file}{filename},
+      )->post;
+
+      push @{ $self->{gl_trans} }, $current_transaction;
+
+      if ($::instance_conf->get_doc_storage) {
+        my $file = SL::File->save(object_id   => $current_transaction->id,
+                       object_type => 'gl_transaction',
+                       mime_type   => 'text/csv',
+                       source      => 'uploaded',
+                       file_type   => 'attachment',
+                       file_name   => $::form->{ATTACHMENTS}{file}{filename},
+                       file_contents   => $::form->{file},
+                      );
+      }
+    }
+
+    1;
+
+  }) or do { die t8("Cannot add Booking, reason: #1 DB: #2 ", $@, SL::DB->client->error) };
+}
+
+sub check_auth {
+  $::auth->assert('general_ledger');
+}
+
+sub setup_pay_posting_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        $::locale->text('Import'),
+        submit    => [ '#form', { action => 'PayPostingImport/import_datev_pay_postings' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Controller::PayPostingImport
+Controller for importing pay postings.
+Currently only DATEV format is supported.
+
+
+=head1 FUNCTIONS
+
+=over 2
+
+=item C<action_upload_pay_postings>
+
+Simple upload form. HTML Form allows only CSV files.
+
+
+=item C<action_import_datev_pay_postings>
+
+Does some sanity checks for the CSV file according to the expected DATEV data structure
+If successful calls the parse_and_import function
+
+=item C<parse_and_import>
+
+Internal function for parsing and importing every line of the CSV data as a GL Booking.
+Adds the attribute imported for the GL Booking.
+If a chart which uses a tax automatic is assigned the tax will be calculated with the
+'tax_included' option, which defaults to the DATEV format.
+
+Furthermore adds the original CSV filename for every AccTransaction and puts the CSV in every GL Booking
+if the feature DMS is active.
+If a Chart is missing or any kind of different error occurs the whole import including the DMS addition
+will be aborted
+
+=back
index 5064f75..4f4c6ca 100644 (file)
@@ -7,6 +7,7 @@ use parent qw(SL::Controller::Base);
 use SL::DB::PaymentTerm;
 use SL::DB::Language;
 use SL::Helper::Flash;
+use SL::Locale::String qw(t8);
 
 use Rose::Object::MakeMethods::Generic
 (
@@ -25,6 +26,7 @@ __PACKAGE__->run_before('setup',             only => [ qw(new      edit) ]);
 sub action_list {
   my ($self) = @_;
 
+  $self->setup_list_action_bar;
   $self->render('payment_term/list',
                 title         => $::locale->text('Payment terms'),
                 PAYMENT_TERMS => SL::DB::Manager::PaymentTerm->get_all_sorted);
@@ -34,12 +36,14 @@ sub action_new {
   my ($self) = @_;
 
   $self->{payment_term} = SL::DB::PaymentTerm->new(auto_calculation => 1);
+  $self->setup_form_action_bar;
   $self->render('payment_term/form', title => $::locale->text('Create a new payment term'));
 }
 
 sub action_edit {
   my ($self) = @_;
 
+  $self->setup_form_action_bar;
   $self->render('payment_term/form', title => $::locale->text('Edit payment term'));
 }
 
@@ -110,6 +114,7 @@ sub create_or_update {
   $self->{payment_term}->save;
   foreach my $language (@{ $self->{languages} }) {
     $self->{payment_term}->save_attribute_translation('description_long', $language, $::form->{"translation_" . $language->id});
+    $self->{payment_term}->save_attribute_translation('description_long_invoice', $language, $::form->{"translation_invoice_" . $language->id});
   }
 
   flash_later('info', $is_new ? $::locale->text('The payment term has been created.') : $::locale->text('The payment term has been saved.'));
@@ -126,4 +131,49 @@ sub load_languages {
   $self->{languages} = SL::DB::Manager::Language->get_all_sorted;
 }
 
+sub setup_list_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      link => [
+        t8('Add'),
+        link => $self->url_for(action => 'new'),
+      ],
+    );
+  }
+}
+
+sub setup_form_action_bar {
+  my ($self) = @_;
+
+  my $is_new = !$self->payment_term->id;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => 'PaymentTerm/' . ($is_new ? 'create' : 'update') } ],
+        checks    => [ 'kivi.validate_form' ],
+        accesskey => 'enter',
+      ],
+
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => 'PaymentTerm/destroy' } ],
+        confirm  => t8('Do you really want to delete this object?'),
+        disabled => $is_new ? t8('This object has not been saved yet.') : undef,
+      ],
+
+      'separator',
+
+      link => [
+        t8('Abort'),
+        link => $self->url_for(action => 'list'),
+      ],
+    );
+  }
+  $::request->layout->add_javascripts('kivi.Validator.js');
+}
+
 1;
diff --git a/SL/Controller/PhoneNumber.pm b/SL/Controller/PhoneNumber.pm
new file mode 100644 (file)
index 0000000..5b83a35
--- /dev/null
@@ -0,0 +1,230 @@
+package SL::Controller::PhoneNumber;
+
+use utf8;
+use strict;
+use parent qw(SL::Controller::Base);
+
+use List::MoreUtils qw(any);
+use List::Util qw(first);
+
+use SL::JSON;
+use SL::DBUtils;
+use SL::Locale::String;
+use SL::CTI;
+
+use SL::DB::Contact;
+use SL::DB::Customer;
+use SL::DB::Vendor;
+
+use Data::Dumper;
+
+sub action_look_up {
+  my ($self) = @_;
+
+  my $number = $self->normalize_number($::form->{number} // '');
+
+  return $self->render(\to_json({}), { type => 'json', process => 0 }) if ($number eq '');
+
+  my $result = $self->find_contact_for_number($number)
+    //         $self->find_customer_vendor_for_number($number)
+    //         {};
+
+  $self->render(\to_json($result), { type => 'json', process => 0 });
+}
+
+sub find_contact_for_number {
+  my ($self, $number) = @_;
+
+  my @number_fields = qw(cp_phone1 cp_phone2 cp_mobile1 cp_mobile2 cp_fax);
+
+  my $contacts = SL::DB::Manager::Contact->get_all(
+    inject_results => 1,
+    where          => [ map({ (or => [ "!$_" => undef, "!$_"  => "" ],) } @number_fields) ],
+  );
+
+  my @hits;
+
+  foreach my $contact (@{ $contacts }) {
+    foreach my $field (@number_fields) {
+      next if $self->normalize_number($contact->$field) ne $number;
+
+      push @hits, $contact;
+      last;
+    }
+  }
+
+  return if !@hits;
+
+  my @cv_ids = grep { $_ } map { $_->cp_cv_id } @hits;
+
+  my %customers_vendors =
+    map { ($_->id => $_) } (
+      @{ SL::DB::Manager::Customer->get_all(where => [ id => \@cv_ids ], inject_results => 1) },
+      @{ SL::DB::Manager::Vendor  ->get_all(where => [ id => \@cv_ids ], inject_results => 1) },
+    );
+
+  my $chosen = first {
+       $_->cp_cv_id
+    &&  $customers_vendors{$_->cp_cv_id}
+    && !$customers_vendors{$_->cp_cv_id}->obsolete
+    && ($_->cp_name !~ m{ungültig}i)
+  } @hits;
+
+  $chosen //= $hits[0];
+
+  return {
+    full_name => join(' ', grep { $_ ne '' } map { $_ // '' } ($chosen->cp_title, $chosen->cp_givenname, $chosen->cp_name)),
+    id        => $chosen->cp_id,
+    type      => 'contact',
+    map({ my $method = "cp_$_"; ($_ => $chosen->$method // '') } qw(title givenname name phone1 phone2 mobile1 mobile2 fax)),
+  };
+}
+
+sub find_customer_vendor_for_number {
+  my ($self, $number) = @_;
+
+  my @number_fields = qw(phone fax);
+
+  my @customers_vendors = map {
+      my $class = "SL::DB::Manager::$_";
+      @{ $class->get_all(
+           inject_results => 1,
+           where          => [ map({ (or => [ "!$_" => undef, "!$_"  => "" ],) } @number_fields) ],
+         ) }
+    } qw(Customer Vendor);
+
+  my @hits;
+
+  foreach my $customer_vendor (@customers_vendors) {
+    foreach my $field (@number_fields) {
+      next if $self->normalize_number($customer_vendor->$field) ne $number;
+
+      push @hits, $customer_vendor;
+      last;
+    }
+  }
+
+  return if !@hits;
+
+  my $chosen = first { !$_->obsolete } @hits;
+  $chosen  //= $hits[0];
+
+  return {
+    full_name => $chosen->name  // '',
+    phone1    => $chosen->phone // '',
+    fax       => $chosen->fax   // '',
+    id        => $chosen->id,
+    type      => ref($chosen) eq 'SL::DB::Customer' ? 'customer' : 'vendor',
+    map({ ($_ => '') } qw(title givenname name phone2 mobile1 mobile2)),
+  };
+}
+
+sub normalize_number {
+  my ($self, $number) = @_;
+
+  return '' if ($number // '') eq '';
+
+  my $config       = $::lx_office_conf{cti} || {};
+  my $idp          = $config->{international_dialing_prefix} // '00';
+  my $country_code = $config->{our_country_code}             // '49';
+
+  $number          = SL::CTI->sanitize_number(number => $number);
+
+  return $number if $number =~ m{^$idp};
+
+  $number =~ s{^0+}{};
+
+  return $idp . $country_code . $number;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Controller::Contact - Looking up information on contacts/customers/vendors based on a phone number
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<action_look_up>
+
+This action can be used by external systems such as PBXes in order to
+match a calling number to a name. Requires one form parameter to be
+passed, C<number>.
+
+The number will then be normalized. This requires that the
+international dialing prefix and the server's own country code be set
+up in C<kivitendo.conf>, section C<[cti]>. They default to C<00> and
+C<49> (Germany) respectively.
+
+Next the function will look up a contact whose normalized numbers
+equals the requested number. The fields C<phone1>, C<phone2>,
+C<mobile1>, C<mobile2> and C<fax> are considered. Active contacts are
+given preference over inactive ones (inactive meaning that they don't
+belong to a customer/vendor anymore or that the customer/vendor itself
+is flagged as obsolete).
+
+If no contact is found, customers & vendors are searched. Their fields
+C<phone> and C<fax> are considered. The first customer/vendor who
+isn't flagged as being obsolete is chosen; if there's none, the first
+obsolete one is.
+
+The function always sends one JSON-encoded object. If there's no hit
+for the number, an empty object is returned. Otherwise the following
+fields are present:
+
+=over 4
+
+=item C<id> — the database ID of the corresponding record
+
+=item C<type> — describes the type of record returned; can be either
+C<contact>, C<customer> or C<vendor>
+
+=item C<full_name> — for contacts this is the concatenation of the
+title, given name and family name; for customers/vendors it's the
+company name
+
+=item C<title> — title (empty for customers/vendors)
+
+=item C<givenname> — first/given name (empty for customers/vendors)
+
+=item C<name> — last/family name (empty for customers/vendors)
+
+=item C<phone1> — first phone number (for all object types)
+
+=item C<phone2> — second phone number (empty for customers/vendors)
+
+=item C<mobile1> — first mobile number (empty for customers/vendors)
+
+=item C<mobile2> — second mobile number (empty for customers/vendors)
+
+=item C<fax> — fax number (for all object types)
+
+=back
+
+Here's an example how querying the API via C<curl> might look:
+
+    curl --user user_name:password 'https://…/kivitendo/controller.pl?action=PhoneNumber/look_up&number=0049987655443321'
+
+Note that the request must be authenticated via a valid Kivitendo
+login. However, the user doesn't need any special permissions within
+Kivitendo; any valid Kivitendo user will do.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
index 1a42a71..0579c53 100644 (file)
@@ -34,6 +34,8 @@ sub action_list {
 
   my $price_rules = $self->models->get;
 
+  $self->setup_search_action_bar;
+
   $self->prepare_report;
 
   $self->report_generator_list_objects(report => $self->{report}, objects => $price_rules, $::form->{inline} ? (layout => 0, header => 0) : ());
@@ -109,6 +111,7 @@ sub display_form {
   my ($self, %params) = @_;
   my $is_new  = !$self->price_rule->id;
   my $title   = $self->form_title(($is_new ? 'create' : 'edit'), $self->price_rule->type);
+  $self->setup_form_action_bar;
   $self->render('price_rule/form',
     title => $title,
     %params
@@ -236,7 +239,7 @@ sub all_price_rule_item_types {
 }
 
 sub add_javascripts  {
-  $::request->{layout}->add_javascripts(qw(kivi.PriceRule.js autocomplete_customer.js autocomplete_vendor.js autocomplete_part.js));
+  $::request->{layout}->add_javascripts(qw(kivi.PriceRule.js autocomplete_vendor.js kivi.Part.js kivi.CustomerVendor.js));
 }
 
 sub init_price_rule {
@@ -271,7 +274,7 @@ sub init_businesses {
 }
 
 sub init_pricegroups {
-  SL::DB::Manager::Pricegroup->get_all;
+  SL::DB::Manager::Pricegroup->get_all_sorted;
 }
 
 sub init_partsgroups {
@@ -300,4 +303,71 @@ sub init_models {
   );
 }
 
+sub setup_search_action_bar {
+  my ($self, %params) = @_;
+
+  return if $::form->{inline};
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#search_form', { action => 'PriceRule/list' } ],
+        accesskey => 'enter',
+      ],
+
+      combobox => [
+        action => [
+          t8('Add'),
+        ],
+        link => [
+          t8('New Sales Price Rule'),
+          link => $self->url_for(action => 'new', 'price_rule.type' => 'customer', callback => $self->models->get_callback),
+        ],
+        link => [
+          t8('New Purchase Price Rule'),
+          link => $self->url_for(action => 'new', 'price_rule.type' => 'vendor', callback => $self->models->get_callback),
+        ],
+      ], # end of combobox "Add"
+    );
+  }
+}
+
+sub setup_form_action_bar {
+  my ($self) = @_;
+
+  my $is_new = !$self->price_rule->id;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [
+          $is_new ? t8('Create') : t8('Save'),
+          submit    => [ '#form', { action => 'PriceRule/' . ($is_new ? 'create' : 'update') } ],
+          accesskey => 'enter',
+        ],
+        action => [
+          t8('Use as new'),
+          submit   => [ '#form', { action => 'PriceRule/create' } ],
+          disabled => $is_new ? t8('The object has not been saved yet.') : undef,
+        ],
+      ], # end of combobox "Save"
+
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => 'PriceRule/destroy' } ],
+        confirm  => t8('Do you really want to delete this object?'),
+        disabled => $is_new                   ? t8('The object has not been saved yet.')
+                  : $self->price_rule->in_use ? t8('This object has already been used.')
+                  :                             undef,
+      ],
+
+      link => [
+        t8('Abort'),
+        link => $self->url_for(action => 'list', 'filter.type' => $self->price_rule->type),
+      ],
+    );
+  }
+}
+
 1;
index aea9f46..c0c113c 100644 (file)
@@ -61,7 +61,13 @@ sub render_price_dialog {
 #
 
 sub check_auth {
-  $::auth->assert('edit_prices');
+  if ($::form->{vc} eq 'customer') {
+    $::auth->assert('sales_edit_prices');
+  } elsif ($::form->{vc} eq 'vendor') {
+    $::auth->assert('purchase_edit_prices');
+  } else {
+    $::auth->assert('no_such_right');
+  }
 }
 
 sub init_record {
@@ -77,6 +83,8 @@ sub _make_record_item {
     sales_quotation         => 'OrderItem',
     request_quotation       => 'OrderItem',
     invoice                 => 'InvoiceItem',
+    invoice_for_advance_payment => 'InvoiceItem',
+    final_invoice           => 'InvoiceItem',
     purchase_invoice        => 'InvoiceItem',
     credit_note             => 'InvoiceItem',
     purchase_delivery_order => 'DeliveryOrderItem',
@@ -87,25 +95,44 @@ sub _make_record_item {
 
   $class = 'SL::DB::' . $class;
 
+  my %translated_methods = (
+    'SL::DB::OrderItem' => {
+      id                      => 'parts_id',
+      orderitems_id           => 'id',
+    },
+    'SL::DB::DeliveryOrderItem' => {
+      id                      => 'parts_id',
+      delivery_order_items_id => 'id',
+    },
+    'SL::DB::InvoiceItem' => {
+      id                      => 'parts_id',
+      invoice_id => 'id',
+    },
+  );
+
   eval "require $class";
 
   my $obj = $::form->{"orderitems_id_$row"}
           ? $class->meta->convention_manager->auto_manager_class_name->find_by(id => $::form->{"orderitems_id_$row"})
           : $class->new;
 
-  for my $method (apply { s/_$row$// } grep { /_$row$/ } keys %$::form) {
+  for my $key (grep { /_$row$/ } keys %$::form) {
+    my $method = $key;
+    $method =~ s/_$row$//;
+    $method = $translated_methods{$class}{$method} // $method;
+    my $value = $::form->{$key};
     if ($obj->meta->column($method)) {
       if ($obj->meta->column($method)->isa('Rose::DB::Object::Metadata::Column::Date')) {
-        $obj->${\"$method\_as_date"}($::form->{"$method\_$row"});
+        $obj->${\"$method\_as_date"}($value);
       } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::(?:Numeric|Float|DoublePrecsion)$/) {
-        $obj->${\"$method\_as_number"}($::form->{"$method\_$row"});
+        $obj->${\"$method\_as_number"}($value);
       } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::Boolean$/) {
-        $obj->$method(!!$::form->{$method});
+        $obj->$method(!!$value);
       } else {
-        $obj->$method($::form->{"$method\_$row"});
+        $obj->$method($value);
       }
     } else {
-      $obj->{__additional_form_attributes}{$method} = $::form->{"$method\_$row"};
+      $obj->{__additional_form_attributes}{$method} = $value;
     }
   }
 
@@ -175,4 +202,3 @@ sub _make_record {
 }
 
 1;
-
index fc72036..702db89 100644 (file)
@@ -12,6 +12,7 @@ use SL::Controller::Helper::ReportGenerator;
 use SL::CVar;
 use SL::DB::Customer;
 use SL::DB::DeliveryOrder;
+use SL::DB::Employee;
 use SL::DB::Invoice;
 use SL::DB::Order;
 use SL::DB::Project;
@@ -28,35 +29,24 @@ use Rose::DB::Object::Helpers qw(as_tree);
 
 use Rose::Object::MakeMethods::Generic
 (
- scalar => [ qw(project linked_records) ],
- 'scalar --get_set_init' => [ qw(models customers project_types project_statuses projects) ],
+ scalar => [ qw(project) ],
+ 'scalar --get_set_init' => [ qw(models customers project_types project_statuses projects linked_records employees may_edit_invoice_permissions
+                                 cvar_configs includeable_cvar_configs include_cvars) ],
 );
 
 __PACKAGE__->run_before('check_auth',   except => [ qw(ajax_autocomplete) ]);
 __PACKAGE__->run_before('load_project', only   => [ qw(edit update destroy) ]);
+__PACKAGE__->run_before('use_multiselect_js', only => [ qw(new create edit update) ]);
 
 #
 # actions
 #
 
-sub action_search {
-  my ($self) = @_;
-
-  my %params;
-
-  $params{CUSTOM_VARIABLES}  = CVar->get_configs(module => 'Projects');
-
-  ($params{CUSTOM_VARIABLES_FILTER_CODE}, $params{CUSTOM_VARIABLES_INCLUSION_CODE})
-    = CVar->render_search_options(variables      => $params{CUSTOM_VARIABLES},
-                                  include_prefix => 'l_',
-                                  include_value  => 'Y');
-
-  $self->render('project/search', %params);
-}
-
 sub action_list {
   my ($self) = @_;
 
+  $self->setup_list_action_bar;
+
   $self->make_filter_summary;
 
   $self->prepare_report;
@@ -69,15 +59,14 @@ sub action_new {
 
   $self->project(SL::DB::Project->new);
   $self->display_form(title    => $::locale->text('Create a new project'),
-                      callback => $::form->{callback} || $self->url_for(action => 'new'));
+                      callback => $::form->{callback} || $self->url_for(action => 'list'));
 }
 
 sub action_edit {
   my ($self) = @_;
 
-  $self->get_linked_records;
   $self->display_form(title    => $::locale->text('Edit project #1', $self->project->projectnumber),
-                      callback => $::form->{callback} || $self->url_for(action => 'edit', id => $self->project->id));
+                      callback => $::form->{callback} || $self->url_for(action => 'list'));
 }
 
 sub action_create {
@@ -101,7 +90,7 @@ sub action_destroy {
     flash_later('error', $::locale->text('The project is in use and cannot be deleted.'));
   }
 
-  $self->redirect_to(action => 'search');
+  $self->redirect_to(action => 'list');
 }
 
 sub action_ajax_autocomplete {
@@ -110,32 +99,35 @@ sub action_ajax_autocomplete {
   $::form->{filter}{'all:substr:multi::ilike'} =~ s{[\(\)]+}{}g;
 
   # if someone types something, and hits enter, assume he entered the full name.
-  # if something matches, treat that as sole match
-  # unfortunately get_models can't do more than one per package atm, so we d it
-  # the oldfashioned way.
+  # if something matches, treat that as the sole match
+  # since we need a second get models instance with different filters for that,
+  # we only modify the original filter temporarily in place
   if ($::form->{prefer_exact}) {
+    local $::form->{filter}{'all::ilike'} = delete local $::form->{filter}{'all:substr:multi::ilike'};
+    # active and valid filters are use as they are
+
+    my $exact_models = SL::Controller::Helper::GetModels->new(
+      controller   => $self,
+      sorted       => 0,
+      paginated    => { per_page => 2 },
+      with_objects => [ 'customer', 'project_status', 'project_type' ],
+    );
     my $exact_matches;
-    if (1 == scalar @{ $exact_matches = SL::DB::Manager::Project->get_all(
-      query => [
-        obsolete => 0,
-        SL::DB::Manager::Project->type_filter($::form->{filter}{type}),
-        or => [
-          description   => { ilike => $::form->{filter}{'all:substr:multi::ilike'} },
-          projectnumber => { ilike => $::form->{filter}{'all:substr:multi::ilike'} },
-        ]
-      ],
-      limit => 2,
-    ) }) {
-      $self->projects($exact_matches);
+    if (1 == scalar @{ $exact_matches = $exact_models->get }) {
+      $self->project($exact_matches);
     }
   }
 
   $::form->{sort_by} = 'customer_and_description';
 
+  my $description_style = ($::form->{description_style} =~ m{both|number|description|full})
+                        ? $::form->{description_style}
+                        : 'full';
+
   my @hashes = map {
    +{
-     value         => $_->full_description(style => 'full'),
-     label         => $_->full_description(style => 'full'),
+     value         => $_->full_description(style => $description_style),
+     label         => $_->full_description(style => $description_style),
      id            => $_->id,
      projectnumber => $_->projectnumber,
      description   => $_->description,
@@ -150,6 +142,14 @@ sub action_test_page {
   $_[0]->render('project/test_page');
 }
 
+sub action_project_picker_search {
+  $_[0]->render('project/project_picker_search', { layout => 0 });
+}
+
+sub action_project_picker_result {
+  $_[0]->render('project/_project_picker_result', { layout => 0 });
+}
+
 #
 # filters
 #
@@ -164,6 +164,57 @@ sub check_auth {
 
 sub init_project_statuses { SL::DB::Manager::ProjectStatus->get_all_sorted }
 sub init_project_types    { SL::DB::Manager::ProjectType->get_all_sorted   }
+sub init_employees        { SL::DB::Manager::Employee->get_all_sorted   }
+sub init_may_edit_invoice_permissions { $::auth->assert('project_edit_view_invoices_permission', 1) }
+sub init_cvar_configs                 { SL::DB::Manager::CustomVariableConfig->get_all_sorted(where => [ module => 'Projects' ]) }
+sub init_includeable_cvar_configs     { [ grep { $_->includeable } @{ $_[0]->cvar_configs } ] };
+
+sub init_include_cvars {
+  my ($self) = @_;
+  return { map { ($_->name => $::form->{"include_cvars_" . $_->name}) }       @{ $self->cvar_configs } } if $::form->{_include_cvars_from_form};
+  return { map { ($_->name => ($_->includeable && $_->included_by_default)) } @{ $self->cvar_configs } };
+}
+
+sub init_linked_records {
+  my ($self) = @_;
+  return [
+    map  { @{ $_ } }
+    grep { $_      } (
+      SL::DB::Manager::Invoice->        get_all(where        => [ invoice => 1, or => [ globalproject_id => $self->project->id, 'invoiceitems.project_id' => $self->project->id ] ],
+                                                with_objects => [ 'invoiceitems', 'customer' ],
+                                                distinct     => [ 'customer' ],
+                                                sort_by       => 'transdate ASC'),
+      SL::DB::Manager::Invoice->        get_all(where        => [ invoice => 0, or => [ globalproject_id => $self->project->id, 'transactions.project_id' => $self->project->id ] ],
+                                                with_objects => [ 'transactions', 'customer' ],
+                                                distinct     => [ 'customer' ],
+                                                sort_by       => 'transdate ASC'),
+      SL::DB::Manager::PurchaseInvoice->get_all(where => [ invoice => 1,
+                                                           or => [ globalproject_id => $self->project->id, 'invoiceitems.project_id' => $self->project->id ]
+                                                         ],
+                                                with_objects => [ 'invoiceitems', 'vendor' ],
+                                                distinct     => [ 'customer' ],
+                                                sort_by => 'transdate ASC'),
+      SL::DB::Manager::PurchaseInvoice->get_all(where => [ invoice => 0,
+                                                           or => [ globalproject_id => $self->project->id, 'transactions.project_id' => $self->project->id ]
+                                                         ],
+                                                with_objects => [ 'transactions', 'vendor' ],
+                                                distinct     => [ 'customer' ],
+                                                sort_by => 'transdate ASC'),
+      SL::DB::Manager::GLTransaction->  get_all(where => [ 'transactions.project_id' => $self->project->id ],
+                                                with_objects => [ 'transactions' ],
+                                                distinct     => 1,
+                                                sort_by => 'transdate ASC'),
+      SL::DB::Manager::Order->          get_all(where => [ or => [ globalproject_id => $self->project->id, 'orderitems.project_id' => $self->project->id ] ],
+                                                with_objects => [ 'orderitems', 'customer', 'vendor' ],
+                                                distinct => [ 'customer', 'vendor' ],
+                                                sort_by => 'transdate ASC' ),
+      SL::DB::Manager::DeliveryOrder->  get_all(where => [ or => [ globalproject_id => $self->project->id, 'orderitems.project_id' => $self->project->id ] ],
+                                                with_objects => [ 'orderitems', 'customer', 'vendor' ],
+                                                distinct => [ 'customer', 'vendor' ],
+                                                sort_by => 'transdate ASC'),
+    )];
+}
+
 
 sub init_projects {
   if ($::form->{no_paginate}) {
@@ -180,6 +231,10 @@ sub init_customers {
   return SL::DB::Manager::Customer->get_all_sorted(where => [ or => [ obsolete => 0, obsolete => undef, @customer_id ]]);
 }
 
+sub use_multiselect_js {
+  $::request->layout->use_javascript("${_}.js") for qw(jquery.selectboxes jquery.multiselect2side);
+}
+
 sub display_form {
   my ($self, %params) = @_;
 
@@ -193,6 +248,9 @@ sub display_form {
 
   CVar->render_inputs(variables => $params{CUSTOM_VARIABLES}) if @{ $params{CUSTOM_VARIABLES} };
 
+  $::request->layout->use_javascript("$_.js") for qw(kivi.File ckeditor/ckeditor ckeditor/adapters/jquery);
+  $self->setup_edit_action_bar(callback => $params{callback});
+
   $self->render('project/form', %params);
 }
 
@@ -201,6 +259,12 @@ sub create_or_update {
   my $is_new = !$self->project->id;
   my $params = delete($::form->{project}) || { };
 
+  if (!$self->may_edit_invoice_permissions) {
+    delete $params->{employee_invoice_permissions};
+  } elsif (!$params->{employee_invoice_permissions}) {
+    $params->{employee_invoice_permissions} = [];
+  }
+
   delete $params->{id};
   $self->project->assign_attributes(%{ $params });
 
@@ -234,18 +298,6 @@ sub load_project {
   $self->project(SL::DB::Project->new(id => $::form->{id})->load);
 }
 
-sub get_linked_records {
-  my ($self) = @_;
-
-  $self->linked_records([
-    map  { @{ $_ } }
-    grep { $_      } (
-      SL::DB::Manager::Order->          get_all(where => [ globalproject_id => $self->project->id ], with_objects => [ 'customer', 'vendor' ], sort_by => 'transdate ASC'),
-      SL::DB::Manager::DeliveryOrder->  get_all(where => [ globalproject_id => $self->project->id ], with_objects => [ 'customer', 'vendor' ], sort_by => 'transdate ASC'),
-      SL::DB::Manager::Invoice->        get_all(where => [ globalproject_id => $self->project->id ], with_objects => [ 'customer'           ], sort_by => 'transdate ASC'),
-      SL::DB::Manager::PurchaseInvoice->get_all(where => [ globalproject_id => $self->project->id ], with_objects => [             'vendor' ], sort_by => 'transdate ASC'),
-    )]);
-}
 
 sub prepare_report {
   my ($self)      = @_;
@@ -263,7 +315,8 @@ sub prepare_report {
     description   => { obj_link => sub { $self->url_for(action => 'edit', id => $_[0]->id, callback => $callback) } },
     project_type  => { sub  => sub { $_[0]->project_type->description } },
     project_status => { sub  => sub { $_[0]->project_status->description }, text => t8('Status') },
-    customer      => { raw_data  => sub { $_[0]->customer_id ? $self->presenter->customer($_[0]->customer, display => 'table-cell', callback => $callback) : '' } },
+    customer      => { sub       => sub { !$_[0]->customer_id ? '' : $_[0]->customer->name },
+                       raw_data  => sub { !$_[0]->customer_id ? '' : $_[0]->customer->presenter->customer(display => 'table-cell', callback => $callback) } },
     active        => { sub  => sub { $_[0]->active   ? $::locale->text('Active') : $::locale->text('Inactive') },
                        text => $::locale->text('Active') },
     valid         => { sub  => sub { $_[0]->valid    ? $::locale->text('Valid')  : $::locale->text('Invalid')  },
@@ -272,6 +325,21 @@ sub prepare_report {
 
   map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
 
+  # Custom variables
+  my %cvar_column_defs = map {
+    my $cfg = $_;
+    (('cvar_' . $cfg->name) => {
+      sub     => sub { my $var = $_[0]->cvar_by_name($cfg->name); $var ? $var->value_as_text : '' },
+      text    => $cfg->description,
+      visible => $self->include_cvars->{ $cfg->name } ? 1 : 0,
+    })
+  } @{ $self->includeable_cvar_configs };
+
+  push @columns, map { 'cvar_' . $_->name } @{ $self->includeable_cvar_configs };
+  %column_defs = (%column_defs, %cvar_column_defs);
+
+  my @cvar_column_form_names = ('_include_cvars_from_form', map { "include_cvars_" . $_->name } @{ $self->includeable_cvar_configs });
+
   $report->set_options(
     std_column_visibility => 1,
     controller_class      => 'Project',
@@ -282,7 +350,7 @@ sub prepare_report {
   );
   $report->set_columns(%column_defs);
   $report->set_column_order(@columns);
-  $report->set_export_options(qw(list filter));
+  $report->set_export_options(qw(list filter), @cvar_column_form_names);
   $report->set_options_from_form;
   $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
   $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
@@ -344,4 +412,60 @@ sub make_filter_summary {
 
   $self->{filter_summary} = join ', ', @filter_strings;
 }
+
+sub setup_edit_action_bar {
+  my ($self, %params) = @_;
+
+  my $is_new = !$self->project->id;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [
+          t8('Save'),
+          submit    => [ '#form', { action => 'Project/' . ($is_new ? 'create' : 'update') } ],
+          accesskey => 'enter',
+        ],
+        action => [
+          t8('Save as new'),
+          submit   => [ '#form', { action => 'Project/create' }],
+          disabled => $is_new ? t8('The object has not been saved yet.') : undef,
+        ],
+      ], # end of combobox "Save"
+
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => 'Project/destroy' } ],
+        confirm  => $::locale->text('Do you really want to delete this object?'),
+        disabled => $is_new                 ? t8('This object has not been saved yet.')
+                  : $self->project->is_used ? t8('This object has already been used.')
+                  :                           undef,
+      ],
+
+      link => [
+        t8('Abort'),
+        link => $params{callback} || $self->url_for(action => 'list'),
+      ],
+    );
+  }
+}
+
+sub setup_list_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#search_form', { action => 'Project/list' } ],
+        accesskey => 'enter',
+      ],
+      link => [
+        t8('Add'),
+        link => $self->url_for(action => 'new'),
+      ],
+    );
+  }
+}
+
 1;
diff --git a/SL/Controller/ProjectStatus.pm b/SL/Controller/ProjectStatus.pm
deleted file mode 100644 (file)
index 686f75c..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-package SL::Controller::ProjectStatus;
-
-use strict;
-
-use parent qw(SL::Controller::Base);
-
-use SL::DB::ProjectStatus;
-use SL::Helper::Flash;
-
-use Rose::Object::MakeMethods::Generic
-(
- scalar => [ qw(project_status) ],
-);
-
-__PACKAGE__->run_before('check_auth');
-__PACKAGE__->run_before('load_project_status', only => [ qw(edit update destroy) ]);
-
-#
-# actions
-#
-
-sub action_list {
-  my ($self) = @_;
-
-  $self->render('project_status/list',
-                title          => $::locale->text('Project Status'),
-                PROJECT_STATUS => SL::DB::Manager::ProjectStatus->get_all_sorted);
-}
-
-sub action_new {
-  my ($self) = @_;
-
-  $self->{project_status} = SL::DB::ProjectStatus->new;
-  $self->render('project_status/form', title => $::locale->text('Create a new project status'));
-}
-
-sub action_edit {
-  my ($self) = @_;
-  $self->render('project_status/form', title => $::locale->text('Edit project status'));
-}
-
-sub action_create {
-  my ($self) = @_;
-
-  $self->{project_status} = SL::DB::ProjectStatus->new;
-  $self->create_or_update;
-}
-
-sub action_update {
-  my ($self) = @_;
-  $self->create_or_update;
-}
-
-sub action_destroy {
-  my ($self) = @_;
-
-  if (eval { $self->{project_status}->delete; 1; }) {
-    flash_later('info',  $::locale->text('The project status has been deleted.'));
-  } else {
-    flash_later('error', $::locale->text('The project status is in use and cannot be deleted.'));
-  }
-
-  $self->redirect_to(action => 'list');
-}
-
-sub action_reorder {
-  my ($self) = @_;
-
-  SL::DB::ProjectStatus->reorder_list(@{ $::form->{project_status_id} || [] });
-
-  $self->render(\'', { type => 'json' });
-}
-
-#
-# filters
-#
-
-sub check_auth {
-  $::auth->assert('config');
-}
-
-#
-# helpers
-#
-
-sub create_or_update {
-  my $self   = shift;
-  my $is_new = !$self->{project_status}->id;
-  my $params = delete($::form->{project_status}) || { };
-
-  $self->{project_status}->assign_attributes(%{ $params });
-
-  my @errors = $self->{project_status}->validate;
-
-  if (@errors) {
-    flash('error', @errors);
-    $self->render('project_status/form', title => $is_new ? $::locale->text('Create a new project status') : $::locale->text('Edit project status'));
-    return;
-  }
-
-  $self->{project_status}->save;
-
-  flash_later('info', $is_new ? $::locale->text('The project status has been created.') : $::locale->text('The project status has been saved.'));
-  $self->redirect_to(action => 'list');
-}
-
-sub load_project_status {
-  my ($self) = @_;
-  $self->{project_status} = SL::DB::ProjectStatus->new(id => $::form->{id})->load;
-}
-
-1;
diff --git a/SL/Controller/ProjectType.pm b/SL/Controller/ProjectType.pm
deleted file mode 100644 (file)
index e986924..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-package SL::Controller::ProjectType;
-
-use strict;
-
-use parent qw(SL::Controller::Base);
-
-use SL::DB::ProjectType;
-use SL::Helper::Flash;
-
-use Rose::Object::MakeMethods::Generic
-(
- scalar => [ qw(project_type) ],
-);
-
-__PACKAGE__->run_before('check_auth');
-__PACKAGE__->run_before('load_project_type', only => [ qw(edit update destroy) ]);
-
-#
-# actions
-#
-
-sub action_list {
-  my ($self) = @_;
-
-  $self->render('project_type/list',
-                title         => $::locale->text('Project Types'),
-                PROJECT_TYPES => SL::DB::Manager::ProjectType->get_all_sorted);
-}
-
-sub action_new {
-  my ($self) = @_;
-
-  $self->{project_type} = SL::DB::ProjectType->new;
-  $self->render('project_type/form', title => $::locale->text('Create a new project type'));
-}
-
-sub action_edit {
-  my ($self) = @_;
-  $self->render('project_type/form', title => $::locale->text('Edit project type'));
-}
-
-sub action_create {
-  my ($self) = @_;
-
-  $self->{project_type} = SL::DB::ProjectType->new;
-  $self->create_or_update;
-}
-
-sub action_update {
-  my ($self) = @_;
-  $self->create_or_update;
-}
-
-sub action_destroy {
-  my ($self) = @_;
-
-  if (eval { $self->{project_type}->delete; 1; }) {
-    flash_later('info',  $::locale->text('The project type has been deleted.'));
-  } else {
-    flash_later('error', $::locale->text('The project type is in use and cannot be deleted.'));
-  }
-
-  $self->redirect_to(action => 'list');
-}
-
-sub action_reorder {
-  my ($self) = @_;
-
-  SL::DB::ProjectType->reorder_list(@{ $::form->{project_type_id} || [] });
-
-  $self->render(\'', { type => 'json' });
-}
-
-#
-# filters
-#
-
-sub check_auth {
-  $::auth->assert('config');
-}
-
-#
-# helpers
-#
-
-sub create_or_update {
-  my $self   = shift;
-  my $is_new = !$self->{project_type}->id;
-  my $params = delete($::form->{project_type}) || { };
-
-  $self->{project_type}->assign_attributes(%{ $params });
-
-  my @errors = $self->{project_type}->validate;
-
-  if (@errors) {
-    flash('error', @errors);
-    $self->render('project_type/form', title => $is_new ? $::locale->text('Create a new project type') : $::locale->text('Edit project type'));
-    return;
-  }
-
-  $self->{project_type}->save;
-
-  flash_later('info', $is_new ? $::locale->text('The project type has been created.') : $::locale->text('The project type has been saved.'));
-  $self->redirect_to(action => 'list');
-}
-
-sub load_project_type {
-  my ($self) = @_;
-  $self->{project_type} = SL::DB::ProjectType->new(id => $::form->{id})->load;
-}
-
-1;
index 1236c44..9cf25a3 100644 (file)
@@ -29,17 +29,22 @@ __PACKAGE__->run_before('_bank_account');
 sub action_search {
   my ($self) = @_;
 
+  $self->setup_search_action_bar;
   $self->render('reconciliation/search');
 }
 
 sub action_reconciliation {
   my ($self) = @_;
 
+  $self->_get_proposals;
+
   $self->_get_linked_transactions;
 
   $self->_get_balances;
 
+  $self->setup_reconciliation_action_bar;
   $self->render('reconciliation/form',
+                ui_tab => scalar(@{$self->{PROPOSALS}}) > 0?1:0,
                 title => t8('Reconciliation'));
 }
 
@@ -83,7 +88,7 @@ sub action_update_reconciliation_table {
   my $output = $self->render('reconciliation/assigning_table', { output => 0 },
                  bt_sum => $::form->format_amount(\%::myconfig, $self->{bt_sum}, 2),
                  bb_sum => $::form->format_amount(\%::myconfig, -1 * $self->{bb_sum}, 2),
-                 show_button => !@errors
+                 errors => @errors,
                  );
 
   my %result = ( html => $output );
@@ -174,26 +179,28 @@ sub action_reconcile_proposals {
 
   my $counter = 0;
 
-  foreach my $bt_id ( @{ $::form->{bt_ids} }) {
-    my $rec_group = SL::DB::Manager::ReconciliationLink->get_new_rec_group();
-    my $bank_transaction = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
-    $bank_transaction->cleared('1');
-    if ( $bank_transaction->isa('SL::DB::BankTransaction') ) {
-      $bank_transaction->invoice_amount($bank_transaction->amount);
-    }
-    $bank_transaction->save;
-    foreach my $acc_trans_id (@{ $::form->{proposal_list}->{$bt_id}->{BB} }) {
-      SL::DB::ReconciliationLink->new(
-        rec_group => $rec_group,
-        bank_transaction_id => $bt_id,
-        acc_trans_id => $acc_trans_id
-      )->save;
-      my $acc_trans = SL::DB::Manager::AccTransaction->find_by(acc_trans_id => $acc_trans_id);
-      $acc_trans->cleared('1');
-      $acc_trans->save;
+  # reconcile transaction safe
+  SL::DB->client->with_transaction(sub {
+    foreach my $bt_id ( @{ $::form->{bt_ids} }) {
+      my $rec_group = SL::DB::Manager::ReconciliationLink->get_new_rec_group();
+      my $bank_transaction = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
+      $bank_transaction->cleared('1');
+      $bank_transaction->save;
+      foreach my $acc_trans_id (@{ $::form->{proposal_list}->{$bt_id}->{BB} }) {
+        SL::DB::ReconciliationLink->new(
+          rec_group => $rec_group,
+          bank_transaction_id => $bt_id,
+          acc_trans_id => $acc_trans_id
+        )->save;
+        my $acc_trans = SL::DB::Manager::AccTransaction->find_by(acc_trans_id => $acc_trans_id);
+        $acc_trans->cleared('1');
+        $acc_trans->save;
+      }
+      $counter++;
     }
-    $counter++;
-  }
+
+  1;
+  }) or die t8('Unable to reconcile, database transaction failure');
 
   flash('ok', t8('#1 proposal(s) saved.', $counter));
 
@@ -339,7 +346,7 @@ sub _get_elements_and_validate {
   }
 
   if ($::form->round_amount($bt_sum + $bb_sum, 2) != 0) {
-    push @errors, t8('Out of balance!');
+    push @errors, t8('Out of balance!'), t8('Sum of bank #1 and sum of bookings #2',$bt_sum, $bb_sum);
   }
 
   $self->{ELEMENTS} = \@elements;
@@ -352,33 +359,38 @@ sub _get_elements_and_validate {
 sub _reconcile {
   my ($self) = @_;
 
-  # 1. step: set AccTrans and BankTransactions to 'cleared'
-  foreach my $element (@{ $self->{ELEMENTS} }) {
-    $element->cleared('1');
-    $element->invoice_amount($element->amount) if $element->isa('SL::DB::BankTransaction');
-    $element->save;
-  }
+  # reconcile transaction safe
+  SL::DB->client->with_transaction(sub {
 
-  # 2. step: insert entry in reconciliation_links
-  my $rec_group = SL::DB::Manager::ReconciliationLink->get_new_rec_group();
-  #There is either a 1:n relation or a n:1 relation
-  if (scalar @{ $::form->{bt_ids} } == 1) {
-    my $bt_id = @{ $::form->{bt_ids} }[0];
-    foreach my $bb_id (@{ $::form->{bb_ids} }) {
-      my $rec_link = SL::DB::ReconciliationLink->new(bank_transaction_id => $bt_id,
-                                                     acc_trans_id        => $bb_id,
-                                                     rec_group           => $rec_group);
-      $rec_link->save;
+    # 1. step: set AccTrans and BankTransactions to 'cleared'
+    foreach my $element (@{ $self->{ELEMENTS} }) {
+      $element->cleared('1');
+      # veto either invoice_amount is fully assigned or not! No state tricks in later workflow!
+      $element->save;
     }
-  } else {
-    my $bb_id = @{ $::form->{bb_ids} }[0];
-    foreach my $bt_id (@{ $::form->{bt_ids} }) {
-      my $rec_link = SL::DB::ReconciliationLink->new(bank_transaction_id => $bt_id,
-                                                     acc_trans_id        => $bb_id,
-                                                     rec_group           => $rec_group);
-      $rec_link->save;
+    # 2. step: insert entry in reconciliation_links
+    my $rec_group = SL::DB::Manager::ReconciliationLink->get_new_rec_group();
+    #There is either a 1:n relation or a n:1 relation
+    if (scalar @{ $::form->{bt_ids} } == 1) {
+      my $bt_id = @{ $::form->{bt_ids} }[0];
+      foreach my $bb_id (@{ $::form->{bb_ids} }) {
+        my $rec_link = SL::DB::ReconciliationLink->new(bank_transaction_id => $bt_id,
+                                                       acc_trans_id        => $bb_id,
+                                                       rec_group           => $rec_group);
+        $rec_link->save;
+      }
+    } else {
+      my $bb_id = @{ $::form->{bb_ids} }[0];
+      foreach my $bt_id (@{ $::form->{bt_ids} }) {
+        my $rec_link = SL::DB::ReconciliationLink->new(bank_transaction_id => $bt_id,
+                                                       acc_trans_id        => $bb_id,
+                                                       rec_group           => $rec_group);
+        $rec_link->save;
+      }
     }
-  }
+
+  1;
+  }) or die t8('Unable to reconcile, database transaction failure');
 }
 
 sub _filter_to_where {
@@ -617,4 +629,32 @@ sub init_BANK_ACCOUNTS {
   SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
 }
 
+sub setup_search_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Show'),
+        submit    => [ '#search_form', { action => 'Reconciliation/reconciliation' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_reconciliation_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Filter'),
+        call      => [ 'filter_table' ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
 1;
index 08a95bd..49661cb 100644 (file)
@@ -10,37 +10,52 @@ use SL::DB::Helper::Mappings;
 use SL::DB::Order;
 use SL::DB::DeliveryOrder;
 use SL::DB::Invoice;
+use SL::DB::Letter;
 use SL::DB::PurchaseInvoice;
 use SL::DB::RecordLink;
 use SL::DB::RequirementSpec;
+use SL::DBUtils qw(like);
+use SL::DB::ShopOrder;
 use SL::JSON;
 use SL::Locale::String;
+use SL::Presenter::Record qw(grouped_record_list);
 
 use Rose::Object::MakeMethods::Generic
 (
   scalar => [ qw(object object_model object_id link_type link_direction link_type_desc) ],
 );
 
+__PACKAGE__->run_before('check_auth');
 __PACKAGE__->run_before('check_object_params', only => [ qw(ajax_list ajax_delete ajax_add_select_type ajax_add_filter ajax_add_list ajax_add_do) ]);
 __PACKAGE__->run_before('check_link_params',   only => [ qw(                                                           ajax_add_list ajax_add_do) ]);
 
 my %link_type_defaults = (
-  filter      => 'type_filter',
-  project     => 'globalproject',
-  description => 'transaction_description',
-  date        => 'transdate',
+  filter            => 'type_filter',
+  project           => 'globalproject',
+  description       => 'transaction_description',
+  description_title => t8('Transaction description'),
+  date              => 'transdate',
 );
 
 my @link_type_specifics = (
   { title => t8('Requirement spec'),        type => 'requirement_spec',        model => 'RequirementSpec', number => 'id', project => 'project', description => 'title', date => undef, filter => 'working_copy_filter', },
+  { title => t8('Shop Order'),              type => 'shop_order',              model => 'ShopOrder',       number => 'shop_ordernumber', date => 'order_date', project => undef, },
   { title => t8('Sales quotation'),         type => 'sales_quotation',         model => 'Order',           number => 'quonumber', },
   { title => t8('Sales Order'),             type => 'sales_order',             model => 'Order',           number => 'ordnumber', },
   { title => t8('Sales delivery order'),    type => 'sales_delivery_order',    model => 'DeliveryOrder',   number => 'donumber',  },
+  { title => t8('RMA delivery order'),      type => 'rma_delivery_order',      model => 'DeliveryOrder',   number => 'rdonumber', },
   { title => t8('Sales Invoice'),           type => 'invoice',                 model => 'Invoice',         number => 'invnumber', },
   { title => t8('Request for Quotation'),   type => 'request_quotation',       model => 'Order',           number => 'quonumber', },
   { title => t8('Purchase Order'),          type => 'purchase_order',          model => 'Order',           number => 'ordnumber', },
   { title => t8('Purchase delivery order'), type => 'purchase_delivery_order', model => 'DeliveryOrder',   number => 'donumber',  },
+  { title => t8('Supplier delivery order'), type => 'supplier_delivery_order', model => 'DeliveryOrder',   number => 'sdonumber', },
   { title => t8('Purchase Invoice'),        type => 'purchase_invoice',        model => 'PurchaseInvoice', number => 'invnumber', },
+  { title => t8('Letter'),                  type => 'letter',                  model => 'Letter',          number => 'letternumber', description => 'subject', description_title => t8('Subject'), date => 'date', project => undef },
+  { title => t8('Email'),                   type => 'email_journal',           model => 'EmailJournal',    number => 'id', description => 'subject', description_title => t8('Subject'), },
+  { title => t8('AR Transaction'),          type => 'ar_transaction',          model => 'Invoice',         number => 'invnumber', },
+  { title => t8('AP Transaction'),          type => 'ap_transaction',          model => 'PurchaseInvoice', number => 'invnumber', },
+  { title => t8('Dunning'),                 type => 'dunning',                 model => 'Dunning',         number => 'dunning_id', },
+  { title => t8('GL Transaction'),          type => 'gl_transaction',          model => 'GLTransaction',   number => 'reference', },
 );
 
 my @link_types = map { +{ %link_type_defaults, %{ $_ } } } @link_type_specifics;
@@ -53,9 +68,13 @@ sub action_ajax_list {
   my ($self) = @_;
 
   eval {
-    my $linked_records = $self->object->linked_records(direction => 'both', recursive => 1, save_path => 1);
+    my $linked_records = ($::instance_conf->get_always_record_links_from_order && ref $self->object ne 'SL::DB::Order')
+                       ?  $self->get_order_centric_linked_records
+                       :  $self->object->linked_records(direction => 'both', recursive => 1, save_path => 1);
+
     push @{ $linked_records }, $self->object->sepa_export_items if $self->object->can('sepa_export_items');
-    my $output         = SL::Presenter->get->grouped_record_list(
+
+    my $output         = grouped_record_list(
       $linked_records,
       with_columns      => [ qw(record_link_direction) ],
       edit_record_links => 1,
@@ -97,7 +116,7 @@ sub action_ajax_add_filter {
   my $presenter = $self->presenter;
 
   my @link_type_select = map { [ $_->{type}, $_->{title} ] } @link_types;
-  my @projects         = map { [ $_->id, $presenter->project($_, display => 'inline', style => 'both', no_link => 1) ] } @{ SL::DB::Manager::Project->get_all_sorted };
+  my @projects         = map { [ $_->id, $_->presenter->project(display => 'inline', style => 'both', no_link => 1) ] } @{ SL::DB::Manager::Project->get_all_sorted };
   my $is_sales         = $self->object->can('customer_id') && $self->object->customer_id;
 
   $self->render(
@@ -114,18 +133,34 @@ sub action_ajax_add_list {
   my ($self) = @_;
 
   my $manager     = 'SL::DB::Manager::' . $self->link_type_desc->{model};
-  my $vc          = $self->link_type =~ m/sales_|^invoice|requirement_spec$/ ? 'customer' : 'vendor';
+  my $vc          = $self->link_type =~ m/shop|sales_|^invoice|requirement_spec|letter|^ar_/ ? 'customer' : 'vendor';
   my $project     = $self->link_type_desc->{project};
+  my $project_id  = "${project}_id";
   my $description = $self->link_type_desc->{description};
   my $filter      = $self->link_type_desc->{filter};
+  my $number      = $self->link_type_desc->{number};
+
+  my @where = $filter && $manager->can($filter) ? $manager->$filter($self->link_type) : ();
+  push @where, ("${vc}.${vc}number"     => { ilike => like($::form->{vc_number}) })               if $::form->{vc_number};
+  push @where, ("${vc}.name"            => { ilike => like($::form->{vc_name}) })                 if $::form->{vc_name};
+  push @where, ($description            => { ilike => like($::form->{transaction_description}) }) if $::form->{transaction_description};
+  push @where, ($project_id             => $::form->{globalproject_id})                           if $::form->{globalproject_id} && $manager->can($project_id);
+
+  if ($::form->{number}) {
+    my $class    = 'SL::DB::' . $self->link_type_desc->{model};
+    my $col_type = ref $class->meta->column($number);
+    if ($col_type =~ /^Rose::DB::Object::Metadata::Column::(?:Integer|Serial)$/) {
+      push @where, ($number => $::form->{number});
+    } elsif ($col_type =~ /^Rose::DB::Object::Metadata::Column::Text$/) {
+      push @where, ($number => { ilike => like($::form->{number}) });
+    }
+  }
 
-  my @where = $filter ? $manager->$filter($self->link_type) : ();
-  push @where, ("${vc}.${vc}number"     => { ilike => '%' . $::form->{vc_number} . '%' })               if $::form->{vc_number};
-  push @where, ("${vc}.name"            => { ilike => '%' . $::form->{vc_name}   . '%' })               if $::form->{vc_name};
-  push @where, ($description            => { ilike => '%' . $::form->{transaction_description} . '%' }) if $::form->{transaction_description};
-  push @where, ("${project}_id"         => $::form->{globalproject_id})                                 if $::form->{globalproject_id};
+  my @with_objects = ($vc);
+  push @with_objects, $project if $manager->can($project_id);
 
-  my $objects = $manager->get_all_sorted(where => \@where, with_objects => [ $vc, $project ]);
+  # show the newest records first (should be better for 80% of the cases TODO sortable click
+  my $objects = $manager->get_all_sorted(where => \@where, with_objects => \@with_objects, sort_by => 'itime',  sort_dir => 'ASC');
   my $output  = $self->render(
     'record_links/add_list',
     { output => 0 },
@@ -133,6 +168,7 @@ sub action_ajax_add_list {
     vc                 => $vc,
     number_column      => $self->link_type_desc->{number},
     description_column => $description,
+    description_title  => $self->link_type_desc->{description_title},
     project_column     => $project,
     date_column        => $self->link_type_desc->{date},
   );
@@ -208,4 +244,28 @@ sub check_link_params {
   return 1;
 }
 
+sub check_auth {
+  $::auth->assert('record_links');
+}
+
+# internal
+
+sub get_order_centric_linked_records {
+  my ($self) = @_;
+
+  my $all_linked_records = $self->object->linked_records(direction => 'from', recursive => 1);
+  my $filtered_orders = [ grep { 'SL::DB::Order' eq ref $_ && $_->is_type('sales_order') } @$all_linked_records ];
+
+  # no orders no call to linked_records via batch mode
+  # but instead return default list
+  return $self->object->linked_records(direction => 'both', recursive => 1, save_path => 1)
+    unless scalar @$filtered_orders;
+
+  # we have a order, therefore get the tree view from the top (order)
+  my $id_ref = [ map { $_->id } @$filtered_orders ];
+  my $linked_records = SL::DB::Order->new->linked_records(direction => 'to', recursive => 1, batch => $id_ref);
+  push @{ $linked_records }, @$filtered_orders;
+
+  return $linked_records;
+}
 1;
diff --git a/SL/Controller/RecordTemplate.pm b/SL/Controller/RecordTemplate.pm
new file mode 100644 (file)
index 0000000..9ce8e15
--- /dev/null
@@ -0,0 +1,128 @@
+package SL::Controller::RecordTemplate;
+
+use strict;
+
+use base qw(SL::Controller::Base);
+
+use SL::Helper::Flash qw(flash);
+use SL::Locale::String qw(t8);
+use SL::DB::RecordTemplate;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(template_type template templates data) ],
+);
+
+__PACKAGE__->run_before('check_auth');
+
+my %modules = (
+  ar_transaction => {
+    controller    => 'ar.pl',
+    load_action   => 'load_record_template',
+    save_action   => 'save_record_template',
+    form_selector => '#form',
+  },
+
+  ap_transaction => {
+    controller    => 'ap.pl',
+    load_action   => 'load_record_template',
+    save_action   => 'save_record_template',
+    form_selector => '#form',
+  },
+
+  gl_transaction => {
+    controller    => 'gl.pl',
+    load_action   => 'load_record_template',
+    save_action   => 'save_record_template',
+    form_selector => '#form',
+  },
+);
+
+#
+# actions
+#
+
+sub action_show_dialog {
+  my ($self) = @_;
+  $self
+    ->js
+    ->dialog->open({
+      html   => $self->dialog_html,
+      id     => 'record_template_dialog',
+      dialog => {
+        title => t8('Record templates'),
+      },
+    })
+    ->focus("#template_filter")
+    ->render;
+}
+
+sub action_rename {
+  my ($self) = @_;
+
+  $self->template_type($self->template->template_type);
+  $self->template->update_attributes(template_name => $::form->{template_name});
+
+  $self
+    ->js
+    ->html('#record_template_dialog', $self->dialog_html)
+    ->focus("#record_template_dialog_new_template_name")
+    ->reinit_widgets
+    ->render;
+}
+
+sub action_delete {
+  my ($self) = @_;
+
+  $self->template_type($self->template->template_type);
+  $self->template->delete;
+
+  $self
+    ->js
+    ->html('#record_template_dialog', $self->dialog_html)
+    ->focus("#record_template_dialog_new_template_name")
+    ->reinit_widgets
+    ->render;
+}
+
+sub action_filter_templates {
+  my ($self) = @_;
+
+  $self->{template_filter} = $::form->{template_filter};
+
+  $self
+    ->js
+    ->html('#record_template_dialog', $self->dialog_html)
+    ->focus("#record_template_dialog_new_template_name")
+    ->reinit_widgets
+    ->focus("#template_filter")
+    ->render();
+}
+
+#
+# helpers
+#
+
+sub check_auth {
+  $::auth->assert('ap_transactions | ar_transactions | gl_transactions');
+}
+
+sub init_template_type { $::form->{template_type} or die 'need template_type'   }
+sub init_data          { $modules{ $_[0]->template_type }                       }
+sub init_template      { SL::DB::RecordTemplate->new(id => $::form->{id})->load }
+
+sub init_templates {
+  my ($self) = @_;
+  return scalar SL::DB::Manager::RecordTemplate->get_all_sorted(
+    where => [ template_type => $self->template_type,
+              (template_name => { ilike => '%' . $::form->{template_filter} . '%' })x!! ($::form->{template_filter})
+             ],
+  );
+}
+
+sub dialog_html {
+  my ($self) = @_;
+
+  return $self->render('record_template/dialog', { layout => 0, output => 0 });
+}
+
+1;
index 38bb446..afbe2bb 100644 (file)
@@ -23,6 +23,7 @@ use SL::DB::RequirementSpec;
 use SL::Helper::CreatePDF qw();
 use SL::Helper::Flash;
 use SL::Locale::String;
+use SL::System::Process;
 use SL::Template::LaTeX;
 
 use Rose::Object::MakeMethods::Generic
@@ -53,6 +54,7 @@ my %sort_columns = (
 sub action_list {
   my ($self) = @_;
 
+  $self->_setup_search_action_bar;
   $self->prepare_report;
   $self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
 }
@@ -66,6 +68,8 @@ sub action_new {
     $self->requirement_spec->$_($self->copy_source->$_) for qw(type_id status_id customer_id title hourly_rate is_template)
   }
 
+  $self->_setup_form_action_bar;
+
   $self->render('requirement_spec/new', title => $self->requirement_spec->is_template ? t8('Create a new requirement spec template') : t8('Create a new requirement spec'));
 }
 
@@ -83,6 +87,7 @@ sub action_ajax_edit {
   $self->js
     ->hide('#basic_settings')
     ->after('#basic_settings', $html)
+    ->reinit_widgets
     ->render;
 }
 
@@ -121,7 +126,7 @@ sub action_ajax_edit_time_and_cost_estimate {
 sub action_ajax_save_time_and_cost_estimate {
   my ($self) = @_;
 
-  $self->requirement_spec->db->do_transaction(sub {
+  $self->requirement_spec->db->with_transaction(sub {
     # Make Emacs happy
     1;
     foreach my $attributes (@{ $::form->{requirement_spec_items} || [] }) {
@@ -214,9 +219,16 @@ sub action_revert_to {
 sub action_create_pdf {
   my ($self, %params) = @_;
 
+  my $keep_temp_files = $::lx_office_conf{debug} && $::lx_office_conf{debug}->{keep_temp_files};
+  my $temp_dir        = File::Temp->newdir(
+    "kivitendo-print-XXXXXX",
+    DIR     => SL::System::Process::exe_dir() . "/" . $::lx_office_conf{paths}->{userspath},
+    CLEANUP => !$keep_temp_files,
+  );
+
   my $base_name       = $self->requirement_spec->type->template_file_name || 'requirement_spec';
-  my @pictures        = $self->prepare_pictures_for_printing;
-  my %result          = SL::Template::LaTeX->parse_and_create_pdf("${base_name}.tex", SELF => $self, rspec => $self->requirement_spec);
+  my @pictures        = $self->prepare_pictures_for_printing($temp_dir->dirname);
+  my %result          = SL::Template::LaTeX->parse_and_create_pdf("${base_name}.tex", SELF => $self, rspec => $self->requirement_spec, userspath => $temp_dir->dirname);
 
   unlink @pictures unless ($::lx_office_conf{debug} || {})->{keep_temp_files};
 
@@ -322,7 +334,8 @@ sub setup {
 
   $::auth->assert('requirement_spec_edit');
   $::request->{layout}->use_stylesheet("${_}.css") for qw(jquery.contextMenu requirement_spec);
-  $::request->{layout}->use_javascript("${_}.js")  for qw(jquery.jstree jquery/jquery.contextMenu jquery/jquery.hotkeys requirement_spec ckeditor/ckeditor ckeditor/adapters/jquery autocomplete_part autocomplete_customer);
+  $::request->{layout}->use_javascript("${_}.js")  for qw(jquery.jstree jquery/jquery.contextMenu jquery/jquery.hotkeys requirement_spec ckeditor/ckeditor ckeditor/adapters/jquery kivi.Part kivi.CustomerVendor
+                                                          ckeditor/ckeditor ckeditor/adapters/jquery);
   $self->init_visible_section;
 
   return 1;
@@ -371,7 +384,7 @@ sub init_includeable_cvar_configs {
 
 sub init_include_cvars {
   my ($self) = @_;
-  return $::form->{include_cvars} if $::form->{include_cvars} && (ref($::form->{include_cvars}) eq 'HASH');
+  return { map { ($_->name => $::form->{"include_cvars_" . $_->name}) }       @{ $self->cvar_configs } } if $::form->{_include_cvars_from_form};
   return { map { ($_->name => ($_->includeable && $_->included_by_default)) } @{ $self->cvar_configs } };
 }
 
@@ -380,10 +393,11 @@ sub init_include_cvars {
 #
 
 sub create_or_update {
-  my $self   = shift;
-  my $is_new = !$self->requirement_spec->id;
-  my $params = delete($::form->{requirement_spec}) || { };
-  my $cvars  = delete($::form->{cvars})            || { };
+  my $self                 = shift;
+  my $is_new               = !$self->requirement_spec->id;
+  my $previous_customer_id = $self->requirement_spec->customer_id;
+  my $params               = delete($::form->{requirement_spec}) || { };
+  my $cvars                = delete($::form->{cvars})            || { };
 
   # Forcefully make it clear to Rose which custom_variables exist (or don't), so that the ones added with »add_custom_variables« are visible when calling »custom_variables«.
   if ($is_new) {
@@ -417,12 +431,25 @@ sub create_or_update {
   }
 
   my $db = $self->requirement_spec->db;
-  if (!$db->do_transaction(sub {
+  if (!$db->with_transaction(sub {
     if ($self->copy_source) {
       $self->requirement_spec($self->copy_source->create_copy(%{ $params }));
     } else {
       $self->requirement_spec->save(cascade => 1);
+
+      # If the current requirement spec has versions and the
+      # customer's been changed, then the customer of all the versions
+      # has to be changed, too.
+      if (   !$is_new
+          && !$self->requirement_spec->is_template
+          && ($previous_customer_id != $self->requirement_spec->customer_id)) {
+        SL::DB::Manager::RequirementSpec->update_all(
+          set   => { customer_id     => $self->requirement_spec->customer_id },
+          where => [ working_copy_id => $self->requirement_spec->id          ],
+        );
+      }
     }
+    1;
   })) {
     $::lxdebug->message(LXDebug::WARN(), "Error: " . $db->error);
     @errors = ($::locale->text('Saving failed. Error message from the database: #1', $db->error));
@@ -473,9 +500,9 @@ sub prepare_report {
   if (!$is_template) {
     %column_defs = (
       %column_defs,
-      customer      => { raw_data => sub { $self->presenter->customer($_[0]->customer, display => 'table-cell', callback => $callback) },
+      customer      => { raw_data => sub { $_[0]->customer->presenter->customer(display => 'table-cell', callback => $callback) },
                          sub      => sub { $_[0]->customer->name } },
-      projectnumber => { raw_data => sub { $self->presenter->project($_[0]->project, display => 'table-cell', callback => $callback) },
+      projectnumber => { raw_data => sub { $_[0]->project ? $_[0]->project->presenter->project(display => 'table-cell', callback => $callback) : '' },
                          sub      => sub { $_[0]->project_id ? $_[0]->project->projectnumber : '' } },
       status        => { sub      => sub { $_[0]->status->description } },
       type          => { sub      => sub { $_[0]->type->description } },
@@ -499,6 +526,8 @@ sub prepare_report {
     %column_defs = (%column_defs, %cvar_column_defs);
   }
 
+  my @cvar_column_form_names = ('_include_cvars_from_form', map { "include_cvars_" . $_->name } @{ $self->includeable_cvar_configs });
+
   $report->set_options(
     std_column_visibility => 1,
     controller_class      => 'RequirementSpec',
@@ -511,7 +540,7 @@ sub prepare_report {
   );
   $report->set_columns(%column_defs);
   $report->set_column_order(@columns);
-  $report->set_export_options(qw(list filter));
+  $report->set_export_options(qw(list filter), @cvar_column_form_names);
   $report->set_options_from_form;
   $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
 }
@@ -538,7 +567,7 @@ sub render_pasted_text_block {
       ->hide('#text-block-list-empty');
   }
 
-  my $node       = $self->presenter->requirement_spec_text_block_jstree_data($text_block);
+  my $node       = $text_block->presenter->jstree_data;
   my $front_back = $text_block->output_position == 0 ? 'front' : 'back';
   $self->js
     ->jstree->create_node('#tree', "#tb-${front_back}", 'last', $node)
@@ -560,7 +589,7 @@ sub set_default_filter_args {
 sub render_pasted_section {
   my ($self, $item, $parent_id) = @_;
 
-  my $node = $self->presenter->requirement_spec_item_jstree_data($item);
+  my $node = $item->presenter->jstree_data;
   $self->js
     ->jstree->create_node('#tree', $parent_id ? "#fb-${parent_id}" : '#sections', 'last', $node)
     ->jstree->open_node(  '#tree', $parent_id ? "#fb-${parent_id}" : '#sections');
@@ -580,11 +609,11 @@ sub render_first_pasted_section_as_list {
 }
 
 sub prepare_pictures_for_printing {
-  my ($self) = @_;
+  my ($self, $userspath) = @_;
 
   my @files;
-  my $userspath = File::Spec->rel2abs($::lx_office_conf{paths}->{userspath});
-  my $target    =  "${userspath}/kivitendo-print-requirement-spec-picture-" . Common::unique_id() . '-';
+  $userspath ||= SL::System::Process::exe_dir() . "/" . $::lx_office_conf{paths}->{userspath};
+  my $target   = "${userspath}/kivitendo-print-requirement-spec-picture-" . Common::unique_id() . '-';
 
   foreach my $picture (map { @{ $_->pictures } } @{ $self->requirement_spec->text_blocks }) {
     my $output_file_name        = $target . $picture->id . '.' . $picture->get_default_file_name_extension;
@@ -635,7 +664,7 @@ sub update_project_link_create {
   return $self->js->error(@errors)->render if @errors;
 
   my $db = $self->requirement_spec->db;
-  if (!$db->do_transaction(sub {
+  if (!$db->with_transaction(sub {
     $project->save;
     $self->requirement_spec->update_attributes(project_id => $project->id);
 
@@ -681,4 +710,41 @@ sub init_html_template {
   return !!$template;
 }
 
+sub _setup_form_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#basic_settings_form', { action => 'RequirementSpec/' . ($self->requirement_spec->id ? 'update' : 'create') } ],
+        accesskey => 'enter',
+      ],
+
+      link => [
+        t8('Abort'),
+        link => $self->url_for(action => 'list', is_template => $self->requirement_spec->is_template),
+      ],
+    );
+  }
+}
+
+sub _setup_search_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#search_form', { action => 'RequirementSpec/list' } ],
+        accesskey => 'enter',
+      ],
+      link => [
+        t8('Add'),
+        link => $self->url_for(action => 'new', is_template => $::form->{is_template}),
+      ],
+    );
+  }
+}
+
 1;
diff --git a/SL/Controller/RequirementSpecAcceptanceStatus.pm b/SL/Controller/RequirementSpecAcceptanceStatus.pm
deleted file mode 100644 (file)
index f9fcad8..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-package SL::Controller::RequirementSpecAcceptanceStatus;
-
-use strict;
-
-use parent qw(SL::Controller::Base);
-
-use SL::DB::RequirementSpecAcceptanceStatus;
-use SL::DB::Language;
-use SL::Helper::Flash;
-use SL::Locale::String;
-
-use Rose::Object::MakeMethods::Generic
-(
- scalar => [ qw(requirement_spec_acceptance_status valid_names) ],
-);
-
-__PACKAGE__->run_before('check_auth');
-__PACKAGE__->run_before('load_requirement_spec_acceptance_status', only => [ qw(edit update destroy) ]);
-__PACKAGE__->run_before(sub { $_[0]->valid_names(\@SL::DB::RequirementSpecAcceptanceStatus::valid_names) });
-
-#
-# actions
-#
-
-sub action_list {
-  my ($self) = @_;
-
-  $self->render('requirement_spec_acceptance_status/list',
-                title                                => t8('Acceptance Statuses'),
-                REQUIREMENT_SPEC_ACCEPTANCE_STATUSES => SL::DB::Manager::RequirementSpecAcceptanceStatus->get_all_sorted);
-}
-
-sub action_new {
-  my ($self) = @_;
-
-  $self->{requirement_spec_acceptance_status} = SL::DB::RequirementSpecAcceptanceStatus->new;
-  $self->render('requirement_spec_acceptance_status/form', title => t8('Create a new acceptance status'));
-}
-
-sub action_edit {
-  my ($self) = @_;
-  $self->render('requirement_spec_acceptance_status/form', title => t8('Edit acceptance status'));
-}
-
-sub action_create {
-  my ($self) = @_;
-
-  $self->{requirement_spec_acceptance_status} = SL::DB::RequirementSpecAcceptanceStatus->new;
-  $self->create_or_update;
-}
-
-sub action_update {
-  my ($self) = @_;
-  $self->create_or_update;
-}
-
-sub action_destroy {
-  my ($self) = @_;
-
-  if (eval { $self->{requirement_spec_acceptance_status}->delete; 1; }) {
-    flash_later('info',  t8('The acceptance status has been deleted.'));
-  } else {
-    flash_later('error', t8('The acceptance status is in use and cannot be deleted.'));
-  }
-
-  $self->redirect_to(action => 'list');
-}
-
-sub action_reorder {
-  my ($self) = @_;
-
-  SL::DB::RequirementSpecAcceptanceStatus->reorder_list(@{ $::form->{requirement_spec_acceptance_status_id} || [] });
-
-  $self->render(\'', { type => 'json' });
-}
-
-#
-# filters
-#
-
-sub check_auth {
-  $::auth->assert('config');
-}
-
-#
-# helpers
-#
-
-sub create_or_update {
-  my $self   = shift;
-  my $is_new = !$self->{requirement_spec_acceptance_status}->id;
-  my $params = delete($::form->{requirement_spec_acceptance_status}) || { };
-  my $title  = $is_new ? t8('Create a new acceptance status') : t8('Edit acceptance status');
-
-  $self->{requirement_spec_acceptance_status}->assign_attributes(%{ $params });
-
-  my @errors = $self->{requirement_spec_acceptance_status}->validate;
-
-  if (@errors) {
-    flash('error', @errors);
-    $self->render('requirement_spec_acceptance_status/form', title => $title);
-    return;
-  }
-
-  $self->{requirement_spec_acceptance_status}->save;
-
-  flash_later('info', $is_new ? t8('The acceptance status has been created.') : t8('The acceptance status has been saved.'));
-  $self->redirect_to(action => 'list');
-}
-
-sub load_requirement_spec_acceptance_status {
-  my ($self) = @_;
-  $self->{requirement_spec_acceptance_status} = SL::DB::RequirementSpecAcceptanceStatus->new(id => $::form->{id})->load;
-}
-
-1;
diff --git a/SL/Controller/RequirementSpecComplexity.pm b/SL/Controller/RequirementSpecComplexity.pm
deleted file mode 100644 (file)
index 392a3c3..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-package SL::Controller::RequirementSpecComplexity;
-
-use strict;
-
-use parent qw(SL::Controller::Base);
-
-use SL::DB::RequirementSpecComplexity;
-use SL::Helper::Flash;
-use SL::Locale::String;
-
-use Rose::Object::MakeMethods::Generic
-(
- scalar => [ qw(requirement_spec_complexity) ],
-);
-
-__PACKAGE__->run_before('check_auth');
-__PACKAGE__->run_before('load_requirement_spec_complexity', only => [ qw(edit update destroy) ]);
-
-#
-# actions
-#
-
-sub action_list {
-  my ($self) = @_;
-
-  $self->render('requirement_spec_complexity/list',
-                title                         => t8('Complexities'),
-                REQUIREMENT_SPEC_COMPLEXITIES => SL::DB::Manager::RequirementSpecComplexity->get_all_sorted);
-}
-
-sub action_new {
-  my ($self) = @_;
-
-  $self->{requirement_spec_complexity} = SL::DB::RequirementSpecComplexity->new;
-  $self->render('requirement_spec_complexity/form', title => t8('Create a new complexity'));
-}
-
-sub action_edit {
-  my ($self) = @_;
-  $self->render('requirement_spec_complexity/form', title => t8('Edit complexity'));
-}
-
-sub action_create {
-  my ($self) = @_;
-
-  $self->{requirement_spec_complexity} = SL::DB::RequirementSpecComplexity->new;
-  $self->create_or_update;
-}
-
-sub action_update {
-  my ($self) = @_;
-  $self->create_or_update;
-}
-
-sub action_destroy {
-  my ($self) = @_;
-
-  if (eval { $self->{requirement_spec_complexity}->delete; 1; }) {
-    flash_later('info',  t8('The complexity has been deleted.'));
-  } else {
-    flash_later('error', t8('The complexity is in use and cannot be deleted.'));
-  }
-
-  $self->redirect_to(action => 'list');
-}
-
-sub action_reorder {
-  my ($self) = @_;
-
-  SL::DB::RequirementSpecComplexity->reorder_list(@{ $::form->{requirement_spec_complexity_id} || [] });
-
-  $self->render(\'', { type => 'json' });
-}
-
-#
-# filters
-#
-
-sub check_auth {
-  $::auth->assert('config');
-}
-
-#
-# helpers
-#
-
-sub create_or_update {
-  my $self   = shift;
-  my $is_new = !$self->{requirement_spec_complexity}->id;
-  my $params = delete($::form->{requirement_spec_complexity}) || { };
-  my $title  = $is_new ? t8('Create a new complexity') : t8('Edit complexity');
-
-  $self->{requirement_spec_complexity}->assign_attributes(%{ $params });
-
-  my @errors = $self->{requirement_spec_complexity}->validate;
-
-  if (@errors) {
-    flash('error', @errors);
-    $self->render('requirement_spec_complexity/form', title => $title);
-    return;
-  }
-
-  $self->{requirement_spec_complexity}->save;
-
-  flash_later('info', $is_new ? t8('The complexity has been created.') : t8('The complexity has been saved.'));
-  $self->redirect_to(action => 'list');
-}
-
-sub load_requirement_spec_complexity {
-  my ($self) = @_;
-  $self->{requirement_spec_complexity} = SL::DB::RequirementSpecComplexity->new(id => $::form->{id})->load;
-}
-
-1;
index c6026ce..1c714ff 100644 (file)
@@ -19,6 +19,7 @@ use SL::DB::RequirementSpecRisk;
 use SL::Helper::Flash;
 use SL::JSON;
 use SL::Locale::String;
+use SL::Presenter::Text qw(truncate);
 
 use Rose::Object::MakeMethods::Generic
 (
@@ -39,7 +40,7 @@ sub action_ajax_list {
 
   if (!$::form->{clicked_id}) {
     # Clicked on "sections" in the tree. Do nothing.
-    return $self->render;
+    return $self->render($self->js);
   }
 
   my $clicked_item = SL::DB::RequirementSpecItem->new(id => $::form->{clicked_id})->load;
@@ -87,21 +88,21 @@ sub action_dragged_and_dropped {
   my $old_type            = $self->item->item_type;
   my $new_type            = !$dropped_item ? 'section' : $position =~ m/before|after/ ? $dropped_item->item_type : $dropped_item->child_type;
 
-  $self->item->db->do_transaction(sub {
+  $self->item->db->with_transaction(sub {
     $self->item->remove_from_list;
     $self->item->parent_id($position =~ m/before|after/ ? $dropped_item->parent_id : $dropped_item->id) if $dropped_item;
     $self->item->item_type($new_type);
     $self->item->add_to_list(position => $position, reference => $::form->{dropped_id} || undef);
   });
 
-  $self->item(SL::DB::RequirementSpecItem->new(id => $self->item->id)->load);
-  my $new_section         = $self->item->section;
-  my $new_visible_section = SL::DB::RequirementSpecItem->new(id => $self->visible_item->id)->load->section;
-
   return $self->invalidate_version->render if !$old_visible_section || ($new_type eq 'section');
 
   # From here on $old_visible_section is definitely set.
 
+  $self->item(SL::DB::RequirementSpecItem->new(id => $self->item->id)->load);
+  my $new_section         = $self->item->section;
+  my $new_visible_section = SL::DB::RequirementSpecItem->new(id => $self->visible_item->id)->load->section;
+
   my $old_parent  = SL::DB::RequirementSpecItem->new(id => $old_parent_id)->load;
   my $old_section = $old_parent->section;
 
@@ -192,7 +193,7 @@ sub action_ajax_create {
   my $type = $self->item->item_type;
 
   if ($type eq 'section') {
-    my $node = $self->presenter->requirement_spec_item_jstree_data($self->item);
+    my $node = $self->item->presenter->jstree_data;
     $self->invalidate_version;
     $self->render_list($self->item)
       ->hide('#section-list-empty')
@@ -205,7 +206,7 @@ sub action_ajax_create {
 
   my $template = 'requirement_spec_item/_' . (apply { s/-/_/g; $_ } $type);
   my $html     = $self->render($template, { output => 0 }, requirement_spec_item => $self->item, id_prefix => $type eq 'function-block' ? '' : 'sub-');
-  my $node     = $self->presenter->requirement_spec_item_jstree_data($self->item);
+  my $node     = $self->item->presenter->jstree_data;
 
   $self->js
     ->replaceWith('#' . $prefix . '_form', $html)
@@ -298,7 +299,7 @@ sub action_ajax_update {
       ->remove('#edit_section_form')
       ->html('#section-header-' . $self->item->id, $html)
       ->show('#section-header-' . $self->item->id)
-      ->jstree->rename_node('#tree', '#fb-' . $self->item->id, $::request->presenter->requirement_spec_item_tree_node_title($self->item))
+      ->jstree->rename_node('#tree', '#fb-' . $self->item->id, $self->item->presenter->tree_node_title)
       ->prop('#fb-' . $self->item->id . ' a', 'title', $self->item->content_excerpt)
       ->addClass('#fb-' . $self->item->id . ' a', 'tooltip')
       ->reinit_widgets
@@ -319,7 +320,7 @@ sub action_ajax_update {
     ->prop('#fb-' . $self->item->id . ' a', 'title', $self->item->content_excerpt)
     ->addClass('#fb-' . $self->item->id . ' a', 'tooltip')
     ->reinit_widgets
-    ->jstree->rename_node('#tree', '#fb-' . $self->item->id, $::request->presenter->requirement_spec_item_tree_node_title($self->item));
+    ->jstree->rename_node('#tree', '#fb-' . $self->item->id, $self->item->presenter->tree_node_title);
 
   $self->replace_bottom($self->item, id_prefix => $id_prefix);
   $self->replace_bottom($self->item->parent) if $type eq 'sub-function-block';
@@ -439,7 +440,7 @@ sub assign_requirement_spec_id_rec {
 sub create_and_insert_node_rec {
   my ($self, $item, $new_parent_id, $insert_after) = @_;
 
-  my $node = $self->presenter->requirement_spec_item_jstree_data($item);
+  my $node = $item->presenter->jstree_data;
   $self->js->jstree->create_node('#tree', $insert_after ? ('#fb-' . $insert_after, 'after') : $new_parent_id ? ('#fb-' . $new_parent_id, 'last') : ('#sections', 'last'), $node);
 
   $self->create_and_insert_node_rec($_, $item->id) for @{ $item->children || [] };
@@ -562,7 +563,7 @@ sub select_node {
 
 sub create_dependency_item {
   my $self = shift;
-  [ $_[0]->id, $self->presenter->truncate(join(' ', grep { $_ } ($_[1], $_[0]->fb_number, $_[0]->description_as_stripped_html))) ];
+  [ $_[0]->id, truncate(join(' ', grep { $_ } ($_[1], $_[0]->fb_number, $_[0]->description_as_stripped_html))) ];
 }
 
 sub create_dependencies {
index f695a51..398c86a 100644 (file)
@@ -318,8 +318,8 @@ sub create_order_item {
     longdescription => $longdescription,
     qty             => $is_time_based ? $section->time_estimation * 1 : 1,
     unit            => $is_time_based ? $self->h_unit_name            : $part->unit,
-    sellprice       => $::form->round_amount($self->requirement_spec->hourly_rate * ($is_time_based ? 1 : $section->time_estimation), 2),
-    lastcost        => $part->lastcost,
+    sellprice       => $::form->round_amount($self->requirement_spec->hourly_rate * ($is_time_based ? 1 : $section->time_estimation * $section->sellprice_factor), 2),
+    lastcost        => $part->lastcost * $section->sellprice_factor,
     discount        => 0,
     project_id      => $self->requirement_spec->project_id,
   );
@@ -365,10 +365,13 @@ sub create_order {
   my @orderitems = map { $self->create_order_item(                section => $_,         language_id => $customer->language_id) } @{ $params{sections} };
   my @add_items  = map { $self->create_additional_part_order_item(additional_part => $_, language_id => $customer->language_id) } @{ $params{additional_parts} };
   my $employee   = SL::DB::Manager::Employee->current;
+  my $reqdate    = !$::form->{quotation} ? undef
+                 : $customer->payment_id ? $customer->payment->calc_date
+                 :                         DateTime->today_local->next_workday(extra_days => $::instance_conf->get_reqdate_interval)->to_kivitendo;
   my $order      = SL::DB::Order->new(
     globalproject_id        => $self->requirement_spec->project_id,
     transdate               => DateTime->today_local,
-    reqdate                 => $::form->{quotation} && $customer->payment_id ? $customer->payment->calc_date : undef,
+    reqdate                 => $reqdate,
     quotation               => !!$::form->{quotation},
     orderitems              => [ @orderitems, @add_items ],
     customer_id             => $customer->id,
index b9df7f5..8b4d10c 100644 (file)
@@ -50,7 +50,7 @@ sub action_ajax_edit {
 sub action_ajax_add {
   my ($self)  = @_;
 
-  my $part      = SL::DB::Part->new(id => $::form->{part_id})->load(with_objects => [ qw(unit_obj) ]);
+  my $part      = SL::DB::Part->new(id => $::form->{part_id})->load(with => [ qw(unit_obj) ]);
   my $rs_part   = SL::DB::RequirementSpecPart->new(
     part        => $part,
     qty         => 1,
@@ -73,7 +73,7 @@ sub action_ajax_save {
   my ($self) = @_;
 
   my $db = $self->requirement_spec->db;
-  $db->do_transaction(sub {
+  $db->with_transaction(sub {
     # Make Emacs happy
     1;
     my $parts    = $::form->{additional_parts} || [];
@@ -81,8 +81,6 @@ sub action_ajax_save {
     $_->{position} = $position++ for @{ $parts };
 
     $self->requirement_spec->update_attributes(parts => $parts)->load;
-
-    1;
   }) or do {
     return $self->js->error(t8('Saving failed. Error message from the database: #1', $db->error))->render;
   };
diff --git a/SL/Controller/RequirementSpecPredefinedText.pm b/SL/Controller/RequirementSpecPredefinedText.pm
deleted file mode 100644 (file)
index 00c86b4..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-package SL::Controller::RequirementSpecPredefinedText;
-
-use strict;
-
-use parent qw(SL::Controller::Base);
-
-use List::MoreUtils qw(none);
-
-use SL::DB::RequirementSpecPredefinedText;
-use SL::Helper::Flash;
-use SL::Locale::String;
-
-use Rose::Object::MakeMethods::Generic
-(
- scalar => [ qw(requirement_spec_predefined_text) ],
-);
-
-__PACKAGE__->run_before('check_auth');
-__PACKAGE__->run_before('setup');
-__PACKAGE__->run_before('load_requirement_spec_predefined_text', only => [ qw(edit update destroy) ]);
-
-#
-# actions
-#
-
-sub action_list {
-  my ($self) = @_;
-
-  $self->render('requirement_spec_predefined_text/list',
-                title                             => t8('Pre-defined Texts'),
-                REQUIREMENT_SPEC_PREDEFINED_TEXTS => SL::DB::Manager::RequirementSpecPredefinedText->get_all_sorted);
-}
-
-sub action_new {
-  my ($self) = @_;
-
-  $self->{requirement_spec_predefined_text} = SL::DB::RequirementSpecPredefinedText->new(useable_for_text_blocks => 1);
-  $self->render('requirement_spec_predefined_text/form', title => t8('Create a new predefined text'));
-}
-
-sub action_edit {
-  my ($self) = @_;
-  $self->render('requirement_spec_predefined_text/form', title => t8('Edit predefined text'));
-}
-
-sub action_create {
-  my ($self) = @_;
-
-  $self->{requirement_spec_predefined_text} = SL::DB::RequirementSpecPredefinedText->new;
-  $self->create_or_update;
-}
-
-sub action_update {
-  my ($self) = @_;
-  $self->create_or_update;
-}
-
-sub action_destroy {
-  my ($self) = @_;
-
-  if (eval { $self->{requirement_spec_predefined_text}->delete; 1; }) {
-    flash_later('info',  t8('The predefined text has been deleted.'));
-  } else {
-    flash_later('error', t8('The predefined text is in use and cannot be deleted.'));
-  }
-
-  $self->redirect_to(action => 'list');
-}
-
-sub action_reorder {
-  my ($self) = @_;
-
-  SL::DB::RequirementSpecPredefinedText->reorder_list(@{ $::form->{requirement_spec_predefined_text_id} || [] });
-
-  $self->render(\'', { type => 'json' });
-}
-
-#
-# filters
-#
-
-sub check_auth {
-  $::auth->assert('config');
-}
-
-sub setup {
-  $::request->layout->use_javascript("${_}.js")  for qw(ckeditor/ckeditor ckeditor/adapters/jquery);
-}
-
-#
-# helpers
-#
-
-sub create_or_update {
-  my $self   = shift;
-  my $is_new = !$self->{requirement_spec_predefined_text}->id;
-  my $params = delete($::form->{requirement_spec_predefined_text}) || { };
-  my $title  = $is_new ? t8('Create a new predefined text') : t8('Edit predefined text');
-
-  # Force presence of booleans for the useable_* flags.
-  my @useable_flags = qw(text_blocks sections);
-  $params->{"useable_for_${_}"} = !!$params->{"useable_for_${_}"} for @useable_flags;
-
-  # Force usage for text blocks if none of the check boxes are marked.
-  $params->{useable_for_text_blocks} = 1 if none { $params->{"useable_for_${_}"} } @useable_flags;
-
-  $self->{requirement_spec_predefined_text}->assign_attributes(%{ $params });
-
-  my @errors = $self->{requirement_spec_predefined_text}->validate;
-
-  if (@errors) {
-    flash('error', @errors);
-    $self->render('requirement_spec_predefined_text/form', title => $title);
-    return;
-  }
-
-  $self->{requirement_spec_predefined_text}->save;
-
-  flash_later('info', $is_new ? t8('The predefined text has been created.') : t8('The predefined text has been saved.'));
-  $self->redirect_to(action => 'list');
-}
-
-sub load_requirement_spec_predefined_text {
-  my ($self) = @_;
-  $self->{requirement_spec_predefined_text} = SL::DB::RequirementSpecPredefinedText->new(id => $::form->{id})->load;
-}
-
-1;
diff --git a/SL/Controller/RequirementSpecRisk.pm b/SL/Controller/RequirementSpecRisk.pm
deleted file mode 100644 (file)
index bfa7aad..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-package SL::Controller::RequirementSpecRisk;
-
-use strict;
-
-use parent qw(SL::Controller::Base);
-
-use SL::DB::RequirementSpecRisk;
-use SL::Helper::Flash;
-use SL::Locale::String;
-
-use Rose::Object::MakeMethods::Generic
-(
- scalar => [ qw(requirement_spec_risk) ],
-);
-
-__PACKAGE__->run_before('check_auth');
-__PACKAGE__->run_before('load_requirement_spec_risk', only => [ qw(edit update destroy) ]);
-
-#
-# actions
-#
-
-sub action_list {
-  my ($self) = @_;
-
-  $self->render('requirement_spec_risk/list',
-                title                  => t8('Risk levels'),
-                REQUIREMENT_SPEC_RISKS => SL::DB::Manager::RequirementSpecRisk->get_all_sorted);
-}
-
-sub action_new {
-  my ($self) = @_;
-
-  $self->{requirement_spec_risk} = SL::DB::RequirementSpecRisk->new;
-  $self->render('requirement_spec_risk/form', title => t8('Create a new risk level'));
-}
-
-sub action_edit {
-  my ($self) = @_;
-  $self->render('requirement_spec_risk/form', title => t8('Edit risk level'));
-}
-
-sub action_create {
-  my ($self) = @_;
-
-  $self->{requirement_spec_risk} = SL::DB::RequirementSpecRisk->new;
-  $self->create_or_update;
-}
-
-sub action_update {
-  my ($self) = @_;
-  $self->create_or_update;
-}
-
-sub action_destroy {
-  my ($self) = @_;
-
-  if (eval { $self->{requirement_spec_risk}->delete; 1; }) {
-    flash_later('info',  t8('The risk level has been deleted.'));
-  } else {
-    flash_later('error', t8('The risk level is in use and cannot be deleted.'));
-  }
-
-  $self->redirect_to(action => 'list');
-}
-
-sub action_reorder {
-  my ($self) = @_;
-
-  SL::DB::RequirementSpecRisk->reorder_list(@{ $::form->{requirement_spec_risk_id} || [] });
-
-  $self->render(\'', { type => 'json' });
-}
-
-#
-# filters
-#
-
-sub check_auth {
-  $::auth->assert('config');
-}
-
-#
-# helpers
-#
-
-sub create_or_update {
-  my $self   = shift;
-  my $is_new = !$self->{requirement_spec_risk}->id;
-  my $params = delete($::form->{requirement_spec_risk}) || { };
-  my $title  = $is_new ? t8('Create a new risk level') : t8('Edit risk level');
-
-  $self->{requirement_spec_risk}->assign_attributes(%{ $params });
-
-  my @errors = $self->{requirement_spec_risk}->validate;
-
-  if (@errors) {
-    flash('error', @errors);
-    $self->render('requirement_spec_risk/form', title => $title);
-    return;
-  }
-
-  $self->{requirement_spec_risk}->save;
-
-  flash_later('info', $is_new ? t8('The risk level has been created.') : t8('The risk level has been saved.'));
-  $self->redirect_to(action => 'list');
-}
-
-sub load_requirement_spec_risk {
-  my ($self) = @_;
-  $self->{requirement_spec_risk} = SL::DB::RequirementSpecRisk->new(id => $::form->{id})->load;
-}
-
-1;
diff --git a/SL/Controller/RequirementSpecStatus.pm b/SL/Controller/RequirementSpecStatus.pm
deleted file mode 100644 (file)
index 12d5a65..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
-package SL::Controller::RequirementSpecStatus;
-
-use strict;
-
-use parent qw(SL::Controller::Base);
-
-use SL::DB::RequirementSpecStatus;
-use SL::Helper::Flash;
-use SL::Locale::String;
-
-use Rose::Object::MakeMethods::Generic
-(
- scalar => [ qw(requirement_spec_status valid_names) ],
-);
-
-__PACKAGE__->run_before('check_auth');
-__PACKAGE__->run_before('load_requirement_spec_status', only => [ qw(edit update destroy) ]);
-__PACKAGE__->run_before(sub { $_[0]->valid_names(\@SL::DB::RequirementSpecStatus::valid_names) });
-
-#
-# actions
-#
-
-sub action_list {
-  my ($self) = @_;
-
-  $self->render('requirement_spec_status/list',
-                title                     => t8('Requirement Spec Statuses'),
-                REQUIREMENT_SPEC_STATUSES => SL::DB::Manager::RequirementSpecStatus->get_all_sorted);
-}
-
-sub action_new {
-  my ($self) = @_;
-
-  $self->{requirement_spec_status} = SL::DB::RequirementSpecStatus->new;
-  $self->render('requirement_spec_status/form', title => t8('Create a new requirement spec status'));
-}
-
-sub action_edit {
-  my ($self) = @_;
-  $self->render('requirement_spec_status/form', title => t8('Edit requirement spec status'));
-}
-
-sub action_create {
-  my ($self) = @_;
-
-  $self->{requirement_spec_status} = SL::DB::RequirementSpecStatus->new;
-  $self->create_or_update;
-}
-
-sub action_update {
-  my ($self) = @_;
-  $self->create_or_update;
-}
-
-sub action_destroy {
-  my ($self) = @_;
-
-  if (eval { $self->{requirement_spec_status}->delete; 1; }) {
-    flash_later('info',  t8('The requirement spec status has been deleted.'));
-  } else {
-    flash_later('error', t8('The requirement spec status is in use and cannot be deleted.'));
-  }
-
-  $self->redirect_to(action => 'list');
-}
-
-sub action_reorder {
-  my ($self) = @_;
-
-  SL::DB::RequirementSpecStatus->reorder_list(@{ $::form->{requirement_spec_status_id} || [] });
-
-  $self->render(\'', { type => 'json' });
-}
-
-#
-# filters
-#
-
-sub check_auth {
-  $::auth->assert('config');
-}
-
-#
-# helpers
-#
-
-sub create_or_update {
-  my $self   = shift;
-  my $is_new = !$self->{requirement_spec_status}->id;
-  my $params = delete($::form->{requirement_spec_status}) || { };
-  my $title  = $is_new ? t8('Create a new requirement spec status') : t8('Edit requirement spec status');
-
-  $self->{requirement_spec_status}->assign_attributes(%{ $params });
-
-  my @errors = $self->{requirement_spec_status}->validate;
-
-  if (@errors) {
-    flash('error', @errors);
-    $self->render('requirement_spec_status/form', title => $title);
-    return;
-  }
-
-  $self->{requirement_spec_status}->save;
-
-  flash_later('info', $is_new ? t8('The requirement spec status has been created.') : t8('The requirement spec status has been saved.'));
-  $self->redirect_to(action => 'list');
-}
-
-sub load_requirement_spec_status {
-  my ($self) = @_;
-  $self->{requirement_spec_status} = SL::DB::RequirementSpecStatus->new(id => $::form->{id})->load;
-}
-
-1;
index 264b83b..ca34ede 100644 (file)
@@ -102,7 +102,7 @@ sub action_ajax_create {
   $self->text_block->add_to_list(position => 'after', reference => $insert_after) if $insert_after;
 
   my $html = $self->render('requirement_spec_text_block/_text_block', { output => 0 }, text_block => $self->text_block);
-  my $node = $self->presenter->requirement_spec_text_block_jstree_data($self->text_block);
+  my $node = $self->text_block->presenter->jstree_data;
 
   $self->invalidate_version
     ->hide('#text-block-list-empty')
@@ -184,7 +184,7 @@ sub action_dragged_and_dropped {
   my $dropped_type       = $position ne 'last' ? undef : $::form->{dropped_type} =~ m/^ text-blocks- (?:front|back) $/x ? $::form->{dropped_type} : die "Unknown 'dropped_type' parameter";
   my $old_where          = $self->text_block->output_position;
 
-  $self->text_block->db->do_transaction(sub {
+  $self->text_block->db->with_transaction(sub {
     1;
     $self->text_block->remove_from_list;
     $self->text_block->output_position($position =~ m/before|after/ ? $dropped_text_block->output_position : $::form->{dropped_type} eq 'text-blocks-front' ? 0 : 1);
@@ -272,7 +272,7 @@ sub action_ajax_paste {
     $self->js->action($::form->{id} ? 'insertAfter' : 'appendTo', $html, '#text-block-' . ($::form->{id} || 'list'));
   }
 
-  my $node = $self->presenter->requirement_spec_text_block_jstree_data($self->text_block);
+  my $node = $self->text_block->presenter->jstree_data;
   $self->invalidate_version
     ->run(SORTABLE_PICTURE_LIST())
     ->jstree->create_node('#tree', $::form->{id} ? ('#tb-' . $::form->{id}, 'after') : ("#tb-${front_back}", 'last'), $node)
@@ -482,7 +482,7 @@ sub show_list {
 sub paste_picture {
   my ($self, $copied) = @_;
 
-  if (!$self->text_block->db->do_transaction(sub {
+  if (!$self->text_block->db->with_transaction(sub {
     1;
     $self->picture($copied->to_object)->save;        # Create new picture from copied data and save
     $self->text_block->add_pictures($self->picture); # Add new picture to text block
diff --git a/SL/Controller/RequirementSpecType.pm b/SL/Controller/RequirementSpecType.pm
deleted file mode 100644 (file)
index e9bdb7b..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-package SL::Controller::RequirementSpecType;
-
-use strict;
-
-use parent qw(SL::Controller::Base);
-
-use SL::DB::RequirementSpecType;
-use SL::Helper::Flash;
-use SL::Locale::String;
-
-use Rose::Object::MakeMethods::Generic
-(
- scalar => [ qw(requirement_spec_type) ],
-);
-
-__PACKAGE__->run_before('check_auth');
-__PACKAGE__->run_before('load_requirement_spec_type', only => [ qw(edit update destroy) ]);
-
-#
-# actions
-#
-
-sub action_list {
-  my ($self) = @_;
-
-  $self->render('requirement_spec_type/list',
-                title                  => t8('Requirement Spec Types'),
-                REQUIREMENT_SPEC_TYPES => SL::DB::Manager::RequirementSpecType->get_all_sorted);
-}
-
-sub action_new {
-  my ($self) = @_;
-
-  $self->{requirement_spec_type} = SL::DB::RequirementSpecType->new(template_file_name => 'requirement_spec');
-  $self->render('requirement_spec_type/form', title => t8('Create a new requirement spec type'));
-}
-
-sub action_edit {
-  my ($self) = @_;
-  $self->render('requirement_spec_type/form', title => t8('Edit requirement spec type'));
-}
-
-sub action_create {
-  my ($self) = @_;
-
-  $self->{requirement_spec_type} = SL::DB::RequirementSpecType->new;
-  $self->create_or_update;
-}
-
-sub action_update {
-  my ($self) = @_;
-  $self->create_or_update;
-}
-
-sub action_destroy {
-  my ($self) = @_;
-
-  if (eval { $self->{requirement_spec_type}->delete; 1; }) {
-    flash_later('info',  t8('The requirement spec type has been deleted.'));
-  } else {
-    flash_later('error', t8('The requirement spec type is in use and cannot be deleted.'));
-  }
-
-  $self->redirect_to(action => 'list');
-}
-
-sub action_reorder {
-  my ($self) = @_;
-
-  SL::DB::RequirementSpecType->reorder_list(@{ $::form->{requirement_spec_type_id} || [] });
-
-  $self->render(\'', { type => 'json' });
-}
-
-#
-# filters
-#
-
-sub check_auth {
-  $::auth->assert('config');
-}
-
-#
-# helpers
-#
-
-sub create_or_update {
-  my $self   = shift;
-  my $is_new = !$self->{requirement_spec_type}->id;
-  my $params = delete($::form->{requirement_spec_type}) || { };
-  my $title  = $is_new ? t8('Create a new requirement spec type') : t8('Edit requirement spec type');
-
-  $self->{requirement_spec_type}->assign_attributes(%{ $params });
-
-  my @errors = $self->{requirement_spec_type}->validate;
-
-  if (@errors) {
-    flash('error', @errors);
-    $self->render('requirement_spec_type/form', title => $title);
-    return;
-  }
-
-  $self->{requirement_spec_type}->save;
-
-  flash_later('info', $is_new ? t8('The requirement spec type has been created.') : t8('The requirement spec type has been saved.'));
-  $self->redirect_to(action => 'list');
-}
-
-sub load_requirement_spec_type {
-  my ($self) = @_;
-  $self->{requirement_spec_type} = SL::DB::RequirementSpecType->new(id => $::form->{id})->load;
-}
-
-1;
diff --git a/SL/Controller/SalesPurchase.pm b/SL/Controller/SalesPurchase.pm
new file mode 100644 (file)
index 0000000..ab31b8c
--- /dev/null
@@ -0,0 +1,70 @@
+package SL::Controller::SalesPurchase;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+use SL::DB::PurchaseInvoice;
+use Carp;
+
+
+sub action_check_duplicate_invnumber {
+  my ($self) = @_;
+
+  croak("no invnumber") unless $::form->{invnumber};
+  croak("no vendor")    unless $::form->{vendor_id};
+
+  my $exists_ap = SL::DB::Manager::PurchaseInvoice->find_by(
+                   invnumber => $::form->{invnumber},
+                   vendor_id => $::form->{vendor_id},
+                 );
+  # we are modifying a existing daily booking - allow this if
+  # booking conditions are not super strict
+  undef $exists_ap if ($::instance_conf->get_ap_changeable != 0
+                    && $exists_ap->gldate == DateTime->today_local);
+
+
+  $_[0]->render(\ !!$exists_ap, { type => 'text' });
+}
+
+1;
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Controller::SalesPurchase - Controller for JS driven actions
+
+=head2 OVERVIEW
+
+Generic Controller Class for validation function
+
+=head1 FUNCTIONS
+
+=over 2
+
+=item C<action_check_duplicate_invnumber>
+
+Needs C<form.invnumber> and C<form.vendor_id>
+
+Returns true if a credit record with this invnumber for this vendor
+already exists.
+
+Example usage (js):
+
+ $.ajax({
+      url: 'controller.pl',
+      data: { action: 'SalesPurchase/check_duplicate_invnumber',
+              vendor_id    : $('#vendor_id').val(),
+              invnumber    : $('#invnumber').val()
+      },
+      method: "GET",
+      async: false,
+      dataType: 'text',
+      success: function(val) {
+        exists_invnumber = val;
+      }
+    });
+
+=back
index ddaf765..ccb0d5f 100644 (file)
@@ -96,6 +96,9 @@ sub prepare_report {
   my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
   $self->{report} = $report;
 
+  my $title    = $::locale->text('Sales Price information');
+  $title      .= ': ' . $::locale->text($::form->{filter}->{order}{type}) if $::form->{filter}->{order}{type};
+
   my @columns  = qw(transdate ordnumber customer ship qty sellprice discount amount);
   my @visible  = qw(transdate ordnumber customer ship qty sellprice discount amount);
   my @sortable = qw(transdate ordnumber customer          sellprice discount       );
@@ -121,7 +124,7 @@ sub prepare_report {
     %{ $params{report_generator_options} || {} },
     output_format        => 'HTML',
     top_info_text        => $self->displayable_filter($::form->{filter}),
-    title                => $::locale->text('Sales Price information'),
+    title                => $title,
   );
   $report->set_options_from_form;
 }
@@ -137,7 +140,11 @@ sub link_to {
     my $vc     = $object->is_sales ? 'customer' : 'vendor';
     my $id     = $object->id;
 
-    return "oe.pl?action=$action&type=$type&vc=$vc&id=$id";
+    if ($::instance_conf->get_feature_experimental_order) {
+      return "controller.pl?action=Order/$action&type=$type&id=$id";
+    } else {
+      return "oe.pl?action=$action&type=$type&vc=$vc&id=$id";
+    }
   }
   if ($object->isa('SL::DB::Customer')) {
     my $id     = $object->id;
@@ -162,9 +169,8 @@ sub displayable_filter {
   my $column_defs = $self->column_defs;
   my @texts;
 
-  push @texts, [ $::locale->text('Type'),    $::locale->text($filter->{order}{type}) ] if $filter->{order}{type};
   push @texts, [ $::locale->text('Sort By'), $column_defs->{$self->{sort_by}}{text}  ] if $column_defs->{$self->{sort_by}}{text};
-  push @texts, [ $::locale->text('Page'),    $::locale->text($self->{pages}{cur})    ] if $self->{pages}{cur} != 1;
+  push @texts, [ $::locale->text('Page'),    $::locale->text($self->{pages}{cur})    ] if $self->{pages}{cur} > 1;
 
   return join '; ', map { "$_->[0]: $_->[1]" } @texts;
 }
diff --git a/SL/Controller/Shop.pm b/SL/Controller/Shop.pm
new file mode 100644 (file)
index 0000000..afc7662
--- /dev/null
@@ -0,0 +1,213 @@
+package SL::Controller::Shop;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use SL::Helper::Flash;
+use SL::Locale::String;
+use SL::DB::Default;
+use SL::DB::Manager::Shop;
+use SL::DB::Pricegroup;
+use SL::DB::TaxZone;
+
+use Rose::Object::MakeMethods::Generic (
+  scalar                  => [ qw(connectors price_types price_sources taxzone_id protocols) ],
+  'scalar --get_set_init' => [ qw(shop) ]
+);
+
+__PACKAGE__->run_before('check_auth');
+__PACKAGE__->run_before('load_types',    only => [ qw(new edit) ]);
+
+#
+# actions
+#
+
+sub action_list {
+  my ($self) = @_;
+
+  $self->_setup_list_action_bar;
+  $self->render('shops/list',
+                title => t8('Shops'),
+                SHOPS => SL::DB::Manager::Shop->get_all_sorted,
+               );
+}
+
+sub action_edit {
+  my ($self) = @_;
+
+  my $is_new = !$self->shop->id;
+  $self->_setup_form_action_bar;
+  $self->render('shops/form', title => ($is_new ? t8('Add shop') : t8('Edit shop')));
+}
+
+sub action_save {
+  my ($self) = @_;
+
+  $self->create_or_update;
+}
+
+sub action_delete {
+  my ($self) = @_;
+
+  if ( eval { $self->shop->delete; 1; } ) {
+    flash_later('info',  $::locale->text('The shop has been deleted.'));
+  } else {
+    flash_later('error', $::locale->text('The shop is in use and cannot be deleted.'));
+  };
+  $self->redirect_to(action => 'list');
+}
+
+sub action_reorder {
+  my ($self) = @_;
+
+  SL::DB::Shop->reorder_list(@{ $::form->{shop_id} || [] });
+  $self->render(\'', { type => 'json' }); # ' emacs happy again
+}
+
+sub action_check_connectivity {
+  my ($self) = @_;
+
+  my $ok = 0;
+  require SL::Shop;
+  my $shop = SL::Shop->new( config => $self->shop );
+  my $connect = $shop->check_connectivity;
+  $ok       = $connect->{success};
+  my  $version = $connect->{data}->{version};
+  $self->render('shops/test_shop_connection', { layout => 0 },
+                title   => t8('Shop Connection Test'),
+                ok      => $ok,
+                version => $version);
+}
+
+sub check_auth {
+  $::auth->assert('config');
+}
+
+sub init_shop {
+  SL::DB::Manager::Shop->find_by_or_create(id => $::form->{id} || 0)->assign_attributes(%{ $::form->{shop} });
+}
+
+#
+# helpers
+#
+
+sub create_or_update {
+  my ($self) = @_;
+
+  my $is_new = !$self->shop->id;
+
+  my @errors = $self->shop->validate;
+  if (@errors) {
+    flash('error', @errors);
+    $self->load_types();
+    $self->action_edit();
+    return;
+  }
+
+  $self->shop->save;
+
+  flash_later('info', $is_new ? t8('The shop has been created.') : t8('The shop has been saved.'));
+  $self->redirect_to(action => 'list');
+}
+
+sub load_types {
+  my ($self) = @_;
+  # data for the dropdowns when editing Shop configs
+
+  require SL::ShopConnector::ALL;
+  $self->connectors(SL::ShopConnector::ALL->connectors);
+
+  $self->price_types( [ { id => "brutto", name => t8('brutto') },
+                        { id => "netto",  name => t8('netto')  } ] );
+
+  $self->protocols(   [ { id => "http",  name => t8('http') },
+                        { id => "https", name => t8('https') } ] );
+
+  my $pricesources;
+  push(@{ $pricesources } , { id => "master_data/sellprice", name => t8("Master Data") . " - " . t8("Sellprice") },
+                            { id => "master_data/listprice", name => t8("Master Data") . " - " . t8("Listprice") },
+                            { id => "master_data/lastcost",  name => t8("Master Data") . " - " . t8("Lastcost")  });
+  my $pricegroups = SL::DB::Manager::Pricegroup->get_all;
+  foreach my $pg ( @$pricegroups ) {
+    push( @{ $pricesources } , { id => "pricegroup/" . $pg->id, name => t8("Pricegroup") . " - " . $pg->pricegroup} );
+  };
+
+  $self->price_sources($pricesources);
+
+  #Buchungsgruppen for calculate the tax for an article
+  my $taxkey_ids;
+  my $taxzones = SL::DB::Manager::TaxZone->get_all_sorted();
+
+  foreach my $tz (@$taxzones) {
+    push  @{ $taxkey_ids }, { id => $tz->id, name => $tz->description };
+  }
+  $self->taxzone_id( $taxkey_ids );
+};
+
+sub _setup_form_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [
+          t8('Save'),
+          submit    => [ '#form', { action => "Shop/save" } ],
+          accesskey => 'enter',
+        ],
+         action => [
+          t8('Delete'),
+          submit => [ '#form', { action => "Shop/delete" } ],
+        ],
+      ],
+      action => [
+        t8('Check Api'),
+        call => [ 'kivi.Shop.check_connectivity', id => "form" ],
+        tooltip => t8('Check connectivity'),
+      ],
+      action => [
+        t8('Cancel'),
+        submit => [ '#form', { action => "Shop/list" } ],
+      ],
+    );
+  }
+}
+
+sub _setup_list_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      link => [
+        t8('Add'),
+        link => $self->url_for(action => 'edit'),
+      ],
+    )
+  };
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+  SL::Controller::Shop
+
+=head1 SYNOPSIS
+
+
+=head1 DESCRIPTION
+
+
+=head1 BUGS
+
+None yet. :)
+
+=head1 AUTHOR
+
+G. Richardson E<lt>information@kivitendo-premium.deE<gt>
+W. Hahn E<lt>wh@futureworldsearch.netE<gt>
diff --git a/SL/Controller/ShopOrder.pm b/SL/Controller/ShopOrder.pm
new file mode 100644 (file)
index 0000000..3e61c63
--- /dev/null
@@ -0,0 +1,429 @@
+package SL::Controller::ShopOrder;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use SL::BackgroundJob::ShopOrderMassTransfer;
+use SL::System::TaskServer;
+use SL::DB::ShopOrder;
+use SL::DB::ShopOrderItem;
+use SL::DB::Shop;
+use SL::DB::History;
+use SL::DBUtils;
+use SL::Shop;
+use SL::Helper::Flash;
+use SL::Locale::String;
+use SL::Controller::Helper::ParseFilter;
+use Rose::Object::MakeMethods::Generic
+(
+  'scalar --get_set_init' => [ qw(shop_order shops transferred js) ],
+);
+
+__PACKAGE__->run_before('check_auth');
+__PACKAGE__->run_before('setup');
+
+use Data::Dumper;
+
+sub action_get_orders {
+  my ( $self ) = @_;
+  my $orders_fetched;
+  my $new_orders;
+
+  my $type = $::form->{type};
+  if ( $type eq "get_next" ) {
+    my $active_shops = SL::DB::Manager::Shop->get_all(query => [ obsolete => 0 ]);
+    foreach my $shop_config ( @{ $active_shops } ) {
+      my $shop = SL::Shop->new( config => $shop_config );
+
+      $new_orders = $shop->connector->get_new_orders;
+      push @{ $orders_fetched }, $new_orders ;
+    }
+
+  } elsif ( $type eq "get_one" ) {
+    my $shop_id = $::form->{shop_id};
+    my $shop_ordernumber = $::form->{shop_ordernumber};
+
+    if ( $shop_id && $shop_ordernumber ){
+      my $shop_config = SL::DB::Manager::Shop->get_first(query => [ id => $shop_id, obsolete => 0 ]);
+      my $shop = SL::Shop->new( config => $shop_config );
+      unless ( SL::DB::Manager::ShopOrder->get_all_count( query => [ shop_ordernumber => $shop_ordernumber, shop_id => $shop_id, obsolete => 'f' ] )) {
+        my $connect = $shop->check_connectivity;
+        $new_orders = $shop->connector->get_one_order($shop_ordernumber);
+        push @{ $orders_fetched }, $new_orders ;
+      } else {
+        flash_later('error', t8('Shoporder "#2" From Shop "#1" is already fetched', $shop->config->description, $shop_ordernumber));
+      }
+    } else {
+        flash_later('error', t8('Shop or ordernumber not selected.'));
+    }
+  }
+
+  foreach my $shop_fetched(@{ $orders_fetched }) {
+    if($shop_fetched->{error}){
+      flash_later('error', t8('From shop "#1" :  #2 ', $shop_fetched->{shop_description}, $shop_fetched->{message},));
+    }else{
+      flash_later('info', t8('From shop #1 :  #2 shoporders have been fetched.', $shop_fetched->{shop_description}, $shop_fetched->{number_of_orders},));
+    }
+  }
+
+  $self->redirect_to(controller => "ShopOrder", action => 'list', filter => { 'transferred:eq_ignore_empty' => 0, obsolete => 0 });
+}
+
+sub action_list {
+  my ( $self ) = @_;
+
+  my %filter = ($::form->{filter} ? parse_filter($::form->{filter}) : query => [ transferred => 0, obsolete => 0 ]);
+  my $transferred = $::form->{filter}->{transferred_eq_ignore_empty} ne '' ? $::form->{filter}->{transferred_eq_ignore_empty} : '';
+  my $sort_by = $::form->{sort_by} ? $::form->{sort_by} : 'order_date';
+  $sort_by .=$::form->{sort_dir} ? ' DESC' : ' ASC';
+  my $shop_orders = SL::DB::Manager::ShopOrder->get_all( %filter, sort_by => $sort_by,
+                                                      with_objects => ['shop_order_items', 'kivi_customer', 'shop'],
+                                                    );
+
+  foreach my $shop_order(@{ $shop_orders }){
+    $shop_order->{open_invoices} = $shop_order->check_for_open_invoices;
+  }
+  $self->_setup_list_action_bar;
+  $self->render('shop_order/list',
+                title       => t8('ShopOrders'),
+                SHOPORDERS  => $shop_orders,
+                TOOK        => $transferred,
+              );
+}
+
+sub action_show {
+  my ( $self ) = @_;
+  my $id = $::form->{id} || {};
+  my $shop_order = SL::DB::ShopOrder->new( id => $id )->load( with => ['kivi_customer'] );
+  die "can't find shoporder with id $id" unless $shop_order;
+
+  my $proposals = $shop_order->check_for_existing_customers;
+
+  $self->render('shop_order/show',
+                title       => t8('Shoporder'),
+                IMPORT      => $shop_order,
+                PROPOSALS   => $proposals,
+              );
+
+}
+
+sub action_customer_assign_to_shoporder {
+  my ($self) = @_;
+
+  $self->shop_order->assign_attributes( kivi_customer => $::form->{customer} );
+  $self->shop_order->save;
+  $self->redirect_to(controller => "ShopOrder", action => 'show', id => $self->shop_order->id);
+}
+
+sub action_delete_order {
+  my ( $self ) = @_;
+
+  $self->shop_order->obsolete(1);
+  $self->shop_order->save;
+  $self->redirect_to(controller => "ShopOrder", action => 'list', filter => { 'transferred:eq_ignore_empty' => 0 });
+}
+
+sub action_undelete_order {
+  my ( $self ) = @_;
+
+  $self->shop_order->obsolete(0);
+  $self->shop_order->save;
+  $self->redirect_to(controller => "ShopOrder", action => 'list', filter => { 'transferred:eq_ignore_empty' => 0, obsolete => 0 });
+}
+
+sub action_transfer {
+  my ( $self ) = @_;
+
+  my $customer = SL::DB::Manager::Customer->find_by(id => $::form->{customer});
+  die "Can't find customer" unless $customer;
+  my $employee = SL::DB::Manager::Employee->current;
+  die "Can't find employee" unless $employee;
+
+  die "Can't load shop_order form form->import_id" unless $self->shop_order;
+  my $order = $self->shop_order->convert_to_sales_order(customer => $customer, employee => $employee);
+
+  if ($order->{error}){
+    flash_later('error',@{$order->{errors}});
+    $self->redirect_to(controller => "ShopOrder", action => 'show', id => $self->shop_order->id);
+  }else{
+    $order->db->with_transaction( sub {
+      $order->calculate_prices_and_taxes;
+      $order->save;
+
+      my $snumbers = "ordernumber_" . $order->ordnumber;
+      SL::DB::History->new(
+                        trans_id    => $order->id,
+                        snumbers    => $snumbers,
+                        employee_id => SL::DB::Manager::Employee->current->id,
+                        addition    => 'SAVED',
+                        what_done   => 'Shopimport -> Order',
+                      )->save();
+      foreach my $item(@{ $order->orderitems }){
+        $item->parse_custom_variable_values->save;
+        $item->{custom_variables} = \@{ $item->cvars_by_config };
+        $item->save;
+      }
+
+      $self->shop_order->transferred(1);
+      $self->shop_order->transfer_date(DateTime->now_local);
+      $self->shop_order->save;
+      $self->shop_order->link_to_record($order);
+    }) || die $order->db->error;
+    my $order_controller = $::instance_conf->get_feature_experimental_order ? 'Order' :'oe.pl';
+    $self->redirect_to(controller => $order_controller, action => 'edit', type => 'sales_order', vc => 'customer', id => $order->id);
+  }
+}
+
+sub action_mass_transfer {
+  my ($self) = @_;
+  my @shop_orders =  @{ $::form->{id} || [] };
+
+  my $job                   = SL::DB::BackgroundJob->new(
+    type                    => 'once',
+    active                  => 1,
+    package_name            => 'ShopOrderMassTransfer',
+  )->set_data(
+     shop_order_record_ids       => [ @shop_orders ],
+     num_order_created           => 0,
+     num_order_failed            => 0,
+     num_delivery_order_created  => 0,
+     status                      => SL::BackgroundJob::ShopOrderMassTransfer->WAITING_FOR_EXECUTION(),
+     conversion_errors         => [],
+   )->update_next_run_at;
+
+   SL::System::TaskServer->new->wake_up;
+
+   my $html = $self->render('shop_order/_transfer_status', { output => 0 }, job => $job);
+
+   $self->js
+      ->html('#status_mass_transfer', $html)
+      ->run('kivi.ShopOrder.massTransferStarted')
+      ->render;
+}
+
+sub action_transfer_status {
+  my ($self)  = @_;
+  my $job     = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
+  my $html    = $self->render('shop_order/_transfer_status', { output => 0 }, job => $job);
+
+  $self->js->html('#status_mass_transfer', $html);
+  $self->js->run('kivi.ShopOrder.massTransferFinished') if $job->data_as_hash->{status} == SL::BackgroundJob::ShopOrderMassTransfer->DONE();
+  $self->js->render;
+
+}
+
+sub action_apply_customer {
+  my ( $self, %params ) = @_;
+  my $shop = SL::DB::Manager::Shop->find_by( id => $self->shop_order->shop_id );
+  my $what = $::form->{create_customer}; # new from billing, customer or delivery address
+  my %address = ( 'name'                  => $::form->{$what.'_name'},
+                  'department_1'          => $::form->{$what.'_company'},
+                  'department_2'          => $::form->{$what.'_department'},
+                  'street'                => $::form->{$what.'_street'},
+                  'zipcode'               => $::form->{$what.'_zipcode'},
+                  'city'                  => $::form->{$what.'_city'},
+                  'email'                 => $::form->{$what.'_email'},
+                  'country'               => $::form->{$what.'_country'},
+                  'phone'                 => $::form->{$what.'_phone'},
+                  'email'                 => $::form->{$what.'_email'},
+                  'greeting'              => $::form->{$what.'_greeting'},
+                  'taxincluded_checked'   => $shop->pricetype eq "brutto" ? 1 : 0,
+                  'taxincluded'           => $shop->pricetype eq "brutto" ? 1 : 0,
+                  'pricegroup_id'         => (split '\/',$shop->price_source)[0] eq "pricegroup" ?  (split '\/',$shop->price_source)[1] : undef,
+                  'taxzone_id'            => $shop->taxzone_id,
+                  'currency'              => $::instance_conf->get_currency_id,
+                  #'payment_id'            => 7345,# TODO hardcoded
+                );
+  my $customer;
+  if($::form->{cv_id}){
+    $customer = SL::DB::Customer->new(id => $::form->{cv_id})->load;
+    $customer->assign_attributes(%address);
+    $customer->save;
+  }else{
+    $customer = SL::DB::Customer->new(%address);
+    $customer->save;
+  }
+  my $snumbers = "customernumber_" . $customer->customernumber;
+  SL::DB::History->new(
+                    trans_id    => $customer->id,
+                    snumbers    => $snumbers,
+                    employee_id => SL::DB::Manager::Employee->current->id,
+                    addition    => 'SAVED',
+                    what_done   => 'Shopimport',
+                  )->save();
+
+  $self->redirect_to(action => 'show', id => $::form->{import_id});
+}
+
+sub setup {
+  my ($self) = @_;
+  $::auth->assert('shop_part_edit');
+  $::request->layout->use_javascript("${_}.js")  for qw(kivi.ShopOrder);
+}
+
+sub check_auth {
+  $::auth->assert('shop_part_edit');
+}
+#
+# Helper
+#
+
+sub init_shop_order {
+  my ( $self ) = @_;
+  return SL::DB::ShopOrder->new(id => $::form->{import_id})->load if $::form->{import_id};
+}
+
+sub init_transferred {
+  [ { title => t8("all"),             value => '' },
+    { title => t8("transferred"),     value => 1  },
+    { title => t8("not transferred"), value => 0  }, ]
+}
+
+sub init_shops {
+  SL::DB::Shop->shops_dd;
+}
+
+sub _setup_list_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+        action => [
+          t8('Search'),
+          submit    => [ '#shoporders', { action => "ShopOrder/list" } ],
+        ],
+        combobox => [
+          link => [
+            t8('Shoporders'),
+            call    => [ 'kivi.ShopOrder.get_orders_next' ],
+            tooltip => t8('New shop orders'),
+          ],
+          action => [
+            t8('Get one order'),
+            call    => [ 'kivi.ShopOrder.get_one_order_setup', id => "get_one" ],
+            tooltip => t8('Get one order by shopordernumber'),
+          ],
+        ],
+        'separator',
+        action => [
+          t8('Execute'),
+          call => [ 'kivi.ShopOrder.setup', id => "mass_transfer" ],
+          tooltip => t8('Transfer all marked'),
+        ],
+    );
+  }
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::ShopOrder - Shoporder CRUD Controller
+
+=head1 DESCRIPTION
+
+Fetches the shoporders and transfers them to orders.
+
+Relations for shoporders
+
+=over 2
+
+=item shop_order_items
+
+=item shops
+
+=item shop_parts
+
+=back
+
+=head1 URL ACTIONS
+
+=over 4
+
+=item C<action_get_orders>
+
+Fetches the shoporders with the shopconnector class
+
+=item C<action_list>
+
+List the shoporders by different filters.
+From the List you can transfer shoporders into orders in batch where it is possible or one by one.
+
+=item C<action_show>
+
+Shows one order. From here you can apply/change/select customer data and transfer the shoporder to an order.
+
+=item C<action_delete>
+
+Marks the shoporder as obsolete. It's for shoporders you don't want to transfer.
+
+=item C<action_undelete>
+
+Marks the shoporder obsolete = false
+
+=item C<action_transfer>
+
+Transfers one shoporder to an order.
+
+=item C<action_apply_customer>
+
+Applys a new customer from the shoporder.
+
+=back
+
+=head1 TASKSERVER ACTIONS
+
+=over 4
+
+=item C<action_mass_transfer>
+
+Transfers more shoporders by backgroundjob called from the taskserver to orders.
+
+=item C<action_transfer_status>
+
+Shows the backgroundjobdata for the popup status window
+
+=back
+
+=head1 SETUP
+
+=over 4
+
+=item C<setup>
+
+=back
+
+=head1 INITS
+
+=over 4
+
+=item C<init_shoporder>
+
+=item C<init_transfered>
+
+Transferstatuses for the filter dropdown
+
+=item C<init_shops>
+
+Filter dropdown Shops
+
+=back
+
+=head1 TODO
+
+Implements different payments, pricesources and pricegroups. Till now not needed.
+
+=head1 BUGS
+
+None yet. :)
+
+=head1 AUTHOR
+
+W. Hahn E<lt>wh@futureworldsearch.netE<gt>
+
+=cut
diff --git a/SL/Controller/ShopPart.pm b/SL/Controller/ShopPart.pm
new file mode 100644 (file)
index 0000000..6ab6507
--- /dev/null
@@ -0,0 +1,490 @@
+package SL::Controller::ShopPart;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use SL::BackgroundJob::ShopPartMassUpload;
+use SL::System::TaskServer;
+use Data::Dumper;
+use SL::Locale::String qw(t8);
+use SL::DB::ShopPart;
+use SL::DB::Shop;
+use SL::DB::File;
+use SL::DB::ShopImage;
+use SL::DB::Default;
+use SL::Helper::Flash;
+use SL::Controller::Helper::ParseFilter;
+use MIME::Base64;
+
+use Rose::Object::MakeMethods::Generic
+(
+   scalar                 => [ qw(price_sources) ],
+  'scalar --get_set_init' => [ qw(shop_part file shops) ],
+);
+
+__PACKAGE__->run_before('check_auth');
+__PACKAGE__->run_before('add_javascripts', only => [ qw(edit_popup list_articles) ]);
+__PACKAGE__->run_before('load_pricesources',    only => [ qw(create_or_edit_popup) ]);
+
+#
+# actions
+#
+
+sub action_create_or_edit_popup {
+  my ($self) = @_;
+
+  $self->render_shop_part_edit_dialog();
+}
+
+sub action_update_shop {
+  my ($self, %params) = @_;
+
+  my $shop_part = SL::DB::Manager::ShopPart->find_by(id => $::form->{shop_part_id});
+  die unless $shop_part;
+
+  require SL::Shop;
+  my $shop = SL::Shop->new( config => $shop_part->shop );
+
+  my $connect = $shop->check_connectivity;
+  if($connect->{success}){
+    my $return    = $shop->connector->update_part($self->shop_part, 'all');
+
+    # the connector deals with parsing/result verification, just needs to return success or failure
+    if ( $return == 1 ) {
+      my $now = DateTime->now;
+      my $attributes->{last_update} = $now;
+      $self->shop_part->assign_attributes(%{ $attributes });
+      $self->shop_part->save;
+      $self->js->html('#shop_part_last_update_' . $shop_part->id, $now->to_kivitendo('precision' => 'minute'))
+             ->flash('info', t8("Updated part [#1] in shop [#2] at #3", $shop_part->part->displayable_name, $shop_part->shop->description, $now->to_kivitendo('precision' => 'minute') ) )
+             ->render;
+    } else {
+      $self->js->flash('error', t8('The shop part wasn\'t updated.'))->render;
+    }
+  }else{
+    $self->js->flash('error', t8('The shop part wasn\'t updated. #1', $connect->{data}->{version}))->render;
+  }
+
+
+}
+
+sub action_show_files {
+  my ($self) = @_;
+
+  my $images = SL::DB::Manager::ShopImage->get_all( where => [ 'files.object_id' => $::form->{id}, ], with_objects => 'file', sort_by => 'position' );
+
+  $self->render('shop_part/_list_images', { header => 0 }, IMAGES => $images);
+}
+
+sub action_ajax_delete_file {
+  my ( $self ) = @_;
+  $self->file->delete;
+
+  $self->js
+    ->run('kivi.ShopPart.show_images',$self->file->object_id)
+    ->render();
+}
+
+sub action_get_categories {
+  my ($self) = @_;
+
+  require SL::Shop;
+  my $shop = SL::Shop->new( config => $self->shop_part->shop );
+
+  my $connect = $shop->check_connectivity;
+  if($connect->{success}){
+    my $categories = $shop->connector->get_categories;
+
+    $self->js
+      ->run(
+        'kivi.ShopPart.shop_part_dialog',
+        t8('Shopcategories'),
+        $self->render('shop_part/categories', { output => 0 }, CATEGORIES => $categories )
+      )
+      ->reinit_widgets;
+      $self->js->render;
+  }else{
+    $self->js->flash('error', t8('Can\'t connect to shop. #1', $connect->{data}->{version}))->render;
+  }
+
+}
+
+sub action_show_price_n_pricesource {
+  my ($self) = @_;
+
+  my ( $price, $price_src_str ) = $self->get_price_n_pricesource($::form->{pricesource});
+
+  if( $price_src_str eq 'sellprice'){
+    $price_src_str = t8('Sellprice');
+  }elsif( $price_src_str eq 'listprice'){
+    $price_src_str = t8('Listprice');
+  }elsif( $price_src_str eq 'lastcost'){
+    $price_src_str = t8('Lastcost');
+  }
+  $self->js->html('#price_' . $self->shop_part->id, $::form->format_amount(\%::myconfig,$price,2))
+           ->html('#active_price_source_' . $self->shop_part->id, $price_src_str)
+           ->render;
+}
+
+sub action_show_stock {
+  my ($self) = @_;
+  my ( $stock_local, $stock_onlineshop, $active_online );
+
+  require SL::Shop;
+  my $shop = SL::Shop->new( config => $self->shop_part->shop );
+
+  if($self->shop_part->last_update) {
+    my $shop_article = $shop->connector->get_article($self->shop_part->part->partnumber);
+    $stock_onlineshop = $shop_article->{data}->{mainDetail}->{inStock};
+    $active_online = $shop_article->{data}->{active};
+  }
+
+  $stock_local = $self->shop_part->part->onhand;
+
+  $self->js->html('#stock_' . $self->shop_part->id, $::form->format_amount(\%::myconfig,$stock_local,0)."/".$::form->format_amount(\%::myconfig,$stock_onlineshop,0))
+           ->html('#toogle_' . $self->shop_part->id,$active_online)
+           ->render;
+}
+
+sub action_get_n_write_categories {
+  my ($self) = @_;
+
+  my @shop_parts =  @{ $::form->{shop_parts_ids} || [] };
+  foreach my $part(@shop_parts){
+
+    my $shop_part = SL::DB::Manager::ShopPart->get_all( where => [id => $part], with_objects => ['part', 'shop'])->[0];
+    require SL::DB::Shop;
+    my $shop = SL::Shop->new( config => $shop_part->shop );
+    my $online_article = $shop->connector->get_article($shop_part->part->partnumber);
+    my $online_cat = $online_article->{data}->{categories};
+    my @cat = ();
+    for(keys %$online_cat){
+      my @cattmp;
+      push @cattmp,$online_cat->{$_}->{id};
+      push @cattmp,$online_cat->{$_}->{name};
+      push @cat,\@cattmp;
+    }
+    my $attributes->{shop_category} = \@cat;
+    my $active->{active} = $online_article->{data}->{active};
+    $shop_part->assign_attributes(%{$attributes}, %{$active});
+    $shop_part->save;
+  }
+  $self->redirect_to( action => 'list_articles' );
+}
+
+sub action_save_categories {
+  my ($self) = @_;
+
+  my @categories =  @{ $::form->{categories} || [] };
+
+    my @cat = ();
+    foreach my $cat ( @categories) {
+      my @cattmp;
+      push( @cattmp,$cat );
+      push( @cattmp,$::form->{"cat_id_${cat}"} );
+      push( @cat,\@cattmp );
+    }
+
+  my $categories->{shop_category} = \@cat;
+
+  my $params = delete($::form->{shop_part}) || { };
+
+  $self->shop_part->assign_attributes(%{ $params });
+  $self->shop_part->assign_attributes(%{ $categories });
+
+  $self->shop_part->save;
+
+  flash('info', t8('The categories has been saved.'));
+
+  $self->js->run('kivi.ShopPart.close_dialog')
+           ->flash('info', t8("Updated categories"))
+           ->render;
+}
+
+sub action_reorder {
+  my ($self) = @_;
+  require SL::DB::ShopImage;
+  SL::DB::ShopImage->reorder_list(@{ $::form->{image_id} || [] });
+
+  $self->render(\'', { type => 'json' });
+}
+
+sub action_list_articles {
+  my ($self) = @_;
+
+  my %filter      = ($::form->{filter} ? parse_filter($::form->{filter}) : query => [ 'shop.obsolete' => 0 ]);
+  my $sort_by     = $::form->{sort_by} ? $::form->{sort_by} : 'part.partnumber';
+  $sort_by .=$::form->{sort_dir} ? ' DESC' : ' ASC';
+
+  my $articles = SL::DB::Manager::ShopPart->get_all( %filter ,with_objects => [ 'part','shop' ], sort_by => $sort_by );
+
+  foreach my $article (@{ $articles}) {
+    my $images = SL::DB::Manager::ShopImage->get_all_count( where => [ 'files.object_id' => $article->part->id, ], with_objects => 'file', sort_by => 'position' );
+    $article->{images} = $images;
+  }
+
+  $self->render('shop_part/_list_articles', title => t8('Webshops articles'), SHOP_PARTS => $articles);
+}
+
+sub action_upload_status {
+  my ($self) = @_;
+  my $job     = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
+  my $html    = $self->render('shop_part/_upload_status', { output => 0 }, job => $job);
+
+  $self->js->html('#status_mass_upload', $html);
+  $self->js->run('kivi.ShopPart.massUploadFinished') if $job->data_as_hash->{status} == SL::BackgroundJob::ShopPartMassUpload->DONE();
+  $self->js->render;
+}
+
+sub action_mass_upload {
+  my ($self) = @_;
+
+  my @shop_parts =  @{ $::form->{shop_parts_ids} || [] };
+
+  my $job = SL::DB::BackgroundJob->new(
+        type                 => 'once',
+        active               => 1,
+        package_name         => 'ShopPartMassUpload',
+        )->set_data(
+        shop_part_record_ids => [ @shop_parts ],
+        todo                 => $::form->{upload_todo},
+        status               => SL::BackgroundJob::ShopPartMassUpload->WAITING_FOR_EXECUTION(),
+        conversation         => [ ],
+        num_uploaded         => 0,
+   )->update_next_run_at;
+
+   SL::System::TaskServer->new->wake_up;
+
+   my $html = $self->render('shop_part/_upload_status', { output => 0 }, job => $job);
+
+   $self->js
+      ->html('#status_mass_upload', $html)
+      ->run('kivi.ShopPart.massUploadStarted')
+      ->render;
+}
+
+sub action_update {
+  my ($self) = @_;
+  $self->create_or_update;
+}
+
+sub render_shop_part_edit_dialog {
+  my ($self) = @_;
+
+  $self->js
+    ->run(
+      'kivi.ShopPart.shop_part_dialog',
+      t8('Shop part'),
+      $self->render('shop_part/edit', { output => 0 })
+    )
+    ->reinit_widgets;
+
+  $self->js->render;
+}
+
+sub create_or_update {
+  my ($self) = @_;
+
+  my $is_new = !$self->shop_part->id;
+  my $params = delete($::form->{shop_part}) || { };
+  $self->shop_part->assign_attributes(%{ $params });
+  $self->shop_part->save;
+
+  my ( $price, $price_src_str ) = $self->get_price_n_pricesource($self->shop_part->active_price_source);
+
+  if(!$is_new){
+    flash('info', $is_new ? t8('The shop part has been created.') : t8('The shop part has been saved.'));
+    $self->js->html('#shop_part_description_' . $self->shop_part->id, $self->shop_part->shop_description)
+           ->html('#shop_part_active_' . $self->shop_part->id, $self->shop_part->active)
+           ->html('#price_' . $self->shop_part->id, $::form->format_amount(\%::myconfig,$price,2))
+           ->html('#active_price_source_' . $self->shop_part->id, $price_src_str)
+           ->run('kivi.ShopPart.close_dialog')
+           ->flash('info', t8("Updated shop part"))
+           ->render;
+         }else{
+    $self->redirect_to(controller => 'Part', action => 'edit', 'part.id' => $self->shop_part->part_id);
+  }
+}
+
+#
+# internal stuff
+#
+sub add_javascripts  {
+  $::request->{layout}->add_javascripts(qw(kivi.ShopPart.js));
+}
+
+sub load_pricesources {
+  my ($self) = @_;
+
+  my $pricesources;
+  push( @{ $pricesources } , { id => "master_data/sellprice", name => t8("Master Data")." - ".t8("Sellprice") },
+                             { id => "master_data/listprice", name => t8("Master Data")." - ".t8("Listprice") },
+                             { id => "master_data/lastcost",  name => t8("Master Data")." - ".t8("Lastcost") }
+                             );
+  my $pricegroups = SL::DB::Manager::Pricegroup->get_all;
+  foreach my $pg ( @$pricegroups ) {
+    push( @{ $pricesources } , { id => "pricegroup/".$pg->id, name => t8("Pricegroup") . " - " . $pg->pricegroup} );
+  };
+
+  $self->price_sources( $pricesources );
+}
+
+sub get_price_n_pricesource {
+  my ($self,$pricesource) = @_;
+
+  my ( $price_src_str, $price_src_id ) = split(/\//,$pricesource);
+
+  require SL::DB::Pricegroup;
+  require SL::DB::Part;
+  my $price;
+  if ($price_src_str eq "master_data") {
+    my $part       = SL::DB::Manager::Part->find_by( id => $self->shop_part->part_id );
+    $price         = $part->$price_src_id;
+    $price_src_str = $price_src_id;
+    }else{
+    my $part       = SL::DB::Manager::Part->get_all( where => [id => $self->shop_part->part_id, 'prices.'.pricegroup_id => $price_src_id], with_objects => ['prices'],limit => 1)->[0];
+    #my $part       = SL::DB::Manager::Part->find_by( id => $self->shop_part->part_id, 'prices.'.pricegroup_id => $price_src_id );
+    my $pricegrp   = SL::DB::Manager::Pricegroup->find_by( id => $price_src_id )->pricegroup;
+    $price         = $part->prices->[0]->price;
+    $price_src_str = $pricegrp;
+  }
+  return($price,$price_src_str);
+}
+
+sub check_auth {
+  $::auth->assert('shop_part_edit');
+}
+
+sub init_shop_part {
+  if ($::form->{shop_part_id}) {
+    SL::DB::Manager::ShopPart->find_by(id => $::form->{shop_part_id});
+  } else {
+    SL::DB::ShopPart->new(shop_id => $::form->{shop_id}, part_id => $::form->{part_id});
+  };
+}
+
+sub init_file {
+  my $file = $::form->{id} ? SL::DB::File->new(id => $::form->{id})->load : SL::DB::File->new;
+  return $file;
+}
+
+sub init_shops {
+  SL::DB::Shop->shops_dd;
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+
+=head1 NAME
+
+SL::Controller::ShopPart - Controller for managing ShopParts
+
+=head1 SYNOPSIS
+
+ShopParts are configured in a tab of the corresponding part.
+
+=head1 ACTIONS
+
+=over 4
+
+=item C<action_update_shop>
+
+To be called from the "Update" button of the shoppart, for manually syncing/upload one part with its shop. Calls some ClientJS functions to modifiy original page.
+
+=item C<action_show_files>
+
+
+
+=item C<action_ajax_delete_file>
+
+
+
+=item C<action_get_categories>
+
+
+
+=item C<action_show_price_n_pricesource>
+
+
+
+=item C<action_show_stock>
+
+
+
+=item C<action_get_n_write_categories>
+
+Can be used to sync the categories of a shoppart with the categories from online.
+
+=item C<action_save_categories>
+
+The ShopwareConnector works with the CategoryID @categories[x][0] in others/new Connectors it must be tested
+Each assigned categorie is saved with id,categorie_name an multidimensional array and could be expanded with categoriepath or what is needed
+
+=item C<action_reorder>
+
+
+
+=item C<action_upload_status>
+
+
+
+=item C<action_mass_upload>
+
+
+
+=item C<action_update>
+
+
+
+=item C<create_or_update>
+
+
+
+=item C<render_shop_part_edit_dialog>
+
+when self->shop_part is called in template, it will be an existing shop_part with id,
+or a new shop_part with only part_id and shop_id set
+
+=item C<add_javascripts>
+
+
+=item C<load_pricesources>
+
+the price sources to use for the article: sellprice, lastcost,
+listprice, or one of the pricegroups. It overwrites the default pricesource from the shopconfig.
+TODO: implement valid pricerules for the article
+
+=item C<get_price_n_pricesource>
+
+
+=item C<check_auth>
+
+
+=item C<init_shop_part>
+
+
+=item C<init_file>
+
+
+=item C<init_shops>
+
+data for drop down filter options
+
+=back
+
+=head1 TODO
+
+CheckAuth
+Pricesrules, pricessources aren't fully implemented yet.
+
+=head1 AUTHORS
+
+G. Richardson E<lt>information@kivitendo-premium.deE<gt>
+W. Hahn E<lt>wh@futureworldsearch.netE<gt>
+
+=cut
diff --git a/SL/Controller/SimpleSystemSetting.pm b/SL/Controller/SimpleSystemSetting.pm
new file mode 100644 (file)
index 0000000..f23edbf
--- /dev/null
@@ -0,0 +1,529 @@
+package SL::Controller::SimpleSystemSetting;
+
+use strict;
+use utf8;
+
+use parent qw(SL::Controller::Base);
+
+use SL::Helper::Flash;
+use SL::Locale::String;
+use SL::DB::Default;
+use SL::System::Process;
+
+use Rose::Object::MakeMethods::Generic (
+  scalar                  => [ qw(type config) ],
+  'scalar --get_set_init' => [ qw(defaults object all_objects class manager_class list_attributes list_url supports_reordering) ],
+);
+
+__PACKAGE__->run_before('check_type_and_auth');
+__PACKAGE__->run_before('setup_javascript', only => [ qw(add create edit update delete) ]);
+
+# Make locales.pl happy: $self->render("simple_system_setting/_default_form")
+
+my %supported_types = (
+  bank_account => {
+    # Make locales.pl happy: $self->render("simple_system_setting/_bank_account_form")
+    class  => 'BankAccount',
+    titles => {
+      list => t8('Bank accounts'),
+      add  => t8('Add bank account'),
+      edit => t8('Edit bank account'),
+    },
+    list_attributes => [
+      { method => 'name',                                      title => t8('Name'), },
+      { method => 'iban',                                      title => t8('IBAN'), },
+      { method => 'bank',                                      title => t8('Bank'), },
+      { method => 'bank_code',                                 title => t8('Bank code'), },
+      { method => 'bank_account_id',                           title => t8('Bank Account Id Number (Swiss)'), },
+      { method => 'bic',                                       title => t8('BIC'), },
+      {                                                        title => t8('Use for Factur-X/ZUGFeRD'), formatter => sub { $_[0]->use_for_zugferd ? t8('yes') : t8('no') } },
+      {                                                        title => t8('Use for Swiss QR-Bill'), formatter => sub { $_[0]->use_for_qrbill ? t8('yes') : t8('no') } },
+      { method => 'reconciliation_starting_date_as_date',      title => t8('Date'),    align => 'right' },
+      { method => 'reconciliation_starting_balance_as_number', title => t8('Balance'), align => 'right' },
+    ],
+  },
+
+  business => {
+    # Make locales.pl happy: $self->render("simple_system_setting/_business_form")
+    class  => 'Business',
+    titles => {
+      list => t8('Businesses'),
+      add  => t8('Add business'),
+      edit => t8('Edit business'),
+    },
+    list_attributes => [
+      { method => 'description',         title => t8('Description'), },
+      {                                  title => t8('Discount'), formatter => sub { $_[0]->discount_as_percent . ' %' }, align => 'right' },
+      { method => 'customernumberinit',  title => t8('Customernumberinit'), },
+    ],
+  },
+
+  contact_department => {
+    class  => 'ContactDepartment',
+    auth   => 'config',
+    titles => {
+      list => t8('Contact Departments'),
+      add  => t8('Add department'),
+      edit => t8('Edit department'),
+    },
+  },
+
+  contact_title => {
+    class  => 'ContactTitle',
+    auth   => 'config',
+    titles => {
+      list => t8('Contact Titles'),
+      add  => t8('Add title'),
+      edit => t8('Edit title'),
+    },
+  },
+
+  department => {
+    class  => 'Department',
+    titles => {
+      list => t8('Departments'),
+      add  => t8('Add department'),
+      edit => t8('Edit department'),
+    },
+  },
+
+  greeting => {
+    class  => 'Greeting',
+    auth   => 'config',
+    titles => {
+      list => t8('Greetings'),
+      add  => t8('Add greeting'),
+      edit => t8('Edit greeting'),
+    },
+  },
+
+  language => {
+    # Make locales.pl happy: $self->render("simple_system_setting/_language_form")
+    class  => 'Language',
+    titles => {
+      list => t8('Languages'),
+      add  => t8('Add language'),
+      edit => t8('Edit language'),
+    },
+    list_attributes => [
+      { method => 'description',   title => t8('Description'), },
+      { method => 'template_code', title => t8('Template Code'), },
+      { method => 'article_code',  title => t8('Article Code'), },
+      {                            title => t8('Number Format'), formatter => sub { $_[0]->output_numberformat || t8('use program settings') } },
+      {                            title => t8('Date Format'),   formatter => sub { $_[0]->output_dateformat   || t8('use program settings') } },
+      {                            title => t8('Long Dates'),    formatter => sub { $_[0]->output_longdates ? t8('yes') : t8('no') } },
+      {                            title => t8('Obsolete'),      formatter => sub { $_[0]->obsolete  ? t8('yes') : t8('no') } },
+    ],
+  },
+
+  part_classification => {
+    # Make locales.pl happy: $self->render("simple_system_setting/_part_classification_form")
+    class  => 'PartClassification',
+    titles => {
+      list => t8('Part classifications'),
+      add  => t8('Add part classification'),
+      edit => t8('Edit part classification'),
+    },
+    list_attributes => [
+      { title => t8('Description'),       formatter => sub { t8($_[0]->description) } },
+      { title => t8('Type abbreviation'), formatter => sub { t8($_[0]->abbreviation) } },
+      { title => t8('Used for Purchase'), formatter => sub { $_[0]->used_for_purchase ? t8('yes') : t8('no') } },
+      { title => t8('Used for Sale'),     formatter => sub { $_[0]->used_for_sale     ? t8('yes') : t8('no') } },
+      { title => t8('Report separately'), formatter => sub { $_[0]->report_separate   ? t8('yes') : t8('no') } },
+    ],
+  },
+
+  parts_group => {
+    # Make locales.pl happy: $self->render("simple_system_setting/_parts_group_form")
+    class  => 'PartsGroup',
+    titles => {
+      list => t8('Partsgroups'),
+      add  => t8('Add partsgroup'),
+      edit => t8('Edit partsgroup'),
+    },
+    list_attributes => [
+      { method => 'partsgroup', title => t8('Description') },
+      { method => 'obsolete',   title => t8('Obsolete'), formatter => sub { $_[0]->obsolete ? t8('yes') : t8('no') } },
+    ],
+  },
+
+  price_factor => {
+    # Make locales.pl happy: $self->render("simple_system_setting/_price_factor_form")
+    class  => 'PriceFactor',
+    titles => {
+      list => t8('Price Factors'),
+      add  => t8('Add Price Factor'),
+      edit => t8('Edit Price Factor'),
+    },
+    list_attributes => [
+      { method => 'description',      title => t8('Description') },
+      { method => 'factor_as_number', title => t8('Factor'), align => 'right' },
+    ],
+  },
+
+  pricegroup => {
+    # Make locales.pl happy: $self->render("simple_system_setting/_pricegroup_form")
+    class  => 'Pricegroup',
+    titles => {
+      list => t8('Pricegroups'),
+      add  => t8('Add pricegroup'),
+      edit => t8('Edit pricegroup'),
+    },
+    list_attributes => [
+      { method => 'pricegroup', title => t8('Description') },
+      { method => 'obsolete',   title => t8('Obsolete'), formatter => sub { $_[0]->obsolete ? t8('yes') : t8('no') } },
+    ],
+  },
+
+  project_status => {
+    class  => 'ProjectStatus',
+    titles => {
+      list => t8('Project statuses'),
+      add  => t8('Add project status'),
+      edit => t8('Edit project status'),
+    },
+  },
+
+  project_type => {
+    class  => 'ProjectType',
+    titles => {
+      list => t8('Project types'),
+      add  => t8('Add project type'),
+      edit => t8('Edit project type'),
+    },
+  },
+
+  requirement_spec_acceptance_status => {
+    # Make locales.pl happy: $self->render("simple_system_setting/_requirement_spec_acceptance_status_form")
+    class  => 'RequirementSpecAcceptanceStatus',
+    titles => {
+      list => t8('Acceptance Statuses'),
+      add  => t8('Add acceptance status'),
+      edit => t8('Edit acceptance status'),
+    },
+    list_attributes => [
+      { method => 'name',        title => t8('Name') },
+      { method => 'description', title => t8('Description') },
+    ],
+  },
+
+  requirement_spec_complexity => {
+    class  => 'RequirementSpecComplexity',
+    titles => {
+      list => t8('Complexities'),
+      add  => t8('Add complexity'),
+      edit => t8('Edit complexity'),
+    },
+  },
+
+  requirement_spec_predefined_text => {
+    # Make locales.pl happy: $self->render("simple_system_setting/_requirement_spec_predefined_text_form")
+    class  => 'RequirementSpecPredefinedText',
+    titles => {
+      list => t8('Pre-defined Texts'),
+      add  => t8('Add pre-defined text'),
+      edit => t8('Edit pre-defined text'),
+    },
+    list_attributes => [
+      { method => 'description', title => t8('Description') },
+      { method => 'title',       title => t8('Title') },
+      {                          title => t8('Content'),                 formatter => sub { my $t = $_[0]->text_as_stripped_html; length($t) > 50 ? substr($t, 0, 50) . '…' : $t } },
+      {                          title => t8('Useable for text blocks'), formatter => sub { $_[0]->useable_for_text_blocks ? t8('yes') : t8('no') } },
+      {                          title => t8('Useable for sections'),    formatter => sub { $_[0]->useable_for_sections    ? t8('yes') : t8('no') } },
+    ],
+  },
+
+  requirement_spec_risk => {
+    class  => 'RequirementSpecRisk',
+    titles => {
+      list => t8('Risk levels'),
+      add  => t8('Add risk level'),
+      edit => t8('Edit risk level'),
+    },
+  },
+
+  requirement_spec_status => {
+    # Make locales.pl happy: $self->render("simple_system_setting/_requirement_spec_status_form")
+    class  => 'RequirementSpecStatus',
+    titles => {
+      list => t8('Requirement Spec Statuses'),
+      add  => t8('Add requirement spec status'),
+      edit => t8('Edit requirement spec status'),
+    },
+    list_attributes => [
+      { method => 'name',        title => t8('Name') },
+      { method => 'description', title => t8('Description') },
+    ],
+  },
+
+  requirement_spec_type => {
+    # Make locales.pl happy: $self->render("simple_system_setting/_requirement_spec_type_form")
+    class  => 'RequirementSpecType',
+    titles => {
+      list => t8('Requirement Spec Types'),
+      add  => t8('Add requirement spec type'),
+      edit => t8('Edit requirement spec type'),
+    },
+    list_attributes => [
+      { method => 'description',                  title => t8('Description') },
+      { method => 'section_number_format',        title => t8('Section number format') },
+      { method => 'function_block_number_format', title => t8('Function block number format') },
+    ],
+  },
+
+  time_recording_article => {
+    # Make locales.pl happy: $self->render("simple_system_setting/_time_recording_article_form")
+    class  => 'TimeRecordingArticle',
+    auth   => 'config',
+    titles => {
+      list => t8('Time Recording Articles'),
+      add  => t8('Add time recording article'),
+      edit => t8('Edit time recording article'),
+    },
+    list_attributes => [
+      { title => t8('Article'), formatter => sub { $_[0]->part->displayable_name } },
+    ],
+  },
+
+);
+
+my @default_list_attributes = (
+  { method => 'description', title => t8('Description') },
+);
+
+#
+# actions
+#
+
+sub action_list {
+  my ($self) = @_;
+
+  $self->setup_list_action_bar;
+  $self->render('simple_system_setting/list', title => $self->config->{titles}->{list});
+}
+
+sub action_new {
+  my ($self) = @_;
+
+  $self->object($self->class->new);
+  $self->render_form(title => $self->config->{titles}->{add});
+}
+
+sub action_edit {
+  my ($self) = @_;
+
+  $self->render_form(title => $self->config->{titles}->{edit});
+}
+
+sub action_create {
+  my ($self) = @_;
+
+  $self->object($self->class->new);
+  $self->create_or_update;
+}
+
+sub action_update {
+  my ($self) = @_;
+
+  $self->create_or_update;
+}
+
+sub action_delete {
+  my ($self) = @_;
+
+  if ($self->object->can('orphaned') && !$self->object->orphaned) {
+    flash_later('error', t8('The object is in use and cannot be deleted.'));
+
+  } elsif ( eval { $self->object->delete; 1; } ) {
+    flash_later('info',  t8('The object has been deleted.'));
+
+  } else {
+    flash_later('error', t8('The object is in use and cannot be deleted.'));
+  }
+
+  $self->redirect_to($self->list_url);
+}
+
+sub action_reorder {
+  my ($self) = @_;
+
+  $self->class->reorder_list(@{ $::form->{object_id} || [] });
+  $self->render(\'', { type => 'json' });
+}
+
+#
+# filters
+#
+
+sub check_type_and_auth {
+  my ($self) = @_;
+
+  $self->type($::form->{type});
+  $self->config($supported_types{$self->type}) || die "Unsupported type";
+
+  $::auth->assert($self->config->{auth} || 'config');
+
+  my $pm = (map { s{::}{/}g; "${_}.pm" } $self->class)[0];
+  require $pm;
+
+  my $setup = "setup_" . $self->type;
+  $self->$setup if $self->can($setup);
+
+  1;
+}
+
+sub setup_javascript {
+  $::request->layout->use_javascript("${_}.js") for qw(ckeditor/ckeditor ckeditor/adapters/jquery);
+}
+
+sub init_class               { "SL::DB::"          . $_[0]->config->{class}                  }
+sub init_manager_class       { "SL::DB::Manager::" . $_[0]->config->{class}                  }
+sub init_object              { $_[0]->class->new(id => $::form->{id})->load                  }
+sub init_all_objects         { $_[0]->manager_class->get_all_sorted                          }
+sub init_list_url            { $_[0]->url_for(action => 'list', type => $_[0]->type)         }
+sub init_supports_reordering { $_[0]->class->new->can('reorder_list')                        }
+sub init_defaults            { SL::DB::Default->get                                          }
+
+sub init_list_attributes {
+  my ($self) = @_;
+
+  my $method = "list_attributes_" . $self->type;
+
+  return $self->$method if $self->can($method);
+  return $self->config->{list_attributes} // \@default_list_attributes;
+}
+
+#
+# helpers
+#
+
+sub create_or_update {
+  my ($self) = @_;
+  my $is_new = !$self->object->id;
+
+  my $params = delete($::form->{object}) || { };
+
+  $self->object->assign_attributes(%{ $params });
+
+  my @errors;
+
+  push @errors, $self->object->validate if $self->object->can('validate');
+
+  if (@errors) {
+    flash('error', @errors);
+    return $self->render_form(title => $self->config->{titles}->{$is_new ? 'add' : 'edit'});
+  }
+
+  $self->object->save;
+
+  flash_later('info', $is_new ? t8('The object has been created.') : t8('The object has been saved.'));
+
+  $self->redirect_to($self->list_url);
+}
+
+sub render_form {
+  my ($self, %params) = @_;
+
+  my $sub_form_template = SL::System::Process->exe_dir . '/templates/webpages/simple_system_setting/_' . $self->type . '_form.html';
+
+  $self->setup_render_form_action_bar;
+  $self->render(
+    'simple_system_setting/form',
+    %params,
+    sub_form_template => (-f $sub_form_template ? $self->type : 'default'),
+  );
+}
+
+#
+# type-specific helper functions
+#
+
+sub setup_requirement_spec_acceptance_status {
+  my ($self) = @_;
+
+  no warnings 'once';
+  $self->{valid_names} = \@SL::DB::RequirementSpecAcceptanceStatus::valid_names;
+}
+
+sub setup_requirement_spec_status {
+  my ($self) = @_;
+
+  no warnings 'once';
+  $self->{valid_names} = \@SL::DB::RequirementSpecStatus::valid_names;
+}
+
+sub setup_language {
+  my ($self) = @_;
+
+  $self->{numberformats} = [ '1,000.00', '1000.00', '1.000,00', '1000,00', "1'000.00" ];
+  $self->{dateformats}   = [ qw(mm/dd/yy dd/mm/yy dd.mm.yy yyyy-mm-dd) ];
+}
+
+#
+# action bar
+#
+
+sub setup_list_action_bar {
+  my ($self, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      link => [
+        t8('Add'),
+        link => $self->url_for(action => 'new', type => $self->type),
+      ],
+    );
+  }
+}
+
+sub setup_render_form_action_bar {
+  my ($self) = @_;
+
+  my $is_new         = !$self->object->id;
+  my $can_be_deleted = !$is_new
+                    && (!$self->object->can("orphaned")       || $self->object->orphaned)
+                    && (!$self->object->can("can_be_deleted") || $self->object->can_be_deleted);
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => 'SimpleSystemSetting/' . ($is_new ? 'create' : 'update') } ],
+        checks    => [ 'kivi.validate_form' ],
+        accesskey => 'enter',
+      ],
+
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => 'SimpleSystemSetting/delete' } ],
+        confirm  => t8('Do you really want to delete this object?'),
+        disabled => $is_new          ? t8('This object has not been saved yet.')
+                  : !$can_be_deleted ? t8('The object is in use and cannot be deleted.')
+                  :                    undef,
+      ],
+
+      link => [
+        t8('Abort'),
+        link => $self->list_url,
+      ],
+    );
+  }
+  $::request->layout->add_javascripts('kivi.Validator.js');
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::SimpleSystemSettings — a common CRUD controller for
+various settings in the "System" menu
+
+=head1 AUTHOR
+
+Moritz Bunkus <m.bunkus@linet-services.de>
+
+=cut
index 97c41c6..e15e345 100644 (file)
@@ -5,6 +5,7 @@ use strict;
 use parent qw(SL::Controller::Base);
 
 use SL::Helper::Flash;
+use SL::Locale::String qw(t8);
 use SL::System::TaskServer;
 
 use Rose::Object::MakeMethods::Generic
@@ -25,6 +26,7 @@ sub action_show {
 
   flash('warning', $::locale->text('The task server does not appear to be running.')) if !$self->task_server->is_running;
 
+  $self->setup_show_action_bar;
   $self->render('task_server/show',
                 title               => $::locale->text('Task server status'),
                 last_command_output => $::auth->get_session_value('TaskServer::last_command_output'));
@@ -84,4 +86,26 @@ sub init_task_server {
   return SL::System::TaskServer->new;
 }
 
+sub setup_show_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        $self->task_server->is_running ? t8('Stop (verb)') : t8('Start (verb)'),
+        submit    => [ '#form' ],
+        accesskey => 'enter',
+      ],
+      link => [
+        t8('List of jobs'),
+        link => $self->url_for(controller => 'BackgroundJob', action => 'list'),
+      ],
+      link => [
+        t8('Job history'),
+        link => $self->url_for(controller => 'BackgroundJobHistory', action => 'list'),
+      ],
+    );
+  }
+}
+
 1;
index 60b820d..72c772f 100644 (file)
@@ -27,6 +27,7 @@ sub action_list {
 
   my $taxzones = SL::DB::Manager::TaxZone->get_all_sorted();
 
+  $self->setup_list_action_bar;
   $self->render('taxzones/list',
                 title    => t8('List of tax zones'),
                 TAXZONES => $taxzones);
@@ -42,6 +43,7 @@ sub action_new {
 sub show_form {
   my ($self, %params) = @_;
 
+  $self->setup_show_form_action_bar;
   $self->render('taxzones/form', %params,
                 BUCHUNGSGRUPPEN => SL::DB::Manager::Buchungsgruppe->get_all_sorted);
 }
@@ -72,12 +74,13 @@ sub action_delete {
   # allow deletion of unused tax zones. Will fail, due to database
   # constraints, if tax zone is used anywhere
 
-  my $db = $self->{config}->db;
-  $db->do_transaction(sub {
-        my $taxzone_charts = SL::DB::Manager::TaxzoneChart->get_all(where => [ taxzone_id => $self->config->id ]);
-        foreach my $taxzonechart ( @{$taxzone_charts} ) { $taxzonechart->delete };
-        $self->config->delete();
-        flash_later('info',  $::locale->text('The tax zone has been deleted.'));
+  $self->{config}->db->with_transaction(sub {
+    my $taxzone_charts = SL::DB::Manager::TaxzoneChart->get_all(where => [ taxzone_id => $self->config->id ]);
+    foreach my $taxzonechart ( @{$taxzone_charts} ) { $taxzonechart->delete };
+    $self->config->delete();
+    flash_later('info',  $::locale->text('The tax zone has been deleted.'));
+
+    1;
   }) || flash_later('error', $::locale->text('The tax zone is in use and cannot be deleted.'));
 
   $self->redirect_to(action => 'list');
@@ -121,7 +124,7 @@ sub create_or_update {
   my @errors;
 
   my $db = $self->config->db;
-  $db->do_transaction( sub {
+  if (!$db->with_transaction(sub {
 
     # always allow editing of description and obsolete
     $self->config->assign_attributes( %{$params} ) ;
@@ -146,8 +149,8 @@ sub create_or_update {
         $income_accno  = SL::DB::Manager::Chart->find_by( id => $income_accno_id )  if $income_accno_id;
         $expense_accno = SL::DB::Manager::Chart->find_by( id => $expense_accno_id ) if $expense_accno_id;
 
-        push(@errors, t8('Buchungsgruppe #1 needs a valid income account' , $bg->description)) unless $income_accno;
-        push(@errors, t8('Buchungsgruppe #1 needs a valid expense account', $bg->description)) unless $expense_accno;
+        push(@errors, t8('Booking group #1 needs a valid income account' , $bg->description)) unless $income_accno;
+        push(@errors, t8('Booking group #1 needs a valid expense account', $bg->description)) unless $expense_accno;
 
         my $taxzone_chart = SL::DB::Manager::TaxzoneChart->find_by_or_create(buchungsgruppen_id => $bg->id, taxzone_id => $self->config->id);
         # if taxzonechart doesn't exist an empty new TaxzoneChart object is
@@ -160,9 +163,13 @@ sub create_or_update {
         $taxzone_chart->save;
       }
     }
-  } ) || die @errors ? join("\n", @errors) . "\n" : $db->error . "\n";
-         # die with rollback of taxzone save if saving of any of the taxzone_charts fails
-         # only show the $db->error if we haven't already identified the likely error ourselves
+
+    1;
+  })) {
+    die @errors ? join("\n", @errors) . "\n" : $db->error . "\n";
+    # die with rollback of taxzone save if saving of any of the taxzone_charts fails
+    # only show the $db->error if we haven't already identified the likely error ourselves
+  }
 
   flash_later('info', $is_new ? t8('The taxzone has been created.') : t8('The taxzone has been saved.'));
   $self->redirect_to(action => 'list');
@@ -174,4 +181,53 @@ sub create_or_update {
 
 sub init_defaults { SL::DB::Default->get };
 
+#
+# helpers
+#
+
+sub setup_show_form_action_bar {
+  my ($self) = @_;
+
+  my $is_new = !$self->config->id;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => 'Taxzones/' . ($is_new ? 'create' : 'update') } ],
+        checks    => [ 'kivi.validate_form' ],
+        accesskey => 'enter',
+      ],
+
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => 'Taxzones/delete' } ],
+        confirm  => t8('Do you really want to delete this object?'),
+        disabled => $is_new                  ? t8('This object has not been saved yet.')
+                  : !$self->config->orphaned ? t8('The object is in use and cannot be deleted.')
+                  :                            undef,
+      ],
+
+      link => [
+        t8('Abort'),
+        link => $self->url_for(action => 'list'),
+      ],
+    );
+  }
+  $::request->layout->add_javascripts('kivi.Validator.js');
+}
+
+sub setup_list_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      link => [
+        t8('Add'),
+        link => $self->url_for(action => 'new'),
+      ],
+    );
+  }
+}
+
 1;
diff --git a/SL/Controller/TimeRecording.pm b/SL/Controller/TimeRecording.pm
new file mode 100644 (file)
index 0000000..930f346
--- /dev/null
@@ -0,0 +1,462 @@
+package SL::Controller::TimeRecording;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+use DateTime;
+use English qw(-no_match_vars);
+use List::Util qw(sum0);
+use POSIX qw(strftime);
+
+use SL::Controller::Helper::GetModels;
+use SL::Controller::Helper::ReportGenerator;
+use SL::Controller::Helper::ReportGenerator::ControlRow qw(make_control_row);
+use SL::DB::Customer;
+use SL::DB::Employee;
+use SL::DB::Order;
+use SL::DB::Part;
+use SL::DB::Project;
+use SL::DB::TimeRecording;
+use SL::DB::TimeRecordingArticle;
+use SL::Helper::Flash qw(flash);
+use SL::Helper::Number qw(_round_number _parse_number _round_total);
+use SL::Helper::UserPreferences::TimeRecording;
+use SL::Locale::String qw(t8);
+use SL::Presenter::Tag qw(checkbox_tag);
+use SL::ReportGenerator;
+
+use Rose::Object::MakeMethods::Generic
+(
+# scalar                  => [ qw() ],
+ 'scalar --get_set_init' => [ qw(time_recording models all_employees all_time_recording_articles all_orders can_view_all can_edit_all use_duration) ],
+);
+
+
+# safety
+__PACKAGE__->run_before('check_auth');
+__PACKAGE__->run_before('check_auth_edit',     only => [ qw(edit save delete) ]);
+__PACKAGE__->run_before('check_auth_edit_all', only => [ qw(mark_as_booked) ]);
+
+
+my %sort_columns = (
+  date         => t8('Date'),
+  start_time   => t8('Start'),
+  end_time     => t8('End'),
+  order        => t8('Sales Order'),
+  customer     => t8('Customer'),
+  part         => t8('Article'),
+  project      => t8('Project'),
+  description  => t8('Description'),
+  staff_member => t8('Mitarbeiter'),
+  duration     => t8('Duration'),
+  booked       => t8('Booked'),
+);
+
+#
+# actions
+#
+
+sub action_list {
+  my ($self, %params) = @_;
+
+  $::form->{filter} //=  {
+    staff_member_id => SL::DB::Manager::Employee->current->id,
+    "date:date::ge" => DateTime->today_local->add(weeks => -2)->to_kivitendo,
+  };
+
+  $self->setup_list_action_bar;
+  $self->make_filter_summary;
+  $self->prepare_report;
+
+  my $objects = $self->models->get;
+
+  my $total   = sum0 map { _round_total($_->duration_in_hours) } @$objects;
+  my $total_h = int($total);
+  my $total_m = int($total * 60.0 + 0.5) % 60;
+  my $total_s = sprintf('%d:%02d', $total_h, $total_m);
+
+  push @$objects, make_control_row("separator");
+  push @$objects, make_control_row("data",
+                                   row => {
+                                     map( { $_ => {class => 'listtotal'} } keys %{$self->{report}->{columns}} ),
+                                     description => {data => t8('Total'), class => 'listtotal'},
+                                     duration    => {data => $total_s,    class => 'listtotal'}
+                                   });
+
+  $self->report_generator_list_objects(report => $self->{report}, objects => $objects);
+}
+
+sub action_mark_as_booked {
+  my ($self) = @_;
+
+  if (scalar @{ $::form->{ids} }) {
+    SL::DB::Manager::TimeRecording->update_all(
+      set   => { booked => 1              },
+      where => [ id     => $::form->{ids} ]
+    );
+  }
+
+  $self->redirect_to(safe_callback());
+}
+
+sub action_edit {
+  my ($self) = @_;
+
+  $::request->{layout}->use_javascript("${_}.js") for qw(kivi.TimeRecording ckeditor/ckeditor ckeditor/adapters/jquery kivi.Validator);
+
+  if ($self->use_duration) {
+    flash('warning', t8('This entry is using start and end time. This information will be overwritten on saving.')) if !$self->time_recording->is_duration_used;
+  } else {
+    flash('warning', t8('This entry is using date and duration. This information will be overwritten on saving.'))  if $self->time_recording->is_duration_used;
+  }
+
+  if ($self->time_recording->start_time) {
+    $self->{start_date} = $self->time_recording->start_time->to_kivitendo;
+    $self->{start_time} = $self->time_recording->start_time->to_kivitendo_time;
+  }
+  if ($self->time_recording->end_time) {
+    $self->{end_date}   = $self->time_recording->end_time->to_kivitendo;
+    $self->{end_time}   = $self->time_recording->end_time->to_kivitendo_time;
+  }
+
+  my $inputs_to_disable = $self->get_inputs_to_disable;
+
+  $self->setup_edit_action_bar;
+
+  $self->render('time_recording/form',
+                title             => t8('Time Recording'),
+                inputs_to_disable => $inputs_to_disable,
+  );
+}
+
+sub action_save {
+  my ($self) = @_;
+
+  if ($self->use_duration) {
+    $self->time_recording->start_time(undef);
+    $self->time_recording->end_time(undef);
+  }
+
+  my @errors = $self->time_recording->validate;
+  if (@errors) {
+    $::form->error(t8('Saving the time recording entry failed: #1', join '<br>', @errors));
+    return;
+  }
+
+  if ( !eval { $self->time_recording->save; 1; } ) {
+    $::form->error(t8('Saving the time recording entry failed: #1', $EVAL_ERROR));
+    return;
+  }
+
+  $self->redirect_to(safe_callback());
+}
+
+sub action_delete {
+  my ($self) = @_;
+
+  $self->time_recording->delete;
+
+  $self->redirect_to(safe_callback());
+}
+
+sub action_ajaj_get_order_info {
+
+  my $order = SL::DB::Order->new(id => $::form->{id})->load;
+  my $data  = { customer => { id    => $order->customer_id,
+                              value => $order->customer->displayable_name,
+                              type  => 'customer'
+                },
+                project => { id     =>  $order->globalproject_id,
+                             value  => ($order->globalproject_id ? $order->globalproject->displayable_name : undef),
+                },
+  };
+
+  $_[0]->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
+}
+
+sub action_ajaj_get_project_info {
+
+  my $project = SL::DB::Project->new(id => $::form->{id})->load;
+
+  my $data;
+  if ($project->customer_id) {
+    $data = { customer => { id    => $project->customer_id,
+                            value => $project->customer->displayable_name,
+                            type  => 'customer'
+                          },
+    };
+  }
+
+  $_[0]->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
+}
+
+sub init_time_recording {
+  my ($self) = @_;
+
+  my $is_new         = !$::form->{id};
+  my $time_recording = !$is_new            ? SL::DB::TimeRecording->new(id => $::form->{id})->load
+                     : $self->use_duration ? SL::DB::TimeRecording->new(date => DateTime->today_local)
+                     :                       SL::DB::TimeRecording->new(start_time => DateTime->now_local);
+
+  my %attributes = %{ $::form->{time_recording} || {} };
+
+  if ($self->use_duration) {
+    if (exists $::form->{duration_h} || exists $::form->{duration_m}) {
+      $attributes{duration} = _round_number(_parse_number($::form->{duration_h}) * 60 + _parse_number($::form->{duration_m}), 0);
+    }
+
+  } else {
+    foreach my $type (qw(start end)) {
+      if ($::form->{$type . '_date'}) {
+        my $date = DateTime->from_kivitendo($::form->{$type . '_date'});
+        $attributes{$type . '_time'} = $date->clone;
+        if ($::form->{$type . '_time'}) {
+          my ($hour, $min) = split ':', $::form->{$type . '_time'};
+          $attributes{$type . '_time'}->set_hour($hour)  if $hour;
+          $attributes{$type . '_time'}->set_minute($min) if $min;
+        }
+      }
+    }
+  }
+
+  # do not overwrite staff member if you do not have the right
+  delete $attributes{staff_member_id}                                     if !$_[0]->can_edit_all;
+  $attributes{staff_member_id} ||= SL::DB::Manager::Employee->current->id if $is_new;
+
+  $attributes{employee_id}       = SL::DB::Manager::Employee->current->id;
+
+  $time_recording->assign_attributes(%attributes);
+
+  return $time_recording;
+}
+
+sub init_can_view_all {
+  $::auth->assert('time_recording_show_all', 1) || $::auth->assert('time_recording_edit_all', 1)
+}
+
+sub init_can_edit_all {
+  $::auth->assert('time_recording_edit_all', 1)
+}
+
+sub init_models {
+  my ($self) = @_;
+
+  my @where;
+  push @where, (staff_member_id => SL::DB::Manager::Employee->current->id) if !$self->can_view_all;
+
+  SL::Controller::Helper::GetModels->new(
+    controller     => $_[0],
+    sorted         => \%sort_columns,
+    disable_plugin => 'paginated',
+    query          => \@where,
+    with_objects   => [ 'customer', 'part', 'project', 'staff_member', 'employee', 'order' ],
+  );
+}
+
+sub init_all_employees {
+  SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]);
+}
+
+sub init_all_time_recording_articles {
+  my $selectable_parts = SL::DB::Manager::TimeRecordingArticle->get_all_sorted(
+    query        => [or => [ 'part.obsolete' => 0, 'part.obsolete' => undef ]],
+    with_objects => ['part']);
+
+  my $res              = [ map { {id => $_->part_id, description => $_->part->displayable_name} } @$selectable_parts];
+  my $curr_id          = $_[0]->time_recording->part_id;
+
+  if ($curr_id && !grep { $curr_id == $_->{id} } @$res) {
+    unshift @$res, {id => $curr_id, description => $_[0]->time_recording->part->displayable_name};
+  }
+
+  return $res;
+}
+
+sub init_all_orders {
+  my $orders = SL::DB::Manager::Order->get_all(query => [or             => [ closed    => 0, closed    => undef, id => $_[0]->time_recording->order_id ],
+                                                         or             => [ quotation => 0, quotation => undef ],
+                                                         '!customer_id' => undef]);
+  return [ map { [$_->id, sprintf("%s %s", $_->number, $_->customervendor->name) ] } sort { $a->number <=> $b->number } @{$orders||[]} ];
+}
+
+sub init_use_duration {
+  return SL::Helper::UserPreferences::TimeRecording->new()->get_use_duration();
+}
+
+sub check_auth {
+  $::auth->assert('time_recording');
+}
+
+sub check_auth_edit {
+  my ($self) = @_;
+
+  if (!$self->can_edit_all && ($self->time_recording->staff_member_id != SL::DB::Manager::Employee->current->id)) {
+    $::form->error(t8('You do not have permission to access this entry.'));
+  }
+}
+
+sub check_auth_edit_all {
+  my ($self) = @_;
+
+  $::auth->assert('time_recording_edit_all');
+}
+
+sub prepare_report {
+  my ($self) = @_;
+
+  my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
+  $self->{report} = $report;
+
+  my @columns  = qw(ids date start_time end_time order customer project part description staff_member duration booked);
+
+  my %column_defs = (
+    ids          => { raw_header_data => checkbox_tag("", id => "check_all", checkall  => "[data-checkall=1]"),
+                      align           => 'center',
+                      raw_data        => sub { $_[0]->booked ? '' : checkbox_tag("ids[]", value => $_[0]->id, "data-checkall" => 1) }   },
+    date         => { text => t8('Date'),         sub => sub { $_[0]->date_as_date },
+                      obj_link => sub { $self->url_for(action => 'edit', 'id' => $_[0]->id, callback => $self->models->get_callback) }  },
+    start_time   => { text => t8('Start'),        sub => sub { $_[0]->start_time_as_timestamp },
+                      obj_link => sub { $self->url_for(action => 'edit', 'id' => $_[0]->id, callback => $self->models->get_callback) }  },
+    end_time     => { text => t8('End'),          sub => sub { $_[0]->end_time_as_timestamp },
+                      obj_link => sub { $self->url_for(action => 'edit', 'id' => $_[0]->id, callback => $self->models->get_callback) }  },
+    order        => { text => t8('Sales Order'),  sub => sub { $_[0]->order && $_[0]->order->number } },
+    customer     => { text => t8('Customer'),     sub => sub { $_[0]->customer->displayable_name } },
+    part         => { text => t8('Article'),      sub => sub { $_[0]->part && $_[0]->part->displayable_name } },
+    project      => { text => t8('Project'),      sub => sub { $_[0]->project && $_[0]->project->full_description(sytle => 'both') } },
+    description  => { text => t8('Description'),  sub => sub { $_[0]->description_as_stripped_html },
+                      raw_data => sub { $_[0]->description_as_restricted_html }, # raw_data only used for html(?)
+                      obj_link => sub { $self->url_for(action => 'edit', 'id' => $_[0]->id, callback => $self->models->get_callback) }  },
+    staff_member => { text => t8('Mitarbeiter'),  sub => sub { $_[0]->staff_member->safe_name } },
+    duration     => { text => t8('Duration'),     sub => sub { $_[0]->duration_as_duration_string },
+                      align => 'right'},
+    booked       => { text => t8('Booked'),       sub => sub { $_[0]->booked ? t8('Yes') : t8('No') } },
+  );
+
+  if (!$self->can_edit_all) {
+    @columns = grep {'ids' ne $_} @columns;
+    delete $column_defs{ids};
+  }
+
+  my $title        = t8('Time Recordings');
+  $report->{title} = $title;    # for browser titlebar (title-tag)
+
+  $report->set_options(
+    controller_class      => 'TimeRecording',
+    std_column_visibility => 1,
+    output_format         => 'HTML',
+    title                 => $title, # for heading
+    allow_pdf_export      => 1,
+    allow_csv_export      => 1,
+  );
+
+  $report->set_columns(%column_defs);
+  $report->set_column_order(@columns);
+  $report->set_export_options(qw(list filter));
+  $report->set_options_from_form;
+
+  $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
+  $self->models->add_additional_url_params(filter => $::form->{filter});
+  $self->models->finalize;
+  $self->models->set_report_generator_sort_options(report => $report, sortable_columns => [keys %sort_columns]);
+
+  $report->set_options(
+    raw_top_info_text    => $self->render('time_recording/report_top',    { output => 0 }),
+    raw_bottom_info_text => $self->render('time_recording/report_bottom', { output => 0 }, models => $self->models),
+    attachment_basename  => t8('time_recordings') . strftime('_%Y%m%d', localtime time),
+  );
+}
+
+sub make_filter_summary {
+  my ($self) = @_;
+
+  my $filter = $::form->{filter} || {};
+  my @filter_strings;
+
+  my $staff_member = $filter->{staff_member_id} ? SL::DB::Employee->new(id => $filter->{staff_member_id})->load->safe_name                         : '';
+  my $project      = $filter->{project_id}      ? SL::DB::Project->new (id => $filter->{project_id})     ->load->full_description(sytle => 'both') : '';
+
+  my @filters = (
+    [ $filter->{"date:date::ge"},                              t8('From Date')       ],
+    [ $filter->{"date:date::le"},                              t8('To Date')         ],
+    [ $filter->{"customer"}->{"name:substr::ilike"},           t8('Customer')        ],
+    [ $filter->{"customer"}->{"customernumber:substr::ilike"}, t8('Customer Number') ],
+    [ $filter->{"order"}->{"ordnumber:substr::ilike"},         t8('Order Number')    ],
+    [ $project,                                                t8('Project')         ],
+    [ $filter->{"description:substr::ilike"},                  t8('Description')     ],
+    [ $staff_member,                                           t8('Mitarbeiter')     ],
+  );
+
+  for (@filters) {
+    push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
+  }
+
+  $self->{filter_summary} = join ', ', @filter_strings;
+}
+
+sub setup_list_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#filter_form', { action => 'TimeRecording/list' } ],
+        accesskey => 'enter',
+      ],
+      combobox => [
+        action => [
+          t8('Actions'),
+          only_if => $self->can_edit_all,
+        ],
+        action => [
+          t8('Mark as booked'),
+          submit  => [ '#form', { action => 'TimeRecording/mark_as_booked', callback => $self->models->get_callback } ],
+          checks  => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ],
+          confirm => $::locale->text('Do you really want to mark the selected entries as booked?'),
+          only_if => $self->can_edit_all,
+        ],
+      ],
+      action => [
+        t8('Add'),
+        link => $self->url_for(action => 'edit', callback => $self->models->get_callback),
+      ],
+    );
+  }
+}
+
+sub setup_edit_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit => [ '#form', { action => 'TimeRecording/save' } ],
+        checks => [ 'kivi.validate_form' ],
+      ],
+      action => [
+        t8('Delete'),
+        submit  => [ '#form', { action => 'TimeRecording/delete' } ],
+        only_if => $self->time_recording->id,
+      ],
+      action => [
+        t8('Cancel'),
+        link  => $self->url_for(safe_callback()),
+      ],
+    );
+  }
+}
+
+sub safe_callback {
+  $::form->{callback} || (action => 'list')
+}
+
+sub get_inputs_to_disable {
+  my ($self) = @_;
+
+  return [qw(customer project)]  if $self->time_recording->order_id;
+  return [qw(customer)]          if $self->time_recording->project_id && $self->time_recording->project->customer_id;
+}
+
+
+1;
diff --git a/SL/Controller/TopQuickSearch.pm b/SL/Controller/TopQuickSearch.pm
new file mode 100644 (file)
index 0000000..4d0aa94
--- /dev/null
@@ -0,0 +1,213 @@
+package SL::Controller::TopQuickSearch;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+use SL::ClientJS;
+use SL::JSON;
+use SL::Locale::String qw(t8);
+use SL::Helper::UserPreferences;
+
+use Rose::Object::MakeMethods::Generic (
+ 'scalar --get_set_init' => [ qw(module js) ],
+);
+
+my @available_modules = (
+  'SL::Controller::TopQuickSearch::Article',
+  'SL::Controller::TopQuickSearch::Part',
+  'SL::Controller::TopQuickSearch::Service',
+  'SL::Controller::TopQuickSearch::Assembly',
+  'SL::Controller::TopQuickSearch::Assortment',
+  'SL::Controller::TopQuickSearch::Contact',
+  'SL::Controller::TopQuickSearch::SalesQuotation',
+  'SL::Controller::TopQuickSearch::SalesOrder',
+  'SL::Controller::TopQuickSearch::SalesDeliveryOrder',
+  'SL::Controller::TopQuickSearch::RequestForQuotation',
+  'SL::Controller::TopQuickSearch::PurchaseOrder',
+  'SL::Controller::TopQuickSearch::PurchaseDeliveryOrder',
+  'SL::Controller::TopQuickSearch::GLTransaction',
+  'SL::Controller::TopQuickSearch::Customer',
+  'SL::Controller::TopQuickSearch::Vendor',
+  'SL::Controller::TopQuickSearch::PhoneNumber',
+);
+my %modules_by_name;
+
+sub action_query_autocomplete {
+  my ($self) = @_;
+
+  my $hashes = $self->module->query_autocomplete;
+
+  $self->render(\ SL::JSON::to_json($hashes), { layout => 0, type => 'json', process => 0 });
+}
+
+sub action_select_autocomplete {
+  my ($self) = @_;
+
+  my $redirect_url = $self->module->select_autocomplete;
+
+  $self->js->redirect_to($redirect_url)->render;
+}
+
+sub action_do_search {
+  my ($self) = @_;
+
+  my $redirect_url = $self->module->do_search;
+
+  if ($redirect_url) {
+    $self->js->redirect_to($redirect_url)
+  }
+
+  $self->js->render;
+}
+
+sub available_modules {
+  my ($self) = @_;
+
+  $self->require_modules;
+
+  map { $_->new } @available_modules;
+}
+
+sub enabled_modules {
+  my $user_prefs = SL::Helper::UserPreferences->new(
+    namespace         => 'TopQuickSearch',
+  );
+
+  my @quick_search_modules;
+  if (my $prefs_val = $user_prefs->get('quick_search_modules')) {
+    @quick_search_modules = split ',', $prefs_val;
+  } else {
+    @quick_search_modules = @{ $::instance_conf->get_quick_search_modules };
+  }
+
+  my %enabled_names = map { $_ => 1 } @quick_search_modules;
+
+  grep {
+    $enabled_names{$_->name}
+  } $_[0]->available_modules
+}
+
+sub active_modules {
+  grep {
+    !$_->auth || $::auth->assert($_->auth, 1)
+  } $_[0]->enabled_modules
+}
+
+sub init_module {
+  my ($self) = @_;
+
+  $self->require_modules;
+
+  die 'Need module' unless $::form->{module};
+
+  die 'Unknown module ' . $::form->{module} unless my $class = $modules_by_name{$::form->{module}};
+
+  $::auth->assert($class->auth) if $class->auth;
+
+  return $class->new;
+}
+
+sub init_js {
+  SL::ClientJS->new(controller => $_[0])
+}
+
+sub require_modules {
+  my ($self) = @_;
+
+  if (!$self->{__modules_required}) {
+    for my $class (@available_modules) {
+      eval "require $class" or die $@;
+      $modules_by_name{ $class->name } = $class;
+    }
+    $self->{__modules_required} = 1;
+  }
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::TopQuickSearch - Framework for pluggable quicksearch fields in the layout top header.
+
+=head1 SYNOPSIS
+
+  use SL::Controller::TopQuickSearch;
+  my $search = SL::Controller::TopQuickSearch->new;
+  $::request->layout->add_javascripts('kivi.QuickSearch.js');
+
+  # in template
+  [%- FOREACH module = search.enabled_modules %]
+  <input type='text' id='top-search-[% module.name %]'>
+  [%- END %]
+
+=head1 DESCRIPTION
+
+This controller provides abstraction for different search plugins, and ensures
+that all follow a common useability scheme.
+
+Modules should be configurable per user, but currently are not. Disabling
+modules can be done by removing them from available_modules or in client_config.
+
+=head1 BEHAVIOUR REQUIREMENTS
+
+=over 4
+
+=item *
+
+A single text input field with the html5 placeholder containing a small
+description of the target will be rendered from the plugin information.
+
+=item *
+
+On typing, the autocompletion must be enabled.
+
+=item *
+
+On C<Enter>, the search should redirect to an appropriate listing of matching
+results.
+
+If only one item matches the result, the plugin should instead redirect
+directly to the matched item.
+
+=item *
+
+Search terms should accept the broadest possible matching, and if possible with
+C<multi> parsing.
+
+=item *
+
+In case nothing is found, a visual indicator should be given, but no actual
+redirect should occur.
+
+=item *
+
+Each search must check rights and must not present a backdoor into data that
+the user should not see.
+
+=item *
+
+By design the search must not try to guess C<exact matches>.
+
+=back
+
+=head1 INTERFACE
+
+The full interface is described in L<SL::Controller::TopQuickSeach::Base>
+
+=head1 TODO
+
+  * user configuration
+
+=head1 BUGS
+
+None yet :)
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut
diff --git a/SL/Controller/TopQuickSearch/Article.pm b/SL/Controller/TopQuickSearch/Article.pm
new file mode 100644 (file)
index 0000000..e689bbe
--- /dev/null
@@ -0,0 +1,103 @@
+package SL::Controller::TopQuickSearch::Article;
+
+use strict;
+use parent qw(Rose::Object);
+
+use SL::Locale::String qw(t8);
+use SL::DB::Part;
+use SL::Controller::Helper::GetModels;
+use SL::Controller::Base;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(parts models part) ],
+);
+
+sub auth { 'part_service_assembly_edit' }
+
+sub name { 'article' }
+
+sub description_config { t8('Articles') }
+
+sub description_field { t8('Articles') }
+
+sub query_autocomplete {
+  my ($self) = @_;
+
+  my $objects = $self->models->get;
+
+  [
+    map {
+     value       => $_->displayable_name,
+     label       => $_->displayable_name,
+     id          => $_->id,
+    }, @$objects
+  ];
+}
+
+sub select_autocomplete {
+  my ($self) = @_;
+  $self->redirect_to_part($::form->{id});
+}
+
+sub do_search {
+  my ($self) = @_;
+
+  my $objects = $self->models->get;
+
+  return !@$objects     ? ()
+       : @$objects == 1 ? $self->redirect_to_part($objects->[0]->id)
+       :                  $self->redirect_to_search($::form->{term});
+}
+
+sub redirect_to_search {
+  my ($self, $term) = @_;
+
+  SL::Controller::Base->new->url_for(
+    controller   => 'ic.pl',
+    action       => 'generate_report',
+    all          => $term,
+    (searchitems => $self->part_type) x!!$self->part_type,
+  );
+}
+
+sub redirect_to_part {
+  my ($self, $term) = @_;
+
+  SL::Controller::Base->new->url_for(
+    controller => 'controller.pl',
+    action     => 'Part/edit',
+    'part.id'  => $term,
+  );
+}
+
+sub part_type {
+  ()
+}
+
+sub init_models {
+  my ($self) = @_;
+
+  SL::Controller::Helper::GetModels->new(
+    controller => $self,
+    model      => 'Part',
+    source     => {
+      filter => {
+        (part_type => $self->part_type) x!!$self->part_type,
+        or => [ obsolete => undef, obsolete => 0 ],
+        'all:substr:multi::ilike' => $::form->{term},
+      },
+    },
+    sorted     => {
+      _default   => {
+        by  => 'partnumber',
+        dir => 1,
+      },
+      partnumber => t8('Partnumber'),
+    },
+    paginated  => {
+      per_page => 10,
+    },
+  )
+}
+
+1;
diff --git a/SL/Controller/TopQuickSearch/Assembly.pm b/SL/Controller/TopQuickSearch/Assembly.pm
new file mode 100644 (file)
index 0000000..da8cbc0
--- /dev/null
@@ -0,0 +1,16 @@
+package SL::Controller::TopQuickSearch::Assembly;
+
+use strict;
+use parent qw(SL::Controller::TopQuickSearch::Article);
+
+use SL::Locale::String qw(t8);
+
+sub name { 'assembly' }
+
+sub description_config { t8('Assemblies') }
+
+sub description_field { t8('Assemblies') }
+
+sub part_type { 'assembly' }
+
+1;
diff --git a/SL/Controller/TopQuickSearch/Assortment.pm b/SL/Controller/TopQuickSearch/Assortment.pm
new file mode 100644 (file)
index 0000000..9de5b84
--- /dev/null
@@ -0,0 +1,16 @@
+package SL::Controller::TopQuickSearch::Assortment;
+
+use strict;
+use parent qw(SL::Controller::TopQuickSearch::Article);
+
+use SL::Locale::String qw(t8);
+
+sub name { 'assortment' }
+
+sub description_config { t8('Assortment') }
+
+sub description_field { t8('Assortment') }
+
+sub part_type { 'assortment' }
+
+1;
diff --git a/SL/Controller/TopQuickSearch/Base.pm b/SL/Controller/TopQuickSearch/Base.pm
new file mode 100644 (file)
index 0000000..1c9e5d3
--- /dev/null
@@ -0,0 +1,87 @@
+package SL::Controller::TopQuickSearch::Base;
+
+use strict;
+use parent qw(Rose::Object);
+
+sub auth { die 'must be overwritten' }
+
+sub name { die 'must be overwritten' }
+
+sub description_config { die 'must be overwritten' }
+
+sub description_field { die 'must be overwritten' }
+
+sub query_autocomplete { die 'must be overwritten' }
+
+sub select_autocomplete { die 'must be overwritten' }
+
+sub do_search { die 'must be overwritten' }
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::TopQuickSearch::Base - base interface class for quick search plugins
+
+=head1 DESCRIPTION
+
+see L<SL::Controller::TopQuickSearch>
+
+=head1 INTERFACE
+
+An implementation must provide these functions.
+
+=over 4
+
+=item C<auth>
+
+Must return a string used for access checks. Empty string or undef will mean
+unrestricted access.
+
+=item C<name>
+
+Internal name, must be plain ASCII.
+
+=item C<description_config>
+
+Localized name used in the configuration.
+
+=item C<description_field>
+
+Localized name used in the search field as hint. Should fit into an input of
+length 20.
+
+=item C<query_autocomplete>
+
+Needs to take C<term> from C<$::form> and must return an arrayref of JSON
+serializable matches fit for jquery autocomplete.
+
+=item C<select_autocomplete>
+
+Needs to take C<id> from C<$::form> and must return a redirect string to be
+used with C<SL::Controller::Base::redirect_to> pointing to a representation of
+the selected object.
+
+=item C<do_search>
+
+Needs to take C<term> from C<$::form> and must return a redirect string to be
+used with C<SL::Controller::Base::redirect_to> pointing to a representation of
+the search results. If the search will display only only one match, it should
+instead return the same result as if that object was selected directly using
+C<select_autocomplete>.
+
+=back
+
+=head1 BUGS
+
+None yet :)
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut
diff --git a/SL/Controller/TopQuickSearch/Contact.pm b/SL/Controller/TopQuickSearch/Contact.pm
new file mode 100644 (file)
index 0000000..e0e915d
--- /dev/null
@@ -0,0 +1,97 @@
+package SL::Controller::TopQuickSearch::Contact;
+
+use strict;
+use parent qw(SL::Controller::TopQuickSearch::Base);
+
+use SL::Controller::CustomerVendor;
+use SL::DB::Vendor;
+use SL::DBUtils qw(selectfirst_array_query like);
+use SL::Locale::String qw(t8);
+
+sub auth { undef }
+
+sub name { 'contact' }
+
+sub description_config { t8('Contact') }
+
+sub description_field { t8('Contacts') }
+
+sub query_autocomplete {
+  my ($self) = @_;
+
+  my $cv_query = <<SQL;
+    SELECT id FROM customer
+    WHERE (obsolete IS NULL)
+       OR (obsolete = FALSE)
+
+    UNION
+
+    SELECT id FROM vendor
+    WHERE (obsolete IS NULL)
+       OR (obsolete = FALSE)
+SQL
+
+  my $result = SL::DB::Manager::Contact->get_all(
+    query => [
+      or => [
+        cp_name      => { ilike => like($::form->{term}) },
+        cp_givenname => { ilike => like($::form->{term}) },
+        cp_email     => { ilike => like($::form->{term}) },
+      ],
+      cp_cv_id => [ \$cv_query ],
+    ],
+    limit => 10,
+    sort_by => 'cp_name',
+  );
+
+  return [
+    map {
+     value       => $_->full_name,
+     label       => $_->full_name,
+     id          => $_->cp_id,
+    }, @$result
+  ];
+}
+
+sub select_autocomplete {
+  my ($self) = @_;
+
+  my $contact = SL::DB::Manager::Contact->find_by(cp_id => $::form->{id});
+
+  SL::Controller::CustomerVendor->new->url_for(action => 'edit', id => $contact->cp_cv_id, contact_id => $contact->cp_id, db => db_for_contact($contact), fragment => 'contacts');
+}
+
+sub do_search {
+  my ($self) = @_;
+
+  my $results = $self->query_autocomplete;
+
+  if (@$results != 1) {
+    return SL::Controller::CustomerVendor->new->url_for(
+      controller      => 'ct.pl',
+      action          => 'list_contacts',
+      'filter.status' => 'active',
+      search_term     => $::form->{term},
+    );
+  } else {
+    $::form->{id} = $results->[0]{id};
+    return $self->select_autocomplete;
+  }
+}
+
+
+sub db_for_contact {
+  my ($contact) = @_;
+
+  my ($customer, $vendor) = selectfirst_array_query($::form, $::form->get_standard_dbh, <<SQL, ($contact->cp_cv_id)x2);
+    SELECT (SELECT COUNT(id) FROM customer WHERE id = ?), (SELECT COUNT(id) FROM vendor WHERE id = ?);
+SQL
+
+  die 'Contact is orphaned, cannot link to it'         if !$customer && !$vendor;
+
+  $customer ? 'customer' : 'vendor';
+}
+
+# TODO: multi search
+
+1;
diff --git a/SL/Controller/TopQuickSearch/Customer.pm b/SL/Controller/TopQuickSearch/Customer.pm
new file mode 100644 (file)
index 0000000..b7c5b39
--- /dev/null
@@ -0,0 +1,21 @@
+package SL::Controller::TopQuickSearch::Customer;
+
+use strict;
+use parent qw(SL::Controller::TopQuickSearch::CustomerVendor);
+use SL::DB::Customer;
+
+use SL::Locale::String qw(t8);
+
+sub auth { undef }
+
+sub name { 'customer' }
+
+sub model { 'Customer' }
+
+sub db { 'customer' }
+
+sub description_config { t8('Customers') }
+
+sub description_field { t8('Customers') }
+
+1;
diff --git a/SL/Controller/TopQuickSearch/CustomerVendor.pm b/SL/Controller/TopQuickSearch/CustomerVendor.pm
new file mode 100644 (file)
index 0000000..0f18b3b
--- /dev/null
@@ -0,0 +1,113 @@
+package SL::Controller::TopQuickSearch::CustomerVendor;
+
+use strict;
+use parent qw(Rose::Object);
+
+use SL::Locale::String qw(t8);
+use SL::Controller::Helper::GetModels;
+use SL::Controller::Base;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(models) ],
+);
+
+# nope. this is only for subclassing
+sub auth { 'NOT ALLOWED' }
+
+sub name { die 'must be overwritten' }
+
+sub description_config { die 'must be overwritten' }
+
+sub description_field { die 'must be overwritten' }
+
+sub query_autocomplete {
+  my ($self) = @_;
+
+  my $objects = $self->models->get;
+
+  [
+    map {
+     value       => $_->displayable_name,
+     label       => $_->displayable_name,
+     id          => $_->id,
+    }, @$objects
+  ];
+}
+
+sub select_autocomplete {
+  $_[0]->redirect_to_object($::form->{id});
+}
+
+sub do_search {
+  my ($self) = @_;
+
+  my $objects = $self->models->get;
+
+  return !@$objects     ? ()
+       : @$objects == 1 ? $self->redirect_to_object($objects->[0]->id)
+       :                  $self->redirect_to_search($::form->{term});
+}
+
+sub redirect_to_search {
+  SL::Controller::Base->new->url_for(
+    controller => 'ct.pl',
+    action     => 'list_names',
+    db         => $_[0]->db,
+    sortdir    => 0,
+    status     => 'all',
+    obsolete   => 'N',
+    all        => $_[1],
+    (map {; "l_$_" => 'Y' } $_[0]->db . "number", qw(name street contact phone zipcode email city country gln)),
+
+  );
+}
+
+sub redirect_to_object {
+  SL::Controller::Base->new->url_for(
+    controller => 'CustomerVendor',
+    action     => 'edit',
+    db         => $_[0]->db,
+    id         => $_[1],
+  );
+}
+
+sub init_models {
+  my ($self) = @_;
+
+  my $cvnumber = $self->db eq 'customer' ? 'customernumber' : 'vendornumber';
+
+  SL::Controller::Helper::GetModels->new(
+    controller => $self,
+    model      => $self->model,
+    source     => {
+      filter => {
+        'all:substr:multi::ilike' => $::form->{term}, # all filter spec is set in SL::DB::Manager::Customer
+        or => [ obsolete => undef, obsolete => 0 ],
+      },
+    },
+    sorted     => {
+      _default   => {
+        by  => $cvnumber,
+        dir => 0,
+      },
+      $cvnumber => $self->db eq 'customer' ? t8('Customer Number') : t8('Vendor Number'),
+    },
+    paginated  => {
+      per_page => 10,
+    },
+  )
+}
+
+sub type {
+  die 'must be overwritten'
+}
+
+sub cv {
+  die 'must be overwritten'
+}
+
+sub model {
+  die 'must be overwritten'
+};
+
+1;
diff --git a/SL/Controller/TopQuickSearch/DeliveryOrder.pm b/SL/Controller/TopQuickSearch/DeliveryOrder.pm
new file mode 100644 (file)
index 0000000..fd2694c
--- /dev/null
@@ -0,0 +1,108 @@
+package SL::Controller::TopQuickSearch::DeliveryOrder;
+
+use strict;
+use parent qw(Rose::Object);
+
+use SL::Locale::String qw(t8);
+use SL::DB::DeliveryOrder;
+use SL::Controller::Helper::GetModels;
+use SL::Controller::Base;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(models) ],
+);
+
+# nope. this is only for subclassing
+sub auth { 'NOT ALLOWED' }
+
+sub name { die 'must be overwritten' }
+
+sub description_config { die 'must be overwritten' }
+
+sub description_field { die 'must be overwritten' }
+
+sub type { die 'must be overwritten' }
+
+sub cv { die 'must be overwritten' }
+
+sub query_autocomplete {
+  my ($self) = @_;
+
+  my $objects = $self->models->get;
+
+  [
+    map {
+     value       => $_->digest,
+     label       => $_->digest,
+     id          => $_->id,
+    }, @$objects
+  ];
+}
+
+sub select_autocomplete {
+  $_[0]->redirect_to_object($::form->{id});
+}
+
+sub do_search {
+  my ($self) = @_;
+
+  my $objects = $self->models->get;
+
+  return !@$objects     ? ()
+       : @$objects == 1 ? $self->redirect_to_object($objects->[0]->id)
+       :                  $self->redirect_to_search($::form->{term});
+}
+
+sub redirect_to_search {
+  SL::Controller::Base->new->url_for(
+    controller   => 'do.pl',
+    action       => 'orders',
+    type         => $_[0]->type,
+    vc           => $_[0]->vc,
+    all          => $_[1],
+    open         => 1,
+    closed       => 1,
+    delivered    => 1,
+    notdelivered => 1,
+    sortdir      => 0,
+    (map {; "l_$_" => 'Y' } qw(donumber transdate cusordnumber reqdate name employee transaction_description)),
+  );
+}
+
+sub redirect_to_object {
+  SL::Controller::Base->new->url_for(
+    controller => 'do.pl',
+    action     => 'edit',
+    type       => $_[0]->type,
+    vc         => $_[0]->vc,
+    id         => $_[1],
+  );
+}
+
+sub init_models {
+  my ($self) = @_;
+
+  SL::Controller::Helper::GetModels->new(
+    controller => $self,
+    model      => 'DeliveryOrder',
+    source     => {
+      filter => {
+        type => $self->type,
+        'all:substr:multi::ilike' => $::form->{term},
+      },
+    },
+    sorted     => {
+      _default   => {
+        by  => 'transdate',
+        dir => 0,
+      },
+      transdate => t8('Date'),
+    },
+    paginated  => {
+      per_page => 10,
+    },
+    with_objects => [ qw(customer vendor orderitems) ]
+  )
+}
+
+1;
diff --git a/SL/Controller/TopQuickSearch/GLTransaction.pm b/SL/Controller/TopQuickSearch/GLTransaction.pm
new file mode 100644 (file)
index 0000000..53d05ea
--- /dev/null
@@ -0,0 +1,116 @@
+package SL::Controller::TopQuickSearch::GLTransaction;
+
+use strict;
+use parent qw(Rose::Object);
+
+use SL::DB::GLTransaction;
+use SL::DB::Invoice;
+use SL::DB::PurchaseInvoice;
+use SL::DB::AccTransaction;
+use SL::Locale::String qw(t8);
+use SL::DBUtils qw(like);
+use List::Util qw(sum);
+
+sub auth { 'general_ledger|gl_transactions|ap_transactions|ar_transactions' }
+
+sub name { 'gl_transaction' }
+
+sub description_config { t8('GL search') }
+
+sub description_field { t8('GL search') }
+
+sub query_autocomplete {
+  my ($self, %params) = @_;
+
+  my $limit = $::form->{limit} || 40; # max number of results per type (AR/AP/GL)
+  my $term  = $::form->{term}  || '';
+
+  my $descriptionquery = { ilike => like($term) };
+  my $referencequery   = { ilike => like($term) };
+  my $apinvnumberquery = { ilike => like($term) };
+  my $namequery        = { ilike => like($term) };
+  my $arinvnumberquery = { ilike => '%' . SL::Util::trim($term) };
+  # ar match is more restrictive. Left fuzzy beginning so it also matches "Storno zu $INVNUMBER"
+  # and numbers like 000123 if you only enter 123.
+  # When used in quicksearch short numbers like 1 or 11 won't match because of the
+  # ajax autocomplete minlimit of 3 characters
+
+  my (@glfilter, @arfilter, @apfilter);
+
+  push( @glfilter, (or => [ description => $descriptionquery, reference => $referencequery ] ) );
+  push( @arfilter, (or => [ invnumber   => $arinvnumberquery, name      => $namequery ] ) );
+  push( @apfilter, (or => [ invnumber   => $apinvnumberquery, name      => $namequery ] ) );
+
+  my $gls = SL::DB::Manager::GLTransaction->get_all(  query => [ @glfilter ], limit => $limit, sort_by => 'transdate DESC');
+  my $ars = SL::DB::Manager::Invoice->get_all(        query => [ @arfilter ], limit => $limit, sort_by => 'transdate DESC', with_objects => [ 'customer' ]);
+  my $aps = SL::DB::Manager::PurchaseInvoice->get_all(query => [ @apfilter ], limit => $limit, sort_by => 'transdate DESC', with_objects => [ 'vendor' ]);
+
+  my $gldata = [
+    map(
+      {
+        {
+           transdate => $_->transdate->ymd(''), # only used for sorting
+           label     => $_->oneline_summary,
+           value     => '',
+           id        => 'gl.pl?action=edit&id=' . $_->id,
+        }
+      }
+      @{$gls}
+    ),
+  ];
+
+  my $ardata = [
+    map(
+      {
+        {
+           transdate => $_->transdate->ymd(''),
+           label     => $_->oneline_summary,
+           value     => "",
+           id        => ($_->invoice ? "is" : "ar" ) . '.pl?action=edit&id=' . $_->id,
+        }
+      }
+      @{$ars}
+    ),
+  ];
+
+  my $apdata = [
+    map(
+      {
+        {
+           transdate => $_->transdate->ymd(''),
+           label     => $_->oneline_summary,
+           value     => "",
+           id        => ($_->invoice ? "ir" : "ap" ) . '.pl?action=edit&id=' . $_->id,
+        }
+      }
+      @{$aps}
+    ),
+  ];
+
+  my $data;
+  push(@{$data},@{$gldata});
+  push(@{$data},@{$ardata});
+  push(@{$data},@{$apdata});
+
+  @$data = reverse sort { $a->{'transdate'} cmp $b->{'transdate'} } @$data;
+
+  $data;
+}
+
+sub select_autocomplete {
+  $::form->{id}
+}
+
+sub do_search {
+  my ($self) = @_;
+
+  my $results = $self->query_autocomplete;
+
+  return @$results == 1
+    ? $results->[0]{id}
+    : undef;
+}
+
+# TODO: result overview page
+
+1;
diff --git a/SL/Controller/TopQuickSearch/OERecord.pm b/SL/Controller/TopQuickSearch/OERecord.pm
new file mode 100644 (file)
index 0000000..d63dee0
--- /dev/null
@@ -0,0 +1,127 @@
+package SL::Controller::TopQuickSearch::OERecord;
+
+use strict;
+use parent qw(Rose::Object);
+
+use SL::Locale::String qw(t8);
+use SL::DB::Order;
+use SL::Controller::Helper::GetModels;
+use SL::Controller::Base;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(models) ],
+);
+
+# nope. this is only for subclassing
+sub auth { 'NOT ALLOWED' }
+
+sub name { die 'must be overwritten' }
+
+sub description_config { die 'must be overwritten' }
+
+sub description_field { die 'must be overwritten' }
+
+sub query_autocomplete {
+  my ($self) = @_;
+
+  my $objects = $self->models->get;
+
+  [
+    map {
+     value       => $_->digest,
+     label       => $_->digest,
+     id          => $_->id,
+    }, @$objects
+  ];
+}
+
+sub select_autocomplete {
+  $_[0]->redirect_to_object($::form->{id});
+}
+
+sub do_search {
+  my ($self) = @_;
+
+  my $objects = $self->models->get;
+
+  return !@$objects     ? ()
+       : @$objects == 1 ? $self->redirect_to_object($objects->[0]->id)
+       :                  $self->redirect_to_search($::form->{term});
+}
+
+sub redirect_to_search {
+  SL::Controller::Base->new->url_for(
+    controller => 'oe.pl',
+    action     => 'orders',
+    type       => $_[0]->type,
+    vc         => $_[0]->vc,
+    all        => $_[1],
+    open       => 1,
+    closed     => 1,
+    sortdir    => 0,
+    (map {; "l_$_" => 'Y' } $_[0]->number, qw(transdate cusordnumber reqdate name employee netamount)),
+  );
+}
+
+sub redirect_to_object {
+  if ($::instance_conf->get_feature_experimental_order) {
+    SL::Controller::Base->new->url_for(
+      controller => 'Order',
+      action     => 'edit',
+      type       => $_[0]->type,
+      id         => $_[1],
+    );
+  } else {
+    SL::Controller::Base->new->url_for(
+      controller => 'oe.pl',
+      action     => 'edit',
+      type       => $_[0]->type,
+      vc         => $_[0]->vc,
+      id         => $_[1],
+    );
+  }
+}
+
+sub type {
+  die 'must be overwritten'
+}
+
+sub cv {
+  die 'must be overwritten'
+}
+
+sub quotation {
+  $_[0]->type !~ /order/
+}
+
+sub number {
+  $_[0]->quotation ? 'quonumber' : 'ordnumber'
+}
+
+sub init_models {
+  my ($self) = @_;
+
+  SL::Controller::Helper::GetModels->new(
+    controller => $self,
+    model      => 'Order',
+    source     => {
+      filter => {
+        type => $self->type,
+        'all:substr:multi::ilike' => $::form->{term},
+      },
+    },
+    sorted     => {
+      _default   => {
+        by  => 'transdate',
+        dir => 0,
+      },
+      transdate => t8('Date'),
+    },
+    paginated  => {
+      per_page => 10,
+    },
+    with_objects => [ qw(customer vendor) ]
+  )
+}
+
+1;
diff --git a/SL/Controller/TopQuickSearch/Part.pm b/SL/Controller/TopQuickSearch/Part.pm
new file mode 100644 (file)
index 0000000..6c81ac5
--- /dev/null
@@ -0,0 +1,16 @@
+package SL::Controller::TopQuickSearch::Part;
+
+use strict;
+use parent qw(SL::Controller::TopQuickSearch::Article);
+
+use SL::Locale::String qw(t8);
+
+sub name { 'part' }
+
+sub description_config { t8('Parts') }
+
+sub description_field { t8('Parts') }
+
+sub part_type { 'part' }
+
+1;
diff --git a/SL/Controller/TopQuickSearch/PhoneNumber.pm b/SL/Controller/TopQuickSearch/PhoneNumber.pm
new file mode 100644 (file)
index 0000000..54431e0
--- /dev/null
@@ -0,0 +1,85 @@
+package SL::Controller::TopQuickSearch::PhoneNumber;
+
+use strict;
+use parent qw(SL::Controller::TopQuickSearch::Base);
+
+use SL::Controller::TopQuickSearch::Customer;
+use SL::Controller::TopQuickSearch::Vendor;
+use SL::DB::Customer;
+use SL::DB::Vendor;
+use SL::Locale::String qw(t8);
+use SL::Util qw(trim);
+
+sub auth { undef }
+
+sub name { 'phone_number' }
+
+sub description_config { t8('All phone numbers') }
+
+sub description_field { t8('All phone numbers') }
+
+sub query_autocomplete {
+  my ($self) = @_;
+
+  my @results;
+  my $search_term = trim($::form->{term});
+  $search_term    =~ s{\p{WSpace}+}{}g;
+  $search_term    = join ' *', split(//, $search_term);
+
+  foreach my $model (qw(Customer Vendor)) {
+    my $manager = 'SL::DB::Manager::' . $model;
+    my $result  = $manager->get_all(
+      query => [ or => [ 'obsolete' => 0, 'obsolete' => undef ],
+                 or => [ phone                     => { imatch => $search_term },
+                         fax                       => { imatch => $search_term },
+                         'contacts.cp_phone1'      => { imatch => $search_term },
+                         'contacts.cp_phone2'      => { imatch => $search_term },
+                         'contacts.cp_fax'         => { imatch => $search_term },
+                         'contacts.cp_mobile1'     => { imatch => $search_term },
+                         'contacts.cp_mobile2'     => { imatch => $search_term },
+                         'contacts.cp_satphone'    => { imatch => $search_term },
+                         'contacts.cp_satfax'      => { imatch => $search_term },
+                         'contacts.cp_privatphone' => { imatch => $search_term },
+                 ] ],
+      with_objects => ['contacts']);
+
+    push @results, map {
+      value => $_->displayable_name,
+      label => $_->displayable_name,
+      id    => lc($model) . '_' . $_->id,
+    }, @$result;
+  }
+
+  return \@results;
+}
+
+sub select_autocomplete {
+  my ($self) = @_;
+
+  if ($::form->{id} =~ m{^(customer|vendor)_(\d+)$}) {
+    my $type      = $1;
+    my $id        = $2;
+    $::form->{id} = $id;
+
+    if ($type eq 'customer') {
+      SL::Controller::TopQuickSearch::Customer->new->select_autocomplete;
+    } elsif ($type eq 'vendor') {
+      SL::Controller::TopQuickSearch::Vendor->new->select_autocomplete;
+    }
+  }
+}
+
+sub do_search {
+  my ($self) = @_;
+
+  my $results = $self->query_autocomplete;
+
+  if (@$results == 1) {
+    $::form->{id} = $results->[0]{id};
+    return $self->select_autocomplete;
+  }
+}
+
+# TODO: result overview page
+
+1;
diff --git a/SL/Controller/TopQuickSearch/PurchaseDeliveryOrder.pm b/SL/Controller/TopQuickSearch/PurchaseDeliveryOrder.pm
new file mode 100644 (file)
index 0000000..e561360
--- /dev/null
@@ -0,0 +1,20 @@
+package SL::Controller::TopQuickSearch::PurchaseDeliveryOrder;
+
+use strict;
+use parent qw(SL::Controller::TopQuickSearch::DeliveryOrder);
+
+use SL::Locale::String qw(t8);
+
+sub auth { 'purchase_delivery_order_edit | purchase_delivery_order_edit' }
+
+sub name { 'purchase_delivery_order' }
+
+sub description_config { t8('Purchase Delivery Orders') }
+
+sub description_field { t8('Purchase Delivery Orders') }
+
+sub type { 'purchase_delivery_order' }
+
+sub vc { 'vendor' }
+
+1;
diff --git a/SL/Controller/TopQuickSearch/PurchaseOrder.pm b/SL/Controller/TopQuickSearch/PurchaseOrder.pm
new file mode 100644 (file)
index 0000000..f64fb83
--- /dev/null
@@ -0,0 +1,20 @@
+package SL::Controller::TopQuickSearch::PurchaseOrder;
+
+use strict;
+use parent qw(SL::Controller::TopQuickSearch::OERecord);
+
+use SL::Locale::String qw(t8);
+
+sub auth { 'purchase_order_edit | purchase_order_view' }
+
+sub name { 'purchase_order' }
+
+sub description_config { t8('Purchase Orders') }
+
+sub description_field { t8('Purchase Orders') }
+
+sub type { 'purchase_order' }
+
+sub vc { 'vendor' }
+
+1;
diff --git a/SL/Controller/TopQuickSearch/RequestForQuotation.pm b/SL/Controller/TopQuickSearch/RequestForQuotation.pm
new file mode 100644 (file)
index 0000000..d957100
--- /dev/null
@@ -0,0 +1,20 @@
+package SL::Controller::TopQuickSearch::RequestForQuotation;
+
+use strict;
+use parent qw(SL::Controller::TopQuickSearch::OERecord);
+
+use SL::Locale::String qw(t8);
+
+sub auth { 'request_quotation_edit | request_quotation_view' }
+
+sub name { 'request_quotation' }
+
+sub description_config { t8('Request Quotations') }
+
+sub description_field { t8('Request Quotations') }
+
+sub type { 'request_quotation' }
+
+sub vc { 'vendor' }
+
+1;
diff --git a/SL/Controller/TopQuickSearch/SalesDeliveryOrder.pm b/SL/Controller/TopQuickSearch/SalesDeliveryOrder.pm
new file mode 100644 (file)
index 0000000..073a9c2
--- /dev/null
@@ -0,0 +1,20 @@
+package SL::Controller::TopQuickSearch::SalesDeliveryOrder;
+
+use strict;
+use parent qw(SL::Controller::TopQuickSearch::DeliveryOrder);
+
+use SL::Locale::String qw(t8);
+
+sub auth { 'sales_delivery_order_edit | sales_delivery_order_view' }
+
+sub name { 'sales_delivery_order' }
+
+sub description_config { t8('Sales Delivery Orders') }
+
+sub description_field { t8('Sales Delivery Orders') }
+
+sub type { 'sales_delivery_order' }
+
+sub vc { 'customer' }
+
+1;
diff --git a/SL/Controller/TopQuickSearch/SalesOrder.pm b/SL/Controller/TopQuickSearch/SalesOrder.pm
new file mode 100644 (file)
index 0000000..8f91e6e
--- /dev/null
@@ -0,0 +1,20 @@
+package SL::Controller::TopQuickSearch::SalesOrder;
+
+use strict;
+use parent qw(SL::Controller::TopQuickSearch::OERecord);
+
+use SL::Locale::String qw(t8);
+
+sub auth { 'sales_order_edit | sales_order_view' }
+
+sub name { 'sales_order' }
+
+sub description_config { t8('Sales Orders') }
+
+sub description_field { t8('Sales Orders') }
+
+sub type { 'sales_order' }
+
+sub vc { 'customer' }
+
+1;
diff --git a/SL/Controller/TopQuickSearch/SalesQuotation.pm b/SL/Controller/TopQuickSearch/SalesQuotation.pm
new file mode 100644 (file)
index 0000000..f7a6b77
--- /dev/null
@@ -0,0 +1,20 @@
+package SL::Controller::TopQuickSearch::SalesQuotation;
+
+use strict;
+use parent qw(SL::Controller::TopQuickSearch::OERecord);
+
+use SL::Locale::String qw(t8);
+
+sub auth { 'sales_quotation_edit | sales_quotation_view' }
+
+sub name { 'sales_quotation' }
+
+sub description_config { t8('Sales Quotations') }
+
+sub description_field { t8('Sales Quotations') }
+
+sub type { 'sales_quotation' }
+
+sub vc { 'customer' }
+
+1;
diff --git a/SL/Controller/TopQuickSearch/Service.pm b/SL/Controller/TopQuickSearch/Service.pm
new file mode 100644 (file)
index 0000000..0bba378
--- /dev/null
@@ -0,0 +1,16 @@
+package SL::Controller::TopQuickSearch::Service;
+
+use strict;
+use parent qw(SL::Controller::TopQuickSearch::Article);
+
+use SL::Locale::String qw(t8);
+
+sub name { 'service' }
+
+sub description_config { t8('Services') }
+
+sub description_field { t8('Services') }
+
+sub part_type { 'service' }
+
+1;
diff --git a/SL/Controller/TopQuickSearch/Vendor.pm b/SL/Controller/TopQuickSearch/Vendor.pm
new file mode 100644 (file)
index 0000000..34e7323
--- /dev/null
@@ -0,0 +1,21 @@
+package SL::Controller::TopQuickSearch::Vendor;
+
+use strict;
+use parent qw(SL::Controller::TopQuickSearch::CustomerVendor);
+use SL::DB::Vendor;
+
+use SL::Locale::String qw(t8);
+
+sub auth { undef }
+
+sub name { 'vendor' }
+
+sub model { 'Vendor' }
+
+sub db { 'vendor' }
+
+sub description_config { t8('Vendors') }
+
+sub description_field { t8('Vendors') }
+
+1;
diff --git a/SL/Controller/YearEndTransactions.pm b/SL/Controller/YearEndTransactions.pm
new file mode 100644 (file)
index 0000000..43b4fee
--- /dev/null
@@ -0,0 +1,516 @@
+package SL::Controller::YearEndTransactions;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use utf8; # Umlauts in hardcoded German default texts
+use DateTime;
+use SL::Locale::String qw(t8);
+use SL::Helper::Flash;
+use SL::DBUtils;
+use Data::Dumper;
+use List::Util qw(sum);
+use SL::ClientJS;
+
+use SL::DB::Chart;
+use SL::DB::GLTransaction;
+use SL::DB::AccTransaction;
+use SL::DB::Employee;
+use SL::DB::Helper::AccountingPeriod qw(get_balance_starting_date get_balance_startdate_method_options);
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(cb_date cb_startdate ob_date) ],
+);
+
+__PACKAGE__->run_before('check_auth');
+
+sub action_form {
+  my ($self) = @_;
+
+  $self->cb_startdate($::locale->parse_date_to_object($self->get_balance_starting_date($self->cb_date)));
+
+  my $defaults         = SL::DB::Default->get;
+  my $carry_over_chart = SL::DB::Manager::Chart->find_by( id => $defaults->carry_over_account_chart_id     );
+  my $profit_chart     = SL::DB::Manager::Chart->find_by( id => $defaults->profit_carried_forward_chart_id );
+  my $loss_chart       = SL::DB::Manager::Chart->find_by( id => $defaults->loss_carried_forward_chart_id   );
+
+  $self->render('yearend/form',
+                title                            => t8('Year-end closing'),
+                carry_over_chart                 => $carry_over_chart,
+                profit_chart                     => $profit_chart,
+                loss_chart                       => $loss_chart,
+                balance_startdate_method_options => get_balance_startdate_method_options(),
+               );
+};
+
+sub action_year_end_bookings {
+  my ($self) = @_;
+
+  $self->_parse_form;
+
+  eval {
+    _year_end_bookings( start_date => $self->cb_startdate,
+                        cb_date    => $self->cb_date,
+                      );
+    1;
+  } or do {
+    $self->js->flash('error', t8('Error while applying year-end bookings!') . ' ' . $@);
+    return $self->js->render;
+  };
+
+  my ($report_data, $profit_loss_sum) = _report(
+                                                cb_date    => $self->cb_date,
+                                                start_date => $self->cb_startdate,
+                                               );
+
+  my $html = $self->render('yearend/_charts', { layout  => 0 , process => 1, output => 0 },
+                           charts          => $report_data,
+                           profit_loss_sum => $profit_loss_sum,
+                          );
+  return $self->js->flash('info', t8('Year-end bookings were successfully completed!'))
+               ->html('#charts', $html)
+               ->render;
+}
+
+sub action_get_start_date {
+  my ($self) = @_;
+
+  my $cb_date = $self->cb_date; # parse from form via init
+  unless ( $self->cb_date ) {
+    return $self->hide('#apply_year_end_bookings_button')
+                ->flash('error', t8('Year-end date missing'))
+                ->render;
+  }
+
+  $self->cb_startdate($::locale->parse_date_to_object($self->get_balance_starting_date($self->cb_date, $::form->{'balance_startdate_method'})));
+
+  # $main::lxdebug->message(0, "found start date: ", $self->cb_startdate->to_kivitendo);
+
+  return $self->js->val('#cb_startdate', $self->cb_startdate->to_kivitendo)
+              ->show('#apply_year_end_bookings_button')
+              ->show('.startdate')
+              ->render;
+}
+
+sub action_update_charts {
+  my ($self) = @_;
+
+  $self->_parse_form;
+
+  my ($report_data, $profit_loss_sum) = _report(
+                                                cb_date   => $self->cb_date,
+                                                start_date => $self->cb_startdate,
+                                               );
+
+  $self->render('yearend/_charts', { layout  => 0 , process => 1 },
+                charts          => $report_data,
+                profit_loss_sum => $profit_loss_sum,
+               );
+}
+
+#
+# helpers
+#
+
+sub _parse_form {
+  my ($self) = @_;
+
+  # parse dates
+  $self->cb_startdate($::locale->parse_date_to_object($self->get_balance_starting_date($self->cb_date)));
+
+  die "cb_date must come after start_date" unless $self->cb_date > $self->cb_startdate;
+}
+
+sub _year_end_bookings {
+  my (%params) = @_;
+
+  my $start_date = delete $params{start_date};
+  my $cb_date    = delete $params{cb_date};
+
+  my $defaults         = SL::DB::Default->get;
+  my $carry_over_chart = SL::DB::Manager::Chart->find_by( id => $defaults->carry_over_account_chart_id     ) // die t8('No carry-over chart configured!');
+  my $profit_chart     = SL::DB::Manager::Chart->find_by( id => $defaults->profit_carried_forward_chart_id ) // die t8('No profit carried forward chart configured!');
+  my $loss_chart       = SL::DB::Manager::Chart->find_by( id => $defaults->loss_carried_forward_chart_id   ) // die t8('No profit and loss carried forward chart configured!');
+
+  my ($report_data, $profit_loss_sum) = _report(
+                                                start_date => $start_date,
+                                                cb_date    => $cb_date,
+                                               );
+
+  # load all charts from report as objects and store them in a hash
+  my @report_chart_ids = map { $_->{chart_id} } @{ $report_data };
+  my %charts_by_id = map { ( $_->id => $_ ) } @{ SL::DB::Manager::Chart->get_all(where => [ id => \@report_chart_ids ]) };
+
+  my @asset_accounts       = grep { $_->{account_type} eq 'asset_account' }       @{ $report_data };
+  my @profit_loss_accounts = grep { $_->{account_type} eq 'profit_loss_account' } @{ $report_data };
+
+  my $ob_date = $cb_date->clone->add(days => 1);
+
+  my ($credit_sum, $debit_sum) = (0,0);
+
+  my $employee_id = SL::DB::Manager::Employee->current->id;
+
+  # rather than having one gl transaction for each asset account, we group all
+  # the debit sums and credit sums for cb and ob bookings, so we will have 4 gl
+  # transactions:
+
+  # * cb for credit
+  # * cb for debit
+  # * ob for credit
+  # * ob for debit
+
+  my $db = SL::DB->client;
+  $db->with_transaction(sub {
+
+    ######### asset accounts ########
+    # need cb and ob transactions
+
+    my $debit_balance  = 0;
+    my $credit_balance = 0;
+
+    my $asset_cb_debit_entry = SL::DB::GLTransaction->new(
+      employee_id    => $employee_id,
+      transdate      => $cb_date,
+      reference      => 'SB ' . $cb_date->year,
+      description    => 'Automatische SB-Buchungen Bestandskonten Soll für ' . $cb_date->year,
+      ob_transaction => 0,
+      cb_transaction => 1,
+      taxincluded    => 0,
+      transactions   => [],
+    );
+    my $asset_ob_debit_entry = SL::DB::GLTransaction->new(
+      employee_id    => $employee_id,
+      transdate      => $ob_date,
+      reference      => 'EB ' . $ob_date->year,
+      description    => 'Automatische EB-Buchungen Bestandskonten Haben für ' . $ob_date->year,
+      ob_transaction => 1,
+      cb_transaction => 0,
+      taxincluded    => 0,
+      transactions   => [],
+    );
+    my $asset_cb_credit_entry = SL::DB::GLTransaction->new(
+      employee_id    => $employee_id,
+      transdate      => $cb_date,
+      reference      => 'SB ' . $cb_date->year,
+      description    => 'Automatische SB-Buchungen Bestandskonten Haben für ' . $cb_date->year,
+      ob_transaction => 0,
+      cb_transaction => 1,
+      taxincluded    => 0,
+      transactions   => [],
+    );
+    my $asset_ob_credit_entry = SL::DB::GLTransaction->new(
+      employee_id    => $employee_id,
+      transdate      => $ob_date,
+      reference      => 'EB ' . $ob_date->year,
+      description    => 'Automatische EB-Buchungen Bestandskonten Soll für ' . $ob_date->year,
+      ob_transaction => 1,
+      cb_transaction => 0,
+      taxincluded    => 0,
+      transactions   => [],
+    );
+
+    foreach my $asset_account ( @asset_accounts ) {
+      next if $asset_account->{amount_with_cb} == 0;
+      my $ass_acc = $charts_by_id{ $asset_account->{chart_id} };
+
+      if ( $asset_account->{amount_with_cb} < 0 ) {
+        # $main::lxdebug->message(0, sprintf("adding accno %s with balance %s to debit", $asset_account->{accno}, $asset_account->{amount_with_cb}));
+        $debit_balance += $asset_account->{amount_with_cb};
+
+        $asset_cb_debit_entry->add_chart_booking(
+          chart  => $ass_acc,
+          credit => - $asset_account->{amount_with_cb},
+          tax_id => 0
+        );
+        $asset_ob_debit_entry->add_chart_booking(
+          chart  => $ass_acc,
+          debit  => - $asset_account->{amount_with_cb},
+          tax_id => 0
+        );
+
+      } else {
+        # $main::lxdebug->message(0, sprintf("adding accno %s with balance %s to credit", $asset_account->{accno}, $asset_account->{amount_with_cb}));
+        $credit_balance += $asset_account->{amount_with_cb};
+
+        $asset_cb_credit_entry->add_chart_booking(
+          chart  => $ass_acc,
+          debit  => $asset_account->{amount_with_cb},
+          tax_id => 0
+        );
+        $asset_ob_credit_entry->add_chart_booking(
+          chart  => $ass_acc,
+          credit  => $asset_account->{amount_with_cb},
+          tax_id => 0
+        );
+      };
+    };
+
+    if ( $debit_balance ) {
+      $asset_cb_debit_entry->add_chart_booking(
+        chart  => $carry_over_chart,
+        debit  => -1 * $debit_balance,
+        tax_id => 0,
+      );
+
+      $asset_ob_debit_entry->add_chart_booking(
+        chart  => $carry_over_chart,
+        credit => -1 * $debit_balance,
+        tax_id => 0,
+      );
+    };
+
+    if ( $credit_balance ) {
+      $asset_cb_credit_entry->add_chart_booking(
+        chart  => $carry_over_chart,
+        credit => $credit_balance,
+        tax_id => 0,
+      );
+      $asset_ob_credit_entry->add_chart_booking(
+        chart  => $carry_over_chart,
+        debit  => $credit_balance,
+        tax_id => 0,
+      );
+    };
+
+    $asset_cb_debit_entry->post  if scalar @{ $asset_cb_debit_entry->transactions  } > 1;
+    $asset_ob_debit_entry->post  if scalar @{ $asset_ob_debit_entry->transactions  } > 1;
+    $asset_cb_credit_entry->post if scalar @{ $asset_cb_credit_entry->transactions } > 1;
+    $asset_ob_credit_entry->post if scalar @{ $asset_ob_credit_entry->transactions } > 1;
+
+    #######  profit-loss accounts #######
+    # these only have a closing balance, the balance is transferred to the profit-loss account
+
+    # need to know if profit or loss first!
+    # use amount_with_cb, so it can be run several times. So sum may be 0 the second time.
+    my $profit_loss_sum = sum map { $_->{amount_with_cb} }
+                              grep { $_->{account_type} eq 'profit_loss_account' }
+                              @{$report_data};
+    $profit_loss_sum ||= 0;
+    my $pl_chart;
+    if ( $profit_loss_sum > 0 ) {
+      $pl_chart = $profit_chart;
+    } else {
+      $pl_chart = $loss_chart;
+    };
+
+    my $pl_debit_balance  = 0;
+    my $pl_credit_balance = 0;
+    # soll = debit, haben = credit
+    my $pl_cb_debit_entry = SL::DB::GLTransaction->new(
+      employee_id    => $employee_id,
+      transdate      => $cb_date,
+      reference      => 'SB ' . $cb_date->year,
+      description    => 'Automatische SB-Buchungen Erfolgskonten Soll für ' . $cb_date->year,
+      ob_transaction => 0,
+      cb_transaction => 1,
+      taxincluded    => 0,
+      transactions   => [],
+    );
+    my $pl_cb_credit_entry = SL::DB::GLTransaction->new(
+      employee_id    => $employee_id,
+      transdate      => $cb_date,
+      reference      => 'SB ' . $cb_date->year,
+      description    => 'Automatische SB-Buchungen Erfolgskonten Haben für ' . $cb_date->year,
+      ob_transaction => 0,
+      cb_transaction => 1,
+      taxincluded    => 0,
+      transactions   => [],
+    );
+
+    foreach my $profit_loss_account ( @profit_loss_accounts ) {
+      # $main::lxdebug->message(0, sprintf("found chart %s with balance %s", $profit_loss_account->{accno}, $profit_loss_account->{amount_with_cb}));
+      my $chart = $charts_by_id{ $profit_loss_account->{chart_id} };
+
+      next if $profit_loss_account->{amount_with_cb} == 0;
+
+      if ( $profit_loss_account->{amount_with_cb} < 0 ) {
+        $pl_debit_balance -= $profit_loss_account->{amount_with_cb};
+        $pl_cb_debit_entry->add_chart_booking(
+          chart  => $chart,
+          tax_id => 0,
+          credit => - $profit_loss_account->{amount_with_cb},
+        );
+      } else {
+        $pl_credit_balance += $profit_loss_account->{amount_with_cb};
+        $pl_cb_credit_entry->add_chart_booking(
+          chart  => $chart,
+          tax_id => 0,
+          debit  => $profit_loss_account->{amount_with_cb},
+        );
+      };
+    };
+
+    # $main::lxdebug->message(0, "pl_debit_balance  = $pl_debit_balance");
+    # $main::lxdebug->message(0, "pl_credit_balance = $pl_credit_balance");
+
+    $pl_cb_debit_entry->add_chart_booking(
+      chart  => $pl_chart,
+      tax_id => 0,
+      debit  => $pl_debit_balance,
+    ) if $pl_debit_balance;
+
+    $pl_cb_credit_entry->add_chart_booking(
+      chart  => $pl_chart,
+      tax_id => 0,
+      credit => $pl_credit_balance,
+    ) if $pl_credit_balance;
+
+    # printf("debit : %s -> %s\n", $_->chart->displayable_name, $_->amount) foreach @{ $pl_cb_debit_entry->transactions };
+    # printf("credit: %s -> %s\n", $_->chart->displayable_name, $_->amount) foreach @{ $pl_cb_credit_entry->transactions };
+
+    $pl_cb_debit_entry->post  if scalar @{ $pl_cb_debit_entry->transactions }  > 1;
+    $pl_cb_credit_entry->post if scalar @{ $pl_cb_credit_entry->transactions } > 1;
+
+    ######### profit-loss transfer #########
+    # and finally transfer the new balance of the profit-loss account via the carry-over account
+    # we want to use profit_loss_sum with cb!
+
+    if ( $profit_loss_sum != 0 ) {
+
+      my $carry_over_cb_entry = SL::DB::GLTransaction->new(
+        employee_id    => $employee_id,
+        transdate      => $cb_date,
+        reference      => 'SB ' . $cb_date->year,
+        description    => sprintf('Automatische SB-Buchung für %s %s',
+                                  $profit_loss_sum >= 0 ? 'Gewinnvortrag' : 'Verlustvortrag',
+                                  $cb_date->year,
+                                 ),
+        ob_transaction => 0,
+        cb_transaction => 1,
+        taxincluded    => 0,
+        transactions   => [],
+      );
+      my $carry_over_ob_entry = SL::DB::GLTransaction->new(
+        employee_id    => $employee_id,
+        transdate      => $ob_date,
+        reference      => 'EB ' . $ob_date->year,
+        description    => sprintf('Automatische EB-Buchung für %s %s',
+                                  $profit_loss_sum >= 0 ? 'Gewinnvortrag' : 'Verlustvortrag',
+                                  $ob_date->year,
+                                 ),
+        ob_transaction => 1,
+        cb_transaction => 0,
+        taxincluded    => 0,
+        transactions   => [],
+      );
+
+      my ($amount1, $amount2);
+      if ( $profit_loss_sum < 0 ) {
+        $amount1 = 'debit';
+        $amount2 = 'credit';
+      } else {
+        $amount1 = 'credit';
+        $amount2 = 'debit';
+      };
+
+      $carry_over_cb_entry->add_chart_booking(
+        chart    => $carry_over_chart,
+        tax_id   => 0,
+        $amount1 => abs($profit_loss_sum),
+      );
+      $carry_over_cb_entry->add_chart_booking(
+        chart    => $pl_chart,
+        tax_id   => 0,
+        $amount2 => abs($profit_loss_sum),
+      );
+      $carry_over_ob_entry->add_chart_booking(
+        chart    => $carry_over_chart,
+        tax_id   => 0,
+        $amount2 => abs($profit_loss_sum),
+      );
+      $carry_over_ob_entry->add_chart_booking(
+        chart    => $pl_chart,
+        tax_id   => 0,
+        $amount1 => abs($profit_loss_sum),
+      );
+
+      # printf("debit : %s -> %s\n", $_->chart->displayable_name, $_->amount) foreach @{ $carry_over_ob_entry->transactions };
+      # printf("credit: %s -> %s\n", $_->chart->displayable_name, $_->amount) foreach @{ $carry_over_ob_entry->transactions };
+
+      $carry_over_cb_entry->post if scalar @{ $carry_over_cb_entry->transactions } > 1;
+      $carry_over_ob_entry->post if scalar @{ $carry_over_ob_entry->transactions } > 1;
+    };
+
+    my $consistency_query = <<SQL;
+select sum(amount)
+  from acc_trans
+ where     (ob_transaction is true or cb_transaction is true)
+       and (transdate = ? or transdate = ?)
+SQL
+    my ($sum) = selectrow_query($::form, $db->dbh, $consistency_query,
+                                $cb_date,
+                                $ob_date
+                               );
+     die "acc_trans transactions don't add up to zero" unless $sum == 0;
+
+    1;
+  }) or die $db->error;
+}
+
+sub _report {
+  my (%params) = @_;
+
+  my $start_date = delete $params{start_date};
+  my $cb_date    = delete $params{cb_date};
+
+  my $defaults = SL::DB::Default->get;
+  die "no carry over account defined"
+    unless defined $defaults->carry_over_account_chart_id
+           and $defaults->carry_over_account_chart_id > 0;
+
+  my $salden_query = <<SQL;
+select c.id as chart_id,
+       c.accno,
+       c.description,
+       c.category,
+       sum(a.amount) filter (where cb_transaction is false and ob_transaction is false) as amount,
+       sum(a.amount) filter (where ob_transaction is true                             ) as ob_amount,
+       sum(a.amount) filter (where cb_transaction is false                            ) as amount_without_cb,
+       sum(a.amount) filter (where cb_transaction is true                             ) as cb_amount,
+       sum(a.amount)                                                                    as amount_with_cb,
+       case when c.category = ANY( '{I,E}'     ) then 'profit_loss_account'
+            when c.category = ANY( '{A,C,L,Q}' ) then 'asset_account'
+                                                 else null
+            end                                                                         as account_type
+  from acc_trans a
+       inner join chart c on (c.id = a.chart_id)
+ where     a.transdate >= ?
+       and a.transdate <= ?
+       and a.chart_id != ?
+ group by c.id, c.accno, c.category
+ order by account_type, c.accno
+SQL
+
+  my $dbh = SL::DB->client->dbh;
+  my $report = selectall_hashref_query($::form, $dbh, $salden_query,
+                                       $start_date,
+                                       $cb_date,
+                                       $defaults->carry_over_account_chart_id,
+                                      );
+  # profit_loss_sum is the actual profit/loss for the year, without cb, use "amount_without_cb")
+  my $profit_loss_sum = sum map { $_->{amount_without_cb} }
+                            grep { $_->{account_type} eq 'profit_loss_account' }
+                            @{$report};
+
+  return ($report, $profit_loss_sum);
+}
+
+#
+# auth
+#
+
+sub check_auth {
+  $::auth->assert('general_ledger');
+}
+
+
+#
+# inits
+#
+
+sub init_ob_date        { $::locale->parse_date_to_object($::form->{ob_date})      }
+sub init_cb_startdate   { $::locale->parse_date_to_object($::form->{cb_startdate}) }
+sub init_cb_date        { $::locale->parse_date_to_object($::form->{cb_date})      }
+
+1;
diff --git a/SL/Controller/ZUGFeRD.pm b/SL/Controller/ZUGFeRD.pm
new file mode 100644 (file)
index 0000000..699e5fe
--- /dev/null
@@ -0,0 +1,225 @@
+package SL::Controller::ZUGFeRD;
+use strict;
+use parent qw(SL::Controller::Base);
+
+use SL::DB::RecordTemplate;
+use SL::Locale::String qw(t8);
+use SL::Helper::DateTime;
+use SL::VATIDNr;
+use SL::ZUGFeRD;
+
+use XML::LibXML;
+
+
+__PACKAGE__->run_before('check_auth');
+
+sub action_upload_zugferd {
+  my ($self, %params) = @_;
+
+  $self->setup_zugferd_action_bar;
+  $self->render('zugferd/form', title => $::locale->text('Factur-X/ZUGFeRD import'));
+}
+
+sub action_import_zugferd {
+  my ($self, %params) = @_;
+
+  die t8("missing file for action import") unless $::form->{file};
+  die t8("can only parse a pdf file")      unless $::form->{file} =~ m/^%PDF/;
+
+  my $info = SL::ZUGFeRD->extract_from_pdf($::form->{file});
+
+  if ($info->{result} != SL::ZUGFeRD::RES_OK()) {
+    # An error occurred; log message from parser:
+    $::lxdebug->message(LXDebug::DEBUG1(), "Could not extract ZUGFeRD data, error message: " . $info->{message});
+    die t8("Could not extract Factur-X/ZUGFeRD data, data and error message:") . $info->{message};
+  }
+  # valid ZUGFeRD metadata
+  my $dom   = XML::LibXML->load_xml(string => $info->{invoice_xml});
+
+  # 1. check if ZUGFeRD SellerTradeParty has a VAT-ID
+  my $ustid = $dom->findnodes('//ram:SellerTradeParty/ram:SpecifiedTaxRegistration')->string_value;
+  die t8("No VAT Info for this Factur-X/ZUGFeRD invoice," .
+         " please ask your vendor to add this for his Factur-X/ZUGFeRD data.") unless $ustid;
+
+  $ustid = SL::VATIDNr->normalize($ustid);
+
+  # 1.1 check if we a have a vendor with this VAT-ID (vendor.ustid)
+  my $vc     = $dom->findnodes('//ram:SellerTradeParty/ram:Name')->string_value;
+  my $vendor = SL::DB::Manager::Vendor->find_by(
+    ustid => $ustid,
+    or    => [
+      obsolete => undef,
+      obsolete => 0,
+    ]);
+
+  if (!$vendor) {
+    # 1.2 If no vendor with the exact VAT ID number is found, the
+    # number might be stored slightly different in the database
+    # (e.g. with spaces breaking up groups of numbers). Iterate over
+    # all existing vendors with VAT ID numbers, normalize their
+    # representation and compare those.
+
+    my $vendors = SL::DB::Manager::Vendor->get_all(
+      where => [
+        '!ustid' => undef,
+        '!ustid' => '',
+        or       => [
+          obsolete => undef,
+          obsolete => 0,
+        ],
+      ]);
+
+    foreach my $other_vendor (@{ $vendors }) {
+      next unless SL::VATIDNr->normalize($other_vendor->ustid) eq $ustid;
+
+      $vendor = $other_vendor;
+      last;
+    }
+  }
+
+  die t8("Please add a valid VAT-ID for this vendor: #1", $vc) unless (ref $vendor eq 'SL::DB::Vendor');
+
+  # 2. check if we have a ap record template for this vendor (TODO only the oldest template is choosen)
+  my $template_ap = SL::DB::Manager::RecordTemplate->get_first(where => [vendor_id => $vendor->id]);
+  die t8("No AP Record Template for this vendor found, please add one") unless (ref $template_ap eq 'SL::DB::RecordTemplate');
+
+
+  # 3. parse the zugferd data and fill the ap record template
+  # -> no need to check sign (credit notes will be negative) just record thei ZUGFeRD type in ap.notes
+  # -> check direct debit (defaults to no)
+  # -> set amount (net amount) and unset taxincluded
+  #    (template and user cares for tax and if there is more than one booking accno)
+  # -> date (can be empty)
+  # -> duedate (may be empty)
+  # -> compare record iban and generate a warning if this differs from vendor's master data iban
+  my $total     = $dom->findnodes('//ram:SpecifiedTradeSettlementHeaderMonetarySummation' .
+                                  '/ram:TaxBasisTotalAmount')->string_value;
+
+  my $invnumber = $dom->findnodes('//rsm:ExchangedDocument/ram:ID')->string_value;
+
+  # parse dates to kivi if set/valid
+  my ($transdate, $duedate, $dt_to_kivi, $due_dt_to_kivi);
+  $transdate = $dom->findnodes('//ram:IssueDateTime')->string_value;
+  $duedate   = $dom->findnodes('//ram:DueDateDateTime')->string_value;
+  $transdate =~ s/^\s+|\s+$//g;
+  $duedate   =~ s/^\s+|\s+$//g;
+
+  if ($transdate =~ /^[0-9]{8}$/) {
+    $dt_to_kivi = DateTime->new(year  => substr($transdate,0,4),
+                                month => substr ($transdate,4,2),
+                                day   => substr($transdate,6,2))->to_kivitendo;
+  }
+  if ($duedate =~ /^[0-9]{8}$/) {
+    $due_dt_to_kivi = DateTime->new(year  => substr($duedate,0,4),
+                                    month => substr ($duedate,4,2),
+                                    day   => substr($duedate,6,2))->to_kivitendo;
+  }
+
+  my $type = $dom->findnodes('//rsm:ExchangedDocument/ram:TypeCode')->string_value;
+
+  my $dd   = $dom->findnodes('//ram:ApplicableHeaderTradeSettlement' .
+                             '/ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode')->string_value;
+  my $direct_debit = $dd == 59 ? 1 : 0;
+
+  my $iban = $dom->findnodes('//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementPaymentMeans' .
+                             '/ram:PayeePartyCreditorFinancialAccount/ram:IBANID')->string_value;
+  my $ibanmessage;
+  $ibanmessage = $iban ne $vendor->iban ? "Record IBAN $iban doesn't match vendor IBAN " . $vendor->iban : $iban if $iban;
+
+  my $url = $self->url_for(
+    controller                           => 'ap.pl',
+    action                               => 'load_record_template',
+    id                                   => $template_ap->id,
+    'form_defaults.amount_1'             => $::form->format_amount(\%::myconfig, $total, 2),
+    'form_defaults.transdate'            => $dt_to_kivi,
+    'form_defaults.invnumber'            => $invnumber,
+    'form_defaults.duedate'              => $due_dt_to_kivi,
+    'form_defaults.no_payment_bookings'  => 0,
+    'form_defaults.paid_1_suggestion'    => $::form->format_amount(\%::myconfig, $total, 2),
+    'form_defaults.notes'                => "ZUGFeRD Import. Type: $type\nIBAN: " . $ibanmessage,
+    'form_defaults.taxincluded'          => 0,
+    'form_defaults.direct_debit'          => $direct_debit,
+  );
+
+  $self->redirect_to($url);
+
+}
+
+sub check_auth {
+  $::auth->assert('ap_transactions');
+}
+sub setup_zugferd_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        $::locale->text('Import'),
+        submit    => [ '#form', { action => 'ZUGFeRD/import_zugferd' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+
+1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Controller::ZUGFeRD
+Controller for importing ZUGFeRD pdf files to kivitendo
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<action_upload_zugferd>
+
+Creates a web from with a single upload dialog.
+
+=item C<action_import_zugferd $pdf>
+
+Expects a single pdf with ZUGFeRD 2.0 metadata.
+Checks if the param <C$pdf> is set and a valid pdf file.
+Calls helper functions to validate and extract the ZUGFeRD data.
+Needs a valid VAT ID (EU) for this vendor and
+expects one ap template for this vendor in kivitendo.
+
+Parses some basic ZUGFeRD data (invnumber, total net amount,
+transdate, duedate, vendor VAT ID, IBAN) and uses the first
+found ap template for this vendor to fill this template with
+ZUGFeRD data.
+If the vendor's master data contain a IBAN and the
+ZUGFeRD record has a IBAN also these values will be compared.
+If they  don't match a warning will be writte in ap.notes.
+Furthermore the ZUGFeRD type code will be written to ap.notes.
+No callback implemented.
+
+=back
+
+=head1 TODO and CAVEAT
+
+This is just a very basic Parser for ZUGFeRD data.
+We assume that the ZUGFeRD generator is a company with a
+valid European VAT ID. Furthermore this vendor needs only
+one and just noe ap template (the first match will be used).
+
+The ZUGFeRD data should also be extracted in the helper package
+and maybe a model should be used for this.
+The user should set one ap template as a default for ZUGFeRD.
+The ZUGFeRD pdf should be written to WebDAV or DMS.
+If the ZUGFeRD data has a payment purpose set, this should
+be the default for the SEPA-XML export.
+
+
+=head1 AUTHOR
+
+Jan Büren E<lt>jan@kivitendo-premium.deE<gt>,
+
+=cut
index 88c5fb4..86cb729 100644 (file)
@@ -18,7 +18,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Datev export module
@@ -30,13 +31,22 @@ use utf8;
 use strict;
 
 use SL::DBUtils;
-use SL::DATEV::KNEFile;
+use SL::DATEV::CSV;
+use SL::DB;
+use SL::HTML::Util ();
+use SL::Iconv;
+use SL::Locale::String qw(t8);
+use SL::VATIDNr;
 
 use Data::Dumper;
 use DateTime;
 use Exporter qw(import);
 use File::Path;
-use List::Util qw(max sum);
+use IO::File;
+use List::MoreUtils qw(any);
+use List::Util qw(min max sum);
+use List::UtilsBy qw(partition_by sort_by);
+use Text::CSV_XS;
 use Time::HiRes qw(gettimeofday);
 
 {
@@ -44,13 +54,15 @@ use Time::HiRes qw(gettimeofday);
   use constant {
     DATEV_ET_BUCHUNGEN => $i++,
     DATEV_ET_STAMM     => $i++,
+    DATEV_ET_CSV       => $i++,
 
     DATEV_FORMAT_KNE   => $i++,
     DATEV_FORMAT_OBE   => $i++,
+    DATEV_FORMAT_CSV   => $i++,
   };
 }
 
-my @export_constants = qw(DATEV_ET_BUCHUNGEN DATEV_ET_STAMM DATEV_FORMAT_KNE DATEV_FORMAT_OBE);
+my @export_constants = qw(DATEV_ET_BUCHUNGEN DATEV_ET_STAMM DATEV_ET_CSV DATEV_FORMAT_KNE DATEV_FORMAT_OBE DATEV_FORMAT_CSV);
 our @EXPORT_OK = (@export_constants);
 our %EXPORT_TAGS = (CONSTANTS => [ @export_constants ]);
 
@@ -204,6 +216,26 @@ sub trans_id {
   return $self->{trans_id};
 }
 
+sub warnings {
+  my $self = shift;
+
+  if (@_) {
+    $self->{warnings} = [@_];
+  } else {
+   return $self->{warnings};
+  }
+}
+
+sub use_pk {
+ my $self = shift;
+
+ if (@_) {
+   $self->{use_pk} = $_[0];
+ }
+
+ return $self->{use_pk};
+}
+
 sub accnofrom {
  my $self = shift;
 
@@ -233,7 +265,7 @@ sub dbh {
     $self->{provided_dbh} = 1;
   }
 
-  $self->{dbh} ||= $::form->get_standard_dbh;
+  $self->{dbh} ||= SL::DB->client->dbh;
 }
 
 sub provided_dbh {
@@ -255,29 +287,6 @@ sub clean_temporary_directories {
   $::lxdebug->leave_sub;
 }
 
-sub _fill {
-  $main::lxdebug->enter_sub();
-
-  my $text      = shift // '';
-  my $field_len = shift;
-  my $fill_char = shift;
-  my $alignment = shift || 'right';
-
-  my $text_len  = length $text;
-
-  if ($field_len < $text_len) {
-    $text = substr $text, 0, $field_len;
-
-  } elsif ($field_len > $text_len) {
-    my $filler = ($fill_char) x ($field_len - $text_len);
-    $text      = $alignment eq 'right' ? $filler . $text : $text . $filler;
-  }
-
-  $main::lxdebug->leave_sub();
-
-  return $text;
-}
-
 sub get_datev_stamm {
   return $_[0]{stamm} ||= selectfirst_hashref_query($::form, $_[0]->dbh, 'SELECT * FROM datev');
 }
@@ -285,43 +294,79 @@ sub get_datev_stamm {
 sub save_datev_stamm {
   my ($self, $data) = @_;
 
-  do_query($::form, $self->dbh, 'DELETE FROM datev');
-
-  my @columns = qw(beraternr beratername dfvkz mandantennr datentraegernr abrechnungsnr);
+  SL::DB->client->with_transaction(sub {
+    do_query($::form, $self->dbh, 'DELETE FROM datev');
 
-  my $query = "INSERT INTO datev (" . join(', ', @columns) . ") VALUES (" . join(', ', ('?') x @columns) . ")";
-  do_query($::form, $self->dbh, $query, map { $data->{$_} } @columns);
+    my @columns = qw(beraternr beratername dfvkz mandantennr datentraegernr abrechnungsnr);
 
-  $self->dbh->commit unless $self->provided_dbh;
+    my $query = "INSERT INTO datev (" . join(', ', @columns) . ") VALUES (" . join(', ', ('?') x @columns) . ")";
+    do_query($::form, $self->dbh, $query, map { $data->{$_} } @columns);
+    1;
+  }) or do { die SL::DB->client->error };
 }
 
 sub export {
   my ($self) = @_;
-  my $result;
 
-  die 'no format set!' unless $self->has_format;
-
-  if ($self->format == DATEV_FORMAT_KNE) {
-    $result = $self->kne_export;
-  } elsif ($self->format == DATEV_FORMAT_OBE) {
-    $result = $self->obe_export;
-  } else {
-    die 'unrecognized export format';
-  }
-
-  return $result;
+  return $self->csv_export;
 }
 
-sub kne_export {
+sub csv_export {
   my ($self) = @_;
   my $result;
 
   die 'no exporttype set!' unless $self->has_exporttype;
 
   if ($self->exporttype == DATEV_ET_BUCHUNGEN) {
-    $result = $self->kne_buchungsexport;
-  } elsif ($self->exporttype == DATEV_ET_STAMM) {
-    $result = $self->kne_stammdatenexport;
+
+    $self->generate_datev_data(from_to => $self->fromto);
+    return if $self->errors;
+
+    my $datev_csv = SL::DATEV::CSV->new(
+      datev_lines  => $self->generate_datev_lines,
+      from         => $self->from,
+      to           => $self->to,
+      locked       => $self->locked,
+    );
+
+
+    my $filename = "EXTF_DATEV_kivitendo" . $self->from->ymd() . '-' . $self->to->ymd() . ".csv";
+
+    my $csv = Text::CSV_XS->new({
+                binary       => 1,
+                sep_char     => ";",
+                always_quote => 1,
+                eol          => "\r\n",
+              }) or die "Cannot use CSV: ".Text::CSV_XS->error_diag();
+
+    # get encoding from defaults - use cp1252 if DATEV strict export is used
+    my $enc = ($::instance_conf->get_datev_export_format eq 'cp1252') ? 'cp1252' : 'utf-8';
+    my $csv_file = IO::File->new($self->export_path . '/' . $filename, ">:encoding($enc)") or die "Can't open: $!";
+
+    $csv->print($csv_file, $_) for @{ $datev_csv->header };
+    $csv->print($csv_file, $_) for @{ $datev_csv->lines  };
+    $csv_file->close;
+    $self->{warnings} = $datev_csv->warnings;
+
+    # convert utf-8 to cp1252//translit if set
+    if ($::instance_conf->get_datev_export_format eq 'cp1252-translit') {
+
+      my $filename_translit = "EXTF_DATEV_kivitendo_translit" . $self->from->ymd() . '-' . $self->to->ymd() . ".csv";
+      open my $fh_in,  '<:encoding(UTF-8)',  $self->export_path . '/' . $filename or die "could not open $filename for reading: $!";
+      open my $fh_out, '>', $self->export_path . '/' . $filename_translit         or die "could not open $filename_translit for writing: $!";
+
+      my $converter = SL::Iconv->new("utf-8", "cp1252//translit");
+
+      print $fh_out $converter->convert($_) while <$fh_in>;
+      close $fh_in;
+      close $fh_out;
+
+      unlink $self->export_path . '/' . $filename or warn "Could not unlink $filename: $!";
+      $filename = $filename_translit;
+    }
+
+    return { download_token => $self->download_token, filenames => $filename };
+
   } else {
     die 'unrecognized exporttype';
   }
@@ -329,10 +374,6 @@ sub kne_export {
   return $result;
 }
 
-sub obe_export {
-  die 'not yet implemented';
-}
-
 sub fromto {
   my ($self) = @_;
 
@@ -345,15 +386,52 @@ sub _sign {
   $_[0] <=> 0;
 }
 
-sub _get_transactions {
+sub locked {
+ my $self = shift;
+
+ if (@_) {
+   $self->{locked} = $_[0];
+ }
+ return $self->{locked};
+}
+sub imported {
+ my $self = shift;
+
+ if (@_) {
+   $self->{imported} = $_[0];
+ }
+ return $self->{imported};
+}
+
+sub generate_datev_data {
   $main::lxdebug->enter_sub();
-  my $self     = shift;
-  my $fromto   = shift;
-  my $progress_callback = shift || sub {};
+
+  my ($self, %params)   = @_;
+  my $fromto            = $params{from_to} // '';
+  my $progress_callback = $params{progress_callback} || sub {};
 
   my $form     =  $main::form;
 
   my $trans_id_filter = '';
+  my $ar_department_id_filter = '';
+  my $ap_department_id_filter = '';
+  my $gl_department_id_filter = '';
+  if ( $form->{department_id} ) {
+    $ar_department_id_filter = " AND ar.department_id = ? ";
+    $ap_department_id_filter = " AND ap.department_id = ? ";
+    $gl_department_id_filter = " AND gl.department_id = ? ";
+  }
+
+  my ($gl_itime_filter, $ar_itime_filter, $ap_itime_filter);
+  if ( $form->{gldatefrom} ) {
+    $gl_itime_filter = " AND gl.itime >= ? ";
+    $ar_itime_filter = " AND ar.itime >= ? ";
+    $ap_itime_filter = " AND ap.itime >= ? ";
+  } else {
+    $gl_itime_filter = "";
+    $ar_itime_filter = "";
+    $ap_itime_filter = "";
+  }
 
   if ( $self->{trans_id} ) {
     # ignore dates when trans_id is passed so that the entire transaction is
@@ -371,61 +449,121 @@ sub _get_transactions {
 
   my %all_taxchart_ids = selectall_as_map($form, $self->dbh, qq|SELECT DISTINCT chart_id, TRUE AS is_set FROM tax|, 'chart_id', 'is_set');
 
+  my $ar_accno = "c.accno";
+  my $ap_accno = "c.accno";
+  if ( $self->use_pk ) {
+    $ar_accno = "CASE WHEN ac.chart_link = 'AR' THEN ct.customernumber ELSE c.accno END as accno";
+    $ap_accno = "CASE WHEN ac.chart_link = 'AP' THEN ct.vendornumber   ELSE c.accno END as accno";
+  }
+  my $gl_imported;
+  if ( !$self->imported ) {
+    $gl_imported = " AND NOT imported";
+  }
+
   my $query    =
-    qq|SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ar.id, ac.amount, ac.taxkey,
-         ar.invnumber, ar.duedate, ar.amount as umsatz, ar.deliverydate,
-         ct.name, ct.ustid,
-         c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
+    qq|SELECT ac.acc_trans_id, ac.transdate, ac.gldate, ac.trans_id,ar.id, ac.amount, ac.taxkey, ac.memo,
+         ar.invnumber, ar.duedate, ar.amount as umsatz, COALESCE(ar.tax_point, ar.deliverydate) AS deliverydate, ar.itime::date,
+         ct.name, ct.ustid, ct.customernumber AS vcnumber, ct.id AS customer_id, NULL AS vendor_id,
+         $ar_accno, c.description AS accname, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
          ar.invoice,
-         t.rate AS taxrate
+         t.rate AS taxrate, t.taxdescription,
+         'ar' as table,
+         tc.accno AS tax_accno, tc.description AS tax_accname,
+         ar.department_id,
+         ar.notes,
+         project.projectnumber as projectnumber, project.description as projectdescription,
+         department.description as departmentdescription
        FROM acc_trans ac
        LEFT JOIN ar          ON (ac.trans_id    = ar.id)
        LEFT JOIN customer ct ON (ar.customer_id = ct.id)
        LEFT JOIN chart c     ON (ac.chart_id    = c.id)
        LEFT JOIN tax t       ON (ac.tax_id      = t.id)
+       LEFT JOIN chart tc    ON (t.chart_id     = tc.id)
+       LEFT JOIN department  ON (department.id  = ar.department_id)
+       LEFT JOIN project     ON (project.id     = ar.globalproject_id)
        WHERE (ar.id IS NOT NULL)
          AND $fromto
          $trans_id_filter
+         $ar_itime_filter
+         $ar_department_id_filter
          $filter
 
        UNION ALL
 
-       SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,ap.id, ac.amount, ac.taxkey,
-         ap.invnumber, ap.duedate, ap.amount as umsatz, ap.deliverydate,
-         ct.name,ct.ustid,
-         c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
+       SELECT ac.acc_trans_id, ac.transdate, ac.gldate, ac.trans_id,ap.id, ac.amount, ac.taxkey, ac.memo,
+         ap.invnumber, ap.duedate, ap.amount as umsatz, COALESCE(ap.tax_point, ap.deliverydate) AS deliverydate, ap.itime::date,
+         ct.name, ct.ustid, ct.vendornumber AS vcnumber, NULL AS customer_id, ct.id AS vendor_id,
+         $ap_accno, c.description AS accname, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
          ap.invoice,
-         t.rate AS taxrate
+         t.rate AS taxrate, t.taxdescription,
+         'ap' as table,
+         tc.accno AS tax_accno, tc.description AS tax_accname,
+         ap.department_id,
+         ap.notes,
+         project.projectnumber as projectnumber, project.description as projectdescription,
+         department.description as departmentdescription
        FROM acc_trans ac
        LEFT JOIN ap        ON (ac.trans_id  = ap.id)
        LEFT JOIN vendor ct ON (ap.vendor_id = ct.id)
        LEFT JOIN chart c   ON (ac.chart_id  = c.id)
        LEFT JOIN tax t     ON (ac.tax_id    = t.id)
+       LEFT JOIN chart tc    ON (t.chart_id     = tc.id)
+       LEFT JOIN department  ON (department.id  = ap.department_id)
+       LEFT JOIN project     ON (project.id     = ap.globalproject_id)
        WHERE (ap.id IS NOT NULL)
          AND $fromto
          $trans_id_filter
+         $ap_itime_filter
+         $ap_department_id_filter
          $filter
 
        UNION ALL
 
-       SELECT ac.acc_trans_id, ac.transdate, ac.trans_id,gl.id, ac.amount, ac.taxkey,
-         gl.reference AS invnumber, gl.transdate AS duedate, ac.amount as umsatz, NULL as deliverydate,
-         gl.description AS name, NULL as ustid,
-         c.accno, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
+       SELECT ac.acc_trans_id, ac.transdate, ac.gldate, ac.trans_id,gl.id, ac.amount, ac.taxkey, ac.memo,
+         gl.reference AS invnumber, NULL AS duedate, ac.amount as umsatz, COALESCE(gl.tax_point, gl.deliverydate) AS deliverydate, gl.itime::date,
+         gl.description AS name, NULL as ustid, '' AS vcname, NULL AS customer_id, NULL AS vendor_id,
+         c.accno, c.description AS accname, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link,
          FALSE AS invoice,
-         t.rate AS taxrate
+         t.rate AS taxrate, t.taxdescription,
+         'gl' as table,
+         tc.accno AS tax_accno, tc.description AS tax_accname,
+         gl.department_id,
+         gl.notes,
+         '' as projectnumber, '' as projectdescription,
+         department.description as departmentdescription
        FROM acc_trans ac
        LEFT JOIN gl      ON (ac.trans_id  = gl.id)
        LEFT JOIN chart c ON (ac.chart_id  = c.id)
        LEFT JOIN tax t   ON (ac.tax_id    = t.id)
+       LEFT JOIN chart tc    ON (t.chart_id     = tc.id)
+       LEFT JOIN department  ON (department.id  = gl.department_id)
        WHERE (gl.id IS NOT NULL)
          AND $fromto
          $trans_id_filter
+         $gl_itime_filter
+         $gl_department_id_filter
+         $gl_imported
+         AND NOT EXISTS (SELECT gl_id from ap_gl where gl_id = gl.id)
          $filter
 
        ORDER BY trans_id, acc_trans_id|;
 
-  my $sth = prepare_execute_query($form, $self->dbh, $query);
+  my @query_args;
+  if ( $form->{gldatefrom} or $form->{department_id} ) {
+
+    for ( 1 .. 3 ) {
+      if ( $form->{gldatefrom} ) {
+        my $glfromdate = $::locale->parse_date_to_object($form->{gldatefrom});
+        die "illegal data" unless ref($glfromdate) eq 'DateTime';
+        push(@query_args, $glfromdate);
+      }
+      if ( $form->{department_id} ) {
+        push(@query_args, $form->{department_id});
+      }
+    }
+  }
+
+  my $sth = prepare_execute_query($form, $self->dbh, $query, @query_args);
   $self->{DATEV} = [];
 
   my $counter = 0;
@@ -469,14 +607,11 @@ sub _get_transactions {
       if ($ref2->{trans_id} != $trans->[0]->{trans_id}) {
         require SL::DB::Manager::AccTransaction;
         if ( $trans->[0]->{trans_id} ) {
-          my $acc_trans_old_obj  = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $trans->[0]->{trans_id} ]);
-          $self->add_error("Unbalanced ledger! Old: " . $acc_trans_old_obj->transaction_name) if ref($acc_trans_old_obj);
-        };
-        if ( $ref2->{trans_id} ) {
-          my $acc_trans_curr_obj = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $ref2->{trans_id} ]);
-          $self->add_error("Unbalanced ledger! New:" . $acc_trans_curr_obj->transaction_name) if ref($acc_trans_curr_obj);
+          my $acc_trans_obj  = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $trans->[0]->{trans_id} ]);
+          $self->add_error(t8("Export error in transaction #1: Unbalanced ledger before next transaction (#2)",
+                              $acc_trans_obj->transaction_name, $ref2->{trans_id})
+          );
         };
-        $self->add_error("count: $count");
         return;
       }
 
@@ -539,7 +674,9 @@ sub _get_transactions {
       # Problem: we can't distinguish between AR and AP and normal invoices via boolean "invoice"
       # for AR and AP transaction exit the loop as soon as an AR or AP account is found
       # there must be only one AR or AP chart in the booking
-      if ( $trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP') {
+      # since it is possible to do this kind of things with GL too, make sure those don't get aborted in case someone
+      # manually pays an invoice in GL.
+      if ($trans->[$j]->{table} ne 'gl' and ($trans->[$j]->{'link'} eq 'AR' or $trans->[$j]->{'link'} eq 'AP')) {
         $notsplitindex = $j;   # position in booking with highest amount
         $absumsatz     = $trans->[$j]->{'amount'};
         last;
@@ -645,8 +782,9 @@ sub _get_transactions {
     if (abs($absumsatz) >= (0.01 * (1 + scalar @taxed))) {
       require SL::DB::Manager::AccTransaction;
       my $acc_trans_obj  = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $trans->[0]->{trans_id} ]);
-      $self->add_error("Datev-Export fehlgeschlagen! Bei Transaktion " . $acc_trans_obj->transaction_name . " ($absumsatz)");
-
+      $self->add_error(t8("Export error in transaction #1: Rounding error too large #2",
+                          $acc_trans_obj->transaction_name, $absumsatz)
+      );
     } elsif (abs($absumsatz) >= 0.01) {
       $self->add_net_gross_differences($absumsatz);
     }
@@ -657,422 +795,165 @@ sub _get_transactions {
   $::lxdebug->leave_sub;
 }
 
-sub make_kne_data_header {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $form) = @_;
-  my ($primanota);
-
-  my $stamm = $self->get_datev_stamm;
-
-  my $jahr = $self->from ? $self->from->year : DateTime->today->year;
-
-  #Header
-  my $header  = "\x1D\x181";
-  $header    .= _fill($stamm->{datentraegernr}, 3, ' ', 'left');
-  $header    .= ($self->fromto) ? "11" : "13"; # Anwendungsnummer
-  $header    .= _fill($stamm->{dfvkz}, 2, '0');
-  $header    .= _fill($stamm->{beraternr}, 7, '0');
-  $header    .= _fill($stamm->{mandantennr}, 5, '0');
-  $header    .= _fill(($stamm->{abrechnungsnr} // '') . $jahr, 6, '0');
-
-  $header .= $self->from ? $self->from->strftime('%d%m%y') : '';
-  $header .= $self->to   ? $self->to->strftime('%d%m%y')   : '';
-
-  if ($self->fromto) {
-    $primanota = "001";
-    $header .= $primanota;
-  }
-
-  $header .= _fill($stamm->{passwort}, 4, '0');
-  $header .= " " x 16;       # Anwendungsinfo
-  $header .= " " x 16;       # Inputinfo
-  $header .= "\x79";
-
-  #Versionssatz
-  my $versionssatz  = $self->exporttype == DATEV_ET_BUCHUNGEN ? "\xB5" . "1," : "\xB6" . "1,";
-
-  my $query         = qq|SELECT accno FROM chart LIMIT 1|;
-  my $ref           = selectfirst_hashref_query($form, $self->dbh, $query);
-
-  $versionssatz    .= length $ref->{accno};
-  $versionssatz    .= ",";
-  $versionssatz    .= length $ref->{accno};
-  $versionssatz    .= ",SELF" . "\x1C\x79";
-
-  $header          .= $versionssatz;
-
-  $main::lxdebug->leave_sub();
-
-  return $header;
-}
-
-sub datetofour {
-  $main::lxdebug->enter_sub();
-
-  my ($date, $six) = @_;
-
-  my ($day, $month, $year) = split(/\./, $date);
-
-  if ($day =~ /^0/) {
-    $day = substr($day, 1, 1);
-  }
-  if (length($month) < 2) {
-    $month = "0" . $month;
-  }
-  if (length($year) > 2) {
-    $year = substr($year, -2, 2);
-  }
-
-  if ($six) {
-    $date = $day . $month . $year;
-  } else {
-    $date = $day . $month;
-  }
-
-  $main::lxdebug->leave_sub();
-
-  return $date;
-}
-
-sub trim_leading_zeroes {
-  my $str = shift;
-
-  $str =~ s/^0+//g;
-
-  return $str;
-}
-
-sub make_ed_versionset {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $header, $filename, $blockcount) = @_;
-
-  my $versionset  = "V" . substr($filename, 2, 5);
-  $versionset    .= substr($header, 6, 22);
-
-  if ($self->fromto) {
-    $versionset .= "0000" . substr($header, 28, 19);
-  } else {
-    my $datum = " " x 16;
-    $versionset .= $datum . "001" . substr($header, 28, 4);
-  }
-
-  $versionset .= _fill($blockcount, 5, '0');
-  $versionset .= "001";
-  $versionset .= " 1";
-  $versionset .= substr($header, -12, 10) . "    ";
-  $versionset .= " " x 53;
-
-  $main::lxdebug->leave_sub();
-
-  return $versionset;
-}
-
-sub make_ev_header {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $form, $fileno) = @_;
-
-  my $stamm = $self->get_datev_stamm;
-
-  my $ev_header  = _fill($stamm->{datentraegernr}, 3, ' ', 'left');
-  $ev_header    .= "   ";
-  $ev_header    .= _fill($stamm->{beraternr}, 7, ' ', 'left');
-  $ev_header    .= _fill($stamm->{beratername}, 9, ' ', 'left');
-  $ev_header    .= " ";
-  $ev_header    .= (_fill($fileno, 5, '0')) x 2;
-  $ev_header    .= " " x 95;
-
-  $main::lxdebug->leave_sub();
-
-  return $ev_header;
-}
-
-sub kne_buchungsexport {
-  $main::lxdebug->enter_sub();
-
+sub generate_datev_lines {
   my ($self) = @_;
 
-  my $form = $::form;
-
-  my @filenames;
-
-  my $filename    = "ED00000";
-  my $evfile      = "EV01";
-  my @ed_versionset;
-  my $fileno = 0;
-
-  my $fromto = $self->fromto;
-
-  $self->_get_transactions($fromto);
-
-  return if $self->errors;
-
-  my $counter = 0;
-
-  while (scalar(@{ $self->{DATEV} || [] })) {
-    my $umsatzsumme = 0;
-    $filename++;
-    my $ed_filename = $self->export_path . $filename;
-    push(@filenames, $filename);
-    my $header = $self->make_kne_data_header($form);
-
-    my $kne_file = SL::DATEV::KNEFile->new();
-    $kne_file->add_block($header);
-
-    while (scalar(@{ $self->{DATEV} }) > 0) {
-      my $transaction = shift @{ $self->{DATEV} };
-      my $trans_lines = scalar(@{$transaction});
-      $counter++;
-
-      my $umsatz         = 0;
-      my $gegenkonto     = "";
-      my $konto          = "";
-      my $belegfeld1     = "";
-      my $datum          = "";
-      my $waehrung       = "";
-      my $buchungstext   = "";
-      my $belegfeld2     = "";
-      my $datevautomatik = 0;
-      my $taxkey         = 0;
-      my $charttax       = 0;
-      my $ustid          ="";
-      my ($haben, $soll);
-      my $iconv          = $::locale->{iconv_utf8};
-      my %umlaute = ($iconv->convert('ä') => 'ae',
-                     $iconv->convert('ö') => 'oe',
-                     $iconv->convert('ü') => 'ue',
-                     $iconv->convert('Ä') => 'Ae',
-                     $iconv->convert('Ö') => 'Oe',
-                     $iconv->convert('Ü') => 'Ue',
-                     $iconv->convert('ß') => 'sz');
-      for (my $i = 0; $i < $trans_lines; $i++) {
-        if ($trans_lines == 2) {
-          if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) {
-            $umsatz = $transaction->[$i]->{'amount'};
-          }
-        } else {
-          if (abs($transaction->[$i]->{'umsatz'}) > abs($umsatz)) {
-            $umsatz = $transaction->[$i]->{'umsatz'};
-          }
-        }
-        if ($transaction->[$i]->{'datevautomatik'}) {
-          $datevautomatik = 1;
-        }
-        if ($transaction->[$i]->{'taxkey'}) {
-          $taxkey = $transaction->[$i]->{'taxkey'};
-        }
-        if ($transaction->[$i]->{'charttax'}) {
-          $charttax = $transaction->[$i]->{'charttax'};
+  my @datev_lines = ();
+
+  foreach my $transaction ( @{ $self->{DATEV} } ) {
+
+    # each $transaction entry contains data from several acc_trans entries
+    # belonging to the same trans_id
+
+    my %datev_data = (); # data for one transaction
+    my $trans_lines = scalar(@{$transaction});
+
+    my $umsatz         = 0;
+    my $gegenkonto     = "";
+    my $konto          = "";
+    my $belegfeld1     = "";
+    my $datum          = "";
+    my $waehrung       = "";
+    my $buchungstext   = "";
+    my $belegfeld2     = "";
+    my $datevautomatik = 0;
+    my $taxkey         = 0;
+    my $charttax       = 0;
+    my $ustid          ="";
+    my ($haben, $soll);
+    for (my $i = 0; $i < $trans_lines; $i++) {
+      if ($trans_lines == 2) {
+        if (abs($transaction->[$i]->{'amount'}) > abs($umsatz)) {
+          $umsatz = $transaction->[$i]->{'amount'};
         }
-        if ($transaction->[$i]->{'amount'} > 0) {
-          $haben = $i;
-        } else {
-          $soll = $i;
+      } else {
+        if (abs($transaction->[$i]->{'umsatz'}) > abs($umsatz)) {
+          $umsatz = $transaction->[$i]->{'umsatz'};
         }
       }
-      # Umwandlung von Umlauten und Sonderzeichen in erlaubte Zeichen bei Textfeldern
-      foreach my $umlaut (keys(%umlaute)) {
-        $transaction->[$haben]->{'invnumber'} =~ s/${umlaut}/${umlaute{$umlaut}}/g;
-        $transaction->[$haben]->{'name'}      =~ s/${umlaut}/${umlaute{$umlaut}}/g;
+      if ($transaction->[$i]->{'datevautomatik'}) {
+        $datevautomatik = 1;
       }
-
-      $transaction->[$haben]->{'invnumber'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
-      $transaction->[$haben]->{'name'}      =~ s/[^0-9A-Za-z\$\%\&\*\+\-\ \/]//g;
-
-      $transaction->[$haben]->{'invnumber'} =  substr($transaction->[$haben]->{'invnumber'}, 0, 12);
-      $transaction->[$haben]->{'name'}      =  substr($transaction->[$haben]->{'name'}, 0, 30);
-      $transaction->[$haben]->{'invnumber'} =~ s/\ *$//;
-      $transaction->[$haben]->{'name'}      =~ s/\ *$//;
-
-      if ($trans_lines >= 2) {
-
-        $gegenkonto = "a" . trim_leading_zeroes($transaction->[$haben]->{'accno'});
-        $konto      = "e" . trim_leading_zeroes($transaction->[$soll]->{'accno'});
-        if ($transaction->[$haben]->{'invnumber'} ne "") {
-          $belegfeld1 = "\xBD" . $transaction->[$haben]->{'invnumber'} . "\x1C";
-        }
-        $datum = "d";
-        $datum .= &datetofour($transaction->[$haben]->{'transdate'}, 0);
-        $waehrung = "\xB3" . "EUR" . "\x1C";
-        if ($transaction->[$haben]->{'name'} ne "") {
-          $buchungstext = "\x1E" . $transaction->[$haben]->{'name'} . "\x1C";
-        }
-        if (($transaction->[$haben]->{'ustid'} // '') ne "") {
-          $ustid = "\xBA" . $transaction->[$haben]->{'ustid'} . "\x1C";
-        }
-        if (($transaction->[$haben]->{'duedate'} // '') ne "") {
-          $belegfeld2 = "\xBE" . &datetofour($transaction->[$haben]->{'duedate'}, 1) . "\x1C";
-        }
+      if ($transaction->[$i]->{'taxkey'}) {
+        $taxkey = $transaction->[$i]->{'taxkey'};
+        # $taxkey = 0 if $taxkey == 94; # taxbookings are in gl
       }
-
-      $umsatz       = $kne_file->format_amount(abs($umsatz), 0);
-      $umsatzsumme += $umsatz;
-      $kne_file->add_block("+" . $umsatz);
-
-      # Dies ist die einzige Stelle die datevautomatik auswertet. Was soll gesagt werden?
-      # Im Prinzip hat jeder acc_trans Eintrag einen Steuerschlüssel, außer, bei gewissen Fällen
-      # wie: Kreditorenbuchung mit negativen Vorzeichen, SEPA-Export oder Rechnungen die per
-      # Skript angelegt werden.
-      # Also falls ein Steuerschlüssel da ist und NICHT datevautomatik diesen Block hinzufügen.
-      # Oder aber datevautomatik ist WAHR, aber der Steuerschlüssel in der acc_trans weicht
-      # von dem in der Chart ab: Also wahrscheinlich Programmfehler (NULL übergeben, statt
-      # DATEV-Steuerschlüssel) oder der Steuerschlüssel des Kontos weicht WIRKLICH von dem Eintrag in der
-      # acc_trans ab. Gibt es für diesen Fall eine plausiblen Grund?
-      #
-      if (   ( $datevautomatik || $taxkey)
-          && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) {
-#         $kne_file->add_block("\x6C" . (!$datevautomatik ? $taxkey : "4"));
-        $kne_file->add_block("\x6C${taxkey}");
+      if ($transaction->[$i]->{'charttax'}) {
+        $charttax = $transaction->[$i]->{'charttax'};
+      }
+      if ($transaction->[$i]->{'amount'} > 0) {
+        $haben = $i;
+      } else {
+        $soll = $i;
       }
-
-      $kne_file->add_block($gegenkonto);
-      $kne_file->add_block($belegfeld1);
-      $kne_file->add_block($belegfeld2);
-      $kne_file->add_block($datum);
-      $kne_file->add_block($konto);
-      $kne_file->add_block($buchungstext);
-      $kne_file->add_block($ustid);
-      $kne_file->add_block($waehrung . "\x79");
     }
 
-    my $mandantenendsumme = "x" . $kne_file->format_amount($umsatzsumme / 100.0, 14) . "\x79\x7a";
-
-    $kne_file->add_block($mandantenendsumme);
-    $kne_file->flush();
+    if ($trans_lines >= 2) {
 
-    open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
-    print(ED $kne_file->get_data());
-    close(ED);
+      # Personenkontenerweiterung: accno has already been replaced if use_pk was set
+      $datev_data{'gegenkonto'} = $transaction->[$haben]->{'accno'};
+      $datev_data{'konto'}      = $transaction->[$soll]->{'accno'};
+      if ($transaction->[$haben]->{'invnumber'} ne "") {
+        $datev_data{belegfeld1} = $transaction->[$haben]->{'invnumber'};
+      }
+      $datev_data{datum} = $transaction->[$haben]->{'transdate'};
+      $datev_data{waehrung} = 'EUR';
+      $datev_data{kost1} = $transaction->[$haben]->{'departmentdescription'};
+      $datev_data{kost2} = $transaction->[$haben]->{'projectdescription'};
 
-    $ed_versionset[$fileno] = $self->make_ed_versionset($header, $filename, $kne_file->get_block_count());
-    $fileno++;
-  }
+      if ($transaction->[$haben]->{'name'} ne "") {
+        $datev_data{buchungstext} = $transaction->[$haben]->{'name'};
+      }
+      if (($transaction->[$haben]->{'ustid'} // '') ne "") {
+        $datev_data{ustid} = SL::VATIDNr->normalize($transaction->[$haben]->{'ustid'});
+      }
+      if (($transaction->[$haben]->{'duedate'} // '') ne "") {
+        $datev_data{belegfeld2} = $transaction->[$haben]->{'duedate'};
+      }
 
-  #Make EV Verwaltungsdatei
-  my $ev_header = $self->make_ev_header($form, $fileno);
-  my $ev_filename = $self->export_path . $evfile;
-  push(@filenames, $evfile);
-  open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
-  print(EV $ev_header);
+      # if deliverydate exists, add it to datev export if it is
+      # * an ar/ap booking that is not a payment
+      # * a gl booking
+      if (    ($transaction->[$haben]->{'deliverydate'} // '') ne ''
+           && (
+                (    $transaction->[$haben]->{'table'} =~ /^(ar|ap)$/
+                  && $transaction->[$haben]->{'link'}  !~ m/_paid/
+                  && $transaction->[$soll]->{'link'}   !~ m/_paid/
+                )
+                || $transaction->[$haben]->{'table'} eq 'gl'
+              )
+         ) {
+        $datev_data{leistungsdatum} = $transaction->[$haben]->{'deliverydate'};
+      }
+    }
+    $datev_data{umsatz} = abs($umsatz); # sales invoices without tax have a different sign???
+
+    # Dies ist die einzige Stelle die datevautomatik auswertet. Was soll gesagt werden?
+    # Im Prinzip hat jeder acc_trans Eintrag einen Steuerschlüssel, außer, bei gewissen Fällen
+    # wie: Kreditorenbuchung mit negativen Vorzeichen, SEPA-Export oder Rechnungen die per
+    # Skript angelegt werden.
+    # Also falls ein Steuerschlüssel da ist und NICHT datevautomatik diesen Block hinzufügen.
+    # Oder aber datevautomatik ist WAHR, aber der Steuerschlüssel in der acc_trans weicht
+    # von dem in der Chart ab: Also wahrscheinlich Programmfehler (NULL übergeben, statt
+    # DATEV-Steuerschlüssel) oder der Steuerschlüssel des Kontos weicht WIRKLICH von dem Eintrag in der
+    # acc_trans ab. Gibt es für diesen Fall eine plausiblen Grund?
+    #
+
+    # only set buchungsschluessel if the following conditions are met:
+    if (   ( $datevautomatik || $taxkey)
+        && (!$datevautomatik || ($datevautomatik && ($charttax ne $taxkey)))) {
+      # $datev_data{buchungsschluessel} = !$datevautomatik ? $taxkey : "4";
+      $datev_data{buchungsschluessel} = $taxkey;
+    }
+    # set lock for each transaction
+    $datev_data{locked} = $self->locked;
 
-  foreach my $file (@ed_versionset) {
-    print(EV $file);
+    push(@datev_lines, \%datev_data) if $datev_data{umsatz};
   }
-  close(EV);
-  ###
 
-  $self->add_filenames(@filenames);
+  # example of modifying export data:
+  # foreach my $datev_line ( @datev_lines ) {
+  #   if ( $datev_line{"konto"} eq '1234' ) {
+  #     $datev_line{"konto"} = '9999';
+  #   }
+  # }
+  #
 
-  $main::lxdebug->leave_sub();
-
-  return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
+  return \@datev_lines;
 }
 
-sub kne_stammdatenexport {
-  $main::lxdebug->enter_sub();
-
+sub check_vcnumbers_are_valid_pk_numbers {
   my ($self) = @_;
-  my $form = $::form;
-
-  $self->get_datev_stamm->{abrechnungsnr} = "99";
-
-  my @filenames;
-
-  my $filename    = "ED00000";
-  my $evfile      = "EV01";
-  my @ed_versionset;
-  my $fileno          = 1;
-  my $i               = 0;
-  my $blockcount      = 1;
-  my $remaining_bytes = 256;
-  my $total_bytes     = 256;
-  my $buchungssatz    = "";
-  $filename++;
-  my $ed_filename = $self->export_path . $filename;
-  push(@filenames, $filename);
-  open(ED, ">", $ed_filename) or die "can't open outputfile: $!\n";
-  my $header = $self->make_kne_data_header($form);
-  $remaining_bytes -= length($header);
-
-  my $fuellzeichen;
-
-  my (@where, @values) = ((), ());
-  if ($self->accnofrom) {
-    push @where, 'c.accno >= ?';
-    push @values, $self->accnofrom;
-  }
-  if ($self->accnoto) {
-    push @where, 'c.accno <= ?';
-    push @values, $self->accnoto;
-  }
 
-  my $where_str = @where ? ' WHERE ' . join(' AND ', map { "($_)" } @where) : '';
+  # better use a class variable and set this in sub new (also needed in DATEV::CSV)
+  # calculation is also a bit more sane in sub check_valid_length_of_accounts
+  my $length_of_accounts = length(SL::DB::Manager::Chart->get_first(where => [charttype => 'A'])->accno) // 4;
+  my $pk_length = $length_of_accounts + 1;
+  my $query = <<"SQL";
+   SELECT customernumber AS vcnumber FROM customer WHERE customernumber !~ '^[[:digit:]]{$pk_length}\$'
+   UNION
+   SELECT vendornumber   AS vcnumber FROM vendor   WHERE vendornumber   !~ '^[[:digit:]]{$pk_length}\$'
+   LIMIT 1;
+SQL
+  my ($has_non_pk_accounts)  = selectrow_query($::form, SL::DB->client->dbh, $query);
+  return defined $has_non_pk_accounts ? 0 : 1;
+}
 
-  my $query     = qq|SELECT c.accno, c.description
-                     FROM chart c
-                     $where_str
-                     ORDER BY c.accno|;
 
-  my $sth = $self->dbh->prepare($query);
-  $sth->execute(@values) || $form->dberror($query);
+sub check_valid_length_of_accounts {
+  my ($self) = @_;
 
-  while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
-    if (($remaining_bytes - length("t" . $ref->{'accno'})) <= 6) {
-      $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
-      $buchungssatz .= "\x00" x $fuellzeichen;
-      $blockcount++;
-      $total_bytes = ($blockcount) * 256;
-    }
-    $buchungssatz .= "t" . $ref->{'accno'};
-    $remaining_bytes = $total_bytes - length($buchungssatz . $header);
-    $ref->{'description'} =~ s/[^0-9A-Za-z\$\%\&\*\+\-\/]//g;
-    $ref->{'description'} = substr($ref->{'description'}, 0, 40);
-    $ref->{'description'} =~ s/\ *$//;
-
-    if (
-        ($remaining_bytes - length("\x1E" . $ref->{'description'} . "\x1C\x79")
-        ) <= 6
-      ) {
-      $fuellzeichen = ($blockcount * 256 - length($buchungssatz . $header));
-      $buchungssatz .= "\x00" x $fuellzeichen;
-      $blockcount++;
-      $total_bytes = ($blockcount) * 256;
-    }
-    $buchungssatz .= "\x1E" . $ref->{'description'} . "\x1C\x79";
-    $remaining_bytes = $total_bytes - length($buchungssatz . $header);
-  }
+  my $query = <<"SQL";
+  SELECT DISTINCT char_length (accno) FROM chart WHERE charttype='A' AND id in (select chart_id from acc_trans);
+SQL
 
-  $sth->finish;
-  print(ED $header);
-  print(ED $buchungssatz);
-  $fuellzeichen = 256 - (length($header . $buchungssatz . "z") % 256);
-  my $dateiende = "\x00" x $fuellzeichen;
-  print(ED "z");
-  print(ED $dateiende);
-  close(ED);
-
-  #Make EV Verwaltungsdatei
-  $ed_versionset[0] =
-    $self->make_ed_versionset($header, $filename, $blockcount);
-
-  my $ev_header = $self->make_ev_header($form, $fileno);
-  my $ev_filename = $self->export_path . $evfile;
-  push(@filenames, $evfile);
-  open(EV, ">", $ev_filename) or die "can't open outputfile: EV01\n";
-  print(EV $ev_header);
-
-  foreach my $file (@ed_versionset) {
-    print(EV $ed_versionset[$file]);
+  my $accno_length = selectall_hashref_query($::form, SL::DB->client->dbh, $query);
+  if (1 < scalar @$accno_length) {
+    $::form->error(t8("Invalid combination of ledger account number length." .
+                      " Mismatch length of #1 with length of #2. Please check your account settings. ",
+                      $accno_length->[0]->{char_length}, $accno_length->[1]->{char_length}));
   }
-  close(EV);
-
-  $self->add_filenames(@filenames);
-
-  $main::lxdebug->leave_sub();
-
-  return { 'download_token' => $self->download_token, 'filenames' => \@filenames };
+  return 1;
 }
 
 sub DESTROY {
@@ -1144,6 +1025,16 @@ SL::DATEV - kivitendo DATEV Export module
   my $path     = $datev->export_path;
   my @files    = glob("$path/*");
 
+  # Only test the datev data of a specific trans_id, without generating an
+  # export file, but filling $datev->errors if errors exist
+
+  my $datev = SL::DATEV->new(
+    trans_id   => $invoice->trans_id,
+  );
+  $datev->generate_datev_data;
+  # if ($datev->errors) { ...
+
+
 =head1 DESCRIPTION
 
 This module implements the DATEV export standard. For usage see above.
@@ -1156,6 +1047,30 @@ This module implements the DATEV export standard. For usage see above.
 
 Generic constructor. See section attributes for information about what to pass.
 
+=item generate_datev_data
+
+Fetches all transactions from the database (via a trans_id or a date range),
+and does an initial transformation (e.g. filters out tax, determines
+the brutto amount, checks split transactions ...) and stores this data in
+$self->{DATEV}.
+
+If any errors are found these are collected in $self->errors.
+
+This function is needed for all the exports, but can be also called
+independently in order to check transactions for DATEV compatibility.
+
+=item generate_datev_lines
+
+Parse the data in $self->{DATEV} and transform it into a format that can be
+used by DATEV, e.g. determines Konto and Gegenkonto, the taxkey, ...
+
+The transformed data is returned as an arrayref, which is ready to be converted
+to a DATEV data format, e.g. KNE, OBE, CSV, ...
+
+At this stage the "DATEV rule" has already been applied to the taxkeys, i.e.
+entries with datevautomatik have an empty taxkey, as the taxkey is already
+determined by the chart.
+
 =item get_datev_stamm
 
 Loads DATEV Stammdaten and returns as hashref.
@@ -1212,13 +1127,54 @@ Forces a garbage collection on previous exports which will delete all exports th
 
 =item errors
 
-Returns a list of errors that occured. If no errors occured, the export was a success.
+Returns a list of errors that occurred. If no errors occurred, the export was a success.
 
 =item export
 
 Exports data. You have to have set L<exporttype> and L<format> or an error will
 occur. OBE exports are currently not implemented.
 
+=item csv_export_for_tax_accountant
+
+Generates up to four downloadable csv files containing data about sales and
+purchase invoices, and their respective payments:
+
+Example:
+  my $startdate = DateTime->new(year => 2012, month =>  1, day =>  1);
+  my $enddate   = DateTime->new(year => 2012, month => 12, day => 31);
+  SL::DATEV->new(from => $startdate, to => $enddate)->csv_export_for_tax_accountant;
+  # {
+  #   'download_token' => '1488551625-815654-22430',
+  #   'filenames' => [
+  #                    'Zahlungen Kreditorenbuchungen 2012-01-01 - 2012-12-31.csv',
+  #                    'Kreditorenbuchungen 2012-01-01 - 2012-12-31.csv',
+  #                    'Zahlungen Debitorenbuchungen 2012-01-01 - 2012-12-31.csv',
+  #                    'Debitorenbuchungen 2012-01-01 - 2012-12-31.csv'
+  #                  ]
+  # };
+
+
+=item check_vcnumbers_are_valid_pk_numbers
+
+Returns 1 if all vcnumbers are suitable for the DATEV export, 0 if not.
+
+Finds the default length of charts (e.g. 4), adds 1 for the pk chart length
+(e.g. 5), and checks the database for any customers or vendors whose customer-
+or vendornumber doesn't consist of only numbers with exactly that length. E.g.
+for a chart length of four "10001" would be ok, but not "10001b" or "1000".
+
+All vcnumbers are checked, obsolete customers or vendors aren't exempt.
+
+There is also no check for the typical customer range 10000-69999 and the
+typical vendor range 70000-99999.
+
+=item check_valid_length_of_accounts
+
+Returns 1 if all currently booked accounts have only one common number length domain (e.g. 4 or 6).
+Will throw an error if more than one distinct size is detected.
+The error message gives a short hint with the value of the (at least)
+two mismatching number length domains.
+
 =back
 
 =head1 ATTRIBUTES
@@ -1232,6 +1188,10 @@ This is a list of attributes set in either the C<new> or a method of the same na
 Set a database handle to use in the process. This allows for an export to be
 done on a transaction in progress without committing first.
 
+Note: If you don't want this code to commit, simply providing a dbh is not
+enough enymore. You'll have to wrap the call into a transaction yourself, so
+that the internal transaction does not commit.
+
 =item exporttype
 
 See L<CONSTANTS> for possible values. This MUST be set before export is called.
@@ -1263,6 +1223,11 @@ correctly.
 
 Set boundary account numbers for the export. Only useful for a stammdaten export.
 
+=item locked
+
+Boolean if the transactions are locked (read-only in kivitenod) or not.
+Default value is false
+
 =back
 
 =head1 CONSTANTS
@@ -1343,6 +1308,7 @@ OBE export is currently not implemented.
 =head1 SEE ALSO
 
 L<SL::DATEV::KNEFile>
+L<SL::DATEV::CSV>
 
 =head1 AUTHORS
 
diff --git a/SL/DATEV/CSV.pm b/SL/DATEV/CSV.pm
new file mode 100644 (file)
index 0000000..8b8e885
--- /dev/null
@@ -0,0 +1,804 @@
+package SL::DATEV::CSV;
+
+use strict;
+use Carp;
+use DateTime;
+use Encode qw(encode);
+use Scalar::Util qw(looks_like_number);
+
+use SL::DB::Datev;
+use SL::DB::Chart;
+use SL::Helper::DateTime;
+use SL::Locale::String qw(t8);
+use SL::Util qw(trim);
+use SL::VATIDNr;
+
+use Rose::Object::MakeMethods::Generic (
+  scalar => [ qw(datev_lines from to locked warnings) ],
+);
+
+my @kivitendo_to_datev = (
+                            {
+                              kivi_datev_name => 'umsatz',
+                              csv_header_name => t8('Transaction Value'),
+                              max_length      => 13,
+                              type            => 'Value',
+                              required        => 1,
+                              input_check     => sub { my ($input) = @_; return (looks_like_number($input) && length($input) <= 13 && $input > 0) },
+                              formatter       => \&_format_amount,
+                              valid_check     => sub { my ($check) = @_; return ($check =~ m/^\d{1,10}(\,\d{1,2})?$/) },
+                            },
+                            {
+                              kivi_datev_name => 'soll_haben_kennzeichen',
+                              csv_header_name => t8('Debit/Credit Label'),
+                              max_length      => 1,
+                              type            => 'Text',
+                              required        => 1,
+                              default         => 'S',
+                              input_check     => sub { my ($check) = @_; return ($check =~ m/^(S|H)$/) },
+                              formatter       => sub { my ($input) = @_; return $input eq 'H' ? 'H' : 'S' },
+                              valid_check     => sub { my ($check) = @_; return ($check =~ m/^(S|H)$/) },
+                            },
+                            {
+                              kivi_datev_name => 'waehrung',
+                              csv_header_name => t8('Transaction Value Currency Code'),
+                              max_length      => 3,
+                              type            => 'Text',
+                              default         => '',
+                              input_check     => sub { my ($check) = @_; return ($check eq '' || $check =~ m/^[A-Z]{3}$/) },
+                              valid_check     => sub { my ($check) = @_; return ($check =~ m/^[A-Z]{3}$/) },
+                            },
+                            {
+                              kivi_datev_name => 'wechselkurs',
+                              csv_header_name => t8('Exchange Rate'),
+                              max_length      => 11,
+                              type            => 'Number',
+                              default         => '',
+                              valid_check     => sub { my ($check) = @_; return ($check =~ m/^[0-9]*\.?[0-9]*$/) },
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                              csv_header_name => t8('Base Transaction Value'),
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                              csv_header_name => t8('Base Transaction Value Currency Code'),
+                            },
+                            {
+                              kivi_datev_name => 'konto',
+                              csv_header_name => t8('Account'),
+                              max_length      => 9,
+                              type            => 'Account',
+                              required        => 1,
+                              input_check     => sub { my ($check) = @_; return ($check =~ m/^[0-9]{4,9}$/) },
+                            },
+                            {
+                              kivi_datev_name => 'gegenkonto',
+                              csv_header_name => t8('Contra Account'),
+                              max_length      => 9,
+                              type            => 'Account',
+                              required        => 1,
+                              input_check     => sub { my ($check) = @_; return ($check =~ m/^[0-9]{4,9}$/) },
+                            },
+                            {
+                              kivi_datev_name => 'buchungsschluessel',
+                              csv_header_name => t8('Posting Key'),
+                              max_length      => 2,
+                              type            => 'Text',
+                              default         => '',
+                              input_check     => sub { my ($check) = @_; return ($check =~ m/^[0-9]{0,2}$/) },
+                            },
+                            {
+                              kivi_datev_name => 'datum',
+                              csv_header_name => t8('Invoice Date'),
+                              max_length      => 4,
+                              type            => 'Date',
+                              required        => 1,
+                              input_check     => sub { my ($check) = @_; return (ref (DateTime->from_kivitendo($check)) eq 'DateTime') },
+                              formatter       => sub { my ($input) = @_; return DateTime->from_kivitendo($input)->strftime('%d%m') },
+                              valid_check     => sub { my ($check) = @_; return ($check =~ m/^[0-9]{4}$/) },
+                            },
+                            {
+                              kivi_datev_name => 'belegfeld1',
+                              csv_header_name => t8('Invoice Field 1'),
+                              max_length      => 12,
+                              type            => 'Text',
+                              default         => '',
+                              input_check     => sub { return 1 unless $::instance_conf->get_datev_export_format eq 'cp1252';
+                                                       my ($text) = @_; check_encoding($text); },
+                              valid_check     => sub { return 1 if     $::instance_conf->get_datev_export_format eq 'cp1252';
+                                                       my ($text) = @_; check_encoding($text); },
+                              formatter       => sub { my ($input) = @_; return substr($input, 0, 12) },
+                            },
+                            {
+                              kivi_datev_name => 'belegfeld2',
+                              csv_header_name => t8('Invoice Field 2'),
+                              max_length      => 12,
+                              type            => 'Text',
+                              default         => '',
+                              input_check     => sub { my ($check) = @_; return 1 unless $check; return (ref (DateTime->from_kivitendo($check)) eq 'DateTime') },
+                              formatter       => sub { my ($input) = @_; return '' unless $input; return trim(DateTime->from_kivitendo($input)->strftime('%e%m%y')) },
+                              valid_check     => sub { my ($check) = @_; return 1 unless $check; return ($check =~ m/^[0-9]{5,6}$/) },
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                              csv_header_name => t8('Discount'),
+                              type            => 'Value',
+                            },
+                            {
+                              kivi_datev_name => 'buchungstext',
+                              csv_header_name => t8('Posting Text'),
+                              max_length      => 60,
+                              type            => 'Text',
+                              default         => '',
+                              input_check     => sub { return 1 unless $::instance_conf->get_datev_export_format eq 'cp1252';
+                                                       my ($text) = @_; check_encoding($text); },
+                              valid_check     => sub { return 1 if     $::instance_conf->get_datev_export_format eq 'cp1252';
+                                                       my ($text) = @_; check_encoding($text); },
+                              formatter       => sub { my ($input) = @_; return substr($input, 0, 60) },
+                            },  # pos 14
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                              csv_header_name => t8('Link to invoice'),
+                              max_length      => 210, # DMS Application shortcut and GUID
+                                                      # Example: "BEDI"
+                                                      # "8DB85C02-4CC3-FF3E-06D7-7F87EEECCF3A".
+                            }, # pos 20
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'kost1',
+                              csv_header_name => t8('Cost Center'),
+                              max_length      => 8,
+                              type            => 'Text',
+                              default         => '',
+                              input_check     => sub { my ($text) = @_; return 1 unless $text; check_encoding($text);  },
+                              formatter       => sub { my ($input) = @_; return substr($input, 0, 8) },
+                            }, # pos 37
+                            {
+                              kivi_datev_name => 'kost2',
+                              csv_header_name => t8('Cost Center'),
+                              max_length      => 8,
+                              type            => 'Text',
+                              default         => '',
+                              input_check     => sub { my ($text) = @_; return 1 unless $text; check_encoding($text);  },
+                              formatter       => sub { my ($input) = @_; return substr($input, 0, 8) },
+                            }, # pos 38
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                              csv_header_name => t8('KOST Quantity'),
+                              max_length      => 9,
+                              type            => 'Number',
+                              valid_check     => sub { my ($check) = @_; return ($check =~ m/^[0-9]{0,9}$/) },
+                            }, # pos 39
+                            {
+                              kivi_datev_name => 'ustid',
+                              csv_header_name => t8('EU Member State and VAT ID Number'),
+                              max_length      => 15,
+                              type            => 'Text',
+                              default         => '',
+                              input_check     => sub {
+                                                       my ($ustid) = @_;
+                                                       return 1 if ('' eq $ustid);
+                                                       return SL::VATIDNr->validate($ustid);
+                                                     },
+                              formatter       => sub { my ($input) = @_; $input =~ s/\s//g; return $input },
+                              valid_check     => sub {
+                                                       my ($ustid) = @_;
+                                                       return 1 if ('' eq $ustid);
+                                                       return SL::VATIDNr->validate($ustid);
+                                                     },
+                            }, # pos 40
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },  # pos 50
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },  # pos 60
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },  # pos 70
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },  # pos 80
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },  # pos 90
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },  # pos 100
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },  # pos 110
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'locked',
+                              csv_header_name => t8('Lock'),
+                              max_length      => 1,
+                              type            => 'Number',
+                              default         => 1,
+                              valid_check     => sub { my ($check) = @_; return ($check =~ m/^(0|1)$/) },
+                            },  # pos 114
+                            {
+                              kivi_datev_name => 'leistungsdatum',
+                              csv_header_name => t8('Payment Date'),
+                              max_length      => 8,
+                              type            => 'Date',
+                              default         => '',
+                              input_check     => sub { my ($check) = @_; return  1 if ('' eq $check); return (ref (DateTime->from_kivitendo($check)) eq 'DateTime') },
+                              formatter       => sub { my ($input) = @_; return '' if ('' eq $input); return DateTime->from_kivitendo($input)->strftime('%d%m%Y') },
+                              valid_check     => sub { my ($check) = @_; return  1 if ('' eq $check); return ($check =~ m/^[0-9]{8}$/) },
+                            },  # pos 115
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },
+                            {
+                              kivi_datev_name => 'not yet implemented',
+                            },  # pos 120
+  );
+
+sub new {
+  my $class = shift;
+  my %data  = @_;
+
+  croak(t8('We need a valid from date'))      unless (ref $data{from} eq 'DateTime');
+  croak(t8('We need a valid to date'))        unless (ref $data{to}   eq 'DateTime');
+  croak(t8('We need a array of datev_lines')) unless (ref $data{datev_lines} eq 'ARRAY');
+
+  my $obj = bless {}, $class;
+  $obj->$_($data{$_}) for keys %data;
+  $obj;
+}
+
+sub check_encoding {
+  my ($test) = @_;
+  return undef unless $test;
+  if (eval {
+    encode('Windows-1252', $test, Encode::FB_CROAK|Encode::LEAVE_SRC);
+    1
+  }) {
+    return 1;
+  }
+}
+
+sub header {
+  my ($self) = @_;
+
+  my @header;
+
+  # we can safely set these defaults
+  # TODO get length_of_accounts from DATEV.pm
+  my $today              = DateTime->now_local;
+  my $created_on         = $today->ymd('') . $today->hms('') . '000';
+  my $length_of_accounts = length(SL::DB::Manager::Chart->get_first(where => [charttype => 'A'])->accno) // 4;
+  my $default_curr       = SL::DB::Default->get_default_currency;
+
+  # datev metadata and the string length limits
+  my %meta_datev;
+  my %meta_datev_to_valid_length = (
+    beraternr   =>  7,
+    beratername => 25,
+    mandantennr =>  5,
+  );
+
+  my $datev = SL::DB::Manager::Datev->get_first();
+
+  while (my ($k, $v) = each %meta_datev_to_valid_length) {
+    next unless $datev->{$k};
+    $meta_datev{$k} = substr $datev->{$k}, 0, $v;
+  }
+
+  my @header_row_1 = (
+    "EXTF", "510", 21, "Buchungsstapel", 7, $created_on, "", "ki",
+    "kivitendo-datev", "", $meta_datev{beraternr}, $meta_datev{mandantennr},
+    $self->first_day_of_fiscal_year->ymd(''), $length_of_accounts,
+    $self->from->ymd(''), $self->to->ymd(''), "", "", 1, "", $self->locked,
+    $default_curr, "", "", "",""
+  );
+  push @header, [ @header_row_1 ];
+
+  # second header row, just the column names
+  push @header, [ map { $_->{csv_header_name} } @kivitendo_to_datev ];
+
+  return \@header;
+}
+
+sub lines {
+  my ($self) = @_;
+
+  my (@array_of_datev, @warnings);
+
+  foreach my $row (@{ $self->datev_lines }) {
+    my @current_datev_row;
+
+    # 1. check all datev_lines and see if we have a defined value
+    # 2. if we don't have a defined value set a default if exists
+    # 3. otherwise die
+    foreach my $column (@kivitendo_to_datev) {
+      if ($column->{kivi_datev_name} eq 'not yet implemented') {
+        push @current_datev_row, '';
+        next;
+      }
+      my $data = $row->{$column->{kivi_datev_name}};
+      if (!defined $data) {
+        if (defined $column->{default}) {
+          $data = $column->{default};
+        } else {
+          die 'No sensible value or a sensible default found for the entry: ' . $column->{kivi_datev_name};
+        }
+      }
+      # checkpoint a: no undefined data. All strict checks now!
+      if (exists $column->{input_check} && !$column->{input_check}->($data)) {
+        die t8("Wrong field value '#1' for field '#2' for the transaction with amount '#3'",
+                $data, $column->{kivi_datev_name}, $row->{umsatz});
+      }
+      # checkpoint b: we can safely format the input
+      if ($column->{formatter}) {
+        $data = $column->{formatter}->($data);
+      }
+      # checkpoint c: all soft checks now, will pop up as a user warning
+      if (exists $column->{valid_check} && !$column->{valid_check}->($data)) {
+        push @warnings, t8("Wrong field value '#1' for field '#2' for the transaction" .
+                           " with amount '#3'", $data, $column->{kivi_datev_name}, $row->{umsatz});
+      }
+      push @current_datev_row, $data;
+    }
+    push @array_of_datev, \@current_datev_row;
+  }
+  $self->warnings(\@warnings);
+  return \@array_of_datev;
+}
+
+# helper
+
+sub _format_amount {
+  $::form->format_amount({ numberformat => '1000,00' }, @_);
+}
+
+sub first_day_of_fiscal_year {
+  $_[0]->to->clone->truncate(to => 'year');
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::DATEV::CSV - kivitendo DATEV CSV Specification
+
+=head1 SYNOPSIS
+
+  use SL::DATEV qw(:CONSTANTS);
+  use SL::DATEV::CSV;
+
+  my $startdate = DateTime->new(year => 2014, month => 9, day => 1);
+  my $enddate   = DateTime->new(year => 2014, month => 9, day => 31);
+  my $datev = SL::DATEV->new(
+    exporttype => DATEV_ET_BUCHUNGEN,
+    format     => DATEV_FORMAT_CSV,
+    from       => $startdate,
+    to         => $enddate,
+  );
+  $datev->generate_datev_data;
+
+  my $datev_csv = SL::DATEV::CSV->new(datev_lines  => $datev->generate_datev_lines,
+                                      from         => $datev->from,
+                                      to           => $datev->to,
+                                      locked       => $datev->locked,
+                                     );
+  $datev_csv->header;   # returns the required 2 rows of header ($aref = [ ["row1" ..], [ "row2" .. ] ]) as array of array
+  $datev_csv->lines;    # returns an array_ref of rows of array_refs soll uns die ein Arrayref von Zeilen zurückgeben, die jeweils Arrayrefs sind
+  $datev_csv->warnings; # returns warnings
+
+
+  # The above object methods can be directly chained to a CSV export function, like this:
+  my $csv_file = IO::File->new($somewhere_in_filesystem)') or die "Can't open: $!";
+  $csv->print($csv_file, $_) for @{ $datev_csv->header };
+  $csv->print($csv_file, $_) for @{ $datev_csv->lines  };
+  $csv_file->close;
+  $self->{warnings} = $datev_csv->warnings;
+
+
+
+
+=head1 DESCRIPTION
+
+The parsing of the DATEV CSV is index based, therefore the correct
+column must be present at the corresponding index, i.e.:
+ Index 2
+ Field Name   : Debit/Credit Label
+ Valid Values : 'S' or 'H'
+ Length:      : 1
+
+The columns in C<@kivi_datev> are in the correct order and the
+specific attributes are defined as a key value hash list for each entry.
+
+The key names are the english translation according to the DATEV specs
+(Leitfaden DATEV englisch).
+
+The two attributes C<max_length> and C<type> are also set as specified
+by the DATEV specs.
+
+To link the structure to kivitendo data, each entry has the attribute C<kivi_datev_name>
+which is by convention the key name as generated by DATEV->generate_datev_data.
+A value of C<'not yet implemented'> indicates that this field has no
+corresponding kivitendo data and will be given an empty value by DATEV->csv_buchungsexport.
+
+
+=head1 SPECIFICATION
+
+This is an excerpt of the DATEV Format 2015 Specification for CSV-Header
+and CSV-Data lines.
+
+=head2 FILENAME
+
+The filename is subject to the following restrictions:
+1. The filename must begin with the prefix DTVF_ or EXTF_.
+2. The filename must end with .csv.
+
+When exporting from or importing into DATEV applications, the filename is
+marked with the prefix "DTVF_" (DATEV Format).
+The prefix "DTVF_" is reserved for DATEV applications.
+If you are using a third-party application to create a file in the DATEV format
+that you want to import using batch processing, use the prefix "EXTF_"
+(External Format).
+
+=head2 File Structure
+
+The file structure of the text file exported/imported is defined as follows
+
+Line 1: Header (serves to assist in the interpretation of the following data)
+
+Line 2: Headline (headline of the user data)
+
+Line 3 – n: Records (user data)
+
+For an valid example file take a look at doc/DATEV-2015/EXTF_Buchungsstapel.csv
+
+
+=head2 Detailed Description
+
+Line 1 must contain 11 fields.
+
+Line 2 must contain 26 fields.
+
+Line 3 - n:  must contain 116 fields, a smaller subset is mandatory.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item new PARAMS
+
+Constructor for CSV-DATEV export.
+Checks mandantory params as described in section synopsis.
+
+=item check_encoding
+
+Helper function, returns true if a string is not empty and cp1252 encoded
+For example some arabic utf-8 like  ݐ  will return false
+
+=item header
+
+Mostly all other header information are constants or metadata loaded
+from SL::DB::Datev.pm.
+
+Returns the first two entries for the header (see above: File Structure)
+as an array.
+
+=item kivitendo_to_datev
+
+Returns the data structure C<@datev_data> as an array
+
+=item _format_amount
+
+Lightweight wrapper for form->format_amount.
+Expects a number in kivitendo database format and returns the same number
+in DATEV format.
+
+=item first_day_of_fiscal_year
+
+Takes a look at $self->to to  determine the first day of the fiscal year.
+
+=item lines
+
+Generates the CSV-Format data for the CSV DATEV export and returns
+an 2-dimensional array as an array_ref.
+May additionally return a second array_ref with warnings.
+
+Requires the same date fields as the constructor for a valid DATEV header.
+
+Furthermore we assume that the first day of the fiscal year is
+the first of January and we cannot guarantee that our data in kivitendo
+is locked, that means a booking cannot be modified after a defined (vat tax)
+period.
+Some validity checks (max_length and regex) will be done if the
+data structure contains them and the field is defined.
+
+To add or alter the structure of the data take a look at the C<@kivitendo_to_datev> structure.
+
+=back
+
+=head1 TODO CAVEAT
+
+One can circumevent the check of the warnings.quite easily,
+becaus warnings are generated after the call to lines:
+
+  # WRONG usage
+  die if @{ $datev_csv->warnings };
+  somethin_with($datev_csv->lines);
+
+  # safe usage
+  my $lines = $datev_csv->lines;
+  die if @{ $datev_csv->warnings };
+  somethin_with($lines);
diff --git a/SL/DATEV/KNEFile.pm b/SL/DATEV/KNEFile.pm
deleted file mode 100644 (file)
index 9816ac0..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-package SL::DATEV::KNEFile;
-
-use strict;
-
-sub new {
-  my $type = shift;
-  my $self = {};
-
-  bless $self, $type;
-
-  $self->_init(@_);
-
-  return $self;
-}
-
-sub _init {
-  my $self   = shift;
-  my %params = @_;
-
-  map { $self->{$_} = $params{$_} } keys %params;
-
-  $self->{remaining_bytes} = 250;
-  $self->{block_count}     =   0;
-  $self->{data}            = '';
-}
-
-sub get_data {
-  my $self = shift;
-
-  return $self->{data} || '';
-}
-
-sub get_block_count {
-  my $self = shift;
-
-  return $self->{block_count};
-}
-
-sub add_block {
-  my $self      = shift;
-  my $block     = shift;
-
-  my $block_len = length $block;
-
-
-  $self->flush() if ($block_len > $self->{remaining_bytes});
-
-  $self->{data}            .= $block;
-  $self->{remaining_bytes} -= $block_len;
-
-  return $self;
-}
-
-sub flush {
-  my $self = shift;
-
-  if (250 == $self->{remaining_bytes}) {
-    return $self;
-  }
-
-  my $num_zeros             = 6 + $self->{remaining_bytes};
-  $self->{data}            .= "\x00" x $num_zeros;
-
-  $self->{remaining_bytes}  = 250;
-  $self->{block_count}++;
-
-  return $self;
-}
-
-sub format_amount {
-  my $self   = shift;
-  my $amount = shift;
-  my $width  = shift;
-
-  $amount =~ s/-//;
-  my ($places, $decimal_places) = split m/\./, "$amount";
-
-  $places          *= 1;
-  $decimal_places ||= 0;
-
-  if (0 < $width) {
-    $width  -= 2;
-    $places  = sprintf("\%0${width}d", $places);
-  }
-
-  $decimal_places .= '0' if (2 > length $decimal_places);
-  $amount          = $places . substr($decimal_places, 0, 2);
-  $amount         *= 1 if (!$width);
-
-  return $amount;
-}
-
-1;
index 0adac9c..a59fd6f 100644 (file)
--- a/SL/DB.pm
+++ b/SL/DB.pm
@@ -6,11 +6,12 @@ use Carp;
 use Data::Dumper;
 use English qw(-no_match_vars);
 use Rose::DB;
-use Rose::DBx::Cache::Anywhere;
+use SL::DB::Helper::Cache;
+use Scalar::Util qw(blessed);
 
 use base qw(Rose::DB);
 
-__PACKAGE__->db_cache_class('Rose::DBx::Cache::Anywhere');
+__PACKAGE__->db_cache_class('SL::DB::Helper::Cache');
 __PACKAGE__->use_private_registry;
 
 my (%_db_registered);
@@ -34,6 +35,14 @@ sub create {
   return $db;
 }
 
+sub client {
+  create(undef, 'KIVITENDO');
+}
+
+sub auth {
+  create(undef, 'KIVITENDO_AUTH');
+}
+
 sub _register_db {
   my $domain = shift;
   my $type   = shift;
@@ -84,7 +93,7 @@ sub _register_db {
   my %flattened_settings = _flatten_settings(%connect_settings);
 
   $domain                = 'KIVITENDO' if $type =~ m/^KIVITENDO/;
-  $type                 .= join($SUBSCRIPT_SEPARATOR, map { ($_, $flattened_settings{$_} || '') } sort grep { $_ ne 'dbpasswd' } keys %flattened_settings);
+  $type                 .= join($SUBSCRIPT_SEPARATOR, map { ($_, $flattened_settings{$_} || '') } sort grep { $_ ne 'password' } keys %flattened_settings);
   my $idx                = "${domain}::${type}";
 
   if (!$_db_registered{$idx}) {
@@ -118,14 +127,30 @@ sub with_transaction {
   my ($self, $code, @args) = @_;
 
   return $code->(@args) if $self->in_transaction;
-  if (wantarray) {
-    my @result;
-    return $self->do_transaction(sub { @result = $code->(@args) }) ? @result : ();
 
-  } else {
-    my $result;
-    return $self->do_transaction(sub { $result = $code->(@args) }) ? $result : undef;
-  }
+  my (@result, $result);
+  my $rv = 1;
+
+  local $@;
+  my $return_array = wantarray;
+  eval {
+    $return_array
+      ? $self->do_transaction(sub { @result = $code->(@args) })
+      : $self->do_transaction(sub { $result = $code->(@args) });
+  } or do {
+    my $error = $self->error;
+    if (blessed $error) {
+      if ($error->isa('SL::X::DBError')) {
+        # gobble the exception
+      } else {
+        $error->rethrow;
+      }
+    } else {
+      die $self->error;
+    }
+  };
+
+  return $return_array ? @result : $result;
 }
 
 1;
@@ -159,25 +184,67 @@ configuration.
 =item C<with_transaction $code_ref, @args>
 
 Executes C<$code_ref> with parameters C<@args> within a transaction,
-starting one if none is currently active. Example:
+starting one only if none is currently active. Example:
 
   return $self->db->with_transaction(sub {
     # do stuff with $self
   });
 
-One big difference to L<Rose::DB/do_transaction> is the return code
-handling. If a transaction is already active then C<with_transcation>
-simply returns the result of calling C<$code_ref> as-is.
+This is a wrapper around L<Rose::DB/do_transaction> that does a few additional
+things, and should always be used in favour of the other:
+
+=over 4
+
+=item Composition of transactions
 
-Otherwise the return value depends on the result of the underlying
-transaction. If the transaction fails then C<undef> is returned in
-scalar context and an empty list in list context. If the transaction
-succeeds then the return value of C<$code_ref> is returned preserving
-context.
+When C<with_transaction> is called without a running transaction, a new one is
+created. If it is called within a running transaction, it performs no
+additional handling. This means that C<with_transaction> can be safely used
+within another C<with_transaction>, whereas L<Rose::DB/do_transaction> can not.
 
-So if you want to differentiate between "transaction failed" and
-"succeeded" then your C<$code_ref> should never return C<undef>
-itself.
+=item Return values
+
+C<with_transaction> adopts the behaviour of C<eval> in that it returns the
+result of the inner block, and C<undef> if an error occurred. This way you can
+use the same pattern you would normally use with C<eval> for
+C<with_transaction>:
+
+  SL::DB->client->with_transaction(sub {
+     # do stuff
+     # and return nominal true value
+     1;
+  }) or do {
+    # transaction error handling
+    my $error = SL::DB->client->error;
+  }
+
+or you can use it to safely calulate things.
+
+=item Error handling
+
+The original L<Rose::DB/do_transaction> gobbles up all exceptions and expects
+the caller to manually check the return value and error, and then to process
+all exceptions as strings. This is very fragile and generally a step backwards
+from proper exception handling.
+
+C<with_transaction> only gobbles up exceptions that are used to signal an
+error in the transaction, and returns undef on those. All other exceptions
+bubble out of the transaction like normal, so that it is transparent to typos,
+runtime exceptions and other generally wanted things.
+
+If you just use the snippet above, your code will catch everything related to
+the transaction aborting, but will not catch other errors that might have been
+thrown. The transaction will be rolled back in both cases.
+
+If you want to play nice in case your transaction is embedded in another
+transaction, just rethrow the error:
+
+  $db->with_transaction(sub {
+    # code deep in the engine
+    1;
+  }) or die $db->error;
+
+=back
 
 =back
 
diff --git a/SL/DB/AdditionalBillingAddress.pm b/SL/DB/AdditionalBillingAddress.pm
new file mode 100644 (file)
index 0000000..c6ffad4
--- /dev/null
@@ -0,0 +1,59 @@
+package SL::DB::AdditionalBillingAddress;
+
+use strict;
+
+use SL::DB::MetaSetup::AdditionalBillingAddress;
+use SL::DB::Manager::AdditionalBillingAddress;
+
+__PACKAGE__->meta->initialize;
+
+__PACKAGE__->after_save('_after_save_ensure_only_one_marked_as_default_per_customer');
+
+sub _after_save_ensure_only_one_marked_as_default_per_customer {
+  my ($self) = @_;
+
+  if ($self->id && $self->customer_id && $self->default_address) {
+    SL::DB::Manager::AdditionalBillingAddress->update_all(
+      set   => { default_address => 0 },
+      where => [
+        customer_id => $self->customer_id,
+        '!id'       => $self->id,
+      ],
+    );
+  }
+
+  return 1;
+}
+
+sub displayable_id {
+  my $self = shift;
+  my $text = join('; ', grep { $_ } (map({ $self->$_ } qw(name street)),
+                                     join(' ', grep { $_ }
+                                               map  { $self->$_ }
+                                               qw(zipcode city))));
+
+  return $text;
+}
+
+sub used {
+  my ($self) = @_;
+
+  return unless $self->id;
+
+  require SL::DB::Order;
+  require SL::DB::Invoice;
+  require SL::DB::DeliveryOrder;
+
+  my %args = (query => [ billing_address_id => $self->id ]);
+
+  return SL::DB::Manager::Invoice->get_all_count(%args)
+      || SL::DB::Manager::Order->get_all_count(%args)
+      || SL::DB::Manager::DeliveryOrder->get_all_count(%args);
+}
+
+sub detach {
+  $_[0]->customer_id(undef);
+  return $_[0];
+}
+
+1;
diff --git a/SL/DB/ApGl.pm b/SL/DB/ApGl.pm
new file mode 100644 (file)
index 0000000..77368a3
--- /dev/null
@@ -0,0 +1,13 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::ApGl;
+
+use strict;
+
+use SL::DB::MetaSetup::ApGl;
+use SL::DB::Manager::ApGl;
+
+__PACKAGE__->meta->initialize;
+
+1;
index 2231214..fd645c2 100644 (file)
@@ -1,23 +1,33 @@
-# This file has been auto-generated only because it didn't exist.
-# Feel free to modify it at will; it will not be overwritten automatically.
-
 package SL::DB::Assembly;
 
 use strict;
 
 use SL::DB::MetaSetup::Assembly;
 
-__PACKAGE__->meta->add_relationships(
-  part => {
-    type         => 'many to one',
-    class        => 'SL::DB::Part',
-    column_map   => { parts_id => 'id' },
-  },
-);
-
 # Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
 __PACKAGE__->meta->make_manager_class;
 
 __PACKAGE__->meta->initialize;
 
+sub linetotal_sellprice {
+  my ($self) = @_;
+
+  return 0 unless $self->qty > 0 and $self->part->sellprice > 0;
+  return $self->qty * $self->part->sellprice / ( $self->part->price_factor_id ? $self->part->price_factor->factor : 1 );
+}
+
+sub linetotal_lastcost {
+  my ($self) = @_;
+
+  return 0 unless $self->qty > 0 and $self->part->lastcost > 0;
+  return $self->qty * $self->part->lastcost / ( $self->part->price_factor_id ? $self->part->price_factor->factor : 1 );
+}
+
+sub linetotal_weight {
+  my ($self) = @_;
+
+  return 0 unless $self->qty > 0 and ($self->part->weight||0) > 0;
+  return $self->qty * $self->part->weight;
+}
+
 1;
diff --git a/SL/DB/AssortmentItem.pm b/SL/DB/AssortmentItem.pm
new file mode 100644 (file)
index 0000000..6ed474f
--- /dev/null
@@ -0,0 +1,39 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::AssortmentItem;
+
+use strict;
+
+use SL::DB::MetaSetup::AssortmentItem;
+use SL::DB::Manager::AssortmentItem;
+use Rose::DB::Object::Helpers qw(clone);
+
+__PACKAGE__->meta->initialize;
+
+sub linetotal_sellprice {
+  my ($self, %params) = @_;
+
+  my $sellprice = $self->part->sellprice;
+  if ($params{pricegroup}) {
+    my $pricegroup = SL::DB::Manager::Pricegroup->find_by( pricegroup => $params{pricegroup});
+    die "Can't find pricegroup with name " . $params{pricegroup} unless $pricegroup;
+    $params{pricegroup_id} = $pricegroup->id if $pricegroup;
+  }
+  if ($params{pricegroup_id}) {
+    my $price = SL::DB::Manager::Price->find_by(pricegroup_id => $params{pricegroup_id}, parts_id => $self->part->id);
+    $sellprice = $price->price if $price;
+  }
+
+  return 0 unless $self->qty > 0 and $sellprice > 0;
+  return $self->qty * $sellprice / ( $self->part->price_factor_id ? $self->part->price_factor->factor : 1 );
+}
+
+sub linetotal_lastcost {
+  my ($self) = @_;
+
+  return 0 unless $self->qty > 0 and $self->part->lastcost > 0;
+  return $self->qty * $self->part->lastcost / ( $self->part->price_factor_id ? $self->part->price_factor->factor : 1 );
+}
+
+1;
diff --git a/SL/DB/AuthMasterRight.pm b/SL/DB/AuthMasterRight.pm
new file mode 100644 (file)
index 0000000..c92bd0e
--- /dev/null
@@ -0,0 +1,13 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::AuthMasterRight;
+
+use strict;
+
+use SL::DB::MetaSetup::AuthMasterRight;
+use SL::DB::Manager::AuthMasterRight;
+
+__PACKAGE__->meta->initialize;
+
+1;
diff --git a/SL/DB/AuthSchemaInfo.pm b/SL/DB/AuthSchemaInfo.pm
new file mode 100644 (file)
index 0000000..93a631c
--- /dev/null
@@ -0,0 +1,13 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::AuthSchemaInfo;
+
+use strict;
+
+use SL::DB::MetaSetup::AuthSchemaInfo;
+use SL::DB::Manager::AuthSchemaInfo;
+
+__PACKAGE__->meta->initialize;
+
+1;
diff --git a/SL/DB/AuthSession.pm b/SL/DB/AuthSession.pm
new file mode 100644 (file)
index 0000000..daf305e
--- /dev/null
@@ -0,0 +1,13 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::AuthSession;
+
+use strict;
+
+use SL::DB::MetaSetup::AuthSession;
+use SL::DB::Manager::AuthSession;
+
+__PACKAGE__->meta->initialize;
+
+1;
diff --git a/SL/DB/AuthSessionContent.pm b/SL/DB/AuthSessionContent.pm
new file mode 100644 (file)
index 0000000..1443e70
--- /dev/null
@@ -0,0 +1,13 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::AuthSessionContent;
+
+use strict;
+
+use SL::DB::MetaSetup::AuthSessionContent;
+use SL::DB::Manager::AuthSessionContent;
+
+__PACKAGE__->meta->initialize;
+
+1;
index a8d8ab5..2565d06 100644 (file)
@@ -11,7 +11,7 @@ use SL::DB::Helper::Util;
 use constant CONFIG_VARS => qw(copies countrycode dateformat timeformat default_media default_printer_id
                                email favorites fax hide_cvar_search_options mandatory_departments menustyle name
                                numberformat show_form_details signature stylesheet taxincluded_checked tel
-                               template_format vclimit focus_position form_cvars_nr_cols item_multiselect);
+                               template_format focus_position form_cvars_nr_cols item_multiselect);
 
 __PACKAGE__->meta->add_relationship(
   groups => {
index 3e6c03d..1fc8d99 100644 (file)
@@ -11,6 +11,7 @@ use SL::DB::MetaSetup::BackgroundJob;
 use SL::DB::Manager::BackgroundJob;
 
 use SL::System::Process;
+use SL::YAML;
 
 __PACKAGE__->meta->initialize;
 
@@ -76,18 +77,18 @@ sub run {
 sub data_as_hash {
   my $self = shift;
 
-  $self->data(YAML::Dump($_[0])) if @_;
+  $self->data(SL::YAML::Dump($_[0])) if @_;
 
   return {}                        if !$self->data;
   return $self->data               if ref($self->{data}) eq 'HASH';
-  return YAML::Load($self->{data}) if !ref($self->{data});
+  return SL::YAML::Load($self->{data}) if !ref($self->{data});
   return {};
 }
 
 sub set_data {
   my ($self, %data) = @_;
 
-  $self->data(YAML::Dump({
+  $self->data(SL::YAML::Dump({
     %{ $self->data_as_hash },
     %data,
   }));
index 5f4a3e4..fba783c 100644 (file)
@@ -1,6 +1,3 @@
-# This file has been auto-generated only because it didn't exist.
-# Feel free to modify it at will; it will not be overwritten automatically.
-
 package SL::DB::BankAccount;
 
 use strict;
@@ -8,6 +5,7 @@ use strict;
 use SL::DB::MetaSetup::BankAccount;
 use SL::DB::Manager::BankAccount;
 use SL::DB::Helper::ActsAsList;
+use SL::DB::Helper::IBANValidation;
 
 __PACKAGE__->meta->initialize;
 
@@ -39,6 +37,7 @@ sub validate {
   };
 
   push @errors, $::locale->text('The IBAN is missing.') unless $self->{iban};
+  push @errors, $self->validate_ibans;
 
   return @errors;
 }
index d5d7c55..b07cdf1 100644 (file)
@@ -8,6 +8,7 @@ use strict;
 use SL::DB::MetaSetup::BankTransaction;
 use SL::DB::Manager::BankTransaction;
 use SL::DB::Helper::LinkedRecords;
+use Carp;
 
 require SL::DB::Invoice;
 require SL::DB::PurchaseInvoice;
@@ -39,17 +40,23 @@ sub linked_invoices {
   my $record_links = SL::DB::Manager::RecordLink->get_all(where => [ from_table => 'bank_transactions', from_id => $self->id ]);
 
   foreach my $record_link (@{ $record_links }) {
-    push @linked_invoices, SL::DB::Manager::Invoice->find_by(id => $record_link->to_id)->invnumber         if $record_link->to_table eq 'ar';
-    push @linked_invoices, SL::DB::Manager::PurchaseInvoice->find_by(id => $record_link->to_id)->invnumber if $record_link->to_table eq 'ap';
+    push @linked_invoices, SL::DB::Manager::Invoice->find_by(id => $record_link->to_id)         if $record_link->to_table eq 'ar';
+    push @linked_invoices, SL::DB::Manager::PurchaseInvoice->find_by(id => $record_link->to_id) if $record_link->to_table eq 'ap';
+    push @linked_invoices, SL::DB::Manager::GLTransaction->find_by(id => $record_link->to_id)   if $record_link->to_table eq 'gl';
   }
 
   return [ @linked_invoices ];
 }
 
+sub is_batch_transaction {
+  ($_[0]->transaction_code // '') eq "191";
+}
+
+
 sub get_agreement_with_invoice {
-  my ($self, $invoice) = @_;
+  my ($self, $invoice, %params) = @_;
 
-  die "first argument is not an invoice object"
+  carp "get_agreement_with_invoice needs an invoice object as its first argument"
     unless ref($invoice) eq 'SL::DB::Invoice' or ref($invoice) eq 'SL::DB::PurchaseInvoice';
 
   my %points = (
@@ -63,17 +70,27 @@ sub get_agreement_with_invoice {
     depositor_matches           => 2,
     exact_amount                => 4,
     exact_open_amount           => 4,
-    invnumber_in_purpose        => 2,
+    invoice_in_purpose          => 2,
+    own_invoice_in_purpose      => 5,
+    invnumber_in_purpose        => 1,
+    own_invnumber_in_purpose    => 4,
     # overpayment                 => -1, # either other invoice is more likely, or several invoices paid at once
     payment_before_invoice      => -2,
     payment_within_30_days      => 1,
     remote_account_number       => 3,
     skonto_exact_amount         => 5,
-    wrong_sign                  => -1,
+    wrong_sign                  => -4,
+    sepa_export_item            => 5,
+    batch_sepa_transaction      => 20,
   );
 
   my ($agreement,$rule_matches);
 
+  if ( $self->is_batch_transaction && $self->{sepa_export_ok}) {
+    $agreement += $points{batch_sepa_transaction};
+    $rule_matches .= 'batch_sepa_transaction(' . $points{'batch_sepa_transaction'} . ') ';
+  }
+
   # compare banking arrangements
   my ($iban, $bank_code, $account_number);
   $bank_code      = $invoice->customer->bank_code      if $invoice->is_sales;
@@ -82,52 +99,82 @@ sub get_agreement_with_invoice {
   $bank_code      = $invoice->vendor->bank_code        if ! $invoice->is_sales;
   $iban           = $invoice->vendor->iban             if ! $invoice->is_sales;
   $account_number = $invoice->vendor->account_number   if ! $invoice->is_sales;
-  if ( $bank_code eq $self->remote_bank_code && $account_number eq $self->remote_account_number ) {
-    $agreement += $points{remote_account_number};
-    $rule_matches .= 'remote_account_number(' . $points{'remote_account_number'} . ') ';
-  };
-  if ( $iban eq $self->remote_account_number ) {
-    $agreement += $points{remote_account_number};
-    $rule_matches .= 'remote_account_number(' . $points{'remote_account_number'} . ') ';
-  };
+
+  # check only valid remote_account_number (with some content)
+  if ($self->remote_account_number) {
+    if ($bank_code eq $self->remote_bank_code && $account_number eq $self->remote_account_number) {
+      $agreement += $points{remote_account_number};
+      $rule_matches .= 'remote_account_number(' . $points{'remote_account_number'} . ') ';
+    } elsif ($iban eq $self->remote_account_number) { # elsif -> do not add twice
+      $agreement += $points{remote_account_number};
+      $rule_matches .= 'remote_account_number(' . $points{'remote_account_number'} . ') ';
+    }
+  }
 
   my $datediff = $self->transdate->{utc_rd_days} - $invoice->transdate->{utc_rd_days};
   $invoice->{datediff} = $datediff;
 
   # compare amount
-  if (abs(abs($invoice->amount) - abs($self->amount)) < 0.01) {
+  if (abs(abs($invoice->amount) - abs($self->amount)) < 0.01 &&
+        $::form->format_amount(\%::myconfig,abs($invoice->amount),2) eq
+        $::form->format_amount(\%::myconfig,abs($self->amount),2)
+      ) {
     $agreement += $points{exact_amount};
     $rule_matches .= 'exact_amount(' . $points{'exact_amount'} . ') ';
-  };
+  }
 
   # compare open amount, preventing double points when open amount = invoice amount
-  if ( $invoice->amount != $invoice->open_amount && abs(abs($invoice->open_amount) - abs($self->amount)) < 0.01) {
+  if ( $invoice->amount != $invoice->open_amount && abs(abs($invoice->open_amount) - abs($self->amount)) < 0.01 &&
+         $::form->format_amount(\%::myconfig,abs($invoice->open_amount),2) eq
+         $::form->format_amount(\%::myconfig,abs($self->amount),2)
+       ) {
     $agreement += $points{exact_open_amount};
     $rule_matches .= 'exact_open_amount(' . $points{'exact_open_amount'} . ') ';
-  };
+  }
 
-  if ( $invoice->skonto_date && abs(abs($invoice->amount_less_skonto) - abs($self->amount)) < 0.01) {
+  if ( $invoice->skonto_date && abs(abs($invoice->amount_less_skonto) - abs($self->amount)) < 0.01 &&
+         $::form->format_amount(\%::myconfig,abs($invoice->amount_less_skonto),2) eq
+         $::form->format_amount(\%::myconfig,abs($self->amount),2)
+       ) {
     $agreement += $points{skonto_exact_amount};
     $rule_matches .= 'skonto_exact_amount(' . $points{'skonto_exact_amount'} . ') ';
-  };
+    $invoice->{skonto_type} = 'with_skonto_pt';
+  }
 
   #search invoice number in purpose
   my $invnumber = $invoice->invnumber;
-  # invnumbernhas to have at least 3 characters
-  if ( length($invnumber) > 2 && $self->purpose =~ /\b$invnumber\b/i ) {
-    $agreement += $points{invnumber_in_purpose};
-    $rule_matches .= 'invnumber_in_purpose(' . $points{'invnumber_in_purpose'} . ') ';
-  };
+  # invnumber has to have at least 3 characters
+  my $squashed_purpose = $self->purpose;
+  $squashed_purpose =~ s/ //g;
+  if (length($invnumber) > 4 && $squashed_purpose =~ /$invnumber/ && $invoice->is_sales){
+    $agreement      += $points{own_invoice_in_purpose};
+    $rule_matches   .= 'own_invoice_in_purpose(' . $points{'own_invoice_in_purpose'} . ') ';
+  } elsif (length($invnumber) > 3 && $squashed_purpose =~ /$invnumber/ ) {
+    $agreement      += $points{invoice_in_purpose};
+    $rule_matches   .= 'invoice_in_purpose(' . $points{'invoice_in_purpose'} . ') ';
+  } else {
+    # only check number part of invoice number
+    $invnumber      =~ s/[A-Za-z_]+//g;
+    if (length($invnumber) > 4 && $squashed_purpose =~ /$invnumber/ && $invoice->is_sales){
+      $agreement    += $points{own_invnumber_in_purpose};
+      $rule_matches .= 'own_invnumber_in_purpose(' . $points{'own_invnumber_in_purpose'} . ') ';
+    } elsif (length($invnumber) > 3 && $squashed_purpose =~ /$invnumber/ ) {
+      $agreement    += $points{invnumber_in_purpose};
+      $rule_matches .= 'invnumber_in_purpose(' . $points{'invnumber_in_purpose'} . ') ';
+    }
+  }
 
   #check sign
-  if ( $invoice->is_sales && $self->amount < 0 ) {
+  if (( $invoice->is_sales && $invoice->amount > 0 && $self->amount < 0 ) ||
+      ( $invoice->is_sales && $invoice->amount < 0 && $self->amount > 0 )     ) { # sales credit note
     $agreement += $points{wrong_sign};
     $rule_matches .= 'wrong_sign(' . $points{'wrong_sign'} . ') ';
-  };
-  if ( ! $invoice->is_sales && $self->amount > 0 ) {
+  }
+  if (( !$invoice->is_sales && $invoice->amount > 0 && $self->amount > 0)  ||
+      ( !$invoice->is_sales && $invoice->amount < 0 && $self->amount < 0)     ) { # purchase credit note
     $agreement += $points{wrong_sign};
     $rule_matches .= 'wrong_sign(' . $points{'wrong_sign'} . ') ';
-  };
+  }
 
   # search customer/vendor number in purpose
   my $cvnumber;
@@ -142,10 +189,10 @@ sub get_agreement_with_invoice {
   my $cvname;
   $cvname = $invoice->customer->name if $invoice->is_sales;
   $cvname = $invoice->vendor->name   if ! $invoice->is_sales;
-  if ( $cvname && $self->purpose =~ /\b$cvname\b/i ) {
+  if ( $cvname && $self->purpose =~ /\b\Q$cvname\E\b/i ) {
     $agreement += $points{cust_vend_name_in_purpose};
     $rule_matches .= 'cust_vend_name_in_purpose(' . $points{'cust_vend_name_in_purpose'} . ') ';
-  };
+  }
 
   # compare depositorname, don't try to match empty depositors
   my $depositorname;
@@ -154,24 +201,24 @@ sub get_agreement_with_invoice {
   if ( $depositorname && $self->remote_name =~ /$depositorname/ ) {
     $agreement += $points{depositor_matches};
     $rule_matches .= 'depositor_matches(' . $points{'depositor_matches'} . ') ';
-  };
+  }
 
   #Check if words in remote_name appear in cvname
   my $check_string_points = _check_string($self->remote_name,$cvname);
   if ( $check_string_points ) {
     $agreement += $check_string_points;
     $rule_matches .= 'remote_name(' . $check_string_points . ') ';
-  };
+  }
 
   # transdate prefilter: compare transdate of bank_transaction with transdate of invoice
   if ( $datediff < -5 ) { # this might conflict with advance payments
     $agreement += $points{payment_before_invoice};
     $rule_matches .= 'payment_before_invoice(' . $points{'payment_before_invoice'} . ') ';
-  };
+  }
   if ( $datediff < 30 ) {
     $agreement += $points{payment_within_30_days};
     $rule_matches .= 'payment_within_30_days(' . $points{'payment_within_30_days'} . ') ';
-  };
+  }
 
   # only if we already have a good agreement, let date further change value of agreement.
   # this is so that if there are several plausible open invoices which are all equal
@@ -196,9 +243,33 @@ sub get_agreement_with_invoice {
       $agreement += $points{datebonus_negative};
       $rule_matches .= 'datebonus_negative(' . $points{'datebonus_negative'} . ') ';
     } else {
-  # e.g. datediff > 120
-    };
-  };
+      # e.g. datediff > 120
+    }
+  }
+
+  # if there is exactly one non-executed sepa_export_item for the invoice
+  my $seis = $params{sepa_export_items}
+           ? [ grep { $invoice->id == ($invoice->is_sales ? $_->ar_id : $_->ap_id) } @{ $params{sepa_export_items} } ]
+           : $invoice->find_sepa_export_items({ executed => 0 });
+  if ($seis) {
+    if (scalar @$seis == 1) {
+      my $sei = $seis->[0];
+
+      # test for amount and id matching only, sepa transfer date and bank
+      # transaction date needn't match
+      if (abs($self->amount) == ($sei->amount) && $invoice->id == $sei->arap_id) {
+        $agreement    += $points{sepa_export_item};
+        $rule_matches .= 'sepa_export_item(' . $points{'sepa_export_item'} . ') ';
+      }
+    } else {
+      # zero or more than one sepa_export_item, do nothing for this invoice
+      # zero: do nothing, no sepa_export_item exists, no match
+      # more than one: does this ever apply? Currently you can't create sepa
+      # exports for invoices that already have a non-executed sepa_export
+      # TODO: Catch the more than one case. User is allowed to split
+      # payments for one invoice item in one sepa export.
+    }
+  }
 
   return ($agreement,$rule_matches);
 };
@@ -221,6 +292,30 @@ sub _check_string {
     return $match;
 };
 
+
+sub not_assigned_amount {
+  my ($self) = @_;
+
+  my $not_assigned_amount = $self->amount - $self->invoice_amount;
+  die ("undefined state") if (abs($not_assigned_amount) > abs($self->amount));
+
+  return $not_assigned_amount;
+
+}
+sub closed_period {
+  my ($self) = @_;
+
+  # check for closed period
+  croak t8('Illegal date') unless ref $self->valutadate eq 'DateTime';
+
+
+  my $closedto = $::locale->parse_date_to_object($::instance_conf->get_closedto);
+  if ( ref $closedto && $self->valutadate < $closedto ) {
+    return 1;
+  } else {
+    return 0;
+  }
+}
 1;
 
 __END__
@@ -263,6 +358,26 @@ Example:
   my $invoice = SL::DB::Manager::Invoice->find_by(invnumber => '198');
   my ($agreement,rule_matches) = $bt->get_agreement_with_invoice($invoice);
 
+=item C<linked_invoices>
+
+Returns an array of record objects (invoices, debit, credit or gl objects)
+which are linked for this bank transaction.
+
+Returns an empty array ref if no links are found.
+Usage:
+ croak("No linked records at all") unless @{ $bt->linked_invoices() };
+
+
+=item C<not_assigned_amount>
+
+Returns the not open amount of this bank transaction.
+Dies if the return amount is higher than the original amount.
+
+=item C<closed_period>
+
+Returns 1 if the bank transaction valutadate is in a closed period, 0 if the
+valutadate of the bank transaction is not in a closed period.
+
 =back
 
 =head1 AUTHOR
diff --git a/SL/DB/BankTransactionAccTrans.pm b/SL/DB/BankTransactionAccTrans.pm
new file mode 100644 (file)
index 0000000..ef2052c
--- /dev/null
@@ -0,0 +1,13 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::BankTransactionAccTrans;
+
+use strict;
+
+use SL::DB::MetaSetup::BankTransactionAccTrans;
+use SL::DB::Manager::BankTransactionAccTrans;
+
+__PACKAGE__->meta->initialize;
+
+1;
index 9074fb9..cad8bb6 100644 (file)
@@ -6,16 +6,10 @@ use SL::DB::MetaSetup::Buchungsgruppe;
 use SL::DB::Manager::Buchungsgruppe;
 use SL::DB::Helper::ActsAsList;
 
-__PACKAGE__->meta->add_relationship(
-  inventory_account => {
-    type          => 'many to one',
-    class         => 'SL::DB::Chart',
-    column_map    => { inventory_accno_id => 'id' },
-  },
-);
-
 __PACKAGE__->meta->initialize;
 
+sub inventory_account { goto &inventory_accno; }
+
 sub validate {
   my ($self) = @_;
 
@@ -24,9 +18,9 @@ sub validate {
   if( $self->inventory_accno_id ) {
     require SL::DB::Chart;
     my $inventory_accno = SL::DB::Manager::Chart->find_by( id => $self->inventory_accno_id );
-    push(@errors, $::locale->text('Buchungsgruppe #1 needs a valid inventory account', $self->description)) unless $inventory_accno;
+    push(@errors, $::locale->text('Booking group #1 needs a valid inventory account', $self->description)) unless $inventory_accno;
   } else {
-    push @errors, $::locale->text('The Buchungsgruppe needs an inventory account.');
+    push @errors, $::locale->text('The booking group needs an inventory account.');
   };
 
   return @errors;
index 25a5536..91cdd67 100644 (file)
@@ -5,6 +5,21 @@ use strict;
 use SL::DB::MetaSetup::Business;
 use SL::DB::Manager::Business;
 
+__PACKAGE__->meta->add_relationship(
+  customers      => {
+    type         => 'one to many',
+    class        => 'SL::DB::Customer',
+    column_map   => { id => 'business_id' },
+    query_args   => [ \' id IN ( SELECT id FROM customer ) ' ],
+  },
+  vendors      => {
+    type         => 'one to many',
+    class        => 'SL::DB::Vendor',
+    column_map   => { id => 'business_id' },
+    query_args   => [ \' id IN ( SELECT id FROM vendor ) ' ],
+  },
+);
+
 __PACKAGE__->meta->initialize;
 
 sub validate {
index ee7ad6a..0697195 100644 (file)
@@ -50,10 +50,12 @@ sub get_balance {
 
   my $query = qq|SELECT SUM(amount) AS sum FROM acc_trans WHERE chart_id = ? AND transdate >= ? and transdate <= ?|;
 
-  my $startdate = $self->get_balance_starting_date;
-  my $today     = DateTime->today_local;
+  my $fromdate = $params{fromdate} || $::locale->parse_date_to_object($self->get_balance_starting_date);
+  my $todate   = $params{todate}   || DateTime->today_local;
 
-  my ($balance)  = selectfirst_array_query($::form, $self->db->dbh, $query, $self->id, $startdate, $today);
+  die "get_balance: fromdate and todate arguments must be DateTime Objects" unless ref($fromdate) eq 'DateTime' and ref($todate) eq 'DateTime';
+
+  my ($balance)  = selectfirst_array_query($::form, $self->db->dbh, $query, $self->id, $fromdate, $todate);
 
   return $balance;
 };
@@ -68,7 +70,7 @@ sub formatted_balance_dc {
   return "" unless $self->has_transaction;
 
   # return abs of current balance with the abbreviation for debit or credit behind it
-  my $balance = $self->get_balance || 0;
+  my $balance = $self->get_balance(%params) || 0;
   my $dc_abbreviation = $balance > 0 ? t8("Credit (one letter abbreviation)") : t8("Debit (one letter abbreviation)");
   my $amount = $::form->format_amount(\%::myconfig, abs($balance), 2);
 
@@ -77,20 +79,25 @@ sub formatted_balance_dc {
 
 sub number_of_transactions {
   my ($self) = @_;
-
-  my ($acc_trans) = $self->db->dbh->selectrow_array('select count(acc_trans_id) from acc_trans where chart_id = ?', {}, $self->id);
-
-  return $acc_trans;
+  require SL::DB::AccTransaction;
+  return SL::DB::Manager::AccTransaction->get_all_count( where => [ chart_id => $self->id ] );
 };
 
 sub has_transaction {
   my ($self) = @_;
 
-  my ($id) = $self->db->dbh->selectrow_array('select acc_trans_id from acc_trans where chart_id = ? limit 1', {}, $self->id) || 0;
+  $self->db->dbh->selectrow_array('select exists(select 1 from acc_trans where chart_id = ?)', {}, $self->id);
+}
 
-  $id ? return 1 : return 0;
+sub new_chart_valid {
+  my ($self) = @_;
 
-};
+  if ( $self->valid_from && DateTime->today >= $self->valid_from ) {
+    return 1;
+  } else {
+    return 0;
+  };
+}
 
 sub displayable_name {
   my ($self) = @_;
@@ -147,7 +154,7 @@ to the current date if undefined.
 Returns the tax rate of the active tax key as determined by
 C<get_active_taxkey>.
 
-=item C<get_balance>
+=item C<get_balance %PARAMS>
 
 Returns the current balance of the chart (sum of amount in acc_trans, positive
 or negative). The transactions are filtered by transdate, the maximum date is
@@ -156,11 +163,20 @@ the current day, the minimum date is determined by get_balance_starting_date.
 The balance should be same as that in the balance report for that chart, with
 the asofdate as the current day, and the accounting_method "accrual".
 
-=item C<formatted_balance_dc>
+If DateTime objects are passed via the params fromdate and todate, the balance
+is calculated only for that period.
+
+=item C<formatted_balance_dc %PARAMS>
 
 Returns a formatted version of C<get_balance>, taking the absolute value and
 adding the translated abbreviation for debit or credit after the number.
 
+Any params are passed on to C<get_balance>.
+
+=item C<number_of_transactions>
+
+Returns number of transactions that exist for this chart in acc_trans.
+
 =item C<has_transaction>
 
 Returns 1 or 0, depending whether the chart has a transaction in the database
@@ -171,6 +187,12 @@ or not.
 Returns the date of the last transaction of the chart in the database, which
 may lie in the future.
 
+=item C<new_chart_valid>
+
+Checks whether a follow-up chart is configured, and returns 1 or 0 depending on
+whether the valid_from date is before or after the current date.
+Is this even used anywhere?
+
 =back
 
 =head1 BUGS
index 81b743f..bed7e9c 100644 (file)
@@ -45,4 +45,10 @@ sub full_name_dep {
     . join '', map { " ($_)" } grep $_, $self->cp_abteilung;
 }
 
+sub formal_greeting {
+  my ($self) = @_;
+  die 'not an accessor' if @_ > 1;
+  join ' ', grep $_, $self->cp_title, $self->cp_givenname, $self->cp_name;
+}
+
 1;
diff --git a/SL/DB/ContactDepartment.pm b/SL/DB/ContactDepartment.pm
new file mode 100644 (file)
index 0000000..8a6d2bd
--- /dev/null
@@ -0,0 +1,22 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::ContactDepartment;
+
+use strict;
+
+use SL::Util qw(trim);
+
+use SL::DB::MetaSetup::ContactDepartment;
+use SL::DB::Manager::ContactDepartment;
+
+__PACKAGE__->meta->initialize;
+
+__PACKAGE__->before_save('_before_save_trim_content');
+
+sub _before_save_trim_content {
+  $_[0]->description(trim($_[0]->description));
+  return 1;
+}
+
+1;
diff --git a/SL/DB/ContactTitle.pm b/SL/DB/ContactTitle.pm
new file mode 100644 (file)
index 0000000..95737d9
--- /dev/null
@@ -0,0 +1,22 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::ContactTitle;
+
+use strict;
+
+use SL::Util qw(trim);
+
+use SL::DB::MetaSetup::ContactTitle;
+use SL::DB::Manager::ContactTitle;
+
+__PACKAGE__->meta->initialize;
+
+__PACKAGE__->before_save('_before_save_trim_content');
+
+sub _before_save_trim_content {
+  $_[0]->description(trim($_[0]->description));
+  return 1;
+}
+
+1;
index 8797a7a..b816bed 100644 (file)
@@ -5,7 +5,6 @@ use strict;
 use List::Util qw(first);
 
 require SL::DB::MetaSetup::CsvImportProfile;
-use Rose::DB::Object::Helpers qw(clone_and_reset);
 
 __PACKAGE__->meta->add_relationship(
   settings => {
@@ -39,6 +38,7 @@ sub set_defaults {
                        escape_char  => '"',
                        charset      => 'CP850',
                        numberformat => $::myconfig{numberformat},
+                       dateformat   => $::myconfig{dateformat},
                        duplicates   => 'no_check',
                       );
 
@@ -84,8 +84,8 @@ sub clone_and_reset_deep {
 
   my $clone = $self->clone_and_reset;
   $clone->settings(map { $_->clone_and_reset } $self->settings);
+  $clone->is_default(0);
   $clone->name('');
-
   return $clone;
 }
 
index b3cdf3b..5249410 100644 (file)
@@ -4,6 +4,7 @@
 package SL::DB::CsvImportReport;
 
 use strict;
+use SL::DB;
 use SL::DBUtils;
 
 use SL::DB::MetaSetup::CsvImportReport;
@@ -56,20 +57,28 @@ sub folded_status {
 sub destroy {
   my ($self) = @_;
 
-  my $dbh = $self->db->dbh;
-
-  $dbh->begin_work;
-
-  do_query($::form, $dbh, 'DELETE FROM csv_import_report_status WHERE csv_import_report_id = ?', $self->id);
-  do_query($::form, $dbh, 'DELETE FROM csv_import_report_rows WHERE csv_import_report_id = ?', $self->id);
-  do_query($::form, $dbh, 'DELETE FROM csv_import_reports WHERE id = ?', $self->id);
-
-  if ($self->profile_id) {
-    do_query($::form, $dbh, 'DELETE FROM csv_import_profile_settings WHERE csv_import_profile_id = ?', $self->profile_id);
-    do_query($::form, $dbh, 'DELETE FROM csv_import_profiles WHERE id = ?', $self->profile_id);
-  }
-
-  $dbh->commit;
+  SL::DB->client->with_transaction(sub {
+    my $dbh = SL::DB->client->dbh;
+
+    do_query($::form, $dbh, 'DELETE FROM csv_import_report_status WHERE csv_import_report_id = ?', $self->id);
+    do_query($::form, $dbh, 'DELETE FROM csv_import_report_rows WHERE csv_import_report_id = ?', $self->id);
+    do_query($::form, $dbh, 'DELETE FROM csv_import_reports WHERE id = ?', $self->id);
+
+    if ($self->profile_id) {
+      my ($is_profile_used_elsewhere) = selectfirst_array_query($::form, $dbh, <<SQL, $self->profile_id);
+        SELECT id
+        FROM csv_import_reports
+        WHERE profile_id = ?
+        LIMIT 1
+SQL
+
+      if (!$is_profile_used_elsewhere) {
+        do_query($::form, $dbh, 'DELETE FROM csv_import_profile_settings WHERE csv_import_profile_id = ?', $self->profile_id);
+        do_query($::form, $dbh, 'DELETE FROM csv_import_profiles WHERE id = ?', $self->profile_id);
+      }
+    }
+    1;
+  }) or do { die SL::DB->client->error };
 }
 
 1;
diff --git a/SL/DB/CustomDataExportQuery.pm b/SL/DB/CustomDataExportQuery.pm
new file mode 100644 (file)
index 0000000..bda91e2
--- /dev/null
@@ -0,0 +1,29 @@
+package SL::DB::CustomDataExportQuery;
+
+use strict;
+
+use SL::DB::MetaSetup::CustomDataExportQuery;
+use SL::DB::Manager::CustomDataExportQuery;
+
+__PACKAGE__->meta->add_relationship(
+  parameters => {
+    type       => 'one to many',
+    class      => 'SL::DB::CustomDataExportQueryParameter',
+    column_map => { id => 'query_id' },
+  },
+);
+
+__PACKAGE__->meta->initialize;
+
+sub used_parameter_names {
+  my ($self) = @_;
+
+  my %parameters;
+
+  my $sql_query   = $self->sql_query // '';
+  $parameters{$1} = 1 while $sql_query =~ m{<\%(.+?)\%>}g;
+
+  return sort keys %parameters;
+}
+
+1;
diff --git a/SL/DB/CustomDataExportQueryParameter.pm b/SL/DB/CustomDataExportQueryParameter.pm
new file mode 100644 (file)
index 0000000..bfefa82
--- /dev/null
@@ -0,0 +1,31 @@
+package SL::DB::CustomDataExportQueryParameter;
+
+use strict;
+
+use SL::DB::MetaSetup::CustomDataExportQueryParameter;
+use SL::DB::Manager::CustomDataExportQueryParameter;
+
+__PACKAGE__->meta->initialize;
+
+sub _default_value_type_fixed_value        { $_[0]->default_value }
+sub _default_value_type_current_user_login { $::myconfig{login} }
+
+sub _default_value_type_sql_query {
+  my ($self) = @_;
+
+  return '' if !$self->default_value;
+
+  my @result = $self->db->dbh->selectrow_array($self->default_value);
+  $::form->dberror if !@result;
+
+  return $result[0];
+}
+
+sub calculate_default_value {
+  my ($self) = @_;
+
+  my $method = "_default_value_type_" . ($self->default_value_type // '');
+  return $self->can($method) ? $self->$method : '';
+}
+
+1;
index 26ff748..eeae39a 100644 (file)
@@ -52,7 +52,7 @@ sub parse_value {
     return $self->timestamp_value(!defined($unparsed) ? undef : ref($unparsed) eq 'DateTime' ? $unparsed->clone : DateTime->from_kivitendo($unparsed));
   }
 
-  # text, textfield, select
+  # text, textfield, htmlfield and select
   $self->text_value($unparsed);
 }
 
@@ -92,7 +92,7 @@ sub value {
     return $self->timestamp_value ? $self->timestamp_value->clone->truncate(to => 'day') : undef;
   }
 
-  goto &text_value; # text, textfield and select
+  goto &text_value; # text, textfield, htmlfield and select
 }
 
 sub value_as_text {
@@ -116,7 +116,7 @@ sub value_as_text {
     return $object ? $object->displayable_name : '';
   }
 
-  goto &text_value; # text, textfield and select
+  goto &text_value; # text, textfield, htmlfield and select
 }
 
 sub is_valid {
@@ -124,8 +124,13 @@ sub is_valid {
 
   require SL::DB::CustomVariableValidity;
 
-  my $query = [config_id => $self->config_id, trans_id => $self->trans_id];
-  return (SL::DB::Manager::CustomVariableValidity->get_all_count(query => $query) == 0) ? 1 : 0;
+  # only base level custom variables can be invalid. ovverloaded ones could potentially clash on trans_id, so disallow them
+  return 1 if $self->sub_module;
+
+  $self->{is_valid} //= do {
+    my $query = [config_id => $self->config_id, trans_id => $self->trans_id];
+    (SL::DB::Manager::CustomVariableValidity->get_all_count(query => $query) == 0) ? 1 : 0;
+  }
 }
 
 1;
index ab7f83b..6ba2238 100644 (file)
@@ -37,8 +37,8 @@ sub validate {
 use constant OPTION_DEFAULTS =>
   {
     MAXLENGTH => 75,
-    WIDTH => 30,
-    HEIGHT => 5,
+    WIDTH     => 225,
+    HEIGHT    => 90,
   };
 
 sub processed_options {
@@ -75,7 +75,7 @@ sub processed_flags {
   }
 
   my $flags = $self->flags;
-  my $ret;
+  my $ret = {};
 
   foreach my $flag (split m/:/, $flags) {
     if ( $flag =~ m/(.*?)=(.*)/ ) {
@@ -117,6 +117,7 @@ sub value_col {
     customer  => 'number_value',
     vendor    => 'number_value',
     part      => 'number_value',
+    htmlfield => 'text_value',
     text      => 'text_value',
     textfield => 'text_value',
     select    => 'text_value'
index 048db7c..d72d0c9 100644 (file)
@@ -2,19 +2,40 @@ package SL::DB::Customer;
 
 use strict;
 
+use List::Util qw(first);
 use Rose::DB::Object::Helpers qw(as_tree);
 
+use SL::Locale::String qw(t8);
+use SL::DBUtils ();
 use SL::DB::MetaSetup::Customer;
 use SL::DB::Manager::Customer;
+use SL::DB::Helper::IBANValidation;
 use SL::DB::Helper::TransNumberGenerator;
+use SL::DB::Helper::VATIDNrValidation;
 use SL::DB::Helper::CustomVariables (
   module      => 'CT',
   cvars_alias => 1,
 );
+use SL::DB::Helper::DisplayableNamePreferences (
+  title   => t8('Customer'),
+  options => [ {name => 'customernumber', title => t8('Customer Number') },
+               {name => 'name',           title => t8('Name')   },
+               {name => 'street',         title => t8('Street') },
+               {name => 'city',           title => t8('City') },
+               {name => 'zipcode',        title => t8('Zipcode')},
+               {name => 'email',          title => t8('E-Mail') },
+               {name => 'phone',          title => t8('Phone')  }, ]
+);
 
 use SL::DB::VC;
 
 __PACKAGE__->meta->add_relationship(
+  additional_billing_addresses => {
+    type         => 'one to many',
+    class        => 'SL::DB::AdditionalBillingAddress',
+    column_map   => { id      => 'customer_id' },
+    manager_args => { sort_by => 'lower(additional_billing_addresses.name)' },
+  },
   shipto => {
     type         => 'one to many',
     class        => 'SL::DB::Shipto',
@@ -34,6 +55,7 @@ __PACKAGE__->meta->initialize;
 
 __PACKAGE__->before_save('_before_save_set_customernumber');
 
+
 sub _before_save_set_customernumber {
   my ($self) = @_;
 
@@ -46,6 +68,8 @@ sub validate {
 
   my @errors;
   push @errors, $::locale->text('The customer name is missing.') if !$self->name;
+  push @errors, $self->validate_ibans;
+  push @errors, $self->validate_vat_id_numbers;
 
   return @errors;
 }
@@ -56,14 +80,46 @@ sub short_address {
   return join ', ', grep { $_ } $self->street, $self->zipcode, $self->city;
 }
 
-sub displayable_name {
-  my $self = shift;
+sub last_used_ar_chart {
+  my ($self) = @_;
 
-  return join ' ', grep $_, $self->customernumber, $self->name;
+  my $query = <<EOSQL;
+    SELECT c.id
+    FROM chart c
+    JOIN acc_trans ac ON (ac.chart_id = c.id)
+    JOIN ar a ON (a.id = ac.trans_id)
+    WHERE (a.customer_id = ?)
+      AND (c.category = 'I')
+      AND (c.link !~ '_(paid|tax)')
+      AND (a.id IN (SELECT max(a2.id) FROM ar a2 WHERE a2.customer_id = ?))
+    ORDER BY ac.acc_trans_id ASC
+    LIMIT 1
+EOSQL
+
+  my ($chart_id) = SL::DBUtils::selectfirst_array_query($::form, $self->db->dbh, $query, ($self->id) x 2);
+
+  return if !$chart_id;
+  return SL::DB::Chart->load_cached($chart_id);
 }
 
 sub is_customer { 1 };
 sub is_vendor   { 0 };
 sub payment_terms { goto &payment }
+sub number { goto &customernumber }
+
+sub create_zugferd_invoices_for_this_customer {
+  my ($self) = @_;
+
+  no warnings 'once';
+  return $::instance_conf->get_create_zugferd_invoices if $self->create_zugferd_invoices == -1;
+  return $self->create_zugferd_invoices;
+}
+
+sub default_billing_address {
+  my $self = shift;
+
+  die 'not an accessor' if @_ > 1;
+  return first { $_->default_address } @{ $self->additional_billing_addresses };
+}
 
 1;
index ee9e2c2..f7eeb92 100644 (file)
@@ -2,6 +2,7 @@ package SL::DB::Default;
 
 use strict;
 
+use Carp;
 use SL::DB::MetaSetup::Default;
 
 __PACKAGE__->meta->initialize;
@@ -21,4 +22,16 @@ sub get {
   return SL::DB::Manager::Default->get_all(limit => 1)->[0];
 }
 
+sub address {
+  # Compatibility function: back in the day there was only a single
+  # address field.
+  my $self = shift;
+
+  croak("SL::DB::Default::address is a read-only accessor") if @_;
+
+  my $zipcode_city = join ' ', grep { $_ } ($self->address_zipcode, $self->address_city);
+
+  return join "\n", grep { $_ } ($self->address_street1, $self->address_street2, $zipcode_city, $self->address_country);
+}
+
 1;
index d15b3fa..45d6588 100644 (file)
@@ -14,7 +14,16 @@ use SL::DB::Helper::FlattenToForm;
 use SL::DB::Helper::LinkedRecords;
 use SL::DB::Helper::TransNumberGenerator;
 
+use SL::DB::Part;
+use SL::DB::Unit;
+
+use SL::DB::DeliveryOrder::TypeData qw(:types);
+
+use SL::Helper::Number qw(_format_total _round_total);
+
 use List::Util qw(first);
+use List::MoreUtils qw(any pairwise);
+use Math::Round qw(nhimult);
 
 __PACKAGE__->meta->add_relationship(orderitems => { type         => 'one to many',
                                                     class        => 'SL::DB::DeliveryOrderItem',
@@ -70,7 +79,11 @@ sub sales_order {
 }
 
 sub type {
-  return shift->customer_id ? 'sales_delivery_order' : 'purchase_delivery_order';
+  goto &order_type;
+}
+
+sub is_type {
+  return shift->type eq shift;
 }
 
 sub displayable_type {
@@ -98,10 +111,14 @@ sub date {
   goto &transdate;
 }
 
+sub number {
+  goto &donumber;
+}
+
 sub _clone_orderitem_cvar {
   my ($cvar) = @_;
 
-  my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
+  my $cloned = $_->clone_and_reset;
   $cloned->sub_module('delivery_order_items');
 
   return $cloned;
@@ -120,43 +137,38 @@ sub new_from {
   }
 
   my %args = ( map({ ( $_ => $source->$_ ) } qw(cp_id currency_id customer_id cusordnumber delivery_term_id department_id employee_id globalproject_id intnotes language_id notes
-                                                ordnumber payment_id reqdate salesman_id shippingpoint shipvia taxincluded taxzone_id transaction_description vendor_id
+                                                ordnumber payment_id reqdate salesman_id shippingpoint shipvia taxincluded taxzone_id transaction_description vendor_id billing_address_id
                                              )),
                closed    => 0,
-               is_sales  => !!$source->customer_id,
                delivered => 0,
+               order_type => $params{type},
                transdate => DateTime->today_local,
             );
 
   # Custom shipto addresses (the ones specific to the sales/purchase
   # record and not to the customer/vendor) are only linked from
-  # shipto -> delivery_orders. Meaning delivery_orders.shipto_id
-  # will not be filled in that case. Therefore we have to return the
-  # new shipto object as a separate object so that the caller can
-  # save it, too.
-  my $custom_shipto;
+  # shipto → delivery_orders. Meaning delivery_orders.shipto_id
+  # will not be filled in that case.
   if (!$source->shipto_id && $source->id) {
-    my $old = $source->custom_shipto;
-    if ($old) {
-      $custom_shipto = SL::DB::Shipto->new(
-        map  { +($_ => $old->$_) }
-        grep { !m{^ (?: itime | mtime | shipto_id | trans_id ) $}x }
-        map  { $_->name }
-        @{ $old->meta->columns }
-      );
-      $custom_shipto->module('DO');
-    }
+    $args{custom_shipto} = $source->custom_shipto->clone($class) if $source->can('custom_shipto') && $source->custom_shipto;
 
   } else {
     $args{shipto_id} = $source->shipto_id;
   }
 
+  # infer type from legacy fields if not given
+  $args{order_type} //= $source->customer_id ? 'sales_delivery_order'
+                      : $source->vendor_id   ? 'purchase_delivery_order'
+                      : $source->is_sales    ? 'sales_delivery_order'
+                      : croak "need some way to set delivery order type from source";
+
   my $delivery_order = $class->new(%args);
   $delivery_order->assign_attributes(%{ $params{attributes} }) if $params{attributes};
   my $items          = delete($params{items}) || $source->items_sorted;
   my %item_parents;
 
-  my @items = map {
+  # do not copy items when converting to supplier delivery order
+  my @items = $delivery_order->is_type(SUPPLIER_DELIVERY_ORDER_TYPE) ? () : map {
     my $source_item      = $_;
     my $source_item_id   = $_->$item_parent_id_column;
     my @custom_variables = map { _clone_orderitem_cvar($_) } @{ $source_item->custom_variables };
@@ -164,14 +176,15 @@ sub new_from {
     $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
     my $item_parent                  = $item_parents{$source_item_id};
 
-    SL::DB::DeliveryOrderItem->new(map({ ( $_ => $source_item->$_ ) }
+    my $current_do_item = SL::DB::DeliveryOrderItem->new(map({ ( $_ => $source_item->$_ ) }
                                          qw(base_qty cusordnumber description discount lastcost longdescription marge_price_factor parts_id price_factor price_factor_id
                                             project_id qty reqdate sellprice serialnumber transdate unit active_discount_source active_price_source
                                          )),
                                    custom_variables => \@custom_variables,
                                    ordnumber        => ref($item_parent) eq 'SL::DB::Order' ? $item_parent->ordnumber : $source_item->ordnumber,
                                  );
-
+    $current_do_item->{"converted_from_orderitems_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::Order';
+    $current_do_item;
   } @{ $items };
 
   @items = grep { $params{item_filter}->($_) } @items if $params{item_filter};
@@ -180,11 +193,144 @@ sub new_from {
 
   $delivery_order->items(\@items);
 
-  return ($delivery_order, $custom_shipto);
+  return $delivery_order;
+}
+
+sub new_from_time_recordings {
+  my ($class, $sources, %params) = @_;
+
+  croak("Unsupported object type in sources")                                      if any { ref($_) ne 'SL::DB::TimeRecording' }            @$sources;
+  croak("Cannot create delivery order from source records of different customers") if any { $_->customer_id != $sources->[0]->customer_id } @$sources;
+
+  # - one item per part (article)
+  # - qty is sum of duration
+  # - description goes to item longdescription
+  #  - ordered and summed by date
+  #  - each description goes to an ordered list
+  #  - (as time recording descriptions are formatted text by now, use stripped text)
+  #  - merge same descriptions
+  #
+
+  my $default_part_id  = $params{default_part_id}     ? $params{default_part_id}
+                       : $params{default_partnumber}  ? SL::DB::Manager::Part->find_by(partnumber => $params{default_partnumber})->id
+                       : undef;
+  my $override_part_id = $params{override_part_id}    ? $params{override_part_id}
+                       : $params{override_partnumber} ? SL::DB::Manager::Part->find_by(partnumber => $params{override_partnumber})->id
+                       : undef;
+
+  # check parts and collect entries
+  my %part_by_part_id;
+  my $entries;
+  foreach my $source (@$sources) {
+    next if !$source->duration;
+
+    my $part_id   = $override_part_id;
+    $part_id    ||= $source->part_id;
+    $part_id    ||= $default_part_id;
+
+    die 'article not found for entry "' . $source->displayable_times . '"' if !$part_id;
+
+    if (!$part_by_part_id{$part_id}) {
+      $part_by_part_id{$part_id} = SL::DB::Part->new(id => $part_id)->load;
+      die 'article unit must be time based for entry "' . $source->displayable_times . '"' if !$part_by_part_id{$part_id}->unit_obj->is_time_based;
+    }
+
+    my $date = $source->date->to_kivitendo;
+    $entries->{$part_id}->{$date}->{duration} += $params{rounding}
+                                               ? nhimult(0.25, ($source->duration_in_hours))
+                                               : _round_total($source->duration_in_hours);
+    # add content if not already in description
+    my $new_description = '' . $source->description_as_stripped_html;
+    $entries->{$part_id}->{$date}->{content} ||= '';
+    $entries->{$part_id}->{$date}->{content}  .= '<li>' . $new_description . '</li>'
+      unless $entries->{$part_id}->{$date}->{content} =~ m/\Q$new_description/;
+
+    $entries->{$part_id}->{$date}->{date_obj}  = $source->start_time || $source->date; # for sorting
+  }
+
+  my @items;
+
+  my $h_unit = SL::DB::Manager::Unit->find_h_unit;
+
+  my @keys = sort { $part_by_part_id{$a}->partnumber cmp $part_by_part_id{$b}->partnumber } keys %$entries;
+  foreach my $key (@keys) {
+    my $qty = 0;
+    my $longdescription = '';
+
+    my @dates = sort { $entries->{$key}->{$a}->{date_obj} <=> $entries->{$key}->{$b}->{date_obj} } keys %{$entries->{$key}};
+    foreach my $date (@dates) {
+      my $entry = $entries->{$key}->{$date};
+
+      $qty             += $entry->{duration};
+      $longdescription .= $date . ' <strong>' . _format_total($entry->{duration}) . ' h</strong>';
+      $longdescription .= '<ul>';
+      $longdescription .= $entry->{content};
+      $longdescription .= '</ul>';
+    }
+
+    my $item = SL::DB::DeliveryOrderItem->new(
+      parts_id        => $part_by_part_id{$key}->id,
+      description     => $part_by_part_id{$key}->description,
+      qty             => $qty,
+      base_qty        => $h_unit->convert_to($qty, $part_by_part_id{$key}->unit_obj),
+      unit_obj        => $h_unit,
+      sellprice       => $part_by_part_id{$key}->sellprice, # Todo: use price rules to get sellprice
+      longdescription => $longdescription,
+    );
+
+    push @items, $item;
+  }
+
+  my $delivery_order;
+
+  if ($params{related_order}) {
+    # collect suitable items in related order
+    my @items_to_use;
+    my @new_attributes;
+    foreach my $item (@items) {
+      my $item_to_use = first {$item->parts_id == $_->parts_id} @{ $params{related_order}->items_sorted };
+
+      die "no suitable item found in related order" if !$item_to_use;
+
+      my %new_attributes;
+      $new_attributes{$_} = $item->$_ for qw(qty base_qty unit_obj longdescription);
+      push @items_to_use,   $item_to_use;
+      push @new_attributes, \%new_attributes;
+    }
+
+    $delivery_order = $class->new_from($params{related_order}, items => \@items_to_use, %params);
+    pairwise { $a->assign_attributes( %$b) } @{$delivery_order->items}, @new_attributes;
+
+  } else {
+    my %args = (
+      is_sales    => 1,
+      order_type  => 'sales_delivery_order',
+      delivered   => 0,
+      customer_id => $sources->[0]->customer_id,
+      taxzone_id  => $sources->[0]->customer->taxzone_id,
+      currency_id => $sources->[0]->customer->currency_id,
+      employee_id => SL::DB::Manager::Employee->current->id,
+      salesman_id => SL::DB::Manager::Employee->current->id,
+      items       => \@items,
+    );
+    $delivery_order = $class->new(%args);
+    $delivery_order->assign_attributes(%{ $params{attributes} }) if $params{attributes};
+  }
+
+  return $delivery_order;
+}
+
+# legacy for compatibility
+# use type_data cusomtervendor and transfer direction instead
+sub is_sales {
+  if ($_[0]->order_type) {
+   return SL::DB::DeliveryOrder::TypeData::get3($_[0]->order_type, "properties", "is_customer");
+  }
+  return $_[0]{is_sales};
 }
 
 sub customervendor {
-  $_[0]->is_sales ? $_[0]->customer : $_[0]->vendor;
+  SL::DB::DeliveryOrder::TypeData::get3($_[0]->order_type, "properties", "is_customer") ? $_[0]->customer : $_[0]->vendor;
 }
 
 sub convert_to_invoice {
@@ -197,8 +343,9 @@ sub convert_to_invoice {
     require SL::DB::Invoice;
     $invoice = SL::DB::Invoice->new_from($self, %params)->post || die;
     $self->link_to_record($invoice);
+    # TODO extend link_to_record for items, otherwise long-term no d.r.y.
     foreach my $item (@{ $invoice->items }) {
-      foreach (qw(delivery_order_items)) {    # expand if needed (delivery_order_items)
+      foreach (qw(delivery_order_items)) {    # expand if needed (orderitems)
         if ($item->{"converted_from_${_}_id"}) {
           die unless $item->{id};
           RecordLinks->create_links('mode'       => 'ids',
@@ -220,6 +367,15 @@ sub convert_to_invoice {
   return $invoice;
 }
 
+sub digest {
+  my ($self) = @_;
+
+  sprintf "%s %s (%s)",
+    $self->donumber,
+    $self->customervendor->name,
+    $self->date->to_kivitendo;
+}
+
 1;
 __END__
 
@@ -265,18 +421,8 @@ quotations and purchase orders) are supported as sources.
 The conversion copies order items into delivery order items. Dates are copied
 as appropriate, e.g. the C<transdate> field will be set to the current date.
 
-Returns one or two objects depending on the context. In list context
-the new delivery order instance and a shipto instance will be
-returned. In scalar instance only the delivery order instance is
-returned.
-
-Custom shipto addresses (the ones specific to the sales/purchase
-record and not to the customer/vendor) are only linked from C<shipto>
-to C<delivery_orders>. Meaning C<delivery_orders.shipto_id> will not
-be filled in that case. That's why a separate shipto object is created
-and returned.
-
-The objects returned are not saved.
+Returns the new delivery order instance. The object returned is not
+saved.
 
 C<%params> can include the following options:
 
@@ -312,6 +458,62 @@ order.
 
 =back
 
+=item C<new_from_time_recordings $sources, %params>
+
+Creates a new C<SL::DB::DeliveryOrder> instance from the time recordings
+given as C<$sources>. All time recording entries must belong to the same
+customer. Time recordings are sorted by article and date. For each article
+a new delivery order item is created. If no article is associated with an
+entry, a default article will be used. The article given in the time
+recording entry can be overriden.
+Entries of the same date (for each article) are summed together and form a
+list entry in the long description of the item.
+
+The created delivery order object will be returnd but not saved.
+
+C<$sources> must be an array reference of C<SL::DB::TimeRecording> instances.
+
+C<%params> can include the following options:
+
+=over 2
+
+=item C<attributes>
+
+An optional hash reference. If it exists then it is used to set
+attributes of the newly created delivery order object.
+
+=item C<default_part_id>
+
+An optional part id which is used as default value if no part is set
+in the time recording entry.
+
+=item C<default_partnumber>
+
+Like C<default_part_id> but given as partnumber, not as id.
+
+=item C<override_part_id>
+
+An optional part id which is used instead of a value set in the time
+recording entry.
+
+=item C<override_partnumber>
+
+Like C<overrride_part_id> but given as partnumber, not as id.
+
+=item C<related_order>
+
+An optional C<SL::DB::Order> object. If it exists then it is used to
+generate the delivery order from that via C<new_from>.
+The generated items are created from a suitable item of the related
+order. If no suitable item is found, an exception is thrown.
+
+=item C<rounding>
+
+An optional boolean value. If truish, then the durations of the time entries
+are rounded up to the full quarters of an hour.
+
+=back
+
 =item C<sales_order>
 
 TODO: Describe sales_order
diff --git a/SL/DB/DeliveryOrder/TypeData.pm b/SL/DB/DeliveryOrder/TypeData.pm
new file mode 100644 (file)
index 0000000..8b523fb
--- /dev/null
@@ -0,0 +1,200 @@
+package SL::DB::DeliveryOrder::TypeData;
+
+use strict;
+use Carp;
+use Exporter qw(import);
+use Scalar::Util qw(weaken);
+use SL::Locale::String qw(t8);
+
+use constant {
+  SALES_DELIVERY_ORDER_TYPE    => 'sales_delivery_order',
+  PURCHASE_DELIVERY_ORDER_TYPE => 'purchase_delivery_order',
+  SUPPLIER_DELIVERY_ORDER_TYPE => 'supplier_delivery_order',
+  RMA_DELIVERY_ORDER_TYPE      => 'rma_delivery_order',
+};
+
+my @export_types = qw(SALES_DELIVERY_ORDER_TYPE PURCHASE_DELIVERY_ORDER_TYPE SUPPLIER_DELIVERY_ORDER_TYPE RMA_DELIVERY_ORDER_TYPE);
+my @export_subs = qw(valid_types validate_type is_valid_type get get3);
+
+our @EXPORT_OK = (@export_types, @export_subs);
+our %EXPORT_TAGS = (types => \@export_types, subs => \@export_subs);
+
+my %type_data = (
+  SALES_DELIVERY_ORDER_TYPE() => {
+    text => {
+      delete => t8('Delivery Order has been deleted'),
+      saved  => t8('Delivery Order has been saved'),
+      add    => t8("Add Sales Delivery Order"),
+      edit   => t8("Edit Sales Delivery Order"),
+      attachment => t8("sales_delivery_order_list"),
+    },
+    show_menu => {
+      save_and_quotation      => 0,
+      save_and_rfq            => 0,
+      save_and_sales_order    => 0,
+      save_and_purchase_order => 0,
+      save_and_delivery_order => 0,
+      save_and_ap_transaction => 0,
+      save_and_invoice        => 0,
+      delete                  => sub { $::instance_conf->get_sales_delivery_order_show_delete },
+      new_controller          => 0,
+    },
+    properties => {
+      customervendor => "customer",
+      is_customer    => 1,
+      nr_key         => "donumber",
+      transfer       => 'out',
+      transnumber    => 'sdonumber',
+    },
+    part_classification_query => [ "used_for_sale" => 1 ],
+    rights => {
+      edit => "sales_delivery_order_edit",
+      view => "sales_delivery_order_edit | sales_delivery_order_view",
+    },
+  },
+  PURCHASE_DELIVERY_ORDER_TYPE() => {
+    text => {
+      delete => t8('Delivery Order has been deleted'),
+      saved  => t8('Delivery Order has been saved'),
+      add    => t8("Add Purchase Delivery Order"),
+      edit   => t8("Edit Purchase Delivery Order"),
+      attachment => t8("purchase_delivery_order_list"),
+    },
+    show_menu => {
+      save_and_quotation      => 0,
+      save_and_rfq            => 0,
+      save_and_sales_order    => 0,
+      save_and_purchase_order => 0,
+      save_and_delivery_order => 0,
+      save_and_ap_transaction => 0,
+      save_and_invoice        => 0,
+      delete                  => sub { $::instance_conf->get_sales_delivery_order_show_delete },
+      new_controller          => 0,
+    },
+    properties => {
+      customervendor => "vendor",
+      is_customer    => 0,
+      nr_key         => "donumber",
+      transfer       => 'in',
+      transnumber    => 'pdonumber',
+    },
+    part_classification_query => [ "used_for_purchase" => 1 ],
+    rights => {
+      edit => "purchase_delivery_order_edit",
+      view => "purchase_delivery_order_edit | purchase_delivery_order_view",
+    },
+  },
+  SUPPLIER_DELIVERY_ORDER_TYPE() => {
+    text => {
+      delete => t8('Supplier Delivery Order has been deleted'),
+      saved  => t8('Supplier Delivery Order has been saved'),
+      add    => t8("Add Supplier Delivery Order"),
+      edit   => t8("Edit Supplier Delivery Order"),
+      attachment => t8("supplier_delivery_order_list"),
+    },
+    show_menu => {
+      save_and_quotation      => 0,
+      save_and_rfq            => 0,
+      save_and_sales_order    => 0,
+      save_and_purchase_order => 0,
+      save_and_delivery_order => 0,
+      save_and_ap_transaction => 0,
+      save_and_invoice        => 0,
+      delete                  => sub { $::instance_conf->get_sales_delivery_order_show_delete },
+      new_controller          => 1,
+    },
+    properties => {
+      customervendor => "vendor",
+      is_customer    => 0,
+      nr_key         => "donumber",
+      transfer       => 'out',
+      transnumber    => 'sudonumber',
+    },
+    part_classification_query => [ "used_for_purchase" => 1 ],
+    rights => {
+      edit => "purchase_delivery_order_edit",
+      view => "purchase_delivery_order_edit | purchase_delivery_order_view",
+    },
+  },
+  RMA_DELIVERY_ORDER_TYPE() => {
+    text => {
+      delete => t8('Delivery Order has been deleted'),
+      saved  => t8('Delivery Order has been saved'),
+      add    => t8("Add RMA Delivery Order"),
+      edit   => t8("Edit RMA Delivery Order"),
+      attachment => t8("rma_delivery_order_list"),
+    },
+    show_menu => {
+      save_and_quotation      => 0,
+      save_and_rfq            => 0,
+      save_and_sales_order    => 0,
+      save_and_purchase_order => 0,
+      save_and_delivery_order => 0,
+      save_and_ap_transaction => 0,
+      save_and_invoice        => 0,
+      delete                  => sub { $::instance_conf->get_sales_delivery_order_show_delete },
+      new_controller          => 1,
+    },
+    properties => {
+      customervendor => "customer",
+      is_customer    => 1,
+      nr_key         => "donumber",
+      transfer       => 'in',
+      transnumber    => 'rdonumber',
+    },
+    part_classification_query => [ "used_for_sale" => 1 ],
+    rights => {
+      edit => "sales_delivery_order_edit",
+      view => "sales_delivery_order_edit | sales_delivery_order_view",
+    },
+  },
+);
+
+my @valid_types = (
+  SALES_DELIVERY_ORDER_TYPE,
+  PURCHASE_DELIVERY_ORDER_TYPE,
+  SUPPLIER_DELIVERY_ORDER_TYPE,
+  RMA_DELIVERY_ORDER_TYPE,
+);
+
+my %valid_types = map { $_ => $_ } @valid_types;
+
+sub valid_types {
+  \@valid_types;
+}
+
+sub is_valid_type {
+  !!exists $type_data{$_[0]};
+}
+
+sub validate_type {
+  my ($type) = @_;
+
+  return $valid_types{$type} // croak "invalid type '$type'";
+}
+
+sub get {
+  my ($type, $key) = @_;
+
+  croak "invalid type '$type'" unless exists $type_data{$type};
+
+  my $ret = $type_data{$type}->{$key} // die "unknown property '$key'";
+
+  ref $ret eq 'CODE'
+    ? $ret->()
+    : $ret;
+}
+
+sub get3 {
+  my ($type, $topic, $key) = @_;
+
+  croak "invalid type '$type'" unless exists $type_data{$type};
+
+  my $ret = $type_data{$type}{$topic}{$key} // croak "unknown property '$key' in topic '$topic' for type '$type'";
+
+  ref $ret eq 'CODE'
+    ? $ret->()
+    : $ret;
+}
+
+1;
index ba2c2e0..7850a64 100644 (file)
@@ -19,6 +19,17 @@ use SL::DB::Helper::CustomVariables (
 
 __PACKAGE__->meta->make_manager_class;
 
+__PACKAGE__->meta->add_relationship(
+  delivery_order_stock_entries => {
+    type         => 'one to many',
+    class        => 'SL::DB::DeliveryOrderItemsStock',
+    column_map   => { id => 'delivery_order_item_id' },
+    manager_args => {
+      with_objects => [ 'inventory' ]
+    },
+  },
+);
+
 __PACKAGE__->meta->initialize;
 
 __PACKAGE__->configure_acts_as_list(group_by => [qw(delivery_order_id)]);
@@ -27,4 +38,65 @@ __PACKAGE__->configure_acts_as_list(group_by => [qw(delivery_order_id)]);
 
 sub record { goto &delivery_order }
 
+sub displayable_delivery_order_info {
+  my ($self, $dec) = @_;
+
+  $dec //= 2;
+
+  $self->delivery_order->presenter->sales_delivery_order(display => 'inline')
+         . " " . $::form->format_amount(\%::myconfig, $self->qty, $dec) . " " . $self->unit
+         . " (" . $self->delivery_order->transdate->to_kivitendo . ")";
+};
+
+sub effective_project {
+  my ($self) = @_;
+
+  $self->project // $self->delivery_order->globalproject;
+}
+
+__END__
+
+=pod
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::DB::DeliveryOrderItem Model for the 'delivery_order_items' table
+
+=head1 SYNOPSIS
+
+This is a standard Rose::DB::Object based model and can be used as one.
+
+=head1 METHODS
+
+=over 4
+
+=item C<displayable_delivery_order_info DEC>
+
+Returns a string with information about the delivery order item in relation to
+its delivery order, specifically
+
+* the (HTML-linked) delivery order number
+
+* the qty and unit of the part in the delivery order
+
+* the date of the delivery order
+
+Doesn't include any part information, it is assumed that is already shown elsewhere.
+
+The method takes an optional argument "dec" which determines how many decimals to
+round to, as used by format_amount.
+
+  SL::DB::Manager::DeliveryOrderItem->get_first->displayable_delivery_order_info(0);
+  # 201601234 5 Stck (12.12.2016)
+
+=back
+
+=head1 AUTHORS
+
+G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
+
+=cut
+
 1;
index 8d8c72f..dc6f00d 100644 (file)
@@ -7,6 +7,19 @@ use strict;
 
 use SL::DB::MetaSetup::DeliveryOrderItemsStock;
 
+__PACKAGE__->meta->add_relationship(
+  inventory => {
+    type         => 'one to one',
+    class        => 'SL::DB::Inventory',
+    column_map   => { id => 'delivery_order_items_stock_id' },
+  },
+  unit_obj => {
+    type         => 'many to one',
+    class        => 'SL::DB::Unit',
+    column_map   => { unit => 'name' },
+  },
+);
+
 __PACKAGE__->meta->initialize;
 
 # Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
index 9be274b..c61bf0f 100644 (file)
@@ -6,10 +6,16 @@ package SL::DB::Dunning;
 use strict;
 
 use SL::DB::MetaSetup::Dunning;
+use SL::DB::Helper::LinkedRecords;
 
 __PACKAGE__->meta->initialize;
 
 # Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
 __PACKAGE__->meta->make_manager_class;
 
+
+sub date {
+  goto &transdate;
+}
+
 1;
index 9920d59..5f3a51d 100644 (file)
@@ -18,4 +18,42 @@ __PACKAGE__->meta->initialize;
 
 __PACKAGE__->attr_sorted('attachments');
 
+sub compare_to {
+  my ($self, $other) = @_;
+
+  return -1 if  $self->sent_on && !$other->sent_on;
+  return  1 if !$self->sent_on &&  $other->sent_on;
+
+  my $result = 0;
+  $result    = $other->sent_on <=> $self->sent_on;
+  return $result || ($self->id <=> $other->id);
+}
+
 1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::EmailJournal - RDBO model for email journal
+
+=head1 SYNOPSIS
+
+This is a standard Rose::DB::Object based model and can be used as one.
+
+=head1 METHODS
+
+=over 4
+
+=item C<compare_to $self, $other>
+
+Compares C<$self> with C<$other> and returns the newer entry.
+
+=back
+
+=cut
+
index 4cc109a..0c321b6 100644 (file)
@@ -5,6 +5,13 @@ use strict;
 use SL::DB::MetaSetup::Employee;
 use SL::DB::Manager::Employee;
 
+__PACKAGE__->meta->add_relationship(
+  project_invoice_permissions  => {
+    type       => 'many to many',
+    map_class  => 'SL::DB::EmployeeProjectInvoices',
+  },
+);
+
 __PACKAGE__->meta->initialize;
 
 sub has_right {
@@ -20,4 +27,14 @@ sub safe_name {
   return $self->name || $self->login;
 }
 
+sub auth_user {
+  my ($self) = @_;
+
+  die 'not an accessor' if scalar(@_) > 1;
+
+  require SL::DB::AuthUser;
+
+  return SL::DB::Manager::AuthUser->find_by(login => $self->login);
+}
+
 1;
diff --git a/SL/DB/EmployeeProjectInvoices.pm b/SL/DB/EmployeeProjectInvoices.pm
new file mode 100644 (file)
index 0000000..e45196a
--- /dev/null
@@ -0,0 +1,10 @@
+package SL::DB::EmployeeProjectInvoices;
+
+use strict;
+
+use SL::DB::MetaSetup::EmployeeProjectInvoices;
+use SL::DB::Manager::EmployeeProjectInvoices;
+
+__PACKAGE__->meta->initialize;
+
+1;
diff --git a/SL/DB/File.pm b/SL/DB/File.pm
new file mode 100644 (file)
index 0000000..0405b74
--- /dev/null
@@ -0,0 +1,53 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::File;
+
+use strict;
+
+use SL::DB::MetaSetup::File;
+use SL::DB::Manager::File;
+
+__PACKAGE__->meta->add_relationship(
+  full_text            => {
+    type               => 'one to one',
+    class              => 'SL::DB::FileFullText',
+    column_map         => { id => 'file_id' },
+  },
+);
+
+__PACKAGE__->meta->initialize;
+
+
+1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::File - Databaseclass for File
+
+=head1 SYNOPSIS
+
+use SL::DB::File;
+
+# synopsis...
+
+=head1 DESCRIPTION
+
+# longer description.
+
+=head1 INTERFACE
+
+=head1 DEPENDENCIES
+
+=head1 SEE ALSO
+
+=head1 AUTHOR
+
+Werner Hahn E<lt>wh@futureworldsearch.netE<gt>
+
+=cut
diff --git a/SL/DB/FileFullText.pm b/SL/DB/FileFullText.pm
new file mode 100644 (file)
index 0000000..6c6cff4
--- /dev/null
@@ -0,0 +1,13 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::FileFullText;
+
+use strict;
+
+use SL::DB::MetaSetup::FileFullText;
+use SL::DB::Manager::FileFullText;
+
+__PACKAGE__->meta->initialize;
+
+1;
index 67a4d3a..1dbf2c8 100644 (file)
@@ -2,8 +2,13 @@ package SL::DB::GLTransaction;
 
 use strict;
 
+use SL::DB::Helper::LinkedRecords;
 use SL::DB::MetaSetup::GLTransaction;
-
+use SL::Locale::String qw(t8);
+use List::Util qw(sum);
+use SL::DATEV;
+use Carp;
+use Data::Dumper;
 
 # Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
 __PACKAGE__->meta->make_manager_class;
@@ -30,11 +35,22 @@ sub abbreviation {
   return $abbreviation;
 }
 
+sub displayable_type {
+  return t8('GL Transaction');
+}
+
+sub oneline_summary {
+  my ($self) = @_;
+  my $amount =  sum map { $_->amount if $_->amount > 0 } @{$self->transactions};
+  $amount = $::form->format_amount(\%::myconfig, $amount, 2);
+  return sprintf("%s: %s %s %s (%s)", $self->abbreviation, $self->description, $self->reference, $amount, $self->transdate->to_kivitendo);
+}
+
 sub link {
   my ($self) = @_;
 
   my $html;
-  $html   = SL::Presenter->get->gl_transaction($self, display => 'inline');
+  $html   = $self->presenter->gl_transaction(display => 'inline');
 
   return $html;
 }
@@ -43,4 +59,292 @@ sub invnumber {
   return $_[0]->reference;
 }
 
+sub date { goto &gldate }
+
+sub post {
+  my ($self) = @_;
+
+  my @errors = $self->validate;
+  croak t8("Errors in GL transaction:") . "\n" . join("\n", @errors) . "\n" if scalar @errors;
+
+  # make sure all the defaults are set:
+  require SL::DB::Employee;
+  my $employee_id = SL::DB::Manager::Employee->current->id;
+  $self->type(undef);
+  $self->employee_id($employee_id) unless defined $self->employee_id || defined $self->employee;
+  $self->ob_transaction('f') unless defined $self->ob_transaction;
+  $self->cb_transaction('f') unless defined $self->cb_transaction;
+  $self->gldate(DateTime->today_local) unless defined $self->gldate; # should user even be allowed to set this manually?
+  $self->transdate(DateTime->today_local) unless defined $self->transdate;
+
+  $self->db->with_transaction(sub {
+    $self->save;
+
+    if ($::instance_conf->get_datev_check_on_gl_transaction) {
+      my $datev = SL::DATEV->new(
+        dbh      => $self->dbh,
+        trans_id => $self->id,
+      );
+
+      $datev->generate_datev_data;
+
+      if ($datev->errors) {
+         die join "\n", t8('DATEV check returned errors:'), $datev->errors;
+      }
+    }
+
+    require SL::DB::History;
+    SL::DB::History->new(
+      trans_id    => $self->id,
+      snumbers    => 'gltransaction_' . $self->id,
+      employee_id => $employee_id,
+      addition    => 'POSTED',
+      what_done   => 'gl transaction',
+    )->save;
+
+    1;
+  }) or die t8("Error when saving: #1", $self->db->error);
+
+  return $self;
+}
+
+sub add_chart_booking {
+  my ($self, %params) = @_;
+
+  require SL::DB::Chart;
+  die "add_chart_booking needs a transdate" unless $self->transdate;
+  die "add_chart_booking needs taxincluded" unless defined $self->taxincluded;
+  die "chart missing" unless $params{chart} && ref($params{chart}) eq 'SL::DB::Chart';
+  die t8('Booking needs at least one debit and one credit booking!')
+    unless $params{debit} or $params{credit}; # must exist and not be 0
+  die t8('Cannot have a value in both Debit and Credit!')
+    if defined($params{debit}) and defined($params{credit});
+
+  my $chart = $params{chart};
+
+  my $dec = delete $params{dec} // 2;
+
+  my ($netamount,$taxamount) = (0,0);
+  my $amount = $params{credit} // $params{debit}; # only one can exist
+
+  croak t8('You cannot use a negative amount with debit/credit!') if $amount < 0;
+
+  require SL::DB::Tax;
+
+  my $ct        = $chart->get_active_taxkey($self->deliverydate // $self->transdate);
+  my $chart_tax = ref $ct eq 'SL::DB::TaxKey' ? $ct->tax : undef;
+
+  my $tax = defined($params{tax_id})        ? SL::DB::Manager::Tax->find_by(id => $params{tax_id}) # 1. user param
+          : ref $chart_tax eq 'SL::DB::Tax' ? $chart_tax                                           # automatic tax
+          : SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.00);                              # no tax
+
+  die "No valid tax found. User input:" . $params{tax_id} unless ref $tax eq 'SL::DB::Tax';
+
+  if ( $tax and $tax->rate != 0 ) {
+    ($netamount, $taxamount) = Form->calculate_tax($amount, $tax->rate, $self->taxincluded, $dec);
+  } else {
+    $netamount = $amount;
+  };
+
+  if ( $params{debit} ) {
+    $amount    *= -1;
+    $netamount *= -1;
+    $taxamount *= -1;
+  };
+
+  next unless $netamount; # skip entries with netamount 0
+
+  # initialise transactions if it doesn't exist yet
+  $self->transactions([]) unless $self->transactions;
+
+  require SL::DB::AccTransaction;
+  $self->add_transactions( SL::DB::AccTransaction->new(
+    chart_id       => $chart->id,
+    chart_link     => $chart->link,
+    amount         => $netamount,
+    taxkey         => $tax->taxkey,
+    tax_id         => $tax->id,
+    transdate      => $self->transdate,
+    source         => $params{source} // '',
+    memo           => $params{memo}   // '',
+    ob_transaction => $self->ob_transaction,
+    cb_transaction => $self->cb_transaction,
+    project_id     => $params{project_id},
+  ));
+
+  # only add tax entry if amount is >= 0.01, defaults to 2 decimals
+  if ( $::form->round_amount(abs($taxamount), $dec) > 0 ) {
+    my $tax_chart = $tax->chart;
+    if ( $tax->chart ) {
+      $self->add_transactions(SL::DB::AccTransaction->new(
+                                chart_id       => $tax_chart->id,
+                                chart_link     => $tax_chart->link,
+                                amount         => $taxamount,
+                                taxkey         => $tax->taxkey,
+                                tax_id         => $tax->id,
+                                transdate      => $self->transdate,
+                                ob_transaction => $self->ob_transaction,
+                                cb_transaction => $self->cb_transaction,
+                                source         => $params{source} // '',
+                                memo           => $params{memo}   // '',
+                                project_id     => $params{project_id},
+                              ));
+    };
+  };
+  return $self;
+};
+
+sub validate {
+  my ($self) = @_;
+
+  my @errors;
+
+  if ( $self->transactions && scalar @{ $self->transactions } ) {
+    my $debit_count  = map { $_->amount } grep { $_->amount > 0 } @{ $self->transactions };
+    my $credit_count = map { $_->amount } grep { $_->amount < 0 } @{ $self->transactions };
+
+    if ( $debit_count > 1 && $credit_count > 1 ) {
+      push @errors, t8('Split entry detected. The values you have entered will result in an entry with more than one position on both debit and credit. ' .
+                       'Due to known problems involving accounting software kivitendo does not allow these.');
+    } elsif ( $credit_count == 0 && $debit_count == 0 ) {
+      push @errors, t8('Booking needs at least one debit and one credit booking!');
+    } else {
+      # transactions formally ok, now check for out of balance:
+      my $sum = sum map { $_->amount } @{ $self->transactions };
+      # compare rounded amount to 0, to get around floating point problems, e.g.
+      # $sum = -2.77555756156289e-17
+      push @errors, t8('Out of balance transaction!') . $sum unless $::form->round_amount($sum,5) == 0;
+    };
+  } else {
+    push @errors, t8('Empty transaction!');
+  };
+
+  # fields enforced by interface
+  push @errors, t8('Reference missing!')   unless $self->reference;
+  push @errors, t8('Description missing!') unless $self->description;
+
+  # date checks
+  push @errors, t8('Transaction Date missing!') unless $self->transdate && ref($self->transdate) eq 'DateTime';
+
+  if ( $self->transdate ) {
+    if ( $::form->date_closed( $self->transdate, \%::myconfig) ) {
+      if ( !$self->id ) {
+        push @errors, t8('Cannot post transaction for a closed period!')
+      } else {
+        push @errors, t8('Cannot change transaction in a closed period!')
+      };
+    };
+
+    push @errors, t8('Cannot post transaction above the maximum future booking date!')
+      if $::form->date_max_future($self->transdate, \%::myconfig);
+  }
+
+  return @errors;
+}
+
 1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+SL::DB::GLTransaction: Rose model for GL transactions (table "gl")
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<post>
+
+Takes an unsaved but initialised GLTransaction object and saves it, but first
+validates the object, sets certain defaults (e.g. employee), and then also runs
+various checks, writes history, runs DATEV check, ...
+
+Returns C<$self> on success and dies otherwise. The whole process is run inside
+a transaction. If it fails then nothing is saved to or changed in the database.
+A new transaction is only started if none are active.
+
+Example of posting a GL transaction from scratch:
+
+  my $tax_0 = SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.00);
+  my $gl_transaction = SL::DB::GLTransaction->new(
+    taxincluded => 1,
+    description => 'bar',
+    reference   => 'bla',
+    transdate   => DateTime->today_local,
+  )->add_chart_booking(
+    chart  => SL::DB::Manager::Chart->find_by( description => 'Kasse' ),
+    credit => 100,
+    tax_id => $tax_0->id,
+  )->add_chart_booking(
+    chart  => SL::DB::Manager::Chart->find_by( description => 'Bank' ),
+    debit  => 100,
+    tax_id => $tax_0->id,
+  )->post;
+
+=item C<add_chart_booking %params>
+
+Adds an acc_trans entry to an existing GL transaction, depending on the tax it
+will also automatically create the tax entry. The GL transaction already needs
+to have certain values, e.g. transdate, taxincluded, ...
+Tax can be either set via the param tax_id or it will be set automatically
+depending on the chart configuration. If not set and no configuration is found
+no tax entry will be created (taxkey 0).
+
+Mandatory params are
+
+=over 2
+
+=item * chart as an RDBO object
+
+=item * either debit OR credit (positive values)
+
+=back
+
+Optional params:
+
+=over 2
+
+=item * dec - number of decimals to round to, defaults to 2
+
+=item * source
+
+=item * memo
+
+=item * project_id
+
+=back
+
+All other values are taken directly from the GL transaction.
+
+For an example, see C<post>.
+
+After adding an acc_trans entry the GL transaction shouldn't be modified (e.g.
+values affecting the acc_trans entries, such as transdate or taxincluded
+shouldn't be changed). There is currently no method for recalculating the
+acc_trans entries after they were added.
+
+Return C<$self>, so it allows chaining.
+
+=item C<validate>
+
+Runs various checks to see if the GL transaction is ready to be C<post>ed.
+
+Will return an array of error strings if any necessary conditions aren't met.
+
+=back
+
+=head1 TODO
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
+G. Richardson E<lt>grichardson@kivitec.deE<gt>
+
+=cut
diff --git a/SL/DB/Greeting.pm b/SL/DB/Greeting.pm
new file mode 100644 (file)
index 0000000..c43edf9
--- /dev/null
@@ -0,0 +1,22 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Greeting;
+
+use strict;
+
+use SL::Util qw(trim);
+
+use SL::DB::MetaSetup::Greeting;
+use SL::DB::Manager::Greeting;
+
+__PACKAGE__->meta->initialize;
+
+__PACKAGE__->before_save('_before_save_trim_content');
+
+sub _before_save_trim_content {
+  $_[0]->description(trim($_[0]->description));
+  return 1;
+}
+
+1;
index 356d831..93848a0 100644 (file)
@@ -3,12 +3,19 @@ package SL::DB::Helper::ALL;
 use strict;
 
 use SL::DB::AccTransaction;
+use SL::DB::AdditionalBillingAddress;
+use SL::DB::ApGl;
 use SL::DB::Assembly;
+use SL::DB::AssortmentItem;
 use SL::DB::AuthClient;
 use SL::DB::AuthClientUser;
 use SL::DB::AuthClientGroup;
 use SL::DB::AuthGroup;
 use SL::DB::AuthGroupRight;
+use SL::DB::AuthMasterRight;
+use SL::DB::AuthSchemaInfo;
+use SL::DB::AuthSession;
+use SL::DB::AuthSessionContent;
 use SL::DB::AuthUser;
 use SL::DB::AuthUserConfig;
 use SL::DB::AuthUserGroup;
@@ -16,17 +23,22 @@ use SL::DB::BackgroundJob;
 use SL::DB::BackgroundJobHistory;
 use SL::DB::BankAccount;
 use SL::DB::BankTransaction;
+use SL::DB::BankTransactionAccTrans;
 use SL::DB::Bin;
 use SL::DB::Buchungsgruppe;
 use SL::DB::Business;
 use SL::DB::Chart;
 use SL::DB::Contact;
+use SL::DB::ContactDepartment;
+use SL::DB::ContactTitle;
 use SL::DB::CsvImportProfile;
 use SL::DB::CsvImportProfileSetting;
 use SL::DB::CsvImportReport;
 use SL::DB::CsvImportReportRow;
 use SL::DB::CsvImportReportStatus;
 use SL::DB::Currency;
+use SL::DB::CustomDataExportQuery;
+use SL::DB::CustomDataExportQueryParameter;
 use SL::DB::CustomVariable;
 use SL::DB::CustomVariableConfig;
 use SL::DB::CustomVariableConfigPartsgroup;
@@ -35,6 +47,7 @@ use SL::DB::Customer;
 use SL::DB::Datev;
 use SL::DB::Default;
 use SL::DB::DeliveryOrder;
+use SL::DB::DeliveryOrder::TypeData;
 use SL::DB::DeliveryOrderItem;
 use SL::DB::DeliveryOrderItemsStock;
 use SL::DB::DeliveryTerm;
@@ -45,13 +58,17 @@ use SL::DB::DunningConfig;
 use SL::DB::EmailJournal;
 use SL::DB::EmailJournalAttachment;
 use SL::DB::Employee;
+use SL::DB::EmployeeProjectInvoices;
 use SL::DB::Exchangerate;
+use SL::DB::File;
+use SL::DB::FileFullText;
 use SL::DB::Finanzamt;
 use SL::DB::FollowUp;
 use SL::DB::FollowUpAccess;
 use SL::DB::FollowUpLink;
 use SL::DB::GLTransaction;
 use SL::DB::GenericTranslation;
+use SL::DB::Greeting;
 use SL::DB::History;
 use SL::DB::Inventory;
 use SL::DB::Invoice;
@@ -66,7 +83,10 @@ use SL::DB::Object;
 use SL::DB::Order;
 use SL::DB::OrderItem;
 use SL::DB::Part;
+use SL::DB::PartClassification;
+use SL::DB::PartCustomerPrice;
 use SL::DB::PartsGroup;
+use SL::DB::PartsPriceHistory;
 use SL::DB::PaymentTerm;
 use SL::DB::PeriodicInvoice;
 use SL::DB::PeriodicInvoicesConfig;
@@ -84,8 +104,10 @@ use SL::DB::ProjectRole;
 use SL::DB::ProjectStatus;
 use SL::DB::ProjectType;
 use SL::DB::PurchaseInvoice;
-use SL::DB::RecordLink;
 use SL::DB::ReconciliationLink;
+use SL::DB::RecordLink;
+use SL::DB::RecordTemplate;
+use SL::DB::RecordTemplateItem;
 use SL::DB::RequirementSpecAcceptanceStatus;
 use SL::DB::RequirementSpecComplexity;
 use SL::DB::RequirementSpecDependency;
@@ -105,17 +127,26 @@ use SL::DB::SepaExport;
 use SL::DB::SepaExportItem;
 use SL::DB::SepaExportMessageId;
 use SL::DB::Shipto;
+use SL::DB::Shop;
+use SL::DB::ShopImage;
+use SL::DB::ShopOrder;
+use SL::DB::ShopOrderItem;
+use SL::DB::ShopPart;
 use SL::DB::Status;
+use SL::DB::Stocktaking;
 use SL::DB::Tax;
 use SL::DB::TaxKey;
 use SL::DB::TaxZone;
 use SL::DB::TaxzoneChart;
+use SL::DB::TimeRecording;
+use SL::DB::TimeRecordingArticle;
 use SL::DB::TodoUserConfig;
 use SL::DB::TransferType;
 use SL::DB::Translation;
 use SL::DB::TriggerInformation;
 use SL::DB::Unit;
 use SL::DB::UnitsLanguage;
+use SL::DB::UserPreference;
 use SL::DB::VC;
 use SL::DB::Vendor;
 use SL::DB::Warehouse;
index 1bd0806..9be3774 100644 (file)
@@ -1,17 +1,29 @@
 package SL::DB::Helper::AccountingPeriod;
 
 use strict;
+use SL::Locale::String qw(t8);
 
 use parent qw(Exporter);
 use SL::DBUtils;
-our @EXPORT = qw(get_balance_starting_date);
+our @EXPORT = qw(get_balance_starting_date get_balance_startdate_method_options);
 
 use Carp;
 
+sub get_balance_startdate_method_options {
+  [
+    { title => t8("After closed period"),                       value => "closed_to"                   },
+    { title => t8("Start of year"),                             value => "start_of_year"               },
+    { title => t8("All transactions"),                          value => "all_transactions"            },
+    { title => t8("Last opening balance or all transactions"),  value => "last_ob_or_all_transactions" },
+    { title => t8("Last opening balance or start of year"),     value => "last_ob_or_start_of_year"    },
+  ]
+}
+
 sub get_balance_starting_date {
-  my ($self,$asofdate) = @_;
+  my ($self, $asofdate, $startdate_method) = @_;
 
-  $asofdate ||= DateTime->today_local;
+  $asofdate         ||= DateTime->today_local;
+  $startdate_method ||= $::instance_conf->get_balance_startdate_method;
 
   unless ( ref $asofdate eq 'DateTime' ) {
     $asofdate = $::locale->parse_date_to_object($asofdate);
@@ -19,7 +31,6 @@ sub get_balance_starting_date {
 
   my $dbh = $::form->get_standard_dbh;
 
-  my $startdate_method = $::instance_conf->get_balance_startdate_method;
 
   # We could use the following objects to determine the starting date for
   # calculating the balance from asofdate (the reference date for the balance):
@@ -106,7 +117,12 @@ SL::DB::Helper::AccountingPeriod - Helper functions for calculating dates relati
 
 =over 4
 
-=item C<get_balance_starting_date $date>
+=item C<get_balance_startdate_method_options>
+
+Returns an arrayref of translated options for determining the startdate of a
+balance period or the yearend period. To be used as the options for a dropdown.
+
+=item C<get_balance_starting_date $date $startdate_method>
 
 Given a date this method calculates and returns the starting date of the
 financial period relative to that date, according to the configured
@@ -118,6 +134,8 @@ date-parsed.
 
 If no argument is passed the current day is assumed as default.
 
+If no startdate method is passed, the default method from defaults is used.
+
 =back
 
 =head1 BUGS
index f48e996..7ca63a3 100644 (file)
@@ -7,6 +7,7 @@ our @EXPORT = qw(move_position_up move_position_down add_to_list remove_from_lis
                  get_previous_in_list get_next_in_list get_full_list);
 
 use Carp;
+use SL::X;
 
 my %list_spec;
 
@@ -40,7 +41,7 @@ sub move_position_down {
 sub remove_from_list {
   my ($self) = @_;
 
-  my $worker = sub {
+  return $self->db->with_transaction(sub {
     remove_position($self);
 
     # Set to -1 manually because $self->update_attributes() would
@@ -56,9 +57,7 @@ sub remove_from_list {
 SQL
     $self->db->dbh->do($sql, undef, $self->$primary_key_col);
     $self->$column(undef);
-  };
-
-  return $self->db->in_transaction ? $worker->() : $self->db->do_transaction($worker);
+  });
 }
 
 sub add_to_list {
@@ -109,12 +108,10 @@ sub add_to_list {
       ${group_by}
 SQL
 
-  my $worker = sub {
+  return $self->db->with_transaction(sub {
     $self->db->dbh->do($query, undef, $new_position - 1, @values);
     $self->update_attributes($column => $new_position);
-  };
-
-  return $self->db->in_transaction ? $worker->() : $self->db->do_transaction($worker);
+  });
 }
 
 sub get_next_in_list {
@@ -144,15 +141,17 @@ sub reorder_list {
 
   my $self   = ref($class_or_self) ? $class_or_self : $class_or_self->new;
   my $column = column_name($self);
-  my $result = $self->db->do_transaction(sub {
+  my $result = $self->db->with_transaction(sub {
     my $query = qq|UPDATE | . $self->meta->table . qq| SET ${column} = ? WHERE id = ?|;
-    my $sth   = $self->db->dbh->prepare($query) || die $self->db->dbh->errstr;
+    my $sth   = $self->db->dbh->prepare($query) || SL::X::DBUtilsError->throw(msg => 'reorder_list error', db_error => $self->db->dbh->errstr);
 
     foreach my $new_position (1 .. scalar(@ids)) {
-      $sth->execute($new_position, $ids[$new_position - 1]) || die $sth->errstr;
+      $sth->execute($new_position, $ids[$new_position - 1]) || SL::X::DBUtilsError->throw(msg => 'reorder_list error', db_error => $sth->errstr);
     }
 
     $sth->finish;
+
+    1;
   });
 
   return $result;
index 6f2dd55..21645f4 100644 (file)
@@ -29,12 +29,14 @@ sub make {
 
 sub _make_by_type {
   my ($package, $name, $type) = @_;
-  _as_number ($package, $name, places => -2) if $type =~ /numeric | real | float/xi;
-  _as_percent($package, $name, places =>  2) if $type =~ /numeric | real | float/xi;
-  _as_number ($package, $name, places =>  0) if $type =~ /int/xi;
-  _as_date   ($package, $name)               if $type =~ /date | timestamp/xi;
-  _as_timestamp($package, $name)             if $type =~ /timestamp/xi;
-  _as_bool_yn($package, $name)               if $type =~ /bool/xi;
+  _as_number     ($package, $name, places => -2) if $type =~ /numeric | real | float/xi;
+  _as_null_number($package, $name, places => -2) if $type =~ /numeric | real | float/xi;
+  _as_percent    ($package, $name, places =>  2) if $type =~ /numeric | real | float/xi;
+  _as_number     ($package, $name, places =>  0) if $type =~ /int/xi;
+  _as_null_number($package, $name, places =>  0) if $type =~ /int/xi;
+  _as_date       ($package, $name)               if $type =~ /date | timestamp/xi;
+  _as_timestamp  ($package, $name)             if $type =~ /timestamp/xi;
+  _as_bool_yn    ($package, $name)               if $type =~ /bool/xi;
 }
 
 sub _as_number {
@@ -54,6 +56,23 @@ sub _as_number {
   };
 }
 
+sub _as_null_number {
+  my $package     = shift;
+  my $attribute   = shift;
+  my %params      = @_;
+
+  $params{places} = 2 if !defined($params{places});
+
+  no strict 'refs';
+  *{ $package . '::' . $attribute . '_as_null_number' } = sub {
+    my ($self, $string) = @_;
+
+    $self->$attribute($string eq '' ? undef : $::form->parse_amount(\%::myconfig, $string)) if @_ > 1;
+
+    return defined $self->$attribute ? $::form->format_amount(\%::myconfig, $self->$attribute, $params{places}) : '';
+  };
+}
+
 sub _as_percent {
   my $package     = shift;
   my $attribute   = shift;
index 5bdd4bf..c627f19 100644 (file)
@@ -116,6 +116,22 @@ sub _make_minutes {
     my $as_minutes = "${attribute}_as_minutes";
     return defined($self->$attribute) ? sprintf('%d:%02d', $self->$as_hours, $self->$as_minutes) : undef;
   };
+
+  *{ $package . '::' . $attribute . '_in_hours' } = sub {
+    my ($self, $value) = @_;
+
+    $self->$attribute(int($value * 60 + 0.5)) if @_ > 1;
+    return $self->$attribute / 60.0;
+  };
+
+  *{ $package . '::' . $attribute . '_in_hours_as_number' } = sub {
+    my ($self, $value) = @_;
+
+    my $sub = "${attribute}_in_hours";
+
+    $self->$sub($::form->parse_amount(\%::myconfig, $value)) if @_ > 1;
+    return $::form->format_amount(\%::myconfig, $self->$sub, 2);
+  };
 }
 
 1;
@@ -234,6 +250,17 @@ Access the full value as a formatted string in the form C<h:mm>,
 e.g. C<1:30> for the value 90 minutes. Parsing such a string is
 supported, too.
 
+=item C<attribute_in_hours [$new_value]>
+
+Access the full value but convert to and from hours when
+reading/writing the value.
+
+=item C<attribute_in_hours_as_number [$new_value]>
+
+Access the full value but convert to and from hours when
+reading/writing the value. The value is formatted to/parsed from the
+user's number format.
+
 =back
 
 =head1 FUNCTIONS
index b5471e0..b914e43 100644 (file)
@@ -67,7 +67,7 @@ all/restricting to wanted HTML tags in columns
 
   # In a Rose model:
   use SL::DB::Helper::AttrHTML;
-  __PACKAGE__->attr_as_html(
+  __PACKAGE__->attr_html(
     'content',
     with_stripped => 0,
     allowed_tags  => { b => [ '/' ], i => [ '/' ] },
index 77c5f19..67c4441 100644 (file)
@@ -29,7 +29,7 @@ sub _make_sorted {
 
     croak 'not an accessor' if @_ > 1;
 
-    my $next_position = (max map { $_->$position_sub // 0 } @{ $self->$unsorted_sub }) + 1;
+    my $next_position = ((max map { $_->$position_sub // 0 } @{ $self->$unsorted_sub }) // 0) + 1;
     return [
       map  { $_->[1] }
       sort { $a->[0] <=> $b->[0] }
diff --git a/SL/DB/Helper/Cache.pm b/SL/DB/Helper/Cache.pm
new file mode 100644 (file)
index 0000000..e93b551
--- /dev/null
@@ -0,0 +1,44 @@
+package SL::DB::Helper::Cache;
+
+use strict;
+use warnings;
+
+use Carp;
+
+use parent qw(Rose::DB::Cache);
+
+sub prepare_db {
+  my ($self, $db, $entry) = @_;
+
+  if (!$entry->is_prepared) {
+    # if this a dummy kivitendo dbh, don't try to actually prepare this.
+    if ($db->type =~ /KIVITENDO_EMPTY/) {
+      return;
+    }
+
+    $entry->prepared(1);
+  }
+
+  if (!$db->dbh->ping) {
+    $db->dbh(undef);
+  }
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+SL::DB::Helper::Cache - database handle caching for kivitendo
+
+=head1 DESCRIPTION
+
+This class provides database cache handling for kivitendo running
+under FastCGI. It's based on Rose::DBx::Cache::Anywhere.
+
+=head1 METHODS
+
+=head2 prepare_db( I<rose_db>, I<entry> )
+
+Overrides default method to always ping() dbh.
index 6ebebda..a6ab5ae 100644 (file)
@@ -4,6 +4,7 @@ use strict;
 use Carp;
 use Data::Dumper;
 use List::Util qw(first);
+use List::UtilsBy qw(partition_by);
 
 use constant META_CVARS => 'cvars_config';
 
@@ -83,17 +84,22 @@ sub make_cvar_by_configs {
     my $configs     = _all_configs(%params);
     my $cvars       = $self->custom_variables;
     my %cvars_by_config = map { $_->config_id => $_ } @$cvars;
+    my $invalids    = _all_invalids($self->${\ $self->meta->primary_key_columns->[0]->name }, $configs, %params);
+    my %invalids_by_config = map { $_->config_id => 1 } @$invalids;
 
     my @return = map(
       {
+        my $cvar;
         if ( $cvars_by_config{$_->id} ) {
-          $cvars_by_config{$_->id};
+          $cvar = $cvars_by_config{$_->id};
         }
         else {
-          my $cvar = _new_cvar($self, %params, config => $_);
+          $cvar = _new_cvar($self, %params, config => $_);
           $self->add_custom_variables($cvar);
-          $cvar;
         }
+        $cvar->{is_valid} = !$invalids_by_config{$_->id};
+        $cvar->{config}   = $_;
+        $cvar;
       }
       @$configs
     );
@@ -165,7 +171,17 @@ sub _all_configs {
 
   require SL::DB::CustomVariableConfig;
 
-  SL::DB::Manager::CustomVariableConfig->get_all_sorted($params{module} ? (query => [ module => $params{module} ]) : ());
+  my $cache  = $::request->cache("::SL::DB::Helper::CustomVariables::object_cache");
+
+  if (!$cache->{all}) {
+    my $configs = SL::DB::Manager::CustomVariableConfig->get_all_sorted;
+    $cache->{all}    =  $configs;
+    $cache->{module} = { partition_by { $_->module } @$configs };
+  }
+
+  return $params{module} && !ref $params{module} ? $cache->{module}{$params{module}}
+       : $params{module} &&  ref $params{module} ? [ map { @{ $cache->{module}{$_} // [] } } @{ $params{module} } ]
+       : $cache->{all};
 }
 
 sub _overload_by_module {
@@ -327,7 +343,7 @@ sub make_cvar_custom_filter {
         # remove rose aliases. query builder sadly is not reentrant, and will reuse the same aliases. :(
         $query{$key} =~ s{\bt\d+(?:\.)?\b}{}g;
 
-        # manually inline the values. again, rose doen't know how to handle bind params in subqueries :(
+        # manually inline the values. again, rose doesn't know how to handle bind params in subqueries :(
         $query{$key} =~ s{\?}{ $config->dbh->quote(shift @{ $bind_vals{$key} }) }xeg;
 
         $query{$key} =~ s{\n}{ }g;
@@ -350,6 +366,32 @@ sub make_cvar_custom_filter {
   );
 }
 
+
+sub _all_invalids {
+  my ($trans_id, $configs, %params) = @_;
+
+  require SL::DB::CustomVariableValidity;
+
+  # easy 1: no trans_id, all valid by default.
+  return [] unless $trans_id;
+
+  # easy 2: no module in params? no validity
+  return [] unless $params{module};
+
+  my %wanted_modules = ref $params{module} ? map { $_ => 1 } @{ $params{module} } : ($params{module} => 1);
+  my @module_configs = grep { $wanted_modules{$_->module} } @$configs;
+
+  return [] unless @module_configs;
+
+  # nor find all entries for that and return
+  SL::DB::Manager::CustomVariableValidity->get_all(
+    query => [
+      config_id => [ map { $_->id } @module_configs ],
+      trans_id => $trans_id,
+    ]
+  );
+}
+
 1;
 
 __END__
@@ -419,7 +461,7 @@ passed to import.
 
 =item C<cvars_by_config>
 
-Thi will return a list of CVars with the following changes over the standard accessor:
+This will return a list of CVars with the following changes over the standard accessor:
 
 =over 4
 
@@ -510,6 +552,23 @@ If the Manager for the calling C<SL::DB::Object> has included the helper L<SL::D
 
 =back
 
+=head1 BUGS AND CAVEATS
+
+=over 4
+
+=item * Conditional method export
+
+Prolonged use has shown that users expect all methods to be present or none.
+Future versions of this will likely remove the optional aliasing.
+
+=item * Semantics need to be updated
+
+There are a few transitions that are currently neither supported nor well
+defined, most of them happening when the config of a cvar gets changed, but
+whose instances have already been saved. This needs to be cleaned up.
+
+=back
+
 =head1 AUTHOR
 
 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>,
diff --git a/SL/DB/Helper/DisplayableNamePreferences.pm b/SL/DB/Helper/DisplayableNamePreferences.pm
new file mode 100644 (file)
index 0000000..5080ea2
--- /dev/null
@@ -0,0 +1,162 @@
+package SL::DB::Helper::DisplayableNamePreferences;
+
+use strict;
+
+use parent qw(Exporter);
+our @EXPORT = qw(displayable_name displayable_name_prefs displayable_name_specs specify_displayable_name_prefs);
+
+use Carp;
+use List::MoreUtils qw(none);
+
+use SL::Helper::UserPreferences::DisplayableName;
+
+
+my %prefs_specs;
+my %prefs;
+
+sub import {
+  my ($class, %params) = @_;
+  my $importing = caller();
+
+  $params{title} && $params{options}  or croak 'need params title and options';
+
+  $prefs_specs{$importing} = \%params;
+  $prefs{$importing}       = SL::Helper::UserPreferences::DisplayableName->new(
+    module => $importing
+  );
+
+  # Don't 'goto' to Exporters import, it would try to parse @params
+  __PACKAGE__->export_to_level(1, $class, @EXPORT);
+}
+
+sub displayable_name {
+  my ($self) = @_;
+
+  my $specs = $self->displayable_name_specs;
+  my $prefs = $self->displayable_name_prefs;
+
+  my @names = $prefs->get =~ m{<\%(.+?)\%>}g;
+  my $display_string = $prefs->get;
+  foreach my $name (@names) {
+    next if none {$name eq $_->{name}} @{$specs->{options}};
+    my $val         = $self->can($name) ? $self->$name // '' : '';
+    $display_string =~ s{<\%$name\%>}{$val}g;
+  }
+
+  return $display_string;
+}
+
+sub displayable_name_prefs {
+  my $class_or_self = shift;
+  my $class         = ref($class_or_self) || $class_or_self;
+
+  return $prefs{$class};
+}
+
+sub displayable_name_specs {
+  my $class_or_self = shift;
+  my $class         = ref($class_or_self) || $class_or_self;
+
+  return $prefs_specs{$class};
+}
+
+1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::Helper::DisplayableNamePreferences - Mixin for managing displayable
+names configured via user preferences
+
+=head1 SYNOPSIS
+
+  # DB object
+  package SL::DB::SomeObject;
+  use SL::DB::Helper::DisplayableNamePreferences(
+    title   => t8('Some Object'),
+    options => [ {name => 'some_attribute_1', title => t8('Some Attribute One') },
+                 {name => 'some_attribute_2,  title => t8('Some Attribute Two') },
+
+  );
+
+  # Controller using displayable_name
+  package SL::Controller::SomeController;
+  $obj       = SL::DB::SomeObject->get_first;
+  my $output = $obj->displayable_name;
+
+  # Controller configuring a displayable name
+  # can get specs to display title and options
+  # and the user prefs to read and set them
+  my specs => SL::DB::SomeObject->displayable_name_specs;
+  my prefs => SL::DB::SomeObject->displayable_name_prefs;
+
+
+This mixin provides a method C<displayable_name> for the calling module
+which returns the a string depending on the settings of the
+C<UserPreferences> (see also L<SL::Helper::UserPrefernces::DisplayableName>.
+The value in the user preferences is scanned for a pattern like E<lt>%name%E<gt>, which
+will be replaced by the value of C<$object-E<gt>name>.
+
+=head1 CONFIGURATION
+
+The mixin must be configured on import giving a hash with the following keys
+in the C<use> statement. This is stored in the specs an can be used
+in a controller setting the preferences to display them.
+
+=over 4
+
+=item C<title>
+
+The (translated) title of the object.
+
+=item C<options>
+
+The C<options> are an array ref of hash refs with the keys C<name> and C<title>.
+The C<name> is the method called to get the needed information from the object
+for which the displayable name is configured. The C<title> can be used to
+display a (translated) text in a controller setting the preferences.
+
+=back
+
+=head1 CLASS FUNCTIONS
+
+=over 4
+
+=item C<displayable_name_specs>
+
+Returns the specification given on importing this helper. This can be used
+in a controller setting the preferences to display the information to the
+user.
+
+=item C<displayable_name_prefs>
+
+This returns an instance of the L<SL::Helper::UserPrefernces::DisplayableName>
+(see there) for the calling class. This can be used to read and set the
+preferences.
+
+=back
+
+=head1 INSTANCE FUNCTIONS
+
+=over 4
+
+=item C<displayable_name>
+
+Displays the name of the object depending on the settings in the
+user preferences.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
index 83f36cd..591073e 100644 (file)
@@ -5,7 +5,7 @@ use strict;
 use parent qw(Exporter);
 our @EXPORT = qw(flatten_to_form);
 
-use List::MoreUtils qw(any);
+use List::MoreUtils qw(uniq any);
 
 sub flatten_to_form {
   my ($self, $form, %params) = @_;
@@ -15,9 +15,17 @@ sub flatten_to_form {
   _copy($self, $form, '', '', 0, qw(id type taxzone_id ordnumber quonumber invnumber donumber cusordnumber taxincluded shippingpoint shipvia notes intnotes cp_id
                                     employee_id salesman_id closed department_id language_id payment_id delivery_customer_id delivery_vendor_id shipto_id proforma
                                     globalproject_id delivered transaction_description container_type accepted_by_customer invoice storno storno_id dunning_config_id
-                                    orddate quodate reqdate gldate duedate deliverydate datepaid transdate delivery_term_id));
+                                    orddate quodate reqdate gldate duedate deliverydate datepaid transdate tax_point delivery_term_id billing_address_id));
   $form->{currency} = $form->{curr} = $self->currency_id ? $self->currency->name || '' : '';
 
+  if ( $vc eq 'customer' ) {
+    $form->{customer_id} = $self->customer_id;
+    $form->{customer}    = $self->customer->name if $self->customer;
+  } else {
+    $form->{vendor_id}   = $self->vendor_id;
+    $form->{vendor}      = $self->vendor->name if $self->vendor;
+  };
+
   if (_has($self, 'transdate')) {
     my $transdate_idx = ref($self) eq 'SL::DB::Order'   ? ($self->quotation ? 'quodate' : 'orddate')
                       : ref($self) eq 'SL::DB::Invoice' ? 'invdate'
@@ -25,30 +33,51 @@ sub flatten_to_form {
     $form->{$transdate_idx} = $self->transdate->to_lxoffice;
   }
 
-  $form->{vc} = $vc if ref($self) =~ m{^SL::DB::(?:.*Invoice|Order)};
+  $form->{vc} = $vc if ref($self) =~ m{^SL::DB::(?:.*Invoice|.*Order)};
 
   my @vc_fields          = (qw(account_number bank bank_code bic business city contact country creditlimit
-                               department_1 department_2 discount email fax homepage iban language name
-                               payment_terms phone street taxnumber ustid zipcode),
+                               department_1 department_2 discount email fax gln greeting homepage iban language name
+                               natural_person phone street taxnumber ustid zipcode),
                             "${vc}number",
-                            ($vc eq 'customer')? 'c_vendor_id': 'v_customer_id');
+                            ($vc eq 'customer')? qw(c_vendor_id c_vendor_routing_id): 'v_customer_id');
   my @vc_prefixed_fields = qw(email fax notes number phone);
 
-  _copy($self,                          $form, '',              '', 1, qw(amount netamount marge_total marge_percent container_remaining_weight container_remaining_volume paid));
+  _copy($self,                          $form, '',              '', 1, qw(amount netamount marge_total marge_percent container_remaining_weight container_remaining_volume paid exchangerate));
   _copy($self->$vc,                     $form, '',              '', 0, @vc_fields);
   _copy($self->$vc,                     $form, $vc,             '', 0, @vc_prefixed_fields);
   _copy($self->contact,                 $form, '',              '', 0, grep { /^cp_/    } map { $_->name } SL::DB::Contact->meta->columns) if _has($self, 'cp_id');
-  _copy($self->shipto,                  $form, '',              '', 0, grep { /^shipto/ } map { $_->name } SL::DB::Shipto->meta->columns)  if _has($self, 'shipto_id');
   _copy($self->globalproject,           $form, 'globalproject', '', 0, qw(number description))                                             if _has($self, 'globalproject_id');
   _copy($self->employee,                $form, 'employee_',     '', 0, map { $_->name } SL::DB::Employee->meta->columns)                   if _has($self, 'employee_id');
   _copy($self->salesman,                $form, 'salesman_',     '', 0, map { $_->name } SL::DB::Employee->meta->columns)                   if _has($self, 'salesman_id');
   _copy($self->acceptance_confirmed_by, $form, 'acceptance_confirmed_by_', '', 0, map { $_->name } SL::DB::Employee->meta->columns)        if _has($self, 'acceptance_confirmed_by_id');
 
+  # Copy selected shipto to form, if set. Else, copy custom shipto, if set.
+  my $shipto = _has($self, 'shipto_id')     ? $self->shipto
+             : _has($self, 'custom_shipto') ? $self->custom_shipto
+             : undef;
+  if ($shipto) {
+    _copy($shipto,                  $form, '',            '', 0, grep { m{^shipto(?!_id$)} } map { $_->name } SL::DB::Shipto->meta->columns);
+    _copy_custom_variables($shipto, $form, 'shiptocvar_', '');
+  }
+
+  _handle_user_data($self, $form);
+
+  # company is employee and login independent
+  $form->{"${_}_company"}  = $::instance_conf->get_company for qw (employee salesman);
+
   $form->{employee}   = $self->employee->name          if _has($self, 'employee_id');
   $form->{language}   = $self->language->template_code if _has($self, 'language_id');
   $form->{department} = $self->department->description if _has($self, 'department_id');
+  $form->{business}   = $self->$vc->business->description if _has($self->$vc, 'business_id');
   $form->{rowcount}   = scalar(@{ $self->items });
 
+  my $items_name = ref($self) eq 'SL::DB::Order'         ? 'orderitems'
+                 : ref($self) eq 'SL::DB::DeliveryOrder' ? 'delivery_order_items'
+                 : ref($self) eq 'SL::DB::Invoice'       ? 'invoice'
+                 : '';
+
+  my %cvar_validity = _determine_cvar_validity($self, $vc);
+
   my $idx = 0;
   my $format_amounts = $params{format_amounts} ? 1 : 0;
   my $format_notnull = $params{format_amounts} ? 2 : 0;
@@ -59,17 +88,22 @@ sub flatten_to_form {
 
     $idx++;
 
-    $form->{"partsgroup_${idx}"} = $item->part->partsgroup->partsgroup if _has($item->part, 'partsgroup_id');
-    _copy($item->part,    $form, '',        "_${idx}", 0,               qw(id partnumber weight));
-    _copy($item->part,    $form, '',        "_${idx}", 0,               qw(listprice));
-    _copy($item,          $form, '',        "_${idx}", 0,               qw(description project_id ship serialnumber pricegroup_id ordnumber donumber cusordnumber unit
-                                                                           subtotal longdescription price_factor_id marge_price_factor approved_sellprice reqdate transdate));
-    _copy($item,          $form, '',        "_${idx}", $format_noround, qw(qty sellprice));
-    _copy($item,          $form, '',        "_${idx}", $format_amounts, qw(marge_total marge_percent lastcost));
-    _copy($item,          $form, '',        "_${idx}", $format_percent, qw(discount));
-    _copy($item->project, $form, 'project', "_${idx}", 0,               qw(number description)) if _has($item, 'project_id');
-
-    _copy_custom_variables($item, $form, 'ic_cvar_', "_${idx}");
+    $form->{"std_warehouse_${idx}"} = $item->part->warehouse->description if _has($item->part, 'warehouse_id');
+    $form->{"std_bin_${idx}"}       = $item->part->bin->description       if _has($item->part, 'bin_id');
+    $form->{"partsgroup_${idx}"}    = $item->part->partsgroup->partsgroup if _has($item->part, 'partsgroup_id');
+    _copy($item,          $form, "${items_name}_", "_${idx}", 0,               qw(id)) if $items_name;
+    # TODO: is part_type correct here? Do we need to set part_type as default?
+    _copy($item->part,    $form, '',               "_${idx}", 0,               qw(id partnumber weight part_type));
+    _copy($item->part,    $form, '',               "_${idx}", 0,               qw(listprice));
+    _copy($item,          $form, '',               "_${idx}", 0,               qw(description project_id ship serialnumber pricegroup_id ordnumber donumber cusordnumber unit
+                                                                                  subtotal longdescription price_factor_id marge_price_factor reqdate transdate
+                                                                                  active_price_source active_discount_source optional));
+    _copy($item,          $form, '',              "_${idx}", $format_noround, qw(qty sellprice fxsellprice));
+    _copy($item,          $form, '',              "_${idx}", $format_amounts, qw(marge_total marge_percent lastcost));
+    _copy($item,          $form, '',              "_${idx}", $format_percent, qw(discount));
+    _copy($item->project, $form, 'project',       "_${idx}", 0,               qw(number description)) if _has($item, 'project_id');
+
+    _copy_custom_variables($item, $form, 'ic_cvar_', "_${idx}", $cvar_validity{items}->{ $item->parts_id });
 
     if (ref($self) eq 'SL::DB::Invoice') {
       my $date                          = $item->deliverydate ? $item->deliverydate->to_lxoffice : undef;
@@ -78,7 +112,8 @@ sub flatten_to_form {
     }
   }
 
-  _copy_custom_variables($self, $form, 'vc_cvar_', '');
+  _copy_custom_variables($self, $form, 'vc_cvar_', '', $cvar_validity{vc});
+  _copy_custom_variables($self->contact, $form, 'cp_cvar_', '') if $self->contact;
 
   return $self;
 }
@@ -103,13 +138,15 @@ sub _copy {
 }
 
 sub _copy_custom_variables {
-  my ($src, $form, $prefix, $postfix, $format_amounts) = @_;
+  my ($src, $form, $prefix, $postfix, $cvar_validity) = @_;
 
-  my $obj = (any { ref($src) eq $_ } qw(SL::DB::OrderItem SL::DB::DeliveryOrderItem SL::DB::InvoiceItem))
+  my $obj = (any { ref($src) eq $_ } qw(SL::DB::OrderItem SL::DB::DeliveryOrderItem SL::DB::InvoiceItem SL::DB::Contact SL::DB::Shipto))
           ? $src
           : $src->customervendor;
 
   foreach my $cvar (@{ $obj->cvars_by_config }) {
+    next if $cvar_validity && !$cvar_validity->{ $cvar->config_id };
+
     my $value = ($cvar->config->type =~ m{^(?:bool|customer|vendor|part)$})
               ? $cvar->value
               : $cvar->value_as_text;
@@ -120,4 +157,42 @@ sub _copy_custom_variables {
   return $src;
 }
 
+sub _determine_cvar_validity {
+  my ($self, $vc) = @_;
+
+  my @part_ids    = uniq map { $_->parts_id } @{ $self->items };
+  my @parts       = map { SL::DB::Part->new(id => $_)->load } @part_ids;
+
+  my %item_cvar_validity;
+  foreach my $part (@parts) {
+    $item_cvar_validity{ $part->id } = { map { ($_->config_id => $_->is_valid) } @{ $part->cvars_by_config } };
+  }
+
+  my %vc_cvar_validity = map { ($_->config_id => $_->is_valid) } @{ $self->$vc->cvars_by_config };
+
+  return (
+    items => \%item_cvar_validity,
+    vc    => \%vc_cvar_validity,
+  );
+}
+
+sub  _handle_user_data {
+  my ($self, $form) = @_;
+
+  foreach my $type (qw(employee salesman)) {
+    next if !_has($self, "${type}_id");
+
+    my $user = User->new(login => $self->$type->login);
+    $form->{"${type}_$_"} = $user->{$_} for qw(tel email fax signature);
+
+    if ($self->$type->deleted) {
+      for my $key (grep { $_ =~ m{^deleted_} } SL::DB::Employee->meta->columns) {
+        $key =~ s{^deleted_}{};
+        $form->{"${type}_${key}"} = $form->{"${type}_deleted_${key}"}
+      }
+    }
+
+  }
+}
+
 1;
diff --git a/SL/DB/Helper/IBANValidation.pm b/SL/DB/Helper/IBANValidation.pm
new file mode 100644 (file)
index 0000000..bc4cbf4
--- /dev/null
@@ -0,0 +1,121 @@
+package SL::DB::Helper::IBANValidation;
+
+use strict;
+
+use Algorithm::CheckDigits ();
+use Carp;
+use SL::Locale::String qw(t8);
+
+my $_validator;
+my %_countries = (
+  AT => { len => 20, name => t8('Austria') },
+  BE => { len => 16, name => t8('Belgium') },
+  CH => { len => 21, name => t8('Switzerland') },
+  CZ => { len => 24, name => t8('Czech Republic') },
+  DE => { len => 22, name => t8('Germany') },
+  DK => { len => 18, name => t8('Denmark') },
+  FR => { len => 27, name => t8('France') },
+  IT => { len => 27, name => t8('Italy') },
+  LU => { len => 20, name => t8('Luxembourg') },
+  NL => { len => 18, name => t8('Netherlands') },
+  PL => { len => 28, name => t8('Poland') },
+);
+
+sub _validate {
+  my ($self, $attribute) = @_;
+
+  my $iban =  $self->$attribute // '';
+  $iban    =~ s{\s+}{}g;
+
+  return () unless length($iban);
+
+  $_validator //= Algorithm::CheckDigits::CheckDigits('iban');
+
+  return ($::locale->text("The value '#1' is not a valid IBAN.", $iban)) if !$_validator->is_valid($iban);
+
+  my $country = $_countries{substr($iban, 0, 2)};
+
+  return () if !$country || (length($iban) == $country->{len});
+
+  return ($::locale->text("The IBAN '#1' is not valid as IBANs in #2 must be exactly #3 characters long.", $iban, $country->{name}, $country->{len}));
+}
+
+sub import {
+  my ($package, @attributes) = @_;
+
+  my $caller_package         = caller;
+  @attributes                = qw(iban) unless @attributes;
+
+  no strict 'refs';
+
+  *{ $caller_package . '::validate_ibans' } = sub {
+    my ($self) = @_;
+
+    return map { SL::DB::Helper::IBANValidation::_validate($self, $_) } @attributes;
+  };
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::Helper::IBANValidation - Mixin for validating IBAN attributes
+
+=head1 SYNOPSIS
+
+  package SL::DB::SomeObject;
+  use SL::DB::Helper::IBANValidation [ ATTRIBUTES ];
+
+  sub validate {
+    my ($self) = @_;
+
+    my @errors;
+    …
+    push @errors, $self->validate_ibans;
+
+    return @errors;
+  }
+
+This mixin provides a function C<validate_ibans> that returns a list
+of error messages, one for each attribute that fails the IBAN
+validation. If all attributes are valid or empty then an empty list
+is returned.
+
+The names of attributes to check can be given as an import list to the
+mixin package. If no attributes are given the single attribute C<iban>
+is used.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<validate_ibans>
+
+This function iterates over all configured attributes and validates
+their content according to the IBAN standard. An attribute that is
+undefined, empty or consists solely of whitespace is considered valid,
+too.
+
+The function returns a list of human-readable error messages suitable
+for use in a general C<validate> function (see SYNOPSIS). For each
+attribute failing the check the list will include one error message.
+
+If all attributes are valid then an empty list is returned.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
index 1151981..4614e5a 100644 (file)
@@ -7,6 +7,8 @@ our @ISA    = qw(Exporter);
 our @EXPORT = qw(linked_records link_to_record);
 
 use Carp;
+use List::MoreUtils qw(any);
+use List::UtilsBy qw(uniq_by);
 use Sort::Naturally;
 use SL::DBUtils;
 
@@ -50,12 +52,30 @@ sub _linked_records_implementation {
     my %from_to    = ( from => delete($params{from}) || $both,
                        to   => delete($params{to})   || $both);
 
-    my @records    = (@{ _linked_records_implementation($self, %params, direction => 'from', from => $from_to{from}) },
-                      @{ _linked_records_implementation($self, %params, direction => 'to',   to   => $from_to{to}  ) });
+    if ($params{batch} && $params{by_id}) {
+      my %results;
+      my @links = (
+        _linked_records_implementation($self, %params, direction => 'from', from => $from_to{from}),
+        _linked_records_implementation($self, %params, direction => 'to',   to   => $from_to{to}  ),
+      );
+
+      for my $by_id (@links) {
+        for (keys %$by_id) {
+          $results{$_} = defined $results{$_}
+                       ? [ uniq_by { $_->id } @{ $results{$_} }, @{ $by_id->{$_} } ]
+                       : $by_id->{$_};
+        }
+      }
+
+      return \%results;
+    } else {
+      my @records    = (@{ _linked_records_implementation($self, %params, direction => 'from', from => $from_to{from}) },
+                        @{ _linked_records_implementation($self, %params, direction => 'to',   to   => $from_to{to}  ) });
 
-    my %record_map = map { ( ref($_) . $_->id => $_ ) } @records;
+      my %record_map = map { ( ref($_) . $_->id => $_ ) } @records;
 
-    return [ values %record_map ];
+      return [ values %record_map ];
+    }
   }
 
   if ($params{via}) {
@@ -68,6 +88,7 @@ sub _linked_records_implementation {
 
   my $sub_wanted_table = "${wanted}_table";
   my $sub_wanted_id    = "${wanted}_id";
+  my $sub_myself_id    = "${myself}_id";
 
   my ($wanted_classes, $wanted_tables);
   if ($params{$wanted}) {
@@ -77,28 +98,78 @@ sub _linked_records_implementation {
 
   my @get_objects_query = ref($params{query}) eq 'ARRAY' ? @{ $params{query} } : ();
   my $get_objects       = sub {
-    my ($link)        = @_;
-    my $manager_class = SL::DB::Helper::Mappings::get_manager_package_for_table($link->$sub_wanted_table);
-    my $object_class  = SL::DB::Helper::Mappings::get_package_for_table($link->$sub_wanted_table);
-    eval "require " . $object_class . "; 1;";
-    return map {
-      $_->{_record_link_direction} = $wanted;
-      $_->{_record_link}           = $link;
-      $_
-    } @{ $manager_class->get_all(query => [ id => $link->$sub_wanted_id, @get_objects_query ]) };
+    my ($links)        = @_;
+    return [] unless @$links;
+
+    my %classes;
+    push @{ $classes{ $_->$sub_wanted_table } //= [] }, $_->$sub_wanted_id for @$links;
+
+    my @objs;
+    for (keys %classes) {
+      my $manager_class = SL::DB::Helper::Mappings::get_manager_package_for_table($_);
+      my $object_class  = SL::DB::Helper::Mappings::get_package_for_table($_);
+      eval "require " . $object_class . "; 1;";
+
+      push @objs, @{ $manager_class->get_all(
+        query         => [ id => $classes{$_}, @get_objects_query ],
+        (with_objects => $params{with_objects}) x !!$params{with_objects},
+        inject_results => 1,
+      ) };
+    }
+
+    my %objs_by_id = map { $_->id => $_ } @objs;
+
+    for (@$links) {
+      if ('ARRAY' eq ref $objs_by_id{$_->$sub_wanted_id}->{_record_link}) {
+        push @{ $objs_by_id{$_->$sub_wanted_id}->{_record_link_direction} }, $wanted;
+        push @{ $objs_by_id{$_->$sub_wanted_id}->{_record_link          } }, $_;
+      } elsif ($objs_by_id{$_->$sub_wanted_id}->{_record_link}) {
+        $objs_by_id{$_->$sub_wanted_id}->{_record_link_direction} = [
+          $objs_by_id{$_->$sub_wanted_id}->{_record_link_direction},
+          $wanted,
+        ];
+        $objs_by_id{$_->$sub_wanted_id}->{_record_link}           = [
+          $objs_by_id{$_->$sub_wanted_id}->{_record_link},
+          $_,
+        ];
+      } else {
+        $objs_by_id{$_->$sub_wanted_id}->{_record_link_direction} = $wanted;
+        $objs_by_id{$_->$sub_wanted_id}->{_record_link}           = $_;
+      }
+    }
+
+    return \@objs;
   };
 
   # If no 'via' is given then use a simple(r) method for querying the wanted objects.
   if (!$params{via} && !$params{recursive}) {
     my @query = ( "${myself}_table" => $my_table,
-                  "${myself}_id"    => $self->id );
+                  "${myself}_id"    => $params{batch} ? $params{batch} : $self->id );
     push @query, ( "${wanted}_table" => $wanted_tables ) if $wanted_tables;
 
-    return [ map { $get_objects->($_) } @{ SL::DB::Manager::RecordLink->get_all(query => [ and => \@query ]) } ];
+    my $links = SL::DB::Manager::RecordLink->get_all(query => [ and => \@query ]);
+    my $objs  = $get_objects->($links);
+
+    if ($params{batch} && $params{by_id}) {
+      return {
+        map {
+          my $id = $_;
+          $_ => [
+            grep {
+              $_->{_record_link}->$sub_myself_id == $id
+            } @$objs
+          ]
+        } @{ $params{batch} }
+      }
+    } else {
+      return $objs;
+    }
   }
 
   # More complex handling for the 'via' case.
   if ($params{via}) {
+    die 'batch mode is not supported with via' if $params{batch};
+
     my @sources = ( $self );
     my @targets = map { SL::DB::Helper::Mappings::get_table_for_package($_) } @{ ref($params{via}) ? $params{via} : [ $params{via} ] };
     push @targets, @{ $wanted_tables } if $wanted_tables;
@@ -112,9 +183,10 @@ sub _linked_records_implementation {
                       "${myself}_id"    => $src->id,
                       "${wanted}_table" => \@targets );
         push @new_sources,
-             map  { $get_objects->($_) }
-             grep { !$seen{$_->$sub_wanted_table . $_->$sub_wanted_id} }
-             @{ SL::DB::Manager::RecordLink->get_all(query => [ and => \@query ]) };
+             @{ $get_objects->([
+               grep { !$seen{$_->$sub_wanted_table . $_->$sub_wanted_id} }
+               @{ SL::DB::Manager::RecordLink->get_all(query => [ and => \@query ]) }
+             ]) };
       }
 
       @sources = @new_sources;
@@ -128,14 +200,23 @@ sub _linked_records_implementation {
 
   # And lastly recursive mode
   if ($params{recursive}) {
+    my ($id_token, @ids);
+    if ($params{batch}) {
+      $id_token = sprintf 'IN (%s)', join ', ', ('?') x @{ $params{batch} };
+      @ids      = @{ $params{batch} };
+    } else {
+      $id_token = '= ?';
+      @ids      = ($self->id);
+    }
+
     # don't use rose retrieval here. too slow.
-    # instead use recursive sql to get all the linked record_links entrys, and retrieve the objects from there
+    # instead use recursive sql to get all the linked record_links entries and retrieve the objects from there
     my $query = <<"";
       WITH RECURSIVE record_links_rec_${wanted}(id, from_table, from_id, to_table, to_id, depth, path, cycle) AS (
         SELECT id, from_table, from_id, to_table, to_id,
           1, ARRAY[id], false
         FROM record_links
-        WHERE ${myself}_id = ? and ${myself}_table = ?
+        WHERE ${myself}_id $id_token and ${myself}_table = ?
       UNION ALL
         SELECT rl.id, rl.from_table, rl.from_id, rl.to_table, rl.to_id,
           rlr.depth + 1, path || rl.id, rl.id = ANY(path)
@@ -147,25 +228,48 @@ sub _linked_records_implementation {
       WHERE NOT cycle
       ORDER BY ${wanted}_table, ${wanted}_id, depth ASC;
 
-    my $links     = selectall_hashref_query($::form, $::form->get_standard_dbh, $query, $self->id, $self->meta->table);
+    my $links     = selectall_hashref_query($::form, $::form->get_standard_dbh, $query, @ids, $self->meta->table);
 
-    return [] unless @$links;
+    if (!@$links) {
+      return $params{by_id} ? {} : [];
+    }
 
     my $link_objs = SL::DB::Manager::RecordLink->get_all(query => [ id => [ map { $_->{id} } @$links ] ]);
-    my @objects = map { $get_objects->($_) } @$link_objs;
+    my $objects = $get_objects->($link_objs);
+
+    my %links_by_id = map { $_->{id} => $_ } @$links;
 
     if ($params{save_path}) {
-       my %links_by_id = map { $_->{id} => $_ } @$links;
-       for (@objects) {
-         my $link = $links_by_id{$_->{_record_link}->id};
-         my $intermediate_links = SL::DB::Manager::RecordLink->get_all(query => [ id => $link->{path} ]);
-         $_->{_record_link_path}     = $link->{path};
-         $_->{_record_link_obj_path} = [ map { $get_objects->($_) } @$intermediate_links ];
-         $_->{_record_link_depth}    = $link->{depth};
+       for (@$objects) {
+         for my $record_link ('ARRAY' eq ref $_->{_record_link} ? @{ $_->{_record_link} } : $_->{_record_link}) {
+           my $link = $links_by_id{$record_link->id};
+           my $intermediate_links = SL::DB::Manager::RecordLink->get_all(query => [ id => $link->{path} ]);
+           $_->{_record_link_path}     = $link->{path};
+           $_->{_record_link_obj_path} = $get_objects->($intermediate_links);
+           $_->{_record_link_depth}    = $link->{depth};
+         }
        }
     }
 
-    return \@objects;
+    if ($params{batch} && $params{by_id}) {
+      my %link_obj_by_id = map { $_->id => $_ } @$link_objs;
+      return +{
+        map {
+         my $id = $_;
+         $id => [
+           grep {
+             any {
+               $link_obj_by_id{
+                 $links_by_id{$_->id}->{path}->[0]
+                }->$sub_myself_id == $id
+             } 'ARRAY' eq $_->{_record_link} ? @{ $_->{_record_link} } : $_->{_record_link}
+           } @$objects
+         ]
+        } @{ $params{batch} }
+      };
+    } else {
+      return $objects;
+    }
   }
 }
 
@@ -207,6 +311,11 @@ sub sort_linked_records {
                   'SL::DB::Invoice'         => sub { $_[0]->invnumber },
                   'SL::DB::PurchaseInvoice' => sub { $_[0]->invnumber },
                   'SL::DB::RequirementSpec' => sub { $_[0]->id },
+                  'SL::DB::Letter'          => sub { $_[0]->letternumber },
+                  'SL::DB::ShopOrder'       => sub { $_[0]->shop_ordernumber },
+                  'SL::DB::EmailJournal'    => sub { $_[0]->id },
+                  'SL::DB::Dunning'         => sub { $_[0]->dunning_id },
+                  'SL::DB::GLTransaction'   => sub { $_[0]->reference },
                   UNKNOWN                   => '9999999999999999',
                 );
   my $number_xtor = sub {
@@ -234,6 +343,11 @@ sub sort_linked_records {
               purchase_order            => 130,
               purchase_delivery_order   => 140,
               'SL::DB::PurchaseInvoice' => 150,
+              'SL::DB::GLTransaction'   => 170,
+              'SL::DB::Letter'          => 200,
+              'SL::DB::ShopOrder'       => 250,
+              'SL::DB::EmailJournal'    => 300,
+              'SL::DB::Dunning'         => 350,
               UNKNOWN                   => 999,
             );
   my $score_xtor = sub {
@@ -461,6 +575,29 @@ C<_record_link_path>
 
 =back
 
+Since record_links is comparatively expensive to call, you will want to cache
+the results for multiple objects if you know in advance you'll need them.
+
+You can pass the optional argument C<batch> with an array ref of ids which will
+be used instead of the id of the invocant. You still need to call it as a
+method on a valid object, because table information is inferred from there.
+
+C<batch> mode will currenty not work with C<via>.
+
+The optional flag C<by_id> will return the objects sorted into a hash instead
+of a plain array. Calling C<<recursive => 1, batch => [1,2], by_id => 1>> on
+ order 1:
+
+  order 1 --> delivery order 1 --> invoice 1
+  order 2 --> delivery order 2 --> invoice 2
+
+will give you:
+
+  { 1 => [ delivery order 1, invoice 1 ],
+    2 => [ delivery order 2, invoice 1 ], }
+
+you may then cache these as you see fit.
+
 
 The optional parameters C<$params{sort_by}> and C<$params{sort_dir}>
 can be used in order to sort the result. If C<$params{sort_by}> is
index b4d66da..9cc94bf 100644 (file)
@@ -2,6 +2,8 @@ package SL::DB::Helper::Manager;
 
 use strict;
 
+use Carp;
+
 use Rose::DB::Object::Manager;
 use base qw(Rose::DB::Object::Manager);
 
@@ -22,7 +24,10 @@ sub find_by_or_create {
   my $class = shift;
 
   my $found;
-  eval { $found = $class->find_by(@_); };
+  eval {
+    $found = $class->find_by(@_);
+    1;
+  } or die $@;
   return defined $found ? $found : $class->object_class->new;
 }
 
@@ -33,4 +38,88 @@ sub get_first {
   )->[0];
 }
 
+sub cache_all {
+  my $manager_class =  shift;
+  my $class         =  $manager_class;
+  $class            =~ s{Manager::}{};
+
+  croak "Caching can only be used with classes with exactly one primary key column" if 1 != scalar(@{ $class->meta->primary_key_columns });
+
+  my $all_have_been_cached =  $::request->cache("::SL::DB::Manager::cache_all");
+  return if $all_have_been_cached->{$class};
+
+  $all_have_been_cached->{$class} = 1;
+
+  my $item_cache                  = $::request->cache("::SL::DB::Object::object_cache::${class}");
+  my $primary_key                 = $class->meta->primary_key_columns->[0]->name;
+  my $objects                     = $class->_get_manager_class->get_all;
+
+  $item_cache->{$_->$primary_key} = $_ for @{ $objects};
+}
+
 1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::Helper::Manager - Base class & helper functions for all Rose manager classes
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<cache_all>
+
+Pre-caches all items from a table. Use this is you expect to need all
+items from a table. You can retrieve them later with the
+C<load_cached> function from the corresponding Rose DB object class.
+
+For example, if you expect to need all unit objects, you can use
+C<SL::DB::Manager::Unit-E<gt>cache_all> before you start the actual
+work. Later you can use C<SL::DB::Unit-E<gt>load_cached> to retrieve
+individual objects and be sure that they're already cached.
+
+=item C<find_by @where>
+
+Retrieves one item from the corresponding table matching the
+conditions given in C<@where>.
+
+This is shorthand for the following call:
+
+    SL::DB::Manager::SomeClass->get_all(where => [ … ], limit => 1)
+
+=item C<find_by_or_create @where>
+
+This calls L</find_by> with C<@where> and returns its result if one is
+found.
+
+If none is found, a new instance of the corresponding DB object class
+is created and returned. Such a new object is not inserted into the
+database automatically.
+
+=item C<get_first @args>
+
+Retrieves the first item from the database by calling C<get_all> with
+a limit of 1. The C<@args> are passed through to C<get_all> allowing
+for arbitrary filters and sort options to be applied.
+
+=item C<make_manager_methods [@method_list]>
+
+Calls Rose's C<make_manager_methods> with the C<@method_list> or
+C<all> if no methods are given.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
index 976dfa7..bc477fc 100644 (file)
@@ -84,26 +84,36 @@ my @kivitendo_blacklist = (@kivitendo_blacklist_permanent, @kivitendo_blacklist_
 my %kivitendo_package_names = (
   # TABLE                           # MODEL (given in C style)
   acc_trans                      => 'acc_transaction',
+  additional_billing_addresses   => 'additional_billing_address',
   'auth.clients'                 => 'auth_client',
   'auth.clients_users'           => 'auth_client_user',
   'auth.clients_groups'          => 'auth_client_group',
   'auth.group'                   => 'auth_group',
   'auth.group_rights'            => 'auth_group_right',
+  'auth.master_rights'           => 'auth_master_right',
+  'auth.schema_info'             => 'auth_schema_info',
+  'auth.session'                 => 'auth_session',
+  'auth.session_content'         => 'auth_session_content',
   'auth.user'                    => 'auth_user',
   'auth.user_config'             => 'auth_user_config',
   'auth.user_group'              => 'auth_user_group',
   ar                             => 'invoice',
   ap                             => 'purchase_invoice',
+  ap_gl                          => 'ap_gl',
   assembly                       => 'assembly',
+  assortment_items               => 'assortment_item',
   background_jobs                => 'background_job',
   background_job_histories       => 'background_job_history',
   ap                             => 'purchase_invoice',
   bank_accounts                  => 'bank_account',
   bank_transactions              => 'bank_transaction',
+  bank_transaction_acc_trans     => 'bank_transaction_acc_trans',
   buchungsgruppen                => 'buchungsgruppe',
   bin                            => 'bin',
   business                       => 'business',
   chart                          => 'chart',
+  contact_departments            => 'contact_department',
+  contact_titles                 => 'contact_title',
   contacts                       => 'contact',
   customer                       => 'customer',
   csv_import_profiles            => 'csv_import_profile',
@@ -112,6 +122,8 @@ my %kivitendo_package_names = (
   csv_import_report_rows         => 'csv_import_report_row',
   csv_import_report_status       => 'csv_import_report_status',
   currencies                     => 'currency',
+  custom_data_export_queries     => 'CustomDataExportQuery',
+  custom_data_export_query_parameters => 'CustomDataExportQueryParameter',
   custom_variable_config_partsgroups => 'custom_variable_config_partsgroup',
   custom_variable_configs        => 'custom_variable_config',
   custom_variables               => 'custom_variable',
@@ -129,13 +141,17 @@ my %kivitendo_package_names = (
   email_journal                  => 'EmailJournal',
   email_journal_attachments      => 'EmailJournalAttachment',
   employee                       => 'employee',
+  employee_project_invoices      => 'EmployeeProjectInvoices',
   exchangerate                   => 'exchangerate',
+  files                          => 'file',
+  file_full_texts                => 'file_full_text',
   finanzamt                      => 'finanzamt',
   follow_up_access               => 'follow_up_access',
   follow_up_links                => 'follow_up_link',
   follow_ups                     => 'follow_up',
   generic_translations           => 'generic_translation',
   gl                             => 'GLTransaction',
+  greetings                      => 'greeting',
   history_erp                    => 'history',
   inventory                      => 'inventory',
   invoice                        => 'invoice_item',
@@ -143,11 +159,15 @@ my %kivitendo_package_names = (
   letter                         => 'letter',
   letter_draft                   => 'letter_draft',
   makemodel                      => 'make_model',
+  mebil_mapping                  => 'mebil_mapping',
   notes                          => 'note',
   orderitems                     => 'order_item',
   oe                             => 'order',
   parts                          => 'part',
   partsgroup                     => 'parts_group',
+  part_classifications           => 'PartClassification',
+  part_customer_prices           => 'PartCustomerPrice',
+  parts_price_history            => 'PartsPriceHistory',
   payment_terms                  => 'payment_term',
   periodic_invoices              => 'periodic_invoice',
   periodic_invoices_configs      => 'periodic_invoices_config',
@@ -164,8 +184,10 @@ my %kivitendo_package_names = (
   project_roles                  => 'project_role',
   project_statuses               => 'project_status',
   project_types                  => 'project_type',
-  record_links                   => 'record_link',
   reconciliation_links           => 'reconciliation_link',
+  record_links                   => 'record_link',
+  record_templates               => 'record_template',
+  record_template_items          => 'record_template_item',
   requirement_spec_acceptance_statuses => 'RequirementSpecAcceptanceStatus',
   requirement_spec_complexities        => 'RequirementSpecComplexity',
   requirement_spec_item_dependencies   => 'RequirementSpecDependency',
@@ -185,17 +207,26 @@ my %kivitendo_package_names = (
   sepa_export_message_ids        => 'SepaExportMessageId',
   schema_info                    => 'schema_info',
   shipto                         => 'shipto',
+  shops                          => 'shop',
+  shop_images                    => 'shop_image',
+  shop_orders                    => 'shop_order',
+  shop_order_items               => 'shop_order_item',
+  shop_parts                     => 'shop_part',
   status                         => 'status',
+  stocktakings                   => 'stocktaking',
   tax                            => 'tax',
   taxkeys                        => 'tax_key',
   tax_zones                      => 'tax_zone',
   taxzone_charts                 => 'taxzone_chart',
+  time_recording_articles        => 'time_recording_article',
+  time_recordings                => 'time_recording',
   todo_user_config               => 'todo_user_config',
   transfer_type                  => 'transfer_type',
   translation                    => 'translation',
   trigger_information            => 'trigger_information',
   units                          => 'unit',
   units_language                 => 'units_language',
+  user_preferences               => 'user_preference',
   vendor                         => 'vendor',
   warehouse                      => 'warehouse',
 );
index 9ce1f11..c65bb28 100644 (file)
@@ -1,6 +1,7 @@
 package SL::DB::Helper::Metadata;
 
 use strict;
+use SL::X;
 
 use Rose::DB::Object::Metadata;
 use SL::DB::Helper::ConventionManager;
@@ -31,4 +32,18 @@ sub make_attr_auto_helpers {
   SL::DB::Helper::Attr::auto_make($self->class);
 }
 
+sub handle_error {
+  my($self, $object) = @_;
+
+  # these are used as Rose internal canaries, don't wrap them
+  die $object->error if UNIVERSAL::isa($object->error, 'Rose::DB::Object::Exception');
+
+  SL::X::DBRoseError->throw(
+    db_error   => $object->error,
+    class      => ref($object),
+    metaobject => $self,
+    object     => $object,
+  );
+}
+
 1;
diff --git a/SL/DB/Helper/PDF_A.pm b/SL/DB/Helper/PDF_A.pm
new file mode 100644 (file)
index 0000000..467d4c1
--- /dev/null
@@ -0,0 +1,69 @@
+package SL::DB::Helper::PDF_A;
+
+use strict;
+
+use parent qw(Exporter);
+our @EXPORT = qw(create_pdf_a_print_options);
+
+use Carp;
+use Template;
+
+sub _create_xmp_data {
+  my ($self, %params) = @_;
+
+  my $template = Template->new({
+    INTERPOLATE  => 0,
+    EVAL_PERL    => 0,
+    ABSOLUTE     => 1,
+    PLUGIN_BASE  => 'SL::Template::Plugin',
+    ENCODING     => 'utf8',
+  }) || croak;
+
+  my $output = '';
+  $template->process(SL::System::Process::exe_dir() . '/templates/pdf/pdf_a_metadata.xmp', \%params, \$output) || croak $template->error;
+
+  return $output;
+}
+
+sub create_pdf_a_print_options {
+  my ($self) = @_;
+
+  require SL::DB::Language;
+
+  my $language_code = $self->can('language_id') && $self->language_id ? SL::DB::Language->load_cached($self->language_id)->template_code : undef;
+  $language_code  ||= 'de';
+  my $pdf_language  = $language_code =~ m{deutsch|german|^de$}i   ? 'de-DE'
+                    : $language_code =~ m{englisch|english|^en$}i ? 'en-US'
+                    :                                               '';
+  my $author        = do {
+    no warnings 'once';
+    $::instance_conf->get_company
+  };
+
+  my $timestamp =  DateTime->now_local->strftime('%Y-%m-%dT%H:%M:%S%z');
+  $timestamp    =~ s{(..)$}{:$1};
+
+  return {
+    version                => '3b',
+    xmp                    => _create_xmp_data(
+      $self,
+      pdf_a_version        => '3',
+      pdf_a_conformance    => 'B',
+      producer             => 'pdfTeX',
+      timestamp            => $timestamp, # 2019-11-05T15:26:20+01:00
+      meta_data            => {
+        title              => $self->displayable_name,
+        author             => $author,
+        language           => $pdf_language,
+      },
+      zugferd              => {
+        conformance_level  => 'EXTENDED',
+        document_file_name => 'factur-x.xml',
+        document_type      => 'INVOICE',
+        version            => '1.0',
+      },
+    ),
+  };
+}
+
+1;
index ad776ca..6d624fa 100644 (file)
@@ -4,18 +4,23 @@ use strict;
 
 use parent qw(Exporter);
 our @EXPORT = qw(pay_invoice);
-our @EXPORT_OK = qw(skonto_date skonto_charts amount_less_skonto within_skonto_period percent_skonto reference_account reference_amount transactions open_amount open_percent remaining_skonto_days skonto_amount check_skonto_configuration valid_skonto_amount get_payment_suggestions validate_payment_type open_sepa_transfer_amount get_payment_select_options_for_bank_transaction);
+our @EXPORT_OK = qw(skonto_date amount_less_skonto within_skonto_period percent_skonto reference_account reference_amount open_amount open_percent remaining_skonto_days skonto_amount check_skonto_configuration valid_skonto_amount get_payment_suggestions validate_payment_type open_sepa_transfer_amount get_payment_select_options_for_bank_transaction exchangerate forex _skonto_charts_and_tax_correction);
 our %EXPORT_TAGS = (
   "ALL" => [@EXPORT, @EXPORT_OK],
 );
 
 require SL::DB::Chart;
+
+use Carp;
 use Data::Dumper;
 use DateTime;
+use List::Util qw(sum);
+
 use SL::DATEV qw(:CONSTANTS);
+use SL::DB::Exchangerate;
+use SL::DB::Currency;
+use SL::HTML::Util;
 use SL::Locale::String qw(t8);
-use List::Util qw(sum);
-use Carp;
 
 #
 # Public functions not exported by default
@@ -28,17 +33,49 @@ sub pay_invoice {
 
   my $is_sales = ref($self) eq 'SL::DB::Invoice';
   my $mult = $is_sales ? 1 : -1;  # multiplier for getting the right sign depending on ar/ap
-
-  my $paid_amount = 0; # the amount that will be later added to $self->paid
+  my @new_acc_ids;
+  my $paid_amount = 0; # the amount that will be later added to $self->paid, should be in default currency
 
   # default values if not set
   $params{payment_type} = 'without_skonto' unless $params{payment_type};
   validate_payment_type($params{payment_type});
 
-  # check for required parameters
-  Common::check_params(\%params, qw(chart_id transdate));
+  # check for required parameters and optional params depending on payment_type
+  Common::check_params(\%params, qw(chart_id transdate amount));
+  Common::check_params(\%params, qw(bt_id)) unless $params{payment_type} eq 'without_skonto';
+
+  # three valid cases, test logical params in depth, before proceeding ...
+  if ( $params{'payment_type'} eq 'without_skonto' && abs($params{'amount'}) < 0) {
+    croak "invalid amount for payment_type 'without_skonto': $params{'amount'}\n";
+  } elsif ($params{'payment_type'} eq 'free_skonto') {
+    # we dont like too much automagic for this payment type.
+    # we force caller input for amount and skonto amount
+    Common::check_params(\%params, qw(skonto_amount));
+    # secondly we dont want to handle credit notes and purchase credit notes
+    croak("Cannot use 'free skonto' for credit or debit notes") if ($params{amount} < 0 || $params{skonto_amount} <= 0);
+    # both amount have to be rounded
+    $params{skonto_amount} = _round($params{skonto_amount});
+    $params{amount}        = _round($params{amount});
+    # lastly skonto_amount has to be smaller or equal than the open invoice amount
+    if ($params{skonto_amount} > _round($self->open_amount)) {
+      croak("Skonto amount:" . $params{skonto_amount} . " higher than the payment or open invoice amount:" . $self->open_amount);
+    }
+  } elsif ( $params{'payment_type'} eq 'with_skonto_pt' ) {
+    # options with_skonto_pt doesn't require the parameter
+    # amount, but if amount is passed, make sure it matches the expected value
+    # note: the parameter isn't used at all - amount_less_skonto will always be used
+    # partial skonto payments are therefore impossible to book
+    croak "amount $params{amount} doesn't match amount less skonto: " . $self->amount_less_skonto . "\n" if $params{amount} && abs($self->amount_less_skonto - $params{amount} ) > 0.0000001;
+    croak "payment type with_skonto_pt can't be used if payments have already been made" if $self->paid != 0;
+  }
+
 
-  my $transdate_obj = $::locale->parse_date_to_object($params{transdate});
+  my $transdate_obj;
+  if (ref($params{transdate}) eq 'DateTime') {
+    $transdate_obj = $params{transdate};
+  } else {
+    $transdate_obj = $::locale->parse_date_to_object($params{transdate});
+  };
   croak t8('Illegal date') unless ref $transdate_obj;
 
   # check for closed period
@@ -52,18 +89,30 @@ sub pay_invoice {
     croak t8('Cannot post transaction above the maximum future booking date!') if $transdate_obj > DateTime->now->add( days => $::instance_conf->get_max_future_booking_interval );
   };
 
-  # input checks:
-  if ( $params{'payment_type'} eq 'without_skonto' ) {
-    croak "invalid amount for payment_type 'without_skonto': $params{'amount'}\n" unless abs($params{'amount'}) > 0;
-  };
-
-  # options with_skonto_pt and difference_as_skonto don't require the parameter
-  # amount, but if amount is passed, make sure it matches the expected value
-  if ( $params{'payment_type'} eq 'difference_as_skonto' ) {
-    croak "amount $params{amount} doesn't match open amount " . $self->open_amount . ", diff = " . ($params{amount}-$self->open_amount) if $params{amount} && abs($self->open_amount - $params{amount} ) > 0.0000001;
-  } elsif ( $params{'payment_type'} eq 'with_skonto_pt' ) {
-    croak "amount $params{amount} doesn't match amount less skonto: " . $self->open_amount . "\n" if $params{amount} && abs($self->amount_less_skonto - $params{amount} ) > 0.0000001;
-    croak "payment type with_skonto_pt can't be used if payments have already been made" if $self->paid != 0;
+  # currency is either passed or use the invoice currency if it differs from the default currency
+  # TODO remove
+  my ($exchangerate,$currency);
+  if ($params{currency} || $params{currency_id}) {
+    if ($params{currency} || $params{currency_id} ) { # currency was specified
+      $currency = SL::DB::Manager::Currency->find_by(name => $params{currency}) || SL::DB::Manager::Currency->find_by(id => $params{currency_id});
+    } else { # use invoice currency
+      $currency = SL::DB::Manager::Currency->find_by(id => $self->currency_id);
+    };
+    die "no currency" unless $currency;
+    if ($currency->id == $::instance_conf->get_currency_id) {
+      $exchangerate = 1;
+    } else {
+      my $rate = SL::DB::Manager::Exchangerate->find_by(currency_id => $currency->id,
+                                                        transdate   => $transdate_obj,
+                                                       );
+      if ($rate) {
+        $exchangerate = $is_sales ? $rate->buy : $rate->sell;
+      } else {
+        die "No exchange rate for " . $transdate_obj->to_kivitendo;
+      };
+    };
+  } else { # no currency param given or currency is the same as default_currency
+    $exchangerate = 1;
   };
 
   # absolute skonto amount for invoice, use as reference sum to see if the
@@ -75,70 +124,107 @@ sub pay_invoice {
 
   # account where money is paid to/from: bank account or cash
   my $account_bank = SL::DB::Manager::Chart->find_by(id => $params{chart_id});
-  croak "can't find bank account" unless ref $account_bank;
+  croak "can't find bank account with id " . $params{chart_id} unless ref $account_bank;
 
   my $reference_account = $self->reference_account;
   croak "can't find reference account (link = AR/AP) for invoice" unless ref $reference_account;
 
-  my $memo   = $params{'memo'}   || '';
-  my $source = $params{'source'} || '';
+  my $memo   = $params{memo}   // '';
+  my $source = $params{source} // '';
 
-  my $rounded_params_amount = _round( $params{amount} );
+  my $rounded_params_amount = _round( $params{amount} ); # / $exchangerate);
+  my $fx_gain_loss_amount = 0; # for fx_gain and fx_loss
 
   my $db = $self->db;
-  $db->do_transaction(sub {
+  $db->with_transaction(sub {
     my $new_acc_trans;
 
     # all three payment type create 1 AR/AP booking (the paid part)
-    # difference_as_skonto creates n skonto bookings (1 for each tax type)
     # with_skonto_pt creates 1 bank booking and n skonto bookings (1 for each tax type)
+    # and one tax correction as a gl booking
     # without_skonto creates 1 bank booking
 
-    # as long as there is no automatic tax, payments are always booked with
-    # taxkey 0
-
-    unless ( $params{payment_type} eq 'difference_as_skonto' ) {
-      # cases with_skonto_pt and without_skonto
+    unless ( $rounded_params_amount == 0 ) {
+      # cases with_skonto_pt, free_skonto and without_skonto
 
       # for case with_skonto_pt we need to know the corrected amount at this
-      # stage if we are going to use $params{amount}
+      # stage because we don't use $params{amount} ?!
 
       my $pay_amount = $rounded_params_amount;
       $pay_amount = $self->amount_less_skonto if $params{payment_type} eq 'with_skonto_pt';
 
       # bank account and AR/AP
-      $paid_amount += $pay_amount;
+      $paid_amount += $pay_amount * $exchangerate;
+
+      my $amount = (-1 * $pay_amount) * $mult;
+
 
       # total amount against bank, do we already know this by now?
       $new_acc_trans = SL::DB::AccTransaction->new(trans_id   => $self->id,
                                                    chart_id   => $account_bank->id,
                                                    chart_link => $account_bank->link,
-                                                   amount     => (-1 * $pay_amount) * $mult,
+                                                   amount     => $amount,
                                                    transdate  => $transdate_obj,
                                                    source     => $source,
                                                    memo       => $memo,
+                                                   project_id => $params{project_id} ? $params{project_id} : undef,
                                                    taxkey     => 0,
                                                    tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
       $new_acc_trans->save;
-    };
 
-    if ( $params{payment_type} eq 'difference_as_skonto' or $params{payment_type} eq 'with_skonto_pt' ) {
+      push @new_acc_ids, $new_acc_trans->acc_trans_id;
+      # deal with fxtransaction
+      if ( $self->currency_id != $::instance_conf->get_currency_id ) {
+        my $fxamount = _round($amount - ($amount * $exchangerate));
+        $new_acc_trans = SL::DB::AccTransaction->new(trans_id       => $self->id,
+                                                     chart_id       => $account_bank->id,
+                                                     chart_link     => $account_bank->link,
+                                                     amount         => $fxamount * -1,
+                                                     transdate      => $transdate_obj,
+                                                     source         => $source,
+                                                     memo           => $memo,
+                                                     taxkey         => 0,
+                                                     fx_transaction => 1,
+                                                     tax_id         => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
+        $new_acc_trans->save;
+        push @new_acc_ids, $new_acc_trans->acc_trans_id;
+        # if invoice exchangerate differs from exchangerate of payment
+        # deal with fxloss and fxamount
+        if ($self->exchangerate and $self->exchangerate != 1 and $self->exchangerate != $exchangerate) {
+          my $fxgain_chart = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_fxgain_accno_id) || die "Can't determine fxgain chart";
+          my $fxloss_chart = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_fxloss_accno_id) || die "Can't determine fxloss chart";
+          my $gain_loss_amount = _round($amount * ($exchangerate - $self->exchangerate ) * -1,2);
+          my $gain_loss_chart = $gain_loss_amount > 0 ? $fxgain_chart : $fxloss_chart;
+          $fx_gain_loss_amount = $gain_loss_amount;
+
+          $new_acc_trans = SL::DB::AccTransaction->new(trans_id       => $self->id,
+                                                       chart_id       => $gain_loss_chart->id,
+                                                       chart_link     => $gain_loss_chart->link,
+                                                       amount         => $gain_loss_amount,
+                                                       transdate      => $transdate_obj,
+                                                       source         => $source,
+                                                       memo           => $memo,
+                                                       taxkey         => 0,
+                                                       fx_transaction => 0,
+                                                       tax_id         => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
+          $new_acc_trans->save;
+          push @new_acc_ids, $new_acc_trans->acc_trans_id;
+
+        }
+      }
+    }
+    # skonto cases
+    if ($params{payment_type} eq 'with_skonto_pt' or $params{payment_type} eq 'free_skonto' ) {
 
       my $total_skonto_amount;
       if ( $params{payment_type} eq 'with_skonto_pt' ) {
         $total_skonto_amount = $self->skonto_amount;
-      } elsif ( $params{payment_type} eq 'difference_as_skonto' ) {
-        $total_skonto_amount = $self->open_amount;
-      };
-
-      my @skonto_bookings = $self->skonto_charts($total_skonto_amount);
-
-      # error checking:
-      if ( $params{payment_type} eq 'difference_as_skonto' ) {
-        my $calculated_skonto_sum  = sum map { $_->{skonto_amount} } @skonto_bookings;
-        croak "calculated skonto for difference_as_skonto = $calculated_skonto_sum doesn't add up open amount: " . $self->open_amount unless _round($calculated_skonto_sum) == _round($self->open_amount);
-      };
-
+      } elsif ( $params{payment_type} eq 'free_skonto') {
+        $total_skonto_amount = $params{skonto_amount};
+      }
+      my @skonto_bookings = $self->_skonto_charts_and_tax_correction(amount => $total_skonto_amount, bt_id => $params{bt_id},
+                                                                     transdate_obj => $transdate_obj, memo => $params{memo},
+                                                                     source => $params{source});
       my $reference_amount = $total_skonto_amount;
 
       # create an acc_trans entry for each result of $self->skonto_charts
@@ -148,106 +234,162 @@ sub pay_invoice {
         my $amount = -1 * $skonto_booking->{skonto_amount};
         $new_acc_trans = SL::DB::AccTransaction->new(trans_id   => $self->id,
                                                      chart_id   => $skonto_booking->{'chart_id'},
-                                                     chart_link => SL::DB::Manager::Chart->find_by(id => $skonto_booking->{'chart_id'})->{'link'},
+                                                     chart_link => SL::DB::Manager::Chart->find_by(id => $skonto_booking->{'chart_id'})->link,
                                                      amount     => $amount * $mult,
                                                      transdate  => $transdate_obj,
                                                      source     => $params{source},
                                                      taxkey     => 0,
                                                      tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
+
+        # the acc_trans entries are saved individually, not added to $self and then saved all at once
         $new_acc_trans->save;
+        push @new_acc_ids, $new_acc_trans->acc_trans_id;
 
         $reference_amount -= abs($amount);
-        $paid_amount      += -1 * $amount;
+        $paid_amount      += -1 * $amount * $exchangerate;
         $skonto_amount_check -= $skonto_booking->{'skonto_amount'};
-      };
-      die "difference_as_skonto calculated incorrectly, sum of calculated payments doesn't add up to open amount $total_open_amount, reference_amount = $reference_amount\n" unless _round($reference_amount) == 0;
-
-    };
-
+      }
+    }
     my $arap_amount = 0;
 
-    if ( $params{payment_type} eq 'difference_as_skonto' ) {
-      $arap_amount = $total_open_amount;
-    } elsif ( $params{payment_type} eq 'without_skonto' ) {
+    if ( $params{payment_type} eq 'without_skonto' ) {
       $arap_amount = $rounded_params_amount;
     } elsif ( $params{payment_type} eq 'with_skonto_pt' ) {
       # this should be amount + sum(amount+skonto), but while we only allow
       # with_skonto_pt for completely unpaid invoices we just use the value
       # from the invoice
       $arap_amount = $total_open_amount;
-    };
+    } elsif ( $params{payment_type} eq 'free_skonto' ) {
+      # we forced positive values and forced rounding at the beginning
+      # therefore the above comment can be safely applied for this payment type
+      $arap_amount = $params{amount} + $params{skonto_amount};
+    }
 
     # regardless of payment_type there is always only exactly one arap booking
     # TODO: compare $arap_amount to running total
     my $arap_booking= SL::DB::AccTransaction->new(trans_id   => $self->id,
                                                   chart_id   => $reference_account->id,
                                                   chart_link => $reference_account->link,
-                                                  amount     => $arap_amount * $mult,
+                                                  amount     => _round($arap_amount * $mult * $exchangerate - $fx_gain_loss_amount),
                                                   transdate  => $transdate_obj,
                                                   source     => '', #$params{source},
                                                   taxkey     => 0,
                                                   tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
     $arap_booking->save;
+    push @new_acc_ids, $arap_booking->acc_trans_id;
+
+    # hook for invoice_for_advance_payment DATEV always pairs, acc_trans_id has to be higher than arap_booking ;-)
+    if ($self->invoice_type eq 'invoice_for_advance_payment') {
+      my $clearing_chart = SL::DB::Chart->new(id => $::instance_conf->get_advance_payment_clearing_chart_id)->load;
+      die "No Clearing Chart for Advance Payment" unless ref $clearing_chart eq 'SL::DB::Chart';
+
+      # what does ptc say
+      my %inv_calc = $self->calculate_prices_and_taxes();
+      my @trans_ids = keys %{ $inv_calc{amounts} };
+      die "Invalid state for advance payment more than one trans_id" if (scalar @trans_ids > 1);
+      my $entry = delete $inv_calc{amounts}{$trans_ids[0]};
+      my $tax;
+      if ($entry->{tax_id}) {
+        $tax = SL::DB::Manager::Tax->find_by(id => $entry->{tax_id}); # || die "Can't find tax with id " . $entry->{tax_id};
+      }
+      if ($tax and $tax->rate != 0) {
+        my ($netamount, $taxamount);
+        my $roundplaces = 2;
+        # we dont have a clue about skonto, that's why we use $arap_amount as taxincluded
+        ($netamount, $taxamount) = Form->calculate_tax($arap_amount, $tax->rate, 1, $roundplaces);
+        # for debugging database set
+        my $fullmatch = $netamount == $entry->{amount} ? '::netamount total true' : '';
+        my $transfer_chart = $tax->taxkey == 2 ? SL::DB::Chart->new(id => $::instance_conf->get_advance_payment_taxable_7_id)->load
+                          :  $tax->taxkey == 3 ? SL::DB::Chart->new(id => $::instance_conf->get_advance_payment_taxable_19_id)->load
+                          :  undef;
+        die "No Transfer Chart for Advance Payment" unless ref $transfer_chart eq 'SL::DB::Chart';
+
+        my $arap_full_booking= SL::DB::AccTransaction->new(trans_id   => $self->id,
+                                                           chart_id   => $clearing_chart->id,
+                                                           chart_link => $clearing_chart->link,
+                                                           amount     => $arap_amount * -1, # full amount
+                                                           transdate  => $transdate_obj,
+                                                           source     => 'Automatic Tax Booking for Payment in Advance' . $fullmatch,
+                                                           taxkey     => 0,
+                                                           tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
+        $arap_full_booking->save;
+        push @new_acc_ids, $arap_full_booking->acc_trans_id;
+
+        my $arap_tax_booking= SL::DB::AccTransaction->new(trans_id   => $self->id,
+                                                          chart_id   => $transfer_chart->id,
+                                                          chart_link => $transfer_chart->link,
+                                                          amount     => _round($netamount), # full amount
+                                                          transdate  => $transdate_obj,
+                                                          source     => 'Automatic Tax Booking for Payment in Advance' . $fullmatch,
+                                                          taxkey     => $tax->taxkey,
+                                                          tax_id     => $tax->id);
+        $arap_tax_booking->save;
+        push @new_acc_ids, $arap_tax_booking->acc_trans_id;
+
+        my $tax_booking= SL::DB::AccTransaction->new(trans_id   => $self->id,
+                                                     chart_id   => $tax->chart_id,
+                                                     chart_link => $tax->chart->link,
+                                                     amount     => _round($taxamount),
+                                                     transdate  => $transdate_obj,
+                                                     source     => 'Automatic Tax Booking for Payment in Advance' . $fullmatch,
+                                                     taxkey     => 0,
+                                                     tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
 
-    $self->paid($self->paid+$paid_amount) if $paid_amount;
+        $tax_booking->save;
+        push @new_acc_ids, $tax_booking->acc_trans_id;
+      }
+    }
+    $fx_gain_loss_amount *= -1 if $self->is_sales;
+    $self->paid($self->paid + _round($paid_amount) + $fx_gain_loss_amount) if $paid_amount;
     $self->datepaid($transdate_obj);
     $self->save;
 
-  my $datev_check = 0;
-  if ( $is_sales )  {
-    if ( (  $self->invoice && $::instance_conf->get_datev_check_on_sales_invoice  ) ||
-         ( !$self->invoice && $::instance_conf->get_datev_check_on_ar_transaction )) {
-      $datev_check = 1;
-    };
-  } else {
-    if ( (  $self->invoice && $::instance_conf->get_datev_check_on_purchase_invoice ) ||
-         ( !$self->invoice && $::instance_conf->get_datev_check_on_ap_transaction   )) {
-      $datev_check = 1;
-    };
-  };
+    # make sure transactions will be reloaded the next time $self->transactions
+    # is called, as pay_invoice saves the acc_trans objects individually rather
+    # than adding them to the transaction relation array.
+    $self->forget_related('transactions');
+
+    my $datev_check = 0;
+    if ( $is_sales )  {
+      if ( (  $self->invoice && $::instance_conf->get_datev_check_on_sales_invoice  ) ||
+           ( !$self->invoice && $::instance_conf->get_datev_check_on_ar_transaction )) {
+        $datev_check = 1;
+      }
+    } else {
+      if ( (  $self->invoice && $::instance_conf->get_datev_check_on_purchase_invoice ) ||
+           ( !$self->invoice && $::instance_conf->get_datev_check_on_ap_transaction   )) {
+        $datev_check = 1;
+      }
+    }
 
-  if ( $datev_check ) {
+    if ( $datev_check ) {
 
-    my $datev = SL::DATEV->new(
-      exporttype => DATEV_ET_BUCHUNGEN,
-      format     => DATEV_FORMAT_KNE,
-      dbh        => $db->dbh,
-      trans_id   => $self->{id},
-    );
+      my $datev = SL::DATEV->new(
+        dbh        => $db->dbh,
+        trans_id   => $self->{id},
+      );
 
-    $datev->clean_temporary_directories;
-    $datev->export;
+      $datev->generate_datev_data;
 
-    if ($datev->errors) {
-      # this exception should be caught by do_transaction, which handles the rollback
-      die join "\n", $::locale->text('DATEV check returned errors:'), $datev->errors;
+      if ($datev->errors) {
+        # this exception should be caught by with_transaction, which handles the rollback
+        die join "\n", $::locale->text('DATEV check returned errors:'), $datev->errors;
+      }
     }
-  };
 
-  }) || die t8('error while paying invoice #1 : ', $self->invnumber) . $db->error . "\n";
+    1;
 
-  return 1;
-};
+  }) || die t8('error while paying invoice #1 : ', $self->invnumber) . $db->error . "\n";
+  return wantarray ? @new_acc_ids : 1;
+}
 
 sub skonto_date {
 
   my $self = shift;
 
-  my $is_sales = ref($self) eq 'SL::DB::Invoice';
-
-  my $skonto_date;
-
-  if ( $is_sales ) {
-    return undef unless ref $self->payment_terms;
-    return undef unless $self->payment_terms->terms_skonto > 0;
-    $skonto_date = DateTime->from_object(object => $self->transdate)->add(days => $self->payment_terms->terms_skonto);
-  } else {
-    return undef unless ref $self->vendor->payment_terms;
-    return undef unless $self->vendor->payment_terms->terms_skonto > 0;
-    $skonto_date = DateTime->from_object(object => $self->transdate)->add(days => $self->vendor->payment_terms->terms_skonto);
-  };
-
-  return $skonto_date;
+  return undef unless ref $self->payment_terms;
+  return undef unless $self->payment_terms->terms_skonto > 0;
+  return DateTime->from_object(object => $self->transdate)->add(days => $self->payment_terms->terms_skonto);
 };
 
 sub reference_account {
@@ -300,7 +442,7 @@ sub open_amount {
   # if the difference is 0.01 Cent this may end up as 0.009999999999998
   # numerically, so round this value when checking for cent threshold >= 0.01
 
-  return $self->amount - $self->paid;
+  return ($self->amount // 0) - ($self->paid // 0);
 };
 
 sub open_percent {
@@ -337,31 +479,23 @@ sub remaining_skonto_days {
 sub percent_skonto {
   my $self = shift;
 
-  my $is_sales = ref($self) eq 'SL::DB::Invoice';
-
   my $percent_skonto = 0;
 
-  if ( $is_sales ) {
-    return undef unless ref $self->payment_terms;
-    return undef unless $self->payment_terms->percent_skonto > 0;
-    $percent_skonto = $self->payment_terms->percent_skonto;
-  } else {
-    return undef unless ref $self->vendor->payment_terms;
-    return undef unless $self->vendor->payment_terms->terms_skonto > 0;
-    $percent_skonto = $self->vendor->payment_terms->percent_skonto;
-  };
+  return undef unless ref $self->payment_terms;
+  return undef unless $self->payment_terms->percent_skonto > 0;
+  $percent_skonto = $self->payment_terms->percent_skonto;
 
   return $percent_skonto;
 };
 
 sub amount_less_skonto {
   # amount that has to be paid if skonto applies, always return positive rounded values
+  # no, rare case, but credit_notes and negative ap have negative amounts
+  # and therefore this comment may be misguiding
   # the result is rounded so we can directly compare it with the user input
   my $self = shift;
 
-  my $is_sales = ref($self) eq 'SL::DB::Invoice';
-
-  my $percent_skonto = $self->percent_skonto;
+  my $percent_skonto = $self->percent_skonto || 0;
 
   return _round($self->amount - ( $self->amount * $percent_skonto) );
 
@@ -374,13 +508,19 @@ sub check_skonto_configuration {
 
   my $skonto_configured = 1; # default is assume skonto works
 
-  my $transactions = $self->transactions;
-  foreach my $transaction (@{ $transactions }) {
+  my $transactions = $self->transactions;
+  foreach my $transaction (@{ $self->transactions }) {
     # find all transactions with an AR_amount or AP_amount link
-    my $tax = SL::DB::Manager::Tax->get_first( where => [taxkey => $transaction->{taxkey}]);
+    my $tax = SL::DB::Manager::Tax->get_first( where => [taxkey => $transaction->taxkey, id => $transaction->tax_id ]);
+
+    # acc_trans entries for the taxes (chart_link == A[RP]_tax) often
+    # have combinations of taxkey & tax_id that don't exist in
+    # tax. Those must be skipped.
+    next if !$tax && ($transaction->chart_link !~ m{A[RP]_amount});
+
     croak "no tax for taxkey " . $transaction->{taxkey} unless ref $tax;
 
-    $transaction->{chartlinks} = { map { $_ => 1 } split(m/:/, $transaction->{chart_link}) };
+    $transaction->{chartlinks} = { map { $_ => 1 } split(m/:/, $transaction->chart_link) };
     if ( $is_sales && $transaction->{chartlinks}->{AR_amount} ) {
       $skonto_configured = 0 unless $tax->skonto_sales_chart_id;
     } elsif ( !$is_sales && $transaction->{chartlinks}->{AP_amount}) {
@@ -413,110 +553,130 @@ sub open_sepa_transfer_amount {
 
   return $open_sepa_amount || 0;
 
-};
-
-
-sub skonto_charts {
-  my $self = shift;
-
-  # TODO: use param for amount, may also want to calculate skonto_amounts by
-  # passing percentage in the future
-
-  my $amount = shift || $self->skonto_amount;
-
-  croak "no amount passed to skonto_charts" unless abs(_round($amount)) >= 0.01;
-
-  # TODO: check whether there are negative values in invoice / acc_trans ... credited items
-
-  # don't check whether skonto applies, because user may want to override this
-  # return undef unless $self->percent_skonto;  # for is_sales
-  # return undef unless $self->vendor->payment_terms->percent_skonto;  # for purchase
-
-  my $is_sales = ref($self) eq 'SL::DB::Invoice';
-
-  my $mult = $is_sales ? 1 : -1;  # multiplier for getting the right sign
-
-  my @skonto_charts;  # resulting array with all income/expense accounts that have to be corrected
-
-  # calculate effective skonto (percentage) in difference_as_skonto mode
-  # only works if there are no negative acc_trans values
-  my $effective_skonto_rate = $amount ? $amount / $self->amount : 0;
-
-  # checks:
-  my $total_skonto_amount  = 0;
-  my $total_rounding_error = 0;
-
-  my $reference_ARAP_amount = 0;
-
-  my $transactions = $self->transactions;
-  foreach my $transaction (@{ $transactions }) {
-    # find all transactions with an AR_amount or AP_amount link
-    $transaction->{chartlinks} = { map { $_ => 1 } split(m/:/, $transaction->{chart_link}) };
-    # second condition is that we can determine an automatic Skonto account for each AR_amount entry
+}
 
-    if ( ( $is_sales && $transaction->{chartlinks}->{AR_amount} ) or ( !$is_sales && $transaction->{chartlinks}->{AP_amount}) ) {
-        # $reference_ARAP_amount += $transaction->{amount} * $mult;
+sub _skonto_charts_and_tax_correction {
+  my ($self, %params)   = @_;
+  my $amount = $params{amount} || $self->skonto_amount;
 
-        # quick hack that works around problem of non-unique tax keys in SKR04
-        my $tax = SL::DB::Manager::Tax->get_first( where => [taxkey => $transaction->{taxkey}]);
-        croak "no tax for taxkey " . $transaction->{taxkey} unless ref $tax;
+  croak "no amount passed to skonto_charts"                    unless abs(_round($amount)) >= 0.01;
+  croak "no banktransaction.id passed to skonto_charts"        unless $params{bt_id};
+  croak "no banktransaction.transdate passed to skonto_charts" unless ref $params{transdate_obj} eq 'DateTime';
 
-        if ( $is_sales ) {
-          die t8('no skonto_chart configured for taxkey #1 : #2 : #3', $transaction->{taxkey} , $tax->taxdescription , $tax->rate*100) unless ref $tax->skonto_sales_chart;
-        } else {
-          die t8('no skonto_chart configured for taxkey #1 : #2 : #3', $transaction->{taxkey} , $tax->taxdescription , $tax->rate*100) unless ref $tax->skonto_purchase_chart;
-        };
+  $params{memo}   //= '';
+  $params{source} //= '';
 
-        my $skonto_amount_unrounded;
 
-        my $skonto_percent_abs = $self->amount ? abs($transaction->amount * (1 + $tax->rate) * 100 / $self->amount) : 0;
+  my $is_sales = $self->is_sales;
+  my (@skonto_charts, $inv_calc, $total_skonto_rounded);
 
-        my $transaction_amount = abs($transaction->{amount} * (1 + $tax->rate));
-        my $transaction_skonto_percent = abs($transaction_amount/$self->amount); # abs($transaction->{amount} * (1 + $tax->rate));
+  $inv_calc = $self->get_tax_and_amount_by_tax_chart_id();
 
+  # foreach tax.chart_id || $entry->{ta..id}
+  while (my ($tax_chart_id, $entry) = each %{ $inv_calc } ) {
+    my $tax = SL::DB::Manager::Tax->find_by(id => $entry->{tax_id}) || die "Can't find tax with id " . $tax_chart_id;
+    die t8('no skonto_chart configured for taxkey #1 : #2 : #3', $tax->taxkey, $tax->taxdescription , $tax->rate * 100)
+      unless $is_sales ? ref $tax->skonto_sales_chart : ref $tax->skonto_purchase_chart;
 
-        $skonto_amount_unrounded   = abs($amount * $transaction_skonto_percent);
-        my $skonto_amount_rounded  = _round($skonto_amount_unrounded);
-        my $rounding_error         = $skonto_amount_unrounded - $skonto_amount_rounded;
-        my $rounded_rounding_error = _round($rounding_error);
+    # percent net amount
+    my $transaction_net_skonto_percent = abs($entry->{netamount} / $self->amount);
+    my $skonto_netamount_unrounded     = abs($amount * $transaction_net_skonto_percent);
 
-        $total_rounding_error += $rounding_error;
-        $total_skonto_amount  += $skonto_amount_rounded;
+    # percent tax amount
+    my $transaction_tax_skonto_percent = abs($entry->{tax} / $self->amount);
+    my $skonto_taxamount_unrounded     = abs($amount * $transaction_tax_skonto_percent);
 
-        my $rec = {
-          # skonto_percent_abs: relative part of amount + tax to the total invoice amount
-          'skonto_percent_abs'     => $skonto_percent_abs,
-          'chart_id'               => $is_sales ? $tax->skonto_sales_chart->id : $tax->skonto_purchase_chart->id,
-          'skonto_amount'          => $skonto_amount_rounded,
-          # 'rounding_error'         => $rounding_error,
-          # 'rounded_rounding_error' => $rounded_rounding_error,
-        };
+    my $skonto_taxamount_rounded   = _round($skonto_taxamount_unrounded);
+    my $skonto_netamount_rounded   = _round($skonto_netamount_unrounded);
+    my $chart_id                   = $is_sales ? $tax->skonto_sales_chart->id : $tax->skonto_purchase_chart->id;
 
-        push @skonto_charts, $rec;
-      };
-  };
-
-  # if the rounded sum of all rounding_errors reaches 0.01 this sum is
-  # subtracted from the largest skonto_amount
-  my $rounded_total_rounding_error = abs(_round($total_rounding_error));
-
-  if ( $rounded_total_rounding_error > 0 ) {
-    my $highest_amount_pos = 0;
-    my $highest_amount = 0;
-    my $i = -1;
-    foreach my $ref ( @skonto_charts ) {
-      $i++;
-      if ( $ref->{skonto_amount} > $highest_amount ) {
-        $highest_amount     = $ref->{skonto_amount};
-        $highest_amount_pos = $i;
-      };
+    # entry net + tax for caller
+    my $rec_net = {
+      chart_id               => $chart_id,
+      skonto_amount          => _round($skonto_netamount_unrounded + $skonto_taxamount_unrounded),
     };
-    $skonto_charts[$i]->{skonto_amount} -= $rounded_total_rounding_error;
-  };
-
+    push @skonto_charts, $rec_net;
+    $total_skonto_rounded += $rec_net->{skonto_amount};
+
+    # add-on: correct tax with one linked gl booking
+
+    # no skonto tax correction for dual tax (reverse charge) or rate = 0 or taxamount below 0.01
+    next if ($tax->rate == 0 || $tax->reverse_charge_chart_id || $skonto_taxamount_rounded < 0.01);
+
+    my ($credit, $debit);
+    $credit = SL::DB::Manager::Chart->find_by(id => $chart_id);
+    $debit  = SL::DB::Manager::Chart->find_by(id => $tax_chart_id);
+    croak("No such Chart ID")  unless ref $credit eq 'SL::DB::Chart' && ref $debit eq 'SL::DB::Chart';
+    my $notes = SL::HTML::Util->strip($self->notes);
+
+    my $current_transaction = SL::DB::GLTransaction->new(
+         employee_id    => $self->employee_id,
+         transdate      => $params{transdate_obj},
+         notes          => $params{source} . ' ' . $params{memo},
+         description    => $notes || $self->invnumber,
+         reference      => t8('Skonto Tax Correction for') . " " . $tax->rate * 100 . '% ' . $self->invnumber,
+         department_id  => $self->department_id ? $self->department_id : undef,
+         imported       => 0, # not imported
+         taxincluded    => 0,
+      )->add_chart_booking(
+         chart  => $is_sales ? $debit : $credit,
+         debit  => abs($skonto_taxamount_rounded),
+         source => t8('Skonto Tax Correction for') . " " . $self->invnumber,
+         memo   => $params{memo},
+         tax_id => 0,
+      )->add_chart_booking(
+         chart  => $is_sales ? $credit : $debit,
+         credit => abs($skonto_taxamount_rounded),
+         source => t8('Skonto Tax Correction for') . " " . $self->invnumber,
+         memo   => $params{memo},
+         tax_id => 0,
+      )->post;
+
+    # add a stable link acc_trans_id to bank_transactions.id
+    foreach my $transaction (@{ $current_transaction->transactions }) {
+      my %props_acc = (
+           acc_trans_id        => $transaction->acc_trans_id,
+           bank_transaction_id => $params{bt_id},
+           gl                  => $current_transaction->id,
+      );
+      SL::DB::BankTransactionAccTrans->new(%props_acc)->save;
+    }
+    # Record a record link from banktransactions to gl
+    my %props_rl = (
+         from_table => 'bank_transactions',
+         from_id    => $params{bt_id},
+         to_table   => 'gl',
+         to_id      => $current_transaction->id,
+    );
+    SL::DB::RecordLink->new(%props_rl)->save;
+    # Record a record link from arap to gl
+    # linked gl booking will appear in tab linked records
+    # this is just a link for convenience
+    %props_rl = (
+         from_table => $is_sales ? 'ar' : 'ap',
+         from_id    => $self->id,
+         to_table   => 'gl',
+         to_id      => $current_transaction->id,
+    );
+    SL::DB::RecordLink->new(%props_rl)->save;
+
+  }
+  # check for rounding errors, at least for the payment chart
+  # we ignore tax rounding errors as long as the amount (user input or calculated)
+  # is fully assigned.
+  # we simply alter one cent for the first skonto booking entry
+  # should be correct for most of the cases (no invoices with mixed taxes)
+  if (_round($total_skonto_rounded - $amount) >= 0.01) {
+    # subtract one cent
+    $skonto_charts[0]->{skonto_amount} -= 0.01;
+  } elsif (_round($amount - $total_skonto_rounded) >= 0.01) {
+    # add one cent
+    $skonto_charts[0]->{skonto_amount} += 0.01;
+  }
+
+  # return same array of skonto charts as sub skonto_charts
   return @skonto_charts;
-};
-
+}
 
 sub within_skonto_period {
   my $self = shift;
@@ -544,29 +704,44 @@ sub valid_skonto_amount {
 sub get_payment_select_options_for_bank_transaction {
   my ($self, $bt_id, %params) = @_;
 
-  my $bt = SL::DB::Manager::BankTransaction->find_by( id => $bt_id );
-  die unless $bt;
-
-  my $open_amount = $self->open_amount;
 
+  # CAVEAT template code expects with_skonto_pt at position 1 for visual help
+  # due to skonto_charts, we cannot offer skonto for credit notes and neg ap
+  my $skontoable = $self->amount > 0 ? 1 : 0;
   my @options;
-  if ( $open_amount &&                   # invoice amount not 0
-       $self->skonto_date &&             # check whether skonto applies
-       abs(abs($self->amount_less_skonto) - abs($bt->amount)) < 0.01 &&
-       $self->check_skonto_configuration) {
-         if ( $self->within_skonto_period($bt->transdate) ) {
-           push(@options, { payment_type => 'without_skonto', display => t8('without skonto') });
-           push(@options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt'), selected => 1 });
-         } else {
-           push(@options, { payment_type => 'without_skonto', display => t8('without skonto') , selected => 1 });
-           push(@options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt')});
-         };
-  };
-
+  if(!$self->skonto_date) {
+    push(@options, { payment_type => 'without_skonto', display => t8('without skonto'), selected => 1 });
+    # wrong call to presenter or not implemented? disabled option is ignored
+    # push(@options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt'), disabled => 1 });
+    push(@options, { payment_type => 'free_skonto', display => t8('free skonto') }) if $skontoable;
+    return @options;
+  }
+  # valid skonto date, check if skonto is preferred
+  my $bt = SL::DB::BankTransaction->new(id => $bt_id)->load;
+  if ($self->skonto_date && $self->within_skonto_period($bt->transdate)) {
+    push(@options, { payment_type => 'without_skonto', display => t8('without skonto') });
+    push(@options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt'), selected => 1 }) if $skontoable;
+  } else {
+    push(@options, { payment_type => 'without_skonto', display => t8('without skonto') , selected => 1 });
+    push(@options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt')}) if $skontoable;
+  }
+  push(@options, { payment_type => 'free_skonto', display => t8('free skonto') }) if $skontoable;
   return @options;
+}
 
-};
+sub exchangerate {
+  my ($self) = @_;
+
+  return 1 if $self->currency_id == $::instance_conf->get_currency_id;
+
+  die "transdate isn't a DateTime object:" . ref($self->transdate) unless ref($self->transdate) eq 'DateTime';
+  my $rate = SL::DB::Manager::Exchangerate->find_by(currency_id => $self->currency_id,
+                                                    transdate   => $self->transdate,
+                                                   );
+  return undef unless $rate;
 
+  return $self->is_sales ? $rate->buy : $rate->sell; # also undef if not defined
+};
 
 sub get_payment_suggestions {
 
@@ -587,6 +762,8 @@ sub get_payment_suggestions {
       push(@{$self->{payment_select_options}} , { payment_type => 'with_skonto_pt',  display => t8('with skonto acc. to pt') , selected => 1 });
     } else {
       if ( ( $self->valid_skonto_amount($self->open_amount) || $self->valid_skonto_amount($open_amount) ) and not $params{sepa} ) {
+        # Will never be reached
+        die "This case is as dead as the dead cat. Go to start, don't pick 2,000 \$";
         $self->{invoice_amount_suggestion} = $open_amount;
         # only suggest difference_as_skonto if open_amount exactly matches skonto_amount
         # AND we aren't in SEPA mode
@@ -600,30 +777,34 @@ sub get_payment_suggestions {
     $self->{invoice_amount_suggestion} = $open_amount;
     # difference_as_skonto doesn't make any sense for SEPA transfer, as this doesn't cause any actual payment
     if ( $self->valid_skonto_amount($self->open_amount) && not $params{sepa} ) {
+      # probably also dead code
+      die "This case is as dead as the dead cat. Go to start, don't pick 2,000 \$";
       push(@{$self->{payment_select_options}} , { payment_type => 'difference_as_skonto',  display => t8('difference as skonto') , selected => 0 });
     };
   };
   return 1;
 };
 
-sub transactions {
-  my ($self) = @_;
-
-  return unless $self->id;
-
-  require SL::DB::AccTransaction;
-  SL::DB::Manager::AccTransaction->get_all(query => [ trans_id => $self->id ]);
-}
+# locales for payment type
+#
+# $main::locale->text('without_skonto')
+# $main::locale->text('with_skonto_pt')
+#
 
 sub validate_payment_type {
   my $payment_type = shift;
 
-  my %allowed_payment_types = map { $_ => 1 } qw(without_skonto with_skonto_pt difference_as_skonto);
+  my %allowed_payment_types = map { $_ => 1 } qw(without_skonto with_skonto_pt free_skonto);
   croak "illegal payment type: $payment_type, must be one of: " . join(' ', keys %allowed_payment_types) unless $allowed_payment_types{ $payment_type };
 
   return 1;
 }
 
+sub forex {
+  my ($self) = @_;
+  $self->currency_id == $::instance_conf->get_currency_id ? return 0 : return 1;
+};
+
 sub _round {
   my $value = shift;
   my $num_dec = 2;
@@ -660,6 +841,24 @@ Create a payment booking for an existing invoice object (type ar/ap/is/ir) via
 a configured bank account.
 
 This function deals with all the acc_trans entries and also updates paid and datepaid.
+The params C<transdate>, C<amount> and C<chart_id> are mandantory.
+
+For all valid skonto types ('free_skonto' or 'with_skonto_pt') the source of
+the bank_transaction is needed, therefore pay_invoice expects the param
+C<bt_id> with a valid bank_transactions.id.
+
+If the payment type ('free_skonto') is used the number param skonto_amount is
+as well mandantory and needs to be positive. Furthermore the skonto amount has
+to be lower or equal than the open invoice amount.
+Payments with only skonto and zero bank transaction amount are possible.
+
+Transdate can either be a date object or a date string.
+Chart_id is the id of the payment booking chart.
+Amount is either a positive or negative number, and for the case 'free_skonto' might be zero.
+
+CAVEAT! The helper tries to get the sign right and all calls from BankTransaction are
+positive (abs($value)) values.
+
 
 Example:
 
@@ -668,22 +867,33 @@ Example:
   $ap->pay_invoice(chart_id      => $bank->chart_id,
                    amount        => $ap->open_amount,
                    transdate     => DateTime->now->to_kivitendo,
-                   memo          => 'foobar;
-                   source        => 'barfoo;
+                   memo          => 'foobar',
+                   source        => 'barfoo',
                    payment_type  => 'without_skonto',  # default if not specified
+                   project_id    => 25,
                   );
 
 or with skonto:
   $ap->pay_invoice(chart_id      => $bank->chart_id,
                    amount        => $ap->amount,       # doesn't need to be specified
                    transdate     => DateTime->now->to_kivitendo,
-                   memo          => 'foobar;
-                   source        => 'barfoo;
+                   memo          => 'foobar',
+                   source        => 'barfoo',
                    payment_type  => 'with_skonto',
                   );
 
+or in a certain currency:
+  $ap->pay_invoice(chart_id      => $bank->chart_id,
+                   amount        => 500,
+                   currency      => 'USD',
+                   transdate     => DateTime->now->to_kivitendo,
+                   memo          => 'foobar',
+                   source        => 'barfoo',
+                   payment_type  => 'with_skonto_pt',
+                  );
+
 Allowed payment types are:
-  without_skonto with_skonto_pt difference_as_skonto
+  without_skonto with_skonto_pt
 
 The option C<payment_type> allows for a basic skonto mechanism.
 
@@ -697,42 +907,37 @@ booked according to the skonto chart configured in the tax settings for each
 tax key. If an amount is passed it is ignored and the actual configured skonto
 amount is used.
 
-C<difference_as_skonto> can only be used after partial payments have been made,
-the whole specified amount is booked according to the skonto charts configured
-in the tax settings for each tax key.
-
-So passing amount doesn't have any effect for the cases C<with_skonto_pt> and
-C<difference_as_skonto>, as all necessary values are taken from the stored
-invoice.
+So passing amount doesn't have any effect for the case C<with_skonto_pt>.
 
 The skonto modes automatically calculate the relative amounts for a mix of
 taxes, e.g. items with 7% and 19% in one invoice. There is a helper method
-skonto_charts, which calculates the relative percentages according to the
-amounts in acc_trans (which are grouped by tax).
+_skonto_charts_and_tax_correction, which calculates the relative percentages
+according to the amounts in acc_trans grouped by different tax rates.
+
+The helper method also generates the tax correction for the skonto booking
+and links this to the original bank transaction and the selected record.
 
 There is currently no way of excluding certain items in an invoice from having
 skonto applied to them.  If this feature was added to parts the calculation
 method of relative skonto would have to be completely rewritten using the
 invoice items rather than acc_trans.
 
-The skonto modes also still don't automatically correct the tax, this still has
-to be done manually. Therefore all payments generated by pay_invoice have
-taxkey 0.
-
-There is currently no way to directly pay an invoice via this method if the
-effective skonto differs from the skonto according to the payment terms
-configured for the invoice/vendor.
-
-In this case one has to pay in two steps: first the actual paid amount via
-"without skonto", and then the remainder via "difference_as_skonto". The user
-has to there actively decide whether to accept the differing skonto.
-
 Because of the way skonto_charts works the calculation doesn't work if there
 are negative values in acc_trans. E.g. one invoice with a positive value for
 19% tax and a negative value for the acc_trans line with 7%
 
 Skonto doesn't/shouldn't apply if the invoice contains credited items.
 
+If no amount is given the whole open amout is paid.
+
+If neither currency or currency_id are given as params, the currency of the
+invoice is assumed to be the payment currency.
+
+If successful the return value will be 1 in scalar context or in list context
+the two or more (gl transaction for skonto tax correction) ids (acc_trans_id)
+of the newly created bookings.
+
+
 =item C<reference_account>
 
 Returns a chart object which is the chart of the invoice with link AR or AP.
@@ -745,12 +950,11 @@ Example (1200 is the AR account for SKR04):
 =item C<percent_skonto>
 
 Returns the configured skonto percentage of the payment terms of an invoice,
-e.g. 0.02 for 2%. Payment terms come from invoice settings for ar, from vendor
-settings for ap.
+e.g. 0.02 for 2%. Payment terms come from invoice settingssettings for ap.
 
 =item C<amount_less_skonto>
 
-If the invoice has a payment term (via ar for sales, via vendor for purchase),
+If the invoice has a payment term,
 calculate the amount to be paid in the case of skonto.  This doesn't check,
 whether skonto applies (i.e. skonto doesn't wasn't exceeded), it just subtracts
 the configured percentage (e.g. 2%) from the total amount.
@@ -788,7 +992,11 @@ Example:
    # ... do something
  }
 
-=item C<skonto_charts [$amount]>
+=item C<_skonto_charts_and_tax_correction [amount => $amount, bt_id => $bank_transaction.id, transdate_ojb => DateTime]>
+
+Needs a valid bank_transaction id and the transdate of the bank_transaction as
+a DateTime object.
+If no amout is passed, the currently open invoice amount will be used.
 
 Returns a list of chart_ids and some calculated numbers that can be used for
 paying the invoice with skonto. This function will automatically calculate the
@@ -797,10 +1005,13 @@ relative skonto amounts even if the invoice contains several types of taxes
 
 Example usage:
   my $invoice = SL::DB::Manager::Invoice->find_by(invnumber => '211');
-  my @skonto_charts = $invoice->skonto_charts;
+  my @skonto_charts = $invoice->_skonto_charts_and_tax_correction(bt_id         => $bt_id,
+                                                                  transdate_obj => $transdate_obj);
 
 or with the total skonto amount as an argument:
-  my @skonto_charts = $invoice->skonto_charts($invoice->open_amount);
+  my @skonto_charts = $invoice->_skonto_charts_and_tax_correction(amount => $invoice->open_amount,
+                                                                  bt_id  => $bt_id,
+                                                                  transdate_obj => $transdate_obj);
 
 The following values are generated for each chart:
 
@@ -814,57 +1025,12 @@ The chart id of the skonto amount to be booked.
 
 The total amount to be paid to the account
 
-=item C<skonto_percent>
-
-The relative percentage of that skonto chart. This can be useful if the actual
-ekonto that is paid deviates from the granted skonto, e.g. customer effectively
-pays 2.6% skonto instead of 2%, and we accept this. Then we can still calculate
-the relative skonto amounts for different taxes based on the absolute
-percentages. Used for case C<difference_as_skonto>.
-
-=item C<skonto_percent_abs>
-
-The absolute percentage of that skonto chart in relation to the total amount.
-Used to calculate skonto_amount for case C<with_skonto_pt>.
-
 =back
 
 If the invoice contains several types of taxes then skonto_charts can be used
 to calculate the relative amounts.
 
-Example in console of an invoice with 100 Euro at 7% and 100 Euro at 19% with
-tax not included:
-
-  my $invoice = invoice(invnumber => '144');
-  $invoice->amount
-  226.00000
-  $invoice->payment_terms->percent_skonto
-  0.02
-  $invoice->skonto_charts
-  pp $invoice->skonto_charts
-  #             $VAR1 = {
-  #               'chart_id'       => 128,
-  #               'skonto_amount'  => '2.14',
-  #               'skonto_percent' => '47.3451327433627'
-  #             };
-  #             $VAR2 = {
-  #               'chart_id'       => 130,
-  #               'skonto_amount'  => '2.38',
-  #               'skonto_percent' => '52.654867256637'
-  #             };
-
-C<skonto_charts> always returns positive values (abs) for C<skonto_amount> and
-C<skonto_percent>.
-
-C<skonto_charts> generates one entry for each acc_trans entry. ar and ap
-bookings only have one acc_trans entry for each taxkey (e.g. 7% and 19%).  This
-is because all the items are grouped according to the Buchungsgruppen mechanism
-and the totals are written to acc_trans.  For is and ir it is possible to have
-several acc_trans entries with the same tax. In this case skonto_charts
-generates a skonto booking for each acc_trans income/expense entry.
-
-In the future this function may also be used to calculate the corrections for
-the income tax.
+C<_skonto_charts_and_tax_correction> generates one entry for each tax type entry.
 
 =item C<open_amount>
 
@@ -888,7 +1054,7 @@ Returns undef if skonto is not configured for that invoice.
 
 Creates data intended for an L.select_tag dropdown that can be used in a
 template. Depending on the rules it will choose from the options
-without_skonto, with_skonto_pt and difference_as_skonto, and select the most
+without_skonto and with_skonto_pt and select the most
 likely one.
 
 If the parameter "sepa" is passed, the SEPA export payments that haven't been
@@ -902,8 +1068,6 @@ The current rules are:
 
 =item * with_skonto_pt is only offered if there haven't been any payments yet and the current date is within the skonto period.
 
-=item * difference_as_skonto is only offered if there have already been payments made and the open amount is smaller than 10% of the total amount.
-
 with_skonto_pt will only be offered, if all the AR_amount/AP_amount have a
 taxkey with a configured skonto chart
 
@@ -942,36 +1106,36 @@ defaults. E.g. when creating a SEPA bank transfer for vendor invoices a company
 might always want to pay quickly making use of skonto, while another company
 might always want to pay as late as possible.
 
-=item C<transactions>
-
-Returns all acc_trans Objects of an ar/ap object.
-
-Example in console to print account numbers and booked amounts of an invoice:
-  my $invoice = invoice(invnumber => '144');
-  foreach my $acc_trans ( @{ $invoice->transactions } ) {
-    print $acc_trans->chart->accno . " : " . $acc_trans->amount_as_number . "\n"
-  };
-  # 1200 : 226,00000
-  # 1800 : -226,00000
-  # 4300 : 100,00000
-  # 3801 : 7,00000
-  # 3806 : 19,00000
-  # 4400 : 100,00000
-  # 1200 : -226,00000
-
 =item C<get_payment_select_options_for_bank_transaction $banktransaction_id %params>
 
 Make suggestion for a skonto payment type by returning an HTML blob of the options
 of a HTML drop-down select with the most likely option preselected.
 
-This is a helper function for BankTransaction/ajax_payment_suggestion.
+This is a helper function for BankTransaction/ajax_payment_suggestion and
+template/webpages/bank_transactions/invoices.html
+
+We are working with an existing payment, so (deprecated) difference_as_skonto never makes sense.
 
-We are working with an existing payment, so difference_as_skonto never makes sense.
+If skonto is not possible (skonto_date does not exists) simply return
+the single 'no skonto' option as a visual hint.
 
 If skonto is possible (skonto_date exists), add two possibilities:
 without_skonto and with_skonto_pt if payment date is within skonto_date,
 preselect with_skonto_pt, otherwise preselect without skonto.
 
+=item C<exchangerate>
+
+Returns 1 immediately if the record uses the default currency.
+
+Returns the exchangerate in database format for the invoice according to that
+invoice's transdate, returning 'buy' for sales, 'sell' for purchases.
+
+If no exchangerate can be found for that day undef is returned.
+
+=item C<forex>
+
+Returns 1 if record uses a different currency, 0 if the default currency is used.
+
 =back
 
 =head1 TODO AND CAVEATS
@@ -983,9 +1147,11 @@ preselect with_skonto_pt, otherwise preselect without skonto.
 when looking at open amount, maybe consider that there may already be queued
 amounts in SEPA Export
 
-=item *
+=item * C<_skonto_charts_and_tax_correction>
+
+Cannot handle negative skonto amounts, will always calculate the skonto amount
+for credit notes or negative ap transactions with a positive sign.
 
-Can only handle default currency.
 
 =back
 
diff --git a/SL/DB/Helper/Presenter.pm b/SL/DB/Helper/Presenter.pm
new file mode 100644 (file)
index 0000000..1dae8cc
--- /dev/null
@@ -0,0 +1,95 @@
+package SL::DB::Helper::Presenter;
+
+use strict;
+
+sub new {
+  # lightweight: 0: class, 1: object
+  bless [ $_[1], $_[2] ], $_[0];
+}
+
+sub AUTOLOAD {
+  our $AUTOLOAD;
+
+  my ($self, @args) = @_;
+
+  my $method = $AUTOLOAD;
+  $method    =~ s/.*:://;
+
+  return if $method eq 'DESTROY';
+
+  eval "require $self->[0]";
+
+  splice @args, -1, 1, %{ $args[-1] } if @args && (ref($args[-1]) eq 'HASH');
+
+  if (my $sub = $self->[0]->can($method)) {
+    return $sub->($self->[1], @args);
+  }
+}
+
+sub can {
+  my ($self, $method) = @_;
+  eval "require $self->[0]";
+  $self->[0]->can($method);
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::DB::Helper::Presenter - proxy class to allow models to access presenters
+
+=head1 SYNOPSIS
+
+  # assuming SL::Presenter::Part exists
+  # and contains a sub link_to($class, $object) {}
+  SL::DB::Part->new(%args)->presenter->link_to
+
+=head1 DESCRIPTION
+
+When coding controllers one often encounters objects that are not crucial to
+the current task, but must be presented in some form to the user. Instead of
+recreating that all the time the C<SL::Presenter> namepace was introduced to
+hold such code.
+
+Unfortunately the Presenter code is designed to be stateless and thus acts _on_
+objects, but can't be instanced or wrapped. The early band-aid to that was to
+export all sub-presenter calls into the main presenter namespace. Fixing it
+would have meant accessing presenter functions like this:
+
+  SL::Presenter::Object->method($object, %additional_args)
+
+which is extremely inconvenient.
+
+This glue code allows C<SL::DB::Object> instances to access routines in their
+presenter without additional boilerplate. C<SL::DB::Object> contains a
+C<presenter> call for all objects, which will return an instance of this proxy
+class. All calls on this will then be forwarded to the appropriate presenter.
+
+=head1 INTERNAL STRUCTURE
+
+The created proxy objects are lightweight blessed arrayrefs instead of the
+usual blessed hashrefs. They only store two elements:
+
+=over 4
+
+=item * The presenter class
+
+=item * The invocing object
+
+=back
+
+Further delegation is done with C<AUTOLOAD>.
+
+=head1 BUGS
+
+None yet :)
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut
index 44c6511..bde711a 100644 (file)
@@ -29,7 +29,8 @@ sub calculate_prices_and_taxes {
                last_incex_chart_id => undef,
                units_by_name       => \%units_by_name,
                price_factors_by_id => \%price_factors_by_id,
-               taxes               => { },
+               taxes_by_chart_id   => { },
+               taxes_by_tax_id     => { },
                amounts             => { },
                amounts_cogs        => { },
                allocated           => { },
@@ -40,15 +41,24 @@ sub calculate_prices_and_taxes {
                items               => [ ],
              );
 
-  _get_exchangerate($self, \%data, %params);
+  # set exchangerate in $data>{exchangerate}
+  if ( ref($self) eq 'SL::DB::Order' ) {
+    # orders store amount in the order currency
+    $data{exchangerate}         = 1;
+    $data{allow_optional_items} = 1;
+  } else {
+    # invoices store amount in the default currency
+    _get_exchangerate($self, \%data, %params);
+    # $data{exchangerate} = $self->exchangerate; # untested alternative for setting exchangerate
+  };
 
   $self->netamount(  0);
   $self->marge_total(0);
 
-  SL::DB::Manager::Chart->cache_taxkeys(date => $self->transdate);
+  SL::DB::Manager::Chart->cache_taxkeys(date => $self->effective_tax_point);
 
   my $idx = 0;
-  foreach my $item ($self->items) {
+  foreach my $item (@{ $self->items_sorted }) {
     $idx++;
     _calculate_item($self, $item, $idx, \%data, %params);
   }
@@ -57,7 +67,7 @@ sub calculate_prices_and_taxes {
 
   return $self unless wantarray;
 
-  return map { ($_ => $data{$_}) } qw(taxes amounts amounts_cogs allocated exchangerate assembly_items items);
+  return map { ($_ => $data{$_}) } qw(taxes_by_chart_id taxes_by_tax_id amounts amounts_cogs allocated exchangerate assembly_items items rounding);
 }
 
 sub _get_exchangerate {
@@ -75,46 +85,33 @@ sub _calculate_item {
   my ($self, $item, $idx, $data, %params) = @_;
 
   my $part       = SL::DB::Part->load_cached($item->parts_id);
-  return unless $item->part;
+  return unless $part;
 
   my $part_unit  = $data->{units_by_name}->{ $part->unit };
   my $item_unit  = $data->{units_by_name}->{ $item->unit };
 
   croak("Undefined unit " . $part->unit) if !$part_unit;
-  croak("Undefined unit " . $item->unit)       if !$item_unit;
+  croak("Undefined unit " . $item->unit) if !$item_unit;
 
   $item->base_qty($item_unit->convert_to($item->qty, $part_unit));
   $item->fxsellprice($item->sellprice) if $data->{is_invoice};
 
   my $num_dec   = max 2, _num_decimal_places($item->sellprice);
-  my $discount  = _round($item->sellprice * ($item->discount || 0), $num_dec);
-  my $sellprice = _round($item->sellprice - $discount,              $num_dec);
 
-  $item->price_factor(      ! $item->price_factor_obj   ? 1 : ($item->price_factor_obj->factor   || 1));
-  $item->marge_price_factor(! $part->price_factor ? 1 : ($part->price_factor->factor || 1));
-  my $linetotal = _round($sellprice * $item->qty / $item->price_factor, 2) * $data->{exchangerate};
-  $linetotal    = _round($linetotal,                                    2);
+  $item->discount(0) if !$item->discount;
 
-  $data->{invoicediff} += $sellprice * $item->qty * $data->{exchangerate} / $item->price_factor - $linetotal if $self->taxincluded;
+  # don't include rounded discount into sellprice for calculation
+  # any time the sellprice is multiplied with qty discount has to be considered as part of the multiplication
+  my $sellprice = $item->sellprice;
 
-  my $linetotal_cost = 0;
-
-  if (!$linetotal) {
-    $item->marge_total(  0);
-    $item->marge_percent(0);
-
-  } else {
-    my $lastcost       = ! ($item->lastcost * 1) ? ($part->lastcost || 0) : $item->lastcost;
-    $linetotal_cost    = _round($lastcost * $item->qty / $item->marge_price_factor, 2);
-
-    $item->marge_total(  $linetotal - $linetotal_cost);
-    $item->marge_percent($item->marge_total * 100 / $linetotal);
+  $item->price_factor(      ! $item->price_factor_obj   ? 1 : ($item->price_factor_obj->factor   || 1));
+  $item->marge_price_factor(! $part->price_factor ? 1 : ($part->price_factor->factor || 1));
+  my $linetotal = _round($sellprice * (1 - $item->discount) * $item->qty / $item->price_factor, 2) * $data->{exchangerate};
+  $linetotal    = _round($linetotal,                                                            2);
 
-    $self->marge_total(  $self->marge_total + $item->marge_total);
-    $data->{lastcost_total} += $linetotal_cost;
-  }
+  $data->{invoicediff} += $sellprice * (1 - $item->discount) * $item->qty * $data->{exchangerate} / $item->price_factor - $linetotal if $self->taxincluded;
 
-  my $taxkey     = $part->get_taxkey(date => $self->transdate, is_sales => $data->{is_sales}, taxzone => $self->taxzone_id);
+  my $taxkey     = $part->get_taxkey(date => $self->effective_tax_point, is_sales => $data->{is_sales}, taxzone => $self->taxzone_id);
   my $tax_rate   = $taxkey->tax->rate;
   my $tax_amount = undef;
 
@@ -125,20 +122,40 @@ sub _calculate_item {
   } else {
     $tax_amount = $linetotal * $tax_rate;
   }
+  my $chart = $part->get_chart(type => $data->{is_sales} ? 'income' : 'expense', taxzone => $self->taxzone_id);
+  unless ($data->{allow_optional_items} && $item->optional) {
+    if ($taxkey->tax->chart_id) {
+      $data->{taxes_by_chart_id}->{ $taxkey->tax->chart_id } ||= 0;
+      $data->{taxes_by_chart_id}->{ $taxkey->tax->chart_id }  += $tax_amount;
+      $data->{taxes_by_tax_id}->{ $taxkey->tax_id }          ||= 0;
+      $data->{taxes_by_tax_id}->{ $taxkey->tax_id }           += $tax_amount;
+    } elsif ($tax_amount) {
+      die "tax_amount != 0 but no chart_id for taxkey " . $taxkey->id . " tax " . $taxkey->tax->id;
+    }
 
-  if ($taxkey->tax->chart_id) {
-    $data->{taxes}->{ $taxkey->tax->chart_id } ||= 0;
-    $data->{taxes}->{ $taxkey->tax->chart_id }  += $tax_amount;
-  } elsif ($tax_amount) {
-    die "tax_amount != 0 but no chart_id for taxkey " . $taxkey->id . " tax " . $taxkey->tax->id;
+    $data->{amounts}->{ $chart->id }           ||= { taxkey => $taxkey->taxkey_id, tax_id => $taxkey->tax_id, amount => 0 };
+    $data->{amounts}->{ $chart->id }->{amount}  += $linetotal;
+    $data->{amounts}->{ $chart->id }->{amount}  -= $tax_amount if $self->taxincluded;
   }
+  my $linetotal_cost = 0;
 
-  $self->netamount($self->netamount + $sellprice * $item->qty / $item->price_factor);
+  if (!$linetotal) {
+    $item->marge_total(  0);
+    $item->marge_percent(0);
 
-  my $chart = $part->get_chart(type => $data->{is_sales} ? 'income' : 'expense', taxzone => $self->taxzone_id);
-  $data->{amounts}->{ $chart->id }           ||= { taxkey => $taxkey->taxkey_id, tax_id => $taxkey->tax_id, amount => 0 };
-  $data->{amounts}->{ $chart->id }->{amount}  += $linetotal;
-  $data->{amounts}->{ $chart->id }->{amount}  -= $tax_amount if $self->taxincluded;
+  } else {
+    my $lastcost       = !(($item->lastcost // 0) * 1) ? ($part->lastcost || 0) : $item->lastcost;
+    $linetotal_cost    = _round($lastcost * $item->qty / $item->marge_price_factor, 2);
+    my $linetotal_net  = $self->taxincluded ? $linetotal - $tax_amount : $linetotal;
+
+    $item->marge_total(  $linetotal_net - $linetotal_cost);
+    $item->marge_percent($item->marge_total * 100 / $linetotal_net);
+
+    unless ($data->{allow_optional_items} && $item->optional) {
+      $self->marge_total(  $self->marge_total + $item->marge_total);
+      $data->{lastcost_total} += $linetotal_cost;
+    }
+  }
 
   push @{ $data->{assembly_items} }, [];
   if ($part->is_assembly) {
@@ -151,10 +168,12 @@ sub _calculate_item {
 
   $data->{last_incex_chart_id} = $chart->id if $data->{is_sales};
 
+  my $item_sellprice = _round($sellprice * (1 - $item->discount), $num_dec);
+
   push @{ $data->{items} }, {
     linetotal      => $linetotal,
     linetotal_cost => $linetotal_cost,
-    sellprice      => $sellprice,
+    sellprice      => $item_sellprice,
     tax_amount     => $tax_amount,
     taxkey_id      => $taxkey->id,
   };
@@ -167,12 +186,14 @@ sub _calculate_amounts {
   my ($self, $data, %params) = @_;
 
   my $tax_diff = 0;
-  foreach my $chart_id (keys %{ $data->{taxes} }) {
-    my $rounded                  = _round($data->{taxes}->{$chart_id} * $data->{exchangerate}, 2);
-    $tax_diff                   += $data->{taxes}->{$chart_id} * $data->{exchangerate} - $rounded if $self->taxincluded;
-    $data->{taxes}->{$chart_id}  = $rounded;
+  foreach my $chart_id (keys %{ $data->{taxes_by_chart_id} }) {
+    my $rounded                              = _round($data->{taxes_by_chart_id}->{$chart_id} * $data->{exchangerate}, 2);
+    $tax_diff                               += $data->{taxes_by_chart_id}->{$chart_id} * $data->{exchangerate} - $rounded if $self->taxincluded;
+    $data->{taxes_by_chart_id}->{$chart_id}  = $rounded;
   }
 
+  $self->netamount(sum map { $_->{amount} } values %{ $data->{amounts} });
+
   my $amount    = _round(($self->netamount + $tax_diff) * $data->{exchangerate}, 2);
   my $diff      = $amount - ($self->netamount + $tax_diff) * $data->{exchangerate};
   my $netamount = $amount;
@@ -184,11 +205,14 @@ sub _calculate_amounts {
 
   _dbg("Sna " . $self->netamount . " idiff " . $data->{invoicediff} . " tdiff ${tax_diff}");
 
-  my $tax              = sum values %{ $data->{taxes} };
-  $data->{arap_amount} = $netamount + $tax;
+  my $tax              = sum values %{ $data->{taxes_by_chart_id} };
+  $amount              = $netamount + $tax;
+  my $grossamount      = _round($amount, 2, 1);
+  $data->{rounding}    = _round($grossamount - $amount, 2);
+  $data->{arap_amount} = $grossamount;
 
   $self->netamount(    $netamount);
-  $self->amount(       $netamount + $tax);
+  $self->amount(       $grossamount);
   $self->marge_percent($self->netamount ? ($self->netamount - $data->{lastcost_total}) * 100 / $self->netamount : 0);
 }
 
@@ -233,7 +257,7 @@ sub _calculate_part_item {
 
     next unless $qty;
 
-    my $linetotal = _round(($entry->sellprice * $qty) / $base_factor, 2);
+    my $linetotal = _round(($entry->sellprice * (1 - $entry->discount) * $qty) / $base_factor, 2);
 
     $data->{amounts_cogs}->{ $expense_income_chart->id } -= $linetotal;
     $data->{amounts_cogs}->{ $inventory_chart->id      } += $linetotal;
@@ -317,9 +341,14 @@ In array context a hash with the following keys is returned:
 
 =over 2
 
-=item C<taxes>
+=item C<taxes_by_chart_id>
 
 A hash reference with the calculated taxes. The keys are chart IDs,
+the values the rounded calculated taxes.
+
+=item C<taxes_by_tax_id>
+
+A hash reference with the calculated taxes. The keys are tax IDs,
 the values the calculated taxes.
 
 =item C<amounts>
diff --git a/SL/DB/Helper/SalesPurchaseInvoice.pm b/SL/DB/Helper/SalesPurchaseInvoice.pm
new file mode 100644 (file)
index 0000000..59e4697
--- /dev/null
@@ -0,0 +1,99 @@
+package SL::DB::Helper::SalesPurchaseInvoice;
+
+use strict;
+use utf8;
+
+use parent qw(Exporter);
+our @EXPORT = qw(get_tax_and_amount_by_tax_chart_id);
+
+sub get_tax_and_amount_by_tax_chart_id {
+  my ($self) = @_;
+
+  my $ARAP = $self->is_sales ? 'AR' : 'AP';
+  my ($tax_and_amount_by_tax_id, $total);
+
+  foreach my $transaction (@{ $self->transactions }) {
+    next if $transaction->chart_link =~ m/(^${ARAP}$|paid)/;
+
+    my $tax_or_netamount = $transaction->chart_link =~ m/tax/            ? 'tax'
+                         : $transaction->chart_link =~ m/(${ARAP}_amount|IC_cogs)/ ? 'netamount'
+                         : undef;
+    if ($tax_or_netamount eq 'netamount') {
+      $tax_and_amount_by_tax_id->{ $transaction->tax->chart_id }->{$tax_or_netamount} ||= 0;
+      $tax_and_amount_by_tax_id->{ $transaction->tax->chart_id }->{$tax_or_netamount}  += $transaction->amount;
+      # die "Invalid state" unless $tax_and_amount_by_tax_id->{ $transaction->tax->chart_id }->{tax_id} == 0
+      $tax_and_amount_by_tax_id->{ $transaction->tax->chart_id }->{tax_id}              = $transaction->tax_id;
+    } elsif ($tax_or_netamount eq 'tax') {
+      $tax_and_amount_by_tax_id->{ $transaction->chart_id }->{$tax_or_netamount} ||= 0;
+      $tax_and_amount_by_tax_id->{ $transaction->chart_id }->{$tax_or_netamount}  += $transaction->amount;
+    } else {
+      die "Invalid chart link at: " . $transaction->chart_link unless $tax_or_netamount;
+    }
+    $total ||= 0;
+    $total  += $transaction->amount;
+  }
+  die "Invalid calculated amount. Calc: $total Amount: " . abs($self->amount) if abs($total) - abs($self->amount) > 0.001;
+  return $tax_and_amount_by_tax_id;
+}
+
+
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::Helper::SalesPurchaseInvoice - Helper functions for Sales or Purchase bookings (mirrored)
+
+Delivers the booked amounts split by net amount and tax amount for one ar or ap transaction
+as persisted in the table acc_trans.
+Should be rounding or calculation error prone because all values are already computed before
+the values are written in the acc_trans table.
+
+That is the main purpose for this helper class.
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<get_tax_and_amount_by_tax_chart_id>
+
+Iterates over all transactions for one distinct ar or ap transaction (trans_id in acc_trans) and
+groups the amounts in relation to distinct tax (tax.id) and net amounts (sums all bookings with
+_cogs or _amount chart links).
+Returns a hashref with the chart_id of the tax entry as key like this:
+
+ '775' => {
+    'tax_id'    => 777
+    'tax'       => '332.18',
+    'netamount' => '1748.32',
+  },
+
+ '194' => {
+    'tax_id'    => 378,
+    'netamount' => '20',
+    'tax'       => '1.4'
+  }
+
+C<tax_id> is the id of the used tax. C<tax> ist the amount of tax booked for the whole transaction.
+C<netamount> is the netamount booked with this tax.
+TODO: Please note the hash key chart_id may not be unique but the entry tax_id is always unique.
+
+As additional safety method the functions dies if the calculated sums do not match the
+the whole amount of the transaction with an accuracy of two decimal places.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Jan Büren E<lt>jan@kivitendo.deE<gt>
+
+=cut
index 00eedb9..c536ccd 100644 (file)
@@ -16,7 +16,7 @@ sub make_sort_string {
   my $sort_dir         = defined($params{sort_dir}) ? $params{sort_dir} * 1 : $sort_spec->{default}->[1];
   my $sort_dir_str     = $sort_dir ? 'ASC' : 'DESC';
 
-  my $sort_by          = $params{sort_by};
+  my $sort_by          = $params{sort_by} || { };
   $sort_by             = $sort_spec->{default}->[0] unless $sort_spec->{columns}->{$sort_by};
 
   my $nulls_str        = '';
@@ -27,6 +27,10 @@ sub make_sort_string {
 
   my $sort_by_str = $sort_spec->{columns}->{$sort_by};
   $sort_by_str    = [ $sort_by_str ] unless ref($sort_by_str) eq 'ARRAY';
+
+  # generate tiebreaker
+  push @$sort_by_str, @{ $sort_spec->{tiebreaker} };
+
   $sort_by_str    = join(', ', map { "${_} ${sort_dir_str}${nulls_str}" } @{ $sort_by_str });
 
   return wantarray ? ($sort_by, $sort_dir, $sort_by_str) : $sort_by_str;
@@ -50,6 +54,7 @@ sub _make_sort_spec {
   my %sort_spec = defined &{ "${class}::_sort_spec" } ? $class->_sort_spec : ();
 
   my $meta = $class->object_class->meta;
+  my $table = $meta->table;
 
   if (!$sort_spec{default}) {
     my @primary_keys = $meta->primary_key;
@@ -59,8 +64,6 @@ sub _make_sort_spec {
   $sort_spec{columns} ||= { SIMPLE => [ map { "$_" } $meta->columns ] };
 
   if ($sort_spec{columns}->{SIMPLE}) {
-    my $table = $meta->table;
-
     if (!ref($sort_spec{columns}->{SIMPLE}) && ($sort_spec{columns}->{SIMPLE} eq 'ALL')) {
       map { $sort_spec{columns}->{"$_"} ||= "${table}.${_}"} @{ $meta->columns };
       delete $sort_spec{columns}->{SIMPLE};
@@ -69,6 +72,8 @@ sub _make_sort_spec {
     }
   }
 
+  $sort_spec{tiebreaker} ||= [ map { "${table}.${_}" } $meta->primary_key ];
+
   return \%sort_spec;
 }
 
@@ -211,6 +216,15 @@ Example:
              default                 => 'LAST',
            },
 
+=item C<tiebreaker>
+
+Optional tiebreaker sorting that gets appended to any user requested sorting.
+Needed to make sorting by non unique columns deterministic.
+
+If present must be an arrayref of column sort specs (see C<column>).
+
+Defaults to primary keys.
+
 =back
 
 =back
index 9c65729..eb89ea8 100644 (file)
@@ -20,7 +20,7 @@ sub do_scoping {
 }
 
 sub parts_scoping {
-  SL::DB::Manager::Part->type_filter($_[0]);
# SL::DB::Manager::Part->type_filter($_[0]);
 }
 
 my %specs = ( ar                      => { number_column => 'invnumber',                                                                           },
@@ -30,11 +30,14 @@ my %specs = ( ar                      => { number_column => 'invnumber',
               purchase_order          => { number_column => 'ordnumber',      number_range_column => 'ponumber',       scoping => \&oe_scoping,    },
               sales_delivery_order    => { number_column => 'donumber',       number_range_column => 'sdonumber',      scoping => \&do_scoping,    },
               purchase_delivery_order => { number_column => 'donumber',       number_range_column => 'pdonumber',      scoping => \&do_scoping,    },
+              supplier_delivery_order => { number_column => 'donumber',       number_range_column => 'sudonumber',     scoping => \&do_scoping,    },
+              rma_delivery_order      => { number_column => 'donumber',       number_range_column => 'rdonumber',      scoping => \&do_scoping,    },
               customer                => { number_column => 'customernumber', number_range_column => 'customernumber',                             },
               vendor                  => { number_column => 'vendornumber',   number_range_column => 'vendornumber',                               },
               part                    => { number_column => 'partnumber',     number_range_column => 'articlenumber',  scoping => \&parts_scoping, },
               service                 => { number_column => 'partnumber',     number_range_column => 'servicenumber',  scoping => \&parts_scoping, },
               assembly                => { number_column => 'partnumber',     number_range_column => 'assemblynumber', scoping => \&parts_scoping, },
+              assortment              => { number_column => 'partnumber',     number_range_column => 'assortmentnumber', scoping => \&parts_scoping, },
             );
 
 sub get_next_trans_number {
@@ -97,7 +100,7 @@ sub get_next_trans_number {
   my $range_table    = ($business ? $business : SL::DB::Default->get)->load(for_update => 1);
 
   my $start_number   = $range_table->$number_range_column;
-  $start_number      = $range_table->articlenumber if ($number_range_column eq 'assemblynumber') && (length($start_number) < 1);
+  $start_number      = $range_table->articlenumber if ($number_range_column =~ /^(assemblynumber|assortmentnumber)$/) && (length($start_number)//0 < 1);
   my $sequence       = SL::PrefixedNumber->new(number => $start_number // 0);
 
   if (!$fill_holes_in_range) {
diff --git a/SL/DB/Helper/VATIDNrValidation.pm b/SL/DB/Helper/VATIDNrValidation.pm
new file mode 100644 (file)
index 0000000..8e4047a
--- /dev/null
@@ -0,0 +1,100 @@
+package SL::DB::Helper::VATIDNrValidation;
+
+use strict;
+
+use Carp;
+use SL::Locale::String qw(t8);
+use SL::VATIDNr;
+
+my $_validator;
+
+sub _validate {
+  my ($self, $attribute) = @_;
+
+  my $number = SL::VATIDNr->clean($self->$attribute);
+
+  return () unless length($number);
+  return () if     SL::VATIDNr->validate($number);
+  return ($::locale->text("The VAT ID number '#1' is invalid.", $self->$attribute));
+}
+
+sub import {
+  my ($package, @attributes) = @_;
+
+  my $caller_package         = caller;
+  @attributes                = qw(ustid) unless @attributes;
+
+  no strict 'refs';
+
+  *{ $caller_package . '::validate_vat_id_numbers' } = sub {
+    my ($self) = @_;
+
+    return map { SL::DB::Helper::VATIDNrValidation::_validate($self, $_) } @attributes;
+  };
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::Helper::VATIDNrValidation - Mixin for validating VAT ID number attributes
+
+=head1 SYNOPSIS
+
+  package SL::DB::SomeObject;
+  use SL::DB::Helper::VATIDNrValidation [ ATTRIBUTES ];
+
+  sub validate {
+    my ($self) = @_;
+
+    my @errors;
+    …
+    push @errors, $self->validate_vat_id_numbers;
+
+    return @errors;
+  }
+
+This mixin provides a function C<validate_vat_id_numbers> that returns
+a list of error messages, one for each attribute that fails the VAT ID
+number validation. If all attributes are valid or empty then an empty
+list is returned.
+
+The names of attributes to check can be given as an import list to the
+mixin package. If no attributes are given the single attribute C<ustid>
+is used.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<validate_vat_id_numbers>
+
+This function iterates over all configured attributes and validates
+their content according to how VAT ID numbers are supposed to be
+formatted in the European Union (or the enterprise identification
+numbers in Switzerland). An attribute that is undefined, empty or
+consists solely of whitespace is considered valid, too.
+
+The function returns a list of human-readable error messages suitable
+for use in a general C<validate> function (see SYNOPSIS). For each
+attribute failing the check the list will include one error message.
+
+If all attributes are valid then an empty list is returned.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
diff --git a/SL/DB/Helper/ValidateAssembly.pm b/SL/DB/Helper/ValidateAssembly.pm
new file mode 100644 (file)
index 0000000..430a662
--- /dev/null
@@ -0,0 +1,80 @@
+package SL::DB::Helper::ValidateAssembly;
+
+use strict;
+use parent qw(Exporter);
+our @EXPORT = qw(validate_assembly);
+
+use SL::Locale::String;
+use SL::DB::Part;
+use SL::DB::Assembly;
+
+sub validate_assembly {
+  my ($new_part, $part) = @_;
+
+  return t8("The assembly '#1' cannot be a part from itself.", $part->partnumber) if $new_part->id == $part->id;
+
+  my @seen = ($part->id);
+
+  return assembly_loop_exists(0, $new_part, @seen);
+}
+
+sub assembly_loop_exists {
+  my ($depth, $new_part, @seen) = @_;
+
+  return t8("Too much recursions in assembly tree (>100)") if $depth > 100;
+
+  # 1. check part is an assembly
+  return unless $new_part->is_assembly;
+
+  # 2. check assembly is still in list
+  return t8("The assembly '#1' would make a loop in assembly tree.", $new_part->partnumber) if grep { $_ == $new_part->id } @seen;
+
+  # 3. add to new list
+
+  push @seen, $new_part->id;
+
+  # 4. go into depth for each child
+
+  foreach my $assembly ($new_part->assemblies) {
+    my $retval = assembly_loop_exists($depth + 1, $assembly->part, @seen);
+    return $retval if $retval;
+  }
+  return undef;
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::DB::Helper::ValidateAssembly - Mixin to check loops in assemblies
+
+=head1 SYNOPSIS
+
+SL::DB::Helper::ValidateAssembly->validate_assembly($newpart,$assembly_part);
+
+
+=head1 HELPER FUNCTION
+
+=over 4
+
+=item C<validate_assembly new_part_object  part_object>
+
+A new part is added to an assembly. C<new_part_object> is the part which is want to added.
+
+First it was checked if the new part is equal the actual part.
+Then recursively all assemblies in the assemby are checked for a loop.
+
+The function returns an error string if a loop exists or the maximum of 100 iterations is reached
+else on success ''.
+
+=back
+
+=head1 AUTHOR
+
+Martin Helmling E<lt>martin.helmling@opendynamic.de>E<gt>
+
+=cut
diff --git a/SL/DB/Helper/ZUGFeRD.pm b/SL/DB/Helper/ZUGFeRD.pm
new file mode 100644 (file)
index 0000000..f989d3d
--- /dev/null
@@ -0,0 +1,703 @@
+package SL::DB::Helper::ZUGFeRD;
+
+use strict;
+use utf8;
+
+use parent qw(Exporter);
+our @EXPORT = qw(create_zugferd_data create_zugferd_xmp_data);
+
+use SL::DB::BankAccount;
+use SL::DB::GenericTranslation;
+use SL::DB::Tax;
+use SL::DB::TaxKey;
+use SL::Helper::ISO3166;
+use SL::Helper::ISO4217;
+use SL::Helper::UNECERecommendation20;
+use SL::VATIDNr;
+use SL::ZUGFeRD qw(:PROFILES);
+
+use Carp;
+use Encode qw(encode);
+use List::MoreUtils qw(any pairwise);
+use List::Util qw(first sum);
+use Template;
+use XML::Writer;
+
+my @line_names = qw(LineOne LineTwo LineThree);
+
+my %standards_ids = (
+  PROFILE_FACTURX_EXTENDED() => 'urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended',
+  PROFILE_XRECHNUNG()        => 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0',
+);
+
+sub _is_profile {
+  my ($self, @profiles) = @_;
+  return any { $self->{_zugferd}->{profile} == $_ } @profiles;
+}
+
+sub _u8 {
+  my ($value) = @_;
+  return encode('UTF-8', $value // '');
+}
+
+sub _r2 {
+  my ($value) = @_;
+  return $::form->round_amount($value, 2);
+}
+
+sub _type_name {
+  my ($self) = @_;
+  my $type   = $self->invoice_type;
+
+  no warnings 'once';
+  return $type eq 'ar_transaction' ? $::locale->text('Invoice') : $self->displayable_type;
+}
+
+sub _type_code {
+  my ($self) = @_;
+  my $type   = $self->invoice_type;
+
+  # 326 (Partial invoice)
+  # 380 (Commercial invoice)
+  # 384 (Corrected Invoice)
+  # 381 (Credit note)
+  # 389 (Credit note, self billed invoice)
+
+  return $type eq 'credit_note'        ? 381
+       : $type eq 'invoice_storno'     ? 457
+       : $type eq 'credit_note_storno' ? 458
+       :                                 380;
+}
+
+sub _unit_code {
+  my ($unit) = @_;
+
+  # Mapping from kivitendo's units to UN/ECE Recommendation 20 & 21.
+  my $code = SL::Helper::UNECERecommendation20::map_name_to_code($unit);
+  return $code if $code;
+
+  $::lxdebug->message(LXDebug::WARN(), "ZUGFeRD unit name mapping: no UN/ECE Recommendation 20/21 unit known for kivitendo unit '$unit'; using 'C62'");
+
+  return 'C62';
+}
+
+sub _parse_our_address {
+  my @result;
+  my @street = grep { $_ } ($::instance_conf->get_address_street1, $::instance_conf->get_address_street2);
+
+  push @result, [ 'PostcodeCode', $::instance_conf->get_address_zipcode ] if $::instance_conf->get_address_zipcode;
+  push @result, grep { $_->[1] } pairwise { [ $a, $b] } @line_names, @street;
+  push @result, [ 'CityName', $::instance_conf->get_address_city ] if $::instance_conf->get_address_city;
+  push @result, [ 'CountryID', SL::Helper::ISO3166::map_name_to_alpha_2_code($::instance_conf->get_address_country) // 'DE' ];
+
+  return @result;
+}
+
+sub _customer_postal_trade_address {
+  my (%params) = @_;
+
+  #       <ram:PostalTradeAddress>
+  $params{xml}->startTag("ram:PostalTradeAddress");
+
+  my @parts = grep { $_ } map { $params{customer}->$_ } qw(department_1 department_2 street);
+
+  $params{xml}->dataElement("ram:PostcodeCode", _u8($params{customer}->zipcode));
+  $params{xml}->dataElement("ram:" . $_->[0],   _u8($_->[1])) for grep { $_->[1] } pairwise { [ $a, $b] } @line_names, @parts;
+  $params{xml}->dataElement("ram:CityName",     _u8($params{customer}->city));
+  $params{xml}->dataElement("ram:CountryID",    _u8(SL::Helper::ISO3166::map_name_to_alpha_2_code($params{customer}->country) // 'DE'));
+  $params{xml}->endTag;
+  #       </ram:PostalTradeAddress>
+}
+
+sub _tax_rate_and_code {
+  my ($taxzone, $tax) = @_;
+
+  my ($tax_rate, $tax_code) = @_;
+
+  if ($taxzone->description =~ m{Au.*erhalb}) {
+    $tax_rate = 0;
+    $tax_code = 'G';
+
+  } elsif ($taxzone->description =~ m{EU mit}) {
+    $tax_rate = 0;
+    $tax_code = 'K';
+
+  } else {
+    $tax_rate = $tax->rate * 100;
+    $tax_code = !$tax_rate ? 'Z' : 'S';
+  }
+
+  return (rate => $tax_rate, code => $tax_code);
+}
+
+sub _line_item {
+  my ($self, %params) = @_;
+
+  my $item_ptc = $params{ptc_data}->{items}->[$params{line_number}];
+
+  my $taxkey   = $item_ptc->{taxkey_id} ? SL::DB::TaxKey->load_cached($item_ptc->{taxkey_id}) : undef;
+  my $tax      = $item_ptc->{taxkey_id} ? SL::DB::Tax->load_cached($taxkey->tax_id)           : undef;
+  my %tax_info = _tax_rate_and_code($self->taxzone, $tax);
+
+  # <ram:IncludedSupplyChainTradeLineItem>
+  $params{xml}->startTag("ram:IncludedSupplyChainTradeLineItem");
+
+  #   <ram:AssociatedDocumentLineDocument>
+  $params{xml}->startTag("ram:AssociatedDocumentLineDocument");
+  $params{xml}->dataElement("ram:LineID", $params{line_number} + 1);
+  $params{xml}->endTag;
+
+  $params{xml}->startTag("ram:SpecifiedTradeProduct");
+  $params{xml}->dataElement("ram:SellerAssignedID", _u8($params{item}->part->partnumber));
+  $params{xml}->dataElement("ram:Name",             _u8($params{item}->description));
+  $params{xml}->endTag;
+
+  $params{xml}->startTag("ram:SpecifiedLineTradeAgreement");
+  $params{xml}->startTag("ram:NetPriceProductTradePrice");
+  $params{xml}->dataElement("ram:ChargeAmount", _r2($item_ptc->{sellprice}));
+  $params{xml}->endTag;
+  $params{xml}->endTag;
+  #   </ram:SpecifiedLineTradeAgreement>
+
+  #   <ram:SpecifiedLineTradeDelivery>
+  $params{xml}->startTag("ram:SpecifiedLineTradeDelivery");
+  $params{xml}->dataElement("ram:BilledQuantity", $params{item}->qty, unitCode => _unit_code($params{item}->unit));
+  $params{xml}->endTag;
+  #   </ram:SpecifiedLineTradeDelivery>
+
+  #   <ram:SpecifiedLineTradeSettlement>
+  $params{xml}->startTag("ram:SpecifiedLineTradeSettlement");
+
+  #     <ram:ApplicableTradeTax>
+  $params{xml}->startTag("ram:ApplicableTradeTax");
+  $params{xml}->dataElement("ram:TypeCode",              "VAT");
+  $params{xml}->dataElement("ram:CategoryCode",          $tax_info{code});
+  $params{xml}->dataElement("ram:RateApplicablePercent", _r2($tax_info{rate}));
+  $params{xml}->endTag;
+  #     </ram:ApplicableTradeTax>
+
+  #     <ram:SpecifiedTradeSettlementLineMonetarySummation>
+  $params{xml}->startTag("ram:SpecifiedTradeSettlementLineMonetarySummation");
+  $params{xml}->dataElement("ram:LineTotalAmount", _r2($item_ptc->{linetotal}));
+  $params{xml}->endTag;
+  #     </ram:SpecifiedTradeSettlementLineMonetarySummation>
+
+  $params{xml}->endTag;
+  #   </ram:SpecifiedLineTradeSettlement>
+
+  $params{xml}->endTag;
+  # <ram:IncludedSupplyChainTradeLineItem>
+}
+
+sub _specified_trade_settlement_payment_means {
+  my ($self, %params) = @_;
+
+  #     <ram:SpecifiedTradeSettlementPaymentMeans>
+  $params{xml}->startTag('ram:SpecifiedTradeSettlementPaymentMeans');
+  $params{xml}->dataElement('ram:TypeCode', $self->direct_debit ? 59 : 58); # 59 = SEPA direct debit, 58 = SEPA credit transfer
+
+  if ($self->direct_debit) {
+    $params{xml}->startTag('ram:PayerPartyDebtorFinancialAccount');
+    $params{xml}->dataElement('ram:IBANID', $self->customer->iban);
+    $params{xml}->endTag;
+
+  } else {
+    $params{xml}->startTag('ram:PayeePartyCreditorFinancialAccount');
+    $params{xml}->dataElement('ram:IBANID', $params{bank_account}->iban);
+    $params{xml}->endTag;
+  }
+
+  $params{xml}->endTag;
+  #     </ram:SpecifiedTradeSettlementPaymentMeans>
+}
+
+sub _taxes {
+  my ($self, %params) = @_;
+
+  my %taxkey_info;
+
+  foreach my $item (@{ $params{ptc_data}->{items} }) {
+    $taxkey_info{$item->{taxkey_id}} //= {
+      linetotal  => 0,
+      tax_amount => 0,
+    };
+    my $info             = $taxkey_info{$item->{taxkey_id}};
+    $info->{taxkey}    //= SL::DB::TaxKey->load_cached($item->{taxkey_id});
+    $info->{tax}       //= SL::DB::Tax->load_cached($info->{taxkey}->tax_id);
+    $info->{linetotal}  += $item->{linetotal};
+  }
+
+  foreach my $taxkey_id (sort keys %taxkey_info) {
+    my $info     = $taxkey_info{$taxkey_id};
+    my %tax_info = _tax_rate_and_code($self->taxzone, $info->{tax});
+
+    #     <ram:ApplicableTradeTax>
+    $params{xml}->startTag("ram:ApplicableTradeTax");
+    $params{xml}->dataElement("ram:CalculatedAmount",      _r2($params{ptc_data}->{taxes_by_tax_id}->{$info->{taxkey}->tax_id}));
+    $params{xml}->dataElement("ram:TypeCode",              "VAT");
+    $params{xml}->dataElement("ram:BasisAmount",           _r2($info->{linetotal}));
+    $params{xml}->dataElement("ram:CategoryCode",          $tax_info{code});
+    $params{xml}->dataElement("ram:RateApplicablePercent", _r2($tax_info{rate}));
+    $params{xml}->endTag;
+    #     </ram:ApplicableTradeTax>
+  }
+}
+
+sub _calculate_payment_terms_values {
+  my ($self) = @_;
+
+  my (%vars, %amounts, %formatted_amounts);
+
+  local $::myconfig{numberformat} = $::myconfig{numberformat};
+  local $::myconfig{dateformat}   = $::myconfig{dateformat};
+
+  if ($self->language_id) {
+    my $language = SL::DB::Language->load_cached($self->language_id);
+    $::myconfig{dateformat}   = $language->output_dateformat   if $language->output_dateformat;
+    $::myconfig{numberformat} = $language->output_numberformat if $language->output_numberformat;
+  }
+
+  $vars{currency}              = $self->currency->name if $self->currency;
+  $vars{$_}                    = $self->customer->$_      for qw(account_number bank bank_code bic iban mandate_date_of_signature mandator_id);
+  $vars{$_}                    = $self->payment_terms->$_ for qw(terms_netto terms_skonto percent_skonto);
+  $vars{payment_description}   = $self->payment_terms->description;
+  $vars{netto_date}            = $self->payment_terms->calc_date(reference_date => $self->transdate, due_date => $self->duedate, terms => 'net')->to_kivitendo;
+  $vars{skonto_date}           = $self->payment_terms->calc_date(reference_date => $self->transdate, due_date => $self->duedate, terms => 'discount')->to_kivitendo;
+
+  $amounts{invtotal}           = $self->amount;
+  $amounts{total}              = $self->amount - $self->paid;
+
+  $amounts{skonto_in_percent}  = 100.0 * $vars{percent_skonto};
+  $amounts{skonto_amount}      = $amounts{invtotal} * $vars{percent_skonto};
+  $amounts{invtotal_wo_skonto} = $amounts{invtotal} * (1 - $vars{percent_skonto});
+  $amounts{total_wo_skonto}    = $amounts{total}    * (1 - $vars{percent_skonto});
+
+  foreach (keys %amounts) {
+    $amounts{$_}           = $::form->round_amount($amounts{$_}, 2);
+    $formatted_amounts{$_} = $::form->format_amount(\%::myconfig, $amounts{$_}, 2);
+  }
+
+  return (
+    vars              => \%vars,
+    amounts           => \%amounts,
+    formatted_amounts => \%formatted_amounts,
+  );
+}
+
+sub _format_payment_terms_description {
+  my ($self, %params) = @_;
+
+  my $description = ($self->payment_terms->translated_attribute('description_long_invoice', $self->language_id) // '') || $self->payment_terms->description_long_invoice;
+  $description    =~ s{<\%$_\%>}{ $params{vars}->{$_} }ge              for keys %{ $params{vars} };
+  $description    =~ s{<\%$_\%>}{ $params{formatted_amounts}->{$_} }ge for keys %{ $params{formatted_amounts} };
+
+  if (_is_profile($self, PROFILE_XRECHNUNG())) {
+    my @terms;
+
+    if ($self->payment_terms->terms_skonto && ($self->payment_terms->percent_skonto * 1)) {
+      push @terms, sprintf("#SKONTO#TAGE=\%d#PROZENT=\%.2f#\n", $self->payment_terms->terms_skonto, $self->payment_terms->percent_skonto * 100);
+    }
+
+    $description =~ s{#}{_}g;
+    $description =  join('', @terms) . $description;
+  }
+
+  return $description;
+}
+
+sub _payment_terms {
+  my ($self, %params) = @_;
+
+  return unless $self->payment_terms;
+
+  my %payment_terms_vars = _calculate_payment_terms_values($self);
+
+  #     <ram:SpecifiedTradePaymentTerms>
+  $params{xml}->startTag("ram:SpecifiedTradePaymentTerms");
+
+  $params{xml}->dataElement("ram:Description", _u8(_format_payment_terms_description($self, %payment_terms_vars)));
+
+  #       <ram:DueDateDateTime>
+  $params{xml}->startTag("ram:DueDateDateTime");
+  $params{xml}->dataElement("udt:DateTimeString", $self->duedate->strftime('%Y%m%d'), format => "102");
+  $params{xml}->endTag;
+  #       </ram:DueDateDateTime>
+
+  if (   _is_profile($self, PROFILE_FACTURX_EXTENDED())
+      && $self->payment_terms->percent_skonto
+      && $self->payment_terms->terms_skonto) {
+    my $currency_id = _u8(SL::Helper::ISO4217::map_currency_name_to_code($self->currency->name) // 'EUR');
+
+    #       <ram:ApplicableTradePaymentDiscountTerms>
+    $params{xml}->startTag("ram:ApplicableTradePaymentDiscountTerms");
+    $params{xml}->dataElement("ram:BasisPeriodMeasure", $self->payment_terms->terms_skonto, unitCode => "DAY");
+    $params{xml}->dataElement("ram:BasisAmount",        _r2($payment_terms_vars{amounts}->{invtotal}), currencyID => $currency_id);
+    $params{xml}->dataElement("ram:CalculationPercent", _r2($self->payment_terms->percent_skonto * 100));
+    $params{xml}->endTag;
+    #       </ram:ApplicableTradePaymentDiscountTerms>
+  }
+
+  $params{xml}->endTag;
+  #     </ram:SpecifiedTradePaymentTerms>
+}
+
+sub _totals {
+  my ($self, %params) = @_;
+
+  #     <ram:SpecifiedTradeSettlementHeaderMonetarySummation>
+  $params{xml}->startTag("ram:SpecifiedTradeSettlementHeaderMonetarySummation");
+
+  $params{xml}->dataElement("ram:LineTotalAmount",     _r2($self->netamount));
+  $params{xml}->dataElement("ram:TaxBasisTotalAmount", _r2($self->netamount));
+  $params{xml}->dataElement("ram:TaxTotalAmount",      _r2(sum(values %{ $params{ptc_data}->{taxes_by_tax_id} })), currencyID => "EUR");
+  $params{xml}->dataElement("ram:GrandTotalAmount",    _r2($self->amount));
+  $params{xml}->dataElement("ram:TotalPrepaidAmount",  _r2($self->paid));
+  $params{xml}->dataElement("ram:DuePayableAmount",    _r2($self->amount - $self->paid));
+
+  $params{xml}->endTag;
+  #     </ram:SpecifiedTradeSettlementHeaderMonetarySummation>
+}
+
+sub _exchanged_document_context {
+  my ($self, %params) = @_;
+
+  #   <rsm:ExchangedDocumentContext>
+  $params{xml}->startTag("rsm:ExchangedDocumentContext");
+
+  if ($self->{_zugferd}->{test_mode}) {
+    $params{xml}->startTag("ram:TestIndicator");
+    $params{xml}->dataElement("udt:Indicator", "true");
+    $params{xml}->endTag;
+  }
+
+  $params{xml}->startTag("ram:GuidelineSpecifiedDocumentContextParameter");
+  $params{xml}->dataElement("ram:ID", $standards_ids{ $self->{_zugferd}->{profile} });
+  $params{xml}->endTag;
+  $params{xml}->endTag;
+  #   </rsm:ExchangedDocumentContext>
+}
+
+sub _included_note {
+  my ($self, %params) = @_;
+
+  $params{xml}->startTag("ram:IncludedNote");
+  $params{xml}->dataElement("ram:Content", _u8($params{note}));
+  $params{xml}->endTag;
+}
+
+sub _exchanged_document {
+  my ($self, %params) = @_;
+
+  #   <rsm:ExchangedDocument>
+  $params{xml}->startTag("rsm:ExchangedDocument");
+
+  $params{xml}->dataElement("ram:ID",       _u8($self->invnumber));
+  $params{xml}->dataElement("ram:Name",     _u8(_type_name($self))) if _is_profile($self, PROFILE_FACTURX_EXTENDED());
+  $params{xml}->dataElement("ram:TypeCode", _u8(_type_code($self)));
+
+  #     <ram:IssueDateTime>
+  $params{xml}->startTag("ram:IssueDateTime");
+  $params{xml}->dataElement("udt:DateTimeString", $self->transdate->strftime('%Y%m%d'), format => "102");
+  $params{xml}->endTag;
+  #     </ram:IssueDateTime>
+
+  if (   _is_profile($self, PROFILE_FACTURX_EXTENDED())
+      && $self->language
+      && (($self->language->template_code // '') =~ m{^(de|en)}i)) {
+    $params{xml}->dataElement("ram:LanguageID", uc($1));
+  }
+
+  my $std_notes = SL::DB::Manager::GenericTranslation->get_all(
+    where => [
+      translation_type => 'ZUGFeRD/notes',
+      or               => [
+        language_id    => undef,
+        language_id    => $self->language_id,
+      ],
+      '!translation'   => undef,
+      '!translation'   => '',
+    ],
+  );
+
+  my $std_note = first { $_->language_id == $self->language_id } @{ $std_notes };
+  $std_note  //= first { !defined $_->language_id }              @{ $std_notes };
+
+  my $notes = $self->notes_as_stripped_html;
+
+  _included_note($self, %params, note => $self->transaction_description) if $self->transaction_description;
+  _included_note($self, %params, note => $notes)                         if $notes;
+  _included_note($self, %params, note => $std_note->translation)         if $std_note;
+
+  $params{xml}->endTag;
+  #   </rsm:ExchangedDocument>
+}
+
+sub _specified_tax_registration {
+  my ($ustid_nr, %params) = @_;
+
+  #         <ram:SpecifiedTaxRegistration>
+  $params{xml}->startTag("ram:SpecifiedTaxRegistration");
+  $params{xml}->dataElement("ram:ID", _u8(SL::VATIDNr->normalize($ustid_nr)), schemeID => "VA");
+  $params{xml}->endTag;
+  #         </ram:SpecifiedTaxRegistration>
+}
+
+sub _seller_trade_party {
+  my ($self, %params) = @_;
+
+  my @our_address            = _parse_our_address();
+
+  my $sales_person           = $self->salesman;
+  my $sales_person_auth      = SL::DB::Manager::AuthUser->find_by(login => $sales_person->login);
+  my %sales_person_cfg       = $sales_person_auth ? %{ $sales_person_auth->config_values } : ();
+  $sales_person_cfg{email} ||= $sales_person->deleted_email;
+  $sales_person_cfg{tel}   ||= $sales_person->deleted_tel;
+
+  #       <ram:SellerTradeParty>
+  $params{xml}->startTag("ram:SellerTradeParty");
+  $params{xml}->dataElement("ram:ID",   _u8($self->customer->c_vendor_id)) if ($self->customer->c_vendor_id // '') ne '';
+  $params{xml}->dataElement("ram:Name", _u8($::instance_conf->get_company));
+
+  #         <ram:DefinedTradeContact>
+  $params{xml}->startTag("ram:DefinedTradeContact");
+
+  $params{xml}->dataElement("ram:PersonName", _u8($sales_person->safe_name));
+
+  if ($sales_person_cfg{tel}) {
+    $params{xml}->startTag("ram:TelephoneUniversalCommunication");
+    $params{xml}->dataElement("ram:CompleteNumber", _u8($sales_person_cfg{tel}));
+    $params{xml}->endTag;
+  }
+
+  if ($sales_person_cfg{email}) {
+    $params{xml}->startTag("ram:EmailURIUniversalCommunication");
+    $params{xml}->dataElement("ram:URIID", _u8($sales_person_cfg{email}));
+    $params{xml}->endTag;
+  }
+
+  $params{xml}->endTag;
+  #         </ram:DefinedTradeContact>
+
+  if (@our_address) {
+    #         <ram:PostalTradeAddress>
+    $params{xml}->startTag("ram:PostalTradeAddress");
+    foreach my $element (@our_address) {
+      $params{xml}->dataElement("ram:" . $element->[0], _u8($element->[1]));
+    }
+    $params{xml}->endTag;
+    #         </ram:PostalTradeAddress>
+  }
+
+  _specified_tax_registration($::instance_conf->get_co_ustid, %params);
+
+  $params{xml}->endTag;
+  #     </ram:SellerTradeParty>
+}
+
+sub _buyer_trade_party {
+  my ($self, %params) = @_;
+
+  #       <ram:BuyerTradeParty>
+  $params{xml}->startTag("ram:BuyerTradeParty");
+  $params{xml}->dataElement("ram:ID",   _u8($self->customer->customernumber));
+  $params{xml}->dataElement("ram:Name", _u8($self->customer->name));
+
+  _customer_postal_trade_address(%params, customer => $self->customer);
+  _specified_tax_registration($self->customer->ustid, %params) if $self->customer->ustid;
+
+  $params{xml}->endTag;
+  #       </ram:BuyerTradeParty>
+}
+
+sub _included_supply_chain_trade_line_item {
+  my ($self, %params) = @_;
+
+  my $line_number = 0;
+  foreach my $item (@{ $self->items }) {
+    _line_item($self, %params, item => $item, line_number => $line_number);
+    $line_number++;
+  }
+}
+
+sub _applicable_header_trade_agreement {
+  my ($self, %params) = @_;
+
+  #     <ram:ApplicableHeaderTradeAgreement>
+  $params{xml}->startTag("ram:ApplicableHeaderTradeAgreement");
+
+  $params{xml}->dataElement("ram:BuyerReference", _u8($self->customer->c_vendor_routing_id)) if $self->customer->c_vendor_routing_id;
+
+  _seller_trade_party($self, %params);
+  _buyer_trade_party($self, %params);
+
+  if ($self->cusordnumber) {
+    #     <ram:BuyerOrderReferencedDocument>
+    $params{xml}->startTag("ram:BuyerOrderReferencedDocument");
+    $params{xml}->dataElement("ram:IssuerAssignedID", _u8($self->cusordnumber));
+    $params{xml}->endTag;
+    #     </ram:BuyerOrderReferencedDocument>
+  }
+
+  $params{xml}->endTag;
+  #     </ram:ApplicableHeaderTradeAgreement>
+}
+
+sub _applicable_header_trade_delivery {
+  my ($self, %params) = @_;
+
+  #     <ram:ApplicableHeaderTradeDelivery>
+  $params{xml}->startTag("ram:ApplicableHeaderTradeDelivery");
+  #       <ram:ActualDeliverySupplyChainEvent>
+  $params{xml}->startTag("ram:ActualDeliverySupplyChainEvent");
+
+  $params{xml}->startTag("ram:OccurrenceDateTime");
+  $params{xml}->dataElement("udt:DateTimeString", ($self->deliverydate // $self->transdate)->strftime('%Y%m%d'), format => "102");
+  $params{xml}->endTag;
+
+  $params{xml}->endTag;
+  #       </ram:ActualDeliverySupplyChainEvent>
+  $params{xml}->endTag;
+  #     </ram:ApplicableHeaderTradeDelivery>
+}
+
+sub _applicable_header_trade_settlement {
+  my ($self, %params) = @_;
+
+  #     <ram:ApplicableHeaderTradeSettlement>
+  $params{xml}->startTag("ram:ApplicableHeaderTradeSettlement");
+  $params{xml}->dataElement("ram:InvoiceCurrencyCode", _u8(SL::Helper::ISO4217::map_currency_name_to_code($self->currency->name) // 'EUR'));
+
+  _specified_trade_settlement_payment_means($self, %params);
+  _taxes($self, %params);
+  _payment_terms($self, %params);
+  _totals($self, %params);
+
+  $params{xml}->endTag;
+  #     </ram:ApplicableHeaderTradeSettlement>
+}
+
+sub _supply_chain_trade_transaction {
+  my ($self, %params) = @_;
+
+  #   <rsm:SupplyChainTradeTransaction>
+  $params{xml}->startTag("rsm:SupplyChainTradeTransaction");
+
+  _included_supply_chain_trade_line_item($self, %params);
+  _applicable_header_trade_agreement($self, %params);
+  _applicable_header_trade_delivery($self, %params);
+  _applicable_header_trade_settlement($self, %params);
+
+  $params{xml}->endTag;
+  #   </rsm:SupplyChainTradeTransaction>
+}
+
+sub _validate_data {
+  my ($self) = @_;
+
+  my %result;
+  my $prefix = $::locale->text('The ZUGFeRD invoice data cannot be generated because the data validation failed.') . ' ';
+
+  if (!$::instance_conf->get_co_ustid) {
+    SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('The VAT registration number is missing in the client configuration.'));
+  }
+
+  if (!SL::VATIDNr->validate($::instance_conf->get_co_ustid)) {
+    SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text("The VAT ID number in the client configuration is invalid."));
+  }
+
+  if (!$::instance_conf->get_company || any { my $get = "get_address_$_"; !$::instance_conf->$get } qw(street1 zipcode city)) {
+    SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('The company\'s address information is incomplete in the client configuration.'));
+  }
+
+  if ($::instance_conf->get_address_country && !SL::Helper::ISO3166::map_name_to_alpha_2_code($::instance_conf->get_address_country)) {
+    SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('The country from the company\'s address in the client configuration cannot be mapped to an ISO 3166-1 alpha 2 code.'));
+  }
+
+  if ($self->customer->country && !SL::Helper::ISO3166::map_name_to_alpha_2_code($self->customer->country)) {
+    SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('The country from the customer\'s address cannot be mapped to an ISO 3166-1 alpha 2 code.'));
+  }
+
+  if (!SL::Helper::ISO4217::map_currency_name_to_code($self->currency->name)) {
+    SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('The currency "#1" cannot be mapped to an ISO 4217 currency code.', $self->currency->name));
+  }
+
+  my $failed_unit = first { !SL::Helper::UNECERecommendation20::map_name_to_code($_) } map { $_->unit } @{ $self->items };
+  if ($failed_unit) {
+    SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('One of the units used (#1) cannot be mapped to a known unit code from the UN/ECE Recommendation 20 list.', $failed_unit));
+  }
+
+  if ($self->direct_debit) {
+    if (!$self->customer->iban) {
+      SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('The customer\'s bank account number (IBAN) is missing.'));
+    }
+
+  } else {
+    my $bank_accounts     = SL::DB::Manager::BankAccount->get_all;
+    $result{bank_account} = scalar(@{ $bank_accounts }) == 1 ? $bank_accounts->[0] : first { $_->use_for_zugferd } @{ $bank_accounts };
+
+    if (!$result{bank_account}) {
+      SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('No bank account flagged for Factur-X/ZUGFeRD usage was found.'));
+    }
+  }
+
+  if (_is_profile($self, PROFILE_XRECHNUNG())) {
+    if (!$self->customer->c_vendor_routing_id) {
+      SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('The value \'our routing id at customer\' must be set in the customer\'s master data for profile #1.', 'XRechnung 2.0'));
+    }
+  }
+
+  return %result;
+}
+
+sub create_zugferd_data {
+  my ($self)        = @_;
+  $self->{_zugferd} = { SL::ZUGFeRD->convert_customer_setting($self->customer->create_zugferd_invoices_for_this_customer) };
+
+  if (!$standards_ids{ $self->{_zugferd}->{profile} }) {
+    croak "Profile '" . $self->{_zugferd}->{profile} . "' is not supported";
+  }
+
+  my $output        = '';
+
+  my %params        = _validate_data($self);
+  $params{ptc_data} = { $self->calculate_prices_and_taxes };
+  $params{xml}      = XML::Writer->new(
+    OUTPUT          => \$output,
+    DATA_MODE       => 1,
+    DATA_INDENT     => 2,
+    ENCODING        => 'utf-8',
+  );
+
+  $params{xml}->xmlDecl();
+
+  # <rsm:CrossIndustryInvoice>
+  $params{xml}->startTag("rsm:CrossIndustryInvoice",
+                         "xmlns:a"   => "urn:un:unece:uncefact:data:standard:QualifiedDataType:100",
+                         "xmlns:rsm" => "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100",
+                         "xmlns:qdt" => "urn:un:unece:uncefact:data:standard:QualifiedDataType:10",
+                         "xmlns:ram" => "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100",
+                         "xmlns:xs"  => "http://www.w3.org/2001/XMLSchema",
+                         "xmlns:udt" => "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100");
+
+  _exchanged_document_context($self, %params);
+  _exchanged_document($self, %params);
+  _supply_chain_trade_transaction($self, %params);
+
+  $params{xml}->endTag;
+  # </rsm:CrossIndustryInvoice>
+
+  return $output;
+}
+
+sub create_zugferd_xmp_data {
+  my ($self) = @_;
+
+  return {
+    conformance_level  => 'EXTENDED',
+    document_file_name => 'factur-x.xml',
+    document_type      => 'INVOICE',
+    version            => '1.0',
+  };
+}
+
+1;
index 7528b73..98c465a 100644 (file)
@@ -12,4 +12,49 @@ __PACKAGE__->meta->initialize;
 # Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
 __PACKAGE__->meta->make_manager_class;
 
+sub parsed_snumber {
+  my ($self) = @_;
+
+  my ($snumber) = $self->snumbers =~ /^.*?_(.*)/;
+  return $snumber ? $snumber : $self->snumbers;
+}
+
+1
+
+__END__
+
+=pod
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::DB::History: Model for the 'history_erp' table
+
+=head1 SYNOPSIS
+
+This is a standard Rose::DB::Object based model and can be used as one.
+
+=head1 METHODS
+
+=over 4
+
+=item C<parsed_snumber>
+
+The column snumbers contains entries such as "partnumber_3" or
+"customernumber_23".
+
+To be able to print only the number, parsed_snumber returns only the part of
+the string following the first "_".
+
+Returns the whole string if the regex doesn't match anything.
+
+=back
+
+=head1 AUTHORS
+
+G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
+
+=cut
+
 1;
index b8f28a3..8557719 100644 (file)
@@ -4,17 +4,81 @@
 package SL::DB::Inventory;
 
 use strict;
+use Carp;
+use DateTime;
 
+use SL::DBUtils qw(selectrow_query);
 use SL::DB::MetaSetup::Inventory;
+use SL::DB::Manager::Inventory;
 
 __PACKAGE__->meta->initialize;
 
-# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
-__PACKAGE__->meta->make_manager_class;
+__PACKAGE__->before_save(\&_before_save_create_trans_id);
+__PACKAGE__->before_save(\&_before_save_set_shippingdate);
+__PACKAGE__->before_save(\&_before_save_set_employee);
 
 # part accessor is badly named
 sub part {
   goto &parts;
 }
 
+sub new_from {
+  my ($class, $obj) = @_;
+
+  if ('SL::DB::DeliveryOrderItemsStock' eq ref $obj) {
+    return $class->new_from_delivery_order_stock($obj);
+  }
+
+  croak "unknown obj type (@{[ ref $obj ]}) for SL::DB::Inventory::new_from";
+}
+
+sub new_from_delivery_order_stock {
+  my ($class, $stock) = @_;
+
+  my $project = $stock->delivery_order_item->effective_project;
+
+  return $class->new(
+    delivery_order_items_stock_id => $stock->id,
+    parts_id                      => $stock->delivery_order_item->parts_id,
+    qty                           => $stock->unit_obj->convert_to($stock->qty => $stock->delivery_order_item->part->unit_obj),
+    warehouse_id                  => $stock->warehouse_id,
+    bin_id                        => $stock->bin_id,
+    chargenumber                  => $stock->chargenumber,
+    bestbefore                    => $stock->bestbefore,
+    project_id                    => $project ? $project->id : undef,
+    # trans_type - not set here, set in controller
+  );
+}
+
+sub _before_save_create_trans_id {
+  my ($self, %params) = @_;
+
+  return 1 if $self->trans_id;
+
+  my ($trans_id) = selectrow_query($::form, SL::DB->client->dbh, qq|SELECT nextval('id')|);
+
+  $self->trans_id($trans_id);
+
+  return 1;
+}
+
+sub _before_save_set_shippingdate {
+  my ($self, %params) = @_;
+
+  return 1 if $self->shippingdate;
+
+  $self->shippingdate(DateTime->now);
+
+  return 1;
+}
+
+sub _before_save_set_employee {
+  my ($self, %params) = @_;
+
+  return 1 if $self->employee_id;
+
+  $self->employee(SL::DB::Manager::Employee->current);
+
+  return 1;
+}
 1;
index 54da8fa..d1ce5be 100644 (file)
@@ -3,9 +3,9 @@ package SL::DB::Invoice;
 use strict;
 
 use Carp;
-use List::Util qw(first);
+use List::Util qw(first sum);
 
-use Rose::DB::Object::Helpers ();
+use Rose::DB::Object::Helpers qw(has_loaded_related forget_related);
 use SL::DB::MetaSetup::Invoice;
 use SL::DB::Manager::Invoice;
 use SL::DB::Helper::Payment qw(:ALL);
@@ -13,9 +13,12 @@ use SL::DB::Helper::AttrHTML;
 use SL::DB::Helper::AttrSorted;
 use SL::DB::Helper::FlattenToForm;
 use SL::DB::Helper::LinkedRecords;
+use SL::DB::Helper::PDF_A;
 use SL::DB::Helper::PriceTaxCalculator;
 use SL::DB::Helper::PriceUpdater;
+use SL::DB::Helper::SalesPurchaseInvoice;
 use SL::DB::Helper::TransNumberGenerator;
+use SL::DB::Helper::ZUGFeRD;
 use SL::Locale::String qw(t8);
 use SL::DB::CustomVariable;
 
@@ -60,6 +63,12 @@ __PACKAGE__->meta->add_relationship(
       sort_by      => 'acc_trans_id ASC',
     },
   },
+  dunnings       => {
+    type         => 'one to many',
+    class        => 'SL::DB::Dunning',
+    column_map   => { id => 'trans_id' },
+    manager_args => { with_objects => [ 'dunnings' ] }
+  },
 );
 
 __PACKAGE__->meta->initialize;
@@ -131,7 +140,7 @@ sub closed {
 sub _clone_orderitem_delivery_order_item_cvar {
   my ($cvar) = @_;
 
-  my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
+  my $cloned = $_->clone_and_reset;
   $cloned->sub_module('invoice');
 
   return $cloned;
@@ -148,7 +157,7 @@ sub new_from {
   my (@columns, @item_columns, $item_parent_id_column, $item_parent_column);
 
   if (ref($source) eq 'SL::DB::Order') {
-    @columns      = qw(quonumber delivery_customer_id delivery_vendor_id);
+    @columns      = qw(quonumber delivery_customer_id delivery_vendor_id tax_point);
     @item_columns = qw(subtotal);
 
     $item_parent_id_column = 'trans_id';
@@ -162,10 +171,12 @@ sub new_from {
   }
 
   my $terms = $source->can('payment_id') ? $source->payment_terms : undef;
+  $terms = $source->customer->payment_terms if !defined $terms && $source->customer;
 
   my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
-                                                cp_id language_id taxzone_id shipto_id globalproject_id transaction_description currency_id delivery_term_id payment_id), @columns),
-               transdate   => DateTime->today_local,
+                                                cp_id language_id taxzone_id tax_point globalproject_id transaction_description currency_id delivery_term_id
+                                                billing_address_id), @columns),
+               transdate   => $params{transdate} // DateTime->today_local,
                gldate      => DateTime->today_local,
                duedate     => $terms ? $terms->calc_date(reference_date => DateTime->today_local) : DateTime->today_local,
                invoice     => 1,
@@ -175,13 +186,33 @@ sub new_from {
                employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
             );
 
-  if ($source->type =~ /_order$/) {
+  $args{payment_id} = ( $terms ? $terms->id : $source->payment_id);
+
+  if ($source->type =~ /_delivery_order$/) {
+    $args{deliverydate} = $source->reqdate;
+    if (my $order = SL::DB::Manager::Order->find_by(ordnumber => $source->ordnumber)) {
+      $args{orddate}    = $order->transdate;
+    }
+
+  } elsif ($source->type =~ /_order$/) {
     $args{deliverydate} = $source->reqdate;
     $args{orddate}      = $source->transdate;
+
   } else {
     $args{quodate}      = $source->transdate;
   }
 
+  # Custom shipto addresses (the ones specific to the sales/purchase
+  # record and not to the customer/vendor) are only linked from shipto
+  # → ar. Meaning ar.shipto_id will not be filled in that
+  # case.
+  if (!$source->shipto_id && $source->id) {
+    $args{custom_shipto} = $source->custom_shipto->clone($class) if $source->can('custom_shipto') && $source->custom_shipto;
+
+  } else {
+    $args{shipto_id} = $source->shipto_id;
+  }
+
   my $invoice = $class->new(%args);
   $invoice->assign_attributes(%{ $params{attributes} }) if $params{attributes};
   my $items   = delete($params{items}) || $source->items_sorted;
@@ -222,16 +253,23 @@ sub new_from {
 sub post {
   my ($self, %params) = @_;
 
+  die "not an invoice" unless $self->invoice;
+
   require SL::DB::Chart;
   if (!$params{ar_id}) {
-    my $chart = SL::DB::Manager::Chart->get_all(query   => [ SL::DB::Manager::Chart->link_filter('AR') ],
-                                                sort_by => 'id ASC',
-                                                limit   => 1)->[0];
-    croak("No AR chart found and no parameter `ar_id' given") unless $chart;
+    my $chart;
+    if ($::instance_conf->get_ar_chart_id) {
+      $chart = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_ar_chart_id);
+    } else {
+      $chart = SL::DB::Manager::Chart->get_all(query   => [ SL::DB::Manager::Chart->link_filter('AR') ],
+                                               sort_by => 'id ASC',
+                                               limit   => 1)->[0];
+    };
+    croak("No AR chart found and no parameter 'ar_id' given") unless $chart;
     $params{ar_id} = $chart->id;
   }
 
-  my $worker = sub {
+  if (!$self->db->with_transaction(sub {
     my %data = $self->calculate_prices_and_taxes;
 
     $self->_post_create_assemblyitem_entries($data{assembly_items});
@@ -239,16 +277,16 @@ sub post {
 
     $self->_post_add_acctrans($data{amounts_cogs});
     $self->_post_add_acctrans($data{amounts});
-    $self->_post_add_acctrans($data{taxes});
+    $self->_post_add_acctrans($data{taxes_by_chart_id});
 
     $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
 
     $self->_post_update_allocated($data{allocated});
-  };
 
-  if ($self->db->in_transaction) {
-    $worker->();
-  } elsif (!$self->db->do_transaction($worker)) {
+    $self->_post_book_rounding($data{rounding});
+
+    1;
+  })) {
     $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
     return undef;
   }
@@ -269,17 +307,197 @@ sub _post_add_acctrans {
     $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
     $chart_link ||= '';
 
+    if ($spec->{amount} != 0) {
+      SL::DB::AccTransaction->new(trans_id   => $self->id,
+                                  chart_id   => $chart_id,
+                                  amount     => $spec->{amount},
+                                  tax_id     => $spec->{tax_id},
+                                  taxkey     => $spec->{taxkey},
+                                  project_id => $self->globalproject_id,
+                                  transdate  => $self->transdate,
+                                  chart_link => $chart_link)->save;
+    }
+  }
+}
+
+sub _post_book_rounding {
+  my ($self, $rounding) = @_;
+
+  my $tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
+  my $rnd_accno = $rounding == 0 ? 0
+                : $rounding > 0  ? SL::DB::Default->get->rndgain_accno_id
+                :                  SL::DB::Default->get->rndloss_accno_id
+  ;
+  if ($rnd_accno != 0) {
     SL::DB::AccTransaction->new(trans_id   => $self->id,
-                                chart_id   => $chart_id,
-                                amount     => $spec->{amount},
-                                tax_id     => $spec->{tax_id},
-                                taxkey     => $spec->{taxkey},
+                                chart_id   => $rnd_accno,
+                                amount     => $rounding,
+                                tax_id     => $tax_id,
+                                taxkey     => 0,
                                 project_id => $self->globalproject_id,
                                 transdate  => $self->transdate,
-                                chart_link => $chart_link)->save;
+                                chart_link => $rnd_accno)->save;
   }
 }
 
+sub add_ar_amount_row {
+  my ($self, %params ) = @_;
+
+  # only allow this method for ar invoices (Debitorenbuchung)
+  die "not an ar invoice" if $self->invoice and not $self->customer_id;
+
+  die "add_ar_amount_row needs a chart object as chart param" unless $params{chart} && $params{chart}->isa('SL::DB::Chart');
+  die "chart must be an AR_amount chart" unless $params{chart}->link =~ /AR_amount/;
+
+  my $acc_trans = [];
+
+  my $roundplaces = 2;
+  my ($netamount,$taxamount);
+
+  $netamount = $params{amount} * 1;
+  my $tax = SL::DB::Manager::Tax->find_by(id => $params{tax_id}) || die "Can't find tax with id " . $params{tax_id};
+
+  if ( $tax and $tax->rate != 0 ) {
+    ($netamount, $taxamount) = Form->calculate_tax($params{amount}, $tax->rate, $self->taxincluded, $roundplaces);
+  };
+  next unless $netamount; # netamount mustn't be zero
+  my $sign = $self->customer_id ? 1 : -1;
+  my $acc = SL::DB::AccTransaction->new(
+    amount     => $netamount * $sign,
+    chart_id   => $params{chart}->id,
+    chart_link => $params{chart}->link,
+    transdate  => $self->transdate,
+    gldate     => $self->gldate,
+    taxkey     => $tax->taxkey,
+    tax_id     => $tax->id,
+    project_id => $params{project_id},
+  );
+
+  $self->add_transactions( $acc );
+  push( @$acc_trans, $acc );
+
+  if ( $taxamount ) {
+     my $acc = SL::DB::AccTransaction->new(
+       amount     => $taxamount * $sign,
+       chart_id   => $tax->chart_id,
+       chart_link => $tax->chart->link,
+       transdate  => $self->transdate,
+       gldate     => $self->gldate,
+       taxkey     => $tax->taxkey,
+       tax_id     => $tax->id,
+     );
+     $self->add_transactions( $acc );
+     push( @$acc_trans, $acc );
+  };
+  return $acc_trans;
+};
+
+sub create_ar_row {
+  my ($self, %params) = @_;
+  # to be called after adding all AR_amount rows, adds an AR row
+
+  # only allow this method for ar invoices (Debitorenbuchung)
+  die if $self->invoice and not $self->customer_id;
+  die "create_ar_row needs a chart object as a parameter" unless $params{chart} and ref($params{chart}) eq 'SL::DB::Chart';
+
+  my @transactions = @{$self->transactions};
+  # die "invoice has no acc_transactions" unless scalar @transactions > 0;
+  return 0 unless scalar @transactions > 0;
+
+  my $chart = $params{chart} || SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_ar_chart_id);
+  die "illegal chart in create_ar_row" unless $chart;
+
+  die "receivables chart must have link 'AR'" unless $chart->link eq 'AR';
+
+  my $acc_trans = [];
+
+  # hardcoded entry for no tax: tax_id and taxkey should be 0
+  my $tax = SL::DB::Manager::Tax->find_by(id => 0, taxkey => 0) || die "Can't find tax with id 0 and taxkey 0";
+
+  my $sign = $self->customer_id ? -1 : 1;
+  my $acc = SL::DB::AccTransaction->new(
+    amount     => $self->amount * $sign,
+    chart_id   => $params{chart}->id,
+    chart_link => $params{chart}->link,
+    transdate  => $self->transdate,
+    taxkey     => $tax->taxkey,
+    tax_id     => $tax->id,
+  );
+  $self->add_transactions( $acc );
+  push( @$acc_trans, $acc );
+  return $acc_trans;
+}
+
+sub validate_acc_trans {
+  my ($self, %params) = @_;
+  # should be able to check unsaved invoice objects with several acc_trans lines
+
+  die "validate_acc_trans can't check invoice object with empty transactions" unless $self->transactions;
+
+  my @transactions = @{$self->transactions};
+  # die "invoice has no acc_transactions" unless scalar @transactions > 0;
+  return 0 unless scalar @transactions > 0;
+  return 0 unless $self->has_loaded_related('transactions');
+  if ( $params{debug} ) {
+    printf("starting validatation of invoice %s with trans_id %s and taxincluded %s\n", $self->invnumber, $self->id, $self->taxincluded);
+    foreach my $acc ( @transactions ) {
+      printf("chart: %s  amount: %s   tax_id: %s  link: %s\n", $acc->chart->accno, $acc->amount, $acc->tax_id, $acc->chart->link);
+    };
+  };
+
+  my $acc_trans_sum = sum map { $_->amount } @transactions;
+
+  unless ( $::form->round_amount($acc_trans_sum, 10) == 0 ) {
+    my $string = "sum of acc_transactions isn't 0: $acc_trans_sum\n";
+
+    if ( $params{debug} ) {
+      foreach my $trans ( @transactions ) {
+          $string .= sprintf("  %s %s %s\n", $trans->chart->accno, $trans->taxkey, $trans->amount);
+      };
+    };
+    return 0;
+  };
+
+  # only use the first AR entry, so it also works for paid invoices
+  my @ar_transactions = map { $_->amount } grep { $_->chart_link eq 'AR' } @transactions;
+  my $ar_sum = $ar_transactions[0];
+  # my $ar_sum = sum map { $_->amount } grep { $_->chart_link eq 'AR' } @transactions;
+
+  unless ( $::form->round_amount($ar_sum * -1,2) == $::form->round_amount($self->amount,2) ) {
+    if ( $params{debug} ) {
+      printf("debug: (ar_sum) %s = %s (amount)\n",  $::form->round_amount($ar_sum * -1,2) , $::form->round_amount($self->amount, 2) );
+      foreach my $trans ( @transactions ) {
+        printf("  %s %s %s %s\n", $trans->chart->accno, $trans->taxkey, $trans->amount, $trans->chart->link);
+      };
+    };
+    die sprintf("sum of ar (%s) isn't equal to invoice amount (%s)", $::form->round_amount($ar_sum * -1,2), $::form->round_amount($self->amount,2));
+  };
+
+  return 1;
+};
+
+sub recalculate_amounts {
+  my ($self, %params) = @_;
+  # calculate and set amount and netamount from acc_trans objects
+
+  croak ("Can only recalculate amounts for ar transactions") if $self->invoice;
+
+  return undef unless $self->has_loaded_related('transactions');
+
+  my ($netamount, $taxamount);
+
+  my @transactions = @{$self->transactions};
+
+  foreach my $acc ( @transactions ) {
+    $netamount += $acc->amount if $acc->chart->link =~ /AR_amount/;
+    $taxamount += $acc->amount if $acc->chart->link =~ /AR_tax/;
+  };
+
+  $self->amount($netamount+$taxamount);
+  $self->netamount($netamount);
+};
+
+
 sub _post_create_assemblyitem_entries {
   my ($self, $assembly_entries) = @_;
 
@@ -321,7 +539,11 @@ sub invoice_type {
   my ($self) = @_;
 
   return 'ar_transaction'     if !$self->invoice;
-  return 'credit_note'        if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
+  return 'invoice_for_advance_payment_storno' if $self->type eq 'invoice_for_advance_payment' && $self->amount < 0 &&  $self->storno;
+  return 'invoice_for_advance_payment'        if $self->type eq 'invoice_for_advance_payment';
+  return 'final_invoice'                      if $self->type eq 'final_invoice';
+  # stornoed credit_notes are still credit notes and not invoices
+  return 'credit_note'        if $self->type eq 'credit_note' && $self->amount < 0;
   return 'invoice_storno'     if $self->type ne 'credit_note' && $self->amount < 0 &&  $self->storno;
   return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 &&  $self->storno;
   return 'invoice';
@@ -340,6 +562,9 @@ sub displayable_type {
   return t8('Credit Note')                            if $self->invoice_type eq 'credit_note';
   return t8('Invoice') . "(" . t8('Storno') . ")"     if $self->invoice_type eq 'invoice_storno';
   return t8('Credit Note') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'credit_note_storno';
+  return t8('Invoice for Advance Payment')            if $self->invoice_type eq 'invoice_for_advance_payment';
+  return t8('Invoice for Advance Payment') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'invoice_for_advance_payment_storno';
+  return t8('Final Invoice')                          if $self->invoice_type eq 'final_invoice';
   return t8('Invoice');
 }
 
@@ -354,9 +579,19 @@ sub abbreviation {
   return t8('Credit note (one letter abbreviation)') if $self->invoice_type eq 'credit_note';
   return t8('Invoice (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'invoice_storno';
   return t8('Credit note (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")"  if $self->invoice_type eq 'credit_note_storno';
+  return t8('Invoice for Advance Payment (one letter abbreviation)')  if $self->invoice_type eq 'invoice_for_advance_payment';
+  return t8('Invoice for Advance Payment with Storno (abbreviation)') if $self->invoice_type eq 'invoice_for_advance_payment_storno';
+  return t8('Final Invoice (one letter abbreviation)')                if $self->invoice_type eq 'final_invoice';
   return t8('Invoice (one letter abbreviation)');
 }
 
+sub oneline_summary {
+  my $self = shift;
+
+  return sprintf("%s: %s %s %s (%s)", $self->abbreviation, $self->invnumber, $self->customer->name,
+                                      $::form->format_amount(\%::myconfig, $self->amount,2), $self->transdate->to_kivitendo);
+}
+
 sub date {
   goto &transdate;
 }
@@ -373,18 +608,32 @@ sub link {
   my ($self) = @_;
 
   my $html;
-  $html   = SL::Presenter->get->sales_invoice($self, display => 'inline') if $self->invoice;
-  $html   = SL::Presenter->get->ar_transaction($self, display => 'inline') if !$self->invoice;
+  $html   = $self->presenter->sales_invoice(display => 'inline') if $self->invoice;
+  $html   = $self->presenter->ar_transaction(display => 'inline') if !$self->invoice;
 
   return $html;
 }
 
+sub mark_as_paid {
+  my ($self) = @_;
+
+  $self->update_attributes(paid => $self->amount);
+}
+
+sub effective_tax_point {
+  my ($self) = @_;
+
+  return $self->tax_point || $self->deliverydate || $self->transdate;
+}
+
 1;
 
 __END__
 
 =pod
 
+=encoding UTF-8
+
 =head1 NAME
 
 SL::DB::Invoice: Rose model for invoices (table "ar")
@@ -456,7 +705,7 @@ Posts the invoice. Required parameters are:
 
 =item * C<ar_id>
 
-The ID of the accounds receivable chart the invoices amounts are
+The ID of the accounts receivable chart the invoice's amounts are
 posted to. If it is not set then the first chart configured for
 accounts receivables is used.
 
@@ -482,7 +731,7 @@ the part's buchungsgruppen.
 and recorded in C<acc_trans>.
 
 =item 6. Items in C<invoice> are updated according to their allocation
-status (regarding for costs of goold sold). Will only be done if
+status (regarding costs of goods sold). Will only be done if
 kivitendo is not configured to use Einnahmenüberschussrechnungen.
 
 =item 7. The invoice and its items are saved.
@@ -491,16 +740,83 @@ kivitendo is not configured to use Einnahmenüberschussrechnungen.
 
 Returns C<$self> on success and C<undef> on failure. The whole process
 is run inside a transaction. If it fails then nothing is saved to or
-changed in the database. A new transaction is only started if none is
+changed in the database. A new transaction is only started if none are
 active.
 
 =item C<basic_info $field>
 
 See L<SL::DB::Object::basic_info>.
 
+=item C<closed>
+
+Returns 1 or 0, depending on whether the invoice is closed or not. Currently
+invoices that are overpaid also count as closed and credit notes in general.
+
+=item C<recalculate_amounts %params>
+
+Calculate and set amount and netamount from acc_trans objects by summing up the
+values of acc_trans objects with AR_amount and AR_tax link charts.
+amount and netamount are set to the calculated values.
+
+=item C<validate_acc_trans>
+
+Checks if the sum of all associated acc_trans objects is 0 and checks whether
+the amount of the AR acc_transaction matches the AR amount. Only the first AR
+line is checked, because the sum of all AR lines is 0 for paid invoices.
+
+Returns 0 or 1.
+
+Can be called with a debug parameter which writes debug info to STDOUT, which is
+useful in console mode or while writing tests.
+
+ my $ar = SL::DB::Manager::Invoice->get_first();
+ $ar->validate_acc_trans(debug => 1);
+
+=item C<create_ar_row %params>
+
+Creates a new acc_trans entry for the receivable (AR) entry of an existing AR
+invoice object, which already has some income and tax acc_trans entries.
+
+The acc_trans entry is also returned inside an array ref.
+
+Mandatory params are
+
+=over 2
+
+=item * chart as an RDBO object, e.g. for bank. Must be a 'paid' chart.
+
+=back
+
+Currently the amount of the invoice object is used for the acc_trans amount.
+Use C<recalculate_amounts> before calling this method if amount isn't known
+yet or you didn't set it manually.
+
+=item C<add_ar_amount_row %params>
+
+Add a new entry for an existing AR invoice object. Creates an acc_trans entry,
+and also adds an acc_trans tax entry, if the tax has an associated tax chart.
+Also all acc_trans entries that were created are returned inside an array ref.
+
+Mandatory params are
+
+=over 2
+
+=item * chart as an RDBO object, should be an income chart (link = AR_amount)
+
+=item * tax_id
+
+=item * amount
+
+=back
+
+=item C<mark_as_paid>
+
+Marks the invoice as paid by setting its C<paid> member to the value of C<amount>.
+
 =back
 
 =head1 TODO
+
  As explained in the new_from example, it is possible to set transdate to a new value.
  From a user / programm point of view transdate is more than holy and there should be
  some validity checker available for controller code. At least the same logic like in
index 08657bd..0753d67 100644 (file)
@@ -1,15 +1,55 @@
-# This file has been auto-generated only because it didn't exist.
-# Feel free to modify it at will; it will not be overwritten automatically.
-
 package SL::DB::Letter;
 
 use strict;
 
+use SL::DB::Helper::AttrHTML;
+use SL::DB::Helper::LinkedRecords;
 use SL::DB::MetaSetup::Letter;
+use SL::DB::Manager::Letter;
 
 __PACKAGE__->meta->initialize;
 
-# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
-__PACKAGE__->meta->make_manager_class;
+__PACKAGE__->attr_html('body');
+
+sub new_from_draft {
+  my ($class, $draft) = @_;
+
+  my $self = $class->new;
+
+  if (!ref $draft) {
+    require SL::DB::LetterDraft;
+    $draft = SL::DB::LetterDraft->new(id => $draft)->load;
+  }
+
+  $self->assign_attributes(map { $_ => $draft->$_ } $draft->meta->columns);
+
+  $self->id(undef);
+
+  $self;
+}
+
+sub is_sales {
+  die 'not an accessor' if @_ > 1;
+  $_[0]{customer_id} * 1;
+}
+
+sub has_customer_vendor {
+  my ($self) = @_;
+  die 'not an accessor' if @_ > 1;
+
+  return $self->is_sales
+    ? ($self->customer_id && $self->customer)
+    : ($self->vendor_id   && $self->vendor);
+}
+
+sub customer_vendor {
+  die 'not an accessor' if @_ > 1;
+  $_[0]->is_sales ? $_[0]->customer : $_[0]->vendor;
+}
+
+sub customer_vendor_id {
+  die 'not an accessor' if @_ > 1;
+  $_[0]->customer_id || $_[0]->vendor_id;
+}
 
 1;
index eb4a4b6..089755b 100644 (file)
@@ -1,15 +1,32 @@
-# This file has been auto-generated only because it didn't exist.
-# Feel free to modify it at will; it will not be overwritten automatically.
-
 package SL::DB::LetterDraft;
 
 use strict;
 
+use SL::DB::Helper::AttrHTML;
 use SL::DB::MetaSetup::LetterDraft;
 
 __PACKAGE__->meta->initialize;
 
+__PACKAGE__->attr_html('body');
+
 # Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
 __PACKAGE__->meta->make_manager_class;
 
+sub new_from_letter {
+  my ($class, $letter) = @_;
+
+  my $self = $class->new;
+
+  if (!ref $letter) {
+    require SL::DB::Draft;
+    $letter = SL::DB::Draft->new(id => $letter)->load;
+  }
+
+  $self->assign_attributes(map { $_ => $letter->$_ } $letter->meta->columns);
+
+  $self->id(undef);
+
+  $self;
+}
+
 1;
index f94cd0a..fdc50bc 100644 (file)
@@ -6,11 +6,9 @@ package SL::DB::MakeModel;
 use strict;
 
 use SL::DB::MetaSetup::MakeModel;
+use SL::DB::Manager::MakeModel;
 use SL::DB::Helper::ActsAsList (column_name => 'sortorder', group_by => [ qw(parts_id) ]);
 
 __PACKAGE__->meta->initialize;
 
-# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
-__PACKAGE__->meta->make_manager_class;
-
 1;
diff --git a/SL/DB/Manager/AdditionalBillingAddress.pm b/SL/DB/Manager/AdditionalBillingAddress.pm
new file mode 100644 (file)
index 0000000..297d534
--- /dev/null
@@ -0,0 +1,14 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::AdditionalBillingAddress;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::AdditionalBillingAddress' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/Manager/ApGl.pm b/SL/DB/Manager/ApGl.pm
new file mode 100644 (file)
index 0000000..3cc0396
--- /dev/null
@@ -0,0 +1,14 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::ApGl;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::ApGl' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/Manager/AssortmentItem.pm b/SL/DB/Manager/AssortmentItem.pm
new file mode 100644 (file)
index 0000000..1b0cb60
--- /dev/null
@@ -0,0 +1,14 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::AssortmentItem;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::AssortmentItem' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/Manager/AuthMasterRight.pm b/SL/DB/Manager/AuthMasterRight.pm
new file mode 100644 (file)
index 0000000..fe86068
--- /dev/null
@@ -0,0 +1,14 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::AuthMasterRight;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::AuthMasterRight' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/Manager/AuthSchemaInfo.pm b/SL/DB/Manager/AuthSchemaInfo.pm
new file mode 100644 (file)
index 0000000..3e72f85
--- /dev/null
@@ -0,0 +1,14 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::AuthSchemaInfo;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::AuthSchemaInfo' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/Manager/AuthSession.pm b/SL/DB/Manager/AuthSession.pm
new file mode 100644 (file)
index 0000000..9289ad8
--- /dev/null
@@ -0,0 +1,14 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::AuthSession;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::AuthSession' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/Manager/AuthSessionContent.pm b/SL/DB/Manager/AuthSessionContent.pm
new file mode 100644 (file)
index 0000000..60c0a2d
--- /dev/null
@@ -0,0 +1,14 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::AuthSessionContent;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::AuthSessionContent' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
index 6212f94..8b2e99a 100644 (file)
@@ -8,6 +8,8 @@ use base qw(SL::DB::Helper::Manager);
 use SL::DB::Helper::Paginated;
 use SL::DB::Helper::Sorted;
 
+use SL::System::TaskServer;
+
 sub object_class { 'SL::DB::BackgroundJob' }
 
 __PACKAGE__->make_manager_methods;
@@ -36,8 +38,21 @@ sub get_all_need_to_run {
                                                  cron_spec   => '',
                                                  next_run_at => undef,
                                                  next_run_at => { le => $now } ] ]);
-
-  return $class->get_all(query => [ or => [ @interval_args, @once_args ] ]);
+  my @node_filter;
+
+  my $node_id = SL::System::TaskServer->node_id;
+  if ($::lx_office_conf{task_server}->{only_run_tasks_for_this_node}) {
+    @node_filter = (node_id => $node_id);
+  } else {
+    @node_filter = (
+      or => [
+        node_id => undef,
+        node_id => '',
+        node_id => $node_id,
+      ]);
+  }
+
+  return $class->get_all(query => [ or => [ @interval_args, @once_args ], @node_filter ]);
 }
 
 1;
diff --git a/SL/DB/Manager/BankTransactionAccTrans.pm b/SL/DB/Manager/BankTransactionAccTrans.pm
new file mode 100644 (file)
index 0000000..6e05779
--- /dev/null
@@ -0,0 +1,14 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::BankTransactionAccTrans;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::BankTransactionAccTrans' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
index 4d11960..d70e7f9 100644 (file)
@@ -135,7 +135,7 @@ sub cache_taxkeys {
   my $rows = selectall_hashref_query($::form, $::form->get_standard_dbh, <<"", $date);
     SELECT DISTINCT ON (chart_id) chart_id, startdate, id
     FROM taxkeys
-    WHERE startdate < ?
+    WHERE startdate <= ?
     ORDER BY chart_id, startdate DESC;
 
   for (@$rows) {
diff --git a/SL/DB/Manager/ContactDepartment.pm b/SL/DB/Manager/ContactDepartment.pm
new file mode 100644 (file)
index 0000000..ed1b1b4
--- /dev/null
@@ -0,0 +1,23 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::ContactDepartment;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::ContactDepartment' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return ( default => [ 'description', 1 ],
+           columns => { SIMPLE => 'ALL',
+                        map { ( $_ => "lower(contact_departments.$_)" ) } qw(description)
+                      });
+}
+
+1;
diff --git a/SL/DB/Manager/ContactTitle.pm b/SL/DB/Manager/ContactTitle.pm
new file mode 100644 (file)
index 0000000..dea095d
--- /dev/null
@@ -0,0 +1,23 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::ContactTitle;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::ContactTitle' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return ( default => [ 'description', 1 ],
+           columns => { SIMPLE => 'ALL',
+                        map { ( $_ => "lower(contact_titles.$_)" ) } qw(description)
+                      });
+}
+
+1;
index 0a8fe2a..0868767 100644 (file)
@@ -19,18 +19,6 @@ sub cleanup {
   ]);
 
   $_->destroy for @$objects;
-
-  # get reports for the active session that aren't the latest
-  $objects = $self->get_all(
-    query => [ session_id => $::auth->get_session_id, ],
-    order_by => [ 'id' ],
-  );
-
-  # skip the last one
-  for (0 .. $#$objects - 1) {
-    $objects->[$_]->destroy;
-  }
 }
 
 1;
-
diff --git a/SL/DB/Manager/CustomDataExportQuery.pm b/SL/DB/Manager/CustomDataExportQuery.pm
new file mode 100644 (file)
index 0000000..cc74ccc
--- /dev/null
@@ -0,0 +1,19 @@
+package SL::DB::Manager::CustomDataExportQuery;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::CustomDataExportQuery' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return ( default => [ 'name', 1 ],
+           name    => 'lower(custom_data_export_queries.name)',
+           columns => { SIMPLE => 'ALL' });
+}
+
+1;
diff --git a/SL/DB/Manager/CustomDataExportQueryParameter.pm b/SL/DB/Manager/CustomDataExportQueryParameter.pm
new file mode 100644 (file)
index 0000000..c230648
--- /dev/null
@@ -0,0 +1,14 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::CustomDataExportQueryParameter;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::CustomDataExportQueryParameter' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
index 30c32a9..4a1d8b0 100644 (file)
@@ -22,7 +22,7 @@ __PACKAGE__->add_filter_specs(
 sub _sort_spec {
   return ( default => [ 'name', 1 ],
            columns => { SIMPLE => 'ALL',
-                        map { ( $_ => "lower(customer.$_)" ) } qw(customernumber vendornumber name contact phone fax email street taxnumber business invnumber ordnumber quonumber)
+                        map { ( $_ => "lower(customer.$_)" ) } qw(customernumber vendornumber name contact phone fax email street gln taxnumber business invnumber ordnumber quonumber)
                       });
 }
 
index 793e88b..e9c1ee7 100644 (file)
@@ -6,19 +6,31 @@ use parent qw(SL::DB::Helper::Manager);
 
 use SL::DB::Helper::Paginated;
 use SL::DB::Helper::Sorted;
+use SL::DB::Helper::Filtered;
+
+use SL::DB::DeliveryOrder::TypeData qw(validate_type);
 
 sub object_class { 'SL::DB::DeliveryOrder' }
 
 __PACKAGE__->make_manager_methods;
 
+__PACKAGE__->add_filter_specs(
+  type => sub {
+    my ($key, $value, $prefix) = @_;
+    return __PACKAGE__->type_filter($value, $prefix);
+  },
+  all => sub {
+    my ($key, $value, $prefix) = @_;
+    return or => [ map { $prefix . $_ => $value } qw(donumber customer.name vendor.name transaction_description orderitems.serialnumber) ]
+  }
+);
+
 sub type_filter {
   my $class = shift;
   my $type  = lc(shift || '');
+  my $prefix = shift // '';
 
-  return ('!customer_id' => undef) if $type eq 'sales_delivery_order';
-  return ('!vendor_id'   => undef) if $type eq 'purchase_delivery_order';
-
-  die "Unknown type $type";
+  return "${prefix}order_type" => validate_type($type);
 }
 
 sub _sort_spec {
index a8c690a..400843b 100644 (file)
@@ -22,7 +22,7 @@ sub _sort_spec {
 
 sub current {
   return undef unless $::myconfig{login};
-  return shift->find_by(login => $::myconfig{login});
+  return $::request->cache('current')->{object} //= shift->find_by(login => $::myconfig{login});
 }
 
 sub update_entries_for_authorized_users {
@@ -31,6 +31,8 @@ sub update_entries_for_authorized_users {
   my %employees_by_login = map { ($_->login => $_) } @{ $class->get_all };
 
   require SL::DB::AuthClient;
+  no warnings 'once';
+
   foreach my $user (@{ SL::DB::AuthClient->new(id => $::auth->client->{id})->load->users || [] }) {
     my $user_config = $user->config_values;
     my $employee    = $employees_by_login{$user->login} || SL::DB::Employee->new(login => $user->login);
diff --git a/SL/DB/Manager/EmployeeProjectInvoices.pm b/SL/DB/Manager/EmployeeProjectInvoices.pm
new file mode 100644 (file)
index 0000000..a176ef4
--- /dev/null
@@ -0,0 +1,11 @@
+package SL::DB::Manager::EmployeeProjectInvoices;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::EmployeeProjectInvoices' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/Manager/File.pm b/SL/DB/Manager/File.pm
new file mode 100644 (file)
index 0000000..3f6a06c
--- /dev/null
@@ -0,0 +1,15 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::File;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::File' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/Manager/FileFullText.pm b/SL/DB/Manager/FileFullText.pm
new file mode 100644 (file)
index 0000000..bb1267d
--- /dev/null
@@ -0,0 +1,14 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::FileFullText;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::FileFullText' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/Manager/Greeting.pm b/SL/DB/Manager/Greeting.pm
new file mode 100644 (file)
index 0000000..0a8de83
--- /dev/null
@@ -0,0 +1,23 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::Greeting;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::Greeting' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return ( default => [ 'description', 1 ],
+           columns => { SIMPLE => 'ALL',
+                        map { ( $_ => "lower(greetings.$_)" ) } qw(description)
+                      });
+}
+
+1;
diff --git a/SL/DB/Manager/Inventory.pm b/SL/DB/Manager/Inventory.pm
new file mode 100644 (file)
index 0000000..030a386
--- /dev/null
@@ -0,0 +1,28 @@
+package SL::DB::Manager::Inventory;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Filtered;
+use SL::DB::Helper::Paginated;
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::Inventory' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return (
+    default        => [ 'itime', 1 ],
+    columns        => {
+      SIMPLE       => 'ALL',
+    });
+}
+
+sub default_objects_per_page {
+  20;
+}
+
+1;
diff --git a/SL/DB/Manager/Letter.pm b/SL/DB/Manager/Letter.pm
new file mode 100644 (file)
index 0000000..a412cd8
--- /dev/null
@@ -0,0 +1,41 @@
+package SL::DB::Manager::Letter;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Filtered;
+use SL::DB::Helper::Paginated;
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::Letter' }
+
+__PACKAGE__->make_manager_methods;
+__PACKAGE__->add_filter_specs(
+  is_sales => sub {
+    my ($key, $value, $prefix) = @_;
+    __PACKAGE__->is_sales_filter($value, $prefix);
+  },
+);
+
+sub is_sales_filter {
+  my ($class, $value, $prefix) = @_;
+
+  return () if !defined $value;
+  return ($prefix . 'customer_id' => { gt => 0 }) if $value;
+  return ($prefix . 'vendor_id'   => { gt => 0 }) if !$value;
+}
+
+sub _sort_spec {
+  return ( columns => { SIMPLE    => 'ALL',
+                        customer  => [ 'lower(customer.name)', ],
+                      },
+           default => [ 'date', 0 ],
+           nulls   => { }
+         );
+}
+
+sub default_objects_per_page { 30 }
+
+1;
diff --git a/SL/DB/Manager/MakeModel.pm b/SL/DB/Manager/MakeModel.pm
new file mode 100644 (file)
index 0000000..2c6f603
--- /dev/null
@@ -0,0 +1,30 @@
+package SL::DB::Manager::MakeModel;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use SL::DB::Helper::Sorted;
+use SL::DB::Helper::Paginated;
+use SL::DB::Helper::Filtered;
+use base qw(SL::DB::Helper::Manager);
+
+use Carp;
+use SL::DBUtils;
+use SL::MoreCommon qw(listify);
+
+sub object_class { 'SL::DB::MakeModel' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  (
+    default  => [ 'sortorder', 1 ],
+    columns  => {
+      SIMPLE => 'ALL',
+    },
+    nulls    => {},
+  );
+}
+
+1;
+__END__
diff --git a/SL/DB/Manager/MebilMapping.pm b/SL/DB/Manager/MebilMapping.pm
new file mode 100644 (file)
index 0000000..6ccaa08
--- /dev/null
@@ -0,0 +1,14 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::MebilMapping;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::MebilMapping' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
index d844683..ff5169b 100644 (file)
@@ -6,11 +6,23 @@ use parent qw(SL::DB::Helper::Manager);
 
 use SL::DB::Helper::Paginated;
 use SL::DB::Helper::Sorted;
+use SL::DB::Helper::Filtered;
 
 sub object_class { 'SL::DB::Order' }
 
 __PACKAGE__->make_manager_methods;
 
+__PACKAGE__->add_filter_specs(
+  type => sub {
+    my ($key, $value, $prefix) = @_;
+    return __PACKAGE__->type_filter($value, $prefix);
+  },
+  all => sub {
+    my ($key, $value, $prefix) = @_;
+    return or => [ map { $prefix . $_ => $value } qw(ordnumber quonumber customer.name vendor.name transaction_description) ]
+  }
+);
+
 sub type_filter {
   my $class  = shift;
   my $type   = lc(shift || '');
index ce673db..379e1fd 100644 (file)
@@ -34,11 +34,11 @@ sub _sort_spec {
                         qty           => [ 'qty'                  ],
                         ordnumber     => [ 'order.ordnumber'      ],
                         customer      => [ 'lower(customer.name)', ],
-                        position      => [ 'trans_id', 'runningnumber' ],
+                        position      => [ 'trans_id', 'position' ],
                         reqdate       => [ 'COALESCE(orderitems.reqdate, order.reqdate)' ],
                         orddate       => [ 'order.orddate' ],
-                        sellprice     => [ 'sellprice' ],
-                        discount      => [ 'discount' ],
+                        sellprice     => [ 'orderitems.sellprice' ],
+                        discount      => [ 'orderitems.discount' ],
                         transdate     => [ 'orderitems.transdate::date', 'order.reqdate' ],
                       },
            default => [ 'position', 1 ],
index 0b751bd..b46f851 100644 (file)
@@ -16,14 +16,24 @@ sub object_class { 'SL::DB::Part' }
 
 __PACKAGE__->make_manager_methods;
 __PACKAGE__->add_filter_specs(
-  type => sub {
+  part_type => sub {
     my ($key, $value, $prefix) = @_;
     return __PACKAGE__->type_filter($value, $prefix);
   },
   all => sub {
     my ($key, $value, $prefix) = @_;
-    return or => [ map { $prefix . $_ => $value } qw(partnumber description) ]
-  }
+    return or => [ map { $prefix . $_ => $value } qw(partnumber description ean) ]
+  },
+  all_with_makemodel => sub {
+    my ($key, $value, $prefix) = @_;
+    return or => [ map { $prefix . $_ => $value } qw(partnumber description ean makemodels.model) ],
+      $prefix . 'makemodels';
+  },
+  all_with_customer_partnumber => sub {
+    my ($key, $value, $prefix) = @_;
+    return or => [ map { $prefix . $_ => $value } qw(partnumber description ean customerprices.customer_partnumber) ],
+      $prefix . 'customerprices';
+  },
 );
 
 sub type_filter {
@@ -33,7 +43,7 @@ sub type_filter {
 
   $prefix //= '';
 
-  # this is to make selection like type => { part => 1, service => 1 } work
+  # this is to make selections like part_type => { part => 1, service => 1 } work
   if ('HASH' eq ref $type) {
     $type = [ grep { $type->{$_} } keys %$type ];
   }
@@ -43,16 +53,13 @@ sub type_filter {
 
   for my $type (@types) {
     if ($type =~ m/^part/) {
-      push @filter, (and => [ or                             => [ $prefix . assembly => 0, $prefix . assembly => undef ],
-                              "!${prefix}inventory_accno_id" => 0,
-                              "!${prefix}inventory_accno_id" => undef,
-                     ]);
+      push @filter, ($prefix . part_type => 'part');
     } elsif ($type =~ m/^service/) {
-      push @filter, (and => [ or => [ $prefix . assembly           => 0, $prefix . assembly           => undef ],
-                              or => [ $prefix . inventory_accno_id => 0, $prefix . inventory_accno_id => undef ],
-                     ]);
-    } elsif ($type =~ m/^assembl/) {
-      push @filter, ($prefix . assembly => 1);
+      push @filter, ($prefix . part_type => 'service');
+    } elsif ($type =~ m/^assembly/) {
+      push @filter, ($prefix . part_type => 'assembly');
+    } elsif ($type =~ m/^assortment/) {
+      push @filter, ($prefix . part_type => 'assortment');
     }
   }
 
diff --git a/SL/DB/Manager/PartClassification.pm b/SL/DB/Manager/PartClassification.pm
new file mode 100644 (file)
index 0000000..c00bea7
--- /dev/null
@@ -0,0 +1,23 @@
+package SL::DB::Manager::PartClassification;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::PartClassification' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
+
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::DB::Manager::PartClassification
+
+=cut
diff --git a/SL/DB/Manager/PartCustomerPrice.pm b/SL/DB/Manager/PartCustomerPrice.pm
new file mode 100644 (file)
index 0000000..4f93ddc
--- /dev/null
@@ -0,0 +1,15 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::PartCustomerPrice;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::PartCustomerPrice' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/Manager/PartsGroup.pm b/SL/DB/Manager/PartsGroup.pm
new file mode 100644 (file)
index 0000000..9b08133
--- /dev/null
@@ -0,0 +1,19 @@
+package SL::DB::Manager::PartsGroup;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::PartsGroup' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return ( default => [ 'sortkey', 1 ],
+           columns => { SIMPLE => 'ALL' });
+}
+
+1;
diff --git a/SL/DB/Manager/PartsPriceHistory.pm b/SL/DB/Manager/PartsPriceHistory.pm
new file mode 100644 (file)
index 0000000..6cc14ca
--- /dev/null
@@ -0,0 +1,23 @@
+package SL::DB::Manager::PartsPriceHistory;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Sorted;
+use SL::DB::Helper::Paginated;
+
+sub object_class { 'SL::DB::PartsPriceHistory' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  (
+    default  => [ 'valid_from', 0 ],
+    columns  => {
+      SIMPLE => 'ALL',
+    },
+  );
+}
+
+1;
index abe6837..895410a 100644 (file)
@@ -14,7 +14,7 @@ __PACKAGE__->make_manager_methods;
 sub _sort_spec {
   return ( default => [ 'sortkey', 1 ],
            columns => { SIMPLE => 'ALL',
-                        map { ( $_ => "lower(payment_terms.${_})" ) } qw(description description_long),
+                        map { ( $_ => "lower(payment_terms.${_})" ) } qw(description description_long description_long_invoice),
                       });
 }
 
diff --git a/SL/DB/Manager/PriceFactor.pm b/SL/DB/Manager/PriceFactor.pm
new file mode 100644 (file)
index 0000000..84030c8
--- /dev/null
@@ -0,0 +1,20 @@
+package SL::DB::Manager::PriceFactor;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::PriceFactor' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return ( default => [ 'sortkey', 1 ],
+           columns => { SIMPLE => 'ALL' });
+}
+
+1;
+
index d5b5489..974011a 100644 (file)
@@ -73,8 +73,7 @@ sub get_all_matching {
   my ($self, %params) = @_;
 
   my ($query, @values) = $self->get_matching_filter(%params);
-  my @ids = selectall_ids($::form, $::form->get_standard_dbh, $query, 0, @values);
-
+  my @ids = selectcol_array_query($::form, SL::DB->client->dbh, $query, @values);
   return [] unless @ids;
 
   $self->get_all(query => [ id => \@ids ]);
index 83a4bf1..ba3694c 100644 (file)
@@ -31,7 +31,7 @@ my %types = (
   'transdate'           => { description => t8('Transdate'),          customer => 1, vendor => 1, data_type => 'date', data => sub { $_[0]->transdate }, ops => 'date' },
   'part'                => { description => t8('Part'),               customer => 1, vendor => 1, data_type => 'int',  data => sub { $_[1]->part->id }, },
   'pricegroup'          => { description => t8('Pricegroup'),         customer => 1, vendor => 1, data_type => 'int',  data => sub { $_[1]->pricegroup_id }, exclude_nulls => 1 },
-  'partsgroup'          => { description => t8('Group'),              customer => 1, vendor => 1, data_type => 'int',  data => sub { $_[1]->part->partsgroup_id }, exclude_nulls => 1 },
+  'partsgroup'          => { description => t8('Partsgroup'),         customer => 1, vendor => 1, data_type => 'int',  data => sub { $_[1]->part->partsgroup_id }, exclude_nulls => 1 },
   'qty'                 => { description => t8('Qty'),                customer => 1, vendor => 1, data_type => 'num',  data => sub { $_[1]->qty }, ops => 'num' },
 );
 
@@ -65,7 +65,7 @@ sub not_matching_sql_and_values {
         push @values, $value;
       }
 
-      push @tokens, "type = '$type' AND " . join ' OR ', map "($_)", @sub_tokens;
+      push @tokens, "type = '$type' AND (@{[ join(' OR ', map qq|($_)|, @sub_tokens) ]})";
     }
   }
 
index 6da4f21..0e65609 100644 (file)
@@ -12,10 +12,8 @@ sub object_class { 'SL::DB::Pricegroup' }
 __PACKAGE__->make_manager_methods;
 
 sub _sort_spec {
-  return ( default => [ 'pricegroup', 1 ],
-           columns => { SIMPLE => 'ALL',
-                        map { ( $_ => "lower(pricegroup.${_})" ) } qw(pricegroup),
-                      });
+  return ( default => [ 'sortkey', 1 ],
+           columns => { SIMPLE => 'ALL' });
 }
 
 1;
index 8f63a1c..76dc126 100644 (file)
@@ -21,7 +21,7 @@ sub get_new_rec_group {
 
   my ($max) = selectfirst_array_query($::form, $class->object_class->init_db->dbh, $query);
 
-  return $max + 1;
+  return ($max // 0) + 1;
 }
 
 1;
diff --git a/SL/DB/Manager/RecordTemplate.pm b/SL/DB/Manager/RecordTemplate.pm
new file mode 100644 (file)
index 0000000..48c7827
--- /dev/null
@@ -0,0 +1,24 @@
+package SL::DB::Manager::RecordTemplate;
+
+use strict;
+
+use base qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Paginated;
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::RecordTemplate' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return (
+    default => [ 'template_name', 1 ],
+    columns => {
+      SIMPLE        => 'ALL',
+      template_name => 'lower(template_name)',
+    },
+  );
+}
+
+1;
diff --git a/SL/DB/Manager/RecordTemplateItem.pm b/SL/DB/Manager/RecordTemplateItem.pm
new file mode 100644 (file)
index 0000000..1c3ba92
--- /dev/null
@@ -0,0 +1,11 @@
+package SL::DB::Manager::RecordTemplateItem;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::RecordTemplateItem' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/Manager/Shop.pm b/SL/DB/Manager/Shop.pm
new file mode 100644 (file)
index 0000000..b7a1958
--- /dev/null
@@ -0,0 +1,28 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::Shop;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::Shop' }
+
+use SL::DB::Helper::Sorted;
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return ( default => [ 'sortkey', 1 ],
+           columns => { SIMPLE => 'ALL' } );
+}
+
+sub get_default {
+    return $_[0]->get_first(where => [ obsolete => 0 ], sort_by => 'sortkey');
+}
+
+1;
+
+1;
diff --git a/SL/DB/Manager/ShopImage.pm b/SL/DB/Manager/ShopImage.pm
new file mode 100644 (file)
index 0000000..5d597d9
--- /dev/null
@@ -0,0 +1,15 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::ShopImage;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::ShopImage' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/Manager/ShopOrder.pm b/SL/DB/Manager/ShopOrder.pm
new file mode 100644 (file)
index 0000000..3311f1b
--- /dev/null
@@ -0,0 +1,15 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::ShopOrder;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::ShopOrder' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/Manager/ShopOrderItem.pm b/SL/DB/Manager/ShopOrderItem.pm
new file mode 100644 (file)
index 0000000..3a279d6
--- /dev/null
@@ -0,0 +1,15 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::ShopOrderItem;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::ShopOrderItem' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/Manager/ShopPart.pm b/SL/DB/Manager/ShopPart.pm
new file mode 100644 (file)
index 0000000..696fdbe
--- /dev/null
@@ -0,0 +1,16 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::ShopPart;
+#package SL::DB::Manager::ShopPart;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::ShopPart' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
diff --git a/SL/DB/Manager/Stocktaking.pm b/SL/DB/Manager/Stocktaking.pm
new file mode 100644 (file)
index 0000000..3481343
--- /dev/null
@@ -0,0 +1,34 @@
+package SL::DB::Manager::Stocktaking;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Filtered;
+use SL::DB::Helper::Paginated;
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::Stocktaking' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return (
+    default        => [ 'itime', 1 ],
+    columns        => {
+      SIMPLE       => 'ALL',
+      comment      => 'lower(comment)',
+      chargenumber => 'lower(chargenumber)',
+      employee     => 'lower(employee.name)',
+      ean          => 'lower(parts.ean)',
+      partnumber   => 'lower(parts.partnumber)',
+      part         => 'lower(parts.description)',
+      bin          => ['lower(warehouse.description)', 'lower(bin.description)'],
+    });
+}
+
+sub default_objects_per_page {
+  20;
+}
+
+1;
diff --git a/SL/DB/Manager/TimeRecording.pm b/SL/DB/Manager/TimeRecording.pm
new file mode 100644 (file)
index 0000000..2a8677d
--- /dev/null
@@ -0,0 +1,37 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::TimeRecording;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::TimeRecording' }
+
+__PACKAGE__->make_manager_methods;
+
+
+sub _sort_spec {
+  return ( default => [ 'start_time', 1 ],
+           nulls   => {
+             date       => 'FIRST',
+             start_time => 'FIRST',
+             end_time   => 'FIRST',
+           },
+           columns => { SIMPLE       => 'ALL' ,
+                        start_time   => [ 'date', 'start_time' ],
+                        end_time     => [ 'date', 'end_time' ],
+                        customer     => [ 'lower(customer.name)', 'date','start_time'],
+                        staff_member => [ 'lower(staff_member.name)', 'date','start_time'],
+                        order        => [ 'order.ordnumber', 'date','start_time'],
+                        part         => [ 'lower(part.partnumber)', 'date','start_time'],
+                        project      => [ 'lower(project.projectnumber)', 'date','start_time'],
+           }
+  );
+}
+
+
+1;
diff --git a/SL/DB/Manager/TimeRecordingArticle.pm b/SL/DB/Manager/TimeRecordingArticle.pm
new file mode 100644 (file)
index 0000000..9048b61
--- /dev/null
@@ -0,0 +1,21 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::TimeRecordingArticle;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::TimeRecordingArticle' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return ( default => [ 'position', 1 ],
+           columns => { SIMPLE => 'ALL' });
+}
+
+1;
diff --git a/SL/DB/Manager/UserPreference.pm b/SL/DB/Manager/UserPreference.pm
new file mode 100644 (file)
index 0000000..3ad81e5
--- /dev/null
@@ -0,0 +1,14 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::UserPreference;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::UserPreference' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
index af4b029..443a41e 100644 (file)
@@ -24,7 +24,7 @@ sub _sort_spec {
     default => [ 'name', 1 ],
     columns => {
       SIMPLE => 'ALL',
-      map { ( $_ => "lower(vendor.$_)" ) } qw(account_number bank bank_code bcc bic cc city contact country department_1 department_2 depositor email fax greeting homepage iban language
+      map { ( $_ => "lower(vendor.$_)" ) } qw(account_number bank bank_code bcc bic cc city contact country department_1 department_2 depositor email fax gln greeting homepage iban language
                                               name notes phone street taxnumber user_password username ustid v_customer_id vendornumber zipcode)
     });
 }
diff --git a/SL/DB/MebilMapping.pm b/SL/DB/MebilMapping.pm
new file mode 100644 (file)
index 0000000..785380f
--- /dev/null
@@ -0,0 +1,16 @@
+package SL::DB::MebilMapping;
+
+use strict;
+
+use SL::DB::MetaSetup::MebilMapping;
+use SL::DB::Manager::MebilMapping;
+
+__PACKAGE__->meta->initialize;
+
+sub getMappings {
+       my $dbh = shift;
+       
+       return SL::DB::Manager::MebilMapping::get_mebilmappings();
+}
+
+1;
diff --git a/SL/DB/MetaSetup/AdditionalBillingAddress.pm b/SL/DB/MetaSetup/AdditionalBillingAddress.pm
new file mode 100644 (file)
index 0000000..3e37db9
--- /dev/null
@@ -0,0 +1,43 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::AdditionalBillingAddress;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('additional_billing_addresses');
+
+__PACKAGE__->meta->columns(
+  city            => { type => 'text' },
+  contact         => { type => 'text' },
+  country         => { type => 'text' },
+  customer_id     => { type => 'integer' },
+  default_address => { type => 'boolean', default => 'false', not_null => 1 },
+  department_1    => { type => 'text' },
+  department_2    => { type => 'text' },
+  email           => { type => 'text' },
+  fax             => { type => 'text' },
+  gln             => { type => 'text' },
+  id              => { type => 'serial', not_null => 1 },
+  itime           => { type => 'timestamp', default => 'now()', not_null => 1 },
+  mtime           => { type => 'timestamp', default => 'now()', not_null => 1 },
+  name            => { type => 'text' },
+  phone           => { type => 'text' },
+  street          => { type => 'text' },
+  zipcode         => { type => 'text' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+__PACKAGE__->meta->foreign_keys(
+  customer => {
+    class       => 'SL::DB::Customer',
+    key_columns => { customer_id => 'id' },
+  },
+);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/ApGl.pm b/SL/DB/MetaSetup/ApGl.pm
new file mode 100644 (file)
index 0000000..9f3f80a
--- /dev/null
@@ -0,0 +1,35 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::ApGl;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('ap_gl');
+
+__PACKAGE__->meta->columns(
+  ap_id => { type => 'integer', not_null => 1 },
+  gl_id => { type => 'integer', not_null => 1 },
+  itime => { type => 'timestamp', default => 'now()' },
+  mtime => { type => 'timestamp' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'ap_id', 'gl_id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+__PACKAGE__->meta->foreign_keys(
+  ap => {
+    class       => 'SL::DB::PurchaseInvoice',
+    key_columns => { ap_id => 'id' },
+  },
+
+  gl => {
+    class       => 'SL::DB::GLTransaction',
+    key_columns => { gl_id => 'id' },
+  },
+);
+
+1;
+;
index 5107340..4e8fe0e 100644 (file)
@@ -11,16 +11,29 @@ __PACKAGE__->meta->table('assembly');
 __PACKAGE__->meta->columns(
   assembly_id => { type => 'serial', not_null => 1 },
   bom         => { type => 'boolean' },
-  id          => { type => 'integer' },
+  id          => { type => 'integer', not_null => 1 },
   itime       => { type => 'timestamp', default => 'now()' },
   mtime       => { type => 'timestamp' },
-  parts_id    => { type => 'integer' },
-  qty         => { type => 'float', scale => 4 },
+  parts_id    => { type => 'integer', not_null => 1 },
+  position    => { type => 'integer' },
+  qty         => { type => 'float', precision => 4, scale => 4 },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'assembly_id' ]);
 
 __PACKAGE__->meta->allow_inline_column_values(1);
 
+__PACKAGE__->meta->foreign_keys(
+  assembly_part => {
+    class       => 'SL::DB::Part',
+    key_columns => { id => 'id' },
+  },
+
+  part => {
+    class       => 'SL::DB::Part',
+    key_columns => { parts_id => 'id' },
+  },
+);
+
 1;
 ;
diff --git a/SL/DB/MetaSetup/AssortmentItem.pm b/SL/DB/MetaSetup/AssortmentItem.pm
new file mode 100644 (file)
index 0000000..8bd1611
--- /dev/null
@@ -0,0 +1,44 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::AssortmentItem;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('assortment_items');
+
+__PACKAGE__->meta->columns(
+  assortment_id => { type => 'integer', not_null => 1 },
+  charge        => { type => 'boolean', default => 'true' },
+  itime         => { type => 'timestamp', default => 'now()' },
+  mtime         => { type => 'timestamp' },
+  parts_id      => { type => 'integer', not_null => 1 },
+  position      => { type => 'integer', not_null => 1 },
+  qty           => { type => 'float', not_null => 1, precision => 4, scale => 4 },
+  unit          => { type => 'varchar', length => 20, not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'assortment_id', 'parts_id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+__PACKAGE__->meta->foreign_keys(
+  assortment => {
+    class       => 'SL::DB::Part',
+    key_columns => { assortment_id => 'id' },
+  },
+
+  part => {
+    class       => 'SL::DB::Part',
+    key_columns => { parts_id => 'id' },
+  },
+
+  unit_obj => {
+    class       => 'SL::DB::Unit',
+    key_columns => { unit => 'name' },
+  },
+);
+
+1;
+;
index 8eb841c..4d88887 100644 (file)
@@ -4,20 +4,21 @@ package SL::DB::AuthClient;
 
 use strict;
 
-use base qw(SL::DB::Object);
+use parent qw(SL::DB::Object);
 
 __PACKAGE__->meta->table('clients');
 __PACKAGE__->meta->schema('auth');
 
 __PACKAGE__->meta->columns(
-  id         => { type => 'serial', not_null => 1 },
-  name       => { type => 'text', not_null => 1 },
-  dbhost     => { type => 'text', not_null => 1 },
-  dbport     => { type => 'integer', default => 5432, not_null => 1 },
-  dbname     => { type => 'text', not_null => 1 },
-  dbuser     => { type => 'text', not_null => 1 },
-  dbpasswd   => { type => 'text', not_null => 1 },
-  is_default => { type => 'boolean', default => 'false', not_null => 1 },
+  dbhost              => { type => 'text', not_null => 1 },
+  dbname              => { type => 'text', not_null => 1 },
+  dbpasswd            => { type => 'text', not_null => 1 },
+  dbport              => { type => 'integer', default => 5432, not_null => 1 },
+  dbuser              => { type => 'text', not_null => 1 },
+  id                  => { type => 'serial', not_null => 1 },
+  is_default          => { type => 'boolean', default => 'false', not_null => 1 },
+  name                => { type => 'text', not_null => 1 },
+  task_server_user_id => { type => 'integer' },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
@@ -27,7 +28,12 @@ __PACKAGE__->meta->unique_keys(
   [ 'name' ],
 );
 
-# __PACKAGE__->meta->initialize;
+__PACKAGE__->meta->foreign_keys(
+  task_server_user => {
+    class       => 'SL::DB::AuthUser',
+    key_columns => { task_server_user_id => 'id' },
+  },
+);
 
 1;
 ;
index 9eac684..d74a1ed 100644 (file)
@@ -4,7 +4,7 @@ package SL::DB::AuthClientGroup;
 
 use strict;
 
-use base qw(SL::DB::Object);
+use parent qw(SL::DB::Object);
 
 __PACKAGE__->meta->table('clients_groups');
 __PACKAGE__->meta->schema('auth');
@@ -28,7 +28,5 @@ __PACKAGE__->meta->foreign_keys(
   },
 );
 
-# __PACKAGE__->meta->initialize;
-
 1;
 ;
index 88a5177..6f83105 100644 (file)
@@ -4,7 +4,7 @@ package SL::DB::AuthClientUser;
 
 use strict;
 
-use base qw(SL::DB::Object);
+use parent qw(SL::DB::Object);
 
 __PACKAGE__->meta->table('clients_users');
 __PACKAGE__->meta->schema('auth');
@@ -28,7 +28,5 @@ __PACKAGE__->meta->foreign_keys(
   },
 );
 
-# __PACKAGE__->meta->initialize;
-
 1;
 ;
index 2671cef..d1bc5fd 100644 (file)
@@ -4,22 +4,20 @@ package SL::DB::AuthGroup;
 
 use strict;
 
-use base qw(SL::DB::Object);
+use parent qw(SL::DB::Object);
 
 __PACKAGE__->meta->table('group');
 __PACKAGE__->meta->schema('auth');
 
 __PACKAGE__->meta->columns(
+  description => { type => 'text' },
   id          => { type => 'serial', not_null => 1 },
   name        => { type => 'text', not_null => 1 },
-  description => { type => 'text' },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
 
 __PACKAGE__->meta->unique_keys([ 'name' ]);
 
-# __PACKAGE__->meta->initialize;
-
 1;
 ;
index 1befd31..92c4db3 100644 (file)
@@ -4,15 +4,15 @@ package SL::DB::AuthGroupRight;
 
 use strict;
 
-use base qw(SL::DB::Object);
+use parent qw(SL::DB::Object);
 
 __PACKAGE__->meta->table('group_rights');
 __PACKAGE__->meta->schema('auth');
 
 __PACKAGE__->meta->columns(
+  granted  => { type => 'boolean', not_null => 1 },
   group_id => { type => 'integer', not_null => 1 },
   right    => { type => 'text', not_null => 1 },
-  granted  => { type => 'boolean', not_null => 1 },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'group_id', 'right' ]);
@@ -24,7 +24,5 @@ __PACKAGE__->meta->foreign_keys(
   },
 );
 
-# __PACKAGE__->meta->initialize;
-
 1;
 ;
diff --git a/SL/DB/MetaSetup/AuthMasterRight.pm b/SL/DB/MetaSetup/AuthMasterRight.pm
new file mode 100644 (file)
index 0000000..8b231dc
--- /dev/null
@@ -0,0 +1,25 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::AuthMasterRight;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('master_rights');
+__PACKAGE__->meta->schema('auth');
+
+__PACKAGE__->meta->columns(
+  category    => { type => 'boolean', default => 'false', not_null => 1 },
+  description => { type => 'text', not_null => 1 },
+  id          => { type => 'serial', not_null => 1 },
+  name        => { type => 'text', not_null => 1 },
+  position    => { type => 'integer', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->unique_keys([ 'name' ]);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/AuthSchemaInfo.pm b/SL/DB/MetaSetup/AuthSchemaInfo.pm
new file mode 100644 (file)
index 0000000..20fdb5b
--- /dev/null
@@ -0,0 +1,23 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::AuthSchemaInfo;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('schema_info');
+__PACKAGE__->meta->schema('auth');
+
+__PACKAGE__->meta->columns(
+  itime => { type => 'timestamp', default => 'now()' },
+  login => { type => 'text' },
+  tag   => { type => 'text', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'tag' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/AuthSession.pm b/SL/DB/MetaSetup/AuthSession.pm
new file mode 100644 (file)
index 0000000..84f0f67
--- /dev/null
@@ -0,0 +1,22 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::AuthSession;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('session');
+__PACKAGE__->meta->schema('auth');
+
+__PACKAGE__->meta->columns(
+  api_token  => { type => 'text' },
+  id         => { type => 'text', not_null => 1 },
+  ip_address => { type => 'scalar' },
+  mtime      => { type => 'timestamp' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/AuthSessionContent.pm b/SL/DB/MetaSetup/AuthSessionContent.pm
new file mode 100644 (file)
index 0000000..fc36381
--- /dev/null
@@ -0,0 +1,29 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::AuthSessionContent;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('session_content');
+__PACKAGE__->meta->schema('auth');
+
+__PACKAGE__->meta->columns(
+  auto_restore => { type => 'boolean' },
+  sess_key     => { type => 'text', not_null => 1 },
+  sess_value   => { type => 'text' },
+  session_id   => { type => 'text', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'session_id', 'sess_key' ]);
+
+__PACKAGE__->meta->foreign_keys(
+  session => {
+    class       => 'SL::DB::AuthSession',
+    key_columns => { session_id => 'id' },
+  },
+);
+
+1;
+;
index 50b87ed..ab05ab0 100644 (file)
@@ -4,7 +4,7 @@ package SL::DB::AuthUser;
 
 use strict;
 
-use base qw(SL::DB::Object);
+use parent qw(SL::DB::Object);
 
 __PACKAGE__->meta->table('user');
 __PACKAGE__->meta->schema('auth');
@@ -19,7 +19,5 @@ __PACKAGE__->meta->primary_key_columns([ 'id' ]);
 
 __PACKAGE__->meta->unique_keys([ 'login' ]);
 
-# __PACKAGE__->meta->initialize;
-
 1;
 ;
index 54ea6ce..8d065a0 100644 (file)
@@ -4,15 +4,15 @@ package SL::DB::AuthUserConfig;
 
 use strict;
 
-use base qw(SL::DB::Object);
+use parent qw(SL::DB::Object);
 
 __PACKAGE__->meta->table('user_config');
 __PACKAGE__->meta->schema('auth');
 
 __PACKAGE__->meta->columns(
-  user_id   => { type => 'integer', not_null => 1 },
   cfg_key   => { type => 'text', not_null => 1 },
   cfg_value => { type => 'text' },
+  user_id   => { type => 'integer', not_null => 1 },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'user_id', 'cfg_key' ]);
@@ -24,7 +24,5 @@ __PACKAGE__->meta->foreign_keys(
   },
 );
 
-# __PACKAGE__->meta->initialize;
-
 1;
 ;
index 6b470cf..321e6c7 100644 (file)
@@ -4,14 +4,14 @@ package SL::DB::AuthUserGroup;
 
 use strict;
 
-use base qw(SL::DB::Object);
+use parent qw(SL::DB::Object);
 
 __PACKAGE__->meta->table('user_group');
 __PACKAGE__->meta->schema('auth');
 
 __PACKAGE__->meta->columns(
-  user_id  => { type => 'integer', not_null => 1 },
   group_id => { type => 'integer', not_null => 1 },
+  user_id  => { type => 'integer', not_null => 1 },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'user_id', 'group_id' ]);
@@ -28,7 +28,5 @@ __PACKAGE__->meta->foreign_keys(
   },
 );
 
-# __PACKAGE__->meta->initialize;
-
 1;
 ;
index 5b674aa..ce2ed7e 100644 (file)
@@ -15,6 +15,7 @@ __PACKAGE__->meta->columns(
   id           => { type => 'serial', not_null => 1 },
   last_run_at  => { type => 'timestamp' },
   next_run_at  => { type => 'timestamp' },
+  node_id      => { type => 'text' },
   package_name => { type => 'varchar', length => 255 },
   type         => { type => 'varchar', length => 255 },
 );
index b72a170..d836778 100644 (file)
@@ -11,6 +11,7 @@ __PACKAGE__->meta->table('bank_accounts');
 __PACKAGE__->meta->columns(
   account_number                  => { type => 'varchar', length => 100 },
   bank                            => { type => 'text' },
+  bank_account_id                 => { type => 'varchar' },
   bank_code                       => { type => 'varchar', length => 100 },
   bic                             => { type => 'varchar', length => 100 },
   chart_id                        => { type => 'integer', not_null => 1 },
@@ -21,6 +22,8 @@ __PACKAGE__->meta->columns(
   reconciliation_starting_balance => { type => 'numeric', precision => 15, scale => 5 },
   reconciliation_starting_date    => { type => 'date' },
   sortkey                         => { type => 'integer', not_null => 1 },
+  use_for_qrbill                  => { type => 'boolean', default => 'false', not_null => 1 },
+  use_for_zugferd                 => { type => 'boolean', default => 'false', not_null => 1 },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
index 9f6e17d..70c4132 100644 (file)
@@ -20,7 +20,9 @@ __PACKAGE__->meta->columns(
   remote_account_number => { type => 'text' },
   remote_bank_code      => { type => 'text' },
   remote_name           => { type => 'text' },
+  transaction_code      => { type => 'text' },
   transaction_id        => { type => 'integer' },
+  transaction_text      => { type => 'text' },
   transdate             => { type => 'date', not_null => 1 },
   valutadate            => { type => 'date', not_null => 1 },
 );
diff --git a/SL/DB/MetaSetup/BankTransactionAccTrans.pm b/SL/DB/MetaSetup/BankTransactionAccTrans.pm
new file mode 100644 (file)
index 0000000..b4ba2e2
--- /dev/null
@@ -0,0 +1,53 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::BankTransactionAccTrans;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('bank_transaction_acc_trans');
+
+__PACKAGE__->meta->columns(
+  acc_trans_id        => { type => 'bigint', not_null => 1 },
+  ap_id               => { type => 'integer' },
+  ar_id               => { type => 'integer' },
+  bank_transaction_id => { type => 'integer', not_null => 1 },
+  gl_id               => { type => 'integer' },
+  itime               => { type => 'timestamp', default => 'now()' },
+  mtime               => { type => 'timestamp' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'bank_transaction_id', 'acc_trans_id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+__PACKAGE__->meta->foreign_keys(
+  acc_transaction => {
+    class       => 'SL::DB::AccTransaction',
+    key_columns => { acc_trans_id => 'acc_trans_id' },
+  },
+
+  ap => {
+    class       => 'SL::DB::PurchaseInvoice',
+    key_columns => { ap_id => 'id' },
+  },
+
+  ar => {
+    class       => 'SL::DB::Invoice',
+    key_columns => { ar_id => 'id' },
+  },
+
+  bank_transaction => {
+    class       => 'SL::DB::BankTransaction',
+    key_columns => { bank_transaction_id => 'id' },
+  },
+
+  gl => {
+    class       => 'SL::DB::GLTransaction',
+    key_columns => { gl_id => 'id' },
+  },
+);
+
+1;
+;
index fde083a..e89cb4b 100644 (file)
@@ -11,11 +11,18 @@ __PACKAGE__->meta->table('buchungsgruppen');
 __PACKAGE__->meta->columns(
   description        => { type => 'text' },
   id                 => { type => 'integer', not_null => 1, sequence => 'id' },
-  inventory_accno_id => { type => 'integer' },
+  inventory_accno_id => { type => 'integer', not_null => 1 },
   sortkey            => { type => 'integer', not_null => 1 },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
 
+__PACKAGE__->meta->foreign_keys(
+  inventory_accno => {
+    class       => 'SL::DB::Chart',
+    key_columns => { inventory_accno_id => 'id' },
+  },
+);
+
 1;
 ;
index 64adf69..08c2d10 100644 (file)
@@ -11,7 +11,7 @@ __PACKAGE__->meta->table('business');
 __PACKAGE__->meta->columns(
   customernumberinit => { type => 'text' },
   description        => { type => 'text' },
-  discount           => { type => 'float', scale => 4 },
+  discount           => { type => 'float', precision => 4, scale => 4 },
   id                 => { type => 'integer', not_null => 1, sequence => 'id' },
   itime              => { type => 'timestamp', default => 'now()' },
   mtime              => { type => 'timestamp' },
index 865296e..993fd0d 100644 (file)
@@ -21,6 +21,7 @@ __PACKAGE__->meta->columns(
   new_chart_id   => { type => 'integer' },
   pos_bilanz     => { type => 'integer' },
   pos_bwa        => { type => 'integer' },
+  pos_er         => { type => 'integer' },
   pos_eur        => { type => 'integer' },
   taxkey_id      => { type => 'integer' },
   valid_from     => { type => 'date' },
index 8d71fcc..419375f 100644 (file)
@@ -18,6 +18,7 @@ __PACKAGE__->meta->columns(
   cp_gender      => { type => 'character', length => 1 },
   cp_givenname   => { type => 'text' },
   cp_id          => { type => 'integer', not_null => 1, sequence => 'id' },
+  cp_main        => { type => 'boolean', default => 'false' },
   cp_mobile1     => { type => 'text' },
   cp_mobile2     => { type => 'text' },
   cp_name        => { type => 'text' },
diff --git a/SL/DB/MetaSetup/ContactDepartment.pm b/SL/DB/MetaSetup/ContactDepartment.pm
new file mode 100644 (file)
index 0000000..bba52e1
--- /dev/null
@@ -0,0 +1,21 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::ContactDepartment;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('contact_departments');
+
+__PACKAGE__->meta->columns(
+  description => { type => 'text', not_null => 1 },
+  id          => { type => 'serial', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->unique_keys([ 'description' ]);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/ContactTitle.pm b/SL/DB/MetaSetup/ContactTitle.pm
new file mode 100644 (file)
index 0000000..63006e9
--- /dev/null
@@ -0,0 +1,21 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::ContactTitle;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('contact_titles');
+
+__PACKAGE__->meta->columns(
+  description => { type => 'text', not_null => 1 },
+  id          => { type => 'serial', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->unique_keys([ 'description' ]);
+
+1;
+;
index 1959132..6b7d296 100644 (file)
@@ -15,6 +15,7 @@ __PACKAGE__->meta->columns(
   numrows    => { type => 'integer', not_null => 1 },
   profile_id => { type => 'integer', not_null => 1 },
   session_id => { type => 'text', not_null => 1 },
+  test_mode  => { type => 'boolean', not_null => 1 },
   type       => { type => 'text', not_null => 1 },
 );
 
diff --git a/SL/DB/MetaSetup/CustomDataExportQuery.pm b/SL/DB/MetaSetup/CustomDataExportQuery.pm
new file mode 100644 (file)
index 0000000..2b3bedc
--- /dev/null
@@ -0,0 +1,26 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::CustomDataExportQuery;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('custom_data_export_queries');
+
+__PACKAGE__->meta->columns(
+  access_right => { type => 'text' },
+  description  => { type => 'text', not_null => 1 },
+  id           => { type => 'serial', not_null => 1 },
+  itime        => { type => 'timestamp', default => 'now()', not_null => 1 },
+  mtime        => { type => 'timestamp', default => 'now()', not_null => 1 },
+  name         => { type => 'text', not_null => 1 },
+  sql_query    => { type => 'text', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/CustomDataExportQueryParameter.pm b/SL/DB/MetaSetup/CustomDataExportQueryParameter.pm
new file mode 100644 (file)
index 0000000..b16c43e
--- /dev/null
@@ -0,0 +1,35 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::CustomDataExportQueryParameter;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('custom_data_export_query_parameters');
+
+__PACKAGE__->meta->columns(
+  default_value      => { type => 'text' },
+  default_value_type => { type => 'enum', check_in => [ 'none', 'current_user_login', 'sql_query', 'fixed_value' ], db_type => 'custom_data_export_query_parameter_default_value_type_enum', not_null => 1 },
+  description        => { type => 'text' },
+  id                 => { type => 'serial', not_null => 1 },
+  itime              => { type => 'timestamp', default => 'now()', not_null => 1 },
+  mtime              => { type => 'timestamp', default => 'now()', not_null => 1 },
+  name               => { type => 'text', not_null => 1 },
+  parameter_type     => { type => 'enum', check_in => [ 'text', 'number', 'date', 'timestamp' ], db_type => 'custom_data_export_query_parameter_type_enum', not_null => 1 },
+  query_id           => { type => 'integer', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+__PACKAGE__->meta->foreign_keys(
+  query => {
+    class       => 'SL::DB::CustomDataExportQuery',
+    key_columns => { query_id => 'id' },
+  },
+);
+
+1;
+;
index af6d690..684d229 100644 (file)
@@ -11,6 +11,7 @@ __PACKAGE__->meta->table('custom_variable_configs');
 __PACKAGE__->meta->columns(
   default_value       => { type => 'text' },
   description         => { type => 'text', not_null => 1 },
+  first_tab           => { type => 'boolean', default => 'false', not_null => 1 },
   flags               => { type => 'text' },
   id                  => { type => 'integer', not_null => 1, sequence => 'custom_variable_configs_id' },
   includeable         => { type => 'boolean', not_null => 1 },
index 173cb75..1785a6e 100644 (file)
@@ -16,38 +16,48 @@ __PACKAGE__->meta->columns(
   bic                       => { type => 'text' },
   business_id               => { type => 'integer' },
   c_vendor_id               => { type => 'text' },
+  c_vendor_routing_id       => { type => 'text' },
   cc                        => { type => 'text' },
   city                      => { type => 'text' },
+  commercial_court          => { type => 'text' },
   contact                   => { type => 'text' },
+  contact_origin            => { type => 'text' },
   country                   => { type => 'text' },
+  create_zugferd_invoices   => { type => 'integer', default => '-1', not_null => 1 },
   creditlimit               => { type => 'numeric', default => '0', precision => 15, scale => 5 },
   currency_id               => { type => 'integer', not_null => 1 },
   customernumber            => { type => 'text' },
+  delivery_order_mail       => { type => 'text' },
   delivery_term_id          => { type => 'integer' },
   department_1              => { type => 'text' },
   department_2              => { type => 'text' },
   depositor                 => { type => 'text' },
   direct_debit              => { type => 'boolean', default => 'false' },
-  discount                  => { type => 'float', scale => 4 },
+  discount                  => { type => 'float', precision => 4, scale => 4 },
   email                     => { type => 'text' },
   fax                       => { type => 'text' },
+  gln                       => { type => 'text' },
   greeting                  => { type => 'text' },
   homepage                  => { type => 'text' },
   hourly_rate               => { type => 'numeric', precision => 8, scale => 2 },
   iban                      => { type => 'text' },
   id                        => { type => 'integer', not_null => 1, sequence => 'id' },
+  invoice_mail              => { type => 'text' },
   itime                     => { type => 'timestamp', default => 'now()' },
-  klass                     => { type => 'integer', default => '0' },
   language                  => { type => 'text' },
   language_id               => { type => 'integer' },
   mandate_date_of_signature => { type => 'date' },
   mandator_id               => { type => 'text' },
   mtime                     => { type => 'timestamp' },
   name                      => { type => 'text', not_null => 1 },
+  natural_person            => { type => 'boolean', default => 'false' },
   notes                     => { type => 'text' },
   obsolete                  => { type => 'boolean', default => 'false' },
+  order_lock                => { type => 'boolean', default => 'false' },
   payment_id                => { type => 'integer' },
   phone                     => { type => 'text' },
+  postal_invoice            => { type => 'boolean', default => 'false' },
+  pricegroup_id             => { type => 'integer' },
   salesman_id               => { type => 'integer' },
   street                    => { type => 'text' },
   taxincluded               => { type => 'boolean' },
@@ -90,6 +100,11 @@ __PACKAGE__->meta->foreign_keys(
     key_columns => { payment_id => 'id' },
   },
 
+  pricegroup => {
+    class       => 'SL::DB::Pricegroup',
+    key_columns => { pricegroup_id => 'id' },
+  },
+
   taxzone => {
     class       => 'SL::DB::TaxZone',
     key_columns => { taxzone_id => 'id' },
index 821ed98..295c6e7 100644 (file)
@@ -10,46 +10,98 @@ __PACKAGE__->meta->table('defaults');
 
 __PACKAGE__->meta->columns(
   accounting_method                         => { type => 'text' },
-  address                                   => { type => 'text' },
+  address_city                              => { type => 'text' },
+  address_country                           => { type => 'text' },
+  address_street1                           => { type => 'text' },
+  address_street2                           => { type => 'text' },
+  address_zipcode                           => { type => 'text' },
+  advance_payment_clearing_chart_id         => { type => 'integer' },
+  advance_payment_taxable_19_id             => { type => 'integer' },
+  advance_payment_taxable_7_id              => { type => 'integer' },
   allow_new_purchase_delivery_order         => { type => 'boolean', default => 'true', not_null => 1 },
   allow_new_purchase_invoice                => { type => 'boolean', default => 'true', not_null => 1 },
   allow_sales_invoice_from_sales_order      => { type => 'boolean', default => 'true', not_null => 1 },
   allow_sales_invoice_from_sales_quotation  => { type => 'boolean', default => 'true', not_null => 1 },
+  always_record_links_from_order            => { type => 'boolean', default => 'false' },
+  ap_add_doc                                => { type => 'boolean', default => 'false', not_null => 1 },
   ap_changeable                             => { type => 'integer', default => 2, not_null => 1 },
+  ap_chart_id                               => { type => 'integer' },
   ap_show_mark_as_paid                      => { type => 'boolean', default => 'true' },
+  ar_add_doc                                => { type => 'boolean', default => 'false', not_null => 1 },
   ar_changeable                             => { type => 'integer', default => 2, not_null => 1 },
+  ar_chart_id                               => { type => 'integer' },
   ar_paid_accno_id                          => { type => 'integer' },
   ar_show_mark_as_paid                      => { type => 'boolean', default => 'true' },
   articlenumber                             => { type => 'text' },
   assemblynumber                            => { type => 'text' },
+  assortmentnumber                          => { type => 'text' },
   balance_startdate_method                  => { type => 'text' },
+  bcc_to_login                              => { type => 'boolean', default => 'false', not_null => 1 },
   bin_id                                    => { type => 'integer' },
   bin_id_ignore_onhand                      => { type => 'integer' },
   businessnumber                            => { type => 'text' },
+  carry_over_account_chart_id               => { type => 'integer' },
   closedto                                  => { type => 'date' },
   cnnumber                                  => { type => 'text' },
   co_ustid                                  => { type => 'text' },
   coa                                       => { type => 'text' },
   company                                   => { type => 'text' },
+  contact_departments_use_textfield         => { type => 'boolean' },
+  contact_titles_use_textfield              => { type => 'boolean' },
+  create_part_if_not_found                  => { type => 'boolean', default => 'false' },
+  create_qrbill_invoices                    => { type => 'integer' },
+  create_zugferd_invoices                   => { type => 'integer' },
   currency_id                               => { type => 'integer', not_null => 1 },
   customer_hourly_rate                      => { type => 'numeric', precision => 8, scale => 2 },
   customer_projects_only_in_sales           => { type => 'boolean', default => 'false', not_null => 1 },
+  customer_ustid_taxnummer_unique           => { type => 'boolean', default => 'false' },
   customernumber                            => { type => 'text' },
   datev_check_on_ap_transaction             => { type => 'boolean', default => 'true' },
   datev_check_on_ar_transaction             => { type => 'boolean', default => 'true' },
   datev_check_on_gl_transaction             => { type => 'boolean', default => 'true' },
   datev_check_on_purchase_invoice           => { type => 'boolean', default => 'true' },
   datev_check_on_sales_invoice              => { type => 'boolean', default => 'true' },
-  delivery_plan_calculate_transferred_do    => { type => 'boolean', default => 'false', not_null => 1 },
+  datev_export_format                       => { type => 'enum', check_in => [ 'cp1252', 'cp1252-translit', 'utf-8' ], db_type => 'datev_export_format_enum', default => 'cp1252-translit' },
+  delivery_date_interval                    => { type => 'integer', default => '0' },
+  deliverydate_on                           => { type => 'boolean', default => 'true' },
   disabled_price_sources                    => { type => 'array' },
+  doc_delete_printfiles                     => { type => 'boolean', default => 'false' },
+  doc_files                                 => { type => 'boolean', default => 'false' },
+  doc_files_rootpath                        => { type => 'text', default => './documents' },
+  doc_max_filesize                          => { type => 'integer', default => 10000000 },
+  doc_storage                               => { type => 'boolean', default => 'false' },
+  doc_storage_for_attachments               => { type => 'text', default => 'Filesystem' },
+  doc_storage_for_documents                 => { type => 'text', default => 'Filesystem' },
+  doc_storage_for_images                    => { type => 'text', default => 'Filesystem' },
+  doc_storage_for_shopimages                => { type => 'text', default => 'Filesystem' },
+  doc_webdav                                => { type => 'boolean', default => 'false' },
   dunning_ar                                => { type => 'integer' },
   dunning_ar_amount_fee                     => { type => 'integer' },
   dunning_ar_amount_interest                => { type => 'integer' },
+  dunning_creator                           => { type => 'enum', check_in => [ 'current_employee', 'invoice_employee' ], db_type => 'dunning_creator', default => 'current_employee' },
   duns                                      => { type => 'text' },
+  email_attachment_part_files_checked       => { type => 'boolean', default => 'true' },
+  email_attachment_record_files_checked     => { type => 'boolean', default => 'true' },
+  email_attachment_vc_files_checked         => { type => 'boolean', default => 'true' },
   email_journal                             => { type => 'integer', default => 2 },
   expense_accno_id                          => { type => 'integer' },
+  fa_bufa_nr                                => { type => 'text' },
+  fa_dauerfrist                             => { type => 'text' },
+  fa_steuerberater_city                     => { type => 'text' },
+  fa_steuerberater_name                     => { type => 'text' },
+  fa_steuerberater_street                   => { type => 'text' },
+  fa_steuerberater_tel                      => { type => 'text' },
+  fa_voranmeld                              => { type => 'text' },
+  feature_balance                           => { type => 'boolean', default => 'true', not_null => 1 },
+  feature_datev                             => { type => 'boolean', default => 'true', not_null => 1 },
+  feature_erfolgsrechnung                   => { type => 'boolean', default => 'false', not_null => 1 },
+  feature_eurechnung                        => { type => 'boolean', default => 'true', not_null => 1 },
+  feature_experimental_assortment           => { type => 'boolean', default => 'true', not_null => 1 },
+  feature_experimental_order                => { type => 'boolean', default => 'true', not_null => 1 },
+  feature_ustva                             => { type => 'boolean', default => 'true', not_null => 1 },
   fxgain_accno_id                           => { type => 'integer' },
   fxloss_accno_id                           => { type => 'integer' },
+  gl_add_doc                                => { type => 'boolean', default => 'false', not_null => 1 },
   gl_changeable                             => { type => 'integer', default => 2, not_null => 1 },
   global_bcc                                => { type => 'text', default => '' },
   id                                        => { type => 'serial', not_null => 1 },
@@ -57,6 +109,9 @@ __PACKAGE__->meta->columns(
   inventory_accno_id                        => { type => 'integer' },
   inventory_system                          => { type => 'text' },
   invnumber                                 => { type => 'text' },
+  invoice_mail_settings                     => { type => 'enum', check_in => [ 'cp', 'invoice_mail', 'invoice_mail_cc_cp' ], db_type => 'invoice_mail_settings', default => 'cp' },
+  invoice_prevent_browser_back              => { type => 'boolean', default => 'false', not_null => 1 },
+  ir_add_doc                                => { type => 'boolean', default => 'false', not_null => 1 },
   ir_changeable                             => { type => 'integer', default => 2, not_null => 1 },
   ir_show_mark_as_paid                      => { type => 'boolean', default => 'true' },
   is_changeable                             => { type => 'integer', default => 2, not_null => 1 },
@@ -65,36 +120,71 @@ __PACKAGE__->meta->columns(
   itime                                     => { type => 'timestamp', default => 'now()' },
   language_id                               => { type => 'integer' },
   letternumber                              => { type => 'integer' },
+  loss_carried_forward_chart_id             => { type => 'integer' },
   max_future_booking_interval               => { type => 'integer', default => 360 },
   mtime                                     => { type => 'timestamp' },
   normalize_part_descriptions               => { type => 'boolean', default => 'true' },
   normalize_vc_names                        => { type => 'boolean', default => 'true' },
+  order_always_project                      => { type => 'boolean', default => 'false' },
+  order_warn_duplicate_parts                => { type => 'boolean', default => 'true' },
+  order_warn_no_cusordnumber                => { type => 'boolean', default => 'false' },
+  order_warn_no_deliverydate                => { type => 'boolean', default => 'true' },
   parts_image_css                           => { type => 'text', default => 'border:0;float:left;max-width:250px;margin-top:20px:margin-right:10px;margin-left:10px;' },
   parts_listing_image                       => { type => 'boolean', default => 'true' },
   parts_show_image                          => { type => 'boolean', default => 'true' },
+  partsgroup_required                       => { type => 'boolean', default => 'false', not_null => 1 },
   payments_changeable                       => { type => 'integer', default => '0', not_null => 1 },
   pdonumber                                 => { type => 'text' },
   ponumber                                  => { type => 'text' },
+  precision                                 => { type => 'numeric', default => '0.01', not_null => 1, precision => 15, scale => 5 },
+  print_interpolate_variables_in_positions  => { type => 'boolean', default => 'true', not_null => 1 },
+  produce_assembly_same_warehouse           => { type => 'boolean', default => 'true' },
+  produce_assembly_transfer_service         => { type => 'boolean', default => 'false' },
+  profit_carried_forward_chart_id           => { type => 'integer' },
   profit_determination                      => { type => 'text' },
+  project_status_id                         => { type => 'integer' },
+  project_type_id                           => { type => 'integer' },
+  purchase_delivery_order_check_service     => { type => 'boolean', default => 'true' },
+  purchase_delivery_order_check_stocked     => { type => 'boolean', default => 'false' },
   purchase_delivery_order_show_delete       => { type => 'boolean', default => 'true' },
   purchase_order_show_delete                => { type => 'boolean', default => 'true' },
+  quick_search_modules                      => { type => 'array' },
+  rdonumber                                 => { type => 'text' },
   reqdate_interval                          => { type => 'integer', default => '0' },
+  reqdate_on                                => { type => 'boolean', default => 'true' },
   require_transaction_description_ps        => { type => 'boolean', default => 'false', not_null => 1 },
   requirement_spec_section_order_part_id    => { type => 'integer' },
   revtrans                                  => { type => 'boolean', default => 'false' },
   rfqnumber                                 => { type => 'text' },
   rmanumber                                 => { type => 'text' },
+  rndgain_accno_id                          => { type => 'integer' },
+  rndloss_accno_id                          => { type => 'integer' },
+  sales_delivery_order_check_service        => { type => 'boolean', default => 'true' },
+  sales_delivery_order_check_stocked        => { type => 'boolean', default => 'false' },
   sales_delivery_order_show_delete          => { type => 'boolean', default => 'true' },
   sales_order_show_delete                   => { type => 'boolean', default => 'true' },
   sales_purchase_order_ship_missing_column  => { type => 'boolean', default => 'false' },
+  sales_purchase_record_numbers_changeable  => { type => 'boolean', default => 'false', not_null => 1 },
+  sales_serial_eq_charge                    => { type => 'boolean', default => 'false', not_null => 1 },
   sdonumber                                 => { type => 'text' },
   sepa_creditor_id                          => { type => 'text' },
+  sepa_reference_add_vc_vc_id               => { type => 'boolean', default => 'false' },
+  sepa_set_duedate_as_default_exec_date     => { type => 'boolean', default => 'false' },
+  sepa_set_skonto_date_as_default_exec_date => { type => 'boolean', default => 'false' },
+  sepa_set_skonto_date_buffer_in_days       => { type => 'integer', default => '0' },
   servicenumber                             => { type => 'text' },
+  shipped_qty_require_stock_out             => { type => 'boolean', default => 'false', not_null => 1 },
   show_bestbefore                           => { type => 'boolean', default => 'false' },
+  show_longdescription_select_item          => { type => 'boolean', default => 'false' },
   show_weight                               => { type => 'boolean', default => 'false', not_null => 1 },
   signature                                 => { type => 'text' },
   sonumber                                  => { type => 'text' },
   sqnumber                                  => { type => 'text' },
+  stocktaking_bin_id                        => { type => 'integer' },
+  stocktaking_cutoff_date                   => { type => 'date' },
+  stocktaking_qty_threshold                 => { type => 'numeric', default => '0', precision => 25, scale => 5 },
+  stocktaking_warehouse_id                  => { type => 'integer' },
+  sudonumber                                => { type => 'text' },
   taxnumber                                 => { type => 'text' },
   templates                                 => { type => 'text' },
   transfer_default                          => { type => 'boolean', default => 'true' },
@@ -102,14 +192,19 @@ __PACKAGE__->meta->columns(
   transfer_default_services                 => { type => 'boolean', default => 'true' },
   transfer_default_use_master_default_bin   => { type => 'boolean', default => 'false' },
   transport_cost_reminder_article_number_id => { type => 'integer' },
+  undo_transfer_interval                    => { type => 'integer', default => 7 },
+  vc_greetings_use_textfield                => { type => 'boolean' },
+  vendor_ustid_taxnummer_unique             => { type => 'boolean', default => 'false' },
   vendornumber                              => { type => 'text' },
   version                                   => { type => 'varchar', length => 8 },
   vertreter                                 => { type => 'boolean', default => 'false' },
   warehouse_id                              => { type => 'integer' },
   warehouse_id_ignore_onhand                => { type => 'integer' },
+  warn_no_delivery_order_for_invoice        => { type => 'boolean', default => 'false' },
   webdav                                    => { type => 'boolean', default => 'false' },
   webdav_documents                          => { type => 'boolean', default => 'false' },
   weightunit                                => { type => 'varchar', length => 5 },
+  workflow_po_ap_chart_id                   => { type => 'integer' },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
@@ -117,6 +212,16 @@ __PACKAGE__->meta->primary_key_columns([ 'id' ]);
 __PACKAGE__->meta->allow_inline_column_values(1);
 
 __PACKAGE__->meta->foreign_keys(
+  ap_chart => {
+    class       => 'SL::DB::Chart',
+    key_columns => { ap_chart_id => 'id' },
+  },
+
+  ar_chart => {
+    class       => 'SL::DB::Chart',
+    key_columns => { ar_chart_id => 'id' },
+  },
+
   bin => {
     class       => 'SL::DB::Bin',
     key_columns => { bin_id => 'id' },
@@ -127,16 +232,51 @@ __PACKAGE__->meta->foreign_keys(
     key_columns => { bin_id_ignore_onhand => 'id' },
   },
 
+  carry_over_account_chart => {
+    class       => 'SL::DB::Chart',
+    key_columns => { carry_over_account_chart_id => 'id' },
+  },
+
   currency => {
     class       => 'SL::DB::Currency',
     key_columns => { currency_id => 'id' },
   },
 
+  loss_carried_forward_chart => {
+    class       => 'SL::DB::Chart',
+    key_columns => { loss_carried_forward_chart_id => 'id' },
+  },
+
+  profit_carried_forward_chart => {
+    class       => 'SL::DB::Chart',
+    key_columns => { profit_carried_forward_chart_id => 'id' },
+  },
+
+  project_status => {
+    class       => 'SL::DB::ProjectStatus',
+    key_columns => { project_status_id => 'id' },
+  },
+
+  project_type => {
+    class       => 'SL::DB::ProjectType',
+    key_columns => { project_type_id => 'id' },
+  },
+
   requirement_spec_section_order_part => {
     class       => 'SL::DB::Part',
     key_columns => { requirement_spec_section_order_part_id => 'id' },
   },
 
+  stocktaking_bin => {
+    class       => 'SL::DB::Bin',
+    key_columns => { stocktaking_bin_id => 'id' },
+  },
+
+  stocktaking_warehouse => {
+    class       => 'SL::DB::Warehouse',
+    key_columns => { stocktaking_warehouse_id => 'id' },
+  },
+
   warehouse => {
     class       => 'SL::DB::Warehouse',
     key_columns => { warehouse_id => 'id' },
index d242fcd..994fa96 100644 (file)
@@ -9,6 +9,7 @@ use parent qw(SL::DB::Object);
 __PACKAGE__->meta->table('delivery_orders');
 
 __PACKAGE__->meta->columns(
+  billing_address_id      => { type => 'integer' },
   closed                  => { type => 'boolean', default => 'false' },
   cp_id                   => { type => 'integer' },
   currency_id             => { type => 'integer', not_null => 1 },
@@ -22,11 +23,11 @@ __PACKAGE__->meta->columns(
   globalproject_id        => { type => 'integer' },
   id                      => { type => 'integer', not_null => 1, sequence => 'id' },
   intnotes                => { type => 'text' },
-  is_sales                => { type => 'boolean' },
   itime                   => { type => 'timestamp', default => 'now()' },
   language_id             => { type => 'integer' },
   mtime                   => { type => 'timestamp' },
   notes                   => { type => 'text' },
+  order_type              => { type => 'text', not_null => 1 },
   ordnumber               => { type => 'text' },
   oreqnumber              => { type => 'text' },
   payment_id              => { type => 'integer' },
@@ -35,6 +36,7 @@ __PACKAGE__->meta->columns(
   shippingpoint           => { type => 'text' },
   shipto_id               => { type => 'integer' },
   shipvia                 => { type => 'text' },
+  tax_point               => { type => 'date' },
   taxincluded             => { type => 'boolean' },
   taxzone_id              => { type => 'integer', not_null => 1 },
   transaction_description => { type => 'text' },
@@ -47,6 +49,11 @@ __PACKAGE__->meta->primary_key_columns([ 'id' ]);
 __PACKAGE__->meta->allow_inline_column_values(1);
 
 __PACKAGE__->meta->foreign_keys(
+  billing_address => {
+    class       => 'SL::DB::AdditionalBillingAddress',
+    key_columns => { billing_address_id => 'id' },
+  },
+
   contact => {
     class       => 'SL::DB::Contact',
     key_columns => { cp_id => 'cp_id' },
index 3a6cd9a..7d24d1c 100644 (file)
@@ -11,11 +11,11 @@ __PACKAGE__->meta->table('delivery_order_items');
 __PACKAGE__->meta->columns(
   active_discount_source => { type => 'text', default => '', not_null => 1 },
   active_price_source    => { type => 'text', default => '', not_null => 1 },
-  base_qty               => { type => 'float', scale => 4 },
+  base_qty               => { type => 'float', precision => 4, scale => 4 },
   cusordnumber           => { type => 'text' },
   delivery_order_id      => { type => 'integer', not_null => 1 },
   description            => { type => 'text' },
-  discount               => { type => 'float', scale => 4 },
+  discount               => { type => 'float', precision => 4, scale => 4 },
   id                     => { type => 'integer', not_null => 1, sequence => 'delivery_order_items_id' },
   itime                  => { type => 'timestamp', default => 'now()' },
   lastcost               => { type => 'numeric', precision => 15, scale => 5 },
index a0098e7..6869197 100644 (file)
@@ -9,18 +9,19 @@ use parent qw(SL::DB::Object);
 __PACKAGE__->meta->table('dunning');
 
 __PACKAGE__->meta->columns(
-  duedate            => { type => 'date' },
-  dunning_config_id  => { type => 'integer' },
-  dunning_id         => { type => 'integer' },
-  dunning_level      => { type => 'integer' },
-  fee                => { type => 'numeric', precision => 15, scale => 5 },
-  fee_interest_ar_id => { type => 'integer' },
-  id                 => { type => 'integer', not_null => 1, sequence => 'id' },
-  interest           => { type => 'numeric', precision => 15, scale => 5 },
-  itime              => { type => 'timestamp', default => 'now()' },
-  mtime              => { type => 'timestamp' },
-  trans_id           => { type => 'integer' },
-  transdate          => { type => 'date' },
+  duedate                  => { type => 'date' },
+  dunning_config_id        => { type => 'integer' },
+  dunning_id               => { type => 'integer' },
+  dunning_level            => { type => 'integer' },
+  fee                      => { type => 'numeric', precision => 15, scale => 5 },
+  fee_interest_ar_id       => { type => 'integer' },
+  id                       => { type => 'integer', not_null => 1, sequence => 'id' },
+  interest                 => { type => 'numeric', precision => 15, scale => 5 },
+  itime                    => { type => 'timestamp', default => 'now()' },
+  mtime                    => { type => 'timestamp' },
+  original_invoice_printed => { type => 'boolean', default => 'false' },
+  trans_id                 => { type => 'integer' },
+  transdate                => { type => 'date' },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
@@ -33,10 +34,15 @@ __PACKAGE__->meta->foreign_keys(
     key_columns => { dunning_config_id => 'id' },
   },
 
-  fee_interest_ar => {
+  fee_interest_invoice => {
     class       => 'SL::DB::Invoice',
     key_columns => { fee_interest_ar_id => 'id' },
   },
+
+  invoice => {
+    class       => 'SL::DB::Invoice',
+    key_columns => { trans_id => 'id' },
+  },
 );
 
 1;
index ef4c594..e737864 100644 (file)
@@ -22,6 +22,7 @@ __PACKAGE__->meta->columns(
   id                       => { type => 'integer', not_null => 1, sequence => 'id' },
   interest_rate            => { type => 'numeric', precision => 15, scale => 5 },
   payment_terms            => { type => 'integer' },
+  print_original_invoice   => { type => 'boolean' },
   template                 => { type => 'text' },
   terms                    => { type => 'integer' },
 );
index e804477..591e799 100644 (file)
@@ -11,6 +11,7 @@ __PACKAGE__->meta->table('email_journal_attachments');
 __PACKAGE__->meta->columns(
   content          => { type => 'bytea', not_null => 1 },
   email_journal_id => { type => 'integer', not_null => 1 },
+  file_id          => { type => 'integer', default => '0', not_null => 1 },
   id               => { type => 'serial', not_null => 1 },
   itime            => { type => 'timestamp', default => 'now()', not_null => 1 },
   mime_type        => { type => 'text', not_null => 1 },
diff --git a/SL/DB/MetaSetup/EmployeeProjectInvoices.pm b/SL/DB/MetaSetup/EmployeeProjectInvoices.pm
new file mode 100644 (file)
index 0000000..c3ef6de
--- /dev/null
@@ -0,0 +1,31 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::EmployeeProjectInvoices;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('employee_project_invoices');
+
+__PACKAGE__->meta->columns(
+  employee_id => { type => 'integer', not_null => 1 },
+  project_id  => { type => 'integer', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'employee_id', 'project_id' ]);
+
+__PACKAGE__->meta->foreign_keys(
+  employee => {
+    class       => 'SL::DB::Employee',
+    key_columns => { employee_id => 'id' },
+  },
+
+  project => {
+    class       => 'SL::DB::Project',
+    key_columns => { project_id => 'id' },
+  },
+);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/File.pm b/SL/DB/MetaSetup/File.pm
new file mode 100644 (file)
index 0000000..e5fd622
--- /dev/null
@@ -0,0 +1,33 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::File;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('files');
+
+__PACKAGE__->meta->columns(
+  backend       => { type => 'text' },
+  backend_data  => { type => 'text' },
+  description   => { type => 'text' },
+  file_name     => { type => 'text', not_null => 1 },
+  file_type     => { type => 'text', not_null => 1 },
+  id            => { type => 'serial', not_null => 1 },
+  itime         => { type => 'timestamp', default => 'now()' },
+  mime_type     => { type => 'text', not_null => 1 },
+  mtime         => { type => 'timestamp' },
+  object_id     => { type => 'integer', not_null => 1 },
+  object_type   => { type => 'text', not_null => 1 },
+  print_variant => { type => 'text' },
+  source        => { type => 'text', not_null => 1 },
+  title         => { type => 'varchar', length => 45 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/FileFullText.pm b/SL/DB/MetaSetup/FileFullText.pm
new file mode 100644 (file)
index 0000000..2a93822
--- /dev/null
@@ -0,0 +1,31 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::FileFullText;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('file_full_texts');
+
+__PACKAGE__->meta->columns(
+  file_id   => { type => 'integer', not_null => 1 },
+  full_text => { type => 'text', not_null => 1 },
+  id        => { type => 'serial', not_null => 1 },
+  itime     => { type => 'timestamp', default => 'now()', not_null => 1 },
+  mtime     => { type => 'timestamp' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+__PACKAGE__->meta->foreign_keys(
+  file => {
+    class       => 'SL::DB::File',
+    key_columns => { file_id => 'id' },
+  },
+);
+
+1;
+;
index b9119fb..1659f8f 100644 (file)
@@ -24,12 +24,12 @@ __PACKAGE__->meta->primary_key_columns([ 'id' ]);
 __PACKAGE__->meta->allow_inline_column_values(1);
 
 __PACKAGE__->meta->foreign_keys(
-  created_by => {
+  created_by_employee => {
     class       => 'SL::DB::Employee',
     key_columns => { created_by => 'id' },
   },
 
-  created_for => {
+  created_for_employee => {
     class       => 'SL::DB::Employee',
     key_columns => { created_for_user => 'id' },
   },
index 3b829d5..a2cda63 100644 (file)
@@ -9,22 +9,26 @@ use parent qw(SL::DB::Object);
 __PACKAGE__->meta->table('gl');
 
 __PACKAGE__->meta->columns(
-  cb_transaction => { type => 'boolean' },
-  department_id  => { type => 'integer' },
-  description    => { type => 'text' },
-  employee_id    => { type => 'integer' },
-  gldate         => { type => 'date', default => 'now' },
-  id             => { type => 'integer', not_null => 1, sequence => 'glid' },
-  itime          => { type => 'timestamp', default => 'now()' },
-  mtime          => { type => 'timestamp' },
-  notes          => { type => 'text' },
-  ob_transaction => { type => 'boolean' },
-  reference      => { type => 'text' },
-  storno         => { type => 'boolean', default => 'false' },
-  storno_id      => { type => 'integer' },
-  taxincluded    => { type => 'boolean' },
-  transdate      => { type => 'date', default => 'now' },
-  type           => { type => 'text' },
+  cb_transaction          => { type => 'boolean' },
+  deliverydate            => { type => 'date' },
+  department_id           => { type => 'integer' },
+  description             => { type => 'text' },
+  employee_id             => { type => 'integer' },
+  gldate                  => { type => 'date', default => 'now' },
+  id                      => { type => 'integer', not_null => 1, sequence => 'glid' },
+  imported                => { type => 'boolean', default => 'false' },
+  itime                   => { type => 'timestamp', default => 'now()' },
+  mtime                   => { type => 'timestamp' },
+  notes                   => { type => 'text' },
+  ob_transaction          => { type => 'boolean' },
+  reference               => { type => 'text' },
+  storno                  => { type => 'boolean', default => 'false' },
+  storno_id               => { type => 'integer' },
+  tax_point               => { type => 'date' },
+  taxincluded             => { type => 'boolean' },
+  transaction_description => { type => 'text' },
+  transdate               => { type => 'date', default => 'now' },
+  type                    => { type => 'text' },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
diff --git a/SL/DB/MetaSetup/Greeting.pm b/SL/DB/MetaSetup/Greeting.pm
new file mode 100644 (file)
index 0000000..5a38749
--- /dev/null
@@ -0,0 +1,21 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::Greeting;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('greetings');
+
+__PACKAGE__->meta->columns(
+  description => { type => 'text', not_null => 1 },
+  id          => { type => 'serial', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->unique_keys([ 'description' ]);
+
+1;
+;
index 14d1ea1..2553004 100644 (file)
@@ -23,7 +23,7 @@ __PACKAGE__->meta->columns(
   parts_id                      => { type => 'integer', not_null => 1 },
   project_id                    => { type => 'integer' },
   qty                           => { type => 'numeric', precision => 25, scale => 5 },
-  shippingdate                  => { type => 'date' },
+  shippingdate                  => { type => 'date', not_null => 1 },
   trans_id                      => { type => 'integer', not_null => 1 },
   trans_type_id                 => { type => 'integer', not_null => 1 },
   warehouse_id                  => { type => 'integer', not_null => 1 },
index b88c2f5..e80b110 100644 (file)
@@ -10,6 +10,7 @@ __PACKAGE__->meta->table('ar');
 
 __PACKAGE__->meta->columns(
   amount                    => { type => 'numeric', default => '0', not_null => 1, precision => 15, scale => 5 },
+  billing_address_id        => { type => 'integer' },
   cp_id                     => { type => 'integer' },
   currency_id               => { type => 'integer', not_null => 1 },
   cusordnumber              => { type => 'text' },
@@ -43,6 +44,7 @@ __PACKAGE__->meta->columns(
   ordnumber                 => { type => 'text' },
   paid                      => { type => 'numeric', default => '0', not_null => 1, precision => 15, scale => 5 },
   payment_id                => { type => 'integer' },
+  qrbill_without_amount     => { type => 'boolean', default => 'false' },
   quodate                   => { type => 'date' },
   quonumber                 => { type => 'text' },
   salesman_id               => { type => 'integer' },
@@ -51,6 +53,7 @@ __PACKAGE__->meta->columns(
   shipvia                   => { type => 'text' },
   storno                    => { type => 'boolean', default => 'false' },
   storno_id                 => { type => 'integer' },
+  tax_point                 => { type => 'date' },
   taxincluded               => { type => 'boolean' },
   taxzone_id                => { type => 'integer', not_null => 1 },
   transaction_description   => { type => 'text' },
@@ -63,6 +66,11 @@ __PACKAGE__->meta->primary_key_columns([ 'id' ]);
 __PACKAGE__->meta->allow_inline_column_values(1);
 
 __PACKAGE__->meta->foreign_keys(
+  billing_address => {
+    class       => 'SL::DB::AdditionalBillingAddress',
+    key_columns => { billing_address_id => 'id' },
+  },
+
   contact => {
     class       => 'SL::DB::Contact',
     key_columns => { cp_id => 'cp_id' },
index 2da0d7f..8a67022 100644 (file)
@@ -11,13 +11,13 @@ __PACKAGE__->meta->table('invoice');
 __PACKAGE__->meta->columns(
   active_discount_source => { type => 'text', default => '', not_null => 1 },
   active_price_source    => { type => 'text', default => '', not_null => 1 },
-  allocated              => { type => 'float', scale => 4 },
+  allocated              => { type => 'float', precision => 4, scale => 4 },
   assemblyitem           => { type => 'boolean', default => 'false' },
-  base_qty               => { type => 'float', scale => 4 },
+  base_qty               => { type => 'float', precision => 4, scale => 4 },
   cusordnumber           => { type => 'text' },
   deliverydate           => { type => 'date' },
   description            => { type => 'text' },
-  discount               => { type => 'float', scale => 4 },
+  discount               => { type => 'float', precision => 4, scale => 4 },
   donumber               => { type => 'text' },
   fxsellprice            => { type => 'numeric', precision => 15, scale => 5 },
   id                     => { type => 'integer', not_null => 1, sequence => 'invoiceid' },
@@ -35,7 +35,7 @@ __PACKAGE__->meta->columns(
   price_factor_id        => { type => 'integer' },
   pricegroup_id          => { type => 'integer' },
   project_id             => { type => 'integer' },
-  qty                    => { type => 'float', scale => 4 },
+  qty                    => { type => 'numeric', precision => 25, scale => 5 },
   sellprice              => { type => 'numeric', precision => 15, scale => 5 },
   serialnumber           => { type => 'text' },
   subtotal               => { type => 'boolean', default => 'false' },
index 9aa8039..96d0a0a 100644 (file)
@@ -14,6 +14,7 @@ __PACKAGE__->meta->columns(
   id                  => { type => 'integer', not_null => 1, sequence => 'id' },
   itime               => { type => 'timestamp', default => 'now()' },
   mtime               => { type => 'timestamp' },
+  obsolete            => { type => 'boolean', default => 'false' },
   output_dateformat   => { type => 'text' },
   output_longdates    => { type => 'boolean' },
   output_numberformat => { type => 'text' },
index 0d29b9f..64d513e 100644 (file)
@@ -9,34 +9,21 @@ use parent qw(SL::DB::Object);
 __PACKAGE__->meta->table('letter');
 
 __PACKAGE__->meta->columns(
-  body              => { type => 'text' },
-  close             => { type => 'text' },
-  company_name      => { type => 'text' },
-  cp_id             => { type => 'integer' },
-  date              => { type => 'date' },
-  employee_id       => { type => 'integer' },
-  employee_position => { type => 'text' },
-  greeting          => { type => 'text' },
-  id                => { type => 'integer', not_null => 1, sequence => 'id' },
-  intnotes          => { type => 'text' },
-  itime             => { type => 'timestamp', default => 'now()' },
-  jobnumber         => { type => 'text' },
-  letternumber      => { type => 'text' },
-  mtime             => { type => 'timestamp' },
-  page_created_for  => { type => 'text' },
-  rcv_address       => { type => 'text' },
-  rcv_city          => { type => 'text' },
-  rcv_contact       => { type => 'text' },
-  rcv_country       => { type => 'text' },
-  rcv_countrycode   => { type => 'text' },
-  rcv_name          => { type => 'text' },
-  rcv_zipcode       => { type => 'text' },
-  reference         => { type => 'text' },
-  salesman_id       => { type => 'integer' },
-  salesman_position => { type => 'text' },
-  subject           => { type => 'text' },
-  text_created_for  => { type => 'text' },
-  vc_id             => { type => 'integer', not_null => 1 },
+  body         => { type => 'text' },
+  cp_id        => { type => 'integer' },
+  customer_id  => { type => 'integer' },
+  date         => { type => 'date' },
+  employee_id  => { type => 'integer' },
+  greeting     => { type => 'text' },
+  id           => { type => 'integer', not_null => 1, sequence => 'id' },
+  intnotes     => { type => 'text' },
+  itime        => { type => 'timestamp', default => 'now()' },
+  letternumber => { type => 'text' },
+  mtime        => { type => 'timestamp' },
+  reference    => { type => 'text' },
+  salesman_id  => { type => 'integer' },
+  subject      => { type => 'text' },
+  vendor_id    => { type => 'integer' },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
@@ -49,6 +36,11 @@ __PACKAGE__->meta->foreign_keys(
     key_columns => { cp_id => 'cp_id' },
   },
 
+  customer => {
+    class       => 'SL::DB::Customer',
+    key_columns => { customer_id => 'id' },
+  },
+
   employee => {
     class       => 'SL::DB::Employee',
     key_columns => { employee_id => 'id' },
@@ -58,6 +50,11 @@ __PACKAGE__->meta->foreign_keys(
     class       => 'SL::DB::Employee',
     key_columns => { salesman_id => 'id' },
   },
+
+  vendor => {
+    class       => 'SL::DB::Vendor',
+    key_columns => { vendor_id => 'id' },
+  },
 );
 
 1;
index 1ef3fb8..d395cfb 100644 (file)
@@ -9,34 +9,21 @@ use parent qw(SL::DB::Object);
 __PACKAGE__->meta->table('letter_draft');
 
 __PACKAGE__->meta->columns(
-  body              => { type => 'text' },
-  close             => { type => 'text' },
-  company_name      => { type => 'text' },
-  cp_id             => { type => 'integer' },
-  date              => { type => 'date' },
-  employee_id       => { type => 'integer' },
-  employee_position => { type => 'text' },
-  greeting          => { type => 'text' },
-  id                => { type => 'integer', not_null => 1, sequence => 'id' },
-  intnotes          => { type => 'text' },
-  itime             => { type => 'timestamp', default => 'now()' },
-  jobnumber         => { type => 'text' },
-  letternumber      => { type => 'text' },
-  mtime             => { type => 'timestamp' },
-  page_created_for  => { type => 'text' },
-  rcv_address       => { type => 'text' },
-  rcv_city          => { type => 'text' },
-  rcv_contact       => { type => 'text' },
-  rcv_country       => { type => 'text' },
-  rcv_countrycode   => { type => 'text' },
-  rcv_name          => { type => 'text' },
-  rcv_zipcode       => { type => 'text' },
-  reference         => { type => 'text' },
-  salesman_id       => { type => 'integer' },
-  salesman_position => { type => 'text' },
-  subject           => { type => 'text' },
-  text_created_for  => { type => 'text' },
-  vc_id             => { type => 'integer', not_null => 1 },
+  body         => { type => 'text' },
+  cp_id        => { type => 'integer' },
+  customer_id  => { type => 'integer' },
+  date         => { type => 'date' },
+  employee_id  => { type => 'integer' },
+  greeting     => { type => 'text' },
+  id           => { type => 'integer', not_null => 1, sequence => 'id' },
+  intnotes     => { type => 'text' },
+  itime        => { type => 'timestamp', default => 'now()' },
+  letternumber => { type => 'text' },
+  mtime        => { type => 'timestamp' },
+  reference    => { type => 'text' },
+  salesman_id  => { type => 'integer' },
+  subject      => { type => 'text' },
+  vendor_id    => { type => 'integer' },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
@@ -49,6 +36,11 @@ __PACKAGE__->meta->foreign_keys(
     key_columns => { cp_id => 'cp_id' },
   },
 
+  customer => {
+    class       => 'SL::DB::Customer',
+    key_columns => { customer_id => 'id' },
+  },
+
   employee => {
     class       => 'SL::DB::Employee',
     key_columns => { employee_id => 'id' },
@@ -58,6 +50,11 @@ __PACKAGE__->meta->foreign_keys(
     class       => 'SL::DB::Employee',
     key_columns => { salesman_id => 'id' },
   },
+
+  vendor => {
+    class       => 'SL::DB::Vendor',
+    key_columns => { vendor_id => 'id' },
+  },
 );
 
 1;
index 6ec58a2..e13461a 100644 (file)
@@ -24,5 +24,12 @@ __PACKAGE__->meta->primary_key_columns([ 'id' ]);
 
 __PACKAGE__->meta->allow_inline_column_values(1);
 
+__PACKAGE__->meta->foreign_keys(
+  vendor => {
+    class       => 'SL::DB::Vendor',
+    key_columns => { make => 'id' },
+  },
+);
+
 1;
 ;
diff --git a/SL/DB/MetaSetup/MebilMapping.pm b/SL/DB/MetaSetup/MebilMapping.pm
new file mode 100644 (file)
index 0000000..a68fc4c
--- /dev/null
@@ -0,0 +1,22 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::MebilMapping;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('mebil_mapping');
+
+__PACKAGE__->meta->columns(
+  fromacc  => { type => 'varchar', length => 200, not_null => 1 },
+  id       => { type => 'serial', not_null => 1 },
+  ordering => { type => 'integer', not_null => 1 },
+  toacc    => { type => 'varchar', length => 200, not_null => 1 },
+  typ      => { type => 'character', length => 1, not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+1;
+;
index b359c07..5dda642 100644 (file)
@@ -10,6 +10,7 @@ __PACKAGE__->meta->table('oe');
 
 __PACKAGE__->meta->columns(
   amount                  => { type => 'numeric', precision => 15, scale => 5 },
+  billing_address_id      => { type => 'integer' },
   closed                  => { type => 'boolean', default => 'false' },
   cp_id                   => { type => 'integer' },
   currency_id             => { type => 'integer', not_null => 1 },
@@ -21,6 +22,7 @@ __PACKAGE__->meta->columns(
   delivery_vendor_id      => { type => 'integer' },
   department_id           => { type => 'integer' },
   employee_id             => { type => 'integer' },
+  exchangerate            => { type => 'numeric', precision => 15, scale => 5 },
   expected_billing_date   => { type => 'date' },
   globalproject_id        => { type => 'integer' },
   id                      => { type => 'integer', not_null => 1, sequence => 'id' },
@@ -43,6 +45,7 @@ __PACKAGE__->meta->columns(
   shippingpoint           => { type => 'text' },
   shipto_id               => { type => 'integer' },
   shipvia                 => { type => 'text' },
+  tax_point               => { type => 'date' },
   taxincluded             => { type => 'boolean' },
   taxzone_id              => { type => 'integer', not_null => 1 },
   transaction_description => { type => 'text' },
@@ -55,6 +58,11 @@ __PACKAGE__->meta->primary_key_columns([ 'id' ]);
 __PACKAGE__->meta->allow_inline_column_values(1);
 
 __PACKAGE__->meta->foreign_keys(
+  billing_address => {
+    class       => 'SL::DB::AdditionalBillingAddress',
+    key_columns => { billing_address_id => 'id' },
+  },
+
   contact => {
     class       => 'SL::DB::Contact',
     key_columns => { cp_id => 'cp_id' },
index 9de8b7e..e83b65b 100644 (file)
@@ -11,10 +11,10 @@ __PACKAGE__->meta->table('orderitems');
 __PACKAGE__->meta->columns(
   active_discount_source => { type => 'text', default => '', not_null => 1 },
   active_price_source    => { type => 'text', default => '', not_null => 1 },
-  base_qty               => { type => 'float', scale => 4 },
+  base_qty               => { type => 'float', precision => 4, scale => 4 },
   cusordnumber           => { type => 'text' },
   description            => { type => 'text' },
-  discount               => { type => 'float', scale => 4 },
+  discount               => { type => 'float', precision => 4, scale => 4 },
   id                     => { type => 'integer', not_null => 1, sequence => 'orderitemsid' },
   itime                  => { type => 'timestamp', default => 'now()' },
   lastcost               => { type => 'numeric', precision => 15, scale => 5 },
@@ -23,6 +23,7 @@ __PACKAGE__->meta->columns(
   marge_price_factor     => { type => 'numeric', default => 1, precision => 15, scale => 5 },
   marge_total            => { type => 'numeric', precision => 15, scale => 5 },
   mtime                  => { type => 'timestamp' },
+  optional               => { type => 'boolean', default => 'false' },
   ordnumber              => { type => 'text' },
   parts_id               => { type => 'integer' },
   position               => { type => 'integer', not_null => 1 },
@@ -30,11 +31,11 @@ __PACKAGE__->meta->columns(
   price_factor_id        => { type => 'integer' },
   pricegroup_id          => { type => 'integer' },
   project_id             => { type => 'integer' },
-  qty                    => { type => 'float', scale => 4 },
+  qty                    => { type => 'numeric', precision => 25, scale => 5 },
   reqdate                => { type => 'date' },
   sellprice              => { type => 'numeric', precision => 15, scale => 5 },
   serialnumber           => { type => 'text' },
-  ship                   => { type => 'float', scale => 4 },
+  ship                   => { type => 'float', precision => 4, scale => 4 },
   subtotal               => { type => 'boolean', default => 'false' },
   trans_id               => { type => 'integer' },
   transdate              => { type => 'text' },
index 2008fef..ef6ed69 100644 (file)
@@ -9,22 +9,18 @@ use parent qw(SL::DB::Object);
 __PACKAGE__->meta->table('parts');
 
 __PACKAGE__->meta->columns(
-  alternate          => { type => 'boolean', default => 'false' },
-  assembly           => { type => 'boolean', default => 'false' },
   bin_id             => { type => 'integer' },
   bom                => { type => 'boolean', default => 'false' },
   buchungsgruppen_id => { type => 'integer' },
+  classification_id  => { type => 'integer', default => '0' },
   description        => { type => 'text' },
   drawing            => { type => 'text' },
   ean                => { type => 'text' },
-  expense_accno_id   => { type => 'integer' },
   formel             => { type => 'text' },
   gv                 => { type => 'numeric', precision => 15, scale => 5 },
   has_sernumber      => { type => 'boolean', default => 'false' },
   id                 => { type => 'integer', not_null => 1, sequence => 'id' },
   image              => { type => 'text' },
-  income_accno_id    => { type => 'integer' },
-  inventory_accno_id => { type => 'integer' },
   itime              => { type => 'timestamp', default => 'now()' },
   lastcost           => { type => 'numeric', precision => 15, scale => 5 },
   listprice          => { type => 'numeric', precision => 15, scale => 5 },
@@ -35,19 +31,20 @@ __PACKAGE__->meta->columns(
   notes              => { type => 'text' },
   obsolete           => { type => 'boolean', default => 'false' },
   onhand             => { type => 'numeric', default => '0', precision => 25, scale => 5 },
+  part_type          => { type => 'enum', check_in => [ 'part', 'service', 'assembly', 'assortment' ], db_type => 'part_type_enum', not_null => 1 },
   partnumber         => { type => 'text', not_null => 1 },
   partsgroup_id      => { type => 'integer' },
   payment_id         => { type => 'integer' },
   price_factor_id    => { type => 'integer' },
   priceupdate        => { type => 'date', default => 'now' },
-  rop                => { type => 'float', scale => 4 },
+  rop                => { type => 'float', precision => 4, scale => 4 },
   sellprice          => { type => 'numeric', precision => 15, scale => 5 },
   shop               => { type => 'boolean', default => 'false' },
   stockable          => { type => 'boolean', default => 'false' },
   unit               => { type => 'varchar', length => 20, not_null => 1 },
   ve                 => { type => 'integer' },
   warehouse_id       => { type => 'integer' },
-  weight             => { type => 'float', scale => 4 },
+  weight             => { type => 'float', precision => 4, scale => 4 },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
@@ -67,6 +64,11 @@ __PACKAGE__->meta->foreign_keys(
     key_columns => { buchungsgruppen_id => 'id' },
   },
 
+  classification => {
+    class       => 'SL::DB::PartClassification',
+    key_columns => { classification_id => 'id' },
+  },
+
   partsgroup => {
     class       => 'SL::DB::PartsGroup',
     key_columns => { partsgroup_id => 'id' },
diff --git a/SL/DB/MetaSetup/PartClassification.pm b/SL/DB/MetaSetup/PartClassification.pm
new file mode 100644 (file)
index 0000000..4fbd022
--- /dev/null
@@ -0,0 +1,23 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::PartClassification;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('part_classifications');
+
+__PACKAGE__->meta->columns(
+  abbreviation      => { type => 'text' },
+  description       => { type => 'text' },
+  id                => { type => 'serial', not_null => 1 },
+  report_separate   => { type => 'boolean', default => 'false', not_null => 1 },
+  used_for_purchase => { type => 'boolean', default => 'true', not_null => 1 },
+  used_for_sale     => { type => 'boolean', default => 'true', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/PartCustomerPrice.pm b/SL/DB/MetaSetup/PartCustomerPrice.pm
new file mode 100644 (file)
index 0000000..c3d4243
--- /dev/null
@@ -0,0 +1,38 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::PartCustomerPrice;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('part_customer_prices');
+
+__PACKAGE__->meta->columns(
+  customer_id         => { type => 'integer', not_null => 1 },
+  customer_partnumber => { type => 'text', default => '' },
+  id                  => { type => 'serial', not_null => 1 },
+  lastupdate          => { type => 'date', default => 'now()' },
+  parts_id            => { type => 'integer', not_null => 1 },
+  price               => { type => 'numeric', default => '0', precision => 15, scale => 5 },
+  sortorder           => { type => 'integer', default => '0' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+__PACKAGE__->meta->foreign_keys(
+  customer => {
+    class       => 'SL::DB::Customer',
+    key_columns => { customer_id => 'id' },
+  },
+
+  parts => {
+    class       => 'SL::DB::Part',
+    key_columns => { parts_id => 'id' },
+  },
+);
+
+1;
+;
index f9915d5..1e3ca3e 100644 (file)
@@ -12,7 +12,9 @@ __PACKAGE__->meta->columns(
   id         => { type => 'integer', not_null => 1, sequence => 'id' },
   itime      => { type => 'timestamp', default => 'now()' },
   mtime      => { type => 'timestamp' },
+  obsolete   => { type => 'boolean', default => 'false' },
   partsgroup => { type => 'text' },
+  sortkey    => { type => 'integer', not_null => 1 },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
diff --git a/SL/DB/MetaSetup/PartsPriceHistory.pm b/SL/DB/MetaSetup/PartsPriceHistory.pm
new file mode 100644 (file)
index 0000000..7d21cd3
--- /dev/null
@@ -0,0 +1,30 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::PartsPriceHistory;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('parts_price_history');
+
+__PACKAGE__->meta->columns(
+  id         => { type => 'serial', not_null => 1 },
+  lastcost   => { type => 'numeric', precision => 15, scale => 5 },
+  listprice  => { type => 'numeric', precision => 15, scale => 5 },
+  part_id    => { type => 'integer', not_null => 1 },
+  sellprice  => { type => 'numeric', precision => 15, scale => 5 },
+  valid_from => { type => 'timestamp', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->foreign_keys(
+  part => {
+    class       => 'SL::DB::Part',
+    key_columns => { part_id => 'id' },
+  },
+);
+
+1;
+;
index bc0587e..876474e 100644 (file)
@@ -9,17 +9,18 @@ use parent qw(SL::DB::Object);
 __PACKAGE__->meta->table('payment_terms');
 
 __PACKAGE__->meta->columns(
-  auto_calculation => { type => 'boolean', not_null => 1 },
-  description      => { type => 'text' },
-  description_long => { type => 'text' },
-  id               => { type => 'integer', not_null => 1, sequence => 'id' },
-  itime            => { type => 'timestamp', default => 'now()' },
-  mtime            => { type => 'timestamp' },
-  percent_skonto   => { type => 'float', scale => 4 },
-  ranking          => { type => 'integer' },
-  sortkey          => { type => 'integer', not_null => 1 },
-  terms_netto      => { type => 'integer' },
-  terms_skonto     => { type => 'integer' },
+  auto_calculation         => { type => 'boolean', not_null => 1 },
+  description              => { type => 'text' },
+  description_long         => { type => 'text' },
+  description_long_invoice => { type => 'text' },
+  id                       => { type => 'integer', not_null => 1, sequence => 'id' },
+  itime                    => { type => 'timestamp', default => 'now()' },
+  mtime                    => { type => 'timestamp' },
+  obsolete                 => { type => 'boolean', default => 'false' },
+  percent_skonto           => { type => 'float', precision => 4, scale => 4 },
+  sortkey                  => { type => 'integer', not_null => 1 },
+  terms_netto              => { type => 'integer' },
+  terms_skonto             => { type => 'integer' },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
index 0c05453..9cb9998 100644 (file)
@@ -9,21 +9,27 @@ use parent qw(SL::DB::Object);
 __PACKAGE__->meta->table('periodic_invoices_configs');
 
 __PACKAGE__->meta->columns(
-  active                  => { type => 'boolean', default => 'true' },
-  ar_chart_id             => { type => 'integer', not_null => 1 },
-  copies                  => { type => 'integer' },
-  direct_debit            => { type => 'boolean', default => 'false', not_null => 1 },
-  end_date                => { type => 'date' },
-  extend_automatically_by => { type => 'integer' },
-  first_billing_date      => { type => 'date' },
-  id                      => { type => 'integer', not_null => 1, sequence => 'id' },
-  oe_id                   => { type => 'integer', not_null => 1 },
-  order_value_periodicity => { type => 'varchar', length => 1, not_null => 1 },
-  periodicity             => { type => 'varchar', length => 1, not_null => 1 },
-  print                   => { type => 'boolean', default => 'false' },
-  printer_id              => { type => 'integer' },
-  start_date              => { type => 'date' },
-  terminated              => { type => 'boolean', default => 'false' },
+  active                     => { type => 'boolean', default => 'true' },
+  ar_chart_id                => { type => 'integer', not_null => 1 },
+  copies                     => { type => 'integer' },
+  direct_debit               => { type => 'boolean', default => 'false', not_null => 1 },
+  email_body                 => { type => 'text' },
+  email_recipient_address    => { type => 'text' },
+  email_recipient_contact_id => { type => 'integer' },
+  email_sender               => { type => 'text' },
+  email_subject              => { type => 'text' },
+  end_date                   => { type => 'date' },
+  extend_automatically_by    => { type => 'integer' },
+  first_billing_date         => { type => 'date' },
+  id                         => { type => 'integer', not_null => 1, sequence => 'id' },
+  oe_id                      => { type => 'integer', not_null => 1 },
+  order_value_periodicity    => { type => 'varchar', length => 1, not_null => 1 },
+  periodicity                => { type => 'varchar', length => 1, not_null => 1 },
+  print                      => { type => 'boolean', default => 'false' },
+  printer_id                 => { type => 'integer' },
+  send_email                 => { type => 'boolean', default => 'false', not_null => 1 },
+  start_date                 => { type => 'date' },
+  terminated                 => { type => 'boolean', default => 'false' },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
@@ -34,6 +40,11 @@ __PACKAGE__->meta->foreign_keys(
     key_columns => { ar_chart_id => 'id' },
   },
 
+  email_recipient_contact => {
+    class       => 'SL::DB::Contact',
+    key_columns => { email_recipient_contact_id => 'cp_id' },
+  },
+
   order => {
     class       => 'SL::DB::Order',
     key_columns => { oe_id => 'id' },
index 29c0de3..a0c2639 100644 (file)
@@ -10,13 +10,15 @@ __PACKAGE__->meta->table('prices');
 
 __PACKAGE__->meta->columns(
   id            => { type => 'serial', not_null => 1 },
-  parts_id      => { type => 'integer' },
+  parts_id      => { type => 'integer', not_null => 1 },
   price         => { type => 'numeric', precision => 15, scale => 5 },
-  pricegroup_id => { type => 'integer' },
+  pricegroup_id => { type => 'integer', not_null => 1 },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
 
+__PACKAGE__->meta->unique_keys([ 'parts_id', 'pricegroup_id' ]);
+
 __PACKAGE__->meta->foreign_keys(
   parts => {
     class       => 'SL::DB::Part',
index 60a7f20..0f35952 100644 (file)
@@ -10,7 +10,9 @@ __PACKAGE__->meta->table('pricegroup');
 
 __PACKAGE__->meta->columns(
   id         => { type => 'integer', not_null => 1, sequence => 'id' },
+  obsolete   => { type => 'boolean', default => 'false' },
   pricegroup => { type => 'text', not_null => 1 },
+  sortkey    => { type => 'integer', not_null => 1 },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
index c9c74f0..4a443ac 100644 (file)
@@ -39,6 +39,7 @@ __PACKAGE__->meta->columns(
   shipvia                 => { type => 'text' },
   storno                  => { type => 'boolean', default => 'false' },
   storno_id               => { type => 'integer' },
+  tax_point               => { type => 'date' },
   taxincluded             => { type => 'boolean', default => 'false' },
   taxzone_id              => { type => 'integer', not_null => 1 },
   transaction_description => { type => 'text' },
diff --git a/SL/DB/MetaSetup/RecordTemplate.pm b/SL/DB/MetaSetup/RecordTemplate.pm
new file mode 100644 (file)
index 0000000..374f00c
--- /dev/null
@@ -0,0 +1,84 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::RecordTemplate;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('record_templates');
+
+__PACKAGE__->meta->columns(
+  ar_ap_chart_id          => { type => 'integer' },
+  cb_transaction          => { type => 'boolean', default => 'false', not_null => 1 },
+  currency_id             => { type => 'integer', not_null => 1 },
+  customer_id             => { type => 'integer' },
+  department_id           => { type => 'integer' },
+  description             => { type => 'text' },
+  direct_debit            => { type => 'boolean', default => 'false', not_null => 1 },
+  employee_id             => { type => 'integer' },
+  id                      => { type => 'serial', not_null => 1 },
+  itime                   => { type => 'timestamp', default => 'now()', not_null => 1 },
+  mtime                   => { type => 'timestamp', default => 'now()', not_null => 1 },
+  notes                   => { type => 'text' },
+  ob_transaction          => { type => 'boolean', default => 'false', not_null => 1 },
+  ordnumber               => { type => 'text' },
+  payment_id              => { type => 'integer' },
+  project_id              => { type => 'integer' },
+  reference               => { type => 'text' },
+  show_details            => { type => 'boolean', default => 'false', not_null => 1 },
+  taxincluded             => { type => 'boolean', default => 'false', not_null => 1 },
+  template_name           => { type => 'text', not_null => 1 },
+  template_type           => { type => 'enum', check_in => [ 'ar_transaction', 'ap_transaction', 'gl_transaction' ], db_type => 'record_template_type', not_null => 1 },
+  transaction_description => { type => 'text' },
+  vendor_id               => { type => 'integer' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+__PACKAGE__->meta->foreign_keys(
+  ar_ap_chart => {
+    class       => 'SL::DB::Chart',
+    key_columns => { ar_ap_chart_id => 'id' },
+  },
+
+  currency => {
+    class       => 'SL::DB::Currency',
+    key_columns => { currency_id => 'id' },
+  },
+
+  customer => {
+    class       => 'SL::DB::Customer',
+    key_columns => { customer_id => 'id' },
+  },
+
+  department => {
+    class       => 'SL::DB::Department',
+    key_columns => { department_id => 'id' },
+  },
+
+  employee => {
+    class       => 'SL::DB::Employee',
+    key_columns => { employee_id => 'id' },
+  },
+
+  payment => {
+    class       => 'SL::DB::PaymentTerm',
+    key_columns => { payment_id => 'id' },
+  },
+
+  project => {
+    class       => 'SL::DB::Project',
+    key_columns => { project_id => 'id' },
+  },
+
+  vendor => {
+    class       => 'SL::DB::Vendor',
+    key_columns => { vendor_id => 'id' },
+  },
+);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/RecordTemplateItem.pm b/SL/DB/MetaSetup/RecordTemplateItem.pm
new file mode 100644 (file)
index 0000000..538ac01
--- /dev/null
@@ -0,0 +1,48 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::RecordTemplateItem;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('record_template_items');
+
+__PACKAGE__->meta->columns(
+  amount1            => { type => 'numeric', not_null => 1, precision => 15, scale => 5 },
+  amount2            => { type => 'numeric', precision => 15, scale => 5 },
+  chart_id           => { type => 'integer', not_null => 1 },
+  id                 => { type => 'serial', not_null => 1 },
+  memo               => { type => 'text' },
+  project_id         => { type => 'integer' },
+  record_template_id => { type => 'integer', not_null => 1 },
+  source             => { type => 'text' },
+  tax_id             => { type => 'integer', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->foreign_keys(
+  chart => {
+    class       => 'SL::DB::Chart',
+    key_columns => { chart_id => 'id' },
+  },
+
+  project => {
+    class       => 'SL::DB::Project',
+    key_columns => { project_id => 'id' },
+  },
+
+  record_template => {
+    class       => 'SL::DB::RecordTemplate',
+    key_columns => { record_template_id => 'id' },
+  },
+
+  tax => {
+    class       => 'SL::DB::Tax',
+    key_columns => { tax_id => 'id' },
+  },
+);
+
+1;
+;
index 07bac07..50c167d 100644 (file)
@@ -24,6 +24,7 @@ __PACKAGE__->meta->columns(
   position             => { type => 'integer', not_null => 1 },
   requirement_spec_id  => { type => 'integer', not_null => 1 },
   risk_id              => { type => 'integer' },
+  sellprice_factor     => { type => 'numeric', default => 1, precision => 10, scale => 5 },
   time_estimation      => { type => 'numeric', default => '0', not_null => 1, precision => 12, scale => 2 },
   title                => { type => 'text' },
 );
index c0cd5d7..b24588a 100644 (file)
@@ -21,7 +21,7 @@ __PACKAGE__->meta->columns(
   our_depositor                => { type => 'text' },
   our_iban                     => { type => 'varchar', length => 100 },
   payment_type                 => { type => 'text', default => 'without_skonto' },
-  reference                    => { type => 'varchar', length => 35 },
+  reference                    => { type => 'varchar', length => 140 },
   requested_execution_date     => { type => 'date' },
   sepa_export_id               => { type => 'integer', not_null => 1 },
   skonto_amount                => { type => 'numeric', precision => 25, scale => 5 },
index beab032..57b4971 100644 (file)
@@ -21,6 +21,7 @@ __PACKAGE__->meta->columns(
   shiptodepartment_2 => { type => 'text' },
   shiptoemail        => { type => 'text' },
   shiptofax          => { type => 'text' },
+  shiptogln          => { type => 'text' },
   shiptoname         => { type => 'text' },
   shiptophone        => { type => 'text' },
   shiptostreet       => { type => 'text' },
diff --git a/SL/DB/MetaSetup/Shop.pm b/SL/DB/MetaSetup/Shop.pm
new file mode 100644 (file)
index 0000000..adb6b3a
--- /dev/null
@@ -0,0 +1,42 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::Shop;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('shops');
+
+__PACKAGE__->meta->columns(
+  connector                => { type => 'text' },
+  description              => { type => 'text' },
+  id                       => { type => 'serial', not_null => 1 },
+  itime                    => { type => 'timestamp', default => 'now()' },
+  last_order_number        => { type => 'integer' },
+  login                    => { type => 'text' },
+  mtime                    => { type => 'timestamp', default => 'now()' },
+  obsolete                 => { type => 'boolean', default => 'false', not_null => 1 },
+  orders_to_fetch          => { type => 'integer' },
+  password                 => { type => 'text' },
+  path                     => { type => 'text', default => '/', not_null => 1 },
+  port                     => { type => 'integer' },
+  price_source             => { type => 'text' },
+  pricetype                => { type => 'text' },
+  protocol                 => { type => 'text', default => 'http', not_null => 1 },
+  proxy                    => { type => 'text', default => '' },
+  realm                    => { type => 'text' },
+  server                   => { type => 'text' },
+  shipping_costs_parts_id  => { type => 'integer' },
+  sortkey                  => { type => 'integer' },
+  taxzone_id               => { type => 'integer' },
+  transaction_description  => { type => 'text' },
+  use_part_longdescription => { type => 'boolean', default => 'false' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/ShopImage.pm b/SL/DB/MetaSetup/ShopImage.pm
new file mode 100644 (file)
index 0000000..3182840
--- /dev/null
@@ -0,0 +1,36 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::ShopImage;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('shop_images');
+
+__PACKAGE__->meta->columns(
+  file_id                => { type => 'integer' },
+  id                     => { type => 'serial', not_null => 1 },
+  itime                  => { type => 'timestamp', default => 'now()' },
+  mtime                  => { type => 'timestamp' },
+  object_id              => { type => 'text', not_null => 1 },
+  org_file_height        => { type => 'integer' },
+  org_file_width         => { type => 'integer' },
+  position               => { type => 'integer' },
+  thumbnail_content      => { type => 'bytea' },
+  thumbnail_content_type => { type => 'text' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+__PACKAGE__->meta->foreign_keys(
+  file => {
+    class       => 'SL::DB::File',
+    key_columns => { file_id => 'id' },
+  },
+);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/ShopOrder.pm b/SL/DB/MetaSetup/ShopOrder.pm
new file mode 100644 (file)
index 0000000..6b685fa
--- /dev/null
@@ -0,0 +1,103 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::ShopOrder;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('shop_orders');
+
+__PACKAGE__->meta->columns(
+  amount                 => { type => 'numeric', precision => 15, scale => 5 },
+  billing_city           => { type => 'text' },
+  billing_company        => { type => 'text' },
+  billing_country        => { type => 'text' },
+  billing_department     => { type => 'text' },
+  billing_email          => { type => 'text' },
+  billing_fax            => { type => 'text' },
+  billing_firstname      => { type => 'text' },
+  billing_greeting       => { type => 'text' },
+  billing_lastname       => { type => 'text' },
+  billing_phone          => { type => 'text' },
+  billing_street         => { type => 'text' },
+  billing_vat            => { type => 'text' },
+  billing_zipcode        => { type => 'text' },
+  customer_city          => { type => 'text' },
+  customer_company       => { type => 'text' },
+  customer_country       => { type => 'text' },
+  customer_department    => { type => 'text' },
+  customer_email         => { type => 'text' },
+  customer_fax           => { type => 'text' },
+  customer_firstname     => { type => 'text' },
+  customer_greeting      => { type => 'text' },
+  customer_lastname      => { type => 'text' },
+  customer_newsletter    => { type => 'boolean' },
+  customer_phone         => { type => 'text' },
+  customer_street        => { type => 'text' },
+  customer_vat           => { type => 'text' },
+  customer_zipcode       => { type => 'text' },
+  delivery_city          => { type => 'text' },
+  delivery_company       => { type => 'text' },
+  delivery_country       => { type => 'text' },
+  delivery_department    => { type => 'text' },
+  delivery_email         => { type => 'text' },
+  delivery_fax           => { type => 'text' },
+  delivery_firstname     => { type => 'text' },
+  delivery_greeting      => { type => 'text' },
+  delivery_lastname      => { type => 'text' },
+  delivery_phone         => { type => 'text' },
+  delivery_street        => { type => 'text' },
+  delivery_vat           => { type => 'text' },
+  delivery_zipcode       => { type => 'text' },
+  host                   => { type => 'text' },
+  id                     => { type => 'serial', not_null => 1 },
+  itime                  => { type => 'timestamp', default => 'now()' },
+  kivi_customer_id       => { type => 'integer' },
+  mtime                  => { type => 'timestamp' },
+  netamount              => { type => 'numeric', precision => 15, scale => 5 },
+  obsolete               => { type => 'boolean', default => 'false', not_null => 1 },
+  order_date             => { type => 'timestamp' },
+  payment_description    => { type => 'text' },
+  payment_id             => { type => 'integer' },
+  positions              => { type => 'integer' },
+  remote_ip              => { type => 'text' },
+  sepa_account_holder    => { type => 'text' },
+  sepa_bic               => { type => 'text' },
+  sepa_iban              => { type => 'text' },
+  shipping_costs         => { type => 'numeric', precision => 15, scale => 5 },
+  shipping_costs_id      => { type => 'integer' },
+  shipping_costs_net     => { type => 'numeric', precision => 15, scale => 5 },
+  shop_c_billing_id      => { type => 'integer' },
+  shop_c_billing_number  => { type => 'text' },
+  shop_c_delivery_id     => { type => 'integer' },
+  shop_c_delivery_number => { type => 'text' },
+  shop_customer_comment  => { type => 'text' },
+  shop_customer_id       => { type => 'integer' },
+  shop_customer_number   => { type => 'text' },
+  shop_id                => { type => 'integer' },
+  shop_ordernumber       => { type => 'text' },
+  shop_trans_id          => { type => 'text', not_null => 1 },
+  tax_included           => { type => 'boolean' },
+  transfer_date          => { type => 'date' },
+  transferred            => { type => 'boolean', default => 'false' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+__PACKAGE__->meta->foreign_keys(
+  kivi_customer => {
+    class       => 'SL::DB::Customer',
+    key_columns => { kivi_customer_id => 'id' },
+  },
+
+  shop => {
+    class       => 'SL::DB::Shop',
+    key_columns => { shop_id => 'id' },
+  },
+);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/ShopOrderItem.pm b/SL/DB/MetaSetup/ShopOrderItem.pm
new file mode 100644 (file)
index 0000000..ae7d21a
--- /dev/null
@@ -0,0 +1,34 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::ShopOrderItem;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('shop_order_items');
+
+__PACKAGE__->meta->columns(
+  active_price_source => { type => 'text' },
+  description         => { type => 'text' },
+  id                  => { type => 'serial', not_null => 1 },
+  partnumber          => { type => 'text' },
+  position            => { type => 'integer' },
+  price               => { type => 'numeric', precision => 15, scale => 5 },
+  quantity            => { type => 'numeric', precision => 25, scale => 5 },
+  shop_order_id       => { type => 'integer' },
+  shop_trans_id       => { type => 'text', not_null => 1 },
+  tax_rate            => { type => 'numeric', precision => 15, scale => 2 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->foreign_keys(
+  shop_order => {
+    class       => 'SL::DB::ShopOrder',
+    key_columns => { shop_order_id => 'id' },
+  },
+);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/ShopPart.pm b/SL/DB/MetaSetup/ShopPart.pm
new file mode 100644 (file)
index 0000000..4dcf159
--- /dev/null
@@ -0,0 +1,49 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::ShopPart;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('shop_parts');
+
+__PACKAGE__->meta->columns(
+  active              => { type => 'boolean', default => 'false', not_null => 1 },
+  active_price_source => { type => 'text' },
+  front_page          => { type => 'boolean', default => 'false', not_null => 1 },
+  id                  => { type => 'serial', not_null => 1 },
+  itime               => { type => 'timestamp', default => 'now()' },
+  last_update         => { type => 'timestamp' },
+  metatag_description => { type => 'text' },
+  metatag_keywords    => { type => 'text' },
+  metatag_title       => { type => 'text' },
+  mtime               => { type => 'timestamp' },
+  part_id             => { type => 'integer', not_null => 1 },
+  shop_category       => { type => 'array' },
+  shop_description    => { type => 'text' },
+  shop_id             => { type => 'integer', not_null => 1 },
+  show_date           => { type => 'date' },
+  sortorder           => { type => 'integer' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->unique_keys([ 'shop_id', 'part_id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+__PACKAGE__->meta->foreign_keys(
+  part => {
+    class       => 'SL::DB::Part',
+    key_columns => { part_id => 'id' },
+  },
+
+  shop => {
+    class       => 'SL::DB::Shop',
+    key_columns => { shop_id => 'id' },
+  },
+);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/Stocktaking.pm b/SL/DB/MetaSetup/Stocktaking.pm
new file mode 100644 (file)
index 0000000..b00b7d2
--- /dev/null
@@ -0,0 +1,59 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::Stocktaking;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('stocktakings');
+
+__PACKAGE__->meta->columns(
+  bestbefore   => { type => 'date' },
+  bin_id       => { type => 'integer', not_null => 1 },
+  chargenumber => { type => 'text', default => '', not_null => 1 },
+  comment      => { type => 'text' },
+  cutoff_date  => { type => 'date', not_null => 1 },
+  employee_id  => { type => 'integer', not_null => 1 },
+  id           => { type => 'integer', not_null => 1, sequence => 'id' },
+  inventory_id => { type => 'integer' },
+  itime        => { type => 'timestamp', default => 'now()' },
+  mtime        => { type => 'timestamp' },
+  parts_id     => { type => 'integer', not_null => 1 },
+  qty          => { type => 'numeric', not_null => 1, precision => 25, scale => 5 },
+  warehouse_id => { type => 'integer', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+__PACKAGE__->meta->foreign_keys(
+  bin => {
+    class       => 'SL::DB::Bin',
+    key_columns => { bin_id => 'id' },
+  },
+
+  employee => {
+    class       => 'SL::DB::Employee',
+    key_columns => { employee_id => 'id' },
+  },
+
+  inventory => {
+    class       => 'SL::DB::Inventory',
+    key_columns => { inventory_id => 'id' },
+  },
+
+  parts => {
+    class       => 'SL::DB::Part',
+    key_columns => { parts_id => 'id' },
+  },
+
+  warehouse => {
+    class       => 'SL::DB::Warehouse',
+    key_columns => { warehouse_id => 'id' },
+  },
+);
+
+1;
+;
index f4a2e2d..c29a455 100644 (file)
@@ -15,11 +15,11 @@ __PACKAGE__->meta->columns(
   itime                    => { type => 'timestamp', default => 'now()' },
   mtime                    => { type => 'timestamp' },
   rate                     => { type => 'numeric', default => '0', not_null => 1, precision => 15, scale => 5 },
+  reverse_charge_chart_id  => { type => 'integer' },
   skonto_purchase_chart_id => { type => 'integer' },
   skonto_sales_chart_id    => { type => 'integer' },
   taxdescription           => { type => 'text', not_null => 1 },
   taxkey                   => { type => 'integer', not_null => 1 },
-  taxnumber                => { type => 'text' },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
diff --git a/SL/DB/MetaSetup/TimeRecording.pm b/SL/DB/MetaSetup/TimeRecording.pm
new file mode 100644 (file)
index 0000000..a500131
--- /dev/null
@@ -0,0 +1,67 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::TimeRecording;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('time_recordings');
+
+__PACKAGE__->meta->columns(
+  booked          => { type => 'boolean', default => 'false' },
+  customer_id     => { type => 'integer', not_null => 1 },
+  date            => { type => 'date', not_null => 1 },
+  description     => { type => 'text', not_null => 1 },
+  duration        => { type => 'integer' },
+  employee_id     => { type => 'integer', not_null => 1 },
+  end_time        => { type => 'timestamp' },
+  id              => { type => 'serial', not_null => 1 },
+  itime           => { type => 'timestamp', default => 'now()', not_null => 1 },
+  mtime           => { type => 'timestamp', default => 'now()', not_null => 1 },
+  order_id        => { type => 'integer' },
+  part_id         => { type => 'integer' },
+  payroll         => { type => 'boolean', default => 'false' },
+  project_id      => { type => 'integer' },
+  staff_member_id => { type => 'integer', not_null => 1 },
+  start_time      => { type => 'timestamp' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+__PACKAGE__->meta->foreign_keys(
+  customer => {
+    class       => 'SL::DB::Customer',
+    key_columns => { customer_id => 'id' },
+  },
+
+  employee => {
+    class       => 'SL::DB::Employee',
+    key_columns => { employee_id => 'id' },
+  },
+
+  order => {
+    class       => 'SL::DB::Order',
+    key_columns => { order_id => 'id' },
+  },
+
+  part => {
+    class       => 'SL::DB::Part',
+    key_columns => { part_id => 'id' },
+  },
+
+  project => {
+    class       => 'SL::DB::Project',
+    key_columns => { project_id => 'id' },
+  },
+
+  staff_member => {
+    class       => 'SL::DB::Employee',
+    key_columns => { staff_member_id => 'id' },
+  },
+);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/TimeRecordingArticle.pm b/SL/DB/MetaSetup/TimeRecordingArticle.pm
new file mode 100644 (file)
index 0000000..5d7bd84
--- /dev/null
@@ -0,0 +1,30 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::TimeRecordingArticle;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('time_recording_articles');
+
+__PACKAGE__->meta->columns(
+  id       => { type => 'serial', not_null => 1 },
+  part_id  => { type => 'integer', not_null => 1 },
+  position => { type => 'integer', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->unique_keys([ 'part_id' ]);
+
+__PACKAGE__->meta->foreign_keys(
+  part => {
+    class       => 'SL::DB::Part',
+    key_columns => { part_id => 'id' },
+    rel_type    => 'one to one',
+  },
+);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/UserPreference.pm b/SL/DB/MetaSetup/UserPreference.pm
new file mode 100644 (file)
index 0000000..9c075d1
--- /dev/null
@@ -0,0 +1,25 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::UserPreference;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('user_preferences');
+
+__PACKAGE__->meta->columns(
+  id        => { type => 'serial', not_null => 1 },
+  key       => { type => 'text', not_null => 1 },
+  login     => { type => 'text', not_null => 1 },
+  namespace => { type => 'text', not_null => 1 },
+  value     => { type => 'text' },
+  version   => { type => 'numeric', precision => 15, scale => 5 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->unique_keys([ 'login', 'namespace', 'version', 'key' ]);
+
+1;
+;
index 54e9d07..a457802 100644 (file)
@@ -26,9 +26,10 @@ __PACKAGE__->meta->columns(
   department_2     => { type => 'text' },
   depositor        => { type => 'text' },
   direct_debit     => { type => 'boolean', default => 'false' },
-  discount         => { type => 'float', scale => 4 },
+  discount         => { type => 'float', precision => 4, scale => 4 },
   email            => { type => 'text' },
   fax              => { type => 'text' },
+  gln              => { type => 'text' },
   greeting         => { type => 'text' },
   homepage         => { type => 'text' },
   iban             => { type => 'text' },
@@ -38,6 +39,7 @@ __PACKAGE__->meta->columns(
   language_id      => { type => 'integer' },
   mtime            => { type => 'timestamp' },
   name             => { type => 'text', not_null => 1 },
+  natural_person   => { type => 'boolean', default => 'false' },
   notes            => { type => 'text' },
   obsolete         => { type => 'boolean', default => 'false' },
   payment_id       => { type => 'integer' },
index c539d6a..477ae3c 100644 (file)
@@ -1,10 +1,9 @@
-# This file has been auto-generated only because it didn't exist.
-# Feel free to modify it at will; it will not be overwritten automatically.
-
 package SL::DB::Note;
 
 use strict;
 
+use Carp;
+
 use SL::DB::MetaSetup::Note;
 
 
@@ -21,5 +20,76 @@ __PACKAGE__->meta->initialize;
 # Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
 __PACKAGE__->meta->make_manager_class;
 
+sub trans_object {
+  my $self = shift;
+
+  croak "Method is not a setter" if @_;
+
+  return undef if !$self->trans_id || !$self->trans_module;
+
+  if ($self->trans_module eq 'fu') {
+    require SL::DB::FollowUp;
+    return SL::DB::Manager::FollowUp->find_by(id => $self->trans_id);
+  }
+
+  if ($self->trans_module eq 'ct') {
+    require SL::DB::Customer;
+    require SL::DB::Vendor;
+    return SL::DB::Manager::Customer->find_by(id => $self->trans_id)
+        || SL::DB::Manager::Vendor  ->find_by(id => $self->trans_id);
+  }
+
+  return undef;
+}
 
 1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::Note - Notes
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<trans_object>
+
+A note object is always attached to another database entity. Which one
+is determined by the columns C<trans_module> and C<trans_id>. This
+function looks at both, retrieves the corresponding object from the
+database and returns it.
+
+Currently the following three types are supported:
+
+=over 2
+
+=item * C<SL::DB::FollowUp> for C<trans_module == 'fu'>
+
+=item * C<SL::DB::Customer> or C<SL::DB::Vendor> for C<trans_module ==
+'ct'> (which class is used depends on the value of C<trans_id>;
+customers are looked up first)
+
+=back
+
+The method returns C<undef> in three cases: if no C<trans_id> or no
+C<trans_module> has been assigned yet; if C<trans_module> is unknown;
+if the referenced object doesn't exist.
+
+This method is a getter only, not a setter.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
index 81ef978..89d15c6 100755 (executable)
@@ -5,12 +5,15 @@ use strict;
 use Carp;
 use English qw(-no_match_vars);
 use Rose::DB::Object;
-use List::MoreUtils qw(any);
+use Rose::DB::Object::Constants qw();
+use List::MoreUtils qw(any pairwise);
+use List::Util qw(first);
 
 use SL::DB;
 use SL::DB::Helper::Attr;
 use SL::DB::Helper::Metadata;
 use SL::DB::Helper::Manager;
+use SL::DB::Helper::Presenter;
 use SL::DB::Object::Hooks;
 
 use base qw(Rose::DB::Object);
@@ -20,6 +23,13 @@ my @rose_reserved_methods = qw(
   not_found save update import
 );
 
+my %db_to_presenter_mapping = (
+  Customer        => 'CustomerVendor',
+  PurchaseInvoice => 'Invoice',
+  Vendor          => 'CustomerVendor',
+  GLTransaction   => 'GL',
+);
+
 sub new {
   my $class = shift;
   my $self  = $class->SUPER::new();
@@ -96,6 +106,50 @@ sub update_attributes {
   return $self;
 }
 
+sub update_collection {
+  my ($self, $attribute, $entries) = @_;
+
+  my $self_primary_key = "" . ($self->meta->primary_key_columns)[0];
+
+  croak "\$self hasn't been saved yet" if !$self->$self_primary_key;
+
+  my $relationship = first { $_->name eq $attribute } @{ $self->meta->relationships };
+
+  croak "No relationship found for attribute '$attribute'" if !$relationship;
+
+  my @primary_key_columns = $relationship->class->meta->primary_key_columns;
+
+  croak "Classes with multiple primary key columns are not supported" if scalar(@primary_key_columns) > 1;
+
+  my $class             = $relationship->class;
+  my $manager_class     = "SL::DB::Manager::" . substr($class, 8);
+  my $other_primary_key = "" . $primary_key_columns[0];
+  my $column_map        = $relationship->column_map;
+  my @new_entries       = @{ $entries          // [] };
+  my @existing_entries  = @{ $self->$attribute // [] };
+  my @to_delete         = grep { my $value = $_->$other_primary_key; !any { $_->{$other_primary_key} == $value } @new_entries } @existing_entries;
+
+  $_->delete for @to_delete;
+
+  foreach my $entry (@new_entries) {
+    if (!$entry->{$other_primary_key}) {
+      my $new_instance = $class->new(%{ $entry });
+
+      foreach my $self_attribute (keys %{ $column_map }) {
+        my $other_attribute = $column_map->{$self_attribute};
+        $new_instance->$other_attribute($self->$self_attribute);
+      }
+
+      $new_instance->save;
+
+      next;
+    }
+
+    my $existing = first { $_->$other_primary_key == $entry->{$other_primary_key} } @existing_entries;
+    $existing->update_attributes(%{ $entry }) if $existing;
+  }
+}
+
 sub call_sub {
   my $self = shift;
   my $sub  = shift;
@@ -139,21 +193,15 @@ sub load {
 sub save {
   my ($self, @args) = @_;
 
-  my ($result, $exception);
-  my $worker = sub {
-    $exception = $EVAL_ERROR unless eval {
-      SL::DB::Object::Hooks::run_hooks($self, 'before_save');
-      $result = $self->SUPER::save(@args);
-      SL::DB::Object::Hooks::run_hooks($self, 'after_save', $result);
-      1;
-    };
+  my $result;
 
-    return $result;
-  };
-
-  $self->db->in_transaction ? $worker->() : $self->db->do_transaction($worker);
+  $self->db->with_transaction(sub {
+    SL::DB::Object::Hooks::run_hooks($self, 'before_save');
+    $result = $self->SUPER::save(@args);
+    SL::DB::Object::Hooks::run_hooks($self, 'after_save', $result);
 
-  die $exception if $exception;
+    1;
+  }) || die $self->db->error;
 
   return $result;
 }
@@ -161,21 +209,15 @@ sub save {
 sub delete {
   my ($self, @args) = @_;
 
-  my ($result, $exception);
-  my $worker = sub {
-    $exception = $EVAL_ERROR unless eval {
-      SL::DB::Object::Hooks::run_hooks($self, 'before_delete');
-      $result = $self->SUPER::delete(@args);
-      SL::DB::Object::Hooks::run_hooks($self, 'after_delete', $result);
-      1;
-    };
+  my $result;
 
-    return $result;
-  };
+  $self->db->with_transaction(sub {
+    SL::DB::Object::Hooks::run_hooks($self, 'before_delete');
+    $result = $self->SUPER::delete(@args);
+    SL::DB::Object::Hooks::run_hooks($self, 'after_delete', $result);
 
-  $self->db->in_transaction ? $worker->() : $self->db->do_transaction($worker);
-
-  die $exception if $exception;
+    1;
+  }) || die $self->db->error;
 
   return $result;
 }
@@ -218,6 +260,60 @@ sub invalidate_cached {
   return $class_or_self;
 }
 
+my %_skip_fields_when_cloning = map { ($_ => 1) } qw(itime mtime);
+
+sub clone_and_reset {
+  my($self)               = shift;
+  my $class               = ref $self;
+  my $cloning             = Rose::DB::Object::Constants::STATE_CLONING();
+  local $self->{$cloning} = 1;
+
+  my $meta                = $class->meta;
+  my @accessors           = $meta->column_accessor_method_names;
+  my @mutators            = $meta->column_mutator_method_names;
+  my @column_names        =
+    grep     { $_->[0] && $_->[1] && !$_skip_fields_when_cloning{ $_->[0] } }
+    pairwise { no warnings qw(once); [ $a, $b] } @accessors, @mutators;
+
+  my $clone = $class->new(map { my $method = $_->[0]; ($_->[1] => $self->$method) } @column_names);
+
+  # Blank all primary and unique key columns
+  my @keys = (
+    $meta->primary_key_column_mutator_names,
+    map { my $uk = $_; map { $meta->column_mutator_method_name($_) } ($uk->columns) } ($meta->unique_keys)
+  );
+
+  $clone->$_(undef) for @keys;
+
+  # Also copy db object, if any
+  $clone->db($self->{db}) if $self->{db};
+
+  return $clone;
+}
+
+sub presenter {
+  my ($self) = @_;
+
+  my $class =  ref $self;
+  $class    =~ s{^SL::DB::}{};
+  $class    =  "SL::Presenter::" . ($db_to_presenter_mapping{$class} // $class);
+
+  return SL::DB::Helper::Presenter->new($class, $self);
+}
+
+sub as_debug_info {
+  my ($self) = @_;
+
+  return {
+    map {
+      my $column_name = $_->name;
+      my $value       = $self->$column_name;
+      $value          = !defined($value) ? undef : "${value}";
+      ($_ => $value)
+    } $self->meta->columns
+  };
+}
+
 1;
 
 __END__
@@ -267,6 +363,28 @@ Assigns the attributes from C<%attributes> by calling the
 C<assign_attributes> function and saves the object afterwards. Returns
 the object itself.
 
+=item C<update_collection $attribute, $entries, %params>
+
+Updates a one-to-many relationship named C<$attribute> to match the
+entries in C<$entries>. C<$entries> is supposed to be an array ref of
+hash refs.
+
+For each hash ref in C<$entries> that does not contain a field for the
+relationship's primary key column, this function creates a new entry
+in the database with its attributes set to the data in the entry.
+
+For each hash ref in C<$entries> that contains a field for the
+relationship's primary key column, this function looks up the
+corresponding entry in C<$self-&gt;$attribute> & updates its
+attributes with the data in the entry.
+
+All objects in C<$self-&gt;$attribute> for which no corresponding
+entry exists in C<$entries> are deleted by calling the object's
+C<delete> method.
+
+In all cases the relationship itself C<$self-&gt;$attribute> is not
+changed.
+
 =item _get_manager_class
 
 Returns the manager package for the object or class that it is called
@@ -304,6 +422,14 @@ Loads objects from the database which haven't been cached before and
 caches them for the duration of the current request (see
 L<SL::Request/cache>).
 
+If you know in advance that you will likely need all objects of a
+particular type then you can pre-cache them by calling the manager's
+C<cache_all> function. For example, if you expect to need all unit
+objects, you can use C<SL::DB::Manager::Unit-E<gt>cache_all> before
+you start the actual work. Later you can use
+C<SL::DB::Unit-E<gt>load_cached> to retrieve individual objects and be
+sure that they're already cached.
+
 This method can be called both as an instance method and a class
 method. It loads objects for the corresponding class (e.g. both
 C<SL::DB::Part-E<gt>load_cached(…)> and
@@ -323,6 +449,30 @@ object's ID is used.
 
 Returns the object/class it was called on.
 
+=item C<clone_and_reset>
+
+This works similar to L<Rose::DB::Object::Helpers/clone_and_reset>: it
+returns a cloned instance of C<$self>. All primary and unique key
+fields have been reset.
+
+The difference between Rose's and this function is that this function
+will also skip setting the following fields if such columns exist for
+C<$self>: C<itime>, C<mtime>.
+
+=item C<presenter>
+
+Returns a proxy wrapper that will dispatch all method calls to the presenter
+with the same name as the class of the involking object.
+
+For the full documentation about its capabilites see
+L<SL::DB::Helper::Presenter>
+
+=item C<as_debug_info>
+
+Returns a hash containing solely the essentials for dumping it with
+L<LXDebug/dump>. The returned hash consists of the column names with
+associated column values in stringified form.
+
 =back
 
 =head1 AUTHOR
index 371e445..6a54869 100644 (file)
@@ -44,10 +44,10 @@ sub run_hooks {
 
   foreach my $sub (@{ ( $hooks{$when} || { })->{ ref($object) } || [ ] }) {
     my $result = ref($sub) eq 'CODE' ? $sub->($object, @args) : $object->call_sub($sub, @args);
-    die SL::X::DBHookError->new(when        => $when,
-                                hook        => (ref($sub) eq 'CODE' ? '<anonymous sub>' : $sub),
-                                object      => $object,
-                                object_type => ref($object))
+    SL::X::DBHookError->throw(when        => $when,
+                              hook        => (ref($sub) eq 'CODE' ? '<anonymous sub>' : $sub),
+                              object      => $object,
+                              object_type => ref($object))
       if !$result;
   }
 }
index d61ec31..fdaa1e8 100644 (file)
@@ -6,9 +6,11 @@ use strict;
 use Carp;
 use DateTime;
 use List::Util qw(max);
+use List::MoreUtils qw(any);
 
 use SL::DB::MetaSetup::Order;
 use SL::DB::Manager::Order;
+use SL::DB::Helper::Attr;
 use SL::DB::Helper::AttrHTML;
 use SL::DB::Helper::AttrSorted;
 use SL::DB::Helper::FlattenToForm;
@@ -16,6 +18,7 @@ use SL::DB::Helper::LinkedRecords;
 use SL::DB::Helper::PriceTaxCalculator;
 use SL::DB::Helper::PriceUpdater;
 use SL::DB::Helper::TransNumberGenerator;
+use SL::Locale::String qw(t8);
 use SL::RecordLinks;
 use Rose::DB::Object::Helpers qw(as_tree);
 
@@ -39,14 +42,34 @@ __PACKAGE__->meta->add_relationship(
     column_map             => { id => 'trans_id' },
     query_args             => [ module => 'OE' ],
   },
+  exchangerate_obj         => {
+    type                   => 'one to one',
+    class                  => 'SL::DB::Exchangerate',
+    column_map             => { currency_id => 'currency_id', transdate => 'transdate' },
+  },
+  phone_notes => {
+    type         => 'one to many',
+    class        => 'SL::DB::Note',
+    column_map   => { id => 'trans_id' },
+    query_args   => [ trans_module => 'oe' ],
+    manager_args => {
+      with_objects => [ 'employee' ],
+      sort_by      => 'notes.itime',
+    }
+  },
 );
 
+SL::DB::Helper::Attr::make(__PACKAGE__, daily_exchangerate => 'numeric');
+
 __PACKAGE__->meta->initialize;
 
 __PACKAGE__->attr_html('notes');
 __PACKAGE__->attr_sorted('items');
 
 __PACKAGE__->before_save('_before_save_set_ord_quo_number');
+__PACKAGE__->before_save('_before_save_create_new_project');
+__PACKAGE__->before_save('_before_save_remove_empty_custom_shipto');
+__PACKAGE__->before_save('_before_save_set_custom_shipto_module');
 
 # hooks
 
@@ -62,6 +85,47 @@ sub _before_save_set_ord_quo_number {
 
   return 1;
 }
+sub _before_save_create_new_project {
+  my ($self) = @_;
+
+  # force new project, if not set yet
+  if ($::instance_conf->get_order_always_project && !$self->globalproject_id && ($self->type eq 'sales_order')) {
+
+    die t8("Error while creating project with project number of new order number, project number #1 already exists!", $self->ordnumber)
+      if SL::DB::Manager::Project->find_by(projectnumber => $self->ordnumber);
+
+    eval {
+      my $new_project = SL::DB::Project->new(
+          projectnumber     => $self->ordnumber,
+          description       => $self->customer->name,
+          customer_id       => $self->customer->id,
+          active            => 1,
+          project_type_id   => $::instance_conf->get_project_type_id,
+          project_status_id => $::instance_conf->get_project_status_id,
+          );
+       $new_project->save;
+       $self->globalproject_id($new_project->id);
+    } or die t8('Could not create new project #1', $@);
+  }
+  return 1;
+}
+
+
+sub _before_save_remove_empty_custom_shipto {
+  my ($self) = @_;
+
+  $self->custom_shipto(undef) if $self->custom_shipto && $self->custom_shipto->is_empty;
+
+  return 1;
+}
+
+sub _before_save_set_custom_shipto_module {
+  my ($self) = @_;
+
+  $self->custom_shipto->module('OE') if $self->custom_shipto;
+
+  return 1;
+}
 
 # methods
 
@@ -84,6 +148,20 @@ sub is_type {
   return shift->type eq shift;
 }
 
+sub deliverydate {
+  # oe doesn't have deliverydate, but it does have reqdate.
+  # But this has a different meaning for sales quotations.
+  # deliverydate can be used to determine tax if tax_point isn't set.
+
+  return $_[0]->reqdate if $_[0]->type ne 'sales_quotation';
+}
+
+sub effective_tax_point {
+  my ($self) = @_;
+
+  return $self->tax_point || $self->deliverydate || $self->transdate;
+}
+
 sub displayable_type {
   my $type = shift->type;
 
@@ -104,6 +182,33 @@ sub is_sales {
   return !!shift->customer_id;
 }
 
+sub daily_exchangerate {
+  my ($self, $val) = @_;
+
+  return 1 if $self->currency_id == $::instance_conf->get_currency_id;
+
+  my $rate = (any { $self->is_type($_) } qw(sales_quotation sales_order))      ? 'buy'
+           : (any { $self->is_type($_) } qw(request_quotation purchase_order)) ? 'sell'
+           : undef;
+  return if !$rate;
+
+  if (defined $val) {
+    croak t8('exchange rate has to be positive') if $val <= 0;
+    if (!$self->exchangerate_obj) {
+      $self->exchangerate_obj(SL::DB::Exchangerate->new(
+        currency_id => $self->currency_id,
+        transdate   => $self->transdate,
+        $rate       => $val,
+      ));
+    } elsif (!defined $self->exchangerate_obj->$rate) {
+      $self->exchangerate_obj->$rate($val);
+    } else {
+      croak t8('exchange rate already exists, no update allowed');
+    }
+  }
+  return $self->exchangerate_obj->$rate if $self->exchangerate_obj;
+}
+
 sub invoices {
   my $self   = shift;
   my %params = @_;
@@ -143,8 +248,23 @@ sub convert_to_invoice {
   my $invoice;
   if (!$self->db->with_transaction(sub {
     require SL::DB::Invoice;
-    $invoice = SL::DB::Invoice->new_from($self)->post(%params) || die;
+    $invoice = SL::DB::Invoice->new_from($self, %params)->post || die;
     $self->link_to_record($invoice);
+    # TODO extend link_to_record for items, otherwise long-term no d.r.y.
+    foreach my $item (@{ $invoice->items }) {
+      foreach (qw(orderitems)) {
+        if ($item->{"converted_from_${_}_id"}) {
+          die unless $item->{id};
+          RecordLinks->create_links('mode'       => 'ids',
+                                    'from_table' => $_,
+                                    'from_ids'   => $item->{"converted_from_${_}_id"},
+                                    'to_table'   => 'invoice',
+                                    'to_id'      => $item->{id},
+          ) || die;
+          delete $item->{"converted_from_${_}_id"};
+        }
+      }
+    }
     $self->update_attributes(closed => 1);
     1;
   })) {
@@ -157,25 +277,248 @@ sub convert_to_invoice {
 sub convert_to_delivery_order {
   my ($self, @args) = @_;
 
-  my ($delivery_order, $custom_shipto);
+  my $delivery_order;
   if (!$self->db->with_transaction(sub {
     require SL::DB::DeliveryOrder;
-    ($delivery_order, $custom_shipto) = SL::DB::DeliveryOrder->new_from($self, @args);
+    $delivery_order = SL::DB::DeliveryOrder->new_from($self, @args);
     $delivery_order->save;
-    $custom_shipto->save if $custom_shipto;
     $self->link_to_record($delivery_order);
-    $self->update_attributes(delivered => 1);
+    # TODO extend link_to_record for items, otherwise long-term no d.r.y.
+    foreach my $item (@{ $delivery_order->items }) {
+      foreach (qw(orderitems)) {    # expand if needed (delivery_order_items)
+        if ($item->{"converted_from_${_}_id"}) {
+          die unless $item->{id};
+          RecordLinks->create_links('dbh'        => $self->db->dbh,
+                                    'mode'       => 'ids',
+                                    'from_table' => $_,
+                                    'from_ids'   => $item->{"converted_from_${_}_id"},
+                                    'to_table'   => 'delivery_order_items',
+                                    'to_id'      => $item->{id},
+          ) || die;
+          delete $item->{"converted_from_${_}_id"};
+        }
+      }
+    }
+
+    $self->update_attributes(delivered => 1) unless $::instance_conf->get_shipped_qty_require_stock_out;
     1;
   })) {
-    return wantarray ? () : undef;
+    return undef;
   }
 
-  return wantarray ? ($delivery_order, $custom_shipto) : $delivery_order;
+  return $delivery_order;
+}
+
+sub _clone_orderitem_cvar {
+  my ($cvar) = @_;
+
+  my $cloned = $_->clone_and_reset;
+  $cloned->sub_module('orderitems');
+
+  return $cloned;
+}
+
+sub new_from {
+  my ($class, $source, %params) = @_;
+
+  croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) eq 'SL::DB::Order';
+  croak("A destination type must be given as parameter")         unless $params{destination_type};
+
+  my $destination_type  = delete $params{destination_type};
+
+  my @from_tos = (
+    { from => 'sales_quotation',   to => 'sales_order',       abbr => 'sqso' },
+    { from => 'request_quotation', to => 'purchase_order',    abbr => 'rqpo' },
+    { from => 'sales_quotation',   to => 'sales_quotation',   abbr => 'sqsq' },
+    { from => 'sales_order',       to => 'sales_order',       abbr => 'soso' },
+    { from => 'request_quotation', to => 'request_quotation', abbr => 'rqrq' },
+    { from => 'purchase_order',    to => 'purchase_order',    abbr => 'popo' },
+    { from => 'sales_order',       to => 'purchase_order',    abbr => 'sopo' },
+    { from => 'purchase_order',    to => 'sales_order',       abbr => 'poso' },
+    { from => 'sales_order',       to => 'sales_quotation',   abbr => 'sosq' },
+    { from => 'purchase_order',    to => 'request_quotation', abbr => 'porq' },
+  );
+  my $from_to = (grep { $_->{from} eq $source->type && $_->{to} eq $destination_type} @from_tos)[0];
+  croak("Cannot convert from '" . $source->type . "' to '" . $destination_type . "'") if !$from_to;
+
+  my $is_abbr_any = sub {
+    any { $from_to->{abbr} eq $_ } @_;
+  };
+
+  my ($item_parent_id_column, $item_parent_column);
+
+  if (ref($source) eq 'SL::DB::Order') {
+    $item_parent_id_column = 'trans_id';
+    $item_parent_column    = 'order';
+  }
+
+  my %args = ( map({ ( $_ => $source->$_ ) } qw(amount cp_id currency_id cusordnumber customer_id delivery_customer_id delivery_term_id delivery_vendor_id
+                                                department_id exchangerate globalproject_id intnotes marge_percent marge_total language_id netamount notes
+                                                ordnumber payment_id quonumber reqdate salesman_id shippingpoint shipvia taxincluded tax_point taxzone_id
+                                                transaction_description vendor_id billing_address_id
+                                             )),
+               quotation => !!($destination_type =~ m{quotation$}),
+               closed    => 0,
+               delivered => 0,
+               transdate => DateTime->today_local,
+               employee  => SL::DB::Manager::Employee->current,
+            );
+
+  if ( $is_abbr_any->(qw(sopo poso)) ) {
+    $args{ordnumber} = undef;
+    $args{quonumber} = undef;
+    $args{reqdate}   = DateTime->today_local->next_workday();
+  }
+  if ( $is_abbr_any->(qw(sopo)) ) {
+    $args{customer_id}      = undef;
+    $args{salesman_id}      = undef;
+    $args{payment_id}       = undef;
+    $args{delivery_term_id} = undef;
+  }
+  if ( $is_abbr_any->(qw(poso)) ) {
+    $args{vendor_id} = undef;
+  }
+  if ( $is_abbr_any->(qw(soso)) ) {
+    $args{periodic_invoices_config} = $source->periodic_invoices_config->clone_and_reset if $source->periodic_invoices_config;
+  }
+  if ( $is_abbr_any->(qw(sosq porq)) ) {
+    $args{ordnumber} = undef;
+    $args{quonumber} = undef;
+    $args{reqdate}   = DateTime->today_local->next_workday();
+  }
+
+  # Custom shipto addresses (the ones specific to the sales/purchase
+  # record and not to the customer/vendor) are only linked from
+  # shipto → order. Meaning order.shipto_id
+  # will not be filled in that case.
+  if (!$source->shipto_id && $source->id) {
+    $args{custom_shipto} = $source->custom_shipto->clone($class) if $source->can('custom_shipto') && $source->custom_shipto;
+
+  } else {
+    $args{shipto_id} = $source->shipto_id;
+  }
+
+  my $order = $class->new(%args);
+  $order->assign_attributes(%{ $params{attributes} }) if $params{attributes};
+  my $items = delete($params{items}) || $source->items_sorted;
+
+  my %item_parents;
+
+  my @items = map {
+    my $source_item      = $_;
+    my $source_item_id   = $_->$item_parent_id_column;
+    my @custom_variables = map { _clone_orderitem_cvar($_) } @{ $source_item->custom_variables };
+
+    $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
+    my $item_parent                  = $item_parents{$source_item_id};
+
+    my $current_oe_item = SL::DB::OrderItem->new(map({ ( $_ => $source_item->$_ ) }
+                                                     qw(active_discount_source active_price_source base_qty cusordnumber
+                                                        description discount lastcost longdescription
+                                                        marge_percent marge_price_factor marge_total
+                                                        ordnumber parts_id price_factor price_factor_id pricegroup_id
+                                                        project_id qty reqdate sellprice serialnumber ship subtotal transdate unit
+                                                        optional
+                                                     )),
+                                                 custom_variables => \@custom_variables,
+    );
+    if ( $is_abbr_any->(qw(sopo)) ) {
+      $current_oe_item->sellprice($source_item->lastcost);
+      $current_oe_item->discount(0);
+    }
+    if ( $is_abbr_any->(qw(poso)) ) {
+      $current_oe_item->lastcost($source_item->sellprice);
+    }
+    $current_oe_item->{"converted_from_orderitems_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::Order';
+    $current_oe_item;
+  } @{ $items };
+
+  @items = grep { $params{item_filter}->($_) } @items if $params{item_filter};
+  @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
+  @items = grep { $_->qty >=0 } @items if $params{skip_items_negative_qty};
+
+  $order->items(\@items);
+
+  return $order;
+}
+
+sub new_from_multi {
+  my ($class, $sources, %params) = @_;
+
+  croak("Unsupported object type in sources")                             if any { ref($_) !~ m{SL::DB::Order} }                   @$sources;
+  croak("Cannot create order for purchase records")                       if any { !$_->is_sales }                                 @$sources;
+  croak("Cannot create order from source records of different customers") if any { $_->customer_id != $sources->[0]->customer_id } @$sources;
+
+  # bb: todo: check shipto: is it enough to check the ids or do we have to compare the entries?
+  if (delete $params{check_same_shipto}) {
+    die "check same shipto address is not implemented yet";
+    die "Source records do not have the same shipto"        if 1;
+  }
+
+  # sort sources
+  if (defined $params{sort_sources_by}) {
+    my $sort_by = delete $params{sort_sources_by};
+    if ($sources->[0]->can($sort_by)) {
+      $sources = [ sort { $a->$sort_by cmp $b->$sort_by } @$sources ];
+    } else {
+      die "Cannot sort source records by $sort_by";
+    }
+  }
+
+  # set this entries to undef that yield different information
+  my %attributes;
+  foreach my $attr (qw(ordnumber transdate reqdate tax_point taxincluded shippingpoint
+                       shipvia notes closed delivered reqdate quonumber
+                       cusordnumber proforma transaction_description
+                       order_probability expected_billing_date)) {
+    $attributes{$attr} = undef if any { ($sources->[0]->$attr//'') ne ($_->$attr//'') } @$sources;
+  }
+  foreach my $attr (qw(cp_id currency_id salesman_id department_id
+                       delivery_customer_id delivery_vendor_id shipto_id
+                       globalproject_id exchangerate)) {
+    $attributes{$attr} = undef if any { ($sources->[0]->$attr||0) != ($_->$attr||0) }   @$sources;
+  }
+
+  # set this entries from customer that yield different information
+  foreach my $attr (qw(language_id taxzone_id payment_id delivery_term_id)) {
+    $attributes{$attr}  = $sources->[0]->customervendor->$attr if any { ($sources->[0]->$attr||0)     != ($_->$attr||0) }      @$sources;
+  }
+  $attributes{intnotes} = $sources->[0]->customervendor->notes if any { ($sources->[0]->intnotes//'') ne ($_->intnotes//'')  } @$sources;
+
+  # no periodic invoice config for new order
+  $attributes{periodic_invoices_config} = undef;
+
+  # set emplyee to the current one
+  $attributes{employee} = SL::DB::Manager::Employee->current;
+
+  # copy global ordnumber, transdate, cusordnumber into item scope
+  #   unless already present there
+  foreach my $attr (qw(ordnumber transdate cusordnumber)) {
+    foreach my $src (@$sources) {
+      foreach my $item (@{ $src->items_sorted }) {
+        $item->$attr($src->$attr) if !$item->$attr;
+      }
+    }
+  }
+
+  # collect items
+  my @items;
+  push @items, @{$_->items_sorted} for @$sources;
+  # make order from first source and all items
+  my $order = $class->new_from($sources->[0],
+                               destination_type => 'sales_order',
+                               attributes       => \%attributes,
+                               items            => \@items,
+                               %params);
+
+  return $order;
 }
 
 sub number {
   my $self = shift;
 
+  return if !$self->type;
+
   my %number_method = (
     sales_order       => 'ordnumber',
     sales_quotation   => 'quonumber',
@@ -194,10 +537,24 @@ sub date {
   goto &transdate;
 }
 
+sub digest {
+  my ($self) = @_;
+
+  sprintf "%s %s %s (%s)",
+    $self->number,
+    $self->customervendor->name,
+    $self->amount_as_number,
+    $self->date->to_kivitendo;
+}
+
 1;
 
 __END__
 
+=pod
+
+=encoding utf8
+
 =head1 NAME
 
 SL::DB::Order - Order Datenbank Objekt.
@@ -224,6 +581,30 @@ Returns one of the following string types:
 
 Returns true if the order is of the given type.
 
+=head2 C<daily_exchangerate $val>
+
+Gets or sets the exchangerate object's value. This is the value from the
+table C<exchangerate> depending on the order's currency, the transdate and
+if it is a sales or purchase order.
+
+The order object (respectively the table C<oe>) has an own column
+C<exchangerate> which can be get or set with the accessor C<exchangerate>.
+
+The idea is to drop the legacy table C<exchangerate> in the future and to
+give all relevant tables it's own C<exchangerate> column.
+
+So, this method is here if you need to access the "legacy" exchangerate via
+an order object.
+
+=over 4
+
+=item C<$val>
+
+(optional) If given, the exchangerate in the "legacy" table is set to this
+value, depending on currency, transdate and sales or purchase.
+
+=back
+
 =head2 C<convert_to_delivery_order %params>
 
 Creates a new delivery order with C<$self> as the basis by calling
@@ -235,16 +616,8 @@ C<true>, and C<$self> is saved.
 The arguments in C<%params> are passed to
 L<SL::DB::DeliveryOrder::new_from>.
 
-Returns C<undef> on failure. Otherwise the return value depends on the
-context. In list context the new delivery order and a shipto instance
-will be returned. In scalar instance only the delivery order instance
-is returned.
-
-Custom shipto addresses (the ones specific to the sales/purchase
-record and not to the customer/vendor) are only linked from C<shipto>
-to C<delivery_orders>. Meaning C<delivery_orders.shipto_id> will not
-be filled in that case. That's why a separate shipto object is created
-and returned.
+Returns C<undef> on failure. Otherwise the new delivery order will be
+returned.
 
 =head2 C<convert_to_invoice %params>
 
@@ -253,7 +626,7 @@ L<SL::DB::Invoice::new_from>. That invoice is posted, and C<$self> is
 linked to the new invoice via L<SL::DB::RecordLink>. C<$self>'s
 C<closed> attribute is set to C<true>, and C<$self> is saved.
 
-The arguments in C<%params> are passed to L<SL::DB::Invoice::post>.
+The arguments in C<%params> are passed to L<SL::DB::Invoice::new_from>.
 
 Returns the new invoice instance on success and C<undef> on
 failure. The whole process is run inside a transaction. On failure
@@ -261,15 +634,80 @@ nothing is created or changed in the database.
 
 At the moment only sales quotations and sales orders can be converted.
 
-=head2 C<create_sales_process>
+=head2 C<new_from $source, %params>
+
+Creates a new C<SL::DB::Order> instance and copies as much
+information from C<$source> as possible. At the moment only records with the
+same destination type as the source type and sales orders from
+sales quotations and purchase orders from requests for quotations can be
+created.
+
+The C<transdate> field will be set to the current date.
+
+The conversion copies the order items as well.
+
+Returns the new order instance. The object returned is not
+saved.
+
+C<%params> can include the following options
+(C<destination_type> is mandatory):
+
+=over 4
+
+=item C<destination_type>
+
+(mandatory)
+The type of the newly created object. Can be C<sales_quotation>,
+C<sales_order>, C<purchase_quotation> or C<purchase_order> for now.
+
+=item C<items>
+
+An optional array reference of RDBO instances for the items to use. If
+missing then the method C<items_sorted> will be called on
+C<$source>. This option can be used to override the sorting, to
+exclude certain positions or to add additional ones.
+
+=item C<skip_items_negative_qty>
+
+If trueish then items with a negative quantity are skipped. Items with
+a quantity of 0 are not affected by this option.
+
+=item C<skip_items_zero_qty>
+
+If trueish then items with a quantity of 0 are skipped.
+
+=item C<item_filter>
+
+An optional code reference that is called for each item with the item
+as its sole parameter. Items for which the code reference returns a
+falsish value will be skipped.
+
+=item C<attributes>
+
+An optional hash reference. If it exists then it is passed to C<new>
+allowing the caller to set certain attributes for the new delivery
+order.
+
+=back
+
+=head2 C<new_from_multi $sources, %params>
+
+Creates a new C<SL::DB::Order> instance from multiple sources and copies as
+much information from C<$sources> as possible.
+At the moment only sales orders can be combined and they must be of the same
+customer.
+
+The new order is created from the first one using C<new_from> and the positions
+of all orders are added to the new order. The orders can be sorted with the
+parameter C<sort_sources_by>.
 
-Creates and saves a new sales process. Can only be called for sales
-orders.
+The orders attributes are kept if they contain the same information for all
+source orders an will be set to empty if they contain different information.
 
-The newly created process will be linked bidirectionally to both
-C<$self> and to all sales quotations that are linked to C<$self>.
+Returns the new order instance. The object returned is not
+saved.
 
-Returns the newly created process instance.
+C<params> other then C<sort_sources_by> are passed to C<new_from>.
 
 =head1 BUGS
 
index 5fcb8be..cc0a92f 100644 (file)
@@ -2,11 +2,8 @@ package SL::DB::OrderItem;
 
 use strict;
 
-use List::Util qw(sum);
-
 use SL::DB::MetaSetup::OrderItem;
 use SL::DB::Manager::OrderItem;
-use SL::DB::DeliveryOrderItemsStock;
 use SL::DB::Helper::ActsAsList;
 use SL::DB::Helper::LinkedRecords;
 use SL::DB::Helper::RecordItem;
@@ -20,6 +17,7 @@ use SL::DB::Helper::CustomVariables (
     }
   },
 );
+use SL::Helper::ShippedQty;
 
 __PACKAGE__->meta->initialize;
 
@@ -31,15 +29,23 @@ sub is_price_update_available {
 }
 
 sub shipped_qty {
-  my ($self) = @_;
+  my ($self, %params) = @_;
+
+  my $force = delete $params{force};
 
-  my $d_orders = $self->order->linked_records(direction => 'to', to => 'SL::DB::DeliveryOrder');
-  my @doi      = grep { $_->parts_id == $self->parts_id } map { $_->orderitems } @$d_orders;
+  SL::Helper::ShippedQty->new(%params)->calculate($self)->write_to_objects if $force || !defined $self->{shipped_qty};
 
-  require SL::AM;
-  return sum(map { AM->convert_unit($_->unit => $self->unit) * $_->qty } @doi);
+  $self->{shipped_qty};
 }
 
+sub linked_delivery_order_items {
+  my ($self) = @_;
+
+  return $self->linked_records(direction => 'to', to => 'SL::DB::DeliveryOrderItem');
+}
+
+sub delivered_qty { goto &shipped_qty }
+
 sub record { goto &order }
 
 1;
@@ -56,35 +62,31 @@ SL::DB::OrderItems: Rose model for orderitems
 
 =over 4
 
-=item C<shipped_qty>
+=item C<shipped_qty PARAMS>
 
-returns the number of orderitems which are already linked to Delivery Orders.
-The linked key is parts_id and not orderitems (id) -> delivery_order_items (id).
-Therefore this function is not safe for identical parts_id.
-Sample call:
-C<$::form-E<gt>format_amount(\%::myconfig, $_[0]-E<gt>shipped_qty);>
+Calculates the shipped qty for this orderitem (measured in the current unit)
+and returns it.
 
-=back
+Note that the shipped qty is expected not to change within the request and is
+cached in C<shipped_qty> once calculated. If C<< force => 1 >> is passed, the
+existibng cache is ignored.
 
-=head1 TODO
+Given parameters will be passed to L<SL::Helper::ShippedQty>, so you can force
+the shipped/delivered distinction like this:
 
-Older versions of OrderItem.pm had more functions which where used for calculating the
-qty for the different states of the Delivery Order.
-For example to get the qty in already marked as delivered Delivery Orders:
+  $_->shipped_qty(require_stock_out => 0);
 
-C<delivered_qty>
+Note however that calculating shipped_qty on individual Orderitems is generally
+a bad idea. See L<SL::Helper::ShippedQty> for way to compute these all at once.
 
-return $self-E<gt>_delivered_qty;
+=item C<delivered_qty>
 
-  sub _delivered_qty {
-  (..)
-    my @d_orders_delivered = grep { $_-E<gt>delivered } @$d_orders;
-    my @doi_delivered      = grep { $_-E<gt>parts_id == $self-E<gt>parts_id } map { $_-E<gt>orderitems } @d_orders_delivered;
-  }
-
-In general the function C<shipped_qty> and all (project) related functions should be marked deprecate,
- because of the better linked item to item data in the record_links table.
+Alias for L</shipped_qty>.
 
+=back
 
+=head1 AUTHORS
 
+G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
 
+=cut
index 57f0f31..89261af 100644 (file)
@@ -3,34 +3,53 @@ package SL::DB::Part;
 use strict;
 
 use Carp;
-use List::MoreUtils qw(any);
+use List::MoreUtils qw(any uniq);
+use List::Util qw(sum);
 use Rose::DB::Object::Helpers qw(as_tree);
 
+use SL::Locale::String qw(t8);
 use SL::DBUtils;
 use SL::DB::MetaSetup::Part;
 use SL::DB::Manager::Part;
 use SL::DB::Chart;
 use SL::DB::Helper::AttrHTML;
+use SL::DB::Helper::AttrSorted;
 use SL::DB::Helper::TransNumberGenerator;
 use SL::DB::Helper::CustomVariables (
   module      => 'IC',
   cvars_alias => 1,
 );
+use SL::DB::Helper::DisplayableNamePreferences (
+  title   => t8('Article'),
+  options => [ {name => 'partnumber',  title => t8('Part Number')     },
+               {name => 'description', title => t8('Description')    },
+               {name => 'notes',       title => t8('Notes')},
+               {name => 'ean',         title => t8('EAN')            }, ],
+);
+
 
 __PACKAGE__->meta->add_relationships(
   assemblies                     => {
     type         => 'one to many',
     class        => 'SL::DB::Assembly',
+    manager_args => { sort_by => 'position' },
     column_map   => { id => 'id' },
   },
   prices         => {
     type         => 'one to many',
     class        => 'SL::DB::Price',
     column_map   => { id => 'parts_id' },
+    manager_args => { with_objects => [ 'pricegroup' ] }
   },
   makemodels     => {
     type         => 'one to many',
     class        => 'SL::DB::MakeModel',
+    manager_args => { sort_by => 'sortorder' },
+    column_map   => { id => 'parts_id' },
+  },
+  customerprices => {
+    type         => 'one to many',
+    class        => 'SL::DB::PartCustomerPrice',
     column_map   => { id => 'parts_id' },
   },
   translations   => {
@@ -38,13 +57,41 @@ __PACKAGE__->meta->add_relationships(
     class        => 'SL::DB::Translation',
     column_map   => { id => 'parts_id' },
   },
+  assortment_items => {
+    type         => 'one to many',
+    class        => 'SL::DB::AssortmentItem',
+    column_map   => { id => 'assortment_id' },
+    manager_args => { sort_by => 'position' },
+  },
+  history_entries   => {
+    type            => 'one to many',
+    class           => 'SL::DB::History',
+    column_map      => { id => 'trans_id' },
+    query_args      => [ what_done => 'part' ],
+    manager_args    => { sort_by => 'itime' },
+  },
+  shop_parts     => {
+    type         => 'one to many',
+    class        => 'SL::DB::ShopPart',
+    column_map   => { id => 'part_id' },
+    manager_args => { with_objects => [ 'shop' ] },
+  },
+  last_price_update => {
+    type         => 'one to one',
+    class        => 'SL::DB::PartsPriceHistory',
+    column_map   => { id => 'part_id' },
+    manager_args => { sort_by => 'valid_from DESC', limit => 1 },
+  },
 );
 
 __PACKAGE__->meta->initialize;
 
 __PACKAGE__->attr_html('notes');
+__PACKAGE__->attr_sorted({ unsorted => 'makemodels',     position => 'sortorder' });
+__PACKAGE__->attr_sorted({ unsorted => 'customerprices', position => 'sortorder' });
 
 __PACKAGE__->before_save('_before_save_set_partnumber');
+__PACKAGE__->before_save('_before_save_set_assembly_weight');
 
 sub _before_save_set_partnumber {
   my ($self) = @_;
@@ -53,54 +100,150 @@ sub _before_save_set_partnumber {
   return 1;
 }
 
+sub _before_save_set_assembly_weight {
+  my ($self) = @_;
+
+  if ( $self->part_type eq 'assembly' ) {
+    my $weight_sum = $self->items_weight_sum;
+    $self->weight($self->items_weight_sum) if $weight_sum;
+  }
+  return 1;
+}
+
+sub items {
+  my ($self) = @_;
+
+  if ( $self->part_type eq 'assembly' ) {
+    return $self->assemblies;
+  } elsif ( $self->part_type eq 'assortment' ) {
+    return $self->assortment_items;
+  } else {
+    return undef;
+  }
+}
+
+sub items_checksum {
+  my ($self) = @_;
+
+  # for detecting if the items of an (orphaned) assembly or assortment have
+  # changed when saving
+
+  return join(' ', sort map { $_->part->id } @{$self->items});
+};
+
+sub validate {
+  my ($self) = @_;
+
+  my @errors;
+  push @errors, $::locale->text('The partnumber is missing.')     if $self->id and !$self->partnumber;
+  push @errors, $::locale->text('The unit is missing.')           unless $self->unit;
+  push @errors, $::locale->text('The buchungsgruppe is missing.') unless $self->buchungsgruppen_id or $self->buchungsgruppe;
+
+  if ( $::instance_conf->get_partsgroup_required
+       && ( !$self->partsgroup_id or ( $self->id && !$self->partsgroup_id && $self->partsgroup ) ) ) {
+    # when unsetting an existing partsgroup in the interface, $self->partsgroup_id will be undef but $self->partsgroup will still have a value
+    # this needs to be checked, as partsgroup dropdown has an empty value
+    push @errors, $::locale->text('The partsgroup is missing.');
+  }
+
+  unless ( $self->id ) {
+    push @errors, $::locale->text('The partnumber already exists.') if SL::DB::Manager::Part->get_all_count(where => [ partnumber => $self->partnumber ]);
+  };
+
+  if ($self->is_assortment && $self->orphaned && scalar @{$self->assortment_items} == 0) {
+    # when assortment isn't orphaned form doesn't contain any items
+    push @errors, $::locale->text('The assortment doesn\'t have any items.');
+  }
+
+  if ($self->is_assembly && scalar @{$self->assemblies} == 0) {
+    push @errors, $::locale->text('The assembly doesn\'t have any items.');
+  }
+
+  return @errors;
+}
+
 sub is_type {
   my $self = shift;
   my $type  = lc(shift || '');
-  die 'invalid type' unless $type =~ /^(?:part|service|assembly)$/;
+  die 'invalid type' unless $type =~ /^(?:part|service|assembly|assortment)$/;
 
   return $self->type eq $type ? 1 : 0;
 }
 
-sub is_part     { $_[0]->is_type('part') }
-sub is_assembly { $_[0]->is_type('assembly') }
-sub is_service  { $_[0]->is_type('service') }
+sub is_part       { $_[0]->part_type eq 'part'       }
+sub is_assembly   { $_[0]->part_type eq 'assembly'   }
+sub is_service    { $_[0]->part_type eq 'service'    }
+sub is_assortment { $_[0]->part_type eq 'assortment' }
 
 sub type {
-  my ($self, $type) = @_;
-  if (@_ > 1) {
-    die 'invalid type' unless $type =~ /^(?:part|service|assembly)$/;
-    $self->assembly(          $type eq 'assembly' ? 1 : 0);
-    $self->inventory_accno_id($type ne 'service'  ? 1 : undef);
-  }
-
-  return 'assembly' if $self->assembly;
-  return 'part'     if $self->inventory_accno_id;
-  return 'service';
+  return $_[0]->part_type;
+  # my ($self, $type) = @_;
+  # if (@_ > 1) {
+  #   die 'invalid type' unless $type =~ /^(?:part|service|assembly)$/;
+  #   $self->assembly(          $type eq 'assembly' ? 1 : 0);
+  #   $self->inventory_accno_id($type ne 'service'  ? 1 : undef);
+  # }
+
+  # return 'assembly' if $self->assembly;
+  # return 'part'     if $self->inventory_accno_id;
+  # return 'service';
 }
 
 sub new_part {
   my ($class, %params) = @_;
-  $class->new(%params, type => 'part');
+  $class->new(%params, part_type => 'part');
 }
 
 sub new_assembly {
   my ($class, %params) = @_;
-  $class->new(%params, type => 'assembly');
+  $class->new(%params, part_type => 'assembly');
 }
 
 sub new_service {
   my ($class, %params) = @_;
-  $class->new(%params, type => 'service');
+  $class->new(%params, part_type => 'service');
 }
 
+sub new_assortment {
+  my ($class, %params) = @_;
+  $class->new(%params, part_type => 'assortment');
+}
+
+sub last_modification {
+  my ($self) = @_;
+  return $self->mtime // $self->itime;
+};
+
+sub used_in_record {
+  my ($self) = @_;
+  die 'not an accessor' if @_ > 1;
+
+  return 1 unless $self->id;
+
+  my @relations = qw(
+    SL::DB::InvoiceItem
+    SL::DB::OrderItem
+    SL::DB::DeliveryOrderItem
+  );
+
+  for my $class (@relations) {
+    eval "require $class";
+    return 1 if $class->_get_manager_class->get_all_count(query => [ parts_id => $self->id ]);
+  }
+  return 0;
+}
 sub orphaned {
   my ($self) = @_;
   die 'not an accessor' if @_ > 1;
 
+  return 1 unless $self->id;
+
   my @relations = qw(
     SL::DB::InvoiceItem
     SL::DB::OrderItem
+    SL::DB::DeliveryOrderItem
     SL::DB::Inventory
+    SL::DB::AssortmentItem
   );
 
   for my $class (@relations) {
@@ -173,7 +316,7 @@ sub get_chart {
   if (!exists $charts->{$taxzone}->{$type}) {
     require SL::DB::Buchungsgruppe;
     my $bugru    = SL::DB::Buchungsgruppe->load_cached($self->buchungsgruppen_id);
-    my $chart_id = ($type eq 'inventory') ? ($self->inventory_accno_id ? $bugru->inventory_accno_id : undef)
+    my $chart_id = ($type eq 'inventory') ? ($self->is_part ? $bugru->inventory_accno_id : undef)
                  :                          $bugru->call_sub("${type}_accno_id", $taxzone);
 
     if ($chart_id) {
@@ -186,6 +329,36 @@ sub get_chart {
   return $charts->{$taxzone}->{$type};
 }
 
+sub get_stock {
+  my ($self, %params) = @_;
+
+  return undef unless $self->id;
+
+  my $query = 'SELECT SUM(qty) FROM inventory WHERE parts_id = ?';
+  my @values = ($self->id);
+
+  if ( $params{bin_id} ) {
+    $query .= ' AND bin_id = ?';
+    push(@values, $params{bin_id});
+  }
+
+  if ( $params{warehouse_id} ) {
+    $query .= ' AND warehouse_id = ?';
+    push(@values, $params{warehouse_id});
+  }
+
+  if ( $params{shippingdate} ) {
+    die unless ref($params{shippingdate}) eq 'DateTime';
+    $query .= ' AND shippingdate <= ?';
+    push(@values, $params{shippingdate});
+  }
+
+  my ($stock) = selectrow_query($::form, $self->db->dbh, $query, @values);
+
+  return $stock || 0; # never return undef
+};
+
+
 # this is designed to ignore chargenumbers, expiration dates and just give a list of how much <-> where
 sub get_simple_stock {
   my ($self, %params) = @_;
@@ -205,10 +378,173 @@ sub get_simple_stock {
   sub bin       { require SL::DB::Bin;       SL::DB::Manager::Bin      ->find_by_or_create(id => $_[0]->{bin_id}) }
 }
 
-sub displayable_name {
-  join ' ', grep $_, map $_[0]->$_, qw(partnumber description);
+sub get_simple_stock_sql {
+  my ($self, %params) = @_;
+
+  return [] unless $self->id;
+
+  my $query = <<SQL;
+     SELECT w.description                         AS warehouse_description,
+            b.description                         AS bin_description,
+            SUM(i.qty)                            AS qty,
+            SUM(i.qty * p.lastcost)               AS stock_value,
+            p.unit                                AS unit,
+            LEAD(w.description)           OVER pt AS wh_lead,            -- to detect warehouse changes for subtotals in template
+            SUM( SUM(i.qty) )             OVER pt AS run_qty,            -- running total of total qty
+            SUM( SUM(i.qty) )             OVER wh AS wh_run_qty,         -- running total of warehouse qty
+            SUM( SUM(i.qty * p.lastcost)) OVER pt AS run_stock_value,    -- running total of total stock_value
+            SUM( SUM(i.qty * p.lastcost)) OVER wh AS wh_run_stock_value  -- running total of warehouse stock_value
+       FROM inventory i
+            LEFT JOIN parts p     ON (p.id           = i.parts_id)
+            LEFT JOIN warehouse w ON (i.warehouse_id = w.id)
+            LEFT JOIN bin b       ON (i.bin_id       = b.id)
+      WHERE parts_id = ?
+   GROUP BY w.description, w.sortkey, b.description, p.unit, i.parts_id
+     HAVING SUM(qty) != 0
+     WINDOW pt AS (PARTITION BY i.parts_id    ORDER BY w.sortkey, b.description, p.unit),
+            wh AS (PARTITION by w.description ORDER BY w.sortkey, b.description, p.unit)
+   ORDER BY w.sortkey, b.description, p.unit
+SQL
+
+  my $stock_info = selectall_hashref_query($::form, $self->db->dbh, $query, $self->id);
+  return $stock_info;
+}
+
+sub get_mini_journal {
+  my ($self) = @_;
+
+  # inventory ids of the most recent 10 inventory trans_ids
+
+  # duplicate code copied from SL::Controller::Inventory mini_journal, except
+  # for the added filter on parts_id
+
+  my $parts_id = $self->id;
+  my $query = <<"SQL";
+with last_inventories as (
+   select id,
+          trans_id,
+          itime
+     from inventory
+    where parts_id = $parts_id
+ order by itime desc
+    limit 20
+),
+grouped_ids as (
+   select trans_id,
+          array_agg(id) as ids
+     from last_inventories
+ group by trans_id
+ order by max(itime)
+     desc limit 10
+)
+select unnest(ids)
+  from grouped_ids
+ limit 20  -- so the planner knows how many ids to expect, the cte is an optimisation fence
+SQL
+
+  my $objs  = SL::DB::Manager::Inventory->get_all(
+    query        => [ id => [ \"$query" ] ],                           # make emacs happy "
+    with_objects => [ 'parts', 'trans_type', 'bin', 'bin.warehouse' ], # prevent lazy loading in template
+    sort_by      => 'itime DESC',
+  );
+  # remember order of trans_ids from query, for ordering hash later
+  my @sorted_trans_ids = uniq map { $_->trans_id } @$objs;
+
+  # at most 2 of them belong to a transaction and the qty determines in or out.
+  my %transactions;
+  for (@$objs) {
+    $transactions{ $_->trans_id }{ $_->qty > 0 ? 'in' : 'out' } = $_;
+    $transactions{ $_->trans_id }{base} = $_;
+  }
+
+  # because the inventory transactions were built in a hash, we need to sort the
+  # hash by using the original sort order of the trans_ids
+  my @sorted = map { $transactions{$_} } @sorted_trans_ids;
+
+  return \@sorted;
+}
+
+sub clone_and_reset_deep {
+  my ($self) = @_;
+
+  my $clone = $self->clone_and_reset; # resets id and partnumber (primary key and unique constraint)
+  $clone->makemodels(   map { $_->clone_and_reset } @{$self->makemodels}   ) if @{$self->makemodels};
+  $clone->translations( map { $_->clone_and_reset } @{$self->translations} ) if @{$self->translations};
+
+  if ( $self->is_assortment ) {
+    # use clone rather than reset_and_clone because the unique constraint would also remove parts_id
+    $clone->assortment_items( map { $_->clone } @{$self->assortment_items} );
+    $_->assortment_id(undef) foreach @{ $clone->assortment_items }
+  };
+
+  if ( $self->is_assembly ) {
+    $clone->assemblies( map { $_->clone_and_reset } @{$self->assemblies});
+  };
+
+  if ( $self->prices ) {
+    $clone->prices( map { $_->clone } @{$self->prices}); # pricegroup_id gets reset here because it is part of a unique contraint
+    if ( $clone->prices ) {
+      foreach my $price ( @{$clone->prices} ) {
+        $price->id(undef);
+        $price->parts_id(undef);
+      };
+    };
+  };
+
+  return $clone;
+}
+
+sub item_diffs {
+  my ($self, $comparison_part) = @_;
+
+  die "item_diffs needs a part object" unless ref($comparison_part) eq 'SL::DB::Part';
+  die "part and comparison_part need to be of the same part_type" unless
+        ( $self->part_type eq 'assembly' or $self->part_type eq 'assortment' )
+    and ( $comparison_part->part_type eq 'assembly' or $comparison_part->part_type eq 'assortment' )
+    and $self->part_type eq $comparison_part->part_type;
+
+  # return [], [] if $self->items_checksum eq $comparison_part->items_checksum;
+  my @self_part_ids       = map { $_->parts_id } $self->items;
+  my @comparison_part_ids = map { $_->parts_id } $comparison_part->items;
+
+  my %orig       = map{ $_ => 1 } @self_part_ids;
+  my %comparison = map{ $_ => 1 } @comparison_part_ids;
+  my (@additions, @removals);
+  @additions = grep { !exists( $orig{$_}       ) } @comparison_part_ids if @comparison_part_ids;
+  @removals  = grep { !exists( $comparison{$_} ) } @self_part_ids       if @self_part_ids;
+
+  return \@additions, \@removals;
+};
+
+sub items_sellprice_sum {
+  my ($self, %params) = @_;
+
+  return unless $self->is_assortment or $self->is_assembly;
+  return unless $self->items;
+
+  if ($self->is_assembly) {
+    return sum map { $_->linetotal_sellprice          } @{$self->items};
+  } else {
+    return sum map { $_->linetotal_sellprice(%params) } grep { $_->charge } @{$self->items};
+  }
 }
 
+sub items_lastcost_sum {
+  my ($self) = @_;
+
+  return unless $self->is_assortment or $self->is_assembly;
+  return unless $self->items;
+  sum map { $_->linetotal_lastcost } @{$self->items};
+};
+
+sub items_weight_sum {
+  my ($self) = @_;
+
+  return unless $self->is_assembly;
+  return unless $self->items;
+  sum map { $_->linetotal_weight} @{$self->items};
+};
+
 1;
 
 __END__
@@ -239,13 +575,15 @@ flavours called:
 
 =item Assembly - a collection of both parts and services
 
+=item Assortment - a collection of items (parts or assemblies)
+
 =back
 
 These types are sadly represented by data inside the class and cannot be
 migrated into a flag. To work around this, each C<Part> object knows what type
 it currently is. Since the type is data driven, there ist no explicit setting
 method for it, but you can construct them explicitly with C<new_part>,
-C<new_service>, and C<new_assembly>. A Buchungsgruppe should be supplied in this
+C<new_service>, C<new_assembly> and C<new_assortment>. A Buchungsgruppe should be supplied in this
 case, but it will use the default Buchungsgruppe if you don't.
 
 Matching these there are assorted helper methods dealing with types,
@@ -334,6 +672,10 @@ fields belonging to the tax zone given by C<$params{taxzone}>.
 
 The information retrieved by the function is cached.
 
+=item C<used_in_record>
+
+Checks if this article has been used in orders, invoices or delivery orders.
+
 =item C<orphaned>
 
 Checks if this article is used in orders, invoices, delivery orders or
@@ -345,6 +687,41 @@ Used to set the accounting information from a L<SL:DB::Buchungsgruppe> object.
 Please note, that this is a write only accessor, the original Buchungsgruppe can
 not be retrieved from an article once set.
 
+=item C<get_simple_stock_sql>
+
+Fetches the qty and the stock value for the current part for each bin and
+warehouse where the part is in stock (or rather different from 0, might be
+negative).
+
+Runs some additional window functions to add the running totals (total running
+total and total per warehouse) for qty and stock value to each line.
+
+Using the LEAD(w.description) the template can check if the warehouse
+description is about to change, i.e. the next line will contain numbers from a
+different warehouse, so that a subtotal line can be added.
+
+The last row will contain the running qty total (run_qty) and the running total
+stock value (run_stock_value) over all warehouses/bins and can be used to add a
+line for the grand totals.
+
+=item C<items_lastcost_sum>
+
+Non-recursive lastcost sum of all the items in an assembly or assortment.
+
+=item C<get_stock %params>
+
+Fetches stock qty in the default unit for a part.
+
+bin_id and warehouse_id may be passed as params. If only a bin_id is passed,
+the stock qty for that bin is returned. If only a warehouse_id is passed, the
+stock qty for all bins in that warehouse is returned.  If a shippingdate is
+passed the stock qty for that date is returned.
+
+Examples:
+ my $qty = $part->get_stock(bin_id => 52);
+
+ $part->get_stock(shippingdate => DateTime->today->add(days => -5));
+
 =back
 
 =head1 AUTHORS
diff --git a/SL/DB/PartClassification.pm b/SL/DB/PartClassification.pm
new file mode 100644 (file)
index 0000000..c02c8b3
--- /dev/null
@@ -0,0 +1,85 @@
+
+package SL::DB::PartClassification;
+
+use strict;
+
+use SL::DB::MetaSetup::PartClassification;
+use SL::DB::Manager::PartClassification;
+
+__PACKAGE__->meta->initialize;
+__PACKAGE__->before_delete('can_be_deleted');
+
+# check if the description and abbreviation is present
+#
+sub validate {
+  my ($self) = @_;
+
+  my @errors;
+  push @errors, $::locale->text('The description is missing.')  if !$self->description;
+  push @errors, $::locale->text('The abbreviation is missing.') if !$self->abbreviation;
+
+  return @errors;
+}
+
+sub can_be_deleted {
+  my ($self) = @_;
+
+  # The first five part classifications must not be deleted.
+  return defined($self->id) && ($self->id >= 5);
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::DB::PartClassification
+
+=head1 SYNOPSIS
+
+Additional to the article types "part", "assembly", "service" and "assortement"
+the parts classification specifies other ortogonal attributes
+
+=head1 DESCRIPTION
+
+The primary attributes are the rule
+of the article as "used_for_sales" or "used_for_purchase".
+
+Another attribute is "report_separate". This attribute may be used for some additional costs like
+transport, packaging. These article are reported separate in the list of an invoice if
+the print template is using the variables <%separate_XXX_subtotal%>  and XXX is the shortcut of the parts classification.
+The variables <%non_separate_subtotal%> has the sum of all other parts of an invoice.
+(See also LaTeX Documentation).
+
+Additional other attributes may follow
+
+To see this attributes in a short way there are shortcuts of one (or two characters, if needed for compare )
+which may be translated in the specified language
+
+The type of the article is also as shortcut available, so this combined type and classification shortcut
+is used short as "Type"
+
+English type shortcuts are 'P','A','S'
+German  type shortcuts are 'W','E','D'
+The can set in the language-files
+
+To get the localized abbreviations you can use L<SL::Presenter::Part> .
+
+=head1 METHODS
+
+=head2 validate
+
+ $self->validate();
+
+check if the description and abbreviation is present
+
+
+=head1 AUTHOR
+
+Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
+
+
+=cut
diff --git a/SL/DB/PartCustomerPrice.pm b/SL/DB/PartCustomerPrice.pm
new file mode 100644 (file)
index 0000000..f3fcde4
--- /dev/null
@@ -0,0 +1,13 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::PartCustomerPrice;
+
+use strict;
+
+use SL::DB::MetaSetup::PartCustomerPrice;
+use SL::DB::Manager::PartCustomerPrice;
+
+__PACKAGE__->meta->initialize;
+
+1;
index ed3c164..4a7f607 100644 (file)
@@ -6,23 +6,57 @@ package SL::DB::PartsGroup;
 use strict;
 
 use SL::DB::MetaSetup::PartsGroup;
+use SL::DB::Manager::PartsGroup;
+use SL::DB::Helper::ActsAsList;
 
 __PACKAGE__->meta->add_relationship(
   custom_variable_configs => {
     type                  => 'many to many',
     map_class             => 'SL::DB::CustomVariableConfigPartsgroup',
   },
+  parts          => {
+    type         => 'one to many',
+    class        => 'SL::DB::Part',
+    column_map   => { id => 'partsgroup_id' },
+  },
 );
 
 __PACKAGE__->meta->initialize;
 
-# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
-__PACKAGE__->meta->make_manager_class;
-
 sub displayable_name {
   my $self = shift;
 
   return join ' ', grep $_, $self->id, $self->partsgroup;
 }
 
+sub validate {
+  my ($self) = @_;
+  require SL::DB::Customer;
+
+  my @errors;
+
+  push @errors, $::locale->text('The description is missing.') if $self->id and !$self->partsgroup;
+
+  return @errors;
+}
+
+sub orphaned {
+  my ($self) = @_;
+  die 'not an accessor' if @_ > 1;
+
+  return 1 unless $self->id;
+
+  my @relations = qw(
+    SL::DB::Part
+    SL::DB::CustomVariableConfigPartsgroup
+  );
+
+  for my $class (@relations) {
+    eval "require $class";
+    return 0 if $class->_get_manager_class->get_all_count(query => [ partsgroup_id => $self->id ]);
+  }
+
+  return 1;
+}
+
 1;
diff --git a/SL/DB/PartsPriceHistory.pm b/SL/DB/PartsPriceHistory.pm
new file mode 100644 (file)
index 0000000..f53e8f2
--- /dev/null
@@ -0,0 +1,10 @@
+package SL::DB::PartsPriceHistory;
+
+use strict;
+
+use SL::DB::MetaSetup::PartsPriceHistory;
+use SL::DB::Manager::PartsPriceHistory;
+
+__PACKAGE__->meta->initialize;
+
+1;
index b78da52..2f62bd9 100644 (file)
@@ -35,7 +35,7 @@ sub calc_date {
   }
 
   my $terms           = ($params{terms} // 'net') eq 'discount' ? 'terms_skonto' : 'terms_netto';
-  my $date            = $reference_date->add(days => $self->$terms);
+  my $date            = $reference_date->clone->add(days => $self->$terms);
 
   my $dow             = $date->day_of_week;
   $date               = $date->add(days => 8 - $dow) if $dow > 5;
@@ -57,8 +57,15 @@ SL::DB::PaymentTerm - Rose model for the payment_terms table
 =head1 SYNOPSIS
 
   my $terms             = SL::DB::PaymentTerm->new(id => $::form->{payment_id})->load;
-  my $due_date_net      = $erms->calc_date(terms => 'net');      # uses terms_netto
-  my $due_date_discount = $erms->calc_date(terms => 'discount'); # uses terms_skonto
+  my $due_date_net      = $terms->calc_date(terms => 'net');      # uses terms_netto
+  my $due_date_discount = $terms->calc_date(terms => 'discount'); # uses terms_skonto
+
+  # Calculate due date taking the existing invoice date and the due
+  # date entered by the user into account:
+  my $due_date = $terms->calc_date(
+    reference_date => $::form->{invdate},
+    due_date       => $::form->{duedate},
+  );
 
 =head1 FUNCTIONS
 
index 0d66085..0523348 100644 (file)
@@ -11,7 +11,7 @@ __PACKAGE__->meta->initialize;
 # Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
 __PACKAGE__->meta->make_manager_class;
 
-our %PERIOD_LENGTHS             = ( m => 1, q => 3, b => 6, y => 12 );
+our %PERIOD_LENGTHS             = ( o => 0, m => 1, q => 3, b => 6, y => 12 );
 our %ORDER_VALUE_PERIOD_LENGTHS = ( %PERIOD_LENGTHS, 2 => 24, 3 => 36, 4 => 48, 5 => 60 );
 our @PERIODICITIES              = keys %PERIOD_LENGTHS;
 our @ORDER_VALUE_PERIODICITIES  = keys %ORDER_VALUE_PERIOD_LENGTHS;
@@ -88,7 +88,7 @@ sub calculate_invoice_dates {
 
   my $period_len = $self->get_billing_period_length;
   my $cur_date   = ($self->first_billing_date || $self->start_date)->clone;
-  my $end_date   = $self->terminated ? $self->end_date : undef;
+  my $end_date   = $self->terminated || !$self->extend_automatically_by ? $self->end_date : undef;
   $end_date    //= DateTime->today_local->add(years => 100);
   my $start_date = $params{past_dates} ? undef                              : $self->get_previous_billed_period_start_date;
   $start_date    = $start_date         ? $start_date->clone->add(days => 1) : $cur_date->clone;
@@ -96,6 +96,10 @@ sub calculate_invoice_dates {
   $start_date    = max($start_date, $params{start_date}) if $params{start_date};
   $end_date      = min($end_date,   $params{end_date})   if $params{end_date};
 
+  if ($self->periodicity eq 'o') {
+    return ($cur_date >= $start_date) && ($cur_date <= $end_date) ? ($cur_date) : ();
+  }
+
   my @dates;
 
   while ($cur_date <= $end_date) {
@@ -128,6 +132,28 @@ sub is_last_bill_date_in_order_value_cycle {
   return $date_itr == $next_billing_date;
 }
 
+sub disable_one_time_config {
+  my $self = shift;
+
+  _log_msg("check one time for " . $self->id . "\n");
+
+  # A periodicity of one time was set. Deactivate this config now.
+  if ($self->periodicity eq 'o') {
+    _log_msg("setting inactive\n");
+    if (!$self->db->with_transaction(sub {
+      1;                          # make Emacs happy
+      $self->active(0);
+      $self->order->update_attributes(closed => 1);
+      $self->save;
+      1;
+    })) {
+      $::lxdebug->message(LXDebug->WARN(), "disalbe_one_time config failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
+      return undef;
+    }
+    return $self->order->ordnumber;
+  }
+  return undef;
+}
 1;
 __END__
 
@@ -235,6 +261,15 @@ date given by the C<date> parameter plus the billing period length
 equals one of those dates then the given date is indeed the date of
 the last invoice in that particular order value cycle.
 
+=item C<sub disable_one_time_config>
+
+Sets the state of the periodic_invoices_configs to inactive
+(active => false) and closes the source order (closed => true)
+if the periodicity is <Co> (one time).
+
+Returns undef if the periodicity is not 'one time' otherwise the
+order number of the deactivated periodic order.
+
 =back
 
 =head1 BUGS
index 93f9dde..6c7e8af 100644 (file)
@@ -6,6 +6,7 @@ package SL::DB::Price;
 use strict;
 
 use SL::DB::MetaSetup::Price;
+use Rose::DB::Object::Helpers qw(clone);
 
 __PACKAGE__->meta->initialize;
 
index 591a97d..e5fab1c 100644 (file)
@@ -3,11 +3,30 @@ package SL::DB::PriceFactor;
 use strict;
 
 use SL::DB::MetaSetup::PriceFactor;
+use SL::DB::Manager::PriceFactor;
 use SL::DB::Helper::ActsAsList;
 
 __PACKAGE__->meta->initialize;
 
-__PACKAGE__->meta->make_manager_class;
+sub orphaned {
+  my ($self) = @_;
+
+  die 'not an accessor' if @_ > 1;
+
+  require SL::DB::DeliveryOrderItem;
+  require SL::DB::InvoiceItem;
+  require SL::DB::OrderItem;
+  require SL::DB::Part;
+
+  return 1 if !$self->id;
+
+  return 0 if SL::DB::Manager::DeliveryOrderItem->get_first(query => [ price_factor_id => $self->id ]);
+  return 0 if SL::DB::Manager::InvoiceItem      ->get_first(query => [ price_factor_id => $self->id ]);
+  return 0 if SL::DB::Manager::OrderItem        ->get_first(query => [ price_factor_id => $self->id ]);
+  return 0 if SL::DB::Manager::Part             ->get_first(query => [ price_factor_id => $self->id ]);
+
+  return 1;
+}
 
 1;
 
index 70b5ab4..1c43d4a 100644 (file)
@@ -7,7 +7,6 @@ use strict;
 
 use SL::DB::MetaSetup::PriceRule;
 use SL::DB::Manager::PriceRule;
-use Rose::DB::Object::Helpers qw(clone_and_reset);
 use SL::Locale::String qw(t8);
 
 __PACKAGE__->meta->add_relationship(
index 8aa71da..006556d 100644 (file)
@@ -7,7 +7,6 @@ use strict;
 
 use SL::DB::MetaSetup::PriceRuleItem;
 use SL::DB::Manager::PriceRuleItem;
-use Rose::DB::Object::Helpers qw(clone_and_reset);
 use SL::Locale::String qw(t8);
 
 __PACKAGE__->meta->initialize;
@@ -109,7 +108,7 @@ sub full_description {
     $type eq 'customer'   ? t8('Customer')         . ' ' . $self->customer->displayable_name
   : $type eq 'vendor'     ? t8('Vendor')           . ' ' . $self->vendor->displayable_name
   : $type eq 'business'   ? t8('Type of Business') . ' ' . $self->business->displayable_name
-  : $type eq 'partsgroup' ? t8('Group')            . ' ' . $self->partsgroup->displayable_name
+  : $type eq 'partsgroup' ? t8('Partsgroup')       . ' ' . $self->partsgroup->displayable_name
   : $type eq 'pricegroup' ? t8('Pricegroup')       . ' ' . $self->pricegroup->displayable_name
   : $type eq 'part'       ? t8('Part')             . ' ' . $self->part->displayable_name
   : $type eq 'qty' ? (
index b318300..a7cfc20 100644 (file)
@@ -4,8 +4,10 @@ use strict;
 
 use SL::DB::MetaSetup::Pricegroup;
 use SL::DB::Manager::Pricegroup;
+use SL::DB::Helper::ActsAsList;
 
 __PACKAGE__->meta->initialize;
+__PACKAGE__->before_save('_before_save_remove_customer_pricegroup');
 
 sub displayable_name {
   my $self = shift;
@@ -13,5 +15,69 @@ sub displayable_name {
   return join ' ', grep $_, $self->id, $self->pricegroup;
 }
 
+sub _before_save_remove_customer_pricegroup {
+  my ($self) = @_;
+
+  return 1 unless $::form->{SELF}{remove_customer_pricegroup};
+
+  my %attributes          = (pricegroup_id => undef);
+  require SL::DB::Customer;
+  SL::DB::Manager::Customer->update_all(
+    set   => \%attributes,
+    where => [
+      'pricegroup_id' => $self->id,
+    ],
+  );
+
+  return 1;
+}
+
+sub validate {
+  my ($self) = @_;
+  require SL::DB::Customer;
+
+  my @errors;
+  if (!$::form->{SELF}{remove_customer_pricegroup}                                    &&
+      $self->obsolete                                                                 &&
+      SL::DB::Manager::Customer->get_all_count(query => [ pricegroup_id => $self->id ]) ) {
+      push @errors, $::locale->text('The pricegroup is being used by customers.');
+  }
+
+  return @errors;
+}
+
+sub orphaned {
+  my ($self) = @_;
+  die 'not an accessor' if @_ > 1;
+
+  return 1 unless $self->id;
+
+  my @relations = qw(
+    SL::DB::Customer
+    SL::DB::Price
+  );
+
+  # check if pricegroup is the default pricegroup for any customers and has any
+  # prices assigned.
+
+  for my $class (@relations) {
+    eval "require $class";
+    return 0 if $class->_get_manager_class->get_all_count(query => [ pricegroup_id => $self->id ]);
+  }
+
+  # check if pricegroup was used in any pricesource
+  my @item_relations = qw(
+    SL::DB::OrderItem
+    SL::DB::DeliveryOrderItem
+    SL::DB::InvoiceItem
+  );
+
+  for my $class (@item_relations) {
+    eval "require $class";
+    return 0 if $class->_get_manager_class->get_all_count(query => [ active_price_source => 'pricegroup/' . $self->id ]);
+  }
+
+  return 1;
+}
 
 1;
index 046027b..9f76c00 100644 (file)
@@ -2,6 +2,8 @@ package SL::DB::Printer;
 
 use strict;
 
+use Carp;
+
 use SL::DB::MetaSetup::Printer;
 use SL::DB::Manager::Printer;
 use SL::DB::Helper::Util;
@@ -23,4 +25,67 @@ sub validate {
   return @errors;
 }
 
+sub print_document {
+  my ($self, %params) = @_;
+
+  croak "Need either a 'content' or a 'file_name' parameter" if !defined($params{content}) && !$params{file_name};
+
+  my $copies  = $params{copies} || 1;
+  my $command = SL::Template::create(type => 'ShellCommand', form => Form->new(''))->parse($self->printer_command);
+  my $content = $params{content} // scalar(File::Slurp::read_file($params{file_name}));
+
+  for (1..$copies) {
+    open my $out, '|-', $command or die $!;
+    binmode $out;
+    print $out $content;
+    close $out;
+  }
+}
+
 1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::Printer - Rose model for database table printers
+
+=head1 SYNOPSIS
+
+  my $printer = SL::DB::Printer->new(id => 4711)->load;
+  $printer->print_document(
+    copies    => 2,
+    file_name => '/path/to/file.pdf',
+  );
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<print_document %params>
+
+Prints a document by spawning the external command stored in
+C<$self-E<gt>printer_command> and sending content to it.
+
+The caller must provide either the content to send to the printer
+(parameter C<content>) or a name to a file whose content is sent
+verbatim (parameter C<file_name>).
+
+An optional parameter C<copies> can be given to specify the number of
+copies to print. This is done by invoking the print command multiple
+times. The number of copies defaults to 1.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
index df5fe09..5497535 100644 (file)
@@ -12,6 +12,13 @@ use SL::DB::Helper::CustomVariables(
   cvars_alias => 1,
 );
 
+__PACKAGE__->meta->add_relationship(
+  employee_invoice_permissions  => {
+    type       => 'many to many',
+    map_class  => 'SL::DB::EmployeeProjectInvoices',
+  },
+);
+
 __PACKAGE__->meta->initialize;
 
 sub validate {
@@ -84,6 +91,23 @@ sub full_description {
   return $description;
 }
 
+sub may_employee_view_project_invoices {
+  my ($self, $employee) = @_;
+
+  return undef if !$self->id;
+
+  my $employee_id = ref($employee) ? $employee->id : $employee * 1;
+  my $query       = <<EOSQL;
+    SELECT project_id
+    FROM employee_project_invoices
+    WHERE (employee_id = ?)
+      AND (project_id  = ?)
+    LIMIT 1
+EOSQL
+
+  return !!$self->db->dbh->selectrow_arrayref($query, undef, $employee_id, $self->id)->[0];
+}
+
 1;
 
 __END__
index c63a2ed..3eb320e 100644 (file)
@@ -3,6 +3,7 @@ package SL::DB::PurchaseInvoice;
 use strict;
 
 use Carp;
+use Data::Dumper;
 
 use SL::DB::MetaSetup::PurchaseInvoice;
 use SL::DB::Manager::PurchaseInvoice;
@@ -10,7 +11,9 @@ use SL::DB::Helper::AttrHTML;
 use SL::DB::Helper::AttrSorted;
 use SL::DB::Helper::LinkedRecords;
 use SL::DB::Helper::Payment qw(:ALL);
+use SL::DB::Helper::SalesPurchaseInvoice;
 use SL::Locale::String qw(t8);
+use Rose::DB::Object::Helpers qw(has_loaded_related forget_related);
 
 # The calculator hasn't been adjusted for purchase invoices yet.
 # use SL::DB::Helper::PriceTaxCalculator;
@@ -86,12 +89,19 @@ sub abbreviation {
 
 };
 
+sub oneline_summary {
+  my $self = shift;
+
+  return sprintf("%s: %s %s %s (%s)", $self->abbreviation, $self->invnumber, $self->vendor->name,
+                                      $::form->format_amount(\%::myconfig, $self->amount,2), $self->transdate->to_kivitendo);
+}
+
 sub link {
   my ($self) = @_;
 
   my $html;
-  $html   = SL::Presenter->get->purchase_invoice($self, display => 'inline') if $self->invoice;
-  $html   = SL::Presenter->get->ap_transaction($self, display => 'inline') if !$self->invoice;
+  $html   = $self->presenter->purchase_invoice(display => 'inline') if $self->invoice;
+  $html   = $self->presenter->ap_transaction(display => 'inline') if !$self->invoice;
 
   return $html;
 }
@@ -114,4 +124,128 @@ sub displayable_name {
   join ' ', grep $_, map $_[0]->$_, qw(displayable_type record_number);
 };
 
+sub create_ap_row {
+  my ($self, %params) = @_;
+  # needs chart as param
+  # to be called after adding all AP_amount rows
+
+  # only allow this method for ap invoices (Kreditorenbuchung)
+  die if $self->invoice and not $self->vendor_id;
+
+  my $acc_trans = [];
+  my $chart = $params{chart} || SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_ap_chart_id);
+  die "illegal chart in create_ap_row" unless $chart;
+
+  die "receivables chart must have link 'AP'" . Dumper($chart) unless $chart->link eq 'AP';
+
+  # hardcoded entry for no tax, tax_id and taxkey should be 0
+  my $tax = SL::DB::Manager::Tax->find_by(id => 0, taxkey => 0) || die "Can't find tax with id 0 and taxkey 0";
+
+  my $sign = $self->vendor_id ? 1 : -1;
+  my $acc = SL::DB::AccTransaction->new(
+    amount     => $self->amount * $sign,
+    chart_id   => $params{chart}->id,
+    chart_link => $params{chart}->link,
+    transdate  => $self->transdate,
+    taxkey     => $tax->taxkey,
+    tax_id     => $tax->id,
+  );
+  $self->add_transactions( $acc );
+  push( @$acc_trans, $acc );
+  return $acc_trans;
+};
+
+sub add_ap_amount_row {
+  my ($self, %params ) = @_;
+
+  # only allow this method for ap invoices (Kreditorenbuchung)
+  die "not an ap invoice" if $self->invoice and not $self->vendor_id;
+
+  die "add_ap_amount_row needs a chart object as chart param" unless $params{chart} && $params{chart}->isa('SL::DB::Chart');
+  die unless $params{chart}->link =~ /AP_amount/;
+
+  my $acc_trans = [];
+
+  my $roundplaces = 2;
+  my ($netamount,$taxamount);
+
+  $netamount = $params{amount} * 1;
+  my $tax = SL::DB::Manager::Tax->find_by(id => $params{tax_id}) || die "Can't find tax with id " . $params{tax_id};
+
+  if ( $tax and $tax->rate != 0 ) {
+    ($netamount, $taxamount) = Form->calculate_tax($params{amount}, $tax->rate, $self->taxincluded, $roundplaces);
+  };
+  next unless $netamount; # netamount mustn't be zero
+
+  my $sign = $self->vendor_id ? -1 : 1;
+  my $acc = SL::DB::AccTransaction->new(
+    amount     => $netamount * $sign,
+    chart_id   => $params{chart}->id,
+    chart_link => $params{chart}->link,
+    transdate  => $self->transdate,
+    gldate     => $self->gldate,
+    taxkey     => $tax->taxkey,
+    tax_id     => $tax->id,
+    project_id => $params{project_id},
+  );
+
+  $self->add_transactions( $acc );
+  push( @$acc_trans, $acc );
+
+  if ( $taxamount ) {
+     my $acc = SL::DB::AccTransaction->new(
+       amount     => $taxamount * $sign,
+       chart_id   => $tax->chart_id,
+       chart_link => $tax->chart->link,
+       transdate  => $self->transdate,
+       gldate     => $self->gldate,
+       taxkey     => $tax->taxkey,
+       tax_id     => $tax->id,
+       project_id => $params{project_id},
+     );
+     $self->add_transactions( $acc );
+     push( @$acc_trans, $acc );
+  };
+  return $acc_trans;
+};
+
+sub mark_as_paid {
+  my ($self) = @_;
+
+  $self->update_attributes(paid => $self->amount);
+}
+
+sub effective_tax_point {
+  my ($self) = @_;
+
+  return $self->tax_point || $self->deliverydate || $self->transdate;
+}
+
 1;
+
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+SL::DB::PurchaseInvoice: Rose model for purchase invoices (table "ap")
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<mark_as_paid>
+
+Marks the invoice as paid by setting its C<paid> member to the value of C<amount>.
+
+=back
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
diff --git a/SL/DB/RecordTemplate.pm b/SL/DB/RecordTemplate.pm
new file mode 100644 (file)
index 0000000..1360c95
--- /dev/null
@@ -0,0 +1,182 @@
+package SL::DB::RecordTemplate;
+
+use strict;
+
+use DateTime::Format::Strptime;
+use List::Util qw(first);
+
+use SL::DB::MetaSetup::RecordTemplate;
+use SL::DB::Manager::RecordTemplate;
+
+__PACKAGE__->meta->add_relationship(
+  record_template_items => {
+    type       => 'one to many',
+    class      => 'SL::DB::RecordTemplateItem',
+    column_map => { id => 'record_template_id' },
+  },
+);
+
+__PACKAGE__->meta->initialize;
+
+sub items { goto &record_template_items; }
+
+sub _replace_variables {
+  my ($self, %params) = @_;
+
+  foreach my $sub (@{ $params{fields} }) {
+    my $value = $params{object}->$sub;
+    next if ($value // '') eq '';
+
+    $value =~ s{ <\% ([a-z0-9_]+) ( \s+ format \s*=\s* (.*?) \s* )? \%> }{
+      my ($key, $format) = ($1, $3);
+      my $new_value;
+
+      if (!$params{variables}->{$key}) {
+        $new_value = '';
+
+      } elsif ($format) {
+        $new_value = DateTime::Format::Strptime->new(
+          pattern     => $format,
+          locale      => 'de_DE',
+          time_zone   => 'local',
+        )->format_datetime($params{variables}->{$key}->[0]);
+
+      } else {
+        $new_value = $params{variables}->{$1}->[1]->($params{variables}->{$1}->[0]);
+      }
+
+      $new_value;
+
+    }eigx;
+
+    $params{object}->$sub($value);
+  }
+}
+
+sub _generate_variables {
+  my ($self, $reference_date) = @_;
+
+  $reference_date           //= DateTime->today_local;
+  my @month_names             = (
+    $::locale->text('January'), $::locale->text('February'), $::locale->text('March'),     $::locale->text('April'),   $::locale->text('May'),      $::locale->text('June'),
+    $::locale->text('July'),    $::locale->text('August'),   $::locale->text('September'), $::locale->text('October'), $::locale->text('November'), $::locale->text('December'),
+  );
+
+  my $variables = {
+    current_quarter     => [ $reference_date->clone->truncate(to => 'month'),                        sub { $_[0]->quarter } ],
+    previous_quarter    => [ $reference_date->clone->truncate(to => 'month')->subtract(months => 3), sub { $_[0]->quarter } ],
+    next_quarter        => [ $reference_date->clone->truncate(to => 'month')->add(     months => 3), sub { $_[0]->quarter } ],
+
+    current_month       => [ $reference_date->clone->truncate(to => 'month'),                        sub { $_[0]->month } ],
+    previous_month      => [ $reference_date->clone->truncate(to => 'month')->subtract(months => 1), sub { $_[0]->month } ],
+    next_month          => [ $reference_date->clone->truncate(to => 'month')->add(     months => 1), sub { $_[0]->month } ],
+
+    current_month_long  => [ $reference_date->clone->truncate(to => 'month'),                        sub { $month_names[ $_[0]->month - 1 ] } ],
+    previous_month_long => [ $reference_date->clone->truncate(to => 'month')->subtract(months => 1), sub { $month_names[ $_[0]->month - 1 ] } ],
+    next_month_long     => [ $reference_date->clone->truncate(to => 'month')->add(     months => 1), sub { $month_names[ $_[0]->month - 1 ] } ],
+
+    current_year        => [ $reference_date->clone->truncate(to => 'year'),                         sub { $_[0]->year } ],
+    previous_year       => [ $reference_date->clone->truncate(to => 'year')->subtract(years => 1),   sub { $_[0]->year } ],
+    next_year           => [ $reference_date->clone->truncate(to => 'year')->add(     years => 1),   sub { $_[0]->year } ],
+
+    reference_date      => [ $reference_date->clone,                                                 sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
+  };
+
+  return $variables;
+}
+
+sub _text_column_names {
+  my ($self, $object) = @_;
+  return map { $_->name } grep { ref($_) =~ m{::Text} } @{ $object->meta->columns };
+}
+
+sub substitute_variables {
+  my ($self, $reference_date) = @_;
+
+  my $variables    = $self->_generate_variables($reference_date);
+  my @text_columns = $self->_text_column_names($self);
+
+  $self->_replace_variables(
+    object    => $self,
+    variables => $variables,
+    fields    => \@text_columns,
+  );
+
+  @text_columns = $self->_text_column_names(SL::DB::RecordTemplateItem->new);
+
+  foreach my $item (@{ $self->items }) {
+    $self->_replace_variables(
+      object    => $item,
+      variables => $variables,
+      fields    => \@text_columns,
+    );
+  }
+}
+
+sub template_name_to_use {
+  my ($self, @names) = @_;
+
+  return first { ($_ // '') ne '' } (@names, $self->template_name, $::locale->text('unnamed record template'));
+}
+
+1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::RecordTemplate — Templates for accounts receivable
+transactions, accounts payable transactions and generic ledger
+transactiona
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<items>
+
+An alias for C<record_template_items>.
+
+=item C<substitute_variables> C<[$reference_date]>
+
+Texts in record templates can contain placeholders. This function
+replaces those placeholders by their actual value. Placeholders use
+the syntax C<E<lt>%variableE<gt>> or C<E<lt>%variable format=…E<gt>>
+for a custom format (see L<DateTime::Format::Strptime> for available
+formatting characters).
+
+The variables are calculated based on C<$reference_date> which must be
+an instance of L<DateTime> if given. If left out, it defaults to the
+current day.
+
+Supported variables are:
+
+=over 2
+
+=item * C<current_quarter>, C<previous_quarter>, C<next_quarter> — the
+quarter as a number between 1 and 4 inclusively
+
+=item * C<current_month>, C<previous_month>, C<next_month> — the
+month as a number between 1 and 12 inclusively
+
+=item * C<current_month_long>, C<previous_month_long>,
+C<next_month_long> — the month's name (e.g. C<August>).
+
+=item * C<current_year>, C<previous_year>, C<next_year> — the
+year (e.g. C<2017>)
+
+=item * C<reference_date> — the reference date in the user's date style
+(e.g. C<27.11.2017>)
+
+=back
+
+=back
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
diff --git a/SL/DB/RecordTemplateItem.pm b/SL/DB/RecordTemplateItem.pm
new file mode 100644 (file)
index 0000000..942936c
--- /dev/null
@@ -0,0 +1,10 @@
+package SL::DB::RecordTemplateItem;
+
+use strict;
+
+use SL::DB::MetaSetup::RecordTemplateItem;
+use SL::DB::Manager::RecordTemplateItem;
+
+__PACKAGE__->meta->initialize;
+
+1;
index 1cc1771..024e60d 100644 (file)
@@ -145,7 +145,7 @@ sub create_copy {
   return $self->_create_copy(%params) if $self->db->in_transaction;
 
   my $copy;
-  if (!$self->db->do_transaction(sub { $copy = $self->_create_copy(%params) })) {
+  if (!$self->db->with_transaction(sub { $copy = $self->_create_copy(%params) })) {
     $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
     return undef;
   }
@@ -156,7 +156,7 @@ sub create_copy {
 sub _create_copy {
   my ($self, %params) = @_;
 
-  my $copy = Rose::DB::Object::Helpers::clone_and_reset($self);
+  my $copy = $self->clone_and_reset;
   $copy->copy_from($self, %params);
 
   return $copy;
@@ -185,14 +185,14 @@ sub _copy_from {
   # Clone text blocks and pictures.
   my $clone_and_reset_position = sub {
     my ($src_obj) = @_;
-    my $cloned    = Rose::DB::Object::Helpers::clone_and_reset($src_obj);
+    my $cloned    = $src_obj->clone_and_reset;
     $cloned->position(undef);
     return $cloned;
   };
 
   my $clone_text_block = sub {
     my ($text_block) = @_;
-    my $cloned       = Rose::DB::Object::Helpers::clone_and_reset($text_block);
+    my $cloned       = $text_block->clone_and_reset;
     $cloned->position(undef);
     $cloned->pictures([ map { $clone_and_reset_position->($_) } @{ $text_block->pictures_sorted } ]);
     return $cloned;
@@ -220,7 +220,7 @@ sub _copy_from {
   my $clone_item;
   $clone_item = sub {
     my ($item) = @_;
-    my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
+    my $cloned = $item->clone_and_reset;
     $cloned->requirement_spec_id($self->id);
     $cloned->position(undef);
     $cloned->fb_number(undef) if $params->{paste_template};
index b75e053..0bcab3e 100644 (file)
@@ -1,6 +1,7 @@
 package SL::DB::SepaExportItem;
 
 use strict;
+use SL::DB::SepaExport;
 
 use SL::DB::MetaSetup::SepaExportItem;
 
@@ -20,4 +21,28 @@ sub compare_to {
   return $result || ($self->sepa_export_id <=> $other->sepa_export_id) || ($self->id <=> $other->id);
 }
 
+sub set_executed {
+  my ($self) = @_;
+
+  $self->executed(1); # does execution date also need to be set?
+  $self->save;
+  # if all the sepa_export_items in the sepa_export are closed (executed), close the sepa_export
+  if ( SL::DB::Manager::SepaExportItem->get_all_count( where => [ sepa_export_id => $self->sepa_export_id, executed => 0] ) == 0 ) {
+    my $sepa_export = SL::DB::Manager::SepaExport->find_by(id => $self->sepa_export_id);
+    $sepa_export->executed(1);
+    $sepa_export->closed(1);
+    $sepa_export->save(changes_only=>1);
+  };
+};
+
+
+sub arap_id {
+  my ($self) = @_;
+  if ( $self->ar_id ) {
+    return $self->ar_id;
+  } else {
+    return $self->ap_id;
+  };
+};
+
 1;
index b8868ab..6cfcba6 100644 (file)
@@ -2,15 +2,24 @@ package SL::DB::Shipto;
 
 use strict;
 
+use Carp;
+use List::MoreUtils qw(all);
+
+use SL::Util qw(trim);
+
 use SL::DB::MetaSetup::Shipto;
 use SL::DB::Manager::Shipto;
-use Rose::DB::Object::Helpers qw(clone_and_reset clone);
+use SL::DB::Helper::CustomVariables (
+  module      => 'ShipTo',
+  cvars_alias => 1,
+);
 
-our @SHIPTO_VARIABLES = qw(shiptoname shiptostreet shiptozipcode shiptocity shiptocountry shiptocontact
+our @SHIPTO_VARIABLES = qw(shiptoname shiptostreet shiptozipcode shiptocity shiptocountry shiptogln shiptocontact
                            shiptophone shiptofax shiptoemail shiptodepartment_1 shiptodepartment_2);
 
 __PACKAGE__->meta->initialize;
 
+
 sub displayable_id {
   my $self = shift;
   my $text = join('; ', grep { $_ } (map({ $self->$_ } qw(shiptoname shiptostreet)),
@@ -35,9 +44,108 @@ sub used {
       || SL::DB::Manager::DeliveryOrder->get_all_count(query => [ shipto_id => $self->shipto_id ]);
 }
 
+sub is_empty {
+  my ($self) = @_;
+
+  # todo: consider cvars
+  my @fields_to_consider = grep { !m{^ (?: itime | mtime | shipto_id | trans_id | shiptocp_gender | module ) $}x } map {$_->name} $self->meta->columns;
+
+  return all { trim($self->$_) eq '' } @fields_to_consider;
+}
+
 sub detach {
   $_[0]->trans_id(undef);
   $_[0];
 }
 
+sub clone {
+  my ($self, $target) = @_;
+
+  my $type   = ref($target) || $target;
+  my $module = $type =~ m{::Order$}               ? 'OE'
+             : $type =~ m{::DeliveryOrder$}       ? 'DO'
+             : $type =~ m{::Invoice$}             ? 'AR'
+             : $type =~ m{::(?:Customer|Vendor)$} ? 'CT'
+             :                                      croak "Unsupported target class '$type'";
+
+  my $new_shipto = SL::DB::Shipto->new(
+    (map  { +($_ => $self->$_) }
+     grep { !m{^ (?: itime | mtime | shipto_id | trans_id ) $}x }
+     map  { $_->name }
+     @{ $self->meta->columns }),
+    module           => $module,
+    custom_variables => [ map { $_->clone_and_reset } @{ $self->custom_variables } ],
+  );
+
+  return $new_shipto;
+}
+
 1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::Shipto - Database model for shipping addresses
+
+=head1 SYNOPSIS
+
+  my $order = SL::DB::Order->new(id => …)->load;
+  if ($order->custom_shipto) {
+    my $cloned_shipto = $order->custom_shipto->clone('SL::DB::Invoice');
+  }
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<is_empty>
+
+Returns truish if all fields to consider are empty, falsish if not.
+Fields are trimmed before the test is performed.
+C<shiptocp_gender> is not considered because in forms this is usually
+a selection with 'm' as default value.
+CVar fields are not considered by now.
+
+=back
+
+=over 4
+
+=item C<clone $target>
+
+Creates and returns a clone of the current object. The mandatory
+parameter C<$target> must be either an instance of a Rose DB class or
+the name of one. It's used for setting the new instance's C<module>
+attribute to the correct value.
+
+Currently the following classes are supported:
+
+=over 2
+
+=item C<SL::DB::Order>
+
+=item C<SL::DB::DeliveryOrder>
+
+=item C<SL::DB::Invoice>
+
+=item C<SL::DB::Customer>
+
+=item C<SL::DB::Vendor>
+
+=back
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
diff --git a/SL/DB/Shop.pm b/SL/DB/Shop.pm
new file mode 100644 (file)
index 0000000..bcae321
--- /dev/null
@@ -0,0 +1,88 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Shop;
+
+use strict;
+
+use SL::DB::MetaSetup::Shop;
+use SL::DB::Manager::Shop;
+use SL::DB::Helper::ActsAsList;
+use SL::Locale::String qw(t8);
+
+__PACKAGE__->meta->initialize;
+
+sub validate {
+  my ($self) = @_;
+
+  my @errors;
+  # critical checks
+  push @errors, $::locale->text('The description is missing.') unless $self->{description};
+  push @errors, $::locale->text('The path is missing.')        unless $self->{path};
+  push @errors, $::locale->text('The Host Name is missing')    unless $self->{server};
+  push @errors, $::locale->text('The Host Name seems invalid') unless $self->{server} =~ m/[0-9A-Za-z].\.[0-9A-Za-z]/;
+  push @errors, $::locale->text('The Protocol for Host Name seems invalid (expected: http:// or https://)!')
+                                                               if ($self->{server} =~ m/:/ && $self->{server} !~ m/(^https:\/\/|^http:\/\/)/);
+  push @errors, $::locale->text('The Proxy Name seems invalid') . $self->{proxy} . ':' unless !$self->{proxy} ||  $self->{proxy} =~ m/[0-9A-Za-z].\.[0-9A-Za-z]/;
+  push @errors, $::locale->text('Orders to fetch neeeds a positive Integer')
+                                                               unless $self->{orders_to_fetch} > 0;
+
+  # not yet implemented checks
+  if ($self->{connector} eq 'shopware6') {
+    push @errors, $::locale->text('Transaction Description is not yet implemented')  if $self->{transaction_description};
+    push @errors, $::locale->text('Shipping cost article is not implemented')        if $self->{shipping_costs_parts_id};
+    push @errors, $::locale->text('Fetch from last order number is not implemented') if $self->{last_order_number};
+  } else {
+    push @errors, $::locale->text('Use Long Description from Parts is only for Shopware6 implemented')
+      if $self->{use_part_longdescription};
+  }
+  return @errors;
+}
+
+sub shops_dd {
+  my ( $self ) = @_;
+
+  my @shops_dd = ( { title => t8("all") ,   value =>'' } );
+  my $shops = SL::DB::Manager::Shop->get_all( where => [ obsolete => 0 ] );
+  my @tmp = map { { title => $_->{description}, value => $_->{id} } } @{ $shops } ;
+  push @shops_dd, @tmp;
+  return \@shops_dd;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::DB::Shop - Model for the 'shops' table
+
+=head1 SYNOPSIS
+
+This is a standard Rose::DB::Object based model and can be used as one.
+
+=head1 METHODS
+
+=over 4
+
+=item C<validate>
+
+Returns an error if the shop description is missing
+
+=item C<shops_dd>
+
+Returns an array of hashes for dropdowns in filters
+
+=back
+
+=head1 AUTHORS
+
+Werner Hahn E<lt>wh@futureworldsearch.netE<gt>
+
+G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/DB/ShopImage.pm b/SL/DB/ShopImage.pm
new file mode 100644 (file)
index 0000000..b95a55c
--- /dev/null
@@ -0,0 +1,16 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::ShopImage;
+
+use strict;
+
+use SL::DB::MetaSetup::ShopImage;
+use SL::DB::Manager::ShopImage;
+use SL::DB::Helper::ActsAsList;
+
+__PACKAGE__->meta->initialize;
+
+__PACKAGE__->configure_acts_as_list(group_by => [qw(object_id)]);
+
+1;
diff --git a/SL/DB/ShopOrder.pm b/SL/DB/ShopOrder.pm
new file mode 100644 (file)
index 0000000..6a53387
--- /dev/null
@@ -0,0 +1,318 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::ShopOrder;
+
+use strict;
+
+use SL::DBUtils;
+use SL::DB::Shop;
+use SL::DB::MetaSetup::ShopOrder;
+use SL::DB::Manager::ShopOrder;
+use SL::DB::PaymentTerm;
+use SL::DB::Helper::LinkedRecords;
+use SL::Locale::String qw(t8);
+use Carp;
+
+__PACKAGE__->meta->add_relationships(
+  shop_order_items => {
+    class      => 'SL::DB::ShopOrderItem',
+    column_map => { id => 'shop_order_id' },
+    type       => 'one to many',
+  },
+);
+
+__PACKAGE__->meta->initialize;
+
+sub convert_to_sales_order {
+  my ($self, %params) = @_;
+
+  my $customer  = delete $params{customer};
+  my $employee  = delete $params{employee};
+  my $transdate = delete $params{transdate} // DateTime->today_local;
+  croak "param customer is missing" unless ref($customer) eq 'SL::DB::Customer';
+  croak "param employee is missing" unless ref($employee) eq 'SL::DB::Employee';
+
+  require SL::DB::Order;
+  require SL::DB::OrderItem;
+  require SL::DB::Part;
+  require SL::DB::Shipto;
+  my @error_report;
+
+  my @items = map{
+
+    my $part = SL::DB::Manager::Part->find_by(partnumber => $_->partnumber);
+
+    unless($part){
+      push @error_report, t8('Part with partnumber: #1 not found', $_->partnumber);
+    }else{
+      my $current_order_item = SL::DB::OrderItem->new(
+        parts_id            => $part->id,
+        description         => $_->description, # description from the shop
+        longdescription     => $part->notes,    # longdescription from parts. TODO locales
+        qty                 => $_->quantity,
+        sellprice           => $_->price,
+        unit                => $part->unit,
+        position            => $_->position,
+        active_price_source => $_->active_price_source,
+      );
+    }
+  }@{ $self->shop_order_items };
+
+  if(!scalar(@error_report)){
+
+    my $shipto_id;
+    if ($self->has_differing_delivery_address) {
+      if(my $address = SL::DB::Manager::Shipto->find_by( shiptoname   => $self->delivery_fullname,
+                                                         shiptostreet => $self->delivery_street,
+                                                         shiptocity   => $self->delivery_city,
+                                                        )) {
+        $shipto_id = $address->{shipto_id};
+      } else {
+        my $deliveryaddress = SL::DB::Shipto->new;
+        $deliveryaddress->assign_attributes(
+          shiptoname         => $self->delivery_fullname,
+          shiptodepartment_1 => $self->delivery_company,
+          shiptodepartment_2 => $self->delivery_department,
+          shiptostreet       => $self->delivery_street,
+          shiptozipcode      => $self->delivery_zipcode,
+          shiptocity         => $self->delivery_city,
+          shiptocountry      => $self->delivery_country,
+          trans_id           => $customer->id,
+          module             => "CT",
+        );
+        $deliveryaddress->save;
+        $shipto_id = $deliveryaddress->{shipto_id};
+      }
+    }
+
+    my $shop = SL::DB::Manager::Shop->find_by(id => $self->shop_id);
+    my $order = SL::DB::Order->new(
+      amount                  => $self->amount,
+      cusordnumber            => $self->shop_ordernumber,
+      customer_id             => $customer->id,
+      shipto_id               => $shipto_id,
+      orderitems              => [ @items ],
+      employee_id             => $employee->id,
+      intnotes                => $customer->notes,
+      salesman_id             => $employee->id,
+      taxincluded             => $self->tax_included,
+      payment_id              => $self->payment_id,
+      taxzone_id              => $customer->taxzone_id,
+      currency_id             => $customer->currency_id,
+      transaction_description => $shop->transaction_description,
+      transdate               => $transdate,
+    );
+     return $order;
+   }else{
+     my %error_order = (error   => 1,
+                        errors  => [ @error_report ],
+                       );
+     return \%error_order;
+   }
+};
+
+sub check_for_existing_customers {
+  my ($self, %params) = @_;
+  my $customers;
+
+  my $name             = $self->billing_lastname ne '' ? $self->billing_firstname . " " . $self->billing_lastname : '';
+  my $lastname         = $self->billing_lastname ne '' ? "%" . $self->billing_lastname . "%"                      : '';
+  my $company          = $self->billing_company  ne '' ? "%" . $self->billing_company  . "%"                      : '';
+  my $street           = $self->billing_street   ne '' ?  $self->billing_street                                   : '';
+  my $street_not_fuzzy = $self->billing_street   ne '' ?  "%" . $self->billing_street . "%"                       : '';
+  my $zipcode          = $self->billing_street   ne '' ?  $self->billing_zipcode                                  : '';
+  my $email            = $self->billing_street   ne '' ?  $self->billing_email                                    : '';
+
+  if(check_trgm($::form->get_standard_dbh())) {
+    # Fuzzysearch for street to find e.g. "Dorfstrasse - Dorfstr. - Dorfstraße"
+    my $fs_query = <<SQL;
+SELECT *
+FROM customer
+WHERE (
+   (
+    ( name ILIKE ? OR name ILIKE ? )
+      AND
+    zipcode ILIKE ?
+   )
+ OR
+   ( street % ?  AND zipcode ILIKE ?)
+ OR
+   ( email ILIKE ? OR invoice_mail ILIKE ? )
+) AND obsolete = 'F'
+SQL
+
+    my @values = ($lastname, $company, $self->billing_zipcode, $street, $self->billing_zipcode, $self->billing_email, $self->billing_email);
+
+    $customers = SL::DB::Manager::Customer->get_objects_from_sql(
+      sql  => $fs_query,
+      args => \@values,
+    );
+  }else{
+    # If trgm extension is not installed
+    $customers = SL::DB::Manager::Customer->get_all(
+      where => [
+                 or => [
+                   and => [
+                            or => [ 'name' => { ilike => $lastname },
+                                    'name' => { ilike => $company  },
+                            ],
+                            'zipcode' => { ilike => $zipcode },
+                   ],
+                   and => [
+                            and => [ 'street'  => { ilike => $street_not_fuzzy },
+                                     'zipcode' => { ilike => $zipcode },
+                            ],
+                   ],
+                   or  => [
+                            'email'        => { ilike => $email },
+                            'invoice_mail' => { ilike => $email },
+                   ],
+                 ],
+                 and => [ obsolete => 'F' ]
+      ],
+    );
+  }
+
+  return $customers;
+}
+
+sub check_for_open_invoices {
+  my ($self) = @_;
+    my $open_invoices = SL::DB::Manager::Invoice->get_all_count(
+      query => [customer_id => $self->{kivi_customer_id},
+              paid => {lt_sql => 'amount'},
+      ],
+    );
+  return $open_invoices;
+}
+
+sub get_customer{
+  my ($self, %params) = @_;
+  my $shop = SL::DB::Manager::Shop->find_by(id => $self->shop_id);
+  my $customer_proposals = $self->check_for_existing_customers;
+  my $name = $self->billing_firstname . " " . $self->billing_lastname;
+  my $customer = 0;
+  my $default_payment    = SL::DB::Manager::PaymentTerm->get_first();
+  my $payment_id = $default_payment ? $default_payment->id : undef;
+  if(!scalar(@{$customer_proposals})){
+    my %address = ( 'name'                  => $name,
+                    'department_1'          => $self->billing_company,
+                    'department_2'          => $self->billing_department,
+                    'street'                => $self->billing_street,
+                    'zipcode'               => $self->billing_zipcode,
+                    'city'                  => $self->billing_city,
+                    'email'                 => $self->billing_email,
+                    'invoice_mail'          => $self->billing_email,
+                    'country'               => $self->billing_country,
+                    'greeting'              => $self->billing_greeting,
+                    'fax'                   => $self->billing_fax,
+                    'phone'                 => $self->billing_phone,
+                    'ustid'                 => $self->billing_vat,
+                    'taxincluded_checked'   => $shop->pricetype eq "brutto" ? 1 : 0,
+                    'taxincluded'           => $shop->pricetype eq "brutto" ? 1 : 0,
+                    'pricegroup_id'         => (split '\/',$shop->price_source)[0] eq "pricegroup" ?  (split '\/',$shop->price_source)[1] : undef,
+                    'taxzone_id'            => $shop->taxzone_id,
+                    'currency'              => $::instance_conf->get_currency_id,
+                    'payment_id'            => $payment_id,
+                  );
+    $customer = SL::DB::Customer->new(%address);
+
+    $customer->save;
+    my $snumbers = "customernumber_" . $customer->customernumber;
+    SL::DB::History->new(
+                      trans_id    => $customer->id,
+                      snumbers    => $snumbers,
+                      employee_id => SL::DB::Manager::Employee->current->id,
+                      addition    => 'SAVED',
+                      what_done   => 'Shopimport',
+                    )->save();
+
+  }elsif(scalar(@{$customer_proposals}) == 1){
+    # check if the proposal is the right customer, could be different names under the same address. Depends on how first- and familyname is handled. Here is for customername = companyname or customername = "firstname familyname"
+    $customer = SL::DB::Manager::Customer->find_by( id       => $customer_proposals->[0]->id,
+                                                    name     => $name,
+                                                    email    => $self->billing_email,
+                                                    street   => $self->billing_street,
+                                                    zipcode  => $self->billing_zipcode,
+                                                    city     => $self->billing_city,
+                                                    obsolete => 'F',
+                                                  );
+  }
+  $customer->update_attributes(invoice_mail => $self->billing_email) if $customer->invoice_mail ne $self->billing_email;
+
+  return $customer;
+}
+
+sub compare_to {
+  my ($self, $other) = @_;
+
+  return  1 if  $self->transfer_date && !$other->transfer_date;
+  return -1 if !$self->transfer_date &&  $other->transfer_date;
+
+  my $result = 0;
+  $result    = $self->transfer_date <=> $other->transfer_date if $self->transfer_date;
+  return $result || ($self->id <=> $other->id);
+}
+
+sub has_differing_delivery_address {
+  my ($self) = @_;
+  ($self->billing_firstname // '') ne ($self->delivery_firstname // '') ||
+  ($self->billing_lastname  // '') ne ($self->delivery_lastname  // '') ||
+  ($self->billing_city      // '') ne ($self->delivery_city      // '') ||
+  ($self->billing_street    // '') ne ($self->delivery_street    // '')
+}
+
+sub delivery_fullname {
+  ($_[0]->delivery_firstname // '') . " " . ($_[0]->delivery_lastname // '')
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::DB::ShopOrder - Model for the 'shop_orders' table
+
+=head1 SYNOPSIS
+
+This is a standard Rose::DB::Object based model and can be used as one.
+
+=head1 METHODS
+
+=over 4
+
+=item C<convert_to_sales_order>
+
+=item C<check_for_existing_customers>
+
+Inexact search for possible matches with existing customers in the database.
+
+Returns all found customers as an arrayref of SL::DB::Customer objects.
+
+=item C<get_customer>
+
+returns only one customer from the check_for_existing_customers if the return from it is 0 or 1 customer.
+
+When it is 0 get customer creates a new customer object of the shop order billing data and returns it
+
+=item C<compare_to>
+
+=back
+
+=head1 TODO
+
+some variables like payments could be better implemented. Transaction description is hardcoded
+
+=head1 AUTHORS
+
+Werner Hahn E<lt>wh@futureworldsearch.netE<gt>
+
+G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/DB/ShopOrderItem.pm b/SL/DB/ShopOrderItem.pm
new file mode 100644 (file)
index 0000000..34333aa
--- /dev/null
@@ -0,0 +1,13 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::ShopOrderItem;
+
+use strict;
+
+use SL::DB::MetaSetup::ShopOrderItem;
+use SL::DB::Manager::ShopOrderItem;
+
+__PACKAGE__->meta->initialize;
+
+1;
diff --git a/SL/DB/ShopPart.pm b/SL/DB/ShopPart.pm
new file mode 100644 (file)
index 0000000..7c67bb1
--- /dev/null
@@ -0,0 +1,115 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::ShopPart;
+
+use strict;
+
+use SL::DBUtils;
+use SL::DB::MetaSetup::ShopPart;
+use SL::DB::Manager::ShopPart;
+use SL::DB::Helper::AttrHTML;
+#use SL::DB::Helper::ActsAsList;
+
+__PACKAGE__->meta->initialize;
+__PACKAGE__->attr_html('shop_description');
+
+sub get_tax_and_price {
+  my ( $self ) = @_;
+
+  require SL::DB::Part;
+  my $tax_n_price;
+  my ( $price_src_str, $price_src_id ) = split(/\//,$self->active_price_source);
+  my $price;
+  my $part;
+  if ($price_src_str eq "master_data") {
+    $part = SL::DB::Manager::Part->find_by( id => $self->part_id );
+    $price = $part->$price_src_id;
+  }else{
+    $part = SL::DB::Manager::Part->find_by( id => $self->part_id );
+    $price =  $part->prices->[0]->price;
+  }
+
+  my $taxrate;
+  my $dbh  = $::form->get_standard_dbh();
+  my $b_id = $part->buchungsgruppen_id;
+  my $t_id = $self->shop->taxzone_id;
+
+  my $sql_str = "SELECT a.rate AS taxrate from tax a
+  WHERE a.taxkey = (SELECT b.taxkey_id
+  FROM chart b LEFT JOIN taxzone_charts c ON b.id = c.income_accno_id
+  WHERE c.taxzone_id = $t_id
+  AND c.buchungsgruppen_id = $b_id)";
+
+  my $rate = selectall_hashref_query($::form, $dbh, $sql_str);
+  $taxrate = @$rate[0]->{taxrate}*100;
+
+  $tax_n_price->{price} = $price;
+  $tax_n_price->{tax}   = $taxrate;
+  return $tax_n_price;
+}
+
+sub get_images {
+  my ($self, %params) = @_;
+
+  require SL::DB::ShopImage;
+  my $images = SL::DB::Manager::ShopImage->get_all( where => [ 'files.object_id' => $self->{part_id}, ], with_objects => 'file', sort_by => 'position' );
+  my @upload_img = ();
+  foreach my $img (@{ $images }) {
+    my $file               = SL::File->get(id => $img->file->id );
+    # no good: split("\." , 202.220.pdf) -> invaild extension 220
+    # file->extension should be in SL::File, a valid extension may also be 'tar.gz'
+    my ($path, $extension) = split(/\.([^\.]+)$/, $file->file_name);
+    my $content            = File::Slurp::read_file($file->get_file);
+
+    my $temp ={ (
+                  link        => $params{want_binary} ? $content : 'data:' . $file->mime_type . ';base64,' . MIME::Base64::encode($content, ""),
+                  description => $img->file->title,
+                  position    => $img->position,
+                  extension   => $extension,
+                  path        => $path,
+                      )}    ;
+    push( @upload_img, $temp);
+  }
+  return @upload_img;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::DB::ShopPart - Model for the 'shop_parts' table
+
+=head1 SYNOPSIS
+
+This is a standard Rose::DB::Object based model and can be used as one.
+
+=head1 METHODS
+
+=over 4
+
+=item C<get_tax_and_price>
+
+Returns the price and the taxrate for an shop_article
+
+=item C<get_images>
+
+Returns the images for the shop_article
+
+=back
+
+=head1 TODO
+
+Prices, pricesources, pricerules could be implemented
+
+=head1 AUTHORS
+
+Werner Hahn E<lt>wh@futureworldsearch.netE<gt>
+
+=cut
diff --git a/SL/DB/Stocktaking.pm b/SL/DB/Stocktaking.pm
new file mode 100644 (file)
index 0000000..4ecd476
--- /dev/null
@@ -0,0 +1,18 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Stocktaking;
+
+use strict;
+
+use SL::DB::MetaSetup::Stocktaking;
+use SL::DB::Manager::Stocktaking;
+
+__PACKAGE__->meta->initialize;
+
+# part accessor is badly named
+sub part {
+  goto &parts;
+}
+
+1;
diff --git a/SL/DB/TimeRecording.pm b/SL/DB/TimeRecording.pm
new file mode 100644 (file)
index 0000000..575e219
--- /dev/null
@@ -0,0 +1,138 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::TimeRecording;
+
+use strict;
+
+use SL::Locale::String qw(t8);
+
+use SL::DB::Helper::AttrDuration;
+use SL::DB::Helper::AttrHTML;
+
+use SL::DB::MetaSetup::TimeRecording;
+use SL::DB::Manager::TimeRecording;
+
+__PACKAGE__->meta->initialize;
+
+__PACKAGE__->attr_duration_minutes(qw(duration));
+
+__PACKAGE__->attr_html('description');
+
+__PACKAGE__->before_save('_before_save_check_valid');
+
+sub _before_save_check_valid {
+  my ($self) = @_;
+
+  my @errors = $self->validate;
+  return (scalar @errors == 0);
+}
+
+sub validate {
+  my ($self) = @_;
+
+  my @errors;
+
+  push @errors, t8('Customer must not be empty.')                              if !$self->customer_id;
+  push @errors, t8('Staff member must not be empty.')                          if !$self->staff_member_id;
+  push @errors, t8('Employee must not be empty.')                              if !$self->employee_id;
+  push @errors, t8('Description must not be empty.')                           if !$self->description;
+  push @errors, t8('Start time must be earlier than end time.')                if $self->is_time_in_wrong_order;
+  push @errors, t8('Assigned order must be a sales order.')                    if $self->order_id && 'sales_order' ne $self->order->type;
+  push @errors, t8('Customer of assigned order must match customer.')          if $self->order_id && $self->order->customer_id != $self->customer_id;
+  push @errors, t8('Customer of assigned project must match customer.')        if $self->project_id && $self->project->customer_id && $self->project->customer_id != $self->customer_id;
+  push @errors, t8('Project of assigned order must match assigned project.')
+    if $self->project_id && $self->order_id && $self->order->globalproject_id && $self->project_id != $self->order->globalproject_id;
+
+  my $conflict = $self->is_time_overlapping;
+  push @errors, t8('Entry overlaps with "#1".', $conflict->displayable_times)  if $conflict;
+
+  return @errors;
+}
+
+sub is_time_overlapping {
+  my ($self) = @_;
+
+  # Do not allow overlapping time periods.
+  # Start time can be equal to another end time
+  # (an end time can be equal to another start time)
+
+  # We cannot check if no staff member is given.
+  return if !$self->staff_member_id;
+
+  # If no start time and no end time are given, there is no overlapping.
+  return if !($self->start_time || $self->end_time);
+
+  my $conflicting;
+
+  # Start time or end time can be undefined.
+  if (!$self->start_time) {
+    $conflicting = SL::DB::Manager::TimeRecording->get_all(where  => [ and => [ '!id'           => $self->id,
+                                                                                staff_member_id => $self->staff_member_id,
+                                                                                start_time      => {lt => $self->end_time},
+                                                                                end_time        => {ge => $self->end_time} ] ],
+                                                           sort_by => 'start_time DESC',
+                                                           limit   => 1);
+  } elsif (!$self->end_time) {
+    $conflicting = SL::DB::Manager::TimeRecording->get_all(where  => [ and => [ '!id'           => $self->id,
+                                                                                staff_member_id => $self->staff_member_id,
+                                                                                or              => [ and => [start_time => {le => $self->start_time},
+                                                                                                             end_time   => {gt => $self->start_time} ],
+                                                                                                     start_time => $self->start_time,
+                                                                                ],
+                                                                       ],
+                                                           ],
+                                                           sort_by => 'start_time DESC',
+                                                           limit   => 1);
+  } else {
+    $conflicting = SL::DB::Manager::TimeRecording->get_all(where  => [ and => [ '!id'           => $self->id,
+                                                                                staff_member_id => $self->staff_member_id,
+                                                                                or              => [ and => [ start_time => {lt => $self->end_time},
+                                                                                                              end_time   => {gt => $self->start_time} ] ,
+                                                                                                     or  => [ start_time => $self->start_time,
+                                                                                                              end_time   => $self->end_time, ],
+                                                                                ]
+                                                                       ]
+                                                           ],
+                                                           sort_by => 'start_time DESC',
+                                                           limit   => 1);
+  }
+
+  return $conflicting->[0] if @$conflicting;
+  return;
+}
+
+sub is_time_in_wrong_order {
+  my ($self) = @_;
+
+  if ($self->start_time && $self->end_time
+      && $self->start_time >= $self->end_time) {
+    return 1;
+  }
+
+  return;
+}
+
+sub is_duration_used {
+  return !$_[0]->start_time;
+}
+
+sub displayable_times {
+  my ($self) = @_;
+
+  my $text;
+
+  if ($self->is_duration_used) {
+    $text = $self->date_as_date . ': ' . ($self->duration_as_duration_string || '--:--');
+
+  } else {
+    # placeholder
+    my $ph =  $::locale->format_date_object(DateTime->new(year => 1111, month => 11, day => 11, hour => 11, minute => 11), precision => 'minute');
+    $ph    =~ s{1}{-}g;
+    $text  =  ($self->start_time_as_timestamp||$ph) . ' - ' . ($self->end_time_as_timestamp||$ph);
+  }
+
+  return $text;
+}
+
+1;
diff --git a/SL/DB/TimeRecordingArticle.pm b/SL/DB/TimeRecordingArticle.pm
new file mode 100644 (file)
index 0000000..6348378
--- /dev/null
@@ -0,0 +1,16 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::TimeRecordingArticle;
+
+use strict;
+
+use SL::DB::MetaSetup::TimeRecordingArticle;
+use SL::DB::Manager::TimeRecordingArticle;
+
+use SL::DB::Helper::ActsAsList;
+
+__PACKAGE__->meta->initialize;
+
+
+1;
index f5f1bc1..508aa74 100644 (file)
@@ -57,7 +57,7 @@ sub convert_to {
   my $my_base_factor    = $self->base_factor       || 1;
   my $other_base_factor = $other_unit->base_factor || 1;
 
-  return $qty * $my_base_factor / $other_base_factor;
+  return ($qty // 0) * $my_base_factor / $other_base_factor;
 }
 
 sub is_time_based {
diff --git a/SL/DB/UserPreference.pm b/SL/DB/UserPreference.pm
new file mode 100644 (file)
index 0000000..c7a7a35
--- /dev/null
@@ -0,0 +1,13 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::UserPreference;
+
+use strict;
+
+use SL::DB::MetaSetup::UserPreference;
+use SL::DB::Manager::UserPreference;
+
+__PACKAGE__->meta->initialize;
+
+1;
index c41f287..3cc7490 100644 (file)
@@ -2,11 +2,13 @@ package SL::DB::VC;
 
 use strict;
 
-require Exporter;
+use List::MoreUtils qw(uniq);
 use SL::DBUtils;
 
+require Exporter;
+
 our @ISA    = qw(Exporter);
-our @EXPORT = qw(get_credit_remaining);
+our @EXPORT = qw(get_credit_remaining get_all_email_addresses);
 
 sub get_credit_remaining {
   my $vc               = shift;
@@ -50,4 +52,42 @@ SQL
   return $credit_remaining;
 }
 
+sub get_all_email_addresses {
+  my ($self) = @_;
+
+  my $is_sales = ref $self eq 'SL::DB::Customer';
+
+  my @addresses;
+
+  # billing address
+  push @addresses, $self->$_ for qw(email cc bcc);
+  if ($is_sales) {
+    push @addresses, $self->$_ for qw(delivery_order_mail invoice_mail);
+  }
+
+  # additional billing addresses
+  if ($is_sales) {
+    foreach my $additional_billing_address (@{ $self->additional_billing_addresses }) {
+      push @addresses, $additional_billing_address->$_ for qw(email);
+    }
+  }
+
+  # contacts
+  foreach my $contact (@{ $self->contacts }) {
+    push @addresses, $contact->$_ for qw(cp_email cp_privatemail);
+  }
+
+  # shiptos
+  foreach my $shipto (@{ $self->shipto }) {
+    push @addresses, $shipto->$_ for qw(shiptoemail);
+  }
+
+  # remove empty ones and duplicates
+  @addresses = grep { $_ } @addresses;
+  @addresses = uniq @addresses;
+
+
+  return \@addresses;
+}
+
 1;
index b1458ef..6844b63 100644 (file)
@@ -4,13 +4,27 @@ use strict;
 
 use Rose::DB::Object::Helpers qw(as_tree);
 
+use SL::Locale::String qw(t8);
+use SL::DBUtils ();
 use SL::DB::MetaSetup::Vendor;
 use SL::DB::Manager::Vendor;
+use SL::DB::Helper::IBANValidation;
 use SL::DB::Helper::TransNumberGenerator;
+use SL::DB::Helper::VATIDNrValidation;
 use SL::DB::Helper::CustomVariables (
   module      => 'CT',
   cvars_alias => 1,
 );
+use SL::DB::Helper::DisplayableNamePreferences (
+  title   => t8('Vendor'),
+  options => [ {name => 'vendornumber', title => t8('Vendor Number') },
+               {name => 'name',         title => t8('Name')   },
+               {name => 'street',         title => t8('Street') },
+               {name => 'city',           title => t8('City') },
+               {name => 'zipcode',        title => t8('Zipcode')},
+               {name => 'email',          title => t8('E-Mail') },
+               {name => 'phone',          title => t8('Phone')  }, ]
+);
 
 use SL::DB::VC;
 
@@ -46,18 +60,37 @@ sub validate {
 
   my @errors;
   push @errors, $::locale->text('The vendor name is missing.') if !$self->name;
+  push @errors, $self->validate_ibans;
+  push @errors, $self->validate_vat_id_numbers;
 
   return @errors;
 }
 
-sub displayable_name {
-  my $self = shift;
-
-  return join ' ', grep $_, $self->vendornumber, $self->name;
-}
-
 sub is_customer { 0 };
 sub is_vendor   { 1 };
 sub payment_terms { goto &payment }
+sub number { goto &vendornumber }
+
+sub last_used_ap_chart {
+  my ($self) = @_;
+
+  my $query = <<EOSQL;
+    SELECT c.id
+    FROM chart c
+    JOIN acc_trans ac ON (ac.chart_id = c.id)
+    JOIN ap a         ON (a.id        = ac.trans_id)
+    WHERE (a.vendor_id = ?)
+      AND (c.category = 'E')
+      AND (c.link !~ '_(paid|tax)')
+      AND (a.id IN (SELECT max(a2.id) FROM ap a2 WHERE a2.vendor_id = ?))
+    ORDER BY ac.acc_trans_id ASC
+    LIMIT 1
+EOSQL
+
+  my ($chart_id) = SL::DBUtils::selectfirst_array_query($::form, $self->db->dbh, $query, ($self->id) x 2);
+
+  return if !$chart_id;
+  return SL::DB::Chart->load_cached($chart_id);
+}
 
 1;
index 9f2472c..9fcf7f4 100644 (file)
@@ -119,7 +119,7 @@ optionally routing through DBIx::Log4perl
 
 Connects to the database. If the configuration parameter
 C<debug.dbix_log4perl> is set then the call is made through
-L<DBIx::Log4per/connect>. Otherwise L<DBI/connect> is called directly.
+L<DBIx::Log4perl/connect>. Otherwise L<DBI/connect> is called directly.
 
 In each case C<@dbi_args> is passed through as-is.
 
index 7e4633b..c133e8a 100644 (file)
@@ -43,6 +43,11 @@ sub clear {
   %cache = ();
 }
 
+sub disconnect_all_and_clear {
+  $_->disconnect for values %cache;
+  %cache = ();
+}
+
 sub _args2str {
   my (@args) = @_;
 
index 4fb1ba9..0b14eca 100644 (file)
@@ -7,6 +7,7 @@ use List::MoreUtils qw(any);
 use SL::Common;
 use SL::DBUpgrade2::Base;
 use SL::DBUtils;
+use SL::System::Process;
 
 use strict;
 
@@ -26,7 +27,7 @@ sub init {
 
   $params{path_suffix} ||= '';
   $params{schema}      ||= '';
-  $params{path}        ||= "sql/Pg-upgrade2" . $params{path_suffix};
+  $params{path}        ||= SL::System::Process->exe_dir . "/sql/Pg-upgrade2" . $params{path_suffix};
 
   map { $self->{$_} = $params{$_} } keys %params;
 
@@ -55,9 +56,10 @@ sub parse_dbupdate_controls {
     $file =~ s|.*/||;
 
     my $control = {
-      "priority" => 1000,
-      "depends"  => [],
-      "locales"  => [],
+      priority    => 1000,
+      depends     => [],
+      required_by => [],
+      locales     => [],
     };
 
     while (<IN>) {
@@ -70,8 +72,8 @@ sub parse_dbupdate_controls {
       my @fields = split(/\s*:\s*/, $_, 2);
       next unless (scalar(@fields) == 2);
 
-      if ($fields[0] eq "depends") {
-        push(@{$control->{"depends"}}, split(/\s+/, $fields[1]));
+      if ($fields[0] =~ m{^(?:depends|required_by)$}) {
+        push(@{$control->{$fields[0]}}, split(/\s+/, $fields[1]));
       } elsif ($fields[0] eq "locales") {
         push @{$control->{locales}}, $fields[1];
       } else {
@@ -97,17 +99,31 @@ sub parse_dbupdate_controls {
       _control_error($form, $file_name, sprintf($locale->text("Missing 'description' field."))) ;
     }
 
+    delete @{$control}{qw(depth applied)};
+
+    my @unknown_keys = grep { !m{^ (?: depends | required_by | description | file | ignore | locales | may_fail | priority | superuser_privileges | tag ) $}x } keys %{ $control };
+    if (@unknown_keys) {
+      _control_error($form, $file_name, sprintf($locale->text("Unknown control fields: #1", join(' ', sort({ lc $a cmp lc $b } @unknown_keys)))));
+    }
+
     $control->{"priority"}  *= 1;
     $control->{"priority"} ||= 1000;
     $control->{"file"}       = $file;
 
-    delete @{$control}{qw(depth applied)};
-
     $all_controls{$control->{"tag"}} = $control;
 
     close(IN);
   }
 
+  foreach my $name (keys %all_controls) {
+    my $control = $all_controls{$name};
+
+    foreach my $dependency (@{ delete $control->{required_by} }) {
+      _control_error($form, $control->{"file"}, sprintf($locale->text("Unknown dependency '%s'."), $dependency)) if (!defined($all_controls{$dependency}));
+      push @{ $all_controls{$dependency}->{depends} }, $name;
+    }
+  }
+
   foreach my $control (values(%all_controls)) {
     foreach my $dependency (@{$control->{"depends"}}) {
       _control_error($form, $control->{"file"}, sprintf($locale->text("Unknown dependency '%s'."), $dependency)) if (!defined($all_controls{$dependency}));
@@ -147,9 +163,6 @@ sub process_query {
     # Remove DOS and Unix style line endings.
     chomp;
 
-    # remove comments
-    s/--.*$//;
-
     for (my $i = 0; $i < length($_); $i++) {
       my $char = substr($_, $i, 1);
 
@@ -176,6 +189,11 @@ sub process_query {
              &&  $tag      =~ /^ (?= [A-Za-z_] [A-Za-z0-9_]* | ) $/x) {  # tag is identifier
           push @quote_chars, $char = '$' . $tag . '$';
           $i = $tag_end;
+        } elsif ($char eq "-") {
+          if ( substr($_, $i+1, 1) eq "-") {
+            # found a comment outside quote
+            last;
+          }
         } elsif ($char eq ";") {
 
           # Query is complete. Send it.
@@ -186,11 +204,13 @@ sub process_query {
             return $errstr // '<unknown database error>' if $self->{return_on_error};
             $sth->finish();
             $dbh->rollback();
-            $form->dberror("The database update/creation did not succeed. " .
-                           "The file ${filename} containing the following " .
-                           "query failed:<br>${query}<br>" .
-                           "The error message was: ${errstr}<br>" .
-                           "All changes in that file have been reverted.");
+            if (!ref $version_or_control || ref $version_or_control ne 'HASH' || !$version_or_control->{may_fail})  {
+              $form->dberror("The database update/creation did not succeed. " .
+                             "The file ${filename} containing the following " .
+                             "query failed:<br>${query}<br>" .
+                             "The error message was: ${errstr}<br>" .
+                             "All changes in that file have been reverted.")
+            }
           }
           $sth->finish();
 
@@ -234,12 +254,11 @@ sub process_perl_script {
 
   my ($self, $dbh, $filename, $version_or_control) = @_;
 
-  my %form_values = map { $_ => $::form->{$_} } qw(dbconnect dbdefault dbhost dbname dboptions dbpasswd dbport dbupdate dbuser login template_object version);
+  my %form_values = %$::form;
 
   $dbh->begin_work;
 
   # setup dbup_ export vars & run script
-  my $old_dbh       = $::form->set_standard_dbh($dbh);
   my %dbup_myconfig = map { ($_ => $::form->{$_}) } qw(dbname dbuser dbpasswd dbhost dbport dbconnect);
   my $result        = eval {
     SL::DBUpgrade2::Base::execute_script(
@@ -252,18 +271,16 @@ sub process_perl_script {
 
   my $error = $EVAL_ERROR;
 
-  $::form->set_standard_dbh($old_dbh);
-
   $dbh->rollback if 1 != ($result // -1);
 
   return $error if $self->{return_on_error} && (1 != ($result // -1));
 
   if (!defined($result)) {
     print $::form->parse_html_template("dbupgrade/error", { file  => $filename, error => $error });
-    ::end_of_request();
+    $::dispatcher->end_request;
   } elsif (1 != $result) {
     SL::System::InstallationLock->unlock if 2 == $result;
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
 
   if (ref($version_or_control) eq "HASH") {
@@ -289,8 +306,12 @@ sub process_perl_script {
 sub process_file {
   my ($self, $dbh, $filename, $version_or_control) = @_;
 
-  return $filename =~ m/sql$/ ? $self->process_query(      $dbh, $filename, $version_or_control)
-                              : $self->process_perl_script($dbh, $filename, $version_or_control);
+  my $result = $filename =~ m/sql$/ ? $self->process_query(      $dbh, $filename, $version_or_control)
+                                    : $self->process_perl_script($dbh, $filename, $version_or_control);
+
+  $::lxdebug->log_time("DB upgrade script '${filename}' finished");
+
+  return $result;
 }
 
 sub unapplied_upgrade_scripts {
@@ -309,14 +330,6 @@ sub unapplied_upgrade_scripts {
   return grep { !$_->{applied} } @all_scripts;
 }
 
-sub update2_available {
-  my ($self, $dbh) = @_;
-
-  my @unapplied_scripts = $self->unapplied_upgrade_scripts($dbh);
-
-  return !!@unapplied_scripts;
-}
-
 sub apply_admin_dbupgrade_scripts {
   my ($self, $called_from_admin) = @_;
 
@@ -336,6 +349,8 @@ sub apply_admin_dbupgrade_scripts {
 
   print $self->{form}->parse_html_template("dbupgrade/header", { dbname => $::auth->{DB_config}->{db} });
 
+  $::lxdebug->log_time("DB upgrades commencing");
+
   foreach my $control (@unapplied_scripts) {
     $::lxdebug->message(LXDebug->DEBUG2(), "Applying Update $control->{file}");
     print $self->{form}->parse_html_template("dbupgrade/upgrade_message2", $control);
@@ -343,6 +358,8 @@ sub apply_admin_dbupgrade_scripts {
     $self->process_file($dbh, "sql/Pg-upgrade2-auth/$control->{file}", $control);
   }
 
+  $::lxdebug->log_time("DB upgrades finished");
+
   print $self->{form}->parse_html_template("dbupgrade/footer", { is_admin => 1 }) if $called_from_admin;
 
   return 1;
@@ -608,7 +625,7 @@ The global C<SL::Form> object.
 =back
 
 A Perl script can actually implement queries that fail while
-continueing the process by handling the transaction itself, e.g. with
+continuing the process by handling the transaction itself, e.g. with
 the following function:
 
   sub do_query {
@@ -642,14 +659,6 @@ to the database that is checked.
 
 Requires that the scripts have been parsed.
 
-=item C<update2_available $dbh>
-
-Returns trueish if at least one upgrade script hasn't been applied to
-a database yet. C<$dbh> is an open handle to the database that is
-checked.
-
-Requires that the scripts have been parsed.
-
 =back
 
 =head1 BUGS
index 00df6d2..cd698c5 100644 (file)
@@ -11,6 +11,8 @@ use File::Basename ();
 use File::Copy ();
 use File::Path ();
 use List::MoreUtils qw(uniq);
+use SL::DBUtils qw(selectfirst_hashref_query);
+use SL::Presenter::EscapedText qw(escape);
 use version;
 
 use Rose::Object::MakeMethods::Generic (
@@ -29,9 +31,10 @@ sub execute_script {
     die $EVAL_ERROR;
   }
 
+  my $auth    =  $file_name =~ m{/Pg-upgrade2-auth/} ? 'Auth::' : '';
   my $package =  delete $params{tag};
   $package    =~ s/[^a-zA-Z0-9_]+/_/g;
-  $package    =  "SL::DBUpgrade2::${package}";
+  $package    =  "SL::DBUpgrade2::${auth}${package}";
 
   $package->new(%params)->run;
 }
@@ -97,7 +100,9 @@ sub add_print_templates {
     croak "File '${src_dir}/$_' does not exist" unless -f "${src_dir}/$_";
   }
 
-  return 1 unless my $template_dir = $::instance_conf->reload->get_templates;
+  # can't use Rose or InstanceConf here because defaults might not be fully upgraded yet.
+  my $defaults = selectfirst_hashref_query($::form, $::form->get_standard_dbh, "SELECT * FROM defaults");
+  return 1 unless my $template_dir = $defaults->{template};
   $::lxdebug->message(LXDebug::DEBUG1(), "add_print_templates: template_dir $template_dir");
 
   foreach my $src_file (@files) {
@@ -140,6 +145,26 @@ SQL
   $self->db_query(qq|ALTER TABLE $params{schema}."$params{table}" DROP CONSTRAINT "${_}"|) for map { $_->[0] } @{ $constraints };
 }
 
+sub convert_column_to_html {
+  my ($self, $table, $column) = @_;
+
+  my $sth = $self->dbh->prepare(qq|UPDATE $table SET $column = ? WHERE id = ?|) || $self->dberror;
+
+  foreach my $row (selectall_hashref_query($::form, $self->dbh, qq|SELECT id, $column FROM $table WHERE $column IS NOT NULL|)) {
+    next if !$row->{$column} || (($row->{$column} =~ m{^<[a-z]+>}) && ($row->{$column} =~ m{</[a-z]+>$}));
+
+    my $new_content = "" . escape($row->{$column});
+    $new_content    =~ s{\r}{}g;
+    $new_content    =~ s{\n\n+}{</p><p>}g;
+    $new_content    =~ s{\n}{<br />}g;
+    $new_content    =  "<p>${new_content}</p>" if $new_content;
+
+    $sth->execute($new_content, $row->{id}) if $new_content ne $row->{$column};
+  }
+
+  $sth->finish;
+}
+
 1;
 __END__
 
@@ -314,6 +339,11 @@ C<acc_trans> yet.
 This method is the entry point for the actual upgrade. Each upgrade
 script must provide this method.
 
+=item C<convert_column_to_html $table, $column>
+
+Converts the content of a single column from text to HTML suitable for
+use with the ckeditor.
+
 =back
 
 =head1 BUGS
index 8976144..f095244 100644 (file)
@@ -1,17 +1,19 @@
 package SL::DBUtils;
 
+use SL::Util qw(trim);
+
 require Exporter;
 our @ISA = qw(Exporter);
 
 our @EXPORT = qw(conv_i conv_date conv_dateq do_query selectrow_query do_statement
-             dump_query quote_db_date
+             dump_query quote_db_date like
              selectfirst_hashref_query selectfirst_array_query
-             selectall_hashref_query selectall_array_query
+             selectall_hashref_query selectall_array_query selectcol_array_query
              selectall_as_map
              selectall_ids
              prepare_execute_query prepare_query
              create_sort_spec does_table_exist
-             add_token);
+             add_token check_trgm);
 
 use strict;
 
@@ -30,7 +32,9 @@ sub conv_b {
 
 sub conv_date {
   my ($value) = @_;
-  return (defined($value) && "$value" ne "") ? $value : undef;
+  return undef if !defined $value;
+  $value = trim($value);
+  return $value eq "" ? undef : $value;
 }
 
 sub conv_dateq {
@@ -88,7 +92,7 @@ sub dump_query {
   my $self_filename = 'SL/DBUtils.pm';
   my $filename      = $self_filename;
   my ($caller_level, $line, $subroutine);
-  while ($filename eq $self_filename) {
+  while ($filename =~ m{$self_filename$}) {
     (undef, $filename, $line, $subroutine) = caller $caller_level++;
   }
 
@@ -167,16 +171,15 @@ sub selectall_hashref_query {
   return wantarray ? @{ $result } : $result;
 }
 
-sub selectall_array_query {
+sub selectall_array_query { goto &selectcol_array_query; }
+
+sub selectcol_array_query {
   $main::lxdebug->enter_sub(2);
 
   my ($form, $dbh, $query) = splice(@_, 0, 3);
 
   my $sth = prepare_execute_query($form, $dbh, $query, @_);
-  my @result;
-  while (my ($value) = $sth->fetchrow_array()) {
-    push(@result, $value);
-  }
+  my @result = @{ $dbh->selectcol_arrayref($sth) };
   $sth->finish();
 
   $main::lxdebug->leave_sub(2);
@@ -344,9 +347,9 @@ sub add_token {
     id     => \&conv_i,
     bool   => \&conv_b,
     date   => \&conv_date,
-    start  => sub { $_[0] . '%' },
-    end    => sub { '%' . $_[0] },
-    substr => sub { '%' . $_[0] . '%' },
+    start  => sub { trim($_[0]) . '%' },
+    end    => sub { '%' . trim($_[0]) },
+    substr => sub { like($_[0]) },
   );
 
   my $_long_token = sub {
@@ -382,14 +385,37 @@ sub add_token {
   return ($token, @vals);
 }
 
+sub like {
+  my ($string) = @_;
+
+  return "%" . SL::Util::trim($string // '') . "%";
+}
+
+sub role_is_superuser {
+  my ($dbh, $login)  = @_;
+  my ($is_superuser) = $dbh->selectrow_array(qq|SELECT usesuper FROM pg_user WHERE usename = ?|, undef, $login);
+
+  return $is_superuser;
+}
+
+sub check_trgm {
+  my ($dbh)  = @_;
+
+  my $version = $dbh->selectrow_array(qq|SELECT installed_version FROM pg_available_extensions WHERE name = 'pg_trgm'|);
+
+  return !!$version;
+}
+
 1;
 
 
 __END__
 
+=encoding utf-8
+
 =head1 NAME
 
-SL::DBUTils.pm: All about database connections in kivitendo
+SL::DBUtils.pm: All about database connections in kivitendo
 
 =head1 SYNOPSIS
 
@@ -400,6 +426,8 @@ SL::DBUTils.pm: All about database connections in kivitendo
   conv_dateq($str)
   quote_db_date($date)
 
+  my $dbh = SL::DB->client->dbh;
+
   do_query($form, $dbh, $query)
   do_statement($form, $sth, $query)
 
@@ -409,30 +437,107 @@ SL::DBUTils.pm: All about database connections in kivitendo
   my $all_results_ref       = selectall_hashref_query($form, $dbh, $query)
   my $first_result_hash_ref = selectfirst_hashref_query($form, $dbh, $query);
 
-  my @first_result =  selectfirst_array_query($form, $dbh, $query);  # ==
+  my @first_result =  selectfirst_array_query($form, $dbh, $query);
   my @first_result =  selectrow_query($form, $dbh, $query);
 
+  my @values = selectcol_array_query($form, $dbh, $query);
+
   my %sort_spec = create_sort_spec(%params);
 
 =head1 DESCRIPTION
 
-DBUtils is the attempt to reduce the amount of overhead it takes to retrieve information from the database in kivitendo. Previously it would take about 15 lines of code just to get one single integer out of the database, including failure procedures and importing the necessary packages. Debugging would take even more.
+DBUtils provides wrapper functions for low level database retrieval. It saves
+you the trouble of mucking around with statement handles for small database
+queries and does exception handling in the common cases for you.
+
+Query and retrieval functions share the parameter scheme:
+
+  query_or_retrieval(C<FORM, DBH, QUERY[, BINDVALUES]>)
+
+=over 4
+
+=item *
+
+C<FORM> is used for error handling only. It can be omitted in theory, but should
+not. In most cases you will call it with C<$::form>.
+
+=item *
+
+C<DBH> is a handle to the database, as returned by the C<DBI::connect> routine.
+If you don't have an active connection, you can use
+C<SL::DB->client->dbh> or get a C<Rose::DB::Object> handle from any RDBO class with
+C<<SL::DB::Part->new->db->dbh>>. In both cases the handle will have AutoCommit set.
+
+See C<PITFALLS AND CAVEATS> for common errors.
+
+=item *
+
+C<QUERY> must be exactly one query. You don't need to include the terminal
+C<;>. There must be no tainted data interpolated into the string. Instead use
+the DBI placeholder syntax.
+
+=item *
+
+All additional parameters will be used as C<BINDVALUES> for the query. Note
+that DBI can't bind arrays to a C<id IN (?)>, so you will need to generate a
+statement with exactly one C<?> for each bind value. DBI can however bind
+DateTime objects, and you should always pass these for date selections.
+
+=back
+
+=head1 PITFALLS AND CAVEATS
+
+=head2 Locking
+
+As mentioned above, there are two sources of database handles in the program:
+C<<$::form->get_standard_dbh>> and C<<SL::DB::Object->new->db->dbh>>. It's easy
+to produce deadlocks when using both of them. To reduce the likelyhood of
+locks, try to obey these rules:
+
+=over 4
+
+=item *
+
+In a controller that uses Rose objects, never use C<get_standard_dbh>.
+
+=item *
+
+In backend code, that has no preference, always accept the database handle as a
+parameter from the controller.
+
+=back
+
+=head2 Exports
+
+C<DBUtils> is one of the last modules in the program to use C<@EXPORT> instead
+of C<@EXPORT_OK>. This means it will flood your namespace with its functions,
+causing potential clashes. When writing new code, always either export nothing
+and call directly:
+
+  use SL::DBUtils ();
+  DBUtils::selectall_hashref_query(...)
+
+or export only what you need:
+
+  use SL::DBUtils qw(selectall_hashref_query);
+  selectall_hashref_query(...)
 
-Using DBUtils most database procedures can be reduced to defining the query, executing it, and retrieving the result. Let DBUtils handle the rest. Whenever there is a database operation not covered in DBUtils, add it here, rather than working around it in the backend code.
 
-DBUtils relies heavily on two parameters which have to be passed to almost every function: $form and $dbh.
-  - $form is used for error handling only. It can be omitted in theory, but should not.
-  - $dbh is a handle to the database, as returned by the DBI::connect routine. If you don't have an active connection, you can query $form->get_standard_dbh() to get a generic no_auto connection. Don't forget to commit in this case!
+=head2 Performance
 
+Since it is really easy to write something like
 
-Every function here should accomplish the follwing things:
-  - Easy debugging. Every handled query gets dumped via LXDebug, if specified there.
-  - Safe value binding. Although DBI is far from perfect in terms of binding, the rest of the bindings should happen here.
-  - Error handling. Should a query fail, an error message will be generated here instead of in the backend code invoking DBUtils.
+  my $all_parts = selectall_hashref_query($::form, $dbh, 'SELECT * FROM parts');
 
-Note that binding is not perfect here either...
+people do so from time to time. When writing code, consider this a ticking
+timebomb. Someone out there has a database with 1mio parts in it, and this
+statement just gobbled up 2GB of memory and timeouted the request.
 
-=head2 QUOTING FUNCTIONS
+Parts may be the obvious example, but the same applies to customer, vendors,
+records, projects or custom variables.
+
+
+=head1 QUOTING FUNCTIONS
 
 =over 4
 
@@ -440,7 +545,8 @@ Note that binding is not perfect here either...
 
 =item conv_i STR,DEFAULT
 
-Converts STR to an integer. If STR is empty, returns DEFAULT. If no DEFAULT is given, returns undef.
+Converts STR to an integer. If STR is empty, returns DEFAULT. If no DEFAULT is
+given, returns undef.
 
 =item conv_date STR
 
@@ -448,38 +554,54 @@ Converts STR to a date string. If STR is emptry, returns undef.
 
 =item conv_dateq STR
 
-Database version of conv_date. Quotes STR before returning. Returns 'NULL' if STR is empty.
+Database version of conv_date. Quotes STR before returning. Returns 'NULL' if
+STR is empty.
 
 =item quote_db_date STR
 
-Treats STR as a database date, quoting it. If STR equals current_date returns an escaped version which is treated as the current date by Postgres.
-Returns 'NULL' if STR is empty.
+Treats STR as a database date, quoting it. If STR equals current_date returns
+an escaped version which is treated as the current date by Postgres.
+
+Returns C<'NULL'> if STR is empty.
+
+=item like STR
+
+Turns C<STR> into an argument suitable for SQL's C<LIKE> and C<ILIKE>
+operators by Trimming the string C<STR> (removes leading and trailing
+whitespaces) and prepending and appending C<%>.
 
 =back
 
-=head2 QUERY FUNCTIONS
+=head1 QUERY FUNCTIONS
 
 =over 4
 
 =item do_query FORM,DBH,QUERY,ARRAY
 
-Uses DBI::do to execute QUERY on DBH using ARRAY for binding values. FORM is only needed for error handling, but should always be passed nevertheless. Use this for insertions or updates that don't need to be prepared.
+Uses DBI::do to execute QUERY on DBH using ARRAY for binding values. FORM is
+only needed for error handling, but should always be passed nevertheless. Use
+this for insertions or updates that don't need to be prepared.
 
-Returns the result of DBI::do which is -1 in case of an error and the number of affected rows otherwise.
+Returns the result of DBI::do which is -1 in case of an error and the number of
+affected rows otherwise.
 
 =item do_statement FORM,STH,QUERY,ARRAY
 
-Uses DBI::execute to execute QUERY on DBH using ARRAY for binding values. As with do_query, FORM is only used for error handling. If you are unsure what to use, refer to the documentation of DBI::do and DBI::execute.
+Uses DBI::execute to execute QUERY on DBH using ARRAY for binding values. As
+with do_query, FORM is only used for error handling. If you are unsure what to
+use, refer to the documentation of DBI::do and DBI::execute.
 
-Returns the result of DBI::execute which is -1 in case of an error and the number of affected rows otherwise.
+Returns the result of DBI::execute which is -1 in case of an error and the
+number of affected rows otherwise.
 
 =item prepare_execute_query FORM,DBH,QUERY,ARRAY
 
-Prepares and executes QUERY on DBH using DBI::prepare and DBI::execute. ARRAY is passed as binding values to execute.
+Prepares and executes QUERY on DBH using DBI::prepare and DBI::execute. ARRAY
+is passed as binding values to execute.
 
 =back
 
-=head2 RETRIEVAL FUNCTIONS
+=head1 RETRIEVAL FUNCTIONS
 
 =over 4
 
@@ -487,23 +609,40 @@ Prepares and executes QUERY on DBH using DBI::prepare and DBI::execute. ARRAY is
 
 =item selectrow_query FORM,DBH,QUERY,ARRAY
 
-Prepares and executes a query using DBUtils functions, retireves the first row from the database, and returns it as an arrayref of the first row.
+Prepares and executes a query using DBUtils functions, retrieves the first row
+from the database, and returns it as an arrayref of the first row.
 
 =item selectfirst_hashref_query FORM,DBH,QUERY,ARRAY
 
-Prepares and executes a query using DBUtils functions, retireves the first row from the database, and returns it as a hashref of the first row.
+Prepares and executes a query using DBUtils functions, retrieves the first row
+from the database, and returns it as a hashref of the first row.
 
 =item selectall_hashref_query FORM,DBH,QUERY,ARRAY
 
-Prepares and executes a query using DBUtils functions, retireves all data from the database, and returns it in hashref mode. This is slightly confusing, as the data structure will actually be a reference to an array, containing hashrefs for each row.
+Prepares and executes a query using DBUtils functions, retrieves all data from
+the database, and returns it in hashref mode. This is slightly confusing, as
+the data structure will actually be a reference to an array, containing
+hashrefs for each row.
+
+
+=item selectall_array_query FORM,DBH,QUERY,ARRAY
+
+Deprecated, see C<selectcol_array_query>
+
+=item selectcol_array_query FORM,DBH,QUERY,ARRAY
+
+Prepares and executes a query using DBUtils functions, retrieves the values of
+the first result column and returns the values as an array.
 
 =item selectall_as_map FORM,DBH,QUERY,KEY_COL,VALUE_COL,ARRAY
 
-Prepares and executes a query using DBUtils functions, retireves all data from the database, and creates a hash from the results using KEY_COL as the column for the hash keys and VALUE_COL for its values.
+Prepares and executes a query using DBUtils functions, retrieves all data from
+the database, and creates a hash from the results using KEY_COL as the column
+for the hash keys and VALUE_COL for its values.
 
 =back
 
-=head2 UTILITY FUNCTIONS
+=head1 UTILITY FUNCTIONS
 
 =over 4
 
@@ -531,8 +670,10 @@ The parameter 'defs' is a hash reference. The keys are the column
 names as they may come from the application. The values are either
 scalars with SQL code or array references of SQL code. Example:
 
-'defs' => { 'customername' => 'lower(customer.name)',
-            'address'      => [ 'lower(customer.city)', 'lower(customer.street)' ], }
+  defs => {
+    customername => 'lower(customer.name)',
+    address      => [ 'lower(customer.city)', 'lower(customer.street)' ],
+  }
 
 'default' is the default column name to sort by. It must be a key of
 'defs' and should not be come from user input.
@@ -551,15 +692,25 @@ application (e.g. if the user clicked on a column header in a
 report). If it is undefined then the 'default_dir' parameter will be
 used instead.
 
+=item check_trgm
+
+Checks if the postgresextension pg_trgm is installed and return trueish
+or falsish.
+
 =back
 
-=head2 DEBUG FUNCTIONS
+=head1 DEBUG FUNCTIONS
 
 =over 4
 
 =item dump_query LEVEL,MSG,QUERY,ARRAY
 
-Dumps a query using LXDebug->message, using LEVEL for the debug-level of LXDebug. If MSG is given, it preceeds the QUERY dump in the logfiles. ARRAY is used to interpolate the '?' placeholders in QUERY, the resulting QUERY can be copy-pasted into a database frontend for debugging. Note that this method is also automatically called by each of the other QUERY FUNCTIONS, so there is in general little need to invoke it manually.
+Dumps a query using LXDebug->message, using LEVEL for the debug-level of
+LXDebug. If MSG is given, it preceeds the QUERY dump in the logfiles. ARRAY is
+used to interpolate the '?' placeholders in QUERY, the resulting QUERY can be
+copy-pasted into a database frontend for debugging. Note that this method is
+also automatically called by each of the other QUERY FUNCTIONS, so there is in
+general little need to invoke it manually.
 
 =back
 
@@ -577,6 +728,11 @@ Dumps a query using LXDebug->message, using LEVEL for the debug-level of LXDebug
   $query = qq|SELECT nextval('glid')|;
   ($new_id) = selectrow_query($form, $dbh, $query);
 
+=item Retrieving all values from a column:
+
+  $query = qq|SELECT id FROM units|;
+  @units = selectcol_array_query($form, $dbh, $query);
+
 =item Using binding values:
 
   $query = qq|UPDATE ar SET paid = amount + paid, storno = 't' WHERE id = ?|;
@@ -587,9 +743,11 @@ Dumps a query using LXDebug->message, using LEVEL for the debug-level of LXDebug
   my @values;
 
   if ($form->{language_values} ne "") {
-    $query = qq|SELECT l.id, l.description, tr.translation, tr.longdescription
-                  FROM language l
-                  LEFT OUTER JOIN translation tr ON (tr.language_id = l.id) AND (tr.parts_id = ?)|;
+    $query = qq|
+      SELECT l.id, l.description, tr.translation, tr.longdescription
+      FROM language l
+      LEFT JOIN translation tr ON (tr.language_id = l.id AND tr.parts_id = ?)
+    |;
     @values = (conv_i($form->{id}));
   } else {
     $query = qq|SELECT id, description FROM language|;
@@ -601,13 +759,13 @@ Dumps a query using LXDebug->message, using LEVEL for the debug-level of LXDebug
 
 =head1 MODULE AUTHORS
 
-Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
-Sven Schoeling E<lt>s.schoeling@linet-services.deE<gt>
+  Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+  Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
 
 =head1 DOCUMENTATION AUTHORS
 
-Udo Spallek E<lt>udono@gmx.netE<gt>
-Sven Schoeling E<lt>s.schoeling@linet-services.deE<gt>
+  Udo Spallek E<lt>udono@gmx.netE<gt>
+  Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
 
 =head1 COPYRIGHT AND LICENSE
 
index 2bead22..76ee1b8 100644 (file)
--- a/SL/DN.pm
+++ b/SL/DN.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Dunning process module
@@ -36,7 +37,10 @@ package DN;
 
 use SL::Common;
 use SL::DBUtils;
+use SL::DB::AuthUser;
 use SL::DB::Default;
+use SL::DB::Employee;
+use SL::File;
 use SL::GenericTranslations;
 use SL::IS;
 use SL::Mailer;
@@ -45,6 +49,12 @@ use SL::Template;
 use SL::DB::Printer;
 use SL::DB::Language;
 use SL::TransNumber;
+use SL::Util qw(trim);
+use SL::DB;
+use SL::Webdav;
+
+use File::Copy;
+use File::Slurp qw(read_file);
 
 use strict;
 
@@ -54,7 +64,7 @@ sub get_config {
   my ($self, $myconfig, $form) = @_;
 
   # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $query =
     qq|SELECT * | .
@@ -68,22 +78,28 @@ sub get_config {
   }
 
   $query =
-    qq|SELECT dunning_ar_amount_fee, dunning_ar_amount_interest, dunning_ar
+    qq|SELECT dunning_ar_amount_fee, dunning_ar_amount_interest, dunning_ar, dunning_creator
        FROM defaults|;
-  ($form->{AR_amount_fee}, $form->{AR_amount_interest}, $form->{AR}) = selectrow_query($form, $dbh, $query);
-
-  $dbh->disconnect();
+  ($form->{AR_amount_fee}, $form->{AR_amount_interest}, $form->{AR}, $form->{dunning_creator})
+    = selectrow_query($form, $dbh, $query);
 
   $main::lxdebug->leave_sub();
 }
 
 sub save_config {
+  my ($self, $myconfig, $form) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_save_config, $self, $myconfig, $form);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _save_config {
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my ($query, @values);
 
@@ -98,7 +114,8 @@ sub save_config {
                  $form->{"template_$i"}, $form->{"fee_$i"}, $form->{"interest_rate_$i"},
                  $form->{"active_$i"} ? 't' : 'f', $form->{"auto_$i"} ? 't' : 'f', $form->{"email_$i"} ? 't' : 'f',
                  $form->{"email_attachment_$i"} ? 't' : 'f', conv_i($form->{"payment_terms_$i"}), conv_i($form->{"terms_$i"}),
-                 $form->{"create_invoices_for_fees_$i"} ? 't' : 'f');
+                 $form->{"create_invoices_for_fees_$i"} ? 't' : 'f',
+                 $form->{"print_original_invoice_$i"} ? 't' : 'f');
       if ($form->{"id_$i"}) {
         $query =
           qq|UPDATE dunning_config SET
@@ -107,7 +124,8 @@ sub save_config {
                template = ?, fee = ?, interest_rate = ?,
                active = ?, auto = ?, email = ?,
                email_attachment = ?, payment_terms = ?, terms = ?,
-               create_invoices_for_fees = ?
+               create_invoices_for_fees = ?,
+               print_original_invoice = ?
              WHERE id = ?|;
         push(@values, conv_i($form->{"id_$i"}));
       } else {
@@ -115,8 +133,9 @@ sub save_config {
           qq|INSERT INTO dunning_config
                (dunning_level, dunning_description, email_subject, email_body,
                 template, fee, interest_rate, active, auto, email,
-                email_attachment, payment_terms, terms, create_invoices_for_fees)
-             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)|;
+                email_attachment, payment_terms, terms, create_invoices_for_fees,
+                print_original_invoice)
+             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)|;
       }
       do_query($form, $dbh, $query, @values);
     }
@@ -127,14 +146,13 @@ sub save_config {
     }
   }
 
-  $query  = qq|UPDATE defaults SET dunning_ar_amount_fee = ?, dunning_ar_amount_interest = ?, dunning_ar = ?|;
-  @values = (conv_i($form->{AR_amount_fee}), conv_i($form->{AR_amount_interest}), conv_i($form->{AR}));
+  $query  = qq|UPDATE defaults SET dunning_ar_amount_fee = ?, dunning_ar_amount_interest = ?, dunning_ar = ?,
+               dunning_creator = ?|;
+  @values = (conv_i($form->{AR_amount_fee}), conv_i($form->{AR_amount_interest}), conv_i($form->{AR}),
+             $form->{dunning_creator});
   do_query($form, $dbh, $query, @values);
 
-  $dbh->commit();
-  $dbh->disconnect();
-
-  $main::lxdebug->leave_sub();
+  return 1;
 }
 
 sub create_invoice_for_fees {
@@ -177,7 +195,8 @@ sub create_invoice_for_fees {
              AND (d_interest.dunning_id <> ?)
              AND NOT (d_interest.fee_interest_ar_id ISNULL)
          ), 0)
-         AS max_previous_interest
+         AS max_previous_interest,
+         d.id AS link_id
        FROM dunning d
        WHERE dunning_id = ?|;
   @values = ($dunning_id, $dunning_id, $dunning_id);
@@ -186,6 +205,8 @@ sub create_invoice_for_fees {
   my ($fee_remaining, $interest_remaining) = (0, 0);
   my ($fee_total, $interest_total) = (0, 0);
 
+  my @link_ids;
+
   while (my $ref = $sth->fetchrow_hashref()) {
     $fee_remaining      += $form->round_amount($ref->{fee}, 2);
     $fee_remaining      -= $form->round_amount($ref->{max_previous_fee}, 2);
@@ -193,6 +214,7 @@ sub create_invoice_for_fees {
     $interest_remaining += $form->round_amount($ref->{interest}, 2);
     $interest_remaining -= $form->round_amount($ref->{max_previous_interest}, 2);
     $interest_total     += $form->round_amount($ref->{interest}, 2);
+    push @link_ids, $ref->{link_id};
   }
 
   $sth->finish();
@@ -256,6 +278,15 @@ sub create_invoice_for_fees {
              $::myconfig{login});   # employee_id
   do_query($form, $dbh, $query, @values);
 
+  RecordLinks->create_links(
+    'dbh'        => $dbh,
+    'mode'       => 'ids',
+    'from_table' => 'dunning',
+    'from_ids'   => \@link_ids,
+    'to_table'   => 'ar',
+    'to_id'      => $ar_id,
+  );
+
   $query =
     qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, gldate, taxkey, tax_id, chart_link)
        VALUES (?, ?, ?, current_date, current_date, 0,
@@ -282,12 +313,45 @@ sub create_invoice_for_fees {
   $main::lxdebug->leave_sub();
 }
 
+
 sub save_dunning {
+  my ($self, $myconfig, $form, $rows) = @_;
   $main::lxdebug->enter_sub();
 
+  $form->{DUNNING_PDFS_STORAGE} = [];
+
+  # Catch any error, either exception or a call to form->error
+  # and return it to the calling function.
+  my ($error, $rc);
+  eval {
+    local $form->{__ERROR_HANDLER} = sub { die @_ };
+    $rc = SL::DB->client->with_transaction(\&_save_dunning, $self, $myconfig, $form, $rows);
+    1;
+  } or do {
+    $error = $@;
+  };
+
+  # Save PDFs in filemanagement and webdav after transation succeeded,
+  # because otherwise files in the storage may exists if the transaction
+  # failed. Ignore all errros.
+  # Todo: Maybe catch errros and display them as warnings or non fatal errors in the status.
+  if (!$error && $form->{DUNNING_PDFS_STORAGE} && scalar @{ $form->{DUNNING_PDFS_STORAGE} }) {
+    _store_pdf_to_webdav_and_filemanagement($_->{dunning_id}, $_->{path}, $_->{name}) for @{ $form->{DUNNING_PDFS_STORAGE} };
+  }
+
+  $error       = 'unknown errror' if !$error && !$rc;
+  $rc->{error} = $error           if $error;
+
+  $::lxdebug->leave_sub;
+
+  return $rc;
+}
+
+
+sub _save_dunning {
   my ($self, $myconfig, $form, $rows) = @_;
-  # connect to database
-  my $dbh = $form->dbconnect_noauto($myconfig);
+
+  my $dbh = SL::DB->client->dbh;
 
   my ($query, @values);
 
@@ -297,9 +361,9 @@ sub save_dunning {
   my $h_update_ar = prepare_query($form, $dbh, $q_update_ar);
 
   my $q_insert_dunning =
-    qq|INSERT INTO dunning (dunning_id, dunning_config_id, dunning_level, trans_id,
-                            fee,        interest,          transdate,     duedate)
-       VALUES (?, ?,
+    qq|INSERT INTO dunning (id,  dunning_id, dunning_config_id, dunning_level, trans_id,
+                            fee, interest,   transdate,         duedate,       original_invoice_printed)
+       VALUES (?, ?, ?,
                (SELECT dunning_level FROM dunning_config WHERE id = ?),
                ?,
                (SELECT SUM(fee)
@@ -309,14 +373,25 @@ sub save_dunning {
                  * (SELECT interest_rate FROM dunning_config WHERE id = ?)
                  / 360,
                current_date,
-               current_date + (SELECT payment_terms FROM dunning_config WHERE id = ?))|;
+               current_date + (SELECT payment_terms FROM dunning_config WHERE id = ?),
+               ?)|;
   my $h_insert_dunning = prepare_query($form, $dbh, $q_insert_dunning);
 
   my @invoice_ids;
   my ($next_dunning_config_id, $customer_id);
-  my $send_email = 0;
+  my ($send_email, $print_invoice) = (0, 0);
 
   foreach my $row (@{ $rows }) {
+    if ($row->{credit_note}) {
+      my $i = $row->{row};
+      %{ $form->{LIST_CREDIT_NOTES}{$row->{customer_id}}{$row->{invoice_id}} } = (
+        open_amount => $form->{"open_amount_$i"},
+        amount      => $form->{"amount_$i"},
+        invnumber   => $form->{"invnumber_$i"},
+        invdate     => $form->{"invdate_$i"},
+      );
+      next;
+    }
     push @invoice_ids, $row->{invoice_id};
     $next_dunning_config_id = $row->{next_dunning_config_id};
     $customer_id            = $row->{customer_id};
@@ -324,16 +399,31 @@ sub save_dunning {
     @values = ($row->{next_dunning_config_id}, $row->{invoice_id});
     do_statement($form, $h_update_ar, $q_update_ar, @values);
 
-    $send_email |= $row->{email};
+    $send_email       |= $row->{email};
+    $print_invoice    |= $row->{print_invoice};
 
+    my ($row_id)       = selectrow_query($form, $dbh, qq|SELECT nextval('id')|);
     my $next_config_id = conv_i($row->{next_dunning_config_id});
     my $invoice_id     = conv_i($row->{invoice_id});
 
-    @values = ($dunning_id,     $next_config_id, $next_config_id,
-               $invoice_id,     $next_config_id, $invoice_id,
-               $next_config_id, $next_config_id);
+    @values = ($row_id,         $dunning_id,     $next_config_id,
+               $next_config_id, $invoice_id,     $next_config_id,
+               $invoice_id,     $next_config_id, $next_config_id,
+               $print_invoice);
     do_statement($form, $h_insert_dunning, $q_insert_dunning, @values);
+
+    RecordLinks->create_links(
+      'dbh'        => $dbh,
+      'mode'       => 'ids',
+      'from_table' => 'ar',
+      'from_ids'   => $invoice_id,
+      'to_table'   => 'dunning',
+      'to_id'      => $row_id,
+    );
   }
+  # die this transaction, because for this customer only credit notes are
+  # selected ...
+  die "only credit notes are selected for this customer\n" unless $customer_id;
 
   $h_update_ar->finish();
   $h_insert_dunning->finish();
@@ -347,15 +437,15 @@ sub save_dunning {
   $self->print_invoice_for_fees($myconfig, $form, $dunning_id, $dbh);
   $self->print_dunning($myconfig, $form, $dunning_id, $dbh);
 
+  if ($print_invoice) {
+    $self->print_original_invoice($myconfig, $form, $dunning_id, $_) for @invoice_ids;
+  }
 
   if ($send_email) {
     $self->send_email($myconfig, $form, $dunning_id, $dbh);
   }
 
-  $dbh->commit();
-  $dbh->disconnect();
-
-  $main::lxdebug->leave_sub();
+  return ({dunning_id => $dunning_id, print_original_invoice => $print_invoice, send_email => $send_email});
 }
 
 sub send_email {
@@ -366,8 +456,8 @@ sub send_email {
   my $query =
     qq|SELECT
          dcfg.email_body,     dcfg.email_subject, dcfg.email_attachment,
-         c.email AS recipient
-
+         COALESCE (NULLIF(c.invoice_mail, ''), c.email) AS recipient, c.name,
+         (SELECT login from employee where id = ar.employee_id) as invoice_employee_login
        FROM dunning d
        LEFT JOIN dunning_config dcfg ON (d.dunning_config_id = dcfg.id)
        LEFT JOIN ar                  ON (d.trans_id          = ar.id)
@@ -376,19 +466,42 @@ sub send_email {
        LIMIT 1|;
   my $ref = selectfirst_hashref_query($form, $dbh, $query, $dunning_id);
 
-  if (!$ref || !$ref->{recipient} || !$myconfig->{email}) {
+  # without a recipient, we cannot send a mail
+  if (!$ref || !$ref->{recipient}) {
     $main::lxdebug->leave_sub();
-    return;
+    die $main::locale->text("No email recipient for customer #1 defined.", $ref->{name});
+  }
+
+  # without a sender we cannot send a mail
+  # two cases: check mail from 1. current user OR  2. employee who created the invoice
+  my ($from, $sign);
+  if ($::instance_conf->get_dunning_creator eq 'current_employee') {
+    $from = $myconfig->{email};
+    die $main::locale->text('No email for current user #1 defined.', $myconfig->{name}) unless $from;
+  } else {
+    eval {
+      $from = SL::DB::Manager::AuthUser->find_by(login =>  $ref->{invoice_employee_login})->get_config_value("email");
+      $sign = SL::DB::Manager::AuthUser->find_by(login =>  $ref->{invoice_employee_login})->get_config_value("signature");
+      die unless ($from);
+      1;
+    } or die $main::locale->text('No email for user with login #1 defined.', $ref->{invoice_employee_login});
   }
 
+  my $html_template = SL::Template::create(type => 'HTML',      form => $form, myconfig => $myconfig);
+  $html_template->set_tag_style('&lt;%', '%&gt;');
+
   my $template     = SL::Template::create(type => 'PlainText', form => $form, myconfig => $myconfig);
   my $mail         = Mailer->new();
-  $mail->{from}    = $myconfig->{email};
+  $mail->{bcc}     = $form->get_bcc_defaults($myconfig, $form->{bcc});
+  $mail->{from}    = $from;
   $mail->{to}      = $ref->{recipient};
   $mail->{subject} = $template->parse_block($ref->{email_subject});
-  $mail->{message} = $template->parse_block($ref->{email_body});
-
+  $mail->{message} = $html_template->parse_block($ref->{email_body});
+  $mail->{content_type} = 'text/html';
+  my $sign_backup  = $::myconfig{signature};
+  $::myconfig{signature} = $sign if $sign;
   $mail->{message} .= $form->create_email_signature();
+  $::myconfig{signature} = $sign_backup if $sign;
 
   $mail->{message} =~ s/\r\n/\n/g;
 
@@ -396,6 +509,11 @@ sub send_email {
     $mail->{attachments} = $form->{DUNNING_PDFS_EMAIL};
   }
 
+  $query  = qq|SELECT id FROM dunning WHERE dunning_id = ?|;
+  my @ids = selectall_array_query($form, $dbh, $query, $dunning_id);
+  $mail->{record_id}   = \@ids;
+  $mail->{record_type} = 'dunning';
+
   $mail->send();
 
   $main::lxdebug->leave_sub();
@@ -460,11 +578,11 @@ sub set_template_options {
   # prepare meta information for template introspection
   $form->{template_meta} = {
     formname  => $form->{formname},
-    language  => SL::DB::Manager::Language->find_by_or_create(id => $form->{language_id}),
+    language  => SL::DB::Manager::Language->find_by_or_create(id => $form->{language_id} || undef),
     format    => $form->{format},
     media     => $form->{media},
     extension => $extension,
-    printer   => SL::DB::Manager::Printer->find_by_or_create(id => $form->{printer_id}),
+    printer   => SL::DB::Manager::Printer->find_by_or_create(id => $form->{printer_id} || undef),
     today     => DateTime->today,
   };
 
@@ -478,7 +596,7 @@ sub get_invoices {
   my ($self, $myconfig, $form) = @_;
 
   # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $where;
   my @values;
@@ -491,7 +609,12 @@ sub get_invoices {
 
   } elsif ($form->{customer}) {
     $where .= qq| AND (ct.name ILIKE ?)|;
-    push(@values, '%' . $form->{customer} . '%');
+    push(@values, like($form->{customer}));
+  }
+
+  if ($form->{department_id}) {
+    $where .= qq| AND (a.department_id = ?)|;
+    push(@values, $form->{department_id});
   }
 
   my %columns = (
@@ -503,7 +626,7 @@ sub get_invoices {
   foreach my $key (keys(%columns)) {
     next unless ($form->{$key});
     $where .= qq| AND $columns{$key} ILIKE ?|;
-    push(@values, '%' . $form->{$key} . '%');
+    push(@values, like($form->{$key}));
   }
 
   if ($form->{dunning_level}) {
@@ -514,7 +637,7 @@ sub get_invoices {
   $form->{minamount} = $form->parse_amount($myconfig,$form->{minamount});
   if ($form->{minamount}) {
     $where .= qq| AND ((a.amount - a.paid) > ?) |;
-    push(@values, $form->{minamount});
+    push(@values, trim($form->{minamount}));
   }
 
   my $query =
@@ -526,6 +649,7 @@ sub get_invoices {
   if (!$form->{l_include_direct_debit}) {
     $where .= qq| AND NOT COALESCE(a.direct_debit, FALSE) |;
   }
+  my $paid = ($form->{l_include_credit_notes}) ? "WHERE (a.paid <> a.amount)" : "WHERE (a.paid < a.amount)";
 
   $query =
     qq|SELECT
@@ -533,7 +657,9 @@ sub get_invoices {
          ct.name AS customername, a.customer_id, a.duedate,
          a.amount - a.paid AS open_amount,
          a.direct_debit,
-
+         pt.description as payment_term,
+         dep.description as departmentname,
+         ct.invoice_mail AS cv_email,
          cfg.dunning_description, cfg.dunning_level,
 
          d.transdate AS dunning_date, d.duedate AS dunning_duedate,
@@ -545,11 +671,13 @@ sub get_invoices {
 
          nextcfg.dunning_description AS next_dunning_description,
          nextcfg.id AS next_dunning_config_id,
-         nextcfg.terms, nextcfg.active, nextcfg.email
+         nextcfg.terms, nextcfg.active, nextcfg.email, nextcfg.print_original_invoice
 
        FROM ar a
 
        LEFT JOIN customer ct ON (a.customer_id = ct.id)
+       LEFT JOIN department dep ON (a.department_id = dep.id)
+       LEFT JOIN payment_terms pt ON (a.payment_id = pt.id)
        LEFT JOIN dunning_config cfg ON (a.dunning_config_id = cfg.id)
        LEFT JOIN dunning_config nextcfg ON
          (nextcfg.id =
@@ -572,9 +700,8 @@ sub get_invoices {
          WHERE (d2.trans_id      = a.id)
            AND (d2.dunning_level = cfg.dunning_level)
        ))
-
-       WHERE (a.paid < a.amount)
-         AND (a.duedate < current_date)
+        $paid
+        AND (a.duedate < current_date)
 
        $where
 
@@ -585,7 +712,7 @@ sub get_invoices {
 
   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
     next if ($ref->{pastdue} < $ref->{terms});
-
+    $ref->{credit_note} = 1 if ($ref->{amount} < 0 && $form->{l_include_credit_notes});
     $ref->{interest} = $form->round_amount($ref->{interest}, 2);
     push(@{ $form->{DUNNINGS} }, $ref);
   }
@@ -595,7 +722,6 @@ sub get_invoices {
   $query = qq|SELECT id, dunning_description FROM dunning_config ORDER BY dunning_level|;
   $form->{DUNNING_CONFIG} = selectall_hashref_query($form, $dbh, $query);
 
-  $dbh->disconnect;
   $main::lxdebug->leave_sub();
 }
 
@@ -606,7 +732,7 @@ sub get_dunning {
   my ($self, $myconfig, $form) = @_;
 
   # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $where = qq| WHERE (da.trans_id = a.id)|;
 
@@ -618,7 +744,7 @@ sub get_dunning {
 
   } elsif ($form->{customer}) {
     $where .= qq| AND (ct.name ILIKE ?)|;
-    push(@values, '%' . $form->{customer} . '%');
+    push(@values, like($form->{customer}));
   }
 
   my %columns = (
@@ -629,7 +755,12 @@ sub get_dunning {
   foreach my $key (keys(%columns)) {
     next unless ($form->{$key});
     $where .= qq| AND $columns{$key} ILIKE ?|;
-    push(@values, '%' . $form->{$key} . '%');
+    push(@values, like($form->{$key}));
+  }
+
+  if ($form->{dunning_id}) {
+    $where .= qq| AND da.dunning_id = ?|;
+    push(@values, conv_i($form->{dunning_id}));
   }
 
   if ($form->{dunning_level}) {
@@ -675,13 +806,14 @@ sub get_dunning {
   }
 
   my %sort_columns = (
-    'dunning_description' => [ qw(dn.dunning_description customername invnumber) ],
-    'customername'        => [ qw(customername invnumber) ],
+    'dunning_description' => [ qw(dn.dunning_description da.dunning_id customername invnumber) ],
+    'customername'        => [ qw(customername da.dunning_id invnumber) ],
     'invnumber'           => [ qw(a.invnumber) ],
     'transdate'           => [ qw(a.transdate a.invnumber) ],
     'duedate'             => [ qw(a.duedate a.invnumber) ],
-    'dunning_date'        => [ qw(dunning_date a.invnumber) ],
-    'dunning_duedate'     => [ qw(dunning_duedate a.invnumber) ],
+    'dunning_date'        => [ qw(dunning_date da.dunning_id a.invnumber) ],
+    'dunning_duedate'     => [ qw(dunning_duedate da.dunning_id a.invnumber) ],
+    'dunning_id'          => [ qw(dunning_id a.invnumber) ],
     'salesman'            => [ qw(salesman) ],
     );
 
@@ -692,8 +824,9 @@ sub get_dunning {
   my $query =
     qq|SELECT a.id, a.ordnumber, a.invoice, a.transdate, a.invnumber, a.amount, a.language_id,
          ct.name AS customername, ct.id AS customer_id, a.duedate, da.fee,
-         da.interest, dn.dunning_description, da.transdate AS dunning_date,
+         da.interest, dn.dunning_description, dn.dunning_level, da.transdate AS dunning_date,
          da.duedate AS dunning_duedate, da.dunning_id, da.dunning_config_id,
+         da.id AS dunning_table_id,
          e2.name AS salesman
        FROM ar a
        JOIN customer ct ON (a.customer_id = ct.id)
@@ -708,7 +841,6 @@ sub get_dunning {
     map { $ref->{$_} = $form->format_amount($myconfig, $ref->{$_}, 2)} qw(amount fee interest);
   }
 
-  $dbh->disconnect;
   $main::lxdebug->leave_sub();
 }
 
@@ -716,7 +848,7 @@ sub melt_pdfs {
 
   $main::lxdebug->enter_sub();
 
-  my ($self, $myconfig, $form, $copies) = @_;
+  my ($self, $myconfig, $form, $copies, %params) = @_;
 
   # Don't allow access outside of $spool.
   map { $_ =~ s|.*/||; } @{ $form->{DUNNING_PDFS} };
@@ -732,23 +864,30 @@ sub melt_pdfs {
   my $in = IO::File->new($::lx_office_conf{applications}->{ghostscript} . " -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile=- $inputfiles |");
   $form->error($main::locale->text('Could not spawn ghostscript.')) unless $in;
 
-  if ($form->{media} eq 'printer') {
-    $form->get_printer_code($myconfig);
-    my $out;
-    if ($form->{printer_command}) {
-      $out = IO::File->new("| $form->{printer_command}");
-    }
+  my $dunning_filename    = $form->get_formname_translation('dunning');
+  my $attachment_filename = "${dunning_filename}_${dunning_id}.pdf";
+  my $content;
+  if ($params{return_content}) {
+    $content = read_file($in);
 
-    $::locale->with_raw_io($out, sub { $out->print($_) while <$in> });
+  } else {
+    if ($form->{media} eq 'printer') {
+      $form->get_printer_code($myconfig);
+      my $out;
+      if ($form->{printer_command}) {
+        $out = IO::File->new("| $form->{printer_command}");
+      }
 
-    $form->error($main::locale->text('Could not spawn the printer command.')) unless $out;
+      $form->error($main::locale->text('Could not spawn the printer command.')) unless $out;
 
-  } else {
-    my $dunning_filename = $form->get_formname_translation('dunning');
-    print qq|Content-Type: Application/PDF\n| .
-          qq|Content-Disposition: attachment; filename="${dunning_filename}_${dunning_id}.pdf"\n\n|;
+      $::locale->with_raw_io($out, sub { $out->print($_) while <$in> });
 
-    $::locale->with_raw_io(\*STDOUT, sub { print while <$in> });
+    } else {
+      print qq|Content-Type: Application/PDF\n| .
+            qq|Content-Disposition: attachment; filename=$attachment_filename\n\n|;
+
+      $::locale->with_raw_io(\*STDOUT, sub { print while <$in> });
+    }
   }
 
   $in->close();
@@ -756,6 +895,7 @@ sub melt_pdfs {
   map { unlink("$spool/$_") } @{ $form->{DUNNING_PDFS} };
 
   $main::lxdebug->leave_sub();
+  return ($attachment_filename, $content) if $params{return_content};
 }
 
 sub print_dunning {
@@ -764,7 +904,7 @@ sub print_dunning {
   my ($self, $myconfig, $form, $dunning_id, $provided_dbh) = @_;
 
   # connect to database
-  my $dbh = $provided_dbh ? $provided_dbh : $form->dbconnect_noauto($myconfig);
+  my $dbh = $provided_dbh || SL::DB->client->dbh;
 
   $dunning_id =~ s|[^\d]||g;
 
@@ -790,7 +930,9 @@ sub print_dunning {
          ar.transdate,       ar.duedate,      ar.customer_id,
          ar.invnumber,       ar.ordnumber,    ar.cp_id,
          ar.amount,          ar.netamount,    ar.paid,
-         (SELECT cu.name FROM currencies cu WHERE cu.id=ar.currency_id) AS curr,
+         ar.employee_id,     ar.salesman_id,
+         (SELECT cu.name FROM currencies cu WHERE cu.id = ar.currency_id) AS curr,
+         (SELECT description from department WHERE id = ar.department_id) AS department,
          ar.amount - ar.paid AS open_amount,
          ar.amount - ar.paid + da.fee + da.interest AS linetotal
 
@@ -813,19 +955,29 @@ sub print_dunning {
   }
   $sth->finish();
 
+  # if we have some credit notes to add, do a safety check on the first customer id
+  # and add one entry for each credit note
+  if ($form->{LIST_CREDIT_NOTES} && $form->{LIST_CREDIT_NOTES}->{$form->{TEMPLATE_ARRAYS}->{"dn_customer_id"}[0]}) {
+    my $first_customer_id = $form->{TEMPLATE_ARRAYS}->{"dn_customer_id"}[0];
+    while ( my ($cred_id, $value) = each(%{ $form->{LIST_CREDIT_NOTES}->{$first_customer_id} } ) ) {
+      map { push @{ $form->{TEMPLATE_ARRAYS}->{"dn_$_"} }, $value->{$_} } keys %{ $value };
+    }
+  }
   $query =
     qq|SELECT
          c.id AS customer_id, c.name,         c.street,       c.zipcode,   c.city,
          c.country,           c.department_1, c.department_2, c.email,     c.customernumber,
          c.greeting,          c.contact,      c.phone,        c.fax,       c.homepage,
          c.email,             c.taxincluded,  c.business_id,  c.taxnumber, c.iban,
-         c,ustid,             e.name as salesman_name,
+         c.ustid,             c.currency_id,  curr.name as currency,
+         ar.id AS invoice_id,
          co.*
        FROM dunning d
        LEFT JOIN ar          ON (d.trans_id = ar.id)
        LEFT JOIN customer c  ON (ar.customer_id = c.id)
        LEFT JOIN contacts co ON (ar.cp_id = co.cp_id)
        LEFT JOIN employee e  ON (ar.salesman_id = e.id)
+       LEFT JOIN currencies curr ON (c.currency_id = curr.id)
        WHERE (d.dunning_id = ?)
        LIMIT 1|;
   my $ref = selectfirst_hashref_query($form, $dbh, $query, $dunning_id);
@@ -833,7 +985,7 @@ sub print_dunning {
 
   $query =
     qq|SELECT
-         cfg.interest_rate, cfg.template AS formname,
+         cfg.interest_rate, cfg.template AS formname, cfg.dunning_level,
          cfg.email_subject, cfg.email_body, cfg.email_attachment,
          d.transdate AS dunning_date,
          (SELECT SUM(fee)
@@ -861,8 +1013,16 @@ sub print_dunning {
   $form->{interest_rate}     = $form->format_amount($myconfig, $ref->{interest_rate} * 100);
   $form->{fee}               = $form->format_amount($myconfig, $ref->{fee}, 2);
   $form->{total_interest}    = $form->format_amount($myconfig, $form->round_amount($ref->{total_interest}, 2), 2);
-  $form->{total_open_amount} = $form->format_amount($myconfig, $form->round_amount($ref->{total_open_amount}, 2), 2);
-  $form->{total_amount}      = $form->format_amount($myconfig, $form->round_amount($ref->{fee} + $ref->{total_interest} + $ref->{total_open_amount}, 2), 2);
+  my $total_open_amount      = $ref->{total_open_amount};
+  if ($form->{l_include_credit_notes}) {
+    # a bit stupid, but redo calc because of credit notes
+    $total_open_amount      = 0;
+    foreach my $amount (@{ $form->{TEMPLATE_ARRAYS}->{dn_open_amount} }) {
+      $total_open_amount += $form->parse_amount($myconfig, $amount, 2);
+    }
+  }
+  $form->{total_open_amount} = $form->format_amount($myconfig, $form->round_amount($total_open_amount, 2), 2);
+  $form->{total_amount}      = $form->format_amount($myconfig, $form->round_amount($ref->{fee} + $ref->{total_interest} + $total_open_amount, 2), 2);
 
   $::form->format_dates($output_dateformat, $output_longdates,
     qw(dn_dunning_date dn_dunning_duedate dn_transdate dn_duedate
@@ -885,13 +1045,29 @@ sub print_dunning {
 
   delete $form->{tmpfile};
 
-  push @{ $form->{DUNNING_PDFS} }, $filename;
-  push @{ $form->{DUNNING_PDFS_EMAIL} }, { 'filename' => "${spool}/$filename",
-                                           'name'     => $form->get_formname_translation('dunning') . "_${dunning_id}.pdf" };
+  my $employee_id = ($::instance_conf->get_dunning_creator eq 'invoice_employee') ?
+                      $form->{employee_id}                                        :
+                      SL::DB::Manager::Employee->current->id;
+
+  $form->get_employee_data('prefix' => 'employee', 'id' => $employee_id);
+  $form->get_employee_data('prefix' => 'salesman', 'id' => $form->{salesman_id});
+
+  $form->{attachment_type}    = "dunning";
+  if ( $form->{dunning_level} ) {
+    $form->{attachment_type} .= $form->{dunning_level} if $form->{dunning_level} < 4;
+  }
+  $form->{attachment_filename} = $form->get_formname_translation($form->{attachment_type}) . "_${dunning_id}.pdf";
+  $form->{attachment_id} = $form->{invoice_id};
 
+  # this generates the file in the spool directory
   $form->parse_template($myconfig);
 
-  $dbh->disconnect() unless $provided_dbh;
+  push @{ $form->{DUNNING_PDFS} }        , $filename;
+  push @{ $form->{DUNNING_PDFS_EMAIL} }  , { 'path'       => "${spool}/$filename",
+                                             'name'       => $form->get_formname_translation('dunning') . "_${dunning_id}.pdf" };
+  push @{ $form->{DUNNING_PDFS_STORAGE} }, { 'dunning_id' => $dunning_id,
+                                             'path'       => "${spool}/$filename",
+                                             'name'       => $form->get_formname_translation('dunning') . "_${dunning_id}.pdf" };
 
   $main::lxdebug->leave_sub();
 }
@@ -901,18 +1077,20 @@ sub print_invoice_for_fees {
 
   my ($self, $myconfig, $form, $dunning_id, $provided_dbh) = @_;
 
-  my $dbh = $provided_dbh ? $provided_dbh : $form->dbconnect($myconfig);
+  my $dbh = $provided_dbh || SL::DB->client->dbh;
 
   my ($query, @values, $sth);
 
   $query =
     qq|SELECT
          d.fee_interest_ar_id,
-         dcfg.template
+         d.trans_id AS invoice_id,
+         dcfg.template,
+         dcfg.dunning_level
        FROM dunning d
        LEFT JOIN dunning_config dcfg ON (d.dunning_config_id = dcfg.id)
        WHERE d.dunning_id = ?|;
-  my ($ar_id, $template) = selectrow_query($form, $dbh, $query, $dunning_id);
+  my ($ar_id, $invoice_id, $template, $dunning_level) = selectrow_query($form, $dbh, $query, $dunning_id);
 
   if (!$ar_id) {
     $main::lxdebug->leave_sub();
@@ -971,7 +1149,7 @@ sub print_invoice_for_fees {
   $self->set_customer_cvars($myconfig, $form);
   $self->set_template_options($myconfig, $form);
 
-  my $filename = Common::unique_id() . "dunning_invoice_${dunning_id}.pdf";
+  my $filename = Common::unique_id() . "dunning_invoice_" . $form->{invnumber} . ".pdf";
 
   my $spool             = $::lx_office_conf{paths}->{spool};
   $form->{OUT}          = "$spool/$filename";
@@ -980,15 +1158,20 @@ sub print_invoice_for_fees {
 
   map { delete $form->{$_} } grep /^[a-z_]+_\d+$/, keys %{ $form };
 
+  my $attachment_filename      = $form->get_formname_translation('dunning_invoice') . "_" . $form->{invnumber} . ".pdf";
+  $form->{attachment_filename} = $attachment_filename;
+  $form->{attachment_type}     = "dunning";
+  $form->{attachment_id}       = $invoice_id;
   $form->parse_template($myconfig);
 
   restore_form($saved_form);
 
-  push @{ $form->{DUNNING_PDFS} }, $filename;
-  push @{ $form->{DUNNING_PDFS_EMAIL} }, { 'filename' => "${spool}/$filename",
-                                           'name'     => "dunning_invoice_${dunning_id}.pdf" };
-
-  $dbh->disconnect() unless $provided_dbh;
+  push @{ $form->{DUNNING_PDFS} },         $filename;
+  push @{ $form->{DUNNING_PDFS_EMAIL} },   { 'path'       => "${spool}/$filename",
+                                             'name'       => $attachment_filename };
+  push @{ $form->{DUNNING_PDFS_STORAGE} }, { 'dunning_id' => $dunning_id,
+                                             'path'       => "${spool}/$filename",
+                                             'name'       => $attachment_filename };
 
   $main::lxdebug->leave_sub();
 }
@@ -1005,6 +1188,108 @@ sub set_customer_cvars {
                                                   translation_type => 'greetings::' . ($form->{cp_gender} eq 'f' ? 'female' : 'male'),
                                                   language_id      => $form->{language_id},
                                                   allow_fallback   => 1);
+  if ($form->{cp_id}) {
+    $custom_variables = CVar->get_custom_variables(dbh      => $form->get_standard_dbh,
+                                                   module   => 'Contacts',
+                                                   trans_id => $form->{cp_id});
+    $form->{"cp_cvar_$_->{name}"} = $_->{value} for @{ $custom_variables };
+  }
+
 }
 
+sub print_original_invoice {
+  my ($self, $myconfig, $form, $dunning_id, $invoice_id) = @_;
+  # get one invoice as object and print to pdf
+  my $invoice = SL::DB::Invoice->new(id => $invoice_id)->load;
+
+  die "Invalid invoice object" unless ref($invoice) eq 'SL::DB::Invoice';
+
+  my $print_form          = Form->new('');
+  $print_form->{type}     = 'invoice';
+  $print_form->{formname} = 'invoice',
+  $print_form->{format}   = 'pdf',
+  $print_form->{media}    = 'file';
+  # no language override, should always be the object's language
+  $invoice->flatten_to_form($print_form, format_amounts => 1);
+  for my $i (1 .. $print_form->{rowcount}) {
+    $print_form->{"sellprice_$i"} = $print_form->{"fxsellprice_$i"};
+  }
+  $print_form->prepare_for_printing;
+
+  my $filename = SL::Helper::CreatePDF->create_pdf(
+                   template               => 'invoice.tex',
+                   variables              => $print_form,
+                   return                 => 'file_name',
+                   variable_content_types => {
+                     longdescription => 'html',
+                     partnotes       => 'html',
+                     notes           => 'html',
+                     $print_form->get_variable_content_types_for_cvars,
+                   },
+  );
+
+  my $spool       = $::lx_office_conf{paths}->{spool};
+  my ($volume, $directory, $file_name) = File::Spec->splitpath($filename);
+  my $full_file_name                   = File::Spec->catfile($spool, $file_name);
+
+  move($filename, $full_file_name) or die "The move operation failed: $!";
+
+  # form get_formname_translation should use language_id_$i
+  my $saved_reicpient_locale = $form->{recipient_locale};
+  $form->{recipient_locale}  = $invoice->language;
+
+  my $attachment_filename    = $form->get_formname_translation('invoice') . "_" . $invoice->invnumber . ".pdf";
+
+  push @{ $form->{DUNNING_PDFS} },         $file_name;
+  push @{ $form->{DUNNING_PDFS_EMAIL} },   { 'path'       => "${spool}/$file_name",
+                                             'name'       => $attachment_filename };
+  push @{ $form->{DUNNING_PDFS_STORAGE} }, { 'dunning_id' => $dunning_id,
+                                             'path'       => "${spool}/$file_name",
+                                             'name'       => $attachment_filename };
+
+  $form->{recipient_locale}  = $saved_reicpient_locale;
+}
+
+sub _store_pdf_to_webdav_and_filemanagement {
+  my ($dunning_id, $path, $name) =@_;
+
+  my @errors;
+
+  if ($::instance_conf->get_doc_storage) {
+    eval {
+      SL::File->save(
+        object_id   => $dunning_id,
+        object_type => 'dunning',
+        mime_type   => 'application/pdf',
+        source      => 'created',
+        file_type   => 'document',
+        file_name   => $name,
+        file_path   => $path,
+      );
+      1;
+    } or do {
+      push @errors, $::locale->text('Storing PDF in storage backend failed: #1', $@);
+    };
+  }
+
+  if ($::instance_conf->get_webdav_documents) {
+    eval {
+      my $webdav = SL::Webdav->new(
+        type     => 'dunning',
+        number   => $dunning_id,
+      );
+      my $webdav_file = SL::Webdav::File->new(
+        webdav   => $webdav,
+        filename => $name,
+      );
+      $webdav_file->store(file => $path);
+    } or do {
+      push @errors, $::locale->text('Storing PDF to webdav folder failed: #1', $@);
+    };
+  }
+
+  return @errors;
+}
+
+
 1;
index 4e109d8..ebc6051 100644 (file)
--- a/SL/DO.pm
+++ b/SL/DO.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Delivery Order entry module
 
 package DO;
 
+use Carp;
 use List::Util qw(max);
-use YAML;
+use Text::ParseWords;
 
 use SL::AM;
 use SL::Common;
 use SL::CVar;
 use SL::DB::DeliveryOrder;
+use SL::DB::DeliveryOrder::TypeData qw(:types is_valid_type);
 use SL::DB::Status;
 use SL::DBUtils;
+use SL::Helper::ShippedQty;
 use SL::HTML::Restrict;
 use SL::RecordLinks;
 use SL::IC;
 use SL::TransNumber;
+use SL::DB;
+use SL::Util qw(trim);
+use SL::YAML;
 
 use strict;
 
@@ -73,6 +80,7 @@ sub transactions {
          dord.transaction_description, dord.itime::DATE AS insertdate,
          pr.projectnumber AS globalprojectnumber,
          dep.description AS department,
+         dord.order_type,
          e.name AS employee,
          sm.name AS salesman
        FROM delivery_orders dord
@@ -84,7 +92,10 @@ sub transactions {
        LEFT JOIN department dep ON (dord.department_id = dep.id)
 |;
 
-  push @where, ($form->{type} eq 'sales_delivery_order' ? '' : 'NOT ') . qq|COALESCE(dord.is_sales, FALSE)|;
+  if ($form->{type} && is_valid_type($form->{type})) {
+    push @where, 'dord.order_type = ?';
+    push @values, $form->{type};
+  }
 
   if ($form->{department_id}) {
     push @where,  qq|dord.department_id = ?|;
@@ -110,12 +121,12 @@ sub transactions {
 
   } elsif ($form->{$vc}) {
     push @where,  qq|ct.name ILIKE ?|;
-    push @values, '%' . $form->{$vc} . '%';
+    push @values, like($form->{$vc});
   }
 
   if ($form->{"cp_name"}) {
     push @where, "(cp.cp_name ILIKE ? OR cp.cp_givenname ILIKE ?)";
-    push @values, ('%' . $form->{"cp_name"} . '%')x2;
+    push @values, (like($form->{"cp_name"}))x2;
   }
 
   foreach my $item (qw(employee_id salesman_id)) {
@@ -123,7 +134,8 @@ sub transactions {
     push @where, "dord.$item = ?";
     push @values, conv_i($form->{$item});
   }
-  if (!$main::auth->assert('sales_all_edit', 1)) {
+  if ( !(    ($vc eq 'customer' && ($main::auth->assert('sales_all_edit',    1) || $main::auth->assert('sales_delivery_order_view',    1)))
+          || ($vc eq 'vendor'   && ($main::auth->assert('purchase_all_edit', 1) || $main::auth->assert('purchase_delivery_order_view', 1))) ) ) {
     push @where, qq|dord.employee_id = (select id from employee where login= ?)|;
     push @values, $::myconfig{login};
   }
@@ -131,7 +143,7 @@ sub transactions {
   foreach my $item (qw(donumber ordnumber cusordnumber transaction_description)) {
     next unless ($form->{$item});
     push @where,  qq|dord.$item ILIKE ?|;
-    push @values, '%' . $form->{$item} . '%';
+    push @values, like($form->{$item});
   }
 
   if (($form->{open} || $form->{closed}) &&
@@ -146,7 +158,7 @@ sub transactions {
 
   if ($form->{serialnumber}) {
     push @where, 'dord.id IN (SELECT doi.delivery_order_id FROM delivery_order_items doi WHERE doi.serialnumber LIKE ?)';
-    push @values, '%' . $form->{serialnumber} . '%';
+    push @values, like($form->{serialnumber});
   }
 
   if($form->{transdatefrom}) {
@@ -179,6 +191,44 @@ sub transactions {
     push @values, conv_date($form->{insertdateto});
   }
 
+  if ($form->{parts_partnumber}) {
+    push @where, <<SQL;
+      EXISTS (
+        SELECT delivery_order_items.delivery_order_id
+        FROM delivery_order_items
+        LEFT JOIN parts ON (delivery_order_items.parts_id = parts.id)
+        WHERE (delivery_order_items.delivery_order_id = dord.id)
+          AND (parts.partnumber ILIKE ?)
+        LIMIT 1
+      )
+SQL
+    push @values, like($form->{parts_partnumber});
+  }
+
+  if ($form->{parts_description}) {
+    push @where, <<SQL;
+      EXISTS (
+        SELECT delivery_order_items.delivery_order_id
+        FROM delivery_order_items
+        WHERE (delivery_order_items.delivery_order_id = dord.id)
+          AND (delivery_order_items.description ILIKE ?)
+        LIMIT 1
+      )
+SQL
+    push @values, like($form->{parts_description});
+  }
+
+  if ($form->{all}) {
+    my @tokens = parse_line('\s+', 0, $form->{all});
+    # ordnumber quonumber customer.name vendor.name transaction_description
+    push @where, <<SQL for @tokens;
+      (   (dord.donumber                ILIKE ?)
+       OR (ct.name                      ILIKE ?)
+       OR (dord.transaction_description ILIKE ?))
+SQL
+    push @values, (like($_))x3 for @tokens;
+  }
+
   if (@where) {
     $query .= " WHERE " . join(" AND ", map { "($_)" } @where);
   }
@@ -231,6 +281,16 @@ sub transactions {
 }
 
 sub save {
+  my ($self) = @_;
+  $main::lxdebug->enter_sub();
+
+  my $rc = SL::DB->client->with_transaction(\&_save, $self);
+
+  $main::lxdebug->leave_sub();
+  return $rc;
+}
+
+sub _save {
   $main::lxdebug->enter_sub();
 
   my ($self)   = @_;
@@ -238,8 +298,7 @@ sub save {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  # connect to database, turn off autocommit
-  my $dbh = $form->get_standard_dbh($myconfig);
+  my $dbh = SL::DB->client->dbh;
   my $restricter = SL::HTML::Restrict->create;
 
   my ($query, @values, $sth, $null);
@@ -262,6 +321,11 @@ sub save {
   if ($form->{id}) {
 
     # only delete shipto complete
+    $query = qq|DELETE FROM custom_variables
+                WHERE (config_id IN (SELECT id        FROM custom_variable_configs WHERE (module = 'ShipTo')))
+                  AND (trans_id  IN (SELECT shipto_id FROM shipto                  WHERE (module = 'DO') AND (trans_id = ?)))|;
+    do_query($form, $dbh, $query, $form->{id});
+
     $query = qq|DELETE FROM shipto WHERE trans_id = ? AND module = 'DO'|;
     do_query($form, $dbh, $query, conv_i($form->{id}));
 
@@ -270,15 +334,15 @@ sub save {
     $query = qq|SELECT nextval('id')|;
     ($form->{id}) = selectrow_query($form, $dbh, $query);
 
-    $query = qq|INSERT INTO delivery_orders (id, donumber, employee_id, currency_id, taxzone_id) VALUES (?, '', ?, (SELECT currency_id FROM defaults LIMIT 1), ?)|;
-    do_query($form, $dbh, $query, $form->{id}, conv_i($form->{employee_id}), $form->{taxzone_id});
+    $query = qq|INSERT INTO delivery_orders (id, donumber, employee_id, currency_id, taxzone_id, order_type) VALUES (?, '', ?, (SELECT currency_id FROM defaults LIMIT 1), ?, ?)|;
+    do_query($form, $dbh, $query, $form->{id}, conv_i($form->{employee_id}), $form->{taxzone_id}, SALES_DELIVERY_ORDER_TYPE);
   }
 
   my $project_id;
   my $items_reqdate;
 
   $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS');
-  my %price_factors = map { $_->{id} => $_->{factor} } @{ $form->{ALL_PRICE_FACTORS} };
+  my %price_factors = map { $_->{id} => $_->{factor} *1 } @{ $form->{ALL_PRICE_FACTORS} };
   my $price_factor;
 
   my %part_id_map = map { $_ => 1 } grep { $_ } map { $form->{"id_$_"} } (1 .. $form->{rowcount});
@@ -390,9 +454,9 @@ SQL
         do_query($form, $dbh, $query, conv_i($sinfo->{"delivery_order_items_stock_id"}),
                   conv_i($form->{"delivery_order_items_id_$i"}), $sinfo->{qty}, $sinfo->{unit}, conv_i($sinfo->{warehouse_id}),
                   conv_i($sinfo->{bin_id}));
-       $h_item_stock_id->finish();
-      # write back the id to the form (important if only transfer was clicked (id fk for invoice)
-      $form->{"stock_${in_out}_$i"} = YAML::Dump($stock_info);
+        $h_item_stock_id->finish();
+        # write back the id to the form (important if only transfer was clicked (id fk for invoice)
+        $form->{"stock_${in_out}_$i"} = SL::YAML::Dump($stock_info);
       }
       @values = ($form->{"delivery_order_items_id_$i"}, $sinfo->{qty}, $sinfo->{unit}, conv_i($sinfo->{warehouse_id}),
                  conv_i($sinfo->{bin_id}), $sinfo->{chargenumber}, conv_date($sinfo->{bestbefore}),
@@ -453,30 +517,32 @@ SQL
   $query =
     qq|UPDATE delivery_orders SET
          donumber = ?, ordnumber = ?, cusordnumber = ?, transdate = ?, vendor_id = ?,
-         customer_id = ?, reqdate = ?,
+         customer_id = ?, reqdate = ?, tax_point = ?,
          shippingpoint = ?, shipvia = ?, notes = ?, intnotes = ?, closed = ?,
-         delivered = ?, department_id = ?, language_id = ?, shipto_id = ?,
+         delivered = ?, department_id = ?, language_id = ?, shipto_id = ?, billing_address_id = ?,
          globalproject_id = ?, employee_id = ?, salesman_id = ?, cp_id = ?, transaction_description = ?,
-         is_sales = ?, taxzone_id = ?, taxincluded = ?, payment_id = ?, currency_id = (SELECT id FROM currencies WHERE name = ?),
+         order_type = ?, taxzone_id = ?, taxincluded = ?, payment_id = ?, currency_id = (SELECT id FROM currencies WHERE name = ?),
          delivery_term_id = ?
        WHERE id = ?|;
 
   @values = ($form->{donumber}, $form->{ordnumber},
              $form->{cusordnumber}, conv_date($form->{transdate}),
              conv_i($form->{vendor_id}), conv_i($form->{customer_id}),
-             conv_date($form->{reqdate}), $form->{shippingpoint}, $form->{shipvia},
+             conv_date($form->{reqdate}), conv_date($form->{tax_point}), $form->{shippingpoint}, $form->{shipvia},
              $restricter->process($form->{notes}), $form->{intnotes},
              $form->{closed} ? 't' : 'f', $form->{delivered} ? "t" : "f",
-             conv_i($form->{department_id}), conv_i($form->{language_id}), conv_i($form->{shipto_id}),
+             conv_i($form->{department_id}), conv_i($form->{language_id}), conv_i($form->{shipto_id}), conv_i($form->{billing_address_id}),
              conv_i($form->{globalproject_id}), conv_i($form->{employee_id}),
              conv_i($form->{salesman_id}), conv_i($form->{cp_id}),
              $form->{transaction_description},
-             $form->{type} =~ /^sales/ ? 't' : 'f',
+             $form->{type} =~ /^sales/ ? SALES_DELIVERY_ORDER_TYPE : PURCHASE_DELIVERY_ORDER_TYPE,
              conv_i($form->{taxzone_id}), $form->{taxincluded} ? 't' : 'f', conv_i($form->{payment_id}), $form->{currency},
              conv_i($form->{delivery_term_id}),
              conv_i($form->{id}));
   do_query($form, $dbh, $query, @values);
 
+  $form->new_lastmtime('delivery_orders');
+
   $form->{name} = $form->{ $form->{vc} };
   $form->{name} =~ s/--$form->{"$form->{vc}_id"}//;
 
@@ -497,12 +563,10 @@ SQL
                             'to_id'      => $form->{id},
     );
   delete $form->{convert_from_oe_ids};
-
-  $self->mark_orders_if_delivered('do_id' => $form->{id},
-                                  'type'  => $form->{type} eq 'sales_delivery_order' ? 'sales' : 'purchase',
-                                  'dbh'   => $dbh,);
-
-  my $rc = $dbh->commit();
+  unless ($::instance_conf->get_shipped_qty_require_stock_out) {
+    $self->mark_orders_if_delivered('do_id' => $form->{id},
+                                    'type'  => $form->{type} eq 'sales_delivery_order' ? 'sales' : 'purchase');
+  }
 
   $form->{saved_donumber} = $form->{donumber};
   $form->{saved_ordnumber} = $form->{ordnumber};
@@ -512,74 +576,28 @@ SQL
 
   $main::lxdebug->leave_sub();
 
-  return $rc;
+  return 1;
 }
 
 sub mark_orders_if_delivered {
-  $main::lxdebug->enter_sub();
-
-  my $self   = shift;
-  my %params = @_;
+  my ($self, %params) = @_;
 
   Common::check_params(\%params, qw(do_id type));
 
-  my $myconfig = \%main::myconfig;
-  my $form     = $main::form;
-
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
-
-  my @links    = RecordLinks->get_links('dbh'        => $dbh,
-                                        'from_table' => 'oe',
-                                        'to_table'   => 'delivery_orders',
-                                        'to_id'      => $params{do_id});
-
-  my $oe_id  = @links ? $links[0]->{from_id} : undef;
-
-  return $main::lxdebug->leave_sub() if (!$oe_id);
-
-  my $all_units = AM->retrieve_all_units();
-
-  my $query     = qq|SELECT oi.parts_id, oi.qty, oi.unit, p.unit AS partunit
-                     FROM orderitems oi
-                     LEFT JOIN parts p ON (oi.parts_id = p.id)
-                     WHERE (oi.trans_id = ?)|;
-  my $sth       = prepare_execute_query($form, $dbh, $query, $oe_id);
-
-  my %shipped   = $self->get_shipped_qty('type'  => $params{type},
-                                         'oe_id' => $oe_id,);
-  my %ordered   = ();
-
-  while (my $ref = $sth->fetchrow_hashref()) {
-    $ref->{baseqty} = $ref->{qty} * $all_units->{$ref->{unit}}->{factor} / $all_units->{$ref->{partunit}}->{factor};
-
-    if ($ordered{$ref->{parts_id}}) {
-      $ordered{$ref->{parts_id}}->{baseqty} += $ref->{baseqty};
-    } else {
-      $ordered{$ref->{parts_id}}             = $ref;
-    }
-  }
+  my $do     = SL::DB::Manager::DeliveryOrder->find_by(id => $params{do_id});
+  my $orders = $do->linked_records(from => 'Order');
 
-  $sth->finish();
+  SL::Helper::ShippedQty->new->calculate($orders)->write_to_objects;
 
-  map { $_->{baseqty} = $_->{qty} * $all_units->{$_->{unit}}->{factor} / $all_units->{$_->{partunit}}->{factor} } values %shipped;
+  SL::DB->client->with_transaction(sub {
+    for my $oe (@$orders) {
+      next if $params{type} eq 'sales'    && !$oe->customer_id;
+      next if $params{type} eq 'purchase' && !$oe->vendor_id;
 
-  my $delivered = 1;
-  foreach my $part (values %ordered) {
-    if (!$shipped{$part->{parts_id}} || ($shipped{$part->{parts_id}}->{baseqty} < $part->{baseqty})) {
-      $delivered = 0;
-      last;
+      $oe->update_attributes(delivered => $oe->{delivered});
     }
-  }
-
-  if ($delivered) {
-    $query = qq|UPDATE oe
-                SET delivered = TRUE
-                WHERE id = ?|;
-    do_query($form, $dbh, $query, $oe_id);
-    $dbh->commit() if (!$params{dbh});
-  }
-
-  $main::lxdebug->leave_sub();
+    1;
+  }) or do { die SL::DB->client->error };
 }
 
 sub close_orders {
@@ -598,13 +616,16 @@ sub close_orders {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
+  SL::DB->client->with_transaction(sub {
+    my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
-  my $query    = qq|UPDATE delivery_orders SET closed = TRUE WHERE id IN (| . join(', ', ('?') x scalar(@{ $params{ids} })) . qq|)|;
+    my $query    = qq|UPDATE delivery_orders SET closed = TRUE WHERE id IN (| . join(', ', ('?') x scalar(@{ $params{ids} })) . qq|)|;
 
-  do_query($form, $dbh, $query, map { conv_i($_) } @{ $params{ids} });
+    do_query($form, $dbh, $query, map { conv_i($_) } @{ $params{ids} });
+    1;
+  }) or die { SL::DB->client->error };
 
-  $dbh->commit() unless ($params{dbh});
+  $form->new_lastmtime('delivery_orders');
 
   $main::lxdebug->leave_sub();
 }
@@ -634,6 +655,38 @@ sub delete {
   return $rc;
 }
 
+sub delete_transfers {
+  $main::lxdebug->enter_sub();
+
+  my ($self)   = @_;
+
+  my $myconfig = \%main::myconfig;
+  my $form     = $main::form;
+
+  my $rc = SL::DB::Order->new->db->with_transaction(sub {
+
+    my $do = SL::DB::DeliveryOrder->new(id => $form->{id})->load;
+    die "No valid delivery order found" unless ref $do eq 'SL::DB::DeliveryOrder';
+
+    my $dt = DateTime->today->subtract(days => $::instance_conf->get_undo_transfer_interval);
+    croak "Wrong call. Please check undoing interval" unless $do->itime > $dt;
+
+    foreach my $doi (@{ $do->orderitems }) {
+      foreach my $dois (@{ $doi->delivery_order_stock_entries}) {
+        $dois->inventory->delete;
+        $dois->delete;
+      }
+    }
+    $do->update_attributes(delivered => 0);
+
+    1;
+  });
+
+  $main::lxdebug->leave_sub();
+
+  return $rc;
+}
+
 sub retrieve {
   $main::lxdebug->enter_sub();
 
@@ -678,13 +731,13 @@ sub retrieve {
   # so if any of these infos is important (or even different) for any item,
   # it will be killed out and then has to be fetched from the item scope query further down
   $query =
-    qq|SELECT dord.cp_id, dord.donumber, dord.ordnumber, dord.transdate, dord.reqdate,
+    qq|SELECT dord.cp_id, dord.donumber, dord.ordnumber, dord.transdate, dord.reqdate, dord.tax_point,
          dord.shippingpoint, dord.shipvia, dord.notes, dord.intnotes,
          e.name AS employee, dord.employee_id, dord.salesman_id,
          dord.${vc}_id, cv.name AS ${vc},
          dord.closed, dord.reqdate, dord.department_id, dord.cusordnumber,
          d.description AS department, dord.language_id,
-         dord.shipto_id,
+         dord.shipto_id, dord.billing_address_id,
          dord.itime, dord.mtime,
          dord.globalproject_id, dord.delivered, dord.transaction_description,
          dord.taxzone_id, dord.taxincluded, dord.payment_id, (SELECT cu.name FROM currencies cu WHERE cu.id=dord.currency_id) AS currency,
@@ -740,10 +793,18 @@ sub retrieve {
     $sth   = prepare_execute_query($form, $dbh, $query, $form->{id});
 
     $ref   = $sth->fetchrow_hashref("NAME_lc");
-    delete $ref->{id};
-    map { $form->{$_} = $ref->{$_} } keys %$ref;
+    $form->{$_} = $ref->{$_} for grep { m{^shipto(?!_id$)} } keys %$ref;
     $sth->finish();
 
+    if ($ref->{shipto_id}) {
+      my $cvars = CVar->get_custom_variables(
+        dbh      => $dbh,
+        module   => 'ShipTo',
+        trans_id => $ref->{shipto_id},
+      );
+      $form->{"shiptocvar_$_->{name}"} = $_->{value} for @{ $cvars };
+    }
+
     # get printed, emailed and queued
     $query = qq|SELECT s.printed, s.emailed, s.spoolfile, s.formname FROM status s WHERE s.trans_id = ?|;
     $sth   = prepare_execute_query($form, $dbh, $query, conv_i($form->{id}));
@@ -765,7 +826,7 @@ sub retrieve {
   # stuff different from the whole will not be overwritten, but saved with a suffix.
   $query =
     qq|SELECT doi.id AS delivery_order_items_id,
-         p.partnumber, p.assembly, p.listprice, doi.description, doi.qty,
+         p.partnumber, p.part_type, p.listprice, doi.description, doi.qty,
          doi.sellprice, doi.parts_id AS id, doi.unit, doi.discount, p.notes AS partnotes,
          doi.reqdate, doi.project_id, doi.serialnumber, doi.lastcost,
          doi.ordnumber, doi.transdate, doi.cusordnumber, doi.longdescription,
@@ -810,7 +871,7 @@ sub retrieve {
         push @{ $requests }, $ref;
       }
 
-      $doi->{"stock_${in_out}"} = YAML::Dump($requests);
+      $doi->{"stock_${in_out}"} = SL::YAML::Dump($requests);
     }
 
     $sth->finish();
@@ -912,7 +973,7 @@ sub order_details {
   push @arrays, map { "project_cvar_$_->{name}" } @{ $project_cvar_configs };
 
   $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS');
-  my %price_factors = map { $_->{id} => $_->{factor} } @{ $form->{ALL_PRICE_FACTORS} };
+  my %price_factors = map { $_->{id} => $_->{factor} *1 } @{ $form->{ALL_PRICE_FACTORS} };
 
   my $totalweight = 0;
   my $sameitem = "";
@@ -1000,16 +1061,16 @@ sub order_details {
       push @{ $form->{TEMPLATE_ARRAYS}{si_unit}[$si_position-1] },          $si->{unit};
     }
 
-    if ($form->{"assembly_$i"}) {
+    if ($form->{"part_type_$i"} eq 'assembly') {
       $sameitem = "";
 
       # get parts and push them onto the stack
       my $sortorder = "";
       if ($form->{groupitems}) {
         $sortorder =
-          qq|ORDER BY pg.partsgroup, a.oid|;
+          qq|ORDER BY pg.partsgroup, a.position|;
       } else {
-        $sortorder = qq|ORDER BY a.oid|;
+        $sortorder = qq|ORDER BY a.position|;
       }
 
       do_statement($form, $h_pg, $q_pg, conv_i($form->{"id_$i"}));
@@ -1053,30 +1114,15 @@ sub order_details {
   $h_pg->finish();
   $h_bin_wh->finish();
 
+  $form->{department}    = SL::DB::Manager::Department->find_by(id => $form->{department_id})->description if $form->{department_id};
   $form->{delivery_term} = SL::DB::Manager::DeliveryTerm->find_by(id => $form->{delivery_term_id} || undef);
   $form->{delivery_term}->description_long($form->{delivery_term}->translated_attribute('description_long', $form->{language_id})) if $form->{delivery_term} && $form->{language_id};
-  $form->{department}    = SL::DB::Manager::Department->find_by(id => $form->{department_id})->load->description if $form->{department_id};
 
   $form->{username} = $myconfig->{name};
 
   $main::lxdebug->leave_sub();
 }
 
-sub project_description {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $dbh, $id) = @_;
-
-  my $form     =  $main::form;
-
-  my $query = qq|SELECT description FROM project WHERE id = ?|;
-  my ($value) = selectrow_query($form, $dbh, $query, $id);
-
-  $main::lxdebug->leave_sub();
-
-  return $value;
-}
-
 sub unpack_stock_information {
   $main::lxdebug->enter_sub();
 
@@ -1087,7 +1133,7 @@ sub unpack_stock_information {
 
   my $unpacked;
 
-  eval { $unpacked = $params{packed} ? YAML::Load($params{packed}) : []; };
+  eval { $unpacked = $params{packed} ? SL::YAML::Load($params{packed}) : []; };
 
   $unpacked = [] if (!$unpacked || ('ARRAY' ne ref $unpacked));
 
@@ -1223,57 +1269,12 @@ sub transfer_in_out {
 
   WH->transfer(@transfers);
 
-  $main::lxdebug->leave_sub();
-}
-
-sub get_shipped_qty {
-  $main::lxdebug->enter_sub();
-
-  my $self     = shift;
-  my %params   = @_;
-
-  Common::check_params(\%params, qw(type oe_id));
-
-  my $myconfig = \%main::myconfig;
-  my $form     = $main::form;
-
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
-
-  my @links    = RecordLinks->get_links('dbh'        => $dbh,
-                                        'from_table' => 'oe',
-                                        'from_id'    => $params{oe_id},
-                                        'to_table'   => 'delivery_orders');
-  my @values   = map { $_->{to_id} } @links;
-
-  if (!scalar @values) {
-    $main::lxdebug->leave_sub();
-    return ();
-  }
-
-  my $query =
-    qq|SELECT doi.parts_id, doi.qty, doi.unit, p.unit AS partunit
-       FROM delivery_order_items doi
-       LEFT JOIN delivery_orders o ON (doi.delivery_order_id = o.id)
-       LEFT JOIN parts p ON (doi.parts_id = p.id)
-       WHERE o.id IN (| . join(', ', ('?') x scalar @values) . qq|)|;
-
-  my %ship      = ();
-  my $entries   = selectall_hashref_query($form, $dbh, $query, @values);
-  my $all_units = AM->retrieve_all_units();
-
-  foreach my $entry (@{ $entries }) {
-    $entry->{qty} *= AM->convert_unit($entry->{unit}, $entry->{partunit}, $all_units);
-
-    if (!$ship{$entry->{parts_id}}) {
-      $ship{$entry->{parts_id}} = $entry;
-    } else {
-      $ship{$entry->{parts_id}}->{qty} += $entry->{qty};
-    }
+  if ($::instance_conf->get_shipped_qty_require_stock_out) {
+    $self->mark_orders_if_delivered('do_id' => $form->{id},
+                                    'type'  => $form->{type} eq 'sales_delivery_order' ? 'sales' : 'purchase');
   }
 
   $main::lxdebug->leave_sub();
-
-  return %ship;
 }
 
 sub is_marked_as_delivered {
diff --git a/SL/DefaultManager.pm b/SL/DefaultManager.pm
new file mode 100644 (file)
index 0000000..b9940c0
--- /dev/null
@@ -0,0 +1,94 @@
+package SL::DefaultManager;
+
+use strict;
+
+use SL::Util qw(camelify);
+use List::Util qw(first);
+
+my %manager_cache;
+
+sub new {
+  my ($class, @defaults) = @_;
+  bless [ @defaults ], $class;
+}
+
+sub _managers {
+  my ($self) = @_;
+
+  map { $self->_get($_) } @$self;
+}
+
+sub _get {
+  my ($class, $name) = @_;
+
+  return if !$name;
+
+  $manager_cache{$name} ||= do {
+    die "'$name' doesn't look like a default manager." unless $name =~ /^\w+$/;
+
+    my $package = 'SL::DefaultManager::' . camelify($name);
+
+    eval "require $package; 1" or die "could not load default manager '$package': $@";
+
+    $package->new;
+  }
+}
+
+sub AUTOLOAD {
+  our $AUTOLOAD;
+
+  my ($self, @args) = @_;
+
+  my $method        =  $AUTOLOAD;
+  $method           =~ s/.*:://;
+  return if $method eq 'DESTROY';
+
+  my $manager = first { $_->can($method) } $self->_managers;
+
+  return $manager ? $manager->$method : @args;
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::DefaultManager - sets of defaults for use outside of clients
+
+=head1 SYNOPSIS
+
+  # during startup
+  my $defaults = SL::DefaultManager->new($::lx_office_conf{default_manager});
+
+  # during tests
+  my $defaults = SL::DefaultManager->new('swiss');
+
+  # in consuming code
+  # will return what the manager provides, or the given value if $defaults does
+  # not handle dateformat
+  my $dateformat = $defaults->dateformat('dd.mm.yyyy');
+
+  # have several default managers for different tasks
+  # if polled the first defined response will win
+  my $defaults = SL::DefaultManager->new('swiss', 'mobile', 'point_of_sale');
+
+=head1 DESCRIPTION
+
+TODO
+
+=head1 FUNCTIONS
+
+TODO
+
+=head1 BUGS
+
+None yet :)
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut
diff --git a/SL/DefaultManager/German.pm b/SL/DefaultManager/German.pm
new file mode 100644 (file)
index 0000000..d4ce4c0
--- /dev/null
@@ -0,0 +1,28 @@
+package SL::DefaultManager::German;
+
+use strict;
+use parent qw(Rose::Object);
+
+# client defaults
+sub chart_of_accounts       { 'Germany-DATEV-SKR03EU' }
+sub accounting_method       { 'cash' }
+sub inventory_system        { 'periodic' }
+sub profit_determination    { 'income' }
+sub currency                { 'EUR' }
+sub precision               { 0.01 }
+sub feature_balance         { 1 }
+sub feature_datev           { 1 }
+sub feature_erfolgsrechnung { 0 }
+sub feature_eurechnung      { 1 }
+sub feature_ustva           { 1 }
+
+# user defaults
+sub numberformat            { '1.000,00' }
+sub dateformat              { 'dd.mm.yy' }
+sub timeformat              { 'hh:mm' }
+
+# default for login/admin areas
+sub country                 { 'DE' }
+sub language                { 'de' }
+
+1;
diff --git a/SL/DefaultManager/Swiss.pm b/SL/DefaultManager/Swiss.pm
new file mode 100644 (file)
index 0000000..f5f553e
--- /dev/null
@@ -0,0 +1,28 @@
+package SL::DefaultManager::Swiss;
+
+use strict;
+use parent qw(Rose::Object);
+
+# client defaults
+sub chart_of_accounts       { 'Switzerland-deutsch-MWST-2014' }
+sub accounting_method       { 'accrual' }
+sub inventory_system        { 'periodic' }
+sub profit_determination    { 'balance' }
+sub currency                { 'CHF' }
+sub precision               { 0.05 }
+sub feature_balance         { 1 }
+sub feature_datev           { 0 }
+sub feature_erfolgsrechnung { 1 }
+sub feature_eurechnung      { 0 }
+sub feature_ustva           { 0 }
+
+# user defaults
+sub numberformat            { "1'000.00" }
+sub dateformat              { 'dd.mm.yy' }
+sub timeformat              { 'hh:mm' }
+
+# default for login/admin areas
+sub country                 { 'CH' }
+sub language                { 'de' }
+
+1;
diff --git a/SL/Dev/ALL.pm b/SL/Dev/ALL.pm
new file mode 100644 (file)
index 0000000..8bf30a5
--- /dev/null
@@ -0,0 +1,55 @@
+package SL::Dev::ALL;
+
+use strict;
+
+use Exporter;
+use SL::Dev::Part;
+use SL::Dev::CustomerVendor;
+use SL::Dev::Inventory;
+use SL::Dev::Record;
+use SL::Dev::Payment;
+use SL::Dev::Shop;
+use SL::Dev::TimeRecording;
+
+sub import {
+  no strict "refs";
+  for (qw(Part CustomerVendor Inventory Record Payment Shop TimeRecording)) {
+    Exporter::export_to_level("SL::Dev::$_", 1, @_);
+  }
+}
+
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+SL::Dev::ALL: Dependency-only package for all SL::Dev::* modules
+
+=head1 SYNOPSIS
+
+  use SL::Dev::ALL;
+
+=head1 DESCRIPTION
+
+This module depends on all modules in SL/Dev/*.pm for the convenience of being
+able to write a simple C<use SL::Dev::ALL> and having everything loaded. This
+is supposed to be used only for test cases or in the kivitendo console. Normal
+modules should C<use> only the modules they actually need.
+
+To automatically include it in the console, add a line in the client section of
+the kivitendo.config, e.g.
+
+[console]
+autorun = require "bin/mozilla/common.pl";
+        = use SL::DB::Helper::ALL;
+        = use SL::Dev::ALL;
+
+=head1 AUTHOR
+
+G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Dev/CustomerVendor.pm b/SL/Dev/CustomerVendor.pm
new file mode 100644 (file)
index 0000000..849bbda
--- /dev/null
@@ -0,0 +1,115 @@
+package SL::Dev::CustomerVendor;
+
+use strict;
+use base qw(Exporter);
+our @EXPORT_OK = qw(new_customer new_vendor);
+our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
+
+use SL::DB::TaxZone;
+use SL::DB::Currency;
+use SL::DB::Customer;
+
+sub new_customer {
+  my (%params) = @_;
+
+  my $taxzone    = _check_taxzone(delete $params{taxzone_id});
+  my $currency   = _check_currency(delete $params{currency_id});
+
+  my $customer = SL::DB::Customer->new( name        => delete $params{name} || 'Testkunde',
+                                        currency_id => $currency->id,
+                                        taxzone_id  => $taxzone->id,
+                                      );
+  $customer->assign_attributes( %params );
+  return $customer;
+}
+
+sub new_vendor {
+  my (%params) = @_;
+
+  my $taxzone    = _check_taxzone(delete $params{taxzone_id});
+  my $currency   = _check_currency(delete $params{currency_id});
+
+  my $vendor = SL::DB::Vendor->new( name        => delete $params{name} || 'Testlieferant',
+                                    currency_id => $currency->id,
+                                    taxzone_id  => $taxzone->id,
+                                  );
+  $vendor->assign_attributes( %params );
+  return $vendor;
+}
+
+sub _check_taxzone {
+  my ($taxzone_id) = @_;
+  # check that taxzone_id exists or if no taxzone_id passed use 'Inland'
+  my $taxzone;
+  if ( $taxzone_id ) {
+    $taxzone = SL::DB::Manager::TaxZone->find_by( id => $taxzone_id ) || die "Can't find taxzone_id";
+  } else {
+    $taxzone = SL::DB::Manager::TaxZone->find_by( description => 'Inland') || die "No taxzone 'Inland'";
+  }
+  return $taxzone;
+}
+
+sub _check_currency {
+  my ($currency_id) = @_;
+  my $currency;
+  if ( $currency_id ) {
+    $currency = SL::DB::Manager::Currency->find_by( id => $currency_id ) || die "Can't find currency_id";
+  } else {
+    $currency = SL::DB::Manager::Currency->find_by( id => $::instance_conf->get_currency_id );
+  }
+  return $currency;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+SL::Dev::CustomerVendor - create customer and vendor objects for testing, with minimal defaults
+
+=head1 FUNCTIONS
+
+=head2 C<new_customer %PARAMS>
+
+Creates a new customer.
+
+Minimal usage, default values, without saving to database:
+
+  my $customer = SL::Dev::CustomerVendor::new_customer();
+
+Complex usage, overwriting some defaults, and save to database:
+
+  SL::Dev::CustomerVendor::new_customer(name        => 'Test customer',
+                                           hourly_rate => 50,
+                                           taxzone_id  => 2,
+                                          )->save;
+
+If neither taxzone_id or currency_id (both are NOT NULL) are passed as params
+then default values are used.
+
+=head2 C<new_vendor %PARAMS>
+
+Creates a new vendor.
+
+Minimal usage, default values, without saving to database:
+
+  my $vendor = SL::Dev::CustomerVendor::new_vendor();
+
+Complex usage, overwriting some defaults, and save to database:
+
+  SL::Dev::CustomerVendor::new_vendor(name        => 'Test vendor',
+                                      taxzone_id  => 2,
+                                      notes       => "Order for 100$ for free delivery",
+                                      payment_id  => 5,
+                                     )->save;
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Dev/File.pm b/SL/Dev/File.pm
new file mode 100644 (file)
index 0000000..9fa1b96
--- /dev/null
@@ -0,0 +1,74 @@
+package SL::Dev::File;
+
+use strict;
+use base qw(Exporter);
+our @EXPORT_OK = qw(create_scanned create_uploaded create_created);
+our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
+
+use SL::DB::File;
+
+sub create_scanned {
+  my (%params) = @_;
+  $params{source}    = 'scanner1';
+  $params{file_type} = 'document';
+  $params{file_path} = '/var/tmp/'.$params{file_name} if !$params{file_path};
+  open(OUT,"> ".$params{file_path});
+  print OUT $params{file_contents};
+  close(OUT);
+  delete $params{file_contents};
+  my $file = _create_file(%params);
+  unlink($params{file_path});
+  return $file;
+}
+
+sub create_uploaded {
+  my (%params) = @_;
+  $params{source}    = 'uploaded';
+  $params{file_type} = 'attachment';
+  return _create_file(%params);
+}
+
+sub create_created {
+  my (%params) = @_;
+  $params{source}    = 'created';
+  $params{file_type} = 'document';
+  return _create_file(%params);
+}
+
+sub _create_file {
+  my (%params) = @_;
+
+  my $fileobj = SL::File->save(
+    mime_type     => 'text/plain',
+    description   => 'Test File',
+    %params,
+    # file_type     => $params{file_type},
+    # source        => $params{source},
+    # file_name     => $params{file_name},
+    # file_contents => $params{file_contents},
+    # file_path     => $params{file_path}
+  );
+  return $fileobj;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+SL::Dev::File - create file objects for testing, with minimal defaults
+
+=head1 FUNCTIONS
+
+=head2 C<create_scanned %PARAMS>
+
+=head2 C<create_uploaded %PARAMS>
+
+=head2 C<create_created %PARAMS>
+
+=head1 AUTHOR
+
+Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
+
+=cut
diff --git a/SL/Dev/Inventory.pm b/SL/Dev/Inventory.pm
new file mode 100644 (file)
index 0000000..5789b49
--- /dev/null
@@ -0,0 +1,619 @@
+package SL::Dev::Inventory;
+
+use strict;
+use base qw(Exporter);
+our @EXPORT_OK = qw(
+  create_warehouse_and_bins set_stock transfer_stock
+  transfer_sales_delivery_order transfer_purchase_delivery_order
+  transfer_delivery_order_item transfer_in transfer_out
+);
+our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
+
+use SL::DB::Warehouse;
+use SL::DB::Bin;
+use SL::DB::Inventory;
+use SL::DB::TransferType;
+use SL::DB::Employee;
+use SL::DB::DeliveryOrderItemsStock;
+use SL::WH;
+use DateTime;
+use Data::Dumper;
+use Carp;
+
+sub create_warehouse_and_bins {
+  my (%params) = @_;
+
+  my $number_of_bins = $params{number_of_bins} || 5;
+  my $wh = SL::DB::Warehouse->new(description => $params{warehouse_description} || "Warehouse", invalid => 0);
+  for my $i ( 1 .. $number_of_bins ) {
+    $wh->add_bins( SL::DB::Bin->new(description => ( $params{bin_description} || "Bin" ) . " $i" ) );
+  }
+  $wh->save;
+  return ($wh, $wh->bins->[0]);
+}
+
+sub set_stock {
+  my (%params) = @_;
+
+  die "param part is missing or not an SL::DB::Part object"
+    unless ref($params{part}) eq 'SL::DB::Part';
+
+  my $part = delete $params{part};
+  die "qty is missing" unless $params{qty} or $params{abs_qty};
+  die "need a bin or default bin" unless $part->warehouse_id or $part->bin_id or $params{bin} or $params{bin_id};
+
+  my ($warehouse_id, $bin_id);
+
+  if ( $params{bin} ) {
+    die "illegal param bin: " . Dumper($params{bin}) unless ref($params{bin}) eq 'SL::DB::Bin';
+    my $bin       = delete $params{bin};
+    $bin_id       = $bin->id;
+    $warehouse_id = $bin->warehouse_id;
+  } elsif ( $params{bin_id} ) {
+    my $bin       = SL::DB::Manager::Bin->find_by(id => delete $params{bin_id});
+    $bin_id       = $bin->id;
+    $warehouse_id = $bin->warehouse_id;
+  } elsif ( $part->bin_id ) {
+    $bin_id       = $part->bin_id;
+    $warehouse_id = $part->warehouse_id;
+  } else {
+    die "can't determine bin and warehouse";
+  }
+
+  my $employee_id = delete $params{employee_id} // SL::DB::Manager::Employee->current->id;
+  die "Can't determine employee" unless $employee_id;
+
+  my $qty = delete $params{qty};
+
+  my $transfer_type_description;
+  my $transfer_type;
+  if ( $params{abs_qty} ) {
+    # determine the current qty and calculate the qty diff that needs to be applied
+    # if abs_qty is set then any value that was in $params{qty} is ignored/overwritten
+    my %get_stock_params;
+    $get_stock_params{bin_id}       = $bin_id       if $bin_id;
+    # $get_stock_params{warehouse_id} = $warehouse_id if $warehouse_id; # redundant
+    my $current_qty = $part->get_stock(%get_stock_params);
+    $qty = $params{abs_qty} - $current_qty;
+  }
+
+  if ( $qty > 0 ) {
+    $transfer_type_description = delete $params{transfer_type} // 'stock';
+    $transfer_type = SL::DB::Manager::TransferType->find_by( description => $transfer_type_description, direction => 'in' );
+  } else {
+    $transfer_type_description = delete $params{transfer_type} // 'shipped';
+    $transfer_type = SL::DB::Manager::TransferType->find_by( description => $transfer_type_description, direction => 'out' );
+  }
+  die "can't determine transfer_type" unless $transfer_type;
+
+  my $shippingdate;
+  if ( $params{shippingdate} ) {
+    $shippingdate = delete $params{shippingdate};
+    $shippingdate = $::locale->parse_date_to_object($shippingdate) unless ref($shippingdate) eq 'DateTime';
+  } else {
+    $shippingdate = DateTime->today;
+  }
+
+  my $unit;
+  if ( $params{unit} ) {
+    $unit = delete $params{unit};
+    $unit = SL::DB::Manager::Unit->find_by( name => $unit ) unless ref($unit) eq 'SL::DB::Unit';
+    $qty  = $unit->convert_to($qty, $part->unit_obj);
+  }
+
+  my ($trans_id) = $part->db->dbh->selectrow_array("select nextval('id')", {});
+
+  SL::DB::Inventory->new(
+    parts_id         => $part->id,
+    bin_id           => $bin_id,
+    warehouse_id     => $warehouse_id,
+    employee_id      => $employee_id,
+    trans_type_id    => $transfer_type->id,
+    comment          => $params{comment},
+    shippingdate     => $shippingdate,
+    qty              => $qty,
+    trans_id         => $trans_id,
+  )->save;
+}
+
+sub transfer_stock {
+  my (%params) = @_;
+
+  # check params:
+  die "missing params" unless ( $params{parts_id} or $params{part} ) and $params{from_bin} and $params{to_bin};
+
+  my $part;
+  if ( $params{parts_id} ) {
+    $part = SL::DB::Manager::Part->find_by( id => delete $params{parts_id} ) or die "illegal parts_id";
+  } else {
+    $part = delete $params{part};
+  }
+  die "illegal part" unless ref($part) eq 'SL::DB::Part';
+
+  my $from_bin = delete $params{from_bin};
+  my $to_bin   = delete $params{to_bin};
+  die "illegal bins" unless ref($from_bin) eq 'SL::DB::Bin' and ref($to_bin) eq 'SL::DB::Bin';
+
+  my $qty = delete($params{qty});
+  die "qty must be > 0" unless $qty > 0;
+
+  # set defaults
+  my $transfer_type = SL::DB::Manager::TransferType->find_by(description => 'transfer') or die "can't determine transfer type";
+  my $employee_id   = delete $params{employee_id} // SL::DB::Manager::Employee->current->id;
+
+  my $WH_params = {
+    'bestbefore'         => undef,
+    'change_default_bin' => undef,
+    'chargenumber'       => '',
+    'comment'            => delete $params{comment} // '',
+    'dst_bin_id'         => $to_bin->id,
+    'dst_warehouse_id'   => $to_bin->warehouse_id,
+    'parts_id'           => $part->id,
+    'qty'                => $qty,
+    'src_bin_id'         => $from_bin->id,
+    'src_warehouse_id'   => $from_bin->warehouse_id,
+    'transfer_type_id'   => $transfer_type->id,
+  };
+
+  WH->transfer($WH_params);
+
+  return 1;
+
+  # do it manually via rose:
+  # my $trans_id;
+
+  # my $db = SL::DB::Inventory->new->db;
+  # $db->with_transaction(sub{
+  #   ($trans_id) = $db->dbh->selectrow_array("select nextval('id')", {});
+  #   die "no trans_id" unless $trans_id;
+
+  #   my %params = (
+  #     shippingdate  => delete $params{shippingdate} // DateTime->today,
+  #     employee_id   => $employee_id,
+  #     trans_id      => $trans_id,
+  #     trans_type_id => $transfer_type->id,
+  #     parts_id      => $part->id,
+  #     comment       => delete $params{comment} || 'Umlagerung',
+  #   );
+
+  #   SL::DB::Inventory->new(
+  #     warehouse_id => $from_bin->warehouse_id,
+  #     bin_id       => $from_bin->id,
+  #     qty          => $qty * -1,
+  #     %params,
+  #   )->save;
+
+  #   SL::DB::Inventory->new(
+  #     warehouse_id => $to_bin->warehouse_id,
+  #     bin_id       => $to_bin->id,
+  #     qty          => $qty,
+  #     %params,
+  #   )->save;
+  # }) or die $@ . "\n";
+  # return 1;
+}
+
+sub _transfer {
+  my (%params) = @_;
+
+  my $transfer_type = delete $params{transfer_type};
+
+  die "param transfer_type is not a SL::DB::TransferType object: " . Dumper($transfer_type)
+    unless ref($transfer_type) eq 'SL::DB::TransferType';
+
+  my $shippingdate  = delete $params{shippingdate}  // DateTime->today;
+
+  my $part = delete($params{part}) or croak 'part missing';
+  my $qty  = delete($params{qty})  or croak 'qty missing';
+
+  # distinguish absolute qty in inventory depending on transfer type direction
+  $qty *= -1 if $transfer_type->direction eq 'out';
+
+  # use defaults for unit/wh/bin is they exist and nothing else is specified
+  my $unit = delete($params{unit}) // $part->unit      or croak 'unit missing';
+  my $bin  = delete($params{bin})  // $part->bin       or croak 'bin missing';
+  # if bin is given, we don't need a warehouse param
+  my $wh   = $bin->warehouse or croak 'wh missing';
+
+  WH->transfer({
+    parts_id         => $part->id,
+    dst_bin          => $bin,
+    dst_wh           => $wh,
+    qty              => $qty,
+    transfer_type    => $transfer_type,
+    unit             => $unit,
+    comment          => delete $params{comment},
+    shippingdate     => $shippingdate,
+  });
+}
+
+sub transfer_in {
+  my (%params) = @_;
+
+  my $transfer_type = delete $params{transfer_type} // 'stock';
+
+  my $transfer_type_obj = SL::DB::Manager::TransferType->find_by(
+    direction   => 'in',
+    description => $transfer_type,
+  ) or die "Can't find transfer_type with direction in and description " . $params{transfer_type};
+
+  $params{transfer_type} = $transfer_type_obj;
+
+  _transfer(%params);
+}
+
+sub transfer_out {
+  my (%params) = @_;
+
+  my $transfer_type = delete $params{transfer_type} // 'shipped';
+
+  my $transfer_type_obj = SL::DB::Manager::TransferType->find_by(
+    direction   => 'out',
+    description => $transfer_type,
+  ) or die "Can't find transfer_type with direction in and description " . $params{transfer_type};
+
+  $params{transfer_type} = $transfer_type_obj;
+
+  _transfer(%params);
+}
+
+sub transfer_sales_delivery_order {
+  my ($sales_delivery_order) = @_;
+  die "first argument must be a sales delivery order Rose DB object"
+    unless ref($sales_delivery_order) eq 'SL::DB::DeliveryOrder'
+           and $sales_delivery_order->is_sales;
+
+  die "the delivery order has already been delivered" if $sales_delivery_order->delivered;
+
+  my ($wh, $bin, $trans_type);
+
+  $sales_delivery_order->db->with_transaction(sub {
+
+   foreach my $doi ( @{ $sales_delivery_order->items } ) {
+     next if $doi->part->is_service or $doi->part->is_assortment;
+     my $trans_type = SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'shipped');
+     transfer_delivery_order_item($doi, $wh, $bin, $trans_type);
+   };
+   $sales_delivery_order->delivered(1);
+   $sales_delivery_order->save(changes_only=>1);
+   1;
+  }) or die "error while transferring sales_delivery_order: " . $sales_delivery_order->db->error;
+};
+
+sub transfer_purchase_delivery_order {
+  my ($purchase_delivery_order) = @_;
+  die "first argument must be a purchase delivery order Rose DB object"
+   unless ref($purchase_delivery_order) eq 'SL::DB::DeliveryOrder'
+          and not $purchase_delivery_order->is_sales;
+
+  my ($wh, $bin, $trans_type);
+
+  $purchase_delivery_order->db->with_transaction(sub {
+
+   foreach my $doi ( @{ $purchase_delivery_order->items } ) {
+     my $trans_type = SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'stock');
+     transfer_delivery_order_item($doi, $wh, $bin, $trans_type);
+   };
+   1;
+  }) or die "error while transferring purchase_Delivery_order: " . $purchase_delivery_order->db->error;
+};
+
+sub transfer_delivery_order_item {
+  my ($doi, $wh, $bin, $trans_type) = @_;
+
+  unless ( defined $trans_type and ref($trans_type eq 'SL::DB::TransferType') ) {
+    if ( $doi->record->is_sales ) {
+      $trans_type //=  SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'shipped');
+    } else {
+      $trans_type //= SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'stock');
+    }
+  }
+
+  $bin //= $doi->part->bin;
+  $wh  //= $doi->part->warehouse;
+
+  die "no bin and wh specified and part has no default bin or wh" unless $bin and $wh;
+
+  my $employee = SL::DB::Manager::Employee->current || die "No employee";
+
+  # dois are converted to base_qty, which is qty
+  # AM->convert_unit( 'g' => 'kg') * 1000;   # 1
+  #               $doi->unit   $doi->part->unit   $doi->qty
+  my $dois = SL::DB::DeliveryOrderItemsStock->new(
+    delivery_order_item => $doi,
+    qty                 => AM->convert_unit($doi->unit => $doi->part->unit) * $doi->qty,
+    unit                => $doi->part->unit,
+    warehouse_id        => $wh->id,
+    bin_id              => $bin->id,
+  )->save;
+
+  my $inventory = SL::DB::Inventory->new(
+    parts                      => $dois->delivery_order_item->part,
+    qty                        => $dois->delivery_order_item->record->is_sales ? $dois->qty * -1 : $dois->qty,
+    oe                         => $doi->record,
+    warehouse_id               => $dois->warehouse_id,
+    bin_id                     => $dois->bin_id,
+    trans_type_id              => $trans_type->id,
+    delivery_order_items_stock => $dois,
+    trans_id                   => $dois->id,
+    employee_id                => $employee->id,
+    shippingdate               => $doi->record->transdate,
+  )->save;
+};
+
+1;
+
+__END__
+
+=head1 NAME
+
+SL::Dev::Inventory - create inventory-related objects for testing, with minimal
+defaults
+
+=head1 FUNCTIONS
+
+=head2 C<create_warehouse_and_bins %PARAMS>
+
+Creates a new warehouse and bins, and immediately saves them. Returns the
+warehouse and the first bin object.
+
+  my ($wh, $bin) = SL::Dev::Inventory::create_warehouse_and_bins();
+
+Create named warehouse with 10 bins:
+
+  my ($wh, $bin) = SL::Dev::Inventory::create_warehouse_and_bins(
+    warehouse_description => 'Test warehouse',
+    bin_description       => 'Test bin',
+    number_of_bins        => 10,
+  );
+
+To access the second bin:
+
+  my $bin2 = $wh->bins->[1];
+
+=head2 C<set_stock %PARAMS>
+
+Change the stock level of a certain part by creating an inventory event.
+To access the updated onhand the part object needs to be loaded afterwards.
+
+Parameter:
+
+=over 4
+
+=item C<part>
+
+Mandatory. An SL::DB::Part object or a parts_id.
+
+=item C<qty>
+
+The qty to increase of decrease the stock level by.
+
+Exactly one of C<qty> and C<abs_qty> is mandatory.
+
+=item C<abs_qty>
+
+Sets stock level for a certain part to abs_qty by creating a stock event with
+the current difference.
+
+Exactly one of C<qty> and C<abs_qty> is mandatory.
+
+=item C<bin_id>
+
+=item C<bin>
+
+Optional. The bin for inventory entry.
+
+If no bin is passed the default bin of the part is used, if that doesn't exist
+either there will be an error.
+
+=item C<shippingdate>
+
+Optional. May be a DateTime object or a string that needs to be parsed by
+parse_date_to_object.
+
+=item C<unit>
+
+Optional. SL::DB::Unit object, or the name of an SL::DB::Unit object.
+
+=back
+
+C<set_stock> creates the SL::DB::Inventory object from scratch, rather
+than passing params to WH->transfer_in or WH->transfer_out.
+
+Examples:
+
+  my $part = SL::DB::Manager::Part->find_by(partnumber => '1');
+  SL::Dev::Inventory::set_stock(part => $part, abs_qty => 5);
+  SL::Dev::Inventory::set_stock(part => $part, qty => -2);
+  $part->load;
+  $part->onhand; # 3
+
+Set stock level of a part in a certain bin_id to 10:
+
+  SL::Dev::Inventory::set_stock(part => $part, bin_id => 99, abs_qty => 10);
+
+Create 10 warehouses with 5 bins each, then create 100 parts and increase the
+stock qty in a random bin by a random positive qty for each of the parts:
+
+  SL::Dev::Inventory::create_warehouse_and_bins(
+    warehouse_description => "Test Warehouse $_"
+  ) for 1 .. 10;
+  SL::Dev::Part::create_part(
+    description => "Test Part $_"
+  )->save for 1 .. 100;
+  my $bins = SL::DB::Manager::Bin->get_all;
+  SL::Dev::Inventory::set_stock(
+    part => $_,
+    qty  => int(rand(99))+1,
+    bin  => $bins->[ rand @{$bins} ],
+  ) for @{ SL::DB::Manager::Part->get_all };
+
+=head2 C<transfer_stock %PARAMS>
+
+Transfers parts from one bin to another.
+
+Parameters:
+
+=over 4
+
+=item C<part>
+
+=item C<part_id>
+
+Mandatory. An SL::DB::Part object or a parts_id.
+
+=item C<from_bin>
+
+=item C<to_bin>
+
+Mandatory. SL::DB::Bin objects.
+
+=item C<qty>
+
+Mandatory.
+
+=item C<shippingdate>
+
+Optional.
+
+=back
+
+The unit is always base_unit and there is no check for negative stock values.
+
+Example: Create a warehouse and bins, a part, stock the part and then move some
+of the stock to a different bin inside the same warehouse:
+
+  my ($wh, $bin) = SL::Dev::Inventory::create_warehouse_and_bins();
+  my $part = SL::Dev::Part::create_part->save;
+  SL::Dev::Inventory::set_stock(
+    part   => $part,
+    bin_id => $wh->bins->[2]->id,
+    qty    => 5,
+  );
+  SL::Dev::Inventory::transfer_stock(
+    part     => $part,
+    from_bin => $wh->bins->[2],
+    to_bin   => $wh->bins->[4],
+    qty      => 3,
+  );
+  $part->get_stock(bin_id => $wh->bins->[4]->id); # 3.00000
+  $part->get_stock(bin_id => $wh->bins->[2]->id); # 2.00000
+
+=head2 C<transfer_sales_delivery_order %PARAMS>
+
+Takes a SL::DB::DeliveryOrder object as its first argument and transfers out
+all the items via their default bin, creating the delivery_order_stock and
+inventory entries.
+
+Assumes a fresh delivery order where nothing has been transferred out yet.
+
+Should work like the functions in do.pl transfer_in/transfer_out and DO.pm
+transfer_in_out, except that those work on the current form where as this just
+works on database objects.
+
+As this is just Dev it doesn't check for negative stocks etc.
+
+Usage:
+
+  my $sales_delivery_order = SL::DB::Manager::DeliveryOrder->find_by(donumber => 112);
+  SL::Dev::Inventory::transfer_sales_delivery_order($sales_delivery_order1);
+
+=head2 C<transfer_purchase_delivery_order %PARAMS>
+
+Transfer in all the items in a purchase order.
+
+Behaves like C<transfer_sales_delivery_order>.
+
+=head2 C<transfer_delivery_order_item @PARAMS>
+
+Transfers a delivery order item from a delivery order. The whole qty is transferred.
+Doesn't check for available qty.
+
+Usage:
+
+  SL::Dev::Inventory::transfer_delivery_order_item($doi, $wh, $bin, $trans_type);
+
+=head2 C<transfer_in %PARAMS>
+
+Create stock in event for a part. Ideally the interface should mirror how data
+is entered via the web interface.
+
+Does some param checking, sets some defaults, but otherwise uses WH->transfer.
+
+Parameters:
+
+=over 4
+
+=item C<part>
+
+Mandatory. An SL::DB::Part object.
+
+=item C<qty>
+
+Mandatory.
+
+=item C<bin>
+
+Optional. An SL::DB::Bin object, defaults to $part->bin.
+
+=item C<wh>
+
+Optional. An SL::DB::Bin object, defaults to $part->warehouse.
+
+=item C<unit>
+
+Optional. A string such as 't', 'Stck', defaults to $part->unit->name.
+
+=item C<shippingdate>
+
+Optional. A DateTime object, defaults to today.
+
+=item C<transfer_type>
+
+Optional. A string such as 'correction', defaults to 'stock'.
+
+=item C<comment>
+
+Optional.
+
+=back
+
+Example minimal usage using part default warehouse and bin:
+
+  my ($wh, $bin) = SL::Dev::Inventory::create_warehouse_and_bins();
+  my $part       = SL::Dev::Part::create_part(
+    unit      => 'kg',
+    warehouse => $wh,
+    bin       => $bin,
+  )->save;
+  SL::Dev::Inventory::transfer_in(
+    part    => $part,
+    qty     => 0.9,
+    unit    => 't',
+    comment => '900 kg in t',
+  );
+
+Example with specific transfer_type and warehouse and bin and shipping_date:
+
+  my $shipping_date = DateTime->today->subtract( days => 20 );
+  SL::Dev::Inventory::transfer_in(
+    part          => $part,
+    qty           => 5,
+    transfer_type => 'correction',
+    bin           => $bin,
+    shipping_date => $shipping_date,
+  );
+
+=head2 C<transfer_out %PARAMS>
+
+Create stock out event for a part. See C<transfer_in>.
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Dev/Part.pm b/SL/Dev/Part.pm
new file mode 100644 (file)
index 0000000..967a1f7
--- /dev/null
@@ -0,0 +1,258 @@
+package SL::Dev::Part;
+
+use strict;
+use base qw(Exporter);
+our @EXPORT_OK = qw(new_part new_service new_assembly new_assortment);
+our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
+
+use SL::DB::Part;
+use SL::DB::Unit;
+use SL::DB::Buchungsgruppe;
+
+sub new_part {
+  my (%params) = @_;
+
+  my $part = SL::DB::Part->new_part(
+    description        => 'Test part',
+    sellprice          => '10',
+    lastcost           => '5',
+    buchungsgruppen_id => _default_buchungsgruppe()->id,
+    unit               => _default_unit()->name
+  );
+  $part->assign_attributes( %params );
+  return $part;
+}
+
+sub new_service {
+  my (%params) = @_;
+
+  my $part = SL::DB::Part->new_service(
+    description        => 'Test service',
+    sellprice          => '10',
+    lastcost           => '5',
+    buchungsgruppen_id => _default_buchungsgruppe()->id,
+    unit               => _default_unit()->name
+  );
+  $part->assign_attributes( %params );
+  return $part;
+}
+
+sub new_assembly {
+  my (%params) = @_;
+
+  my $assnumber       = delete $params{assnumber};
+  my $base_partnumber = delete $params{partnumber} || 'ap';
+
+  my $assembly_items = [];
+
+  if ( $params{assembly_items} ) {
+    $assembly_items = delete $params{assembly_items};
+  } else {
+    for my $i ( 1 .. delete $params{number_of_parts} || 3) {
+      my $part = new_part(partnumber  => "$base_partnumber $i",
+                          description => "Testpart $i",
+                         )->save;
+      push( @{$assembly_items}, SL::DB::Assembly->new(parts_id => $part->id,
+                                                      qty      => 1,
+                                                      position => $i,
+                                                     ));
+    }
+  }
+
+  my $assembly = SL::DB::Part->new_assembly(
+    partnumber         => $assnumber,
+    description        => 'Test Assembly',
+    sellprice          => '10',
+    lastcost           => '5',
+    assemblies         => $assembly_items,
+    buchungsgruppen_id => _default_buchungsgruppe()->id,
+    unit               => _default_unit()->name
+  );
+  $assembly->assign_attributes( %params );
+  return $assembly;
+}
+
+sub new_assortment {
+  my (%params) = @_;
+
+  my $assnumber       = delete $params{assnumber};
+  my $base_partnumber = delete $params{partnumber} || 'ap';
+
+  my $assortment_items = [];
+
+  if ( $params{assortment_items} ) {
+    $assortment_items = delete $params{assortment_items};
+  } else {
+    for my $i ( 1 .. delete $params{number_of_parts} || 3) {
+      my $part = new_part(partnumber  => "$base_partnumber $i",
+                          description => "Testpart $i",
+                         )->save;
+      push( @{$assortment_items}, SL::DB::AssortmentItem->new(parts_id => $part->id,
+                                                              qty      => 1,
+                                                              position => $i,
+                                                              unit     => $part->unit,
+                                                             ));
+    }
+  }
+
+  my $assortment = SL::DB::Part->new_assortment(
+    partnumber         => $assnumber,
+    description        => 'Test Assortment',
+    sellprice          => '10',
+    lastcost           => '5',
+    assortment_items   => $assortment_items,
+    buchungsgruppen_id => _default_buchungsgruppe()->id,
+    unit               => _default_unit()->name
+  );
+
+  $assortment->assign_attributes( %params );
+  return $assortment;
+}
+
+
+sub _default_buchungsgruppe {
+  return SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%') || die "No accounting group";
+}
+
+sub _default_unit {
+  return SL::DB::Manager::Unit->find_by(name => 'Stck') || die "No unit";
+}
+
+
+1;
+
+__END__
+
+=head1 NAME
+
+SL::Dev::Part - create part objects for testing, with minimal defaults
+
+=head1 SYNOPSIS
+
+  use SL::Dev::Part qw(new_part new_assembly new_service new_assortment);
+
+  # simple default objects
+  my $part     = new_part()->save;
+  my $assembly = new_assembly()->save;
+  my $service  = new_service()->save;
+  my $assortment = new_assortment()->save;
+
+  # pass additional params to the generated object
+  # see individual functions for special parameters
+  my $part     = new_part(
+    partnumber   => 'Test 001',
+    warehouse_id => $bin->warehouse->id,
+    bin_id       => $bin->id,
+  );
+
+=head1 FUNCTIONS
+
+=head2 C<new_part %PARAMS>
+
+Creates a new part (part_type = part).
+
+=head2 C<new_service %PARAMS>
+
+Creates a new service (part_type = service).
+
+=head2 C<new_assembly %PARAMS>
+
+Create a new assembly (part_type = assembly).
+
+Special params:
+
+=over 2
+
+=item * C<number_of_parts>
+
+The number of automatically created assembly parts.
+
+=item * C<assnumber>
+
+the partnumber of the assembly
+
+=item * C<partnumber>
+
+the partnumber of the first assembly part to be created
+
+=back
+
+By default 3 parts (p1, p2, p3) are created and saved as an assembly (as1).
+
+Create a new assembly with 10 parts, the assembly gets partnumber 'Ass1' and the
+parts get partnumbers 'Testpart 1' to 'Testpart 10':
+
+  my $assembly = SL::Dev::Part::new_assembly(
+    number_of_parts => 10,
+    partnumber      => 'Testpart',
+    assnumber       => 'Ass1'
+  )->save;
+
+Create an assembly with specific parts:
+
+  my $assembly_item_1 = SL::DB::Assembly->new( parts_id => $part1->id, qty => 3, position => 1);
+  my $assembly_item_2 = SL::DB::Assembly->new( parts_id => $part2->id, qty => 3, position => 2);
+  my $assembly_part   = new_assembly(
+    assnumber      => 'Assembly 1',
+    description    => 'Assembly test',
+    sellprice      => $part1->sellprice + $part2->sellprice,
+    assembly_items => [ $assembly_item_1, $assembly_item_2 ],
+  );
+
+=head2 C<new_assortment %PARAMS>
+
+Create a new assortment (part_type = assortment).
+
+Special params:
+
+=over 2
+
+=item * C<number_of_parts>
+
+The number of automatically created assembly parts.
+
+=item * C<assnumber>
+
+the partnumber of the assortment
+
+=item * C<partnumber>
+
+the partnumber of the first assembly part to be created
+
+=back
+
+By default 3 parts (p1, p2, p3) are created and saved as an assortment.
+
+Create a new assortment with 10 automatically created parts using the
+number_of_parts param:
+
+  my $assortment = new_assortment(number_of_parts => 10)->save;
+
+Create an assortment with a certain name and pass some assortment_item Objects
+from newly created parts:
+
+  my $part1             = new_part(sellprice => 7.77)->save;
+  my $part2             = new_part(sellprice => 6.66)->save;
+  my $assortment_item_1 = SL::DB::AssortmentItem->new( parts_id => $part1->id, qty => 3, unit => $part1->unit, position => 1);
+  my $assortment_item_2 = SL::DB::AssortmentItem->new( parts_id => $part2->id, qty => 3, unit => $part2->unit, position => 2);
+  my $assortment_part   = SL::Dev::Part::new_assortment(
+    assnumber        => 'Assortment 1',
+    description      => 'assortment test',
+    sellprice        => (3*$part1->sellprice + 3*$part2->sellprice),
+    lastcost         => (3*$part1->lastcost  + 3*$part2->lastcost),
+    assortment_items => [ $assortment_item_1, $assortment_item_2 ],
+  )->save;
+
+=head1 TODO
+
+Nothing here yet.
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Dev/Payment.pm b/SL/Dev/Payment.pm
new file mode 100644 (file)
index 0000000..a093ed4
--- /dev/null
@@ -0,0 +1,185 @@
+package SL::Dev::Payment;
+
+use strict;
+use base qw(Exporter);
+our @EXPORT_OK = qw(create_payment_terms create_bank_account create_bank_transaction create_sepa_export create_sepa_export_item);
+our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
+
+use SL::DB::PaymentTerm;
+use SL::DB::BankAccount;
+use SL::DB::Chart;
+use DateTime;
+
+sub create_payment_terms {
+  my (%params) = @_;
+
+  my $payment_terms =  SL::DB::PaymentTerm->new(
+    description      => 'payment',
+    description_long => 'payment',
+    terms_netto      => '30',
+    terms_skonto     => '5',
+    percent_skonto   => '0.05',
+    auto_calculation => 1,
+  );
+  $payment_terms->assign_attributes(%params) if %params;
+  $payment_terms->save;
+}
+
+sub create_bank_account {
+  my (%params) = @_;
+  my $bank_account = SL::DB::BankAccount->new(
+    iban           => 'DE12500105170648489890',
+    account_number => '0648489890',
+    bank           => 'Testbank',
+    chart_id       => delete $params{chart_id} // $::instance_conf->get_ar_paid_accno_id,
+    name           => 'Test bank account',
+    bic            => 'BANK1234',
+    bank_code      => '50010517'
+  );
+  $bank_account->assign_attributes(%params) if %params;
+  $bank_account->save;
+}
+
+sub create_sepa_export {
+  my (%params) = @_;
+  my $sepa_export = SL::DB::SepaExport->new(
+    closed       => 0,
+    employee_id  => $params{employee_id} // SL::DB::Manager::Employee->current->id,
+    executed     => 0,
+    vc           => 'customer',
+  );
+  $sepa_export->assign_attributes(%params) if %params;
+  $sepa_export->save;
+}
+
+sub create_sepa_export_item {
+  my (%params) = @_;
+  my $sepa_exportitem = SL::DB::SepaExportItem->new(
+    chart_id     => delete $params{chart_id} // $::instance_conf->get_ar_paid_accno_id,
+    payment_type => 'without_skonto',
+    our_bic      => 'BANK1234',
+    our_iban     => 'DE12500105170648489890',
+  );
+  $sepa_exportitem->assign_attributes(%params) if %params;
+  $sepa_exportitem->save;
+}
+
+sub create_bank_transaction {
+ my (%params) = @_;
+
+ my $record = delete $params{record};
+ die "bank_transactions can only be created for invoices" unless ref($record) eq 'SL::DB::Invoice' or ref($record) eq 'SL::DB::PurchaseInvoice';
+
+ my $multiplier = $record->is_sales ? 1 : -1;
+ my $amount = (delete $params{amount} || $record->amount) * $multiplier;
+
+ my $bank_chart;
+ if ( $params{bank_chart_id} ) {
+   $bank_chart = SL::DB::Manager::Chart->find_by(id => delete $params{bank_chart_id}) or die "Can't find bank chart";
+ } elsif ( $::instance_conf->get_ar_paid_accno_id ) {
+   $bank_chart   = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_ar_paid_accno_id);
+ } else {
+   $bank_chart = SL::DB::Manager::Chart->find_by(description => 'Bank') or die "Can't find bank chart";
+ }
+ my $bank_account = SL::DB::Manager::BankAccount->find_by( chart_id => $bank_chart->id );
+ die "bank account missing" unless $bank_account;
+
+ my $bt = SL::DB::BankTransaction->new(
+   local_bank_account_id => $bank_account->id,
+   remote_bank_code      => $record->customervendor->bank_code,
+   remote_account_number => $record->customervendor->account_number,
+   transdate             => DateTime->today,
+   valutadate            => DateTime->today,
+   amount                => $amount,
+   currency              => $record->currency->id,
+   remote_name           => $record->customervendor->depositor,
+   purpose               => $record->invnumber
+ );
+ $bt->assign_attributes(%params) if %params;
+ $bt->save;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+SL::Dev::Payment - create objects for payment-related testing, with minimal defaults
+
+=head1 FUNCTIONS
+
+=head2 C<create_payment_terms %PARAMS>
+
+Create payment terms.
+
+Minimal example with default values (30days, 5% skonto within 5 days):
+  my $payment_terms = SL::Dev::Payment::create_payment_terms;
+
+=head2 C<create_bank_account %PARAMS>
+
+Required params: chart_id
+
+Example:
+  my $bank_account = SL::Dev::Payment::create_bank_account(chart_id => SL::DB::Manager::Chart->find_by(description => 'Bank')->id);
+
+=head2 C<create_bank_transaction %PARAMS>
+
+Create a bank transaction that matches an existing invoice record, e.g. to be able to
+test the point system.
+
+Required params: record  (an SL::DB::Invoice or SL::DB::PurchaseInvoice object)
+
+Optional params: bank_chart_id : the chart id of a configured bank account
+                 amount        : the amount of the bank transaction
+
+If no bank_chart_id is given, it tries to find a chart via defaults
+(ar_paid_accno_id) or by searching for the chart named "Bank". The chart must
+be connected to an existing BankAccount.
+
+Param amount should always be relative to the absolute amount of the invoice, i.e. use positive
+values for sales and purchases.
+
+Example:
+  my $payment_terms = SL::Dev::Payment::create_payment_terms;
+  my $bank_chart    = SL::DB::Manager::Chart->find_by(description => 'Bank');
+  my $bank_account  = SL::Dev::Payment::create_bank_account(chart_id => $bank_chart->id);
+  my $customer      = SL::Dev::CustomerVendor::create_customer(iban           => 'DE12500105170648489890',
+                                                               bank_code      => 'abc',
+                                                               account_number => '44444',
+                                                               bank           => 'Testbank',
+                                                               bic            => 'foobar',
+                                                               depositor      => 'Name')->save;
+  my $sales_invoice = SL::Dev::Record::create_sales_invoice(customer      => $customer,
+                                                            payment_terms => $payment_terms,
+                                                           );
+  my $bt            = SL::Dev::Payment::create_bank_transaction(record        => $sales_invoice,
+                                                                amount        => $sales_invoice->amount_less_skonto,
+                                                                transdate     => DateTime->today->add(days => 10),
+                                                                bank_chart_id => $bank_chart->id
+                                                               );
+  my ($agreement, $rule_matches) = $bt->get_agreement_with_invoice($sales_invoice);
+  # 14, 'remote_account_number(3) skonto_exact_amount(5) cust_vend_number_in_purpose(1) depositor_matches(2) payment_within_30_days(1) datebonus14(2)'
+
+To create a payment for 3 invoices that were all paid together, all with skonto:
+  my $ar1 = SL::DB::Manager::Invoice->find_by(invnumber=>'20');
+  my $ar2 = SL::DB::Manager::Invoice->find_by(invnumber=>'21');
+  my $ar3 = SL::DB::Manager::Invoice->find_by(invnumber=>'22');
+  SL::Dev::Payment::create_bank_transaction(record  => $ar1
+                                            amount  => ($ar1->amount_less_skonto + $ar2->amount_less_skonto + $ar2->amount_less_skonto),
+                                            purpose => 'Rechnungen 20, 21, 22',
+                                           );
+
+=head1 TODO
+
+Nothing here yet.
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Dev/Record.pm b/SL/Dev/Record.pm
new file mode 100644 (file)
index 0000000..73f06d0
--- /dev/null
@@ -0,0 +1,1013 @@
+package SL::Dev::Record;
+
+use strict;
+use base qw(Exporter);
+our @EXPORT_OK = qw(create_invoice_item
+                    create_sales_invoice
+                    create_credit_note
+                    create_order_item
+                    create_sales_order
+                    create_purchase_order
+                    create_delivery_order_item
+                    create_sales_delivery_order
+                    create_purchase_delivery_order
+                    create_project create_department
+                    create_ap_transaction
+                    create_ar_transaction
+                    create_gl_transaction
+                   );
+our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
+
+use SL::DB::Invoice;
+use SL::DB::InvoiceItem;
+use SL::DB::DeliveryOrder::TypeData qw(:types);
+use SL::DB::Employee;
+use SL::Dev::Part qw(new_part);
+use SL::Dev::CustomerVendor qw(new_vendor new_customer);
+use SL::DB::Project;
+use SL::DB::ProjectStatus;
+use SL::DB::ProjectType;
+use SL::Form;
+use DateTime;
+use List::Util qw(sum);
+use Data::Dumper;
+use SL::Locale::String qw(t8);
+use SL::DATEV;
+
+my %record_type_to_item_type = ( sales_invoice        => 'SL::DB::InvoiceItem',
+                                 credit_note          => 'SL::DB::InvoiceItem',
+                                 sales_order          => 'SL::DB::OrderItem',
+                                 purchase_order       => 'SL::DB::OrderItem',
+                                 sales_delivery_order => 'SL::DB::DeliveryOrderItem',
+                               );
+
+sub create_sales_invoice {
+  my (%params) = @_;
+
+  my $record_type = 'sales_invoice';
+  my $invoiceitems = delete $params{invoiceitems} // _create_two_items($record_type);
+  _check_items($invoiceitems, $record_type);
+
+  my $customer = delete $params{customer} // new_customer(name => 'Testcustomer')->save;
+  die "illegal customer" unless defined $customer && ref($customer) eq 'SL::DB::Customer';
+
+  my $invoice = SL::DB::Invoice->new(
+    invoice      => 1,
+    type         => 'invoice',
+    customer_id  => $customer->id,
+    taxzone_id   => $customer->taxzone->id,
+    invnumber    => delete $params{invnumber}   // undef,
+    currency_id  => $params{currency_id} // $::instance_conf->get_currency_id,
+    taxincluded  => $params{taxincluded} // 0,
+    employee_id  => $params{employee_id} // SL::DB::Manager::Employee->current->id,
+    salesman_id  => $params{employee_id} // SL::DB::Manager::Employee->current->id,
+    transdate    => $params{transdate}   // DateTime->today_local->to_kivitendo,
+    payment_id   => $params{payment_id}  // undef,
+    gldate       => DateTime->today,
+    invoiceitems => $invoiceitems,
+  );
+  $invoice->assign_attributes(%params) if %params;
+
+  $invoice->post;
+  return $invoice;
+}
+
+sub create_credit_note {
+  my (%params) = @_;
+
+  my $record_type = 'credit_note';
+  my $invoiceitems = delete $params{invoiceitems} // _create_two_items($record_type);
+  _check_items($invoiceitems, $record_type);
+
+  my $customer = delete $params{customer} // new_customer(name => 'Testcustomer')->save;
+  die "illegal customer" unless defined $customer && ref($customer) eq 'SL::DB::Customer';
+
+  # adjust qty for credit note items
+  $_->qty( $_->qty * -1) foreach @{$invoiceitems};
+
+  my $invoice = SL::DB::Invoice->new(
+    invoice      => 1,
+    type         => 'credit_note',
+    customer_id  => $customer->id,
+    taxzone_id   => $customer->taxzone->id,
+    invnumber    => delete $params{invnumber}   // undef,
+    currency_id  => $params{currency_id} // $::instance_conf->get_currency_id,
+    taxincluded  => $params{taxincluded} // 0,
+    employee_id  => $params{employee_id} // SL::DB::Manager::Employee->current->id,
+    salesman_id  => $params{employee_id} // SL::DB::Manager::Employee->current->id,
+    transdate    => $params{transdate}   // DateTime->today_local->to_kivitendo,
+    payment_id   => $params{payment_id}  // undef,
+    gldate       => DateTime->today,
+    invoiceitems => $invoiceitems,
+  );
+  $invoice->assign_attributes(%params) if %params;
+
+  $invoice->post;
+  return $invoice;
+}
+
+sub create_sales_delivery_order {
+  my (%params) = @_;
+
+  my $record_type = 'sales_delivery_order';
+  my $orderitems = delete $params{orderitems} // _create_two_items($record_type);
+  _check_items($orderitems, $record_type);
+
+  my $customer = $params{customer} // new_customer(name => 'Testcustomer')->save;
+  die "illegal customer" unless ref($customer) eq 'SL::DB::Customer';
+
+  my $delivery_order = SL::DB::DeliveryOrder->new(
+    order_type   => SALES_DELIVERY_ORDER_TYPE,
+    'closed'     => undef,
+    customer_id  => $customer->id,
+    taxzone_id   => $customer->taxzone_id,
+    donumber     => $params{donumber}    // undef,
+    currency_id  => $params{currency_id} // $::instance_conf->get_currency_id,
+    taxincluded  => $params{taxincluded} // 0,
+    employee_id  => $params{employee_id} // SL::DB::Manager::Employee->current->id,
+    salesman_id  => $params{employee_id} // SL::DB::Manager::Employee->current->id,
+    transdate    => $params{transdate}   // DateTime->today,
+    orderitems   => $orderitems,
+  );
+  $delivery_order->assign_attributes(%params) if %params;
+  $delivery_order->save;
+  return $delivery_order;
+}
+
+sub create_purchase_delivery_order {
+  my (%params) = @_;
+
+  my $record_type = 'purchase_delivery_order';
+  my $orderitems = delete $params{orderitems} // _create_two_items($record_type);
+  _check_items($orderitems, $record_type);
+
+  my $vendor = $params{vendor} // new_vendor(name => 'Testvendor')->save;
+  die "illegal customer" unless ref($vendor) eq 'SL::DB::Vendor';
+
+  my $delivery_order = SL::DB::DeliveryOrder->new(
+    order_type   => PURCHASE_DELIVERY_ORDER_TYPE,
+    'closed'     => undef,
+    vendor_id    => $vendor->id,
+    taxzone_id   => $vendor->taxzone_id,
+    donumber     => $params{donumber}    // undef,
+    currency_id  => $params{currency_id} // $::instance_conf->get_currency_id,
+    taxincluded  => $params{taxincluded} // 0,
+    employee_id  => $params{employee_id} // SL::DB::Manager::Employee->current->id,
+    salesman_id  => $params{employee_id} // SL::DB::Manager::Employee->current->id,
+    transdate    => $params{transdate}   // DateTime->today,
+    orderitems   => $orderitems,
+  );
+  $delivery_order->assign_attributes(%params) if %params;
+  $delivery_order->save;
+  return $delivery_order;
+}
+
+sub create_sales_order {
+  my (%params) = @_;
+
+  my $record_type = 'sales_order';
+  my $orderitems = delete $params{orderitems} // _create_two_items($record_type);
+  _check_items($orderitems, $record_type);
+
+  my $save = delete $params{save} // 0;
+
+  my $customer = $params{customer} // new_customer(name => 'Testcustomer')->save;
+  die "illegal customer" unless ref($customer) eq 'SL::DB::Customer';
+
+  my $order = SL::DB::Order->new(
+    customer_id  => delete $params{customer_id} // $customer->id,
+    taxzone_id   => delete $params{taxzone_id}  // $customer->taxzone->id,
+    currency_id  => delete $params{currency_id} // $::instance_conf->get_currency_id,
+    taxincluded  => delete $params{taxincluded} // 0,
+    employee_id  => delete $params{employee_id} // SL::DB::Manager::Employee->current->id,
+    salesman_id  => delete $params{employee_id} // SL::DB::Manager::Employee->current->id,
+    transdate    => delete $params{transdate}   // DateTime->today,
+    orderitems   => $orderitems,
+  );
+  $order->assign_attributes(%params) if %params;
+
+  if ( $save ) {
+    $order->calculate_prices_and_taxes;
+    $order->save;
+  }
+  return $order;
+}
+
+sub create_purchase_order {
+  my (%params) = @_;
+
+  my $record_type = 'purchase_order';
+  my $orderitems = delete $params{orderitems} // _create_two_items($record_type);
+  _check_items($orderitems, $record_type);
+
+  my $save = delete $params{save} // 0;
+
+  my $vendor = $params{vendor} // new_vendor(name => 'Testvendor')->save;
+  die "illegal vendor" unless ref($vendor) eq 'SL::DB::Vendor';
+
+  my $order = SL::DB::Order->new(
+    vendor_id    => delete $params{vendor_id}   // $vendor->id,
+    taxzone_id   => delete $params{taxzone_id}  // $vendor->taxzone->id,
+    currency_id  => delete $params{currency_id} // $::instance_conf->get_currency_id,
+    taxincluded  => delete $params{taxincluded} // 0,
+    transdate    => delete $params{transdate}   // DateTime->today,
+    'closed'     => undef,
+    orderitems   => $orderitems,
+  );
+  $order->assign_attributes(%params) if %params;
+
+  if ( $save ) {
+    $order->calculate_prices_and_taxes; # not tested for purchase orders
+    $order->save;
+  }
+  return $order;
+};
+
+sub _check_items {
+  my ($items, $record_type) = @_;
+
+  if  ( scalar @{$items} == 0 or grep { ref($_) ne $record_type_to_item_type{"$record_type"} } @{$items} ) {
+    die "Error: items must be an arrayref of " . $record_type_to_item_type{"$record_type"} . "objects.";
+  }
+}
+
+sub create_invoice_item {
+  my (%params) = @_;
+
+  return _create_item(record_type => 'sales_invoice', %params);
+}
+
+sub create_order_item {
+  my (%params) = @_;
+
+  return _create_item(record_type => 'sales_order', %params);
+}
+
+sub create_delivery_order_item {
+  my (%params) = @_;
+
+  return _create_item(record_type => 'sales_delivery_order', %params);
+}
+
+sub _create_item {
+  my (%params) = @_;
+
+  my $record_type = delete($params{record_type});
+  my $part        = delete($params{part});
+
+  die "illegal record type: $record_type, must be one of: " . join(' ', keys %record_type_to_item_type) unless $record_type_to_item_type{ $record_type };
+  die "part missing as param" unless $part && ref($part) eq 'SL::DB::Part';
+
+  my ($sellprice, $lastcost);
+
+  if ( $record_type =~ /^sales/ ) {
+    $sellprice = delete $params{sellprice} // $part->sellprice;
+    $lastcost  = delete $params{lastcost}  // $part->lastcost;
+  } else {
+    $sellprice = delete $params{sellprice} // $part->lastcost;
+    $lastcost  = delete $params{lastcost}  // 0; # $part->lastcost;
+  }
+
+  my $item = "$record_type_to_item_type{$record_type}"->new(
+    parts_id    => $part->id,
+    sellprice   => $sellprice,
+    lastcost    => $lastcost,
+    description => $part->description,
+    unit        => $part->unit,
+    qty         => $params{qty} || 5,
+  );
+  $item->assign_attributes(%params) if %params;
+  return $item;
+}
+
+sub _create_two_items {
+  my ($record_type) = @_;
+
+  my $part1 = new_part(description => 'Testpart 1',
+                       sellprice   => 12,
+                      )->save;
+  my $part2 = new_part(description => 'Testpart 2',
+                       sellprice   => 10,
+                      )->save;
+  my $item1 = _create_item(record_type => $record_type, part => $part1, qty => 5);
+  my $item2 = _create_item(record_type => $record_type, part => $part2, qty => 8);
+  return [ $item1, $item2 ];
+}
+
+sub create_project {
+  my (%params) = @_;
+  my $project = SL::DB::Project->new(
+    projectnumber     => delete $params{projectnumber} // 1,
+    description       => delete $params{description} // "Test project",
+    active            => 1,
+    valid             => 1,
+    project_status_id => SL::DB::Manager::ProjectStatus->find_by(name => "running")->id,
+    project_type_id   => SL::DB::Manager::ProjectType->find_by(description => "Standard")->id,
+  )->save;
+  $project->assign_attributes(%params) if %params;
+  return $project;
+}
+
+sub create_department {
+  my (%params) = @_;
+
+  my $department = SL::DB::Department->new(
+    'description' => delete $params{description} // 'Test Department',
+  )->save;
+
+  $department->assign_attributes(%params) if %params;
+  return $department;
+
+}
+
+sub create_ap_transaction {
+  my (%params) = @_;
+
+  my $vendor = delete $params{vendor};
+  if ( $vendor ) {
+    die "vendor missing or not a SL::DB::Vendor object" unless ref($vendor) eq 'SL::DB::Vendor';
+  } else {
+    # use default SL/Dev vendor if it exists, or create a new one
+    $vendor = SL::DB::Manager::Vendor->find_by(name => 'Testlieferant') // new_vendor->save;
+  };
+
+  my $taxincluded = $params{taxincluded} // 1;
+  delete $params{taxincluded};
+
+  my $bookings    = delete $params{bookings};
+  # default bookings
+  unless ( $bookings ) {
+    my $chart_postage   = SL::DB::Manager::Chart->find_by(description => 'Porto');
+    my $chart_telephone = SL::DB::Manager::Chart->find_by(description => 'Telefon');
+    $bookings = [
+                  {
+                    chart  => $chart_postage,
+                    amount => 1000,
+                  },
+                  {
+                    chart  => $chart_telephone,
+                    amount => $taxincluded ? 1190 : 1000,
+                  },
+                ]
+  };
+
+  # optional params:
+  my $project_id         = delete $params{globalproject_id};
+
+  # if amount or netamount are given, then it compares them to the final values, and dies if they don't match
+  my $expected_amount    = delete $params{amount};
+  my $expected_netamount = delete $params{netamount};
+
+  my $dec = delete $params{dec} // 2;
+
+  my $today      = DateTime->today_local;
+  my $transdate  = delete $params{transdate} // $today;
+  die "transdate hat to be DateTime object" unless ref($transdate) eq 'DateTime';
+
+  my $gldate     = delete $params{gldate} // $today;
+  die "gldate hat to be DateTime object" unless ref($gldate) eq 'DateTime';
+
+  my $ap_chart = delete $params{ap_chart} // SL::DB::Manager::Chart->find_by( accno => '1600' );
+  die "no ap_chart found or not an AP chart" unless $ap_chart and $ap_chart->link eq 'AP';
+
+  my $ap_transaction = SL::DB::PurchaseInvoice->new(
+    vendor_id        => $vendor->id,
+    invoice          => 0,
+    transactions     => [],
+    globalproject_id => $project_id,
+    invnumber        => delete $params{invnumber} // 'test ap_transaction',
+    notes            => delete $params{notes}     // 'test ap_transaction',
+    transdate        => $transdate,
+    gldate           => $gldate,
+    taxincluded      => $taxincluded,
+    taxzone_id       => $vendor->taxzone_id, # taxzone_id shouldn't have any effect on ap transactions
+    currency_id      => $::instance_conf->get_currency_id,
+    type             => undef, # isn't set for ap
+    employee_id      => SL::DB::Manager::Employee->current->id,
+  );
+  # assign any parameters that weren't explicitly handled above, e.g. itime
+  $ap_transaction->assign_attributes(%params) if %params;
+
+  foreach my $booking ( @{$bookings} ) {
+    my $chart = delete $booking->{chart};
+    die "illegal chart" unless ref($chart) eq 'SL::DB::Chart';
+
+    my $tax = _transaction_tax_helper($booking, $chart, $transdate); # will die if tax can't be found
+
+    $ap_transaction->add_ap_amount_row(
+      amount     => $booking->{amount}, # add_ap_amount_row expects the user input amount, does its own calculate_tax
+      chart      => $chart,
+      tax_id     => $tax->id,
+      project_id => $booking->{project_id},
+    );
+  }
+
+  my $acc_trans_sum = sum map { $_->amount  } grep { $_->chart_link =~ 'AP_amount' } @{$ap_transaction->transactions};
+  # $main::lxdebug->message(0, sprintf("accno: %s    amount: %s   chart_link: %s\n",
+  #                                    $_->amount,
+  #                                    $_->chart->accno,
+  #                                    $_->chart_link
+  #                                   )) foreach @{$ap_transaction->transactions};
+
+  # determine netamount and amount from the transactions that were added via bookings
+  $ap_transaction->netamount( -1 * sum map { $_->amount  } grep { $_->chart_link =~ 'AP_amount' } @{$ap_transaction->transactions} );
+  # $main::lxdebug->message(0, sprintf('found netamount %s', $ap_transaction->netamount));
+
+  my $taxamount = -1 * sum map { $_->amount  } grep { $_->chart_link =~ /tax/ } @{$ap_transaction->transactions};
+  $ap_transaction->amount( $ap_transaction->netamount + $taxamount );
+  # additional check, add up all transactions before AP-transaction is added
+  my $refamount = -1 * sum map { $_->amount  } @{$ap_transaction->transactions};
+  die "refamount = $refamount, ap_transaction->amount = " . $ap_transaction->amount unless $refamount == $ap_transaction->amount;
+
+  # if amount or netamount were passed as params, check if the values are still
+  # the same after recalculating them from the acc_trans entries
+  if (defined $expected_amount) {
+    die "amount doesn't match acc_trans amounts: $expected_amount != " . $ap_transaction->amount unless $expected_amount == $ap_transaction->amount;
+  }
+  if (defined $expected_netamount) {
+    die "netamount doesn't match acc_trans netamounts: $expected_netamount != " . $ap_transaction->netamount unless $expected_netamount == $ap_transaction->netamount;
+  }
+
+  $ap_transaction->create_ap_row(chart => $ap_chart);
+  $ap_transaction->save;
+  # $main::lxdebug->message(0, sprintf("created ap_transaction with invnumber %s and trans_id %s",
+  #                                     $ap_transaction->invnumber,
+  #                                     $ap_transaction->id));
+  return $ap_transaction;
+}
+
+sub create_ar_transaction {
+  my (%params) = @_;
+
+  my $customer = delete $params{customer};
+  if ( $customer ) {
+    die "customer missing or not a SL::DB::Customer object" unless ref($customer) eq 'SL::DB::Customer';
+  } else {
+    # use default SL/Dev vendor if it exists, or create a new one
+    $customer = SL::DB::Manager::Customer->find_by(name => 'Testkunde') // new_customer->save;
+  };
+
+  my $taxincluded = $params{taxincluded} // 1;
+  delete $params{taxincluded};
+
+  my $bookings    = delete $params{bookings};
+  # default bookings
+  unless ( $bookings ) {
+    my $chart_19 = SL::DB::Manager::Chart->find_by(accno => '8400');
+    my $chart_7  = SL::DB::Manager::Chart->find_by(accno => '8300');
+    my $chart_0  = SL::DB::Manager::Chart->find_by(accno => '8200');
+    $bookings = [
+                  {
+                    chart  => $chart_19,
+                    amount => $taxincluded ? 119 : 100,
+                  },
+                  {
+                    chart  => $chart_7,
+                    amount => $taxincluded ? 107 : 100,
+                  },
+                  {
+                    chart  => $chart_0,
+                    amount => 100,
+                  },
+                ]
+  };
+
+  # optional params:
+  my $project_id = delete $params{globalproject_id};
+
+  # if amount or netamount are given, then it compares them to the final values, and dies if they don't match
+  my $expected_amount    = delete $params{amount};
+  my $expected_netamount = delete $params{netamount};
+
+  my $dec = delete $params{dec} // 2;
+
+  my $today      = DateTime->today_local;
+  my $transdate  = delete $params{transdate} // $today;
+  die "transdate hat to be DateTime object" unless ref($transdate) eq 'DateTime';
+
+  my $gldate     = delete $params{gldate} // $today;
+  die "gldate hat to be DateTime object" unless ref($gldate) eq 'DateTime';
+
+  my $ar_chart = delete $params{ar_chart} // SL::DB::Manager::Chart->find_by( accno => '1400' );
+  die "no ar_chart found or not an AR chart" unless $ar_chart and $ar_chart->link eq 'AR';
+
+  my $ar_transaction = SL::DB::Invoice->new(
+    customer_id      => $customer->id,
+    invoice          => 0,
+    transactions     => [],
+    globalproject_id => $project_id,
+    invnumber        => delete $params{invnumber} // 'test ar_transaction',
+    notes            => delete $params{notes}     // 'test ar_transaction',
+    transdate        => $transdate,
+    gldate           => $gldate,
+    taxincluded      => $taxincluded,
+    taxzone_id       => $customer->taxzone_id, # taxzone_id shouldn't have any effect on ar transactions
+    currency_id      => $::instance_conf->get_currency_id,
+    type             => undef, # isn't set for ar
+    employee_id      => SL::DB::Manager::Employee->current->id,
+  );
+  # assign any parameters that weren't explicitly handled above, e.g. itime
+  $ar_transaction->assign_attributes(%params) if %params;
+
+  foreach my $booking ( @{$bookings} ) {
+    my $chart = delete $booking->{chart};
+    die "illegal chart" unless ref($chart) eq 'SL::DB::Chart';
+
+    my $tax = _transaction_tax_helper($booking, $chart, $transdate); # will die if tax can't be found
+
+    $ar_transaction->add_ar_amount_row(
+      amount     => $booking->{amount}, # add_ar_amount_row expects the user input amount, does its own calculate_tax
+      chart      => $chart,
+      tax_id     => $tax->id,
+      project_id => $booking->{project_id},
+    );
+  }
+
+  my $acc_trans_sum = sum map { $_->amount  } grep { $_->chart_link =~ 'AR_amount' } @{$ar_transaction->transactions};
+  # $main::lxdebug->message(0, sprintf("accno: %s    amount: %s   chart_link: %s\n",
+  #                                    $_->amount,
+  #                                    $_->chart->accno,
+  #                                    $_->chart_link
+  #                                   )) foreach @{$ar_transaction->transactions};
+
+  # determine netamount and amount from the transactions that were added via bookings
+  $ar_transaction->netamount( 1 * sum map { $_->amount  } grep { $_->chart_link =~ 'AR_amount' } @{$ar_transaction->transactions} );
+  # $main::lxdebug->message(0, sprintf('found netamount %s', $ar_transaction->netamount));
+
+  my $taxamount = 1 * sum map { $_->amount  } grep { $_->chart_link =~ /tax/ } @{$ar_transaction->transactions};
+  $ar_transaction->amount( $ar_transaction->netamount + $taxamount );
+  # additional check, add up all transactions before AP-transaction is added
+  my $refamount = 1 * sum map { $_->amount  } @{$ar_transaction->transactions};
+  die "refamount = $refamount, ar_transaction->amount = " . $ar_transaction->amount unless $refamount == $ar_transaction->amount;
+
+  # if amount or netamount were passed as params, check if the values are still
+  # the same after recalculating them from the acc_trans entries
+  if (defined $expected_amount) {
+    die "amount doesn't match acc_trans amounts: $expected_amount != " . $ar_transaction->amount unless $expected_amount == $ar_transaction->amount;
+  }
+  if (defined $expected_netamount) {
+    die "netamount doesn't match acc_trans netamounts: $expected_netamount != " . $ar_transaction->netamount unless $expected_netamount == $ar_transaction->netamount;
+  }
+
+  $ar_transaction->create_ar_row(chart => $ar_chart);
+  $ar_transaction->save;
+  # $main::lxdebug->message(0, sprintf("created ar_transaction with invnumber %s and trans_id %s",
+  #                                     $ar_transaction->invnumber,
+  #                                     $ar_transaction->id));
+  return $ar_transaction;
+}
+
+sub create_gl_transaction {
+  my (%params) = @_;
+
+  my $ob_transaction = delete $params{ob_transaction} // 0;
+  my $cb_transaction = delete $params{cb_transaction} // 0;
+  my $dec            = delete $params{rec} // 2;
+
+  my $taxincluded = defined $params{taxincluded} ? $params{taxincluded} : 1;
+
+  my $today      = DateTime->today_local;
+  my $transdate  = delete $params{transdate} // $today;
+  my $gldate     = delete $params{gldate}    // $today;
+
+  my $reference   = delete $params{reference}   // 'reference';
+  my $description = delete $params{description} // 'description';
+
+  my $department_id = delete $params{department_id};
+
+  my $bookings = delete $params{bookings};
+  unless ( $bookings && scalar @{$bookings} ) {
+    # default bookings if left empty
+    my $expense_chart = SL::DB::Manager::Chart->find_by(accno => '4660') or die "Can't find expense chart 4660\n"; # Reisekosten
+    my $cash_chart    = SL::DB::Manager::Chart->find_by(accno => '1000') or die "Can't find cash chart 1000\n";    # Kasse
+
+    $taxincluded = 0;
+
+    $reference   = 'Reise';
+    $description = 'Reise';
+
+    $bookings = [
+                  {
+                    chart  => $expense_chart, # has default tax of 19%
+                    credit => 84.03,
+                    taxkey => 9,
+                  },
+                  {
+                    chart  => $cash_chart,
+                    debit  => 100,
+                    taxkey => 0,
+                  },
+    ];
+  }
+
+  my $gl_transaction = SL::DB::GLTransaction->new(
+    reference      => $reference,
+    description    => $description,
+    transdate      => $transdate,
+    gldate         => $gldate,
+    taxincluded    => $taxincluded,
+    type           => undef,
+    ob_transaction => $ob_transaction,
+    cb_transaction => $cb_transaction,
+    storno         => 0,
+    storno_id      => undef,
+    transactions   => [],
+  );
+  # assign any parameters that weren't explicitly handled above, e.g. itime
+  $gl_transaction->assign_attributes(%params) if %params;
+
+  my @acc_trans;
+  if ( scalar @{$bookings} ) {
+    # there are several ways of determining the tax:
+    # * tax_id : fetches SL::DB::Tax object via id (as used in dropdown in interface)
+    # * tax : SL::DB::Tax object (where $tax->id = tax_id)
+    # * taxkey : tax is determined from startdate
+    # * none of the above defined: use the default tax for that chart
+
+    foreach my $booking ( @{$bookings} ) {
+      my $chart = delete $booking->{chart};
+      die "illegal chart" unless ref($chart) eq 'SL::DB::Chart';
+
+      die t8('Empty transaction!')
+        unless $booking->{debit} or $booking->{credit}; # must exist and not be 0
+      die t8('Cannot post transaction with a debit and credit entry for the same account!')
+        if defined($booking->{debit}) and defined($booking->{credit});
+
+      my $tax = _transaction_tax_helper($booking, $chart, $transdate); # will die if tax can't be found
+
+      $gl_transaction->add_chart_booking(
+        chart      => $chart,
+        debit      => $booking->{debit},
+        credit     => $booking->{credit},
+        tax_id     => $tax->id,
+        source     => $booking->{source} // '',
+        memo       => $booking->{memo}   // '',
+        project_id => $booking->{project_id}
+      );
+    }
+  };
+
+  $gl_transaction->post;
+
+  return $gl_transaction;
+}
+
+sub _transaction_tax_helper {
+  # checks for hash-entries with key tax, tax_id or taxkey
+  # returns an SL::DB::Tax object
+  # can be used for booking hashref in ar_transaction, ap_transaction and gl_transaction
+  # will modify hashref, e.g. removing taxkey if tax_id was also supplied
+
+  my ($booking, $chart, $transdate) = @_;
+
+  die "_transaction_tax_helper: chart missing"     unless $chart && ref($chart) eq 'SL::DB::Chart';
+  die "_transaction_tax_helper: transdate missing" unless $transdate && ref($transdate) eq 'DateTime';
+
+  my $tax;
+
+  if ( defined $booking->{tax_id} ) { # tax_id may be 0
+    delete $booking->{taxkey}; # ignore any taxkeys that may have been added, tax_id has precedence
+    $tax = SL::DB::Tax->new(id => $booking->{tax_id})->load( with => [ 'chart' ] );
+  } elsif ( $booking->{tax} ) {
+    die "illegal tax entry" unless ref($booking->{tax}) eq 'SL::DB::Tax';
+    $tax = $booking->{tax};
+  } elsif ( defined $booking->{taxkey} ) {
+    # If a taxkey is given, find the taxkey entry for that chart that
+    # matches the stored taxkey and with the correct transdate. This will only work
+    # if kivitendo has that taxkey configured for that chart, i.e. it should barf if
+    # e.g. the bank chart is called with taxkey 3.
+
+    # example query:
+    #   select *
+    #     from taxkeys
+    #    where     taxkey_id = 3
+    #          and chart_id = (select id from chart where accno = '8400')
+    #          and startdate <= '2018-01-01'
+    # order by startdate desc
+    #    limit 1;
+
+    my $taxkey = SL::DB::Manager::TaxKey->get_first(
+      query        => [ and => [ chart_id  => $chart->id,
+                                 startdate => { le => $transdate },
+                                 taxkey    => $booking->{taxkey}
+                               ]
+                      ],
+      sort_by      => "startdate DESC",
+      limit        => 1,
+      with_objects => [ qw(tax) ],
+    );
+    die sprintf("Chart %s doesn't have a taxkey chart configured for taxkey %s", $chart->accno, $booking->{taxkey})
+      unless $taxkey;
+
+    $tax = $taxkey->tax;
+  } else {
+    # use default tax for that chart if neither tax_id, tax or taxkey were defined
+    my $active_taxkey = $chart->get_active_taxkey($transdate);
+    $tax = $active_taxkey->tax;
+    # $main::lxdebug->message(0, sprintf("found default taxrate %s for chart %s", $tax->rate, $chart->displayable_name));
+  };
+
+  die "no tax" unless $tax && ref($tax) eq 'SL::DB::Tax';
+  return $tax;
+};
+
+1;
+
+__END__
+
+=head1 NAME
+
+SL::Dev::Record - create record objects for testing, with minimal defaults
+
+=head1 FUNCTIONS
+
+=head2 C<create_sales_invoice %PARAMS>
+
+Creates a new sales invoice (table ar, invoice = 1).
+
+If neither customer nor invoiceitems are passed as params a customer and two
+parts are created and used for building the invoice.
+
+Minimal usage example:
+
+  my $invoice = SL::Dev::Record::create_sales_invoice();
+
+Example with params:
+
+  my $invoice2 = SL::Dev::Record::create_sales_invoice(
+    invnumber   => 777,
+    transdate   => DateTime->today->subtract(days => 7),
+    taxincluded => 1,
+  );
+
+=head2 C<create_credit_note %PARAMS>
+
+Create a credit note (sales). Use positive quantities when adding items.
+
+Example including creation of parts and of credit_note:
+
+  my $part1 = SL::Dev::Part::new_part(   partnumber => 'T4254')->save;
+  my $part2 = SL::Dev::Part::new_service(partnumber => 'Serv1')->save;
+  my $credit_note = SL::Dev::Record::create_credit_note(
+    invnumber    => '34',
+    taxincluded  => 0,
+    invoiceitems => [ SL::Dev::Record::create_invoice_item(part => $part1, qty =>  3, sellprice => 70),
+                      SL::Dev::Record::create_invoice_item(part => $part2, qty => 10, sellprice => 50),
+                    ]
+  );
+
+=head2 C<create_sales_order %PARAMS>
+
+Examples:
+
+Create a sales order and save it directly via rose, without running
+calculate_prices_and_taxes:
+
+  my $order = SL::Dev::Record::create_sales_order()->save;
+
+Let create_sales_order run calculate_prices_and_taxes and save:
+
+  my $order = SL::Dev::Record::create_sales_order(save => 1);
+
+
+Example including creation of part and of sales order:
+
+  my $part1 = SL::Dev::Part::new_part(   partnumber => 'T4254')->save;
+  my $part2 = SL::Dev::Part::new_service(partnumber => 'Serv1')->save;
+  my $order = SL::Dev::Record::create_sales_order(
+    save         => 1,
+    taxincluded  => 0,
+    orderitems => [ SL::Dev::Record::create_order_item(part => $part1, qty =>  3, sellprice => 70),
+                    SL::Dev::Record::create_order_item(part => $part2, qty => 10, sellprice => 50),
+                  ]
+  );
+
+Example: create 100 orders with the same part for 100 new customers:
+
+  my $part1 = SL::Dev::Part::new_part(partnumber => 'T6256')->save;
+  SL::Dev::Record::create_sales_order(
+    save         => 1,
+    taxincluded  => 0,
+    orderitems => [ SL::Dev::Record::create_order_item(part => $part1, qty => 1, sellprice => 9) ]
+  ) for 1 .. 100;
+
+=head2 C<create_purchase_order %PARAMS>
+
+See comments for C<create_sales_order>.
+
+Example:
+
+  my $purchase_order = SL::Dev::Record::create_purchase_order(save => 1);
+
+
+=head2 C<create_item %PARAMS>
+
+Creates an item from a part object that can be added to a record.
+
+Required params:
+
+  record_type (sales_invoice, sales_order, sales_delivery_order)
+  part        (an SL::DB::Part object)
+
+Example including creation of part and of invoice:
+
+  my $part    = SL::Dev::Part::new_part(  partnumber  => 'T4254')->save;
+  my $item    = SL::Dev::Record::create_invoice_item(part => $part, qty => 2.5);
+  my $invoice = SL::Dev::Record::create_sales_invoice(
+    taxincluded  => 0,
+    invoiceitems => [ $item ],
+  );
+
+=head2 C<create_project %PARAMS>
+
+Creates a default project.
+
+Minimal example, creating a project with status "running" and type "Standard":
+
+  my $project = SL::Dev::Record::create_project();
+
+  $project = SL::Dev::Record::create_project(
+    projectnumber => 'p1',
+    description   => 'Test project',
+  )
+
+If C<$params{description}> or C<$params{projectnumber}> exists, this will override the
+default value 'Test project'.
+
+C<%params> should only contain alterable keys from the object Project.
+
+=head2 C<create_department %PARAMS>
+
+Creates a default department.
+
+Minimal example:
+
+  my $department = SL::Dev::Record::create_department();
+
+  my $department = SL::Dev::Record::create_department(
+    description => 'Hawaii',
+  )
+
+If C<$params{description}> exists, this will override the
+default value 'Test Department'.
+
+C<%params> should only contain alterable keys from the object Department.
+
+=head2 C<create_ap_transaction %PARAMS>
+
+Creates a new AP transaction (table ap, invoice = 0), and will try to add as
+many defaults as possible.
+
+Possible parameters:
+ * vendor (SL::DB::Vendor object, defaults to SL::Dev default vendor)
+ * taxincluded (0 or 1, defaults to 1)
+ * transdate (DateTime object, defaults to current date)
+ * bookings (arrayref for the charts to be booked, see examples below)
+ * amount (to check if final amount matches this amount)
+ * netamount (to check if final amount matches this amount)
+ * dec (number of decimals to round to, defaults to 2)
+ * ap_chart (SL::DB::Chart object, default to accno 1600)
+ * invnumber (defaults to 'test ap_transaction')
+ * notes (defaults to 'test ap_transaction')
+ * globalproject_id
+
+Currently doesn't support exchange rates.
+
+Minimal usage example, creating an AP transaction with a default vendor and
+default bookings (telephone, postage):
+
+  use SL::Dev::Record qw(create_ap_transaction);
+  my $invoice = create_ap_transaction();
+
+Create an AP transaction with a specific vendor and specific charts:
+
+  my $vendor = SL::Dev::CustomerVendor::new_vendor(name => 'My Vendor')->save;
+  my $chart_postage   = SL::DB::Manager::Chart->find_by(description => 'Porto');
+  my $chart_telephone = SL::DB::Manager::Chart->find_by(description => 'Telefon');
+
+  my $ap_transaction = create_ap_transaction(
+    vendor      => $vendor,
+    invnumber   => 'test invoice taxincluded',
+    taxincluded => 1,
+    amount      => 2190, # optional param for checking whether final amount matches
+    netamount   => 2000, # optional param for checking whether final netamount matches
+    bookings    => [
+                     {
+                       chart  => $chart_postage,
+                       amount => 1000,
+                     },
+                     {
+                       chart  => $chart_telephone,
+                       amount => 1190,
+                     },
+                   ]
+  );
+
+Or the same example with tax not included, but an old transdate and old taxrate (16%):
+
+  my $ap_transaction = create_ap_transaction(
+    vendor      => $vendor,
+    invnumber   => 'test invoice tax not included',
+    transdate   => DateTime->new(year => 2000, month => 10, day => 1),
+    taxincluded => 0,
+    amount      => 2160, # optional param for checking whether final amount matches
+    netamount   => 2000, # optional param for checking whether final netamount matches
+    bookings    => [
+                     {
+                       chart  => $chart_postage,
+                       amount => 1000,
+                     },
+                     {
+                       chart  => $chart_telephone,
+                       amount => 1000,
+                     },
+                 ]
+  );
+
+Don't use the default tax, e.g. postage with 19%:
+
+  my $tax_9          = SL::DB::Manager::Tax->find_by(taxkey => 9, rate => 0.19);
+  my $chart_postage  = SL::DB::Manager::Chart->find_by(description => 'Porto');
+  my $ap_transaction = create_ap_transaction(
+    invnumber   => 'postage with tax',
+    taxincluded => 0,
+    bookings    => [
+                     {
+                       chart  => $chart_postage,
+                       amount => 1000,
+                       tax    => $tax_9,
+                     },
+                   ],
+  );
+
+=head2 C<create_ar_transaction %PARAMS>
+
+See C<create_ap_transaction>, except use customer instead of vendor.
+
+=head2 C<create_gl_transaction %PARAMS>
+
+Creates a new GL transaction (table gl), which is basically a wrapper around
+SL::DB::GLTransaction->new(...) and add_chart_booking and post, while setting
+as many defaults as possible.
+
+Possible parameters:
+
+ * taxincluded (0 or 1, defaults to 1)
+ * transdate (DateTime object, defaults to current date)
+ * dec (number of decimals to round to, defaults to 2)
+ * bookings (arrayref for the charts and taxes to be booked, see examples below)
+
+bookings must include a least:
+
+ * chart as an SL::DB::Chart object
+ * credit or debit, as positive numbers
+ * tax_id, tax (an SL::DB::Tax object) or taxkey (e.g. 9)
+
+Can't be used to create storno transactions.
+
+Minimal usage example, using all the defaults, creating a GL transaction with
+travel expenses:
+
+  use SL::Dev::Record qw(create_gl_transaction);
+  $gl_transaction = create_gl_transaction();
+
+Create a GL transaction with a specific charts and taxes (the default taxes for
+those charts are used if none are explicitly given in bookings):
+
+  my $cash           = SL::DB::Manager::Chart->find_by( description => 'Kasse'          );
+  my $betriebsbedarf = SL::DB::Manager::Chart->find_by( description => 'Betriebsbedarf' );
+  $gl_transaction = create_gl_transaction(
+    reference   => 'betriebsbedarf',
+    taxincluded => 1,
+    bookings    => [
+                     {
+                       chart  => $betriebsbedarf,
+                       memo   => 'foo 1',
+                       source => 'foo 1',
+                       credit => 119,
+                     },
+                     {
+                       chart  => $betriebsbedarf,
+                       memo   => 'foo 2',
+                       source => 'foo 2',
+                       credit => 119,
+                     },
+                     {
+                       chart  => $cash,
+                       debit  => 238,
+                       memo   => 'foo 1+2',
+                       source => 'foo 1+2',
+                     },
+                   ],
+  );
+
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+G. Richardson E<lt>grichardson@kivitec.deE<gt>
+
+=cut
diff --git a/SL/Dev/Shop.pm b/SL/Dev/Shop.pm
new file mode 100644 (file)
index 0000000..857a1c4
--- /dev/null
@@ -0,0 +1,77 @@
+package SL::Dev::Shop;
+
+use strict;
+use base qw(Exporter);
+use Data::Dumper;
+our @EXPORT_OK = qw(new_shop new_shop_part new_shop_order);
+our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
+
+use SL::DB::Shop;
+
+sub new_shop {
+  my (%params) = @_;
+
+  my $shop = SL::DB::Shop->new(
+    description => delete $params{description} || 'testshop',
+    %params
+  );
+  return $shop;
+}
+
+sub new_shop_part {
+  my (%params) = @_;
+
+  my $part = delete $params{part};
+  my $shop = delete $params{shop};
+
+  my $shop_part = SL::DB::ShopPart->new(
+    part => $part,
+    shop => $shop,
+    %params
+  )->save;
+  return $shop_part;
+}
+
+sub new_shop_order {
+  my (%params) = @_;
+
+  my $shop_order = SL::DB::ShopOrder->new(
+    shop => $params{shop},
+    %params
+  );
+  return $shop_order;
+}
+
+
+1;
+
+__END__
+
+=head1 NAME
+
+SL::Dev::Shop - create shop objects for testing, with minimal defaults
+
+=head1 FUNCTIONS
+
+=head2 C<create_shop %PARAMS>
+
+Creates a new shop object.
+
+  my $shop = SL::Dev::Shop::create_shop();
+
+Add a part as a shop part to the shop:
+
+  my $part = SL::Dev::Part::create_part();
+  $shop->add_shop_parts( SL::DB::ShopPart->new(part => $part, shop_description => 'Simply the best part!' ) );
+  $shop->save;
+
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Dev/TimeRecording.pm b/SL/Dev/TimeRecording.pm
new file mode 100644 (file)
index 0000000..825472f
--- /dev/null
@@ -0,0 +1,41 @@
+package SL::Dev::TimeRecording;
+
+use strict;
+use base qw(Exporter);
+our @EXPORT_OK = qw(new_time_recording);
+our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
+
+use DateTime;
+
+use SL::DB::TimeRecording;
+
+use SL::DB::Employee;
+use SL::Dev::CustomerVendor qw(new_customer);
+
+
+sub new_time_recording {
+  my (%params) = @_;
+
+  my $customer = delete $params{customer} // new_customer(name => 'Testcustomer')->save;
+  die "illegal customer" unless defined $customer && ref($customer) eq 'SL::DB::Customer';
+
+  my $employee     = $params{employee}     // SL::DB::Manager::Employee->current;
+  my $staff_member = $params{staff_member} // $employee;
+
+  my $now = DateTime->now_local;
+
+  my $time_recording = SL::DB::TimeRecording->new(
+    start_time   => $now,
+    end_time     => $now->add(hours => 1),
+    customer     => $customer,
+    description  => '<p>this and that</p>',
+    staff_member => $staff_member,
+    employee     => $employee,
+    %params,
+  );
+
+  return $time_recording;
+}
+
+
+1;
index 77074cc..31c76a5 100644 (file)
@@ -7,25 +7,18 @@ use strict;
 #   parse_html_template('login_screen/user_login')
 #   parse_html_template('generic/error')
 
-BEGIN {
-  use SL::System::Process;
-  my $exe_dir = SL::System::Process::exe_dir;
-
-  unshift @INC, "${exe_dir}/modules/override"; # Use our own versions of various modules (e.g. YAML).
-  push    @INC, "${exe_dir}/modules/fallback"; # Only use our own versions of modules if there's no system version.
-  unshift @INC, $exe_dir;
-}
-
 use Carp;
 use CGI qw( -no_xhtml);
 use Config::Std;
 use DateTime;
 use Encode;
 use English qw(-no_match_vars);
+use FCGI;
 use File::Basename;
+use IO::File;
 use List::MoreUtils qw(all);
 use List::Util qw(first);
-use SL::ArchiveZipFixes;
+use POSIX qw(setlocale);
 use SL::Auth;
 use SL::Dispatcher::AuthHandler;
 use SL::LXDebug;
@@ -36,9 +29,14 @@ use SL::Common;
 use SL::Form;
 use SL::Helper::DateTime;
 use SL::InstanceConfiguration;
+use SL::MoreCommon qw(uri_encode);
 use SL::Template::Plugin::HTMLFixes;
 use SL::User;
 
+use Rose::Object::MakeMethods::Generic (
+  scalar => [ qw(restart_after_request) ],
+);
+
 # Trailing new line is added so that Perl will not add the line
 # number 'die' was called in.
 use constant END_OF_REQUEST => "END-OF-REQUEST\n";
@@ -52,7 +50,10 @@ sub new {
   $self->{interface} = lc($interface || 'cgi');
   $self->{auth_handler} = SL::Dispatcher::AuthHandler->new;
 
-  SL::ArchiveZipFixes->apply_fixes;
+  # Initialize character type locale to be UTF-8 instead of C:
+  foreach my $locale (qw(de_DE.UTF-8 en_US.UTF-8)) {
+    last if setlocale('LC_CTYPE', $locale);
+  }
 
   return $self;
 }
@@ -137,7 +138,7 @@ sub show_error {
   print $::form->parse_html_template($template, \%params);
   $::lxdebug->leave_sub;
 
-  ::end_of_request();
+  end_request();
 }
 
 sub pre_startup_setup {
@@ -221,6 +222,20 @@ sub _run_controller {
   "SL::Controller::$_[0]"->new->_run_action($_[1]);
 }
 
+sub handle_all_requests {
+  my ($self) = @_;
+
+  my $request = FCGI::Request();
+  while ($request->Accept() >= 0) {
+    $self->handle_request($request);
+
+    $self->restart_after_request(1) if $self->_interface_is_fcgi && SL::System::Process::memory_usage_is_too_high();
+    $request->LastCall              if $self->restart_after_request;
+  }
+
+  exec $0 if $self->restart_after_request;
+}
+
 sub handle_request {
   my $self         = shift;
   $self->{request} = shift;
@@ -232,7 +247,7 @@ sub handle_request {
 
   my $session_result = $self->pre_request_initialization;
 
-  $::form->read_cgi_input;
+  $::request->read_cgi_input($::form);
 
   my %routing;
   eval { %routing = $self->_route_request($ENV{SCRIPT_NAME}); 1; } or return;
@@ -274,8 +289,11 @@ sub handle_request {
     if (   (($script eq 'login') && !$action)
         || ($script eq 'admin')
         || (SL::Auth::SESSION_EXPIRED() == $session_result)) {
-      $self->redirect_to_login(script => $script, error => 'session');
-
+      $self->handle_login_error(routing_type => $routing_type,
+                                script       => $script,
+                                controller   => $script_name,
+                                action       => $action,
+                                error        => 'session');
     }
 
     my %auth_result = $self->{auth_handler}->handle(
@@ -285,7 +303,7 @@ sub handle_request {
       action       => $action,
     );
 
-    ::end_of_request() unless $auth_result{auth_ok};
+    $self->end_request unless $auth_result{auth_ok};
 
     delete @{ $::form }{ grep { m/^\{AUTH\}/ } keys %{ $::form } } unless $auth_result{keep_auth_vars};
 
@@ -324,7 +342,7 @@ sub handle_request {
     $self->{request}->Finish;
   }
 
-  $::lxdebug->end_request;
+  $::lxdebug->end_request(routing_type => $routing_type, script_name => $script_name, action => $action);
 
   # cleanup
   $::auth->save_session;
@@ -333,24 +351,112 @@ sub handle_request {
 
   $::locale   = undef;
   $::form     = undef;
-  $::myconfig = ();
+  %::myconfig = ();
   $::request  = undef;
 
   SL::DBConnect::Cache->reset_all;
-  Form::disconnect_standard_dbh;
 
   $self->_watch_for_changed_files;
 
   $::lxdebug->leave_sub;
 }
 
-sub redirect_to_login {
+sub reply_with_json_error {
   my ($self, %params) = @_;
+
+  my %errors = (
+    session  => { code => '401 Unauthorized',          text => 'session expired' },
+    password => { code => '401 Unauthorized',          text => 'incorrect username or password' },
+    action   => { code => '400 Bad request',           text => 'incorrect or missing action' },
+    access   => { code => '403 Forbidden',             text => 'no permissions for accessing this function' },
+    _default => { code => '500 Internal server error', text => 'general server-side error' },
+  );
+
+  my $error = $errors{$params{error}} // $errors{_default};
+  my $reply = SL::JSON::to_json({ status => 'failed', error => $error->{text} });
+
+  print $::request->cgi->header(
+    -type    => 'application/json',
+    -charset => 'utf-8',
+    -status  => $error->{code},
+  );
+
+  print $reply;
+
+  $self->end_request;
+}
+
+sub handle_login_error {
+  my ($self, %params) = @_;
+
+  return $self->reply_with_json_error(error => $params{error}) if $::request->type eq 'json';
+
   my $action          = ($params{script} // '') =~ m/^admin/i ? 'Admin/login' : 'LoginScreen/user_login';
   $action            .= '&error=' . $params{error} if $params{error};
 
-  print $::request->cgi->redirect("controller.pl?action=${action}");
-  ::end_of_request();
+  my $redirect_url = "controller.pl?action=${action}";
+
+  if (   $action =~ m/LoginScreen\/user_login/
+      && $params{action}
+      && 'get' eq lc($ENV{REQUEST_METHOD})
+      && !_is_callback_blacklisted(map {$_ => $params{$_}} qw(routing_type script controller action) )
+  ) {
+
+    require SL::Controller::Base;
+    my $controller = SL::Controller::Base->new;
+
+    delete $params{error};
+    delete $params{routing_type};
+    delete @{ $::form }{ grep { m/^\{AUTH\}/ } keys %{ $::form } };
+
+    my $callback   = $controller->url_for(%params, %{$::form});
+    $redirect_url .= '&callback=' . uri_encode($callback);
+  }
+
+  print $::request->cgi->redirect($redirect_url);
+  $self->end_request;
+}
+
+sub _is_callback_blacklisted {
+  my (%params) = @_;
+
+  # You can give a name only, then all actions are blackisted.
+  # Or you can give name and action, then only this action is blacklisted
+  # examples:
+  # {name => 'is',      action => 'edit'}
+  # {name => 'Project', action => 'edit'},
+  my @script_blacklist = (
+    {name => 'admin'},
+    {name => 'login'},
+  );
+
+  my @controller_blacklist = (
+    {name => 'Admin'},
+    {name => 'LoginScreen'},
+  );
+
+  my ($name, $blacklist);
+  if ('old' eq ($params{routing_type} // '')) {
+    $name      = $params{script};
+    $blacklist = \@script_blacklist;
+  } else {
+    $name      = $params{controller};
+    $blacklist = \@controller_blacklist;
+  }
+
+  foreach my $bl (@$blacklist) {
+    return 1 if _is_name_action_blacklisted($bl->{name}, $bl->{action}, $name, $params{action});
+  }
+
+  return;
+}
+
+sub _is_name_action_blacklisted {
+  my ($blacklisted_name, $blacklisted_action, $name, $action) = @_;
+
+  return 1 if ($name // '') eq $blacklisted_name && !$blacklisted_action;
+  return 1 if ($name // '') eq $blacklisted_name && ($action // '') eq $blacklisted_action;
+  return;
 }
 
 sub unrequire_bin_mozilla {
@@ -416,7 +522,7 @@ sub _route_controller_request {
   eval {
     # Redirect simple requests to controller.pl without any GET/POST
     # param to the login page.
-    $self->redirect_to_login(error => 'action') if !$::form->{action};
+    $self->handle_login_error(error => 'action') if !$::form->{action};
 
     # Show an error if the »action« parameter doesn't match the
     # pattern »Controller/action«.
@@ -461,7 +567,7 @@ sub _watch_for_changed_files {
   my $ok = all { (stat($_))[9] == $fcgi_file_cache{$_} } keys %fcgi_file_cache;
   return if $ok;
   $::lxdebug->message(LXDebug::DEBUG1(), "Program modifications detected. Restarting.");
-  exit;
+  $self->restart_after_request(1);
 }
 
 sub get_standard_filehandles {
@@ -478,14 +584,10 @@ sub _check_for_old_config_files {
   $::form->header;
   print $::form->parse_html_template('login_screen/old_configuration_files', { FILES => \@old_files });
 
-  ::end_of_request();
+  end_request();
 }
 
-package main;
-
-use strict;
-
-sub end_of_request {
+sub end_request {
   die SL::Dispatcher->END_OF_REQUEST;
 }
 
index a1a5bdc..acdf29c 100644 (file)
@@ -3,15 +3,22 @@ package SL::Dispatcher::AuthHandler::User;
 use strict;
 use parent qw(Rose::Object);
 
+use Encode ();
+use MIME::Base64 ();
+
 use SL::Layout::Dispatcher;
 
 sub handle {
   my ($self, %param) = @_;
 
-  my $login = $::form->{'{AUTH}login'} || $::auth->get_session_value('login');
+  my ($http_auth_login, $http_auth_password) = $self->_parse_http_basic_auth;
+
+  my $login = $::form->{'{AUTH}login'} // $http_auth_login // $::auth->get_session_value('login');
+
   return $self->_error(%param) if !defined $login;
 
-  my $client_id = $::form->{'{AUTH}client_id'} || $::auth->get_session_value('client_id');
+  my $client_id = $::form->{'{AUTH}client_id'} // $::auth->get_session_value('client_id') // $::auth->get_default_client_id;
+
   return $self->_error(%param) if !$client_id || !$::auth->set_client($client_id);
 
   %::myconfig = User->get_default_myconfig($::auth->read_user(login => $login));
@@ -19,11 +26,14 @@ sub handle {
   return $self->_error(%param) unless $::myconfig{login};
 
   $::locale = Locale->new($::myconfig{countrycode});
-  $::request->{layout} = SL::Layout::Dispatcher->new(style => $::myconfig{menustyle});
+  $::request->{layout} = $::request->is_mobile
+    ? SL::Layout::Dispatcher->new(style => 'mobile')
+    : SL::Layout::Dispatcher->new(style => $::myconfig{menustyle});
 
   my $ok   =  $::auth->is_api_token_cookie_valid;
-  $ok    ||=  $::form->{'{AUTH}login'} && (SL::Auth::OK() == $::auth->authenticate($::myconfig{login}, $::form->{'{AUTH}password'}));
-  $ok    ||= !$::form->{'{AUTH}login'} && (SL::Auth::OK() == $::auth->authenticate($::myconfig{login}, undef));
+  $ok    ||=  $::form->{'{AUTH}login'}                      && (SL::Auth::OK() == $::auth->authenticate($::myconfig{login}, $::form->{'{AUTH}password'}));
+  $ok    ||= !$::form->{'{AUTH}login'} &&  $http_auth_login && (SL::Auth::OK() == $::auth->authenticate($::myconfig{login}, $http_auth_password));
+  $ok    ||= !$::form->{'{AUTH}login'} && !$http_auth_login && (SL::Auth::OK() == $::auth->authenticate($::myconfig{login}, undef));
 
   return $self->_error(%param) if !$ok;
 
@@ -35,11 +45,34 @@ sub handle {
 }
 
 sub _error {
-  my $self = shift;
+  my ($self, %param) = @_;
 
   $::auth->punish_wrong_login;
-  print $::request->{cgi}->redirect('controller.pl?action=LoginScreen/user_login&error=password');
+  $::dispatcher->handle_login_error(%param, error => 'password');
+
   return 0;
 }
 
+sub _parse_http_basic_auth {
+  my ($self) = @_;
+
+  # See RFC 7617.
+
+  # Requires that the server passes the 'Authorization' header as the
+  # environment variable 'HTTP_AUTHORIZATION'. Example code for
+  # Apache:
+
+  # SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
+
+  my $data = $ENV{HTTP_AUTHORIZATION};
+
+  return unless ($data // '') =~ m{^basic +(.+)}i;
+
+  $data = Encode::decode('utf-8', MIME::Base64::decode($1));
+
+  return unless $data =~ m{(.+?):(.+)};
+
+  return ($1, $2);
+}
+
 1;
diff --git a/SL/Drafts.pm b/SL/Drafts.pm
deleted file mode 100644 (file)
index e5242aa..0000000
+++ /dev/null
@@ -1,151 +0,0 @@
-#======================================================================
-# LX-Office ERP
-#
-#======================================================================
-#
-# Saving and loading drafts
-#
-#======================================================================
-
-package Drafts;
-
-use YAML;
-
-use SL::Common;
-use SL::DBUtils;
-
-use strict;
-
-sub get_module {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $form) = @_;
-
-  my ($module, $submodule);
-
-  $module = $form->{"script"};
-  $module =~ s/\.pl$//;
-  if (grep({ $module eq $_ } qw(is ir ar ap))) {
-    $submodule = "invoice";
-  } else {
-    $submodule = "unknown";
-  }
-
-  $main::lxdebug->leave_sub();
-
-  return ($module, $submodule);
-}
-
-my @dont_save = qw(login password action);
-
-sub dont_save {
-  return @dont_save;
-}
-
-sub save {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form, $draft_id, $draft_description) = @_;
-
-  my ($dbh, $sth, $query, %saved, $dumped);
-
-  $dbh = $form->get_standard_dbh;
-  $dbh->begin_work;
-
-  my ($module, $submodule) = $self->get_module($form);
-
-  $query = "SELECT COUNT(*) FROM drafts WHERE id = ?";
-  my ($res) = selectrow_query($form, $dbh, $query, $draft_id);
-
-  if (!$res) {
-    $draft_id = $module . "-" . $submodule . "-" . Common::unique_id();
-    $query    = "INSERT INTO drafts (id, module, submodule) VALUES (?, ?, ?)";
-    do_query($form, $dbh, $query, $draft_id, $module, $submodule);
-  }
-
-  map({ $saved{$_} = $form->{$_};
-        delete($form->{$_}); } @dont_save);
-
-  $dumped = YAML::Dump($form);
-  map({ $form->{$_} = $saved{$_}; } @dont_save);
-
-  $query =
-    qq|UPDATE drafts SET description = ?, form = ?, employee_id = | .
-    qq|  (SELECT id FROM employee WHERE login = ?) | .
-    qq|WHERE id = ?|;
-
-  do_query($form, $dbh, $query, $draft_description, $dumped, $::myconfig{login}, $draft_id);
-
-  $dbh->commit();
-
-  $form->{draft_id}          = $draft_id;
-  $form->{draft_description} = $draft_description;
-
-  $main::lxdebug->leave_sub();
-}
-
-sub load {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form, $draft_id) = @_;
-
-  my ($dbh, $sth, $query, @values);
-
-  $dbh = $form->get_standard_dbh;
-
-  $query = qq|SELECT id, description, form FROM drafts WHERE id = ?|;
-
-  $sth = prepare_execute_query($form, $dbh, $query, $draft_id);
-
-  if (my $ref = $sth->fetchrow_hashref()) {
-    @values = ($ref->{form}, $ref->{id}, $ref->{description});
-  }
-  $sth->finish();
-
-  $main::lxdebug->leave_sub();
-
-  return @values;
-}
-
-sub remove {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form, @draft_ids) = @_;
-
-  return $main::lxdebug->leave_sub() unless (@draft_ids);
-
-  my ($dbh, $sth, $query);
-
-  $dbh = $form->get_standard_dbh;
-
-  $query = qq|DELETE FROM drafts WHERE id IN (| . join(", ", map { "?" } @draft_ids) . qq|)|;
-  do_query($form, $dbh, $query, @draft_ids);
-
-  $dbh->commit;
-
-  $main::lxdebug->leave_sub();
-}
-
-sub list {
-  $::lxdebug->enter_sub;
-
-  my $self     = shift;
-  my $myconfig = shift || \%::myconfig;
-  my $form     = shift ||  $::form;
-  my $dbh      = $form->get_standard_dbh;
-
-  my @list = selectall_hashref_query($form, $dbh, <<SQL, $self->get_module($form));
-    SELECT d.id, d.description, d.itime::timestamp(0) AS itime,
-      e.name AS employee_name
-    FROM drafts d
-    LEFT JOIN employee e ON d.employee_id = e.id
-    WHERE (d.module = ?) AND (d.submodule = ?)
-    ORDER BY d.itime
-SQL
-
-  $::lxdebug->leave_sub;
-
-  return @list;
-}
-
-1;
index 55f58f0..822e31f 100644 (file)
--- a/SL/FU.pm
+++ b/SL/FU.pm
@@ -6,20 +6,29 @@ use List::Util qw(first);
 
 use SL::Common;
 use SL::DBUtils;
+use SL::DB;
 use SL::Notes;
 
 use strict;
 
 sub save {
+  my ($self, %params) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_save, $self, %params);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _save {
   my $self     = shift;
   my %params   = @_;
 
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
+  my $dbh      = $params{dbh} || SL::DB->client->dbh;
   my ($query, @values);
 
   if (!$params{id}) {
@@ -56,9 +65,7 @@ sub save {
 
   $sth->finish();
 
-  $dbh->commit() unless ($params{dbh});
-
-  $main::lxdebug->leave_sub();
+  return 1;
 }
 
 sub finish {
@@ -72,11 +79,10 @@ sub finish {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $form->get_standard_dbh($myconfig);
-
-  do_query($form, $dbh, qq|UPDATE follow_ups SET done = TRUE WHERE id = ?|, conv_i($params{id}));
-
-  $dbh->commit();
+  SL::DB->client->with_transaction(sub {
+    do_query($form, SL::DB->client->dbh, qq|UPDATE follow_ups SET done = TRUE WHERE id = ?|, conv_i($params{id}));
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
@@ -92,15 +98,16 @@ sub delete {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $form->get_standard_dbh($myconfig);
-
-  my $id       = conv_i($params{id});
+  SL::DB->client->with_transaction(sub {
+    my $dbh      = SL::DB->client->dbh;
 
-  do_query($form, $dbh, qq|DELETE FROM follow_up_links WHERE follow_up_id = ?|,                         $id);
-  do_query($form, $dbh, qq|DELETE FROM follow_ups      WHERE id = ?|,                                   $id);
-  do_query($form, $dbh, qq|DELETE FROM notes           WHERE (trans_id = ?) AND (trans_module = 'fu')|, $id);
+    my $id       = conv_i($params{id});
 
-  $dbh->commit();
+    do_query($form, $dbh, qq|DELETE FROM follow_up_links WHERE follow_up_id = ?|,                         $id);
+    do_query($form, $dbh, qq|DELETE FROM follow_ups      WHERE id = ?|,                                   $id);
+    do_query($form, $dbh, qq|DELETE FROM notes           WHERE (trans_id = ?) AND (trans_module = 'fu')|, $id);
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
@@ -188,8 +195,10 @@ sub follow_ups {
   my @values_user   = ();
 
   if ($params{trans_id}) {
-    $where .= qq| AND EXISTS (SELECT * FROM follow_up_links ful
-                              WHERE (ful.follow_up_id = fu.id) AND (ful.trans_id = ?))|;
+    $where .= qq| AND fu.id IN (select follow_up_id from follow_up_links where trans_id = ?)|;
+   # $where .= qq| AND (ful.follow_up_id = fu.id) AND (ful.trans_id = ?))|;
+   # $where .= qq| AND EXISTS (SELECT * FROM follow_up_links ful
+   #                           WHERE (ful.follow_up_id = fu.id) AND (ful.trans_id = ?))|;
     push @values, conv_i($params{trans_id});
   }
 
@@ -210,7 +219,7 @@ sub follow_ups {
   foreach my $item (qw(subject body)) {
     next unless ($params{$item});
     $where .= qq| AND (n.${item} ILIKE ?)|;
-    push @values, '%' . $params{$item} . '%';
+    push @values, like($params{$item});
   }
 
   if ($params{reference}) {
@@ -219,7 +228,7 @@ sub follow_ups {
                               WHERE (ful.follow_up_id = fu.id)
                                 AND (ful.trans_info ILIKE ?)
                               LIMIT 1)|;
-    push @values, '%' . $params{reference} . '%';
+    push @values, like($params{reference});
   }
 
   if ($params{follow_up_date_from}) {
@@ -239,8 +248,12 @@ sub follow_ups {
     $where .= qq| AND (date_trunc('DAY', fu.itime) <= ?)|;
     push @values, conv_date($params{itime_to});
   }
+  if ($params{created_for}) {
+    $where .= qq| AND fu.created_for_user = ?|;
+    push @values, conv_i($params{created_for});
+  }
 
-  if ($params{all_users}) {
+  if ($params{all_users} || $params{trans_id}) {  # trans_id only for documents?
     $where_user = qq|OR (fu.created_by IN (SELECT DISTINCT what FROM follow_up_access WHERE who = ?))|;
     push @values_user, $employee_id;
   }
@@ -325,8 +338,14 @@ sub link_details {
     };
 
   } elsif ($params{trans_type} eq 'sales_quotation') {
+    my $script = 'oe.pl';
+    my $action = 'edit';
+    if ($::instance_conf->get_feature_experimental_order) {
+      $script = 'controller.pl';
+      $action = 'Order/edit';
+    }
     $link = {
-      'url'   => 'oe.pl?action=edit&type=sales_quotation&id=' . $params{trans_id},
+      'url'   => $script . '?action=' . $action . '&type=sales_quotation&id=' . $params{trans_id},
       'title' => $locale->text('Sales quotation') . " $params{trans_info}",
     };
 
@@ -345,8 +364,14 @@ sub link_details {
     };
 
   } elsif ($params{trans_type} eq 'sales_order') {
+    my $script = 'oe.pl';
+    my $action = 'edit';
+    if ($::instance_conf->get_feature_experimental_order) {
+      $script = 'controller.pl';
+      $action = 'Order/edit';
+    }
     $link = {
-      'url'   => 'oe.pl?action=edit&type=sales_order&id=' . $params{trans_id},
+      'url'   => $script . '?action=' . $action . '&type=sales_order&id=' . $params{trans_id},
       'title' => $locale->text('Sales Order') . " $params{trans_info}",
     };
 
@@ -356,6 +381,12 @@ sub link_details {
       'title' => $locale->text('Sales Invoice') . " $params{trans_info}",
     };
 
+  } elsif ($params{trans_type} eq 'purchase_invoice') {
+    $link = {
+      'url'   => 'ir.pl?action=edit&type=purchase_invoice&id=' . $params{trans_id},
+      'title' => $locale->text('Purchase Invoice') . " $params{trans_info}",
+    };
+
   } elsif ($params{trans_type} eq 'credit_note') {
     $link = {
       'url'   => 'is.pl?action=edit&type=credit_note&id=' . $params{trans_id},
@@ -369,14 +400,26 @@ sub link_details {
     };
 
   } elsif ($params{trans_type} eq 'request_quotation') {
+    my $script = 'oe.pl';
+    my $action = 'edit';
+    if ($::instance_conf->get_feature_experimental_order) {
+      $script = 'controller.pl';
+      $action = 'Order/edit';
+    }
     $link = {
-      'url'   => 'oe.pl?action=edit&type=request_quotation&id=' . $params{trans_id},
+      'url'   => $script . '?action=' . $action . '&type=request_quotation&id=' . $params{trans_id},
       'title' => $locale->text('Request quotation') . " $params{trans_info}",
     };
 
   } elsif ($params{trans_type} eq 'purchase_order') {
+    my $script = 'oe.pl';
+    my $action = 'edit';
+    if ($::instance_conf->get_feature_experimental_order) {
+      $script = 'controller.pl';
+      $action = 'Order/edit';
+    }
     $link = {
-      'url'   => 'oe.pl?action=edit&type=purchase_order&id=' . $params{trans_id},
+      'url'   => $script . '?action=' . $action . '&type=purchase_order&id=' . $params{trans_id},
       'title' => $locale->text('Purchase Order') . " $params{trans_info}",
     };
 
@@ -422,24 +465,25 @@ sub save_access_rights {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $form->get_standard_dbh($myconfig);
-
-  my ($id)     = selectrow_query($form, $dbh, qq|SELECT id FROM employee WHERE login = ?|, $::myconfig{login});
+  SL::DB->client->with_transaction(sub {
+    my $dbh      = SL::DB->client->dbh;
 
-  do_query($form, $dbh, qq|DELETE FROM follow_up_access WHERE what = ?|, $id);
+    my ($id)     = selectrow_query($form, $dbh, qq|SELECT id FROM employee WHERE login = ?|, $::myconfig{login});
 
-  my $query    = qq|INSERT INTO follow_up_access (who, what) VALUES (?, ?)|;
-  my $sth      = prepare_query($form, $dbh, $query);
+    do_query($form, $dbh, qq|DELETE FROM follow_up_access WHERE what = ?|, $id);
 
-  while (my ($who, $access_allowed) = each %{ $params{access} }) {
-    next unless ($access_allowed);
+    my $query    = qq|INSERT INTO follow_up_access (who, what) VALUES (?, ?)|;
+    my $sth      = prepare_query($form, $dbh, $query);
 
-    do_statement($form, $sth, $query, conv_i($who), $id);
-  }
+    while (my ($who, $access_allowed) = each %{ $params{access} }) {
+      next unless ($access_allowed);
 
-  $sth->finish();
+      do_statement($form, $sth, $query, conv_i($who), $id);
+    }
 
-  $dbh->commit();
+    $sth->finish();
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
diff --git a/SL/File.pm b/SL/File.pm
new file mode 100644 (file)
index 0000000..ca065e0
--- /dev/null
@@ -0,0 +1,735 @@
+package SL::File;
+
+use strict;
+
+use parent qw(Rose::Object);
+
+use SL::File::Backend;
+use SL::File::Object;
+use SL::DB::History;
+use SL::DB::ShopImage;
+use SL::DB::File;
+use SL::Helper::UserPreferences;
+use SL::Controller::Helper::ThumbnailCreator qw(file_probe_type);
+use SL::JSON;
+
+use constant RENAME_OK          => 0;
+use constant RENAME_EXISTS      => 1;
+use constant RENAME_NOFILE      => 2;
+use constant RENAME_SAME        => 3;
+use constant RENAME_NEW_VERSION => 4;
+
+sub get {
+  my ($self, %params) = @_;
+  die "no id or dbfile" unless $params{id} || $params{dbfile};
+  $params{dbfile} = SL::DB::Manager::File->get_first(query => [id => $params{id}]) if !$params{dbfile};
+  die 'not found' unless $params{dbfile};
+  $main::lxdebug->message(LXDebug->DEBUG2(), "object_id=".$params{dbfile}->object_id." object_type=".$params{dbfile}->object_type." dbfile=".$params{dbfile});
+  SL::File::Object->new(db_file => $params{dbfile}, id => $params{dbfile}->id, loaded => 1);
+}
+
+sub get_version_count {
+  my ($self, %params) = @_;
+  die "no id or dbfile" unless $params{id} || $params{dbfile};
+  $params{dbfile} = SL::DB::Manager::File->get_first(query => [id => $params{id}]) if !$params{dbfile};
+  die 'not found' unless $params{dbfile};
+  my $backend = $self->_get_backend($params{dbfile}->backend);
+  return $backend->get_version_count(%params);
+}
+
+sub get_all {
+  my ($self, %params) = @_;
+
+  my @files;
+  return @files unless $params{object_type};
+  return @files unless defined($params{object_id});
+
+  my @query = (
+    object_id   => $params{object_id},
+    object_type => $params{object_type}
+  );
+  push @query, (file_name     => $params{file_name})     if $params{file_name};
+  push @query, (file_type     => $params{file_type})     if $params{file_type};
+  push @query, (mime_type     => $params{mime_type})     if $params{mime_type};
+  push @query, (source        => $params{source})        if $params{source};
+  push @query, (print_variant => $params{print_variant}) if $params{print_variant};
+
+  my $sortby = $params{sort_by} || 'itime DESC,file_name ASC';
+
+  @files = @{ SL::DB::Manager::File->get_all(query => [@query], sort_by => $sortby) };
+  map { SL::File::Object->new(db_file => $_, id => $_->id, loaded => 1) } @files;
+}
+
+sub get_all_versions {
+  my ($self, %params) = @_;
+  my @versionobjs;
+  my @fileobjs;
+  if ( $params{dbfile} ) {
+    push @fileobjs, SL::File::Object->new(db_file => $params{dbfile}, id => $params{dbfile}->id, loaded => 1);
+  } else {
+    @fileobjs = $self->get_all(%params);
+  }
+  foreach my $fileobj (@fileobjs) {
+    $main::lxdebug->message(LXDebug->DEBUG2(), "obj=" . $fileobj . " id=" . $fileobj->id." versions=".$fileobj->version_count);
+    my $maxversion = $fileobj->version_count;
+    $fileobj->version($maxversion);
+    push @versionobjs, $fileobj;
+    if ($maxversion > 1) {
+      for my $version (2..$maxversion) {
+        $main::lxdebug->message(LXDebug->DEBUG2(), "clone for version=".($maxversion-$version+1));
+        eval {
+          my $clone = $fileobj->clone;
+          $clone->version($maxversion-$version+1);
+          $clone->newest(0);
+          $main::lxdebug->message(LXDebug->DEBUG2(), "clone version=".$clone->version." mtime=". $clone->mtime);
+          push @versionobjs, $clone;
+          1;
+        } or do {$::lxdebug->message(LXDebug::WARN(), "clone for version=".($maxversion-$version+1) . "failed: " . $@)};
+      }
+    }
+  }
+  return @versionobjs;
+}
+
+sub get_all_count {
+  my ($self, %params) = @_;
+  return 0 unless $params{object_type};
+
+  my @query = (
+    object_id   => $params{object_id},
+    object_type => $params{object_type}
+  );
+  push @query, (file_name     => $params{file_name})     if $params{file_name};
+  push @query, (file_type     => $params{file_type})     if $params{file_type};
+  push @query, (mime_type     => $params{mime_type})     if $params{mime_type};
+  push @query, (source        => $params{source})        if $params{source};
+  push @query, (print_variant => $params{print_variant}) if $params{print_variant};
+
+  my $cnt = SL::DB::Manager::File->get_all_count(query => [@query]);
+  return $cnt;
+}
+
+sub delete_all {
+  my ($self, %params) = @_;
+  return 0 unless defined($params{object_id}) || $params{object_type};
+  my $files = SL::DB::Manager::File->get_all(
+    query => [
+      object_id   => $params{object_id},
+      object_type => $params{object_type}
+    ]
+  );
+  foreach my $file (@{$files}) {
+    $params{dbfile} = $file;
+    $self->delete(%params);
+  }
+}
+
+sub delete {
+  my ($self, %params) = @_;
+  die "no id or dbfile in delete" unless $params{id} || $params{dbfile};
+  my $rc = 0;
+  eval {
+    $rc = SL::DB->client->with_transaction(\&_delete, $self, %params);
+    1;
+  } or do { die $@ };
+  return $rc;
+}
+
+sub _delete {
+  my ($self, %params) = @_;
+  $params{dbfile} = SL::DB::Manager::File->get_first(query => [id => $params{id}]) if !$params{dbfile};
+
+  my $backend = $self->_get_backend($params{dbfile}->backend);
+  if ( $params{dbfile}->file_type eq 'document' && $params{dbfile}->source ne 'created')
+  {
+    ## must unimport
+    my $hist = SL::DB::Manager::History->get_first(
+      where => [
+        addition  => 'IMPORT',
+        trans_id  => $params{dbfile}->object_id,
+        what_done => $params{dbfile}->id
+      ]
+    );
+
+    if ($hist) {
+      if (!$main::auth->assert('import_ar | import_ap', 1)) {
+        die 'no permission to unimport';
+      }
+      my $file = $backend->get_filepath(dbfile => $params{dbfile});
+      $main::lxdebug->message(LXDebug->DEBUG2(), "del file=" . $file . " to=" . $hist->snumbers);
+      File::Copy::copy($file, $hist->snumbers) if $file;
+      $hist->addition('UNIMPORT');
+      $hist->save;
+    }
+  }
+  if ($backend->delete(%params)) {
+    my $do_delete = 0;
+    if ( $params{last} || $params{version} || $params{all_but_notlast} ) {
+      if ( $backend->get_version_count(%params) > 0 ) {
+        $params{dbfile}->mtime(DateTime->now_local);
+        $params{dbfile}->save;
+      } else {
+        $do_delete = 1;
+      }
+    } else {
+      $do_delete = 1;
+    }
+    $params{dbfile}->delete if $do_delete;
+    return 1;
+  }
+  return 0;
+}
+
+sub save {
+  my ($self, %params) = @_;
+
+  my $obj;
+  eval {
+    $obj = SL::DB->client->with_transaction(\&_save, $self, %params);
+    1;
+  } or do { die $@ };
+  return $obj;
+}
+
+sub _save {
+  my ($self, %params) = @_;
+  my $file = $params{dbfile};
+  my $exists = 0;
+
+  if ($params{id}) {
+    $file = SL::DB::File->new(id => $params{id})->load;
+    die 'dbfile not exists'     unless $file;
+  } elsif (!$file) {
+  $main::lxdebug->message(LXDebug->DEBUG2(), "obj_id=" .$params{object_id});
+    die 'no object type set'    unless $params{object_type};
+    die 'no object id set'      unless defined($params{object_id});
+
+    $exists = $self->get_all_count(%params);
+    die 'filename still exist' if $exists && $params{fail_if_exists};
+    if ($exists) {
+      my ($obj1) = $self->get_all(%params);
+      $file = $obj1->db_file;
+    } else {
+      $file = SL::DB::File->new();
+      $file->assign_attributes(
+        object_id      => $params{object_id},
+        object_type    => $params{object_type},
+        source         => $params{source},
+        file_type      => $params{file_type},
+        file_name      => $params{file_name},
+        mime_type      => $params{mime_type},
+        title          => $params{title},
+        description    => $params{description},
+        print_variant  => $params{print_variant},
+      );
+      $file->itime($params{mtime})    if $params{mtime};
+      $params{itime} = $params{mtime} if $params{mtime};
+    }
+  } else {
+    $exists = 1;
+  }
+  if ($exists) {
+    #change attr on existing file
+    $file->file_name  ($params{file_name})   if $params{file_name};
+    $file->mime_type  ($params{mime_type})   if $params{mime_type};
+    $file->title      ($params{title})       if $params{title};
+    $file->description($params{description}) if $params{description};
+  }
+  if ( !$file->backend ) {
+    $file->backend($self->_get_backend_by_file_type($file));
+    # load itime for new file
+    $file->save->load;
+  }
+
+  $file->mtime(DateTime->now_local) unless $params{mtime};
+  $file->mtime($params{mtime}     ) if     $params{mtime};
+
+  my $backend = $self->_get_backend($file->backend);
+  $params{dbfile} = $file;
+  $backend->save(%params);
+
+  $file->save;
+  #ShopImage
+  if($file->object_type eq "shop_image"){
+    my $image_content = $params{file_contents};
+    my $thumbnail = file_probe_type($image_content);
+    my $shopimage = SL::DB::ShopImage->new();
+    $shopimage->assign_attributes(
+                                  file_id                => $file->id,
+                                  thumbnail_content      => $thumbnail->{thumbnail_img_content},
+                                  org_file_height        => $thumbnail->{file_image_height},
+                                  org_file_width         => $thumbnail->{file_image_width},
+                                  thumbnail_content_type => $thumbnail->{thumbnail_img_content_type},
+                                  object_id              => $file->object_id,
+                                 );
+    $shopimage->save;
+  }
+  if ($params{file_type} eq 'document' && $params{source} ne 'created') {
+    SL::DB::History->new(
+      addition    => 'IMPORT',
+      trans_id    => $params{object_id},
+      snumbers    => $params{file_path},
+      employee_id => SL::DB::Manager::Employee->current->id,
+      what_done   => $params{dbfile}->id
+    )->save();
+  }
+  return $params{obj} if $params{dbfile} && $params{obj};
+  return SL::File::Object->new(db_file => $file, id => $file->id, loaded => 1);
+}
+
+sub rename {
+  my ($self, %params) = @_;
+  return RENAME_NOFILE unless $params{id} || $params{dbfile};
+  my $file = $params{dbfile};
+  $file = SL::DB::Manager::File->get_first(query => [id => $params{id}]) if !$file;
+  return RENAME_NOFILE unless $file;
+
+  $main::lxdebug->message(LXDebug->DEBUG2(), "rename id=" . $file->id . " to=" . $params{to});
+  if ($params{to}) {
+    return RENAME_SAME   if $params{to} eq $file->file_name;
+    return RENAME_EXISTS if $self->get_all_count( object_id     => $file->object_id,
+                                                  object_type   => $file->object_type,
+                                                  mime_type     => $file->mime_type,
+                                                  source        => $file->source,
+                                                  file_type     => $file->file_type,
+                                                  file_name     => $params{to}
+                                                ) > 0;
+
+    my $backend = $self->_get_backend($file->backend);
+    $backend->rename(dbfile => $file) if $backend;
+    $file->file_name($params{to});
+    $file->save;
+  }
+  return RENAME_OK;
+}
+
+sub get_backend_class {
+  my ($self, $backendname) = @_;
+  die "no backend name set" unless $backendname;
+  $self->_get_backend($backendname);
+}
+
+sub get_other_sources {
+  my ($self) = @_;
+  my $pref = SL::Helper::UserPreferences->new(namespace => 'file_sources');
+  $pref->login("#default#");
+  my @sources;
+  foreach my $tuple (@{ $pref->get_all() }) {
+    my %lkeys  = %{ SL::JSON::from_json($tuple->{value}) };
+    my $source = {
+      'name'        => $tuple->{key},
+      'description' => $lkeys{desc},
+      'directory'   => $lkeys{dir}
+    };
+    push @sources, $source;
+  }
+  return @sources;
+}
+
+sub sync_from_backend {
+  my ($self, %params) = @_;
+  return unless $params{file_type};
+  my $file = SL::DB::File->new;
+  $file->file_type($params{file_type});
+  my $backend = $self->_get_backend($self->_get_backend_by_file_type($file));
+  return unless $backend;
+  $backend->sync_from_backend(%params);
+}
+
+#
+# internal
+#
+sub _get_backend {
+  my ($self, $backend_name) = @_;
+  my $class = 'SL::File::Backend::' . $backend_name;
+  my $obj   = undef;
+  die $::locale->text('no backend enabled') if $backend_name eq 'None';
+  eval {
+    eval "require $class";
+    $obj = $class->new;
+    die $::locale->text('backend "#1" not enabled',$backend_name) unless $obj->enabled;
+    1;
+  } or do {
+    if ( $obj ) {
+      die $@;
+    } else {
+      die $::locale->text('backend "#1" not found',$backend_name);
+    }
+  };
+  return $obj;
+}
+
+sub _get_backend_by_file_type {
+  my ($self, $dbfile) = @_;
+
+  $main::lxdebug->message(LXDebug->DEBUG2(), "_get_backend_by_file_type=" .$dbfile." type=".$dbfile->file_type);
+  return "Filesystem" unless $dbfile;
+  return $::instance_conf->get_doc_storage_for_documents   if $dbfile->file_type eq 'document';
+  return $::instance_conf->get_doc_storage_for_attachments if $dbfile->file_type eq 'attachment';
+  return $::instance_conf->get_doc_storage_for_images      if $dbfile->file_type eq 'image';
+  return "Filesystem";
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::File - The intermediate Layer for handling files
+
+=head1 SYNOPSIS
+
+  # In a controller or helper ( see SL::Controller::File or SL::Helper::File )
+  # you can create, remove, delete etc. a file in a backend independent way
+
+  my $file  = SL::File->save(
+                     object_id     => $self->object_id,
+                     object_type   => $self->object_type,
+                     mime_type     => 'application/pdf',
+                     file_type     => 'document',
+                     file_contents => 'this is no pdf');
+
+  my $file1  = SL::File->get(id => $id);
+  SL::File->delete(id => $id);
+  SL::File->delete(dbfile => $file1);
+  SL::File->delete_all(object_id   => $object_id,
+                       object_type => $object_type,
+                       file_type   => $filetype      # may be optional
+                      );
+  SL::File->rename(id => $id,to => $newname);
+  my $files1 = SL::File->get_all(object_id   => $object_id,
+                                 object_type => $object_type,
+                                 file_type   => 'image',   # may be optional
+                                 source      => 'uploaded' # may be optional
+                                );
+
+  # Alternativelly some operation can be done with the filemangement object wrapper
+  # and additional oparations see L<SL::File::Object>
+
+=head1 OVERVIEW
+
+The Filemanagemt can handle files in a storage independent way. Internal the File
+use the configured storage backend for the type of file.
+These backends must be configured in L<SL::Controller::ClientConfig> or an extra database table.
+
+There are three types of files:
+
+=over 2
+
+=item - document,
+
+which can be generated files (for sales), scanned files or uploaded files (for purchase) for an ERP-object.
+They can exist in different versions. The versioning is handled implicit. All versions of a file may be
+deleted by the user if she/he is allowed to do this.
+
+=item - attachment,
+
+which have additional information for an ERP-objects. They are uploadable. If a filename still exists
+on a ERP-Object the new uploaded file is a new version of this or it must be renamed by user.
+
+There are generic attachments for a specific document group (like sales_invoices). This attachments can be
+combinide/merged with the document-file in the time of printing.
+Today only PDF-Attachmnets can be merged with the generated document-PDF.
+
+=item - image,
+
+they are like attachments, but they may be have thumbnails for displaying.
+So the must have an image format like png,jpg. The versioning is like attachments
+
+=back
+
+For each type of files the backend can configured in L<SL::Controller::ClientConfig>.
+
+The files have also the parameter C<Source>:
+
+=over 2
+
+=item - created, generated by LaTeX
+
+=item - uploaded
+
+=item - scanner, import from scanner
+
+( or scanner1, scanner2 if there are different scanner, be configurable via UserPreferences )
+
+=item - email, received by email and imported by hand or automatic.
+
+=back
+
+The files from source 'scanner' or 'email' are not allowed to delete else they must be send back to the sources.
+This means they are moved back into the correspondent source directories.
+
+The scanner and email import must be configured  via Table UserPreferences:
+
+=begin text
+
+ id |  login  |  namespace   | version |   key    |                        value
+----+---------+--------------+---------+----------+------------------------------------------------------
+  1 | default | file_sources | 0.00000 | scanner1 | {"dir":"/var/tmp/scanner1","desc":"Scanner Einkauf" }
+  2 | default | file_sources | 0.00000 | emails   | {"dir":"/var/tmp/emails"  ,"desc":"Empfangene Mails"}
+
+=end text
+
+.
+
+The Fileinformation is stored in the table L<SL::DB::File> for saving the information.
+The modul and object_id describe the link to the object.
+
+The interface SL::File:Object encapsulate SL::DB:File, see L<SL::DB::Object>
+
+The storage backends are extra classes which depends from L<SL::File::Backend>.
+So additional backend classes can be added.
+
+The implementation of versioning is done in the different backends.
+
+=head1 METHODS
+
+=over 4
+
+=item C<save>
+
+Creates a new SL::DB:File object or save an existing object for a specific backend depends of the C<file_type>
+and config, like
+
+=begin text
+
+          SL::File->save(
+                         object_id    => $self->object_id,
+                         object_type  => $self->object_type,
+                         content_type => 'application/pdf'
+                        );
+
+=end text
+
+.
+
+The file data is stored in the backend. If the file_type is "document" and the source is not "created" the file is imported,
+so in the history the import is documented also as a hint to can unimport the file later.
+
+Available C<PARAMS>:
+
+=over 4
+
+=item C<id>
+
+The id of SL::DB::File for an existing file
+
+=item C<object_id>
+
+The Id of the ERP-object for a new file.
+
+=item C<object_type>
+
+The Type of the ERP-object like "sales_quotation" for a new file. A clear mapping to the class/model exists in the controller.
+
+=item C<file_type>
+
+The type may be "document", "attachment" or "image" for a new file.
+
+=item C<source>
+
+The type may be "created", "uploaded" or email sources or scanner sources for a new file.
+
+=item C<file_name>
+
+The file_name of the file for a new file. This name is used in the WebGUI and as name for download.
+
+=item C<mime_type>
+
+The mime_type of a new file. This is used for downloading or for email attachments.
+
+=item C<description> or C<title>
+
+The description or title of a new file. This must be discussed if this attribute is needed.
+
+=back
+
+=item C<delete PARAMS>
+
+The file data is deleted in the backend. If the file comes from source 'scanner' or 'email'
+they moved back to the source folders. This is documented in the history.
+
+Available C<PARAMS>:
+
+=over 4
+
+=item C<id>
+
+The id of SL::DB::File
+
+=item C<dbfile>
+
+As alternative if the SL::DB::File as object is available.
+
+=back
+
+=item C<delete_all PARAMS>
+
+All file data of an ERP-object is deleted in the backend.
+
+=over 4
+
+=item C<object_id>
+
+The Id of the ERP-object.
+
+=item C<object_type>
+
+The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
+
+=back
+
+=item C<rename PARAMS>
+
+The Filename of the file is changed
+
+Available C<PARAMS>:
+
+=over 4
+
+=item C<id>
+
+The id of SL::DB::File
+
+=item C<to>
+
+The new filename
+
+=back
+
+=item C<get PARAMS>
+
+The actual file object is retrieved. The id of the object is needed.
+
+Available C<PARAMS>:
+
+=over 4
+
+=item C<id>
+
+The id of SL::DB::File
+
+=back
+
+=item C<get_all PARAMS>
+
+All last versions of file data objects which are related to an ERP-Document,Part,Customer,Vendor,... are retrieved.
+
+Available C<PARAMS>:
+
+=over 4
+
+=item C<object_id>
+
+The Id of the ERP-object.
+
+=item C<object_type>
+
+The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
+
+=item C<file_type>
+
+The type may be "document", "attachment" or "image". This parameter is optional.
+
+=item C<file_name>
+
+The name of the file . This parameter is optional.
+
+=item C<mime_type>
+
+The MIME type of the file . This parameter is optional.
+
+=item C<source>
+
+The type may be "created", "uploaded" or email sources or scanner soureces. This parameter is optional.
+
+=item C<sort_by>
+
+An optional parameter in which sorting the files are retrieved. Default is decrementing itime and ascending filename
+
+=back
+
+=item C<get_all_versions PARAMS>
+
+All versions of file data objects which are related to an ERP-Document,Part,Customer,Vendor,... are retrieved.
+If only the versions of one file are wanted, additional parameter like file_name must be set.
+If the param C<dbfile> set, only the versions of this file are returned.
+
+Available C<PARAMS> ar the same as L<get_all>
+
+
+
+=item C<get_all_count PARAMS>
+
+The count of available files is returned.
+Available C<PARAMS> ar the same as L<get_all>
+
+
+=item C<get_content PARAMS>
+
+The data of a file can retrieved. A reference to the data is returned.
+
+Available C<PARAMS>:
+
+=over 4
+
+=item C<id>
+
+The id of SL::DB::File
+
+=item C<dbfile>
+
+If no Id exists the object SL::DB::File as param.
+
+=back
+
+=item C<get_file_path PARAMS>
+
+Sometimes it is more useful to have a path to the file not the contents. If the backend has not stored the content as file
+it is in the responsibility of the backend to create a tempory session file.
+
+Available C<PARAMS>:
+
+=over 4
+
+=item C<id>
+
+The id of SL::DB::File
+
+=item C<dbfile>
+
+If no Id exists the object SL::DB::File as param.
+
+=back
+
+=item C<get_other_sources>
+
+A helpful method to get the sources for scanner and email from UserPreferences. This method is need from SL::Controller::File
+
+=item C<sync_from_backend>
+
+For Backends which may be changed outside of kivitendo a synchronization of the database is done.
+This sync must be triggered by a periodical task.
+
+Needed C<PARAMS>:
+
+=over 4
+
+=item C<file_type>
+
+The synchronization is done file_type by file_type.
+
+=back
+
+=back
+
+=head1 AUTHOR
+
+Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
+
+=cut
diff --git a/SL/File/Backend.pm b/SL/File/Backend.pm
new file mode 100644 (file)
index 0000000..2f1da83
--- /dev/null
@@ -0,0 +1,201 @@
+package SL::File::Backend;
+
+use strict;
+
+use parent qw(Rose::Object);
+
+sub store { die 'store needs to be implemented' }
+
+sub delete { die 'delete needs to be implemented' }
+
+sub rename { die 'rename needs to be implemented' }
+
+sub get_content { die 'get_content needs to be implemented' }
+
+sub get_filepath { die 'get_filepath needs to be implemented' }
+
+sub get_mtime { die 'get_mtime needs to be implemented' }
+
+sub get_version_count { die 'get_version_count needs to be implemented' }
+
+sub enabled { 0; }
+
+sub sync_from_backend { }
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::File::Backend  - Base class for file storage backend
+
+=head1 SYNOPSIS
+
+See the synopsis of L<SL::File> and L<SL::File::Object>
+
+=head1 OVERVIEW
+
+The most methods must be overridden by the specific storage backend
+
+See also the overview of L<SL::File> and L<SL::File::Object>.
+
+
+=head1 METHODS
+
+=over 4
+
+=item C<store PARAMS>
+
+The file data is stored in the backend.
+
+Available C<PARAMS>:
+
+=over 4
+
+=item C<dbfile>
+
+The object SL::DB::File as param.
+
+=item C<file_contents>
+
+The data of the file to store
+
+=item C<file_path>
+
+If the data is still in a file, the contents of this file is copied.
+
+=back
+
+If both parameter C<file_contents> and C<file_path> exists,
+the backend is responsible in which way the contents is fetched.
+
+If the file exists the backend is responsible to decide to save a new version of the file or override the
+latest existing file.
+
+=item C<delete PARAMS>
+
+The file data is deleted in the backend.
+
+Available C<PARAMS>:
+
+=over 4
+
+=item C<dbfile>
+
+The object SL::DB::File as param.
+
+=item C<last>
+
+If this parameter is set only the latest version of the file are deleted.
+
+=item C<all_but_notlast>
+
+If this parameter is set all versions of the file are deleted except the latest version.
+
+If none of the two parameters C<all_versions> or C<all__but_notlast> is set
+all version of the file are deleted.
+
+=back
+
+=item C<rename PARAMS>
+
+The Filename of the file is changed. If the backend is not dependent from the filename
+nothing must happens. The rename must work on all versions of the file.
+
+Available C<PARAMS>:
+
+=over 4
+
+=item C<dbfile>
+
+The object SL::DB::File as param.
+
+=back
+
+=item C<get_version_count PARAMS>
+
+The count of the available versions of a file will returned.
+The versions are numbered from 1 up to the returned value
+
+Available C<PARAMS>:
+
+=over 4
+
+=item C<dbfile>
+
+=back
+
+=item C<get_mtime PARAMS>
+
+Available C<PARAMS>:
+
+=over 4
+
+=item C<dbfile>
+
+The object SL::DB::File as param.
+
+=item C<version>
+
+The version number of the file for which the modification timestamp is wanted.
+If no version set or version is 0 , the mtime of the latest version is returned.
+
+=back
+
+=item C<get_content PARAMS>
+
+For downloading or printing the real data need to retrieve.
+A reference of the data must be returned.
+
+Available C<PARAMS>:
+
+=over 4
+
+=item C<dbfile>
+
+The object SL::DB::File as param.
+
+=back
+
+=item C<get_file_path PARAMS>
+
+If the backend has files as storage, the file path can returned.
+If a file is not available in the backend a temporary file must be created with the contents.
+
+Available C<PARAMS>:
+
+=over 4
+
+=item C<dbfile>
+
+The object SL::DB::File as param.
+
+=back
+
+=item C<enabled>
+
+returns 1 if the backend is enabled and has all config to work.
+In other cases it must return 0
+
+=item C<sync_from_backend>
+
+For Backends which may be changed outside of kivitendo a synchronization of the database is done.
+Normally the backend is responsible to actualise the data if it needed.
+This interface can be used if a long work must be done and runs in a extra task.
+
+=back
+
+=head1 SEE ALSO
+
+L<SL::File>, L<SL::File::Object>
+
+=head1 AUTHOR
+
+Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
+
+=cut
diff --git a/SL/File/Backend/Filesystem.pm b/SL/File/Backend/Filesystem.pm
new file mode 100644 (file)
index 0000000..758142b
--- /dev/null
@@ -0,0 +1,231 @@
+package SL::File::Backend::Filesystem;
+
+use strict;
+
+use parent qw(SL::File::Backend);
+use SL::DB::File;
+use File::Copy;
+use File::Slurp;
+use File::stat;
+use File::Path qw(make_path);
+
+#
+# public methods
+#
+
+sub delete {
+  my ($self, %params) = @_;
+  die "no dbfile in backend delete" unless $params{dbfile};
+  my $last_version  = $params{dbfile}->backend_data;
+  my $first_version = 1;
+  $last_version     = 0                               if $params{last};
+  $last_version     = $params{dbfile}->backend_data-1 if $params{all_but_notlast};
+  $last_version     = $params{version}                if $params{version};
+  $first_version    = $params{version}                if $params{version};
+
+  if ($last_version > 0 ) {
+    for my $version ( $first_version..$last_version) {
+      my $file_path = $self->_filesystem_path($params{dbfile},$version);
+      unlink($file_path);
+    }
+    if ($params{version}) {
+      for my $version ( $last_version+1 .. $params{dbfile}->backend_data) {
+        my $from = $self->_filesystem_path($params{dbfile},$version);
+        my $to   = $self->_filesystem_path($params{dbfile},$version - 1);
+        die "file not exists in backend delete" unless -f $from;
+        rename($from,$to);
+      }
+      $params{dbfile}->backend_data($params{dbfile}->backend_data-1);
+    }
+    elsif ($params{all_but_notlast}) {
+      my $from = $self->_filesystem_path($params{dbfile},$params{dbfile}->backend_data);
+      my $to   = $self->_filesystem_path($params{dbfile},1);
+      die "file not exists in backend delete" unless -f $from;
+      rename($from,$to);
+      $params{dbfile}->backend_data(1);
+    } else {
+      $params{dbfile}->backend_data(0);
+    }
+    unless ($params{dbfile}->backend_data) {
+      my $dir_path = $self->_filesystem_path($params{dbfile});
+      rmdir($dir_path);
+    }
+  } else {
+    my $file_path = $self->_filesystem_path($params{dbfile},$params{dbfile}->backend_data);
+    die "file not exists in backend delete" unless -f $file_path;
+    unlink($file_path);
+    $params{dbfile}->backend_data($params{dbfile}->backend_data-1);
+  }
+  return 1;
+}
+
+sub rename {
+}
+
+sub save {
+  my ($self, %params) = @_;
+  die 'dbfile not exists' unless $params{dbfile};
+  my $dbfile = $params{dbfile};
+  die 'no file contents' unless $params{file_path} || $params{file_contents};
+
+  # Do not save and do not create a new version of the document if file size of last version is the same.
+  if ($dbfile->source eq 'created' && $self->get_version_count(dbfile => $dbfile)) {
+    my $new_file_size  = $params{file_path} ? stat($params{file_path})->size : length($params{file_contents});
+    my $last_file_size = stat($self->_filesystem_path($dbfile))->size;
+
+    return 1 if $last_file_size == $new_file_size;
+  }
+
+  $dbfile->backend_data(0) unless $dbfile->backend_data;
+  $dbfile->backend_data($dbfile->backend_data*1+1);
+  $dbfile->save->load;
+
+  my $tofile = $self->_filesystem_path($dbfile);
+  if ($params{file_path} && -f $params{file_path}) {
+    File::Copy::copy($params{file_path}, $tofile);
+  }
+  elsif ($params{file_contents}) {
+    open(OUT, "> " . $tofile);
+    print OUT $params{file_contents};
+    close(OUT);
+  }
+  if ($params{mtime}) {
+    utime($params{mtime}, $params{mtime}, $tofile);
+  }
+  return 1;
+}
+
+sub get_version_count {
+  my ($self, %params) = @_;
+  die "no dbfile" unless $params{dbfile};
+  return $params{dbfile}->backend_data//0 * 1;
+}
+
+sub get_mtime {
+  my ($self, %params) = @_;
+  die "no dbfile" unless $params{dbfile};
+  die "unknown version" if $params{version} &&
+                          ($params{version} < 0 || $params{version} > $params{dbfile}->backend_data);
+  my $path = $self->_filesystem_path($params{dbfile}, $params{version});
+
+  die "No file found at $path. Expected: $params{dbfile}{file_name}, file.id: $params{dbfile}{id}" if !-f $path;
+
+  my $dt = DateTime->from_epoch(epoch => stat($path)->mtime, time_zone => $::locale->get_local_time_zone()->name, locale => $::lx_office_conf{system}->{language})->clone();
+  return $dt;
+}
+
+sub get_filepath {
+  my ($self, %params) = @_;
+  die "no dbfile" unless $params{dbfile};
+  my $path = $self->_filesystem_path($params{dbfile},$params{version});
+
+  die "No file found at $path. Expected: $params{dbfile}{file_name}, file.id: $params{dbfile}{id}" if !-f $path;
+
+  return $path;
+}
+
+sub get_content {
+  my ($self, %params) = @_;
+  my $path = $self->get_filepath(%params);
+  return "" unless $path;
+  my $contents = File::Slurp::read_file($path);
+  return \$contents;
+}
+
+sub enabled {
+  return 0 unless $::instance_conf->get_doc_files;
+  return 0 unless $::lx_office_conf{paths}->{document_path};
+  return 0 unless -d $::lx_office_conf{paths}->{document_path};
+  return 1;
+}
+
+sub sync_from_backend {
+  my ($self, %params) = @_;
+  my @query = (file_type => $params{file_type});
+  push @query, (file_name => $params{file_name}) if $params{file_name};
+  push @query, (mime_type => $params{mime_type}) if $params{mime_type};
+  push @query, (source    => $params{source})    if $params{source};
+
+  my $sortby = $params{sort_by} || 'itime DESC,file_name ASC';
+
+  my @files = @{ SL::DB::Manager::File->get_all(query => [@query], sort_by => $sortby) };
+  for (@files) {
+    $main::lxdebug->message(LXDebug->DEBUG2(), "file id=" . $_->id." version=".$_->backend_data);
+    my $newversion = $_->backend_data;
+    for my $version ( reverse 1 .. $_->backend_data ) {
+      my $path = $self->_filesystem_path($_, $version);
+      $main::lxdebug->message(LXDebug->DEBUG2(), "path=".$path." exists=".( -f $path?1:0));
+      last if -f $path;
+      $newversion = $version - 1;
+    }
+    $main::lxdebug->message(LXDebug->DEBUG2(), "newversion=".$newversion." version=".$_->backend_data);
+    if ( $newversion < $_->backend_data ) {
+      $_->backend_data($newversion);
+      $_->save   if $newversion >  0;
+      $_->delete if $newversion <= 0;
+    }
+  }
+
+}
+
+#
+# internals
+#
+
+sub _filesystem_path {
+  my ($self, $dbfile, $version) = @_;
+
+  die "No files backend enabled" unless $::instance_conf->get_doc_files || $::lx_office_conf{paths}->{document_path};
+
+  # use filesystem with depth 3
+  $version    = $dbfile->backend_data if !$version || $version < 1 || $version > $dbfile->backend_data;
+  my $iddir   = sprintf("%04d", $dbfile->id % 1000);
+  my $path    = File::Spec->catdir($::lx_office_conf{paths}->{document_path}, $::auth->client->{id}, $iddir, $dbfile->id);
+  if (!-d $path) {
+    File::Path::make_path($path, { chmod => 0770 });
+  }
+  return $path if !$version;
+  return File::Spec->catdir($path, $dbfile->id . '_' . $version);
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::File::Backend::Filesystem  - Filesystem class for file storage backend
+
+=head1 SYNOPSIS
+
+See the synopsis of L<SL::File::Backend>.
+
+=head1 OVERVIEW
+
+This specific storage backend use a Filesystem which is only accessed by this interface.
+This is the big difference to the Webdav backend where the files can be accessed without the control of that backend.
+This backend use the database id of the SL::DB::File object as filename. The filesystem has up to 1000 subdirectories
+to store the files not to flat in the filesystem. In this Subdirectories for each file an additional subdirectory exists
+for the versions of this file.
+
+The Versioning is done via a Versionnumber which is incremented by one for each version.
+So the Version 2 of the file with the database id 4 is stored as path {root}/0004/4/4_2.
+
+
+=head1 METHODS
+
+See methods of L<SL::File::Backend>.
+
+=head1 SEE ALSO
+
+L<SL::File::Backend>
+
+=head1 AUTHOR
+
+Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
+
+=cut
diff --git a/SL/File/Backend/Webdav.pm b/SL/File/Backend/Webdav.pm
new file mode 100644 (file)
index 0000000..bf8f9af
--- /dev/null
@@ -0,0 +1,341 @@
+package SL::File::Backend::Webdav;
+
+use strict;
+
+use parent qw(SL::File::Backend);
+use SL::DB::File;
+
+use SL::System::Process;
+use File::Copy;
+use File::Slurp;
+use File::Basename;
+use File::Path qw(make_path);
+use File::MimeInfo::Magic;
+use File::stat;
+
+#
+# public methods
+#
+
+sub delete {
+  my ($self, %params) = @_;
+  $main::lxdebug->message(LXDebug->DEBUG2(), "del in backend " . $self . "  file " . $params{dbfile});
+  $main::lxdebug->message(LXDebug->DEBUG2(), "file id=" . $params{dbfile}->id * 1);
+  return 0 unless $params{dbfile};
+  my ($file_path, undef, undef) = $self->webdav_path($params{dbfile});
+  unlink($file_path);
+  return 1;
+}
+
+sub rename {
+  my ($self, %params) = @_;
+  return 0 unless $params{dbfile};
+  my (undef, $oldwebdavname) = split(/:/, $params{dbfile}->location, 2);
+  my ($tofile, $basepath, $basename) = $self->webdav_path($params{dbfile});
+  my $fromfile = File::Spec->catfile($basepath, $oldwebdavname);
+  $main::lxdebug->message(LXDebug->DEBUG2(), "renamefrom=" . $fromfile . " to=" . $tofile);
+  move($fromfile, $tofile);
+}
+
+sub save {
+  my ($self, %params) = @_;
+  die 'dbfile not exists' unless $params{dbfile};
+  $main::lxdebug->message(LXDebug->DEBUG2(), "in backend " . $self . "  file " . $params{dbfile});
+  $main::lxdebug->message(LXDebug->DEBUG2(), "file id=" . $params{dbfile}->id);
+  my $dbfile = $params{dbfile};
+  die 'no file contents' unless $params{file_path} || $params{file_contents};
+
+  if ($params{dbfile}->id * 1 == 0) {
+
+    # new element: need id for file
+    $params{dbfile}->save;
+  }
+  my ($tofile, undef, $basename) = $self->webdav_path($params{dbfile});
+  if ($params{file_path} && -f $params{file_path}) {
+    copy($params{file_path}, $tofile);
+  }
+  elsif ($params{file_contents}) {
+    open(OUT, "> " . $tofile);
+    print OUT $params{file_contents};
+    close(OUT);
+  }
+  return 1;
+}
+
+sub get_version_count {
+  my ($self, %params) = @_;
+  die "no dbfile" unless $params{dbfile};
+  ## TODO
+  return 1;
+}
+
+sub get_mtime {
+  my ($self, %params) = @_;
+  die "no dbfile" unless $params{dbfile};
+  $main::lxdebug->message(LXDebug->DEBUG2(), "version=" .$params{version});
+  my ($path, undef, undef) = $self->webdav_path($params{dbfile});
+  die "No file found in Backend: " . $path unless -f $path;
+  my $dt = DateTime->from_epoch(epoch => stat($path)->mtime, time_zone => $::locale->get_local_time_zone()->name)->clone();
+  $main::lxdebug->message(LXDebug->DEBUG2(), "dt=" .$dt);
+  return $dt;
+}
+
+sub get_filepath {
+  my ($self, %params) = @_;
+  die "no dbfile" unless $params{dbfile};
+  my ($path, undef, undef) = $self->webdav_path($params{dbfile});
+  die "No file found in Backend: " . $path unless -f $path;
+  return $path;
+}
+
+sub get_content {
+  my ($self, %params) = @_;
+  my $path = $self->get_filepath(%params);
+  return "" unless $path;
+  my $contents = File::Slurp::read_file($path);
+  return \$contents;
+}
+
+sub sync_from_backend {
+  my ($self, %params) = @_;
+  return unless $params{file_type};
+
+  $self->sync_all_locations(%params);
+
+}
+
+sub enabled {
+  return $::instance_conf->get_doc_webdav;
+}
+
+#
+# internals
+#
+
+my %type_to_path = (
+  sales_quotation             => 'angebote',
+  sales_order                 => 'bestellungen',
+  request_quotation           => 'anfragen',
+  purchase_order              => 'lieferantenbestellungen',
+  sales_delivery_order        => 'verkaufslieferscheine',
+  purchase_delivery_order     => 'einkaufslieferscheine',
+  credit_note                 => 'gutschriften',
+  invoice                     => 'rechnungen',
+  invoice_for_advance_payment => 'rechnungen',
+  final_invoice               => 'rechnungen',
+  purchase_invoice            => 'einkaufsrechnungen',
+  part                        => 'waren',
+  service                     => 'dienstleistungen',
+  assembly                    => 'erzeugnisse',
+  letter                      => 'briefe',
+  general_ledger              => 'dialogbuchungen',
+  gl_transaction              => 'dialogbuchungen',
+  accounts_payable            => 'kreditorenbuchungen',
+  shop_image                  => 'shopbilder',
+  customer                    => 'kunden',
+  vendor                      => 'lieferanten',
+);
+
+my %type_to_model = (
+  sales_quotation             => 'Order',
+  sales_order                 => 'Order',
+  request_quotation           => 'Order',
+  purchase_order              => 'Order',
+  sales_delivery_order        => 'DeliveryOrder',
+  purchase_delivery_order     => 'DeliveryOrder',
+  credit_note                 => 'Invoice',
+  invoice                     => 'Invoice',
+  invoice_for_advance_payment => 'Invoice',
+  final_invoice               => 'Invoice',
+  purchase_invoice            => 'PurchaseInvoice',
+  part                        => 'Part',
+  service                     => 'Part',
+  assembly                    => 'Part',
+  letter                      => 'Letter',
+  general_ledger              => 'GLTransaction',
+  gl_transaction              => 'GLTransaction',
+  accounts_payable            => 'GLTransaction',
+  shop_image                  => 'Part',
+  customer                    => 'Customer',
+  vendor                      => 'Vendor',
+);
+
+my %model_to_number = (
+  Order           => 'ordnumber',
+  DeliveryOrder   => 'ordnumber',
+  Invoice         => 'invnumber',
+  PurchaseInvoice => 'invnumber',
+  Part            => 'partnumber',
+  Letter          => 'letternumber',
+  GLTransaction   => 'reference',
+  ShopImage       => 'partnumber',
+  Customer        => 'customernumber',
+  Vendor          => 'vendornumber',
+);
+
+sub webdav_path {
+  my ($self, $dbfile) = @_;
+
+  #die "No webdav backend enabled" unless $::instance_conf->get_webdav;
+
+  my $type = $type_to_path{ $dbfile->object_type };
+
+  die "Unknown type" unless $type;
+
+  my $number = $dbfile->backend_data;
+  if ($number eq '') {
+    $number = $self->_get_number_from_model($dbfile);
+    $dbfile->backend_data($number);
+    $dbfile->save;
+  }
+  $main::lxdebug->message(LXDebug->DEBUG2(), "file_name=" . $dbfile->file_name ." number=".$number);
+
+  my @fileparts = split(/_/, $dbfile->file_name);
+  my $number_ext = pop @fileparts;
+  my ($maynumber, $ext) = split(/\./, $number_ext, 2);
+  push @fileparts, $maynumber if $maynumber ne $number;
+
+  my $basename = join('_', @fileparts);
+
+  my $path = File::Spec->catdir($self->get_rootdir, "webdav", $::auth->client->{id}, $type, $number);
+  if (!-d $path) {
+    File::Path::make_path($path, { chmod => 0770 });
+  }
+  my $fname = $basename . '_' . $number . '_' . $dbfile->itime->strftime('%Y%m%d_%H%M%S');
+  $fname .= '.' . $ext if $ext;
+
+  $main::lxdebug->message(LXDebug->DEBUG2(), "webdav path=" . $path . " filename=" . $fname);
+
+  return (File::Spec->catfile($path, $fname), $path, $fname);
+}
+
+sub get_rootdir { SL::System::Process::exe_dir() }
+
+sub _get_number_from_model {
+  my ($self, $dbfile) = @_;
+
+  my $class = 'SL::DB::' . $type_to_model{ $dbfile->object_type };
+  eval "require $class";
+  my $obj = $class->new(id => $dbfile->object_id)->load;
+  die 'no object found' unless $obj;
+  my $numberattr = $model_to_number{ $type_to_model{ $dbfile->object_type } };
+  return $obj->$numberattr;
+}
+
+#
+# TODO not fully imlemented and tested
+#
+sub sync_all_locations {
+  my ($self, %params) = @_;
+
+  my %dateparms = (dateformat => 'yyyymmdd');
+
+  foreach my $type (keys %type_to_path) {
+
+    my @query = (
+      file_type => $params{file_type},
+      object_type    => $type
+    );
+    my @oldfiles = @{ SL::DB::Manager::File->get_all(
+        query => [
+          file_type => $params{file_type},
+          object_type    => $type
+        ]
+      )
+    };
+
+    my $path = File::Spec->catdir($self->get_rootdir, "webdav", $::auth->client->{id},$type_to_path{$type});
+
+    if (opendir my $dir, $path) {
+      foreach my $file (sort { lc $a cmp lc $b }
+        map { decode("UTF-8", $_) } readdir $dir)
+      {
+        next if (($file eq '.') || ($file eq '..'));
+
+        my $fname = $file;
+        $fname =~ s|.*/||;
+
+        my ($filename, $number, $date, $time_ext) = split(/_/, $fname);
+        my ($time, $ext) = split(/\./, $time_ext, 2);
+
+        $time = substr($time, 0, 2) . ':' . substr($time, 2, 2) . ':' . substr($time, 4, 2);
+
+        #my @found = grep { $_->backend_data eq $fname } @oldfiles;
+        #if (scalar(@found) > 0) {
+        #  @oldfiles = grep { $_ != @found[0] } @oldfiles;
+        #}
+        #else {
+          my $dbfile = SL::DB::File->new();
+          my $class  = 'SL::DB::Manager::' . $type_to_model{$type};
+          my $obj =
+            $class->find_by(
+            $model_to_number{ $type_to_model{$type} } => $number);
+          if ($obj) {
+
+            my $mime_type = File::MimeInfo::Magic::magic(File::Spec->catfile($path, $fname));
+            if (!$mime_type) {
+              # if filename has the suffix "pdf", but is really no pdf set mimetype for no suffix
+              $mime_type = File::MimeInfo::Magic::mimetype($fname);
+              $mime_type = 'application/octet-stream' if $mime_type eq 'application/pdf' || !$mime_type;
+            }
+
+            $dbfile->assign_attributes(
+              object_id   => $obj->id,
+              object_type => $type,
+              source      => $params{file_type} eq 'document' ? 'created' : 'uploaded',
+              file_type   => $params{file_type},
+              file_name   => $filename . '_' . $number . '_' . $ext,
+              mime_type   => $mime_type,
+              itime       => $::locale->parse_date_to_object($date . ' ' . $time, %dateparms),
+            );
+            $dbfile->save;
+          }
+        #}
+
+        closedir $dir;
+      }
+    }
+  }
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::File::Backend::Filesystem  - Filesystem class for file storage backend
+
+=head1 SYNOPSIS
+
+See the synopsis of L<SL::File::Backend>.
+
+=head1 OVERVIEW
+
+This specific storage backend use a Filesystem which is only accessed by this interface.
+This is the big difference to the Webdav backend where the files can be accessed without the control of that backend.
+This backend use the database id of the SL::DB::File object as filename. The filesystem has up to 1000 subdirectories
+to store the files not to flat in the filesystem.
+
+
+=head1 METHODS
+
+See methods of L<SL::File::Backend>.
+
+=head1 SEE ALSO
+
+L<SL::File::Backend>
+
+=head1 TODO
+
+The synchronization must be tested and a periodical task is needed to synchronize in some time periods.
+
+=head1 AUTHOR
+
+Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
+
+=cut
diff --git a/SL/File/Object.pm b/SL/File/Object.pm
new file mode 100644 (file)
index 0000000..42a651f
--- /dev/null
@@ -0,0 +1,285 @@
+package SL::File::Object;
+
+use strict;
+use parent qw(Rose::Object);
+use DateTime;
+
+use Rose::Object::MakeMethods::Generic (
+  scalar => [ qw() ],
+  'scalar --get_set_init' => [ qw(db_file loaded id version newest) ],
+);
+
+#use SL::DB::Helper::Attr;
+#__PACKAGE__->_as_timestamp('mtime');
+# wie wird das mit dem Attr Helper gemacht damit er bei nicht DB Objekten auch geht?
+
+sub mtime_as_timestamp_s {
+  $::locale->format_date_object($_[0]->mtime, precision => 'second');
+}
+
+# wrapper methods
+
+sub itime {
+  $_[0]->loaded_db_file->itime;
+}
+
+sub file_name {
+  $_[0]->loaded_db_file->file_name;
+}
+
+sub file_type {
+  $_[0]->loaded_db_file->file_type;
+}
+
+sub object_type {
+  $_[0]->loaded_db_file->object_type;
+}
+
+sub object_id {
+  $_[0]->loaded_db_file->object_id;
+}
+
+sub mime_type {
+  $_[0]->loaded_db_file->mime_type;
+}
+
+sub file_title {
+  $_[0]->loaded_db_file->title;
+}
+
+sub file_description {
+  $_[0]->loaded_db_file->description;
+}
+
+sub backend {
+  $_[0]->loaded_db_file->backend;
+}
+
+sub source {
+  $_[0]->loaded_db_file->source;
+}
+
+# methods to go directly into the backends
+
+sub get_file {
+  $_[0]->backend_class->get_filepath(dbfile => $_[0]->loaded_db_file, version => $_[0]->version)
+}
+
+sub get_content {
+  $_[0]->backend_class->get_content(dbfile => $_[0]->loaded_db_file,  version => $_[0]->version)
+}
+
+sub mtime {
+  $_[0]->backend_class->get_mtime(dbfile => $_[0]->loaded_db_file, version => $_[0]->version)
+}
+
+sub version_count {
+  $_[0]->backend_class->get_version_count(dbfile => $_[0]->loaded_db_file)
+}
+
+sub versions {
+  SL::File->get_all_versions(dbfile => $_[0]->loaded_db_file)
+}
+
+sub save_contents {
+  SL::File->save(dbfile => $_[0]->loaded_db_file, file_contents => $_[1] )
+}
+
+sub save_file {
+  SL::File->save(dbfile => $_[0]->loaded_db_file, file_path => $_[1] )
+}
+
+sub delete {
+  SL::File->delete(dbfile => $_[0]->loaded_db_file)
+}
+
+sub delete_last_version {
+  SL::File->delete(dbfile => $_[0]->loaded_db_file, last => 1 )
+}
+
+sub delete_version {
+  SL::File->delete(dbfile => $_[0]->loaded_db_file, version => $_[0]->version )
+}
+
+sub purge {
+  SL::File->delete(dbfile => $_[0]->loaded_db_file, all_but_notlast => 1 )
+}
+
+sub rename {
+  SL::File->rename(dbfile => $_[0]->loaded_db_file, to => $_[1])
+}
+
+# internals
+
+sub backend_class {
+  SL::File->get_backend_class($_[0]->backend)
+}
+
+
+sub loaded_db_file {  # so, dass wir die nur einmal laden.
+  if (!$_[0]->loaded) {
+    $_[0]->db_file->load;
+    $_[0]->loaded(1);
+  }
+  $_[0]->db_file;
+}
+
+sub clone {
+  bless +{ %{ $_[0] } }, __PACKAGE__;
+}
+
+
+sub init_db_file { die 'must always have a db file'; }
+sub init_loaded  { 0 }
+sub init_id      { 0 }
+sub init_version { 0 }
+sub init_newest  { 1 }
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::File::Object - a filemangement object wrapper
+
+=head1 SYNOPSIS
+
+  use SL::File;
+
+  my ($object) = SL::File->get_all(object_id   => $object_id,
+                                   object_type => $object_type,
+                                   file_type   => 'image',   # may be optional
+                                   source      => 'uploaded' # may be optional
+                                  );
+# read attributes
+
+  my $object_id   = $object->object_id,
+  my $object_type = $object->object_type,
+  my $file_type   = $object->file_type;
+  my $file_name   = $object->file_name;
+  my $mime_type   = $object->mime_type;
+
+  my $mtime       = $object->mtime;
+  my $itime       = $object->itime;
+  my $id          = $object->id;
+  my $newest      = $object->newest;
+
+  my $versions    = $object->version_count;
+
+  my @versionobjs = $object->versions;
+  foreach ( @versionobjs ) {
+    my $mtime    = $_->mtime;
+    my $contents = $_->get_content;
+  }
+
+# update
+
+  $object->rename("image1.png");
+  $object->save_contents("new text");
+  $object->save_file("/tmp/empty.png");
+  $object->purge;
+  $object->delete_last_version;
+  $object->delete;
+
+=head1 DESCRIPTION
+
+This is a wrapper around a single object in the filemangement.
+
+=head1 METHODS
+
+Following methods are wrapper to read the attributes of L<SL::DB::File> :
+
+=over 4
+
+=item C<object_id>
+
+=item C<object_type>
+
+=item C<file_type>
+
+=item C<file_name>
+
+=item C<mime_name>
+
+=item C<file_title>
+
+=item C<file_description>
+
+=item C<backend>
+
+=item C<source>
+
+=item C<itime>
+
+=item C<id>
+
+=back
+
+Additional are there special methods. If the Object is created by SL::File::get_all_versions()
+or by "$object->versions"
+it has a version number. So the different mtime, filepath or content can be retrieved:
+
+=over 4
+
+=item C<mtime>
+
+get the modification time of a (versioned) object
+
+=item C<get_file>
+
+get the full qualified file path of the (versioned) object
+
+=item C<get_content>
+
+get the content of the (versioned) object
+
+=item C<version_count>
+
+Get the available versions of the file
+
+=item C<versions>
+
+Returns an array of SL::File::Object objects with the available versions of the file, starting with the newest version.
+
+=item C<newest>
+
+If set this is the newest version of the file.
+
+=item C<save_contents $contents>
+
+Store a new contents to the file (as a new version).
+
+=item C<save_file $filepath>
+
+Store the content of an (absolute)file path to the file
+
+=item C<delete>
+
+Delete the file with all of his versions
+
+=item C<delete_last_version>
+
+Delete only the last version of the file with implicit decrement of the version_count.
+
+=item C<purge>
+
+Delete all old versions of the file. Only one version exist after purge. The version count is reset to 1.
+
+=item C<rename $newfilename>
+
+Renames the filename
+
+=back
+
+=head1 SEE ALSO
+
+L<SL::File>
+
+=head1 AUTHOR
+
+Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
+
+=cut
index eee73db..fd79b96 100644 (file)
@@ -1,4 +1,4 @@
-#========= ===========================================================
+#=====================================================================
 # LX-Office ERP
 # Copyright (C) 2004
 # Based on SQL-Ledger Version 2.1.9
@@ -27,7 +27,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 # Utilities for parsing forms
 # and supporting routines for linking account numbers
@@ -41,13 +42,14 @@ use Carp;
 use Data::Dumper;
 
 use Carp;
-use Config;
 use CGI;
 use Cwd;
 use Encode;
 use File::Copy;
+use File::Temp ();
 use IO::File;
 use Math::BigInt;
+use POSIX qw(strftime);
 use SL::Auth;
 use SL::Auth::DB;
 use SL::Auth::LDAP;
@@ -57,11 +59,14 @@ use SL::CVar;
 use SL::DB;
 use SL::DBConnect;
 use SL::DBUtils;
+use SL::DB::AdditionalBillingAddress;
 use SL::DB::Customer;
+use SL::DB::CustomVariableConfig;
 use SL::DB::Default;
 use SL::DB::PaymentTerm;
 use SL::DB::Vendor;
 use SL::DO;
+use SL::Helper::Flash qw();
 use SL::IC;
 use SL::IS;
 use SL::Layout::Dispatcher;
@@ -75,37 +80,22 @@ use SL::PrefixedNumber;
 use SL::Request;
 use SL::Template;
 use SL::User;
+use SL::Util;
+use SL::Version;
 use SL::X;
 use Template;
 use URI;
 use List::Util qw(first max min sum);
 use List::MoreUtils qw(all any apply);
 use SL::DB::Tax;
+use SL::Helper::File qw(:all);
+use SL::Helper::Number;
+use SL::Helper::CreatePDF qw(merge_pdfs);
 
 use strict;
 
-my $standard_dbh;
-
-END {
-  disconnect_standard_dbh();
-}
-
-sub disconnect_standard_dbh {
-  return unless $standard_dbh;
-
-  $standard_dbh->rollback();
-  undef $standard_dbh;
-}
-
 sub read_version {
-  my ($self) = @_;
-
-  open VERSION_FILE, "VERSION";                 # New but flexible code reads version from VERSION-file
-  my $version =  <VERSION_FILE>;
-  $version    =~ s/[^0-9A-Za-z\.\_\-]//g; # only allow numbers, letters, points, underscores and dashes. Prevents injecting of malicious code.
-  close VERSION_FILE;
-
-  return $version;
+  SL::Version->get_version;
 }
 
 sub new {
@@ -123,18 +113,11 @@ sub new {
 
   bless $self, $type;
 
-  $self->{version} = $self->read_version;
-
   $main::lxdebug->leave_sub();
 
   return $self;
 }
 
-sub read_cgi_input {
-  my ($self) = @_;
-  SL::Request::read_cgi_input($self);
-}
-
 sub _flatten_variables_rec {
   $main::lxdebug->enter_sub(2);
 
@@ -165,7 +148,7 @@ sub _flatten_variables_rec {
           $first_array_entry = 0;
         }
       } else {
-        @result = ({ 'key' => $prefix . $key . ($first_array_entry ? '[+]' : '[]'), 'value' => $element });
+        push @result, { 'key' => $prefix . $key . '[]', 'value' => $element };
       }
     }
   }
@@ -196,7 +179,7 @@ sub flatten_standard_variables {
   $main::lxdebug->enter_sub(2);
 
   my $self      = shift;
-  my %skip_keys = map { $_ => 1 } (qw(login password header stylesheet titlebar version), @_);
+  my %skip_keys = map { $_ => 1 } (qw(login password header stylesheet titlebar), @_);
 
   my @variables;
 
@@ -209,36 +192,6 @@ sub flatten_standard_variables {
   return @variables;
 }
 
-sub debug {
-  $main::lxdebug->enter_sub();
-
-  my ($self) = @_;
-
-  print "\n";
-
-  map { print "$_ = $self->{$_}\n" } (sort keys %{$self});
-
-  $main::lxdebug->leave_sub();
-}
-
-sub dumper {
-  $main::lxdebug->enter_sub(2);
-
-  my $self          = shift;
-  my $password      = $self->{password};
-
-  $self->{password} = 'X' x 8;
-
-  local $Data::Dumper::Sortkeys = 1;
-  my $output                    = Dumper($self);
-
-  $self->{password} = $password;
-
-  $main::lxdebug->leave_sub(2);
-
-  return $output;
-}
-
 sub escape {
   my ($self, $str) = @_;
 
@@ -294,7 +247,7 @@ sub hide_form {
 
 sub throw_on_error {
   my ($self, $code) = @_;
-  local $self->{__ERROR_HANDLER} = sub { die SL::X::FormError->new($_[0]) };
+  local $self->{__ERROR_HANDLER} = sub { SL::X::FormError->throw(error => $_[0]) };
   $code->();
 }
 
@@ -354,13 +307,12 @@ sub numtextrows {
 }
 
 sub dberror {
-  $main::lxdebug->enter_sub();
-
   my ($self, $msg) = @_;
 
-  $self->error("$msg\n" . $DBI::errstr);
-
-  $main::lxdebug->leave_sub();
+  SL::X::DBError->throw(
+    msg      => $msg,
+    db_error => $DBI::errstr,
+  );
 }
 
 sub isblank {
@@ -385,7 +337,7 @@ sub _get_request_uri {
   return URI->new($ENV{HTTP_REFERER})->canonical() if $ENV{HTTP_X_FORWARDED_FOR};
   return URI->new                                  if !$ENV{REQUEST_URI}; # for testing
 
-  my $scheme =  $ENV{HTTPS} && (lc $ENV{HTTPS} eq 'on') ? 'https' : 'http';
+  my $scheme =  $::request->is_https ? 'https' : 'http';
   my $port   =  $ENV{SERVER_PORT};
   $port      =  undef if (($scheme eq 'http' ) && ($port == 80))
                       || (($scheme eq 'https') && ($port == 443));
@@ -432,10 +384,12 @@ sub create_http_response {
     my $session_cookie_value = $main::auth->get_session_id();
 
     if ($session_cookie_value) {
-      $session_cookie = $cgi->cookie('-name'   => $main::auth->get_session_cookie_name(),
-                                     '-value'  => $session_cookie_value,
-                                     '-path'   => $uri->path,
-                                     '-secure' => $ENV{HTTPS});
+      $session_cookie = $cgi->cookie('-name'    => $main::auth->get_session_cookie_name(),
+                                     '-value'   => $session_cookie_value,
+                                     '-path'    => $uri->path,
+                                     '-expires' => '+' . $::auth->{session_timeout} . 'm',
+                                     '-secure'  => $::request->is_https);
+      $session_cookie = "$session_cookie; SameSite=strict";
     }
   }
 
@@ -443,7 +397,7 @@ sub create_http_response {
   $cgi_params{'-charset'} = $params{charset} if ($params{charset});
   $cgi_params{'-cookie'}  = $session_cookie  if ($session_cookie);
 
-  map { $cgi_params{'-' . $_} = $params{$_} if exists $params{$_} } qw(content_disposition content_length);
+  map { $cgi_params{'-' . $_} = $params{$_} if exists $params{$_} } qw(content_disposition content_length status);
 
   my $output = $cgi->header(%cgi_params);
 
@@ -480,11 +434,13 @@ sub header {
     jquery jquery-ui jquery.cookie jquery.checkall jquery.download
     jquery/jquery.form jquery/fixes client_js
     jquery/jquery.tooltipster.min
-    common part_selection switchmenuframe
+    common part_selection
   ), "jquery/ui/i18n/jquery.ui.datepicker-$::myconfig{countrycode}");
 
+  $layout->use_javascript("$_.js") for @{ $params{use_javascripts} // [] };
+
   $self->{favicon} ||= "favicon.ico";
-  $self->{titlebar} = join ' - ', grep $_, $self->{title}, $self->{login}, $::myconfig{dbname}, $self->{version} if $self->{title} || !$self->{titlebar};
+  $self->{titlebar} = join ' - ', grep $_, $self->{title}, $self->{login}, $::myconfig{dbname}, $self->read_version if $self->{title} || !$self->{titlebar};
 
   # build includes
   if ($self->{refresh_url} || $self->{refresh_time}) {
@@ -499,6 +455,7 @@ sub header {
   push @header, "<style type='text/css'>\@page { size:landscape; }</style> "                     if $self->{landscape};
   push @header, "<link rel='shortcut icon' href='$self->{favicon}' type='image/x-icon'>"         if -f $self->{favicon};
   push @header, map { qq|<script type="text/javascript" src="${_}${auto_reload_resources_param}"></script>| }                    $layout->javascripts;
+  push @header, '<meta name="viewport" content="width=device-width, initial-scale=1">';
   push @header, $self->{javascript} if $self->{javascript};
   push @header, map { $_->show_javascript } @{ $self->{AJAX} || [] };
 
@@ -578,24 +535,13 @@ sub set_standard_title {
   $::lxdebug->enter_sub;
   my $self = shift;
 
-  $self->{titlebar}  = "kivitendo " . $::locale->text('Version') . " $self->{version}";
+  $self->{titlebar}  = "kivitendo " . $::locale->text('Version') . " " . $self->read_version;
   $self->{titlebar} .= "- $::myconfig{name}"   if $::myconfig{name};
   $self->{titlebar} .= "- $::myconfig{dbname}" if $::myconfig{name};
 
   $::lxdebug->leave_sub;
 }
 
-sub prepare_global_vars {
-  my ($self) = @_;
-
-  $self->{AUTH}            = $::auth;
-  $self->{INSTANCE_CONF}   = $::instance_conf;
-  $self->{LOCALE}          = $::locale;
-  $self->{LXCONFIG}        = $::lx_office_conf;
-  $self->{LXDEBUG}         = $::lxdebug;
-  $self->{MYCONFIG}        = \%::myconfig;
-}
-
 sub _prepare_html_template {
   $main::lxdebug->enter_sub();
 
@@ -609,8 +555,10 @@ sub _prepare_html_template {
   }
   $language = "de" unless ($language);
 
-  if (-f "templates/webpages/${file}.html") {
-    $file = "templates/webpages/${file}.html";
+  my $webpages_path = $::request->layout->webpages_path;
+
+  if (-f "${webpages_path}/${file}.html") {
+    $file = "${webpages_path}/${file}.html";
 
   } elsif (ref $file eq 'SCALAR') {
     # file is a scalarref, use inline mode
@@ -618,39 +566,15 @@ sub _prepare_html_template {
     my $info = "Web page template '${file}' not found.\n";
     $::form->header;
     print qq|<pre>$info</pre>|;
-    ::end_of_request();
-  }
-
-  if ($self->{"DEBUG"}) {
-    $additional_params->{"DEBUG"} = $self->{"DEBUG"};
-  }
-
-  if ($additional_params->{"DEBUG"}) {
-    $additional_params->{"DEBUG"} =
-      "<br><em>DEBUG INFORMATION:</em><pre>" . $additional_params->{"DEBUG"} . "</pre>";
-  }
-
-  if (%main::myconfig) {
-    $::myconfig{jsc_dateformat} = apply {
-      s/d+/\%d/gi;
-      s/m+/\%m/gi;
-      s/y+/\%Y/gi;
-    } $::myconfig{"dateformat"};
-    $additional_params->{"myconfig"} ||= \%::myconfig;
-    map { $additional_params->{"myconfig_${_}"} = $main::myconfig{$_}; } keys %::myconfig;
+    $::dispatcher->end_request;
   }
 
+  $additional_params->{AUTH}          = $::auth;
   $additional_params->{INSTANCE_CONF} = $::instance_conf;
-
-  if (my $debug_options = $::lx_office_conf{debug}{options}) {
-    map { $additional_params->{'DEBUG_' . uc($_)} = $debug_options->{$_} } keys %$debug_options;
-  }
-
-  if ($main::auth && $main::auth->{RIGHTS} && $main::auth->{RIGHTS}->{$self->{login}}) {
-    while (my ($key, $value) = each %{ $main::auth->{RIGHTS}->{$self->{login}} }) {
-      $additional_params->{"AUTH_RIGHTS_" . uc($key)} = $value;
-    }
-  }
+  $additional_params->{LOCALE}        = $::locale;
+  $additional_params->{LXCONFIG}      = \%::lx_office_conf;
+  $additional_params->{LXDEBUG}       = $::lxdebug;
+  $additional_params->{MYCONFIG}      = \%::myconfig;
 
   $main::lxdebug->leave_sub();
 
@@ -665,7 +589,7 @@ sub parse_html_template {
   $additional_params ||= { };
 
   my $real_file = $self->_prepare_html_template($file, $additional_params);
-  my $template  = $self->template || $self->init_template;
+  my $template  = $self->template;
 
   map { $additional_params->{$_} ||= $self->{$_} } keys %{ $self };
 
@@ -677,32 +601,7 @@ sub parse_html_template {
   return $output;
 }
 
-sub init_template {
-  my $self = shift;
-
-  return $self->template if $self->template;
-
-  # Force scripts/locales.pl to pick up the exception handling template.
-  # parse_html_template('generic/exception')
-  return $self->template(Template->new({
-     'INTERPOLATE'  => 0,
-     'EVAL_PERL'    => 0,
-     'ABSOLUTE'     => 1,
-     'CACHE_SIZE'   => 0,
-     'PLUGIN_BASE'  => 'SL::Template::Plugin',
-     'INCLUDE_PATH' => '.:templates/webpages',
-     'COMPILE_EXT'  => '.tcc',
-     'COMPILE_DIR'  => $::lx_office_conf{paths}->{userspath} . '/templates-cache',
-     'ERROR'        => 'templates/webpages/generic/exception.html',
-     'ENCODING'     => 'utf8',
-  })) || die;
-}
-
-sub template {
-  my $self = shift;
-  $self->{template_object} = shift if @_;
-  return $self->{template_object};
-}
+sub template { $::request->presenter->get_template }
 
 sub show_generic_error {
   $main::lxdebug->enter_sub();
@@ -719,7 +618,7 @@ sub show_generic_error {
     SL::ClientJS->new
       ->error($error)
       ->render(SL::Controller::Base->new);
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
 
   my $add_params = {
@@ -727,22 +626,18 @@ sub show_generic_error {
     'label_error' => $error,
   };
 
-  if ($params{action}) {
-    my @vars;
-
-    map { delete($self->{$_}); } qw(action);
-    map { push @vars, { "name" => $_, "value" => $self->{$_} } if (!ref($self->{$_})); } keys %{ $self };
-
-    $add_params->{SHOW_BUTTON}  = 1;
-    $add_params->{BUTTON_LABEL} = $params{label} || $params{action};
-    $add_params->{VARIABLES}    = \@vars;
+  $self->{title} = $params{title} if $params{title};
 
-  } elsif ($params{back_button}) {
-    $add_params->{SHOW_BACK_BUTTON} = 1;
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Back'),
+        call      => [ 'kivi.history_back' ],
+        accesskey => 'enter',
+      ],
+    );
   }
 
-  $self->{title} = $params{title} if $params{title};
-
   $self->header();
   print $self->parse_html_template("generic/error", $add_params);
 
@@ -750,7 +645,7 @@ sub show_generic_error {
 
   $main::lxdebug->leave_sub();
 
-  ::end_of_request();
+  $::dispatcher->end_request;
 }
 
 sub show_generic_information {
@@ -770,7 +665,7 @@ sub show_generic_information {
 
   $main::lxdebug->leave_sub();
 
-  ::end_of_request();
+  $::dispatcher->end_request;
 }
 
 sub _store_redirect_info_in_session {
@@ -792,11 +687,12 @@ sub redirect {
     $self->info($msg);
 
   } else {
+    SL::Helper::Flash::flash_later('info', $msg) if $msg;
     $self->_store_redirect_info_in_session;
     print $::form->redirect_header($self->{callback});
   }
 
-  ::end_of_request();
+  $::dispatcher->end_request;
 
   $main::lxdebug->leave_sub();
 }
@@ -812,112 +708,10 @@ sub sort_columns {
   return @columns;
 }
 #
-sub format_amount {
-  $main::lxdebug->enter_sub(2);
 
+sub format_amount {
   my ($self, $myconfig, $amount, $places, $dash) = @_;
-  $amount ||= 0;
-  $dash   ||= '';
-  my $neg = $amount < 0;
-  my $force_places = defined $places && $places >= 0;
-
-  $amount = $self->round_amount($amount, abs $places) if $force_places;
-  $neg    = 0 if $amount == 0; # don't show negative zero
-  $amount = sprintf "%.*f", ($force_places ? $places : 10), abs $amount; # 6 is default for %fa
-
-  # before the sprintf amount was a number, afterwards it's a string. because of the dynamic nature of perl
-  # this is easy to confuse, so keep in mind: before this comment no s///, m//, concat or other strong ops on
-  # $amount. after this comment no +,-,*,/,abs. it will only introduce subtle bugs.
-
-  $amount =~ s/0*$// unless defined $places && $places == 0;             # cull trailing 0s
-
-  my @d = map { s/\d//g; reverse split // } my $tmp = $myconfig->{numberformat}; # get delim chars
-  my @p = split(/\./, $amount);                                          # split amount at decimal point
-
-  $p[0] =~ s/\B(?=(...)*$)/$d[1]/g if $d[1];                             # add 1,000 delimiters
-  $amount = $p[0];
-  if ($places || $p[1]) {
-    $amount .= $d[0]
-            .  ( $p[1] || '' )
-            .  (0 x max(abs($places || 0) - length ($p[1]||''), 0));     # pad the fraction
-  }
-
-  $amount = do {
-    ($dash =~ /-/)    ? ($neg ? "($amount)"                            : "$amount" )                              :
-    ($dash =~ /DRCR/) ? ($neg ? "$amount " . $main::locale->text('DR') : "$amount " . $main::locale->text('CR') ) :
-                        ($neg ? "-$amount"                             : "$amount" )                              ;
-  };
-
-  $main::lxdebug->leave_sub(2);
-  return $amount;
-}
-
-sub format_amount_units {
-  $main::lxdebug->enter_sub();
-
-  my $self             = shift;
-  my %params           = @_;
-
-  my $myconfig         = \%main::myconfig;
-  my $amount           = $params{amount} * 1;
-  my $places           = $params{places};
-  my $part_unit_name   = $params{part_unit};
-  my $amount_unit_name = $params{amount_unit};
-  my $conv_units       = $params{conv_units};
-  my $max_places       = $params{max_places};
-
-  if (!$part_unit_name) {
-    $main::lxdebug->leave_sub();
-    return '';
-  }
-
-  my $all_units        = AM->retrieve_all_units;
-
-  if (('' eq ref $conv_units) && ($conv_units =~ /convertible/)) {
-    $conv_units = AM->convertible_units($all_units, $part_unit_name, $conv_units eq 'convertible_not_smaller');
-  }
-
-  if (!scalar @{ $conv_units }) {
-    my $result = $self->format_amount($myconfig, $amount, $places, undef, $max_places) . " " . $part_unit_name;
-    $main::lxdebug->leave_sub();
-    return $result;
-  }
-
-  my $part_unit  = $all_units->{$part_unit_name};
-  my $conv_unit  = ($amount_unit_name && ($amount_unit_name ne $part_unit_name)) ? $all_units->{$amount_unit_name} : $part_unit;
-
-  $amount       *= $conv_unit->{factor};
-
-  my @values;
-  my $num;
-
-  foreach my $unit (@$conv_units) {
-    my $last = $unit->{name} eq $part_unit->{name};
-    if (!$last) {
-      $num     = int($amount / $unit->{factor});
-      $amount -= $num * $unit->{factor};
-    }
-
-    if ($last ? $amount : $num) {
-      push @values, { "unit"   => $unit->{name},
-                      "amount" => $last ? $amount / $unit->{factor} : $num,
-                      "places" => $last ? $places : 0 };
-    }
-
-    last if $last;
-  }
-
-  if (!@values) {
-    push @values, { "unit"   => $part_unit_name,
-                    "amount" => 0,
-                    "places" => 0 };
-  }
-
-  my $result = join " ", map { $self->format_amount($myconfig, $_->{amount}, $_->{places}, undef, $max_places), $_->{unit} } @values;
-
-  $main::lxdebug->leave_sub();
-
-  return $result;
+  SL::Helper::Number::_format_number($amount, $places, %$myconfig, dash => $dash);
 }
 
 sub format_string {
@@ -938,75 +732,11 @@ sub format_string {
 #
 
 sub parse_amount {
-  $main::lxdebug->enter_sub(2);
-
   my ($self, $myconfig, $amount) = @_;
-
-  if (!defined($amount) || ($amount eq '')) {
-    $main::lxdebug->leave_sub(2);
-    return 0;
-  }
-
-  if (   ($myconfig->{numberformat} eq '1.000,00')
-      || ($myconfig->{numberformat} eq '1000,00')) {
-    $amount =~ s/\.//g;
-    $amount =~ s/,/\./g;
-  }
-
-  if ($myconfig->{numberformat} eq "1'000.00") {
-    $amount =~ s/\'//g;
-  }
-
-  $amount =~ s/,//g;
-
-  $main::lxdebug->leave_sub(2);
-
-  # Make sure no code wich is not a math expression ends up in eval().
-  return 0 unless $amount =~ /^ [\s \d \( \) \- \+ \* \/ \. ]* $/x;
-
-  # Prevent numbers from being parsed as octals;
-  $amount =~ s{ (?<! [\d.] ) 0+ (?= [1-9] ) }{}gx;
-
-  return scalar(eval($amount)) * 1 ;
+  SL::Helper::Number::_parse_number($amount, %$myconfig);
 }
 
-sub round_amount {
-  my ($self, $amount, $places) = @_;
-
-  return 0 if !defined $amount;
-
-  # We use Perl's knowledge of string representation for
-  # rounding. First, convert the floating point number to a string
-  # with a high number of places. Then split the string on the decimal
-  # sign and use integer calculation for rounding the decimal places
-  # part. If an overflow occurs then apply that overflow to the part
-  # before the decimal sign as well using integer arithmetic again.
-
-  my $int_amount = int(abs $amount);
-  my $str_places = max(min(10, 16 - length("$int_amount") - $places), $places);
-  my $amount_str = sprintf '%.*f', $places + $str_places, abs($amount);
-
-  return $amount unless $amount_str =~ m{^(\d+)\.(\d+)$};
-
-  my ($pre, $post)      = ($1, $2);
-  my $decimals          = '1' . substr($post, 0, $places);
-
-  my $propagation_limit = $Config{i32size} == 4 ? 7 : 18;
-  my $add_for_rounding  = substr($post, $places, 1) >= 5 ? 1 : 0;
-
-  if ($places > $propagation_limit) {
-    $decimals = Math::BigInt->new($decimals)->badd($add_for_rounding);
-    $pre      = Math::BigInt->new($decimals)->badd(1) if substr($decimals, 0, 1) eq '2';
-
-  } else {
-    $decimals += $add_for_rounding;
-    $pre      += 1 if substr($decimals, 0, 1) eq '2';
-  }
-
-  $amount  = ("${pre}." . substr($decimals, 1)) * ($amount <=> 0);
-
-  return $amount;
-}
+sub round_amount { shift; goto &SL::Helper::Number::_round_number; }
 
 sub parse_template {
   $main::lxdebug->enter_sub();
@@ -1016,11 +746,18 @@ sub parse_template {
 
   local (*IN, *OUT);
 
-  my $defaults  = SL::DB::Default->get;
-  my $userspath = $::lx_office_conf{paths}->{userspath};
+  my $defaults        = SL::DB::Default->get;
+
+  my $keep_temp_files = $::lx_office_conf{debug} && $::lx_office_conf{debug}->{keep_temp_files};
+  $self->{cwd}        = getcwd();
+  my $temp_dir        = File::Temp->newdir(
+    "kivitendo-print-XXXXXX",
+    DIR     => $self->{cwd} . "/" . $::lx_office_conf{paths}->{userspath},
+    CLEANUP => !$keep_temp_files,
+  );
 
-  $self->{"cwd"} = getcwd();
-  $self->{"tmpdir"} = $self->{cwd} . "/${userspath}";
+  my $userspath   = File::Spec->abs2rel($temp_dir->dirname);
+  $self->{tmpdir} = $temp_dir->dirname;
 
   my $ext_for_format;
 
@@ -1037,13 +774,6 @@ sub parse_template {
     $template_type  = 'HTML';
     $ext_for_format = 'html';
 
-  } elsif (($self->{"format"} =~ /xml/i) || (!$self->{"format"} && ($self->{"IN"} =~ /xml$/i))) {
-    $template_type  = 'XML';
-    $ext_for_format = 'xml';
-
-  } elsif ( $self->{"format"} =~ /elster(?:winston|taxbird)/i ) {
-    $template_type = 'XML';
-
   } elsif ( $self->{"format"} =~ /excel/i ) {
     $template_type  = 'Excel';
     $ext_for_format = 'xls';
@@ -1087,16 +817,18 @@ sub parse_template {
 
   # OUT is used for the media, screen, printer, email
   # for postscript we store a copy in a temporary file
+
   my ($temp_fh, $suffix);
   $suffix =  $self->{IN};
   $suffix =~ s/.*\.//;
   ($temp_fh, $self->{tmpfile}) = File::Temp::tempfile(
-    'kivitendo-printXXXXXX',
+    strftime('kivitendo-print-%Y%m%d%H%M%S-XXXXXX', localtime()),
     SUFFIX => '.' . ($suffix || 'tex'),
     DIR    => $userspath,
-    UNLINK => ($::lx_office_conf{debug} && $::lx_office_conf{debug}->{keep_temp_files})? 0 : 1,
+    UNLINK => $keep_temp_files ? 0 : 1,
   );
   close $temp_fh;
+  chmod 0644, $self->{tmpfile} if $keep_temp_files;
   (undef, undef, $self->{template_meta}{tmpfile}) = File::Spec->splitpath( $self->{tmpfile} );
 
   $out              = $self->{OUT};
@@ -1126,11 +858,29 @@ sub parse_template {
   close OUT if $self->{OUT};
   # check only one flag (webdav_documents)
   # therefore copy to webdav, even if we do not have the webdav feature enabled (just archive)
-  my $copy_to_webdav =  $::instance_conf->get_webdav_documents && !$self->{preview} && $self->{tmpdir} && $self->{tmpfile} && $self->{type};
+  my $copy_to_webdav =  $::instance_conf->get_webdav_documents && !$self->{preview} && $self->{tmpdir} && $self->{tmpfile} && $self->{type}
+                        && $self->{type} ne 'statement';
+
+  $self->{attachment_filename} ||= $self->generate_attachment_filename;
 
+  if ( $ext_for_format eq 'pdf' && $self->doc_storage_enabled ) {
+    $self->append_general_pdf_attachments(filepath =>  $self->{tmpdir}."/".$self->{tmpfile},
+                                          type     =>  $self->{type});
+  }
   if ($self->{media} eq 'file') {
     copy(join('/', $self->{cwd}, $userspath, $self->{tmpfile}), $out =~ m|^/| ? $out : join('/', $self->{cwd}, $out)) if $template->uses_temp_file;
-    Common::copy_file_to_webdav_folder($self)                                                                         if $copy_to_webdav;
+
+    if ($copy_to_webdav) {
+      if (my $error = Common::copy_file_to_webdav_folder($self)) {
+        chdir("$self->{cwd}");
+        $self->error($error);
+      }
+    }
+
+    if (!$self->{preview} && $self->{attachment_type} !~ m{^dunning} && $self->doc_storage_enabled)
+    {
+      $self->store_pdf($self);
+    }
     $self->cleanup;
     chdir("$self->{cwd}");
 
@@ -1139,100 +889,192 @@ sub parse_template {
     return;
   }
 
-  Common::copy_file_to_webdav_folder($self) if $copy_to_webdav;
+  if ($copy_to_webdav) {
+    if (my $error = Common::copy_file_to_webdav_folder($self)) {
+      chdir("$self->{cwd}");
+      $self->error($error);
+    }
+  }
 
+  if ( !$self->{preview} && $ext_for_format eq 'pdf' && $self->{attachment_type} !~ m{^dunning} && $self->doc_storage_enabled) {
+    my $file_obj = $self->store_pdf($self);
+    $self->{print_file_id} = $file_obj->id if $file_obj;
+  }
   if ($self->{media} eq 'email') {
+    if ( getcwd() eq $self->{"tmpdir"} ) {
+      # in the case of generating pdf we are in the tmpdir, but WHY ???
+      $self->{tmpfile} = $userspath."/".$self->{tmpfile};
+      chdir("$self->{cwd}");
+    }
+    $self->send_email(\%::myconfig,$ext_for_format);
+  }
+  else {
+    $self->{OUT}      = $out;
+    $self->{OUT_MODE} = $out_mode;
+    $self->output_file($template->get_mime_type,$command_formatter);
+  }
+  delete $self->{print_file_id};
 
-    my $mail = Mailer->new;
-
-    map { $mail->{$_} = $self->{$_} }
-      qw(cc bcc subject message version format);
-    $mail->{to} = $self->{EMAIL_RECIPIENT} ? $self->{EMAIL_RECIPIENT} : $self->{email};
-    $mail->{from}   = qq|"$myconfig->{name}" <$myconfig->{email}>|;
-    $mail->{fileid} = time() . '.' . $$ . '.';
-    my $full_signature     =  $self->create_email_signature();
-    $full_signature        =~ s/\r//g;
-
-    # if we send html or plain text inline
-    if (($self->{format} eq 'html') && ($self->{sendmode} eq 'inline')) {
-      $mail->{contenttype}    =  "text/html";
-      $mail->{message}        =~ s/\r//g;
-      $mail->{message}        =~ s/\n/<br>\n/g;
-      $full_signature         =~ s/\n/<br>\n/g;
-      $mail->{message}       .=  $full_signature;
-
-      open(IN, "<:encoding(UTF-8)", $self->{tmpfile})
-        or $self->error($self->cleanup . "$self->{tmpfile} : $!");
-      $mail->{message} .= $_ while <IN>;
-      close(IN);
+  $self->cleanup;
 
-    } else {
+  chdir("$self->{cwd}");
+  $main::lxdebug->leave_sub();
+}
 
-      if (!$self->{"do_not_attach"}) {
-        my $attachment_name  =  $self->{attachment_filename} || $self->{tmpfile};
-        $attachment_name     =~ s/\.(.+?)$/.${ext_for_format}/ if ($ext_for_format);
-        $mail->{attachments} =  [{ "filename" => $self->{tmpfile},
-                                   "name"     => $attachment_name }];
-      }
+sub get_bcc_defaults {
+  my ($self, $myconfig, $mybcc) = @_;
+  if (SL::DB::Default->get->bcc_to_login) {
+    $mybcc .= ", " if $mybcc;
+    $mybcc .= $myconfig->{email};
+  }
+  my $otherbcc = SL::DB::Default->get->global_bcc;
+  if ($otherbcc) {
+    $mybcc .= ", " if $mybcc;
+    $mybcc .= $otherbcc;
+  }
+  return $mybcc;
+}
 
-      $mail->{message} .= $full_signature;
-    }
+sub send_email {
+  $main::lxdebug->enter_sub();
+  my ($self, $myconfig, $ext_for_format) = @_;
+  my $mail = Mailer->new;
 
-    my $err = $mail->send();
-    $self->error($self->cleanup . "$err") if ($err);
+  map { $mail->{$_} = $self->{$_} }
+    qw(cc subject message format);
 
-  } else {
+  if ($self->{cc_employee}) {
+    my ($user, $my_emp_cc);
+    $user        = SL::DB::Manager::AuthUser->find_by(login => $self->{cc_employee});
+    $my_emp_cc   = $user->get_config_value('email') if ref $user eq 'SL::DB::AuthUser';
+    $mail->{cc} .= ", "       if $mail->{cc};
+    $mail->{cc} .= $my_emp_cc if $my_emp_cc;
+  }
 
-    $self->{OUT}      = $out;
-    $self->{OUT_MODE} = $out_mode;
+  $mail->{bcc}    = $self->get_bcc_defaults($myconfig, $self->{bcc});
+  $mail->{to}     = $self->{EMAIL_RECIPIENT} ? $self->{EMAIL_RECIPIENT} : $self->{email};
+  $mail->{from}   = qq|"$myconfig->{name}" <$myconfig->{email}>|;
+  $mail->{fileid} = time() . '.' . $$ . '.';
+  $mail->{content_type}  =  "text/html";
+  my $full_signature     =  $self->create_email_signature();
+
+  $mail->{attachments} =  [];
+  my @attfiles;
+  # if we send html or plain text inline
+  if (($self->{format} eq 'html') && ($self->{sendmode} eq 'inline')) {
+    $mail->{message}        =~ s/\r//g;
+    $mail->{message}        =~ s{\n}{<br>\n}g;
+    $mail->{message}       .=  $full_signature;
 
-    my $numbytes = (-s $self->{tmpfile});
     open(IN, "<", $self->{tmpfile})
       or $self->error($self->cleanup . "$self->{tmpfile} : $!");
-    binmode IN;
+    $mail->{message} .= $_ while <IN>;
+    close(IN);
 
-    $self->{copies} = 1 unless $self->{media} eq 'printer';
+  } elsif (($self->{attachment_policy} // '') ne 'no_file') {
+    my $attachment_name  =  $self->{attachment_filename}  || $self->{tmpfile};
+    $attachment_name     =~ s{\.(.+?)$}{.${ext_for_format}} if ($ext_for_format);
 
-    chdir("$self->{cwd}");
-    #print(STDERR "Kopien $self->{copies}\n");
-    #print(STDERR "OUT $self->{OUT}\n");
-    for my $i (1 .. $self->{copies}) {
-      if ($self->{OUT}) {
-        $self->{OUT} = $command_formatter->($self->{OUT_MODE}, $self->{OUT});
+    if (($self->{attachment_policy} // '') eq 'old_file') {
+      my ( $attfile ) = SL::File->get_all(object_id     => $self->{id},
+                                          object_type   => $self->{type},
+                                          file_type     => 'document',
+                                          print_variant => $self->{formname},);
 
-        open  OUT, $self->{OUT_MODE}, $self->{OUT} or $self->error($self->cleanup . "$self->{OUT} : $!");
-        print OUT $_ while <IN>;
-        close OUT;
-        seek  IN, 0, 0;
+      if ($attfile) {
+        $attfile->{override_file_name} = $attachment_name if $attachment_name;
+        push @attfiles, $attfile;
+      }
 
-      } else {
-        my %headers = ('-type'       => $template->get_mime_type,
-                       '-connection' => 'close',
-                       '-charset'    => 'UTF-8');
-
-        $self->{attachment_filename} ||= $self->generate_attachment_filename;
-
-        if ($self->{attachment_filename}) {
-          %headers = (
-            %headers,
-            '-attachment'     => $self->{attachment_filename},
-            '-content-length' => $numbytes,
-            '-charset'        => '',
-          );
-        }
+    } else {
+      push @{ $mail->{attachments} }, { path => $self->{tmpfile},
+                                        id   => $self->{print_file_id},
+                                        type => "application/pdf",
+                                        name => $attachment_name };
+    }
+  }
 
-        print $::request->cgi->header(%headers);
+  push @attfiles,
+    grep { $_ }
+    map  { SL::File->get(id => $_) }
+    @{ $self->{attach_file_ids} // [] };
 
-        $::locale->with_raw_io(\*STDOUT, sub { print while <IN> });
-      }
-    }
+  foreach my $attfile ( @attfiles ) {
+    push @{ $mail->{attachments} }, {
+      path    => $attfile->get_file,
+      id      => $attfile->id,
+      type    => $attfile->mime_type,
+      name    => $attfile->{override_file_name} // $attfile->file_name,
+      content => $attfile->get_content ? ${ $attfile->get_content } : undef,
+    };
+  }
 
-    close(IN);
+  $mail->{message}  =~ s/\r//g;
+  $mail->{message} .= $full_signature;
+  $self->{emailerr} = $mail->send();
+
+  if ($self->{emailerr}) {
+    $self->cleanup;
+    $self->error($::locale->text('The email was not sent due to the following error: #1.', $self->{emailerr}));
   }
 
-  $self->cleanup;
+  $self->{email_journal_id} = $mail->{journalentry};
+  $self->{snumbers}  = "emailjournal" . "_" . $self->{email_journal_id};
+  $self->{what_done} = $::form->{type};
+  $self->{addition}  = "MAILED";
+  $self->save_history;
+
+  #write back for message info and mail journal
+  $self->{cc}  = $mail->{cc};
+  $self->{bcc} = $mail->{bcc};
+  $self->{email} = $mail->{to};
+
+  $main::lxdebug->leave_sub();
+}
+
+sub output_file {
+  $main::lxdebug->enter_sub();
+
+  my ($self,$mimeType,$command_formatter) = @_;
+  my $numbytes = (-s $self->{tmpfile});
+  open(IN, "<", $self->{tmpfile})
+    or $self->error($self->cleanup . "$self->{tmpfile} : $!");
+  binmode IN;
+
+  $self->{copies} = 1 unless $self->{media} eq 'printer';
 
   chdir("$self->{cwd}");
+  for my $i (1 .. $self->{copies}) {
+    if ($self->{OUT}) {
+      $self->{OUT} = $command_formatter->($self->{OUT_MODE}, $self->{OUT});
+
+      open  OUT, $self->{OUT_MODE}, $self->{OUT} or $self->error($self->cleanup . "$self->{OUT} : $!");
+      print OUT $_ while <IN>;
+      close OUT;
+      seek  IN, 0, 0;
+
+    } else {
+      my %headers = ('-type'       => $mimeType,
+                     '-connection' => 'close',
+                     '-charset'    => 'UTF-8');
+
+      $self->{attachment_filename} ||= $self->generate_attachment_filename;
+
+      if ($self->{attachment_filename}) {
+        %headers = (
+          %headers,
+          '-attachment'     => $self->{attachment_filename},
+          '-content-length' => $numbytes,
+          '-charset'        => '',
+        );
+      }
+
+      print $::request->cgi->header(%headers);
+
+      $::locale->with_raw_io(\*STDOUT, sub { print while <IN> });
+    }
+  }
+  close(IN);
   $main::lxdebug->leave_sub();
 }
 
@@ -1246,36 +1088,61 @@ sub get_formname_translation {
   local $::locale = Locale->new($self->{recipient_locale});
 
   my %formname_translations = (
-    bin_list                => $main::locale->text('Bin List'),
-    credit_note             => $main::locale->text('Credit Note'),
-    invoice                 => $main::locale->text('Invoice'),
-    pick_list               => $main::locale->text('Pick List'),
-    proforma                => $main::locale->text('Proforma Invoice'),
-    purchase_order          => $main::locale->text('Purchase Order'),
-    request_quotation       => $main::locale->text('RFQ'),
-    sales_order             => $main::locale->text('Confirmation'),
-    sales_quotation         => $main::locale->text('Quotation'),
-    storno_invoice          => $main::locale->text('Storno Invoice'),
-    sales_delivery_order    => $main::locale->text('Delivery Order'),
-    purchase_delivery_order => $main::locale->text('Delivery Order'),
-    dunning                 => $main::locale->text('Dunning'),
-    letter                  => $main::locale->text('Letter')
+    bin_list                    => $main::locale->text('Bin List'),
+    credit_note                 => $main::locale->text('Credit Note'),
+    invoice                     => $main::locale->text('Invoice'),
+    invoice_copy                => $main::locale->text('Invoice Copy'),
+    invoice_for_advance_payment => $main::locale->text('Invoice for Advance Payment'),
+    final_invoice               => $main::locale->text('Final Invoice'),
+    pick_list                   => $main::locale->text('Pick List'),
+    proforma                    => $main::locale->text('Proforma Invoice'),
+    purchase_order              => $main::locale->text('Purchase Order'),
+    request_quotation           => $main::locale->text('RFQ'),
+    sales_order                 => $main::locale->text('Confirmation'),
+    sales_quotation             => $main::locale->text('Quotation'),
+    storno_invoice              => $main::locale->text('Storno Invoice'),
+    sales_delivery_order        => $main::locale->text('Delivery Order'),
+    purchase_delivery_order     => $main::locale->text('Delivery Order'),
+    supplier_delivery_order     => $main::locale->text('Supplier Delivery Order'),
+    rma_delivery_order          => $main::locale->text('RMA Delivery Order'),
+    dunning                     => $main::locale->text('Dunning'),
+    dunning1                    => $main::locale->text('Payment Reminder'),
+    dunning2                    => $main::locale->text('Dunning'),
+    dunning3                    => $main::locale->text('Last Dunning'),
+    dunning_invoice             => $main::locale->text('Dunning Invoice'),
+    letter                      => $main::locale->text('Letter'),
+    ic_supply                   => $main::locale->text('Intra-Community supply'),
+    statement                   => $main::locale->text('Statement'),
   );
 
   $main::lxdebug->leave_sub();
   return $formname_translations{$formname};
 }
 
+sub get_cusordnumber_translation {
+  $main::lxdebug->enter_sub();
+  my ($self, $formname) = @_;
+
+  $formname ||= $self->{formname};
+
+  $self->{recipient_locale} ||=  Locale->lang_to_locale($self->{language});
+  local $::locale = Locale->new($self->{recipient_locale});
+
+
+  $main::lxdebug->leave_sub();
+  return $main::locale->text('Your Order');
+}
+
 sub get_number_prefix_for_type {
   $main::lxdebug->enter_sub();
   my ($self) = @_;
 
   my $prefix =
-      (first { $self->{type} eq $_ } qw(invoice credit_note)) ? 'inv'
-    : ($self->{type} =~ /_quotation$/)                        ? 'quo'
-    : ($self->{type} =~ /_delivery_order$/)                   ? 'do'
-    : ($self->{type} =~ /letter/)                             ? 'letter'
-    :                                                           'ord';
+      (first { $self->{type} eq $_ } qw(invoice invoice_for_advance_payment final_invoice credit_note)) ? 'inv'
+    : ($self->{type} =~ /_quotation$/)                                                                  ? 'quo'
+    : ($self->{type} =~ /_delivery_order$/)                                                             ? 'do'
+    : ($self->{type} =~ /letter/)                                                                       ? 'letter'
+    :                                                                                                     'ord';
 
   # better default like this?
   # : ($self->{type} =~ /(sales|purcharse)_order/           :  'ord';
@@ -1310,7 +1177,7 @@ sub generate_attachment_filename {
   my $attachment_filename = $main::locale->unquote_special_chars('HTML', $self->get_formname_translation());
   my $prefix              = $self->get_number_prefix_for_type();
 
-  if ($self->{preview} && (first { $self->{type} eq $_ } qw(invoice credit_note))) {
+  if ($self->{preview} && (first { $self->{type} eq $_ } qw(invoice invoice_for_advance_payment final_invoice credit_note))) {
     $attachment_filename .= ' (' . $recipient_locale->text('Preview') . ')' . $self->get_extension_for_format();
 
   } elsif ($attachment_filename && $self->{"${prefix}number"}) {
@@ -1341,10 +1208,51 @@ sub generate_email_subject {
     $subject .= " " . $self->{"${prefix}number"}
   }
 
+  if ($self->{cusordnumber}) {
+    $subject = $self->get_cusordnumber_translation() . ' ' . $self->{cusordnumber} . ' / ' . $subject;
+  }
+
   $main::lxdebug->leave_sub();
   return $subject;
 }
 
+sub generate_email_body {
+  $main::lxdebug->enter_sub();
+  my ($self, %params) = @_;
+  # simple german and english will work grammatically (most european languages as well)
+  # Dear Mr Alan Greenspan:
+  # Sehr geehrte Frau Meyer,
+  # A l’attention de Mme Villeroy,
+  # Gentile Signora Ferrari,
+  my $body = '';
+
+  if ($self->{cp_id} && !$params{record_email}) {
+    my $givenname = SL::DB::Contact->load_cached($self->{cp_id})->cp_givenname; # for qw(gender givename name);
+    my $name      = SL::DB::Contact->load_cached($self->{cp_id})->cp_name; # for qw(gender givename name);
+    my $gender    = SL::DB::Contact->load_cached($self->{cp_id})->cp_gender; # for qw(gender givename name);
+    my $mf = $gender eq 'f' ? 'female' : 'male';
+    $body  = GenericTranslations->get(translation_type => "salutation_$mf", language_id => $self->{language_id});
+    $body .= ' ' . $givenname . ' ' . $name if $body;
+  } else {
+    $body  = GenericTranslations->get(translation_type => "salutation_general", language_id => $self->{language_id});
+  }
+
+  return undef unless $body;
+
+  $body .= GenericTranslations->get(translation_type => "salutation_punctuation_mark", language_id => $self->{language_id});
+  $body  = '<p>' . $::locale->quote_special_chars('HTML', $body) . '</p>';
+
+  my $translation_type = $params{translation_type} // "preset_text_$self->{formname}";
+  my $main_body        = GenericTranslations->get(translation_type => $translation_type,                  language_id => $self->{language_id});
+  $main_body           = GenericTranslations->get(translation_type => $params{fallback_translation_type}, language_id => $self->{language_id}) if !$main_body && $params{fallback_translation_type};
+  $body               .= $main_body;
+
+  $body = $main::locale->unquote_special_chars('HTML', $body);
+
+  $main::lxdebug->leave_sub();
+  return $body;
+}
+
 sub cleanup {
   $main::lxdebug->enter_sub();
 
@@ -1414,69 +1322,29 @@ sub datetonum {
 }
 
 # Database routines used throughout
+# DB Handling got moved to SL::DB, these are only shims for compatibility
 
 sub dbconnect {
-  $main::lxdebug->enter_sub(2);
-
-  my ($self, $myconfig) = @_;
-
-  # connect to database
-  my $dbh = SL::DBConnect->connect or $self->dberror;
-
-  # set db options
-  if ($myconfig->{dboptions}) {
-    $dbh->do($myconfig->{dboptions}) || $self->dberror($myconfig->{dboptions});
-  }
-
-  $main::lxdebug->leave_sub(2);
-
-  return $dbh;
-}
-
-sub dbconnect_noauto {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig) = @_;
-
-  # connect to database
-  my $dbh = SL::DBConnect->connect(SL::DBConnect->get_connect_args(AutoCommit => 0)) or $self->dberror;
-
-  # set db options
-  if ($myconfig->{dboptions}) {
-    $dbh->do($myconfig->{dboptions}) || $self->dberror($myconfig->{dboptions});
-  }
-
-  $main::lxdebug->leave_sub();
-
-  return $dbh;
+  SL::DB->client->dbh;
 }
 
 sub get_standard_dbh {
-  $main::lxdebug->enter_sub(2);
+  my $dbh = SL::DB->client->dbh;
 
-  my $self     = shift;
-  my $myconfig = shift || \%::myconfig;
-
-  if ($standard_dbh && !$standard_dbh->{Active}) {
-    $main::lxdebug->message(LXDebug->INFO(), "get_standard_dbh: \$standard_dbh is defined but not Active anymore");
-    undef $standard_dbh;
+  if ($dbh && !$dbh->{Active}) {
+    $main::lxdebug->message(LXDebug->INFO(), "get_standard_dbh: \$dbh is defined but not Active anymore");
+    SL::DB->client->dbh(undef);
   }
 
-  $standard_dbh ||= $self->dbconnect_noauto($myconfig);
-
-  $main::lxdebug->leave_sub(2);
-
-  return $standard_dbh;
+  SL::DB->client->dbh;
 }
 
-sub set_standard_dbh {
-  my ($self, $dbh) = @_;
-  my $old_dbh      = $standard_dbh;
-  $standard_dbh    = $dbh;
-
-  return $old_dbh;
+sub disconnect_standard_dbh {
+  SL::DB->client->dbh->rollback;
 }
 
+# /database
+
 sub date_closed {
   $main::lxdebug->enter_sub();
 
@@ -1614,18 +1482,18 @@ sub save_exchangerate {
 
   my ($self, $myconfig, $currency, $transdate, $rate, $fld) = @_;
 
-  my $dbh = $self->dbconnect($myconfig);
-
-  my ($buy, $sell);
-
-  $buy  = $rate if $fld eq 'buy';
-  $sell = $rate if $fld eq 'sell';
+  SL::DB->client->with_transaction(sub {
+    my $dbh = SL::DB->client->dbh;
 
+    my ($buy, $sell);
 
-  $self->update_exchangerate($dbh, $currency, $transdate, $buy, $sell);
+    $buy  = $rate if $fld eq 'buy';
+    $sell = $rate if $fld eq 'sell';
 
 
-  $dbh->disconnect;
+    $self->update_exchangerate($dbh, $currency, $transdate, $buy, $sell);
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
@@ -1723,16 +1591,17 @@ sub get_default_currency {
 }
 
 sub set_payment_options {
-  my ($self, $myconfig, $transdate) = @_;
+  my ($self, $myconfig, $transdate, $type) = @_;
 
   my $terms = $self->{payment_id} ? SL::DB::PaymentTerm->new(id => $self->{payment_id})->load : undef;
   return if !$terms;
 
+  my $is_invoice                = $type =~ m{invoice}i;
+
   $transdate                  ||= $self->{invdate} || $self->{transdate};
   my $due_date                  = $self->{duedate} || $self->{reqdate};
 
   $self->{$_}                   = $terms->$_ for qw(terms_netto terms_skonto percent_skonto);
-  $self->{payment_terms}        = $terms->description_long;
   $self->{payment_description}  = $terms->description;
   $self->{netto_date}           = $terms->calc_date(reference_date => $transdate, due_date => $due_date, terms => 'net')->to_kivitendo;
   $self->{skonto_date}          = $terms->calc_date(reference_date => $transdate, due_date => $due_date, terms => 'discount')->to_kivitendo;
@@ -1765,39 +1634,26 @@ sub set_payment_options {
   }
 
   if ($self->{"language_id"}) {
-    my $dbh   = $self->get_standard_dbh($myconfig);
-    my $query =
-      qq|SELECT t.translation, l.output_numberformat, l.output_dateformat, l.output_longdates | .
-      qq|FROM generic_translations t | .
-      qq|LEFT JOIN language l ON t.language_id = l.id | .
-      qq|WHERE (t.language_id = ?)
-           AND (t.translation_id = ?)
-           AND (t.translation_type = 'SL::DB::PaymentTerm/description_long')|;
-    my ($description_long, $output_numberformat, $output_dateformat,
-      $output_longdates) =
-      selectrow_query($self, $dbh, $query,
-                      $self->{"language_id"}, $self->{"payment_id"});
-
-    $self->{payment_terms} = $description_long if ($description_long);
-
-    if ($output_dateformat) {
+    my $language             = SL::DB::Language->new(id => $self->{language_id})->load;
+
+    $self->{payment_terms}   = $type =~ m{invoice}i ? $terms->translated_attribute('description_long_invoice', $language->id) : undef;
+    $self->{payment_terms} ||= $terms->translated_attribute('description_long', $language->id);
+
+    if ($language->output_dateformat) {
       foreach my $key (qw(netto_date skonto_date)) {
-        $self->{$key} =
-          $main::locale->reformat_date($myconfig, $self->{$key},
-                                       $output_dateformat,
-                                       $output_longdates);
+        $self->{$key} = $::locale->reformat_date($myconfig, $self->{$key}, $language->output_dateformat, $language->output_longdates);
       }
     }
 
-    if ($output_numberformat &&
-        ($output_numberformat ne $myconfig->{"numberformat"})) {
-      my $saved_numberformat = $myconfig->{"numberformat"};
-      $myconfig->{"numberformat"} = $output_numberformat;
-      map { $formatted_amounts{$_} = $self->format_amount($myconfig, $amounts{$_}) } keys %amounts;
-      $myconfig->{"numberformat"} = $saved_numberformat;
+    if ($language->output_numberformat && ($language->output_numberformat ne $myconfig->{numberformat})) {
+      local $myconfig->{numberformat};
+      $myconfig->{"numberformat"} = $language->output_numberformat;
+      $formatted_amounts{$_} = $self->format_amount($myconfig, $amounts{$_}) for keys %amounts;
     }
   }
 
+  $self->{payment_terms} =  $self->{payment_terms} || ($is_invoice ? $terms->description_long_invoice : undef) || $terms->description_long;
+
   $self->{payment_terms} =~ s/<%netto_date%>/$self->{netto_date}/g;
   $self->{payment_terms} =~ s/<%skonto_date%>/$self->{skonto_date}/g;
   $self->{payment_terms} =~ s/<%currency%>/$self->{currency}/g;
@@ -1864,73 +1720,100 @@ sub get_shipto {
     my $query = qq|SELECT * FROM shipto WHERE shipto_id = ?|;
     my $ref = selectfirst_hashref_query($self, $dbh, $query, $self->{shipto_id});
     map({ $self->{$_} = $ref->{$_} } keys(%$ref));
+
+    my $cvars = CVar->get_custom_variables(
+      dbh      => $dbh,
+      module   => 'ShipTo',
+      trans_id => $self->{shipto_id},
+    );
+    $self->{"shiptocvar_$_->{name}"} = $_->{value} for @{ $cvars };
   }
 
   $main::lxdebug->leave_sub();
 }
 
 sub add_shipto {
-  $main::lxdebug->enter_sub();
-
   my ($self, $dbh, $id, $module) = @_;
 
   my $shipto;
   my @values;
 
-  foreach my $item (qw(name department_1 department_2 street zipcode city country
-                       contact cp_gender phone fax email)) {
+  foreach my $item (qw(name department_1 department_2 street zipcode city country gln
+                       contact phone fax email)) {
     if ($self->{"shipto$item"}) {
       $shipto = 1 if ($self->{$item} ne $self->{"shipto$item"});
     }
     push(@values, $self->{"shipto${item}"});
   }
 
-  if ($shipto) {
-    if ($self->{shipto_id}) {
-      my $query = qq|UPDATE shipto set
-                       shiptoname = ?,
-                       shiptodepartment_1 = ?,
-                       shiptodepartment_2 = ?,
-                       shiptostreet = ?,
-                       shiptozipcode = ?,
-                       shiptocity = ?,
-                       shiptocountry = ?,
-                       shiptocontact = ?,
-                       shiptocp_gender = ?,
-                       shiptophone = ?,
-                       shiptofax = ?,
-                       shiptoemail = ?
-                     WHERE shipto_id = ?|;
-      do_query($self, $dbh, $query, @values, $self->{shipto_id});
-    } else {
-      my $query = qq|SELECT * FROM shipto
-                     WHERE shiptoname = ? AND
-                       shiptodepartment_1 = ? AND
-                       shiptodepartment_2 = ? AND
-                       shiptostreet = ? AND
-                       shiptozipcode = ? AND
-                       shiptocity = ? AND
-                       shiptocountry = ? AND
-                       shiptocontact = ? AND
-                       shiptocp_gender = ? AND
-                       shiptophone = ? AND
-                       shiptofax = ? AND
-                       shiptoemail = ? AND
-                       module = ? AND
-                       trans_id = ?|;
-      my $insert_check = selectfirst_hashref_query($self, $dbh, $query, @values, $module, $id);
-      if(!$insert_check){
-        $query =
-          qq|INSERT INTO shipto (trans_id, shiptoname, shiptodepartment_1, shiptodepartment_2,
-                                 shiptostreet, shiptozipcode, shiptocity, shiptocountry,
-                                 shiptocontact, shiptocp_gender, shiptophone, shiptofax, shiptoemail, module)
-             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)|;
-        do_query($self, $dbh, $query, $id, @values, $module);
-      }
+  return if !$shipto;
+
+  # shiptocp_gender only makes sense, if any other shipto attribute is set.
+  # Because shiptocp_gender is set to 'm' by default in forms
+  # it must not be considered above to decide if shiptos has to be added or
+  # updated, but must be inserted or updated as well in case.
+  push(@values, $self->{shiptocp_gender});
+
+  my $shipto_id = $self->{shipto_id};
+
+  if ($self->{shipto_id}) {
+    my $query = qq|UPDATE shipto set
+                     shiptoname = ?,
+                     shiptodepartment_1 = ?,
+                     shiptodepartment_2 = ?,
+                     shiptostreet = ?,
+                     shiptozipcode = ?,
+                     shiptocity = ?,
+                     shiptocountry = ?,
+                     shiptogln = ?,
+                     shiptocontact = ?,
+                     shiptophone = ?,
+                     shiptofax = ?,
+                     shiptoemail = ?
+                     shiptocp_gender = ?,
+                   WHERE shipto_id = ?|;
+    do_query($self, $dbh, $query, @values, $self->{shipto_id});
+  } else {
+    my $query = qq|SELECT * FROM shipto
+                   WHERE shiptoname = ? AND
+                     shiptodepartment_1 = ? AND
+                     shiptodepartment_2 = ? AND
+                     shiptostreet = ? AND
+                     shiptozipcode = ? AND
+                     shiptocity = ? AND
+                     shiptocountry = ? AND
+                     shiptogln = ? AND
+                     shiptocontact = ? AND
+                     shiptophone = ? AND
+                     shiptofax = ? AND
+                     shiptoemail = ? AND
+                     shiptocp_gender = ? AND
+                     module = ? AND
+                     trans_id = ?|;
+    my $insert_check = selectfirst_hashref_query($self, $dbh, $query, @values, $module, $id);
+    if(!$insert_check){
+      my $insert_query =
+        qq|INSERT INTO shipto (trans_id, shiptoname, shiptodepartment_1, shiptodepartment_2,
+                               shiptostreet, shiptozipcode, shiptocity, shiptocountry, shiptogln,
+                               shiptocontact, shiptophone, shiptofax, shiptoemail, shiptocp_gender, module)
+           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)|;
+      do_query($self, $dbh, $insert_query, $id, @values, $module);
+
+      $insert_check = selectfirst_hashref_query($self, $dbh, $query, @values, $module, $id);
     }
+
+    $shipto_id = $insert_check->{shipto_id};
   }
 
-  $main::lxdebug->leave_sub();
+  return unless $shipto_id;
+
+  CVar->save_custom_variables(
+    dbh         => $dbh,
+    module      => 'ShipTo',
+    trans_id    => $shipto_id,
+    variables   => $self,
+    name_prefix => 'shipto',
+  );
 }
 
 sub get_employee {
@@ -2060,26 +1943,6 @@ sub _get_projects {
   $main::lxdebug->leave_sub();
 }
 
-sub _get_shipto {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $dbh, $vc_id, $key) = @_;
-
-  $key = "all_shipto" unless ($key);
-
-  if ($vc_id) {
-    # get shipping addresses
-    my $query = qq|SELECT * FROM shipto WHERE trans_id = ?|;
-
-    $self->{$key} = selectall_hashref_query($self, $dbh, $query, $vc_id);
-
-  } else {
-    $self->{$key} = [];
-  }
-
-  $main::lxdebug->leave_sub();
-}
-
 sub _get_printers {
   $main::lxdebug->enter_sub();
 
@@ -2119,36 +1982,6 @@ sub _get_charts {
   $main::lxdebug->leave_sub();
 }
 
-sub _get_taxcharts {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $dbh, $params) = @_;
-
-  my $key = "all_taxcharts";
-  my @where;
-
-  if (ref $params eq 'HASH') {
-    $key = $params->{key} if ($params->{key});
-    if ($params->{module} eq 'AR') {
-      push @where, 'chart_categories ~ \'[ACILQ]\'';
-
-    } elsif ($params->{module} eq 'AP') {
-      push @where, 'chart_categories ~ \'[ACELQ]\'';
-    }
-
-  } elsif ($params) {
-    $key = $params;
-  }
-
-  my $where = @where ? ' WHERE ' . join(' AND ', map { "($_)" } @where) : '';
-
-  my $query = qq|SELECT * FROM tax $where ORDER BY taxkey, rate|;
-
-  $self->{$key} = selectall_hashref_query($self, $dbh, $query);
-
-  $main::lxdebug->leave_sub();
-}
-
 sub _get_taxzones {
   $main::lxdebug->enter_sub();
 
@@ -2362,31 +2195,19 @@ sub _get_simple {
   $main::lxdebug->leave_sub();
 }
 
-#sub _get_groups {
-#  $main::lxdebug->enter_sub();
-#
-#  my ($self, $dbh, $key) = @_;
-#
-#  $key ||= "all_groups";
-#
-#  my $groups = $main::auth->read_groups();
-#
-#  $self->{$key} = selectall_hashref_query($self, $dbh, $query);
-#
-#  $main::lxdebug->leave_sub();
-#}
-
 sub get_lists {
   $main::lxdebug->enter_sub();
 
   my $self = shift;
   my %params = @_;
 
+  croak "get_lists: shipto is no longer supported" if $params{shipto};
+
   my $dbh = $self->get_standard_dbh(\%main::myconfig);
   my ($sth, $query, $ref);
 
   my ($vc, $vc_id);
-  if ($params{contacts} || $params{shipto}) {
+  if ($params{contacts}) {
     $vc = 'customer' if $self->{"vc"} eq "customer";
     $vc = 'vendor'   if $self->{"vc"} eq "vendor";
     die "invalid use of get_lists, need 'vc'" unless $vc;
@@ -2397,10 +2218,6 @@ sub get_lists {
     $self->_get_contacts($dbh, $vc_id, $params{"contacts"});
   }
 
-  if ($params{"shipto"}) {
-    $self->_get_shipto($dbh, $vc_id, $params{"shipto"});
-  }
-
   if ($params{"projects"} || $params{"all_projects"}) {
     $self->_get_projects($dbh, $params{"all_projects"} ?
                          $params{"all_projects"} : $params{"projects"},
@@ -2419,10 +2236,6 @@ sub get_lists {
     $self->_get_charts($dbh, $params{"charts"});
   }
 
-  if ($params{"taxcharts"}) {
-    $self->_get_taxcharts($dbh, $params{"taxcharts"});
-  }
-
   if ($params{"taxzones"}) {
     $self->_get_taxzones($dbh, $params{"taxzones"});
   }
@@ -2475,10 +2288,6 @@ sub get_lists {
     $self->_get_warehouses($dbh, $params{warehouses});
   }
 
-#  if ($params{groups}) {
-#    $self->_get_groups($dbh, $params{groups});
-#  }
-
   if ($params{partsgroup}) {
     $self->get_partsgroup(\%main::myconfig, { all => 1, target => $params{partsgroup} });
   }
@@ -2504,10 +2313,10 @@ sub get_name {
     my $where;
     if ($self->{customernumber} ne "") {
       $where = qq|(vc.customernumber ILIKE ?)|;
-      push(@values, '%' . $self->{customernumber} . '%');
+      push(@values, like($self->{customernumber}));
     } else {
       $where = qq|(vc.name ILIKE ?)|;
-      push(@values, '%' . $self->{$table} . '%');
+      push(@values, like($self->{$table}));
     }
 
     $query =
@@ -2524,7 +2333,7 @@ sub get_name {
          JOIN $table vc ON (a.${table}_id = vc.id)
          WHERE NOT (a.amount = a.paid) AND (vc.name ILIKE ?)
          ORDER BY vc.name~;
-    push(@values, '%' . $self->{$table} . '%');
+    push(@values, like($self->{$table}));
   }
 
   $self->{name_list} = selectall_hashref_query($self, $dbh, $query, @values);
@@ -2534,77 +2343,19 @@ sub get_name {
   return scalar(@{ $self->{name_list} });
 }
 
-# the selection sub is used in the AR, AP, IS, IR, DO and OE module
-#
-sub all_vc {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $table, $module) = @_;
-
-  my $ref;
-  my $dbh = $self->get_standard_dbh;
-
-  $table = $table eq "customer" ? "customer" : "vendor";
-
-  # build selection list
-  # Hotfix für Bug 1837 - Besser wäre es alte Buchungsbelege
-  # OHNE Auswahlliste (reines Textfeld) zu laden. Hilft aber auch
-  # nicht für veränderbare Belege (oe, do, ...)
-  my $obsolete = $self->{id} ? '' : "WHERE NOT obsolete";
-  my $query = qq|SELECT count(*) FROM $table $obsolete|;
-  my ($count) = selectrow_query($self, $dbh, $query);
-
-  if ($count <= $myconfig->{vclimit}) {
-    $query = qq|SELECT id, name, salesman_id
-                FROM $table $obsolete
-                ORDER BY name|;
-    $self->{"all_$table"} = selectall_hashref_query($self, $dbh, $query);
-  }
-
-  # get self
-  $self->get_employee($dbh);
-
-  # setup sales contacts
-  $query = qq|SELECT e.id, e.name
-              FROM employee e
-              WHERE (e.sales = '1') AND (NOT e.id = ?)
-              ORDER BY name|;
-  $self->{all_employees} = selectall_hashref_query($self, $dbh, $query, $self->{employee_id});
-
-  # this is for self
-  push(@{ $self->{all_employees} },
-       { id   => $self->{employee_id},
-         name => $self->{employee} });
-
-    # prepare query for departments
-    $query = qq|SELECT id, description
-                FROM department
-                ORDER BY description|;
-
-  $self->{all_departments} = selectall_hashref_query($self, $dbh, $query);
-
-  # get languages
-  $query = qq|SELECT id, description
-              FROM language
-              ORDER BY id|;
+sub new_lastmtime {
 
-  $self->{languages} = selectall_hashref_query($self, $dbh, $query);
+  my ($self, $table, $provided_dbh) = @_;
 
-  # get printer
-  $query = qq|SELECT printer_description, id
-              FROM printers
-              ORDER BY printer_description|;
-
-  $self->{printers} = selectall_hashref_query($self, $dbh, $query);
-
-  # get payment terms
-  $query = qq|SELECT id, description
-              FROM payment_terms
-              ORDER BY sortkey|;
+  my $dbh = $provided_dbh ? $provided_dbh : $self->get_standard_dbh;
+  return                                       unless $self->{id};
+  croak ("wrong call, no valid table defined") unless $table =~ /^(oe|ar|ap|delivery_orders|parts)$/;
 
-  $self->{payment_terms} = selectall_hashref_query($self, $dbh, $query);
+  my $query       = "SELECT mtime, itime FROM " . $table . " WHERE id = ?";
+  my $ref         = selectfirst_hashref_query($self, $dbh, $query, $self->{id});
+  $ref->{mtime} ||= $ref->{itime};
+  $self->{lastmtime} = $ref->{mtime};
 
-  $main::lxdebug->leave_sub();
 }
 
 sub mtime_ischanged {
@@ -2622,10 +2373,13 @@ sub mtime_ischanged {
         t8("The document has been changed by another user. No mail was sent. Please reopen it in another window and copy the changes to the new window") :
         t8("The document has been changed by another user. Please reopen it in another window and copy the changes to the new window")
       );
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
 }
 
+# language_payment duplicates some of the functionality of all_vc (language,
+# printer, payment_terms), and at least in the case of sales invoices both
+# all_vc and language_payment are called when adding new invoices
 sub language_payment {
   $main::lxdebug->enter_sub();
 
@@ -2649,9 +2403,9 @@ sub language_payment {
   # get payment terms
   $query = qq|SELECT id, description
               FROM payment_terms
-              ORDER BY sortkey|;
-
-  $self->{payment_terms} = selectall_hashref_query($self, $dbh, $query);
+              WHERE ( obsolete IS FALSE OR id = ? )
+              ORDER BY sortkey |;
+  $self->{payment_terms} = selectall_hashref_query($self, $dbh, $query, $self->{payment_id} || undef);
 
   # get buchungsgruppen
   $query = qq|SELECT id, description
@@ -2695,8 +2449,6 @@ sub create_links {
     $arap = "ap";
   }
 
-  $self->all_vc($myconfig, $table, $module);
-
   # get last customers or vendors
   my ($query, $sth, $ref);
 
@@ -2711,15 +2463,8 @@ sub create_links {
     }
 
     # now get the account numbers
-#    $query = qq|SELECT c.accno, c.description, c.link, c.taxkey_id, tk.tax_id
-#                FROM chart c, taxkeys tk
-#                WHERE (c.link LIKE ?) AND (c.id = tk.chart_id) AND tk.id =
-#                  (SELECT id FROM taxkeys WHERE (taxkeys.chart_id = c.id) AND (startdate <= $transdate) ORDER BY startdate DESC LIMIT 1)
-#                ORDER BY c.accno|;
-
-#  same query as above, but without expensive subquery for each row. about 80% faster
     $query = qq|
-      SELECT c.accno, c.description, c.link, c.taxkey_id, tk2.tax_id
+      SELECT c.accno, c.description, c.link, c.taxkey_id, c.id AS chart_id, tk2.tax_id
         FROM chart c
         -- find newest entries in taxkeys
         INNER JOIN (
@@ -2736,7 +2481,7 @@ sub create_links {
 
     $sth = $dbh->prepare($query);
 
-    do_statement($self, $sth, $query, '%' . $module . '%');
+    do_statement($self, $sth, $query, like($module));
 
     $self->{accounts} = "";
     while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
@@ -2749,6 +2494,7 @@ sub create_links {
 
           push @{ $self->{"${module}_links"}{$key} },
             { accno       => $ref->{accno},
+              chart_id    => $ref->{chart_id},
               description => $ref->{description},
               taxkey      => $ref->{taxkey_id},
               tax_id      => $ref->{tax_id} };
@@ -2775,12 +2521,12 @@ sub create_links {
   if ($self->{id}) {
     $query =
       qq|SELECT
-           a.cp_id, a.invnumber, a.transdate, a.${table}_id, a.datepaid,
-           a.duedate, a.ordnumber, a.taxincluded, (SELECT cu.name FROM currencies cu WHERE cu.id=a.currency_id) AS currency, a.notes,
+           a.cp_id, a.invnumber, a.transdate, a.${table}_id, a.datepaid, a.deliverydate,
+           a.duedate, a.tax_point, a.ordnumber, a.taxincluded, (SELECT cu.name FROM currencies cu WHERE cu.id=a.currency_id) AS currency, a.notes,
            a.mtime, a.itime,
            a.intnotes, a.department_id, a.amount AS oldinvtotal,
            a.paid AS oldtotalpaid, a.employee_id, a.gldate, a.type,
-           a.globalproject_id, ${extra_columns}
+           a.globalproject_id, a.transaction_description, ${extra_columns}
            c.name AS $table,
            d.description AS department,
            e.name AS employee
@@ -2802,7 +2548,7 @@ sub create_links {
     }
 
     # now get the account numbers
-    $query = qq|SELECT c.accno, c.description, c.link, c.taxkey_id, tk.tax_id
+    $query = qq|SELECT c.accno, c.description, c.link, c.taxkey_id, c.id AS chart_id, tk.tax_id
                 FROM chart c
                 LEFT JOIN taxkeys tk ON (tk.chart_id = c.id)
                 WHERE c.link LIKE ?
@@ -2811,7 +2557,7 @@ sub create_links {
                 ORDER BY c.accno|;
 
     $sth = $dbh->prepare($query);
-    do_statement($self, $sth, $query, "%$module%");
+    do_statement($self, $sth, $query, like($module));
 
     $self->{accounts} = "";
     while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
@@ -2824,6 +2570,7 @@ sub create_links {
 
           push @{ $self->{"${module}_links"}{$key} },
             { accno       => $ref->{accno},
+              chart_id    => $ref->{chart_id},
               description => $ref->{description},
               taxkey      => $ref->{taxkey_id},
               tax_id      => $ref->{tax_id} };
@@ -2838,7 +2585,7 @@ sub create_links {
     $query =
       qq|SELECT
            c.accno, c.description,
-           a.acc_trans_id, a.source, a.amount, a.memo, a.transdate, a.gldate, a.cleared, a.project_id, a.taxkey,
+           a.acc_trans_id, a.source, a.amount, a.memo, a.transdate, a.gldate, a.cleared, a.project_id, a.taxkey, a.chart_id,
            p.projectnumber,
            t.rate, t.id
          FROM acc_trans a
@@ -2878,7 +2625,9 @@ sub create_links {
            d.closedto, d.revtrans,
            (SELECT cu.name FROM currencies cu WHERE cu.id=d.currency_id) AS defaultcurrency,
            (SELECT c.accno FROM chart c WHERE d.fxgain_accno_id = c.id) AS fxgain_accno,
-           (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id = c.id) AS fxloss_accno
+           (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id = c.id) AS fxloss_accno,
+           (SELECT c.accno FROM chart c WHERE d.rndgain_accno_id = c.id) AS rndgain_accno,
+           (SELECT c.accno FROM chart c WHERE d.rndloss_accno_id = c.id) AS rndloss_accno
          FROM defaults d|;
     $ref = selectfirst_hashref_query($self, $dbh, $query);
     map { $self->{$_} = $ref->{$_} } keys %$ref;
@@ -2891,7 +2640,9 @@ sub create_links {
             current_date AS transdate, d.closedto, d.revtrans,
             (SELECT cu.name FROM currencies cu WHERE cu.id=d.currency_id) AS defaultcurrency,
             (SELECT c.accno FROM chart c WHERE d.fxgain_accno_id = c.id) AS fxgain_accno,
-            (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id = c.id) AS fxloss_accno
+            (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id = c.id) AS fxloss_accno,
+            (SELECT c.accno FROM chart c WHERE d.rndgain_accno_id = c.id) AS rndgain_accno,
+            (SELECT c.accno FROM chart c WHERE d.rndloss_accno_id = c.id) AS rndloss_accno
           FROM defaults d|;
     $ref = selectfirst_hashref_query($self, $dbh, $query);
     map { $self->{$_} = $ref->{$_} } keys %$ref;
@@ -2971,6 +2722,51 @@ sub lastname_used {
   $main::lxdebug->leave_sub();
 }
 
+sub get_variable_content_types {
+  my ($self) = @_;
+
+  my %html_variables = (
+    longdescription  => 'html',
+    partnotes        => 'html',
+    notes            => 'html',
+    orignotes        => 'html',
+    notes1           => 'html',
+    notes2           => 'html',
+    notes3           => 'html',
+    notes4           => 'html',
+    header_text      => 'html',
+    footer_text      => 'html',
+  );
+
+  return {
+    %html_variables,
+    $self->get_variable_content_types_for_cvars,
+  };
+}
+
+sub get_variable_content_types_for_cvars {
+  my ($self)       = @_;
+  my $html_configs = SL::DB::Manager::CustomVariableConfig->get_all(where => [ type => 'htmlfield' ]);
+  my %types;
+
+  if (@{ $html_configs }) {
+    my %prefix_by_module = (
+      Contacts => 'cp_cvar_',
+      CT       => 'vc_cvar_',
+      IC       => 'ic_cvar_',
+      Projects => 'project_cvar_',
+      ShipTo   => 'shiptocvar_',
+    );
+
+    foreach my $cfg (@{ $html_configs }) {
+      my $prefix = $prefix_by_module{$cfg->module};
+      $types{$prefix . $cfg->name} = 'html' if $prefix;
+    }
+  }
+
+  return %types;
+}
+
 sub current_date {
   $main::lxdebug->enter_sub();
 
@@ -2998,22 +2794,6 @@ sub current_date {
   return $thisdate;
 }
 
-sub like {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $string) = @_;
-
-  if ($string !~ /%/) {
-    $string = "%$string%";
-  }
-
-  $string =~ s/\'/\'\'/g;
-
-  $main::lxdebug->leave_sub();
-
-  return $string;
-}
-
 sub redo_rows {
   $main::lxdebug->enter_sub();
 
@@ -3047,52 +2827,52 @@ sub update_status {
 
   my ($i, $id);
 
-  my $dbh = $self->dbconnect_noauto($myconfig);
+  SL::DB->client->with_transaction(sub {
+    my $dbh = SL::DB->client->dbh;
 
-  my $query = qq|DELETE FROM status
-                 WHERE (formname = ?) AND (trans_id = ?)|;
-  my $sth = prepare_query($self, $dbh, $query);
+    my $query = qq|DELETE FROM status
+                   WHERE (formname = ?) AND (trans_id = ?)|;
+    my $sth = prepare_query($self, $dbh, $query);
 
-  if ($self->{formname} =~ /(check|receipt)/) {
-    for $i (1 .. $self->{rowcount}) {
-      do_statement($self, $sth, $query, $self->{formname}, $self->{"id_$i"} * 1);
+    if ($self->{formname} =~ /(check|receipt)/) {
+      for $i (1 .. $self->{rowcount}) {
+        do_statement($self, $sth, $query, $self->{formname}, $self->{"id_$i"} * 1);
+      }
+    } else {
+      do_statement($self, $sth, $query, $self->{formname}, $self->{id});
     }
-  } else {
-    do_statement($self, $sth, $query, $self->{formname}, $self->{id});
-  }
-  $sth->finish();
+    $sth->finish();
 
-  my $printed = ($self->{printed} =~ /\Q$self->{formname}\E/) ? "1" : "0";
-  my $emailed = ($self->{emailed} =~ /\Q$self->{formname}\E/) ? "1" : "0";
+    my $printed = ($self->{printed} =~ /\Q$self->{formname}\E/) ? "1" : "0";
+    my $emailed = ($self->{emailed} =~ /\Q$self->{formname}\E/) ? "1" : "0";
 
-  my %queued = split / /, $self->{queued};
-  my @values;
+    my %queued = split / /, $self->{queued};
+    my @values;
 
-  if ($self->{formname} =~ /(check|receipt)/) {
+    if ($self->{formname} =~ /(check|receipt)/) {
 
-    # this is a check or receipt, add one entry for each lineitem
-    my ($accno) = split /--/, $self->{account};
-    $query = qq|INSERT INTO status (trans_id, printed, spoolfile, formname, chart_id)
-                VALUES (?, ?, ?, ?, (SELECT c.id FROM chart c WHERE c.accno = ?))|;
-    @values = ($printed, $queued{$self->{formname}}, $self->{prinform}, $accno);
-    $sth = prepare_query($self, $dbh, $query);
+      # this is a check or receipt, add one entry for each lineitem
+      my ($accno) = split /--/, $self->{account};
+      $query = qq|INSERT INTO status (trans_id, printed, spoolfile, formname, chart_id)
+                  VALUES (?, ?, ?, ?, (SELECT c.id FROM chart c WHERE c.accno = ?))|;
+      @values = ($printed, $queued{$self->{formname}}, $self->{prinform}, $accno);
+      $sth = prepare_query($self, $dbh, $query);
 
-    for $i (1 .. $self->{rowcount}) {
-      if ($self->{"checked_$i"}) {
-        do_statement($self, $sth, $query, $self->{"id_$i"}, @values);
+      for $i (1 .. $self->{rowcount}) {
+        if ($self->{"checked_$i"}) {
+          do_statement($self, $sth, $query, $self->{"id_$i"}, @values);
+        }
       }
-    }
-    $sth->finish();
-
-  } else {
-    $query = qq|INSERT INTO status (trans_id, printed, emailed, spoolfile, formname)
-                VALUES (?, ?, ?, ?, ?)|;
-    do_query($self, $dbh, $query, $self->{id}, $printed, $emailed,
-             $queued{$self->{formname}}, $self->{formname});
-  }
+      $sth->finish();
 
-  $dbh->commit;
-  $dbh->disconnect;
+    } else {
+      $query = qq|INSERT INTO status (trans_id, printed, emailed, spoolfile, formname)
+                  VALUES (?, ?, ?, ?, ?)|;
+      do_query($self, $dbh, $query, $self->{id}, $printed, $emailed,
+               $queued{$self->{formname}}, $self->{formname});
+    }
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
@@ -3153,6 +2933,7 @@ sub save_status {
 
 #--- 4 locale ---#
 # $main::locale->text('SAVED')
+# $main::locale->text('SCREENED')
 # $main::locale->text('DELETED')
 # $main::locale->text('ADDED')
 # $main::locale->text('PAYMENT POSTED')
@@ -3161,11 +2942,17 @@ sub save_status {
 # $main::locale->text('ELSE')
 # $main::locale->text('SAVED FOR DUNNING')
 # $main::locale->text('DUNNING STARTED')
+# $main::locale->text('PREVIEWED')
 # $main::locale->text('PRINTED')
 # $main::locale->text('MAILED')
 # $main::locale->text('SCREENED')
 # $main::locale->text('CANCELED')
+# $main::locale->text('IMPORT')
+# $main::locale->text('UNDO TRANSFER')
+# $main::locale->text('UNIMPORT')
 # $main::locale->text('invoice')
+# $main::locale->text('invoice_for_advance_payment')
+# $main::locale->text('final_invoice')
 # $main::locale->text('proforma')
 # $main::locale->text('sales_order')
 # $main::locale->text('pick_list')
@@ -3178,20 +2965,21 @@ sub save_history {
   $main::lxdebug->enter_sub();
 
   my $self = shift;
-  my $dbh  = shift || $self->get_standard_dbh;
+  my $dbh  = shift || SL::DB->client->dbh;
+  SL::DB->client->with_transaction(sub {
 
-  if(!exists $self->{employee_id}) {
-    &get_employee($self, $dbh);
-  }
-
-  my $query =
-   qq|INSERT INTO history_erp (trans_id, employee_id, addition, what_done, snumbers) | .
-   qq|VALUES (?, (SELECT id FROM employee WHERE login = ?), ?, ?, ?)|;
-  my @values = (conv_i($self->{id}), $self->{login},
-                $self->{addition}, $self->{what_done}, "$self->{snumbers}");
-  do_query($self, $dbh, $query, @values);
+    if(!exists $self->{employee_id}) {
+      &get_employee($self, $dbh);
+    }
 
-  $dbh->commit;
+    my $query =
+     qq|INSERT INTO history_erp (trans_id, employee_id, addition, what_done, snumbers) | .
+     qq|VALUES (?, (SELECT id FROM employee WHERE login = ?), ?, ?, ?)|;
+    my @values = (conv_i($self->{id}), $self->{login},
+                  $self->{addition}, $self->{what_done}, "$self->{snumbers}");
+    do_query($self, $dbh, $query, @values);
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
@@ -3209,7 +2997,7 @@ sub get_history {
       qq|SELECT h.employee_id, h.itime::timestamp(0) AS itime, h.addition, h.what_done, emp.name, h.snumbers, h.trans_id AS id | .
       qq|FROM history_erp h | .
       qq|LEFT JOIN employee emp ON (emp.id = h.employee_id) | .
-      qq|WHERE (trans_id = | . $trans_id . qq|) $restriction | .
+      qq|WHERE (trans_id = | . $dbh->quote($trans_id) . qq|) $restriction | .
       $order;
 
     my $sth = $dbh->prepare($query) || $self->dberror($query);
@@ -3219,7 +3007,10 @@ sub get_history {
     while(my $hash_ref = $sth->fetchrow_hashref()) {
       $hash_ref->{addition} = $main::locale->text($hash_ref->{addition});
       $hash_ref->{what_done} = $main::locale->text($hash_ref->{what_done});
-      $hash_ref->{snumbers} =~ s/^.+_(.*)$/$1/g;
+      my ( $what, $number ) = split /_/, $hash_ref->{snumbers};
+      $hash_ref->{snumbers} = $number;
+      $hash_ref->{haslink}  = 'controller.pl?action=EmailJournal/show&id='.$number if $what eq 'emailjournal';
+      $hash_ref->{snumbers} = $main::locale->text("E-Mail").' '.$number if $what eq 'emailjournal';
       $tempArray[$i++] = $hash_ref;
     }
     $main::lxdebug->leave_sub() and return \@tempArray
@@ -3243,16 +3034,13 @@ sub get_partsgroup {
   my @values;
 
   if ($p->{searchitems} eq 'part') {
-    $query .= qq|WHERE p.inventory_accno_id > 0|;
+    $query .= qq|WHERE p.part_type = 'part'|;
   }
   if ($p->{searchitems} eq 'service') {
-    $query .= qq|WHERE p.inventory_accno_id IS NULL|;
+    $query .= qq|WHERE p.part_type = 'service'|;
   }
   if ($p->{searchitems} eq 'assembly') {
-    $query .= qq|WHERE p.assembly = '1'|;
-  }
-  if ($p->{searchitems} eq 'labor') {
-    $query .= qq|WHERE (p.inventory_accno_id > 0) AND (p.income_accno_id IS NULL)|;
+    $query .= qq|WHERE p.part_type = 'assembly'|;
   }
 
   $query .= qq|ORDER BY partsgroup|;
@@ -3382,12 +3170,6 @@ sub prepare_for_printing {
     $self->{"employee_${_}"} = $defaults->$_   for qw(address businessnumber co_ustid company duns sepa_creditor_id taxnumber);
   }
 
-  # Load shipping address from database if shipto_id is set.
-  if ($self->{shipto_id}) {
-    my $shipto  = SL::DB::Shipto->new(shipto_id => $self->{shipto_id})->load;
-    $self->{$_} = $shipto->$_ for grep { m{^shipto} } map { $_->name } @{ $shipto->meta->columns };
-  }
-
   my $language = $self->{language} ? '_' . $self->{language} : '';
 
   my ($language_tc, $output_numberformat, $output_dateformat, $output_longdates);
@@ -3414,6 +3196,8 @@ sub prepare_for_printing {
     IS->invoice_details(\%::myconfig, $self, $::locale);
   }
 
+  $self->set_addition_billing_address_print_variables;
+
   # Chose extension & set source file name
   my $extension = 'html';
   if ($self->{format} eq 'postscript') {
@@ -3436,7 +3220,7 @@ sub prepare_for_printing {
 
   # Format dates.
   $self->format_dates($output_dateformat, $output_longdates,
-                      qw(invdate orddate quodate pldate duedate reqdate transdate shippingdate deliverydate validitydate paymentdate datepaid
+                      qw(invdate orddate quodate pldate duedate reqdate transdate tax_point shippingdate deliverydate validitydate paymentdate datepaid
                          transdate_oe deliverydate_oe employee_startdate employee_enddate),
                       grep({ /^(?:datepaid|transdate_oe|reqdate|deliverydate|deliverydate_oe|transdate)_\d+$/ } keys(%{$self})));
 
@@ -3456,6 +3240,14 @@ sub prepare_for_printing {
     $self->reformat_numbers($output_numberformat, $precision, @{ $field_list });
   }
 
+  # Translate units
+  if (($self->{language} // '') ne '') {
+    my $template_arrays = $self->{TEMPLATE_ARRAYS} || $self;
+    for my $idx (0..scalar(@{ $template_arrays->{unit} }) - 1) {
+      $template_arrays->{unit}->[$idx] = AM->translate_units($self, $self->{language}, $template_arrays->{unit}->[$idx], $template_arrays->{qty}->[$idx])
+    }
+  }
+
   $self->{template_meta} = {
     formname  => $self->{formname},
     language  => SL::DB::Manager::Language->find_by_or_create(id => $self->{language_id} || undef),
@@ -3466,6 +3258,43 @@ sub prepare_for_printing {
     today     => DateTime->today,
   };
 
+  if ($defaults->print_interpolate_variables_in_positions) {
+    $self->substitute_placeholders_in_template_arrays({ field => 'description', type => 'text' }, { field => 'longdescription', type => 'html' });
+  }
+
+  return $self;
+}
+
+sub set_addition_billing_address_print_variables {
+  my ($self) = @_;
+
+  return if !$self->{billing_address_id};
+
+  my $address = SL::DB::Manager::AdditionalBillingAddress->find_by(id => $self->{billing_address_id});
+  return if !$address;
+
+  $self->{"billing_address_${_}"} = $address->$_ for map { $_->name } @{ $address->meta->columns };
+}
+
+sub substitute_placeholders_in_template_arrays {
+  my ($self, @fields) = @_;
+
+  foreach my $spec (@fields) {
+    $spec     = { field => $spec, type => 'text' } if !ref($spec);
+    my $field = $spec->{field};
+
+    next unless exists $self->{TEMPLATE_ARRAYS} && exists $self->{TEMPLATE_ARRAYS}->{$field};
+
+    my $tag_start = $spec->{type} eq 'html' ? '&lt;%' : '<%';
+    my $tag_end   = $spec->{type} eq 'html' ? '%&gt;' : '%>';
+    my $formatter = $spec->{type} eq 'html' ? sub { $::locale->quote_special_chars('html', $_[0] // '') } : sub { $_[0] };
+
+    $self->{TEMPLATE_ARRAYS}->{$field} = [
+      apply { s{${tag_start}(.+?)${tag_end}}{ $formatter->($self->{$1}) }eg }
+        @{ $self->{TEMPLATE_ARRAYS}->{$field} }
+    ];
+  }
+
   return $self;
 }
 
@@ -3620,41 +3449,10 @@ sub reformat_numbers {
 }
 
 sub create_email_signature {
-
   my $client_signature = $::instance_conf->get_signature;
   my $user_signature   = $::myconfig{signature};
 
-  my $signature = '';
-  if ( $client_signature or $user_signature ) {
-    $signature  = "\n\n-- \n";
-    $signature .= $user_signature   . "\n" if $user_signature;
-    $signature .= $client_signature . "\n" if $client_signature;
-  };
-  return $signature;
-
-};
-
-sub layout {
-  my ($self) = @_;
-  $::lxdebug->enter_sub;
-
-  my %style_to_script_map = (
-    v3  => 'v3',
-    neu => 'new',
-  );
-
-  my $menu_script = $style_to_script_map{$::myconfig{menustyle}} || '';
-
-  package main;
-  require "bin/mozilla/menu$menu_script.pl";
-  package Form;
-  require SL::Controller::FrameHeader;
-
-
-  my $layout = SL::Controller::FrameHeader->new->action_header . ::render();
-
-  $::lxdebug->leave_sub;
-  return $layout;
+  return join '', grep { $_ } ($user_signature, $client_signature);
 }
 
 sub calculate_tax {
@@ -3671,11 +3469,12 @@ sub calculate_tax {
 
   my ($self,$amount,$taxrate,$taxincluded,$roundplaces) = @_;
 
-  $roundplaces = 2 unless defined $roundplaces;
+  $roundplaces //= 2;
+  $taxincluded //= 0;
 
   my $tax;
 
-  if ($taxincluded *= 1) {
+  if ($taxincluded) {
     # calculate tax (unrounded), subtract from amount, round amount and round tax
     $tax       = $amount - ($amount / ($taxrate + 1)); # equivalent to: taxrate * amount / (taxrate + 1)
     $amount    = $self->round_amount($amount - $tax, $roundplaces);
index 01417aa..46c2d49 100644 (file)
--- a/SL/GL.pm
+++ b/SL/GL.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # General ledger backend code
 
 package GL;
 
+use List::Util qw(first);
+
 use Data::Dumper;
 use SL::DATEV qw(:CONSTANTS);
 use SL::DBUtils;
+use SL::DB::Chart;
+use SL::DB::Draft;
+use SL::Util qw(trim);
+use SL::DB;
 
 use strict;
 
@@ -48,22 +55,25 @@ sub delete_transaction {
   my ($self, $myconfig, $form) = @_;
   $main::lxdebug->enter_sub();
 
-  # connect to database
-  my $dbh = $form->dbconnect_noauto($myconfig);
-
-  # acc_trans entries are deleted by database triggers.
-  do_query($form, $dbh, qq|DELETE FROM gl WHERE id = ?|, conv_i($form->{id}));
+  SL::DB->client->with_transaction(sub {
+    do_query($form, SL::DB->client->dbh, qq|DELETE FROM gl WHERE id = ?|, conv_i($form->{id}));
+    1;
+  }) or do { die SL::DB->client->error };
 
-  # commit and redirect
-  my $rc = $dbh->commit;
-  $dbh->disconnect;
   $main::lxdebug->leave_sub();
+}
+
+sub post_transaction {
+  my ($self, $myconfig, $form) = @_;
+  $main::lxdebug->enter_sub();
 
-  $rc;
+  my $rc = SL::DB->client->with_transaction(\&_post_transaction, $self, $myconfig, $form);
 
+  $::lxdebug->leave_sub;
+  return $rc;
 }
 
-sub post_transaction {
+sub _post_transaction {
   my ($self, $myconfig, $form) = @_;
   $main::lxdebug->enter_sub();
 
@@ -72,8 +82,7 @@ sub post_transaction {
 
   my $i;
 
-  # connect to database, turn off AutoCommit
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   # post the transaction
   # make up a unique handle and store in reference field
@@ -108,28 +117,26 @@ sub post_transaction {
     do_query($form, $dbh, $query, @values);
   }
 
-  my ($null, $department_id) = split(/--/, $form->{department});
-
   $form->{ob_transaction} *= 1;
   $form->{cb_transaction} *= 1;
 
   $query =
     qq|UPDATE gl SET
          reference = ?, description = ?, notes = ?,
-         transdate = ?, department_id = ?, taxincluded = ?,
-         storno = ?, storno_id = ?, ob_transaction = ?, cb_transaction = ?
+         transdate = ?, deliverydate = ?, tax_point = ?, department_id = ?, taxincluded = ?,
+         storno = ?, storno_id = ?, ob_transaction = ?, cb_transaction = ?,
+         transaction_description = ?
        WHERE id = ?|;
 
   @values = ($form->{reference}, $form->{description}, $form->{notes},
-             conv_date($form->{transdate}), conv_i($department_id), $form->{taxincluded} ? 't' : 'f',
+             conv_date($form->{transdate}), conv_date($form->{deliverydate}), conv_date($form->{tax_point}), conv_i($form->{department_id}), $form->{taxincluded} ? 't' : 'f',
              $form->{storno} ? 't' : 'f', conv_i($form->{storno_id}), $form->{ob_transaction} ? 't' : 'f', $form->{cb_transaction} ? 't' : 'f',
+             $form->{transaction_description},
              conv_i($form->{id}));
   do_query($form, $dbh, $query, @values);
 
   # insert acc_trans transactions
   for $i (1 .. $form->{rowcount}) {
-    # extract accno
-    my ($accno) = split(/--/, $form->{"accno_$i"});
     ($form->{"tax_id_$i"}) = split(/--/, $form->{"taxchart_$i"});
     if ($form->{"tax_id_$i"} ne "") {
       $query = qq|SELECT taxkey, rate FROM tax WHERE id = ?|;
@@ -158,10 +165,9 @@ sub post_transaction {
       $query =
         qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate,
                                   source, memo, project_id, taxkey, ob_transaction, cb_transaction, tax_id, chart_link)
-           VALUES (?, (SELECT id FROM chart WHERE accno = ?),
-                   ?, ?, ?, ?, ?, ?, ?, ?, ?, (SELECT link FROM chart WHERE accno = ?))|;
-      @values = (conv_i($form->{id}), $accno, $amount, conv_date($form->{transdate}),
-                 $form->{"source_$i"}, $form->{"memo_$i"}, $project_id, $taxkey, $form->{ob_transaction} ? 't' : 'f', $form->{cb_transaction} ? 't' : 'f', conv_i($form->{"tax_id_$i"}), $accno);
+           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, (SELECT link FROM chart WHERE id = ?))|;
+      @values = (conv_i($form->{id}), $form->{"accno_id_$i"}, $amount, conv_date($form->{transdate}),
+                 $form->{"source_$i"}, $form->{"memo_$i"}, $project_id, $taxkey, $form->{ob_transaction} ? 't' : 'f', $form->{cb_transaction} ? 't' : 'f', conv_i($form->{"tax_id_$i"}), $form->{"accno_id_$i"});
       do_query($form, $dbh, $query, @values);
     }
 
@@ -187,40 +193,34 @@ sub post_transaction {
     do_query($form, $dbh, qq|UPDATE gl SET storno = 't' WHERE id = ?|, conv_i($form->{storno_id}));
   }
 
+  if ($form->{draft_id}) {
+    SL::DB::Manager::Draft->delete_all(where => [ id => delete($form->{draft_id}) ]);
+  }
+
   # safety check datev export
   if ($::instance_conf->get_datev_check_on_gl_transaction) {
-    my $transdate = $::form->{transdate} ? DateTime->from_lxoffice($::form->{transdate}) : undef;
-    $transdate  ||= DateTime->today;
 
+    # create datev object
     my $datev = SL::DATEV->new(
-      exporttype => DATEV_ET_BUCHUNGEN,
-      format     => DATEV_FORMAT_KNE,
       dbh        => $dbh,
       trans_id   => $form->{id},
     );
 
-    $datev->export;
+    $datev->generate_datev_data;
 
     if ($datev->errors) {
-      $dbh->rollback;
       die join "\n", $::locale->text('DATEV check returned errors:'), $datev->errors;
     }
   }
 
-  # commit and redirect
-  my $rc = $dbh->commit;
-  $dbh->disconnect;
-  $main::lxdebug->leave_sub();
-
-  return $rc;
+  return 1;
 }
 
 sub all_transactions {
   my ($self, $myconfig, $form) = @_;
   $main::lxdebug->enter_sub();
 
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
   my ($query, $sth, $source, $null, $space);
 
   my ($glwhere, $arwhere, $apwhere) = ("1 = 1", "1 = 1", "1 = 1");
@@ -230,58 +230,57 @@ sub all_transactions {
     $glwhere .= qq| AND g.reference ILIKE ?|;
     $arwhere .= qq| AND a.invnumber ILIKE ?|;
     $apwhere .= qq| AND a.invnumber ILIKE ?|;
-    push(@glvalues, '%' . $form->{reference} . '%');
-    push(@arvalues, '%' . $form->{reference} . '%');
-    push(@apvalues, '%' . $form->{reference} . '%');
+    push(@glvalues, like($form->{reference}));
+    push(@arvalues, like($form->{reference}));
+    push(@apvalues, like($form->{reference}));
   }
 
-  if ($form->{department}) {
-    my ($null, $department) = split /--/, $form->{department};
+  if ($form->{department_id}) {
     $glwhere .= qq| AND g.department_id = ?|;
     $arwhere .= qq| AND a.department_id = ?|;
     $apwhere .= qq| AND a.department_id = ?|;
-    push(@glvalues, $department);
-    push(@arvalues, $department);
-    push(@apvalues, $department);
+    push(@glvalues, $form->{department_id});
+    push(@arvalues, $form->{department_id});
+    push(@apvalues, $form->{department_id});
   }
 
   if ($form->{source}) {
     $glwhere .= " AND ac.trans_id IN (SELECT trans_id from acc_trans WHERE source ILIKE ?)";
     $arwhere .= " AND ac.trans_id IN (SELECT trans_id from acc_trans WHERE source ILIKE ?)";
     $apwhere .= " AND ac.trans_id IN (SELECT trans_id from acc_trans WHERE source ILIKE ?)";
-    push(@glvalues, '%' . $form->{source} . '%');
-    push(@arvalues, '%' . $form->{source} . '%');
-    push(@apvalues, '%' . $form->{source} . '%');
+    push(@glvalues, like($form->{source}));
+    push(@arvalues, like($form->{source}));
+    push(@apvalues, like($form->{source}));
   }
 
   # default Datumseinschränkung falls nicht oder falsch übergeben (sollte nie passieren)
   $form->{datesort} = 'transdate' unless $form->{datesort} =~ /^(transdate|gldate)$/;
 
-  if ($form->{datefrom}) {
+  if (trim($form->{datefrom})) {
     $glwhere .= " AND ac.$form->{datesort} >= ?";
     $arwhere .= " AND ac.$form->{datesort} >= ?";
     $apwhere .= " AND ac.$form->{datesort} >= ?";
-    push(@glvalues, $form->{datefrom});
-    push(@arvalues, $form->{datefrom});
-    push(@apvalues, $form->{datefrom});
+    push(@glvalues, trim($form->{datefrom}));
+    push(@arvalues, trim($form->{datefrom}));
+    push(@apvalues, trim($form->{datefrom}));
   }
 
-  if ($form->{dateto}) {
+  if (trim($form->{dateto})) {
     $glwhere .= " AND ac.$form->{datesort} <= ?";
     $arwhere .= " AND ac.$form->{datesort} <= ?";
     $apwhere .= " AND ac.$form->{datesort} <= ?";
-    push(@glvalues, $form->{dateto});
-    push(@arvalues, $form->{dateto});
-    push(@apvalues, $form->{dateto});
+    push(@glvalues, trim($form->{dateto}));
+    push(@arvalues, trim($form->{dateto}));
+    push(@apvalues, trim($form->{dateto}));
   }
 
-  if ($form->{description}) {
+  if (trim($form->{description})) {
     $glwhere .= " AND g.description ILIKE ?";
     $arwhere .= " AND ct.name ILIKE ?";
     $apwhere .= " AND ct.name ILIKE ?";
-    push(@glvalues, '%' . $form->{description} . '%');
-    push(@arvalues, '%' . $form->{description} . '%');
-    push(@apvalues, '%' . $form->{description} . '%');
+    push(@glvalues, like($form->{description}));
+    push(@arvalues, like($form->{description}));
+    push(@apvalues, like($form->{description}));
   }
 
   if ($form->{employee_id}) {
@@ -293,13 +292,22 @@ sub all_transactions {
     push(@apvalues, conv_i($form->{employee_id}));
   }
 
-  if ($form->{notes}) {
+  if (trim($form->{notes})) {
     $glwhere .= " AND g.notes ILIKE ?";
     $arwhere .= " AND a.notes ILIKE ?";
     $apwhere .= " AND a.notes ILIKE ?";
-    push(@glvalues, '%' . $form->{notes} . '%');
-    push(@arvalues, '%' . $form->{notes} . '%');
-    push(@apvalues, '%' . $form->{notes} . '%');
+    push(@glvalues, like($form->{notes}));
+    push(@arvalues, like($form->{notes}));
+    push(@apvalues, like($form->{notes}));
+  }
+
+  if (trim($form->{transaction_description})) {
+    $glwhere .= " AND g.transaction_description ILIKE ?";
+    $arwhere .= " AND a.transaction_description ILIKE ?";
+    $apwhere .= " AND a.transaction_description ILIKE ?";
+    push(@glvalues, like($form->{transaction_description}));
+    push(@arvalues, like($form->{transaction_description}));
+    push(@apvalues, like($form->{transaction_description}));
   }
 
   if ($form->{accno}) {
@@ -331,10 +339,15 @@ sub all_transactions {
     push(@apvalues, $project_id, $project_id);
   }
 
-  my ($project_columns, $project_join);
+  my ($project_columns,            $project_join);
+  my ($arap_globalproject_columns, $arap_globalproject_join);
+  my ($gl_globalproject_columns);
   if ($form->{"l_projectnumbers"}) {
-    $project_columns = qq|, ac.project_id, pr.projectnumber|;
-    $project_join = qq|LEFT JOIN project pr ON (ac.project_id = pr.id)|;
+    $project_columns            = qq|, ac.project_id, pr.projectnumber|;
+    $project_join               = qq|LEFT JOIN project pr ON (ac.project_id = pr.id)|;
+    $arap_globalproject_columns = qq|, a.globalproject_id, globalpr.projectnumber AS globalprojectnumber|;
+    $arap_globalproject_join    = qq|LEFT JOIN project globalpr ON (a.globalproject_id = globalpr.id)|;
+    $gl_globalproject_columns   = qq|, NULL AS globalproject_id, '' AS globalprojectnumber|;
   }
 
   if ($form->{accno}) {
@@ -353,17 +366,20 @@ sub all_transactions {
   }
 
   my %sort_columns =  (
-    'id'           => [ qw(id)                   ],
-    'transdate'    => [ qw(transdate id)         ],
-    'gldate'       => [ qw(gldate id)         ],
-    'reference'    => [ qw(lower_reference id)   ],
-    'description'  => [ qw(lower_description id) ],
-    'accno'        => [ qw(accno transdate id)   ],
+    'id'                      => [ qw(id)                   ],
+    'transdate'               => [ qw(transdate id)         ],
+    'gldate'                  => [ qw(gldate id)         ],
+    'reference'               => [ qw(lower_reference id)   ],
+    'description'             => [ qw(lower_description id) ],
+    'accno'                   => [ qw(accno transdate id)   ],
+    'department'              => [ qw(department transdate id)   ],
+    'transaction_description' => [ qw(lower_transaction_description id) ],
     );
   my %lowered_columns =  (
-    'reference'       => { 'gl' => 'g.reference',   'arap' => 'a.invnumber', },
-    'source'          => { 'gl' => 'ac.source',     'arap' => 'ac.source',   },
-    'description'     => { 'gl' => 'g.description', 'arap' => 'ct.name',     },
+    'reference'               => { 'gl' => 'g.reference',               'arap' => 'a.invnumber',               },
+    'source'                  => { 'gl' => 'ac.source',                 'arap' => 'ac.source',                 },
+    'description'             => { 'gl' => 'g.description',             'arap' => 'ct.name',                   },
+    'transaction_description' => { 'gl' => 'g.transaction_description', 'arap' => 'a.transaction_description', },
     );
 
   # sortdir = sort direction (ascending or descending)
@@ -384,11 +400,13 @@ sub all_transactions {
         ac.acc_trans_id, g.id, 'gl' AS type, FALSE AS invoice, g.reference, ac.taxkey, c.link,
         g.description, ac.transdate, ac.gldate, ac.source, ac.trans_id,
         ac.amount, c.accno, g.notes, t.chart_id,
+        d.description AS department, g.transaction_description,
         CASE WHEN (COALESCE(e.name, '') = '') THEN e.login ELSE e.name END AS employee
-        $project_columns
+        $project_columns $gl_globalproject_columns
         $columns_for_sorting{gl}
       FROM gl g
-      LEFT JOIN employee e ON (g.employee_id = e.id),
+      LEFT JOIN employee e ON (g.employee_id = e.id)
+      LEFT JOIN department d ON (g.department_id = d.id),
       acc_trans ac $project_join, chart c
       LEFT JOIN tax t ON (t.chart_id = c.id)
       WHERE $glwhere
@@ -400,11 +418,14 @@ sub all_transactions {
       SELECT ac.acc_trans_id, a.id, 'ar' AS type, a.invoice, a.invnumber, ac.taxkey, c.link,
         ct.name, ac.transdate, ac.gldate, ac.source, ac.trans_id,
         ac.amount, c.accno, a.notes, t.chart_id,
+        d.description AS department, a.transaction_description,
         CASE WHEN (COALESCE(e.name, '') = '') THEN e.login ELSE e.name END AS employee
-        $project_columns
+        $project_columns $arap_globalproject_columns
         $columns_for_sorting{arap}
       FROM ar a
-      LEFT JOIN employee e ON (a.employee_id = e.id),
+      LEFT JOIN employee e ON (a.employee_id = e.id)
+      LEFT JOIN department d ON (a.department_id = d.id)
+      $arap_globalproject_join,
       acc_trans ac $project_join, customer ct, chart c
       LEFT JOIN tax t ON (t.chart_id=c.id)
       WHERE $arwhere
@@ -417,11 +438,14 @@ sub all_transactions {
       SELECT ac.acc_trans_id, a.id, 'ap' AS type, a.invoice, a.invnumber, ac.taxkey, c.link,
         ct.name, ac.transdate, ac.gldate, ac.source, ac.trans_id,
         ac.amount, c.accno, a.notes, t.chart_id,
+        d.description AS department, a.transaction_description,
         CASE WHEN (COALESCE(e.name, '') = '') THEN e.login ELSE e.name END AS employee
-        $project_columns
+        $project_columns $arap_globalproject_columns
         $columns_for_sorting{arap}
       FROM ap a
-      LEFT JOIN employee e ON (a.employee_id = e.id),
+      LEFT JOIN employee e ON (a.employee_id = e.id)
+      LEFT JOIN department d ON (a.department_id = d.id)
+      $arap_globalproject_join,
       acc_trans ac $project_join, vendor ct, chart c
       LEFT JOIN tax t ON (t.chart_id=c.id)
       WHERE $apwhere
@@ -487,7 +511,8 @@ sub all_transactions {
       }
 
       $ref->{"projectnumbers"} = {};
-      $ref->{"projectnumbers"}->{$ref->{"projectnumber"}} = 1 if ($ref->{"projectnumber"});
+      $ref->{"projectnumbers"}->{$ref->{"projectnumber"}}       = 1 if ($ref->{"projectnumber"});
+      $ref->{"projectnumbers"}->{$ref->{"globalprojectnumber"}} = 1 if ($ref->{"globalprojectnumber"});
 
       $balance = $ref->{amount};
 
@@ -543,7 +568,8 @@ sub all_transactions {
       $balance =
         (int($balance * 100000) + int(100000 * $ref2->{amount})) / 100000;
 
-      $ref->{"projectnumbers"}->{$ref2->{"projectnumber"}} = 1 if ($ref2->{"projectnumber"});
+      $ref->{"projectnumbers"}->{$ref2->{"projectnumber"}}       = 1 if ($ref2->{"projectnumber"});
+      $ref->{"projectnumbers"}->{$ref2->{"globalprojectnumber"}} = 1 if ($ref2->{"globalprojectnumber"});
 
       if ($ref2->{chart_id} > 0) { # all tax accounts, following lines
         if ($ref2->{amount} < 0) {
@@ -619,8 +645,6 @@ sub all_transactions {
     ($form->{account_description}) = selectrow_query($form, $dbh, $query, $form->{accno});
   }
 
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
@@ -630,8 +654,7 @@ sub transaction {
 
   my ($query, $sth, $ref, @values);
 
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   $query = qq|SELECT closedto, revtrans FROM defaults|;
   ($form->{closedto}, $form->{revtrans}) = selectrow_query($form, $dbh, $query);
@@ -643,9 +666,12 @@ sub transaction {
 
   if ($form->{id}) {
     $query =
-      qq|SELECT g.reference, g.description, g.notes, g.transdate, g.storno, g.storno_id,
-           d.description AS department, e.name AS employee, g.taxincluded, g.gldate,
-         g.ob_transaction, g.cb_transaction
+      qq|SELECT g.reference, g.description, g.notes, g.transdate, g.deliverydate, g.tax_point,
+           g.storno, g.storno_id,
+           g.department_id, d.description AS department,
+           e.name AS employee, g.taxincluded, g.gldate,
+           g.ob_transaction, g.cb_transaction,
+           g.transaction_description
          FROM gl g
          LEFT JOIN department d ON (d.id = g.department_id)
          LEFT JOIN employee e ON (e.id = g.employee_id)
@@ -657,7 +683,7 @@ sub transaction {
     $query =
       qq|SELECT c.accno, t.taxkey AS accnotaxkey, a.amount, a.memo, a.source,
            a.transdate, a.cleared, a.project_id, p.projectnumber,
-           a.taxkey, t.rate AS taxrate, t.id,
+           a.taxkey, t.rate AS taxrate, t.id, a.chart_id,
            (SELECT c1.accno
             FROM chart c1, tax t1
             WHERE (t1.id = t.id) AND (c1.id = t.chart_id)) AS taxaccno,
@@ -703,18 +729,24 @@ sub transaction {
        ORDER BY c.accno|;
   $form->{chart} = selectall_hashref_query($form, $dbh, $query, conv_date($form->{transdate}));
 
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
 sub storno {
+  my ($self, $form, $myconfig, $id) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_storno, $self, $form, $myconfig, $id);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _storno {
   my ($self, $form, $myconfig, $id) = @_;
 
   my ($query, $new_id, $storno_row, $acc_trans_rows);
-  my $dbh = $form->get_standard_dbh($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   $query = qq|SELECT nextval('glid')|;
   ($new_id) = selectrow_query($form, $dbh, $query);
@@ -751,66 +783,96 @@ sub storno {
     do_query($form, $dbh, $query, (values %$row));
   }
 
-  $dbh->commit;
-
-  $main::lxdebug->leave_sub();
+  return 1;
 }
 
 sub get_chart_balances {
-  $main::lxdebug->enter_sub();
-
-  my $self     = shift;
-  my %params   = @_;
+  my ($self, @chart_ids) = @_;
 
-  Common::check_params(\%params, qw(charts));
+  return () unless @chart_ids;
 
-  my $myconfig = \%main::myconfig;
-  my $form     = $main::form;
+  my $placeholders = join ', ', ('?') x scalar(@chart_ids);
+  my $query = qq|SELECT chart_id, SUM(amount) AS sum
+                 FROM acc_trans
+                 WHERE chart_id IN (${placeholders})
+                 GROUP BY chart_id|;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
+  my %balances = selectall_as_map($::form, $::form->get_standard_dbh(\%::myconfig), $query, 'chart_id', 'sum', @chart_ids);
 
-  my @ids      = map { $_->{id} } @{ $params{charts} };
+  return %balances;
+}
 
-  if (!@ids) {
-    $main::lxdebug->leave_sub();
-    return;
-  }
+sub get_active_taxes_for_chart {
+  my ($self, $chart_id, $transdate, $tax_id) = @_;
 
-  my $query = qq|SELECT chart_id, SUM(amount) AS sum
-                 FROM acc_trans
-                 WHERE chart_id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)
-                 GROUP BY chart_id|;
+  my $chart         = SL::DB::Chart->new(id => $chart_id)->load;
+  my $active_taxkey = $chart->get_active_taxkey($transdate);
 
-  my %balances = selectall_as_map($form, $dbh, $query, 'chart_id', 'sum', @ids);
+  my $where = [ chart_categories => { like => '%' . $chart->category . '%' } ];
 
-  foreach my $chart (@{ $params{charts} }) {
-    $chart->{balance} = $balances{ $chart->{id} } || 0;
+  if ( defined $tax_id && $tax_id >= 0 ) {
+    $where = [ or => [ chart_categories => { like => '%' . $chart->category . '%' },
+                       id               => $tax_id
+                     ]
+             ];
   }
 
-  $main::lxdebug->leave_sub();
+  my $taxes         = SL::DB::Manager::Tax->get_all(
+    where   => $where,
+    sort_by => 'taxkey, rate',
+  );
+
+  my $default_tax            = first { $active_taxkey->tax_id == $_->id } @{ $taxes };
+  $default_tax->{is_default} = 1 if $default_tax;
+
+  return @{ $taxes };
 }
 
-sub get_tax_dropdown {
-  my ($self, $accno) = @_;
+1;
 
-  my $myconfig = \%main::myconfig;
-  my $form = $main::form;
+__END__
 
-  my $dbh = $form->get_standard_dbh($myconfig);
+=pod
 
-  my $query = qq|SELECT category FROM chart WHERE accno = ?|;
-  my ($category) = selectrow_query($form, $dbh, $query, $accno);
+=encoding utf8
 
-  $query = qq|SELECT * FROM tax WHERE chart_categories like '%$category%' order by taxkey, rate|;
+=head1 NAME
 
-  my $sth = prepare_execute_query($form, $dbh, $query);
+SL::GL - some useful GL functions
 
-  my @tax_accounts = ();
-  while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
-    push(@tax_accounts, $ref);
-  }
+=head1 FUNCTIONS
 
-  return @tax_accounts;
-}
+=over 4
 
-1;
+=item C<get_active_taxes_for_chart> $transdate $tax_id
+
+Returns a list of valid taxes for a certain chart.
+
+If the optional param transdate exists one entry in the returning list
+may get the attribute C<is_default> for this specific tax-dependent date.
+The possible entries are filtered by the charttype of the tax, i.e. only taxes
+whose chart_categories match the category of the chart will be shown.
+
+In the case of existing records, e.g. when opening an old ar record, due to
+changes in the configurations the desired tax might not be available in the
+dropdown anymore. If we are loading an old record and know its tax_id (from
+acc_trans), we can pass $tax_id as the third parameter and be sure that the
+original tax always appears in the dropdown.
+
+The functions returns an array which may be used for building dropdowns in ar/ap/gl code.
+
+=back
+
+=head1 TODO
+
+Nothing here yet.
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+G. Richardson E<lt>grichardson@kivitec.deE<gt>
+
+=cut
index a509212..263c827 100644 (file)
@@ -1,6 +1,7 @@
 package GenericTranslations;
 
 use SL::DBUtils;
+use SL::DB;
 
 use strict;
 
@@ -90,8 +91,16 @@ sub list {
 }
 
 sub save {
+  my ($self, %params) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_save, $self, %params);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _save {
   my $self     = shift;
   my %params   = @_;
 
@@ -100,7 +109,7 @@ sub save {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
+  my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
   $params{translation} =~ s/^\s+//;
   $params{translation} =~ s/\s+$//;
@@ -139,9 +148,7 @@ sub save {
     do_query($form, $dbh, $q_insert, @v_insert);
   }
 
-  $dbh->commit() unless ($params{dbh});
-
-  $main::lxdebug->leave_sub();
+  return 1;
 }
 
 
diff --git a/SL/GoBD.pm b/SL/GoBD.pm
new file mode 100644 (file)
index 0000000..9c95a0a
--- /dev/null
@@ -0,0 +1,838 @@
+package SL::GoBD;
+
+# TODO:
+# optional: background jobable
+
+use strict;
+use utf8;
+
+use parent qw(Rose::Object);
+
+use Text::CSV_XS;
+use XML::Writer;
+use Archive::Zip;
+use File::Temp ();
+use File::Spec ();
+use List::MoreUtils qw(any);
+use List::UtilsBy qw(partition_by sort_by);
+
+use SL::DB::Helper::ALL; # since we work on meta data, we need everything
+use SL::DB::Helper::Mappings;
+use SL::Locale::String qw(t8);
+use SL::Version;
+
+use Rose::Object::MakeMethods::Generic (
+  scalar                  => [ qw(from to writer company location) ],
+  'scalar --get_set_init' => [ qw(files tempfiles export_ids tables csv_headers) ],
+);
+
+# in this we find:
+# key:         table name
+# name:        short name, translated
+# description: long description, translated
+# columns:     list of columns to export. export all columns if not present
+# primary_key: override primary key
+my %known_tables = (
+  chart    => { name => t8('Charts'),    description => t8('Chart of Accounts'),    primary_key => 'accno', columns => [ qw(id accno description) ],     },
+  customer => { name => t8('Customers'), description => t8('Customer Master Data'), columns => [ qw(id customernumber name department_1 department_2 street zipcode city country contact phone fax email notes taxnumber obsolete ustid) ] },
+  vendor   => { name => t8('Vendors'),   description => t8('Vendor Master Data'),   columns => [ qw(id vendornumber name department_1 department_2 street zipcode city country contact phone fax email notes taxnumber obsolete ustid) ] },
+);
+
+my %column_titles = (
+   chart => {
+     id             => t8('ID'),
+     accno          => t8('Account Number'),
+     description    => t8('Description'),
+   },
+   customer_vendor => {
+     id             => t8('ID (lit)'),
+     name           => t8('Name'),
+     department_1   => t8('Department 1'),
+     department_2   => t8('Department 2'),
+     street         => t8('Street'),
+     zipcode        => t8('Zipcode'),
+     city           => t8('City'),
+     country        => t8('Country'),
+     contact        => t8('Contact'),
+     phone          => t8('Phone'),
+     fax            => t8('Fax'),
+     email          => t8('E-mail'),
+     notes          => t8('Notes'),
+     customernumber => t8('Customer Number'),
+     vendornumber   => t8('Vendor Number'),
+     taxnumber      => t8('Tax Number'),
+     obsolete       => t8('Obsolete'),
+     ustid          => t8('Tax ID number'),
+   },
+);
+$column_titles{$_} = $column_titles{customer_vendor} for qw(customer vendor);
+
+my %datev_column_defs = (
+  trans_id          => { type => 'Rose::DB::Object::Metadata::Column::Integer', text => t8('ID'), },
+  amount            => { type => 'Rose::DB::Object::Metadata::Column::Numeric', text => t8('Amount'), },
+  credit_accname    => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Credit Account Name'), },
+  credit_accno      => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Credit Account'), },
+  credit_amount     => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Credit Amount'), },
+  credit_tax        => { type => 'Rose::DB::Object::Metadata::Column::Numeric', text => t8('Credit Tax (lit)'), },
+  debit_accname     => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Debit Account Name'), },
+  debit_accno       => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Debit Account'), },
+  debit_amount      => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Debit Amount'), },
+  debit_tax         => { type => 'Rose::DB::Object::Metadata::Column::Numeric', text => t8('Debit Tax (lit)'), },
+  invnumber         => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Reference'), },
+  name              => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Name'), },
+  notes             => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Notes'), },
+  tax               => { type => 'Rose::DB::Object::Metadata::Column::Numeric', text => t8('Tax'), },
+  taxdescription    => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('tax_taxdescription'), },
+  taxkey            => { type => 'Rose::DB::Object::Metadata::Column::Integer', text => t8('Taxkey'), },
+  tax_accname       => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Tax Account Name'), },
+  tax_accno         => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Tax Account'), },
+  transdate         => { type => 'Rose::DB::Object::Metadata::Column::Date',    text => t8('Transdate'), },
+  vcnumber          => { type => 'Rose::DB::Object::Metadata::Column::Text',    text => t8('Customer/Vendor Number'), },
+  customer_id       => { type => 'Rose::DB::Object::Metadata::Column::Integer', text => t8('Customer (database ID)'), },
+  vendor_id         => { type => 'Rose::DB::Object::Metadata::Column::Integer', text => t8('Vendor (database ID)'), },
+  itime             => { type => 'Rose::DB::Object::Metadata::Column::Date',    text => t8('Create Date'), },
+  gldate            => { type => 'Rose::DB::Object::Metadata::Column::Date',    text => t8('Gldate'), },
+);
+
+my @datev_columns = qw(
+  trans_id
+  customer_id vendor_id
+  name           vcnumber
+  transdate    invnumber      amount
+  debit_accno  debit_accname debit_amount debit_tax
+  credit_accno credit_accname credit_amount credit_tax
+  taxdescription tax
+  tax_accno    tax_accname    taxkey
+  notes itime gldate
+);
+
+# rows in this listing are tiers.
+# tables may depend on ids in a tier above them
+my @export_table_order = qw(
+  ar ap gl oe delivery_orders
+  invoice orderitems delivery_order_items
+  customer vendor
+  parts
+  acc_trans
+  chart
+);
+
+# needed because the standard dbh sets datestyle german and we don't want to mess with that
+my $date_format = 'DD.MM.YYYY';
+my $number_format = '1000.00';
+
+my $myconfig = { numberformat => $number_format };
+
+# callbacks that produce the xml spec for these column types
+my %column_types = (
+  'Rose::DB::Object::Metadata::Column::Integer'   => sub { $_[0]->tag('Numeric') },  # see Caveats for integer issues
+  'Rose::DB::Object::Metadata::Column::BigInt'    => sub { $_[0]->tag('Numeric') },  # see Caveats for integer issues
+  'Rose::DB::Object::Metadata::Column::Text'      => sub { $_[0]->tag('AlphaNumeric') },
+  'Rose::DB::Object::Metadata::Column::Varchar'   => sub { $_[0]->tag('AlphaNumeric') },
+  'Rose::DB::Object::Metadata::Column::Character' => sub { $_[0]->tag('AlphaNumeric') },
+  'Rose::DB::Object::Metadata::Column::Numeric'   => sub { $_[0]->tag('Numeric', sub { $_[0]->tag('Accuracy', 5) }) },
+  'Rose::DB::Object::Metadata::Column::Date'      => sub { $_[0]->tag('Date', sub { $_[0]->tag('Format', $date_format) }) },
+  'Rose::DB::Object::Metadata::Column::Timestamp' => sub { $_[0]->tag('Date', sub { $_[0]->tag('Format', $date_format) }) },
+  'Rose::DB::Object::Metadata::Column::Float'     => sub { $_[0]->tag('Numeric') },
+  'Rose::DB::Object::Metadata::Column::Boolean'   => sub { $_[0]
+    ->tag('AlphaNumeric')
+    ->tag('Map', sub { $_[0]
+      ->tag('From', 1)
+      ->tag('To', t8('true'))
+    })
+    ->tag('Map', sub { $_[0]
+      ->tag('From', 0)
+      ->tag('To', t8('false'))
+    })
+    ->tag('Map', sub { $_[0]
+      ->tag('From', '')
+      ->tag('To', t8('false'))
+    })
+  },
+);
+
+sub generate_export {
+  my ($self) = @_;
+
+  # verify data
+  $self->from && 'DateTime' eq ref $self->from or die 'need from date';
+  $self->to   && 'DateTime' eq ref $self->to   or die 'need to date';
+  $self->from <= $self->to                     or die 'from date must be earlier or equal than to date';
+  $self->tables && @{ $self->tables }          or die 'need tables';
+  for (@{ $self->tables }) {
+    next if $known_tables{$_};
+    die "unknown table '$_'";
+  }
+
+  # get data from those tables and save to csv
+  # for that we need to build queries that fetch all the columns
+  for ($self->sorted_tables) {
+    $self->do_csv_export($_);
+  }
+
+  $self->do_datev_csv_export;
+
+  # write xml file
+  $self->do_xml_file;
+
+  # add dtd
+  $self->files->{'gdpdu-01-08-2002.dtd'} = File::Spec->catfile('users', 'gdpdu-01-08-2002.dtd');
+
+  # make zip
+  my ($fh, $zipfile) = File::Temp::tempfile();
+  my $zip            = Archive::Zip->new;
+
+  while (my ($name, $file) = each %{ $self->files }) {
+    $zip->addFile($file, $name);
+  }
+
+  $zip->writeToFileHandle($fh) == Archive::Zip::AZ_OK() or die 'error writing zip file';
+  close($fh);
+
+  return $zipfile;
+}
+
+sub do_xml_file {
+  my ($self) = @_;
+
+  my ($fh, $filename) = File::Temp::tempfile();
+  binmode($fh, ':utf8');
+
+  $self->files->{'INDEX.XML'} = $filename;
+  push @{ $self->tempfiles }, $filename;
+
+  my $writer = XML::Writer->new(
+    OUTPUT      => $fh,
+    ENCODING    => 'UTF-8',
+  );
+
+  $self->writer($writer);
+  $self->writer->xmlDecl('UTF-8');
+  $self->writer->doctype('DataSet', undef, "gdpdu-01-08-2002.dtd");
+  $self->tag('DataSet', sub { $self
+    ->tag('Version', '1.0')
+    ->tag('DataSupplier', sub { $self
+      ->tag('Name', $self->client_name)
+      ->tag('Location', $self->client_location)
+      ->tag('Comment', $self->make_comment)
+    })
+    ->tag('Media', sub { $self
+      ->tag('Name', t8('DataSet #1', 1));
+      for (reverse $self->sorted_tables) { $self  # see CAVEATS for table order
+        ->table($_)
+      }
+      $self->do_datev_xml_table;
+    })
+  });
+  close($fh);
+}
+
+sub table {
+  my ($self, $table) = @_;
+  my $writer = $self->writer;
+
+  $self->tag('Table', sub { $self
+    ->tag('URL', "$table.csv")
+    ->tag('Name', $known_tables{$table}{name})
+    ->tag('Description', $known_tables{$table}{description})
+    ->tag('Validity', sub { $self
+      ->tag('Range', sub { $self
+        ->tag('From', $self->from->to_kivitendo(dateformat => 'dd.mm.yyyy'))
+        ->tag('To',   $self->to->to_kivitendo(dateformat => 'dd.mm.yyyy'))
+      })
+      ->tag('Format', $date_format)
+    })
+    ->tag('UTF8')
+    ->tag('DecimalSymbol', '.')
+    ->tag('DigitGroupingSymbol', '|')     # see CAVEATS in documentation
+    ->tag('Range', sub { $self
+      ->tag('From', $self->csv_headers ? 2 : 1)
+    })
+    ->tag('VariableLength', sub { $self
+      ->tag('ColumnDelimiter', ',')       # see CAVEATS for missing RecordDelimiter
+      ->tag('TextEncapsulator', '"')
+      ->columns($table)
+      ->foreign_keys($table)
+    })
+  });
+}
+
+sub _table_columns {
+  my ($table) = @_;
+  my $package = SL::DB::Helper::Mappings::get_package_for_table($table);
+
+  my %white_list;
+  my $use_white_list = 0;
+  if ($known_tables{$table}{columns}) {
+    $use_white_list = 1;
+    $white_list{$_} = 1 for @{ $known_tables{$table}{columns} || [] };
+  }
+
+  # PrimaryKeys must come before regular columns, so partition first
+  partition_by {
+    $known_tables{$table}{primary_key}
+      ? 1 * ($_ eq $known_tables{$table}{primary_key})
+      : 1 * $_->is_primary_key_member
+  } grep {
+    $use_white_list ? $white_list{$_->name} : 1
+  } $package->meta->columns;
+}
+
+sub columns {
+  my ($self, $table) = @_;
+
+  my %cols_by_primary_key = _table_columns($table);
+
+  for my $column (@{ $cols_by_primary_key{1} }) {
+    my $type = $column_types{ ref $column };
+
+    die "unknown col type @{[ ref $column ]}" unless $type;
+
+    $self->tag('VariablePrimaryKey', sub { $self
+      ->tag('Name', $column_titles{$table}{$column->name});
+      $type->($self);
+    })
+  }
+
+  for my $column (@{ $cols_by_primary_key{0} }) {
+    my $type = $column_types{ ref $column };
+
+    die "unknown col type @{[ ref $column]}" unless $type;
+
+    $self->tag('VariableColumn', sub { $self
+      ->tag('Name', $column_titles{$table}{$column->name});
+      $type->($self);
+    })
+  }
+
+  $self;
+}
+
+sub foreign_keys {
+  my ($self, $table) = @_;
+  my $package = SL::DB::Helper::Mappings::get_package_for_table($table);
+
+  my %requested = map { $_ => 1 } @{ $self->tables };
+
+  for my $rel ($package->meta->foreign_keys) {
+    next unless $requested{ $rel->class->meta->table };
+
+    # ok, now extract the columns used as foreign key
+    my %key_columns = $rel->key_columns;
+
+    if (1 != keys %key_columns) {
+      die "multi keys? we don't support this currently. fix it please";
+    }
+
+    if ($table eq $rel->class->meta->table) {
+      # self referential foreign keys are a PITA to export correctly. skip!
+      next;
+    }
+
+    $self->tag('ForeignKey', sub {
+      $_[0]->tag('Name',  $column_titles{$table}{$_}) for keys %key_columns;
+      $_[0]->tag('References', $rel->class->meta->table);
+   });
+  }
+}
+
+sub do_datev_xml_table {
+  my ($self) = @_;
+  my $writer = $self->writer;
+
+  $self->tag('Table', sub { $self
+    ->tag('URL', "transactions.csv")
+    ->tag('Name', t8('Transactions'))
+    ->tag('Description', t8('Transactions'))
+    ->tag('Validity', sub { $self
+      ->tag('Range', sub { $self
+        ->tag('From', $self->from->to_kivitendo(dateformat => 'dd.mm.yyyy'))
+        ->tag('To',   $self->to->to_kivitendo(dateformat => 'dd.mm.yyyy'))
+      })
+      ->tag('Format', $date_format)
+    })
+    ->tag('UTF8')
+    ->tag('DecimalSymbol', '.')
+    ->tag('DigitGroupingSymbol', '|')     # see CAVEATS in documentation
+    ->tag('Range', sub { $self
+      ->tag('From', $self->csv_headers ? 2 : 1)
+    })
+    ->tag('VariableLength', sub { $self
+      ->tag('ColumnDelimiter', ',')       # see CAVEATS for missing RecordDelimiter
+      ->tag('TextEncapsulator', '"')
+      ->datev_columns
+      ->datev_foreign_keys
+    })
+  });
+}
+
+sub datev_columns {
+  my ($self, $table) = @_;
+
+  my %cols_by_primary_key = partition_by { 1 * $datev_column_defs{$_}{primary_key} } @datev_columns;
+
+  for my $column (@{ $cols_by_primary_key{1} }) {
+    my $type = $column_types{ $datev_column_defs{$column}{type} };
+
+    die "unknown col type @{[ $column ]}" unless $type;
+
+    $self->tag('VariablePrimaryKey', sub { $self
+      ->tag('Name', $datev_column_defs{$column}{text});
+      $type->($self);
+    })
+  }
+
+  for my $column (@{ $cols_by_primary_key{0} }) {
+    my $type = $column_types{ $datev_column_defs{$column}{type} };
+
+    die "unknown col type @{[ ref $column]}" unless $type;
+
+    $self->tag('VariableColumn', sub { $self
+      ->tag('Name', $datev_column_defs{$column}{text});
+      $type->($self);
+    })
+  }
+
+  $self;
+}
+
+sub datev_foreign_keys {
+  my ($self) = @_;
+  # hard code weeee
+  $self->tag('ForeignKey', sub { $_[0]
+    ->tag('Name', $datev_column_defs{customer_id}{text})
+    ->tag('References', 'customer')
+  });
+  $self->tag('ForeignKey', sub { $_[0]
+    ->tag('Name', $datev_column_defs{vendor_id}{text})
+    ->tag('References', 'vendor')
+  });
+  $self->tag('ForeignKey', sub { $_[0]
+    ->tag('Name', $datev_column_defs{$_}{text})
+    ->tag('References', 'chart')
+  }) for qw(debit_accno credit_accno tax_accno);
+}
+
+sub do_datev_csv_export {
+  my ($self) = @_;
+
+  my $datev = SL::DATEV->new(from => $self->from, to => $self->to);
+
+  $datev->generate_datev_data(from_to => $datev->fromto);
+
+  if ($datev->errors) {
+    die [ $datev->errors ];
+  }
+
+  for my $transaction (@{ $datev->{DATEV} }) {
+    for my $entry (@{ $transaction }) {
+      $entry->{sortkey} = join '-', map { lc } (DateTime->from_kivitendo($entry->{transdate})->strftime('%Y%m%d'), $entry->{name}, $entry->{reference});
+    }
+  }
+
+  my @transactions = sort_by { $_->[0]->{sortkey} } @{ $datev->{DATEV} };
+
+  my $csv = Text::CSV_XS->new({ binary => 1, eol => "\r\n", sep_char => ",", quote_char => '"' });
+
+  my ($fh, $filename) = File::Temp::tempfile();
+  binmode($fh, ':utf8');
+
+  $self->files->{"transactions.csv"} = $filename;
+  push @{ $self->tempfiles }, $filename;
+
+  if ($self->csv_headers) {
+    $csv->print($fh, [ map { _normalize_cell($datev_column_defs{$_}{text}) } @datev_columns ]);
+  }
+
+  for my $transaction (@transactions) {
+    my $is_payment     = any { $_->{link} =~ m{A[PR]_paid} } @{ $transaction };
+
+    my ($soll, $haben) = map { $transaction->[$_] } ($transaction->[0]->{amount} > 0 ? (1, 0) : (0, 1));
+    my $tax            = defined($soll->{tax_accno}) ? $soll : defined($haben->{tax_accno}) ? $haben : {};
+    my $amount         = defined($soll->{net_amount}) ? $soll : $haben;
+    $haben->{notes}    = ($haben->{memo} || $soll->{memo}) if $haben->{memo} || $soll->{memo};
+    $haben->{notes}  //= '';
+    $haben->{notes}    =  SL::HTML::Util->strip($haben->{notes});
+
+    my $tax_amount = defined $amount->{net_amount} ? abs($amount->{amount}) - abs($amount->{net_amount}) : 0;
+
+    $tax = {} if abs($tax_amount) < 0.001;
+
+    my %row            = (
+      amount           => $::form->format_amount($myconfig, abs($amount->{amount}),5),
+      debit_accno      => $soll->{accno},
+      debit_accname    => $soll->{accname},
+      debit_amount     => $::form->format_amount($myconfig, abs(-$soll->{amount}),5),
+      debit_tax        => $soll->{tax_accno} ? $::form->format_amount($myconfig, $tax_amount, 5) : 0,
+      credit_accno     => $haben->{accno},
+      credit_accname   => $haben->{accname},
+      credit_amount    => $::form->format_amount($myconfig, abs($haben->{amount}),5),,
+      credit_tax       => $haben->{tax_accno} ? $::form->format_amount($myconfig, $tax_amount, 5) : 0,
+      tax              => $::form->format_amount($myconfig, $tax_amount, 5),
+      notes            => $haben->{notes},
+      (map { ($_ => $tax->{$_})                    } qw(taxkey tax_accname tax_accno taxdescription)),
+      (map { ($_ => ($haben->{$_} // $soll->{$_})) } qw(trans_id invnumber name vcnumber transdate gldate itime customer_id vendor_id)),
+    );
+
+#     if ($row{debit_amount} + $row{debit_tax} - ($row{credit_amount} + $row{credit_tax}) > 0.005) {
+#       $::lxdebug->dump(0,  "broken taxes", [ $transaction, \%row,  $row{debit_amount} + $row{debit_tax}, $row{credit_amount} + $row{credit_tax} ]);
+#     }
+
+    _normalize_cell($_) for values %row; # see CAVEATS
+
+    $csv->print($fh, [ map { $row{$_} } @datev_columns ]);
+  }
+
+  # and build xml spec for it
+}
+
+sub do_csv_export {
+  my ($self, $table) = @_;
+
+  my $csv = Text::CSV_XS->new({ binary => 1, eol => "\r\n", sep_char => ",", quote_char => '"' });
+
+  my ($fh, $filename) = File::Temp::tempfile();
+  binmode($fh, ':utf8');
+
+  $self->files->{"$table.csv"} = $filename;
+  push @{ $self->tempfiles }, $filename;
+
+  # in the right order (primary keys first)
+  my %cols_by_primary_key = _table_columns($table);
+  my @columns = (@{ $cols_by_primary_key{1} }, @{ $cols_by_primary_key{0} });
+  my %col_index = do { my $i = 0; map {; "$_" => $i++ } @columns };
+
+  if ($self->csv_headers) {
+    $csv->print($fh, [ map { _normalize_cell($column_titles{$table}{$_->name}) } @columns ]) or die $csv->error_diag;
+  }
+
+  # and normalize date stuff
+  my @select_tokens = map { (ref $_) =~ /Time/ ? $_->name . '::date' : $_->name } @columns;
+
+  my @where_tokens;
+  my @values;
+  if ($known_tables{$table}{transdate}) {
+    if ($self->from) {
+      push @where_tokens, "$known_tables{$table}{transdate} >= ?";
+      push @values, $self->from;
+    }
+    if ($self->to) {
+      push @where_tokens, "$known_tables{$table}{transdate} <= ?";
+      push @values, $self->to;
+    }
+  }
+  if ($known_tables{$table}{tables}) {
+    my ($col, @col_specs) = @{ $known_tables{$table}{tables} };
+    my %ids;
+    for (@col_specs) {
+      my ($ftable, $fkey) = split /\./, $_;
+      if (!exists $self->export_ids->{$ftable}{$fkey}) {
+         # check if we forgot to keep it
+         if (!grep { $_ eq $fkey } @{ $known_tables{$ftable}{keep} || [] }) {
+           die "unknown table spec '$_' for table $table, did you forget to keep $fkey in $ftable?"
+         } else {
+           # hmm, most likely just an empty set.
+           $self->export_ids->{$ftable}{$fkey} = {};
+         }
+      }
+      $ids{$_}++ for keys %{ $self->export_ids->{$ftable}{$fkey} };
+    }
+    if (keys %ids) {
+      push @where_tokens, "$col IN (@{[ join ',', ('?') x keys %ids ]})";
+      push @values, keys %ids;
+    } else {
+      push @where_tokens, '1=0';
+    }
+  }
+
+  my $where_clause = @where_tokens ? 'WHERE ' . join ' AND ', @where_tokens : '';
+
+  my $query = "SELECT " . join(', ', @select_tokens) . " FROM $table $where_clause";
+
+  my $sth = $::form->get_standard_dbh->prepare($query);
+  $sth->execute(@values) or $::form->dberror($query);
+
+  while (my $row = $sth->fetch) {
+    for my $keep_col (@{ $known_tables{$table}{keep} || [] }) {
+      next if !$row->[$col_index{$keep_col}];
+      $self->export_ids->{$table}{$keep_col} ||= {};
+      $self->export_ids->{$table}{$keep_col}{$row->[$col_index{$keep_col}]}++;
+    }
+    _normalize_cell($_) for @$row; # see CAVEATS
+
+    $csv->print($fh, $row) or $csv->error_diag;
+  }
+  $sth->finish();
+}
+
+sub tag {
+  my ($self, $tag, $content) = @_;
+
+  $self->writer->startTag($tag);
+  if ('CODE' eq ref $content) {
+    $content->($self);
+  } else {
+    $self->writer->characters($content);
+  }
+  $self->writer->endTag;
+  return $self;
+}
+
+sub make_comment {
+  my $gobd_version  = API_VERSION();
+  my $kivi_version  = SL::Version->get_version;
+  my $person        = $::myconfig{name};
+  my $contact       = join ', ',
+    (t8("Email") . ": $::myconfig{email}" ) x!! $::myconfig{email},
+    (t8("Tel")   . ": $::myconfig{tel}" )   x!! $::myconfig{tel},
+    (t8("Fax")   . ": $::myconfig{fax}" )   x!! $::myconfig{fax};
+
+  t8('DataSet for GoBD version #1. Created with kivitendo #2 by #3 (#4)',
+    $gobd_version, $kivi_version, $person, $contact
+  );
+}
+
+sub client_name {
+  $_[0]->company
+}
+
+sub client_location {
+  $_[0]->location
+}
+
+sub sorted_tables {
+  my ($self) = @_;
+
+  my %given = map { $_ => 1 } @{ $self->tables };
+
+  grep { $given{$_} } @export_table_order;
+}
+
+sub all_tables {
+  my ($self, $yesno) = @_;
+
+  $self->tables(\@export_table_order) if $yesno;
+}
+
+sub _normalize_cell {
+  $_[0] =~ s/\r\n/ /g;
+  $_[0] =~ s/,/;/g;
+  $_[0] =~ s/"/'/g;
+  $_[0] =~ s/!/./g;
+  $_[0]
+}
+
+sub init_files { +{} }
+sub init_export_ids { +{} }
+sub init_tempfiles { [] }
+sub init_tables { [ grep { $known_tables{$_} } @export_table_order ] }
+sub init_csv_headers { 1 }
+
+sub API_VERSION {
+  DateTime->new(year => 2002, month => 8, day => 14)->to_kivitendo;
+}
+
+sub DESTROY {
+  unlink $_ for @{ $_[0]->tempfiles || [] };
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::GoBD - IDEA export generator
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<new PARAMS>
+
+Create new export object. C<PARAMS> may contain:
+
+=over 4
+
+=item company
+
+The name of the company, needed for the supplier header
+
+=item location
+
+Location of the company, needed for the supplier header
+
+=item from
+
+=item to
+
+Will only include records in the specified date range. Data pulled from other
+tables will be culled to match what is needed for these records.
+
+=item csv_headers
+
+Optional. If set, will include a header line in the exported CSV files. Default true.
+
+=item tables
+
+Ooptional list of tables to be exported. Defaults to all tables.
+
+=item all_tables
+
+Optional alternative to C<tables>, forces all known tables.
+
+=back
+
+=item C<generate_export>
+
+Do the work. Will return an absolute path to a temp file where all export files
+are zipped together.
+
+=back
+
+=head1 CAVEATS
+
+Sigh. There are a lot of issues with the IDEA software that were found out by
+trial and error.
+
+=head2 Problems in the Specification
+
+=over 4
+
+=item *
+
+The specced date format is capable of only C<YY>, C<YYYY>, C<MM>,
+and C<DD>. There are no timestamps or timezones.
+
+=item *
+
+Numbers have the same issue. There is not dedicated integer type, and hinting
+at an integer type by setting accuracy to 0 generates a warning for redundant
+accuracy.
+
+Also the number parsing is documented to be fragile. Official docs state that
+behaviour for too low C<Accuracy> settings is undefined.
+
+=item *
+
+Foreign key definition is broken. Instead of giving column maps it assumes that
+foreign keys map to the primary keys given for the target table, and in that
+order. Also the target table must be known in full before defining a foreign key.
+
+As a consequence any additional keys apart from primary keys are not possible.
+Self-referencing tables are also not possible.
+
+=item *
+
+The spec does not support splitting data sets into smaller chunks. For data
+sets that exceed 700MB the spec helpfully suggests: "Use a bigger medium, such
+as a DVD".
+
+=item *
+
+It is not possible to set an empty C<DigitGroupingSymbol> since then the import
+will just work with the default. This was asked in their forum, and the
+response actually was to use a bogus grouping symbol that is not used:
+
+  Einfache Lösung: Definieren Sie das Tausendertrennzeichen als Komma, auch
+  wenn es nicht verwendet wird. Sollten Sie das Komma bereits als Feldtrenner
+  verwenden, so wählen Sie als Tausendertrennzeichen eine Alternative wie das
+  Pipe-Symbol |.
+
+L<http://www.gdpdu-portal.com/forum/index.php?mode=thread&id=1392>
+
+=item *
+
+It is not possible to define a C<RecordDelimiter> with XML entities. &#x0A;
+generates the error message:
+
+  C<RecordDelimiter>-Wert (&#x0A;) sollte immer aus ein oder zwei Zeichen
+  bestehen.
+
+Instead we just use the implicit default RecordDelimiter CRLF.
+
+=back
+
+=head2 Bugs in the IDEA software
+
+=over 4
+
+=item *
+
+The CSV import library used in IDEA is not able to parse newlines (or more
+exactly RecordDelimiter) in data. So this export substites all of these with
+spaces.
+
+=item *
+
+Neither it is able to parse escaped C<ColumnDelimiter> in data. It just splits
+on that symbol no matter what surrounds or preceeds it.
+
+=item *
+
+Oh and of course C<TextEncapsulator> is also not allowed in data. It's just
+stripped at the beginning and end of data.
+
+=item *
+
+And the character "!" is used internally as a warning signal and must not be
+present in the data as well.
+
+=item *
+
+C<VariableLength> data is truncated on import to 512 bytes (Note: it said
+characters, but since they are mutilating data into a single byte encoding
+anyway, they most likely meant bytes). The auditor recommends splitting into
+multiple columns.
+
+=item *
+
+Despite the standard specifying UTF-8 as a valid encoding the IDEA software
+will just downgrade everything to latin1.
+
+=back
+
+=head2 Problems outside of the software
+
+=over 4
+
+=item *
+
+The law states that "all business related data" should be made available. In
+practice there's no definition for what makes data "business related", and
+different auditors seems to want different data.
+
+Currently we export most of the transactional data with supplementing
+customers, vendors and chart of accounts.
+
+=item *
+
+While the standard explicitely state to provide data normalized, in practice
+autditors aren't trained database operators and can not create complex vies on
+normalized data on their own. The reason this works for other software is, that
+DATEV and SAP seem to have written import plugins for their internal formats in
+the IDEA software.
+
+So what is really exported is not unlike a DATEV export. Each transaction gets
+splitted into chunks of 2 positions (3 with tax on one side). Those get
+denormalized into a single data row with credfit/debit/tax fields. The charts
+get denormalized into it as well, in addition to their account number serving
+as a foreign key.
+
+Customers and vendors get denormalized into this as well, but are linked by ids
+to their tables. And the reason for this is...
+
+=item *
+
+Some auditors do not have a full license of the IDEA software, and
+can't do table joins.
+
+=back
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut
index f68fb0a..0703186 100644 (file)
@@ -27,7 +27,7 @@ sub strip {
   if (!%stripper) {
     %stripper = ( parser => HTML::Parser->new );
 
-    $stripper{parser}->handler(text => sub { $stripper{text} .= $_[1]; });
+    $stripper{parser}->handler(text => sub { $stripper{text} .= ' ' . $_[1]; });
   }
 
   $stripper{text} = '';
@@ -35,10 +35,37 @@ sub strip {
   $stripper{parser}->eof;
 
   $stripper{text} =~ s{\&([^;]+);}{ $entities{$1} || "\&$1;" }eg;
+  $stripper{text} =~ s{^ +| +$}{}g;
+  $stripper{text} =~ s{ {2,}}{ }g;
 
   return delete $stripper{text};
 }
 
+sub plain_text_to_html {
+  my ($class_or_text) = @_;
+
+  my $text = !ref($class_or_text) && (($class_or_text // '') eq 'SL::HTML::Util') ? $_[1] : $class_or_text;
+
+  return $text if $text =~ m{^<p>.*</p>$};
+
+  $text =~ s{\r+}{}g;
+  $text =~ s{^[[:space:]]+|[[:space:]]+$}{}g;
+
+  return '' if $text eq '';
+
+  my @paragraphs;
+
+  foreach my $paragraph (split m{\n{2,}}, $text) {
+    no warnings 'once';
+    $paragraph =  $::locale->quote_special_chars('HTML', $paragraph);
+    $paragraph =~ s{\n}{<br>}g;
+
+    push @paragraphs, $paragraph;
+  }
+
+  return '<p>' . join('</p><p>', @paragraphs) . '</p>';
+}
+
 1;
 __END__
 
@@ -63,6 +90,12 @@ SL::HTML::Util - Utility functions dealing with HTML
 Removes all HTML elements and tags from C<$html_content> and returns
 the remaining plain text.
 
+=item C<plain_text_to_html $text>
+
+Converts a plain text to HTML: paragraphs will be recognized by empty
+lines; remaining newlines will be converted into forced line breaks;
+the rest will be HTML escaped.
+
 =back
 
 =head1 BUGS
index d85ad7d..06fb819 100644 (file)
@@ -6,19 +6,22 @@ use Carp;
 use Cwd;
 use English qw(-no_match_vars);
 use File::Slurp ();
-use File::Spec ();
-use File::Temp ();
+use File::Spec  ();
+use File::Temp  ();
+use File::Copy qw(move);
 use List::MoreUtils qw(uniq);
 use List::Util qw(first);
+use Scalar::Util qw(blessed);
 use String::ShellQuote ();
 
-use SL::Form;
 use SL::Common;
 use SL::DB::Language;
 use SL::DB::Printer;
 use SL::MoreCommon;
+use SL::System::Process;
 use SL::Template;
 use SL::Template::LaTeX;
+use SL::X;
 
 use Exporter 'import';
 our @EXPORT_OK = qw(create_pdf merge_pdfs find_template);
@@ -39,34 +42,53 @@ sub create_pdf {
 sub create_parsed_file {
   my ($class, %params) = @_;
 
-  my $userspath       = $::lx_office_conf{paths}->{userspath};
-  my $vars            = $params{variables} || {};
-  my $form            = Form->new('');
-  $form->{$_}         = $vars->{$_} for keys %{ $vars };
-  $form->{format}     = lc($params{format} || 'pdf');
-  $form->{cwd}        = getcwd();
-  $form->{templates}  = $::instance_conf->get_templates;
-  $form->{IN}         = $params{template};
-  $form->{tmpdir}     = $form->{cwd} . '/' . $userspath;
-  my $tmpdir          = $form->{tmpdir};
-  my ($suffix)        = $params{template} =~ m{\.(.+)};
+  my $keep_temp_files = $::lx_office_conf{debug} && $::lx_office_conf{debug}->{keep_temp_files};
+  my $userspath       = SL::System::Process::exe_dir() . "/" . $::lx_office_conf{paths}->{userspath};
+  my $temp_dir        = File::Temp->newdir(
+    "kivitendo-print-XXXXXX",
+    DIR     => $userspath,
+    CLEANUP => !$keep_temp_files,
+  );
+
+  my $vars           = $params{variables} || {};
+  my $form           = Form->new('');
+  $form->{$_}        = $vars->{$_} for keys %{$vars};
+  $form->{format}    = lc($params{format} || 'pdf');
+  $form->{cwd}       = SL::System::Process::exe_dir();
+  $form->{templates} = $::instance_conf->get_templates;
+  $form->{IN}        = $params{template};
+  $form->{tmpdir}    = $temp_dir->dirname;
+  my $tmpdir         = $form->{tmpdir};
+  my ($suffix)       = $params{template} =~ m{\.(.+)};
 
   my ($temp_fh, $tmpfile) = File::Temp::tempfile(
     'kivitendo-printXXXXXX',
     SUFFIX => ".${suffix}",
     DIR    => $form->{tmpdir},
-    UNLINK => ($::lx_office_conf{debug} && $::lx_office_conf{debug}->{keep_temp_files})? 0 : 1,
+    UNLINK => !$keep_temp_files,
   );
 
   $form->{tmpfile} = $tmpfile;
+  (undef, undef, $form->{template_meta}{tmpfile}) = File::Spec->splitpath($tmpfile);
+
+  my %driver_options;
+  eval {
+    %driver_options = _maybe_attach_zugferd_data($params{record});
+  };
+
+  if (my $e = SL::X::ZUGFeRDValidation->caught) {
+    $form->cleanup;
+    die $e->message;
+  }
 
-  my $parser  = SL::Template::create(
-    type      => ($params{template_type} || 'LaTeX'),
-    source    => $form->{IN},
-    form      => $form,
-    myconfig  => \%::myconfig,
-    userspath => $tmpdir,
+  my $parser               = SL::Template::create(
+    type                   => ($params{template_type} || 'LaTeX'),
+    source                 => $form->{IN},
+    form                   => $form,
+    myconfig               => \%::myconfig,
+    userspath              => $tmpdir,
     variable_content_types => $params{variable_content_types},
+    %driver_options,
   );
 
   my $result = $parser->parse($temp_fh);
@@ -86,7 +108,7 @@ sub create_parsed_file {
   my ($volume, $directory, $file_name) = File::Spec->splitpath($form->{tmpfile});
   my $full_file_name                   = File::Spec->catfile($tmpdir, $file_name);
   if (($params{return} || 'content') eq 'file_name') {
-    my $new_name = File::Spec->catfile($tmpdir, 'keep-' . $form->{tmpfile});
+    my $new_name = File::Spec->catfile($userspath, 'keep-' . $form->{tmpfile});
     rename $full_file_name, $new_name;
 
     $form->cleanup;
@@ -101,10 +123,43 @@ sub create_parsed_file {
   return $content;
 }
 
+#
+# Alternativen zu pdfinfo wären (aber wesentlich langamer):
+#
+# gs  -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -sOutputFile=/dev/null $filename | grep 'Processing pages'
+# my (undef,undef,undef,undef,$pages)  = split / +/,$shell_out;
+#
+# gs  -dBATCH -dNOPAUSE -q -dNODISPLAY -c "($filename) (r) file runpdfbegin pdfpagecount = quit"
+# $pages=$shell_out;
+#
+
+sub has_odd_pages {
+  my ($class, $filename) = @_;
+  return 0 unless -f $filename;
+  my $shell_out = `pdfinfo $filename | grep 'Pages:'`;
+  my ($label, $pages) = split / +/, $shell_out;
+  return $pages & 1;
+}
+
 sub merge_pdfs {
   my ($class, %params) = @_;
-
-  return scalar(File::Slurp::read_file($params{file_names}->[0])) if scalar(@{ $params{file_names} }) < 2;
+  my $filecount = scalar(@{ $params{file_names} });
+
+  if ($params{inp_content}) {
+    return $params{inp_content} if $filecount == 0 && !$params{out_path};
+  } elsif ($params{out_path}) {
+    return 0 if $filecount == 0;
+    if ($filecount == 1) {
+      if (!rename($params{file_names}->[0], $params{out_path})) {
+        # special filesystem or cross filesystem etc
+        move($params{file_names}->[0], $params{out_path});
+      }
+      return 1;
+    }
+  } else {
+    return '' if $filecount == 0;
+    return scalar(File::Slurp::read_file($params{file_names}->[0])) if $filecount == 1;
+  }
 
   my ($temp_fh, $temp_name) = File::Temp::tempfile(
     'kivitendo-printXXXXXX',
@@ -114,13 +169,49 @@ sub merge_pdfs {
   );
   close $temp_fh;
 
-  my $input_names = join ' ', String::ShellQuote::shell_quote(@{ $params{file_names} });
-  my $exe         = $::lx_office_conf{applications}->{ghostscript} || 'gs';
-  my $output      = `$exe -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile=${temp_name} ${input_names} 2>&1`;
+  my $input_names = '';
+  my $hasodd      = 0;
+  my $emptypage   = '';
+  if ($params{bothsided}) {
+    $emptypage = $::instance_conf->get_templates . '/emptyPage.pdf';
+    unless (-f $emptypage) {
+      $emptypage = '';
+      delete $params{bothsided};
+    }
+  }
+  if ($params{inp_content}) {
+    my ($temp_fh, $inp_name) = File::Temp::tempfile(
+      'kivitendo-contentXXXXXX',
+      SUFFIX => '.pdf',
+      DIR    => $::lx_office_conf{paths}->{userspath},
+      UNLINK => ($::lx_office_conf{debug} && $::lx_office_conf{debug}->{keep_temp_files})? 0 : 1,
+    );
+    binmode $temp_fh;
+    print $temp_fh $params{inp_content};
+    close $temp_fh;
+    $input_names = $inp_name . ' ';
+    $hasodd = $params{bothsided} && __PACKAGE__->has_odd_pages($inp_name);
+  }
+  foreach (@{ $params{file_names} }) {
+    $input_names .= $emptypage . ' ' if $hasodd;
+    $input_names .= String::ShellQuote::shell_quote($_) . ' ';
+    $hasodd = $params{bothsided} && __PACKAGE__->has_odd_pages($_);
+  }
+  my $exe = $::lx_office_conf{applications}->{ghostscript} || 'gs';
+  my $output =
+    `$exe -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile=${temp_name} ${input_names} 2>&1`;
 
   die "Executing gs failed: $ERRNO" if !defined $output;
   die $output                       if $? != 0;
 
+  if ($params{out_path}) {
+    if (!rename($temp_name, $params{out_path})) {
+
+      # special filesystem or cross filesystem etc
+      move($temp_name, $params{out_path});
+    }
+    return 1;
+  }
   return scalar File::Slurp::read_file($temp_name);
 }
 
@@ -172,6 +263,35 @@ sub find_template {
   return wantarray ? ($template, @template_files) : $template;
 }
 
+sub _maybe_attach_zugferd_data {
+  my ($record) = @_;
+
+  return if !blessed($record)
+    || !$record->can('customer')
+    || !$record->customer
+    || !$record->can('create_pdf_a_print_options')
+    || !$record->can('create_zugferd_data')
+    || !$record->customer->create_zugferd_invoices_for_this_customer;
+
+  my $xmlfile = File::Temp->new;
+  $xmlfile->print($record->create_zugferd_data);
+  $xmlfile->close;
+
+  my %driver_options = (
+    pdf_a           => $record->create_pdf_a_print_options(zugferd_xmp_data => $record->create_zugferd_xmp_data),
+    pdf_attachments => [
+      { source       => $xmlfile,
+        name         => 'factur-x.xml',
+        description  => $::locale->text('Factur-X/ZUGFeRD invoice'),
+        relationship => '/Alternative',
+        mime_type    => 'text/xml',
+      }
+    ],
+  );
+
+  return %driver_options;
+}
+
 1;
 __END__
 
@@ -296,6 +416,10 @@ names containing C<_printer_template_code> are considered as well.
 Merges two or more PDFs into a single PDF by using the external
 application ghostscript.
 
+Normally the function returns the contents of the resulting PDF.
+if The parameter C<out_path> is set the resulting PDF is in this file
+and the return value is 1 if it successful or 0 if not.
+
 The recognized parameters are:
 
 =over 2
@@ -303,6 +427,10 @@ The recognized parameters are:
 =item * C<file_names> – mandatory array reference containing the file
 names to merge.
 
+=item * C<inp_content> – optional, contents of first file to merge with C<file_names>.
+
+=item * C<out_path> – optional, returns not the merged contents but wrote him into this file
+
 =back
 
 Note that this function relies on the presence of the external
index 43aaabb..0d471eb 100644 (file)
@@ -114,6 +114,7 @@ sub _check_multiplexed {
       # Each profile needs a class and a row_ident
       my $info_ok = all { defined $_->{class} && defined $_->{row_ident} } @profile;
       $self->_push_error([
+        undef,
         0,
         "missing class or row_ident in one of the profiles for multiplexed data",
         0,
@@ -125,6 +126,7 @@ sub _check_multiplexed {
         my @header = @{ $self->header };
         my $t_ok = scalar @profile == scalar @header;
         $self->_push_error([
+          undef,
           0,
           "number of headers and number of profiles must be the same for multiplexed data",
           0,
@@ -133,6 +135,7 @@ sub _check_multiplexed {
 
         $t_ok = all { scalar @$_ > 0} @header;
         $self->_push_error([
+          undef,
           0,
           "no empty headers are allowed for multiplexed data",
           0,
@@ -158,10 +161,11 @@ sub _check_header {
     foreach my $p_num (0..$n_header - 1) {
       my $h = $self->_csv->getline($self->_io);
 
+      my ($code, $string, $position, $record, $field) = $self->_csv->error_diag;
+
       $self->_push_error([
         $self->_csv->error_input,
-        $self->_csv->error_diag,
-        0,
+        $code, $string, $position, $record // 0,
       ]) unless $h;
 
       if ($self->is_multiplexed) {
@@ -202,12 +206,13 @@ sub _check_header {
       my $h_aref = ($self->is_multiplexed)? $header : [ $header ];
       my $p_num  = 0;
       foreach my $h (@{ $h_aref }) {
-        my @names = (
-          keys %{ $self->profile->[$p_num]->{profile} || {} },
+        my %names = (
+          (map { $_ => $_                                     } keys %{ $self->profile->[$p_num]->{profile} || {} }),
+          (map { $_ => $self->profile->[$p_num]{mapping}{$_}  } keys %{ $self->profile->[$p_num]->{mapping} || {} }),
         );
-        for my $name (@names) {
+        for my $name (keys %names) {
           for my $i (0..$#$h) {
-            $h->[$i] = $name if lc $h->[$i] eq lc $name;
+            $h->[$i] = $names{$name} if lc $h->[$i] eq lc $name;
           }
         }
         $p_num++;
@@ -229,7 +234,8 @@ sub _check_multiplex_datatype_position {
     $self->_multiplex_datatype_position($first_pos);
     return 1;
   } else {
-    $self->_push_error([0,
+    $self->_push_error([undef,
+                        0,
                         "datatype field must be at the same position for all datatypes for multiplexed data",
                         0,
                         0]);
@@ -237,6 +243,10 @@ sub _check_multiplex_datatype_position {
   }
 }
 
+sub _is_empty_row {
+  return !!all { !$_ } @{$_[0]};
+}
+
 sub _parse_data {
   my ($self, %params) = @_;
   my (@data, @errors);
@@ -244,25 +254,31 @@ sub _parse_data {
   while (1) {
     my $row = $self->_csv->getline($self->_io);
     if ($row) {
+      next if _is_empty_row($row);
       my $header = $self->_header_by_row($row);
+      if (!$header) {
+        push @errors, [
+          undef,
+          0,
+          "Cannot get header for row. Maybe row name and datatype field not matching.",
+          0,
+          0];
+        last;
+      }
       my %hr;
       @hr{@{ $header }} = @$row;
       push @data, \%hr;
     } else {
       last if $self->_csv->eof;
+
       # Text::CSV_XS 0.89 added record number to error_diag
-      if (qv(Text::CSV_XS->VERSION) >= qv('0.89')) {
-        push @errors, [
-          $self->_csv->error_input,
-          $self->_csv->error_diag,
-        ];
-      } else {
-        push @errors, [
-          $self->_csv->error_input,
-          $self->_csv->error_diag,
-          $self->_io->input_line_number,
-        ];
-      }
+      my ($code, $string, $position, $record, $field) = $self->_csv->error_diag;
+
+      push @errors, [
+        $self->_csv->error_input,
+        $code, $string, $position,
+        $record // $self->_io->input_line_number,
+      ];
     }
     last if $self->_csv->eof;
   }
@@ -332,6 +348,9 @@ sub _push_error {
   $self->_errors(\@new_errors);
 }
 
+sub specs {
+  $_[0]->dispatcher->_specs
+}
 
 1;
 
@@ -480,84 +499,132 @@ Examples:
   [ [ 'datatype', 'ordernumber', 'customer', 'transdate' ],
     [ 'datatype', 'partnumber', 'qty', 'sellprice' ] ]
 
-=item C<profile> [{profile => \%ACCESSORS, class => class, row_ident => ri},]
+=item C<profile> PROFILE_DATA
+
+The profile mapping csv to the objects.
+
+See section L</PROFILE> for information on this topic.
+
+=item C<ignore_unknown_columns>
+
+If set, the import will ignore unknown header columns. Useful for lazy imports,
+but deactivated by default.
+
+=item C<case_insensitive_header>
+
+If set, header columns will be matched against profile entries case
+insensitive, and on match the profile name will be taken.
+
+Only works if a profile is given, will die otherwise.
+
+If both C<case_insensitive_header> and C<strict_profile> is set, matched header
+columns will be accepted.
+
+=item C<strict_profile>
+
+If set, all columns to be parsed must be specified in C<profile>. Every header
+field not listed there will be treated like an unknown column.
+
+If both C<case_insensitive_header> and C<strict_profile> is set, matched header
+columns will be accepted.
+
+=back
+
+=head1 PROFILE
+
+The profile is needed for mapping csv data to the accessors in the data object.
 
-This is an ARRAYREF to HASHREFs which may contain the keys C<profile>, C<class>
-and C<row_ident>.
+The basic structure is:
 
-The C<profile> is a HASHREF which may be used to map header fields to custom
+  PROFILE       := [ CLASS_PROFILE, CLASS_PROFILE* ]
+  CLASS_PROFILE := {
+                      profile   => { ACCESSORS+ },
+                      class     => $classname,
+                      row_ident => $row_ident,
+                      mapping   => { MAPPINGS* },
+                   }
+  ACCESSORS     := $field => $accessor
+  MAPPINGS      := $alias => $field
+
+The C<ACCESSORS> may be used to map header fields to custom
 accessors. Example:
 
-  [ {profile => { listprice => listprice_as_number }} ]
+  profile => {
+    listprice => 'listprice_as_number',
+  }
 
-In this case C<listprice_as_number> will be used to read in values from the
+In this case C<listprice_as_number> will be used to store the values from the
 C<listprice> column.
 
 In case of a One-To-One relationship these can also be set over
 relationships by separating the steps with a dot (C<.>). This will work:
 
-  [ {profile => { customer => 'customer.name' }} ]
+  customer => 'customer.name',
 
 And will result in something like this:
 
   $obj->customer($obj->meta->relationship('customer')->class->new);
   $obj->customer->name($csv_line->{customer})
 
-But beware, this will not try to look up anything in the database. You will
+Beware, this will not try to look up anything in the database! You will
 simply receive objects that represent what the profile defined. If some of
-these information are unique, and should be connected to preexisting data, you
+these information are unique, or should be connected to preexisting data, you
 will have to do that for yourself. Since you provided the profile, it is
 assumed you know what to do in this case.
 
 If no profile is given, any header field found will be taken as is.
 
 If the path in a profile entry is empty, the field will be subjected to
-C<strict_profile> and C<case_insensitive_header> checking, will be parsed into
-C<get_data>, but will not be attempted to be dispatched into objects.
-
-If C<class> is present, the line will be handed to the new sub of this class,
-and the return value used instead of the line itself.
+C<strict_profile> and C<case_insensitive_header> checking and will be parsed
+into C<get_data>, but will not be attempted to be dispatched into objects.
 
-C<row_ident> is a string to recognize the right profile and class for each data
-line in multiplexed data. It must match the value in the column 'dataype' for
-each class.
-
-In case of multiplexed data, C<class> and C<row_ident> must be given.
-Example:
-  [ {
-      class     => 'SL::DB::Order',
-      row_ident => 'O'
-    },
-    {
-      class     => 'SL::DB::OrderItem',
-      row_ident => 'I',
-      profile   => {sellprice => sellprice_as_number}
-    } ]
+C<class> must be present. A new instance will be created for each line before
+dispatching into it.
 
-=item C<ignore_unknown_columns>
+C<row_ident> is used to determine the correct profile in multiplexed data and
+must be given there. It's not used in non-multiplexed data.
 
-If set, the import will ignore unkown header columns. Useful for lazy imports,
-but deactivated by default.
+If C<mappings> is present, it must contain a hashref that maps strings to known
+fields. This can be used to add custom profiles for known sources, that don't
+comply with the expected header identities.
 
-=item C<case_insensitive_header>
+Without strict profiles, mappings can also directly map header fields that
+should end up in the same accessor.
 
-If set, header columns will be matched against profile entries case
-insensitive, and on match the profile name will be taken.
+With case insensitive headings, mappings will also modify the headers, to fit
+the expected profile.
 
-Only works if a profile is given, will die otherwise.
+Mappings can be identical to known fields and will be prefered during lookup,
+but will not replace the field, meaning that:
 
-If both C<case_insensitive_header> and C<strict_profile> is set, matched header
-columns will be accepted.
+  profile => {
+    name        => 'name',
+    description => 'description',
+  }
+  mapping => {
+    name        => 'description',
+    shortname   => 'name',
+  }
 
-=item C<strict_profile>
+will work as expected, and shortname will not end up in description. This also
+works with the case insensitive option. Note however that the case insensitive
+option will not enable true unicode collating.
 
-If set, all columns to be parsed must be specified in C<profile>. Every header
-field not listed there will be treated like an unknown column.
 
-If both C<case_insensitive_header> and C<strict_profile> is set, matched header
-columns will be accepted.
+Here's a full example:
 
-=back
+  [
+    {
+      class     => 'SL::DB::Order',
+      row_ident => 'O'
+    },
+    {
+      class     => 'SL::DB::OrderItem',
+      row_ident => 'I',
+      profile   => { sellprice => 'sellprice_as_number' },
+      mapping   => { 'Verkaufspreis' => 'sellprice' }
+    },
+  ]
 
 =head1 ERROR HANDLING
 
@@ -572,6 +639,9 @@ Each entry is an object with the following attributes:
 
 Note that the last entry can be off, but will give an estimate.
 
+Error handling is also known to break on new Perl versions and need to be
+adjusted from time to time due to changes in Text::CSV_XS.
+
 =head1 CAVEATS
 
 =over 4
index 3a725b5..801e822 100644 (file)
@@ -99,8 +99,8 @@ sub apply {
 }
 
 sub is_known {
-  my ($self, $col) = @_;
-  return grep { $col eq $_->{key} } $self->_specs;
+  my ($self, $col, $row) = @_;
+  return grep { $col eq $_->{key} } @{ $self->_specs->[$row // 0] };
 }
 
 sub parse_profile {
@@ -113,6 +113,7 @@ sub parse_profile {
   my $i = 0;
   foreach my $header (@{ $h_aref }) {
     my $spec = $self->_parse_profile(profile => $csv_profile->[$i]->{profile},
+                                     mapping => $csv_profile->[$i]->{mapping},
                                      class   => $csv_profile->[$i]->{class},
                                      header  => $header);
     push @specs, $spec;
@@ -129,23 +130,24 @@ sub parse_profile {
 sub _parse_profile {
   my ($self, %params) = @_;
 
-  my $profile = $params{profile};
+  my $profile = $params{profile} // {};
   my $class   = $params{class};
   my $header  = $params{header};
+  my $mapping = $params{mapping};
 
   my @specs;
 
   for my $col (@$header) {
     next unless $col;
-    if ($self->_csv->strict_profile) {
-      if (exists $profile->{$col}) {
-        push @specs, $self->make_spec($col, $profile->{$col}, $class);
-      } else {
-        $self->unknown_column($col, undef);
-      }
+    if (exists $mapping->{$col} && $profile->{$mapping->{$col}}) {
+      push @specs, $self->make_spec($col, $profile->{$mapping->{$col}}, $class);
+    } elsif (exists $mapping->{$col} && !%{ $profile }) {
+      push @specs, $self->make_spec($col, $mapping->{$col}, $class);
+    } elsif (exists $profile->{$col}) {
+      push @specs, $self->make_spec($col, $profile->{$col}, $class);
     } else {
-      if (exists $profile->{$col}) {
-        push @specs, $self->make_spec($col, $profile->{$col}, $class);
+      if ($self->_csv->strict_profile) {
+        $self->unknown_column($col, undef);
       } else {
         push @specs, $self->make_spec($col, $col, $class);
       }
@@ -158,7 +160,7 @@ sub _parse_profile {
 sub make_spec {
   my ($self, $col, $path, $cur_class) = @_;
 
-  my $spec = { key => $col, steps => [] };
+  my $spec = { key => $col, path => $path, steps => [] };
 
   return unless $path;
 
index 831e301..15ab95a 100644 (file)
@@ -2,8 +2,12 @@ package DateTime;
 
 use strict;
 
+use DateTime::Format::Strptime;
+
 use SL::Util qw(_hashify);
 
+my ($ymd_parser, $ymdhms_parser);
+
 sub new_local {
   my ($class, %params) = @_;
   return $class->new(hour => 0, minute => 0, second => 0, time_zone => $::locale->get_local_time_zone, %params);
@@ -94,6 +98,37 @@ sub next_workday {
   return $self;
 }
 
+sub from_ymd {
+  my ($class, $ymd_string) = @_;
+
+  if (!$ymd_parser) {
+    $ymd_parser = DateTime::Format::Strptime->new(
+      pattern   => '%Y-%m-%d',
+      locale    => 'de_DE',
+      time_zone => 'local'
+    );
+  }
+
+  return $ymd_parser->parse_datetime($ymd_string // '');
+}
+
+sub from_ymdhms {
+  my ($class, $ymdhms_string) = @_;
+
+  if (!$ymdhms_parser) {
+    $ymdhms_parser = DateTime::Format::Strptime->new(
+      pattern   => '%Y-%m-%dT%H:%M:%S',
+      locale    => 'de_DE',
+      time_zone => 'local'
+    );
+  }
+
+  $ymdhms_string //= '';
+  $ymdhms_string   =~ s{ }{T};
+
+  return $ymdhms_parser->parse_datetime($ymdhms_string);
+}
+
 1;
 
 __END__
@@ -138,6 +173,17 @@ component (as opposed to L<to_kivitendo>).
 
 The legacy name C<from_lxoffice> is still supported.
 
+=item C<from_ymd $string>
+
+Parses a date string in the ISO 8601 format C<YYYY-MM-DD> and returns
+an instance of L<DateTime>. The time is set to midnight (00:00:00).
+
+=item C<from_ymdhms $string>
+
+Parses a date/time string in the ISO 8601 format
+C<YYYY-MM-DDTHH:MM:SS> (a space instead of C<T> is also supported) and
+returns an instance of L<DateTime>.
+
 =item C<end_of_month>
 
 Sets the object to the last day of object's month at midnight. Returns
diff --git a/SL/Helper/File.pm b/SL/Helper/File.pm
new file mode 100644 (file)
index 0000000..e658b05
--- /dev/null
@@ -0,0 +1,160 @@
+package SL::Helper::File;
+
+use strict;
+
+use Exporter 'import';
+our @EXPORT_OK = qw(store_pdf append_general_pdf_attachments doc_storage_enabled);
+our %EXPORT_TAGS = (all => \@EXPORT_OK,);
+use SL::File;
+
+sub doc_storage_enabled {
+  return 0 unless $::instance_conf->get_doc_storage;
+  return 1 if     $::instance_conf->get_doc_storage_for_documents eq 'Filesystem' && $::instance_conf->get_doc_files;
+  return 1 if     $::instance_conf->get_doc_storage_for_documents eq 'Webdav'     && $::instance_conf->get_doc_webdav;
+  return 0;
+}
+
+sub store_pdf {
+  my ($self, $form) = @_;
+  return unless $self->doc_storage_enabled;
+  my $type = $form->{type};
+  $type = $form->{formname}        if $form->{formname} && !$form->{type};
+  $type = $form->{attachment_type} if $form->{attachment_type};
+  my $id = $form->{id};
+  $id = $form->{attachment_id} if $form->{attachment_id} && !$form->{id};
+  return if !$id || !$type;
+
+  SL::File->save(
+    object_id     => $id,
+    object_type   => $type,
+    mime_type     => 'application/pdf',
+    source        => 'created',
+    file_type     => 'document',
+    file_name     => $form->{attachment_filename},
+    file_path     => $form->{tmpfile},
+    print_variant => $form->{formname},
+  );
+}
+
+# This method also needed by $form to append all general pdf attachments
+#
+sub append_general_pdf_attachments {
+  my ($self, %params) = @_;
+  return 0 unless $::instance_conf->get_doc_storage;
+  return 0 if !$params{filepath} || !$params{type};
+
+  my @files = SL::File->get_all(
+    object_id   => 0,
+    object_type => $params{type},
+    mime_type   => 'application/pdf'
+  );
+  return 0 if $#files < 0;
+
+  my @pdf_file_names = ($params{filepath});
+  foreach my $file (@files) {
+    my $path = $file->get_file;
+    push @pdf_file_names, $path if $path;
+  }
+
+  #TODO immer noch das alte Problem:
+  #je nachdem von woher der Aufruf kommt ist man in ./users oder .
+  my $savedir = POSIX::getcwd();
+  chdir("$self->{cwd}");
+  $self->merge_pdfs(
+    file_names => \@pdf_file_names,
+    out_path   => $params{filepath}
+  );
+  chdir("$savedir");
+
+  return 0;
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Helper::File - Helper for $::Form to store generated PDF-Documents
+
+=head1 SYNOPSIS
+
+# This Helper is used by SL::Form to store new generated PDF-Files and append general attachments to this documents.
+#
+# in SL::Form.pm:
+
+ $self->store_pdf($self);
+
+ $self->append_general_pdf_attachments(filepath => $pdf_filename, type => $form->{type}) if ( $ext_for_format eq 'pdf' );
+
+#It is also used in MassPrint Helper
+#
+
+=head1 DESCRIPTION
+
+The files with file_type "generated" are stored.
+
+See also L<SL::File>.
+
+=head1 METHODS
+
+
+=head2 C<store_pdf>
+
+Copy generated PDF-File to File destination.
+This method is need from SL::Form after LaTeX-PDF Generation
+
+=over 4
+
+=item C<form.id>
+
+ID of ERP-Document
+
+=item C<form.type>
+
+type of ERP-document
+
+=item C<form.formname>
+
+if no type is set this is used as type
+
+=item C<form.attachment_id>
+
+if no id is set this is used as id
+
+=item C<form.tmpfile>
+
+The path of the generated PDF-file
+
+=item C<form.attachment_filename>
+
+The generated filename which is used as new filename (without timestamp)
+
+=back
+
+=head2 C<append_general_pdf_attachments PARAMS>
+
+This method also needed by SL::Form to append all general pdf attachments
+
+needed C<PARAMS>:
+
+=over 4
+
+=item C<type>
+
+type of ERP-document
+
+=item C<outname>
+
+Name of file to which the general attachments must be added
+
+=back
+
+=head1 AUTHOR
+
+Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
+
+
+=cut
diff --git a/SL/Helper/GlAttachments.pm b/SL/Helper/GlAttachments.pm
new file mode 100644 (file)
index 0000000..aee71a4
--- /dev/null
@@ -0,0 +1,101 @@
+#
+# Helper for General Ledger Reports
+#
+# 1. Fetch the Count of PDF-Documents of one item of a General Ledger Report
+# 2. Append the contents of all items of a General Ledger Report
+
+package SL::Helper::GlAttachments;
+
+use strict;
+
+use Exporter 'import';
+our @EXPORT_OK = qw(count_gl_attachments append_gl_pdf_attachments);
+our %EXPORT_TAGS = (
+  all => \@EXPORT_OK,
+);
+use SL::File;
+
+my %gl_types = (
+  'ar' => 'invoice',
+  'ap' => 'purchase_invoice',
+  'gl' => 'gl_transaction',
+);
+
+#
+# Fetch the Count of PDF-Documents with are related to the $id parameter
+# The parameter $gltype may be 'ar','ap' or 'gl'.
+#
+sub count_gl_pdf_attachments {
+  my ($self,$id,$gltype) = @_;
+  return SL::File->get_all_count(object_id   => $id,
+                                 object_type => $gl_types{$gltype},
+                                 mime_type   => 'application/pdf',
+                                 );
+}
+
+# Append the contents of all PDF-Documents to the base $content
+# This Method is only used in SL/Reportgenerator.pm if the $form->{GD} array is set.
+# The elements of the array need the two elements $ref->{type},$ref->{id}
+#
+sub append_gl_pdf_attachments {
+  my ($self,$form,$content) = @_;
+  my @filelist;
+  foreach my $ref (@{ $form->{GL} }) {
+    my @files = SL::File->get_all(object_id   => $ref->{id},
+                                  object_type => $gl_types{$ref->{type}},
+                                  mime_type   => 'application/pdf',
+                                 );
+    push @filelist, $_->get_file for @files;
+  }
+  return $self->merge_pdfs(file_names => \@filelist , inp_content => $content );
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Helper::GlAttachments - Helper for General Ledger Reports
+
+=head1 SYNOPSIS
+
+   $self->count_gl_pdf_attachments($ref->{id},$ref->{type});
+   $self->append_gl_pdf_attachments($form,$base_content);
+
+
+=head1 DESCRIPTION
+
+Helper for General Ledger Reports
+
+1. Fetch the Count of PDF-Documents of one item of a General Ledger Report
+
+2. Append the contents of all items of a General Ledger Report
+
+
+=head1 METHODS
+
+=head2 C<count_gl_pdf_attachments>
+
+count_gl_pdf_attachments($id,$type);
+
+Fetch the Count of PDF-Documents with are related to the $id parameter
+The parameter $type may be 'ar','ap' or 'gl'.
+
+=head2 C<append_gl_pdf_attachments>
+
+append_gl_pdf_attachments($form,$content);
+
+Append the contents of all PDF-Documents to the base $content
+This Method is only used in SL/Reportgenerator.pm if the $form->{GD} array is set.
+The elements of the array need the two elements $ref->{type},$ref->{id}
+
+=head1 AUTHOR
+
+Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
+
+
+=cut
+
diff --git a/SL/Helper/ISO3166.pm b/SL/Helper/ISO3166.pm
new file mode 100644 (file)
index 0000000..4a7b245
--- /dev/null
@@ -0,0 +1,278 @@
+package SL::Helper::ISO3166;
+
+use strict;
+use warnings;
+use utf8;
+
+use Exporter qw(import);
+our @EXPORT_OK = qw(map_name_to_alpha_2_code);
+
+use List::Util qw(first);
+
+my @alpha_2_mappings = (
+  [ 'AD', qr{^(?:AD|Andorra)$}i ],
+  [ 'AE', qr{^(?:AE|United Arab Emirates)$}i ],
+  [ 'AF', qr{^(?:AF|Afghanistan)$}i ],
+  [ 'AG', qr{^(?:AG|Antigua and Barbuda)$}i ],
+  [ 'AI', qr{^(?:AI|Anguilla)$}i ],
+  [ 'AL', qr{^(?:AL|Albania)$}i ],
+  [ 'AM', qr{^(?:AM|Armenia)$}i ],
+  [ 'AO', qr{^(?:AO|Angola)$}i ],
+  [ 'AQ', qr{^(?:AQ|Antarctica)$}i ],
+  [ 'AR', qr{^(?:AR|Argentina)$}i ],
+  [ 'AS', qr{^(?:AS|American Samoa)$}i ],
+  [ 'AT', qr{^(?:AT|A|Austria|Österreich)$}i ],
+  [ 'AU', qr{^(?:AU|Australia)$}i ],
+  [ 'AW', qr{^(?:AW|Aruba)$}i ],
+  [ 'AX', qr{^(?:AX|Åland Islands)$}i ],
+  [ 'AZ', qr{^(?:AZ|Azerbaijan)$}i ],
+  [ 'BA', qr{^(?:BA|Bosnia and Herzegovina)$}i ],
+  [ 'BB', qr{^(?:BB|Barbados)$}i ],
+  [ 'BD', qr{^(?:BD|Bangladesh)$}i ],
+  [ 'BE', qr{^(?:BE|Belgium|Belgien)$}i ],
+  [ 'BF', qr{^(?:BF|Burkina Faso)$}i ],
+  [ 'BG', qr{^(?:BG|Bulgaria)$}i ],
+  [ 'BH', qr{^(?:BH|Bahrain)$}i ],
+  [ 'BI', qr{^(?:BI|Burundi)$}i ],
+  [ 'BJ', qr{^(?:BJ|Benin)$}i ],
+  [ 'BL', qr{^(?:BL|Saint Barthélemy)$}i ],
+  [ 'BM', qr{^(?:BM|Bermuda)$}i ],
+  [ 'BN', qr{^(?:BN|Brunei Darussalam)$}i ],
+  [ 'BO', qr{^(?:BO|Bolivia \(Plurinational State of\))$}i ],
+  [ 'BQ', qr{^(?:BQ|Bonaire, Sint Eustatius and Saba)$}i ],
+  [ 'BR', qr{^(?:BR|Brazil)$}i ],
+  [ 'BS', qr{^(?:BS|Bahamas)$}i ],
+  [ 'BT', qr{^(?:BT|Bhutan)$}i ],
+  [ 'BV', qr{^(?:BV|Bouvet Island)$}i ],
+  [ 'BW', qr{^(?:BW|Botswana)$}i ],
+  [ 'BY', qr{^(?:BY|Belarus)$}i ],
+  [ 'BZ', qr{^(?:BZ|Belize)$}i ],
+  [ 'CA', qr{^(?:CA|Canada)$}i ],
+  [ 'CC', qr{^(?:CC|Cocos \(Keeling\) Islands|Cocos Islands|Keeling Islands)$}i ],
+  [ 'CD', qr{^(?:CD|Congo, Democratic Republic of the)$}i ],
+  [ 'CF', qr{^(?:CF|Central African Republic)$}i ],
+  [ 'CG', qr{^(?:CG|Congo)$}i ],
+  [ 'CH', qr{^(?:CH|Switzerland|Schweiz)$}i ],
+  [ 'CI', qr{^(?:CI|Côte d'Ivoire)$}i ],
+  [ 'CK', qr{^(?:CK|Cook Islands)$}i ],
+  [ 'CL', qr{^(?:CL|Chile)$}i ],
+  [ 'CM', qr{^(?:CM|Cameroon)$}i ],
+  [ 'CN', qr{^(?:CN|China)$}i ],
+  [ 'CO', qr{^(?:CO|Colombia)$}i ],
+  [ 'CR', qr{^(?:CR|Costa Rica)$}i ],
+  [ 'CU', qr{^(?:CU|Cuba)$}i ],
+  [ 'CV', qr{^(?:CV|Cabo Verde)$}i ],
+  [ 'CW', qr{^(?:CW|Curaçao)$}i ],
+  [ 'CX', qr{^(?:CX|Christmas Island)$}i ],
+  [ 'CY', qr{^(?:CY|Cyprus)$}i ],
+  [ 'CZ', qr{^(?:CZ|Czechia)$}i ],
+  [ 'DE', qr{^(?:DE|Germany|D|Deutschland)$}i ],
+  [ 'DJ', qr{^(?:DJ|Djibouti)$}i ],
+  [ 'DK', qr{^(?:DK|Denmark)$}i ],
+  [ 'DM', qr{^(?:DM|Dominica)$}i ],
+  [ 'DO', qr{^(?:DO|Dominican Republic)$}i ],
+  [ 'DZ', qr{^(?:DZ|Algeria)$}i ],
+  [ 'EC', qr{^(?:EC|Ecuador)$}i ],
+  [ 'EE', qr{^(?:EE|Estonia)$}i ],
+  [ 'EG', qr{^(?:EG|Egypt)$}i ],
+  [ 'EH', qr{^(?:EH|Western Sahara)$}i ],
+  [ 'ER', qr{^(?:ER|Eritrea)$}i ],
+  [ 'ES', qr{^(?:ES|Spain|Spanien)$}i ],
+  [ 'ET', qr{^(?:ET|Ethiopia)$}i ],
+  [ 'FI', qr{^(?:FI|Finland)$}i ],
+  [ 'FJ', qr{^(?:FJ|Fiji)$}i ],
+  [ 'FK', qr{^(?:FK|Falkland Islands \(Malvinas\)|Falkland Islands|Falklands)$}i ],
+  [ 'FM', qr{^(?:FM|Micronesia \(Federated States of\)|Micronesia)$}i ],
+  [ 'FO', qr{^(?:FO|Faroe Islands)$}i ],
+  [ 'FR', qr{^(?:FR|France)$}i ],
+  [ 'GA', qr{^(?:GA|Gabon)$}i ],
+  [ 'GB', qr{^(?:GB|United Kingdom of Great Britain and Northern Ireland)$}i ],
+  [ 'GD', qr{^(?:GD|Grenada)$}i ],
+  [ 'GE', qr{^(?:GE|Georgia)$}i ],
+  [ 'GF', qr{^(?:GF|French Guiana)$}i ],
+  [ 'GG', qr{^(?:GG|Guernsey)$}i ],
+  [ 'GH', qr{^(?:GH|Ghana)$}i ],
+  [ 'GI', qr{^(?:GI|Gibraltar)$}i ],
+  [ 'GL', qr{^(?:GL|Greenland)$}i ],
+  [ 'GM', qr{^(?:GM|Gambia)$}i ],
+  [ 'GN', qr{^(?:GN|Guinea)$}i ],
+  [ 'GP', qr{^(?:GP|Guadeloupe)$}i ],
+  [ 'GQ', qr{^(?:GQ|Equatorial Guinea)$}i ],
+  [ 'GR', qr{^(?:GR|Greece)$}i ],
+  [ 'GS', qr{^(?:GS|South Georgia and the South Sandwich Islands)$}i ],
+  [ 'GT', qr{^(?:GT|Guatemala)$}i ],
+  [ 'GU', qr{^(?:GU|Guam)$}i ],
+  [ 'GW', qr{^(?:GW|Guinea-Bissau)$}i ],
+  [ 'GY', qr{^(?:GY|Guyana)$}i ],
+  [ 'HK', qr{^(?:HK|Hong Kong)$}i ],
+  [ 'HM', qr{^(?:HM|Heard Island and McDonald Islands)$}i ],
+  [ 'HN', qr{^(?:HN|Honduras)$}i ],
+  [ 'HR', qr{^(?:HR|Croatia)$}i ],
+  [ 'HT', qr{^(?:HT|Haiti)$}i ],
+  [ 'HU', qr{^(?:HU|Hungary)$}i ],
+  [ 'ID', qr{^(?:ID|Indonesia)$}i ],
+  [ 'IE', qr{^(?:IE|Ireland)$}i ],
+  [ 'IL', qr{^(?:IL|Israel)$}i ],
+  [ 'IM', qr{^(?:IM|Isle of Man)$}i ],
+  [ 'IN', qr{^(?:IN|India)$}i ],
+  [ 'IO', qr{^(?:IO|British Indian Ocean Territory)$}i ],
+  [ 'IQ', qr{^(?:IQ|Iraq)$}i ],
+  [ 'IR', qr{^(?:IR|Iran \(Islamic Republic of\)|Iran)$}i ],
+  [ 'IS', qr{^(?:IS|Iceland)$}i ],
+  [ 'IT', qr{^(?:IT|Italy|Italien)$}i ],
+  [ 'JE', qr{^(?:JE|Jersey)$}i ],
+  [ 'JM', qr{^(?:JM|Jamaica)$}i ],
+  [ 'JO', qr{^(?:JO|Jordan)$}i ],
+  [ 'JP', qr{^(?:JP|Japan)$}i ],
+  [ 'KE', qr{^(?:KE|Kenya)$}i ],
+  [ 'KG', qr{^(?:KG|Kyrgyzstan)$}i ],
+  [ 'KH', qr{^(?:KH|Cambodia)$}i ],
+  [ 'KI', qr{^(?:KI|Kiribati)$}i ],
+  [ 'KM', qr{^(?:KM|Comoros)$}i ],
+  [ 'KN', qr{^(?:KN|Saint Kitts and Nevis)$}i ],
+  [ 'KP', qr{^(?:KP|Korea \(Democratic People's Republic of\))$}i ],
+  [ 'KR', qr{^(?:KR|Korea, Republic of|Republic of Korea|Korea)$}i ],
+  [ 'KW', qr{^(?:KW|Kuwait)$}i ],
+  [ 'KY', qr{^(?:KY|Cayman Islands)$}i ],
+  [ 'KZ', qr{^(?:KZ|Kazakhstan)$}i ],
+  [ 'LA', qr{^(?:LA|Lao People's Democratic Republic)$}i ],
+  [ 'LB', qr{^(?:LB|Lebanon)$}i ],
+  [ 'LC', qr{^(?:LC|Saint Lucia)$}i ],
+  [ 'LI', qr{^(?:LI|Liechtenstein)$}i ],
+  [ 'LK', qr{^(?:LK|Sri Lanka)$}i ],
+  [ 'LR', qr{^(?:LR|Liberia)$}i ],
+  [ 'LS', qr{^(?:LS|Lesotho)$}i ],
+  [ 'LT', qr{^(?:LT|Lithuania)$}i ],
+  [ 'LU', qr{^(?:LU|Luxembourg|Luxemburg)$}i ],
+  [ 'LV', qr{^(?:LV|Latvia)$}i ],
+  [ 'LY', qr{^(?:LY|Libya)$}i ],
+  [ 'MA', qr{^(?:MA|Morocco)$}i ],
+  [ 'MC', qr{^(?:MC|Monaco)$}i ],
+  [ 'MD', qr{^(?:MD|Moldova, Republic of)$}i ],
+  [ 'ME', qr{^(?:ME|Montenegro)$}i ],
+  [ 'MF', qr{^(?:MF|Saint Martin \(French part\)|Saint Martin)$}i ],
+  [ 'MG', qr{^(?:MG|Madagascar)$}i ],
+  [ 'MH', qr{^(?:MH|Marshall Islands)$}i ],
+  [ 'MK', qr{^(?:MK|North Macedonia)$}i ],
+  [ 'ML', qr{^(?:ML|Mali)$}i ],
+  [ 'MM', qr{^(?:MM|Myanmar)$}i ],
+  [ 'MN', qr{^(?:MN|Mongolia)$}i ],
+  [ 'MO', qr{^(?:MO|Macao)$}i ],
+  [ 'MP', qr{^(?:MP|Northern Mariana Islands)$}i ],
+  [ 'MQ', qr{^(?:MQ|Martinique)$}i ],
+  [ 'MR', qr{^(?:MR|Mauritania)$}i ],
+  [ 'MS', qr{^(?:MS|Montserrat)$}i ],
+  [ 'MT', qr{^(?:MT|Malta)$}i ],
+  [ 'MU', qr{^(?:MU|Mauritius)$}i ],
+  [ 'MV', qr{^(?:MV|Maldives)$}i ],
+  [ 'MW', qr{^(?:MW|Malawi)$}i ],
+  [ 'MX', qr{^(?:MX|Mexico)$}i ],
+  [ 'MY', qr{^(?:MY|Malaysia)$}i ],
+  [ 'MZ', qr{^(?:MZ|Mozambique)$}i ],
+  [ 'NA', qr{^(?:NA|Namibia)$}i ],
+  [ 'NC', qr{^(?:NC|New Caledonia)$}i ],
+  [ 'NE', qr{^(?:NE|Niger)$}i ],
+  [ 'NF', qr{^(?:NF|Norfolk Island)$}i ],
+  [ 'NG', qr{^(?:NG|Nigeria)$}i ],
+  [ 'NI', qr{^(?:NI|Nicaragua)$}i ],
+  [ 'NL', qr{^(?:NL|Netherlands|Niederlande)$}i ],
+  [ 'NO', qr{^(?:NO|Norway)$}i ],
+  [ 'NP', qr{^(?:NP|Nepal)$}i ],
+  [ 'NR', qr{^(?:NR|Nauru)$}i ],
+  [ 'NU', qr{^(?:NU|Niue)$}i ],
+  [ 'NZ', qr{^(?:NZ|New Zealand)$}i ],
+  [ 'OM', qr{^(?:OM|Oman)$}i ],
+  [ 'PA', qr{^(?:PA|Panama)$}i ],
+  [ 'PE', qr{^(?:PE|Peru)$}i ],
+  [ 'PF', qr{^(?:PF|French Polynesia)$}i ],
+  [ 'PG', qr{^(?:PG|Papua New Guinea)$}i ],
+  [ 'PH', qr{^(?:PH|Philippines)$}i ],
+  [ 'PK', qr{^(?:PK|Pakistan)$}i ],
+  [ 'PL', qr{^(?:PL|Poland)$}i ],
+  [ 'PM', qr{^(?:PM|Saint Pierre and Miquelon)$}i ],
+  [ 'PN', qr{^(?:PN|Pitcairn)$}i ],
+  [ 'PR', qr{^(?:PR|Puerto Rico)$}i ],
+  [ 'PS', qr{^(?:PS|Palestine, State of)$}i ],
+  [ 'PT', qr{^(?:PT|Portugal)$}i ],
+  [ 'PW', qr{^(?:PW|Palau)$}i ],
+  [ 'PY', qr{^(?:PY|Paraguay)$}i ],
+  [ 'QA', qr{^(?:QA|Qatar)$}i ],
+  [ 'RE', qr{^(?:RE|Réunion)$}i ],
+  [ 'RO', qr{^(?:RO|Romania)$}i ],
+  [ 'RS', qr{^(?:RS|Serbia)$}i ],
+  [ 'RU', qr{^(?:RU|Russian Federation)$}i ],
+  [ 'RW', qr{^(?:RW|Rwanda)$}i ],
+  [ 'SA', qr{^(?:SA|Saudi Arabia)$}i ],
+  [ 'SB', qr{^(?:SB|Solomon Islands)$}i ],
+  [ 'SC', qr{^(?:SC|Seychelles)$}i ],
+  [ 'SD', qr{^(?:SD|Sudan)$}i ],
+  [ 'SE', qr{^(?:SE|Sweden)$}i ],
+  [ 'SG', qr{^(?:SG|Singapore)$}i ],
+  [ 'SH', qr{^(?:SH|Saint Helena, Ascension and Tristan da Cunha)$}i ],
+  [ 'SI', qr{^(?:SI|Slovenia)$}i ],
+  [ 'SJ', qr{^(?:SJ|Svalbard and Jan Mayen)$}i ],
+  [ 'SK', qr{^(?:SK|Slovakia)$}i ],
+  [ 'SL', qr{^(?:SL|Sierra Leone)$}i ],
+  [ 'SM', qr{^(?:SM|San Marino)$}i ],
+  [ 'SN', qr{^(?:SN|Senegal)$}i ],
+  [ 'SO', qr{^(?:SO|Somalia)$}i ],
+  [ 'SR', qr{^(?:SR|Suriname)$}i ],
+  [ 'SS', qr{^(?:SS|South Sudan)$}i ],
+  [ 'ST', qr{^(?:ST|Sao Tome and Principe)$}i ],
+  [ 'SV', qr{^(?:SV|El Salvador)$}i ],
+  [ 'SX', qr{^(?:SX|Sint Maarten \(Dutch part\)|Sint Maarten)$}i ],
+  [ 'SY', qr{^(?:SY|Syrian Arab Republic)$}i ],
+  [ 'SZ', qr{^(?:SZ|Eswatini)$}i ],
+  [ 'TC', qr{^(?:TC|Turks and Caicos Islands)$}i ],
+  [ 'TD', qr{^(?:TD|Chad)$}i ],
+  [ 'TF', qr{^(?:TF|French Southern Territories)$}i ],
+  [ 'TG', qr{^(?:TG|Togo)$}i ],
+  [ 'TH', qr{^(?:TH|Thailand)$}i ],
+  [ 'TJ', qr{^(?:TJ|Tajikistan)$}i ],
+  [ 'TK', qr{^(?:TK|Tokelau)$}i ],
+  [ 'TL', qr{^(?:TL|Timor-Leste)$}i ],
+  [ 'TM', qr{^(?:TM|Turkmenistan)$}i ],
+  [ 'TN', qr{^(?:TN|Tunisia)$}i ],
+  [ 'TO', qr{^(?:TO|Tonga)$}i ],
+  [ 'TR', qr{^(?:TR|Turkey)$}i ],
+  [ 'TT', qr{^(?:TT|Trinidad and Tobago)$}i ],
+  [ 'TV', qr{^(?:TV|Tuvalu)$}i ],
+  [ 'TW', qr{^(?:TW|Taiwan, Province of China)$}i ],
+  [ 'TZ', qr{^(?:TZ|Tanzania, United Republic of)$}i ],
+  [ 'UA', qr{^(?:UA|Ukraine)$}i ],
+  [ 'UG', qr{^(?:UG|Uganda)$}i ],
+  [ 'UM', qr{^(?:UM|United States Minor Outlying Islands)$}i ],
+  [ 'US', qr{^(?:US|United States of America)$}i ],
+  [ 'UY', qr{^(?:UY|Uruguay)$}i ],
+  [ 'UZ', qr{^(?:UZ|Uzbekistan)$}i ],
+  [ 'VA', qr{^(?:VA|Holy See)$}i ],
+  [ 'VC', qr{^(?:VC|Saint Vincent and the Grenadines)$}i ],
+  [ 'VE', qr{^(?:VE|Venezuela \(Bolivian Republic of\)|Venezuela)$}i ],
+  [ 'VG', qr{^(?:VG|Virgin Islands \(British\))$}i ],
+  [ 'VI', qr{^(?:VI|Virgin Islands \(U\.?S\.?\))$}i ],
+  [ 'VN', qr{^(?:VN|Viet Nam)$}i ],
+  [ 'VU', qr{^(?:VU|Vanuatu)$}i ],
+  [ 'WF', qr{^(?:WF|Wallis and Futuna)$}i ],
+  [ 'WS', qr{^(?:WS|Samoa)$}i ],
+  [ 'YE', qr{^(?:YE|Yemen)$}i ],
+  [ 'YT', qr{^(?:YT|Mayotte)$}i ],
+  [ 'ZA', qr{^(?:ZA|South Africa)$}i ],
+  [ 'ZM', qr{^(?:ZM|Zambia)$}i ],
+  [ 'ZW', qr{^(?:ZW|Zimbabwe)$}i ],
+);
+
+sub map_name_to_alpha_2_code {
+  my ($country) = @_;
+
+  return undef if ($country // '') eq '';
+
+  my $code = first { $country =~ $_->[1] } @alpha_2_mappings;
+  return $code->[0] if $code;
+
+  no warnings 'once';
+  $::lxdebug->message(LXDebug::WARN(), "ISO3166::map_name_to_alpha_2_code: no mapping found for '$country'");
+
+  return undef;
+}
+
+1;
diff --git a/SL/Helper/ISO4217.pm b/SL/Helper/ISO4217.pm
new file mode 100644 (file)
index 0000000..89d996b
--- /dev/null
@@ -0,0 +1,211 @@
+package SL::Helper::ISO4217;
+
+use strict;
+use warnings;
+use utf8;
+
+use Exporter qw(import);
+our @EXPORT_OK = qw(map_currency_name_to_code);
+
+use List::Util qw(first);
+
+my @currency_name_to_code_mappings = (
+  [ 'AED', qr{^(?:UAE Dirham|AED)$}i ],
+  [ 'AFN', qr{^(?:Afghani|AFN)$}i ],
+  [ 'ALL', qr{^(?:Lek|ALL)$}i ],
+  [ 'AMD', qr{^(?:Armenian Dram|AMD)$}i ],
+  [ 'ANG', qr{^(?:Netherlands Antillean Guilder|ANG)$}i ],
+  [ 'AOA', qr{^(?:Kwanza|AOA)$}i ],
+  [ 'ARS', qr{^(?:Argentine Peso|ARS)$}i ],
+  [ 'AUD', qr{^(?:Australian Dollar|AUD)$}i ],
+  [ 'AWG', qr{^(?:Aruban Florin|AWG)$}i ],
+  [ 'AZN', qr{^(?:Azerbaijan Manat|AZN)$}i ],
+  [ 'BAM', qr{^(?:Convertible Mark|BAM)$}i ],
+  [ 'BBD', qr{^(?:Barbados Dollar|BBD)$}i ],
+  [ 'BDT', qr{^(?:Taka|BDT)$}i ],
+  [ 'BGN', qr{^(?:Bulgarian Lev|BGN)$}i ],
+  [ 'BHD', qr{^(?:Bahraini Dinar|BHD)$}i ],
+  [ 'BIF', qr{^(?:Burundi Franc|BIF)$}i ],
+  [ 'BMD', qr{^(?:Bermudian Dollar|BMD)$}i ],
+  [ 'BND', qr{^(?:Brunei Dollar|BND)$}i ],
+  [ 'BOB', qr{^(?:Boliviano|BOB)$}i ],
+  [ 'BOV', qr{^(?:US Dollar|BOV)$}i ],
+  [ 'BRL', qr{^(?:Brazilian Real|BRL)$}i ],
+  [ 'BSD', qr{^(?:Bahamian Dollar|BSD)$}i ],
+  [ 'BTN', qr{^(?:Ngultrum|BTN)$}i ],
+  [ 'BWP', qr{^(?:Pula|BWP)$}i ],
+  [ 'BYN', qr{^(?:Belarusian Ruble|BYN)$}i ],
+  [ 'BZD', qr{^(?:Belize Dollar|BZD)$}i ],
+  [ 'CAD', qr{^(?:Canadian Dollar|CAD)$}i ],
+  [ 'CDF', qr{^(?:Congolese Franc|CDF)$}i ],
+  [ 'CHE', qr{^(?:Syrian Pound|CHE)$}i ],
+  [ 'CHF', qr{^(?:Swiss Franc|CHF)$}i ],
+  [ 'CHW', qr{^(?:Syrian Pound|CHW)$}i ],
+  [ 'CLF', qr{^(?:Yuan Renminbi|CLF)$}i ],
+  [ 'CLP', qr{^(?:Chilean Peso|CLP)$}i ],
+  [ 'CNY', qr{^(?:Yuan Renminbi|CNY)$}i ],
+  [ 'COP', qr{^(?:Colombian Peso|COP)$}i ],
+  [ 'COU', qr{^(?:Comorian Franc |COU)$}i ],
+  [ 'CRC', qr{^(?:Costa Rican Colon|CRC)$}i ],
+  [ 'CUC', qr{^(?:Peso Convertible|CUC)$}i ],
+  [ 'CUP', qr{^(?:Cuban Peso|CUP)$}i ],
+  [ 'CVE', qr{^(?:Cabo Verde Escudo|CVE)$}i ],
+  [ 'CZK', qr{^(?:Czech Koruna|CZK)$}i ],
+  [ 'DJF', qr{^(?:Djibouti Franc|DJF)$}i ],
+  [ 'DKK', qr{^(?:Danish Krone|DKK)$}i ],
+  [ 'DOP', qr{^(?:Dominican Peso|DOP)$}i ],
+  [ 'DZD', qr{^(?:Algerian Dinar|DZD)$}i ],
+  [ 'EGP', qr{^(?:Egyptian Pound|EGP)$}i ],
+  [ 'ERN', qr{^(?:Nakfa|ERN)$}i ],
+  [ 'ETB', qr{^(?:Ethiopian Birr|ETB)$}i ],
+  [ 'EUR', qr{^(?:Euro|EUR|€)$}i ],
+  [ 'FJD', qr{^(?:Fiji Dollar|FJD)$}i ],
+  [ 'FKP', qr{^(?:Falkland Islands Pound|FKP)$}i ],
+  [ 'GBP', qr{^(?:Pound Sterling|GBP)$}i ],
+  [ 'GEL', qr{^(?:Lari|GEL)$}i ],
+  [ 'GHS', qr{^(?:Ghana Cedi|GHS)$}i ],
+  [ 'GIP', qr{^(?:Gibraltar Pound|GIP)$}i ],
+  [ 'GMD', qr{^(?:Dalasi|GMD)$}i ],
+  [ 'GNF', qr{^(?:Guinean Franc|GNF)$}i ],
+  [ 'GTQ', qr{^(?:Quetzal|GTQ)$}i ],
+  [ 'GYD', qr{^(?:Guyana Dollar|GYD)$}i ],
+  [ 'HKD', qr{^(?:Hong Kong Dollar|HKD)$}i ],
+  [ 'HNL', qr{^(?:Lempira|HNL)$}i ],
+  [ 'HRK', qr{^(?:Kuna|HRK)$}i ],
+  [ 'HTG', qr{^(?:Gourde|HTG)$}i ],
+  [ 'HUF', qr{^(?:Forint|HUF)$}i ],
+  [ 'IDR', qr{^(?:Rupiah|IDR)$}i ],
+  [ 'ILS', qr{^(?:New Israeli Sheqel|ILS)$}i ],
+  [ 'INR', qr{^(?:Indian Rupee|INR)$}i ],
+  [ 'IQD', qr{^(?:Iraqi Dinar|IQD)$}i ],
+  [ 'IRR', qr{^(?:Iranian Rial|IRR)$}i ],
+  [ 'ISK', qr{^(?:Iceland Krona|ISK)$}i ],
+  [ 'JMD', qr{^(?:Jamaican Dollar|JMD)$}i ],
+  [ 'JOD', qr{^(?:Jordanian Dinar|JOD)$}i ],
+  [ 'JPY', qr{^(?:Yen|JPY|¥)$}i ],
+  [ 'KES', qr{^(?:Kenyan Shilling|KES)$}i ],
+  [ 'KGS', qr{^(?:Som|KGS)$}i ],
+  [ 'KHR', qr{^(?:Riel|KHR)$}i ],
+  [ 'KMF', qr{^(?:Comorian Franc |KMF)$}i ],
+  [ 'KPW', qr{^(?:North Korean Won|KPW)$}i ],
+  [ 'KRW', qr{^(?:Won|KRW)$}i ],
+  [ 'KWD', qr{^(?:Kuwaiti Dinar|KWD)$}i ],
+  [ 'KYD', qr{^(?:Cayman Islands Dollar|KYD)$}i ],
+  [ 'KZT', qr{^(?:Tenge|KZT)$}i ],
+  [ 'LAK', qr{^(?:Lao Kip|LAK)$}i ],
+  [ 'LBP', qr{^(?:Lebanese Pound|LBP)$}i ],
+  [ 'LKR', qr{^(?:Sri Lanka Rupee|LKR)$}i ],
+  [ 'LRD', qr{^(?:Liberian Dollar|LRD)$}i ],
+  [ 'LSL', qr{^(?:Loti|LSL)$}i ],
+  [ 'LYD', qr{^(?:Libyan Dinar|LYD)$}i ],
+  [ 'MAD', qr{^(?:Moroccan Dirham|MAD)$}i ],
+  [ 'MDL', qr{^(?:Moldovan Leu|MDL)$}i ],
+  [ 'MGA', qr{^(?:Malagasy Ariary|MGA)$}i ],
+  [ 'MKD', qr{^(?:Denar|MKD)$}i ],
+  [ 'MMK', qr{^(?:Kyat|MMK)$}i ],
+  [ 'MNT', qr{^(?:Tugrik|MNT)$}i ],
+  [ 'MOP', qr{^(?:Pataca|MOP)$}i ],
+  [ 'MRU', qr{^(?:Ouguiya|MRU)$}i ],
+  [ 'MUR', qr{^(?:Mauritius Rupee|MUR)$}i ],
+  [ 'MVR', qr{^(?:Rufiyaa|MVR)$}i ],
+  [ 'MWK', qr{^(?:Malawi Kwacha|MWK)$}i ],
+  [ 'MXN', qr{^(?:Mexican Peso|MXN)$}i ],
+  [ 'MXV', qr{^(?:US Dollar|MXV)$}i ],
+  [ 'MYR', qr{^(?:Malaysian Ringgit|MYR)$}i ],
+  [ 'MZN', qr{^(?:Mozambique Metical|MZN)$}i ],
+  [ 'NAD', qr{^(?:Namibia Dollar|NAD)$}i ],
+  [ 'NGN', qr{^(?:Naira|NGN)$}i ],
+  [ 'NIO', qr{^(?:Cordoba Oro|NIO)$}i ],
+  [ 'NOK', qr{^(?:Norwegian Krone|NOK)$}i ],
+  [ 'NPR', qr{^(?:Nepalese Rupee|NPR)$}i ],
+  [ 'NZD', qr{^(?:New Zealand Dollar|NZD)$}i ],
+  [ 'OMR', qr{^(?:Rial Omani|OMR)$}i ],
+  [ 'PAB', qr{^(?:Balboa|PAB)$}i ],
+  [ 'PAB', qr{^(?:No universal currency|PAB)$}i ],
+  [ 'PEN', qr{^(?:Sol|PEN)$}i ],
+  [ 'PGK', qr{^(?:Kina|PGK)$}i ],
+  [ 'PHP', qr{^(?:Philippine Peso|PHP)$}i ],
+  [ 'PKR', qr{^(?:Pakistan Rupee|PKR)$}i ],
+  [ 'PLN', qr{^(?:Zloty|PLN)$}i ],
+  [ 'PYG', qr{^(?:Guarani|PYG)$}i ],
+  [ 'QAR', qr{^(?:Qatari Rial|QAR)$}i ],
+  [ 'RON', qr{^(?:Romanian Leu|RON)$}i ],
+  [ 'RSD', qr{^(?:Serbian Dinar|RSD)$}i ],
+  [ 'RUB', qr{^(?:Russian Ruble|RUB)$}i ],
+  [ 'RWF', qr{^(?:Rwanda Franc|RWF)$}i ],
+  [ 'SAR', qr{^(?:Saudi Riyal|SAR)$}i ],
+  [ 'SBD', qr{^(?:Solomon Islands Dollar|SBD)$}i ],
+  [ 'SCR', qr{^(?:Seychelles Rupee|SCR)$}i ],
+  [ 'SDG', qr{^(?:Sudanese Pound|SDG)$}i ],
+  [ 'SEK', qr{^(?:Swedish Krona|SEK)$}i ],
+  [ 'SGD', qr{^(?:Singapore Dollar|SGD)$}i ],
+  [ 'SHP', qr{^(?:Saint Helena Pound|SHP)$}i ],
+  [ 'SLL', qr{^(?:Leone|SLL)$}i ],
+  [ 'SOS', qr{^(?:Somali Shilling|SOS)$}i ],
+  [ 'SRD', qr{^(?:Surinam Dollar|SRD)$}i ],
+  [ 'SSP', qr{^(?:No universal currency|SSP)$}i ],
+  [ 'SSP', qr{^(?:South Sudanese Pound|SSP)$}i ],
+  [ 'STN', qr{^(?:Dobra|STN)$}i ],
+  [ 'SVC', qr{^(?:El Salvador Colon|SVC)$}i ],
+  [ 'SYP', qr{^(?:Syrian Pound|SYP)$}i ],
+  [ 'SZL', qr{^(?:Lilangeni|SZL)$}i ],
+  [ 'THB', qr{^(?:Baht|THB)$}i ],
+  [ 'TJS', qr{^(?:Somoni|TJS)$}i ],
+  [ 'TMT', qr{^(?:Turkmenistan New Manat|TMT)$}i ],
+  [ 'TND', qr{^(?:Tunisian Dinar|TND)$}i ],
+  [ 'TOP', qr{^(?:Pa’anga|TOP)$}i ],
+  [ 'TRY', qr{^(?:Turkish Lira|TRY)$}i ],
+  [ 'TTD', qr{^(?:Trinidad and Tobago Dollar|TTD)$}i ],
+  [ 'TWD', qr{^(?:New Taiwan Dollar|TWD)$}i ],
+  [ 'TZS', qr{^(?:Tanzanian Shilling|TZS)$}i ],
+  [ 'UAH', qr{^(?:Hryvnia|UAH)$}i ],
+  [ 'UGX', qr{^(?:Uganda Shilling|UGX)$}i ],
+  [ 'USD', qr{^(?:US Dollar|USD|\$)$}i ],
+  [ 'USN', qr{^(?:Peso Uruguayo|USN)$}i ],
+  [ 'UYI', qr{^(?:Unidad Previsional|UYI)$}i ],
+  [ 'UYU', qr{^(?:Peso Uruguayo|UYU)$}i ],
+  [ 'UYW', qr{^(?:Unidad Previsional|UYW)$}i ],
+  [ 'UZS', qr{^(?:Uzbekistan Sum|UZS)$}i ],
+  [ 'VES', qr{^(?:Bolívar Soberano|VES)$}i ],
+  [ 'VND', qr{^(?:Dong|VND)$}i ],
+  [ 'VUV', qr{^(?:Vatu|VUV)$}i ],
+  [ 'WST', qr{^(?:Tala|WST)$}i ],
+  [ 'XAF', qr{^(?:CFA Franc BEAC|XAF)$}i ],
+  [ 'XAG', qr{^(?:Silver|XAG)$}i ],
+  [ 'XAU', qr{^(?:Gold|XAU)$}i ],
+  [ 'XBA', qr{^(?:Bond Markets Unit European Composite Unit \(EURCO\)|XBA)$}i ],
+  [ 'XBB', qr{^(?:Bond Markets Unit European Monetary Unit \(E\.?M\.?U\.?-6\)|XBB)$}i ],
+  [ 'XBC', qr{^(?:Bond Markets Unit European Unit of Account 9 \(E\.?U\.?A\.?-9\)|XBC)$}i ],
+  [ 'XBD', qr{^(?:Bond Markets Unit European Unit of Account 17 \(E\.?U\.?A\.?-17\)|XBD)$}i ],
+  [ 'XCD', qr{^(?:East Caribbean Dollar|XCD)$}i ],
+  [ 'XCD', qr{^(?:No universal currency|XCD)$}i ],
+  [ 'XDR', qr{^(?:SDR \(Special Drawing Right\)|SDR|XDR)$}i ],
+  [ 'XOF', qr{^(?:CFA Franc BCEAO|XOF)$}i ],
+  [ 'XPD', qr{^(?:Palladium|XPD)$}i ],
+  [ 'XPF', qr{^(?:CFP Franc|XPF)$}i ],
+  [ 'XPT', qr{^(?:Platinum|XPT)$}i ],
+  [ 'XSU', qr{^(?:Sucre|XSU)$}i ],
+  [ 'XTS', qr{^(?:Codes specifically reserved for testing purposes|XTS)$}i ],
+  [ 'XUA', qr{^(?:ADB Unit of Account|XUA)$}i ],
+  [ 'XXX', qr{^(?:The codes assigned for transactions where no currency is involved|XXX)$}i ],
+  [ 'YER', qr{^(?:Yemeni Rial|YER)$}i ],
+  [ 'ZAR', qr{^(?:Rand|ZAR)$}i ],
+  [ 'ZMW', qr{^(?:Zambian Kwacha|ZMW)$}i ],
+  [ 'ZWL', qr{^(?:Zimbabwe Dollar|ZWL)$}i ],
+);
+
+sub map_currency_name_to_code {
+  my ($currency) = @_;
+
+  return undef if ($currency // '') eq '';
+
+  my $code = first { $currency =~ $_->[1] } @currency_name_to_code_mappings;
+  return $code->[0] if $code;
+
+  no warnings 'once';
+  $::lxdebug->message(LXDebug::WARN(), "ISO4217::map_currency_name_to_code: no mapping found for '$currency'");
+
+  return undef;
+}
+
+1;
diff --git a/SL/Helper/Inventory.pm b/SL/Helper/Inventory.pm
new file mode 100644 (file)
index 0000000..944a410
--- /dev/null
@@ -0,0 +1,796 @@
+package SL::Helper::Inventory;
+
+use strict;
+use Carp;
+use DateTime;
+use Exporter qw(import);
+use List::Util qw(min sum);
+use List::UtilsBy qw(sort_by);
+use List::MoreUtils qw(any);
+use POSIX qw(ceil);
+
+use SL::Locale::String qw(t8);
+use SL::MoreCommon qw(listify);
+use SL::DBUtils qw(selectall_hashref_query selectrow_query);
+use SL::DB::TransferType;
+use SL::Helper::Number qw(_format_number _round_number);
+use SL::Helper::Inventory::Allocation;
+use SL::X;
+
+our @EXPORT_OK = qw(get_stock get_onhand allocate allocate_for_assembly produce_assembly check_constraints);
+our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
+
+sub _get_stock_onhand {
+  my (%params) = @_;
+
+  my $onhand_mode = !!$params{onhand};
+
+  my @selects = (
+    'SUM(qty) AS qty',
+    'MIN(EXTRACT(epoch FROM inventory.itime)) AS itime',
+  );
+  my @values;
+  my @where;
+  my @groups;
+
+  if ($params{part}) {
+    my @ids = map { ref $_ ? $_->id : $_ } listify($params{part});
+    push @where, sprintf "parts_id IN (%s)", join ', ', ("?") x @ids;
+    push @values, @ids;
+  }
+
+  if ($params{bin}) {
+    my @ids = map { ref $_ ? $_->id : $_ } listify($params{bin});
+    push @where, sprintf "bin_id IN (%s)", join ', ', ("?") x @ids;
+    push @values, @ids;
+  }
+
+  if ($params{warehouse}) {
+    my @ids = map { ref $_ ? $_->id : $_ } listify($params{warehouse});
+    push @where, sprintf "warehouse.id IN (%s)", join ', ', ("?") x @ids;
+    push @values, @ids;
+  }
+
+  if ($params{chargenumber}) {
+    my @ids = listify($params{chargenumber});
+    push @where, sprintf "chargenumber IN (%s)", join ', ', ("?") x @ids;
+    push @values, @ids;
+  }
+
+  if ($params{date}) {
+    Carp::croak("not DateTime ".$params{date}) unless ref($params{date}) eq 'DateTime';
+    push @where, sprintf "shippingdate <= ?";
+    push @values, $params{date};
+  }
+
+  if (!$params{bestbefore} && $onhand_mode && default_show_bestbefore()) {
+    $params{bestbefore} = DateTime->now_local;
+  }
+
+  if ($params{bestbefore}) {
+    Carp::croak("not DateTime ".$params{date}) unless ref($params{bestbefore}) eq 'DateTime';
+    push @where, sprintf "(bestbefore IS NULL OR bestbefore >= ?)";
+    push @values, $params{bestbefore};
+  }
+
+  # by
+  my %allowed_by = (
+    part          => [ qw(parts_id) ],
+    bin           => [ qw(bin_id inventory.warehouse_id)],
+    warehouse     => [ qw(inventory.warehouse_id) ],
+    chargenumber  => [ qw(chargenumber) ],
+    bestbefore    => [ qw(bestbefore) ],
+    for_allocate  => [ qw(parts_id bin_id inventory.warehouse_id chargenumber bestbefore) ],
+  );
+
+  if ($params{by}) {
+    for (listify($params{by})) {
+      my $selects = $allowed_by{$_} or Carp::croak("unknown option for by: $_");
+      push @selects, @$selects;
+      push @groups,  @$selects;
+    }
+  }
+
+  my $select   = join ',', @selects;
+  my $where    = @where  ? 'WHERE ' . join ' AND ', @where : '';
+  my $group_by = @groups ? 'GROUP BY ' . join ', ', @groups : '';
+
+  my $query = <<"";
+    SELECT $select FROM inventory
+    LEFT JOIN bin ON bin_id = bin.id
+    LEFT JOIN warehouse ON bin.warehouse_id = warehouse.id
+    $where
+    $group_by
+
+  if ($onhand_mode) {
+    $query .= ' HAVING SUM(qty) > 0';
+  }
+
+  my $results = selectall_hashref_query($::form, SL::DB->client->dbh, $query, @values);
+
+  my %with_objects = (
+    part         => 'SL::DB::Manager::Part',
+    bin          => 'SL::DB::Manager::Bin',
+    warehouse    => 'SL::DB::Manager::Warehouse',
+  );
+
+  my %slots = (
+    part      =>  'parts_id',
+    bin       =>  'bin_id',
+    warehouse =>  'warehouse_id',
+  );
+
+  if ($params{by} && $params{with_objects}) {
+    for my $with_object (listify($params{with_objects})) {
+      Carp::croak("unknown with_object $with_object") if !exists $with_objects{$with_object};
+
+      my $manager = $with_objects{$with_object};
+      my $slot = $slots{$with_object};
+      next if !(my @ids = map { $_->{$slot} } @$results);
+      my $objects = $manager->get_all(query => [ id => \@ids ]);
+      my %objects_by_id = map { $_->id => $_ } @$objects;
+
+      $_->{$with_object} = $objects_by_id{$_->{$slot}} for @$results;
+    }
+  }
+
+  if ($params{by}) {
+    return $results;
+  } else {
+    return $results->[0]{qty};
+  }
+}
+
+sub get_stock {
+  _get_stock_onhand(@_, onhand => 0);
+}
+
+sub get_onhand {
+  _get_stock_onhand(@_, onhand => 1);
+}
+
+sub allocate {
+  my (%params) = @_;
+
+  croak('allocate needs a part') unless $params{part};
+  croak('allocate needs a qty')  unless $params{qty};
+
+  my $part = $params{part};
+  my $qty  = $params{qty};
+
+  return () if $qty <= 0;
+
+  my $results = get_stock(part => $part, by => 'for_allocate');
+  my %bin_whitelist = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{bin});
+  my %wh_whitelist  = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{warehouse});
+  my %chargenumbers = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{chargenumber});
+
+  # sort results so that chargenumbers are matched first, then wanted bins, then wanted warehouses
+  my @sorted_results = sort {
+       exists $chargenumbers{$b->{chargenumber}}  <=> exists $chargenumbers{$a->{chargenumber}} # then prefer wanted chargenumbers
+    || exists $bin_whitelist{$b->{bin_id}}        <=> exists $bin_whitelist{$a->{bin_id}}       # then prefer wanted bins
+    || exists $wh_whitelist{$b->{warehouse_id}}   <=> exists $wh_whitelist{$a->{warehouse_id}}  # then prefer wanted bins
+    || $a->{itime}                                <=> $b->{itime}                               # and finally prefer earlier charges
+  } @$results;
+  my @allocations;
+  my $rest_qty = $qty;
+
+  for my $chunk (@sorted_results) {
+    my $qty = min($chunk->{qty}, $rest_qty);
+
+    # since allocate operates on stock, this also ensures that no negative stock results are used
+    if ($qty > 0) {
+      push @allocations, SL::Helper::Inventory::Allocation->new(
+        parts_id          => $chunk->{parts_id},
+        qty               => $qty,
+        comment           => $params{comment},
+        bin_id            => $chunk->{bin_id},
+        warehouse_id      => $chunk->{warehouse_id},
+        chargenumber      => $chunk->{chargenumber},
+        bestbefore        => $chunk->{bestbefore},
+        for_object_id     => undef,
+      );
+      $rest_qty -=  _round_number($qty, 5);
+    }
+    $rest_qty = _round_number($rest_qty, 5);
+    last if $rest_qty == 0;
+  }
+  if ($rest_qty > 0) {
+    die SL::X::Inventory::Allocation->new(
+      code    => 'not enough to allocate',
+      message => t8("can not allocate #1 units of #2, missing #3 units", _format_number($qty), $part->displayable_name, _format_number($rest_qty)),
+    );
+  } else {
+    if ($params{constraints}) {
+      check_constraints($params{constraints},\@allocations);
+    }
+    return @allocations;
+  }
+}
+
+sub allocate_for_assembly {
+  my (%params) = @_;
+
+  my $part = $params{part} or Carp::croak('allocate needs a part');
+  my $qty  = $params{qty}  or Carp::croak('allocate needs a qty');
+  my $wh   = $params{warehouse};
+  my $wh_strict       = $::instance_conf->get_produce_assembly_same_warehouse;
+  my $consume_service = $::instance_conf->get_produce_assembly_transfer_service;
+
+  Carp::croak('not an assembly')       unless $part->is_assembly;
+  Carp::croak('No warehouse selected') if $wh_strict && !$wh;
+
+  my %parts_to_allocate;
+
+  for my $assembly ($part->assemblies) {
+    next if $assembly->part->type eq 'service' && !$consume_service;
+    $parts_to_allocate{ $assembly->part->id } //= 0;
+    $parts_to_allocate{ $assembly->part->id } += $assembly->qty * $qty;
+  }
+
+  my @allocations;
+
+  for my $part_id (keys %parts_to_allocate) {
+    my $part = SL::DB::Part->load_cached($part_id);
+    push @allocations, allocate(%params, part => $part, qty => $parts_to_allocate{$part_id});
+    if ($wh_strict) {
+      die SL::X::Inventory::Allocation->new(
+        code    => "wrong warehouse for part",
+        message => t8('Part #1 exists in warehouse #2, but not in warehouse #3 ',
+                        $part->partnumber . ' ' . $part->description,
+                        SL::DB::Manager::Warehouse->find_by(id => $allocations[-1]->{warehouse_id})->description,
+                        $wh->description),
+      ) unless $allocations[-1]->{warehouse_id} == $wh->id;
+    }
+  }
+
+  @allocations;
+}
+
+sub check_constraints {
+  my ($constraints, $allocations) = @_;
+  if ('CODE' eq ref $constraints) {
+    if (!$constraints->(@$allocations)) {
+      die SL::X::Inventory::Allocation->new(
+        code    => 'allocation constraints failure',
+        message => t8("Allocations didn't pass constraints"),
+      );
+    }
+  } else {
+    croak 'constraints needs to be a hashref' unless 'HASH' eq ref $constraints;
+
+    my %supported_constraints = (
+      bin_id       => 'bin_id',
+      warehouse_id => 'warehouse_id',
+      chargenumber => 'chargenumber',
+    );
+
+    for (keys %$constraints ) {
+      croak "unsupported constraint '$_'" unless $supported_constraints{$_};
+      next unless defined $constraints->{$_};
+
+      my %whitelist = map { (ref $_ ? $_->id : $_) => 1 } listify($constraints->{$_});
+      my $accessor = $supported_constraints{$_};
+
+      if (any { !$whitelist{$_->$accessor} } @$allocations) {
+        my %error_constraints = (
+          bin_id         => t8('Bins'),
+          warehouse_id   => t8('Warehouses'),
+          chargenumber   => t8('Chargenumbers'),
+        );
+        my @allocs = grep { $whitelist{$_->$accessor} } @$allocations;
+        my $needed = sum map { $_->qty } grep { !$whitelist{$_->$accessor} } @$allocations;
+        my $err    = t8("Cannot allocate parts.");
+        $err      .= ' '.t8('part \'#\'1 in bin \'#2\' only with qty #3 (need additional #4) and chargenumber \'#5\'.',
+              SL::DB::Part->load_cached($_->parts_id)->description,
+              SL::DB::Bin->load_cached($_->bin_id)->full_description,
+              _format_number($_->qty), _format_number($needed), $_->chargenumber ? $_->chargenumber : '--') for @allocs;
+        die SL::X::Inventory::Allocation->new(
+          code    => 'allocation constraints failure',
+          message => $err,
+        );
+      }
+    }
+  }
+}
+
+sub produce_assembly {
+  my (%params) = @_;
+
+  my $part = $params{part} or Carp::croak('produce_assembly needs a part');
+  my $qty  = $params{qty}  or Carp::croak('produce_assembly needs a qty');
+  my $bin  = $params{bin}  or Carp::croak("need target bin");
+
+  my $allocations = $params{allocations};
+  my $strict_wh = $::instance_conf->get_produce_assembly_same_warehouse ? $bin->warehouse : undef;
+  if ($params{auto_allocate}) {
+    Carp::croak("produce_assembly: can't have both allocations and auto_allocate") if $params{allocations};
+    $allocations = [ allocate_for_assembly(part => $part, qty => $qty, warehouse => $strict_wh, chargenumber => $params{chargenumber}) ];
+  } else {
+    Carp::croak("produce_assembly: need allocations or auto_allocate to produce something") if !$params{allocations};
+    $allocations = $params{allocations};
+  }
+
+  my $chargenumber  = $params{chargenumber};
+  my $bestbefore    = $params{bestbefore};
+  my $for_object_id = $params{for_object_id};
+  my $comment       = $params{comment} // '';
+  my $invoice       = $params{invoice};
+  my $project       = $params{project};
+  my $shippingdate  = $params{shippingsdate} // DateTime->now_local;
+  my $trans_id      = $params{trans_id};
+
+  ($trans_id) = selectrow_query($::form, SL::DB->client->dbh, qq|SELECT nextval('id')| ) unless $trans_id;
+
+  my $trans_type_out = SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'used');
+  my $trans_type_in  = SL::DB::Manager::TransferType->find_by(direction => 'in',  description => 'assembled');
+
+  # check whether allocations are sane
+  if (!$params{no_check_allocations} && !$params{auto_allocate}) {
+    my %allocations_by_part = map { $_->parts_id  => $_->qty } @$allocations;
+    for my $assembly ($part->assemblies) {
+      $allocations_by_part{ $assembly->parts_id } -= $assembly->qty * $qty;
+    }
+
+    die SL::X::Inventory::Allocation->new(
+      code    => "allocations are insufficient for production",
+      message => t8('can not allocate enough resources for production'),
+    ) if any { $_ < 0 } values %allocations_by_part;
+  }
+
+  my @transfers;
+  for my $allocation (@$allocations) {
+    my $oe_id = delete $allocation->{for_object_id};
+    push @transfers, $allocation->transfer_object(
+      trans_id     => $trans_id,
+      qty          => -$allocation->qty,
+      trans_type   => $trans_type_out,
+      shippingdate => $shippingdate,
+      employee     => SL::DB::Manager::Employee->current,
+      comment      => t8('Used for assembly #1 #2', $part->partnumber, $part->description),
+    );
+  }
+
+  push @transfers, SL::DB::Inventory->new(
+    trans_id          => $trans_id,
+    trans_type        => $trans_type_in,
+    part              => $part,
+    qty               => $qty,
+    bin               => $bin,
+    warehouse         => $bin->warehouse_id,
+    chargenumber      => $chargenumber,
+    bestbefore        => $bestbefore,
+    shippingdate      => $shippingdate,
+    project           => $project,
+    invoice           => $invoice,
+    comment           => $comment,
+    employee          => SL::DB::Manager::Employee->current,
+    oe_id             => $for_object_id,
+  );
+
+  SL::DB->client->with_transaction(sub {
+    $_->save for @transfers;
+    1;
+  }) or do {
+    die SL::DB->client->error;
+  };
+
+  @transfers;
+}
+
+sub default_show_bestbefore {
+  $::instance_conf->get_show_bestbefore
+}
+
+1;
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::WH - Warehouse and Inventory API
+
+=head1 SYNOPSIS
+
+  # See description for an intro to the concepts used here.
+
+  use SL::Helper::Inventory qw(:ALL);
+
+  # stock, get "what's there" for a part with various conditions:
+  my $qty = get_stock(part => $part);                              # how much is on stock?
+  my $qty = get_stock(part => $part, date => $date);               # how much was on stock at a specific time?
+  my $qty = get_stock(part => $part, bin => $bin);                 # how much is on stock in a specific bin?
+  my $qty = get_stock(part => $part, warehouse => $warehouse);     # how much is on stock in a specific warehouse?
+  my $qty = get_stock(part => $part, chargenumber => $chargenumber); # how much is on stock of a specific chargenumber?
+
+  # onhand, get "what's available" for a part with various conditions:
+  my $qty = get_onhand(part => $part);                              # how much is available?
+  my $qty = get_onhand(part => $part, date => $date);               # how much was available at a specific time?
+  my $qty = get_onhand(part => $part, bin => $bin);                 # how much is available in a specific bin?
+  my $qty = get_onhand(part => $part, warehouse => $warehouse);     # how much is available in a specific warehouse?
+  my $qty = get_onhand(part => $part, chargenumber => $chargenumber); # how much is availbale of a specific chargenumber?
+
+  # onhand batch mode:
+  my $data = get_onhand(
+    warehouse    => $warehouse,
+    by           => [ qw(bin part chargenumber) ],
+    with_objects => [ qw(bin part) ],
+  );
+
+  # allocate:
+  my @allocations = allocate(
+    part         => $part,          # part_id works too
+    qty          => $qty,           # must be positive
+    chargenumber => $chargenumber,  # optional, may be arrayref. if provided these charges will be used first
+    bestbefore   => $datetime,      # optional, defaults to today. items with bestbefore prior to that date wont be used
+    bin          => $bin,           # optional, may be arrayref. if provided
+  );
+
+  # shortcut to allocate all that is needed for producing an assembly, will use chargenumbers as appropriate
+  my @allocations = allocate_for_assembly(
+    part         => $assembly,      # part_id works too
+    qty          => $qty,           # must be positive
+  );
+
+  # create allocation manually, bypassing checks. all of these need to be passed, even undefs
+  my $allocation = SL::Helper::Inventory::Allocation->new(
+    part_id           => $part->id,
+    qty               => 15,
+    bin_id            => $bin_obj->id,
+    warehouse_id      => $bin_obj->warehouse_id,
+    chargenumber      => '1823772365',
+    bestbefore        => undef,
+    for_object_id     => $order->id,
+  );
+
+  # produce_assembly:
+  produce_assembly(
+    part         => $part,           # target assembly
+    qty          => $qty,            # qty
+    allocations  => \@allocations,   # allocations to use. alternatively use "auto_allocate => 1,"
+
+    # where to put it
+    bin          => $bin,           # needed unless a global standard target is configured
+    chargenumber => $chargenumber,  # optional
+    bestbefore   => $datetime,      # optional
+    comment      => $comment,       # optional
+  );
+
+=head1 DESCRIPTION
+
+New functions for the warehouse and inventory api.
+
+The WH api currently has three large shortcomings: It is very hard to just get
+the current stock for an item, it's extremely complicated to use it to produce
+assemblies while ensuring that no stock ends up negative, and it's very hard to
+use it to get an overview over the actual contents of the inventory.
+
+The first problem has spawned several dozen small functions in the program that
+try to implement that, and those usually miss some details. They may ignore
+bestbefore times, comments, ignore negative quantities etc.
+
+To get this cleaned up a bit this code introduces two concepts: stock and onhand.
+
+=over 4
+
+=item * Stock is defined as the actual contents of the inventory, everything that is
+there.
+
+=item * Onhand is what is available, which means things that are stocked,
+not expired and not in any other way reserved for other uses.
+
+=back
+
+The two new functions C<get_stock> and C<get_onhand> encapsulate these principles and
+allow simple access with some optional filters for chargenumbers or warehouses.
+Both of them have a batch mode that can be used to get these information to
+supplement simple reports.
+
+To address the safe assembly creation a new function has been added.
+C<allocate> will try to find the requested quantity of a part in the inventory
+and will return allocations of it which can then be used to create the
+assembly. Allocation will happen with the C<onhand> semantics defined above,
+meaning that by default no expired goods will be used. The caller can supply
+hints of what shold be used and in those cases chargenumbers will be used up as
+much as possible first. C<allocate> will always try to fulfil the request even
+beyond those. Should the required amount not be stocked, allocate will throw an
+exception.
+
+C<produce_assembly> has been rewritten to only accept parameters about the
+target of the production, and requires allocations to complete the request. The
+allocations can be supplied manually, or can be generated automatically.
+C<produce_assembly> will check whether enough allocations are given to create
+the assembly, but will not check whether the allocations are backed. If the
+allocations are not sufficient or if the auto-allocation fails an exception
+is returned. If you need to produce something that is not in the inventory, you
+can bypass those checks by creating the allocations yourself (see
+L</"ALLOCATION DATA STRUCTURE">).
+
+Note: this is only intended to cover the scenarios described above. For other cases:
+
+=over 4
+
+=item *
+
+If you need actual inventory objects because of record links or something like
+that load them directly. And strongly consider redesigning that, because it's
+really fragile.
+
+=item *
+
+You need weight or accounting information you're on your own. The inventory api
+only concerns itself with the raw quantities.
+
+=item *
+
+If you need the first stock date of parts, or anything related to a specific
+transfer type or direction, this is not covered yet.
+
+=back
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item * get_stock PARAMS
+
+Returns for single parts how much actually exists in the inventory.
+
+Options:
+
+=over 4
+
+=item * part
+
+The part. Must be present without C<by>. May be arrayref with C<by>. Can be object or id.
+
+=item * bin
+
+If given, will only return stock on these bins. Optional. May be array, May be object or id.
+
+=item * warehouse
+
+If given, will only return stock on these warehouses. Optional. May be array, May be object or id.
+
+=item * date
+
+If given, will return stock as it were on this timestamp. Optional. Must be L<DateTime> object.
+
+=item * chargenumber
+
+If given, will only show stock with this chargenumber. Optional. May be array.
+
+=item * by
+
+See L</"STOCK/ONHAND REPORT MODE">
+
+=item * with_objects
+
+See L</"STOCK/ONHAND REPORT MODE">
+
+=back
+
+Will return a single qty normally, see L</"STOCK/ONHAND REPORT MODE"> for batch
+mode when C<by> is given.
+
+=item * get_onhand PARAMS
+
+Returns for single parts how much is available in the inventory. That excludes
+stock with expired bestbefore.
+
+It takes the same options as L</get_stock>.
+
+=over 4
+
+=item * bestbefore
+
+If given, will only return stock with a bestbefore at or after the given date.
+Optional. Must be L<DateTime> object.
+
+=back
+
+=item * allocate PARAMS
+
+Accepted parameters:
+
+=over 4
+
+=item * part
+
+=item * qty
+
+=item * bin
+
+Bin object. Optional.
+
+=item * warehouse
+
+Warehouse object. Optional.
+
+=item * chargenumber
+
+Optional.
+
+=item * bestbefore
+
+Datetime. Optional.
+
+=back
+
+Tries to allocate the required quantity using what is currently onhand. If
+given any of C<bin>, C<warehouse>, C<chargenumber>
+
+=item * allocate_for_assembly PARAMS
+
+Shortcut to allocate everything for an assembly. Takes the same arguments. Will
+compute the required amount for each assembly part and allocate all of them.
+
+=item * produce_assembly
+
+
+=back
+
+=head1 STOCK/ONHAND REPORT MODE
+
+If the special option C<by> is given with an arrayref, the result will instead
+be an arrayref of partitioned stocks by those fields. Valid partitions are:
+
+=over 4
+
+=item * part
+
+If this is given, part is optional in the parameters
+
+=item * bin
+
+=item * warehouse
+
+=item * chargenumber
+
+=item * bestbefore
+
+=back
+
+Note: If you want to use the returned data to create allocations you I<need> to
+enable all of these. To make this easier a special shortcut exists
+
+In this mode, C<with_objects> can be used to load C<warehouse>, C<bin>,
+C<parts>  objects in one go, just like with Rose. They
+need to be present in C<by> before that though.
+
+=head1 ALLOCATION ALGORITHM
+
+When calling allocate, the current onhand (== available stock) of the item will
+be used to decide which bins/chargenumbers/bestbefore can be used.
+
+In general allocate will try to make the request happen, and will use the
+provided charges up first, and then tap everything else. If you need to only
+I<exactly> use the provided charges, you'll need to craft the allocations
+yourself. See L</"ALLOCATION DATA STRUCTURE"> for that.
+
+If C<chargenumber> is given, those will be used up next.
+
+After that normal quantities will be used.
+
+These are tiebreakers and expected to rarely matter in reality. If you need
+finegrained control over which allocation is used, you may want to get the
+onhands yourself and select the appropriate ones.
+
+Only quantities with C<bestbefore> unset or after the given date will be
+considered. If more than one charge is eligible, the earlier C<bestbefore>
+will be used.
+
+Allocations do NOT have an internal memory and can't react to other allocations
+of the same part earlier. Never double allocate the same part within a
+transaction.
+
+=head1 ALLOCATION DATA STRUCTURE
+
+Allocations are instances of the helper class C<SL::Helper::Inventory::Allocation>. They require
+each of the following attributes to be set at creation time:
+
+=over 4
+
+=item * parts_id
+
+=item * qty
+
+=item * bin_id
+
+=item * warehouse_id
+
+=item * chargenumber
+
+=item * bestbefore
+
+=item * for_object_id
+
+If set the allocations will be marked as allocated for the given object.
+If these allocations are later used to produce an assembly, the resulting
+consuming transactions will be marked as belonging to the given object.
+The object may be an order, productionorder or other objects
+
+=back
+
+C<chargenumber>, C<bestbefore> and C<for_object_id> and C<comment> may be
+C<undef> (but must still be present at creation time). Instances are considered
+immutable.
+
+Allocations also provide the method C<transfer_object> which will create a new
+C<SL::DB::Inventory> bject with all the playload.
+
+=head1 CONSTRAINTS
+
+  # whitelist constraints
+  ->allocate(
+    ...
+    constraints => {
+      bin_id       => \@allowed_bins,
+      chargenumber => \@allowed_chargenumbers,
+    }
+  );
+
+  # custom constraints
+  ->allocate(
+    constraints => sub {
+      # only allow chargenumbers with specific format
+      all { $_->chargenumber =~ /^ C \d{8} - \a{d2} $/x } @_
+
+      &&
+      # and must all have a bestbefore date
+      all { $_->bestbefore } @_;
+    }
+  )
+
+C<allocation> is "best effort" in nature. It will take the C<bin>,
+C<chargenumber> etc hints from the parameters, but will try it's bvest to
+fulfil the request anyway and only bail out if it is absolutely not possible.
+
+Sometimes you need to restrict allocations though. For this you can pass
+additional constraints to C<allocate>. A constraint serves as a whitelist.
+Every allocation must fulfil every constraint by having that attribute be one
+of the given values.
+
+In case even that is not enough, you may supply a custom check by passing a
+function that will be given the allocation objects.
+
+Note that both whitelists and constraints do not influence the order of
+allocations, which is done purely from the initial parameters. They only serve
+to reject allocations made in good faith which do fulfil required assertions.
+
+=head1 ERROR HANDLING
+
+C<allocate> and C<produce_assembly> will throw exceptions if the request can
+not be completed. The usual reason will be insufficient onhand to allocate, or
+insufficient allocations to process the request.
+
+=head1 KNOWN PROBLEMS
+
+  * It's not currently possible to identify allocations between requests, for
+    example for presenting the user possible allocations and then actually using
+    them on the next request.
+  * It's not currently possible to give C<allocate> prior constraints.
+    Currently all constraints are treated as hints (and will be preferred) but
+    the internal ordering of the hints is fixed and more complex preferentials
+    are not supported.
+  * bestbefore handling is untested
+  * interaction with config option "transfer_default_ignore_onhand" is
+    currently undefined (and implicitly ignores it)
+
+=head1 TODO
+
+  * define and describe error classes
+  * define wrapper classes for stock/onhand batch mode return values
+  * handle extra arguments in produce: shippingdate, project
+  * document no_ check
+  * tests
+
+=head1 BUGS
+
+None yet :)
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>sven.schoeling@googlemail.comE<gt>
+
+=cut
diff --git a/SL/Helper/Inventory/Allocation.pm b/SL/Helper/Inventory/Allocation.pm
new file mode 100644 (file)
index 0000000..e406bf4
--- /dev/null
@@ -0,0 +1,65 @@
+package SL::Helper::Inventory::Allocation;
+
+use strict;
+
+my @attributes = qw(parts_id qty bin_id warehouse_id chargenumber bestbefore comment for_object_id);
+my %attributes = map { $_ => 1 } @attributes;
+my %mapped_attributes = (
+  for_object_id => 'oe_id',
+);
+
+for my $name (@attributes) {
+  no strict 'refs';
+  *{"SL::Helper::Inventory::Allocation::$name"} = sub { $_[0]{$name} };
+}
+
+sub new {
+  my ($class, %params) = @_;
+
+  Carp::croak("missing attribute $_") for grep { !exists $params{$_}     } @attributes;
+  Carp::croak("unknown attribute $_") for grep { !exists $attributes{$_} } keys %params;
+  Carp::croak("$_ must be set")       for grep { !$params{$_} } qw(parts_id qty bin_id);
+  Carp::croak("$_ must be positive")  for grep { !($params{$_} > 0) } qw(parts_id qty bin_id);
+
+  bless { %params }, $class;
+}
+
+sub transfer_object {
+  my ($self, %params) = @_;
+
+  SL::DB::Inventory->new(
+    (map {
+      my $attr = $mapped_attributes{$_} // $_;
+      $attr => $self->{$attr}
+    } @attributes),
+    %params,
+  );
+}
+
+1;
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Helper::Inventory::Allocation - Inventory API allocation data structure
+
+=head1 SYNOPSIS
+
+  # all of these need to be present
+  my $allocation = SL::Helper::Inventory::Allocation->new(
+    part_id           => $part->id,
+    qty               => 15,
+    bin_id            => $bin_obj->id,
+    warehouse_id      => $bin_obj->warehouse_id,
+    chargenumber      => '1823772365',           # can be undef
+    bestbefore        => undef,                  # can be undef
+    for_object_id     => $order->id,             # can be undef
+  );
+
+
+=head1 SEE ALSO
+
+The full documentation can be found in L<SL::Helper::Inventory>
+
+=cut
diff --git a/SL/Helper/MT940.pm b/SL/Helper/MT940.pm
deleted file mode 100644 (file)
index 5c8865d..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-package SL::Helper::MT940;
-
-use strict;
-
-sub convert_mt940_data {
-  my ($mt940_data) = @_;
-
-  # takes the data from an uploaded mt940 file, converts it to csv via aqbanking and returns the converted data
-  # The uploaded file data is stored as a session file, just like the aqbanking settings file.
-
-  my $import_filename = 'bank_transfer.940';
-  my $sfile = SL::SessionFile->new($import_filename, mode => '>');
-  $sfile->fh->print($mt940_data);
-  $sfile->fh->close;
-
-  my $aqbin = $::lx_office_conf{applications}->{aqbanking};
-  die "Can't find aqbanking-cli, please check your configuration file.\n" unless -f $aqbin;
-  my $cmd = "$aqbin --cfgdir=\"" . $sfile->get_path . "\" import --importer=\"swift\" --profile=\"SWIFT-MT940\" -f " . $sfile->get_path . "/$import_filename | $aqbin --cfgdir=\"" . $sfile->get_path . "\" listtrans --exporter=\"csv\" --profile=\"AqMoney2\" ";
-
-  my $converted_data = '"empty";"local_bank_code";"local_account_number";"remote_bank_code";"remote_account_number";"transdate";"valutadate";"amount";"currency";"remote_name";"remote_name_1";"purpose";"purpose1";"purpose2";"purpose3";"purpose4";"purpose5";"purpose6";"purpose7";"purpose8";"purpose9";"purpose10";"purpose11"' . "\n";
-
-  open my $mt, "-|", "$cmd" || die "Problem with executing aqbanking\n";
-  my $headerline = <$mt>;  # discard original aqbanking header line
-  while (<$mt>) {
-    $converted_data .= $_;
-  };
-  close $mt;
-  return $converted_data;
-};
-
-1;
diff --git a/SL/Helper/MassPrintCreatePDF.pm b/SL/Helper/MassPrintCreatePDF.pm
new file mode 100644 (file)
index 0000000..be80a69
--- /dev/null
@@ -0,0 +1,200 @@
+package SL::Helper::MassPrintCreatePDF;
+
+use strict;
+
+use SL::Webdav;
+
+use Exporter 'import';
+our @EXPORT_OK = qw(create_massprint_pdf merge_massprint_pdf create_pdfs print_pdfs);
+our %EXPORT_TAGS = (
+  all => \@EXPORT_OK,
+);
+
+sub create_pdfs {
+  my ($self, %params) = @_;
+  my @pdf_file_names;
+  foreach my $document (@{ $params{documents} }) {
+    $params{document} = $document;
+    push @pdf_file_names, $self->create_massprint_pdf(%params);
+  }
+
+  return @pdf_file_names;
+}
+
+sub create_massprint_pdf {
+  my ($self, %params) = @_;
+  my $form = Form->new('');
+  my %create_params = (
+    variables => $form,
+    record    => $params{document},
+    return    => 'file_name',
+  );
+  ## find_template may return a list !
+  $create_params{template} = $self->find_template(name => $params{variables}->{formname}, printer_id => $params{printer_id});
+  $form->{cwd}= POSIX::getcwd();
+
+  $form->{$_} = $params{variables}->{$_} for keys %{ $params{variables} };
+
+  $create_params{variable_content_types} = $form->get_variable_content_types();
+  $params{document}->flatten_to_form($form, format_amounts => 1);
+  # flatten_to_form sets payment_terms from customer/vendor - we do not want that here
+  # really ??
+  delete $form->{payment_terms} if !$form->{payment_id};
+  for my $i (1 .. $form->{rowcount}) {
+    $form->{"sellprice_$i"} = $form->{"fxsellprice_$i"};
+  }
+
+  $form->prepare_for_printing;
+
+  $form->{language}            = '_' . $form->{language};
+  $form->{attachment_filename} = $form->generate_attachment_filename;
+
+  my $pdf_filename = $self->create_pdf(%create_params);
+
+  if ($::instance_conf->get_webdav_documents && !$form->{preview}) {
+    my $webdav = SL::Webdav->new(
+      type     => $params{document}->type,
+      number   => $params{document}->record_number,
+    );
+    my $webdav_file = SL::Webdav::File->new(
+      webdav   => $webdav,
+      filename => $form->{attachment_filename},
+    );
+    eval {
+      $webdav_file->store(file => $pdf_filename);
+      1;
+    } or do {
+      push @{ $params{errors} }, $@ if exists $params{errors};
+    }
+  }
+
+  if ( $::instance_conf->get_doc_storage && ! $form->{preview}) {
+    $self->append_general_pdf_attachments(filepath => $pdf_filename, type => $form->{type} );
+    $form->{tmpfile} = $pdf_filename;
+    $form->{id}      = $params{document}->id;
+    $self->store_pdf($form);
+  }
+  $form->{id} = $params{document}->id;
+  if ( ! $form->{preview} ) {
+    if ( ref($params{document}) eq 'SL::DB::DeliveryOrder' ) {
+      $form->{snumbers} = "ordnumber_" . $params{document}->donumber;
+    }
+    else {
+      $form->{snumbers} = "unknown";
+    }
+    $form->{addition} = "PRINTED";
+    $form->{what_done} = $::form->{type};
+    $form->save_history;
+  }
+  return $pdf_filename;
+}
+
+sub merge_massprint_pdf {
+  my ($self, %params)     = @_;
+  return unless $params{file_names} && $params{type};
+
+  my $job_obj = $self->{job_obj};
+  my $data    = $job_obj->data_as_hash;
+  my @pdf_file_names = @{$params{file_names}};
+
+  eval {
+    my $file_name = 'mass_'.$params{type}.'_'.$job_obj->id . '.pdf';
+    my $sfile     = SL::SessionFile->new($file_name, mode => 'w', session_id => $data->{session_id});
+    $sfile->fh->close;
+    $data->{pdf_file_name} = $sfile->file_name;
+
+    $self->merge_pdfs(file_names => \@pdf_file_names, bothsided => $data->{bothsided}, out_path => $data->{pdf_file_name});
+    unlink @pdf_file_names;
+
+    1;
+
+  } or do {
+    push @{ $data->{print_errors} }, { message => $@ };
+  };
+
+  $job_obj->update_attributes(data_as_hash => $data);
+}
+
+sub print_pdfs {
+  my ($self)     = @_;
+
+  my $job_obj         = $self->{job_obj};
+  my $data            = $job_obj->data_as_hash;
+  my $printer_id      = $data->{printer_id};
+  my $copy_printer_id = $data->{copy_printer_id};
+
+  return if !$printer_id;
+
+  my $out;
+
+  foreach  my $local_printer_id ($printer_id, $copy_printer_id) {
+    next unless $local_printer_id;
+    SL::DB::Printer
+      ->new(id => $local_printer_id)
+      ->load
+      ->print_document(file_name => $data->{pdf_file_name});
+  }
+
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Helper::MassPrint_CreatePDF
+
+
+=head1 DESCRIPTION
+
+This Helper used bei Background Processing for Mass Printing.
+The redundant way to fill data for createPDF is concentrated into this helper.
+There are some additional settings for printing which are missed in CreatePDF Helper
+and also the appending of generic PDF-Documents.
+
+(This extension may be included in the CreatePDF Helper).
+
+
+=head1 REQUIRES
+
+L<SL::Helper::CreatePDF>
+
+=head1 METHODS
+
+=head2 C<create_massprint_pdf PARAMS>
+
+a tempory $form is used to set
+
+=over 2
+
+=item 1. content types
+
+=item 2. flatten_to_form
+
+=item 3. prepare_for_printing
+
+=item 4. set history
+
+=back
+
+before printing is done
+
+Recognized parameters are (not a complete list):
+
+=over 2
+
+=item * C<errors> – optional. If given, it must be an array ref. This will be
+filled with potential errors.
+
+=back
+
+
+=head1 AUTHOR
+
+Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
+
+
+=cut
diff --git a/SL/Helper/Number.pm b/SL/Helper/Number.pm
new file mode 100644 (file)
index 0000000..39a69fc
--- /dev/null
@@ -0,0 +1,252 @@
+package SL::Helper::Number;
+
+use strict;
+use Exporter qw(import);
+use List::Util qw(max min);
+use List::UtilsBy qw(rev_nsort_by);
+use Config;
+
+our @EXPORT_OK = qw(
+  _format_number _round_number
+  _format_total  _round_total
+  _parse_number
+);
+our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
+
+sub _format_number {
+  my ($amount, $places, %params) = @_;
+  $amount        ||= 0;
+  my $dash         = $params{dash} // '';
+  my $numberformat = $params{numberformat} // $::myconfig{numberformat};
+  my $neg          = $amount < 0;
+  my $force_places = defined $places && $places >= 0;
+
+  $amount = _round_number($amount, abs $places) if $force_places;
+  $neg    = 0 if $amount == 0; # don't show negative zero
+  $amount = sprintf "%.*f", ($force_places ? $places : 10), abs $amount; # 6 is default for %fa
+
+  # before the sprintf amount was a number, afterwards it's a string. because of the dynamic nature of perl
+  # this is easy to confuse, so keep in mind: before this comment no s///, m//, concat or other strong ops on
+  # $amount. after this comment no +,-,*,/,abs. it will only introduce subtle bugs.
+
+  $amount =~ s/0*$// unless defined $places && $places == 0;             # cull trailing 0s
+
+  my @d = reverse $numberformat =~ /(\D)/g;                              # get delim chars
+  my @p = split(/\./, $amount);                                          # split amount at decimal point
+
+  $p[0] =~ s/\B(?=(...)*$)/$d[1]/g if $d[1];                             # add 1,000 delimiters
+  $amount = $p[0];
+  if ($places || $p[1]) {
+    $amount .= $d[0]
+            .  ( $p[1] || '' )
+            .  (0 x max(abs($places || 0) - length ($p[1]||''), 0));     # pad the fraction
+  }
+
+  $amount = do {
+    ($dash =~ /-/)    ? ($neg ? "($amount)"                            : "$amount" )                              :
+    ($dash =~ /DRCR/) ? ($neg ? "$amount " . $main::locale->text('DR') : "$amount " . $main::locale->text('CR') ) :
+                        ($neg ? "-$amount"                             : "$amount" )                              ;
+  };
+
+  $amount;
+}
+
+sub _round_number {
+  my ($amount, $places, $adjust) = @_;
+
+  return 0 if !defined $amount;
+
+  $places //= 0;
+
+  if ($adjust) {
+    no warnings 'once';
+    my $precision = $::instance_conf->get_precision || 0.01;
+    return _round_number( _round_number($amount / $precision, 0) * $precision, $places);
+  }
+
+  # We use Perl's knowledge of string representation for
+  # rounding. First, convert the floating point number to a string
+  # with a high number of places. Then split the string on the decimal
+  # sign and use integer calculation for rounding the decimal places
+  # part. If an overflow occurs then apply that overflow to the part
+  # before the decimal sign as well using integer arithmetic again.
+
+  my $int_amount = int(abs $amount);
+  my $str_places = max(min(10, 16 - length("$int_amount") - $places), $places);
+  my $amount_str = sprintf '%.*f', $places + $str_places, abs($amount);
+
+  return $amount unless $amount_str =~ m{^(\d+)\.(\d+)$};
+
+  my ($pre, $post)      = ($1, $2);
+  my $decimals          = '1' . substr($post, 0, $places);
+
+  my $propagation_limit = $Config{i32size} == 4 ? 7 : 18;
+  my $add_for_rounding  = substr($post, $places, 1) >= 5 ? 1 : 0;
+
+  if ($places > $propagation_limit) {
+    $decimals = Math::BigInt->new($decimals)->badd($add_for_rounding);
+    $pre      = Math::BigInt->new($decimals)->badd(1) if substr($decimals, 0, 1) eq '2';
+
+  } else {
+    $decimals += $add_for_rounding;
+    $pre      += 1 if substr($decimals, 0, 1) eq '2';
+  }
+
+  $amount  = ("${pre}." . substr($decimals, 1)) * ($amount <=> 0);
+
+  return $amount;
+}
+
+sub _parse_number {
+  my ($amount, %params) = @_;
+
+  return 0 if !defined $amount || $amount eq '';
+
+  my $numberformat = $params{numberformat} // $::myconfig{numberformat};
+
+  if (   ($numberformat eq '1.000,00')
+      || ($numberformat eq '1000,00')) {
+    $amount =~ s/\.//g;
+    $amount =~ s/,/\./g;
+  }
+
+  if ($numberformat eq "1'000.00") {
+    $amount =~ s/\'//g;
+  }
+
+  $amount =~ s/,//g;
+
+  # Make sure no code wich is not a math expression ends up in eval().
+  return 0 unless $amount =~ /^ [\s \d \( \) \- \+ \* \/ \. ]* $/x;
+
+  # Prevent numbers from being parsed as octals;
+  $amount =~ s{ (?<! [\d.] ) 0+ (?= [1-9] ) }{}gx;
+
+  return scalar(eval($amount)) * 1 ;
+}
+
+sub _format_total    { _format_number($_[0], 2, @_[1..$#_])  }
+sub _round_total    { _round_number($_[0], 2, @_[1..$#_]) }
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Helper::Number - number formating functions formerly sitting in SL::Form
+
+=head1 SYNOPSIS
+
+  use SL::Helper::Number qw(all);
+
+  my $str       = _format_number($val, 2); # round to 2
+  my $str       = _format_number($val, 2, %::myconfig);                # also works, is implied
+  my $str       = _format_number($val, 2, numberformat => '1.000,00'); # with custom numberformat
+  my $total     = _format_total($val);     # round to 2
+  my $total     = _format_total($val, numberformat => '1.000,00');
+
+  my $val       = _parse_number($str);                             # parse with the current numberformat
+  my $val       = _parse_number($str, numberformat => '1.000,00'); # parse with the current numberformat
+
+  my $str       = _round_number($val, 2);
+  my $total     = _round_total($val);     # rounded to 2
+
+=head1 DESCRIPTION
+
+This package contains all the number parsing/formating functions that were
+previously in SL::Form.
+
+Instead of invoking them as methods on C<$::form> these are pure functions.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item * C<_format_number VALUE PLACES PARAMS>
+
+The old C<SL::Form::format_amount> with a different signature.
+
+The value is expected to be a numeric value, but undef and empty string will be
+vivified to 0 for convinience. Bigints are supported.
+
+For the semantics of places, see L</PLACES>.
+
+If C<params> contains a dash parameter, it will change the formatting of
+positive/negative numbers. If C<-> is given for dash, negative numbers will
+instead be formatted with prentheses. If C<DRCR> is given, the numbers will be
+formatted absolute, but suffixed with the localized versions of C<DR> and
+C<CR>.
+
+=item * _format_total
+
+A curried version used for formatting ledger entries. C<myconfig> is set from the
+current user, C<places> is set to 2. C<dash> is left empty.
+
+=item * _parse_number VALUE PARAMS
+
+Parses expressions into numbers. C<PARAMS> may contain C<numberformat> just
+like with C<L/_format_amount>.
+
+Also implements basic arithmetic interpretation, so that C<2 * 1400> is
+interpreted as 2800.
+
+=item * _round_number VALUE PLACES
+
+Rounds a number. Due to the way Perl handles floating point we take a lot of
+precautions that rounding ends up being close to where we want. Usually the
+internal floats have more than enough precision to not have any floating point
+issues, but the cumulative error can interfere with proper formatting later.
+
+For places, see L</PLACES>
+
+=item * _round_total
+
+A curried version used for rounding ledger entries. C<places> is set to 2.
+
+=back
+
+=head1 PLACES
+
+Places can be:
+
+=over 4
+
+=item * not present
+
+In that case a representation is chosen that looks sufficiently human. For
+example C<1/10> equals C<.1000000000000000555> but will be displayed as the
+localized version of 0.1.
+
+=item * 0
+
+The number will be rounded to the nearest integer (towards 0).
+
+=item * a positive integer
+
+The number will be rounded to this many places. Formatting functions will then
+make sure to pad the output to this many places.
+
+=item * a negative inteher
+
+The number will not be rounded, but padded to at least this many places.
+
+=back
+
+=head1 ERROR REPORTING
+
+All of these do not thow exceptions and will simply return undef should
+something unforeseen happen.
+
+=head1 BUGS AND CAVEATS
+
+Beware that the old C<amount> is now called plain C<number>. C<amount> is
+deliberately unused in the new version for that reason.
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut
diff --git a/SL/Helper/Object.pm b/SL/Helper/Object.pm
new file mode 100644 (file)
index 0000000..922dafb
--- /dev/null
@@ -0,0 +1,195 @@
+package SL::Helper::Object;
+
+use strict;
+
+sub import {
+  my ($class, @args) = @_;
+
+  my $caller = (caller)[0];
+
+  while (@args > 1) {
+    my $method = shift @args;
+    my $args   = shift @args;
+    die "invalid method '$method' for $class" unless $class->can($method);
+    $class->$method($caller, $args);
+  }
+}
+
+my %args_string_by_key = (
+  none     => '',
+  raw      => '(@_)',
+  standard => '(@_[1..$#_])',
+);
+
+my %pre_context_by_key = (
+  void   => '',
+  scalar => 'my $return =',
+  list   => 'my @return =',
+);
+
+my %post_context_by_key = (
+  void   => 'return',
+  scalar => '$return',
+  list   => '@return',
+);
+
+my %known_delegate_args = map { $_ => 1 } qw(target_method args force_context class_function);
+
+my $_ident  = '^[a-zA-Z0-9_]+$';
+my $_cident = '^[a-zA-Z0-9_:]+$';
+
+sub delegate {
+  my ($class, $caller, $args) = @_;
+
+  die 'delegate needs an array ref of parameters' if 'ARRAY' ne ref $args;
+  die 'delegate needs an even number of args'     if @$args % 2;
+
+  while (@$args > 1) {
+    my $target        = shift @$args;
+    my $delegate_args = shift @$args;
+    my $params = 'HASH' eq ref $delegate_args->[0] ? $delegate_args->[0] : {};
+
+    $known_delegate_args{$_} || die "unknown parameter '$_'" for keys %$params;
+
+    die "delegate: target '$target' must match /$_cident/" if $target !~ /$_cident/;
+    die "delegate: target_method '$params->{target_method}' must match /$_ident/" if $params->{target_method} && $params->{target_method} !~ /$_ident/;
+
+    my $method_joiner = $params->{class_function} ? '::' : '->';
+
+    for my $method (@$delegate_args) {
+      next if ref $method;
+
+      die "delegate: method name '$method' must match /$_ident/" if $method !~ /$_ident/;
+
+      my $target_method = $params->{target_method} // $method;
+
+      my ($pre_context, $post_context) = ('', '');
+      if (exists $params->{force_context}) {
+        $pre_context  = $pre_context_by_key { $params->{force_context} };
+        $post_context = $post_context_by_key{ $params->{force_context} };
+        die "invalid context '$params->{force_context}' to force" unless defined $pre_context && defined $post_context;
+      }
+
+      my $target_code = ucfirst($target) eq $target ? $target : "\$_[0]->$target";
+
+      my $args_string = $args_string_by_key{ $params->{args} // 'standard' };
+      die "invalid args handling '$params->{args}'" unless defined $target_code;
+
+      eval "
+        sub ${caller}::$method {
+          $pre_context $target_code$method_joiner$target_method$args_string; $post_context
+        }
+        1;
+      " or die "could not create ${caller}::$method: $@";
+    }
+  }
+}
+
+
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Helper::Object - Meta Object Helper Mixin
+
+=head1 SYNOPSIS
+
+  use SL::Helper::Object (
+    delegate => [
+      $target => [ qw(method1 method2 method3 ...) ],
+      $target => [ { DELEGATE_OPTIONS }, qw(method1 method2 method3 ...) ],
+      ...
+    ],
+  );
+
+=head1 DESCRIPTION
+
+Sick of writing getter, setter? No because Rose::Object::MakeMethods has you covered.
+
+Sick of writing all the rest that Rose can't do? Put it here. Functionality in this
+mixin is passed as an include parameter, but are still described as functions:
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<delegate PARAMS>
+
+Creates a method that delegates to the target. If the target string starts with
+a lower case character, the generated code will be called on an object found
+within the calling object by calling an accessor. This way, it is possible to
+delegate to an object:
+
+  delegate => [
+    backend_obj => [ qw(save) ],
+  ],
+
+will generate:
+
+  sub save {
+    $_[0]->backend_obj->save
+  }
+
+If it starts with an upper case letter, it is assumed that it is a class name:
+
+  delegate => [
+    'Backend' => [ qw(save) ],
+  ],
+
+will generate:
+
+  sub save {
+    Backend->save
+  }
+
+Possible delegate args are:
+
+=over 4
+
+=item * C<target_method>
+
+Optional. If not given, the generated method will dispatch to the same method
+in the target class. If this is not possible, this can be used to overwrite it.
+
+=item * C<args>
+
+Controls how the arguments are passed.
+
+If set to C<none>, the generated code will not bother passing args. This has the benefit
+of not needing to splice the caller class out of @_, or to touch @_ at all for that matter.
+
+If set to C<raw>, the generated code will pass @_ without changes. This will
+result in the calling class or object being left in the arg, but is fine if the
+delegator is called as a function.
+
+If set to C<standard> (which is also the default), the original caller will be
+spliced out and replaced with the new calling context.
+
+=item * C<force_context>
+
+Forces the given context on the delegated method. Valid arguments can be
+C<void>, C<scalar>, C<list>. Default behaviour simply puts the call at the end
+of the sub so that context is propagated.
+
+=item * C<class_function>
+
+If true, the function will be called as a class function instead of a method call.
+
+=back
+
+=back
+
+=head1 BUGS
+
+None yet :)
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut
diff --git a/SL/Helper/PrintOptions.pm b/SL/Helper/PrintOptions.pm
new file mode 100644 (file)
index 0000000..9108a22
--- /dev/null
@@ -0,0 +1,253 @@
+package SL::Helper::PrintOptions;
+
+use strict;
+
+use List::MoreUtils qw(any);
+
+sub opthash { +{ value => shift, selected => shift, oname => shift } }
+
+# generate the printing options displayed at the bottom of oe and is forms.
+# this function will attempt to guess what type of form is displayed, and will generate according options
+#
+# about the coding:
+# this version builds the arrays of options pretty directly. if you have trouble understanding how,
+# the opthash function builds hashrefs which are then pieced together for the template arrays.
+# unneeded options are "undef"ed out, and then grepped out.
+#
+# the inline options is untested, but intended to be used later in metatemplating
+sub get_print_options {
+  my ($class, %params) = @_;
+
+  no warnings 'once';
+
+  my $form     = $params{form}     || $::form;
+  my $myconfig = $params{myconfig} || \%::myconfig;
+  my $locale   = $params{locale}   || $::locale;
+  my $options  = $params{options};
+
+  use warnings 'once';
+
+  my $prefix = $options->{dialog_name_prefix} || '';
+
+  # names 3 parameters and returns a hashref, for use in templates
+  my (@FORMNAME, @LANGUAGE_ID, @FORMAT, @SENDMODE, @MEDIA, @PRINTER_ID, @SELECTS) = ();
+
+  # note: "||"-selection is only correct for values where "0" is _not_ a correct entry
+  $form->{sendmode}   = "attachment";
+  $form->{format}     = $form->{format} || $myconfig->{template_format} || "pdf";
+  $form->{copies}     = $form->{copies} || $myconfig->{copies}          || 3;
+  $form->{media}      = $form->{media}  || $myconfig->{default_media}   || "screen";
+  $form->{printer_id} = defined $form->{printer_id}           ? $form->{printer_id} :
+                        defined $myconfig->{default_printer_id} ? $myconfig->{default_printer_id} : "";
+
+  $form->{PD}{ $form->{formname} } = "selected";
+  $form->{DF}{ $form->{format} }   = "selected";
+  $form->{OP}{ $form->{media} }    = "selected";
+  $form->{SM}{ $form->{sendmode} } = "selected";
+
+  push @FORMNAME, grep $_,
+    ($form->{type} eq 'purchase_order') ? (
+      opthash("purchase_order",      $form->{PD}{purchase_order},      $locale->text('Purchase Order')),
+      opthash("bin_list",            $form->{PD}{bin_list},            $locale->text('Bin List'))
+    ) : undef,
+    ($form->{type} eq 'credit_note') ?
+      opthash("credit_note",         $form->{PD}{credit_note},         $locale->text('Credit Note')) : undef,
+    ($form->{type} eq 'sales_order') ? (
+      opthash("sales_order",         $form->{PD}{sales_order},         $locale->text('Confirmation')),
+      opthash("proforma",            $form->{PD}{proforma},            $locale->text('Proforma Invoice')),
+      opthash("ic_supply",           $form->{PD}{ic_supply},            $locale->text('Intra-Community supply')),
+    ) : undef,
+    ($form->{type} =~ /sales_quotation$/) ?
+      opthash('sales_quotation',     $form->{PD}{sales_quotation},     $locale->text('Quotation')) : undef,
+    ($form->{type} =~ /request_quotation$/) ?
+      opthash('request_quotation',   $form->{PD}{request_quotation},   $locale->text('Request for Quotation')) : undef,
+    ($form->{type} eq 'invoice') ? (
+      opthash("invoice",             $form->{PD}{invoice},             $locale->text('Invoice')),
+      opthash("proforma",            $form->{PD}{proforma},            $locale->text('Proforma Invoice')),
+      opthash("invoice_copy",        $form->{PD}{invoice_copy},        $locale->text('Invoice Copy')),
+    ) : undef,
+    ($form->{type} eq 'invoice' && $form->{storno}) ? (
+      opthash("storno_invoice",      $form->{PD}{storno_invoice},      $locale->text('Storno Invoice')),
+    ) : undef,
+    ($form->{type} eq 'invoice_for_advance_payment') ? (
+      opthash("invoice_for_advance_payment", $form->{PD}{invoice_for_advance_payment},      $locale->text('Invoice for Advance Payment')),
+    ) : undef,
+    ($form->{type} eq 'final_invoice') ? (
+      opthash("final_invoice", $form->{PD}{final_invoice},             $locale->text('Final Invoice')),
+    ) : undef,
+    ($form->{type} =~ /^supplier_delivery_order$/) ? (
+      opthash('supplier_delivery_order', $form->{PD}{supplier_delivery_order},  $locale->text('Supplier Delivery Order')),
+    ) : undef,
+    ($form->{type} =~ /(sales|purchase)_delivery_order$/) ? (
+      opthash($form->{type},         $form->{PD}{$form->{type}},       $locale->text('Delivery Order')),
+      opthash('pick_list',           $form->{PD}{pick_list},           $locale->text('Pick List')),
+    ) : undef,
+    ($form->{type} =~ /^letter$/) ? (
+      opthash('letter',              $form->{PD}{letter},              $locale->text('Letter')),
+    ) : undef;
+
+  push @SENDMODE,
+    opthash("attachment",            $form->{SM}{attachment},          $locale->text('Attachment')),
+    opthash("inline",                $form->{SM}{inline},              $locale->text('In-line'))
+      if ($form->{media} eq 'email');
+
+  my $printable_templates = any { $::lx_office_conf{print_templates}->{$_} } qw(latex opendocument);
+  push @MEDIA, grep $_,
+      opthash("screen",              $form->{OP}{screen},              $locale->text('Screen')),
+    ($printable_templates && $form->{printers} && scalar @{ $form->{printers} }) ?
+      opthash("printer",             $form->{OP}{printer},             $locale->text('Printer')) : undef,
+    ($printable_templates && !$options->{no_queue}) ?
+      opthash("queue",               $form->{OP}{queue},               $locale->text('Queue')) : undef
+        if ($form->{media} ne 'email');
+
+  push @FORMAT, grep $_,
+    ($::lx_office_conf{print_templates}->{opendocument} &&     $::lx_office_conf{applications}->{openofficeorg_writer}  &&     $::lx_office_conf{applications}->{xvfb}
+                                                        && (-x $::lx_office_conf{applications}->{openofficeorg_writer}) && (-x $::lx_office_conf{applications}->{xvfb})
+     && !$options->{no_opendocument_pdf}) ?
+      opthash("opendocument_pdf",    $form->{DF}{"opendocument_pdf"},  $locale->text("PDF (OpenDocument/OASIS)")) : undef,
+    ($::lx_office_conf{print_templates}->{latex}) ?
+      opthash("pdf",                 $form->{DF}{pdf},                 $locale->text('PDF')) : undef,
+    ($::lx_office_conf{print_templates}->{latex} && !$options->{no_postscript}) ?
+      opthash("postscript",          $form->{DF}{postscript},          $locale->text('Postscript')) : undef,
+    (!$options->{no_html}) ?
+      opthash("html", $form->{DF}{html}, "HTML") : undef,
+    ($::lx_office_conf{print_templates}->{opendocument} && !$options->{no_opendocument}) ?
+      opthash("opendocument",        $form->{DF}{opendocument},        $locale->text("OpenDocument/OASIS")) : undef,
+    ($::lx_office_conf{print_templates}->{excel} && !$options->{no_excel}) ?
+      opthash("excel",               $form->{DF}{excel},               $locale->text("Excel")) : undef;
+
+  push @LANGUAGE_ID,
+    map { opthash($_->{id}, ($_->{id} eq $form->{language_id} ? 'selected' : ''), $_->{description}) } +{}, @{ $form->{languages} }
+      if (ref $form->{languages} eq 'ARRAY');
+
+  push @PRINTER_ID,
+    map { opthash($_->{id}, ($_->{id} eq $form->{printer_id} ? 'selected' : ''), $_->{printer_description}) } +{}, @{ $form->{printers} }
+      if ((ref $form->{printers} eq 'ARRAY') && scalar @{ $form->{printers } });
+
+  @SELECTS = map {
+    sname  => $_->[1],
+    DATA   => $_->[0],
+    show   => !$options->{"hide_" . $_->[1]} && scalar @{ $_->[0]},
+    hname  => $locale->text($_->[2])
+  },
+  [ \@FORMNAME,    'formname',    'Formname' ],
+  [ \@LANGUAGE_ID, 'language_id', 'Language' ],
+  [ \@FORMAT,      'format',      'Format'   ],
+  [ \@SENDMODE,    'sendmode',    'Sendmode' ],
+  [ \@MEDIA,       'media',       'Media'    ],
+  [ \@PRINTER_ID,  'printer_id',  'Printer'  ];
+
+  my %dont_display_groupitems = (
+    'dunning' => 1,
+    );
+
+  my %template_vars = (
+    name_prefix          => $prefix || '',
+    show_headers         => $options->{show_headers},
+    display_copies       => scalar @{ $form->{printers} || [] } && $::lx_office_conf{print_templates}->{latex} && $form->{media} ne 'email',
+    display_remove_draft => (!$form->{id} && $form->{draft_id}),
+    display_groupitems   => !$dont_display_groupitems{$form->{type}},
+    display_bothsided    => $options->{show_bothsided},
+    groupitems_checked   => $form->{groupitems} ? "checked" : '',
+    bothsided_checked    => $form->{bothsided}  ? "checked" : '',
+    remove_draft_checked => $form->{remove_draft} ? "checked" : ''
+  );
+
+  return $form->parse_html_template("generic/print_options", { SELECTS  => \@SELECTS, %template_vars } );
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Helper::PrintOptions - A helper for generating the print options for
+templates
+
+=head1 SYNOPSIS
+
+  # render your template with print_options
+  $self->render('letter/edit',
+    %params,
+    letter        => $letter,
+    print_options => SL::Helper::PrintOptions->get_print_options (
+      options => { no_postscript   => 1,
+                   no_opendocument => 1,
+                   no_html         => 1,
+                   no_queue        => 1 }),
+
+  );
+
+Then, in the template, you can render the options with
+    C<[% print_options %]>. Look at the template
+    C<generic/print_options> to see, which variables you get back.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<get_print_options %params>
+
+Parses the template C<generic/print_options>. It does some guessings
+    and settings according to the params, (namely C<form>).
+
+
+The recognized parameters are:
+
+=over 2
+
+=item * C<form>: defaults to $::form if not given. There are several
+    keys in C<form> which control the output of the options,
+    e.g. C<format>, C<media>, C<copies>, C<printers>, C<printer_id>,
+    C<type>, C<formname>, ...
+
+=item * C<myconfig>: defaults to %::myconfig
+
+=item * C<locale>: defaults to $::locale
+
+=item * C<options>: Options can be:
+
+* C<dialog_name_prefix>: a string prefixed to the template
+    variables. E.g. if prefix is C<mypref_> the value for copies
+    returned from the user is in $::form->{mypref_copies}
+
+* C<show_header>: render headings for the input elements
+
+* C<no_queue>: if set, do not show option for printing to queue
+
+* C<no_opendocument>: if set, do not show option for printing
+    opendocument format
+
+* C<no_postscript>: if set, do not show option for printing
+    postscript format
+
+* C<no_html>: if set, do not show option for printing
+    html format
+
+* C<no_opendocument_pdf>
+
+* C<no_excel>
+
+* and some more
+
+=back
+
+=back
+
+=head1 AUTHOR
+
+?
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt> (I just moved
+    it from io.pl to here and did some minor changes)
+
+=head1 BUGS
+
+incomplete documentation
+
+=cut
diff --git a/SL/Helper/QrBill.pm b/SL/Helper/QrBill.pm
new file mode 100644 (file)
index 0000000..c448eb2
--- /dev/null
@@ -0,0 +1,338 @@
+package SL::Helper::QrBill;
+
+use strict;
+use warnings;
+
+use Imager;
+use Imager::QRCode;
+
+my %Config = (
+  cross_file => 'image/CH-Kreuz_7mm.png',
+  out_file   => 'out.png',
+);
+
+sub new {
+  my $class = shift;
+
+  my $self = bless {}, $class;
+
+  $self->_init_check(@_);
+  $self->_init(@_);
+
+  return $self;
+}
+
+sub _init {
+  my $self = shift;
+  my ($biller_information, $biller_data, $payment_information, $invoice_recipient_data, $ref_nr_data) = @_;
+
+  $self->{data}{header} = [
+    'SPC',  # QRType
+    '0200', # Version
+     1,     # Coding Type
+  ];
+  $self->{data}{biller_information} = [
+    $biller_information->{iban},
+  ];
+  $self->{data}{biller_data} = [
+    $biller_data->{address_type},
+    $biller_data->{company},
+    $biller_data->{address_row1},
+    $biller_data->{address_row2},
+    '',
+    '',
+    $biller_data->{countrycode},
+  ];
+  $self->{data}{payment_information} = [
+    $payment_information->{amount},
+    $payment_information->{currency},
+  ];
+  $self->{data}{invoice_recipient_data} = [
+    $invoice_recipient_data->{address_type},
+    $invoice_recipient_data->{name},
+    $invoice_recipient_data->{address_row1},
+    $invoice_recipient_data->{address_row2},
+    '',
+    '',
+    $invoice_recipient_data->{countrycode},
+  ];
+  $self->{data}{ref_nr_data} = [
+    $ref_nr_data->{type},
+    $ref_nr_data->{ref_number},
+  ];
+  $self->{data}{additional_information} = [
+    '',
+    'EPD', # End Payment Data
+  ];
+}
+
+sub _init_check {
+  my $self = shift;
+  my ($biller_information, $biller_data, $payment_information, $invoice_recipient_data, $ref_nr_data) = @_;
+
+  my $check_re = sub {
+    my ($group, $href, $elem, $regex) = @_;
+    defined $href->{$elem} && $href->{$elem} =~ $regex
+      or die "field '$elem' in group '$group' not valid", "\n";
+  };
+
+  my $group = 'biller information';
+  $check_re->($group, $biller_information, 'iban', qr{^(?:CH|LI)[0-9a-zA-Z]{19}$});
+
+  $group = 'biller data';
+  $check_re->($group, $biller_data, 'address_type', qr{^[KS]$});
+  $check_re->($group, $biller_data, 'company', qr{^.{1,70}$});
+  $check_re->($group, $biller_data, 'address_row1', qr{^.{0,70}$});
+  $check_re->($group, $biller_data, 'address_row2', qr{^.{0,70}$});
+  $check_re->($group, $biller_data, 'countrycode', qr{^[A-Z]{2}$});
+
+  $group = 'payment information';
+  $check_re->($group, $payment_information, 'amount', qr{^(?:(?:0|[1-9][0-9]{0,8})\.[0-9]{2})?$});
+  $check_re->($group, $payment_information, 'currency', qr{^(?:CHF|EUR)$});
+
+  $group = 'invoice recipient data';
+  $check_re->($group, $invoice_recipient_data, 'address_type', qr{^[KS]$});
+  $check_re->($group, $invoice_recipient_data, 'name', qr{^.{1,70}$});
+  $check_re->($group, $invoice_recipient_data, 'address_row1', qr{^.{0,70}$});
+  $check_re->($group, $invoice_recipient_data, 'address_row2', qr{^.{0,70}$});
+  $check_re->($group, $invoice_recipient_data, 'countrycode', qr{^[A-Z]{2}$});
+
+  $group = 'reference number data';
+  my %ref_nr_regexes = (
+    QRR => qr{^\d{27}$},
+    NON => qr{^$},
+  );
+  $check_re->($group, $ref_nr_data, 'type', qr{^(?:QRR|SCOR|NON)$});
+  $check_re->($group, $ref_nr_data, 'ref_number', $ref_nr_regexes{$ref_nr_data->{type}});
+}
+
+sub generate {
+  my $self = shift;
+  my $out_file = $_[0] // $Config{out_file};
+
+  $self->{qrcode} = $self->_qrcode();
+  $self->{cross}  = $self->_cross();
+  $self->{img}    = $self->_plot();
+
+  $self->_paste();
+  $self->_write($out_file);
+}
+
+sub _qrcode {
+  my $self = shift;
+
+  return Imager::QRCode->new(
+    size   =>  4,
+    margin =>  0,
+    level  => 'M',
+  );
+}
+
+sub _cross {
+  my $self = shift;
+
+  my $cross = Imager->new();
+  $cross->read(file => $Config{cross_file}) or die $cross->errstr, "\n";
+
+  return $cross->scale(xpixels => 35, ypixels => 35, qtype => 'mixing');
+}
+
+sub _plot {
+  my $self = shift;
+
+  my @data = (
+    @{$self->{data}{header}},
+    @{$self->{data}{biller_information}},
+    @{$self->{data}{biller_data}},
+    ('') x 7, # for future use
+    @{$self->{data}{payment_information}},
+    @{$self->{data}{invoice_recipient_data}},
+    @{$self->{data}{ref_nr_data}},
+    @{$self->{data}{additional_information}},
+  );
+
+  foreach (@data) {
+    s/[\r\n]/ /g;
+    s/ {2,}/ /g;
+    s/^\s+//;
+    s/\s+$//;
+  }
+                  # CR + LF
+  my $text = join "\015\012", @data;
+
+  return $self->{qrcode}->plot($text);
+}
+
+sub _paste {
+  my $self = shift;
+
+  $self->{img}->paste(
+    src  => $self->{cross},
+    left => ($self->{img}->getwidth  / 2) - ($self->{cross}->getwidth  / 2),
+    top  => ($self->{img}->getheight / 2) - ($self->{cross}->getheight / 2),
+  );
+}
+
+sub _write {
+  my $self = shift;
+  my ($out_file) = @_;
+
+  $self->{img}->write(file => $out_file) or die $self->{img}->errstr, "\n";
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Helper::QrBill - Helper methods for generating Swiss QR-Code
+
+=head1 SYNOPSIS
+
+     use SL::Helper::QrBill;
+
+     eval {
+       my $qr_image = SL::Helper::QrBill->new(
+         \%biller_information,
+         \%biller_data,
+         \%payment_information,
+         \%invoice_recipient_data,
+         \%ref_nr_data,
+       );
+       $qr_image->generate($out_file);
+     } or do {
+       local $_ = $@; chomp; my $error = $_;
+       $::form->error($::locale->text('QR-Image generation failed: ' . $error));
+     };
+
+=head1 DESCRIPTION
+
+This module generates the Swiss QR-Code with data provided to the constructor.
+
+=head1 METHODS
+
+=head2 C<new>
+
+Creates a new object. Expects five references to hashes as arguments.
+
+The hashes are structured as follows:
+
+=over 4
+
+=item C<%biller_information>
+
+Fields: iban.
+
+=over 4
+
+=item C<iban>
+
+Fixed length; 21 alphanumerical characters, only IBANs with CH- or LI-
+country code.
+
+=back
+
+=item C<%biller_data>
+
+Fields: address_type, company, address_row1, address_row2 and countrycode.
+
+=over 4
+
+=item C<address_type>
+
+Fixed length; 1-digit, alphanumerical. 'K' implemented only.
+
+=item C<company>
+
+Maximum of 70 characters, name (surname allowable) or company.
+
+=item C<address_row1>
+
+Maximum of 70 characters, street/nr.
+
+=item C<address_row2>
+
+Maximum of 70 characters, postal code/place.
+
+=item C<countrycode>
+
+2-digit country code according to ISO 3166-1.
+
+=back
+
+=item C<%payment_information>
+
+Fields: amount and currency.
+
+=over 4
+
+=item C<amount>
+
+Decimal, no leading zeroes, maximum of 12 digits (inclusive decimal
+separator and places). Only dot as decimal separator is permitted.
+
+=item C<currency>
+
+CHF/EUR.
+
+=back
+
+=item C<%invoice_recipient_data>
+
+Fields: address_type, name, address_row1, address_row2 and countrycode.
+
+=over 4
+
+=item C<address_type>
+
+Fixed length; 1-digit, alphanumerical. 'K' implemented only.
+
+=item C<name>
+
+Maximum of 70 characters, name (surname allowable) or company.
+
+=item C<address_row1>
+
+Maximum of 70 characters, street/nr.
+
+=item C<address_row2>
+
+Maximum of 70 characters, postal code/place.
+
+=item C<countrycode>
+
+2-digit country code according to ISO 3166-1.
+
+=back
+
+=item C<%ref_nr_data>
+
+Fields: type and ref_number.
+
+=over 4
+
+=item C<type>
+
+Maximum of 4 characters, alphanumerical. QRR/SCOR/NON.
+
+=item C<ref_number>
+
+QR-Reference: 27 characters, numerical; without Reference: empty.
+
+=back
+
+=back
+
+=head2 C<generate>
+
+Generates the QR-Code image. Accepts filename of image as argument.
+Defaults to C<out.png>.
+
+=head1 AUTHOR
+
+Steven Schubiger E<lt>stsc@refcnt.orgE<gt>
+
+=cut
diff --git a/SL/Helper/ShippedQty.pm b/SL/Helper/ShippedQty.pm
new file mode 100644 (file)
index 0000000..6b878a9
--- /dev/null
@@ -0,0 +1,613 @@
+package SL::Helper::ShippedQty;
+
+use strict;
+use parent qw(Rose::Object);
+
+use Carp;
+use Scalar::Util qw(blessed);
+use List::Util qw(min);
+use List::MoreUtils qw(any all uniq);
+use List::UtilsBy qw(partition_by);
+use SL::AM;
+use SL::DBUtils qw(selectall_hashref_query selectall_as_map);
+use SL::Locale::String qw(t8);
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar'                => [ qw(objects objects_or_ids shipped_qty keep_matches) ],
+  'scalar --get_set_init' => [ qw(oe_ids dbh require_stock_out oi2oe oi_qty delivered matches services_deliverable) ],
+);
+
+my $no_stock_item_links_query = <<'';
+  SELECT oi.trans_id, oi.id AS oi_id, oi.qty AS oi_qty, oi.unit AS oi_unit, doi.id AS doi_id, doi.qty AS doi_qty, doi.unit AS doi_unit
+  FROM record_links rl
+  INNER JOIN orderitems oi            ON oi.id = rl.from_id AND rl.from_table = 'orderitems'
+  INNER JOIN delivery_order_items doi ON doi.id = rl.to_id AND rl.to_table = 'delivery_order_items'
+  WHERE oi.trans_id IN (%s)
+  ORDER BY oi.trans_id, oi.position
+
+my $stock_item_links_query = <<'';
+  SELECT oi.trans_id, oi.id AS oi_id, oi.qty AS oi_qty, oi.unit AS oi_unit, doi.id AS doi_id,
+    (CASE WHEN doe.customer_id > 0 THEN -1 ELSE 1 END) * i.qty AS doi_qty, p.unit AS doi_unit
+  FROM record_links rl
+  INNER JOIN orderitems oi                   ON oi.id = rl.from_id AND rl.from_table = 'orderitems'
+  INNER JOIN delivery_order_items doi        ON doi.id = rl.to_id AND rl.to_table = 'delivery_order_items'
+  INNER JOIN delivery_orders doe             ON doe.id = doi.delivery_order_id
+  INNER JOIN delivery_order_items_stock dois ON dois.delivery_order_item_id = doi.id
+  INNER JOIN inventory i                     ON dois.id = i.delivery_order_items_stock_id
+  INNER JOIN parts p                         ON p.id = doi.parts_id
+  WHERE oi.trans_id IN (%s)
+  ORDER BY oi.trans_id, oi.position
+
+sub calculate {
+  my ($self, $data) = @_;
+
+  croak 'Need exactly one argument, either id, object or arrayref of ids or objects.' unless 2 == @_;
+
+  $self->normalize_input($data);
+
+  return $self unless @{ $self->oe_ids };
+
+  $self->calculate_item_links;
+
+  $self;
+}
+
+sub calculate_item_links {
+  my ($self) = @_;
+
+  my @oe_ids = @{ $self->oe_ids };
+
+  my $item_links_query = $self->require_stock_out ? $stock_item_links_query : $no_stock_item_links_query;
+
+  my $query = sprintf $item_links_query, join (', ', ('?')x @oe_ids);
+
+  my $data = selectall_hashref_query($::form, $self->dbh, $query, @oe_ids);
+
+  for (@$data) {
+    my $qty = $_->{doi_qty} * AM->convert_unit($_->{doi_unit} => $_->{oi_unit});
+    $self->shipped_qty->{$_->{oi_id}} //= 0;
+    $self->shipped_qty->{$_->{oi_id}} += $qty;
+    $self->oi2oe->{$_->{oi_id}}        = $_->{trans_id};
+    $self->oi_qty->{$_->{oi_id}}       = $_->{oi_qty};
+
+    push @{ $self->matches }, [ $_->{oi_id}, $_->{doi_id}, $qty, 1 ] if $self->keep_matches;
+  }
+}
+
+sub write_to {
+  my ($self, $objects) = @_;
+
+  croak 'expecting array of objects' unless 'ARRAY' eq ref $objects;
+
+  my $shipped_qty = $self->shipped_qty;
+
+  for my $obj (@$objects) {
+    if ('SL::DB::OrderItem' eq ref $obj) {
+      $obj->{shipped_qty} = $shipped_qty->{$obj->id} //= 0;
+      $obj->{delivered}   = $shipped_qty->{$obj->id} == $obj->qty;
+    } elsif ('SL::DB::Order' eq ref $obj) {
+      # load all orderitems unless not already loaded
+      $obj->orderitems unless (defined $obj->{orderitems});
+      $self->write_to($obj->{orderitems});
+      if ($self->services_deliverable) {
+        $obj->{delivered} = all { $_->{delivered} } grep { !$_->{optional} } @{ $obj->{orderitems} };
+      } else {
+        $obj->{delivered} = all { $_->{delivered} } grep { !$_->{optional} && !$_->part->is_service } @{ $obj->{orderitems} };
+      }
+    } else {
+      die "unknown reference '@{[ ref $obj ]}' for @{[ __PACKAGE__ ]}::write_to";
+    }
+  }
+  $self;
+}
+
+sub write_to_objects {
+  my ($self) = @_;
+
+  return unless @{ $self->oe_ids };
+
+  croak 'Can only use write_to_objects, when calculate was called with objects. Use write_to instead.' unless $self->objects_or_ids;
+
+  $self->write_to($self->objects);
+}
+
+sub normalize_input {
+  my ($self, $data) = @_;
+
+  $data = [$data] if 'ARRAY' ne ref $data;
+
+  $self->objects_or_ids(!!blessed($data->[0]));
+
+  if ($self->objects_or_ids) {
+    croak 'unblessed object in data while expecting object' if any { !blessed($_) } @$data;
+    $self->objects($data);
+  } else {
+    croak 'object or reference in data while expecting ids' if any { ref($_) } @$data;
+    croak 'ids need to be numbers'                          if any { ! ($_ * 1) } @$data;
+    $self->oe_ids($data);
+  }
+
+  $self->shipped_qty({});
+}
+
+
+sub init_oe_ids {
+  my ($self) = @_;
+
+  croak 'oe_ids not initialized in id mode'            if !$self->objects_or_ids;
+  croak 'objects not initialized before accessing ids' if $self->objects_or_ids && !defined $self->objects;
+  croak 'objects need to be Order or OrderItem'        if any  {  ref($_) !~ /^SL::DB::Order(?:Item)?$/ } @{ $self->objects };
+
+  [ uniq map { ref($_) =~ /Item/ ? $_->trans_id : $_->id } @{ $self->objects } ]
+}
+
+sub init_dbh { SL::DB->client->dbh }
+
+sub init_oi2oe { {} }
+sub init_oi_qty { {} }
+sub init_matches { [] }
+sub init_delivered {
+  my ($self) = @_;
+
+  my $d = { };
+  for (keys %{ $self->oi_qty }) {
+    my $oe_id = $self->oi2oe->{$_};
+    $d->{$oe_id} //= 1;
+    $d->{$oe_id} &&= $self->shipped_qty->{$_} == $self->oi_qty->{$_};
+  }
+  $d;
+}
+
+sub init_require_stock_out    { $::instance_conf->get_shipped_qty_require_stock_out }
+
+sub init_services_deliverable  {
+  my ($self) = @_;
+  if (($::form->{type}//'') =~ m/^sales_/ || $self->{objects}->[0]->{customer_id}) {
+    $::instance_conf->get_sales_delivery_order_check_service;
+  } elsif (($::form->{type}//'') =~ m/^purchase_/ || $self->{objects}->[0]->{vendor_id}) {
+    $::instance_conf->get_purchase_delivery_order_check_service;
+  } else {
+    croak "wrong call, no customer or vendor object referenced";
+  }
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Helper::ShippedQty - Algorithmic module for calculating shipped qty
+
+=head1 SYNOPSIS
+
+  use SL::Helper::ShippedQty;
+
+  my $helper = SL::Helper::ShippedQty->new(
+    require_stock_out    => 0,
+    item_identity_fields => [ qw(parts_id description reqdate serialnumber) ],
+  );
+
+  $helper->calculate($order_object);
+  $helper->calculate(\@order_objects);
+  $helper->calculate($orderitem_object);
+  $helper->calculate(\@orderitem_objects);
+  $helper->calculate($oe_id);
+  $helper->calculate(\@oe_ids);
+
+  # if these are items set delivered and shipped_qty
+  # if these are orders, iterate through their items and set delivered on order
+  $helper->write_to($objects);
+
+  # if calculate was called with objects, you can use this shortcut:
+  $helper->write_to_objects;
+
+  # shipped_qtys by oi_id
+  my $shipped_qty = $helper->shipped_qty->{$oi->id};
+
+  # delivered by oe_id
+  my $delivered = $helper->delievered->{$oi->id};
+
+  # calculate and write_to can be chained:
+  my $helper = SL::Helper::ShippedQty->new->calculate($orders)->write_to_objects;
+
+=head1 DESCRIPTION
+
+This module encapsulates the algorithm needed to compute the shipped qty for
+orderitems (hopefully) correctly and efficiently for several use cases.
+
+While this is used in object accessors, it can not be fast when called in a
+loop over and over, so take advantage of batch processing when possible.
+
+=head1 MOTIVATION AND PROBLEMS
+
+The concept of shipped qty is sadly not as straight forward as it sounds at
+first glance. Any correct implementation must in some way deal with the
+following problems.
+
+=over 4
+
+=item *
+
+When is an order shipped? For users that use the inventory it
+will mean when a delivery order is stocked out. For those not using the
+inventory it will mean when the delivery order is saved.
+
+=item *
+
+orderitems and oe entries may link to many of their counterparts in
+delivery_orders. delivery_orders may be created from multiple orders. The
+only constant is that a single entry in delivery_order_items has at most one
+link from an orderitem.
+
+=item *
+
+Certain delivery orders might not be eligible for qty calculations if delivery
+orders are used for other purposes.
+
+=item *
+
+Units need to be handled correctly
+
+=item *
+
+Negative positions must be taken into account. A negative delivery order is
+assumed to be a RMA of sorts, but a negative order is not as straight forward.
+
+=item *
+
+Must be able to work with plain ids and Rose objects, and absolutely must
+include a bulk mode to speed up multiple objects.
+
+=back
+
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<new PARAMS>
+
+Creates a new helper object, $::form->{type} is mandatory.
+
+PARAMS may include:
+
+=over 4
+
+=item * C<require_stock_out>
+
+Boolean. If set, delivery orders must be stocked out to be considered
+delivered. The default is a client setting.
+
+
+=item * C<keep_matches>
+
+Boolean. If set to true the internal matchings of OrderItems and
+DeliveryOrderItems will be kept for later postprocessing, in case you need more
+than this modules provides.
+
+See C<matches> for the returned format.
+
+
+=back
+
+=item C<calculate OBJECTS>
+
+=item C<calculate IDS>
+
+Do the main work. There must be a single argument: Either an id or an
+C<SL::DB::Order> object, or an arrayref of one of these types.
+
+Mixing ids and objects will generate an exception.
+
+No return value. All internal errors will throw an exception.
+
+=item C<write_to OBJECTS>
+
+=item C<write_to_objects>
+
+Save the C<shipped_qty> and C<delivered> state to the given objects. If
+L</calculate> was called with objects, then C<write_to_objects> will use these.
+
+C<shipped_qty> and C<delivered> will be directly infused into the objects
+without calling the accessor for delivered. If you want to save afterwards,
+you'll have to do that yourself.
+
+C<shipped_qty> is guaranteed to be coerced to a number. If no delivery_order
+was found it will be set to zero.
+
+C<delivered> is guaranteed only to be the correct boolean value, but not
+any specific value.
+
+Note: C<write_to> will avoid loading unnecessary objects. This means if it is
+called with an Order object that has not loaded its orderitems yet, only
+C<delivered> will be set in the Order object. A subsequent C<<
+$order->orderitems->[0]->{delivered} >> will return C<undef>, and C<<
+$order->orderitems->[0]->shipped_qty >> will invoke another implicit
+calculation.
+
+=item C<shipped_qty>
+
+Valid after L</calculate>. Returns a hasref with shipped qtys by orderitems id.
+
+Unlike the result of C</write_to>, entries in C<shipped_qty> may be C<undef> if
+linked elements were found.
+
+=item C<delivered>
+
+Valid after L</calculate>. Returns a hashref with a delivered flag by order id.
+
+=item C<matches>
+
+Valid after L</calculate> with C<with_matches> set. Returns an arrayref of
+individual matches. Each match is an arrayref with these fields:
+
+=over 4
+
+=item *
+
+The id of the OrderItem.
+
+=item *
+
+The id of the DeliveryOrderItem.
+
+=item *
+
+The qty that was matched between the two converted to the unit of the OrderItem.
+
+=item *
+
+A boolean flag indicating if this match was found with record_item links. If
+false, the match was made in the fill up stage.
+
+=back
+
+=back
+
+=head1 REPLACED FUNCTIONALITY
+
+=head2 delivered mode
+
+Originally used in mark_orders_if_delivered. Searches for orders associated
+with a delivery order and evaluates whether those are delivered or not. No
+detailed information is needed.
+
+This is to be integrated into fast delivered check on the orders. The calling
+convention for the delivery_order is not part of the scope of this module.
+
+=head2 do_mode
+
+Originally used for printing delivery orders. Resolves for each position for
+how much was originally ordered, and how much remains undelivered.
+
+This one is likely to be dropped. The information only makes sense without
+combined merge/split deliveries and is very fragile with unaccounted delivery
+orders.
+
+=head2 oe mode
+
+Same from the order perspective. Used for transitions to delivery orders, where
+delivered qtys should be removed from positions. Also used each time a record
+is rendered to show the shipped qtys. Also used to find orders that are not
+fully delivered.
+
+Acceptable shortcuts would be the concepts fully shipped (for the order) and
+providing already loaded objects.
+
+=head2 Replaces the following functions
+
+C<DO::get_shipped_qty>
+
+C<SL::Controller::DeliveryPlan::calc_qtys>
+
+C<SL::DB::OrderItem::shipped_qty>
+
+C<SL::DB::OrderItem::delivered_qty>
+
+=head1 OLD ALGORITHM
+
+this is the old get_shipped_qty algorithm by Martin for reference
+
+    in: oe_id, do_id, doctype, delivered flag
+
+    not needed with better signatures
+     if do_id:
+       load oe->do links for this id,
+       set oe_ids from those
+     fi
+     if oe_id:
+       set oe_ids to this
+
+    return if no oe_ids;
+
+  2 load all orderitems for these oe_ids
+    for orderitem:
+      nomalize qty
+      set undelivered := qty
+    end
+
+    create tuple: [ position => qty_ordered, qty_not_delivered, orderitem.id ]
+
+  1 load all oe->do links for these oe_ids
+
+    if no links:
+      return all tuples so far
+    fi
+
+  4 create dictionary for orderitems from [2] by id
+
+  3 load all delivery_order_items for do_ids from [1], with recorditem_links from orderitems
+      - optionally with doctype filter (identity filter)
+
+    # first pass for record_item_links
+    for dois:
+      normalize qty
+      if link from orderitem exists and orderitem is in dictionary [4]
+        reduce qty_notdelivered in orderitem by doi.qty
+        keep link to do entry in orderitem
+    end
+
+    # second pass fill up
+    for dois:
+      ignroe if from link exists or qty == 0
+
+      for orderitems from [2]:
+        next if notdelivered_qty == 0
+        if doi.parts_id == orderitem.parts_id:
+          if oi.notdelivered_qty < 0:
+            doi :+= -oi.notdelivered_qty,
+            oi.notdelivered_qty := 0
+          else:
+            fi doi.qty < oi.notdelivered_qty:
+              doi.qty := 0
+              oi.notdelivered_qty :-= doi.qty
+            else:
+              doi.qty :-= oi.notdelivered_qty
+              oi.notdelivered_qty := 0
+            fi
+            keep link to oi in doi
+          fi
+        fi
+        last wenn doi.qty <= 0
+      end
+    end
+
+    # post process for return
+
+    if oe_id:
+      copy notdelivered from oe to ship{position}{notdelivered}
+    if !oe_id and do_id and delivered:
+      ship.{oi.trans_id}.delivered := oi.notdelivered_qty <= 0
+    if !oe_id and do_id and !delivered:
+      for all doi:
+        ignore if do.id != doi.delivery_order_id
+        if oi in doi verlinkt und position bekannt:
+          addiere oi.qty               zu doi.ordered_qty
+          addiere oi.notdelievered_qty zu doi.notdelivered_qty
+        fi
+      end
+    fi
+
+=head1 NEW ALGORITHM
+
+  in: orders, parameters
+
+  normalize orders to ids
+
+  # handle record_item links
+  retrieve record_links entries with inner joins on orderitems, delivery_orderitems and stock/inventory if requested
+  for all record_links:
+    initialize shipped_qty for this doi to 0 if not yet seen
+    convert doi.qty to oi.unit
+    add normalized doi.qty to shipped_qty
+  end
+
+  # handle fill up
+  abort if fill up is not requested
+
+  retrieve all orderitems matching the given order ids
+  retrieve all doi with a link to the given order ids but without item link (and optionally with stock/inventory)
+  retrieve all record_links between orders and delivery_orders                  (1)
+
+  abort when no dois were found
+
+  create a partition of the delivery order items by do_id                       (2)
+  create empty mapping for delivery order items by order_id                     (3)
+  for all record_links from [1]:
+    add all matching doi from (2) to (3)
+  end
+
+  create a partition of the orderitems by item identity                         (4)
+  create a partition of the delivery order items by item identity               (5)
+
+  for each identity in (4):
+    skip if no matching entries in (5)
+
+    create partition of all orderitems for this identity by order id            (6)
+    for each sorted order id in [6]:
+      look up matching delivery order items by identity from [5]                (7)
+      look up matching delivery order items by order id from [3]                (8)
+      create stable sorted intersection between [7] and [8]                     (9)
+
+      sort the orderitems from (6) by position                                 (10)
+
+      parallel walk through [9] and [10]:
+        missing qty :=  oi.qty - shipped_qty[oi]
+
+
+        next orderitem           if missing_qty <= 0
+        next delivery order item if doi.qty == 0
+
+        min_qty := minimum(missing_qty, [doi.qty converted to oi.unit]
+
+        # transfer min_qty from doi.qty to shipped[qty]:
+        shipped_qty[oi] += min_qty
+        doi.qty         -= [min_qty converted to doi.unit]
+      end
+    end
+  end
+
+=head1 COMPLEXITY OBSERVATIONS
+
+Perl ops except for sort are expected to be constant (relative to the op overhead).
+
+=head2 Record item links
+
+The query itself has indices available for all joins and filters and should
+scale with sublinear with the number of affected orderitems.
+
+The rest of the code iterates through the result and calls C<AM::convert_unit>,
+which caches internally and is asymptotically constant.
+
+=head2 Fill up
+
+C<partition_by> and C<intersect> both scale linearly. The first two scale with
+input size, but use existing indices. The delivery order items query scales
+with the nested loop anti join of the "NOT EXISTS" subquery, which takes most
+of the time. For large databases omitting the order id filter may be faster.
+
+Three partitions after that scale linearly. Building the doi_by_oe_id
+multimap is O(n²) worst case, but will be linear for most real life data.
+
+Iterating through the values of the partitions scales with the number of
+elements in the multimap, and does not add additional complexity.
+
+The sort and parallel walk are O(nlogn) for the length of the subdivisions,
+which again makes square worst case, but much less than that in the general
+case.
+
+=head3 Space requirements
+
+In the current form the results of the 4 queries get fetched, and 4 of them are
+held in memory at the same time. Three persistent structures are held:
+C<shipped_qty>, C<oi2oe>, and C<oi_qty> - all hashes with one entry for each
+orderitem. C<delivered> is calculated on demand and is a hash with an entry for
+each order id of input.
+
+Temporary structures are partitions of the orderitems, of which again the fill
+up multi map between order id and delivery order items is potentially the
+largest with square requierment worst case.
+
+
+=head1 TODO
+
+  * delivery order identity
+  * test stocked
+  * rewrite to avoid division
+  * rewrite to avoid selectall for really large queries (no problem for up to 100k)
+  * calling mode or return to flag delivery_orders as delivered?
+  * add localized field white list
+  * reduce worst case square space requirement to linear
+
+=head1 BUGS
+
+None yet, but there are most likely a lot in code this funky.
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut
diff --git a/SL/Helper/UNECERecommendation20.pm b/SL/Helper/UNECERecommendation20.pm
new file mode 100644 (file)
index 0000000..e79991c
--- /dev/null
@@ -0,0 +1,59 @@
+package SL::Helper::UNECERecommendation20;
+
+use strict;
+use warnings;
+use utf8;
+
+use Exporter qw(import);
+our @EXPORT_OK = qw(map_name_to_alpha_2_code);
+
+use List::Util qw(first);
+
+my @mappings = (
+  # space and time
+  # areas
+  [ 'MTK', qr{^(?:m²|qm|quadrat *meter|quadrat *metre)$}i ],
+
+  # distances
+  [ 'CMT', qr{^(?:cm|centi *meter|centi *metre)$}i ],
+  [ 'MTR', qr{^(?:m|meter|metre)$}i ],
+  [ 'KMT', qr{^(?:km|kilo *meter|kilo *metre)$}i ],
+
+  # durations
+  [ 'SEC', qr{^(?:s|sec|second|sek|sekunde)$}i ],
+  [ 'MIN', qr{^min(?:ute)?$}i ],
+  [ 'HUR', qr{^(?:h(?:our)?|std(?:unde)?)$}i ],
+  [ 'DAY', qr{^(?:day|tag)$}i ],
+  [ 'MON', qr{^mon(?:th|at|atlich)?$}i ],
+  [ 'QAN', qr{^quart(?:er|al|alsweise)?$}i ],
+  [ 'ANN', qr{^(?:yearly|annually|jährlich|Jahr)?$}i ],
+
+  # mass
+  [ 'MGM', qr{^(?:mg|milli *gramm?)$}i ],
+  [ 'GRM', qr{^(?:g|gramm?)$}i ],
+  [ 'KGM', qr{^(?:kg|kilo *gramm?)$}i ],
+  [ 'KTN', qr{^(?:t|tonne|kilo *tonne)$}i ],
+
+  # volumes
+  [ 'MLT', qr{^(?:ml|milli *liter|milli *litre)$}i ],
+  [ 'LTR', qr{^(?:l|liter|litre)$}i ],
+
+  # miscellaneous
+  [ 'C62', qr{^(?:stck|stück|pieces?|pc|psch|pauschal|licenses?|lizenz(?:en)?)$}i ],
+);
+
+sub map_name_to_code {
+  my ($unit) = @_;
+
+  return undef if ($unit // '') eq '';
+
+  my $code = first { $unit =~ $_->[1] } @mappings;
+  return $code->[0] if $code;
+
+  no warnings 'once';
+  $::lxdebug->message(LXDebug::WARN(), "UNECERecommendation20::map_name_code: no mapping found for '$unit'");
+
+  return undef;
+}
+
+1;
diff --git a/SL/Helper/UserPreferences.pm b/SL/Helper/UserPreferences.pm
new file mode 100644 (file)
index 0000000..2e5f127
--- /dev/null
@@ -0,0 +1,376 @@
+package SL::Helper::UserPreferences;
+
+use strict;
+use parent qw(Rose::Object);
+use version;
+
+use SL::DBUtils qw(selectall_hashref_query selectfirst_hashref_query do_query selectcol_array_query);
+use SL::DB;
+
+use Rose::Object::MakeMethods::Generic (
+ 'scalar --get_set_init' => [ qw(login namespace upgrade_callbacks current_version auto_store_back) ],
+);
+
+sub store {
+  my ($self, $key, $value) = @_;
+
+  SL::DB->client->with_transaction(sub {
+    my $tuple = $self->get_tuple($key);
+
+    if ($tuple && $tuple->{id}) {
+      $tuple->{value}  = $value;
+      $self->_update($tuple);
+    } else {
+      my $query = 'INSERT INTO user_preferences (login, namespace, version, key, value) VALUES (?, ?, ?, ?, ?)';
+      do_query($::form, $::form->get_standard_dbh, $query, $self->login, $self->namespace, $self->current_version, $key, $value);
+    }
+    1;
+  }) or do { die SL::DB->client->error };
+}
+
+sub get {
+  my ($self, $key) = @_;
+
+  my $tuple = $self->get_tuple($key);
+
+  $tuple ? $tuple->{value} : undef;
+}
+
+sub get_tuple {
+  my ($self, $key) = @_;
+
+  my $tuple;
+
+  SL::DB->client->with_transaction(sub {
+    $tuple = selectfirst_hashref_query($::form, $::form->get_standard_dbh, <<"", $self->login, $self->namespace, $key);
+      SELECT * FROM user_preferences WHERE login = ? AND namespace = ? AND key = ?
+
+    if ($tuple && $tuple->{version} < $self->current_version) {
+      $self->_upgrade($tuple);
+    }
+
+    if ($tuple && $tuple->{version} > $self->current_version) {
+      die "Future version $tuple->{version} for user preference @{ $self->namespace }/$key. Expected @{ $self->current_version } or less.";
+    }
+    1;
+  }) or do { die SL::DB->client->error };
+
+  return $tuple;
+}
+
+sub get_all {
+  my ($self) = @_;
+
+  my $data;
+
+  SL::DB->client->with_transaction(sub {
+    $data = selectall_hashref_query($::form, $::form->get_standard_dbh, <<"", $self->login, $self->namespace);
+      SELECT * FROM user_preferences WHERE login = ? AND namespace = ?
+
+    for my $tuple (@$data) {
+      if ($tuple->{version} < $self->current_version) {
+        $self->_upgrade($tuple);
+      }
+
+      if ($tuple->{version} > $self->current_version) {
+        die "Future version $tuple->{version} for user preference @{ $self->namespace }/$tuple->{key}. Expected @{ $self->current_version } or less.";
+      }
+    }
+    1;
+  }) or do { die SL::DB->client->error };
+
+  return $data;
+}
+
+sub get_keys {
+  my ($self) = @_;
+
+  my @keys = selectcol_array_query($::form, SL::DB->client->dbh, <<"", $self->login, $self->namespace);
+    SELECT key FROM user_preferences WHERE login = ? AND namespace = ?
+
+  return @keys;
+}
+
+sub delete {
+  my ($self, $key) = @_;
+
+  die 'delete without  key is not allowed, use delete_all instead' unless $key;
+
+  SL::DB->client->with_transaction(sub {
+    my $query =  'DELETE FROM user_preferences WHERE login = ? AND namespace = ? AND key = ?';
+    do_query($::form, $::form->get_standard_dbh, $query, $self->login, $self->namespace, $key);
+    1;
+  }) or do { die SL::DB->client->error };
+}
+
+sub delete_all {
+  my ($self, $key) = @_;
+
+  my @keys;
+
+  SL::DB->client->with_transaction(sub {
+    my $query = 'DELETE FROM user_preferences WHERE login = ? AND namespace = ?';
+    do_query($::form, $::form->get_standard_dbh, $query, $self->login, $self->namespace);
+    1;
+  }) or do { die SL::DB->client->error };
+}
+
+### internal stuff
+
+sub _upgrade {
+  my ($self, $tuple) = @_;
+
+  for my $to_version (sort { $a <=> $b } grep { $_ > $tuple->{version} } keys %{ $self->upgrade_callbacks }) {
+    $tuple->{value}   = $self->upgrade_callbacks->{$to_version}->($tuple->{value});
+    $tuple->{version} = $to_version;
+  }
+
+  if ($self->auto_store_back) {
+    $self->_update($tuple);
+  }
+}
+
+sub _update {
+  my ($self, $tuple) = @_;
+
+  my $query = 'UPDATE user_preferences SET version = ?, value = ? WHERE id = ?';
+  do_query($::form, $::form->get_standard_dbh, $query, $tuple->{version}, $tuple->{value}, $tuple->{id});
+}
+
+### defaults stuff
+
+sub init_login             { SL::DB::Manager::Employee->current->login    }
+sub init_namespace         { ref $_[0]                                    }
+sub init_upgrade_callbacks { +{}                                          }
+sub init_current_version   { version->parse((ref $_[0])->VERSION)->numify }
+sub init_auto_store_back   { 1                                            }
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Helper::UserPreferences - user based preferences store
+
+=head1 SYNOPSIS
+
+  use SL::Helper::UserPreferences;
+  my $user_pref = SL::Helper::UserPreferences->new(
+    login             => $login,        # defaults to current user
+    namespace         => $namespace,    # defaults to current package
+    upgrade_callbacks => $upgrade_callbacks,
+    current_version   => $version,      # defaults to __PACKAGE__->VERSION->numify
+    auto_store_back   => 0,             # default 1
+  );
+
+  $user_pref->store($key, $value);
+  my $val    = $user_pref->get($key);
+  my $tuple  = $user_pref->get_tuple($key);
+  my $tuples = $user_pref->get_all;
+  my $keys   = $user_pref->get_keys;
+  $user_pref->delete($key);
+  $user_pref->delete_all;
+
+=head1 DESCRIPTION
+
+This module provides a generic storage for information that needs to be stored
+between sessions per user and per client and between versions of the program.
+
+The storage can be accessed as a generic key/value dictionary, but also
+requires a namespace to avoid clashes and a version of the information.
+Additionally you must provide means to upgrade or invalidate stored information
+that is out of date, i.e. after a program upgrade.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<new PARAMS>
+
+Creates a new instance. Available C<PARAMS>:
+
+=over 4
+
+=item C<login>
+
+The user for this storage. Defaults to current user login.
+
+=item C<namespace>
+
+A unique namespace. Defaults to the calling package.
+
+=item C<upgrade_callbacks>
+
+A hashref with version numbers as keys and subs as values. These subs are
+expected to take a value and return an upgraded value for the version of their
+key.
+
+No default. Mandatory.
+
+=item C<current_version>
+
+The version object that is considered current for stored information. Defaults
+to the version of the calling package. MUST be a number, and not a version
+object, so that versions can be used as hash keys in the ugrade_callbacks.
+
+=item C<auto_store_back>
+
+An otional flag indicating whether values from the database that were upgraded to a
+newer version should be stored back automatically. Defaults to
+C<$::lx_office_conf{debug}{auto_store_back_upgraded_user_preferences}> which in
+turn defaults to true.
+
+=back
+
+=item C<store KEY VALUE>
+
+Stores a key-value tuple. If there exists already a value for this key, it will
+be overwritten.
+
+=item C<get KEY>
+
+Retrieves a value.
+
+Returns the value. If no such value exists returns undef instead.
+
+This is for easy of use, and does no distinction between non-existing values
+and valid undefined values. Use C<get_tuple> if you need this.
+
+=item C<get_tuple KEY>
+
+Retrieves a key-value tuple.
+
+Returns a hashref with C<key> and C<value> entries. If no such value
+exists returns undef instead.
+
+=item C<get_all>
+
+Retrieve all key-value tuples in this namespace and user.
+
+Returns an arrayref of hashrefs.
+
+=item C<get_keys>
+
+Retrieve all keys for this namespace. Note: Unless you store vast amount of
+data, it's most likely easier to just C<get_all>.
+
+Returns an arrayref of keys.
+
+=item C<delete KEY>
+
+Deletes a tuple.
+
+=item C<delete_all>
+
+Delete all tuples for this namespace and user.
+
+=back
+
+=head1 VERSIONING
+
+Every entry in the user prefs must have a version to be compatible in case of
+code upgrades.
+
+Code reading user prefs must check if the version is the expected one, and must
+have upgrade code to upgrade out of date preferences to the current version.
+
+Code SHOULD write the upgraded version back to the store at the earliest time
+to keep preferences up to date. This should be able to be disabled to have
+developer versions not overwrite preferences with unsupported versions.
+
+Example:
+
+Initial code dealing with prefs:
+
+  our $VERSION = v1;
+
+  $user_prefs->store("selected tab", $::form->{selected_tab});
+
+And the someone edits the code and removes the tab "Webdav". To ensure
+favorites with webdav selected are upgraded:
+
+  our $VERSION = v2;
+
+  my $upgrade_callbacks = {
+    2 => sub { $_[0] eq 'WebDav' ? 'MasterData' : $_[0]; },
+  };
+
+  my $val = $user_prefs->get("selected tab");
+
+=head1 LACK OF TYPING
+
+This controller will not attempt to preserve types. All data will be
+stringified. If your code needs to preserve numbers, you MUST encode the data
+to JSON or YAML before storing.
+
+=head1 PLANNED BEST PRACTICE
+
+To be able to decouple controllers and the schema upgrading required for this,
+there should be exactly one module responsible for managing user preferences for
+each namespace. You should find the corresponding preferences owners in the
+class namespace C<SL::Helper::UserPreferences>.
+
+For example the namespace C<PartsSearchFavorites> should only be managed by
+C<SL::Helper::UserPreferences::PartsSearchFavorites>. This way, it's possible
+to keep the upgrades in one place, and to migrate upgrades out of there into
+database upgrades during major releases. They also don't clutter up
+controllers.
+
+It is planned to strip all modules located there of their upgrade for a release
+and do automatic database upgrades.
+
+To avoid version clashes when developing customer branches, please only use
+stable version bumps in the unstable branch, and use dev versions in customer
+branches.
+
+=head1 BEHAVIOUR
+
+=over 4
+
+=item *
+
+If a (namepace, key) tuple exists, a store will overwrite the last version
+
+=item *
+
+If the value retrieved from the database is newer than the code version, an
+error must be thrown.
+
+=item *
+
+get will check the version against the current version and apply all upgrade
+steps.
+
+=item *
+
+If the final step is not the current version, behaviour is undefined
+
+=item *
+
+get_all will always return scalar context.
+
+=back
+
+=head1 TODO AND SPECIAL CASES
+
+* not defined whether it should be possible to retrieve the version of a tuple
+
+* it's not specified how to return invalidation from upgrade, nor how to handle
+  that
+
+* it's not specified whether admin is a user. for now it dies.
+
+* We're missing user agnostic methods for database upgrades
+
+=head1 BUGS
+
+None yet :)
+
+=head1 AUTHOR
+
+Sven Schöling <s.schoeling@linet-services.de>
+
+=cut
diff --git a/SL/Helper/UserPreferences/DisplayPreferences.pm b/SL/Helper/UserPreferences/DisplayPreferences.pm
new file mode 100644 (file)
index 0000000..f3bcae3
--- /dev/null
@@ -0,0 +1,66 @@
+package SL::Helper::UserPreferences::DisplayPreferences;
+
+use strict;
+use parent qw(Rose::Object);
+
+use SL::Helper::UserPreferences;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(user_prefs) ],
+);
+
+sub get_longdescription_dialog_size_percentage {
+  $_[0]->user_prefs->get('longdescription_dialog_size_percentage');
+}
+
+sub store_longdescription_dialog_size_percentage {
+  $_[0]->user_prefs->store('longdescription_dialog_size_percentage', $_[1]);
+}
+
+sub init_user_prefs {
+  SL::Helper::UserPreferences->new(
+    namespace => $_[0]->namespace,
+  )
+}
+
+# read only stuff
+sub namespace     { 'DisplayPreferences' }
+sub version       { 1 }
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Helper::UserPreferences::DisplayPreferences - preferences intended
+to store user settings for various display settings.
+
+=head1 SYNOPSIS
+
+  use SL::Helper::UserPreferences::DisplayPreferences;
+  my $prefs = SL::Helper::UserPreferences::DisplayPreferences->new();
+
+  $prefs->store_use_duration(1);
+  my $value = $prefs->get_longdescription_dialog_size_percentage;
+
+=head1 DESCRIPTION
+
+This module manages storing the user's choise for settings for
+various display settings.
+For now the preferred procentual size of the edit-dialog for longdescriptions
+of positions can be stored.
+
+=head1 BUGS
+
+None yet :)
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Helper/UserPreferences/DisplayableName.pm b/SL/Helper/UserPreferences/DisplayableName.pm
new file mode 100644 (file)
index 0000000..b7b753e
--- /dev/null
@@ -0,0 +1,108 @@
+package SL::Helper::UserPreferences::DisplayableName;
+
+use strict;
+use parent qw(Rose::Object);
+
+use Carp;
+use List::MoreUtils qw(none);
+
+use SL::Helper::UserPreferences;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(module default_prefs user_prefs data) ],
+);
+
+sub get {
+  $_[0]->data;
+}
+
+sub _store {
+  my ($self, $val, $target) = @_;
+
+  return if $self->data eq $val;
+
+  $self->data($val);
+  $self->$target->store($self->module, $self->data);
+}
+
+sub init_default_prefs {
+  SL::Helper::UserPreferences->new(
+    login           => $_[0]->default_login,
+    namespace       => $_[0]->namespace,
+  )
+}
+
+sub init_user_prefs {
+  SL::Helper::UserPreferences->new(
+    namespace => $_[0]->namespace,
+  )
+}
+
+sub init_data {
+  my $data;
+  $data   = $_[0]->user_prefs   ->get($_[0]->module);
+  $data //= $_[0]->default_prefs->get($_[0]->module);
+
+  return $data;
+}
+
+sub init_module {
+  die 'need module';
+}
+
+# proxy to user prefs
+sub delete        { $_[0]->user_prefs->delete($_[0]->module); $_[0]->data($_[0]->init_data()) }
+sub login         { $_[0]->user_prefs->login }
+
+# proxy to default prefs
+sub get_default   { $_[0]->default_prefs->get($_[0]->module) }
+
+# aliases
+sub store_value   { _store(@_, 'user_prefs')    }
+sub store_default { _store(@_, 'default_prefs') }
+
+# read only stuff
+sub default_login { '#default#' }
+sub namespace     { 'DisplayableName' }
+sub version       { 1 }
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Helper::UserPreferences::DisplayableName - hybrid preferences intended
+for two tiered (user over default) displayable name preferences
+
+=head1 SYNOPSIS
+
+  use SL::Helper::UserPreferences::DisplayableName;
+  my $prefs = SL::Helper::UserPreferences::DisplayableName->new(
+    module => 'SL::DB::Customer'
+  );
+
+  my $value = $prefs->get;
+  my $value = $prefs->store_value('<%number%> <%name%> (PLZ <%zipcode%>)');
+  my $value = $prefs->store_default('<%number%> <%name%>');
+
+=head1 DESCRIPTION
+
+This module proxies two L<SL::Helper::UserPreferences> instances, one global and
+one for the current user.
+It is intended to be used with the C<SL::DB::SomeObject> classes via
+L<SL::DB::Helper::DisplayableNamePreferences> (see there).
+
+=head1 BUGS
+
+None yet :)
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Helper/UserPreferences/PartPickerSearch.pm b/SL/Helper/UserPreferences/PartPickerSearch.pm
new file mode 100644 (file)
index 0000000..7d4821d
--- /dev/null
@@ -0,0 +1,75 @@
+package SL::Helper::UserPreferences::PartPickerSearch;
+
+use strict;
+use parent qw(Rose::Object);
+
+use Carp;
+use List::MoreUtils qw(none);
+
+use SL::Helper::UserPreferences;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(user_prefs) ],
+);
+
+sub get_sales_search_customer_partnumber {
+  !!$_[0]->user_prefs->get('sales_search_customer_partnumber');
+}
+
+sub get_purchase_search_makemodel {
+  !!$_[0]->user_prefs->get('purchase_search_makemodel');
+}
+
+sub store_sales_search_customer_partnumber {
+  $_[0]->user_prefs->store('sales_search_customer_partnumber', $_[1]);
+}
+
+sub store_purchase_search_makemodel {
+  $_[0]->user_prefs->store('purchase_search_makemodel', $_[1]);
+}
+
+sub init_user_prefs {
+  SL::Helper::UserPreferences->new(
+    namespace => $_[0]->namespace,
+  )
+}
+
+# read only stuff
+sub namespace     { 'PartPickerSearch' }
+sub version       { 1 }
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Helper::UserPreferences::PartPickerSearch - preferences intended
+to store user settings for the behavior of a partpicker search.
+
+=head1 SYNOPSIS
+
+  use SL::Helper::UserPreferences::PartPickerSearch;
+  my $prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
+
+  $prefs->store_purchase_search_makemodel(1);
+  my $value = $prefs->get_purchase_search_makemodel;
+
+=head1 DESCRIPTION
+
+This module manages storing the settings for the part picker to search for
+customer/vendor partnumber in sales/purchase forms (new order controller).
+
+=head1 BUGS
+
+None yet :)
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Helper/UserPreferences/PositionsScrollbar.pm b/SL/Helper/UserPreferences/PositionsScrollbar.pm
new file mode 100644 (file)
index 0000000..9d0a338
--- /dev/null
@@ -0,0 +1,69 @@
+package SL::Helper::UserPreferences::PositionsScrollbar;
+
+use strict;
+use parent qw(Rose::Object);
+
+use Carp;
+use List::MoreUtils qw(none);
+
+use SL::Helper::UserPreferences;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(user_prefs) ],
+);
+
+sub get_height {
+  my $value = $_[0]->user_prefs->get('height');
+  return !defined($value) ? 25 : $value;
+}
+
+sub store_height {
+  $_[0]->user_prefs->store('height', $_[1]);
+}
+
+sub init_user_prefs {
+  SL::Helper::UserPreferences->new(
+    namespace => $_[0]->namespace,
+  )
+}
+
+# read only stuff
+sub namespace     { 'PositionsScrollbar' }
+sub version       { 1 }
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Helper::UserPreferences::PositionsScrollbar - preferences intended
+to store user settings for displaying a scrollbar for the postions area
+of document forms (it's height).
+
+=head1 SYNOPSIS
+
+  use SL::Helper::UserPreferences::PositionsScrollbar;
+  my $prefs = SL::Helper::UserPreferences::PositionsScrollbar->new();
+
+  $prefs->store_height(75);
+  my $value = $prefs->get_height;
+
+=head1 DESCRIPTION
+
+This module manages storing the height for displaying the scrollbar in the
+positions area in forms (new order controller).
+
+=head1 BUGS
+
+None yet :)
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Helper/UserPreferences/TimeRecording.pm b/SL/Helper/UserPreferences/TimeRecording.pm
new file mode 100644 (file)
index 0000000..7686a7c
--- /dev/null
@@ -0,0 +1,69 @@
+package SL::Helper::UserPreferences::TimeRecording;
+
+use strict;
+use parent qw(Rose::Object);
+
+use Carp;
+use List::MoreUtils qw(none);
+
+use SL::Helper::UserPreferences;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(user_prefs) ],
+);
+
+sub get_use_duration {
+  !!$_[0]->user_prefs->get('use_duration');
+}
+
+sub store_use_duration {
+  $_[0]->user_prefs->store('use_duration', $_[1]);
+}
+
+sub init_user_prefs {
+  SL::Helper::UserPreferences->new(
+    namespace => $_[0]->namespace,
+  )
+}
+
+# read only stuff
+sub namespace     { 'TimeRecording' }
+sub version       { 1 }
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Helper::UserPreferences::TimeRecording - preferences intended
+to store user settings for using the time recording functionality.
+
+=head1 SYNOPSIS
+
+  use SL::Helper::UserPreferences::TimeRecording;
+  my $prefs = SL::Helper::UserPreferences::TimeRecording->new();
+
+  $prefs->store_use_duration(1);
+  my $value = $prefs->get_use_duration;
+
+=head1 DESCRIPTION
+
+This module manages storing the user's choise for settings for
+the time recording controller.
+For now it can be choosen if an entry is done by entering start and
+end time or a date and a duration.
+
+=head1 BUGS
+
+None yet :)
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/Helper/UserPreferences/UpdatePositions.pm b/SL/Helper/UserPreferences/UpdatePositions.pm
new file mode 100644 (file)
index 0000000..ae06285
--- /dev/null
@@ -0,0 +1,68 @@
+package SL::Helper::UserPreferences::UpdatePositions;
+
+use strict;
+use parent qw(Rose::Object);
+
+use Carp;
+use List::MoreUtils qw(none);
+
+use SL::Helper::UserPreferences;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(user_prefs) ],
+);
+
+sub get_show_update_button {
+  !!$_[0]->user_prefs->get('show_update_button');
+}
+
+sub store_show_update_button {
+  $_[0]->user_prefs->store('show_update_button', $_[1]);
+}
+
+sub init_user_prefs {
+  SL::Helper::UserPreferences->new(
+    namespace => $_[0]->namespace,
+  )
+}
+
+# read only stuff
+sub namespace     { 'UpdatePositions' }
+sub version       { 1 }
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Helper::UserPreferences::UpdatePositions - preferences intended
+to store user settings for displaying an update button for the postions
+of document forms to update the positions (parts) from master data.
+
+=head1 SYNOPSIS
+
+  use SL::Helper::UserPreferences::UpdatePositions;
+  my $prefs = SL::Helper::UserPreferences::UpdatePositions->new();
+
+  $prefs->store_show_update_button(1);
+  my $value = $prefs->get_show_update_button;
+
+=head1 DESCRIPTION
+
+This module manages storing the user's choise for displaying an update button
+in the positions area in forms (new order controller).
+
+=head1 BUGS
+
+None yet :)
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
index dc86484..2a94607 100644 (file)
--- a/SL/IC.pm
+++ b/SL/IC.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Inventory Control backend
@@ -36,177 +37,18 @@ package IC;
 
 use Data::Dumper;
 use List::MoreUtils qw(all any uniq);
-use YAML;
 
 use SL::CVar;
 use SL::DBUtils;
 use SL::HTML::Restrict;
 use SL::TransNumber;
+use SL::Util qw(trim);
+use SL::DB;
+use SL::Presenter::Part qw(type_abbreviation classification_abbreviation separate_abbreviation);
+use Carp;
 
 use strict;
 
-sub get_part {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to db
-  my $dbh = $form->get_standard_dbh;
-
-  my $sth;
-
-  my $query =
-    qq|SELECT p.*,
-         c1.accno AS inventory_accno,
-         c2.accno AS income_accno,
-         c3.accno AS expense_accno,
-         pg.partsgroup
-       FROM parts p
-       LEFT JOIN chart c1 ON (p.inventory_accno_id = c1.id)
-       LEFT JOIN chart c2 ON (p.income_accno_id = c2.id)
-       LEFT JOIN chart c3 ON (p.expense_accno_id = c3.id)
-       LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
-       WHERE p.id = ? |;
-  my $ref = selectfirst_hashref_query($form, $dbh, $query, conv_i($form->{id}));
-
-  # copy to $form variables
-  map { $form->{$_} = $ref->{$_} } (keys %{$ref});
-
-  $form->{mtime} = $form->{itime} if !$form->{mtime};
-  $form->{lastmtime} = $form->{mtime};
-  $form->{onhand} *= 1;
-
-  # part or service item
-  $form->{item} = ($form->{inventory_accno}) ? 'part' : 'service';
-  if ($form->{assembly}) {
-    $form->{item} = 'assembly';
-
-    # retrieve assembly items
-    $query =
-      qq|SELECT p.id, p.partnumber, p.description,
-           p.sellprice, p.lastcost, p.weight, a.qty, a.bom, p.unit,
-           pg.partsgroup, p.price_factor_id, pfac.factor AS price_factor
-         FROM parts p
-         JOIN assembly a ON (a.parts_id = p.id)
-         LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
-         LEFT JOIN price_factors pfac ON pfac.id = p.price_factor_id
-         WHERE (a.id = ?)
-         ORDER BY a.oid|;
-    $sth = prepare_execute_query($form, $dbh, $query, conv_i($form->{id}));
-
-    $form->{assembly_rows} = 0;
-    while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
-      $form->{assembly_rows}++;
-      foreach my $key (keys %{$ref}) {
-        $form->{"${key}_$form->{assembly_rows}"} = $ref->{$key};
-      }
-    }
-    $sth->finish;
-
-  }
-
-  # setup accno hash for <option checked> {amount} is used in create_links
-  $form->{amount}{IC}         = $form->{inventory_accno};
-  $form->{amount}{IC_income}  = $form->{income_accno};
-  $form->{amount}{IC_sale}    = $form->{income_accno};
-  $form->{amount}{IC_expense} = $form->{expense_accno};
-  $form->{amount}{IC_cogs}    = $form->{expense_accno};
-
-  # get prices
-  $query = <<SQL;
-    SELECT pg.pricegroup, pg.id AS pricegroup_id, COALESCE(pr.price, 0) AS price
-    FROM pricegroup pg
-    LEFT JOIN prices pr ON (pr.pricegroup_id = pg.id) AND (pr.parts_id = ?)
-    ORDER BY lower(pg.pricegroup)
-SQL
-
-  my $row = 1;
-  foreach $ref (selectall_hashref_query($form, $dbh, $query, conv_i($form->{id}))) {
-    $form->{"${_}_${row}"} = $ref->{$_} for qw(pricegroup_id pricegroup price);
-    $row++;
-  }
-  $form->{price_rows} = $row - 1;
-
-  # get makes
-  if ($form->{makemodel}) {
-  #hli
-    $query = qq|SELECT m.make, m.model,m.lastcost,m.lastcost,m.lastupdate,m.sortorder FROM makemodel m | .
-             qq|WHERE m.parts_id = ? order by m.sortorder asc|;
-    my @values = ($form->{id});
-    $sth = $dbh->prepare($query);
-    $sth->execute(@values) || $form->dberror("$query (" . join(', ', @values) . ")");
-
-    my $i = 1;
-
-    while (($form->{"make_$i"}, $form->{"model_$i"}, $form->{"old_lastcost_$i"},
-              $form->{"lastcost_$i"}, $form->{"lastupdate_$i"}, $form->{"sortorder_$i"}) = $sth->fetchrow_array)
-    {
-      $i++;
-    }
-    $sth->finish;
-    $form->{makemodel_rows} = $i - 1;
-
-  }
-
-  # get translations
-  $query = qq|SELECT language_id, translation, longdescription
-              FROM translation
-              WHERE parts_id = ?|;
-  $form->{translations} = selectall_hashref_query($form, $dbh, $query, conv_i($form->{id}));
-
-  # is it an orphan
-  my @referencing_tables = qw(invoice orderitems inventory);
-  my %column_map         = ( );
-  my $parts_id           = conv_i($form->{id});
-
-  $form->{orphaned}      = 1;
-
-  foreach my $table (@referencing_tables) {
-    my $column  = $column_map{$table} || 'parts_id';
-    $query      = qq|SELECT $column FROM $table WHERE $column = ? LIMIT 1|;
-    my ($found) = selectrow_query($form, $dbh, $query, $parts_id);
-
-    if ($found) {
-      $form->{orphaned} = 0;
-      last;
-    }
-  }
-
-  $form->{"unit_changeable"} = $form->{orphaned};
-
-  Common::webdav_folder($form) if $::lx_office_conf{features}{webdav};
-
-  $main::lxdebug->leave_sub();
-}
-
-sub get_pricegroups {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  my $dbh = $form->get_standard_dbh;
-
-  # get pricegroups
-  my $query = qq|SELECT id, pricegroup FROM pricegroup ORDER BY lower(pricegroup)|;
-  my $pricegroups = selectall_hashref_query($form, $dbh, $query);
-
-  my $i = 1;
-  foreach my $pg (@{ $pricegroups }) {
-    $form->{"klass_$i"} = "$pg->{id}";
-    $form->{"price_$i"} = $form->format_amount($myconfig, $form->{"price_$i"}, -2);
-    $form->{"pricegroup_id_$i"} = "$pg->{id}";
-    $form->{"pricegroup_$i"}    = "$pg->{pricegroup}";
-    $i++;
-  }
-
-  #correct rows
-  $form->{price_rows} = $i - 1;
-
-  $main::lxdebug->leave_sub();
-
-  return $pricegroups;
-}
-
 sub retrieve_buchungsgruppen {
   $main::lxdebug->enter_sub();
 
@@ -223,415 +65,6 @@ sub retrieve_buchungsgruppen {
   $main::lxdebug->leave_sub();
 }
 
-sub save {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-  my @values;
-  # connect to database, turn off AutoCommit
-  my $dbh = $form->get_standard_dbh;
-  my $restricter = SL::HTML::Restrict->create;
-
-  # save the part
-  # make up a unique handle and store in partnumber field
-  # then retrieve the record based on the unique handle to get the id
-  # replace the partnumber field with the actual variable
-  # add records for makemodel
-
-  # if there is a $form->{id} then replace the old entry
-  # delete all makemodel entries and add the new ones
-
-  # undo amount formatting
-  map { $form->{$_} = $form->parse_amount($myconfig, $form->{$_}) }
-    qw(rop weight listprice sellprice gv lastcost);
-
-  my $makemodel = ($form->{make_1} || $form->{model_1} || ($form->{makemodel_rows} > 1)) ? 1 : 0;
-
-  $form->{assembly} = ($form->{item} eq 'assembly') ? 1 : 0;
-
-  my ($query, $sth);
-
-  my $priceupdate = ', priceupdate = current_date';
-
-  if ($form->{id}) {
-    my $trans_number = SL::TransNumber->new(type => $form->{item}, dbh => $dbh, number => $form->{partnumber}, id => $form->{id});
-    if (!$trans_number->is_unique) {
-      $::lxdebug->leave_sub;
-      return 3;
-    }
-
-    # get old price
-    $query = qq|SELECT sellprice, weight FROM parts WHERE id = ?|;
-    my ($sellprice, $weight) = selectrow_query($form, $dbh, $query, conv_i($form->{id}));
-
-    # if item is part of an assembly adjust all assemblies
-    $query = qq|SELECT id, qty FROM assembly WHERE parts_id = ?|;
-    $sth = prepare_execute_query($form, $dbh, $query, conv_i($form->{id}));
-    while (my ($id, $qty) = $sth->fetchrow_array) {
-      &update_assembly($dbh, $form, $id, $qty, $sellprice * 1, $weight * 1);
-    }
-    $sth->finish;
-
-    # delete makemodel records
-    do_query($form, $dbh, qq|DELETE FROM makemodel WHERE parts_id = ?|, conv_i($form->{id}));
-
-    if ($form->{item} eq 'assembly') {
-      # delete assembly records
-      do_query($form, $dbh, qq|DELETE FROM assembly WHERE id = ?|, conv_i($form->{id}));
-    }
-
-    # delete translations
-    do_query($form, $dbh, qq|DELETE FROM translation WHERE parts_id = ?|, conv_i($form->{id}));
-
-    # Check whether or not the prices have changed. If they haven't
-    # then 'priceupdate' should not be updated.
-    my $previous_values = selectfirst_hashref_query($form, $dbh, qq|SELECT * FROM parts WHERE id = ?|, conv_i($form->{id})) || {};
-    $priceupdate        = '' if (all { $previous_values->{$_} == $form->{$_} } qw(sellprice lastcost listprice));
-
-  } else {
-    my $trans_number = SL::TransNumber->new(type => $form->{item}, dbh => $dbh, number => $form->{partnumber}, save => 1);
-
-    if ($form->{partnumber} && !$trans_number->is_unique) {
-      $::lxdebug->leave_sub;
-      return 3;
-    }
-
-    $form->{partnumber} ||= $trans_number->create_unique;
-
-    ($form->{id}) = selectrow_query($form, $dbh, qq|SELECT nextval('id')|);
-    do_query($form, $dbh, qq|INSERT INTO parts (id, partnumber, unit) VALUES (?, ?, ?)|, $form->{id}, $form->{partnumber}, $form->{unit});
-
-    $form->{orphaned} = 1;
-  }
-  my $partsgroup_id = undef;
-
-  if ($form->{partsgroup}) {
-    (my $partsgroup, $partsgroup_id) = split(/--/, $form->{partsgroup});
-  }
-
-  my ($subq_inventory, $subq_expense, $subq_income);
-  if ($form->{"item"} eq "part") {
-    $subq_inventory =
-      qq|(SELECT bg.inventory_accno_id
-          FROM buchungsgruppen bg
-          WHERE bg.id = | . conv_i($form->{"buchungsgruppen_id"}, 'NULL') . qq|)|;
-  } else {
-    $subq_inventory = "NULL";
-  }
-
-  if ($form->{"item"} ne "assembly") {
-    $subq_expense =
-      qq|(SELECT tc.expense_accno_id
-          FROM taxzone_charts tc
-          WHERE tc.buchungsgruppen_id = | . conv_i($form->{"buchungsgruppen_id"}, 'NULL') . qq| and tc.taxzone_id = 0)|;
-  } else {
-    $subq_expense = "NULL";
-  }
-
-  normalize_text_blocks();
-
-  $query =
-    qq|UPDATE parts SET
-         partnumber = ?,
-         description = ?,
-         makemodel = ?,
-         alternate = 'f',
-         assembly = ?,
-         listprice = ?,
-         sellprice = ?,
-         lastcost = ?,
-         weight = ?,
-         unit = ?,
-         notes = ?,
-         formel = ?,
-         rop = ?,
-         warehouse_id = ?,
-         bin_id = ?,
-         buchungsgruppen_id = ?,
-         payment_id = ?,
-         inventory_accno_id = $subq_inventory,
-         income_accno_id = (SELECT tc.income_accno_id FROM taxzone_charts tc WHERE tc.taxzone_id = 0 and tc.buchungsgruppen_id = ?),
-         expense_accno_id = $subq_expense,
-         obsolete = ?,
-         image = ?,
-         drawing = ?,
-         shop = ?,
-         ve = ?,
-         gv = ?,
-         ean = ?,
-         has_sernumber = ?,
-         not_discountable = ?,
-         microfiche = ?,
-         partsgroup_id = ?,
-         price_factor_id = ?
-         $priceupdate
-       WHERE id = ?|;
-  @values = ($form->{partnumber},
-             $form->{description},
-             $makemodel ? 't' : 'f',
-             $form->{assembly} ? 't' : 'f',
-             $form->{listprice},
-             $form->{sellprice},
-             $form->{lastcost},
-             $form->{weight},
-             $form->{unit},
-             $restricter->process($form->{notes}),
-             $form->{formel},
-             $form->{rop},
-             conv_i($form->{warehouse_id}),
-             conv_i($form->{bin_id}),
-             conv_i($form->{buchungsgruppen_id}),
-             conv_i($form->{payment_id}),
-             conv_i($form->{buchungsgruppen_id}),
-             $form->{obsolete} ? 't' : 'f',
-             $form->{image},
-             $form->{drawing},
-             $form->{shop} ? 't' : 'f',
-             conv_i($form->{ve}),
-             conv_i($form->{gv}),
-             $form->{ean},
-             $form->{has_sernumber} ? 't' : 'f',
-             $form->{not_discountable} ? 't' : 'f',
-             $form->{microfiche},
-             conv_i($partsgroup_id),
-             conv_i($form->{price_factor_id}),
-             conv_i($form->{id})
-  );
-  do_query($form, $dbh, $query, @values);
-
-  # delete translation records
-  do_query($form, $dbh, qq|DELETE FROM translation WHERE parts_id = ?|, conv_i($form->{id}));
-
-  my @translations = grep { $_->{language_id} && $_->{translation} } @{ $form->{translations} || [] };
-  if (@translations) {
-    $query = qq|INSERT into translation (parts_id, language_id, translation, longdescription)
-                VALUES ( ?, ?, ?, ? )|;
-    $sth   = $dbh->prepare($query);
-
-    foreach my $translation (@translations) {
-      do_statement($form, $sth, $query, conv_i($form->{id}), conv_i($translation->{language_id}), $translation->{translation}, $restricter->process($translation->{longdescription}));
-    }
-
-    $sth->finish();
-  }
-
-  # delete price records
-  do_query($form, $dbh, qq|DELETE FROM prices WHERE parts_id = ?|, conv_i($form->{id}));
-
-  $query = qq|INSERT INTO prices (parts_id, pricegroup_id, price) VALUES(?, ?, ?)|;
-  $sth   = prepare_query($form, $dbh, $query);
-
-  for my $i (1 .. $form->{price_rows}) {
-    my $price = $form->parse_amount($myconfig, $form->{"price_$i"});
-    next unless $price;
-
-    @values = (conv_i($form->{id}), conv_i($form->{"pricegroup_id_$i"}), $price);
-    do_statement($form, $sth, $query, @values);
-  }
-
-  $sth->finish;
-
-  # insert makemodel records
-    my $lastupdate = '';
-    my $value = 0;
-    for my $i (1 .. $form->{makemodel_rows}) {
-      if (($form->{"make_$i"}) || ($form->{"model_$i"})) {
-        #hli
-        $value = $form->parse_amount($myconfig, $form->{"lastcost_$i"});
-        if ($value == $form->parse_amount($myconfig, $form->{"old_lastcost_$i"}))
-        {
-            if ($form->{"lastupdate_$i"} eq "") {
-                $lastupdate = 'now()';
-            } else {
-                $lastupdate = $dbh->quote($form->{"lastupdate_$i"});
-            }
-        } else {
-            $lastupdate = 'now()';
-        }
-        $query = qq|INSERT INTO makemodel (parts_id, make, model, lastcost, lastupdate, sortorder) | .
-                 qq|VALUES (?, ?, ?, ?, ?, ?)|;
-        @values = (conv_i($form->{id}), conv_i($form->{"make_$i"}), $form->{"model_$i"}, $value, $lastupdate, conv_i($form->{"sortorder_$i"}) );
-
-        do_query($form, $dbh, $query, @values);
-      }
-    }
-
-  # add assembly records
-  if ($form->{item} eq 'assembly') {
-    # check additional assembly row
-    my $i = $form->{assembly_rows};
-    # if last row is not empty add them
-    if ($form->{"partnumber_$i"} ne "") {
-      $query = qq|SELECT id FROM parts WHERE partnumber = ?|;
-      my ($partid) = selectrow_query($form, $dbh, $query,$form->{"partnumber_$i"} );
-      if ( $partid ) {
-        $form->{"qty_$i"} = 1 unless ($form->{"qty_$i"});
-        $form->{"id_$i"} = $partid;
-        $form->{"bom_$i"} = 0;
-        $form->{assembly_rows}++;
-      }
-      else {
-        $::form->error($::locale->text("uncorrect partnumber ").$form->{"partnumber_$i"});
-      }
-    }
-
-    for my $i (1 .. $form->{assembly_rows}) {
-      $form->{"qty_$i"} = $form->parse_amount($myconfig, $form->{"qty_$i"});
-
-      if ($form->{"qty_$i"} != 0) {
-        $form->{"bom_$i"} *= 1;
-        $query = qq|INSERT INTO assembly (id, parts_id, qty, bom) | .
-                 qq|VALUES (?, ?, ?, ?)|;
-        @values = (conv_i($form->{id}), conv_i($form->{"id_$i"}), conv_i($form->{"qty_$i"}), $form->{"bom_$i"} ? 't' : 'f');
-        do_query($form, $dbh, $query, @values);
-      }
-    }
-    my @a = localtime;
-    $a[5] += 1900;
-    $a[4]++;
-    my $shippingdate = "$a[5]-$a[4]-$a[3]";
-
-    $form->get_employee($dbh);
-
-  }
-
-  #set expense_accno=inventory_accno if they are different => bilanz
-  my $vendor_accno =
-    ($form->{expense_accno} != $form->{inventory_accno})
-    ? $form->{inventory_accno}
-    : $form->{expense_accno};
-
-  # get tax rates and description
-  my $accno_id =
-    ($form->{vc} eq "customer") ? $form->{income_accno} : $vendor_accno;
-  $query =
-    qq|SELECT c.accno, c.description, t.rate, t.taxnumber
-       FROM chart c, tax t
-       WHERE (c.id = t.chart_id) AND (t.taxkey IN (SELECT taxkey_id FROM chart where accno = ?))
-       ORDER BY c.accno|;
-  my $stw = prepare_execute_query($form, $dbh, $query, $accno_id);
-
-  $form->{taxaccount} = "";
-  while (my $ptr = $stw->fetchrow_hashref("NAME_lc")) {
-    $form->{taxaccount} .= "$ptr->{accno} ";
-    if (!($form->{taxaccount2} =~ /\Q$ptr->{accno}\E/)) {
-      $form->{"$ptr->{accno}_rate"}        = $ptr->{rate};
-      $form->{"$ptr->{accno}_description"} = $ptr->{description};
-      $form->{"$ptr->{accno}_taxnumber"}   = $ptr->{taxnumber};
-      $form->{taxaccount2} .= " $ptr->{accno} ";
-    }
-  }
-
-  CVar->save_custom_variables(dbh           => $dbh,
-                              module        => 'IC',
-                              trans_id      => $form->{id},
-                              variables     => $form,
-                              save_validity => 1);
-
-  # Delete saved custom variable values for configs that have been
-  # marked invalid for this part.
-  $query = <<SQL;
-    DELETE FROM custom_variables
-    WHERE (config_id IN (
-        SELECT val.config_id
-        FROM custom_variables_validity val
-        LEFT JOIN custom_variable_configs val_cfg ON (val.config_id = val_cfg.id)
-        WHERE (val_cfg.module = 'IC')
-          AND (val.trans_id   = ?)))
-      AND (trans_id = ?)
-SQL
-  do_query($form, $dbh, $query, ($form->{id}) x 2);
-
-  # commit
-  my $rc = $dbh->commit;
-
-  $main::lxdebug->leave_sub();
-
-  return $rc;
-}
-
-sub update_assembly {
-  $main::lxdebug->enter_sub();
-
-  my ($dbh, $form, $id, $qty, $sellprice, $weight) = @_;
-
-  my $query = qq|SELECT id, qty FROM assembly WHERE parts_id = ?|;
-  my $sth = prepare_execute_query($form, $dbh, $query, conv_i($id));
-
-  while (my ($pid, $aqty) = $sth->fetchrow_array) {
-    &update_assembly($dbh, $form, $pid, $aqty * $qty, $sellprice, $weight);
-  }
-  $sth->finish;
-
-  $query =
-    qq|UPDATE parts SET sellprice = sellprice + ?, weight = weight + ?
-       WHERE id = ?|;
-  my @values = ($qty * ($form->{sellprice} - $sellprice),
-             $qty * ($form->{weight} - $weight), conv_i($id));
-  do_query($form, $dbh, $query, @values);
-
-  $main::lxdebug->leave_sub();
-}
-
-sub retrieve_assemblies {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->get_standard_dbh;
-
-  my $where = qq|NOT p.obsolete|;
-  my @values;
-
-  if ($form->{partnumber}) {
-    $where .= qq| AND (p.partnumber ILIKE ?)|;
-    push(@values, '%' . $form->{partnumber} . '%');
-  }
-
-  if ($form->{description}) {
-    $where .= qq| AND (p.description ILIKE ?)|;
-    push(@values, '%' . $form->{description} . '%');
-  }
-
-  # retrieve assembly items
-  my $query =
-    qq|SELECT p.id, p.partnumber, p.description,
-              p.onhand, p.rop,
-         (SELECT sum(p2.inventory_accno_id)
-          FROM parts p2, assembly a
-          WHERE (p2.id = a.parts_id) AND (a.id = p.id)) AS inventory
-       FROM parts p
-       WHERE NOT p.obsolete AND p.assembly $where|;
-
-  $form->{assembly_items} = selectall_hashref_query($form, $dbh, $query, @values);
-
-  $main::lxdebug->leave_sub();
-}
-
-sub delete {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-  my @values = (conv_i($form->{id}));
-  # connect to database, turn off AutoCommit
-  my $dbh = $form->get_standard_dbh;
-
-  my %columns = ( "assembly" => "id", "parts" => "id" );
-
-  for my $table (qw(prices makemodel inventory assembly translation parts)) {
-    my $column = defined($columns{$table}) ? $columns{$table} : "parts_id";
-    do_query($form, $dbh, qq|DELETE FROM $table WHERE $column = ?|, @values);
-  }
-
-  # commit
-  my $rc = $dbh->commit;
-
-  $main::lxdebug->leave_sub();
-
-  return $rc;
-}
-
 sub assembly_item {
   $main::lxdebug->enter_sub();
 
@@ -647,7 +80,7 @@ sub assembly_item {
   while (my ($column, $table) = each(%columns)) {
     next unless ($form->{"${column}_$i"});
     $where .= qq| AND ${table}.${column} ILIKE ?|;
-    push(@values, '%' . $form->{"${column}_$i"} . '%');
+    push(@values, like($form->{"${column}_$i"}));
   }
 
   if ($form->{id}) {
@@ -667,18 +100,16 @@ sub assembly_item {
     $where .= qq| ORDER BY p.description|;
   }
 
-  # connect to database
-  my $dbh = $form->get_standard_dbh;
-
   my $query =
     qq|SELECT p.id, p.partnumber, p.description, p.sellprice,
+       p.classification_id,
        p.weight, p.onhand, p.unit, pg.partsgroup, p.lastcost,
-       p.price_factor_id, pfac.factor AS price_factor
+       p.price_factor_id, pfac.factor AS price_factor, p.notes as longdescription
        FROM parts p
        LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
        LEFT JOIN price_factors pfac ON pfac.id = p.price_factor_id
        WHERE $where|;
-  $form->{item_list} = selectall_hashref_query($form, $dbh, $query, @values);
+  $form->{item_list} = selectall_hashref_query($form, SL::DB->client->dbh, $query, @values);
 
   $main::lxdebug->leave_sub();
 }
@@ -697,6 +128,7 @@ sub assembly_item {
 #
 # column flags:
 #   l_partnumber l_description l_listprice l_sellprice l_lastcost l_priceupdate l_weight l_unit l_rop l_image l_drawing l_microfiche l_partsgroup
+#   l_warehouse  l_bin
 #
 # exclusives:
 #   itemstatus  = active | onhand | short | obsolete | orphaned
@@ -705,6 +137,8 @@ sub assembly_item {
 # joining filters:
 #   make model                               - makemodel
 #   serialnumber transdatefrom transdateto   - invoice/orderitems
+#   warehouse                                - warehouse
+#   bin                                      - bin
 #
 # binary flags:
 #   bought sold onorder ordered rfq quoted   - aggreg joins with invoices/orders
@@ -720,6 +154,8 @@ sub assembly_item {
 #   onhand                                   - as above, but masking the simple itemstatus results (doh!)
 #   warehouse onhand
 #   search by overrides of description
+#   soldtotal drops option default warehouse and bin
+#   soldtotal can not work if there are no documents checked
 #
 # disabled sanity checks and changes:
 #  - searchitems = assembly will no longer disable bought
@@ -736,6 +172,9 @@ sub all_parts {
   my ($self, $myconfig, $form) = @_;
   my $dbh = $form->get_standard_dbh($myconfig);
 
+  # sanity backend check
+  croak "Cannot combine soldtotal with default bin or default warehouse" if ($form->{l_soldtotal} && ($form->{l_bin} || $form->{l_warehouse}));
+
   $form->{parts}     = +{ };
   $form->{soldtotal} = undef if $form->{l_soldtotal}; # security fix. top100 insists on putting strings in there...
 
@@ -747,13 +186,14 @@ sub all_parts {
   my @like_filters         = (@simple_filters, @invoice_oi_filters);
   my @all_columns          = (@simple_filters, @makemodel_filters, @apoe_filters, @project_filters, qw(serialnumber));
   my @simple_l_switches    = (@all_columns, qw(notes listprice sellprice lastcost priceupdate weight unit rop image shop insertdate));
+  my %no_simple_l_switches = (warehouse => 'wh.description as warehouse', bin => 'bin.description as bin');
   my @oe_flags             = qw(bought sold onorder ordered rfq quoted);
   my @qsooqr_flags         = qw(invnumber ordnumber quonumber trans_id name module qty);
   my @deliverydate_flags   = qw(deliverydate);
 #  my @other_flags          = qw(onhand); # ToDO: implement these
 #  my @inactive_flags       = qw(l_subtotal short l_linetotal);
 
-  my @select_tokens = qw(id factor);
+  my @select_tokens = qw(id factor part_type classification_id);
   my @where_tokens  = qw(1=1);
   my @group_tokens  = ();
   my @bind_vars     = ();
@@ -781,8 +221,10 @@ sub all_parts {
          ) AS cv ON cv.id = apoe.customer_id OR cv.id = apoe.vendor_id|,
     mv         => 'LEFT JOIN vendor AS mv ON mv.id = mm.make',
     project    => 'LEFT JOIN project AS pj ON pj.id = COALESCE(ioi.project_id, apoe.globalproject_id)',
+    warehouse  => 'LEFT JOIN warehouse AS wh ON wh.id = p.warehouse_id',
+    bin        => 'LEFT JOIN bin ON bin.id = p.bin_id',
   );
-  my @join_order = qw(partsgroup makemodel mv invoice_oi apoe cv pfac project);
+  my @join_order = qw(partsgroup makemodel mv invoice_oi apoe cv pfac project warehouse bin);
 
   my %table_prefix = (
      deliverydate => 'apoe.', serialnumber => 'ioi.',
@@ -804,7 +246,7 @@ sub all_parts {
   );
 
   # if the join condition in these blocks are met, the column
-  # of the scecified table will gently override (coalesce actually) the original value
+  # of the specified table will gently override (coalesce actually) the original value
   # use it to conditionally coalesce values from subtables
   my @column_override = (
     #  column name,   prefix,  joins_needed,  nick name (in case column is named like another)
@@ -830,10 +272,6 @@ sub all_parts {
     insertdate         => 'itime::DATE',
   );
 
-  if (($form->{searchitems} eq 'assembly') && $form->{l_lastcost}) {
-    @simple_l_switches = grep { $_ ne 'lastcost' } @simple_l_switches;
-  }
-
   my $make_token_builder = sub {
     my $joins_needed = shift;
     sub {
@@ -858,18 +296,30 @@ sub all_parts {
   #===== switches and simple filters ========#
 
   # special case transdate
-  if (grep { $form->{$_} } qw(transdatefrom transdateto)) {
+  if (grep { trim($form->{$_}) } qw(transdatefrom transdateto)) {
     $form->{"l_transdate"} = 1;
     push @select_tokens, 'transdate';
     for (qw(transdatefrom transdateto)) {
-      next unless $form->{$_};
+      my $value = trim($form->{$_});
+      next unless $value;
       push @where_tokens, sprintf "transdate %s ?", /from$/ ? '>=' : '<=';
-      push @bind_vars,    $form->{$_};
+      push @bind_vars,    $value;
     }
   }
 
+  # special case smart search
+  if ($form->{all}) {
+    $form->{"l_$_"}       = 1 for qw(partnumber description unit sellprice lastcost linetotal);
+    $form->{l_service}    = 1 if $form->{searchitems} eq 'service'    || $form->{searchitems} eq '';
+    $form->{l_assembly}   = 1 if $form->{searchitems} eq 'assembly'   || $form->{searchitems} eq '';
+    $form->{l_part}       = 1 if $form->{searchitems} eq 'part'       || $form->{searchitems} eq '';
+    $form->{l_assortment} = 1 if $form->{searchitems} eq 'assortment' || $form->{searchitems} eq '';
+    push @where_tokens, "p.partnumber ILIKE ? OR p.description ILIKE ?";
+    push @bind_vars,    (like($form->{all})) x 2;
+  }
+
   # special case insertdate
-  if (grep { $form->{$_} } qw(insertdatefrom insertdateto)) {
+  if (grep { trim($form->{$_}) } qw(insertdatefrom insertdateto)) {
     $form->{"l_insertdate"} = 1;
     push @select_tokens, 'insertdate';
 
@@ -877,9 +327,10 @@ sub all_parts {
     my $token = $token_builder->('insertdate');
 
     for (qw(insertdatefrom insertdateto)) {
-      next unless $form->{$_};
+      my $value = trim($form->{$_});
+      next unless $value;
       push @where_tokens, sprintf "$token %s ?", /from$/ ? '>=' : '<=';
-      push @bind_vars,    $form->{$_};
+      push @bind_vars,    $value;
     }
   }
 
@@ -903,7 +354,7 @@ sub all_parts {
     next unless $form->{$_};
     $form->{"l_$_"} = '1'; # show the column
     push @where_tokens, "$table_prefix{$_}$_ ILIKE ?";
-    push @bind_vars,    "%$form->{$_}%";
+    push @bind_vars,    like($form->{$_});
   }
 
   foreach (@simple_l_switches) {
@@ -911,11 +362,23 @@ sub all_parts {
     push @select_tokens, $_;
   }
 
-  for ($form->{searchitems}) {
-    push @where_tokens, 'p.inventory_accno_id > 0'     if /part/;
-    push @where_tokens, 'p.inventory_accno_id IS NULL' if /service/;
-    push @where_tokens, 'NOT p.assembly'               if /service/;
-    push @where_tokens, '    p.assembly'               if /assembly/;
+  # Oder Bedingungen fuer Ware Dienstleistung Erzeugnis:
+  if ($form->{l_part} || $form->{l_assembly} || $form->{l_service} || $form->{l_assortment}) {
+      my @or_tokens = ();
+      push @or_tokens, "p.part_type = 'service'"    if $form->{l_service};
+      push @or_tokens, "p.part_type = 'assembly'"   if $form->{l_assembly};
+      push @or_tokens, "p.part_type = 'part'"       if $form->{l_part};
+      push @or_tokens, "p.part_type = 'assortment'" if $form->{l_assortment};
+      push @where_tokens, join ' OR ', map { "($_)" } @or_tokens;
+  }
+  else {
+      # gar keine Teile
+      push @where_tokens, q|'F' = 'T'|;
+  }
+
+  if ( $form->{classification_id} > 0 ) {
+    push @where_tokens, "p.classification_id = ?";
+    push @bind_vars, $form->{classification_id};
   }
 
   for ($form->{itemstatus}) {
@@ -935,7 +398,7 @@ sub all_parts {
         FROM assembly a_lc
         LEFT JOIN parts p_lc            ON (a_lc.parts_id        = p_lc.id)
         LEFT JOIN price_factors pfac_lc ON (p_lc.price_factor_id = pfac_lc.id)
-        WHERE (a_lc.id = p.id)) AS lastcost|;
+        WHERE (a_lc.id = p.id)) AS assembly_lastcost|;
   $table_prefix{$q_assembly_lastcost} = ' ';
 
   # special case makemodel search
@@ -944,11 +407,11 @@ sub all_parts {
   # fortunately makemodel doesn't need to be displayed later, so adding a special clause to where_token is sufficient.
   if ($form->{make}) {
     push @where_tokens, 'mv.name ILIKE ?';
-    push @bind_vars, "%$form->{make}%";
+    push @bind_vars, like($form->{make});
   }
   if ($form->{model}) {
     push @where_tokens, 'mm.model ILIKE ?';
-    push @bind_vars, "%$form->{model}%";
+    push @bind_vars, like($form->{model});
   }
 
   # special case: sorting by partnumber
@@ -973,7 +436,7 @@ sub all_parts {
 
   push @select_tokens, @qsooqr_flags, 'quotation', 'cv', 'ioi.id', 'ioi.ioi'  if $bsooqr;
   push @select_tokens, @deliverydate_flags                                    if $bsooqr && $form->{l_deliverydate};
-  push @select_tokens, $q_assembly_lastcost                                   if ($form->{searchitems} eq 'assembly') && $form->{l_lastcost};
+  push @select_tokens, $q_assembly_lastcost                                   if $form->{l_assembly} && $form->{l_lastcost};
   push @bsooqr_tokens, q|module = 'ir' AND NOT ioi.assemblyitem|              if $form->{bought};
   push @bsooqr_tokens, q|module = 'is' AND NOT ioi.assemblyitem|              if $form->{sold};
   push @bsooqr_tokens, q|module = 'oe' AND NOT quotation AND cv = 'customer'| if $form->{ordered};
@@ -990,6 +453,8 @@ sub all_parts {
   $joins_needed{cv}          = 1 if $bsooqr;
   $joins_needed{apoe}        = 1 if $joins_needed{project} || $joins_needed{cv}   || grep { $form->{$_} || $form->{"l_$_"} } @apoe_filters;
   $joins_needed{invoice_oi}  = 1 if $joins_needed{project} || $joins_needed{apoe} || grep { $form->{$_} || $form->{"l_$_"} } @invoice_oi_filters;
+  $joins_needed{bin}         = 1 if $form->{l_bin};
+  $joins_needed{warehouse}   = 1 if $form->{l_warehouse};
 
   # special case for description search.
   # up in the simple filter section the description filter got interpreted as something like: WHERE description ILIKE '%$form->{description}%'
@@ -1016,7 +481,7 @@ sub all_parts {
 
   my $token_builder = $make_token_builder->(\%joins_needed);
 
-  my @sort_cols    = (@simple_filters, qw(id priceupdate onhand invnumber ordnumber quonumber name serialnumber soldtotal deliverydate insertdate shop));
+  my @sort_cols    = (@simple_filters, qw(id onhand invnumber ordnumber quonumber name serialnumber soldtotal deliverydate insertdate shop));
      $form->{sort} = 'id' unless grep { $form->{"l_$_"} } grep { $form->{sort} eq $_ } @sort_cols; # sort by id if unknown or invisible column
   my $sort_order   = ($form->{revers} ? ' DESC' : ' ASC');
   my $order_clause = " ORDER BY " . $token_builder->($form->{sort}) . ($form->{revers} ? ' DESC' : ' ASC');
@@ -1026,6 +491,16 @@ sub all_parts {
   my $where_clause  = join ' AND ', map { "($_)" } @where_tokens;
   my $group_clause  = @group_tokens ? ' GROUP BY ' . join ', ',    map { $token_builder->($_) } @group_tokens : '';
 
+  # key of %no_simple_l_switch is the logical l_switch.
+  # the assigned value is the 'not so simple
+  # select token'
+  my $no_simple_select_clause;
+  foreach my $no_simple_l_switch (keys %no_simple_l_switches) {
+    next unless $form->{"l_${no_simple_l_switch}"};
+    $no_simple_select_clause .= ', '. $no_simple_l_switches{$no_simple_l_switch};
+  }
+  $select_clause .= $no_simple_select_clause;
+
   my %oe_flag_to_cvar = (
     bought   => 'invoice',
     sold     => 'invoice',
@@ -1047,6 +522,24 @@ sub all_parts {
     push @bind_vars, @cvar_values;
   }
 
+  # simple search for assemblies by items used in assemblies
+  if ($form->{bom} eq '2' && $form->{l_assembly}) {
+    # assembly_qty is the column name
+    $form->{l_assembly_qty} = 1;
+    # nuke where clause and bind vars
+    $where_clause = ' 1=1 AND p.id in (SELECT id from assembly where parts_id IN ' .
+                    ' (select id from parts where 1=1';
+    @bind_vars    = ();
+    # use only like filter for items used in assemblies
+    foreach (@like_filters) {
+      next unless $form->{$_};
+      $form->{"l_$_"} = '1'; # show the column
+      $where_clause .= " AND $_ ILIKE ? ";
+      push @bind_vars,    like($form->{$_});
+    }
+    $where_clause .='))';
+  }
+
   my $query = <<"  SQL";
     SELECT DISTINCT $select_clause
     FROM parts p
@@ -1056,7 +549,6 @@ sub all_parts {
     $order_clause
     $limit_clause
   SQL
-
   $form->{parts} = selectall_hashref_query($form, $dbh, $query, @bind_vars);
 
   map { $_->{onhand} *= 1 } @{ $form->{parts} };
@@ -1071,12 +563,12 @@ sub all_parts {
   # post processing for assembly parts lists (bom)
   # for each part get the assembly parts and add them into the partlist.
   my @assemblies;
-  if ($form->{searchitems} eq 'assembly' && $form->{bom}) {
+  if ($form->{l_assembly} && $form->{bom}) {
     $query =
-      qq|SELECT p.id, p.partnumber, p.description, a.qty AS onhand,
+      qq|SELECT p.id, p.partnumber, p.description, a.qty AS assembly_qty,
            p.unit, p.notes, p.itime::DATE as insertdate,
            p.sellprice, p.listprice, p.lastcost,
-           p.rop, p.weight, p.priceupdate,
+           p.rop, p.weight,
            p.image, p.drawing, p.microfiche,
            pfac.factor
          FROM parts p
@@ -1118,271 +610,11 @@ SQL
       }
       $sth->finish;
     }
-  };
-
-
-  $main::lxdebug->leave_sub();
-
-  return @{ $form->{parts} };
-}
-
-sub _create_filter_for_priceupdate {
-  $main::lxdebug->enter_sub();
-
-  my $self     = shift;
-  my $myconfig = \%main::myconfig;
-  my $form     = $main::form;
-
-  my @where_values;
-  my $where = '1 = 1';
-
-  foreach my $item (qw(partnumber drawing microfiche make model pg.partsgroup)) {
-    my $column = $item;
-    $column =~ s/.*\.//;
-    next unless ($form->{$column});
-
-    $where .= qq| AND $item ILIKE ?|;
-    push(@where_values, '%' . $form->{$column} . '%');
-  }
-
-  foreach my $item (qw(description serialnumber)) {
-    next unless ($form->{$item});
-
-    $where .= qq| AND (${item} ILIKE ?)|;
-    push(@where_values, '%' . $form->{$item} . '%');
-  }
-
-
-  # items which were never bought, sold or on an order
-  if ($form->{itemstatus} eq 'orphaned') {
-    $where .=
-      qq| AND (p.onhand = 0)
-          AND p.id NOT IN
-            (
-              SELECT DISTINCT parts_id FROM invoice
-              UNION
-              SELECT DISTINCT parts_id FROM assembly
-              UNION
-              SELECT DISTINCT parts_id FROM orderitems
-            )|;
-
-  } elsif ($form->{itemstatus} eq 'active') {
-    $where .= qq| AND p.obsolete = '0'|;
-
-  } elsif ($form->{itemstatus} eq 'obsolete') {
-    $where .= qq| AND p.obsolete = '1'|;
-
-  } elsif ($form->{itemstatus} eq 'onhand') {
-    $where .= qq| AND p.onhand > 0|;
-
-  } elsif ($form->{itemstatus} eq 'short') {
-    $where .= qq| AND p.onhand < p.rop|;
-
-  }
-
-  foreach my $column (qw(make model)) {
-    next unless ($form->{$column});
-    $where .= qq| AND p.id IN (SELECT DISTINCT parts_id FROM makemodel WHERE $column ILIKE ?|;
-    push(@where_values, '%' . $form->{$column} . '%');
-  }
-
-  $main::lxdebug->leave_sub();
-
-  return ($where, @where_values);
-}
-
-sub get_num_matches_for_priceupdate {
-  $main::lxdebug->enter_sub();
-
-  my $self     = shift;
-
-  my $myconfig = \%main::myconfig;
-  my $form     = $main::form;
-
-  my $dbh      = $form->get_standard_dbh($myconfig);
-
-  my ($where, @where_values) = $self->_create_filter_for_priceupdate();
-
-  my $num_updated = 0;
-  my $query;
-
-  for my $column (qw(sellprice listprice)) {
-    next if ($form->{$column} eq "");
-
-    $query =
-      qq|SELECT COUNT(*)
-         FROM parts
-         WHERE id IN
-           (SELECT p.id
-            FROM parts p
-            LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
-            WHERE $where)|;
-    my ($result)  = selectfirst_array_query($form, $dbh, $query, @where_values);
-    $num_updated += $result if (0 <= $result);
-  }
-
-  $query =
-    qq|SELECT COUNT(*)
-       FROM prices
-       WHERE parts_id IN
-         (SELECT p.id
-          FROM parts p
-          LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
-          WHERE $where) AND (pricegroup_id = ?)|;
-  my $sth = prepare_query($form, $dbh, $query);
-
-  for my $i (1 .. $form->{price_rows}) {
-    next if ($form->{"price_$i"} eq "");
-
-    my ($result)  = do_statement($form, $sth, $query, @where_values, conv_i($form->{"pricegroup_id_$i"}));
-    $num_updated += $result if (0 <= $result);
   }
-  $sth->finish();
 
   $main::lxdebug->leave_sub();
 
-  return $num_updated;
-}
-
-sub update_prices {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  my ($where, @where_values) = $self->_create_filter_for_priceupdate();
-  my $num_updated = 0;
-
-  # connect to database
-  my $dbh = $form->get_standard_dbh;
-
-  for my $column (qw(sellprice listprice)) {
-    next if ($form->{$column} eq "");
-
-    my $value = $form->parse_amount($myconfig, $form->{$column});
-    my $operator = '+';
-
-    if ($form->{"${column}_type"} eq "percent") {
-      $value = ($value / 100) + 1;
-      $operator = '*';
-    }
-
-    my $query =
-      qq|UPDATE parts SET $column = $column $operator ?
-         WHERE id IN
-           (SELECT p.id
-            FROM parts p
-            LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
-            WHERE $where)|;
-    my $result    = do_query($form, $dbh, $query, $value, @where_values);
-    $num_updated += $result if (0 <= $result);
-  }
-
-  my $q_add =
-    qq|UPDATE prices SET price = price + ?
-       WHERE parts_id IN
-         (SELECT p.id
-          FROM parts p
-          LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
-          WHERE $where) AND (pricegroup_id = ?)|;
-  my $sth_add = prepare_query($form, $dbh, $q_add);
-
-  my $q_multiply =
-    qq|UPDATE prices SET price = price * ?
-       WHERE parts_id IN
-         (SELECT p.id
-          FROM parts p
-          LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id)
-          WHERE $where) AND (pricegroup_id = ?)|;
-  my $sth_multiply = prepare_query($form, $dbh, $q_multiply);
-
-  for my $i (1 .. $form->{price_rows}) {
-    next if ($form->{"price_$i"} eq "");
-
-    my $value = $form->parse_amount($myconfig, $form->{"price_$i"});
-    my $result;
-
-    if ($form->{"pricegroup_type_$i"} eq "percent") {
-      $result = do_statement($form, $sth_multiply, $q_multiply, ($value / 100) + 1, @where_values, conv_i($form->{"pricegroup_id_$i"}));
-    } else {
-      $result = do_statement($form, $sth_add, $q_add, $value, @where_values, conv_i($form->{"pricegroup_id_$i"}));
-    }
-
-    $num_updated += $result if (0 <= $result);
-  }
-
-  $sth_add->finish();
-  $sth_multiply->finish();
-
-  my $rc= $dbh->commit;
-
-  $main::lxdebug->leave_sub();
-
-  return $num_updated;
-}
-
-sub create_links {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $module, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->get_standard_dbh;
-
-  my @values = ('%' . $module . '%');
-  my $query;
-
-  if ($form->{id}) {
-    $query =
-      qq|SELECT c.accno, c.description, c.link, c.id,
-           p.inventory_accno_id, p.income_accno_id, p.expense_accno_id
-         FROM chart c, parts p
-         WHERE (c.link LIKE ?) AND (p.id = ?)
-         ORDER BY c.accno|;
-    push(@values, conv_i($form->{id}));
-
-  } else {
-    $query =
-      qq|SELECT c.accno, c.description, c.link, c.id,
-           d.inventory_accno_id, d.income_accno_id, d.expense_accno_id
-         FROM chart c, defaults d
-         WHERE c.link LIKE ?
-         ORDER BY c.accno|;
-  }
-
-  my $sth = prepare_execute_query($form, $dbh, $query, @values);
-  while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
-    foreach my $key (split(/:/, $ref->{link})) {
-      if ($key =~ /\Q$module\E/) {
-        if (   ($ref->{id} eq $ref->{inventory_accno_id})
-            || ($ref->{id} eq $ref->{income_accno_id})
-            || ($ref->{id} eq $ref->{expense_accno_id})) {
-          push @{ $form->{"${module}_links"}{$key} },
-            { accno       => $ref->{accno},
-              description => $ref->{description},
-              selected    => "selected" };
-          $form->{"${key}_default"} = "$ref->{accno}--$ref->{description}";
-            } else {
-          push @{ $form->{"${module}_links"}{$key} },
-            { accno       => $ref->{accno},
-              description => $ref->{description},
-              selected    => "" };
-        }
-      }
-    }
-  }
-  $sth->finish;
-
-  # get buchungsgruppen
-  $form->{BUCHUNGSGRUPPEN} = selectall_hashref_query($form, $dbh, qq|SELECT id, description FROM buchungsgruppen|);
-
-  # get payment terms
-  $form->{payment_terms} = selectall_hashref_query($form, $dbh, qq|SELECT id, description FROM payment_terms ORDER BY sortkey|);
-
-  if (!$form->{id}) {
-    ($form->{priceupdate}) = selectrow_query($form, $dbh, qq|SELECT current_date|);
-  }
-
-  $main::lxdebug->leave_sub();
+  return $form->{parts};
 }
 
 # get partnumber, description, unit, sellprice and soldtotal with choice through $sortorder for Top100
@@ -1397,21 +629,22 @@ sub get_parts {
 
   if ($sortorder eq "all") {
     $where .= qq| AND (partnumber ILIKE ?) AND (description ILIKE ?)|;
-    push(@values, '%' . $form->{partnumber} . '%', '%' . $form->{description} . '%');
+    push(@values, like($form->{partnumber}), like($form->{description}));
 
   } elsif ($sortorder eq "partnumber") {
     $where .= qq| AND (partnumber ILIKE ?)|;
-    push(@values, '%' . $form->{partnumber} . '%');
+    push(@values, like($form->{partnumber}));
 
   } elsif ($sortorder eq "description") {
     $where .= qq| AND (description ILIKE ?)|;
-    push(@values, '%' . $form->{description} . '%');
+    push(@values, like($form->{description}));
     $order = "description";
 
   }
 
   my $query =
-    qq|SELECT id, partnumber, description, unit, sellprice
+    qq|SELECT id, partnumber, description, unit, sellprice,
+       classification_id
        FROM parts
        WHERE $where ORDER BY $order|;
 
@@ -1424,6 +657,8 @@ sub get_parts {
     }
 
     $j++;
+    $form->{"type_and_classific_$j"} = type_abbreviation($ref->{part_type}).
+                                       classification_abbreviation($ref->{classification_id});
     $form->{"id_$j"}          = $ref->{id};
     $form->{"partnumber_$j"}  = $ref->{partnumber};
     $form->{"description_$j"} = $ref->{description};
@@ -1454,39 +689,6 @@ sub get_soldtotal {
   return $sum;
 }    #end get_soldtotal
 
-sub retrieve_languages {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->get_standard_dbh;
-
-  my @values;
-  my $where;
-  my $query;
-
-  if ($form->{language_values} ne "") {
-    $query =
-      qq|SELECT l.id, l.description, tr.translation, tr.longdescription
-         FROM language l
-         LEFT OUTER JOIN translation tr ON (tr.language_id = l.id) AND (tr.parts_id = ?)
-         ORDER BY lower(l.description)|;
-    @values = (conv_i($form->{id}));
-
-  } else {
-    $query = qq|SELECT id, description
-                FROM language
-                ORDER BY lower(description)|;
-  }
-
-  my $languages = selectall_hashref_query($form, $dbh, $query, @values);
-
-  $main::lxdebug->leave_sub();
-
-  return $languages;
-}
-
 sub follow_account_chain {
   $main::lxdebug->enter_sub(2);
 
@@ -1534,37 +736,14 @@ sub retrieve_accounts {
 
   # transdate madness.
   my $transdate = "";
-  if ($form->{type} eq "invoice" or $form->{type} eq "credit_note") {
+  if ( (any {$form->{type} eq $_} qw(invoice credit_note invoice_for_advance_payment final_invoice)) or ($form->{script} eq 'ir.pl') ) {
     # use deliverydate for sales and purchase invoice, if it exists
     # also use deliverydate for credit notes
-    if (!$form->{deliverydate}) {
-      $transdate = $form->{invdate};
-    } else {
-      $transdate = $form->{deliverydate};
-    }
-  } elsif ($form->{script} eq 'ir.pl') {
-    # when a purchase invoice is opened from the report of purchase invoices
-    # $form->{type} isn't set, but $form->{script} is, not sure why this is or
-    # whether this distinction matters in some other scenario. Otherwise one
-    # could probably take out this elsif and add a
-    # " or $form->{script} eq 'ir.pl' "
-    # to the above if-statement
-    if (!$form->{deliverydate}) {
-      $transdate = $form->{invdate};
-    } else {
-      $transdate = $form->{deliverydate};
-    }
-  } elsif (($form->{type} eq "credit_note") and $form->{deliverydate}) {
-    # if credit_note has a deliverydate, use this instead of invdate
-    # useful for credit_notes of invoices from an old period with different tax
-    # if there is no deliverydate then invdate is used, old default (see next elsif)
-    # Falls hier der Stichtag für Steuern anders bestimmt wird,
-    # entsprechend auch bei Taxkeys.pm anpassen
-    $transdate = $form->{deliverydate};
-  } elsif (($form->{type} eq "credit_note") || ($form->{script} eq 'ir.pl')) {
-    $transdate = $form->{invdate};
+    $transdate = $form->{tax_point} || $form->{deliverydate} || $form->{invdate};
   } else {
-    $transdate = $form->{transdate};
+    my $deliverydate;
+    $deliverydate = $form->{reqdate} if any { $_ eq $form->{type} } qw(sales_order request_quotation purchase_order);
+    $transdate = $form->{tax_point} || $deliverydate || $form->{transdate};
   }
 
   if ($transdate eq "") {
@@ -1581,7 +760,7 @@ sub retrieve_accounts {
   my %accno_by_part = map { $_->{id} => $_ }
     selectall_hashref_query($form, $dbh, <<SQL, @part_ids);
     SELECT
-      p.id, p.inventory_accno_id AS is_part,
+      p.id, p.part_type,
       bg.inventory_accno_id,
       tc.income_accno_id AS income_accno_id,
       tc.expense_accno_id AS expense_accno_id,
@@ -1600,8 +779,9 @@ sub retrieve_accounts {
     p.id IN ($in)
 SQL
 
-  my $sth_tax = prepare_query($::form, $dbh, <<SQL);
-    SELECT c.accno, t.taxdescription AS description, t.rate, t.taxnumber
+  my $query_tax = <<SQL;
+    SELECT c.accno, t.taxdescription AS description, t.id as tax_id, t.rate,
+           c.accno as taxnumber
     FROM tax t
     LEFT JOIN chart c ON c.id = t.chart_id
     WHERE t.id IN
@@ -1610,11 +790,12 @@ SQL
        WHERE tk.chart_id = ? AND startdate <= ?
        ORDER BY startdate DESC LIMIT 1)
 SQL
+  my $sth_tax = prepare_query($::form, $dbh, $query_tax);
 
   while (my ($index => $part_id) = each %args) {
     my $ref = $accno_by_part{$part_id} or next;
 
-    $ref->{"inventory_accno_id"} = undef unless $ref->{"is_part"};
+    $ref->{"inventory_accno_id"} = undef unless $ref->{"part_type"} eq 'part';
 
     my %accounts;
     for my $type (qw(inventory income expense)) {
@@ -1625,13 +806,13 @@ SQL
 
     $form->{"${_}_accno_$index"} = $accounts{"${_}_accno"} for qw(inventory income expense);
 
-    $sth_tax->execute($accounts{$inc_exp}, quote_db_date($transdate));
+    $sth_tax->execute($accounts{$inc_exp}, quote_db_date($transdate)) || $::form->dberror($query_tax);
     $ref = $sth_tax->fetchrow_hashref or next;
 
     $form->{"taxaccounts_$index"} = $ref->{"accno"};
     $form->{"taxaccounts"} .= "$ref->{accno} "if $form->{"taxaccounts"} !~ /$ref->{accno}/;
 
-    $form->{"$ref->{accno}_${_}"} = $ref->{$_} for qw(rate description taxnumber);
+    $form->{"$ref->{accno}_${_}"} = $ref->{$_} for qw(rate description taxnumber tax_id);
   }
 
   $sth_tax->finish;
@@ -1715,6 +896,25 @@ sub prepare_parts_for_printing {
 
   $sth->finish();
 
+  $query           = qq|SELECT
+                        cp.parts_id,
+                        cp.customer_partnumber AS customer_model,
+                        c.name                 AS customer_make
+                        FROM part_customer_prices cp
+                        LEFT JOIN customer c ON (cp.customer_id = c.id)
+                        WHERE cp.parts_id IN ($placeholders)|;
+
+  my %customermodel = ();
+
+  $sth              = prepare_execute_query($form, $dbh, $query, @part_ids);
+
+  while (my $ref = $sth->fetchrow_hashref()) {
+    $customermodel{$ref->{parts_id}} ||= [];
+    push @{ $customermodel{$ref->{parts_id}} }, $ref;
+  }
+
+  $sth->finish();
+
   my @columns = qw(ean image microfiche drawing);
 
   $query      = qq|SELECT id, | . join(', ', @columns) . qq|
@@ -1724,7 +924,7 @@ sub prepare_parts_for_printing {
   my %data    = selectall_as_map($form, $dbh, $query, 'id', \@columns, @part_ids);
 
   my %template_arrays;
-  map { $template_arrays{$_} = [] } (qw(make model), @columns);
+  map { $template_arrays{$_} = [] } (qw(make model customer_make customer_model), @columns);
 
   foreach my $i (1 .. $rowcount) {
     my $id = $form->{"${prefix}${i}"};
@@ -1738,11 +938,21 @@ sub prepare_parts_for_printing {
     push @{ $template_arrays{make} },  [];
     push @{ $template_arrays{model} }, [];
 
-    next if (!$makemodel{$id});
+    if ($makemodel{$id}) {
+      foreach my $ref (@{ $makemodel{$id} }) {
+        map { push @{ $template_arrays{$_}->[-1] }, $ref->{$_} } qw(make model);
+      }
+    }
+
+    push @{ $template_arrays{customer_make} },  [];
+    push @{ $template_arrays{customer_model} }, [];
 
-    foreach my $ref (@{ $makemodel{$id} }) {
-      map { push @{ $template_arrays{$_}->[-1] }, $ref->{$_} } qw(make model);
+    if ($customermodel{$id}) {
+      foreach my $ref (@{ $customermodel{$id} }) {
+        push @{ $template_arrays{$_}->[-1] }, $ref->{$_} for qw(customer_make customer_model);
+      }
     }
+
   }
 
   my $parts = SL::DB::Manager::Part->get_all(query => [ id => \@part_ids ]);
@@ -1751,32 +961,16 @@ sub prepare_parts_for_printing {
   for my $i (1..$rowcount) {
     my $id = $form->{"${prefix}${i}"};
     next unless $id;
-
-    push @{ $template_arrays{part_type} },  $parts_by_id{$id}->type;
+    my $prt = $parts_by_id{$id};
+    my $type_abbr = type_abbreviation($prt->part_type);
+    push @{ $template_arrays{part_type}         }, $prt->part_type;
+    push @{ $template_arrays{part_abbreviation} }, $type_abbr;
+    push @{ $template_arrays{type_and_classific}}, $type_abbr . classification_abbreviation($prt->classification_id);
+    push @{ $template_arrays{separate}  }, separate_abbreviation($prt->classification_id);
   }
 
-  return %template_arrays;
   $main::lxdebug->leave_sub();
+  return %template_arrays;
 }
 
-sub normalize_text_blocks {
-  $main::lxdebug->enter_sub();
-
-  my $self     = shift;
-  my %params   = @_;
-
-  my $form     = $params{form}     || $main::form;
-
-  # check if feature is enabled (select normalize_part_descriptions from defaults)
-  return unless ($::instance_conf->get_normalize_part_descriptions);
-
-  foreach (qw(description notes)) {
-    $form->{$_} =~ s/\s+$//s;
-    $form->{$_} =~ s/^\s+//s;
-    $form->{$_} =~ s/ {2,}/ /g;
-  }
-   $main::lxdebug->leave_sub();
-}
-
-
 1;
index b840e45..c0d6987 100644 (file)
--- a/SL/IO.pm
+++ b/SL/IO.pm
@@ -4,6 +4,7 @@ use List::Util qw(first);
 use List::MoreUtils qw(any);
 
 use SL::DBUtils;
+use SL::DB;
 
 use strict;
 
@@ -40,36 +41,38 @@ sub set_datepaid {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
-  my $id       = conv_i($params{id});
-  my $table    = (any { $_ eq $params{table} } qw(ar ap gl)) ? $params{table} : 'ar';
+  SL::DB->client->with_transaction(sub {
+    my $dbh      = $params{dbh} || SL::DB->client->dbh;
+    my $id       = conv_i($params{id});
+    my $table    = (any { $_ eq $params{table} } qw(ar ap gl)) ? $params{table} : 'ar';
 
-  my ($curr_datepaid, $curr_paid) = selectfirst_array_query($form, $dbh, qq|SELECT datepaid, paid FROM $table WHERE id = ?|, $id);
+    my ($curr_datepaid, $curr_paid) = selectfirst_array_query($form, $dbh, qq|SELECT datepaid, paid FROM $table WHERE id = ?|, $id);
 
-  my $query    = <<SQL;
-    SELECT MAX(at.transdate)
-    FROM acc_trans at
-    LEFT JOIN chart c ON (at.chart_id = c.id)
-    WHERE (at.trans_id = ?)
-      AND (c.link LIKE '%paid%')
+    my $query    = <<SQL;
+      SELECT MAX(at.transdate)
+      FROM acc_trans at
+      LEFT JOIN chart c ON (at.chart_id = c.id)
+      WHERE (at.trans_id = ?)
+        AND (c.link LIKE '%paid%')
 SQL
 
-  my ($max_acc_trans_date) = selectfirst_array_query($form, $dbh, $query, $id);
+    my ($max_acc_trans_date) = selectfirst_array_query($form, $dbh, $query, $id);
 
-  if ($max_acc_trans_date && ($max_acc_trans_date ne $curr_datepaid)) {
-    # 1. Fall: Es gab mindestens eine Zahlung, und das Datum der Zahlung entspricht nicht
-    # dem vermerkten Zahlungsdatum.
-    do_query($form, $dbh, qq|UPDATE $table SET datepaid = ? WHERE id = ?|, $max_acc_trans_date, $id);
+    if ($max_acc_trans_date && ($max_acc_trans_date ne $curr_datepaid)) {
+      # 1. Fall: Es gab mindestens eine Zahlung, und das Datum der Zahlung entspricht nicht
+      # dem vermerkten Zahlungsdatum.
+      do_query($form, $dbh, qq|UPDATE $table SET datepaid = ? WHERE id = ?|, $max_acc_trans_date, $id);
 
-  } elsif (!$max_acc_trans_date && ($curr_paid * 1)) {
-    # 2. Fall: Es gab keine Zahlung, aber paid ist nicht 0. Das ist z.B. der Fall, wenn
-    # die Funktion "als bezahlt buchen" verwendet oder wenn ein Beleg storniert wird.
-    # In diesem Fall das letzte Modifikationsdatum als Bezahldatum nehmen, oder aber das
-    # Erstelldatum, wenn keine Modifikation erfolgt ist (bei Stornos z.B.).
-    do_query($form, $dbh, qq|UPDATE $table SET datepaid = COALESCE(mtime::date, itime::date) WHERE id = ?|, $id);
-  }
+    } elsif (!$max_acc_trans_date && ($curr_paid * 1)) {
+      # 2. Fall: Es gab keine Zahlung, aber paid ist nicht 0. Das ist z.B. der Fall, wenn
+      # die Funktion "als bezahlt buchen" verwendet oder wenn ein Beleg storniert wird.
+      # In diesem Fall das letzte Modifikationsdatum als Bezahldatum nehmen, oder aber das
+      # Erstelldatum, wenn keine Modifikation erfolgt ist (bei Stornos z.B.).
+      do_query($form, $dbh, qq|UPDATE $table SET datepaid = COALESCE(mtime::date, itime::date) WHERE id = ?|, $id);
+    }
 
-  $dbh->commit() unless $params{dbh};
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
index fbff03e..3d8ee36 100644 (file)
--- a/SL/IR.pm
+++ b/SL/IR.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Inventory received module
@@ -40,6 +41,7 @@ use SL::Common;
 use SL::CVar;
 use SL::DATEV qw(:CONSTANTS);
 use SL::DBUtils;
+use SL::DB::Draft;
 use SL::DO;
 use SL::GenericTranslations;
 use SL::HTML::Restrict;
@@ -47,17 +49,31 @@ use SL::IO;
 use SL::MoreCommon;
 use SL::DB::Default;
 use SL::DB::TaxZone;
+use SL::DB::MakeModel;
+use SL::DB;
+use SL::Presenter::Part qw(type_abbreviation classification_abbreviation);
 use List::Util qw(min);
 
 use strict;
+use constant PCLASS_OK             =>   0;
+use constant PCLASS_NOTFORSALE     =>   1;
+use constant PCLASS_NOTFORPURCHASE =>   2;
 
 sub post_invoice {
+  my ($self, $myconfig, $form, $provided_dbh, %params) = @_;
   $main::lxdebug->enter_sub();
 
-  my ($self, $myconfig, $form, $provided_dbh, $payments_only) = @_;
+  my $rc = SL::DB->client->with_transaction(\&_post_invoice, $self, $myconfig, $form, $provided_dbh, %params);
 
-  # connect to database, turn off autocommit
-  my $dbh = $provided_dbh ? $provided_dbh : $form->dbconnect_noauto($myconfig);
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _post_invoice {
+  my ($self, $myconfig, $form, $provided_dbh, %params) = @_;
+
+  my $payments_only = $params{payments_only};
+  my $dbh = $provided_dbh || SL::DB->client->dbh;
   my $restricter = SL::HTML::Restrict->create;
 
   $form->{defaultcurrency} = $form->get_default_currency($myconfig);
@@ -418,7 +434,7 @@ SQL
                                 dbh          => $dbh);
 
     # link previous items with invoice items See IS.pm (no credit note -> no invoice item)
-    foreach (qw(delivery_order_items orderitems)) {
+    foreach (qw(delivery_order_items orderitems invoice)) {
       if (!$form->{useasnew} && $form->{"converted_from_${_}_id_$i"}) {
         RecordLinks->create_links('dbh'        => $dbh,
                                   'mode'       => 'ids',
@@ -533,6 +549,7 @@ SQL
     if ($form->{currency} ne $defaultcurrency) && !$exchangerate;
 
 # record acc_trans transactions
+  my $taxdate = $form->{tax_point} || $form->{deliverydate} || $form->{invdate};
   foreach my $trans_id (keys %{ $form->{amount} }) {
     foreach my $accno (keys %{ $form->{amount}{$trans_id} }) {
       $form->{amount}{$trans_id}{$accno} = $form->round_amount($form->{amount}{$trans_id}{$accno}, 2);
@@ -559,7 +576,7 @@ SQL
                    ORDER BY startdate DESC LIMIT 1),
                   (SELECT link FROM chart WHERE accno = ?))|;
       @values = ($trans_id, $accno, $form->{amount}{$trans_id}{$accno},
-                 conv_date($form->{invdate}), $accno, conv_date($form->{invdate}), $project_id, $accno, conv_date($form->{invdate}), $accno);
+                 conv_date($form->{invdate}), $accno, conv_date($taxdate), $project_id, $accno, conv_date($taxdate), $accno);
       do_query($form, $dbh, $query, @values);
     }
   }
@@ -576,6 +593,8 @@ SQL
 
   $form->{amount}{ $form->{id} }{ $form->{AP} } = $form->{paid} if $form->{amount}{$form->{id}}{$form->{AP}} == 0;
 
+  my %already_cleared = %{ $params{already_cleared} // {} };
+
   # record payments and offsetting AP
   for my $i (1 .. $form->{paidaccounts}) {
     if ($form->{"acc_trans_id_$i"}
@@ -592,9 +611,16 @@ SQL
 
     $amount = $form->round_amount($form->{"paid_$i"} * $form->{exchangerate} + $paiddiff, 2) * -1;
 
+    my $new_cleared = !$form->{"acc_trans_id_$i"}                                                  ? 'f'
+                    : !$already_cleared{$form->{"acc_trans_id_$i"}}                                ? 'f'
+                    : $already_cleared{$form->{"acc_trans_id_$i"}}->{amount} != $form->{"paid_$i"} ? 'f'
+                    : $already_cleared{$form->{"acc_trans_id_$i"}}->{accno}  != $accno             ? 'f'
+                    : $already_cleared{$form->{"acc_trans_id_$i"}}->{cleared}                      ? 't'
+                    :                                                                                'f';
+
     # record AP
     if ($form->{amount}{ $form->{id} }{ $form->{AP} } != 0) {
-      $query = qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, taxkey, project_id, tax_id, chart_link)
+      $query = qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, taxkey, project_id, cleared, tax_id, chart_link)
                   VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?,
                           (SELECT taxkey_id
                            FROM taxkeys
@@ -603,7 +629,7 @@ SQL
                                             WHERE accno = ?)
                            AND startdate <= ?
                            ORDER BY startdate DESC LIMIT 1),
-                          ?,
+                          ?, ?,
                           (SELECT tax_id
                            FROM taxkeys
                            WHERE chart_id= (SELECT id
@@ -613,7 +639,7 @@ SQL
                            ORDER BY startdate DESC LIMIT 1),
                           (SELECT link FROM chart WHERE accno = ?))|;
       @values = (conv_i($form->{id}), $form->{AP}, $amount,
-                 $form->{"datepaid_$i"}, $form->{AP}, conv_date($form->{"datepaid_$i"}), $project_id, $form->{AP}, conv_date($form->{"datepaid_$i"}), $form->{AP});
+                 $form->{"datepaid_$i"}, $form->{AP}, conv_date($form->{"datepaid_$i"}), $project_id, $new_cleared, $form->{AP}, conv_date($form->{"datepaid_$i"}), $form->{AP});
       do_query($form, $dbh, $query, @values);
     }
 
@@ -621,7 +647,7 @@ SQL
     my $gldate = (conv_date($form->{"gldate_$i"}))? conv_date($form->{"gldate_$i"}) : conv_date($form->current_date($myconfig));
 
     $query =
-      qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, gldate, source, memo, taxkey, project_id, tax_id, chart_link)
+      qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, gldate, source, memo, taxkey, project_id, cleared, tax_id, chart_link)
                 VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?, ?, ?, ?,
                 (SELECT taxkey_id
                  FROM taxkeys
@@ -629,7 +655,7 @@ SQL
                                   FROM chart WHERE accno = ?)
                  AND startdate <= ?
                  ORDER BY startdate DESC LIMIT 1),
-                ?,
+                ?, ?,
                 (SELECT tax_id
                  FROM taxkeys
                  WHERE chart_id= (SELECT id
@@ -638,7 +664,7 @@ SQL
                  ORDER BY startdate DESC LIMIT 1),
                 (SELECT link FROM chart WHERE accno = ?))|;
     @values = (conv_i($form->{id}), $accno, $form->{"paid_$i"}, $form->{"datepaid_$i"},
-               $gldate, $form->{"source_$i"}, $form->{"memo_$i"}, $accno, conv_date($form->{"datepaid_$i"}), $project_id, $accno, conv_date($form->{"datepaid_$i"}), $accno);
+               $gldate, $form->{"source_$i"}, $form->{"memo_$i"}, $accno, conv_date($form->{"datepaid_$i"}), $project_id, $new_cleared, $accno, conv_date($form->{"datepaid_$i"}), $accno);
     do_query($form, $dbh, $query, @values);
 
     $exchangerate = 0;
@@ -690,13 +716,8 @@ SQL
   if ($payments_only) {
     $query = qq|UPDATE ap SET paid = ? WHERE id = ?|;
     do_query($form, $dbh, $query, $form->{paid}, conv_i($form->{id}));
+    $form->new_lastmtime('ap');
 
-    if (!$provided_dbh) {
-      $dbh->commit();
-      $dbh->disconnect();
-    }
-
-    $main::lxdebug->leave_sub();
     return;
   }
 
@@ -710,21 +731,25 @@ SQL
 
   # save AP record
   $query = qq|UPDATE ap SET
-                invnumber    = ?, ordnumber   = ?, quonumber     = ?, transdate   = ?,
-                orddate      = ?, quodate     = ?, vendor_id     = ?, amount      = ?,
-                netamount    = ?, paid        = ?, duedate       = ?,
-                invoice      = ?, taxzone_id  = ?, notes         = ?, taxincluded = ?,
-                intnotes     = ?, storno_id   = ?, storno        = ?,
+                invnumber    = ?, ordnumber   = ?, quonumber     = ?, transdate    = ?,
+                orddate      = ?, quodate     = ?, vendor_id     = ?, amount       = ?,
+                netamount    = ?, paid        = ?, duedate       = ?, deliverydate = ?,
+                invoice      = ?, taxzone_id  = ?, notes         = ?, taxincluded  = ?,
+                intnotes     = ?, storno_id   = ?, storno        = ?, tax_point    = ?,
                 cp_id        = ?, employee_id = ?, department_id = ?, delivery_term_id = ?,
+                payment_id   = ?, transaction_description        = ?,
+                currency_id = (SELECT id FROM currencies WHERE name = ?),
                 globalproject_id = ?, direct_debit = ?
               WHERE id = ?|;
   @values = (
                 $form->{invnumber},          $form->{ordnumber},           $form->{quonumber},      conv_date($form->{invdate}),
       conv_date($form->{orddate}), conv_date($form->{quodate}),     conv_i($form->{vendor_id}),               $amount,
-                $netamount,                  $form->{paid},      conv_date($form->{duedate}),
+                $netamount,                  $form->{paid},        conv_date($form->{duedate}),     conv_date($form->{deliverydate}),
             '1',                             $taxzone_id, $restricter->process($form->{notes}),               $form->{taxincluded} ? 't' : 'f',
-                $form->{intnotes},           conv_i($form->{storno_id}),     $form->{storno}      ? 't' : 'f',
+                $form->{intnotes},           conv_i($form->{storno_id}),     $form->{storno}      ? 't' : 'f', conv_date($form->{tax_point}),
          conv_i($form->{cp_id}),      conv_i($form->{employee_id}), conv_i($form->{department_id}), conv_i($form->{delivery_term_id}),
+         conv_i($form->{payment_id}), $form->{transaction_description},
+                $form->{"currency"},
          conv_i($form->{globalproject_id}),
                 $form->{direct_debit} ? 't' : 'f',
          conv_i($form->{id})
@@ -745,6 +770,7 @@ SQL
     do_query($form, $dbh, $query, conv_i($form->{id}));
   }
 
+  $form->new_lastmtime('ap');
 
   $form->{name} = $form->{vendor};
   $form->{name} =~ s/--\Q$form->{vendor_id}\E//;
@@ -757,16 +783,18 @@ SQL
 
   Common::webdav_folder($form);
 
-  # Link this record to the records it was created from.
-  if ($form->{convert_from_oe_ids}) {
-    RecordLinks->create_links('dbh'        => $dbh,
-                              'mode'       => 'ids',
-                              'from_table' => 'oe',
-                              'from_ids'   => $form->{convert_from_oe_ids},
-                              'to_table'   => 'ap',
-                              'to_id'      => $form->{id},
+  # Link this record to the records it was created from order or invoice (storno)
+  foreach (qw(oe ap)) {
+    if ($form->{"convert_from_${_}_ids"}) {
+      RecordLinks->create_links('dbh'        => $dbh,
+                                'mode'       => 'ids',
+                                'from_table' => $_,
+                                'from_ids'   => $form->{"convert_from_${_}_ids"},
+                                'to_table'   => 'ap',
+                                'to_id'      => $form->{id},
       );
-    delete $form->{convert_from_oe_ids};
+      delete $form->{"convert_from_${_}_ids"};
+    }
   }
 
   my @convert_from_do_ids = map { $_ * 1 } grep { $_ } split m/\s+/, $form->{convert_from_do_ids};
@@ -798,35 +826,26 @@ SQL
     do_query($form, $dbh, $query, @orphaned_ids);
   }
 
+  if ($form->{draft_id}) {
+    SL::DB::Manager::Draft->delete_all(where => [ id => delete($form->{draft_id}) ]);
+  }
+
   # safety check datev export
   if ($::instance_conf->get_datev_check_on_purchase_invoice) {
-    my $transdate = $::form->{invdate} ? DateTime->from_lxoffice($::form->{invdate}) : undef;
-    $transdate  ||= DateTime->today;
 
     my $datev = SL::DATEV->new(
-      exporttype => DATEV_ET_BUCHUNGEN,
-      format     => DATEV_FORMAT_KNE,
       dbh        => $dbh,
       trans_id   => $form->{id},
     );
 
-    $datev->export;
+    $datev->generate_datev_data;
 
     if ($datev->errors) {
-      $dbh->rollback;
       die join "\n", $::locale->text('DATEV check returned errors:'), $datev->errors;
     }
   }
 
-  my $rc = 1;
-  if (!$provided_dbh) {
-    $rc = $dbh->commit();
-    $dbh->disconnect();
-  }
-
-  $main::lxdebug->leave_sub();
-
-  return $rc;
+  return 1;
 }
 
 sub reverse_invoice {
@@ -836,7 +855,7 @@ sub reverse_invoice {
 
   # reverse inventory items
   my $query =
-    qq|SELECT i.parts_id, p.inventory_accno_id, p.expense_accno_id, i.qty, i.allocated, i.sellprice
+    qq|SELECT i.parts_id, p.part_type, i.qty, i.allocated, i.sellprice
        FROM invoice i, parts p
        WHERE (i.parts_id = p.id)
          AND (i.trans_id = ?)|;
@@ -847,7 +866,7 @@ sub reverse_invoice {
   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
     $netamount += $form->round_amount($ref->{sellprice} * $ref->{qty} * -1, 2);
 
-    next unless $ref->{inventory_accno_id};
+    next unless $ref->{part_type} eq 'part';
 
     # if $ref->{allocated} > 0 than we sold that many items
     next if ($ref->{allocated} <= 0);
@@ -912,31 +931,30 @@ sub delete_invoice {
   my ($self, $myconfig, $form) = @_;
   my $query;
   # connect to database
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
-  &reverse_invoice($dbh, $form);
+  SL::DB->client->with_transaction(sub{
 
-  my @values = (conv_i($form->{id}));
-
-  # delete zero entries
-  # wtf? use case for this?
-  $query = qq|DELETE FROM acc_trans WHERE amount = 0|;
-  do_query($form, $dbh, $query);
+    &reverse_invoice($dbh, $form);
 
+    my @values = (conv_i($form->{id}));
 
-  my @queries = (
-    qq|DELETE FROM invoice WHERE trans_id = ?|,
-    qq|DELETE FROM ap WHERE id = ?|,
-  );
+    # delete zero entries
+    # wtf? use case for this?
+    $query = qq|DELETE FROM acc_trans WHERE amount = 0|;
+    do_query($form, $dbh, $query);
 
-  map { do_query($form, $dbh, $_, @values) } @queries;
 
-  my $rc = $dbh->commit;
-  $dbh->disconnect;
+    my @queries = (
+      qq|DELETE FROM invoice WHERE trans_id = ?|,
+      qq|DELETE FROM ap WHERE id = ?|,
+    );
 
-  $main::lxdebug->leave_sub();
+    map { do_query($form, $dbh, $_, @values) } @queries;
+    1;
+  }) or do { die SL::DB->client->error };
 
-  return $rc;
+  return 1;
 }
 
 sub retrieve_invoice {
@@ -945,7 +963,7 @@ sub retrieve_invoice {
   my ($self, $myconfig, $form) = @_;
 
   # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my ($query, $sth, $ref, $q_invdate);
 
@@ -977,7 +995,6 @@ sub retrieve_invoice {
   map { $form->{$_} = $ref->{$_} } keys %$ref;
 
   if (!$form->{id}) {
-    $dbh->disconnect();
     $main::lxdebug->leave_sub();
 
     return;
@@ -985,11 +1002,11 @@ sub retrieve_invoice {
 
   # retrieve invoice
   $query = qq|SELECT cp_id, invnumber, transdate AS invdate, duedate,
-                orddate, quodate, globalproject_id,
+                orddate, quodate, deliverydate, tax_point, globalproject_id,
                 ordnumber, quonumber, paid, taxincluded, notes, taxzone_id, storno, gldate,
                 mtime, itime,
                 intnotes, (SELECT cu.name FROM currencies cu WHERE cu.id=ap.currency_id) AS currency, direct_debit,
-                delivery_term_id
+                payment_id, delivery_term_id, transaction_description
               FROM ap
               WHERE id = ?|;
   $ref = selectfirst_hashref_query($form, $dbh, $query, conv_i($form->{id}));
@@ -1005,7 +1022,7 @@ sub retrieve_invoice {
   delete $ref->{id};
   map { $form->{$_} = $ref->{$_} } keys %$ref;
 
-  my $transdate  = $form->{invdate} ? $dbh->quote($form->{invdate}) : "current_date";
+  my $transdate  = $form->{tax_point} ? $dbh->quote($form->{tax_point}) :$form->{invdate} ? $dbh->quote($form->{invdate}) : "current_date";
 
   my $taxzone_id = $form->{taxzone_id} * 1;
   $taxzone_id = SL::DB::Manager::TaxZone->get_default->id unless SL::DB::Manager::TaxZone->find_by(id => $taxzone_id);
@@ -1020,7 +1037,8 @@ sub retrieve_invoice {
         i.id AS invoice_id,
         i.description, i.longdescription, i.qty, i.fxsellprice AS sellprice, i.parts_id AS id, i.unit, i.deliverydate, i.project_id, i.serialnumber,
         i.price_factor_id, i.price_factor, i.marge_price_factor, i.discount, i.active_price_source, i.active_discount_source,
-        p.partnumber, p.inventory_accno_id AS part_inventory_accno_id,  pr.projectnumber, pg.partsgroup
+        p.partnumber, p.part_type, pr.projectnumber, pg.partsgroup
+        ,p.classification_id
 
         FROM invoice i
         JOIN parts p ON (i.parts_id = p.id)
@@ -1044,8 +1062,7 @@ sub retrieve_invoice {
                                           );
     map { $ref->{"ic_cvar_$_->{name}"} = $_->{value} } @{ $cvars };
 
-    map({ delete($ref->{$_}); } qw(inventory_accno inventory_new_chart inventory_valid)) if !$ref->{"part_inventory_accno_id"};
-    delete($ref->{"part_inventory_accno_id"});
+    map({ delete($ref->{$_}); } qw(inventory_accno inventory_new_chart inventory_valid)) if !$ref->{"part_type"} eq 'part';
 
     foreach my $type (qw(inventory income expense)) {
       while ($ref->{"${type}_new_chart"} && ($ref->{"${type}_valid"} >=0)) {
@@ -1057,7 +1074,9 @@ sub retrieve_invoice {
     # get tax rates and description
     my $accno_id = ($form->{vc} eq "customer") ? $ref->{income_accno} : $ref->{expense_accno};
     $query =
-      qq|SELECT c.accno, t.taxdescription, t.rate, t.taxnumber FROM tax t
+      qq|SELECT c.accno, t.taxdescription, t.rate, t.id as tax_id,
+                c.accno as taxnumber   -- taxnumber is same as accno, but still accessed as taxnumber in code
+         FROM tax t
          LEFT JOIN chart c ON (c.id = t.chart_id)
          WHERE t.id in
            (SELECT tk.tax_id FROM taxkeys tk
@@ -1082,6 +1101,7 @@ sub retrieve_invoice {
         $form->{"$ptr->{accno}_rate"}         = $ptr->{rate};
         $form->{"$ptr->{accno}_description"}  = $ptr->{taxdescription};
         $form->{"$ptr->{accno}_taxnumber"}    = $ptr->{taxnumber};
+        $form->{"$ptr->{accno}_tax_id"}       = $ptr->{tax_id};
         $form->{taxaccounts}                 .= "$ptr->{accno} ";
       }
 
@@ -1095,8 +1115,6 @@ sub retrieve_invoice {
 
   Common::webdav_folder($form);
 
-  $dbh->disconnect();
-
   $main::lxdebug->leave_sub();
 }
 
@@ -1108,7 +1126,7 @@ sub get_vendor {
   $params = $form unless defined $params && ref $params eq "HASH";
 
   # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $dateformat = $myconfig->{dateformat};
   $dateformat .= "yy" if $myconfig->{dateformat} !~ /^y/;
@@ -1171,45 +1189,6 @@ sub get_vendor {
   }
   $sth->finish();
 
-  if (!$params->{id} && $params->{type} !~ /_(order|quotation)/) {
-    # setup last accounts used
-    $query =
-      qq|SELECT c.id, c.accno, c.description, c.link, c.category
-         FROM chart c
-         JOIN acc_trans ac ON (ac.chart_id = c.id)
-         JOIN ap a         ON (a.id = ac.trans_id)
-         WHERE (a.vendor_id = ?)
-           AND (NOT ((c.link LIKE '%_tax%') OR (c.link LIKE '%_paid%')))
-           AND (a.id IN (SELECT max(a2.id) FROM ap a2 WHERE a2.vendor_id = ?))|;
-    my $refs = selectall_hashref_query($form, $dbh, $query, $vid, $vid);
-
-    my $i = 0;
-    for $ref (@$refs) {
-      if ($ref->{category} eq 'E') {
-        $i++;
-        my ($tax_id, $rate);
-        if ($params->{initial_transdate}) {
-          my $tax_query = qq|SELECT tk.tax_id, t.rate FROM taxkeys tk
-                             LEFT JOIN tax t ON (tk.tax_id = t.id)
-                             WHERE (tk.chart_id = ?) AND (startdate <= ?)
-                             ORDER BY tk.startdate DESC
-                             LIMIT 1|;
-          ($tax_id, $rate) = selectrow_query($form, $dbh, $tax_query, $ref->{id}, $params->{initial_transdate});
-          $params->{"taxchart_$i"} = "${tax_id}--${rate}";
-        }
-
-        $params->{"AP_amount_$i"} = "$ref->{accno}--$tax_id";
-      }
-
-      if ($ref->{category} eq 'L') {
-        $params->{APselected} = $params->{AP_1} = $ref->{accno};
-      }
-    }
-    $params->{rowcount} = $i if ($i && !$params->{type});
-  }
-
-  $dbh->disconnect();
-
   $main::lxdebug->leave_sub();
 }
 
@@ -1218,20 +1197,19 @@ sub retrieve_item {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $i = $form->{rowcount};
 
   # don't include assemblies or obsolete parts
-  my $where = "NOT p.assembly = '1' AND NOT p.obsolete = '1'";
+  my $where = "NOT p.part_type = 'assembly' AND NOT p.obsolete = '1'";
   my @values;
 
   foreach my $table_column (qw(p.partnumber p.description pg.partsgroup)) {
     my $field = (split m{\.}, $table_column)[1];
     next unless $form->{"${field}_${i}"};
     $where .= " AND lower(${table_column}) LIKE lower(?)";
-    push @values, '%' . $form->{"${field}_${i}"} . '%';
+    push @values, like($form->{"${field}_${i}"});
   }
 
   my (%mm_by_id);
@@ -1245,7 +1223,7 @@ sub retrieve_item {
       LEFT JOIN parts ON parts.id = parts_id
       WHERE NOT parts.obsolete AND model ILIKE ? AND (make IS NULL OR make = ?);
     |;
-    my $mm_results = selectall_hashref_query($::form, $dbh, $mm_query, '%' . $form->{"partnumber_$i"} . '%', $::form->{vendor_id});
+    my $mm_results = selectall_hashref_query($::form, $dbh, $mm_query, like($form->{"partnumber_$i"}), $::form->{vendor_id});
     my @mm_ids     = map { $_->{parts_id} } @$mm_results;
     push @{$mm_by_id{ $_->{parts_id} } ||= []}, $_ for @$mm_results;
 
@@ -1282,10 +1260,11 @@ sub retrieve_item {
   my $query =
     qq|SELECT
          p.id, p.partnumber, p.description, p.lastcost AS sellprice, p.listprice,
-         p.unit, p.assembly, p.onhand, p.formel,
+         p.unit, p.part_type, p.onhand, p.formel,
          p.notes AS partnotes, p.notes AS longdescription, p.not_discountable,
-         p.inventory_accno_id, p.price_factor_id,
+         p.price_factor_id,
          p.ean,
+         p.classification_id,
 
          pfac.factor AS price_factor,
 
@@ -1301,6 +1280,7 @@ sub retrieve_item {
          c3.new_chart_id                  AS expense_new_chart,
          date($transdate) - c3.valid_from AS expense_valid,
 
+         pt.used_for_purchase AS used_for_purchase,
          pg.partsgroup
 
        FROM parts p
@@ -1317,6 +1297,7 @@ sub retrieve_item {
            FROM taxzone_charts tc
            WHERE tc.taxzone_id = '$taxzone_id' and tc.buchungsgruppen_id = p.buchungsgruppen_id) = c3.id)
        LEFT JOIN partsgroup pg ON (pg.id = p.partsgroup_id)
+       LEFT JOIN part_classifications pt ON (pt.id = p.classification_id)
        LEFT JOIN price_factors pfac ON (pfac.id = p.price_factor_id)
        WHERE $where|;
   my $sth = prepare_execute_query($form, $dbh, $query, @values);
@@ -1335,6 +1316,7 @@ sub retrieve_item {
   map { push @{ $_ }, prepare_query($form, $dbh, $_->[0]) } @translation_queries;
 
   $form->{item_list} = [];
+  my $has_wrong_pclass = PCLASS_OK;
   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
 
     if ($mm_by_id{$ref->{id}}) {
@@ -1342,10 +1324,16 @@ sub retrieve_item {
       push @{ $ref->{matches} ||= [] }, $::locale->text('Model') . ': ' . join ', ', map { $_->{model} } @{ $mm_by_id{$ref->{id}} };
     }
 
-    if ($ref->{ean} eq $::form->{"partnumber_$i"}) {
+    if (($::form->{"partnumber_$i"} ne '') && ($ref->{ean} eq $::form->{"partnumber_$i"})) {
       push @{ $ref->{matches} ||= [] }, $::locale->text('EAN') . ': ' . $ref->{ean};
     }
+    $ref->{type_and_classific} = type_abbreviation($ref->{part_type}) .
+                                 classification_abbreviation($ref->{classification_id});
 
+    if (! $ref->{used_for_purchase} ) {
+       $has_wrong_pclass = PCLASS_NOTFORPURCHASE;
+       next;
+    }
     # In der Buchungsgruppe ist immer ein Bestandskonto verknuepft, auch wenn
     # es sich um eine Dienstleistung handelt. Bei Dienstleistungen muss das
     # Buchungskonto also aus dem Ergebnis rausgenommen werden.
@@ -1357,7 +1345,7 @@ sub retrieve_item {
     # get tax rates and description
     my $accno_id = ($form->{vc} eq "customer") ? $ref->{income_accno} : $ref->{expense_accno};
     $query =
-      qq|SELECT c.accno, t.taxdescription, t.rate, t.taxnumber
+      qq|SELECT c.accno, t.taxdescription, t.rate, c.accno as taxnumber, t.id as tax_id
          FROM tax t
          LEFT JOIN chart c on (c.id = t.chart_id)
          WHERE t.id IN
@@ -1388,6 +1376,7 @@ sub retrieve_item {
         $form->{"$ptr->{accno}_rate"}         = $ptr->{rate};
         $form->{"$ptr->{accno}_description"}  = $ptr->{taxdescription};
         $form->{"$ptr->{accno}_taxnumber"}    = $ptr->{taxnumber};
+        $form->{"$ptr->{accno}_tax_id"}       = $ptr->{tax_id};
         $form->{taxaccounts}                 .= "$ptr->{accno} ";
       }
 
@@ -1407,7 +1396,6 @@ sub retrieve_item {
     chop $ref->{taxaccounts};
 
     $ref->{onhand} *= 1;
-
     push @{ $form->{item_list} }, $ref;
 
   }
@@ -1415,17 +1403,18 @@ sub retrieve_item {
   $sth->finish();
   $_->[1]->finish for @translation_queries;
 
+  $form->{is_wrong_pclass} = $has_wrong_pclass;
+  $form->{NOTFORSALE}      = PCLASS_NOTFORSALE;
+  $form->{NOTFORPURCHASE}  = PCLASS_NOTFORPURCHASE;
   foreach my $item (@{ $form->{item_list} }) {
     my $custom_variables = CVar->get_custom_variables(module   => 'IC',
                                                       trans_id => $item->{id},
                                                       dbh      => $dbh,
                                                      );
-
+    $form->{is_wrong_pclass} = PCLASS_OK; # one correct type
     map { $item->{"ic_cvar_" . $_->{name} } = $_->{value} } @{ $custom_variables };
   }
 
-  $dbh->disconnect();
-
   $main::lxdebug->leave_sub();
 }
 
@@ -1434,8 +1423,7 @@ sub vendor_details {
 
   my ($self, $myconfig, $form, @wanted_vars) = @_;
 
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my @values;
 
@@ -1460,8 +1448,8 @@ sub vendor_details {
        LIMIT 1|;
   my $ref = selectfirst_hashref_query($form, $dbh, $query, $form->{vendor_id}, @values);
 
-  # remove id and taxincluded before copy back
-  delete @$ref{qw(id taxincluded)};
+  # remove id,notes (double of vendornotes) and taxincluded before copy back
+  delete @$ref{qw(id taxincluded notes)};
 
   @wanted_vars = grep({ $_ } @wanted_vars);
   if (scalar(@wanted_vars) > 0) {
@@ -1477,12 +1465,17 @@ sub vendor_details {
                                                     'trans_id' => $form->{vendor_id});
   map { $form->{"vc_cvar_$_->{name}"} = $_->{value} } @{ $custom_variables };
 
+  if ($form->{cp_id}) {
+    $custom_variables = CVar->get_custom_variables(dbh      => $dbh,
+                                                   module   => 'Contacts',
+                                                   trans_id => $form->{cp_id});
+    $form->{"cp_cvar_$_->{name}"} = $_->{value} for @{ $custom_variables };
+  }
+
   $form->{cp_greeting} = GenericTranslations->get('dbh'              => $dbh,
                                                   'translation_type' => 'greetings::' . ($form->{cp_gender} eq 'f' ? 'female' : 'male'),
                                                   'allow_fallback'   => 1);
 
-  $dbh->disconnect();
-
   $main::lxdebug->leave_sub();
 }
 
@@ -1491,8 +1484,7 @@ sub item_links {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $query =
     qq|SELECT accno, description, link
@@ -1512,8 +1504,6 @@ sub item_links {
   }
 
   $sth->finish();
-  $dbh->disconnect();
-
   $main::lxdebug->leave_sub();
 }
 
@@ -1557,17 +1547,33 @@ sub _delete_payments {
 }
 
 sub post_payment {
+  my ($self, $myconfig, $form, $locale) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_post_payment, $self, $myconfig, $form, $locale);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _post_payment {
   my ($self, $myconfig, $form, $locale) = @_;
 
-  # connect to database, turn off autocommit
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my (%payments, $old_form, $row, $item, $query, %keep_vars);
 
   $old_form = save_form();
 
+  $query = <<SQL;
+    SELECT at.acc_trans_id, at.amount, at.cleared, c.accno
+    FROM acc_trans at
+    LEFT JOIN chart c ON (at.chart_id = c.id)
+    WHERE (at.trans_id = ?)
+SQL
+
+  my %already_cleared = selectall_as_map($form, $dbh, $query, 'acc_trans_id', [ qw(amount cleared accno) ], $form->{id});
+
   # Delete all entries in acc_trans from prior payments.
   if (SL::DB::Default->get->payments_changeable != 0) {
     $self->_delete_payments($form, $dbh);
@@ -1614,16 +1620,11 @@ sub post_payment {
   ($form->{AP}) = selectfirst_array_query($form, $dbh, $query, conv_i($form->{id}));
 
   # Post the new payments.
-  $self->post_invoice($myconfig, $form, $dbh, 1);
+  $self->post_invoice($myconfig, $form, $dbh, payments_only => 1, already_cleared => \%already_cleared);
 
   restore_form($old_form);
 
-  my $rc = $dbh->commit();
-  $dbh->disconnect();
-
-  $main::lxdebug->leave_sub();
-
-  return $rc;
+  return 1;
 }
 
 sub get_duedate {
index b275833..fdc70ab 100644 (file)
--- a/SL/IS.pm
+++ b/SL/IS.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Inventory invoicing module
@@ -34,7 +35,8 @@
 
 package IS;
 
-use List::Util qw(max);
+use List::Util qw(max sum0);
+use List::MoreUtils qw(any);
 
 use Carp;
 use SL::AM;
@@ -50,13 +52,20 @@ use SL::MoreCommon;
 use SL::IC;
 use SL::IO;
 use SL::TransNumber;
+use SL::DB::Chart;
 use SL::DB::Default;
+use SL::DB::Draft;
 use SL::DB::Tax;
 use SL::DB::TaxZone;
 use SL::TransNumber;
+use SL::DB;
+use SL::Presenter::Part qw(type_abbreviation classification_abbreviation);
 use Data::Dumper;
 
 use strict;
+use constant PCLASS_OK             =>   0;
+use constant PCLASS_NOTFORSALE     =>   1;
+use constant PCLASS_NOTFORPURCHASE =>   2;
 
 sub invoice_details {
   $main::lxdebug->enter_sub();
@@ -148,6 +157,7 @@ sub invoice_details {
   # so that they can be sorted in later
   my %prepared_template_arrays = IC->prepare_parts_for_printing(myconfig => $myconfig, form => $form);
   my @prepared_arrays          = keys %prepared_template_arrays;
+  my @separate_totals          = qw(non_separate_subtotal);
 
   my $ic_cvar_configs = CVar->get_configs(module => 'IC');
   my $project_cvar_configs = CVar->get_configs(module => 'Projects');
@@ -163,11 +173,17 @@ sub invoice_details {
   push @arrays, map { "ic_cvar_$_->{name}" } @{ $ic_cvar_configs };
   push @arrays, map { "project_cvar_$_->{name}" } @{ $project_cvar_configs };
 
-  my @tax_arrays = qw(taxbase tax taxdescription taxrate taxnumber);
+  my @tax_arrays = qw(taxbase tax taxdescription taxrate taxnumber tax_id);
 
   my @payment_arrays = qw(payment paymentaccount paymentdate paymentsource paymentmemo);
 
-  map { $form->{TEMPLATE_ARRAYS}->{$_} = [] } (@arrays, @tax_arrays, @payment_arrays, @prepared_arrays);
+  my @invoices_for_advance_payment_arrays = qw(iap_invnumber iap_transdate
+                                               iap_amount iap_amount_nofmt
+                                               iap_taxamount iap_taxamount_nofmt
+                                               iap_open_amount iap_open_amount_nofmt
+                                               iap_netamount);
+
+  map { $form->{TEMPLATE_ARRAYS}->{$_} = [] } (@arrays, @tax_arrays, @payment_arrays, @prepared_arrays, @invoices_for_advance_payment_arrays);
 
   my $totalweight = 0;
   foreach $item (sort { $a->[1] cmp $b->[1] } @partsgroup) {
@@ -331,6 +347,17 @@ sub invoice_details {
       push @{ $form->{TEMPLATE_ARRAYS}->{discount_nofmt} }, ($discount != 0) ? $discount * -1 : '';
       push @{ $form->{TEMPLATE_ARRAYS}->{p_discount} },     $form->{"discount_$i"};
 
+      if ( $prepared_template_arrays{separate}[$i - 1]  ) {
+        my $pabbr = $prepared_template_arrays{separate}[$i - 1];
+        if ( ! $form->{"separate_${pabbr}_subtotal"} ) {
+            push @separate_totals , "separate_${pabbr}_subtotal";
+            $form->{"separate_${pabbr}_subtotal"} = 0;
+        }
+        $form->{"separate_${pabbr}_subtotal"} += $linetotal;
+      } else {
+        $form->{non_separate_subtotal} += $linetotal;
+      }
+
       $form->{total}            += $linetotal;
       $form->{nodiscount_total} += $nodiscount_linetotal;
       $form->{discount_total}   += $discount;
@@ -416,16 +443,16 @@ sub invoice_details {
       }
       my $tax_rate = $taxrate * 100;
       push(@{ $form->{TEMPLATE_ARRAYS}->{tax_rate} }, qq|$tax_rate|);
-      if ($form->{"assembly_$i"}) {
+      if ($form->{"part_type_$i"} eq 'assembly') {
         $sameitem = "";
 
         # get parts and push them onto the stack
         my $sortorder = "";
         if ($form->{groupitems}) {
           $sortorder =
-            qq|ORDER BY pg.partsgroup, a.oid|;
+            qq|ORDER BY pg.partsgroup, a.position|;
         } else {
-          $sortorder = qq|ORDER BY a.oid|;
+          $sortorder = qq|ORDER BY a.position|;
         }
 
         my $query =
@@ -486,10 +513,25 @@ sub invoice_details {
     push(@{ $form->{TEMPLATE_ARRAYS}->{taxrate} },        $form->format_amount($myconfig, $form->{"${item}_rate"} * 100));
     push(@{ $form->{TEMPLATE_ARRAYS}->{taxrate_nofmt} },  $form->{"${item}_rate"} * 100);
     push(@{ $form->{TEMPLATE_ARRAYS}->{taxnumber} },      $form->{"${item}_taxnumber"});
+    push(@{ $form->{TEMPLATE_ARRAYS}->{tax_id} },         $form->{"${item}_tax_id"});
+
+    # taxnumber (= accno) is used for grouping the amounts of the various taxes and as a prefix in form
+
+    # This code used to assume that at most one tax entry can point to the same
+    # chart_id, even though chart_id does not have a unique constraint!
+
+    # This chart_id was then looked up via its accno, which is the key that is
+    # used to group the different taxes by for a record
+
+    # As we now also store the tax_id we can use that to look up the tax
+    # instead, this is only done here to get the (translated) taxdescription.
+
+    if ( $form->{"${item}_tax_id"} ) {
+      my $tax_obj = SL::DB::Manager::Tax->find_by(id => $form->{"${item}_tax_id"}) or die "Can't find tax with id " . $form->{"${item}_tax_id"};
+      my $description = $tax_obj ? $tax_obj->translated_attribute('taxdescription',  $form->{language_id}, 0) : '';
+      push(@{ $form->{TEMPLATE_ARRAYS}->{taxdescription} }, $description . q{ } . 100 * $form->{"${item}_rate"} . q{%});
+    }
 
-    my $tax_obj     = SL::DB::Manager::Tax->find_by(taxnumber => $form->{"${item}_taxnumber"});
-    my $description = $tax_obj ? $tax_obj->translated_attribute('taxdescription',  $form->{language_id}, 0) : '';
-    push(@{ $form->{TEMPLATE_ARRAYS}->{taxdescription} }, $description . q{ } . 100 * $form->{"${item}_rate"} . q{%});
   }
 
   for my $i (1 .. $form->{paidaccounts}) {
@@ -519,35 +561,66 @@ sub invoice_details {
   $form->{nodiscount}          = $form->format_amount($myconfig, $nodiscount, 2);
   $form->{yesdiscount}         = $form->format_amount($myconfig, $form->{nodiscount_total} - $nodiscount, 2);
 
-  $form->{invtotal} = ($form->{taxincluded}) ? $form->{total} : $form->{total} + $tax;
-  $form->{total}    = $form->format_amount($myconfig, $form->{invtotal} - $form->{paid}, 2);
+  my $grossamount = ($form->{taxincluded}) ? $form->{total} : $form->{total} + $tax;
+  $form->{invtotal} = $form->round_amount($grossamount, 2, 1);
+  $form->{rounding} = $form->round_amount(
+    $form->{invtotal} - $form->round_amount($grossamount, 2),
+    2
+  );
+
+  $form->{rounding_nofmt} = $form->{rounding};
+  $form->{total_nofmt}    = $form->{total};
+  $form->{invtotal_nofmt} = $form->{invtotal};
+  $form->{paid_nofmt}     = $form->{paid};
 
+  $form->{rounding} = $form->format_amount($myconfig, $form->{rounding}, 2);
+  $form->{total}    = $form->format_amount($myconfig, $form->{invtotal} - $form->{paid}, 2);
   $form->{invtotal} = $form->format_amount($myconfig, $form->{invtotal}, 2);
   $form->{paid}     = $form->format_amount($myconfig, $form->{paid}, 2);
 
-  $form->set_payment_options($myconfig, $form->{invdate});
+  $form->set_payment_options($myconfig, $form->{invdate}, 'sales_invoice');
 
+  $form->{department}    = SL::DB::Manager::Department->find_by(id => $form->{department_id})->description if $form->{department_id};
   $form->{delivery_term} = SL::DB::Manager::DeliveryTerm->find_by(id => $form->{delivery_term_id} || undef);
   $form->{delivery_term}->description_long($form->{delivery_term}->translated_attribute('description_long', $form->{language_id})) if $form->{delivery_term} && $form->{language_id};
-  $form->{department}    = SL::DB::Manager::Department->find_by(id => $form->{department_id})->load->description if $form->{department_id};
 
   $form->{username} = $myconfig->{name};
+  $form->{$_} = $form->format_amount($myconfig, $form->{$_}, 2) for @separate_totals;
+
+  my $id_for_iap = $form->{convert_from_oe_ids} || $form->{convert_from_ar_ids} || $form->{id};
+  my $from_order = !!$form->{convert_from_oe_ids};
+  foreach my $invoice_for_advance_payment (@{$self->_get_invoices_for_advance_payment($id_for_iap, $from_order)}) {
+    # Collect VAT of invoices for advance payment.
+    # Set sellprices to fxsellprices for items, because
+    # the PriceTaxCalculator sets fxsellprice from sellprice before calculating.
+    $_->sellprice($_->fxsellprice) for @{$invoice_for_advance_payment->items};
+    my %pat       = $invoice_for_advance_payment->calculate_prices_and_taxes;
+    my $taxamount = sum0 values %{ $pat{taxes_by_tax_id} };
+
+    push(@{ $form->{TEMPLATE_ARRAYS}->{"iap_$_"} },                $invoice_for_advance_payment->$_) for qw(invnumber transdate);
+    push(@{ $form->{TEMPLATE_ARRAYS}->{"iap_amount_nofmt"} },      $invoice_for_advance_payment->amount);
+    push(@{ $form->{TEMPLATE_ARRAYS}->{"iap_amount"} },            $invoice_for_advance_payment->amount_as_number);
+    push(@{ $form->{TEMPLATE_ARRAYS}->{"iap_netamount"} },         $invoice_for_advance_payment->netamount_as_number);
+    push(@{ $form->{TEMPLATE_ARRAYS}->{"iap_taxamount_nofmt"} },   $taxamount);
+    push(@{ $form->{TEMPLATE_ARRAYS}->{"iap_taxamount"} },         $form->format_amount($myconfig, $taxamount, 2));
+
+    my $open_amount = $form->round_amount($invoice_for_advance_payment->open_amount, 2);
+    push(@{ $form->{TEMPLATE_ARRAYS}->{"iap_open_amount_nofmt"} }, $open_amount);
+    push(@{ $form->{TEMPLATE_ARRAYS}->{"iap_open_amount"} },       $form->format_amount($myconfig, $open_amount, 2));
+
+    $form->{iap_amount_nofmt}      += $invoice_for_advance_payment->amount;
+    $form->{iap_taxamount_nofmt}   += $taxamount;
+    $form->{iap_open_amount_nofmt} += $open_amount;
+    $form->{iap_existing}           = 1;
+  }
+  $form->{iap_amount}      = $form->format_amount($myconfig, $form->{iap_amount_nofmt},      2);
+  $form->{iap_taxamount}   = $form->format_amount($myconfig, $form->{iap_taxamount_nofmt},   2);
+  $form->{iap_open_amount} = $form->format_amount($myconfig, $form->{iap_open_amount_nofmt}, 2);
 
-  $main::lxdebug->leave_sub();
-}
-
-sub project_description {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $dbh, $id) = @_;
-  my $form = \%main::form;
-
-  my $query = qq|SELECT description FROM project WHERE id = ?|;
-  my ($description) = selectrow_query($form, $dbh, $query, conv_i($id));
+  $form->{iap_final_amount_nofmt} = $form->{invtotal_nofmt} - $form->{iap_amount_nofmt};
+  $form->{iap_final_amount}       = $form->format_amount($myconfig, $form->{iap_final_amount_nofmt}, 2);
 
   $main::lxdebug->leave_sub();
-
-  return $_;
 }
 
 sub customer_details {
@@ -591,6 +664,7 @@ sub customer_details {
       $ref->{street} = $customer->street;
       $ref->{zipcode} = $customer->zipcode;
       $ref->{country} = $customer->country;
+      $ref->{gln} = $customer->gln;
     }
     my $contact = SL::DB::Manager::Contact->find_by(cp_id => $::form->{cp_id});
     if ($contact) {
@@ -599,8 +673,8 @@ sub customer_details {
       $ref->{cp_gender} = $contact->cp_gender;
     }
   }
-  # remove id and taxincluded before copy back
-  delete @$ref{qw(id taxincluded)};
+  # remove id,notes (double of customernotes) and taxincluded before copy back
+  delete @$ref{qw(id taxincluded notes)};
 
   @wanted_vars = grep({ $_ } @wanted_vars);
   if (scalar(@wanted_vars) > 0) {
@@ -638,6 +712,13 @@ sub customer_details {
                                                     'trans_id' => $form->{customer_id});
   map { $form->{"vc_cvar_$_->{name}"} = $_->{value} } @{ $custom_variables };
 
+  if ($form->{cp_id}) {
+    $custom_variables = CVar->get_custom_variables(dbh      => $dbh,
+                                                   module   => 'Contacts',
+                                                   trans_id => $form->{cp_id});
+    $form->{"cp_cvar_$_->{name}"} = $_->{value} for @{ $custom_variables };
+  }
+
   $form->{cp_greeting} = GenericTranslations->get('dbh'              => $dbh,
                                                   'translation_type' => 'greetings::' . ($form->{cp_gender} eq 'f' ? 'female' : 'male'),
                                                   'language_id'      => $language_id,
@@ -648,12 +729,20 @@ sub customer_details {
 }
 
 sub post_invoice {
+  my ($self, $myconfig, $form, $provided_dbh, %params) = @_;
   $main::lxdebug->enter_sub();
 
-  my ($self, $myconfig, $form, $provided_dbh, $payments_only) = @_;
+  my $rc = SL::DB->client->with_transaction(\&_post_invoice, $self, $myconfig, $form, $provided_dbh, %params);
 
-  # connect to database, turn off autocommit
-  my $dbh = $provided_dbh ? $provided_dbh : $form->get_standard_dbh;
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _post_invoice {
+  my ($self, $myconfig, $form, $provided_dbh, %params) = @_;
+
+  my $payments_only = $params{payments_only};
+  my $dbh = $provided_dbh || SL::DB->client->dbh;
   my $restricter = SL::HTML::Restrict->create;
 
   my ($query, $sth, $null, $project_id, @values);
@@ -671,6 +760,8 @@ sub post_invoice {
 
   my $all_units = AM->retrieve_units($myconfig, $form);
 
+  my $already_booked = !!$form->{id};
+
   if (!$payments_only) {
     if ($form->{storno}) {
       _delete_transfers($dbh, $form, $form->{storno_id});
@@ -825,9 +916,9 @@ sub post_invoice {
 
       next if $payments_only;
 
-      if ($form->{"inventory_accno_$i"} || $form->{"assembly_$i"}) {
+      if ($form->{"inventory_accno_$i"} || $form->{"part_type_$i"} eq 'assembly') {
 
-        if ($form->{"assembly_$i"}) {
+        if ($form->{"part_type_$i"} eq 'assembly') {
           # record assembly item as allocated
           &process_assembly($dbh, $myconfig, $form, $position, $form->{"id_$i"}, $baseqty);
 
@@ -967,9 +1058,21 @@ SQL
     }
   }
 
-  $form->{amount}{ $form->{id} }{ $form->{AR} } = $netamount + $tax;
-  $form->{paid} =
-    $form->round_amount($form->{paid} * $form->{exchangerate} + $diff, 2);
+  # Invoice Summary includes Rounding
+  my $grossamount = $netamount + $tax;
+  my $rounding = $form->round_amount(
+    $form->round_amount($grossamount, 2, 1) - $form->round_amount($grossamount, 2),
+    2
+  );
+  my $rnd_accno = $rounding == 0 ? 0
+                : $rounding > 0  ? $form->{rndgain_accno}
+                :                  $form->{rndloss_accno}
+  ;
+  $form->{amount}{ $form->{id} }{ $form->{AR} } = $form->round_amount($grossamount, 2, 1);
+  $form->{paid} = $form->round_amount(
+    $form->{paid} * $form->{exchangerate} + $diff,
+    2
+  );
 
   # reverse AR
   $form->{amount}{ $form->{id} }{ $form->{AR} } *= -1;
@@ -982,7 +1085,106 @@ SQL
 
   $project_id = conv_i($form->{"globalproject_id"});
   # entsprechend auch beim Bestimmen des Steuerschlüssels in Taxkey.pm berücksichtigen
-  my $taxdate = $form->{deliverydate} ? $form->{deliverydate} : $form->{invdate};
+  my $taxdate = $form->{tax_point} ||$form->{deliverydate} || $form->{invdate};
+
+  # Sanity checks for invoices for advance payment and final invoices
+  my $advance_payment_clearing_chart;
+  if (any { $_ eq $form->{type} } qw(invoice_for_advance_payment final_invoice)) {
+    $advance_payment_clearing_chart = SL::DB::Chart->new(id => $::instance_conf->get_advance_payment_clearing_chart_id)->load;
+    die "No Clearing Chart for Advance Payment" unless ref $advance_payment_clearing_chart eq 'SL::DB::Chart';
+
+    my @current_taxaccounts = (split(/ /, $form->{taxaccounts}));
+    die 'Wrong call: Cannot post invoice for advance payment or final invoice with more than one tax' if (scalar @current_taxaccounts > 1);
+
+    my @trans_ids = keys %{ $form->{amount} };
+    if (scalar @trans_ids > 1) {
+      require Data::Dumper;
+      die "Invalid state for advance payment more than one trans_id " . Dumper($form->{amount});
+    }
+  }
+
+  my $iap_amounts;
+  if ($form->{type} eq 'final_invoice') {
+    my $id_for_iap = $form->{convert_from_oe_ids} || $form->{convert_from_ar_ids} || $form->{id};
+    my $from_order = !!$form->{convert_from_oe_ids};
+    my $invoices_for_advance_payment = $self->_get_invoices_for_advance_payment($id_for_iap, $from_order);
+    if (scalar @$invoices_for_advance_payment > 0) {
+      # reverse booking for invoices for advance payment
+      foreach my $invoice_for_advance_payment (@$invoices_for_advance_payment) {
+        # delete ?
+        # --> is implemented below (bookings are marked in memo field)
+        #
+        # TODO: helper table acc_trans_advance_payment
+        # trans_id for final invoice connects to acc_trans_id here
+        # my $booking = SL::DB::AccTrans->new( ...)
+        # --> helper table not nessessary because of mark in memo field
+        #
+        # TODO: If final_invoice change (delete storno) delete all connectin acc_trans entries, if
+        # period is not closed
+        # --> no problem because gldate of reverse booking is date of final invoice
+        #     if deletion of final invoice is allowed, reverting bookings in invoices
+        #     for advance payment are allowed, too.
+        # $booking->id, $self->id in helper table
+        if (!$already_booked) {
+          # move all netamount to correct transfer chart (19% or 7%)
+          my %inv_calc = $invoice_for_advance_payment->calculate_prices_and_taxes();
+          my @trans_ids = keys %{ $inv_calc{amounts} };
+          die "Invalid state for advance payment invoice,more than one trans_id" if (scalar @trans_ids > 1);
+          my $entry = delete $inv_calc{amounts}{$trans_ids[0]};
+          my $tax;
+          if ($entry->{tax_id}) {
+            $tax = SL::DB::Manager::Tax->find_by(id => $entry->{tax_id}); # || die "Can't find tax with id " . $entry->{tax_id};
+          }
+          # no tax, no prob
+          if ($tax and $tax->rate != 0) {
+            my $transfer_chart = $tax->taxkey == 2 ? SL::DB::Chart->new(id => $::instance_conf->get_advance_payment_taxable_7_id)->load
+                              :  $tax->taxkey == 3 ? SL::DB::Chart->new(id => $::instance_conf->get_advance_payment_taxable_19_id)->load
+                              :  undef;
+            die "No Transfer Chart for Advance Payment" unless ref $transfer_chart eq 'SL::DB::Chart';
+            $form->{amount}->{$invoice_for_advance_payment->id}->{$transfer_chart->accno} = -1 * $invoice_for_advance_payment->netamount;
+            $form->{memo}  ->{$invoice_for_advance_payment->id}->{$transfer_chart->accno} = 'reverse booking by final invoice';
+            # AR
+            $form->{amount}->{$invoice_for_advance_payment->id}->{$form->{AR}} = $invoice_for_advance_payment->netamount;
+            $form->{memo}  ->{$invoice_for_advance_payment->id}->{$form->{AR}} = 'reverse booking by final invoice';
+          }
+        }
+
+        # VAT for invoices for advance payment is booked on payment of these. So do not book this VAT for final invoice.
+        # And book the amount of the invoices for advance payment with taxkey 0 (see below).
+        # Collect amounts and VAT of invoices for advance payment.
+
+        # Set sellprices to fxsellprices for items, because
+        # the PriceTaxCalculator sets fxsellprice from sellprice before calculating.
+        $_->sellprice($_->fxsellprice) for @{$invoice_for_advance_payment->items};
+        my %pat = $invoice_for_advance_payment->calculate_prices_and_taxes;
+
+        foreach my $tax_chart_id (keys %{ $pat{taxes_by_chart_id} }) {
+          my $tax_accno = SL::DB::Chart->load_cached($tax_chart_id)->accno;
+          $form->{amount}{ $form->{id} }{$tax_accno}  -= $pat{taxes_by_chart_id}->{$tax_chart_id};
+          $form->{amount}{ $form->{id} }{$form->{AR}} += $pat{taxes_by_chart_id}->{$tax_chart_id};
+        }
+
+        foreach my $amount_chart_id (keys %{ $pat{amounts} }) {
+          my $amount_accno = SL::DB::Chart->load_cached($amount_chart_id)->accno;
+          $iap_amounts->{$amount_accno}                 += $pat{amounts}->{$amount_chart_id}->{amount};
+          $form->{amount}{ $form->{id} }{$amount_accno} -= $pat{amounts}->{$amount_chart_id}->{amount};
+        }
+      }
+    }
+  }
+
+  if ($form->{type} eq 'invoice_for_advance_payment') {
+    # get gross and move to clearing chart - delete everything else
+    # 1. gross
+    my $gross = $form->{amount}{ $form->{id} }{$form->{AR}};
+    # 2. destroy
+    undef $form->{amount}{ $form->{id} };
+    # 3. rebuild
+    $form->{amount}{ $form->{id} }{$form->{AR}}            = $gross;
+    $form->{amount}{ $form->{id} }{$advance_payment_clearing_chart->accno} = $gross * -1;
+    # 4. no cogs, hopefully not commonly used at all
+    undef $form->{amount_cogs};
+  }
 
   foreach my $trans_id (keys %{ $form->{amount_cogs} }) {
     foreach my $accno (keys %{ $form->{amount_cogs}{$trans_id} }) {
@@ -1021,7 +1223,7 @@ SQL
 
       if (!$payments_only && ($form->{amount}{$trans_id}{$accno} != 0)) {
         $query =
-          qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, tax_id, taxkey, project_id, chart_link)
+          qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, tax_id, taxkey, project_id, chart_link, memo)
              VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?,
                      (SELECT tax_id
                       FROM taxkeys
@@ -1038,8 +1240,9 @@ SQL
                       AND startdate <= ?
                       ORDER BY startdate DESC LIMIT 1),
                      ?,
-                     (SELECT link FROM chart WHERE accno = ?))|;
-        @values = (conv_i($trans_id), $accno, $form->{amount}{$trans_id}{$accno}, conv_date($form->{invdate}), $accno, conv_date($taxdate), $accno, conv_date($taxdate), conv_i($project_id), $accno);
+                     (SELECT link FROM chart WHERE accno = ?),
+                     ?)|;
+        @values = (conv_i($trans_id), $accno, $form->{amount}{$trans_id}{$accno}, conv_date($form->{invdate}), $accno, conv_date($taxdate), $accno, conv_date($taxdate), conv_i($project_id), $accno, $form->{memo}{$trans_id}{$accno});
         do_query($form, $dbh, $query, @values);
         $form->{amount}{$trans_id}{$accno} = 0;
       }
@@ -1050,7 +1253,7 @@ SQL
 
       if (!$payments_only && ($form->{amount}{$trans_id}{$accno} != 0)) {
         $query =
-          qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, tax_id, taxkey, project_id, chart_link)
+          qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, tax_id, taxkey, project_id, chart_link, memo)
              VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?,
                      (SELECT tax_id
                       FROM taxkeys
@@ -1067,11 +1270,31 @@ SQL
                       AND startdate <= ?
                       ORDER BY startdate DESC LIMIT 1),
                      ?,
-                     (SELECT link FROM chart WHERE accno = ?))|;
-        @values = (conv_i($trans_id), $accno, $form->{amount}{$trans_id}{$accno}, conv_date($form->{invdate}), $accno, conv_date($taxdate), $accno, conv_date($taxdate), conv_i($project_id), $accno);
+                     (SELECT link FROM chart WHERE accno = ?),
+                     ?)|;
+        @values = (conv_i($trans_id), $accno, $form->{amount}{$trans_id}{$accno}, conv_date($form->{invdate}), $accno, conv_date($taxdate), $accno, conv_date($taxdate), conv_i($project_id), $accno,$form->{memo}{$trans_id}{$accno});
         do_query($form, $dbh, $query, @values);
       }
     }
+    if (!$payments_only && ($rnd_accno != 0)) {
+      $query =
+        qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, tax_id, taxkey, project_id, chart_link)
+             VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?, (SELECT id FROM tax WHERE taxkey=0), 0, ?, (SELECT link FROM chart WHERE accno = ?))|;
+      @values = (conv_i($trans_id), $rnd_accno, $rounding, conv_date($form->{invdate}), conv_i($project_id), $rnd_accno);
+      do_query($form, $dbh, $query, @values);
+      $rnd_accno = 0;
+    }
+  }
+
+  # Book the amount of the invoices for advance payment with taxkey 0 (see below).
+  if ($form->{type} eq 'final_invoice' && $iap_amounts) {
+    foreach my $accno (keys %$iap_amounts) {
+      $query =
+        qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, tax_id, taxkey, project_id, chart_link)
+        VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?, (SELECT id FROM tax WHERE taxkey=0), 0, ?, (SELECT link FROM chart WHERE accno = ?))|;
+      @values = (conv_i($form->{id}), $accno, $iap_amounts->{$accno}, conv_date($form->{invdate}), conv_i($project_id), $accno);
+      do_query($form, $dbh, $query, @values);
+    }
   }
 
   # deduct payment differences from diff
@@ -1083,6 +1306,8 @@ SQL
     }
   }
 
+  my %already_cleared = %{ $params{already_cleared} // {} };
+
   # record payments and offsetting AR
   if (!$form->{storno}) {
     for my $i (1 .. $form->{paidaccounts}) {
@@ -1112,9 +1337,16 @@ SQL
       # record AR
       $amount = $form->round_amount($form->{"paid_$i"} * $form->{exchangerate} + $diff, 2);
 
+      my $new_cleared = !$form->{"acc_trans_id_$i"}                                                       ? 'f'
+                      : !$already_cleared{$form->{"acc_trans_id_$i"}}                                     ? 'f'
+                      : $already_cleared{$form->{"acc_trans_id_$i"}}->{amount} != $form->{"paid_$i"} * -1 ? 'f'
+                      : $already_cleared{$form->{"acc_trans_id_$i"}}->{accno}  != $accno                  ? 'f'
+                      : $already_cleared{$form->{"acc_trans_id_$i"}}->{cleared}                           ? 't'
+                      :                                                                                     'f';
+
       if ($form->{amount}{ $form->{id} }{ $form->{AR} } != 0) {
         $query =
-        qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, tax_id, taxkey, project_id, chart_link)
+        qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, tax_id, taxkey, project_id, cleared, chart_link)
            VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?,
                    (SELECT tax_id
                     FROM taxkeys
@@ -1130,9 +1362,9 @@ SQL
                                      WHERE accno = ?)
                     AND startdate <= ?
                     ORDER BY startdate DESC LIMIT 1),
-                   ?,
+                   ?, ?,
                    (SELECT link FROM chart WHERE accno = ?))|;
-        @values = (conv_i($form->{"id"}), $form->{AR}, $amount, $form->{"datepaid_$i"}, $form->{AR}, conv_date($taxdate), $form->{AR}, conv_date($taxdate), $project_id, $form->{AR});
+        @values = (conv_i($form->{"id"}), $form->{AR}, $amount, $form->{"datepaid_$i"}, $form->{AR}, conv_date($taxdate), $form->{AR}, conv_date($taxdate), $project_id, $new_cleared, $form->{AR});
         do_query($form, $dbh, $query, @values);
       }
 
@@ -1141,7 +1373,7 @@ SQL
       my $gldate = (conv_date($form->{"gldate_$i"}))? conv_date($form->{"gldate_$i"}) : conv_date($form->current_date($myconfig));
 
       $query =
-      qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, gldate, source, memo, tax_id, taxkey, project_id, chart_link)
+      qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, gldate, source, memo, tax_id, taxkey, project_id, cleared, chart_link)
          VALUES (?, (SELECT id FROM chart WHERE accno = ?), ?, ?, ?, ?, ?,
                  (SELECT tax_id
                   FROM taxkeys
@@ -1157,10 +1389,10 @@ SQL
                                    WHERE accno = ?)
                   AND startdate <= ?
                   ORDER BY startdate DESC LIMIT 1),
-                 ?,
+                 ?, ?,
                  (SELECT link FROM chart WHERE accno = ?))|;
       @values = (conv_i($form->{"id"}), $accno, $form->{"paid_$i"}, $form->{"datepaid_$i"},
-                 $gldate, $form->{"source_$i"}, $form->{"memo_$i"}, $accno, conv_date($taxdate), $accno, conv_date($taxdate), $project_id, $accno);
+                 $gldate, $form->{"source_$i"}, $form->{"memo_$i"}, $accno, conv_date($taxdate), $accno, conv_date($taxdate), $project_id, $new_cleared, $accno);
       do_query($form, $dbh, $query, @values);
 
       # exchangerate difference
@@ -1228,45 +1460,44 @@ SQL
     $query = qq|UPDATE ar SET paid = ? WHERE id = ?|;
     do_query($form, $dbh, $query,  $form->{paid}, conv_i($form->{id}));
 
-    $dbh->commit if !$provided_dbh;
+    $form->new_lastmtime('ar');
 
-    $main::lxdebug->leave_sub();
     return;
   }
 
-  $amount = $netamount + $tax;
+  $amount = $form->round_amount( $netamount + $tax, 2, 1);
 
   # save AR record
   #erweiterung fuer lieferscheinnummer (donumber) 12.02.09 jb
 
   $query = qq|UPDATE ar set
                 invnumber   = ?, ordnumber     = ?, quonumber     = ?, cusordnumber  = ?,
-                transdate   = ?, orddate       = ?, quodate       = ?, customer_id   = ?,
+                transdate   = ?, orddate       = ?, quodate       = ?, tax_point     = ?, customer_id   = ?,
                 amount      = ?, netamount     = ?, paid          = ?,
                 duedate     = ?, deliverydate  = ?, invoice       = ?, shippingpoint = ?,
                 shipvia     = ?,                    notes         = ?, intnotes      = ?,
                 currency_id = (SELECT id FROM currencies WHERE name = ?),
                 department_id = ?, payment_id    = ?, taxincluded   = ?,
-                type        = ?, language_id   = ?, taxzone_id    = ?, shipto_id     = ?,
+                type        = ?, language_id   = ?, taxzone_id    = ?, shipto_id     = ?, billing_address_id = ?,
                 employee_id = ?, salesman_id   = ?, storno_id     = ?, storno        = ?,
                 cp_id       = ?, marge_total   = ?, marge_percent = ?,
                 globalproject_id               = ?, delivery_customer_id             = ?,
                 transaction_description        = ?, delivery_vendor_id               = ?,
-                donumber    = ?, invnumber_for_credit_note = ?,        direct_debit  = ?,
+                donumber    = ?, invnumber_for_credit_note = ?,        direct_debit  = ?, qrbill_without_amount = ?,
                 delivery_term_id = ?
               WHERE id = ?|;
   @values = (          $form->{"invnumber"},           $form->{"ordnumber"},             $form->{"quonumber"},          $form->{"cusordnumber"},
-             conv_date($form->{"invdate"}),  conv_date($form->{"orddate"}),    conv_date($form->{"quodate"}),    conv_i($form->{"customer_id"}),
+             conv_date($form->{"invdate"}),  conv_date($form->{"orddate"}),    conv_date($form->{"quodate"}), conv_date($form->{tax_point}), conv_i($form->{"customer_id"}),
                        $amount,                        $netamount,                       $form->{"paid"},
              conv_date($form->{"duedate"}),  conv_date($form->{"deliverydate"}),    '1',                                $form->{"shippingpoint"},
                        $form->{"shipvia"},                                $restricter->process($form->{"notes"}),       $form->{"intnotes"},
                        $form->{"currency"},     conv_i($form->{"department_id"}), conv_i($form->{"payment_id"}),        $form->{"taxincluded"} ? 't' : 'f',
-                       $form->{"type"},         conv_i($form->{"language_id"}),   conv_i($form->{"taxzone_id"}), conv_i($form->{"shipto_id"}),
+                       $form->{"type"},         conv_i($form->{"language_id"}),   conv_i($form->{"taxzone_id"}), conv_i($form->{"shipto_id"}), conv_i($form->{billing_address_id}),
                 conv_i($form->{"employee_id"}), conv_i($form->{"salesman_id"}),   conv_i($form->{storno_id}),           $form->{"storno"} ? 't' : 'f',
                 conv_i($form->{"cp_id"}),            1 * $form->{marge_total} ,      1 * $form->{marge_percent},
                 conv_i($form->{"globalproject_id"}),                              conv_i($form->{"delivery_customer_id"}),
                        $form->{transaction_description},                          conv_i($form->{"delivery_vendor_id"}),
-                       $form->{"donumber"}, $form->{"invnumber_for_credit_note"},        $form->{direct_debit} ? 't' : 'f',
+                       $form->{"donumber"}, $form->{"invnumber_for_credit_note"},        $form->{direct_debit} ? 't' : 'f', $form->{qrbill_without_amount} ? 't' : 'f',
                 conv_i($form->{delivery_term_id}),
                 conv_i($form->{"id"}));
   do_query($form, $dbh, $query, @values);
@@ -1275,7 +1506,7 @@ SQL
   if ($form->{storno}) {
     $query =
       qq!UPDATE ar SET
-           paid = paid + amount,
+           paid = amount,
            storno = 't',
            intnotes = ? || intnotes
          WHERE id = ?!;
@@ -1283,8 +1514,10 @@ SQL
     do_query($form, $dbh, qq|UPDATE ar SET paid = amount WHERE id = ?|, conv_i($form->{"id"}));
   }
 
-  $form->{name} = $form->{customer};
-  $form->{name} =~ s/--\Q$form->{customer_id}\E//;
+  # maybe we are in a larger transaction and the current
+  # object is not yet persistent in the db, therefore we
+  # need the current dbh to get the not yet committed mtime
+  $form->new_lastmtime('ar', $provided_dbh);
 
   # add shipto
   if (!$form->{shipto_id}) {
@@ -1349,34 +1582,87 @@ SQL
     do_query($form, $dbh, $query, @orphaned_ids);
   }
 
+  if ($form->{draft_id}) {
+    SL::DB::Manager::Draft->delete_all(where => [ id => delete($form->{draft_id}) ]);
+  }
+
   # safety check datev export
   if ($::instance_conf->get_datev_check_on_sales_invoice) {
-    my $transdate = $::form->{invdate} ? DateTime->from_lxoffice($::form->{invdate}) : undef;
-    $transdate  ||= DateTime->today;
 
     my $datev = SL::DATEV->new(
-      exporttype => DATEV_ET_BUCHUNGEN,
-      format     => DATEV_FORMAT_KNE,
       dbh        => $dbh,
       trans_id   => $form->{id},
     );
 
-    $datev->export;
+    $datev->generate_datev_data;
 
     if ($datev->errors) {
-      $dbh->rollback;
       die join "\n", $::locale->text('DATEV check returned errors:'), $datev->errors;
     }
   }
 
-  my $rc = 1;
-  $dbh->commit if !$provided_dbh;
+  # update shop status
+  my $invoice = SL::DB::Invoice->new( id => $form->{id} )->load;
+  my @linked_shop_orders = $invoice->linked_records(
+    from      => 'ShopOrder',
+    via       => ['DeliveryOrder','Order',],
+  );
+    #do update
+    my $shop_order = $linked_shop_orders[0][0];
+  if ( $shop_order ) {
+    require SL::Shop;
+    my $shop_config = SL::DB::Manager::Shop->get_first( query => [ id => $shop_order->shop_id ] );
+    my $shop = SL::Shop->new( config => $shop_config );
+    $shop->connector->set_orderstatus($shop_order->shop_trans_id, "completed");
+  }
 
-  $main::lxdebug->leave_sub();
+  return 1;
+}
 
-  return $rc;
+sub _get_invoices_for_advance_payment {
+  my ($self, $id, $id_is_from_order) = @_;
+
+  return [] if !$id;
+
+  # Search all related invoices for advance payment.
+  # Case 1:
+  # (order) -> invoice for adv. payment 1 -> invoice for adv. payment 2 -> invoice for adv. payment 3 -> final invoice
+  #
+  # Case 2:
+  # order -> invoice for adv. payment 1
+  #   | |`-> invoice for adv. payment 2
+  #   | `--> invoice for adv. payment 3
+  #   `----> final invoice
+  #
+  # The id is currently that from the last invoice for adv. payment (3 in this example),
+  # that from the final invoice or that from the order.
+
+  my $invoice_obj;
+  my $order_obj;
+  my $links;
+
+  if (!$id_is_from_order) {
+    $invoice_obj = SL::DB::Invoice->load_cached($id*1);
+    $links       = $invoice_obj->linked_records(direction => 'from', from => ['Order']);
+    $order_obj   = $links->[0];
+  } else {
+    $order_obj   = SL::DB::Order->load_cached($id*1);
+  }
+
+  if ($order_obj) {
+    $links        = $order_obj  ->linked_records(direction => 'to',   to => ['Invoice']);
+  } else {
+    $links        = $invoice_obj->linked_records(direction => 'from', from => ['Invoice'], recursive => 1);
+  }
+
+  my @related_invoices = grep {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$links;
+
+  push @related_invoices, $invoice_obj if !$order_obj && "invoice_for_advance_payment" eq $invoice_obj->type;
+
+  return \@related_invoices;
 }
 
+
 sub transfer_out {
   $::lxdebug->enter_sub;
 
@@ -1384,7 +1670,7 @@ sub transfer_out {
 
   my (@errors, @transfers);
 
-  # do nothing, if transfer default is not requeseted at all
+  # do nothing, if transfer default is not requested at all
   if (!$::instance_conf->get_transfer_default) {
     $::lxdebug->leave_sub;
     return \@errors;
@@ -1394,26 +1680,56 @@ sub transfer_out {
 
   foreach my $i (1 .. $form->{rowcount}) {
     next if !$form->{"id_$i"};
-    my ($err, $wh_id, $bin_id) = _determine_wh_and_bin($dbh, $::instance_conf,
-                                                       $form->{"id_$i"},
-                                                       $form->{"qty_$i"},
-                                                       $form->{"unit_$i"});
-    if (!@{ $err } && $wh_id && $bin_id) {
-      push @transfers, {
-        'parts_id'         => $form->{"id_$i"},
-        'qty'              => $form->{"qty_$i"},
-        'unit'             => $form->{"unit_$i"},
-        'transfer_type'    => 'shipped',
-        'src_warehouse_id' => $wh_id,
-        'src_bin_id'       => $bin_id,
-        'project_id'       => $form->{"project_id_$i"},
-        'invoice_id'       => $form->{"invoice_id_$i"},
-        'comment'          => $::locale->text("Default transfer invoice"),
-      };
-    }
 
+    my ($err, $qty, $wh_id, $bin_id, $chargenumber);
+
+    if ($::instance_conf->get_sales_serial_eq_charge && $form->{"serialnumber_$i"}) {
+      my @serials = split(" ", $form->{"serialnumber_$i"});
+      if (scalar @serials != $form->{"qty_$i"}) {
+        push @errors, $::locale->text("Cannot transfer #1 qty with #2 serial number(s)", $form->{"qty_$i"}, scalar @serials);
+        last;
+      }
+      foreach my $serial (@serials) {
+        ($qty, $wh_id, $bin_id, $chargenumber) = WH->get_wh_and_bin_for_charge(chargenumber => $serial);
+        if (!$qty) {
+          push @errors, $::locale->text("Not enough in stock for the serial number #1", $serial);
+          last;
+        }
+        push @transfers, {
+            'parts_id'         => $form->{"id_$i"},
+            'qty'              => 1,
+            'unit'             => $form->{"unit_$i"},
+            'transfer_type'    => 'shipped',
+            'src_warehouse_id' => $wh_id,
+            'src_bin_id'       => $bin_id,
+            'chargenumber'     => $chargenumber,
+            'project_id'       => $form->{"project_id_$i"},
+            'invoice_id'       => $form->{"invoice_id_$i"},
+            'comment'          => $::locale->text("Default transfer invoice with charge number"),
+        };
+      }
+      $err = []; # error handling uses @errors direct
+    } else {
+      ($err, $wh_id, $bin_id)    = _determine_wh_and_bin($dbh, $::instance_conf,
+                                                         $form->{"id_$i"},
+                                                         $form->{"qty_$i"},
+                                                         $form->{"unit_$i"});
+      if (!@{ $err } && $wh_id && $bin_id) {
+        push @transfers, {
+          'parts_id'         => $form->{"id_$i"},
+          'qty'              => $form->{"qty_$i"},
+          'unit'             => $form->{"unit_$i"},
+          'transfer_type'    => 'shipped',
+          'src_warehouse_id' => $wh_id,
+          'src_bin_id'       => $bin_id,
+          'project_id'       => $form->{"project_id_$i"},
+          'invoice_id'       => $form->{"invoice_id_$i"},
+          'comment'          => $::locale->text("Default transfer invoice"),
+        };
+      }
+    }
     push @errors, @{ $err };
-  }
+  } # end form rowcount
 
   if (!@errors) {
     WH->transfer(@transfers);
@@ -1458,7 +1774,7 @@ sub _determine_wh_and_bin {
                                                       parts_id => $part->id,
                                                       bin_id   => $bin_id);
     if ($error == 1) {
-      push @errors, $::locale->text('Part "#1" has chargenumber or best before date set. So it cannot be transfered automaticaly.',
+      push @errors, $::locale->text('Part "#1" has chargenumber or best before date set. So it cannot be transfered automatically.',
                                     $part->description);
     }
     my $form_unit_obj = SL::DB::Unit->new(name => $unit)->load;
@@ -1546,17 +1862,33 @@ sub _delete_payments {
 }
 
 sub post_payment {
+  my ($self, $myconfig, $form, $locale) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_post_payment, $self, $myconfig, $form, $locale);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _post_payment {
   my ($self, $myconfig, $form, $locale) = @_;
 
-  # connect to database, turn off autocommit
-  my $dbh = $form->get_standard_dbh;
+  my $dbh = SL::DB->client->dbh;
 
   my (%payments, $old_form, $row, $item, $query, %keep_vars);
 
   $old_form = save_form();
 
+  $query = <<SQL;
+    SELECT at.acc_trans_id, at.amount, at.cleared, c.accno
+    FROM acc_trans at
+    LEFT JOIN chart c ON (at.chart_id = c.id)
+    WHERE (at.trans_id = ?)
+SQL
+
+  my %already_cleared = selectall_as_map($form, $dbh, $query, 'acc_trans_id', [ qw(amount cleared accno) ], $form->{id});
+
   # Delete all entries in acc_trans from prior payments.
   if (SL::DB::Default->get->payments_changeable != 0) {
     $self->_delete_payments($form, $dbh);
@@ -1603,15 +1935,11 @@ sub post_payment {
   ($form->{AR}) = selectfirst_array_query($form, $dbh, $query, conv_i($form->{id}));
 
   # Post the new payments.
-  $self->post_invoice($myconfig, $form, $dbh, 1);
+  $self->post_invoice($myconfig, $form, $dbh, payments_only => 1, already_cleared => \%already_cleared);
 
   restore_form($old_form);
 
-  my $rc = $dbh->commit();
-
-  $main::lxdebug->leave_sub();
-
-  return $rc;
+  return 1;
 }
 
 sub process_assembly {
@@ -1620,8 +1948,7 @@ sub process_assembly {
   my ($dbh, $myconfig, $form, $position, $id, $totalqty) = @_;
 
   my $query =
-    qq|SELECT a.parts_id, a.qty, p.assembly, p.partnumber, p.description, p.unit,
-         p.inventory_accno_id, p.income_accno_id, p.expense_accno_id
+    qq|SELECT a.parts_id, a.qty, p.part_type, p.partnumber, p.description, p.unit
        FROM assembly a
        JOIN parts p ON (a.parts_id = p.id)
        WHERE (a.id = ?)|;
@@ -1761,7 +2088,7 @@ sub reverse_invoice {
 
   # reverse inventory items
   my $query =
-    qq|SELECT i.id, i.parts_id, i.qty, i.assemblyitem, p.assembly, p.inventory_accno_id
+    qq|SELECT i.id, i.parts_id, i.qty, i.assemblyitem, p.part_type
        FROM invoice i
        JOIN parts p ON (i.parts_id = p.id)
        WHERE i.trans_id = ?|;
@@ -1798,18 +2125,30 @@ sub reverse_invoice {
   # delete acc_trans
   my @values = (conv_i($form->{id}));
   do_query($form, $dbh, qq|DELETE FROM acc_trans WHERE trans_id = ?|, @values);
+
+  $query = qq|DELETE FROM custom_variables
+              WHERE (config_id IN (SELECT id        FROM custom_variable_configs WHERE (module = 'ShipTo')))
+                AND (trans_id  IN (SELECT shipto_id FROM shipto                  WHERE (module = 'AR') AND (trans_id = ?)))|;
+  do_query($form, $dbh, $query, @values);
   do_query($form, $dbh, qq|DELETE FROM shipto WHERE (trans_id = ?) AND (module = 'AR')|, @values);
 
   $main::lxdebug->leave_sub();
 }
 
 sub delete_invoice {
+  my ($self, $myconfig, $form) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_delete_invoice, $self, $myconfig, $form);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _delete_invoice {
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->get_standard_dbh;
+  my $dbh = SL::DB->client->dbh;
 
   &reverse_invoice($dbh, $form);
   _delete_transfers($dbh, $form, $form->{id});
@@ -1830,6 +2169,18 @@ sub delete_invoice {
     do_query($form, $dbh, qq|UPDATE ar SET storno = 'f', paid = 0 WHERE id = ?|, $invoice_id);
   }
 
+  # if we delete a final invoice, the reverse bookings for the clearing account in the invoice for advance payment
+  # must be deleted as well
+  my $invoices_for_advance_payment = $self->_get_invoices_for_advance_payment($form->{id});
+
+  # Todo: allow only if invoice for advance payment is not paid.
+  # die if any { $_->paid } for @$invoices_for_advance_payment;
+  my @trans_ids_to_consider        = map { $_->id } @$invoices_for_advance_payment;
+  if (scalar @trans_ids_to_consider) {
+    my $query = sprintf 'DELETE FROM acc_trans WHERE memo LIKE ? AND trans_id IN (%s)', join ', ', ("?") x scalar @trans_ids_to_consider;
+    do_query($form, $dbh, $query, 'reverse booking by final invoice', @trans_ids_to_consider);
+  }
+
   # delete spool files
   my @spoolfiles = selectall_array_query($form, $dbh, qq|SELECT spoolfile FROM status WHERE trans_id = ?|, @values);
 
@@ -1842,25 +2193,26 @@ sub delete_invoice {
 
   map { do_query($form, $dbh, $_, @values) } @queries;
 
-  my $rc = $dbh->commit;
+  my $spool = $::lx_office_conf{paths}->{spool};
+  map { unlink "$spool/$_" if -f "$spool/$_"; } @spoolfiles;
 
-  if ($rc) {
-    my $spool = $::lx_office_conf{paths}->{spool};
-    map { unlink "$spool/$_" if -f "$spool/$_"; } @spoolfiles;
-  }
-
-  $main::lxdebug->leave_sub();
-
-  return $rc;
+  return 1;
 }
 
 sub retrieve_invoice {
+  my ($self, $myconfig, $form) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_retrieve_invoice, $self, $myconfig, $form);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _retrieve_invoice {
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->get_standard_dbh;
+  my $dbh = SL::DB->client->dbh;
 
   my ($sth, $ref, $query);
 
@@ -1872,7 +2224,9 @@ sub retrieve_invoice {
          (SELECT c.accno FROM chart c WHERE d.income_accno_id = c.id)    AS income_accno,
          (SELECT c.accno FROM chart c WHERE d.expense_accno_id = c.id)   AS expense_accno,
          (SELECT c.accno FROM chart c WHERE d.fxgain_accno_id = c.id)    AS fxgain_accno,
-         (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id = c.id)    AS fxloss_accno
+         (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id = c.id)    AS fxloss_accno,
+         (SELECT c.accno FROM chart c WHERE d.rndgain_accno_id = c.id)   AS rndgain_accno,
+         (SELECT c.accno FROM chart c WHERE d.rndloss_accno_id = c.id)   AS rndloss_accno
          ${query_transdate}
        FROM defaults d|;
 
@@ -1889,14 +2243,15 @@ sub retrieve_invoice {
       qq|SELECT
            a.invnumber, a.ordnumber, a.quonumber, a.cusordnumber,
            a.orddate, a.quodate, a.globalproject_id,
-           a.transdate AS invdate, a.deliverydate, a.paid, a.storno, a.gldate,
+           a.transdate AS invdate, a.deliverydate, a.tax_point, a.paid, a.storno, a.storno_id, a.gldate,
            a.shippingpoint, a.shipvia, a.notes, a.intnotes, a.taxzone_id,
            a.duedate, a.taxincluded, (SELECT cu.name FROM currencies cu WHERE cu.id=a.currency_id) AS currency, a.shipto_id, a.cp_id,
+           a.billing_address_id,
            a.employee_id, a.salesman_id, a.payment_id,
            a.mtime, a.itime,
            a.language_id, a.delivery_customer_id, a.delivery_vendor_id, a.type,
            a.transaction_description, a.donumber, a.invnumber_for_credit_note,
-           a.marge_total, a.marge_percent, a.direct_debit, a.delivery_term_id,
+           a.marge_total, a.marge_percent, a.direct_debit, a.qrbill_without_amount, a.delivery_term_id,
            dc.dunning_description,
            e.name AS employee
          FROM ar a
@@ -1915,6 +2270,11 @@ sub retrieve_invoice {
       ($form->{"delivery_${vc}_string"}) = selectrow_query($form, $dbh, qq|SELECT name FROM customer WHERE id = ?|, $id);
     }
 
+    # get shipto
+    $query = qq|SELECT * FROM shipto WHERE (trans_id = ?) AND (module = 'AR')|;
+    $ref = selectfirst_hashref_query($form, $dbh, $query, $id);
+    $form->{$_} = $ref->{$_} for grep { m{^shipto(?!_id$)} } keys %$ref;
+
     # get printed, emailed
     $query = qq|SELECT printed, emailed, spoolfile, formname FROM status WHERE trans_id = ?|;
     $sth = prepare_execute_query($form, $dbh, $query, $id);
@@ -1927,7 +2287,8 @@ sub retrieve_invoice {
     $sth->finish;
     map { $form->{$_} =~ s/ +$//g } qw(printed emailed queued);
 
-    my $transdate = $form->{deliverydate} ? $dbh->quote($form->{deliverydate})
+    my $transdate = $form->{tax_point}    ? $dbh->quote($form->{tax_point})
+                  : $form->{deliverydate} ? $dbh->quote($form->{deliverydate})
                   : $form->{invdate}      ? $dbh->quote($form->{invdate})
                   :                         "current_date";
 
@@ -1946,7 +2307,8 @@ sub retrieve_invoice {
            i.description, i.longdescription, i.qty, i.fxsellprice AS sellprice, i.discount, i.parts_id AS id, i.unit, i.deliverydate AS reqdate,
            i.project_id, i.serialnumber, i.pricegroup_id, i.ordnumber, i.donumber, i.transdate, i.cusordnumber, i.subtotal, i.lastcost,
            i.price_factor_id, i.price_factor, i.marge_price_factor, i.active_price_source, i.active_discount_source,
-           p.partnumber, p.assembly, p.notes AS partnotes, p.inventory_accno_id AS part_inventory_accno_id, p.formel, p.listprice,
+           p.partnumber, p.part_type, p.notes AS partnotes, p.formel, p.listprice,
+           p.classification_id,
            pr.projectnumber, pg.partsgroup, prg.pricegroup
 
          FROM invoice i
@@ -1985,7 +2347,8 @@ sub retrieve_invoice {
       # get tax rates and description
       my $accno_id = ($form->{vc} eq "customer") ? $ref->{income_accno} : $ref->{expense_accno};
       $query =
-        qq|SELECT c.accno, t.taxdescription, t.rate, t.taxnumber FROM tax t
+        qq|SELECT c.accno, t.taxdescription, t.rate, t.id as tax_id, c.accno as taxnumber
+           FROM tax t
            LEFT JOIN chart c ON (c.id = t.chart_id)
            WHERE t.id IN
              (SELECT tk.tax_id FROM taxkeys tk
@@ -2007,7 +2370,8 @@ sub retrieve_invoice {
         if (!($form->{taxaccounts} =~ /\Q$ptr->{accno}\E/)) {
           $form->{"$ptr->{accno}_rate"}        = $ptr->{rate};
           $form->{"$ptr->{accno}_description"} = $ptr->{taxdescription};
-          $form->{"$ptr->{accno}_taxnumber"}   = $ptr->{taxnumber};
+          $form->{"$ptr->{accno}_taxnumber"}   = $ptr->{taxnumber}; # don't use this anymore
+          $form->{"$ptr->{accno}_tax_id"}      = $ptr->{tax_id};
           $form->{taxaccounts} .= "$ptr->{accno} ";
         }
 
@@ -2021,14 +2385,25 @@ sub retrieve_invoice {
     }
     $sth->finish;
 
-    Common::webdav_folder($form);
-  }
+    # Fetch shipping address.
+    $query = qq|SELECT s.* FROM shipto s WHERE s.trans_id = ? AND s.module = 'AR'|;
+    $ref   = selectfirst_hashref_query($form, $dbh, $query, $form->{id});
 
-  my $rc = $dbh->commit;
+    $form->{$_} = $ref->{$_} for grep { $_ ne 'id' } keys %$ref;
 
-  $main::lxdebug->leave_sub();
+    if ($form->{shipto_id}) {
+      my $cvars = CVar->get_custom_variables(
+        dbh      => $dbh,
+        module   => 'ShipTo',
+        trans_id => $form->{shipto_id},
+      );
+      $form->{"shiptocvar_$_->{name}"} = $_->{value} for @{ $cvars };
+    }
 
-  return $rc;
+    Common::webdav_folder($form);
+  }
+
+  return 1;
 }
 
 sub get_customer {
@@ -2048,23 +2423,31 @@ sub get_customer {
   my $payment_id;
 
   # get customer
+  my $where = '';
+  if ($cid) {
+    $where .= 'AND c.id = ?';
+    push @values, $cid;
+  }
   $query =
     qq|SELECT
          c.id AS customer_id, c.name AS customer, c.discount as customer_discount, c.creditlimit,
          c.email, c.cc, c.bcc, c.language_id, c.payment_id, c.delivery_term_id,
          c.street, c.zipcode, c.city, c.country,
-         c.notes AS intnotes, c.klass as customer_klass, c.taxzone_id, c.salesman_id, cu.name AS curr,
+         c.notes AS intnotes, c.pricegroup_id as customer_pricegroup_id, c.taxzone_id, c.salesman_id, cu.name AS curr,
          c.taxincluded_checked, c.direct_debit,
+         (SELECT aba.id
+          FROM additional_billing_addresses aba
+          WHERE aba.default_address
+          LIMIT 1) AS default_billing_address_id,
          b.discount AS tradediscount, b.description AS business
        FROM customer c
        LEFT JOIN business b ON (b.id = c.business_id)
        LEFT JOIN currencies cu ON (c.currency_id=cu.id)
-       WHERE c.id = ?|;
-  push @values, $cid;
+       WHERE 1 = 1 $where|;
   $ref = selectfirst_hashref_query($form, $dbh, $query, @values);
 
   delete $ref->{salesman_id} if !$ref->{salesman_id};
-  delete $ref->{payment_id}  if $form->{payment_id};
+  delete $ref->{payment_id}  if !$ref->{payment_id};
 
   map { $form->{$_} = $ref->{$_} } keys %$ref;
 
@@ -2119,46 +2502,6 @@ sub get_customer {
   }
   $sth->finish;
 
-  # setup last accounts used for this customer
-  if (!$form->{id} && $form->{type} !~ /_(order|quotation)/) {
-    $query =
-      qq|SELECT c.id, c.accno, c.description, c.link, c.category
-         FROM chart c
-         JOIN acc_trans ac ON (ac.chart_id = c.id)
-         JOIN ar a ON (a.id = ac.trans_id)
-         WHERE a.customer_id = ?
-           AND NOT (c.link LIKE '%_tax%' OR c.link LIKE '%_paid%')
-           AND a.id IN (SELECT max(a2.id) FROM ar a2 WHERE a2.customer_id = ?)|;
-    $sth = prepare_execute_query($form, $dbh, $query, $cid, $cid);
-
-    my $i = 0;
-    while ($ref = $sth->fetchrow_hashref('NAME_lc')) {
-      if ($ref->{category} eq 'I') {
-        $i++;
-        $form->{"AR_amount_$i"} = "$ref->{accno}--$ref->{description}";
-
-        if ($form->{initial_transdate}) {
-          my $tax_query =
-            qq|SELECT tk.tax_id, t.rate
-               FROM taxkeys tk
-               LEFT JOIN tax t ON tk.tax_id = t.id
-               WHERE (tk.chart_id = ?) AND (startdate <= date(?))
-               ORDER BY tk.startdate DESC
-               LIMIT 1|;
-          my ($tax_id, $rate) =
-            selectrow_query($form, $dbh, $tax_query, $ref->{id},
-                            $form->{initial_transdate});
-          $form->{"taxchart_$i"} = "${tax_id}--${rate}";
-        }
-      }
-      if ($ref->{category} eq 'A') {
-        $form->{ARselected} = $form->{AR_1} = $ref->{accno};
-      }
-    }
-    $sth->finish;
-    $form->{rowcount} = $i if ($i && !$form->{type});
-  }
-
   $main::lxdebug->leave_sub();
 }
 
@@ -2179,7 +2522,7 @@ sub retrieve_item {
     my ($table, $field) = split m/\./, $column;
     next if !$form->{"${field}_${i}"};
     $where .= qq| AND lower(${column}) ILIKE ?|;
-    push @values, '%' . $form->{"${field}_${i}"} . '%';
+    push @values, like($form->{"${field}_${i}"});
   }
 
   my (%mm_by_id);
@@ -2191,7 +2534,7 @@ sub retrieve_item {
     my $mm_query = qq|
       SELECT parts_id, model FROM makemodel LEFT JOIN parts ON parts.id = parts_id WHERE NOT parts.obsolete AND model ILIKE ?;
     |;
-    my $mm_results = selectall_hashref_query($::form, $dbh, $mm_query, '%' . $form->{"partnumber_$i"} . '%');
+    my $mm_results = selectall_hashref_query($::form, $dbh, $mm_query, like($form->{"partnumber_$i"}));
     my @mm_ids     = map { $_->{parts_id} } @$mm_results;
     push @{$mm_by_id{ $_->{parts_id} } ||= []}, $_ for @$mm_results;
 
@@ -2231,8 +2574,9 @@ sub retrieve_item {
   my $query =
     qq|SELECT
          p.id, p.partnumber, p.description, p.sellprice,
-         p.listprice, p.inventory_accno_id, p.lastcost,
-         p.ean,
+         p.listprice, p.part_type, p.lastcost,
+         p.ean, p.notes,
+         p.classification_id,
 
          c1.accno AS inventory_accno,
          c1.new_chart_id AS inventory_new_chart,
@@ -2246,13 +2590,13 @@ sub retrieve_item {
          c3.new_chart_id AS expense_new_chart,
          date($transdate) - c3.valid_from AS expense_valid,
 
-         p.unit, p.assembly, p.onhand,
+         p.unit, p.part_type, p.onhand,
          p.notes AS partnotes, p.notes AS longdescription,
          p.not_discountable, p.formel, p.payment_id AS part_payment_id,
          p.price_factor_id, p.weight,
 
          pfac.factor AS price_factor,
-
+         pt.used_for_sale AS used_for_sale,
          pg.partsgroup
 
        FROM parts p
@@ -2269,6 +2613,7 @@ sub retrieve_item {
            FROM taxzone_charts tc
            WHERE tc.buchungsgruppen_id = p.buchungsgruppen_id and tc.taxzone_id = ${taxzone_id}) = c3.id)
        LEFT JOIN partsgroup pg ON (pg.id = p.partsgroup_id)
+       LEFT JOIN part_classifications pt ON (pt.id = p.classification_id)
        LEFT JOIN price_factors pfac ON (pfac.id = p.price_factor_id)
        WHERE $where|;
   my $sth = prepare_execute_query($form, $dbh, $query, @values);
@@ -2286,6 +2631,7 @@ sub retrieve_item {
                                    LIMIT 1| ] );
   map { push @{ $_ }, prepare_query($form, $dbh, $_->[0]) } @translation_queries;
 
+  my $has_wrong_pclass = PCLASS_OK;
   while (my $ref = $sth->fetchrow_hashref('NAME_lc')) {
 
     if ($mm_by_id{$ref->{id}}) {
@@ -2293,10 +2639,16 @@ sub retrieve_item {
       push @{ $ref->{matches} ||= [] }, $::locale->text('Model') . ': ' . join ', ', map { $_->{model} } @{ $mm_by_id{$ref->{id}} };
     }
 
-    if ($ref->{ean} eq $::form->{"partnumber_$i"}) {
+    if (($::form->{"partnumber_$i"} ne '') && ($ref->{ean} eq $::form->{"partnumber_$i"})) {
       push @{ $ref->{matches} ||= [] }, $::locale->text('EAN') . ': ' . $ref->{ean};
     }
 
+    $ref->{type_and_classific} = type_abbreviation($ref->{part_type}) .
+                                 classification_abbreviation($ref->{classification_id});
+    if (! $ref->{used_for_sale} ) {
+      $has_wrong_pclass = PCLASS_NOTFORSALE ;
+      next;
+    }
     # In der Buchungsgruppe ist immer ein Bestandskonto verknuepft, auch wenn
     # es sich um eine Dienstleistung handelt. Bei Dienstleistungen muss das
     # Buchungskonto also aus dem Ergebnis rausgenommen werden.
@@ -2325,7 +2677,7 @@ sub retrieve_item {
     # get tax rates and description
     my $accno_id = ($form->{vc} eq "customer") ? $ref->{income_accno} : $ref->{expense_accno};
     $query =
-      qq|SELECT c.accno, t.taxdescription, t.rate, t.taxnumber
+      qq|SELECT c.accno, t.taxdescription, t.id as tax_id, t.rate, c.accno as taxnumber
          FROM tax t
          LEFT JOIN chart c ON (c.id = t.chart_id)
          WHERE t.id in
@@ -2354,6 +2706,7 @@ sub retrieve_item {
         $form->{"$ptr->{accno}_rate"}        = $ptr->{rate};
         $form->{"$ptr->{accno}_description"} = $ptr->{taxdescription};
         $form->{"$ptr->{accno}_taxnumber"}   = $ptr->{taxnumber};
+        $form->{"$ptr->{accno}_tax_id"}      = $ptr->{tax_id};
         $form->{taxaccounts} .= "$ptr->{accno} ";
       }
 
@@ -2374,21 +2727,22 @@ sub retrieve_item {
     }
 
     $ref->{onhand} *= 1;
-
     push @{ $form->{item_list} }, $ref;
   }
   $sth->finish;
   $_->[1]->finish for @translation_queries;
 
+  $form->{is_wrong_pclass} = $has_wrong_pclass;
+  $form->{NOTFORSALE}      = PCLASS_NOTFORSALE;
+  $form->{NOTFORPURCHASE}  = PCLASS_NOTFORPURCHASE;
   foreach my $item (@{ $form->{item_list} }) {
     my $custom_variables = CVar->get_custom_variables(module   => 'IC',
                                                       trans_id => $item->{id},
                                                       dbh      => $dbh,
                                                      );
-
+    $form->{is_wrong_pclass} = PCLASS_OK; # one correct type
     map { $item->{"ic_cvar_" . $_->{name} } = $_->{value} } @{ $custom_variables };
   }
-
   $main::lxdebug->leave_sub();
 }
 
index 6ecb41f..30afc66 100644 (file)
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #=====================================================================
 #
 # routines to retrieve / manipulate win ini style files
index 4d46fd6..acb34c5 100644 (file)
@@ -17,61 +17,84 @@ BEGIN {
 #   dist_name: name of the package in cpan if it differs from name (ex.: LWP != libwww-perl)
 @required_modules = (
   { name => "parent",                              url => "http://search.cpan.org/~corion/",    debian => 'libparent-perl' },
-  { name => "Archive::Zip",    version => '1.16',  url => "http://search.cpan.org/~phred/",     debian => 'libarchive-zip-perl' },
-  { name => "CGI",             version => '3.43',  url => "http://search.cpan.org/~leejo/",     debian => 'libcgi-perl' }, # 3.43 is core 5.10.1
+  { name => "Algorithm::CheckDigits",              url => "http://search.cpan.org/~mamawe/",    debian => 'libalgorithm-checkdigits-perl' },
+  { name => "Archive::Zip",    version => '1.40',  url => "http://search.cpan.org/~phred/",     debian => 'libarchive-zip-perl' },
+  { name => "CAM::PDF",                            url => "https://metacpan.org/pod/CAM::PDF",  debian => 'libcam-pdf-perl' },
+  { name => "CGI",             version => '3.43',  url => "http://search.cpan.org/~leejo/",     debian => 'libcgi-pm-perl' }, # 4.09 is not core anymore (perl 5.20)
   { name => "Clone",                               url => "http://search.cpan.org/~rdf/",       debian => 'libclone-perl' },
   { name => "Config::Std",                         url => "http://search.cpan.org/~dconway/",   debian => 'libconfig-std-perl' },
+  { name => "Daemon::Generic", version => '0.71',  url => "http://search.cpan.org/~muir/",      debian => 'libdaemon-generic-perl'},
   { name => "DateTime",                            url => "http://search.cpan.org/~drolsky/",   debian => 'libdatetime-perl' },
+  { name => "DateTime::Event::Cron", version => '0.08', url => "http://search.cpan.org/~msisk/", debian => 'libdatetime-event-cron-perl' },
   { name => "DateTime::Format::Strptime",          url => "http://search.cpan.org/~drolsky/",   debian => 'libdatetime-format-strptime-perl' },
+  { name => "DateTime::Set",   version => '0.12',  url => "http://search.cpan.org/~fglock/",    debian => 'libdatetime-set-perl' },
   { name => "DBI",             version => '1.50',  url => "http://search.cpan.org/~timb/",      debian => 'libdbi-perl' },
   { name => "DBD::Pg",         version => '1.49',  url => "http://search.cpan.org/~dbdpg/",     debian => 'libdbd-pg-perl' },
-  { name => "Email::Address",                      url => "http://search.cpan.org/~rjbs/",      debian => 'libemail-address-perl' },
+  { name => "Digest::SHA",                         url => "http://search.cpan.org/~mshelor/",   debian => 'libdigest-sha-perl' },
+  { name => "Exception::Class", version => '1.44', url => "https://metacpan.org/pod/Exception::Class", debian => 'libexception-class-perl' },
+  { name => "Email::Address",  version => '1.888', url => "http://search.cpan.org/~rjbs/",      debian => 'libemail-address-perl' },
   { name => "Email::MIME",                         url => "http://search.cpan.org/~rjbs/",      debian => 'libemail-mime-perl' },
   { name => "FCGI",            version => '0.72',  url => "http://search.cpan.org/~mstrout/",   debian => 'libfcgi-perl' },
   { name => "File::Copy::Recursive",               url => "http://search.cpan.org/~dmuey/",     debian => 'libfile-copy-recursive-perl' },
+  { name => "File::Flock",   version => '2008.01', url => "http://search.cpan.org/~muir/",      debian => 'libfile-flock-perl' },
+  { name => "File::MimeInfo",                      url => "http://search.cpan.org/~michielb/",  debian => 'libfile-mimeinfo-perl' },
+  { name => "File::Slurp",                         url => "https://metacpan.org/author/CAPOEIRAB", debian => 'libfile-slurp-perl' },
   { name => "GD",                                  url => "http://search.cpan.org/~lds/",       debian => 'libgd-gd2-perl', },
   { name => 'HTML::Parser',                        url => 'http://search.cpan.org/~gaas/',      debian => 'libhtml-parser-perl', },
-  { name => 'HTML::Restrict',                      url => 'http://search.cpan.org/~oalders/', },
+  { name => 'HTML::Restrict',                      url => 'http://search.cpan.org/~oalders/',   debian => 'libhtml-restrict-perl'},
   { name => "Image::Info",                         url => "http://search.cpan.org/~srezic/",    debian => 'libimage-info-perl' },
+  { name => "Imager",                              url => "http://search.cpan.org/~tonyc/",     debian => 'libimager-perl' },
+  { name => "Imager::QRCode",                      url => "http://search.cpan.org/~kurihara/",  debian => 'libimager-qrcode-perl' },
+  { name => "IPC::Run",                            url => "https://metacpan.org/pod/IPC::Run",  debian => 'libipc-run-perl' },
   { name => "JSON",                                url => "http://search.cpan.org/~makamaka",   debian => 'libjson-perl' },
-  { name => "List::MoreUtils", version => '0.21',  url => "http://search.cpan.org/~vparseval/", debian => 'liblist-moreutils-perl' },
-  { name => "List::UtilsBy",                       url => "http://search.cpan.org/~pevans/",    debian => 'liblist-utilsby-perl' },
+  { name => "List::MoreUtils", version => '0.30',  url => "http://search.cpan.org/~vparseval/", debian => 'liblist-moreutils-perl' },
+  { name => "List::UtilsBy",   version => '0.09',  url => "http://search.cpan.org/~pevans/",    debian => 'liblist-utilsby-perl' },
+  { name => "LWP::Authen::Digest",                 url => "http://search.cpan.org/~gaas/",      debian => 'libwww-perl', dist_name => 'libwww-perl' },
+  { name => "LWP::UserAgent",                      url => "http://search.cpan.org/~gaas/",      debian => 'libwww-perl', dist_name => 'libwww-perl' },
+  { name => "Math::Round",                         url => "https://metacpan.org/pod/Math::Round", debian => 'libmath-round-perl' },
   { name => "Params::Validate",                    url => "http://search.cpan.org/~drolsky/",   debian => 'libparams-validate-perl' },
+  { name => "PBKDF2::Tiny",    version => '0.005', url => "http://search.cpan.org/~dagolden/",  debian => 'libpbkdf2-tiny-perl' },
   { name => "PDF::API2",       version => '2.000', url => "http://search.cpan.org/~areibens/",  debian => 'libpdf-api2-perl' },
+  { name => "Regexp::IPv6",    version => '0.03',  url => "http://search.cpan.org/~salva/",     debian => 'libregexp-ipv6-perl' },
+  { name => "REST::Client",                        url => "https://metacpan.org/pod/REST::Client", debian => 'librest-client-perl' },
   { name => "Rose::Object",                        url => "http://search.cpan.org/~jsiracusa/", debian => 'librose-object-perl' },
   { name => "Rose::DB",                            url => "http://search.cpan.org/~jsiracusa/", debian => 'librose-db-perl' },
   { name => "Rose::DB::Object", version => 0.788,  url => "http://search.cpan.org/~jsiracusa/", debian => 'librose-db-object-perl' },
+  { name => "Set::Infinite",    version => '0.63', url => "http://search.cpan.org/~fglock/",    debian => 'libset-infinite-perl' },
   { name => "String::ShellQuote", version => 1.01, url => "http://search.cpan.org/~rosch/",     debian => 'libstring-shellquote-perl' },
   { name => "Sort::Naturally",                     url => "http://search.cpan.org/~sburke/",    debian => 'libsort-naturally-perl' },
-  # Test::Harness is core, so no Debian packages. Test::Harness 3.00 was first packaged in 5.10.1
-  { name => "Test::Harness",   version => '3.00',  url => "http://search.cpan.org/~petdance/",  },
   { name => "Template",        version => '2.18',  url => "http://search.cpan.org/~abw/",       debian => 'libtemplate-perl' },
   { name => "Text::CSV_XS",    version => '0.23',  url => "http://search.cpan.org/~hmbrand/",   debian => 'libtext-csv-xs-perl' },
   { name => "Text::Iconv",     version => '1.2',   url => "http://search.cpan.org/~mpiotr/",    debian => 'libtext-iconv-perl' },
+  { name => "Text::Unidecode",                     url => "http://search.cpan.org/~sburke/",    debian => 'libtext-unidecode-perl' },
+  { name => "Try::Tiny",                           url => "https://metacpan.org/release/Try-Tiny", debian => 'libtry-tiny-perl' },
   { name => "URI",             version => '1.35',  url => "http://search.cpan.org/~gaas/",      debian => 'liburi-perl' },
+  { name => "XML::LibXML",                         url => "https://metacpan.org/pod/XML::LibXML", debian => 'libxml-libxml-perl' },
   { name => "XML::Writer",     version => '0.602', url => "http://search.cpan.org/~josephw/",   debian => 'libxml-writer-perl' },
   { name => "YAML",            version => '0.62',  url => "http://search.cpan.org/~ingy/",      debian => 'libyaml-perl' },
 );
 
 @optional_modules = (
-  { name => "Digest::SHA",                         url => "http://search.cpan.org/~mshelor/",   debian => 'libdigest-sha-perl' },
   { name => "IO::Socket::SSL",                     url => "http://search.cpan.org/~sullr/",     debian => 'libio-socket-ssl-perl' },
   { name => "Net::LDAP",                           url => "http://search.cpan.org/~gbarr/",     debian => 'libnet-ldap-perl' },
   # Net::SMTP is core since 5.7.3
   { name => "Net::SMTP::SSL",                      url => "http://search.cpan.org/~cwest/",     debian => 'libnet-smtp-ssl-perl' },
   { name => "Net::SSLGlue",                        url => "http://search.cpan.org/~sullr/",     debian => 'libnet-sslglue-perl' },
+  { name => "YAML::XS",                            url => "https://metacpan.org/pod/distribution/YAML-LibYAML/lib/YAML/LibYAML.pod", debian => 'libyaml-libyaml-perl' },
 );
 
 @developer_modules = (
   { name => "DBIx::Log4perl",                      url => "http://search.cpan.org/~mjevans/", },
   { name => "Devel::REPL",                         url => "http://search.cpan.org/~doy/",       debian => 'libdevel-repl-perl' },
+  { name => "Term::ReadLine::Gnu",                 url => "http://search.cpan.org/~hayashi/",   debian => 'libterm-readline-gnu-perl' },
   { name => "Log::Log4perl",                       url => "http://search.cpan.org/~mschilli/",  debian => 'liblog-log4perl-perl' },
   { name => "LWP::Simple",                         url => "http://search.cpan.org/~gaas/",      debian => 'libwww-perl', dist_name => 'libwww-perl' },
   { name => "Moose::Role",                         url => "http://search.cpan.org/~doy/",       debian => 'libmoose-perl' },
-  { name => "Perl::Tags",                          url => "http://search.cpan.org/~osfameron/", debian => 'libperl-tags-perl' },
+  { name => "Sys::CPU",                            url => "http://search.cpan.org/~mkoderer/",  debian => 'libsys-cpu-perl' },
   { name => "Test::Deep",                          url => "http://search.cpan.org/~rjbs/",      debian => 'libtest-deep-perl' },
   { name => "Test::Exception",                     url => "http://search.cpan.org/~adie/",      debian => 'libtest-exception-perl' },
   { name => "Test::Output",                        url => "http://search.cpan.org/~bdfoy/",     debian => 'libtest-output-perl' },
+  { name => "Thread::Pool::Simple",                url => "http://search.cpan.org/~jwu/",       debian => 'libthread-pool-simple-perl' },
   { name => "URI::Find",                           url => "http://search.cpan.org/~mschwern/",  debian => 'liburi-find-perl' },
   { name => "GD",              version => '2.00',  url => "http://search.cpan.org/~lds/",       debian => 'libgd-perl' },
   { name => "Rose::DB::Object", version => 0.809,  url => "http://search.cpan.org/~jsiracusa/", debian => 'librose-db-object-perl' },
@@ -112,9 +135,9 @@ sub template_dirs {
 
 sub classes_from_latex {
   my ($path, $class) = @_;
-  eval { use String::ShellQuote; 1 } or warn "can't load String::ShellQuote" && return;
-  $path  = shell_quote $path;
-  $class = shell_quote $class;
+  eval { require String::ShellQuote; 1 } or warn "can't load String::ShellQuote" && return;
+  $path  = String::ShellQuote::shell_quote $path;
+  $class = String::ShellQuote::shell_quote $class;
 
   open my $pipe, q#egrep -rs '^[\ \t]*# . "$class' $path". q# | sed 's/ //g' | awk -F '{' '{print $2}' | awk -F '}' '{print $1}' |#;
   my @cls = <$pipe>;
index 905f38b..4c76796 100644 (file)
@@ -52,6 +52,16 @@ sub get_currencies {
   return @{ $self->currencies };
 }
 
+sub get_address {
+  # Compatibility function: back in the day there was only a single
+  # address field.
+  my ($self) = @_;
+
+  my $zipcode_city = join ' ', grep { $_ } ($self->get_address_zipcode, $self->get_address_city);
+
+  return join "\n", grep { $_ } ($self->get_address_street1, $self->get_address_street2, $zipcode_city, $self->get_address_country);
+}
+
 sub AUTOLOAD {
   our $AUTOLOAD;
 
@@ -176,7 +186,7 @@ Returns the default behavior for showing best before date, true or false
 
 =item C<get_ap_show_mark_as_paid>
 
-Returns the default behavior for showing the mark as paid button for the
+Returns the default behavior for showing the "mark as paid" button for the
 corresponding record type (true or false).
 
 =item C<get_sales_order_show_delete>
@@ -205,11 +215,9 @@ current stock quantity
 
 =item C<get_bin_id_ignore_onhand>
 
-Returns the default bin_id for transfers without checking the.
+Returns the default bin_id for transfers without checking the
 current stock quantity
 
-
-
 =item C<get_transfer_default>
 
 =item C<get_transfer_default_use_master_default_bin>
@@ -234,6 +242,14 @@ Returns the configuration for storing documents in the corresponding WebDAV fold
 
 Returns the configuration for "vertreter"
 
+=item C<get_feature_experimental_assortment>
+
+Returns the configuration for experimental feature "assortment"
+
+=item C<get_feature_experimental_order>
+
+Returns the configuration for the experimental feature "order"
+
 =item C<get_parts_show_image>
 
 Returns the configuarion for show image in parts
@@ -244,7 +260,7 @@ Returns the css format string for images shown in parts
 
 =item C<get_parts_listing_image>
 
-Returns the configuartion for showing the picture in the results when you search for parts
+Returns the configuration for showing the picture in the results when you search for parts
 
 =back
 
index 6602b39..a09ef45 100644 (file)
@@ -28,7 +28,7 @@ sub to_json {
 }
 
 sub from_json {
-  goto &JSON::decode_json;
+  goto &JSON::from_json;
 }
 
 1;
index 553ab50..a1e1976 100644 (file)
@@ -11,17 +11,20 @@ use constant REQUEST_TIMER      =>  1 << 6;
 use constant REQUEST            =>  1 << 7;
 use constant WARN               =>  1 << 8;
 use constant TRACE2             =>  1 << 9;
-use constant ALL                => (1 << 10) - 1;
+use constant SHOW_CALLER        =>  1 << 10;
+use constant ALL                => (1 << 11) - 1;
 use constant DEVEL              => INFO | DEBUG1 | QUERY | TRACE | BACKTRACE_ON_ERROR | REQUEST_TIMER;
 
 use constant FILE_TARGET   => 0;
 use constant STDERR_TARGET => 1;
 
 use Data::Dumper;
-use POSIX qw(strftime getppid);
+use List::MoreUtils qw(all);
+use POSIX qw(strftime getpid);
+use Scalar::Util qw(blessed refaddr weaken looks_like_number);
 use Time::HiRes qw(gettimeofday tv_interval);
-use YAML;
 use SL::Request ();
+use SL::YAML;
 
 use strict;
 use utf8;
@@ -144,7 +147,10 @@ sub message {
   no warnings;
   my ($self, $level, $message) = @_;
 
-  $self->_write(level2string($level), $message) if (($self->{"level"} | $global_level) & $level || !$level);
+  my $show_caller = ($level | $global_level) & SHOW_CALLER();
+  $level         &= ~SHOW_CALLER();
+
+  $self->_write(level2string($level), $message, show_caller => $show_caller) if (($self->{"level"} | $global_level) & $level || !$level);
 }
 sub warn {
   no warnings;
@@ -152,15 +158,50 @@ sub warn {
   $self->message(WARN, $message);
 }
 
-sub dump {
-  my ($self, $level, $name, $variable, %options) = @_;
+sub clone_for_dump {
+  my ($src, $dumped) = @_;
+
+  return undef if !defined($src);
+  return $src  if !ref($src);
+
+  $dumped ||= {};
+  my $addr  = refaddr($src);
+
+  return $dumped->{$addr} if $dumped->{$addr // ''};
+
+
+  if (blessed($src) && $src->can('as_debug_info')) {
+    $dumped->{$addr} = $src->as_debug_info;
+
+  } elsif (ref($src) eq 'ARRAY') {
+    $dumped->{$addr} = [];
+
+    foreach my $entry (@{ $src }) {
+      my $exists = !!$dumped->{refaddr($entry) // ''};
+      push @{ $dumped->{$addr} }, clone_for_dump($entry, $dumped);
 
-  my $password;
-  if ($variable && ('Form' eq ref $variable) && defined $variable->{password}) {
-    $password             = $variable->{password};
-    $variable->{password} = 'X' x 8;
+      weaken($dumped->{$addr}->[-1]) if $exists;
+
+    }
+
+  } elsif (ref($src) =~ m{^(?:HASH|Form|SL::.+)$}) {
+    $dumped->{$addr} = {};
+
+    foreach my $key (keys %{ $src }) {
+      my $exists             = !!$dumped->{refaddr($src->{$key}) // ''};
+      $dumped->{$addr}->{$key} = clone_for_dump($src->{$key}, $dumped);
+
+      weaken($dumped->{$addr}->{$key}) if $exists;
+    }
   }
 
+  return $dumped->{$addr} // "$src";
+}
+
+sub dump {
+  my ($self, $level, $name, $variable, %options) = @_;
+
+  $variable  = clone_for_dump($variable);
   my $dumper = Data::Dumper->new([$variable]);
   $dumper->Sortkeys(1);
   $dumper->Indent(2);
@@ -168,22 +209,13 @@ sub dump {
   my $output = $dumper->Dump();
   $self->message($level, "dumping ${name}:\n" . $output);
 
-  $variable->{password} = $password if (defined $password);
-
-  # Data::Dumper does not reset the iterator belonging to this hash
-  # if 'Sortkeys' is true. Therefore clear the iterator manually.
-  # See "perldoc -f each".
-  if ($variable && (('HASH' eq ref $variable) || ('Form' eq ref $variable))) {
-    keys %{ $variable };
-  }
-
   return $output;
 }
 
 sub dump_yaml {
   my ($self, $level, $name, $variable) = @_;
 
-  $self->message($level, "dumping ${name}:\n" . YAML::Dump($variable));
+  $self->message($level, "dumping ${name}:\n" . SL::YAML::Dump($variable));
 }
 
 sub dump_sql_result {
@@ -200,8 +232,14 @@ sub dump_sql_result {
     map { $column_lengths{$_} = length $row->{$_} if (length $row->{$_} > $column_lengths{$_}) } keys %{ $row };
   }
 
+  my %alignment;
+  foreach my $column (keys %column_lengths) {
+    my $all_look_like_number = all { (($_->{$column} // '') eq '') || looks_like_number($_->{$column}) } @{ $results };
+    $alignment{$column}      = $all_look_like_number ? '' : '-';
+  }
+
   my @sorted_names = sort keys %column_lengths;
-  my $format       = join '|', map { '%' . $column_lengths{$_} . 's' } @sorted_names;
+  my $format       = join '|', map { '%'  . $alignment{$_} . $column_lengths{$_} . 's' } @sorted_names;
 
   $prefix .= ' ' if $prefix;
 
@@ -214,17 +252,6 @@ sub dump_sql_result {
   $self->message($level, $prefix . sprintf('(%d row%s)', scalar @{ $results }, scalar @{ $results } > 1 ? 's' : ''));
 }
 
-sub dump_object {
-  my ($self, $level, $text, $object) = @_;
-
-  my $copy;
-  if ($object) {
-    $copy->{$_} = $object->$_ for $object->meta->columns;
-  }
-
-  $self->dump($level, $text, $copy);
-}
-
 sub show_diff {
   my ($self, $level, $item1, $item2, %params) = @_;
 
@@ -233,7 +260,7 @@ sub show_diff {
     return;
   }
 
-  my @texts = map { ref $_ ? YAML::Dump($_) : $_ } ($item1, $item2);
+  my @texts = map { ref $_ ? SL::YAML::Dump($_) : $_ } ($item1, $item2);
 
   $self->message($level, Text::Diff::diff(\$texts[0], \$texts[1], \%params));
 }
@@ -260,9 +287,29 @@ sub is_tracing_enabled {
 
 sub _write {
   no warnings;
-  my ($self, $prefix, $message) = @_;
+  my ($self, $prefix, $message, %options) = @_;
+
+  my @prefixes = ($prefix);
+
+  if ($options{show_caller}) {
+    my $level = 1;
+    while (1) {
+      my ($package, $filename, $line, $subroutine) = caller($level);
+
+      if (($filename // '') =~ m{LXDebug\.pm$}) {
+        $level++;
+        next;
+      }
+
+      push @prefixes, "${filename}:${line}";
+      last;
+    }
+  }
+
+  $prefix = join ' ', grep { $_ } @prefixes;
+
   my @now  = gettimeofday();
-  my $date = strftime("%Y-%m-%d %H:%M:%S." . sprintf('%03d', int($now[1] / 1000)) . " $$ [" . getppid() . "] ${prefix}: ", localtime($now[0]));
+  my $date = strftime("%Y-%m-%d %H:%M:%S." . sprintf('%03d', int($now[1] / 1000)) . " $$ [" . getpid() . "] ${prefix}: ", localtime($now[0]));
   local *FILE;
 
   chomp($message);
@@ -286,7 +333,7 @@ sub _write_raw {
 sub level2string {
   no warnings;
   # use $_[0] as a bit mask and return levelstrings separated by /
-  join '/', qw(info debug1 debug2 query trace error_call_trace request_timer WARNING)[ grep { (reverse split //, sprintf "%08b", $_[0])[$_] } 0..7 ]
+  join '/', qw(info debug1 debug2 query trace error_call_trace request_timer request WARNING trace2 show_caller)[ grep { (reverse split //, sprintf "%011b", $_[0])[$_] } 0..11 ]
 }
 
 sub begin_request {
@@ -296,9 +343,10 @@ sub begin_request {
 }
 
 sub end_request {
-  my $self = shift;
+  my ($self, %params) = @_;
   return 1 unless want_request_timer();
-  $self->_write("time", $self->get_request_time);
+
+  $self->_write("time", sprintf('%f (%s/%s)', $self->get_request_time, $params{script_name}, $params{action}));
 
   $self->{calldepth} = 0;
 }
@@ -308,7 +356,10 @@ sub log_time {
   return 1 unless want_request_timer();
 
   my $now                    = $self->get_request_time;
-  my $diff                   = int((($now - ($self->{previous_log_time} // 0)) * 10_000 + 5) / 10);
+
+  return 1 unless $now;
+
+  my $diff                   = $self->{previous_log_time} ? int((($now - ($self->{previous_log_time} // 0)) * 10_000 + 5) / 10) : $now * 10_0000 + 5;
   $self->{previous_log_time} = $now;
 
   $self->_write("time", "${now}s Δ ${diff}ms" . (@slurp ? " (@slurp)" : ''));
diff --git a/SL/Layout/ActionBar.pm b/SL/Layout/ActionBar.pm
new file mode 100644 (file)
index 0000000..5345e9e
--- /dev/null
@@ -0,0 +1,299 @@
+package SL::Layout::ActionBar;
+
+use strict;
+use parent qw(SL::Layout::Base);
+
+use Carp;
+use Scalar::Util qw(blessed);
+use SL::Layout::ActionBar::Action;
+use SL::Layout::ActionBar::ComboBox;
+use SL::Layout::ActionBar::Link;
+use SL::Layout::ActionBar::Separator;
+
+use SL::Presenter::Tag qw(html_tag);
+
+use constant HTML_CLASS => 'layout-actionbar';
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(actions) ],
+);
+
+my %class_descriptors = (
+  action    => { class => 'SL::Layout::ActionBar::Action',    num_params => 1, },
+  combobox  => { class => 'SL::Layout::ActionBar::ComboBox',  num_params => 1, },
+  link      => { class => 'SL::Layout::ActionBar::Link',      num_params => 1, },
+  separator => { class => 'SL::Layout::ActionBar::Separator', num_params => 0, },
+);
+
+###### Layout overrides
+
+sub pre_content {
+  my ($self) = @_;
+
+  my $content = join '', map { $_->render } @{ $self->actions };
+  return if !$content;
+  html_tag('div', $content, class => HTML_CLASS);
+}
+
+sub javascripts_inline {
+  join '', map { $_->script } @{ $_[0]->actions };
+}
+
+sub static_javascripts {
+  'kivi.ActionBar.js'
+}
+
+###### interface
+
+sub add {
+  my ($self, @actions) = @_;
+
+  push @{ $self->actions }, $self->parse_actions(@actions);
+
+  return $self->actions->[-1];
+}
+
+sub parse_actions {
+  my ($self_or_class, @actions) = @_;
+
+  my @parsed;
+
+  while (my $type = shift(@actions)) {
+    if (blessed($type) && $type->isa('SL::Layout::ActionBar::Action')) {
+      push @parsed, $type;
+      next;
+    }
+
+    my $descriptor = $class_descriptors{lc $type} || croak("Unknown action type '${type}'");
+    my @params     = splice(@actions, 0, $descriptor->{num_params});
+
+    push @parsed, $descriptor->{class}->from_params(@params);
+  }
+
+  return @parsed;
+}
+
+sub init_actions {
+  []
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Layout::ActionBar - Unified action buttons for controllers
+
+=head1 SYNOPSIS
+
+  # short sugared syntax:
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Description'),
+        call      => [ 'kivi.Javascript.function', @arguments ],
+        accesskey => 'enter',
+        disabled  => $tooltip_with_reason_or_falsish,
+        only_if   => $precomputed_condition,
+        not_if    => $precomputed_condition,
+        id        => 'html-element-id',
+      ],
+      combobox => [
+        action => [...],
+        action => [...],
+        action => [...],
+        action => [...],
+      ],
+      link => [
+        t8('Description'),
+        link => $url,
+      ],
+      'separator',
+    );
+  }
+
+  # full syntax without sugar
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      (SL::Layout::ActionBar::Action->new(
+        text => t8('Description'),
+        params => {
+          call      => [ 'kivi.Javascript.function', @arguments ],
+          accesskey => 'enter',
+          disabled  => $tooltip_with_reason_or_falsish,
+        },
+      )) x(!!$only_id && !$not_if),
+      SL::Layout::ActionBar::ComboBox->new(
+        actions => [
+          SL::Layout::ActionBar::Action->new(...),
+          SL::Layout::ActionBar::Action->new(...),
+          SL::Layout::ActionBar::Action->new(...),
+          SL::Layout::ActionBar::Action->new(...),
+        ],
+      ),
+      SL::Layout::ActionBar::Link->new(
+        text => t8('Description'),
+        params => {
+          link => $url,
+        },
+      ),
+      SL::Layout::ActionBar::Separator->new,
+    );
+  }
+
+=head1 CONCEPT
+
+This is a layout block that creates an action bar for any controller who
+wants to use it. It's designed to be rendered above the content and to be
+fixed when scrolling. It's structured as a container for elements that can be
+extended when needed.
+
+=head1 METHODS
+
+=over 4
+
+=item * C<new>
+
+Will be used during initialization of the layout. You should never have to
+instanciate an action bar yourself. Get the current request instances from
+
+  $::request->layout->get('actionbar')
+
+instead.
+
+=item * C<add>
+
+Add new elements to the bar. Can be instances of
+L<SL::Layout::ActionBar::Action> or scalar strings matching the sugar syntax
+which is described further down.
+
+=back
+
+=head1 SYNTACTIC SUGAR
+
+Instead of passing full objects to L</add>, you can instead pass the arguments
+to be used for instantiation to make the code easier to read. The short syntax
+looks like this:
+
+  type => [
+    localized_description,
+    param => value,
+    param => value,
+    ...
+  ]
+
+A string type, followed by the parameters needed for that type. Type may be one of:
+
+=over 4
+
+=item * C<action>
+
+=item * C<combobox>
+
+=item * C<link>
+
+=item * C<separator>
+
+=back
+
+C<separator> will use no parameters, the other three will expect one arrayref.
+
+Two additional pseudo parameters are supported for those:
+
+=over 4
+
+=item * C<only_if>
+
+=item * C<not_if>
+
+=back
+
+These are meant to reduce enterprise operators (C<()x!!>) when conditionally adding lots
+of elements.
+
+The combobox element is in itself a container and will simply expect the same
+syntax in an arrayref.
+
+For the full list of parameters supported by the elements, see L<SL::Layout::ActionBar::Action/RECOGNIZED PARAMETERS>.
+
+
+=head1 GUIDELINES
+
+The current implementation follows these design guidelines:
+
+=over 4
+
+=item *
+
+Don't put too many elements into the action bar. Group into comboboxes if
+possible. Consider seven elements a reasonable limit.
+
+=item *
+
+If you've got an update button, put it first and bind the enter accesskey to
+it.
+
+=item *
+
+Put mutating actions (save, post, delete, check out, ship) before the separator
+and non mutating actions (export, search, history, workflow) after the
+separator. Combined actions (save and close) still mutate and go before the
+separator.
+
+=item *
+
+Avoid abusing the actionbar as a secondary menu. As a principle every action
+should act upon the current element or topic.
+
+=item *
+
+Hide elements with C<only_if> if they are known to be useless for the current
+topic, but disable when they would be useful in principle but are not
+applicable right now. For example C<delete> does not make sense in a creating
+form, but makes still sense because the element can be deleted later. This
+keeps the actionbar stable and reduces surprising elements that only appear in
+rare situations.
+
+=item *
+
+Always add a tooltip when disabling an action.
+
+=item *
+
+Try to always add a default action with accesskey enter. Since the actionbar
+lies outside of the main form, the usual submit on enter does not work out of
+the box.
+
+=back
+
+=head1 DOM MODEL AND IMPLEMENTATION DETAILS
+
+The entire block is rendered into a div with the class 'layout-actionbar'. Each
+action will render itself and will get added to the div. To keep the DOM small
+and reduce startup overhead, the presentation is pure CSS and only the sticky
+expansion of comboboxes is done with javascript.
+
+To keep startup times and HTML parsing fast the action data is simply written
+into the data elements of the actions and handlers are added in a ready hook.
+
+=head1 BUGS
+
+none yet. :)
+
+=head1 SEE ALSO
+
+L<SL::Layout::ActioBar::Base>,
+L<SL::Layout::ActioBar::Action>,
+L<SL::Layout::ActioBar::Submit>,
+L<SL::Layout::ActioBar::ComboBox>,
+L<SL::Layout::ActioBar::Separator>,
+L<SL::Layout::ActioBar::Link>,
+
+=head1 AUTHOR
+
+Sven Schoeling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut
diff --git a/SL/Layout/ActionBar/Action.pm b/SL/Layout/ActionBar/Action.pm
new file mode 100644 (file)
index 0000000..aea2e2c
--- /dev/null
@@ -0,0 +1,217 @@
+package SL::Layout::ActionBar::Action;
+
+use strict;
+use parent qw(Rose::Object);
+
+use SL::Presenter::Tag qw(name_to_id);
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(id params text) ],
+);
+
+# subclassing interface
+
+sub render {
+  die 'needs to be implemented';
+}
+
+sub script {
+  sprintf q|$('#%s').data('action', %s);|, $_[0]->id, JSON->new->allow_blessed->convert_blessed->encode($_[0]->params);
+}
+
+# this is mostly so that outside consumer don't need to load subclasses themselves
+sub from_params {
+  my ($class, $data) = @_;
+
+  require SL::Layout::ActionBar::Submit;
+
+  my ($text, %params) = @$data;
+  return if exists($params{only_if}) && !$params{only_if};
+  return if exists($params{not_if})  &&  $params{not_if};
+  return SL::Layout::ActionBar::Submit->new(text => $text, params => \%params);
+}
+
+sub callable { 0 }
+
+# shortcut for presenter
+
+sub init_params {
+  +{}
+}
+
+# unique id to tie div and javascript together
+sub init_id {
+  $_[0]->params->{id} // name_to_id('action[]')
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Layout::ActionBar::Action - base class for action bar actions
+
+=head1 DESCRIPTION
+
+This base class for actions can be used to implement elements that can be
+added to L<SL::Layout::ActionBar>.
+
+Elements can be interactive or simply used for layout. Most of the actual
+semantics are handled in the corresponding javascript C<js/kivi.ActionBar.js>, so
+this is only used to generate the DOM elements and to provide information for
+request time logic decisions.
+
+
+=head1 SYNOPSIS
+
+  # implement:
+  package SL::Layout::ActionBar::Custom;
+  use parent qw(SL::Layout::ActionBar::Action);
+
+  # unsugared use
+  SL::Layout::ActionBar::Custom->new(
+    text   => t8('Description'),
+    params => {
+      key => $attr,
+      key => $attr,
+      ...
+    },
+  );
+
+  # parse sugared version:
+  SL::Layout::ActionBar::Custom->from_params(
+    t8('Description'),
+    key => $attr,
+    key => $attr,
+    ...
+  );
+
+=head1 INTERFACE
+
+=over 4
+
+=item * C<render>
+
+Needs to be implemented. Should render only the bare minimum necessary to
+identify the element at run time.
+
+=item * C<script>
+
+Will be called during layout rendering. Defaults to dumping the params section
+into data field of the rendered DOM element.
+
+=item * C<from_params>
+
+Parse sugared version. Defaults for historic reasons to the implementation of
+L<SL::Layout::ActionBar::Submit>, all others must implement their own.
+
+=item * C<callable>
+
+Used to determine whether an instance is callable or only a layout element.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item * C<p>
+
+Returns the current request presenter.
+
+=item * C<id>
+
+Will get initialized to either the provided id from the params or to a
+generated unique id. Should be used to tie the rendered DOM and script
+together.
+
+=back
+
+=head1 RECOGNIZED PARAMETERS
+
+=over 4
+
+=item * C<< submit => [ $selector, \%params ] >>
+
+On click, submit the form found with the first parameter. If params is present
+and a hashref, the key value elements will be added to the form before
+submitting. Beware that this will destroy the form if the user uses the browser
+history to jump back to this page, so ony use parametrized submits for post
+submits that redirect on completion.
+
+=item * C<< link => $url >>
+
+On click, will load the given url.
+
+=item * C<< call => [ $function_name, @args ] >>
+
+On click, will call the function name with the argument array. The return will
+be discarded. It is assumed that the fucntion will trigger an asynchronous
+action.
+
+Contrast with C<checks>.
+
+=item * C<< checks => \@function_names >>
+
+Before any of C<submit>, C<link>, or C<call> are evaluated all
+functions in C<check> are called. Only if all of them return a true value the
+action will be triggered.
+
+Checks are expected not to trigger asynchronous actions (contrast with C<call>),
+but may change the DOM to indicate to the user why they fail.
+
+Each must return a boolean value.
+
+=item * C<< confirm => t8('Yes/No Question') >>
+
+Before any of C<submit>, C<link>, or C<call> are evaluated, the user
+will be asked to confirm. If checks are present and failed, confirm will not be
+triggered.
+
+=item * C<< only_if => $bool >>
+
+Pseudo parameter. If present and false, the element will not be rendered.
+
+=item * C<< not_if => $bool >>
+
+Pseudo parameter. If present and true, the element will not be rendered.
+
+=item * C<< only_once => 1 >>
+
+If present, a click will C<disable> the action to prevent multiple activations.
+
+=item * C<< accesskey => $text >>
+
+Registers an accesskey for this element. While the most common accesskey is
+'enter', in theory every other should work as well. Modifier keys can be added
+to the accesskey string with 'ctrl+', 'alt+', or 'shift+'. 'shift+' is not
+necessary for upper case letters.
+
+=item * C<< disabled => t8('tooltip') >>
+
+Renders the element disabled, ignores all actions (C<submit>, C<call>, C<link>)
+and adds the given tooltip hopefully explaining why the element is disabled.
+
+=item * C<< id => $id >>
+
+Sets the DOM id of the rendered element. If missing, the element will get a
+random id.
+
+=item * C<< tooltip => t8('tooltip') >>
+
+Sets a tooltip for the element.
+
+=back
+
+=head1 BUGS
+
+None yet :)
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut
diff --git a/SL/Layout/ActionBar/ComboBox.pm b/SL/Layout/ActionBar/ComboBox.pm
new file mode 100644 (file)
index 0000000..477151d
--- /dev/null
@@ -0,0 +1,43 @@
+package SL::Layout::ActionBar::ComboBox;
+
+use strict;
+use parent qw(SL::Layout::ActionBar::Action);
+
+use JSON;
+use List::MoreUtils qw(none);
+use SL::Presenter::Tag qw(html_tag);
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(actions) ],
+);
+
+sub from_params {
+  my ($class, $actions) = @_;
+
+  my $combobox = $class->new;
+  push @{ $combobox->actions }, SL::Layout::ActionBar->parse_actions(@{ $actions });
+
+  return $combobox;
+}
+
+sub render {
+  my ($first, @rest) = @{ $_[0]->actions };
+
+  return                if none { $_->callable } @{ $_[0]->actions };
+  return $first->render if !@rest;
+
+  html_tag('div',
+    html_tag('div', $first->render . html_tag('span'), class => 'layout-actionbar-combobox-head') .
+    html_tag('div', join('', map { $_->render } @rest), class => 'layout-actionbar-combobox-list'),
+    id    => $_[0]->id,
+    class => 'layout-actionbar-combobox',
+  );
+}
+
+sub script {
+  map { $_->script } @{ $_[0]->actions }
+}
+
+sub init_actions { [] }
+
+1;
diff --git a/SL/Layout/ActionBar/Link.pm b/SL/Layout/ActionBar/Link.pm
new file mode 100644 (file)
index 0000000..5e47663
--- /dev/null
@@ -0,0 +1,30 @@
+package SL::Layout::ActionBar::Link;
+
+use strict;
+use parent qw(SL::Layout::ActionBar::Action);
+
+use SL::Presenter::Tag qw(html_tag);
+
+sub from_params {
+  my ($class, $data) = @_;
+
+  my ($text, %params) = @$data;
+
+  return if exists($params{only_if}) && !$params{only_if};
+  return if exists($params{not_if})  &&  $params{not_if};
+  return SL::Layout::ActionBar::Link->new(text => $text, params => \%params);
+}
+
+sub render {
+  my ($self) = @_;
+
+  html_tag(
+    'div', $self->text,
+    id    => $self->id,
+    class => 'layout-actionbar-action layout-actionbar-link',
+  );
+}
+
+sub callable { 1 }
+
+1;
diff --git a/SL/Layout/ActionBar/Separator.pm b/SL/Layout/ActionBar/Separator.pm
new file mode 100644 (file)
index 0000000..afa202e
--- /dev/null
@@ -0,0 +1,18 @@
+package SL::Layout::ActionBar::Separator;
+
+use strict;
+use parent qw(SL::Layout::ActionBar::Action);
+
+use SL::Presenter::Tag qw(html_tag);
+
+sub from_params { $_[0]->new }
+
+sub render {
+  html_tag('div', '', class => 'layout-actionbar-separator');
+}
+
+sub script {
+  ()
+}
+
+1;
diff --git a/SL/Layout/ActionBar/Submit.pm b/SL/Layout/ActionBar/Submit.pm
new file mode 100644 (file)
index 0000000..f3288c7
--- /dev/null
@@ -0,0 +1,20 @@
+package SL::Layout::ActionBar::Submit;
+
+use strict;
+use parent qw(SL::Layout::ActionBar::Action);
+
+use SL::Presenter::Tag qw(html_tag);
+
+sub render {
+  html_tag('div', $_[0]->text,
+    id    => $_[0]->id,
+    class => 'layout-actionbar-action layout-actionbar-submit',
+  );
+}
+
+sub callable {
+  my ($self) = @_;
+  return $self->params->{submit} || $self->params->{call} || $self->params->{link};
+}
+
+1;
index e39502f..c64a99a 100644 (file)
@@ -3,11 +3,12 @@ package SL::Layout::Base;
 use strict;
 use parent qw(Rose::Object);
 
+use File::Slurp qw(read_file);
 use List::MoreUtils qw(uniq);
 use Time::HiRes qw();
 
 use Rose::Object::MakeMethods::Generic (
-  'scalar --get_set_init' => [ qw(menu auto_reload_resources_param) ],
+  'scalar --get_set_init' => [ qw(menu auto_reload_resources_param sub_layouts_by_name) ],
   'scalar'                => qw(focus),
   'array'                 => [
     'add_stylesheets_inline' => { interface => 'add', hash_key => 'stylesheets_inline' },
@@ -19,6 +20,7 @@ use Rose::Object::MakeMethods::Generic (
 
 use SL::Menu;
 use SL::Presenter;
+use SL::System::Process;
 
 my %menu_cache;
 
@@ -32,9 +34,41 @@ sub init_menu {
   SL::Menu->new('user');
 }
 
+sub init_sublayouts_by_name {
+  {}
+}
+
+sub webpages_path {
+  "templates/webpages";
+}
+
+sub get {
+  $_[0]->sub_layouts;
+  return grep { $_ } ($_[0]->sub_layouts_by_name->{$_[1]});
+}
+
 sub init_auto_reload_resources_param {
-  return '' unless $::lx_office_conf{debug}->{auto_reload_resources};
-  return sprintf('?rand=%d-%d-%d', Time::HiRes::gettimeofday(), int(rand 1000000000000));
+  if ($::lx_office_conf{debug}->{auto_reload_resources}) {
+    return sprintf('?rand=%d-%d-%d', Time::HiRes::gettimeofday(), int(rand 1000000000000));
+  }
+
+  if ($::lx_office_conf{debug}{git_commit_reload_resources}) {
+    my $git_dir = SL::System::Process::exe_dir() . '/.git';
+
+    return '' unless -d $git_dir;
+
+    my $content = eval { scalar(read_file($git_dir . '/HEAD')) };
+
+    return '' unless ($content // '') =~ m{\Aref: ([^\r\n]+)};
+
+    $content = eval { scalar(read_file($git_dir . '/' . $1)) };
+
+    return '' unless ($content // '') =~ m{\A([0-9a-fA-F]+)};
+
+    return '?rand=' . $1;
+  }
+
+  return '';
 }
 
 ##########################################
@@ -69,11 +103,16 @@ sub javascripts_inline {
 
 sub init_sub_layouts { [] }
 
+sub init_sub_layouts_by_name { +{} }
+
 
 #########################################
-# Interface
+# Stylesheets
 ########################################
 
+# override in sub layouts
+sub static_stylesheets {}
+
 sub add_stylesheets {
   &use_stylesheet;
 }
@@ -81,7 +120,7 @@ sub add_stylesheets {
 sub use_stylesheet {
   my $self = shift;
   push @{ $self->{stylesheets} ||= [] }, @_ if @_;
-  @{ $self->{stylesheets} ||= [] };
+    (map { $_->use_stylesheet } $self->sub_layouts), $self->static_stylesheets, @{ $self->{stylesheets} ||= [] };
 }
 
 sub stylesheets {
@@ -89,7 +128,7 @@ sub stylesheets {
   my $css_path = $self->get_stylesheet_for_user;
 
   return uniq grep { $_ } map { $self->_find_stylesheet($_, $css_path)  }
-    $self->use_stylesheet, map { $_->stylesheets } $self->sub_layouts;
+    $self->use_stylesheet;
 }
 
 sub _find_stylesheet {
@@ -98,6 +137,7 @@ sub _find_stylesheet {
   return "$css_path/$stylesheet" if -f "$css_path/$stylesheet";
   return "css/$stylesheet"       if -f "css/$stylesheet";
   return $stylesheet             if -f $stylesheet;
+  return $stylesheet             if $stylesheet =~ /^http/; # external
 }
 
 sub get_stylesheet_for_user {
@@ -117,6 +157,13 @@ sub get_stylesheet_for_user {
   return $css_path;
 }
 
+#########################################
+# Javascripts
+########################################
+
+# override in sub layouts
+sub static_javascripts {}
+
 sub add_javascripts {
   &use_javascript
 }
@@ -124,14 +171,14 @@ sub add_javascripts {
 sub use_javascript {
   my $self = shift;
   push @{ $self->{javascripts} ||= [] }, @_ if @_;
-  @{ $self->{javascripts} ||= [] };
+  map({ $_->use_javascript } $self->sub_layouts), $self->static_javascripts, @{ $self->{javascripts} ||= [] };
 }
 
 sub javascripts {
   my ($self) = @_;
 
   return uniq grep { $_ } map { $self->_find_javascript($_)  }
-    map({ $_->javascripts } $self->sub_layouts), $self->use_javascript;
+     $self->use_javascript;
 }
 
 sub _find_javascript {
@@ -139,6 +186,7 @@ sub _find_javascript {
 
   return "js/$javascript"        if -f "js/$javascript";
   return $javascript             if -f $javascript;
+  return $javascript             if $javascript =~ /^http/;
 }
 
 
@@ -251,15 +299,33 @@ For the C<*_content> callbacks this works if you just remember to dispatch to th
     $_[0]->SUPER::post_content
   }
 
-For the stylesheet and javascript callbacks things are hard, because of the
-backwards compatibility, and the built-in sanity checks. The best way currently
-is to just add your content and dispatch to the base method.
 
-  sub stylesheets {
-    $_[0]->add_stylesheets(qw(mystyle1.css mystyle2.css);
-    $_[0]->SUPER::stylesheets;
+Stylesheets and Javascripts can be added to every layout and sub-layout at
+runtime with L<SL::Layout::Dispatcher/add_stylesheets> and
+L<SL::Layout::Dispatcher/add_javascripts> (C<use_stylesheets> and
+C<use_javascripts> are aliases for backwards compatibility):
+
+  $layout->add_stylesheets("custom.css");
+  $layout->add_javascripts("app.js", "widget.js");
+
+Or they can be overwritten in sub layouts with the calls
+L<SL::Layout::Displatcher/static_stylesheets> and
+L<SL::Layout::Dispatcher/static_javascripts>:
+
+  sub static_stylesheets {
+    "custom.css"
+  }
+
+  sub static_javascripts {
+    qw(app.css widget.js)
   }
 
+Note how these are relative to the base dirs of the currently selected
+stylesheets. Javascripts are resolved relative to the C<js/> basedir.
+
+Setting directly with C<stylesheets> and C<javascripts> is eprecated.
+
+
 =head1 GORY DETAILS ABOUT JAVASCRIPT AND STYLESHEET OVERLOADING
 
 The original code used to store one stylesheet in C<< $form->{stylesheet} >> and
@@ -319,7 +385,12 @@ classes, which should be changed. The other points work pretty well.
 
 =head1 BUGS
 
-None yet, if you don't count the horrible stylesheet/javascript interface.
+* stylesheet/javascript interface is a horrible mess.
+
+* It's currently not possible to do compositor layouts without assupmtions
+about the position of the content. That's because the content will return
+control to the actual controller, so the layouts need to know where to split
+pre- and post-content.
 
 =head1 AUTHOR
 
index 94f12b3..e8974c1 100644 (file)
@@ -6,12 +6,20 @@ use parent qw(SL::Layout::Base);
 use SL::Layout::Top;
 use SL::Layout::MenuLeft;
 use SL::Layout::None;
+use SL::Layout::Split;
+use SL::Layout::ActionBar;
+use SL::Layout::Content;
 
 sub init_sub_layouts {
+  $_[0]->sub_layouts_by_name->{actionbar} = SL::Layout::ActionBar->new;
+
   [
     SL::Layout::None->new,
     SL::Layout::Top->new,
-    SL::Layout::MenuLeft->new,
+    SL::Layout::Split->new(
+      left  => [ SL::Layout::MenuLeft->new ],
+      right => [ $_[0]->sub_layouts_by_name->{actionbar}, SL::Layout::Content->new ],
+    )
   ]
 }
 
diff --git a/SL/Layout/Content.pm b/SL/Layout/Content.pm
new file mode 100644 (file)
index 0000000..8e0549b
--- /dev/null
@@ -0,0 +1,33 @@
+package SL::Layout::Content;
+
+use strict;
+use parent qw(SL::Layout::Base);
+
+sub start_content {
+  "<div id='content'>";
+}
+
+sub end_content {
+  "</div>";
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Layout::Content
+
+=head1 DESCRIPTION
+
+Pseudo layout for the position of the actual content in the layout. Currently
+only implements the start_content/end_content blocks used for styling.
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut
index b67475f..6897a47 100644 (file)
@@ -3,7 +3,7 @@ package SL::Layout::CssMenu;
 use strict;
 use parent qw(SL::Layout::Base);
 
-sub use_stylesheet {
+sub static_stylesheets {
   qw(icons16.css),
 }
 
diff --git a/SL/Layout/DHTMLMenu.pm b/SL/Layout/DHTMLMenu.pm
new file mode 100644 (file)
index 0000000..43e83c1
--- /dev/null
@@ -0,0 +1,39 @@
+package SL::Layout::DHTMLMenu;
+
+use strict;
+use parent qw(SL::Layout::Base);
+
+use SL::Presenter::JavascriptMenu qw(render_menu);
+
+sub static_javascripts {
+  qw(dhtmlsuite/menu-for-applications.js),
+}
+
+sub javascripts_inline {
+<<'EOJS',
+  DHTMLSuite.createStandardObjects();
+  DHTMLSuite.configObj.setImagePath('image/dhtmlsuite/');
+  var menu_model = new DHTMLSuite.menuModel();
+  menu_model.addItemsFromMarkup('main_menu_model');
+  menu_model.init();
+  var menu_bar = new DHTMLSuite.menuBar();
+  menu_bar.addMenuItems(menu_model);
+  menu_bar.setTarget('main_menu_div');
+  menu_bar.init();
+EOJS
+}
+
+sub pre_content {
+  render_menu($_[0]->menu),
+}
+
+sub static_stylesheets {
+  qw(
+    dhtmlsuite/menu-item.css
+    dhtmlsuite/menu-bar.css
+    icons16.css
+    menu.css
+  );
+}
+
+1;
index b125a26..4d0345c 100644 (file)
@@ -8,6 +8,8 @@ use SL::Layout::Login;
 use SL::Layout::Classic;
 use SL::Layout::V3;
 use SL::Layout::Javascript;
+use SL::Layout::Material;
+use SL::Layout::MobileLogin;
 
 sub new {
   my ($class, %params) = @_;
@@ -18,6 +20,8 @@ sub new {
   return SL::Layout::Admin->new      if $params{style} eq 'admin';
   return SL::Layout::AdminLogin->new if $params{style} eq 'admin_login';
   return SL::Layout::Login->new      if $params{style} eq 'login';
+  return SL::Layout::Material->new   if $params{style} eq 'mobile';
+  return SL::Layout::MobileLogin->new if $params{style} eq 'mobile_login';
   return SL::Layout::None->new;
 }
 
@@ -167,6 +171,12 @@ Non-existing files will be pruned from the list.
 
 Backwards compatible alias for C<add_stylesheets>. Deprecated.
 
+=item C<static_stylesheets>
+
+Can be overwritten in sub-layouts to return a list of needed stylesheets. The
+values will be resolved by the actual layout in addition to the
+C<add_stylesheets> accumulator.
+
 =item C<add_javascripts>
 
 Adds the list of arguments to the list of used javascripts.
@@ -179,6 +189,13 @@ Non-existing files will be pruned from the list.
 
 Backwards compatible alias for C<add_javascripts>. Deprecated.
 
+
+=item C<static_javascripts>
+
+Can be overwritten in sub-layouts to return a list of needed javascripts. The
+values will be resolved by the actual layout in addition to the
+C<add_javascripts> accumulator.
+
 =item C<add_javascripts_inline>
 
 Add a snippet of javascript.
index e900012..de3c7f3 100644 (file)
@@ -3,65 +3,25 @@ package SL::Layout::Javascript;
 use strict;
 use parent qw(SL::Layout::Base);
 
+use SL::Layout::None;
+use SL::Layout::DHTMLMenu;
+use SL::Layout::Top;
+use SL::Layout::ActionBar;
+use SL::Layout::Content;
+
 use List::Util qw(max);
+use List::MoreUtils qw(uniq);
 use URI;
 
 sub init_sub_layouts {
+  $_[0]->sub_layouts_by_name->{actionbar} = SL::Layout::ActionBar->new;
   [
     SL::Layout::None->new,
     SL::Layout::Top->new,
+    SL::Layout::DHTMLMenu->new,
+    $_[0]->sub_layouts_by_name->{actionbar},
+    SL::Layout::Content->new,
   ]
 }
 
-sub use_javascript {
-  my $self = shift;
-  qw(
-    js/dhtmlsuite/menu-for-applications.js
-  ),
-  $self->SUPER::use_javascript(@_);
-}
-
-sub javascripts_inline {
-  $_[0]->SUPER::javascripts_inline,
-<<'EOJS'
-  DHTMLSuite.createStandardObjects();
-  DHTMLSuite.configObj.setImagePath('image/dhtmlsuite/');
-  var menu_model = new DHTMLSuite.menuModel();
-  menu_model.addItemsFromMarkup('main_menu_model');
-  menu_model.init();
-  var menu_bar = new DHTMLSuite.menuBar();
-  menu_bar.addMenuItems(menu_model);
-  menu_bar.setTarget('main_menu_div');
-  menu_bar.init();
-EOJS
-}
-
-sub pre_content {
-  $_[0]->SUPER::pre_content .
-  $_[0]->presenter->render("menu/menunew",
-    force_ul_width  => 1,
-    menu            => $_[0]->menu,
-    icon_path       => sub { my $img = "image/icons/16x16/$_[0].png"; -f $img ? $img : () },
-    max_width       => sub { 10 * max map { length $::locale->text($_->{name}) } @{ $_[0]{children} || [] } },
-  );
-}
-
-sub start_content {
-  "<div id='content'>\n";
-}
-
-sub end_content {
-  "</div>\n";
-}
-
-sub stylesheets {
-  $_[0]->add_stylesheets(qw(
-    dhtmlsuite/menu-item.css
-    dhtmlsuite/menu-bar.css
-    icons16.css
-    menu.css
-  ));
-  $_[0]->SUPER::stylesheets;
-}
-
 1;
diff --git a/SL/Layout/Material.pm b/SL/Layout/Material.pm
new file mode 100644 (file)
index 0000000..78afd26
--- /dev/null
@@ -0,0 +1,29 @@
+package SL::Layout::Material;
+
+use strict;
+use parent qw(SL::Layout::Base);
+
+use SL::Layout::None;
+use SL::Layout::MaterialMenu;
+use SL::Layout::MaterialStyle;
+use SL::Layout::Content;
+
+sub get_stylesheet_for_user {
+  # overwrite kivitendo fallback
+  'css/material';
+}
+
+sub webpages_path {
+  "templates/mobile_webpages";
+}
+
+sub init_sub_layouts {
+  [
+    SL::Layout::None->new,
+    SL::Layout::MaterialStyle->new,
+    SL::Layout::MaterialMenu->new,
+    SL::Layout::Content->new,
+  ]
+}
+
+1;
diff --git a/SL/Layout/MaterialMenu.pm b/SL/Layout/MaterialMenu.pm
new file mode 100644 (file)
index 0000000..e8ef3b8
--- /dev/null
@@ -0,0 +1,16 @@
+package SL::Layout::MaterialMenu;
+
+use strict;
+use parent qw(SL::Layout::Base);
+use SL::Menu;
+use SL::Controller::Base;
+
+sub init_menu {
+  SL::Menu->new('mobile');
+}
+
+sub pre_content {
+  $_[0]->presenter->render('menu/menu', menu => $_[0]->menu, C => SL::Controller::Base->new);
+}
+
+1;
diff --git a/SL/Layout/MaterialStyle.pm b/SL/Layout/MaterialStyle.pm
new file mode 100644 (file)
index 0000000..9616bd4
--- /dev/null
@@ -0,0 +1,25 @@
+package SL::Layout::MaterialStyle;
+
+use strict;
+use parent qw(SL::Layout::Base);
+
+sub static_stylesheets {
+  "materialize.css",   #  "https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css",
+  "icons.css"          #  "https://fonts.googleapis.com/icon?family=Material+Icons";
+}
+
+sub static_javascripts {
+  "materialize.js",    # "https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js",
+  "kivi.Materialize.js";
+}
+
+sub javascripts_inline {
+  "kivi.Materialize.init();"
+}
+
+sub get_stylesheet_for_user {
+  # overwrite kivitendo fallback
+  'css/material';
+}
+
+1;
index 17ea51a..ad7937e 100644 (file)
@@ -3,26 +3,22 @@ package SL::Layout::MenuLeft;
 use strict;
 use parent qw(SL::Layout::Base);
 
-use URI;
-
 use List::MoreUtils qw(apply);
+use SL::JSON qw(to_json);
+use URI;
 
-sub stylesheets {
+sub static_stylesheets {
   qw(icons16.css icons24.css menu.css)
 }
 
 sub javascripts_inline {
-  my $self = shift;
-  my $sections = [ section_menu($self->menu) ];
-  $self->presenter->render('menu/menu',
-    sections  => $sections,
-  )
+  "\$(function(){kivi.LeftMenu.init(@{[ to_json([ section_menu($_[0]->menu) ]) ]})});"
 }
 
-sub javascripts {
+sub static_javascripts {
   qw(
     js/jquery.cookie.js
-    js/switchmenuframe.js
+    js/kivi.LeftMenu.js
   );
 }
 
@@ -30,14 +26,6 @@ sub pre_content {
   "<div id='html-menu'></div>\n";
 }
 
-sub start_content {
-  "<div id='content' class='html-menu'>\n";
-}
-
-sub end_content {
-  "</div>\n";
-}
-
 sub section_menu {
   my ($menu) = @_;
   my @items;
diff --git a/SL/Layout/MobileLogin.pm b/SL/Layout/MobileLogin.pm
new file mode 100644 (file)
index 0000000..54277df
--- /dev/null
@@ -0,0 +1,25 @@
+package SL::Layout::MobileLogin;
+
+use strict;
+use parent qw(SL::Layout::Base);
+use SL::Layout::MaterialStyle;
+use SL::Layout::MaterialMenu;
+
+sub get_stylesheet_for_user {
+  # overwrite kivitendo fallback
+  'css/material';
+}
+
+sub webpages_path {
+  "templates/mobile_webpages"
+}
+
+sub init_sub_layouts {
+  [
+    SL::Layout::None->new,
+    SL::Layout::MaterialStyle->new,
+    SL::Layout::MaterialMenu->new,
+  ]
+}
+
+1;
index 72d3e75..1504f97 100644 (file)
@@ -23,25 +23,22 @@ sub javascripts_inline {
   );
 }
 
-sub use_javascript {
-  my $self = shift;
+sub static_javascripts {
   qw(
     jquery.js
     common.js
     namespace.js
+    jquery-ui.js
     kivi.js
   ),
   'locale/'. $::myconfig{countrycode} .'.js',
-  $self->SUPER::use_javascript(@_);
 }
 
-sub use_stylesheet {
-  my $self = shift;
+sub static_stylesheets {
   qw(
     main.css
     menu.css
   ),
-  $self->SUPER::use_stylesheet(@_);
 }
 
 1;
diff --git a/SL/Layout/Split.pm b/SL/Layout/Split.pm
new file mode 100644 (file)
index 0000000..a262b5b
--- /dev/null
@@ -0,0 +1,65 @@
+package SL::Layout::Split;
+
+use strict;
+use parent qw(SL::Layout::Base);
+
+use SL::Presenter::Tag qw(html_tag);
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar'                => [ qw(left right) ],
+);
+
+sub sub_layouts {
+  @{ $_[0]->left || [] },
+  @{ $_[0]->right || [] },
+}
+
+sub pre_content {
+  my $left  = join '', map { $_->pre_content } @{ $_[0]->left  || [] };
+  my $right = join '', map { $_->pre_content } @{ $_[0]->right || [] };
+
+  html_tag('div', $left, class => 'layout-split-left')
+  .'<div class="layout-split-right">' . $right;
+}
+
+sub post_content {
+  my $left  = join '', map { $_->post_content } @{ $_[0]->left  || [] };
+  my $right = join '', map { $_->post_content } @{ $_[0]->right || [] };
+
+  $right . '</div>'
+  . html_tag('div', $left, class => 't-layout-left');
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Layout::Split
+
+=head1 SYNOPSIS
+
+  use SL::Layout::Split;
+
+  SL::Layout::Split->new(
+    left  => [ LIST OF SUBLAYOUTS ],
+    right => [ LIST OF SUBLAYOUTS ],
+  );
+
+=head1 DESCRIPTION
+
+Layout with left and right components, with content being part of the
+right block.
+
+=head1 BUGS
+
+Due to the way content is serialized it's currently not possible to shift the content into the other blocks
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut
index e2e5d2d..471d612 100644 (file)
@@ -3,23 +3,32 @@ package SL::Layout::Top;
 use strict;
 use parent qw(SL::Layout::Base);
 
+use SL::Controller::TopQuickSearch;
+
 sub pre_content {
   my ($self) = @_;
 
+  my @options;
+  # Only enable the quick search functionality if all database
+  # upgrades have already been applied as quick search requires
+  # certain columns that are only created by said database upgrades.
+  push @options, (quick_search => SL::Controller::TopQuickSearch->new) unless $::request->applying_database_upgrades;
+
   $self->presenter->render('menu/header',
     now        => DateTime->now_local,
     is_fastcgi => $::dispatcher ? scalar($::dispatcher->interface_type =~ /fastcgi/i) : 0,
     is_links   => scalar($ENV{HTTP_USER_AGENT}         =~ /links/i),
+    @options,
   );
 }
 
-sub stylesheets {
+sub static_stylesheets {
  'frame_header/header.css';
 }
 
-sub javascripts {
-  ('jquery-ui.js', 'quicksearch_input.js') x!! $::auth->assert('customer_vendor_edit', 1),
-  ('jquery-ui.js', 'glquicksearch.js')     x!! $::auth->assert('general_ledger', 1)
+sub static_javascripts {
+  'jquery-ui.js',
+  'kivi.QuickSearch.js',
 }
 
 1;
index 8bf270f..dc39a18 100644 (file)
@@ -6,21 +6,20 @@ use parent qw(SL::Layout::Base);
 use SL::Layout::None;
 use SL::Layout::Top;
 use SL::Layout::CssMenu;
+use SL::Layout::ActionBar;
+use SL::Layout::Content;
 
 sub init_sub_layouts {
+  $_[0]->sub_layouts_by_name->{actionbar} = SL::Layout::ActionBar->new;
+
   [
     SL::Layout::None->new,
     SL::Layout::Top->new,
     SL::Layout::CssMenu->new,
+    $_[0]->sub_layouts_by_name->{actionbar},
+    SL::Layout::Content->new,
   ]
 }
 
-sub start_content {
-  "<div id='content'>\n";
-}
-
-sub end_content {
-  "</div>\n";
-}
 
 1;
diff --git a/SL/Letter.pm b/SL/Letter.pm
deleted file mode 100644 (file)
index 1d1b974..0000000
+++ /dev/null
@@ -1,404 +0,0 @@
-#=====================================================================
-# LX-Office ERP
-# Copyright (C) 2008
-# Based on SQL-Ledger Version 2.1.9
-# Web http://www.lx-office.org
-#
-#=====================================================================
-#
-# Letter module
-#
-#=====================================================================
-
-package SL::Letter;
-
-use strict;
-use List::Util qw(max);
-
-use SL::Common;
-use SL::CT;
-use SL::DBUtils;
-use SL::MoreCommon;
-use SL::TransNumber;
-use SL::DB::Manager::Customer;
-
-my $DEFINITION = <<SQL;
-                                      Table "public.letter"
-        Column       |            Type             |                Modifiers
-  -------------------+-----------------------------+------------------------------------------
-   id                | integer                     | not null default nextval('id'::regclass)
-   vc_id             | integer                     | not null
-   letternumber      | text                        |
-   jobnumber         | text                        |
-   text_created_for  | text                        |
-   date              | date                        |
-   subject           | text                        |
-   greeting          | text                        |
-   body              | text                        |
-   close             | text                        |
-   company_name      | text                        |
-   employee_id       | integer                     |
-   employee_position | text                        |
-   salesman_id       | integer                     |
-   salesman_position | text                        |
-   itime             | timestamp without time zone | default now()
-   mtime             | timestamp without time zone |
-   page_created_for  | text                        |
-   intnotes          | text                        |
-   cp_id             | integer                     |
-   reference         | text                        |
-  Indexes:
-      "letter_pkey" PRIMARY KEY, btree (id)
-  Foreign-key constraints:
-      "letter_cp_id_fkey" FOREIGN KEY (cp_id) REFERENCES contacts(cp_id)
-      "letter_employee_id_fkey" FOREIGN KEY (employee_id) REFERENCES employee(id)
-      "letter_salesman_id_fkey" FOREIGN KEY (salesman_id) REFERENCES employee(id)
-SQL
-
-# XXX not working yet
-#sub customer {
-#  my $self = shift;
-#
-#  die 'not a setter' if @_;
-#
-#  return unless $self->{customer_id};
-#
-#  # resolve customer_obj
-#}
-
-sub new {
-  my $class  = ref $_[0] || $_[0]; shift;
-  my %params = @_;
-  my $ref    = $_[0];
-
-  $ref = ref $_[0] eq 'HASH' ? $ref : \%params; # don't like it either...
-
-  my $self = bless $ref, $class;
-
-  $self->_lastname_used;
-  $self->_resolve_customer;
-  $self->set_greetings;
-
-  return $self;
-}
-
-sub _create {
-  my $self = shift;
-  my $dbh  = $::form->get_standard_dbh;
-  ($self->{id}) = selectfirst_array_query($::form, $dbh, "select nextval('id')");
-
-  do_query($::form, $dbh, <<SQL, $self->{id}, $self->{customer_id});
-    INSERT INTO letter (id, vc_id) VALUES (?, ?);
-SQL
-}
-
-sub _create_draft {
-  my $self = shift;
-  my $dbh  = $::form->get_standard_dbh;
-  ($self->{draft_id}) = selectfirst_array_query($::form, $dbh, "select nextval('id')");
-
-  do_query($::form, $dbh, <<SQL, $self->{draft_id}, $self->{customer_id});
-    INSERT INTO letter_draft (id, vc_id) VALUES (?, ?);
-SQL
-}
-
-
-sub save {
-  $::lxdebug->enter_sub;
-
-  my $self     = shift;
-  my %params   = @_;
-  my $dbh      = $::form->get_standard_dbh;
-  my ($table, $update_value);
-
-  if ($params{draft}) {
-    $self->_create_draft unless $self->{draft_id};
-    $table = 'letter_draft';
-    $update_value = 'draft_id';
-  } else {
-    $self->_create unless $self->{id};
-    $table = 'letter';
-    $update_value = 'id';
-  }
-
-  my %fields         = __PACKAGE__->_get_fields;
-  my %field_mappings = __PACKAGE__->_get_field_mappings;
-
-  delete $fields{id};
-
-  my @update_fields = keys %fields;
-  my $set_clause    = join ', ', map { "$_ = ?" } @update_fields;
-  my @values        = map { _escaper($_)->( $self->{ $field_mappings{$_} || $_ } ) } @update_fields, $update_value;
-
-  my $query = "UPDATE $table SET $set_clause WHERE id = ?";
-
-  do_query($::form, $dbh, $query, @values);
-
-  $dbh->commit;
-
-  $::lxdebug->leave_sub;
-}
-
-sub find {
-  $::lxdebug->enter_sub;
-
-  my $class    = ref $_[0] || $_[0]; shift;
-  my $myconfig = \%main::myconfig;
-  my $form     = $main::form;
-  my $dbh      = $form->get_standard_dbh($myconfig);
-  my %params   = @_;
-  my $letter_table = 'letter';
-
-  $letter_table = 'letter_draft' if $params{draft};
-  %params = %$form if  !scalar keys %params;
-
-  my (@wheres, @values);
-  my $add_token = sub { add_token(\@wheres, \@values, @_) };
-
-  $add_token->(col => 'letter.id',           val => $params{id},           esc => 'id'    ) if $params{id};
-  $add_token->(col => 'letter.letternumber', val => $params{letternumber}, esc => 'substr') if $params{letternumber};
-  $add_token->(col => 'vc.name',             val => $params{customer},     esc => 'substr') if $params{customer};
-  $add_token->(col => 'vc.id',               val => $params{customer_id},  esc => 'id'    ) if $params{customer_id};
-  $add_token->(col => 'letter.cp_id',        val => $params{cp_id},        esc => 'id'    ) if $params{cp_id};
-  $add_token->(col => 'ct.cp_name',          val => $params{contact},      esc => 'substr') if $params{contact};
-  $add_token->(col => 'letter.subject',      val => $params{subject},      esc => 'substr') if $params{subject};
-  $add_token->(col => 'letter.body',         val => $params{body},         esc => 'substr') if $params{body};
-  $add_token->(col => 'letter.date',         val => $params{date_from}, method => '>='    ) if $params{date_from};
-  $add_token->(col => 'letter.date',         val => $params{date_to},   method => '<='    ) if $params{date_to};
-
-  my $query = qq|
-    SELECT $letter_table.*, vc.name AS customer, vc.id AS customer_id, ct.cp_name AS contact FROM $letter_table
-      LEFT JOIN customer vc ON vc.id = $letter_table.vc_id
-      LEFT JOIN contacts ct ON $letter_table.cp_id = ct.cp_id
-  |;
-
-  if (@wheres) {
-    $query .= ' WHERE ' . join ' AND ', @wheres;
-  }
-
-  my @results = selectall_hashref_query($form, $dbh, $query, @values);
-  my @objects = map { $class->new($_) } @results;
-
-  $::lxdebug->leave_sub;
-
-  return @objects;
-}
-
-sub delete {
-  $::lxdebug->enter_sub;
-
-  my $self     = shift;
-
-  do_query($::form, $::form->get_standard_dbh, <<SQL, $self->{id});
-    DELETE FROM letter WHERE id = ?
-SQL
-
-  $::form->get_standard_dbh->commit;
-
-  $::lxdebug->leave_sub;
-}
-
-sub delete_drafts {
-  $::lxdebug->enter_sub;
-
-  my $self        = shift;
-  my @draft_ids   = @_;
-
-  my $form        = $main::form;
-  my $myconfig = \%main::myconfig;
-  my $dbh         = $form->get_standard_dbh($myconfig);
-
-
-  return $main::lxdebug->leave_sub() unless (@draft_ids);
-
-  my  $query = qq|DELETE FROM letter_draft WHERE id IN (| . join(", ", map { "?" } @draft_ids) . qq|)|;
-  do_query($form, $dbh, $query, @draft_ids);
-
-  $dbh->commit;
-
-  $::lxdebug->leave_sub;
-}
-
-
-sub check_number {
-  my $self = shift;
-
-  return if $self->{letternumber}
-         && $self->{id}
-         && 1 == scalar __PACKAGE__->find(letternumber => $self->{letternumber});
-
-  $self->{letternumber} = SL::TransNumber->new(type => 'letter', id => $self->{id}, number => $self->{letternumber})->create_unique;
-}
-
-sub check_name {
-  my $self   = shift;
-  my %params = @_;
-
-  unless ($params{_name_selected}) {
-    $::form->{$_} = $self->{$_} for qw(oldcustomer customer selectcustomer customer_id);
-
-    if (::check_name('customer')) {
-      $self->_set_customer_from($::form);
-    }
-  } else {
-    $self->_set_customer_from($::form);
-  }
-}
-
-sub _set_customer_from {
-  my $self = shift;
-  my $from = shift;
-
-  $self->{$_} = $from->{$_} for qw(oldcustomer customer_id customer selectcustomer);
-
-  $self;
-}
-
-sub check_date {
-  my $self = shift;
-  $self->{date} ||= $::form->current_date(\%::myconfig);
-}
-
-sub load {
-  my $self   = shift;
-  my $table  = 'letter';
-  my $draft = $self->{draft};
-  $table     = 'letter_draft' if $draft;
-
-
-  return $self unless $self && $self->{id}; # no id? dont load.
-
-  my %mappings      = _get_field_mappings();
-  my $mapped_select = join ', ', '*', map { "$_ AS $mappings{$_}" } keys %mappings;
-
-  my ($db_letter) = selectfirst_hashref_query($::form, $::form->get_standard_dbh, <<SQL, $self->{id});
-    SELECT $mapped_select FROM $table WHERE id = ?
-SQL
-
-  $self->update_from($db_letter);
-  $self->_resolve_customer;
-  $self->set_greetings;
-  $self->{draft_id} = delete $self->{id} if $draft;  # set draft if we have one
-
-  return $self;
-}
-
-sub update_from {
-  my $self   = shift;
-  my $src    = shift;
-  my %fields = $self->_get_fields;
-
-  $fields{$_} = $src->{$_} for qw{customer_id customer selectcustomer oldcustomer}; # customer stuff
-
-  $self->{$_} = $src->{$_} for keys %fields;
-
-  return $self;
-}
-
-sub export_to {
-  my $self = shift;
-  my $form = shift;
-
-  my %fields         = $self->_get_fields;
-  my %field_mappings = $self->_get_field_mappings;
-
-  for (keys %fields) {
-    $form->{$_} =  _escaper($_)->( $self->{ $field_mappings{$_} || $_ } );
-  }
-}
-
-sub language {
-  my $self = shift;
-  die 'not a setter' if @_;
-
-  return unless $self->{cp_id};
-
-  # umetec/cetaq only!
-  # contacts have a custom variable called "mailing"
-  # it contains either a language code or the string "No"
-
-  my $custom_variables = CVar->get_custom_variables(
-    module      => 'Contacts',
-    name_prefix => 'cp',
-    trans_id    => $self->{cp_id},
-  );
-
-  my ($mailing) = grep { $_->{name} eq 'Mailing' } @$custom_variables;
-
-  return $mailing->{value} eq 'No' ? undef : $mailing->{value};
-}
-
-sub set_greetings {
-  $::lxdebug->enter_sub;
-
-  my $self = shift;
-  return $::lxdebug->leave_sub if $self->{greeting};
-
-  # automatically set greetings
-  # greetings depend mainly on contact person
-#   my $contact = $self->_get_contact;
-
-  $self->{greeting} = $::locale->text('Dear Sir or Madam,');
-
-  $::lxdebug->leave_sub;
-}
-
-sub _lastname_used {
-  # wrapper for form lastname_used
-  # sets customer to last used customer,
-  # also used to initalize customer for new objects
-  my $self = shift;
-
-  return if $self->{customer_id};
-
-  my $saved_form = save_form($::form);
-
-  $::form->lastname_used($::form->get_standard_dbh, \%::myconfig, 'customer');
-
-  $self->{customer_id} = $::form->{customer_id};
-  $self->{customer}    = $::form->{customer};
-
-  restore_form($saved_form);
-
-  return $self;
-}
-
-sub _resolve_customer {
-  # used if an object is created with only id.
-  my $self = shift;
-
-  return unless $self->{customer_id} && !$self->{customer};
-
-#  my ($customer) = CT->find_by_id(cv => 'customer', id => $self->{customer_id});
-#  my ($customer) = CT->find_by_id(cv => 'customer', id => $self->{customer_id});
-  # SL/CVar.pm:        : $cfg->{type} eq 'customer'  ? (SL::DB::Manager::Customer->find_by(id => 1*$ref->{number_value}) || SL::DB::Customer->new)->name
-  $self->{customer} = SL::DB::Manager::Customer->find_by(id => $self->{customer_id})->name; # || SL::DB::Customer->new)->name
-
-
-}
-
-sub _get_definition {
-  $DEFINITION;
-}
-
-sub _get_field_mappings {
-  return (
-    vc_id => 'customer_id',
-  );
-}
-
-sub _get_fields {
-  my %fields = _get_definition() =~ /(\w+) \s+ \| \s+ (integer|text|timestamp|numeric|date)/xg;
-}
-
-sub _escaper {
-  my $field_name = shift;
-  my %fields     = __PACKAGE__->_get_fields;
-
-  for ($fields{$field_name}) {
-    return sub { conv_i(shift) } if /integer/;
-    return sub { shift };
-  }
-}
-
-1;
index 710c099..5954e00 100644 (file)
@@ -113,6 +113,7 @@ sub create {
     FROM periodic_invoices pi
     LEFT JOIN periodic_invoices_configs pcfg ON (pi.config_id = pcfg.id)
     WHERE pcfg.active
+      AND NOT pcfg.periodicity = 'o'
       AND (pi.period_start_date >= to_date($q_min_date, 'YYYY-MM-DD'))
 SQL
 
@@ -138,6 +139,7 @@ SQL
     LEFT JOIN buchungsgruppen bg             ON (p.buchungsgruppen_id                     = bg.id)
     LEFT JOIN employee e                     ON (COALESCE(oe.salesman_id, oe.employee_id) = e.id)
     WHERE pcfg.active
+      AND NOT pcfg.periodicity = 'o'
 SQL
 
   # 3. Iterieren über Saldierungsintervalle, vormerken
@@ -180,7 +182,7 @@ SQL
     WHERE (oe.customer_id IS NOT NULL)
       AND NOT COALESCE(oe.quotation, FALSE)
       AND NOT COALESCE(oe.closed,    FALSE)
-      AND (oe.id NOT IN (SELECT oe_id FROM periodic_invoices_configs))
+      AND (oe.id NOT IN (SELECT oe_id FROM periodic_invoices_configs WHERE periodicity <> 'o'))
 SQL
 
   # 5. Initialisierung der Datenstrukturen zum Speichern der
index 25e8043..3b91502 100644 (file)
@@ -27,7 +27,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Translations and number/date formatting
@@ -73,6 +74,12 @@ sub new {
   return $locales_by_country{$country}
 }
 
+sub is_supported {
+  my ($country) = @_;
+
+  return -f "locale/$country/all";
+}
+
 sub _init {
   my $self     = shift;
   my $country  = shift;
@@ -394,6 +401,9 @@ sub parse_date_to_object {
 
   return undef if !defined $string;
 
+  return DateTime->today_local                      if lc($string) eq 'today';
+  return DateTime->today_local->subtract(days => 1) if lc($string) eq 'yesterday';
+
   $params{dateformat}        ||= $::myconfig{dateformat}   || 'yy-mm-dd';
   $params{numberformat}      ||= $::myconfig{numberformat} || '1,000.00';
   my $num_separator            = $params{numberformat} =~ m{,\d+$} ? ',' : '.';
@@ -622,7 +632,7 @@ TODO: Describe format_date
 
 =item C<format_date_object $datetime, %params>
 
-Formats the C<$datetime> object accoring to the user's locale setting.
+Formats the C<$datetime> object according to the user's locale setting.
 
 The parameter C<precision> can control whether or not the time
 component is formatted as well:
index 63a5323..7229743 100644 (file)
@@ -13,7 +13,8 @@ our @EXPORT = qw(t8);
 
 use overload
   '""' => \&translated,
-  eq   => \&my_eq;
+  eq   => \&my_eq,
+  ne   => \&my_ne;
 
 sub translated {
   my ($self) = @_;
@@ -31,6 +32,10 @@ sub my_eq {
   $_[1] eq $_[0]->translated;
 }
 
+sub my_ne {
+  $_[1] ne $_[0]->translated;
+}
+
 sub TO_JSON {
   return $_[0]->translated;
 }
index 4e26c4e..f6ae49e 100644 (file)
@@ -7,18 +7,20 @@ use Encode;
 my $environment_initialized;
 
 sub safe_require {
-  my ($class, $may_fail);
+  my ($class, $may_fail) = @_;
 
-  my $failed = !eval { require Config::Std; };
-
-  if ($failed) {
+  eval {
+    require Config::Std;
+    require SL::System::Process;
+    1;
+  } or do {
     if ($may_fail) {
       warn $@;
       return 0;
     } else {
       die $@;
     }
-  }
+  };
 
   Config::Std->import;
 
@@ -32,11 +34,12 @@ sub read {
 
   # Backwards compatibility: read lx_office.conf.default if
   # kivitendo.conf.default does't exist.
-  my $default_config = -f "config/kivitendo.conf.default" ? 'kivitendo' : 'lx_office';
-  read_config("config/${default_config}.conf.default" => \%::lx_office_conf);
+  my $dir            = SL::System::Process->exe_dir;
+  my $default_config = -f "${dir}/config/kivitendo.conf.default" ? 'kivitendo' : 'lx_office';
+  read_config("${dir}/config/${default_config}.conf.default" => \%::lx_office_conf);
   _decode_recursively(\%::lx_office_conf);
 
-  $file_name ||= -f 'config/kivitendo.conf' ? 'config/kivitendo.conf' : 'config/lx_office.conf';
+  $file_name ||= -f "${dir}/config/kivitendo.conf" ? "${dir}/config/kivitendo.conf" : "${dir}/config/lx_office.conf";
 
   if (-f $file_name) {
     read_config($file_name => \ my %local_conf);
diff --git a/SL/MT940.pm b/SL/MT940.pm
new file mode 100644 (file)
index 0000000..2a97e5e
--- /dev/null
@@ -0,0 +1,138 @@
+package SL::MT940;
+
+use strict;
+use warnings;
+
+use Data::Dumper;
+use DateTime;
+use SL::DB::Default;
+use Encode;
+use File::Slurp qw(read_file);
+
+sub _join_entries {
+  my ($parts, $from, $to, $separator) = @_;
+
+  $separator //= ' ';
+
+  return
+    join $separator,
+    grep { $_ }
+    map  { s{^\s+|\s+$}{}g; $_ }
+    grep { $_ }
+    map  { $parts->{$_} }
+    ($from..$to);
+}
+
+sub parse {
+  my ($class, $file_name, %params) = @_;
+
+  my ($local_bank_code, $local_account_number, %transaction, @transactions, @lines);
+  my $line_number = 0;
+  my $default_currency = substr(SL::DB::Default->get_default_currency, -1, 1);
+
+  my $store_transaction = sub {
+    if (%transaction) {
+      push @transactions, { %transaction };
+      %transaction = ();
+    }
+  };
+
+  my ($active_field);
+  foreach my $line (read_file($file_name)) {
+    chomp $line;
+    $line = Encode::decode($params{charset} // 'UTF-8', $line);
+    $line =~ s{\r+}{};
+    $line_number++;
+
+    my $current_field;
+    if ($line =~ m{^:(\d+[a-z]*):}i) {
+      $current_field = $1;
+      $active_field  = $1;
+    }
+
+    if (@lines && ($line =~ m{^\%})) {
+      $lines[-1]->[0] .= substr($line, 1);
+
+    } elsif (@lines && ($active_field eq '86') && !$current_field) {
+      $lines[-1]->[0] .= $line;
+
+    } else {
+      push @lines, [ $line, $line_number ];
+    }
+  }
+
+  foreach my $line (@lines) {
+    # AT MT940 has the format  :25://AT20151/00797453990/EUR
+    # DE MT940 has the format  :25:BLZ/Konto
+    # https://www.bankaustria.at/files/MBS_MT940_V5107.pdf
+    if ($line->[0] =~ m{^:25:(?://AT)?(\d+)/(\d+)}) {
+
+      $local_bank_code      = $1;
+      $local_account_number = $2;
+
+    } elsif ($line->[0] =~ m{^:61: (\d{2}) (\d{2}) (\d{2}) (\d{4})? (C|D|RC|RD) ([a-zA-Z]?) (\d+) (?:, (\d*))? N (.{3}) (.*)}x) {
+      #                            1       2       3       4        5           6           7          8         9      10
+      # :61:2008060806CR952,N051NONREF
+
+      $store_transaction->();
+
+      my $valuta_year      = $1 * 1 + 2000;
+      my $valuta_month     = $2;
+      my $valuta_day       = $3;
+      my $trans_month      = $4 ? substr($4, 0, 2) : $valuta_month;
+      my $trans_day        = $4 ? substr($4, 2, 2) : $valuta_day;
+      my $debit_credit     = $5;
+      my $currency         = $6 || $default_currency;
+      my $amount1          = $7;
+      my $amount2          = $8 || 0;
+      my $transaction_code = $9;
+      my $reference        = $10;
+
+      my $valuta_date      = DateTime->new_local(year => $valuta_year, month => $valuta_month, day => $valuta_day);
+      my $trans_date       = DateTime->new_local(year => $valuta_year, month => $trans_month,  day => $trans_day);
+      my $diff             = $valuta_date->subtract_datetime($trans_date);
+      my $trans_year_diff  = $diff->months < 6           ?  0
+                           : $valuta_date  > $trans_date ?  1
+                           :                               -1;
+      $trans_date          = DateTime->new_local(year => $valuta_year + $trans_year_diff, month => $trans_month,  day => $trans_day);
+      my $sign             = ($debit_credit eq 'D') || ($debit_credit eq 'RC') ? -1 : 1;
+      $reference           =~ s{//.*}{};
+      $reference           = '' if $reference eq 'NONREF';
+
+      %transaction = (
+        line_number          => $line->[1],
+        currency             => $currency,
+        valutadate           => $valuta_date,
+        transdate            => $trans_date,
+        amount               => ($amount1 * 1 + ($amount2 / (10 ** length($amount2))))* $sign,
+        reference            => $reference,
+        transaction_code     => $transaction_code,
+        local_bank_code      => $local_bank_code,
+        local_account_number => $local_account_number,
+      );
+
+    } elsif (%transaction && ($line->[0] =~ m{^:86:})) {
+      if ($line->[0] =~ m{^:86:\d+\?(.+)}) {
+        # structured
+        my %parts = map { ((substr($_, 0, 2) // '0') * 1 => substr($_, 2)) } split m{\?}, $1;
+
+        $transaction{purpose}               = _join_entries(\%parts, 20, 29);
+        $transaction{remote_name}           = _join_entries(\%parts, 32, 33, '');
+        $transaction{remote_bank_code}      = $parts{30};
+        $transaction{remote_account_number} = $parts{31};
+
+      } else {
+        # unstructured
+        $transaction{purpose} = substr($line->[0], 5);
+      }
+
+      $store_transaction->();
+    }
+  }
+
+  $store_transaction->();
+
+  return @transactions;
+}
+
+1;
index 6536850..f242120 100644 (file)
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 
 package Mailer;
 
 use Email::Address;
 use Email::MIME::Creator;
+use Encode;
+use File::MimeInfo::Magic;
 use File::Slurp;
 use List::UtilsBy qw(bundle_by);
+use List::Util qw(sum);
 
 use SL::Common;
 use SL::DB::EmailJournal;
 use SL::DB::EmailJournalAttachment;
 use SL::DB::Employee;
-use SL::MIME;
+use SL::Locale::String qw(t8);
 use SL::Template;
+use SL::Version;
 
 use strict;
 
 my $num_sent = 0;
 
+my %mail_delivery_modules = (
+  sendmail => 'SL::Mailer::Sendmail',
+  smtp     => 'SL::Mailer::SMTP',
+);
+
+my %type_to_table = (
+  sales_quotation         => 'oe',
+  request_quotation       => 'oe',
+  sales_order             => 'oe',
+  purchase_order          => 'oe',
+  invoice                 => 'ar',
+  credit_note             => 'ar',
+  purchase_invoice        => 'ap',
+  letter                  => 'letter',
+  purchase_delivery_order => 'delivery_orders',
+  sales_delivery_order    => 'delivery_orders',
+  dunning                 => 'dunning',
+);
+
 sub new {
   my ($type, %params) = @_;
   my $self = { %params };
@@ -54,7 +78,7 @@ sub _create_driver {
     myconfig => \%::myconfig,
   );
 
-  my $module = ($::lx_office_conf{mail_delivery}->{method} || 'smtp') ne 'smtp' ? 'SL::Mailer::Sendmail' : 'SL::Mailer::SMTP';
+  my $module = $mail_delivery_modules{ $::lx_office_conf{mail_delivery}->{method} };
   eval "require $module" or return undef;
 
   return $module->new(%params);
@@ -81,7 +105,7 @@ sub _create_message_id {
   $domain     =~ s/.*\@//;
   $domain     =~ s/>.*//;
 
-  return  "kivitendo-$self->{version}-" . time() . "-${$}-${num_sent}\@$domain";
+  return  "kivitendo-" . SL::Version->get_version . "-" . time() . "-${$}-${num_sent}\@$domain";
 }
 
 sub _create_address_headers {
@@ -99,8 +123,21 @@ sub _create_address_headers {
     next if !$self->{$item};
 
     my @header_addresses;
+    my @addresses = Email::Address->parse($self->{$item});
+
+    # if either no address was parsed or
+    # there are more than 3 characters per parsed email extra, assume the the user has entered bunk
+    if (!@addresses) {
+       die t8('"#1" seems to be a faulty list of email addresses. No addresses could be extracted.',
+         $self->{$item},
+       );
+    } elsif ((length($self->{$item}) - sum map { length $_->original } @addresses) / @addresses > 3) {
+       die t8('"#1" seems to be a faulty list of email addresses. After extracing addresses (#2) too many characters are left.',
+         $self->{$item}, join ', ', map { $_->original } @addresses,
+       );
+    }
 
-    foreach my $addr_obj (Email::Address->parse($self->{$item})) {
+    foreach my $addr_obj (@addresses) {
       push @{ $self->{addresses}->{$item} }, $addr_obj->address;
       next if $self->{driver}->keep_from_header($item);
 
@@ -127,31 +164,58 @@ sub _create_attachment_part {
   );
 
   my $attachment_content;
+  my $file_id       = 0;
+  my $email_journal = $::instance_conf->get_email_journal;
 
   if (ref($attachment) eq "HASH") {
-    $attributes{filename} = $attachment->{name};
-    $attachment_content   = $attachment->{content} // eval { read_file($attachment->{filename}) };
+    $attributes{filename}     = $attachment->{name};
+    $file_id                  = $attachment->{id}   || '0';
+    $attributes{content_type} = $attachment->{type} || 'application/pdf';
+    $attachment_content       = $attachment->{content};
+    $attachment_content       = eval { read_file($attachment->{path}) } if !$attachment_content;
 
   } else {
-    # strip path
     $attributes{filename} =  $attachment;
     $attributes{filename} =~ s:.*\Q$self->{fileid}\E:: if $self->{fileid};
     $attributes{filename} =~ s:.*/::g;
-    $attachment_content   =  eval { read_file($attachment) };
+
+    my $application             = ($attachment =~ /(^\w+$)|\.(html|text|txt|sql)$/) ? 'text' : 'application';
+    $attributes{content_type}   = File::MimeInfo::Magic::magic($attachment);
+    $attributes{content_type} ||= "${application}/$self->{format}" if $self->{format};
+    $attributes{content_type} ||= 'application/octet-stream';
+    $attachment_content         = eval { read_file($attachment) };
   }
 
-  return undef if !defined $attachment_content;
+  return undef if $email_journal > 1 && !defined $attachment_content;
 
-  my $application             = ($attachment =~ /(^\w+$)|\.(html|text|txt|sql)$/) ? 'text' : 'application';
-  $attributes{content_type}   = SL::MIME->mime_type_from_ext($attributes{filename});
-  $attributes{content_type} ||= "${application}/$self->{format}" if $self->{format};
-  $attributes{content_type} ||= 'application/octet-stream';
-  $attributes{charset}        = $self->{charset} if lc $application eq 'text' && $self->{charset};
+  $attachment_content ||= ' ';
+  $attributes{charset}  = $self->{charset} if $self->{charset} && ($attributes{content_type} =~ m{^text/});
 
-  return Email::MIME->create(
-    attributes => \%attributes,
-    body       => $attachment_content,
+  my $ent;
+  if ( $attributes{content_type} eq 'message/rfc822' ) {
+    $ent = Email::MIME->new($attachment_content);
+  } else {
+    $ent = Email::MIME->create(
+      attributes => \%attributes,
+      body       => $attachment_content,
+    );
+  }
+
+  # Due to a bug in Email::MIME it's not enough to hand over the encoded file name in the "attributes" hash in the
+  # "create" call. Email::MIME iterates over the keys in the hash, and depending on which key it has already seen during
+  # the iteration it might revert the encoding. As Perl's hash key order is randomized for each Perl run, this means
+  # that the file name stays unencoded sometimes.
+  # Setting the header manually after the "create" call circumvents this problem.
+  $ent->header_set('Content-disposition' => 'attachment; filename="' . encode('MIME-Q', $attributes{filename}) . '"');
+
+  push @{ $self->{mail_attachments}} , SL::DB::EmailJournalAttachment->new(
+    name      => $attributes{filename},
+    mime_type => $attributes{content_type},
+    content   => ( $email_journal > 1 ? $attachment_content : ' '),
+    file_id   => $file_id,
   );
+
+  return $ent;
 }
 
 sub _create_message {
@@ -162,7 +226,7 @@ sub _create_message {
   if ($self->{message}) {
     push @parts, Email::MIME->create(
       attributes => {
-        content_type => $self->{contenttype},
+        content_type => $self->{content_type},
         charset      => $self->{charset},
         encoding     => 'quoted-printable',
       },
@@ -170,15 +234,15 @@ sub _create_message {
     );
 
     push @{ $self->{headers} }, (
-      'Content-Type' => qq|$self->{contenttype}; charset="$self->{charset}"|,
+      'Content-Type' => qq|$self->{content_type}; charset="$self->{charset}"|,
     );
   }
 
   push @parts, grep { $_ } map { $self->_create_attachment_part($_) } @{ $self->{attachments} || [] };
 
   return Email::MIME->create(
-    header_str => $self->{headers},
-    parts      => \@parts,
+      header_str => $self->{headers},
+      parts      => \@parts,
   );
 }
 
@@ -188,18 +252,23 @@ sub send {
   # Create driver for delivery method (sendmail/SMTP)
   $self->{driver} = eval { $self->_create_driver };
   if (!$self->{driver}) {
-    $self->_store_in_journal('failed', 'driver could not be created; check your configuration');
-    return "send email : $@";
+    my $error = $@;
+    $self->_store_in_journal('failed', 'driver could not be created; check your configuration & log files');
+    $::lxdebug->message(LXDebug::WARN(), "Mailer error during 'send': $error");
+
+    return $error;
   }
 
   # Set defaults & headers
-  $self->{charset}       =  'UTF-8';
-  $self->{contenttype} ||=  "text/plain";
-  $self->{headers}       =  [
-    Subject              => $self->{subject},
-    'Message-ID'         => '<' . $self->_create_message_id . '>',
-    'X-Mailer'           => "kivitendo $self->{version}",
-  ];
+  $self->{charset}        =  'UTF-8';
+  $self->{content_type} ||=  "text/plain";
+  $self->{headers}      ||=  [];
+  push @{ $self->{headers} }, (
+    Subject               => $self->{subject},
+    'Message-ID'          => '<' . $self->_create_message_id . '>',
+    'X-Mailer'            => "kivitendo " . SL::Version->get_version,
+  );
+  $self->{mail_attachments} = [];
 
   my $error;
   my $ok = eval {
@@ -209,10 +278,9 @@ sub send {
 
     my $email = $self->_create_message;
 
-    # $::lxdebug->message(0, "message: " . $email->as_string);
-    # return "boom";
+    my $from_obj = (Email::Address->parse($self->{from}))[0];
 
-    $self->{driver}->start_mail(from => $self->{from}, to => [ $self->_all_recipients ]);
+    $self->{driver}->start_mail(from => $from_obj->address, to => [ $self->_all_recipients ]);
     $self->{driver}->print($email->as_string);
     $self->{driver}->send;
 
@@ -221,14 +289,15 @@ sub send {
 
   $error = $@ if !$ok;
 
-  $self->_store_in_journal;
+  # create journal and link to record
+  $self->{journalentry} = $self->_store_in_journal;
+  $self->_create_record_link if $self->{journalentry};
 
-  return $ok ? '' : "send email: $error";
+  return $ok ? '' : ($error || "undefined error");
 }
 
 sub _all_recipients {
   my ($self) = @_;
-
   $self->{addresses} ||= {};
   return map { @{ $self->{addresses}->{$_} || [] } } qw(to cc bcc);
 }
@@ -245,22 +314,9 @@ sub _store_in_journal {
   $extended_status //= $self->{driver}->extended_status if $self->{driver};
   $extended_status //= 'unknown error';
 
-  my @attachments;
-
-  @attachments = grep { $_ } map {
-    my $part = $self->_create_attachment_part($_);
-    if ($part) {
-      SL::DB::EmailJournalAttachment->new(
-        name      => $part->filename,
-        mime_type => $part->content_type,
-        content   => $part->body,
-      )
-    }
-  } @{ $self->{attachments} || [] } if $journal_enable > 1;
-
   my $headers = join "\r\n", (bundle_by { join(': ', @_) } 2, @{ $self->{headers} || [] });
 
-  SL::DB::EmailJournal->new(
+  my $jentry = SL::DB::EmailJournal->new(
     sender          => SL::DB::Manager::Employee->current,
     from            => $self->{from}    // '',
     recipients      => join(', ', $self->_all_recipients),
@@ -268,10 +324,133 @@ sub _store_in_journal {
     headers         => $headers,
     body            => $self->{message} // '',
     sent_on         => DateTime->now_local,
-    attachments     => \@attachments,
+    attachments     => \@{ $self->{mail_attachments} },
     status          => $status,
     extended_status => $extended_status,
   )->save;
+  return $jentry->id;
+}
+
+
+sub _create_record_link {
+  my ($self) = @_;
+
+  # check for custom/overloaded types and ids (form != controller)
+  my $record_type = $self->{record_type} || $::form->{type};
+  my $record_id   = $self->{record_id}   || $::form->{id};
+
+  # you may send mails for unsaved objects (no record_id => unlinkable case)
+  if ($self->{journalentry} && $record_id && exists($type_to_table{$record_type})) {
+    RecordLinks->create_links(
+      mode       => 'ids',
+      from_table => $type_to_table{$record_type},
+      from_ids   => $record_id,
+      to_table   => 'email_journal',
+      to_id      => $self->{journalentry},
+    );
+  }
 }
 
 1;
+
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Mailer - Base class for sending mails from kivitendo
+
+=head1 SYNOPSIS
+
+  package SL::BackgroundJob::CreatePeriodicInvoices;
+
+  use SL::Mailer;
+
+  my $mail              = Mailer->new;
+  $mail->{from}         = $config{periodic_invoices}->{email_from};
+  $mail->{to}           = $email;
+  $mail->{subject}      = $config{periodic_invoices}->{email_subject};
+  $mail->{content_type} = $filename =~ m/.html$/ ? 'text/html' : 'text/plain';
+  $mail->{message}      = $output;
+
+  $mail->send;
+
+=head1 OVERVIEW
+
+Mail can be sent from kivitendo via the sendmail command or the smtp protocol.
+
+
+=head1 INTERNAL DATA TYPES
+
+
+=over 2
+
+=item C<%mail_delivery_modules>
+
+  Currently two modules are supported: smtp or sendmail.
+
+=item C<%type_to_table>
+
+  Due to the lack of a single global mapping for $form->{type},
+  type is mapped to the corresponding database table. All types which
+  implement a mail action are currently mapped and should be mapped.
+  Type is either the value of the old form or the newer controller
+  based object type.
+
+=back
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<new>
+
+=item C<_create_driver>
+
+=item C<_cleanup_addresses>
+
+=item C<_create_address_headers>
+
+=item C<_create_message_id>
+
+=item C<_create_attachment_part>
+
+=item C<_create_message>
+
+=item C<send>
+
+  If a mail was sent successfully the internal function _store_in_journal
+  is called if email journaling is enabled. If _store_in_journal was executed
+  successfully and the calling form is already persistent (database id) a
+  record_link will be created.
+
+=item C<_all_recipients>
+
+=item C<_store_in_journal>
+
+=item C<_create_record_link $self->{journalentry}, $::form->{id}, $self->{record_id}>
+
+
+  If $self->{journalentry} and either $self->{record_id} or $::form->{id} (checked in
+  this order) exist a record link from record to email journal is created.
+  It is possible to provide an array reference with more than one id in
+  $self->{record_id} or $::form->{id}. In this case all records are linked to
+  the mail.
+  Will fail silently if record_link creation wasn't successful (same behaviour as
+  _store_in_journal).
+
+=item C<validate>
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+=cut
index 6bbcffb..0c6df22 100644 (file)
@@ -3,14 +3,9 @@ package SL::Menu;
 use strict;
 
 use SL::Auth;
-use YAML ();
 use File::Spec;
 use SL::MoreCommon qw(uri_encode);
-
-our $yaml_xs;
-BEGIN {
-   $yaml_xs =  eval { require YAML::XS };
-}
+use SL::YAML;
 
 our %menu_cache;
 
@@ -28,11 +23,20 @@ sub new {
     my $nodes_by_id = {};
     for my $file (@files) {
       my $data;
-      if ($yaml_xs) {
-        $data = YAML::XS::LoadFile(File::Spec->catfile($path, $file));
-      } else {
-        $data = YAML::LoadFile(File::Spec->catfile($path, $file));
-      }
+      eval {
+        $data = SL::YAML::LoadFile(File::Spec->catfile($path, $file));
+        1;
+      } or do {
+        die "Error while parsing $file: $@";
+      };
+
+      # check if this file is internally consistent.
+      die 'not an array ref' unless $data && 'ARRAY' eq ref $data; # TODO get better diag to user
+
+      # in particular duplicate ids tend to come up as a user error when editing the menu files
+      #my %uniq_ids;
+      #$uniq_ids{$_->{id}}++ && die "Error in $file: duplicate id $_->{id}" for @$data;
+
       _merge($nodes, $nodes_by_id, $data);
     }
 
@@ -57,11 +61,11 @@ sub new {
 sub _merge {
   my ($nodes, $by_id, $data) = @_;
 
-  die 'not an array ref' unless $data && 'ARRAY' eq ref $data; # TODO check this sooner, to get better diag to user
-
   for my $node (@$data) {
     my $id = $node->{id};
 
+    die "menu: node with name '$node->{name}' does not have an id" if !$id;
+
     my $merge_to = $by_id->{$id};
 
     if (!$merge_to) {
@@ -106,6 +110,19 @@ sub build_tree {
     push @{ $by_parent{ $node->{parent} // '' } //= [] }, $node;
   }
 
+  # autovivify order in by_parent, so that numerical sorting for entries without order
+  # preserves their order and position with respect to entries with order.
+  for (values %by_parent) {
+    my $last_order = 0;
+    for my $node (@$_) {
+      if (defined $node->{order} && $node->{order} * 1) {
+        $last_order = $node->{order};
+      } else {
+        $node->{order} = ++$last_order;
+      }
+    }
+  }
+
   my $tree = { };
   $self->{by_id}{''} = $tree;
 
@@ -154,7 +171,7 @@ sub parse_access_string {
 
   my $access = $node->{access};
 
-  while ($access =~ m/^([a-z_\/]+|\||\&|\(|\)|\s+)/) {
+  while ($access =~ m/^([a-z_\/]+|\!|\||\&|\(|\)|\s+)/) {
     my $token = $1;
     substr($access, 0, length($1)) = "";
 
@@ -173,7 +190,7 @@ sub parse_access_string {
       }
       $cur_ary = $stack[-1];
 
-    } elsif (($token eq "|") || ($token eq "&")) {
+    } elsif (($token eq "|") || ($token eq "&") || ($token eq "!")) {
       push @{$cur_ary}, $token;
 
     } else {
@@ -201,14 +218,16 @@ sub href_for_node {
 
   return undef if !$node->{href} && !$node->{module} && !$node->{params};
 
-  my $href = $node->{href} || $node->{module} || 'controller.pl';
-  my @tokens;
+  return $node->{href_for_node} ||= do {
+    my $href = $node->{href} || $node->{module} || 'controller.pl';
+    my @tokens;
 
-  while (my ($key, $value) = each %{ $node->{params} }) {
-    push @tokens, uri_encode($key, 1) . "=" . uri_encode($value, 1);
-  }
+    while (my ($key, $value) = each %{ $node->{params} }) {
+      push @tokens, uri_encode($key, 1) . "=" . uri_encode($value, 1);
+    }
 
-  return join '?', $href, grep $_, join '&', @tokens;
+    join '?', $href, grep $_, join '&', @tokens;
+  }
 }
 
 sub name_for_node {
@@ -245,4 +264,3 @@ sub set_access {
 }
 
 1;
-
index b1ea817..0aa84a7 100644 (file)
@@ -8,7 +8,7 @@ our @EXPORT_OK = qw(ary_union ary_intersect ary_diff listify ary_to_hash uri_enc
 
 use Encode ();
 use List::MoreUtils qw(zip);
-use YAML;
+use SL::YAML;
 
 use strict;
 
@@ -23,7 +23,7 @@ sub save_form {
     delete $main::form->{$key};
   }
 
-  my $old_form = YAML::Dump($main::form);
+  my $old_form = SL::YAML::Dump($main::form);
   $old_form =~ s|!|!:|g;
   $old_form =~ s|\n|!n|g;
   $old_form =~ s|\r|!r|g;
@@ -49,7 +49,7 @@ sub restore_form {
   $old_form =~ s|!n|\n|g;
   $old_form =~ s|![!:]|!|g;
 
-  my $new_form = YAML::Load($old_form);
+  my $new_form = SL::YAML::Load($old_form);
   map { $form->{$_} = $new_form->{$_} if (!$keep_vars_map{$_}) } keys %{ $new_form };
 
   $main::lxdebug->leave_sub();
index 38959f4..1d768f3 100644 (file)
@@ -4,6 +4,7 @@ package Notes;
 
 use SL::Common;
 use SL::DBUtils;
+use SL::DB;
 
 use strict;
 
@@ -16,24 +17,25 @@ sub save {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
-  my ($query, @values);
+  SL::DB->client->with_transaction(sub {
+    my $dbh      = $params{dbh} || SL::DB->client->dbh;
+    my ($query, @values);
 
-  if (!$params{id}) {
-    ($params{id}) = selectrow_query($form, $dbh, qq|SELECT nextval('note_id')|);
-    $query        = qq|INSERT INTO notes (created_by, trans_id, trans_module, subject, body, id)
-                       VALUES ((SELECT id FROM employee WHERE login = ?), ?, ?, ?, ?, ?)|;
-    push @values, $::myconfig{login}, conv_i($params{trans_id}), $params{trans_module};
+    if (!$params{id}) {
+      ($params{id}) = selectrow_query($form, $dbh, qq|SELECT nextval('note_id')|);
+      $query        = qq|INSERT INTO notes (created_by, trans_id, trans_module, subject, body, id)
+                         VALUES ((SELECT id FROM employee WHERE login = ?), ?, ?, ?, ?, ?)|;
+      push @values, $::myconfig{login}, conv_i($params{trans_id}), $params{trans_module};
 
-  } else {
-    $query        = qq|UPDATE notes SET subject = ?, body = ? WHERE id = ?|;
-  }
+    } else {
+      $query        = qq|UPDATE notes SET subject = ?, body = ? WHERE id = ?|;
+    }
 
-  push @values, $params{subject}, $params{body}, conv_i($params{id});
+    push @values, $params{subject}, $params{body}, conv_i($params{id});
 
-  do_query($form, $dbh, $query, @values);
-
-  $dbh->commit() unless ($params{dbh});
+    do_query($form, $dbh, $query, @values);
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 
@@ -71,14 +73,15 @@ sub delete {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
-  my $id       = conv_i($params{id});
-
-  do_query($form, $dbh, qq|DELETE FROM follow_up_links WHERE follow_up_id IN (SELECT DISTINCT id FROM follow_ups WHERE note_id = ?)|, $id);
-  do_query($form, $dbh, qq|DELETE FROM follow_ups      WHERE note_id = ?|, $id);
-  do_query($form, $dbh, qq|DELETE FROM notes           WHERE id = ?|, $id);
+  SL::DB->client->with_transaction(sub {
+    my $dbh      = $params{dbh} || SL::DB->client->dbh;
+    my $id       = conv_i($params{id});
 
-  $dbh->commit() unless ($params{dbh});
+    do_query($form, $dbh, qq|DELETE FROM follow_up_links WHERE follow_up_id IN (SELECT DISTINCT id FROM follow_ups WHERE note_id = ?)|, $id);
+    do_query($form, $dbh, qq|DELETE FROM follow_ups      WHERE note_id = ?|, $id);
+    do_query($form, $dbh, qq|DELETE FROM notes           WHERE id = ?|, $id);
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
index 15575c3..2b2c51a 100644 (file)
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #=====================================================================
 #
 # this is the default code for the Check package
index 3bb6458..4ce1b10 100644 (file)
--- a/SL/OE.pm
+++ b/SL/OE.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Order entry module
 package OE;
 
 use List::Util qw(max first);
-use YAML;
 
 use SL::AM;
 use SL::Common;
 use SL::CVar;
 use SL::DB::Order;
 use SL::DB::PeriodicInvoicesConfig;
+use SL::DB::Project;
+use SL::DB::ProjectType;
+use SL::DB::RequirementSpecOrder;
 use SL::DB::Status;
 use SL::DB::Tax;
 use SL::DBUtils;
 use SL::HTML::Restrict;
 use SL::IC;
 use SL::TransNumber;
+use SL::Util qw(trim);
+use SL::DB;
+use SL::YAML;
+use Text::ParseWords;
 
 use strict;
 
@@ -94,6 +101,12 @@ sub transactions {
         FROM record_links rl1
         LEFT JOIN record_links rl2 ON (rl1.to_table = rl2.from_table AND rl1.to_id = rl2.from_id)
         WHERE rl1.from_table = 'oe' AND rl2.to_table = 'ar'
+        UNION
+        SELECT rl1.from_id, rl3.to_id
+        FROM record_links rl1
+        JOIN record_links rl2 ON (rl1.to_table = rl2.from_table AND rl1.to_id = rl2.from_id)
+        JOIN record_links rl3 ON (rl2.to_table = rl3.from_table AND rl2.to_id = rl3.from_id)
+        WHERE rl1.from_table = 'oe' AND rl2.to_table = 'ar' AND rl3.to_table = 'ar'
       ) rl
       LEFT JOIN ar ON ar.id = rl.to_id
 
@@ -103,20 +116,31 @@ sub transactions {
     }
   }
 
+  my ($phone_notes_columns, $phone_notes_join);
+  $form->{phone_notes} = trim($form->{phone_notes});
+  if ($form->{phone_notes}) {
+    $phone_notes_columns = qq| , phone_notes.subject AS phone_notes_subject, phone_notes.body AS phone_notes_body |;
+    $phone_notes_join    = qq| JOIN notes phone_notes ON (o.id = phone_notes.trans_id AND phone_notes.trans_module LIKE 'oe') |;
+  }
+
   $query =
     qq|SELECT o.id, o.ordnumber, o.transdate, o.reqdate, | .
     qq|  o.amount, ct.${vc}number, ct.name, o.netamount, o.${vc}_id, o.globalproject_id, | .
     qq|  o.closed, o.delivered, o.quonumber, o.cusordnumber, o.shippingpoint, o.shipvia, | .
     qq|  o.transaction_description, | .
     qq|  o.marge_total, o.marge_percent, | .
+    qq|  o.exchangerate, | .
     qq|  o.itime::DATE AS insertdate, | .
-    qq|  ex.$rate AS exchangerate, | .
+    qq|  o.intnotes, | .
+    qq|  department.description as department, | .
+    qq|  ex.$rate AS daily_exchangerate, | .
     qq|  pt.description AS payment_terms, | .
     qq|  pr.projectnumber AS globalprojectnumber, | .
     qq|  e.name AS employee, s.name AS salesman, | .
     qq|  ct.${vc}number AS vcnumber, ct.country, ct.ustid, ct.business_id,  | .
     qq|  tz.description AS taxzone | .
     $periodic_invoices_columns .
+    $phone_notes_columns .
     qq|  , o.order_probability, o.expected_billing_date, (o.netamount * o.order_probability / 100) AS expected_netamount | .
     qq|FROM oe o | .
     qq|JOIN $vc ct ON (o.${vc}_id = ct.id) | .
@@ -128,15 +152,15 @@ sub transactions {
     qq|LEFT JOIN project pr ON (o.globalproject_id = pr.id) | .
     qq|LEFT JOIN payment_terms pt ON (pt.id = o.payment_id)| .
     qq|LEFT JOIN tax_zones tz ON (o.taxzone_id = tz.id) | .
+    qq|LEFT JOIN department   ON (o.department_id = department.id) | .
     qq|$periodic_invoices_joins | .
+    $phone_notes_join .
     qq|WHERE (o.quotation = ?) |;
   push(@values, $quotation);
 
-  my ($null, $split_department_id) = split /--/, $form->{department};
-  my $department_id = $form->{department_id} || $split_department_id;
-  if ($department_id) {
+  if ($form->{department_id}) {
     $query .= qq| AND o.department_id = ?|;
-    push(@values, $department_id);
+    push(@values, $form->{department_id});
   }
 
   if ($form->{"project_id"}) {
@@ -155,7 +179,7 @@ sub transactions {
         WHERE proi.projectnumber ILIKE ? AND oi.trans_id = o.id
       ))
 SQL
-    push @values, "%" . $form->{"projectnumber"} . "%", "%" . $form->{"projectnumber"} . "%" ;
+    push @values, like($form->{"projectnumber"}), like($form->{"projectnumber"});
   }
 
   if ($form->{"business_id"}) {
@@ -169,15 +193,16 @@ SQL
 
   } elsif ($form->{$vc}) {
     $query .= " AND ct.name ILIKE ?";
-    push(@values, '%' . $form->{$vc} . '%');
+    push(@values, like($form->{$vc}));
   }
 
   if ($form->{"cp_name"}) {
     $query .= " AND (cp.cp_name ILIKE ? OR cp.cp_givenname ILIKE ?)";
-    push(@values, ('%' . $form->{"cp_name"} . '%')x2);
+    push(@values, (like($form->{"cp_name"}))x2);
   }
 
-  if (!$main::auth->assert('sales_all_edit', 1)) {
+  if ( !(    ($vc eq 'customer' && ($main::auth->assert('sales_all_edit',    1) || $main::auth->assert('sales_order_view',    1)))
+          || ($vc eq 'vendor'   && ($main::auth->assert('purchase_all_edit', 1) || $main::auth->assert('purchase_order_view', 1))) ) ) {
     $query .= " AND o.employee_id = (select id from employee where login= ?)";
     push @values, $::myconfig{login};
   }
@@ -205,12 +230,12 @@ SQL
 
   if ($form->{$ordnumber}) {
     $query .= qq| AND o.$ordnumber ILIKE ?|;
-    push(@values, '%' . $form->{$ordnumber} . '%');
+    push(@values, like($form->{$ordnumber}));
   }
 
   if ($form->{cusordnumber}) {
     $query .= qq| AND o.cusordnumber ILIKE ?|;
-    push(@values, '%' . $form->{cusordnumber} . '%');
+    push(@values, like($form->{cusordnumber}));
   }
 
   if($form->{transdatefrom}) {
@@ -245,7 +270,7 @@ SQL
 
   if ($form->{shippingpoint}) {
     $query .= qq| AND o.shippingpoint ILIKE ?|;
-    push(@values, '%' . $form->{shippingpoint} . '%');
+    push(@values, like($form->{shippingpoint}));
   }
 
   if ($form->{taxzone_id} ne '') { # taxzone_id could be 0
@@ -255,7 +280,7 @@ SQL
 
   if ($form->{transaction_description}) {
     $query .= qq| AND o.transaction_description ILIKE ?|;
-    push(@values, '%' . $form->{transaction_description} . '%');
+    push(@values, like($form->{transaction_description}));
   }
 
   if ($form->{periodic_invoices_active} ne $form->{periodic_invoices_inactive}) {
@@ -270,7 +295,7 @@ SQL
   if (($form->{order_probability_value} || '') ne '') {
     my $op  = $form->{order_probability_value} eq 'le' ? '<=' : '>=';
     $query .= qq| AND (o.order_probability ${op} ?)|;
-    push @values, $form->{order_probability_value};
+    push @values, trim($form->{order_probability_value});
   }
 
   if ($form->{expected_billing_date_from}) {
@@ -283,6 +308,98 @@ SQL
     push @values, conv_date($form->{expected_billing_date_to});
   }
 
+  if ($form->{intnotes}) {
+    $query .= qq| AND o.intnotes ILIKE ?|;
+    push(@values, like($form->{intnotes}));
+  }
+
+  if ($form->{phone_notes}) {
+    $query .= qq| AND (phone_notes.subject ILIKE ? OR phone_notes.body ILIKE ?)|;
+    push(@values, like($form->{phone_notes}), like($form->{phone_notes}));
+  }
+
+  $form->{fulltext} = trim($form->{fulltext});
+  if ($form->{fulltext}) {
+    my @fulltext_fields = qw(o.notes
+                             o.intnotes
+                             o.shippingpoint
+                             o.shipvia
+                             o.transaction_description
+                             o.quonumber
+                             o.ordnumber
+                             o.cusordnumber);
+    $query .= ' AND (';
+    $query .= join ' ILIKE ? OR ', @fulltext_fields;
+    $query .= ' ILIKE ?';
+
+    $query .= <<SQL;
+      OR EXISTS (
+        SELECT files.id FROM files LEFT JOIN file_full_texts ON (file_full_texts.file_id = files.id)
+          WHERE files.object_id = o.id AND files.object_type = 'sales_order'
+            AND file_full_texts.full_text ILIKE ?)
+SQL
+
+    $query .= <<SQL;
+      OR EXISTS (
+        SELECT notes.id FROM notes
+          WHERE notes.trans_id = o.id AND notes.trans_module LIKE 'oe'
+            AND (notes.subject ILIKE ? OR notes.body ILIKE ?))
+SQL
+
+    $query .= <<SQL;
+      OR EXISTS (
+        SELECT follow_up_links.id FROM follow_up_links
+          WHERE follow_up_links.trans_id = o.id AND trans_type = 'sales_order'
+            AND EXISTS (
+              SELECT notes.id FROM notes
+                WHERE trans_module LIKE 'fu' AND trans_id = follow_up_links.follow_up_id
+                  AND (notes.subject ILIKE ? OR notes.body ILIKE ?)))
+SQL
+
+    $query .= ')';
+
+    push(@values, like($form->{fulltext})) for 1 .. (scalar @fulltext_fields) + 5;
+  }
+
+  if ($form->{parts_partnumber}) {
+    $query .= <<SQL;
+      AND EXISTS (
+        SELECT orderitems.trans_id
+        FROM orderitems
+        LEFT JOIN parts ON (orderitems.parts_id = parts.id)
+        WHERE (orderitems.trans_id = o.id)
+          AND (parts.partnumber ILIKE ?)
+        LIMIT 1
+      )
+SQL
+    push @values, like($form->{parts_partnumber});
+  }
+
+  if ($form->{parts_description}) {
+    $query .= <<SQL;
+      AND EXISTS (
+        SELECT orderitems.trans_id
+        FROM orderitems
+        WHERE (orderitems.trans_id = o.id)
+          AND (orderitems.description ILIKE ?)
+        LIMIT 1
+      )
+SQL
+    push @values, like($form->{parts_description});
+  }
+
+  if ($form->{all}) {
+    my @tokens = parse_line('\s+', 0, $form->{all});
+    # ordnumber quonumber customer.name vendor.name transaction_description
+    $query .= qq| AND (
+      o.ordnumber ILIKE ? OR
+      o.quonumber ILIKE ? OR
+      ct.name     ILIKE ? OR
+      o.transaction_description ILIKE ?
+    )| for @tokens;
+    push @values, (like($_))x4 for @tokens;
+  }
+
   my ($cvar_where, @cvar_values) = CVar->build_filter_query('module'         => 'CT',
                                                             'trans_id_field' => 'ct.id',
                                                             'filter'         => $form,
@@ -310,6 +427,8 @@ SQL
     "insertdate"              => "o.itime",
     "taxzone"                 => "tz.description",
     "payment_terms"           => "pt.description",
+    "department"              => "department.description",
+    "intnotes"                => "o.intnotes",
   );
   if ($form->{sort} && grep($form->{sort}, keys(%allowed_sort_columns))) {
     $sortorder = $allowed_sort_columns{$form->{sort}} . " ${sortdir}"  . ", o.itime ${sortdir}";
@@ -325,9 +444,15 @@ SQL
   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
     $ref->{billed_amount}    = $billed_amount{$ref->{id}};
     $ref->{billed_netamount} = $billed_netamount{$ref->{id}};
-    $ref->{remaining_amount} = $ref->{amount} - $ref->{billed_amount};
-    $ref->{remaining_netamount} = $ref->{netamount} - $ref->{billed_netamount};
-    $ref->{exchangerate} = 1 unless $ref->{exchangerate};
+    if ($ref->{billed_amount} < 0) { # case: credit note(s) higher than invoices
+      $ref->{remaining_amount} = $ref->{amount} + $ref->{billed_amount};
+      $ref->{remaining_netamount} = $ref->{netamount} + $ref->{billed_netamount};
+    } else {
+      $ref->{remaining_amount} = $ref->{amount} - $ref->{billed_amount};
+      $ref->{remaining_netamount} = $ref->{netamount} - $ref->{billed_netamount};
+    }
+    $ref->{exchangerate} ||= $ref->{daily_exchangerate};
+    $ref->{exchangerate} ||= 1;
     push @{ $form->{OE} }, $ref if $ref->{id} != $id{ $ref->{id} };
     $id{ $ref->{id} } = $ref->{id};
   }
@@ -376,12 +501,22 @@ sub transactions_for_todo_list {
 }
 
 sub save {
+  my ($self, $myconfig, $form) = @_;
+  $main::lxdebug->enter_sub();
+
+  my $rc = SL::DB->client->with_transaction(\&_save, $self, $myconfig, $form);
+
+  $::lxdebug->leave_sub;
+
+  return $rc;
+}
+
+sub _save {
   $main::lxdebug->enter_sub();
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database, turn off autocommit
-  my $dbh = $form->get_standard_dbh;
+  my $dbh = SL::DB->client->dbh;
   my $restricter = SL::HTML::Restrict->create;
 
   my ($query, @values, $sth, $null);
@@ -402,9 +537,15 @@ sub save {
 
   my $number_field         = $form->{type} =~ m{order} ? 'ordnumber' : 'quonumber';
   my $trans_number         = SL::TransNumber->new(type => $form->{type}, dbh => $dbh, number => $form->{$number_field}, id => $form->{id});
-  $form->{$number_field} ||= $trans_number->create_unique;
+  $form->{$number_field} ||= $trans_number->create_unique; # set $form->{ordnumber} or $form->{quonumber}
+  my $is_new               = !$form->{id};
 
   if ($form->{id}) {
+    $query = qq|DELETE FROM custom_variables
+                WHERE (config_id IN (SELECT id        FROM custom_variable_configs WHERE (module = 'ShipTo')))
+                  AND (trans_id  IN (SELECT shipto_id FROM shipto                  WHERE (module = 'OE') AND (trans_id = ?)))|;
+    do_query($form, $dbh, $query, $form->{id});
+
     $query = qq|DELETE FROM shipto | .
              qq|WHERE trans_id = ? AND module = 'OE'|;
     do_query($form, $dbh, $query, $form->{id});
@@ -533,6 +674,26 @@ sub save {
       $pricegroup_id *= 1;
       $pricegroup_id  = undef if !$pricegroup_id;
 
+      # force new project, if not set yet
+      if ($::instance_conf->get_order_always_project && !$form->{"globalproject_id"} && ($form->{type} eq 'sales_order')) {
+        require SL::DB::Customer;
+        my $customer = SL::DB::Manager::Customer->find_by(id => $form->{customer_id});
+        die "Can't find customer" unless $customer;
+        die $main::locale->text("Error while creating project with project number of new order number, project number #1 already exists!", $form->{ordnumber})
+          if SL::DB::Manager::Project->find_by(projectnumber => $form->{ordnumber});
+
+        my $new_project = SL::DB::Project->new(
+          projectnumber     => $form->{ordnumber},
+          description       => $customer->name,
+          customer_id       => $customer->id,
+          active            => 1,
+          project_type_id   => $::instance_conf->get_project_type_id,
+          project_status_id => $::instance_conf->get_project_status_id,
+        );
+        $new_project->save;
+        $form->{"globalproject_id"} = $new_project->id;
+      }
+
       CVar->get_non_editable_ic_cvars(form               => $form,
                                       dbh                => $dbh,
                                       row                => $i,
@@ -593,8 +754,12 @@ SQL
                                   dbh          => $dbh);
 
       # link previous items with orderitems
-      foreach (qw(orderitems invoice)) {
-        if (!$form->{saveasnew} && !$form->{useasnew} && $form->{"converted_from_${_}_id_$i"}) {
+      # assume we have a new workflow if we link from invoice or order to quotation
+      # unluckily orderitems are used for quotation and orders - therefore one more
+      # check to be sure NOT to link from order to quotation
+      foreach (qw(orderitems)) {
+        if (!$form->{saveasnew} && !$form->{useasnew} && $form->{"converted_from_${_}_id_$i"}
+              && $form->{type} !~ 'quotation') {
           RecordLinks->create_links('dbh'        => $dbh,
                                     'mode'       => 'ids',
                                     'from_table' => $_,
@@ -625,7 +790,7 @@ SQL
   my $tax = 0;
   map { $tax += $form->round_amount($taxaccounts{$_}, 2) } keys %taxaccounts;
 
-  $amount = $form->round_amount($netamount + $tax, 2);
+  $amount = $form->round_amount($netamount + $tax, 2, 1);
   $netamount = $form->round_amount($netamount, 2);
 
   if ($form->{currency} eq $form->{defaultcurrency}) {
@@ -634,20 +799,21 @@ SQL
     $exchangerate = $form->check_exchangerate($myconfig, $form->{currency}, $form->{transdate}, ($form->{vc} eq 'customer') ? 'buy' : 'sell');
   }
 
-  $form->{exchangerate} = $exchangerate || $form->parse_amount($myconfig, $form->{exchangerate});
+  # from inputfield (exchangerate) or hidden (forex)
+  my $exchangerate_from_form = $form->{forex} || $form->parse_amount($myconfig, $form->{exchangerate});
 
-  my $quotation = $form->{type} =~ /_order$/ ? 'f' : 't';
+  $form->{exchangerate} = $exchangerate || $exchangerate_from_form;
 
-  ($null, $form->{department_id}) = split(/--/, $form->{department}) if $form->{department};
+  my $quotation = $form->{type} =~ /_order$/ ? 'f' : 't';
 
   # save OE record
   $query =
     qq|UPDATE oe SET
          ordnumber = ?, quonumber = ?, cusordnumber = ?, transdate = ?, vendor_id = ?,
-         customer_id = ?, amount = ?, netamount = ?, reqdate = ?, taxincluded = ?,
+         customer_id = ?, amount = ?, netamount = ?, reqdate = ?, tax_point = ?, taxincluded = ?,
          shippingpoint = ?, shipvia = ?, notes = ?, intnotes = ?, currency_id = (SELECT id FROM currencies WHERE name=?), closed = ?,
          delivered = ?, proforma = ?, quotation = ?, department_id = ?, language_id = ?,
-         taxzone_id = ?, shipto_id = ?, payment_id = ?, delivery_vendor_id = ?, delivery_customer_id = ?,delivery_term_id = ?,
+         taxzone_id = ?, shipto_id = ?, billing_address_id = ?, payment_id = ?, delivery_vendor_id = ?, delivery_customer_id = ?,delivery_term_id = ?,
          globalproject_id = ?, employee_id = ?, salesman_id = ?, cp_id = ?, transaction_description = ?, marge_total = ?, marge_percent = ?
          , order_probability = ?, expected_billing_date = ?
        WHERE id = ?|;
@@ -655,14 +821,14 @@ SQL
   @values = ($form->{ordnumber} || '', $form->{quonumber},
              $form->{cusordnumber}, conv_date($form->{transdate}),
              conv_i($form->{vendor_id}), conv_i($form->{customer_id}),
-             $amount, $netamount, conv_date($reqdate),
+             $amount, $netamount, conv_date($reqdate), conv_date($form->{tax_point}),
              $form->{taxincluded} ? 't' : 'f', $form->{shippingpoint},
              $form->{shipvia}, $restricter->process($form->{notes}), $form->{intnotes},
              $form->{currency}, $form->{closed} ? 't' : 'f',
              $form->{delivered} ? "t" : "f", $form->{proforma} ? 't' : 'f',
              $quotation, conv_i($form->{department_id}),
              conv_i($form->{language_id}), conv_i($form->{taxzone_id}),
-             conv_i($form->{shipto_id}), conv_i($form->{payment_id}),
+             conv_i($form->{shipto_id}), conv_i($form->{billing_address_id}), conv_i($form->{payment_id}),
              conv_i($form->{delivery_vendor_id}),
              conv_i($form->{delivery_customer_id}),
              conv_i($form->{delivery_term_id}),
@@ -674,6 +840,8 @@ SQL
              conv_i($form->{id}));
   do_query($form, $dbh, $query, @values);
 
+  $form->new_lastmtime('oe');
+
   $form->{ordtotal} = $amount;
 
   $form->{name} = $form->{ $form->{vc} };
@@ -688,28 +856,21 @@ SQL
   $form->save_status($dbh);
 
   # Link this record to the records it was created from.
-  # check every record type we may link. i am not happy with converting the string to array back
-  # should be a array from the start (OE.pm -> retrieve).
-  #  and that i need the local array ref for close_quotation_rfqs. better ideas welcome
   $form->{convert_from_oe_ids} =~ s/^\s+//;
   $form->{convert_from_oe_ids} =~ s/\s+$//;
   my @convert_from_oe_ids      =  split m/\s+/, $form->{convert_from_oe_ids};
   delete $form->{convert_from_oe_ids};
-  @{ $form->{convert_from_oe_ids} }      =  @convert_from_oe_ids;
-  foreach (qw(ar oe)) {
-    if (!$form->{useasnew} && $form->{"convert_from_${_}_ids"}) {
-      RecordLinks->create_links('dbh'        => $dbh,
-                                'mode'       => 'ids',
-                                'from_table' => $_,
-                                'from_ids'   => $form->{"convert_from_${_}_ids"},
-                                'to_table'   => 'oe',
-                                'to_id'      => $form->{id},
-        );
-      delete $form->{"convert_from_${_}_ids"};
-    }
+  if (!$form->{useasnew} && scalar @convert_from_oe_ids) {
+    RecordLinks->create_links('dbh'        => $dbh,
+                              'mode'       => 'ids',
+                              'from_table' => 'oe',
+                              'from_ids'   => \@convert_from_oe_ids,
+                              'to_table'   => 'oe',
+                              'to_id'      => $form->{id},
+      );
     $self->_close_quotations_rfqs('dbh'     => $dbh,
                                   'from_id' => \@convert_from_oe_ids,
-                                  'to_id'   => $form->{id}) if $_ eq 'oe';
+                                  'to_id'   => $form->{id});
   }
 
   if (($form->{currency} ne $form->{defaultcurrency}) && !$exchangerate) {
@@ -726,16 +887,80 @@ SQL
 
   Common::webdav_folder($form);
 
-  my $rc = $dbh->commit;
-
   $self->save_periodic_invoices_config(dbh         => $dbh,
                                        oe_id       => $form->{id},
                                        config_yaml => $form->{periodic_invoices_config})
     if ($form->{type} eq 'sales_order');
 
+  $self->_link_created_sales_order_to_requirement_specs_for_sales_quotations(
+    type               => $form->{type},
+    converted_from_ids => \@convert_from_oe_ids,
+    sales_order_id     => $form->{id},
+    is_new             => $is_new,
+  );
+
+  $self->_set_project_in_linked_requirement_spec(
+    type           => $form->{type},
+    project_id     => $form->{globalproject_id},
+    sales_order_id => $form->{id},
+  );
+
   $main::lxdebug->leave_sub();
 
-  return $rc;
+  return 1;
+}
+
+sub _link_created_sales_order_to_requirement_specs_for_sales_quotations {
+  my ($self, %params) = @_;
+
+  # If this is a sales order created from a sales quotation and if
+  # that sales quotation was created from a requirement spec document
+  # then link the newly created sales order to the requirement spec
+  # document, too.
+
+  return if !$params{is_new};
+  return if  $params{type} ne 'sales_order';
+  return if !@{ $params{converted_from_ids} };
+
+  my $oe_objects       = SL::DB::Manager::Order->get_all(where => [ id => $params{converted_from_ids} ]);
+  my @sales_quotations = grep { $_->is_type('sales_quotation') } @{ $oe_objects };
+
+  return if !@sales_quotations;
+
+  my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => [ map { $_->id } @sales_quotations ] ]);
+
+  return if !@{ $rs_orders };
+
+  $rs_orders->[0]->db->with_transaction(sub {
+    foreach my $rs_order (@{ $rs_orders }) {
+      SL::DB::RequirementSpecOrder->new(
+        order_id            => $params{sales_order_id},
+        requirement_spec_id => $rs_order->requirement_spec_id,
+        version_id          => $rs_order->version_id,
+      )->save;
+    }
+
+    1;
+  });
+}
+
+sub _set_project_in_linked_requirement_spec {
+  my ($self, %params) = @_;
+
+  return if  $params{type} ne 'sales_order';
+  return if !$params{project_id} || !$params{sales_order_id};
+
+  my $query = <<SQL;
+    UPDATE requirement_specs
+    SET project_id = ?
+    WHERE id IN (
+      SELECT so.requirement_spec_id
+      FROM requirement_spec_orders so
+      WHERE so.order_id = ?
+    )
+SQL
+
+  do_query($::form, $::form->get_standard_dbh, $query, $params{project_id}, $params{sales_order_id});
 }
 
 sub save_periodic_invoices_config {
@@ -743,7 +968,7 @@ sub save_periodic_invoices_config {
 
   return if !$params{oe_id};
 
-  my $config = $params{config_yaml} ? YAML::Load($params{config_yaml}) : undef;
+  my $config = $params{config_yaml} ? SL::YAML::Load($params{config_yaml}) : undef;
   return if 'HASH' ne ref $config;
 
   my $obj  = SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $params{oe_id})
@@ -762,8 +987,8 @@ sub load_periodic_invoice_config {
 
     if ($config_obj) {
       my $config = { map { $_ => $config_obj->$_ } qw(active terminated periodicity order_value_periodicity start_date_as_date end_date_as_date first_billing_date_as_date extend_automatically_by ar_chart_id
-                                                      print printer_id copies direct_debit) };
-      $form->{periodic_invoices_config} = YAML::Dump($config);
+                                                      print printer_id copies direct_debit send_email email_recipient_contact_id email_recipient_address email_sender email_subject email_body) };
+      $form->{periodic_invoices_config} = SL::YAML::Dump($config);
     }
   }
 }
@@ -779,37 +1004,38 @@ sub _close_quotations_rfqs {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
+  my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
-  my $query    = qq|SELECT quotation FROM oe WHERE id = ?|;
-  my $sth      = prepare_query($form, $dbh, $query);
+  SL::DB->client->with_transaction(sub {
 
-  do_statement($form, $sth, $query, conv_i($params{to_id}));
+    my $query    = qq|SELECT quotation FROM oe WHERE id = ?|;
+    my $sth      = prepare_query($form, $dbh, $query);
 
-  my ($quotation) = $sth->fetchrow_array();
+    do_statement($form, $sth, $query, conv_i($params{to_id}));
 
-  if ($quotation) {
-    $main::lxdebug->leave_sub();
-    return;
-  }
+    my ($quotation) = $sth->fetchrow_array();
 
-  my @close_ids;
+    if ($quotation) {
+      return 1;
+    }
 
-  foreach my $from_id (@{ $params{from_id} }) {
-    $from_id = conv_i($from_id);
-    do_statement($form, $sth, $query, $from_id);
-    ($quotation) = $sth->fetchrow_array();
-    push @close_ids, $from_id if ($quotation);
-  }
+    my @close_ids;
 
-  $sth->finish();
+    foreach my $from_id (@{ $params{from_id} }) {
+      $from_id = conv_i($from_id);
+      do_statement($form, $sth, $query, $from_id);
+      ($quotation) = $sth->fetchrow_array();
+      push @close_ids, $from_id if ($quotation);
+    }
 
-  if (scalar @close_ids) {
-    $query = qq|UPDATE oe SET closed = TRUE WHERE id IN (| . join(', ', ('?') x scalar @close_ids) . qq|)|;
-    do_query($form, $dbh, $query, @close_ids);
+    $sth->finish();
 
-    $dbh->commit() unless ($params{dbh});
-  }
+    if (scalar @close_ids) {
+      $query = qq|UPDATE oe SET closed = TRUE WHERE id IN (| . join(', ', ('?') x scalar @close_ids) . qq|)|;
+      do_query($form, $dbh, $query, @close_ids);
+    }
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
@@ -828,7 +1054,7 @@ sub delete {
     unlink map { "$spool/$_" } @spoolfiles if $spool;
 
     1;
-  });
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 
@@ -836,12 +1062,20 @@ sub delete {
 }
 
 sub retrieve {
+  my ($self, $myconfig, $form) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_retrieve, $self, $myconfig, $form);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _retrieve {
   my ($self, $myconfig, $form) = @_;
 
   # connect to database
-  my $dbh = $form->get_standard_dbh;
+  my $dbh = SL::DB->client->dbh;
 
   my ($query, $query_add, @values, @ids, $sth);
 
@@ -876,8 +1110,14 @@ sub retrieve {
   $form->{useasnew} = 1 if $is_collective_order == 1;
 
   if (!$form->{id}) {
-    my $extra_days     = $form->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval : 1;
-    $form->{reqdate}   = DateTime->today_local->next_workday(extra_days => $extra_days)->to_kivitendo;
+    my $extra_days = $form->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
+                     $form->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
+    if (   ($form->{type} eq 'sales_order'     &&  !$::instance_conf->get_deliverydate_on)
+        || ($form->{type} eq 'sales_quotation' &&  !$::instance_conf->get_reqdate_on)) {
+      $form->{reqdate}   = '';
+    } else {
+      $form->{reqdate}   = DateTime->today_local->next_workday(extra_days => $extra_days)->to_kivitendo;
+    }
     $form->{transdate} = DateTime->today_local->to_kivitendo;
   }
 
@@ -886,7 +1126,9 @@ sub retrieve {
                      (SELECT c.accno FROM chart c WHERE d.income_accno_id    = c.id) AS income_accno,
                      (SELECT c.accno FROM chart c WHERE d.expense_accno_id   = c.id) AS expense_accno,
                      (SELECT c.accno FROM chart c WHERE d.fxgain_accno_id    = c.id) AS fxgain_accno,
-                     (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id    = c.id) AS fxloss_accno
+                     (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id    = c.id) AS fxloss_accno,
+                     (SELECT c.accno FROM chart c WHERE d.rndgain_accno_id   = c.id) AS rndgain_accno,
+                     (SELECT c.accno FROM chart c WHERE d.rndloss_accno_id   = c.id) AS rndloss_accno
               $query_add
               FROM defaults d|;
   my $ref = selectfirst_hashref_query($form, $dbh, $query);
@@ -913,10 +1155,10 @@ sub retrieve {
            o.taxincluded, o.shippingpoint, o.shipvia, o.notes, o.intnotes,
            (SELECT cu.name FROM currencies cu WHERE cu.id=o.currency_id) AS currency, e.name AS employee, o.employee_id, o.salesman_id,
            o.${vc}_id, cv.name AS ${vc}, o.amount AS invtotal,
-           o.closed, o.reqdate, o.quonumber, o.department_id, o.cusordnumber,
+           o.closed, o.reqdate, o.tax_point, o.quonumber, o.department_id, o.cusordnumber,
            o.mtime, o.itime,
            d.description AS department, o.payment_id, o.language_id, o.taxzone_id,
-           o.delivery_customer_id, o.delivery_vendor_id, o.proforma, o.shipto_id,
+           o.delivery_customer_id, o.delivery_vendor_id, o.proforma, o.shipto_id, o.billing_address_id,
            o.globalproject_id, o.delivered, o.transaction_description, o.delivery_term_id,
            o.itime::DATE AS insertdate, o.order_probability, o.expected_billing_date
          FROM oe o
@@ -967,10 +1209,18 @@ sub retrieve {
       $sth = prepare_execute_query($form, $dbh, $query, $form->{id});
 
       $ref = $sth->fetchrow_hashref("NAME_lc");
-      delete($ref->{id});
-      map { $form->{$_} = $ref->{$_} } keys %$ref;
+      $form->{$_} = $ref->{$_} for grep { m{^shipto(?!_id$)} } keys %$ref;
       $sth->finish;
 
+      if ($ref->{shipto_id}) {
+        my $cvars = CVar->get_custom_variables(
+          dbh      => $dbh,
+          module   => 'ShipTo',
+          trans_id => $ref->{shipto_id},
+        );
+        $form->{"shiptocvar_$_->{name}"} = $_->{value} for @{ $cvars };
+      }
+
       # get printed, emailed and queued
       $query = qq|SELECT s.printed, s.emailed, s.spoolfile, s.formname FROM status s WHERE s.trans_id = ?|;
       $sth = prepare_execute_query($form, $dbh, $query, $form->{id});
@@ -984,9 +1234,10 @@ sub retrieve {
       map { $form->{$_} =~ s/ +$//g } qw(printed emailed queued);
     }    # if !@ids
 
-    my $transdate = $form->{transdate} ? $dbh->quote($form->{transdate}) : "current_date";
+    my $transdate = $form->{tax_point} ? $dbh->quote($form->{tax_point}) : $form->{transdate} ? $dbh->quote($form->{transdate}) : "current_date";
 
     $form->{taxzone_id} = 0 unless ($form->{taxzone_id});
+    unshift @values, ($form->{taxzone_id}) x 2;
 
     # retrieve individual items
     # this query looks up all information about the items
@@ -997,8 +1248,9 @@ sub retrieve {
            c2.accno AS income_accno,    c2.new_chart_id AS income_new_chart,    date($transdate) - c2.valid_from as income_valid,
            c3.accno AS expense_accno,   c3.new_chart_id AS expense_new_chart,   date($transdate) - c3.valid_from as expense_valid,
            oe.ordnumber AS ordnumber_oe, oe.transdate AS transdate_oe, oe.cusordnumber AS cusordnumber_oe,
-           p.partnumber, p.assembly, p.listprice, o.description, o.qty,
-           o.sellprice, o.parts_id AS id, o.unit, o.discount, p.notes AS partnotes, p.inventory_accno_id AS part_inventory_accno_id,
+           p.partnumber, p.part_type, p.listprice, o.description, o.qty,
+           p.classification_id,
+           o.sellprice, o.parts_id AS id, o.unit, o.discount, p.notes AS partnotes, p.part_type,
            o.reqdate, o.project_id, o.serialnumber, o.ship, o.lastcost,
            o.ordnumber, o.transdate, o.cusordnumber, o.subtotal, o.longdescription,
            o.price_factor_id, o.price_factor, o.marge_price_factor, o.active_price_source, o.active_discount_source,
@@ -1008,8 +1260,8 @@ sub retrieve {
          JOIN parts p ON (o.parts_id = p.id)
          JOIN oe ON (o.trans_id = oe.id)
          LEFT JOIN chart c1 ON ((SELECT inventory_accno_id                   FROM buchungsgruppen WHERE id=p.buchungsgruppen_id) = c1.id)
-         LEFT JOIN chart c2 ON ((SELECT tc.income_accno_id FROM taxzone_charts tc WHERE tc.taxzone_id = '$form->{taxzone_id}' and tc.buchungsgruppen_id = p.buchungsgruppen_id) = c2.id)
-         LEFT JOIN chart c3 ON ((SELECT tc.expense_accno_id FROM taxzone_charts tc WHERE tc.taxzone_id = '$form->{taxzone_id}' and tc.buchungsgruppen_id = p.buchungsgruppen_id) = c3.id)
+         LEFT JOIN chart c2 ON ((SELECT tc.income_accno_id  FROM taxzone_charts tc WHERE tc.taxzone_id = ? and tc.buchungsgruppen_id = p.buchungsgruppen_id) = c2.id)
+         LEFT JOIN chart c3 ON ((SELECT tc.expense_accno_id FROM taxzone_charts tc WHERE tc.taxzone_id = ? and tc.buchungsgruppen_id = p.buchungsgruppen_id) = c3.id)
          LEFT JOIN project pr ON (o.project_id = pr.id)
          LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id) | .
       ($form->{id}
@@ -1030,10 +1282,10 @@ sub retrieve {
       map { $ref->{"ic_cvar_$_->{name}"} = $_->{value} } @{ $cvars };
 
       # Handle accounts.
-      if (!$ref->{"part_inventory_accno_id"}) {
+      if (!$ref->{"part_type"} eq 'part') {
         map({ delete($ref->{$_}); } qw(inventory_accno inventory_new_chart inventory_valid));
       }
-      delete($ref->{"part_inventory_accno_id"});
+      delete($ref->{"part_inventory_accno_id"});
 
       # in collective order, copy global ordnumber, transdate, cusordnumber into item scope
       #   unless already present there
@@ -1083,8 +1335,9 @@ sub retrieve {
       # get tax rates and description
       my $accno_id = ($form->{vc} eq "customer") ? $ref->{income_accno} : $ref->{expense_accno};
       $query =
-        qq|SELECT c.accno, t.taxdescription, t.rate, t.taxnumber | .
-        qq|FROM tax t LEFT JOIN chart c on (c.id = t.chart_id) | .
+        qq|SELECT c.accno, t.taxdescription, t.rate, t.id as tax_id, c.accno as taxnumber | .
+        qq|FROM tax t | .
+        qq|LEFT JOIN chart c on (c.id = t.chart_id) | .
         qq|WHERE t.id IN (SELECT tk.tax_id FROM taxkeys tk | .
         qq|               WHERE tk.chart_id = (SELECT id FROM chart WHERE accno = ?) | .
         qq|                 AND startdate <= $transdate ORDER BY startdate DESC LIMIT 1) | .
@@ -1102,6 +1355,7 @@ sub retrieve {
           $form->{"$ptr->{accno}_rate"}        = $ptr->{rate};
           $form->{"$ptr->{accno}_description"} = $ptr->{taxdescription};
           $form->{"$ptr->{accno}_taxnumber"}   = $ptr->{taxnumber};
+          $form->{"$ptr->{accno}_tax_id"}      = $ptr->{tax_id};
           $form->{taxaccounts} .= "$ptr->{accno} ";
         }
 
@@ -1128,11 +1382,7 @@ sub retrieve {
 
   $self->load_periodic_invoice_config($form);
 
-  my $rc = $dbh->commit;
-
-  $main::lxdebug->leave_sub();
-
-  return $rc;
+  return 1;
 }
 
 sub retrieve_simple {
@@ -1165,7 +1415,7 @@ sub order_details {
   my ($self, $myconfig, $form) = @_;
 
   # connect to database
-  my $dbh = $form->get_standard_dbh;
+  my $dbh = SL::DB->client->dbh;
   my $query;
   my @values = ();
   my $sth;
@@ -1189,8 +1439,7 @@ sub order_details {
 
   push(@project_ids, $form->{"globalproject_id"}) if ($form->{"globalproject_id"});
 
-  $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS',
-                   'departments'   => 'ALL_DEPARTMENTS');
+  $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS');
   my %price_factors;
 
   foreach my $pfac (@{ $form->{ALL_PRICE_FACTORS} }) {
@@ -1199,13 +1448,6 @@ sub order_details {
     $pfac->{formatted_factor}    = $form->format_amount($myconfig, $pfac->{factor});
   }
 
-  # lookup department
-  foreach my $dept (@{ $form->{ALL_DEPARTMENTS} }) {
-    next unless $dept->{id} eq $form->{department_id};
-    $form->{department} = $dept->{description};
-    last;
-  }
-
   # sort items by partsgroup
   for $i (1 .. $form->{rowcount}) {
     $partsgroup = "";
@@ -1238,6 +1480,7 @@ sub order_details {
   # so that they can be sorted in later
   my %prepared_template_arrays = IC->prepare_parts_for_printing(myconfig => $myconfig, form => $form);
   my @prepared_arrays          = keys %prepared_template_arrays;
+  my @separate_totals          = qw(non_separate_subtotal);
 
   $form->{TEMPLATE_ARRAYS} = { };
 
@@ -1249,7 +1492,7 @@ sub order_details {
        partnotes serialnumber reqdate sellprice sellprice_nofmt listprice listprice_nofmt netprice netprice_nofmt
        discount discount_nofmt p_discount discount_sub discount_sub_nofmt nodiscount_sub nodiscount_sub_nofmt
        linetotal linetotal_nofmt nodiscount_linetotal nodiscount_linetotal_nofmt tax_rate projectnumber projectdescription
-       price_factor price_factor_name partsgroup weight weight_nofmt lineweight lineweight_nofmt);
+       price_factor price_factor_name partsgroup weight weight_nofmt lineweight lineweight_nofmt optional);
 
   push @arrays, map { "ic_cvar_$_->{name}" } @{ $ic_cvar_configs };
   push @arrays, map { "project_cvar_$_->{name}" } @{ $project_cvar_configs };
@@ -1316,6 +1559,7 @@ sub order_details {
       push @{ $form->{TEMPLATE_ARRAYS}->{price_factor} },      $price_factor->{formatted_factor};
       push @{ $form->{TEMPLATE_ARRAYS}->{price_factor_name} }, $price_factor->{description};
       push @{ $form->{TEMPLATE_ARRAYS}->{partsgroup} },        $form->{"partsgroup_$i"};
+      push @{ $form->{TEMPLATE_ARRAYS}->{optional} },          $form->{"optional_$i"};
 
       my $sellprice     = $form->parse_amount($myconfig, $form->{"sellprice_$i"});
       my ($dec)         = ($sellprice =~ /\.(\d+)/);
@@ -1344,7 +1588,18 @@ sub order_details {
       push @{ $form->{TEMPLATE_ARRAYS}->{discount_nofmt} }, ($discount != 0) ? $discount * -1 : '';
       push @{ $form->{TEMPLATE_ARRAYS}->{p_discount} },     $form->{"discount_$i"};
 
-      $form->{ordtotal}         += $linetotal;
+      if ( $prepared_template_arrays{separate}[$i - 1]  ) {
+        my $pabbr = $prepared_template_arrays{separate}[$i - 1];
+        if ( ! $form->{"separate_${pabbr}_subtotal"} ) {
+            push @separate_totals , "separate_${pabbr}_subtotal";
+            $form->{"separate_${pabbr}_subtotal"} = 0;
+        }
+        $form->{"separate_${pabbr}_subtotal"} += $linetotal;
+      } else {
+        $form->{non_separate_subtotal} += $linetotal;
+      }
+
+      $form->{ordtotal}         += $linetotal unless $form->{"optional_$i"};
       $form->{nodiscount_total} += $nodiscount_linetotal;
       $form->{discount_total}   += $discount;
 
@@ -1392,14 +1647,16 @@ sub order_details {
 
       map { $taxrate += $form->{"${_}_rate"} } split(/ /, $form->{"taxaccounts_$i"});
 
-      if ($form->{taxincluded}) {
+      unless ($form->{"optional_$i"}) {
+        if ($form->{taxincluded}) {
 
-        # calculate tax
-        $taxamount = $linetotal * $taxrate / (1 + $taxrate);
-        $taxbase = $linetotal / (1 + $taxrate);
-      } else {
-        $taxamount = $linetotal * $taxrate;
-        $taxbase   = $linetotal;
+          # calculate tax
+          $taxamount = $linetotal * $taxrate / (1 + $taxrate);
+          $taxbase = $linetotal / (1 + $taxrate);
+        } else {
+          $taxamount = $linetotal * $taxrate;
+          $taxbase   = $linetotal;
+        }
       }
 
       if ($taxamount != 0) {
@@ -1412,15 +1669,15 @@ sub order_details {
       $tax_rate = $taxrate * 100;
       push(@{ $form->{TEMPLATE_ARRAYS}->{tax_rate} }, qq|$tax_rate|);
 
-      if ($form->{"assembly_$i"}) {
+      if ($form->{"part_type_$i"} eq 'assembly') {
         $sameitem = "";
 
         # get parts and push them onto the stack
         my $sortorder = "";
         if ($form->{groupitems}) {
-          $sortorder = qq|ORDER BY pg.partsgroup, a.oid|;
+          $sortorder = qq|ORDER BY pg.partsgroup, a.position|;
         } else {
-          $sortorder = qq|ORDER BY a.oid|;
+          $sortorder = qq|ORDER BY a.position|;
         }
 
         $query = qq|SELECT p.partnumber, p.description, p.unit, a.qty, | .
@@ -1479,10 +1736,13 @@ sub order_details {
     push(@{ $form->{TEMPLATE_ARRAYS}->{taxrate} },        $form->format_amount($myconfig, $form->{"${item}_rate"} * 100));
     push(@{ $form->{TEMPLATE_ARRAYS}->{taxrate_nofmt} },  $form->{"${item}_rate"} * 100);
     push(@{ $form->{TEMPLATE_ARRAYS}->{taxnumber} },      $form->{"${item}_taxnumber"});
+    push(@{ $form->{TEMPLATE_ARRAYS}->{tax_id} },         $form->{"${item}_tax_id"});
 
-    my $tax_obj     = SL::DB::Manager::Tax->find_by(taxnumber => $form->{"${item}_taxnumber"});
-    my $description = $tax_obj ? $tax_obj->translated_attribute('taxdescription',  $form->{language_id}, 0) : '';
-    push(@{ $form->{TEMPLATE_ARRAYS}->{taxdescription} }, $description . q{ } . 100 * $form->{"${item}_rate"} . q{%});
+    if ( $form->{"${item}_tax_id"} ) {
+      my $tax_obj = SL::DB::Manager::Tax->find_by(id => $form->{"${item}_tax_id"}) or die "Can't find tax with id " . $form->{"${item}_tax_id"};
+      my $description = $tax_obj ? $tax_obj->translated_attribute('taxdescription',  $form->{language_id}, 0) : '';
+      push(@{ $form->{TEMPLATE_ARRAYS}->{taxdescription} }, $description . q{ } . 100 * $form->{"${item}_rate"} . q{%});
+    }
   }
 
   $form->{nodiscount_subtotal} = $form->format_amount($myconfig, $form->{nodiscount_total}, 2);
@@ -1498,42 +1758,31 @@ sub order_details {
     $form->{subtotal_nofmt} = $form->{ordtotal};
   }
 
-  $form->{ordtotal} = ($form->{taxincluded}) ? $form->{ordtotal} : $form->{ordtotal} + $tax;
+  my $grossamount = ($form->{taxincluded}) ? $form->{ordtotal} : $form->{ordtotal} + $tax;
+  $form->{ordtotal} = $form->round_amount( $grossamount, 2, 1);
+  $form->{rounding} = $form->round_amount(
+    $form->{ordtotal} - $form->round_amount($grossamount, 2),
+    2
+  );
 
   # format amounts
+  $form->{rounding} = $form->format_amount($myconfig, $form->{rounding}, 2);
   $form->{quototal} = $form->{ordtotal} = $form->format_amount($myconfig, $form->{ordtotal}, 2);
 
-  if ($form->{type} =~ /_quotation/) {
-    $form->set_payment_options($myconfig, $form->{quodate});
-  } else {
-    $form->set_payment_options($myconfig, $form->{orddate});
-  }
+  $form->set_payment_options($myconfig, $form->{$form->{type} =~ /_quotation/ ? 'quodate' : 'orddate'}, $form->{type});
 
   $form->{username} = $myconfig->{name};
 
-  $dbh->disconnect;
-
+  $form->{department}    = SL::DB::Manager::Department->find_by(id => $form->{department_id})->description if $form->{department_id};
   $form->{delivery_term} = SL::DB::Manager::DeliveryTerm->find_by(id => $form->{delivery_term_id} || undef);
   $form->{delivery_term}->description_long($form->{delivery_term}->translated_attribute('description_long', $form->{language_id})) if $form->{delivery_term} && $form->{language_id};
 
   $form->{order} = SL::DB::Manager::Order->find_by(id => $form->{id}) if $form->{id};
+  $form->{$_} = $form->format_amount($myconfig, $form->{$_}, 2) for @separate_totals;
 
   $main::lxdebug->leave_sub();
 }
 
-sub project_description {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $dbh, $id) = @_;
-
-  my $query = qq|SELECT description FROM project WHERE id = ?|;
-  my ($value) = selectrow_query($main::form, $dbh, $query, $id);
-
-  $main::lxdebug->leave_sub();
-
-  return $value;
-}
-
 1;
 
 __END__
diff --git a/SL/PE.pm b/SL/PE.pm
deleted file mode 100644 (file)
index 4d7176d..0000000
--- a/SL/PE.pm
+++ /dev/null
@@ -1,281 +0,0 @@
-#=====================================================================
-# LX-Office ERP
-# Copyright (C) 2004
-# Based on SQL-Ledger Version 2.1.9
-# Web http://www.lx-office.org
-#
-#=====================================================================
-# SQL-Ledger Accounting
-# Copyright (C) 1998-2002
-#
-#  Author: Dieter Simader
-#   Email: dsimader@sql-ledger.org
-#     Web: http://www.sql-ledger.org
-#
-#  Contributors:
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-#======================================================================
-#
-# Partsgroups and pricegroups
-#
-#======================================================================
-
-package PE;
-
-use Data::Dumper;
-
-use SL::DBUtils;
-
-use strict;
-
-sub partsgroups {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-
-  my ($where, @values);
-
-  if ($form->{partsgroup}) {
-    $where .= qq| AND partsgroup ILIKE ?|;
-    push(@values, '%' . $form->{partsgroup} . '%');
-  }
-
-  if ($form->{status} eq 'orphaned') {
-    $where .=
-      qq| AND id NOT IN | .
-      qq|  (SELECT DISTINCT partsgroup_id FROM parts | .
-      qq|   WHERE NOT partsgroup_id ISNULL | .
-      qq| UNION | .
-      qq|   SELECT DISTINCT partsgroup_id FROM custom_variable_config_partsgroups | .
-      qq|   WHERE NOT partsgroup_id ISNULL) |;
-  }
-
-  substr($where, 0, 4) = "WHERE " if ($where);
-
-  my $sortorder = $form->{sort} ? $form->{sort} : "partsgroup";
-  $sortorder =~ s/[^a-z_]//g;
-
-  my $query =
-    qq|SELECT id, partsgroup FROM partsgroup | .
-    $where .
-    qq|ORDER BY $sortorder|;
-
-  $form->{item_list} = selectall_hashref_query($form, $dbh, $query, @values);
-
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-
-  return scalar(@{ $form->{item_list} });
-}
-
-sub save_partsgroup {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-
-  $form->{discount} /= 100;
-
-  my @values = ($form->{partsgroup});
-  my $query;
-
-  if ($form->{id}) {
-    $query = qq|UPDATE partsgroup SET partsgroup = ? WHERE id = ?|;
-    push(@values, $form->{id});
-  } else {
-    $query = qq|INSERT INTO partsgroup (partsgroup) VALUES (?)|;
-  }
-  do_query($form, $dbh, $query, @values);
-
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-}
-
-sub get_partsgroup {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-
-  my $query =
-    qq|SELECT pg.*, | .
-    qq|(SELECT COUNT(*) FROM parts WHERE partsgroup_id = ?) = 0 AS orphaned | .
-    qq|FROM partsgroup pg | .
-    qq|WHERE pg.id = ?|;
-  my $sth = prepare_execute_query($form, $dbh, $query, $form->{id},
-                                  $form->{id});
-  my $ref = $sth->fetchrow_hashref("NAME_lc");
-
-  map({ $form->{$_} = $ref->{$_} } keys(%{$ref}));
-  $sth->finish;
-
-  $dbh->disconnect;
-
-  # also not orphaned if partsgroup is selected for a cvar filter
-  if ($form->{orphaned}) {
-    my $cvar_count = scalar( @{ SL::DB::PartsGroup->new(id => $form->{id})->custom_variable_configs } );
-    $form->{orphaned} = !$cvar_count;
-  }
-
-  $main::lxdebug->leave_sub();
-}
-
-sub delete_tuple {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-
-  my $table = $form->{type} eq "pricegroup" ? "pricegroup" : "partsgroup";
-
-  my $query = qq|DELETE FROM $table WHERE id = ?|;
-  do_query($form, $dbh, $query, $form->{id});
-
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-}
-
-##########################
-# get pricegroups from database
-#
-sub pricegroups {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-
-  my ($where, @values);
-
-  if ($form->{pricegroup}) {
-    $where .= qq| AND pricegroup ILIKE ?|;
-    push(@values, '%' . $form->{pricegroup} . '%');
-  }
-
-  if ($form->{status} eq 'orphaned') {
-    my $first = 1;
-
-    $where .= qq| AND id NOT IN (|;
-    foreach my $table (qw(invoice orderitems prices)) {
-      $where .= "UNION " unless ($first);
-      $first = 0;
-      $where .=
-        qq|SELECT DISTINCT pricegroup_id FROM $table | .
-        qq|WHERE NOT pricegroup_id ISNULL |;
-    }
-    $where .= qq|) |;
-  }
-
-  substr($where, 0, 4) = "WHERE " if ($where);
-
-  my $sortorder = $form->{sort} ? $form->{sort} : "pricegroup";
-  $sortorder =~ s/[^a-z_]//g;
-
-  my $query =
-    qq|SELECT id, pricegroup FROM pricegroup | .
-    $where .
-    qq|ORDER BY $sortorder|;
-
-  $form->{item_list} = selectall_hashref_query($form, $dbh, $query, @values);
-
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-
-  return scalar(@{ $form->{item_list} });
-}
-
-########################
-# save pricegruop to database
-#
-sub save_pricegroup {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-  my $query;
-
-  $form->{discount} /= 100;
-
-  my @values = ($form->{pricegroup});
-
-  if ($form->{id}) {
-    $query = qq|UPDATE pricegroup SET pricegroup = ? WHERE id = ? |;
-    push(@values, $form->{id});
-  } else {
-    $query = qq|INSERT INTO pricegroup (pricegroup) VALUES (?)|;
-  }
-  do_query($form, $dbh, $query, @values);
-
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-}
-
-############################
-# get one pricegroup from database
-#
-sub get_pricegroup {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $form) = @_;
-
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-
-  my $query = qq|SELECT id, pricegroup FROM pricegroup WHERE id = ?|;
-  my $sth = prepare_execute_query($form, $dbh, $query, $form->{id});
-  my $ref = $sth->fetchrow_hashref("NAME_lc");
-
-  map({ $form->{$_} = $ref->{$_} } keys(%{$ref}));
-
-  $sth->finish;
-
-  my $first = 1;
-
-  my @values = ();
-  $query = qq|SELECT |;
-  foreach my $table (qw(invoice orderitems prices)) {
-    $query .= " + " unless ($first);
-    $first = 0;
-    $query .= qq|(SELECT COUNT(*) FROM $table WHERE pricegroup_id = ?) |;
-    push(@values, $form->{id});
-  }
-
-  ($form->{orphaned}) = selectrow_query($form, $dbh, $query, @values);
-  $form->{orphaned} = !$form->{orphaned};
-
-  $dbh->disconnect;
-
-  $main::lxdebug->leave_sub();
-}
-
-1;
-
index deeed1e..44332bd 100644 (file)
@@ -7,23 +7,7 @@ use parent qw(Rose::Object);
 use Carp;
 use Template;
 
-use SL::Presenter::Chart;
-use SL::Presenter::CustomerVendor;
-use SL::Presenter::DeliveryOrder;
-use SL::Presenter::EscapedText;
-use SL::Presenter::Invoice;
-use SL::Presenter::GL;
-use SL::Presenter::Order;
-use SL::Presenter::Part;
-use SL::Presenter::Project;
-use SL::Presenter::Record;
-use SL::Presenter::RequirementSpec;
-use SL::Presenter::RequirementSpecItem;
-use SL::Presenter::RequirementSpecTextBlock;
-use SL::Presenter::SepaExport;
-use SL::Presenter::Text;
-use SL::Presenter::Tag;
-use SL::Presenter::BankAccount;
+use SL::Presenter::EscapedText qw(is_escaped);
 
 use Rose::Object::MakeMethods::Generic (
   scalar => [ qw(need_reinit_widgets) ],
@@ -61,8 +45,10 @@ sub render {
   # Look for the file given by $template if $template is not a reference.
   my $source;
   if (!ref $template) {
+    my $webpages_path = $::request->layout->webpages_path;
+
     my $ext = $options->{type} eq 'text' ? 'txt' : $options->{type};
-    $source = "templates/webpages/${template}.${ext}";
+    $source = "${webpages_path}/${template}.${ext}";
     croak "Template file ${source} not found" unless -f $source;
 
   } elsif (ref($template) eq 'SCALAR') {
@@ -78,15 +64,15 @@ sub render {
   if (!$options->{process}) {
     # If $template is a reference then don't try to read a file.
     my $ref = ref $template;
-    return $template                                                                if $ref eq 'SL::Presenter::EscapedText';
-    return SL::Presenter::EscapedText->new(text => ${ $template }, is_escaped => 1) if $ref eq 'SCALAR';
+    return $template                  if $ref eq 'SL::Presenter::EscapedText';
+    return is_escaped(${ $template }) if $ref eq 'SCALAR';
 
     # Otherwise return the file's content.
     my $file    = IO::File->new($source, "r") || croak("Template file ${source} could not be read");
     my $content = do { local $/ = ''; <$file> };
     $file->close;
 
-    return SL::Presenter::EscapedText->new(text => $content, is_escaped => 1);
+    return is_escaped($content);
   }
 
   # Processing was requested. Set up all variables.
@@ -106,50 +92,32 @@ sub render {
   my $parser = $self->get_template;
   $parser->process($source, \%params, \$output) || croak $parser->error;
 
-  return SL::Presenter::EscapedText->new(text => $output, is_escaped => 1);
+  return is_escaped($output);
 }
 
 sub get_template {
   my ($self) = @_;
 
+  my $webpages_path = $::request->layout->webpages_path;
+
+  # Make locales.pl parse generic/exception.html, too:
+  # $::form->parse_html_template("generic/exception")
   $self->{template} ||=
     Template->new({ INTERPOLATE  => 0,
                     EVAL_PERL    => 0,
                     ABSOLUTE     => 1,
                     CACHE_SIZE   => 0,
                     PLUGIN_BASE  => 'SL::Template::Plugin',
-                    INCLUDE_PATH => '.:templates/webpages',
+                    INCLUDE_PATH => ".:$webpages_path",
                     COMPILE_EXT  => '.tcc',
                     COMPILE_DIR  => $::lx_office_conf{paths}->{userspath} . '/templates-cache',
-                    ERROR        => 'templates/webpages/generic/exception.html',
+                    ERROR        => "${webpages_path}/generic/exception.html",
                     ENCODING     => 'utf8',
                   }) || croak;
 
   return $self->{template};
 }
 
-sub escape {
-  my ($self, $text) = @_;
-
-  return SL::Presenter::EscapedText->new(text => $text);
-}
-
-sub escaped_text {
-  my ($self, $text) = @_;
-
-  return SL::Presenter::EscapedText->new(text => $text, is_escaped => 1);
-}
-
-sub escape_js {
-  my ($self, $text) = @_;
-
-  $text =~ s|\\|\\\\|g;
-  $text =~ s|\"|\\\"|g;
-  $text =~ s|\n|\\n|g;
-
-  return SL::Presenter::EscapedText->new(text => $text, is_escaped => 1);
-}
-
 1;
 
 __END__
@@ -172,14 +140,15 @@ SL::Presenter - presentation layer class
   # Higher-level rendering of certain objects:
   use SL::DB::Customer;
 
-  my $linked_customer_name = $presenter->customer($customer, display => 'table-cell');
+  my $linked_customer_name = $customer->presenter->customer(display => 'table-cell');
 
   # Render a list of links to sales/purchase records:
   use SL::DB::Order;
+  use SL::Presenter::Record qw(grouped_record_list);
 
   my $quotation = SL::DB::Manager::Order->get_first(where => { quotation => 1 });
   my $records   = $quotation->linked_records(direction => 'to');
-  my $html      = $presenter->grouped_record_list($records);
+  my $html      = grouped_record_list($records);
 
 =head1 CLASS FUNCTIONS
 
@@ -305,35 +274,6 @@ it at all:
     { type => 'json', process => 0 }
   );
 
-=item C<escape $text>
-
-Returns an HTML-escaped version of C<$text>. Instead of a string an
-instance of the thin proxy-object L<SL::Presenter::EscapedText> is
-returned.
-
-It is safe to call C<escape> on an instance of
-L<SL::Presenter::EscapedText>. This is a no-op (the same instance will
-be returned).
-
-=item C<escaped_text $text>
-
-Returns an instance of L<SL::Presenter::EscapedText>. C<$text> is
-assumed to be a string that has already been HTML-escaped.
-
-It is safe to call C<escaped_text> on an instance of
-L<SL::Presenter::EscapedText>. This is a no-op (the same instance will
-be returned).
-
-=item C<escape_js $text>
-
-Returns a JavaScript-escaped version of C<$text>. Instead of a string
-an instance of the thin proxy-object L<SL::Presenter::EscapedText> is
-returned.
-
-It is safe to call C<escape> on an instance of
-L<SL::Presenter::EscapedText>. This is a no-op (the same instance will
-be returned).
-
 =item C<get_template>
 
 Returns the global instance of L<Template> and creates it if it
diff --git a/SL/Presenter/ALL.pm b/SL/Presenter/ALL.pm
new file mode 100644 (file)
index 0000000..974b1a6
--- /dev/null
@@ -0,0 +1,76 @@
+package SL::Presenter::ALL;
+
+use strict;
+
+use SL::Presenter::Chart;
+use SL::Presenter::CustomerVendor;
+use SL::Presenter::DeliveryOrder;
+use SL::Presenter::Dunning;
+use SL::Presenter::EscapedText;
+use SL::Presenter::FileObject;
+use SL::Presenter::Invoice;
+use SL::Presenter::GL;
+use SL::Presenter::Letter;
+use SL::Presenter::Order;
+use SL::Presenter::Part;
+use SL::Presenter::Project;
+use SL::Presenter::Record;
+use SL::Presenter::RequirementSpec;
+use SL::Presenter::RequirementSpecItem;
+use SL::Presenter::RequirementSpecTextBlock;
+use SL::Presenter::SepaExport;
+use SL::Presenter::ShopOrder;
+use SL::Presenter::Text;
+use SL::Presenter::Tag;
+use SL::Presenter::BankAccount;
+use SL::Presenter::MaterialComponents;
+
+our %presenters = (
+  chart                       => 'SL::Presenter::Chart',
+  customer_vendor             => 'SL::Presenter::CustomerVendor',
+  delivery_order              => 'SL::Presenter::DeliveryOrder',
+  dunning                     => 'SL::Presenter::Dunning',
+  escaped_text                => 'SL::Presenter::EscapedText',
+  file_object                 => 'SL::Presenter::FileObject',
+  invoice                     => 'SL::Presenter::Invoice',
+  gl                          => 'SL::Presenter::GL',
+  letter                      => 'SL::Presenter::Letter',
+  order                       => 'SL::Presenter::Order',
+  part                        => 'SL::Presenter::Part',
+  project                     => 'SL::Presenter::Project',
+  record                      => 'SL::Presenter::Record',
+  requirement_spec            => 'SL::Presenter::RequirementSpec',
+  requirement_spec_item       => 'SL::Presenter::RequirementSpecItem',
+  requirement_spec_text_block => 'SL::Presenter::RequirementSpecTextBlock',
+  sepa_export                 => 'SL::Presenter::SepaExport',
+  shop_order                  => 'SL::Presenter::ShopOrder',
+  text                        => 'SL::Presenter::Text',
+  tag                         => 'SL::Presenter::Tag',
+  bank_account                => 'SL::Presenter::BankAccount',
+  M                           => 'SL::Presenter::MaterialComponents',
+);
+
+sub wrap {
+  bless [ $_[0] ], 'SL::Presenter::ALL::Wrapper';
+}
+
+package SL::Presenter::ALL::Wrapper;
+
+sub AUTOLOAD {
+  our $AUTOLOAD;
+
+  my ($self, @args) = @_;
+
+  my $method = $AUTOLOAD;
+  $method    =~ s/.*:://;
+
+  return if $method eq 'DESTROY';
+
+  splice @args, -1, 1, %{ $args[-1] } if @args && (ref($args[-1]) eq 'HASH');
+
+  if (my $sub = $self->[0]->can($method)) {
+    return $sub->(@args);
+  }
+}
+
+1;
index 13a8cb2..925dd97 100644 (file)
@@ -2,21 +2,21 @@ package SL::Presenter::BankAccount;
 
 use strict;
 
-use parent qw(Exporter);
+use SL::Presenter::EscapedText qw(escape);
 
 use Exporter qw(import);
-our @EXPORT = qw(account_number bank_code);
+our @EXPORT_OK = qw(account_number bank_code);
 
 use Carp;
 
 sub account_number {
-  my ($self, $bank_account) = @_;
-  return $self->escaped_text($bank_account->account_number);
+  my ($bank_account) = @_;
+  escape($bank_account->account_number);
 }
 
 sub bank_code {
-  my ($self, $bank_account) = @_;
-  return $self->escaped_text($bank_account->bank_code);
+  my ($bank_account) = @_;
+  escape($bank_account->bank_code);
 }
 
 1;
index 025fa80..3a4f483 100644 (file)
@@ -5,31 +5,33 @@ use strict;
 use SL::DB::Chart;
 
 use Exporter qw(import);
-use Data::Dumper;
-our @EXPORT = qw(chart_picker chart);
+our @EXPORT_OK = qw(chart_picker chart);
 
 use Carp;
+use Data::Dumper;
+use SL::Presenter::EscapedText qw(escape is_escaped);
+use SL::Presenter::Tag qw(input_tag name_to_id html_tag);
 
 sub chart {
-  my ($self, $chart, %params) = @_;
+  my ($chart, %params) = @_;
 
   $params{display} ||= 'inline';
 
   croak "Unknown display type '$params{display}'" unless $params{display} =~ m/^(?:inline|table-cell)$/;
 
   my $text = join '', (
-    $params{no_link} ? '' : '<a href="am.pl?action=edit_account&id=' . $self->escape($chart->id) . '">',
-    $self->escape($chart->accno),
+    $params{no_link} ? '' : '<a href="am.pl?action=edit_account&id=' . escape($chart->id) . '">',
+    escape($chart->accno),
     $params{no_link} ? '' : '</a>',
   );
-  return $self->escaped_text($text);
+  is_escaped($text);
 }
 
 sub chart_picker {
-  my ($self, $name, $value, %params) = @_;
+  my ($name, $value, %params) = @_;
 
   $value = SL::DB::Manager::Chart->find_by(id => $value) if $value && !ref $value;
-  my $id = delete($params{id}) || $self->name_to_id($name);
+  my $id = delete($params{id}) || name_to_id($name);
   my $fat_set_item = delete $params{fat_set_item};
 
   my @classes = $params{class} ? ($params{class}) : ();
@@ -37,16 +39,18 @@ sub chart_picker {
   push @classes, 'chartpicker_fat_set_item' if $fat_set_item;
 
   my $ret =
-    $self->input_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => "@classes", type => 'hidden', id => $id) .
-    join('', map { $params{$_} ? $self->input_tag("", delete $params{$_}, id => "${id}_${_}", type => 'hidden') : '' } qw(type category choose booked)) .
-    $self->input_tag("", (ref $value && $value->can('displayable_name')) ? $value->displayable_name : '', id => "${id}_name", %params);
+    input_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => "@classes", type => 'hidden', id => $id) .
+    join('', map { $params{$_} ? input_tag("", delete $params{$_}, id => "${id}_${_}", type => 'hidden') : '' } qw(type category choose booked)) .
+    input_tag("", (ref $value && $value->can('displayable_name')) ? $value->displayable_name : '', id => "${id}_name", %params);
 
   $::request->layout->add_javascripts('autocomplete_chart.js');
   $::request->presenter->need_reinit_widgets($id);
 
-  $self->html_tag('span', $ret, class => 'chart_picker');
+  html_tag('span', $ret, class => 'chart_picker');
 }
 
+sub picker { goto &chart_picker }
+
 1;
 
 __END__
@@ -61,7 +65,7 @@ SL::Presenter::Chart - Chart related presenter stuff
 
   # Create an html link for editing/opening a chart
   my $object = SL::DB::Manager::Chart->get_first;
-  my $html   = SL::Presenter->get->chart($object, display => 'inline');
+  my $html   = SL::Presenter::Chart::chart($object, display => 'inline');
 
 see also L<SL::Presenter>
 
@@ -159,7 +163,7 @@ C<chart_picker> will register its javascript for inclusion in the next header
 rendering. If you write a standard controller that only calls C<render> once, it
 will just work.  In case the header is generated in a different render call
 (multiple blocks, ajax, old C<bin/mozilla> style controllers) you need to
-include C<js/autocomplete_part.js> yourself.
+include C<js/autocomplete_chart.js> yourself.
 
 =back
 
index 5f41a2a..ab95172 100644 (file)
@@ -2,25 +2,31 @@ package SL::Presenter::CustomerVendor;
 
 use strict;
 
-use parent qw(Exporter);
+use SL::Presenter::EscapedText qw(escape is_escaped);
+use SL::Presenter::Tag qw(input_tag html_tag name_to_id select_tag);
 
 use Exporter qw(import);
-our @EXPORT = qw(customer vendor customer_vendor_picker);
+our @EXPORT_OK = qw(customer_vendor customer vendor customer_vendor_picker customer_picker vendor_picker);
 
 use Carp;
 
+sub customer_vendor {
+  my ($customer_vendor, %params) = @_;
+  return _customer_vendor($customer_vendor, ref($customer_vendor) eq 'SL::DB::Customer' ? 'customer' : 'vendor', %params);
+}
+
 sub customer {
-  my ($self, $customer, %params) = @_;
-  return _customer_vendor($self, $customer, 'customer', %params);
+  my ($customer, %params) = @_;
+  return _customer_vendor($customer, 'customer', %params);
 }
 
 sub vendor {
-  my ($self, $vendor, %params) = @_;
-  return _customer_vendor($self, $vendor, 'vendor', %params);
+  my ($vendor, %params) = @_;
+  return _customer_vendor($vendor, 'vendor', %params);
 }
 
 sub _customer_vendor {
-  my ($self, $cv, $type, %params) = @_;
+  my ($cv, $type, %params) = @_;
 
   $params{display} ||= 'inline';
 
@@ -29,15 +35,19 @@ sub _customer_vendor {
   my $callback = $params{callback} ? '&callback=' . $::form->escape($params{callback}) : '';
 
   my $text = join '', (
-    $params{no_link} ? '' : '<a href="controller.pl?action=CustomerVendor/edit&amp;db=' . $type . '&amp;id=' . $self->escape($cv->id) . '">',
-    $self->escape($cv->name),
+    $params{no_link} ? '' : '<a href="controller.pl?action=CustomerVendor/edit&amp;db=' . $type . '&amp;id=' . escape($cv->id) . '">',
+    escape($cv->name),
     $params{no_link} ? '' : '</a>',
   );
-  return $self->escaped_text($text);
+
+  is_escaped($text);
 }
 
 sub customer_vendor_picker {
-  my ($self, $name, $value, %params) = @_;
+  my ($name, $value, %params) = @_;
+
+  $params{type} //= 'customer' if 'SL::DB::Customer' eq ref $value;
+  $params{type} //= 'vendor'   if 'SL::DB::Vendor'   eq ref $value;
 
   croak 'Unknown "type" parameter' unless $params{type} =~ m{^(?:customer|vendor)$};
   croak 'Unknown value class'      if     $value && ref($value) && (ref($value) !~ m{^SL::DB::(?:Customer|Vendor)$});
@@ -47,24 +57,30 @@ sub customer_vendor_picker {
     $value    = $class->find_by(id => $value);
   }
 
-  my $id = delete($params{id}) || $self->name_to_id($name);
-  my $fat_set_item = delete $params{fat_set_item};
+  my $id = delete($params{id}) || name_to_id($name);
 
   my @classes = $params{class} ? ($params{class}) : ();
   push @classes, 'customer_vendor_autocomplete';
-  push @classes, 'customer-vendor-picker-fat-set-item' if $fat_set_item;
+
+  # do not use reserved html attribute 'type' for cv type
+  $params{cv_type} = delete $params{type};
 
   my $ret =
-    $self->input_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => "@classes", type => 'hidden', id => $id) .
-    join('', map { $params{$_} ? $self->input_tag("", delete $params{$_}, id => "${id}_${_}", type => 'hidden') : '' } qw(type)) .
-    $self->input_tag("", ref $value  ? $value->displayable_name : '', id => "${id}_name", %params);
+    input_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => "@classes", type => 'hidden', id => $id,
+      'data-customer-vendor-picker-data' => JSON::to_json(\%params),
+    ) .
+    input_tag("", ref $value  ? $value->displayable_name : '', id => "${id}_name", %params);
 
-  $::request->layout->add_javascripts('autocomplete_customer.js');
+  $::request->layout->add_javascripts('kivi.CustomerVendor.js');
   $::request->presenter->need_reinit_widgets($id);
 
-  $self->html_tag('span', $ret, class => 'customer_vendor_picker');
+  html_tag('span', $ret, class => 'customer_vendor_picker');
 }
 
+sub customer_picker { my ($name, $value, @slurp) = @_; customer_vendor_picker($name, $value, @slurp, type => 'customer') }
+sub vendor_picker   { my ($name, $value, @slurp) = @_; customer_vendor_picker($name, $value, @slurp, type => 'vendor') }
+sub picker          { goto &customer_vendor_picker }
+
 1;
 
 __END__
@@ -82,11 +98,11 @@ vendor Rose::DB objects
 
   # Customers:
   my $customer = SL::DB::Manager::Customer->get_first;
-  my $html     = SL::Presenter->get->customer($customer, display => 'inline');
+  my $html     = SL::Presenter::CustomerVendor::customer($customer, display => 'inline');
 
   # Vendors:
   my $vendor = SL::DB::Manager::Vendor->get_first;
-  my $html   = SL::Presenter->get->vendor($customer, display => 'inline');
+  my $html   = SL::Presenter::Customer::Vendor::vendor($customer, display => 'inline');
 
 =head1 FUNCTIONS
 
@@ -136,6 +152,14 @@ the "edit vendor" dialog from the master data menu.
 
 =back
 
+=item C<customer_vendor $object, %params>
+
+Returns a rendered version (actually an instance of
+L<SL::Presenter::EscapedText>) of the customer or vendor object
+C<$object> by calling either L</customer> or L</vendor> depending on
+C<$object>'s type. See the respective functions for available
+parameters.
+
 =back
 
 =head1 BUGS
index 1abc835..9f525b3 100644 (file)
@@ -2,40 +2,99 @@ package SL::Presenter::DeliveryOrder;
 
 use strict;
 
-use parent qw(Exporter);
+use SL::DB::DeliveryOrder::TypeData ();
+use SL::Locale::String qw(t8);
+use SL::Presenter::EscapedText qw(escape is_escaped);
 
 use Exporter qw(import);
-our @EXPORT = qw(sales_delivery_order purchase_delivery_order);
+our @EXPORT_OK = qw(sales_delivery_order purchase_delivery_order delivery_order_status_line);
 
 use Carp;
 
 sub sales_delivery_order {
-  my ($self, $delivery_order, %params) = @_;
+  my ($delivery_order, %params) = @_;
 
-  return _do_record($self, $delivery_order, 'sales_delivery_order', %params);
+  return _do_record($delivery_order, 'sales_delivery_order', %params);
+}
+
+sub rma_delivery_order {
+  my ($delivery_order, %params) = @_;
+
+  return _do_new_record($delivery_order, 'rma_delivery_order', %params);
 }
 
 sub purchase_delivery_order {
-  my ($self, $delivery_order, %params) = @_;
+  my ($delivery_order, %params) = @_;
+
+  return _do_record($delivery_order, 'purchase_delivery_order', %params);
+}
+
+sub supplier_delivery_order {
+  my ($delivery_order, %params) = @_;
 
-  return _do_record($self, $delivery_order, 'purchase_delivery_order', %params);
+  return _do_new_record($delivery_order, 'supplier_delivery_order', %params);
+}
+
+sub _do_new_record {
+  my ($delivery_order, $type, %params) = @_;
+
+  $params{display} ||= 'inline';
+
+  croak "Unknown display type '$params{display}'" unless $params{display} =~ m/^(?:inline|table-cell)$/;
+
+  my $text = join '', (
+    $params{no_link} ? '' : '<a href="contoller.pl?action=DeliveryOrder/edit&amp;type=' . $type . '&amp;id=' . escape($delivery_order->id) . '">',
+    escape($delivery_order->donumber),
+    $params{no_link} ? '' : '</a>',
+  );
+  is_escaped($text);
 }
 
 sub _do_record {
-  my ($self, $delivery_order, $type, %params) = @_;
+  my ($delivery_order, $type, %params) = @_;
 
   $params{display} ||= 'inline';
 
   croak "Unknown display type '$params{display}'" unless $params{display} =~ m/^(?:inline|table-cell)$/;
 
   my $text = join '', (
-    $params{no_link} ? '' : '<a href="do.pl?action=edit&amp;type=' . $type . '&amp;id=' . $self->escape($delivery_order->id) . '">',
-    $self->escape($delivery_order->donumber),
+    $params{no_link} ? '' : '<a href="do.pl?action=edit&amp;type=' . $type . '&amp;id=' . escape($delivery_order->id) . '">',
+    escape($delivery_order->donumber),
     $params{no_link} ? '' : '</a>',
   );
-  return $self->escaped_text($text);
+  is_escaped($text);
+}
+
+sub stock_status {
+  my ($delivery_order) = @_;
+
+  my $in_out = SL::DB::DeliveryOrder::TypeData::get3($delivery_order->type, "properties", "transfer");
+
+  if ($in_out eq 'in') {
+    return escape($delivery_order->delivered ? t8('transferred in') : t8('not transferred in yet'));
+  }
+
+  if ($in_out eq 'out') {
+    return escape($delivery_order->delivered ? t8('transferred out') : t8('not transferred out yet'));
+  }
+}
+
+sub closed_status {
+  my ($delivery_order) = @_;
+
+  return escape($delivery_order->closed ? t8('Closed') : t8('Open'))
 }
 
+sub status_line {
+  my ($delivery_order) = @_;
+
+  return "" unless $delivery_order->id;
+
+  stock_status($delivery_order) . " ; " . closed_status($delivery_order)
+}
+
+sub delivery_order_status_line { goto &status_line };
+
 1;
 
 __END__
@@ -53,11 +112,11 @@ for sales and purchase delivery orders
 
   # Sales delivery orders:
   my $object = SL::DB::Manager::DeliveryOrder->get_first(where => [ is_sales => 1 ]);
-  my $html   = SL::Presenter->get->sales_delivery_order($object, display => 'inline');
+  my $html   = SL::Presenter::DeliveryOrder::sales_delivery_order($object, display => 'inline');
 
   # Purchase delivery orders:
   my $object = SL::DB::Manager::DeliveryOrder->get_first(where => [ or => [ is_sales => undef, is_sales => 0 ]]);
-  my $html   = SL::Presenter->get->purchase_delivery_order($object, display => 'inline');
+  my $html   = SL::Presenter::DeliveryOrder::purchase_delivery_order($object, display => 'inline');
 
 =head1 FUNCTIONS
 
diff --git a/SL/Presenter/Dunning.pm b/SL/Presenter/Dunning.pm
new file mode 100644 (file)
index 0000000..de49614
--- /dev/null
@@ -0,0 +1,37 @@
+package SL::Presenter::Dunning;
+
+use strict;
+
+use SL::Presenter::EscapedText qw(escape is_escaped);
+use SL::Presenter::Tag         qw(link_tag);
+
+use Exporter qw(import);
+our @EXPORT_OK = qw(dunning);
+
+use Carp;
+
+sub dunning {
+  my ($dunning, %params) = @_;
+
+  $params{display} ||= 'inline';
+
+  croak "Unknown display type '$params{display}'" unless $params{display} =~ m/^(?:inline|table-cell)$/;
+
+  my $text = escape($dunning->dunning_config->dunning_description);
+
+  if (! delete $params{no_link}) {
+    my @flags;
+    push @flags, 'showold=1';
+    push @flags, 'l_mails=1'      if $::instance_conf->get_email_journal;
+    push @flags, 'l_webdav=1'     if $::instance_conf->get_webdav;
+    push @flags, 'l_documents=1'  if $::instance_conf->get_doc_storage;
+
+    my $href  = 'dn.pl?action=show_dunning&dunning_id=' . $dunning->dunning_id;
+    $href    .= '&' . join '&', @flags if @flags;
+    $text     = link_tag($href, $text, %params);
+  }
+
+  is_escaped($text);
+}
+
+1;
diff --git a/SL/Presenter/EmailJournal.pm b/SL/Presenter/EmailJournal.pm
new file mode 100644 (file)
index 0000000..5bdfcd8
--- /dev/null
@@ -0,0 +1,89 @@
+package SL::Presenter::EmailJournal;
+
+use strict;
+
+use SL::Presenter::EscapedText qw(escape is_escaped);
+
+use Exporter qw(import);
+our @EXPORT_OK = qw(email_journal);
+
+use Carp;
+
+sub email_journal {
+  my ($email_journal_entry, %params) = @_;
+
+  $params{display} ||= 'inline';
+
+  croak "Unknown display type '$params{display}'" unless $params{display} =~ m/^(?:inline|table-cell)$/;
+
+  my $text = join '', (
+    $params{no_link} ? '' : '<a href="controller.pl?action=EmailJournal/show&amp;id=' . escape($email_journal_entry->id) . '">',
+    escape($email_journal_entry->subject),
+    $params{no_link} ? '' : '</a>',
+  );
+
+  is_escaped($text);
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Presenter::EmailJournal - Presenter module for mail entries in email_journal
+
+=head1 SYNOPSIS
+
+  use SL::Presenter::EmailJournal;
+
+  my $journal_entry = SL::DB::Manager::EmailJournal->get_first();
+  my $html   = SL::Presenter::EmailJournal::email_journal($journal_entry, display => 'inline');
+
+  # pp $html
+  # <a href="controller.pl?action=EmailJournal/show&amp;id=1">IDEV Daten fuer webdav/idev/2017-KW-26.csv erzeugt</a>
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<email_journal $object, %params>
+
+Returns a rendered version (actually an instance of
+L<SL::Presenter::EscapedText>) of the email journal object C<$object>
+.
+
+
+C<%params> can include:
+
+=over 2
+
+=item * display
+
+Either C<inline> (the default) or C<table-cell>. At the moment both
+representations are identical and produce the invoice number linked
+to the corresponding 'edit' action.
+
+=item * no_link
+
+If falsish (the default) then the mail subject will be linked to the
+'view details of email' dialog from the email journal report.
+
+=back
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+copied from Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+by Jan Büren E<lt>jan@kivitendo-premium.deE<gt>
+
+=cut
index c8c78a0..1c92f6f 100644 (file)
@@ -1,29 +1,82 @@
 package SL::Presenter::EscapedText;
 
 use strict;
+use Exporter qw(import);
+use Scalar::Util qw(looks_like_number);
+
+our @EXPORT_OK = qw(escape is_escaped escape_js escape_js_call);
+our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
 
 use JSON ();
 
-use overload '""' => \&escaped;
+use overload '""' => \&escaped_text;
+
+my %html_entities = (
+  '<' => '&lt;',
+  '>' => '&gt;',
+  '&' => '&amp;',
+  '"' => '&quot;',
+  "'" => '&apos;',
+);
 
+# static constructors
 sub new {
   my ($class, %params) = @_;
 
   return $params{text} if ref($params{text}) eq $class;
 
   my $self      = bless {}, $class;
-  $self->{text} = $params{is_escaped} ? $params{text} : $::locale->quote_special_chars('HTML', $params{text});
+  $self->{text} = $params{is_escaped} ? $params{text} : quote_html($params{text});
 
   return $self;
 }
 
-sub escaped {
+sub quote_html {
+  return undef unless defined $_[0];
+  (my $x = $_[0]) =~ s/(["'<>&])/$html_entities{$1}/ge;
+  $x
+}
+
+sub escape {
+  __PACKAGE__->new(text => $_[0]);
+}
+
+sub is_escaped {
+  __PACKAGE__->new(text => $_[0], is_escaped => 1);
+}
+
+sub escape_js {
+  my ($text) = @_;
+
+  $text =~ s|\\|\\\\|g;
+  $text =~ s|\"|\\\"|g;
+  $text =~ s|\n|\\n|g;
+
+  __PACKAGE__->new(text => $text, is_escaped => 1);
+}
+
+sub escape_js_call {
+  my ($func, @args) = @_;
+
+  escape(
+      sprintf "%s(%s)",
+      escape_js($func),
+      join ", ", map {
+        looks_like_number($_)
+          ? $_
+          : '"' . escape_js($_) . '"'
+      } @args
+  );
+}
+
+# internal magic
+sub escaped_text {
   my ($self) = @_;
   return $self->{text};
 }
 
 sub TO_JSON {
-  goto &escaped;
+  goto &escaped_text;
 }
 
 1;
@@ -35,15 +88,18 @@ __END__
 
 =head1 NAME
 
-SL::Presenter::EscapedText - Thin proxy object around HTML-escaped strings
+SL::Presenter::EscapedText - Thin proxy object to invert the burden of escaping HTML output
 
 =head1 SYNOPSIS
 
-  use SL::Presenter::EscapedText;
+  use SL::Presenter::EscapedText qw(escape is_escaped escape_js);
 
   sub blackbox {
     my ($text) = @_;
     return SL::Presenter::EscapedText->new(text => $text);
+
+    # or shorter:
+    # return escape($text);
   }
 
   sub build_output {
@@ -67,7 +123,7 @@ But higher functions should not have to care if the output is already
 escaped -- they should be able to simply escape it again. Without
 producing stuff like '&amp;amp;'.
 
-Stringification is overloaded. It will return the same as L<escaped>.
+Stringification is overloaded. It will return the same as L<escaped_text>.
 
 This works together with the template plugin
 L<SL::Template::Plugin::P> and its C<escape> method.
@@ -88,7 +144,37 @@ Otherwise C<text> is HTML-escaped and stored in the new instance. This
 can be overridden by setting C<$params{is_escaped}> to a trueish
 value.
 
-=item C<escaped>
+=item C<escape $text>
+
+Static constructor, can be exported. Equivalent to calling C<< new(text => $text) >>.
+
+=item C<is_escaped $text>
+
+Static constructor, can be exported. Equivalent to calling C<< new(text => $text, escaped => 1) >>.
+
+=item C<escape_js $text>
+
+Static constructor, can be exported. Like C<escape> but also escapes Javascript.
+
+=item C<escape_js_call $func_name, @args>
+
+Static constructor, can be exported. Used to construct a javascript call than
+can be used for onclick handlers in other Presenter functions.
+
+For example:
+
+  L.button_tag(
+    P.escape_js_call("kivi.Package.some_func", arg_one, arg_two, arg_three)
+    title
+  )
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item C<escaped_text>
 
 Returns the escaped string (not an instance of C<EscapedText> but an
 actual string).
diff --git a/SL/Presenter/FileObject.pm b/SL/Presenter/FileObject.pm
new file mode 100644 (file)
index 0000000..a339f94
--- /dev/null
@@ -0,0 +1,78 @@
+package SL::Presenter::FileObject;
+
+use strict;
+
+use SL::Presenter::Tag         qw(link_tag);
+use SL::Presenter::EscapedText qw(escape is_escaped);
+
+use Exporter qw(import);
+our @EXPORT_OK = qw(file_object);
+
+use Carp;
+
+sub file_object {
+  my ($file_object, %params) = @_;
+
+
+  my $text = escape($file_object->file_name);
+  if (! delete $params{no_link}) {
+    my $href  = 'controller.pl?action=File/download&id=' . $file_object->id;
+    $href    .= '&version=' . $file_object->version if $file_object->version;
+    $text     = link_tag($href, $text, %params);
+  }
+
+  is_escaped($text);
+}
+
+1;
+
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Presenter::FileObject - Presenter module for SL::File::Object(s), the
+file objects of the filemanagement. (Note, that this are not instances of
+SL::DB::File)
+
+=head1 SYNOPSIS
+
+  my $file_object = SL::File->get(id => 1);
+  my $html        = SL::Presenter::FileObject::file_object($file_object, no_link => 1);
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<file_object $file_object, %params>
+
+Returns a rendered version (actually an instance of
+L<SL::Presenter::EscapedText>) of the file object
+C<$file_object>.
+
+C<%params> can include:
+
+=over 2
+
+=item * no_link
+
+If falsish (the default) then the file name of the object will be linked
+to the "download action" for that file.
+
+=back
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
index d1c60ea..cf0998b 100644 (file)
@@ -2,26 +2,27 @@ package SL::Presenter::GL;
 
 use strict;
 
-use parent qw(Exporter);
+use SL::Presenter::EscapedText qw(escape is_escaped);
 
 use Exporter qw(import);
-our @EXPORT = qw(gl_transaction);
+our @EXPORT_OK = qw(gl_transaction);
 
 use Carp;
 
 sub gl_transaction {
-  my ($self, $gl_transaction, %params) = @_;
+  my ($gl_transaction, %params) = @_;
 
   $params{display} ||= 'inline';
 
   croak "Unknown display type '$params{display}'" unless $params{display} =~ m/^(?:inline|table-cell)$/;
 
   my $text = join '', (
-    $params{no_link} ? '' : '<a href="gl.pl?action=edit&amp;id=' . $self->escape($gl_transaction->id) . '">',
-    $self->escape($gl_transaction->id),
+    $params{no_link} ? '' : '<a href="gl.pl?action=edit&amp;id=' . escape($gl_transaction->id) . '">',
+    escape($gl_transaction->reference),
     $params{no_link} ? '' : '</a>',
   );
-  return $self->escaped_text($text);
+
+  is_escaped($text);
 }
 
 1;
@@ -39,7 +40,7 @@ SL::Presenter::GL - Presenter module for GL transaction
 =head1 SYNOPSIS
 
   my $object = SL::DB::Manager::GLTransaction->get_first();
-  my $html   = SL::Presenter->get->gl_transaction($object, display => 'inline');
+  my $html   = SL::Presenter::GL::gl_transaction($object, display => 'inline');
 
 =head1 FUNCTIONS
 
index ebde34f..ba662ed 100644 (file)
@@ -2,68 +2,69 @@ package SL::Presenter::Invoice;
 
 use strict;
 
-use parent qw(Exporter);
+use SL::Presenter::EscapedText qw(escape is_escaped);
 
 use Exporter qw(import);
-our @EXPORT = qw(invoice sales_invoice ar_transaction purchase_invoice ap_transaction);
+our @EXPORT_OK = qw(invoice sales_invoice ar_transaction purchase_invoice ap_transaction);
 
 use Carp;
 
 sub invoice {
-  my ($self, $invoice, %params) = @_;
+  my ($invoice, %params) = @_;
 
   if ( $invoice->is_sales ) {
     if ( $invoice->invoice ) {
-      return _is_ir_record($self, $invoice, 'is', %params);
+      return _is_ir_record($invoice, 'is', %params);
     } else {
-      return _is_ir_record($self, $invoice, 'ar', %params);
+      return _is_ir_record($invoice, 'ar', %params);
     }
   } else {
     if ( $invoice->invoice ) {
-      return _is_ir_record($self, $invoice, 'ir', %params);
+      return _is_ir_record($invoice, 'ir', %params);
     } else {
-      return _is_ir_record($self, $invoice, 'ap', %params);
+      return _is_ir_record($invoice, 'ap', %params);
     }
   };
 };
 
 sub sales_invoice {
-  my ($self, $invoice, %params) = @_;
+  my ($invoice, %params) = @_;
 
-  return _is_ir_record($self, $invoice, 'is', %params);
+  _is_ir_record($invoice, 'is', %params);
 }
 
 sub ar_transaction {
-  my ($self, $invoice, %params) = @_;
+  my ($invoice, %params) = @_;
 
-  return _is_ir_record($self, $invoice, 'ar', %params);
+  _is_ir_record($invoice, 'ar', %params);
 }
 
 sub purchase_invoice {
-  my ($self, $invoice, %params) = @_;
+  my ($invoice, %params) = @_;
 
-  return _is_ir_record($self, $invoice, 'ir', %params);
+  _is_ir_record($invoice, 'ir', %params);
 }
 
 sub ap_transaction {
-  my ($self, $invoice, %params) = @_;
+  my ($invoice, %params) = @_;
 
-  return _is_ir_record($self, $invoice, 'ap', %params);
+  _is_ir_record($invoice, 'ap', %params);
 }
 
 sub _is_ir_record {
-  my ($self, $invoice, $controller, %params) = @_;
+  my ($invoice, $controller, %params) = @_;
 
   $params{display} ||= 'inline';
 
   croak "Unknown display type '$params{display}'" unless $params{display} =~ m/^(?:inline|table-cell)$/;
 
   my $text = join '', (
-    $params{no_link} ? '' : '<a href="' . $controller . '.pl?action=edit&amp;type=invoice&amp;id=' . $self->escape($invoice->id) . '">',
-    $self->escape($invoice->invnumber),
+    $params{no_link} ? '' : '<a href="' . $controller . '.pl?action=edit&amp;type=invoice&amp;id=' . escape($invoice->id) . '">',
+    escape($invoice->invnumber),
     $params{no_link} ? '' : '</a>',
   );
-  return $self->escaped_text($text);
+
+  is_escaped($text);
 }
 
 1;
@@ -83,22 +84,22 @@ transaction, purchase invoice and AP transaction Rose::DB objects
 
   # Sales invoices:
   my $object = SL::DB::Manager::Invoice->get_first(where => [ invoice => 1 ]);
-  my $html   = SL::Presenter->get->sales_invoice($object, display => 'inline');
+  my $html   = SL::Presenter::Invoice::sales_invoice($object, display => 'inline');
 
   # AR transactions:
   my $object = SL::DB::Manager::Invoice->get_first(where => [ or => [ invoice => undef, invoice => 0 ]]);
-  my $html   = SL::Presenter->get->ar_transaction($object, display => 'inline');
+  my $html   = SL::Presenter::Invoice::ar_transaction($object, display => 'inline');
 
   # Purchase invoices:
   my $object = SL::DB::Manager::PurchaseInvoice->get_first(where => [ invoice => 1 ]);
-  my $html   = SL::Presenter->get->purchase_invoice($object, display => 'inline');
+  my $html   = SL::Presenter::Invoice::purchase_invoice($object, display => 'inline');
 
   # AP transactions:
   my $object = SL::DB::Manager::PurchaseInvoice->get_first(where => [ or => [ invoice => undef, invoice => 0 ]]);
-  my $html   = SL::Presenter->get->ar_transaction($object, display => 'inline');
+  my $html   = SL::Presenter::Invoice::ar_transaction($object, display => 'inline');
 
   # use with any of the above ar/ap/is/ir types:
-  my $html   = SL::Presenter->get->invoice($object, display => 'inline');
+  my $html   = SL::Presenter::Invoice::invoice($object, display => 'inline');
 
 =head1 FUNCTIONS
 
diff --git a/SL/Presenter/JavascriptMenu.pm b/SL/Presenter/JavascriptMenu.pm
new file mode 100644 (file)
index 0000000..24999b2
--- /dev/null
@@ -0,0 +1,70 @@
+package SL::Presenter::JavascriptMenu;
+
+use strict;
+use SL::Presenter::EscapedText qw(escape is_escaped);
+use SL::Presenter::Tag qw( html_tag link_tag);
+use SL::Locale::String qw(t8);
+use SL::System::ResourceCache;
+
+use List::Util qw(max);
+
+use Exporter qw(import);
+our @EXPORT_OK = qw(render_menu);
+
+sub render_menu {
+  my ($menu) = @_;
+
+  html_tag('div', '', id => 'main_menu_div') .
+  html_tag('ul', render_children($menu, 100, $menu->{tree}),
+    id    => "main_menu_model",
+    style => 'display:none',
+  );
+}
+
+sub render_node {
+  my ($menu, $node, $id) = @_;
+  return '' if !$node->{visible};
+
+  my $icon = get_icon($node->{icon});
+  my $link = $menu->href_for_node($node) || '#';
+  my $name = $menu->name_for_node($node);
+
+  html_tag('li',
+      link_tag($link, $name, target => $node->{target})
+    . html_tag('ul', render_children($menu, $id * 100, $node->{children} // []),
+        width => max_width($node)
+      ),
+    id        => $id,
+    (itemIcon => $icon)x!!$icon,
+  )
+}
+
+sub render_children {
+  my ($menu, $id, $children) = @_;
+  my $sub_id = 1;
+
+  join '', map {
+    render_node($menu, $_, 100 * $id + $sub_id++)
+  } @$children
+}
+
+sub max_width {
+  11 * ( max( map { length $::locale->text($_->{name}) } @{ $_[0]{children} || [] } ) // 1 )
+}
+
+sub get_icon {
+  my $name = $_[0];
+
+  return undef if !defined $name;
+
+  my $simg = "image/icons/svg/$name.svg";
+  my $pimg = "image/icons/16x16/$name.png";
+
+    SL::System::ResourceCache->get($simg) ? $simg
+  : SL::System::ResourceCache->get($pimg) ? $pimg
+  :                                         ();
+}
+
+1;
+
+
diff --git a/SL/Presenter/Letter.pm b/SL/Presenter/Letter.pm
new file mode 100644 (file)
index 0000000..0747bfd
--- /dev/null
@@ -0,0 +1,82 @@
+package SL::Presenter::Letter;
+
+use strict;
+
+use SL::Presenter::EscapedText qw(escape is_escaped);
+
+use Exporter qw(import);
+our @EXPORT_OK = qw(letter);
+
+use Carp;
+
+sub letter {
+  my ($letter, %params) = @_;
+
+  $params{display} ||= 'inline';
+
+  croak "Unknown display type '$params{display}'" unless $params{display} =~ m/^(?:inline|table-cell)$/;
+
+  my $text = join '', (
+    $params{no_link} ? '' : '<a href="controller.pl?action=Letter/edit&amp;letter.id=' . escape($letter->id) . '">',
+    escape($letter->letternumber),
+    $params{no_link} ? '' : '</a>',
+  );
+
+  is_escaped($text);
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Presenter::Letter - Presenter module for letter objects
+
+=head1 SYNOPSIS
+
+  my $letter = SL::DB::Manager::Letter->get_first(where => [ … ]);
+  my $html   = SL::Presenter::Letter::letter($letter, display => 'inline');
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<letter $object, %params>
+
+Returns a rendered version (actually an instance of
+L<SL::Presenter::EscapedText>) of the letter object C<$object>
+.
+
+C<%params> can include:
+
+=over 2
+
+=item * display
+
+Either C<inline> (the default) or C<table-cell>. At the moment both
+representations are identical and produce the invoice number linked
+to the corresponding 'edit' action.
+
+=item * no_link
+
+If falsish (the default) then the invoice number will be linked to the
+"edit invoice" dialog from the general ledger menu.
+
+=back
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
diff --git a/SL/Presenter/MaterialComponents.pm b/SL/Presenter/MaterialComponents.pm
new file mode 100644 (file)
index 0000000..30123b7
--- /dev/null
@@ -0,0 +1,354 @@
+package SL::Presenter::MaterialComponents;
+
+use strict;
+
+use SL::HTML::Restrict;
+use SL::MoreCommon qw(listify);
+use SL::Presenter::EscapedText qw(escape);
+use SL::Presenter::Tag qw(html_tag);
+use Scalar::Util qw(blessed);
+use List::UtilsBy qw(partition_by);
+
+use Exporter qw(import);
+our @EXPORT_OK = qw(
+  button_tag
+  input_tag
+  date_tag
+  submit_tag
+  icon
+  select_tag
+  checkbox_tag
+);
+our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
+
+use constant BUTTON          => 'btn';
+use constant BUTTON_FLAT     => 'btn-flat';
+use constant BUTTON_FLOATING => 'btn-floating';
+use constant BUTTON_LARGE    => 'btn-large';
+use constant BUTTON_SMALL    => 'btn-small';
+use constant DISABLED        => 'disabled';
+use constant LEFT            => 'left';
+use constant MATERIAL_ICONS  => 'material-icons';
+use constant RIGHT           => 'right';
+use constant LARGE           => 'large';
+use constant MEDIUM          => 'medium';
+use constant SMALL           => 'small';
+use constant TINY            => 'tiny';
+use constant INPUT_FIELD     => 'input-field';
+use constant DATEPICKER      => 'datepicker';
+
+use constant WAVES_EFFECT    => 'waves-effect';
+use constant WAVES_LIGHT     => 'waves-light';
+
+
+my %optional_classes = (
+  button => {
+    disabled => DISABLED,
+    flat     => BUTTON_FLAT,
+    floating => BUTTON_FLOATING,
+    large    => BUTTON_LARGE,
+    small    => BUTTON_SMALL,
+  },
+  icon => {
+    left   => LEFT,
+    right  => RIGHT,
+    large  => LARGE,
+    medium => MEDIUM,
+    small  => SMALL,
+    tiny   => TINY,
+  },
+  size => {
+    map { $_ => $_ }
+      qw(col row),
+      (map { "s$_" } 1..12),
+      (map { "m$_" } 1..12),
+      (map { "l$_" } 1..12),
+  },
+);
+
+use Carp;
+
+sub _confirm_js {
+  'if (!confirm("'. _J($_[0]) .'")) return false;'
+}
+
+sub _confirm_to_onclick {
+  my ($attributes, $onclick) = @_;
+
+  if ($attributes->{confirm}) {
+    $$onclick //= '';
+    $$onclick = _confirm_js(delete($attributes->{confirm})) . $attributes->{onlick};
+  }
+}
+
+# used to extract material properties that need to be translated to classes
+# supports prefixing for delegation
+# returns a list of classes, mutates the attributes
+sub _extract_attribute_classes {
+  my ($attributes, $type, $prefix) = @_;
+
+  my @classes;
+  my $attr;
+  for my $key (keys %$attributes) {
+    if ($prefix) {
+      next unless $key =~ /^${prefix}_(.*)/;
+      $attr = $1;
+    } else {
+      $attr = $key;
+    }
+
+    if ($optional_classes{$type}{$attr}) {
+      $attributes->{$key} = undef;
+      push @classes, $optional_classes{$type}{$attr};
+    }
+  }
+
+  # delete all undefined values
+  my @delete_keys = grep { !defined $attributes->{$_} } keys %$attributes;
+  delete $attributes->{$_} for @delete_keys;
+
+  @classes;
+}
+
+# used to extract material classes that are passed directly as classes
+sub _extract_classes {
+  my ($attributes, $type) = @_;
+
+  my @classes = map { split / / } listify($attributes->{class});
+  my %classes = partition_by { !!$optional_classes{$type}{$_} } @classes;
+
+  $attributes->{class} = $classes{''};
+  $classes{1};
+}
+
+sub _set_id_attribute {
+  my ($attributes, $name, $unique) = @_;
+
+  if (!delete($attributes->{no_id}) && !$attributes->{id}) {
+    $attributes->{id}  = name_to_id($name);
+    $attributes->{id} .= '_' . $attributes->{value} if $unique;
+  }
+
+  %{ $attributes };
+}
+
+{ # This will give you an id for identifying html tags and such.
+  # It's guaranteed to be unique unless you exceed 10 mio calls per request.
+  # Do not use these id's to store information across requests.
+my $_id_sequence = int rand 1e7;
+sub _id {
+  return ( $_id_sequence = ($_id_sequence + 1) % 1e7 );
+}
+}
+
+sub name_to_id {
+  my ($name) = @_;
+
+  if (!$name) {
+    return "id_" . _id();
+  }
+
+  $name =~ s/\[\+?\]/ _id() /ge; # give constructs with [] or [+] unique ids
+  $name =~ s/[^\w_]/_/g;
+  $name =~ s/_+/_/g;
+
+  return $name;
+}
+
+sub button_tag {
+  my ($onclick, $value, %attributes) = @_;
+
+  _set_id_attribute(\%attributes, $attributes{name}) if $attributes{name};
+  _confirm_to_onclick(\%attributes, \$onclick);
+
+  my @button_classes = _extract_attribute_classes(\%attributes, "button");
+  my @icon_classes   = _extract_attribute_classes(\%attributes, "icon", "icon");
+
+  $attributes{class} = [
+    grep { $_ } $attributes{class}, WAVES_EFFECT, WAVES_LIGHT, BUTTON, @button_classes
+  ];
+
+  if ($attributes{icon}) {
+    $value = icon(delete $attributes{icon}, class => \@icon_classes)
+           . $value;
+  }
+
+  html_tag('a', $value, %attributes, onclick => $onclick);
+}
+
+sub submit_tag {
+  my ($name, $value, %attributes) = @_;
+
+  _set_id_attribute(\%attributes, $attributes{name}) if $attributes{name};
+  _confirm_to_onclick(\%attributes, \($attributes{onclick} //= ''));
+
+  my @button_classes = _extract_attribute_classes(\%attributes, "button");
+  my @icon_classes   = _extract_attribute_classes(\%attributes, "icon", "icon");
+
+  $attributes{class} = [
+    grep { $_ } $attributes{class}, WAVES_EFFECT, WAVES_LIGHT, BUTTON, @button_classes
+  ];
+
+  if ($attributes{icon}) {
+    $value = icon(delete $attributes{icon}, class => \@icon_classes)
+           . $value;
+  }
+
+  html_tag('button', $value, type => 'submit',  %attributes);
+}
+
+
+sub icon {
+  my ($name, %attributes) = @_;
+
+  my @icon_classes = _extract_attribute_classes(\%attributes, "icon");
+
+  html_tag('i', $name, class => [ grep { $_ } MATERIAL_ICONS, @icon_classes, delete $attributes{class} ], %attributes);
+}
+
+
+sub input_tag {
+  my ($name, $value, %attributes) = @_;
+
+  _set_id_attribute(\%attributes, $attributes{name});
+
+  my $class = delete $attributes{class};
+  my $icon  = $attributes{icon}
+    ? icon(delete $attributes{icon}, class => 'prefix')
+    : '';
+
+  my $label = $attributes{label}
+    ? html_tag('label', delete $attributes{label}, for => $attributes{id})
+    : '';
+
+  $attributes{type} //= 'text';
+
+  html_tag('div',
+    $icon .
+    html_tag('input', undef, value => $value, %attributes, name => $name) .
+    $label,
+    class => [ grep $_, $class, INPUT_FIELD ],
+  );
+}
+
+sub date_tag {
+  my ($name, $value, %attributes) = @_;
+
+  _set_id_attribute(\%attributes, $name);
+
+  my $icon  = $attributes{icon}
+    ? icon(delete $attributes{icon}, class => 'prefix')
+    : '';
+
+  my $label = $attributes{label}
+    ? html_tag('label', delete $attributes{label}, for => $attributes{id})
+    : '';
+
+  $attributes{type} = 'text'; # required for materialize
+
+  my @onchange = $attributes{onchange} ? (onChange => delete $attributes{onchange}) : ();
+  my @classes  = (delete $attributes{class});
+
+  $::request->layout->add_javascripts('kivi.Validator.js');
+  $::request->presenter->need_reinit_widgets($attributes{id});
+
+  $attributes{'data-validate'} = join(' ', "date", grep { $_ } (delete $attributes{'data-validate'}));
+
+  html_tag('div',
+    $icon .
+    html_tag('input',
+      blessed($value) ? $value->to_lxoffice : $value,
+      size   => 11, type => 'text', name => $name,
+      %attributes,
+      class => DATEPICKER, @onchange,
+    ) .
+    $label,
+    class => [ grep $_, @classes, INPUT_FIELD ],
+  );
+}
+
+sub select_tag {
+  my ($name, $collection, %attributes) = @_;
+
+
+  _set_id_attribute(\%attributes, $name);
+  my @size_classes   = _extract_classes(\%attributes, "size");
+
+
+  my $icon  = $attributes{icon}
+    ? icon(delete $attributes{icon}, class => 'prefix')
+    : '';
+
+  my $label = $attributes{label}
+    ? html_tag('label', delete $attributes{label}, for => $attributes{id})
+    : '';
+
+  my $select_html = SL::Presenter::Tag::select_tag($name, $collection, %attributes);
+
+  html_tag('div',
+    $icon . $select_html . $label,
+    class => [ INPUT_FIELD, @size_classes ],
+  );
+}
+
+sub checkbox_tag {
+  my ($name, %attributes) = @_;
+
+  _set_id_attribute(\%attributes, $name);
+
+  my $label = $attributes{label}
+    ? html_tag('span', delete $attributes{label})
+    : '';
+
+  my $checkbox_html = SL::Presenter::Tag::checkbox_tag($name, %attributes);
+
+  html_tag('label',
+    $checkbox_html . $label,
+  );
+}
+
+
+1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Presenter::MaterialComponents - MaterialCSS Component wrapper
+
+=head1 SYNOPSIS
+
+
+=head1 DESCRIPTION
+
+This is a collection of components in the style of L<SL::Presenter::Tag>
+intended for materialzecss. They should be useable similarly to their original
+versions but be well-behaved for materialize.
+
+They will also recognize some materialize conventions:
+
+=over 4
+
+=item icon>
+
+Most elements can be decorated with an icon by supplying the C<icon> with the name.
+
+=item grid classes
+
+Grid classes like C<s12> or C<m6> can be given as keys with any truish value or
+directly as classes.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@googlemail.comE<gt>
+
+=cut
index 19d8483..35005a7 100644 (file)
@@ -2,39 +2,39 @@ package SL::Presenter::Order;
 
 use strict;
 
-use parent qw(Exporter);
+use SL::Presenter::EscapedText qw(escape is_escaped);
 
 use Exporter qw(import);
-our @EXPORT = qw(sales_quotation sales_order request_quotation purchase_order);
+our @EXPORT_OK = qw(sales_quotation sales_order request_quotation purchase_order);
 
 use Carp;
 
 sub sales_quotation {
-  my ($self, $order, %params) = @_;
+  my ($order, %params) = @_;
 
-  return _oe_record($self, $order, 'sales_quotation', %params);
+  return _oe_record($order, 'sales_quotation', %params);
 }
 
 sub sales_order {
-  my ($self, $order, %params) = @_;
+  my ($order, %params) = @_;
 
-  return _oe_record($self, $order, 'sales_order', %params);
+  return _oe_record($order, 'sales_order', %params);
 }
 
 sub request_quotation {
-  my ($self, $order, %params) = @_;
+  my ($order, %params) = @_;
 
-  return _oe_record($self, $order, 'request_quotation', %params);
+  return _oe_record($order, 'request_quotation', %params);
 }
 
 sub purchase_order {
-  my ($self, $order, %params) = @_;
+  my ($order, %params) = @_;
 
-  return _oe_record($self, $order, 'purchase_order', %params);
+  return _oe_record($order, 'purchase_order', %params);
 }
 
 sub _oe_record {
-  my ($self, $order, $type, %params) = @_;
+  my ($order, $type, %params) = @_;
 
   $params{display} ||= 'inline';
 
@@ -42,12 +42,19 @@ sub _oe_record {
 
   my $number_method = $order->quotation ? 'quonumber' : 'ordnumber';
 
-  my $text = join '', (
-    $params{no_link} ? '' : '<a href="oe.pl?action=edit&amp;type=' . $type . '&amp;id=' . $self->escape($order->id) . '">',
-    $self->escape($order->$number_method),
-    $params{no_link} ? '' : '</a>',
-  );
-  return $self->escaped_text($text);
+  my $link_start = '';
+  my $link_end   = '';
+  unless ($params{no_link}) {
+    my $action  = $::instance_conf->get_feature_experimental_order
+                ? 'controller.pl?action=Order/edit'
+                : 'oe.pl?action=edit';
+    $link_start = '<a href="' . $action . '&amp;type=' . $type . '&amp;id=' . escape($order->id) . '">';
+    $link_end   = '</a>';
+  }
+
+  my $text = join '', ($link_start, escape($order->$number_method), $link_end);
+
+  is_escaped($text);
 }
 
 1;
@@ -68,19 +75,19 @@ quotations, sales orders, requests for quotations and purchase orders
 
   # Sales quotations:
   my $object = SL::DB::Manager::Order->get_first(where => [ SL::DB::Manager::Order->type_filter('sales_quotation') ]);
-  my $html   = SL::Presenter->get->sales_quotation($object, display => 'inline');
+  my $html   = SL::Presenter::Order::sales_quotation($object, display => 'inline');
 
   # Sales orders:
   my $object = SL::DB::Manager::Order->get_first(where => [ SL::DB::Manager::Order->type_filter('sales_order') ]);
-  my $html   = SL::Presenter->get->sales_order($object, display => 'inline');
+  my $html   = SL::Presenter::Order::sales_order($object, display => 'inline');
 
   # Requests for quotations:
   my $object = SL::DB::Manager::Order->get_first(where => [ SL::DB::Manager::Order->type_filter('request_quotation') ]);
-  my $html   = SL::Presenter->get->request_quotation($object, display => 'inline');
+  my $html   = SL::Presenter::Order::request_quotation($object, display => 'inline');
 
   # Purchase orders:
   my $object = SL::DB::Manager::Order->get_first(where => [ SL::DB::Manager::Order->type_filter('purchase_order') ]);
-  my $html   = SL::Presenter->get->purchase_order($object, display => 'inline');
+  my $html   = SL::Presenter::Order::purchase_order($object, display => 'inline');
 
 =head1 FUNCTIONS
 
index 3088eaa..2f8624d 100644 (file)
@@ -3,47 +3,133 @@ package SL::Presenter::Part;
 use strict;
 
 use SL::DB::Part;
+use SL::DB::PartClassification;
+use SL::Locale::String qw(t8);
+use SL::Presenter::EscapedText qw(escape is_escaped);
+use SL::Presenter::Tag qw(input_tag html_tag name_to_id select_tag);
 
 use Exporter qw(import);
-our @EXPORT = qw(part_picker part);
+our @EXPORT_OK = qw(
+  part_picker part select_classification classification_abbreviation
+  type_abbreviation separate_abbreviation typeclass_abbreviation
+);
+our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
 
 use Carp;
 
 sub part {
-  my ($self, $part, %params) = @_;
+  my ($part, %params) = @_;
 
   $params{display} ||= 'inline';
 
   croak "Unknown display type '$params{display}'" unless $params{display} =~ m/^(?:inline|table-cell)$/;
 
   my $text = join '', (
-    $params{no_link} ? '' : '<a href="ic.pl?action=edit&id=' . $self->escape($part->id) . '">',
-    $self->escape($part->partnumber),
+    $params{no_link} ? '' : '<a href="controller.pl?action=Part/edit&part.id=' . escape($part->id) . '">',
+    escape($part->partnumber),
     $params{no_link} ? '' : '</a>',
   );
-  return $self->escaped_text($text);
+
+  is_escaped($text);
 }
 
 sub part_picker {
-  my ($self, $name, $value, %params) = @_;
+  my ($name, $value, %params) = @_;
 
   $value = SL::DB::Manager::Part->find_by(id => $value) if $value && !ref $value;
-  my $id = delete($params{id}) || $self->name_to_id($name);
-  my $fat_set_item = delete $params{fat_set_item};
+  my $id = $params{id} || name_to_id($name);
 
   my @classes = $params{class} ? ($params{class}) : ();
   push @classes, 'part_autocomplete';
-  push @classes, 'partpicker_fat_set_item' if $fat_set_item;
 
   my $ret =
-    $self->input_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => "@classes", type => 'hidden', id => $id) .
-    join('', map { $params{$_} ? $self->input_tag("", delete $params{$_}, id => "${id}_${_}", type => 'hidden') : '' } qw(type unit convertible_unit)) .
-    $self->input_tag("", ref $value ? $value->displayable_name : '', id => "${id}_name", %params);
+    input_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => "@classes", type => 'hidden', id => $id,
+      'data-part-picker-data' => JSON::to_json(\%params),
+    ) .
+    input_tag("", ref $value ? $value->displayable_name : '', id => "${id}_name", %params);
 
-  $::request->layout->add_javascripts('autocomplete_part.js');
+  $::request->layout->add_javascripts('kivi.Part.js');
   $::request->presenter->need_reinit_widgets($id);
 
-  $self->html_tag('span', $ret, class => 'part_picker');
+  html_tag('span', $ret, class => 'part_picker');
+}
+
+sub picker { goto &part_picker }
+
+#
+# shortcut for article type
+#
+sub type_abbreviation {
+  my ($part_type) = @_;
+
+  return ''                                               if !$part_type;
+  return $::locale->text('Assembly (typeabbreviation)')   if $part_type eq 'assembly';
+  return $::locale->text('Part (typeabbreviation)')       if $part_type eq 'part';
+  return $::locale->text('Assortment (typeabbreviation)') if $part_type eq 'assortment';
+  return $::locale->text('Service (typeabbreviation)');
+}
+
+#
+# Translations for Abbreviations:
+#
+# $::locale->text('None (typeabbreviation)')
+# $::locale->text('Purchase (typeabbreviation)')
+# $::locale->text('Sales (typeabbreviation)')
+# $::locale->text('Merchandise (typeabbreviation)')
+# $::locale->text('Production (typeabbreviation)')
+#
+# and for descriptions
+# $::locale->text('Purchase')
+# $::locale->text('Sales')
+# $::locale->text('Merchandise')
+# $::locale->text('Production')
+
+#
+# shortcut for article type
+#
+sub classification_abbreviation {
+  my ($id) = @_;
+
+  return '' if !$id;
+
+  SL::DB::Manager::PartClassification->cache_all();
+  my $obj = SL::DB::PartClassification->load_cached($id);
+  $obj && $obj->abbreviation ? t8($obj->abbreviation) : '';
+}
+
+sub typeclass_abbreviation {
+  my ($part) = @_;
+  return '' if !$part || !$part->isa('SL::DB::Part');
+  return type_abbreviation($part->part_type) . classification_abbreviation($part->classification_id);
+}
+
+#
+# shortcut for article type
+#
+sub separate_abbreviation {
+  my ($id) = @_;
+
+  return '' if !$id;
+
+  SL::DB::Manager::PartClassification->cache_all();
+  my $obj = SL::DB::PartClassification->load_cached($id);
+  $obj && $obj->abbreviation && $obj->report_separate ? t8($obj->abbreviation) : '';
+}
+
+#
+# generate selection tag
+#
+sub select_classification {
+  my ($name, %attributes) = @_;
+
+  $attributes{value_key} = 'id';
+  $attributes{title_key} = 'description';
+
+  my $classification_type_filter = delete $attributes{type} // [];
+
+  my $collection = SL::DB::Manager::PartClassification->get_all_sorted( where => $classification_type_filter );
+  $_->description($::locale->text($_->description)) for @{ $collection };
+  select_tag( $name, $collection, %attributes );
 }
 
 1;
@@ -59,7 +145,7 @@ SL::Presenter::Part - Part related presenter stuff
 =head1 SYNOPSIS
 
   # Create an html link for editing/opening a part/service/assembly
-  my $object = my $object = SL::DB::Manager::Part->get_first;
+  my $object = SL::DB::Manager::Part->get_first;
   my $html   = SL::Presenter->get->part($object, display => 'inline');
 
 see also L<SL::Presenter>
@@ -93,6 +179,40 @@ to the corresponding 'edit' action.
 
 =over 2
 
+=item C<classification_abbreviation $classification_id>
+
+Returns the shortcut of the classification
+
+=back
+
+=over 2
+
+=item C<separate_abbreviation $classification_id>
+
+Returns the shortcut of the classification if the classification has the separate flag set.
+
+=back
+
+=over 2
+
+=item C<select_classification $name,%params>
+
+Returns an HTML select tag with all available classifications.
+
+C<%params> can include:
+
+=over 4
+
+=item * default
+
+The id of the selected item.
+
+=back
+
+=back
+
+=over 2
+
 =item C<part_picker $name, $value, %params>
 
 All-in-one picker widget for parts. The name will be both id and name
@@ -105,10 +225,15 @@ C<PART PICKER SPECIFICATION>.
 
 C<$value> can be a parts id or a C<Rose::DB:Object> instance.
 
-If C<%params> contains C<type> only parts of this type will be used
+If C<%params> contains C<part_type> only parts of this type will be used
 for autocompletion. You may comma separate multiple types as in
 C<part,assembly>.
 
+If C<%params> contains C<status> only parts of this status will be used
+for autocompletion. C<status> can be one of the following strings:
+C<active>, C<obsolete> or C<all>. C<active> is the default if C<status> is
+not given.
+
 If C<%params> contains C<unit> only parts with this unit will be used
 for autocompletion. You may comma separate multiple units as in
 C<h,min>.
@@ -116,15 +241,74 @@ C<h,min>.
 If C<%params> contains C<convertible_unit> only parts with a unit
 that's convertible to unit will be used for autocompletion.
 
+If C<%params> contains C<with_makemodel> or C<with_customer_partnumber> even
+parts will be used for autocompletion which partnumber is a vendor partnumber
+(makemodel) or a customer partnumber.
+
+If C<%params> contains C<multiple> an alternative popup will be opened,
+allowing multiple items to be selected. Note however that this requires
+an additional callback C<set_multi_items> to work.
+Also note that you can set C<multiple> to 0 (or not set C<multiple>) on
+creation of the picker, but can open the alternative multi select popup
+with js like this:
+C<$("#pp_id").data("part_picker").o.multiple=1; $("#pp_id").data("part_picker").open_dialog()'>
+where C<pp_id> is the dom id of the part picker.
+Or you can even do it the other way round setting C<multiple> to 1 on creation
+and open a single selection popup with js.
+
+If C<%params> contains C<multiple_pos_input> an input field with the dom id
+C<multi_items_position> will be rendered in the alternative popup.
+This can be used in the callback for C<set_multi_items> to controll the
+input postion for the items.
+
+If C<%params> contains C<multiple_limit> the alternative popup will not
+show any results if there are more than C<multiple_limit> results. A warning
+message is displayed in this case. Set C<multiple_limit> to 0 to disable
+the limitation. The limit defaults to 100.
+
 Obsolete parts will by default not be displayed for selection. However they are
 accepted as default values and can persist during updates. As with other
 selectors though, they are not selectable once overridden.
 
 C<part_picker> will register it's javascript for inclusion in the next header
-rendering. If you write a standard controller that only call C<render> once, it
-will just work.  In case the header is generated in a different render call
-(multiple blocks, ajax, old C<bin/moilla> style controllers) you need to
-include C<js/autocomplete_part.js> yourself.
+rendering. If you write a standard controller that only calls C<render> once, it
+will just work. In case the header is generated in a different render call
+(multiple blocks, ajax, old C<bin/mozilla> style controllers) you need to
+include C<kivi.Part.js> yourself.
+
+On pressing <enter> the picker will try to commit the current selection,
+resulting in one of the following events, whose corresponding callbacks can be
+set in C<params.actions>:
+
+=over 4
+
+=item * C<commit_one>
+
+If exactly one element matches the input, the internal id will be set to this
+id, the internal state will be set to C<PICKED> and the C<change> event on the
+picker will be fired. Additionally, if C<params> contains C<fat_set_item> a
+special event C<set_item:PartPicker> will be fired which is guaranteed to
+contain a complete JSON representation of the part.
+
+After that the action C<commit_one> will be executed, which defaults to
+clicking a button with id C<update_button> for backward compatibility reasons.
+
+=item * C<commit_many>
+
+If more than one element matches the input, the internal state will be set to
+undefined.
+
+After that the action C<commit_one> will be executed, which defaults to
+opening a popup dialog for graphical interaction.
+
+=item * C<commit_none>
+
+If no element matches the input, the internal state will be set to undefined.
+
+If an action for C<commit_none> exists, it will be called with the picker
+object and current term. The caller can then implement creation of new parts.
+
+=back
 
 =back
 
@@ -169,6 +353,14 @@ Should not require a feedback/check loop in the common case
 
 Should not be constrained to exact matches
 
+=item *
+
+Must be atomic
+
+=item *
+
+Action should be overridable
+
 =back
 
 The implementation consists of the following parts which will be referenced later:
@@ -214,4 +406,6 @@ None atm :)
 
 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
 
+Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
+
 =cut
index 5f39771..8aecdfe 100644 (file)
@@ -2,15 +2,16 @@ package SL::Presenter::Project;
 
 use strict;
 
-use parent qw(Exporter);
+use SL::Presenter::EscapedText qw(escape is_escaped);
+use SL::Presenter::Tag qw(input_tag html_tag name_to_id select_tag);
 
 use Exporter qw(import);
-our @EXPORT = qw(project project_picker);
+our @EXPORT_OK = qw(project project_picker);
 
 use Carp;
 
 sub project {
-  my ($self, $project, %params) = @_;
+  my ($project, %params) = @_;
 
   return '' unless $project;
 
@@ -22,32 +23,38 @@ sub project {
   my $callback    = $params{callback} ? '&callback=' . $::form->escape($params{callback}) : '';
 
   my $text = join '', (
-    $params{no_link} ? '' : '<a href="controller.pl?action=Project/edit&amp;id=' . $self->escape($project->id) . $callback . '">',
-    $self->escape($description),
+    $params{no_link} ? '' : '<a href="controller.pl?action=Project/edit&amp;id=' . escape($project->id) . $callback . '">',
+    escape($description),
     $params{no_link} ? '' : '</a>',
   );
-  return $self->escaped_text($text);
+  is_escaped($text);
 }
 
 sub project_picker {
-  my ($self, $name, $value, %params) = @_;
+  my ($name, $value, %params) = @_;
 
   $value      = SL::DB::Manager::Project->find_by(id => $value) if $value && !ref $value;
-  my $id      = delete($params{id}) || $self->name_to_id($name);
+  my $id      = delete($params{id}) || name_to_id($name);
   my @classes = $params{class} ? ($params{class}) : ();
   push @classes, 'project_autocomplete';
 
+
+  my %data_params = map { $_ => delete $params{$_}  } grep { defined $params{$_} } qw(customer_id active valid description_style);
+
   my $ret =
-    $self->input_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => "@classes", type => 'hidden', id => $id) .
-    join('', map { $params{$_} ? $self->input_tag("", delete $params{$_}, id => "${id}_${_}", type => 'hidden') : '' } qw(customer_id)) .
-    $self->input_tag("", ref $value ? $value->displayable_name : '', id => "${id}_name", %params);
+    input_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => "@classes", type => 'hidden', id => $id,
+              'data-project-picker-data' => JSON::to_json(\%data_params),
+    ) .
+    input_tag("", ref $value ? $value->displayable_name : '', id => "${id}_name", %params);
 
   $::request->layout->add_javascripts('autocomplete_project.js');
   $::request->presenter->need_reinit_widgets($id);
 
-  $self->html_tag('span', $ret, class => 'project_picker');
+  html_tag('span', $ret, class => 'project_picker');
 }
 
+sub picker { goto &project_picker };
+
 1;
 
 __END__
@@ -63,7 +70,7 @@ SL::Presenter::Project - Presenter module for project Rose::DB objects
 =head1 SYNOPSIS
 
   my $project = SL::DB::Manager::Project->get_first;
-  my $html    = SL::Presenter->get->project($project, display => 'inline');
+  my $html    = SL::Presenter::Project->project($project, display => 'inline');
 
 =head1 FUNCTIONS
 
index b395e82..592af55 100644 (file)
@@ -2,10 +2,11 @@ package SL::Presenter::Record;
 
 use strict;
 
-use parent qw(Exporter);
+use SL::Presenter;
+use SL::Presenter::EscapedText qw(escape is_escaped);
 
 use Exporter qw(import);
-our @EXPORT = qw(grouped_record_list empty_record_list record_list record);
+our @EXPORT_OK = qw(grouped_record_list empty_record_list record_list record);
 
 use SL::Util;
 
@@ -20,58 +21,72 @@ sub _arrayify {
 }
 
 sub record {
-  my ($self, $record, %params) = @_;
+  my ($record, %params) = @_;
 
   my %grouped = _group_records( [ $record ] ); # pass $record as arrayref
   my $type    = (keys %grouped)[0];
 
-  return $self->sales_invoice(   $record, %params) if $type eq 'sales_invoices';
-  return $self->purchase_invoice($record, %params) if $type eq 'purchase_invoices';
-  return $self->ar_transaction(  $record, %params) if $type eq 'ar_transactions';
-  return $self->ap_transaction(  $record, %params) if $type eq 'ap_transactions';
-  return $self->gl_transaction(  $record, %params) if $type eq 'gl_transactions';
+  $record->presenter->sales_invoice(   $record, %params) if $type eq 'sales_invoices';
+  $record->presenter->purchase_invoice($record, %params) if $type eq 'purchase_invoices';
+  $record->presenter->ar_transaction(  $record, %params) if $type eq 'ar_transactions';
+  $record->presenter->ap_transaction(  $record, %params) if $type eq 'ap_transactions';
+  $record->presenter->gl_transaction(  $record, %params) if $type eq 'gl_transactions';
 
   return '';
 }
 
 sub grouped_record_list {
-  my ($self, $list, %params) = @_;
+  my ($list, %params) = @_;
 
   %params    = map { exists $params{$_} ? ($_ => $params{$_}) : () } qw(edit_record_links with_columns object_id object_model);
 
   my %groups = _sort_grouped_lists(_group_records($list));
   my $output = '';
 
-  $output .= _requirement_spec_list(       $self, $groups{requirement_specs},        %params) if $groups{requirement_specs};
-  $output .= _sales_quotation_list(        $self, $groups{sales_quotations},         %params) if $groups{sales_quotations};
-  $output .= _sales_order_list(            $self, $groups{sales_orders},             %params) if $groups{sales_orders};
-  $output .= _sales_delivery_order_list(   $self, $groups{sales_delivery_orders},    %params) if $groups{sales_delivery_orders};
-  $output .= _sales_invoice_list(          $self, $groups{sales_invoices},           %params) if $groups{sales_invoices};
-  $output .= _ar_transaction_list(         $self, $groups{ar_transactions},          %params) if $groups{ar_transactions};
+  $output .= _requirement_spec_list(       $groups{requirement_specs},        %params) if $groups{requirement_specs};
+  $output .= _shop_order_list(             $groups{shop_orders},              %params) if $groups{shop_orders};
+  $output .= _sales_quotation_list(        $groups{sales_quotations},         %params) if $groups{sales_quotations};
+  $output .= _sales_order_list(            $groups{sales_orders},             %params) if $groups{sales_orders};
+  $output .= _sales_delivery_order_list(   $groups{sales_delivery_orders},    %params) if $groups{sales_delivery_orders};
+  $output .= _rma_delivery_order_list(     $groups{rma_delivery_orders},      %params) if $groups{rma_delivery_orders};
+  $output .= _sales_invoice_list(          $groups{sales_invoices},           %params) if $groups{sales_invoices};
+  $output .= _ar_transaction_list(         $groups{ar_transactions},          %params) if $groups{ar_transactions};
 
-  $output .= _request_quotation_list(      $self, $groups{purchase_quotations},      %params) if $groups{purchase_quotations};
-  $output .= _purchase_order_list(         $self, $groups{purchase_orders},          %params) if $groups{purchase_orders};
-  $output .= _purchase_delivery_order_list($self, $groups{purchase_delivery_orders}, %params) if $groups{purchase_delivery_orders};
-  $output .= _purchase_invoice_list(       $self, $groups{purchase_invoices},        %params) if $groups{purchase_invoices};
-  $output .= _ap_transaction_list(         $self, $groups{ap_transactions},          %params) if $groups{ap_transactions};
+  $output .= _request_quotation_list(      $groups{purchase_quotations},      %params) if $groups{purchase_quotations};
+  $output .= _purchase_order_list(         $groups{purchase_orders},          %params) if $groups{purchase_orders};
+  $output .= _purchase_delivery_order_list($groups{purchase_delivery_orders}, %params) if $groups{purchase_delivery_orders};
+  $output .= _supplier_delivery_order_list($groups{supplier_delivery_orders}, %params) if $groups{supplier_delivery_orders};
+  $output .= _purchase_invoice_list(       $groups{purchase_invoices},        %params) if $groups{purchase_invoices};
+  $output .= _ap_transaction_list(         $groups{ap_transactions},          %params) if $groups{ap_transactions};
 
-  $output .= _bank_transactions(           $self, $groups{bank_transactions},        %params) if $groups{bank_transactions};
+  $output .= _gl_transaction_list(         $groups{gl_transactions},          %params) if $groups{gl_transactions};
 
-  $output .= _sepa_collection_list(        $self, $groups{sepa_collections},         %params) if $groups{sepa_collections};
-  $output .= _sepa_transfer_list(          $self, $groups{sepa_transfers},           %params) if $groups{sepa_transfers};
+  $output .= _bank_transactions(           $groups{bank_transactions},        %params) if $groups{bank_transactions};
 
-  $output  = $self->render('presenter/record/grouped_record_list', %params, output => $output);
+  $output .= _sepa_collection_list(        $groups{sepa_collections},         %params) if $groups{sepa_collections};
+  $output .= _sepa_transfer_list(          $groups{sepa_transfers},           %params) if $groups{sepa_transfers};
+
+  $output .= _letter_list(                 $groups{letters},                  %params) if $groups{letters};
+  $output .= _email_journal_list(          $groups{email_journals},           %params) if $groups{email_journals};
+
+  $output .= _dunning_list(                $groups{dunnings},                 %params) if $groups{dunnings};
+
+  $output  = SL::Presenter->get->render('presenter/record/grouped_record_list', %params, output => $output);
 
   return $output;
 }
 
+sub grouped_list { goto &grouped_record_list }
+
 sub empty_record_list {
-  my ($self, %params) = @_;
-  return $self->grouped_record_list([], %params);
+  my (%params) = @_;
+  return grouped_record_list([], %params);
 }
 
+sub empty_list { goto &empty_record_list }
+
 sub record_list {
-  my ($self, $list, %params) = @_;
+  my ($list, %params) = @_;
 
   my @columns;
 
@@ -128,7 +143,7 @@ sub record_list {
         $cell{value} = $spec->{data}->($obj);
 
       } else {
-        $cell{value} = $rel_type && $self->can($rel_type)                                       ? $self->$rel_type($obj->$method, display => 'table-cell')
+        $cell{value} = ref $obj->$method && $obj->$method->isa('SL::DB::Object') && $obj->$method->presenter->can($rel_type) ? $obj->$method->presenter->$rel_type(display => 'table-cell')
                      : $type eq 'Rose::DB::Object::Metadata::Column::Date'                      ? $call->($obj, $method . '_as_date')
                      : $type =~ m/^Rose::DB::Object::Metadata::Column::(?:Float|Numeric|Real)$/ ? $::form->format_amount(\%::myconfig, $call->($obj, $method), 2)
                      : $type eq 'Rose::DB::Object::Metadata::Column::Boolean'                   ? $call->($obj, $method . '_as_bool_yn')
@@ -149,7 +164,7 @@ sub record_list {
            alignment => $data[0]->{columns}->[$_]->{alignment},
          }, (0..scalar(@columns) - 1);
 
-  return $self->render(
+  return SL::Presenter->get->render(
     'presenter/record/record_list',
     %params,
     TABLE_HEADER => \@header,
@@ -157,29 +172,36 @@ sub record_list {
   );
 }
 
+sub list { goto &record_list }
+
 #
 # private methods
 #
 
 sub _group_records {
   my ($list) = @_;
-
   my %matchers = (
     requirement_specs        => sub { (ref($_[0]) eq 'SL::DB::RequirementSpec')                                         },
+    shop_orders              => sub { (ref($_[0]) eq 'SL::DB::ShopOrder')       &&  $_[0]->id                           },
     sales_quotations         => sub { (ref($_[0]) eq 'SL::DB::Order')           &&  $_[0]->is_type('sales_quotation')   },
     sales_orders             => sub { (ref($_[0]) eq 'SL::DB::Order')           &&  $_[0]->is_type('sales_order')       },
-    sales_delivery_orders    => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder')   &&  $_[0]->is_sales                     },
+    sales_delivery_orders    => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder')   &&  $_[0]->is_type('sales_delivery_order') },
+    rma_delivery_orders      => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder')   &&  $_[0]->is_type('rma_delivery_order')   },
     sales_invoices           => sub { (ref($_[0]) eq 'SL::DB::Invoice')         &&  $_[0]->invoice                      },
     ar_transactions          => sub { (ref($_[0]) eq 'SL::DB::Invoice')         && !$_[0]->invoice                      },
     purchase_quotations      => sub { (ref($_[0]) eq 'SL::DB::Order')           &&  $_[0]->is_type('request_quotation') },
     purchase_orders          => sub { (ref($_[0]) eq 'SL::DB::Order')           &&  $_[0]->is_type('purchase_order')    },
-    purchase_delivery_orders => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder')   && !$_[0]->is_sales                     },
+    purchase_delivery_orders => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder')   &&  $_[0]->is_type('purchase_delivery_order') },
+    supplier_delivery_orders => sub { (ref($_[0]) eq 'SL::DB::DeliveryOrder')   &&  $_[0]->is_type('supplier_delivery_order') },
     purchase_invoices        => sub { (ref($_[0]) eq 'SL::DB::PurchaseInvoice') &&  $_[0]->invoice                      },
     ap_transactions          => sub { (ref($_[0]) eq 'SL::DB::PurchaseInvoice') && !$_[0]->invoice                      },
     sepa_collections         => sub { (ref($_[0]) eq 'SL::DB::SepaExportItem')  &&  $_[0]->ar_id                        },
     sepa_transfers           => sub { (ref($_[0]) eq 'SL::DB::SepaExportItem')  &&  $_[0]->ap_id                        },
     gl_transactions          => sub { (ref($_[0]) eq 'SL::DB::GLTransaction')                                           },
     bank_transactions        => sub { (ref($_[0]) eq 'SL::DB::BankTransaction') &&  $_[0]->id                           },
+    letters                  => sub { (ref($_[0]) eq 'SL::DB::Letter')          &&  $_[0]->id                           },
+    email_journals           => sub { (ref($_[0]) eq 'SL::DB::EmailJournal')    &&  $_[0]->id                           },
+    dunnings                 => sub { (ref($_[0]) eq 'SL::DB::Dunning')                                                 },
   );
 
   my %groups;
@@ -209,14 +231,14 @@ sub _sort_grouped_lists {
 }
 
 sub _requirement_spec_list {
-  my ($self, $list, %params) = @_;
+  my ($list, %params) = @_;
 
-  return $self->record_list(
+  return record_list(
     $list,
     title   => $::locale->text('Requirement specs'),
     type    => 'requirement_spec',
     columns => [
-      [ $::locale->text('Requirement spec number'), sub { $self->requirement_spec($_[0], display => 'table-cell') } ],
+      [ $::locale->text('Requirement spec number'), sub { $_[0]->presenter->requirement_spec(display => 'table-cell') } ],
       [ $::locale->text('Customer'),                'customer'                                                      ],
       [ $::locale->text('Title'),                   'title'                                                         ],
       [ $::locale->text('Project'),                 'project',                                                      ],
@@ -226,16 +248,33 @@ sub _requirement_spec_list {
   );
 }
 
+sub _shop_order_list {
+  my ($list, %params) = @_;
+
+  return record_list(
+    $list,
+    title   => $::locale->text('Shop Orders'),
+    type    => 'shop_order',
+    columns => [
+      [ $::locale->text('Shop Order Date'),         sub { $_[0]->order_date->to_kivitendo }                         ],
+      [ $::locale->text('Shop Order Number'),       sub { $_[0]->presenter->shop_order(display => 'table-cell') }   ],
+      [ $::locale->text('Transfer Date'),           'transfer_date'                                                 ],
+      [ $::locale->text('Amount'),                  'amount'                                                        ],
+    ],
+    %params,
+  );
+}
+
 sub _sales_quotation_list {
-  my ($self, $list, %params) = @_;
+  my ($list, %params) = @_;
 
-  return $self->record_list(
+  return record_list(
     $list,
     title   => $::locale->text('Sales Quotations'),
     type    => 'sales_quotation',
     columns => [
       [ $::locale->text('Quotation Date'),          'transdate'                                                                ],
-      [ $::locale->text('Quotation Number'),        sub { $self->sales_quotation($_[0], display => 'table-cell') }   ],
+      [ $::locale->text('Quotation Number'),        sub { $_[0]->presenter->sales_quotation(display => 'table-cell') }         ],
       [ $::locale->text('Customer'),                'customer'                                                                 ],
       [ $::locale->text('Net amount'),              'netamount'                                                                ],
       [ $::locale->text('Transaction description'), 'transaction_description'                                                  ],
@@ -247,15 +286,15 @@ sub _sales_quotation_list {
 }
 
 sub _request_quotation_list {
-  my ($self, $list, %params) = @_;
+  my ($list, %params) = @_;
 
-  return $self->record_list(
+  return record_list(
     $list,
     title   => $::locale->text('Request Quotations'),
     type    => 'request_quotation',
     columns => [
       [ $::locale->text('Quotation Date'),          'transdate'                                                                ],
-      [ $::locale->text('Quotation Number'),        sub { $self->request_quotation($_[0], display => 'table-cell') }   ],
+      [ $::locale->text('Quotation Number'),        sub { $_[0]->presenter->request_quotation(display => 'table-cell') }       ],
       [ $::locale->text('Vendor'),                  'vendor'                                                                   ],
       [ $::locale->text('Net amount'),              'netamount'                                                                ],
       [ $::locale->text('Transaction description'), 'transaction_description'                                                  ],
@@ -267,15 +306,15 @@ sub _request_quotation_list {
 }
 
 sub _sales_order_list {
-  my ($self, $list, %params) = @_;
+  my ($list, %params) = @_;
 
-  return $self->record_list(
+  return record_list(
     $list,
     title   => $::locale->text('Sales Orders'),
     type    => 'sales_order',
     columns => [
       [ $::locale->text('Order Date'),              'transdate'                                                                ],
-      [ $::locale->text('Order Number'),            sub { $self->sales_order($_[0], display => 'table-cell') }   ],
+      [ $::locale->text('Order Number'),            sub { $_[0]->presenter->sales_order(display => 'table-cell') }             ],
       [ $::locale->text('Quotation'),               'quonumber' ],
       [ $::locale->text('Customer'),                'customer'                                                                 ],
       [ $::locale->text('Net amount'),              'netamount'                                                                ],
@@ -288,15 +327,15 @@ sub _sales_order_list {
 }
 
 sub _purchase_order_list {
-  my ($self, $list, %params) = @_;
+  my ($list, %params) = @_;
 
-  return $self->record_list(
+  return record_list(
     $list,
     title   => $::locale->text('Purchase Orders'),
     type    => 'purchase_order',
     columns => [
       [ $::locale->text('Order Date'),              'transdate'                                                                ],
-      [ $::locale->text('Order Number'),            sub { $self->purchase_order($_[0], display => 'table-cell') }   ],
+      [ $::locale->text('Order Number'),            sub { $_[0]->presenter->purchase_order(display => 'table-cell') }          ],
       [ $::locale->text('Request for Quotation'),   'quonumber' ],
       [ $::locale->text('Vendor'),                  'vendor'                                                                 ],
       [ $::locale->text('Net amount'),              'netamount'                                                                ],
@@ -309,15 +348,36 @@ sub _purchase_order_list {
 }
 
 sub _sales_delivery_order_list {
-  my ($self, $list, %params) = @_;
+  my ($list, %params) = @_;
 
-  return $self->record_list(
+  return record_list(
     $list,
     title   => $::locale->text('Sales Delivery Orders'),
     type    => 'sales_delivery_order',
     columns => [
       [ $::locale->text('Delivery Order Date'),     'transdate'                                                                ],
-      [ $::locale->text('Delivery Order Number'),   sub { $self->sales_delivery_order($_[0], display => 'table-cell') } ],
+      [ $::locale->text('Delivery Order Number'),   sub { $_[0]->presenter->sales_delivery_order(display => 'table-cell') }    ],
+      [ $::locale->text('Order Number'),            'ordnumber' ],
+      [ $::locale->text('Customer'),                'customer'                                                                 ],
+      [ $::locale->text('Transaction description'), 'transaction_description'                                                  ],
+      [ $::locale->text('Project'),                 'globalproject', ],
+      [ $::locale->text('Delivered'),               'delivered'                                                                ],
+      [ $::locale->text('Closed'),                  'closed'                                                                   ],
+    ],
+    %params,
+  );
+}
+
+sub _rma_delivery_order_list {
+  my ($list, %params) = @_;
+
+  return record_list(
+    $list,
+    title   => $::locale->text('RMA Delivery Orders'),
+    type    => 'rma_delivery_order',
+    columns => [
+      [ $::locale->text('Delivery Order Date'),     'transdate'                                                                ],
+      [ $::locale->text('Delivery Order Number'),   sub { $_[0]->presenter->rma_delivery_order(display => 'table-cell') }    ],
       [ $::locale->text('Order Number'),            'ordnumber' ],
       [ $::locale->text('Customer'),                'customer'                                                                 ],
       [ $::locale->text('Transaction description'), 'transaction_description'                                                  ],
@@ -330,15 +390,36 @@ sub _sales_delivery_order_list {
 }
 
 sub _purchase_delivery_order_list {
-  my ($self, $list, %params) = @_;
+  my ($list, %params) = @_;
 
-  return $self->record_list(
+  return record_list(
     $list,
     title   => $::locale->text('Purchase Delivery Orders'),
     type    => 'purchase_delivery_order',
     columns => [
       [ $::locale->text('Delivery Order Date'),     'transdate'                                                                ],
-      [ $::locale->text('Delivery Order Number'),   sub { $self->purchase_delivery_order($_[0], display => 'table-cell') } ],
+      [ $::locale->text('Delivery Order Number'),   sub { $_[0]->presenter->purchase_delivery_order(display => 'table-cell') } ],
+      [ $::locale->text('Order Number'),            'ordnumber' ],
+      [ $::locale->text('Vendor'),                  'vendor'                                                                 ],
+      [ $::locale->text('Transaction description'), 'transaction_description'                                                  ],
+      [ $::locale->text('Project'),                 'globalproject', ],
+      [ $::locale->text('Delivered'),               'delivered'                                                                ],
+      [ $::locale->text('Closed'),                  'closed'                                                                   ],
+    ],
+    %params,
+  );
+}
+
+sub _supplier_delivery_order_list {
+  my ($list, %params) = @_;
+
+  return record_list(
+    $list,
+    title   => $::locale->text('Supplier Delivery Orders'),
+    type    => 'supplier_delivery_order',
+    columns => [
+      [ $::locale->text('Delivery Order Date'),     'transdate'                                                                ],
+      [ $::locale->text('Delivery Order Number'),   sub { $_[0]->presenter->supplier_delivery_order(display => 'table-cell') } ],
       [ $::locale->text('Order Number'),            'ordnumber' ],
       [ $::locale->text('Vendor'),                  'vendor'                                                                 ],
       [ $::locale->text('Transaction description'), 'transaction_description'                                                  ],
@@ -351,16 +432,16 @@ sub _purchase_delivery_order_list {
 }
 
 sub _sales_invoice_list {
-  my ($self, $list, %params) = @_;
+  my ($list, %params) = @_;
 
-  return $self->record_list(
+  return record_list(
     $list,
     title   => $::locale->text('Sales Invoices'),
     type    => 'sales_invoice',
     columns => [
       [ $::locale->text('Invoice Date'),            'transdate'               ],
       [ $::locale->text('Type'),                    sub { $_[0]->displayable_type } ],
-      [ $::locale->text('Invoice Number'),          sub { $self->sales_invoice($_[0], display => 'table-cell') } ],
+      [ $::locale->text('Invoice Number'),          sub { $_[0]->presenter->sales_invoice(display => 'table-cell') } ],
       [ $::locale->text('Quotation Number'),        'quonumber' ],
       [ $::locale->text('Order Number'),            'ordnumber' ],
       [ $::locale->text('Customer'),                'customer'                ],
@@ -373,15 +454,15 @@ sub _sales_invoice_list {
 }
 
 sub _purchase_invoice_list {
-  my ($self, $list, %params) = @_;
+  my ($list, %params) = @_;
 
-  return $self->record_list(
+  return record_list(
     $list,
     title   => $::locale->text('Purchase Invoices'),
     type    => 'purchase_invoice',
     columns => [
       [ $::locale->text('Invoice Date'),                 'transdate'               ],
-      [ $::locale->text('Invoice Number'),               sub { $self->purchase_invoice($_[0], display => 'table-cell') } ],
+      [ $::locale->text('Invoice Number'),               sub { $_[0]->presenter->purchase_invoice(display => 'table-cell') } ],
       [ $::locale->text('Request for Quotation Number'), 'quonumber' ],
       [ $::locale->text('Order Number'),                 'ordnumber' ],
       [ $::locale->text('Vendor'),                       'vendor'                 ],
@@ -394,16 +475,16 @@ sub _purchase_invoice_list {
 }
 
 sub _ar_transaction_list {
-  my ($self, $list, %params) = @_;
+  my ($list, %params) = @_;
 
-  return $self->record_list(
+  return record_list(
     $list,
     title   => $::locale->text('AR Transactions'),
     type    => 'ar_transaction',
     columns => [
       [ $::locale->text('Invoice Date'),            'transdate'               ],
       [ $::locale->text('Type'),                    sub { $_[0]->displayable_type } ],
-      [ $::locale->text('Invoice Number'),          sub { $self->ar_transaction($_[0], display => 'table-cell') } ],
+      [ $::locale->text('Invoice Number'),          sub { $_[0]->presenter->ar_transaction(display => 'table-cell') } ],
       [ $::locale->text('Customer'),                'customer'                ],
       [ $::locale->text('Net amount'),              'netamount'               ],
       [ $::locale->text('Paid'),                    'paid'                    ],
@@ -414,15 +495,15 @@ sub _ar_transaction_list {
 }
 
 sub _ap_transaction_list {
-  my ($self, $list, %params) = @_;
+  my ($list, %params) = @_;
 
-  return $self->record_list(
+  return record_list(
     $list,
     title   => $::locale->text('AP Transactions'),
     type    => 'ap_transaction',
     columns => [
       [ $::locale->text('Invoice Date'),            'transdate'                      ],
-      [ $::locale->text('Invoice Number'),          sub { $self->ap_transaction($_[0 ], display => 'table-cell') } ],
+      [ $::locale->text('Invoice Number'),          sub { $_[0]->presenter->ap_transaction(display => 'table-cell') } ],
       [ $::locale->text('Vendor'),                  'vendor'                         ],
       [ $::locale->text('Net amount'),              'netamount'                      ],
       [ $::locale->text('Paid'),                    'paid'                           ],
@@ -432,17 +513,33 @@ sub _ap_transaction_list {
   );
 }
 
+sub _gl_transaction_list {
+  my ($list, %params) = @_;
+
+  return record_list(
+    $list,
+    title   => $::locale->text('GL Transactions'),
+    type    => 'gl_transaction',
+    columns => [
+      [ $::locale->text('Transdate'),        'transdate'                                                    ],
+      [ $::locale->text('Reference'),   'reference'                                                    ],
+      [ $::locale->text('Description'), sub { $_[0]->presenter->gl_transaction(display => 'table-cell') } ],
+    ],
+    %params,
+  );
+}
+
 sub _bank_transactions {
-  my ($self, $list, %params) = @_;
+  my ($list, %params) = @_;
 
-  return $self->record_list(
+  return record_list(
     $list,
     title   => $::locale->text('Bank transactions'),
     type    => 'bank_transactions',
     columns => [
       [ $::locale->text('Transdate'),            'transdate'                      ],
-      [ $::locale->text('Local Bank Code'),      sub { $self->bank_code($_[0]->local_bank_account) }  ],
-      [ $::locale->text('Local account number'), sub { $self->account_number($_[0]->local_bank_account) }  ],
+      [ $::locale->text('Local Bank Code'),      sub { $_[0]->local_bank_account->presenter->bank_code }  ],
+      [ $::locale->text('Local account number'), sub { $_[0]->local_bank_account->presenter->account_number }  ],
       [ $::locale->text('Remote Bank Code'),     'remote_bank_code' ],
       [ $::locale->text('Remote account number'),'remote_account_number' ],
       [ $::locale->text('Valutadate'),           'valutadate' ],
@@ -456,7 +553,7 @@ sub _bank_transactions {
 }
 
 sub _sepa_export_list {
-  my ($self, $list, %params) = @_;
+  my ($list, %params) = @_;
 
   my ($source, $destination) = $params{type} eq 'sepa_transfer' ? qw(our vc)                                 : qw(vc our);
   $params{title}             = $params{type} eq 'sepa_transfer' ? $::locale->text('Bank transfers via SEPA') : $::locale->text('Bank collections via SEPA');
@@ -464,7 +561,7 @@ sub _sepa_export_list {
 
   delete $params{edit_record_links};
 
-  return $self->record_list(
+  return record_list(
     $list,
     columns => [
       [ $::locale->text('Export Number'),    'sepa_export',                                  ],
@@ -481,13 +578,66 @@ sub _sepa_export_list {
 }
 
 sub _sepa_transfer_list {
-  my ($self, $list, %params) = @_;
-  _sepa_export_list($self, $list, %params, type => 'sepa_transfer');
+  my ($list, %params) = @_;
+  _sepa_export_list($list, %params, type => 'sepa_transfer');
 }
 
 sub _sepa_collection_list {
-  my ($self, $list, %params) = @_;
-  _sepa_export_list($self, $list, %params, type => 'sepa_collection');
+  my ($list, %params) = @_;
+  _sepa_export_list($list, %params, type => 'sepa_collection');
+}
+
+sub _letter_list {
+  my ($list, %params) = @_;
+
+  return record_list(
+    $list,
+    title   => $::locale->text('Letters'),
+    type    => 'letter',
+    columns => [
+      [ $::locale->text('Date'),         'date'                                                ],
+      [ $::locale->text('Letternumber'), sub { $_[0]->presenter->letter(display => 'table-cell') } ],
+      [ $::locale->text('Customer'),     'customer'                                            ],
+      [ $::locale->text('Reference'),    'reference'                                           ],
+      [ $::locale->text('Subject'),      'subject'                                             ],
+    ],
+    %params,
+  );
+}
+
+sub _email_journal_list {
+  my ($list, %params) = @_;
+
+  return record_list(
+    $list,
+    title   => $::locale->text('Email'),
+    type    => 'email_journal',
+    columns => [
+      [ $::locale->text('Sent on'), sub { $_[0]->sent_on->to_kivitendo(precision => 'seconds') } ],
+      [ $::locale->text('Subject'), sub { $_[0]->presenter->email_journal(display => 'table-cell') } ],
+      [ $::locale->text('Status'),  'status'                                                     ],
+      [ $::locale->text('From'),    'from'                                                       ],
+      [ $::locale->text('To'),      'recipients'                                                 ],
+    ],
+    %params,
+  );
+}
+sub _dunning_list {
+  my ($list, %params) = @_;
+
+  return record_list(
+    $list,
+    title   => $::locale->text('Dunnings'),
+    type    => 'dunning',
+    columns => [
+      [ $::locale->text('Dunning Level'),   sub { $_[0]->presenter->dunning(display => 'table-cell') } ],
+      [ $::locale->text('Dunning Date'),    'transdate'                                                ],
+      [ $::locale->text('Dunning Duedate'), 'duedate'                                                  ],
+      [ $::locale->text('Total Fees'),      'fee'                                                      ],
+      [ $::locale->text('Interest'),        'interest'                                                 ],
+    ],
+    %params,
+  );
 }
 
 1;
@@ -558,6 +708,8 @@ The order in which the records are grouped is:
 
 =item * sales delivery orders
 
+=item * rma delivery orders
+
 =item * sales invoices
 
 =item * AR transactions
@@ -568,10 +720,14 @@ The order in which the records are grouped is:
 
 =item * purchase delivery orders
 
+=item * supplier delivery orders
+
 =item * purchase invoices
 
 =item * AP transactions
 
+=item * GL transactions
+
 =item * SEPA collections
 
 =item * SEPA transfers
index 5cca5a8..33746df 100644 (file)
@@ -2,26 +2,27 @@ package SL::Presenter::RequirementSpec;
 
 use strict;
 
-use parent qw(Exporter);
+use SL::Presenter::EscapedText qw(escape is_escaped);
 
 use Exporter qw(import);
-our @EXPORT = qw(requirement_spec);
+our @EXPORT_OK = qw(requirement_spec);
 
 use Carp;
 
 sub requirement_spec {
-  my ($self, $requirement_spec, %params) = @_;
+  my ($requirement_spec, %params) = @_;
 
   $params{display} ||= 'inline';
 
   croak "Unknown display type '$params{display}'" unless $params{display} =~ m/^(?:inline|table-cell)$/;
 
   my $text = join '', (
-    $params{no_link} ? '' : '<a href="controller.pl?action=RequirementSpec/show&amp;id=' . $self->escape($requirement_spec->id) . '">',
-    $self->escape($requirement_spec->id),
+    $params{no_link} ? '' : '<a href="controller.pl?action=RequirementSpec/show&amp;id=' . escape($requirement_spec->id) . '">',
+    escape($requirement_spec->id),
     $params{no_link} ? '' : '</a>',
   );
-  return $self->escaped_text($text);
+
+  is_escaped($text);
 }
 
 1;
index faafd26..58be195 100644 (file)
@@ -2,39 +2,45 @@ package SL::Presenter::RequirementSpecItem;
 
 use strict;
 
-use parent qw(Exporter);
+use SL::Presenter::Text qw(truncate);
 
 use Exporter qw(import);
-our @EXPORT = qw(requirement_spec_item_tree_node_title requirement_spec_item_jstree_data requirement_spec_item_dependency_list);
+our @EXPORT_OK = qw(requirement_spec_item_tree_node_title requirement_spec_item_jstree_data requirement_spec_item_dependency_list);
 
 use Carp;
 
 sub requirement_spec_item_tree_node_title {
-  my ($self, $item) = @_;
+  my ($item) = @_;
 
-  return join(' ', map { $_ || '' } ($item->fb_number, $self->truncate($item->parent_id ? $item->description_as_stripped_html : $item->title, at => 30)));
+  return join(' ', map { $_ || '' } ($item->fb_number, truncate($item->parent_id ? $item->description_as_stripped_html : $item->title, at => 30)));
 }
 
+sub tree_node_title { goto &requirement_spec_item_tree_node_title }
+
 sub requirement_spec_item_jstree_data {
-  my ($self, $item, %params) = @_;
+  my ($item, %params) = @_;
 
-  my @children = map { $self->requirement_spec_item_jstree_data($_, %params) } @{ $item->children_sorted };
+  my @children = map { requirement_spec_item_jstree_data($_, %params) } @{ $item->children_sorted };
   my $type     = !$item->parent_id ? 'section' : 'function-block';
   my $class    = $type . '-context-menu tooltip';
   $class      .= ' flagged' if $item->is_flagged;
 
   return {
-    data     => $self->requirement_spec_item_tree_node_title($item),
+    data     => requirement_spec_item_tree_node_title($item),
     metadata => { id =>         $item->id, type => $type },
     attr     => { id => "fb-" . $item->id, href => $params{href} || '#', class => $class, title => $item->content_excerpt },
     children => \@children,
   };
 }
 
+sub jstree_data { goto &requirement_spec_item_jstree_data }
+
 sub requirement_spec_item_dependency_list {
-  my ($self, $item) = @_;
+  my ($item) = @_;
 
   $::locale->language_join([ map { $_->fb_number } @{ $item->dependencies } ]);
 }
 
+sub dependency_list { goto &requirement_spec_item_dependency_list }
+
 1;
index 8a209d3..0f8c561 100644 (file)
@@ -2,17 +2,15 @@ package SL::Presenter::RequirementSpecTextBlock;
 
 use strict;
 
-use parent qw(Exporter);
-
 use Exporter qw(import);
-our @EXPORT = qw(requirement_spec_text_block_jstree_data);
+our @EXPORT_OK = qw(requirement_spec_text_block_jstree_data);
 
 use Carp;
 
 use SL::JSON;
 
 sub requirement_spec_text_block_jstree_data {
-  my ($self, $text_block, %params) = @_;
+  my ($text_block, %params) = @_;
 
   my $class  = 'text-block-context-menu tooltip';
   $class    .= ' flagged' if $text_block->is_flagged;
@@ -24,4 +22,6 @@ sub requirement_spec_text_block_jstree_data {
   };
 }
 
+sub jstree_data { goto &requirement_spec_text_block_jstree_data }
+
 1;
index ceb483f..e2b5423 100644 (file)
@@ -2,26 +2,26 @@ package SL::Presenter::SepaExport;
 
 use strict;
 
-use parent qw(Exporter);
+use SL::Presenter::EscapedText qw(escape is_escaped);
 
 use Exporter qw(import);
-our @EXPORT = qw(sepa_export);
+our @EXPORT_OK = qw(sepa_export);
 
 use Carp;
 
 sub sepa_export {
-  my ($self, $sepa_export, %params) = @_;
+  my ($sepa_export, %params) = @_;
 
   $params{display} ||= 'inline';
 
   croak "Unknown display type '$params{display}'" unless $params{display} =~ m/^(?:inline|table-cell)$/;
 
   my $text = join '', (
-    $params{no_link} ? '' : '<a href="sepa.pl?action=bank_transfer_edit&amp;vc=' . $self->escape($sepa_export->vc) . '&amp;id=' . $self->escape($sepa_export->id) . '">',
-    $self->escape($sepa_export->id),
+    $params{no_link} ? '' : '<a href="sepa.pl?action=bank_transfer_edit&amp;vc=' . escape($sepa_export->vc) . '&amp;id=' . escape($sepa_export->id) . '">',
+    escape($sepa_export->id),
     $params{no_link} ? '' : '</a>',
   );
-  return $self->escaped_text($text);
+  is_escaped($text);
 }
 
 1;
diff --git a/SL/Presenter/ShopOrder.pm b/SL/Presenter/ShopOrder.pm
new file mode 100644 (file)
index 0000000..9c0bfee
--- /dev/null
@@ -0,0 +1,27 @@
+package SL::Presenter::ShopOrder;
+
+use strict;
+
+use SL::Presenter::EscapedText qw(escape is_escaped);
+
+use Exporter qw(import);
+our @EXPORT_OK = qw(shop_order);
+
+use Carp;
+
+sub shop_order {
+  my ($shop_order, $type, %params) = @_;
+
+  $params{display} ||= 'inline';
+
+  croak "Unknown display type '$params{display}'" unless $params{display} =~ m/^(?:inline|table-cell)$/;
+
+  my $text = join '', (
+    $params{no_link} ? '' : '<a href="controller.pl?action=ShopOrder/show&amp;id='. escape($shop_order->id) .'">',
+    escape($shop_order->shop_ordernumber),
+    $params{no_link} ? '' : '</a>',
+  );
+
+  is_escaped($text);
+}
+1;
diff --git a/SL/Presenter/Simple.pm b/SL/Presenter/Simple.pm
new file mode 100644 (file)
index 0000000..0bd7cb4
--- /dev/null
@@ -0,0 +1,10 @@
+package SL::Presenter::Simple;
+
+use strict;
+
+use SL::Presenter::Tag qw(:ALL);
+use SL::Presenter::Text qw(:ALL);
+use SL::Presenter::EscapedText qw(:ALL);
+
+1;
+
index 7ee8531..a140fe4 100644 (file)
@@ -3,17 +3,27 @@ package SL::Presenter::Tag;
 use strict;
 
 use SL::HTML::Restrict;
-
-use parent qw(Exporter);
+use SL::Presenter::EscapedText qw(escape);
+use Scalar::Util qw(blessed);
 
 use Exporter qw(import);
-our @EXPORT = qw(html_tag input_tag man_days_tag name_to_id select_tag stringify_attributes restricted_html);
+our @EXPORT_OK = qw(
+  html_tag input_tag hidden_tag javascript man_days_tag name_to_id select_tag
+  checkbox_tag button_tag submit_tag ajax_submit_tag input_number_tag
+  stringify_attributes textarea_tag link_tag date_tag
+  div_tag radio_button_tag img_tag);
+our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
 
 use Carp;
 
 my %_valueless_attributes = map { $_ => 1 } qw(
   checked compact declare defer disabled ismap multiple noresize noshade nowrap
-  readonly selected
+  readonly selected hidden
+);
+
+my %_singleton_tags = map { $_ => 1 } qw(
+  area base br col command embed hr img input keygen link meta param source
+  track wbr
 );
 
 sub _call_on {
@@ -30,54 +40,74 @@ sub _id {
 }
 }
 
+sub _J {
+  my $string = shift;
+  $string    =~ s/(\"|\'|\\)/\\$1/g;
+  return $string;
+}
+
+sub join_values {
+  my ($name, $value) = @_;
+  my $spacer = $name eq 'class' ? ' ' : ''; # join classes with spaces, everything else as is
+
+  ref $value && 'ARRAY' eq ref $value
+  ? join $spacer, map { join_values($name, $_) } @$value
+  : $value
+}
 
 sub stringify_attributes {
-  my ($self, %params) = @_;
+  my (%params) = @_;
 
   my @result = ();
   while (my ($name, $value) = each %params) {
     next unless $name;
     next if $_valueless_attributes{$name} && !$value;
     $value = '' if !defined($value);
-    push @result, $_valueless_attributes{$name} ? $self->escape($name) : $self->escape($name) . '="' . $self->escape($value) . '"';
+    $value = join_values($name, $value) if ref $value && 'ARRAY' eq ref $value;
+    push @result, $_valueless_attributes{$name} ? escape($name) : escape($name) . '="' . escape($value) . '"';
   }
 
   return @result ? ' ' . join(' ', @result) : '';
 }
 
 sub html_tag {
-  my ($self, $tag, $content, %params) = @_;
-  my $attributes = $self->stringify_attributes(%params);
+  my ($tag, $content, %params) = @_;
+  my $attributes = stringify_attributes(%params);
 
-  return "<${tag}${attributes}>" unless defined($content);
+  return "<${tag}${attributes}>" if !defined($content) && $_singleton_tags{$tag};
   return "<${tag}${attributes}>${content}</${tag}>";
 }
 
 sub input_tag {
-  my ($self, $name, $value, %attributes) = @_;
+  my ($name, $value, %attributes) = @_;
 
   _set_id_attribute(\%attributes, $name);
   $attributes{type} ||= 'text';
 
-  return $self->html_tag('input', undef, %attributes, name => $name, value => $value);
+  html_tag('input', undef, %attributes, name => $name, value => $value);
+}
+
+sub hidden_tag {
+  my ($name, $value, %attributes) = @_;
+  input_tag($name, $value, %attributes, type => 'hidden');
 }
 
 sub man_days_tag {
-  my ($self, $name, $object, %attributes) = @_;
+  my ($name, $object, %attributes) = @_;
 
   my $size           =  delete($attributes{size})   || 5;
   my $method         =  $name;
   $method            =~ s/^.*\.//;
 
-  my $time_selection =  $self->input_tag( "${name}_as_man_days_string", _call_on($object, "${method}_as_man_days_string"), %attributes, size => $size);
-  my $unit_selection =  $self->select_tag("${name}_as_man_days_unit",   [[ 'h', $::locale->text('h') ], [ 'man_day', $::locale->text('MD') ]],
+  my $time_selection = input_tag("${name}_as_man_days_string", _call_on($object, "${method}_as_man_days_string"), %attributes, size => $size);
+  my $unit_selection = select_tag("${name}_as_man_days_unit",   [[ 'h', $::locale->text('h') ], [ 'man_day', $::locale->text('MD') ]],
                                           %attributes, default => _call_on($object, "${method}_as_man_days_unit"));
 
   return $time_selection . $unit_selection;
 }
 
 sub name_to_id {
-  my ($self, $name) = @_;
+  my ($name) = @_;
 
   $name =~ s/\[\+?\]/ _id() /ge; # give constructs with [] or [+] unique ids
   $name =~ s/[^\w_]/_/g;
@@ -87,10 +117,14 @@ sub name_to_id {
 }
 
 sub select_tag {
-  my ($self, $name, $collection, %attributes) = @_;
+  my ($name, $collection, %attributes) = @_;
 
   _set_id_attribute(\%attributes, $name);
 
+  $collection         = [] if defined($collection) && !ref($collection) && ($collection eq '');
+
+  my $with_filter     = delete($attributes{with_filter});
+  my $fil_placeholder = delete($attributes{filter_placeholder});
   my $value_key       = delete($attributes{value_key})   || 'id';
   my $title_key       = delete($attributes{title_key})   || $value_key;
   my $default_key     = delete($attributes{default_key}) || 'selected';
@@ -170,11 +204,11 @@ sub select_tag {
       push(@options, [$value, $title, $selected{$value} || $default]);
     }
 
-    return join '', map { $self->html_tag('option', $self->escape($_->[1]), value => $_->[0], selected => $_->[2]) } @options;
+    return join '', map { html_tag('option', escape($_->[1]), value => $_->[0], selected => $_->[2]) } @options;
   };
 
   my $code  = '';
-  $code    .= $self->html_tag('option', $self->escape($empty_title || ''), value => '') if $with_empty;
+  $code    .= html_tag('option', escape($empty_title || ''), value => '') if $with_empty;
 
   if (!$with_optgroups) {
     $code .= $list_to_code->($collection);
@@ -182,31 +216,212 @@ sub select_tag {
   } else {
     $code .= join '', map {
       my ($optgroup_title, $sub_collection) = @{ $_ };
-      $self->html_tag('optgroup', $list_to_code->($sub_collection), label => $optgroup_title)
+      html_tag('optgroup', $list_to_code->($sub_collection), label => $optgroup_title)
     } @{ $collection };
   }
 
-  return $self->html_tag('select', $code, %attributes, name => $name);
+  my $select_html = html_tag('select', $code, %attributes, name => $name);
+
+  if ($with_filter) {
+    my $input_style;
+
+    if (($attributes{style} // '') =~ m{width: *(\d+) *px}i) {
+      $input_style = "width: " . ($1 - 22) . "px";
+    }
+
+    my $input_html = html_tag(
+      'input', undef,
+      autocomplete     => 'off',
+      type             => 'text',
+      id               => $attributes{id} . '_filter',
+      'data-select-id' => $attributes{id},
+      (placeholder     => $fil_placeholder) x !!$fil_placeholder,
+      (style           => $input_style)     x !!$input_style,
+    );
+    $select_html = html_tag('div', $input_html . $select_html, class => "filtered_select");
+  }
+
+  return $select_html;
+}
+
+sub checkbox_tag {
+  my ($name, %attributes) = @_;
+
+  my %label_attributes = map { (substr($_, 6) => $attributes{$_}) } grep { m{^label_} } keys %attributes;
+  delete @attributes{grep { m{^label_} } keys %attributes};
+
+  _set_id_attribute(\%attributes, $name);
+
+  $attributes{value}   = 1 unless defined $attributes{value};
+  my $label            = delete $attributes{label};
+  my $checkall         = delete $attributes{checkall};
+  my $for_submit       = delete $attributes{for_submit};
+
+  if ($attributes{checked}) {
+    $attributes{checked} = 'checked';
+  } else {
+    delete $attributes{checked};
+  }
+
+  my $code  = '';
+  $code    .= hidden_tag($name, 0, %attributes, id => $attributes{id} . '_hidden') if $for_submit;
+  $code    .= html_tag('input', undef,  %attributes, name => $name, type => 'checkbox');
+  $code    .= html_tag('label', $label, for => $attributes{id}, %label_attributes) if $label;
+  $code    .= javascript(qq|\$('#$attributes{id}').checkall('$checkall');|) if $checkall;
+
+  return $code;
+}
+
+sub radio_button_tag {
+  my ($name, %attributes) = @_;
+
+  my %label_attributes = map { (substr($_, 6) => $attributes{$_}) } grep { m{^label_} } keys %attributes;
+  delete @attributes{grep { m{^label_} } keys %attributes};
+
+  $attributes{value}   = 1 unless exists $attributes{value};
+
+  _set_id_attribute(\%attributes, $name, 1);
+  my $label            = delete $attributes{label};
+
+  _set_id_attribute(\%attributes, $name . '_' . $attributes{value});
+
+  if ($attributes{checked}) {
+    $attributes{checked} = 'checked';
+  } else {
+    delete $attributes{checked};
+  }
+
+  my $code  = html_tag('input', undef,  %attributes, name => $name, type => 'radio');
+  $code    .= html_tag('label', $label, for => $attributes{id}, %label_attributes) if $label;
+
+  return $code;
+}
+
+sub button_tag {
+  my ($onclick, $value, %attributes) = @_;
+
+  _set_id_attribute(\%attributes, $attributes{name}) if $attributes{name};
+  $attributes{type} ||= 'button';
+
+  $onclick = 'if (!confirm("'. _J(delete($attributes{confirm})) .'")) return false; ' . $onclick if $attributes{confirm};
+
+  html_tag('input', undef, %attributes, value => $value, (onclick => $onclick)x!!$onclick);
+}
+
+sub submit_tag {
+  my ($name, $value, %attributes) = @_;
+
+  _set_id_attribute(\%attributes, $attributes{name}) if $attributes{name};
+
+  if ( $attributes{confirm} ) {
+    $attributes{onclick} = 'return confirm("'. _J(delete($attributes{confirm})) .'");';
+  }
+
+  input_tag($name, $value, %attributes, type => 'submit', class => 'submit');
+}
+
+sub ajax_submit_tag {
+  my ($url, $form_selector, $text, %attributes) = @_;
+
+  $url           = _J($url);
+  $form_selector = _J($form_selector);
+  my $onclick    = qq|kivi.submit_ajax_form('${url}', '${form_selector}')|;
+
+  button_tag($onclick, $text, %attributes);
+}
+
+sub input_number_tag {
+  my ($name, $value, %params) = @_;
+
+  _set_id_attribute(\%params, $name);
+  my @onchange = $params{onchange} ? (onChange => delete $params{onchange}) : ();
+  my @classes  = ('numeric');
+  push @classes, delete($params{class}) if $params{class};
+  my %class    = @classes ? (class => join(' ', @classes)) : ();
+
+  $::request->layout->add_javascripts('kivi.Validator.js');
+  $::request->presenter->need_reinit_widgets($params{id});
+
+  input_tag(
+    $name, $::form->format_amount(\%::myconfig, $value, $params{precision}),
+    "data-validate" => "number",
+    %params,
+    %class, @onchange,
+  );
+}
+
+
+sub javascript {
+  my ($data) = @_;
+  html_tag('script', $data, type => 'text/javascript');
 }
 
 sub _set_id_attribute {
   my ($attributes, $name, $unique) = @_;
 
   if (!delete($attributes->{no_id}) && !$attributes->{id}) {
-    $attributes->{id}  = name_to_id(undef, $name);
+    $attributes->{id}  = name_to_id($name);
     $attributes->{id} .= '_' . $attributes->{value} if $unique;
   }
 
-  return %{ $attributes };
+  %{ $attributes };
 }
 
 my $html_restricter;
 
-sub restricted_html {
-  my ($self, $value) = @_;
+sub textarea_tag {
+  my ($name, $content, %attributes) = @_;
+
+  _set_id_attribute(\%attributes, $name);
+  $attributes{rows}  *= 1; # required by standard
+  $attributes{cols}  *= 1; # required by standard
+
+  html_tag('textarea', $content, %attributes, name => $name);
+}
+
+sub link_tag {
+  my ($href, $content, %params) = @_;
+
+  $href ||= '#';
+
+  html_tag('a', $content, %params, href => $href);
+}
+# alias for compatibility
+sub link { goto &link_tag }
+
+sub date_tag {
+  my ($name, $value, %params) = @_;
+
+  _set_id_attribute(\%params, $name);
+  my @onchange = $params{onchange} ? (onChange => delete $params{onchange}) : ();
+  my @classes  = $params{no_cal} || $params{readonly} ? () : ('datepicker');
+  push @classes, delete($params{class}) if $params{class};
+  my %class    = @classes ? (class => join(' ', @classes)) : ();
+
+  $::request->layout->add_javascripts('kivi.Validator.js');
+  $::request->presenter->need_reinit_widgets($params{id});
+
+  $params{'data-validate'} = join(' ', "date", grep { $_ } (delete $params{'data-validate'}));
+
+  input_tag(
+    $name, blessed($value) ? $value->to_lxoffice : $value,
+    size   => 11,
+    %params,
+    %class, @onchange,
+  );
+}
+
+sub div_tag {
+  my ($content, %params) = @_;
+  return html_tag('div', $content, %params);
+}
+
+sub img_tag {
+  my (%params) = @_;
+
+  $params{alt} ||= '';
 
-  $html_restricter ||= SL::HTML::Restrict->create;
-  return $html_restricter->process($value);
+  return html_tag('img', undef, %params);
 }
 
 1;
@@ -278,10 +493,6 @@ Creates a string from all elements in C<%items> suitable for usage as
 HTML tag attributes. Keys and values are HTML escaped even though keys
 must not contain non-ASCII characters for browsers to accept them.
 
-=item C<restricted_html $html>
-
-Returns HTML stripped of unknown tags. See L<SL::HTML::Restrict>.
-
 =back
 
 =head2 HIGH-LEVEL FUNCTIONS
@@ -294,6 +505,36 @@ Creates a HTML 'input type=text' tag named C<$name> with the value
 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
 tag's C<id> defaults to C<name_to_id($name)>.
 
+=item C<submit_tag $name, $value, %attributes>
+
+Creates a HTML 'input type=submit class=submit' tag named C<$name> with the
+value C<$value> and with arbitrary HTML attributes from C<%attributes>. The
+tag's C<id> defaults to C<name_to_id($name)>.
+
+If C<$attributes{confirm}> is set then a JavaScript popup dialog will
+be added via the C<onclick> handler asking the question given with
+C<$attributes{confirm}>. The request is only submitted if the user
+clicks the dialog's ok/yes button.
+
+=item C<ajax_submit_tag $url, $form_selector, $text, %attributes>
+
+Creates a HTML 'input type="button"' tag with a very specific onclick
+handler that submits the form given by the jQuery selector
+C<$form_selector> to the URL C<$url> (the actual JavaScript function
+called for that is C<kivi.submit_ajax_form()> in
+C<js/client_js.js>). The button's label will be C<$text>.
+
+=item C<button_tag $onclick, $text, %attributes>
+
+Creates a HTML 'input type="button"' tag with an onclick handler
+C<$onclick> and a value of C<$text>. The button does not have a name
+nor an ID by default.
+
+If C<$attributes{confirm}> is set then a JavaScript popup dialog will
+be prepended to the C<$onclick> handler asking the question given with
+C<$attributes{confirm}>. The request is only submitted if the user
+clicks the dialog's "ok/yes" button.
+
 =item C<man_days_tag $name, $object, %attributes>
 
 Creates two HTML inputs: a text input for entering a number and a drop
@@ -314,6 +555,42 @@ makes it possible to write statements like e.g.
 The attribute C<size> can be used to set the text input's size. It
 defaults to 5.
 
+=item C<hidden_tag $name, $value, %attributes>
+
+Creates a HTML 'input type=hidden' tag named C<$name> with the value
+C<$value> and with arbitrary HTML attributes from C<%attributes>. The
+tag's C<id> defaults to C<name_to_id($name)>.
+
+=item C<checkbox_tag $name, %attributes>
+
+Creates a HTML 'input type=checkbox' tag named C<$name> with arbitrary
+HTML attributes from C<%attributes>. The tag's C<id> defaults to
+C<name_to_id($name)>. The tag's C<value> defaults to C<1>.
+
+If C<%attributes> contains a key C<label> then a HTML 'label' tag is
+created with said C<label>. No attribute named C<label> is created in
+that case. Furthermore, all attributes whose names start with
+C<label_> become attributes on the label tag without the C<label_>
+prefix. For example, C<label_style='#ff0000'> will be turned into
+C<style='#ff0000'> on the label tag, causing the text to become red.
+
+If C<%attributes> contains a key C<checkall> then the value is taken as a
+JQuery selector and clicking this checkbox will also toggle all checkboxes
+matching the selector.
+
+=item C<radio_button_tag $name, %attributes>
+
+Creates a HTML 'input type=radio' tag named C<$name> with arbitrary
+HTML attributes from C<%attributes>. The tag's C<value> defaults to
+C<1>. The tag's C<id> defaults to C<name_to_id($name . "_" . $value)>.
+
+If C<%attributes> contains a key C<label> then a HTML 'label' tag is
+created with said C<label>. No attribute named C<label> is created in
+that case. Furthermore, all attributes whose names start with
+C<label_> become attributes on the label tag without the C<label_>
+prefix. For example, C<label_style='#ff0000'> will be turned into
+C<style='#ff0000'> on the label tag, causing the text to become red.
+
 =item C<select_tag $name, \@collection, %attributes>
 
 Creates an HTML 'select' tag named C<$name> with the contents of one
index af37323..7d56d94 100644 (file)
@@ -2,21 +2,26 @@ package SL::Presenter::Text;
 
 use strict;
 
-use parent qw(Exporter);
+use SL::Presenter::EscapedText qw(escape);
+use SL::HTML::Restrict;
+use SL::HTML::Util;
 
 use Exporter qw(import);
-our @EXPORT = qw(format_man_days simple_format truncate);
+our @EXPORT_OK = qw(format_man_days simple_format truncate restricted_html);
+our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
 
 use Carp;
 
+my $html_cleaner;
+
 sub truncate {
-  my ($self, $text, %params) = @_;
+  my ($text, %params) = @_;
 
-  return Common::truncate($text, %params);
+  escape(Common::truncate($text, %params));
 }
 
 sub simple_format {
-  my ($self, $text, %params) = @_;
+  my ($text, %params) = @_;
 
   $text =  $::locale->quote_special_chars('HTML', $text || '');
 
@@ -28,18 +33,29 @@ sub simple_format {
 }
 
 sub format_man_days {
-  my ($self, $value, %params) = @_;
+  my ($value, %params) = @_;
 
   return '---' if $params{skip_zero} && !$value;
 
-  return $self->escape($::locale->text('#1 h', $::form->format_amount(\%::myconfig, $value, 2))) if 8.0 > $value;
+  return escape($::locale->text('#1 h', $::form->format_amount(\%::myconfig, $value, 2))) if 8.0 > $value;
 
   $value     /= 8.0;
   my $output  = $::locale->text('#1 MD', int($value));
   my $rest    = ($value - int($value)) * 8.0;
   $output    .= ' ' . $::locale->text('#1 h', $::form->format_amount(\%::myconfig, $rest)) if $rest > 0.0;
 
-  return $self->escape($output);
+  escape($output);
+}
+
+sub restricted_html {
+  my ($value) = @_;
+  $html_cleaner //= SL::HTML::Restrict->create;
+  return $html_cleaner->process($value);
+}
+
+sub stripped_html {
+  my ($value) = @_;
+  return SL::HTML::Util::strip($value);
 }
 
 1;
@@ -55,8 +71,10 @@ SL::Presenter::Text - Presenter module for assorted text helpers
 
 =head1 SYNOPSIS
 
+  use  SL::Presenter::Text qw(truncate);
+
   my $long_text = "This is very, very long. Need shorter, surely.";
-  my $truncated = $::request->presenter->truncate($long_text, at => 10);
+  my $truncated = truncate($long_text, at => 10);
   # Result: "This is..."
 
 =head1 FUNCTIONS
@@ -86,6 +104,16 @@ paragraph change: they close the current paragraph tag and start a new
 one. Single newlines are converted to line breaks. Carriage returns
 are removed.
 
+=item C<restricted_html $unsafe_html>
+
+Returns HTML code stripped from unwanted/unsupported content. This is
+done via the module L<SL::HTML::Restrict>.
+
+=item C<stripped_html $html>
+
+Returns the raw text with all HTML tags and comments stripped. This is
+done via L<SL::HTML::Util/strip>.
+
 =back
 
 =head1 BUGS
diff --git a/SL/Presenter/WebdavObject.pm b/SL/Presenter/WebdavObject.pm
new file mode 100644 (file)
index 0000000..7318f0c
--- /dev/null
@@ -0,0 +1,79 @@
+package SL::Presenter::WebdavObject;
+
+use strict;
+
+use SL::Presenter::Tag         qw(link_tag);
+use SL::Presenter::EscapedText qw(escape is_escaped);
+
+use Exporter qw(import);
+our @EXPORT_OK = qw(webdav_object);
+
+use Carp;
+
+sub webdav_object {
+  my ($webdav_object, %params) = @_;
+
+
+  my $text = escape($webdav_object->filename);
+  if (! delete $params{no_link}) {
+    my $href  = SL::Presenter::EscapedText::escape($webdav_object->full_filedescriptor);
+    $text     = link_tag($href, $text, %params);
+  }
+
+  is_escaped($text);
+}
+
+1;
+
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Presenter::WebdavObject - Presenter module for SL::Webdav::Object(s).
+
+=head1 SYNOPSIS
+
+  my $webdav = SL::Webdav->new(
+    type     => 'sales_order',
+    number   => '1234',
+  );
+  my @all_objects = $webdav->get_all_objects;
+  my $html        = SL::Presenter::WebdavObject::webdav_object($all_objects[0], no_link => 1);
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<webdav_object $webdav_object, %params>
+
+Returns a rendered version (actually an instance of
+L<SL::Presenter::EscapedText>) of the webdav object
+C<$webdav_object>.
+
+C<%params> can include:
+
+=over 2
+
+=item * no_link
+
+If falsish (the default) then the file name of the object will be linked
+to the download path for that file.
+
+=back
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
index 66daad2..05168ce 100644 (file)
@@ -3,8 +3,14 @@ package SL::PriceSource;
 use strict;
 use parent 'SL::DB::Object';
 use Rose::Object::MakeMethods::Generic (
-  scalar => [ qw(record_item record) ],
-  'array --get_set_init' => [ qw(all_price_sources) ],
+  scalar => [ qw(record_item record fast) ],
+  'scalar --get_set_init' => [ qw(
+    best_price best_discount
+  ) ],
+  'array --get_set_init' => [ qw(
+    all_price_sources
+    available_prices available_discounts
+  ) ],
 );
 
 use List::UtilsBy qw(min_by max_by);
@@ -16,46 +22,62 @@ sub init_all_price_sources {
   my ($self) = @_;
 
   [ map {
-    $_->new(record_item => $self->record_item, record => $self->record)
+    $self->price_source_by_class($_);
   } SL::PriceSource::ALL->all_enabled_price_sources ]
 }
 
+sub price_source_by_class {
+  my ($self, $class) = @_;
+  return unless $class;
+
+  $self->{price_source_by_name}{$class} //=
+    $class->new(record_item => $self->record_item, record => $self->record, fast => $self->fast);
+}
+
 sub price_from_source {
   my ($self, $source) = @_;
-  my ($source_name, $spec) = split m{/}, $source, 2;
+  return empty_price() if !$source;
 
-  my $class = SL::PriceSource::ALL->price_source_class_by_name($source_name);
+  ${ $self->{price_from_source} //= {} }{$source} //= do {
+    my ($source_name, $spec) = split m{/}, $source, 2;
+    my $class = SL::PriceSource::ALL->price_source_class_by_name($source_name);
+    my $source_object = $self->price_source_by_class($class);
 
-  return $class
-    ? $class->new(record_item => $self->record_item, record => $self->record)->price_from_source($source, $spec)
-    : empty_price();
+    $source_object
+      ? $source_object->price_from_source($source, $spec)
+      : empty_price();
+  }
 }
 
 sub discount_from_source {
   my ($self, $source) = @_;
-  my ($source_name, $spec) = split m{/}, $source, 2;
+  return empty_discount() if !$source;
 
-  my $class = SL::PriceSource::ALL->price_source_class_by_name($source_name);
+  ${ $self->{discount_from_source} //= {} }{$source} //= do {
+    my ($source_name, $spec) = split m{/}, $source, 2;
+    my $class = SL::PriceSource::ALL->price_source_class_by_name($source_name);
+    my $source_object = $self->price_source_by_class($class);
 
-  return $class
-    ? $class->new(record_item => $self->record_item, record => $self->record)->discount_from_source($source, $spec)
-    : empty_discount();
+    $source_object
+      ? $source_object->discount_from_source($source, $spec)
+      : empty_discount();
+  }
 }
 
-sub available_prices {
-  map { $_->available_prices } $_[0]->all_price_sources;
+sub init_available_prices {
+  [ map { $_->available_prices } $_[0]->all_price_sources ];
 }
 
-sub available_discounts {
-  return if $_[0]->record_item->part->not_discountable;
-  map { $_->available_discounts } $_[0]->all_price_sources;
+sub init_available_discounts {
+  return [] if $_[0]->record_item->part->not_discountable;
+  [ map { $_->available_discounts } $_[0]->all_price_sources ];
 }
 
-sub best_price {
+sub init_best_price {
   min_by { $_->price } max_by { $_->priority } grep { $_->price > 0 } grep { $_ } map { $_->best_price } $_[0]->all_price_sources;
 }
 
-sub best_discount {
+sub init_best_discount {
   max_by { $_->discount } max_by { $_->priority } grep { $_->discount } grep { $_ } map { $_->best_discount } $_[0]->all_price_sources;
 }
 
@@ -92,43 +114,77 @@ part, customer, vendor, date, quantity etc, which was previously not possible.
 =head1 BACKGROUND AND PHILOSOPHY
 
 sql ledger and subsequently Lx-Office had three prices per part: sellprice,
-listprice and lastcost. At the moment a part is loaded into a record, the
-applicable price is copied and after that it is free to be changed.
+listprice and lastcost. When adding an item to a record, the applicable price
+was copied and after that it was free to be changed.
 
 Later on additional things were added. Various types of discount, vendor pricelists
-and the infamous price groups. The problem is not that those didn't work, the
-problem is, that they had to guess too much when to change a price with the
+and the infamous price groups. The problem was not that those didn't work, the
+problem was they had to guess too much when to change a price with the
 available price from the database, and when to leave the user entered price.
 
+The result was that the price of an item in a record seemed to change on a
+whim, and the origin of the price itself being opaque.
+
 Unrelated to that, users asked for more ways to store special prices, based on
 qty (block pricing, bulk discount), based on date (special offers), based on
 customers (special terms), up to full blown calculation modules.
 
 On a third front sales personnel asked for ways to see what price options a
-position in a quotation has, and wanted information available when a price
-offer changed.
+position in a quotation has, and wanted information available when prices
+changed to make better informed choices about sales later in the workflow.
 
-Price sources put that together by making some compromises:
+Price sources now extend the previous pricing by attaching a source to every
+price in records. The information it provides are:
 
 =over 4
 
 =item 1.
 
-Only change the price on creation of a position or when asked to.
+Where did this price originate?
 
 =item 2.
 
-Either set the price from a price source and let it be read only, or use a free
-price.
+If this price would be calculated today, is it still the same as it was when
+this record was created?
 
 =item 3.
 
-Save the origin of each price with the record so that the calculation can be
-reproduced.
+If I want to price an item in this record now, which prices are available?
 
 =item 4.
 
-Make price calculation flexible and pluggable.
+Which one is the "best"?
+
+=back
+
+=head1 GUARANTEES
+
+To ensure price source prices are comprehensible and reproducible, some
+invariants are guaranteed:
+
+=over 4
+
+=item 1.
+
+Price sources will never on their own change a price. They will offer options,
+and it is up to the user to change a price.
+
+=item 2.
+
+If a price is set from a source then the system will try to prevent the user
+from messing it up. By default this means the price will be read-only.
+Implementations can choose to make prices editable, but even then deviations
+from the calculatied price will be marked.
+
+A price that is not set from a source will not have any of this.
+
+=item 3.
+
+A price should be able to repeat the calculations done to arrive at the price
+when it was first used. If these calculations are no longer applicable (special
+offer expired) this should be signalled. If the calculations result in a
+different price, this should be signalled. If the calculations fail (needed
+information is no longer present) this must be signalled.
 
 =back
 
@@ -137,8 +193,20 @@ without their explicit consent, eliminating all problems originating from
 trying to be smart. The second and third one ensure that later on the
 calculation can be repeated so that invalid prices can be caught (because for
 example the special offer is no longer valid), and so that sales personnel have
-information about rising or falling prices. The fourth point ensures that
-insular calculation processes can be developed independent of the core code.
+information about rising or falling prices.
+
+=head1 STRUCTURE
+
+Price sources are managed by this package (L<SL::PriceSource>), and all
+external access should be by using its interface.
+
+Each source is an instance of L<SL::PriceSource::Base> and the available
+implementations are recorded in L<SL::PriceSource::ALL>. Prices and discounts
+returned by interface methods are instances of L<SL::PriceSource::Price> and
+L<SL::PriceSource::Discount>.
+
+Returned prices and discounts should be checked for entries in C<invalid> and
+C<missing>, see documentation in their classes.
 
 =head1 INTERFACE METHODS
 
@@ -167,24 +235,37 @@ Returns all available discounts.
 
 =item C<best_price>
 
-Attempts to get the best available price. returns L<empty_price> if no price is found.
+Attempts to get the best available price. returns L<empty_price> if no price is
+found.
 
 =item C<best_discount>
 
-Attempts to get the best available discount. returns L<empty_discount> if no discount is found.
+Attempts to get the best available discount. returns L<empty_discount> if no
+discount is found.
 
 =item C<empty_price>
 
-A special empty price, that does not change the previously entered price, and
+A special empty price that does not change the previously entered price and
 opens the price field to manual changes.
 
 =item C<empty_discount>
 
-A special empty discount, that does not change the previously entered discount, and
-opens the discount field to manual changes.
+A special empty discount that does not change the previously entered discount
+and opens the discount field to manual changes.
+
+=item C<fast>
+
+If set to true, indicates that calls may skip doing intensive work and instead
+return a price or discount flagged as unknown. The caller must be prepared to
+deal with those.
+
+Typically this is intended to delay expensive calculations until they can be
+done in a second batch pass. If the information is already present, it is still
+encouraged that implementations return the correct values.
 
 =back
 
+
 =head1 SEE ALSO
 
 L<SL::PriceSource::Base>,
@@ -198,16 +279,37 @@ L<SL::PriceSource::ALL>
 
 =item *
 
-The current simple model of price sources providing a simple value in simple
-cases doesn't work well in situations where prices are modified by other
-properties. The same problem also causes headaches when trying to use price
-sources to compute positions in assemblies.
+The current model of price sources requires a record and a record_item for
+every price calculation. This means that price structures can never be used
+when no record is available, such as calculation the worth of assembly rows.
+
+A possible solution is to either split price sources into simple and complex
+ones (where the former do not require records).
+
+Another would be to have default values for the input normally taken from
+records (like qty defaulting to 1).
+
+A last one would be to provide an alternative input channel for needed
+properties.
+
+=item *
+
+Discount sources were implemented as a copy of the prices with slightly
+different semantics. Need to do a real design. A requirement is, that a single
+source can provide both prices and discounts (needed for price_rules).
 
-The solution should be to split price sources in simple ones, which do not
-manage their interactions with record_items, but can be used in contexts
-without record_items, and complex ones which do, but have to be fed a dummy
-record_item. For the former there should be a wrapper that handles interactions
-with units, price_factors etc..
+=item *
+
+Priorities are implemented ad hoc. The semantics which are chosen by the "best"
+accessors are unintuitive because they do not guarantee anything. Better
+terminology might help.
+
+=item *
+
+It is currently not possible to link a price to the price of the generating
+record_item (i.e. the price of a delivery order item to the order item it was
+generated from). This is crucial to enterprises that calculate all their prices
+in orders, and update those after they made delivery orders.
 
 =item *
 
@@ -215,6 +317,57 @@ Currently it is only possible to provide additional prices, but not to restrict
 prices. Potential scenarios include credit limit customers which do not receive
 benefits from sales, or general ALLOW, DENY order calculation.
 
+=item *
+
+Composing price sources is disallowed for clarity, but all price sources need
+to be aware of units and price_factors. This is madness.
+
+=item *
+
+The current implementation of lastcost is useless. Since it's one of the
+master_data prices it will always compete with listprice. But in real scenarios
+the listprice tends to go up, while lastcost stays the same, so lastcost
+usually wins. Lastcost could be lower priority, but a better design would be
+nice.
+
+=item *
+
+Guarantee 1 states that price sources will never change prices on their own.
+Further testing in the wild has shown that this is desirable within a record,
+but not when copying items from one record to another within a workflow.
+
+Specifically when changing from sales to purchase records prices don't make
+sense anymore. The guarantees should be updated to reflect this and
+transposition guidelines should be documented.
+
+The previously mentioned linked prices can emulated by allowing price sources
+to set a new price when changing to a new record in the workflow. The decision
+about whether a price is eligable to be set can be suggested by the price
+source implementation but is ultimately up to the surrounding framework, which
+can make this configurable.
+
+=item *
+
+Prices were originally planned as a context element rather than a modal popup.
+It would be great to have this now with better framework.
+
+=item *
+
+Large records (30 positions or more) in combination with complicated price
+sources run into n+1 problems. There should be an extra hook that allows price
+source implementations to make bulk calculations before the actual position loop.
+
+=item *
+
+Prices have defined information channels for missing and invalid, but it would
+be deriable to have more information flow. For example a limited offer might
+expire in three days while the record is valid for 20 days. THis mismatch is
+impossible to resolve automatically, but informing the user about it would be a
+nice thing.
+
+This can also extend to diagnostics on class level, where implementations can
+call attention to likely misconfigurations.
+
 =back
 
 =head1 AUTHOR
index dd6ee2f..e7c553e 100644 (file)
@@ -4,6 +4,7 @@ use strict;
 use SL::PriceSource::Pricegroup;
 use SL::PriceSource::MasterData;
 use SL::PriceSource::Makemodel;
+use SL::PriceSource::CustomerPrice;
 use SL::PriceSource::Customer;
 use SL::PriceSource::Vendor;
 use SL::PriceSource::Business;
@@ -15,6 +16,7 @@ my %price_sources_by_name = (
   vendor_discount   => 'SL::PriceSource::Vendor',
   pricegroup        => 'SL::PriceSource::Pricegroup',
   makemodel         => 'SL::PriceSource::Makemodel',
+  customerprice     => 'SL::PriceSource::CustomerPrice',
   business          => 'SL::PriceSource::Business',
   price_rules       => 'SL::PriceSource::PriceRules',
 );
@@ -25,6 +27,7 @@ my @price_sources_order = qw(
   vendor_discount
   pricegroup
   makemodel
+  customerprice
   business
   price_rules
 );
index 1e8f20d..8cc3888 100644 (file)
@@ -4,7 +4,7 @@ use strict;
 
 use parent qw(SL::DB::Object);
 use Rose::Object::MakeMethods::Generic (
-  scalar => [ qw(record_item record) ],
+  scalar => [ qw(record_item record fast) ],
 );
 
 sub name { die 'name needs to be implemented' }
@@ -164,7 +164,7 @@ Note that constraints from the rest of the C<record> do not apply anymore. If
 information needed for the retrieval can be deleted elsewhere, then you must
 guard against that.
 
-If the price for the same coditions changed, return the new price. It will be
+If the price for the same conditions changed, return the new price. It will be
 presented as an option to the user if the record is still editable.
 
 If the price is not valid anymore or not reconstructable, return a price with
index c0cfbc0..fe67d91 100644 (file)
@@ -43,7 +43,7 @@ sub discount_from_source {
     )
   }
 
-  if (!$self->record->customer) {
+  if (!$self->record->can('customer') || !$self->record->customer) {
     return SL::PriceSource::Discount->new(
       discount     => $customer->discount,
       spec         => $customer->id,
@@ -59,7 +59,7 @@ sub discount_from_source {
       spec         => $customer->id,
       description  => t8('Customer Discount'),
       price_source => $self,
-      invalid      => t8('This discount is only valid for customer #1', $customer->full_description),
+      invalid      => t8('This discount is only valid for customer #1', $customer->displayable_name),
     )
   }
 
diff --git a/SL/PriceSource/CustomerPrice.pm b/SL/PriceSource/CustomerPrice.pm
new file mode 100644 (file)
index 0000000..666c37c
--- /dev/null
@@ -0,0 +1,62 @@
+package SL::PriceSource::CustomerPrice;
+
+use strict;
+use parent qw(SL::PriceSource::Base);
+
+use SL::PriceSource::Price;
+use SL::Locale::String;
+use SL::DB::PartCustomerPrice;
+# use List::UtilsBy qw(min_by max_by);
+
+sub name { 'customer_price' }
+
+sub description { t8('Customer specific Price') }
+
+sub available_prices {
+  my ($self, %params) = @_;
+
+  return () if !$self->part;
+  return () if !$self->record->is_sales;
+
+  map { $self->make_price_from_customerprice($_) }
+  grep { $_->customer_id == $self->record->customer_id }
+  $self->part->customerprices;
+}
+
+sub available_discounts { }
+
+sub price_from_source {
+  my ($self, $source, $spec) = @_;
+
+  my $customerprice = SL::DB::Manager::PartCustomerPrice->find_by(id => $spec);
+
+  return $self->make_price_from_customerprice($customerprice);
+
+}
+
+sub best_price {
+  my ($self, %params) = @_;
+
+  return () if !$self->record->is_sales;
+
+#  min_by { $_->price } $self->available_prices;
+#  max_by { $_->price } $self->available_prices;
+  &available_prices;
+
+}
+
+sub best_discount { }
+
+sub make_price_from_customerprice {
+  my ($self, $customerprice) = @_;
+
+  return SL::PriceSource::Price->new(
+    price        => $customerprice->price,
+    spec         => $customerprice->id,
+    description  => $customerprice->customer_partnumber,
+    price_source => $self,
+  );
+}
+
+
+1;
index ae4592b..66f9532 100644 (file)
@@ -4,7 +4,7 @@ use strict;
 
 use parent 'SL::DB::Object';
 use Rose::Object::MakeMethods::Generic (
-  scalar => [ qw(discount description spec price_source invalid missing) ],
+  scalar => [ qw(discount description spec price_source invalid missing unknown) ],
   'scalar --get_set_init' => [ qw(priority) ],
 );
 
@@ -133,6 +133,12 @@ discount are no longer valid, and that the discount should be changed.
 
 If discount is missing, you do not need to supply anything except C<source>.
 
+=item C<unknown>
+
+OPTIONAL. Boolean indicator that this discount was not computed for performance
+reasons. This is only valid for PriceSources flagged as C<fast>. This discount
+must be ignored.
+
 =back
 
 =head1 SEE ALSO
index 1305f4b..bf22c99 100644 (file)
@@ -75,6 +75,7 @@ sub make_lastcost {
     spec         => 'lastcost',
     description  => t8('Lastcost'),
     price_source => $self,
+    priority     => 2,
   );
 }
 
index 0ecc8b0..5255847 100644 (file)
@@ -4,8 +4,8 @@ use strict;
 
 use parent 'SL::DB::Object';
 use Rose::Object::MakeMethods::Generic (
-  scalar => [ qw(price description spec price_source invalid missing) ],
-  'scalar --get_set_init' => [ qw(priority) ],
+  scalar => [ qw(price description spec price_source invalid missing unknown) ],
+  'scalar --get_set_init' => [ qw(priority editable) ],
 );
 
 require SL::DB::Helper::Attr;
@@ -43,6 +43,10 @@ sub init_priority {
   3
 }
 
+sub init_editable {
+  0
+}
+
 1;
 
 __END__
@@ -119,6 +123,18 @@ A ref to the creating algorithm.
 OPTIONAL. Prices may supply a numerical priority. Higher will trump lower, even when
 supplying higher prices. Defaults to 3 (as in middle of 1-5).
 
+=item C<editable>
+
+OPTIONAL. Prices may flag themselves as editable. An editable price will still
+be subject to checks for higher or lower prices, but will be rendered in a
+fashion that allows the user to overwrite it.
+
+This is potentially very distracting if the price is usually a default price
+and will be changed in a lot of instances so use with caution.
+
+On the other hand it can provide the capability that users unfamiliar with the
+system will intuitively expect so it can be a good way to introduce the system.
+
 =item C<missing>
 
 OPTIONAL. Both indicator and localized message that the price with this spec
@@ -133,6 +149,12 @@ price are no longer valid, and that the price should be changed.
 
 If price is missing, you do not need to supply anything except C<source>.
 
+=item C<unknown>
+
+OPTIONAL. Boolean indicator that this price was not computed for performance
+reasons. This is only valid for PriceSources flagged as C<fast>. This price
+must be ignored.
+
 =back
 
 =head1 SEE ALSO
index 6408300..3bae2d0 100644 (file)
@@ -20,10 +20,21 @@ sub available_prices {
 
   my $item = $self->record_item;
 
+  my $query = [ parts_id => $item->parts_id, price => { gt => 0 } ];
+
+  # add a pricegroup_filter for obsolete pricegroups, unless part of an
+  # existing pricegroup where that pricegroup was actually used.
+  if ( $self->record->id and $item->active_price_source =~ m/^pricegroup/ ) {
+    my ($pricegroup_id) = $item->active_price_source =~ m/^pricegroup\/(\d+)$/;
+    push(@{$query}, or => [ 'pricegroup.obsolete' => 0, 'pricegroup_id' => $pricegroup_id ]);
+  } else {
+    push(@{$query}, 'pricegroup.obsolete' => 0);
+  }
+
   my $prices = SL::DB::Manager::Price->get_all(
-    query        => [ parts_id => $item->parts_id, price => { gt => 0 } ],
+    query        => $query,
     with_objects => 'pricegroup',
-    order_by     => 'pricegroup.id',
+    sort_by      => 'pricegroup.sortkey',
   );
 
   return () unless @$prices;
@@ -40,7 +51,13 @@ sub price_from_source {
 
   my $price = SL::DB::Manager::Price->find_by(pricegroup_id => $spec, parts_id => $self->part->id);
 
-  # TODO: if someone deletes the prices entry, this fails. add a fallback
+  if (!$price) {
+    return SL::PriceSource::Price->new(
+      price_source => $self,
+      missing      => t8('Could not find an entry for this part in the pricegroup.'),
+    );
+  }
+
   return $self->make_price($price);
 }
 
@@ -54,9 +71,9 @@ sub best_price {
   my @prices    = $self->available_prices;
   my $customer  = $self->record->customer;
 
-  return () if !$customer || !$customer->klass;
+  return () if !$customer || !$customer->pricegroup_id;
 
-  my $best_price = first { $_->spec == $customer->klass } @prices;
+  my $best_price = first { $_->spec == $customer->pricegroup_id } @prices;
 
   return $best_price || ();
 }
index 7eee130..0f0a0cf 100644 (file)
@@ -58,7 +58,7 @@ sub discount_from_source {
       spec         => $vendor->id,
       description  => t8('Vendor Discount'),
       price_source => $self,
-      invalid      => t8('This discount is only valid for vendor #1', $vendor->full_description),
+      invalid      => t8('This discount is only valid for vendor #1', $vendor->displayable_name),
     )
   }
 
index 89904d8..d7d71a3 100644 (file)
--- a/SL/RC.pm
+++ b/SL/RC.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Account reconciliation routines
@@ -35,6 +36,7 @@
 package RC;
 
 use SL::DBUtils;
+use SL::DB;
 
 use strict;
 
@@ -43,8 +45,7 @@ sub paymentaccounts {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $query =
     qq|SELECT accno, description | .
@@ -53,7 +54,6 @@ sub paymentaccounts {
     qq|ORDER BY accno|;
 
   $form->{PR} = selectall_hashref_query($form, $dbh, $query);
-  $dbh->disconnect;
 
   $main::lxdebug->leave_sub();
 }
@@ -64,7 +64,7 @@ sub payment_transactions {
   my ($self, $myconfig, $form) = @_;
 
   # connect to database, turn AutoCommit off
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my ($query, @values);
 
@@ -211,12 +211,10 @@ sub payment_transactions {
     push(@values, conv_i($form->{filter_amount}));
   }
 
-  $query .= " ORDER BY 3,7,8 LIMIT 6";
+  $query .= " ORDER BY 3,7,8";
 
   $form->{PR} = selectall_hashref_query($form, $dbh, $query, @values);
 
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
@@ -225,30 +223,30 @@ sub reconcile {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
-
-  my ($query, $i);
+  SL::DB->client->with_transaction(sub {
+    my $dbh = SL::DB->client->dbh;
 
-  # clear flags
-  for $i (1 .. $form->{rowcount}) {
-    if ($form->{"cleared_$i"}) {
-      $query =
-        qq|UPDATE acc_trans SET cleared = '1' | .
-        qq|WHERE acc_trans_id = ?|;
-      do_query($form, $dbh, $query, $form->{"oid_$i"});
+    my ($query, $i);
 
-      # clear fx_transaction
-      if ($form->{"fxoid_$i"}) {
+    # clear flags
+    for $i (1 .. $form->{rowcount}) {
+      if ($form->{"cleared_$i"}) {
         $query =
           qq|UPDATE acc_trans SET cleared = '1' | .
           qq|WHERE acc_trans_id = ?|;
-        do_query($form, $dbh, $query, $form->{"fxoid_$i"});
+        do_query($form, $dbh, $query, $form->{"oid_$i"});
+
+        # clear fx_transaction
+        if ($form->{"fxoid_$i"}) {
+          $query =
+            qq|UPDATE acc_trans SET cleared = '1' | .
+            qq|WHERE acc_trans_id = ?|;
+          do_query($form, $dbh, $query, $form->{"fxoid_$i"});
+        }
       }
     }
-  }
-
-  $dbh->disconnect;
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
@@ -259,7 +257,7 @@ sub get_statement_balance {
   my ($self, $myconfig, $form) = @_;
 
   # connect to database, turn AutoCommit off
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my ($query, @values);
 
@@ -277,8 +275,6 @@ sub get_statement_balance {
 
   ($form->{statement_balance}) = selectrow_query($form, $dbh, $query, @values);
 
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
index b2fd5e0..28d01af 100644 (file)
--- a/SL/RP.pm
+++ b/SL/RP.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # backend code for reports
@@ -38,6 +39,8 @@ use SL::DBUtils;
 use Data::Dumper;
 use SL::DB::Helper::AccountingPeriod qw(get_balance_starting_date);
 use List::Util qw(sum);
+use List::UtilsBy qw(partition_by sort_by);
+use SL::DB;
 
 # use warnings;
 use strict;
@@ -308,7 +311,8 @@ sub get_accounts {
          FROM invoice ac
          JOIN ar a ON (a.id = ac.trans_id)
          JOIN parts p ON (ac.parts_id = p.id)
-         JOIN chart c on (p.income_accno_id = c.id)
+         JOIN taxzone_charts t ON (p.buchungsgruppen_id = t.id)
+         JOIN chart c on (t.income_accno_id = c.id)
          -- use transdate from subwhere
          WHERE (c.category = 'I')
            $subwhere
@@ -329,7 +333,8 @@ sub get_accounts {
          FROM invoice ac
          JOIN ap a ON (a.id = ac.trans_id)
          JOIN parts p ON (ac.parts_id = p.id)
-         JOIN chart c on (p.expense_accno_id = c.id)
+         JOIN taxzone_charts t ON (p.buchungsgruppen_id = t.id)
+         JOIN chart c on (t.expense_accno_id = c.id)
          WHERE (c.category = 'E')
            $subwhere
            $dpt_where
@@ -371,7 +376,8 @@ sub get_accounts {
       FROM invoice ac
       JOIN ar a ON (a.id = ac.trans_id)
       JOIN parts p ON (ac.parts_id = p.id)
-      JOIN chart c on (p.income_accno_id = c.id)
+      JOIN taxzone_charts t ON (p.buchungsgruppen_id = t.id)
+      JOIN chart c on (t.income_accno_id = c.id)
       -- use transdate from subwhere
       WHERE (c.category = 'I')
         $subwhere
@@ -385,7 +391,8 @@ sub get_accounts {
       FROM invoice ac
       JOIN ap a ON (a.id = ac.trans_id)
       JOIN parts p ON (ac.parts_id = p.id)
-      JOIN chart c on (p.expense_accno_id = c.id)
+      JOIN taxzone_charts t ON (p.buchungsgruppen_id = t.id)
+      JOIN chart c on (t.expense_accno_id = c.id)
       WHERE (c.category = 'E')
         $subwhere
         $dpt_where
@@ -473,6 +480,9 @@ sub get_accounts_g {
       $inwhere   = " AND (acc.transdate >= $fromdate)";
     } else {
       $where    .= " AND (ac.transdate >= $fromdate)";
+      # hotfix for projectfilter in guv and bwa
+      # fromdate is otherwise ignored if project is selected
+      $prwhere   = " AND (a.transdate  >= $fromdate)";
     }
   }
 
@@ -517,72 +527,74 @@ sub get_accounts_g {
             ELSE 0
             /* ar amount is zero, or we are checking with a non-ar-transaction, so we return 0 in both cases as multiplicator of ac.amount */
             END
-                ) AS amount, c.$category
+                ) AS amount, c.$category, c.accno, c.description
        FROM acc_trans ac
        LEFT JOIN chart c ON (c.id  = ac.chart_id)
        LEFT JOIN ar      ON (ar.id = ac.trans_id)
       WHERE ac.trans_id IN (SELECT DISTINCT trans_id FROM acc_trans WHERE 1=1 $subwhere)
 
-      GROUP BY c.$category
+      GROUP BY c.$category, c.accno, c.description
 
 /*
-       SELECT SUM(ac.amount * chart_category_to_sgn(c.category)) AS amount, c.$category
+       SELECT SUM(ac.amount * chart_category_to_sgn(c.category)) AS amount, c.$category, c.accno, c.description
          FROM acc_trans ac
          JOIN chart c ON (c.id = ac.chart_id)
          JOIN ar a ON (a.id = ac.trans_id)
          WHERE $where $dpt_where
            AND ac.trans_id IN ( SELECT trans_id FROM acc_trans a WHERE (a.chart_link LIKE '%AR_paid%') $subwhere)
            $project
-         GROUP BY c.$category
+         GROUP BY c.$category, c.accno, c.description
 */
          UNION
 
-         SELECT SUM(ac.amount * chart_category_to_sgn(c.category)) AS amount, c.$category
+         SELECT SUM(ac.amount * chart_category_to_sgn(c.category)) AS amount, c.$category, c.accno, c.description
          FROM acc_trans ac
          JOIN chart c ON (c.id = ac.chart_id)
          JOIN ap a ON (a.id = ac.trans_id)
          WHERE $where $dpt_where
            AND ac.trans_id IN ( SELECT trans_id FROM acc_trans a WHERE (a.chart_link LIKE '%AP_paid%') $subwhere)
            $project
-         GROUP BY c.$category
+         GROUP BY c.$category, c.accno, c.description
 
          UNION
 
-         SELECT SUM(ac.amount * chart_category_to_sgn(c.category)) AS amount, c.$category
+         SELECT SUM(ac.amount * chart_category_to_sgn(c.category)) AS amount, c.$category, c.accno, c.description
          FROM acc_trans ac
          JOIN chart c ON (c.id = ac.chart_id)
          JOIN gl a ON (a.id = ac.trans_id)
          WHERE $where $dpt_where $glwhere
            AND NOT ((ac.chart_link = 'AR') OR (ac.chart_link = 'AP'))
            $project
-         GROUP BY c.$category
+         GROUP BY c.$category, c.accno, c.description
         |;
 
     if ($form->{project_id}) {
       $query .= qq|
          UNION
 
-         SELECT SUM(ac.sellprice * ac.qty * chart_category_to_sgn(c.category)) AS amount, c.$category
+         SELECT SUM(ac.sellprice * ac.qty * chart_category_to_sgn(c.category)) AS amount, c.$category, c.accno, c.description
          FROM invoice ac
          JOIN ar a ON (a.id = ac.trans_id)
          JOIN parts p ON (ac.parts_id = p.id)
-         JOIN chart c on (p.income_accno_id = c.id)
+         JOIN taxzone_charts t ON (p.buchungsgruppen_id = t.id)
+         JOIN chart c on (t.income_accno_id = c.id)
          WHERE (c.category = 'I') $prwhere $dpt_where
            AND ac.trans_id IN ( SELECT trans_id FROM acc_trans a WHERE (a.chart_link LIKE '%AR_paid%') $subwhere)
            $project
-         GROUP BY c.$category
+         GROUP BY c.$category, c.accno, c.description
 
          UNION
 
-         SELECT SUM(ac.sellprice * chart_category_to_sgn(c.category)) AS amount, c.$category
+         SELECT SUM(ac.sellprice * chart_category_to_sgn(c.category)) AS amount, c.$category, c.accno, c.description
          FROM invoice ac
          JOIN ap a ON (a.id = ac.trans_id)
          JOIN parts p ON (ac.parts_id = p.id)
-         JOIN chart c on (p.expense_accno_id = c.id)
+         JOIN taxzone_charts t ON (p.buchungsgruppen_id = t.id)
+         JOIN chart c on (t.expense_accno_id = c.id)
          WHERE (c.category = 'E') $prwhere $dpt_where
            AND ac.trans_id IN ( SELECT trans_id FROM acc_trans a WHERE (a.chart_link LIKE '%AP_paid%') $subwhere)
          $project
-         GROUP BY c.$category
+         GROUP BY c.$category, c.accno, c.description
          |;
     }
 
@@ -595,41 +607,43 @@ sub get_accounts_g {
     }
 
     $query = qq|
-        SELECT sum(ac.amount * chart_category_to_sgn(c.category)) AS amount, c.$category
+        SELECT sum(ac.amount * chart_category_to_sgn(c.category)) AS amount, c.$category, c.accno, c.description
         FROM acc_trans ac
         JOIN chart c ON (c.id = ac.chart_id)
         WHERE $where
           $dpt_where_without_arapgl
           $project
-        GROUP BY c.$category |;
+        GROUP BY c.$category, c.accno, c.description |;
 
     if ($form->{project_id}) {
       $query .= qq|
         UNION
 
-        SELECT SUM(ac.sellprice * ac.qty * chart_category_to_sgn(c.category)) AS amount, c.$category
+        SELECT SUM(ac.sellprice * ac.qty * chart_category_to_sgn(c.category)) AS amount, c.$category, c.accno, c.description
         FROM invoice ac
         JOIN ar a ON (a.id = ac.trans_id)
         JOIN parts p ON (ac.parts_id = p.id)
-        JOIN chart c on (p.income_accno_id = c.id)
+        JOIN taxzone_charts t ON (p.buchungsgruppen_id = t.id)
+        JOIN chart c on (t.income_accno_id = c.id)
         WHERE (c.category = 'I')
           $prwhere
           $dpt_where
           $project
-        GROUP BY c.$category
+        GROUP BY c.$category, c.accno, c.description
 
         UNION
 
-        SELECT SUM(ac.sellprice * ac.qty * chart_category_to_sgn(c.category)) AS amount, c.$category
+        SELECT SUM(ac.sellprice * ac.qty * chart_category_to_sgn(c.category)) AS amount, c.$category, c.accno, c.description
         FROM invoice ac
         JOIN ap a ON (a.id = ac.trans_id)
         JOIN parts p ON (ac.parts_id = p.id)
-        JOIN chart c on (p.expense_accno_id = c.id)
+        JOIN taxzone_charts t ON (p.buchungsgruppen_id = t.id)
+        JOIN chart c on (t.expense_accno_id = c.id)
         WHERE (c.category = 'E')
           $prwhere
           $dpt_where
           $project
-        GROUP BY c.$category |;
+        GROUP BY c.$category, c.accno, c.description |;
     }
   }
 
@@ -637,15 +651,27 @@ sub get_accounts_g {
   my $accno;
   my $ref;
 
+  # store information for chart list in $form->{charts}
   foreach my $ref (selectall_hashref_query($form, $dbh, $query)) {
+    unless ( defined $form->{charts}->{$ref->{accno}}  ) {
+      # a chart may appear several times in the resulting hashref, init it the first time
+      $form->{charts}->{$ref->{accno}} = { amount      => 0,
+                                           "$category" => $ref->{"$category"},
+                                           accno       => $ref->{accno},
+                                           description => $ref->{description},
+                                         };
+    }
     if ($category eq "pos_bwa") {
       if ($last_period) {
         $form->{ $ref->{$category} }{kumm} += $ref->{amount};
       } else {
         $form->{ $ref->{$category} }{jetzt} += $ref->{amount};
+        # only increase chart amount for current period, not last_period
+        $form->{charts}->{$ref->{accno}}->{amount} +=  $ref->{amount},
       }
     } else {
       $form->{ $ref->{$category} } += $ref->{amount};
+      $form->{charts}->{$ref->{accno}}->{amount} +=  $ref->{amount}; # no last_period for eur
     }
   }
 
@@ -657,7 +683,7 @@ sub trial_balance {
 
   my ($self, $myconfig, $form, %options) = @_;
 
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my ($query, $sth, $ref);
   my %balance = ();
@@ -746,7 +772,6 @@ sub trial_balance {
           $customer_join
           WHERE ((select date_trunc('year', ac.transdate::date)) = (select date_trunc('year', ?::date))) AND ac.ob_transaction
             $dpt_where_without_arapgl
-            $dpt_where
             $customer_where
             $project
           GROUP BY c.accno, c.category, c.description |;
@@ -807,14 +832,16 @@ sub trial_balance {
   if ($form->{fromdate} || $form->{todate}) {
     if ($form->{fromdate}) {
       $fromdate = conv_dateq($form->{fromdate});
+      my $fiscal_year_startdate = conv_dateq($self->get_balance_starting_date($form->{fromdate}));
+      # my $date_trunc = "(select date_trunc('year', date $fromdate))";
       $tofrom        .= " AND (ac.transdate >= $fromdate)";
       $subwhere      .= " AND (ac.transdate >= $fromdate)";
-      $sumsubwhere   .= " AND (ac.transdate >= (select date_trunc('year', date $fromdate))) ";
-      $saldosubwhere .= " AND (ac,transdate>=(select date_trunc('year', date $fromdate)))  ";
-      $invwhere      .= " AND (a.transdate >= $fromdate)";
-      $glsaldowhere  .= " AND ac.transdate>=(select date_trunc('year', date $fromdate)) ";
+      $sumsubwhere   .= " AND (ac.transdate >= $fiscal_year_startdate) ";
+      $saldosubwhere .= " AND (ac.transdate >= $fiscal_year_startdate) ";
+      $invwhere      .= " AND (a.transdate  >= $fromdate)";
+      $glsaldowhere  .= " AND (ac.transdate >= $fiscal_year_startdate) ";
       $glwhere        = " AND (ac.transdate >= $fromdate)";
-      $glsumwhere     = " AND (ac.transdate >= (select date_trunc('year', date $fromdate))) ";
+      $glsumwhere     = " AND (ac.transdate >= $fiscal_year_startdate) ";
     }
     if ($form->{todate}) {
       $todate = conv_dateq($form->{todate});
@@ -874,7 +901,8 @@ sub trial_balance {
       FROM invoice ac
       JOIN ar a ON (ac.trans_id = a.id)
       JOIN parts p ON (ac.parts_id = p.id)
-      JOIN chart c ON (p.income_accno_id = c.id)
+      JOIN taxzone_charts t ON (p.buchungsgruppen_id = t.id)
+      JOIN chart c ON (t.income_accno_id = c.id)
       WHERE $invwhere
         $dpt_where
         $customer_where
@@ -887,7 +915,8 @@ sub trial_balance {
       FROM invoice ac
       JOIN ap a ON (ac.trans_id = a.id)
       JOIN parts p ON (ac.parts_id = p.id)
-      JOIN chart c ON (p.expense_accno_id = c.id)
+      JOIN taxzone_charts t ON (p.buchungsgruppen_id = t.id)
+      JOIN chart c ON (t.expense_accno_id = c.id)
       WHERE $invwhere
         $dpt_where
         $customer_no_union
@@ -909,7 +938,7 @@ sub trial_balance {
   }
   $sth->finish;
 
-  if (!$form->{method} ne "cash") {
+  if ($form->{method} ne "cash") {  # better eq 'accrual'
     $sth = prepare_execute_query($form, $dbh, $fetch_accounts_before_from);
     while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
       $trb{ $ref->{accno} }{description} = $ref->{description};
@@ -931,7 +960,6 @@ sub trial_balance {
           $customer_join
           WHERE $where
             $dpt_where_without_arapgl
-            $dpt_where
             $customer_where
             $project
           AND (ac.amount < 0)
@@ -943,7 +971,6 @@ sub trial_balance {
           $customer_join
           WHERE $where
             $dpt_where_without_arapgl
-            $dpt_where
             $customer_where
             $project
           AND ac.amount > 0
@@ -954,7 +981,6 @@ sub trial_balance {
          $customer_join
          WHERE $saldowhere
            $dpt_where_without_arapgl
-           $dpt_where
            $customer_where
            $project
          AND c.accno = ? AND (NOT ac.ob_transaction OR ac.ob_transaction IS NULL)) AS saldo,
@@ -965,7 +991,6 @@ sub trial_balance {
          $customer_join
          WHERE $sumwhere
            $dpt_where_without_arapgl
-           $dpt_where
            $customer_where
            $project
          AND ac.amount > 0
@@ -977,7 +1002,6 @@ sub trial_balance {
          $customer_join
          WHERE $sumwhere
            $dpt_where_without_arapgl
-           $dpt_where
            $customer_where
            $project
          AND ac.amount < 0
@@ -988,7 +1012,6 @@ sub trial_balance {
         $customer_join
         WHERE $where
           $dpt_where_without_arapgl
-          $dpt_where
           $customer_where
           $project
         AND c.accno = ?) AS last_transaction
@@ -1006,7 +1029,8 @@ sub trial_balance {
            FROM invoice ac
            JOIN parts p ON (ac.parts_id = p.id)
            JOIN ap a ON (ac.trans_id = a.id)
-           JOIN chart c ON (p.expense_accno_id = c.id)
+           JOIN taxzone_charts t ON (p.buchungsgruppen_id = t.id)
+           JOIN chart c ON (t.expense_accno_id = c.id)
            WHERE $invwhere
              $dpt_where
              $customer_no_union
@@ -1017,7 +1041,8 @@ sub trial_balance {
            FROM invoice ac
            JOIN parts p ON (ac.parts_id = p.id)
            JOIN ar a ON (ac.trans_id = a.id)
-           JOIN chart c ON (p.income_accno_id = c.id)
+           JOIN taxzone_charts t ON (p.buchungsgruppen_id = t.id)
+           JOIN chart c ON (t.income_accno_id = c.id)
            WHERE $invwhere
              $dpt_where
              $customer_where
@@ -1065,7 +1090,6 @@ sub trial_balance {
         $customer_join
         WHERE $where
           $dpt_where_without_arapgl
-          $dpt_where
           $customer_where
           $project
         AND c.accno = ?) AS last_transaction
@@ -1075,7 +1099,7 @@ sub trial_balance {
   }
 
 
-  my ($debit, $credit, $saldo, $soll_saldo, $haben_saldo,$soll_kummuliert, $haben_kummuliert, $last_transaction);
+  my ($debit, $credit, $saldo, $soll_saldo, $haben_saldo, $soll_kumuliert, $haben_kumuliert, $last_transaction);
 
   foreach my $accno (sort keys %trb) {
     $ref = {};
@@ -1167,8 +1191,6 @@ sub trial_balance {
 
   }
 
-  $dbh->disconnect;
-
   # debits and credits for headings
   foreach my $accno (@headingaccounts) {
     foreach $ref (@{ $form->{TB} }) {
@@ -1186,26 +1208,13 @@ sub trial_balance {
   $main::lxdebug->leave_sub();
 }
 
-sub get_storno {
-  $main::lxdebug->enter_sub();
-  my ($self, $dbh, $form) = @_;
-  my $arap = $form->{arap} eq "ar" ? "ar" : "ap";
-  my $query = qq|SELECT invnumber FROM $arap WHERE invnumber LIKE "Storno zu "|;
-  my $sth =  $dbh->prepare($query);
-  while(my $ref = $sth->fetchrow_hashref()) {
-    $ref->{invnumer} =~ s/Storno zu //g;
-    $form->{storno}{$ref->{invnumber}} = 1;
-  }
-  $main::lxdebug->leave_sub();
-}
-
 sub aging {
   $main::lxdebug->enter_sub();
 
   my ($self, $myconfig, $form) = @_;
 
   # connect to database
-  my $dbh     = $form->dbconnect($myconfig);
+  my $dbh     = SL::DB->client->dbh;
 
   my ($invoice, $arap, $buysell, $ct, $ct_id, $ml);
 
@@ -1271,7 +1280,7 @@ sub aging {
   if ($form->{$ct_id}) {
     $where .= qq| AND (ct.id = | . conv_i($form->{$ct_id}) . qq|)|;
   } elsif ($form->{ $form->{ct} }) {
-    $where .= qq| AND (ct.name ILIKE | . $dbh->quote('%' . $form->{$ct} . '%') . qq|)|;
+    $where .= qq| AND (ct.name ILIKE | . $dbh->quote(like($form->{$ct})) . qq|)|;
   }
 
   my $dpt_join;
@@ -1287,9 +1296,9 @@ sub aging {
     SELECT ${ct}.id AS ctid, ${ct}.name,
       street, zipcode, city, country, contact, email,
       phone as customerphone, fax as customerfax, ${ct}number,
-      "invnumber", "transdate",
+      "invnumber", "transdate", "type",
       (amount - COALESCE((SELECT sum(amount)*$ml FROM acc_trans WHERE chart_link ilike '%paid%' AND acc_trans.trans_id=${arap}.id AND acc_trans.transdate <= (date $todate)),0)) as "open", "amount",
-      "duedate", invoice, ${arap}.id, date_part('days', now() - duedate) as overduedays,
+      "duedate", invoice, ${arap}.id, date_part('days', now() - duedate) as overduedays, datepaid, (amount - paid) as current_open,
       (SELECT $buysell
        FROM exchangerate
        WHERE (${arap}.currency_id = exchangerate.currency_id)
@@ -1337,9 +1346,6 @@ sub aging {
 
   $sth->finish;
 
-  # disconnect
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
@@ -1348,8 +1354,7 @@ sub get_customer {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $ct = $form->{ct} eq "customer" ? "customer" : "vendor";
 
@@ -1359,7 +1364,6 @@ sub get_customer {
        WHERE ct.id = ?|;
   ($form->{ $form->{ct} }, $form->{email}, $form->{cc}, $form->{bcc}) =
     selectrow_query($form, $dbh, $query, $form->{"${ct}_id"});
-  $dbh->disconnect;
 
   $main::lxdebug->leave_sub();
 }
@@ -1369,8 +1373,7 @@ sub tax_report {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my ($null, $department_id) = split /--/, $form->{department};
 
@@ -1459,8 +1462,6 @@ sub tax_report {
 
   $form->{TR} = selectall_hashref_query($form, $dbh, $query);
 
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
@@ -1470,7 +1471,7 @@ sub paymentaccounts {
   my ($self, $myconfig, $form) = @_;
 
   # connect to database, turn AutoCommit off
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $ARAP = $form->{db} eq "ar" ? "AR" : "AP";
 
@@ -1481,8 +1482,6 @@ sub paymentaccounts {
        WHERE link LIKE '%${ARAP}_paid%'|;
   $form->{PR} = selectall_hashref_query($form, $dbh, $query);
 
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
@@ -1492,7 +1491,7 @@ sub payments {
   my ($self, $myconfig, $form) = @_;
 
   # connect to database, turn AutoCommit off
-  my $dbh = $form->dbconnect_noauto($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $ml = 1;
   my $arap;
@@ -1526,15 +1525,15 @@ sub payments {
   my $invnumber;
   my $reference;
   if ($form->{reference}) {
-    $reference = $dbh->quote('%' . $form->{reference} . '%');
+    $reference = $dbh->quote(like($form->{reference}));
     $invnumber = " AND (a.invnumber LIKE $reference)";
     $reference = " AND (a.reference LIKE $reference)";
   }
   if ($form->{source}) {
-    $where .= " AND (ac.source ILIKE " . $dbh->quote('%' . $form->{source} . '%') . ") ";
+    $where .= " AND (ac.source ILIKE " . $dbh->quote(like($form->{source})) . ") ";
   }
   if ($form->{memo}) {
-    $where .= " AND (ac.memo ILIKE " . $dbh->quote('%' . $form->{memo} . '%') . ") ";
+    $where .= " AND (ac.memo ILIKE " . $dbh->quote(like($form->{memo})) . ") ";
   }
 
   my %sort_columns =  (
@@ -1613,8 +1612,6 @@ sub payments {
     $sth_details->finish();
   }
 
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
@@ -1623,8 +1620,7 @@ sub bwa {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->dbconnect($myconfig);
+  my $dbh = SL::DB->client->dbh;
 
   my $last_period = 0;
   my $category;
@@ -1651,6 +1647,15 @@ sub bwa {
     &get_accounts_g($dbh, $last_period, $kummfromdate, $kummtodate, $form, "pos_bwa");
   }
 
+  my %charts_by_category =
+    partition_by { $_->{pos_bwa} }
+    sort_by      { $_->{accno}   }
+    map          { $form->{charts}->{$_} }
+    keys %{ $form->{charts} };
+  $form->{"charts_by_category"} = \%charts_by_category;
+
+  $form->{category_names} = AM->get_bwa_categories($myconfig, $form);
+
   my @periods        = qw(jetzt kumm);
   my @gesamtleistung = qw(1 3);
   my @gesamtkosten   = qw (10 11 12 13 14 15 16 17 18 20);
@@ -1824,7 +1829,6 @@ sub bwa {
     }
 
   }
-  $dbh->disconnect;
 
   $main::lxdebug->leave_sub();
 }
@@ -1852,23 +1856,131 @@ sub income_statement {
                   $form, "pos_eur");
 
 
+  # add extra information to form to be used by template
+  my %charts_by_category =
+    partition_by { $_->{pos_eur} }
+    sort_by      { $_->{accno}   }
+    map          { $form->{charts}->{$_} }
+    keys %{ $form->{charts} };
+  $form->{"charts_by_category"} = \%charts_by_category;
+
+  $form->{"categories_income"}  = \@categories_einnahmen;
+  $form->{"categories_expense"} = \@categories_ausgaben;
+
+  $form->{category_names} = AM->get_eur_categories($myconfig, $form);
+
+  my %eur_amounts;
+
   foreach my $item (@categories_einnahmen) {
-    $form->{"eur${item}"} =
-      $form->format_amount($myconfig, $form->round_amount($form->{$item}, 2),2);
+    $eur_amounts{$item} = $form->format_amount($myconfig, $form->round_amount($form->{$item}, 2),2);
     $form->{"sumeura"} += $form->{$item};
   }
   foreach my $item (@categories_ausgaben) {
-    $form->{"eur${item}"} =
-      $form->format_amount($myconfig, $form->round_amount($form->{$item}, 2),2);
+    $eur_amounts{$item} = $form->format_amount($myconfig, $form->round_amount($form->{$item}, 2),2);
     $form->{"sumeurb"} += $form->{$item};
   }
 
   $form->{"guvsumme"} = $form->{"sumeura"} - $form->{"sumeurb"};
 
+  $form->{eur_amounts} = \%eur_amounts;
+
   foreach my $item (@ergebnisse) {
     $form->{$item} =
       $form->format_amount($myconfig, $form->round_amount($form->{$item}, 2),2);
   }
   $main::lxdebug->leave_sub();
 }
+
+sub erfolgsrechnung {
+  $main::lxdebug->enter_sub();
+
+  my ($self, $myconfig, $form) = @_;
+  $form->{company} = $::instance_conf->get_company;
+  $form->{address} = $::instance_conf->get_address;
+  $form->{fromdate} = DateTime->new(year => 2000, month => 1, day => 1)->to_kivitendo unless $form->{fromdate};
+  $form->{todate} = $form->current_date(%{$myconfig}) unless $form->{todate};
+
+  my %categories = (I => "ERTRAG", E => "AUFWAND");
+  my $fromdate = conv_dateq($form->{fromdate});
+  my $todate = conv_dateq($form->{todate});
+  my $department_id = conv_i((split /--/, $form->{department})[1], 'NULL');
+
+  $form->{total} = 0;
+
+  foreach my $category ('I', 'E') {
+    my %category = (
+      name => $categories{$category},
+      total => 0,
+      accounts => get_accounts_ch($category)
+    );
+    foreach my $account (@{$category{accounts}}) {
+      $account->{total} = get_total_ch($department_id, $account->{id}, $fromdate, $todate);
+      $category{total} += $account->{total};
+      $account->{total} = $form->format_amount($myconfig, $form->round_amount($account->{total}, 2), 2);
+    }
+    $form->{total} += $category{total};
+    $category{total} = $form->format_amount($myconfig, $form->round_amount($category{total}, 2), 2);
+    push(@{$form->{categories}}, \%category);
+  }
+  $form->{total} = $form->format_amount($myconfig, $form->round_amount($form->{total}, 2), 2);
+
+  $main::lxdebug->leave_sub();
+  return {};
+}
+
+sub get_accounts_ch {
+  $main::lxdebug->enter_sub();
+
+  my ($category) = @_;
+  my $inclusion = '' ;
+
+  if ($category eq 'I') {
+    $inclusion = "AND pos_er = NULL OR pos_er = '1'";
+  } elsif ($category eq 'E') {
+    $inclusion = "AND pos_er = NULL OR pos_er = '6'";
+  } else {
+    $inclusion = "";
+  }
+
+  my $query = qq|
+    SELECT id, accno, description, category
+    FROM chart
+    WHERE category = ? $inclusion
+    ORDER BY accno
+  |;
+  my $accounts = _query($query, $category);
+
+  $main::lxdebug->leave_sub();
+  return $accounts;
+}
+
+sub get_total_ch {
+  $main::lxdebug->enter_sub();
+
+  my ($department_id, $chart_id, $fromdate, $todate) = @_;
+  my $total = 0;
+  my $query = qq|
+    SELECT SUM(amount)
+    FROM acc_trans
+    WHERE chart_id = ?
+      AND transdate >= ?
+      AND transdate <= ?
+  |;
+  if ($department_id) {
+    $query .= qq| AND COALESCE(
+        (SELECT department_id FROM ar WHERE ar.id=trans_id),
+        (SELECT department_id FROM gl WHERE gl.id=trans_id),
+        (SELECT department_id FROM ap WHERE ap.id=trans_id)
+    ) = ? |;
+    $total += _query($query, $chart_id, $fromdate, $todate, $department_id)->[0]->{sum};
+  } else {
+    $total += _query($query, $chart_id, $fromdate, $todate)->[0]->{sum};
+  }
+
+  $main::lxdebug->leave_sub();
+  return $total;
+}
+
+sub _query {return selectall_hashref_query($::form, $::form->get_standard_dbh, @_);}
+
 1;
index 77086ba..da5d54a 100644 (file)
@@ -7,6 +7,7 @@ use SL::Common;
 use SL::DBUtils;
 use Data::Dumper;
 use List::Util qw(reduce);
+use SL::DB;
 
 sub create_links {
   $main::lxdebug->enter_sub();
@@ -54,19 +55,21 @@ sub create_links {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
+  SL::DB->client->with_transaction(sub {
+    my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
-  my $query    = qq|INSERT INTO record_links (from_table, from_id, to_table, to_id) VALUES (?, ?, ?, ?)|;
-  my $sth      = prepare_query($form, $dbh, $query);
+    my $query    = qq|INSERT INTO record_links (from_table, from_id, to_table, to_id) VALUES (?, ?, ?, ?)|;
+    my $sth      = prepare_query($form, $dbh, $query);
 
-  foreach my $link (@links) {
-    next if ('HASH' ne ref $link);
-    next if (!$link->{from_table} || !$link->{from_id} || !$link->{to_table} || !$link->{to_id});
+    foreach my $link (@links) {
+      next if ('HASH' ne ref $link);
+      next if (!$link->{from_table} || !$link->{from_id} || !$link->{to_table} || !$link->{to_id});
 
-    do_statement($form, $sth, $query, $link->{from_table}, conv_i($link->{from_id}), $link->{to_table}, conv_i($link->{to_id}));
-  }
+      do_statement($form, $sth, $query, $link->{from_table}, conv_i($link->{from_id}), $link->{to_table}, conv_i($link->{to_id}));
+    }
 
-  $dbh->commit() unless ($params{dbh});
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
@@ -184,21 +187,23 @@ sub delete {
   my $myconfig   = \%main::myconfig;
   my $form       = $main::form;
 
-  my $dbh        = $params{dbh} || $form->get_standard_dbh($myconfig);
+  SL::DB->client->with_transaction(sub {
+    my $dbh        = $params{dbh} || SL::DB->client->dbh;
 
-  # content
-  my (@where_tokens, @where_values);
+    # content
+    my (@where_tokens, @where_values);
 
-  for my $col (qw(from_table from_id to_table to_id)) {
-    add_token(\@where_tokens, \@where_values, col => $col, val => $params{$col}) if $params{$col};
-  }
+    for my $col (qw(from_table from_id to_table to_id)) {
+      add_token(\@where_tokens, \@where_values, col => $col, val => $params{$col}) if $params{$col};
+    }
 
-  my $where = @where_tokens ? "WHERE ". join ' AND ', map { "($_)" } @where_tokens : '';
-  my $query = "DELETE FROM record_links $where";
+    my $where = @where_tokens ? "WHERE ". join ' AND ', map { "($_)" } @where_tokens : '';
+    my $query = "DELETE FROM record_links $where";
 
-  do_query($form, $dbh, $query, @where_values);
+    do_query($form, $dbh, $query, @where_values);
 
-  $dbh->commit() unless ($params{dbh});
+    1;
+  }) or die { SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
@@ -207,6 +212,10 @@ sub delete {
 
 __END__
 
+=pod
+
+=encoding utf8
+
 =head1 NAME
 
 SL::RecordLinks - Verlinkung von kivitendo Objekten.
index 65a1492..275e2e3 100644 (file)
@@ -2,11 +2,14 @@ package SL::ReportGenerator;
 
 use Data::Dumper;
 use List::Util qw(max);
+use Scalar::Util qw(blessed);
 use Text::CSV_XS;
 #use PDF::API2;    # these two eat up to .75s on startup. only load them if we actually need them
 #use PDF::Table;
 
 use strict;
+use SL::Helper::GlAttachments qw(append_gl_pdf_attachments);
+use SL::Helper::CreatePDF     qw(merge_pdfs);
 
 # Cause locales.pl to parse these files:
 # parse_html_template('report_generator/html_report')
@@ -47,6 +50,7 @@ sub new {
       'escape_char'         => '"',
       'eol_style'           => 'Unix',
       'headers'             => 1,
+      'encoding'            => 'UTF-8',
     },
   };
   $self->{export}   = {
@@ -161,7 +165,9 @@ sub set_options {
 
   while (my ($key, $value) = each %options) {
     if ($key eq 'pdf_export') {
-      map { $self->{options}->{pdf_export}->{$_} = $value->{$_} } keys %{ $value };
+      $self->{options}->{pdf_export}->{$_} = $value->{$_} for keys %{ $value };
+    } elsif ($key eq 'csv_export') {
+      $self->{options}->{csv_export}->{$_} = $value->{$_} for keys %{ $value };
     } else {
       $self->{options}->{$key} = $value;
     }
@@ -229,12 +235,13 @@ sub generate_with_headers {
   }
 
   if ($format eq 'html') {
+    my $content    = $self->generate_html_content(%params);
     my $title      = $form->{title};
     $form->{title} = $self->{title} if ($self->{title});
     $form->header(no_layout => $params{no_layout});
     $form->{title} = $title;
 
-    print $self->generate_html_content();
+    print $content;
 
   } elsif ($format eq 'csv') {
     # FIXME: don't do mini http in here
@@ -272,7 +279,7 @@ sub html_format {
 }
 
 sub prepare_html_content {
-  my $self = shift;
+  my ($self, %params) = @_;
 
   my ($column, $name, @column_headers);
 
@@ -287,6 +294,7 @@ sub prepare_html_content {
       'align'                    => $column->{align},
       'link'                     => $column->{link},
       'text'                     => $column->{text},
+      'raw_header_data'          => $column->{raw_header_data},
       'show_sort_indicator'      => $name eq $opts->{sort_indicator_column},
       'sort_indicator_direction' => $opts->{sort_indicator_direction},
     };
@@ -403,14 +411,63 @@ sub prepare_html_content {
     'DATA_PRESENT'         => $self->{data_present},
     'CONTROLLER_DISPATCH'  => $opts->{controller_class},
     'TABLE_CLASS'          => $opts->{table_class},
+    'SKIP_BUTTONS'         => !!$params{action_bar},
   };
 
   return $variables;
 }
 
+sub create_action_bar_actions {
+  my ($self, $variables) = @_;
+
+  my @actions;
+  foreach my $type (qw(pdf csv)) {
+    next unless $variables->{"ALLOW_" . uc($type) . "_EXPORT"};
+
+    my $key   = $variables->{CONTROLLER_DISPATCH} ? 'action' : 'report_generator_dispatch_to';
+    my $value = "report_generator_export_as_${type}";
+    $value    = $variables->{CONTROLLER_DISPATCH} . "/${value}" if $variables->{CONTROLLER_DISPATCH};
+
+    push @actions, action => [
+      $type eq 'pdf' ? $::locale->text('PDF export') : $::locale->text('CSV export'),
+      submit => [ '#report_generator_form', { $key => $value } ],
+    ];
+  }
+
+  if (scalar(@actions) > 1) {
+    @actions = (
+      combobox => [
+        action => [ $::locale->text('Export') ],
+        @actions,
+      ],
+    );
+  }
+
+  return @actions;
+}
+
+sub setup_action_bar {
+  my ($self, $variables, %params) = @_;
+
+  my @actions = $self->create_action_bar_actions($variables);
+
+  if ($params{action_bar_setup_hook}) {
+    $params{action_bar_setup_hook}->(@actions);
+
+  } elsif (@actions) {
+    my $action_bar = blessed($params{action_bar}) ? $params{action_bar} : ($::request->layout->get('actionbar'))[0];
+    $action_bar->add(@actions);
+  }
+}
+
 sub generate_html_content {
-  my $self      = shift;
-  my $variables = $self->prepare_html_content();
+  my ($self, %params) = @_;
+
+  $params{action_bar} //= 1;
+
+  my $variables = $self->prepare_html_content(%params);
+
+  $self->setup_action_bar($variables, %params) if $params{action_bar};
 
   my $stuff  = $self->{form}->parse_html_template($self->{options}->{html_template}, $variables);
   return $stuff;
@@ -429,6 +486,7 @@ sub generate_pdf_content {
   };
 
   my $self       = shift;
+  my %params     = @_;
   my $variables  = $self->prepare_html_content();
   my $form       = $self->{form};
   my $myconfig   = $self->{myconfig};
@@ -497,7 +555,9 @@ sub generate_pdf_content {
 
         foreach (0 .. $num_columns - 1) {
           push @{ $cell_props_row }, { 'background_color' => '#666666',
-                                       'font_color'       => '#ffffff',
+               #  BUG PDF:Table  -> 0.9.12:
+               # font_color is used in next row, so dont set font_color
+               #                       'font_color'       => '#ffffff',
                                        'colspan'          => $_ == 0 ? -1 : undef, };
         }
       }
@@ -654,13 +714,22 @@ sub generate_pdf_content {
 
   my $content = $pdf->stringify();
 
+  $main::lxdebug->message(LXDebug->DEBUG2(),"addattachments ?? =".$form->{report_generator_addattachments}." GL=".$form->{GL});
+  if ($form->{report_generator_addattachments} && $form->{GL}) {
+    $content = $self->append_gl_pdf_attachments($form,$content);
+  }
+
+  # 1. check if we return the report as binary pdf
+  if ($params{want_binary_pdf}) {
+    return $content;
+  }
+  # 2. check if we want and can directly print the report
   my $printer_command;
   if ($pdfopts->{print} && $pdfopts->{printer_id}) {
     $form->{printer_id} = $pdfopts->{printer_id};
     $form->get_printer_code($myconfig);
     $printer_command = $form->{printer_command};
   }
-
   if ($printer_command) {
     $self->_print_content('printer_command' => $printer_command,
                           'content'         => $content,
@@ -668,6 +737,7 @@ sub generate_pdf_content {
     $form->{report_generator_printed} = 1;
 
   } else {
+  # 3. default: redirect http with file attached
     my $filename = $self->get_attachment_basename();
 
     print qq|content-type: application/pdf\n|;
@@ -702,10 +772,10 @@ sub _print_content {
 }
 
 sub _handle_quoting_and_encoding {
-  my ($self, $text, $do_unquote) = @_;
+  my ($self, $text, $do_unquote, $encoding) = @_;
 
   $text = $main::locale->unquote_special_chars('HTML', $text) if $do_unquote;
-  $text = Encode::encode('UTF-8', $text);
+  $text = Encode::encode($encoding || 'UTF-8', $text);
 
   return $text;
 }
@@ -744,7 +814,7 @@ sub _generate_csv_content {
 
   if ($opts->{headers}) {
     if (!$self->{custom_headers}) {
-      $csv->print($stdout, [ map { $self->_handle_quoting_and_encoding($self->{columns}->{$_}->{text}, 1) } @visible_columns ]);
+      $csv->print($stdout, [ map { $self->_handle_quoting_and_encoding($self->{columns}->{$_}->{text}, 1, $opts->{encoding}) } @visible_columns ]);
 
     } else {
       foreach my $row (@{ $self->{custom_headers} }) {
@@ -752,7 +822,7 @@ sub _generate_csv_content {
 
         foreach my $col (@{ $row }) {
           my $num_output = ($col->{colspan} && ($col->{colspan} > 1)) ? $col->{colspan} : 1;
-          push @{ $fields }, ($self->_handle_quoting_and_encoding($col->{text}, 1)) x $num_output;
+          push @{ $fields }, ($self->_handle_quoting_and_encoding($col->{text}, 1, $opts->{encoding})) x $num_output;
         }
 
         $csv->print($stdout, $fields);
@@ -774,7 +844,7 @@ sub _generate_csv_content {
         my $num_output = ($row->{$col}{colspan} && ($row->{$col}->{colspan} > 1)) ? $row->{$col}->{colspan} : 1;
         $skip_next     = $num_output - 1;
 
-        push @data, join($eol, map { s/\r?\n/$eol/g; $self->_handle_quoting_and_encoding($_, 0) } @{ $row->{$col}->{data} });
+        push @data, join($eol, map { s/\r?\n/$eol/g; $self->_handle_quoting_and_encoding($_, 0, $opts->{encoding}) } @{ $row->{$col}->{data} });
         push @data, ('') x $skip_next if ($skip_next);
       }
 
@@ -911,6 +981,11 @@ The html generation function. Is invoked by generate_with_headers.
 
 The PDF generation function. It is invoked by generate_with_headers and renders the PDF with the PDF::API2 library.
 
+If the param want_binary_pdf is set, the binary pdf stream will be returned.
+If $pdfopts->{print} && $pdfopts->{printer_id} are set, the pdf will be printed (output is directed to print command).
+
+Otherwise and the default a html form with a downloadable file is returned.
+
 =item generate_csv_content
 
 The CSV generation function. Uses XS_CSV to parse the information into csv.
@@ -1025,6 +1100,10 @@ End of line style. Default is Unix.
 
 Include headers? Default is yes.
 
+=item encoding
+
+Character encoding. Default is UTF-8.
+
 =back
 
 =head1 SEE ALO
index f72694d..7c4fa71 100644 (file)
@@ -10,15 +10,17 @@ use List::MoreUtils qw(all any apply);
 use Exporter qw(import);
 
 use SL::Common;
+use SL::JSON;
 use SL::MoreCommon qw(uri_encode uri_decode);
 use SL::Layout::None;
 use SL::Presenter;
 
-our @EXPORT_OK = qw(flatten unflatten read_cgi_input);
+our @EXPORT_OK = qw(flatten unflatten);
 
 use Rose::Object::MakeMethods::Generic
 (
-  'scalar --get_set_init' => [ qw(cgi layout presenter is_ajax type) ],
+  scalar                  => [ qw(applying_database_upgrades post_data) ],
+  'scalar --get_set_init' => [ qw(cgi layout presenter is_ajax is_mobile type) ],
 );
 
 sub init_cgi {
@@ -37,10 +39,20 @@ sub init_is_ajax {
   return ($ENV{HTTP_X_REQUESTED_WITH} || '') eq 'XMLHttpRequest' ? 1 : 0;
 }
 
+sub init_is_mobile {
+  # mobile clients will change their user agent when the user requests
+  # desktop version so user agent is the most reliable way to identify
+  return ($ENV{HTTP_USER_AGENT} || '') =~ /Mobi/ ? 1 : 0;
+}
+
 sub init_type {
   return 'html';
 }
 
+sub is_https {
+  $ENV{HTTPS} && 'on' eq lc $ENV{HTTPS};
+}
+
 sub cache {
   my ($self, $topic, $default) = @_;
 
@@ -227,6 +239,12 @@ sub _parse_multipart_formdata {
   $::lxdebug->leave_sub(2);
 }
 
+sub _parse_json_formdata {
+  my ($content) = @_;
+
+  return $content ? SL::JSON::decode_json($content) : undef;
+}
+
 sub _recode_recursively {
   $::lxdebug->enter_sub;
   my ($iconv, $from, $to) = @_;
@@ -265,7 +283,7 @@ sub _recode_recursively {
 sub read_cgi_input {
   $::lxdebug->enter_sub;
 
-  my ($target) = @_;
+  my ($self, $target) = @_;
 
   # yes i know, copying all those values around isn't terribly efficient, but
   # the old version of dumping everything into form and then launching a
@@ -274,29 +292,38 @@ sub read_cgi_input {
   # this way the data can at least be recoded on the fly as soon as we get to
   # know the source encoding and only in the cases where encoding may be hidden
   # among the payload we take the hit of copying the request around
-  my $temp_target = { };
+  $self->post_data(undef);
+  my $data_to_decode = { };
+  my $iconv          = SL::Iconv->new('UTF-8', 'UTF-8');
 
-  # since both of these can potentially bring their encoding in INPUT_ENCODING
-  # they get dumped into temp_target
-  _input_to_hash($temp_target, $ENV{QUERY_STRING}, 1) if $ENV{QUERY_STRING};
-  _input_to_hash($temp_target, $ARGV[0],           1) if @ARGV && $ARGV[0];
+  _input_to_hash($data_to_decode, $ENV{QUERY_STRING}, 1) if $ENV{QUERY_STRING};
+  _input_to_hash($data_to_decode, $ARGV[0],           1) if @ARGV && $ARGV[0];
 
   if ($ENV{CONTENT_LENGTH}) {
     my $content;
     read STDIN, $content, $ENV{CONTENT_LENGTH};
+
     if ($ENV{'CONTENT_TYPE'} && $ENV{'CONTENT_TYPE'} =~ /multipart\/form-data/) {
+      $self->post_data({});
+      my $post_data_to_decode = { };
+
       # multipart formdata can bring it's own encoding, so give it both
       # and let it decide on it's own
-      _parse_multipart_formdata($target, $temp_target, $content, 1);
+      _parse_multipart_formdata($self->post_data, $post_data_to_decode, $content, 1);
+      _recode_recursively($iconv, $post_data_to_decode, $self->post_data) if keys %$post_data_to_decode;
+
+      $target->{$_} = $self->post_data->{$_} for keys %{ $self->post_data };
+
+    } elsif (($ENV{CONTENT_TYPE} // '') =~ m{^application/json}i) {
+      $self->post_data(_parse_json_formdata($content));
+
     } else {
       # normal encoding must be recoded
-      _input_to_hash($temp_target, $content, 1);
+      _input_to_hash($data_to_decode, $content, 1);
     }
   }
 
-  my $encoding     = delete $temp_target->{INPUT_ENCODING} || 'UTF-8';
-
-  _recode_recursively(SL::Iconv->new($encoding, 'UTF-8'), $temp_target => $target) if keys %$target;
+  _recode_recursively($iconv, $data_to_decode, $target) if keys %$data_to_decode;
 
   if ($target->{RESTORE_FORM_FROM_SESSION_ID}) {
     my %temp_form;
@@ -376,10 +403,10 @@ This module handles unpacking of CGI parameters. It also gives
 information about the request, such as whether or not it was done via AJAX,
 or the requested content type.
 
-  use SL::Request qw(read_cgi_input);
+  use SL::Request;
 
   # read cgi input depending on request type, unflatten and recode
-  read_cgi_input($target_hash_ref);
+  $::request->read_cgi_input($target_hash_ref);
 
   # $hashref and $new_hashref should be identical
   my $new_arrayref = flatten($hashref);
@@ -559,6 +586,26 @@ its initial value is set to C<$default>. If C<$default> is not given
 
 Returns the cached item.
 
+=item C<post_data>
+
+If the client sends data in the request body with the content type of
+either C<application/json> or C<multipart/form-data>, the content will
+be stored in the global request object, too. It can be retrieved via
+the C<post_data> function.
+
+For content type C<multipart/form-data> the same data is additionally
+stored in the global C<$::form> instance, potentially overwriting
+parameters given in the URL. This is done primarily for compatibility
+purposes with existing code that expects all parameters to be present
+in C<$::form>.
+
+For content type C<application/json> the data is only available in
+C<$::request>. The reason is that the top-level data in a JSON
+documents doesn't have to be an object which could be mapped to the
+hash C<$::form>. Instead, the top-level data can also be an
+array. Additionally keeping the namespaces of URL and POST parameters
+separate is cleaner and allows for fewer accidental conflicts.
+
 =back
 
 =head1 SPECIAL FUNCTIONS
index 9457917..fc92bcf 100644 (file)
@@ -8,8 +8,10 @@ use Data::Dumper;
 use SL::DBUtils;
 use SL::DB::Invoice;
 use SL::DB::PurchaseInvoice;
+use SL::DB;
 use SL::Locale::String qw(t8);
 use DateTime;
+use Carp;
 
 sub retrieve_open_invoices {
   $main::lxdebug->enter_sub();
@@ -23,16 +25,10 @@ sub retrieve_open_invoices {
   my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
   my $arap     = $params{vc} eq 'customer' ? 'ar'       : 'ap';
   my $vc       = $params{vc} eq 'customer' ? 'customer' : 'vendor';
+  my $vc_vc_id = $params{vc} eq 'customer' ? 'c_vendor_id' : 'v_customer_id';
 
   my $mandate  = $params{vc} eq 'customer' ? " AND COALESCE(vc.mandator_id, '') <> '' AND vc.mandate_date_of_signature IS NOT NULL " : '';
 
-  # in query: for customers, use payment terms from invoice, for vendors use
-  # payment terms from vendor settings
-  # currently there is no option in vendor invoices for setting payment terms,
-  # so the vendor settings are always used
-
-  my $payment_term_type = $params{vc} eq 'customer' ? "${arap}" : 'vc';
-
   # open_amount is not the current open amount according to bookkeeping, but
   # the open amount minus the SEPA transfer amounts that haven't been closed yet
   my $query =
@@ -42,14 +38,17 @@ sub retrieve_open_invoices {
          (${arap}.amount - (${arap}.amount * pt.percent_skonto)) as amount_less_skonto,
          (${arap}.amount * pt.percent_skonto) as skonto_amount,
          vc.name AS vcname, vc.language_id, ${arap}.duedate as duedate, ${arap}.direct_debit,
+         vc.${vc_vc_id} as vc_vc_id,
 
          COALESCE(vc.iban, '') <> '' AND COALESCE(vc.bic, '') <> '' ${mandate} AS vc_bank_info_ok,
 
-         ${arap}.amount - ${arap}.paid - COALESCE(open_transfers.amount, 0) AS open_amount
+         ${arap}.amount - ${arap}.paid - COALESCE(open_transfers.amount, 0) AS open_amount,
+         COALESCE(open_transfers.amount, 0) AS transfer_amount,
+         pt.description as pt_description
 
        FROM ${arap}
        LEFT JOIN ${vc} vc ON (${arap}.${vc}_id = vc.id)
-       LEFT JOIN (SELECT sei.${arap}_id, SUM(sei.amount) AS amount
+       LEFT JOIN (SELECT sei.${arap}_id, SUM(sei.amount) + SUM(COALESCE(sei.skonto_amount,0)) AS amount
                   FROM sepa_export_items sei
                   LEFT JOIN sepa_export se ON (sei.sepa_export_id = se.id)
                   WHERE NOT se.closed
@@ -57,12 +56,13 @@ sub retrieve_open_invoices {
                   GROUP BY sei.${arap}_id)
          AS open_transfers ON (${arap}.id = open_transfers.${arap}_id)
 
-       LEFT JOIN payment_terms pt ON (${payment_term_type}.payment_id = pt.id)
+       LEFT JOIN payment_terms pt ON (${arap}.payment_id = pt.id)
 
        WHERE ${arap}.amount > (COALESCE(open_transfers.amount, 0) + ${arap}.paid)
 
        ORDER BY lower(vc.name) ASC, lower(${arap}.invnumber) ASC
 |;
+    #  $main::lxdebug->message(LXDebug->DEBUG2(),"sepa add query:".$query);
 
   my $results = selectall_hashref_query($form, $dbh, $query);
 
@@ -87,8 +87,16 @@ sub retrieve_open_invoices {
 }
 
 sub create_export {
+  my ($self, %params) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_create_export, $self, %params);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _create_export {
   my $self     = shift;
   my %params   = @_;
 
@@ -100,7 +108,7 @@ sub create_export {
   my $vc       = $params{vc} eq 'customer' ? 'customer' : 'vendor';
   my $ARAP     = uc $arap;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
+  my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
   my ($export_id) = selectfirst_array_query($form, $dbh, qq|SELECT nextval('sepa_export_id_seq')|);
   my $query       =
@@ -154,7 +162,7 @@ sub create_export {
       $transfer->{reference} = "${invnumber}-${num_payments}";
     }
 
-    $h_item_id->execute();
+    $h_item_id->execute() || $::form->dberror($q_item_id);
     my ($item_id)      = $h_item_id->fetchrow_array();
 
     my $end_to_end_id  = strftime "LXO%Y%m%d%H%M%S", localtime;
@@ -189,10 +197,6 @@ sub create_export {
   $h_insert->finish();
   $h_item_id->finish();
 
-  $dbh->commit() unless ($params{dbh});
-
-  $main::lxdebug->leave_sub();
-
   return $export_id;
 }
 
@@ -267,15 +271,35 @@ sub close_export {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
+  SL::DB->client->with_transaction(sub {
+    my $dbh      = $params{dbh} || SL::DB->client->dbh;
+
+    my @ids          = ref $params{id} eq 'ARRAY' ? @{ $params{id} } : ($params{id});
+    my $placeholders = join ', ', ('?') x scalar @ids;
+    my $query        = qq|UPDATE sepa_export SET closed = TRUE WHERE id IN ($placeholders)|;
+
+    do_query($form, $dbh, $query, map { conv_i($_) } @ids);
+    1;
+  }) or do { die SL::DB->client->error };
+
+  $main::lxdebug->leave_sub();
+}
+
+sub undo_export {
+  $main::lxdebug->enter_sub();
+
+  my $self     = shift;
+  my %params   = @_;
+
+  Common::check_params(\%params, qw(id));
 
-  my @ids          = ref $params{id} eq 'ARRAY' ? @{ $params{id} } : ($params{id});
-  my $placeholders = join ', ', ('?') x scalar @ids;
-  my $query        = qq|UPDATE sepa_export SET closed = TRUE WHERE id IN ($placeholders)|;
+  my $sepa_export = SL::DB::Manager::SepaExport->find_by(id => $params{id});
 
-  do_query($form, $dbh, $query, map { conv_i($_) } @ids);
+  croak "Not a valid SEPA Export id: $params{id}" unless $sepa_export;
+  croak "Cannot undo closed exports."             if $sepa_export->closed;
+  croak "Cannot undo executed exports."           if $sepa_export->executed;
 
-  $dbh->commit() unless ($params{dbh});
+  die "Could not undo $sepa_export->id" if !$sepa_export->delete();
 
   $main::lxdebug->leave_sub();
 }
@@ -322,12 +346,12 @@ sub list_exports {
 
   if ($filter->{invnumber}) {
     push @where_sub,  "arap.invnumber ILIKE ?";
-    push @values_sub, '%' . $filter->{invnumber} . '%';
+    push @values_sub, like($filter->{invnumber});
     $joins_sub{$arap} = 1;
   }
 
   if ($filter->{message_id}) {
-    push @values, '%' . $filter->{message_id} . '%';
+    push @values, like($filter->{message_id});
     push @where,  <<SQL;
       se.id IN (
         SELECT sepa_export_id
@@ -339,7 +363,7 @@ SQL
 
   if ($filter->{vc}) {
     push @where_sub,  "vc.name ILIKE ?";
-    push @values_sub, '%' . $filter->{vc} . '%';
+    push @values_sub, like($filter->{vc});
     $joins_sub{$arap} = 1;
     $joins_sub{vc}    = 1;
   }
@@ -402,8 +426,16 @@ SQL
 }
 
 sub post_payment {
+  my ($self, %params) = @_;
   $main::lxdebug->enter_sub();
 
+  my $rc = SL::DB->client->with_transaction(\&_post_payment, $self, %params);
+
+  $::lxdebug->leave_sub;
+  return $rc;
+}
+
+sub _post_payment {
   my $self     = shift;
   my %params   = @_;
 
@@ -416,7 +448,7 @@ sub post_payment {
   my $mult     = $params{vc} eq 'customer' ? -1         : 1;
   my $ARAP     = uc $arap;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
+  my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
   my @items    = ref $params{items} eq 'ARRAY' ? @{ $params{items} } : ($params{items});
 
@@ -502,9 +534,53 @@ sub post_payment {
 
   map { $_->[0]->finish() } values %handles;
 
-  $dbh->commit() unless ($params{dbh});
-
-  $main::lxdebug->leave_sub();
+  return 1;
 }
 
 1;
+
+
+__END__
+
+=head1 NAME
+
+SL::SEPA - Base class for SEPA objects
+
+=head1 SYNOPSIS
+
+ # get all open invoices we like to pay via SEPA
+ my $invoices = SL::SEPA->retrieve_open_invoices(vc => 'vendor');
+
+ # add some IBAN and purposes for open transaction
+ # and assign this to a SEPA export
+ my $id = SL::SEPA->create_export('employee'       => $::myconfig{login},
+                                 'bank_transfers' => \@bank_transfers,
+                                 'vc'             => 'vendor');
+
+=head1 DESCRIPTIONS
+
+This is the base class for SEPA. SEPA and the underlying directories
+(SEPA::XML etc) are used to genereate valid XML files for the SEPA
+(Single European Payment Area) specification and offers this structure
+as a download via a xml file.
+
+An export can have one or more transaction which have to
+comply to the specification (IBAN, BIC, amount, purpose, etc).
+
+Furthermore kivitendo sepa exports have two
+valid states: Open or closed and executed or not executed.
+
+The state closed can be set via a user interface and the
+state executed is automatically assigned if the action payment
+is triggered.
+
+=head1 FUNCTIONS
+
+=head2 C<undo_export> $sepa_export_id
+
+Needs a valid sepa_export id and deletes the sepa export if
+the state of the export is neither executed nor closed.
+Returns undef if the deletion was successfully.
+Otherwise the function just dies with a short notice of the id.
+
+=cut
index 3aa88b7..a4cf5f5 100644 (file)
@@ -72,6 +72,10 @@ sub _replace_special_chars {
 
   map { $text =~ s/$_/$special_chars{$_}/g; } keys %special_chars;
 
+  # for all other non ascii chars 'OLÉ S.L.' and 'Årdberg AB'!
+  use Text::Unidecode qw(unidecode);
+  $text = unidecode($text);
+
   return $text;
 }
 
@@ -139,7 +143,7 @@ sub to_xml {
   my $is_coll   = $self->{collection};
   my $cd_src    = $is_coll ? 'Cdtr'              : 'Dbtr';
   my $cd_dst    = $is_coll ? 'Dbtr'              : 'Cdtr';
-  my $pain_id   = $is_coll ? 'pain.008.002.02'   : 'pain.001.002.03';
+  my $pain_id   = $is_coll ? 'pain.008.001.02'   : 'pain.001.001.03';
   my $pain_elmt = $is_coll ? 'CstmrDrctDbtInitn' : 'CstmrCdtTrfInitn';
   my @pii_base  = (strftime('PII%Y%m%d%H%M%S', @now), rand(1000000000));
 
index 7536b56..8392640 100644 (file)
@@ -26,7 +26,7 @@ sub new {
   }
 
   my $path   = $self->prepare_path;
-  $file_name =~ s:.*/::g;
+  $file_name =~ s{.*/}{}g;
   $file_name =  "${path}/${file_name}";
 
   $self->file_name($file_name);
diff --git a/SL/Shop.pm b/SL/Shop.pm
new file mode 100644 (file)
index 0000000..22b5667
--- /dev/null
@@ -0,0 +1,96 @@
+package SL::Shop;
+
+use strict;
+
+use parent qw(Rose::Object);
+use SL::ShopConnector::ALL;
+use SL::DB::Part;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar'                => [ qw(config) ],
+  'scalar --get_set_init' => [ qw(connector) ],
+);
+
+sub updatable_parts {
+  my ($self, $last_update) = @_;
+  $last_update ||= DateTime->now(); # need exact timestamp, with minutes
+
+  my $parts;
+  my $active_shops = SL::DB::Manager::Shop->get_all(query => [ obsolete => 0 ]);
+  foreach my $shop ( @{ $active_shops } ) {
+    # maybe run as an iterator? does that make sense with with_objects?
+    my $update_parts = SL::DB::Manager::ShopPart->get_all(query => [
+             and => [
+                'active' => 1,
+                'shop_id' => $shop->id,
+                # shop => '1',
+                or => [ 'part.mtime' => { ge => $last_update },
+                        'part.itime' => { ge => $last_update },
+                        'itime'      => { ge => $last_update },
+                        'mtime'      => { ge => $last_update },
+                      ],
+                    ]
+             ],
+             with_objects => ['shop', 'part'],
+             # multi_many_ok   => 1,
+          );
+    push( @{ $parts }, @{ $update_parts });
+  };
+  return $parts;
+
+};
+
+sub check_connectivity {
+  my ($self) = @_;
+  my $version = $self->connector->get_version;
+  return $version;
+}
+
+sub init_connector {
+  my ($self) = @_;
+  # determine the connector from the connector type in the webshop config
+  return SL::ShopConnector::ALL->shop_connector_class_by_name($self->config->connector)->new( config => $self->config);
+
+};
+
+1;
+
+__END__
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Shop - Do stuff with WebShop instances
+
+=head1 SYNOPSIS
+
+my $config = SL::DB::Manager::Shop->get_first();
+my $shop = SL::Shop->new( config => $config );
+
+From the config we know which Connector class to load, save in $shop->connector
+and do stuff from there:
+
+$shop->connector->get_new_orders;
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<updatable_parts>
+
+=item C<check_connectivity>
+
+=item C<init_connector>
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+G. Richardson <lt>information@kivitendo-premium.deE<gt>
+
+=cut
diff --git a/SL/ShopConnector/ALL.pm b/SL/ShopConnector/ALL.pm
new file mode 100644 (file)
index 0000000..1b1b0c8
--- /dev/null
@@ -0,0 +1,39 @@
+package SL::ShopConnector::ALL;
+
+use strict;
+
+use SL::ShopConnector::Shopware;
+use SL::ShopConnector::Shopware6;
+use SL::ShopConnector::WooCommerce;
+
+my %shop_connector_by_name = (
+  shopware    => 'SL::ShopConnector::Shopware',
+  shopware6   => 'SL::ShopConnector::Shopware6',
+  woocommerce => 'SL::ShopConnector::WooCommerce',
+);
+
+my @shop_connector_order = qw(
+  woocommerce
+  shopware
+  shopware6
+);
+
+my @shop_connectors = (
+  { id => "shopware",    description => "Shopware" },
+  { id => "shopware6",   description => "Shopware6" },
+  { id => "woocommerce", description => "WooCommerce" },
+);
+
+
+sub all_shop_connectors {
+  map { $shop_connector_by_name{$_} } @shop_connector_order;
+}
+
+sub shop_connector_class_by_name {
+  $shop_connector_by_name{$_[1]};
+}
+
+sub connectors {
+  \@shop_connectors;
+}
+1;
diff --git a/SL/ShopConnector/Base.pm b/SL/ShopConnector/Base.pm
new file mode 100644 (file)
index 0000000..1dc4a2e
--- /dev/null
@@ -0,0 +1,159 @@
+package SL::ShopConnector::Base;
+
+use strict;
+
+use parent qw(SL::DB::Object);
+use Rose::Object::MakeMethods::Generic (
+  scalar => [ qw(config) ],
+);
+
+sub get_one_order  {
+  die 'get_one_order needs to be implemented';
+
+  my ($self, $ordnumber) = @_;
+  my %fetched_order;
+
+  # 1. fetch the order and import it as a kivi order
+  # 2. update the order state for report
+  # 3. return a hash with either success or error state
+  my $one_order; # REST call
+
+  my $error = $self->import_data_to_shop_order($one_order);
+
+  $self->set_orderstatus($one_order->{id}, "fetched") unless $error;
+
+  return \(
+      shop_id          => $self->config->id,
+      shop_description => $self->config->description,
+      number_of_orders => $error ? 0 : 1,
+      message          => $error ? "Error: $error->{msg}"  : '',
+      error            => $error ? 1 : 0,
+    );
+}
+
+
+sub get_new_orders { die 'get_order needs to be implemented' }
+
+sub update_part    {
+  die 'update_part needs to be implemented';
+
+  my ($self, $shop_part, $todo) = @_;
+  #shop_part is passed as a param
+  die "Need a valid Shop Part for updating Part" unless ref($shop_part) eq 'SL::DB::ShopPart';
+  die "Invalid todo for updating Part"           unless $todo =~ m/(price|stock|price_stock|active|all)/;
+
+  my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
+  die "Shop Part but no kivi Part?" unless ref $part eq 'SL::DB::Part';
+
+  my $success;
+
+  return $success ? 1 : 0;
+}
+
+sub get_article    { die 'get_article needs to be implemented' }
+
+sub get_categories { die 'get_categories needs to be implemented' }
+
+sub get_version    {
+
+  die 'get_version needs to be implemented';
+  # has to return a hashref with this structure:
+  # version has to return the connection error message
+  my $connect = {};
+  $connect->{success}         = 0 || 1;
+  $connect->{data}->{version} = '1234';
+  return $connect;
+}
+
+sub set_orderstatus { die 'set_orderstatus needs to be implemented' }
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+  SL::ShopConnectorBase - this is the base class for shop connectors
+
+=head1 SYNOPSIS
+
+
+=head1 DESCRIPTION
+
+=head1 AVAILABLE METHODS
+
+=over 4
+
+=item C<get_one_order $ordnumber>
+
+Needs a order number and fetch (one or more) orders
+which are returned by the Shop system. The function
+has to take care of getting the order including customer
+and item information to kivi.
+It has to return a hash with either the number of succesful
+imported order or within the same hash structure a error message.
+
+
+
+=item C<get_new_orders>
+
+=item C<update_part $shop_part $todo>
+
+Updates one Part including all metadata and Images in a Shop.
+Needs a valid 'SL::DB::ShopPart' as first parameter and a requested action
+as second parameter. Valid values for the second parameter are
+"price, stock, price_stock, active, all".
+The method updates either all metadata or a subset.
+Name and action for the subsets:
+
+ price       => updates only the price
+ stock       => updates only the available stock (qty)
+ price_stock => combines both predecessors
+ active      => updates only the state of the shop part
+
+Images should always be updated, regardless of the requested action.
+Returns 1 if all updates were executed successful.
+
+
+
+=item C<get_article>
+
+=item C<get_categories>
+
+=item C<get_version>
+
+IMPORTANT: This call is used to test the connection and if succesful
+it returns the version number of the shop. If not succesful the
+returning function has to make sure a error string is returned in
+the same data structure. Details of the returning hashref:
+
+ my $connect = {};
+ $connect->{success}         = 0 || 1;
+ $connect->{data}->{version} = '1234';
+ return $connect;
+
+=item C<set_orderstatus>
+
+Sets the state of the order in the Shop.
+Valid values depend on the Shop API, common states
+are delivered, fetched, paid, in progress ...
+
+
+=back
+
+=head1 SEE ALSO
+
+L<SL::ShopConnector::ALL>
+
+=head1 BUGS
+
+None yet. :)
+
+=head1 AUTHOR
+
+G. Richardson <lt>information@kivitendo-premium.deE<gt>
+W. Hahn E<lt>wh@futureworldsearch.netE<gt>
+
+=cut
diff --git a/SL/ShopConnector/Shopware.pm b/SL/ShopConnector/Shopware.pm
new file mode 100644 (file)
index 0000000..65c42fd
--- /dev/null
@@ -0,0 +1,550 @@
+package SL::ShopConnector::Shopware;
+
+use strict;
+
+use parent qw(SL::ShopConnector::Base);
+
+
+use SL::JSON;
+use LWP::UserAgent;
+use LWP::Authen::Digest;
+use SL::DB::ShopOrder;
+use SL::DB::ShopOrderItem;
+use SL::DB::History;
+use SL::DB::PaymentTerm;
+use DateTime::Format::Strptime;
+use SL::DB::File;
+use Data::Dumper;
+use Sort::Naturally ();
+use SL::Helper::Flash;
+use Encode qw(encode_utf8);
+use SL::File;
+use File::Slurp;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(connector url) ],
+);
+
+sub get_one_order {
+  my ($self, $ordnumber) = @_;
+
+  my $dbh       = SL::DB::client;
+  my $of        = 0;
+  my $url       = $self->url;
+  my $data      = $self->connector->get($url . "api/orders/$ordnumber?useNumberAsId=true");
+  my @errors;
+
+  my %fetched_orders;
+  if ($data->is_success && $data->content_type eq 'application/json'){
+    my $data_json = $data->content;
+    my $import    = SL::JSON::decode_json($data_json);
+    my $shoporder = $import->{data};
+    $dbh->with_transaction( sub{
+      $self->import_data_to_shop_order($import);
+      1;
+    })or do {
+      push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
+    };
+
+    if(!@errors){
+      $self->set_orderstatus($import->{data}->{id}, "fetched");
+      $of++;
+    }else{
+      flash_later('error', $::locale->text('Database errors: #1', @errors));
+    }
+    %fetched_orders = (shop_description => $self->config->description, number_of_orders => $of);
+  } else {
+    my %error_msg  = (
+      shop_id          => $self->config->id,
+      shop_description => $self->config->description,
+      message          => "Error: $data->status_line",
+      error            => 1,
+    );
+    %fetched_orders = %error_msg;
+  }
+
+  return \%fetched_orders;
+}
+
+sub get_new_orders {
+  my ($self, $id) = @_;
+
+  my $url              = $self->url;
+  my $last_order_number = $self->config->last_order_number;
+  my $otf              = $self->config->orders_to_fetch;
+  my $of               = 0;
+  my $last_data      = $self->connector->get($url . "api/orders/$last_order_number?useNumberAsId=true");
+  my $last_data_json = $last_data->content;
+  my $last_import    = SL::JSON::decode_json($last_data_json);
+
+  my $orders_data      = $self->connector->get($url . "api/orders?limit=$otf&filter[1][property]=status&filter[1][value]=0&filter[0][property]=id&filter[0][expression]=>&filter[0][value]=" . $last_import->{data}->{id});
+
+  my $dbh = SL::DB->client;
+  my @errors;
+  my %fetched_orders;
+  if ($orders_data->is_success && $orders_data->content_type eq 'application/json'){
+    my $orders_data_json = $orders_data->content;
+    my $orders_import    = SL::JSON::decode_json($orders_data_json);
+    foreach my $shoporder(@{ $orders_import->{data} }){
+
+      my $data      = $self->connector->get($url . "api/orders/" . $shoporder->{id});
+      my $data_json = $data->content;
+      my $import    = SL::JSON::decode_json($data_json);
+
+      $dbh->with_transaction( sub{
+          $self->import_data_to_shop_order($import);
+
+          $self->config->assign_attributes( last_order_number => $shoporder->{number});
+          $self->config->save;
+          1;
+      })or do {
+        push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
+      };
+
+      if(!@errors){
+        $self->set_orderstatus($shoporder->{id}, "fetched");
+        $of++;
+      }else{
+        flash_later('error', $::locale->text('Database errors: #1', @errors));
+      }
+    }
+    %fetched_orders = (shop_description => $self->config->description, number_of_orders => $of);
+  } else {
+    my %error_msg  = (
+      shop_id          => $self->config->id,
+      shop_description => $self->config->description,
+      message          => "Error: $orders_data->status_line",
+      error            => 1,
+    );
+    %fetched_orders = %error_msg;
+  }
+
+  return \%fetched_orders;
+}
+
+sub import_data_to_shop_order {
+  my ( $self, $import ) = @_;
+  my $shop_order = $self->map_data_to_shoporder($import);
+
+  $shop_order->save;
+  my $id = $shop_order->id;
+
+  my @positions = sort { Sort::Naturally::ncmp($a->{"articleNumber"}, $b->{"articleNumber"}) } @{ $import->{data}->{details} };
+  #my @positions = sort { Sort::Naturally::ncmp($a->{"partnumber"}, $b->{"partnumber"}) } @{ $import->{data}->{details} };
+  my $position = 1;
+  my $active_price_source = $self->config->price_source;
+  #Mapping Positions
+  foreach my $pos(@positions) {
+    my $price = $::form->round_amount($pos->{price},2);
+    my %pos_columns = ( description          => $pos->{articleName},
+                        partnumber           => $pos->{articleNumber},
+                        price                => $price,
+                        quantity             => $pos->{quantity},
+                        position             => $position,
+                        tax_rate             => $pos->{taxRate},
+                        shop_trans_id        => $pos->{articleId},
+                        shop_order_id        => $id,
+                        active_price_source  => $active_price_source,
+                      );
+    my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
+    $pos_insert->save;
+    $position++;
+  }
+  $shop_order->positions($position-1);
+
+  if ( $self->config->shipping_costs_parts_id ) {
+    my $shipping_part = SL::DB::Part->find_by( id => $self->config->shipping_costs_parts_id);
+    my %shipping_pos = ( description    => $import->{data}->{dispatch}->{name},
+                         partnumber     => $shipping_part->partnumber,
+                         price          => $import->{data}->{invoiceShipping},
+                         quantity       => 1,
+                         position       => $position,
+                         shop_trans_id  => 0,
+                         shop_order_id  => $id,
+                       );
+    my $shipping_pos_insert = SL::DB::ShopOrderItem->new(%shipping_pos);
+    $shipping_pos_insert->save;
+  }
+
+  my $customer = $shop_order->get_customer;
+
+  if(ref($customer)){
+    $shop_order->kivi_customer_id($customer->id);
+  }
+  $shop_order->save;
+}
+
+sub map_data_to_shoporder {
+  my ($self, $import) = @_;
+
+  my $parser = DateTime::Format::Strptime->new( pattern   => '%Y-%m-%dT%H:%M:%S',
+                                                  locale    => 'de_DE',
+                                                  time_zone => 'local'
+                                                );
+  my $orderdate = $parser->parse_datetime($import->{data}->{orderTime});
+
+  my $shop_id      = $self->config->id;
+  my $tax_included = $self->config->pricetype;
+
+  # Mapping Zahlungsmethoden muss an Firmenkonfiguration angepasst werden
+  my %payment_ids_methods = (
+    # shopware_paymentId => kivitendo_payment_id
+  );
+  my $default_payment    = SL::DB::Manager::PaymentTerm->get_first();
+  my $default_payment_id = $default_payment ? $default_payment->id : undef;
+  # Mapping to table shoporders. See http://community.shopware.com/_detail_1690.html#GET_.28Liste.29
+  my %columns = (
+    amount                  => $import->{data}->{invoiceAmount},
+    billing_city            => $import->{data}->{billing}->{city},
+    billing_company         => $import->{data}->{billing}->{company},
+    billing_country         => $import->{data}->{billing}->{country}->{name},
+    billing_department      => $import->{data}->{billing}->{department},
+    billing_email           => $import->{data}->{customer}->{email},
+    billing_fax             => $import->{data}->{billing}->{fax},
+    billing_firstname       => $import->{data}->{billing}->{firstName},
+    #billing_greeting        => ($import->{data}->{billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
+    billing_lastname        => $import->{data}->{billing}->{lastName},
+    billing_phone           => $import->{data}->{billing}->{phone},
+    billing_street          => $import->{data}->{billing}->{street},
+    billing_vat             => $import->{data}->{billing}->{vatId},
+    billing_zipcode         => $import->{data}->{billing}->{zipCode},
+    customer_city           => $import->{data}->{billing}->{city},
+    customer_company        => $import->{data}->{billing}->{company},
+    customer_country        => $import->{data}->{billing}->{country}->{name},
+    customer_department     => $import->{data}->{billing}->{department},
+    customer_email          => $import->{data}->{customer}->{email},
+    customer_fax            => $import->{data}->{billing}->{fax},
+    customer_firstname      => $import->{data}->{billing}->{firstName},
+    #customer_greeting       => ($import->{data}->{billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
+    customer_lastname       => $import->{data}->{billing}->{lastName},
+    customer_phone          => $import->{data}->{billing}->{phone},
+    customer_street         => $import->{data}->{billing}->{street},
+    customer_vat            => $import->{data}->{billing}->{vatId},
+    customer_zipcode        => $import->{data}->{billing}->{zipCode},
+    customer_newsletter     => $import->{data}->{customer}->{newsletter},
+    delivery_city           => $import->{data}->{shipping}->{city},
+    delivery_company        => $import->{data}->{shipping}->{company},
+    delivery_country        => $import->{data}->{shipping}->{country}->{name},
+    delivery_department     => $import->{data}->{shipping}->{department},
+    delivery_email          => "",
+    delivery_fax            => $import->{data}->{shipping}->{fax},
+    delivery_firstname      => $import->{data}->{shipping}->{firstName},
+    #delivery_greeting       => ($import->{data}->{shipping}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
+    delivery_lastname       => $import->{data}->{shipping}->{lastName},
+    delivery_phone          => $import->{data}->{shipping}->{phone},
+    delivery_street         => $import->{data}->{shipping}->{street},
+    delivery_vat            => $import->{data}->{shipping}->{vatId},
+    delivery_zipcode        => $import->{data}->{shipping}->{zipCode},
+    host                    => $import->{data}->{shop}->{hosts},
+    netamount               => $import->{data}->{invoiceAmountNet},
+    order_date              => $orderdate,
+    payment_description     => $import->{data}->{payment}->{description},
+    payment_id              => $payment_ids_methods{$import->{data}->{paymentId}} || $default_payment_id,
+    remote_ip               => $import->{data}->{remoteAddress},
+    sepa_account_holder     => $import->{data}->{paymentIntances}->{accountHolder},
+    sepa_bic                => $import->{data}->{paymentIntances}->{bic},
+    sepa_iban               => $import->{data}->{paymentIntances}->{iban},
+    shipping_costs          => $import->{data}->{invoiceShipping},
+    shipping_costs_net      => $import->{data}->{invoiceShippingNet},
+    shop_c_billing_id       => $import->{data}->{billing}->{customerId},
+    shop_c_billing_number   => $import->{data}->{billing}->{number},
+    shop_c_delivery_id      => $import->{data}->{shipping}->{id},
+    shop_customer_id        => $import->{data}->{customerId},
+    shop_customer_number    => $import->{data}->{billing}->{number},
+    shop_customer_comment   => $import->{data}->{customerComment},
+    shop_id                 => $shop_id,
+    shop_ordernumber        => $import->{data}->{number},
+    shop_trans_id           => $import->{data}->{id},
+    tax_included            => $tax_included eq "brutto" ? 1 : 0,
+  );
+
+  my $shop_order = SL::DB::ShopOrder->new(%columns);
+  return $shop_order;
+}
+
+sub get_categories {
+  my ($self) = @_;
+
+  my $url        = $self->url;
+  my $data       = $self->connector->get($url . "api/categories");
+  my $data_json  = $data->content;
+  my $import     = SL::JSON::decode_json($data_json);
+  my @daten      = @{$import->{data}};
+  my %categories = map { ($_->{id} => $_) } @daten;
+
+  my @categories_tree;
+  for(@daten) {
+    # ignore root with id=1
+    if( $_->{id} == 1) {
+      next;
+    }
+    my $parent = $categories{$_->{parentId}};
+    if($parent && $parent->{id} != 1) {
+      $parent->{children} ||= [];
+      push @{$parent->{children}},$_;
+    } else {
+      push @categories_tree, $_;
+    }
+  }
+
+  return \@categories_tree;
+}
+
+sub get_version {
+  my ($self) = @_;
+
+  my $url       = $self->url;
+  my $data      = $self->connector->get($url . "api/version");
+  my $type = $data->content_type;
+  my $status_line = $data->status_line;
+
+  if($data->is_success && $type eq 'application/json'){
+    my $data_json = $data->content;
+    return SL::JSON::decode_json($data_json);
+  }else{
+    my %return = ( success => 0,
+                   data    => { version => $url . ": " . $status_line, revision => $type },
+                   message => "Server not found or wrong data type",
+                );
+    return \%return;
+  }
+}
+
+sub update_part {
+  my ($self, $shop_part, $todo) = @_;
+
+  #shop_part is passed as a param
+  die unless ref($shop_part) eq 'SL::DB::ShopPart';
+
+  my $url = $self->url;
+  my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
+
+  # CVARS to map
+  my $cvars = { map { ($_->config->name => { value => $_->value_as_text, is_valid => $_->is_valid }) } @{ $part->cvars_by_config } };
+
+  my @cat = ();
+  foreach my $row_cat ( @{ $shop_part->shop_category } ) {
+    my $temp = { ( id => @{$row_cat}[0], ) };
+    push ( @cat, $temp );
+  }
+
+  my @upload_img = $shop_part->get_images;
+  my $tax_n_price = $shop_part->get_tax_and_price;
+  my $price = $tax_n_price->{price};
+  my $taxrate = $tax_n_price->{tax};
+  # mapping to shopware still missing attributes,metatags
+  my %shop_data;
+
+  if($todo eq "price"){
+    %shop_data = ( mainDetail => { number   => $part->partnumber,
+                                   prices   =>  [ { from             => 1,
+                                                    price            => $price,
+                                                    customerGroupKey => 'EK',
+                                                  },
+                                                ],
+                                  },
+                 );
+  }elsif($todo eq "stock"){
+    %shop_data = ( mainDetail => { number   => $part->partnumber,
+                                   inStock  => $part->onhand,
+                                 },
+                 );
+  }elsif($todo eq "price_stock"){
+    %shop_data =  ( mainDetail => { number   => $part->partnumber,
+                                    inStock  => $part->onhand,
+                                    prices   =>  [ { from             => 1,
+                                                     price            => $price,
+                                                     customerGroupKey => 'EK',
+                                                   },
+                                                 ],
+                                   },
+                   );
+  }elsif($todo eq "active"){
+    %shop_data =  ( mainDetail => { number   => $part->partnumber,
+                                   },
+                    active => ($part->partnumber == 1 ? 0 : 1),
+                   );
+  }elsif($todo eq "all"){
+  # mapping to shopware still missing attributes,metatags
+    %shop_data =  (   name              => $part->description,
+                      mainDetail        => { number   => $part->partnumber,
+                                             inStock  => $part->onhand,
+                                             prices   =>  [ {          from   => 1,
+                                                                       price  => $price,
+                                                            customerGroupKey  => 'EK',
+                                                            },
+                                                          ],
+                                             active   => $shop_part->active,
+                                             #attribute => { attr1  => $cvars->{CVARNAME}->{value}, } , #HowTo handle attributes
+                                       },
+                      supplier          => 'AR', # Is needed by shopware,
+                      descriptionLong   => $shop_part->shop_description,
+                      active            => $shop_part->active,
+                      images            => [ @upload_img ],
+                      __options_images  => { replace => 1, },
+                      categories        => [ @cat ],
+                      description       => $shop_part->shop_description,
+                      categories        => [ @cat ],
+                      tax               => $taxrate,
+                    )
+                  ;
+  }
+
+  my $dataString = SL::JSON::to_json(\%shop_data);
+  $dataString    = encode_utf8($dataString);
+
+  my $upload_content;
+  my $upload;
+  my ($import,$data,$data_json);
+  my $partnumber = $::form->escape($part->partnumber);#shopware don't accept / in articlenumber
+  # Shopware RestApi sends an erroremail if configured and part not found. But it needs this info to decide if update or create a new article
+  # LWP->post = create LWP->put = update
+    $data       = $self->connector->get($url . "api/articles/$partnumber?useNumberAsId=true");
+    $data_json  = $data->content;
+    $import     = SL::JSON::decode_json($data_json);
+  if($import->{success}){
+    #update
+    my $partnumber  = $::form->escape($part->partnumber);#shopware don't accept / in articlenumber
+    $upload         = $self->connector->put($url . "api/articles/$partnumber?useNumberAsId=true", Content => $dataString);
+    my $data_json   = $upload->content;
+    $upload_content = SL::JSON::decode_json($data_json);
+  }else{
+    #upload
+    $upload         = $self->connector->post($url . "api/articles/", Content => $dataString);
+    my $data_json   = $upload->content;
+    $upload_content = SL::JSON::decode_json($data_json);
+  }
+  # don't know if this is needed
+  if(@upload_img) {
+    my $partnumber = $::form->escape($part->partnumber);#shopware don't accept / in articlenumber
+    my $imgup      = $self->connector->put($url . "api/generatearticleimages/$partnumber?useNumberAsId=true");
+  }
+
+  return $upload_content->{success};
+}
+
+sub get_article {
+  my ($self,$partnumber) = @_;
+
+  my $url       = $self->url;
+  $partnumber   = $::form->escape($partnumber);#shopware don't accept / in articlenumber
+  my $data      = $self->connector->get($url . "api/articles/$partnumber?useNumberAsId=true");
+  my $data_json = $data->content;
+  return SL::JSON::decode_json($data_json);
+}
+
+sub set_orderstatus {
+  my ($self,$order_id, $status) = @_;
+  if ($status eq "fetched") { $status = 1; }
+  if ($status eq "completed") { $status = 2; }
+  my %new_status = (orderStatusId => $status);
+  my $new_status_json = SL::JSON::to_json(\%new_status);
+  $self->connector->put($self->url . "api/orders/$order_id", Content => $new_status_json);
+}
+
+sub init_url {
+  my ($self) = @_;
+  $self->url($self->config->protocol . "://" . $self->config->server . ":" . $self->config->port . $self->config->path);
+}
+
+sub init_connector {
+  my ($self) = @_;
+  my $ua = LWP::UserAgent->new;
+  $ua->credentials(
+      $self->config->server . ":" . $self->config->port,
+      $self->config->realm,
+      $self->config->login => $self->config->password
+  );
+
+  return $ua;
+
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Shopconnector::Shopware - connector for shopware 5
+
+=head1 SYNOPSIS
+
+
+=head1 DESCRIPTION
+
+This is the connector to shopware.
+In this file you can do the mapping to your needs.
+see https://developers.shopware.com/developers-guide/rest-api/
+for more information.
+
+=head1 METHODS
+
+=over 4
+
+=item C<get_one_order>
+
+Fetches one order specified by ordnumber
+
+=item C<get_new_orders>
+
+Fetches new order by parameters from shop configuration
+
+=item C<import_data_to_shop_order>
+
+Creates on shoporder object from json
+Here is the mapping for the positions.
+see https://developers.shopware.com/developers-guide/rest-api/
+for detailed information
+
+=item C<map_data_to_shoporder>
+
+Here is the mapping for the order data.
+see https://developers.shopware.com/developers-guide/rest-api/
+for detailed information
+
+=item C<get_categories>
+
+=item C<get_version>
+
+Use this for test Connection
+see SL::Shop
+
+=item C<update_part>
+
+Here is the mapping for the article data.
+see https://developers.shopware.com/developers-guide/rest-api/
+for detailed information
+
+=item C<get_article>
+
+=back
+
+=head1 INITS
+
+=over 4
+
+=item init_url
+
+build an url for LWP
+
+=item init_connector
+
+=back
+
+=head1 TODO
+
+Pricesrules, pricessources aren't fully implemented yet.
+Payments aren't implemented( need to map payments from Shopware like invoice, paypal etc. to payments in kivitendo)
+
+=head1 BUGS
+
+None yet. :)
+
+=head1 AUTHOR
+
+W. Hahn E<lt>wh@futureworldsearch.netE<gt>
+
+=cut
diff --git a/SL/ShopConnector/Shopware6.pm b/SL/ShopConnector/Shopware6.pm
new file mode 100644 (file)
index 0000000..6d04a1b
--- /dev/null
@@ -0,0 +1,1095 @@
+package SL::ShopConnector::Shopware6;
+
+use strict;
+
+use parent qw(SL::ShopConnector::Base);
+
+use Carp;
+use Encode qw(encode);
+use List::Util qw(first);
+use REST::Client;
+use Try::Tiny;
+
+use SL::JSON;
+use SL::Helper::Flash;
+use SL::Locale::String qw(t8);
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(connector) ],
+);
+
+sub all_open_orders {
+  my ($self) = @_;
+
+  my $assoc = {
+              'associations' => {
+                'deliveries'   => {
+                  'associations' => {
+                    'shippingMethod' => [],
+                      'shippingOrderAddress' => {
+                        'associations' => {
+                                            'salutation'   => [],
+                                            'country'      => [],
+                                            'countryState' => []
+                                          }
+                                                }
+                                     }
+                                   }, # end deliveries
+                'language' => [],
+                'orderCustomer' => [],
+                'addresses' => {
+                  'associations' => {
+                                      'salutation'   => [],
+                                      'countryState' => [],
+                                      'country'      => []
+                                    }
+                                },
+                'tags' => [],
+                'lineItems' => {
+                  'associations' => {
+                    'product' => {
+                      'associations' => {
+                                          'tax' => []
+                                        }
+                                 }
+                                    }
+                                }, # end line items
+                'salesChannel' => [],
+                  'documents' => {          # currently not used
+                    'associations' => {
+                      'documentType' => []
+                                      }
+                                 },
+                'transactions' => {
+                  'associations' => {
+                    'paymentMethod' => []
+                                    }
+                                  },
+                'currency' => []
+            }, # end associations
+         'limit' => $self->config->orders_to_fetch ? $self->config->orders_to_fetch : undef,
+        # 'page' => 1,
+     'aggregations' => [
+                            {
+                              'field'      => 'billingAddressId',
+                              'definition' => 'order_address',
+                              'name'       => 'BillingAddress',
+                              'type'       => 'entity'
+                            }
+                          ],
+        'filter' => [
+                     {
+                        'value' => 'open', # open or completed (mind the past)
+                        'type' => 'equals',
+                        'field' => 'order.stateMachineState.technicalName'
+                      }
+                    ],
+        'total-count-mode' => 0
+      };
+  return $assoc;
+}
+
+# used for get_new_orders and get_one_order
+sub get_fetched_order_structure {
+  my ($self) = @_;
+  # set known params for the return structure
+  my %fetched_order  = (
+      shop_id          => $self->config->id,
+      shop_description => $self->config->description,
+      message          => '',
+      error            => '',
+      number_of_orders => 0,
+    );
+  return %fetched_order;
+}
+
+sub update_part {
+  my ($self, $shop_part, $todo) = @_;
+
+  #shop_part is passed as a param
+  croak t8("Need a valid Shop Part for updating Part") unless ref($shop_part) eq 'SL::DB::ShopPart';
+  croak t8("Invalid todo for updating Part")           unless $todo =~ m/(price|stock|price_stock|active|all)/;
+
+  my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
+  die "Shop Part but no kivi Part?" unless ref $part eq 'SL::DB::Part';
+
+  my $tax_n_price = $shop_part->get_tax_and_price;
+  my $price       = $tax_n_price->{price};
+  my $taxrate     = $tax_n_price->{tax};
+
+  # simple calc for both cases, always give sw6 the calculated gross price
+  my ($net, $gross);
+  if ($self->config->pricetype eq 'brutto') {
+    $gross = $price;
+    $net   = $price / (1 + $taxrate/100);
+  } elsif ($self->config->pricetype eq 'netto') {
+    $net   = $price;
+    $gross = $price * (1 + $taxrate/100);
+  } else { die "Invalid state for price type"; }
+
+  my $update_p;
+  $update_p->{productNumber} = $part->partnumber;
+  $update_p->{name}          = _u8($part->description);
+  $update_p->{description}   =   $shop_part->shop->use_part_longdescription
+                               ? _u8($part->notes)
+                               : _u8($shop_part->shop_description);
+
+  # locales simple check for english
+  my $english = SL::DB::Manager::Language->get_first(query => [ description   => { ilike => 'Englisch' },
+                                                        or => [ template_code => { ilike => 'en' } ],
+                                                    ]);
+  if (ref $english eq 'SL::DB::Language') {
+    # add english translation for product
+    # TODO (or not): No Translations for shop_part->shop_description available
+    my $translation = first { $english->id == $_->language_id } @{ $part->translations };
+    $update_p->{translations}->{'en-GB'}->{name}        = _u8($translation->{translation});
+    $update_p->{translations}->{'en-GB'}->{description} = _u8($translation->{longdescription});
+  }
+
+  $update_p->{stock}  = $::form->round_amount($part->onhand, 0) if ($todo =~ m/(stock|all)/);
+  # JSON::true JSON::false
+  # These special values become JSON true and JSON false values, respectively.
+  # You can also use \1 and \0 directly if you want
+  $update_p->{active} = (!$part->obsolete && $part->shop) ? \1 : \0 if ($todo =~ m/(active|all)/);
+
+  # 1. check if there is already a product
+  my $product_filter = {
+          'filter' => [
+                        {
+                          'value' => $part->partnumber,
+                          'type'  => 'equals',
+                          'field' => 'productNumber'
+                        }
+                      ]
+    };
+  my $ret = $self->connector->POST('api/search/product', to_json($product_filter));
+  my $response_code = $ret->responseCode();
+  die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+
+  my $one_d; # maybe empty
+  try {
+    $one_d = from_json($ret->responseContent())->{data}->[0];
+  } catch { die "Malformed JSON Data: $_ " . $ret->responseContent();  };
+  # edit or create if not found
+  if ($one_d->{id}) {
+    #update
+    # we need price object structure and taxId
+    $update_p->{$_} = $one_d->{$_} foreach qw(taxId price);
+    if ($todo =~ m/(price|all)/) {
+      $update_p->{price}->[0]->{gross} = $gross;
+    }
+    undef $update_p->{partNumber}; # we dont need this one
+    $ret = $self->connector->PATCH('api/product/' . $one_d->{id}, to_json($update_p));
+    unless (204 == $ret->responseCode()) {
+      die t8('Part Description is too long for this Shopware version. It should have lower than 255 characters.')
+         if $ret->responseContent() =~ m/Diese Zeichenkette ist zu lang. Sie sollte.*255 Zeichen/;
+      die "Updating part with " .  $part->partnumber . " failed: " . $ret->responseContent() unless (204 == $ret->responseCode());
+    }
+  } else {
+    # create part
+    # 1. get the correct tax for this product
+    my $tax_filter = {
+          'filter' => [
+                        {
+                          'value' => $taxrate,
+                          'type' => 'equals',
+                          'field' => 'taxRate'
+                        }
+                      ]
+        };
+    $ret = $self->connector->POST('api/search/tax', to_json($tax_filter));
+    die "Search for Tax with rate: " .  $part->partnumber . " failed: " . $ret->responseContent() unless (200 == $ret->responseCode());
+    try {
+      $update_p->{taxId} = from_json($ret->responseContent())->{data}->[0]->{id};
+    } catch { die "Malformed JSON Data or Taxkey entry missing: $_ " . $ret->responseContent();  };
+
+    # 2. get the correct currency for this product
+    my $currency_filter = {
+        'filter' => [
+                      {
+                        'value' => SL::DB::Default->get_default_currency,
+                        'type' => 'equals',
+                        'field' => 'isoCode'
+                      }
+                    ]
+      };
+    $ret = $self->connector->POST('api/search/currency', to_json($currency_filter));
+    die "Search for Currency with ISO Code: " . SL::DB::Default->get_default_currency . " failed: "
+      . $ret->responseContent() unless (200 == $ret->responseCode());
+
+    try {
+      $update_p->{price}->[0]->{currencyId} = from_json($ret->responseContent())->{data}->[0]->{id};
+    } catch { die "Malformed JSON Data or Currency ID entry missing: $_ " . $ret->responseContent();  };
+
+    # 3. add net and gross price and allow variants
+    $update_p->{price}->[0]->{gross}  = $gross;
+    $update_p->{price}->[0]->{net}    = $net;
+    $update_p->{price}->[0]->{linked} = \1; # link product variants
+
+    $ret = $self->connector->POST('api/product', to_json($update_p));
+    die "Create for Product " .  $part->partnumber . " failed: " . $ret->responseContent() unless (204 == $ret->responseCode());
+  }
+
+  # if there are images try to sync this with the shop_part
+  try {
+    $self->sync_all_images(shop_part => $shop_part, set_cover => 1, delete_orphaned => 1);
+  } catch { die "Could not sync images for Part " . $part->partnumber . " Reason: $_" };
+
+  # if there are categories try to sync this with the shop_part
+  try {
+    $self->sync_all_categories(shop_part => $shop_part);
+  } catch { die "Could not sync Categories for Part " . $part->partnumber . " Reason: $_" };
+
+  return 1; # no invalid response code -> success
+}
+sub sync_all_categories {
+  my ($self, %params) = @_;
+
+  my $shop_part = delete $params{shop_part};
+  croak "Need a valid Shop Part for updating Images" unless ref($shop_part) eq 'SL::DB::ShopPart';
+
+  my $partnumber = $shop_part->part->partnumber;
+  die "Shop Part but no kivi Partnumber" unless $partnumber;
+
+  my ($ret, $response_code);
+  # 1 get  uuid for product
+  my $product_filter = {
+          'filter' => [
+                        {
+                          'value' => $partnumber,
+                          'type'  => 'equals',
+                          'field' => 'productNumber'
+                        }
+                      ]
+    };
+
+  $ret = $self->connector->POST('api/search/product', to_json($product_filter));
+  $response_code = $ret->responseCode();
+  die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+  my ($product_id, $category_tree);
+  try {
+    $product_id    = from_json($ret->responseContent())->{data}->[0]->{id};
+    $category_tree = from_json($ret->responseContent())->{data}->[0]->{categoryIds};
+  } catch { die "Malformed JSON Data: $_ " . $ret->responseContent();  };
+  my $cat;
+  # if the part is connected to a category at all
+  if ($shop_part->shop_category) {
+    foreach my $row_cat (@{ $shop_part->shop_category }) {
+      $cat->{@{ $row_cat }[0]} = @{ $row_cat }[1];
+    }
+  }
+  # delete
+  foreach my $shopware_cat (@{ $category_tree }) {
+    if ($cat->{$shopware_cat}) {
+      # cat exists and no delete
+      delete $cat->{$shopware_cat};
+      next;
+    }
+    # cat exists and delete
+    $ret = $self->connector->DELETE("api/product/$product_id/categories/$shopware_cat");
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
+  }
+  # now add only new categories
+  my $p;
+  $p->{id}  = $product_id;
+  $p->{categories} = ();
+  foreach my $new_cat (keys %{ $cat }) {
+    push @{ $p->{categories} }, {id => $new_cat};
+  }
+    $ret = $self->connector->PATCH("api/product/$product_id", to_json($p));
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
+}
+
+sub sync_all_images {
+  my ($self, %params) = @_;
+
+  $params{set_cover}       //= 1;
+  $params{delete_orphaned} //= 0;
+
+  my $shop_part = delete $params{shop_part};
+  croak "Need a valid Shop Part for updating Images" unless ref($shop_part) eq 'SL::DB::ShopPart';
+
+  my $partnumber = $shop_part->part->partnumber;
+  die "Shop Part but no kivi Partnumber" unless $partnumber;
+
+  my @upload_img  = $shop_part->get_images(want_binary => 1);
+
+  return unless (@upload_img); # there are no images, but delete wont work TODO extract to method
+
+  my ($ret, $response_code);
+  # 1. get part uuid and get media associations
+  # 2. create or update the media entry for the filename
+  # 2.1 if no media entry exists create one
+  # 2.2 update file
+  # 2.2 create or update media_product and set position
+  # 3. optional set cover image
+  # 4. optional delete images in shopware which are not in kivi
+
+  # 1 get mediaid uuid for prodcut
+  my $product_filter = {
+              'associations' => {
+                'media'   => []
+              },
+          'filter' => [
+                        {
+                          'value' => $partnumber,
+                          'type'  => 'equals',
+                          'field' => 'productNumber'
+                        }
+                      ]
+    };
+
+  $ret = $self->connector->POST('api/search/product', to_json($product_filter));
+  $response_code = $ret->responseCode();
+  die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+  my ($product_id, $media_data);
+  try {
+    $product_id = from_json($ret->responseContent())->{data}->[0]->{id};
+    # $media_data = from_json($ret->responseContent())->{data}->[0]->{media};
+  } catch { die "Malformed JSON Data: $_ " . $ret->responseContent();  };
+
+  # 2 iterate all kivi images and save distinct name for later sync
+  my %existing_images;
+  foreach my $img (@upload_img) {
+    die $::locale->text("Need a image title") unless $img->{description};
+    my $distinct_media_name = $partnumber . '_' . $img->{description};
+    $existing_images{$distinct_media_name} = 1;
+    my $image_filter = {  'filter' => [
+                          {
+                            'value' => $distinct_media_name,
+                            'type'  => 'equals',
+                            'field' => 'fileName'
+                          }
+                        ]
+                      };
+    $ret           = $self->connector->POST('api/search/media', to_json($image_filter));
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+    my $current_image_id; # maybe empty
+    try {
+      $current_image_id = from_json($ret->responseContent())->{data}->[0]->{id};
+    } catch { die "Malformed JSON Data: $_ " . $ret->responseContent();  };
+
+    # 2.1 no image with this title, create metadata for media and upload image
+    if (!$current_image_id) {
+      # not yet uploaded, create media entry
+      $ret = $self->connector->POST("/api/media?_response=true");
+      $response_code = $ret->responseCode();
+      die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+      try {
+        $current_image_id = from_json($ret->responseContent())->{data}{id};
+      } catch { die "Malformed JSON Data: $_ " . $ret->responseContent();  };
+    }
+    # 2.2 update the image data (current_image_id was found or created)
+    $ret = $self->connector->POST("/api/_action/media/$current_image_id/upload?fileName=$distinct_media_name&extension=$img->{extension}",
+                                    $img->{link},
+                                   {
+                                    "Content-Type"  => "image/$img->{extension}",
+                                   });
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
+
+    # 2.3 check if a product media entry exists for this id
+    my $product_media_filter = {
+              'filter' => [
+                        {
+                          'value' => $product_id,
+                          'type' => 'equals',
+                          'field' => 'productId'
+                        },
+                        {
+                          'value' => $current_image_id,
+                          'type' => 'equals',
+                          'field' => 'mediaId'
+                        },
+                      ]
+        };
+    $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+    my ($has_product_media, $product_media_id);
+    try {
+      $has_product_media = from_json($ret->responseContent())->{total};
+      $product_media_id  = from_json($ret->responseContent())->{data}->[0]->{id};
+    } catch { die "Malformed JSON Data: $_ " . $ret->responseContent();  };
+
+    # 2.4 ... and either update or create the entry
+    #     set shopware position to kivi position
+    my $product_media;
+    $product_media->{position} = $img->{position}; # position may change
+
+    if ($has_product_media == 0) {
+      # 2.4.1 new entry. link product to media
+      $product_media->{productId} = $product_id;
+      $product_media->{mediaId}   = $current_image_id;
+      $ret = $self->connector->POST('api/product-media', to_json($product_media));
+    } elsif ($has_product_media == 1 && $product_media_id) {
+      $ret = $self->connector->PATCH("api/product-media/$product_media_id", to_json($product_media));
+    } else {
+      die "Invalid state, please inform Shopware master admin at product-media filter: $product_media_filter";
+    }
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
+  }
+  # 3. optional set image with position 1 as cover image
+  if ($params{set_cover}) {
+    # set cover if position == 1
+    my $product_media_filter = {
+              'filter' => [
+                        {
+                          'value' => $product_id,
+                          'type' => 'equals',
+                          'field' => 'productId'
+                        },
+                        {
+                          'value' => '1',
+                          'type' => 'equals',
+                          'field' => 'position'
+                        },
+                          ]
+                             };
+
+    $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+    my $cover;
+    try {
+      $cover->{coverId} = from_json($ret->responseContent())->{data}->[0]->{id};
+    } catch { die "Malformed JSON Data: $_ " . $ret->responseContent();  };
+    $ret = $self->connector->PATCH('api/product/' . $product_id, to_json($cover));
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
+  }
+  # 4. optional delete orphaned images in shopware
+  if ($params{delete_orphaned}) {
+    # delete orphaned images
+    my $product_media_filter = {
+              'filter' => [
+                        {
+                          'value' => $product_id,
+                          'type' => 'equals',
+                          'field' => 'productId'
+                        }, ] };
+    $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+    my $img_ary;
+    try {
+      $img_ary = from_json($ret->responseContent())->{data};
+    } catch { die "Malformed JSON Data: $_ " . $ret->responseContent();  };
+
+    if (scalar @{ $img_ary} > 0) { # maybe no images at all
+      my %existing_img;
+      $existing_img{$_->{media}->{fileName}}= $_->{media}->{id} for @{ $img_ary };
+
+      while (my ($name, $id) = each %existing_img) {
+        next if $existing_images{$name};
+        $ret = $self->connector->DELETE("api/media/$id");
+        $response_code = $ret->responseCode();
+        die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
+      }
+    }
+  }
+  return;
+}
+
+sub get_categories {
+  my ($self) = @_;
+
+  my $ret           = $self->connector->POST('api/search/category');
+  my $response_code = $ret->responseCode();
+
+  die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
+
+  my $import;
+  try {
+    $import = decode_json $ret->responseContent();
+  } catch {
+    die "Malformed JSON Data: $_ " . $ret->responseContent();
+  };
+
+  my @daten      = @{ $import->{data} };
+  my %categories = map { ($_->{id} => $_) } @daten;
+
+  my @categories_tree;
+  for (@daten) {
+    my $parent = $categories{$_->{parentId}};
+    if ($parent) {
+      $parent->{children} ||= [];
+      push @{ $parent->{children} }, $_;
+    } else {
+      push @categories_tree, $_;
+    }
+  }
+  return \@categories_tree;
+}
+
+sub get_one_order  {
+  my ($self, $ordnumber) = @_;
+
+  croak t8("No Order Number") unless $ordnumber;
+  # set known params for the return structure
+  my %fetched_order  = $self->get_fetched_order_structure;
+  my $assoc          = $self->all_open_orders();
+
+  # overwrite filter for exactly one ordnumber
+  $assoc->{filter}->[0]->{value} = $ordnumber;
+  $assoc->{filter}->[0]->{type}  = 'equals';
+  $assoc->{filter}->[0]->{field} = 'orderNumber';
+
+  # 1. fetch the order and import it as a kivi order
+  # 2. return the number of processed order (1)
+  my $one_order = $self->connector->POST('api/search/order', to_json($assoc));
+
+  # 1. check for bad request or connection problems
+  if ($one_order->responseCode() != 200) {
+    $fetched_order{error}   = 1;
+    $fetched_order{message} = $one_order->responseCode() . ' ' . $one_order->responseContent();
+    return \%fetched_order;
+  }
+
+  # 1.1 parse json or exit
+  my $content;
+  try {
+    $content = from_json($one_order->responseContent());
+  } catch {
+    $fetched_order{error}   = 1;
+    $fetched_order{message} = "Malformed JSON Data: $_ " . $one_order->responseContent();
+    return \%fetched_order;
+  };
+
+  # 2. check if we found ONE order at all
+  my $total = $content->{total};
+  if ($total == 0) {
+    $fetched_order{number_of_orders} = 0;
+    return \%fetched_order;
+  } elsif ($total != 1) {
+    $fetched_order{error}   = 1;
+    $fetched_order{message} = "More than one Order returned. Invalid State: $total";
+    return \%fetched_order;
+  }
+
+  # 3. there is one valid order, try to import this one
+  if ($self->import_data_to_shop_order($content->{data}->[0])) {
+    %fetched_order = (shop_description => $self->config->description, number_of_orders => 1);
+  } else {
+    $fetched_order{message} = "Error: $@";
+    $fetched_order{error}   = 1;
+  }
+  return \%fetched_order;
+}
+
+sub get_new_orders {
+  my ($self) = @_;
+
+  my %fetched_order  = $self->get_fetched_order_structure;
+  my $assoc          = $self->all_open_orders();
+
+  # 1. fetch all open orders and try to import it as a kivi order
+  # 2. return the number of processed order $total
+  my $open_orders = $self->connector->POST('api/search/order', to_json($assoc));
+
+  # 1. check for bad request or connection problems
+  if ($open_orders->responseCode() != 200) {
+    $fetched_order{error}   = 1;
+    $fetched_order{message} = $open_orders->responseCode() . ' ' . $open_orders->responseContent();
+    return \%fetched_order;
+  }
+
+  # 1.1 parse json or exit
+  my $content;
+  try {
+    $content = from_json($open_orders->responseContent());
+  } catch {
+    $fetched_order{error}   = 1;
+    $fetched_order{message} = "Malformed JSON Data: $_ " . $open_orders->responseContent();
+    return \%fetched_order;
+  };
+
+  # 2. check if we found one or more order at all
+  my $total = $content->{total};
+  if ($total == 0) {
+    $fetched_order{number_of_orders} = 0;
+    return \%fetched_order;
+  } elsif (!$total || !($total > 0)) {
+    $fetched_order{error}   = 1;
+    $fetched_order{message} = "Undefined value for total orders returned. Invalid State: $total";
+    return \%fetched_order;
+  }
+
+  # 3. there are open orders. try to import one by one
+  $fetched_order{number_of_orders} = 0;
+  foreach my $open_order (@{ $content->{data} }) {
+    if ($self->import_data_to_shop_order($open_order)) {
+      $fetched_order{number_of_orders}++;
+    } else {
+      $fetched_order{message} .= "Error at importing order with running number:"
+                                  . $fetched_order{number_of_orders}+1 . ": $@ \n";
+      $fetched_order{error}    = 1;
+    }
+  }
+  return \%fetched_order;
+}
+
+sub get_article {
+  my ($self, $partnumber) = @_;
+
+  $partnumber   = $::form->escape($partnumber);
+  my $product_filter = {
+              'filter' => [
+                            {
+                              'value' => $partnumber,
+                              'type' => 'equals',
+                              'field' => 'productNumber'
+                            }
+                          ]
+                       };
+  my $ret = $self->connector->POST('api/search/product', to_json($product_filter));
+
+  my $response_code = $ret->responseCode();
+  die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
+
+  my $data_json;
+  try {
+    $data_json = decode_json $ret->responseContent();
+  } catch {
+    die "Malformed JSON Data: $_ " . $ret->responseContent();
+  };
+
+  # maybe no product was found ...
+  return undef unless scalar @{ $data_json->{data} } > 0;
+  # caller wants this structure:
+  # $stock_onlineshop = $shop_article->{data}->{mainDetail}->{inStock};
+  # $active_online = $shop_article->{data}->{active};
+  my $data;
+  $data->{data}->{mainDetail}->{inStock} = $data_json->{data}->[0]->{stock};
+  $data->{data}->{active}                = $data_json->{data}->[0]->{active};
+  return $data;
+}
+
+sub get_version {
+  my ($self) = @_;
+
+  my $return  = {}; # return for caller
+  my $ret     = {}; # internal return
+
+  #  1. check if we can connect at all
+  #  2. request version number
+
+  $ret = $self->connector;
+  if (200 != $ret->responseCode()) {
+    $return->{success}         = 0;
+    $return->{data}->{version} = $self->{errors}; # whatever init puts in errors
+    return $return;
+  }
+
+  $ret = $self->connector->GET('api/_info/version');
+  if (200 == $ret->responseCode()) {
+    my $version = from_json($self->connector->responseContent())->{version};
+    $return->{success}         = 1;
+    $return->{data}->{version} = $version;
+  } else {
+    $return->{success}         = 0;
+    $return->{data}->{version} = $ret->responseContent(); # Whatever REST Interface says
+  }
+
+  return $return;
+}
+
+sub set_orderstatus {
+  my ($self, $order_id, $transition) = @_;
+
+  # one state differs
+  $transition = 'complete' if $transition eq 'completed';
+
+  croak "No shop order ID, should be in format [0-9a-f]{32}" unless $order_id   =~ m/^[0-9a-f]{32}$/;
+  croak "NO valid transition value"                          unless $transition =~ m/(open|process|cancel|complete)/;
+  my $ret;
+  $ret = $self->connector->POST("/api/_action/order/$order_id/state/$transition");
+  my $response_code = $ret->responseCode();
+  die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
+
+}
+
+sub init_connector {
+  my ($self) = @_;
+
+  my $protocol = $self->config->server =~ /(^https:\/\/|^http:\/\/)/ ? '' : $self->config->protocol . '://';
+  my $client   = REST::Client->new(host => $protocol . $self->config->server);
+
+  $client->getUseragent()->proxy([$self->config->protocol], $self->config->proxy) if $self->config->proxy;
+  $client->addHeader('Content-Type', 'application/json');
+  $client->addHeader('charset',      'UTF-8');
+  $client->addHeader('Accept',       'application/json');
+
+  my %auth_req = (
+                   client_id     => $self->config->login,
+                   client_secret => $self->config->password,
+                   grant_type    => "client_credentials",
+                 );
+
+  my $ret = $client->POST('/api/oauth/token', encode_json(\%auth_req));
+
+  unless (200 == $ret->responseCode()) {
+    $self->{errors} .= $ret->responseContent();
+    return;
+  }
+
+  my $token = from_json($client->responseContent())->{access_token};
+  unless ($token) {
+    $self->{errors} .= "No Auth-Token received";
+    return;
+  }
+  # persist refresh token
+  $client->addHeader('Authorization' => 'Bearer ' . $token);
+  return $client;
+}
+
+sub import_data_to_shop_order {
+  my ($self, $import) = @_;
+
+  # failsafe checks for not yet implemented
+  die t8('Shipping cost article not implemented') if $self->config->shipping_costs_parts_id;
+
+  # no mapping unless we also have at least one shop order item ...
+  my $order_pos = delete $import->{lineItems};
+  croak t8("No Order items fetched") unless ref $order_pos eq 'ARRAY';
+
+  my $shop_order = $self->map_data_to_shoporder($import);
+
+  my $shop_transaction_ret = $shop_order->db->with_transaction(sub {
+    $shop_order->save;
+    my $id = $shop_order->id;
+
+    my @positions = sort { Sort::Naturally::ncmp($a->{"label"}, $b->{"label"}) } @{ $order_pos };
+    my $position = 0;
+    my $active_price_source = $self->config->price_source;
+    #Mapping Positions
+    foreach my $pos (@positions) {
+      $position++;
+      my $price       = $::form->round_amount($pos->{unitPrice}, 2); # unit
+      my %pos_columns = ( description          => $pos->{product}->{name},
+                          partnumber           => $pos->{product}->{productNumber},
+                          price                => $price,
+                          quantity             => $pos->{quantity},
+                          position             => $position,
+                          tax_rate             => $pos->{priceDefinition}->{taxRules}->[0]->{taxRate},
+                          shop_trans_id        => $pos->{id}, # pos id or shop_trans_id ? or dont care?
+                          shop_order_id        => $id,
+                          active_price_source  => $active_price_source,
+                        );
+      my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
+      $pos_insert->save;
+    }
+    $shop_order->positions($position);
+
+    if ( $self->config->shipping_costs_parts_id ) {
+      die t8("Not yet implemented");
+      # TODO NOT YET Implemented nor tested, this is shopware5 code:
+      my $shipping_part = SL::DB::Part->find_by( id => $self->config->shipping_costs_parts_id);
+      my %shipping_pos = ( description    => $import->{data}->{dispatch}->{name},
+                           partnumber     => $shipping_part->partnumber,
+                           price          => $import->{data}->{invoiceShipping},
+                           quantity       => 1,
+                           position       => $position,
+                           shop_trans_id  => 0,
+                           shop_order_id  => $id,
+                         );
+      my $shipping_pos_insert = SL::DB::ShopOrderItem->new(%shipping_pos);
+      $shipping_pos_insert->save;
+    }
+
+    my $customer = $shop_order->get_customer;
+
+    if (ref $customer eq 'SL::DB::Customer') {
+      $shop_order->kivi_customer_id($customer->id);
+    }
+    $shop_order->save;
+
+    # update state in shopware before transaction ends
+    $self->set_orderstatus($shop_order->shop_trans_id, "process");
+
+    1;
+
+  }) || die t8('Error while saving shop order #1. DB Error #2. Generic exception #3.',
+                $shop_order->{shop_ordernumber}, $shop_order->db->error, $@);
+}
+
+sub map_data_to_shoporder {
+  my ($self, $import) = @_;
+
+  croak "Expect a hash with one order." unless ref $import eq 'HASH';
+  # we need one number and a order date, some total prices and one customer
+  croak "Does not look like a shopware6 order" unless    $import->{orderNumber}
+                                                      && $import->{orderDateTime}
+                                                      && ref $import->{price} eq 'HASH'
+                                                      && ref $import->{orderCustomer} eq 'HASH';
+
+  my $shipto_id = $import->{deliveries}->[0]->{shippingOrderAddressId};
+  die t8("Cannot get shippingOrderAddressId for #1", $import->{orderNumber}) unless $shipto_id;
+
+  my $billing_ary = [ grep { $_->{id} == $import->{billingAddressId} }       @{ $import->{addresses} } ];
+  my $shipto_ary  = [ grep { $_->{id} == $shipto_id }                        @{ $import->{addresses} } ];
+  my $payment_ary = [ grep { $_->{id} == $import->{paymentMethodId} }        @{ $import->{paymentMethods} } ];
+
+  die t8("No Billing and ship to address, for Order Number #1 with ID Billing #2 and ID Shipping #3",
+          $import->{orderNumber}, $import->{billingAddressId}, $import->{deliveries}->[0]->{shippingOrderAddressId})
+    unless scalar @{ $billing_ary } == 1 && scalar @{ $shipto_ary } == 1;
+
+  my $billing = $billing_ary->[0];
+  my $shipto  = $shipto_ary->[0];
+  # TODO payment info is not used at all
+  my $payment = scalar @{ $payment_ary } ? delete $payment_ary->[0] : undef;
+
+  # check mandatory fields from shopware
+  die t8("No billing city")   unless $billing->{city};
+  die t8("No shipto city")    unless $shipto->{city};
+  die t8("No customer email") unless $import->{orderCustomer}->{email};
+
+  # extract order date
+  my $parser = DateTime::Format::Strptime->new(pattern   => '%Y-%m-%dT%H:%M:%S',
+                                               locale    => 'de_DE',
+                                               time_zone => 'local'             );
+  my $orderdate;
+  try {
+    $orderdate = $parser->parse_datetime($import->{orderDateTime});
+  } catch { die "Cannot parse Order Date" . $_ };
+
+  my $shop_id      = $self->config->id;
+  my $tax_included = $self->config->pricetype;
+
+  # TODO copied from shopware5 connector
+  # Mapping Zahlungsmethoden muss an Firmenkonfiguration angepasst werden
+  my %payment_ids_methods = (
+    # shopware_paymentId => kivitendo_payment_id
+  );
+  my $default_payment    = SL::DB::Manager::PaymentTerm->get_first();
+  my $default_payment_id = $default_payment ? $default_payment->id : undef;
+  #
+
+
+  my %columns = (
+    amount                  => $import->{amountTotal},
+    billing_city            => $billing->{city},
+    billing_company         => $billing->{company},
+    billing_country         => $billing->{country}->{name},
+    billing_department      => $billing->{department},
+    billing_email           => $import->{orderCustomer}->{email},
+    billing_fax             => $billing->{fax},
+    billing_firstname       => $billing->{firstName},
+    #billing_greeting        => ($import->{billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
+    billing_lastname        => $billing->{lastName},
+    billing_phone           => $billing->{phone},
+    billing_street          => $billing->{street},
+    billing_vat             => $billing->{vatId},
+    billing_zipcode         => $billing->{zipcode},
+    customer_city           => $billing->{city},
+    customer_company        => $billing->{company},
+    customer_country        => $billing->{country}->{name},
+    customer_department     => $billing->{department},
+    customer_email          => $billing->{email},
+    customer_fax            => $billing->{fax},
+    customer_firstname      => $billing->{firstName},
+    #customer_greeting       => ($billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
+    customer_lastname       => $billing->{lastName},
+    customer_phone          => $billing->{phoneNumber},
+    customer_street         => $billing->{street},
+    customer_vat            => $billing->{vatId},
+    customer_zipcode        => $billing->{zipcode},
+#    customer_newsletter     => $customer}->{newsletter},
+    delivery_city           => $shipto->{city},
+    delivery_company        => $shipto->{company},
+    delivery_country        => $shipto->{country}->{name},
+    delivery_department     => $shipto->{department},
+    delivery_email          => "",
+    delivery_fax            => $shipto->{fax},
+    delivery_firstname      => $shipto->{firstName},
+    #delivery_greeting       => ($shipto}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
+    delivery_lastname       => $shipto->{lastName},
+    delivery_phone          => $shipto->{phone},
+    delivery_street         => $shipto->{street},
+    delivery_vat            => $shipto->{vatId},
+    delivery_zipcode        => $shipto->{zipCode},
+#    host                    => $shop}->{hosts},
+    netamount               => $import->{amountNet},
+    order_date              => $orderdate,
+    payment_description     => $payment->{name},
+    payment_id              => $payment_ids_methods{$import->{paymentId}} || $default_payment_id,
+    tax_included            => $tax_included eq "brutto" ? 1 : 0,
+    shop_ordernumber        => $import->{orderNumber},
+    shop_id                 => $shop_id,
+    shop_trans_id           => $import->{id},
+    # TODO map these:
+    #remote_ip               => $import->{remoteAddress},
+    #sepa_account_holder     => $import->{paymentIntances}->{accountHolder},
+    #sepa_bic                => $import->{paymentIntances}->{bic},
+    #sepa_iban               => $import->{paymentIntances}->{iban},
+    #shipping_costs          => $import->{invoiceShipping},
+    #shipping_costs_net      => $import->{invoiceShippingNet},
+    #shop_c_billing_id       => $import->{billing}->{customerId},
+    #shop_c_billing_number   => $import->{billing}->{number},
+    #shop_c_delivery_id      => $import->{shipping}->{id},
+    #shop_customer_id        => $import->{customerId},
+    #shop_customer_number    => $import->{billing}->{number},
+    #shop_customer_comment   => $import->{customerComment},
+  );
+
+  my $shop_order = SL::DB::ShopOrder->new(%columns);
+  return $shop_order;
+}
+
+sub _u8 {
+  my ($value) = @_;
+  return encode('UTF-8', $value // '');
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+  SL::ShopConnector::Shopware6 - this is the Connector Class for Shopware 6
+
+=head1 SYNOPSIS
+
+
+=head1 DESCRIPTION
+
+=head1 AVAILABLE METHODS
+
+=over 4
+
+=item C<get_one_order>
+
+=item C<get_new_orders>
+
+=item C<update_part>
+
+Updates all metadata for a shop part. See base class for a general description.
+Specific Implementation notes:
+=over 4
+
+=item Calls sync_all_images with set_cover = 1 and delete_orphaned = 1
+
+=item Checks if longdescription should be taken from part or shop_part
+
+=item Checks if a language with the name 'Englisch' or template_code 'en'
+      is available and sets the shopware6 'en-GB' locales for the product
+
+=item C<sync_all_images (set_cover: 0|1, delete_orphaned: 0|1)>
+
+The connecting key for shopware to kivi images is the image name.
+To get distinct entries the kivi partnumber is combined with the title (description)
+of the image. Therefore part1000_someTitlefromUser should be unique in
+Shopware.
+All image data is simply send to shopware whether or not image data
+has been edited recently.
+If set_cover is set, the image with the position 1 will be used as
+the shopware cover image.
+If delete_orphaned ist set, all images related to the shopware product
+which are not also in kivitendo will be deleted.
+Shopware (6.4.x) takes care of deleting all the relations if the media
+entry for the image is deleted.
+More on media and Shopware6 can be found here:
+https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyNjI1Mzkw-media-handling
+
+=back
+
+=over 4
+
+=item C<get_article>
+
+=item C<get_categories>
+
+=item C<get_version>
+
+Tries to establish a connection and in a second step
+tries to get the server's version number.
+Returns a hashref with the data structure the Base class expects.
+
+=item C<set_orderstatus>
+
+=item C<init_connector>
+
+Inits the connection to the REST Server.
+Errors are collected in $self->{errors} and undef will be returned.
+If successful returns a REST::Client object for further communications.
+
+=back
+
+=head1 SEE ALSO
+
+L<SL::ShopConnector::ALL>
+
+=head1 BUGS
+
+None yet. :)
+
+=head1 TODOS
+
+=over 4
+
+=item * Map all data to shop_order
+
+Missing fields are commented in the sub map_data_to_shoporder.
+Some items are SEPA debit info, IP adress, delivery costs etc
+Furthermore Shopware6 uses currency, country and locales information.
+Detailed list:
+
+    #customer_newsletter     => $customer}->{newsletter},
+    #remote_ip               => $import->{remoteAddress},
+    #sepa_account_holder     => $import->{paymentIntances}->{accountHolder},
+    #sepa_bic                => $import->{paymentIntances}->{bic},
+    #sepa_iban               => $import->{paymentIntances}->{iban},
+    #shipping_costs          => $import->{invoiceShipping},
+    #shipping_costs_net      => $import->{invoiceShippingNet},
+    #shop_c_billing_id       => $import->{billing}->{customerId},
+    #shop_c_billing_number   => $import->{billing}->{number},
+    #shop_c_delivery_id      => $import->{shipping}->{id},
+    #shop_customer_id        => $import->{customerId},
+    #shop_customer_number    => $import->{billing}->{number},
+    #shop_customer_comment   => $import->{customerComment},
+
+=item * Use shipping_costs_parts_id for additional shipping costs
+
+Currently dies if a shipping_costs_parts_id is set in the config
+
+=item * Payment Infos can be read from shopware but is not linked with kivi
+
+Unused data structures in sub map_data_to_shoporder => payment_ary
+
+=item * Delete orphaned images is new in this connector, but should be in a separate method
+
+=item * Fetch from last order number is ignored and should not be needed
+
+Fetch orders also sets the state of the order from open to process. The state setting
+is transaction safe and therefore get_new_orders should always fetch only unprocessed orders
+at all. Nevertheless get_one_order just gets one order with the exactly matching order number
+and ignores any shopware order transition state.
+
+=item * Get one order and get new orders is basically the same except for the filter
+
+Right now the returning structure and the common parts of the filter are in two separate functions
+
+=item * Locales!
+
+Many error messages are thrown, but at least the more common cases should be localized.
+
+=item * Multi language support
+
+By guessing the correct german name for the english language some translation for parts can
+also be synced. This should be more clear (language configuration for shops) and the order
+synchronisation should also handle this (longdescription is simply copied from part.notes)
+
+=back
+
+=head1 AUTHOR
+
+Jan Büren jan@kivitendo.de
+
+=cut
diff --git a/SL/ShopConnector/WooCommerce.pm b/SL/ShopConnector/WooCommerce.pm
new file mode 100644 (file)
index 0000000..77aea40
--- /dev/null
@@ -0,0 +1,547 @@
+package SL::ShopConnector::WooCommerce;
+
+use strict;
+
+use parent qw(SL::ShopConnector::Base);
+
+use SL::JSON;
+use LWP::UserAgent;
+use LWP::Authen::Digest;
+use SL::DB::ShopOrder;
+use SL::DB::ShopOrderItem;
+use SL::DB::PaymentTerm;
+use SL::DB::History;
+use SL::DB::File;
+use Data::Dumper;
+use SL::Helper::Flash;
+use Encode qw(encode_utf8);
+
+sub get_one_order {
+  my ($self, $order_id) = @_;
+
+  my $dbh       = SL::DB::client;
+  my $number_of_orders = 0;
+  my @errors;
+
+  my $answer = $self->send_request(
+    "orders/" . $order_id,
+    undef,
+    "get"
+  );
+  my %fetched_orders;
+  if($answer->{success}) {
+    my $shoporder = $answer->{data};
+
+    $main::lxdebug->dump(0, 'WH: ANSWER ', $answer);
+    $dbh->with_transaction( sub{
+        #update status on server
+        $shoporder->{status} = "processing";
+        my $answer = $self->set_orderstatus($shoporder->{id}, "completed");
+        unless($answer){
+          push @errors,($::locale->text('Saving failed. Error message from the server: #1', $answer->message));
+          return 0;
+        }
+
+        unless ($self->import_data_to_shop_order($shoporder)) { return 0;}
+
+        1;
+      })or do {
+      push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
+    };
+
+    if(@errors){
+      flash_later('error', $::locale->text('Errors: #1', @errors));
+    } else {
+      $number_of_orders++;
+    }
+    %fetched_orders = (shop_description => $self->config->description, number_of_orders => $number_of_orders);
+  } else {
+    my %error_msg  = (
+      shop_id          => $self->config->id,
+      shop_description => $self->config->description,
+      message          => $answer->{message},
+      error            => 1,
+    );
+    %fetched_orders = %error_msg;
+  }
+  return \%fetched_orders;
+}
+
+sub get_new_orders {
+  my ($self) = @_;
+
+  my $dbh       = SL::DB::client;
+  my $otf              = $self->config->orders_to_fetch || 10;
+  my $number_of_orders = 0;
+  my @errors;
+
+  my $answer = $self->send_request(
+    "orders",
+    undef,
+    "get",
+    "&per_page=$otf&status=processing&after=2020-12-31T23:59:59&order=asc"
+  );
+  my %fetched_orders;
+  if($answer->{success}) {
+    my $orders = $answer->{data};
+    foreach my $shoporder(@{$orders}){
+      $dbh->with_transaction( sub{
+          #update status on server
+          $shoporder->{status} = "completed";
+          my $anwser = $self->set_orderstatus($shoporder->{id}, "completed");
+          unless($answer){
+            push @errors,($::locale->text('Saving failed. Error message from the server: #1', $answer->message));
+            return 0;
+          }
+
+          unless ($self->import_data_to_shop_order($shoporder)) { return 0;}
+
+          1;
+      })or do {
+        push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
+      };
+
+      if(@errors){
+        flash_later('error', $::locale->text('Errors: #1', @errors));
+      } else {
+        $number_of_orders++;
+      }
+    }
+    %fetched_orders = (shop_description => $self->config->description, number_of_orders => $number_of_orders);
+
+  } else {
+    my %error_msg  = (
+      shop_id          => $self->config->id,
+      shop_description => $self->config->description,
+      message          => $answer->{message},
+      error            => 1,
+    );
+    %fetched_orders = %error_msg;
+  }
+
+  return \%fetched_orders;
+}
+
+sub import_data_to_shop_order {
+  my ( $self, $import ) = @_;
+  my $shop_order = $self->map_data_to_shoporder($import);
+
+  $shop_order->save;
+  my $id = $shop_order->id;
+
+  my @positions = sort { Sort::Naturally::ncmp($a->{"sku"}, $b->{"sku"}) } @{ $import->{line_items} };
+  my $position = 1;
+
+  my $active_price_source = $self->config->price_source;
+  my $tax_included = $self->config->pricetype eq 'brutto' ? 1 : 0;
+  #Mapping Positions
+  foreach my $pos(@positions) {
+    my $tax_rate = $pos->{tax_class} eq "reduced-rate" ? 7 : 19;
+    my $tax_factor = $tax_rate/100+1;
+    my $price = $pos->{price};
+    if ( $tax_included ) {
+      $price = $price * $tax_factor;
+      $price = $::form->round_amount($price,2);
+    } else {
+      $price = $::form->round_amount($price,2);
+    }
+    my %pos_columns = ( description          => $pos->{name},
+                        partnumber           => $pos->{sku}, # sku has to be a valid value in WooCommerce
+                        price                => $price,
+                        quantity             => $pos->{quantity},
+                        position             => $position,
+                        tax_rate             => $tax_rate,
+                        shop_trans_id        => $pos->{product_id},
+                        shop_order_id        => $id,
+                        active_price_source  => $active_price_source,
+                      );
+    my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
+    $pos_insert->save;
+    $position++;
+  }
+  $shop_order->positions($position-1);
+
+  if ( $self->config->shipping_costs_parts_id ) {
+    my $shipping_part = SL::DB::Manager::Part->find_by( id => $self->config->shipping_costs_parts_id);
+    my %shipping_pos = (
+      description    => $import->{data}->{dispatch}->{name},
+      partnumber     => $shipping_part->partnumber,
+      price          => $import->{data}->{invoiceShipping},
+      quantity       => 1,
+      position       => $position,
+      shop_trans_id  => 0,
+      shop_order_id  => $id,
+    );
+    my $shipping_pos_insert = SL::DB::ShopOrderItem->new(%shipping_pos);
+    $shipping_pos_insert->save;
+  }
+
+  my $customer = $shop_order->get_customer;
+
+  if(ref($customer)){
+    $shop_order->kivi_customer_id($customer->id);
+  }
+  $shop_order->save;
+}
+
+
+sub map_data_to_shoporder {
+  my ($self, $import) = @_;
+
+  my $parser = DateTime::Format::Strptime->new( pattern   => '%Y-%m-%dT%H:%M:%S',
+                                                  locale    => 'de_DE',
+                                                  time_zone => 'local'
+                                                );
+
+  my $shop_id      = $self->config->id;
+  my $tax_included = $self->config->pricetype;
+
+  # Mapping to table shoporders. See https://woocommerce.github.io/woocommerce-rest-api-docs/?shell#order-properties
+    my $d_street;
+    if ( $import->{shipping}->{address_1} ne "" ) {
+      $d_street = $import->{shipping}->{address_1} . ($import->{shipping}->{address_2} ? " " . $import->{shipping}->{address_2} : "");
+    } else {
+      $d_street = $import->{billing}->{address_1} . ($import->{billing}->{address_2} ? " " . $import->{billing}->{address_2} : "");
+    }
+  # Mapping Zahlungsmethoden muss an Firmenkonfiguration angepasst werden
+  my %payment_ids_methods = (
+    # woocommerce_payment_method_title => kivitendo_payment_id
+  );
+  my $default_payment    = SL::DB::Manager::PaymentTerm->get_first();
+  my $default_payment_id = $default_payment ? $default_payment->id : undef;
+  my %columns = (
+#billing Shop can have different billing addresses, and may have 1 customer_address
+    billing_firstname       => $import->{billing}->{first_name},
+    billing_lastname        => $import->{billing}->{last_name},
+    #address_1 address_2
+    billing_street         => $import->{billing}->{address_1} . ($import->{billing}->{address_2} ? " " . $import->{billing}->{address_2} : ""),
+    # ???
+    billing_city            => $import->{billing}->{city},
+    #state
+    # ???
+    billing_zipcode         => $import->{billing}->{postcode},
+    billing_country         => $import->{billing}->{country},
+    billing_email           => $import->{billing}->{email},
+    billing_phone           => $import->{billing}->{phone},
+
+    #billing_greeting        => "",
+    #billing_fax             => "",
+    #billing_vat             => "",
+    billing_company         => $import->{billing}->{company},
+    #billing_department      => "",
+
+#customer
+    #customer_id
+    shop_customer_id        => $import->{customer_id},
+    shop_customer_number    => $import->{customer_id},
+    #customer_ip_address
+    remote_ip               => $import->{customer_ip_address},
+    #customer_user_agent
+    #customer_note
+    shop_customer_comment   => $import->{customer_note},
+
+    #customer_city           => "",
+    #customer_company        => "",
+    #customer_country        => "",
+    #customer_department     => "",
+    #customer_email          => "",
+    #customer_fax            => "",
+    #customer_firstname      => "",
+    #customer_greeting       => "",
+    #customer_lastname       => "",
+    #customer_phone          => "",
+    #customer_street         => "",
+    #customer_vat            => "",
+
+#shipping
+    delivery_firstname      => $import->{shipping}->{first_name} || $import->{billing}->{first_name},
+    delivery_lastname       => $import->{shipping}->{last_name} || $import->{billing}->{last_name},
+    delivery_company        => $import->{shipping}->{company} || $import->{billing}->{company},
+    #address_1 address_2
+    delivery_street         => $d_street,
+    delivery_city           => $import->{shipping}->{city} || $import->{billing}->{city},
+    #state ???
+    delivery_zipcode        => $import->{shipping}->{postcode} || $import->{billing}->{postcode},
+    delivery_country        => $import->{shipping}->{country} || $import->{billing}->{country},
+    #delivery_department     => "",
+    #delivery_email          => "",
+    #delivery_fax            => "",
+    #delivery_phone          => "",
+    #delivery_vat            => "",
+
+#other
+    #id
+    #parent_id
+    #number
+    shop_ordernumber        => $import->{number},
+    #order_key
+    #created_via
+    #version
+    #status
+    #currency
+    #date_created
+    order_date              => $parser->parse_datetime($import->{date_created}),
+    #date_created_gmt
+    #date_modified
+    #date_modified_gmt
+    #discount_total
+    #discount_tax
+    #shipping_total
+    shipping_costs          => $import->{shipping_total},
+    #shipping_tax
+    shipping_costs_net      => $import->{shipping_total},
+    #cart_tax
+    #total
+    amount                  => $import->{total},
+    #total_tax
+    netamount               => $import->{total} - $import->{total_tax},
+    #prices_include_tax
+    tax_included            => $tax_included,
+    #payment_method
+    payment_id              => $payment_ids_methods{$import->{payment_method}} || $default_payment_id,
+    #payment_method_title
+    payment_description     => $import->{payment_method_title},
+    #transaction_id
+    shop_trans_id           => $import->{id},
+    #date_paid
+    #date_paid_gmt
+    #date_completed
+    #date_completed_gmt
+
+    host                    => $import->{_links}->{self}[0]->{href},
+
+    #sepa_account_holder     => "",
+    #sepa_bic                => "",
+    #sepa_iban               => "",
+
+    #shop_c_billing_id       => "",
+    #shop_c_billing_number   => "",
+    shop_c_delivery_id      => $import->{shipping_lines}[0]->{id}, # ???
+
+# not in Shop
+    shop_id                 => $shop_id,
+  );
+
+  my $shop_order = SL::DB::ShopOrder->new(%columns);
+  return $shop_order;
+}
+
+#TODO CVARS, tax and images
+sub update_part {
+  my ($self, $shop_part, $todo) = @_;
+
+  #shop_part is passed as a param
+  die unless ref($shop_part) eq 'SL::DB::ShopPart';
+  my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
+
+  # CVARS to map
+  #my $cvars = {
+  #  map {
+  #    ($_->config->name => {
+  #      value => $_->value_as_text,
+  #      is_valid => $_->is_valid
+  #    })
+  #  }
+  #  @{ $part->cvars_by_config }
+  #};
+
+  my @categories = ();
+  foreach my $row_cat ( @{ $shop_part->shop_category } ) {
+    my $temp = { ( id => @{$row_cat}[0], ) };
+    push ( @categories, $temp );
+  }
+
+  #my @upload_img = $shop_part->get_images;
+  my $partnumber = $::form->escape($part->partnumber);#don't accept / in articlenumber
+  my $stock_status = ($part->onhand ? "instock" : "outofstock");
+  my $status = ($shop_part->active ? "publish" : "private");
+  my $tax_n_price = $shop_part->get_tax_and_price;
+  my $price = $tax_n_price->{price};
+  #my $taxrate = $tax_n_price->{tax};
+  #my $tax_class = ($taxrate >= 16 ? "standard" : "reduzierter-preis");
+
+  my %shop_data;
+
+  if($todo eq "price"){
+    %shop_data = (
+      regular_price => $price,
+    );
+  }elsif($todo eq "stock"){
+    %shop_data = (
+      stock_status => $stock_status,
+    );
+  }elsif($todo eq "price_stock"){
+    %shop_data =  (
+      stock_status => $stock_status,
+      regular_price => $price,
+    );
+  }elsif($todo eq "active"){
+    %shop_data =  (
+      status => $status,
+    );
+  }elsif($todo eq "all"){
+  # mapping  still missing attributes,metatags
+    %shop_data =  (
+      sku => $partnumber,
+      name => $part->description,
+      stock_status => $stock_status,
+      regular_price => $price,
+      status => $status,
+      description=> $shop_part->shop_description,
+      short_description=> $shop_part->shop_description,
+      categories => [ @categories ],
+      #tax_class => $tax_class,
+    );
+  }
+
+  my $dataString = SL::JSON::to_json(\%shop_data);
+  $dataString    = encode_utf8($dataString);
+
+  # LWP->post = create || LWP->put = update
+  my $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
+
+  if($answer->{success} && scalar @{$answer->{data}}){
+    #update
+    my $woo_shop_part_id = $answer->{data}[0]->{id};
+    $answer = $self->send_request("products/$woo_shop_part_id", $dataString, "put");
+  }else{
+    #upload
+    $answer = $self->send_request("products", $dataString, "post");
+  }
+
+  # don't know if this is needed
+  #if(@upload_img) {
+  #  my $partnumber = $::form->escape($part->partnumber);#shopware don't accept / in articlenumber
+  #  my $imgup      = $self->connector->put($url . "api/generatearticleimages/$partnumber?useNumberAsId=true");
+  #}
+
+  return $answer->{success};
+}
+
+sub get_article {
+  my ($self) = @_;
+  my $partnumber = $_[1];
+
+  $partnumber   = $::form->escape($partnumber);#don't accept / in partnumber
+  my $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
+
+  if($answer->{success} && scalar @{$answer->{data}}){
+    my $article = $answer->{data}[0];
+    return $article;
+  } else {
+    #What shut be here?
+    return $answer
+  }
+}
+
+sub get_categories {
+  my ($self) = @_;
+
+  my $answer = $self->send_request("products/categories",undef,"get","&per_page=100");
+  unless($answer->{success}) {
+    return $answer;
+  }
+  my @data = @{$answer->{data}};
+  my %categories = map { ($_->{id} => $_) } @data;
+
+  my @categories_tree;
+  for(@data) {
+    my $parent = $categories{$_->{parent}};
+    if($parent) {
+      $parent->{children} ||= [];
+      push @{$parent->{children}},$_;
+    } else {
+      push @categories_tree, $_;
+    }
+  }
+
+  return \@categories_tree;
+}
+
+sub get_version {
+  my ($self) = @_;
+
+  my $answer = $self->send_request("system_status");
+  if($answer->{success}) {
+    my $version = $answer->{data}->{environment}->{version};
+    my %return = (
+      success => 1,
+      data    => { version => $version },
+    );
+    return \%return;
+  } else {
+    return $answer;
+  }
+}
+
+sub set_orderstatus {
+  my ($self,$order_id, $status) = @_;
+  #  if ($status eq "fetched") { $status =  "processing"; }
+  #  if ($status eq "processing") { $status = "completed"; }
+  my %new_status = (status => $status);
+  my $status_json = SL::JSON::to_json( \%new_status);
+  my $answer = $self->send_request("orders/$order_id", $status_json, "put");
+  unless($answer->{success}){
+    return 0;
+  }
+  return 1;
+}
+
+sub create_url {
+  my ($self) = @_;
+  my $request = $_[1];
+  my $parameters = $_[2];
+
+  my $consumer_key = $self->config->login;
+  my $consumer_secret = $self->config->password;
+  my $protocol = $self->config->protocol;
+  my $server = $self->config->server;
+  my $port = $self->config->port;
+  my $path = $self->config->path;
+
+  return $protocol . "://". $server . ":" . $port . $path . $request . "?consumer_key=" . $consumer_key . "&consumer_secret=" . $consumer_secret . $parameters;
+}
+
+sub send_request {
+  my ($self) = @_;
+  my $request = $_[1];
+  my $json_data = $_[2];
+  my $method_type = $_[3];
+  my $parameters = $_[4];
+
+  my $ua = LWP::UserAgent->new;
+  my $url = $self->create_url( $request, $parameters );
+
+  my $answer;
+  if( $method_type eq "put" ) {
+    $answer = $ua->put($url, "Content-Type" => "application/json", Content => $json_data);
+  } elsif ( $method_type eq "post") {
+    $answer = $ua->post($url, "Content-Type" => "application/json", Content => $json_data);
+  } else {
+    $answer = $ua->get($url);
+  }
+
+  my $type = $answer->content_type;
+  my $status_line = $answer->status_line;
+
+  my %return;
+  if($answer->is_success && $type eq 'application/json'){
+    my $data_json = $answer->content;
+    my $json = SL::JSON::decode_json($data_json);
+    %return = (
+      success => 1,
+      data    => $json,
+    );
+  }else{
+    %return = (
+      success => 0,
+      data    => { version => $url . ": " . $status_line, data_type => $type },
+      message => "Error: $status_line",
+    );
+  }
+  #$main::lxdebug->dump(0, "TST: WooCommerce send_request return ", \%return);
+  return \%return;
+
+}
+
+1;
index 482e6d3..62df831 100644 (file)
@@ -5,21 +5,75 @@ use strict;
 use parent qw(Rose::Object);
 
 use English qw(-no_match_vars);
+use FindBin;
 use File::Spec;
 use File::Basename;
+use List::Util qw(first);
+
+my $cached_exe_dir;
 
 sub exe_dir {
-  my $dir        = dirname(File::Spec->rel2abs($PROGRAM_NAME));
-  my $system_dir = File::Spec->catdir($dir, 'SL', 'System');
-  return $dir if -d $system_dir && -f File::Spec->catfile($system_dir, 'TaskServer.pm');
+  return $cached_exe_dir if defined $cached_exe_dir;
+
+  my $bin_dir       = File::Spec->rel2abs($FindBin::Bin);
+  my @dirs          = File::Spec->splitdir($bin_dir);
+
+  $cached_exe_dir   = first { -f File::Spec->catdir(@dirs[0..$_], 'SL', 'System', 'TaskServer.pm') }
+                      reverse(0..scalar(@dirs) - 1);
+  $cached_exe_dir   = defined($cached_exe_dir) ? File::Spec->catdir(@dirs[0..$cached_exe_dir]) : File::Spec->curdir;
+
+  return $cached_exe_dir;
+}
+
+sub _parse_number_with_unit {
+  my ($number) = @_;
 
-  my @dirs = reverse File::Spec->splitdir($dir);
-  shift @dirs;
-  $dir        = File::Spec->catdir(reverse @dirs);
-  $system_dir = File::Spec->catdir($dir, 'SL', 'System');
-  return File::Spec->curdir unless -d $system_dir && -f File::Spec->catfile($system_dir, 'TaskServer.pm');
+  return undef   unless defined $number;
+  return $number unless $number =~ m{^ \s* (\d+) \s* ([kmg])b \s* $}xi;
 
-  return $dir;
+  my %factors = (K => 1024, M => 1024 * 1024, G => 1024 * 1024 * 1024);
+
+  return $1 * $factors{uc $2};
+}
+
+sub memory_usage_is_too_high {
+  return undef unless $::lx_office_conf{system};
+
+  my %limits = (
+    rss  => _parse_number_with_unit($::lx_office_conf{system}->{memory_limit_rss}),
+    size => _parse_number_with_unit($::lx_office_conf{system}->{memory_limit_vsz}),
+  );
+
+  # $::lxdebug->dump(0, "limits", \%limits);
+
+  return undef unless $limits{rss} || $limits{vsz};
+
+  my %usage;
+
+  my $in = IO::File->new("/proc/$$/status", "r") or return undef;
+
+  while (<$in>) {
+    chomp;
+    $usage{lc $1} = _parse_number_with_unit($2) if m{^ vm(rss|size): \s* (\d+ \s* [kmg]b) \s* $}ix;
+  }
+
+  $in->close;
+
+  # $::lxdebug->dump(0, "usage", \%usage);
+
+  foreach my $type (keys %limits) {
+    next if !$limits{$type};
+    next if $limits{$type} >= ($usage{$type} // 0);
+
+    {
+      no warnings 'once';
+      $::lxdebug->message(LXDebug::WARN(), "Exiting due to memory size limit reached for type '${type}': limit " . $limits{$type} . " bytes, usage " . $usage{$type} . " bytes");
+    }
+
+    return 1;
+  }
+
+  return 0;
 }
 
 1;
@@ -48,6 +102,11 @@ Returns the absolute path to the directory the kivitendo executables
 (C<login.pl> etc.) and modules (sub-directory C<SL/> etc.) are located
 in.
 
+=item C<memory_usage_is_too_high>
+
+Returns true if the current process uses more memory than the configured
+limits.
+
 =back
 
 =head1 BUGS
diff --git a/SL/System/ResourceCache.pm b/SL/System/ResourceCache.pm
new file mode 100644 (file)
index 0000000..be7a1d4
--- /dev/null
@@ -0,0 +1,79 @@
+package SL::System::ResourceCache;
+
+use strict;
+use File::stat;
+use File::Find;
+
+our @paths = qw(image css);
+our $cache;
+
+sub generate_data {
+  return if $cache;
+
+  $cache = {};
+
+  File::Find::find(sub {
+    $cache->{ $File::Find::name =~ s{^\./}{}r } = stat($_);
+  }, @paths);
+}
+
+sub get {
+  my ($class, $file) = @_;
+  no warnings 'once';
+
+  return stat($file) if ($::dispatcher // { interface => 'cgi' })->{interface} eq 'cgi';
+
+  $class->generate_data;
+  $cache->{$file};
+}
+
+1;
+
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::System::ResourceCache - provides access to resource files without having to access the filesystem all the time
+
+=head1 SYNOPSIS
+
+  use SL::System::ResourceCache;
+
+  SL::System::ResourceCache->get($filename);
+
+=head1 DESCRIPTION
+
+This will stat() all files in the configured paths at startup once, so that
+subsequent calls can use the cached values. Particularly useful for icons in
+the menu, which would otherwise generate a few hundred file sytem accesses per
+request.
+
+The caching will not happen in CGI and script environments.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item * C<get FILENAME>
+
+If the file exists, returns a L<File::stat> object. If it doesn't exists, returns undef.
+
+=back
+
+=head1 BUGS
+
+None yet :)
+
+=head1 TODO
+
+Make into instance cache and keep it as system wide object
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>sven.schoeling@googlemail.comE<gt>
+
+=cut
+
index f5b4122..a37cd03 100644 (file)
@@ -10,7 +10,8 @@ use Rose::Object::MakeMethods::Generic (
 
 use File::Slurp;
 use File::Spec::Functions qw(:ALL);
-use File::Temp qw(tempfile);
+use File::Temp;
+use Sys::Hostname ();
 
 use SL::System::Process;
 
@@ -22,6 +23,8 @@ use constant {
 
 use constant PID_BASE => "users/pid";
 
+my $node_id;
+
 sub status {
   my ($self) = @_;
 
@@ -63,6 +66,14 @@ sub wake_up {
   return kill('ALRM', $pid) ? 1 : undef;
 }
 
+sub node_id {
+  return $node_id if $node_id;
+
+  $node_id = ($::lx_office_conf{task_server} // {})->{node_id} || Sys::Hostname::hostname();
+
+  return $node_id;
+}
+
 #
 # private methods
 #
@@ -82,12 +93,13 @@ sub _read_pid {
 sub _run_script_command {
   my ($self, $command) = @_;
 
-  my ($fh, $file_name) = tempfile();
   my $exe              = catfile(catdir(SL::System::Process->exe_dir, 'scripts'), 'task_server.pl');
+  my $temp_file        = File::Temp->new;
+  my $file_name        = $temp_file->filename;
 
-  system "${exe} ${command} >> ${file_name} 2>&1";
+  $temp_file->close;
 
-  $fh->close;
+  system "${exe} ${command} >> ${file_name} 2>&1";
 
   $self->last_command_output(read_file($file_name));
 
index de99ad1..ff29abb 100644 (file)
@@ -3,6 +3,7 @@
 package TODO;
 
 use SL::DBUtils;
+use SL::DB;
 
 use strict;
 
@@ -59,42 +60,43 @@ sub save_user_config {
   my $myconfig = \%main::myconfig;
   my $form     = $main::form;
 
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
-
-  my $query    = qq|SELECT id FROM employee WHERE login = ?|;
+  SL::DB->client->with_transaction(sub {
+    my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
-  my ($id)     = selectfirst_array_query($form, $dbh, $query, $params{login});
+    my $query    = qq|SELECT id FROM employee WHERE login = ?|;
 
-  if (!$id) {
-    $main::lxdebug->leave_sub();
-    return;
-  }
+    my ($id)     = selectfirst_array_query($form, $dbh, $query, $params{login});
 
-  $query =
-    qq|SELECT show_after_login
-       FROM todo_user_config
-       WHERE employee_id = ?|;
+    if (!$id) {
+      $main::lxdebug->leave_sub();
+      return;
+    }
 
-  if (! selectfirst_hashref_query($form, $dbh, $query, $id)) {
-    do_query($form, $dbh, qq|INSERT INTO todo_user_config (employee_id) VALUES (?)|, $id);
-  }
+    $query =
+      qq|SELECT show_after_login
+         FROM todo_user_config
+         WHERE employee_id = ?|;
 
-  $query =
-    qq|UPDATE todo_user_config SET
-         show_after_login = ?,
-         show_follow_ups = ?,
-         show_follow_ups_login = ?,
-         show_overdue_sales_quotations = ?,
-         show_overdue_sales_quotations_login = ?
+    if (! selectfirst_hashref_query($form, $dbh, $query, $id)) {
+      do_query($form, $dbh, qq|INSERT INTO todo_user_config (employee_id) VALUES (?)|, $id);
+    }
 
-       WHERE employee_id = ?|;
+    $query =
+      qq|UPDATE todo_user_config SET
+           show_after_login = ?,
+           show_follow_ups = ?,
+           show_follow_ups_login = ?,
+           show_overdue_sales_quotations = ?,
+           show_overdue_sales_quotations_login = ?
 
-  my @values = map { $params{$_} ? 't' : 'f' } qw(show_after_login show_follow_ups show_follow_ups_login show_overdue_sales_quotations show_overdue_sales_quotations_login);
-  push @values, $id;
+         WHERE employee_id = ?|;
 
-  do_query($form, $dbh, $query, @values);
+    my @values = map { $params{$_} ? 't' : 'f' } qw(show_after_login show_follow_ups show_follow_ups_login show_overdue_sales_quotations show_overdue_sales_quotations_login);
+    push @values, $id;
 
-  $dbh->commit();
+    do_query($form, $dbh, $query, @values);
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
index d921a90..a682dc2 100644 (file)
@@ -60,7 +60,7 @@ sub get_tax_info {
 
   if (!$self->{handles}->{get_tax_info}) {
     $self->{queries}->{get_tax_info} = qq|
-      SELECT t.rate AS taxrate, t.taxnumber, t.taxdescription, t.chart_id AS taxchart_id,
+      SELECT t.rate AS taxrate, c.accno as taxnumber, t.taxdescription, t.chart_id AS taxchart_id,
         c.accno AS taxaccno, c.description AS taxaccount
       FROM taxkeys tk
       LEFT JOIN tax t   ON (tk.tax_id  = t.id)
index 39157d2..b20a7b1 100644 (file)
@@ -19,7 +19,6 @@ use SL::Template::LaTeX;
 use SL::Template::OpenDocument;
 use SL::Template::PlainText;
 use SL::Template::ShellCommand;
-use SL::Template::XML;
 
 sub create {
   my %params  = @_;
@@ -47,8 +46,8 @@ sub available_templates {
   my @alldir  = sort grep {
        -d ($::lx_office_conf{paths}->{templates} . "/$_")
     && !/^\.\.?$/
-    && !m/\.(?:html|tex|sty|odt|xml|txb)$/
-    && !m/^(?:webpages$|print$|mail$|\.)/
+    && !m/\.(?:html|tex|sty|odt)$/
+    && !m/^(?:webpages$|mobile_webpages$|pdf$|print$|mail$|\.)/
   } keys %dir_h;
 
   tie %dir_h, 'IO::Dir', "$::lx_office_conf{paths}->{templates}/print";
index 352f793..293db2e 100644 (file)
@@ -11,9 +11,11 @@ use File::Basename;
 use File::Temp;
 use HTML::Entities ();
 use List::MoreUtils qw(any);
+use Scalar::Util qw(blessed);
 use Unicode::Normalize qw();
 
 use SL::DB::Default;
+use SL::System::Process;
 
 my %text_markup_replace = (
   b => 'textbf',
@@ -66,6 +68,13 @@ my %html_replace = (
   '<br>'      => "\\newline ",
 );
 
+sub _lb_to_space {
+  my ($to_replace) = @_;
+
+  my $vspace = '\vspace*{0.5cm}';
+  return $vspace x (length($to_replace) / length($html_replace{'<br>'}));
+}
+
 sub _format_html {
   my ($self, $content, %params) = @_;
 
@@ -74,6 +83,8 @@ sub _format_html {
   $content =~ s{ (?:\&nbsp;|\s)+ }{ }gx;
   $content =~ s{ (?:\&nbsp;|\s)+$ }{}gx;
   $content =~ s{ (?: <br/?> )+$ }{}gx;
+  $content =~ s{ <ul>\s*</ul> | <ol>\s*</ol> }{}igx;
+  $content =~ s{ (?: <p>\s*</p>\s* )+ \Z }{}imgx;
 
   my @parts = grep { $_ } map {
     if (substr($_, 0, 1) eq '<') {
@@ -86,7 +97,13 @@ sub _format_html {
   } split(m{(<.*?>)}x, $content);
 
   $content =  join '', @parts;
-  $content =~ s{ (?: [\n\s] | \\newline )+$ }{}gx;
+  $content =~ s{ (?: [\n\s] | \\newline )+ $ }{}gx;                                         # remove line breaks at the end of the text
+  $content =~ s{ ^ \s+ }{}gx;                                                               # remove white space at the start of the text
+  $content =~ s{ ^ ( \\newline \  )+ }{ _lb_to_space($1) }gxe;                              # convert line breaks at the start of the text to vertical space
+  $content =~ s{ ( \n\n+ ) ( \\newline \  )+ }{ $1 . _lb_to_space($2) }gxe;                 # convert line breaks at the start of a paragraph to vertical space
+  $content =~ s{ ( \\end\{ [^\}]+ \} \h* ) ( \\newline \  )+ }{ $1 . _lb_to_space($2) }gxe; # convert line breaks after LaTeX environments like lists to vertical space
+  $content =~ s{ ^ \h+ \\newline }{\\newline}gmx;
+  $content =~ s{ \n\n \h* \\newline \h* }{\n\n}gmx;
 
   return $content;
 }
@@ -112,6 +129,9 @@ sub format_string {
     // $formatters{ $self->{default_content_type} }
     // $formatters{ text };
 
+  $content  =~ s{[^\p{Print}\n]|\p{Cf}}{}g;
+  $variable =~ s{[^\p{Print}\n]|\p{Cf}}{}g;
+
   return $formatter->($self, $content, variable => $variable);
 }
 
@@ -372,33 +392,76 @@ sub _parse_config_lines {
   }
 }
 
+sub _embed_file_directive {
+  my ($self, $file) = @_;
+
+  # { source      => $xmlfile,
+  #   name        => 'factur-x.xml',
+  #   description => $::locale->text('Factur-X/ZUGFeRD invoice'), }
+
+  my $file_name  =  blessed($file->{source}) && $file->{source}->can('filename') ? $file->{source}->filename : "" . $file->{source}->filename;
+  my $embed_name =  $file->{name} // $file_name;
+  $embed_name    =~ s{.*/}{};
+
+  my $embed_name_ascii = $::locale->quote_special_chars('filenames', $embed_name);
+  $embed_name_ascii    =~ s{[^a-z0-9!@#$%^&*(){}[\],.+'"=_-]+}{}gi;
+
+  my @options;
+
+  my $add_opt = sub {
+    my ($name, $value) = @_;
+    return if ($value // '') eq '';
+    push @options, sprintf('%s={%s}', $name, $value); # TODO: escaping
+  };
+
+  $add_opt->('filespec',       $embed_name_ascii);
+  $add_opt->('ucfilespec',     $embed_name);
+  $add_opt->('desc',           $file->{description});
+  $add_opt->('afrelationship', $file->{relationship});
+  $add_opt->('mimetype',       $file->{mime_type});
+
+  return sprintf('\embedfile[%s]{%s}', join(',', @options), $file_name);
+}
+
 sub _force_mandatory_packages {
-  my $self  = shift;
-  my $lines = shift;
+  my ($self, @lines) = @_;
+  my @new_lines;
 
-  my (%used_packages, $document_start_line, $last_usepackage_line);
+  my %used_packages;
+  my @required_packages = qw(textcomp ulem);
+  push @required_packages, 'embedfile' if $self->{pdf_a};
 
-  foreach my $i (0 .. scalar @{ $lines } - 1) {
-    if ($lines->[$i] =~ m/\\usepackage[^\{]*{(.*?)}/) {
+  foreach my $line (@lines) {
+    if ($line =~ m/\\usepackage[^\{]*{(.*?)}/) {
       $used_packages{$1} = 1;
-      $last_usepackage_line = $i;
 
-    } elsif ($lines->[$i] =~ m/\\begin\{document\}/) {
-      $document_start_line = $i;
-      last;
+    } elsif ($line =~ m/\\begin\{document\}/) {
+      if ($self->{pdf_a} && $self->{pdf_a}->{xmp}) {
+        my $version       = $self->{pdf_a}->{version}   // '3a';
+        my $xmp_file_name = $self->{userspath} . "/pdfa.xmp";
+        my $out           = IO::File->new($xmp_file_name, ">:encoding(utf-8)") || croak "Error creating ${xmp_file_name}: $!";
+        $out->print(Encode::encode('utf-8', $self->{pdf_a}->{xmp}));
+        $out->close;
+
+        push @new_lines, (
+          "\\usepackage[a-${version},mathxmp]{pdfx}[2018/12/22]\n",
+          "\\usepackage[genericmode]{tagpdf}\n",
+          "\\tagpdfsetup{activate-all}\n",
+          "\\hypersetup{pdfstartview=}\n",
+        );
+      }
 
-    }
-  }
+      push @new_lines, map { "\\usepackage{$_}\n" } grep { !$used_packages{$_} } @required_packages;
+      push @new_lines, $line;
+      push @new_lines, map { $self->_embed_file_directive($_) } @{ $self->{pdf_attachments} // [] };
 
-  my $insertion_point = defined($document_start_line)  ? $document_start_line
-                      : defined($last_usepackage_line) ? $last_usepackage_line
-                      :                                  scalar @{ $lines } - 1;
+      next;
+    }
 
-  foreach my $package (qw(textcomp ulem)) {
-    next if $used_packages{$package};
-    splice @{ $lines }, $insertion_point, 0, "\\usepackage{${package}}\n";
-    $insertion_point++;
+    push @new_lines, $line;
   }
+
+  return @new_lines;
 }
 
 sub parse {
@@ -415,7 +478,7 @@ sub parse {
   close(IN);
 
   $self->_parse_config_lines(\@lines);
-  $self->_force_mandatory_packages(\@lines) if (ref $self eq 'SL::Template::LaTeX');
+  @lines = $self->_force_mandatory_packages(@lines) if (ref $self eq 'SL::Template::LaTeX');
 
   my $contents = join("", @lines);
 
@@ -437,9 +500,9 @@ sub parse {
       $contents = "[% TAGS $self->{tag_start} $self->{tag_end} %]\n" . $contents;
     }
 
-    $form->prepare_global_vars;
+    my $globals = global_vars();
 
-    $::form->init_template->process(\$contents, $form, \$new_contents) || die $::form->template->error;
+    $::form->template->process(\$contents, { %$form, %$globals }, \$new_contents) || die $::form->template->error;
   } else {
     $new_contents = $self->parse_block($contents);
   }
@@ -460,12 +523,21 @@ sub parse {
   }
 }
 
+sub _texinputs_path {
+  my ($self, $templates_path) = @_;
+
+  my $exe_dir     = SL::System::Process::exe_dir();
+  $templates_path = $exe_dir . '/' . $templates_path unless $templates_path =~ m{^/};
+
+  return join(':', grep({ $_ } ('.', $exe_dir . '/texmf', $exe_dir . '/users', $templates_path, $ENV{TEXINPUTS})), '');
+}
+
 sub convert_to_postscript {
   my ($self) = @_;
   my ($form, $userspath) = ($self->{"form"}, $self->{"userspath"});
 
   # Convert the tex file to postscript
-  local $ENV{TEXINPUTS} = ".:" . $form->{cwd} . "/" . $form->{templates} . ":" . $ENV{TEXINPUTS};
+  local $ENV{TEXINPUTS} = $self->_texinputs_path($form->{templates});
 
   if (!chdir("$userspath")) {
     $self->{"error"} = "chdir : $!";
@@ -479,7 +551,7 @@ sub convert_to_postscript {
   my $old_home = $ENV{HOME};
   my $old_openin_any = $ENV{openin_any};
   $ENV{HOME}   = $userspath =~ m|^/| ? $userspath : getcwd();
-  $ENV{openin_any} = "p";
+  $ENV{openin_any} = "r";
 
   for (my $run = 1; $run <= 2; $run++) {
     if (system("${latex} --interaction=nonstopmode $form->{tmpfile} " .
@@ -519,7 +591,7 @@ sub convert_to_pdf {
   my ($form, $userspath) = ($self->{"form"}, $self->{"userspath"});
 
   # Convert the tex file to PDF
-  local $ENV{TEXINPUTS} = ".:" . $form->{cwd} . "/" . $form->{templates} . ":" . $ENV{TEXINPUTS};
+  local $ENV{TEXINPUTS} = $self->_texinputs_path($form->{templates});
 
   if (!chdir("$userspath")) {
     $self->{"error"} = "chdir : $!";
@@ -533,7 +605,7 @@ sub convert_to_pdf {
   my $old_home = $ENV{HOME};
   my $old_openin_any = $ENV{openin_any};
   $ENV{HOME}   = $userspath =~ m|^/| ? $userspath : getcwd();
-  $ENV{openin_any} = "p";
+  $ENV{openin_any} = "r";
 
   for (my $run = 1; $run <= 2; $run++) {
     if (system("${latex} --interaction=nonstopmode $form->{tmpfile} " .
@@ -579,11 +651,12 @@ sub uses_temp_file {
 sub parse_and_create_pdf {
   my ($class, $template_file_name, %params) = @_;
 
+  my $userspath                = delete($params{userspath}) || $::lx_office_conf{paths}->{userspath};
   my $keep_temp                = $::lx_office_conf{debug} && $::lx_office_conf{debug}->{keep_temp_files};
   my ($tex_fh, $tex_file_name) = File::Temp::tempfile(
     'kivitendo-printXXXXXX',
     SUFFIX => '.tex',
-    DIR    => $::lx_office_conf{paths}->{userspath},
+    DIR    => $userspath,
     UNLINK => $keep_temp ? 0 : 1,,
   );
 
@@ -592,7 +665,7 @@ sub parse_and_create_pdf {
   my $local_form           = Form->new('');
   $local_form->{cwd}       = $old_wd;
   $local_form->{IN}        = $template_file_name;
-  $local_form->{tmpdir}    = $::lx_office_conf{paths}->{userspath};
+  $local_form->{tmpdir}    = $userspath;
   $local_form->{tmpfile}   = $tex_file_name;
   $local_form->{templates} = SL::DB::Default->get->templates;
 
@@ -603,7 +676,7 @@ sub parse_and_create_pdf {
 
   my $error;
   eval {
-    my $template = SL::Template::LaTeX->new(file_name => $template_file_name, form => $local_form);
+    my $template = SL::Template::LaTeX->new(file_name => $template_file_name, form => $local_form, userspath => $userspath);
     my $result   = $template->parse($tex_fh) && $template->convert_to_pdf;
 
     die $template->{error} unless $result;
@@ -626,4 +699,15 @@ sub parse_and_create_pdf {
   return (file_name => do { $tex_file_name =~ s/tex$/pdf/; $tex_file_name });
 }
 
+sub global_vars {
+  {
+    AUTH            => $::auth,
+    INSTANCE_CONF   => $::instance_conf,
+    LOCALE          => $::locale,
+    LXCONFIG        => $::lx_office_conf,
+    LXDEBUG         => $::lxdebug,
+    MYCONFIG        => \%::myconfig,
+  };
+}
+
 1;
index 50c191b..66d8f83 100644 (file)
@@ -6,15 +6,21 @@ use Archive::Zip;
 use Encode;
 use HTML::Entities;
 use POSIX 'setsid';
+use XML::LibXML;
 
 use SL::Iconv;
 use SL::Template::OpenDocument::Styles;
 
+use SL::DB::BankAccount;
+use SL::Helper::QrBill;
+use SL::Helper::ISO3166;
+
 use Cwd;
 # use File::Copy;
 # use File::Spec;
 # use File::Temp qw(:mktemp);
 use IO::File;
+use List::Util qw(first);
 
 use strict;
 
@@ -346,11 +352,20 @@ sub parse_block {
 sub parse {
   $main::lxdebug->enter_sub();
   my $self = $_[0];
+
   local *OUT = $_[1];
   my $form = $self->{"form"};
 
   close(OUT);
 
+  my $qr_image_path;
+  if ($::instance_conf->get_create_qrbill_invoices && $form->{formname} eq 'invoice') {
+    # the biller account information, biller address and the reference number,
+    # are needed in the template aswell as in the qr-code generation, therefore
+    # assemble these and add to $::form
+    $qr_image_path = $self->generate_qr_code;
+  }
+
   my $file_name;
   if ($form->{"IN"} =~ m|^/|) {
     $file_name = $form->{"IN"};
@@ -382,7 +397,7 @@ sub parse {
   if ($self->{use_template_toolkit}) {
     my $additional_params = $::form;
 
-    $::form->init_template->process(\$contents, $additional_params, \$new_contents) || die $::form->template->error;
+    $::form->template->process(\$contents, $additional_params, \$new_contents) || die $::form->template->error;
   } else {
     $self->{tag_stack} = [];
     $new_contents = $self->parse_block($contents);
@@ -420,6 +435,28 @@ sub parse {
     $zip->contents("styles.xml", Encode::encode('utf-8-strict', $new_styles));
   }
 
+  if ($::instance_conf->get_create_qrbill_invoices && $form->{formname} eq 'invoice') {
+    # get placeholder path from odt XML
+    my $qr_placeholder_path;
+    my $dom = XML::LibXML->load_xml(string => $contents);
+    my @nodelist = $dom->getElementsByTagName("draw:frame");
+    for my $node (@nodelist) {
+      my $attr = $node->getAttribute('draw:name');
+      if ($attr eq 'QRCodePlaceholder') {
+        my @children = $node->getChildrenByTagName('draw:image');
+        $qr_placeholder_path = $children[0]->getAttribute('xlink:href');
+      }
+    }
+    if (!defined($qr_placeholder_path)) {
+      $::form->error($::locale->text('QR-Code placeholder image: QRCodePlaceholder not found in template.'));
+    }
+    # replace QR-Code Placeholder Image in zip file (odt) with generated one
+    $zip->updateMember(
+     $qr_placeholder_path,
+     $qr_image_path
+    );
+  }
+
   $zip->writeToFileNamed($form->{"tmpfile"}, 1);
 
   my $res = 1;
@@ -431,6 +468,276 @@ sub parse {
   return $res;
 }
 
+sub get_qrbill_account {
+  $main::lxdebug->enter_sub();
+  my ($self) = @_;
+
+  my $qr_account;
+
+  my $bank_accounts     = SL::DB::Manager::BankAccount->get_all;
+  $qr_account = scalar(@{ $bank_accounts }) == 1 ?
+    $bank_accounts->[0] :
+    first { $_->use_for_qrbill } @{ $bank_accounts };
+
+  if (!$qr_account) {
+    $::form->error($::locale->text('No bank account flagged for QRBill usage was found.'));
+  }
+
+  $main::lxdebug->leave_sub();
+  return $qr_account;
+}
+
+sub remove_letters_prefix {
+  my $s = $_[0];
+  $s =~ s/^[a-zA-Z]+//;
+  return $s;
+}
+
+sub check_digits_and_max_length {
+  my $s = $_[0];
+  my $length = $_[1];
+
+  return 0 if (!($s =~ /^\d*$/) || length($s) > $length);
+  return 1;
+}
+
+sub calculate_check_digit {
+  # calculate ESR check digit using algorithm: "modulo 10, recursive"
+  my $ref_number_str = $_[0];
+
+  my @m = (0, 9, 4, 6, 8, 2, 7, 1, 3, 5);
+  my $carry = 0;
+
+  my @ref_number_split = map int($_), split(//, $ref_number_str);
+
+  for my $v (@ref_number_split) {
+    $carry = @m[($carry + $v) % 10];
+  }
+
+  return (10 - $carry) % 10;
+}
+
+sub assemble_ref_number {
+  $main::lxdebug->enter_sub();
+
+  my $bank_id = $_[0];
+  my $customer_number = $_[1];
+  my $order_number = $_[2] // "0";
+  my $invoice_number = $_[3] // "0";
+
+  # check values (analog to checks in makro)
+  # - bank_id
+  #     input: 6 digits, only numbers
+  #     output: 6 digits, only numbers
+  if (!($bank_id =~ /^\d*$/) || length($bank_id) != 6) {
+    $::form->error($::locale->text('Bank account id number invalid. Must be 6 digits.'));
+  }
+
+  # - customer_number
+  #     input: prefix (letters) + up to 6 digits (numbers)
+  #     output: prefix removed, 6 digits, filled with leading zeros
+  $customer_number = remove_letters_prefix($customer_number);
+  if (!check_digits_and_max_length($customer_number, 6)) {
+    $::form->error($::locale->text('Customer number invalid. Must be less then or equal to 6 digits after prefix.'));
+  }
+  # fill with zeros
+  $customer_number = sprintf "%06d", $customer_number;
+
+  # - order_number
+  #     input: prefix (letters) + up to 7 digits, may be zero
+  #     output: prefix removed, 7 digits, filled with leading zeros
+  $order_number = remove_letters_prefix($order_number);
+  if (!check_digits_and_max_length($order_number, 7)) {
+    $::form->error($::locale->text('Order number invalid. Must be less then or equal to 7 digits after prefix.'));
+  }
+  # fill with zeros
+  $order_number = sprintf "%07d", $order_number;
+
+  # - invoice_number
+  #     input: prefix (letters) + up to 7 digits, may be zero
+  #     output: prefix removed, 7 digits, filled with leading zeros
+  $invoice_number = remove_letters_prefix($invoice_number);
+  if (!check_digits_and_max_length($invoice_number, 7)) {
+    $::form->error($::locale->text('Invoice number invalid. Must be less then or equal to 7 digits after prefix.'));
+  }
+  # fill with zeros
+  $invoice_number = sprintf "%07d", $invoice_number;
+
+  # assemble ref. number
+  my $ref_number = $bank_id . $customer_number . $order_number . $invoice_number;
+
+  # calculate check digit
+  my $ref_number_cpl = $ref_number . calculate_check_digit($ref_number);
+
+  $main::lxdebug->leave_sub();
+  return $ref_number_cpl;
+}
+
+sub get_ref_number_formatted {
+  $main::lxdebug->enter_sub();
+
+  my $ref_number = $_[0];
+
+  # create ref. number in format:
+  # 'XX XXXXX XXXXX XXXXX XXXXX XXXXX' (2 digits + 5 x 5 digits)
+  my $ref_number_spaced = substr($ref_number, 0, 2) . ' ' .
+                          substr($ref_number, 2, 5) . ' ' .
+                          substr($ref_number, 7, 5) . ' ' .
+                          substr($ref_number, 12, 5) . ' ' .
+                          substr($ref_number, 17, 5) . ' ' .
+                          substr($ref_number, 22, 5);
+
+  $main::lxdebug->leave_sub();
+  return $ref_number_spaced;
+}
+
+sub get_iban_formatted {
+  $main::lxdebug->enter_sub();
+
+  my $iban = $_[0];
+
+  # create iban number in format:
+  # 'XXXX XXXX XXXX XXXX XXXX X' (5 x 4 + 1digits)
+  my $iban_spaced = substr($iban, 0, 4) . ' ' .
+                    substr($iban, 4, 4) . ' ' .
+                    substr($iban, 8, 4) . ' ' .
+                    substr($iban, 12, 4) . ' ' .
+                    substr($iban, 16, 4) . ' ' .
+                    substr($iban, 20, 1);
+
+  $main::lxdebug->leave_sub();
+  return $iban_spaced;
+}
+
+sub get_amount_formatted {
+  $main::lxdebug->enter_sub();
+
+  unless ($_[0] =~ /^\d+\.\d{2}$/) {
+    $::form->error($::locale->text('Amount has wrong format.'));
+  }
+
+  local $_ = shift;
+  $_ = reverse split //;
+  m/^\d{2}\./g;
+  s/\G(\d{3})(?=\d)/$1 /g;
+
+  $main::lxdebug->leave_sub();
+  return scalar reverse split //;
+}
+
+sub generate_qr_code {
+  $main::lxdebug->enter_sub();
+  my $self = $_[0];
+  my $form = $self->{"form"};
+
+  # assemble data for QR-Code
+
+  # get qr-account data
+  my $qr_account = $self->get_qrbill_account();
+
+  my %biller_information = (
+    'iban' => $qr_account->{'iban'}
+  );
+
+  my $biller_countrycode = SL::Helper::ISO3166::map_name_to_alpha_2_code(
+    $::instance_conf->get_address_country()
+  );
+  if (!$biller_countrycode) {
+    $::form->error($::locale->text('Error mapping biller countrycode.'));
+  }
+  my %biller_data = (
+    'address_type' => 'K',
+    'company' => $::instance_conf->get_company(),
+    'address_row1' => $::instance_conf->get_address_street1(),
+    'address_row2' => $::instance_conf->get_address_zipcode() . ' ' . $::instance_conf->get_address_city(),
+    'countrycode' => $biller_countrycode,
+  );
+
+  my $amount;
+  if ($form->{'qrbill_without_amount'}) {
+    $amount = '';
+  } else {
+    $amount = sprintf("%.2f", $form->parse_amount(\%::myconfig, $form->{'total'}));
+  }
+
+  my %payment_information = (
+    'amount' => $amount,
+    'currency' => $form->{'currency'},
+  );
+
+  my $customer_countrycode = SL::Helper::ISO3166::map_name_to_alpha_2_code($form->{'country'});
+  if (!$customer_countrycode) {
+    $::form->error($::locale->text('Error mapping customer countrycode.'));
+  }
+  my %invoice_recipient_data = (
+    'address_type' => 'K',
+    'name' => $form->{'name'},
+    'address_row1' => $form->{'street'},
+    'address_row2' => $form->{'zipcode'} . ' ' . $form->{'city'},
+    'countrycode' => $customer_countrycode,
+  );
+
+  my %ref_nr_data;
+  if ($::instance_conf->get_create_qrbill_invoices == 1) {
+    # generate ref.-no. with check digit
+    my $ref_number = assemble_ref_number(
+      $qr_account->{'bank_account_id'},
+      $form->{'customernumber'},
+      $form->{'ordnumber'},
+      $form->{'invnumber'},
+    );
+    %ref_nr_data = (
+      'type' => 'QRR',
+      'ref_number' => $ref_number,
+    );
+    # get ref. number/iban formatted with spaces and set into form for template
+    # processing
+    $form->{'ref_number'} = $ref_number;
+    $form->{'ref_number_formatted'} = get_ref_number_formatted($ref_number);
+  } elsif ($::instance_conf->get_create_qrbill_invoices == 2) {
+    %ref_nr_data = (
+      'type' => 'NON',
+      'ref_number' => '',
+    );
+  } else {
+    $::form->error($::locale->text('Error getting QR-Bill type.'));
+  }
+
+  # set into form for template processing
+  $form->{'biller_information'} = \%biller_information;
+  $form->{'biller_data'} = \%biller_data;
+  $form->{'iban_formatted'} = get_iban_formatted($qr_account->{'iban'});
+
+  # format amount for template
+  $form->{'amount_formatted'} = get_amount_formatted(
+    sprintf(
+      "%.2f",
+      $form->parse_amount(\%::myconfig, $form->{'total'})
+    )
+  );
+
+  # set outfile
+  my $outfile = $form->{"tmpdir"} . '/' . 'qr-code.png';
+
+  # generate QR-Code Image
+  eval {
+   my $qr_image = SL::Helper::QrBill->new(
+     \%biller_information,
+     \%biller_data,
+     \%payment_information,
+     \%invoice_recipient_data,
+     \%ref_nr_data,
+   );
+   $qr_image->generate($outfile);
+  } or do {
+   local $_ = $@; chomp; my $error = $_;
+   $::form->error($::locale->text('QR-Image generation failed: ' . $error));
+  };
+
+  $main::lxdebug->leave_sub();
+  return $outfile;
+}
+
 sub is_xvfb_running {
   $main::lxdebug->enter_sub();
 
index ca40971..920dee4 100644 (file)
@@ -3,6 +3,8 @@ package SL::Template::Plugin::KiviLatex;
 use strict;
 use parent qw( Template::Plugin::Filter );
 
+use SL::Template::LaTeX;
+
 my $cached_instance;
 
 sub new {
@@ -55,22 +57,7 @@ my %html_replace = (
 sub filter_html {
   my ($self, $text, $args) = @_;
 
-  $text =~ s{ \r+ }{}gx;
-  $text =~ s{ \n+ }{ }gx;
-  $text =~ s{ (?:\&nbsp;|\s)+ }{ }gx;
-  $text =~ s{ <ul>\s*</ul> | <ol>\s*</ol> }{}gx; # Remove lists without items. Can happen with copy & paste from e.g. LibreOffice.
-
-  my @parts = map {
-    if (substr($_, 0, 1) eq '<') {
-      s{ +}{}g;
-      $html_replace{$_} || '';
-
-    } else {
-      $::locale->quote_special_chars('Template/LaTeX', HTML::Entities::decode_entities($_));
-    }
-  } split(m{(<.*?>)}x, $text);
-
-  return join('', @parts);
+  return SL::Template::LaTeX->new->_format_html($text);
 }
 
 sub required_packages_for_html {
index 4896009..abb6046 100644 (file)
@@ -8,6 +8,8 @@ use List::Util qw(max);
 use Scalar::Util qw(blessed);
 
 use SL::Presenter;
+use SL::Presenter::ALL;
+use SL::Presenter::Simple;
 use SL::Util qw(_hashify);
 
 use strict;
@@ -50,107 +52,47 @@ sub _call_presenter {
 
   my $presenter              = $::request->presenter;
 
-  if (!$presenter->can($method)) {
-    $::lxdebug->message(LXDebug::WARN(), "SL::Presenter has no method named '$method'!");
-    return '';
+  splice @args, -1, 1, %{ $args[-1] } if @args && (ref($args[-1]) eq 'HASH');
+
+  if (my $sub = SL::Presenter::Simple->can($method)) {
+    return $sub->(@args);
   }
 
-  splice @args, -1, 1, %{ $args[-1] } if @args && (ref($args[-1]) eq 'HASH');
+  if ($presenter->can($method)) {
+    return $presenter->$method(@args);
+  }
 
-  $presenter->$method(@args);
+  $::lxdebug->message(LXDebug::WARN(), "SL::Presenter has no method named '$method'!");
+  return;
 }
 
 sub name_to_id    { return _call_presenter('name_to_id',    @_); }
 sub html_tag      { return _call_presenter('html_tag',      @_); }
+sub hidden_tag    { return _call_presenter('hidden_tag',    @_); }
 sub select_tag    { return _call_presenter('select_tag',    @_); }
+sub checkbox_tag  { return _call_presenter('checkbox_tag',  @_); }
 sub input_tag     { return _call_presenter('input_tag',     @_); }
+sub javascript    { return _call_presenter('javascript',    @_); }
 sub truncate      { return _call_presenter('truncate',      @_); }
 sub simple_format { return _call_presenter('simple_format', @_); }
-sub part_picker   { return _call_presenter('part_picker',   @_); }
-sub chart_picker  { return _call_presenter('chart_picker',  @_); }
-sub customer_vendor_picker   { return _call_presenter('customer_vendor_picker',   @_); }
-sub project_picker           { return _call_presenter('project_picker',           @_); }
+sub button_tag               { return _call_presenter('button_tag',               @_); }
+sub submit_tag               { return _call_presenter('submit_tag',               @_); }
+sub ajax_submit_tag          { return _call_presenter('ajax_submit_tag',          @_); }
+sub link                     { return _call_presenter('link_tag',                 @_); }
+sub input_number_tag         { return _call_presenter('input_number_tag',         @_); }
+sub textarea_tag             { return _call_presenter('textarea_tag',             @_); }
+sub date_tag                 { return _call_presenter('date_tag',                 @_); }
+sub div_tag                  { return _call_presenter('div_tag',                  @_); }
+sub radio_button_tag         { return _call_presenter('radio_button_tag',         @_); }
+sub img_tag                  { return _call_presenter('img_tag',                  @_); }
+sub restricted_html          { return _call_presenter('restricted_html',          @_); }
+sub stripped_html            { return _call_presenter('stripped_html',            @_); }
 
 sub _set_id_attribute {
   my ($attributes, $name, $unique) = @_;
   SL::Presenter::Tag::_set_id_attribute($attributes, $name, $unique);
 }
 
-sub img_tag {
-  my ($self, %options) = _hashify(1, @_);
-
-  $options{alt} ||= '';
-
-  return $self->html_tag('img', undef, %options);
-}
-
-sub textarea_tag {
-  my ($self, $name, $content, %attributes) = _hashify(3, @_);
-
-  _set_id_attribute(\%attributes, $name);
-  $attributes{rows}  *= 1; # required by standard
-  $attributes{cols}  *= 1; # required by standard
-  $content            = $content ? _H($content) : '';
-
-  return $self->html_tag('textarea', $content, %attributes, name => $name);
-}
-
-sub checkbox_tag {
-  my ($self, $name, %attributes) = _hashify(2, @_);
-
-  _set_id_attribute(\%attributes, $name);
-  $attributes{value}   = 1 unless defined $attributes{value};
-  my $label            = delete $attributes{label};
-  my $checkall         = delete $attributes{checkall};
-  my $for_submit       = delete $attributes{for_submit};
-
-  if ($attributes{checked}) {
-    $attributes{checked} = 'checked';
-  } else {
-    delete $attributes{checked};
-  }
-
-  my $code  = '';
-  $code    .= $self->hidden_tag($name, 0, %attributes, id => $attributes{id} . '_hidden') if $for_submit;
-  $code    .= $self->html_tag('input', undef,  %attributes, name => $name, type => 'checkbox');
-  $code    .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
-  $code    .= $self->javascript(qq|\$('#$attributes{id}').checkall('$checkall');|) if $checkall;
-
-  return $code;
-}
-
-sub radio_button_tag {
-  my ($self, $name, %attributes) = _hashify(2, @_);
-
-  $attributes{value}   = 1 unless exists $attributes{value};
-
-  _set_id_attribute(\%attributes, $name, 1);
-  my $label            = delete $attributes{label};
-
-  _set_id_attribute(\%attributes, $name . '_' . $attributes{value});
-
-  if ($attributes{checked}) {
-    $attributes{checked} = 'checked';
-  } else {
-    delete $attributes{checked};
-  }
-
-  my $code  = $self->html_tag('input', undef,  %attributes, name => $name, type => 'radio');
-  $code    .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
-
-  return $code;
-}
-
-sub hidden_tag {
-  my ($self, $name, $value, %attributes) = _hashify(3, @_);
-  return $self->input_tag($name, $value, %attributes, type => 'hidden');
-}
-
-sub div_tag {
-  my ($self, $content, @slurp) = @_;
-  return $self->html_tag('div', $content, @slurp);
-}
-
 sub ul_tag {
   my ($self, $content, @slurp) = @_;
   return $self->html_tag('ul', $content, @slurp);
@@ -161,56 +103,12 @@ sub li_tag {
   return $self->html_tag('li', $content, @slurp);
 }
 
-sub link {
-  my ($self, $href, $content, %params) = _hashify(3, @_);
-
-  $href ||= '#';
-
-  return $self->html_tag('a', $content, %params, href => $href);
-}
-
-sub submit_tag {
-  my ($self, $name, $value, %attributes) = _hashify(3, @_);
-
-  if ( $attributes{confirm} ) {
-    $attributes{onclick} = 'return confirm("'. _J(delete($attributes{confirm})) .'");';
-  }
-
-  return $self->input_tag($name, $value, %attributes, type => 'submit', class => 'submit');
-}
-
-sub button_tag {
-  my ($self, $onclick, $value, %attributes) = _hashify(3, @_);
-
-  _set_id_attribute(\%attributes, $attributes{name}) if $attributes{name};
-  $attributes{type} ||= 'button';
-
-  $onclick = 'if (!confirm("'. _J(delete($attributes{confirm})) .'")) return false; ' . $onclick if $attributes{confirm};
-
-  return $self->html_tag('input', undef, %attributes, value => $value, onclick => $onclick);
-}
-
-sub ajax_submit_tag {
-  my ($self, $url, $form_selector, $text, @slurp) = @_;
-
-  $url           = _J($url);
-  $form_selector = _J($form_selector);
-  my $onclick    = qq|kivi.submit_ajax_form('${url}', '${form_selector}')|;
-
-  return $self->button_tag($onclick, $text, @slurp);
-}
-
 sub yes_no_tag {
   my ($self, $name, $value, %attributes) = _hashify(3, @_);
 
   return $self->select_tag($name, [ [ 1 => $::locale->text('Yes') ], [ 0 => $::locale->text('No') ] ], default => $value ? 1 : 0, %attributes);
 }
 
-sub javascript {
-  my ($self, $data) = @_;
-  return $self->html_tag('script', $data, type => 'text/javascript');
-}
-
 sub stylesheet_tag {
   my $self = shift;
   my $code = '';
@@ -225,26 +123,6 @@ sub stylesheet_tag {
   return $code;
 }
 
-my $date_tag_id_idx = 0;
-sub date_tag {
-  my ($self, $name, $value, %params) = _hashify(3, @_);
-
-  _set_id_attribute(\%params, $name);
-  my @onchange = $params{onchange} ? (onChange => delete $params{onchange}) : ();
-  my @classes  = $params{no_cal} || $params{readonly} ? () : ('datepicker');
-  push @classes, delete($params{class}) if $params{class};
-  my %class    = @classes ? (class => join(' ', @classes)) : ();
-
-  $::request->presenter->need_reinit_widgets($params{id});
-
-  return $self->input_tag(
-    $name, blessed($value) ? $value->to_lxoffice : $value,
-    size   => 11,
-    onblur => "check_right_date_format(this);",
-    %params,
-    %class, @onchange,
-  );
-}
 
 # simple version with select_tag
 sub vendor_selector {
@@ -345,9 +223,15 @@ sub areainput_tag {
   my $maxrows = delete $attributes{max_rows};
   my $rows    = $::form->numtextrows($value, $cols, $maxrows, $minrows);
 
-  return $rows > 1
-    ? $self->textarea_tag($name, $value, %attributes, rows => $rows, cols => $cols)
-    : $self->input_tag($name, $value, %attributes, size => $cols);
+  $attributes{id} ||= _tag_id();
+  my $id            = $attributes{id};
+
+  return $self->textarea_tag($name, $value, %attributes, rows => $rows, cols => $cols) if $rows > 1;
+
+  return '<span>'
+    . $self->input_tag($name, $value, %attributes, size => $cols)
+    . "<img src=\"image/edit-entry.png\" onclick=\"kivi.switch_areainput_to_textarea('${id}')\" style=\"margin-left: 2px;\">"
+    . '</span>';
 }
 
 sub multiselect2side {
@@ -390,9 +274,13 @@ JAVASCRIPT
     $filter    .= ".map(function(idx, str) { return str.replace('$params{with}_', ''); })";
 
     my $params_js = $params{params} ? qq| + ($params{params})| : '';
+    my $ajax_return = '';
+    if ($params{ajax_return}) {
+      $ajax_return = 'kivi.eval_json_result';
+    }
 
     $stop_event = <<JAVASCRIPT;
-        \$.post('$params{url}'${params_js}, { '${as}[]': \$(\$('${selector}').sortable('toArray'))${filter}.toArray() });
+        \$.post('$params{url}'${params_js}, { '${as}[]': \$(\$('${selector}').sortable('toArray'))${filter}.toArray() }, $ajax_return);
 JAVASCRIPT
   }
 
@@ -538,8 +426,14 @@ The following functions are just forwarded to L<SL::Presenter::Tag>:
 
 =item * C<input_tag $name, $value, %attributes>
 
+=item * C<hidden_tag $name, $value, %attributes>
+
+=item * C<checkbox_tag $name, %attributes>
+
 =item * C<select_tag $name, \@collection, %attributes>
 
+=item * C<link $href, $content, %attributes>
+
 =back
 
 Available high-level functions implemented in this module:
@@ -553,77 +447,17 @@ calling L<select_tag>. C<$value> determines
 which entry is selected. The C<%attributes> are passed through to
 L<select_tag>.
 
-=item C<hidden_tag $name, $value, %attributes>
-
-Creates a HTML 'input type=hidden' tag named C<$name> with the value
-C<$value> and with arbitrary HTML attributes from C<%attributes>. The
-tag's C<id> defaults to C<name_to_id($name)>.
-
-=item C<submit_tag $name, $value, %attributes>
-
-Creates a HTML 'input type=submit class=submit' tag named C<$name> with the
-value C<$value> and with arbitrary HTML attributes from C<%attributes>. The
-tag's C<id> defaults to C<name_to_id($name)>.
-
-If C<$attributes{confirm}> is set then a JavaScript popup dialog will
-be added via the C<onclick> handler asking the question given with
-C<$attributes{confirm}>. The request is only submitted if the user
-clicks the dialog's ok/yes button.
-
-=item C<ajax_submit_tag $url, $form_selector, $text, %attributes>
-
-Creates a HTML 'input type="button"' tag with a very specific onclick
-handler that submits the form given by the jQuery selector
-C<$form_selector> to the URL C<$url> (the actual JavaScript function
-called for that is C<kivi.submit_ajax_form()> in
-C<js/client_js.js>). The button's label will be C<$text>.
-
-=item C<button_tag $onclick, $text, %attributes>
-
-Creates a HTML 'input type="button"' tag with an onclick handler
-C<$onclick> and a value of C<$text>. The button does not have a name
-nor an ID by default.
-
-If C<$attributes{confirm}> is set then a JavaScript popup dialog will
-be prepended to the C<$onclick> handler asking the question given with
-C<$attributes{confirm}>. The request is only submitted if the user
-clicks the dialog's "ok/yes" button.
-
 =item C<textarea_tag $name, $value, %attributes>
 
 Creates a HTML 'textarea' tag named C<$name> with the content
 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
 tag's C<id> defaults to C<name_to_id($name)>.
 
-=item C<checkbox_tag $name, %attributes>
-
-Creates a HTML 'input type=checkbox' tag named C<$name> with arbitrary
-HTML attributes from C<%attributes>. The tag's C<id> defaults to
-C<name_to_id($name)>. The tag's C<value> defaults to C<1>.
-
-If C<%attributes> contains a key C<label> then a HTML 'label' tag is
-created with said C<label>. No attribute named C<label> is created in
-that case.
-
-If C<%attributes> contains a key C<checkall> then the value is taken as a
-JQuery selector and clicking this checkbox will also toggle all checkboxes
-matching the selector.
-
 =item C<date_tag $name, $value, %attributes>
 
 Creates a date input field, with an attached javascript that will open a
 calendar on click.
 
-=item C<radio_button_tag $name, %attributes>
-
-Creates a HTML 'input type=radio' tag named C<$name> with arbitrary
-HTML attributes from C<%attributes>. The tag's C<value> defaults to
-C<1>. The tag's C<id> defaults to C<name_to_id($name . "_" . $value)>.
-
-If C<%attributes> contains a key C<label> then a HTML 'label' tag is
-created with said C<label>. No attribute named C<label> is created in
-that case.
-
 =item C<javascript_tag $file1, $file2, $file3...>
 
 Creates a HTML 'E<lt>script type="text/javascript" src="..."E<gt>'
@@ -696,9 +530,13 @@ C<%params> can contain the following entries:
 =item C<url>
 
 The URL to POST an AJAX request to after a dragged element has been
-dropped. The AJAX request's return value is ignored. If given then
+dropped. The AJAX request's return value is ignored by default. If given then
 C<$params{with}> must be given as well.
 
+=item C<ajax_return>
+
+If trueish then the AJAX request's return is accepted.
+
 =item C<with>
 
 A string that is interpreted as the prefix of the children's ID. Upon
index f935a59..66424fc 100644 (file)
@@ -41,15 +41,6 @@ sub round_amount {
   return '';
 }
 
-sub format_amount_units {
-  my ($self, $amount, $amount_unit, $part_unit) = @_;
-
-  return $main::form->format_amount_units('amount'      => $amount,
-                                          'part_unit'   => $part_unit,
-                                          'amount_unit' => $amount_unit,
-                                          'conv_units'  => 'convertible_not_smaller');
-}
-
 sub format_percent {
   my ($self, $var, $places, $skip_zero) = @_;
 
index 44eeb43..9862577 100644 (file)
@@ -3,6 +3,8 @@ package SL::Template::Plugin::P;
 use base qw( Template::Plugin );
 
 use SL::Presenter;
+use SL::Presenter::ALL;
+use SL::Presenter::Simple;
 use SL::Presenter::EscapedText;
 
 use strict;
@@ -24,21 +26,28 @@ sub AUTOLOAD {
   our $AUTOLOAD;
 
   my ($self, @args) = @_;
-
   my $presenter     = SL::Presenter->get;
   my $method        =  $AUTOLOAD;
   $method           =~ s/.*:://;
 
   return '' if $method eq 'DESTROY';
 
-  if (!$presenter->can($method)) {
-    $::lxdebug->message(LXDebug::WARN(), "SL::Presenter has no method named '$method'!");
-    return '';
+  splice @args, -1, 1, %{ $args[-1] } if @args && (ref($args[-1]) eq 'HASH');
+
+  if ($SL::Presenter::ALL::presenters{$method}) {
+    return SL::Presenter::ALL::wrap($SL::Presenter::ALL::presenters{$method});
   }
 
-  splice @args, -1, 1, %{ $args[-1] } if @args && (ref($args[-1]) eq 'HASH');
+  if (my $sub = SL::Presenter::Simple->can($method)) {
+    return $sub->(@args);
+  }
+
+  if ($presenter->can($method)) {
+    return $presenter->$method(@args);
+  }
 
-  $presenter->$method(@args);
+  $::lxdebug->message(LXDebug::WARN(), "SL::Presenter has no method named '$method'!");
+  return;
 }
 
 1;
@@ -57,10 +66,10 @@ SL::Template::Plugin::P - Template plugin for the presentation layer
 
   [% USE P %]
 
-  Customer: [% P.customer(customer) %]
+  Customer: [% customer.presenter.customer %]
 
   Linked records:
-  [% P.grouped_record_list(RECORDS) %]
+  [% P.record.grouped_list(RECORDS) %]
 
 =head1 FUNCTIONS
 
diff --git a/SL/Template/XML.pm b/SL/Template/XML.pm
deleted file mode 100644 (file)
index d9bd766..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-package SL::Template::XML;
-
-use parent qw(SL::Template::HTML);
-
-use strict;
-
-sub new {
-  #evtl auskommentieren
-  my $type = shift;
-
-  return $type->SUPER::new(@_);
-}
-
-sub format_string {
-  my ($self, $variable) = @_;
-  my $form = $self->{"form"};
-
-  $variable = $main::locale->quote_special_chars('Template/XML', $variable);
-
-  # Allow no markup to be converted into the output format
-  my @markup_replace = ('b', 'i', 's', 'u', 'sub', 'sup');
-
-  foreach my $key (@markup_replace) {
-    $variable =~ s/\&lt;(\/?)${key}\&gt;//g;
-  }
-
-  return $variable;
-}
-
-sub get_mime_type() {
-  my ($self) = @_;
-
-  return "text";
-
-}
-
-sub uses_temp_file {
-  # tempfile needet for XML Output
-  return 1;
-}
-
-1;
index 1e62cfb..b6fa365 100644 (file)
@@ -8,13 +8,18 @@ use Carp;
 use List::MoreUtils qw(any none);
 use SL::DBUtils;
 use SL::PrefixedNumber;
+use SL::DB;
+use SL::DB::DeliveryOrder::TypeData;
 
 use Rose::Object::MakeMethods::Generic
 (
  scalar => [ qw(type id number save dbh dbh_provided business_id) ],
 );
 
-my @SUPPORTED_TYPES = qw(invoice credit_note customer vendor sales_delivery_order purchase_delivery_order sales_order purchase_order sales_quotation request_quotation part service assembly letter);
+my @SUPPORTED_TYPES = (
+  qw(invoice invoice_for_advance_payment final_invoice credit_note customer vendor sales_delivery_order purchase_delivery_order sales_order purchase_order sales_quotation request_quotation part service assembly assortment letter),
+  @{ SL::DB::DeliveryOrder::TypeData::valid_types() },
+);
 
 sub new {
   my $class = shift;
@@ -23,7 +28,7 @@ sub new {
   croak "Invalid type " . $self->type if none { $_ eq $self->type } @SUPPORTED_TYPES;
 
   $self->dbh_provided($self->dbh);
-  $self->dbh($::form->get_standard_dbh) if !$self->dbh;
+  $self->dbh(SL::DB->client->dbh) if !$self->dbh;
   $self->save(1) unless defined $self->save;
   $self->business_id(undef) if $self->type ne 'customer';
 
@@ -36,7 +41,7 @@ sub _get_filters {
   my $type    = $self->type;
   my %filters = ( where => '' );
 
-  if (any { $_ eq $type } qw(invoice credit_note)) {
+  if (any { $_ eq $type } qw(invoice invoice_for_advance_payment final_invoice credit_note)) {
     $filters{trans_number}  = "invnumber";
     $filters{numberfield}   = $type eq 'credit_note' ? "cnnumber" : "invnumber";
     $filters{table}         = "ar";
@@ -46,11 +51,12 @@ sub _get_filters {
     $filters{numberfield}   = "${type}number";
     $filters{table}         = $type;
 
-  } elsif ($type =~ /_delivery_order$/) {
-    $filters{trans_number}  = "donumber";
-    $filters{numberfield}   = $type eq 'sales_delivery_order' ? "sdonumber" : "pdonumber";
+  } elsif ($type =~ /_delivery_order$/ && SL::DB::DeliveryOrder::TypeData::is_valid_type($type)) {
+    $filters{trans_number}  = SL::DB::DeliveryOrder::TypeData::get3($type, 'properties', 'nr_key'),
+    $filters{numberfield}   = SL::DB::DeliveryOrder::TypeData::get3($type, 'properties', 'transnumber'),
     $filters{table}         = "delivery_orders";
-    $filters{where}         = $type =~ /^sales/ ? '(customer_id IS NOT NULL)' : '(vendor_id IS NOT NULL)';
+    $filters{where}         = "order_type = ?";
+    $filters{values}        = [ $::form->{type} ];
 
   } elsif ($type =~ /_order$/) {
     $filters{trans_number}  = "ordnumber";
@@ -66,10 +72,14 @@ sub _get_filters {
     $filters{where}         = 'COALESCE(quotation, FALSE)';
     $filters{where}        .= $type =~ /^sales/ ? ' AND (customer_id IS NOT NULL)' : ' AND (vendor_id IS NOT NULL)';
 
-  } elsif ($type =~ /part|service|assembly/) {
+  } elsif ($type =~ /^(part|service|assembly|assortment)$/) {
     $filters{trans_number}  = "partnumber";
-    $filters{numberfield}   = $type eq 'service' ? 'servicenumber' : 'articlenumber';
-    $filters{numberfield}   = $type eq 'assembly' ? 'assemblynumber' : $filters{numberfield};
+    my %numberfield_hash = ( service    => 'servicenumber',
+                             assembly   => 'assemblynumber',
+                             assortment => 'assortmentnumber',
+                             part       => 'articlenumber'
+                           );
+    $filters{numberfield}   = $numberfield_hash{$type};
     $filters{table}         = "parts";
   } elsif ($type =~ /letter/) {
     $filters{trans_number}  = "letternumber";
@@ -91,6 +101,7 @@ sub is_unique {
   my @values = ($self->number);
 
   push @where, $filters{where} if $filters{where};
+  push @values, @{ $filters{values} } if $filters{values};
 
   if ($self->id) {
     push @where,  qq|id <> ?|;
@@ -115,42 +126,44 @@ sub create_unique {
 
   my $form    = $main::form;
   my %filters = $self->_get_filters();
-
-  $self->dbh->begin_work if $self->dbh->{AutoCommit};
-
-  my $where = $filters{where} ? ' WHERE ' . $filters{where} : '';
-  my $query = <<SQL;
-    SELECT DISTINCT $filters{trans_number}, 1 AS in_use
-    FROM $filters{table}
-    $where
+  my $number;
+
+  SL::DB->client->with_transaction(sub {
+    my $where = $filters{where} ? ' WHERE ' . $filters{where} : '';
+    my $query = <<SQL;
+      SELECT DISTINCT $filters{trans_number}, 1 AS in_use
+      FROM $filters{table}
+      $where
 SQL
 
-  do_query($form, $self->dbh, "LOCK TABLE " . $filters{table}) || die $self->dbh->errstr;
-  my %numbers_in_use = selectall_as_map($form, $self->dbh, $query, $filters{trans_number}, 'in_use');
+    do_query($form, $self->dbh, "LOCK TABLE " . $filters{table}) || die $self->dbh->errstr;
+    my %numbers_in_use = selectall_as_map($form, $self->dbh, $query, $filters{trans_number}, 'in_use', @{ $filters{values} // [] });
 
-  my $business_number;
-  ($business_number) = selectfirst_array_query($form, $self->dbh, qq|SELECT customernumberinit FROM business WHERE id = ? FOR UPDATE|, $self->business_id) if $self->business_id;
-  my $number         = $business_number;
-  ($number)          = selectfirst_array_query($form, $self->dbh, qq|SELECT $filters{numberfield} FROM defaults FOR UPDATE|)                               if !$number;
-  if ($filters{numberfield} eq 'assemblynumber' and length($number) < 1) {
-    $filters{numberfield} = 'articlenumber';
-    ($number)        = selectfirst_array_query($form, $self->dbh, qq|SELECT $filters{numberfield} FROM defaults FOR UPDATE|)                               if !$number;
-  }
-  $number          ||= '';
-  my $sequence       = SL::PrefixedNumber->new(number => $number);
-
-  do {
-    $number = $sequence->get_next;
-  } while ($numbers_in_use{$number});
-
-  if ($self->save) {
-    if ($self->business_id && $business_number) {
-      do_query($form, $self->dbh, qq|UPDATE business SET customernumberinit = ? WHERE id = ?|, $number, $self->business_id);
-    } else {
-      do_query($form, $self->dbh, qq|UPDATE defaults SET $filters{numberfield} = ?|, $number);
+    my $business_number;
+    ($business_number) = selectfirst_array_query($form, $self->dbh, qq|SELECT customernumberinit FROM business WHERE id = ? FOR UPDATE|, $self->business_id) if $self->business_id;
+    $number         = $business_number;
+    ($number)          = selectfirst_array_query($form, $self->dbh, qq|SELECT $filters{numberfield} FROM defaults FOR UPDATE|)                               if !$number;
+    if ($filters{numberfield} eq 'assemblynumber' and length($number) < 1) {
+      $filters{numberfield} = 'articlenumber';
+      ($number)        = selectfirst_array_query($form, $self->dbh, qq|SELECT $filters{numberfield} FROM defaults FOR UPDATE|)                               if !$number;
     }
-    $self->dbh->commit if !$self->dbh_provided;
-  }
+    $number          ||= '';
+    my $sequence       = SL::PrefixedNumber->new(number => $number);
+
+    do {
+      $number = $sequence->get_next;
+    } while ($numbers_in_use{$number});
+
+    if ($self->save) {
+      if ($self->business_id && $business_number) {
+        do_query($form, $self->dbh, qq|UPDATE business SET customernumberinit = ? WHERE id = ?|, $number, $self->business_id);
+      } else {
+        do_query($form, $self->dbh, qq|UPDATE defaults SET $filters{numberfield} = ?|, $number);
+      }
+    }
+
+    1;
+  }) or do { die SL::DB->client->error };
 
   return $number;
 }
index a80372b..ee2d21f 100644 (file)
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 # Utilities for ustva
 #=====================================================================
 
 package USTVA;
 
+use Carp;
+use Data::Dumper;
 use List::Util qw(first);
 
+use SL::DB;
 use SL::DBUtils;
+use SL::DB::Default;
+use SL::DB::Finanzamt;
+use SL::Locale::String qw(t8);
 
 use utf8;
 use strict;
 
 my @tax_office_information = (
-  { 'id' =>  8, 'name' => 'Baden-Württemberg',      'taxbird_nr' => '0',  'elster_format' => 'FF/BBB/UUUUP',  },
-  { 'id' =>  9, 'name' => 'Bayern',                 'taxbird_nr' => '1',  'elster_format' => 'FFF/BBB/UUUUP', },
-  { 'id' => 11, 'name' => 'Berlin',                 'taxbird_nr' => '2',  'elster_format' => 'FF/BBB/UUUUP',  },
-  { 'id' => 12, 'name' => 'Brandenburg',            'taxbird_nr' => '3',  'elster_format' => 'FFF/BBB/UUUUP', },
-  { 'id' =>  4, 'name' => 'Bremen',                 'taxbird_nr' => '4',  'elster_format' => 'FF BBB UUUUP',  },
-  { 'id' =>  2, 'name' => 'Hamburg',                'taxbird_nr' => '5',  'elster_format' => 'FF/BBB/UUUUP',  },
-  { 'id' =>  6, 'name' => 'Hessen',                 'taxbird_nr' => '6',  'elster_format' => '0FF BBB UUUUP', },
-  { 'id' => 13, 'name' => 'Mecklenburg-Vorpommern', 'taxbird_nr' => '7',  'elster_format' => 'FFF/BBB/UUUUP', },
-  { 'id' =>  3, 'name' => 'Niedersachsen',          'taxbird_nr' => '8',  'elster_format' => 'FF/BBB/UUUUP',  },
-  { 'id' =>  5, 'name' => 'Nordrhein-Westfalen',    'taxbird_nr' => '9',  'elster_format' => 'FFF/BBBB/UUUP', },
-  { 'id' =>  7, 'name' => 'Rheinland-Pfalz',        'taxbird_nr' => '10', 'elster_format' => 'FF/BBB/UUUU/P', },
-  { 'id' => 10, 'name' => 'Saarland',               'taxbird_nr' => '11', 'elster_format' => 'FFF/BBB/UUUUP', },
-  { 'id' => 14, 'name' => 'Sachsen',                'taxbird_nr' => '12', 'elster_format' => 'FFF/BBB/UUUUP', },
-  { 'id' => 15, 'name' => 'Sachsen-Anhalt',         'taxbird_nr' => '13', 'elster_format' => 'FFF/BBB/UUUUP', },
-  { 'id' =>  1, 'name' => 'Schleswig-Holstein',     'taxbird_nr' => '14', 'elster_format' => 'FF BBB UUUUP',  },
-  { 'id' => 16, 'name' => 'Thüringen',              'taxbird_nr' => '15', 'elster_format' => 'FFF/BBB/UUUUP', },
+  { 'id' =>  8, 'name' => 'Baden-Württemberg',      'taxbird_nr' => '1',  'elster_format' => 'FFBBB/UUUUP',  },
+  { 'id' =>  9, 'name' => 'Bayern',                 'taxbird_nr' => '2',  'elster_format' => 'FFF/BBB/UUUUP', },
+  { 'id' => 11, 'name' => 'Berlin',                 'taxbird_nr' => '3',  'elster_format' => 'FF/BBB/UUUUP',  },
+  { 'id' => 12, 'name' => 'Brandenburg',            'taxbird_nr' => '4',  'elster_format' => 'FFF/BBB/UUUUP', },
+  { 'id' =>  4, 'name' => 'Bremen',                 'taxbird_nr' => '5',  'elster_format' => 'FF BBB UUUUP',  },
+  { 'id' =>  2, 'name' => 'Hamburg',                'taxbird_nr' => '6',  'elster_format' => 'FF/BBB/UUUUP',  },
+  { 'id' =>  6, 'name' => 'Hessen',                 'taxbird_nr' => '7',  'elster_format' => '0FF BBB UUUUP', },
+  { 'id' => 13, 'name' => 'Mecklenburg-Vorpommern', 'taxbird_nr' => '8',  'elster_format' => 'FFF/BBB/UUUUP', },
+  { 'id' =>  3, 'name' => 'Niedersachsen',          'taxbird_nr' => '9',  'elster_format' => 'FF/BBB/UUUUP',  },
+  { 'id' =>  5, 'name' => 'Nordrhein-Westfalen',    'taxbird_nr' => '10', 'elster_format' => 'FFF/BBBB/UUUP', },
+  { 'id' =>  7, 'name' => 'Rheinland-Pfalz',        'taxbird_nr' => '11', 'elster_format' => 'FF/BBB/UUUUP', },
+  { 'id' => 10, 'name' => 'Saarland',               'taxbird_nr' => '12', 'elster_format' => 'FFF/BBB/UUUUP', },
+  { 'id' => 14, 'name' => 'Sachsen',                'taxbird_nr' => '13', 'elster_format' => 'FFF/BBB/UUUUP', },
+  { 'id' => 15, 'name' => 'Sachsen-Anhalt',         'taxbird_nr' => '14', 'elster_format' => 'FFF/BBB/UUUUP', },
+  { 'id' =>  1, 'name' => 'Schleswig-Holstein',     'taxbird_nr' => '15', 'elster_format' => 'FF BBB UUUUP',  },
+  { 'id' => 16, 'name' => 'Thüringen',              'taxbird_nr' => '16', 'elster_format' => 'FFF/BBB/UUUUP', },
   );
 
+  my @fiamt_config = qw(taxnumber fa_bufa_nr fa_dauerfrist fa_steuerberater_city fa_steuerberater_name
+  fa_steuerberater_street fa_steuerberater_tel fa_voranmeld);
+
+  my @fiamt_finanzamt = qw(
+    fa_land_nr          fa_bufa_nr            fa_name             fa_strasse
+    fa_plz              fa_ort                fa_telefon          fa_fax
+    fa_plz_grosskunden  fa_plz_postfach       fa_postfach
+    fa_blz_1 fa_kontonummer_1 fa_bankbezeichnung_1
+    fa_blz_2 fa_kontonummer_2 fa_bankbezeichnung_2 fa_oeffnungszeiten
+    fa_email fa_internet);
+
+
 sub new {
   my $type = shift;
 
@@ -114,63 +133,25 @@ sub report_variables {
     $where_dcp
   |;
 
-  my $dbh = $form->dbconnect($myconfig);
-  my $sth = $dbh->prepare($query);
-
-  $sth->execute() || $form->dberror($query);
-
   my @positions;
 
-  while ( my $row_ref = $sth->fetchrow_arrayref() ) {
-    push @positions, @$row_ref;  # Copy the array contents
-  }
+  SL::DB->client->with_transaction(sub {
+    my $dbh = SL::DB->client->dbh;
+    my $sth = $dbh->prepare($query);
 
-  $sth->finish;
+    $sth->execute() || $form->dberror($query);
 
-  $dbh->disconnect;
+    while ( my $row_ref = $sth->fetchrow_arrayref() ) {
+      push @positions, @$row_ref;  # Copy the array contents
+    }
 
-  return @positions;
+    $sth->finish;
+    1;
+  }) or do { die SL::DB->client->error };
 
+  return @positions;
 }
 
-sub create_steuernummer {
-  $main::lxdebug->enter_sub();
-
-  my $form = $main::form;
-
-  our ($elster_FFFF);
-
-  my $part           = $form->{part};
-  my $patterncount   = $form->{patterncount};
-  my $delimiter      = $form->{delimiter};
-  my $elster_pattern = $form->{elster_pattern};
-
-  # rebuild steuernummer and elstersteuernummer
-  # es gibt eine gespeicherte steuernummer $form->{steuernummer}
-  # und die parts und delimiter
-
-  my $h = 0;
-  my $i = 0;
-
-  my $steuernummer_new        = $part;
-  my $elstersteuernummer_new  = $elster_FFFF;
-  $elstersteuernummer_new    .= '0';
-
-  for ($h = 1; $h < $patterncount; $h++) {
-    $steuernummer_new .= qq|$delimiter|;
-    for ($i = 1; $i <= length($elster_pattern); $i++) {
-      $steuernummer_new       .= $form->{"part_$h\_$i"};
-      $elstersteuernummer_new .= $form->{"part_$h\_$i"};
-    }
-  }
-  if ($form->{steuernummer} ne $steuernummer_new) {
-    $form->{steuernummer}       = $steuernummer_new;
-    $form->{elstersteuernummer} = $elstersteuernummer_new;
-    $form->{steuernummer_new}   = $steuernummer_new;
-  }
-  $main::lxdebug->leave_sub();
-  return ($steuernummer_new, $elstersteuernummer_new);
-}
 
 sub steuernummer_input {
   $main::lxdebug->enter_sub();
@@ -191,16 +172,27 @@ sub steuernummer_input {
   #Pattern description Elstersteuernummer
 
   #split the pattern
-  my $tax_office     = first { $_->{name} eq $elster_land } @{ $self->{tax_office_information} };
+  my $tax_office     = first { $_->{id} eq $elster_land } @{ $self->{tax_office_information} };
   my $elster_pattern = $tax_office->{elster_format};
+ # $::lxdebug->message(LXDebug->DEBUG2, "stnr=".$stnr." elster_FFFF=".$elster_FFFF.
+ #                     " pattern=".$elster_pattern." land=".$elster_land);
   my @elster_pattern = split(' ', $elster_pattern);
-  my $delimiter      = '&nbsp;';
+  my $delimiter1      = '&nbsp;';
+  my $delimiter2      = '&nbsp;';
   my $patterncount   = @elster_pattern;
   if ($patterncount < 2) {
     @elster_pattern = ();
     @elster_pattern = split('/', $elster_pattern);
-    $delimiter      = '/';
+    $delimiter1      = '/';
+    $delimiter2      = '/';
     $patterncount   = @elster_pattern;
+    if ($patterncount < 2) {
+        @elster_pattern = ();
+        @elster_pattern = split(' ', $elster_pattern);
+        $delimiter1      = ' ';
+        $delimiter2      = ' ';
+        $patterncount   = @elster_pattern;
+    }
   }
 
   # no we have an array of patternparts and a delimiter
@@ -208,6 +200,7 @@ sub steuernummer_input {
 
   $steuernummer_input .= qq|<b><font size="+1">|;
   my $part = '';
+#  $::lxdebug->message(LXDebug->DEBUG2, "pattern0=".$elster_pattern[0]);
 SWITCH: {
     $elster_pattern[0] eq 'FFF' && do {
       $part = substr($elster_FFFF, 1, 4);
@@ -219,6 +212,15 @@ SWITCH: {
       $steuernummer_input .= qq|$part|;
       last SWITCH;
     };
+    $elster_pattern[0] eq 'FFBBB' && do {
+      $part = substr($elster_FFFF, 2, 4);
+      $steuernummer_input .= qq|$part|;
+      $delimiter1 = '';
+      $patterncount++ ;
+      # Sonderfall BW
+      @elster_pattern = ('FF','BBB','UUUUP');
+      last SWITCH;
+    };
     $elster_pattern[0] eq 'FF' && do {
       $part = substr($elster_FFFF, 2, 4);
       $steuernummer_input .= qq|$part|;
@@ -231,19 +233,22 @@ SWITCH: {
   }
 
   #now the rest of the Steuernummer ...
-  $steuernummer_input .= qq|</b></font>|;
+  $steuernummer_input .= qq|</font></b>|;
   $steuernummer_input .= qq|\n
            <input type=hidden name="elster_pattern" value="$elster_pattern">
            <input type=hidden name="patterncount" value="$patterncount">
            <input type=hidden name="patternlength" value="$patterncount">
-           <input type=hidden name="delimiter" value="$delimiter">
+           <input type=hidden name="delimiter1" value="$delimiter1">
+           <input type=hidden name="delimiter2" value="$delimiter2">
            <input type=hidden name="part" value="$part">
   |;
 
   my $k = 0;
 
   for (my $h = 1; $h < $patterncount; $h++) {
+    my $delimiter = ( $h==1?$delimiter1:$delimiter2);
     $steuernummer_input .= qq|&nbsp;$delimiter&nbsp;\n|;
+#  $::lxdebug->message(LXDebug->DEBUG2, "pattern[$h]=".$elster_pattern[$h]);
     for (my $i = 1; $i <= length($elster_pattern[$h]); $i++) {
       $steuernummer_input .= qq|<select name="part_$h\_$i">\n|;
 
@@ -269,42 +274,46 @@ SWITCH: {
 sub fa_auswahl {
   $main::lxdebug->enter_sub();
 
-#  use SL::Form;
-
   # Referenz wird übergeben, hash of hash wird nicht
   # in neues  Hash kopiert, sondern direkt über die Referenz verändert
   # Prototyp für diese Konstruktion
 
   my ($self, $land, $elsterFFFF, $elster_init) = @_;
 
+#  $::lxdebug->message(LXDebug->DEBUG2,"land=".$land." amt=".$elsterFFFF);
   my $terminal = '';
   my $FFFF     = $elsterFFFF;
   my $ffff     = '';
   my $checked  = '';
   $checked = 'checked' if ($elsterFFFF eq '' and $land eq '');
   my %elster_land_fa;
+  my %elster_land_name = ();
 
   my $fa_auswahl = qq|
         <script language="Javascript">
         function update_auswahl()
         {
-                var elsterBLAuswahl = document.verzeichnis.elsterland_new;
-                var elsterFAAuswahl = document.verzeichnis.elsterFFFF_new;
+                var elsterBLAuswahl = document.verzeichnis.fa_land_nr_new;
+                var elsterFAAuswahl = document.verzeichnis.fa_bufa_nr_new;
 
                 elsterFAAuswahl.options.length = 0; // dropdown aufräumen
                 |;
 
   foreach my $elster_land (sort keys %$elster_init) {
     $fa_auswahl .= qq|
-               if (elsterBLAuswahl.options[elsterBLAuswahl.selectedIndex].
-               value == "$elster_land")
+               if (elsterBLAuswahl.options[elsterBLAuswahl.selectedIndex].value == "$elster_land")
                {
                |;
     my $j              = 0;
     %elster_land_fa = ();
     $FFFF = '';
     for $FFFF (keys %{ $elster_init->{$elster_land} }) {
-      $elster_land_fa{$FFFF} = $elster_init->{$elster_land}->{$FFFF}->[0];
+        if ( $FFFF eq 'name' ) {
+            $elster_land_name{$elster_land} = $elster_init->{$elster_land}{$FFFF};
+            delete $elster_init->{$elster_land}{$FFFF};
+        } else {
+            $elster_land_fa{$FFFF} = $elster_init->{$elster_land}{$FFFF}->fa_name;
+       }
     }
     foreach $ffff (sort { $elster_land_fa{$a} cmp $elster_land_fa{$b} }
                    keys(%elster_land_fa)
@@ -326,20 +335,22 @@ sub fa_auswahl {
                Bundesland
             </td>
             <td>
-              <select size="1" name="elsterland_new" onchange="update_auswahl()">|;
+              <select size="1" name="fa_land_nr_new" onchange="update_auswahl()">|;
   if ($land eq '') {
     $fa_auswahl .= qq|<option value="Auswahl" $checked>| . $main::locale->text('Select federal state...') . qq|</option>\n|;
   }
   foreach my $elster_land (sort keys %$elster_init) {
     $fa_auswahl .= qq|
                   <option value="$elster_land"|;
+#  $::lxdebug->message(LXDebug->DEBUG2,"land=".$land." elster_land=".$elster_land." lname=".$elster_land_name{$elster_land});
     if ($elster_land eq $land and $checked eq '') {
       $fa_auswahl .= qq| selected|;
     }
-    $fa_auswahl .= qq|>$elster_land</option>
+    $fa_auswahl .= qq|>$elster_land_name{$elster_land}</option>
              |;
   }
   $fa_auswahl .= qq|
+              </select>
             </td>
           </tr>
           |;
@@ -348,7 +359,7 @@ sub fa_auswahl {
   $elster_land = ($land ne '') ? $land : '';
   %elster_land_fa = ();
   for $FFFF (keys %{ $elster_init->{$elster_land} }) {
-    $elster_land_fa{$FFFF} = $elster_init->{$elster_land}->{$FFFF}->[0];
+    $elster_land_fa{$FFFF} = $elster_init->{$elster_land}{$FFFF}->fa_name;
   }
 
   $fa_auswahl .= qq|
@@ -356,7 +367,7 @@ sub fa_auswahl {
               <td>Finanzamt
               </td>
               <td>
-                 <select size="1" name="elsterFFFF_new">|;
+                 <select size="1" name="fa_bufa_nr_new">|;
   if ($elsterFFFF eq '') {
     $fa_auswahl .= qq|<option value="Auswahl" $checked>| . $main::locale->text('Select tax office...') . qq|</option>|;
   } else {
@@ -373,10 +384,10 @@ sub fa_auswahl {
     }
   }
   $fa_auswahl .= qq|
-                 </td>
-              </tr>
-            </table>
-            </select>|;
+                 </select>
+              </td>
+          </tr>
+        </table>|;
 
   $main::lxdebug->leave_sub();
 
@@ -401,7 +412,7 @@ sub info {
     </body>
     |;
 
-    ::end_of_request();
+    $::dispatcher->end_request;
 
   } else {
 
@@ -411,95 +422,12 @@ sub info {
   $main::lxdebug->leave_sub();
 }
 
-# 20.10.2009 sschoeling: this sub seems to be orphaned.
-sub stichtag {
-  $main::lxdebug->enter_sub();
-
-  # noch nicht fertig
-  # soll mal eine Erinnerungsfunktion für USTVA Abgaben werden, die automatisch
-  # den Termin der nächsten USTVA anzeigt.
-  #
-  #
-  my ($today, $FA_dauerfrist, $FA_voranmeld) = @_;
-
-  #$today zerlegen:
-
-  #$today =today * 1;
-  $today =~ /(\d\d\d\d)(\d\d)(\d\d)/;
-  my $year     = $1;
-  my $month    = $2;
-  my $day      = $3;
-  my $yy       = $year;
-  my $mm       = $month;
-  my $yymmdd   = "$year$month$day" * 1;
-  my $mmdd     = "$month$day" * 1;
-  my $stichtag = '';
-
-  #$tage_bis = '1234';
-  #$ical = '...vcal format';
-
-  #if ($FA_voranmeld eq 'month'){
-
-  my %liste = (
-    "0110" => 'December',
-    "0210" => 'January',
-    "0310" => 'February',
-    "0410" => 'March',
-    "0510" => 'April',
-    "0610" => 'May',
-    "0710" => 'June',
-    "0810" => 'July',
-    "0910" => 'August',
-    "1010" => 'September',
-    "1110" => 'October',
-    "1210" => 'November',
-  );
-
-  #$mm += $dauerfrist
-  #$month *= 1;
-  $month += 1 if ($day > 10);
-  $month    = sprintf("%02d", $month);
-  $stichtag = $year . $month . "10";
-  my $ust_va   = $month . "10";
-
-  foreach my $date (%liste) {
-    $ust_va = $liste{$date} if ($date eq $stichtag);
-  }
-
-  #} elsif ($FA_voranmeld eq 'quarter'){
-  #1;
-
-  #}
-
-  #@stichtag = ('10.04.2004', '10.05.2004');
-
-  #@liste = ['0110', '0210', '0310', '0410', '0510', '0610', '0710', '0810', '0910',
-  #          '1010', '1110', '1210', ];
-  #
-  #foreach $key (@liste){
-  #  #if ($ddmm < ('0110' * 1));
-  #  if ($ddmm ){}
-  #  $stichtag = $liste[$key - 1] if ($ddmm > $key);
-  #
-  #}
-  #
-  #$stichtag =~ /([\d]\d)(\d\d)$/
-  #$stichtag = "$1.$2.$yy"
-  #$stichtag=$1;
-  our $description; # most probably not existent.
-  our $tage_bis;    # most probably not existent.
-  our $ical;        # most probably not existent.
-
-  $main::lxdebug->leave_sub();
-  return ($stichtag, $description, $tage_bis, $ical);
-}
-
 sub query_finanzamt {
   $main::lxdebug->enter_sub();
 
   my ($self, $myconfig, $form) = @_;
 
-  my $dbh = $form->dbconnect($myconfig) or $self->error(DBI->errstr);
+  my $dbh = SL::DB->client->dbh;
 
   #Test, if table finanzamt exist
   my $table    = 'finanzamt';
@@ -510,77 +438,23 @@ sub query_finanzamt {
     #There is no table, read the table from sql/finanzamt.sql
     print qq|<p>Bitte warten, Tabelle $table wird einmalig in Datenbank:
     $myconfig->{dbname} als Benutzer: $myconfig->{dbuser} hinzugefügt...</p>|;
-    process_query($form, $dbh, $filename) || $self->error(DBI->errstr);
-
-    #execute second last call
-    my $dbh = $form->dbconnect($myconfig) or $self->error(DBI->errstr);
-    $dbh->disconnect();
+    SL::DB->client->with_transaction(sub {
+      process_query($form, $dbh, $filename) || $self->error(DBI->errstr);
+      1;
+    }) or do { die SL::DB->client->error };
   };
   $tst->finish();
 
-  #$dbh->disconnect();
-
-  my @vars = (
-    'FA_Land_Nr',             #  0
-    'FA_BUFA_Nr',             #  1
-                              #'FA_Verteiler',                             #  2
-    'FA_Name',                #  3
-    'FA_Strasse',             #  4
-    'FA_PLZ',                 #  5
-    'FA_Ort',                 #  6
-    'FA_Telefon',             #  7
-    'FA_Fax',                 #  8
-    'FA_PLZ_Grosskunden',     #  9
-    'FA_PLZ_Postfach',        # 10
-    'FA_Postfach',            # 11
-    'FA_BLZ_1',               # 12
-    'FA_Kontonummer_1',       # 13
-    'FA_Bankbezeichnung_1',   # 14
-                              #'FA_BankIBAN_1',                            # 15
-                              #'FA_BankBIC_1',                             # 16
-                              #'FA_BankInhaber_BUFA_Nr_1',                 # 17
-    'FA_BLZ_2',               # 18
-    'FA_Kontonummer_2',       # 19
-    'FA_Bankbezeichnung_2',   # 20
-                              #'FA_BankIBAN_2',                            # 21
-                              #'FA_BankBIC_2',                             # 22
-                              #'FA_BankInhaber_BUFA_Nr_2',                 # 23
-    'FA_Oeffnungszeiten',     # 24
-    'FA_Email',               # 25
-    'FA_Internet'             # 26
-                              #'FA_zustaendige_Hauptstelle_BUFA_Nr',       # 27
-                              #'FA_zustaendige_vorgesetzte_Finanzbehoerde' # 28
-  );
 
-  my $field = join(', ', @vars);
-
-  my $query = "SELECT $field FROM finanzamt ORDER BY FA_Land_nr";
-  my $sth = $dbh->prepare($query) or $self->error($dbh->errstr);
-  $sth->execute || $form->dberror($query);
-  my $array_ref = $sth->fetchall_arrayref();
-  my $land      = '';
+  my $fiamt =  SL::DB::Finanzamt->_get_manager_class->get_all(sort => 'fa_land_nr');
+  my $land      = 0;
   my %finanzamt;
-  foreach my $row (@$array_ref) {
-    my $FA_finanzamt = $row;
-    my $tax_office   = first { $_->{id} == $FA_finanzamt->[0] } @{ $self->{tax_office_information} };
-    $land            = $tax_office->{name};
-
-    # $land = $main::locale->{iconv}->convert($land);
-
-    my $ffff = @$FA_finanzamt[1];
-
-    my $rec = {};
-    $rec->{$land} = $ffff;
-
-    shift @$row;
-    shift @$row;
-
-    $finanzamt{$land}{$ffff} = [@$FA_finanzamt];
+  foreach my $row (@$fiamt) {
+    my $tax_office   = first { $_->{id} == $row->fa_land_nr } @{ $self->{tax_office_information} };
+    $land            = $tax_office->{id};
+    $finanzamt{$land}{$row->fa_bufa_nr}  = $row;
+    $finanzamt{$land}{'name'} ||= $tax_office->{name};
   }
-
-  $sth->finish();
-  $dbh->disconnect();
-
   $main::lxdebug->leave_sub();
 
   return \%finanzamt;
@@ -589,11 +463,8 @@ sub query_finanzamt {
 sub process_query {
   $main::lxdebug->enter_sub();
 
-  # Copyright D. Simander -> SL::Form under Gnu GPL.
   my ($form, $dbh, $filename) = @_;
 
-  #  return unless (-f $filename);
-
   open my $FH, "<", "$filename" or $form->error("$filename : $!\n");
   my $query = "";
   my $sth;
@@ -648,12 +519,16 @@ sub ustva {
 
   my ($self, $myconfig, $form) = @_;
 
-  # connect to database
-  my $dbh = $form->get_standard_dbh;
+  my $dbh = SL::DB->client->dbh;
 
   my $last_period     = 0;
   my $category        = "pos_ustva";
 
+  $form->{coa} = $::instance_conf->get_coa;
+
+  unless ($form->{coa} eq 'Germany-DATEV-SKR03EU' or $form->{coa} eq 'Germany-DATEV-SKR04EU') {
+    croak t8("Advance turnover tax return only valid for SKR03 or SKR04");
+  }
   my @category_cent = USTVA->report_variables({
       myconfig    => $myconfig,
       form        => $form,
@@ -661,9 +536,10 @@ sub ustva {
       attribute   => 'position',
       dec_places  => '2',
   });
-
-  push @category_cent, qw(83  Z43  Z45  Z53  Z62  Z65  Z67);
-
+  push @category_cent, ("pos_ustva_811b_kivi", "pos_ustva_861b_kivi");
+  if ( $form->{coa} eq 'Germany-DATEV-SKR03EU' or $form->{coa} eq 'Germany-DATEV-SKR04EU') {
+      push @category_cent, qw(Z43  Z45  Z53  Z54  Z62  Z65  Z67);
+  }
   my @category_euro = USTVA->report_variables({
       myconfig    => $myconfig,
       form        => $form,
@@ -671,15 +547,9 @@ sub ustva {
       attribute   => 'position',
       dec_places  => '0',
   });
-
-  push @category_euro, USTVA->report_variables({
-      myconfig    => $myconfig,
-      form        => $form,
-      type        => '',
-      attribute   => 'position',
-      dec_places  => '0',
-  });
-
+  push @category_euro, ("pos_ustva_81b_kivi", "pos_ustva_86b_kivi");
+  @{$form->{category_cent}} = @category_cent;
+  @{$form->{category_euro}} = @category_euro;
   $form->{decimalplaces} *= 1;
 
   foreach my $item (@category_cent) {
@@ -688,14 +558,11 @@ sub ustva {
   foreach my $item (@category_euro) {
     $form->{"$item"} = 0;
   }
-  my $coa_name = $::instance_conf->get_coa;
-  $form->{coa} = $coa_name;
 
   # Controlvariable for templates
+  my $coa_name = $form->{coa};
   $form->{"$coa_name"} = '1';
 
-  $main::lxdebug->message(LXDebug->DEBUG2(), "COA: '$form->{coa}',  \$form->{$coa_name} = 1");
-
   &get_accounts_ustva($dbh, $last_period, $form->{fromdate}, $form->{todate},
                       $form, $category);
 
@@ -707,7 +574,7 @@ sub ustva {
 
   # Germany
 
-  if ( $form->{coa} eq 'Germany-DATEV-SKR03EU' or $form->{coa} eq 'Germany-DATEV-SKR04EU'){
+  if ( $form->{coa} eq 'Germany-DATEV-SKR03EU' or $form->{coa} eq 'Germany-DATEV-SKR04EU') {
 
     # 16%/19% Umstellung
     # Umordnen der Kennziffern
@@ -730,7 +597,7 @@ sub ustva {
 
   # Fixme: Wird auch noch für Oesterreich gebraucht,
   # weil kein eigenes Ausgabeformular
-  # sotte aber aus der allgeméinen Steuerberechnung verschwinden
+  # sollte aber aus der allgemeinen Steuerberechnung verschwinden
   #
   # Berechnung der USTVA Formularfelder laut Bogen 207
   #
@@ -757,8 +624,6 @@ sub ustva {
   $form->{"Z65"} = $form->{"Z62"}     - $form->{"69"};
   $form->{"83"}  = $form->{"Z65"}     - $form->{"39"};
 
-  $dbh->disconnect;
-
   $main::lxdebug->leave_sub();
 }
 
@@ -777,10 +642,10 @@ sub get_accounts_ustva {
   my $arwhere  = "";
   my $item;
 
-    my $gltaxkey_where = "((tk.pos_ustva = 46) OR (tk.pos_ustva>=59 AND tk.pos_ustva<=67) or (tk.pos_ustva>=89 AND tk.pos_ustva<=93))";
+  my $gltaxkey_where = "((tk.pos_ustva = 46) OR (tk.pos_ustva>=59 AND tk.pos_ustva<=67) or (tk.pos_ustva>=89 AND tk.pos_ustva<=93))";
 
   if ($fromdate) {
-    if ($form->{method} eq 'cash') {
+    if ($form->{accounting_method} eq 'cash') {
       $subwhere .= " AND transdate >= '$fromdate'";
       $glwhere = " AND ac.transdate >= '$fromdate'";
       $ARwhere .= " AND acc.transdate >= '$fromdate'";
@@ -816,7 +681,7 @@ sub get_accounts_ustva {
   #
   ############################################
 
-  if ($form->{method} eq 'cash') {
+  if ($form->{accounting_method} eq 'cash') {
 
     $query = qq|
        SELECT
@@ -836,16 +701,16 @@ sub get_accounts_ustva {
               1=1
               $ARwhere
               AND acc.trans_id = ac.trans_id
-              )
-           /
+              )           /
            (
             SELECT amount FROM ar WHERE id = ac.trans_id
            )
          ) AS amount,
-         tk.pos_ustva
+         tk.pos_ustva,  t.rate, c.accno
        FROM acc_trans ac
        LEFT JOIN chart c ON (c.id  = ac.chart_id)
        LEFT JOIN ar      ON (ar.id = ac.trans_id)
+       LEFT JOIN tax t   ON (t.id = ac.tax_id)
        LEFT JOIN taxkeys tk ON (
          tk.id = (
            SELECT id FROM taxkeys
@@ -857,10 +722,10 @@ sub get_accounts_ustva {
        )
        WHERE
        $acc_trans_where
-       GROUP BY tk.pos_ustva
+       GROUP BY tk.pos_ustva, t.rate, c.accno
     |;
 
-  } elsif ($form->{method} eq 'accrual') {
+  } elsif ($form->{accounting_method} eq 'accrual') {
     #########################################
     # Method eq 'accrual' = Soll Versteuerung
     #########################################
@@ -869,10 +734,11 @@ sub get_accounts_ustva {
        -- Alle Einnahmen AR und pos_ustva erfassen
        SELECT
          - sum(ac.amount) AS amount,
-         tk.pos_ustva
+         tk.pos_ustva, t.rate, c.accno
        FROM acc_trans ac
        JOIN chart c ON (c.id = ac.chart_id)
        JOIN ar ON (ar.id = ac.trans_id)
+       JOIN tax t ON (t.id = ac.tax_id)
        JOIN taxkeys tk ON (
          tk.id = (
            SELECT id FROM taxkeys
@@ -884,12 +750,12 @@ sub get_accounts_ustva {
        $dpt_join
        WHERE 1 = 1
        $where
-       GROUP BY tk.pos_ustva
+       GROUP BY tk.pos_ustva, t.rate, c.accno
   |;
 
   } else {
 
-    $form->error("Unknown tax method: $form->{method}")
+    $form->error("Unknown tax method: $form->{accounting_method}")
 
   }
 
@@ -902,10 +768,11 @@ sub get_accounts_ustva {
 
        SELECT
          sum(ac.amount) AS amount,
-         tk.pos_ustva
+         tk.pos_ustva, t.rate, c.accno
        FROM acc_trans ac
        JOIN ap ON (ap.id = ac.trans_id )
        JOIN chart c ON (c.id = ac.chart_id)
+       JOIN tax t ON (t.id = ac.tax_id)
        LEFT JOIN taxkeys tk ON (
            tk.id = (
              SELECT id FROM taxkeys
@@ -919,16 +786,17 @@ sub get_accounts_ustva {
        WHERE
        1=1
        $where
-       GROUP BY tk.pos_ustva
+       GROUP BY tk.pos_ustva, t.rate, c.accno
 
      UNION -- Einnahmen direkter gl Buchungen erfassen
 
        SELECT sum
          ( - ac.amount) AS amount,
-         tk.pos_ustva
+         tk.pos_ustva, t.rate, c.accno
        FROM acc_trans ac
        JOIN chart c ON (c.id = ac.chart_id)
        JOIN gl a ON (a.id = ac.trans_id)
+       JOIN tax t ON (t.id = ac.tax_id)
        LEFT JOIN taxkeys tk ON (
          tk.id = (
            SELECT id FROM taxkeys
@@ -942,17 +810,18 @@ sub get_accounts_ustva {
        $dpt_join
        WHERE 1 = 1
        $where
-       GROUP BY tk.pos_ustva
+       GROUP BY tk.pos_ustva, t.rate, c.accno
 
 
      UNION -- Ausgaben direkter gl Buchungen erfassen
 
        SELECT sum
          (ac.amount) AS amount,
-         tk.pos_ustva
+         tk.pos_ustva, t.rate, c.accno
        FROM acc_trans ac
        JOIN chart c ON (c.id = ac.chart_id)
        JOIN gl a ON (a.id = ac.trans_id)
+       JOIN tax t ON (t.id = ac.tax_id)
        LEFT JOIN taxkeys tk ON (
          tk.id = (
            SELECT id FROM taxkeys
@@ -966,14 +835,10 @@ sub get_accounts_ustva {
        $dpt_join
        WHERE 1 = 1
        $where
-       GROUP BY tk.pos_ustva
+       GROUP BY tk.pos_ustva, t.rate, c.accno
 
   |;
 
-  my @accno;
-  my $accno;
-  my $ref;
-
   # Show all $query in Debuglevel LXDebug::QUERY
   my $callingdetails = (caller (0))[3];
   $main::lxdebug->message(LXDebug->QUERY(), "$callingdetails \$query=\n $query");
@@ -981,11 +846,53 @@ sub get_accounts_ustva {
   my $sth = $dbh->prepare($query);
 
   $sth->execute || $form->dberror($query);
+  # ugly, but we need to use static accnos
+  my ($accno_five, $accno_sixteen, $corr);
+
+  if ($form->{coa} eq 'Germany-DATEV-SKR03EU') {
+    $accno_five     = 1773;
+    $accno_sixteen  = 1775;
+  } elsif (($form->{coa} eq 'Germany-DATEV-SKR04EU')) {
+    $accno_five     = 3803; # SKR04
+    $accno_sixteen  = 3805; # SKR04
+  } else {die "wrong call"; }
 
   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
-    # Bug 365 solved?!
+    next unless $ref->{$category};
+    $corr = 0;
     $ref->{amount} *= -1;
-    $form->{ $ref->{$category} } += $ref->{amount};
+    # USTVA Pos 35
+    if ($ref->{pos_ustva} eq '35') {
+      if ($ref->{rate} == 0.16) {
+        $form->{"pos_ustva_81b_kivi"} += $ref->{amount};
+      } elsif ($ref->{rate} == 0.05) {
+        $form->{"pos_ustva_86b_kivi"} += $ref->{amount};
+      } elsif ($ref->{rate} == 0.19) {
+        # pos_ustva says 16, but rate says 19
+        # (pos_ustva should be tax dependent and not taxkeys dependent)
+        # correction hotfix for this case:
+        # bookings exists with 19% ->
+        # move 19% bookings to the 19% position
+        # Dont rely on dates of taxkeys
+        $corr = 1;
+        $form->{"81"} += $ref->{amount};
+      }  elsif ($ref->{rate} == 0.07) {
+        # pos_ustva says 5, but rate says 7
+        # see comment above:
+        # Dont rely on dates of taxkeys
+        $corr = 1;
+        $form->{"86"} += $ref->{amount};
+      } else {die ("No valid tax rate for pos 35" . Dumper($ref)); }
+    }
+    # USTVA Pos 36 (Steuerkonten)
+    if ($ref->{pos_ustva} eq '36') {
+      if ($ref->{accno} =~ /^$accno_sixteen/) {
+        $form->{"pos_ustva_811b_kivi"} += $ref->{amount};
+      } elsif ($ref->{accno} =~ /^$accno_five/) {
+        $form->{"pos_ustva_861b_kivi"} += $ref->{amount};
+      } else { die ("No valid accno for pos 36" . Dumper($ref)); }
+    }
+  $form->{ $ref->{$category} } += $ref->{amount} unless $corr;
   }
 
   $sth->finish;
@@ -994,27 +901,173 @@ sub get_accounts_ustva {
 
 }
 
-sub get_config {
+sub set_FromTo {
   $main::lxdebug->enter_sub();
 
-  my ($self, $userspath, $filename) = @_;
+  my ($self, $form) = @_;
 
-  my $form = $main::form;
+  # init some form vars
+  my @anmeldungszeitraum =
+    qw('0401' '0402' '0403'
+       '0404' '0405' '0406'
+       '0407' '0408' '0409'
+       '0410' '0411' '0412'
+       '0441' '0442' '0443' '0444');
 
-  $form->error("Missing Parameter: @_") if !$userspath || !$filename;
+  foreach my $item (@anmeldungszeitraum) {
+    $form->{$item} = "";
+  }
 
-  $filename = "$::myconfig{login}_$filename";
-  $filename =~ s|.*/||;
-  $filename = "$userspath/$filename";
-  open my $FACONF, "<", $filename or do {# Annon Sub
-    # catch open error
-    # create file if file does not exist
-    open my $FANEW, ">", $filename  or $form->error("CREATE: $filename : $!");
-    close $FANEW                    or $form->error("CLOSE: $filename : $!");
+  #forgotten the year --> thisyear
+  if ($form->{year} !~ m/^\d\d\d\d$/) {
+      $form->{year} = substr(
+          $form->datetonum(
+              $form->current_date(\%::myconfig), \%::myconfig
+          ),
+          0, 4);
+      $::lxdebug->message(LXDebug->DEBUG1,
+                          qq|Actual year from Database: $form->{year}\n|);
+  }
 
-    #try again open file
-    open my $FACONF, "<", $filename or $form->error("OPEN: $filename : $!");
-  };
+  #
+  # using dates in ISO-8601 format: yyyymmmdd  for Postgres...
+  #
+
+  #yearly report
+  if ($form->{period} eq "13") {
+      $form->{fromdate} = "$form->{year}0101";
+      $form->{todate}   = "$form->{year}1231";
+  }
+
+  #quarter reports
+  if ($form->{period} eq "41") {
+      $form->{fromdate} = "$form->{year}0101";
+      $form->{todate}   = "$form->{year}0331";
+      $form->{'0441'}   = "X";
+  }
+  if ($form->{period} eq "42") {
+      $form->{fromdate} = "$form->{year}0401";
+      $form->{todate}   = "$form->{year}0630";
+      $form->{'0442'}   = "X";
+  }
+  if ($form->{period} eq "43") {
+      $form->{fromdate} = "$form->{year}0701";
+      $form->{todate}   = "$form->{year}0930";
+      $form->{'0443'}   = "X";
+  }
+  if ($form->{period} eq "44") {
+      $form->{fromdate} = "$form->{year}1001";
+      $form->{todate}   = "$form->{year}1231";
+      $form->{'0444'}   = "X";
+  }
+
+   #Monthly reports
+  SWITCH: {
+      $form->{period} eq "01" && do {
+        $form->{fromdate} = "$form->{year}0101";
+        $form->{todate}   = "$form->{year}0131";
+        $form->{'0401'}   = "X";
+        last SWITCH;
+      };
+      $form->{period} eq "02" && do {
+        $form->{fromdate} = "$form->{year}0201";
+
+        #this works from 1901 to 2099, 1900 and 2100 fail.
+        my $leap = ($form->{year} % 4 == 0) ? "29" : "28";
+        $form->{todate} = "$form->{year}02$leap";
+        $form->{"0402"} = "X";
+        last SWITCH;
+      };
+      $form->{period} eq "03" && do {
+        $form->{fromdate} = "$form->{year}0301";
+        $form->{todate}   = "$form->{year}0331";
+        $form->{"0403"}   = "X";
+        last SWITCH;
+      };
+      $form->{period} eq "04" && do {
+        $form->{fromdate} = "$form->{year}0401";
+        $form->{todate}   = "$form->{year}0430";
+        $form->{"0404"}   = "X";
+        last SWITCH;
+      };
+      $form->{period} eq "05" && do {
+        $form->{fromdate} = "$form->{year}0501";
+        $form->{todate}   = "$form->{year}0531";
+        $form->{"0405"}   = "X";
+        last SWITCH;
+      };
+      $form->{period} eq "06" && do {
+        $form->{fromdate} = "$form->{year}0601";
+        $form->{todate}   = "$form->{year}0630";
+        $form->{"0406"}   = "X";
+        last SWITCH;
+      };
+      $form->{period} eq "07" && do {
+        $form->{fromdate} = "$form->{year}0701";
+        $form->{todate}   = "$form->{year}0731";
+        $form->{"0407"}   = "X";
+        last SWITCH;
+      };
+      $form->{period} eq "08" && do {
+        $form->{fromdate} = "$form->{year}0801";
+        $form->{todate}   = "$form->{year}0831";
+        $form->{"0408"}   = "X";
+        last SWITCH;
+      };
+      $form->{period} eq "09" && do {
+        $form->{fromdate} = "$form->{year}0901";
+        $form->{todate}   = "$form->{year}0930";
+        $form->{"0409"}   = "X";
+        last SWITCH;
+      };
+      $form->{period} eq "10" && do {
+        $form->{fromdate} = "$form->{year}1001";
+        $form->{todate}   = "$form->{year}1031";
+        $form->{"0410"}   = "X";
+        last SWITCH;
+      };
+      $form->{period} eq "11" && do {
+        $form->{fromdate} = "$form->{year}1101";
+        $form->{todate}   = "$form->{year}1130";
+        $form->{"0411"}   = "X";
+        last SWITCH;
+      };
+      $form->{period} eq "12" && do {
+        $form->{fromdate} = "$form->{year}1201";
+        $form->{todate}   = "$form->{year}1231";
+        $form->{"0412"}   = "X";
+        last SWITCH;
+      };
+    }
+
+  # Kontrollvariablen für die Templates
+  $form->{"year$_"} = ($form->{year} >= $_ ) ? "1":"0" for 2007..2107;
+
+  $main::lxdebug->leave_sub();
+}
+
+sub get_fiamt_vars {
+    return @fiamt_finanzamt;
+}
+
+sub get_oldconfig {
+  $main::lxdebug->enter_sub();
+
+  my $ret = 0;
+  my %oldkeys = (
+      'steuernummer' => 'taxnumber',
+      'elsterFFFF' => 'fa_bufa_nr',
+      'FA_dauerfrist' => 'fa_dauerfrist',
+      'FA_steuerberater_city' => 'fa_steuerberater_city',
+      'FA_steuerberater_name' => 'fa_steuerberater_name',
+      'FA_steuerberater_street' => 'fa_steuerberater_street',
+      'FA_steuerberater_tel' => 'fa_steuerberater_tel',
+      'FA_voranmeld' => 'fa_voranmeld',
+      );
+
+  my $filename = $::lx_office_conf{paths}{userspath}."/finanzamt.ini";
+  my $FACONF;
+  return unless (open( $FACONF, "<", $filename));
 
   while (<$FACONF>) {
     last if (/^\[/);
@@ -1027,14 +1080,61 @@ sub get_config {
     s/^\s*(.*?)\s*$/$1/;
     my ($key, $value) = split(/=/, $_, 2);
 
-    $form->{$key} = "$value";
-
+    $main::lxdebug->message(LXDebug->DEBUG2(), "oldkey: ".$key." val=".$value." newkey=".
+                          $oldkeys{$key}." oval=".$::form->{$oldkeys{$key}});
+    if ( $oldkeys{$key} && $::form->{$oldkeys{$key}} eq '' ) {
+        $::form->{$oldkeys{$key}} = $::locale->{iconv_utf8}->convert($value);
+        $main::lxdebug->message(LXDebug->DEBUG2(), "set ".$oldkeys{$key}."=".$::form->{$oldkeys{$key}});
+        $ret = 1;
+    }
   }
+  $main::lxdebug->leave_sub();
+  return $ret;
+}
+
+sub get_config {
+    $main::lxdebug->enter_sub();
+    my $defaults   = SL::DB::Default->get;
+    my @rd_config =  @fiamt_config;
+    push @rd_config ,qw(accounting_method coa company address co_ustid duns);
+    $::form->{$_} = $defaults->$_ for @rd_config;
+
+    if ( $::form->{taxnumber} eq '' || $::form->{fa_bufa_nr} eq '') {
+        #alte finanzamt.ini lesen, ggf abspeichern
+        if ( get_oldconfig() ) {
+            get_finanzamt();
+            save_config();
+        }
+    }
 
-  close $FACONF;
+    my $coa = $::form->{coa};
+    $::form->{"COA_$coa"} = '1';
+    $::form->{COA_Germany} = '1' if ($coa =~ m/^germany/i);
+    $main::lxdebug->leave_sub();
+}
 
-  $main::lxdebug->leave_sub();
+sub get_finanzamt {
+    $main::lxdebug->enter_sub();
+    if ( $::form->{fa_bufa_nr} && $::form->{fa_bufa_nr} ne '' ) {
+        my $fiamt =  SL::DB::Finanzamt->_get_manager_class->get_first(
+                 query => [ fa_bufa_nr => $::form->{fa_bufa_nr} ]);
+        $::form->{$_} = $fiamt->$_ for @fiamt_finanzamt;
+    }
+    $main::lxdebug->leave_sub();
 }
 
+sub save_config {
+    $main::lxdebug->enter_sub();
+    my $defaults  = SL::DB::Default->get;
+    $defaults->$_($::form->{$_}) for @fiamt_config;
+    $defaults->save;
+    if ( $defaults->fa_bufa_nr ) {
+        my $fiamt =  SL::DB::Finanzamt->_get_manager_class->get_first(
+                 query => [ fa_bufa_nr => $defaults->fa_bufa_nr ]);
+        $fiamt->$_($::form->{$_}) for @fiamt_finanzamt;
+        $fiamt->save;
+    }
+    $main::lxdebug->leave_sub();
+}
 
 1;
index 44efe5c..68fd2c5 100644 (file)
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #=====================================================================
 #
 # user related functions
@@ -35,8 +36,9 @@
 package User;
 
 use IO::File;
-use Fcntl qw(:seek);
+use List::MoreUtils qw(any);
 
+use SL::DB;
 #use SL::Auth;
 use SL::DB::AuthClient;
 use SL::DB::Employee;
@@ -46,6 +48,7 @@ use SL::DBUtils;
 use SL::Iconv;
 use SL::Inifile;
 use SL::System::InstallationLock;
+use SL::DefaultManager;
 
 use strict;
 
@@ -100,6 +103,42 @@ sub country_codes {
   return %cc;
 }
 
+sub _handle_superuser_privileges {
+  my ($self, $form) = @_;
+
+  if ($form->{database_superuser_username}) {
+    $::auth->set_session_value("database_superuser_username" => $form->{database_superuser_username}, "database_superuser_password" => $form->{database_superuser_password});
+  }
+
+  my %dbconnect_form          = %{ $form };
+  my ($su_user, $su_password) = map { $::auth->get_session_value("database_superuser_$_") } qw(username password);
+
+  if ($su_user) {
+    $dbconnect_form{dbuser}   = $su_user;
+    $dbconnect_form{dbpasswd} = $su_password;
+  }
+
+  dbconnect_vars(\%dbconnect_form, $form->{dbname});
+
+  my %result = (
+    username => $dbconnect_form{dbuser},
+    password => $dbconnect_form{dbpasswd},
+  );
+
+  $::auth->set_session_value("database_superuser_username" => $dbconnect_form{dbuser}, "database_superuser_password" => $dbconnect_form{dbpasswd});
+
+  my $dbh = SL::DBConnect->connect($dbconnect_form{dbconnect}, $dbconnect_form{dbuser}, $dbconnect_form{dbpasswd}, SL::DBConnect->get_options);
+  return (%result, error => $::locale->text('The credentials (username & password) for connecting database are wrong.')) if !$dbh;
+
+  my $is_superuser = SL::DBUtils::role_is_superuser($dbh, $dbconnect_form{dbuser});
+
+  $dbh->disconnect;
+
+  return (%result, have_privileges => 1) if $is_superuser;
+  return (%result)                       if !$su_user; # no error message if credentials weren't set by the user
+  return (%result, error => $::locale->text('The database user \'#1\' does not have superuser privileges.', $dbconnect_form{dbuser}));
+}
+
 sub login {
   my ($self, $form) = @_;
 
@@ -112,7 +151,7 @@ sub login {
   return LOGIN_AUTH_DBUPDATE_AVAILABLE() if $dbupdater_auth->unapplied_upgrade_scripts($::auth->dbconnect);
 
   # check if database is down
-  my $dbh = $form->dbconnect_noauto;
+  my $dbh = SL::DB->client->dbh;
 
   # we got a connection, check the version
   my ($dbversion) = $dbh->selectrow_array(qq|SELECT version FROM defaults|);
@@ -125,13 +164,18 @@ sub login {
 
   my $dbupdater        = SL::DBUpgrade2->new(form => $form)->parse_dbupdate_controls;
   my @unapplied_scripts = $dbupdater->unapplied_upgrade_scripts($dbh);
-  $dbh->disconnect;
+  $dbh->disconnect;
 
   if (!@unapplied_scripts) {
     SL::DB::Manager::Employee->update_entries_for_authorized_users;
     return LOGIN_OK();
   }
 
+  # Store the fact that we're applying database upgrades at the
+  # moment. That way functions called from the layout modules that may
+  # require updated tables can chose only to use basic features.
+  $::request->applying_database_upgrades(1);
+
   $form->{$_} = $::auth->client->{$_} for qw(dbname dbhost dbport dbuser dbpasswd);
   $form->{$_} = $myconfig{$_}         for qw(datestyle);
 
@@ -141,9 +185,23 @@ sub login {
 
   $form->{dbupdate} = "db" . $::auth->client->{dbname};
 
-  if ($form->{"show_dbupdate_warning"}) {
-    print $form->parse_html_template("dbupgrade/warning", { unapplied_scripts => \@unapplied_scripts });
-    ::end_of_request();
+  my $show_update_warning = $form->{"show_dbupdate_warning"};
+  my %superuser           = (need_privileges => (any { $_->{superuser_privileges} } @unapplied_scripts));
+
+  if ($superuser{need_privileges}) {
+    %superuser = (
+      %superuser,
+      $self->_handle_superuser_privileges($form),
+    );
+    $show_update_warning = 1 if !$superuser{have_privileges};
+  }
+
+  if ($show_update_warning) {
+    print $form->parse_html_template("dbupgrade/warning", {
+      unapplied_scripts => \@unapplied_scripts,
+      superuser         => \%superuser,
+    });
+    $::dispatcher->end_request;
   }
 
   # update the tables
@@ -158,7 +216,7 @@ sub login {
   # If $self->dbupdate2 returns than this means all upgrade scripts
   # have been applied successfully, none required user
   # interaction. Otherwise the deeper layers would have called
-  # ::end_of_request() already, and return would not have returned to
+  # $::dispatcher->end_request already, and return would not have returned to
   # us. Therefore we can now use RDBO instances because their supposed
   # table structures do match the actual structures. So let's ensure
   # that the "employee" table contains the appropriate entries for all
@@ -276,18 +334,60 @@ sub dbcreate {
 
   &dbconnect_vars($form, $form->{db});
 
+  # make a shim myconfig so that rose db connections work
+  $::myconfig{$_}     = $form->{$_} for qw(dbhost dbport dbuser dbpasswd);
+  $::myconfig{dbname} = $form->{db};
+
   $dbh = SL::DBConnect->connect($form->{dbconnect}, $form->{dbuser}, $form->{dbpasswd}, SL::DBConnect->get_options)
     or $form->dberror;
 
-  my $dbupdater = SL::DBUpgrade2->new(form => $form);
+  my $dbupdater = SL::DBUpgrade2->new(form => $form, return_on_error => 1, silent => 1)->parse_dbupdate_controls;
   # create the tables
   $dbupdater->process_query($dbh, "sql/lx-office.sql");
-
-  # load chart of accounts
   $dbupdater->process_query($dbh, "sql/$form->{chart}-chart.sql");
 
-  $query = qq|UPDATE defaults SET coa = ?, accounting_method = ?, profit_determination = ?, inventory_system = ?, curr = ?|;
-  do_query($form, $dbh, $query, map { $form->{$_} } qw(chart accounting_method profit_determination inventory_system defaultcurrency));
+  $query = qq|UPDATE defaults SET coa = ?|;
+  do_query($form, $dbh, $query, map { $form->{$_} } qw(chart));
+
+  $dbh->disconnect;
+
+  # update new database
+  $self->dbupdate2(form => $form, updater => $dbupdater, database => $form->{db}, silent => 1);
+
+  $dbh = SL::DBConnect->connect($form->{dbconnect}, $form->{dbuser}, $form->{dbpasswd}, SL::DBConnect->get_options)
+    or $form->dberror;
+
+  $query = "SELECT * FROM currencies WHERE name = ?";
+  my $curr = selectfirst_hashref_query($form, $dbh, $query, $form->{defaultcurrency});
+  if (!$curr->{id}) {
+    do_query($form, $dbh, "INSERT INTO currencies (name) VALUES (?)", $form->{defaultcurrency});
+    $curr = selectfirst_hashref_query($form, $dbh, $query, $form->{defaultcurrency});
+  }
+
+  $query = qq|UPDATE defaults SET
+    accounting_method = ?,
+    profit_determination = ?,
+    inventory_system = ?,
+    precision = ?,
+    currency_id = ?,
+    feature_balance = ?,
+    feature_datev = ?,
+    feature_erfolgsrechnung = ?,
+    feature_eurechnung = ?,
+    feature_ustva = ?
+  |;
+  do_query($form, $dbh, $query,
+    $form->{accounting_method},
+    $form->{profit_determination},
+    $form->{inventory_system},
+    $form->parse_amount(\%::myconfig, $form->{precision_as_number}),
+    $curr->{id},
+    $form->{feature_balance},
+    $form->{feature_datev},
+    $form->{feature_erfolgsrechnung},
+    $form->{feature_eurechnung},
+    $form->{feature_ustva}
+  );
 
   $dbh->disconnect;
 
@@ -379,14 +479,12 @@ sub dbupdate2 {
   my $form            = $params{form};
   my $dbupdater       = $params{updater};
   my $db              = $params{database};
+  my $silent          = $params{silent};
 
   map { $_->{description} = SL::Iconv::convert($_->{charset}, 'UTF-8', $_->{description}) } values %{ $dbupdater->{all_controls} };
 
   &dbconnect_vars($form, $db);
 
-  # Flush potentially held database locks.
-  $form->get_standard_dbh->commit;
-
   my $dbh = SL::DBConnect->connect($form->{dbconnect}, $form->{dbuser}, $form->{dbpasswd}, SL::DBConnect->get_options) or $form->dberror;
 
   $dbh->do($form->{dboptions}) if ($form->{dboptions});
@@ -394,18 +492,40 @@ sub dbupdate2 {
   $self->create_schema_info_table($form, $dbh);
 
   my @upgradescripts = $dbupdater->unapplied_upgrade_scripts($dbh);
+  my $need_superuser = (any { $_->{superuser_privileges} } @upgradescripts);
+  my $superuser_dbh;
+
+  if ($need_superuser) {
+    my %dbconnect_form = (
+      %{ $form },
+      dbuser   => $::auth->get_session_value("database_superuser_username"),
+      dbpasswd => $::auth->get_session_value("database_superuser_password"),
+    );
+
+    if ($dbconnect_form{dbuser} ne $form->{dbuser}) {
+      dbconnect_vars(\%dbconnect_form, $db);
+      $superuser_dbh = SL::DBConnect->connect($dbconnect_form{dbconnect}, $dbconnect_form{dbuser}, $dbconnect_form{dbpasswd}, SL::DBConnect->get_options) or $form->dberror;
+    }
+  }
+
+  $::lxdebug->log_time("DB upgrades commencing");
 
   foreach my $control (@upgradescripts) {
     # Apply upgrade. Control will only return to us if the upgrade has
     # been applied correctly and if the update has not requested user
     # interaction.
-    $main::lxdebug->message(LXDebug->DEBUG2(), "Applying Update $control->{file}");
-    print $form->parse_html_template("dbupgrade/upgrade_message2", $control);
+    my $script_dbh = $control->{superuser_privileges} ? ($superuser_dbh // $dbh) : $dbh;
 
-    $dbupdater->process_file($dbh, "sql/Pg-upgrade2/$control->{file}", $control);
+    $::lxdebug->message(LXDebug->DEBUG2(), "Applying Update $control->{file}" . ($control->{superuser_privileges} ? " with superuser privileges" : ""));
+    print $form->parse_html_template("dbupgrade/upgrade_message2", $control) unless $silent;
+
+    $dbupdater->process_file($script_dbh, "sql/Pg-upgrade2/$control->{file}", $control);
   }
 
+  $::lxdebug->log_time("DB upgrades finished");
+
   $dbh->disconnect;
+  $superuser_dbh->disconnect if $superuser_dbh;
 }
 
 sub data {
@@ -414,14 +534,15 @@ sub data {
 
 sub get_default_myconfig {
   my ($self_or_class, %user_config) = @_;
+  my $defaults = SL::DefaultManager->new($::lx_office_conf{system}->{default_manager});
 
   return (
-    countrycode  => 'de',
+    countrycode  => $defaults->language('de'),
     css_path     => 'css',      # Needed for menunew, see SL::Layout::Base::get_stylesheet_for_user
-    dateformat   => 'dd.mm.yy',
-    numberformat => '1.000,00',
-    stylesheet   => 'kivitendo.css',
-    timeformat   => 'hh:mm',
+    dateformat   => $defaults->dateformat('dd.mm.yy'),
+    numberformat => $defaults->numberformat('1.000,00'),
+    stylesheet   => $defaults->stylesheet('kivitendo.css'),
+    timeformat   => $defaults->timeformat('hh:mm'),
     %user_config,
   );
 }
index 2fdd9c1..bf2e9ff 100644 (file)
@@ -6,7 +6,7 @@ use parent qw(Exporter);
 
 use Carp;
 
-our @EXPORT_OK = qw(_hashify camelify snakify);
+our @EXPORT_OK = qw(_hashify camelify snakify trim);
 
 sub _hashify {
   my $keep = shift;
@@ -31,6 +31,13 @@ sub snakify {
   lc $str;
 }
 
+sub trim {
+  my $value = shift;
+  $value    =~ s{^ \p{WSpace}+ }{}xg if defined($value);
+  $value    =~ s{ \p{WSpace}+ $}{}xg if defined($value);
+  return $value;
+}
+
 1;
 __END__
 
@@ -90,6 +97,14 @@ return C<even_worse_example>.
 
 L</camilify> does the reverse.
 
+=item C<trim $string>
+
+Removes all leading and trailing whitespaces from C<$string> and
+returns it. Whitespaces within the string won't be changed.
+
+This function considers everything matching the Unicode character
+property "Whitespace" (C<WSpace>) to be a whitespace.
+
 =back
 
 =head1 BUGS
diff --git a/SL/VATIDNr.pm b/SL/VATIDNr.pm
new file mode 100644 (file)
index 0000000..dc3fbf5
--- /dev/null
@@ -0,0 +1,110 @@
+package SL::VATIDNr;
+
+use strict;
+use warnings;
+
+use Algorithm::CheckDigits;
+
+sub clean {
+  my ($class, $ustid) = @_;
+
+  $ustid //= '';
+  $ustid   =~ s{[[:space:].-]+}{}g;
+
+  return $ustid;
+}
+
+sub normalize {
+  my ($class, $ustid) = @_;
+
+  $ustid = $class->clean($ustid);
+
+  if ($ustid =~ m{^CHE(\d{3})(\d{3})(\d{3})$}) {
+    return sprintf('CHE-%s.%s.%s', $1, $2, $3);
+  }
+
+  return $ustid;
+}
+
+sub _validate_switzerland {
+  my ($ustid) = @_;
+
+  return $ustid =~ m{^CHE\d{9}$} ? 1 : 0;
+}
+
+sub _validate_european_union {
+  my ($ustid) = @_;
+
+  # 1. Two upper-case letters with the ISO 3166-1 Alpha-2 country code (exception: Greece uses EL instead of GR)
+  # 2. Up to twelve alphanumeric characters
+
+  return 0 unless $ustid =~ m{^(?:AT|BE|BG|CY|CZ|DE|DK|EE|EL|ES|FI|FR|GB|HR|HU|IE|IT|LT|LU|LV|MT|NL|PL|PT|RO|SE|SI|SK|SM)[[:alnum:]]{1,12}$};
+
+  my $algo_name = "ustid_" . lc(substr($ustid, 0, 2));
+  my $checker   = eval { CheckDigits($algo_name) };
+
+  return $checker->is_valid(substr($ustid, 2)) if $checker;
+  return 1;
+}
+
+sub validate {
+  my ($class, $ustid) = @_;
+
+  $ustid = $class->clean($ustid);
+
+  return _validate_switzerland($ustid) if $ustid =~ m{^CHE};
+  return _validate_european_union($ustid);
+}
+
+1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::VATIDNr - Helper routines for dealing with VAT ID numbers
+("Umsatzsteuer-Identifikationsnummern", "UStID-Nr" in German) and
+Switzerland's enterprise identification numbers (UIDs)
+
+=head1 SYNOPSIS
+
+    my $is_valid = SL::VATIDNr->validate($ustid);
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<clean> C<$ustid>
+
+Returns the number with all spaces, dashes & points removed.
+
+=item C<normalize> C<$ustid>
+
+Normalizes the given number to the format usually used in the country
+given by the country code at the start of the number
+(e.g. C<CHE-123.456.789> for a Swiss UID or DE123456789 for a German
+VATIDNr).
+
+=item C<validate> C<$ustid>
+
+Returns whether or not a number is valid. Depending on the country
+code at the start several tests are done including check digit
+validation.
+
+The number in question is first run through the L</clean> function and
+may therefore contain certain ignored characters.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
index f20414c..5a3db9c 100644 (file)
--- a/SL/VK.pm
+++ b/SL/VK.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Sold Items report
@@ -55,11 +56,12 @@ sub invoice_transactions {
   # so we extract both versions in our query and later overwrite the description in article mode
 
   my $query =
-    qq|SELECT ct.id as customerid, ct.name as customername,ct.customernumber,ct.country,ar.invnumber,ar.id,ar.transdate,p.partnumber,p.description as description, pg.partsgroup,i.parts_id,i.qty,i.price_factor,i.discount,i.description as invoice_description,i.lastcost,i.sellprice,i.fxsellprice,i.marge_total,i.marge_percent,i.unit,b.description as business,e.name as employee,e2.name as salesman, to_char(ar.transdate,'Month') as month, to_char(ar.transdate, 'YYYYMM') as nummonth, p.unit as parts_unit, p.weight, ar.taxincluded | .
-    qq|, COALESCE((SELECT e.buy FROM exchangerate e WHERE e.transdate = ar.transdate and ar.currency_id = e.currency_id),1) as exchangerate | .
+    qq|SELECT ct.id as customerid, ct.name as customername,ct.customernumber,ct.country,ar.invnumber,ar.shipvia,ar.id,ar.transdate,p.partnumber,p.description as description, pg.partsgroup,i.parts_id,i.qty,i.price_factor,i.discount,i.description as invoice_description,i.lastcost,i.sellprice,i.fxsellprice,i.marge_total,i.marge_percent,i.unit,b.description as business,e.name as employee,e2.name as salesman, to_char(ar.transdate,'Month') as month, to_char(ar.transdate, 'YYYYMM') as nummonth, p.unit as parts_unit, p.weight, ar.taxincluded | .
+    qq|, COALESCE(er.buy, 1) | .
     qq|FROM invoice i | .
-    qq|JOIN ar on (i.trans_id = ar.id) | .
+    qq|RIGHT JOIN ar on (i.trans_id = ar.id) | .
     qq|JOIN parts p on (i.parts_id = p.id) | .
+    qq|LEFT JOIN exchangerate er on (er.transdate = ar.transdate and ar.currency_id = er.currency_id) | .
     qq|LEFT JOIN partsgroup pg on (p.partsgroup_id = pg.id) | .
     qq|LEFT JOIN customer ct on (ct.id = ar.customer_id) | .
     qq|LEFT JOIN business b on (ct.business_id = b.id) | .
@@ -83,7 +85,7 @@ sub invoice_transactions {
   $where .= " AND i.assemblyitem is not true ";
 
   # filter allowed parameters for mainsort and subsort as passed by POST
-  my @databasefields = qw(description customername country partsgroup business salesman month);
+  my @databasefields = qw(description customername country partsgroup business salesman month shipvia);
   my ($mainsort) = grep { /^$form->{mainsort}$/ } @databasefields;
   my ($subsort) = grep { /^$form->{subsort}$/ } @databasefields;
   die "illegal parameter for mainsort or subsort" unless $mainsort and $subsort;
@@ -112,7 +114,7 @@ sub invoice_transactions {
     push(@values, $form->{customer_id});
   } elsif ($form->{customer}) {
     $where .= " AND ct.name ILIKE ?";
-    push(@values, $form->like($form->{customer}));
+    push(@values, like($form->{customer}));
   }
   if ($form->{customernumber}) {
     $where .= qq| AND ct.customernumber = ? |;
@@ -120,7 +122,7 @@ sub invoice_transactions {
   }
   if ($form->{partnumber}) {
     $where .= qq| AND (p.partnumber ILIKE ?)|;
-    push(@values, '%' . $form->{partnumber} . '%');
+    push(@values, like($form->{partnumber}));
   }
   if ($form->{partsgroup_id}) {
     $where .= qq| AND (pg.id = ?)|;
@@ -128,7 +130,7 @@ sub invoice_transactions {
   }
   if ($form->{country}) {
     $where .= qq| AND (ct.country ILIKE ?)|;
-    push(@values, '%' . $form->{country} . '%');
+    push(@values, like($form->{country}));
   }
 
   # when filtering for parts by description we probably want to filter by the description of the part as per the master data
@@ -136,7 +138,7 @@ sub invoice_transactions {
   # at least in the translation case we probably want the report to also include translated articles, so we have to filter via parts.description
   if ($form->{description}) {
     $where .= qq| AND (p.description ILIKE ?)|;
-    push(@values, '%' . $form->{description} . '%');
+    push(@values, like($form->{description}));
   }
   if ($form->{transdatefrom}) {
     $where .= " AND ar.transdate >= ?";
@@ -146,10 +148,9 @@ sub invoice_transactions {
     $where .= " AND ar.transdate <= ?";
     push(@values, $form->{transdateto});
   }
-  if ($form->{department}) {
-    my ($null, $department_id) = split /--/, $form->{department};
+  if ($form->{department_id}) {
     $where .= " AND ar.department_id = ?";
-    push(@values, $department_id);
+    push @values, conv_i($form->{department_id});
   }
   if ($form->{employee_id}) {
     $where .= " AND ar.employee_id = ?";
diff --git a/SL/Version.pm b/SL/Version.pm
new file mode 100644 (file)
index 0000000..f515721
--- /dev/null
@@ -0,0 +1,82 @@
+package SL::Version;
+
+use strict;
+
+our $instance;
+
+sub new {
+  bless \my $instace, __PACKAGE__;
+}
+
+sub get_instance {
+  $instance //= $_[0]->new;
+}
+
+sub get_version {
+  $$instance //= do {
+    open my $version_file, '<', "VERSION" or die 'can not open VERSION file';
+    my $version = <$version_file>;
+    close $version_file;
+
+    if ( -f "BUILD" ) {
+      open my $build_file, '<', "BUILD" or die 'can not open BUILD file';
+      my $build =  <$build_file>;
+      close $build_file;
+      $version .= '-' . $build;
+    }
+
+    # only allow numbers, letters, points, underscores and dashes. Prevents injecting of malicious code.
+    $version =~ s/[^0-9A-Za-z\.\_\-]//g;
+
+    $version;
+  }
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Version
+
+=head1 SYNOPSIS
+
+  use SL::Version;
+
+  my $version = SL::Version->get_version
+
+=head1 DESCRIPTION
+
+This module is a singleton for the sole reason that SL::Form doesn't have to
+cache the version.
+
+=head1 FUNCTIONS
+
+=head2 C<new>
+
+Creates a new object. Should never be called.
+
+=head2 C<get_instance>
+
+Creates a singleton instance if none exists and returns.
+
+=head2 C<get_version>
+
+Parses the version from the C<VERSION> file.
+
+If the file C<BUILD> exists, appends its contents as a build number.
+
+Returns a sanitized version string.
+
+=head1 BUGS
+
+None yet :)
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut
index 8ff63f2..52e174f 100644 (file)
--- a/SL/WH.pm
+++ b/SL/WH.pm
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 #  Warehouse module
 
 package WH;
 
+use Carp qw(croak);
+
 use SL::AM;
 use SL::DBUtils;
+use SL::DB::Inventory;
 use SL::Form;
-
-use SL::DB::Unit;
-use SL::DB::Assembly;
+use SL::Locale::String qw(t8);
+use SL::Util qw(trim);
 
 use warnings;
 use strict;
@@ -57,9 +60,8 @@ sub transfer {
   require SL::DB::TransferType;
   require SL::DB::Part;
   require SL::DB::Employee;
-  require SL::DB::Inventory;
 
-  my $employee   = SL::DB::Manager::Employee->find_by(login => $::myconfig{login});
+  my $employee   = SL::DB::Manager::Employee->current;
   my ($now)      = selectrow_query($::form, $::form->get_standard_dbh, qq|SELECT current_date|);
   my @directions = (undef, qw(out in transfer));
 
@@ -81,7 +83,8 @@ sub transfer {
   my $db = SL::DB::Inventory->new->db;
   $db->with_transaction(sub{
     while (my $transfer = shift @args) {
-      my ($trans_id) = selectrow_query($::form, $::form->get_standard_dbh, qq|SELECT nextval('id')|);
+      my $trans_id;
+      ($trans_id) = selectrow_query($::form, $::form->get_standard_dbh, qq|SELECT nextval('id')|) if $transfer->{qty};
 
       my $part          = $objectify->($transfer, 'parts',         'SL::DB::Part');
       my $unit          = $objectify->($transfer, 'unit',          'SL::DB::Unit',         name => $transfer->{unit});
@@ -99,13 +102,21 @@ sub transfer {
       $direction |= 1 if $src_bin;
       $direction |= 2 if $dst_bin;
 
-      my $transfer_type = $objectify->($transfer, 'transfer_type', 'SL::DB::TransferType', direction   => $directions[$direction],
-                                                                                           description => $transfer->{transfer_type});
+      my $transfer_type_id;
+      if ($transfer->{transfer_type_id}) {
+        $transfer_type_id = $transfer->{transfer_type_id};
+      } else {
+        my $transfer_type = $objectify->($transfer, 'transfer_type', 'SL::DB::TransferType', direction   => $directions[$direction],
+                                                                                             description => $transfer->{transfer_type});
+        $transfer_type_id = $transfer_type->id;
+      }
+
+      my $stocktaking_qty = $transfer->{stocktaking_qty};
 
       my %params = (
           part             => $part,
           employee         => $employee,
-          trans_type       => $transfer_type,
+          trans_type_id    => $transfer_type_id,
           project          => $project,
           trans_id         => $trans_id,
           shippingdate     => !$transfer->{shippingdate} || $transfer->{shippingdate} eq 'current_date'
@@ -114,13 +125,15 @@ sub transfer {
       );
 
       if ($unit) {
-        $qty = $unit->convert_to($qty, $part->unit_obj);
+        $qty             = $unit->convert_to($qty,             $part->unit_obj);
+        $stocktaking_qty = $unit->convert_to($stocktaking_qty, $part->unit_obj);
       }
 
       $params{chargenumber} ||= '';
 
-      if ($direction & 1) {
-        SL::DB::Inventory->new(
+      my @inventories;
+      if ($qty && $direction & 1) {
+        push @inventories, SL::DB::Inventory->new(
           %params,
           warehouse => $src_wh,
           bin       => $src_bin,
@@ -128,8 +141,8 @@ sub transfer {
         )->save;
       }
 
-      if ($direction & 2) {
-        SL::DB::Inventory->new(
+      if ($qty && $direction & 2) {
+        push @inventories, SL::DB::Inventory->new(
           %params,
           warehouse => $dst_wh->id,
           bin       => $dst_bin->id,
@@ -141,6 +154,30 @@ sub transfer {
         }
       }
 
+      # Record stocktaking if requested.
+      # This is only possible if transfer was a stock in or stock out,
+      # but not both (transfer).
+      if ($transfer->{record_stocktaking}) {
+        die 'Stocktaking can only be recorded for stock in or stock out, but not on a transfer.' if scalar @inventories > 1;
+
+        my $inventory_id;
+        $inventory_id = $inventories[0]->id if $inventories[0];
+
+        SL::DB::Stocktaking->new(
+          inventory_id => $inventory_id,
+          warehouse    => $src_wh  || $dst_wh,
+          bin          => $src_bin || $dst_bin,
+          parts_id     => $part->id,
+          employee_id  => $employee->id,
+          qty          => $stocktaking_qty,
+          comment      => $transfer->{comment},
+          cutoff_date  => $transfer->{stocktaking_cutoff_date},
+          chargenumber => $transfer->{chargenumber},
+          bestbefore   => $transfer->{bestbefore},
+        )->save;
+
+      }
+
       push @trans_ids, $trans_id;
     }
 
@@ -154,152 +191,6 @@ sub transfer {
   return @trans_ids;
 }
 
-sub transfer_assembly {
-  $main::lxdebug->enter_sub();
-
-  my $self     = shift;
-  my %params   = @_;
-  Common::check_params(\%params, qw(assembly_id dst_warehouse_id login qty unit dst_bin_id chargenumber bestbefore comment));
-
-
-  my $unit = SL::DB::Manager::Unit->find_by(name => $params{unit});
-  if ($unit) {
-    my $assembly = SL::DB::Manager::Assembly->get_all(
-      query => [ id => $params{assembly_id} ],
-      with_objects => ['part'],
-      limit => 1,
-    )->[0];
-    $params{qty} = $unit->convert_to($params{qty}, $assembly->part->unit_obj);
-  }
-
-#  my $maxcreate=WH->check_assembly_max_create(assembly_id =>$params{'assembly_id'}, dbh => $my_dbh);
-
-  my $myconfig = \%main::myconfig;
-  my $form     = $main::form;
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
-
-
-  # Ablauferklärung
-  #
-  # ... Standard-Check oben Ende. Hier die eigentliche SQL-Abfrage
-  # select parts_id,qty from assembly where id=1064;
-  # Erweiterung für bug 935 am 23.4.09 -
-  # Erzeugnisse können Dienstleistungen enthalten, die ja nicht 'lagerbar' sind.
-  # select parts_id,qty from assembly inner join parts on assembly.parts_id = parts.id
-  # where assembly.id=1066 and inventory_accno_id IS NOT NULL;
-  #
-  # Erweiterung für bug 23.4.09 -2 Erzeugnisse in Erzeugnissen können nicht ausgelagert werden,
-  # wenn assembly nicht überprüft wird ...
-  # patch von joachim eingespielt 24.4.2009:
-  # my $query    = qq|select parts_id,qty from assembly inner join parts
-  # on assembly.parts_id = parts.id  where assembly.id = ? and
-  # (inventory_accno_id IS NOT NULL or parts.assembly = TRUE)|;
-
-
-  my $query = qq|select parts_id,qty from assembly inner join parts on assembly.parts_id = parts.id
-                  where assembly.id = ? and (inventory_accno_id IS NOT NULL or parts.assembly = TRUE)|;
-
-  my $sth_part_qty_assembly = prepare_execute_query($form, $dbh, $query, $params{assembly_id});
-
-  # Hier wird das prepared Statement für die Schleife über alle Lagerplätze vorbereitet
-  my $transferPartSQL = qq|INSERT INTO inventory (parts_id, warehouse_id, bin_id, chargenumber, bestbefore, comment, employee_id, qty, trans_id, trans_type_id)
-                           VALUES (?, ?, ?, ?, ?, ?, (SELECT id FROM employee WHERE login = ?), ?, nextval('id'),
-                           (SELECT id FROM transfer_type WHERE direction = 'out' AND description = 'used'))|;
-  my $sthTransferPartSQL   = prepare_query($form, $dbh, $transferPartSQL);
-
-  # der return-string für die fehlermeldung inkl. welche waren zum fertigen noch fehlen
-
-  my $kannNichtFertigen ="";  # Falls leer dann erfolgreich
-  my $schleife_durchlaufen=0; # Falls die Schleife nicht ausgeführt wird -> Keine Einzelteile definiert. Bessere Idee? jan
-  while (my $hash_ref = $sth_part_qty_assembly->fetchrow_hashref()) { #Schleife für select parts_id,(...) from assembly
-    $schleife_durchlaufen=1;  # Erzeugnis definiert
-    my $partsQTY = $hash_ref->{qty} * $params{qty}; # benötigte teile * anzahl erzeugnisse
-    my $currentPart_ID = $hash_ref->{parts_id};
-
-    # Überprüfen, ob diese Anzahl gefertigt werden kann
-    my $max_parts = $self->get_max_qty_parts(parts_id => $currentPart_ID, # $self->method() == this.method()
-                                             warehouse_id => $params{dst_warehouse_id});
-
-    if ($partsQTY  > $max_parts){
-      # Gibt es hier ein Problem mit nicht "escapten" Zeichen?
-      # 25.4.09 Antwort: Ja.  Aber erst wenn im Frontend die locales-Funktion aufgerufen wird
-
-      $kannNichtFertigen .= "Zum Fertigen fehlen:" . abs($partsQTY - $max_parts) .
-                            " Einheiten der Ware:" . $self->get_part_description(parts_id => $currentPart_ID) .
-                            ", um das Erzeugnis herzustellen. <br>"; # Konnte die Menge nicht mit der aktuellen Anzahl der Waren fertigen
-      next; # die weiteren Überprüfungen sind unnötig, daher das nächste elemente prüfen (genaue Ausgabe, was noch fehlt)
-    }
-
-    # Eine kurze Vorabfrage, um den Lagerplatz, Chargennummer und die Mindesthaltbarkeit zu bestimmen
-    # Offen: Die Summe über alle Lagerplätze wird noch nicht gebildet
-    # Gelöst: Wir haben vorher schon die Abfrage durchgeführt, ob wir fertigen können.
-    # Noch besser gelöst: Wir laufen durch alle benötigten Waren zum Fertigen und geben eine Rückmeldung an den Benutzer was noch fehlt
-    # und lösen den Rest dann so wie bei xplace im Barcode-Programm
-    # S.a. Kommentar im bin/mozilla-Code mb übernimmt und macht das in ordentlich
-
-    my $tempquery = qq|SELECT SUM(qty), bin_id, chargenumber, bestbefore   FROM inventory
-                       WHERE warehouse_id = ? AND parts_id = ?  GROUP BY bin_id, chargenumber, bestbefore having SUM(qty)>0|;
-    my $tempsth   = prepare_execute_query($form, $dbh, $tempquery, $params{dst_warehouse_id}, $currentPart_ID);
-
-    # Alle Werte zu dem einzelnen Artikel, die wir später auslagern
-    my $tmpPartsQTY = $partsQTY;
-
-    while (my $temphash_ref = $tempsth->fetchrow_hashref()) {
-      my $temppart_bin_id       = $temphash_ref->{bin_id}; # kann man hier den quelllagerplatz beim verbauen angeben?
-      my $temppart_chargenumber = $temphash_ref->{chargenumber};
-      my $temppart_bestbefore   = conv_date($temphash_ref->{bestbefore});
-      my $temppart_qty          = $temphash_ref->{sum};
-
-      if ($tmpPartsQTY > $temppart_qty) {  # wir haben noch mehr waren zum wegbuchen.
-                                           # Wir buchen den kompletten Lagerplatzbestand und zählen die Hilfsvariable runter
-        $tmpPartsQTY = $tmpPartsQTY - $temppart_qty;
-        $temppart_qty = $temppart_qty * -1; # TODO beim analyiseren des sql-trace, war dieser wert positiv,
-                                            # wenn * -1 als berechnung in der parameter-übergabe angegeben wird.
-                                            # Dieser Wert IST und BLEIBT positiv!! Hilfe.
-                                            # Liegt das daran, dass dieser Wert aus einem SQL-Statement stammt?
-        do_statement($form, $sthTransferPartSQL, $transferPartSQL, $currentPart_ID, $params{dst_warehouse_id},
-                     $temppart_bin_id, $temppart_chargenumber, $temppart_bestbefore, 'Verbraucht für ' .
-                     $self->get_part_description(parts_id => $params{assembly_id}), $params{login}, $temppart_qty);
-
-        # hier ist noch ein fehler am besten mit definierten erzeugnissen debuggen 02/2009 jb
-        # idee: ausbuch algorithmus mit rekursion lösen und an- und abschaltbar machen
-        # das problem könnte sein, dass strict nicht an war und sth global eine andere zuweisung bekam
-        # auf jeden fall war der internal-server-error nach aktivierung von strict und warnings plus ein paar my-definitionen weg
-      } else { # okay, wir haben weniger oder gleich Waren die wir wegbuchen müssen, wir können also aufhören
-        $tmpPartsQTY *=-1;
-        do_statement($form, $sthTransferPartSQL, $transferPartSQL, $currentPart_ID, $params{dst_warehouse_id},
-                     $temppart_bin_id, $temppart_chargenumber, $temppart_bestbefore, 'Verbraucht für ' .
-                     $self->get_part_description(parts_id => $params{assembly_id}), $params{login}, $tmpPartsQTY);
-        last; # beendet die schleife (springt zum letzten element)
-      }
-    }  # ende while SELECT SUM(qty), bin_id, chargenumber, bestbefore   FROM inventory  WHERE warehouse_id
-  } #ende while select parts_id,qty from assembly where id = ?
-
-  if ($schleife_durchlaufen==0){  # falls die schleife nicht durchlaufen wurde, wurden auch
-                                  # keine einzelteile definiert
-      $kannNichtFertigen ="Für dieses Erzeugnis sind keine Einzelteile definiert.
-                           Dementsprechend kann auch nichts hergestellt werden";
- }
-  # gibt die Fehlermeldung zurück. A.) Keine Teile definiert
-  #                                B.) Artikel und Anzahl der fehlenden Teile/Dienstleistungen
-  if ($kannNichtFertigen) {
-    return $kannNichtFertigen;
-  }
-
-  # soweit alles gut. Jetzt noch die wirkliche Lagerbewegung für das Erzeugnis ausführen ...
-  my $transferAssemblySQL = qq|INSERT INTO inventory (parts_id, warehouse_id, bin_id, chargenumber, bestbefore,
-                                                      comment, employee_id, qty, trans_id, trans_type_id)
-                               VALUES (?, ?, ?, ?, ?, ?, (SELECT id FROM employee WHERE login = ?), ?, nextval('id'),
-                               (SELECT id FROM transfer_type WHERE direction = 'in' AND description = 'stock'))|;
-  my $sthTransferAssemblySQL   = prepare_query($form, $dbh, $transferAssemblySQL);
-  do_statement($form, $sthTransferAssemblySQL, $transferAssemblySQL, $params{assembly_id}, $params{dst_warehouse_id},
-               $params{dst_bin_id}, $params{chargenumber}, conv_date($params{bestbefore}), $params{comment}, $params{login}, $params{qty});
-  $dbh->commit();
-
-  $main::lxdebug->leave_sub();
-  return 1; # Alles erfolgreich
-}
-
 sub get_warehouse_journal {
   $main::lxdebug->enter_sub();
 
@@ -329,32 +220,37 @@ sub get_warehouse_journal {
 
   if ($filter{partnumber}) {
     push @filter_ary, "p.partnumber ILIKE ?";
-    push @filter_vars, '%' . $filter{partnumber} . '%';
+    push @filter_vars, like($filter{partnumber});
   }
 
   if ($filter{description}) {
     push @filter_ary, "(p.description ILIKE ?)";
-    push @filter_vars, '%' . $filter{description} . '%';
+    push @filter_vars, like($filter{description});
+  }
+
+  if ($filter{classification_id}) {
+    push @filter_ary, "p.classification_id = ?";
+    push @filter_vars, $filter{classification_id};
   }
 
   if ($filter{chargenumber}) {
     push @filter_ary, "i1.chargenumber ILIKE ?";
-    push @filter_vars, '%' . $filter{chargenumber} . '%';
+    push @filter_vars, like($filter{chargenumber});
   }
 
-  if ($form->{bestbefore}) {
+  if (trim($form->{bestbefore})) {
     push @filter_ary, "?::DATE = i1.bestbefore::DATE";
-    push @filter_vars, $form->{bestbefore};
+    push @filter_vars, trim($form->{bestbefore});
   }
 
-  if ($form->{fromdate}) {
-    push @filter_ary, "?::DATE <= i1.itime::DATE";
-    push @filter_vars, $form->{fromdate};
+  if (trim($form->{fromdate})) {
+    push @filter_ary, "? <= i1.shippingdate";
+    push @filter_vars, trim($form->{fromdate});
   }
 
-  if ($form->{todate}) {
-    push @filter_ary, "?::DATE >= i1.itime::DATE";
-    push @filter_vars, $form->{todate};
+  if (trim($form->{todate})) {
+    push @filter_ary, "? >= i1.shippingdate";
+    push @filter_vars, trim($form->{todate});
   }
 
   if ($form->{l_employee}) {
@@ -383,18 +279,52 @@ sub get_warehouse_journal {
   my $sort_order = $form->{order};
 
   $sort_col      = $filter{sort}         unless $sort_col;
-  $sort_order    = ($sort_col = 'itime') unless $sort_col;
-  $sort_col      = 'itime'               if     $sort_col eq 'date';
-  $sort_order    = $filter{order}        unless $sort_order;
-  my $sort_spec  = "${sort_col} " . ($sort_order ? " DESC" : " ASC");
+  $sort_col      = 'shippingdate'        if     $sort_col eq 'date';
+  $sort_order    = ($sort_col = 'shippingdate') unless $sort_col;
+
+  my %orderspecs = (
+    'shippingdate'   => ['shippingdate', 'r_itime', 'r_parts_id'],
+    'bin_to'         => ['bin_to', 'r_itime', 'r_parts_id'],
+    'bin_from'       => ['bin_from', 'r_itime', 'r_parts_id'],
+    'warehouse_to'   => ['warehouse_to, r_itime, r_parts_id'],
+    'warehouse_from' => ['warehouse_from, r_itime, r_parts_id'],
+    'partnumber'     => ['partnumber'],
+    'partdescription'=> ['partdescription'],
+    'partunit'       => ['partunit, r_itime, r_parts_id'],
+    'qty'            => ['qty, r_itime, r_parts_id'],
+    'oe_id'          => ['oe_id'],
+    'comment'        => ['comment'],
+    'trans_type'     => ['trans_type'],
+    'employee'       => ['employee'],
+    'projectnumber'  => ['projectnumber'],
+    'chargenumber'   => ['chargenumber'],
+  );
+
+  $sort_order    = $filter{order}  unless $sort_order;
+  my $ASC = ($sort_order ? " DESC" : " ASC");
+  my $sort_spec  = join("$ASC , ", @{$orderspecs{$sort_col}}). " $ASC";
 
   my $where_clause = @filter_ary ? join(" AND ", @filter_ary) . " AND " : '';
 
+  my ($cvar_where, @cvar_values) = CVar->build_filter_query(
+    module         => 'IC',
+    trans_id_field => 'p.id',
+    filter         => $form,
+    sub_module     => undef,
+  );
+
+  if ($cvar_where) {
+    $where_clause .= qq| ($cvar_where) AND |;
+    push @filter_vars, @cvar_values;
+  }
+
   $select_tokens{'trans'} = {
      "parts_id"             => "i1.parts_id",
      "qty"                  => "ABS(SUM(i1.qty))",
      "partnumber"           => "p.partnumber",
      "partdescription"      => "p.description",
+     "classification_id"    => "p.classification_id",
+     "part_type"            => "p.part_type",
      "bindescription"       => "b.description",
      "chargenumber"         => "i1.chargenumber",
      "bestbefore"           => "i1.bestbefore",
@@ -407,10 +337,12 @@ sub get_warehouse_journal {
      "comment"              => "i1.comment",
      "trans_type"           => "tt.description",
      "trans_id"             => "i1.trans_id",
+     "id"                   => "i1.id",
      "oe_id"                => "COALESCE(i1.oe_id, i2.oe_id)",
      "invoice_id"           => "COALESCE(i1.invoice_id, i2.invoice_id)",
-     "date"                 => "i1.itime::DATE",
+     "date"                 => "i1.shippingdate",
      "itime"                => "i1.itime",
+     "shippingdate"         => "i1.shippingdate",
      "employee"             => "e.name",
      "projectnumber"        => "COALESCE(pr.projectnumber, '$filter{na}')",
      };
@@ -425,40 +357,29 @@ sub get_warehouse_journal {
      "warehouse_from"       => "'$filter{na}'",
      };
 
+  $form->{l_classification_id}  = 'Y';
+  $form->{l_trans_id}           = 'Y';
+  $form->{l_part_type}          = 'Y';
+  $form->{l_itime}              = 'Y';
   $form->{l_invoice_id} = $form->{l_oe_id} if $form->{l_oe_id};
 
   # build the select clauses.
   # take all the requested ones from the first hash and overwrite them from the out/in hashes if present.
   for my $i ('trans', 'out', 'in') {
     $select{$i} = join ', ', map { +/^l_/; ($select_tokens{$i}{"$'"} || $select_tokens{'trans'}{"$'"}) . " AS r_$'" }
-          ( grep( { !/qty$/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form), qw(l_parts_id l_qty l_partunit l_itime) );
+          ( grep( { !/qty$/ and !/^l_cvar/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form), qw(l_parts_id l_qty l_partunit l_shippingdate) );
   }
 
   my $group_clause = join ", ", map { +/^l_/; "r_$'" }
-        ( grep( { !/qty$/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form), qw(l_parts_id l_partunit l_itime) );
+        ( grep( { !/qty$/ and !/^l_cvar/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form), qw(l_parts_id l_partunit l_shippingdate l_itime) );
 
   $where_clause = defined($where_clause) ? $where_clause : '';
-  my $query =
-  qq|SELECT DISTINCT $select{trans}
-    FROM inventory i1
-    LEFT JOIN inventory i2 ON i1.trans_id = i2.trans_id
-    LEFT JOIN parts p ON i1.parts_id = p.id
-    LEFT JOIN bin b1 ON i1.bin_id = b1.id
-    LEFT JOIN bin b2 ON i2.bin_id = b2.id
-    LEFT JOIN warehouse w1 ON i1.warehouse_id = w1.id
-    LEFT JOIN warehouse w2 ON i2.warehouse_id = w2.id
-    LEFT JOIN transfer_type tt ON i1.trans_type_id = tt.id
-    LEFT JOIN project pr ON i1.project_id = pr.id
-    LEFT JOIN employee e ON i1.employee_id = e.id
-    WHERE $where_clause i2.qty = -i1.qty AND i2.qty > 0 AND
-          i1.trans_id IN ( SELECT i.trans_id FROM inventory i GROUP BY i.trans_id HAVING COUNT(i.trans_id) = 2 )
-    GROUP BY $group_clause
-
-    UNION
 
+  my $query =
+  qq|SELECT * FROM (
     SELECT DISTINCT $select{out}
     FROM inventory i1
-    LEFT JOIN inventory i2 ON i1.trans_id = i2.trans_id
+    LEFT JOIN inventory i2 ON i1.trans_id = i2.trans_id AND i1.id = i2.id
     LEFT JOIN parts p ON i1.parts_id = p.id
     LEFT JOIN bin b1 ON i1.bin_id = b1.id
     LEFT JOIN bin b2 ON i2.bin_id = b2.id
@@ -467,15 +388,15 @@ sub get_warehouse_journal {
     LEFT JOIN transfer_type tt ON i1.trans_type_id = tt.id
     LEFT JOIN project pr ON i1.project_id = pr.id
     LEFT JOIN employee e ON i1.employee_id = e.id
-    WHERE $where_clause i1.qty < 0 AND
-          i1.trans_id IN ( SELECT i.trans_id FROM inventory i GROUP BY i.trans_id HAVING COUNT(i.trans_id) = 1 )
+    WHERE $where_clause i1.qty != 0 AND tt.direction = 'out' AND
+          i1.trans_id IN ( SELECT i.trans_id FROM inventory i GROUP BY i.trans_id HAVING COUNT(i.trans_id) >= 1 )
     GROUP BY $group_clause
 
     UNION
 
     SELECT DISTINCT $select{in}
     FROM inventory i1
-    LEFT JOIN inventory i2 ON i1.trans_id = i2.trans_id
+    LEFT JOIN inventory i2 ON i1.trans_id = i2.trans_id AND i1.id = i2.id
     LEFT JOIN parts p ON i1.parts_id = p.id
     LEFT JOIN bin b1 ON i1.bin_id = b1.id
     LEFT JOIN bin b2 ON i2.bin_id = b2.id
@@ -484,29 +405,27 @@ sub get_warehouse_journal {
     LEFT JOIN transfer_type tt ON i1.trans_type_id = tt.id
     LEFT JOIN project pr ON i1.project_id = pr.id
     LEFT JOIN employee e ON i1.employee_id = e.id
-    WHERE $where_clause i1.qty > 0 AND
-          i1.trans_id IN ( SELECT i.trans_id FROM inventory i GROUP BY i.trans_id HAVING COUNT(i.trans_id) = 1 )
+    WHERE $where_clause i1.qty != 0 AND tt.direction = 'in' AND
+          i1.trans_id IN ( SELECT i.trans_id FROM inventory i GROUP BY i.trans_id HAVING COUNT(i.trans_id) >= 1 )
     GROUP BY $group_clause
-    ORDER BY r_${sort_spec}|;
+    ORDER BY r_${sort_spec}) AS lines WHERE r_qty != 0|;
+
+  my @all_vars = (@filter_vars,@filter_vars);
+
+  if ($filter{limit}) {
+    $query .= " LIMIT ?";
+    push @all_vars,$filter{limit};
+  }
+  if ($filter{offset}) {
+    $query .= " OFFSET ?";
+    push @all_vars, $filter{offset};
+  }
 
-  my $sth = prepare_execute_query($form, $dbh, $query, @filter_vars, @filter_vars, @filter_vars);
+  my $sth = prepare_execute_query($form, $dbh, $query, @all_vars);
 
   my ($h_oe_id, $q_oe_id);
   if ($form->{l_oe_id}) {
     $q_oe_id = <<SQL;
-      SELECT oe.id AS id,
-        CASE WHEN oe.quotation THEN oe.quonumber ELSE oe.ordnumber END AS number,
-        CASE
-          WHEN oe.customer_id IS NOT NULL AND     COALESCE(oe.quotation, FALSE) THEN 'sales_quotation'
-          WHEN oe.customer_id IS NOT NULL AND NOT COALESCE(oe.quotation, FALSE) THEN 'sales_order'
-          WHEN oe.customer_id IS     NULL AND     COALESCE(oe.quotation, FALSE) THEN 'request_quotation'
-          ELSE                                                                       'purchase_order'
-        END AS type
-      FROM oe
-      WHERE oe.id = ?
-
-      UNION
-
       SELECT dord.id AS id, dord.donumber AS number,
         CASE
           WHEN dord.customer_id IS NULL THEN 'purchase_delivery_order'
@@ -517,18 +436,6 @@ sub get_warehouse_journal {
 
       UNION
 
-      SELECT ar.id AS id, ar.invnumber AS number, 'sales_invoice' AS type
-      FROM ar
-      WHERE ar.id = ?
-
-      UNION
-
-      SELECT ap.id AS id, ap.invnumber AS number, 'purchase_invoice' AS type
-      FROM ap
-      WHERE ap.id = ?
-
-      UNION
-
       SELECT ar.id AS id, ar.invnumber AS number, 'sales_invoice' AS type
       FROM ar
       WHERE ar.id = (SELECT trans_id FROM invoice WHERE id = ?)
@@ -559,8 +466,7 @@ SQL
     }
 
     if ($h_oe_id && ($ref->{oe_id} || $ref->{invoice_id})) {
-      my $id = $ref->{oe_id} ? $ref->{oe_id} : $ref->{invoice_id};
-      do_statement($form, $h_oe_id, $q_oe_id, ($id) x 6);
+      do_statement($form, $h_oe_id, $q_oe_id, $ref->{oe_id}, ($ref->{invoice_id}) x 2);
       $ref->{oe_id_info} = $h_oe_id->fetchrow_hashref() || {};
     }
 
@@ -581,6 +487,7 @@ SQL
 #  - warehouse_id - will return matches with this warehouse_id only
 #  - partnumber   - will return only matches where the given string is a substring of the partnumber
 #  - partsid      - will return matches with this parts_id only
+#  - classification_id - will return matches with this parts with this classification only
 #  - description  - will return only matches where the given string is a substring of the description
 #  - chargenumber - will return only matches where the given string is a substring of the chargenumber
 #  - bestbefore   - will return only matches with this bestbefore date
@@ -629,12 +536,17 @@ sub get_warehouse_report {
 
   if ($filter{partnumber}) {
     push @filter_ary,  "p.partnumber ILIKE ?";
-    push @filter_vars, '%' . $filter{partnumber} . '%';
+    push @filter_vars, like($filter{partnumber});
+  }
+
+  if ($filter{classification_id}) {
+    push @filter_ary, "p.classification_id = ?";
+    push @filter_vars, $filter{classification_id};
   }
 
   if ($filter{description}) {
     push @filter_ary,  "p.description ILIKE ?";
-    push @filter_vars, '%' . $filter{description} . '%';
+    push @filter_vars, like($filter{description});
   }
 
   if ($filter{partsid}) {
@@ -642,24 +554,34 @@ sub get_warehouse_report {
     push @filter_vars, $filter{partsid};
   }
 
+  if ($filter{partsgroup_id}) {
+    push @filter_ary,  "p.partsgroup_id = ?";
+    push @filter_vars, $filter{partsgroup_id};
+  }
+
   if ($filter{chargenumber}) {
     push @filter_ary,  "i.chargenumber ILIKE ?";
-    push @filter_vars, '%' . $filter{chargenumber} . '%';
+    push @filter_vars, like($filter{chargenumber});
   }
 
-  if ($form->{bestbefore}) {
+  if (trim($form->{bestbefore})) {
     push @filter_ary, "?::DATE = i.bestbefore::DATE";
-    push @filter_vars, $form->{bestbefore};
+    push @filter_vars, trim($form->{bestbefore});
+  }
+
+  if ($filter{classification_id}) {
+    push @filter_ary, "p.classification_id = ?";
+    push @filter_vars, $filter{classification_id};
   }
 
   if ($filter{ean}) {
     push @filter_ary,  "p.ean ILIKE ?";
-    push @filter_vars, '%' . $filter{ean} . '%';
+    push @filter_vars, like($filter{ean});
   }
 
-  if ($filter{date}) {
-    push @filter_ary, "i.itime <= ?";
-    push @filter_vars, $filter{date};
+  if (trim($filter{date})) {
+    push @filter_ary, "i.shippingdate <= ?";
+    push @filter_vars, trim($filter{date});
   }
   if (!$filter{include_invalid_warehouses}){
     push @filter_ary,  "NOT (w.invalid)";
@@ -704,6 +626,8 @@ sub get_warehouse_report {
      "warehouseid"          => "i.warehouse_id",
      "partnumber"           => "p.partnumber",
      "partdescription"      => "p.description",
+     "classification_id"    => "p.classification_id",
+     "part_type"            => "p.part_type",
      "bindescription"       => "b.description",
      "binid"                => "b.id",
      "chargenumber"         => "i.chargenumber",
@@ -712,14 +636,19 @@ sub get_warehouse_report {
      "chargeid"             => "c.id",
      "warehousedescription" => "w.description",
      "partunit"             => "p.unit",
-     "stock_value"          => "p.lastcost / COALESCE(pfac.factor, 1)",
+     "stock_value"          => ($form->{stock_value_basis} // '') eq 'list_price' ? "p.listprice / COALESCE(pfac.factor, 1)" : "p.lastcost / COALESCE(pfac.factor, 1)",
+     "purchase_price"       => "p.lastcost",
+     "list_price"           => "p.listprice",
   );
+  $form->{l_classification_id}  = 'Y';
+  $form->{l_part_type}          = 'Y';
+
   my $select_clause = join ', ', map { +/^l_/; "$select_tokens{$'} AS $'" }
-        ( grep( { !/qty/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form),
+        ( grep( { !/qty/ and !/^l_cvar/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form),
           qw(l_parts_id l_qty l_partunit) );
 
   my $group_clause = join ", ", map { +/^l_/; "$'" }
-        ( grep( { !/qty/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form),
+        ( grep( { !/qty/ and !/^l_cvar/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form),
           qw(l_parts_id l_partunit) );
 
   my %join_tokens = (
@@ -727,11 +656,23 @@ sub get_warehouse_report {
     );
 
   my $joins = join ' ', grep { $_ } map { +/^l_/; $join_tokens{"$'"} }
-        ( grep( { !/qty/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form),
+        ( grep( { !/qty/ and !/^l_cvar/ and /^l_/ and $form->{$_} eq 'Y' } keys %$form),
           qw(l_parts_id l_qty l_partunit) );
 
+  my ($cvar_where, @cvar_values) = CVar->build_filter_query(
+    module         => 'IC',
+    trans_id_field => 'p.id',
+    filter         => $form,
+    sub_module     => undef,
+  );
+
+  if ($cvar_where) {
+    $where_clause .= qq| AND ($cvar_where)|;
+    push @filter_vars, @cvar_values;
+  }
+
   my $query =
-    qq|SELECT $select_clause
+    qq|SELECT * FROM ( SELECT $select_clause
       FROM inventory i
       LEFT JOIN parts     p ON i.parts_id     = p.id
       LEFT JOIN bin       b ON i.bin_id       = b.id
@@ -739,9 +680,17 @@ sub get_warehouse_report {
       $joins
       WHERE $where_clause
       GROUP BY $group_clause
-      ORDER BY $sort_spec|;
+      ORDER BY $sort_spec ) AS lines WHERE qty<>0|;
 
-  my $sth = prepare_execute_query($form, $dbh, $query, @filter_vars);
+  if ($filter{limit}) {
+    $query .= " LIMIT ?";
+    push @filter_vars,$filter{limit};
+  }
+  if ($filter{offset}) {
+    $query .= " OFFSET ?";
+    push @filter_vars, $filter{offset};
+  }
+  my $sth = prepare_execute_query($form, $dbh, $query, @filter_vars );
 
   my (%non_empty_bins, @all_fields, @contents);
 
@@ -885,6 +834,40 @@ sub get_basic_bin_info {
 
   return map { $_->{bin_id} => $_ } @{ $result };
 }
+
+sub get_basic_warehouse_info {
+  $main::lxdebug->enter_sub();
+
+  my $self     = shift;
+  my %params   = @_;
+
+  Common::check_params(\%params, qw(id));
+
+  my $myconfig = \%main::myconfig;
+  my $form     = $main::form;
+
+  my $dbh      = $params{dbh} || $form->get_standard_dbh();
+
+  my @ids      = 'ARRAY' eq ref $params{id} ? @{ $params{id} } : ($params{id});
+
+  my $query    =
+    qq|SELECT w.id AS warehouse_id, w.description AS warehouse_description
+       FROM warehouse w
+       WHERE w.id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
+
+  my $result = selectall_hashref_query($form, $dbh, $query, map { conv_i($_) } @ids);
+
+  if ('' eq ref $params{id}) {
+    $result = $result->[0] || { };
+    $main::lxdebug->leave_sub();
+
+    return $result;
+  }
+
+  $main::lxdebug->leave_sub();
+
+  return map { $_->{warehouse_id} => $_ } @{ $result };
+}
 #
 # Eingabe:  Teilenummer, Lagernummer (warehouse)
 # Ausgabe:  Die maximale Anzahl der Teile in diesem Lager
@@ -903,9 +886,9 @@ $main::lxdebug->enter_sub();
   my $dbh      = $params{dbh} || $form->get_standard_dbh();
 
   my $query = qq| SELECT SUM(qty), bin_id, chargenumber, bestbefore  FROM inventory where parts_id = ? AND warehouse_id = ? GROUP BY bin_id, chargenumber, bestbefore|;
-
   my $sth_QTY      = prepare_execute_query($form, $dbh, $query, ,$params{parts_id}, $params{warehouse_id}); #info: aufruf an DBUtils.pm
 
+
   my $max_qty_parts = 0; #Initialisierung mit 0
   while (my $ref = $sth_QTY->fetchrow_hashref()) {  # wir laufen über alle Haltbarkeiten, chargen und Lagerorte (s.a. SQL-Query oben)
     $max_qty_parts += $ref->{sum};
@@ -977,18 +960,39 @@ $main::lxdebug->enter_sub();
     $max_qty_parts += $ref->{sum};
     $i++;
     if (($ref->{chargenumber} || $ref->{bestbefore}) && $ref->{sum} != 0){
-      $error=1;
+      $error = 1;
     }
   }
-  #if ($i < 1){
-  #  $error = 2;
-  #}
-
   $main::lxdebug->leave_sub();
 
   return ($max_qty_parts, $error);
 }
 
+sub get_wh_and_bin_for_charge {
+  $main::lxdebug->enter_sub();
+
+  my $self     = shift;
+  my %params   = @_;
+  my %bin_qty;
+
+  croak t8('Need charge number!') unless $params{chargenumber};
+
+  my $inv_items = SL::DB::Manager::Inventory->get_all(where => [chargenumber => $params{chargenumber} ]);
+
+  croak t8("Invalid charge number: #1", $params{chargenumber}) unless (ref @{$inv_items}[0] eq 'SL::DB::Inventory');
+  # add all qty for one bin and add wh_id
+  ($bin_qty{$_->bin_id}{qty}, $bin_qty{$_->bin_id}{wh}) = ($bin_qty{$_->bin_id}{qty} + $_->qty, $_->warehouse_id) for @{ $inv_items };
+
+  while (my ($bin, $value) = each (%bin_qty)) {
+    if ($value->{qty} > 0) {
+      $main::lxdebug->leave_sub();
+      return ($value->{qty}, $value->{wh}, $bin, $params{chargenumber});
+    }
+  }
+
+  $main::lxdebug->leave_sub();
+  return undef;
+}
 1;
 
 __END__
@@ -1004,7 +1008,7 @@ SL::WH - Warehouse backend
 
 =head1 DESCRIPTION
 
-Backend for lx-office warehousing functions.
+Backend for kivitendo warehousing functions.
 
 =head1 FUNCTIONS
 
@@ -1018,7 +1022,7 @@ is called like this:
     qty              => 12.45,
     transfer_type    => 'transfer',
     src_warehouse_id => 12,
-    stc_bin_id       => 23,
+    src_bin_id       => 23,
     dst_warehouse_id => 25,
     dst_bin_id       => 167,
   });
@@ -1031,6 +1035,13 @@ transfer accepts more than one transaction parameter, each being a hash ref. If
 more than one is supplied, it is guaranteed, that all are processed in the same
 transaction.
 
+It is possible to record stocktakings within this transaction as well.
+This is useful if the transfer is the result of stocktaking (see also
+C<SL::Controller::Inventory>). To do so the parameters C<record_stocktaking>,
+C<stocktaking_qty> and C<stocktaking_cutoff_date> hava to be given.
+If stocktaking should be saved, then the transfer quantity can be zero. In this
+case no entry in inventory will be made, but only the stocktaking entry.
+
 Here is a full list of parameters. All "_id" parameters except oe and
 orderitems can be called without id with RDB objects as well.
 
@@ -1097,8 +1108,113 @@ An optional comment.
 
 An expiration date. Note that this is not by default used by C<warehouse_report>.
 
+=item record_stocktaking
+
+A boolean flag to indicate that a stocktaking entry should be saved.
+
+=item stocktaking_qty
+
+The quantity for the stocktaking entry.
+
+=item stocktaking_cutoff_date
+
+The cutoff date for the stocktaking entry.
+
+=back
+
+=head2 create_assembly \%PARAMS, [ \%PARAMS, ... ]
+
+Creates an assembly if all defined items are available.
+
+Assembly item(s) will be stocked out and the assembly will be stocked in,
+taking into account the qty and units which can be defined for each
+assembly item separately.
+
+The calling params originate from C<transfer> but only parts_id with the
+attribute assembly are processed.
+
+The typical params would be:
+
+  my %TRANSFER = (
+    'login'            => $::myconfig{login},
+    'dst_warehouse_id' => $form->{warehouse_id},
+    'dst_bin_id'       => $form->{bin_id},
+    'chargenumber'     => $form->{chargenumber},
+    'bestbefore'       => $form->{bestbefore},
+    'assembly_id'      => $form->{parts_id},
+    'qty'              => $form->{qty},
+    'comment'          => $form->{comment}
+  );
+
+
+=head2 get_wh_and_bin_for_charge C<$params{chargenumber}>
+
+Gets the current qty from the inventory entries with the mandatory chargenumber: C<$params{chargenumber}>.
+Croaks if the chargenumber is missing or no entry currently exists.
+If there is one bin and warehouse with a positive qty, this fields are returned:
+C<qty> C<warehouse_id>, C<bin_id>, C<chargenumber>.
+Otherwise returns undef.
+
+
+=head3 Prerequisites
+
+All of these prerequisites have to be trueish, otherwise the function will exit
+unsuccessfully with a return value of undef.
+
+=over 4
+
+=item Mandantory params
+
+  assembly_id, qty, login, dst_warehouse_id and dst_bin_id are mandatory.
+
+=item Subset named 'Assembly' of data set 'Part'
+
+  assembly_id has to be an id in the table parts with the valid subset assembly.
+
+=item Assembly is composed of assembly item(s)
+
+  There has to be at least one data set in the table assembly referenced to this assembly_id.
+
+=item Assembly can be disassembled
+
+  Assemblies are like cakes. You cannot disassemble it. NEVER.
+  But if your assembly is a mechanical cake you may unscrew it.
+  Assemblies are created in one transaction therefore you can
+  safely rely on the trans_id in inventory to disassemble the
+  created assemblies (see action disassemble_assembly in wh.pl).
+
+=item The assembly item(s) have to be in the same warehouse
+
+  inventory.warehouse_id equals dst_warehouse_id (client configurable).
+
+=item The assembly item(s) have to be in stock with the qty needed
+
+  I can only make a cake by receipt if I have ALL ingredients and
+  in the needed stock amount.
+  The qty of stocked in assembly item(s) has to fit into the
+  number of the qty of the assemblies, which are going to be created (client configurable).
+
+=item assembly item(s) with the parts set 'service' are ignored
+
+  The subset 'Services' of part will not transferred for assembly item(s).
+
 =back
 
+Client configurable prerequisites can be changed with different
+prerequisites as described in client_config (s.a. next chapter).
+
+
+=head2 default creation of assembly
+
+The valid state of the assembly item(s) used for the assembly process are
+'out' for the general direction and 'used' as the specific reason.
+The valid state of the assembly is 'in' for the direction and 'assembled'
+as the specific reason.
+
+The method is transaction safe, in case of errors not a single entry will be made
+in inventory.
+
+
 =head1 BUGS
 
 None yet.
index d31a588..85f6249 100644 (file)
@@ -17,18 +17,28 @@ use Rose::Object::MakeMethods::Generic (
 );
 
 my %type_to_path = (
-  sales_quotation         => 'angebote',
-  sales_order             => 'bestellungen',
-  request_quotation       => 'anfragen',
-  purchase_order          => 'lieferantenbestellungen',
-  sales_delivery_order    => 'verkaufslieferscheine',
-  purchase_delivery_order => 'einkaufslieferscheine',
-  credit_note             => 'gutschriften',
-  invoice                 => 'rechnungen',
-  purchase_invoice        => 'einkaufsrechnungen',
-  part                    => 'waren',
-  service                 => 'dienstleistungen',
-  assembly                => 'erzeugnisse',
+  sales_quotation             => 'angebote',
+  sales_order                 => 'bestellungen',
+  request_quotation           => 'anfragen',
+  purchase_order              => 'lieferantenbestellungen',
+  sales_delivery_order        => 'verkaufslieferscheine',
+  purchase_delivery_order     => 'einkaufslieferscheine',
+  supplier_delivery_order     => 'beistelllieferscheine',
+  rma_delivery_order          => 'retourenlieferscheine',
+  credit_note                 => 'gutschriften',
+  invoice                     => 'rechnungen',
+  invoice_for_advance_payment => 'rechnungen',
+  final_invoice               => 'rechnungen',
+  purchase_invoice            => 'einkaufsrechnungen',
+  part                        => 'waren',
+  service                     => 'dienstleistungen',
+  assembly                    => 'erzeugnisse',
+  letter                      => 'briefe',
+  general_ledger              => 'dialogbuchungen',
+  accounts_payable            => 'kreditorenbuchungen',
+  customer                    => 'kunden',
+  vendor                      => 'lieferanten',
+  dunning                     => 'mahnungen',
 );
 
 sub get_all_files {
index 313c9ff..54af37a 100644 (file)
@@ -58,6 +58,19 @@ sub store {
       $params{new_version} = 1;
     }
 
+    # Do not create a new version of the document if file size of last version is the same.
+    if ($params{new_version}) {
+      my $last_file_size = $last->size;
+      my $new_file_size;
+      if ($params{file}) {
+        croak 'No valid file' unless -f $params{file};
+        $new_file_size  = (stat($params{file}))[7];
+      } else {
+        $new_file_size  = length(${ $params{data} });
+      }
+      $params{new_version} = 0 if $last_file_size == $new_file_size;
+    }
+
     if ($params{new_version}) {
       my $new_version  = $self->webdav->version_scheme->next_version($last);
       my $sep          = $self->webdav->version_scheme->separator;
@@ -153,6 +166,9 @@ C<file> and C<data> are exclusive.
 If param C<new_version> is set, force a new version, even if the versioning
 scheme would keep the old one.
 
+No new version is stored if the file or data size is euqal to the size of
+the last stored version.
+
 =back
 
 =head1 SEE ALSO
index 4ecf423..e4ece79 100644 (file)
@@ -42,6 +42,10 @@ sub full_filedescriptor {
   File::Spec->catfile($self->webdav->webdav_path, $self->filename);
 }
 
+sub size {
+  ($_[0]->stat)[7];
+}
+
 sub atime {
   DateTime->from_epoch(epoch => ($_[0]->stat)[8]);
 }
@@ -132,6 +136,10 @@ Returns the version string.
 
 Returns the extension.
 
+=item C<size>
+
+wrapped stat[7]
+
 =item C<atime>
 
 L<DateTime> wrapped stat[8]
@@ -148,6 +156,10 @@ Ref to the actual data in raw encoding.
 
 URL relative to the web base dir for download.
 
+=item C<full_filedescriptor>
+
+Fully qualified path to file.
+
 =back
 
 =head1 SEE ALSO
diff --git a/SL/X.pm b/SL/X.pm
index 552e5ef..e1707cd 100644 (file)
--- a/SL/X.pm
+++ b/SL/X.pm
@@ -1,10 +1,48 @@
 package SL::X;
 
 use strict;
+use warnings;
 
-use Exception::Lite qw(declareExceptionClass);
+use SL::X::Base;
 
-declareExceptionClass('SL::X::FormError');
-declareExceptionClass('SL::X::DBHookError', [ '%s hook \'%s\' for object type \'%s\' failed', qw(when hook object_type object) ]);
+
+# note! the default fields "message", "error" and "show_trace" are created by
+# Exception::Class if message or error are given, they are used for
+# stringification, so don't use them in error_templates
+#
+use Exception::Class (
+  'SL::X::FormError'    => {
+    isa                 => 'SL::X::Base',
+  },
+  'SL::X::DBError'      => {
+    isa                 => 'SL::X::Base',
+    fields              => [ qw(msg db_error) ],
+    defaults            => { error_template => [ '%s: %s', qw(msg db_error) ] },
+  },
+  'SL::X::DBHookError'  => {
+    isa                 => 'SL::X::DBError',
+    fields              => [ qw(when hook object object_type) ],
+    defaults            => { error_template => [ '%s hook \'%s\' for object type \'%s\' failed', qw(when hook object_type object) ] },
+  },
+  'SL::X::DBRoseError'  => {
+    isa                 => 'SL::X::DBError',
+    fields              => [ qw(class metaobject object) ],
+    defaults            => { error_template => [ '\'%s\' in object of type \'%s\' occurred', qw(db_error class) ] },
+  },
+  'SL::X::DBUtilsError' => {
+    isa                 => 'SL::X::DBError',
+  },
+  'SL::X::ZUGFeRDValidation' => {
+    isa                 => 'SL::X::Base',
+  },
+  'SL::X::Inventory' => {
+    isa                 => 'SL::X::Base',
+    fields              => [ qw(code) ],
+  },
+  'SL::X::Inventory::Allocation' => {
+    isa                 => 'SL::X::Base',
+    fields              => [ qw(code) ],
+  },
+);
 
 1;
diff --git a/SL/X/Base.pm b/SL/X/Base.pm
new file mode 100644 (file)
index 0000000..3e6d251
--- /dev/null
@@ -0,0 +1,26 @@
+package SL::X::Base;
+
+use strict;
+use warnings;
+
+use parent qw(Exception::Class::Base);
+
+sub _defaults { return () }
+
+sub message { goto &error }
+
+sub error {
+  my ($self, @params) = @_;
+
+  return $self->{message} if ($self->{message} // '') ne '';
+
+  return $self->SUPER::error(@params) if !$self->can('_defaults');
+
+  my %defaults = $self->_defaults;
+  return $self->SUPER::error(@params) if !$defaults{error_template};
+
+  my ($format, @fields) = @{ $defaults{error_template} };
+  return sprintf $format, map { $self->$_ } @fields;
+}
+
+1;
diff --git a/SL/YAML.pm b/SL/YAML.pm
new file mode 100644 (file)
index 0000000..bfccfba
--- /dev/null
@@ -0,0 +1,60 @@
+package SL::YAML;
+
+use strict;
+use warnings;
+
+sub _choose_yaml_module {
+  return 'YAML::XS' if $INC{'YAML/XS.pm'};
+  return 'YAML'     if $INC{'YAML.pm'};
+
+  my @err;
+
+  return 'YAML::XS' if eval { require YAML::XS; 1; };
+  push @err, "Error loading YAML::XS: $@";
+
+  return 'YAML' if eval { require YAML; 1; };
+  push @err, "Error loading YAML: $@";
+
+  die join("\n", "Couldn't load a YAML module:", @err);
+}
+
+BEGIN {
+  our $YAML_Class = _choose_yaml_module();
+  $YAML_Class->import(qw(Dump Load DumpFile LoadFile));
+}
+
+sub YAML { our $YAML_Class }
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::YAML - A thin wrapper around YAML::XS and YAML
+
+=head1 SYNOPSIS
+
+    use SL::YAML;
+
+    my $menu_data = SL::YAML::LoadFile("menus/user/00-erp.yml");
+
+=head1 OVERVIEW
+
+This is a thin wrapper around the YAML::XS and YAML modules. It'll
+prefer loading YAML::XS if that's found and will fallback to YAML
+otherwise. It only provides the four functions C<Dump>, C<Load>,
+C<DumpFile> and C<LoadFile> — just enough to get by for kivitendo.
+
+The functions are direct imports from the imported module. Please see
+the documentation for YAML::XS or YAML for details.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
diff --git a/SL/ZUGFeRD.pm b/SL/ZUGFeRD.pm
new file mode 100644 (file)
index 0000000..38d7623
--- /dev/null
@@ -0,0 +1,254 @@
+package SL::ZUGFeRD;
+
+use strict;
+use warnings;
+use utf8;
+
+use CAM::PDF;
+use Data::Dumper;
+use List::Util qw(first);
+use XML::LibXML;
+
+use SL::Locale::String qw(t8);
+
+use parent qw(Exporter);
+our @EXPORT_PROFILES = qw(PROFILE_FACTURX_EXTENDED PROFILE_XRECHNUNG);
+our @EXPORT_OK       = (@EXPORT_PROFILES);
+our %EXPORT_TAGS     = (PROFILES => \@EXPORT_PROFILES);
+
+use constant PROFILE_FACTURX_EXTENDED => 0;
+use constant PROFILE_XRECHNUNG        => 1;
+
+use constant RES_OK                              => 0;
+use constant RES_ERR_FILE_OPEN                   => 1;
+use constant RES_ERR_NO_XMP_METADATA             => 2;
+use constant RES_ERR_NO_XML_INVOICE              => 3;
+use constant RES_ERR_NOT_ZUGFERD                 => 4;
+use constant RES_ERR_UNSUPPORTED_ZUGFERD_VERSION => 5;
+
+our @customer_settings = (
+  [ 0,                                  t8('Do not create Factur-X/ZUGFeRD invoices')                                    ],
+  [ PROFILE_FACTURX_EXTENDED() * 2 + 1, t8('Create with profile \'Factur-X 1.0.05/ZUGFeRD 2.1.1 extended\'')             ],
+  [ PROFILE_FACTURX_EXTENDED() * 2 + 2, t8('Create with profile \'Factur-X 1.0.05/ZUGFeRD 2.1.1 extended\' (test mode)') ],
+  [ PROFILE_XRECHNUNG()        * 2 + 1, t8('Create with profile \'XRechnung 2.0.0\'')                                    ],
+  [ PROFILE_XRECHNUNG()        * 2 + 2, t8('Create with profile \'XRechnung 2.0.0\' (test mode)')                        ],
+);
+
+sub convert_customer_setting {
+  my ($class, $customer_setting) = @_;
+
+  return () if ($customer_setting <= 0) || ($customer_setting >= scalar(@customer_settings));
+
+  return (
+    profile   => int(($customer_setting - 1) / 2),
+    test_mode => ($customer_setting - 1) % 2,
+  );
+}
+
+sub _extract_zugferd_invoice_xml {
+  my $doc        = shift;
+  my $names_dict = $doc->getValue($doc->getRootDict->{Names}) or return {};
+  my $files_tree = $names_dict->{EmbeddedFiles}               or return {};
+  my @agenda     = $files_tree;
+  my $ret        = {};
+
+  # Hardly ever more than single leaf, but...
+
+  while (@agenda) {
+    my $item = $doc->getValue(shift @agenda);
+
+    if ($item->{Kids}) {
+      my $kids = $doc->getValue($item->{Kids});
+      push @agenda, @$kids
+
+    } else {
+      my $nodes = $doc->getValue($item->{Names});
+      my @names = map { $doc->getValue($_)} @$nodes;
+
+      while (@names) {
+        my ($k, $v)  = splice @names, 0, 2;
+        my $ef_node  = $v->{EF};
+        my $ef_dict  = $doc->getValue($ef_node);
+        my $fnode    = (values %$ef_dict)[0];
+        my $any_num  = $fnode->{value};
+        my $obj_node = $doc->dereference($any_num);
+        my $content  = $doc->decodeOne($obj_node->{value}, 0) // '';
+
+        #print "1\n";
+
+        next if $content !~ m{<rsm:CrossIndustryInvoice};
+        #print "2\n";
+
+        my $dom = eval { XML::LibXML->load_xml(string => $content) };
+        return $content if $dom && ($dom->documentElement->nodeName eq 'rsm:CrossIndustryInvoice');
+      }
+    }
+  }
+
+  return undef;
+}
+
+sub _get_xmp_metadata {
+  my ($doc) = @_;
+
+  my $node = $doc->getValue($doc->getRootDict->{Metadata});
+  if ($node && $node->{StreamData} && defined($node->{StreamData}->{value})) {
+    return $node->{StreamData}->{value};
+  }
+
+  return undef;
+}
+
+sub extract_from_pdf {
+  my ($self, $file_name) = @_;
+
+  my $pdf_doc = CAM::PDF->new($file_name);
+
+  if (!$pdf_doc) {
+    return {
+      result  => RES_ERR_FILE_OPEN(),
+      message => $::locale->text('The file \'#1\' could not be opened for reading.', $file_name),
+    };
+  }
+
+  my $xmp = _get_xmp_metadata($pdf_doc);
+  if (!defined $xmp) {
+    return {
+      result  => RES_ERR_NO_XMP_METADATA(),
+      message => $::locale->text('The file \'#1\' does not contain the required XMP meta data.', $file_name),
+    };
+  }
+
+  my $bad = {
+    result  => RES_ERR_NO_XMP_METADATA(),
+    message => $::locale->text('Parsing the XMP metadata failed.'),
+  };
+
+  my $dom = eval { XML::LibXML->load_xml(string => $xmp) };
+
+  return $bad if !$dom;
+
+  my $xpc = XML::LibXML::XPathContext->new($dom);
+  $xpc->registerNs('rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
+
+  my $zugferd_version;
+
+  foreach my $node ($xpc->findnodes('/x:xmpmeta/rdf:RDF/rdf:Description')) {
+    my $ns = first { ref($_) eq 'XML::LibXML::Namespace' } $node->attributes;
+    next unless $ns;
+
+    if ($ns->getData =~ m{urn:zugferd:pdfa:CrossIndustryDocument:invoice:2p0}) {
+      $zugferd_version = 'zugferd:2p0';
+      last;
+    }
+
+    if ($ns->getData =~ m{urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0}) {
+      $zugferd_version = 'factur-x:1p0';
+      last;
+    }
+
+    if ($ns->getData =~ m{zugferd|factur-x}i) {
+      $zugferd_version = 'unsupported';
+      last;
+    }
+  }
+
+  if (!$zugferd_version) {
+    return {
+      result  => RES_ERR_NOT_ZUGFERD(),
+      message => $::locale->text('The XMP metadata does not declare the Factur-X/ZUGFeRD data.'),
+    };
+  }
+
+  if ($zugferd_version eq 'unsupported') {
+    return {
+      result  => RES_ERR_UNSUPPORTED_ZUGFERD_VERSION(),
+      message => $::locale->text('The Factur-X/ZUGFeRD version used is not supported.'),
+    };
+  }
+
+  my $invoice_xml = _extract_zugferd_invoice_xml($pdf_doc);
+
+  if (!defined $invoice_xml) {
+    return {
+      result  => RES_ERR_NO_XML_INVOICE(),
+      message => $::locale->text('The Factur-X/ZUGFeRD XML invoice was not found.'),
+    };
+  }
+
+  return {
+    result       => RES_OK(),
+    metadata_xmp => $xmp,
+    invoice_xml  => $invoice_xml,
+  };
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::ZUGFeRD - Helper functions for dealing with PDFs containing Factur-X/ZUGFeRD invoice data
+
+=head1 SYNOPSIS
+
+    my $pdf  = '/path/to/my.pdf';
+    my $info = SL::ZUGFeRD->extract_from_pdf($pdf);
+
+    if ($info->{result} != SL::ZUGFeRD::RES_OK()) {
+      # An error occurred; log message from parser:
+      $::lxdebug->message(LXDebug::DEBUG1(), "Could not extract ZUGFeRD data from $pdf: " . $info->{message});
+      return;
+    }
+
+    # Parse & handle invoice XML:
+    my $dom = XML::LibXML->load_xml(string => $info->{invoice_xml});
+
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<extract_from_pdf> C<$file_name>
+
+Opens an existing PDF in the file system and tries to extract
+Factur-X/ZUGFeRD invoice data from it. First it'll parse the XMP
+metadata and look for the Factur-X/ZUGFeRD declaration inside. If the
+declaration isn't found or the declared version isn't 2p0, an error is
+returned.
+
+Otherwise it'll continue to look through all embedded files in the
+PDF. The first embedded XML file with a root node of
+C<rsm:CrossCountryInvoice> will be returnd.
+
+Always returns a hash ref containing the key C<result>, a number that
+can be one of the following constants:
+
+=over 4
+
+=item C<RES_OK> (0): parsing was OK; the returned hash will also
+contain the keys C<xmp_metadata> and C<invoice_xml> which will contain
+the XML text of the metadata & the Factur-X/ZUGFeRD invoice.
+
+=item C<RES_ERR_…> (all values E<gt> 0): parsing failed; the hash will
+also contain a key C<message> which contains a human-readable
+information about what exactly failed.
+
+=back
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
diff --git a/VERSION b/VERSION
index 3049e4e..8ab2db9 100644 (file)
--- a/VERSION
+++ b/VERSION
@@ -1,2 +1,2 @@
-3.3.0
-xxxxx
+3.6.1
+
index 0a9a047..cde5b44 100644 (file)
@@ -1,2 +1,9 @@
-Order Allow,Deny
-Deny from all
+<IfModule mod_authz_core.c>
+  # Apache 2.4
+  Require all denied
+</IfModule>
+<IfModule !mod_authz_core.c>
+  # Apache 2.2
+  Order deny,allow
+  Deny from all
+</IfModule>
index 6ee397f..18d237b 100644 (file)
@@ -1,8 +1,8 @@
 use SL::AccTransCorrections;
 use SL::Form;
+use SL::Locale::String qw(t8);
 use SL::User;
 use Data::Dumper;
-use YAML;
 
 require "bin/mozilla/common.pl";
 
@@ -14,6 +14,8 @@ sub analyze_filter {
   my $form     = $main::form;
   my $locale   = $main::locale;
 
+  setup_analyze_filter_action_bar();
+
   $form->{title}    = $locale->text('General ledger corrections');
   $form->header();
   print $form->parse_html_template('acctranscorrections/analyze_filter');
@@ -47,6 +49,8 @@ sub analyze {
     return;
   }
 
+  setup_analyze_action_bar();
+
   $form->header();
   print $form->parse_html_template('acctranscorrections/analyze_overview',
                                    { 'PROBLEMS' => \@problems,
@@ -251,4 +255,31 @@ sub dispatcher {
   $form->error($locale->text('No action defined.'));
 }
 
+sub setup_analyze_filter_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Analyze'),
+        submit    => [ '#form' ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_analyze_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Back'),
+        call => [ 'kivi.history_back' ],
+      ],
+    );
+  }
+}
+
 1;
index 56c48f3..541abab 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # administration
 
 use utf8;
 
+use List::MoreUtils qw(any);
+
 use SL::Auth;
 use SL::Auth::PasswordPolicy;
 use SL::AM;
 use SL::CA;
 use SL::Form;
+use SL::Helper::Flash;
+use SL::Helper::UserPreferences;
 use SL::User;
 use SL::USTVA;
 use SL::Iconv;
+use SL::Locale::String qw(t8);
 use SL::TODO;
 use SL::DB::Printer;
 use SL::DB::Tax;
 use SL::DB::Language;
+use SL::DB::Default;
+use SL::DBUtils qw(selectall_array_query conv_dateq);
 use CGI;
 
 require "bin/mozilla/common.pl";
@@ -77,7 +85,6 @@ sub add_account {
   $form->{callback} = "am.pl?action=list_account" unless $form->{callback};
 
   &account_header;
-  &form_footer;
 
   $main::lxdebug->leave_sub();
 }
@@ -87,10 +94,17 @@ sub edit_account {
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
+  my $defaults = SL::DB::Default->get;
 
   $main::auth->assert('config');
 
   $form->{title} = "Edit";
+  $form->{feature_balance} = $defaults->feature_balance;
+  $form->{feature_datev} = $defaults->feature_datev;
+  $form->{feature_erfolgsrechnung} = $defaults->feature_erfolgsrechnung;
+  $form->{feature_eurechnung} = $defaults->feature_eurechnung;
+  $form->{feature_ustva} = $defaults->feature_ustva;
+
   AM->get_account(\%myconfig, \%$form);
 
   foreach my $item (split(/:/, $form->{link})) {
@@ -98,7 +112,6 @@ sub edit_account {
   }
 
   &account_header;
-  &form_footer;
 
   $main::lxdebug->leave_sub();
 }
@@ -214,38 +227,7 @@ sub account_header {
   }
 
   my $select_eur = q|<option value=""> |. $locale->text('None') .q|</option>\n|;
-  my %eur = (
-          1  => "Umsatzerlöse",
-          2  => "sonstige Erlöse",
-          3  => "Privatanteile",
-          4  => "Zinserträge",
-          5  => "Ausserordentliche Erträge",
-          6  => "Vereinnahmte Umsatzst.",
-          7  => "Umsatzsteuererstattungen",
-          8  => "Wareneingänge",
-          9  => "Löhne und Gehälter",
-          10 => "Gesetzl. sozialer Aufw.",
-          11 => "Mieten",
-          12 => "Gas, Strom, Wasser",
-          13 => "Instandhaltung",
-          14 => "Steuern, Versich., Beiträge",
-          15 => "Kfz-Steuern",
-          16 => "Kfz-Versicherungen",
-          17 => "Sonst. Fahrzeugkosten",
-          18 => "Werbe- und Reisekosten",
-          19 => "Instandhaltung u. Werkzeuge",
-          20 => "Fachzeitschriften, Bücher",
-          21 => "Miete für Einrichtungen",
-          22 => "Rechts- und Beratungskosten",
-          23 => "Bürobedarf, Porto, Telefon",
-          24 => "Sonstige Aufwendungen",
-          25 => "Abschreibungen auf Anlagever.",
-          26 => "Abschreibungen auf GWG",
-          27 => "Vorsteuer",
-          28 => "Umsatzsteuerzahlungen",
-          29 => "Zinsaufwand",
-          30 => "Ausserordentlicher Aufwand",
-          31 => "Betriebliche Steuern");
+  my %eur = %{ AM->get_eur_categories(\%myconfig, $form) };
   foreach my $item (sort({ $a <=> $b } keys(%eur))) {
     my $text = H($::locale->{iconv_utf8}->convert($eur{$item}));
     if ($item == $form->{pos_eur}) {
@@ -256,31 +238,23 @@ sub account_header {
 
   }
 
+  my $select_er = q|<option value=""> |. $locale->text('None') .q|</option>\n|;
+  my %er = (
+       1  => "Ertrag",
+       6  => "Aufwand");
+  foreach my $item (sort({ $a <=> $b } keys(%er))) {
+    my $text = H($::locale->{iconv_utf8}->convert($er{$item}));
+    if ($item == $form->{pos_er}) {
+      $select_er .= qq|<option value=$item selected>|. sprintf("%.2d", $item) .qq|. $text</option>\n|;
+    } else {
+      $select_er .= qq|<option value=$item>|. sprintf("%.2d", $item) .qq|. $text</option>\n|;
+    }
+
+  }
+
   my $select_bwa = q|<option value=""> |. $locale->text('None') .q|</option>\n|;
 
-  my %bwapos = (
-             1  => 'Umsatzerlöse',
-             2  => 'Best.Verdg.FE/UE',
-             3  => 'Aktiv.Eigenleistung',
-             4  => 'Mat./Wareneinkauf',
-             5  => 'So.betr.Erlöse',
-             10 => 'Personalkosten',
-             11 => 'Raumkosten',
-             12 => 'Betriebl.Steuern',
-             13 => 'Vers./Beiträge',
-             14 => 'Kfz.Kosten o.St.',
-             15 => 'Werbe-Reisek.',
-             16 => 'Kosten Warenabgabe',
-             17 => 'Abschreibungen',
-             18 => 'Rep./instandhlt.',
-             19 => 'Übrige Steuern',
-             20 => 'Sonst.Kosten',
-             30 => 'Zinsauwand',
-             31 => 'Sonst.neutr.Aufw.',
-             32 => 'Zinserträge',
-             33 => 'Sonst.neutr.Ertrag',
-             34 => 'Verr.kalk.Kosten',
-             35 => 'Steuern Eink.u.Ertr.');
+  my %bwapos = %{ AM->get_bwa_categories(\%myconfig, $form) };
   foreach my $item (sort({ $a <=> $b } keys %bwapos)) {
     my $text = H($::locale->{iconv_utf8}->convert($bwapos{$item}));
     if ($item == $form->{pos_bwa}) {
@@ -347,6 +321,8 @@ sub account_header {
   my $ChartTypeIsAccount = ($form->{charttype} eq "A") ? "1":"";
   my $AccountIsPosted = ($form->{orphaned} ) ? "":"1";
 
+  setup_am_edit_account_action_bar();
+
   $form->header();
 
   my $parameters_ref = {
@@ -358,6 +334,7 @@ sub account_header {
     select_bwa                 => $select_bwa,
     select_bilanz              => $select_bilanz,
     select_eur                 => $select_eur,
+    select_er                  => $select_er,
   };
 
   # Ausgabe des Templates
@@ -367,21 +344,6 @@ sub account_header {
   $main::lxdebug->leave_sub();
 }
 
-sub form_footer {
-  $::lxdebug->enter_sub;
-  $::auth->assert('config');
-
-  print $::form->parse_html_template('am/form_footer', {
-    show_save        => !$::form->{id}
-                     || ($::form->{id} && $::form->{orphaned})
-                     || ($::form->{type} eq "account" && !$::form->{new_chart_valid}),
-    show_delete      => $::form->{id} && $::form->{orphaned},
-    show_save_as_new => $::form->{id} && $::form->{type} eq "account",
-  });
-
-  $::lxdebug->leave_sub;
-}
-
 sub save_account {
   $main::lxdebug->enter_sub();
 
@@ -575,7 +537,7 @@ sub delete_account {
   $form->{title} = $locale->text('Delete Account');
 
   foreach my $id (
-    qw(inventory_accno_id income_accno_id expense_accno_id fxgain_accno_id fxloss_accno_id)
+    qw(inventory_accno_id income_accno_id expense_accno_id fxgain_accno_id fxloss_accno_id rndgain_accno_id rndloss_accno_id)
     ) {
     if ($form->{id} == $form->{$id}) {
       $form->error($locale->text('Cannot delete default account!'));
@@ -589,207 +551,6 @@ sub delete_account {
   $main::lxdebug->leave_sub();
 }
 
-sub add_lead {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-
-  $main::auth->assert('config');
-
-  $form->{title} = "Add";
-
-  $form->{callback} = "am.pl?action=add_lead" unless $form->{callback};
-
-  &lead_header;
-  &form_footer;
-
-  $main::lxdebug->leave_sub();
-}
-
-sub edit_lead {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-
-  $main::auth->assert('config');
-
-  $form->{title} = "Edit";
-
-  AM->get_lead(\%myconfig, \%$form);
-
-  &lead_header;
-
-  $form->{orphaned} = 1;
-  &form_footer;
-
-  $main::lxdebug->leave_sub();
-}
-
-sub list_lead {
-  $::lxdebug->enter_sub;
-  $::auth->assert('config');
-
-  AM->lead(\%::myconfig, $::form);
-
-  $::form->{callback} = "am.pl?action=list_lead";
-  $::form->{title}    = $::locale->text('Lead');
-
-  $::form->header;
-  print $::form->parse_html_template('am/lead_list');
-
-  $::lxdebug->leave_sub;
-}
-
-sub lead_header {
-  $::lxdebug->enter_sub;
-  $::auth->assert('config');
-
-  # $locale->text('Add Lead')
-  # $locale->text('Edit Lead')
-  $::form->{title} = $::locale->text("$::form->{title} Lead");
-
-  $::form->header;
-  print $::form->parse_html_template('am/lead_header');
-
-  $::lxdebug->leave_sub;
-}
-
-sub save_lead {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  $main::auth->assert('config');
-
-  $form->isblank("description", $locale->text('Description missing!'));
-  AM->save_lead(\%myconfig, \%$form);
-  $form->redirect($locale->text('lead saved!'));
-
-  $main::lxdebug->leave_sub();
-}
-
-sub delete_lead {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  $main::auth->assert('config');
-
-  AM->delete_lead(\%myconfig, \%$form);
-  $form->redirect($locale->text('lead deleted!'));
-
-  $main::lxdebug->leave_sub();
-}
-
-sub add_language {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-
-  $main::auth->assert('config');
-
-  $form->{title} = "Add";
-
-  $form->{callback} = "am.pl?action=add_language" unless $form->{callback};
-
-  &language_header;
-  &form_footer;
-
-  $main::lxdebug->leave_sub();
-}
-
-sub edit_language {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-
-  $main::auth->assert('config');
-
-  $form->{title} = "Edit";
-
-  AM->get_language(\%myconfig, \%$form);
-
-  &language_header;
-
-  $form->{orphaned} = 1;
-  &form_footer;
-
-  $main::lxdebug->leave_sub();
-}
-
-sub list_language {
-  $::lxdebug->enter_sub;
-  $::auth->assert('config');
-
-  AM->language(\%::myconfig, $::form);
-
-  $::form->{callback} = "am.pl?action=list_language";
-  $::form->{title}   = $::locale->text('Languages');
-
-  $::form->header;
-
-  print $::form->parse_html_template('am/language_list');
-
-  $::lxdebug->leave_sub;
-}
-
-sub language_header {
-  $::lxdebug->enter_sub;
-  $::auth->assert('config');
-
-  # $locale->text('Add Language')
-  # $locale->text('Edit Language')
-  $::form->{title} = $::locale->text("$::form->{title} Language");
-
-  $::form->header;
-
-  print $::form->parse_html_template('am/language_header', {
-    numberformats => [ '1,000.00', '1000.00', '1.000,00', '1000,00' ],
-    dateformats => [ qw(mm/dd/yy dd/mm/yy dd.mm.yy yyyy-mm-dd) ],
-  });
-
-  $::lxdebug->leave_sub;
-}
-
-sub save_language {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  $main::auth->assert('config');
-
-  $form->isblank("description", $locale->text('Language missing!'));
-  $form->isblank("template_code", $locale->text('Template Code missing!'));
-  $form->isblank("article_code", $locale->text('Article Code missing!'));
-  AM->save_language(\%myconfig, \%$form);
-  $form->redirect($locale->text('Language saved!'));
-
-  $main::lxdebug->leave_sub();
-}
-
-sub delete_language {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  $main::auth->assert('config');
-
-  AM->delete_language(\%myconfig, \%$form);
-  $form->redirect($locale->text('Language deleted!'));
-
-  $main::lxdebug->leave_sub();
-}
-
 sub _build_cfg_options {
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -813,10 +574,11 @@ sub config {
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
+  my $defaults = SL::DB::Default->get;
 
   _build_cfg_options('dateformat', qw(mm/dd/yy dd/mm/yy dd.mm.yy yyyy-mm-dd));
   _build_cfg_options('timeformat', qw(hh:mm hh:mm:ss));
-  _build_cfg_options('numberformat', ('1,000.00', '1000.00', '1.000,00', '1000,00'));
+  _build_cfg_options('numberformat', ('1,000.00', '1000.00', '1.000,00', '1000,00', "1'000.00"));
 
   my @formats = ();
   if ($::lx_office_conf{print_templates}->{opendocument}
@@ -883,15 +645,39 @@ sub config {
     };
   }
 
+  my $user_prefs = SL::Helper::UserPreferences->new(
+    namespace         => 'TopQuickSearch',
+  );
+  my $prefs_val;
+  my @quick_search_modules;
+  if ($user_prefs) {
+    $prefs_val            = $user_prefs->get('quick_search_modules');
+    @quick_search_modules = split ',', $prefs_val;
+  }
+
+  my $enabled_quick_search = [ SL::Controller::TopQuickSearch->new->available_modules ];
+  $form->{enabled_quick_searchmodules} = \@{$enabled_quick_search};
+  $form->{default_quick_searchmodules} = \@quick_search_modules;
+
+  $form->{displayable_name_specs_by_module}       = AM->displayable_name_specs_by_module();
+  $form->{positions_scrollbar_height}             = AM->positions_scrollbar_height();
+  $form->{purchase_search_makemodel}              = AM->purchase_search_makemodel();
+  $form->{sales_search_customer_partnumber}       = AM->sales_search_customer_partnumber();
+  $form->{positions_show_update_button}           = AM->positions_show_update_button();
+  $form->{time_recording_use_duration}            = AM->time_recording_use_duration();
+  $form->{longdescription_dialog_size_percentage} = AM->longdescription_dialog_size_percentage();
+
   $myconfig{show_form_details} = 1 unless (defined($myconfig{show_form_details}));
   $form->{CAN_CHANGE_PASSWORD} = $main::auth->can_change_password();
   $form->{todo_cfg}            = { TODO->get_user_config('login' => $::myconfig{login}) };
-
   $form->{title}               = $locale->text('Edit Preferences for #1', $::myconfig{login});
 
+  $::request->{layout}->use_javascript("${_}.js") for qw(jquery.multiselect2side ckeditor/ckeditor ckeditor/adapters/jquery);
+
+  setup_am_config_action_bar();
   $form->header();
 
-  $form->{full_signature} = $form->create_email_signature();
+  $form->{company_signature} = SL::DB::Default->get->signature;
 
   print $form->parse_html_template('am/config');
 
@@ -909,6 +695,11 @@ sub save_preferences {
 
   TODO->save_user_config('login' => $::myconfig{login}, %{ $form->{todo_cfg} || { } });
 
+  if ($form->{quick_search_modules}) {
+    my $user_prefs = SL::Helper::UserPreferences->new( namespace => 'TopQuickSearch',);
+    my $quick_search_modules = join ',', @{$form->{quick_search_modules}};
+    $user_prefs->store('quick_search_modules', $quick_search_modules);
+  }
   if (AM->save_preferences($form)) {
     if ($::auth->can_change_password()
         && defined $form->{new_password}
@@ -939,6 +730,8 @@ sub audit_control {
 
   AM->closedto(\%::myconfig, $::form);
 
+  setup_am_audit_control_action_bar();
+
   $::form->header;
   print $::form->parse_html_template('am/audit_control');
 
@@ -967,6 +760,29 @@ sub doclose {
   $main::lxdebug->leave_sub();
 }
 
+sub add_unit {
+  $::auth->assert('config');
+
+  # my $units = AM->retrieve_units(\%::myconfig, $::form, "resolved_");
+  # # AM->units_in_use(\%::myconfig, $::form, $units);
+
+  # $units->{$_}->{BASE_UNIT_DDBOX} = AM->unit_select_data($units, $units->{$_}->{base_unit}, 1) for keys %{$units};
+
+  my @languages = @{ SL::DB::Manager::Language->get_all_sorted };
+
+  my $units = AM->retrieve_units(\%::myconfig, $::form);
+  my $ddbox = AM->unit_select_data($units, undef, 1);
+
+  setup_am_add_unit_action_bar();
+
+  $::form->{title} = $::locale->text("Add unit");
+  $::form->header();
+  print($::form->parse_html_template("am/add_unit", {
+    NEW_BASE_UNIT_DDBOX => $ddbox,
+    LANGUAGES           => \@languages,
+  }));
+}
+
 sub edit_units {
   $main::lxdebug->enter_sub();
 
@@ -980,7 +796,7 @@ sub edit_units {
   AM->units_in_use(\%myconfig, $form, $units);
   map({ $units->{$_}->{"BASE_UNIT_DDBOX"} = AM->unit_select_data($units, $units->{$_}->{"base_unit"}, 1); } keys(%{$units}));
 
-  my @languages = AM->language(\%myconfig, $form, 1);
+  my @languages = @{ SL::DB::Manager::Language->get_all_sorted };
 
   my @unit_list = sort({ $a->{"sortkey"} <=> $b->{"sortkey"} } values(%{$units}));
 
@@ -990,11 +806,11 @@ sub edit_units {
     $_->{"UNITLANGUAGES"} = [];
     foreach my $lang (@languages) {
       push(@{ $_->{"UNITLANGUAGES"} },
-           { "idx" => $i,
-             "unit" => $_->{"name"},
-             "language_id" => $lang->{"id"},
-             "localized" => $_->{"LANGUAGES"}->{$lang->{"template_code"}}->{"localized"},
-             "localized_plural" => $_->{"LANGUAGES"}->{$lang->{"template_code"}}->{"localized_plural"},
+           { "idx"              => $i,
+             "unit"             => $_->{"name"},
+             "language_id"      => $lang->id,
+             "localized"        => $_->{"LANGUAGES"}->{$lang->template_code}->{"localized"},
+             "localized_plural" => $_->{"LANGUAGES"}->{$lang->template_code}->{"localized_plural"},
            });
     }
     $i++;
@@ -1003,7 +819,9 @@ sub edit_units {
   $units = AM->retrieve_units(\%myconfig, $form);
   my $ddbox = AM->unit_select_data($units, undef, 1);
 
-  $form->{"title"} = $locale->text("Add and edit units");
+  setup_am_edit_units_action_bar();
+
+  $form->{"title"} = $locale->text("Edit units");
   $form->header();
   print($form->parse_html_template("am/edit_units",
                                    { "UNITS"               => \@unit_list,
@@ -1014,7 +832,7 @@ sub edit_units {
   $main::lxdebug->leave_sub();
 }
 
-sub add_unit {
+sub create_unit {
   $main::lxdebug->enter_sub();
 
   my $form     = $main::form;
@@ -1039,19 +857,19 @@ sub add_unit {
   }
 
   my @languages;
-  foreach my $lang (AM->language(\%myconfig, $form, 1)) {
+  foreach my $lang (@{ SL::DB::Manager::Language->get_all_sorted }) {
     next unless ($form->{"new_localized_$lang->{id}"} || $form->{"new_localized_plural_$lang->{id}"});
-    push(@languages, { "id" => $lang->{"id"},
-                       "localized" => $form->{"new_localized_$lang->{id}"},
-                       "localized_plural" => $form->{"new_localized_plural_$lang->{id}"},
+    push(@languages, { "id"               => $lang->id,
+                       "localized"        => $form->{"new_localized_" . $lang->id},
+                       "localized_plural" => $form->{"new_localized_plural_" . $lang->id},
          });
   }
 
   AM->add_unit(\%myconfig, $form, $form->{"new_name"}, $base_unit, $factor, \@languages);
 
-  $form->{"saved_message"} = $locale->text("The unit has been saved.");
+  flash_later('info', $locale->text("The unit has been added."));
 
-  edit_units();
+  print $form->redirect_header('am.pl?action=edit_units');
 
   $main::lxdebug->leave_sub();
 }
@@ -1069,9 +887,9 @@ sub set_unit_languages {
 
   foreach my $lang (@{$languages}) {
     push(@{ $unit->{"LANGUAGES"} },
-         { "id" => $lang->{"id"},
-           "localized" => $form->{"localized_${idx}_$lang->{id}"},
-           "localized_plural" => $form->{"localized_plural_${idx}_$lang->{id}"},
+         { "id"               => $lang->id,
+           "localized"        => $form->{"localized_${idx}_" . $lang->id},
+           "localized_plural" => $form->{"localized_plural_${idx}_" . $lang->id},
          });
   }
 
@@ -1090,7 +908,7 @@ sub save_unit {
   my $old_units = AM->retrieve_units(\%myconfig, $form, "resolved_");
   AM->units_in_use(\%myconfig, $form, $old_units);
 
-  my @languages = AM->language(\%myconfig, $form, 1);
+  my @languages = @{ SL::DB::Manager::Language->get_all_sorted };
 
   my $new_units = {};
   my @delete_units = ();
@@ -1155,9 +973,9 @@ sub save_unit {
 
   AM->save_units(\%myconfig, $form, $new_units, \@delete_units);
 
-  $form->{"saved_message"} = $locale->text("The units have been saved.");
+  flash_later('info', $locale->text("The units have been saved."));
 
-  edit_units();
+  print $form->redirect_header('am.pl?action=edit_units');
 
   $main::lxdebug->leave_sub();
 }
@@ -1170,6 +988,8 @@ sub show_history_search {
 
   $main::auth->assert('config');
 
+  setup_am_show_history_search_action_bar();
+
   $form->{title} = $locale->text("History Search");
   $form->header();
 
@@ -1226,17 +1046,27 @@ sub show_am_history {
     $restriction  .= qq| AND employee_id = (SELECT id FROM employee WHERE name ILIKE | . $dbh->quote('%' . $form->{mitarbeiter} . '%') . qq|)|;
   }
 
-  my $query = qq|SELECT trans_id AS id FROM history_erp | .
-    (  $form->{'searchid'} ? qq| WHERE snumbers = '|  . $searchNo{$form->{'what2search'}} . qq|_| . $form->{'searchid'} . qq|'|
-     :                       qq| WHERE snumbers ~ '^| . $searchNo{$form->{'what2search'}} . qq|'|);
+  my $snumbers_where = '';
+  my $snumbers_value;
+  if ($form->{'searchid'}) {
+    $snumbers_where = ' WHERE snumbers = ?';
+    $snumbers_value = $searchNo{$form->{'what2search'}} . '_' . $form->{'searchid'};
+  } else {
+    $snumbers_where = ' WHERE snumbers ~ ?';
+    $snumbers_value = '^' . $searchNo{$form->{'what2search'}};
+  }
+  my $query = qq|SELECT trans_id AS id FROM history_erp $snumbers_where|;
 
-  my @ids    = grep { $_ * 1 } selectall_array_query($form, $dbh, $query);
+  my @ids    = grep { $_ * 1 } selectall_array_query($form, $dbh, $query, $snumbers_value);
   my $daten .= shift @ids;
-  $daten    .= join '', map { " OR trans_id = $_" } @ids;
-
+  if (scalar(@ids) > 0 ) {
+    $daten  .= ' OR trans_id IN (' . join(',', @ids) . ')';
+  }
   my ($sort, $sortby) = split(/\-\-/, $form->{order});
   $sort =~ s/.*\.(.*)$/$1/;
 
+  setup_am_show_am_history_action_bar();
+
   $form->{title} = $locale->text("History Search");
   $form->header();
 
@@ -1274,10 +1104,10 @@ sub add_tax {
   $form->{expense}    = 1;
   $form->{costs}      = 1;
 
+  setup_am_edit_tax_action_bar();
   $form->header();
 
   my $parameters_ref = {
-#    ChartTypeIsAccount         => $ChartTypeIsAccount,
     LANGUAGES => SL::DB::Manager::Language->get_all_sorted,
   };
 
@@ -1311,6 +1141,7 @@ sub edit_tax {
 
   $form->{rate} = $form->format_amount(\%myconfig, $form->{rate}, 2);
 
+  setup_am_edit_tax_action_bar();
   $form->header();
 
   my $parameters_ref = {
@@ -1341,13 +1172,11 @@ sub list_tax {
 
   $form->{title} = $locale->text('Tax-O-Matic');
 
+  setup_am_list_tax_action_bar();
   $form->header();
 
-  my $parameters_ref = {
-  };
-
   # Ausgabe des Templates
-  print($form->parse_html_template('am/list_tax', $parameters_ref));
+  print($form->parse_html_template('am/list_tax'));
 
   $main::lxdebug->leave_sub();
 }
@@ -1398,7 +1227,9 @@ sub save_tax {
   $form->{translations} = { map { $_ =~ '^translation_(\d+)'; $1 => $form->{$_} } @translation_keys };
 
   AM->save_tax(\%myconfig, \%$form);
-  $form->redirect($locale->text('Tax saved!'));
+  flash_later('info', $locale->text("Tax saved!"));
+
+  print $form->redirect_header('am.pl?action=list_tax');
 
   $main::lxdebug->leave_sub();
 }
@@ -1418,7 +1249,7 @@ sub delete_tax {
   $main::lxdebug->leave_sub();
 }
 
-sub add_price_factor {
+sub add_warehouse {
   $main::lxdebug->enter_sub();
 
   my $form     = $main::form;
@@ -1426,16 +1257,18 @@ sub add_price_factor {
 
   $main::auth->assert('config');
 
-  $form->{title}      = $locale->text('Add Price Factor');
-  $form->{callback} ||= build_std_url('action=add_price_factor');
+  $form->{title}      = $locale->text('Add Warehouse');
+  $form->{callback} ||= build_std_url('action=add_warehouse');
+
+  setup_am_edit_warehouse_action_bar();
 
   $form->header();
-  print $form->parse_html_template('am/edit_price_factor');
+  print $form->parse_html_template('am/edit_warehouse');
 
   $main::lxdebug->leave_sub();
 }
 
-sub edit_price_factor {
+sub edit_warehouse {
   $main::lxdebug->enter_sub();
 
   my $form     = $main::form;
@@ -1444,45 +1277,36 @@ sub edit_price_factor {
 
   $main::auth->assert('config');
 
-  $form->{title}      = $locale->text('Edit Price Factor');
-  $form->{callback} ||= build_std_url('action=add_price_factor');
+  AM->get_warehouse(\%myconfig, $form);
 
-  AM->get_price_factor(\%myconfig, $form);
+  $form->get_lists('employees' => 'EMPLOYEES');
 
-  $form->{factor} = $form->format_amount(\%myconfig, $form->{factor} * 1);
+  $form->{title}      = $locale->text('Edit Warehouse');
+  $form->{callback} ||= build_std_url('action=list_warehouses');
+
+  setup_am_edit_warehouse_action_bar(id => $::form->{id}, in_use => any { $_->{in_use} } @{ $::form->{BINS} });
 
   $form->header();
-  print $form->parse_html_template('am/edit_price_factor');
+  print $form->parse_html_template('am/edit_warehouse');
 
   $main::lxdebug->leave_sub();
 }
 
-sub list_price_factors {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  $main::auth->assert('config');
+sub edit_bins {
+  $::auth->assert('config');
 
-  AM->get_all_price_factors(\%myconfig, \%$form);
+  AM->get_warehouse(\%::myconfig, $::form);
 
-  foreach my $current (@{ $form->{PRICE_FACTORS} }) {
-    $current->{factor} = $form->format_amount(\%myconfig, $current->{factor} * 1);
-  }
-
-  $form->{callback} = build_std_url('action=list_price_factors');
-  $form->{title}    = $locale->text('Price Factors');
-  $form->{url_base} = build_std_url('callback');
+  $::form->{title}      = $::locale->text('Edit Bins for Warehouse \'#1\'', $::form->{description});
+  $::form->{callback} ||= build_std_url('action=list_warehouses');
 
-  $form->header();
-  print $form->parse_html_template('am/list_price_factors');
+  setup_am_edit_bins_action_bar(id => $::form->{id});
 
-  $main::lxdebug->leave_sub();
+  $::form->header;
+  print $::form->parse_html_template('am/edit_bins');
 }
 
-sub save_price_factor {
+sub list_warehouses {
   $main::lxdebug->enter_sub();
 
   my $form     = $main::form;
@@ -1491,21 +1315,21 @@ sub save_price_factor {
 
   $main::auth->assert('config');
 
-  $form->isblank("description", $locale->text('Description missing!'));
-  $form->isblank("factor", $locale->text('Factor missing!'));
-
-  $form->{factor} = $form->parse_amount(\%myconfig, $form->{factor});
+  AM->get_all_warehouses(\%myconfig, $form);
 
-  AM->save_price_factor(\%myconfig, $form);
+  $form->{callback} = build_std_url('action=list_warehouses');
+  $form->{title}    = $locale->text('Warehouses');
+  $form->{url_base} = build_std_url('callback');
 
-  $form->{callback} .= '&MESSAGE=' . $form->escape($locale->text('Price factor saved!')) if ($form->{callback});
+  setup_am_list_warehouses_action_bar();
 
-  $form->redirect($locale->text('Price factor saved!'));
+  $form->header();
+  print $form->parse_html_template('am/list_warehouses');
 
   $main::lxdebug->leave_sub();
 }
 
-sub delete_price_factor {
+sub save_warehouse {
   $main::lxdebug->enter_sub();
 
   my $form     = $main::form;
@@ -1514,33 +1338,41 @@ sub delete_price_factor {
 
   $main::auth->assert('config');
 
-  AM->delete_price_factor(\%myconfig, \%$form);
+  $form->isblank("description", $locale->text('Description missing!'));
+  $form->isblank("number_of_new_bins", $locale->text('Number')  . $locale->text(' missing!'));
 
-  $form->{callback} .= '&MESSAGE=' . $form->escape($locale->text('Price factor deleted!')) if ($form->{callback});
+  $form->{number_of_new_bins} = $form->parse_amount(\%myconfig, $form->{number_of_new_bins});
 
-  $form->redirect($locale->text('Price factor deleted!'));
+  AM->save_warehouse(\%myconfig, $form);
+
+  $form->{callback} .= '&saved_message=' . E($locale->text('Warehouse saved.')) if ($form->{callback});
+
+  $form->redirect($locale->text('Warehouse saved.'));
 
   $main::lxdebug->leave_sub();
 }
 
-sub add_warehouse {
+sub delete_warehouse {
   $main::lxdebug->enter_sub();
 
   my $form     = $main::form;
+  my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
   $main::auth->assert('config');
 
-  $form->{title}      = $locale->text('Add Warehouse');
-  $form->{callback} ||= build_std_url('action=add_warehouse');
+  if (AM->delete_warehouse(\%myconfig, $form)) {
+    $form->{callback} .= '&saved_message=' . E($locale->text('Warehouse deleted.')) if ($form->{callback});
+    $form->redirect($locale->text('Warehouse deleted.'));
 
-  $form->header();
-  print $form->parse_html_template('am/edit_warehouse');
+  } else {
+    $form->error($locale->text('The warehouse could not be deleted because it has already been used.'));
+  }
 
   $main::lxdebug->leave_sub();
 }
 
-sub edit_warehouse {
+sub save_bin {
   $main::lxdebug->enter_sub();
 
   my $form     = $main::form;
@@ -1549,104 +1381,246 @@ sub edit_warehouse {
 
   $main::auth->assert('config');
 
-  AM->get_warehouse(\%myconfig, $form);
-
-  $form->get_lists('employees' => 'EMPLOYEES');
+  AM->save_bins(\%myconfig, $form);
 
-  $form->{title}      = $locale->text('Edit Warehouse');
-  $form->{callback} ||= build_std_url('action=list_warehouses');
+  $form->{callback} .= '&saved_message=' . E($locale->text('Bins saved.')) if ($form->{callback});
 
-  $form->header();
-  print $form->parse_html_template('am/edit_warehouse');
+  $form->redirect($locale->text('Bins saved.'));
 
   $main::lxdebug->leave_sub();
 }
 
-sub list_warehouses {
-  $main::lxdebug->enter_sub();
+sub setup_am_config_action_bar {
+  my %params = @_;
 
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  $main::auth->assert('config');
-
-  AM->get_all_warehouses(\%myconfig, $form);
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => "save_preferences" } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
 
-  $form->{callback} = build_std_url('action=list_warehouses');
-  $form->{title}    = $locale->text('Warehouses');
-  $form->{url_base} = build_std_url('callback');
+sub setup_am_edit_account_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [
+          t8('Save'),
+          submit    => [ '#form', { action => "save_account" } ],
+          accesskey => 'enter',
+        ],
+
+        action => [
+          t8('Save as new'),
+          submit   => [ '#form', { action => "save_as_new_account" } ],
+          disabled => !$::form->{id} ? t8('The object has not been saved yet.') : undef,
+        ],
+      ],
+
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => "delete_account" } ],
+        disabled => !$::form->{id}                         ? t8('The object has not been saved yet.')
+                  :  $::form->{id} && !$::form->{orphaned} ? t8('The object is in use and cannot be deleted.')
+                  :                                          undef,
+        confirm  => t8('Do you really want to delete this object?'),
+      ],
+    );
+  }
+}
 
-  $form->header();
-  print $form->parse_html_template('am/list_warehouses');
+sub setup_am_list_tax_action_bar {
+  my %params = @_;
 
-  $main::lxdebug->leave_sub();
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      link => [
+        t8('Add'),
+        link => 'am.pl?action=add_tax',
+      ],
+    );
+  }
 }
 
-sub save_warehouse {
-  $main::lxdebug->enter_sub();
+sub setup_am_edit_tax_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => "save_tax" } ],
+        accesskey => 'enter',
+      ],
+
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => "delete_tax" } ],
+        disabled => !$::form->{id}                                      ? t8('The object has not been saved yet.')
+                  : !$::form->{orphaned} || $::form->{tax_already_used} ? t8('The object is in use and cannot be deleted.')
+                  :                                                       undef,
+        confirm  => t8('Do you really want to delete this object?'),
+      ],
+    );
+  }
+}
 
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
+sub setup_am_add_unit_action_bar {
+  my %params = @_;
 
-  $main::auth->assert('config');
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => "create_unit" } ],
+        accesskey => 'enter',
+      ],
 
-  $form->isblank("description", $locale->text('Description missing!'));
+      'separator',
 
-  $form->{number_of_new_bins} = $form->parse_amount(\%myconfig, $form->{number_of_new_bins});
+      link => [
+        t8('Back'),
+        link => 'am.pl?action=edit_units',
+      ],
+    );
+  }
+}
 
-  AM->save_warehouse(\%myconfig, $form);
+sub setup_am_edit_units_action_bar {
+  my %params = @_;
 
-  $form->{callback} .= '&saved_message=' . E($locale->text('Warehouse saved.')) if ($form->{callback});
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => "save_unit" } ],
+        accesskey => 'enter',
+      ],
 
-  $form->redirect($locale->text('Warehouse saved.'));
+      'separator',
 
-  $main::lxdebug->leave_sub();
+      link => [
+        t8('Add'),
+        link => 'am.pl?action=add_unit',
+      ],
+    );
+  }
 }
 
-sub delete_warehouse {
-  $main::lxdebug->enter_sub();
+sub setup_am_list_warehouses_action_bar {
+  my %params = @_;
 
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      link => [
+        t8('Add'),
+        link      => 'am.pl?action=add&type=warehouse&callback=' . E($::form->{callback}),
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
 
-  $main::auth->assert('config');
+sub setup_am_edit_warehouse_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => 'save_warehouse' } ],
+        accesskey => 'enter',
+      ],
+
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => 'delete_warehouse' } ],
+        disabled => !$params{id}    ? t8('The object has not been saved yet.')
+                  : $params{in_use} ? t8('The object is in use and cannot be deleted.')
+                  :                   undef,
+        confirm  => t8('Do you really want to delete this object?'),
+      ],
+
+      'separator',
+
+      link => [
+        t8('Bins'),
+        link    => 'am.pl?action=edit_bins&id=' . E($params{id}),
+        only_if => $params{id},
+      ],
+
+      link => [
+        t8('Abort'),
+        link => $::form->{callback} || 'am.pl?action=list_warehouses',
+      ],
+    );
+  }
+}
 
-  if (!$form->{confirmed}) {
-    $form->{title} = $locale->text('Confirmation');
+sub setup_am_edit_bins_action_bar {
+  my %params = @_;
 
-    $form->header();
-    print $form->parse_html_template('am/confirm_delete_warehouse');
-    ::end_of_request();
-  }
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => 'save_bin' } ],
+        accesskey => 'enter',
+      ],
 
-  if (AM->delete_warehouse(\%myconfig, $form)) {
-    $form->{callback} .= '&saved_message=' . E($locale->text('Warehouse deleted.')) if ($form->{callback});
-    $form->redirect($locale->text('Warehouse deleted.'));
+      'separator',
 
-  } else {
-    $form->error($locale->text('The warehouse could not be deleted because it has already been used.'));
+      link => [
+        t8('Abort'),
+        link => 'am.pl?action=edit_warehouse&id=' . E($params{id}),
+      ],
+    );
   }
-
-  $main::lxdebug->leave_sub();
 }
 
-sub save_bin {
-  $main::lxdebug->enter_sub();
+sub setup_am_audit_control_action_bar {
+  my %params = @_;
 
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  $main::auth->assert('config');
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => 'doclose' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
 
-  AM->save_bins(\%myconfig, $form);
+sub setup_am_show_history_search_action_bar {
+  my %params = @_;
 
-  $form->{callback} .= '&saved_message=' . E($locale->text('Bins saved.')) if ($form->{callback});
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Show'),
+        submit    => [ '#form' ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
 
-  $form->redirect($locale->text('Bins saved.'));
+sub setup_am_show_am_history_action_bar {
+  my %params = @_;
 
-  $main::lxdebug->leave_sub();
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Back'),
+        call => [ 'kivi.history_back' ],
+      ],
+    );
+  }
 }
index 0045635..0917769 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # administration
@@ -36,6 +37,7 @@ use File::Find;
 use SL::DB::Default;
 use SL::AM;
 use SL::Form;
+use SL::Locale::String qw(t8);
 
 use Data::Dumper;
 
@@ -94,7 +96,7 @@ sub save_template {
 
   $main::auth->assert('admin');
 
-  $form->isblank("formname", $locale->text("You're not editing a file.")) unless ($form->{type} eq "stylesheet");
+  $form->isblank("formname", $locale->text("You're not editing a file."));
 
   my ($filename) = AM->prepare_template_filename(\%myconfig, $form);
   if (my $error = AM->save_template($filename, $form->{content})) {
@@ -125,7 +127,7 @@ sub display_template_form {
 
   my $format = $form->{"format"} eq "html" ? "html" : "tex";
 
-  $form->{"title"} = $form->{"type"} eq "stylesheet" ? $locale->text("Edit the stylesheet") : $locale->text("Edit templates");
+  $form->{"title"} = $locale->text("Edit templates");
   if ($form->{"format"}) {
       $form->{"title"} = uc($form->{"format"}) . " - " . $form->{"title"};
   }
@@ -134,7 +136,7 @@ sub display_template_form {
 
   my @hidden = qw(type format);
 
-  if (($form->{"type"} ne "stylesheet") && !$form->{"edit"}) {
+  if (!$form->{"edit"}) {
     $options{"SHOW_EDIT_OPTIONS"} = 1;
 
     #
@@ -265,7 +267,7 @@ sub display_template_form {
     push(@hidden, qw(formname language printer));
   }
 
-  if ($form->{formname} || ($form->{type} eq "stylesheet")) {
+  if ($form->{formname}) {
     $options{"SHOW_CONTENT"} = 1;
 
     ($options{"filename"}, $options{"display_filename"})
@@ -277,17 +279,50 @@ sub display_template_form {
     $options{"CAN_EDIT"} = $form->{"edit"};
 
     if (!$form->{edit}) {
-      $options{"content"}                 = "\n\n" if (!$options{"content"});
-      $options{"SHOW_SECOND_EDIT_BUTTON"} = $options{"lines"} > 25;
+      $options{"content"} = "\n\n" if (!$options{"content"});
     }
   }
 
   $options{"HIDDEN"} = [ map(+{ "name" => $_, "value" => $form->{$_} }, @hidden) ];
 
+  setup_amtemplates_display_form_action_bar(
+    mode              => $form->{edit} ? 'edit' : 'show',
+    template_selected => $options{SHOW_CONTENT},
+  );
+
   $form->header;
   print($form->parse_html_template("am/edit_templates", \%options));
 
   $main::lxdebug->leave_sub();
 }
 
+sub setup_amtemplates_display_form_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Edit'),
+        submit    => [ '#form', { action => 'edit_template' } ],
+        accesskey => 'enter',
+        only_if   => $params{mode} eq 'show',
+        disabled  => !$params{template_selected} ? t8('No template has been selected yet.') : undef,
+      ],
+
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => 'save_template' } ],
+        accesskey => 'enter',
+        only_if   => $params{mode} eq 'edit',
+      ],
+
+      action => [
+        t8('Abort'),
+        call    => [ 'kivi.history_back' ],
+        only_if => $params{mode} eq 'edit',
+      ],
+    );
+  }
+}
+
 1;
index 1070c67..0e3048a 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Accounts Payables
 #======================================================================
 
 use POSIX qw(strftime);
-use List::Util qw(max sum);
+use List::Util qw(first max sum);
 use List::UtilsBy qw(sort_by);
 
 use SL::AP;
 use SL::FU;
+use SL::GL;
+use SL::Helper::Flash qw(flash flash_later);
 use SL::IR;
 use SL::IS;
-use SL::PE;
 use SL::ReportGenerator;
+use SL::DB::BankTransactionAccTrans;
+use SL::DB::Chart;
+use SL::DB::Currency;
 use SL::DB::Default;
+use SL::DB::Order;
+use SL::DB::PaymentTerm;
 use SL::DB::PurchaseInvoice;
+use SL::DB::RecordTemplate;
+use SL::DB::Tax;
+use SL::Webdav;
+use SL::Locale::String qw(t8);
 
-require "bin/mozilla/arap.pl";
 require "bin/mozilla/common.pl";
-require "bin/mozilla/drafts.pl";
 require "bin/mozilla/reportgenerator.pl";
 
 use strict;
@@ -83,24 +92,179 @@ use strict;
 # $locale->text('Nov')
 # $locale->text('Dec')
 
+sub _may_view_or_edit_this_invoice {
+  return 1 if  $::auth->assert('ap_transactions', 1); # may edit all invoices
+  return 0 if !$::form->{id};                         # creating new invoices isn't allowed without invoice_edit
+  return 0 if !$::form->{globalproject_id};           # existing records without a project ID are not allowed
+  return SL::DB::Project->new(id => $::form->{globalproject_id})->load->may_employee_view_project_invoices(SL::DB::Manager::Employee->current);
+}
+
+sub _assert_access {
+  my $cache = $::request->cache('ap.pl::_assert_access');
+
+  $cache->{_may_view_or_edit_this_invoice} = _may_view_or_edit_this_invoice()                              if !exists $cache->{_may_view_or_edit_this_invoice};
+  $::form->show_generic_error($::locale->text("You do not have the permissions to access this function.")) if !       $cache->{_may_view_or_edit_this_invoice};
+}
+
+sub load_record_template {
+  $::auth->assert('ap_transactions');
+
+  # Load existing template and verify that its one for this module.
+  my $template = SL::DB::RecordTemplate
+    ->new(id => $::form->{id})
+    ->load(
+      with_object => [ qw(customer payment currency record_items record_items.chart) ],
+    );
+
+  die "invalid template type" unless $template->template_type eq 'ap_transaction';
+
+  $template->substitute_variables;
+
+  # Clean the current $::form before rebuilding it from the template.
+  my $form_defaults = delete $::form->{form_defaults};
+  delete @{ $::form }{ grep { !m{^(?:script|login)$}i } keys %{ $::form } };
+
+  # Fill $::form from the template.
+  my $today                   = DateTime->today_local;
+  $::form->{title}            = "Add";
+  $::form->{currency}         = $template->currency->name;
+  $::form->{direct_debit}     = $template->direct_debit;
+  $::form->{globalproject_id} = $template->project_id;
+  $::form->{payment_id}       = $template->payment_id;
+  $::form->{AP_chart_id}      = $template->ar_ap_chart_id;
+  $::form->{transdate}        = $today->to_kivitendo;
+  $::form->{duedate}          = $today->to_kivitendo;
+  $::form->{rowcount}         = @{ $template->items };
+  $::form->{paidaccounts}     = 1;
+  $::form->{$_}               = $template->$_ for qw(department_id ordnumber taxincluded notes transaction_description);
+
+  if ($template->vendor) {
+    $::form->{vendor_id} = $template->vendor_id;
+    $::form->{vendor}    = $template->vendor->name;
+    $::form->{duedate}   = $template->vendor->payment->calc_date(reference_date => $today)->to_kivitendo if $template->vendor->payment;
+  }
+
+  my $row = 0;
+  foreach my $item (@{ $template->items }) {
+    $row++;
+
+    my $active_taxkey = $item->chart->get_active_taxkey;
+    my $taxes         = SL::DB::Manager::Tax->get_all(
+      where   => [ chart_categories => { like => '%' . $item->chart->category . '%' }],
+      sort_by => 'taxkey, rate',
+    );
+
+    my $tax   = first { $item->tax_id          == $_->id } @{ $taxes };
+    $tax    //= first { $active_taxkey->tax_id == $_->id } @{ $taxes };
+    $tax    //= $taxes->[0];
+
+    if (!$tax) {
+      $row--;
+      next;
+    }
+
+    $::form->{"AP_amount_chart_id_${row}"}          = $item->chart_id;
+    $::form->{"previous_AP_amount_chart_id_${row}"} = $item->chart_id;
+    $::form->{"amount_${row}"}                      = $::form->format_amount(\%::myconfig, $item->amount1, 2);
+    $::form->{"taxchart_${row}"}                    = $item->tax_id . '--' . $tax->rate;
+    $::form->{"project_id_${row}"}                  = $item->project_id;
+  }
+
+  $::form->{$_} = $form_defaults->{$_} for keys %{ $form_defaults // {} };
+
+  flash('info', $::locale->text("The record template '#1' has been loaded.", $template->template_name));
+  flash('info', $::locale->text("Payment bookings disallowed. After the booking this record may be " .
+                                "suggested with the amount of '#1' or otherwise has to be choosen manually." .
+                                " No automatic payment booking will be done to chart '#2'.",
+                                  $form_defaults->{paid_1_suggestion},
+                                  $form_defaults->{AP_paid_1_suggestion},
+                                )) if $::form->{no_payment_bookings};
+
+  update(
+    keep_rows_without_amount => 1,
+    dont_add_new_row         => 1,
+  );
+}
+
+sub save_record_template {
+  $::auth->assert('ap_transactions');
+
+  my $template = $::form->{record_template_id} ? SL::DB::RecordTemplate->new(id => $::form->{record_template_id})->load : SL::DB::RecordTemplate->new;
+  my $js       = SL::ClientJS->new(controller => SL::Controller::Base->new);
+  my $new_name = $template->template_name_to_use($::form->{record_template_new_template_name});
+
+  $js->dialog->close('#record_template_dialog');
+
+  my @items = grep {
+    $_->{chart_id} && (($_->{tax_id} // '') ne '')
+  } map {
+    +{ chart_id   => $::form->{"AP_amount_chart_id_${_}"},
+       amount1    => $::form->parse_amount(\%::myconfig, $::form->{"amount_${_}"}),
+       tax_id     => (split m{--}, $::form->{"taxchart_${_}"})[0],
+       project_id => $::form->{"project_id_${_}"} || undef,
+     }
+  } (1..($::form->{rowcount} || 1));
+
+  $template->assign_attributes(
+    template_type           => 'ap_transaction',
+    template_name           => $new_name,
+
+    currency_id             => SL::DB::Manager::Currency->find_by(name => $::form->{currency})->id,
+    ar_ap_chart_id          => $::form->{AP_chart_id}      || undef,
+    vendor_id               => $::form->{vendor_id}        || undef,
+    department_id           => $::form->{department_id}    || undef,
+    project_id              => $::form->{globalproject_id} || undef,
+    payment_id              => $::form->{payment_id}       || undef,
+    taxincluded             => $::form->{taxincluded}  ? 1 : 0,
+    direct_debit            => $::form->{direct_debit} ? 1 : 0,
+    ordnumber               => $::form->{ordnumber},
+    notes                   => $::form->{notes},
+    transaction_description => $::form->{transaction_description},
+
+    items                   => \@items,
+  );
+
+  eval {
+    $template->save;
+    1;
+  } or do {
+    return $js
+      ->flash('error', $::locale->text("Saving the record template '#1' failed.", $new_name))
+      ->render;
+  };
+
+  return $js
+    ->flash('info', $::locale->text("The record template '#1' has been saved.", $new_name))
+    ->render;
+}
+
 sub add {
   $main::lxdebug->enter_sub();
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  $main::auth->assert('general_ledger');
-
-  return $main::lxdebug->leave_sub() if (load_draft_maybe());
+  $main::auth->assert('ap_transactions');
 
   $form->{title} = "Add";
 
-  $form->{callback} = "ap.pl?action=add&DONT_LOAD_DRAFT=1" unless $form->{callback};
+  $form->{callback} = "ap.pl?action=add" unless $form->{callback};
 
   AP->get_transdate(\%myconfig, $form);
   $form->{initial_transdate} = $form->{transdate};
   create_links(dont_save => 1);
   $form->{transdate} = $form->{initial_transdate};
+
+  if ($form->{vendor_id}) {
+    my $vendor = SL::DB::Vendor->load_cached($form->{vendor_id});
+
+    # set initial payment terms
+    $form->{payment_id} = $vendor->payment_id;
+
+    my $last_used_ap_chart = $vendor->last_used_ap_chart;
+    $form->{"AP_amount_chart_id_1"} = $last_used_ap_chart->id if $last_used_ap_chart;
+  }
+
   &display_form;
 
   $main::lxdebug->leave_sub();
@@ -109,9 +273,11 @@ sub add {
 sub edit {
   $main::lxdebug->enter_sub();
 
-  my $form     = $main::form;
+  # Delay access check to after the invoice's been loaded in
+  # "create_links" so that project-specific invoice rights can be
+  # evaluated.
 
-  $main::auth->assert('general_ledger');
+  my $form     = $main::form;
 
   $form->{title} = "Edit";
 
@@ -124,10 +290,22 @@ sub edit {
 sub display_form {
   $main::lxdebug->enter_sub();
 
-  my $form     = $main::form;
+  _assert_access();
 
-  $main::auth->assert('general_ledger');
+  my $form     = $main::form;
 
+  # get all files stored in the webdav folder
+  if ($form->{invnumber} && $::instance_conf->get_webdav) {
+    my $webdav = SL::Webdav->new(
+      type     => 'accounts_payable',
+      number   => $form->{invnumber},
+    );
+    my @all_objects = $webdav->get_all_objects;
+    @{ $form->{WEBDAV} } = map { { name => $_->filename,
+                                   type => t8('File'),
+                                   link => File::Spec->catfile($_->full_filedescriptor),
+                               } } @all_objects;
+  }
   &form_header;
   &form_footer;
 
@@ -137,25 +315,31 @@ sub display_form {
 sub create_links {
   $main::lxdebug->enter_sub();
 
+  # Delay access check to after the invoice's been loaded so that
+  # project-specific invoice rights can be evaluated.
+
   my %params   = @_;
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  $main::auth->assert('general_ledger');
-
   $form->create_links("AP", \%myconfig, "vendor");
+
+  _assert_access();
+
   my %saved;
   if (!$params{dont_save}) {
     %saved = map { ($_ => $form->{$_}) } qw(direct_debit taxincluded);
     $saved{duedate} = $form->{duedate} if $form->{duedate};
+    $saved{currency} = $form->{currency} if $form->{currency};
+    $saved{taxincluded} = $form->{taxincluded} if $form->{taxincluded};
   }
 
   IR->get_vendor(\%myconfig, \%$form);
 
   $form->{$_}        = $saved{$_} for keys %saved;
-  $form->{oldvendor} = "$form->{vendor}--$form->{vendor_id}";
   $form->{rowcount}  = 1;
+  $form->{AP_chart_id} = $form->{acc_trans} && $form->{acc_trans}->{AP} ? $form->{acc_trans}->{AP}->[0]->{chart_id} : $::instance_conf->get_ap_chart_id || $form->{AP_links}->{AP}->[0]->{chart_id};
 
   # build the popup menus
   $form->{taxincluded} = ($form->{id}) ? $form->{taxincluded} : "checked";
@@ -163,34 +347,17 @@ sub create_links {
   # currencies
   $form->{defaultcurrency} = $form->get_default_currency(\%myconfig);
 
-  map { my $quoted = H($_); $form->{selectcurrency} .= "<option value=\"${quoted}\">${quoted}\n" } $form->get_all_currencies(\%myconfig);
-
-  # vendors
-  if (@{ $form->{all_vendor} || [] }) {
-    $form->{vendor} = qq|$form->{vendor}--$form->{vendor_id}|;
-    map { my $quoted = H($_->{name} . "--" . $_->{id}); $form->{selectvendor} .= "<option value=\"${quoted}\">${quoted}\n" }
-      (@{ $form->{all_vendor} });
-  }
-
-  # departments
-  if (@{ $form->{all_departments} || [] }) {
-    $form->{department}       = "$form->{department}--$form->{department_id}";
-    $form->{selectdepartment} = "<option>\n" . join('', map { my $quoted = H("$_->{description}--$_->{id}"); "<option value=\"${quoted}\">${quoted}\n"} @{ $form->{all_departments} || [] });
-  }
+  $::form->{ALL_DEPARTMENTS} = SL::DB::Manager::Department->get_all_sorted;
 
   $form->{employee} = "$form->{employee}--$form->{employee_id}";
 
   AP->setup_form($form);
 
-  $form->{locked} =
-    ($form->datetonum($form->{transdate}, \%myconfig) <=
-     $form->datetonum($form->{closedto}, \%myconfig));
-
   $main::lxdebug->leave_sub();
 }
 
 sub _sort_payments {
-  my @fields   = qw(acc_trans_id gldate datepaid source memo paid AR_paid paid_project_id);
+  my @fields   = qw(acc_trans_id gldate datepaid source memo paid AP_paid paid_project_id);
   my @payments =
     grep { $_->{paid} != 0 }
     map  {
@@ -211,27 +378,23 @@ sub _sort_payments {
 sub form_header {
   $main::lxdebug->enter_sub();
 
+  _assert_access();
+
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
   my $cgi      = $::request->{cgi};
 
-  $main::auth->assert('general_ledger');
-
   $::form->{invoice_obj} = SL::DB::PurchaseInvoice->new(id => $::form->{id})->load if $::form->{id};
 
+  $form->{initial_focus} = !($form->{amount_1} * 1) ? 'vendor_id' : 'row_' . $form->{rowcount};
+
   $form->{title_} = $form->{title};
   $form->{title} = $form->{title} eq 'Add' ? $locale->text('Add Accounts Payables Transaction') : $locale->text('Edit Accounts Payables Transaction');
 
   # type=submit $locale->text('Add Accounts Payables Transaction')
   # type=submit $locale->text('Edit Accounts Payables Transaction')
 
-  # set option selected
-  foreach my $item (qw(vendor currency department)) {
-    my $to_replace         =  H($form->{$item});
-    $form->{"select$item"} =~ s/ selected//;
-    $form->{"select$item"} =~ s/>\Q${to_replace}\E/ selected>${to_replace}/;
-  }
   my $readonly = $form->{id} ? "readonly" : "";
 
   $form->{radier} = ($::instance_conf->get_ap_changeable == 2)
@@ -259,75 +422,51 @@ sub form_header {
 
   $form->{creditremaining_plus} = ($form->{creditremaining} =~ /-/) ? "0" : "1";
 
-  my @old_project_ids = ();
-  map(
-    {
-      if ($form->{"project_id_$_"}) {
-        push(@old_project_ids, $form->{"project_id_$_"});
-      }
-    }
-    (1..$form->{"rowcount"})
-  );
-
-  $form->get_lists("projects"  => { "key"       => "ALL_PROJECTS",
-                                    "all"       => 0,
-                                    "old_id"    => \@old_project_ids },
-                   "charts"    => { "key"       => "ALL_CHARTS",
+  $form->get_lists("charts"    => { "key"       => "ALL_CHARTS",
                                     "transdate" => $form->{transdate} },
-                   "taxcharts" => { "key"       => "ALL_TAXCHARTS",
-                                    "module"    => "AP" },);
+                  );
 
   map(
     { $_->{link_split} = [ split(/:/, $_->{link}) ]; }
     @{ $form->{ALL_CHARTS} }
   );
 
-  my %project_labels = ();
-  foreach my $item (@{ $form->{"ALL_PROJECTS"} }) {
-    $project_labels{$item->{id}} = $item->{projectnumber};
-  }
+  $form->{ALL_DEPARTMENTS} = SL::DB::Manager::Department->get_all_sorted;
+
+  my %project_labels = map { $_->id => $_->projectnumber }  @{ SL::DB::Manager::Project->get_all };
 
   my %charts;
-  my $taxchart_init;
+  my $default_ap_amount_chart_id;
 
   foreach my $item (@{ $form->{ALL_CHARTS} }) {
     if ( grep({ $_ eq 'AP_amount' } @{ $item->{link_split} }) ) {
-      if ( $taxchart_init eq '' ) {
-        $taxchart_init = $item->{tax_id};
-      }
+      $default_ap_amount_chart_id //= $item->{id};
 
-      push(@{ $form->{ALL_CHARTS_AP_amount} }, $item);
-    }
-    elsif ( grep({ $_ eq 'AP' } @{ $item->{link_split} }) ) {
-      push(@{ $form->{ALL_CHARTS_AP} }, $item);
-    }
-    elsif ( grep({ $_ eq 'AP_paid' } @{ $item->{link_split} }) ) {
+    } elsif ( grep({ $_ eq 'AP_paid' } @{ $item->{link_split} }) ) {
       push(@{ $form->{ALL_CHARTS_AP_paid} }, $item);
     }
 
     $charts{$item->{accno}} = $item;
   }
 
-  my %taxcharts = ();
-  foreach my $item (@{ $form->{ALL_TAXCHARTS} }) {
-    my $key = $item->{id} .'--'. $item->{rate};
-
-    if ( $taxchart_init eq $item->{id} ) {
-      $taxchart_init = $key;
-    }
-
-    $taxcharts{$item->{id}} = $item;
-  }
-
-  my $follow_up_vc         =  $form->{vendor};
-  $follow_up_vc            =~ s/--.*?//;
+  my $follow_up_vc         = $form->{vendor_id} ? SL::DB::Vendor->load_cached($form->{vendor_id})->name : '';
   my $follow_up_trans_info =  "$form->{invnumber} ($follow_up_vc)";
 
-  $form->{javascript} .= qq|<script type="text/javascript" src="js/common.js"></script>|;
-  $form->{javascript} .= qq|<script type="text/javascript" src="js/show_vc_details.js"></script>|;
-  $form->{javascript} .= qq|<script type="text/javascript" src="js/follow_up.js"></script>|;
+  $::request->layout->add_javascripts("autocomplete_chart.js", "show_vc_details.js", "show_history.js", "follow_up.js", "kivi.Draft.js", "kivi.SalesPurchase.js", "kivi.GL.js", "kivi.RecordTemplate.js", "kivi.File.js", "kivi.AP.js", "kivi.CustomerVendor.js", "kivi.Validator.js", "autocomplete_project.js");
+  # $form->{totalpaid} is used by the action bar setup to determine
+  # whether or not canceling is allowed. Therefore it must be
+  # calculated prior to the action bar setup.
+  $form->{totalpaid} = sum map { $form->{"paid_${_}"} } (1..$form->{paidaccounts});
+
+  setup_ap_display_form_action_bar();
 
   $form->header();
+  # get the correct date for tax
+  my $transdate    = $::form->{transdate}    ? DateTime->from_kivitendo($::form->{transdate})    : DateTime->today_local;
+  my $deliverydate = $::form->{deliverydate} ? DateTime->from_kivitendo($::form->{deliverydate}) : undef;
+  my $taxdate      = $deliverydate ? $deliverydate : $transdate;
+  # helper for loop
+  my $first_taxchart;
 
   for my $i (1 .. $form->{rowcount}) {
 
@@ -335,42 +474,44 @@ sub form_header {
     $form->{"amount_$i"} = $form->format_amount(\%myconfig, $form->{"amount_$i"}, 2);
     $form->{"tax_$i"} = $form->format_amount(\%myconfig, $form->{"tax_$i"}, 2);
 
-    my $selected_accno_full;
-    my ($accno_row) = split(/--/, $form->{"AP_amount_$i"});
-    my $item = $charts{$accno_row};
-    $selected_accno_full = "$item->{accno}--$item->{tax_id}";
-
-    my $selected_taxchart = $form->{"taxchart_$i"};
-    my ($selected_accno, $selected_tax_id) = split(/--/, $selected_accno_full);
-    my ($previous_accno, $previous_tax_id) = split(/--/, $form->{"previous_AP_amount_$i"});
-
-    if ($previous_accno &&
-        ($previous_accno eq $selected_accno) &&
-        ($previous_tax_id ne $selected_tax_id)) {
-      my $item = $taxcharts{$selected_tax_id};
-      $selected_taxchart = "$item->{id}--$item->{rate}";
+    my ($default_taxchart, $taxchart_to_use);
+    my $used_tax_id;
+    if ( $form->{"taxchart_$i"} ) {
+      ($used_tax_id) = split(/--/, $form->{"taxchart_$i"});
+    }
+    my $amount_chart_id = $form->{"AP_amount_chart_id_$i"} || $default_ap_amount_chart_id;
+
+    my @taxcharts       = GL->get_active_taxes_for_chart($amount_chart_id, $taxdate, $used_tax_id);
+    foreach my $item (@taxcharts) {
+      my $key             = $item->id . "--" . $item->rate;
+      $first_taxchart   //= $item;
+      $default_taxchart   = $item if $item->{is_default};
+      $taxchart_to_use    = $item if $key eq $form->{"taxchart_$i"};
     }
 
-    $selected_taxchart = $taxchart_init unless ($form->{"taxchart_$i"});
+    $taxchart_to_use               //= $default_taxchart // $first_taxchart;
+    my $selected_taxchart            = $taxchart_to_use->id . '--' . $taxchart_to_use->rate;
+    $form->{"selected_taxchart_$i"}  = $selected_taxchart;
+    $form->{"AP_amount_chart_id_$i"} = $amount_chart_id;
+    $form->{"taxcharts_$i"}          = \@taxcharts;
 
-    $form->{'selected_accno_full_'. $i} = $selected_accno_full;
+    # reverse charge hack for template, display two taxes
+    if ($taxchart_to_use->reverse_charge_chart_id) {
+      my $tmpnetamount;
+      ($tmpnetamount, $form->{"tax_reverse_$i"}) = $form->calculate_tax($form->parse_amount(\%myconfig, $form->{"amount_$i"}),
+                                                                        $taxchart_to_use->rate, $form->{taxincluded}, 2        );
 
-    $form->{'selected_taxchart_'. $i} = $selected_taxchart;
+      $form->{"tax_charge_$i"}  = $form->{"tax_reverse_$i"} * -1;
+      $form->{"tax_reverse_$i"} = $form->format_amount(\%myconfig, $form->{"tax_reverse_$i"}, 2);
+      $form->{"tax_charge_$i"}  = $form->format_amount(\%myconfig, $form->{"tax_charge_$i"}, 2);
+    }
   }
 
-  $form->{AP_amount_value_title_sub} = sub {
-    my $item = shift;
-    return [
-      $item->{accno} .'--'. $item->{tax_id},
-      $item->{accno} .'--'. $item->{description},
-    ];
-  };
-
   $form->{taxchart_value_title_sub} = sub {
     my $item = shift;
     return [
       $item->{id} .'--'. $item->{rate},
-      $item->{taxdescription} .' '. ($item->{rate} * 100) .' %',
+      $item->{taxkey} . ' - ' . $item->{taxdescription} .' '. ($item->{rate} * 100) .' %',
     ];
   };
 
@@ -382,19 +523,9 @@ sub form_header {
     ];
   };
 
-  $form->{APselected_value_title_sub} = sub {
-    my $item = shift;
-    return [
-      $item->{accno},
-      $item->{accno} .'--'. $item->{description}
-    ];
-  };
-
   $form->{invtotal_unformatted} = $form->{invtotal};
   $form->{invtotal} = $form->format_amount(\%myconfig, $form->{invtotal}, 2);
 
-  $form->{totalpaid} = 0;
-
   _sort_payments();
 
   if ( $form->{'paid_'. $form->{paidaccounts}} ) {
@@ -405,8 +536,6 @@ sub form_header {
   $form->{accno_arap} = IS->get_standard_accno_current_assets(\%myconfig, \%$form);
 
   for my $i (1 .. $form->{paidaccounts}) {
-    $form->{totalpaid} += $form->{"paid_$i"};
-
     # format amounts
     if ($form->{"paid_$i"}) {
       $form->{"paid_$i"} = $form->format_amount(\%myconfig, $form->{"paid_$i"}, 2);
@@ -428,15 +557,27 @@ sub form_header {
       $changeable = (($form->{"gldate_$i"} eq '') || $form->current_date(\%myconfig) eq $form->{"gldate_$i"});
     }
 
+    #deaktivieren von gebuchten Zahlungen ausserhalb der Bücherkontrolle, vorher prüfen ob heute eingegeben
+    if ($form->date_closed($form->{"gldate_$i"})) {
+       $changeable = 0;
+    }
+
     $form->{'paidaccount_changeable_'. $i} = $changeable;
 
     $form->{'labelpaid_project_id_'. $i} = $project_labels{$form->{'paid_project_id_'. $i}};
+    # accno and description as info text
+    $form->{'AP_paid_readonly_desc_' . $i} =  $form->{'AP_paid_' . $i} ?
+       $form->{'AP_paid_' . $i} . " " . SL::DB::Manager::Chart->find_by(accno => $form->{'AP_paid_' . $i})->description
+     : '';
   }
 
   $form->{paid_missing} = $form->{invtotal_unformatted} - $form->{totalpaid};
 
+  $form->{payment_id} = $form->{invoice_obj}->{payment_id} // $form->{payment_id};
   print $form->parse_html_template('ap/form_header', {
     today => DateTime->today,
+    currencies => SL::DB::Manager::Currency->get_all_sorted,
+    payment_terms => SL::DB::Manager::PaymentTerm->get_all_sorted(query => [ or => [ obsolete => 0, id => $form->{payment_id}*1 ]]),
   });
 
   $main::lxdebug->leave_sub();
@@ -444,12 +585,13 @@ sub form_header {
 
 sub form_footer {
   $::lxdebug->enter_sub;
-  $::auth->assert('general_ledger');
+
+  _assert_access();
 
   my $num_due;
   my $num_follow_ups;
   if ($::form->{id}) {
-    my $follow_ups = FU->follow_ups('trans_id' => $::form->{id});
+    my $follow_ups = FU->follow_ups('trans_id' => $::form->{id}, 'not_done' => 1);
 
     if (@{ $follow_ups }) {
       $num_due        = sum map { $_->{due} * 1 } @{ $follow_ups };
@@ -469,33 +611,34 @@ sub form_footer {
   print $::form->parse_html_template('ap/form_footer', {
     num_due           => $num_due,
     num_follow_ups    => $num_follow_ups,
-    show_post_draft   => ($transdate > $closedto) && !$::form->{id},
-    show_storno       => $storno,
   });
 
   $::lxdebug->leave_sub;
 }
 
 sub mark_as_paid {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
+  $::auth->assert('ap_transactions');
 
-  $main::auth->assert('general_ledger');
+  SL::DB::PurchaseInvoice->new(id => $::form->{id})->load->mark_as_paid;
 
-  &mark_as_paid_common(\%myconfig,"ap");
+  $::form->redirect($::locale->text("Marked as paid"));
+}
 
-  $main::lxdebug->leave_sub();
+sub show_draft {
+  $::form->{transdate} = DateTime->today_local->to_kivitendo if !$::form->{transdate};
+  $::form->{gldate}    = $::form->{transdate} if !$::form->{gldate};
+  update();
 }
 
 sub update {
+  my %params = @_;
+
   $main::lxdebug->enter_sub();
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ap_transactions');
 
   my $display = shift;
 
@@ -506,12 +649,12 @@ sub update {
   map { $form->{$_} = $form->parse_amount(\%myconfig, $form->{$_}) }
     qw(exchangerate creditlimit creditremaining);
 
-  my @flds  = qw(amount AP_amount projectnumber oldprojectnumber project_id taxchart);
+  my @flds  = qw(amount AP_amount_chart_id projectnumber oldprojectnumber project_id taxchart tax);
   my $count = 0;
   my (@a, $j, $totaltax);
   for my $i (1 .. $form->{rowcount}) {
     $form->{"amount_$i"} = $form->parse_amount(\%myconfig, $form->{"amount_$i"});
-    if ($form->{"amount_$i"}) {
+    if ($form->{"amount_$i"} || $params{keep_rows_without_amount}) {
       push @a, {};
       $j = $#a;
       my ($taxkey, $rate) = split(/--/, $form->{"taxchart_$i"});
@@ -519,7 +662,6 @@ sub update {
       # calculate tax exactly the same way as AP in post_transaction via form->calculate_tax
       my $tmpnetamount;
       ($tmpnetamount,$form->{"tax_$i"}) = $form->calculate_tax($form->{"amount_$i"},$rate,$form->{taxincluded},2);
-
       $totaltax += $form->{"tax_$i"};
       map { $a[$j]->{$_} = $form->{"${_}_$i"} } @flds;
       $count++;
@@ -533,18 +675,22 @@ sub update {
   $form->{exchangerate} = $form->{forex} if $form->{forex};
 
   $form->{invdate} = $form->{transdate};
-  my %saved_variables = map +( $_ => $form->{$_} ), qw(AP AP_amount_1 taxchart_1 notes);
 
-  my $vendor_changed = &check_name("vendor");
+  if (($form->{previous_vendor_id} || $form->{vendor_id}) != $form->{vendor_id}) {
+    IR->get_vendor(\%::myconfig, $form);
 
-  $form->{AP} = $saved_variables{AP};
-  if ($saved_variables{AP_amount_1} =~ m/.--./) {
-    map { $form->{$_} = $saved_variables{$_} } qw(AP_amount_1 taxchart_1);
-  } else {
-    delete $form->{taxchart_1};
+    my $vendor = SL::DB::Vendor->load_cached($form->{vendor_id});
+
+    # reset payment to new vendor
+    $form->{payment_id} = $vendor->payment_id;
+
+    if (($form->{rowcount} == 1) && ($form->{amount_1} == 0)) {
+      my $last_used_ap_chart = $vendor->last_used_ap_chart;
+      $form->{"AP_amount_chart_id_1"} = $last_used_ap_chart->id if $last_used_ap_chart;
+    }
   }
 
-  $form->{rowcount} = $count + 1;
+  $form->{rowcount} = $count + ($params{dont_add_new_row} ? 0 : 1);
 
   $form->{invtotal} =
     ($form->{taxincluded}) ? $form->{invtotal} : $form->{invtotal} + $totaltax;
@@ -570,7 +716,7 @@ sub update {
   $form->{oldinvtotal}  = $form->{invtotal};
   $form->{oldtotalpaid} = $totalpaid;
 
-  &display_form;
+  display_form();
 
   $main::lxdebug->leave_sub();
 }
@@ -583,7 +729,7 @@ sub post_payment {
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ap_transactions');
   $form->mtime_ischanged('ap');
 
   $form->{defaultcurrency} = $form->get_default_currency(\%myconfig);
@@ -596,8 +742,13 @@ sub post_payment {
 
       $form->isblank("datepaid_$i", $locale->text('Payment date missing!'));
 
+      $form->error($locale->text('Cannot post transaction above the maximum future booking date!'))
+        if ($form->date_max_future($form->{"datepaid_$i"}, \%myconfig));
+
+      #Zusätzlich noch das Buchungsdatum in die Bücherkontrolle einbeziehen
+      # (Dient zur Prüfung ob ZE oder ZA geprüft werden soll)
       $form->error($locale->text('Cannot post payment for a closed period!'))
-        if ($form->date_closed($form->{"datepaid_$i"}, \%myconfig));
+        if ($form->date_closed($form->{"datepaid_$i"})  && !$form->date_closed($form->{"gldate_$i"}, \%myconfig));
 
       if ($form->{defaultcurrency} && ($form->{currency} ne $form->{defaultcurrency})) {
         $form->{"exchangerate_$i"} = $form->{exchangerate}
@@ -632,18 +783,19 @@ sub post {
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ap_transactions');
   $form->mtime_ischanged('ap');
 
   my ($inline) = @_;
 
   # check if there is a vendor, invoice, due date and invnumber
-  $form->isblank("transdate", $locale->text("Invoice Date missing!"));
-  $form->isblank("duedate",   $locale->text("Due Date missing!"));
-  $form->isblank("vendor",    $locale->text('Vendor missing!'));
-  $form->isblank("invnumber", $locale->text('Invoice Number missing!'));
+  $form->isblank("transdate",   $locale->text("Invoice Date missing!"));
+  $form->isblank("duedate",     $locale->text("Due Date missing!"));
+  $form->isblank("vendor_id",   $locale->text('Vendor missing!'));
+  $form->isblank("invnumber",   $locale->text('Invoice Number missing!'));
+  $form->isblank("AP_chart_id", $locale->text('No contra account selected!'));
 
-  if ($myconfig{mandatory_departments} && !$form->{department}) {
+  if ($myconfig{mandatory_departments} && !$form->{department_id}) {
     $form->{saved_message} = $::locale->text('You have to specify a department.');
     update();
     exit;
@@ -658,9 +810,16 @@ sub post {
 
   my $zero_amount_posting = 1;
   for my $i (1 .. $form->{rowcount}) {
+
+    # no taxincluded for reverse charge
+    my ($used_tax_id) = split(/--/, $form->{"taxchart_$i"});
+    my $tax = SL::DB::Manager::Tax->find_by(id => $used_tax_id);
+    if ($tax->reverse_charge_chart_id && $form->{taxincluded}) {
+      $form->error($locale->text('Cannot Post AP transaction with tax included!'));
+    }
+
     if ($form->parse_amount(\%myconfig, $form->{"amount_$i"})) {
       $zero_amount_posting = 0;
-      last;
     }
   }
 
@@ -676,8 +835,13 @@ sub post {
 
       $form->isblank("datepaid_$i", $locale->text('Payment date missing!'));
 
+      $form->error($locale->text('Cannot post transaction above the maximum future booking date!'))
+      if ($form->date_max_future($form->{"datepaid_$i"}, \%myconfig));
+
+      #Zusätzlich noch das Buchungsdatum in die Bücherkontrolle einbeziehen
+      # (Dient zur Prüfung ob ZE oder ZA geprüft werden soll)
       $form->error($locale->text('Cannot post payment for a closed period!'))
-        if ($form->date_closed($form->{"datepaid_$i"}, \%myconfig));
+        if ($form->date_closed($form->{"datepaid_$i"})  && !$form->date_closed($form->{"gldate_$i"}, \%myconfig));
 
       if ($form->{defaultcurrency} && ($form->{currency} ne $form->{defaultcurrency})) {
         $form->{"exchangerate_$i"} = $form->{exchangerate}
@@ -690,22 +854,21 @@ sub post {
   }
 
   # if old vendor ne vendor redo form
-  my ($vendor) = split /--/, $form->{vendor};
-  if ($form->{oldvendor} ne "$vendor--$form->{vendor_id}") {
+  if (($form->{previous_customer_id} || $form->{customer_id}) != $form->{customer_id}) {
     &update;
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
-  my ($debitaccno,    $debittaxkey)    = split /--/, $form->{AP_amountselected};
-  my ($taxkey,        $NULL)           = split /--/, $form->{taxchartselected};
-  my ($payablesaccno, $payablestaxkey) = split /--/, $form->{APselected};
-#  $form->{AP_amount_1}  = $debitaccno;
-  $form->{AP_payables}  = $payablesaccno;
-  $form->{taxkey}       = $taxkey;
   $form->{storno}       = 0;
 
   $form->{id} = 0 if $form->{postasnew};
 
   if (AP->post_transaction(\%myconfig, \%$form)) {
+    # create webdav folder
+    if ($::instance_conf->get_webdav) {
+      SL::Webdav->new(type     => 'accounts_payable',
+                      number   => $form->{invnumber},
+                     )->webdav_path;
+    }
     # saving the history
     if(!exists $form->{addition} && $form->{id} ne "") {
       $form->{snumbers}  = qq|invnumber_| . $form->{invnumber};
@@ -713,10 +876,31 @@ sub post {
       $form->{what_done} = "invoice";
       $form->save_history;
     }
-    # /saving the history
-    remove_draft() if $form->{remove_draft};
-    # Dieser Text wird niemals ausgegeben: Probleme beim redirect?
-    $form->redirect($locale->text('Transaction posted!')) unless $inline;
+
+    if (!$inline) {
+      my $msg = $locale->text("AP transaction '#1' posted (ID: #2)", $form->{invnumber}, $form->{id});
+      if ($form->{callback} =~ /BankTransaction/) {
+        # no restore_from_session_id needed. we like to have a newly generated
+        # list of invoices for bank transactions
+        SL::Helper::Flash::flash_later('info', $msg);
+        print $form->redirect_header($form->{callback});
+        $::dispatcher->end_request;
+
+      } elsif ('doc-tab' eq $form->{after_action}) {
+        # Redirect with callback containing a fragment does not work (by now)
+        # because the callback info is stored in the session an parsing the
+        # callback parameters does not support fragments (see SL::Form::redirect).
+        # So use flash_later for the message and redirect_headers for redirecting.
+        my $add_doc_url = build_std_url("script=ap.pl", 'action=edit', 'id=' . E($form->{id}), 'fragment=ui-tabs-docs');
+        SL::Helper::Flash::flash_later('info', $msg);
+        print $form->redirect_header($add_doc_url);
+        $::dispatcher->end_request;
+
+      } else {
+        $form->redirect($msg);
+      }
+    }
+
   } else {
     $form->error($locale->text('Cannot post transaction!'));
   }
@@ -730,7 +914,7 @@ sub post_as_new {
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ap_transactions');
 
   $form->{postasnew} = 1;
   # saving the history
@@ -755,64 +939,32 @@ sub use_as_new {
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ap_transactions');
 
-  map { delete $form->{$_} } qw(printed emailed queued invnumber invdate deliverydate id datepaid_1 gldate_1 acc_trans_id_1 source_1 memo_1 paid_1 exchangerate_1 AP_paid_1 storno);
+  map { delete $form->{$_} } qw(printed emailed queued invnumber deliverydate id datepaid_1 gldate_1 acc_trans_id_1 source_1 memo_1 paid_1 exchangerate_1 AP_paid_1 storno convert_from_oe_id);
   $form->{paidaccounts} = 1;
   $form->{rowcount}--;
-  $form->{invdate} = $form->current_date(\%myconfig);
-  &update;
 
-  $main::lxdebug->leave_sub();
-}
-
-sub delete {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my $locale   = $main::locale;
-
-  $main::auth->assert('general_ledger');
-
-  $form->{title} = $locale->text('Confirm!');
-
-  $form->header;
-
-  delete $form->{header};
+  my $today          = DateTime->today_local;
+  $form->{transdate} = $today->to_kivitendo;
+  $form->{duedate}   = $form->{transdate};
 
-  print qq|
-<form method=post action=$form->{script}>
-|;
-
-  foreach my $key (keys %$form) {
-    next if (($key eq 'login') || ($key eq 'password') || ('' ne ref $form->{$key}));
-    $form->{$key} =~ s/\"/&quot;/g;
-    print qq|<input type=hidden name=$key value="$form->{$key}">\n|;
+  if ($form->{vendor_id}) {
+    my $payment_terms = SL::DB::Vendor->load_cached($form->{vendor_id})->payment;
+    $form->{duedate}  = $payment_terms->calc_date(reference_date => $today)->to_kivitendo if $payment_terms;
   }
 
-  print qq|
-<h2 class=confirm>$form->{title}</h2>
-
-<h4>|
-    . $locale->text('Are you sure you want to delete Transaction')
-    . qq| $form->{invnumber}</h4>
-
-<input name=action class=submit type=submit value="|
-    . $locale->text('Yes') . qq|">
-</form>
-|;
+  &update;
 
   $main::lxdebug->leave_sub();
 }
 
-sub yes {
-  $main::lxdebug->enter_sub();
-
+sub delete {
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ap_transactions');
 
   if (AP->delete_transaction(\%myconfig, \%$form)) {
     # saving the history
@@ -826,31 +978,27 @@ sub yes {
     $form->redirect($locale->text('Transaction deleted!'));
   }
   $form->error($locale->text('Cannot delete transaction!'));
-
-  $main::lxdebug->leave_sub();
 }
 
 sub search {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('vendor_invoice_edit');
-
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
-  # setup customer selection
-  $form->all_vc(\%myconfig, "vendor", "AP");
-
-  $form->{title}    = $locale->text('AP Transactions');
+  $form->{title} = $locale->text('Vendor Invoices & AP Transactions');
 
-  $form->get_lists("projects"     => { "key" => "ALL_PROJECTS", "all" => 1 },
-                   "departments"  => "ALL_DEPARTMENTS",
-                   "vendors"      => "ALL_VC");
+  $::form->{ALL_DEPARTMENTS} = SL::DB::Manager::Department->get_all_sorted;
+  $::form->{ALL_TAXZONES}    = SL::DB::Manager::TaxZone   ->get_all_sorted;
 
   # constants and subs for template
   $form->{vc_keys}   = sub { "$_[0]->{name}--$_[0]->{id}" };
 
+  $::request->layout->add_javascripts("autocomplete_project.js");
+
+  setup_ap_search_action_bar();
+
   $form->header;
   print $form->parse_html_template('ap/search', { %myconfig });
 
@@ -885,25 +1033,23 @@ sub ap_transactions {
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
-  $main::auth->assert('vendor_invoice_edit');
-
-  ($form->{vendor}, $form->{vendor_id}) = split(/--/, $form->{vendor});
-
   report_generator_set_default_sort('transdate', 1);
 
   AP->ap_transactions(\%myconfig, \%$form);
 
-  $form->{title} = $locale->text('AP Transactions');
+  $form->{title} = $locale->text('Vendor Invoices & AP Transactions');
 
   my $report = SL::ReportGenerator->new(\%myconfig, $form);
 
   my @columns =
     qw(transdate id type invnumber ordnumber name netamount tax amount paid datepaid
-       due duedate transaction_description notes employee globalprojectnumber
-       vendornumber country ustid taxzone payment_terms charts direct_debit);
+       due duedate transaction_description notes employee globalprojectnumber department
+       vendornumber country ustid taxzone payment_terms charts debit_chart direct_debit
+       insertdate);
 
   my @hidden_variables = map { "l_${_}" } @columns;
-  push @hidden_variables, "l_subtotal", qw(open closed vendor invnumber ordnumber transaction_description notes project_id transdatefrom transdateto department);
+  push @hidden_variables, "l_subtotal", qw(open closed vendor invnumber ordnumber transaction_description notes project_id transdatefrom transdateto
+                                           parts_partnumber parts_description department_id taxzone_id);
 
   my $href = build_std_url('action=ap_transactions', grep { $form->{$_} } @hidden_variables);
 
@@ -925,16 +1071,19 @@ sub ap_transactions {
     'notes'                   => { 'text' => $locale->text('Notes'), },
     'employee'                => { 'text' => $locale->text('Employee'), },
     'globalprojectnumber'     => { 'text' => $locale->text('Document Project Number'), },
+    'department'              => { 'text' => $locale->text('Department'), },
     'vendornumber'            => { 'text' => $locale->text('Vendor Number'), },
     'country'                 => { 'text' => $locale->text('Country'), },
     'ustid'                   => { 'text' => $locale->text('USt-IdNr.'), },
-    'taxzone'                 => { 'text' => $locale->text('Steuersatz'), },
+    'taxzone'                 => { 'text' => $locale->text('Tax rate'), },
     'payment_terms'           => { 'text' => $locale->text('Payment Terms'), },
-    'charts'                  => { 'text' => $locale->text('Buchungskonto'), },
+    'charts'                  => { 'text' => $locale->text('Chart'), },
+    'debit_chart'             => { 'text' => $locale->text('Debit Account'), },
     'direct_debit'            => { 'text' => $locale->text('direct debit'), },
+    'insertdate'              => { 'text' => $locale->text('Insert Date'), },
   );
 
-  foreach my $name (qw(id transdate duedate invnumber ordnumber name datepaid employee shippingpoint shipvia transaction_description direct_debit)) {
+  foreach my $name (qw(id transdate duedate invnumber ordnumber name datepaid employee shippingpoint shipvia transaction_description direct_debit department taxzone)) {
     my $sortdir                 = $form->{sort} eq $name ? 1 - $form->{sortdir} : $form->{sortdir};
     $column_defs{$name}->{link} = $href . "&sort=$name&sortdir=$sortdir";
   }
@@ -951,21 +1100,28 @@ sub ap_transactions {
 
   $report->set_sort_indicator($form->{sort}, $form->{sortdir});
 
+  my $department_description;
+  $department_description = SL::DB::Manager::Department->find_by(id => $form->{department_id})->description if $form->{department_id};
+  my $project_description;
+  $project_description = SL::DB::Manager::Project->find_by(id => $form->{project_id})->description if $form->{project_id};
+
   my @options;
   push @options, $locale->text('Vendor')                  . " : $form->{vendor}"                         if ($form->{vendor});
   push @options, $locale->text('Contact Person')          . " : $form->{cp_name}"                        if ($form->{cp_name});
-  push @options, $locale->text('Department')              . " : " . (split /--/, $form->{department})[0] if ($form->{department});
+  push @options, $locale->text('Department')              . " : $department_description"                 if ($form->{department_id});
+  push @options, $locale->text('Project')                 . " : $project_description"                    if ($project_description);
   push @options, $locale->text('Invoice Number')          . " : $form->{invnumber}"                      if ($form->{invnumber});
   push @options, $locale->text('Order Number')            . " : $form->{ordnumber}"                      if ($form->{ordnumber});
   push @options, $locale->text('Notes')                   . " : $form->{notes}"                          if ($form->{notes});
   push @options, $locale->text('Transaction description') . " : $form->{transaction_description}"        if ($form->{transaction_description});
+  push @options, $locale->text('Part Description')        . " : $form->{parts_description}"              if $form->{parts_description};
+  push @options, $locale->text('Part Number')             . " : $form->{parts_partnumber}"               if $form->{parts_partnumber};
   push @options, $locale->text('From') . " " . $locale->date(\%myconfig, $form->{transdatefrom}, 1)      if ($form->{transdatefrom});
   push @options, $locale->text('Bis')  . " " . $locale->date(\%myconfig, $form->{transdateto},   1)      if ($form->{transdateto});
   push @options, $locale->text('Open')                                                                   if ($form->{open});
   push @options, $locale->text('Closed')                                                                 if ($form->{closed});
 
   $report->set_options('top_info_text'        => join("\n", @options),
-                       'raw_bottom_info_text' => $form->parse_html_template('ap/ap_transactions_bottom'),
                        'output_format'        => 'HTML',
                        'title'                => $form->{title},
                        'attachment_basename'  => $locale->text('vendor_invoice_list') . strftime('_%Y%m%d', localtime time),
@@ -1040,6 +1196,7 @@ sub ap_transactions {
   $report->add_separator();
   $report->add_data(create_subtotal_row(\%totals, \@columns, \%column_alignment, \@subtotal_columns, 'listtotal'));
 
+  setup_ap_transactions_action_bar();
   $report->generate_with_headers();
 
   $main::lxdebug->leave_sub();
@@ -1052,7 +1209,7 @@ sub storno {
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ap_transactions');
 
   if (IS->has_storno(\%myconfig, $form, 'ap')) {
     $form->{title} = $locale->text("Cancel Accounts Payables Transaction");
@@ -1077,3 +1234,277 @@ sub storno {
 
   $main::lxdebug->leave_sub();
 }
+
+sub add_from_purchase_order {
+  $main::auth->assert('ap_transactions');
+
+  return if !$::form->{id};
+
+  my $order_id = delete $::form->{id};
+  my $order    = SL::DB::Order->new(id => $order_id)->load(with => [ 'vendor', 'currency', 'payment_terms' ]);
+
+  return if $order->type ne 'purchase_order';
+
+  my $today                     = DateTime->today_local;
+  $::form->{title}                   = "Add";
+  $::form->{vc}                      = 'vendor';
+  $::form->{vendor_id}               = $order->customervendor->id;
+  $::form->{vendor}                  = $order->vendor->name;
+  $::form->{convert_from_oe_id}      = $order->id;
+  $::form->{globalproject_id}        = $order->globalproject_id;
+  $::form->{ordnumber}               = $order->number;
+  $::form->{department_id}           = $order->department_id;
+  $::form->{transaction_description} = $order->transaction_description;
+  $::form->{currency}                = $order->currency->name;
+  $::form->{taxincluded}             = 1; # we use amount below, so tax is included
+  $::form->{transdate}               = $today->to_kivitendo;
+  $::form->{duedate}                 = $today->to_kivitendo;
+  $::form->{duedate}                 = $order->payment_terms->calc_date(reference_date => $today)->to_kivitendo if $order->payment_terms;
+  $::form->{deliverydate}            = $order->reqdate->to_kivitendo                                            if $order->reqdate;
+  create_links();
+
+  my $config_po_ap_workflow_chart_id = $::instance_conf->get_workflow_po_ap_chart_id;
+
+  my ($first_taxchart, $default_taxchart, $taxchart_to_use);
+  my @taxcharts = ();
+  @taxcharts    = GL->get_active_taxes_for_chart($config_po_ap_workflow_chart_id, $::form->{transdate}) if (defined $config_po_ap_workflow_chart_id);
+  foreach my $item (@taxcharts) {
+    $first_taxchart   //= $item;
+    $default_taxchart   = $item if $item->{is_default};
+  }
+  $taxchart_to_use      = $default_taxchart // $first_taxchart;
+
+  my %pat = $order->calculate_prices_and_taxes;
+  my $row = 1;
+  foreach my $amount_chart (keys %{$pat{amounts}}) {
+    my $tax = SL::DB::Manager::Tax->find_by(id => $pat{amounts}->{$amount_chart}->{tax_id});
+    # If tax chart from order for this amount is active, use it. Use default or first tax chart for selected chart else.
+    if (defined $config_po_ap_workflow_chart_id) {
+      $taxchart_to_use = (first {$_->{id} == $tax->id} @taxcharts) // $taxchart_to_use;
+    } else {
+      $taxchart_to_use = $tax;
+    }
+
+    $::form->{"AP_amount_chart_id_$row"}          = $config_po_ap_workflow_chart_id // $amount_chart;
+    $::form->{"previous_AP_amount_chart_id_$row"} = $::form->{"AP_amount_chart_id_$row"};
+    $::form->{"amount_$row"}                      = $::form->format_amount(\%::myconfig, $pat{amounts}->{$amount_chart}->{amount} * (1 + $tax->rate), 2);
+    $::form->{"taxchart_$row"}                    = $taxchart_to_use->id . '--' . $taxchart_to_use->rate;
+    $::form->{"project_id_$row"}                  = $order->globalproject_id;
+
+    $row++;
+  }
+
+  my $last_used_ap_chart               = SL::DB::Vendor->load_cached($::form->{vendor_id})->last_used_ap_chart;
+  $::form->{"AP_amount_chart_id_$row"} = $last_used_ap_chart->id if $last_used_ap_chart;
+  $::form->{rowcount}                  = $row;
+
+  update(
+    keep_rows_without_amount => 1,
+    dont_add_new_row         => 1,
+  );
+}
+
+sub setup_ap_search_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        $::locale->text('Search'),
+        submit    => [ '#form', { action => "ap_transactions" } ],
+        checks    => [ 'kivi.validate_form' ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+  $::request->layout->add_javascripts('kivi.Validator.js');
+}
+
+sub setup_ap_transactions_action_bar {
+  my %params          = @_;
+  my $may_edit_create = $::auth->assert('ap_transactions', 1);
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [ t8('Add') ],
+        link => [
+          t8('Purchase Invoice'),
+          link     => [ 'ir.pl?action=add' ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+
+        ],
+        link => [
+          t8('AP Transaction'),
+          link     => [ 'ap.pl?action=add' ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+      ], # end of combobox "Add"
+    );
+  }
+}
+
+sub setup_ap_display_form_action_bar {
+  my $transdate               = $::form->datetonum($::form->{transdate}, \%::myconfig);
+  my $closedto                = $::form->datetonum($::form->{closedto},  \%::myconfig);
+  my $is_closed               = $transdate <= $closedto;
+
+  my $change_never            = $::instance_conf->get_ap_changeable == 0;
+  my $change_on_same_day_only = $::instance_conf->get_ap_changeable == 2 && ($::form->current_date(\%::myconfig) ne $::form->{gldate});
+
+  my $is_storno               = IS->is_storno(\%::myconfig, $::form, 'ap', $::form->{id});
+  my $has_storno              = IS->has_storno(\%::myconfig, $::form, 'ap');
+
+  my $may_edit_create         = $::auth->assert('ap_transactions', 1);
+
+  my $has_sepa_exports;
+  if ($::form->{id}) {
+    my $invoice = SL::DB::Manager::PurchaseInvoice->find_by(id => $::form->{id});
+    $has_sepa_exports = 1 if ($invoice->find_sepa_export_items()->[0]);
+  }
+
+  my $is_linked_bank_transaction;
+  if ($::form->{id}
+      && SL::DB::Default->get->payments_changeable != 0
+      && SL::DB::Manager::BankTransactionAccTrans->find_by(ap_id => $::form->{id})) {
+
+    $is_linked_bank_transaction = 1;
+  }
+  my $is_linked_gl_transaction;
+  if ($::form->{id} && SL::DB::Manager::ApGl->find_by(ap_id => $::form->{id})) {
+    $is_linked_gl_transaction = 1;
+  }
+
+  my $create_post_action = sub {
+    # $_[0]: description
+    # $_[1]: after_action
+    action => [
+      $_[0],
+      submit   => [ '#form', { action => "post", after_action => $_[1] } ],
+      checks   => [ 'kivi.validate_form', 'kivi.AP.check_fields_before_posting', 'kivi.AP.check_duplicate_invnumber' ],
+      disabled => !$may_edit_create                           ? t8('You must not change this AP transaction.')
+                : $is_closed                                  ? t8('The billing period has already been locked.')
+                : $is_storno                                  ? t8('A canceled invoice cannot be posted.')
+                : ($::form->{id} && $change_never)            ? t8('Changing invoices has been disabled in the configuration.')
+                : ($::form->{id} && $change_on_same_day_only) ? t8('Invoices can only be changed on the day they are posted.')
+                : $is_linked_bank_transaction                 ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                :                                               undef,
+    ],
+  };
+
+  my @post_entries;
+  if ($::instance_conf->get_ap_add_doc && $::instance_conf->get_doc_storage) {
+    @post_entries = ( $create_post_action->(t8('Post'), 'doc-tab'),
+                      $create_post_action->(t8('Post and new booking')) );
+  } elsif ($::instance_conf->get_doc_storage) {
+    @post_entries = ( $create_post_action->(t8('Post')),
+                      $create_post_action->(t8('Post and upload document'), 'doc-tab') );
+  } else {
+    @post_entries = ( $create_post_action->(t8('Post')) );
+  }
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#form', { action => "update" } ],
+        id        => 'update_button',
+        checks    => [ 'kivi.validate_form' ],
+        accesskey => 'enter',
+        disabled  => !$may_edit_create ? t8('You must not change this AP transaction.') : undef,
+      ],
+      combobox => [
+        @post_entries,
+        action => [
+          t8('Post Payment'),
+          submit   => [ '#form', { action => "post_payment" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create           ? t8('You must not change this AP transaction.')
+                    : !$::form->{id}              ? t8('This invoice has not been posted yet.')
+                    : $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                    :                               undef,
+        ],
+        action => [ t8('Mark as paid'),
+          submit   => [ '#form', { action => "mark_as_paid" } ],
+          confirm  => t8('This will remove the invoice from showing as unpaid even if the unpaid amount does not match the amount. Proceed?'),
+          disabled => !$may_edit_create ? t8('You must not change this AP transaction.')
+                    : !$::form->{id}    ? t8('This invoice has not been posted yet.')
+                    :                     undef,
+          only_if  => $::instance_conf->get_is_show_mark_as_paid,
+        ],
+      ], # end of combobox "Post"
+
+      combobox => [
+        action => [ t8('Storno'),
+          submit   => [ '#form', { action => "storno" } ],
+          checks   => [ 'kivi.validate_form', 'kivi.AP.check_fields_before_posting' ],
+          confirm  => t8('Do you really want to cancel this invoice?'),
+          disabled => !$may_edit_create    ? t8('You must not change this AP transaction.')
+                    : !$::form->{id}       ? t8('This invoice has not been posted yet.')
+                    : $has_storno          ? t8('This invoice has been canceled already.')
+                    : $is_storno           ? t8('Reversal invoices cannot be canceled.')
+                    : $::form->{totalpaid} ? t8('Invoices with payments cannot be canceled.')
+                    : $has_sepa_exports    ? t8('This invoice has been linked with a sepa export, undo this first.')
+                    : $is_linked_gl_transaction ? t8('This transaction is linked with a gl transaction. Please delete the ap transaction booking if needed.')
+                    :                        undef,
+        ],
+        action => [ t8('Delete'),
+          submit   => [ '#form', { action => "delete" } ],
+          confirm  => t8('Do you really want to delete this object?'),
+          disabled => !$may_edit_create           ? t8('You must not change this AP transaction.')
+                    : !$::form->{id}              ? t8('This invoice has not been posted yet.')
+                    : $is_closed                  ? t8('The billing period has already been locked.')
+                    : $has_sepa_exports           ? t8('This invoice has been linked with a sepa export, undo this first.')
+                    : $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                    : $is_linked_gl_transaction   ? undef # linked transactions can be deleted, if period is not closed
+                    : $change_never               ? t8('Changing invoices has been disabled in the configuration.')
+                    : $change_on_same_day_only    ? t8('Invoices can only be changed on the day they are posted.')
+                    : $has_storno                 ? t8('This invoice has been canceled already.')
+                    :                               undef,
+        ],
+      ], # end of combobox "Storno"
+
+      'separator',
+
+      combobox => [
+        action => [ t8('Workflow') ],
+        action => [
+          t8('Use As New'),
+          submit   => [ '#form', { action => "use_as_new" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create ? t8('You must not change this AP transaction.')
+                    : !$::form->{id}    ? t8('This invoice has not been posted yet.')
+                    :                     undef,
+        ],
+      ], # end of combobox "Workflow"
+
+      combobox => [
+        action => [ t8('more') ],
+        action => [
+          t8('History'),
+          call     => [ 'set_history_window', $::form->{id} * 1, 'glid' ],
+          disabled => !$::form->{id} ? t8('This invoice has not been posted yet.') : undef,
+        ],
+        action => [
+          t8('Follow-Up'),
+          call     => [ 'follow_up_window' ],
+          disabled => !$::form->{id} ? t8('This invoice has not been posted yet.') : undef,
+        ],
+        action => [
+          t8('Record templates'),
+          call     => [ 'kivi.RecordTemplate.popup', 'ap_transaction' ],
+          disabled => !$may_edit_create ? t8('You must not change this AP transaction.') : undef,
+        ],
+        action => [
+          t8('Drafts'),
+          call     => [ 'kivi.Draft.popup', 'ap', 'invoice', $::form->{draft_id}, $::form->{draft_description} ],
+          disabled => !$may_edit_create ? t8('You must not change this AP transaction.')
+                    : $::form->{id}     ? t8('This invoice has already been posted.')
+                    : $is_closed        ? t8('The billing period has already been locked.')
+                    :                     undef,
+        ],
+      ], # end of combobox "more"
+    );
+  }
+  $::request->layout->add_javascripts('kivi.Validator.js');
+}
index 28c8efb..cf0555f 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Accounts Receivables
@@ -36,16 +37,26 @@ use List::Util qw(sum first max);
 use List::UtilsBy qw(sort_by);
 
 use SL::AR;
+use SL::Controller::Base;
 use SL::FU;
+use SL::GL;
 use SL::IS;
-use SL::PE;
+use SL::DB::BankTransactionAccTrans;
+use SL::DB::Business;
+use SL::DB::Chart;
+use SL::DB::Currency;
 use SL::DB::Default;
+use SL::DB::Employee;
 use SL::DB::Invoice;
+use SL::DB::RecordTemplate;
+use SL::DB::Tax;
+use SL::Helper::Flash qw(flash flash_later);
+use SL::Locale::String qw(t8);
+use SL::Presenter::Tag;
+use SL::Presenter::Chart;
 use SL::ReportGenerator;
 
-require "bin/mozilla/arap.pl";
 require "bin/mozilla/common.pl";
-require "bin/mozilla/drafts.pl";
 require "bin/mozilla/reportgenerator.pl";
 
 use strict;
@@ -79,16 +90,154 @@ use strict;
 # $locale->text('Nov')
 # $locale->text('Dec')
 
+sub _may_view_or_edit_this_invoice {
+  return 1 if  $::auth->assert('ar_transactions', 1); # may edit all invoices
+  return 0 if !$::form->{id};                         # creating new invoices isn't allowed without invoice_edit
+  return 0 if !$::form->{globalproject_id};           # existing records without a project ID are not allowed
+  return SL::DB::Project->new(id => $::form->{globalproject_id})->load->may_employee_view_project_invoices(SL::DB::Manager::Employee->current);
+}
+
+sub _assert_access {
+  my $cache = $::request->cache('ar.pl::_assert_access');
+
+  $cache->{_may_view_or_edit_this_invoice} = _may_view_or_edit_this_invoice()                              if !exists $cache->{_may_view_or_edit_this_invoice};
+  $::form->show_generic_error($::locale->text("You do not have the permissions to access this function.")) if !       $cache->{_may_view_or_edit_this_invoice};
+}
+
+sub load_record_template {
+  $::auth->assert('ar_transactions');
+
+  # Load existing template and verify that its one for this module.
+  my $template = SL::DB::RecordTemplate
+    ->new(id => $::form->{id})
+    ->load(
+      with_object => [ qw(customer payment currency record_items record_items.chart) ],
+    );
+
+  die "invalid template type" unless $template->template_type eq 'ar_transaction';
+
+  $template->substitute_variables;
+
+  # Clean the current $::form before rebuilding it from the template.
+  my $form_defaults = delete $::form->{form_defaults};
+  delete @{ $::form }{ grep { !m{^(?:script|login)$}i } keys %{ $::form } };
+
+  # Fill $::form from the template.
+  my $today                   = DateTime->today_local;
+  $::form->{title}                   = "Add";
+  $::form->{currency}                = $template->currency->name;
+  $::form->{direct_debit}            = $template->direct_debit;
+  $::form->{globalproject_id}        = $template->project_id;
+  $::form->{transaction_description} = $template->transaction_description;
+  $::form->{AR_chart_id}             = $template->ar_ap_chart_id;
+  $::form->{transdate}               = $today->to_kivitendo;
+  $::form->{duedate}                 = $today->to_kivitendo;
+  $::form->{rowcount}                = @{ $template->items };
+  $::form->{paidaccounts}            = 1;
+  $::form->{$_}                      = $template->$_ for qw(department_id ordnumber taxincluded employee_id notes);
+
+  if ($template->customer) {
+    $::form->{customer_id} = $template->customer_id;
+    $::form->{customer}    = $template->customer->name;
+    $::form->{duedate}     = $template->customer->payment->calc_date(reference_date => $today)->to_kivitendo if $template->customer->payment;
+  }
+
+  my $row = 0;
+  foreach my $item (@{ $template->items }) {
+    $row++;
+
+    my $active_taxkey = $item->chart->get_active_taxkey;
+    my $taxes         = SL::DB::Manager::Tax->get_all(
+      where   => [ chart_categories => { like => '%' . $item->chart->category . '%' }],
+      sort_by => 'taxkey, rate',
+    );
+
+    my $tax   = first { $item->tax_id          == $_->id } @{ $taxes };
+    $tax    //= first { $active_taxkey->tax_id == $_->id } @{ $taxes };
+    $tax    //= $taxes->[0];
+
+    if (!$tax) {
+      $row--;
+      next;
+    }
+
+    $::form->{"AR_amount_chart_id_${row}"}          = $item->chart_id;
+    $::form->{"previous_AR_amount_chart_id_${row}"} = $item->chart_id;
+    $::form->{"amount_${row}"}                      = $::form->format_amount(\%::myconfig, $item->amount1, 2);
+    $::form->{"taxchart_${row}"}                    = $item->tax_id . '--' . $tax->rate;
+    $::form->{"project_id_${row}"}                  = $item->project_id;
+  }
+
+  $::form->{$_} = $form_defaults->{$_} for keys %{ $form_defaults // {} };
+
+  flash('info', $::locale->text("The record template '#1' has been loaded.", $template->template_name));
+
+  update(
+    keep_rows_without_amount => 1,
+    dont_add_new_row         => 1,
+  );
+}
+
+sub save_record_template {
+  $::auth->assert('ar_transactions');
+
+  my $template = $::form->{record_template_id} ? SL::DB::RecordTemplate->new(id => $::form->{record_template_id})->load : SL::DB::RecordTemplate->new;
+  my $js       = SL::ClientJS->new(controller => SL::Controller::Base->new);
+  my $new_name = $template->template_name_to_use($::form->{record_template_new_template_name});
+
+  $js->dialog->close('#record_template_dialog');
+
+  my @items = grep {
+    $_->{chart_id} && (($_->{tax_id} // '') ne '')
+  } map {
+    +{ chart_id   => $::form->{"AR_amount_chart_id_${_}"},
+       amount1    => $::form->parse_amount(\%::myconfig, $::form->{"amount_${_}"}),
+       tax_id     => (split m{--}, $::form->{"taxchart_${_}"})[0],
+       project_id => $::form->{"project_id_${_}"} || undef,
+     }
+  } (1..($::form->{rowcount} || 1));
+
+  $template->assign_attributes(
+    template_type           => 'ar_transaction',
+    template_name           => $new_name,
+
+    currency_id             => SL::DB::Manager::Currency->find_by(name => $::form->{currency})->id,
+    ar_ap_chart_id          => $::form->{AR_chart_id}      || undef,
+    customer_id             => $::form->{customer_id}      || undef,
+    department_id           => $::form->{department_id}    || undef,
+    project_id              => $::form->{globalproject_id} || undef,
+    employee_id             => $::form->{employee_id}      || undef,
+    taxincluded             => $::form->{taxincluded}  ? 1 : 0,
+    direct_debit            => $::form->{direct_debit} ? 1 : 0,
+    ordnumber               => $::form->{ordnumber},
+    notes                   => $::form->{notes},
+    transaction_description => $::form->{transaction_description},
+
+    items                   => \@items,
+  );
+
+  eval {
+    $template->save;
+    1;
+  } or do {
+    return $js
+      ->flash('error', $::locale->text("Saving the record template '#1' failed.", $new_name))
+      ->render;
+  };
+
+  return $js
+    ->flash('info', $::locale->text("The record template '#1' has been saved.", $new_name))
+    ->render;
+}
+
 sub add {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ar_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  return $main::lxdebug->leave_sub() if (load_draft_maybe());
-
   # saving the history
   if(!exists $form->{addition} && ($form->{id} ne "")) {
     $form->{snumbers} = qq|invnumber_| . $form->{invnumber};
@@ -98,12 +247,18 @@ sub add {
   # /saving the history
 
   $form->{title}    = "Add";
-  $form->{callback} = "ar.pl?action=add&DONT_LOAD_DRAFT=1" unless $form->{callback};
+  $form->{callback} = "ar.pl?action=add" unless $form->{callback};
 
   AR->get_transdate(\%myconfig, $form);
   $form->{initial_transdate} = $form->{transdate};
   create_links(dont_save => 1);
   $form->{transdate} = $form->{initial_transdate};
+
+  if ($form->{customer_id}) {
+    my $last_used_ar_chart = SL::DB::Customer->load_cached($form->{customer_id})->last_used_ar_chart;
+    $form->{"AR_amount_chart_id_1"} = $last_used_ar_chart->id if $last_used_ar_chart;
+  }
+
   &display_form;
   $main::lxdebug->leave_sub();
 }
@@ -111,7 +266,9 @@ sub add {
 sub edit {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  # Delay access check to after the invoice's been loaded in
+  # "create_links" so that project-specific invoice rights can be
+  # evaluated.
 
   my $form     = $main::form;
 
@@ -130,7 +287,7 @@ sub edit {
 sub display_form {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  _assert_access();
 
   my $form     = $main::form;
 
@@ -149,7 +306,8 @@ sub _retrieve_invoice_object {
 sub create_links {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  # Delay access check to after the invoice's been loaded so that
+  # project-specific invoice rights can be evaluated.
 
   my %params   = @_;
   my $form     = $main::form;
@@ -158,49 +316,25 @@ sub create_links {
   $form->create_links("AR", \%myconfig, "customer");
   $form->{invoice_obj} = _retrieve_invoice_object();
 
+  _assert_access();
+
   my %saved;
   if (!$params{dont_save}) {
     %saved = map { ($_ => $form->{$_}) } qw(direct_debit id taxincluded);
     $saved{duedate} = $form->{duedate} if $form->{duedate};
+    $saved{currency} = $form->{currency} if $form->{currency};
   }
 
   IS->get_customer(\%myconfig, \%$form);
 
   $form->{$_}          = $saved{$_} for keys %saved;
-  $form->{oldcustomer} = "$form->{customer}--$form->{customer_id}";
   $form->{rowcount}    = 1;
+  $form->{AR_chart_id} = $form->{acc_trans} && $form->{acc_trans}->{AR} ? $form->{acc_trans}->{AR}->[0]->{chart_id} : $::instance_conf->get_ar_chart_id || $form->{AR_links}->{AR}->[0]->{chart_id};
 
   # currencies
   $form->{defaultcurrency} = $form->get_default_currency(\%myconfig);
 
-  map { $form->{selectcurrency} .= "<option>$_\n" } $form->get_all_currencies(\%myconfig);
-
-  # customers
-  if (@{ $form->{all_customer} || [] }) {
-    $form->{customer} = "$form->{customer}--$form->{customer_id}";
-    map { $form->{selectcustomer} .= "<option>$_->{name}--$_->{id}\n" }
-      (@{ $form->{all_customer} });
-  }
-
-  # departments
-  if (@{ $form->{all_departments} || [] }) {
-    $form->{selectdepartment} = "<option>\n";
-    $form->{department}       = "$form->{department}--$form->{department_id}";
-
-    map {
-      $form->{selectdepartment} .=
-        "<option>$_->{description}--$_->{id}\n"
-    } (@{ $form->{all_departments} || [] });
-  }
-
-  $form->{employee} = "$form->{employee}--$form->{employee_id}";
-
-  # sales staff
-  if (@{ $form->{all_employees} || [] }) {
-    $form->{selectemployee} = "";
-    map { $form->{selectemployee} .= "<option>$_->{name}--$_->{id}\n" }
-      (@{ $form->{all_employees} || [] });
-  }
+  $form->{ALL_DEPARTMENTS} = SL::DB::Manager::Department->get_all_sorted;
 
   # build the popup menus
   $form->{taxincluded} = ($form->{id}) ? $form->{taxincluded} : "checked";
@@ -217,7 +351,7 @@ sub create_links {
 sub form_header {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  _assert_access();
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -227,37 +361,15 @@ sub form_header {
   $form->{invoice_obj} = _retrieve_invoice_object();
 
   my ($title, $readonly, $exchangerate, $rows);
-  my ($notes, $department, $customer, $employee, $amount, $project);
-  my ($ARselected);
+  my ($notes, $amount, $project);
 
+  $form->{initial_focus} = !($form->{amount_1} * 1) ? 'customer_id' : 'row_' . $form->{rowcount};
 
   $title = $form->{title};
   # $locale->text('Add Accounts Receivables Transaction')
   # $locale->text('Edit Accounts Receivables Transaction')
   $form->{title} = $locale->text("$title Accounts Receivables Transaction");
 
-  $form->{javascript} = qq|<script type="text/javascript">
-  <!--
-  function setTaxkey(accno, row) {
-    var taxkey = accno.options[accno.selectedIndex].value;
-    var reg = /--([0-9]*)/;
-    var found = reg.exec(taxkey);
-    var index = found[1];
-    index = parseInt(index);
-    var tax = 'taxchart_' + row;
-    for (var i = 0; i < document.getElementById(tax).options.length; ++i) {
-      var reg2 = new RegExp("^"+ index, "");
-      if (reg2.exec(document.getElementById(tax).options[i].value)) {
-        document.getElementById(tax).options[i].selected = true;
-        break;
-      }
-    }
-  };
-  //-->
-  </script>|;
-  # show history button js
-  $form->{javascript} .= qq|<script type="text/javascript" src="js/show_history.js"></script>|;
-  #/show history button js
   $readonly = ($form->{id}) ? "readonly" : "";
 
   $form->{radier} = ($::instance_conf->get_ar_changeable == 2)
@@ -265,12 +377,6 @@ sub form_header {
                       : ($::instance_conf->get_ar_changeable == 1);
   $readonly = ($form->{radier}) ? "" : $readonly;
 
-  # set option selected
-  foreach my $item (qw(customer currency department employee)) {
-    $form->{"select$item"} =~ s/ selected//;
-    $form->{"select$item"} =~ s/option>\Q$form->{$item}\E/option selected>$form->{$item}/;
-  }
-
   $form->{forex}        = $form->check_exchangerate( \%myconfig, $form->{currency}, $form->{transdate}, 'buy');
   $form->{exchangerate} = $form->{forex} if $form->{forex};
 
@@ -283,61 +389,39 @@ sub form_header {
                                     "old_id"    => \@old_project_ids },
                    "charts"    => { "key"       => "ALL_CHARTS",
                                     "transdate" => $form->{transdate} },
-                   "taxcharts" => { "key"       => "ALL_TAXCHARTS",
-                                    "module"    => "AR" },);
+                  );
+
+  $form->{ALL_DEPARTMENTS} = SL::DB::Manager::Department->get_all_sorted;
 
   $_->{link_split} = { map { $_ => 1 } split/:/, $_->{link} } for @{ $form->{ALL_CHARTS} };
 
   my %project_labels = map { $_->{id} => $_->{projectnumber} } @{ $form->{"ALL_PROJECTS"} };
 
-  my (@AR_amount_values);
-  my (@AR_values);
-  my (@AR_paid_values);
-  my %chart_labels;
-  my %charts;
-  my $taxchart_init;
+  my (@AR_paid_values, %AR_paid_labels);
+  my $default_ar_amount_chart_id;
 
   foreach my $item (@{ $form->{ALL_CHARTS} }) {
     if ($item->{link_split}{AR_amount}) {
-      $taxchart_init = $item->{tax_id} if ($taxchart_init eq "");
-      my $key = "$item->{accno}--$item->{tax_id}";
-      push(@AR_amount_values, $key);
-    } elsif ($item->{link_split}{AR}) {
-      push(@AR_values, $item->{accno});
+      $default_ar_amount_chart_id //= $item->{id};
+
     } elsif ($item->{link_split}{AR_paid}) {
       push(@AR_paid_values, $item->{accno});
+      $AR_paid_labels{$item->{accno}} = "$item->{accno}--$item->{description}";
     }
-
-    # weirdness for AR_amount
-    $chart_labels{$item->{accno}} = "$item->{accno}--$item->{description}";
-    $chart_labels{"$item->{accno}--$item->{tax_id}"} = "$item->{accno}--$item->{description}";
-
-    $charts{$item->{accno}} = $item;
   }
 
-  my %taxchart_labels = ();
-  my @taxchart_values = ();
-  my %taxcharts = ();
-  foreach my $item (@{ $form->{ALL_TAXCHARTS} }) {
-    my $key = "$item->{id}--$item->{rate}";
-    $taxchart_init = $key if ($taxchart_init eq $item->{id});
-    push(@taxchart_values, $key);
-    $taxchart_labels{$key} = "$item->{taxdescription} " . ($item->{rate} * 100) . ' %';
-    $taxcharts{$item->{id}} = $item;
-  }
-
-  my $follow_up_vc         =  $form->{customer};
-  $follow_up_vc            =~ s/--.*?//;
+  my $follow_up_vc         = $form->{customer_id} ? SL::DB::Customer->load_cached($form->{customer_id})->name : '';
   my $follow_up_trans_info =  "$form->{invnumber} ($follow_up_vc)";
 
-  $form->{javascript} .=
-    qq|<script type="text/javascript" src="js/show_vc_details.js"></script>| .
-    qq|<script type="text/javascript" src="js/follow_up.js"></script>|;
-
-#  $amount  = $locale->text('Amount');
-#  $project = $locale->text('Project');
-
+  $::request->layout->add_javascripts("autocomplete_chart.js", "show_vc_details.js", "show_history.js", "follow_up.js", "kivi.Draft.js", "kivi.GL.js", "kivi.File.js", "kivi.RecordTemplate.js", "kivi.AR.js", "kivi.CustomerVendor.js", "kivi.Validator.js");
+  # get the correct date for tax
+  my $transdate    = $::form->{transdate}    ? DateTime->from_kivitendo($::form->{transdate})    : DateTime->today_local;
+  my $deliverydate = $::form->{deliverydate} ? DateTime->from_kivitendo($::form->{deliverydate}) : undef;
+  my $taxdate      = $deliverydate ? $deliverydate : $transdate;
+  # helpers for loop
+  my $first_taxchart;
   my @transactions;
+
   for my $i (1 .. $form->{rowcount}) {
     my $transaction = {
       amount     => $form->{"amount_$i"},
@@ -345,40 +429,29 @@ sub form_header {
       project_id => ($i==$form->{rowcount}) ? $form->{globalproject_id} : $form->{"project_id_$i"},
     };
 
-    my $selected_accno_full;
-    my ($accno_row) = split(/--/, $form->{"AR_amount_$i"});
-    my $item = $charts{$accno_row};
-    $selected_accno_full = "$item->{accno}--$item->{tax_id}";
-
-    my $selected_taxchart = $form->{"taxchart_$i"};
-    my ($selected_accno, $selected_tax_id) = split(/--/, $selected_accno_full);
-    my ($previous_accno, $previous_tax_id) = split(/--/, $form->{"previous_AR_amount_$i"});
+    my (%taxchart_labels, @taxchart_values, $default_taxchart, $taxchart_to_use);
+    my $amount_chart_id = $form->{"AR_amount_chart_id_$i"} // $default_ar_amount_chart_id;
 
-    if ($previous_accno &&
-        ($previous_accno eq $selected_accno) &&
-        ($previous_tax_id ne $selected_tax_id)) {
-      my $item = $taxcharts{$selected_tax_id};
-      $selected_taxchart = "$item->{id}--$item->{rate}";
+    my $used_tax_id;
+    if ( $form->{"taxchart_$i"} ) {
+      ($used_tax_id) = split(/--/, $form->{"taxchart_$i"});
     }
-
-    if (!$form->{"taxchart_$i"}) {
-      if ($form->{"AR_amount_$i"} =~ m/.--./) {
-        $selected_taxchart = join '--', map { ($_->{id}, $_->{rate}) } first { $_->{id} == $item->{tax_id} } @{ $form->{ALL_TAXCHARTS} };
-      } else {
-        $selected_taxchart = $taxchart_init;
-      }
+    foreach my $item ( GL->get_active_taxes_for_chart($amount_chart_id, $taxdate, $used_tax_id) ) {
+      my $key             = $item->id . "--" . $item->rate;
+      $first_taxchart   //= $item;
+      $default_taxchart   = $item if $item->{is_default};
+      $taxchart_to_use    = $item if $key eq $form->{"taxchart_$i"};
+
+      push(@taxchart_values, $key);
+      $taxchart_labels{$key} = $item->taxkey . " - " . $item->taxdescription . " " . $item->rate * 100 . ' %';
     }
 
+    $taxchart_to_use    //= $default_taxchart // $first_taxchart;
+    my $selected_taxchart = $taxchart_to_use->id . '--' . $taxchart_to_use->rate;
+
     $transaction->{selectAR_amount} =
-      NTI($cgi->popup_menu('-name' => "AR_amount_$i",
-                           '-id' => "AR_amount_$i",
-                           '-style' => 'width:400px',
-                           '-onChange' => "setTaxkey(this, $i)",
-                           '-values' => \@AR_amount_values,
-                           '-labels' => \%chart_labels,
-                           '-default' => $selected_accno_full))
-      . $cgi->hidden('-name' => "previous_AR_amount_$i",
-                     '-default' => $selected_accno_full);
+        SL::Presenter::Chart::picker("AR_amount_chart_id_$i", $amount_chart_id, style => "width: 400px", type => "AR_amount", class => ($form->{initial_focus} eq "row_$i" ? "initial_focus" : ""))
+      . SL::Presenter::Tag::hidden_tag("previous_AR_amount_chart_id_$i", $amount_chart_id);
 
     $transaction->{taxchart} =
       NTI($cgi->popup_menu('-name' => "taxchart_$i",
@@ -393,13 +466,6 @@ sub form_header {
 
   $form->{invtotal_unformatted} = $form->{invtotal};
 
-  $ARselected =
-    NTI($cgi->popup_menu('-name' => "ARselected", '-id' => "ARselected",
-                         '-style' => 'width:400px',
-                         '-values' => \@AR_values, '-labels' => \%chart_labels,
-                         '-default' => $form->{ARselected}));
-
-
   $form->{paidaccounts}++ if ($form->{"paid_$form->{paidaccounts}"});
 
   my $now = $form->current_date(\%myconfig);
@@ -427,7 +493,7 @@ sub form_header {
       NTI($cgi->popup_menu('-name' => "AR_paid_$i",
                            '-id' => "AR_paid_$i",
                            '-values' => \@AR_paid_values,
-                           '-labels' => \%chart_labels,
+                           '-labels' => \%AR_paid_labels,
                            '-default' => $payment->{AR_paid} || $form->{accno_arap}));
 
 
@@ -437,6 +503,11 @@ sub form_header {
       : SL::DB::Default->get->payments_changeable == 2 ? $payment->{gldate} eq '' || $payment->{gldate} eq $now
       :                                                           1;
 
+    #deaktivieren von gebuchten Zahlungen ausserhalb der Bücherkontrolle, vorher prüfen ob heute eingegeben
+    if ($form->date_closed($payment->{"gldate_$i"})) {
+        $payment->{changeable} = 0;
+    }
+
     push @payments, $payment;
   }
 
@@ -448,6 +519,18 @@ sub form_header {
 
   $form->{totalpaid} = sum map { $_->{paid} } @payments;
 
+  my $employees = SL::DB::Manager::Employee->get_all_sorted(
+    where => [
+      or => [
+        (id     => $::form->{employee_id}) x !!$::form->{employee_id},
+        deleted => undef,
+        deleted => 0,
+      ],
+    ],
+  );
+
+  setup_ar_form_header_action_bar();
+
   $form->header;
   print $::form->parse_html_template('ar/form_header', {
     paid_missing         => $::form->{invtotal} - $::form->{totalpaid},
@@ -456,10 +539,12 @@ sub form_header {
     transactions         => \@transactions,
     project_labels       => \%project_labels,
     rows                 => $rows,
-    ARselected           => $ARselected,
+    AR_chart_id          => $form->{AR_chart_id},
     title_str            => $title,
     follow_up_trans_info => $follow_up_trans_info,
     today                => DateTime->today,
+    currencies           => scalar(SL::DB::Manager::Currency->get_all_sorted),
+    employees            => $employees,
   });
 
   $main::lxdebug->leave_sub();
@@ -468,7 +553,7 @@ sub form_header {
 sub form_footer {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  _assert_access();
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -476,49 +561,36 @@ sub form_footer {
   my $cgi      = $::request->{cgi};
 
   if ( $form->{id} ) {
-    my $follow_ups = FU->follow_ups('trans_id' => $form->{id});
+    my $follow_ups = FU->follow_ups('trans_id' => $form->{id}, 'not_done' => 1);
     if ( @{ $follow_ups} ) {
       $form->{follow_up_length} = scalar(@{$follow_ups});
       $form->{follow_up_due_length} = sum(map({ $_->{due} * 1 } @{ $follow_ups }));
     }
   }
 
-  my $transdate = $form->datetonum($form->{transdate}, \%myconfig);
-  my $closedto  = $form->datetonum($form->{closedto},  \%myconfig);
-
-  $form->{is_closed} = $transdate <= $closedto;
-
-  # ToDO: - insert a global check for stornos, so that a storno is only possible a limited time after saving it
-  $form->{show_storno_button} =
-    $form->{id} &&
-    !IS->has_storno(\%myconfig, $form, 'ar') &&
-    !IS->is_storno(\%myconfig, $form, 'ar') &&
-    ($form->{totalpaid} == 0 || $form->{totalpaid} eq "");
-
-  $form->{show_mark_as_paid_button} = $form->{id} && $::instance_conf->get_ar_show_mark_as_paid();
-
   print $::form->parse_html_template('ar/form_footer');
 
   $main::lxdebug->leave_sub();
 }
 
 sub mark_as_paid {
-  $main::lxdebug->enter_sub();
-
-  $main::auth->assert('general_ledger');
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
+  $::auth->assert('ar_transactions');
 
-  &mark_as_paid_common(\%myconfig,"ar");
+  SL::DB::Invoice->new(id => $::form->{id})->load->mark_as_paid;
+  $::form->redirect($::locale->text("Marked as paid"));
+}
 
-  $main::lxdebug->leave_sub();
+sub show_draft {
+  $::form->{transdate} = DateTime->today_local->to_kivitendo if !$::form->{transdate};
+  $::form->{gldate}    = $::form->{transdate} if !$::form->{gldate};
+  update();
 }
 
 sub update {
+  my %params = @_;
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ar_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -534,13 +606,13 @@ sub update {
   map { $form->{$_} = $form->parse_amount(\%myconfig, $form->{$_}) }
     qw(exchangerate creditlimit creditremaining);
 
-  my @flds  = qw(amount AR_amount projectnumber oldprojectnumber project_id);
+  my @flds  = qw(amount AR_amount_chart_id projectnumber oldprojectnumber project_id taxchart tax);
   my $count = 0;
   my @a     = ();
 
   for my $i (1 .. $form->{rowcount}) {
     $form->{"amount_$i"} = $form->parse_amount(\%myconfig, $form->{"amount_$i"});
-    if ($form->{"amount_$i"}) {
+    if ($form->{"amount_$i"} || $params{keep_rows_without_amount}) {
       push @a, {};
       my $j = $#a;
       my ($taxkey, $rate) = split(/--/, $form->{"taxchart_$i"});
@@ -555,7 +627,7 @@ sub update {
   }
 
   $form->redo_rows(\@flds, \@a, $count, $form->{rowcount});
-  $form->{rowcount} = $count + 1;
+  $form->{rowcount} = $count + ($params{dont_add_new_row} ? 0 : 1);
   map { $form->{invtotal} += $form->{"amount_$_"} } (1 .. $form->{rowcount});
 
   $form->{forex}        = $form->check_exchangerate( \%myconfig, $form->{currency}, $form->{transdate}, 'buy');
@@ -563,17 +635,12 @@ sub update {
 
   $form->{invdate} = $form->{transdate};
 
-  $form->{invdate} = $form->{transdate};
-
-  my %saved_variables = map +( $_ => $form->{$_} ), qw(AR AR_amount_1 taxchart_1 customer_id notes);
-
-  &check_name("customer");
-
-  $form->{AR} = $saved_variables{AR};
-  if ($saved_variables{AR_amount_1} =~ m/.--./) {
-    map { $form->{$_} = $saved_variables{$_} } qw(AR_amount_1 taxchart_1);
-  } else {
-    delete $form->{taxchart_1};
+  if (($form->{previous_customer_id} || $form->{customer_id}) != $form->{customer_id}) {
+    IS->get_customer(\%myconfig, $form);
+    if (($form->{rowcount} == 1) && ($form->{amount_1} == 0)) {
+      my $last_used_ar_chart = SL::DB::Customer->load_cached($form->{customer_id})->last_used_ar_chart;
+      $form->{"AR_amount_chart_id_1"} = $last_used_ar_chart->id if $last_used_ar_chart;
+    }
   }
 
   $form->{invtotal} =
@@ -599,7 +666,7 @@ sub update {
   $form->{oldinvtotal}  = $form->{invtotal};
   $form->{oldtotalpaid} = $form->{totalpaid};
 
-  &display_form;
+  display_form();
 
   $main::lxdebug->leave_sub();
 }
@@ -610,7 +677,7 @@ sub update {
 sub post_payment {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ar_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -628,7 +695,13 @@ sub post_payment {
 
       $form->isblank("datepaid_$i", $locale->text('Payment date missing!'));
 
-      $form->error($locale->text('Cannot post payment for a closed period!')) if ($form->date_closed($form->{"datepaid_$i"}, \%myconfig));
+      $form->error($locale->text('Cannot post transaction above the maximum future booking date!'))
+        if ($form->date_max_future($form->{"datepaid_$i"}, \%myconfig));
+
+      #Zusätzlich noch das Buchungsdatum in die Bücherkontrolle einbeziehen
+      # (Dient zur Prüfung ob ZE oder ZA geprüft werden soll)
+      $form->error($locale->text('Cannot post payment for a closed period!'))
+        if ($form->date_closed($form->{"datepaid_$i"})  && !$form->date_closed($form->{"gldate_$i"}, \%myconfig));
 
       if ($form->{defaultcurrency} && ($form->{currency} ne $form->{defaultcurrency})) {
 #        $form->{"exchangerate_$i"} = $form->{exchangerate} if ($invdate == $datepaid);
@@ -654,7 +727,7 @@ sub post_payment {
 
 sub _post {
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ar_transactions');
 
   my $form     = $main::form;
 
@@ -665,7 +738,7 @@ sub _post {
 sub post {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ar_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -680,9 +753,9 @@ sub post {
   # check if there is an invoice number, invoice and due date
   $form->isblank("transdate", $locale->text('Invoice Date missing!'));
   $form->isblank("duedate",   $locale->text('Due Date missing!'));
-  $form->isblank("customer",  $locale->text('Customer missing!'));
+  $form->isblank("customer_id", $locale->text('Customer missing!'));
 
-  if ($myconfig{mandatory_departments} && !$form->{department}) {
+  if ($myconfig{mandatory_departments} && !$form->{department_id}) {
     $form->{saved_message} = $::locale->text('You have to specify a department.');
     update();
     exit;
@@ -693,6 +766,7 @@ sub post {
 
   $form->error($locale->text('Cannot post transaction above the maximum future booking date!'))
     if ($form->date_max_future($transdate, \%myconfig));
+
   $form->error($locale->text('Cannot post transaction for a closed period!')) if ($form->date_closed($form->{"transdate"}, \%myconfig));
 
   $form->error($locale->text('Zero amount posting!'))
@@ -709,8 +783,13 @@ sub post {
 
       $form->isblank("datepaid_$i", $locale->text('Payment date missing!'));
 
+      $form->error($locale->text('Cannot post transaction above the maximum future booking date!'))
+        if ($form->date_max_future($form->{"datepaid_$i"}, \%myconfig));
+
+      #Zusätzlich noch das Buchungsdatum in die Bücherkontrolle einbeziehen
+      # (Dient zur Prüfung ob ZE oder ZA geprüft werden soll)
       $form->error($locale->text('Cannot post payment for a closed period!'))
-        if ($form->date_closed($form->{"datepaid_$i"}, \%myconfig));
+        if ($form->date_closed($form->{"datepaid_$i"})  && !$form->date_closed($form->{"gldate_$i"}, \%myconfig));
 
       if ($form->{defaultcurrency} && ($form->{currency} ne $form->{defaultcurrency})) {
         $form->{"exchangerate_$i"} = $form->{exchangerate} if ($transdate == $datepaid);
@@ -720,10 +799,9 @@ sub post {
   }
 
   # if oldcustomer ne customer redo form
-  my ($customer) = split /--/, $form->{customer};
-  if ($form->{oldcustomer} ne "$customer--$form->{customer_id}") {
+  if (($form->{previous_customer_id} || $form->{customer_id}) != $form->{customer_id}) {
     update();
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
 
   $form->{AR}{receivables} = $form->{ARselected};
@@ -740,9 +818,19 @@ sub post {
     $form->save_history;
   }
   # /saving the history
-  remove_draft() if $form->{remove_draft};
 
-  $form->redirect($locale->text('Transaction posted!')) unless $inline;
+  if (!$inline) {
+    my $msg = $locale->text("AR transaction '#1' posted (ID: #2)", $form->{invnumber}, $form->{id});
+    if ($::instance_conf->get_ar_add_doc && $::instance_conf->get_doc_storage) {
+      my $add_doc_url = build_std_url("script=ar.pl", 'action=edit', 'id=' . E($form->{id}));
+      SL::Helper::Flash::flash_later('info', $msg);
+      print $form->redirect_header($add_doc_url);
+      $::dispatcher->end_request;
+
+    } else {
+      $form->redirect($msg);
+    }
+  }
 
   $main::lxdebug->leave_sub();
 }
@@ -750,7 +838,7 @@ sub post {
 sub post_as_new {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ar_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -772,63 +860,31 @@ sub post_as_new {
 sub use_as_new {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ar_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  map { delete $form->{$_} } qw(printed emailed queued invnumber invdate deliverydate id datepaid_1 gldate_1 acc_trans_id_1 source_1 memo_1 paid_1 exchangerate_1 AP_paid_1 storno);
+  map { delete $form->{$_} } qw(printed emailed queued invnumber deliverydate id datepaid_1 gldate_1 acc_trans_id_1 source_1 memo_1 paid_1 exchangerate_1 AP_paid_1 storno);
   $form->{paidaccounts} = 1;
   $form->{rowcount}--;
-  $form->{invdate} = $form->current_date(\%myconfig);
-  &update;
-
-  $main::lxdebug->leave_sub();
-}
 
-sub delete {
-  $main::lxdebug->enter_sub();
+  my $today          = DateTime->today_local;
+  $form->{transdate} = $today->to_kivitendo;
+  $form->{duedate}   = $form->{transdate};
 
-  $main::auth->assert('general_ledger');
-
-  my $form     = $main::form;
-  my $locale   = $main::locale;
-
-  $form->{title} = $locale->text('Confirm!');
-
-  $form->header;
-
-  delete $form->{header};
-
-  print qq|
-<form method=post action=$form->{script}>
-|;
-
-  foreach my $key (keys %$form) {
-    next if (($key eq 'login') || ($key eq 'password') || ('' ne ref $form->{$key}));
-    $form->{$key} =~ s/\"/&quot;/g;
-    print qq|<input type=hidden name=$key value="$form->{$key}">\n|;
+  if ($form->{customer_id}) {
+    my $payment_terms = SL::DB::Customer->load_cached($form->{customer_id})->payment;
+    $form->{duedate}  = $payment_terms->calc_date(reference_date => $today)->to_kivitendo if $payment_terms;
   }
 
-  print qq|
-<h2 class=confirm>$form->{title}</h2>
-
-<h4>|
-    . $locale->text('Are you sure you want to delete Transaction')
-    . qq| $form->{invnumber}</h4>
-
-<input name=action class=submit type=submit value="|
-    . $locale->text('Yes') . qq|">
-</form>
-|;
+  &update;
 
   $main::lxdebug->leave_sub();
 }
 
-sub yes {
-  $main::lxdebug->enter_sub();
-
-  $main::auth->assert('general_ledger');
+sub delete {
+  $::auth->assert('ar_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -846,32 +902,69 @@ sub yes {
     $form->redirect($locale->text('Transaction deleted!'));
   }
   $form->error($locale->text('Cannot delete transaction!'));
+}
 
-  $main::lxdebug->leave_sub();
+sub setup_ar_search_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        $::locale->text('Search'),
+        submit    => [ '#form' ],
+        checks    => [ 'kivi.validate_form' ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+  $::request->layout->add_javascripts('kivi.Validator.js');
+}
+
+sub setup_ar_transactions_action_bar {
+  my %params          = @_;
+  my $may_edit_create = $::auth->assert('invoice_edit', 1);
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        $::locale->text('Print'),
+        call     => [ 'kivi.MassInvoiceCreatePrint.showMassPrintOptionsOrDownloadDirectly' ],
+        disabled => !$may_edit_create  ? t8('You do not have the permissions to access this function.')
+                  : !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.')
+                  :                      undef,
+      ],
+
+      combobox => [
+        action => [ $::locale->text('Create new') ],
+        action => [
+          $::locale->text('AR Transaction'),
+          submit   => [ '#create_new_form', { action => 'ar_transaction' } ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          $::locale->text('Sales Invoice'),
+          submit   => [ '#create_new_form', { action => 'sales_invoice' } ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+      ], # end of combobox "Create new"
+    );
+  }
 }
 
 sub search {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('invoice_edit');
-
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
   my $cgi      = $::request->{cgi};
 
-  # setup customer selection
-  $form->all_vc(\%myconfig, "customer", "AR");
-
-  $form->{title}    = $locale->text('AR Transactions');
+  $form->{title} = $locale->text('Invoices, Credit Notes & AR Transactions');
 
-  # Auch in Rechnungsübersicht nach Kundentyp filtern - jan
-  $form->get_lists("projects"       => { "key" => "ALL_PROJECTS", "all" => 1 },
-                   "departments"    => "ALL_DEPARTMENTS",
-                   "customers"      => "ALL_VC",
-                   "business_types" => "ALL_BUSINESS_TYPES");
-  $form->{ALL_EMPLOYEES} = SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]);
-  $form->{SHOW_BUSINESS_TYPES} = scalar @{ $form->{ALL_BUSINESS_TYPES} } > 0;
+  $form->{ALL_EMPLOYEES}      = SL::DB::Manager::Employee  ->get_all_sorted(query => [ deleted => 0 ]);
+  $form->{ALL_DEPARTMENTS}    = SL::DB::Manager::Department->get_all_sorted;
+  $form->{ALL_BUSINESS_TYPES} = SL::DB::Manager::Business  ->get_all_sorted;
+  $form->{ALL_TAXZONES}       = SL::DB::Manager::TaxZone   ->get_all_sorted;
 
   $form->{CT_CUSTOM_VARIABLES}                  = CVar->get_configs('module' => 'CT');
   ($form->{CT_CUSTOM_VARIABLES_FILTER_CODE},
@@ -882,6 +975,10 @@ sub search {
   # constants and subs for template
   $form->{vc_keys}   = sub { "$_[0]->{name}--$_[0]->{id}" };
 
+  $::request->layout->add_javascripts("autocomplete_project.js");
+
+  setup_ar_search_action_bar();
+
   $form->header;
   print $form->parse_html_template('ar/search', { %myconfig });
 
@@ -912,28 +1009,26 @@ sub create_subtotal_row {
 sub ar_transactions {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('invoice_edit');
-
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
   my ($callback, $href, @columns);
 
-  ($form->{customer}, $form->{customer_id}) = split(/--/, $form->{customer});
-
+  my %params   = @_;
   report_generator_set_default_sort('transdate', 1);
 
   AR->ar_transactions(\%myconfig, \%$form);
 
-  $form->{title} = $locale->text('AR Transactions');
+  $form->{title} = $locale->text('Invoices, Credit Notes & AR Transactions');
 
   my $report = SL::ReportGenerator->new(\%myconfig, $form);
 
   @columns =
-    qw(transdate id type invnumber ordnumber cusordnumber name netamount tax amount paid
+    qw(ids transdate id type invnumber ordnumber cusordnumber donumber deliverydate name netamount tax amount paid
        datepaid due duedate transaction_description notes salesman employee shippingpoint shipvia
-       marge_total marge_percent globalprojectnumber customernumber country ustid taxzone payment_terms charts customertype direct_debit dunning_description);
+       marge_total marge_percent globalprojectnumber customernumber country ustid taxzone
+       payment_terms charts customertype direct_debit dunning_description department attachments);
 
   my $ct_cvar_configs                 = CVar->get_configs('module' => 'CT');
   my @ct_includeable_custom_variables = grep { $_->{includeable} } @{ $ct_cvar_configs };
@@ -943,18 +1038,23 @@ sub ar_transactions {
   push @columns, map { "cvar_$_->{name}" } @ct_includeable_custom_variables;
 
   my @hidden_variables = map { "l_${_}" } @columns;
-  push @hidden_variables, "l_subtotal", qw(open closed customer invnumber ordnumber cusordnumber transaction_description notes project_id transdatefrom transdateto duedatefrom duedateto employee_id salesman_id business_id);
+  push @hidden_variables, "l_subtotal", qw(open closed customer invnumber ordnumber cusordnumber transaction_description notes project_id transdatefrom transdateto duedatefrom duedateto
+                                           employee_id salesman_id business_id parts_partnumber parts_description department_id show_marked_as_closed show_not_mailed
+                                           shippingpoint shipvia taxzone_id);
   push @hidden_variables, map { "cvar_$_->{name}" } @ct_searchable_custom_variables;
 
-  $href = build_std_url('action=ar_transactions', grep { $form->{$_} } @hidden_variables);
+  $href =  $params{want_binary_pdf} ? '' : build_std_url('action=ar_transactions', grep { $form->{$_} } @hidden_variables);
 
   my %column_defs = (
+    'ids'                     => { raw_header_data => SL::Presenter::Tag::checkbox_tag("", id => "check_all", checkall => "[data-checkall=1]"), align => 'center' },
     'transdate'               => { 'text' => $locale->text('Date'), },
     'id'                      => { 'text' => $locale->text('ID'), },
     'type'                    => { 'text' => $locale->text('Type'), },
     'invnumber'               => { 'text' => $locale->text('Invoice'), },
     'ordnumber'               => { 'text' => $locale->text('Order'), },
     'cusordnumber'            => { 'text' => $locale->text('Customer Order Number'), },
+    'donumber'                => { 'text' => $locale->text('Delivery Order'), },
+    'deliverydate'            => { 'text' => $locale->text('Delivery Date'), },
     'name'                    => { 'text' => $locale->text('Customer'), },
     'netamount'               => { 'text' => $locale->text('Amount'), },
     'tax'                     => { 'text' => $locale->text('Tax'), },
@@ -977,14 +1077,16 @@ sub ar_transactions {
     'ustid'                   => { 'text' => $locale->text('USt-IdNr.'), },
     'taxzone'                 => { 'text' => $locale->text('Steuersatz'), },
     'payment_terms'           => { 'text' => $locale->text('Payment Terms'), },
-    'charts'                  => { 'text' => $locale->text('Buchungskonto'), },
+    'charts'                  => { 'text' => $locale->text('Chart'), },
     'customertype'            => { 'text' => $locale->text('Customer type'), },
     'direct_debit'            => { 'text' => $locale->text('direct debit'), },
+    'department'              => { 'text' => $locale->text('Department'), },
     dunning_description       => { 'text' => $locale->text('Dunning level'), },
+    attachments               => { 'text' => $locale->text('Attachments'), },
     %column_defs_cvars,
   );
 
-  foreach my $name (qw(id transdate duedate invnumber ordnumber cusordnumber name datepaid employee shippingpoint shipvia transaction_description direct_debit)) {
+  foreach my $name (qw(id transdate duedate invnumber ordnumber cusordnumber donumber deliverydate name datepaid employee shippingpoint shipvia transaction_description direct_debit department taxzone)) {
     my $sortdir                 = $form->{sort} eq $name ? 1 - $form->{sortdir} : $form->{sortdir};
     $column_defs{$name}->{link} = $href . "&sort=$name&sortdir=$sortdir";
   }
@@ -994,6 +1096,8 @@ sub ar_transactions {
   $form->{"l_type"} = "Y";
   map { $column_defs{$_}->{visible} = $form->{"l_${_}"} ? 1 : 0 } @columns;
 
+  $column_defs{ids}->{visible} = 'HTML';
+
   $report->set_columns(%column_defs);
   $report->set_column_order(@columns);
 
@@ -1014,12 +1118,10 @@ sub ar_transactions {
   if ($form->{cp_name}) {
     push @options, $locale->text('Contact Person') . " : $form->{cp_name}";
   }
-  if ($form->{department}) {
-    my ($department) = split /--/, $form->{department};
-    push @options, $locale->text('Department') . " : $department";
-  }
+
   if ($form->{department_id}) {
-    push @options, $locale->text('Department Id') . " : $form->{department_id}";
+    my $department = SL::DB::Manager::Department->find_by( id => $form->{department_id} );
+    push @options, $locale->text('Department') . " : " . $department->description;
   }
   if ($form->{invnumber}) {
     push @options, $locale->text('Invoice Number') . " : $form->{invnumber}";
@@ -1036,6 +1138,12 @@ sub ar_transactions {
   if ($form->{transaction_description}) {
     push @options, $locale->text('Transaction description') . " : $form->{transaction_description}";
   }
+  if ($form->{parts_partnumber}) {
+    push @options, $locale->text('Part Number') . " : $form->{parts_partnumber}";
+  }
+  if ($form->{parts_description}) {
+    push @options, $locale->text('Part Description') . " : $form->{parts_description}";
+  }
   if ($form->{transdatefrom}) {
     push @options, $locale->text('From') . " " . $locale->date(\%myconfig, $form->{transdatefrom}, 1);
   }
@@ -1056,8 +1164,18 @@ sub ar_transactions {
   if ($form->{closed}) {
     push @options, $locale->text('Closed');
   }
+  if ($form->{shipvia}) {
+    push @options, $locale->text('Ship via') . " : $form->{shipvia}";
+  }
+  if ($form->{shippingpoint}) {
+    push @options, $locale->text('Shipping Point') . " : $form->{shippingpoint}";
+  }
+
+
+  $form->{ALL_PRINTERS} = SL::DB::Manager::Printer->get_all_sorted;
 
   $report->set_options('top_info_text'        => join("\n", @options),
+                       'raw_top_info_text'    => $form->parse_html_template('ar/ar_transactions_header'),
                        'raw_bottom_info_text' => $form->parse_html_template('ar/ar_transactions_bottom'),
                        'output_format'        => 'HTML',
                        'title'                => $form->{title},
@@ -1089,17 +1207,32 @@ sub ar_transactions {
     $subtotals{marge_percent} = $subtotals{netamount} ? ($subtotals{marge_total} * 100 / $subtotals{netamount}) : 0;
     $totals{marge_percent}    = $totals{netamount}    ? ($totals{marge_total}    * 100 / $totals{netamount}   ) : 0;
 
-    map { $ar->{$_} = $form->format_amount(\%myconfig, $ar->{$_}, 2) } qw(netamount tax amount paid due marge_total marge_percent);
+    # Preserve $ar->{type} before changing it to the abbreviation letter for
+    # getting files from file management below.
+    $ar->{object_type} = $ar->{type};
 
     my $is_storno  = $ar->{storno} &&  $ar->{storno_id};
     my $has_storno = $ar->{storno} && !$ar->{storno_id};
 
-    $ar->{type} =
-      $has_storno       ? $locale->text("Invoice with Storno (abbreviation)") :
-      $is_storno        ? $locale->text("Storno (one letter abbreviation)") :
-      $ar->{amount} < 0 ? $locale->text("Credit note (one letter abbreviation)") :
-      $ar->{invoice}    ? $locale->text("Invoice (one letter abbreviation)") :
-                          $locale->text("AR Transaction (abbreviation)");
+    if ($ar->{type} eq 'invoice_for_advance_payment') {
+      $ar->{type} =
+        $has_storno       ? $locale->text("Invoice for Advance Payment with Storno (abbreviation)") :
+        $is_storno        ? $locale->text("Storno (one letter abbreviation)") :
+                            $locale->text("Invoice for Advance Payment (one letter abbreviation)");
+
+    } elsif ($ar->{type} eq 'final_invoice') {
+      $ar->{type} = t8('Final Invoice (one letter abbreviation)');
+
+    } else {
+      $ar->{type} =
+        $has_storno       ? $locale->text("Invoice with Storno (abbreviation)") :
+        $is_storno        ? $locale->text("Storno (one letter abbreviation)") :
+        $ar->{amount} < 0 ? $locale->text("Credit note (one letter abbreviation)") :
+        $ar->{invoice}    ? $locale->text("Invoice (one letter abbreviation)") :
+                            $locale->text("AR Transaction (abbreviation)");
+    }
+
+    map { $ar->{$_} = $form->format_amount(\%myconfig, $ar->{$_}, 2) } qw(netamount tax amount paid due marge_total marge_percent);
 
     $ar->{direct_debit} = $ar->{direct_debit} ? $::locale->text('yes') : $::locale->text('no');
 
@@ -1113,7 +1246,27 @@ sub ar_transactions {
     }
 
     $row->{invnumber}->{link} = build_std_url("script=" . ($ar->{invoice} ? 'is.pl' : 'ar.pl'), 'action=edit')
-      . "&id=" . E($ar->{id}) . "&callback=${callback}";
+      . "&id=" . E($ar->{id}) . "&callback=${callback}" unless $params{want_binary_pdf};
+
+    $row->{ids} = {
+      raw_data =>  SL::Presenter::Tag::checkbox_tag("id[]", value => $ar->{id}, "data-checkall" => 1),
+      valign   => 'center',
+      align    => 'center',
+    };
+
+    if ($::instance_conf->get_doc_storage && $form->{l_attachments}) {
+      my @files  = SL::File->get_all_versions(object_id   => $ar->{id},
+                                              object_type => $ar->{object_type} || 'invoice',
+                                              file_type   => 'attachment',);
+      if (scalar @files) {
+        my $html            = join '<br>', map { SL::Presenter::FileObject::file_object($_) } @files;
+        my $text            = join "\n",   map { $_->file_name                              } @files;
+        $row->{attachments} = { 'raw_data' => $html, data => $text };
+      } else {
+        $row->{attachments} = { };
+      }
+
+    }
 
     my $row_set = [ $row ];
 
@@ -1131,6 +1284,14 @@ sub ar_transactions {
   $report->add_separator();
   $report->add_data(create_subtotal_row(\%totals, \@columns, \%column_alignment, \@subtotal_columns, 'listtotal'));
 
+  if ($params{want_binary_pdf}) {
+    $report->generate_with_headers();
+    return $report->generate_pdf_content(want_binary_pdf => 1);
+  }
+
+  $::request->layout->add_javascripts('kivi.MassInvoiceCreatePrint.js');
+  setup_ar_transactions_action_bar(num_rows => scalar(@{ $form->{AR} }));
+
   $report->generate_with_headers();
 
   $main::lxdebug->leave_sub();
@@ -1139,7 +1300,7 @@ sub ar_transactions {
 sub storno {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('ar_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -1167,4 +1328,134 @@ sub storno {
   $main::lxdebug->leave_sub();
 }
 
+sub setup_ar_form_header_action_bar {
+  my $transdate               = $::form->datetonum($::form->{transdate}, \%::myconfig);
+  my $closedto                = $::form->datetonum($::form->{closedto},  \%::myconfig);
+  my $is_closed               = $transdate <= $closedto;
+
+  my $change_never            = $::instance_conf->get_ar_changeable == 0;
+  my $change_on_same_day_only = $::instance_conf->get_ar_changeable == 2 && ($::form->current_date(\%::myconfig) ne $::form->{gldate});
+
+  my $is_storno               = IS->is_storno(\%::myconfig, $::form, 'ar', $::form->{id});
+  my $has_storno              = IS->has_storno(\%::myconfig, $::form, 'ar');
+  my $may_edit_create         = $::auth->assert('ar_transactions', 1);
+
+  my $is_linked_bank_transaction;
+  if ($::form->{id}
+      && SL::DB::Default->get->payments_changeable != 0
+      && SL::DB::Manager::BankTransactionAccTrans->find_by(ar_id => $::form->{id})) {
+
+    $is_linked_bank_transaction = 1;
+  }
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#form', { action => "update" } ],
+        id        => 'update_button',
+        checks    => [ 'kivi.validate_form' ],
+        disabled  => !$may_edit_create ? t8('You must not change this AR transaction.') : undef,
+        accesskey => 'enter',
+      ],
+
+      combobox => [
+        action => [
+          t8('Post'),
+          submit   => [ '#form', { action => "post" } ],
+          checks   => [ 'kivi.validate_form', 'kivi.AR.check_fields_before_posting' ],
+          disabled => !$may_edit_create                           ? t8('You must not change this AR transaction.')
+                    : $is_closed                                  ? t8('The billing period has already been locked.')
+                    : $is_storno                                  ? t8('A canceled invoice cannot be posted.')
+                    : ($::form->{id} && $change_never)            ? t8('Changing invoices has been disabled in the configuration.')
+                    : ($::form->{id} && $change_on_same_day_only) ? t8('Invoices can only be changed on the day they are posted.')
+                    : $is_linked_bank_transaction                 ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                    :                                               undef,
+        ],
+        action => [
+          t8('Post Payment'),
+          submit   => [ '#form', { action => "post_payment" } ],
+          disabled => !$may_edit_create           ? t8('You must not change this AR transaction.')
+                    : !$::form->{id}              ? t8('This invoice has not been posted yet.')
+                    : $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                    :                               undef,
+        ],
+        action => [ t8('Mark as paid'),
+          submit   => [ '#form', { action => "mark_as_paid" } ],
+          confirm  => t8('This will remove the invoice from showing as unpaid even if the unpaid amount does not match the amount. Proceed?'),
+          disabled => !$may_edit_create ? t8('You must not change this AR transaction.')
+                    : !$::form->{id}    ? t8('This invoice has not been posted yet.')
+                    :                     undef,
+          only_if  => $::instance_conf->get_is_show_mark_as_paid,
+        ],
+      ], # end of combobox "Post"
+
+      combobox => [
+        action => [ t8('Storno'),
+          submit   => [ '#form', { action => "storno" } ],
+          checks   => [ 'kivi.validate_form', 'kivi.AR.check_fields_before_posting' ],
+          confirm  => t8('Do you really want to cancel this invoice?'),
+          disabled => !$may_edit_create    ? t8('You must not change this AR transaction.')
+                    : !$::form->{id}       ? t8('This invoice has not been posted yet.')
+                    : $has_storno          ? t8('This invoice has been canceled already.')
+                    : $is_storno           ? t8('Reversal invoices cannot be canceled.')
+                    : $::form->{totalpaid} ? t8('Invoices with payments cannot be canceled.')
+                    :                        undef,
+        ],
+        action => [ t8('Delete'),
+          submit   => [ '#form', { action => "delete" } ],
+          confirm  => t8('Do you really want to delete this object?'),
+          disabled => !$may_edit_create        ? t8('You must not change this AR transaction.')
+                    : !$::form->{id}           ? t8('This invoice has not been posted yet.')
+                    : $change_never            ? t8('Changing invoices has been disabled in the configuration.')
+                    : $change_on_same_day_only ? t8('Invoices can only be changed on the day they are posted.')
+                    : $is_closed               ? t8('The billing period has already been locked.')
+                    :                            undef,
+        ],
+      ], # end of combobox "Storno"
+
+      'separator',
+
+      combobox => [
+        action => [ t8('Workflow') ],
+        action => [
+          t8('Use As New'),
+          submit   => [ '#form', { action => "use_as_new" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create ? t8('You must not change this AR transaction.')
+                    : !$::form->{id} ? t8('This invoice has not been posted yet.')
+                    :                  undef,
+        ],
+      ], # end of combobox "Workflow"
+
+      combobox => [
+        action => [ t8('more') ],
+        action => [
+          t8('History'),
+          call     => [ 'set_history_window', $::form->{id} * 1, 'glid' ],
+          disabled => !$::form->{id} ? t8('This invoice has not been posted yet.') : undef,
+        ],
+        action => [
+          t8('Follow-Up'),
+          call     => [ 'follow_up_window' ],
+          disabled => !$::form->{id} ? t8('This invoice has not been posted yet.') : undef,
+        ],
+        action => [
+          t8('Record templates'),
+          call     => [ 'kivi.RecordTemplate.popup', 'ar_transaction' ],
+          disabled => !$may_edit_create ? t8('You must not change this AR transaction.') : undef,
+        ],
+        action => [
+          t8('Drafts'),
+          call     => [ 'kivi.Draft.popup', 'ar', 'invoice', $::form->{draft_id}, $::form->{draft_description} ],
+          disabled => !$may_edit_create ? t8('You must not change this AR transaction.')
+                    : $::form->{id}     ? t8('This invoice has already been posted.')
+                    : $is_closed        ? t8('The billing period has already been locked.')
+                    :                     undef,
+        ],
+      ], # end of combobox "more"
+    );
+  }
+  $::request->layout->add_javascripts('kivi.Validator.js');
+}
+
 1;
diff --git a/bin/mozilla/arap.pl b/bin/mozilla/arap.pl
deleted file mode 100644 (file)
index 75764a4..0000000
+++ /dev/null
@@ -1,487 +0,0 @@
-#=====================================================================
-# LX-Office ERP
-# Copyright (C) 2004
-# Based on SQL-Ledger Version 2.1.9
-# Web http://www.lx-office.org
-#
-#=====================================================================
-# SQL-Ledger Accounting
-# Copyright (c) 2002
-#
-#  Author: Dieter Simader
-#   Email: dsimader@sql-ledger.org
-#     Web: http://www.sql-ledger.org
-#
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-#======================================================================
-#
-# common routines for gl, ar, ap, is, ir, oe
-#
-
-use strict;
-
-# any custom scripts for this one
-if (-f "bin/mozilla/custom_arap.pl") {
-  eval { require "bin/mozilla/custom_arap.pl"; };
-}
-if (-f "bin/mozilla/$::myconfig{login}_arap.pl") {
-  eval { require "bin/mozilla/$::myconfig{login}_arap.pl"; };
-}
-
-1;
-
-require "bin/mozilla/common.pl";
-
-# end of main
-
-sub check_name {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  $main::auth->assert('general_ledger               | vendor_invoice_edit       | sales_order_edit    | invoice_edit |' .
-                'request_quotation_edit       | sales_quotation_edit      | purchase_order_edit | cash         |' .
-                'purchase_delivery_order_edit | sales_delivery_order_edit');
-
-  my ($name, %params) = @_;
-
-  $name = $name eq "customer" ? "customer" : "vendor";
-
-  my ($new_name,$new_id) = $form->{$name} =~ /^(.*?)--(\d+)$/;
-  my $i = 0;
-  # if we use a selection
-  if ($form->{"select$name"}) {
-    if ($form->{"old$name"} ne $form->{$name}) {
-
-      # this is needed for is, ir and oe
-      $form->{update} = 0;
-      # for credit calculations
-      $form->{oldinvtotal}  = 0;
-      $form->{oldtotalpaid} = 0;
-      $form->{calctax}      = 1;
-
-      $form->{"${name}_id"} = $new_id;
-
-      _reset_salesman_id();
-      delete @{ $form }{qw(payment_id)};
-
-      IS->get_customer(\%myconfig, \%$form) if ($name eq 'customer');
-      IR->get_vendor(\%myconfig, \%$form) if ($name eq 'vendor');
-
-      $form->{$name} = $form->{"old$name"} = "$new_name--$new_id";
-
-      $i = 1;
-    }
-  } else {
-
-    # check name, combine name and id
-    if ($form->{"old$name"} ne qq|$form->{$name}--$form->{"${name}_id"}|) {
-
-      # this is needed for is, ir and oe
-      $form->{update} = 0;
-
-      # for credit calculations
-      $form->{oldinvtotal}  = 0;
-      $form->{oldtotalpaid} = 0;
-      $form->{calctax}      = 1;
-
-      # return one name or a list of names in $form->{name_list}
-      $i = $form->get_name(\%myconfig, $name);
-
-      if ($i > 1) {
-        if ($params{no_select}) {
-          # $locale->text('Customer')
-          # $locale->text('Vendor')
-          $form->error($locale->text("More than one #1 found matching, please be more specific.", $locale->text(ucfirst $name)));
-        } else {
-          &select_name($name);
-          ::end_of_request();
-        }
-      }
-
-      if ($i == 1) {
-
-        # we got one name
-        $form->{"${name}_id"} = $form->{name_list}[0]->{id};
-        $form->{$name}        = $form->{name_list}[0]->{name};
-        $form->{"old$name"}   = qq|$form->{$name}--$form->{"${name}_id"}|;
-
-        _reset_salesman_id();
-        delete @{ $form }{qw(payment_id)};
-
-        IS->get_customer(\%myconfig, \%$form) if ($name eq 'customer');
-        IR->get_vendor(\%myconfig, \%$form) if ($name eq 'vendor');
-
-      } else {
-
-        # name is not on file
-        # $locale->text('Customer not on file or locked!')
-        # $locale->text('Vendor not on file or locked!')
-        my $msg = ucfirst $name . " not on file or locked!";
-        $form->error($locale->text($msg));
-      }
-    }
-  }
-  $form->language_payment(\%myconfig);
-
-  $main::lxdebug->leave_sub();
-
-  return $i;
-}
-
-# $locale->text('Customer not on file!')
-# $locale->text('Vendor not on file!')
-
-sub select_name {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my $locale   = $main::locale;
-
-  $main::auth->assert('general_ledger         | vendor_invoice_edit  | sales_order_edit    | invoice_edit | sales_delivery_order_edit |' .
-                'request_quotation_edit | sales_quotation_edit | purchase_order_edit | cash');
-
-  my ($table) = @_;
-
-  my @column_index = qw(ndx name address);
-
-  my $label             = ucfirst $table;
-  my %column_data;
-  $column_data{ndx}  = qq|<th>&nbsp;</th>|;
-  $column_data{name} =
-    qq|<th class=listheading>| . $locale->text($label) . qq|</th>|;
-  $column_data{address} =
-    qq|<th class=listheading>| . $locale->text('Address') . qq|</th>|;
-
-  # list items with radio button on a form
-  $form->header;
-
-  my $title = $locale->text('Select from one of the names below');
-
-  print qq|
-    <h1>$title</h1>
-
-<form method=post action=$form->{script}>
-
-<table width=100%>
-  <tr>
-    <td>
-      <table width=100%>
-        <tr class=listheading>|;
-
-  map { print "\n$column_data{$_}" } @column_index;
-
-  print qq|
-        </tr>
-|;
-
-  my $i = 0;
-  my $j;
-  foreach my $ref (@{ $form->{name_list} }) {
-    my $checked = ($i++) ? "" : "checked";
-
-    $ref->{name} =~ s/\"/&quot;/g;
-
-    $column_data{ndx} =
-      qq|<td><input name=ndx class=radio type=radio value=$i $checked></td>|;
-    $column_data{name} =
-      qq|<td><input name="new_name_$i" type=hidden value="$ref->{name}">$ref->{name}</td>|;
-    $column_data{address} = qq|<td>$ref->{address}&nbsp;</td>|;
-
-    $j++;
-    $j %= 2;
-    print qq|
-        <tr class=listrow$j>|;
-
-    map { print "\n$column_data{$_}" } @column_index;
-
-    print qq|
-        </tr>
-
-<input name="new_id_$i" type=hidden value=$ref->{id}>
-
-|;
-
-  }
-
-  print qq|
-      </table>
-    </td>
-  </tr>
-  <tr>
-    <td><hr size=3 noshade></td>
-  </tr>
-</table>
-
-<input name=lastndx type=hidden value=$i>
-
-|;
-
-  # delete variables
-  map { delete $form->{$_} } qw(action name_list header);
-
-  # save all other form variables
-  foreach my $key (keys %${form}) {
-    next if (($key eq 'login') || ($key eq 'password') || ('' ne ref $form->{$key}));
-    $form->{$key} =~ s/\"/&quot;/g;
-    print qq|<input name=$key type=hidden value="$form->{$key}">\n|;
-  }
-
-  print qq|
-<input type=hidden name=nextsub value=name_selected>
-
-<input type=hidden name=vc value=$table>
-<br>
-<input class=submit type=submit name=action value="|
-    . $locale->text('Continue') . qq|">
-</form>
-|;
-
-  $main::lxdebug->leave_sub();
-}
-
-sub name_selected {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-
-  $main::auth->assert('general_ledger         | vendor_invoice_edit  | sales_order_edit    | invoice_edit | sales_delivery_order_edit | ' .
-                'request_quotation_edit | sales_quotation_edit | purchase_order_edit | cash');
-
-  # replace the variable with the one checked
-
-  # index for new item
-  my $i = $form->{ndx};
-
-  _reset_salesman_id();
-
-  $form->{ $form->{vc} }    = $form->{"new_name_$i"};
-  $form->{"$form->{vc}_id"} = $form->{"new_id_$i"};
-  $form->{"old$form->{vc}"} =
-    qq|$form->{$form->{vc}}--$form->{"$form->{vc}_id"}|;
-
-  # delete all the new_ variables
-  for $i (1 .. $form->{lastndx}) {
-    map { delete $form->{"new_${_}_$i"} } qw(id name);
-  }
-
-  map { delete $form->{$_} } qw(ndx lastndx nextsub);
-
-  IS->get_customer(\%myconfig, \%$form) if ($form->{vc} eq 'customer');
-  IR->get_vendor(\%myconfig, \%$form) if ($form->{vc} eq 'vendor');
-
-  &update(1);
-
-  $main::lxdebug->leave_sub();
-}
-
-# Reset the $::form field 'salesman_id' to the ID of the currently
-# logged in user. Useful when changing to a customer/vendor that has
-# no salesman listed in their master data.
-sub _reset_salesman_id {
-  my $current_employee   = SL::DB::Manager::Employee->current;
-  $::form->{salesman_id} = $current_employee->id if $current_employee && exists $::form->{salesman_id};
-}
-
-sub select_project {
-  $::lxdebug->enter_sub;
-
-  $::auth->assert('general_ledger         | vendor_invoice_edit  | sales_order_edit    | invoice_edit |' .
-                  'request_quotation_edit | sales_quotation_edit | purchase_order_edit | cash         | report');
-
-  my ($is_global, $nextsub) = @_;
-  my $project_list = delete $::form->{project_list};
-
-  map { delete $::form->{$_} } qw(action header update);
-
-  my @hiddens;
-  for my $key (keys %$::form) {
-    next if $key eq 'login' || $key eq 'password' || '' ne ref $::form->{$key};
-    push @hiddens, { key => $key, value => $::form->{$key} };
-  }
-  push @hiddens, { key => 'is_global',                value => $is_global },
-                 { key => 'project_selected_nextsub', value => $nextsub };
-
-  $::form->header;
-  print $::form->parse_html_template('arap/select_project', { hiddens => \@hiddens, project_list => $project_list });
-
-  $::lxdebug->leave_sub;
-}
-
-sub project_selected {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-
-  $main::auth->assert('general_ledger         | vendor_invoice_edit  | sales_order_edit    | invoice_edit |' .
-                'request_quotation_edit | sales_quotation_edit | purchase_order_edit | cash         | report');
-
-  # replace the variable with the one checked
-
-  # index for new item
-  my $i = $form->{ndx};
-
-  my $prefix = $form->{"is_global"} ? "global" : "";
-  my $suffix = $form->{"is_global"} ? "" : "_$form->{rownumber}";
-
-  $form->{"${prefix}projectnumber${suffix}"} =
-    $form->{"new_projectnumber_$i"};
-  $form->{"old${prefix}projectnumber${suffix}"} =
-    $form->{"new_projectnumber_$i"};
-  $form->{"${prefix}project_id${suffix}"} = $form->{"new_id_$i"};
-
-  # delete all the new_ variables
-  for $i (1 .. $form->{lastndx}) {
-    map { delete $form->{"new_${_}_$i"} } qw(id projectnumber description);
-  }
-
-  my $nextsub = $form->{project_selected_nextsub} || 'update';
-
-  map { delete $form->{$_} } qw(ndx lastndx nextsub is_global project_selected_nextsub);
-
-  call_sub($nextsub);
-
-  $main::lxdebug->leave_sub();
-}
-
-sub continue       { call_sub($main::form->{"nextsub"}); }
-
-1;
-
-__END__
-
-=head1 NAME
-
-arap.pl - helper functions or customer/vendor retrieval
-
-=head1 SYNOPSIS
-
- check_name('vendor')
-
-=head1 DESCRIPTION
-
-Don't use anyting in this file without extreme care, and even then be prepared for massive headaches.
-
-It's a collection of helper routines that wrap the customer/vendor dropdown/textfield duality into something even complexer.
-
-=head1 FUNCTIONS
-
-=head2 check_name customer|vendor
-
-check_name was originally meant to update the selected customer or vendor. The
-way it does that has generted more hate than almost any other part of this
-software.
-
-What it does is:
-
-=over 4
-
-=item *
-
-It checks if a vendor or customer is given. No failsafe, vendor fallback if
-$_[0] is something fancy.
-
-=item *
-
-It assumes, that there is a field named customer or vendor in $form.
-
-=item *
-
-It assumes, that this field is filled with name--id, and tries to split that.
-sql ledger uses that combination to get ids into the select keys.
-
-=item *
-
-It looks for a field selectcustomer or selectvendor in $form. sql ledger used
-to store a copy of the html select in there. (again, don't ask)
-
-=item *
-
-If this field exists, it looks for a field called oldcustomer or oldvendor, in
-which the old name--id string was stored in sql ledger, and compares those.
-
-=item *
-
-if they don't match, it will set customer_id or vendor_id in $form, load the
-entry (which will clobber everything in $form named like a column in customer
-oder vendor) and return.
-
-=item *
-
-If there was no select* entry, it assumes that vclimit was lower than the
-number of entries, and that an input field was generated. In that case the
-splitting is omitted (since users don't generally include ids in entered names)
-
-=item *
-
-It looks for a *_id field, and combines it with the given input into a name--id
-entry and compares it to the old* entry. (Missing any of these will instantly
-break check_namea.
-
-=item *
-
-If those do not match, $form->get_name is called to get matching results.
-get_name only matches by *number and name, not by id, don't try to get it to do
-so.
-
-=item *
-
-The results are stored in $form>{name_list} but a count is returned, and
-checked.
-
-=item *
-
-If only one result was found, *_id, * and old* are copied into $form, the entry
-is loaded (like above, clobbering)
-
-=item *
-
-If there is more than one, a selection dialog is rendered
-
-=item *
-
-If none is found, an error is generated.
-
-=back
-
-=head3 I built a customer/vendor box somewhere and it doesn't work, what's wrong?
-
-Make sure a select* field is given if and only if you render a select box. The
-actual contents are ignored, but recognition fails if not present.
-
-Make sure old* and *_id fields are set correctly (name--id form for old*). They
-are necessary in all steps and branches.
-
-Since get_customer and get_vendor clobber a lot of fields, make sure what
-changes exactly.
-
-=head3 select- version works fine, but things go awry when I use a textbox, any idea?
-
-If there is more than one match, check_name will display a select form, that
-will redirect to the original C<nextsub>. Unfortunately any hidden vars or
-input fields will be lost in the process unless saved before in a callback.
-
-If you still want to use it, you can disable this feature, like this:
-
-  check_name('customer', no_select => 1)
-
-In that case multiple matches will trigger an error.
-
-Otherwise you'll have to care to include a complete state in callback.
-
-=cut
index d8180a6..439709e 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Batch printing
@@ -32,6 +33,7 @@
 #======================================================================
 
 use SL::BP;
+use SL::Locale::String qw(t8);
 use Data::Dumper;
 use List::Util qw(first);
 
@@ -68,9 +70,6 @@ sub search {
 
   assert_bp_access();
 
-  # setup customer/vendor selection
-  BP->get_vc(\%::myconfig, $::form);
-
   my %label = (
        invoice           => { title => $::locale->text('Sales Invoices'),  invnumber => 1, ordnumber => 1 },
        sales_order       => { title => $::locale->text('Sales Orders'),    ordnumber => 1, },
@@ -85,6 +84,8 @@ sub search {
   my $bp_accounts = $::form->{type} =~ /check|receipt/
                  && BP->payment_accounts(\%::myconfig, $::form);
 
+  setup_bp_search_action_bar();
+
   $::form->header;
   print $::form->parse_html_template('bp/search', {
     label         => \%label,
@@ -135,7 +136,7 @@ sub print {
         print $::locale->text('done');
         $::form->redirect($::locale->text('Marked entries printed!'));
       }
-      ::end_of_request();
+      $::dispatcher->end_request;
     }
   }
 
@@ -185,9 +186,10 @@ sub list_spool {
 
   $::form->get_lists(printers => "ALL_PRINTERS");
 
+  setup_bp_list_spool_action_bar();
+
   $::form->header;
   print $::form->parse_html_template('bp/list_spool', {
-     spool        => $::lx_office_conf{paths}->{spool},
      href         => build_std_url('bp.pl', @href_options),
      is_invoice   => scalar ($::form->{type} =~ /^invoice$/),
      is_order     => scalar ($::form->{type} =~ /_order$/),
@@ -198,5 +200,36 @@ sub list_spool {
   $::lxdebug->leave_sub;
 }
 
-sub continue { call_sub($::form->{"nextsub"}); }
+sub setup_bp_search_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Show'),
+        submit    => [ '#form', { action => "list_spool" } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
 
+sub setup_bp_list_spool_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Remove'),
+        submit  => [ '#form', { action => "remove" } ],
+        checks  => [ [ 'kivi.check_if_entries_selected', '.check_all' ] ],
+        confirm => t8('Are you sure you want to remove the marked entries from the queue?'),
+      ],
+      action => [
+        t8('Print'),
+        submit => [ '#form', { action => "print" } ],
+        checks => [ [ 'kivi.check_if_entries_selected', '.check_all' ] ],
+      ],
+    );
+  }
+}
index 4bef1d2..774da85 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # module for Chart of Accounts, Income Statement and Balance Sheet
index 9756f92..8cbdac7 100644 (file)
 
 use SL::Common;
 use SL::DB::Helper::Mappings;
-use SL::DBUtils;
+use SL::DB;
+use SL::DBUtils qw(do_query);
 use SL::Form;
-use SL::MoreCommon;
-use SL::Helper::Flash;
+use SL::MoreCommon qw(restore_form save_form);
 
 use strict;
 
@@ -23,8 +23,8 @@ sub build_std_url {
 
   my $form     = $main::form;
 
-  my $script = $form->{script};
-
+  my $script   = $form->{script};
+  my $fragment;
   my @parts;
 
   foreach my $key (@_) {
@@ -33,6 +33,10 @@ sub build_std_url {
     if ($key =~ /(.*?)=(.*)/) {
       if ($1 eq 'script') {
         $script = $2;
+
+      } elsif ($1 eq 'fragment') {
+        $fragment = $2;
+
       } else {
         push @parts, $key;
       }
@@ -44,7 +48,7 @@ sub build_std_url {
     }
   }
 
-  my $url = "${script}?" . join('&', @parts);
+  my $url = "${script}?" . join('&', @parts) . (defined $fragment ? "#$fragment" : '');
 
   $main::lxdebug->leave_sub(2);
 
@@ -53,158 +57,6 @@ sub build_std_url {
 
 # -------------------------------------------------------------------------
 
-sub select_part {
-  $main::lxdebug->enter_sub();
-
-  my ($callback_sub, @parts) = @_;
-
-  my $form     = $main::form;
-  my $locale   = $main::locale;
-
-  my $remap_parts_id = 0;
-  if (defined($parts[0]->{parts_id}) && !defined($parts[0]->{id})) {
-    $remap_parts_id = 1;
-    map { $_->{id} = $_->{parts_id}; } @parts;
-  }
-
-  my $remap_partnumber = 0;
-  if (defined($parts[0]->{partnumber}) && !defined($parts[0]->{number})) {
-    $remap_partnumber = 1;
-    map { $_->{number} = $_->{partnumber}; } @parts;
-  }
-
-  my $has_charge = 0;
-  if (defined($parts[0]->{chargenumber})) {
-    $has_charge = 1;
-    map { $_->{has_charge} = 1; } @parts;
-  }
-  my $has_bestbefore = 0;
-  if (defined($parts[0]->{bestbefore})) {
-    $has_bestbefore = 1;
-    map { $_->{has_bestbefore} = 1; } @parts;
-  }
-  my $has_ean = 0;
-  if (defined($parts[0]->{ean})) {
-    $has_ean = 1;
-    map { $_->{has_ean} = 1; } @parts;
-  }
-
-  my $old_form = save_form();
-
-  $form->header();
-  print $form->parse_html_template("generic/select_part",
-                                   { "PARTS"            => \@parts,
-                                     "old_form"         => $old_form,
-                                     "title"            => $locale->text("Select a part"),
-                                     "nextsub"          => "select_part_internal",
-                                     "callback_sub"     => $callback_sub,
-                                     "has_charge"       => $has_charge,
-                                     "has_bestbefore"   => $has_bestbefore,
-                                     "has_ean"          => $has_ean,
-                                     "remap_parts_id"   => $remap_parts_id,
-                                     "remap_partnumber" => $remap_partnumber });
-
-  $main::lxdebug->leave_sub();
-}
-
-sub select_part_internal {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-
-  my ($new_item, $callback_sub);
-
-  my $re = "^new_.*_$form->{selection}\$";
-
-  foreach (grep /$re/, keys %{ $form }) {
-    my $new_key           =  $_;
-    $new_key              =~ s/^new_//;
-    $new_key              =~ s/_\d+$//;
-    $new_item->{$new_key} =  $form->{$_};
-  }
-
-  if ($form->{remap_parts_id}) {
-    $new_item->{parts_id} = $new_item->{id};
-    delete $new_item->{id};
-  }
-
-  if ($form->{remap_partnumber}) {
-    $new_item->{partnumber} = $new_item->{number};
-    delete $new_item->{number};
-  }
-
-  $callback_sub = $form->{callback_sub};
-
-  restore_form($form->{old_form});
-
-  call_sub($callback_sub, $new_item);
-
-  $main::lxdebug->leave_sub();
-}
-
-sub part_selection_internal {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  my $order_by  = "description";
-  $order_by  = $form->{"order_by"} if (defined($form->{"order_by"}));
-  my $order_dir = 1;
-  $order_dir = $form->{"order_dir"} if (defined($form->{"order_dir"}));
-
-  my %options;
-
-  foreach my $opt (split m/:/, $form->{options}) {
-    if ($opt =~ /=/) {
-      my ($key, $value) = split m/=/, $opt, 2;
-      $options{$key} = $value;
-
-    } else {
-      $options{$opt} = 1;
-    }
-  }
-
-  map { $form->{$_} = $options{$_} if ($options{$_}) } qw(no_services no_assemblies assemblies click_button);
-
-  my $parts = Common->retrieve_parts(\%myconfig, $form, $order_by, $order_dir);
-
-  if (0 == scalar(@{$parts})) {
-    $form->show_generic_information($locale->text("No part was found matching the search parameters."));
-  } elsif (1 == scalar(@{$parts})) {
-    $::request->{layout}->add_javascripts_inline("part_selected('1')");
-  }
-
-  map { $parts->[$_]->{selected} = $_ ? 0 : 1; } (0..$#{$parts});
-
-  my $callback = build_std_url('action=part_selection_internal', qw(partnumber description input_partnumber input_description input_partsid),
-                               grep({ /^[fl]_/ } keys %{ $form }));
-
-  my @header_sort  = qw(partnumber description);
-  my %header_title = ( "partnumber"  => $locale->text("Part Number"),
-                       "description" => $locale->text("Part Description"),
-                       );
-
-  my @header =
-    map(+{ "column_title" => $header_title{$_},
-           "column"       => $_,
-           "callback"     => $callback . "order_by=${_}&order_dir=" . ($order_by eq $_ ? 1 - $order_dir : $order_dir),
-         },
-        @header_sort);
-
-  $form->{formname} ||= 'Form';
-
-  $form->{title} = $locale->text("Select a part");
-  $form->header(no_layout => 1);
-  print $form->parse_html_template("generic/part_selection", { "HEADER" => \@header,
-                                                               "PARTS"  => $parts, });
-
-  $main::lxdebug->leave_sub();
-}
-
-# -------------------------------------------------------------------------
-
 sub delivery_customer_selection {
   $main::lxdebug->enter_sub();
 
@@ -333,10 +185,9 @@ sub calculate_qty {
   }, @header_sort;
 
   $form->{formel} = $formel;
-  $form->{title}  = $locale->text("Please enter values");
-  $form->header(no_layout => 1);
-  print $form->parse_html_template("generic/calculate_qty", { "HEADER"    => \@header,
-                                                              "VARIABLES" => \@variable, });
+  my $html = $form->parse_html_template("generic/calculate_qty", { "HEADER"    => \@header,
+                                                                   "VARIABLES" => \@variable, });
+  print $::form->ajax_response_header, $html;
 
   $main::lxdebug->leave_sub();
 }
@@ -386,11 +237,12 @@ sub show_history {
   $form->{title} = $locale->text("History");
   $form->header(no_layout => 1);
 
+  my $callback = build_std_url(qw(action longdescription trans_id_type input_name));
   my $restriction;
-  if ( $form->{trans_id_type} eq 'glid' ) {
-    $restriction = "AND ( snumbers LIKE 'invnumber%' OR what_done LIKE '%Buchungsnummer%' OR snumbers LIKE 'gltransaction%' ) ";
-  } elsif ( $form->{trans_id_type} eq 'id' ) {
-    $restriction = " AND ( snumbers NOT LIKE 'invnumber_%' AND snumbers NOT LIKE 'gltransaction%' AND (what_done NOT LIKE '%Buchungsnummer%' OR what_done IS null))";
+  if ( $form->{trans_id_type} eq 'glid' ) { # for invoices
+    $restriction = "AND ( snumbers LIKE 'invnumber%' OR what_done LIKE '%Buchungsnummer%' OR snumbers LIKE 'gltransaction%' OR (snumbers LIKE 'emailjournal%' AND what_done ~ 'invoice|credit_note') ) ";
+  } elsif ( $form->{trans_id_type} eq 'id' ) { # for non invoices
+    $restriction = " AND ( snumbers NOT LIKE 'invnumber_%' AND snumbers NOT LIKE 'gltransaction%' AND (what_done NOT LIKE '%Buchungsnummer%' AND what_done NOT LIKE '%invoice%' OR what_done IS null))";
   } else {
     $restriction = '';
   };
@@ -400,6 +252,7 @@ sub show_history {
     "SUCCESS"      => ($form->get_history($dbh,$form->{input_name}) ne "0"),
     uc($sort)      => 1,
     uc($sort)."BY" => $sortby,
+    callback       => $callback,
   } );
 
   $dbh->disconnect();
@@ -483,103 +336,7 @@ sub retrieve_partunits {
 
 # -------------------------------------------------------------------------
 
-sub mark_as_paid_common {
-  $main::lxdebug->enter_sub();
 
-  my ($myconfig, $db_name) = @_;
-
-  my $form     = $main::form;
-  my $locale   = $main::locale;
-
-  if($form->{mark_as_paid}) {
-    my $dbh ||= $form->get_standard_dbh($myconfig);
-    my $query = qq|UPDATE $db_name SET paid = amount, datepaid = current_date WHERE id = ?|;
-    do_query($form, $dbh, $query, $form->{id});
-    $dbh->commit();
-    $form->redirect($locale->text("Marked as paid"));
-
-  } else {
-    my $referer = $ENV{HTTP_REFERER};
-    my $script;
-    my $callback;
-    if ($referer =~ /action/) {
-      $referer =~ /^(.*)\?action\=[^\&]*(\&.*)$/;
-      $script = $1;
-      $callback = $2;
-    } elsif ($referer =~ /RESTORE_FORM_FROM_SESSION_ID/){
-      $referer =~ /^(.*)\?RESTORE_FORM_FROM_SESSION_ID\=(.*)$/;
-      $script = $1;
-      $callback = "";
-    } else {
-      $script = $referer;
-      $callback = "";
-    }
-    $referer = $script . "?action=mark_as_paid&mark_as_paid=1&id=$form->{id}" . $callback;
-    $form->header();
-    print qq|<p><b>|.$locale->text('Mark as paid?').qq|</b></p>|;
-    print qq|<input type="button" value="|.$locale->text('yes').qq|" onclick="document.location.href='|.$referer.qq|'">&nbsp;|;
-    print qq|<input type="button" value="|.$locale->text('no').qq|" onclick="javascript:history.back();">|;
-  }
-
-  $main::lxdebug->leave_sub();
-}
-
-sub cov_selection_internal {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  my $order_by = "name";
-  $order_by = $form->{"order_by"} if (defined($form->{"order_by"}));
-  my $order_dir = 1;
-  $order_dir = $form->{"order_dir"} if (defined($form->{"order_dir"}));
-
-  my $type = $form->{"is_vendor"} ? $locale->text("vendor") : $locale->text("customer");
-
-  my $covs = Common->retrieve_customers_or_vendors(\%myconfig, $form, $order_by, $order_dir, $form->{"is_vendor"}, $form->{"allow_both"});
-  map({ $covs->[$_]->{"selected"} = $_ ? 0 : 1; } (0..$#{$covs}));
-
-  if (0 == scalar(@{$covs})) {
-    $form->show_generic_information(sprintf($locale->text("No %s was found matching the search parameters."), $type));
-  } elsif (1 == scalar(@{$covs})) {
-    $::request->{layout}->add_javascripts_inline("cov_selected('1')");
-  }
-
-  my $callback = "$form->{script}?action=cov_selection_internal&";
-  map({ $callback .= "$_=" . $form->escape($form->{$_}) . "&" }
-      (qw(name input_name input_id is_vendor allow_both), grep({ /^[fl]_/ } keys %$form)));
-
-  my @header_sort = qw(name address contact);
-  my %header_title = ( "name" => $locale->text("Name"),
-                       "address" => $locale->text("Address"),
-                       "contact" => $locale->text("Contact"),
-                       );
-
-  my @header =
-    map(+{ "column_title" => $header_title{$_},
-           "column" => $_,
-           "callback" => $callback . "order_by=${_}&order_dir=" . ($order_by eq $_ ? 1 - $order_dir : $order_dir),
-         },
-        @header_sort);
-
-  foreach my $cov (@{ $covs }) {
-    $cov->{address} = "$cov->{street}, $cov->{zipcode} $cov->{city}";
-    $cov->{address} =~ s{^,}{}x;
-    $cov->{address} =~ s{\ +}{\ }gx;
-
-    $cov->{contact} = join " ", map { $cov->{$_} } qw(cp_gender cp_title cp_givenname cp_name);
-    $cov->{contact} =~ s{\ +}{\ }gx;
-  }
-
-  $form->{"title"} = $form->{is_vendor} ? $locale->text("Select a vendor") : $locale->text("Select a customer");
-  $form->header();
-  print($form->parse_html_template("generic/cov_selection", { "HEADER" => \@header,
-                                                              "COVS" => $covs, }));
-
-  $main::lxdebug->leave_sub();
-}
 
 
 # Functions to call add routines beneath different reports
@@ -628,4 +385,6 @@ sub db {
   goto &SL::DB::Helper::Mappings::db;
 }
 
+sub continue { call_sub($::form->{nextsub}); }
+
 1;
index 888bf8a..9b77e57 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Payment module
@@ -37,10 +38,10 @@ use SL::IR;
 use SL::AR;
 use SL::AP;
 use Data::Dumper;
+use SL::Locale::String qw(t8);
 use strict;
 #use warnings;
 
-require "bin/mozilla/arap.pl";
 require "bin/mozilla/common.pl";
 
 our ($form, %myconfig, $lxdebug, $locale, $auth);
@@ -59,27 +60,6 @@ sub payment {
   $form->{ARAP} = ($form->{type} eq 'receipt') ? "AR" : "AP";
   $form->{arap} = lc $form->{ARAP};
 
-  # setup customer/vendor selection for open invoices
-  if ($form->{all_vc}) {
-    # Dieser Zweig funktioniert derzeit NIE. Ggf. ganz raus oder
-    # alle offenen Zahlungen wieder korrekt anzeigen. jb 12.10.2010
-    $form->all_vc(\%myconfig, $form->{vc}, $form->{ARAP});
-  } else {
-    CP->get_openvc(\%myconfig, \%$form);
-  }
-  # Auswahlliste für vc zusammenbauen
-  # Erweiterung für schliessende option und erweiterung um value
-  # für bugfix 1771 (doppelte Leerzeichen werden nicht 'gepostet')
-  $form->{"select$form->{vc}"} = "";
-
-  if ($form->{"all_$form->{vc}"}) {
-    $form->{"select$form->{vc}"} .= "<option value=\"\"></option>\n";
-    # s.o. jb 12.10.2010
-    $form->{"$form->{vc}_id"} = $form->{"all_$form->{vc}"}->[0]->{id};
-    # hotfix for 2450. TODO remove legacy code and use L
-    map { $form->{"select$form->{vc}"} .= "<option value=\"" . H($_->{name}) . "--$_->{id}\">" . H($_->{name}) . "--$_->{id}</option>\n" }
-      @{ $form->{"all_$form->{vc}"} };
-  }
   CP->paymentaccounts(\%myconfig, \%$form);
 
   # Standard Konto für Umlaufvermögen
@@ -94,13 +74,6 @@ sub payment {
   map { $form->{selectaccount} .= "<option value=\"$_->{accno}--$_->{description}\">$_->{accno}--$_->{description}</option>\n";
         $form->{account}        = "$_->{accno}--$_->{description}" if ($_->{accno} eq $accno_arap) } @{ $form->{PR}{"$form->{ARAP}_paid"} };
 
-  # Braucht man das hier überhaupt? Erstmal auskommentieren .. jan 18.12.2010
-  #  map {
-  #    $form->{"select$form->{ARAP}"} .=
-  #      "<option>$_->{accno}--$_->{description}\n"
-  #  } @{ $form->{PR}{ $form->{ARAP} } };
-  # ENDE LOESCHMICH in 2012
-
   # currencies
   # oldcurrency ist zwar noch hier als fragment enthalten, wird aber bei
   # der aktualisierung der form auch nicht mitübernommen. das konzept
@@ -130,39 +103,32 @@ sub form_header {
 
   $auth->assert('cash');
 
-  my ($vc, $arap, $exchangerate);
+  $::request->layout->add_javascripts("kivi.CustomerVendor.js");
 
-  if ($form->{ $form->{vc} } eq "") {
+  my ($arap, $exchangerate);
+
+  if (!$form->{ $form->{vc} . '_id' }) {
     map { $form->{"addr$_"} = "" } (1 .. 4);
   }
 
-  # sometimes it happens that values in customer arrive without the signs '--'
-  # but in order to select the right option field we need values with '--'
-  if ($form->{vc} eq "customer" && $form->{"all_$form->{vc}"}){
-    my ($customername) = split /--/, $form->{ $form->{vc} };
-    $form->{ $form->{vc} } = $customername . "--" . $form->{customer_id};
-  }
   # bugfix 1771
   # geändert von <option>asdf--2929
   # nach:
   #              <option value="asdf--2929">asdf--2929</option>
   # offen: $form->{ARAP} kann raus?
-  for my $item ($form->{vc}, "account", "currency", $form->{ARAP}) {
+  for my $item ("account", "currency", $form->{ARAP}) {
     $form->{$item} = H($form->{$item});
     $form->{"select$item"} =~ s/ selected//;
     $form->{"select$item"} =~ s/option value="\Q$form->{$item}\E">\Q$form->{$item}\E/option selected value="$form->{$item}">$form->{$item}/;
   }
 
-  $vc =
-    ($form->{"select$form->{vc}"})
-    ? qq|<select name=$form->{vc}>$form->{"select$form->{vc}"}\n</select>|
-    : qq|<input name=$form->{vc} size=35 value="$form->{$form->{vc}}">|;
-
-  $form->{openinvoices} = $form->{all_vc} ? "" : 1;
+  $form->{openinvoices} = 1;
 
   # $locale->text('AR')
   # $locale->text('AP')
 
+  setup_cp_form_action_bar(can_post => !!$form->{rowcount});
+
   $form->header;
 
   $arap = lc $form->{ARAP};
@@ -171,7 +137,6 @@ sub form_header {
     is_customer => $form->{vc}   eq 'customer',
     is_receipt  => $form->{type} eq 'receipt',
     arap        => $arap,
-    vccontent   => $vc,
   });
 
   $lxdebug->leave_sub;
@@ -210,9 +175,7 @@ sub update {
 
   $auth->assert('cash');
 
-  my ($new_name_selected) = @_;
-
-  my ($buysell, $newvc, $updated, $exchangerate, $amount);
+  my ($buysell, $updated, $exchangerate, $amount);
 
   if ($form->{vc} eq 'customer') {
     $buysell = "buy";
@@ -220,59 +183,6 @@ sub update {
     $buysell = "sell";
   }
 
-  # if we switched to all_vc
-  # funktioniert derzeit nicht 12.10.2010 jb
-  if ($form->{all_vc} ne $form->{oldall_vc}) {
-
-    $form->{openinvoices} = ($form->{all_vc}) ? 0 : 1;
-
-    $form->{"select$form->{vc}"} = "";
-
-    if ($form->{all_vc}) {
-      $form->all_vc(\%myconfig, $form->{vc}, $form->{ARAP});
-
-      if ($form->{"all_$form->{vc}"}) {
-        map {
-          $form->{"select$form->{vc}"} .=
-            "<option>$_->{name}--$_->{id}\n"
-        } @{ $form->{"all_$form->{vc}"} };
-      }
-    } else {  # ab hier wieder ausgeführter code (s.o.):
-      CP->get_openvc(\%myconfig, \%$form);
-
-      if ($form->{"all_$form->{vc}"}) {
-        $newvc =
-          qq|$form->{"all_$form->{vc}"}[0]->{name}--$form->{"all_$form->{vc}"}[0]->{id}|;
-        map {
-          $form->{"select$form->{vc}"} .=
-            "<option>$_->{name}--$_->{id}\n"
-        } @{ $form->{"all_$form->{vc}"} };
-      }
-
-      # if the name is not the same
-      if ($form->{"select$form->{vc}"} !~ /$form->{$form->{vc}}/) {
-        $form->{ $form->{vc} } = $newvc;
-      }
-    }
-  }
-
-  # search by customernumber
-  # the customernumber has to be correct otherwise nothing is found
-  if ($form->{vc} eq 'customer' and $form->{customernumber} and $form->{ARAP} eq 'AR') {
-    $form->{open} ='Y'; # only open invoices
-    # ar_transactions automatically searches by $form->{customer_id} or else
-    # $form->{customer} if available, and these variables will always be set
-    # so we have to empty these values first
-    $form->{customer_id} = '';
-    $form->{customer} = '';
-    AR->ar_transactions(\%myconfig, \%$form);
-
-    # Here we just take the first returned value even if the custumernumber
-    # may not be unique
-    $form->{customer} = $form->{AR}[0]{name};
-    $form->{customer_id} = $form->{AR}[0]{customer_id};
-  }
-
   # search by invoicenumber,
   if ($form->{invnumber}) {
     $form->{open} ='Y'; # only open invoices
@@ -295,51 +205,32 @@ sub update {
       foreach my $i ( @{ $form->{AR} } ) {
         next unless $i->{invnumber} eq $form->{invnumber};
         # found exactly matching invnumber
-        $form->{$form->{vc}} = $i->{name};
         $form->{customer_id} = $i->{customer_id};
-        #$form->{"old${form->{vc}"} = $i->{customer_id};
         $found_exact_invnumber_match = 1;
       };
 
       unless ( $found_exact_invnumber_match ) {
         # use first returned entry, may not be the correct one if invnumber doesn't
         # match uniquely
-        $form->{$form->{vc}} = $form->{AR}[0]{name};
         $form->{customer_id} = $form->{AR}[0]{customer_id};
       };
     } else {
       # s.o. nur für zahlungsausgang
       AP->ap_transactions(\%myconfig, \%$form);
-      $form->{$form->{vc}} = $form->{AP}[0]{name};
+      $form->{vendor_id} = $form->{AP}[0]{vendor_id};
     }
   }
 
   # determine customer/vendor
-  if ( $form->{customer_id} and ($form->{invnumber} or $form->{customernumber}) ) {
-    # we already know the exact customer_id, so fill $form with customer data
+  my $vc = $form->{vc};
+  if (($form->{"previous_${vc}_id"} || $form->{"${vc}_id"}) != $form->{"${vc}_id"}) {
     IS->get_customer(\%myconfig, \%$form);
-    $updated = 1;
-  } else {
-    # check_name is called with "customer" or "vendor" and otherwise uses contents of $form
-    # check_name also runs get_customer/get_vendor
-    $updated = &check_name($form->{vc});
-  };
-
-  if ($new_name_selected || $updated) {
-    # get open invoices from ar/ap using $form->{vc} and a.${vc}_id, i.e. customer_id
-    CP->get_openinvoices(\%myconfig, \%$form);
-    ($newvc) = split /--/, $form->{ $form->{vc} };
-    $form->{"old$form->{vc}"} = qq|$newvc--$form->{"$form->{vc}_id"}|;
-    $updated = 1;
   }
 
-  if ($form->{currency} ne $form->{oldcurrency}) {
-    $form->{oldcurrency} = $form->{currency};
-    if (!$updated) {
-      CP->get_openinvoices(\%myconfig, \%$form);
-      $updated = 1;
-    }
-  }
+  $form->{oldcurrency} = $form->{currency};
+
+  # get open invoices from ar/ap using a.${vc}_id, i.e. customer_id
+  CP->get_openinvoices(\%myconfig, \%$form) if $form->{"${vc}_id"};
 
   if (!$form->{forex}) {        # read exchangerate from input field (not hidden)
     $form->{exchangerate} = $form->parse_amount(\%myconfig, $form->{exchangerate});
@@ -349,7 +240,7 @@ sub update {
 
   $amount = $form->{amount} = $form->parse_amount(\%myconfig, $form->{amount});
 
-  if ($updated) {
+  if ($form->{"${vc}_id"}) {
     $form->{rowcount} = 0;
 
     $form->{queued} = "";
@@ -364,8 +255,6 @@ sub update {
       $form->{"amount_$i"} = $ref->{amount} / $ref->{exchangerate};
       $form->{"due_$i"}    =
         ($ref->{amount} - $ref->{paid}) / $ref->{exchangerate};
-      $form->{"checked_$i"} = "";
-      $form->{"paid_$i"}    = "";
 
       # need to format
       map {
@@ -414,7 +303,7 @@ sub update {
   $form->{amount}=$amount;
 
   &form_header;
-  &list_invoices;
+  list_invoices() if $form->{"${vc}_id"};
   &form_footer;
 
   $lxdebug->leave_sub();
@@ -455,11 +344,15 @@ sub check_form {
 
   my ($closedto, $datepaid, $amount);
 
-  &check_name($form->{vc});
+  my $vc = $form->{vc};
+  if (($form->{"previous_${vc}_id"} || $form->{"${vc}_id"}) != $form->{"${vc}_id"}) {
+    IS->get_customer(\%myconfig, $form) if $vc eq 'customer';
+    IR->get_vendor(\%myconfig, $form)   if $vc eq 'vendor';
+  }
 
   if ($form->{currency} ne $form->{oldcurrency}) {
     &update;
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
   $form->error($locale->text('Date missing!')) unless $form->{datepaid};
   my $selected_check = 1;
@@ -504,3 +397,21 @@ sub check_form {
 
   $lxdebug->leave_sub();
 }
+
+sub setup_cp_form_action_bar {
+  my (%params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#form', { action => "update" } ],
+        accesskey => 'enter',
+      ],
+      action => [
+        t8('Post'),
+        submit => [ '#form', { action => "post" } ],
+      ],
+    );
+  }
+}
index ec1ccbf..0a62ab5 100644 (file)
@@ -25,7 +25,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # customer/vendor module
@@ -54,9 +55,10 @@ use SL::Request qw(flatten);
 use SL::DB::Business;
 use SL::DB::Default;
 use SL::DB::DeliveryTerm;
-use SL::Helper::Flash;
 use SL::ReportGenerator;
+use SL::Locale::String qw(t8);
 use SL::MoreCommon qw(uri_encode);
+use SL::ZUGFeRD;
 
 require "bin/mozilla/common.pl";
 require "bin/mozilla/reportgenerator.pl";
@@ -66,11 +68,14 @@ use strict;
 
 # end of main
 
+sub _zugferd_settings {
+  return ([ -1, $::locale->text('Use settings from client configuration') ],
+          @SL::ZUGFeRD::customer_settings);
+}
+
 sub search {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('customer_vendor_edit');
-
   my $form     = $main::form;
   my $locale   = $main::locale;
 
@@ -88,6 +93,10 @@ sub search {
 
   $form->{title}    = $form->{IS_CUSTOMER} ? $locale->text('Customers') : $locale->text('Vendors');
 
+  $form->{ZUGFERD_SETTINGS} = [ _zugferd_settings() ];
+
+  setup_ct_search_action_bar();
+
   $form->header();
   print $form->parse_html_template('ct/search');
 
@@ -96,7 +105,6 @@ sub search {
 
 sub search_contact {
   $::lxdebug->enter_sub;
-  $::auth->assert('customer_vendor_edit');
 
   $::form->{CUSTOM_VARIABLES}                  = CVar->get_configs('module' => 'Contacts');
   ($::form->{CUSTOM_VARIABLES_FILTER_CODE},
@@ -106,6 +114,8 @@ sub search_contact {
                                                                            'include_value'  => 'Y');
 
   $::form->{title} = $::locale->text('Search contacts');
+
+  setup_ct_search_contact_action_bar();
   $::form->header;
   print $::form->parse_html_template('ct/search_contact');
 
@@ -115,8 +125,6 @@ sub search_contact {
 sub list_names {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('customer_vendor_edit');
-
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
@@ -136,15 +144,22 @@ sub list_names {
     push @options, $locale->text('Orphaned');
   }
 
-  push @options, $locale->text('Name') . " : $form->{name}"                                    if $form->{name};
-  push @options, $locale->text('Contact') . " : $form->{contact}"                              if $form->{contact};
-  push @options, $locale->text('Number') . qq| : $form->{"$form->{db}number"}|                 if $form->{"$form->{db}number"};
-  push @options, $locale->text('E-mail') . " : $form->{email}"                                 if $form->{email};
-  push @options, $locale->text('Contact person (surname)')           . " : $form->{cp_name}"   if $form->{cp_name};
-  push @options, $locale->text('Billing/shipping address (city)')    . " : $form->{addr_city}" if $form->{addr_city};
-  push @options, $locale->text('Billing/shipping address (zipcode)') . " : $form->{zipcode}"   if $form->{addr_zipcode};
-  push @options, $locale->text('Billing/shipping address (street)')  . " : $form->{street}"    if $form->{addr_street};
-  push @options, $locale->text('Billing/shipping address (country)') . " : $form->{country}"   if $form->{addr_country};
+  my @zugferd_settings_list = _zugferd_settings();
+  my $zugferd_filter        = $form->{create_zugferd_invoices} eq '' ? undef : $zugferd_settings_list[$form->{create_zugferd_invoices} + 1]->[1];
+
+  push @options, $locale->text('Name')                               . " : $form->{name}"                  if $form->{name};
+  push @options, $locale->text('Contact')                            . " : $form->{contact}"               if $form->{contact};
+  push @options, $locale->text('Number')                           . qq| : $form->{"$form->{db}number"}|   if $form->{"$form->{db}number"};
+  push @options, $locale->text('E-mail')                             . " : $form->{email}"                 if $form->{email};
+  push @options, $locale->text('All phone numbers')                  . " : $form->{all_phonenumbers}"      if $form->{all_phonenumbers};
+  push @options, $locale->text('Contact person (surname)')           . " : $form->{cp_name}"               if $form->{cp_name};
+  push @options, $locale->text('Billing/shipping address (city)')    . " : $form->{addr_city}"             if $form->{addr_city};
+  push @options, $locale->text('Billing/shipping address (zipcode)') . " : $form->{addr_zipcode}"          if $form->{addr_zipcode};
+  push @options, $locale->text('Billing/shipping address (street)')  . " : $form->{addr_street}"           if $form->{addr_street};
+  push @options, $locale->text('Billing/shipping address (country)') . " : $form->{addr_country}"          if $form->{addr_country};
+  push @options, $locale->text('Billing/shipping address (GLN)')     . " : $form->{addr_gln}"              if $form->{addr_gln};
+  push @options, $locale->text('Quick Search')                       . " : $form->{all}"                   if $form->{all};
+  push @options, $locale->text('Factur-X/ZUGFeRD settings')          . " : $zugferd_filter"                if $zugferd_filter;
 
   if ($form->{business_id}) {
     my $business = SL::DB::Manager::Business->find_by(id => $form->{business_id});
@@ -167,10 +182,12 @@ sub list_names {
   };
 
   my @columns = (
-    'id',        'name',    "$form->{db}number",   'contact',   'phone',    'discount',
+    'id',        'name',    "$form->{db}number",   'contact', 'main_contact_person',
+    'department_1',         'department_2',        'phone',   'discount',
     'fax',       'email',   'taxnumber',           'street',    'zipcode' , 'city',
     'business',  'payment', 'invnumber', 'ordnumber',           'quonumber', 'salesman',
-    'country',   'insertdate',           'pricegroup'
+    'country',   'gln',     'insertdate',           'pricegroup', 'contact_origin', 'invoice_mail',
+    'creditlimit', 'ustid', 'commercial_court', 'delivery_order_mail'
   );
 
   my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs };
@@ -184,6 +201,9 @@ sub list_names {
     "$form->{db}number" => { 'text' => $locale->text('Number'), },
     'name'              => { 'text' => $form->{IS_CUSTOMER} ? $::locale->text('Customer Name') : $::locale->text('Vendor Name'), },
     'contact'           => { 'text' => $locale->text('Contact'), },
+    'main_contact_person'  => { 'text' => $locale->text('Main Contact Person'), },
+    'department_1'      => { 'text' => $locale->text('Department') . " 1", },
+    'department_2'      => { 'text' => $locale->text('Department') . " 2", },
     'phone'             => { 'text' => $locale->text('Phone'), },
     'fax'               => { 'text' => $locale->text('Fax'), },
     'email'             => { 'text' => $locale->text('E-mail'), },
@@ -197,11 +217,19 @@ sub list_names {
     'zipcode'           => { 'text' => $locale->text('Zipcode'), },
     'city'              => { 'text' => $locale->text('City'), },
     'country'           => { 'text' => $locale->text('Country'), },
+    'gln'               => { 'text' => $locale->text('GLN'), },
     'salesman'          => { 'text' => $locale->text('Salesman'), },
     'discount'          => { 'text' => $locale->text('Discount'), },
     'payment'           => { 'text' => $locale->text('Payment Terms'), },
     'insertdate'        => { 'text' => $locale->text('Insert Date'), },
     'pricegroup'        => { 'text' => $locale->text('Pricegroup'), },
+    'invoice_mail'      => { 'text' => $locale->text('Email of the invoice recipient'), },
+    'delivery_order_mail' => { 'text' => $locale->text('Email of the delivery order recipient'), },
+    'contact_origin'    => { 'text' => $locale->text('Origin of personal data'), },
+    'creditlimit'       => { 'text' => $locale->text('Credit Limit'), },
+    'ustid'             => { 'text' => $locale->text('VAT ID'), },
+    'commercial_court'  => { 'text' => $locale->text('Commercial court'), },
+    create_zugferd_invoices => { text => $locale->text('Factur-X/ZUGFeRD settings'), },
     %column_defs_cvars,
   );
 
@@ -209,9 +237,12 @@ sub list_names {
 
   my @hidden_variables  = ( qw(
       db status obsolete name contact email cp_name addr_street addr_zipcode
-      addr_city addr_country business_id salesman_id insertdateto insertdatefrom
+      addr_city addr_country addr_gln business_id salesman_id insertdateto insertdatefrom all
+      all_phonenumbers
     ), "$form->{db}number",
     map({ "cvar_$_->{name}" } @searchable_custom_variables),
+    map({'cvar_'. $_->{name} .'_from'} grep({$_->{type} eq 'date'} @searchable_custom_variables)),
+    map({'cvar_'. $_->{name} .'_to'}   grep({$_->{type} eq 'date'} @searchable_custom_variables)),
     map({'cvar_'. $_->{name} .'_qtyop'} grep({$_->{type} eq 'number'} @searchable_custom_variables)),
     map({ "l_$_" } @columns),
   );
@@ -271,6 +302,7 @@ sub list_names {
     if ($ref->{id} ne $previous_id) {
       $previous_id = $ref->{id};
       $ref->{discount} = $form->format_amount(\%myconfig, $ref->{discount} * 100.0, 2);
+      $ref->{creditlimit} = $form->format_amount(\%myconfig, $ref->{creditlimit}, 2);
       map { $row->{$_}->{data} = $ref->{$_} } @columns;
 
       $row->{name}->{link}  = build_std_url('script=controller.pl', 'action=CustomerVendor/edit', 'id=' . E($ref->{id}), 'callback', @hidden_nondefault);
@@ -278,6 +310,12 @@ sub list_names {
     }
 
     my $base_url              = build_std_url("script=$ref->{module}.pl", 'action=edit', 'id=' . E($ref->{invid}), 'callback', @hidden_nondefault);
+    if ($::instance_conf->get_feature_experimental_order) {
+      if ('oe' eq $ref->{module}) {
+        $base_url             = build_std_url("script=controller.pl", 'action=Order/edit', 'id=' . E($ref->{invid}), 'callback', @hidden_nondefault);
+      }
+    }
+
     $row->{invnumber}->{link} = $base_url;
     $row->{ordnumber}->{link} = $base_url . "&type=${ordertype}";
     $row->{quonumber}->{link} = $base_url . "&type=${quotationtype}";
@@ -292,6 +330,7 @@ sub list_names {
     $report->add_data($row);
   }
 
+  setup_ct_list_names_action_bar();
   $report->generate_with_headers();
 
   $main::lxdebug->leave_sub();
@@ -299,7 +338,6 @@ sub list_names {
 
 sub list_contacts {
   $::lxdebug->enter_sub;
-  $::auth->assert('customer_vendor_edit');
 
   $::form->{sortdir} = 1 unless defined $::form->{sortdir};
 
@@ -423,9 +461,51 @@ sub list_contacts {
     $report->add_data($row);
   }
 
-  $report->generate_with_headers;
+  $report->generate_with_headers();
 
   $::lxdebug->leave_sub;
 }
 
+sub setup_ct_search_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Search'),
+        submit    => [ '#form', { action => 'list_names' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_ct_list_names_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Add'),
+        submit    => [ '#new_form', { action => 'CustomerVendor/add' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_ct_search_contact_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Search'),
+        submit    => [ '#form', { action => 'list_contacts' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
 sub continue { call_sub($main::form->{nextsub}); }
index 9572e74..7909702 100644 (file)
@@ -18,7 +18,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Datev export module
@@ -30,6 +31,8 @@ use Archive::Zip qw(:ERROR_CODES :CONSTANTS);
 
 use SL::Common;
 use SL::DATEV qw(:CONSTANTS);
+use SL::Locale::String qw(t8);
+use SL::DB::Department;
 
 use strict;
 
@@ -47,6 +50,8 @@ sub export {
 
   my $stamm = SL::DATEV->new->get_datev_stamm;
 
+  setup_datev_export_action_bar();
+
   $::form->header;
   print $::form->parse_html_template('datev/export', $stamm);
 
@@ -57,11 +62,8 @@ sub export2 {
   $::lxdebug->enter_sub;
   $::auth->assert('datev_export');
 
-  if ($::form->{exporttype} == 0) {
-    export_bewegungsdaten();
-  } else {
-    export_stammdaten();
-  }
+  export_bewegungsdaten();
+
   $::lxdebug->leave_sub;
 }
 
@@ -69,18 +71,16 @@ sub export_bewegungsdaten {
   $::lxdebug->enter_sub;
   $::auth->assert('datev_export');
 
-  $::form->header;
-  print $::form->parse_html_template('datev/export_bewegungsdaten');
+  setup_datev_export2_action_bar();
 
-  $::lxdebug->leave_sub;
-}
+  $::form->header;
+  $::form->{ALL_DEPARTMENTS} = SL::DB::Manager::Department->get_all_sorted;
+  $::form->{show_pk_option}  = SL::DATEV->new->check_vcnumbers_are_valid_pk_numbers;
 
-sub export_stammdaten {
-  $::lxdebug->enter_sub;
-  $::auth->assert('datev_export');
+  # check if we have mismatching number length domains
+  SL::DATEV->new->check_valid_length_of_accounts;
 
-  $::form->header;
-  print $::form->parse_html_template('datev/export_stammdaten');
+  print $::form->parse_html_template('datev/export_bewegungsdaten');
 
   $::lxdebug->leave_sub;
 }
@@ -91,20 +91,16 @@ sub export3 {
 
   my %data = (
     exporttype => $::form->{exporttype} ? DATEV_ET_STAMM : DATEV_ET_BUCHUNGEN,
-    format     => $::form->{kne}        ? DATEV_FORMAT_KNE : DATEV_FORMAT_OBE,
+    format     => $::form->{exportformat} eq 'kne' ? DATEV_FORMAT_KNE :  DATEV_FORMAT_CSV,
   );
 
-  if ($::form->{exporttype} == DATEV_ET_STAMM) {
-    $data{accnofrom}  = $::form->{accnofrom},
-    $data{accnoto}    = $::form->{accnoto},
-  } elsif ($::form->{exporttype} == DATEV_ET_BUCHUNGEN) {
-    @data{qw(from to)} = _get_dates(
-      $::form->{zeitraum}, $::form->{monat}, $::form->{quartal},
-      $::form->{transdatefrom}, $::form->{transdateto},
-    );
-  } else {
-    die 'invalid exporttype';
-  }
+  @data{qw(from to)} = _get_dates(
+    $::form->{zeitraum}, $::form->{monat}, $::form->{quartal},
+    $::form->{transdatefrom}, $::form->{transdateto},
+  );
+  $data{use_pk} = $::form->{use_pk};
+  $data{locked} = $::form->{locked};
+  $data{imported} = $::form->{imported};
 
   my $datev = SL::DATEV->new(%data);
 
@@ -114,8 +110,10 @@ sub export3 {
   $datev->export;
 
   if (!$datev->errors) {
+    setup_datev_export3_action_bar(download_token => $datev->download_token);
+
     $::form->header;
-    print $::form->parse_html_template('datev/export3', { datev => $datev });
+    print $::form->parse_html_template('datev/export3', { WARNINGS => $datev->warnings });
   } else {
     $::form->error("Export schlug fehl.\n" . join "\n", $datev->errors);
   }
@@ -181,6 +179,9 @@ sub _get_dates {
 
   if ($mode eq "monat") {
     $fromdate = DateTime->new(day => 1, month => $month, year => DateTime->today->year);
+    # december export is usually in january/february
+    $fromdate = $fromdate->subtract(years => 1) if ($month == 12);
+
     $todate   = $fromdate->clone->add(months => 1)->add(days => -1);
   } elsif ($mode eq "quartal") {
     die 'quarter out of of bounds' if $quarter < 1 || $quarter > 4;
@@ -198,3 +199,52 @@ sub _get_dates {
 
   return ($fromdate, $todate);
 }
+
+sub setup_datev_export_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Continue'),
+        submit    => [ '#form', { action => 'export2' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_datev_export2_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Export'),
+        submit    => [ '#form', { action => 'export3' } ],
+        accesskey => 'enter',
+      ],
+      action => [
+        t8('Back'),
+        call => [ 'kivi.history_back' ],
+      ],
+    );
+  }
+}
+
+sub setup_datev_export3_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      link => [
+        t8('Download'),
+        link => [ 'datev.pl?action=download&download_token=' . $::form->escape($params{download_token}) ],
+      ],
+      action => [
+        t8('Back'),
+        call => [ 'kivi.history_back' ],
+      ],
+    );
+  }
+}
index e418fa2..419b4f4 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Dunning process module
 
 use POSIX qw(strftime);
 
+use List::Util qw(notall);
+use List::MoreUtils qw(none);
+
 use SL::IS;
-use SL::PE;
 use SL::DN;
+use SL::DB::Department;
 use SL::DB::Dunning;
-use SL::Helper::Flash;
+use SL::File;
+use SL::Helper::Flash qw(flash);
 use SL::Locale::String qw(t8);
+use SL::Presenter::EmailJournal;
+use SL::Presenter::FileObject;
+use SL::Presenter::WebdavObject;
 use SL::ReportGenerator;
 
 require "bin/mozilla/common.pl";
@@ -83,6 +91,8 @@ sub edit_config {
   $form->{title}      = $locale->text('Edit Dunning Process Config');
   $form->{callback} ||= build_std_url("action=edit_config");
 
+  setup_dn_edit_config_action_bar();
+
   $form->header();
   print $form->parse_html_template("dunning/edit_config");
 
@@ -98,16 +108,16 @@ sub add {
 
   $main::auth->assert('dunning_edit');
 
-  # setup customer selection
-  $form->all_vc(\%myconfig, "customer", "AR");
-
   DN->get_config(\%myconfig, \%$form);
 
-  $form->{SHOW_CUSTOMER_SELECTION}      = $form->{all_customer}    && scalar @{ $form->{all_customer} };
+  $form->get_lists("departments" => "ALL_DEPARTMENTS");
+
   $form->{SHOW_DUNNING_LEVEL_SELECTION} = $form->{DUNNING}         && scalar @{ $form->{DUNNING} };
-  $form->{SHOW_DEPARTMENT_SELECTION}    = $form->{all_departments} && scalar @{ $form->{all_departments} || [] };
+  $form->{SHOW_DEPARTMENT_SELECTION}    = $form->{ALL_DEPARTMENTS} && scalar @{ $form->{ALL_DEPARTMENTS} || [] };
 
   $form->{title}    = $locale->text('Start Dunning Process');
+
+  setup_dn_add_action_bar();
   $form->header();
 
   print $form->parse_html_template("dunning/add");
@@ -133,7 +143,7 @@ sub show_invoices {
     if ($row->{next_dunning_config_id}) {
       map { $_->{SELECTED} = $_->{id} == $row->{next_dunning_config_id} } @{ $row->{DUNNING_CONFIG } };
     }
-    map { $row->{$_} = $form->format_amount(\%myconfig, $row->{$_} * 1, -2) } qw(amount open_amount fee interest);
+    map { $row->{$_} = $form->format_amount(\%myconfig, $row->{$_} * 1, 2) } qw(amount open_amount fee interest);
 
     if ($row->{'language_id'}) {
       $row->{language} = SL::DB::Manager::Language->find_by_or_create('id' => $row->{'language_id'})->{'description'};
@@ -153,6 +163,7 @@ sub show_invoices {
                                           'no_html'         => 1,
                                           'no_opendocument' => 1,);
 
+  setup_dn_show_invoices_action_bar();
   $form->header();
   print $form->parse_html_template("dunning/show_invoices");
 
@@ -201,11 +212,12 @@ sub save_dunning {
 
   my $active=1;
   my @rows = ();
+  my @status;
   undef($form->{DUNNING_PDFS});
 
   my $saved_language_id = $form->{language_id};
 
-  if ($form->{groupinvoices}) {
+  if ($form->{groupinvoices} || $form->{l_include_credit_notes}) {
     my %dunnings_for;
 
     for my $i (1 .. $form->{rowcount}) {
@@ -219,9 +231,11 @@ sub save_dunning {
 
       push @{ $level }, { "row"                    => $i,
                           "invoice_id"             => $form->{"inv_id_$i"},
+                          "credit_note"            => $form->{"credit_note_$i"},
                           "customer_id"            => $form->{"customer_id_$i"},
                           "language_id"            => $form->{"language_id_$i"},
                           "next_dunning_config_id" => $form->{"next_dunning_config_id_$i"},
+                          "print_invoice"          => $form->{"include_invoice_$i"},
                           "email"                  => $form->{"email_$i"}, };
     }
 
@@ -231,7 +245,10 @@ sub save_dunning {
         if (!$form->{force_lang}) {
           $form->{language_id} = @{$level}[0]->{language_id};
         }
-        DN->save_dunning(\%myconfig, $form, $level);
+        my $rc =  DN->save_dunning(\%myconfig, $form, $level);
+        $rc->{error} =~ s{\n}{<br />}g if $rc->{error};
+        push @status, { invnumbers => [map { $form->{'invnumber_' . $_->{row}} } @$level],
+                        map { ( $_ => $rc->{$_} ) } qw(error dunning_id print_original_invoice send_email), };
       }
     }
 
@@ -244,19 +261,32 @@ sub save_dunning {
                       "customer_id"            => $form->{"customer_id_$i"},
                       "language_id"            => $form->{"language_id_$i"},
                       "next_dunning_config_id" => $form->{"next_dunning_config_id_$i"},
+                      "print_invoice"          => $form->{"include_invoice_$i"},
                       "email"                  => $form->{"email_$i"}, } ];
       if (!$form->{force_lang}) {
         $form->{language_id} = @{$level}[0]->{language_id};
       }
-      DN->save_dunning(\%myconfig, $form, $level);
+      my $rc = DN->save_dunning(\%myconfig, $form, $level);
+      $rc->{error} =~ s{\n}{<br />}g if $rc->{error};
+      push @status, { invnumbers => [map { $form->{'invnumber_' . $_->{row}} } @$level],
+                      map { ( $_ => $rc->{$_} ) } qw(error dunning_id print_original_invoice send_email), };
     }
   }
 
   $form->{language_id} = $saved_language_id;
 
-  if (scalar @{ $form->{DUNNING_PDFS} }) {
+  my $pdf_filename;
+  my $pdf_content;
+  if ($form->{DUNNING_PDFS} && scalar @{ $form->{DUNNING_PDFS} }) {
     $form->{dunning_id} = strftime("%Y%m%d", localtime time) if scalar @{ $form->{DUNNING_PDFS}} > 1;
-    DN->melt_pdfs(\%myconfig, $form, $form->{copies});
+    ($pdf_filename, $pdf_content) = DN->melt_pdfs(\%myconfig, $form, $form->{copies}, return_content => $form->{media} ne 'printer');
+
+    flash('info', t8('Dunning Process started for selected invoices!'));
+    if ($form->{media} eq 'printer') {
+      flash('info', t8('The PDF has been printed'));
+    } else {
+      flash('info', t8('The PDF has been created'));
+    }
   }
 
   # saving the history
@@ -267,10 +297,13 @@ sub save_dunning {
   }
   # /saving the history
 
-  if ($form->{media} eq 'printer') {
-    delete $form->{callback};
-    $form->redirect($locale->text('Dunning Process started for selected invoices!'));
-  }
+  setup_dn_status_action_bar();
+  $form->{"title"} = $locale->text("Dunning status");
+  $form->header();
+  print $form->parse_html_template('dunning/status', {
+    pdf_filename => $pdf_filename,
+    pdf_content  => $pdf_content,
+    status       => \@status, });
 
   $main::lxdebug->leave_sub();
 }
@@ -284,7 +317,10 @@ sub set_email {
   $main::auth->assert('dunning_edit');
 
   $form->{"title"} = $locale->text("Set eMail text");
-  $form->header(no_layout => 1);
+  $form->header(
+    no_layout       => 1,
+    use_javascripts => [ qw(ckeditor/ckeditor ckeditor/adapters/jquery) ],
+  );
   print($form->parse_html_template("dunning/set_email"));
 
   $main::lxdebug->leave_sub();
@@ -305,12 +341,11 @@ sub search {
 
   DN->get_config(\%myconfig, \%$form);
 
-  $form->{SHOW_CUSTOMER_DDBOX}   = scalar @{ $form->{ALL_CUSTOMERS} } <= $myconfig{vclimit};
-  $form->{SHOW_DEPARTMENT_DDBOX} = scalar @{ $form->{ALL_CUSTOMERS} };
   $form->{SHOW_DUNNING_LEVELS}   = scalar @{ $form->{DUNNING} };
 
   $form->{title}    = $locale->text('Dunnings');
 
+  setup_dn_search_action_bar();
   $form->header();
 
   print $form->parse_html_template("dunning/search");
@@ -329,8 +364,9 @@ sub show_dunning {
 
   $main::auth->assert('dunning_edit');
 
-  my @filter_field_list = qw(customer_id customer dunning_level department_id invnumber ordnumber
-                             transdatefrom transdateto dunningfrom dunningto notes showold l_salesman salesman_id);
+  my @filter_field_list = qw(customer_id customer dunning_id dunning_level department_id invnumber ordnumber
+                             transdatefrom transdateto dunningfrom dunningto notes showold l_salesman salesman_id
+                             l_mails l_webdav l_documents);
 
   report_generator_set_default_sort('customername', 1);
 
@@ -361,33 +397,38 @@ sub show_dunning {
     'checkbox'            => { 'text' => '', 'visible' => 'HTML' },
     'dunning_description' => { 'text' => $locale->text('Dunning Level') },
     'customername'        => { 'text' => $locale->text('Customername') },
+    'departmentname'      => { 'text' => $locale->text('Department') },
     'language'            => { 'text' => $locale->text('Language') },
     'invnumber'           => { 'text' => $locale->text('Invnumber') },
     'transdate'           => { 'text' => $locale->text('Invdate') },
     'duedate'             => { 'text' => $locale->text('Invoice Duedate') },
     'amount'              => { 'text' => $locale->text('Amount') },
+    'dunning_id'          => { 'text' => $locale->text('Dunning number') },
     'dunning_date'        => { 'text' => $locale->text('Dunning Date') },
     'dunning_duedate'     => { 'text' => $locale->text('Dunning Duedate') },
     'fee'                 => { 'text' => $locale->text('Total Fees') },
     'interest'            => { 'text' => $locale->text('Interest') },
     'salesman'            => { 'text' => $locale->text('Salesperson'), 'visible' => $form->{l_salesman} ? 1 : 0 },
+    'documents'           => { 'text' => $locale->text('Documents'),   'visible' => $form->{l_documents}? 1 : 0 },
+    'webdav'              => { 'text' => $locale->text('WebDAV'),      'visible' => $form->{l_webdav}   ? 1 : 0 },
+    'mails'               => { 'text' => $locale->text('Mails'),       'visible' => $form->{l_mails}    ? 1 : 0 },
   );
 
   $report->set_columns(%column_defs);
-  $report->set_column_order(qw(checkbox dunning_description customername language invnumber transdate
-                               duedate amount dunning_date dunning_duedate fee interest salesman));
+  $report->set_column_order(qw(checkbox dunning_description dunning_id customername language invnumber transdate
+                               duedate amount dunning_date dunning_duedate fee interest salesman departmentname mails webdav documents));
   $report->set_sort_indicator($form->{sort}, $form->{sortdir});
 
   my $edit_url  = sub { build_std_url('script=' . ($_[0]->{invoice} ? 'is' : 'ar') . '.pl', 'action=edit', 'callback') . '&id=' . $::form->escape($_[0]->{id}) };
   my $print_url = sub { build_std_url('action=print_dunning', 'format=pdf', 'media=screen', 'dunning_id='.$_[0]->{dunning_id}, 'language_id=' . $_[0]->{language_id}) };
   my $sort_url  = build_std_url('action=show_dunning', grep { $form->{$_} } @filter_field_list);
 
-  foreach my $name (qw(dunning_description customername invnumber transdate duedate dunning_date dunning_duedate salesman)) {
+  foreach my $name (qw(dunning_description customername invnumber transdate duedate dunning_date dunning_duedate salesman dunning_id)) {
     my $sortdir                 = $form->{sort} eq $name ? 1 - $form->{sortdir} : $form->{sortdir};
     $column_defs{$name}->{link} = $sort_url . "&sort=$name&sortdir=$sortdir";
   }
 
-  my %alignment = map { $_ => 'right' } qw(transdate duedate amount dunning_date dunning_duedate fee interest salesman);
+  my %alignment = map { $_ => 'right' } qw(transdate duedate amount dunning_date dunning_duedate fee interest salesman dunning_id);
 
   my ($current_dunning_rows, $previous_dunning_id, $first_row_for_dunning);
 
@@ -413,12 +454,13 @@ sub show_dunning {
     my $row = { };
     foreach my $column (keys %{ $ref }) {
       $row->{$column} = {
-        'data'  => $first_row_for_dunning || (($column ne 'dunning_description') && ($column ne 'customername')) ? $ref->{$column} : '',
+        'data'  => $first_row_for_dunning || (none { $_ eq $column } qw(dunning_description customername dunning_id)) ? $ref->{$column} : '',
 
         'align' => $alignment{$column},
 
         'link'  => (  $column eq 'invnumber'           ? $edit_url->($ref)
                     : $column eq 'dunning_description' ? $print_url->($ref)
+                    : $column eq 'dunning_id'          ? $print_url->($ref)
                     :                                    ''),
       };
     }
@@ -437,6 +479,45 @@ sub show_dunning {
       $row->{language} = { };
     }
 
+    if ($form->{l_documents} && $first_row_for_dunning) {
+      my @files  = SL::File->get_all_versions(object_id   => $ref->{dunning_id},
+                                              object_type => 'dunning',
+                                              file_type   => 'document',);
+      if (scalar @files) {
+        my $html          = join '<br>', map { SL::Presenter::FileObject::file_object($_) } @files;
+        my $text          = join "\n",   map { $_->file_name                              } @files;
+        $row->{documents} = { 'raw_data' => $html, data => $text };
+      } else {
+        $row->{documents} = { };
+      }
+    }
+    if ($form->{l_webdav} && $first_row_for_dunning) {
+      my $webdav = SL::Webdav->new(
+        type     => 'dunning',
+        number   => $ref->{dunning_id},
+      );
+      my @all_objects = $webdav->get_all_objects;
+      if (scalar @all_objects) {
+        my $html          = join '<br>', map { SL::Presenter::WebdavObject::webdav_object($_) } @all_objects;
+        my $text          = join "\n",   map { $_->filename                                   } @all_objects;
+        $row->{webdav}    = { 'raw_data' => $html, data => $text };
+      } else {
+        $row->{webdav}    = { };
+      }
+    }
+
+    if ($form->{l_mails}) {
+      my @mail_links = RecordLinks->get_links(from_table => 'dunning', to_table => 'email_journal', from_id => $ref->{dunning_table_id});
+      if (scalar @mail_links) {
+        my $email_journals = SL::DB::Manager::EmailJournal->get_all(where => [id => [ map { $_->{to_id} } @mail_links ]]);
+        my $html          = join '<br>', map { SL::Presenter::EmailJournal::email_journal($_) } @$email_journals;
+        my $text          = join "\n",   map { $_->subject                                    } @$email_journals;
+        $row->{mails}     = { 'raw_data' => $html, data => $text };
+      } else {
+        $row->{mails}     = { };
+      }
+    }
+
     push @{ $current_dunning_rows }, $row;
 
     $previous_dunning_id   = $ref->{dunning_id};
@@ -453,6 +534,7 @@ sub show_dunning {
 
   $report->set_options_from_form();
 
+  setup_dn_show_dunning_action_bar();
   $report->generate_with_headers();
 
   $main::lxdebug->leave_sub();
@@ -528,6 +610,11 @@ sub print_multiple {
     $form->{dunning_id} = $dunning_id;
     DN->print_invoice_for_fees(\%myconfig, $form, $dunning_id);
     DN->print_dunning(\%myconfig, $form, $dunning_id);
+
+    # print original dunned invoices, if they where printed on dunning run
+    my $dunnings = SL::DB::Manager::Dunning->get_all(where => [dunning_id => $dunning_id, original_invoice_printed => 1]);
+    DN->print_original_invoice(\%myconfig, $form, $dunning_id, $_->trans_id) for @$dunnings;
+
     $i++;
   }
   $form->{language_id} = $saved_language_id;
@@ -562,4 +649,98 @@ sub dispatcher {
 
   $::form->error($::locale->text('No action defined.'));
 }
+
+sub setup_dn_add_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Search'),
+        submit    => [ '#form', { action => "show_invoices" } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_dn_show_invoices_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Create'),
+        submit    => [ '#form', { action => "save_dunning" } ],
+        checks    => [ [ 'kivi.check_if_entries_selected', '[name^=active_]' ] ],
+        accesskey => 'enter',
+        only_once => 1,
+      ],
+    );
+  }
+}
+
+sub setup_dn_search_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Search'),
+        submit    => [ '#form', { action => "show_dunning" } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_dn_show_dunning_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Print'),
+        submit    => [ '#form', { action => "print_multiple" } ],
+        checks    => [ [ 'kivi.check_if_entries_selected', '[name^=selected_]' ] ],
+        accesskey => 'enter',
+      ],
+
+      action => [
+        t8('Delete'),
+        submit  => [ '#form', { action => "delete" } ],
+        checks  => [ [ 'kivi.check_if_entries_selected', '[name^=selected_]' ] ],
+        confirm => $::locale->text('This resets the dunning process for the selected invoices. Posted dunning invoices will not be changed!'),
+      ],
+    );
+  }
+}
+
+sub setup_dn_edit_config_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => "save" } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_dn_status_action_bar {
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Back'),
+        link      => $::form->{callback},
+        accesskey => 'enter',
+      ],
+    );
+  }
+
+}
+
 # end of main
index ac71eca..e0b4dbc 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Delivery orders
@@ -34,19 +35,20 @@ use Carp;
 use List::MoreUtils qw(uniq);
 use List::Util qw(max sum);
 use POSIX qw(strftime);
-use YAML;
 
+use SL::Controller::DeliveryOrder;
 use SL::DB::DeliveryOrder;
+use SL::DB::DeliveryOrder::TypeData qw(:types validate_type);
+use SL::Helper::UserPreferences::DisplayPreferences;
 use SL::DO;
 use SL::IR;
 use SL::IS;
-use SL::MoreCommon qw(ary_diff);
+use SL::MoreCommon qw(ary_diff restore_form save_form);
 use SL::ReportGenerator;
 use SL::WH;
+use SL::YAML;
 use Sort::Naturally ();
-require "bin/mozilla/arap.pl";
 require "bin/mozilla/common.pl";
-require "bin/mozilla/invoice_io.pl";
 require "bin/mozilla/io.pl";
 require "bin/mozilla/reportgenerator.pl";
 
@@ -56,8 +58,18 @@ use strict;
 
 # end of main
 
+sub check_do_access_for_edit {
+  validate_type($::form->{type});
+
+  my $right = SL::DB::DeliveryOrder::TypeData::get3($::form->{type}, "rights", "edit");
+  $main::auth->assert($right);
+}
+
 sub check_do_access {
-  $main::auth->assert($main::form->{type} . '_edit');
+  validate_type($::form->{type});
+
+  my $right = SL::DB::DeliveryOrder::TypeData::get3($::form->{type}, "rights", "view");
+  $main::auth->assert($right);
 }
 
 sub set_headings {
@@ -86,7 +98,7 @@ sub set_headings {
 sub add {
   $main::lxdebug->enter_sub();
 
-  check_do_access();
+  check_do_access_for_edit();
 
   if (($::form->{type} =~ /purchase/) && !$::instance_conf->get_allow_new_purchase_invoice) {
     $::form->show_generic_error($::locale->text("You do not have the permissions to access this function."));
@@ -96,9 +108,10 @@ sub add {
 
   set_headings("add");
 
+  $form->{show_details} = $::myconfig{show_form_details};
   $form->{callback} = build_std_url('action=add', 'type', 'vc') unless ($form->{callback});
 
-  order_links();
+  order_links(is_new => 1);
   prepare_order();
   display_form();
 
@@ -112,6 +125,8 @@ sub edit {
 
   my $form     = $main::form;
 
+  $form->{show_details} = $::myconfig{show_form_details};
+
   # show history button
   $form->{javascript} = qq|<script type="text/javascript" src="js/show_history.js"></script>|;
   #/show hhistory button
@@ -166,15 +181,11 @@ sub order_links {
 
   check_do_access();
 
+  my %params   = @_;
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  # get customer/vendor
-  $form->all_vc(\%myconfig, $form->{vc}, ($form->{vc} eq 'customer') ? "AR" : "AP");
-
   # retrieve order/quotation
-  $form->{webdav}   = $::instance_conf->get_webdav;
-
   my $editing = $form->{id};
 
   DO->retrieve('vc'  => $form->{vc},
@@ -189,6 +200,7 @@ sub order_links {
   } else {
     IS->get_customer(\%myconfig, \%$form);
     $form->{discount} = $form->{customer_discount};
+    $form->{billing_address_id} = $form->{default_billing_address_id} if $params{is_new};
   }
 
   $form->restore_vars(qw(payment_id language_id taxzone_id intnotes cp_id delivery_term_id));
@@ -196,17 +208,6 @@ sub order_links {
   $form->restore_vars(qw(taxincluded)) if $form->{id};
   $form->restore_vars(qw(salesman_id)) if $editing;
 
-  if ($form->{"all_$form->{vc}"}) {
-    unless ($form->{"$form->{vc}_id"}) {
-      $form->{"$form->{vc}_id"} = $form->{"all_$form->{vc}"}->[0]->{id};
-    }
-  }
-
-  ($form->{ $form->{vc} })  = split /--/, $form->{ $form->{vc} };
-  $form->{"old$form->{vc}"} = qq|$form->{$form->{vc}}--$form->{"$form->{vc}_id"}|;
-
-  $form->{employee} = "$form->{employee}--$form->{employee_id}";
-
   $main::lxdebug->leave_sub();
 }
 
@@ -250,6 +251,203 @@ sub prepare_order {
   $main::lxdebug->leave_sub();
 }
 
+sub setup_do_action_bar {
+  my @transfer_qty   = qw(kivi.SalesPurchase.delivery_order_check_transfer_qty);
+  my @req_trans_desc = qw(kivi.SalesPurchase.check_transaction_description) x!!$::instance_conf->get_require_transaction_description_ps;
+  my $is_customer    = $::form->{vc} eq 'customer';
+
+  my $undo_date  = DateTime->today->subtract(days => $::instance_conf->get_undo_transfer_interval);
+  my $insertdate = DateTime->from_kivitendo($::form->{insertdate});
+  my $undo_transfer  = 0;
+  if (ref $undo_date eq 'DateTime' && ref $insertdate eq 'DateTime') {
+    $undo_transfer = $insertdate > $undo_date;
+  }
+
+  my $may_edit_create = $::auth->assert(SL::DB::DeliveryOrder::TypeData::get3($::form->{type}, "rights", "edit"), 1);
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action =>
+        [ t8('Update'),
+          submit    => [ '#form', { action => "update" } ],
+          disabled  => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+          id        => 'update_button',
+          accesskey => 'enter',
+        ],
+
+      combobox => [
+        action => [
+          t8('Save'),
+          submit   => [ '#form', { action => "save" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create    ? t8('You do not have the permissions to access this function.')
+                    : $::form->{delivered} ? t8('This record has already been delivered.')
+                    :                        undef,
+        ],
+        action => [
+          t8('Save as new'),
+          submit   => [ '#form', { action => "save_as_new" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
+                    : !$::form->{id},
+        ],
+        action => [
+          t8('Mark as closed'),
+          submit   => [ '#form', { action => "mark_closed" } ],
+          checks   => [ 'kivi.validate_form' ],
+          confirm  => t8('This will remove the delivery order from showing as open even if contents are not delivered. Proceed?'),
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
+                    : !$::form->{id}    ? t8('This record has not been saved yet.')
+                    : $::form->{closed} ? t8('This record has already been closed.')
+                    :                     undef,
+        ],
+      ], # end of combobox "Save"
+
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => "delete" } ],
+        confirm  => t8('Do you really want to delete this object?'),
+        disabled => !$may_edit_create                                                                           ? t8('You do not have the permissions to access this function.')
+                  : !$::form->{id}                                                                              ? t8('This record has not been saved yet.')
+                  : $::form->{delivered}                                                                        ? t8('This record has already been delivered.')
+                  : ($::form->{vc} eq 'customer' && !$::instance_conf->get_sales_delivery_order_show_delete)    ? t8('Deleting this type of record has been disabled in the configuration.')
+                  : ($::form->{vc} eq 'vendor'   && !$::instance_conf->get_purchase_delivery_order_show_delete) ? t8('Deleting this type of record has been disabled in the configuration.')
+                  :                                                                                               undef,
+      ],
+
+      combobox => [
+        action => [
+          t8('Transfer out'),
+          submit   => [ '#form', { action => "transfer_out" } ],
+          checks   => [ 'kivi.validate_form', @transfer_qty ],
+          disabled => !$may_edit_create    ? t8('You do not have the permissions to access this function.')
+                    : $::form->{delivered} ? t8('This record has already been delivered.')
+                    :                        undef,
+          only_if  => $is_customer,
+        ],
+        action => [
+          t8('Transfer out via default'),
+          submit   => [ '#form', { action => "transfer_out_default" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create    ? t8('You do not have the permissions to access this function.')
+                    : $::form->{delivered} ? t8('This record has already been delivered.')
+                    :                        undef,
+          only_if  => $is_customer && $::instance_conf->get_transfer_default,
+        ],
+        action => [
+          t8('Transfer in'),
+          submit   => [ '#form', { action => "transfer_in" } ],
+          checks   => [ 'kivi.validate_form', @transfer_qty ],
+          disabled => !$may_edit_create    ? t8('You do not have the permissions to access this function.')
+                    : $::form->{delivered} ? t8('This record has already been delivered.')
+                    :                        undef,
+          only_if  => !$is_customer,
+        ],
+        action => [
+          t8('Transfer in via default'),
+          submit   => [ '#form', { action => "transfer_in_default" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create    ? t8('You do not have the permissions to access this function.')
+                    : $::form->{delivered} ? t8('This record has already been delivered.')
+                    :                        undef,
+          only_if  => !$is_customer && $::instance_conf->get_transfer_default,
+        ],
+        action => [
+          t8('Undo Transfer'),
+          submit   => [ '#form', { action => "delete_transfers" } ],
+          checks   => [ 'kivi.validate_form' ],
+          only_if  => $::form->{delivered},
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
+                    : !$undo_transfer   ? t8('Transfer date exceeds the maximum allowed interval.')
+                    :                     undef,
+        ],
+      ], # end of combobox "Transfer out"
+
+
+      'separator',
+
+      action => [
+        t8('Invoice'),
+        submit => [ '#form', { action => "invoice" } ],
+        disabled => !$::form->{id} ? t8('This record has not been saved yet.') : undef,
+        confirm  => $::form->{delivered}                                                                         ? undef
+                  : ($::form->{vc} eq 'customer' && $::instance_conf->get_sales_delivery_order_check_stocked)    ? t8('This record has not been stocked out. Proceed?')
+                  : ($::form->{vc} eq 'vendor'   && $::instance_conf->get_purchase_delivery_order_check_stocked) ? t8('This record has not been stocked in. Proceed?')
+                  :                                                                                                undef,
+      ],
+
+      combobox => [
+        action => [ t8('Export') ],
+        action => [
+          t8('Print'),
+          call     => [ 'kivi.SalesPurchase.show_print_dialog' ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
+        ],
+        action => [
+          t8('E Mail'),
+          call   => [ 'kivi.SalesPurchase.show_email_dialog' ],
+          checks => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
+                    : !$::form->{id} ?    t8('This record has not been saved yet.')
+                    :                     undef,
+        ],
+      ], # end of combobox "Export"
+
+      combobox =>  [
+        action => [ t8('more') ],
+        action => [
+          t8('History'),
+          call     => [ 'set_history_window', $::form->{id} * 1, 'id' ],
+          disabled => !$::form->{id} ? t8('This record has not been saved yet.') : undef,
+        ],
+        action => [
+          t8('Follow-Up'),
+          call     => [ 'follow_up_window' ],
+          disabled => !$::form->{id} ? t8('This record has not been saved yet.') : undef,
+        ],
+      ], # end if combobox "more"
+    );
+  }
+  $::request->layout->add_javascripts('kivi.Validator.js');
+}
+
+sub setup_do_search_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Search'),
+        submit    => [ '#form' ],
+        accesskey => 'enter',
+        checks    => [ 'kivi.validate_form' ],
+      ],
+    );
+  }
+  $::request->layout->add_javascripts('kivi.Validator.js');
+}
+
+sub setup_do_orders_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('New invoice'),
+        submit    => [ '#form', { action => 'invoice_multi' } ],
+        checks    => [ [ 'kivi.check_if_entries_selected', '#form tbody input[type=checkbox]' ] ],
+        accesskey => 'enter',
+      ],
+      action => [
+        t8('Print'),
+        call   => [ 'kivi.SalesPurchase.show_print_dialog', 'js:kivi.MassDeliveryOrderPrint.submitMultiOrders' ],
+        checks => [ [ 'kivi.check_if_entries_selected', '#form tbody input[type=checkbox]' ] ],
+      ],
+    );
+  }
+}
+
 sub form_header {
   $main::lxdebug->enter_sub();
 
@@ -258,15 +456,22 @@ sub form_header {
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  $form->{employee_id} = $form->{old_employee_id} if $form->{old_employee_id};
-  $form->{salesman_id} = $form->{old_salesman_id} if $form->{old_salesman_id};
+  my $class       = "SL::DB::" . ($form->{vc} eq 'customer' ? 'Customer' : 'Vendor');
+  $form->{VC_OBJ} = $class->load_cached($form->{ $form->{vc} . '_id' });
+
+  $form->{CONTACT_OBJ}   = $form->{cp_id} ? SL::DB::Contact->load_cached($form->{cp_id}) : undef;
+  my $current_employee   = SL::DB::Manager::Employee->current;
+  $form->{employee_id}   = $form->{old_employee_id} if $form->{old_employee_id};
+  $form->{salesman_id}   = $form->{old_salesman_id} if $form->{old_salesman_id};
+  $form->{employee_id} ||= $current_employee->id;
+  $form->{salesman_id} ||= $current_employee->id;
 
   my $vc = $form->{vc} eq "customer" ? "customers" : "vendors";
-  $form->get_lists($vc              => "ALL_VC",
-                   "price_factors"  => "ALL_PRICE_FACTORS",
-                   "departments"    => "ALL_DEPARTMENTS",
+  $form->get_lists("price_factors"  => "ALL_PRICE_FACTORS",
                    "business_types" => "ALL_BUSINESS_TYPES",
     );
+  $form->{ALL_DEPARTMENTS} = SL::DB::Manager::Department->get_all_sorted;
+  $form->{ALL_LANGUAGES}   = SL::DB::Manager::Language->get_all_sorted;
 
   # Projects
   my @old_project_ids = uniq grep { $_ } map { $_ * 1 } ($form->{"globalproject_id"}, map { $form->{"project_id_$_"} } 1..$form->{"rowcount"});
@@ -301,31 +506,23 @@ sub form_header {
     ]
   ]);
 
-  map { $_->{value} = "$_->{description}--$_->{id}" } @{ $form->{ALL_DEPARTMENTS} };
-  map { $_->{value} = "$_->{name}--$_->{id}"        } @{ $form->{ALL_VC} };
-
-  $form->{SHOW_VC_DROP_DOWN} =  $myconfig{vclimit} > scalar @{ $form->{ALL_VC} };
-
-  $form->{oldvcname}         =  $form->{"old$form->{vc}"};
-  $form->{oldvcname}         =~ s/--.*//;
-
   my $dispatch_to_popup = '';
   if ($form->{resubmit} && ($form->{format} eq "html")) {
     $dispatch_to_popup  = "window.open('about:blank','Beleg'); document.do.target = 'Beleg';";
     $dispatch_to_popup .= "document.do.submit();";
-  } elsif ($form->{resubmit}) {
+  } elsif ($form->{resubmit} && $form->{action_print}) {
     # emulate click for resubmitting actions
-    $dispatch_to_popup  = "document.do.${_}.click(); " for grep { /^action_/ } keys %$form;
+    $dispatch_to_popup  = "kivi.SalesPurchase.show_print_dialog(); kivi.SalesPurchase.print_record();";
   }
   $::request->{layout}->add_javascripts_inline("\$(function(){$dispatch_to_popup});");
 
 
-  my $follow_up_vc                =  $form->{ $form->{vc} eq 'customer' ? 'customer' : 'vendor' };
-  $follow_up_vc                   =~ s/--\d*\s*$//;
+  $form->{follow_up_trans_info} = $form->{donumber} .'('. $form->{VC_OBJ}->name .')' if $form->{VC_OBJ};
+  $form->{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
 
-  $form->{follow_up_trans_info} = $form->{donumber} .'('. $follow_up_vc .')';
+  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.File kivi.MassDeliveryOrderPrint kivi.SalesPurchase kivi.Part kivi.CustomerVendor kivi.Validator ckeditor/ckeditor ckeditor/adapters/jquery kivi.io));
 
-  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.SalesPurchase ckeditor/ckeditor ckeditor/adapters/jquery kivi.io autocomplete_customer autocomplete_part));
+  setup_do_action_bar();
 
   $form->header();
   # Fix für Bug 1082 Erwartet wird: 'abteilungsNAME--abteilungsID'
@@ -352,11 +549,18 @@ sub form_footer {
 
   my $form     = $main::form;
 
-  $form->{PRINT_OPTIONS} = print_options('inline' => 1);
+  $form->{PRINT_OPTIONS}      = setup_sales_purchase_print_options();
   $form->{ALL_DELIVERY_TERMS} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
+  my $shipto_cvars       = SL::DB::Shipto->new->cvars_by_config;
+  foreach my $var (@{ $shipto_cvars }) {
+    my $name = "shiptocvar_" . $var->config->name;
+    $var->value($form->{$name}) if exists $form->{$name};
+  }
+
   print $form->parse_html_template('do/form_footer',
-    {transfer_default         => ($::instance_conf->get_transfer_default)});
+    {transfer_default => ($::instance_conf->get_transfer_default),
+     shipto_cvars     => $shipto_cvars});
 
   $main::lxdebug->leave_sub();
 }
@@ -378,7 +582,18 @@ sub update_delivery_order {
   my $payment_id;
   $payment_id = $form->{payment_id} if $form->{payment_id};
 
-  check_name($form->{vc});
+  my $vc = $form->{vc};
+  if (($form->{"previous_${vc}_id"} || $form->{"${vc}_id"}) != $form->{"${vc}_id"}) {
+    $::form->{salesman_id} = SL::DB::Manager::Employee->current->id if exists $::form->{salesman_id};
+
+    if ($vc eq 'customer') {
+      IS->get_customer(\%myconfig, $form);
+      $::form->{billing_address_id} = $::form->{default_billing_address_id};
+    } else {
+      IR->get_vendor(\%myconfig, $form);
+    }
+  }
+
   $form->{discount} =  $form->{"$form->{vc}_discount"} if defined $form->{"$form->{vc}_discount"};
   # Problem: Wenn man ohne Erneuern einen Kunden/Lieferanten
   # wechselt, wird der entsprechende Kunden/ Lieferantenrabatt
@@ -424,7 +639,7 @@ sub update_delivery_order {
       if ($rows > 1) {
 
         select_item(mode => $mode, pre_entered_qty => $form->{"qty_$i"});
-        ::end_of_request();
+        $::dispatcher->end_request;
 
       } else {
 
@@ -496,14 +711,13 @@ sub search {
 
   $form->get_lists("projects"       => { "key" => "ALL_PROJECTS",
                                          "all" => 1 },
-                   "departments"    => "ALL_DEPARTMENTS",
-                   "$form->{vc}s"   => "ALL_VC",
                    "business_types" => "ALL_BUSINESS_TYPES");
   $form->{ALL_EMPLOYEES} = SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]);
-
-  $form->{SHOW_VC_DROP_DOWN} =  $myconfig{vclimit} > scalar @{ $form->{ALL_VC} };
+  $form->{ALL_DEPARTMENTS} = SL::DB::Manager::Department->get_all_sorted;
   $form->{title}             = $locale->text('Delivery Orders');
 
+  setup_do_search_action_bar();
+
   $form->header();
 
   print $form->parse_html_template('do/search');
@@ -521,7 +735,7 @@ sub orders {
   my $locale   = $main::locale;
   my $cgi      = $::request->{cgi};
 
-  $form->{department_id} = (split /--/, $form->{department})[-1];
+  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.MassDeliveryOrderPrint kivi.SalesPurchase));
   ($form->{ $form->{vc} }, $form->{"$form->{vc}_id"}) = split(/--/, $form->{ $form->{vc} });
 
   report_generator_set_default_sort('transdate', 1);
@@ -553,13 +767,13 @@ sub orders {
   my @hidden_variables = map { "l_${_}" } @columns;
   push @hidden_variables, $form->{vc}, qw(l_closed l_notdelivered open closed delivered notdelivered donumber ordnumber serialnumber cusordnumber
                                           transaction_description transdatefrom transdateto reqdatefrom reqdateto
-                                          type vc employee_id salesman_id project_id
-                                          insertdatefrom insertdateto business_id);
+                                          type vc employee_id salesman_id project_id parts_partnumber parts_description
+                                          insertdatefrom insertdateto business_id all department_id);
 
   my $href = build_std_url('action=orders', grep { $form->{$_} } @hidden_variables);
 
   my %column_defs = (
-    'ids'                     => { 'text' => '', },
+    'ids'                     => { raw_header_data => SL::Presenter::Tag::checkbox_tag("", id => "multi_all", checkall => "[data-checkall=1]"), align => 'center' },
     'transdate'               => { 'text' => $locale->text('Delivery Order Date'), },
     'reqdate'                 => { 'text' => $locale->text('Reqdate'), },
     'id'                      => { 'text' => $locale->text('ID'), },
@@ -606,9 +820,8 @@ sub orders {
   if ($form->{cp_name}) {
     push @options, $locale->text('Contact Person') . " : $form->{cp_name}";
   }
-  if ($form->{department}) {
-    my ($department) = split /--/, $form->{department};
-    push @options, $locale->text('Department') . " : $department";
+  if ($form->{department_id}) {
+    push @options, $locale->text('Department') . " : " . SL::DB::Department->new(id => $form->{department_id})->load->description;
   }
   if ($form->{donumber}) {
     push @options, $locale->text('Delivery Order Number') . " : $form->{donumber}";
@@ -624,6 +837,12 @@ sub orders {
   if ($form->{transaction_description}) {
     push @options, $locale->text('Transaction description') . " : $form->{transaction_description}";
   }
+  if ($form->{parts_description}) {
+    push @options, $locale->text('Part Description') . " : $form->{parts_description}";
+  }
+  if ($form->{parts_partnumber}) {
+    push @options, $locale->text('Part Number') . " : $form->{parts_partnumber}";
+  }
   if ( $form->{transdatefrom} or $form->{transdateto} ) {
     push @options, $locale->text('Delivery Order Date');
     push @options, $locale->text('From') . " " . $locale->date(\%myconfig, $form->{transdatefrom}, 1)     if $form->{transdatefrom};
@@ -651,10 +870,25 @@ sub orders {
   if ($form->{notdelivered}) {
     push @options, $locale->text('Not delivered');
   }
+  push @options, $locale->text('Quick Search') . " : $form->{all}" if $form->{all};
+
+  my $pr = SL::DB::Manager::Printer->find_by(
+      printer_description => $::locale->text("sales_delivery_order_printer"));
+  if ($pr ) {
+      $form->{printer_id} = $pr->id;
+  }
+
+  my $print_options = SL::Helper::PrintOptions->get_print_options(
+    options => {
+      hide_language_id => 1,
+      show_bothsided   => 1,
+      show_headers     => 1,
+    },
+  );
 
   $report->set_options('top_info_text'        => join("\n", @options),
                        'raw_top_info_text'    => $form->parse_html_template('do/orders_top'),
-                       'raw_bottom_info_text' => $form->parse_html_template('do/orders_bottom'),
+                       'raw_bottom_info_text' => $form->parse_html_template('do/orders_bottom', { print_options => $print_options }),
                        'output_format'        => 'HTML',
                        'title'                => $form->{title},
                        'attachment_basename'  => $attachment_basename . strftime('_%Y%m%d', localtime time),
@@ -669,7 +903,9 @@ sub orders {
   my $callback = $form->escape($href);
 
   my $edit_url       = build_std_url('action=edit', 'type', 'vc');
-  my $edit_order_url = build_std_url('script=oe.pl', 'type=' . ($form->{type} eq 'sales_delivery_order' ? 'sales_order' : 'purchase_order'), 'action=edit');
+  my $edit_order_url = ($::instance_conf->get_feature_experimental_order)
+                     ? build_std_url('script=controller.pl', 'action=Order/edit', 'type=' . ($form->{type} eq 'sales_delivery_order' ? 'sales_order' : 'purchase_order'))
+                     : build_std_url('script=oe.pl',         'action=edit',       'type=' . ($form->{type} eq 'sales_delivery_order' ? 'sales_order' : 'purchase_order'));
 
   my $idx            = 1;
 
@@ -679,20 +915,25 @@ sub orders {
 
     my $row = { map { $_ => { 'data' => $dord->{$_} } } @columns };
 
+    my $ord_id = $dord->{id};
     $row->{ids}  = {
-      'raw_data' =>   $cgi->hidden('-name' => "trans_id_${idx}", '-value' => $dord->{id})
-                    . $cgi->checkbox('-name' => "multi_id_${idx}", '-value' => 1, '-label' => ''),
+      'raw_data' =>   $cgi->hidden('-name' => "trans_id_${idx}", '-value' => $ord_id)
+                    . $cgi->checkbox('-name' => "multi_id_${idx}",' id' => "multi_id_id_".$ord_id, '-value' => 1, 'data-checkall' => 1, '-label' => ''),
       'valign'   => 'center',
       'align'    => 'center',
     };
 
-    $row->{donumber}->{link}  = $edit_url       . "&id=" . E($dord->{id})      . "&callback=${callback}";
+    $row->{donumber}->{link}  = SL::DB::DeliveryOrder::TypeData::get3($dord->{order_type}, "show_menu", "new_controller")
+                              ? SL::Controller::DeliveryOrder->url_for(action => "edit", id => $dord->{id}, type => $dord->{order_type})
+                              : $edit_url  . "&id=" . E($dord->{id})      . "&callback=${callback}";
     $row->{ordnumber}->{link} = $edit_order_url . "&id=" . E($dord->{oe_id})   . "&callback=${callback}" if $dord->{oe_id};
     $report->add_data($row);
 
     $idx++;
   }
 
+  setup_do_orders_action_bar();
+
   $report->generate_with_headers();
 
   $main::lxdebug->leave_sub();
@@ -703,7 +944,7 @@ sub save {
 
   my (%params) = @_;
 
-  check_do_access();
+  check_do_access_for_edit();
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -719,7 +960,7 @@ sub save {
   $form->{donumber} =~ s/\s*$//g;
 
   my $msg = ucfirst $form->{vc};
-  $form->isblank($form->{vc}, $locale->text($msg . " missing!"));
+  $form->isblank($form->{vc} . "_id", $locale->text($msg . " missing!"));
 
   # $locale->text('Customer missing!');
   # $locale->text('Vendor missing!');
@@ -727,14 +968,43 @@ sub save {
   remove_emptied_rows();
   validate_items();
 
+  # check for serial number if part needs one
+  for my $i (1 .. $form->{rowcount} - 1) {
+    next unless $form->{"has_sernumber_$i"};
+    $form->isblank("serialnumber_$i",
+                   $locale->text('Serial Number missing in Row') . " $i");
+  }
   # if the name changed get new values
-  if (check_name($form->{vc})) {
+  my $vc = $form->{vc};
+  if (($form->{"previous_${vc}_id"} || $form->{"${vc}_id"}) != $form->{"${vc}_id"}) {
+    $::form->{salesman_id} = SL::DB::Manager::Employee->current->id if exists $::form->{salesman_id};
+
+    if ($vc eq 'customer') {
+      IS->get_customer(\%myconfig, $form);
+      $::form->{billing_address_id} = $::form->{default_billing_address_id};
+    } else {
+      IR->get_vendor(\%myconfig, $form);
+    }
+
     update();
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
 
   $form->{id} = 0 if $form->{saveasnew};
-
+  # we rely on converted_from_orderitems, if the workflow is used
+  # be sure that at least one position is linked to the original orderitem
+  if ($form->{convert_from_oe_ids}) {
+    my $has_linked_pos;
+    for my $i (1 .. $form->{rowcount}) {
+      if ($form->{"converted_from_orderitems_id_$i"}) {
+        $has_linked_pos = 1;
+        last;
+      }
+    }
+    if (!$has_linked_pos) {
+      $form->error($locale->text('Need at least one original position for the workflow Order to Delivery Order!'));
+    }
+  }
   DO->save();
   # saving the history
   if(!exists $form->{addition}) {
@@ -748,7 +1018,7 @@ sub save {
   if (!$params{no_redirect} && !$form->{print_and_save}) {
     delete @{$form}{ary_diff([keys %{ $form }], [qw(login id script type cursor_fokus)])};
     edit();
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
   $main::lxdebug->leave_sub();
 }
@@ -756,13 +1026,13 @@ sub save {
 sub delete {
   $main::lxdebug->enter_sub();
 
-  check_do_access();
+  check_do_access_for_edit();
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
-
-  if (DO->delete()) {
+  my $ret;
+  if ($ret = DO->delete()) {
     # saving the history
     if(!exists $form->{addition}) {
       $form->{snumbers} = qq|donumber_| . $form->{donumber};
@@ -772,10 +1042,41 @@ sub delete {
     # /saving the history
 
     $form->info($locale->text('Delivery Order deleted!'));
-    ::end_of_request();
+    $::dispatcher->end_request;
+  }
+
+  $form->error($locale->text('Cannot delete delivery order!') . $ret);
+
+  $main::lxdebug->leave_sub();
+}
+sub delete_transfers {
+  $main::lxdebug->enter_sub();
+
+  check_do_access_for_edit();
+
+  my $form     = $main::form;
+  my %myconfig = %main::myconfig;
+  my $locale   = $main::locale;
+  my $ret;
+
+  die "Invalid form type" unless $form->{type} =~ m/^(sales|purchase)_delivery_order$/;
+
+  if ($ret = DO->delete_transfers()) {
+    # saving the history
+    if(!exists $form->{addition}) {
+      $form->{snumbers} = qq|donumber_| . $form->{donumber};
+      $form->{addition} = "UNDO TRANSFER";
+      $form->save_history;
+    }
+    # /saving the history
+
+    flash_later('info', $locale->text("Transfer undone."));
+
+    $form->{callback} = 'do.pl?action=edit&type=' . $form->{type} . '&id=' . $form->escape($form->{id});
+    $form->redirect;
   }
 
-  $form->error($locale->text('Cannot delete delivery order!'));
+  $form->error($locale->text('Cannot undo delivery order transfer!') . $ret);
 
   $main::lxdebug->leave_sub();
 }
@@ -792,8 +1093,12 @@ sub invoice {
 
   $main::auth->assert($form->{type} eq 'purchase_delivery_order' ? 'vendor_invoice_edit' : 'invoice_edit');
 
+  $form->get_employee();
+
   $form->{convert_from_do_ids} = $form->{id};
-  $form->{deliverydate}        = $form->{transdate};
+  # if we have a reqdate (Liefertermin), this is definetely the preferred
+  # deliverydate for invoices
+  $form->{deliverydate}        = $form->{reqdate} || $form->{transdate};
   $form->{transdate}           = $form->{invdate} = $form->current_date(\%myconfig);
   $form->{duedate}             = $form->current_date(\%myconfig, $form->{invdate}, $form->{terms} * 1);
   $form->{defaultcurrency}     = $form->get_default_currency(\%myconfig);
@@ -817,7 +1122,10 @@ sub invoice {
   }
 
   for my $i (1 .. $form->{rowcount}) {
+    map { $form->{"${_}_${i}"} = $form->parse_amount(\%myconfig, $form->{"${_}_${i}"}) if $form->{"${_}_${i}"} } qw(ship qty sellprice lastcost basefactor discount);
     # für bug 1284
+    # adds a customer/vendor discount, unless we have a workflow case
+    # CAVEAT: has to be done, after the above parse_amount
     unless ($form->{"ordnumber"}) {
       if ($form->{discount}) { # Falls wir einen Lieferanten-/Kundenrabatt haben
         # und rabattfähig sind, dann
@@ -826,7 +1134,6 @@ sub invoice {
         }
       }
     }
-    map { $form->{"${_}_${i}"} = $form->parse_amount(\%myconfig, $form->{"${_}_${i}"}) if $form->{"${_}_${i}"} } qw(ship qty sellprice lastcost basefactor);
     $form->{"donumber_$i"} = $form->{donumber};
     $form->{"converted_from_delivery_order_items_id_$i"} = delete $form->{"delivery_order_items_id_$i"};
   }
@@ -844,10 +1151,12 @@ sub invoice {
 
   if ($form->{ordnumber}) {
     require SL::DB::Order;
-    if (my $order = SL::DB::Manager::Order->find_by(ordnumber => $form->{ordnumber})) {
+    my $vc_id  = $form->{type} =~ /^sales/ ? 'customer_id' : 'vendor_id';
+    if (my $order = SL::DB::Manager::Order->find_by(ordnumber => $form->{ordnumber}, $vc_id => $form->{"$vc_id"})) {
       $order->load;
       $form->{orddate} = $order->transdate_as_date;
-      $form->{$_}      = $order->$_ for qw(payment_id salesman_id taxzone_id quonumber);
+      $form->{$_}      = $order->$_ for qw(payment_id salesman_id taxzone_id quonumber taxincluded);
+      $form->{taxincluded_changed_by_user} = 1;
     }
   }
 
@@ -904,7 +1213,7 @@ sub invoice_multi {
   my @do_ids = map { $form->{"trans_id_$_"} } grep { $form->{"multi_id_$_"} } (1..$form->{rowcount});
 
   if (!scalar @do_ids) {
-    $form->show_generic_error($locale->text('You have not selected any delivery order.'), 'back_button' => 1);
+    $form->show_generic_error($locale->text('You have not selected any delivery order.'));
   }
 
   map { delete $form->{$_} } grep { m/^(?:trans|multi)_id_\d+/ } keys %{ $form };
@@ -958,7 +1267,8 @@ sub invoice_multi {
     IS->get_customer(\%myconfig, \%$form);
     $vc_discount = $form->{customer_discount};
   }
-  restore_form($saved_form);
+  # use payment terms from customer or vendor
+  restore_form($saved_form,0,qw(payment_id));
 
   $form->{rowcount} = 0;
   foreach my $ref (@{ $form->{form_details} }) {
@@ -996,7 +1306,7 @@ sub invoice_multi {
 sub save_as_new {
   $main::lxdebug->enter_sub();
 
-  check_do_access();
+  check_do_access_for_edit();
 
   my $form     = $main::form;
 
@@ -1019,26 +1329,6 @@ sub save_as_new {
   $main::lxdebug->leave_sub();
 }
 
-sub e_mail {
-  $main::lxdebug->enter_sub();
-
-  check_do_access();
-
-  $::form->mtime_ischanged('delivery_orders','mail');
-
-  $::form->{print_and_save} = 1;
-
-  my $saved_form = save_form();
-
-  save();
-
-  restore_form($saved_form, 0, qw(id ordnumber quonumber));
-
-  edit_e_mail();
-
-  $main::lxdebug->leave_sub();
-}
-
 sub calculate_stock_in_out {
   $main::lxdebug->enter_sub();
 
@@ -1060,11 +1350,9 @@ sub calculate_stock_in_out {
   my $sum      = AM->sum_with_unit(map { $_->{qty}, $_->{unit} } @{ $sinfo });
   my $matches  = $do_qty == $sum;
 
-  my $content  = $form->format_amount_units('amount'      => $sum * 1,
-                                            'part_unit'   => $form->{"partunit_$i"},
-                                            'amount_unit' => $all_units->{$form->{"partunit_$i"}}->{base_unit},
-                                            'conv_units'  => 'convertible_not_smaller',
-                                            'max_places'  => 2);
+  my $amount_unit = $all_units->{$form->{"partunit_$i"}}->{base_unit};
+  my $content     = $form->format_amount(\%::myconfig, AM->convert_unit($amount_unit, $form->{"unit_$i"}) * $sum * 1) . ' ' . $form->{"unit_$i"};
+
   $content     = qq|<span id="stock_in_out_qty_display_${i}">${content}</span><input type=hidden id='stock_in_out_qty_matches_$i' value='$matches'> <input type="button" onclick="open_stock_in_out_window('${in_out}', $i);" value="?">|;
 
   $main::lxdebug->leave_sub();
@@ -1198,11 +1486,8 @@ sub _stock_in_out_set_qty_display {
   my $form             = $::form;
   my $all_units        = AM->retrieve_all_units();
   my $sum              = AM->sum_with_unit(map { $_->{qty}, $_->{unit} } @{ $stock_info });
-  $form->{qty_display} = $form->format_amount_units(amount      => $sum * 1,
-                                                    part_unit   => $form->{partunit},
-                                                    amount_unit => $all_units->{ $form->{partunit} }->{base_unit},
-                                                    conv_units  => 'convertible_not_smaller',
-                                                    max_places  => 2);
+  my $amount_unit      = $all_units->{$form->{"partunit"}}->{base_unit};
+  $form->{qty_display} = $form->format_amount(\%::myconfig, AM->convert_unit($amount_unit, $form->{"do_unit"}) * $sum * 1) . ' ' . $form->{"do_unit"};
 }
 
 sub set_stock_in {
@@ -1221,7 +1506,7 @@ sub set_stock_in {
     push @{ $stock_info }, { map { $_ => $form->{"${_}_${i}"} } qw(delivery_order_items_stock_id warehouse_id bin_id chargenumber bestbefore qty unit) };
   }
 
-  $form->{stock} = YAML::Dump($stock_info);
+  $form->{stock} = SL::YAML::Dump($stock_info);
 
   _stock_in_out_set_qty_display($stock_info);
 
@@ -1256,10 +1541,7 @@ sub stock_out_form {
 
   if (!$form->{delivered}) {
     foreach my $row (@contents) {
-      $row->{available_qty} = $form->format_amount_units('amount'      => $row->{qty} * 1,
-                                                         'part_unit'   => $part_info->{unit},
-                                                         'conv_units'  => 'convertible_not_smaller',
-                                                         'max_places'  => 2);
+      $row->{available_qty} = $form->format_amount(\%::myconfig, $row->{qty} * 1) . ' ' . $part_info->{unit};
 
       foreach my $sinfo (@{ $stock_info }) {
         next if (($row->{bin_id}       != $sinfo->{bin_id}) ||
@@ -1316,7 +1598,7 @@ sub set_stock_out {
   my @errors     = DO->check_stock_availability('requests' => $stock_info,
                                                 'parts_id' => $form->{parts_id});
 
-  $form->{stock} = YAML::Dump($stock_info);
+  $form->{stock} = SL::YAML::Dump($stock_info);
 
   if (@errors) {
     $form->{ERRORS} = [];
@@ -1346,7 +1628,7 @@ sub transfer_in {
   my $locale   = $main::locale;
 
   if ($form->{id} && DO->is_marked_as_delivered(id => $form->{id})) {
-    $form->show_generic_error($locale->text('The parts for this delivery order have already been transferred in.'), 'back_button' => 1);
+    $form->show_generic_error($locale->text('The parts for this delivery order have already been transferred in.'));
   }
 
   save(no_redirect => 1);
@@ -1393,7 +1675,7 @@ sub transfer_in {
       update();
       $main::lxdebug->leave_sub();
 
-      ::end_of_request();
+      $::dispatcher->end_request;
     }
   }
 
@@ -1402,6 +1684,7 @@ sub transfer_in {
 
   SL::DB::DeliveryOrder->new(id => $form->{id})->load->update_attributes(delivered => 1);
 
+  flash_later('info', $locale->text("Transfer successful"));
   $form->{callback} = 'do.pl?action=edit&type=purchase_delivery_order&id=' . $form->escape($form->{id});
   $form->redirect;
 
@@ -1416,7 +1699,7 @@ sub transfer_out {
   my $locale   = $main::locale;
 
   if ($form->{id} && DO->is_marked_as_delivered(id => $form->{id})) {
-    $form->show_generic_error($locale->text('The parts for this delivery order have already been transferred out.'), 'back_button' => 1);
+    $form->show_generic_error($locale->text('The parts for this delivery order have already been transferred out.'));
   }
 
   save(no_redirect => 1);
@@ -1489,18 +1772,14 @@ sub transfer_out {
                                                      $binfo->{bin_description},
                                                      $request->{chargenumber} ? $locale->text('chargenumber #1', $request->{chargenumber}) : $locale->text('no chargenumber'),
                                                      $request->{bestbefore} ? $locale->text('bestbefore #1', $request->{bestbefore}) : $locale->text('no bestbefore'),
-                                                     $form->format_amount_units('amount'      => $request->{sum_base_qty},
-                                                                                'part_unit'   => $pinfo->{unit},
-                                                                                'conv_units'  => 'convertible_not_smaller'));
+                                                     $form->format_amount(\%::myconfig, $request->{sum_base_qty}) . ' ' . $pinfo->{unit});
         } else {
             push @{ $form->{ERRORS} }, $locale->text("There is not enough available of '#1' at warehouse '#2', bin '#3', #4, for the transfer of #5.",
                                                      $pinfo->{description},
                                                      $binfo->{warehouse_description},
                                                      $binfo->{bin_description},
                                                      $request->{chargenumber} ? $locale->text('chargenumber #1', $request->{chargenumber}) : $locale->text('no chargenumber'),
-                                                     $form->format_amount_units('amount'      => $request->{sum_base_qty},
-                                                                                'part_unit'   => $pinfo->{unit},
-                                                                                'conv_units'  => 'convertible_not_smaller'));
+                                                     $form->format_amount(\%::myconfig, $request->{sum_base_qty}) . ' ' . $pinfo->{unit});
         }
       }
     }
@@ -1512,7 +1791,7 @@ sub transfer_out {
       update();
       $main::lxdebug->leave_sub();
 
-      ::end_of_request();
+      $::dispatcher->end_request;
     }
   }
   DO->transfer_in_out('direction' => 'out',
@@ -1520,6 +1799,7 @@ sub transfer_out {
 
   SL::DB::DeliveryOrder->new(id => $form->{id})->load->update_attributes(delivered => 1);
 
+  flash_later('info', $locale->text("Transfer successful"));
   $form->{callback} = 'do.pl?action=edit&type=sales_delivery_order&id=' . $form->escape($form->{id});
   $form->redirect;
 
@@ -1540,6 +1820,27 @@ sub mark_closed {
   $main::lxdebug->leave_sub();
 }
 
+sub display_form {
+  $::lxdebug->enter_sub;
+
+  check_do_access();
+
+  relink_accounts();
+  retrieve_partunits();
+
+  my $new_rowcount = $::form->{"rowcount"} * 1 + 1;
+  $::form->{"project_id_${new_rowcount}"} = $::form->{"globalproject_id"};
+
+  $::form->language_payment(\%::myconfig);
+
+  Common::webdav_folder($::form);
+
+  form_header();
+  display_row(++$::form->{rowcount});
+  form_footer();
+
+  $::lxdebug->leave_sub;
+}
 
 sub yes {
   call_sub($main::form->{yes_nextsub});
@@ -1557,7 +1858,7 @@ sub dispatcher {
   my $form     = $main::form;
   my $locale   = $main::locale;
 
-  foreach my $action (qw(update ship_to print e_mail save transfer_out transfer_out_default sort
+  foreach my $action (qw(update print save transfer_out transfer_out_default sort
                          transfer_in transfer_in_default mark_closed save_as_new invoice delete)) {
     if ($form->{"action_${action}"}) {
       call_sub($action);
@@ -1621,12 +1922,12 @@ sub transfer_in_out_default {
       my $base_unit_factor = $units->{ $part_info_map{$form->{"id_$i"}}->{unit} }->{factor} || 1;
       my $qty =   $form->parse_amount(\%myconfig, $form->{"qty_$i"}) * $units->{$form->{"unit_$i"}}->{factor} / $base_unit_factor;
 
-      $form->show_generic_error($locale->text("Cannot transfer negative entries." ), 'back_button' => 1) if ($qty < 0);
+      $form->show_generic_error($locale->text("Cannot transfer negative entries." )) if ($qty < 0);
       # if we do not want to transfer services and this part is a service, set qty to zero
       # ... and do not create a hash entry in %qty_parts below (will skip check for bins for the transfer == out case)
       # ... and push only a empty (undef) element to @all_requests (will skip check for bin_id and warehouse_id and will not alter the row)
 
-      $qty = 0 if (!$::instance_conf->get_transfer_default_services && !defined($part_info_map{$form->{"id_$i"}}->{inventory_accno_id}) && !$part_info_map{$form->{"id_$i"}}->{assembly});
+      $qty = 0 if (!$::instance_conf->get_transfer_default_services && $part_info_map{$form->{"id_$i"}}->{part_type} eq 'service');
       $qty_parts{$form->{"id_$i"}} += $qty;
       if ($qty == 0) {
         delete $qty_parts{$form->{"id_$i"}} unless $qty_parts{$form->{"id_$i"}};
@@ -1708,7 +2009,7 @@ sub transfer_in_out_default {
         }
       } else {
         #$main::lxdebug->message(0, 'Fehlertext: ' . $fehlertext);
-        $form->show_generic_error($locale->text("Cannot transfer. <br> Reason:<br>#1", $fehlertext ), 'back_button' => 1);
+        $form->show_generic_error($locale->text("Cannot transfer. <br> Reason:<br>#1", $fehlertext ));
       }
     }
   }
@@ -1720,15 +2021,25 @@ sub transfer_in_out_default {
   # dieser array_ref ist für DO->save da:
   # einmal die all_requests in YAML verwandeln, damit delivery_order_items_stock
   # gefüllt werden kann.
+  # could be dumped to the form in the first loop,
+  # but maybe bin_id and warehouse_id has changed to the "korrekturlager" with
+  # allowed negative qty ($::instance_conf->get_warehouse_id_ignore_onhand) ...
   my $i = 0;
   foreach (@all_requests){
     $i++;
     next unless scalar(%{ $_ });
-    $form->{"stock_${prefix}_$i"} = YAML::Dump([$_]);
+    $form->{"stock_${prefix}_$i"} = SL::YAML::Dump([$_]);
   }
 
   save(no_redirect => 1); # Wir können auslagern, deshalb beleg speichern
                           # und in delivery_order_items_stock speichern
+
+  # ... and fill back the persistent dois_id for inventory fk
+  undef (@all_requests);
+  foreach my $i (1 .. $form->{rowcount}) {
+    next unless ($form->{"id_$i"} && $form->{"stock_${prefix}_$i"});
+    push @all_requests, @{ DO->unpack_stock_information('packed' => $form->{"stock_${prefix}_$i"}) };
+  }
   DO->transfer_in_out('direction' => $prefix,
                       'requests'  => \@all_requests);
 
@@ -1748,11 +2059,16 @@ sub sort {
   my $form     = $main::form;
   my %temp_hash;
 
-  croak ("Delivery Order needs to be saved") unless $form->{id};
+  save(no_redirect => 1); # has to be done, at least for newly added positions
 
   # hashify partnumbers, positions. key is delivery_order_items_id
   for my $i (1 .. ($form->{rowcount}) ) {
     $temp_hash{$form->{"delivery_order_items_id_$i"}} = { runningnumber => $form->{"runningnumber_$i"}, partnumber => $form->{"partnumber_$i"} };
+    if ($form->{id} && $form->{"discount_$i"}) {
+      # prepare_order assumes a db value if there is a form->id and multiplies *100
+      # We hope for new controller code (no more format_amount/parse_amount distinction)
+      $form->{"discount_$i"} /=100;
+    }
   }
   # naturally sort partnumbers and get a sorted array of doi_ids
   my @sorted_doi_ids =  sort { Sort::Naturally::ncmp($temp_hash{$a}->{"partnumber"}, $temp_hash{$b}->{"partnumber"}) }  keys %temp_hash;
@@ -1764,6 +2080,12 @@ sub sort {
     $form->{"runningnumber_$temp_hash{$_}->{runningnumber}"} = $new_number;
     $new_number++;
   }
+  # all parse_amounts changes are in form (i.e. , to .) therefore we need
+  # another format_amount to change it back, for the next save ;-(
+  # works great except for row discounts (see above comment)
+  prepare_order();
+
+
     $main::lxdebug->leave_sub();
     save();
 }
@@ -1778,7 +2100,6 @@ __END__
 
 do.pl - Script for all calls to delivery order
 
-
 =head1 FUNCTIONS
 
 =over 2
diff --git a/bin/mozilla/drafts.pl b/bin/mozilla/drafts.pl
deleted file mode 100644 (file)
index 7670c7e..0000000
+++ /dev/null
@@ -1,195 +0,0 @@
-#======================================================================
-# LX-Office ERP
-#
-#======================================================================
-#
-# Saving and loading drafts
-#
-#======================================================================
-
-use YAML;
-
-use SL::Drafts;
-
-require "bin/mozilla/common.pl";
-
-use strict;
-
-sub save_draft {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  if (!$form->{draft_id} && !$form->{draft_description}) {
-    restore_form($form->{SAVED_FORM}, 1) if ($form->{SAVED_FORM});
-    delete $form->{SAVED_FORM};
-
-    $form->{SAVED_FORM}   = save_form(qw(login password));
-    $form->{remove_draft} = 1;
-
-    $form->header();
-    print($form->parse_html_template("drafts/save_new"));
-
-    return $main::lxdebug->leave_sub();
-  }
-
-  my ($draft_id, $draft_description) = ($form->{draft_id}, $form->{draft_description});
-
-  restore_form($form->{SAVED_FORM}, 1);
-  delete $form->{SAVED_FORM};
-
-  Drafts->save(\%myconfig, $form, $draft_id, $draft_description);
-
-  $form->{saved_message} = $locale->text("Draft saved.");
-
-  update();
-
-  $main::lxdebug->leave_sub();
-}
-
-sub remove_draft {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-
-  Drafts->remove(\%myconfig, $form, $form->{draft_id}) if ($form->{draft_id});
-
-  delete @{$form}{qw(draft_id draft_description)};
-
-  $main::lxdebug->leave_sub();
-}
-
-sub load_draft_maybe {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-
-  $main::lxdebug->leave_sub() and return 0 if ($form->{DONT_LOAD_DRAFT});
-
-  my ($draft_nextsub) = @_;
-
-  my @drafts = Drafts->list(\%myconfig, $form);
-
-  $main::lxdebug->leave_sub() and return 0 unless (@drafts);
-
-  $draft_nextsub = "add" unless ($draft_nextsub);
-
-  delete $form->{action};
-  my $saved_form = save_form(qw(login password));
-
-  $form->header();
-  print($form->parse_html_template("drafts/load",
-                                   { "DRAFTS"        => \@drafts,
-                                     "SAVED_FORM"    => $saved_form,
-                                     "draft_nextsub" => $draft_nextsub }));
-
-  $main::lxdebug->leave_sub();
-
-  return 1;
-}
-
-sub dont_load_draft {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-
-  my $draft_nextsub = $form->{draft_nextsub} || "add";
-
-  restore_form($form->{SAVED_FORM}, 1);
-  delete $form->{SAVED_FORM};
-
-  $form->{DONT_LOAD_DRAFT} = 1;
-
-  call_sub($draft_nextsub);
-
-  $main::lxdebug->leave_sub();
-}
-
-sub load_draft {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-
-  # check and store certain form parameters that might have been passed as get, so we can later overwrite the values from the draft
-  # the overwrite happens at the end of this function
-  my @valid_overwrite_vars = qw(remove_draft amount_1 invnumber ordnumber transdate duedate notes datepaid_1 paid_1 callback AP_paid_1 currency);  # reference description
-  my $overwrite_hash;
-  # my @valid_fields;
-  foreach ( @valid_overwrite_vars ) {
-    $overwrite_hash->{$_} = $form->{$_} if exists $form->{$_};  # variant 1
-    # push(@valid_fields, $_) if exists $form->{$_}; # variant 2
-  };
-
-  my ($old_form, $id, $description) = Drafts->load(\%myconfig, $form, $form->{id});
-
-  if ($old_form) {
-    $old_form = YAML::Load($old_form);
-
-    my %dont_save_vars      = map { $_ => 1 } Drafts->dont_save;
-    my @restore_vars        = grep { !$dont_save_vars{$_} } keys %{ $old_form };
-
-    @{$form}{@restore_vars} = @{$old_form}{@restore_vars};
-
-    $form->{draft_id}              = $id;
-    $form->{draft_description}     = $description;
-    $form->{remove_draft}          = 'checked';
-  }
-  # Ich vergesse bei Rechnungsentwürfe das Rechnungsdatum zu ändern. Dadurch entstehen
-  # ungültige Belege. Vielleicht geht es anderen ähnlich jan 19.2.2011
-  $form->{invdate} = $form->current_date(\%myconfig); # Aktuelles Rechnungsdatum  ...
-  $form->{duedate} = $form->current_date(\%myconfig); # Aktuelles Fälligkeitsdatum  ...
-
-  if ( $overwrite_hash ) {
-    foreach ( keys %$overwrite_hash ) {
-      $form->{$_} = $overwrite_hash->{$_};  # variante 1
-    };
-  };
-  # @{$form}{@valid_fields} = @{$overwrite_hash}{@valid_fields};  # variante 2
-
-  update();
-
-  $main::lxdebug->leave_sub();
-}
-
-sub delete_drafts {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-
-  my @ids;
-  foreach (keys %{$form}) {
-    push @ids, $1 if (/^checked_(.*)/ && $form->{$_});
-  }
-  Drafts->remove(\%myconfig, $form, @ids) if (@ids);
-
-  restore_form($form->{SAVED_FORM}, 1);
-  delete $form->{SAVED_FORM};
-
-  add();
-
-  $main::lxdebug->leave_sub();
-}
-
-sub draft_action_dispatcher {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my $locale   = $main::locale;
-
-  if ($form->{draft_action} eq $locale->text("Skip")) {
-    dont_load_draft();
-
-  } elsif ($form->{draft_action} eq $locale->text("Delete drafts")) {
-    delete_drafts();
-  }
-
-  $main::lxdebug->leave_sub();
-}
-
-1;
index 5cd6a07..b987d1f 100644 (file)
@@ -1,6 +1,7 @@
 use POSIX qw(strftime);
 
 use SL::FU;
+use SL::Locale::String qw(t8);
 use SL::ReportGenerator;
 
 require "bin/mozilla/reportgenerator.pl";
@@ -41,6 +42,8 @@ sub add {
   $form->get_employee($form->get_standard_dbh(\%myconfig));
   $form->{created_for_user} = $form->{employee_id};
 
+  $form->{subject} = $form->{trans_subject_1} if $form->{trans_subject_1};
+
   my $link_details;
 
   if (0 < scalar @{ $form->{LINKS} }) {
@@ -97,7 +100,12 @@ sub display_form {
   my %params;
   $params{not_id}     = $form->{id} if ($form->{id});
   $params{trans_id}   = $form->{LINKS}->[0]->{trans_id} if (@{ $form->{LINKS} });
-  $form->{FOLLOW_UPS} = FU->follow_ups(%params);
+
+  $form->{sort}               = 'follow_up_date';
+  $form->{FOLLOW_UPS_DONE}    = FU->follow_ups(%params,     done => 1);
+  $form->{FOLLOW_UPS_PENDING} = FU->follow_ups(%params, not_done => 1);
+
+  setup_fu_display_form_action_bar() unless $::form->{POPUP_MODE};
 
   $form->header(no_layout => $::form->{POPUP_MODE});
   print $form->parse_html_template('fu/add_edit');
@@ -126,7 +134,7 @@ sub save_follow_up {
   if ($form->{POPUP_MODE}) {
     $form->header();
     print $form->parse_html_template('fu/close_window');
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
 
   $form->{SAVED_MESSAGE} = $locale->text('Follow-Up saved.');
@@ -172,7 +180,7 @@ sub finish {
   if ($form->{POPUP_MODE}) {
     $form->header();
     print $form->parse_html_template('fu/close_window');
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
 
   $form->redirect() if ($form->{callback});
@@ -210,7 +218,7 @@ sub delete {
   if ($form->{POPUP_MODE}) {
     $form->header();
     print $form->parse_html_template('fu/close_window');
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
 
   $form->redirect() if ($form->{callback});
@@ -232,6 +240,7 @@ sub search {
 
   $form->{title}    = $locale->text('Follow-Ups');
 
+  setup_fu_search_action_bar();
   $form->header();
   print $form->parse_html_template('fu/search');
 
@@ -347,6 +356,7 @@ sub report {
     $report->add_data($row);
   }
 
+  setup_fu_report_action_bar();
   $report->generate_with_headers();
 
   $main::lxdebug->leave_sub();
@@ -409,6 +419,8 @@ sub edit_access_rights {
 
   $form->{title} = $locale->text('Edit Access Rights for Follow-Ups');
 
+  setup_fu_edit_access_rights_action_bar();
+
   $form->header();
   print $form->parse_html_template('fu/edit_access_rights');
 
@@ -475,4 +487,77 @@ sub dispatcher {
   $form->error($locale->text('No action defined.'));
 }
 
+sub setup_fu_search_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Show'),
+        submit    => [ '#form', { action => "report" } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_fu_display_form_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => "save" } ],
+        accesskey => 'enter',
+      ],
+      action => [
+        t8('Finish'),
+        submit   => [ '#form', { action => "finish" } ],
+        disabled => !$::form->{id} ? t8('The object has not been saved yet.') : undef,
+      ],
+      action => [
+        t8('Delete'),
+        submit   => [ '#form', { action => "delete" } ],
+        disabled => !$::form->{id} ? t8('The object has not been saved yet.') : undef,
+        confirm  => t8('Do you really want to delete this object?'),
+      ],
+    );
+  }
+}
+
+sub setup_fu_report_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Finish'),
+        submit => [ '#form', { action => "finish" } ],
+        checks => [ [ 'kivi.check_if_entries_selected', '[name^=selected_]' ] ],
+      ],
+      action => [
+        t8('Delete'),
+        submit  => [ '#form', { action => "delete" } ],
+        checks  => [ [ 'kivi.check_if_entries_selected', '[name^=selected_]' ] ],
+        confirm => t8('Do you really want to delete the selected objects?'),
+      ],
+    );
+  }
+}
+
+sub setup_fu_edit_access_rights_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => "save_access_rights" } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
 1;
index 7b8d4a1..de7d924 100644 (file)
@@ -1,9 +1,30 @@
 use SL::Auth;
 use SL::Form;
 use SL::GenericTranslations;
+use SL::Locale::String qw(t8);
 
 use strict;
 
+# convention:
+# preset_text_$formname will generate a input textarea
+# and will be preset in $form email dialog if the form name matches
+
+my %mail_strings = (
+  salutation_male                             => t8('Salutation male'),
+  salutation_female                           => t8('Salutation female'),
+  salutation_general                          => t8('Salutation general'),
+  salutation_punctuation_mark                 => t8('Salutation punctuation mark'),
+  preset_text_sales_quotation                 => t8('Preset email text for sales quotations'),
+  preset_text_sales_order                     => t8('Preset email text for sales orders'),
+  preset_text_sales_delivery_order            => t8('Preset email text for sales delivery orders'),
+  preset_text_invoice                         => t8('Preset email text for sales invoices'),
+  preset_text_invoice_direct_debit            => t8('Preset email text for sales invoices with direct debit'),
+  preset_text_request_quotation               => t8('Preset email text for requests (rfq)'),
+  preset_text_purchase_order                  => t8('Preset email text for purchase orders'),
+  preset_text_periodic_invoices_email_body    => t8('Preset email body for periodic invoices'),
+  preset_text_periodic_invoices_email_subject => t8('Preset email subject for periodic invoices'),
+);
+
 sub edit_greetings {
   $main::lxdebug->enter_sub();
 
@@ -34,6 +55,8 @@ sub edit_greetings {
     }
   }
 
+  setup_generictranslations_edit_greetings_action_bar();
+
   $form->{title} = $locale->text('Edit greetings');
   $form->header();
   print $form->parse_html_template('generictranslations/edit_greetings');
@@ -84,12 +107,18 @@ sub edit_sepa_strings {
   my $translation_list = GenericTranslations->list(translation_type => 'sepa_remittance_info_pfx');
   my %translations     = map { ( ($_->{language_id} || 'default') => $_->{translation} ) } @{ $translation_list };
 
+  my $translation_list_vc = GenericTranslations->list(translation_type => 'sepa_remittance_vc_no_pfx');
+  my %translations_vc     =  map { ( ($_->{language_id} || 'default') => $_->{translation} ) } @{ $translation_list_vc };
+
   unshift @{ $form->{LANGUAGES} }, { 'id' => 'default', };
 
   foreach my $language (@{ $form->{LANGUAGES} }) {
-    $language->{translation} = $translations{$language->{id}};
+    $language->{translation}    = $translations{$language->{id}};
+    $language->{translation_vc} = $translations_vc{$language->{id}};
   }
 
+  setup_generictranslations_edit_sepa_strings_action_bar();
+
   $form->{title} = $locale->text('Edit SEPA strings');
   $form->header();
   print $form->parse_html_template('generictranslations/edit_sepa_strings');
@@ -114,6 +143,10 @@ sub save_sepa_strings {
                               'translation_id'   => undef,
                               'language_id'      => $language->{id},
                               'translation'      => $form->{"translation__" . ($language->{id} || 'default')},);
+    GenericTranslations->save('translation_type' => 'sepa_remittance_vc_no_pfx',
+                              'translation_id'   => undef,
+                              'language_id'      => $language->{id},
+                              'translation'      => $form->{"translation__" . ($language->{id} || 'default') . "__vc" },);
   }
 
   $form->{message} = $locale->text('The SEPA strings have been saved.');
@@ -122,5 +155,159 @@ sub save_sepa_strings {
 
   $main::lxdebug->leave_sub();
 }
+sub edit_email_strings {
+  $main::lxdebug->enter_sub();
+
+  $main::auth->assert('config');
+
+  my $form     = $main::form;
+  my $locale   = $main::locale;
+
+  $form->get_lists('languages' => 'LANGUAGES');
+  unshift @{ $form->{LANGUAGES} }, { 'id' => 'default', };
+
+  my (%translations, $translation_list);
+  foreach (keys %mail_strings)  {
+    $translation_list = GenericTranslations->list(translation_type => $_);
+    %translations     = map { ( ($_->{language_id} || 'default') => $_->{translation} ) } @{ $translation_list };
+
+    foreach my $language (@{ $form->{LANGUAGES} }) {
+      $language->{$_} = $translations{$language->{id}};
+    }
+  }
+  setup_generictranslations_edit_email_strings_action_bar();
+
+  $form->{title} = $locale->text('Edit preset email strings');
+  $::request->{layout}->use_javascript(map { "${_}.js" } qw(ckeditor/ckeditor ckeditor/adapters/jquery));
+  $form->header();
+  print $form->parse_html_template('generictranslations/edit_email_strings',{ 'MAIL_STRINGS' => \%mail_strings });
+
+  $main::lxdebug->leave_sub();
+}
+
+sub save_email_strings {
+  $main::lxdebug->enter_sub();
+
+  $main::auth->assert('config');
+
+  my $form     = $main::form;
+  my $locale   = $main::locale;
+
+  $form->get_lists('languages' => 'LANGUAGES');
+
+  unshift @{ $form->{LANGUAGES} }, { };
+  foreach my $language (@{ $form->{LANGUAGES} }) {
+    foreach (keys %mail_strings)  {
+      GenericTranslations->save('translation_type' => $_,
+                                'translation_id'   => undef,
+                                'language_id'      => $language->{id},
+                                'translation'      => $form->{"translation__" . ($language->{id} || 'default') . "__" . $_},
+                               );
+    }
+  }
+  $form->{message} = $locale->text('The Mail strings have been saved.');
+
+  edit_email_strings();
+
+  $main::lxdebug->leave_sub();
+}
+
+sub edit_zugferd_notes {
+  $::auth->assert('config');
+
+  $::form->get_lists('languages' => 'LANGUAGES');
+
+  my $translation_list = GenericTranslations->list(translation_type => 'ZUGFeRD/notes');
+  my %translations     = map { ( ($_->{language_id} || 'default') => $_->{translation} ) } @{ $translation_list };
+
+  unshift @{ $::form->{LANGUAGES} }, { 'id' => 'default', };
+
+  foreach my $language (@{ $::form->{LANGUAGES} }) {
+    $language->{translation} = $translations{$language->{id}};
+  }
+
+  setup_generictranslations_edit_zugferd_notes_action_bar();
+
+  $::form->{title} = $::locale->text('Edit Factur-X/ZUGFeRD notes');
+  $::form->header;
+  print $::form->parse_html_template('generictranslations/edit_zugferd_notes');
+}
+
+sub save_zugferd_notes {
+  $::auth->assert('config');
+
+  $::form->get_lists('languages' => 'LANGUAGES');
+
+  unshift @{ $::form->{LANGUAGES} }, { };
+
+  foreach my $language (@{ $::form->{LANGUAGES} }) {
+    GenericTranslations->save(
+      translation_type => 'ZUGFeRD/notes',
+      translation_id   => undef,
+      language_id      => $language->{id},
+      translation      => $::form->{"translation__" . ($language->{id} || 'default')},
+    );
+  }
+
+  $::form->{message} = $::locale->text('The Factur-X/ZUGFeRD notes have been saved.');
+
+  edit_zugferd_notes();
+}
+
+sub setup_generictranslations_edit_greetings_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => "save_greetings" } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_generictranslations_edit_sepa_strings_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => "save_sepa_strings" } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_generictranslations_edit_email_strings_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => "save_email_strings" } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_generictranslations_edit_zugferd_notes_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => "save_zugferd_notes" } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
 
 1;
index 026cf6a..faca8ce 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Genereal Ledger
@@ -35,16 +36,25 @@ use utf8;
 use strict;
 
 use POSIX qw(strftime);
-use List::Util qw(sum);
+use List::Util qw(first sum);
 
+use SL::DB::ApGl;
+use SL::DB::RecordTemplate;
+use SL::DB::ReconciliationLink;
+use SL::DB::BankTransactionAccTrans;
+use SL::DB::Tax;
 use SL::FU;
 use SL::GL;
+use SL::Helper::Flash qw(flash flash_later);
 use SL::IS;
-use SL::PE;
 use SL::ReportGenerator;
-
+use SL::DBUtils qw(selectrow_query selectall_hashref_query);
+use SL::Webdav;
+use SL::Locale::String qw(t8);
+use SL::Helper::GlAttachments qw(count_gl_attachments);
+use SL::Presenter::Tag;
+use SL::Presenter::Chart;
 require "bin/mozilla/common.pl";
-require "bin/mozilla/drafts.pl";
 require "bin/mozilla/reportgenerator.pl";
 
 # this is for our long dates
@@ -75,16 +85,143 @@ require "bin/mozilla/reportgenerator.pl";
 # $locale->text('Nov')
 # $locale->text('Dec')
 
+sub load_record_template {
+  $::auth->assert('gl_transactions');
+
+  # Load existing template and verify that its one for this module.
+  my $template = SL::DB::RecordTemplate
+    ->new(id => $::form->{id})
+    ->load(
+      with_object => [ qw(customer payment currency record_items record_items.chart) ],
+    );
+
+  die "invalid template type" unless $template->template_type eq 'gl_transaction';
+
+  $template->substitute_variables;
+  my $payment_suggestion =  $::form->{form_defaults}->{amount_1};
+
+  # Clean the current $::form before rebuilding it from the template.
+  my $form_defaults = delete $::form->{form_defaults};
+  delete @{ $::form }{ grep { !m{^(?:script|login)$}i } keys %{ $::form } };
+
+  my $dummy_form = {};
+  GL->transaction(\%::myconfig, $dummy_form);
+
+  # Fill $::form from the template.
+  my $today                   = DateTime->today_local;
+  $::form->{title}            = "Add";
+  $::form->{transdate}        = $today->to_kivitendo;
+  $::form->{duedate}          = $today->to_kivitendo;
+  $::form->{rowcount}         = @{ $template->items };
+  $::form->{paidaccounts}     = 1;
+  $::form->{$_}               = $template->$_     for qw(department_id taxincluded ob_transaction cb_transaction reference description show_details transaction_description);
+  $::form->{$_}               = $dummy_form->{$_} for qw(closedto revtrans previous_id previous_gldate);
+
+  my $row = 0;
+  foreach my $item (@{ $template->items }) {
+    $row++;
+
+    my $active_taxkey = $item->chart->get_active_taxkey;
+    my $taxes         = SL::DB::Manager::Tax->get_all(
+      where   => [ chart_categories => { like => '%' . $item->chart->category . '%' }],
+      sort_by => 'taxkey, rate',
+    );
+
+    my $tax   = first { $item->tax_id          == $_->id } @{ $taxes };
+    $tax    //= first { $active_taxkey->tax_id == $_->id } @{ $taxes };
+    $tax    //= $taxes->[0];
+
+    if (!$tax) {
+      $row--;
+      next;
+    }
+
+    $::form->{"accno_id_${row}"}          = $item->chart_id;
+    $::form->{"previous_accno_id_${row}"} = $item->chart_id;
+    $::form->{"debit_${row}"}             = $::form->format_amount(\%::myconfig, ($payment_suggestion ? $payment_suggestion : $item->amount1), 2) if $item->amount1 * 1;
+    $::form->{"credit_${row}"}            = $::form->format_amount(\%::myconfig, ($payment_suggestion ? $payment_suggestion : $item->amount2), 2) if $item->amount2 * 1;
+    $::form->{"taxchart_${row}"}          = $item->tax_id . '--' . $tax->rate;
+    $::form->{"${_}_${row}"}              = $item->$_ for qw(source memo project_id);
+  }
+
+  $::form->{$_} = $form_defaults->{$_} for keys %{ $form_defaults // {} };
+
+  flash('info', $::locale->text("The record template '#1' has been loaded.", $template->template_name));
+
+  update(
+    keep_rows_without_amount => 1,
+    dont_add_new_row         => 1,
+  );
+}
+
+sub save_record_template {
+  $::auth->assert('gl_transactions');
+
+  my $template = $::form->{record_template_id} ? SL::DB::RecordTemplate->new(id => $::form->{record_template_id})->load : SL::DB::RecordTemplate->new;
+  my $js       = SL::ClientJS->new(controller => SL::Controller::Base->new);
+  my $new_name = $template->template_name_to_use($::form->{record_template_new_template_name});
+  $js->dialog->close('#record_template_dialog');
+
+
+  # bank transactions need amounts for assignment
+  my $can_save = 0;
+  $can_save    = 1 if ($::form->{credit_1} > 0 && $::form->{debit_2} > 0 && $::form->{credit_2} == 0 && $::form->{debit_1} == 0);
+  $can_save    = 1 if ($::form->{credit_2} > 0 && $::form->{debit_1} > 0 && $::form->{credit_1} == 0 && $::form->{debit_2} == 0);
+  return $js->flash('error', t8('Can only save template if amounts,i.e. 1 for debit and credit are set.'))->render unless $can_save;
+
+  my @items = grep {
+    $_->{chart_id} && (($_->{tax_id} // '') ne '')
+  } map {
+    +{ chart_id   => $::form->{"accno_id_${_}"},
+       amount1    => $::form->parse_amount(\%::myconfig, $::form->{"debit_${_}"}),
+       amount2    => $::form->parse_amount(\%::myconfig, $::form->{"credit_${_}"}),
+       tax_id     => (split m{--}, $::form->{"taxchart_${_}"})[0],
+       project_id => $::form->{"project_id_${_}"} || undef,
+       source     => $::form->{"source_${_}"},
+       memo       => $::form->{"memo_${_}"},
+     }
+  } (1..($::form->{rowcount} || 1));
+
+  $template->assign_attributes(
+    template_type  => 'gl_transaction',
+    template_name  => $new_name,
+
+    currency_id             => $::instance_conf->get_currency_id,
+    department_id           => $::form->{department_id}    || undef,
+    project_id              => $::form->{globalproject_id} || undef,
+    taxincluded             => $::form->{taxincluded}     ? 1 : 0,
+    ob_transaction          => $::form->{ob_transaction}  ? 1 : 0,
+    cb_transaction          => $::form->{cb_transaction}  ? 1 : 0,
+    reference               => $::form->{reference},
+    description             => $::form->{description},
+    show_details            => $::form->{show_details},
+    transaction_description => $::form->{transaction_description},
+
+    items          => \@items,
+  );
+
+  eval {
+    $template->save;
+    1;
+  } or do {
+    return $js
+      ->flash('error', $::locale->text("Saving the record template '#1' failed.", $new_name))
+      ->render;
+  };
+
+  return $js
+    ->flash('info', $::locale->text("The record template '#1' has been saved.", $new_name))
+    ->render;
+}
+
 sub add {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('gl_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  return $main::lxdebug->leave_sub() if (load_draft_maybe());
-
   $form->{title} = "Add";
 
   $form->{callback} = "gl.pl?action=add" unless $form->{callback};
@@ -99,16 +236,7 @@ sub add {
   $form->{credit} = 0;
   $form->{tax}    = 0;
 
-  # departments
-  $form->all_departments(\%myconfig);
-  if (@{ $form->{all_departments} || [] }) {
-    $form->{selectdepartment} = "<option>\n";
-
-    map {
-      $form->{selectdepartment} .=
-        "<option>$_->{description}--$_->{id}\n"
-    } (@{ $form->{all_departments} || [] });
-  }
+  $::form->{ALL_DEPARTMENTS} = SL::DB::Manager::Department->get_all_sorted;
 
   $form->{show_details} = $myconfig{show_form_details} unless defined $form->{show_details};
 
@@ -120,7 +248,7 @@ sub add {
 sub prepare_transaction {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('gl_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -129,16 +257,7 @@ sub prepare_transaction {
 
   $form->{amount} = $form->format_amount(\%myconfig, $form->{amount}, 2);
 
-  # departments
-  $form->all_departments(\%myconfig);
-  if (@{ $form->{all_departments} || [] }) {
-    $form->{selectdepartment} = "<option>\n";
-
-    map {
-      $form->{selectdepartment} .=
-        "<option>$_->{description}--$_->{id}\n"
-    } (@{ $form->{all_departments} || [] });
-  }
+  $::form->{ALL_DEPARTMENTS} = SL::DB::Manager::Department->get_all_sorted;
 
   my $i        = 1;
   my $tax      = 0;
@@ -158,7 +277,7 @@ sub prepare_transaction {
       $form->{"project_id_$j"} = $ref->{project_id};
 
     } else {
-      $form->{"accno_$i"} = "$ref->{accno}--$ref->{tax_id}";
+      $form->{"accno_id_$i"} = $ref->{chart_id};
       for (qw(fx_transaction source memo)) { $form->{"${_}_$i"} = $ref->{$_} }
       if ($ref->{amount} < 0) {
         $form->{totaldebit} -= $ref->{amount};
@@ -167,7 +286,7 @@ sub prepare_transaction {
         $form->{totalcredit} += $ref->{amount};
         $form->{"credit_$i"} = $ref->{amount};
       }
-      $form->{"taxchart_$i"} = "0--0.00";
+      $form->{"taxchart_$i"} = $ref->{id}."--0.00000";
       $form->{"project_id_$i"} = $ref->{project_id};
       $i++;
     }
@@ -191,7 +310,7 @@ sub prepare_transaction {
 sub edit {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('gl_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -202,6 +321,17 @@ sub edit {
 
   $form->{show_details} = $myconfig{show_form_details} unless defined $form->{show_details};
 
+  if ($form->{id} && $::instance_conf->get_webdav) {
+    my $webdav = SL::Webdav->new(
+      type     => 'general_ledger',
+      number   => $form->{id},
+    );
+    my @all_objects = $webdav->get_all_objects;
+    @{ $form->{WEBDAV} } = map { { name => $_->filename,
+                                   type => t8('File'),
+                                   link => File::Spec->catfile($_->full_filedescriptor),
+                               } } @all_objects;
+  }
   form_header();
   display_rows();
   form_footer();
@@ -212,17 +342,18 @@ sub edit {
 
 sub search {
   $::lxdebug->enter_sub;
-  $::auth->assert('general_ledger');
+  $::auth->assert('general_ledger | gl_transactions');
 
-  $::form->all_departments(\%::myconfig);
   $::form->get_lists(
     projects  => { key => "ALL_PROJECTS", all => 1 },
   );
   $::form->{ALL_EMPLOYEES} = SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]);
+  $::form->{ALL_DEPARTMENTS} = SL::DB::Manager::Department->get_all_sorted;
+
+  setup_gl_search_action_bar();
 
   $::form->header;
   print $::form->parse_html_template('gl/search', {
-    department_label => sub { ("$_[0]{description}--$_[0]{id}")x2 },
     employee_label => sub { "$_[0]{id}--$_[0]{name}" },
   });
 
@@ -251,7 +382,7 @@ sub create_subtotal_row {
 sub generate_report {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('general_ledger | gl_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -297,37 +428,39 @@ sub generate_report {
   my $ml = ($form->{ml} =~ /(A|E|Q)/) ? -1 : 1;
 
   my @columns = qw(
-    gldate         transdate        id             reference      description
-    notes          source           debit          debit_accno
-    credit         credit_accno     debit_tax      debit_tax_accno
-    credit_tax     credit_tax_accno projectnumbers balance employee
+    transdate     gldate              id                         reference
+    description   notes               transaction_description    source
+    doccnt        debit               debit_accno
+    credit        credit_accno        debit_tax                  debit_tax_accno
+    credit_tax    credit_tax_accno    balance                    projectnumbers
+    department    employee
   );
 
   # add employee here, so that variable is still known and passed in url when choosing a different sort order in resulting table
-  my @hidden_variables = qw(accno source reference department description notes project_id datefrom dateto employee_id datesort category l_subtotal);
+  my @hidden_variables = qw(accno source reference description notes project_id datefrom dateto employee_id datesort category l_subtotal department_id transaction_description);
   push @hidden_variables, map { "l_${_}" } @columns;
 
   my $employee = $form->{employee_id} ? SL::DB::Employee->new(id => $form->{employee_id})->load->name : '';
 
   my (@options, @date_options);
-  push @options,      $locale->text('Account')     . " : $form->{accno} $form->{account_description}" if ($form->{accno});
-  push @options,      $locale->text('Source')      . " : $form->{source}"                             if ($form->{source});
-  push @options,      $locale->text('Reference')   . " : $form->{reference}"                          if ($form->{reference});
-  push @options,      $locale->text('Description') . " : $form->{description}"                        if ($form->{description});
-  push @options,      $locale->text('Notes')       . " : $form->{notes}"                              if ($form->{notes});
-  push @options,      $locale->text('Employee')    . " : $employee"                                   if $employee;
-  my $datesorttext = $form->{datesort} eq 'transdate' ? $locale->text('Invoice Date') :  $locale->text('Booking Date');
+  push @options,      $locale->text('Account')                 . " : $form->{accno} $form->{account_description}" if ($form->{accno});
+  push @options,      $locale->text('Source')                  . " : $form->{source}"                             if ($form->{source});
+  push @options,      $locale->text('Reference')               . " : $form->{reference}"                          if ($form->{reference});
+  push @options,      $locale->text('Description')             . " : $form->{description}"                        if ($form->{description});
+  push @options,      $locale->text('Notes')                   . " : $form->{notes}"                              if ($form->{notes});
+  push @options,      $locale->text('Transaction description') . " : $form->{transaction_description}"            if $form->{transaction_description};
+  push @options,      $locale->text('Employee')                . " : $employee"                                   if $employee;
+  my $datesorttext = $form->{datesort} eq 'transdate' ? $locale->text('Transdate') :  $locale->text('Gldate');
   push @date_options,      "$datesorttext"                              if ($form->{datesort} and ($form->{datefrom} or $form->{dateto}));
   push @date_options, $locale->text('From'), $locale->date(\%myconfig, $form->{datefrom}, 1)          if ($form->{datefrom});
   push @date_options, $locale->text('Bis'),  $locale->date(\%myconfig, $form->{dateto},   1)          if ($form->{dateto});
   push @options,      join(' ', @date_options)                                                        if (scalar @date_options);
 
-  if ($form->{department}) {
-    my ($department) = split /--/, $form->{department};
-    push @options, $locale->text('Department') . " : $department";
+  if ($form->{department_id}) {
+    my $department = SL::DB::Manager::Department->find_by( id => $form->{department_id} );
+    push @options, $locale->text('Department') . " : " . $department->description;
   }
 
-
   my $callback = build_std_url('action=generate_report', grep { $form->{$_} } @hidden_variables);
 
   $form->{l_credit_accno}     = 'Y';
@@ -339,29 +472,33 @@ sub generate_report {
   $form->{l_datesort} = 'Y';
   $form->{l_debit_tax_accno}  = 'Y';
   $form->{l_balance}          = $form->{accno} ? 'Y' : '';
+  $form->{l_doccnt}           = $form->{l_source} ? 'Y' : '';
 
   my %column_defs = (
-    'id'               => { 'text' => $locale->text('ID'), },
-    'transdate'        => { 'text' => $locale->text('Invoice Date'), },
-    'gldate'           => { 'text' => $locale->text('Booking Date'), },
-    'reference'        => { 'text' => $locale->text('Reference'), },
-    'source'           => { 'text' => $locale->text('Source'), },
-    'description'      => { 'text' => $locale->text('Description'), },
-    'notes'            => { 'text' => $locale->text('Notes'), },
-    'debit'            => { 'text' => $locale->text('Debit'), },
-    'debit_accno'      => { 'text' => $locale->text('Debit Account'), },
-    'credit'           => { 'text' => $locale->text('Credit'), },
-    'credit_accno'     => { 'text' => $locale->text('Credit Account'), },
-    'debit_tax'        => { 'text' => $locale->text('Debit Tax'), },
-    'debit_tax_accno'  => { 'text' => $locale->text('Debit Tax Account'), },
-    'credit_tax'       => { 'text' => $locale->text('Credit Tax'), },
-    'credit_tax_accno' => { 'text' => $locale->text('Credit Tax Account'), },
-    'balance'          => { 'text' => $locale->text('Balance'), },
-    'projectnumbers'   => { 'text' => $locale->text('Project Numbers'), },
-    'employee'         => { 'text' => $locale->text('Employee'), },
+    'id'                      => { 'text' => $locale->text('ID'), },
+    'transdate'               => { 'text' => $locale->text('Transdate'), },
+    'gldate'                  => { 'text' => $locale->text('Gldate'), },
+    'reference'               => { 'text' => $locale->text('Reference'), },
+    'source'                  => { 'text' => $locale->text('Source'), },
+    'doccnt'                  => { 'text' => $locale->text('Document Count'), },
+    'description'             => { 'text' => $locale->text('Description'), },
+    'notes'                   => { 'text' => $locale->text('Notes'), },
+    'debit'                   => { 'text' => $locale->text('Debit'), },
+    'debit_accno'             => { 'text' => $locale->text('Debit Account'), },
+    'credit'                  => { 'text' => $locale->text('Credit'), },
+    'credit_accno'            => { 'text' => $locale->text('Credit Account'), },
+    'debit_tax'               => { 'text' => $locale->text('Debit Tax'), },
+    'debit_tax_accno'         => { 'text' => $locale->text('Debit Tax Account'), },
+    'credit_tax'              => { 'text' => $locale->text('Credit Tax'), },
+    'credit_tax_accno'        => { 'text' => $locale->text('Credit Tax Account'), },
+    'balance'                 => { 'text' => $locale->text('Balance'), },
+    'projectnumbers'          => { 'text' => $locale->text('Project Numbers'), },
+    'department'              => { 'text' => $locale->text('Department'), },
+    'employee'                => { 'text' => $locale->text('Employee'), },
+    'transaction_description' => { 'text' => $locale->text('Transaction description'), },
   );
 
-  foreach my $name (qw(id transdate gldate reference description debit_accno credit_accno debit_tax_accno credit_tax_accno)) {
+  foreach my $name (qw(id transdate gldate reference description debit_accno credit_accno debit_tax_accno credit_tax_accno department transaction_description)) {
     my $sortname                = $name =~ m/accno/ ? 'accno' : $name;
     my $sortdir                 = $sortname eq $form->{sort} ? 1 - $form->{sortdir} : $form->{sortdir};
     $column_defs{$name}->{link} = $callback . "&sort=$sortname&sortdir=$sortdir";
@@ -381,7 +518,8 @@ sub generate_report {
   $report->set_columns(%column_defs);
   $report->set_column_order(@columns);
 
-  $report->set_export_options('generate_report', @hidden_variables, qw(sort sortdir));
+  $form->{l_attachments} = 'Y';
+  $report->set_export_options('generate_report', @hidden_variables, qw(sort sortdir l_attachments));
 
   $report->set_sort_indicator($form->{sort} eq 'accno' ? 'debit_accno' : $form->{sort}, $form->{sortdir});
 
@@ -430,6 +568,10 @@ sub generate_report {
     my $row = { };
     map { $row->{$_} = { 'data' => '', 'align' => $column_alignment{$_} } } @columns;
 
+    if ( $form->{l_doccnt} ) {
+      $row->{doccnt}->{data} = SL::Helper::GlAttachments->count_gl_pdf_attachments($ref->{id},$ref->{type});
+    }
+
     my $sh = "";
     if ($form->{balance} < 0) {
       $sh = " S";
@@ -444,7 +586,7 @@ sub generate_report {
     $row->{balance}->{data}        = $data;
     $row->{projectnumbers}->{data} = join ", ", sort { lc($a) cmp lc($b) } keys %{ $ref->{projectnumbers} };
 
-    map { $row->{$_}->{data} = $ref->{$_} } qw(id reference description notes gldate employee);
+    map { $row->{$_}->{data} = $ref->{$_} } qw(id reference description notes gldate employee department transaction_description);
 
     map { $row->{$_}->{data} = \@{ $rows{$_} }; } qw(transdate debit credit debit_accno credit_accno debit_tax_accno credit_tax_accno source);
 
@@ -507,15 +649,25 @@ sub generate_report {
 
   $report->set_options('raw_bottom_info_text' => $raw_bottom_info_text);
 
+  setup_gl_transactions_action_bar();
+
   $report->generate_with_headers();
 
   $main::lxdebug->leave_sub();
 }
 
+sub show_draft {
+  $::form->{transdate} = DateTime->today_local->to_kivitendo if !$::form->{transdate};
+  $::form->{gldate}    = $::form->{transdate} if !$::form->{gldate};
+  update();
+}
+
 sub update {
+  my %params = @_;
+
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('gl_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -530,72 +682,73 @@ sub update {
   my $creditcount = 0;
   my ($debitcredit, $amount);
 
+  my $dbh = SL::DB->client->dbh;
+  my ($notax_id) = selectrow_query($form, $dbh, "SELECT id FROM tax WHERE taxkey = 0 LIMIT 1", );
+  my $zerotaxes  = selectall_hashref_query($form, $dbh, "SELECT id FROM tax WHERE rate = 0", );
+
   my @flds =
-    qw(accno debit credit projectnumber fx_transaction source memo tax taxchart);
+    qw(accno_id debit credit projectnumber fx_transaction source memo tax taxchart);
 
   for my $i (1 .. $form->{rowcount}) {
+    $form->{"${_}_$i"} = $form->parse_amount(\%myconfig, $form->{"${_}_$i"}) for qw(debit credit tax);
 
-    unless (($form->{"debit_$i"} eq "") && ($form->{"credit_$i"} eq "")) {
-      for (qw(debit credit tax)) {
-        $form->{"${_}_$i"} =
-          $form->parse_amount(\%myconfig, $form->{"${_}_$i"});
-      }
+    next if !$form->{"debit_$i"} && !$form->{"credit_$i"} && !$params{keep_rows_without_amount};
+
+    push @a, {};
+    $debitcredit = ($form->{"debit_$i"} == 0) ? "0" : "1";
+    if ($debitcredit) {
+      $debitcount++;
+    } else {
+      $creditcount++;
+    }
 
-      push @a, {};
-      $debitcredit = ($form->{"debit_$i"} == 0) ? "0" : "1";
+    if (($debitcount >= 2) && ($creditcount == 2)) {
+      $form->{"credit_$i"} = 0;
+      $form->{"tax_$i"}    = 0;
+      $creditcount--;
+      $form->{creditlock} = 1;
+    }
+    if (($creditcount >= 2) && ($debitcount == 2)) {
+      $form->{"debit_$i"} = 0;
+      $form->{"tax_$i"}   = 0;
+      $debitcount--;
+      $form->{debitlock} = 1;
+    }
+    if (($creditcount == 1) && ($debitcount == 2)) {
+      $form->{creditlock} = 1;
+    }
+    if (($creditcount == 2) && ($debitcount == 1)) {
+      $form->{debitlock} = 1;
+    }
+    if ($debitcredit && $credittax) {
+      $form->{"taxchart_$i"} = "$notax_id--0.00000";
+    }
+    if (!$debitcredit && $debittax) {
+      $form->{"taxchart_$i"} = "$notax_id--0.00000";
+    }
+    $amount =
+      ($form->{"debit_$i"} == 0)
+      ? $form->{"credit_$i"}
+      : $form->{"debit_$i"};
+    my $j = $#a;
+    if (($debitcredit && $credittax) || (!$debitcredit && $debittax)) {
+      $form->{"taxchart_$i"} = "$notax_id--0.00000";
+      $form->{"tax_$i"}      = 0;
+    }
+    my ($taxkey, $rate) = split(/--/, $form->{"taxchart_$i"});
+    my $iswithouttax = grep { $_->{id} == $taxkey } @{ $zerotaxes };
+    if (!$iswithouttax) {
       if ($debitcredit) {
-        $debitcount++;
+        $debittax = 1;
       } else {
-        $creditcount++;
+        $credittax = 1;
       }
+    };
+    my ($tmpnetamount,$tmpdiff);
+    ($tmpnetamount,$form->{"tax_$i"},$tmpdiff) = $form->calculate_tax($amount,$rate,$form->{taxincluded} *= 1,2);
 
-      if (($debitcount >= 2) && ($creditcount == 2)) {
-        $form->{"credit_$i"} = 0;
-        $form->{"tax_$i"}    = 0;
-        $creditcount--;
-        $form->{creditlock} = 1;
-      }
-      if (($creditcount >= 2) && ($debitcount == 2)) {
-        $form->{"debit_$i"} = 0;
-        $form->{"tax_$i"}   = 0;
-        $debitcount--;
-        $form->{debitlock} = 1;
-      }
-      if (($creditcount == 1) && ($debitcount == 2)) {
-        $form->{creditlock} = 1;
-      }
-      if (($creditcount == 2) && ($debitcount == 1)) {
-        $form->{debitlock} = 1;
-      }
-      if ($debitcredit && $credittax) {
-        $form->{"taxchart_$i"} = "0--0.00";
-      }
-      if (!$debitcredit && $debittax) {
-        $form->{"taxchart_$i"} = "0--0.00";
-      }
-      $amount =
-        ($form->{"debit_$i"} == 0)
-        ? $form->{"credit_$i"}
-        : $form->{"debit_$i"};
-      my $j = $#a;
-      if (($debitcredit && $credittax) || (!$debitcredit && $debittax)) {
-        $form->{"taxchart_$i"} = "0--0.00";
-        $form->{"tax_$i"}      = 0;
-      }
-      my ($taxkey, $rate) = split(/--/, $form->{"taxchart_$i"});
-      if ($taxkey > 1) {
-        if ($debitcredit) {
-          $debittax = 1;
-        } else {
-          $credittax = 1;
-        }
-      };
-      my ($tmpnetamount,$tmpdiff);
-      ($tmpnetamount,$form->{"tax_$i"},$tmpdiff) = $form->calculate_tax($amount,$rate,$form->{taxincluded} *= 1,2);
-
-      for (@flds) { $a[$j]->{$_} = $form->{"${_}_$i"} }
-      $count++;
-    }
+    for (@flds) { $a[$j]->{$_} = $form->{"${_}_$i"} }
+    $count++;
   }
 
   for my $i (1 .. $count) {
@@ -607,9 +760,9 @@ sub update {
     for (@flds) { delete $form->{"${_}_$i"} }
   }
 
-  $form->{rowcount} = $count + 1;
+  $form->{rowcount} = $count + ($params{dont_add_new_row} ? 0 : 1);
 
-  &display_form;
+  display_form();
   $main::lxdebug->leave_sub();
 
 }
@@ -618,7 +771,7 @@ sub display_form {
   my ($init) = @_;
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('gl_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -641,37 +794,22 @@ sub display_rows {
   my ($init) = @_;
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('gl_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $cgi      = $::request->{cgi};
 
+  my %balances = GL->get_chart_balances(map { $_->{id} } @{ $form->{ALL_CHARTS} });
+
   $form->{debit_1}     = 0 if !$form->{"debit_1"};
   $form->{totaldebit}  = 0;
   $form->{totalcredit} = 0;
 
-  my %project_labels = ();
-  my @project_values = ("");
-  foreach my $item (@{ $form->{"ALL_PROJECTS"} }) {
-    push(@project_values, $item->{"id"});
-    $project_labels{$item->{"id"}} = $item->{"projectnumber"};
-  }
-
-  my %chart_labels = ();
-  my @chart_values = ();
-  my %charts = ();
-  my $taxchart_init;
-  foreach my $item (@{ $form->{ALL_CHARTS} }) {
-    if ($item->{charttype} eq 'H'){ #falls ÃŒberschrift
-      next;                         #ÃŒberspringen (Bug 1150)
-    }
-    my $key = $item->{accno} . "--" . $item->{tax_id};
-    $taxchart_init = $item->{tax_id} unless (@chart_values);
-    push(@chart_values, $key);
-    $chart_labels{$key} = $item->{accno} . "--" . $item->{description};
-    $charts{$item->{accno}} = $item;
-  }
+  my %charts_by_id  = map { ($_->{id} => $_) } @{ $::form->{ALL_CHARTS} };
+  my $default_chart = $::form->{ALL_CHARTS}[0];
+  my $transdate     = $::form->{transdate} ? DateTime->from_kivitendo($::form->{transdate}) : DateTime->today_local;
+  my $deliverydate  = $::form->{deliverydate} ? DateTime->from_kivitendo($::form->{deliverydate}) : undef;
 
   my ($source, $memo, $source_hidden, $memo_hidden);
   for my $i (1 .. $form->{rowcount}) {
@@ -687,49 +825,36 @@ sub display_rows {
       <input type="hidden" name="memo_$i" value="$form->{"memo_$i"}" size="16">|;
     }
 
-    my $selected_accno_full;
-    my ($accno_row) = split(/--/, $form->{"accno_$i"});
-    my $item = $charts{$accno_row};
-    $selected_accno_full = "$item->{accno}--$item->{tax_id}";
-
-    my $selected_taxchart = $form->{"taxchart_$i"};
-    my ($selected_accno, $selected_tax_id) = split(/--/, $selected_accno_full);
-    my ($previous_accno, $previous_tax_id) = split(/--/, $form->{"previous_accno_$i"});
-
     my %taxchart_labels = ();
     my @taxchart_values = ();
-    my %taxcharts = ();
-    my $filter_accno;
-    $filter_accno = $::form->{ALL_CHARTS}[0]->{accno};
-    $filter_accno = $selected_accno if (!$init and $i < $form->{rowcount});
-    foreach my $item ( GL->get_tax_dropdown($filter_accno) ) {
-      my $key = $item->{id} . "--" . $item->{rate};
-      $taxchart_init = $key if ($taxchart_init == $item->{id});
-      push(@taxchart_values, $key);
-      $taxchart_labels{$key} = $item->{taxdescription} . " " . $item->{rate} * 100 . ' %';
-      $taxcharts{$item->{id}} = $item;
+
+    my $accno_id = $::form->{"accno_id_$i"};
+    my $chart    = $charts_by_id{$accno_id} // $default_chart;
+    $accno_id    = $chart->{id};
+    my ($first_taxchart, $default_taxchart, $taxchart_to_use);
+
+    my $used_tax_id;
+    if ( $form->{"taxchart_$i"} ) {
+      ($used_tax_id) = split(/--/, $form->{"taxchart_$i"});
     }
 
-    if ($previous_accno &&
-        ($previous_accno eq $selected_accno) &&
-        ($previous_tax_id ne $selected_tax_id)) {
-      my $item = $taxcharts{$selected_tax_id};
-      $selected_taxchart = "$item->{id}--$item->{rate}";
+    my $taxdate = $deliverydate ? $deliverydate : $transdate;
+    foreach my $item ( GL->get_active_taxes_for_chart($accno_id, $taxdate, $used_tax_id) ) {
+      my $key             = $item->id . "--" . $item->rate;
+      $first_taxchart   //= $item;
+      $default_taxchart   = $item if $item->{is_default};
+      $taxchart_to_use    = $item if $key eq $form->{"taxchart_$i"};
+
+      push(@taxchart_values, $key);
+      $taxchart_labels{$key} = $item->taxkey . " - " . $item->taxdescription . " " . $item->rate * 100 . ' %';
     }
 
-    $selected_accno      = '' if ($init);
-    $selected_taxchart ||= $taxchart_init;
+    $taxchart_to_use    //= $default_taxchart // $first_taxchart;
+    my $selected_taxchart = $taxchart_to_use->id . '--' . $taxchart_to_use->rate;
 
     my $accno = qq|<td>| .
-      NTI($cgi->popup_menu('-name' => "accno_$i",
-                           '-id' => "accno_$i",
-                           '-onChange' => "updateTaxes($i);",
-                           '-style' => 'width:200px',
-                           '-values' => \@chart_values,
-                           '-labels' => \%chart_labels,
-                           '-default' => $selected_accno_full))
-      . $cgi->hidden('-name' => "previous_accno_$i",
-                     '-default' => $selected_accno_full)
+      SL::Presenter::Chart::picker("accno_id_$i", $accno_id, style => "width: 300px") .
+      SL::Presenter::Tag::hidden_tag("previous_accno_id_$i", $accno_id)
       . qq|</td>|;
     my $tax_ddbox = qq|<td>| .
       NTI($cgi->popup_menu('-name' => "taxchart_$i",
@@ -796,19 +921,21 @@ sub display_rows {
       }
     }
 
-    my $projectnumber =
-      NTI($cgi->popup_menu('-name' => "project_id_$i",
-                           '-values' => \@project_values,
-                           '-labels' => \%project_labels,
-                           '-default' => $form->{"project_id_$i"} ));
-    my $projectnumber_hidden = qq|
-    <input type="hidden" name="project_id_$i" value="$form->{"project_id_$i"}">|;
+    my $projectnumber = SL::Presenter::Project::picker("project_id_$i", $form->{"project_id_$i"});
+    my $projectnumber_hidden = SL::Presenter::Tag::hidden_tag("project_id_$i", $form->{"project_id_$i"});
 
     my $copy2credit = $i == 1 ? 'onkeyup="copy_debit_to_credit()"' : '';
+    my $balance     = $form->format_amount(\%::myconfig, $balances{$accno_id} // 0, 2, 'DRCR');
+
+    # if we have a bt_chart_id we disallow changing the amount of the bank account
+    if ($form->{bt_chart_id}) {
+      $debitreadonly = $creditreadonly = "readonly" if ($form->{"accno_id_$i"} eq $form->{bt_chart_id});
+      $copy2credit   = '' if $i == 1;   # and disallow copy2credit
+    }
 
     print qq|<tr valign=top>
     $accno
-    <td id="chart_balance_$i" align="right">&nbsp;</td>
+    <td id="chart_balance_$i" align="right">${balance}</td>
     $fx_transaction
     <td><input name="debit_$i" size="8" value="$form->{"debit_$i"}" accesskey=$i $copy2credit $debitreadonly></td>
     <td><input name="credit_$i" size=8 value="$form->{"credit_$i"}" $creditreadonly></td>
@@ -843,21 +970,182 @@ sub _get_radieren {
   return ($::instance_conf->get_gl_changeable == 2) ? ($::form->current_date(\%::myconfig) eq $::form->{gldate}) : ($::instance_conf->get_gl_changeable == 1);
 }
 
+sub setup_gl_action_bar {
+  my %params = @_;
+  my $form   = $::form;
+  my $change_never            = $::instance_conf->get_gl_changeable == 0;
+  my $change_on_same_day_only = $::instance_conf->get_gl_changeable == 2 && ($form->current_date(\%::myconfig) ne $form->{gldate});
+  my ($is_linked_bank_transaction, $is_linked_ap_transaction, $is_reconciled_bank_transaction);
+
+  if ($form->{id} && SL::DB::Manager::BankTransactionAccTrans->find_by(gl_id => $form->{id})) {
+    $is_linked_bank_transaction = 1;
+  }
+  if ($form->{id} && SL::DB::Manager::ApGl->find_by(gl_id => $form->{id})) {
+    $is_linked_ap_transaction = 1;
+  }
+  # dont edit reconcilated bookings!
+  if ($form->{id}) {
+    my @acc_trans = map { $_->acc_trans_id } @{ SL::DB::Manager::AccTransaction->get_all( where => [ trans_id => $form->{id} ] ) };
+    if (scalar @acc_trans && scalar @{ SL::DB::Manager::ReconciliationLink->get_all(where => [ acc_trans_id  => [ @acc_trans ] ]) }) {
+      $is_reconciled_bank_transaction = 1;
+    }
+  }
+  my $create_post_action = sub {
+    # $_[0]: description
+    # $_[1]: after_action
+    action => [
+      $_[0],
+      submit   => [ '#form', { action => 'post', after_action => $_[1] } ],
+      disabled => $form->{locked}                           ? t8('The billing period has already been locked.')
+                : $form->{storno}                           ? t8('A canceled general ledger transaction cannot be posted.')
+                : ($form->{id} && $change_never)            ? t8('Changing general ledger transaction has been disabled in the configuration.')
+                : ($form->{id} && $change_on_same_day_only) ? t8('General ledger transactions can only be changed on the day they are posted.')
+                : $is_linked_bank_transaction               ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                : $is_linked_ap_transaction                 ? t8('This transaction is linked with a AP transaction. Please undo and redo the AP transaction booking if needed.')
+                : $is_reconciled_bank_transaction           ? t8('This transaction is reconciled with a bank transaction. Please undo the reconciliation if needed.')
+                : undef,
+    ],
+  };
+
+  my %post_entry;
+  if ($::instance_conf->get_gl_add_doc && $::instance_conf->get_doc_storage) {
+    %post_entry = (combobox => [ $create_post_action->(t8('Post'), 'doc-tab'),
+                                 $create_post_action->(t8('Post and new booking')) ]);
+  } elsif ($::instance_conf->get_doc_storage) {
+    %post_entry = (combobox => [ $create_post_action->(t8('Post')),
+                                 $create_post_action->(t8('Post and upload document'), 'doc-tab') ]);
+  } else {
+    %post_entry = $create_post_action->(t8('Post'));
+  }
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#form', { action => 'update' } ],
+        id        => 'update_button',
+        accesskey => 'enter',
+      ],
+      %post_entry,
+      combobox => [
+        action => [ t8('Storno'),
+          submit   => [ '#form', { action => 'storno' } ],
+          confirm  => t8('Do you really want to cancel this general ledger transaction?'),
+          disabled => !$form->{id}                ? t8('This general ledger transaction has not been posted yet.')
+                    : $form->{storno}             ? t8('A canceled general ledger transaction cannot be canceled again.')
+                    : $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                    : $is_linked_ap_transaction   ? t8('This transaction is linked with a AP transaction. Please undo and redo the AP transaction booking if needed.')
+                    : $is_reconciled_bank_transaction ? t8('This transaction is reconciled with a bank transaction. Please undo the reconciliation if needed.')
+                    : undef,
+        ],
+        action => [ t8('Delete'),
+          submit   => [ '#form', { action => 'delete' } ],
+          confirm  => t8('Do you really want to delete this object?'),
+          disabled => !$form->{id}             ? t8('This invoice has not been posted yet.')
+                    : $form->{locked}          ? t8('The billing period has already been locked.')
+                    : $change_never            ? t8('Changing invoices has been disabled in the configuration.')
+                    : $change_on_same_day_only ? t8('Invoices can only be changed on the day they are posted.')
+                    : $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                    : $is_linked_ap_transaction   ? t8('This transaction is linked with a AP transaction. Please undo and redo the AP transaction booking if needed.')
+                    : $is_reconciled_bank_transaction ? t8('This transaction is reconciled with a bank transaction. Please undo the reconciliation if needed.')
+                    : $form->{storno}             ? t8('A canceled general ledger transaction cannot be deleted.')
+                    : undef,
+        ],
+      ], # end of combobox "Storno"
+
+      combobox => [
+        action => [ t8('more') ],
+        action => [
+          t8('History'),
+          call     => [ 'set_history_window', $form->{id} * 1, 'glid' ],
+          disabled => !$form->{id} ? t8('This invoice has not been posted yet.') : undef,
+        ],
+        action => [
+          t8('Follow-Up'),
+          call     => [ 'follow_up_window' ],
+          disabled => !$form->{id} ? t8('This invoice has not been posted yet.') : undef,
+        ],
+        action => [
+          t8('Record templates'),
+          call => [ 'kivi.RecordTemplate.popup', 'gl_transaction' ],
+        ],
+        action => [
+          t8('Drafts'),
+          call     => [ 'kivi.Draft.popup', 'gl', 'unknown', $form->{draft_id}, $form->{draft_description} ],
+          disabled => $form->{id}     ? t8('This invoice has already been posted.')
+                    : $form->{locked} ? t8('The billing period has already been locked.')
+                    : undef,
+        ],
+      ], # end of combobox "more"
+    );
+  }
+}
+
+sub setup_gl_search_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Search'),
+        submit    => [ '#form', { action => 'continue', nextsub => 'generate_report' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_gl_transactions_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [ $::locale->text('Create new') ],
+        action => [
+          $::locale->text('GL Transaction'),
+          submit => [ '#create_new_form', { action => 'gl_transaction' } ],
+        ],
+        action => [
+          $::locale->text('AR Transaction'),
+          submit => [ '#create_new_form', { action => 'ar_transaction' } ],
+        ],
+        action => [
+          $::locale->text('AP Transaction'),
+          submit => [ '#create_new_form', { action => 'ap_transaction' } ],
+        ],
+        action => [
+          $::locale->text('Sales Invoice'),
+          submit => [ '#create_new_form', { action => 'sales_invoice'  } ],
+        ],
+        action => [
+          $::locale->text('Vendor Invoice'),
+          submit => [ '#create_new_form', { action => 'vendor_invoice' } ],
+        ],
+      ], # end of combobox "Create new"
+    );
+  }
+}
+
 sub form_header {
   $::lxdebug->enter_sub;
-  $::auth->assert('general_ledger');
+  $::auth->assert('gl_transactions');
 
   my ($init) = @_;
 
-  my @old_project_ids = grep { $_ } map{ $::form->{"project_id_$_"} } 1..$::form->{rowcount};
+  $::request->layout->add_javascripts("autocomplete_chart.js", "autocomplete_project.js", "kivi.File.js", "kivi.GL.js", "kivi.RecordTemplate.js", "kivi.Validator.js", "show_history.js");
 
-  $::form->get_lists("projects"  => { "key"       => "ALL_PROJECTS",
-                                    "all"       => 0,
-                                    "old_id"    => \@old_project_ids },
-                   "charts"    => { "key"       => "ALL_CHARTS",
-                                    "transdate" => $::form->{transdate} });
+  my @old_project_ids     = grep { $_ } map{ $::form->{"project_id_$_"} } 1..$::form->{rowcount};
+  my @conditions          = @old_project_ids ? (id => \@old_project_ids) : ();
+  $::form->{ALL_PROJECTS} = SL::DB::Manager::Project->get_all_sorted(query => [ or => [ active => 1, @conditions ]]);
 
-  GL->get_chart_balances('charts' => $::form->{ALL_CHARTS});
+  $::form->get_lists(
+    "charts"    => { "key" => "ALL_CHARTS", "transdate" => $::form->{transdate} },
+  );
+
+  # we cannot book on charttype header
+  @{ $::form->{ALL_CHARTS} } = grep { $_->{charttype} ne 'H' }  @{ $::form->{ALL_CHARTS} };
+  $::form->{ALL_DEPARTMENTS} = SL::DB::Manager::Department->get_all_sorted;
 
   my $title      = $::form->{title};
   $::form->{title} = $::locale->text("$title General Ledger Transaction");
@@ -867,20 +1155,18 @@ sub form_header {
   map { $::form->{$_} =~ s/\"/&quot;/g }
     qw(chart taxchart);
 
-  $::form->{selectdepartment} =~ s/ selected//;
-  $::form->{selectdepartment} =~
-    s/option>\Q$::form->{department}\E/option selected>$::form->{department}/;
-
   if ($init) {
     $::request->{layout}->focus("#reference");
     $::form->{taxincluded} = "1";
   } else {
-    $::request->{layout}->focus("#accno_$::form->{rowcount}");
+    $::request->{layout}->focus("#accno_id_$::form->{rowcount}_name");
   }
 
   $::form->{previous_id}     ||= "--";
   $::form->{previous_gldate} ||= "--";
 
+  setup_gl_action_bar();
+
   $::form->header;
   print $::form->parse_html_template('gl/form_header', {
     hide_title => $title,
@@ -893,12 +1179,12 @@ sub form_header {
 
 sub form_footer {
   $::lxdebug->enter_sub;
-  $::auth->assert('general_ledger');
+  $::auth->assert('gl_transactions');
 
   my ($follow_ups, $follow_ups_due);
 
   if ($::form->{id}) {
-    $follow_ups     = FU->follow_ups('trans_id' => $::form->{id});
+    $follow_ups     = FU->follow_ups('trans_id' => $::form->{id}, 'not_done' => 1);
     $follow_ups_due = sum map { $_->{due} * 1 } @{ $follow_ups || [] };
   }
 
@@ -914,42 +1200,6 @@ sub form_footer {
 sub delete {
   $main::lxdebug->enter_sub();
 
-  my $form     = $main::form;
-  my $locale   = $main::locale;
-
-  $form->header;
-
-  print qq|
-<form method=post action=gl.pl>
-|;
-
-  map { $form->{$_} =~ s/\"/&quot;/g } qw(reference description);
-
-  delete $form->{header};
-
-  foreach my $key (keys %$form) {
-    next if (($key eq 'login') || ($key eq 'password') || ('' ne ref $form->{$key}));
-    print qq|<input type="hidden" name="$key" value="$form->{$key}">\n|;
-  }
-
-  print qq|
-<h2 class=confirm>| . $locale->text('Confirm!') . qq|</h2>
-
-<h4>|
-    . $locale->text('Are you sure you want to delete Transaction')
-    . qq| $form->{reference}</h4>
-
-<input name=action class=submit type=submit value="|
-    . $locale->text('Yes') . qq|">
-</form>
-|;
-  $main::lxdebug->leave_sub();
-
-}
-
-sub yes {
-  $main::lxdebug->enter_sub();
-
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
@@ -978,9 +1228,10 @@ sub post_transaction {
   my $locale   = $main::locale;
 
   # check if there is something in reference and date
-  $form->isblank("reference",   $locale->text('Reference missing!'));
-  $form->isblank("transdate",   $locale->text('Transaction Date missing!'));
-  $form->isblank("description", $locale->text('Description missing!'));
+  $form->isblank("reference",               $locale->text('Reference missing!'));
+  $form->isblank("transdate",               $locale->text('Transaction Date missing!'));
+  $form->isblank("description",             $locale->text('Description missing!'));
+  $form->isblank("transaction_description", $locale->text('A transaction description is required.')) if $::instance_conf->get_require_transaction_description_ps;
 
   my $transdate = $form->datetonum($form->{transdate}, \%myconfig);
   my $closedto  = $form->datetonum($form->{closedto},  \%myconfig);
@@ -994,11 +1245,11 @@ sub post_transaction {
   my $debitcredit;
   my %split_safety = ();
 
-  my $dbh = $form->dbconnect_noauto(\%myconfig);
+  my $dbh = SL::DB->client->dbh;
   my ($notax_id) = selectrow_query($form, $dbh, "SELECT id FROM tax WHERE taxkey = 0 LIMIT 1", );
-  $dbh->disconnect;
+  my $zerotaxes  = selectall_hashref_query($form, $dbh, "SELECT id FROM tax WHERE rate = 0", );
 
-  my @flds = qw(accno debit credit projectnumber fx_transaction source memo tax taxchart);
+  my @flds = qw(accno_id debit credit projectnumber fx_transaction source memo tax taxchart);
 
   for my $i (1 .. $form->{rowcount}) {
     next if $form->{"debit_$i"} eq "" && $form->{"credit_$i"} eq "";
@@ -1038,21 +1289,22 @@ sub post_transaction {
       $form->{debitlock} = 1;
     }
     if ($debitcredit && $credittax) {
-      $form->{"taxchart_$i"} = "$notax_id--0.00";
+      $form->{"taxchart_$i"} = "$notax_id--0.00000";
     }
     if (!$debitcredit && $debittax) {
-      $form->{"taxchart_$i"} = "$notax_id--0.00";
+      $form->{"taxchart_$i"} = "$notax_id--0.00000";
     }
     my $amount = ($form->{"debit_$i"} == 0)
             ? $form->{"credit_$i"}
             : $form->{"debit_$i"};
     my $j = $#a;
     if (($debitcredit && $credittax) || (!$debitcredit && $debittax)) {
-      $form->{"taxchart_$i"} = "$notax_id--0.00";
+      $form->{"taxchart_$i"} = "$notax_id--0.00000";
       $form->{"tax_$i"}      = 0;
     }
     my ($taxkey, $rate) = split(/--/, $form->{"taxchart_$i"});
-    if ($taxkey > 1) {
+    my $iswithouttax = grep { $_->{id} == $taxkey } @{ $zerotaxes };
+    if (!$iswithouttax) {
       if ($debitcredit) {
         $debittax = 1;
       } else {
@@ -1118,24 +1370,72 @@ sub post_transaction {
     $form->error($locale->text('Empty transaction!'));
   }
 
-  if ((my $errno = GL->post_transaction(\%myconfig, \%$form)) <= -1) {
-    $errno *= -1;
-    my @err;
-    $err[1] = $locale->text('Cannot have a value in both Debit and Credit!');
-    $err[2] = $locale->text('Debit and credit out of balance!');
-    $err[3] = $locale->text('Cannot post a transaction without a value!');
 
-    $form->error($err[$errno]);
-  }
-  undef($form->{callback});
-  # saving the history
-  if(!exists $form->{addition} && $form->{id} ne "") {
-    $form->{snumbers} = qq|gltransaction_| . $form->{id};
-    $form->{addition} = "POSTED";
-    $form->{what_done} = "gl transaction";
-    $form->save_history;
-  }
-  # /saving the history
+  # start transaction (post + history + (optional) banktrans)
+  SL::DB->client->with_transaction(sub {
+
+    if ((my $errno = GL->post_transaction(\%myconfig, \%$form)) <= -1) {
+      $errno *= -1;
+      my @err;
+      $err[1] = $locale->text('Cannot have a value in both Debit and Credit!');
+      $err[2] = $locale->text('Debit and credit out of balance!');
+      $err[3] = $locale->text('Cannot post a transaction without a value!');
+
+      die $err[$errno];
+    }
+    # saving the history
+    if(!exists $form->{addition} && $form->{id} ne "") {
+      $form->{snumbers} = qq|gltransaction_| . $form->{id};
+      $form->{addition} = "POSTED";
+      $form->{what_done} = "gl transaction";
+      $form->save_history;
+    }
+
+    # Case BankTransaction: update RecordLink and BankTransaction
+    if ($form->{callback} =~ /BankTransaction/ && $form->{bt_id}) {
+      # set invoice_amount - we only rely on bt_id in form, do all other stuff ui independent
+      # die if we have a unlogic or NYI case and abort the whole transaction
+      my ($bt, $chart_id, $payment);
+      require SL::DB::Manager::BankTransaction;
+
+      $bt = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
+      die "No bank transaction found" unless $bt;
+
+      $chart_id = SL::DB::Manager::BankAccount->find_by(id => $bt->local_bank_account_id)->chart_id;
+      die "no chart id" unless $chart_id;
+
+      $payment = SL::DB::Manager::AccTransaction->get_all(where => [ trans_id => $::form->{id},
+                                                                     chart_link => { like => '%_paid%' },
+                                                                     chart_id => $chart_id                  ]);
+      die "guru meditation error: Can only assign amount to one bank account booking" if scalar @{ $payment } > 1;
+
+      # credit/debit * -1 matches the sign for bt.amount and bt.invoice_amount
+
+      die "Can only assign the full (partial) bank amount to a single general ledger booking: " . $bt->not_assigned_amount . " " .  ($payment->[0]->amount * -1)
+        unless (abs($bt->not_assigned_amount - ($payment->[0]->amount * -1)) < 0.001);
+
+      $bt->update_attributes(invoice_amount => $bt->invoice_amount + ($payment->[0]->amount * -1));
+
+      # create record_link
+      my %props = (
+        from_table => 'bank_transactions',
+        from_id    => $::form->{bt_id},
+        to_table   => 'gl',
+        to_id      => $::form->{id},
+      );
+      SL::DB::RecordLink->new(%props)->save;
+      # and tighten holy acc_trans_id for this bank_transaction
+      my  %props_acc = (
+        acc_trans_id        => $payment->[0]->acc_trans_id,
+        bank_transaction_id => $bt->id,
+        gl_id               => $payment->[0]->trans_id,
+      );
+      my $bta = SL::DB::BankTransactionAccTrans->new(%props_acc);
+      $bta->save;
+
+    }
+    1;
+  }) or do { die SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
@@ -1143,26 +1443,43 @@ sub post_transaction {
 sub post {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('gl_transactions');
 
   my $form     = $main::form;
   my $locale   = $main::locale;
 
-  if ($::myconfig{mandatory_departments} && !$form->{department}) {
-    $form->{saved_message} = $::locale->text('You have to specify a department.');
-    update();
-    exit;
+  if ($::myconfig{mandatory_departments} && !$form->{department_id}) {
+    $form->error($locale->text('You have to specify a department.'));
   }
 
   $form->{title}  = $locale->text("$form->{title} General Ledger Transaction");
   $form->{storno} = 0;
 
   post_transaction();
+  if ($::instance_conf->get_webdav) {
+    SL::Webdav->new(type     => 'general_ledger',
+                    number   => $form->{id},
+                   )->webdav_path;
+  }
+
+  my $msg = $::locale->text("General ledger transaction '#1' posted (ID: #2)", $form->{reference}, $form->{id});
+  if ($form->{callback} =~ /BankTransaction/ && $form->{bt_id}) {
+    $form->redirect($msg);
 
-  remove_draft() if $form->{remove_draft};
+  } elsif ('doc-tab' eq $form->{after_action}) {
+    # Redirect with callback containing a fragment does not work (by now)
+    # because the callback info is stored in the session an parsing the
+    # callback parameters does not support fragments (see SL::Form::redirect).
+    # So use flash_later for the message and redirect_headers for redirecting.
+    my $add_doc_url = build_std_url("script=gl.pl", 'action=edit', 'id=' . E($form->{id}), 'fragment=ui-tabs-docs');
+    SL::Helper::Flash::flash_later('info', $msg);
+    print $form->redirect_header($add_doc_url);
+    $::dispatcher->end_request;
 
-  $form->{callback} = build_std_url("action=add&DONT_LOAD_DRAFT=1", "show_details");
-  $form->redirect($form->{callback});
+  } else {
+    $form->{callback} = build_std_url("action=add", "show_details");
+    $form->redirect($msg);
+  }
 
   $main::lxdebug->leave_sub();
 }
@@ -1170,7 +1487,7 @@ sub post {
 sub post_as_new {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('gl_transactions');
 
   my $form     = $main::form;
 
@@ -1183,7 +1500,7 @@ sub post_as_new {
 sub storno {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('gl_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -1216,22 +1533,19 @@ sub continue {
 }
 
 sub get_tax_dropdown {
-  $main::lxdebug->enter_sub();
-
-  my $form = $main::form;
-  my @tax_accounts = GL->get_tax_dropdown($form->{accno});
+  my $transdate    = $::form->{transdate}    ? DateTime->from_kivitendo($::form->{transdate}) : DateTime->today_local;
+  my $deliverydate = $::form->{deliverydate} ? DateTime->from_kivitendo($::form->{deliverydate}) : undef;
+  my @tax_accounts = GL->get_active_taxes_for_chart($::form->{accno_id}, $deliverydate // $transdate);
+  my $html         = $::form->parse_html_template("gl/update_tax_accounts", { TAX_ACCOUNTS => \@tax_accounts });
 
-  foreach my $item (@tax_accounts) {
-    $item->{taxdescription} = $::locale->{iconv_utf8}->convert($item->{taxdescription});
-    $item->{taxdescription} .= ' ' . $form->round_amount($item->{rate} * 100);
-  }
-
-  $form->{TAX_ACCOUNTS} = [ @tax_accounts ];
-
-  print $form->ajax_response_header, $form->parse_html_template("gl/update_tax_accounts");
+  print $::form->ajax_response_header, $html;
+}
 
-  $main::lxdebug->leave_sub();
+sub get_chart_balance {
+  my %balances = GL->get_chart_balances($::form->{accno_id});
+  my $balance  = $::form->format_amount(\%::myconfig, $balances{ $::form->{accno_id} }, 2, 'DRCR');
 
+  print $::form->ajax_response_header, $balance;
 }
 
 1;
index 3328735..f3e7fe5 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Inventory Control module
@@ -38,8 +39,9 @@ use List::MoreUtils qw(any);
 use SL::AM;
 use SL::CVar;
 use SL::IC;
-use SL::Helper::Flash;
+use SL::Helper::Flash qw(flash);
 use SL::HTML::Util;
+use SL::Presenter::Part;
 use SL::ReportGenerator;
 
 #use SL::PE;
@@ -51,7 +53,6 @@ use strict;
 our ($form, $locale, %myconfig, $lxdebug, $auth);
 
 require "bin/mozilla/io.pl";
-require "bin/mozilla/invoice_io.pl";
 require "bin/mozilla/common.pl";
 require "bin/mozilla/reportgenerator.pl";
 
@@ -75,23 +76,6 @@ require "bin/mozilla/reportgenerator.pl";
 
 # end of main
 
-sub add {
-  $lxdebug->enter_sub();
-
-  $auth->assert('part_service_assembly_edit');
-
-  my $title                = 'Add ' . ucfirst $form->{item};
-  $form->{title}           = $locale->text($title);
-  $form->{callback}        = "$form->{script}?action=add&item=$form->{item}" unless $form->{callback};
-  $form->{unit_changeable} = 1;
-
-  IC->get_pricegroups(\%myconfig, \%$form);
-  &link_part;
-  &display_form;
-
-  $lxdebug->leave_sub();
-}
-
 sub search {
   $lxdebug->enter_sub();
 
@@ -101,11 +85,9 @@ sub search {
   $form->{lastsort}     = ""; # memory for which table was sort at last time
   $form->{ndxs_counter} = 0;  # counter for added entries to top100
 
-  my %is_xyz     = map { +"is_$_" => ($form->{searchitems} eq $_) } qw(part service assembly);
-
   $form->{title} = (ucfirst $form->{searchitems}) . "s";
+  $form->{title} =~ s/ys$/ies/;
   $form->{title} = $locale->text($form->{title});
-  $form->{title} = $locale->text('Assemblies') if ($is_xyz{is_assembly});
 
   $form->{CUSTOM_VARIABLES}                  = CVar->get_configs('module' => 'IC');
   ($form->{CUSTOM_VARIABLES_FILTER_CODE},
@@ -113,868 +95,40 @@ sub search {
                                                                            'include_prefix' => 'l_',
                                                                            'include_value'  => 'Y');
 
+  setup_ic_search_action_bar();
   $form->header;
 
   $form->get_lists('partsgroup'    => 'ALL_PARTSGROUPS');
-  print $form->parse_html_template('ic/search', { %is_xyz,
-                                                  dateformat => $myconfig{dateformat},
-                                                  limit => $myconfig{vclimit}, });
-
-  $lxdebug->leave_sub();
-}    #end search()
-
-sub search_update_prices {
-  $lxdebug->enter_sub();
-
-  $auth->assert('part_service_assembly_edit');
-
-  my $pricegroups = IC->get_pricegroups(\%myconfig, \%$form);
-
-  $form->{title} = $locale->text('Update Prices');
-
-  $form->header;
-
-  print $form->parse_html_template('ic/search_update_prices', { PRICE_ROWS => $pricegroups });
+  print $form->parse_html_template('ic/search');
 
   $lxdebug->leave_sub();
 }    #end search()
 
-sub confirm_price_update {
-  $lxdebug->enter_sub();
-
-  $auth->assert('part_service_assembly_edit');
-
-  my @errors      = ();
-  my $value_found = undef;
-
-  foreach my $idx (qw(sellprice listprice), (1..$form->{price_rows})) {
-    my $name      = $idx =~ m/\d/ ? $form->{"pricegroup_${idx}"}      : $idx eq 'sellprice' ? $locale->text('Sell Price') : $locale->text('List Price');
-    my $type      = $idx =~ m/\d/ ? $form->{"pricegroup_type_${idx}"} : $form->{"${idx}_type"};
-    my $value_idx = $idx =~ m/\d/ ? "price_${idx}" : $idx;
-    my $value     = $form->parse_amount(\%myconfig, $form->{$value_idx});
-
-    if ((0 > $value) && ($type eq 'percent')) {
-      push @errors, $locale->text('You cannot adjust the price for pricegroup "#1" by a negative percentage.', $name);
-
-    } elsif (!$value && ($form->{$value_idx} ne '')) {
-      push @errors, $locale->text('No valid number entered for pricegroup "#1".', $name);
-
-    } elsif (0 < $value) {
-      $value_found = 1;
-    }
-  }
-
-  push @errors, $locale->text('No prices will be updated because no prices have been entered.') if (!$value_found);
-
-  my $num_matches = IC->get_num_matches_for_priceupdate();
-
-  $form->header();
-
-  if (@errors) {
-    $form->show_generic_error(join('<br>', @errors), 'back_button' => 1);
-  }
-
-  $form->{nextsub} = "update_prices";
-
-  map { delete $form->{$_} } qw(action header);
-
-  print $form->parse_html_template('ic/confirm_price_update', { HIDDENS     => [ map { name => $_, value => $form->{$_} }, keys %$form ],
-                                                                num_matches => $num_matches });
-
-  $lxdebug->leave_sub();
-}
-
-sub update_prices {
-  $lxdebug->enter_sub();
-
-  $auth->assert('part_service_assembly_edit');
-
-  my $num_updated = IC->update_prices(\%myconfig, \%$form);
-
-  if (-1 != $num_updated) {
-    $form->redirect($locale->text('#1 prices were updated.', $num_updated));
-  } else {
-    $form->error($locale->text('Could not update prices!'));
-  }
-
-  $lxdebug->leave_sub();
-}
-
-#sub choice {
-#  $lxdebug->enter_sub();
-#
-#  $auth->assert('part_service_assembly_edit');
-#
-#  our ($j, $lastndx);
-#  my ($totop100);
-#
-#  $form->{title} = $locale->text('Top 100 hinzufuegen');
-#
-#  $form->header;
-#
-#  push @custom_hiddens, qw(searchitems title bom titel revers lastsort sort ndxs_counter extras);
-#  push @custom_hiddens, qw(itemstatus l_linetotal l_partnumber l_description l_onhand l_unit l_sellprice l_linetotalsellprice);
-#  my @HIDDENS = (
-#        +{ name => 'row',     value => $j              },
-#        +{ name => 'nextsub', value => 'item_selected' },
-#        +{ name => 'test',    value => 'item_selected' },
-#        +{ name => 'lastndx', value => $lastndx        },
-#    map(+{ name => $_,        value => $form->{$_}     }, @custom_hiddens),
-#  );
-#
-#  my ($partnumber, $description, $unit, $sellprice, $soldtotal);
-#  # if choice set data
-##  if ($form->{ndx}) {
-##    for my $i (0 .. $form->{ndxs_counter}) {
-##
-##      # insert data into top100
-##      push @{ $form->{parts} },
-##        { number      => "",
-##          partnumber  => $form->{"totop100_partnumber_$j"},
-##          description => $form->{"totop100_description_$j"},
-##          unit        => $form->{"totop100_unit_$j"},
-##          sellprice   => $form->{"totop100_sellprice_$j"},
-##          soldtotal   => $form->{"totop100_soldtotal_$j"},
-##        };
-##    }    #rof
-##  }    #fi
-#
-#  $totop100 = "";
-#
-#  # set data for next page
-#  for my $i (1 .. $form->{ndxs_counter}) {
-#    $partnumber  = $form->{"totop100_partnumber_$i"};
-#    $description = $form->{"totop100_description_$i"};
-#    $unit        = $form->{"totop100_unit_$i"};
-#    $sellprice   = $form->{"totop100_sellprice_$i"};
-#    $soldtotal   = $form->{"totop100_soldtotal_$i"};
-#
-#  push @PARTS, {
-#    totop100_partnumber  => $form->{"totop100_partnumber_$i"},
-#    totop100_description => $form->{"totop100_description_$i"},
-#    totop100_unit        => $form->{"totop100_unit_$i"},
-#    totop100_sellprice   => $form->{"totop100_sellprice_$i"},
-#    totop100_soldtotal   => $form->{"totop100_soldtotal_$i"},
-#  }
-#
-##    $totop100 .= qq|
-##<input type=hidden name=totop100_partnumber_$i value=$form->{"totop100_partnumber_$i"}>
-##<input type=hidden name=totop100_description_$i value=$form->{"totop100_description_$i"}>
-##<input type=hidden name=totop100_unit_$i value=$form->{"totop100_unit_$i"}>
-##<input type=hidden name=totop100_sellprice_$i value=$form->{"totop100_sellprice_$i"}>
-##<input type=hidden name=totop100_soldtotal_$i value=$form->{"totop100_soldtotal_$i"}>
-##    |;
-#  }    #rof
-#
-#  print $form->parse_html_template('ic/choice', +{ HIDDENS => \@HIDDENS, PARTS => \@PARTS });
-#
-#  $lxdebug->leave_sub();
-#}    #end choice
-
-#sub list {
-#  $lxdebug->enter_sub();
-#
-#  $auth->assert('part_service_assembly_edit');
-#
-#  our ($lastndx);
-#  our ($partnumber, $description, $unit, $sellprice, $soldtotal);
-#
-#  my @sortorders = ("", "partnumber", "description", "all");
-#  my $sortorder = $sortorders[($form->{description} ? 2 : 0) + ($form->{partnumber} ? 1 : 0)];
-#  IC->get_parts(\%myconfig, \%$form, $sortorder);
-#
-#  $form->{title} = $locale->text('Top 100 hinzufuegen');
-#
-#  $form->header;
-#
-#  print qq|
-#  <h1>| . $locale->text('choice part') . qq|</h1>
-#  <form method=post action=ic.pl>
-#    <table width=100%>
-#        <tr class=listheading>
-#          <th>&nbsp;</th>
-#          <th class=listheading>| . $locale->text('Part Number') . qq|</th>
-#          <th class=listheading>| . $locale->text('Part Description') . qq|</th>
-#          <th class=listheading>| . $locale->text('Unit of measure') . qq|</th>
-#          <th class=listheading>| . $locale->text('Sell Price') . qq|</th>
-#          <th class=listheading>| . $locale->text('soldtotal') . qq|</th>
-#        </tr>|;
-#
-#  my $j = 0;
-#  my $i = $form->{rows};
-#
-#  for ($j = 1; $j <= $i; $j++) {
-#
-#    print qq|
-#        <tr class=listrow| . ($j % 2) . qq|>|;
-#    if ($j == 1) {
-#      print qq|
-#            <td><input name=ndx class=radio type=radio value=$j checked></td>|;
-#    } else {
-#      print qq|
-#          <td><input name=ndx class=radio type=radio value=$j></td>|;
-#    }
-#    print qq|
-#          <td><input name="new_partnumber_$j" type=hidden value="$form->{"partnumber_$j"}">$form->{"partnumber_$j"}</td>
-#          <td><input name="new_description_$j" type=hidden value="$form->{"description_$j"}">$form->{"description_$j"}</td>
-#          <td><input name="new_unit_$j" type=hidden value="$form->{"unit_$j"}">$form->{"unit_$j"}</td>
-#          <td><input name="new_sellprice_$j" type=hidden value="$form->{"sellprice_$j"}">$form->{"sellprice_$j"}</td>
-#          <td><input name="new_soldtotal_$j" type=hidden value="$form->{"soldtotal_$j"}">$form->{"soldtotal_$j"}</td>
-#        </tr>
-#
-#        <input name="new_id_$j" type=hidden value="$form->{"id_$j"}">|;
-#  }
-#
-#  print qq|
-#
-#</table>
-#
-#<br>
-#
-#
-#<input type=hidden name=itemstatus value="$form->{itemstatus}">
-#<input type=hidden name=l_linetotal value="$form->{l_linetotal}">
-#<input type=hidden name=l_partnumber value="$form->{l_partnumber}">
-#<input type=hidden name=l_description value="$form->{l_description}">
-#<input type=hidden name=l_onhand value="$form->{l_onhand}">
-#<input type=hidden name=l_unit value="$form->{l_unit}">
-#<input type=hidden name=l_sellprice value="$form->{l_sellprice}">
-#<input type=hidden name=l_linetotalsellprice value="$form->{l_linetotalsellprice}">
-#<input type=hidden name=sort value="$form->{sort}">
-#<input type=hidden name=revers value="$form->{revers}">
-#<input type=hidden name=lastsort value="$form->{lastsort}">
-#
-#<input type=hidden name=bom value="$form->{bom}">
-#<input type=hidden name=titel value="$form->{titel}">
-#<input type=hidden name=searchitems value="$form->{searchitems}">
-#
-#<input type=hidden name=row value=$j>
-#
-#<input type=hidden name=nextsub value=item_selected>
-#
-#<input name=lastndx type=hidden value=$lastndx>
-#
-#<input name=ndxs_counter type=hidden value=$form->{ndxs_counter}>|;
-#
-#  my $totop100 = "";
-#
-#  if (($form->{ndxs_counter}) > 0) {
-#    for ($i = 1; ($i < $form->{ndxs_counter} + 1); $i++) {
-#
-#      $partnumber  = $form->{"totop100_partnumber_$i"};
-#      $description = $form->{"totop100_description_$i"};
-#      $unit        = $form->{"totop100_unit_$i"};
-#      $sellprice   = $form->{"totop100_sellprice_$i"};
-#      $soldtotal   = $form->{"totop100_soldtotal_$i"};
-#
-#      $totop100 .= qq|
-#<input type=hidden name=totop100_partnumber_$i value=$form->{"totop100_partnumber_$i"}>
-#<input type=hidden name=totop100_description_$i value=$form->{"totop100_description_$i"}>
-#<input type=hidden name=totop100_unit_$i value=$form->{"totop100_unit_$i"}>
-#<input type=hidden name=totop100_sellprice_$i value=$form->{"totop100_sellprice_$i"}>
-#<input type=hidden name=totop100_soldtotal_$i value=$form->{"totop100_soldtotal_$i"}>
-#      |;
-#    }    #rof
-#  }    #fi
-#
-#  print $totop100;
-#
-#  print qq|
-#<input class=submit type=submit name=action value="|
-#    . $locale->text('TOP100') . qq|">
-#
-#</form>
-#|;
-#  $lxdebug->leave_sub();
-#}    #end list()
-
 sub top100 {
-  $lxdebug->enter_sub();
-
-  $auth->assert('part_service_assembly_edit');
-
-  if ($form->{ndx}) {
-    $form->{ndxs_counter}++;
-
-    if ($form->{ndxs_counter} > 0) {
-
-      my $index = $form->{ndx};
-
-      $form->{"totop100_partnumber_$form->{ndxs_counter}"} = $form->{"new_partnumber_$index"};
-      $form->{"totop100_description_$form->{ndxs_counter}"} = $form->{"new_description_$index"};
-      $form->{"totop100_unit_$form->{ndxs_counter}"} = $form->{"new_unit_$index"};
-      $form->{"totop100_sellprice_$form->{ndxs_counter}"} = $form->{"new_sellprice_$index"};
-      $form->{"totop100_soldtotal_$form->{ndxs_counter}"} = $form->{"new_soldtotal_$index"};
-    }    #fi
-  }    #fi
-  &addtop100();
-  $lxdebug->leave_sub();
-}    #end top100
-
-sub addtop100 {
-  $lxdebug->enter_sub();
-
-  $auth->assert('part_service_assembly_edit');
-
-  my ($revers, $lastsort, $callback, $option, $description, $sameitem,
-      $partnumber, $unit, $sellprice, $soldtotal, $totop100, $onhand, $align);
-  my (@column_index, %column_header, %column_data);
-  my ($totalsellprice, $totallastcost, $totallistprice, $subtotalonhand, $subtotalsellprice, $subtotallastcost, $subtotallistprice);
-
-  $form->{top100}      = "top100";
-  $form->{l_soldtotal} = "Y";
-  $form->{soldtotal}   = "soldtotal";
-  $form->{sort}        = "soldtotal";
-  $form->{l_qty}       = "N";
-  $form->{l_linetotal} = "";
-  $form->{revers}      = 1;
-  $form->{number}      = "position";
-  $form->{l_number}    = "Y";
-
-  $totop100 = "";
-
-  $form->{title} = $locale->text('Top 100');
-
-  $revers   = $form->{revers};
-  $lastsort = $form->{lastsort};
-
-  if (($form->{lastsort} eq "") && ($form->{sort} eq undef)) {
-    $form->{revers}   = 0;
-    $form->{lastsort} = "partnumber";
-    $form->{sort}     = "partnumber";
-  }    #fi
-
-  $callback =
-    "$form->{script}?action=top100&searchitems=$form->{searchitems}&itemstatus=$form->{itemstatus}&bom=$form->{bom}&l_linetotal=$form->{l_linetotal}&title="
-    . $form->escape($form->{title}, 1);
-
-  # if we have a serialnumber limit search
-  if ($form->{serialnumber} || $form->{l_serialnumber}) {
-    $form->{l_serialnumber} = "Y";
-    unless (   $form->{bought}
-            || $form->{sold}
-            || $form->{rfq}
-            || $form->{quoted}) {
-      $form->{bought} = $form->{sold} = 1;
-    }
-  }
-  IC->all_parts(\%myconfig, \%$form);
-
-  if ($form->{itemstatus} eq 'active') {
-    $option .= $locale->text('Active') . " : ";
-  }
-  if ($form->{itemstatus} eq 'obsolete') {
-    $option .= $locale->text('Obsolete') . " : ";
-  }
-  if ($form->{itemstatus} eq 'orphaned') {
-    $option .= $locale->text('Orphaned') . " : ";
-  }
-  if ($form->{itemstatus} eq 'onhand') {
-    $option .= $locale->text('On Hand') . " : ";
-    $form->{l_onhand} = "Y";
-  }
-  if ($form->{itemstatus} eq 'short') {
-    $option .= $locale->text('Short') . " : ";
-    $form->{l_onhand} = "Y";
-  }
-  if ($form->{onorder}) {
-    $form->{l_ordnumber} = "Y";
-    $callback .= "&onorder=$form->{onorder}";
-    $option   .= $locale->text('On Order') . " : ";
-  }
-  if ($form->{ordered}) {
-    $form->{l_ordnumber} = "Y";
-    $callback .= "&ordered=$form->{ordered}";
-    $option   .= $locale->text('Ordered') . " : ";
-  }
-  if ($form->{rfq}) {
-    $form->{l_quonumber} = "Y";
-    $callback .= "&rfq=$form->{rfq}";
-    $option   .= $locale->text('RFQ') . " : ";
-  }
-  if ($form->{quoted}) {
-    $form->{l_quonumber} = "Y";
-    $callback .= "&quoted=$form->{quoted}";
-    $option   .= $locale->text('Quoted') . " : ";
-  }
-  if ($form->{bought}) {
-    $form->{l_invnumber} = "Y";
-    $callback .= "&bought=$form->{bought}";
-    $option   .= $locale->text('Bought') . " : ";
-  }
-  if ($form->{sold}) {
-    $form->{l_invnumber} = "Y";
-    $callback .= "&sold=$form->{sold}";
-    $option   .= $locale->text('Sold') . " : ";
-  }
-  if (   $form->{bought}
-      || $form->{sold}
-      || $form->{onorder}
-      || $form->{ordered}
-      || $form->{rfq}
-      || $form->{quoted}) {
-
-    $form->{l_lastcost} = "";
-    $form->{l_name}     = "Y";
-    if ($form->{transdatefrom}) {
-      $callback .= "&transdatefrom=$form->{transdatefrom}";
-      $option   .= "\n<br>"
-        . $locale->text('From')
-        . "&nbsp;"
-        . $locale->date(\%myconfig, $form->{transdatefrom}, 1);
-    }
-    if ($form->{transdateto}) {
-      $callback .= "&transdateto=$form->{transdateto}";
-      $option   .= "\n<br>"
-        . $locale->text('To')
-        . "&nbsp;"
-        . $locale->date(\%myconfig, $form->{transdateto}, 1);
-    }
-  }
-
-  $option .= "<br>";
-
-  if ($form->{partnumber}) {
-    $callback .= "&partnumber=$form->{partnumber}";
-    $option   .= $locale->text('Part Number') . qq| : $form->{partnumber}<br>|;
-  }
-  if ($form->{ean}) {
-    $callback .= "&partnumber=$form->{ean}";
-    $option   .= $locale->text('EAN') . qq| : $form->{ean}<br>|;
-  }
-  if ($form->{partsgroup}) {
-    $callback .= "&partsgroup=$form->{partsgroup}";
-    $option   .= $locale->text('Group') . qq| : $form->{partsgroup}<br>|;
-  }
-  if ($form->{serialnumber}) {
-    $callback .= "&serialnumber=$form->{serialnumber}";
-    $option   .= $locale->text('Serial Number') . qq| : $form->{serialnumber}<br>|;
-  }
-  if ($form->{description}) {
-    $callback   .= "&description=$form->{description}";
-    $description = $form->{description};
-    $description =~ s/\n/<br>/g;
-    $option     .= $locale->text('Part Description') . qq| : $form->{description}<br>|;
-  }
-  if ($form->{make}) {
-    $callback .= "&make=$form->{make}";
-    $option   .= $locale->text('Make') . qq| : $form->{make}<br>|;
-  }
-  if ($form->{model}) {
-    $callback .= "&model=$form->{model}";
-    $option   .= $locale->text('Model') . qq| : $form->{model}<br>|;
-  }
-  if ($form->{drawing}) {
-    $callback .= "&drawing=$form->{drawing}";
-    $option   .= $locale->text('Drawing') . qq| : $form->{drawing}<br>|;
-  }
-  if ($form->{microfiche}) {
-    $callback .= "&microfiche=$form->{microfiche}";
-    $option   .= $locale->text('Microfiche') . qq| : $form->{microfiche}<br>|;
-  }
-  if ($form->{l_soldtotal}) {
-    $callback .= "&soldtotal=$form->{soldtotal}";
-    $option   .= $locale->text('soldtotal') . qq| : $form->{soldtotal}<br>|;
-  }
-
-  my @columns = $form->sort_columns(
-    qw(number partnumber ean description partsgroup bin onhand rop unit listprice linetotallistprice sellprice linetotalsellprice lastcost linetotallastcost priceupdate weight image drawing microfiche invnumber ordnumber quonumber name serialnumber soldtotal)
-  );
-
-  if ($form->{l_linetotal}) {
-    $form->{l_onhand} = "Y";
-    $form->{l_linetotalsellprice} = "Y" if $form->{l_sellprice};
-    if ($form->{l_lastcost}) {
-      $form->{l_linetotallastcost} = "Y";
-      if (($form->{searchitems} eq 'assembly') && !$form->{bom}) {
-        $form->{l_linetotallastcost} = "";
-      }
-    }
-    $form->{l_linetotallistprice} = "Y" if $form->{l_listprice};
-  }
-
-  if ($form->{searchitems} eq 'service') {
-
-    # remove bin, weight and rop from list
-    map { $form->{"l_$_"} = "" } qw(bin weight rop);
-
-    $form->{l_onhand} = "";
-
-    # qty is irrelevant unless bought or sold
-    if (   $form->{bought}
-        || $form->{sold}
-        || $form->{onorder}
-        || $form->{ordered}
-        || $form->{rfq}
-        || $form->{quoted}) {
-      $form->{l_onhand} = "Y";
-    } else {
-      $form->{l_linetotalsellprice} = "";
-      $form->{l_linetotallastcost}  = "";
-    }
-  }
-
-  foreach my $item (@columns) {
-    if ($form->{"l_$item"} eq "Y") {
-      push @column_index, $item;
+  $::lxdebug->enter_sub();
 
-      # add column to callback
-      $callback .= "&l_$item=Y";
-    }
-  }
-
-  if ($form->{l_subtotal} eq 'Y') {
-    $callback .= "&l_subtotal=Y";
-  }
-
-  $column_header{number} =
-    qq|<th class=listheading nowrap>| . $locale->text('number') . qq|</th>|;
-  $column_header{partnumber} =
-    qq|<th nowrap><a class=listheading href=$callback&sort=partnumber&revers=$form->{revers}&lastsort=$form->{lastsort}>|
-    . $locale->text('Part Number')
-    . qq|</a></th>|;
-  $column_header{description} =
-    qq|<th nowrap><a class=listheading href=$callback&sort=description&revers=$form->{revers}&lastsort=$form->{lastsort}>|
-    . $locale->text('Part Description')
-    . qq|</a></th>|;
-  $column_header{partsgroup} =
-      qq|<th nowrap><a class=listheading href=$callback&sort=partsgroup>|
-    . $locale->text('Group')
-    . qq|</a></th>|;
-  $column_header{bin} =
-      qq|<th><a class=listheading href=$callback&sort=bin>|
-    . $locale->text('Bin')
-    . qq|</a></th>|;
-  $column_header{priceupdate} =
-      qq|<th nowrap><a class=listheading href=$callback&sort=priceupdate>|
-    . $locale->text('Updated')
-    . qq|</a></th>|;
-  $column_header{onhand} =
-    qq|<th nowrap><a  class=listheading href=$callback&sort=onhand&revers=$form->{revers}&lastsort=$form->{lastsort}>|
-    . $locale->text('Qty')
-    . qq|</th>|;
-  $column_header{unit} =
-    qq|<th class=listheading nowrap>| . $locale->text('Unit') . qq|</th>|;
-  $column_header{listprice} =
-      qq|<th class=listheading nowrap>|
-    . $locale->text('List Price')
-    . qq|</th>|;
-  $column_header{lastcost} =
-    qq|<th class=listheading nowrap>| . $locale->text('Last Cost') . qq|</th>|;
-  $column_header{rop} =
-    qq|<th class=listheading nowrap>| . $locale->text('ROP') . qq|</th>|;
-  $column_header{weight} =
-    qq|<th class=listheading nowrap>| . $locale->text('Weight') . qq|</th>|;
-
-  $column_header{invnumber} =
-      qq|<th nowrap><a class=listheading href=$callback&sort=invnumber>|
-    . $locale->text('Invoice Number')
-    . qq|</a></th>|;
-  $column_header{ordnumber} =
-      qq|<th nowrap><a class=listheading href=$callback&sort=ordnumber>|
-    . $locale->text('Order Number')
-    . qq|</a></th>|;
-  $column_header{quonumber} =
-      qq|<th nowrap><a class=listheading href=$callback&sort=quonumber>|
-    . $locale->text('Quotation')
-    . qq|</a></th>|;
-
-  $column_header{name} =
-      qq|<th nowrap><a class=listheading href=$callback&sort=name>|
-    . $locale->text('Name')
-    . qq|</a></th>|;
-
-  $column_header{sellprice} =
-      qq|<th class=listheading nowrap>|
-    . $locale->text('Sell Price')
-    . qq|</th>|;
-  $column_header{linetotalsellprice} =
-    qq|<th class=listheading nowrap>| . $locale->text('Extended') . qq|</th>|;
-  $column_header{linetotallastcost} =
-    qq|<th class=listheading nowrap>| . $locale->text('Extended') . qq|</th>|;
-  $column_header{linetotallistprice} =
-    qq|<th class=listheading nowrap>| . $locale->text('Extended') . qq|</th>|;
-
-  $column_header{image} =
-    qq|<th class=listheading nowrap>| . $locale->text('Image') . qq|</a></th>|;
-  $column_header{drawing} =
-      qq|<th nowrap><a class=listheading href=$callback&sort=drawing>|
-    . $locale->text('Drawing')
-    . qq|</a></th>|;
-  $column_header{microfiche} =
-      qq|<th nowrap><a class=listheading href=$callback&sort=microfiche>|
-    . $locale->text('Microfiche')
-    . qq|</a></th>|;
-
-  $column_header{serialnumber} =
-      qq|<th nowrap><a class=listheading href=$callback&sort=serialnumber>|
-    . $locale->text('Serial Number')
-    . qq|</a></th>|;
-  $column_header{soldtotal} =
-    qq|<th nowrap><a class=listheading href=$callback&sort=soldtotal&revers=$form->{revers}&lastsort=$form->{lastsort}>|
-    . $locale->text('soldtotal')
-    . qq|</a></th>|;
-
-  $form->header;
-  my $colspan = $#column_index + 1;
-
-  print qq|
-    <h1>$form->{title}</h1>
-
-<table width=100%>
-
-  <tr><td colspan=$colspan>$option</td></tr>
-
-  <tr class=listheading>
-|;
-
-  map { print "\n$column_header{$_}" } @column_index;
-
-  print qq|
-  </tr>
-  |;
-
-  # add order to callback
-  $form->{callback} = $callback .= "&sort=$form->{sort}";
+  $::auth->assert('part_service_assembly_edit');
 
-  # escape callback for href
-  $callback = $form->escape($callback);
+  $::form->{l_soldtotal} = "Y";
+  $::form->{sort}        = "soldtotal";
+  $::form->{lastsort}    = "soldtotal";
 
-  if (@{ $form->{parts} }) {
-    $sameitem = $form->{parts}->[0]->{ $form->{sort} };
-  }
+  $::form->{l_qty}       = undef;
+  $::form->{l_linetotal} = undef;
+  $::form->{l_number}    = "Y";
+  $::form->{number}      = "position";
 
-  # insert numbers for top100
-  my $j = 0;
-  foreach my $ref (@{ $form->{parts} }) {
-    $j++;
-    $ref->{number} = $j;
+  unless (   $::form->{bought}
+          || $::form->{sold}
+          || $::form->{rfq}
+          || $::form->{quoted}) {
+    $::form->{bought} = $::form->{sold} = 1;
   }
 
-  # if avaible -> insert choice here
-  if (($form->{ndxs_counter}) > 0) {
-    for (my $i = 1; ($i < $form->{ndxs_counter} + 1); $i++) {
-      $partnumber  = $form->{"totop100_partnumber_$i"};
-      $description = $form->{"totop100_description_$i"};
-      $unit        = $form->{"totop100_unit_$i"};
-      $sellprice   = $form->{"totop100_sellprice_$i"};
-      $soldtotal   = $form->{"totop100_soldtotal_$i"};
-
-      $totop100 .= qq|
-<input type=hidden name=totop100_partnumber_$i value=$form->{"totop100_partnumber_$i"}>
-<input type=hidden name=totop100_description_$i value=$form->{"totop100_description_$i"}>
-<input type=hidden name=totop100_unit_$i value=$form->{"totop100_unit_$i"}>
-<input type=hidden name=totop100_sellprice_$i value=$form->{"totop100_sellprice_$i"}>
-<input type=hidden name=totop100_soldtotal_$i value=$form->{"totop100_soldtotal_$i"}>
-      |;
-
-      # insert into list
-      push @{ $form->{parts} },
-        { number      => "",
-          partnumber  => "$partnumber",
-          description => "$description",
-          unit        => "$unit",
-          sellprice   => "$sellprice",
-          soldtotal   => "$soldtotal" };
-    }    #rof
-  }    #fi
-       # build data for columns
-  my $i = 0;
-  foreach my $ref (@{ $form->{parts} }) {
-
-    if ($form->{l_subtotal} eq 'Y' && !$ref->{assemblyitem}) {
-      if ($sameitem ne $ref->{ $form->{sort} }) {
-        parts_subtotal(\@column_index, \$subtotalonhand, \$subtotalsellprice, \$subtotallastcost, \$subtotallistprice);
-        $sameitem = $ref->{ $form->{sort} };
-      }
-    }
-
-    $ref->{exchangerate} = 1 unless $ref->{exchangerate};
-    $ref->{sellprice} *= $ref->{exchangerate};
-    $ref->{listprice} *= $ref->{exchangerate};
-    $ref->{lastcost}  *= $ref->{exchangerate};
-
-    # use this for assemblies
-    $onhand = $ref->{onhand};
-
-    $align = "left";
-    if ($ref->{assemblyitem}) {
-      $align = "right";
-      $onhand = 0 if ($form->{sold});
-    }
-
-    $ref->{description} =~ s/\n/<br>/g;
-
-    $column_data{number} =
-        "<td align=right>"
-      . $form->format_amount(\%myconfig, $ref->{number})
-      . "</td>";
-    $column_data{partnumber} =
-      "<td align=$align>$ref->{partnumber}&nbsp;</a></td>";
-    $column_data{description} = "<td>$ref->{description}&nbsp;</td>";
-    $column_data{partsgroup}  = "<td>$ref->{partsgroup}&nbsp;</td>";
-
-    $column_data{onhand} =
-        "<td align=right>"
-      . $form->format_amount(\%myconfig, $ref->{onhand})
-      . "</td>";
-    $column_data{sellprice} =
-        "<td align=right>"
-      . $form->format_amount(\%myconfig, $ref->{sellprice})
-      . "</td>";
-    $column_data{listprice} =
-        "<td align=right>"
-      . $form->format_amount(\%myconfig, $ref->{listprice})
-      . "</td>";
-    $column_data{lastcost} =
-        "<td align=right>"
-      . $form->format_amount(\%myconfig, $ref->{lastcost})
-      . "</td>";
-
-    $column_data{linetotalsellprice} = "<td align=right>"
-      . $form->format_amount(\%myconfig, $ref->{onhand} * $ref->{sellprice}, 2)
-      . "</td>";
-    $column_data{linetotallastcost} = "<td align=right>"
-      . $form->format_amount(\%myconfig, $ref->{onhand} * $ref->{lastcost}, 2)
-      . "</td>";
-    $column_data{linetotallistprice} = "<td align=right>"
-      . $form->format_amount(\%myconfig, $ref->{onhand} * $ref->{listprice}, 2)
-      . "</td>";
-
-    if (!$ref->{assemblyitem}) {
-      $totalsellprice += $onhand * $ref->{sellprice};
-      $totallastcost  += $onhand * $ref->{lastcost};
-      $totallistprice += $onhand * $ref->{listprice};
-
-      $subtotalonhand    += $onhand;
-      $subtotalsellprice += $onhand * $ref->{sellprice};
-      $subtotallastcost  += $onhand * $ref->{lastcost};
-      $subtotallistprice += $onhand * $ref->{listprice};
-    }
-
-    $column_data{rop} =
-      "<td align=right>"
-      . $form->format_amount(\%myconfig, $ref->{rop}) . "</td>";
-    $column_data{weight} =
-        "<td align=right>"
-      . $form->format_amount(\%myconfig, $ref->{weight})
-      . "</td>";
-    $column_data{unit}        = "<td>$ref->{unit}&nbsp;</td>";
-    $column_data{bin}         = "<td>$ref->{bin}&nbsp;</td>";
-    $column_data{priceupdate} = "<td>$ref->{priceupdate}&nbsp;</td>";
-
-    $column_data{invnumber} =
-      ($ref->{module} ne 'oe')
-      ? "<td><a href=$ref->{module}.pl?action=edit&type=invoice&id=$ref->{trans_id}&callback=$callback>$ref->{invnumber}</a></td>"
-      : "<td>$ref->{invnumber}</td>";
-    $column_data{ordnumber} =
-      ($ref->{module} eq 'oe')
-      ? "<td><a href=$ref->{module}.pl?action=edit&type=$ref->{type}&id=$ref->{trans_id}&callback=$callback>$ref->{ordnumber}</a></td>"
-      : "<td>$ref->{ordnumber}</td>";
-    $column_data{quonumber} =
-      ($ref->{module} eq 'oe' && !$ref->{ordnumber})
-      ? "<td><a href=$ref->{module}.pl?action=edit&type=$ref->{type}&id=$ref->{trans_id}&callback=$callback>$ref->{quonumber}</a></td>"
-      : "<td>$ref->{quonumber}</td>";
-
-    $column_data{name} = "<td>$ref->{name}</td>";
-
-    $column_data{image} =
-      ($ref->{image})
-      ? "<td><a href=$ref->{image}><img src=$ref->{image} height=32 border=0></a></td>"
-      : "<td>&nbsp;</td>";
-    $column_data{drawing} =
-      ($ref->{drawing})
-      ? "<td><a href=$ref->{drawing}>$ref->{drawing}</a></td>"
-      : "<td>&nbsp;</td>";
-    $column_data{microfiche} =
-      ($ref->{microfiche})
-      ? "<td><a href=$ref->{microfiche}>$ref->{microfiche}</a></td>"
-      : "<td>&nbsp;</td>";
-
-    $column_data{serialnumber} = "<td>$ref->{serialnumber}</td>";
-
-    $column_data{soldtotal} = "<td  align=right>$ref->{soldtotal}</td>";
-
-    $i++;
-    $i %= 2;
-    print "<tr class=listrow$i>";
-
-    map { print "\n$column_data{$_}" } @column_index;
-
-    print qq|
-    </tr>
-|;
-  }
-
-  if ($form->{l_subtotal} eq 'Y') {
-    parts_subtotal(\@column_index, \$subtotalonhand, \$subtotalsellprice, \$subtotallastcost, \$subtotallistprice);
-  }    #fi
-
-  if ($form->{"l_linetotal"}) {
-    map { $column_data{$_} = "<td>&nbsp;</td>" } @column_index;
-    $column_data{linetotalsellprice} =
-        "<th class=listtotal align=right>"
-      . $form->format_amount(\%myconfig, $totalsellprice, 2)
-      . "</th>";
-    $column_data{linetotallastcost} =
-        "<th class=listtotal align=right>"
-      . $form->format_amount(\%myconfig, $totallastcost, 2)
-      . "</th>";
-    $column_data{linetotallistprice} =
-        "<th class=listtotal align=right>"
-      . $form->format_amount(\%myconfig, $totallistprice, 2)
-      . "</th>";
-
-    print "<tr class=listtotal>";
-
-    map { print "\n$column_data{$_}" } @column_index;
-
-    print qq|</tr>
-    |;
-  }
-
-  print qq|
-  <tr><td colspan=$colspan><hr size=3 noshade></td></tr>
-</table>
-
-|;
-
-  print qq|
-
-<br>
-
-<form method=post action=$form->{script}>
-
-<input type=hidden name=itemstatus value="$form->{itemstatus}">
-<input type=hidden name=l_linetotal value="$form->{l_linetotal}">
-<input type=hidden name=l_partnumber value="$form->{l_partnumber}">
-<input type=hidden name=l_description value="$form->{l_description}">
-<input type=hidden name=l_onhand value="$form->{l_onhand}">
-<input type=hidden name=l_unit value="$form->{l_unit}">
-<input type=hidden name=l_sellprice value="$form->{l_sellprice}">
-<input type=hidden name=l_linetotalsellprice value="$form->{l_linetotalsellprice}">
-<input type=hidden name=sort value="$form->{sort}">
-<input type=hidden name=revers value="$form->{revers}">
-<input type=hidden name=lastsort value="$form->{lastsort}">
-<input type=hidden name=parts value="$form->{parts}">
-
-<input type=hidden name=bom value="$form->{bom}">
-<input type=hidden name=titel value="$form->{titel}">
-<input type=hidden name=searchitems value="$form->{searchitems}">|;
-
-  print $totop100;
-
-  print qq|
-<!--    <input type=hidden name=ndxs_counter value="$form->{ndxs_counter}">-->
-
-<!--    <input class=submit type=submit name=action value="|
-    . $locale->text('choice') . qq|"> -->
-
-  </form>
-|;
+  generate_report();
 
   $lxdebug->leave_sub();
-}    # end addtop100
+}
 
 #
 # Report for Wares.
@@ -986,7 +140,7 @@ sub addtop100 {
 #  searchitems=part revers=0 lastsort=''
 #
 # filter:
-# partnumber ean description partsgroup serialnumber make model drawing microfiche
+# partnumber ean description partsgroup classification serialnumber make model drawing microfiche
 # transdatefrom transdateto
 #
 # radio:
@@ -1011,12 +165,9 @@ sub generate_report {
 
   my $cvar_configs = CVar->get_configs('module' => 'IC');
 
-  $form->{title} = (ucfirst $form->{searchitems}) . "s";
-  $form->{title} =~ s/ys$/ies/;
-  $form->{title} = $locale->text($form->{title});
+  $form->{title} = $locale->text('Articles');
 
   my %column_defs = (
-    'bin'                => { 'text' => $locale->text('Bin'), },
     'deliverydate'       => { 'text' => $locale->text('deliverydate'), },
     'description'        => { 'text' => $locale->text('Part Description'), },
     'notes'              => { 'text' => $locale->text('Notes'), },
@@ -1026,6 +177,7 @@ sub generate_report {
     'insertdate'         => { 'text' => $locale->text('Insert Date'), },
     'invnumber'          => { 'text' => $locale->text('Invoice Number'), },
     'lastcost'           => { 'text' => $locale->text('Last Cost'), },
+    'assembly_lastcost'  => { 'text' => $locale->text('Assembly Last Cost'), },
     'linetotallastcost'  => { 'text' => $locale->text('Extended'), },
     'linetotallistprice' => { 'text' => $locale->text('Extended'), },
     'linetotalsellprice' => { 'text' => $locale->text('Extended'), },
@@ -1033,22 +185,28 @@ sub generate_report {
     'microfiche'         => { 'text' => $locale->text('Microfiche'), },
     'name'               => { 'text' => $locale->text('Name'), },
     'onhand'             => { 'text' => $locale->text('Stocked Qty'), },
+    'assembly_qty'       => { 'text' => $locale->text('Assembly Item Qty'), },
     'ordnumber'          => { 'text' => $locale->text('Order Number'), },
     'partnumber'         => { 'text' => $locale->text('Part Number'), },
-    'partsgroup'         => { 'text' => $locale->text('Group'), },
-    'priceupdate'        => { 'text' => $locale->text('Updated'), },
+    'partsgroup'         => { 'text' => $locale->text('Partsgroup'), },
+    'priceupdate'        => { 'text' => $locale->text('Price updated'), },
     'quonumber'          => { 'text' => $locale->text('Quotation'), },
     'rop'                => { 'text' => $locale->text('ROP'), },
     'sellprice'          => { 'text' => $locale->text('Sell Price'), },
     'serialnumber'       => { 'text' => $locale->text('Serial Number'), },
     'soldtotal'          => { 'text' => $locale->text('Qty in Selected Records'), },
     'name'               => { 'text' => $locale->text('Name in Selected Records'), },
-    'transdate'          => { 'text' => $locale->text('Transdate'), },
+    'transdate'          => { 'text' => $locale->text('Transdate Record'), },
     'unit'               => { 'text' => $locale->text('Unit'), },
     'weight'             => { 'text' => $locale->text('Weight'), },
-    'shop'               => { 'text' => $locale->text('Shopartikel'), },
+    'shop'               => { 'text' => $locale->text('Shop article'), },
+    'type_and_classific' => { 'text' => $locale->text('Type'), },
     'projectnumber'      => { 'text' => $locale->text('Project Number'), },
     'projectdescription' => { 'text' => $locale->text('Project Description'), },
+    'warehouse'          => { 'text' => $locale->text('Default Warehouse'), },
+    'bin'                => { 'text' => $locale->text('Default Bin'), },
+    'make'               => { 'text' => $locale->text('Make'), },
+    'model'              => { 'text' => $locale->text('Model'), },
   );
 
   $revers     = $form->{revers};
@@ -1111,6 +269,7 @@ sub generate_report {
     obsolete      => $locale->text('Obsolete'),
     orphaned      => $locale->text('Orphaned'),
     onhand        => $locale->text('On Hand'),
+    assembly_qty  => $locale->text('Assembly Item Qty'),
     short         => $locale->text('Short'),
     onorder       => $locale->text('On Order'),
     ordered       => $locale->text('Ordered'),
@@ -1121,23 +280,29 @@ sub generate_report {
     transdatefrom => $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{transdatefrom}, 1),
     transdateto   => $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{transdateto}, 1),
     partnumber    => $locale->text('Part Number')      . ": '$form->{partnumber}'",
-    partsgroup    => $locale->text('Group')            . ": '$form->{partsgroup}'",
-    partsgroup_id => $locale->text('Group')            . ": '$pg_name'",
+    partsgroup    => $locale->text('Partsgroup')       . ": '$form->{partsgroup}'",
+    partsgroup_id => $locale->text('Partsgroup')       . ": '$pg_name'",
     serialnumber  => $locale->text('Serial Number')    . ": '$form->{serialnumber}'",
     description   => $locale->text('Part Description') . ": '$form->{description}'",
     make          => $locale->text('Make')             . ": '$form->{make}'",
     model         => $locale->text('Model')            . ": '$form->{model}'",
+    customername  => $locale->text('Customer')         . ": '$form->{customername}'",
+    customernumber=> $locale->text('Customer Part Number').": '$form->{customernumber}'",
     drawing       => $locale->text('Drawing')          . ": '$form->{drawing}'",
     microfiche    => $locale->text('Microfiche')       . ": '$form->{microfiche}'",
     l_soldtotal   => $locale->text('Qty in Selected Records'),
     ean           => $locale->text('EAN')              . ": '$form->{ean}'",
     insertdatefrom => $locale->text('Insert Date') . ": " . $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{insertdatefrom}, 1),
     insertdateto   => $locale->text('Insert Date') . ": " . $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{insertdateto}, 1),
+    l_service     => $locale->text('Services'),
+    l_assembly    => $locale->text('Assemblies'),
+    l_part        => $locale->text('Parts'),
   );
 
   my @itemstatus_keys = qw(active obsolete orphaned onhand short);
   my @callback_keys   = qw(onorder ordered rfq quoted bought sold partnumber partsgroup partsgroup_id serialnumber description make model
-                           drawing microfiche l_soldtotal l_deliverydate transdatefrom transdateto insertdatefrom insertdateto ean shop);
+                           drawing microfiche l_soldtotal l_deliverydate transdatefrom transdateto insertdatefrom insertdateto ean shop all
+                           l_service l_assembly l_part);
 
   # calculate dependencies
   for (@itemstatus_keys, @callback_keys) {
@@ -1159,6 +324,7 @@ sub generate_report {
     $column_defs{sellprice}{text} = $locale->text('Price');
     $form->{l_lastcost} = ""
   }
+  $form->{l_assembly_lastcost} = "Y" if $form->{l_assembly} && $form->{l_lastcost};
 
   if ($form->{description}) {
     $description = $form->{description};
@@ -1171,11 +337,12 @@ sub generate_report {
     $form->{l_linetotallastcost}  = $form->{searchitems} eq 'assembly' && !$form->{bom} ? "" : 'Y' if  $form->{l_lastcost};
     $form->{l_linetotallistprice} = "Y" if $form->{l_listprice};
   }
+  $form->{"l_type_and_classific"} = "Y";
 
-  if ($form->{searchitems} eq 'service') {
+  if ($form->{l_service} && !$form->{l_assembly} && !$form->{l_part}) {
 
-    # remove bin, weight and rop from list
-    map { $form->{"l_$_"} = "" } qw(bin weight rop);
+    # remove warehouse, bin, weight and rop from list
+    map { $form->{"l_$_"} = "" } qw(bin weight rop warehouse);
 
     $form->{l_onhand} = "";
 
@@ -1211,6 +378,10 @@ sub generate_report {
 
     flash('warning', $::locale->text('Soldtotal does not make sense without any bsooqr options'));
   }
+  if ($form->{l_soldtotal} && ($form->{l_warehouse} || $form->{l_bin})) {
+    delete $form->{"l_$_"} for  qw(bin warehouse);
+    flash('warning', $::locale->text('Sorry, I am too stupid to figure out the default warehouse/bin and the sold qty. I drop the default warehouse/bin option.'));
+  }
   if ($form->{l_name} && !$bsooqr_mode) {
     delete $form->{l_name};
 
@@ -1219,14 +390,15 @@ sub generate_report {
   IC->all_parts(\%myconfig, \%$form);
 
   my @columns = qw(
-    partnumber description notes partsgroup bin onhand rop soldtotal unit listprice
-    linetotallistprice sellprice linetotalsellprice lastcost linetotallastcost
+    partnumber type_and_classific description notes partsgroup warehouse bin
+    make model assembly_qty onhand rop soldtotal unit listprice
+    linetotallistprice sellprice linetotalsellprice lastcost assembly_lastcost linetotallastcost
     priceupdate weight image drawing microfiche invnumber ordnumber quonumber
     transdate name serialnumber deliverydate ean projectnumber projectdescription
     insertdate shop
   );
 
-  my $pricegroups = SL::DB::Manager::Pricegroup->get_all(sort => 'id');
+  my $pricegroups = SL::DB::Manager::Pricegroup->get_all_sorted;
   my @pricegroup_columns;
   my %column_defs_pricegroups;
   if ($form->{l_pricegroups}) {
@@ -1248,13 +420,16 @@ sub generate_report {
 
   %column_defs = (%column_defs, %column_defs_cvars, %column_defs_pricegroups);
   map { $column_defs{$_}->{visible} ||= $form->{"l_$_"} ? 1 : 0 } @columns;
-  map { $column_defs{$_}->{align}   = 'right' } qw(onhand sellprice listprice lastcost linetotalsellprice linetotallastcost linetotallistprice rop weight soldtotal shop), @pricegroup_columns;
+  map { $column_defs{$_}->{align}   = 'right' } qw(assembly_qty onhand sellprice listprice lastcost assembly_lastcost linetotalsellprice linetotallastcost linetotallistprice rop weight soldtotal shop), @pricegroup_columns;
 
   my @hidden_variables = (
     qw(l_subtotal l_linetotal searchitems itemstatus bom l_pricegroups insertdatefrom insertdateto),
+    qw(l_type_and_classific classification_id l_part l_service l_assembly l_assortment),
     @itemstatus_keys,
     @callback_keys,
     map({ "cvar_$_->{name}" } @searchable_custom_variables),
+    map({'cvar_'. $_->{name} .'_from'} grep({$_->{type} eq 'date'} @searchable_custom_variables)),
+    map({'cvar_'. $_->{name} .'_to'}   grep({$_->{type} eq 'date'} @searchable_custom_variables)),
     map({'cvar_'. $_->{name} .'_qtyop'} grep({$_->{type} eq 'number'} @searchable_custom_variables)),
     map({ "l_$_" } @columns),
   );
@@ -1262,7 +437,7 @@ sub generate_report {
   my $callback         = build_std_url('action=generate_report', grep { $form->{$_} } @hidden_variables);
 
   my @sort_full        = qw(partnumber description onhand soldtotal deliverydate insertdate shop);
-  my @sort_no_revers   = qw(partsgroup bin priceupdate invnumber ordnumber quonumber name image drawing serialnumber);
+  my @sort_no_revers   = qw(partsgroup invnumber ordnumber quonumber name image drawing serialnumber);
 
   foreach my $col (@sort_full) {
     $column_defs{$col}->{link} = join '&', $callback, "sort=$col", map { "$_=" . E($form->{$_}) } qw(revers lastsort);
@@ -1278,13 +453,15 @@ sub generate_report {
     'part'     => $locale->text('part_list'),
     'service'  => $locale->text('service_list'),
     'assembly' => $locale->text('assembly_list'),
+    'article'  => $locale->text('article_list'),
   );
 
   $report->set_options('raw_top_info_text'     => $form->parse_html_template('ic/generate_report_top', { options => \@options }),
-                       'raw_bottom_info_text'  => $form->parse_html_template('ic/generate_report_bottom'),
+                       'raw_bottom_info_text'  => $form->parse_html_template('ic/generate_report_bottom' ,
+                                                  { PART_CLASSIFICATIONS => SL::DB::Manager::PartClassification->get_all_sorted }),
                        'output_format'         => 'HTML',
                        'title'                 => $form->{title},
-                       'attachment_basename'   => $attachment_basenames{$form->{searchitems}} . strftime('_%Y%m%d', localtime time),
+                       'attachment_basename'   => 'article_list' . strftime('_%Y%m%d', localtime time),
   );
   $report->set_options_from_form();
   $locale->set_numberformat_wo_thousands_separator(\%myconfig) if lc($report->{options}->{output_format}) eq 'csv';
@@ -1328,6 +505,7 @@ sub generate_report {
     $ref->{sellprice}     *= $ref->{exchangerate} / $ref->{price_factor};
     $ref->{listprice}     *= $ref->{exchangerate} / $ref->{price_factor};
     $ref->{lastcost}      *= $ref->{exchangerate} / $ref->{price_factor};
+    $ref->{assembly_lastcost} *= $ref->{exchangerate} / $ref->{price_factor};
 
     # use this for assemblies
     my $soldtotal = $bsooqr_mode ? $ref->{soldtotal} : $ref->{onhand};
@@ -1338,11 +516,11 @@ sub generate_report {
       $soldtotal                  = 0 if ($form->{sold});
     }
 
-    my $edit_link               = build_std_url('action=edit', 'id=' . E($ref->{id}), 'callback');
+    my $edit_link               = build_std_url('script=controller.pl', 'action=Part/edit', 'part.id=' . E($ref->{id}));
     $row->{partnumber}->{link}  = $edit_link;
     $row->{description}->{link} = $edit_link;
 
-    foreach (qw(sellprice listprice lastcost)) {
+    foreach (qw(sellprice listprice lastcost assembly_lastcost)) {
       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{$_}, 2);
       $row->{"linetotal$_"}{data} = $form->format_amount(\%myconfig, $ref->{onhand} * $ref->{$_}, 2);
     }
@@ -1376,14 +554,23 @@ sub generate_report {
       # | ist bestellt  | Von Kunden bestellt |  -> edit_oe_ord_link
       # | Anfrage       | Angebot             |  -> edit_oe_quo_link
 
-      my $edit_oe_ord_link = build_std_url("script=oe.pl", 'action=edit', 'type=' . E($ref->{cv} eq 'vendor' ? 'purchase_order' : 'sales_order'), 'id=' . E($ref->{trans_id}), 'callback');
-      my $edit_oe_quo_link = build_std_url("script=oe.pl", 'action=edit', 'type=' . E($ref->{cv} eq 'vendor' ? 'request_quotation' : 'sales_quotation'), 'id=' . E($ref->{trans_id}), 'callback');
+      my $edit_oe_ord_link = ($::instance_conf->get_feature_experimental_order)
+                           ? build_std_url("script=controller.pl", 'action=Order/edit',
+                                           'type=' . E($ref->{cv} eq 'vendor' ? 'purchase_order' : 'sales_order'),        'id=' . E($ref->{trans_id}), 'callback')
+                           : build_std_url("script=oe.pl",         'action=edit',
+                                           'type=' . E($ref->{cv} eq 'vendor' ? 'purchase_order' : 'sales_order'),        'id=' . E($ref->{trans_id}), 'callback');
+
+      my $edit_oe_quo_link = ($::instance_conf->get_feature_experimental_order)
+                           ? build_std_url("script=controller.pl", 'action=Order/edit',
+                                           'type=' . E($ref->{cv} eq 'vendor' ? 'request_quotation' : 'sales_quotation'), 'id=' . E($ref->{trans_id}), 'callback')
+                           : build_std_url("script=oe.pl",         'action=edit',
+                                           'type=' . E($ref->{cv} eq 'vendor' ? 'request_quotation' : 'sales_quotation'), 'id=' . E($ref->{trans_id}), 'callback');
 
       $row->{ordnumber}{link} = $edit_oe_ord_link;
       $row->{quonumber}{link} = $edit_oe_quo_link if (!$ref->{ordnumber});
 
     } else {
-      $row->{invnumber}{link} = build_std_url("script=$ref->{module}.pl", 'action=edit', 'type=invoice', 'id=' . E($ref->{trans_id}), 'callback');
+      $row->{invnumber}{link} = build_std_url("script=$ref->{module}.pl", 'action=edit', 'type=invoice', 'id=' . E($ref->{trans_id}), 'callback') if ($ref->{invnumber});
     }
 
     # set properties of images
@@ -1394,6 +581,11 @@ sub generate_report {
     map { $row->{$_}{link} = $ref->{$_} } qw(drawing microfiche);
 
     $row->{notes}{data} = SL::HTML::Util->strip($ref->{notes});
+    $row->{type_and_classific}{data} = SL::Presenter::Part::type_abbreviation($ref->{part_type}).
+                                       SL::Presenter::Part::classification_abbreviation($ref->{classification_id});
+
+    # last price update
+    $row->{priceupdate}{data} = SL::DB::Part->new(id => $ref->{id})->load->last_price_update->valid_from->to_kivitendo;
 
     $report->add_data($row);
 
@@ -1405,7 +597,7 @@ sub generate_report {
          (!$next_ref->{assemblyitem} && ($same_item ne $next_ref->{ $form->{sort} })))) {
       my $row = { map { $_ => { 'class' => 'listsubtotal', } } @columns };
 
-      if (($form->{searchitems} ne 'assembly') || !$form->{bom}) {
+      if ( !$form->{l_assembly} || !$form->{bom}) {
         $row->{soldtotal}->{data} = $form->format_amount(\%myconfig, $subtotals{soldtotal});
       }
 
@@ -1429,714 +621,58 @@ sub generate_report {
     $report->add_data($row);
   }
 
+  setup_ic_generate_report_action_bar();
   $report->generate_with_headers();
 
   $lxdebug->leave_sub();
 }    #end generate_report
 
-sub parts_subtotal {
-  $lxdebug->enter_sub();
-
-  $auth->assert('part_service_assembly_edit');
-
-  my (%column_data);
-  my ($column_index, $subtotalonhand, $subtotalsellprice, $subtotallastcost, $subtotallistprice) = @_;
-
-  map { $column_data{$_} = "<td>&nbsp;</td>" } @{ $column_index };
-  $$subtotalonhand = 0 if ($form->{searchitems} eq 'assembly' && $form->{bom});
-
-  $column_data{onhand} =
-      "<th class=listsubtotal align=right>"
-    . $form->format_amount(\%myconfig, $$subtotalonhand)
-    . "</th>";
-
-  $column_data{linetotalsellprice} =
-      "<th class=listsubtotal align=right>"
-    . $form->format_amount(\%myconfig, $$subtotalsellprice, 2)
-    . "</th>";
-  $column_data{linetotallistprice} =
-      "<th class=listsubtotal align=right>"
-    . $form->format_amount(\%myconfig, $$subtotallistprice, 2)
-    . "</th>";
-  $column_data{linetotallastcost} =
-      "<th class=listsubtotal align=right>"
-    . $form->format_amount(\%myconfig, $$subtotallastcost, 2)
-    . "</th>";
-
-  $$subtotalonhand    = 0;
-  $$subtotalsellprice = 0;
-  $$subtotallistprice = 0;
-  $$subtotallastcost  = 0;
-
-  print "<tr class=listsubtotal>";
-
-  map { print "\n$column_data{$_}" } @{ $column_index };
-
-  print qq|
-  </tr>
-|;
-
-  $lxdebug->leave_sub();
-}
-
-sub edit {
-  $lxdebug->enter_sub();
-
-  $auth->assert('part_service_assembly_details');
-
-  # show history button
-  $form->{javascript} = qq|<script type="text/javascript" src="js/show_history.js"></script>|;
-  #/show hhistory button
-  IC->get_part(\%myconfig, \%$form);
-
-  $form->{"original_partnumber"} = $form->{"partnumber"};
-
-  my $title      = 'Edit ' . ucfirst $form->{item};
-  $form->{title} = $locale->text($title);
-
-  &link_part;
-  &display_form;
-
-  $lxdebug->leave_sub();
-}
-
-sub link_part {
-  $lxdebug->enter_sub();
-
-  $auth->assert('part_service_assembly_details');
-
-  IC->create_links("IC", \%myconfig, \%$form);
-
-  # currencies
-  map({ $form->{selectcurrency} .= "<option>$_\n" } $::form->get_all_currencies());
-
-  # parts and assemblies have the same links
-  my $item = $form->{item};
-  if ($form->{item} eq 'assembly') {
-    $item = 'part';
-  }
-
-  # build the popup menus
-  $form->{taxaccounts} = "";
-  foreach my $key (keys %{ $form->{IC_links} }) {
-    foreach my $ref (@{ $form->{IC_links}{$key} }) {
-
-      # if this is a tax field
-      if ($key =~ /IC_tax/) {
-        if ($key =~ /\Q$item\E/) {
-          $form->{taxaccounts} .= "$ref->{accno} ";
-          $form->{"IC_tax_$ref->{accno}_description"} =
-            "$ref->{accno}--$ref->{description}";
-
-          if ($form->{id}) {
-            if ($form->{amount}{ $ref->{accno} }) {
-              $form->{"IC_tax_$ref->{accno}"} = "checked";
-            }
-          } else {
-            $form->{"IC_tax_$ref->{accno}"} = "checked";
-          }
-        }
-      } else {
-
-        $form->{"select$key"} .=
-          "<option $ref->{selected}>$ref->{accno}--$ref->{description}\n";
-        if ($form->{amount}{$key} eq $ref->{accno}) {
-          $form->{$key} = "$ref->{accno}--$ref->{description}";
-        }
-
-      }
-    }
-  }
-  chop $form->{taxaccounts};
-
-  if (($form->{item} eq "part") || ($form->{item} eq "assembly")) {
-    $form->{selectIC_income}  = $form->{selectIC_sale};
-    $form->{selectIC_expense} = $form->{selectIC_cogs};
-    $form->{IC_income}        = $form->{IC_sale};
-    $form->{IC_expense}       = $form->{IC_cogs};
-  }
-
-  delete $form->{IC_links};
-  delete $form->{amount};
-
-  $form->get_partsgroup(\%myconfig, { all => 1 });
-
-  $form->{partsgroup} = "$form->{partsgroup}--$form->{partsgroup_id}";
-
-  if (@{ $form->{all_partsgroup} }) {
-    $form->{selectpartsgroup} = qq|<option>\n|;
-    map { $form->{selectpartsgroup} .= qq|<option value="$_->{partsgroup}--$_->{id}">$_->{partsgroup}\n| } @{ $form->{all_partsgroup} };
-  }
-
-  if ($form->{item} eq 'assembly') {
-
-    foreach my $i (1 .. $form->{assembly_rows}) {
-      if ($form->{"partsgroup_id_$i"}) {
-        $form->{"partsgroup_$i"} =
-          qq|$form->{"partsgroup_$i"}--$form->{"partsgroup_id_$i"}|;
-      }
-    }
-    $form->get_partsgroup(\%myconfig);
-
-    if (@{ $form->{all_partsgroup} }) {
-      $form->{selectassemblypartsgroup} = qq|<option>\n|;
-
-      map {
-        $form->{selectassemblypartsgroup} .=
-          qq|<option value="$_->{partsgroup}--$_->{id}">$_->{partsgroup}\n|
-      } @{ $form->{all_partsgroup} };
-    }
-  }
-  $lxdebug->leave_sub();
-}
-
-sub form_header {
-  $lxdebug->enter_sub();
-
-  $auth->assert('part_service_assembly_details');
-
-  $form->{pg_keys}          = sub { "$_[0]->{partsgroup}--$_[0]->{id}" };
-  $form->{description_area} = ($form->{rows} = $form->numtextrows($form->{description}, 40)) > 1;
-  $form->{notes_rows}       =  max 4, $form->numtextrows($form->{notes}, 40), $form->numtextrows($form->{formel}, 40);
-
-  map { $form->{"is_$_"}  = ($form->{item} eq $_) } qw(part service assembly);
-  map { $form->{$_}       =~ s/"/&quot;/g;        } qw(unit);
-
-  $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS',
-                   'partsgroup'    => 'all_partsgroup',
-                   'vendors'       => 'ALL_VENDORS',
-                   'warehouses'    => { 'key'    => 'WAREHOUSES',
-                                        'bins'   => 'BINS', });
-  # leerer wert für Lager und Lagerplatz korrekt einstellt
-  # ID 0 sollte in Ordnung sein, da der Zähler sowieso höher ist
-  my $no_default_bin_entry = { 'id' => '0', description => '--', 'BINS' => [ { id => '0', description => ''} ] };
-  push @ { $form->{WAREHOUSES} }, $no_default_bin_entry;
-  if (my $max = scalar @{ $form->{WAREHOUSES} }) {
-    my ($default_warehouse_id, $default_bin_id);
-    if ($form->{action} eq 'add') { # default only for new entries
-      $default_warehouse_id = $::instance_conf->get_warehouse_id;
-      $default_bin_id       = $::instance_conf->get_bin_id;
-    }
-    $form->{warehouse_id} ||= $default_warehouse_id || $form->{WAREHOUSES}->[$max -1]->{id};
-    $form->{bin_id}       ||= $default_bin_id       ||  $form->{WAREHOUSES}->[$max -1]->{BINS}->[0]->{id};
-  }
-
-  $form->{LANGUAGES}        = SL::DB::Manager::Language->get_all_sorted;
-  $form->{translations_map} = { map { ($_->{language_id} => $_) } @{ $form->{translations} || [] } };
-
-  IC->retrieve_buchungsgruppen(\%myconfig, $form);
-  @{ $form->{BUCHUNGSGRUPPEN} } = grep { $_->{id} eq $form->{buchungsgruppen_id} || ($form->{id} && $form->{orphaned}) || !$form->{id} } @{ $form->{BUCHUNGSGRUPPEN} };
-
-  if (($form->{partnumber} ne '') && !SL::TransNumber->new(number => $form->{partnumber}, type => $form->{item}, id => $form->{id})->is_unique) {
-    flash('info', $::locale->text('This partnumber is not unique. You should change it.'));
-  }
-
-  my $units = AM->retrieve_units(\%myconfig, $form);
-  $form->{ALL_UNITS} = [ map +{ name => $_ }, sort { $units->{$a}{sortkey} <=> $units->{$b}{sortkey} } keys %$units ];
-
-  $form->{defaults} = AM->get_defaults();
-
-  $form->{CUSTOM_VARIABLES} = CVar->get_custom_variables('module' => 'IC', 'trans_id' => $form->{id});
-
-  my ($null, $partsgroup_id) = split /--/, $form->{partsgroup};
-
-  CVar->render_inputs('variables' => $form->{CUSTOM_VARIABLES}, show_disabled_message => 1, partsgroup_id => $partsgroup_id)
-    if (scalar @{ $form->{CUSTOM_VARIABLES} });
-
-  $::request->layout->use_javascript("${_}.js") for qw(ckeditor/ckeditor ckeditor/adapters/jquery kivi.PriceRule);
-  $::request->layout->add_javascripts_inline("\$(function(){kivi.PriceRule.load_price_rules_for_part(@{[ $::form->{id} * 1 ]})});") if $::form->{id};
-  $form->header;
-  #print $form->parse_html_template('ic/form_header', { ALL_PRICE_FACTORS => $form->{ALL_PRICE_FACTORS},
-  #                                                     ALL_UNITS         => $form->{ALL_UNITS},
-  #                                                     BUCHUNGSGRUPPEN   => $form->{BUCHUNGSGRUPPEN},
-  #                                                     payment_terms     => $form->{payment_terms},
-  #                                                     all_partsgroup    => $form->{all_partsgroup}});
-
-  $form->{show_edit_buttons} = $main::auth->check_right($::myconfig{login}, 'part_service_assembly_edit');
-
-  print $form->parse_html_template('ic/form_header');
-  $lxdebug->leave_sub();
-}
-
-sub form_footer {
-  $lxdebug->enter_sub();
-
-  $auth->assert('part_service_assembly_details');
-
-  print $form->parse_html_template('ic/form_footer');
-
-  $lxdebug->leave_sub();
-}
-
-sub makemodel_row {
-  $lxdebug->enter_sub();
-  my ($numrows) = @_;
-  #hli
-  my @mm_data = grep { any { $_ ne '' } @$_{qw(make model)} } map +{ make => $form->{"make_$_"}, model => $form->{"model_$_"}, lastcost => $form->{"lastcost_$_"}, lastupdate => $form->{"lastupdate_$_"}, sortorder => $form->{"sortorder_$_"} }, 1 .. $numrows;
-  delete @{$form}{grep { m/^make_\d+/ || m/^model_\d+/ } keys %{ $form }};
-  print $form->parse_html_template('ic/makemodel', { MM_DATA => [ @mm_data, {} ], mm_rows => scalar @mm_data + 1 });
-
-  $lxdebug->leave_sub();
-}
-
-sub assembly_row {
-  $lxdebug->enter_sub();
-  my ($numrows) = @_;
-  my (@column_index);
-  my ($nochange, $callback, $previousform, $linetotal, $line_purchase_price, $href);
-
-  @column_index = qw(runningnumber qty unit bom partnumber description partsgroup lastcost total);
-
-  if ($form->{previousform}) {
-    $nochange     = 1;
-    @column_index = qw(qty unit bom partnumber description partsgroup total);
-  } else {
-
-    # change callback
-    $form->{old_callback} = $form->{callback};
-    $callback             = $form->{callback};
-    $form->{callback}     = "$form->{script}?action=display_form";
-
-    # delete action
-    map { delete $form->{$_} } qw(action header);
-
-    # save form variables in a previousform variable
-    my %form_to_save = map   { ($_ => m/^ (?: listprice | sellprice | lastcost ) $/x ? $form->format_amount(\%myconfig, $form->{$_}) : $form->{$_}) }
-                       keys %{ $form };
-    $previousform    = $::auth->save_form_in_session(form => \%form_to_save);
-
-    $form->{callback} = $callback;
-    $form->{assemblytotal} = 0;
-    $form->{assembly_purchase_price_total} = 0;
-    $form->{weight}        = 0;
-  }
-
-  my %header = (
-   runningnumber => { text =>  $locale->text('No.'),              nowrap => 1, width => '5%',  align => 'left',},
-   qty           => { text =>  $locale->text('Qty'),              nowrap => 1, width => '10%', align => 'left',},
-   unit          => { text =>  $locale->text('Unit'),             nowrap => 1, width => '5%',  align => 'left',},
-   partnumber    => { text =>  $locale->text('Part Number'),      nowrap => 1, width => '20%', align => 'left',},
-   description   => { text =>  $locale->text('Part Description'), nowrap => 1, width => '50%', align => 'left',},
-   lastcost      => { text =>  $locale->text('Purchase Prices'),  nowrap => 1, width => '50%', align => 'right',},
-   total         => { text =>  $locale->text('Sale Prices'),      nowrap => 1,                 align => 'right',},
-   bom           => { text =>  $locale->text('BOM'),                                           align => 'center',},
-   partsgroup    => { text =>  $locale->text('Group'),                                         align => 'left',},
-  );
-
-  my @ROWS;
-
-  for my $i (1 .. $numrows) {
-    my (%row, @row_hiddens);
-
-    $form->{"partnumber_$i"} =~ s/\"/&quot;/g;
-
-    $linetotal           = $form->round_amount($form->{"sellprice_$i"} * $form->{"qty_$i"} / ($form->{"price_factor_$i"} || 1), 4);
-    $line_purchase_price = $form->round_amount($form->{"lastcost_$i"} *  $form->{"qty_$i"} / ($form->{"price_factor_$i"} || 1), 4);
-    $form->{assemblytotal}                  += $linetotal;
-    $form->{assembly_purchase_price_total}  += $line_purchase_price;
-    $form->{"qty_$i"}    = $form->format_amount(\%myconfig, $form->{"qty_$i"});
-    $linetotal           = $form->format_amount(\%myconfig, $linetotal, 2);
-    $line_purchase_price = $form->format_amount(\%myconfig, $line_purchase_price, 2);
-    $href                = build_std_url("action=edit", qq|id=$form->{"id_$i"}|, "rowcount=$numrows", "currow=$i", "previousform=$previousform");
-    map { $row{$_}{data} = "" } qw(qty unit partnumber description bom partsgroup runningnumber);
-
-    # last row
-    if (($i >= 1) && ($i == $numrows)) {
-      if (!$form->{previousform}) {
-        $row{partnumber}{data}  = qq|<input name="partnumber_$i" size=15 value="$form->{"partnumber_$i"}">|;
-        $row{qty}{data}         = qq|<input name="qty_$i" size=5 value="$form->{"qty_$i"}">|;
-        $row{description}{data} = qq|<input name="description_$i" size=40 value="$form->{"description_$i"}">|;
-        $row{partsgroup}{data}  = qq|<input name="partsgroup_$i" size=10 value="$form->{"partsgroup_$i"}">|;
-      }
-    # other rows
-    } else {
-      if ($form->{previousform}) {
-        push @row_hiddens,          qw(qty bom);
-        $row{partnumber}{data}    = $form->{"partnumber_$i"};
-        $row{qty}{data}           = $form->{"qty_$i"};
-        $row{bom}{data}           = $form->{"bom_$i"} ? "x" : "&nbsp;";
-        $row{qty}{align}          = 'right';
-      } else {
-        $row{partnumber}{data}    = qq|$form->{"partnumber_$i"}|;
-        $row{partnumber}{link}     = $href;
-        $row{qty}{data}           = qq|<input name="qty_$i" size=5 value="$form->{"qty_$i"}">|;
-        $row{runningnumber}{data} = qq|<input name="runningnumber_$i" size=3 value="$i">|;
-        $row{bom}{data}   = sprintf qq|<input name="bom_$i" type=checkbox class=checkbox value=1 %s>|,
-                                       $form->{"bom_$i"} ? 'checked' : '';
-      }
-      push @row_hiddens,        qw(unit description partnumber partsgroup);
-      $row{unit}{data}        = $form->{"unit_$i"};
-      #Bei der Artikelbeschreibung und Warengruppe können Sonderzeichen verwendet
-      #werden, die den HTML Code stören. Daher sollen diese im Template escaped werden
-      #dies geschieht, wenn die Variable escape gesetzt ist
-      $row{description}{data}   = $form->{"description_$i"};
-      $row{description}{escape} = 1;
-      $row{partsgroup}{data}    = $form->{"partsgroup_$i"};
-      $row{partsgroup}{escape}  = 1;
-      $row{bom}{align}          = 'center';
-    }
-
-    $row{lastcost}{data}      = $line_purchase_price;
-    $row{total}{data}         = $linetotal;
-    $row{lastcost}{align}     = 'right';
-    $row{total}{align}        = 'right';
-    $row{deliverydate}{align} = 'right';
-
-    push @row_hiddens, qw(id sellprice lastcost weight price_factor_id price_factor);
-    $row{hiddens} = [ map +{ name => "${_}_$i", value => $form->{"${_}_$i"} }, @row_hiddens ];
-
-    push @ROWS, \%row;
-  }
-
-  print $form->parse_html_template('ic/assembly_row', { COLUMNS => \@column_index, ROWS => \@ROWS, HEADER => \%header });
-
-  $lxdebug->leave_sub();
-}
-
-sub update {
-  $lxdebug->enter_sub();
-
-  $auth->assert('part_service_assembly_edit');
-
-  # update checks whether pricegroups, makemodels or assembly items have been changed/added
-  # new items might have been added (and the original form might have been stored and restored)
-  # so at the end the ic form is run through check_form in io.pl
-  # The various combination of events can lead to problems with the order of parse_amount and format_amount
-  # Currently check_form parses some variables in assembly mode, but not in article or service mode
-  # This will only ever really be sanely resolved with a rewrite...
-
-  # parse pricegroups. and no, don't rely on check_form for this...
-  map { $form->{"price_$_"} = $form->parse_amount(\%myconfig, $form->{"price_$_"}) } 1 .. $form->{price_rows};
-
-  unless ($form->{item} eq 'assembly') {
-    # for assemblies check_form will parse sellprice and listprice, but not for parts or services
-    $form->{$_} = $form->parse_amount(\%myconfig, $form->{$_}) for qw(sellprice listprice ve gv);
-  };
-
-  if ($form->{item} eq 'part') {
-    $form->{$_} = $form->parse_amount(\%myconfig, $form->{$_}) for qw(weight rop);
-  }
-
-  # same for makemodel lastcosts
-  # but parse_amount not necessary for assembly component lastcosts
-  unless ($form->{item} eq "assembly") {
-    map { $form->{"lastcost_$_"} = $form->parse_amount(\%myconfig, $form->{"lastcost_$_"}) } 1 .. $form->{"makemodel_rows"};
-    $form->{lastcost} = $form->parse_amount(\%myconfig, $form->{lastcost});
-  }
-
-  if ($form->{item} eq "assembly") {
-    my $i = $form->{assembly_rows};
-
-    # if last row is empty check the form otherwise retrieve item
-    if (   ($form->{"partnumber_$i"} eq "")
-        && ($form->{"description_$i"} eq "")
-        && ($form->{"partsgroup_$i"}  eq "")) {
-      # no new assembly item was added
-
-      &check_form;
-
-    } else {
-      # search db for newly added assemblyitems, via partnumber or description
-      IC->assembly_item(\%myconfig, \%$form);
-
-      # form->{item_list} contains the possible matches, next check whether the
-      # match is unique or we need to call the page to select the item
-      my $rows = scalar @{ $form->{item_list} };
-
-      if ($rows) {
-        $form->{"qty_$i"} = 1 unless ($form->{"qty_$i"});
-
-        if ($rows > 1) {
-          $form->{makemodel_rows}--;
-          select_item(mode => 'IC', pre_entered_qty => $form->parse_amount(\%myconfig, $form->{"qty_$i"}));
-          ::end_of_request();
-        } else {
-          map { $form->{item_list}[$i]{$_} =~ s/\"/&quot;/g }
-            qw(partnumber description unit partsgroup);
-          map { $form->{"${_}_$i"} = $form->{item_list}[0]{$_} }
-            keys %{ $form->{item_list}[0] };
-          $form->{"runningnumber_$i"} = $form->{assembly_rows};
-          $form->{assembly_rows}++;
-
-          &check_form;
-
-        }
-
-      } else {
-
-        $form->{rowcount} = $i;
-        $form->{assembly_rows}++;
-
-        &new_item;
-
-      }
-    }
-
-  } elsif (($form->{item} eq 'part') || ($form->{item} eq 'service')) {
-    &check_form;
-  }
-
-  $lxdebug->leave_sub();
-}
-
-sub save {
-  $lxdebug->enter_sub();
-
-  $auth->assert('part_service_assembly_edit');
-  $::form->mtime_ischanged('parts');
-  my ($parts_id, %newform, $amount, $callback);
-
-  # check if there is a part number - commented out, cause there is an automatic allocation of numbers
-  # $form->isblank("partnumber", $locale->text(ucfirst $form->{item}." Part Number missing!"));
-
-  # check if there is a description
-  $form->isblank("description", $locale->text("Part Description missing!"));
-
-  $form->error($locale->text("Inventory quantity must be zero before you can set this $form->{item} obsolete!"))
-    if $form->{obsolete} && $form->{onhand} * 1 && $form->{item} ne 'service';
-
-  if (!$form->{buchungsgruppen_id}) {
-    $form->error($locale->text("Parts must have an entry type.") . " " .
-     $locale->text("If you see this message, you most likely just setup your LX-Office and haven't added any entry types. If this is the case, the option is accessible for administrators in the System menu.")
+sub setup_ic_search_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Search'),
+        submit    => [ '#form', { action => 'generate_report' } ],
+        accesskey => 'enter',
+      ],
+
+      action => [
+        t8('TOP100'),
+        submit => [ '#form', { action => 'top100' } ],
+      ],
     );
   }
-
-  $form->error($locale->text('Description must not be empty!')) unless $form->{description};
-  $form->error($locale->text('Partnumber must not be set to empty!')) if $form->{id} && !$form->{partnumber};
-
-  # undef warehouse_id if the empty value is selected
-  if ( ($form->{warehouse_id} == 0) && ($form->{bin_id} == 0) ) {
-    undef $form->{warehouse_id};
-    undef $form->{bin_id};
-  }
-  # save part
-  if (IC->save(\%myconfig, \%$form) == 3) {
-    $form->error($locale->text('Partnumber not unique!'));
-  }
-  # saving the history
-  if(!exists $form->{addition}) {
-    $form->{snumbers}  = qq|partnumber_| . $form->{partnumber};
-    $form->{what_done} = "part";
-    $form->{addition}  = "SAVED";
-    $form->save_history;
-  }
-  # /saving the history
-  $parts_id = $form->{id};
-
-  my $i;
-  # load previous variables
-  if ($form->{previousform}) {
-
-    # save the new form variables before splitting previousform
-    map { $newform{$_} = $form->{$_} } keys %$form;
-
-    # don't trample on previous variables
-    map { delete $form->{$_} } keys %newform;
-
-    my $ic_cvar_configs = CVar->get_configs(module => 'IC');
-    my @ic_cvar_fields  = map { "cvar_$_->{name}" } @{ $ic_cvar_configs };
-
-    # restore original values
-    $::auth->restore_form_from_session($newform{previousform}, form => $form);
-    $form->{taxaccounts} = $newform{taxaccount2};
-
-    if ($form->{item} eq 'assembly') {
-
-      # undo number formatting
-      map { $form->{$_} = $form->parse_amount(\%myconfig, $form->{$_}) }
-        qw(weight listprice sellprice rop);
-
-      $form->{assembly_rows}--;
-      if ($newform{currow}) {
-        $i = $newform{currow};
-      } else {
-        $i = $form->{assembly_rows};
-      }
-      $form->{"qty_$i"} = 1 unless ($form->{"qty_$i"} > 0);
-
-      $form->{sellprice} -= $form->{"sellprice_$i"} * $form->{"qty_$i"};
-      $form->{weight}    -= $form->{"weight_$i"} * $form->{"qty_$i"};
-
-      # change/add values for assembly item
-      map { $form->{"${_}_$i"} = $newform{$_} } qw(partnumber description bin unit weight listprice sellprice inventory_accno income_accno expense_accno price_factor_id);
-      map { $form->{"ic_${_}_$i"} = $newform{$_} } @ic_cvar_fields;
-
-      # das ist __voll__ bekloppt, dass so auszurechnen jb 22.5.09
-      #$form->{sellprice} += $form->{"sellprice_$i"} * $form->{"qty_$i"};
-      $form->{weight}    += $form->{"weight_$i"} * $form->{"qty_$i"};
-
-    } else {
-
-      # set values for last invoice/order item
-      $i = $form->{rowcount};
-      $form->{"qty_$i"} = 1 unless ($form->{"qty_$i"} > 0);
-
-      map { $form->{"${_}_$i"} = $newform{$_} } qw(partnumber description bin unit listprice inventory_accno income_accno expense_accno sellprice lastcost price_factor_id);
-      map { $form->{"ic_${_}_$i"} = $newform{$_} } @ic_cvar_fields;
-
-      $form->{"longdescription_$i"} = $newform{notes};
-
-      $form->{"sellprice_$i"} = $newform{lastcost} if ($form->{vendor_id});
-
-      if ($form->{exchangerate} != 0) {
-        $form->{"sellprice_$i"} /= $form->{exchangerate};
-      }
-
-      map { $form->{"taxaccounts_$i"} .= "$_ " } split / /, $newform{taxaccount};
-      chop $form->{"taxaccounts_$i"};
-      foreach my $item (qw(description rate taxnumber)) {
-        my $index = $form->{"taxaccounts_$i"} . "_$item";
-        $form->{$index} = $newform{$index};
-      }
-
-      # credit remaining calculation
-      $amount = $form->{"sellprice_$i"} * (1 - $form->{"discount_$i"} / 100) * $form->{"qty_$i"};
-
-      map { $form->{"${_}_base"} += $amount } (split / /, $form->{"taxaccounts_$i"});
-      map { $amount += ($form->{"${_}_base"} * $form->{"${_}_rate"}) } split / /, $form->{"taxaccounts_$i"} if !$form->{taxincluded};
-
-      $form->{creditremaining} -= $amount;
-
-      # redo number formatting, because invoice parse them!
-      map { $form->{"${_}_$i"} = $form->format_amount(\%myconfig, $form->{"${_}_$i"}) } qw(weight listprice sellprice lastcost rop);
-    }
-
-    $form->{"id_$i"} = $parts_id;
-
-    # Get the actual price factor (not just the ID) for the marge calculation.
-    $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS');
-    foreach my $pfac (@{ $form->{ALL_PRICE_FACTORS} }) {
-      next if ($pfac->{id} != $newform{price_factor_id});
-      $form->{"marge_price_factor_$i"} = $pfac->{factor};
-      last;
-    }
-    delete $form->{ALL_PRICE_FACTORS};
-
-    delete $form->{action};
-
-    # restore original callback
-    $callback = $form->unescape($form->{callback});
-    $form->{callback} = $form->unescape($form->{old_callback});
-    delete $form->{old_callback};
-
-    $form->{makemodel_rows}--;
-
-    # put callback together
-    foreach my $key (keys %$form) {
-
-      # do single escape for Apache 2.0
-      my $value = $form->escape($form->{$key}, 1);
-      $callback .= qq|&$key=$value|;
-    }
-    $form->{callback} = $callback;
-  }
-
-  # redirect
-  $form->redirect;
-
-  $lxdebug->leave_sub();
-}
-
-sub save_as_new {
-  $lxdebug->enter_sub();
-
-  $auth->assert('part_service_assembly_edit');
-
-  # saving the history
-  if(!exists $form->{addition}) {
-    $form->{snumbers}  = qq|partnumber_| . $form->{partnumber};
-    $form->{addition}  = "SAVED AS NEW";
-    $form->{what_done} = "part";
-    $form->save_history;
-  }
-  # /saving the history
-  $form->{id} = 0;
-  if ($form->{"original_partnumber"} &&
-      ($form->{"partnumber"} eq $form->{"original_partnumber"})) {
-    $form->{partnumber} = "";
-  }
-  &save;
-  $lxdebug->leave_sub();
 }
 
-sub delete {
-  $lxdebug->enter_sub();
-
-  $auth->assert('part_service_assembly_edit');
-
-  # saving the history
-  if(!exists $form->{addition}) {
-    $form->{snumbers}  = qq|partnumber_| . $form->{partnumber};
-    $form->{addition}  = "DELETED";
-    $form->{what_done} = "part";
-    $form->save_history;
+sub setup_ic_generate_report_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [
+          t8('Add'),
+        ],
+        action => [
+          t8('Add Part'),
+          submit    => [ '#new_form', { action => 'Part/add_part' } ],
+          accesskey => 'enter',
+        ],
+        action => [
+          t8('Add Service'),
+          submit    => [ '#new_form', { action => 'Part/add_service' } ],
+        ],
+        action => [
+          t8('Add Assembly'),
+          submit    => [ '#new_form', { action => 'Part/add_assembly' } ],
+        ],
+        action => [
+          t8('Add Assortment'),
+          submit    => [ '#new_form', { action => 'Part/add_assortment' } ],
+        ],
+      ], # end of combobox "Add part"
+    );
   }
-  # /saving the history
-  my $rc = IC->delete(\%myconfig, \%$form);
-
-  # redirect
-  $form->redirect($locale->text('Item deleted!')) if ($rc > 0);
-  $form->error($locale->text('Cannot delete item!'));
-
-  $lxdebug->leave_sub();
-}
-
-sub price_row {
-  $lxdebug->enter_sub();
-
-  $auth->assert('part_service_assembly_details');
-
-  my ($numrows) = @_;
-
-  my @PRICES = map +{
-    pricegroup    => $form->{"pricegroup_$_"},
-    pricegroup_id => $form->{"pricegroup_id_$_"},
-    price         => $form->{"price_$_"},
-  }, 1 .. $numrows;
-
-  print $form->parse_html_template('ic/price_row', { PRICES => \@PRICES });
-
-  $lxdebug->leave_sub();
-}
-
-sub ajax_autocomplete {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-
-  $form->{column}          = 'description'     unless $form->{column} =~ /^partnumber|description$/;
-  $form->{$form->{column}} = $form->{q}           || '';
-  $form->{limit}           = ($form->{limit} * 1) || 10;
-  $form->{searchitems}   ||= '';
-
-  my @results = IC->all_parts(\%myconfig, $form);
-
-  print $form->ajax_response_header(),
-        $form->parse_html_template('ic/ajax_autocomplete');
-
-  $main::lxdebug->leave_sub();
-}
-
-sub back_to_record {
-  _check_io_auth();
-
-
-  delete @{$::form}{qw(action action_add action_back_to_record back_sub description item notes partnumber sellprice taxaccount2 unit vc)};
-
-  $::auth->restore_form_from_session($::form->{previousform}, clobber => 1);
-  $::form->{rowcount}--;
-  $::form->{action}   = 'display_form';
-  $::form->{callback} = $::form->{script} . '?' . join('&', map { $::form->escape($_) . '=' . $::form->escape($::form->{$_}) } sort keys %{ $::form });
-  $::form->redirect;
-}
-
-sub continue { call_sub($form->{"nextsub"}); }
-
-sub dispatcher {
-  my $action = first { $::form->{"action_${_}"} } qw(add back_to_record);
-  $::form->error($::locale->text('No action defined.')) unless $action;
-
-  $::form->{dispatched_action} = $action;
-  call_sub($action);
 }
index fdffa64..f52cc52 100644 (file)
@@ -87,7 +87,7 @@ sub verify_installation {
 </html>
 |);
 
-  ::end_of_request();
+  $::dispatcher->end_request;
 }
 
 1;
diff --git a/bin/mozilla/invoice_io.pl b/bin/mozilla/invoice_io.pl
deleted file mode 100644 (file)
index a4e50ff..0000000
+++ /dev/null
@@ -1,188 +0,0 @@
-#=====================================================================
-# LX-Office ERP
-# Copyright (C) 2004
-# Based on SQL-Ledger Version 2.1.9
-# Web http://www.lx-office.org
-#############################################################################
-# Veraendert 2005-01-05 - Marco Welter <mawe@linux-studio.de> - Neue Optik  #
-#############################################################################
-# SQL-Ledger, Accounting
-# Copyright (c) 1998-2002
-#
-#  Author: Dieter Simader
-#   Email: dsimader@sql-ledger.org
-#     Web: http://www.sql-ledger.org
-#
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-#
-#######################################################################
-#
-# common routines used in is, ir but not in oe
-#
-#######################################################################
-
-use CGI;
-use List::Util qw(max);
-
-use SL::Common;
-use SL::CT;
-use SL::IC;
-
-require "bin/mozilla/common.pl";
-
-use strict;
-
-# any custom scripts for this one
-if (-f "bin/mozilla/custom_invoice_io.pl") {
-  eval { require "bin/mozilla/custom_invoice_io.pl"; };
-}
-if (-f "bin/mozilla/$::myconfig{login}_invoice_io.pl") {
-  eval { require "bin/mozilla/$::myconfig{login}_invoice_io.pl"; };
-}
-
-1;
-
-# end of main
-
-# this is for our long dates
-# $locale->text('January')
-# $locale->text('February')
-# $locale->text('March')
-# $locale->text('April')
-# $locale->text('May ')
-# $locale->text('June')
-# $locale->text('July')
-# $locale->text('August')
-# $locale->text('September')
-# $locale->text('October')
-# $locale->text('November')
-# $locale->text('December')
-
-# this is for our short month
-# $locale->text('Jan')
-# $locale->text('Feb')
-# $locale->text('Mar')
-# $locale->text('Apr')
-# $locale->text('May')
-# $locale->text('Jun')
-# $locale->text('Jul')
-# $locale->text('Aug')
-# $locale->text('Sep')
-# $locale->text('Oct')
-# $locale->text('Nov')
-# $locale->text('Dec')
-use SL::IS;
-use SL::PE;
-use SL::AM;
-use Data::Dumper;
-
-sub display_form {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-
-  $main::auth->assert('part_service_assembly_edit   | vendor_invoice_edit       | sales_order_edit    | invoice_edit |' .
-                'request_quotation_edit       | sales_quotation_edit      | purchase_order_edit | '.
-                'purchase_delivery_order_edit | sales_delivery_order_edit | part_service_assembly_details');
-
-  relink_accounts();
-  retrieve_partunits() if ($form->{type} =~ /_delivery_order$/);
-
-  my $new_rowcount = $form->{"rowcount"} * 1 + 1;
-  $form->{"project_id_${new_rowcount}"} = $form->{"globalproject_id"};
-
-  $form->language_payment(\%myconfig);
-
-  # if we have a display_form
-  if ($form->{display_form}) {
-    call_sub($form->{"display_form"});
-    ::end_of_request();
-  }
-
-  Common::webdav_folder($form);
-
-  #   if (   $form->{print_and_post}
-  #       && $form->{second_run}
-  #       && ($form->{action} eq "display_form")) {
-  #     for (keys %$form) { $old_form->{$_} = $form->{$_} }
-  #     $old_form->{rowcount}++;
-  #
-  #     #$form->{rowcount}--;
-  #     #$form->{rowcount}--;
-  #
-  #     $form->{print_and_post} = 0;
-  #
-  #     &print_form($old_form);
-  #     ::end_of_request();
-  #   }
-  #
-  #   $form->{action}   = "";
-  #   $form->{resubmit} = 0;
-  #
-  #   if ($form->{print_and_post} && !$form->{second_run}) {
-  #     $form->{second_run} = 1;
-  #     $form->{action}     = "display_form";
-  #     $form->{rowcount}--;
-  #     my $rowcount = $form->{rowcount};
-  #
-  #     $form->{resubmit} = 1;
-  #
-  #   }
-  &form_header;
-
-  {
-    no strict 'refs';
-
-    my $numrows    = ++$form->{rowcount};
-    my $subroutine = "display_row";
-
-    if ($form->{item} =~ /(part|service)/) {
-      #set preisgruppenanzahl
-      $numrows    = $form->{price_rows};
-      $subroutine = "price_row";
-
-      &{$subroutine}($numrows);
-
-      $numrows    = ++$form->{makemodel_rows};
-      $subroutine = "makemodel_row";
-    }
-    if ($form->{item} eq 'assembly') {
-      $numrows    = $form->{price_rows};
-      $subroutine = "price_row";
-
-      &{$subroutine}($numrows);
-
-      $numrows    = ++$form->{makemodel_rows};
-      $subroutine = "makemodel_row";
-
-      # assemblies are built from components, they aren't purchased from a vendor
-      # also the lastcost_$i from makemodel conflicted with the component lastcost_$i
-      # so we don't need the makemodel rows for assemblies
-      # create makemodel rows
-      # &{$subroutine}($numrows);
-
-      $numrows    = ++$form->{assembly_rows};
-      $subroutine = "assembly_row";
-    }
-
-    # create rows
-    &{$subroutine}($numrows) if $numrows;
-  }
-
-  &form_footer;
-
-  $main::lxdebug->leave_sub();
-}
index f42db46..793099f 100644 (file)
@@ -28,7 +28,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #
 #######################################################################
 #
@@ -39,7 +40,8 @@
 use Carp;
 use CGI;
 use List::MoreUtils qw(any uniq apply);
-use List::Util qw(min max first);
+use List::Util qw(sum min max first);
+use List::UtilsBy qw(sort_by uniq_by);
 
 use SL::ClientJS;
 use SL::CVar;
@@ -49,15 +51,24 @@ use SL::CT;
 use SL::Locale::String qw(t8);
 use SL::IC;
 use SL::IO;
+use SL::File;
 use SL::PriceSource;
+use SL::Presenter::Part;
+use SL::Util qw(trim);
 
+use SL::DB::AuthUser;
+use SL::DB::Contact;
+use SL::DB::Currency;
 use SL::DB::Customer;
+use SL::DB::DeliveryOrder::TypeData qw();
 use SL::DB::Default;
 use SL::DB::Language;
 use SL::DB::Printer;
 use SL::DB::Vendor;
 use SL::Helper::CreatePDF;
 use SL::Helper::Flash;
+use SL::Helper::PrintOptions;
+use SL::Helper::ShippedQty;
 
 require "bin/mozilla/common.pl";
 
@@ -103,7 +114,6 @@ if (-f "bin/mozilla/$::myconfig{login}_io.pl") {
 # $locale->text('Nov')
 # $locale->text('Dec')
 use SL::IS;
-use SL::PE;
 use SL::AM;
 use Data::Dumper;
 
@@ -120,8 +130,6 @@ sub _check_io_auth {
 sub display_row {
   $main::lxdebug->enter_sub();
 
-  _check_io_auth();
-
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
@@ -136,7 +144,6 @@ sub display_row {
   $form->{weightunit} = $defaults->{weightunit};
 
   my $is_purchase        = (first { $_ eq $form->{type} } qw(request_quotation purchase_order purchase_delivery_order)) || ($form->{script} eq 'ir.pl');
-  my $show_min_order_qty =  first { $_ eq $form->{type} } qw(request_quotation purchase_order);
   my $is_delivery_order  = $form->{type} =~ /_delivery_order$/;
   my $is_quotation       = $form->{type} =~ /_quotation$/;
   my $is_invoice         = $form->{type} =~ /invoice/;
@@ -159,16 +166,24 @@ sub display_row {
 
   # column_index
   my @header_sort = qw(
-    runningnumber partnumber description ship ship_missing qty price_factor
+    runningnumber partnumber type_and_classific description ship ship_missing qty price_factor
     unit weight price_source sellprice discount linetotal
     bin stock_in_out
   );
   my @row2_sort   = qw(
     serialnr projectnr reqdate subtotal marge listprice lastcost onhand
   );
+  # serialnr is important for delivery_orders
+  if ($form->{type} eq 'sales_delivery_order') {
+    splice @row2_sort, 0, 1;
+    splice @header_sort, 4, 0, "serialnr";
+  }
+
   my %column_def = (
     runningnumber => { width => 5,     value => $locale->text('No.'),                  display => 1, },
     partnumber    => { width => 8,     value => $locale->text('Number'),               display => 1, },
+    type_and_classific
+                  => { width => 2,     value => $locale->text('Type'),                 display => 1, },
     description   => { width => 30,    value => $locale->text('Part Description'),     display => 1, },
     ship          => { width => 5,     value => $locale->text('Delivered'),            display => $is_s_p_order, },
     ship_missing  => { width => 5,     value => $locale->text('Not delivered'),        display => $show_ship_missing, },
@@ -212,7 +227,7 @@ sub display_row {
 
   # special alignings
   my %align  = map { $_ => 'right' } qw(qty ship right discount linetotal stock_in_out weight ship_missing);
-  my %nowrap = map { $_ => 1 }       qw(description unit);
+  my %nowrap = map { $_ => 1 }       qw(description unit  price_source);
 
   $form->{marge_total}           = 0;
   $form->{sellprice_total}       = 0;
@@ -295,12 +310,14 @@ sub display_row {
     my $rows            = $form->numtextrows($form->{"description_$i"}, 30, 6);
 
     # quick delete single row
-    $column_data{runningnumber} .= q|<a onclick= "$('#partnumber_| . $i . q|').val(''); $('#update_button').click();">| .
+    $column_data{runningnumber}  = q|<a onclick= "$('#partnumber_| . $i . q|').val(''); $('#update_button').click();">| .
                                    q|<img height="10px" width="10px" src="image/cross.png" alt="| . $locale->text('Remove') . q|"></a> |;
     $column_data{runningnumber} .= $cgi->textfield(-name => "runningnumber_$i", -id => "runningnumber_$i", -size => 5,  -value => $i);    # HuT
 
 
     $column_data{partnumber}    = $cgi->textfield(-name => "partnumber_$i",    -id => "partnumber_$i",    -size => 12, -value => $form->{"partnumber_$i"});
+    $column_data{type_and_classific} = SL::Presenter::Part::type_abbreviation($form->{"part_type_$i"}).
+                                       SL::Presenter::Part::classification_abbreviation($form->{"classification_id_$i"}) if $form->{"id_$i"};
     $column_data{description} = (($rows > 1) # if description is too large, use a textbox instead
                                 ? $cgi->textarea( -name => "description_$i", -id => "description_$i", -default => $form->{"description_$i"}, -rows => $rows, -columns => 30)
                                 : $cgi->textfield(-name => "description_$i", -id => "description_$i",   -value => $form->{"description_$i"}, -size => 30))
@@ -308,9 +325,9 @@ sub display_row {
 
     my $qty_dec = ($form->{"qty_$i"} =~ /\.(\d+)/) ? length $1 : 2;
 
-    $column_data{qty}  = $cgi->textfield(-name => "qty_$i", -size => 5, -value => $form->format_amount(\%myconfig, $form->{"qty_$i"}, $qty_dec));
-    $column_data{qty} .= $cgi->button(-onclick => "calculate_qty_selection_window('qty_$i','alu_$i', 'formel_$i', $i)", -value => $locale->text('*/'))
-                       . $cgi->hidden(-name => "formel_$i", -value => $form->{"formel_$i"}) . $cgi->hidden("-name" => "alu_$i", "-value" => $form->{"alu_$i"})
+    $column_data{qty}  = $cgi->textfield(-name => "qty_$i", -size => 5, -class => "numeric", -value => $form->format_amount(\%myconfig, $form->{"qty_$i"}, $qty_dec));
+    $column_data{qty} .= $cgi->button(-onclick => "calculate_qty_selection_dialog('qty_$i', '', 'formel_$i', '')", -value => $locale->text('*/'))
+                       . $cgi->hidden(-name => "formel_$i", -value => $form->{"formel_$i"})
       if $form->{"formel_$i"};
 
     $column_data{ship} = '';
@@ -319,7 +336,8 @@ sub display_row {
       $ship_qty          *= $all_units->{$form->{"partunit_$i"}}->{factor};
       $ship_qty          /= ( $all_units->{$form->{"unit_$i"}}->{factor} || 1 );
 
-      $column_data{ship}  = $form->format_amount(\%myconfig, $form->round_amount($ship_qty, 2) * 1) . ' ' . $form->{"unit_$i"};
+      $column_data{ship}  = $form->format_amount(\%myconfig, $form->round_amount($ship_qty, 2) * 1) . ' ' . $form->{"unit_$i"}
+      . $cgi->hidden(-name => "ship_$i", -value => $form->{"ship_$i"}, $qty_dec);
 
       my $ship_missing_qty    = $form->{"qty_$i"} - $ship_qty;
       my $ship_missing_amount = $form->round_amount($ship_missing_qty * $form->{"sellprice_$i"} * (100 - $form->{"discount_$i"}) / 100 / $price_factor, 2);
@@ -327,24 +345,18 @@ sub display_row {
       $column_data{ship_missing} = $form->format_amount(\%myconfig, $ship_missing_qty) . ' ' . $form->{"unit_$i"} . '; ' . $form->format_amount(\%myconfig, $ship_missing_amount, $decimalplaces);
     }
 
-    my $sellprice_value = $form->format_amount(\%myconfig, $form->{"sellprice_$i"}, $decimalplaces);
-    my $discount_value  = $form->format_amount(\%myconfig, $form->{"discount_$i"});
-    my $edit_prices     = $main::auth->assert('edit_prices', 1) && !$::form->{"active_price_source_$i"};
-    my $edit_discounts  = $main::auth->assert('edit_prices', 1) && !$::form->{"active_discount_source_$i"};
-    $column_data{sellprice}   = (!$edit_prices)
-                                ? $cgi->hidden(   -name => "sellprice_$i", -id => "sellprice_$i", -value => $sellprice_value) . $sellprice_value
-                                : $cgi->textfield(-name => "sellprice_$i", -id => "sellprice_$i", -size => 10, -onBlur => "check_right_number_format(this)", -value => $sellprice_value);
-    $column_data{discount}    = (!$edit_discounts)
-                                  ? $cgi->hidden(   -name => "discount_$i", -id => "discount_$i", -value => $discount_value) . $discount_value . ' %'
-                                  : $cgi->textfield(-name => "discount_$i", -id => "discount_$i", -size => 3, -value => $discount_value);
     $column_data{linetotal}   = $form->format_amount(\%myconfig, $linetotal, 2);
     $column_data{bin}         = $form->{"bin_$i"};
 
     $column_data{weight}      = $form->format_amount(\%myconfig, $form->{"qty_$i"} * $form->{"weight_$i"}, 3) . ' ' . $defaults->{weightunit} if $defaults->{show_weight};
 
+    my $sellprice_value = $form->format_amount(\%myconfig, $form->{"sellprice_$i"}, $decimalplaces);
+    my $discount_value  = $form->format_amount(\%myconfig, $form->{"discount_$i"});
+
+    my $price;
     if ($form->{"id_${i}"} && !$is_delivery_order) {
       my $price_source  = SL::PriceSource->new(record_item => $record_item, record => $record);
-      my $price         = $price_source->price_from_source($::form->{"active_price_source_$i"});
+         $price         = $price_source->price_from_source($::form->{"active_price_source_$i"});
       my $discount      = $price_source->discount_from_source($::form->{"active_discount_source_$i"});
       my $best_price    = $price_source->best_price;
       my $best_discount = $price_source->best_discount;
@@ -369,6 +381,16 @@ sub display_row {
       }
     }
 
+    my $right_to_edit_prices  = (!$is_purchase && $main::auth->assert('sales_edit_prices', 1)) || ($is_purchase && $main::auth->assert('purchase_edit_prices', 1));
+    my $edit_prices           = $right_to_edit_prices && (!$::form->{"active_price_source_$i"} || !$price || $price->editable);
+    my $edit_discounts        = $right_to_edit_prices && !$::form->{"active_discount_source_$i"};
+    $column_data{sellprice}   = (!$edit_prices)
+                                ? $cgi->hidden(   -name => "sellprice_$i", -id => "sellprice_$i", -value => $sellprice_value) . $sellprice_value
+                                : $cgi->textfield(-name => "sellprice_$i", -id => "sellprice_$i", -size => 10, -class => "numeric", -value => $sellprice_value);
+    $column_data{discount}    = (!$edit_discounts)
+                                  ? $cgi->hidden(   -name => "discount_$i", -id => "discount_$i", -value => $discount_value) . $discount_value . ' %'
+                                  : $cgi->textfield(-name => "discount_$i", -id => "discount_$i", -size => 3, -"data-validate" => "number", -class => "numeric", -value => $discount_value);
+
     if ($is_delivery_order) {
       $column_data{stock_in_out} =  calculate_stock_in_out($i);
     }
@@ -380,7 +402,7 @@ sub display_row {
       '-labels' => \%projectnumber_labels,
       '-default' => $form->{"project_id_$i"}
     ));
-    $column_data{reqdate}   = qq|<input name="reqdate_$i" size="11" onBlur="check_right_date_format(this)" value="$form->{"reqdate_$i"}">|;
+    $column_data{reqdate}   = qq|<input name="reqdate_$i" size="11" data-validate="date" value="$form->{"reqdate_$i"}">|;
     $column_data{subtotal}  = sprintf qq|<input type="checkbox" name="subtotal_$i" value="1" %s>|, $form->{"subtotal_$i"} ? 'checked' : '';
 
 # begin marge calculations
@@ -458,7 +480,7 @@ sub display_row {
       map { $form->{"${_}_${i}"} = $form->format_amount(\%myconfig, $form->{"${_}_${i}"}) } qw(sellprice discount lastcost);
       push @hidden_vars, grep { defined $form->{"${_}_${i}"} } qw(sellprice discount not_discountable price_factor_id lastcost);
       push @hidden_vars, "stock_${stock_in_out}_sum_qty", "stock_${stock_in_out}";
-      push @hidden_vars, qw(delivery_order_items_id converted_from_orderitems_id converted_from_delivery_order_items_id);
+      push @hidden_vars, qw(delivery_order_items_id converted_from_orderitems_id converted_from_delivery_order_items_id has_sernumber);
     }
 
     my @HIDDENS = map { value => $_}, (
@@ -466,7 +488,7 @@ sub display_row {
           $cgi->hidden("-name" => "price_new_$i", "-value" => $form->format_amount(\%myconfig, $form->{"price_new_$i"})),
           map { ($cgi->hidden("-name" => $_, "-id" => $_, "-value" => $form->{$_})); } map { $_."_$i" }
             (qw(bo price_old id inventory_accno bin partsgroup partnotes active_price_source active_discount_source
-                income_accno expense_accno listprice assembly taxaccounts ordnumber donumber transdate cusordnumber
+                income_accno expense_accno listprice part_type taxaccounts ordnumber donumber transdate cusordnumber
                 longdescription basefactor marge_absolut marge_percent marge_price_factor weight), @hidden_vars)
     );
 
@@ -487,13 +509,27 @@ sub display_row {
                                                        HEADER => \@HEADER,
                                                      });
 
-  if (0 != ($form->{sellprice_total} * 1)) {
+  if (abs($form->{sellprice_total} * 1) >= 0.01) {
     $form->{marge_percent} = ($form->{sellprice_total} - $form->{lastcost_total}) / $form->{sellprice_total} * 100;
   }
 
   $main::lxdebug->leave_sub();
 }
 
+sub setup_io_select_item_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Continue'),
+        submit    => [ '#form' ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
 sub select_item {
   $main::lxdebug->enter_sub();
 
@@ -502,12 +538,17 @@ sub select_item {
   my $pre_entered_qty = $params{pre_entered_qty} || 1;
   _check_io_auth();
 
+  setup_io_select_item_action_bar();
+
   my $previous_form = $::auth->save_form_in_session(form => $::form);
-  $::form->{title}  = $::locale->text('Select from one of the items below');
+  $::form->{title}  = $::myconfig{item_multiselect} ?
+      $::locale->text('Set count for one or more of the items to select them'):
+      $::locale->text('Select from one of the items below');
   $::form->header;
 
   my @item_list = map {
-    $_->{display_sellprice} /= $_->{price_factor} if ($_->{price_factor});
+    # maybe there is a better backend function or way to calc
+    $_->{display_sellprice} = ($_->{price_factor}) ? $_->{sellprice} / $_->{price_factor} : $_->{sellprice};
     $_;
   } @{ $::form->{item_list} };
 
@@ -545,13 +586,15 @@ sub item_selected {
   my $row = $curr_row;
 
   if ($myconfig{item_multiselect}) {
-    foreach (grep(/^select_qty_/, keys(%{ $form }))) {
+    my %multi_items;
+    for (keys %$form) {
       next unless $form->{$_};
-      $_ =~ /^select_qty_(\d+)/;
-      $form->{"id_${row}"}  = $1;
-      $form->{"qty_${row}"} = $form->{$_};
+      next unless /^select_qty_(\d+)/;
+      $multi_items{"id_${row}"}  = $1;
+      $multi_items{"qty_${row}"} = $form->{$_};
       $row++;
     }
+    $form->{$_} = $multi_items{$_} for keys %multi_items;
   } else {
     $form->{"id_${row}"} = delete($form->{select_item_id}) || croak 'Missing item selection ID';
     $row++;
@@ -600,7 +643,7 @@ sub item_selected {
 
     my @new_fields =
         qw(id partnumber description sellprice listprice inventory_accno
-           income_accno expense_accno bin unit weight assembly taxaccounts
+           income_accno expense_accno bin unit weight part_type taxaccounts
            partsgroup formel longdescription not_discountable partnotes lastcost
            price_factor_id price_factor);
 
@@ -661,15 +704,14 @@ sub item_selected {
     map { $amount += ($form->{"${_}_base"} * $form->{"${_}_rate"}) } split / /, $form->{"taxaccounts_$i"} if !$form->{taxincluded};
 
     $form->{creditremaining} -= $amount;
-
     $form->{"runningnumber_$i"} = $i;
 
     # format amounts
     map {
       $form->{"${_}_$i"} =
           $form->format_amount(\%myconfig, $form->{"${_}_$i"}, $decimalplaces)
-    } qw(sellprice lastcost qty) if $form->{item} ne 'assembly';
-    $form->{"discount_$i"} = $form->format_amount(\%myconfig, $form->{"discount_$i"} * 100.0) if $form->{item} ne 'assembly';
+    } qw(sellprice lastcost qty) if $form->{part_type} ne 'assembly';
+    $form->{"discount_$i"} = $form->format_amount(\%myconfig, $form->{"discount_$i"} * 100.0) if $form->{part_type} ne 'assembly';
 
     delete $form->{nextsub};
 
@@ -681,34 +723,39 @@ sub item_selected {
 }
 
 sub new_item {
-  $main::lxdebug->enter_sub();
+  _check_io_auth();
 
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
+  my $price = $::form->{vc} eq 'customer' ? 'sellprice_as_number' : 'lastcost_as_number';
+  my $previousform = $::auth->save_form_in_session;
+  my $callback     = build_std_url("action=return_from_new_item", "previousform=$previousform");
+  my $i            = $::form->{rowcount};
 
-  _check_io_auth();
+  my $parts_classification_type = $::form->{vc} eq 'customer' ? 'sales' : 'purchases';
 
-  my $price_key = ($form->{type} =~ m/request_quotation|purchase_order/) || ($form->{script} eq 'ir.pl') ? 'lastcost' : 'sellprice';
+  my @HIDDENS;
+  push @HIDDENS,      { 'name' => 'callback',     'value' => $callback };
+  push @HIDDENS, map +{ 'name' => $_,             'value' => $::form->{$_} },        qw(rowcount vc);
+  push @HIDDENS, map +{ 'name' => "part.$_",      'value' => $::form->{"${_}_$i"} }, qw(partnumber description unit price_factor_id);
+  push @HIDDENS,      { 'name' => "part.$price",  'value' => $::form->{"sellprice_$i"} };
+  push @HIDDENS,      { 'name' => "part.notes",   'value' => $::form->{"longdescription_$i"} };
 
-  # change callback
-  $form->{old_callback} = $form->escape($form->{callback}, 1);
-  $form->{callback}     = $form->escape("$form->{script}?action=display_form", 1);
+  push @HIDDENS,      { 'name' => "parts_classification_type", 'value' => $parts_classification_type };
 
-  # save all form variables except action in the session and keep the key in the previousform variable
-  my $previousform = $::auth->save_form_in_session(skip_keys => [ qw(action) ]);
+  $::form->header;
+  print $::form->parse_html_template("generic/new_item", { HIDDENS => [ sort { $a->{name} cmp $b->{name} } @HIDDENS ] } );
+}
 
-  my @HIDDENS;
-  push @HIDDENS,      { 'name' => 'previousform', 'value' => $previousform };
-  push @HIDDENS, map +{ 'name' => $_,             'value' => $form->{$_} },                       qw(rowcount vc);
-  push @HIDDENS, map +{ 'name' => $_,             'value' => $form->{"${_}_$form->{rowcount}"} }, qw(partnumber description unit);
-  push @HIDDENS,      { 'name' => 'taxaccount2',  'value' => $form->{taxaccounts} };
-  push @HIDDENS,      { 'name' => $price_key,     'value' => $form->parse_amount(\%myconfig, $form->{"sellprice_$form->{rowcount}"}) };
-  push @HIDDENS,      { 'name' => 'notes',        'value' => $form->{"longdescription_$form->{rowcount}"} };
+sub return_from_new_item {
+  _check_io_auth();
 
-  $form->header();
-  print $form->parse_html_template("generic/new_item", { HIDDENS => [ sort { $a->{name} cmp $b->{name} } @HIDDENS ] } );
+  my $part = SL::DB::Manager::Part->find_by(id => delete $::form->{new_parts_id}) or die 'can not find part that was just saved!';
 
-  $main::lxdebug->leave_sub();
+  $::auth->restore_form_from_session(delete $::form->{previousform}, form => $::form);
+
+  $::form->{"id_$::form->{rowcount}"} = $part->id;
+
+  my $url = build_std_url("script=$::form->{script}", "RESTORE_FORM_FROM_SESSION_ID=" . $::auth->save_form_in_session);
+  print $::request->{cgi}->redirect($url);
 }
 
 sub check_form {
@@ -723,7 +770,7 @@ sub check_form {
   my $count = 0;
 
   # remove any makes or model rows
-  if ($form->{item} eq 'assembly') {
+  if ($form->{part_type} eq 'assembly') {
 
     # fuer assemblies auskommentiert. seiteneffekte? ;-) wird die woanders benoetigt?
     #$form->{sellprice} = 0;
@@ -756,7 +803,7 @@ sub check_form {
     $form->redo_rows(\@flds, \@a, $count, $form->{assembly_rows});
     $form->{assembly_rows} = $count;
 
-  } elsif ($form->{item} !~ m{^(?:part|service)$}) {
+  } elsif ($form->{part_type} !~ m{^(?:part|service)$}) {
     remove_emptied_rows(1);
 
     $form->{creditremaining} -= &invoicetotal;
@@ -856,7 +903,7 @@ sub validate_items {
   if ($form->{rowcount} == 1) {
     flash('warning', $::locale->text('The action you\'ve chosen has not been executed because the document does not contain any item yet.'));
     &update;
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
 
   for my $i (1 .. $form->{rowcount} - 1) {
@@ -870,6 +917,55 @@ sub validate_items {
 sub order {
   $main::lxdebug->enter_sub();
 
+  _order();
+
+  if ($::instance_conf->get_feature_experimental_order) {
+
+    # At this point, the record is saved and the exchangerate contains
+    # an unformatted value. _make_record uses RDBO attributes (i.e. _as_number)
+    # to assign values and thus expects an formatted value.
+    $::form->{exchangerate} = $::form->format_amount(\%::myconfig, $::form->{exchangerate});
+
+    my $order = _make_record();
+
+    $order->currency(SL::DB::Currency->new(name => $::form->{currency})->load) if $::form->{currency};
+    $order->globalproject_id(undef)                                            if !$order->globalproject_id;
+    $order->payment_id(undef)                                                  if !$order->payment_id;
+
+    my $row = 1;
+    foreach my $item (@{$order->items_sorted}) {
+      $item->custom_variables([]);
+
+      $item->price_factor_id(undef) if !$item->price_factor_id;
+      $item->project_id(undef)      if !$item->project_id;
+
+      # autovivify all cvars that are not in the form (cvars_by_config can do it).
+      # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
+       foreach my $var (@{ $item->cvars_by_config }) {
+        my $key = 'ic_cvar_' . $var->config->name . '_' . $row;
+        $var->unparsed_value($::form->{$key});
+        $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
+      }
+      $item->parse_custom_variable_values;
+
+      $row++;
+    }
+
+    require SL::Controller::Order;
+    my $c = SL::Controller::Order->new(order => $order);
+    $c->setup_custom_shipto_from_form($order, $::form);
+    $c->action_edit();
+
+    $main::lxdebug->leave_sub();
+    $::dispatcher->end_request;
+  }
+
+  &display_form;
+
+  $main::lxdebug->leave_sub();
+}
+
+sub _order {
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
@@ -884,10 +980,22 @@ sub order {
   $form->{old_employee_id} = $form->{employee_id};
   $form->{old_salesman_id} = $form->{salesman_id};
 
-  # link doc invoice -> quotation (single id no multi mode)
-  $form->{convert_from_ar_ids} = delete $form->{id};
+  delete $form->{$_} foreach (qw(id printed emailed queued));
+
+  # When creating a new sales order from a saved sales invoice, reset id,
+  # ordnumber, transdate and deliverydate as we are creating a new order. This
+  # workflow is probably mainly used as a template mechanism for creating new
+  # orders from existing invoices, so we probably don't want to link the items.
+  # Is this order function called anywhere else?
+  # The worksflows in oe already call sales_order and purchase_order in oe, not
+  # this general function which now only seems to be called from saved sales
+  # invoices
+  # Why is ordnumber set to invnumber above, does this ever make sense?
+
+  if ( $form->{script} eq 'is.pl' && $form->{type} eq 'invoice' ) {
+    delete $form->{$_} foreach (qw(ordnumber id transdate deliverydate));
+  };
 
-  delete $form->{$_} foreach (qw(printed emailed queued));
   my $buysell;
   if ($form->{script} eq 'ir.pl' || $form->{type} eq 'request_quotation') {
     $form->{title} = $locale->text('Add Purchase Order');
@@ -931,9 +1039,6 @@ sub order {
   }
 
   &prepare_order;
-  &display_form;
-
-  $main::lxdebug->leave_sub();
 }
 
 sub quotation {
@@ -950,13 +1055,10 @@ sub quotation {
   if ($form->{type} =~  /(sales|purchase)_order/) {
     $form->{"converted_from_orderitems_id_$_"} = delete $form->{"orderitems_id_$_"} for 1 .. $form->{"rowcount"};
   }
-  # link doc order -> quotation (single id no multi mode)
-  $form->{convert_from_oe_ids} = delete $form->{id};
-
   if ($form->{second_run}) {
     $form->{print_and_post} = 0;
   }
-  delete $form->{$_} foreach (qw(printed emailed queued));
+  delete $form->{$_} foreach (qw(id printed emailed queued quonumber transaction_description));
 
   my $buysell;
   if ($form->{script} eq 'ir.pl' || $form->{type} eq 'purchase_order') {
@@ -1007,220 +1109,32 @@ sub request_for_quotation {
   quotation();
 }
 
-sub edit_e_mail {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  _check_io_auth();
-
-  if ($form->{second_run}) {
-    $form->{print_and_post} = 0;
-    $form->{resubmit}       = 0;
-  }
-
-  $form->{email} = $form->{shiptoemail} if $form->{shiptoemail} && $form->{formname} =~ /(pick|packing|bin)_list/;
-
-  if ($form->{"cp_id"}) {
-    CT->get_contact(\%myconfig, $form);
-    $form->{"email"} = $form->{"cp_email"} if $form->{"cp_email"};
-  }
-
-  $form->{language} = $form->get_template_language(\%myconfig);
-  $form->{language} = "_" . $form->{language} if $form->{language};
-
-  my $title = $locale->text('E-mail') . " " . $form->get_formname_translation();
-
-  $form->{oldmedia} = $form->{media};
-  $form->{media}    = "email";
-
-  my $global_bcc = AM->get_defaults()->{global_bcc};
-
-  $form->{bcc} = join ', ', grep $_, $form->{bcc}, $global_bcc;
-
-  my $attachment_filename = $form->generate_attachment_filename();
-  my $subject             = $form->{subject} || $form->generate_email_subject();
-
-  $form->header;
-
-  my (@dont_hide_key_list, %dont_hide_key, @hidden_keys);
-  @dont_hide_key_list = qw(action email cc bcc subject message sendmode format header override login password);
-  @dont_hide_key{@dont_hide_key_list} = (1) x @dont_hide_key_list;
-  @hidden_keys = sort grep { !$dont_hide_key{$_} } grep { !ref $form->{$_} } keys %$form;
-
-  print $form->parse_html_template('generic/edit_email',
-                                   { title         => $title,
-                                     a_filename    => $attachment_filename,
-                                     subject       => $subject,
-                                     print_options => print_options('inline' => 1),
-                                     HIDDEN        => [ map +{ name => $_, value => $form->{$_} }, @hidden_keys ],
-                                     SHOW_BCC      => $::auth->assert('email_bcc', 'may fail') });
-
-  $main::lxdebug->leave_sub();
-}
-
-sub send_email {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-
-  _check_io_auth();
-
-  my $callback = $form->{script} . "?action=edit";
-  map({ $callback .= "\&${_}=" . E($form->{$_}); } qw(type id));
-
-  print_form("return");
-
-  Common->save_email_status(\%myconfig, $form);
-
-  $form->{callback} = $callback;
-  $form->redirect();
-
-  $main::lxdebug->leave_sub();
-}
-
-# generate the printing options displayed at the bottom of oe and is forms.
-# this function will attempt to guess what type of form is displayed, and will generate according options
-#
-# about the coding:
-# this version builds the arrays of options pretty directly. if you have trouble understanding how,
-# the opthash function builds hashrefs which are then pieced together for the template arrays.
-# unneeded options are "undef"ed out, and then grepped out.
-#
-# the inline options is untested, but intended to be used later in metatemplating
 sub print_options {
-  $main::lxdebug->enter_sub();
+  $::lxdebug->enter_sub();
 
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
+  my (%options) = @_;
 
   _check_io_auth();
 
-  my %options = @_;
-
-  # names 3 parameters and returns a hashref, for use in templates
-  sub opthash { +{ value => shift, selected => shift, oname => shift } }
-  my (@FORMNAME, @LANGUAGE_ID, @FORMAT, @SENDMODE, @MEDIA, @PRINTER_ID, @SELECTS) = ();
-
-  # note: "||"-selection is only correct for values where "0" is _not_ a correct entry
-  $form->{sendmode}   = "attachment";
-  $form->{format}     = $form->{format} || $myconfig{template_format} || "pdf";
-  $form->{copies}     = $form->{copies} || $myconfig{copies}          || 3;
-  $form->{media}      = $form->{media}  || $myconfig{default_media}   || "screen";
-  $form->{printer_id} = defined $form->{printer_id}           ? $form->{printer_id} :
-                        defined $myconfig{default_printer_id} ? $myconfig{default_printer_id} : "";
-
-  $form->{PD}{ $form->{formname} } = "selected";
-  $form->{DF}{ $form->{format} }   = "selected";
-  $form->{OP}{ $form->{media} }    = "selected";
-  $form->{SM}{ $form->{formname} } = "selected";
-
-  push @FORMNAME, grep $_,
-    ($form->{type} eq 'purchase_order') ? (
-      opthash("purchase_order",      $form->{PD}{purchase_order},      $locale->text('Purchase Order')),
-      opthash("bin_list",            $form->{PD}{bin_list},            $locale->text('Bin List'))
-    ) : undef,
-    ($form->{type} eq 'credit_note') ?
-      opthash("credit_note",         $form->{PD}{credit_note},         $locale->text('Credit Note')) : undef,
-    ($form->{type} eq 'sales_order') ? (
-      opthash("sales_order",         $form->{PD}{sales_order},         $locale->text('Confirmation')),
-      opthash("proforma",            $form->{PD}{proforma},            $locale->text('Proforma Invoice')),
-    ) : undef,
-    ($form->{type} =~ /sales_quotation$/) ?
-      opthash('sales_quotation',     $form->{PD}{sales_quotation},     $locale->text('Quotation')) : undef,
-    ($form->{type} =~ /request_quotation$/) ?
-      opthash('request_quotation',   $form->{PD}{request_quotation},   $locale->text('Request for Quotation')) : undef,
-    ($form->{type} eq 'invoice') ? (
-      opthash("invoice",             $form->{PD}{invoice},             $locale->text('Invoice')),
-      opthash("proforma",            $form->{PD}{proforma},            $locale->text('Proforma Invoice')),
-    ) : undef,
-    ($form->{type} eq 'invoice' && $form->{storno}) ? (
-      opthash("storno_invoice",      $form->{PD}{storno_invoice},      $locale->text('Storno Invoice')),
-    ) : undef,
-    ($form->{type} =~ /_delivery_order$/) ? (
-      opthash($form->{type},         $form->{PD}{$form->{type}},       $locale->text('Delivery Order')),
-      opthash('pick_list',           $form->{PD}{pick_list},           $locale->text('Pick List')),
-    ) : undef;
-
-  push @SENDMODE,
-    opthash("attachment",            $form->{SM}{attachment},          $locale->text('Attachment')),
-    opthash("inline",                $form->{SM}{inline},              $locale->text('In-line'))
-      if ($form->{media} eq 'email');
-
-  my $printable_templates = any { $::lx_office_conf{print_templates}->{$_} } qw(latex opendocument);
-  push @MEDIA, grep $_,
-      opthash("screen",              $form->{OP}{screen},              $locale->text('Screen')),
-    ($printable_templates && $form->{printers} && scalar @{ $form->{printers} }) ?
-      opthash("printer",             $form->{OP}{printer},             $locale->text('Printer')) : undef,
-    ($printable_templates && !$options{no_queue}) ?
-      opthash("queue",               $form->{OP}{queue},               $locale->text('Queue')) : undef
-        if ($form->{media} ne 'email');
-
-  push @FORMAT, grep $_,
-    ($::lx_office_conf{print_templates}->{opendocument} &&     $::lx_office_conf{applications}->{openofficeorg_writer}  &&     $::lx_office_conf{applications}->{xvfb}
-                                                        && (-x $::lx_office_conf{applications}->{openofficeorg_writer}) && (-x $::lx_office_conf{applications}->{xvfb})
-     && !$options{no_opendocument_pdf}) ?
-      opthash("opendocument_pdf",    $form->{DF}{"opendocument_pdf"},  $locale->text("PDF (OpenDocument/OASIS)")) : undef,
-    ($::lx_office_conf{print_templates}->{latex}) ?
-      opthash("pdf",                 $form->{DF}{pdf},                 $locale->text('PDF')) : undef,
-    ($::lx_office_conf{print_templates}->{latex} && !$options{no_postscript}) ?
-      opthash("postscript",          $form->{DF}{postscript},          $locale->text('Postscript')) : undef,
-    (!$options{no_html}) ?
-      opthash("html", $form->{DF}{html}, "HTML") : undef,
-    ($::lx_office_conf{print_templates}->{opendocument} && !$options{no_opendocument}) ?
-      opthash("opendocument",        $form->{DF}{opendocument},        $locale->text("OpenDocument/OASIS")) : undef,
-    ($::lx_office_conf{print_templates}->{excel} && !$options{no_excel}) ?
-      opthash("excel",               $form->{DF}{excel},               $locale->text("Excel")) : undef;
-
-  push @LANGUAGE_ID,
-    map { opthash($_->{id}, ($_->{id} eq $form->{language_id} ? 'selected' : ''), $_->{description}) } +{}, @{ $form->{languages} }
-      if (ref $form->{languages} eq 'ARRAY');
-
-  push @PRINTER_ID,
-    map { opthash($_->{id}, ($_->{id} eq $form->{printer_id} ? 'selected' : ''), $_->{printer_description}) } +{}, @{ $form->{printers} }
-      if ((ref $form->{printers} eq 'ARRAY') && scalar @{ $form->{printers } });
-
-  @SELECTS = map {
-    sname => $_->[1],
-    DATA  => $_->[0],
-    show  => !$options{"hide_" . $_->[1]} && scalar @{ $_->[0] }
-  },
-  [ \@FORMNAME,    'formname',    ],
-  [ \@LANGUAGE_ID, 'language_id', ],
-  [ \@FORMAT,      'format',      ],
-  [ \@SENDMODE,    'sendmode',    ],
-  [ \@MEDIA,       'media',       ],
-  [ \@PRINTER_ID,  'printer_id',  ];
-
-  my %dont_display_groupitems = (
-    'dunning' => 1,
-    'letter'  => 1,
-    );
-
-  my %template_vars = (
-    display_copies       => scalar @{ $form->{printers} || [] } && $::lx_office_conf{print_templates}->{latex} && $form->{media} ne 'email',
-    display_remove_draft => (!$form->{id} && $form->{draft_id}),
-    display_groupitems   => !$dont_display_groupitems{$form->{type}},
-    groupitems_checked   => $form->{groupitems} ? "checked" : '',
-    remove_draft_checked => $form->{remove_draft} ? "checked" : ''
-  );
+  my $inline = delete $options{inline};
 
-  my $print_options = $form->parse_html_template("generic/print_options", { SELECTS  => \@SELECTS, %template_vars } );
+  require SL::Helper::PrintOptions;
+  my $print_options = SL::Helper::PrintOptions->get_print_options(
+    form     => $::form,
+    myconfig => \%::myconfig,
+    locale   => $::locale,
+    options  => \%options);
 
-  if ($options{inline}) {
-    $main::lxdebug->leave_sub();
+  if ($inline) {
+    $::lxdebug->leave_sub();
     return $print_options;
   }
 
   print $print_options;
-
-  $main::lxdebug->leave_sub();
+  $::lxdebug->leave_sub();
 }
 
+
 sub print {
   $main::lxdebug->enter_sub();
 
@@ -1255,7 +1169,7 @@ sub print {
     $form->{formname} = $formname;
     &edit();
     $::lxdebug->leave_sub();
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
 
   &print_form($old_form);
@@ -1292,6 +1206,15 @@ sub print_form {
   if ($form->{formname} eq "invoice") {
     $form->{label} = $locale->text('Invoice');
   }
+
+  if ($form->{formname} eq "invoice_for_advance_payment") {
+    $form->{label} = $locale->text('Invoice for Advance Payment');
+  }
+
+  if ($form->{formname} eq "final_invoice") {
+    $form->{label} = $locale->text('Final Invoice');
+  }
+
   if ($form->{formname} eq 'sales_order') {
     $inv                  = "ord";
     $due                  = "req";
@@ -1355,6 +1278,17 @@ sub print_form {
     $order                = 1;
   }
 
+  if (($form->{type} eq 'sales_order') && ($form->{formname} eq 'ic_supply') ) {
+    $inv                  = "inv";
+    $due                  = "due";
+    $form->{"${inv}date"} = $form->{transdate};
+    $form->{"invdate"}    = $form->{transdate};
+    $form->{invnumber}    = $form->{ordnumber};
+    $form->{label}        = $locale->text('Intra-Community supply');
+    $numberfld            = "sonumber";
+    $order                = 1;
+  }
+
   if ($form->{formname} eq 'request_quotation') {
     $inv                  = "quo";
     $due                  = "req";
@@ -1378,12 +1312,12 @@ sub print_form {
   }
 
   $form->{TEMPLATE_DRIVER_OPTIONS} = { };
-  if (any { $form->{type} eq $_ } qw(sales_quotation sales_order sales_delivery_order invoice request_quotation purchase_order purchase_delivery_order credit_note)) {
-    $form->{TEMPLATE_DRIVER_OPTIONS}->{variable_content_types} = {
-      longdescription => 'html',
-      partnotes       => 'html',
-      notes           => 'html',
-    };
+  if (any { $form->{type} eq $_ } qw(sales_quotation sales_order sales_delivery_order invoice invoice_for_advance_payment final_invoice request_quotation purchase_order purchase_delivery_order credit_note)) {
+    $form->{TEMPLATE_DRIVER_OPTIONS}->{variable_content_types} = $form->get_variable_content_types();
+  }
+
+  if ($form->{format} =~ m{pdf}) {
+    _maybe_attach_zugferd_data($form);
   }
 
   $form->isblank("email", $locale->text('E-mail address missing!'))
@@ -1470,6 +1404,8 @@ sub print_form {
     $form->get_shipto(\%myconfig);
   }
 
+  $form->set_addition_billing_address_print_variables;
+
   $form->{notes} =~ s/^\s+//g;
 
   delete $form->{printer_command};
@@ -1493,7 +1429,7 @@ sub print_form {
 
   # Format dates.
   format_dates($output_dateformat, $output_longdates,
-               qw(invdate orddate quodate pldate duedate reqdate transdate
+               qw(invdate orddate quodate pldate duedate reqdate transdate tax_point
                   shippingdate deliverydate validitydate paymentdate
                   datepaid transdate_oe transdate_do transdate_quo deliverydate_oe dodate
                   employee_startdate employee_enddate
@@ -1577,6 +1513,7 @@ sub print_form {
 
     $form->{emailed} .= " $form->{formname}";
     $form->{emailed} =~ s/^ //;
+    $form->{addition} = "MAILED";
   }
   my $emailed = $form->{emailed};
 
@@ -1647,6 +1584,10 @@ sub print_form {
     today     => DateTime->today,
   };
 
+  if ($defaults->print_interpolate_variables_in_positions) {
+    $form->substitute_placeholders_in_template_arrays({ field => 'description', type => 'text' }, { field => 'longdescription', type => 'html' });
+  }
+
   $form->parse_template(\%myconfig);
 
   $form->{callback} = "";
@@ -1688,7 +1629,7 @@ sub print_form {
       }
 
       call_sub($display_form);
-      ::end_of_request();
+      $::dispatcher->end_request;
     }
 
     my $msg =
@@ -1702,7 +1643,7 @@ sub print_form {
   }
   if ($form->{printing}) {
    call_sub($display_form);
-   ::end_of_request();
+   $::dispatcher->end_request;
   }
 
   $main::lxdebug->leave_sub();
@@ -1745,54 +1686,16 @@ sub post_as_new {
   $main::lxdebug->leave_sub();
 }
 
-sub ship_to {
-  $main::lxdebug->enter_sub();
-
-  _check_io_auth();
-
-  $::form->{print_and_post} = 0 if $::form->{second_run};
-
-  map { $::form->{$_} = $::form->parse_amount(\%::myconfig, $::form->{$_}) } qw(exchangerate creditlimit creditremaining);
-
-  # get details for customer/vendor
-  call_sub($::form->{vc} . "_details", qw(name department_1 department_2 street zipcode city country contact email phone fax), $::form->{vc} . "number");
-
-  $::form->{rowcount}--;
-
-  my @shipto_vars   = qw(shiptoname shiptostreet shiptozipcode shiptocity shiptocountry
-                         shiptocontact shiptocp_gender shiptophone shiptofax shiptoemail
-                         shiptodepartment_1 shiptodepartment_2);
-  my $previous_form = $::auth->save_form_in_session(skip_keys => [ @shipto_vars, qw(header shipto_id) ]);
-  $::form->{title}  = $::locale->text('Ship to');
-  $::form->header;
-
-  my $vc_obj = ($::form->{vc} eq 'customer' ? "SL::DB::Customer" : "SL::DB::Vendor")->new(id => $::form->{$::form->{vc} . "_id"})->load;
-
-  print $::form->parse_html_template('io/ship_to', { previousform => $previous_form,
-                                                     nextsub      => $::form->{display_form} || 'display_form',
-                                                     vc_obj       => $vc_obj,
-                                                   });
-
-  $main::lxdebug->leave_sub();
-}
-
-sub ship_to_entered {
-  $::auth->restore_form_from_session(delete $::form->{previousform});
-  call_sub($::form->{nextsub});
-}
-
 sub relink_accounts {
   $main::lxdebug->enter_sub();
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  _check_io_auth();
-
   $form->{"taxaccounts"} =~ s/\s*$//;
   $form->{"taxaccounts"} =~ s/^\s*//;
   foreach my $accno (split(/\s*/, $form->{"taxaccounts"})) {
-    map({ delete($form->{"${accno}_${_}"}); } qw(rate description taxnumber));
+    map({ delete($form->{"${accno}_${_}"}); } qw(rate description taxnumber tax_id)); # add tax_id ?
   }
   $form->{"taxaccounts"} = "";
 
@@ -1844,61 +1747,26 @@ sub _update_part_information {
   foreach my $i (1..$form->{rowcount}) {
     next unless ($form->{"id_${i}"});
 
-    my $info                 = $form->{PART_INFORMATION}->{$form->{"id_${i}"}} || { };
-    $form->{"partunit_${i}"} = $info->{unit};
-    $form->{"weight_$i"}     = $info->{weight};
+    my $info                        = $form->{PART_INFORMATION}->{$form->{"id_${i}"}} || { };
+    $form->{"partunit_${i}"}        = $info->{unit};
+    $form->{"weight_$i"}            = $info->{weight};
+    $form->{"part_type_$i"}         = $info->{part_type};
+    $form->{"classification_id_$i"} = $info->{classification_id};
+    $form->{"has_sernumber_$i"}     = $info->{has_sernumber};
   }
 
   $main::lxdebug->leave_sub();
 }
 
 sub _update_ship {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-
-  if (!$form->{ordnumber} || !$form->{id}) {
-    map { $form->{"ship_$_"} = 0 } (1..$form->{rowcount});
-    $main::lxdebug->leave_sub();
-    return;
-  }
-
-  my $all_units = AM->retrieve_all_units();
+  return unless $::form->{id};
+  my $helper = SL::Helper::ShippedQty->new->calculate($::form->{id});
 
-  my %ship = DO->get_shipped_qty('type'  => ($form->{type} eq 'purchase_order') ? 'purchase' : 'sales',
-                                 'oe_id' => $form->{id},);
-
-  foreach my $i (1..$form->{rowcount}) {
-    next unless ($form->{"id_${i}"});
-
-    $form->{"ship_$i"} = 0;
-
-    my $ship_entry = $ship{$form->{"id_$i"}};
-
-    next if (!$ship_entry || ($ship_entry->{qty} <= 0));
-
-    my $rowqty =
-      ($form->{simple_save} ? $form->{"qty_$i"} : $form->parse_amount(\%myconfig, $form->{"qty_$i"}))
-      * $all_units->{$form->{"unit_$i"}}->{factor}
-      / $all_units->{$form->{"partunit_$i"}}->{factor};
-
-    $form->{"ship_$i"}  = min($rowqty, $ship_entry->{qty});
-    $ship_entry->{qty} -= $form->{"ship_$i"};
-  }
-
-  foreach my $i (1..$form->{rowcount}) {
-    next unless ($form->{"id_${i}"});
-
-    my $ship_entry = $ship{$form->{"id_$i"}};
-
-    next if (!$ship_entry || ($ship_entry->{qty} <= 0.01));
-
-    $form->{"ship_$i"} += $ship_entry->{qty};
-    $ship_entry->{qty}  = 0;
+  for my $i (1..$::form->{rowcount}) {
+    if (my $oid = $::form->{"orderitems_id_$i"}) {
+      $::form->{"ship_$i"} = $helper->shipped_qty->{$oid};
+    }
   }
-
-  $main::lxdebug->leave_sub();
 }
 
 sub _update_custom_variables {
@@ -2019,7 +1887,7 @@ sub _remove_billed_or_delivered_rows {
 # TODO: both of these are makeshift so that price sources can operate on rdbo objects. if
 # this ever gets rewritten in controller style, throw this out
 sub _make_record_item {
-  my ($row) = @_;
+  my ($row, %params) = @_;
 
   my $class = {
     sales_order             => 'OrderItem',
@@ -2027,6 +1895,8 @@ sub _make_record_item {
     sales_quotation         => 'OrderItem',
     request_quotation       => 'OrderItem',
     invoice                 => 'InvoiceItem',
+    invoice_for_advance_payment => 'InvoiceItem',
+    final_invoice           => 'InvoiceItem',
     credit_note             => 'InvoiceItem',
     purchase_invoice        => 'InvoiceItem',
     purchase_delivery_order => 'DeliveryOrderItem',
@@ -2037,25 +1907,51 @@ sub _make_record_item {
 
   $class = 'SL::DB::' . $class;
 
+  my %translated_methods = (
+    'SL::DB::OrderItem' => {
+      id                      => 'parts_id',
+      orderitems_id           => 'id',
+    },
+    'SL::DB::DeliveryOrderItem' => {
+      id                      => 'parts_id',
+      delivery_order_items_id => 'id',
+    },
+    'SL::DB::InvoiceItem' => {
+      id                      => 'parts_id',
+      invoice_id => 'id',
+    },
+  );
+
   eval "require $class";
 
   my $obj = $::form->{"orderitems_id_$row"}
           ? $class->meta->convention_manager->auto_manager_class_name->find_by(id => $::form->{"orderitems_id_$row"})
           : $class->new;
 
-  for my $method (apply { s/_$row$// } grep { /_$row$/ } keys %$::form) {
+  for my $key (grep { /_$row$/ } keys %$::form) {
+    my $method = $key;
+    $method =~ s/_$row$//;
+    $method = $translated_methods{$class}{$method} // $method;
+    my $value = $::form->{$key};
     if ($obj->meta->column($method)) {
       if ($obj->meta->column($method)->isa('Rose::DB::Object::Metadata::Column::Date')) {
-        $obj->${\"$method\_as_date"}($::form->{"$method\_$row"});
+        $obj->${\"$method\_as_date"}($value);
       } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::(?:Numeric|Float|DoublePrecsion)$/) {
-        $obj->${\"$method\_as_number"}($::form->{"$method\_$row"});
+        $obj->${\"$method\_as_number"}(($value // '') eq '' ? undef : $value);
       } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::Boolean$/) {
-        $obj->$method(!!$::form->{$method});
+        $obj->$method(!!$value);
+      } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::(?:Big)?(?:Int(?:eger)?|Serial)$/) {
+        $obj->$method(($value // '') eq '' ? undef : $value * 1);
       } else {
-        $obj->$method($::form->{"$method\_$row"});
+        $obj->$method($value);
+      }
+
+      if ($method eq 'discount') {
+        $obj->discount($obj->discount / 100.0);
       }
+
     } else {
-      $obj->{__additional_form_attributes}{$method} = $::form->{"$method\_$row"};
+      $obj->{__additional_form_attributes}{$method} = $value;
     }
   }
 
@@ -2063,6 +1959,11 @@ sub _make_record_item {
     $obj->part(SL::DB::Part->load_cached($::form->{"id_$row"}));
   }
 
+  if ($obj->can('qty')) {
+    $obj->qty(     $obj->qty      * $params{factor});
+    $obj->base_qty($obj->base_qty * $params{factor});
+  }
+
   return $obj;
 }
 
@@ -2082,6 +1983,8 @@ sub _make_record {
            : do { die 'unknown invoice type' };
   }
 
+  my $factor = $::form->{type} =~ m{credit_note} ? -1 : 1;
+
   return unless $class;
 
   $class = 'SL::DB::' . $class;
@@ -2099,9 +2002,11 @@ sub _make_record {
     if ($obj->meta->column($method)->isa('Rose::DB::Object::Metadata::Column::Date')) {
       $obj->${\"$method\_as_date"}($::form->{$method});
     } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::(?:Numeric|Float|DoublePrecsion)$/) {
-      $obj->${\"$method\_as_number"}($::form->{$method});
+      $obj->${\"$method\_as_number"}(($::form->{$method} // '') eq '' ? undef : $::form->{$method});
     } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::Boolean$/) {
       $obj->$method(!!$::form->{$method});
+    } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::(?:Big)?(?:Int(?:eger)?|Serial)$/) {
+      $obj->$method(($::form->{$method} // '') eq '' ? undef : $::form->{$method} * 1);
     } else {
       $obj->$method($::form->{$method});
     }
@@ -2110,11 +2015,259 @@ sub _make_record {
   my @items;
   for my $i (1 .. $::form->{rowcount}) {
     next unless $::form->{"id_$i"};
-    push @items, _make_record_item($i);
+    push @items, _make_record_item($i, factor => $factor);
   }
 
   $obj->items(@items) if @items;
-  $obj->is_sales(!!$obj->customer_id) if $class eq 'SL::DB::DeliveryOrder';
+
+  if ($class eq 'SL::DB::DeliveryOrder' && !$obj->order_type) {
+    $obj->order_type(SL::DB::DeliveryOrder::TypeData::validate_type($::form->{type}));
+  }
+
+  if ($class eq 'SL::DB::Invoice') {
+    my $paid = $factor *
+      sum
+      map  { $::form->parse_amount(\%::myconfig, $::form->{$_}) }
+      grep { m{^paid_\d+$} }
+      keys %{ $::form };
+    $obj->paid($paid);
+  }
 
   return $obj;
 }
+
+sub setup_sales_purchase_print_options {
+  my $print_form = Form->new('');
+  $print_form->{printers}  = SL::DB::Manager::Printer->get_all_sorted;
+
+  $print_form->{$_} = $::form->{$_} for qw(type media printer_id storno formname groupitems);
+
+  return SL::Helper::PrintOptions->get_print_options(
+    form    => $print_form,
+    options => {
+      show_headers => 1,
+    },
+  );
+}
+
+sub _get_files_for_email_dialog {
+  my %files = map { ($_ => []) } qw(versions files vc_files part_files project_files);
+
+  return %files if !$::instance_conf->get_doc_storage;
+
+  if ($::form->{id}) {
+    $files{versions} = [ SL::File->get_all_versions(object_id => $::form->{id},    object_type => $::form->{type}, file_type => 'document') ];
+    $files{files}    = [ SL::File->get_all(         object_id => $::form->{id},    object_type => $::form->{type}, file_type => 'attachment') ];
+    $files{vc_files} = [ SL::File->get_all(         object_id => $::form->{vc_id}, object_type => $::form->{vc},   file_type => 'attachment') ]
+      if $::form->{vc} && $::form->{"vc_id"};
+    $files{project_files} = [ SL::File->get_all(object_id => $::form->{project_id}, object_type => 'project',file_type => 'attachment') ]
+      if $::form->{project_id};
+  }
+
+  my @parts =
+    uniq_by { $_->{id} }
+    grep    { $_->{id} }
+    map     {
+      +{ id         => $::form->{"id_$_"},
+         partnumber => $::form->{"partnumber_$_"},
+       }
+    } (1 .. $::form->{rowcount});
+
+  foreach my $part (@parts) {
+    my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
+    push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
+  }
+
+  foreach my $key (keys %files) {
+    $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
+  }
+
+  return %files;
+}
+
+sub show_sales_purchase_email_dialog {
+  my $email = '';
+  my $email_cc = '';
+  my $record_email;
+  if ($::form->{cp_id}) {
+    $email = SL::DB::Contact->load_cached($::form->{cp_id})->cp_email;
+  }
+  # write a dispatch table if a third type enters
+  # check record mail for sales_invoice
+  if ($::form->{type} eq 'invoice' && (!$email || $::instance_conf->get_invoice_mail_settings ne 'cp')) {
+    # check for invoice_mail if defined (vc.invoice_email)
+    $record_email = SL::DB::Customer->load_cached($::form->{vc_id})->invoice_mail;
+    if ($record_email) {
+      # check if cc for contact is also wanted
+      $email_cc = $email if ($::instance_conf->get_invoice_mail_settings eq 'invoice_mail_cc_cp');
+      $email    = $record_email;
+    }
+  }
+  # check record mail for sales_delivery_order
+  if ($::form->{type} eq 'sales_delivery_order') {
+    # check for deliver_order_mail if defined (vc.delivery_order_mail)
+    $record_email = SL::DB::Customer->load_cached($::form->{vc_id})->delivery_order_mail;
+    if ($record_email) {
+      # check if cc for contact is also wanted
+      $email_cc = $email; # always cc to cp
+      $email    = $record_email;
+    }
+  }
+  # still no email? use general mail (vc.email)
+  if (!$email && $::form->{vc} && $::form->{vc_id}) {
+    $email = SL::DB::Customer->load_cached($::form->{vc_id})->email if 'customer' eq $::form->{vc};
+    $email = SL::DB::Vendor  ->load_cached($::form->{vc_id})->email if 'vendor'   eq $::form->{vc};
+  }
+
+  $email = '' if $::form->{type} eq 'purchase_delivery_order';
+
+  $::form->{language} = $::form->get_template_language(\%::myconfig);
+  $::form->{language} = "_" . $::form->{language};
+
+  my %body_params = (record_email => $record_email);
+  if (($::form->{type} eq 'invoice') && $::form->{direct_debit}) {
+    $body_params{translation_type}          = "preset_text_invoice_direct_debit";
+    $body_params{fallback_translation_type} = "preset_text_invoice";
+  }
+
+  my @employees_with_email = grep {
+    my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
+    $user && !!trim($user->get_config_value('email'));
+  } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
+
+  my $email_form = {
+    to                  => $email,
+    cc                  => $email_cc,
+    subject             => $::form->generate_email_subject,
+    message             => $::form->generate_email_body(%body_params),
+    attachment_filename => $::form->generate_attachment_filename,
+    js_send_function    => 'kivi.SalesPurchase.send_email()',
+  };
+
+  my %files = _get_files_for_email_dialog();
+
+  my $all_partner_email_addresses;
+  $all_partner_email_addresses = SL::DB::Customer->load_cached($::form->{vc_id})->get_all_email_addresses() if 'customer' eq $::form->{vc};
+  $all_partner_email_addresses = SL::DB::Vendor  ->load_cached($::form->{vc_id})->get_all_email_addresses() if 'vendor'   eq $::form->{vc};
+
+  my $html  = $::form->parse_html_template("common/_send_email_dialog", {
+    email_form      => $email_form,
+    show_bcc        => $::auth->assert('email_bcc', 'may fail'),
+    FILES           => \%files,
+    is_customer     => $::form->{vc} eq 'customer',
+    is_invoice_mail => ($record_email && $::form->{type} eq 'invoice'),
+    ALL_EMPLOYEES   => \@employees_with_email,
+    ALL_PARTNER_EMAIL_ADDRESSES => $all_partner_email_addresses,
+  });
+
+  print $::form->ajax_response_header, $html;
+}
+
+sub send_sales_purchase_email {
+  my $type        = $::form->{type};
+  my $id          = $::form->{id};
+  my $script      = $type =~ m{sales_order|purchase_order|quotation} ? 'oe.pl'
+                  : $type =~ m{delivery_}                            ? 'do.pl'
+                  :                                                    'is.pl';
+
+  my $email_form  = delete $::form->{email_form};
+
+  if ($email_form->{additional_to}) {
+    $email_form->{to} = join ', ', grep { $_ } $email_form->{to}, @{$email_form->{additional_to}};
+    delete $email_form->{additional_to};
+  }
+
+  my %field_names = (to => 'email');
+
+  $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
+
+  $::form->{media} = 'email';
+
+  $::form->{attachment_policy} //= '';
+
+  # Is an old file version available?
+  my $attfile;
+  if ($::form->{attachment_policy} eq 'old_file') {
+    $attfile = SL::File->get_all(object_id     => $id,
+                                 object_type   => $type,
+                                 file_type     => 'document',
+                                 print_variant => $::form->{formname},);
+  }
+
+  if ($::form->{attachment_policy} eq 'no_file' || ($::form->{attachment_policy} eq 'old_file' && $attfile)) {
+    $::form->send_email(\%::myconfig, 'pdf');
+
+  } else {
+    print_form("return");
+    Common->save_email_status(\%::myconfig, $::form);
+  }
+
+  flash_later('info', $::locale->text('The email has been sent.'));
+
+  print $::form->redirect_header($script . '?action=edit&id=' . $::form->escape($id) . '&type=' . $::form->escape($type));
+}
+
+sub _maybe_attach_zugferd_data {
+  my ($form) = @_;
+
+  my $record = _make_record();
+
+  return if !$record
+    || !$record->can('customer')
+    || !$record->customer
+    || !$record->can('create_pdf_a_print_options')
+    || !$record->can('create_zugferd_data')
+    || !$record->customer->create_zugferd_invoices_for_this_customer;
+
+  eval {
+    my $xmlfile = File::Temp->new;
+    $xmlfile->print($record->create_zugferd_data);
+    $xmlfile->close;
+
+    $form->{TEMPLATE_DRIVER_OPTIONS}->{pdf_a}           = $record->create_pdf_a_print_options(zugferd_xmp_data => $record->create_zugferd_xmp_data);
+    $form->{TEMPLATE_DRIVER_OPTIONS}->{pdf_attachments} = [
+      { source       => $xmlfile,
+        name         => 'factur-x.xml',
+        description  => $::locale->text('Factur-X/ZUGFeRD invoice'),
+        relationship => '/Alternative',
+        mime_type    => 'text/xml',
+      }
+    ];
+  };
+
+  if (my $e = SL::X::ZUGFeRDValidation->caught) {
+    $::form->error($e->message);
+  }
+}
+
+sub download_factur_x_xml {
+  my ($form) = @_;
+
+  my $record = _make_record();
+
+  die if !$record
+      || !$record->can('customer')
+      || !$record->customer
+      || !$record->can('create_pdf_a_print_options')
+      || !$record->can('create_zugferd_data')
+      || !$record->customer->create_zugferd_invoices_for_this_customer;
+
+  my $xml_content = eval { $record->create_zugferd_data };
+
+  if (my $e = SL::X::ZUGFeRDValidation->caught) {
+    $::form->error($e->message);
+  }
+
+  my $attachment_filename = $::form->generate_attachment_filename;
+  $attachment_filename    =~ s{\.[^.]+$}{.xml};
+  my %headers             = (
+    '-type'           => 'application/xml',
+    '-connection'     => 'close',
+    '-attachment'     => $attachment_filename,
+    '-content-length' => length($xml_content),
+  );
+
+  print $::request->cgi->header(%headers);
+
+  $::locale->with_raw_io(\*STDOUT, sub { print $xml_content });
+}
index 6bcba9b..b0df30c 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Inventory received module
 #======================================================================
 
 use SL::FU;
+use SL::Helper::Flash qw(flash_later);
+use SL::Helper::UserPreferences::DisplayPreferences;
 use SL::IR;
 use SL::IS;
-use SL::PE;
+use SL::DB::BankTransactionAccTrans;
 use SL::DB::Default;
+use SL::DB::Department;
+use SL::DB::Project;
 use SL::DB::PurchaseInvoice;
+use SL::DB::Vendor;
+use List::MoreUtils qw(uniq);
 use List::Util qw(max sum);
 use List::UtilsBy qw(sort_by);
 
 require "bin/mozilla/io.pl";
-require "bin/mozilla/invoice_io.pl";
-require "bin/mozilla/arap.pl";
 require "bin/mozilla/common.pl";
-require "bin/mozilla/drafts.pl";
 
 use strict;
 
@@ -52,6 +56,21 @@ use strict;
 
 # end of main
 
+sub _may_view_or_edit_this_invoice {
+  return 1 if  $::auth->assert('ap_transactions', 1);       # may edit all invoices
+  return 0 if !$::form->{id};                               # creating new invoices isn't allowed without invoice_edit
+  return 1 if  $::auth->assert('purchase_invoice_view', 1); # viewing is allowed with this right
+  return 0 if !$::form->{globalproject_id};                 # existing records without a project ID are not allowed
+  return SL::DB::Project->new(id => $::form->{globalproject_id})->load->may_employee_view_project_invoices(SL::DB::Manager::Employee->current);
+}
+
+sub _assert_access {
+  my $cache = $::request->cache('ap.pl::_assert_access');
+
+  $cache->{_may_view_or_edit_this_invoice} = _may_view_or_edit_this_invoice()                              if !exists $cache->{_may_view_or_edit_this_invoice};
+  $::form->show_generic_error($::locale->text("You do not have the permissions to access this function.")) if !       $cache->{_may_view_or_edit_this_invoice};
+}
+
 sub add {
   $main::lxdebug->enter_sub();
 
@@ -64,7 +83,7 @@ sub add {
     $::form->show_generic_error($::locale->text("You do not have the permissions to access this function."));
   }
 
-  return $main::lxdebug->leave_sub() if (load_draft_maybe());
+  $form->{show_details} = $::myconfig{show_form_details};
 
   $form->{title} = $locale->text('Record Vendor Invoice');
 
@@ -78,10 +97,14 @@ sub add {
 sub edit {
   $main::lxdebug->enter_sub();
 
+  # Delay access check to after the invoice's been loaded in
+  # "create_links" so that project-specific invoice rights can be
+  # evaluated.
+
   my $form     = $main::form;
   my $locale   = $main::locale;
 
-  $main::auth->assert('vendor_invoice_edit');
+  $form->{show_details} = $::myconfig{show_form_details};
 
   # show history button
   $form->{javascript} = qq|<script type=text/javascript src=js/show_history.js></script>|;
@@ -99,28 +122,18 @@ sub edit {
 sub invoice_links {
   $main::lxdebug->enter_sub();
 
+  # Delay access check to after the invoice's been loaded so that
+  # project-specific invoice rights can be evaluated.
+
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  $main::auth->assert('vendor_invoice_edit');
-
   $form->{vc} = 'vendor';
 
   # create links
-  $form->{webdav}   = $::instance_conf->get_webdav;
-
   $form->create_links("AP", \%myconfig, "vendor");
 
-  #quote all_vendor Bug 133
-  foreach my $ref (@{ $form->{all_vendor} }) {
-    $ref->{name} = $form->quote($ref->{name});
-  }
-
-  if ($form->{all_vendor}) {
-    unless ($form->{vendor_id}) {
-      $form->{vendor_id} = $form->{all_vendor}->[0]->{id};
-    }
-  }
+  _assert_access();
 
   $form->backup_vars(qw(payment_id language_id taxzone_id
                         currency delivery_term_id intnotes cp_id));
@@ -134,25 +147,6 @@ sub invoice_links {
   my @curr = $form->get_all_currencies();
   map { $form->{selectcurrency} .= "<option>$_\n" } @curr;
 
-  $form->{oldvendor} = "$form->{vendor}--$form->{vendor_id}";
-
-  # build vendor/customer drop down comatibility... don't ask
-  if (@{ $form->{"all_vendor"} || [] }) {
-    $form->{"selectvendor"} = 1;
-    $form->{vendor}         = qq|$form->{vendor}--$form->{vendor_id}|;
-  }
-
-  # departments
-  if ($form->{all_departments}) {
-    $form->{selectdepartment} = "<option>\n";
-    $form->{department}       = "$form->{department}--$form->{department_id}";
-
-    map {
-      $form->{selectdepartment} .=
-        "<option>$_->{description}--$_->{id}\n"
-    } (@{ $form->{all_departments} || [] });
-  }
-
   # forex
   $form->{forex} = $form->{exchangerate};
   my $exchangerate = ($form->{exchangerate}) ? $form->{exchangerate} : 1;
@@ -204,11 +198,11 @@ sub invoice_links {
 sub prepare_invoice {
   $main::lxdebug->enter_sub();
 
+  _assert_access();
+
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  $main::auth->assert('vendor_invoice_edit');
-
   $form->{type}     = "purchase_invoice";
 
   if ($form->{id}) {
@@ -247,37 +241,189 @@ sub prepare_invoice {
   $main::lxdebug->leave_sub();
 }
 
+sub setup_ir_action_bar {
+  my $form                    = $::form;
+  my $change_never            = $::instance_conf->get_ir_changeable == 0;
+  my $change_on_same_day_only = $::instance_conf->get_ir_changeable == 2 && ($form->current_date(\%::myconfig) ne $form->{gldate});
+  my $has_storno              = ($::form->{storno} && !$::form->{storno_id});
+  my $payments_balanced       = ($::form->{oldtotalpaid} == 0);
+  my $may_edit_create         = $::auth->assert('vendor_invoice_edit', 1);
+
+  my $has_sepa_exports;
+  if ($form->{id}) {
+    my $invoice = SL::DB::Manager::PurchaseInvoice->find_by(id => $form->{id});
+    $has_sepa_exports = 1 if ($invoice->find_sepa_export_items()->[0]);
+  }
+
+  my $is_linked_bank_transaction;
+  if ($::form->{id}
+      && SL::DB::Default->get->payments_changeable != 0
+      && SL::DB::Manager::BankTransactionAccTrans->find_by(ap_id => $::form->{id})) {
+
+    $is_linked_bank_transaction = 1;
+  }
+
+  my $create_post_action = sub {
+    # $_[0]: description
+    # $_[1]: after_action
+    action => [
+      $_[0],
+      submit   => [ '#form', { action => "post", after_action => $_[1] } ],
+      checks   => [ 'kivi.validate_form' ],
+      checks   => [ 'kivi.validate_form', 'kivi.AP.check_fields_before_posting', 'kivi.AP.check_duplicate_invnumber' ],
+      disabled => !$may_edit_create                         ? t8('You must not change this invoice.')
+                : $form->{locked}                           ? t8('The billing period has already been locked.')
+                : $form->{storno}                           ? t8('A canceled invoice cannot be posted.')
+                : ($form->{id} && $change_never)            ? t8('Changing invoices has been disabled in the configuration.')
+                : ($form->{id} && $change_on_same_day_only) ? t8('Invoices can only be changed on the day they are posted.')
+                : $is_linked_bank_transaction               ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                :                                             undef,
+    ],
+  };
+
+  my @post_entries;
+  if ($::instance_conf->get_ir_add_doc && $::instance_conf->get_doc_storage) {
+    @post_entries = ( $create_post_action->(t8('Post'), 'doc-tab') );
+  } elsif ($::instance_conf->get_doc_storage) {
+    @post_entries = ( $create_post_action->(t8('Post')),
+                      $create_post_action->(t8('Post and upload document'), 'doc-tab') );
+  } else {
+    @post_entries = ( $create_post_action->(t8('Post')) );
+  }
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#form', { action => "update" } ],
+        id        => 'update_button',
+        accesskey => 'enter',
+        disabled  => !$may_edit_create ? t8('You must not change this invoice.') : undef,
+      ],
+      combobox => [
+        @post_entries,
+        action => [
+          t8('Post Payment'),
+          submit   => [ '#form', { action => "post_payment" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create           ? t8('You must not change this invoice.')
+                    : !$form->{id}                ? t8('This invoice has not been posted yet.')
+                    : $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                    :                               undef,
+        ],
+        action => [
+          t8('Mark as paid'),
+          submit   => [ '#form', { action => "mark_as_paid" } ],
+          checks   => [ 'kivi.validate_form' ],
+          confirm  => t8('This will remove the invoice from showing as unpaid even if the unpaid amount does not match the amount. Proceed?'),
+          disabled => !$may_edit_create ? t8('You must not change this invoice.')
+                    : !$form->{id}      ? t8('This invoice has not been posted yet.')
+                    :                     undef,
+          only_if  => $::instance_conf->get_ir_show_mark_as_paid,
+        ],
+      ], # end of combobox "Post"
+
+      combobox => [
+        action => [ t8('Storno'),
+          submit   => [ '#form', { action => "storno" } ],
+          checks   => [ 'kivi.validate_form' ],
+          confirm  => t8('Do you really want to cancel this invoice?'),
+          disabled => !$may_edit_create   ? t8('You must not change this invoice.')
+                    : !$form->{id}        ? t8('This invoice has not been posted yet.')
+                    : $has_sepa_exports   ? t8('This invoice has been linked with a sepa export, undo this first.')
+                    : !$payments_balanced ? t8('Cancelling is disallowed. Either undo or balance the current payments until the open amount matches the invoice amount')
+                    : undef,
+        ],
+        action => [ t8('Delete'),
+          submit   => [ '#form', { action => "delete" } ],
+          checks   => [ 'kivi.validate_form' ],
+          confirm  => t8('Do you really want to delete this object?'),
+          disabled => !$may_edit_create           ? t8('You must not change this invoice.')
+                    : !$form->{id}                ? t8('This invoice has not been posted yet.')
+                    : $form->{locked}             ? t8('The billing period has already been locked.')
+                    : $change_never               ? t8('Changing invoices has been disabled in the configuration.')
+                    : $change_on_same_day_only    ? t8('Invoices can only be changed on the day they are posted.')
+                    : $has_sepa_exports           ? t8('This invoice has been linked with a sepa export, undo this first.')
+                    : $has_storno                 ? t8('Can only delete the "Storno zu" part of the cancellation pair.')
+                    : $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                    :                            undef,
+        ],
+      ], # end of combobox "Storno"
+
+      'separator',
+
+      combobox => [
+        action => [ t8('Workflow') ],
+        action => [
+          t8('Use As New'),
+          submit   => [ '#form', { action => "use_as_new" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create ? t8('You must not change this invoice.')
+                    : !$form->{id}      ? t8('This invoice has not been posted yet.')
+                    :                     undef,
+        ],
+       ], # end of combobox "Workflow"
+
+      combobox => [
+        action => [ t8('more') ],
+        action => [
+          t8('History'),
+          call     => [ 'set_history_window', $::form->{id} * 1, 'glid' ],
+          disabled => !$form->{id} ? t8('This invoice has not been posted yet.') : undef,
+        ],
+        action => [
+          t8('Follow-Up'),
+          call     => [ 'follow_up_window' ],
+          disabled => !$form->{id} ? t8('This invoice has not been posted yet.') : undef,
+        ],
+        action => [
+          t8('Drafts'),
+          call     => [ 'kivi.Draft.popup', 'ir', 'invoice', $::form->{draft_id}, $::form->{draft_description} ],
+          disabled => !$may_edit_create ? t8('You must not change this invoice.')
+                    : $form->{id}       ? t8('This invoice has already been posted.')
+                    : $form->{locked}   ? t8('The billing period has already been locked.')
+                    :                     undef,
+        ],
+      ], # end of combobox "more"
+    );
+  }
+  $::request->layout->add_javascripts('kivi.Validator.js', 'kivi.AP.js');
+
+}
+
 sub form_header {
   $main::lxdebug->enter_sub();
 
+  _assert_access();
+
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
   my $cgi      = $::request->{cgi};
 
-  $main::auth->assert('vendor_invoice_edit');
-
   my %TMPL_VAR = ();
   my @custom_hiddens;
 
-  $TMPL_VAR{invoice_obj} = SL::DB::PurchaseInvoice->new(id => $form->{id})->load if $form->{id};
-  $form->{employee_id} = $form->{old_employee_id} if $form->{old_employee_id};
-  $form->{salesman_id} = $form->{old_salesman_id} if $form->{old_salesman_id};
+  $TMPL_VAR{invoice_obj} = SL::DB::PurchaseInvoice->load_cached($form->{id}) if $form->{id};
+  $TMPL_VAR{vendor_obj}  = SL::DB::Vendor->load_cached($form->{vendor_id})   if $form->{vendor_id};
+  my $current_employee   = SL::DB::Manager::Employee->current;
+  $form->{employee_id}   = $form->{old_employee_id} if $form->{old_employee_id};
+  $form->{salesman_id}   = $form->{old_salesman_id} if $form->{old_salesman_id};
+  $form->{employee_id} ||= $current_employee->id;
+  $form->{salesman_id} ||= $current_employee->id;
 
   $form->{defaultcurrency} = $form->get_default_currency(\%myconfig);
 
-  my @old_project_ids = ($form->{"globalproject_id"});
-  map { push @old_project_ids, $form->{"project_id_$_"} if $form->{"project_id_$_"}; } 1..$form->{"rowcount"};
+  my @old_project_ids     = uniq grep { $_ } map { $_ * 1 } ($form->{"globalproject_id"}, map { $form->{"project_id_$_"} } 1..$form->{"rowcount"});
+  my @conditions          = @old_project_ids ? (id => \@old_project_ids) : ();
+  $TMPL_VAR{ALL_PROJECTS} = SL::DB::Manager::Project->get_all_sorted(query => [ or => [ active => 1, @conditions ]]);
+  $form->{ALL_PROJECTS}   = $TMPL_VAR{ALL_PROJECTS}; # make projects available for second row drop-down in io.pl
 
-  $form->get_lists("projects"      => { "key"    => "ALL_PROJECTS",
-                                        "all"    => 0,
-                                        "old_id" => \@old_project_ids },
-                   "taxzones"      => "ALL_TAXZONES",
+  $form->get_lists("taxzones"      => ($form->{id} ? "ALL_TAXZONES" : "ALL_ACTIVE_TAXZONES"),
                    "currencies"    => "ALL_CURRENCIES",
-                   "vendors"       => "ALL_VENDORS",
-                   "departments"   => "all_departments",
                    "price_factors" => "ALL_PRICE_FACTORS");
 
+  $TMPL_VAR{ALL_DEPARTMENTS}       = SL::DB::Manager::Department->get_all_sorted;
   $TMPL_VAR{ALL_EMPLOYEES}         = SL::DB::Manager::Employee->get_all_sorted(query => [ or => [ id => $::form->{employee_id},  deleted => 0 ] ]);
   $TMPL_VAR{ALL_CONTACTS}          = SL::DB::Manager::Contact->get_all_sorted(query => [
     or => [
@@ -288,15 +434,6 @@ sub form_header {
       ]
     ]
   ]);
-  $TMPL_VAR{department_labels}     = sub { "$_[0]->{description}--$_[0]->{id}" };
-
-  # customer
-  $TMPL_VAR{vc_keys} = sub { "$_[0]->{name}--$_[0]->{id}" };
-  $TMPL_VAR{vclimit} = $myconfig{vclimit};
-  $TMPL_VAR{vc_select} = "customer_or_vendor_selection_window('vendor', '', 1, 0)";
-  push @custom_hiddens, "vendor_id";
-  push @custom_hiddens, "oldvendor";
-  push @custom_hiddens, "selectvendor";
 
   # currencies and exchangerate
   my @values = map { $_       } @{ $form->{ALL_CURRENCIES} };
@@ -314,36 +451,34 @@ sub form_header {
   $TMPL_VAR{creditwarning} = ($form->{creditlimit} != 0) && ($form->{creditremaining} < 0) && !$form->{update};
   $TMPL_VAR{is_credit_remaining_negativ} = $form->{creditremaining} =~ /-/;
 
-  my $follow_up_vc         =  $form->{vendor};
-  $follow_up_vc            =~ s/--\d*\s*$//;
-  $TMPL_VAR{vendor_name} = $follow_up_vc;
-
 # set option selected
   foreach my $item (qw(AP)) {
     $form->{"select$item"} =~ s/ selected//;
     $form->{"select$item"} =~ s/option>\Q$form->{$item}\E/option selected>$form->{$item}/;
   }
 
-  $TMPL_VAR{is_type_credit_note} = $form->{type}   eq "credit_note";
-  $TMPL_VAR{is_format_html}      = $form->{format} eq 'html';
-  $TMPL_VAR{dateformat}          = $myconfig{dateformat};
-  $TMPL_VAR{numberformat}        = $myconfig{numberformat};
+  $TMPL_VAR{is_format_html}                         = $form->{format} eq 'html';
+  $TMPL_VAR{dateformat}                             = $myconfig{dateformat};
+  $TMPL_VAR{numberformat}                           = $myconfig{numberformat};
+  $TMPL_VAR{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
 
   # hiddens
   $TMPL_VAR{HIDDENS} = [qw(
-    id action type media format queued printed emailed title vc discount
+    id type queued printed emailed title vc discount
     title creditlimit creditremaining tradediscount business closedto locked shipped storno storno_id
     max_dunning_level dunning_amount
-    shiptoname shiptostreet shiptozipcode shiptocity shiptocountry  shiptocontact shiptophone shiptofax
+    shiptoname shiptostreet shiptozipcode shiptocity shiptocountry shiptogln shiptocontact shiptophone shiptofax
     shiptoemail shiptodepartment_1 shiptodepartment_2 message email subject cc bcc taxaccounts cursor_fokus
-    convert_from_do_ids convert_from_oe_ids show_details gldate useasnew
+    convert_from_do_ids convert_from_oe_ids convert_from_ap_ids show_details gldate useasnew
   ), @custom_hiddens,
-  map { $_.'_rate', $_.'_description', $_.'_taxnumber' } split / /, $form->{taxaccounts}];
+  map { $_.'_rate', $_.'_description', $_.'_taxnumber', $_.'_tax_id' } split / /, $form->{taxaccounts}];
 
   $TMPL_VAR{payment_terms_obj} = get_payment_terms_for_invoice();
-  $form->{duedate}             = $TMPL_VAR{payment_terms_obj}->calc_date(reference_date => $form->{invdate}, due_date => $form->{due_due})->to_kivitendo if $TMPL_VAR{payment_terms_obj};
+  $form->{duedate}             = $TMPL_VAR{payment_terms_obj}->calc_date(reference_date => $form->{invdate}, due_date => $form->{duedate})->to_kivitendo if $TMPL_VAR{payment_terms_obj};
+
+  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.Draft kivi.File kivi.SalesPurchase kivi.Part kivi.CustomerVendor kivi.Validator ckeditor/ckeditor ckeditor/adapters/jquery kivi.io autocomplete_project client_js));
 
-  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.SalesPurchase ckeditor/ckeditor ckeditor/adapters/jquery kivi.io autocomplete_customer autocomplete_part client_js));
+  setup_ir_action_bar();
 
   $form->header();
 
@@ -353,7 +488,7 @@ sub form_header {
 }
 
 sub _sort_payments {
-  my @fields   = qw(acc_trans_id gldate datepaid source memo paid AR_paid);
+  my @fields   = qw(acc_trans_id gldate datepaid source memo paid AP_paid);
   my @payments =
     grep { $_->{paid} != 0 }
     map  {
@@ -374,21 +509,15 @@ sub _sort_payments {
 sub form_footer {
   $main::lxdebug->enter_sub();
 
+  _assert_access();
+
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
-  $main::auth->assert('vendor_invoice_edit');
-
   $form->{invtotal}    = $form->{invsubtotal};
   $form->{oldinvtotal} = $form->{invtotal};
 
-  # note rows
-  $form->{rows} = max 2,
-    $form->numtextrows($form->{notes},    26, 8),
-    $form->numtextrows($form->{intnotes}, 35, 8);
-
-
   # tax, total and subtotal calculations
   my ($tax, $subtotal);
   $form->{taxaccounts_array} = [ split / /, $form->{taxaccounts} ];
@@ -408,7 +537,7 @@ sub form_footer {
 
   # follow ups
   if ($form->{id}) {
-    $form->{follow_ups}            = FU->follow_ups('trans_id' => $form->{id}) || [];
+    $form->{follow_ups}            = FU->follow_ups('trans_id' => $form->{id}, 'not_done' => 1) || [];
     $form->{follow_ups_unfinished} = ( sum map { $_->{due} * 1 } @{ $form->{follow_ups} } ) || 0;
   }
 
@@ -433,6 +562,14 @@ sub form_footer {
                                   ($form->current_date(\%myconfig) eq $form->{"gldate_$i"}));
     }
 
+    $form->error($locale->text('Cannot post transaction above the maximum future booking date!'))
+      if ($form->date_max_future($form->{"datepaid_$i"}, \%myconfig));
+
+    #deaktivieren von Zahlungen ausserhalb der Bücherkontrolle
+    if ($form->date_closed($form->{"gldate_$i"})) {
+      $form->{"changeable_$i"} = 0;
+    }
+
     $form->{"selectAP_paid_$i"} = $form->{selectAP_paid};
     if (!$form->{"AP_paid_$i"}) {
       $form->{"selectAP_paid_$i"} =~ s/option>$accno_arap--(.*?)>/option selected>$accno_arap--$1>/;
@@ -446,7 +583,6 @@ sub form_footer {
   $form->{ALL_DELIVERY_TERMS} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
   print $form->parse_html_template('ir/form_footer', {
-    is_type_credit_note => ($form->{type} eq "credit_note"),
     totalpaid           => $totalpaid,
     paid_missing        => $form->{invtotal} - $totalpaid,
     show_storno         => $form->{id} && !$form->{storno} && !IS->has_storno(\%myconfig, $form, "ap") && !$totalpaid,
@@ -462,16 +598,15 @@ sub form_footer {
 }
 
 sub mark_as_paid {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
+  $::auth->assert('vendor_invoice_edit');
 
-  $main::auth->assert('vendor_invoice_edit');
+  SL::DB::PurchaseInvoice->new(id => $::form->{id})->load->mark_as_paid;
 
-  &mark_as_paid_common(\%myconfig,"ap");
+  $::form->redirect($::locale->text("Marked as paid"));
+}
 
-  $main::lxdebug->leave_sub();
+sub show_draft {
+  update();
 }
 
 sub update {
@@ -482,7 +617,9 @@ sub update {
 
   $main::auth->assert('vendor_invoice_edit');
 
-  &check_name('vendor');
+  if (($form->{previous_vendor_id} || $form->{vendor_id}) != $form->{vendor_id}) {
+    IR->get_vendor(\%myconfig, $form);
+  }
 
   if (!$form->{forex}) {        # read exchangerate from input field (not hidden)
     $form->{exchangerate} = $form->parse_amount(\%myconfig, $form->{exchangerate});
@@ -524,7 +661,7 @@ sub update {
       if ($rows > 1) {
 
         select_item(mode => 'IR', pre_entered_qty => $form->{"qty_$i"});
-        ::end_of_request();
+        $::dispatcher->end_request;
 
       } else {
 
@@ -620,7 +757,8 @@ sub storno {
   $form->{paidaccounts} = 0;
   map { my $key = $_; delete $form->{$key} if grep { $key =~ /^$_/ } qw(datepaid_ gldate_ acc_trans_id_ source_ memo_ paid_ exchangerate_ AR_paid_) } keys %{ $form };
   # set new ids for storno invoice
-  delete $form->{"invoice_id_$_"} for 1 .. $form->{"rowcount"};
+  # set new persistent ids for storno invoice items
+  $form->{"converted_from_invoice_id_$_"} = delete $form->{"invoice_id_$_"} for 1 .. $form->{"rowcount"};
 
   # saving the history
   if(!exists $form->{addition} && $form->{id} ne "") {
@@ -631,6 +769,8 @@ sub storno {
   }
   # /saving the history
 
+  # record link invoice to storno
+  $form->{convert_from_ap_ids} = $form->{id};
   $form->{storno_id} = $form->{id};
   $form->{storno} = 1;
   $form->{id} = "";
@@ -680,8 +820,13 @@ sub post_payment {
 
       $form->isblank("datepaid_$i", $locale->text('Payment date missing!'));
 
+      $form->error($locale->text('Cannot post transaction above the maximum future booking date!'))
+        if ($form->date_max_future($form->{"datepaid_$i"}, \%myconfig));
+
+      #Zusätzlich noch das Buchungsdatum in die Bücherkontrolle einbeziehen
+      # (Dient zur Prüfung ob ZE oder ZA geprüft werden soll)
       $form->error($locale->text('Cannot post payment for a closed period!'))
-        if ($form->date_closed($form->{"datepaid_$i"}, \%myconfig));
+        if ($form->date_closed($form->{"datepaid_$i"})  && !$form->date_closed($form->{"gldate_$i"}, \%myconfig));
 
       if ($form->{currency} ne $form->{defaultcurrency}) {
 #        $form->{"exchangerate_$i"} = $form->{exchangerate} if ($invdate == $datepaid); # invdate isn't set here
@@ -736,16 +881,16 @@ sub post {
   $form->{defaultcurrency} = $form->get_default_currency(\%myconfig);
 
   $form->isblank("invdate",   $locale->text('Invoice Date missing!'));
-  $form->isblank("vendor",    $locale->text('Vendor missing!'));
+  $form->isblank("vendor_id", $locale->text('Vendor missing!'));
   $form->isblank("invnumber", $locale->text('Invnumber missing!'));
 
   $form->{invnumber} =~ s/^\s*//g;
   $form->{invnumber} =~ s/\s*$//g;
 
   # if the vendor changed get new values
-  if (&check_name('vendor')) {
+  if (($form->{previous_vendor_id} || $form->{vendor_id}) != $form->{vendor_id}) {
     &update;
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
 
   if ($myconfig{mandatory_departments} && !$form->{department_id}) {
@@ -776,8 +921,13 @@ sub post {
 
       $form->isblank("datepaid_$i", $locale->text('Payment date missing!'));
 
+      $form->error($locale->text('Cannot post transaction above the maximum future booking date!'))
+        if ($form->date_max_future($form->{"datepaid_$i"}, \%myconfig));
+
+      #Zusätzlich noch das Buchungsdatum in die Bücherkontrolle einbeziehen
+      # (Dient zur Prüfung ob ZE oder ZA geprüft werden soll)
       $form->error($locale->text('Cannot post payment for a closed period!'))
-        if ($form->date_closed($form->{"datepaid_$i"}, \%myconfig));
+        if ($form->date_closed($form->{"datepaid_$i"})  && !$form->date_closed($form->{"gldate_$i"}, \%myconfig));
 
       if ($form->{currency} ne $form->{defaultcurrency}) {
         $form->{"exchangerate_$i"} = $form->{exchangerate}
@@ -805,10 +955,21 @@ sub post {
       $form->save_history;
     }
     # /saving the history
-    remove_draft() if $form->{remove_draft};
-    $form->redirect(  $locale->text('Invoice')
-                  . " $form->{invnumber} "
-                  . $locale->text('posted!'));
+
+    my $redirect_url;
+    if ('doc-tab' eq $form->{after_action}) {
+      $redirect_url = build_std_url("script=ir.pl", 'action=edit', 'id=' . E($form->{id}), 'fragment=ui-tabs-docs');
+    } else {
+      $redirect_url = build_std_url("script=ir.pl", 'action=edit', 'id=' . E($form->{id}));
+    }
+    SL::Helper::Flash::flash_later('info',
+                                   $locale->text('Invoice')
+                                   . " $form->{invnumber} "
+                                   . ", " . $locale->text('ID')
+                                   . ': ' . $form->{id} . ' '
+                                   . $locale->text('posted!'));
+    print $form->redirect_header($redirect_url);
+    $::dispatcher->end_request;
   }
   $form->error($locale->text('Cannot post invoice!'));
 
@@ -852,6 +1013,27 @@ sub delete {
   $main::lxdebug->leave_sub();
 }
 
+sub display_form {
+  $::lxdebug->enter_sub;
+
+  _assert_access();
+
+  relink_accounts();
+
+  my $new_rowcount = $::form->{"rowcount"} * 1 + 1;
+  $::form->{"project_id_${new_rowcount}"} = $::form->{"globalproject_id"};
+
+  $::form->language_payment(\%::myconfig);
+
+  Common::webdav_folder($::form);
+
+  form_header();
+  display_row(++$::form->{rowcount});
+  form_footer();
+
+  $::lxdebug->leave_sub;
+}
+
 sub yes {
   $main::lxdebug->enter_sub();
 
index 46abc9e..d0e2506 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Inventory invoicing module
 
 use SL::FU;
 use SL::IS;
-use SL::PE;
 use SL::OE;
+use SL::Helper::UserPreferences::DisplayPreferences;
+use SL::MoreCommon qw(restore_form save_form);
+use SL::RecordLinks;
+
 use Data::Dumper;
 use DateTime;
-use List::MoreUtils qw(uniq);
+use List::MoreUtils qw(any uniq);
 use List::Util qw(max sum);
 use List::UtilsBy qw(sort_by);
 use English qw(-no_match_vars);
 
+use SL::DB::BankTransactionAccTrans;
 use SL::DB::Default;
 use SL::DB::Customer;
+use SL::DB::Department;
+use SL::DB::Invoice;
 use SL::DB::PaymentTerm;
 
+require "bin/mozilla/common.pl";
 require "bin/mozilla/io.pl";
-require "bin/mozilla/invoice_io.pl";
-require "bin/mozilla/arap.pl";
-require "bin/mozilla/drafts.pl";
 
 use strict;
 
@@ -57,6 +62,21 @@ use strict;
 
 # end of main
 
+sub _may_view_or_edit_this_invoice {
+  return 1 if  $::auth->assert('invoice_edit', 1);       # may edit all invoices
+  return 0 if !$::form->{id};                            # creating new invoices isn't allowed without invoice_edit
+  return 1 if  $::auth->assert('sales_invoice_view', 1); # viewing is allowed with this right
+  return 0 if !$::form->{globalproject_id};              # existing records without a project ID are not allowed
+  return SL::DB::Project->new(id => $::form->{globalproject_id})->load->may_employee_view_project_invoices(SL::DB::Manager::Employee->current);
+}
+
+sub _assert_access {
+  my $cache = $::request->cache('is.pl::_assert_access');
+
+  $cache->{_may_view_or_edit_this_invoice} = _may_view_or_edit_this_invoice()                              if !exists $cache->{_may_view_or_edit_this_invoice};
+  $::form->show_generic_error($::locale->text("You do not have the permissions to access this function.")) if !       $cache->{_may_view_or_edit_this_invoice};
+}
+
 sub add {
   $main::lxdebug->enter_sub();
 
@@ -65,7 +85,7 @@ sub add {
 
   $main::auth->assert('invoice_edit');
 
-  return $main::lxdebug->leave_sub() if (load_draft_maybe());
+  $form->{show_details} = $::myconfig{show_form_details};
 
   if ($form->{type} eq "credit_note") {
     $form->{title} = $locale->text('Add Credit Note');
@@ -73,6 +93,13 @@ sub add {
     if ($form->{storno}) {
       $form->{title} = $locale->text('Add Storno Credit Note');
     }
+
+  } elsif ($form->{type} eq "invoice_for_advance_payment") {
+    $form->{title} = $locale->text('Add Invoice for Advance Payment');
+
+  } elsif ($form->{type} eq "final_invoice") {
+    $form->{title} = $locale->text('Add Final Invoice');
+
   } else {
     $form->{title} = $locale->text('Add Sales Invoice');
 
@@ -81,7 +108,7 @@ sub add {
 
   $form->{callback} = "$form->{script}?action=add&type=$form->{type}" unless $form->{callback};
 
-  &invoice_links;
+  invoice_links(is_new => 1);
   &prepare_invoice;
   &display_form;
 
@@ -91,11 +118,14 @@ sub add {
 sub edit {
   $main::lxdebug->enter_sub();
 
+  # Delay access check to after the invoice's been loaded in
+  # "invoice_links" so that project-specific invoice rights can be
+  # evaluated.
+
   my $form     = $main::form;
   my $locale   = $main::locale;
 
-  $main::auth->assert('invoice_edit');
-
+  $form->{show_details}                = $::myconfig{show_form_details};
   $form->{taxincluded_changed_by_user} = 1;
 
   # show history button
@@ -113,6 +143,14 @@ sub edit {
   if ($form->{type} eq "credit_note") {
     $form->{title} = $locale->text('Edit Credit Note');
     $form->{title} = $locale->text('Edit Storno Credit Note') if $form->{storno};
+
+  } elsif ($form->{type} eq "invoice_for_advance_payment") {
+    $form->{title} = $locale->text('Edit Invoice for Advance Payment');
+    $form->{title} = $locale->text('Edit Storno Invoice for Advance Payment') if $form->{storno};
+
+  } elsif ($form->{type} eq "final_invoice") {
+    $form->{title} = $locale->text('Edit Final Invoice');
+
   } else {
     $form->{title} = $locale->text('Edit Sales Invoice');
     $form->{title} = $locale->text('Edit Storno Invoice')     if $form->{storno};
@@ -132,24 +170,19 @@ sub edit {
 sub invoice_links {
   $main::lxdebug->enter_sub();
 
+  # Delay access check to after the invoice's been loaded so that
+  # project-specific invoice rights can be evaluated.
+
+  my %params   = @_;
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  $main::auth->assert('invoice_edit');
-
   $form->{vc} = 'customer';
 
   # create links
-  $form->{webdav}   = $::instance_conf->get_webdav;
-
   $form->create_links("AR", \%myconfig, "customer");
 
-  if ($form->{all_customer}) {
-    unless ($form->{customer_id}) {
-      $form->{customer_id} = $form->{all_customer}->[0]->{id};
-      $form->{salesman_id} = $form->{all_customer}->[0]->{salesman_id};
-    }
-  }
+  _assert_access();
 
   my $editing = $form->{id};
 
@@ -159,10 +192,7 @@ sub invoice_links {
 
   IS->get_customer(\%myconfig, \%$form);
 
-  #quote all_customer Bug 133
-  foreach my $ref (@{ $form->{all_customer} }) {
-    $ref->{name} = $form->quote($ref->{name});
-  }
+  $form->{billing_address_id} = $form->{default_billing_address_id} if $params{is_new};
 
   $form->restore_vars(qw(id));
 
@@ -172,23 +202,6 @@ sub invoice_links {
   $form->restore_vars(qw(taxincluded)) if $form->{id};
   $form->restore_vars(qw(salesman_id)) if $editing;
 
-
-  # build vendor/customer drop down compatibility... don't ask
-  if (@{ $form->{"all_customer"} }) {
-    $form->{"selectcustomer"} = 1;
-    $form->{customer}         = qq|$form->{customer}--$form->{"customer_id"}|;
-  }
-
-  $form->{"oldcustomer"}  = $form->{customer};
-
-  if ($form->{"oldcustomer"} !~ m/--\d+$/ && $form->{"customer_id"}) {
-    $form->{"oldcustomer"} .= qq|--$form->{"customer_id"}|
-  }
-
-
-#  $form->{oldcustomer} = "$form->{customer}--$form->{customer_id}";
-#  $form->{selectcustomer} = 1;
-
   $form->{employee} = "$form->{employee}--$form->{employee_id}";
 
   # forex
@@ -235,14 +248,26 @@ sub invoice_links {
 sub prepare_invoice {
   $main::lxdebug->enter_sub();
 
+  _assert_access();
+
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  $main::auth->assert('invoice_edit');
-
   if ($form->{type} eq "credit_note") {
     $form->{type}     = "credit_note";
     $form->{formname} = "credit_note";
+
+  } elsif ($form->{type} eq "invoice_for_advance_payment") {
+    $form->{type}     = "invoice_for_advance_payment";
+    $form->{formname} = "invoice_for_advance_payment";
+
+  } elsif ($form->{type} eq "final_invoice") {
+    $form->{type}     = "final_invoice";
+    $form->{formname} = "final_invoice";
+
+  } elsif ($form->{formname} eq "proforma" ) {
+    $form->{type}     = "invoice";
+
   } else {
     $form->{type}     = "invoice";
     $form->{formname} = "invoice";
@@ -279,34 +304,273 @@ sub prepare_invoice {
   $main::lxdebug->leave_sub();
 }
 
+sub setup_is_action_bar {
+  my ($tmpl_var)              = @_;
+  my $form                    = $::form;
+  my $change_never            = $::instance_conf->get_is_changeable == 0;
+  my $change_on_same_day_only = $::instance_conf->get_is_changeable == 2 && ($form->current_date(\%::myconfig) ne $form->{gldate});
+  my $payments_balanced       = ($::form->{oldtotalpaid} == 0);
+  my $has_storno              = ($::form->{storno} && !$::form->{storno_id});
+  my $may_edit_create         = $::auth->assert('invoice_edit', 1);
+  my $factur_x_enabled        = $tmpl_var->{invoice_obj} && $tmpl_var->{invoice_obj}->customer->create_zugferd_invoices_for_this_customer;
+  my ($is_linked_bank_transaction, $warn_unlinked_delivery_order);
+    if ($::form->{id}
+        && SL::DB::Default->get->payments_changeable != 0
+        && SL::DB::Manager::BankTransactionAccTrans->find_by(ar_id => $::form->{id})) {
+
+      $is_linked_bank_transaction = 1;
+    }
+  if ($::instance_conf->get_warn_no_delivery_order_for_invoice && !$form->{id}) {
+    $warn_unlinked_delivery_order = 1 unless $form->{convert_from_do_ids};
+  }
+
+  my $has_further_invoice_for_advance_payment;
+  if ($form->{id} && $form->{type} eq "invoice_for_advance_payment") {
+    my $invoice_obj = SL::DB::Invoice->load_cached($form->{id});
+    my $lr          = $invoice_obj->linked_records(direction => 'to', to => ['Invoice']);
+    $has_further_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
+  }
+
+  my $has_final_invoice;
+  if ($form->{id} && $form->{type} eq "invoice_for_advance_payment") {
+    my $invoice_obj = SL::DB::Invoice->load_cached($form->{id});
+    my $lr          = $invoice_obj->linked_records(direction => 'to', to => ['Invoice']);
+    $has_final_invoice = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->invoice_type} @$lr;
+  }
+
+  my $is_invoice_for_advance_payment_from_order;
+  if ($form->{id} && $form->{type} eq "invoice_for_advance_payment") {
+    my $invoice_obj = SL::DB::Invoice->load_cached($form->{id});
+    my $lr          = $invoice_obj->linked_records(direction => 'from', from => ['Order']);
+    $is_invoice_for_advance_payment_from_order = scalar @$lr >= 1;
+  }
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#form', { action => "update" } ],
+        disabled  => !$may_edit_create ? t8('You must not change this invoice.')
+                   : $form->{locked}   ? t8('The billing period has already been locked.')
+                   :                     undef,
+        id        => 'update_button',
+        accesskey => 'enter',
+      ],
+
+      combobox => [
+        action => [
+          t8('Post'),
+          submit   => [ '#form', { action => "post" } ],
+          checks   => [ 'kivi.validate_form' ],
+          confirm  => t8('The invoice is not linked with a sales delivery order. Post anyway?') x !!$warn_unlinked_delivery_order,
+          disabled => !$may_edit_create                         ? t8('You must not change this invoice.')
+                    : $form->{locked}                           ? t8('The billing period has already been locked.')
+                    : $form->{storno}                           ? t8('A canceled invoice cannot be posted.')
+                    : ($form->{id} && $change_never)            ? t8('Changing invoices has been disabled in the configuration.')
+                    : ($form->{id} && $change_on_same_day_only) ? t8('Invoices can only be changed on the day they are posted.')
+                    : $is_linked_bank_transaction               ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                    :                                             undef,
+        ],
+        action => [
+          t8('Post Payment'),
+          submit   => [ '#form', { action => "post_payment" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create           ? t8('You must not change this invoice.')
+                    : !$form->{id}                ? t8('This invoice has not been posted yet.')
+                    : $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                    :                               undef,
+          only_if  => $form->{type} ne "invoice_for_advance_payment",
+        ],
+        action => [ t8('Mark as paid'),
+          submit   => [ '#form', { action => "mark_as_paid" } ],
+          confirm  => t8('This will remove the invoice from showing as unpaid even if the unpaid amount does not match the amount. Proceed?'),
+          disabled => !$may_edit_create ? t8('You must not change this invoice.')
+                    : !$form->{id}      ? t8('This invoice has not been posted yet.')
+                    :                     undef,
+          only_if  => ($::instance_conf->get_is_show_mark_as_paid && $form->{type} ne "invoice_for_advance_payment") || $form->{type} eq 'final_invoice',
+        ],
+      ], # end of combobox "Post"
+
+      combobox => [
+        action => [ t8('Storno'),
+          submit   => [ '#form', { action => "storno" } ],
+          confirm  => t8('Do you really want to cancel this invoice?'),
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create   ? t8('You must not change this invoice.')
+                    : !$form->{id}        ? t8('This invoice has not been posted yet.')
+                    : $form->{storno}     ? t8('Cannot storno storno invoice!')
+                    : $form->{locked}     ? t8('The billing period has already been locked.')
+                    : !$payments_balanced ? t8('Cancelling is disallowed. Either undo or balance the current payments until the open amount matches the invoice amount')
+                    : undef,
+        ],
+        action => [ t8('Delete'),
+          submit   => [ '#form', { action => "delete" } ],
+          confirm  => t8('Do you really want to delete this object?'),
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create        ? t8('You must not change this invoice.')
+                    : !$form->{id}             ? t8('This invoice has not been posted yet.')
+                    : $form->{locked}          ? t8('The billing period has already been locked.')
+                    : $change_never            ? t8('Changing invoices has been disabled in the configuration.')
+                    : $change_on_same_day_only ? t8('Invoices can only be changed on the day they are posted.')
+                    : $has_storno              ? t8('Can only delete the "Storno zu" part of the cancellation pair.')
+                    :                            undef,
+        ],
+      ], # end of combobox "Storno"
+
+      'separator',
+
+      combobox => [
+        action => [ t8('Workflow') ],
+        action => [
+          t8('Use As New'),
+          submit   => [ '#form', { action => "use_as_new" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create ? t8('You must not change this invoice.')
+                    : !$form->{id}      ? t8('This invoice has not been posted yet.')
+                    :                     undef,
+        ],
+        action => [
+          t8('Further Invoice for Advance Payment'),
+          submit   => [ '#form', { action => "further_invoice_for_advance_payment" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create                          ? t8('You must not change this invoice.')
+                    : !$form->{id}                               ? t8('This invoice has not been posted yet.')
+                    : $has_further_invoice_for_advance_payment   ? t8('This invoice has already a further invoice for advanced payment.')
+                    : $has_final_invoice                         ? t8('This invoice has already a final invoice.')
+                    : $is_invoice_for_advance_payment_from_order ? t8('This invoice was added from an order. See there.')
+                    :                                              undef,
+          only_if  => $form->{type} eq "invoice_for_advance_payment",
+        ],
+        action => [
+          t8('Final Invoice'),
+          submit   => [ '#form', { action => "final_invoice" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create                          ? t8('You must not change this invoice.')
+                    : !$form->{id}                               ? t8('This invoice has not been posted yet.')
+                    : $has_further_invoice_for_advance_payment   ? t8('This invoice has a further invoice for advanced payment.')
+                    : $has_final_invoice                         ? t8('This invoice has already a final invoice.')
+                    : $is_invoice_for_advance_payment_from_order ? t8('This invoice was added from an order. See there.')
+                    :                                              undef,
+          only_if  => $form->{type} eq "invoice_for_advance_payment",
+        ],
+        action => [
+          t8('Credit Note'),
+          submit   => [ '#form', { action => "credit_note" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create              ? t8('You must not change this invoice.')
+                    : $form->{type} eq "credit_note" ? t8('Credit notes cannot be converted into other credit notes.')
+                    : !$form->{id}                   ? t8('This invoice has not been posted yet.')
+                    : $form->{storno}                ? t8('A canceled invoice cannot be used. Please undo the cancellation first.')
+                    :                                  undef,
+        ],
+        action => [
+          t8('Sales Order'),
+          submit   => [ '#form', { action => "order" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$form->{id} ? t8('This invoice has not been posted yet.') : undef,
+        ],
+      ], # end of combobox "Workflow"
+
+      combobox => [
+        action => [ t8('Export') ],
+        action => [
+          ($form->{id} ? t8('Print') : t8('Preview')),
+          call     => [ 'kivi.SalesPurchase.show_print_dialog', $form->{id} ? 'print' : 'preview' ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create               ? t8('You must not print this invoice.')
+                    : !$form->{id} && $form->{locked} ? t8('The billing period has already been locked.')
+                    :                                   undef,
+        ],
+        action => [ t8('Print and Post'),
+          call     => [ 'kivi.SalesPurchase.show_print_dialog', 'print_and_post' ],
+          checks   => [ 'kivi.validate_form' ],
+          confirm  => t8('The invoice is not linked with a sales delivery order. Post anyway?') x !!$warn_unlinked_delivery_order,
+          disabled => !$may_edit_create                         ? t8('You must not change this invoice.')
+                    : $form->{locked}                           ? t8('The billing period has already been locked.')
+                    : $form->{storno}                           ? t8('A canceled invoice cannot be posted.')
+                    : ($form->{id} && $change_never)            ? t8('Changing invoices has been disabled in the configuration.')
+                    : ($form->{id} && $change_on_same_day_only) ? t8('Invoices can only be changed on the day they are posted.')
+                    : $is_linked_bank_transaction               ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
+                    :                                             undef,
+        ],
+        action => [ t8('E Mail'),
+          call     => [ 'kivi.SalesPurchase.show_email_dialog' ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create       ? t8('You must not print this invoice.')
+                    : !$form->{id}            ? t8('This invoice has not been posted yet.')
+                    : $form->{postal_invoice} ? t8('This customer wants a postal invoices.')
+                    :                     undef,
+        ],
+        action => [ t8('Factur-X/ZUGFeRD'),
+          submit   => [ '#form', { action => "download_factur_x_xml" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create  ? t8('You must not print this invoice.')
+                    : !$form->{id}       ? t8('This invoice has not been posted yet.')
+                    : !$factur_x_enabled ? t8('Creating Factur-X/ZUGFeRD invoices is not enabled for this customer.')
+                    :                      undef,
+        ],
+      ], # end of combobox "Export"
+
+      combobox => [
+        action => [ t8('more') ],
+        action => [
+          t8('History'),
+          call     => [ 'set_history_window', $form->{id} * 1, 'glid' ],
+          disabled => !$form->{id} ? t8('This invoice has not been posted yet.') : undef,
+        ],
+        action => [
+          t8('Follow-Up'),
+          call     => [ 'follow_up_window' ],
+          disabled => !$form->{id} ? t8('This invoice has not been posted yet.') : undef,
+        ],
+        action => [
+          t8('Drafts'),
+          call     => [ 'kivi.Draft.popup', 'is', 'invoice', $form->{draft_id}, $form->{draft_description} ],
+          disabled => !$may_edit_create ? t8('You must not change this invoice.')
+                    :  $form->{id}      ? t8('This invoice has already been posted.')
+                    : $form->{locked}   ? t8('The billing period has already been locked.')
+                    :                     undef,
+        ],
+      ], # end of combobox "more"
+    );
+  }
+  $::request->layout->add_javascripts('kivi.Validator.js');
+}
+
 sub form_header {
   $main::lxdebug->enter_sub();
 
+  _assert_access();
+
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
   my $cgi      = $::request->{cgi};
 
-  $main::auth->assert('invoice_edit');
-
   my %TMPL_VAR = ();
   my @custom_hiddens;
 
-  if ($form->{id}) {
-    require SL::DB::Invoice;
-    $TMPL_VAR{invoice_obj} = SL::DB::Invoice->new(id => $form->{id})->load;
-  }
-  $form->{employee_id} = $form->{old_employee_id} if $form->{old_employee_id};
-  $form->{salesman_id} = $form->{old_salesman_id} if $form->{old_salesman_id};
+  $TMPL_VAR{customer_obj} = SL::DB::Customer->load_cached($form->{customer_id}) if $form->{customer_id};
+  $TMPL_VAR{invoice_obj}  = SL::DB::Invoice->load_cached($form->{id})           if $form->{id};
+
+  # only print, no mail
+  $form->{postal_invoice} = $TMPL_VAR{customer_obj}->postal_invoice if ref $TMPL_VAR{customer_obj} eq 'SL::DB::Customer';
+
+  my $current_employee   = SL::DB::Manager::Employee->current;
+  $form->{employee_id}   = $form->{old_employee_id} if $form->{old_employee_id};
+  $form->{salesman_id}   = $form->{old_salesman_id} if $form->{old_salesman_id};
+  $form->{employee_id} ||= $current_employee->id;
+  $form->{salesman_id} ||= $current_employee->id;
 
   $form->{defaultcurrency} = $form->get_default_currency(\%myconfig);
 
   $form->get_lists("taxzones"      => ($form->{id} ? "ALL_TAXZONES" : "ALL_ACTIVE_TAXZONES"),
                    "currencies"    => "ALL_CURRENCIES",
-                   "customers"     => "ALL_CUSTOMERS",
-                   "departments"   => "all_departments",
                    "price_factors" => "ALL_PRICE_FACTORS");
 
+  $form->{ALL_DEPARTMENTS} = SL::DB::Manager::Department->get_all_sorted;
+  $form->{ALL_LANGUAGES}   = SL::DB::Manager::Language->get_all_sorted;
+
   # Projects
   my @old_project_ids = uniq grep { $_ } map { $_ * 1 } ($form->{"globalproject_id"}, map { $form->{"project_id_$_"} } 1..$form->{"rowcount"});
   my @old_ids_cond    = @old_project_ids ? (id => \@old_project_ids) : ();
@@ -340,15 +604,6 @@ sub form_header {
       ]
     ]
   ]);
-  $TMPL_VAR{department_labels}     = sub { "$_[0]->{description}--$_[0]->{id}" };
-
-  # customer
-  $TMPL_VAR{vc_keys} = sub { "$_[0]->{name}--$_[0]->{id}" };
-  $TMPL_VAR{vclimit} = $myconfig{vclimit};
-  $TMPL_VAR{vc_select} = "customer_or_vendor_selection_window('customer', '', 0, 0)";
-  push @custom_hiddens, "customer_id";
-  push @custom_hiddens, "oldcustomer";
-  push @custom_hiddens, "selectcustomer";
 
   # currencies and exchangerate
   my @values = map { $_       } @{ $form->{ALL_CURRENCIES} };
@@ -365,38 +620,37 @@ sub form_header {
   $TMPL_VAR{creditwarning} = ($form->{creditlimit} != 0) && ($form->{creditremaining} < 0) && !$form->{update};
   $TMPL_VAR{is_credit_remaining_negativ} = $form->{creditremaining} =~ /-/;
 
-  my $follow_up_vc         =  $form->{customer};
-  $follow_up_vc            =~ s/--\d*\s*$//;
-  $TMPL_VAR{customer_name} = $follow_up_vc;
-
 # set option selected
   foreach my $item (qw(AR)) {
     $form->{"select$item"} =~ s/ selected//;
     $form->{"select$item"} =~ s/option>\Q$form->{$item}\E/option selected>$form->{$item}/;
   }
 
-  $TMPL_VAR{is_type_credit_note} = $form->{type}   eq "credit_note";
-  $TMPL_VAR{is_format_html}      = $form->{format} eq 'html';
-  $TMPL_VAR{dateformat}          = $myconfig{dateformat};
-  $TMPL_VAR{numberformat}        = $myconfig{numberformat};
+  $TMPL_VAR{is_type_normal_invoice}                 = $form->{type} eq "invoice";
+  $TMPL_VAR{is_type_credit_note}                    = $form->{type}   eq "credit_note";
+  $TMPL_VAR{is_format_html}                         = $form->{format} eq 'html';
+  $TMPL_VAR{dateformat}                             = $myconfig{dateformat};
+  $TMPL_VAR{numberformat}                           = $myconfig{numberformat};
+  $TMPL_VAR{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
 
   # hiddens
   $TMPL_VAR{HIDDENS} = [qw(
-    id action type media format queued printed emailed title vc discount
+    id type queued printed emailed vc discount
     title creditlimit creditremaining tradediscount business closedto locked shipped storno storno_id
     max_dunning_level dunning_amount dunning_description
-    shiptoname shiptostreet shiptozipcode shiptocity shiptocountry  shiptocontact shiptophone shiptofax
-    shiptoemail shiptodepartment_1 shiptodepartment_2  shiptocp_gender message email subject cc bcc taxaccounts cursor_fokus
+    taxaccounts cursor_fokus
     convert_from_do_ids convert_from_oe_ids convert_from_ar_ids useasnew
     invoice_id
     show_details
   ), @custom_hiddens,
-  map { $_.'_rate', $_.'_description', $_.'_taxnumber' } split / /, $form->{taxaccounts}];
+  map { $_.'_rate', $_.'_description', $_.'_taxnumber', $_.'_tax_id' } split / /, $form->{taxaccounts}];
 
-  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.SalesPurchase ckeditor/ckeditor ckeditor/adapters/jquery kivi.io autocomplete_customer autocomplete_part client_js));
+  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.Draft kivi.File kivi.SalesPurchase kivi.Part kivi.CustomerVendor kivi.Validator ckeditor/ckeditor ckeditor/adapters/jquery kivi.io client_js));
 
   $TMPL_VAR{payment_terms_obj} = get_payment_terms_for_invoice();
-  $form->{duedate}             = $TMPL_VAR{payment_terms_obj}->calc_date(reference_date => $form->{invdate}, due_date => $form->{due_due})->to_kivitendo if $TMPL_VAR{payment_terms_obj};
+  $form->{duedate}             = $TMPL_VAR{payment_terms_obj}->calc_date(reference_date => $form->{invdate}, due_date => $form->{duedate})->to_kivitendo if $TMPL_VAR{payment_terms_obj};
+
+  setup_is_action_bar(\%TMPL_VAR);
 
   $form->header();
 
@@ -427,26 +681,20 @@ sub _sort_payments {
 sub form_footer {
   $main::lxdebug->enter_sub();
 
+  _assert_access();
+
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
-  $main::auth->assert('invoice_edit');
-
   $form->{invtotal}    = $form->{invsubtotal};
 
-  # note rows
-  $form->{rows} = max 2,
-    $form->numtextrows($form->{notes},    26, 8),
-    $form->numtextrows($form->{intnotes}, 35, 8);
-
-
   # tax, total and subtotal calculations
   my ($tax, $subtotal);
   $form->{taxaccounts_array} = [ split(/ /, $form->{taxaccounts}) ];
 
   if( $form->{customer_id} && !$form->{taxincluded_changed_by_user} ) {
-    my $customer = SL::DB::Customer->new(id => $form->{customer_id})->load();
+    my $customer = SL::DB::Customer->load_cached($form->{customer_id});
     $form->{taxincluded} = defined($customer->taxincluded_checked) ? $customer->taxincluded_checked : $myconfig{taxincluded_checked};
   }
 
@@ -463,9 +711,16 @@ sub form_footer {
     }
   }
 
+  my $grossamount = $form->{invtotal};
+  $form->{invtotal} = $form->round_amount( $form->{invtotal}, 2, 1 );
+  $form->{rounding} = $form->round_amount(
+    $form->{invtotal} - $form->round_amount($grossamount, 2),
+    2
+  );
+
   # follow ups
   if ($form->{id}) {
-    $form->{follow_ups}            = FU->follow_ups('trans_id' => $form->{id}) || [];
+    $form->{follow_ups}            = FU->follow_ups('trans_id' => $form->{id}, 'not_done' => 1) || [];
     $form->{follow_ups_unfinished} = ( sum map { $_->{due} * 1 } @{ $form->{follow_ups} } ) || 0;
   }
 
@@ -490,6 +745,11 @@ sub form_footer {
                                   ($form->current_date(\%myconfig) eq $form->{"gldate_$i"}));
     }
 
+    #deaktivieren von gebuchten Zahlungen ausserhalb der Bücherkontrolle, vorher prüfen ob heute eingegeben
+    if ($form->date_closed($form->{"gldate_$i"})) {
+      $form->{"changeable_$i"} = 0;
+    }
+
     $form->{"selectAR_paid_$i"} = $form->{selectAR_paid};
     if (!$form->{"AR_paid_$i"}) {
       $form->{"selectAR_paid_$i"} =~ s/option>$accno_arap--(.*?)</option selected>$accno_arap--$1</;
@@ -504,16 +764,25 @@ sub form_footer {
 
   $form->{ALL_DELIVERY_TERMS} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
 
+  my $shipto_cvars       = SL::DB::Shipto->new->cvars_by_config;
+  foreach my $var (@{ $shipto_cvars }) {
+    my $name = "shiptocvar_" . $var->config->name;
+    $var->value($form->{$name}) if exists $form->{$name};
+  }
+
   print $form->parse_html_template('is/form_footer', {
-    is_type_credit_note => ($form->{type} eq "credit_note"),
-    totalpaid           => $totalpaid,
-    paid_missing        => $form->{invtotal} - $totalpaid,
-    print_options       => print_options(inline => 1),
-    show_storno         => $form->{id} && !$form->{storno} && !IS->has_storno(\%myconfig, $form, "ar") && !$totalpaid,
-    show_delete         => ($::instance_conf->get_is_changeable == 2)
-                             ? ($form->current_date(\%myconfig) eq $form->{gldate})
-                             : ($::instance_conf->get_is_changeable == 1),
-    today               => DateTime->today,
+    is_type_normal_invoice              => ($form->{type} eq "invoice"),
+    is_type_credit_note                 => ($form->{type} eq "credit_note"),
+    totalpaid                           => $totalpaid,
+    paid_missing                        => $form->{invtotal} - $totalpaid,
+    print_options                       => setup_sales_purchase_print_options(),
+    show_storno                         => $form->{id} && !$form->{storno} && !IS->has_storno(\%myconfig, $form, "ar") && !$totalpaid,
+    show_delete                         => ($::instance_conf->get_is_changeable == 2)
+                                             ? ($form->current_date(\%myconfig) eq $form->{gldate})
+                                             : ($::instance_conf->get_is_changeable == 1),
+    today                               => DateTime->today,
+    vc_obj                              => $form->{customer_id} ? SL::DB::Customer->load_cached($form->{customer_id}) : undef,
+    shipto_cvars                        => $shipto_cvars,
   });
 ##print $form->parse_html_template('is/_payments'); # parser
 ##print $form->parse_html_template('webdav/_list'); # parser
@@ -522,33 +791,40 @@ sub form_footer {
 }
 
 sub mark_as_paid {
-  $main::lxdebug->enter_sub();
+  $::auth->assert('invoice_edit');
 
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-
-  $main::auth->assert('invoice_edit');
+  SL::DB::Invoice->new(id => $::form->{id})->load->mark_as_paid;
 
-  &mark_as_paid_common(\%myconfig,"ar");
+  $::form->redirect($::locale->text("Marked as paid"));
+}
 
-  $main::lxdebug->leave_sub();
+sub show_draft {
+  # unless no lazy implementation of save draft without invdate
+  # set the current date like in version <= 3.4.1
+  $::form->{invdate}   = DateTime->today->to_lxoffice;
+  update();
 }
 
 sub update {
   $main::lxdebug->enter_sub();
 
+  _assert_access();
+
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
-  $main::auth->assert('invoice_edit');
-
   my ($recursive_call) = @_;
 
   $form->{print_and_post} = 0         if $form->{second_run};
   my $taxincluded         = $form->{taxincluded} ? "checked" : '';
   $form->{update} = 1;
 
-  &check_name("customer");
+  if (($form->{previous_customer_id} || $form->{customer_id}) != $form->{customer_id}) {
+    $::form->{salesman_id} = SL::DB::Manager::Employee->current->id if exists $::form->{salesman_id};
+
+    IS->get_customer(\%myconfig, $form);
+    $::form->{billing_address_id} = $::form->{default_billing_address_id};
+  }
 
   $form->{taxincluded} ||= $taxincluded;
 
@@ -560,10 +836,7 @@ sub update {
 
   for my $i (1 .. $form->{paidaccounts}) {
     next unless $form->{"paid_$i"};
-    map { $form->{"${_}_$i"} = $form->parse_amount(\%myconfig, $form->{"${_}_$i"}) } qw(paid exchangerate);
-    if (!$form->{"forex_$i"}) {   #read exchangerate from input field (not hidden)
-      $form->{exchangerate} = $form->{"exchangerate_$i"};
-    }
+    map { $form->{"${_}_$i"}   = $form->parse_amount(\%myconfig, $form->{"${_}_$i"}) } qw(paid exchangerate);
     $form->{"forex_$i"}        = $form->check_exchangerate(\%myconfig, $form->{currency}, $form->{"datepaid_$i"}, 'buy');
     $form->{"exchangerate_$i"} = $form->{"forex_$i"} if $form->{"forex_$i"};
   }
@@ -597,7 +870,7 @@ sub update {
       if ($rows > 1) {
 
         select_item(mode => 'IS', pre_entered_qty => $form->{"qty_$i"});
-        ::end_of_request();
+        $::dispatcher->end_request;
 
       } else {
 
@@ -658,7 +931,7 @@ sub update {
       # ask if it is a part or service item
 
       if (   $form->{"partsgroup_$i"}
-          && ($form->{"partsnumber_$i"} eq "")
+          && ($form->{"partnumber_$i" } eq "")
           && ($form->{"description_$i"} eq "")) {
         $form->{rowcount}--;
         $form->{"discount_$i"} = "";
@@ -699,20 +972,15 @@ sub post_payment {
         $form->isblank("exchangerate_$i",
                        $locale->text('Exchangerate for payment missing!'));
       }
+      $form->error($locale->text('Cannot post transaction above the maximum future booking date!'))
+        if ($form->date_max_future($form->{"datepaid_$i"}, \%myconfig));
+
+      #Zusätzlich noch das Buchungsdatum in die Bücherkontrolle einbeziehen
+      # (Dient zur Prüfung ob ZE oder ZA geprüft werden soll)
+      $form->error($locale->text('Cannot post payment for a closed period!'))
+        if ($form->date_closed($form->{"datepaid_$i"})  && !$form->date_closed($form->{"gldate_$i"}, \%myconfig));
     }
   }
-  # Abgeschlossene Zeiträume nur für den letzten (aktuellen) Zahlungseingang prüfen
-  # Details s.a. Bug 1502
-  # Das Problem ist jetzt, dass man Zahlungseingänge nachträglich ändern kann
-  # Wobei dies für Installationen die sowieso nicht mit Bücherkontrolle arbeiten keinen
-  # keinen Unterschied macht.
-  # Optimal wäre, wenn gegen einen Zeitstempel des Zahlungsfelds geprüft würde ...
-  # Das Problem hierbei ist, dass in IS.pm post_invoice IMMER alle Zahlungseingänge aus $form
-  # erneut gespeichert werden. Prinzipiell wäre es besser NUR die Änderungen des Rechnungs-
-  # belegs (neue Zahlung aber nichts anderes) zu speichern ...
-  # Vielleicht könnte man ähnlich wie bei Rechnung löschen verfahren
-  $form->error($locale->text('Cannot post payment for a closed period!'))
-    if ($form->date_closed($form->{"datepaid_$form->{paidaccounts}"}, \%myconfig));
 
   ($form->{AR})      = split /--/, $form->{AR};
   ($form->{AR_paid}) = split /--/, $form->{AR_paid};
@@ -742,7 +1010,7 @@ sub post {
 
   $form->{defaultcurrency} = $form->get_default_currency(\%myconfig);
   $form->isblank("invdate",  $locale->text('Invoice Date missing!'));
-  $form->isblank("customer", $locale->text('Customer missing!'));
+  $form->isblank("customer_id", $locale->text('Customer missing!'));
   $form->error($locale->text('Cannot post invoice for a closed period!'))
         if ($form->date_closed($form->{"invdate"}, \%myconfig));
 
@@ -750,9 +1018,9 @@ sub post {
   $form->{invnumber} =~ s/\s*$//g;
 
   # if oldcustomer ne customer redo form
-  if (&check_name('customer')) {
+  if (($form->{previous_customer_id} || $form->{customer_id}) != $form->{customer_id}) {
     &update;
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
 
   if ($myconfig{mandatory_departments} && !$form->{department_id}) {
@@ -778,6 +1046,14 @@ sub post {
 
   $form->isblank("exchangerate", $locale->text('Exchangerate missing!'))
     if ($form->{currency} ne $form->{defaultcurrency});
+  # advance payment allows only one tax
+  if ($form->{type} eq 'invoice_for_advance_payment') {
+    my @current_taxaccounts = (split(/ /, $form->{taxaccounts}));
+    $form->error($locale->text('Cannot post invoice for advance payment with more than one tax'))
+      if (scalar @current_taxaccounts > 1);
+    $form->error($locale->text('Cannot post invoice for advance payment with taxincluded'))
+      if ($form->{taxincluded});
+  }
 
   for my $i (1 .. $form->{paidaccounts}) {
     if ($form->parse_amount(\%myconfig, $form->{"paid_$i"})) {
@@ -785,8 +1061,13 @@ sub post {
 
       $form->isblank("datepaid_$i", $locale->text('Payment date missing!'));
 
+      $form->error($locale->text('Cannot post transaction above the maximum future booking date!'))
+        if ($form->date_max_future($form->{"datepaid_$i"}, \%myconfig));
+
+      #Zusätzlich noch das Buchungsdatum in die Bücherkontrolle einbeziehen
+      # (Dient zur Prüfung ob ZE oder ZA geprüft werden soll)
       $form->error($locale->text('Cannot post payment for a closed period!'))
-        if ($form->date_closed($form->{"datepaid_$i"}, \%myconfig));
+        if ($form->date_closed($form->{"datepaid_$i"})  && !$form->date_closed($form->{"gldate_$i"}, \%myconfig));
 
       if ($form->{currency} ne $form->{defaultcurrency}) {
         $form->{"exchangerate_$i"} = $form->{exchangerate}
@@ -817,7 +1098,7 @@ sub post {
   relink_accounts();
 
   my $terms        = get_payment_terms_for_invoice();
-  $form->{duedate} = $terms->calc_date(reference_date => $form->{invdate}, due_date => $form->{due_due})->to_kivitendo if $terms;
+  $form->{duedate} = $terms->calc_date(reference_date => $form->{invdate}, due_date => $form->{duedate})->to_kivitendo if $terms;
 
   # If transfer_out is requested, get rose db handle and do post and
   # transfer out in one transaction. Otherwise just post the invoice.
@@ -841,7 +1122,7 @@ sub post {
         1;
       }) {
         push @errors, $EVAL_ERROR;
-        die 'transaction error';
+        $form->error($locale->text('Cannot post invoice and/or transfer out! Error message:') . "\n" . join("\n", @errors));
       }
 
       1;
@@ -855,8 +1136,6 @@ sub post {
     }
   }
 
-  remove_draft() if $form->{remove_draft};
-
   if(!exists $form->{addition}) {
     $form->{snumbers}  =  'invnumber' .'_'. $form->{invnumber}; # ($form->{type} eq 'credit_note' ? 'cnnumber' : 'invnumber') .'_'. $form->{invnumber};
     $form->{what_done} = 'invoice';
@@ -869,9 +1148,8 @@ sub post {
   if (!$form->{no_redirect_after_post}) {
     $form->{action} = 'edit';
     $form->{script} = 'is.pl';
-    $form->{saved_message} = $form->{label} . " $form->{invnumber} " . $locale->text('posted!');
     $form->{callback} = build_std_url(qw(action edit id callback saved_message));
-    $form->redirect;
+    $form->redirect($form->{label} . " $form->{invnumber} " . $locale->text('posted!'));
   }
 
   $main::lxdebug->leave_sub();
@@ -920,6 +1198,80 @@ sub use_as_new {
   $main::lxdebug->leave_sub();
 }
 
+sub further_invoice_for_advance_payment {
+  my $form     = $main::form;
+  my %myconfig = %main::myconfig;
+
+  $main::auth->assert('invoice_edit');
+
+  delete @{ $form }{qw(printed emailed queued invnumber invdate exchangerate forex deliverydate datepaid_1 gldate_1 acc_trans_id_1 source_1 memo_1 paid_1 exchangerate_1 AP_paid_1 storno locked)};
+  $form->{convert_from_ar_ids} = $form->{id};
+  $form->{id}                  = '';
+  $form->{rowcount}--;
+  $form->{paidaccounts}        = 1;
+  $form->{invdate}             = $form->current_date(\%myconfig);
+  my $terms                    = get_payment_terms_for_invoice();
+  $form->{duedate}             = $terms ? $terms->calc_date(reference_date => $form->{invdate})->to_kivitendo : $form->{invdate};
+  $form->{employee_id}         = SL::DB::Manager::Employee->current->id;
+  $form->{forex}               = $form->check_exchangerate(\%myconfig, $form->{currency}, $form->{invdate}, 'buy');
+  $form->{exchangerate}        = $form->{forex} if $form->{forex};
+
+  $form->{"converted_from_invoice_id_$_"} = delete $form->{"invoice_id_$_"} for 1 .. $form->{"rowcount"};
+
+  &display_form;
+}
+
+sub final_invoice {
+  my $form     = $main::form;
+  my %myconfig = %main::myconfig;
+
+  $main::auth->assert('invoice_edit');
+
+  my $related_invoices = IS->_get_invoices_for_advance_payment($form->{id});
+
+  delete @{ $form }{qw(printed emailed queued invnumber invdate exchangerate forex deliverydate datepaid_1 gldate_1 acc_trans_id_1 source_1 memo_1 paid_1 exchangerate_1 AP_paid_1 storno locked)};
+
+  $form->{convert_from_ar_ids} = $form->{id};
+  $form->{id}                  = '';
+  $form->{type}                = 'final_invoice';
+  $form->{title}               = t8('Edit Final Invoice');
+  $form->{paidaccounts}        = 1;
+  $form->{invdate}             = $form->current_date(\%myconfig);
+  my $terms                    = get_payment_terms_for_invoice();
+  $form->{duedate}             = $terms ? $terms->calc_date(reference_date => $form->{invdate})->to_kivitendo : $form->{invdate};
+  $form->{employee_id}         = SL::DB::Manager::Employee->current->id;
+  $form->{forex}               = $form->check_exchangerate(\%myconfig, $form->{currency}, $form->{invdate}, 'buy');
+  $form->{exchangerate}        = $form->{forex} if $form->{forex};
+
+  foreach my $i (1 .. $form->{"rowcount"}) {
+    delete $form->{"id_$i"};
+    delete $form->{"invoice_id_$i"};
+    delete $form->{"parts_id_$i"};
+    delete $form->{"partnumber_$i"};
+    delete $form->{"description_$i"};
+  }
+
+  remove_emptied_rows(1);
+
+  my $i = 0;
+  foreach my $ri (@$related_invoices) {
+    foreach my $item (@{$ri->items_sorted}) {
+      $i++;
+      $form->{"id_$i"}         = $item->parts_id;
+      $form->{"partnumber_$i"} = $item->part->partnumber;
+      $form->{"discount_$i"}   = $item->discount*100.0;
+      $form->{"sellprice_$i"}  = $item->fxsellprice;
+      $form->{$_ . "_" . $i}   = $item->$_       for qw(description longdescription qty price_factor_id unit active_price_source active_discount_source);
+
+      $form->{$_ . "_" . $i}   = $form->format_amount(\%myconfig, $form->{$_ . "_" . $i}) for qw(qty sellprice discount);
+    }
+  }
+  $form->{rowcount} = $i;
+
+  update();
+  $::dispatcher->end_request;
+}
+
 sub storno {
   $main::lxdebug->enter_sub();
 
@@ -956,6 +1308,8 @@ sub storno {
   $form->{paidaccounts} = 0;
   map { my $key = $_; delete $form->{$key} if grep { $key =~ /^$_/ } qw(datepaid_ gldate_ acc_trans_id_ source_ memo_ paid_ exchangerate_ AR_paid_) } keys %{ $form };
 
+  # record link invoice to storno
+  $form->{convert_from_ar_ids} = $form->{id};
   $form->{storno_id} = $form->{id};
   $form->{storno} = 1;
   $form->{id} = "";
@@ -963,7 +1317,8 @@ sub storno {
   $form->{invdate}   = DateTime->today->to_lxoffice;
   $form->{rowcount}++;
   # set new ids for storno invoice
-  delete $form->{"invoice_id_$_"} for 1 .. $form->{"rowcount"};
+  # set new persistent ids for storno invoice items
+  $form->{"converted_from_invoice_id_$_"} = delete $form->{"invoice_id_$_"} for 1 .. $form->{"rowcount"};
 
   post();
   $main::lxdebug->leave_sub();
@@ -985,49 +1340,6 @@ sub preview {
 
 }
 
-sub delete {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my $locale   = $main::locale;
-
-  $main::auth->assert('invoice_edit');
-
-  if ($form->{second_run}) {
-    $form->{print_and_post} = 0;
-  }
-  $form->header;
-
-  print qq|
-<form method="post" action="$form->{script}">
-|;
-
-  # delete action variable
-  map { delete $form->{$_} } qw(action header);
-
-  foreach my $key (keys %$form) {
-    next if (($key eq 'login') || ($key eq 'password') || ('' ne ref $form->{$key}));
-    $form->{$key} =~ s/\"/&quot;/g;
-    print qq|<input type="hidden" name="$key" value="$form->{$key}">\n|;
-  }
-
-  print qq|
-<h2 class="confirm">| . $locale->text('Confirm!') . qq|</h2>
-
-<h4>|
-    . $locale->text('Are you sure you want to delete Invoice Number')
-    . qq| $form->{invnumber}
-</h4>
-
-<p>
-<input name="action" class="submit" type="submit" value="|
-    . $locale->text('Yes') . qq|">
-</form>
-|;
-
-  $main::lxdebug->leave_sub();
-}
-
 sub credit_note {
   $main::lxdebug->enter_sub();
 
@@ -1089,59 +1401,60 @@ sub credit_note {
 
   &prepare_invoice;
 
-
   &display_form;
 
   $main::lxdebug->leave_sub();
 }
 
-sub yes {
-  $main::lxdebug->enter_sub();
+sub display_form {
+  $::lxdebug->enter_sub;
 
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  $main::auth->assert('invoice_edit');
+  _assert_access();
 
-  if (IS->delete_invoice(\%myconfig, \%$form)) {
-    # saving the history
-    if(!exists $form->{addition}) {
-      $form->{snumbers}  = 'invnumber' .'_'. $form->{invnumber}; # ($form->{type} eq 'credit_note' ? 'cnnumber' : 'invnumber') .'_'. $form->{invnumber};
-      $form->{what_done} = 'invoice';
-      $form->{addition}  = "DELETED";
-      $form->save_history;
-    }
-    # /saving the history
-    $form->redirect($locale->text('Invoice deleted!'));
-  }
-  $form->error($locale->text('Cannot delete invoice!'));
-
-  $main::lxdebug->leave_sub();
-}
+  relink_accounts();
 
-sub post_and_e_mail {
-  e_mail();
-};
+  my $new_rowcount = $::form->{"rowcount"} * 1 + 1;
+  $::form->{"project_id_${new_rowcount}"} = $::form->{"globalproject_id"};
 
-sub e_mail {
-  $main::lxdebug->enter_sub();
+  $::form->language_payment(\%::myconfig);
 
-  my $form     = $main::form;
+  Common::webdav_folder($::form);
 
-  $main::auth->assert('invoice_edit');
+  form_header();
+  display_row(++$::form->{rowcount});
+  form_footer();
 
-  if (!$form->{id}) {
-    $form->{no_redirect_after_post} = 1;
-
-    my $saved_form = save_form();
+  $::lxdebug->leave_sub;
+}
 
-    post();
+sub delete {
+  $::auth->assert('invoice_edit');
 
-    restore_form($saved_form, 0, qw(id invnumber));
+  if (IS->delete_invoice(\%::myconfig, $::form)) {
+    # saving the history
+    if(!exists $::form->{addition}) {
+      $::form->{snumbers}  = 'invnumber' .'_'. $::form->{invnumber};
+      $::form->{what_done} = 'invoice';
+      $::form->{addition}  = "DELETED";
+      $::form->save_history;
+    }
+    # /saving the history
+    $::form->redirect($::locale->text('Invoice deleted!'));
   }
+  $::form->error($::locale->text('Cannot delete invoice!'));
+}
 
-  edit_e_mail();
+sub dispatcher {
+  for my $action (qw(
+    print update ship_to storno post_payment use_as_new credit_note
+    delete post order preview post_and_e_mail print_and_post
+    mark_as_paid
+  )) {
+    if ($::form->{"action_$action"}) {
+      call_sub($action);
+      return;
+    }
+  }
 
-  $main::lxdebug->leave_sub();
+  $::form->error($::locale->text('No action defined.'));
 }
diff --git a/bin/mozilla/letter.pl b/bin/mozilla/letter.pl
deleted file mode 100755 (executable)
index d9a0742..0000000
+++ /dev/null
@@ -1,568 +0,0 @@
-#=====================================================================
-# LX-Office ERP
-# Copyright (C) 2008
-# Based on SQL-Ledger Version 2.1.9
-# Web http://www.lx-office.org
-#
-#=====================================================================
-#
-# Letter module
-#
-#======================================================================
-
-use strict;
-use POSIX qw(strftime);
-
-use SL::GenericTranslations;
-use SL::ReportGenerator;
-use SL::Letter;
-use SL::CT;
-use SL::DB::Contact;
-use SL::DB::Default;
-use SL::Helper::CreatePDF;
-use SL::Helper::Flash;
-use SL::Common;
-use Cwd;
-require "bin/mozilla/reportgenerator.pl";
-require "bin/mozilla/io.pl";
-require "bin/mozilla/arap.pl";
-
-use constant TEXT_CREATED_FOR_VALUES => (qw(presskit fax letter));
-use constant PAGE_CREATED_FOR_VALUES => (qw(sketch 1 2));
-
-our ($form, %myconfig, $locale, $lxdebug);
-
-# parserhappy(R)
-# $locale->text('Presskit')
-# $locale->text('Sketch')
-# $locale->text('Fax')
-# $locale->text('Letter')
-
-sub add {
-  $::lxdebug->enter_sub;
-
-  $main::auth->assert('sales_letter_edit');
-  my %params = @_;
-
-  return $main::lxdebug->leave_sub if load_letter_draft();
-
-  my $letter = SL::Letter->new(%params);
-
-  if (my $cp_id = delete $::form->{contact_id}) {
-    my $contact = SL::DB::Manager::Contact->find_by(cp_id => $cp_id);
-    $letter->{cp_id}     = $contact->cp_id;
-    $letter->{vc_id}     = $contact->cp_cv_id;
-    $letter->{greeting}  = GenericTranslations->get(
-      translation_type => 'greetings::' . ($contact->{cp_gender} eq 'f' ? 'female' : 'male'),
-      language_id      => $contact->language_id,
-      allow_fallback   => 1
-    );
-    $params{language_id} = $contact->language_id;
-  }
-
-  $letter->check_date;
-
-  _display(
-    letter      => $letter,
-    title       => $locale->text('Add Letter'),
-    language_id => $params{language_id},
-  );
-
-  $::lxdebug->leave_sub;
-}
-
-sub edit {
-  $::lxdebug->enter_sub;
-
-  $main::auth->assert('sales_letter_edit');
-  add() unless ($form->{id});
-
-  my $letter = SL::Letter->new( id => $form->{id}, draft => $form->{draft} )->load;
-
-  add() unless $letter && ($letter->{id} || $letter->{draft_id});
-
-  _display(
-    letter => $letter,
-    title  => $locale->text('Edit Letter'),
-  );
-
-  $::lxdebug->leave_sub;
-}
-
-sub save {
-  $::lxdebug->enter_sub;
-
-  $main::auth->assert('sales_letter_edit');
-  my %params = @_;
-
-
-  $::form->error(t8('The subject is missing.')) unless $form->{letter}->{subject};
-  $::form->error(t8('The body is missing.')) unless $form->{letter}->{body};
-  $::form->error(t8('The employee is missing.')) unless $form->{letter}->{employee_id};
-
-  my $letter = _update();
-
-  $letter->check_number;
-  $letter->save;
-
-  $form->{SAVED_MESSAGE} = $locale->text('Letter saved!');
-
-  _display(
-    letter => $letter,
-  );
-
-  $::lxdebug->leave_sub;
-}
-
-sub save_letter_draft {
-  $::lxdebug->enter_sub;
-
-  $main::auth->assert('sales_letter_edit');
-
-  $::form->error(t8('The subject is missing.')) unless $form->{letter}->{subject};
-  $::form->error(t8('The body is missing.')) unless $form->{letter}->{body};
-  $::form->error(t8('The employee is missing.')) unless $form->{letter}->{employee_id};
-  $::form->error(t8('Already as letter saved.')) if $form->{letter}->{letternumber};
-
-  my $letter_draft = _update();
-  $letter_draft->{draft_id} = delete $letter_draft->{id}; # if we have one
-  $letter_draft->save(draft => '1');
-  $letter_draft->{vergiss_mich_nicht} = 'nicht vergessen';
-  $form->{SAVED_MESSAGE} = $locale->text('Draft for this Letter saved!');
-
-  _display(
-    letter => $letter_draft,
-  );
-
-  $::lxdebug->leave_sub;
-}
-
-sub delete {
-  $main::lxdebug->enter_sub();
-
-  $main::auth->assert('sales_letter_edit');
-  # NYI
-  $form->{SAVED_MESSAGE} = $locale->text('Not yet implemented!');
-  _display();
-
-  $main::lxdebug->leave_sub();
-}
-
-sub delete_letter_drafts {
-  $main::lxdebug->enter_sub();
-
-  $main::auth->assert('sales_letter_edit');
-
-  my @ids;
-  foreach (keys %{$form}) {
-    push @ids, $1 if (/^checked_(.*)/ && $form->{$_});
-  }
-
-  SL::Letter->delete_drafts(@ids) if (@ids); #->{id});
-
-  add();
-
-  $main::lxdebug->leave_sub();
-}
-
-sub _display {
-  $main::lxdebug->enter_sub();
-
-  $main::auth->assert('sales_letter_edit');
-  my %params = @_;
-
-  my $letter = $params{letter};
-
-  my %TMPL_VAR;
-
-  $form->{type}             = 'letter';   # needed for print_options
-  $form->{vc}               = 'customer'; # needs to be for _get_contacts...
-  $form->{"$form->{vc}_id"} ||= $letter->{customer_id};
-  $form->{jsscript}         = 1;
-  $form->{javascript}       =
-     qq|<script type="text/javascript" src="js/customer_or_vendor_selection.js"></script>
-        <script type="text/javascript" src="js/edit_part_window.js"></script>|;
-
-  $form->get_lists("contacts"      => "ALL_CONTACTS",
-  "employees"     => "ALL_EMPLOYEES",
-                   "salesmen"      => "ALL_SALESMEN",
-                   "departments"   => "ALL_DEPARTMENTS",
-                   "languages"     => "languages",
-                   "customers"     => { key   => "ALL_CUSTOMERS",
-                                        limit => $myconfig{vclimit} + 1 },
-                   "vc"            => 'customer',
-                   );
-
-  $TMPL_VAR{vc_keys}       = sub { "$_[0]->{name}--$_[0]->{id}" };
-  $TMPL_VAR{vc_select}     = "customer_or_vendor_selection_window('letter.customer', '', 0, 0)";
-  $TMPL_VAR{ct_labels}     = sub { ($_[0]->{cp_greeting} ? "$_[0]->{cp_greeting} " : '') .  $_[0]->{cp_name} .  ($_[0]->{cp_givenname} ? ", $_[0]->{cp_givenname}" : '') };
-  $TMPL_VAR{TCF}           = [ map { key => $_, value => $locale->text(ucfirst $_) }, TEXT_CREATED_FOR_VALUES() ];
-  $TMPL_VAR{PCF}           = [ map { key => $_, value => $locale->text(ucfirst $_) }, PAGE_CREATED_FOR_VALUES() ];
-
-  $form->header();
-
-  $form->{language_id} ||= $params{language_id};
-
-  print $form->parse_html_template('letter/edit', {
-    %params,
-    %TMPL_VAR,
-    letter        => $letter,
-    print_options => print_options(inline => 1),
-  });
-
-  $main::lxdebug->leave_sub();
-}
-
-sub search {
-  $lxdebug->enter_sub();
-
-  $main::auth->assert('sales_letter_report');
-
-  $form->get_lists("employees" => "EMPLOYEES",
-                   "salesmen"  => "SALESMEN",
-                   "customers" => "ALL_CUSTOMERS");
-
-  $form->{jsscript} = 1;
-  $form->{title}    = $locale->text('Letters');
-
-  $form->header();
-  print $form->parse_html_template('letter/search');
-
-  $lxdebug->leave_sub();
-}
-
-sub report {
-  $lxdebug->enter_sub();
-
-  $main::auth->assert('sales_letter_report');
-
-  my %params = @_;
-
-  my @report_params = qw(letternumber subject body contact date_from date_to cp_id);
-
-  if ($form->{selectcustomer}) {
-    push @report_params, 'customer_id';
-    $form->{customer_id} = $form->{customer};
-  } else {
-    push @report_params, 'customer';
-  }
-
-  report_generator_set_default_sort('date', 1);
-
-  %params = (%params, map { $_ => $form->{$_} } @report_params);
-
-  my @letters       = SL::Letter->find(%params);
-
-  $form->{rowcount} = @letters;
-  $form->{title}    = $locale->text('Letters');
-
-  my %column_defs = (
-    'date'                  => { 'text' => $locale->text('Date'), },
-    'subject'               => { 'text' => $locale->text('Subject'), },
-    'letternumber'          => { 'text' => $locale->text('Letternumber'), },
-    'customer'              => { 'text' => $locale->text('Customer') },
-    'contact'               => { 'text' => $locale->text('Contact') },
-    'date'                  => { 'text' => $locale->text('Date') },
-  );
-
-  my @columns = qw(date subject letternumber customer contact date);
-  my $href    = build_std_url('action=report', grep { $form->{$_} } @report_params);
-
-  my @sortable_columns = qw(date subject letternumber customer contact date);
-
-  foreach my $name (@sortable_columns) {
-    my $sortdir                 = $form->{sort} eq $name ? 1 - $form->{sortdir} : $form->{sortdir};
-    $column_defs{$name}->{link} = $href . "&sort=$name&sortdir=$sortdir";
-  }
-
-  my @options;
-
-  # option line
-
-  push @options, $locale->text('Subject')                  . " : $form->{subject}"   if ($form->{subject});
-  push @options, $locale->text('Body')                     . " : $form->{body}"      if ($form->{body});
-
-  my @hidden_report_params = map { +{ 'key' => $_, 'value' => $form->{$_} } } @report_params;
-
-  my $report = SL::ReportGenerator->new(\%myconfig, $form, 'std_column_visibility' => 1);
-
-  $report->set_columns(%column_defs);
-  $report->set_column_order(@columns);
-
-  $report->set_export_options('report', @report_params);
-
-  $report->set_sort_indicator($form->{sort}, $form->{sortdir});
-
-  $report->set_options('raw_top_info_text'    => $form->parse_html_template('letter/report_top',    { 'OPTIONS' => \@options }),
-                       'raw_bottom_info_text' => $form->parse_html_template('letter/report_bottom', { 'HIDDEN'  => \@hidden_report_params }),
-                       'output_format'        => 'HTML',
-                       'title'                => $form->{title},
-                       'attachment_basename'  => $locale->text('letters_list') . strftime('_%Y%m%d', localtime time),
-    );
-  $report->set_options_from_form();
-
-  my $idx      = 0;
-  my $callback = build_std_url('action=report', grep { $form->{$_} } @report_params);
-  my $edit_url = build_std_url('action=edit', 'callback=' . E($callback));
-
-  foreach my $l (@letters) {
-    $idx++;
-
-    my $row = { map { $_ => { 'data' => $l->{$_} } } keys %{ $l } };
-
-    $row->{subject}->{link}      = $edit_url . '&id=' . Q($l->{id});
-    $row->{letternumber}->{link} = $edit_url . '&id=' . Q($l->{id});
-
-    $report->add_data($row);
-  }
-
-  $report->generate_with_headers();
-
-  $lxdebug->leave_sub();
-}
-
-sub print_letter {
-  $lxdebug->enter_sub();
-
-  $main::auth->assert('sales_letter_edit');
-
-  my ($old_form) = @_;
-
-  my $display_form = $form->{display_form} || "display_form";
-  my $letter       = _update();
-
-  $letter->export_to($form);
-  $form->{formname} = "letter";
-  $form->{format} = "pdf";
-
-  my $language_saved      = $form->{language_id};
-  my $greeting_saved      = $form->{greeting};
-  my $cp_id_saved         = $form->{cp_id};
-
-  call_sub("customer_details");
-
-  if (!$cp_id_saved) {
-    # No contact was selected. Delete all contact variables because
-    # IS->customer_details() and IR->vendor_details() get the default
-    # contact anyway.
-    map({ delete($form->{$_}); } grep(/^cp_/, keys(%{ $form })));
-  }
-
-  $form->{greeting} = $greeting_saved;
-  $form->{language_id} = $language_saved;
-
-  if ($form->{cp_id}) {
-    CT->get_contact(\%myconfig, $form);
-  }
-
-  $form->{cp_contact_formal} = ($form->{cp_greeting} ? "$form->{cp_greeting} " : '') . ($form->{cp_givenname} ? "$form->{cp_givenname} " : '') . $form->{cp_name};
-
-  $form->get_employee_data('prefix' => 'employee', 'id' => $letter->{employee_id});
-  $form->get_employee_data('prefix' => 'salesman', 'id' => $letter->{salesman_id});
-
-  my %create_params = (
-    template  => scalar(SL::Helper::CreatePDF->find_template(
-      name        => 'letter',
-      printer_id  => $::form->{printer_id},
-      language_id => $::form->{language_id},
-      formname    => 'letter',
-      format      => 'pdf',
-    )),
-    variables => $::form,
-    return    => 'file_name',
-  );
-  my $pdf_file_name;
-  eval {
-    # catch LaTeX template not found error
-    my $tex_templates  = $::instance_conf->get_templates . '/letter.tex';
-    die( t8('Please create/copy a template named letter.tex in your client template dir') ) unless (-e $tex_templates);
-
-    $pdf_file_name = SL::Helper::CreatePDF->create_pdf(%create_params);
-
-    # set some form defaults for printing webdav copy variables
-    $form->{tmpfile} = $pdf_file_name;
-    $form->{tmpdir} = 'users';
-    $form->{type} = 'letter';
-    $form->{cwd}        = getcwd();
-    if ( $::form->{media} eq 'email') {
-      my $mail             = Mailer->new;
-      my $signature        = $::myconfig{signature};
-      $mail->{$_}          = $::form->{$_}               for qw(cc subject message bcc to);
-      $mail->{from}        = qq|"$::myconfig{name}" <$::myconfig{email}>|;
-      $mail->{fileid}      = time() . '.' . $$ . '.';
-      $mail->{attachments} =  [{ "filename" => $pdf_file_name,
-                                 "name"     => $::form->{attachment_name} }];
-      $mail->{message}    .=  "\n-- \n$signature";
-      $mail->{message}     =~ s/\r//g;
-
-      # copy_file_to_webdav was already done via io.pl -> edit_e_mail
-      my $err = $mail->send;
-      # TODO
-      #       $self
-      #           ->js
-      #           ->flash($err?'error':'info',
-      #                   $err?t8('A mail error occurred: #1', $err):
-      #                        t8('The document have been sent to \'#1\'.', $mail->{to}))
-      #           ->render($self);
-      return $err?0:1;
-    }
-
-    if (!$::form->{printer_id} || $::form->{media} eq 'screen') {
-
-      my $file = IO::File->new($pdf_file_name, 'r') || croak("Cannot open file '$pdf_file_name'");
-      my $size = -s $pdf_file_name;
-      my $content_type    =  'application/pdf';
-      my $attachment_name =  $::form->generate_attachment_filename;
-      $attachment_name    =~ s:.*//::g;
-
-      print $::form->create_http_response(content_type        => $content_type,
-                                          content_disposition => 'attachment; filename="' . $attachment_name . '"',
-                                          content_length      => $size);
-
-      $::locale->with_raw_io(\*STDOUT, sub { print while <$file> });
-      $file->close;
-      Common::copy_file_to_webdav_folder($form) if $::instance_conf->get_webdav_documents;
-      unlink $pdf_file_name;
-      return 1;
-    }
-
-    my $printer = SL::DB::Printer->new(id => $::form->{printer_id})->load;
-    my $command = SL::Template::create(type => 'ShellCommand', form => Form->new(''))->parse($printer->printer_command);
-
-    open my $out, '|-', $command or die $!;
-    binmode $out;
-    print $out scalar(read_file($pdf_file_name));
-    close $out;
-    Common::copy_file_to_webdav_folder($form) if $::instance_conf->get_webdav_documents;
-
-    flash_later('info', t8('The documents have been sent to the printer \'#1\'.', $printer->printer_description));
-    my $callback = build_std_url('letter.pl', 'action=edit', 'id=' . $letter->{id}, 'printer_id');
-    $::form->redirect;
-    1;
-  } or do {
-    unlink $pdf_file_name;
-    $::form->error(t8("Creating the PDF failed:") . " " . $@);
-  };
-
-  $lxdebug->leave_sub();
-}
-
-sub update {
-  $::lxdebug->enter_sub;
-
-  $main::auth->assert('sales_letter_edit');
-
-  my $name_selected = shift;
-
-  _display(
-    letter => _update(
-      _name_selected => $name_selected,
-    ),
-  );
-
-  $::lxdebug->leave_sub;
-}
-
-sub _update {
-  $::lxdebug->enter_sub;
-
-  $main::auth->assert('sales_letter_edit');
-
-  my %params = @_;
-
-  my $from_letter = $::form->{letter};
-
-  my $letter      = SL::Letter->new( id => $from_letter->{id} )
-                              ->load
-                              ->update_from($from_letter);
-
-  $letter->check_name(%params);
-  $letter->check_date;
-  $letter->set_greetings;
-
-  $::lxdebug->leave_sub;
-
-  return $letter;
-}
-
-sub letter_tab {
-  $::lxdebug->enter_sub;
-
-  $main::auth->assert('sales_letter_edit');
-
-  my @report_params = qw(letternumber subject contact date);
-
-  my @letters       = SL::Letter->find(map { $_ => $form->{$_} } @report_params);
-
-  $::lxdebug->leave_sub;
-}
-
-sub e_mail {
-  $::lxdebug->enter_sub;
-
-  $main::auth->assert('sales_letter_edit');
-  my $letter = _update();
-
-  $letter->check_number;
-  $letter->save;
-
-  $form->{formname} = "letter";
-  $letter->export_to($::form);
-
-  $::form->{id} = $letter->{id};
-  edit_e_mail();
-
-  $::lxdebug->leave_sub;
-}
-
-sub dispatcher {
-  $main::lxdebug->enter_sub();
-  # dispatch drafts
-  my $locale   = $main::locale;
-
-
-  if ($form->{letter_draft_action} eq $locale->text("Skip")) {
-    $form->{DONT_LOAD_DRAFT} = 1;
-    add();
-    return 1;
-  } elsif ($form->{letter_draft_action} eq $locale->text("Delete drafts")) {
-    delete_letter_drafts();
-    return 1;
-  }
-
-  foreach my $action (qw(e_mail print save update save_letter_draft)) {
-    if ($::form->{"action_${action}"}) {
-      $::form->{dispatched_action} = $action;
-      call_sub($action);
-      return;
-    }
-  }
-
-  $::form->error($::locale->text('No action defined.'));
-  $::lxdebug->leave_sub;
-}
-
-sub continue {
-  call_sub($form->{nextsub});
-}
-
-
-sub load_letter_draft {
-  $lxdebug->enter_sub();
-
-  $main::auth->assert('sales_letter_edit');
- $main::lxdebug->leave_sub() and return 0 if ($form->{DONT_LOAD_DRAFT});
- $form->{title}    = $locale->text('Letter Draft');
- $form->{script}   = 'letter.pl';
-
-  my @letter_drafts = SL::Letter->find(draft => 1);
-
-  return unless @letter_drafts;
-  $form->header();
-  print $form->parse_html_template('letter/load_drafts', { LETTER_DRAFTS => \@letter_drafts });
-
-  return 1;
-  $lxdebug->leave_sub();
-}
-
-1;
index 5539093..fe00085 100644 (file)
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #######################################################################
 
 use SL::DB::Default;
 use SL::Form;
 use SL::Git;
+use DateTime;
 
 require "bin/mozilla/common.pl";
 require "bin/mozilla/todo.pl";
 
 use strict;
 
-our $cgi;
 our $form;
 our $auth;
 
@@ -44,7 +45,7 @@ sub company_logo {
   $main::lxdebug->enter_sub();
 
   my %myconfig = %main::myconfig;
-  $form->{todo_list}  =  create_todo_list('login_screen' => 1) if (!$form->{no_todo_list}) and ($main::auth->check_right($::myconfig{login}, 'productivity'));
+  $form->{todo_list}  =  create_todo_list('login_screen' => 1) if (!$::request->is_mobile) and (!$form->{no_todo_list}) and ($main::auth->check_right($::myconfig{login}, 'productivity'));
 
   $form->{stylesheet} =  $myconfig{stylesheet};
   $form->{title}      =  $::locale->text('kivitendo');
@@ -54,11 +55,15 @@ sub company_logo {
 
   my $git             = SL::Git->new;
   ($form->{git_head}) = $git->get_log(since => 'HEAD~1', until => 'HEAD') if $git->is_git_installation;
+  $form->{xmas}       = '_xmas' if (DateTime->today->month == 12 && DateTime->today->day < 27);
+  $form->{xmas}       = '_mir'  if (DateTime->today->month >= 3 && DateTime->today->year == 2022
+                                    && DateTime->today->month <= 9);
+  $form->{xmas}       = '_mir'  if (DateTime->today->day == 24 && DateTime->today->month == 2);
 
   # create the logo screen
   $form->header() unless $form->{noheader};
 
-  print $form->parse_html_template('login/company_logo');
+  print $form->parse_html_template('login/company_logo', { version => $::form->read_version });
 
   $main::lxdebug->leave_sub();
 }
index 7b33183..d859335 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Order entry module
@@ -41,24 +42,26 @@ use SL::FU;
 use SL::OE;
 use SL::IR;
 use SL::IS;
-use SL::MoreCommon qw(ary_diff);
-use SL::PE;
+use SL::Helper::UserPreferences::DisplayPreferences;
+use SL::MoreCommon qw(ary_diff restore_form save_form);
 use SL::ReportGenerator;
+use SL::YAML;
 use List::MoreUtils qw(uniq any none);
 use List::Util qw(min max reduce sum);
 use Data::Dumper;
 
+use SL::Controller::Order;
 use SL::DB::Customer;
 use SL::DB::TaxZone;
+use SL::DB::PaymentTerm;
+use SL::DB::Vendor;
 
+require "bin/mozilla/common.pl";
 require "bin/mozilla/io.pl";
-require "bin/mozilla/arap.pl";
 require "bin/mozilla/reportgenerator.pl";
 
 use strict;
 
-our %TMPL_VAR;
-
 1;
 
 # end of main
@@ -81,10 +84,18 @@ my $oe_access_map = {
   'sales_quotation'   => 'sales_quotation_edit',
 };
 
+my $oe_view_access_map = {
+  'sales_order'       => 'sales_order_edit       | sales_order_view',
+  'purchase_order'    => 'purchase_order_edit    | purchase_order_view',
+  'request_quotation' => 'request_quotation_edit | request_quotation_view',
+  'sales_quotation'   => 'sales_quotation_edit   | sales_quotation_view',
+};
+
 sub check_oe_access {
+  my (%params) = @_;
   my $form     = $main::form;
 
-  my $right   = $oe_access_map->{$form->{type}};
+  my $right   = ($params{with_view}) ? $oe_view_access_map->{$form->{type}} : $oe_access_map->{$form->{type}};
   $right    ||= 'DOES_NOT_EXIST';
 
   $main::auth->assert($right);
@@ -155,7 +166,9 @@ sub add {
     "$form->{script}?action=add&type=$form->{type}&vc=$form->{vc}"
     unless $form->{callback};
 
-  &order_links;
+  $form->{show_details} = $::myconfig{show_form_details};
+
+  order_links(is_new => 1);
   &prepare_order;
   &display_form;
 
@@ -169,6 +182,7 @@ sub edit {
 
   check_oe_access();
 
+  $form->{show_details}                = $::myconfig{show_form_details};
   $form->{taxincluded_changed_by_user} = 1;
 
   # show history button
@@ -181,6 +195,14 @@ sub edit {
 
   # editing without stuff to edit? try adding it first
   if ($form->{rowcount} && !$form->{print_and_save}) {
+    if ($::instance_conf->get_feature_experimental_order) {
+      my $c = SL::Controller::Order->new;
+      $c->action_edit_collective();
+
+      $main::lxdebug->leave_sub();
+      $::dispatcher->end_request;
+    }
+
     my $id;
     map { $id++ if $form->{"multi_id_$_"} } (1 .. $form->{rowcount});
     if (!$id) {
@@ -231,18 +253,15 @@ sub edit {
 sub order_links {
   $main::lxdebug->enter_sub();
 
+  my (%params) = @_;
+
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
   check_oe_access();
 
-  # get customer/vendor
-  $form->all_vc(\%myconfig, $form->{vc}, ($form->{vc} eq 'customer') ? "AR" : "AP");
-
-  # retrieve order/quotation and webdav config
-  $form->{webdav}   = $::instance_conf->get_webdav;
-
+  # retrieve order/quotation
   my $editing = $form->{id};
 
   OE->retrieve(\%myconfig, \%$form);
@@ -254,13 +273,15 @@ sub order_links {
     if          $form->{rowcount}  && $form->{type}     eq 'sales_order'
      && defined $form->{customer}  && $form->{customer} eq '';
 
-  $form->{"$form->{vc}_id"} ||= $form->{"all_$form->{vc}"}->[0]->{id} if $form->{"all_$form->{vc}"};
-
   $form->backup_vars(qw(payment_id language_id taxzone_id salesman_id taxincluded cp_id intnotes shipto_id delivery_term_id currency));
 
   # get customer / vendor
-  IR->get_vendor(\%myconfig, \%$form)   if $form->{type} =~ /(purchase_order|request_quotation)/;
-  IS->get_customer(\%myconfig, \%$form) if $form->{type} =~ /sales_(order|quotation)/;
+  if ($form->{type} =~ /(purchase_order|request_quotation)/) {
+    IR->get_vendor(\%myconfig, \%$form);
+  } else {
+    IS->get_customer(\%myconfig, \%$form);
+    $form->{billing_address_id} = $form->{default_billing_address_id} if $params{is_new};
+  }
 
   $form->restore_vars(qw(payment_id language_id taxzone_id intnotes cp_id shipto_id delivery_term_id));
   $form->restore_vars(qw(currency))    if $form->{id};
@@ -269,18 +290,6 @@ sub order_links {
   $form->{forex}       = $form->{exchangerate};
   $form->{employee}    = "$form->{employee}--$form->{employee_id}";
 
-  # build vendor/customer drop down comatibility... don't ask
-  if (@{ $form->{"all_$form->{vc}"} || [] }) {
-    $form->{"select$form->{vc}"} = 1;
-    $form->{$form->{vc}}         = qq|$form->{$form->{vc}}--$form->{"$form->{vc}_id"}|;
-  }
-
-  $form->{"old$form->{vc}"}  = $form->{$form->{vc}};
-
-  if ($form->{"old$form->{vc}"} !~ m/--\d+$/ && $form->{"$form->{vc}_id"}) {
-    $form->{"old$form->{vc}"} .= qq|--$form->{"$form->{vc}_id"}|
-  }
-
   $main::lxdebug->leave_sub();
 }
 
@@ -308,6 +317,185 @@ sub prepare_order {
   $main::lxdebug->leave_sub();
 }
 
+sub setup_oe_action_bar {
+  my %params = @_;
+  my $form   = $::form;
+
+  my $has_active_periodic_invoice;
+  if ($params{oe_obj}) {
+    $has_active_periodic_invoice =
+         $params{oe_obj}->is_type('sales_order')
+      && $params{oe_obj}->periodic_invoices_config
+      && $params{oe_obj}->periodic_invoices_config->active
+      && (   !$params{oe_obj}->periodic_invoices_config->end_date
+          || ($params{oe_obj}->periodic_invoices_config->end_date > DateTime->today_local))
+      && $params{oe_obj}->periodic_invoices_config->get_previous_billed_period_start_date;
+  }
+
+  my $allow_invoice      = $params{is_req_quo}
+                        || $params{is_pur_ord}
+                        || ($params{is_sales_quo} && $::instance_conf->get_allow_sales_invoice_from_sales_quotation)
+                        || ($params{is_sales_ord} && $::instance_conf->get_allow_sales_invoice_from_sales_order);
+  my @req_trans_cost_art = qw(kivi.SalesPurchase.check_transport_cost_article_presence) x!!$::instance_conf->get_transport_cost_reminder_article_number_id;
+  my @warn_p_invoice     = qw(kivi.SalesPurchase.oe_warn_save_active_periodic_invoice)  x!!$has_active_periodic_invoice;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#form', { action => "update" } ],
+        id        => 'update_button',
+        accesskey => 'enter',
+      ],
+
+      combobox => [
+        action => [
+          t8('Save'),
+          submit  => [ '#form', { action => "save" } ],
+          checks  => [ 'kivi.validate_form', @req_trans_cost_art, @warn_p_invoice ],
+        ],
+        action => [
+          t8('Save as new'),
+          submit   => [ '#form', { action => "save_as_new" } ],
+          checks   => [ 'kivi.validate_form', @req_trans_cost_art ],
+          disabled => !$form->{id} ? t8('This record has not been saved yet.') : undef,
+        ],
+        action => [
+          t8('Save and Close'),
+          submit  => [ '#form', { action => "save_and_close" } ],
+          checks  => [ 'kivi.validate_form', @req_trans_cost_art, @warn_p_invoice ],
+        ],
+        action => [
+          t8('Delete'),
+          submit   => [ '#form', { action => "delete" } ],
+          confirm  => t8('Do you really want to delete this object?'),
+          disabled => !$form->{id}                                                                      ? t8('This record has not been saved yet.')
+                    : (   ($params{is_sales_ord} && !$::instance_conf->get_sales_order_show_delete)
+                       || ($params{is_pur_ord}   && !$::instance_conf->get_purchase_order_show_delete)) ? t8('Deleting this type of record has been disabled in the configuration.')
+                    :                                                                                     undef,
+        ],
+      ], # end of combobox "Save"
+
+      'separator',
+
+      combobox => [
+        action => [ t8('Workflow') ],
+        action => [
+          t8('Sales Order'),
+          submit   => [ '#form', { action => "sales_order" } ],
+          disabled => !$form->{id} ? t8('This record has not been saved yet.') : undef,
+          checks   => [ 'kivi.validate_form' ],
+          only_if  => $params{is_sales_quo} || $params{is_pur_ord},
+        ],
+        action => [
+          t8('Purchase Order'),
+          submit   => [ '#form', { action => "purchase_order" } ],
+          disabled => !$form->{id} ? t8('This record has not been saved yet.') : undef,
+          checks   => [ 'kivi.validate_form' ],
+          only_if  => $params{is_sales_ord} || $params{is_req_quo},
+        ],
+        action => [
+          t8('Delivery Order'),
+          submit   => [ '#form', { action => "delivery_order" } ],
+          disabled => !$form->{id} ? t8('This record has not been saved yet.') : undef,
+          checks   => [ 'kivi.validate_form' ],
+          only_if  => $params{is_sales_ord} || $params{is_pur_ord},
+        ],
+        action => [
+          t8('Invoice'),
+          submit   => [ '#form', { action => "invoice" } ],
+          disabled => !$form->{id} ? t8('This record has not been saved yet.') : undef,
+          checks   => [ 'kivi.validate_form' ],
+          only_if  => $allow_invoice,
+        ],
+        action => [
+          t8('Quotation'),
+          submit   => [ '#form', { action => "quotation" } ],
+          disabled => !$form->{id} ? t8('This record has not been saved yet.') : undef,
+          checks   => [ 'kivi.validate_form' ],
+          only_if  => $params{is_sales_ord},
+        ],
+        action => [
+          t8('Request for Quotation'),
+          submit   => [ '#form', { action => "request_for_quotation" } ],
+          disabled => !$form->{id} ? t8('This record has not been saved yet.') : undef,
+          checks   => [ 'kivi.validate_form' ],
+          only_if  => $params{is_pur_ord},
+        ],
+      ], # end of combobox "Workflow"
+
+      combobox => [
+        action => [ t8('Export') ],
+        action => [
+          t8('Print'),
+          call   => [ 'kivi.SalesPurchase.show_print_dialog' ],
+          checks => [ 'kivi.validate_form' ],
+        ],
+        action => [
+          t8('E Mail'),
+          call     => [ 'kivi.SalesPurchase.show_email_dialog' ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$form->{id} ? t8('This record has not been saved yet.') : undef,
+        ],
+        action => [
+          t8('Download attachments of all parts'),
+          call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
+          disabled => !$form->{id} ? t8('This record has not been saved yet.') : undef,
+          only_if  => $::instance_conf->get_doc_storage,
+        ],
+      ], #end of combobox "Export"
+
+      combobox => [
+        action => [ t8('more') ],
+        action => [
+          t8('History'),
+          call     => [ 'set_history_window', $form->{id} * 1, 'id' ],
+          disabled => !$form->{id} ? t8('This record has not been saved yet.') : undef,
+        ],
+        action => [
+          t8('Follow-Up'),
+          call     => [ 'follow_up_window' ],
+          disabled => !$form->{id} ? t8('This record has not been saved yet.') : undef,
+        ],
+      ], # end of combobox "more"
+    );
+  }
+  $::request->layout->add_javascripts('kivi.Validator.js');
+}
+
+sub setup_oe_search_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Search'),
+        submit    => [ '#form' ],
+        accesskey => 'enter',
+        checks    => [ 'kivi.validate_form' ],
+      ],
+    );
+  }
+  $::request->layout->add_javascripts('kivi.Validator.js');
+}
+
+sub setup_oe_orders_action_bar {
+  my %params = @_;
+
+  return unless $::form->{type} eq 'sales_order';
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('New sales order'),
+        submit    => [ '#form', { action => 'edit' } ],
+        checks    => [ [ 'kivi.check_if_entries_selected', '[name^=multi_id_]' ] ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
 sub form_header {
   $main::lxdebug->enter_sub();
   my @custom_hiddens;
@@ -321,24 +509,20 @@ sub form_header {
 
   # Container for template variables. Unfortunately this has to be
   # visible in form_footer too, so package local level and not my here.
-  %TMPL_VAR = ();
+  my $TMPL_VAR = $::request->cache('tmpl_var', {});
   if ($form->{id}) {
-    my $obj = SL::DB::Order->new(id => $form->{id})->load;
-    $TMPL_VAR{warn_save_active_periodic_invoice} =
-         $obj->is_type('sales_order')
-      && $obj->periodic_invoices_config
-      && $obj->periodic_invoices_config->active
-      && (   !$obj->periodic_invoices_config->end_date
-          || ($obj->periodic_invoices_config->end_date > DateTime->today_local))
-      && $obj->periodic_invoices_config->get_previous_billed_period_start_date;
-
-    $TMPL_VAR{oe_obj} = $obj;
+    $TMPL_VAR->{oe_obj} = SL::DB::Order->new(id => $form->{id})->load;
   }
+  $TMPL_VAR->{vc_obj} = SL::DB::Customer->new(id => $form->{customer_id})->load if $form->{customer_id};
+  $TMPL_VAR->{vc_obj} = SL::DB::Vendor->new(id => $form->{vendor_id})->load     if $form->{vendor_id};
 
   $form->{defaultcurrency} = $form->get_default_currency(\%myconfig);
 
-  $form->{employee_id} = $form->{old_employee_id} if $form->{old_employee_id};
-  $form->{salesman_id} = $form->{old_salesman_id} if $form->{old_salesman_id};
+  my $current_employee   = SL::DB::Manager::Employee->current;
+  $form->{employee_id}   = $form->{old_employee_id} if $form->{old_employee_id};
+  $form->{salesman_id}   = $form->{old_salesman_id} if $form->{old_salesman_id};
+  $form->{employee_id} ||= $current_employee->id;
+  $form->{salesman_id} ||= $current_employee->id;
 
   # openclosed checkboxes
   my @tmp;
@@ -346,17 +530,17 @@ sub form_header {
                         $form->{"delivered"} ? "checked" : "",  $locale->text('Delivery Order(s) for full qty created') if $form->{"type"} =~ /_order$/;
   push @tmp, sprintf qq|<input name="closed" id="closed" type="checkbox" class="checkbox" value="1" %s><label for="closed">%s</label>|,
                         $form->{"closed"}    ? "checked" : "",  $locale->text('Closed')    if $form->{id};
-  $TMPL_VAR{openclosed} = sprintf qq|<tr><td colspan=%d align=center>%s</td></tr>\n|, 2 * scalar @tmp, join "\n", @tmp if @tmp;
+  $TMPL_VAR->{openclosed} = sprintf qq|<tr><td colspan=%d align=center>%s</td></tr>\n|, 2 * scalar @tmp, join "\n", @tmp if @tmp;
 
   my $vc = $form->{vc} eq "customer" ? "customers" : "vendors";
 
   $form->get_lists("taxzones"      => ($form->{id} ? "ALL_TAXZONES" : "ALL_ACTIVE_TAXZONES"),
-                   "payments"      => "ALL_PAYMENTS",
                    "currencies"    => "ALL_CURRENCIES",
-                   "departments"   => "ALL_DEPARTMENTS",
-                   $vc             => { key   => "ALL_" . uc($vc),
-                                        limit => $myconfig{vclimit} + 1 },
                    "price_factors" => "ALL_PRICE_FACTORS");
+  $form->{ALL_PAYMENTS} = SL::DB::Manager::PaymentTerm->get_all( where => [ or => [ obsolete => 0, id => $form->{payment_id} || undef ] ]);
+
+  $form->{ALL_DEPARTMENTS} = SL::DB::Manager::Department->get_all_sorted;
+  $form->{ALL_LANGUAGES}   = SL::DB::Manager::Language->get_all_sorted;
 
   # Projects
   my @old_project_ids = uniq grep { $_ } map { $_ * 1 } ($form->{"globalproject_id"}, map { $form->{"project_id_$_"} } 1..$form->{"rowcount"});
@@ -375,17 +559,17 @@ sub form_header {
       @old_ids_cond,
     ]);
 
-  $TMPL_VAR{ALL_PROJECTS}          = SL::DB::Manager::Project->get_all_sorted(query => \@conditions);
-  $form->{ALL_PROJECTS}            = $TMPL_VAR{ALL_PROJECTS}; # make projects available for second row drop-down in io.pl
+  $TMPL_VAR->{ALL_PROJECTS}          = SL::DB::Manager::Project->get_all_sorted(query => \@conditions);
+  $form->{ALL_PROJECTS}            = $TMPL_VAR->{ALL_PROJECTS}; # make projects available for second row drop-down in io.pl
 
   # label subs
   my $employee_list_query_gen      = sub { $::form->{$_[0]} ? [ or => [ id => $::form->{$_[0]}, deleted => 0 ] ] : [ deleted => 0 ] };
-  $TMPL_VAR{ALL_EMPLOYEES}         = SL::DB::Manager::Employee->get_all_sorted(query => $employee_list_query_gen->('employee_id'));
-  $TMPL_VAR{ALL_SALESMEN}          = SL::DB::Manager::Employee->get_all_sorted(query => $employee_list_query_gen->('salesman_id'));
-  $TMPL_VAR{ALL_SHIPTO}            = SL::DB::Manager::Shipto->get_all_sorted(query => [
+  $TMPL_VAR->{ALL_EMPLOYEES}         = SL::DB::Manager::Employee->get_all_sorted(query => $employee_list_query_gen->('employee_id'));
+  $TMPL_VAR->{ALL_SALESMEN}          = SL::DB::Manager::Employee->get_all_sorted(query => $employee_list_query_gen->('salesman_id'));
+  $TMPL_VAR->{ALL_SHIPTO}            = SL::DB::Manager::Shipto->get_all_sorted(query => [
     or => [ trans_id  => $::form->{"$::form->{vc}_id"} * 1, and => [ shipto_id => $::form->{shipto_id} * 1, trans_id => undef ] ]
   ]);
-  $TMPL_VAR{ALL_CONTACTS}          = SL::DB::Manager::Contact->get_all_sorted(query => [
+  $TMPL_VAR->{ALL_CONTACTS}          = SL::DB::Manager::Contact->get_all_sorted(query => [
     or => [
       cp_cv_id => $::form->{"$::form->{vc}_id"} * 1,
       and      => [
@@ -394,50 +578,35 @@ sub form_header {
       ]
     ]
   ]);
-  $TMPL_VAR{sales_employee_labels} = sub { $_[0]->{name} || $_[0]->{login} };
-  $TMPL_VAR{department_labels}     = sub { "$_[0]->{description}--$_[0]->{id}" };
-
-  # vendor/customer
-  $TMPL_VAR{vc_keys} = sub { "$_[0]->{name}--$_[0]->{id}" };
-  $TMPL_VAR{vclimit} = $myconfig{vclimit};
-  $TMPL_VAR{vc_select} = "customer_or_vendor_selection_window('$form->{vc}', '', @{[ $form->{vc} eq 'vendor' ? 1 : 0 ]}, 0)";
-  push @custom_hiddens, "$form->{vc}_id";
-  push @custom_hiddens, "old$form->{vc}";
-  push @custom_hiddens, "select$form->{vc}";
+  $TMPL_VAR->{sales_employee_labels} = sub { $_[0]->{name} || $_[0]->{login} };
 
   # currencies and exchangerate
-  my @values = map { $_ } @{ $form->{ALL_CURRENCIES} };
-  my %labels = map { $_ => $_ } @{ $form->{ALL_CURRENCIES} };
   $form->{currency}            = $form->{defaultcurrency} unless $form->{currency};
-  $TMPL_VAR{show_exchangerate} = $form->{currency} ne $form->{defaultcurrency};
-  $TMPL_VAR{currencies}        = NTI($cgi->popup_menu('-name' => 'currency', '-default' => $form->{"currency"},
-                                                      '-values' => \@values, '-labels' => \%labels,
-                                                      '-onchange' => "document.getElementById('update_button').click();"
-                                     )) if scalar @values;
+  $TMPL_VAR->{show_exchangerate} = $form->{currency} ne $form->{defaultcurrency};
   push @custom_hiddens, "forex";
   push @custom_hiddens, "exchangerate" if $form->{forex};
 
   # credit remaining
   my $creditwarning = (($form->{creditlimit} != 0) && ($form->{creditremaining} < 0) && !$form->{update}) ? 1 : 0;
-  $TMPL_VAR{is_credit_remaining_negativ} = ($form->{creditremaining} =~ /-/) ? "0" : "1";
+  $TMPL_VAR->{is_credit_remaining_negativ} = ($form->{creditremaining} =~ /-/) ? "0" : "1";
 
   # business
-  $TMPL_VAR{business_label} = ($form->{vc} eq "customer" ? $locale->text('Customer type') : $locale->text('Vendor type'));
+  $TMPL_VAR->{business_label} = ($form->{vc} eq "customer" ? $locale->text('Customer type') : $locale->text('Vendor type'));
 
-  push @custom_hiddens, "customer_klass" if $form->{vc} eq 'customer';
+  push @custom_hiddens, "customer_pricegroup_id" if $form->{vc} eq 'customer';
 
   my $credittext = $locale->text('Credit Limit exceeded!!!');
 
   my $follow_up_vc                =  $form->{ $form->{vc} eq 'customer' ? 'customer' : 'vendor' };
   $follow_up_vc                   =~ s/--\d*\s*$//;
-  $TMPL_VAR{follow_up_trans_info} =  ($form->{type} =~ /_quotation$/ ? $form->{quonumber} : $form->{ordnumber}) . " ($follow_up_vc)";
+  $TMPL_VAR->{follow_up_trans_info} =  ($form->{type} =~ /_quotation$/ ? $form->{quonumber} : $form->{ordnumber}) . " ($follow_up_vc)";
 
   if ($form->{id}) {
-    my $follow_ups = FU->follow_ups('trans_id' => $form->{id});
+    my $follow_ups = FU->follow_ups('trans_id' => $form->{id}, 'not_done' => 1);
 
     if (scalar @{ $follow_ups }) {
-      $TMPL_VAR{num_follow_ups}     = scalar                    @{ $follow_ups };
-      $TMPL_VAR{num_due_follow_ups} = sum map { $_->{due} * 1 } @{ $follow_ups };
+      $TMPL_VAR->{num_follow_ups}     = scalar                    @{ $follow_ups };
+      $TMPL_VAR->{num_due_follow_ups} = sum map { $_->{due} * 1 } @{ $follow_ups };
     }
   }
 
@@ -445,57 +614,74 @@ sub form_header {
   if ($form->{resubmit} && ($form->{format} eq "html")) {
       $dispatch_to_popup  = "window.open('about:blank','Beleg'); document.oe.target = 'Beleg';";
       $dispatch_to_popup .= "document.do.submit();";
-  } elsif ($form->{resubmit}) {
+  } elsif ($form->{resubmit}  && $form->{action_print}) {
     # emulate click for resubmitting actions
-    $dispatch_to_popup  = "document.oe.${_}.click(); " for grep { /^action_/ } keys %$form;
+    $dispatch_to_popup  = "kivi.SalesPurchase.show_print_dialog(); kivi.SalesPurchase.print_record();";
   } elsif ($creditwarning) {
     $::request->{layout}->add_javascripts_inline("alert('$credittext');");
   }
 
   $::request->{layout}->add_javascripts_inline("\$(function(){$dispatch_to_popup});");
-  $TMPL_VAR{dateformat}          = $myconfig{dateformat};
-  $TMPL_VAR{numberformat}        = $myconfig{numberformat};
+  $TMPL_VAR->{dateformat}                             = $myconfig{dateformat};
+  $TMPL_VAR->{numberformat}                           = $myconfig{numberformat};
+  $TMPL_VAR->{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
 
   if ($form->{type} eq 'sales_order') {
     if (!$form->{periodic_invoices_config}) {
       $form->{periodic_invoices_status} = $locale->text('not configured');
 
     } else {
-      my $config                        = YAML::Load($form->{periodic_invoices_config});
+      my $config                        = SL::YAML::Load($form->{periodic_invoices_config});
       $form->{periodic_invoices_status} = $config->{active} ? $locale->text('active') : $locale->text('inactive');
     }
   }
 
-  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.SalesPurchase show_form_details show_history show_vc_details ckeditor/ckeditor ckeditor/adapters/jquery kivi.io autocomplete_customer autocomplete_part));
+  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.SalesPurchase kivi.File kivi.Part kivi.CustomerVendor kivi.Validator show_form_details show_history show_vc_details ckeditor/ckeditor ckeditor/adapters/jquery kivi.io));
+
+
+  # original snippets:
+  my %type_check_vars = (
+    is_sales     => scalar($form->{type} =~ /^sales_/),
+    is_order     => scalar($form->{type} =~ /_order$/),
+    is_sales_quo => scalar($form->{type} =~ /sales_quotation$/),
+    is_req_quo   => scalar($form->{type} =~ /request_quotation$/),
+    is_sales_ord => scalar($form->{type} =~ /sales_order$/),
+    is_pur_ord   => scalar($form->{type} =~ /purchase_order$/),
+  );
+
+  setup_oe_action_bar(
+    %type_check_vars,
+    oe_obj => $TMPL_VAR->{oe_obj},
+    vc_obj => $TMPL_VAR->{vc_obj},
+  );
 
   $form->header;
   if ($form->{CFDD_shipto} && $form->{CFDD_shipto_id} ) {
       $form->{shipto_id} = $form->{CFDD_shipto_id};
   }
-  $TMPL_VAR{HIDDENS} = [ map { name => $_, value => $form->{$_} },
-     qw(id action type vc formname media format proforma queued printed emailed
+
+  $TMPL_VAR->{HIDDENS} = [ map { name => $_, value => $form->{$_} },
+     qw(id type vc proforma queued printed emailed
         title creditlimit creditremaining tradediscount business
-        max_dunning_level dunning_amount shiptoname shiptostreet shiptozipcode
-        CFDD_shipto CFDD_shipto_id shiptocity shiptocountry shiptocontact shiptophone shiptofax
-        shiptodepartment_1 shiptodepartment_2 shiptoemail shiptocp_gender
-        message email subject cc bcc taxpart taxservice taxaccounts cursor_fokus
+        max_dunning_level dunning_amount
+        CFDD_shipto CFDD_shipto_id
+        taxpart taxservice taxaccounts cursor_fokus
         show_details useasnew),
         @custom_hiddens,
-        map { $_.'_rate', $_.'_description', $_.'_taxnumber' } split / /, $form->{taxaccounts} ];  # deleted: discount
+        map { $_.'_rate', $_.'_description', $_.'_taxnumber', $_.'_tax_id' } split / /, $form->{taxaccounts} ];  # deleted: discount
 
-  %TMPL_VAR = (
-     %TMPL_VAR,
-     is_sales        => scalar ($form->{type} =~ /^sales_/),              # these vars are exported, so that the template
-     is_order        => scalar ($form->{type} =~ /_order$/),              # may determine what to show
-     is_sales_quo    => scalar ($form->{type} =~ /sales_quotation$/),
-     is_req_quo      => scalar ($form->{type} =~ /request_quotation$/),
-     is_sales_ord    => scalar ($form->{type} =~ /sales_order$/),
-     is_pur_ord      => scalar ($form->{type} =~ /purchase_order$/),
-  );
+  $TMPL_VAR->{$_} = $type_check_vars{$_} for keys %type_check_vars;
 
-  $TMPL_VAR{ORDER_PROBABILITIES} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
+  $TMPL_VAR->{ORDER_PROBABILITIES} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
+
+  if ($type_check_vars{is_sales} && $::instance_conf->get_transport_cost_reminder_article_number_id) {
+    $TMPL_VAR->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
+  }
 
-  print $form->parse_html_template("oe/form_header", { %TMPL_VAR });
+  print $form->parse_html_template("oe/form_header", {
+    %$TMPL_VAR,
+    %type_check_vars,
+  });
 
   $main::lxdebug->leave_sub();
 }
@@ -511,10 +697,7 @@ sub form_footer {
 
   $form->{invtotal} = $form->{invsubtotal};
 
-  my $introws = max 5, $form->numtextrows($form->{intnotes}, 35, 8);
-
-  $TMPL_VAR{notes}    = qq|<textarea name="notes" class="texteditor" wrap="soft" style="width: 350px; height: 150px">| . H($form->{notes}) . qq|</textarea>|;
-  $TMPL_VAR{intnotes} = qq|<textarea name=intnotes rows="$introws" cols="35">| . H($form->{intnotes}) . qq|</textarea>|;
+  my $TMPL_VAR = $::request->cache('tmpl_var', {});
 
   if( $form->{customer_id} && !$form->{taxincluded_changed_by_user} ) {
     my $customer = SL::DB::Customer->new(id => $form->{customer_id})->load();
@@ -528,16 +711,13 @@ sub form_footer {
         $form->{invtotal} += $form->{"${item}_total"} = $form->round_amount( $form->{"${item}_base"} * $form->{"${item}_rate"}, 2);
         $form->{"${item}_total"} = $form->format_amount(\%myconfig, $form->{"${item}_total"}, 2);
 
-        $TMPL_VAR{tax} .= qq|
+        $TMPL_VAR->{tax} .= qq|
               <tr>
                 <th align=right>$form->{"${item}_description"}&nbsp;| . $form->{"${item}_rate"} * 100 .qq|%</th>
                 <td align=right>$form->{"${item}_total"}</td>
               </tr> |;
       }
     }
-
-#    $form->{invsubtotal} = $form->format_amount(\%myconfig, $form->{invsubtotal}, 2, 0); # template does this
-
   } else {
     foreach my $item (split / /, $form->{taxaccounts}) {
       if ($form->{"${item}_base"}) {
@@ -546,7 +726,7 @@ sub form_footer {
         $form->{"${item}_total"} = $form->format_amount(\%myconfig, $form->{"${item}_total"}, 2);
         $form->{"${item}_netto"} = $form->format_amount(\%myconfig, $form->{"${item}_netto"}, 2);
 
-        $TMPL_VAR{tax} .= qq|
+        $TMPL_VAR->{tax} .= qq|
               <tr>
                 <th align=right>Enthaltene $form->{"${item}_description"}&nbsp;| . $form->{"${item}_rate"} * 100 .qq|%</th>
                 <td align=right>$form->{"${item}_total"}</td>
@@ -559,25 +739,34 @@ sub form_footer {
     }
   }
 
+  my $grossamount = $form->{invtotal};
+  $form->{invtotal} = $form->round_amount( $form->{invtotal}, 2, 1);
+  $form->{rounding} = $form->round_amount(
+    $form->{invtotal} - $form->round_amount($grossamount, 2),
+    2
+  );
   $form->{oldinvtotal} = $form->{invtotal};
 
-  $TMPL_VAR{ALL_DELIVERY_TERMS} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
+  $TMPL_VAR->{ALL_DELIVERY_TERMS} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
+
+  my $print_options_html = setup_sales_purchase_print_options();
+
+  my $shipto_cvars       = SL::DB::Shipto->new->cvars_by_config;
+  foreach my $var (@{ $shipto_cvars }) {
+    my $name = "shiptocvar_" . $var->config->name;
+    $var->value($form->{$name}) if exists $form->{$name};
+  }
 
-  my $tpca_reminder;
-  $tpca_reminder = check_transport_cost_reminder_article_number() if $::instance_conf->get_transport_cost_reminder_article_number_id;
   print $form->parse_html_template("oe/form_footer", {
-     %TMPL_VAR,
-     webdav          => $::instance_conf->get_webdav,
-     tpca_reminder   => $tpca_reminder,
-     print_options   => print_options(inline => 1),
-     label_edit      => $locale->text("Edit the $form->{type}"),
-     label_workflow  => $locale->text("Workflow $form->{type}"),
+     %$TMPL_VAR,
+     print_options   => $print_options_html,
      is_sales        => scalar ($form->{type} =~ /^sales_/),              # these vars are exported, so that the template
      is_order        => scalar ($form->{type} =~ /_order$/),              # may determine what to show
      is_sales_quo    => scalar ($form->{type} =~ /sales_quotation$/),
      is_req_quo      => scalar ($form->{type} =~ /request_quotation$/),
      is_sales_ord    => scalar ($form->{type} =~ /sales_order$/),
      is_pur_ord      => scalar ($form->{type} =~ /purchase_order$/),
+     shipto_cvars    => $shipto_cvars,
   });
 
   $main::lxdebug->leave_sub();
@@ -597,7 +786,17 @@ sub update {
 
   $form->{update} = 1;
 
-  &check_name($form->{vc});
+  my $vc = $form->{vc};
+  if (($form->{"previous_${vc}_id"} || $form->{"${vc}_id"}) != $form->{"${vc}_id"}) {
+    $::form->{salesman_id} = SL::DB::Manager::Employee->current->id if exists $::form->{salesman_id};
+
+    if ($vc eq 'customer') {
+      IS->get_customer(\%myconfig, $form);
+      $::form->{billing_address_id} = $::form->{default_billing_address_id};
+    } else {
+      IR->get_vendor(\%myconfig, $form);
+    }
+  }
 
   if (!$form->{forex}) {        # read exchangerate from input field (not hidden)
     map { $form->{$_} = $form->parse_amount(\%myconfig, $form->{$_}) } qw(exchangerate) unless $recursive_call;
@@ -647,7 +846,7 @@ sub update {
       if ($rows > 1) {
 
         select_item(mode => $mode, pre_entered_qty => $form->{"qty_$i"});
-        ::end_of_request();
+        $::dispatcher->end_request;
 
       } else {
 
@@ -736,7 +935,7 @@ sub search {
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
-  check_oe_access();
+  check_oe_access(with_view => 1);
 
   if ($form->{type} eq 'purchase_order') {
     $form->{vc}        = 'vendor';
@@ -763,17 +962,15 @@ sub search {
     $form->{ordlabel}  = $locale->text('Quotation Number');
 
   } else {
-    $form->show_generic_error($locale->text('oe.pl::search called with unknown type'), back_button => 1);
+    $form->show_generic_error($locale->text('oe.pl::search called with unknown type'));
   }
 
   # setup vendor / customer data
-  $form->all_vc(\%myconfig, $form->{vc}, ($form->{vc} eq 'customer') ? "AR" : "AP");
   $form->get_lists("projects"     => { "key" => "ALL_PROJECTS", "all" => 1 },
-                   "departments"  => "ALL_DEPARTMENTS",
-                   "$form->{vc}s" => "ALL_VC",
                    "taxzones"     => "ALL_TAXZONES",
                    "business_types" => "ALL_BUSINESS_TYPES",);
   $form->{ALL_EMPLOYEES} = SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]);
+  $form->{ALL_DEPARTMENTS} = SL::DB::Manager::Department->get_all;
 
   $form->{CT_CUSTOM_VARIABLES}                  = CVar->get_configs('module' => 'CT');
   ($form->{CT_CUSTOM_VARIABLES_FILTER_CODE},
@@ -786,10 +983,13 @@ sub search {
 
   $form->{ORDER_PROBABILITIES} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
 
+  $::request->{layout}->use_javascript(map { "${_}.js" } qw(autocomplete_project));
+
+  setup_oe_search_action_bar();
+
   $form->header();
 
   print $form->parse_html_template('oe/search', {
-    %myconfig,
     is_order => scalar($form->{type} =~ /_order/),
   });
 
@@ -825,14 +1025,13 @@ sub orders {
   my $locale   = $main::locale;
   my $cgi      = $::request->{cgi};
 
-  check_oe_access();
+  my %params   = @_;
+  check_oe_access(with_view => 1);
 
   my $ordnumber = ($form->{type} =~ /_order$/) ? "ordnumber" : "quonumber";
 
   ($form->{ $form->{vc} }, $form->{"$form->{vc}_id"}) = split(/--/, $form->{ $form->{vc} });
-
   report_generator_set_default_sort('transdate', 1);
-
   OE->transactions(\%myconfig, \%$form);
 
   $form->{rowcount} = scalar @{ $form->{OE} };
@@ -847,14 +1046,14 @@ sub orders {
     "curr",                    "employee",
     "salesman",
     "shipvia",                 "globalprojectnumber",
-    "transaction_description", "open",
+    "transaction_description", "department",            "open",
     "delivered",               "periodic_invoices",
     "marge_total",             "marge_percent",
     "vcnumber",                "ustid",
     "country",                 "shippingpoint",
     "taxzone",                 "insertdate",
     "order_probability",       "expected_billing_date", "expected_netamount",
-    "payment_terms",
+    "payment_terms",           "intnotes",
   );
 
   # only show checkboxes if gotten here via sales_order form.
@@ -866,7 +1065,6 @@ sub orders {
   $form->{l_open}              = $form->{l_closed} = "Y" if ($form->{open}      && $form->{closed});
   $form->{l_delivered}         = "Y"                     if ($form->{delivered} && $form->{notdelivered});
   $form->{l_periodic_invoices} = "Y"                     if ($form->{periodic_invoices_active} && $form->{periodic_invoices_inactive});
-
   map { $form->{"l_${_}"} = 'Y' } qw(order_probability expected_billing_date expected_netamount) if $form->{l_order_probability_expected_billing_date};
 
   my $attachment_basename;
@@ -903,13 +1101,14 @@ sub orders {
                                                         transaction_description transdatefrom transdateto type vc employee_id salesman_id
                                                         reqdatefrom reqdateto projectnumber project_id periodic_invoices_active periodic_invoices_inactive
                                                         business_id shippingpoint taxzone_id reqdate_unset_or_old insertdatefrom insertdateto
-                                                        order_probability_op order_probability_value expected_billing_date_from expected_billing_date_to);
+                                                        order_probability_op order_probability_value expected_billing_date_from expected_billing_date_to
+                                                        parts_partnumber parts_description all department_id intnotes phone_notes fulltext);
   push @hidden_variables, map { "cvar_$_->{name}" } @ct_searchable_custom_variables;
 
   my   @keys_for_url = grep { $form->{$_} } @hidden_variables;
   push @keys_for_url, 'taxzone_id' if $form->{taxzone_id} ne ''; # taxzone_id could be 0
 
-  my $href = build_std_url('action=orders', @keys_for_url);
+  my $href = $params{want_binary_pdf} ? '' : build_std_url('action=orders', @keys_for_url);
 
   my %column_defs = (
     'ids'                     => { 'text' => '', },
@@ -932,6 +1131,7 @@ sub orders {
     'shipvia'                 => { 'text' => $locale->text('Ship via'), },
     'globalprojectnumber'     => { 'text' => $locale->text('Project Number'), },
     'transaction_description' => { 'text' => $locale->text('Transaction description'), },
+    'department'              => { 'text' => $locale->text('Department'), },
     'open'                    => { 'text' => $locale->text('Open'), },
     'delivered'               => { 'text' => $locale->text('Delivery Order created'), },
     'marge_total'             => { 'text' => $locale->text('Ertrag'), },
@@ -947,10 +1147,11 @@ sub orders {
     'expected_billing_date'   => { 'text' => $locale->text('Exp. bill. date'), },
     'expected_netamount'      => { 'text' => $locale->text('Exp. netamount'), },
     'payment_terms'           => { 'text' => $locale->text('Payment Terms'), },
+    'intnotes'                => { 'text' => $locale->text('Internal Notes'), },
     %column_defs_cvars,
   );
 
-  foreach my $name (qw(id transdate reqdate quonumber ordnumber cusordnumber name employee salesman shipvia transaction_description shippingpoint taxzone insertdate payment_terms)) {
+  foreach my $name (qw(id transdate reqdate quonumber ordnumber cusordnumber name employee salesman shipvia transaction_description shippingpoint taxzone insertdate payment_terms department intnotes)) {
     my $sortdir                 = $form->{sort} eq $name ? 1 - $form->{sortdir} : $form->{sortdir};
     $column_defs{$name}->{link} = $href . "&sort=$name&sortdir=$sortdir";
   }
@@ -973,17 +1174,22 @@ sub orders {
                                        'data'           => $form->{OE});
 
   my @options;
-  my ($department) = split m/--/, $form->{department};
 
   push @options, $locale->text('Customer')                . " : $form->{customer}"                        if $form->{customer};
   push @options, $locale->text('Vendor')                  . " : $form->{vendor}"                          if $form->{vendor};
   push @options, $locale->text('Contact Person')          . " : $form->{cp_name}"                         if $form->{cp_name};
-  push @options, $locale->text('Department')              . " : $department"                              if $form->{department};
+  push @options, $locale->text('Department')              . " : $form->{department}"                      if $form->{department};
   push @options, $locale->text('Order Number')            . " : $form->{ordnumber}"                       if $form->{ordnumber};
   push @options, $locale->text('Customer Order Number')   . " : $form->{cusordnumber}"                    if $form->{cusordnumber};
   push @options, $locale->text('Notes')                   . " : $form->{notes}"                           if $form->{notes};
+  push @options, $locale->text('Internal Notes')          . " : $form->{intnotes}"                        if $form->{intnotes};
   push @options, $locale->text('Transaction description') . " : $form->{transaction_description}"         if $form->{transaction_description};
+  push @options, $locale->text('Quick Search')            . " : $form->{all}"                             if $form->{all};
   push @options, $locale->text('Shipping Point')          . " : $form->{shippingpoint}"                   if $form->{shippingpoint};
+  push @options, $locale->text('Part Description')        . " : $form->{parts_description}"               if $form->{parts_description};
+  push @options, $locale->text('Part Number')             . " : $form->{parts_partnumber}"                if $form->{parts_partnumber};
+  push @options, $locale->text('Phone Notes')             . " : $form->{phone_notes}"                     if $form->{phone_notes};
+  push @options, $locale->text('Full Text')               . " : $form->{fulltext}"                        if $form->{fulltext};
   if ( $form->{transdatefrom} or $form->{transdateto} ) {
     push @options, $locale->text('Order Date');
     push @options, $locale->text('From') . " " . $locale->date(\%myconfig, $form->{transdatefrom}, 1)     if $form->{transdatefrom};
@@ -1014,6 +1220,10 @@ sub orders {
     push @options, $locale->text('Steuersatz') . " : " . SL::DB::TaxZone->new(id => $form->{taxzone_id})->load->description;
   }
 
+  if ($form->{department_id}) {
+    push @options, $locale->text('Department') . " : " . SL::DB::Department->new(id => $form->{department_id})->load->description;
+  }
+
   if (($form->{order_probability_value} || '') ne '') {
     push @options, $::locale->text('Order probability') . ' ' . ($form->{order_probability_op} eq 'le' ? '<=' : '>=') . ' ' . $form->{order_probability_value} . '%';
   }
@@ -1026,7 +1236,7 @@ sub orders {
 
   $report->set_options('top_info_text'        => join("\n", @options),
                        'raw_top_info_text'    => $form->parse_html_template('oe/orders_top'),
-                       'raw_bottom_info_text' => $form->parse_html_template('oe/orders_bottom', { 'SHOW_CONTINUE_BUTTON' => $allow_multiple_orders }),
+                       'raw_bottom_info_text' => $form->parse_html_template('oe/orders_bottom'),
                        'output_format'        => 'HTML',
                        'title'                => $form->{title},
                        'attachment_basename'  => $attachment_basename . strftime('_%Y%m%d', localtime time),
@@ -1048,8 +1258,11 @@ sub orders {
 
   my $idx = 1;
 
-  my $edit_url = build_std_url('action=edit', 'type', 'vc');
-
+  my $edit_url = $params{want_binary_pdf}
+               ? ''
+               : ($::instance_conf->get_feature_experimental_order)
+               ? build_std_url('script=controller.pl', 'action=Order/edit', 'type')
+               : build_std_url('action=edit', 'type', 'vc');
   foreach my $oe (@{ $form->{OE} }) {
     map { $oe->{$_} *= $oe->{exchangerate} } @subtotal_columns;
 
@@ -1085,7 +1298,7 @@ sub orders {
       'align'    => 'center',
     };
 
-    $row->{$ordnumber}->{link} = $edit_url . "&id=" . E($oe->{id}) . "&callback=${callback}";
+    $row->{$ordnumber}->{link} = $edit_url . "&id=" . E($oe->{id}) . "&callback=${callback}" unless $params{want_binary_pdf};
 
     my $row_set = [ $row ];
 
@@ -1102,7 +1315,11 @@ sub orders {
 
   $report->add_separator();
   $report->add_data(create_subtotal_row(\%totals, \@columns, \%column_alignment, \@subtotal_columns, 'listtotal'));
-
+  if ($params{want_binary_pdf}) {
+    $report->generate_with_headers();
+    return $report->generate_pdf_content(want_binary_pdf => 1);
+  }
+  setup_oe_orders_action_bar();
   $report->generate_with_headers();
 
   $main::lxdebug->leave_sub();
@@ -1125,6 +1342,8 @@ sub check_delivered_flag {
   foreach my $i (1 .. $form->{rowcount}) {
     next if (!$form->{"id_$i"});
 
+    $form->{"ship_$i"} = 0 if $form->{saveasnew};
+
     if ($form->parse_amount(\%myconfig, $form->{"qty_$i"}) == $form->parse_amount(\%myconfig, $form->{"ship_$i"})) {
       $all_delivered = 1;
       next;
@@ -1135,6 +1354,7 @@ sub check_delivered_flag {
   }
 
   $form->{delivered} = 1 if $all_delivered;
+  $form->{delivered} = 0 if $form->{saveasnew};
 
   $main::lxdebug->leave_sub();
 }
@@ -1162,7 +1382,7 @@ sub save_and_close {
   $form->{$idx} =~ s/\s*$//g;
 
   my $msg = ucfirst $form->{vc};
-  $form->isblank($form->{vc}, $locale->text($msg . " missing!"));
+  $form->isblank($form->{vc} . '_id', $locale->text($msg . " missing!"));
 
   # $locale->text('Customer missing!');
   # $locale->text('Vendor missing!');
@@ -1172,18 +1392,17 @@ sub save_and_close {
 
   &validate_items;
 
-  my $payment_id;
-  if($form->{payment_id}) {
-    $payment_id = $form->{payment_id};
-  }
+  my $vc = $form->{vc};
+  if (($form->{"previous_${vc}_id"} || $form->{"${vc}_id"}) != $form->{"${vc}_id"}) {
+    $::form->{salesman_id} = SL::DB::Manager::Employee->current->id if exists $::form->{salesman_id};
 
-  # if the name changed get new values
-  if (&check_name($form->{vc})) {
-    if($form->{payment_id} eq "") {
-      $form->{payment_id} = $payment_id;
-    }
-    &update;
-    ::end_of_request();
+    IS->get_customer(\%myconfig, $form) if $vc eq 'customer';
+    IR->get_vendor(\%myconfig, $form)   if $vc eq 'vendor';
+
+    $::form->{billing_address_id} = $::form->{default_billing_address_id};
+
+    update();
+    $::dispatcher->end_request;
   }
 
   $form->{id} = 0 if $form->{saveasnew};
@@ -1269,7 +1488,7 @@ sub save {
   $form->{$idx} =~ s/\s*$//g;
 
   my $msg = ucfirst $form->{vc};
-  $form->isblank($form->{vc}, $locale->text($msg . " missing!"));
+  $form->isblank($form->{vc} . '_id', $locale->text($msg . " missing!"));
 
   # $locale->text('Customer missing!');
   # $locale->text('Vendor missing!');
@@ -1280,18 +1499,20 @@ sub save {
   remove_emptied_rows();
   &validate_items;
 
-  my $payment_id;
-  if($form->{payment_id}) {
-    $payment_id = $form->{payment_id};
-  }
+  my $vc = $form->{vc};
+  if (($form->{"previous_${vc}_id"} || $form->{"${vc}_id"}) != $form->{"${vc}_id"}) {
+    $::form->{salesman_id} = SL::DB::Manager::Employee->current->id if exists $::form->{salesman_id};
 
-  # if the name changed get new values
-  if (&check_name($form->{vc})) {
-    if($form->{payment_id} eq "") {
-      $form->{payment_id} = $payment_id;
+    if ($vc eq 'customer') {
+      IS->get_customer(\%myconfig, $form);
+      $::form->{billing_address_id} = $::form->{default_billing_address_id};
+
+    } else {
+      IR->get_vendor(\%myconfig, $form);
     }
-    &update;
-    ::end_of_request();
+
+    update();
+    $::dispatcher->end_request;
   }
 
   $form->{id} = 0 if $form->{saveasnew};
@@ -1354,7 +1575,7 @@ sub save {
   if(!$form->{print_and_save}) {
     delete @{$form}{ary_diff([keys %{ $form }], [qw(login id script type cursor_fokus)])};
     edit();
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
   $main::lxdebug->leave_sub();
 }
@@ -1390,7 +1611,7 @@ sub delete {
     }
     # /saving the history
     $form->info($msg);
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
   $form->error($err);
 
@@ -1433,24 +1654,23 @@ sub invoice {
     $form->{quodate}      = $form->{transdate};
   }
 
-  my $payment_id;
-  if ($form->{payment_id}) {
-    $payment_id = $form->{payment_id};
-  }
+  my $vc = $form->{vc};
+  if (($form->{"previous_${vc}_id"} || $form->{"${vc}_id"}) != $form->{"${vc}_id"}) {
+    $::form->{salesman_id} = SL::DB::Manager::Employee->current->id if exists $::form->{salesman_id};
+
+    IS->get_customer(\%myconfig, $form) if $vc eq 'customer';
+    IR->get_vendor(\%myconfig, $form)   if $vc eq 'vendor';
 
-  # if the name changed get new values
-  if (&check_name($form->{vc})) {
-    $form->{payment_id} = $payment_id if $form->{payment_id} eq "";
-    &update;
-    ::end_of_request();
+    update();
+    $::dispatcher->end_request;
   }
 
-  _oe_remove_delivered_or_billed_rows(id => $form->{id}, type => 'billed');
+  _oe_remove_delivered_or_billed_rows(id => $form->{id}, type => 'billed') if $form->{new_invoice_type} ne 'final_invoice';
 
   $form->{cp_id} *= 1;
 
   for my $i (1 .. $form->{rowcount}) {
-    for (qw(ship qty sellprice basefactor)) {
+    for (qw(ship qty sellprice basefactor discount)) {
       $form->{"${_}_${i}"} = $form->parse_amount(\%myconfig, $form->{"${_}_${i}"}) if $form->{"${_}_${i}"};
     }
     $form->{"converted_from_orderitems_id_$i"} = delete $form->{"orderitems_id_$i"};
@@ -1479,11 +1699,6 @@ sub invoice {
   delete @{$form}{qw(id closed)};
   $form->{rowcount}--;
 
-  if ($form->{type} =~ /_order$/) {
-    $form->{exchangerate} = $exchangerate;
-    &create_backorder;
-  }
-
   my ($script);
   if (   $form->{type} eq 'purchase_order'
       || $form->{type} eq 'request_quotation') {
@@ -1495,7 +1710,9 @@ sub invoice {
 
   if (   $form->{type} eq 'sales_order'
       || $form->{type} eq 'sales_quotation') {
-    $form->{title}  = $locale->text('Add Sales Invoice');
+    $form->{title}  = ($form->{new_invoice_type} eq 'invoice_for_advance_payment') ? $locale->text('Add Invoice for Advance Payment')
+                    : ($form->{new_invoice_type} eq 'final_invoice')               ? $locale->text('Add Final Invoice')
+                    : $locale->text('Add Sales Invoice');
     $form->{script} = 'is.pl';
     $script         = "is";
     $buysell        = 'buy';
@@ -1504,7 +1721,7 @@ sub invoice {
   # bo creates the id, reset it
   map { delete $form->{$_} } qw(id subject message cc bcc printed emailed queued);
   $form->{ $form->{vc} } =~ s/--.*//g;
-  $form->{type} = "invoice";
+  $form->{type} = $form->{new_invoice_type} || "invoice";
 
   # locale messages
   $main::locale = Locale->new("$myconfig{countrycode}", "$script");
@@ -1512,8 +1729,6 @@ sub invoice {
 
   require "bin/mozilla/$form->{script}";
 
-  map { $form->{"select$_"} = "" } ($form->{vc}, "currency");
-
   my $currency = $form->{currency};
   &invoice_links;
 
@@ -1572,76 +1787,6 @@ sub save_exchangerate {
   $main::lxdebug->leave_sub();
 }
 
-sub create_backorder {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-
-  $form->{shipped} = 1;
-
-  # figure out if we need to create a backorder
-  # items aren't saved if qty != 0
-
-  my ($totalqty, $totalship);
-  for my $i (1 .. $form->{rowcount}) {
-    my $qty  = $form->{"qty_$i"};
-    my $ship = $form->{"ship_$i"};
-    $totalqty  += $qty;
-    $totalship += $ship;
-
-    $form->{"qty_$i"} = $qty - $ship;
-  }
-
-  if ($totalship == 0) {
-    map { $form->{"ship_$_"} = $form->{"qty_$_"} } (1 .. $form->{rowcount});
-    $form->{ordtotal} = 0;
-    $form->{shipped}  = 0;
-    return;
-  }
-
-  if ($totalqty == $totalship) {
-    map { $form->{"qty_$_"} = $form->{"ship_$_"} } (1 .. $form->{rowcount});
-    $form->{ordtotal} = 0;
-    return;
-  }
-
-  my @flds = (
-    qw(partnumber description qty ship unit sellprice discount id inventory_accno bin income_accno expense_accno listprice assembly taxaccounts partsgroup)
-  );
-
-  for my $i (1 .. $form->{rowcount}) {
-    map {
-      $form->{"${_}_$i"} =
-        $form->format_amount(\%myconfig, $form->{"${_}_$i"})
-    } qw(sellprice discount);
-  }
-
-  relink_accounts();
-
-  OE->save(\%myconfig, \%$form);
-
-  # rebuild rows for invoice
-  my @a     = ();
-  my $count = 0;
-
-  for my $i (1 .. $form->{rowcount}) {
-    $form->{"qty_$i"} = $form->{"ship_$i"};
-
-    if ($form->{"qty_$i"}) {
-      push @a, {};
-      my $j = $#a;
-      map { $a[$j]->{$_} = $form->{"${_}_$i"} } @flds;
-      $count++;
-    }
-  }
-
-  $form->redo_rows(\@flds, \@a, $count, $form->{rowcount});
-  $form->{rowcount} = $count;
-
-  $main::lxdebug->leave_sub();
-}
-
 sub save_as_new {
   $main::lxdebug->enter_sub();
 
@@ -1667,12 +1812,18 @@ sub save_as_new {
   if ( $form->{reqdate} && $form->{id} ) {
     my $saved_order = OE->retrieve_simple(id => $form->{id});
     if ( $saved_order && $saved_order->{reqdate} eq $form->{reqdate} && $saved_order->{transdate} eq $form->{transdate} ) {
-      my $extra_days     = $form->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval : 1;
+      my $extra_days = $form->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
+                       $form->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
+
+    if (   ($form->{type} eq 'sales_order'     &&  !$::instance_conf->get_deliverydate_on)
+        || ($form->{type} eq 'sales_quotation' &&  !$::instance_conf->get_reqdate_on)) {
+      $form->{reqdate}   = '';
+    } else {
       $form->{reqdate}   = DateTime->today_local->next_workday(extra_days => $extra_days)->to_kivitendo;
+    }
       $form->{transdate} = DateTime->today_local->to_kivitendo;
     }
   }
-
   # update employee
   $form->get_employee();
 
@@ -1725,6 +1876,8 @@ sub check_for_direct_delivery {
     return;
   }
 
+  my $cvars = SL::DB::Shipto->new->cvars_by_config;
+
   if ($form->{shipto_id}) {
     Common->get_shipto_by_id(\%myconfig, $form, $form->{shipto_id}, "CFDD_");
 
@@ -1732,15 +1885,17 @@ sub check_for_direct_delivery {
     map { $form->{"CFDD_${_}"} = $form->{$_ } } grep /^shipto/, keys %{ $form };
   }
 
+  $_->value($::form->{"CFDD_shiptocvar_" . $_->config->name}) for @{ $cvars };
+
   delete $form->{action};
   $form->{VARIABLES} = [ map { { "key" => $_, "value" => $form->{$_} } } grep { ($_ ne 'login') && ($_ ne 'password') && (ref $_ eq "") } keys %{ $form } ];
 
   $form->header();
-  print $form->parse_html_template("oe/check_for_direct_delivery");
+  print $form->parse_html_template("oe/check_for_direct_delivery", { cvars => $cvars });
 
   $main::lxdebug->leave_sub();
 
-  ::end_of_request();
+  $::dispatcher->end_request;
 }
 
 sub purchase_order {
@@ -1836,7 +1991,8 @@ sub poso {
   $form->{old_salesman_id}     = $form->{salesman_id};
 
   # reset
-  map { delete $form->{$_} } qw(id subject message cc bcc printed emailed queued customer vendor creditlimit creditremaining discount tradediscount oldinvtotal delivered ordnumber);
+  map { delete $form->{$_} } qw(id subject message cc bcc printed emailed queued customer vendor creditlimit creditremaining discount tradediscount oldinvtotal delivered ordnumber
+                                taxzone_id currency);
   # this converted variable is also used for sales_order to purchase order and vice versa
   $form->{"converted_from_orderitems_id_$_"} = delete $form->{"orderitems_id_$_"} for 1 .. $form->{"rowcount"};
 
@@ -1934,25 +2090,34 @@ sub delivery_order {
   $main::lxdebug->leave_sub();
 }
 
-sub e_mail {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-
-  check_oe_access();
+sub oe_prepare_xyz_from_order {
+  return if !$::form->{id};
 
-  $form->mtime_ischanged('oe','mail');
-  $form->{print_and_save} = 1;
+  my $order = SL::DB::Order->new(id => $::form->{id})->load;
+  $order->flatten_to_form($::form, format_amounts => 1);
+  $::form->{taxincluded_changed_by_user} = 1;
 
-  my $saved_form = save_form();
+  # hack: add partsgroup for first row if it does not exists,
+  # because _remove_billed_or_delivered_rows and _remove_full_delivered_rows
+  # determine fields to handled by existing fields for the first row. If partsgroup
+  # is missing there, for deleted rows the partsgroup_field is not emptied and in
+  # update_delivery_order it will not considered an empty row ...
+  $::form->{partsgroup_1} = '' if !exists $::form->{partsgroup_1};
 
-  save();
+  # fake last empty row
+  $::form->{rowcount}++;
 
-  restore_form($saved_form, 0, qw(id ordnumber quonumber));
+  _update_ship();
+}
 
-  edit_e_mail();
+sub oe_delivery_order_from_order {
+  oe_prepare_xyz_from_order();
+  delivery_order();
+}
 
-  $main::lxdebug->leave_sub();
+sub oe_invoice_from_order {
+  oe_prepare_xyz_from_order();
+  invoice();
 }
 
 sub yes {
@@ -2011,7 +2176,9 @@ sub report_for_todo_list {
   my $content;
 
   if (@{ $quotations }) {
-    my $edit_url = build_std_url('script=oe.pl', 'action=edit');
+    my $edit_url = ($::instance_conf->get_feature_experimental_order)
+                 ? build_std_url('script=controller.pl', 'action=Order/edit')
+                 : build_std_url('script=oe.pl', 'action=edit');
 
     $content     = $form->parse_html_template('oe/report_for_todo_list', { 'QUOTATIONS' => $quotations,
                                                                            'edit_url'   => $edit_url });
@@ -2030,7 +2197,7 @@ sub edit_periodic_invoices_config {
   check_oe_access();
 
   my $config;
-  $config = YAML::Load($::form->{periodic_invoices_config}) if $::form->{periodic_invoices_config};
+  $config = SL::YAML::Load($::form->{periodic_invoices_config}) if $::form->{periodic_invoices_config};
 
   if ('HASH' ne ref $config) {
     $config =  { periodicity             => 'm',
@@ -2040,6 +2207,13 @@ sub edit_periodic_invoices_config {
                  active                  => 1,
                };
   }
+  # for older configs, replace email preset text if not yet set.
+  $config->{email_subject} ||= GenericTranslations->get(language_id => $::form->{lanuage_id}, translation_type => "preset_text_periodic_invoices_email_subject");
+  $config->{email_body}    ||= GenericTranslations->get(language_id => $::form->{lanuage_id}, translation_type => "salutation_general")
+                             . GenericTranslations->get(language_id => $::form->{lanuage_id}, translation_type => "salutation_punctuation_mark")
+                             . "\n\n"
+                             . GenericTranslations->get(language_id => $::form->{lanuage_id}, translation_type => "preset_text_periodic_invoices_email_body");
+  $config->{email_body}      =~ s{\A[ \n\r]+|[ \n\r]+\Z}{}g;
 
   $config->{periodicity}             = 'm' if none { $_ eq $config->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
   $config->{order_value_periodicity} = 'p' if none { $_ eq $config->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
@@ -2051,8 +2225,13 @@ sub edit_periodic_invoices_config {
   $::form->{AR}    = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
   $::form->{title} = $::locale->text('Edit the configuration for periodic invoices');
 
+  if ($::form->{customer_id}) {
+    $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
+    $::form->{email_recipient_invoice_address} = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id})->invoice_mail;
+  }
+
   $::form->header(no_layout => 1);
-  print $::form->parse_html_template('oe/edit_periodic_invoices_config', $config);
+  print $::form->parse_html_template('oe/edit_periodic_invoices_config', {config => $config});
 
   $::lxdebug->leave_sub();
 }
@@ -2079,9 +2258,15 @@ sub save_periodic_invoices_config {
                  copies                  => $::form->{copies} * 1 ? $::form->{copies} : 1,
                  extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
                  ar_chart_id             => $::form->{ar_chart_id} * 1,
+                 send_email                 => $::form->{send_email} ? 1 : 0,
+                 email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
+                 email_recipient_address    => $::form->{email_recipient_address},
+                 email_sender               => $::form->{email_sender},
+                 email_subject              => $::form->{email_subject},
+                 email_body                 => $::form->{email_body},
                };
 
-  $::form->{periodic_invoices_config} = YAML::Dump($config);
+  $::form->{periodic_invoices_config} = SL::YAML::Dump($config);
 
   $::form->{title} = $::locale->text('Edit the configuration for periodic invoices');
   $::form->header;
@@ -2090,6 +2275,34 @@ sub save_periodic_invoices_config {
   $::lxdebug->leave_sub();
 }
 
+sub _remove_full_delivered_rows {
+
+  my @fields = map { s/_1$//; $_ } grep { m/_1$/ } keys %{ $::form };
+  my @new_rows;
+
+  my $removed_rows = 0;
+  my $row          = 0;
+  while ($row < $::form->{rowcount}) {
+    $row++;
+    next unless $::form->{"id_$row"};
+    my $base_factor = SL::DB::Manager::Unit->find_by(name => $::form->{"unit_$row"})->base_factor;
+    my $base_qty = $::form->parse_amount(\%::myconfig, $::form->{"qty_$row"}) *  $base_factor;
+    my $ship_qty = $::form->{"ship_$row"} *  $base_factor;
+    #$main::lxdebug->message(LXDebug->DEBUG2(),"shipto=".$ship_qty." qty=".$base_qty);
+
+    if (!$ship_qty || ($ship_qty < $base_qty)) {
+      $::form->{"qty_$row"}  = $::form->format_amount(\%::myconfig, ($base_qty - $ship_qty) / $base_factor );
+      $::form->{"ship_$row"} = 0;
+      push @new_rows, { map { $_ => $::form->{"${_}_${row}"} } @fields };
+
+    } else {
+      $removed_rows++;
+    }
+  }
+  $::form->redo_rows(\@fields, \@new_rows, scalar(@new_rows), $::form->{rowcount});
+  $::form->{rowcount} -= $removed_rows;
+}
+
 sub _oe_remove_delivered_or_billed_rows {
   my (%params) = @_;
 
@@ -2098,6 +2311,18 @@ sub _oe_remove_delivered_or_billed_rows {
   my $ord_quot = SL::DB::Order->new(id => $params{id})->load;
   return if !$ord_quot;
 
+  # Prüfung ob itemlinks existieren, falls ja dann neue Implementierung
+
+  if (  $params{type} eq 'delivered' ) {
+      my $orderitem = SL::DB::Manager::OrderItem->get_first( where => [trans_id => $ord_quot->id]);
+      if ( $orderitem) {
+          my @links = $orderitem->linked_records(to => 'SL::DB::DeliveryOrderItem');
+          if ( scalar(@links ) > 0 ) {
+              #$main::lxdebug->message(LXDebug->DEBUG2(),"item recordlinks vorhanden");
+              return _remove_full_delivered_rows();
+          }
+      }
+  }
   my %args    = (
     direction => 'to',
     to        =>   $params{type} eq 'delivered' ? 'DeliveryOrder' : 'Invoice',
@@ -2119,26 +2344,8 @@ sub _oe_remove_delivered_or_billed_rows {
   _remove_billed_or_delivered_rows(quantities => \%handled_base_qtys);
 }
 
-# iterate all positions and match articlenumber
-sub check_transport_cost_reminder_article_number {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-
-  check_oe_access();
-
-  my $transport_article_id = $::instance_conf->get_transport_cost_reminder_article_number_id;
-  for my $i (1 .. $form->{rowcount}) {
-    return if $form->{"id_${i}"} eq $transport_article_id;
-  }
-
-  # simply return the name of the part
-  return SL::DB::Part->new(id => $transport_article_id)->load()->partnumber;
-
-  $main::lxdebug->leave_sub();
-}
 sub dispatcher {
-  foreach my $action (qw(delete delivery_order e_mail invoice print purchase_order quotation
+  foreach my $action (qw(delete delivery_order invoice print purchase_order quotation
                          request_for_quotation sales_order save save_and_close save_as_new ship_to update)) {
     if ($::form->{"action_${action}"}) {
       call_sub($action);
@@ -2148,4 +2355,3 @@ sub dispatcher {
 
   $::form->error($::locale->text('No action defined.'));
 }
-
diff --git a/bin/mozilla/pe.pl b/bin/mozilla/pe.pl
deleted file mode 100644 (file)
index a9cee79..0000000
+++ /dev/null
@@ -1,224 +0,0 @@
-#=====================================================================
-# LX-Office ERP
-# Copyright (C) 2004
-# Based on SQL-Ledger Version 2.1.9
-# Web http://www.lx-office.org
-#
-#=====================================================================
-# SQL-Ledger Accounting
-# Copyright (c) 1998-2002
-#
-#  Author: Dieter Simader
-#   Email: dsimader@sql-ledger.org
-#     Web: http://www.sql-ledger.org
-#
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-#======================================================================
-#
-# partsgroup, pricegroup administration
-#
-#======================================================================
-
-use SL::PE;
-
-require "bin/mozilla/common.pl";
-
-use strict;
-
-1;
-
-# end of main
-
-sub add {
-  $::lxdebug->enter_sub;
-  $::auth->assert('config');
-
-  $::form->{title} = "Add";
-  $::form->{callback} ||= "$::form->{script}?action=add&type=$::form->{type}";
-
-  call_sub("form_$::form->{type}");
-
-  $::lxdebug->leave_sub;
-}
-
-sub edit {
-  $::lxdebug->enter_sub;
-  $::auth->assert('config');
-
-  $::form->{title} = "Edit";
-
-  if ($::form->{type} eq 'partsgroup') {
-    PE->get_partsgroup(\%::myconfig, $::form);
-  }
-  if ($::form->{type} eq 'pricegroup') {
-    PE->get_pricegroup(\%::myconfig, $::form);
-  }
-  call_sub("form_$::form->{type}");
-
-  $::lxdebug->leave_sub;
-}
-
-sub search {
-  $::lxdebug->enter_sub;
-  $::auth->assert('config');
-
-  $::form->header;
-  print $::form->parse_html_template('pe/search', {
-    is_pricegroup => $::form->{type} eq 'pricegroup',
-  });
-
-  $::lxdebug->leave_sub;
-}
-
-sub save {
-  $::lxdebug->enter_sub;
-  $::auth->assert('config');
-
-  if ($::form->{type} eq 'partsgroup') {
-    $::form->isblank("partsgroup", $::locale->text('Group missing!'));
-    PE->save_partsgroup(\%::myconfig, $::form);
-    $::form->redirect($::locale->text('Group saved!'));
-  }
-
-  # choice pricegroup and save
-  if ($::form->{type} eq 'pricegroup') {
-    $::form->isblank("pricegroup", $::locale->text('Pricegroup missing!'));
-    PE->save_pricegroup(\%::myconfig, $::form);
-    $::form->redirect($::locale->text('Pricegroup saved!'));
-  }
-  # saving the history
-  if(!exists $::form->{addition} && $::form->{id} ne "") {
-    $::form->{snumbers} = qq|projectnumber_| . $::form->{projectnumber};
-    $::form->{addition} = "SAVED";
-    $::form->save_history;
-  }
-  # /saving the history
-
-  $::lxdebug->leave_sub;
-}
-
-sub delete {
-  $::lxdebug->enter_sub;
-  $::auth->assert('config');
-
-  PE->delete_tuple(\%::myconfig, $::form);
-
-  if ($::form->{type} eq 'partsgroup') {
-    $::form->redirect($::locale->text('Group deleted!'));
-  }
-  if ($::form->{type} eq 'pricegroup') {
-    $::form->redirect($::locale->text('Pricegroup deleted!'));
-  }
-  # saving the history
-  if(!exists $::form->{addition}) {
-    $::form->{snumbers} = qq|projectnumber_| . $::form->{projectnumber};
-    $::form->{addition} = "DELETED";
-    $::form->save_history;
-  }
-  # /saving the history
-  $::lxdebug->leave_sub;
-}
-
-sub continue { call_sub($::form->{nextsub}); }
-
-sub partsgroup_report {
-  $::lxdebug->enter_sub;
-  $::auth->assert('config');
-
-  $::form->{$_} = $::form->unescape($::form->{$_}) for qw(partsgroup);
-  PE->partsgroups(\%::myconfig, $::form);
-
-  my $callback = build_std_url("action=partsgroup_report", qw(type status));
-
-  my $option = '';
-  $option .= $::locale->text('All')      if $::form->{status} eq 'all';
-  $option .= $::locale->text('Orphaned') if $::form->{status} eq 'orphaned';
-
-  if ($::form->{partsgroup}) {
-    $callback .= "&partsgroup=$::form->{partsgroup}";
-    $option   .= ", " . $::locale->text('Group') . " : $::form->{partsgroup}";
-  }
-
-  # escape callback
-  $::form->{callback} = $callback;
-
-  $::form->header;
-  print $::form->parse_html_template('pe/partsgroup_report', {
-    option   => $option,
-    callback => $callback,
-    editlink => build_std_url('action=edit', qw(type status callback)),
-  });
-
-  $::lxdebug->leave_sub;
-}
-
-sub form_partsgroup {
-  $::lxdebug->enter_sub;
-  $::auth->assert('config');
-
-  # $locale->text('Add Group')
-  # $locale->text('Edit Group')
-  $::form->{title} = $::locale->text("$::form->{title} Group");
-
-  $::form->header;
-  print $::form->parse_html_template('pe/partsgroup_form');
-
-  $::lxdebug->leave_sub;
-}
-
-sub pricegroup_report {
-  $::lxdebug->enter_sub;
-  $::auth->assert('config');
-
-  $::form->{$_} = $::form->unescape($::form->{$_}) for qw(pricegroup);
-  PE->pricegroups(\%::myconfig, $::form);
-
-  my $callback = build_std_url('action=pricegroup_report', qw(type status));
-
-  my $option = '';
-  $option .= $::locale->text('All')      if $::form->{status} eq 'all';
-  $option .= $::locale->text('Orphaned') if $::form->{status} eq 'orphaned';
-
-  if ($::form->{pricegroup}) {
-    $callback .= "&pricegroup=$::form->{pricegroup}";
-    $option   .= ", " . $::locale->text('Pricegroup') . " : $::form->{pricegroup}";
-  }
-
-  # escape callback
-  $::form->{callback} = $callback;
-
-  $::form->header;
-  print $::form->parse_html_template('pe/pricegroup_report', {
-    option   => $option,
-    callback => $callback,
-    editlink => build_std_url('action=edit', qw(type status callback)),
-  });
-
-  $::lxdebug->leave_sub;
-}
-
-sub form_pricegroup {
-  $::lxdebug->enter_sub;
-  $::auth->assert('config');
-
-  # $locale->text('Add Pricegroup')
-  # $locale->text('Edit Pricegroup')
-  $::form->{title} = $::locale->text("$::form->{title} Pricegroup");
-
-  $::form->header;
-  print $::form->parse_html_template('pe/pricegroup_form');
-
-  $::lxdebug->leave_sub;
-}
index f1aeb47..f0ef98e 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Account reconciliation module
@@ -32,6 +33,7 @@
 #======================================================================
 
 use SL::RC;
+use SL::Locale::String qw(t8);
 
 require "bin/mozilla/common.pl";
 
@@ -47,6 +49,8 @@ sub reconciliation {
 
   RC->paymentaccounts(\%::myconfig, $::form);
 
+  setup_rc_reconciliation_action_bar();
+
   $::form->header;
   print $::form->parse_html_template('rc/step1', {
     selection_sub => sub { ("$_[0]{accno}--$_[0]{description}")x2 },
@@ -55,8 +59,6 @@ sub reconciliation {
   $::lxdebug->leave_sub;
 }
 
-sub continue { call_sub($::form->{"nextsub"}); }
-
 sub get_payments {
   $::lxdebug->enter_sub;
   $::auth->assert('cash');
@@ -102,6 +104,8 @@ sub display_form {
   my $statementbalance = $::form->parse_amount(\%::myconfig, $::form->{statementbalance});
   my $difference       = $statementbalance - $clearedbalance - $cleared;
 
+  setup_rc_display_form_action_bar();
+
   $::form->header;
   print $::form->parse_html_template('rc/step2', {
     is_asset         => $::form->{category} eq 'A',
@@ -145,7 +149,7 @@ sub update {
   $::lxdebug->leave_sub;
 }
 
-sub done {
+sub reconcile {
   $::lxdebug->enter_sub;
   $::auth->assert('cash');
 
@@ -159,3 +163,34 @@ sub done {
   $::lxdebug->leave_sub;
 }
 
+sub setup_rc_reconciliation_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Show'),
+        submit    => [ '#form', { action => "get_payments" } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_rc_display_form_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#form', { action => "update" } ],
+        accesskey => 'enter',
+      ],
+      action => [
+        t8('Reconcile'),
+        submit => [ '#form', { action => "reconcile" } ],
+      ],
+    );
+  }
+}
index 89f3b72..f2eb1ef 100644 (file)
@@ -13,7 +13,7 @@ use List::Util qw(max);
 
 use SL::Form;
 use SL::Common;
-use SL::MoreCommon;
+use SL::MoreCommon qw(restore_form save_form);
 use SL::ReportGenerator;
 
 use strict;
@@ -34,6 +34,30 @@ sub report_generator_set_default_sort {
 }
 
 
+sub report_generator_setup_action_bar {
+  my ($type, %params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [
+          $type eq 'pdf' ? $::locale->text('PDF export') : $::locale->text('CSV export'),
+          submit => [ '#report_generator_form', { 'report_generator_dispatch_to' => "report_generator_export_as_${type}" } ],
+        ],
+        action => [
+          $::locale->text('PDF export with attachments'),
+          submit  => [ '#report_generator_form', { report_generator_dispatch_to => "report_generator_export_as_pdf", report_generator_addattachments => 1 } ],
+          only_if => $params{allow_attachments},
+        ],
+      ],
+      action => [
+        $::locale->text('Back'),
+        submit => [ '#report_generator_form', { 'report_generator_dispatch_to' => "report_generator_back" } ],
+      ],
+    );
+  }
+}
+
 sub report_generator_export_as_pdf {
   $main::lxdebug->enter_sub();
 
@@ -68,6 +92,9 @@ sub report_generator_export_as_pdf {
   $allow_font_selection = 0 if ($@);
 
   $form->{title} = $locale->text('PDF export -- options');
+
+  report_generator_setup_action_bar('pdf', allow_attachments => !!$form->{report_generator_hidden_l_attachments});
+
   $form->header();
   print $form->parse_html_template('report_generator/pdf_export_options', { 'HIDDEN'               => \@form_values,
                                                                             'ALLOW_FONT_SELECTION' => $allow_font_selection, });
@@ -90,6 +117,9 @@ sub report_generator_export_as_csv {
   my @form_values = $form->flatten_variables(grep { ($_ ne 'login') && ($_ ne 'password') } keys %{ $form });
 
   $form->{title} = $locale->text('CSV export -- options');
+
+  report_generator_setup_action_bar('csv');
+
   $form->header();
   print $form->parse_html_template('report_generator/csv_export_options', { 'HIDDEN' => \@form_values });
 
index 02023f5..630a2b5 100644 (file)
@@ -28,7 +28,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # module for preparing Income Statement and Balance Sheet
@@ -40,14 +41,14 @@ use POSIX qw(strftime);
 use SL::DB::Default;
 use SL::DB::Project;
 use SL::DB::Customer;
-use SL::PE;
 use SL::RP;
 use SL::Iconv;
+use SL::Locale::String qw(t8);
+use SL::Presenter::Tag;
 use SL::ReportGenerator;
 use Data::Dumper;
 use List::MoreUtils qw(any);
 
-require "bin/mozilla/arap.pl";
 require "bin/mozilla/common.pl";
 require "bin/mozilla/reportgenerator.pl";
 
@@ -97,19 +98,21 @@ use strict;
 # $locale->text('Payments')
 # $locale->text('Project Transactions')
 # $locale->text('Business evaluation')
+# $locale->text('Final Invoice, please use mark as paid manually')
 
 # $form->parse_html_template('rp/html_report_susa')
 
 my $rp_access_map = {
-  'projects'         => 'report',
-  'ar_aging'         => 'general_ledger',
-  'ap_aging'         => 'general_ledger',
-  'receipts'         => 'cash',
-  'payments'         => 'cash',
-  'trial_balance'    => 'report',
-  'income_statement' => 'report',
-  'bwa'              => 'report',
-  'balance_sheet'    => 'report',
+  'projects'           => 'report',
+  'ar_aging'           => 'general_ledger',
+  'ap_aging'           => 'general_ledger',
+  'receipts'           => 'cash',
+  'payments'           => 'cash',
+  'trial_balance'      => 'report',
+  'income_statement'   => 'report',
+  'erfolgsrechnung'    => 'report',
+  'bwa'                => 'report',
+  'balance_sheet'      => 'report',
 };
 
 sub check_rp_access {
@@ -129,6 +132,7 @@ sub report {
   my %title = (
     balance_sheet        => $::locale->text('Balance Sheet'),
     income_statement     => $::locale->text('Income Statement'),
+    erfolgsrechnung      => $::locale->text('Erfolgsrechnung'),
     trial_balance        => $::locale->text('Trial Balance'),
     ar_aging             => $::locale->text('Search AR Aging'),
     ap_aging             => $::locale->text('Search AP Aging'),
@@ -141,7 +145,10 @@ sub report {
   );
 
   $::form->{title} = $title{$::form->{report}};
-  $::request->{layout}->add_javascripts('autocomplete_customer.js');
+  $::request->{layout}->add_javascripts('kivi.CustomerVendor.js');
+  $::request->{layout}->add_javascripts('autocomplete_project.js');
+  $::form->{fromdate} = DateTime->today->truncate(to => 'year')->to_kivitendo;
+  $::form->{todate} = DateTime->today->truncate(to => 'year')->add(years => 1)->add(days => -1)->to_kivitendo;
 
   # get departments
   $::form->all_departments(\%::myconfig);
@@ -152,13 +159,15 @@ sub report {
 
   $::form->get_lists("projects" => { "key" => "ALL_PROJECTS", "all" => 1 });
 
-  my $is_projects         = $::form->{report} eq "projects";
-  my $is_income_statement = $::form->{report} eq "income_statement";
-  my $is_bwa              = $::form->{report} eq "bwa";
-  my $is_balance_sheet    = $::form->{report} eq "balance_sheet";
-  my $is_trial_balance    = $::form->{report} eq "trial_balance";
-  my $is_aging            = $::form->{report} =~ /^a[rp]_aging$/;
-  my $is_payments         = $::form->{report} =~ /(receipts|payments)$/;
+  my $is_projects            = $::form->{report} eq "projects";
+  my $is_income_statement    = $::form->{report} eq "income_statement";
+  my $is_erfolgsrechnung     = $::form->{report} eq "erfolgsrechnung";
+  my $is_bwa                 = $::form->{report} eq "bwa";
+  my $is_balance_sheet       = $::form->{report} eq "balance_sheet";
+  my $is_trial_balance       = $::form->{report} eq "trial_balance";
+  my $is_aging               = $::form->{report} =~ /^a[rp]_aging$/;
+  my $is_payments            = $::form->{report} =~ /(receipts|payments)$/;
+  my $format                 = 'html';
 
   my ($label, $nextsub, $vc);
   if ($is_aging) {
@@ -168,12 +177,9 @@ sub report {
 
     $nextsub = "generate_$::form->{report}";
 
-    # setup vc selection
-    $::form->all_vc(\%::myconfig, $::form->{vc}, $is_sales ? "AR" : "AP");
-    $vc .= "<option>$_->{name}--$_->{id}\n" for @{ $::form->{"all_$::form->{vc}"} };
-    $vc = ($vc)
-        ? qq|<select name=$::form->{vc} class="initial_focus"><option>\n$vc</select>|
-        : qq|<input name=$::form->{vc} size=35 class="initial_focus">|;
+    $vc = qq|<input name=$::form->{vc} size=35 class="initial_focus">|;
+
+    $format = 'pdf';
   }
 
   my ($selection, $paymentaccounts);
@@ -189,24 +195,26 @@ sub report {
     }
   }
 
+  setup_rp_report_action_bar();
+
   $::form->header;
   print $::form->parse_html_template('rp/report', {
-    paymentaccounts     => $paymentaccounts,
-    selection           => $selection,
-    is_aging            => $is_aging,
-    vc                  => $vc,
-    label               => $label,
-    year                => DateTime->today->year,
-    today               => DateTime->today,
-    nextsub             => $nextsub,
-    accrual             => $::instance_conf->get_accounting_method ne 'cash',
-    cash                => $::instance_conf->get_accounting_method eq 'cash',
-    is_payments         => $is_payments,
-    is_trial_balance    => $is_trial_balance,
-    is_balance_sheet    => $is_balance_sheet,
-    is_bwa              => $is_bwa,
-    is_income_statement => $is_income_statement,
-    is_projects         => $is_projects,
+    paymentaccounts        => $paymentaccounts,
+    selection              => $selection,
+    is_aging               => $is_aging,
+    vc                     => $vc,
+    label                  => $label,
+    year                   => DateTime->today->year,
+    today                  => DateTime->today,
+    nextsub                => $nextsub,
+    is_payments            => $is_payments,
+    is_trial_balance       => $is_trial_balance,
+    is_balance_sheet       => $is_balance_sheet,
+    is_bwa                 => $is_bwa,
+    is_income_statement    => $is_income_statement,
+    is_erfolgsrechnung     => $is_erfolgsrechnung,
+    is_projects            => $is_projects,
+    format                 => $format,
   });
 
   $::lxdebug->leave_sub;
@@ -397,6 +405,25 @@ sub generate_income_statement {
   $main::lxdebug->leave_sub();
 }
 
+sub generate_erfolgsrechnung {
+  $::lxdebug->enter_sub;
+  $::auth->assert('report');
+
+  $::form->{decimalplaces} = $::form->{decimalplaces} * 1 || 2;
+  $::form->{padding}       = "&emsp;";
+  $::form->{bold}          = "<b>";
+  $::form->{endbold}       = "</b>";
+  $::form->{br}            = "<br>";
+
+  my $data = RP->erfolgsrechnung(\%::myconfig, $::form);
+
+  $::form->header();
+  print $::form->parse_html_template('rp/erfolgsrechnung', $data);
+
+  $::lxdebug->leave_sub;
+}
+
+
 sub generate_balance_sheet {
   $::lxdebug->enter_sub;
   $::auth->assert('report');
@@ -444,6 +471,18 @@ sub generate_projects {
   my $project            = $form->{project_id} ? SL::DB::Project->new(id => $form->{project_id})->load : undef;
   $form->{projectnumber} = $project ? $project->projectnumber : '';
 
+  # make sure todate and fromdate always have a value, even if the date fields
+  # were left empty or the inputs weren't valid dates/couldn't be parsed
+
+  $project = SL::DB::Project->new() unless $project;  # dummy object for dbh
+  unless ($::locale->parse_date_to_object($::form->{fromdate})) {
+    ($form->{fromdate}) = $project->db->dbh->selectrow_array('select min(transdate) from acc_trans');
+  };
+
+  unless ($::locale->parse_date_to_object($::form->{todate})) {
+    ($form->{todate})   = $project->db->dbh->selectrow_array('select max(transdate) from acc_trans');
+  };
+
   $form->{nextsub} = "generate_projects";
   $form->{title}   = $locale->text('Project Transactions');
   RP->trial_balance(\%myconfig, \%$form);
@@ -592,7 +631,7 @@ sub generate_trial_balance {
   my $attachment_basename = $locale->text('trial_balance');
   my $report              = SL::ReportGenerator->new(\%myconfig, $form);
 
-  my @hidden_variables    = qw(fromdate todate year method);
+  my @hidden_variables    = qw(fromdate todate year method department_id all_accounts);
 
   my $href                = build_std_url('action=generate_trial_balance', grep { $form->{$_} } @hidden_variables);
 
@@ -892,7 +931,7 @@ sub list_accounts {
 sub generate_ar_aging {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('general_ledger | ar_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -915,7 +954,7 @@ sub generate_ar_aging {
 sub generate_ap_aging {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger');
+  $main::auth->assert('general_ledger | ap_transactions');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -968,26 +1007,27 @@ sub aging {
 
   my $report = SL::ReportGenerator->new(\%myconfig, $form);
 
-  my @columns = qw(statement ct invnumber transdate duedate amount open);
-
+  my @columns = qw(statement ct invnumber transdate duedate amount open datepaid current_open type);
   my %column_defs = (
-    'statement' => { 'text' => '', 'visible' => $form->{ct} eq 'customer' ? 'HTML' : 0, },
+    'statement' => { raw_header_data => SL::Presenter::Tag::checkbox_tag("checkall", checkall => '[name^=statement_]'), 'visible' => $form->{ct} eq 'customer' ? 'HTML' : 0, align => "center" },
     'ct'        => { 'text' => $form->{ct} eq 'customer' ? $locale->text('Customer') : $locale->text('Vendor'), },
     'invnumber' => { 'text' => $locale->text('Invoice'), },
     'transdate' => { 'text' => $locale->text('Date'), },
     'duedate'   => { 'text' => $locale->text('Due'), },
     'amount'    => { 'text' => $locale->text('Amount'), },
     'open'      => { 'text' => $locale->text('Open'), },
+    'datepaid'  => { 'text' => $locale->text('Date of Last Payment'), visible => ($form->{reporttype} eq 'custom') },
+    'current_open' => { 'text' => $locale->text('Open Amount at Last Payment Date'), visible => ($form->{reporttype} eq 'custom') },
+    'type'      => { 'text' => $locale->text('Note'), },
   );
 
   my %column_alignment = ('statement' => 'center',
-                          map { $_ => 'right' } qw(open amount));
+                          map { $_ => 'right' } qw(open amount current_open datepaid));
 
   $report->set_options('std_column_visibility' => 1);
   $report->set_columns(%column_defs);
   $report->set_column_order(@columns);
-
-  my @hidden_variables = qw(todate customer vendor arap title ct fordate reporttype);
+  my @hidden_variables = qw(todate customer vendor arap title ct fordate reporttype department fromdate);
   $report->set_export_options('generate_' . ($form->{arap} eq 'ar' ? 'ar' : 'ap') . '_aging', @hidden_variables);
 
   my @options;
@@ -1011,10 +1051,20 @@ sub aging {
     $form->{title} = sprintf($locale->text('Ap aging on %s'), $form->{todate});
   }
 
-  if ($form->{fromdate}) {
-    push @options, $locale->text('for Period') . " " . $locale->text('From') . " " .$locale->date(\%myconfig, $form->{fromdate}, 1) . " " . $locale->text('Bis') . " " . $locale->date(\%myconfig, $form->{todate}, 1);
+  $form->{callback} .= "&reporttype=" . E($form->{reporttype});
+  if ($form->{reporttype} eq 'free') {
+    if ($form->{fromdate}) {
+      push @options, $locale->text('for Period') . " " . $locale->text('From') . " " .
+      $locale->date(\%myconfig, $form->{fromdate}, 1) . " "                          .
+      $locale->text('Bis') . " " . $locale->date(\%myconfig, $form->{todate}, 1);
+    } else {
+      push @options, $locale->text('for Period') . " " . $locale->text('Bis') . " " .
+      $locale->date(\%myconfig, $form->{todate}, 1);
+    }
+  } elsif ($form->{reporttype} eq 'custom') {
+    push @options, $locale->text('Reference day') . " " . $locale->date(\%myconfig, $form->{fordate}, 1);
   } else {
-    push @options, $locale->text('for Period') . " " . $locale->text('Bis') . " " . $locale->date(\%myconfig, $form->{todate}, 1);
+    die "Unknown reporttype for aging";
   }
 
   $attachment_basename = $form->{ct} eq 'customer' ? $locale->text('ar_aging_list') : $locale->text('ap_aging_list');
@@ -1029,7 +1079,7 @@ sub aging {
 
   my $previous_ctid = 0;
   my $row_idx       = 0;
-  my @periods       = qw(open amount);
+  my @periods       = qw(open amount current_open);
   my %subtotals     = map { $_ => 0 } @periods;
   my %totals        = map { $_ => 0 } @periods;
 
@@ -1055,6 +1105,12 @@ sub aging {
     }
 
     $row->{invnumber}->{link} =  build_std_url("script=$ref->{module}.pl", 'action=edit', 'callback', 'id=' . E($ref->{id}));
+    if ($row->{type}->{data} eq 'final_invoice') {
+      $row->{type}->{data} = $locale->text('Final Invoice, please use mark as paid manually');
+      $row->{type}->{link} = build_std_url("script=$ref->{module}.pl", 'action=edit', 'callback', 'id=' . E($ref->{id}));
+    } else {
+      $row->{type}->{data} = '';
+    }
 
     if ($previous_ctid != $ref->{ctid}) {
       $row->{statement}->{raw_data} =
@@ -1082,63 +1138,12 @@ sub aging {
                          'raw_bottom_info_text' => $raw_bottom_info_text);
   }
 
+  setup_rp_aging_action_bar(arap => $form->{arap});
   $report->generate_with_headers();
 
   $main::lxdebug->leave_sub();
 }
 
-sub select_all {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  RP->aging(\%myconfig, \%$form);
-
-  map { $_->{checked} = "checked" } @{ $form->{AG} };
-
-  &aging;
-
-  $main::lxdebug->leave_sub();
-}
-
-sub e_mail {
-  $::lxdebug->enter_sub;
-  $::auth->assert('general_ledger');
-
-  # get name and email addresses
-  my $selected = 0;
-  for my $i (1 .. $::form->{rowcount}) {
-    next unless $::form->{"statement_$i"};
-    $::form->{"$::form->{ct}_id"} = $::form->{"$::form->{ct}_id_$i"};
-    RP->get_customer(\%::myconfig, $::form);
-    $selected = 1;
-    last;
-  }
-
-  $::form->error($::locale->text('Nothing selected!')) unless $selected;
-
-  $::form->{media} = "email";
-
-  # save all other variables
-  my @hidden_values;
-  for my $key (keys %$::form) {
-    next if any { $key eq $_ } qw(login password action email cc bcc subject message type sendmode format header);
-    next unless '' eq ref $::form->{$key};
-    push @hidden_values, $key;
-  }
-
-  $::form->header;
-  print $::form->parse_html_template('rp/e_mail', {
-    show_bcc      => $::auth->assert('email_bcc', 'may fail'),
-    print_options => print_options(inline => 1),
-    hidden_values => \@hidden_values,
-  });
-
-  $::lxdebug->leave_sub;
-}
-
 sub send_email {
   $main::lxdebug->enter_sub();
 
@@ -1153,8 +1158,11 @@ sub send_email {
 
   RP->aging(\%myconfig, \%$form);
 
-  $form->{"statement_1"} = 1;
 
+  my $email_form  = delete $form->{email_form};
+  my %field_names = (to => 'email');
+
+  $form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
   $form->{media} = 'email';
   print_form();
 
@@ -1630,8 +1638,6 @@ sub print_options {
   $::form->{SM}{ $::form->{sendmode} } = "selected";
 
   my $output = $::form->parse_html_template('rp/print_options', {
-    got_printer => $::myconfig{printer},
-    show_latex  => $::lx_office_conf{print_templates}->{latex},
     is_email    => $::form->{media} eq 'email',
   });
 
@@ -1883,4 +1889,42 @@ sub hotfix_reformat_date {
   $main::lxdebug->leave_sub();
 
 }
+
+sub setup_rp_aging_action_bar {
+  my %params = @_;
+
+  return unless $params{arap} eq 'ar';
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [
+          t8('Print'),
+          call   => [ 'kivi.SalesPurchase.show_print_dialog' ],
+          checks => [ [ 'kivi.check_if_entries_selected', '[name^=statement_]' ] ],
+        ],
+        action => [
+          t8('E Mail'),
+          call   => [ 'kivi.SalesPurchase.show_email_dialog', 'send_email' ],
+          checks => [ [ 'kivi.check_if_entries_selected', '[name^=statement_]' ] ],
+        ],
+      ],
+    );
+  }
+}
+
+sub setup_rp_report_action_bar {
+  my %params = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Continue'),
+        submit    => [ '#form', { action => 'continue' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
 1;
index 2e45511..ca4ad0a 100755 (executable)
@@ -4,13 +4,13 @@ use List::MoreUtils qw(any none uniq);
 use List::Util qw(sum first);
 use POSIX qw(strftime);
 
-use Data::Dumper;
 use SL::DB::BankAccount;
 use SL::DB::SepaExport;
 use SL::Chart;
 use SL::CT;
 use SL::Form;
 use SL::GenericTranslations;
+use SL::Locale::String qw(t8);
 use SL::ReportGenerator;
 use SL::SEPA;
 use SL::SEPA::XML;
@@ -24,6 +24,7 @@ sub bank_transfer_add {
   my $form          = $main::form;
   my $locale        = $main::locale;
   my $vc            = $form->{vc} eq 'customer' ? 'customer' : 'vendor';
+  my $vc_no         = $form->{vc} eq 'customer' ? $::locale->text('VN') : $::locale->text('CN');
 
   $form->{title}    = $vc eq 'customer' ? $::locale->text('Prepare bank collection via SEPA XML') : $locale->text('Prepare bank transfer via SEPA XML');
 
@@ -56,8 +57,17 @@ sub bank_transfer_add {
     my $prefix                    = $translations{ $invoice->{language_id} } || $translations{default} || $::locale->text('Invoice');
     $prefix                      .= ' ' unless $prefix =~ m/ $/;
     $invoice->{reference_prefix}  = $prefix;
+
+    # add c_vendor_id or v_vendor_id as a prefix if a entry exists
+    next unless $invoice->{vc_vc_id};
+
+    my $prefix_vc_number             = $translations{ $invoice->{language_id} } || $translations{default} || $vc_no;
+    $prefix_vc_number               .= ' ' unless $prefix_vc_number =~ m/ $/;
+    $invoice->{reference_prefix_vc}  = ' '  . $prefix_vc_number unless $prefix_vc_number =~ m/^ /;
   }
 
+  setup_sepa_add_transfer_action_bar();
+
   $form->header();
   print $form->parse_html_template('sepa/bank_transfer_add',
                                    { 'INVOICES'           => $invoices,
@@ -92,20 +102,36 @@ sub bank_transfer_create {
   my $arap_id        = $vc eq 'customer' ? 'ar_id' : 'ap_id';
   my $invoices       = SL::SEPA->retrieve_open_invoices(vc => $vc);
 
-  # load all open invoices (again), but grep out the ones that were selected with checkboxes beforehand ($_->selected). At this stage we again have all the invoice information, including dropdown with payment_type options
-  # all the information from retrieve_open_invoices is then ADDED to what was passed via @{ $form->{bank_transfers} }
-  # parse amount from the entry in the form, but take skonto_amount from PT again
-  # the map inserts the values of invoice_map directly into the array of hashes
+  # Load all open invoices (again), but grep out the ones that were selected with checkboxes beforehand ($_->selected).
+  # At this stage we again have all the invoice information, including dropdown with payment_type options.
+  # All the information from retrieve_open_invoices is then ADDED to what was passed via @{ $form->{bank_transfers} }.
+  # Parse amount from the entry in the form, but take skonto_amount from PT again.
+  # The map inserts the values of invoice_map directly into the array of hashes.
+  my %selected_ids   = map { ($_ => 1) } @{ $form->{ids} || [] };
   my %invoices_map   = map { $_->{id} => $_ } @{ $invoices };
   my @bank_transfers =
     map  +{ %{ $invoices_map{ $_->{$arap_id} } }, %{ $_ } },
-    grep  { $_->{selected} && (0 < $_->{amount}) && $invoices_map{ $_->{$arap_id} } }
+    grep  { ($_->{selected} || $selected_ids{$_->{$arap_id}}) && (0 < $_->{amount}) && $invoices_map{ $_->{$arap_id} } }
     map   { $_->{amount} = $form->parse_amount($myconfig, $_->{amount}); $_ }
           @{ $form->{bank_transfers} || [] };
 
   # override default payment_type selection and set it to the one chosen by the user
   # in the previous step, so that we don't need the logic in the template
+  my $subtract_days   = $::instance_conf->get_sepa_set_skonto_date_buffer_in_days;
+  my $set_skonto_date = $::instance_conf->get_sepa_set_skonto_date_as_default_exec_date;
+  my $set_duedate     = $::instance_conf->get_sepa_set_duedate_as_default_exec_date;
   foreach my $bt (@bank_transfers) {
+    # add a good recommended exec date
+    # set to skonto date if exists or to duedate
+    # in both cases subtract the same buffer (if configured, default 0)
+    $bt->{recommended_execution_date} =
+      $set_skonto_date && $bt->{payment_type} eq 'with_skonto_pt' ?
+                   DateTime->from_kivitendo($bt->{skonto_date})->subtract(days => $subtract_days)->to_kivitendo
+   :  $set_duedate && $bt->{duedate}                              ?
+                   DateTime->from_kivitendo($bt->{duedate}    )->subtract(days => $subtract_days)->to_kivitendo
+   :  undef;
+
+
     foreach my $type ( @{$bt->{payment_select_options}} ) {
       if ( $type->{payment_type} eq $bt->{payment_type} ) {
         $type->{selected} = 1;
@@ -144,6 +170,8 @@ sub bank_transfer_create {
                                                    'id' => \@vc_ids);
     my @vc_bank_info           = sort { lc $a->{name} cmp lc $b->{name} } values %{ $vc_bank_info };
 
+    setup_sepa_create_transfer_action_bar(is_vendor => $vc eq 'vendor');
+
     $form->header();
     print $form->parse_html_template('sepa/bank_transfer_create',
                                      { 'BANK_TRANSFERS'     => \@bank_transfers,
@@ -185,6 +213,8 @@ sub bank_transfer_search {
 
   $form->{title}    = $vc eq 'customer' ? $::locale->text('List of bank collections') : $locale->text('List of bank transfers');
 
+  setup_sepa_search_transfer_action_bar();
+
   $form->header();
   print $form->parse_html_template('sepa/bank_transfer_search', { vc => $vc });
 
@@ -294,14 +324,14 @@ sub bank_transfer_list {
     $row->{$_}->{data} = $::form->format_amount(\%::myconfig, $row->{$_}->{data}, 2) for qw(sum_amounts);
 
     if (!$export->{closed}) {
-      $row->{selected}->{raw_data} =
-          $cgi->hidden(-name => "exports[+].id", -value => $export->{id})
-        . $cgi->checkbox(-name => "exports[].selected", -value => 1, -label => '');
+      $row->{selected}->{raw_data} = $cgi->checkbox(-name => "ids[]", -value => $export->{id}, -label => '');
     }
 
     $report->add_data($row);
   }
 
+  setup_sepa_list_transfers_action_bar(show_buttons => $open_available, is_vendor => $vc eq 'vendor');
+
   $report->generate_with_headers();
 
   $main::lxdebug->leave_sub();
@@ -318,10 +348,10 @@ sub bank_transfer_edit {
   if (!$form->{mode} || ($form->{mode} eq 'single')) {
     push @ids, $form->{id};
   } else {
-    @ids = map $_->{id}, grep { $_->{selected} } @{ $form->{exports} || [] };
+    @ids = @{ $form->{ids} || [] };
 
     if (!@ids) {
-      $form->show_generic_error($locale->text('You have not selected any export.'), 'back_button' => 1);
+      $form->show_generic_error($locale->text('You have not selected any export.'));
     }
   }
 
@@ -345,20 +375,28 @@ sub bank_transfer_edit {
     $export->{items} = [ grep { !$_->{export_closed} && !$_->{executed} } @{ $export->{items} } ];
 
     if (!@{ $export->{items} }) {
-      $form->show_generic_error($locale->text('All the selected exports have already been closed, or all of their items have already been executed.'), 'back_button' => 1);
+      $form->show_generic_error($locale->text('All the selected exports have already been closed, or all of their items have already been executed.'));
     }
 
   } elsif (!$export) {
     $form->error($locale->text('That export does not exist.'));
   }
 
+  my $show_post_payments_button = any { !$_->{export_closed} && !$_->{executed} } @{ $export->{items} };
+  my $has_executed              = any { $_->{executed}                          } @{ $export->{items} };
+
+  setup_sepa_edit_transfer_action_bar(
+    show_post_payments_button => $show_post_payments_button,
+    has_executed              => $has_executed,
+  );
+
   $form->{title}    = $locale->text('View SEPA export');
   $form->header();
   print $form->parse_html_template('sepa/bank_transfer_edit',
-                                   { 'ids'                       => \@ids,
-                                     'export'                    => $export,
-                                     'current_date'              => $form->current_date(\%main::myconfig),
-                                     'show_post_payments_button' => any { !$_->{export_closed} && !$_->{executed} } @{ $export->{items} },
+                                   { ids                       => \@ids,
+                                     export                    => $export,
+                                     current_date              => $form->current_date(\%main::myconfig),
+                                     show_post_payments_button => $show_post_payments_button,
                                    });
 
   $main::lxdebug->leave_sub();
@@ -371,10 +409,11 @@ sub bank_transfer_post_payments {
   my $locale = $main::locale;
   my $vc     = $form->{vc} eq 'customer' ? 'customer' : 'vendor';
 
-  my @items  = grep { $_->{selected} } @{ $form->{items} || [] };
+  my %selected = map { ($_ => 1) } @{ $form->{ids} || [] };
+  my @items    = grep { $selected{$_->{id}} } @{ $form->{items} || [] };
 
   if (!@items) {
-    $form->show_generic_error($locale->text('You have not selected any item.'), 'back_button' => 1);
+    $form->show_generic_error($locale->text('You have not selected any item.'));
   }
   my @export_ids    = uniq map { $_->{sepa_export_id} } @items;
   my %exports       = map { $_ => SL::SEPA->retrieve_export('id' => $_, 'details' => 1, vc => $vc) } @export_ids;
@@ -388,11 +427,11 @@ sub bank_transfer_post_payments {
   }
 
   if (!@items_to_post) {
-    $form->show_generic_error($locale->text('All the selected exports have already been closed, or all of their items have already been executed.'), 'back_button' => 1);
+    $form->show_generic_error($locale->text('All the selected exports have already been closed, or all of their items have already been executed.'));
   }
 
   if (any { !$_->{execution_date} } @items_to_post) {
-    $form->show_generic_error($locale->text('You have to specify an execution date for each antry.'), 'back_button' => 1);
+    $form->show_generic_error($locale->text('You have to specify an execution date for each antry.'));
   }
 
   SL::SEPA->post_payment('items' => \@items_to_post, vc => $vc);
@@ -410,20 +449,20 @@ sub bank_transfer_payment_list_as_pdf {
   my $locale     = $main::locale;
   my $vc         = $form->{vc} eq 'customer' ? 'customer' : 'vendor';
 
-  my @ids        = @{ $form->{items} || [] };
-  my @export_ids = uniq map { $_->{export_id} } @ids;
+  my @ids        = @{ $form->{ids} || [] };
+  my @export_ids = uniq map { $_->{sepa_export_id} } @{ $form->{items} || [] };
 
-  $form->show_generic_error($locale->text('Multi mode not supported.'), 'back_button' => 1) if 1 != scalar @export_ids;
+  $form->show_generic_error($locale->text('Multi mode not supported.')) if 1 != scalar @export_ids;
 
   my $export = SL::SEPA->retrieve_export('id' => $export_ids[0], 'details' => 1, vc => $vc);
   my @items  = ();
 
   foreach my $id (@ids) {
-    my $item = first { $_->{id} == $id->{id} } @{ $export->{items} };
+    my $item = first { $_->{id} == $id } @{ $export->{items} };
     push @items, $item if $item;
   }
 
-  $form->show_generic_error($locale->text('No transfers were executed in this export.'), 'back_button' => 1) if 1 > scalar @items;
+  $form->show_generic_error($locale->text('No transfers were executed in this export.')) if 1 > scalar @items;
 
   my $report         =  SL::ReportGenerator->new(\%main::myconfig, $form);
 
@@ -476,23 +515,23 @@ sub bank_transfer_download_sepa_xml {
   my $defaults = SL::DB::Default->get;
 
   if (!$defaults->company) {
-    $form->show_generic_error($locale->text('You have to enter a company name in the client configuration.'), 'back_button' => 1);
+    $form->show_generic_error($locale->text('You have to enter a company name in the client configuration.'));
   }
 
   if (($vc eq 'customer') && !$defaults->sepa_creditor_id) {
-    $form->show_generic_error($locale->text('You have to enter the SEPA creditor ID in the client configuration.'), 'back_button' => 1);
+    $form->show_generic_error($locale->text('You have to enter the SEPA creditor ID in the client configuration.'));
   }
 
   my @ids;
   if ($form->{mode} && ($form->{mode} eq 'multi')) {
-     @ids = map $_->{id}, grep { $_->{selected} } @{ $form->{exports} || [] };
+     @ids = @{ $form->{ids} || [] };
 
   } else {
     @ids = ($form->{id});
   }
 
   if (!@ids) {
-    $form->show_generic_error($locale->text('You have not selected any export.'), 'back_button' => 1);
+    $form->show_generic_error($locale->text('You have not selected any export.'));
   }
 
   my @items = ();
@@ -503,7 +542,7 @@ sub bank_transfer_download_sepa_xml {
   }
 
   if (!@items) {
-    $form->show_generic_error($locale->text('All the selected exports have already been closed, or all of their items have already been executed.'), 'back_button' => 1);
+    $form->show_generic_error($locale->text('All the selected exports have already been closed, or all of their items have already been executed.'));
   }
 
   my $message_id = strftime('MSG%Y%m%d%H%M%S', localtime) . sprintf('%06d', $$);
@@ -568,47 +607,32 @@ sub bank_transfer_download_sepa_xml {
   $main::lxdebug->leave_sub();
 }
 
-sub bank_transfer_mark_as_closed_step1 {
+sub bank_transfer_mark_as_closed {
   $main::lxdebug->enter_sub();
 
   my $form       = $main::form;
   my $locale     = $main::locale;
-  my $vc         = $form->{vc} eq 'customer' ? 'customer' : 'vendor';
-
-  my @export_ids = map { $_->{id} } grep { $_->{selected} } @{ $form->{exports} || [] };
-
-  if (!@export_ids) {
-    $form->show_generic_error($locale->text('You have not selected any export.'), 'back_button' => 1);
-  }
-
-  my @open_export_ids = ();
-  foreach my $id (@export_ids) {
-    my $export = SL::SEPA->retrieve_export('id' => $id, vc => $vc);
-    push @open_export_ids, $id if (!$export->{closed});
-  }
 
-  if (!@open_export_ids) {
-    $form->show_generic_error($locale->text('All of the exports you have selected were already closed.'), 'back_button' => 1);
-  }
+  map { SL::SEPA->close_export('id' => $_); } @{ $form->{ids} || [] };
 
   $form->{title} = $locale->text('Close SEPA exports');
   $form->header();
-  print $form->parse_html_template('sepa/bank_transfer_mark_as_closed_step1', { 'OPEN_EXPORT_IDS' => \@open_export_ids, vc => $vc });
+  $form->show_generic_information($locale->text('The selected exports have been closed.'));
 
   $main::lxdebug->leave_sub();
 }
 
-sub bank_transfer_mark_as_closed_step2 {
+sub bank_transfer_undo_sepa_xml {
   $main::lxdebug->enter_sub();
 
   my $form       = $main::form;
   my $locale     = $main::locale;
 
-  map { SL::SEPA->close_export('id' => $_); } @{ $form->{open_export_ids} || [] };
+  map { SL::SEPA->undo_export('id' => $_); } @{ $form->{ids} || [] };
 
-  $form->{title} = $locale->text('Close SEPA exports');
+  $form->{title} = $locale->text('Undo SEPA exports');
   $form->header();
-  $form->show_generic_information($locale->text('The selected exports have been closed.'));
+  $form->show_generic_information($locale->text('The selected exports have been undone.'));
 
   $main::lxdebug->leave_sub();
 }
@@ -619,7 +643,7 @@ sub dispatcher {
   foreach my $action (qw(bank_transfer_create bank_transfer_edit bank_transfer_list
                          bank_transfer_post_payments bank_transfer_download_sepa_xml
                          bank_transfer_mark_as_closed_step1 bank_transfer_mark_as_closed_step2
-                         bank_transfer_payment_list_as_pdf)) {
+                         bank_transfer_payment_list_as_pdf bank_transfer_undo_sepa_xml)) {
     if ($form->{"action_${action}"}) {
       call_sub($action);
       return;
@@ -629,4 +653,114 @@ sub dispatcher {
   $form->error($main::locale->text('No action defined.'));
 }
 
+sub setup_sepa_add_transfer_action_bar {
+  my (%params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Step 2'),
+        submit    => [ '#form', { action => "bank_transfer_create" } ],
+        accesskey => 'enter',
+        checks    => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ],
+      ],
+    );
+  }
+}
+
+sub setup_sepa_create_transfer_action_bar {
+  my (%params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Create'),
+        submit    => [ '#form', { action => "bank_transfer_create" } ],
+        accesskey => 'enter',
+        tooltip   => $params{is_vendor} ? t8('Create bank transfer') : t8('Create bank collection'),
+      ],
+      action => [
+        t8('Back'),
+        call => [ 'kivi.history_back' ],
+      ],
+    );
+  }
+}
+
+sub setup_sepa_search_transfer_action_bar {
+  my (%params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Search'),
+        submit    => [ '#form', { action => 'bank_transfer_list' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_sepa_list_transfers_action_bar {
+  my (%params) = @_;
+
+  return unless $params{show_buttons};
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [ t8('Actions') ],
+        action => [
+          t8('SEPA XML download'),
+          submit => [ '#form', { action => 'bank_transfer_download_sepa_xml' } ],
+          checks => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ],
+        ],
+        action => [
+          t8('Post payments'),
+          submit => [ '#form', { action => 'bank_transfer_edit' } ],
+          checks => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ],
+        ],
+        action => [
+          t8('Mark as closed'),
+          submit => [ '#form', { action => 'bank_transfer_mark_as_closed' } ],
+          checks => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ],
+          confirm => [ $params{is_vendor} ? t8('Do you really want to close the selected SEPA exports? No payment will be recorded for bank transfers that haven\'t been marked as executed yet.')
+                                          : t8('Do you really want to close the selected SEPA exports? No payment will be recorded for bank collections that haven\'t been marked as executed yet.') ],
+        ],
+        action => [
+          t8('Undo SEPA exports'),
+          submit => [ '#form', { action => 'bank_transfer_undo_sepa_xml' } ],
+          checks => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ],
+          confirm => [ t8('Do you really want to undo the selected SEPA exports? You have to reassign the export again.') ],
+        ],
+      ], # end of combobox "Actions"
+    );
+  }
+}
+
+sub setup_sepa_edit_transfer_action_bar {
+  my (%params) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Post'),
+        submit    => [ '#form', { action => 'bank_transfer_post_payments' } ],
+        accesskey => 'enter',
+        tooltip   => t8('Post payments for selected invoices'),
+        checks    => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ],
+        disabled  => $params{show_post_payments_button} ? undef : t8('All payments have already been posted.'),
+      ],
+      action => [
+        t8('Payment list'),
+        submit    => [ '#form', { action => 'bank_transfer_payment_list_as_pdf' } ],
+        accesskey => 'enter',
+        tooltip   => t8('Download list of payments as PDF'),
+        checks    => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ],
+        disabled  => $params{show_post_payments_button} ? t8('All payments must be posted before the payment list can be downloaded.') : undef,
+      ],
+    );
+  }
+}
+
 1;
index 3589d7a..44b9eaf 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #######################################################################
 
 use SL::TODO;
index b66fead..0f26de3 100644 (file)
@@ -18,7 +18,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 # German Tax authority Module and later ELSTER Interface
 # 08.01.14  ELSTER Interface software (taxbird/winston) removed
@@ -29,21 +30,13 @@ use utf8;
 
 require "bin/mozilla/common.pl";
 
-#use strict;
-#no strict 'refs';
-#use diagnostics;
-#use warnings; # FATAL=> 'all';
-#use vars qw($locale $form %myconfig);
-#our ($myconfig);
-#use CGI::Carp "fatalsToBrowser";
-
 use List::Util qw(first);
 
 use SL::DB::Default;
-use SL::PE;
 use SL::RP;
 use SL::USTVA;
 use SL::User;
+use SL::Locale::String qw(t8);
 1;
 
 # this is for our long dates
@@ -94,11 +87,14 @@ sub report {
 
   my $department = '';
   my $hide = '';
+
+  setup_ustva_report_action_bar();
   $form->header;
 
   # Einlesen der Finanzamtdaten
   my $ustva = USTVA->new();
-  $ustva->get_config($::lx_office_conf{paths}{userspath}, 'finanzamt.ini');
+  $ustva->get_config();
+  $ustva->get_finanzamt();
 
   # Hier Einlesen der user-config
   # steuernummer entfernt für prerelease
@@ -116,14 +112,13 @@ sub report {
   );
 
   $form->{$_} = $myconfig{$_} for @a;
-  $form->{$_} = $defaults->$_ for qw(company address co_ustid duns);
 
-  my $openings = $form->{FA_Oeffnungszeiten};
+  my $openings = $form->{fa_oeffnungszeiten};
   $openings =~ s/\\\\n/<br>/g;
 
   my $company_given = ($form->{company} ne '')
     ? qq|<h3>$form->{company}</h3>\n|
-    : qq|<a href="am.pl?action=config">|
+    : qq|<a href="controller.pl?action=ClientConfig/edit">|
       . $locale->text('No Company Name given') . qq|!</a><br>|;
 
 
@@ -142,8 +137,7 @@ sub report {
     ? qq|$form->{co_street}<br>|
         . qq|$form->{co_street1}<br>|
         . qq|$form->{co_zip} $form->{co_city}|
-    : qq|<a href="am.pl?action=config|
-        . qq|&level=Programm--Preferences">|
+    : qq|<a href="controller.pl?action=ClientConfig/edit">|
         . $locale->text('No Company Address given')
         . qq|!</a>\n|;
 
@@ -152,8 +146,8 @@ sub report {
   $form->{co_fax}   = $form->{fax}   unless $form->{co_fax};
   $form->{co_url}   = $form->{urlx}  unless $form->{co_url};
 
-  my $taxnumber_given = ($form->{steuernummer} ne '') ? $form->{steuernummer} : qq|<a href="ustva.pl?action=config_step1">Keine Steuernummer hinterlegt!</a><br>|;
-
+  my $taxnumber_given = ($form->{taxnumber} ne '') ? $form->{taxnumber} : qq|<a href="ustva.pl?action=config_step1">Keine Steuernummer hinterlegt!</a><br>|;
+  my $fa_name_given = ($form->{fa_name} ne '') ? $form->{fa_name} : qq|<a href="ustva.pl?action=config_step1">Kein Finanzamt hinterlegt!</a><br>|;
   my $ustva_vorauswahl = &ustva_vorauswahl();
 
   my @all_years = $form->all_years(\%myconfig);
@@ -171,30 +165,49 @@ sub report {
   $_checked = "checked" if ($form->{kz10} eq '1');
   my $checkbox_kz_10 = qq|<input name="FA_10" id=FA_10 class=checkbox|
     . qq| type=checkbox value="1" $_checked title = "|
-    . $locale->text('Amended Advance Turnover Tax Return (Nr. 10)')
+    . $locale->text('Amended Advance Turnover Tax Return').'(Nr. 10)'
     . qq|">|
     . $locale->text('Amended Advance Turnover Tax Return');
 
-  my $method_local = ($form->{method} eq 'accrual') ? $locale->text('accrual')
-                   : ($form->{method} eq 'cash')    ? $locale->text('cash')
+  $_checked = "checked" if ($form->{kz22} eq '1');
+  my $checkbox_kz_22 = qq|<input name="FA_22" id=FA_22 class=checkbox|
+    . qq| type=checkbox value="1" $_checked title = "|
+    . $locale->text('Receipts attached/extra').'(Nr. 22)'
+    . qq|">|
+    . $locale->text('Receipts attached/extra');
+
+  $_checked = "checked" if ($form->{kz29} eq '1');
+  my $checkbox_kz_29 = qq|<input name="FA_29" id=FA_29 class=checkbox|
+    . qq| type=checkbox value="1" $_checked title = "|
+    . $locale->text('Accounting desired').'(Nr. 29)'
+    . qq|">|
+    . $locale->text('Accounting desired');
+
+  $_checked = "checked" if ($form->{kz26} eq '1');
+  my $checkbox_kz_26 = qq|<input name="FA_26" id=FA_26 class=checkbox|
+    . qq| type=checkbox value="1" $_checked title = "|
+    . $locale->text('Direct debit revoked').'(Nr. 26)'
+    . qq|">|
+    . $locale->text('Direct debit revoked');
+
+  my $method_local = ($form->{accounting_method} eq 'accrual') ? $locale->text('accrual')
+                   : ($form->{accounting_method} eq 'cash')    ? $locale->text('cash')
                    : '';
 
-  my $period_local = ( $form->{FA_voranmeld} eq 'month')   ? $locale->text('month')
-                   : ( $form->{FA_voranmeld} eq 'quarter') ? $locale->text('quarter')
+  my $period_local = ( $form->{fa_voranmeld} eq 'month')   ? $locale->text('month')
+                   : ( $form->{fa_voranmeld} eq 'quarter') ? $locale->text('quarter')
                    : '';
 
-  my $tax_office_banks_ref = [
-    { BLZ             => $form->{FA_BLZ_1},
-      Kontonummer     => $form->{FA_Kontonummer_1},
-      Bankbezeichnung => $form->{FA_Bankbezeichnung_1}
+  my @tax_office_banks_ref = (
+    { BLZ             => $form->{fa_blz_1},
+      Kontonummer     => $form->{fa_kontonummer_1},
+      Bankbezeichnung => $form->{fa_bankbezeichnung_1}
     },
-    { BLZ             => $form->{FA_BLZ_2},
-      Kontonummer     => $form->{FA_Kontonummer_2},
-      Bankbezeichnung => $form->{FA_Bankbezeichnung_oertlich}
+    { BLZ             => $form->{fa_blz_2},
+      Kontonummer     => $form->{fa_kontonummer_2},
+      Bankbezeichnung => $form->{fa_bankbezeichnung_2}
     }
-  ];
-
-  # Which COA is in use?
+  );
 
   $ustva->get_coa($form); # fetches coa and modifies some form variables
 
@@ -203,50 +216,26 @@ sub report {
     company_given    => $company_given,
     address_given    => $address_given,
     taxnumber_given  => $taxnumber_given,
+    fa_name_given    => $fa_name_given,
     taxnumber        => $defaults->taxnumber,
     select_year      => $select_year,
     period_local     => $period_local,
     method_local     => $method_local,
     ustva_vorauswahl => $ustva_vorauswahl,
     checkbox_kz_10   => $checkbox_kz_10,
-    tax_office_banks => $tax_office_banks_ref,
+    checkbox_kz_22   => $checkbox_kz_22,
+    checkbox_kz_29   => $checkbox_kz_29,
+    checkbox_kz_26   => $checkbox_kz_26,
+    tax_office_banks => \@tax_office_banks_ref,
     select_options   => &show_options,
 
   };
 
   print($form->parse_html_template('ustva/report', $template_ref));
 
-
-
-  $::lxdebug->leave_sub();
-}
-
-
-
-sub help {
-  $::lxdebug->enter_sub();
-
-  $::auth->assert('advance_turnover_tax_return');
-
-  # parse help documents under doc
-  $::form->{templates} = 'doc';
-  $::form->{help}      = 'ustva';
-  $::form->{type}      = 'help';
-  $::form->{format}    = 'html';
-  generate_ustva();
-
   $::lxdebug->leave_sub();
 }
 
-sub show {
-  $::lxdebug->enter_sub();
-
-  $::auth->assert('advance_turnover_tax_return');
-
-  #&generate_ustva();
-  $::lxdebug->leave_sub();
-  call_sub($::form->{"nextsub"});
-}
 
 sub ustva_vorauswahl {
   $::lxdebug->enter_sub();
@@ -280,13 +269,13 @@ sub ustva_vorauswahl {
   #$form->{month}= '01';
   #$form->{year}= 2004;
   $select_vorauswahl = qq|
-     <input type=hidden name=day value=$form->{day}>
-     <input type=hidden name=month value=$form->{month}>
-     <input type=hidden name=yymmdd value=$yymmdd>
-     <input type=hidden name=sel value=$sel>
+     <input type="hidden" name="day" value="$form->{day}">
+     <input type="hidden" name="month" value="$form->{month}">
+     <input type="hidden" name="yymmdd" value="$yymmdd">
+     <input type="hidden" name="sel" value="$sel">
   |;
 
-  if ($form->{FA_voranmeld} eq 'month') {
+  if ($form->{fa_voranmeld} eq 'month') {
 
     # Vorauswahl bei monatlichem Voranmeldungszeitraum
 
@@ -311,7 +300,7 @@ sub ustva_vorauswahl {
     my $dfv = '';
 
     # Offset für Dauerfristverlängerung
-    $dfv = '100' if ($form->{FA_dauerfrist} eq '1');
+    $dfv = '100' if ($form->{fa_dauerfrist} eq '1');
 
   SWITCH: {
       $yymmdd <= ($yy + 110 + $dfv) && do {
@@ -382,7 +371,7 @@ sub ustva_vorauswahl {
     }
     $select_vorauswahl .= qq|</select>|;
 
-  } elsif ($form->{FA_voranmeld} eq 'quarter') {
+  } elsif ($form->{fa_voranmeld} eq 'quarter') {
 
     # Vorauswahl bei quartalsweisem Voranmeldungszeitraum
     my %liste = ('41'  => $locale->text('1. Quarter'),
@@ -395,7 +384,7 @@ sub ustva_vorauswahl {
     $yymmdd = "$form->{year}$form->{month}$form->{day}" * 1;
     $sel    = '';
     my $dfv = '';    # Offset für Dauerfristverlängerung
-    $dfv = '100' if ($form->{FA_dauerfrist} eq '1');
+    $dfv = '100' if ($form->{fa_dauerfrist} eq '1');
 
   SWITCH: {
       $yymmdd <= ($yy + 110 + $dfv) && do {
@@ -478,18 +467,6 @@ sub ustva_vorauswahl {
   return $select_vorauswahl;
 }
 
-#sub config {
-#  $::lxdebug->enter_sub();
-#  config_step1();
-#  $::lxdebug->leave_sub();
-#}
-
-sub debug {
-  $::lxdebug->enter_sub();
-  $::form->debug();
-  $::lxdebug->leave_sub();
-}
-
 sub show_options {
   $::lxdebug->enter_sub();
 
@@ -506,6 +483,16 @@ sub show_options {
     . $::locale->text('HTML')
     . qq|</option>|;
 
+  #my $disabled= qq|disabled="disabled"|;
+  #$disabled='' if ($form->{elster} eq '1' );
+  #if ($::form->{elster} eq '1') {
+  if ( 1 ) {
+    $format .=
+        qq|<option value=elstertaxbird>|
+      . $::locale->text('ELSTER Export (via Geierlein)')
+      . qq|</option>|;
+  }
+
   my $show_options = qq|
     $type
     $media
@@ -527,150 +514,14 @@ sub generate_ustva {
   $::auth->assert('advance_turnover_tax_return');
 
   my $defaults = SL::DB::Default->get;
-  $form->error($::locale->text('No print templates have been created for this client yet. Please do so in the client configuration.')) if !$defaults->templates;
-  $form->{templates} = $defaults->templates;
-
-  # Aufruf von get_config zum Einlesen der Finanzamtdaten aus finanzamt.ini
 
   my $ustva = USTVA->new();
-  $ustva->get_config($::lx_office_conf{paths}{userspath}, 'finanzamt.ini');
-
-  # init some form vars
-  my @anmeldungszeitraum =
-    qw('0401' '0402' '0403'
-       '0404' '0405' '0406'
-       '0407' '0408' '0409'
-       '0410' '0411' '0412'
-       '0441' '0442' '0443' '0444');
-
-  foreach my $item (@anmeldungszeitraum) {
-    $form->{$item} = "";
-  }
-
-    #forgotten the year --> thisyear
-    if ($form->{year} !~ m/^\d\d\d\d$/) {
-      $form->{year} = substr(
-                             $form->datetonum(
-                                    $form->current_date(\%myconfig), \%myconfig
-                             ),
-                             0, 4);
-      $::lxdebug->message(LXDebug->DEBUG1,
-                        qq|Actual year from Database: $form->{year}\n|);
-    }
-
-    #
-    # using dates in ISO-8601 format: yyyymmmdd  for Postgres...
-    #
-
-    #yearly report
-    if ($form->{period} eq "13") {
-      $form->{fromdate} = "$form->{year}0101";
-      $form->{todate}   = "$form->{year}1231";
-    }
-
-    #Quater reports
-    if ($form->{period} eq "41") {
-      $form->{fromdate} = "$form->{year}0101";
-      $form->{todate}   = "$form->{year}0331";
-      $form->{'0441'}   = "X";
-    }
-    if ($form->{period} eq "42") {
-      $form->{fromdate} = "$form->{year}0401";
-      $form->{todate}   = "$form->{year}0630";
-      $form->{'0442'}   = "X";
-    }
-    if ($form->{period} eq "43") {
-      $form->{fromdate} = "$form->{year}0701";
-      $form->{todate}   = "$form->{year}0930";
-      $form->{'0443'}   = "X";
-    }
-    if ($form->{period} eq "44") {
-      $form->{fromdate} = "$form->{year}1001";
-      $form->{todate}   = "$form->{year}1231";
-      $form->{'0444'}   = "X";
-    }
-
-    #Monthly reports
-  SWITCH: {
-      $form->{period} eq "01" && do {
-        $form->{fromdate} = "$form->{year}0101";
-        $form->{todate}   = "$form->{year}0131";
-        $form->{'0401'}   = "X";
-        last SWITCH;
-      };
-      $form->{period} eq "02" && do {
-        $form->{fromdate} = "$form->{year}0201";
+  $ustva->get_config();
+  $ustva->get_finanzamt();
 
-        #this works from 1901 to 2099, 1900 and 2100 fail.
-        my $leap = ($form->{year} % 4 == 0) ? "29" : "28";
-        $form->{todate} = "$form->{year}02$leap";
-        $form->{"0402"} = "X";
-        last SWITCH;
-      };
-      $form->{period} eq "03" && do {
-        $form->{fromdate} = "$form->{year}0301";
-        $form->{todate}   = "$form->{year}0331";
-        $form->{"0403"}   = "X";
-        last SWITCH;
-      };
-      $form->{period} eq "04" && do {
-        $form->{fromdate} = "$form->{year}0401";
-        $form->{todate}   = "$form->{year}0430";
-        $form->{"0404"}   = "X";
-        last SWITCH;
-      };
-      $form->{period} eq "05" && do {
-        $form->{fromdate} = "$form->{year}0501";
-        $form->{todate}   = "$form->{year}0531";
-        $form->{"0405"}   = "X";
-        last SWITCH;
-      };
-      $form->{period} eq "06" && do {
-        $form->{fromdate} = "$form->{year}0601";
-        $form->{todate}   = "$form->{year}0630";
-        $form->{"0406"}   = "X";
-        last SWITCH;
-      };
-      $form->{period} eq "07" && do {
-        $form->{fromdate} = "$form->{year}0701";
-        $form->{todate}   = "$form->{year}0731";
-        $form->{"0407"}   = "X";
-        last SWITCH;
-      };
-      $form->{period} eq "08" && do {
-        $form->{fromdate} = "$form->{year}0801";
-        $form->{todate}   = "$form->{year}0831";
-        $form->{"0408"}   = "X";
-        last SWITCH;
-      };
-      $form->{period} eq "09" && do {
-        $form->{fromdate} = "$form->{year}0901";
-        $form->{todate}   = "$form->{year}0930";
-        $form->{"0409"}   = "X";
-        last SWITCH;
-      };
-      $form->{period} eq "10" && do {
-        $form->{fromdate} = "$form->{year}1001";
-        $form->{todate}   = "$form->{year}1031";
-        $form->{"0410"}   = "X";
-        last SWITCH;
-      };
-      $form->{period} eq "11" && do {
-        $form->{fromdate} = "$form->{year}1101";
-        $form->{todate}   = "$form->{year}1130";
-        $form->{"0411"}   = "X";
-        last SWITCH;
-      };
-      $form->{period} eq "12" && do {
-        $form->{fromdate} = "$form->{year}1201";
-        $form->{todate}   = "$form->{year}1231";
-        $form->{"0412"}   = "X";
-        last SWITCH;
-      };
-    }
+  # Setze Anmeldungszeitraum
 
-  # Kontrollvariablen für die Templates
-  $form->{"year$_"} = ($form->{year} >= $_ ) ? "1":"0" for 2007..2107;
+  $ustva->set_FromTo(\%$form);
 
   # Get the USTVA
   $ustva->ustva(\%myconfig, \%$form);
@@ -737,69 +588,15 @@ sub generate_ustva {
 
   if ($form->{address} ne '') {
     my $temp = $form->{address};
-    $temp =~ s/\\n/<br \/>/;
-    ($form->{co_street}, $form->{co_city}) = split("<br \/>", $temp);
+    $temp =~ s/\n/<br \/>/;
+    ($form->{co_street}, $form->{co_city}) = split("<br \/>", $temp,2);
     $form->{co_city} =~ s/\\n//g;
   }
 
-  ################################
-  #
-  # Nation specific customisations
-  #
-  ################################
-
-  # Germany
-
-  if ( $form->{coa} eq 'Germany-DATEV-SKR03EU' or $form->{coa} eq 'Germany-DATEV-SKR04EU') {
-
-    #
-    # Outputformat specific customisation's
-    #
-
-    my @category_cent = $ustva->report_variables({
-        myconfig    => \%myconfig,
-        form        => $form,
-        type        => '',
-        attribute   => 'position',
-        dec_places  => '2',
-    });
-
-    push @category_cent, qw(Z43  Z45  Z53  Z62  Z65  Z67);
-
-    my @category_euro = $ustva->report_variables({
-        myconfig    => \%myconfig,
-        form        => $form,
-        type        => '',
-        attribute   => 'position',
-        dec_places  => '0',
-    });
-
-    $form->{id} = [];
-    $form->{amount} = [];
-
-    if ( $form->{format} eq 'pdf' or $form->{format} eq 'postscript') {
-
-      $form->{IN} = "$form->{type}-$form->{year}.tex";
-      $form->{padding} = "~~";
-      $form->{bold}    = "\textbf{";
-      $form->{endbold} = "}";
-      $form->{br}      = '\\\\';
-
-      # Zahlenformatierung für Latex USTVA Formulare
-
-      foreach my $number (@category_euro) {
-        $form->{$number} = $form->format_amount(\%myconfig, $form->{$number}, '0', '');
-      }
-
-      my ${decimal_comma} = ( $myconfig{numberformat} eq '1.000,00'
-           or $myconfig{numberformat} eq '1000,00' ) ? ',':'.';
+   $form->{id} = [];
+   $form->{amount} = [];
 
-      foreach my $number (@category_cent) {
-        $form->{$number} = $form->format_amount(\%myconfig, $form->{$number}, '2', '');
-        $form->{$number} =~ s/${decimal_comma}/~~/g;
-      }
-
-    } elsif ( $form->{format} eq 'html') { # Formatierungen für HTML Ausgabe
+   if ( $form->{format} eq 'html') { # Formatierungen für HTML Ausgabe
 
       $form->{IN} = $form->{type} . '.html';
       $form->{padding} = "&nbsp;&nbsp;";
@@ -808,63 +605,18 @@ sub generate_ustva {
       $form->{br}      = "<br>";
       $form->{address} =~ s/\\n/\n/g;
 
-      foreach my $number (@category_cent) {
+      foreach my $number (@{$::form->{category_cent}}) {
         $form->{$number} = $form->format_amount(\%myconfig, $form->{$number}, '2', '0');
       }
 
-      foreach my $number (@category_euro) {
+      foreach my $number (@{$::form->{category_euro}}) {
         $form->{$number} = $form->format_amount(\%myconfig, $form->{$number}, '0', '0');
       }
-    } elsif ( $form->{format} eq '' ){ # No format error.
-      $form->header;
-      USTVA::error( $locale->text('Application Error. No Format given' ) . "!");
-      ::end_of_request();
-
-    } else { # All other Formats are wrong
+   } else { # we have only html
       $form->header;
       USTVA::error( $locale->text('Application Error. Wrong Format') . ": " . $form->{format} );
-      ::end_of_request();
-    }
-
-
-  } else  # Outputformat for generic output
-  {
-
-    my @category_cent = $ustva->report_variables({
-        myconfig    => \%myconfig,
-        form        => $form,
-        type        => '',
-        attribute   => 'position',
-        dec_places  => '2',
-    });
-
-    my @category_euro = $ustva->report_variables({
-        myconfig    => \%myconfig,
-        form        => $form,
-        type        => '',
-        attribute   => 'position',
-        dec_places  => '0',
-    });
-
-    $form->{USTVA} = [];
-
-    if ( $form->{format} eq 'generic') { # Formatierungen für HTML Ausgabe
-
-      my $rec_ref = {};
-      for my $kennziffer (@category_cent, @category_euro) {
-        $rec_ref = {};
-        $rec_ref->{id} = $kennziffer;
-        $rec_ref->{amount} = $form->format_amount(\%myconfig, $form->{$kennziffer}, 2, '0');
-
-        $::lxdebug->message($LXDebug::DEBUG, "Kennziffer $kennziffer: '$form->{$kennziffer}'" );
-        $::lxdebug->dump($LXDebug::DEBUG, $rec_ref );
-        push @ { $form->{USTVA} }, $rec_ref;
-      }
-
-    }
-
-  }
-
+      $::dispatcher->end_request;
+   }
   if ( $form->{period} eq '13' and $form->{format} ne 'html') {
     $form->header;
     USTVA::info(
@@ -873,29 +625,12 @@ sub generate_ustva {
       . '!');
   }
 
-  $form->{templates} = "doc" if ( $form->{type} eq 'help' );
-
-  if ($form->{format} eq 'generic'){
-
-    $form->header();
-
-    my $template_ref = {
-        taxnumber => $defaults->taxnumber,
-    };
+  # add a prefix for ustva pos numbers, i.e.: 81 ->  post_ustva_81
+  $form->{"pos_ustva_$_"} = $form->{$_} for grep { m{^\d+} } keys %{ $form };
+  $form->{title} = $locale->text('Advance turnover tax return');
 
-    print($form->parse_html_template('ustva/generic_taxreport', $template_ref));
-
-  } else
-  {
-   # add a prefix for ustva pos numbers, i.e.: 81 ->  post_ustva_81
-   $form->{"pos_ustva_$_"} = $form->{$_} for grep { m{^\d+} } keys %{ $form };
-   $form->{title} = $locale->text('Advance turnover tax return');
-
-   $form->header;
-   print $form->parse_html_template('ustva/ustva');
-
-
-  }
+  $form->header;
+  print $form->parse_html_template('ustva/ustva');
 
   $::lxdebug->leave_sub();
 }
@@ -909,32 +644,34 @@ $::form->{title} = $::locale->text('Tax Office Preferences');
 
   # edit all taxauthority prefs
 
+  setup_ustva_config_step1_action_bar();
+
   $::form->header;
 
   my $ustva = USTVA->new();
-  $ustva->get_config($::lx_office_conf{paths}{userspath}, 'finanzamt.ini');
+  $ustva->get_config();
+  $ustva->get_finanzamt();
 
-  my $land = $::form->{elsterland};
-  my $amt  = $::form->{elsterFFFF};
+  my $land = $::form->{fa_land_nr};
+  my $amt  = $::form->{fa_bufa_nr};
 
 
   $::form->{title} = $::locale->text('Tax Office Preferences');
 
 
   my $select_tax_office               = $ustva->fa_auswahl($land, $amt, $ustva->query_finanzamt(\%::myconfig, $::form));
-  my $checked_accrual                 = $::form->{method}        eq 'accrual' ? q|checked="checked"| : '';
-  my $checked_cash                    = $::form->{method}        eq 'cash'    ? q|checked="checked"| : '';
-  my $checked_monthly                 = $::form->{FA_voranmeld}  eq 'month'   ? "checked"            : '';
-  my $checked_quarterly               = $::form->{FA_voranmeld}  eq 'quarter' ? "checked"            : '';
-  my $checked_dauerfristverlaengerung = $::form->{FA_dauerfrist} eq '1'       ? "checked"            : '';
-  my $checked_kz_71                   = $::form->{FA_71}         eq 'X'       ? "checked"            : '';
+  my $method_local = ($::form->{accounting_method} eq 'accrual') ? $::locale->text('accrual')
+                   : ($::form->{accounting_method} eq 'cash')    ? $::locale->text('cash')
+                   : '';
+
+  my $checked_monthly                 = $::form->{fa_voranmeld}  eq 'month'   ? "checked"            : '';
+  my $checked_quarterly               = $::form->{fa_voranmeld}  eq 'quarter' ? "checked"            : '';
+  my $checked_dauerfristverlaengerung = $::form->{da_dauerfrist} eq '1'       ? "checked"            : '';
 
   my $_hidden_variables_ref;
 
   my %_hidden_local_variables = (
     'saved'       => $::locale->text('Check Details'),
-    'nextsub'     => 'config_step2',
-    'warnung'     => '0',
   );
 
   foreach my $variable (keys %_hidden_local_variables) {
@@ -942,34 +679,20 @@ $::form->{title} = $::locale->text('Tax Office Preferences');
         { 'variable' => $variable, 'value' => $_hidden_local_variables{$variable} };
   }
 
-  my @_hidden_form_variables = qw(
-    FA_Name             FA_Strasse        FA_PLZ
-    FA_Ort              FA_Telefon        FA_Fax
-    FA_PLZ_Grosskunden  FA_PLZ_Postfach   FA_Postfach
-    FA_BLZ_1            FA_Kontonummer_1  FA_Bankbezeichnung_1
-    FA_BLZ_2            FA_Kontonummer_2  FA_Bankbezeichnung_oertlich
-    FA_Oeffnungszeiten  FA_Email          FA_Internet
-    steuernummer        elsterland        elstersteuernummer
-    elsterFFFF
-  );
+  my @_hidden_form_variables = $ustva->get_fiamt_vars();
+  push @_hidden_form_variables ,qw(fa_bufa_nr taxnumber accounting_method coa);
 
   foreach my $variable (@_hidden_form_variables) {
     push @{ $_hidden_variables_ref},
         { 'variable' => $variable, 'value' => $::form->{$variable} };
   }
 
-# Which COA is in use?
-
   $ustva->get_coa($::form); # fetches coa and modifies some form variables
 
-  # hä? kann die weg?
-  my $steuernummer_new = '';
-
   # Variablen für das Template zur Verfügung stellen
   my $template_ref = {
      select_tax_office               => $select_tax_office,
-     checked_accrual                 => $checked_accrual,
-     checked_cash                    => $checked_cash,
+     method_local                    => $method_local,
      checked_monthly                 => $checked_monthly,
      checked_quarterly               => $checked_quarterly,
      checked_dauerfristverlaengerung => $checked_dauerfristverlaengerung,
@@ -992,99 +715,87 @@ sub config_step2 {
 
   $::auth->assert('advance_turnover_tax_return');
 
+  setup_ustva_config_step2_action_bar();
+
   $form->header();
 
-  my $elsterland         = '';
-  my $elster_amt         = '';
-  my $elsterFFFF         = '';
-  my $elstersteuernummer = '';
+  my $fa_land_nr         = '';
+  my $fa_bufa_nr         = '';
 
   my $ustva = USTVA->new();
-  $ustva->get_config($::lx_office_conf{paths}{userspath}, 'finanzamt.ini')
-    if ($form->{saved} eq $locale->text('saved'));
+  $ustva->get_config() if ($form->{saved} eq $locale->text('saved'));
+  my $coa = $::form->{coa};
+  $form->{"COA_$coa"}  = '1';
+  $form->{COA_Germany} = '1' if ($coa =~ m/^germany/i);
+  $ustva->get_finanzamt();
+
 
   # Auf Übergabefehler checken
   USTVA::info(  $locale->text('Missing Tax Authoritys Preferences') . "\n"
               . $locale->text('USTVA-Hint: Tax Authoritys'))
-    if (   $form->{elsterFFFF_new} eq 'Auswahl'
-        || $form->{elsterland_new} eq 'Auswahl');
+    if (   $form->{fa_bufa_nr_new} eq 'Auswahl'
+        || $form->{fa_land_nr_new} eq 'Auswahl');
   USTVA::info(  $locale->text('Missing Method!') . "\n"
               . $locale->text('USTVA-Hint: Method'))
-    if ($form->{method} eq '');
+    if ($form->{accounting_method} eq '');
 
-  # Klären, ob Variablen bereits befüllt sind UND ob veräderungen auf
+  # Klären, ob Variablen bereits befüllt sind UND ob veränderungen auf
   # der vorherigen Maske stattfanden: $change = 1(in der edit sub,
   # mittels get_config)
 
-  my $change = $form->{elsterland} eq $form->{elsterland_new}
-    && $form->{elsterFFFF} eq $form->{elsterFFFF_new} ? '0' : '1';
+#  $::lxdebug->message(LXDebug->DEBUG2,"land old=".$form->{fa_land_nr}." new=".$form->{fa_land_nr_new});
+#  $::lxdebug->message(LXDebug->DEBUG2,"bufa old=".$form->{fa_bufa_nr}." new=".$form->{fa_bufa_nr_new});
+  my $change = $form->{fa_land_nr} eq $form->{fa_land_nr_new}
+    && $form->{fa_bufa_nr} eq $form->{fa_bufa_nr_new} ? '0' : '1';
   $change = '0' if ($form->{saved} eq $locale->text('saved'));
-  my $elster_init = $ustva->query_finanzamt(\%myconfig, $form);
 
-  my %elster_init = %$elster_init;
 
   if ($change eq '1') {
 
     # Daten ändern
-    $elsterland           = $form->{elsterland_new};
-    $elsterFFFF           = $form->{elsterFFFF_new};
-    $form->{elsterland}   = $elsterland;
-    $form->{elsterFFFF}   = $elsterFFFF;
-    $form->{steuernummer} = '';
+    $fa_land_nr           = $form->{fa_land_nr_new};
+    $fa_bufa_nr           = $form->{fa_bufa_nr_new};
+    $form->{fa_land_nr}   = $fa_land_nr;
+    $form->{fa_bufa_nr}   = $fa_bufa_nr;
+    $form->{taxnumber} = '';
 
     create_steuernummer();
 
     # rebuild elster_amt
-    my $amt = $elster_init{$elsterFFFF};
-
-    # load the predefined hash data into the FA_* Vars
-    my @variables = qw(FA_Name FA_Strasse FA_PLZ FA_Ort
-      FA_Telefon FA_Fax FA_PLZ_Grosskunden FA_PLZ_Postfach
-      FA_Postfach
-      FA_BLZ_1 FA_Kontonummer_1 FA_Bankbezeichnung_1
-      FA_BLZ_2 FA_Kontonummer_2 FA_Bankbezeichnung_oertlich
-      FA_Oeffnungszeiten FA_Email FA_Internet);
-
-    for (my $i = 0; $i <= 20; $i++) {
-      $form->{ $variables[$i] } =
-        $elster_init->{$elsterland}->{$elsterFFFF}->[$i];
-    }
+    $ustva->get_finanzamt();
 
   } else {
 
-    $elsterland = $form->{elsterland};
-    $elsterFFFF = $form->{elsterFFFF};
+    $fa_land_nr = $form->{fa_land_nr};
+    $fa_bufa_nr = $form->{fa_bufa_nr};
 
   }
-  my $stnr = $form->{steuernummer};
+#  $::lxdebug->message(LXDebug->DEBUG2, "form stnr=".$form->{taxnumber}." fa_bufa_nr=".$fa_bufa_nr.
+#                      " pattern=".$form->{elster_pattern}." fa_land_nr=".$fa_land_nr);
+  my $stnr = $form->{taxnumber};
   $stnr =~ s/\D+//g;
-  my $patterncount   = $form->{patterncount};
-  my $elster_pattern = $form->{elster_pattern};
-  my $delimiter      = $form->{delimiter};
-  my $steuernummer   = $stnr eq '' ? $form->{steuernummer} : '';
+  my $taxnumber      = $stnr eq '' ? $form->{taxnumber} : '';
 
-  $form->{FA_Oeffnungszeiten} =~ s/\\\\n/\n/g;
+  $form->{fa_oeffnungszeiten} =~ s/\\\\n/\n/g;
 
 
   $ustva->get_coa($form); # fetches coa and modifies some form variables
 
   my $input_steuernummer = $ustva->steuernummer_input(
-                             $form->{elsterland},
-                             $form->{elsterFFFF},
-                             $form->{steuernummer}
+                             $fa_land_nr,
+                             $fa_bufa_nr,
+                             $form->{taxnumber}
   );
 
-  $::lxdebug->message(LXDebug->DEBUG1, qq|$input_steuernummer|);
+#  $::lxdebug->message(LXDebug->DEBUG2, qq|$input_steuernummer|);
 
 
   my $_hidden_variables_ref;
 
   my %_hidden_local_variables = (
-      'elsterland'          => $elsterland,
-      'elsterFFFF'          => $elsterFFFF,
-      'warnung'             => 0,
-      'elstersteuernummer'  => $elstersteuernummer,
-      'steuernummer'        => $stnr,
+      'fa_land_nr'          => $fa_land_nr,
+      'fa_bufa_nr'          => $fa_bufa_nr,
+      'taxnumber'           => $stnr,
       'lastsub'             => 'config_step1',
       'nextsub'             => 'save',
 
@@ -1096,13 +807,12 @@ sub config_step2 {
   }
 
   my @_hidden_form_variables = qw(
-    FA_steuerberater_name   FA_steuerberater_street
-    FA_steuerberater_city   FA_steuerberater_tel
-    FA_voranmeld            method
-    FA_dauerfrist           FA_71
-    elster
-    type                    elster_init
-    saved                   callback
+    fa_dauerfrist fa_steuerberater_city fa_steuerberater_name
+    fa_steuerberater_street fa_steuerberater_tel
+    fa_voranmeld fa_dauerfrist
+    accounting_method
+    type
+    saved
   );
 
   foreach my $variable (@_hidden_form_variables) {
@@ -1113,7 +823,7 @@ sub config_step2 {
   my $template_ref = {
      input_steuernummer              => $input_steuernummer,
      readonly                        => '', #q|disabled="disabled"|,
-     callback                        => $form->{callback},
+     COA_Germany                     => $form->{COA_Germany},
      hidden_variables                => $_hidden_variables_ref,
   };
 
@@ -1131,34 +841,31 @@ sub create_steuernummer {
 
   my $part           = $::form->{part};
   my $patterncount   = $::form->{patterncount};
-  my $delimiter      = $::form->{delimiter};
+  my $delimiter      = $::form->{delimiter1};
   my $elster_pattern = $::form->{elster_pattern};
 
-  # rebuild steuernummer and elstersteuernummer
-  # es gibt eine gespeicherte steuernummer $form->{steuernummer}
+  # rebuild taxnumber
+  # es gibt eine gespeicherte steuernummer $form->{taxnumber}
   # und die parts und delimiter
 
   my $h = 0;
   my $i = 0;
 
-  my $steuernummer_new       = $part;
-  my $elstersteuernummer_new = $::form->{elster_FFFF};
-  $elstersteuernummer_new .= '0';
+  my $taxnumber_new       = $part;
 
   for ($h = 1; $h < $patterncount; $h++) {
-    $steuernummer_new .= qq|$delimiter|;
+    $delimiter = $::form->{delimiter2} if $h > 1;
+    $taxnumber_new .= qq|$delimiter|;
     for (my $i = 1; $i <= length($elster_pattern); $i++) {
-      $steuernummer_new       .= $::form->{"part_$h\_$i"};
-      $elstersteuernummer_new .= $::form->{"part_$h\_$i"};
+      $taxnumber_new       .= $::form->{"part_$h\_$i"};
     }
   }
-  if ($::form->{steuernummer} ne $steuernummer_new) {
-    $::form->{steuernummer}       = $steuernummer_new;
-    $::form->{elstersteuernummer} = $elstersteuernummer_new;
-    $::form->{steuernummer_new}   = $steuernummer_new;
+#  $::lxdebug->message(LXDebug->DEBUG2, "oldstnr=".$::form->{taxnumber}." newstnr=".$taxnumber_new);
+  if ($::form->{taxnumber} ne $taxnumber_new) {
+    $::form->{taxnumber}       = $taxnumber_new;
+    $::form->{taxnumber_new}   = $taxnumber_new;
   } else {
-    $::form->{steuernummer_new}       = '';
-    $::form->{elstersteuernummer_new} = '';
+    $::form->{taxnumber_new}       = '';
   }
   $::lxdebug->leave_sub();
 }
@@ -1168,56 +875,23 @@ sub save {
 
   $::auth->assert('advance_turnover_tax_return');
 
-  my $filename = "$::myconfig{login}_$::form->{filename}";
-  $filename =~ s|.*/||;
-
   #zuerst die steuernummer aus den part, parts_X_Y und delimiter herstellen
   create_steuernummer();
 
   # Textboxen formatieren: Linebreaks entfernen
   #
-  $::form->{FA_Oeffnungszeiten} =~ s/\r\n/\\n/g;
+  $::form->{fa_oeffnungszeiten} =~ s/\r\n/\\n/g;
 
   #URL mit http:// davor?
-  $::form->{FA_Internet} =~ s/^http:\/\///;
-  $::form->{FA_Internet} = 'http://' . $::form->{FA_Internet};
-
-  my @config = qw(
-    elster              elsterland            elstersteuernummer  steuernummer
-    elsteramt           elsterFFFF            FA_Name             FA_Strasse
-    FA_PLZ              FA_Ort                FA_Telefon          FA_Fax
-    FA_PLZ_Grosskunden  FA_PLZ_Postfach       FA_Postfach         FA_BLZ_1
-    FA_Kontonummer_1    FA_Bankbezeichnung_1  FA_BLZ_2            FA_Kontonummer_2
-    FA_Bankbezeichnung_oertlich FA_Oeffnungszeiten
-    FA_Email FA_Internet FA_voranmeld method FA_steuerberater_name
-    FA_steuerberater_street FA_steuerberater_city FA_steuerberater_tel
-    FA_71 FA_dauerfrist);
-
-  # Hier kommt dann die Plausibilitätsprüfung der ELSTERSteuernummer
-  if ($::form->{elstersteuernummer} ne '000000000') {
+  $::form->{fa_internet} =~ s/^http:\/\///;
+  $::form->{fa_internet} = 'http://' . $::form->{fa_internet};
 
-    $::form->{elster} = '1';
+  # Hier kommt dann die Plausibilitätsprüfung der ELSTERSteuernummer TODO ??
+  if (1) {
+    my $ustva = USTVA->new();
+    $ustva->save_config();
 
-    open my $ustvaconfig, ">", "$::lx_office_conf{paths}{userspath}/$filename" or $::form->error("$filename : $!");
-
-    # create the config file
-    print {$ustvaconfig} qq|# Configuration file for USTVA\n\n|;
-    my $key = '';
-    foreach $key (sort @config) {
-      $::form->{$key} =~ s/\\/\\\\/g;
-      # strip M
-      $::form->{$key} =~ s/\r\n/\n/g;
-
-      print {$ustvaconfig} qq|$key=|;
-      if ($::form->{$key} ne 'Y') {
-        print {$ustvaconfig} qq|$::form->{$key}\n|;
-      }
-      if ($::form->{$key} eq 'Y') {
-        print {$ustvaconfig} qq|checked \n|;
-      }
-    }
-    print {$ustvaconfig} qq|\n\n|;
-    close $ustvaconfig;
+    #$::form->{elster} = '1';
     $::form->{saved} = $::locale->text('saved');
 
   } else {
@@ -1243,3 +917,49 @@ sub back {
   call_sub($::form->{"lastsub"});
   $::lxdebug->leave_sub();
 }
+
+sub setup_ustva_report_action_bar {
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Show'),
+        submit    => [ '#form_do', { action => 'generate_ustva' } ],
+        accesskey => 'enter',
+      ],
+      action => [
+        t8('Geierlein'),
+        call     => [ 'sendGeierlein' ],
+        disabled => !length($::lx_office_conf{paths}{geierlein_path} // '') ? t8('The Geierlein path has not been set in the configuration.') : undef,
+        tooltip  => t8('Transfer data to Geierlein ELSTER application'),
+      ],
+    );
+  }
+}
+
+sub setup_ustva_config_step1_action_bar {
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Continue'),
+        submit    => [ '#form', { action => 'config_step2' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_ustva_config_step2_action_bar {
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Save'),
+        submit    => [ '#form', { action => 'save' } ],
+        accesskey => 'enter',
+      ],
+      action => [
+        t8('Back'),
+        call => [ 'kivi.history_back' ],
+      ],
+    );
+  }
+}
index a6476d9..8a0dc46 100644 (file)
@@ -24,7 +24,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # Sales report
@@ -35,40 +36,38 @@ use POSIX qw(strftime);
 use List::Util qw(sum first);
 
 use SL::AM;
+use SL::DB::Employee;
 use SL::VK;
 use SL::IS;
 use SL::ReportGenerator;
 use Data::Dumper;
 
-require "bin/mozilla/arap.pl";
 require "bin/mozilla/common.pl";
-require "bin/mozilla/drafts.pl";
 require "bin/mozilla/reportgenerator.pl";
 
 use strict;
 
 sub search_invoice {
   $main::lxdebug->enter_sub();
-  $main::auth->assert('general_ledger | invoice_edit');
+  $main::auth->assert('ar_transactions | ap_transactions | invoice_edit');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
-  my ($customer, $department);
+  my ($customer);
 
-  # setup customer selection
-  $form->all_vc(\%myconfig, "customer", "AR");
+  $::request->layout->add_javascripts("autocomplete_project.js");
 
   $form->{title}    = $locale->text('Sales Report');
 
-  $form->get_lists("projects"        => { "key" => "ALL_PROJECTS", "all" => 1 },
-                   "departments"     => "ALL_DEPARTMENTS",
+  $form->get_lists("departments"     => "ALL_DEPARTMENTS",
                    "business_types"  => "ALL_BUSINESS_TYPES",
                    "salesmen"        => "ALL_SALESMEN",
-                   'employees'       => 'ALL_EMPLOYEES',
-                   'partsgroup'      => 'ALL_PARTSGROUPS',
-                   "customers"       => "ALL_VC");
+                   'partsgroup'      => 'ALL_PARTSGROUPS');
+
+  $form->{ALL_EMPLOYEES} = SL::DB::Manager::Employee->get_all_sorted;
+
   $form->{CUSTOM_VARIABLES_IC}                  = CVar->get_configs('module' => 'IC');
   ($form->{CUSTOM_VARIABLES_FILTER_CODE_IC},
    $form->{CUSTOM_VARIABLES_INCLUSION_CODE_IC}) = CVar->render_search_options('variables'      => $form->{CUSTOM_VARIABLES_IC},
@@ -80,10 +79,6 @@ sub search_invoice {
    $form->{CUSTOM_VARIABLES_INCLUSION_CODE_CT}) = CVar->render_search_options('variables'      => $form->{CUSTOM_VARIABLES_CT},
                                                                            'include_prefix' => 'l_',
                                                                            'include_value'  => 'Y');
-  $form->{vc_keys}   = sub { "$_[0]->{name}--$_[0]->{id}" };
-  $form->{employee_labels} = sub { $_[0]->{"name"} || $_[0]->{"login"} };
-  $form->{salesman_labels} = $form->{employee_labels};
-
   $form->header;
   print $form->parse_html_template('vk/search_invoice', { %myconfig });
 
@@ -93,7 +88,7 @@ sub search_invoice {
 sub invoice_transactions {
   $main::lxdebug->enter_sub();
 
-  $main::auth->assert('general_ledger | invoice_edit');
+  $main::auth->assert('ar_transactions | ap_transactions | invoice_edit');
 
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
@@ -142,7 +137,7 @@ sub invoice_transactions {
   $form->{title} = $locale->text('Sales Report');
 
   @columns =
-    qw(description invnumber transdate customernumber customername partnumber partsgroup country business transdate qty parts_unit weight sellprice sellprice_total discount lastcost lastcost_total marge_total marge_percent employee salesman);
+    qw(description invnumber transdate shipvia customernumber customername partnumber partsgroup country business transdate qty parts_unit weight sellprice sellprice_total discount lastcost lastcost_total marge_total marge_percent employee salesman);
 
   my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs_ic }, @{ $cvar_configs_ct };
   my @searchable_custom_variables  = grep { $_->{searchable} }  @{ $cvar_configs_ic }, @{ $cvar_configs_ct };
@@ -154,7 +149,7 @@ sub invoice_transactions {
   # pass hidden variables for pdf/csv export
   # first with l_ to determine which columns to show
   # then with the options for headings (such as transdatefrom, partnumber, ...)
-  my @hidden_variables  = (qw(l_headers_mainsort l_headers_subsort l_subtotal_mainsort l_subtotal_subsort l_total l_parts l_customername l_customernumber transdatefrom transdateto decimalplaces customer customer_id department partnumber partsgroup country business description project_id customernumber salesman employee salesman_id employee_id business_id partsgroup_id mainsort subsort),
+  my @hidden_variables  = (qw(l_headers_mainsort l_headers_subsort l_subtotal_mainsort l_subtotal_subsort l_total l_parts l_customername l_customernumber transdatefrom transdateto decimalplaces customer customer_id department_id partnumber partsgroup country business description project_id customernumber salesman employee salesman_id employee_id business_id partsgroup_id mainsort subsort),
       "$form->{db}number",
       map({ "cvar_$_->{name}" } @searchable_custom_variables),
       map { "l_$_" } @columns
@@ -169,13 +164,14 @@ sub invoice_transactions {
   my %column_defs = (
     'description'             => { 'text' => $locale->text('Description'), },
     'partnumber'              => { 'text' => $locale->text('Part Number'), },
-    'partsgroup'              => { 'text' => $locale->text('Group'), },
+    'partsgroup'              => { 'text' => $locale->text('Partsgroup'), },
     'country'                 => { 'text' => $locale->text('Country'), },
     'business'                => { 'text' => $locale->text('Customer type'), },
     'employee'                => { 'text' => $locale->text('Employee'), },
     'salesman'                => { 'text' => $locale->text('Salesperson'), },
     'invnumber'               => { 'text' => $locale->text('Invoice Number'), },
     'transdate'               => { 'text' => $locale->text('Invoice Date'), },
+    'shipvia'                 => { 'text' => $locale->text('Ship via'), },
     'qty'                     => { 'text' => $locale->text('Quantity'), },
     'parts_unit'              => { 'text' => $locale->text('Base unit'), },
     'weight'                  => { 'text' => $locale->text('Weight'), },
@@ -211,11 +207,12 @@ sub invoice_transactions {
   push @options, $locale->text('Customer')                . " : $form->{customer}"                                                          if $form->{customer};
   push @options, $locale->text('Customer Number')         . " : $form->{customernumber}"                                                    if $form->{customernumber};
   # TODO: only customer id is passed
-  push @options, $locale->text('Department')              . " : " . (split /--/, $form->{department})[0]                                    if $form->{department};
+  push @options, $locale->text('Department')              . " : " . SL::DB::Department->new(id => $form->{department_id})->load->description if $form->{department_id};
   push @options, $locale->text('Invoice Number')          . " : $form->{invnumber}"                                                         if $form->{invnumber};
   push @options, $locale->text('Invoice Date')            . " : $form->{invdate}"                                                           if $form->{invdate};
+  push @options, $locale->text('Ship via')                . " : $form->{shipvia}"                                                         if $form->{shipvia};
   push @options, $locale->text('Part Number')             . " : $form->{partnumber}"                                                        if $form->{partnumber};
-  push @options, $locale->text('Group')                   . " : " . SL::DB::PartsGroup->new(id => $form->{partsgroup_id})->load->partsgroup if $form->{partsgroup_id};
+  push @options, $locale->text('Partsgroup')              . " : " . SL::DB::PartsGroup->new(id => $form->{partsgroup_id})->load->partsgroup if $form->{partsgroup_id};
   push @options, $locale->text('Country')                 . " : $form->{country}"                                                           if $form->{country};
   push @options, $locale->text('Employee')                . ' : ' . SL::DB::Employee->new(id => $form->{employee_id})->load->name           if $form->{employee_id};
   push @options, $locale->text('Salesman')                . ' : ' . SL::DB::Employee->new(id => $form->{salesman_id})->load->name           if $form->{salesman_id};
@@ -297,7 +294,7 @@ sub invoice_transactions {
 
     # The sellprice total can be calculated from sellprice or fxsellprice (the
     # value that was actually entered in the sellprice field and is always
-    # stored seperately).  However, for fxsellprice this method only works when
+    # stored separately).  However, for fxsellprice this method only works when
     # the tax is not included, because otherwise fxsellprice includes the tax
     # and there is no simple way to extract the tax rate of the article from
     # the big query.
index fbc645e..3cb1243 100644 (file)
@@ -23,7 +23,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #
 #######################################################################
 #
@@ -38,11 +39,17 @@ use SL::Form;
 use SL::User;
 
 use SL::AM;
+use SL::CVar;
 use SL::CT;
+use SL::Helper::Flash qw(flash_later);
 use SL::IC;
 use SL::WH;
 use SL::OE;
+use SL::Helper::Inventory qw(produce_assembly);
+use SL::Locale::String qw(t8);
 use SL::ReportGenerator;
+use SL::Presenter::Tag qw(checkbox_tag);
+use SL::Presenter::Part;
 
 use SL::DB::Part;
 
@@ -67,6 +74,8 @@ use strict;
 #  $locale->text('used')
 #  $locale->text('return_material')
 #  $locale->text('release_material')
+#  $locale->text('assembled')
+#  $locale->text('stocktaking')
 
 # --------------------------------------------------------------------
 # Transfer
@@ -106,20 +115,17 @@ sub transfer_warehouse_selection {
   my $content;
 
   if ($form->{trans_type} eq 'removal') {
-    $form->{nextsub} = "removal_parts_selection";
+    setup_wh_transfer_warehouse_selection_action_bar("removal_parts_selection");
     $form->{title}   = $locale->text('Removal from Warehouse');
     $content         = $form->parse_html_template('wh/warehouse_selection');
 
-  } elsif ($form->{trans_type} eq 'stock') {
-    $form->{title} = $locale->text('Stock');
-    $content       = $form->parse_html_template('wh/warehouse_selection_stock');
-
   } elsif (!$form->{trans_type} || ($form->{trans_type} eq 'transfer')) {
-    $form->{nextsub} = "transfer_parts_selection";
+    setup_wh_transfer_warehouse_selection_action_bar("transfer_parts_selection");
     $form->{title}   = $locale->text('Transfer');
     $content         = $form->parse_html_template('wh/warehouse_selection');
 
   } elsif ($form->{trans_type} eq 'assembly') {
+    setup_wh_transfer_warehouse_selection_assembly_action_bar();
     $form->{title} = $locale->text('Produce Assembly');
     $content       = $form->parse_html_template('wh/warehouse_selection_assembly');
   }
@@ -141,6 +147,8 @@ sub transfer_parts_selection {
 
   transfer_or_removal_prepare_contents('direction' => 'transfer');
 
+  setup_wh_transfer_parts_action_bar();
+
   $form->{title} = $locale->text('Transfer');
   $form->header();
   print $form->parse_html_template("wh/transfer_parts_selection");
@@ -180,9 +188,8 @@ sub transfer_or_removal_prepare_contents {
                                            "bin_id"       => $form->{bin_id},
                                            "chargenumber" => $form->{chargenumber},
                                            "bestbefore"   => $form->{bestbefore},
-                                           "partnumber"   => $form->{partnumber},
-                                           "ean"          => $form->{ean},
-                                           "description"  => $form->{description});
+                                           "partsid"      => $form->{part_id},
+                                           "ean"          => $form->{ean});
 
   if (0 == scalar(@contents)) {
     $form->show_generic_error($locale->text("The selected warehouse is empty, or no stocked items where found that match the filter settings."));
@@ -191,9 +198,8 @@ sub transfer_or_removal_prepare_contents {
   my $all_units = AM->retrieve_units(\%myconfig, $form);
 
   foreach (@contents) {
-    $_->{qty} = $form->format_amount_units('amount'     => $_->{qty},
-                                           'part_unit'  => $_->{partunit},
-                                           'conv_units' => 'convertible');
+    $_->{qty} = $form->format_amount(\%myconfig, $_->{qty}) . ' ' . $_->{partunit};
+
     my $this_unit = $_->{partunit};
 
     if ($all_units->{$_->{partunit}} && ($all_units->{g}->{base_unit} eq $all_units->{$_->{partunit}}->{base_unit})) {
@@ -289,7 +295,7 @@ sub transfer_parts {
 
   if (!scalar @transfers) {
     $form->show_generic_information($locale->text('Nothing has been selected for transfer.'));
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
 
   WH->transfer(@transfers);
@@ -306,46 +312,6 @@ sub transfer_parts {
 # Transfer: stock
 # --------------------------------------------------------------------
 
-sub transfer_stock_update_part {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  $form->{trans_type} = 'stock';
-  $form->{qty}        = $form->parse_amount(\%myconfig, $form->{qty});
-
-  if (!$form->{partnumber} && !$form->{description} && !$form->{ean}) {
-    delete @{$form}{qw(parts_id partunit ean)};
-    transfer_warehouse_selection();
-
-  } elsif (($form->{partnumber} && ($form->{partnumber} ne $form->{old_partnumber})) || $form->{description} || $form->{ean}) {
-
-#    $form->{no_services}   = 1; # services may now be transfered. fix for Bug 1383.
-    $form->{no_assemblies} = 0; # assemblies duerfen eingelagert werden (z.B. bei retouren)
-
-    my $parts = Common->retrieve_parts(\%myconfig, $form, 'description', 1);
-
-    if (!scalar @{ $parts }) {
-      new_item(action => "transfer_stock_update_part");
-    } elsif (scalar @{ $parts } == 1) {
-      @{$form}{qw(parts_id partnumber description ean warehouse_id bin_id)} = @{$parts->[0]}{qw(id partnumber description ean warehouse_id bin_id)};
-      transfer_stock_get_partunit();
-      transfer_warehouse_selection();
-
-    } else {
-      select_part('transfer_stock_part_selected', @{ $parts });
-    }
-
-  } else {
-    transfer_stock_get_partunit();
-    transfer_warehouse_selection();
-  }
-
-  $main::lxdebug->leave_sub();
-}
-
 # --------------------------------------------------------------------
 # Transfer: assemblies
 # Dies ist die Auswahlmaske für ein assembly.
@@ -355,8 +321,6 @@ sub transfer_stock_update_part {
 # --------------------------------------------------------------------
 
 sub transfer_assembly_update_part {
-  $main::lxdebug->enter_sub();
-
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
@@ -364,30 +328,18 @@ sub transfer_assembly_update_part {
   $form->{trans_type} = 'assembly';
   $form->{qty}        = $form->parse_amount(\%myconfig, $form->{qty});
 
-  if (!$form->{partnumber} && !$form->{description}) {
-    delete @{$form}{qw(parts_id partunit)};
+  if (!$form->{parts_id}) {
+    delete $form->{partunit};
     transfer_warehouse_selection();
+    return;
 
-  } elsif (($form->{partnumber} && ($form->{partnumber} ne $form->{old_partnumber})) || $form->{description}) {
-    $form->{assemblies} = 1;
-    $form->{no_assemblies} = 0;
-    my $parts = Common->retrieve_parts(\%myconfig, $form, 'description', 1);
-    if (scalar @{ $parts } == 1) {
-      @{$form}{qw(parts_id partnumber description)} = @{$parts->[0]}{qw(id partnumber description)};
-      transfer_stock_get_partunit();
-      transfer_warehouse_selection();
-    } else {
-      select_part('transfer_stock_part_selected', @{ $parts });
-    }
-
-  } else {
-    transfer_stock_get_partunit();
-    transfer_warehouse_selection();
   }
 
-# hier die oben benannte idee
-#    my $maxcreate = Common->check_assembly_max_create(assembly_id => $form->{parts_id}, dbh => $my_dbh);
-  $main::lxdebug->leave_sub();
+  my $part = SL::DB::Part->new(id => $::form->{parts_id})->load;
+  @{$form}{qw(parts_id partnumber description)} = ($part->id, $part->partnumber, $part->description);
+
+  transfer_stock_get_partunit();
+  transfer_warehouse_selection();
 }
 
 sub transfer_stock_part_selected {
@@ -418,13 +370,6 @@ sub transfer_stock_get_partunit {
   $main::lxdebug->leave_sub();
 }
 
-# vorüberlegung jb 22.2.2009
-# wir benötigen für diese funktion, die anzahl die vom erzeugnis hergestellt werden soll. vielleicht direkt per js fehleingaben verhindern?
-# ferner dann nochmal mit check_asssembly_max_create gegenprüfen und dann transaktionssicher wegbuchen.
-# wir brauchen eine hilfsfunktion, die nee. brauchen wir nicht. der algorithmus läuft genau wie bei check max_create, nur dass hier auch eine lagerbewegung (verbraucht) stattfindet
-# Manko ist derzeit noch, dass unterschiedliche Lagerplätze, bzw. das Quelllager an sich nicht ausgewählt werden können.
-# Laut Absprache in KW11 09 übernimmt mb hier den rest im April ... jb 18.3.09
-
 sub create_assembly {
   $main::lxdebug->enter_sub();
 
@@ -434,46 +379,33 @@ sub create_assembly {
 
   $form->{qty} = $form->parse_amount(\%myconfig, $form->{qty});
   if ($form->{qty} <= 0) {
-    $form->show_generic_error($locale->text('Invalid quantity.'), 'back_button' => 1);
+    $form->show_generic_error($locale->text('Invalid quantity.'));
   }
-  # TODO Es wäre schön, hier schon die maximale Anzahl der zu fertigenden Erzeugnisse zu haben
-  #else { if ($form->{qty} > $maxcreate) { #s.o.
-  #     $form->show_generic_error($locale->text('Can not create that quantity with current stock'), 'back_button' => 1);
-  #     $form->show_generic_error('Maximale Stückzahl' . $maxcreate , 'back_button' => 1);
-  #   }
-  #  }
-
   if (!$form->{warehouse_id} || !$form->{bin_id}) {
     $form->error($locale->text('The warehouse or the bin is missing.'));
   }
+  # need part and bin object
+  my ($bin, $assembly);
+  $assembly = SL::DB::Manager::Part->find_by(id => $form->{parts_id}, part_type => 'assembly');
+  $form->show_generic_error($locale->text('Invalid assembly')) unless ref $assembly eq 'SL::DB::Part';
+
+  $bin = SL::DB::Manager::Bin->find_by(id => $form->{bin_id});
+  $form->show_generic_error($locale->text('Invalid bin')) unless ref $bin eq 'SL::DB::Bin';
 
   if (!$::instance_conf->get_show_bestbefore) {
-      $form->{bestbefore} = '';
+    $form->{bestbefore} = '';
   }
 
-  # WIESO war das nicht vorher schon ein %HASH?? ein hash ist ein hash! das hat mich mehr als eine Stunde gekostet herauszufinden. grr. jb 3.3.2009
-  # Anm. jb 18.3. vielleicht auch nur meine unwissenheit in perl-datenstrukturen
-  my %TRANSFER = (
-    'transfer_type'    => 'assembly',
-    'login'            => $::myconfig{login},
-    'dst_warehouse_id' => $form->{warehouse_id},
-    'dst_bin_id'       => $form->{bin_id},
-    'chargenumber'     => $form->{chargenumber},
-    'bestbefore'       => $form->{bestbefore},
-    'assembly_id'      => $form->{parts_id},
-    'qty'              => $form->{qty},
-    'unit'             => $form->{unit},
-    'comment'          => $form->{comment}
+  produce_assembly(
+              part           => $assembly,               # target assembly
+              qty            => $form->{qty},            # qty
+              auto_allocate  => 1,
+              bin            => $bin,                    # needed unless a global standard target is configured
+              chargenumber   => $form->{chargenumber},   # optional
+              bestbefore     => $form->{bestbefore},
+              comment        => $form->{comment},        # optional
   );
 
-  my $ret = WH->transfer_assembly (%TRANSFER);
-  # Frage: Ich pack in den return-wert auch gleich die Fehlermeldung. Irgendwelche Nummern als Fehlerkonstanten definieren find ich auch nicht besonders schick...
-  # Ideen? jb 18.3.09
-  if ($ret ne "1"){
-    # Die locale-Funktion kann keine Double-Quotes escapen, deswegen hier erstmal so (ein wahrscheinlich immerwährender Hotfix) s.a. Frage davor jb 25.4.09
-    $form->show_generic_error($ret, 'back_button' => 1);
-  }
-
   delete @{$form}{qw(parts_id partnumber description qty unit chargenumber bestbefore comment)};
 
   $form->{saved_message} = $locale->text('The assembly has been created.');
@@ -484,47 +416,6 @@ sub create_assembly {
   $main::lxdebug->leave_sub();
 }
 
-sub transfer_stock {
-  $main::lxdebug->enter_sub();
-
-  my $form     = $main::form;
-  my %myconfig = %main::myconfig;
-  my $locale   = $main::locale;
-
-  $form->{qty} = $form->parse_amount(\%myconfig, $form->{qty});
-
-  if ($form->{qty} <= 0) {
-    $form->show_generic_error($locale->text('Invalid quantity.'), 'back_button' => 1);
-  }
-
-  if (!$form->{warehouse_id} || !$form->{bin_id}) {
-    $form->error($locale->text('The warehouse or the bin is missing.'));
-  }
-
-  my $transfer = {
-    'transfer_type'    => 'stock',
-    'dst_warehouse_id' => $form->{warehouse_id},
-    'dst_bin_id'       => $form->{bin_id},
-    'chargenumber'     => $form->{chargenumber},
-    'bestbefore'       => $form->{bestbefore},
-    'parts_id'         => $form->{parts_id},
-    'qty'              => $form->{qty},
-    'unit'             => $form->{unit},
-    'comment'          => $form->{comment},
-  };
-
-  WH->transfer($transfer);
-
-  delete @{$form}{qw(parts_id partnumber description qty unit chargenumber bestbefore comment ean)};
-
-  $form->{saved_message} = $locale->text('The parts have been stocked.');
-  $form->{trans_type}    = 'stock';
-
-  transfer_warehouse_selection();
-
-  $main::lxdebug->leave_sub();
-}
-
 # --------------------------------------------------------------------
 # Transfer: removal
 # --------------------------------------------------------------------
@@ -540,6 +431,8 @@ sub removal_parts_selection {
 
   transfer_or_removal_prepare_contents('direction' => 'out');
 
+  setup_wh_removal_parts_selection_action_bar();
+
   $form->{title} = $locale->text('Removal');
   $form->header();
   print $form->parse_html_template("wh/removal_parts_selection");
@@ -623,7 +516,7 @@ sub remove_parts {
 
   if (!scalar @transfers) {
     $form->show_generic_information($locale->text('Nothing has been selected for removal.'));
-    ::end_of_request();
+    $::dispatcher->end_request;
   }
 
   WH->transfer(@transfers);
@@ -636,6 +529,31 @@ sub remove_parts {
   $main::lxdebug->leave_sub();
 }
 
+sub disassemble_assembly {
+  $main::lxdebug->enter_sub();
+
+  $main::auth->assert('warehouse_management');
+
+  my $form = $main::form;
+
+  croak("No assembly ids") unless scalar @{ $form->{ids}} > 0;
+
+  # everything in one transaction
+  my $db = SL::DB::Inventory->new->db;
+  $db->with_transaction(sub {
+
+    foreach my $trans_id (@{ $::form->{ids}} )  {
+      SL::DB::Manager::Inventory->delete_all(where => [ trans_id => $trans_id ]);
+      flash_later('info', t8("Disassembly successful for trans_id #1",  $trans_id));
+    }
+
+    1;
+  }) || die t8('error while disassembling for trans_ids #1 : #2', $form->{ids})  . $db->error . "\n";
+
+  $main::lxdebug->leave_sub();
+  $form->redirect;
+}
+
 # --------------------------------------------------------------------
 # Journal
 # --------------------------------------------------------------------
@@ -655,6 +573,14 @@ sub journal {
 
   show_no_warehouses_error() if (!scalar @{ $form->{WAREHOUSES} });
 
+  my $cvar_configs                           = CVar->get_configs('module' => 'IC');
+  ($form->{CUSTOM_VARIABLES_FILTER_CODE},
+   $form->{CUSTOM_VARIABLES_INCLUSION_CODE}) = CVar->render_search_options('variables'      => $cvar_configs,
+                                                                           'include_prefix' => 'l_',
+                                                                           'include_value'  => 'Y');
+
+  setup_wh_journal_action_bar();
+
   $form->header();
   print $form->parse_html_template("wh/journal_filter", { "UNITS" => AM->unit_select_data(AM->retrieve_units(\%myconfig, $form)) });
 
@@ -670,14 +596,17 @@ sub generate_journal {
   my %myconfig = %main::myconfig;
   my $locale   = $main::locale;
 
+  setup_wh_journal_list_all_action_bar();
   $form->{title}   = $locale->text("WHJournal");
   $form->{sort}  ||= 'date';
 
+  $form->{report_generator_output_format} = 'HTML' if !$form->{report_generator_output_format};
+
   my %filter;
-  my @columns = qw(trans_id date warehouse_from bin_from warehouse_to bin_to partnumber partdescription chargenumber bestbefore trans_type comment qty employee oe_id projectnumber);
+  my @columns = qw(ids trans_id date warehouse_from bin_from warehouse_to bin_to partnumber type_and_classific partdescription chargenumber bestbefore trans_type comment qty unit partunit employee oe_id projectnumber);
 
   # filter stuff
-  map { $filter{$_} = $form->{$_} if ($form->{$_}) } qw(warehouse_id bin_id partnumber description chargenumber bestbefore);
+  map { $filter{$_} = $form->{$_} if ($form->{$_}) } qw(warehouse_id bin_id classification_id partnumber description chargenumber bestbefore transtype_id transtype_ids comment projectnumber);
 
   $filter{qty_op} = WH->convert_qty_op($form->{qty_op});
   if ($filter{qty_op}) {
@@ -689,12 +618,49 @@ sub generate_journal {
   }
   # /filter stuff
 
+  my $allrows        = !!($form->{report_generator_output_format} ne 'HTML') ;
+
+  # manual paginating
+  my $pages          = {};
+  my $page           = $::form->{page} || 1;
+  $pages->{per_page} = $::form->{per_page} || 15;
+  my $first_nr       = ($page - 1) * $pages->{per_page};
+  my $last_nr        = $first_nr + $pages->{per_page};
+
+  # no optimisation if qty op
+  if ( !$allrows && $form->{maxrows} && !$filter{qty_op}) {
+    $filter{limit}  = $pages->{per_page};
+    $filter{offset} = ($page - 1) * $pages->{per_page};
+    $first_nr       = 0;
+    $last_nr        = $pages->{per_page};
+  }
+
+  my $old_l_trans_id = $form->{l_trans_id};
+  my @contents  = WH->get_warehouse_journal(%filter);
+  $form->{l_trans_id} = $old_l_trans_id;
+
+  # get maxcount
+  if (!$form->{maxrows}) {
+    $form->{maxrows} = scalar @contents ;
+  }
+
   my $report = SL::ReportGenerator->new(\%myconfig, $form);
 
+  my $cvar_configs                 = CVar->get_configs('module' => 'IC');
+  my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs };
+  my @searchable_custom_variables  = grep { $_->{searchable} }  @{ $cvar_configs };
+  push @columns, map { "cvar_$_->{name}" } @includeable_custom_variables;
+
   my @hidden_variables = map { "l_${_}" } @columns;
-  push @hidden_variables, qw(warehouse_id bin_id partnumber description chargenumber bestbefore qty_op qty qty_unit fromdate todate);
+  push @hidden_variables, qw(warehouse_id bin_id partnumber description chargenumber bestbefore qty_op qty qty_unit unit partunit fromdate todate transtype_ids comment projectnumber);
+  push @hidden_variables, qw(classification_id);
+  push @hidden_variables, map({'cvar_'. $_->{name}}                                         @searchable_custom_variables);
+  push @hidden_variables, map({'cvar_'. $_->{name} .'_from'}  grep({$_->{type} eq  'date'}  @searchable_custom_variables));
+  push @hidden_variables, map({'cvar_'. $_->{name} .'_to'}    grep({$_->{type} eq  'date'}  @searchable_custom_variables));
+  push @hidden_variables, map({'cvar_'. $_->{name} .'_qtyop'} grep({$_->{type} eq 'number'} @searchable_custom_variables));
 
   my %column_defs = (
+    'ids'             => { raw_header_data => checkbox_tag("", id => "check_all", checkall  => "[data-checkall=1]") },
     'date'            => { 'text' => $locale->text('Date'), },
     'trans_id'        => { 'text' => $locale->text('Trans Id'), },
     'trans_type'      => { 'text' => $locale->text('Trans Type'), },
@@ -704,21 +670,41 @@ sub generate_journal {
     'bin_from'        => { 'text' => $locale->text('Bin From'), },
     'bin_to'          => { 'text' => $locale->text('Bin To'), },
     'partnumber'      => { 'text' => $locale->text('Part Number'), },
+    'type_and_classific'
+                      => { 'text' => $locale->text('Type'), },
     'partdescription' => { 'text' => $locale->text('Part Description'), },
     'chargenumber'    => { 'text' => $locale->text('Charge Number'), },
     'bestbefore'      => { 'text' => $locale->text('Best Before'), },
     'qty'             => { 'text' => $locale->text('Qty'), },
+    'unit'            => { 'text' => $locale->text('Part Unit'), },
+    'partunit'        => { 'text' => $locale->text('Unit'), },
     'employee'        => { 'text' => $locale->text('Employee'), },
     'projectnumber'   => { 'text' => $locale->text('Project Number'), },
     'oe_id'           => { 'text' => $locale->text('Document'), },
   );
 
+  my %column_defs_cvars = map { +"cvar_$_->{name}" => { 'text' => $_->{description} } } @includeable_custom_variables;
+  %column_defs          = (%column_defs, %column_defs_cvars);
+
+  if ($form->{transtype_ids} && 'ARRAY' eq ref $form->{transtype_ids}) {
+    for (my $i = 0; $i < scalar(@{ $form->{transtype_ids} }); $i++) {
+      delete $form->{transtype_ids}[$i] if $form->{transtype_ids}[$i] eq '';
+    }
+    $form->{transtype_ids} = join(",", @{ $form->{transtype_ids} });
+  }
+
   my $href = build_std_url('action=generate_journal', grep { $form->{$_} } @hidden_variables);
-  map { $column_defs{$_}->{link} = $href . "&sort=${_}&order=" . Q($_ eq $form->{sort} ? 1 - $form->{order} : $form->{order}) } @columns;
+  $href .= "&maxrows=".$form->{maxrows};
+
+  map { $column_defs{$_}->{link} = $href ."&page=".$page. "&sort=${_}&order=" . Q($_ eq $form->{sort} ? 1 - $form->{order} : $form->{order}) } grep {!/^cvar/} @columns;
 
   my %column_alignment = map { $_ => 'right' } qw(qty);
 
   map { $column_defs{$_}->{visible} = $form->{"l_${_}"} ? 1 : 0 } @columns;
+  $column_defs{partunit}->{visible} = 1;
+  $column_defs{type_and_classific}->{visible} = 1;
+  $column_defs{type_and_classific}->{link} ='';
+  $column_defs{ids}->{visible} = 1;
 
   $report->set_columns(%column_defs);
   $report->set_column_order(@columns);
@@ -734,7 +720,13 @@ sub generate_journal {
   $locale->set_numberformat_wo_thousands_separator(\%myconfig) if lc($report->{options}->{output_format}) eq 'csv';
 
   my $all_units = AM->retrieve_units(\%myconfig, $form);
-  my @contents  = WH->get_warehouse_journal(%filter);
+
+  CVar->add_custom_variables_to_report('module'         => 'IC',
+                                       'trans_id_field' => 'parts_id',
+                                       'configs'        => $cvar_configs,
+                                       'column_defs'    => \%column_defs,
+                                       'data'           => \@contents);
+
 
   my %doc_types = ( 'sales_quotation'         => { script => 'oe', title => $locale->text('Sales quotation') },
                     'sales_order'             => { script => 'oe', title => $locale->text('Sales Order') },
@@ -746,12 +738,14 @@ sub generate_journal {
                     'purchase_invoice'        => { script => 'ir', title => $locale->text('Purchase Invoice') },
                   );
 
+  my $idx       = 0;
+  my $undo_date  = DateTime->today->subtract(days => $::instance_conf->get_undo_transfer_interval);
   foreach my $entry (@contents) {
-    $entry->{qty}        = $form->format_amount_units('amount'     => $entry->{qty},
-                                                      'part_unit'  => $entry->{partunit},
-                                                      'conv_units' => 'convertible');
+    $entry->{type_and_classific} = SL::Presenter::Part::type_abbreviation($entry->{part_type}) .
+                                   SL::Presenter::Part::classification_abbreviation($entry->{classification_id});
+    $entry->{qty}        = $form->format_amount(\%myconfig, $entry->{qty});
+    $entry->{assembled} = $entry->{trans_type} eq 'assembled' ? 1 : '';
     $entry->{trans_type} = $locale->text($entry->{trans_type});
-
     my $row = { };
 
     foreach my $column (@columns) {
@@ -761,8 +755,13 @@ sub generate_journal {
       };
     }
 
+    if ($entry->{assembled}) {
+      my $insertdate = DateTime->from_kivitendo($entry->{shippingdate});
+      if (ref $undo_date eq 'DateTime' && ref $insertdate eq 'DateTime' && $insertdate > $undo_date) {
+        $row->{ids}->{raw_data} = checkbox_tag("ids[]", value => $entry->{trans_id}, "data-checkall" => 1);
+      }
+    }
     $row->{trans_type}->{raw_data} = $entry->{trans_type};
-
     if ($form->{l_oe_id}) {
       $row->{oe_id}->{data} = '';
       my $info              = $entry->{oe_id_info};
@@ -773,9 +772,25 @@ sub generate_journal {
       }
     }
 
-    $report->add_data($row);
+    if ( $allrows || ($idx >= $first_nr && $idx < $last_nr )) {
+      $report->add_data($row);
+    }
+    $idx++;
   }
 
+
+    $report->set_options(
+      raw_top_info_text     => $form->parse_html_template('wh/report_top'),
+      raw_bottom_info_text  => $form->parse_html_template('wh/report_bottom', { callback => $href }),
+    );
+  if ( ! $allrows ) {
+      $pages->{max}  = SL::DB::Helper::Paginated::ceil($form->{maxrows}, $pages->{per_page}) || 1;
+      $pages->{page} = $page < 1 ? 1: $page > $pages->{max} ? $pages->{max}: $page;
+      $pages->{common} = [ grep { $_->{visible} } @{ SL::DB::Helper::Paginated::make_common_pages($pages->{page}, $pages->{max}) } ];
+
+      $report->set_options('raw_bottom_info_text' =>  $form->parse_html_template('wh/report_bottom', { callback => $href }) . $form->parse_html_template('common/paginate',
+                                                            { 'pages' => $pages , 'base_url' => $href.'&sort='.$form->{sort}.'&order='.$form->{order}}) );
+  }
   $report->generate_with_headers();
 
   $main::lxdebug->leave_sub();
@@ -795,17 +810,27 @@ sub report {
   my $locale   = $main::locale;
 
   $form->get_lists('warehouses' => { 'key'    => 'WAREHOUSES',
-                                     'bins'   => 'BINS', });
+                                     'bins'   => 'BINS', },
+                   'partsgroup' => 'PARTSGROUPS');
 
   show_no_warehouses_error() if (!scalar @{ $form->{WAREHOUSES} });
 
+  my $cvar_configs                           = CVar->get_configs('module' => 'IC');
+  ($form->{CUSTOM_VARIABLES_FILTER_CODE},
+   $form->{CUSTOM_VARIABLES_INCLUSION_CODE}) = CVar->render_search_options('variables'      => $cvar_configs,
+                                                                           'include_prefix' => 'l_',
+                                                                           'include_value'  => 'Y');
+
   $form->{title}   = $locale->text("Report about warehouse contents");
 
+  setup_wh_report_action_bar();
+
   $form->header();
   print $form->parse_html_template("wh/report_filter",
-                                   { "nextsub"    => "generate_report",
-                                     "WAREHOUSES" => $form->{WAREHOUSES},
-                                     "UNITS"      => AM->unit_select_data(AM->retrieve_units(\%myconfig, $form)) });
+                                   { "WAREHOUSES"  => $form->{WAREHOUSES},
+                                     "PARTSGROUPS" => $form->{PARTSGROUPS},
+                                     "UNITS"       => AM->unit_select_data(AM->retrieve_units(\%myconfig, $form)),
+                                   });
 
   $main::lxdebug->leave_sub();
 }
@@ -824,13 +849,16 @@ sub generate_report {
   my $sort_col     = $form->{sort};
 
   my %filter;
-  my @columns = qw(warehousedescription bindescription partnumber partdescription chargenumber bestbefore qty stock_value);
+  my @columns = qw(warehousedescription bindescription partnumber type_and_classific partdescription chargenumber bestbefore comment qty partunit list_price purchase_price stock_value);
 
   # filter stuff
-  map { $filter{$_} = $form->{$_} if ($form->{$_}) } qw(warehouse_id bin_id partnumber description chargenumber bestbefore date include_invalid_warehouses);
+  map { $filter{$_} = $form->{$_} if ($form->{$_}) } qw(warehouse_id bin_id classification_id partnumber description partsgroup_id chargenumber bestbefore date include_invalid_warehouses);
 
   # show filter stuff also in report
   my @options;
+  my $currentdate = $form->current_date(\%myconfig);
+  push @options, $locale->text('Printdate') . " : ".$locale->date(\%myconfig, $currentdate, 1);
+
   # dispatch all options
   my $dispatch_options = {
    warehouse_id   => sub { push @options, $locale->text('Warehouse') . " : " .
@@ -838,15 +866,20 @@ sub generate_report {
    bin_id         => sub { push @options, $locale->text('Bin') . " : " .
                                             SL::DB::Manager::Bin->find_by(id => $form->{bin_id})->description},
    partnumber     => sub { push @options, $locale->text('Partnumber')     . " : $form->{partnumber}"},
+   classification_id => sub { push @options, $locale->text('Parts Classification'). " : ".
+                                               SL::DB::Manager::PartClassification->get_first(where => [ id => $form->{classification_id} ] )->description; },
    description    => sub { push @options, $locale->text('Description')    . " : $form->{description}"},
+   partsgroup_id  => sub { push @options, $locale->text('Partsgroup')     . " : " .
+                                            SL::DB::PartsGroup->new(id => $form->{partsgroup_id})->load->partsgroup},
    chargenumber   => sub { push @options, $locale->text('Charge Number')  . " : $form->{chargenumber}"},
    bestbefore     => sub { push @options, $locale->text('Best Before')    . " : $form->{bestbefore}"},
-   date           => sub { push @options, $locale->text('Date')           . " : $form->{date}"},
    include_invalid_warehouses    => sub { push @options, $locale->text('Include invalid warehouses ')},
   };
   foreach (keys %filter) {
    $dispatch_options->{$_}->() if $dispatch_options->{$_};
   }
+  push @options, $locale->text('Stock Qty for Date') . " " . $locale->date(\%myconfig, $form->{date}?$form->{date}:$currentdate, 1);
+
   # / end show filter stuff also in report
 
   $filter{qty_op} = WH->convert_qty_op($form->{qty_op});
@@ -859,32 +892,80 @@ sub generate_report {
   }
   # /filter stuff
 
+  $form->{report_generator_output_format} = 'HTML' if !$form->{report_generator_output_format};
+
+  # manual paginating
+  my $allrows        = $form->{report_generator_output_format} eq 'HTML' ? $form->{allrows} : 1;
+  my $page           = $::form->{page} || 1;
+  my $pages          = {};
+  $pages->{per_page} = $::form->{per_page} || 20;
+  my $first_nr       = ($page - 1) * $pages->{per_page};
+  my $last_nr        = $first_nr + $pages->{per_page};
+
+  # no optimisation if qty op
+  if ( !$allrows && $form->{maxrows} && !$filter{qty_op}) {
+    $filter{limit}  = $pages->{per_page};
+    $filter{offset} = ($page - 1) * $pages->{per_page};
+    $first_nr       = 0;
+    $last_nr        = $pages->{per_page};
+  }
+
+  my @contents  = WH->get_warehouse_report(%filter);
+
+  # get maxcount
+  if (!$form->{maxrows}) {
+    $form->{maxrows} = scalar @contents ;
+  }
+
   $form->{subtotal} = '' if (!first { $_ eq $sort_col } qw(partnumber partdescription));
 
   my $report = SL::ReportGenerator->new(\%myconfig, $form);
 
+  my $cvar_configs                 = CVar->get_configs('module' => 'IC');
+  my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs };
+  my @searchable_custom_variables  = grep { $_->{searchable} }  @{ $cvar_configs };
+  push @columns, map { "cvar_$_->{name}" } @includeable_custom_variables;
+
   my @hidden_variables = map { "l_${_}" } @columns;
-  push @hidden_variables, qw(warehouse_id bin_id partnumber description chargenumber bestbefore qty_op qty qty_unit l_warehousedescription l_bindescription);
+  push @hidden_variables, qw(warehouse_id bin_id partnumber partstypes_id description partsgroup_id chargenumber bestbefore qty_op qty qty_unit partunit l_warehousedescription l_bindescription);
   push @hidden_variables, qw(include_empty_bins subtotal include_invalid_warehouses date);
+  push @hidden_variables, qw(classification_id stock_value_basis allrows);
+  push @hidden_variables, map({'cvar_'. $_->{name}}                                         @searchable_custom_variables);
+  push @hidden_variables, map({'cvar_'. $_->{name} .'_from'}  grep({$_->{type} eq 'date'}   @searchable_custom_variables));
+  push @hidden_variables, map({'cvar_'. $_->{name} .'_to'}    grep({$_->{type} eq 'date'}   @searchable_custom_variables));
+  push @hidden_variables, map({'cvar_'. $_->{name} .'_qtyop'} grep({$_->{type} eq 'number'} @searchable_custom_variables));
 
   my %column_defs = (
     'warehousedescription' => { 'text' => $locale->text('Warehouse'), },
     'bindescription'       => { 'text' => $locale->text('Bin'), },
     'partnumber'           => { 'text' => $locale->text('Part Number'), },
+    'type_and_classific'   => { 'text' => $locale->text('Type'), },
     'partdescription'      => { 'text' => $locale->text('Part Description'), },
     'chargenumber'         => { 'text' => $locale->text('Charge Number'), },
     'bestbefore'           => { 'text' => $locale->text('Best Before'), },
     'qty'                  => { 'text' => $locale->text('Qty'), },
+    'partunit'             => { 'text' => $locale->text('Unit'), },
     'stock_value'          => { 'text' => $locale->text('Stock value'), },
+    'purchase_price'       => { 'text' => $locale->text('Purchase price'), },
+    'list_price'           => { 'text' => $locale->text('List Price'), },
   );
 
+  my %column_defs_cvars = map { +"cvar_$_->{name}" => { 'text' => $_->{description} } } @includeable_custom_variables;
+  %column_defs = (%column_defs, %column_defs_cvars);
+
   my $href = build_std_url('action=generate_report', grep { $form->{$_} } @hidden_variables);
-  map { $column_defs{$_}->{link} = $href . "&sort=${_}&order=" . Q($_ eq $sort_col ? 1 - $form->{order} : $form->{order}) } @columns;
+  $href .= "&maxrows=".$form->{maxrows};
+
+  map { $column_defs{$_}->{link} = $href . "&page=".$page."&sort=${_}&order=" . Q($_ eq $sort_col ? 1 - $form->{order} : $form->{order}) } grep {!/^cvar_/} @columns;
 
-  my %column_alignment = map { $_ => 'right' } qw(qty stock_value);
+  my %column_alignment = map { $_ => 'right' } qw(qty list_price purchase_price stock_value);
 
   map { $column_defs{$_}->{visible} = $form->{"l_${_}"} ? 1 : 0 } @columns;
 
+  $column_defs{partunit}->{visible}           = 1;
+  $column_defs{type_and_classific}->{visible} = 1;
+  $column_defs{type_and_classific}->{link} ='';
+
   $report->set_columns(%column_defs);
   $report->set_column_order(@columns);
 
@@ -898,10 +979,13 @@ sub generate_report {
                        'attachment_basename'  => strftime($locale->text('warehouse_report_list') . '_%Y%m%d', localtime time));
   $report->set_options_from_form();
   $locale->set_numberformat_wo_thousands_separator(\%myconfig) if lc($report->{options}->{output_format}) eq 'csv';
+  CVar->add_custom_variables_to_report('module'         => 'IC',
+                                       'trans_id_field' => 'parts_id',
+                                       'configs'        => $cvar_configs,
+                                       'column_defs'    => \%column_defs,
+                                       'data'           => \@contents);
 
   my $all_units = AM->retrieve_units(\%myconfig, $form);
-  my @contents  = WH->get_warehouse_report(%filter);
-
   my $idx       = 0;
 
   my @subtotals_columns = qw(qty stock_value);
@@ -910,13 +994,15 @@ sub generate_report {
   my $total_stock_value = 0;
 
   foreach my $entry (@contents) {
+
+    $entry->{type_and_classific} = SL::Presenter::Part::type_abbreviation($entry->{part_type}).
+                                   SL::Presenter::Part::classification_abbreviation($entry->{classification_id});
     map { $subtotals{$_} += $entry->{$_} } @subtotals_columns;
     $total_stock_value   += $entry->{stock_value} * 1;
-
-    $entry->{qty}         = $form->format_amount_units('amount'     => $entry->{qty},
-                                                       'part_unit'  => $entry->{partunit},
-                                                       'conv_units' => 'convertible');
+    $entry->{qty}         = $form->format_amount(\%myconfig, $entry->{qty});
     $entry->{stock_value} = $form->format_amount(\%myconfig, $entry->{stock_value} * 1, 2);
+    $entry->{purchase_price} = $form->format_amount(\%myconfig, $entry->{purchase_price} * 1, 2);
+    $entry->{list_price}     = $form->format_amount(\%myconfig, $entry->{list_price}     * 1, 2);
 
     my $row_set = [ { map { $_ => { 'data' => $entry->{$_}, 'align' => $column_alignment{$_} } } @columns } ];
 
@@ -925,18 +1011,19 @@ sub generate_report {
             || ($entry->{$sort_col} ne $contents[$idx + 1]->{$sort_col}))) {
 
       my $row = { map { $_ => { 'data' => '', 'class' => 'listsubtotal', 'align' => $column_alignment{$_}, } } @columns };
-      $row->{qty}->{data}         = $form->format_amount_units('amount'     => $subtotals{qty} * 1,
-                                                               'part_unit'  => $entry->{partunit},
-                                                               'conv_units' => 'convertible');
+      $row->{qty}->{data}         = $form->format_amount(\%myconfig, $subtotals{qty});
       $row->{stock_value}->{data} = $form->format_amount(\%myconfig, $subtotals{stock_value} * 1, 2);
+      $row->{purchase_price}->{data} = $form->format_amount(\%myconfig, $subtotals{purchase_price} * 1, 2);
+      $row->{list_price}->{data}     = $form->format_amount(\%myconfig, $subtotals{list_price}     * 1, 2);
 
       %subtotals                  = map { $_ => 0 } @subtotals_columns;
 
       push @{ $row_set }, $row;
     }
 
-    $report->add_data($row_set);
-
+    if ( $allrows || ($idx >= $first_nr && $idx < $last_nr )) {
+        $report->add_data($row_set);
+    }
     $idx++;
   }
 
@@ -953,6 +1040,14 @@ sub generate_report {
 
     $report->add_data($row);
   }
+  if ( ! $allrows ) {
+    $pages->{max}  = SL::DB::Helper::Paginated::ceil($form->{maxrows}, $pages->{per_page}) || 1;
+    $pages->{page} = $page < 1 ? 1: $page > $pages->{max} ? $pages->{max}: $page;
+    $pages->{common} = [ grep { $_->{visible} } @{ SL::DB::Helper::Paginated::make_common_pages($pages->{page}, $pages->{max}) } ];
+
+    $report->set_options('raw_bottom_info_text' => $form->parse_html_template('common/paginate',
+                                                                              {'pages' => $pages , 'base_url' => $href}) );
+  }
 
   $report->generate_with_headers();
 
@@ -1051,6 +1146,119 @@ sub stock {
   call_sub($form->{stock_nextsub} || $form->{nextsub});
 }
 
+sub setup_wh_transfer_warehouse_selection_action_bar {
+  my ($action) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#form', { action => $action } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_wh_transfer_warehouse_selection_assembly_action_bar {
+  my ($action) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Update'),
+        submit    => [ '#form', { action => 'transfer_assembly_update_part' } ],
+        accesskey => 'enter',
+      ],
+      action => [
+        t8('Produce'),
+        submit   => [ '#form', { action => 'create_assembly' } ],
+        disabled => $::form->{parts_id} ? undef : $::locale->text('No assembly has been selected yet.'),
+      ],
+    );
+  }
+}
+
+sub setup_wh_transfer_parts_action_bar {
+  my ($action) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Transfer'),
+        submit    => [ '#form', { action => 'transfer_parts' } ],
+        accesskey => 'enter',
+      ],
+      action => [
+        t8('Back'),
+        call => [ 'kivi.history_back' ],
+      ],
+    );
+  }
+}
+
+sub setup_wh_removal_parts_selection_action_bar {
+  my ($action) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Transfer out'),
+        submit    => [ '#form', { action => 'remove_parts' } ],
+        accesskey => 'enter',
+      ],
+      action => [
+        t8('Back'),
+        call => [ 'kivi.history_back' ],
+      ],
+    );
+  }
+}
+
+sub setup_wh_report_action_bar {
+  my ($action) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Show'),
+        submit    => [ '#form', { action => 'generate_report' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_wh_journal_action_bar {
+  my ($action) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Show'),
+        submit    => [ '#form', { action => 'generate_journal' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+sub setup_wh_journal_list_all_action_bar {
+  my ($action) = @_;
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      combobox => [
+        action => [ t8('Actions') ],
+        action => [
+          t8('Disassemble Assembly'),
+            submit => [ '#form', { action => 'disassemble_assembly' } ],
+            checks => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ],
+          ],
+        ],
+    );
+  }
+}
+
+
 1;
 
 __END__
index 0a9a047..cde5b44 100644 (file)
@@ -1,2 +1,9 @@
-Order Allow,Deny
-Deny from all
+<IfModule mod_authz_core.c>
+  # Apache 2.4
+  Require all denied
+</IfModule>
+<IfModule !mod_authz_core.c>
+  # Apache 2.2
+  Order deny,allow
+  Deny from all
+</IfModule>
index 0ff2aa1..07ae59d 100644 (file)
@@ -4,9 +4,16 @@
 # interface.
 admin_password = admin123
 
-# Which module to use for authentication. Valid values are 'DB' and
-# 'LDAP'.  If 'LDAP' is used then users cannot change their password
-# via kivitendo.
+# Which modules to use for authentication. Valid values are 'DB' and
+# 'LDAP'. You can use multiple modules separated by spaces.
+#
+# Multiple LDAP modules with different configurations can be used by
+# postfixing 'LDAP' with the name of the configuration section to use:
+# 'LDAP:ldap_fallback' would use the data from
+# '[authentication/ldap_fallback]'. The name defaults to 'ldap' if it
+# isn't given.
+#
+# Note that the LDAP module doesn't support changing the password.
 module = DB
 
 # The cookie name can be changed if desired.
@@ -43,6 +50,8 @@ password =
 # specified.
 #
 # tls:       Activate encryption via TLS
+# verify:    If 'tls' is used, how to verify the server's certificate.
+#            Can be one of 'require' or 'none'.
 # attribute: Name of the LDAP attribute containing the user's login name
 # base_dn:   Base DN the LDAP searches start from
 # filter:    An optional LDAP filter specification. The string '<%login%>'
@@ -51,6 +60,12 @@ password =
 #            If searching the LDAP tree requires user credentials
 #            (e.g. ActiveDirectory) then these two parameters specify
 #            the user name and password to use.
+# timeout:   Timeout when connecting to the server in seconds.
+#
+# You can specify a fallback LDAP server to use in case the main one
+# isn't reachable by duplicating this whole section as
+# "[authentication/ldap_fallback]".
+#
 host          = localhost
 port          = 389
 tls           = 0
@@ -59,11 +74,42 @@ base_dn       =
 filter        =
 bind_dn       =
 bind_password =
+timeout       = 10
+verify        = require
 
 [system]
 # Set language for login and admin forms. Currently "de" (German)
 # and "en" (English, not perfect) are available.
 language = de
+# MassPrint Timeout
+# must be less than cgi timeout
+#
+massprint_timeout = 30
+
+# Set default_manager for admin forms. Currently "german"
+# and "swiss" are available.
+default_manager = german
+
+# The memory limits given here determine the maximum process size
+# (vsz, the total amount of memory this process uses including memory
+# swapped out or shared with other processes) or resident set size
+# (rss, the amount of memory not swapped out/shared with other
+# processes). If either limit is reached at the end of the request
+# then the kivitendo process will exit.
+#
+# This only applies for processes under FCGI and the task manager.
+# For CGI configurations the process will be terminated after each request
+# regardless of this setting.
+#
+# Note: this will only terminate processes with too high memory consumption. It
+# is assumed that an external managing service will start new instances. For
+# FCGI this will usually be apache or the wrapper scripts for nginx, for the
+# task server this will have to be the system manager.
+#
+# Numbers can be postfixed with KB, MB, GB. If no number is given or
+# the number is 0 then no checking will be performed.
+memory_limit_rss =
+memory_limit_vsz =
 
 [paths]
 # path to temporary files (must be writeable by the web server)
@@ -74,13 +120,23 @@ spool = spool
 templates = templates
 # Path to the old memberfile (ignored on new installations)
 memberfile = users/members
+# Path to ELSTER geierlein webserver path inside kivitendo
+# (must be inside kivitendo but you can set an ALIAS for apache/oe
+# if set the export to geierlein is enabled
+# geierlein_path = geierlein
+
+#
+# document path for FileSystem FileManagement:
+#  (must be reachable read/write but not executable from webserver)
+# document_path = /var/local/kivi_documents
+#
 
 [mail_delivery]
-# Delivery method can be 'sendmail' or 'smtp' (the default). For
-# 'method = sendmail' the parameter 'mail_delivery.sendmail' is used
-# as the executable to call. If 'applications.sendmail' still exists
-# (backwards compatibility) then 'applications.sendmail' will be used
-# instead of 'mail_delivery.sendmail'.
+# Delivery method can be 'sendmail' or 'smtp'. For 'method = sendmail' the
+# parameter 'mail_delivery.sendmail' is used as the executable to call. If
+# 'applications.sendmail' still exists (backwards compatibility) then
+# 'applications.sendmail' will be used instead of 'mail_delivery.sendmail'.
+# If method is empty, mail delivery is disabled.
 method = smtp
 # Location of sendmail for 'method = sendmail'
 sendmail = /usr/sbin/sendmail -t<%if myconfig_email%> -f <%myconfig_email%><%end%>
@@ -116,10 +172,6 @@ latex = pdflatex
 # binary.
 python_uno = python
 
-# Location of the aqbanking binary to use when converting MT940 files
-# into the kivitendo import format
-aqbanking = /usr/bin/aqbanking-cli
-
 [environment]
 # Add the following paths to the PATH environment variable.
 path = /usr/local/bin:/usr/X11R6/bin:/usr/X11/bin
@@ -146,16 +198,15 @@ openofficeorg_daemon = 1
 openofficeorg_daemon_port = 2002
 
 [task_server]
-# kivitendo client (either its name or its database ID) for database
-# access (both 'client' and 'login' are required)
-client =
-# kivitendo user (login) name to use for certain jobs (both 'client'
-# and 'login' are required)
-login =
 # Set to 1 for debug messages in /tmp/kivitendo-debug.log
 debug = 0
 # Chose a system user the daemon should run under when started as root.
 run_as =
+# Task servers can run on multiple machines. Each needs its own unique
+# ID. If unset, it defaults to the host name. All but one task server
+# must have 'only_run_tasks_for_this_node' set to 1.
+node_id =
+only_run_tasks_for_this_node = 0
 
 [task_server/notify_on_failure]
 # If you want email notifications for failed jobs then set this to a
@@ -169,15 +220,18 @@ email_subject  = kivitendo Task-Server: Hintergrundjob fehlgeschlagen
 email_template = templates/webpages/task_server/failure_notification_email.txt
 
 [periodic_invoices]
-# The user name a report about the posted and printed invoices is sent
-# to.
-send_email_to  = mb
+# The user name or email address a report about the posted and printed
+# invoices is sent to.
+send_email_to  =
 # The "From:" header for said email.
 email_from     = kivitendo Daemon <root@localhost>
 # The subject for said email.
 email_subject  = Benachrichtigung: automatisch erstellte Rechnungen
 # The template file used for the email's body.
 email_template = templates/webpages/oe/periodic_invoices_email.txt
+# Whether to always send the mail (0), or only if there were errors
+# (1).
+send_for_errors_only = 0
 
 [self_test]
 
@@ -229,12 +283,14 @@ log_file = /tmp/kivitendo_console_debug.log
 # database will be dropped & created before any other test is run. The
 # following parameters must be given:
 [testing/database]
-host     = localhost
-port     = 5432
-db       =
-user     = postgres
-password =
-template = template1
+host               = localhost
+port               = 5432
+db                 =
+user               = postgres
+password           =
+template           = template1
+superuser_user     = postgres
+superuser_password =
 
 [devel]
 # Several settings related to the development of kivitendo.
@@ -263,7 +319,7 @@ dbix_log4perl_config = log4perl.logger = FATAL, LOGFILE
                      = log4perl.appender.A1.layout.ConversionPattern=%d %p> %F{1}:%L %M - %m%n
 
 # Activate certain global debug messages. If you want to combine
-# several options then list them seperated by spaces.
+# several options then list them separated by spaces.
 #
 # Possible values include:
 #   NONE   - no debug output (default)
@@ -277,6 +333,8 @@ dbix_log4perl_config = log4perl.logger = FATAL, LOGFILE
 #   REQUEST            - Log each request. Careful! Passwords get filtered, but
 #                        there may be confidential information being logged here
 #   WARN               - warnings
+#   SHOW_CALLER        - include the file name & line number from where a call
+#                        to "message" or "dump" was called
 #   ALL                - all possible debug messages
 #
 #   DEVEL              - sames as "INFO QUERY TRACE BACKTRACE_ON_ERROR REQUEST_TIMER"
@@ -316,6 +374,11 @@ keep_installation_unlocked = 0
 # the web browser to always reload the resources.
 auto_reload_resources = 0
 
+# Alternative to auto_reload_resources. If the installation dir is under git
+# version control, this will use the HEAD commit sha1 as the random GET
+# parameter, so that resources are reloaded if the installed version charnges.
+git_commit_reload_recources = 0
+
 # If set to 1 each exception will include a full stack backtrace.
 backtrace_on_die = 0
 
@@ -339,3 +402,5 @@ dial_command =
 external_prefix = 0
 # The prefix for international calls (numbers starting with +).
 international_dialing_prefix = 00
+# Our own country code
+our_country_code = 49
index 4357a97..e9b4d6b 100644 (file)
@@ -1,6 +1,5 @@
 /* Allgemeine Schriftdefinition */
 th,td {
-       font-family: Arial, Verdana, Helvetica, Sans-serif;
        font-size:small;
 }
 
index fe764b8..dd29500 100644 (file)
@@ -16,6 +16,7 @@
 .position-relative { position: relative }
 .position-absolute { position: absolute }
 
+.hidden { display: none; }
 
 /* media stuff */
 @media screen {   .noscreen { display: none } }
@@ -72,3 +73,167 @@ input.grow_on_focus:focus { width: 150px }
 #dunning_invoice_list .direct_debit a {
   color: #aaa;
 }
+/* orderitems */
+.shipped     { color: green }
+.not_shipped { color: red   }
+
+/* actionbar styling */
+div.layout-actionbar {
+  position: fixed;
+  height: 28px;
+  top: 20px;
+  z-index: 20;
+  width: 100%;
+  padding: 2px;
+}
+
+div.layout-actionbar-action {
+  -webkit-touch-callout: none; /* iOS Safari */
+  -webkit-user-select: none;   /* Chrome/Safari/Opera */
+  -khtml-user-select: none;    /* Konqueror */
+  -moz-user-select: none;      /* Firefox */
+  -ms-user-select: none;       /* Internet Explorer/Edge */
+  user-select: none;           /* don't select text on double click */
+}
+
+div.layout-actionbar ~ div:first {
+  padding-top: 25px;
+}
+
+div.layout-actionbar > div + div {
+  margin-left: 2px;
+}
+
+div.layout-actionbar-separator {
+  display: inline-block;
+  width: 20px;
+}
+
+div.layout-actionbar div.layout-actionbar-link,
+div.layout-actionbar div.layout-actionbar-submit,
+div.layout-actionbar div.layout-actionbar-scriptbutton,
+div.layout-actionbar div.layout-actionbar-link:focus,
+div.layout-actionbar div.layout-actionbar-submit:focus,
+div.layout-actionbar div.layout-actionbar-scriptbutton:focus {
+  display: inline-block;
+  width: 120px;
+  box-sizing: border-box;
+  text-align: center;
+  border-width: 1px;
+  border-style: solid;
+  padding: 4px 4px;
+  cursor: default;
+}
+
+div.layout-actionbar div.layout-actionbar-link:hover,
+div.layout-actionbar div.layout-actionbar-submit:hover,
+div.layout-actionbar div.layout-actionbar-scriptbutton:hover {
+  border-width: 1px;
+  border-style: solid;
+}
+
+div.layout-actionbar-combobox {
+  position: relative;
+  display: inline-block;
+}
+
+div.layout-actionbar div.layout-actionbar-action {
+  height: 25px;
+}
+
+div.layout-actionbar-combobox div.layout-actionbar-combobox-head div {
+  width: 100px;
+  height: 25px;
+}
+
+div.layout-actionbar-combobox div.layout-actionbar-combobox-head span {
+  display: inline-block;
+  border-width: 1px 1px 1px 1px;
+  border-style: solid;
+  padding: 4px;
+  width: 14px;
+  height: 15px;
+  position: absolute;
+  top: 0;
+  right: 0;
+}
+
+div.layout-actionbar-combobox div.layout-actionbar-combobox-head span:after {
+  content: "";
+  width: 0;
+  height: 0;
+  position: absolute;
+  right: 8px;
+  top: 50%;
+  margin-top: -3px;
+  border-width: 3px 3px 0 3px;
+  border-style: solid;
+}
+
+div.layout-actionbar-combobox.active div.layout-actionbar-combobox-head span:after {
+  border-width: 0 3px 3px 3px;
+}
+
+
+div.layout-actionbar-combobox div.layout-actionbar-combobox-head {
+  padding-right: 20px;
+  white-space: nowrap;
+  display: block;
+}
+
+div.layout-actionbar-combobox div.layout-actionbar-combobox-list {
+  position: absolute;
+  display: none;
+  min-width: 120px;
+}
+
+div.layout-actionbar-combobox.active div.layout-actionbar-combobox-list {
+  display: inline-block;
+  z-index: 10;
+}
+
+div.layout-actionbar-combobox-list div.layout-actionbar-action {
+  white-space: nowrap;
+  display: block;
+  position: relative;
+  width: 100%;
+  text-align: left;
+  padding: 4px;
+}
+
+div.cke_textarea_inline > :first-child {
+  margin-top: 0;
+}
+div.cke_textarea_inline > :last-child {
+  margin-bottom: 0;
+}
+div.cke_textarea_inline {
+  overflow-y: auto;
+}
+div.cke_textarea_inline:focus {
+  outline: 0;
+}
+
+span.upload_drop_zone {
+  padding: 4px;
+  border: 2px;
+  border-color: darkgray lightgray lightgray;
+  border-style: solid;
+  background-color: whitesmoke;
+}
+
+.overlay_div {
+  position: relative;
+}
+
+.overlay_img {
+  position: absolute;
+  top: -100px;
+  left: -100px;
+  z-index: 2;
+  cursor: pointer;
+}
+
+.thumbnail {
+  cursor: pointer;
+}
index 71fa9b3..9ae0fc3 100644 (file)
@@ -1,36 +1,32 @@
-/************************************************************************************************************\r
-\r
-       DHTML Suite for Applications\r
-       (C) www.dhtmlgoodies.com, August 2006\r
-\r
-       CSS for the DHTMLsuite_tableWidget class.\r
-\r
-       Terms of use:\r
-       Look at the terms of use at http://www.dhtmlgoodies.com/index.html?page=termsOfUse\r
-\r
-       Thank you!\r
-\r
-       www.dhtmlgoodies.com\r
-       Alf Magne Kalleland\r
-\r
-************************************************************************************************************/\r
-\r
-.DHTMLSuite_menuBar_top{       /* The bar that is parent of the menu strip */\r
-       height:26px;\r
-       width:100%;\r
-       background-repeat:repeat-x;\r
-       font-family: sans-serif, Verdana, Arial, Helvetica;\r
-       font-size:12px;\r
-       z-index:100000;\r
-       padding-left:10px;\r
-}\r
-\r
-.DHTMLSuite_menuBar_sub{\r
-       position:absolute;\r
-       background-color:#FFF;\r
-       border:1px solid #D1D1D1;\r
-       background-repeat:repeat-y;\r
-       background-position: left center;\r
-       display:inline;\r
-}\r
-\r
+/************************************************************************************************************
+
+       DHTML Suite for Applications
+       (C) www.dhtmlgoodies.com, August 2006
+
+       CSS for the DHTMLsuite_tableWidget class.
+
+       Terms of use:
+       Look at the terms of use at http://www.dhtmlgoodies.com/index.html?page=termsOfUse
+
+       Thank you!
+
+       www.dhtmlgoodies.com
+       Alf Magne Kalleland
+
+************************************************************************************************************/
+
+.DHTMLSuite_menuBar_top{       /* The bar that is parent of the menu strip */
+  position: fixed;
+  height: 26px;
+  width: 100%;
+  z-index: 100;
+  padding-left: 10px;
+  background-color: white;
+}
+
+.DHTMLSuite_menuBar_sub{
+  position: fixed;
+  background-color: #FFF;
+  border: 1px solid #000;
+  display: inline;
+}
index 5f75312..8bfe131 100644 (file)
-/* CSS FOR MENU ITEM OF TYPE "top" */\r
-\r
-.DHTMLSuite_menuItem_top_regular,.DHTMLSuite_menuItem_top_over,.DHTMLSuite_menuItem_top_click,.DHTMLSuite_menuItem_top_disabled,.DHTMLSuite_menuItem_top_active{\r
-       height:19px;\r
-       line-height:16px;\r
-       margin-right:2px;\r
-       margin-top:1px;\r
-       padding-left:4px;\r
-       padding-right:4px;\r
-       padding-top:2px;\r
-       padding-bottom:2px;\r
-}\r
-.DHTMLSuite_menuItem_top_regular div,.DHTMLSuite_menuItem_top_over div,.DHTMLSuite_menuItem_top_click div,.DHTMLSuite_menuItem_top_disabled div,.DHTMLSuite_menuItem_top_active div{\r
-       padding-top:2px;\r
-       padding-bottom:2px;\r
-}\r
-.DHTMLSuite_menuItem_top_regular{      /* Top level menu item - Regular state */\r
-       cursor:pointer;\r
-}\r
-\r
-.DHTMLSuite_menuItem_top_over{ /* Top level menu item - Mouse over state */\r
-       color:#FE5F14;\r
-       background-color:#D1D1D1;\r
-       cursor:pointer;\r
-}\r
-\r
-.DHTMLSuite_menuItem_top_active{       /* Top level menu item - Active state - this is typical the state for menu item 1 when a sub group is expanded and the mouse is located over one of the sub menu items */\r
-       cursor:pointer;\r
-}\r
-\r
-.DHTMLSuite_menuItem_top_click{        /* Top level menu item - Mouse click state */\r
-       color:#FE5F14;\r
-       background-color:#D1D1D1;\r
-       cursor:pointer;\r
-       z-index:20000;\r
-}\r
-\r
-.DHTMLSuite_menuItem_top_disabled{     /* Disabled menu item */\r
-       cursor:default;\r
-}\r
-\r
-.DHTMLSuite_menuItem_top_disabled img,.DHTMLSuite_menuItem_top_disabled div{   /* Sub divs of disabled top level items. A menu item is a div with some subdivs(one for the icon, one for text etc.). this is the css for these sub divs */\r
-       filter:alpha(opacity=40);       /* Transparency */\r
-       opacity:0.4;    /* Transparency */\r
-       -moz-opacity:0.4;       /* Transparency */\r
-       -khtml-opacity:.4;      /* Transparency */\r
-}\r
-\r
-.DHTMLSuite_menuItem_top_disabled div,.DHTMLSuite_menuItem_top_active div, .DHTMLSuite_menuItem_top_click div, .DHTMLSuite_menuItem_top_regular div,.DHTMLSuite_menuItem_top_over div{ /* divs for the text,icon and arrow of a menu item */\r
-       float:left;\r
-       padding-left:1px;\r
-       padding-right:1px;\r
-}\r
-\r
-/* CSS FOR THE SMALL ARROW DIV - WHEN YOU CLICK ON THIS DIV, SUB ELEMENTS WILL SHOW */\r
-.DHTMLSuite_menuItem_top_arrowShowSub{ /* This is the arrow for top level elements */\r
-       width:12px;     /* Width of item */\r
-       background-repeat:no-repeat;    /* No background repeat for the arrow */\r
-       background-position:center center;      /* Position of the arrow. at the center of this div */\r
-       background-image: url('../../../image/dhtmlsuite/menu_strip_down_arrow.png');   /* Relative path to the arrow */\r
-       margin:0px;\r
-       margin-right:-1px;      /* -1 pixel is added to get the arrow a little bit more to the right. this is because of the right padding of top level items */\r
-       padding:0px;\r
-       height:16px;\r
-       margin-left:2px;\r
-       float:right;\r
-}\r
-\r
-.DHTMLSuite_menuItem_top_over .DHTMLSuite_menuItem_top_arrowShowSub,\r
-.DHTMLSuite_menuItem_top_click .DHTMLSuite_menuItem_top_arrowShowSub\r
-{      /* Sub menu arrows */\r
-       margin-left:1px;\r
-       border-left:1px solid #000;\r
-}\r
-\r
-/* CSS FOR SEPARATOR */\r
-\r
-.DHTMLSuite_menuItem_separator_top{    /* Separator of type "top" */\r
-       height:20px;\r
-       margin-top:3px;\r
-       margin-bottom:3px;\r
-       width:4px;\r
-       padding-left:3px;\r
-       padding-right:3px;\r
-       background-repeat:repeat-y;\r
-       background-image:url('../../../image/dhtmlsuite/menu_strip_separator.gif');\r
-\r
-}\r
-\r
-\r
-/* CSS FOR MENU ITEM OF TYPE "sub" */\r
-\r
-.DHTMLSuite_menuItem_sub_regular, .DHTMLSuite_menuItem_sub_over,.DHTMLSuite_menuItem_sub_click,.DHTMLSuite_menuItem_sub_disabled,.DHTMLSuite_menuItem_sub_active{      /* Commom css for regular, mouse over and mouse click items */\r
-       clear:both;\r
-       line-height:18px;       /* Height of menu item */\r
-       height:18px;            /* Height of menu item */\r
-       padding-left:25px;      /* Space inside the menu item - the 25 pixels are used in order to avoid text overlapping menu item icon */\r
-       padding-right:4px;      /* Space inside the menu item */\r
-       padding-top:2px;        /* Space inside the menu item */\r
-       padding-bottom:2px;      /* Space inside the menu item */\r
-       cursor:pointer;         /* Mouse cursor set to a "hand" */\r
-       margin:1px;     /* A little space around the item */\r
-       background-repeat:no-repeat;    /* No background repeat */\r
-}\r
-.DHTMLSuite_menuItem_sub_disabled{     /* Disabled sub menu item */\r
-       cursor:default; /* Arrow as cursor instead of hand */\r
-}\r
-.DHTMLSuite_menuItem_sub_disabled div,.DHTMLSuite_menuItem_sub_disabled{       /* Disabled sub menu item - Here, we apply the rules on the divs inside the element, i.e. the div for the icon, text and arrow */\r
-       filter:alpha(opacity=40);       /* Transparency */\r
-       opacity:0.4;    /* Transparency */\r
-       -moz-opacity:0.4;       /* Transparency */\r
-       -khtml-opacity:.4;      /* Transparency */\r
-}\r
-.DHTMLSuite_menuItem_sub_regular,.DHTMLSuite_menuItem_sub_disabled{    /* Regular menu item */\r
-}\r
-\r
-.DHTMLSuite_menuItem_sub_over,.DHTMLSuite_menuItem_sub_click,.DHTMLSuite_menuItem_sub_active{\r
-}\r
-\r
-.DHTMLSuite_menuItem_sub_over,.DHTMLSuite_menuItem_sub_active{ /* Mouse over effect */\r
-       color:#FE5F14;\r
-       background-color:#D1D1D1;\r
-}\r
-\r
-.DHTMLSuite_menuItem_sub_click{        /* Mouse click effect */\r
-       color:#FE5F14;\r
-       background-color:#D1D1D1;\r
-}\r
-\r
-.DHTMLSuite_menuItem_sub_click div, .DHTMLSuite_menuItem_sub_regular div,.DHTMLSuite_menuItem_sub_over div,.DHTMLSuite_menuItem_sub_active div,.DHTMLSuite_menuItem_sub_disabled div{  /* divs for the text,icon and arrow of a menu item */\r
-       float:left;             /* To get the icons and text of sub elements side by side */\r
-       padding-left:1px;\r
-       padding-right:1px;\r
-\r
-}\r
-.DHTMLSuite_menuItem_sub_arrowShowSub{ /* Arrow div for sub elements (Right pointing arrow ) */\r
-       position:absolute;      /* Never change this one */\r
-       background-image:url('../../../image/dhtmlsuite/menu-bar-right-arrow.png');     /* Path relative to the css file */\r
-       width:18px;\r
-       height:18px;\r
-       text-align:right;\r
-       right:0px;\r
-       background-repeat:no-repeat;    /* No background repeat */\r
-       background-position: center right;      /* Position of arrow */\r
-}\r
-\r
-\r
-.DHTMLSuite_menuItem_separator_sub{    /* Separator of type "sub" */\r
-       height:1px;     /* Height of separator */\r
-       margin-top:1px; /* Space above the separator */\r
-       margin-bottom:1px;      /* Space below the separator */\r
-       margin-left:24px;       /* left margin because we don't want the separator to cover the gradient */\r
-       padding-right:3px;      /* space at the right of the separator */\r
-       color:#FE5F14;\r
-       background-color:#D1D1D1;\r
-}\r
-\r
-.DHTMLSuite_menuItem_textContent\r
-{\r
-  border-bottom-style: none !important;\r
-  background-color: inherit !important;\r
-  color: inherit !important;\r
-}\r
+/* CSS FOR MENU ITEM OF TYPE "top" */
+
+.DHTMLSuite_menuItem_top_regular,.DHTMLSuite_menuItem_top_over,.DHTMLSuite_menuItem_top_click,.DHTMLSuite_menuItem_top_disabled,.DHTMLSuite_menuItem_top_active{
+       height:19px;
+       line-height:16px;
+       margin-right:2px;
+       margin-top:1px;
+       padding-left:4px;
+       padding-right:4px;
+       padding-top:2px;
+       padding-bottom:2px;
+}
+.DHTMLSuite_menuItem_top_regular div,.DHTMLSuite_menuItem_top_over div,.DHTMLSuite_menuItem_top_click div,.DHTMLSuite_menuItem_top_disabled div,.DHTMLSuite_menuItem_top_active div{
+       padding-top:2px;
+       padding-bottom:2px;
+}
+.DHTMLSuite_menuItem_top_regular{      /* Top level menu item - Regular state */
+       cursor:pointer;
+}
+
+.DHTMLSuite_menuItem_top_over{ /* Top level menu item - Mouse over state */
+       color:#FE5F14;
+       background-color:whitesmoke;
+       cursor:pointer;
+}
+
+.DHTMLSuite_menuItem_top_active{       /* Top level menu item - Active state - this is typical the state for menu item 1 when a sub group is expanded and the mouse is located over one of the sub menu items */
+       cursor:pointer;
+}
+
+.DHTMLSuite_menuItem_top_click{        /* Top level menu item - Mouse click state */
+       color:#FE5F14;
+       background-color:whitesmoke;
+       cursor:pointer;
+       z-index:20000;
+}
+
+.DHTMLSuite_menuItem_top_disabled{     /* Disabled menu item */
+       cursor:default;
+}
+
+.DHTMLSuite_menuItem_top_disabled img,.DHTMLSuite_menuItem_top_disabled div{   /* Sub divs of disabled top level items. A menu item is a div with some subdivs(one for the icon, one for text etc.). this is the css for these sub divs */
+       filter:alpha(opacity=40);       /* Transparency */
+       opacity:0.4;    /* Transparency */
+       -moz-opacity:0.4;       /* Transparency */
+       -khtml-opacity:.4;      /* Transparency */
+}
+
+.DHTMLSuite_menuItem_top_disabled div,.DHTMLSuite_menuItem_top_active div, .DHTMLSuite_menuItem_top_click div, .DHTMLSuite_menuItem_top_regular div,.DHTMLSuite_menuItem_top_over div{ /* divs for the text,icon and arrow of a menu item */
+       float:left;
+       padding-left:1px;
+       padding-right:1px;
+}
+
+/* CSS FOR THE SMALL ARROW DIV - WHEN YOU CLICK ON THIS DIV, SUB ELEMENTS WILL SHOW */
+.DHTMLSuite_menuItem_top_arrowShowSub{ /* This is the arrow for top level elements */
+       width:12px;     /* Width of item */
+       background-repeat:no-repeat;    /* No background repeat for the arrow */
+       background-position:center center;      /* Position of the arrow. at the center of this div */
+       background-image: url('../../../image/dhtmlsuite/menu_strip_down_arrow.png');   /* Relative path to the arrow */
+       margin:0px;
+       margin-right:-1px;      /* -1 pixel is added to get the arrow a little bit more to the right. this is because of the right padding of top level items */
+       padding:0px;
+       height:16px;
+       margin-left:2px;
+       float:right;
+}
+
+.DHTMLSuite_menuItem_top_over .DHTMLSuite_menuItem_top_arrowShowSub,
+.DHTMLSuite_menuItem_top_click .DHTMLSuite_menuItem_top_arrowShowSub
+{      /* Sub menu arrows */
+       margin-left:1px;
+       border-left:1px solid #000;
+}
+
+/* CSS FOR SEPARATOR */
+
+.DHTMLSuite_menuItem_separator_top{    /* Separator of type "top" */
+       height:20px;
+       margin-top:3px;
+       margin-bottom:3px;
+       width:4px;
+       padding-left:3px;
+       padding-right:3px;
+       background-repeat:repeat-y;
+       background-image:url('../../../image/dhtmlsuite/menu_strip_separator.gif');
+
+}
+
+
+/* CSS FOR MENU ITEM OF TYPE "sub" */
+
+.DHTMLSuite_menuItem_sub_regular, .DHTMLSuite_menuItem_sub_over,.DHTMLSuite_menuItem_sub_click,.DHTMLSuite_menuItem_sub_disabled,.DHTMLSuite_menuItem_sub_active{      /* Commom css for regular, mouse over and mouse click items */
+       clear:both;
+       line-height:18px;       /* Height of menu item */
+       height:18px;            /* Height of menu item */
+       padding-left:25px;      /* Space inside the menu item - the 25 pixels are used in order to avoid text overlapping menu item icon */
+       padding-right:4px;      /* Space inside the menu item */
+       padding-top:2px;        /* Space inside the menu item */
+       padding-bottom:2px;      /* Space inside the menu item */
+       cursor:pointer;         /* Mouse cursor set to a "hand" */
+       margin:1px;     /* A little space around the item */
+       background-repeat:no-repeat;    /* No background repeat */
+}
+.DHTMLSuite_menuItem_sub_disabled{     /* Disabled sub menu item */
+       cursor:default; /* Arrow as cursor instead of hand */
+}
+.DHTMLSuite_menuItem_sub_disabled div,.DHTMLSuite_menuItem_sub_disabled{       /* Disabled sub menu item - Here, we apply the rules on the divs inside the element, i.e. the div for the icon, text and arrow */
+       filter:alpha(opacity=40);       /* Transparency */
+       opacity:0.4;    /* Transparency */
+       -moz-opacity:0.4;       /* Transparency */
+       -khtml-opacity:.4;      /* Transparency */
+}
+.DHTMLSuite_menuItem_sub_regular,.DHTMLSuite_menuItem_sub_disabled{    /* Regular menu item */
+}
+
+.DHTMLSuite_menuItem_sub_over,.DHTMLSuite_menuItem_sub_click,.DHTMLSuite_menuItem_sub_active{
+}
+
+.DHTMLSuite_menuItem_sub_over,.DHTMLSuite_menuItem_sub_active{ /* Mouse over effect */
+       color:#FE5F14;
+       background-color:whitesmoke;
+}
+
+.DHTMLSuite_menuItem_sub_click{        /* Mouse click effect */
+       color:#FE5F14;
+       background-color:whitesmoke;
+}
+
+.DHTMLSuite_menuItem_sub_click div, .DHTMLSuite_menuItem_sub_regular div,.DHTMLSuite_menuItem_sub_over div,.DHTMLSuite_menuItem_sub_active div,.DHTMLSuite_menuItem_sub_disabled div{  /* divs for the text,icon and arrow of a menu item */
+       float:left;             /* To get the icons and text of sub elements side by side */
+       padding-left:1px;
+       padding-right:1px;
+
+}
+.DHTMLSuite_menuItem_sub_arrowShowSub{ /* Arrow div for sub elements (Right pointing arrow ) */
+       position:absolute;      /* Never change this one */
+       background-image:url('../../../image/dhtmlsuite/menu-bar-right-arrow.png');     /* Path relative to the css file */
+       width:18px;
+       height:18px;
+       text-align:right;
+       right:0px;
+       background-repeat:no-repeat;    /* No background repeat */
+       background-position: center right;      /* Position of arrow */
+}
+
+
+.DHTMLSuite_menuItem_separator_sub{    /* Separator of type "sub" */
+       height:1px;     /* Height of separator */
+       margin-top:1px; /* Space above the separator */
+       margin-bottom:1px;      /* Space below the separator */
+       margin-left:24px;       /* left margin because we don't want the separator to cover the gradient */
+       padding-right:3px;      /* space at the right of the separator */
+       color:#FE5F14;
+       background-color:whitesmoke;
+}
+
+.DHTMLSuite_menuItem_textContent
+{
+  border-bottom-style: none !important;
+  background-color: inherit !important;
+  color: inherit !important;
+}
index 029ca5c..b2176a7 100644 (file)
@@ -6,22 +6,33 @@
 }
 
 #frame-header {
-  text-align: center;
+  text-align: left;
   margin: 0;
-  padding: 0;
   border: 0;
   overflow: hidden;
-  min-height: 20px;
+  height: 20px;
   width: 100%;
   border-spacing: 0;
   font-size: 12px;
+  position: fixed;
+  top: 0;
+  z-index: 40;
+  background-color: white;
+  line-height: 100%;
+}
+
+#frame-header + div {
+  padding-top: 20px;
 }
 
 #frame-header .frame-header-left {
   float: left;
+  margin-right: 10px;
+  margin-top: 2px;
 }
 #frame-header .frame-header-right {
   float: right;
+  margin-top: 2px;
 }
 
 #frame-header .frame-header-left,
 #frame-header .frame-header-right  {
   border-spacing: 0;
   padding: 0;
-  font-family: verdana,arial,sans-serif;
   vertical-align: middle;
 }
 
 #frame-header .frame-header-right {
-  margin-top: 3px;
+  vertical-align: middle;
 }
 
 #frame-header #ajax-spinner {
-  margin-top: 2px;
-  margin-right: 10px;
   display: none;
 }
 
 #frame-header input {
   font-size: 12px;
+  margin-top: 2px;
 }
index 4f8a19d..ba0c31d 100644 (file)
@@ -13,7 +13,7 @@
   border-radius: 4px;
   border: 0;
   color: #333333;
-  font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif;
+  font-family: sans-serif;
   font-size: 1.1em;
   overflow: hidden;
 }
 .jstree a {
   border-bottom: none;
 }
+/* show scrollbar vertical */
+.ui-tabs-panel {
+  overflow: scroll;
+}
index 9c5ea64..1cbebb2 100644 (file)
-/* Stylesheet for kivitendo * Name:kivitendo.css*/
-/* Colortable
-
-Background:    #EBEBEB burlywood
-Links:                 #006400 DarkGreen
-Link-hover             #FE5F14 Orange / #FFFFE0 lightyellow
-Titles, BG/VG: #79B61B Mid-green FFFFFF White
-Tabcolor: #CAFFA3
+/* Stylesheet for kivitendo * Name:kivitendo.css
+
+Color table
+-----------
+
+Background:    #EBEBEB burlywood
+Links:         #006400 DarkGreen
+Link-hover     #FE5F14 Orange / #FFFFE0 lightyellow
+Titles, BG/VG: #79B61B Mid-green FFFFFF White
+Tabcolor:      #CAFFA3
 */
 
 
 body {
-       font-family: Verdana, Arial, Helvetica;
-       background-color: #FFFFFF;
-       color: #000000;
-    font-size: 9pt;
+  background-color: #FFFFFF;
+  color: #000000;
+  font-family: sans-serif;
+  font-size: 80%;
+}
+
+/* Input elements */
+input,
+textarea,
+select,
+div.cke_textarea_inline {
+  -moz-border-radius: 0;
+  -webkit-border-radius: 0;
+  -khtml-border-radius: 0;
+  background-color: white;
+  border: 1px;
+  border-color: darkgray lightgray lightgray;
+  border-radius: 0;
+  border-style: solid;
+  outline: none;
+  padding: 1px;
+}
+
+input[type="text"], input[type="password"]
+textarea,
+select {
+  -moz-appearance: none;
+  -webkit-appearance: none;
+  -o-appearance: none;
+}
+
+select {
+  appearance : none;
+  background: white url('../../image/select-down.png') no-repeat scroll right center;
+  padding: 0 14px 0 0;
+}
+
+input:focus,
+textarea:focus,
+select:focus,
+div.cke_textarea_inline:focus {
+  background-color: #ffffa0;
+  border: 1px solid #fe5f14;
 }
 
+input[type="button"],
+input[type="submit"],
+button {
+  background-color: whitesmoke;
+  border: 1px;
+  border-color: darkgray;
+  border-style: solid;
+  padding: 0px 4px;
+}
+
+input[type="button"]:focus,
+input[type="submit"]:focus,
+button:focus {
+  background-color: #ffffa0;
+  border-color: #fe5f14;
+}
+
+button:hover:enabled,
+input[type="button"]:hover:enabled,
+input[type="submit"]:hover:enabled {
+  color: #fe5f14;
+}
 
 /* The look of links */
 a {
-       padding: 0 0.2em;
-       text-decoration: none;
-       /* border-bottom: thin solid; */
-       /* font-weight: bold; */
+  padding: 0 0.2em;
+  text-decoration: none;
 }
 A:link, A:visited, A:active {
-       color: #000000;
-       border-bottom: thin solid #FE5F14;
+  color: #000000;
+  border-bottom: thin solid #FE5F14;
 }
 a:hover {
-       color: #FE5F14;
-       background-color: #D1D1D1;
+  color: #FE5F14;
+  background-color: whitesmoke;
 }
 a.selected:hover {
-       color:#EBEBEB;
+  color:#EBEBEB;
 }
 a.nomobile {
-       background-color:transparent;
-       border:none;
+  background-color:transparent;
+  border:none;
 }
 a.green {
-       background-color:#40FF00;
-       border:none;
+  background-color: DarkGreen;
+  color: white !important;
+  border:none;
 }
 a.orange {
-       background-color:#FF8000;
-       border:none;
+  background-color:#FF8000;
+  border:none;
 }
 a.red {
-       background-color:#FF0000;
-       border:none;
+  background-color:#FF0000;
+  border:none;
 }
 
 table {
-    font-size: 90% !important;
-       table-layout: auto;
-       border-spacing: 0.3em;
+  font-size: 90% !important;
+  table-layout: auto;
+  border-spacing: 0.3em;
 }
 
-/* table a {
-       color:#FE5F14 !important;
-       border-bottom:none;
-} */
+hr {
+  background-color: #006400;
+  border: none;
+  color: #79B61B;
+  height: 2px;
+}
 
-ul {
+tr.rule-before th, tr.rule-before td {
+  padding-top: 2px;
+  border-top: 2px solid #EBEBEB;
 }
 
-hr {
-       background-color: #006400;
-       border: none;
-       color: #79B61B;
-       height: 2px;
-}
-
-/* I.E. & Chrome können das nicht! */
-/* input[type="radio"], input[type="checkbox"]{
-       width:1.15em;
-       height:1.15em;
-       border:1px solid;
-       color: #006400;
-} */
-input:focus, textarea:focus, select:focus {
-       background-color: #FFFFA0;
-       border: 2px solid #FE5F14;
-       /* border-bottom: medium solid #FE5F14; */
-}
-/* Fängt den "Schrink" beim focus - problem für i.e. und chrome */
-/* input[type="radio"]:focus, input[type="checkbox"]:focus{
-       width:1.2em;
-       height:1.2em;
-} */
 td {
-       color: #000000;
-       font-weight: normal;
+  color: #000000;
+  font-weight: normal;
 }
-/* td.hover:hover {
-       color: #006400;
-       background-color: #FFFFE0;
-} */
 th {
-       color: #000000;
-       font-weight: bold;
+  color: #000000;
+  font-weight: bold;
 }
 /* login and admin */
 a.no-underlined-links, a.no-underlined-links:visited, a.no-underlined-links:hover {
-       text-decoration: none !important;
-       background-color:transparent !important;
-       border:none;
+  text-decoration: none !important;
+  background-color:transparent !important;
+  border:none;
 }
 a.no-underlined-links:hover {
-       background: none;
+  background: none;
 }
 body.login {
-       background-color: #FFFFE0;
-       color: #000000;
+  background-color: #FFFFE0;
+  color: #000000;
 }
 .login h1 {
   text-align: center;
-  font-size: 20px;
+  font-size: 150%;
 }
 table.login {
-       background-color: #FFFFE0;
-       padding: 20px;
+  background-color: #FFFFE0;
+  padding: 20px;
   width: 500px;
 }
 td.login {
-       text-align: center;
+  text-align: center;
 }
 th.login {
-       text-align: right;
+  text-align: right;
 }
 .admin h1 {
-       background-color: #fe5f14;
+  background-color: #fe5f14;
   text-color: #ffffff;
 }
 body.menu {
-       color: #000000;
+  color: #000000;
 }
 /* Warnings */
 .message_error_login {
-       color: #000000;
-       border: 1px solid #8b0000;
-       background-color: #ffcccc;
-       padding: 3px;
+  color: #000000;
+  border: 1px solid #8b0000;
+  background-color: #ffcccc;
+  padding: 3px;
 }
 .message_ok {
-       padding: 5px;
-       background-color: #ADFFB6;
-       color: black;
-       font-weight: bolder;
-       text-align: center;
-       border-style: solid;
-       border-width: thin;
+  padding: 5px;
+  background-color: #ADFFB6;
+  color: black;
+  font-weight: bolder;
+  text-align: center;
+  border-style: solid;
+  border-width: thin;
 }
 .message_error {
-       padding: 5px;
-       background-color: #CC0000;
-       color: white;
-       font-weight: bolder;
-       text-align: center;
-       border-style: solid;
-       border-width: thin;
+  padding: 5px;
+  background-color: #CC0000;
+  color: white;
+  font-weight: bolder;
+  text-align: center;
+  border-style: solid;
+  border-width: thin;
 }
 .message_hint {
-       padding: 0.5em;
-       background-color: #FFEE66;
-       color: black;
-       font-weight: bolder;
-       text-align: center;
-       border-style: solid;
-       border-width: thin;
+  padding: 0.5em;
+  background-color: #FFEE66;
+  color: black;
+  font-weight: bolder;
+  text-align: center;
+  border-style: solid;
+  border-width: thin;
 }
 .message_error_label {
-       padding: 0.5em;
-       background-color: #E00000;
+  padding: 0.5em;
+  background-color: #E00000;
   color: white;
-       font-weight: normal;
-       text-align: left;
-       border-style: solid;
-       border-width: thin;
+  font-weight: normal;
+  text-align: left;
+  border-style: solid;
+  border-width: thin;
 }
-/*    Headings */
+/* Headings */
 .listtop, h1 {
-    font-size:125%;
-       background-color: #006400;
-       text-align: left;
-       padding: 0.5em;
-       color: #FFFFFF;
-       font-weight: bolder;
-       border-style: none;
-       border-width: thin;
-       -moz-border-radius:0.4em; /* Firefox */
-       -webkit-border-radius:0.4em; /* Safari, Chrome */
-       -khtml-border-radius:0.4em; /* Konqueror */
-       border-radius:0.4em; /* CSS3 */
-       behavior:url(border-radius.htc);
-}
-
-/* .listelement {
-       background-color: #f8ffb3;
-       color: #000000;
-}
-.listelement2 {
-       background-color: #8ee085;
-       color: #000000;
-} */
-.listheading, #content h2 {
-       padding: 0.2em;
-       background-color: #EBEBEB;
-       color: #006400;
-       font-weight: bolder;
-       text-align: left;
-       border-style: none;
+  background-color: #006400;
+  text-align: left;
+  padding: 0.5em;
+  color: #FFFFFF;
+  font-size:100%;
+  font-weight: bolder;
+  border-style: none;
+  border-width: thin;
+  -moz-border-radius:0.4em; /* Firefox */
+  -webkit-border-radius:0.4em; /* Safari, Chrome */
+  -khtml-border-radius:0.4em; /* Konqueror */
+  border-radius:0.4em; /* CSS3 */
+  behavior:url(border-radius.htc);
 }
 
-/* .listheadingcontent {
-       background-color: #EBEBEB;
-       color: #006400;
-       font-weight: bolder;
-       text-align: left;
-} */
+.listheading, #content h2 {
+  padding: 0.2em;
+  background-color: #EBEBEB;
+  color: #006400;
+  font-size: 95%;
+  font-weight: bolder;
+  text-align: left;
+  border-style: none;
+}
 
 .accountlistheading {
-       padding: 0.3em;
-       color: #006400;
-       font-weight: bold;
-       text-align: left;
-       background-color: #EBEBEB;
+  padding: 0.3em;
+  color: #006400;
+  font-weight: bold;
+  text-align: left;
+  background-color: #EBEBEB;
 }
 .subsubheading {
-       color: #000000;
-       font-weight: bolder;
-       text-decoration: underline;
+  color: #000000;
+  font-weight: bolder;
+  text-decoration: underline;
 }
 .optionen {
-       border: dashed;
-       border-width: 1px;
-       background: #FFFFE0;
+  border: dashed;
+  border-width: 1px;
+  background: #FFFFE0;
 }
 .listrow1, .listrow:nth-child(even) {
-       background-color: #FFFFFF;
-       color: black;
-       vertical-align: top;
+  background-color: #FFFFFF;
+  color: black;
+  vertical-align: top;
 }
 .listrow0, .listrow:nth-child(odd) {
-       background-color: #FFFF99;
-       color: black;
-       vertical-align: top;
+  background-color: #FFFF99;
+  color: black;
+  vertical-align: top;
 }
 .listrow_error1, .listrow_error:nth-child(even) {
-       background-color: #F6CECE;
-       color: black;
-       vertical-align: top;
+  background-color: #F6CECE;
+  color: black;
+  vertical-align: top;
 }
 .listrow_error0, .listrow_error:nth-child(odd) {
-       background-color: #F5A9A9;
-       color: black;
-       vertical-align: top;
+  background-color: #F5A9A9;
+  color: black;
+  vertical-align: top;
 }
 .listrowempty {
-       background-color: #FFFFFF;
-       color: black;
-       vertical-align: top;
+  background-color: #FFFFFF;
+  color: black;
+  vertical-align: top;
 }
 .listsubtotal {
-       background-color: rgb(236,233,216);
-       color: black;
-       font-weight: bolder;
+  background-color: rgb(236,233,216);
+  color: black;
+  font-weight: bolder;
 }
 .listtotal, .listtotal td {
-       background-color: rgb(236,233,216);
-       color: black;
-       font-weight: bolder;
+  background-color: rgb(236,233,216);
+  color: black;
+  font-weight: bolder;
 }
 /* Verkaufsbericht */
 .listmainsortheader {
-       background-color: rgb(236,233,216);
-       color: red;
-       font-weight: bolder;
-       padding-left: 10px;
-       padding-top: 0px;
+  background-color: rgb(236,233,216);
+  color: red;
+  font-weight: bolder;
+  padding-left: 10px;
+  padding-top: 0px;
 }
 .listmainsortsubtotal {
-       background-color: rgb(236,233,216);
-       color: red;
-       font-weight: bolder;
-       padding-left: 10px;
+  background-color: rgb(236,233,216);
+  color: red;
+  font-weight: bolder;
+  padding-left: 10px;
 }
 .listsubsortheader {
-       background-color: rgb(236,233,216);
-       color: green;
-       font-weight: bolder;
-       padding-left: 20px
+  background-color: rgb(236,233,216);
+  color: green;
+  font-weight: bolder;
+  padding-left: 20px
 }
 .listsubsortsubtotal {
-       background-color: rgb(236,233,216);
-       color: green;
-       font-weight: bolder;
-       padding-left: 20px
+  background-color: rgb(236,233,216);
+  color: green;
+  font-weight: bolder;
+  padding-left: 20px
 }
 .listsortdescription {
-       background-color: rgb(236,233,216);
-       color: black;
-       font-weight: normal;
-       padding-left: 30px
+  background-color: rgb(236,233,216);
+  color: black;
+  font-weight: normal;
+  padding-left: 30px
 }
 .submit {
-       font-family: Verdana, Arial, Helvetica;
-       color: #000000;
+  color: #000000;
 }
 .checkbox, .radio {
-       font-family: Verdana, Arial, Helvetica;
-       color: #778899;
+  color: #778899;
 }
 .plus0 {
-/* font color for negative numbers */
-       color: red;
+  /* font color for negative numbers */
+  color: red;
 }
 .plus1 {
-       color: green;
+  color: green;
 }
 h2.confirm {
-       color: blue;
+  color: blue;
 }
 h2.error {
-       color: red;
+  color: red;
 }
 fieldset {
-       margin-top: 15px;
-       color: black;
-       font-weight: bolder;
+  margin-top: 15px;
+  color: black;
+  font-weight: bolder;
 }
 .filecontent {
-       border: 1px solid blue;
-       padding-left: 2px;
-       padding-right: 2px;
+  border: 1px solid blue;
+  padding-left: 2px;
+  padding-right: 2px;
 }
 label {
-       cursor: pointer;
-       vertical-align: top;
+  cursor: pointer;
+  vertical-align: top;
 }
 .unbalanced_ledger {
-       background-color: #ffa0a0;
+  background-color: #ffa0a0;
 }
 .flash_message_error {
-       background-color: #FFD6D6;
-       border: 1px solid #AE0014;
-       margin-top: 5px;
-       margin-bottom: 5px;
-       padding: 5px;
+  background-color: #FFD6D6;
+  border: 1px solid #AE0014;
+  margin-top: 5px;
+  margin-bottom: 5px;
+  padding: 5px;
 }
 .flash_message_ok {
-       background-color: #ADFFB6;
-       border: 1px solid #007F0F;
-       margin-top: 5px;
-       margin-bottom: 5px;
-       padding: 5px;
+  background-color: #ADFFB6;
+  border: 1px solid #007F0F;
+  margin-top: 5px;
+  margin-bottom: 5px;
+  padding: 5px;
 }
 .flash_message_warning {
-       background-color: #FFE8C7;
-       border: 1px solid #FF6600;
-       margin-top: 5px;
-       margin-bottom: 5px;
-       padding: 5px;
+  background-color: #FFE8C7;
+  border: 1px solid #FF6600;
+  margin-top: 5px;
+  margin-bottom: 5px;
+  padding: 5px;
 }
 .flash_message_info {
-       background-color: #DCF2FF;
-       border: 1px solid #4690FF;
-       margin-top: 5px;
-       margin-bottom: 5px;
-       padding: 5px;
+  background-color: #DCF2FF;
+  border: 1px solid #4690FF;
+  margin-top: 5px;
+  margin-bottom: 5px;
+  padding: 5px;
 }
 
 .flash_title {
@@ -386,12 +408,19 @@ label {
   margin-right: 6px;
 }
 
-.part_picker {
-  padding-right: 16px;
-}
-.chart_picker {
-  padding-right: 16px;
-}
+.chart_picker,
+.part_picker,
+.project_picker,
+  display: inline-block;
+}
+.chart_picker:before,
+.part_picker:before,
+.project_picker:before {
+  display: inline-block;
+  vertical-align: middle;
+  height: 100%;
+}
+.kivi-validator-invalid,
 .customer-vendor-picker-undefined,
 .chartpicker-undefined,
 .projectpicker-undefined,
@@ -400,6 +429,7 @@ label {
   font-style: italic;
 }
 
+div.project_picker_project,
 div.part_picker_part,
 div.chart_picker_chart {
   padding: 5px;
@@ -413,28 +443,51 @@ div.chart_picker_chart {
   background-color: white;
   cursor: pointer;
 }
+div.project_picker_project:hover,
 div.part_picker_part:hover,
 div.chart_picker_chart:hover {
-  background-color: #CCCCCC;
   color: #FE5F14;
-  border-color: gray;
 }
 
+div.cpc_block,
 div.ppp_block {
   overflow:hidden;
   float:left;
   width: 350px;
 }
-/* div.cpc_block { */
-/*   overflow:hidden; */
-/*   float:left; */
-/*   width: 350px; */
-/* } */
+span.cpc_popup_button,
+span.ppp_popup_button {
+  display: inline-block;
+  vertical-align: middle;
+  margin-left: -24px;
+  height: 20px;
+  width: 20px;
+  cursor: pointer;
+  background: url("../../image/search.svg") no-repeat center right;
+  background-size: contain;
+}
+span.chart_picker input,
+span.part_picker input,
+span.project_picker input {
+  padding-right: 20px;
+  box-sizing: padding-box;
+  -moz-box-sizing: padding-box;
+  -webkit-box-sizing: padding-box;
+}
+span.chart_picker,
+span.part_picker,
+span.project_picker {
+  white-space: nowrap;
+}
 div.ppp_block span.ppp_block_number,
 div.cpc_block span.cpc_block_number
 {
   float:left;
 }
+div.ppp_block span.ppp_block_ean {
+  float:left;
+  margin-left:1em;
+}
 div.ppp_block span.ppp_block_description {
   float:right;
   margin-left:1em;
@@ -445,6 +498,11 @@ div.cpc_block span.cpc_block_description {
   margin-left:1em;
   font-weight:bold;
 }
+div.ppp_line span.ppp_block_number,
+div.ppp_line span.ppp_block_ean {
+  float:left;
+  margin-left:1em;
+}
 div.ppp_line span.ppp_block_description,
 div.cpc_line span.cpc_block_description
 {
@@ -461,7 +519,6 @@ div.cpc_line span.cpc_block_second_row {
   display:none;
 }
 div.cpc_block span.cpc_block_second_row {
-  font-size:80%;
 }
 span.toggle_selected {
   font-weight: bold;
@@ -473,3 +530,115 @@ span.toggle_selected {
 .customer_dunning_level {
   font-weight: bold;
 }
+
+#expand_all, .expand {
+    cursor: pointer;
+    display: block;
+    max-width: 16px;
+    max-height: 16px;
+}
+#update_from_master {
+    cursor: pointer;
+    display: block;
+    max-width: 16px;
+    max-height: 16px;
+}
+#update_from_master:hover {
+    background: #ddd;
+}
+
+/* Bank transactions */
+#bank_transactions_proposals .invoice_number_highlight a,
+#bank_transactions_proposals span.invoice_number_highlight {
+  background-color: #006400;
+  color: #FFFFFF;
+}
+
+/* actionbar styling */
+div.layout-actionbar {
+  background-color: white;
+}
+
+div.layout-actionbar div.layout-actionbar-link,
+div.layout-actionbar div.layout-actionbar-submit,
+div.layout-actionbar div.layout-actionbar-scriptbutton,
+div.layout-actionbar div.layout-actionbar-link:focus,
+div.layout-actionbar div.layout-actionbar-submit:focus,
+div.layout-actionbar div.layout-actionbar-scriptbutton:focus {
+  border-color: darkgray;
+  background-color: whitesmoke;
+}
+
+div.layout-actionbar div.layout-actionbar-link:hover,
+div.layout-actionbar div.layout-actionbar-submit:hover,
+div.layout-actionbar div.layout-actionbar-scriptbutton:hover {
+  color: #FE5F14;
+}
+
+div.layout-actionbar div.layout-actionbar-action-disabled,
+div.layout-actionbar div.layout-actionbar-action-disabled:hover {
+  color: gray;
+  background-color: whitesmoke;
+  border-color: lightgray;
+}
+
+div.layout-actionbar-combobox div.layout-actionbar-combobox-head span {
+  border-color: darkgray;
+  background-color: whitesmoke;
+}
+
+div.layout-actionbar-combobox div.layout-actionbar-combobox-head span:hover {
+  color: #FE5F14;
+}
+
+div.layout-actionbar-combobox div.layout-actionbar-combobox-head span:after {
+  border-color: black transparent;
+}
+
+div.layout-actionbar-combobox div.layout-actionbar-combobox-head span:hover:after {
+  color: #FE5F14;
+  border-color: #FE5F14 transparent;
+}
+div.layout-actionbar .layout-actionbar-default-action {
+  font-weight: bold;
+}
+
+/* Admin section: the menu itself doesn't occupy space. So make room
+   at the top of the div covering the whole admin area. */
+body > div.admin {
+  padding-top: 24px;
+}
+
+/* cke editor */
+.cke_top {
+  padding: 0 !important;
+}
+.cke_toolgroup {
+  margin-bottom: 0 !important;
+  margin-top: 0 !important;
+}
+.cke_button {
+  padding: 0px; 6px !important;
+}
+
+/* selects with text filters */
+div.filtered_select input, div.filtered_select select {
+  display: block;
+}
+
+div.filtered_select input {
+  background-image: url(../../image/glass14x14.png);
+  background-repeat: no-repeat;
+  background-position: 2px 2px;
+  border-radius: 0px;
+  border: solid #a0a0a0 1px;
+  border-bottom: none;
+  padding: 0px;
+  padding-left: 20px;
+  margin: 0;
+  width: 500px;
+}
+
+div.filtered_select select {
+  width: 522px;
+}
index 1b004b7..ffeea2b 100644 (file)
@@ -4,22 +4,52 @@ orangemenu color #FFFFFF
 whiteMenu Hover background color #FFFFE0
 DarkGreensubenu pointer
 */
+/* #main_menu_div { */
+/*   background-color: #d0cfc9 */
+/* } */
+
+div.layout-actionbar ~ #content {
+  padding-top: 32px;
+}
+
+#main_menu_div ~ div.layout-actionbar {
+  top: 45px;
+}
+
+#main_menu_div ~ #content {
+  padding-top: 25px;
+}
+#main_menu_div ~ div.layout-actionbar ~ #content {
+  padding-top: 54px;
+}
+
 body.menuv3 {
        behavior: url("css/csshover.htc");
        /*font-size: 14pt;*/
        line-height: 20pt;
-       font-family: Verdana, Geneva, Tahoma, sans-serif;
        background-color: #FFFFFF;
        color: #000000;
 }
 #menuv3 {
-       /*font-size: 85%;*/
-       width: 99.8%;
-       float: left;
-       /*border: 3px solid;*/
        background-color: #FFFFFF;
        color: #000000;
+  width: 100%;
+  position: fixed;
+  z-index: 30;
+}
+
+#menuv3 ~ div.layout-actionbar {
+  top: 40px;
+}
+
+#menuv3  ~ #content {
+  padding-top: 35px;
+}
+
+#menuv3 ~ div.layout-actionbar ~ #content {
+  padding-top: 64px;
 }
+
 #menuv3 a, #menuv3 h2, #menuv3 div.x {
        font-size: 80%;
        line-height: 120%;
@@ -80,6 +110,7 @@ body.menuv3 {
        position: relative;
        float: none;
        border: 0;
+  border-width:0 0 1px 0;
 }
 li.sub {
        position: relativ;
@@ -106,7 +137,6 @@ li.sub {
 #menuv3 ul ul {
        position: absolute;
        z-index: 500;
-       top: auto;
        display: none;
 }
 #menuv3 ul ul ul {
@@ -126,7 +156,6 @@ div#menuv3 h2:hover {
 }
 div#menuv3 li:hover {
        cursor: pointer;
-       z-index: 100;
 }
 div#menuv3 li:hover ul ul, div#menuv3 li li:hover ul ul, div#menuv3 li li li:hover ul ul, div#menuv3 li li li li:hover ul ul {
        display: none;
@@ -149,16 +178,20 @@ li.sub {
    each line is a mi (menuitem) and has one mii (menu-item-icon) whcih is ms (menu-spacer)
    and one mic (menu-item-chunk)
    indenting is done with the levels s0, s1, s2 */
-#content.html-menu, #html-menu {
+#html-menu {
+  position: fixed;
+  overflow-y: scroll;
+  overflow-x: hidden;
+  height: 95%;
   transition:         margin-left 0.2s, width 0.2s;
   -moz-transition:    margin-left 0.2s, width 0.2s;
   -webkit-transition: margin-left 0.2s, width 0.2s;
   -o-transition:      margin-left 0.2s, width 0.2s;
 }
-#content.html-menu { margin-left: 190px; }
-#content.html-menu.folded { margin-left: 40px }
-#html-menu.folded:hover + #content.html-menu.folded { margin-left: 190px }
-#html-menu { float:left; width: 183px; font-size: 8pt; margin-top: 10px; overflow:hidden; }
+div.layout-split-right { margin-left: 190px; }
+div.layout-split-right.folded { margin-left: 40px }
+#html-menu.folded:hover + div.layout-split-right.folded { margin-left: 190px }
+#html-menu { float:left; width: 183px; font-size: 8pt; margin-top: 10px; }
 #html-menu.folded { width: 32px; }
 #html-menu.folded:hover { width: 183px; }
 #html-menu div.mi { margin-top: 4px; margin-bottom: 3px; white-space: nowrap; clear:both; position:relative; }
index d08f358..4a2bd64 100644 (file)
 ************************************************************************************************************/\r
 \r
 .DHTMLSuite_menuBar_top{       /* The bar that is parent of the menu strip */\r
-       color:#FFF;\r
-       height:26px;\r
-       width:100%;\r
-       background-repeat:repeat-x;\r
-       font-family: sans-serif, Verdana, Arial, Helvetica;\r
-       font-size:12px;\r
-       z-index:100000;\r
-       padding-left:10px;\r
-/*     background-image:url('../../../image/dhtmlsuite/menu_strip_bg.jpg');*/\r
-       background-image:url('../../../image/bg_css_menu.png');\r
+  position: fixed;\r
+  height: 26px;\r
+  width: 100%;\r
+  z-index: 100;\r
+  padding-left: 10px;\r
+  background-color: #d0cfc9;\r
 }\r
 \r
 .DHTMLSuite_menuBar_sub{\r
-       position:absolute;\r
-       background-color:#FFF;\r
-       border:1px solid #000;\r
-       background-image:url('../../../image/dhtmlsuite/menu-bar-gradient.jpg');        /* Background image for sub menu items */\r
-       background-repeat:repeat-y;\r
-       background-position: left center;\r
-       display:inline;\r
+  position: fixed;\r
+  background-color: #FFF;\r
+  border: 1px solid #000;\r
+  display: inline;\r
 }\r
 \r
index cf5d146..915615e 100644 (file)
@@ -2,27 +2,34 @@
 #frame-header .frame-header-element a:visited,
 #frame-header .frame-header-element a:hover,
 #frame-header .frame-header-element a:active {
-  color: white;
+  color: black;
   background: none;
   text-decoration: underline;
 }
 
 #frame-header {
-  background: url('../../../image/bg_titel.gif') repeat-x;
-  text-align: center;
+  background-color: #d0cfc9;
+  text-align: left;
   margin: 0;
-  padding: 0;
-  color: white;
+  color: black;
   border: 0;
   overflow: hidden;
-  min-height: 20px;
+  height: 20px;
   width: 100%;
   border-spacing: 0;
-  font-size: 12px;
+  position: fixed;
+  top: 0;
+  z-index: 40;
+}
+
+#frame-header + div {
+  padding-top: 20px;
 }
 
+
 #frame-header .frame-header-left {
   float: left;
+  margin-right: 10px;
 }
 #frame-header .frame-header-right {
   float: right;
 #frame-header .frame-header-center,
 #frame-header .frame-header-right  {
   border-spacing: 0;
-  color: white;
+  color: black;
   padding: 0;
-  font-family: verdana,arial,sans-serif;
   vertical-align: middle;
 }
 
 #frame-header .frame-header-right {
-  margin-top: 3px;
+  vertical-align: middle;
 }
 
 #frame-header #ajax-spinner {
-  margin-top: 2px;
-  margin-right: 10px;
   display: none;
   width: 16px;
   height: 16px;
@@ -53,5 +57,6 @@
 }
 
 #frame-header input {
-  font-size: 12px;
+  font-size: 100%;
+  margin-top: 1px;
 }
index cc66e7b..0d22e50 100644 (file)
@@ -15,8 +15,7 @@
   border-radius: 0;
   border: 0;
   color: #000000;
-  font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif;
-  font-size: 1.1em;
+  font-family: sans-serif;
   overflow: hidden;
 }
 
   border-style: dashed;
   border-width: thin;
   color: black;
-  font-size: 10pt;
   font-weight: bolder;
   margin: 5px;
   padding: 5px;
   position: relative;
   text-align:left;
 }
+/* show scrollbar vertical */
+.ui-tabs-panel {
+  overflow: scroll;
+}
index 552a901..a7c0c7b 100644 (file)
@@ -4,7 +4,6 @@
   background-color:rgb(236,233,216);
   text-align:left;
   padding:5px;
-  font-size:12pt;
   color:black;
   font-weight: bolder;
   border-style:dashed;
@@ -18,7 +17,6 @@
 }
 
 .coa_listheading_element {
-  font-size:10pt;
   padding:3px;
   font-weight:bolder;
   text-align:left;
@@ -44,7 +42,6 @@
 }
 
 .coa_account_header {
-  font-size:10pt;
   padding:3px;
   font-weight:bolder;
   text-align:left;
@@ -53,7 +50,6 @@
 }
 
 .coa_account_header_sc {
-  font-size:10pt;
   padding:3px;
   font-weight:bolder;
   text-align:left;
 }
 
 .coa_detail_emph {
-  font-size:10pt;
   font-weight:bold;
   color:darkred;
 }
 
 .coa_details_header {
-  font-size:8pt;
   padding:3px;
   font-weight:bolder;
   text-align:center;
@@ -83,7 +77,6 @@
 }
 
 .coa_details_header2 {
-  font-size:8pt;
   padding:3px;
   font-weight:normal;
   text-align:left;
index f6b1f16..0c604fd 100644 (file)
@@ -9,13 +9,8 @@ A:hover { color: black;
            background-color: lemonchiffon;
            text-decoration: none;
          }
-a, div {
-  transition: background-color 0.2s;
-  -moz-transition: background-color 0.2s;
-  -webkit-transition: background-color 0.2s;
-}
 
-input, textarea, select {
+input, textarea, select, div.cke_textarea_inline {
   border: 1px;
   border-color: darkgray lightgray lightgray;
   border-style: solid;
@@ -24,17 +19,22 @@ input, textarea, select {
 }
 
 select {
-  padding: 0px;
+  -moz-appearance: none;
+  -webkit-appearance: none;
+  -o-appearance: none;
+  appearance : none;
+  background: white url('../../image/select-down.png') no-repeat scroll right center;
+  padding: 0 14px 0 0;
 }
 
-input:focus, textarea:focus, select:focus {
+input:focus, textarea:focus, select:focus, div.cke_textarea_inline:focus {
   background-color: whitesmoke;
   border: 1px;
   border-color: gray lightgray lightgray;
   border-style: solid;
 }
 
-input:hover, textarea:hover, select:hover {
+input:hover, textarea:hover, select:hover, div.cke_textarea_inline:hover {
   border-color: dimgray darkgray darkgray;
 }
 
@@ -71,43 +71,28 @@ html {
 }
 
 body {
-  font-family: Verdana, Arial, Helvetica, sans-serif;
-  font-size: 10pt;
+  font-family: sans-serif;
+  font-size: 80%;
   background-color: white;
-  background-image: url("../../image/fade.png"); background-repeat:repeat-x;
   color: black;
   height: 100%;
 }
 
 td {
-  font-family: Verdana, Arial, Helvetica, sans-serif;
   color: black;
-  font-size: 8pt;
   font-weight: normal;
 }
 td.hover:hover {
-    color: black;
-/*          background-color: #FFFFCC;
-          font-size: 8pt;
-          text-decoration: none;
-          border:none;
-          borderWidth:0px;
-          borderColor:2557AD;
-*/
+  color: black;
 }
 
 
 th {
-  font-family: Verdana, Arial, Helvetica, sans-serif;
   color: black;
-  font-size: 8pt;
   font-weight: normal;
 }
 
 /* login and admin */
-.login {
-  font-family: Verdana, Arial, Helvetica, sans-serif;
-}
 div.login {
   min-height: 100%;
   height: auto !important;
@@ -115,8 +100,9 @@ div.login {
   background: #b8d1f3;
   color: #A0A0A0;
 }
-h1.login {
-  font-size: 18pt;
+.login h1 {
+  text-align: center;
+  font-size: 150%;
 }
 table.login {
   background-color: #efedde;
@@ -141,7 +127,6 @@ div.admin {
     padding: 3px;
 }
 .message_ok {
-    font-size: 12pt;
     padding:5px;
     background-color: #ADFFB6;
     color: black;
@@ -151,7 +136,6 @@ div.admin {
     border-width:thin;
 }
 .message_error {
-    font-size: 12pt;
     padding:5px;
     background-color: #FFAAAA;
     color: black;
@@ -161,7 +145,6 @@ div.admin {
     border-width:thin;
 }
 .message_hint {
-    font-size: 12pt;
     padding:5px;
     background-color: #FFFE66;
     color: black;
@@ -171,7 +154,6 @@ div.admin {
     border-width:thin;
 }
 .message_error_label {
-    font-size: 0.8em;
     padding:5px;
     background-color: #FEE;
     font-weight:normal;
@@ -185,9 +167,9 @@ div.admin {
 */
 .listtop, h1 {
     background-color: rgb(236,233,216);
+    font-size: 100%;
     text-align:left;
     padding:5px;
-    font-size: 10pt;
     color: black;
     font-weight: bolder;
     border-style:dashed;
@@ -210,12 +192,12 @@ div.admin {
 }
 
 .listheading, .listheading th, #content h2 {
-    font-size: 9pt;
+    font-size: 95%;
     padding:3px;
     background-color:
     rgb(236,233,216);
     color: black;
-    font-weight: bolder;
+    font-weight: bold;
     text-align:left;
     background-image: url("../../image/fade.png");
     border-style:dotted;
@@ -223,7 +205,6 @@ div.admin {
 }
 
 .listheadingcontent {
-    font-size: 9pt;
     background-color:
     rgb(236,233,216);
     color: black;
@@ -232,7 +213,6 @@ div.admin {
 }
 
 .accountlistheading {
-    font-size: 10pt;
     padding:3px;
     color: white;
     font-weight: bold;
@@ -263,30 +243,25 @@ div.admin {
 .listrow_error1, .listrow_error:nth-child(even) { background-color: #F6CECE; color: black; vertical-align: top; }
 .listrow_error0, .listrow_error:nth-child(odd) { background-color: #F5A9A9; color: black; vertical-align: top; }
 
-.redrow1 { background-color: rgb(250,167, 161); color: black; vertical-align: top; }
-.redrow0 { background-color: rgb(255,193,176); color: black; vertical-align: top; }
-
 .greenrow1 { background-color: rgb(0,250,0); color: black; vertical-align: top; }
 .greenrow0 { background-color: rgb(0,255,0); color: black; vertical-align: top; }
 
-.listsubtotal { font-size: 8pt; background-color: rgb(236,233,216); color: black; font-weight: bolder;}
+.listsubtotal { background-color: rgb(236,233,216); color: black; font-weight: bolder;}
 
-.listtotal, .listtotal td { font-size: 8pt; background-color: rgb(236,233,216); color: black; font-weight: bolder;}
+.listtotal, .listtotal td { background-color: rgb(236,233,216); color: black; font-weight: bolder;}
 
 /* Verkaufsbericht */
-.listmainsortheader { font-size: 8pt; background-color: rgb(236,233,216); color: red; font-weight: bolder; padding-left: 10px; padding-top: 0px;}
-.listmainsortsubtotal { font-size: 8pt; background-color: rgb(236,233,216); color: red; font-weight: bolder; padding-left: 10px;}
-.listsubsortheader { font-size: 8pt; background-color: rgb(236,233,216); color: green; font-weight: bolder; padding-left: 20px}
-.listsubsortsubtotal { font-size: 8pt; background-color: rgb(236,233,216); color: green; font-weight: bolder; padding-left: 20px}
-.listsortdescription { font-size: 8pt; background-color: rgb(236,233,216); color: black; font-weight: normal; padding-left: 30px}
+.listmainsortheader { background-color: rgb(236,233,216); color: red; font-weight: bolder; padding-left: 10px; padding-top: 0px;}
+.listmainsortsubtotal { background-color: rgb(236,233,216); color: red; font-weight: bolder; padding-left: 10px;}
+.listsubsortheader { background-color: rgb(236,233,216); color: green; font-weight: bolder; padding-left: 20px}
+.listsubsortsubtotal { background-color: rgb(236,233,216); color: green; font-weight: bolder; padding-left: 20px}
+.listsortdescription { background-color: rgb(236,233,216); color: black; font-weight: normal; padding-left: 30px}
 
 
 .submit {
-  font-family: Verdana, Arial, Helvetica, sans-serif;
   color: #000000;
 }
 .checkbox, .radio {
-  font-family: Verdana, Arial, Helvetica, sans-serif;
   color: #778899;
 }
 
@@ -300,12 +275,10 @@ div.admin {
 
 h2.confirm {
   color: blue;
-  font-size: 14pt;
 }
 
 h2.error {
   color: red;
-  font-size: 14pt;
 }
 
 fieldset {
@@ -372,13 +345,11 @@ label {
 }
 
 .coa_detail_emph {
-  font-size:10pt;
   font-weight:bold;
   color:darkred;
 }
 
 .coa_details_header {
-  font-size:8pt;
   padding:3px;
   font-weight:bolder;
   text-align:center;
@@ -387,7 +358,6 @@ label {
 }
 
 .coa_details_header2 {
-  font-size:8pt;
   padding:3px;
   font-weight:normal;
   text-align:left;
@@ -405,12 +375,11 @@ label {
   margin-right: 6px;
 }
 
-.part_picker {
-  padding-right: 16px;
-}
-.chart_picker {
-  padding-right: 16px;
+.chart_picker,
+.part_picker,
+.project_picker {
 }
+.kivi-validator-invalid,
 .customer-vendor-picker-undefined,
 .chartpicker-undefined,
 .projectpicker-undefined,
@@ -419,7 +388,8 @@ label {
   font-style: italic;
 }
 div.part_picker_part,
-div.chart_picker_chart {
+div.chart_picker_chart,
+div.project_picker_project {
   padding: 5px;
   margin: 5px;
   border: 1px;
@@ -432,16 +402,64 @@ div.chart_picker_chart {
   cursor: pointer;
 }
 div.part_picker_part:hover,
-div.chart_picker_chart:hover {
+div.chart_picker_chart:hover,
+div.project_picker_project:hover {
   background-color: lightgray;
   border-color: gray;
 }
 
+div.cpc_block,
 div.ppp_block {
   overflow:hidden;
   float:left;
   width: 350px;
 }
+span.cpc_popup_button,
+span.ppp_popup_button {
+  display: inline-block;
+  position: relative;
+  margin-left: -18px;
+  margin-top: 3px;
+  height: 16px;
+  width: 16px;
+  cursor: pointer;
+}
+
+td span.cpc_popup_button,
+th span.cpc_popup_button,
+td span.ppp_popup_button,
+th span.ppp_popup_button {
+  height: 9px;
+  width: 9px;
+  margin-left: -13px;
+}
+span.chart_picker input,
+span.part_picker input,
+span.project_picker input {
+  padding-right: 20px;
+  background: white url("../../image/search.svg") no-repeat center right;
+  background-size: contain;
+  box-sizing: padding-box;
+  -moz-box-sizing: padding-box;
+  -webkit-box-sizing: padding-box;
+}
+
+td span.chart_picker input,
+th span.chart_picker input,
+td span.part_picker input,
+th span.part_picker input,
+td span.project_picker input,
+th span.project_picker input {
+
+  padding-right: 15px;
+}
+
+span.chart_picker,
+span.part_picker,
+span.project_picker {
+ /* white-space: nowrap;*/
+}
+
 div.ppp_block span.ppp_block_number,
 div.cpc_block span.cpc_block_number
 {
@@ -451,11 +469,21 @@ div.ppp_block span.ppp_block_description {
   float:right;
   font-weight:bold;
 }
+div.ppp_block span.ppp_block_ean {
+  float:left;
+  margin-left:1em;
+}
 div.cpc_block span.cpc_block_description {
   float:left;
   margin-left:1em;
   font-weight:bold;
 }
+div.ppp_line span.ppp_block_number,
+div.ppp_line span.ppp_block_ean {
+  float:left;
+  margin-left:1em;
+}
+
 div.ppp_line span.ppp_block_description,
 div.cpc_line span.cpc_block_description
 {
@@ -475,7 +503,6 @@ div.cpc_line span.cpc_block_second_row {
   display:none;
 }
 div.cpc_block span.cpc_block_second_row {
-  font-size:80%;
 }
 span.toggle_selected {
   font-weight: bold;
@@ -487,3 +514,131 @@ span.toggle_selected {
 .customer_dunning_level {
   font-weight: bold;
 }
+a.green {
+      background-color: DarkGreen;
+      color: white !important;
+      border:none;
+}
+a.orange {
+       background-color:#FF8000;
+       border:none;
+}
+a.red {
+       background-color:#FF0000;
+       border:none;
+}
+
+#expand_all, .expand {
+    cursor: pointer;
+    display: block;
+    max-width: 16px;
+    max-height: 16px;
+}
+#update_from_master {
+    cursor: pointer;
+    display: block;
+    max-width: 16px;
+    max-height: 16px;
+}
+#update_from_master:hover {
+    background: darkgrey;
+}
+
+/* Bank transactions */
+#bank_transactions_proposals .invoice_number_highlight a,
+#bank_transactions_proposals span.invoice_number_highlight {
+  background-color: #006400;
+  color: #FFFFFF;
+
+}
+
+/* actionbar styling */
+div.layout-actionbar {
+  background-color: #d0cfc9;
+}
+
+div.layout-actionbar div.layout-actionbar-link,
+div.layout-actionbar div.layout-actionbar-submit,
+div.layout-actionbar div.layout-actionbar-scriptbutton,
+div.layout-actionbar div.layout-actionbar-link:focus,
+div.layout-actionbar div.layout-actionbar-submit:focus,
+div.layout-actionbar div.layout-actionbar-scriptbutton:focus {
+  border-color: darkgray;
+  background-color: whitesmoke;
+  -webkit-border-radius: 2px;
+  -moz-border-radius: 2px;
+  border-radius: 2px;
+}
+
+div.layout-actionbar div.layout-actionbar-link:hover,
+div.layout-actionbar div.layout-actionbar-submit:hover,
+div.layout-actionbar div.layout-actionbar-scriptbutton:hover {
+  background-color: lightgray;
+  border-color: gray;
+  -webkit-border-radius: 2px;
+  -moz-border-radius: 2px;
+  border-radius: 2px;
+}
+
+div.layout-actionbar div.layout-actionbar-action-disabled,
+div.layout-actionbar div.layout-actionbar-action-disabled:hover {
+  color: gray;
+  background-color: whitesmoke;
+  border-color: lightgray;
+}
+
+div.layout-actionbar-combobox div.layout-actionbar-combobox-head span {
+  border-color: darkgray;
+  background-color: whitesmoke;
+  -webkit-border-top-right-radius: 2px;
+  -webkit-border-bottom-right-radius: 2px;
+  -moz-border-radius-topright: 2px;
+  -moz-border-radius-bottomright: 2px;
+  border-top-right-radius: 2px;
+  border-bottom-right-radius: 2px;
+}
+
+div.layout-actionbar-combobox div.layout-actionbar-combobox-head span:hover {
+  background-color: lightgray;
+}
+
+div.layout-actionbar-combobox div.layout-actionbar-combobox-head span:after {
+  border-color: black transparent;
+}
+div.layout-actionbar .layout-actionbar-default-action {
+  font-weight: bold;
+}
+
+/* cke editor */
+.cke_top {
+  padding: 0 !important;
+}
+.cke_toolgroup {
+  margin-bottom: 0 !important;
+  margin-top: 0 !important;
+}
+.cke_button {
+  padding: 0px; 6px !important;
+}
+
+/* selects with text filters */
+div.filtered_select input, div.filtered_select select {
+  display: block;
+}
+
+div.filtered_select > input {
+  background-image: url(../../image/glass14x14.png);
+  background-repeat: no-repeat;
+  background-position: 2px 2px;
+  border-radius: 0px;
+  border: solid #a0a0a0 1px;
+  border-bottom: none;
+  padding: 0px;
+  padding-left: 20px;
+  margin: 0;
+  width: 500px;
+}
+
+div.filtered_select select {
+  width: 522px;
+}
index 5ebbd85..156c57f 100644 (file)
@@ -1,23 +1,19 @@
 
 body.menu {
-  background-image: url("../../image/fade.png");background-repeat:repeat-x;
-  font-family: Verdana, Arial, Helvetica, sans-serif;
-  font-size:8pt;
+  font-size: 80%;
   color: black;
 }
 
 table.menunew {
   border: 0;
   width: 100%;
-  background-image: url("../../image/bg_titel.gif");
+  background-color: #f0efde;
   border-spacing: 0;
 }
 
 table.menunew td {
   padding: 0;
-  color:white;
-  font-family: Verdana, Arial, sans-serif;
-  font-size: 12px;
+  color:black;
 }
 
 body.menunew {
@@ -25,16 +21,46 @@ body.menunew {
   margin:0px;
 }
 
+div.layout-actionbar ~ #content {
+  padding-top: 32px;
+}
+
+#main_menu_div {
+  background-color: #d0cfc9
+}
+
+#main_menu_div ~ div.layout-actionbar {
+  top: 45px;
+}
+
+#main_menu_div ~ #content {
+  padding-top: 25px;
+}
+#main_menu_div ~ div.layout-actionbar ~ #content {
+  padding-top: 54px;
+}
+
 #menuv3 {
-width:99.8%;
-float:left;
-background:url(../../image/bg_css_menu.png) repeat bottom;
-border:1px solid;
-border-color:#ccc #888 #555 #bbb;
+  width: 100%;
+  position: fixed;
+  background-color: #d0cfc9;
+  border-color:#ccc #888 #555 #bbb;
+  z-index: 30;
+}
+
+#menuv3 ~ div.layout-actionbar {
+  top: 40px;
+}
+#menuv3  ~ #content {
+  padding-top: 35px;
+}
+#menuv3 ~ div.layout-actionbar ~ #content {
+  padding-top: 64px;
 }
 
 #menuv3 a, #menuv3 h2, #menuv3 div.x {
-font:11px/16px arial,helvetica,sans-serif;
+font-size:13px;
+font-weight: normal;
 display:block;
 border:0;
 border-right:1px;
@@ -46,7 +72,7 @@ padding:1px 0 1px 3px;
 }
 
 #menuv3 h2 {
-color:#fff;
+color: black;
 padding:2px 10px;
 }
 
@@ -64,13 +90,14 @@ background:#eee url(../../image/right.gif) no-repeat right;
 }
 
 #menuv3 a:hover, #menuv3 div.x:hover {
-color:#a00;
-background-color:#ddd;
+/*color:#a00;
+background-color:#ddd;*/
+  background-color: #c6c39b;
 }
 
 #menuv3 a:active, #menuv3 div.x:active {
-color:#060;
-background-color:#ccc;
+color:#000;
+background-color:#c6c39b;
 }
 
 #menuv3 ul {
@@ -84,6 +111,7 @@ float:left;
 position:relative;
 float:none;
 border:0;
+border-width:0 0 1px 0;
 }
 
 /* IE6 spacing bug fix, <li>s without a bottom border get spaced to far
@@ -106,8 +134,7 @@ border-width:0 0 1px 0;
  * causing the menu to close. Opera 9 has the same bug btw. */
 #menuv3 ul ul {
 position:absolute;
-z-index:500;
-top:auto;
+z-index: 500;
 display:none;
 }
 
@@ -125,13 +152,12 @@ improves IE's performance speed to use the older
 file and this method */
 
 div#menuv3 h2:hover {
-background:#A3C5FF;
-color:#a00;
+background:#c6c39b;
+color:#000;
 }
 
 div#menuv3 li:hover {
 cursor:pointer;
-z-index:100;
 }
 
 div#menuv3 li:hover ul ul,
@@ -153,16 +179,21 @@ div#menuv3 li li li li:hover ul
    each line is a mi (menuitem) and has one mii (menu-item-icon) whcih is ms (menu-spacer)
    and one mic (menu-item-chunk)
    indenting is done with the levels s0, s1, s2 */
-#content.html-menu, #html-menu {
+#html-menu {
+  position: fixed;
+  overflow-y: scroll;
+  overflow-x: hidden;
+  height: 95%;
   transition:         margin-left 0.2s, width 0.2s;
   -moz-transition:    margin-left 0.2s, width 0.2s;
   -webkit-transition: margin-left 0.2s, width 0.2s;
   -o-transition:      margin-left 0.2s, width 0.2s;
 }
-#content.html-menu { margin-left: 190px; }
-#content.html-menu.folded { margin-left: 40px }
-#html-menu.folded:hover + #content.html-menu.folded { margin-left: 190px }
-#html-menu { float:left; width: 183px; font-size: 8pt; margin-top: 10px; overflow:hidden; }
+
+div.layout-split-right { margin-left: 190px; height: 100%; }
+div.layout-split-right.folded { margin-left: 40px }
+#html-menu.folded:hover + #content.layout-split-right.folded { margin-left: 190px }
+#html-menu { float:left; width: 183px; font-size: 85%; margin-top: 10px; }
 #html-menu.folded { width: 32px; }
 #html-menu.folded:hover { width: 183px; }
 #html-menu div.mi { margin-top: 4px; margin-bottom: 3px; white-space: nowrap; clear:both; position:relative; }
diff --git a/css/material/icons.css b/css/material/icons.css
new file mode 100644 (file)
index 0000000..c2e41d7
--- /dev/null
@@ -0,0 +1,20 @@
+@font-face {
+  font-family: 'Material Icons';
+  font-style: normal;
+  font-weight: 400;
+  src: url("icons.ttf") format('truetype');
+}
+
+.material-icons {
+  font-family: 'Material Icons';
+  font-weight: normal;
+  font-style: normal;
+  font-size: 24px;
+  line-height: 1;
+  letter-spacing: normal;
+  text-transform: none;
+  display: inline-block;
+  white-space: nowrap;
+  word-wrap: normal;
+  direction: ltr;
+}
diff --git a/css/material/icons.ttf b/css/material/icons.ttf
new file mode 100644 (file)
index 0000000..62ecae7
Binary files /dev/null and b/css/material/icons.ttf differ
diff --git a/css/material/materialize.css b/css/material/materialize.css
new file mode 120000 (symlink)
index 0000000..116f061
--- /dev/null
@@ -0,0 +1 @@
+materialize.min.css
\ No newline at end of file
diff --git a/css/material/materialize.min.css b/css/material/materialize.min.css
new file mode 100644 (file)
index 0000000..74b1741
--- /dev/null
@@ -0,0 +1,13 @@
+/*!\r
+ * Materialize v1.0.0 (http://materializecss.com)\r
+ * Copyright 2014-2017 Materialize\r
+ * MIT License (https://raw.githubusercontent.com/Dogfalo/materialize/master/LICENSE)\r
+ */\r
+.materialize-red{background-color:#e51c23 !important}.materialize-red-text{color:#e51c23 !important}.materialize-red.lighten-5{background-color:#fdeaeb !important}.materialize-red-text.text-lighten-5{color:#fdeaeb !important}.materialize-red.lighten-4{background-color:#f8c1c3 !important}.materialize-red-text.text-lighten-4{color:#f8c1c3 !important}.materialize-red.lighten-3{background-color:#f3989b !important}.materialize-red-text.text-lighten-3{color:#f3989b !important}.materialize-red.lighten-2{background-color:#ee6e73 !important}.materialize-red-text.text-lighten-2{color:#ee6e73 !important}.materialize-red.lighten-1{background-color:#ea454b !important}.materialize-red-text.text-lighten-1{color:#ea454b !important}.materialize-red.darken-1{background-color:#d0181e !important}.materialize-red-text.text-darken-1{color:#d0181e !important}.materialize-red.darken-2{background-color:#b9151b !important}.materialize-red-text.text-darken-2{color:#b9151b !important}.materialize-red.darken-3{background-color:#a21318 !important}.materialize-red-text.text-darken-3{color:#a21318 !important}.materialize-red.darken-4{background-color:#8b1014 !important}.materialize-red-text.text-darken-4{color:#8b1014 !important}.red{background-color:#F44336 !important}.red-text{color:#F44336 !important}.red.lighten-5{background-color:#FFEBEE !important}.red-text.text-lighten-5{color:#FFEBEE !important}.red.lighten-4{background-color:#FFCDD2 !important}.red-text.text-lighten-4{color:#FFCDD2 !important}.red.lighten-3{background-color:#EF9A9A !important}.red-text.text-lighten-3{color:#EF9A9A !important}.red.lighten-2{background-color:#E57373 !important}.red-text.text-lighten-2{color:#E57373 !important}.red.lighten-1{background-color:#EF5350 !important}.red-text.text-lighten-1{color:#EF5350 !important}.red.darken-1{background-color:#E53935 !important}.red-text.text-darken-1{color:#E53935 !important}.red.darken-2{background-color:#D32F2F !important}.red-text.text-darken-2{color:#D32F2F !important}.red.darken-3{background-color:#C62828 !important}.red-text.text-darken-3{color:#C62828 !important}.red.darken-4{background-color:#B71C1C !important}.red-text.text-darken-4{color:#B71C1C !important}.red.accent-1{background-color:#FF8A80 !important}.red-text.text-accent-1{color:#FF8A80 !important}.red.accent-2{background-color:#FF5252 !important}.red-text.text-accent-2{color:#FF5252 !important}.red.accent-3{background-color:#FF1744 !important}.red-text.text-accent-3{color:#FF1744 !important}.red.accent-4{background-color:#D50000 !important}.red-text.text-accent-4{color:#D50000 !important}.pink{background-color:#e91e63 !important}.pink-text{color:#e91e63 !important}.pink.lighten-5{background-color:#fce4ec !important}.pink-text.text-lighten-5{color:#fce4ec !important}.pink.lighten-4{background-color:#f8bbd0 !important}.pink-text.text-lighten-4{color:#f8bbd0 !important}.pink.lighten-3{background-color:#f48fb1 !important}.pink-text.text-lighten-3{color:#f48fb1 !important}.pink.lighten-2{background-color:#f06292 !important}.pink-text.text-lighten-2{color:#f06292 !important}.pink.lighten-1{background-color:#ec407a !important}.pink-text.text-lighten-1{color:#ec407a !important}.pink.darken-1{background-color:#d81b60 !important}.pink-text.text-darken-1{color:#d81b60 !important}.pink.darken-2{background-color:#c2185b !important}.pink-text.text-darken-2{color:#c2185b !important}.pink.darken-3{background-color:#ad1457 !important}.pink-text.text-darken-3{color:#ad1457 !important}.pink.darken-4{background-color:#880e4f !important}.pink-text.text-darken-4{color:#880e4f !important}.pink.accent-1{background-color:#ff80ab !important}.pink-text.text-accent-1{color:#ff80ab !important}.pink.accent-2{background-color:#ff4081 !important}.pink-text.text-accent-2{color:#ff4081 !important}.pink.accent-3{background-color:#f50057 !important}.pink-text.text-accent-3{color:#f50057 !important}.pink.accent-4{background-color:#c51162 !important}.pink-text.text-accent-4{color:#c51162 !important}.purple{background-color:#9c27b0 !important}.purple-text{color:#9c27b0 !important}.purple.lighten-5{background-color:#f3e5f5 !important}.purple-text.text-lighten-5{color:#f3e5f5 !important}.purple.lighten-4{background-color:#e1bee7 !important}.purple-text.text-lighten-4{color:#e1bee7 !important}.purple.lighten-3{background-color:#ce93d8 !important}.purple-text.text-lighten-3{color:#ce93d8 !important}.purple.lighten-2{background-color:#ba68c8 !important}.purple-text.text-lighten-2{color:#ba68c8 !important}.purple.lighten-1{background-color:#ab47bc !important}.purple-text.text-lighten-1{color:#ab47bc !important}.purple.darken-1{background-color:#8e24aa !important}.purple-text.text-darken-1{color:#8e24aa !important}.purple.darken-2{background-color:#7b1fa2 !important}.purple-text.text-darken-2{color:#7b1fa2 !important}.purple.darken-3{background-color:#6a1b9a !important}.purple-text.text-darken-3{color:#6a1b9a !important}.purple.darken-4{background-color:#4a148c !important}.purple-text.text-darken-4{color:#4a148c !important}.purple.accent-1{background-color:#ea80fc !important}.purple-text.text-accent-1{color:#ea80fc !important}.purple.accent-2{background-color:#e040fb !important}.purple-text.text-accent-2{color:#e040fb !important}.purple.accent-3{background-color:#d500f9 !important}.purple-text.text-accent-3{color:#d500f9 !important}.purple.accent-4{background-color:#a0f !important}.purple-text.text-accent-4{color:#a0f !important}.deep-purple{background-color:#673ab7 !important}.deep-purple-text{color:#673ab7 !important}.deep-purple.lighten-5{background-color:#ede7f6 !important}.deep-purple-text.text-lighten-5{color:#ede7f6 !important}.deep-purple.lighten-4{background-color:#d1c4e9 !important}.deep-purple-text.text-lighten-4{color:#d1c4e9 !important}.deep-purple.lighten-3{background-color:#b39ddb !important}.deep-purple-text.text-lighten-3{color:#b39ddb !important}.deep-purple.lighten-2{background-color:#9575cd !important}.deep-purple-text.text-lighten-2{color:#9575cd !important}.deep-purple.lighten-1{background-color:#7e57c2 !important}.deep-purple-text.text-lighten-1{color:#7e57c2 !important}.deep-purple.darken-1{background-color:#5e35b1 !important}.deep-purple-text.text-darken-1{color:#5e35b1 !important}.deep-purple.darken-2{background-color:#512da8 !important}.deep-purple-text.text-darken-2{color:#512da8 !important}.deep-purple.darken-3{background-color:#4527a0 !important}.deep-purple-text.text-darken-3{color:#4527a0 !important}.deep-purple.darken-4{background-color:#311b92 !important}.deep-purple-text.text-darken-4{color:#311b92 !important}.deep-purple.accent-1{background-color:#b388ff !important}.deep-purple-text.text-accent-1{color:#b388ff !important}.deep-purple.accent-2{background-color:#7c4dff !important}.deep-purple-text.text-accent-2{color:#7c4dff !important}.deep-purple.accent-3{background-color:#651fff !important}.deep-purple-text.text-accent-3{color:#651fff !important}.deep-purple.accent-4{background-color:#6200ea !important}.deep-purple-text.text-accent-4{color:#6200ea !important}.indigo{background-color:#3f51b5 !important}.indigo-text{color:#3f51b5 !important}.indigo.lighten-5{background-color:#e8eaf6 !important}.indigo-text.text-lighten-5{color:#e8eaf6 !important}.indigo.lighten-4{background-color:#c5cae9 !important}.indigo-text.text-lighten-4{color:#c5cae9 !important}.indigo.lighten-3{background-color:#9fa8da !important}.indigo-text.text-lighten-3{color:#9fa8da !important}.indigo.lighten-2{background-color:#7986cb !important}.indigo-text.text-lighten-2{color:#7986cb !important}.indigo.lighten-1{background-color:#5c6bc0 !important}.indigo-text.text-lighten-1{color:#5c6bc0 !important}.indigo.darken-1{background-color:#3949ab !important}.indigo-text.text-darken-1{color:#3949ab !important}.indigo.darken-2{background-color:#303f9f !important}.indigo-text.text-darken-2{color:#303f9f !important}.indigo.darken-3{background-color:#283593 !important}.indigo-text.text-darken-3{color:#283593 !important}.indigo.darken-4{background-color:#1a237e !important}.indigo-text.text-darken-4{color:#1a237e !important}.indigo.accent-1{background-color:#8c9eff !important}.indigo-text.text-accent-1{color:#8c9eff !important}.indigo.accent-2{background-color:#536dfe !important}.indigo-text.text-accent-2{color:#536dfe !important}.indigo.accent-3{background-color:#3d5afe !important}.indigo-text.text-accent-3{color:#3d5afe !important}.indigo.accent-4{background-color:#304ffe !important}.indigo-text.text-accent-4{color:#304ffe !important}.blue{background-color:#2196F3 !important}.blue-text{color:#2196F3 !important}.blue.lighten-5{background-color:#E3F2FD !important}.blue-text.text-lighten-5{color:#E3F2FD !important}.blue.lighten-4{background-color:#BBDEFB !important}.blue-text.text-lighten-4{color:#BBDEFB !important}.blue.lighten-3{background-color:#90CAF9 !important}.blue-text.text-lighten-3{color:#90CAF9 !important}.blue.lighten-2{background-color:#64B5F6 !important}.blue-text.text-lighten-2{color:#64B5F6 !important}.blue.lighten-1{background-color:#42A5F5 !important}.blue-text.text-lighten-1{color:#42A5F5 !important}.blue.darken-1{background-color:#1E88E5 !important}.blue-text.text-darken-1{color:#1E88E5 !important}.blue.darken-2{background-color:#1976D2 !important}.blue-text.text-darken-2{color:#1976D2 !important}.blue.darken-3{background-color:#1565C0 !important}.blue-text.text-darken-3{color:#1565C0 !important}.blue.darken-4{background-color:#0D47A1 !important}.blue-text.text-darken-4{color:#0D47A1 !important}.blue.accent-1{background-color:#82B1FF !important}.blue-text.text-accent-1{color:#82B1FF !important}.blue.accent-2{background-color:#448AFF !important}.blue-text.text-accent-2{color:#448AFF !important}.blue.accent-3{background-color:#2979FF !important}.blue-text.text-accent-3{color:#2979FF !important}.blue.accent-4{background-color:#2962FF !important}.blue-text.text-accent-4{color:#2962FF !important}.light-blue{background-color:#03a9f4 !important}.light-blue-text{color:#03a9f4 !important}.light-blue.lighten-5{background-color:#e1f5fe !important}.light-blue-text.text-lighten-5{color:#e1f5fe !important}.light-blue.lighten-4{background-color:#b3e5fc !important}.light-blue-text.text-lighten-4{color:#b3e5fc !important}.light-blue.lighten-3{background-color:#81d4fa !important}.light-blue-text.text-lighten-3{color:#81d4fa !important}.light-blue.lighten-2{background-color:#4fc3f7 !important}.light-blue-text.text-lighten-2{color:#4fc3f7 !important}.light-blue.lighten-1{background-color:#29b6f6 !important}.light-blue-text.text-lighten-1{color:#29b6f6 !important}.light-blue.darken-1{background-color:#039be5 !important}.light-blue-text.text-darken-1{color:#039be5 !important}.light-blue.darken-2{background-color:#0288d1 !important}.light-blue-text.text-darken-2{color:#0288d1 !important}.light-blue.darken-3{background-color:#0277bd !important}.light-blue-text.text-darken-3{color:#0277bd !important}.light-blue.darken-4{background-color:#01579b !important}.light-blue-text.text-darken-4{color:#01579b !important}.light-blue.accent-1{background-color:#80d8ff !important}.light-blue-text.text-accent-1{color:#80d8ff !important}.light-blue.accent-2{background-color:#40c4ff !important}.light-blue-text.text-accent-2{color:#40c4ff !important}.light-blue.accent-3{background-color:#00b0ff !important}.light-blue-text.text-accent-3{color:#00b0ff !important}.light-blue.accent-4{background-color:#0091ea !important}.light-blue-text.text-accent-4{color:#0091ea !important}.cyan{background-color:#00bcd4 !important}.cyan-text{color:#00bcd4 !important}.cyan.lighten-5{background-color:#e0f7fa !important}.cyan-text.text-lighten-5{color:#e0f7fa !important}.cyan.lighten-4{background-color:#b2ebf2 !important}.cyan-text.text-lighten-4{color:#b2ebf2 !important}.cyan.lighten-3{background-color:#80deea !important}.cyan-text.text-lighten-3{color:#80deea !important}.cyan.lighten-2{background-color:#4dd0e1 !important}.cyan-text.text-lighten-2{color:#4dd0e1 !important}.cyan.lighten-1{background-color:#26c6da !important}.cyan-text.text-lighten-1{color:#26c6da !important}.cyan.darken-1{background-color:#00acc1 !important}.cyan-text.text-darken-1{color:#00acc1 !important}.cyan.darken-2{background-color:#0097a7 !important}.cyan-text.text-darken-2{color:#0097a7 !important}.cyan.darken-3{background-color:#00838f !important}.cyan-text.text-darken-3{color:#00838f !important}.cyan.darken-4{background-color:#006064 !important}.cyan-text.text-darken-4{color:#006064 !important}.cyan.accent-1{background-color:#84ffff !important}.cyan-text.text-accent-1{color:#84ffff !important}.cyan.accent-2{background-color:#18ffff !important}.cyan-text.text-accent-2{color:#18ffff !important}.cyan.accent-3{background-color:#00e5ff !important}.cyan-text.text-accent-3{color:#00e5ff !important}.cyan.accent-4{background-color:#00b8d4 !important}.cyan-text.text-accent-4{color:#00b8d4 !important}.teal{background-color:#009688 !important}.teal-text{color:#009688 !important}.teal.lighten-5{background-color:#e0f2f1 !important}.teal-text.text-lighten-5{color:#e0f2f1 !important}.teal.lighten-4{background-color:#b2dfdb !important}.teal-text.text-lighten-4{color:#b2dfdb !important}.teal.lighten-3{background-color:#80cbc4 !important}.teal-text.text-lighten-3{color:#80cbc4 !important}.teal.lighten-2{background-color:#4db6ac !important}.teal-text.text-lighten-2{color:#4db6ac !important}.teal.lighten-1{background-color:#26a69a !important}.teal-text.text-lighten-1{color:#26a69a !important}.teal.darken-1{background-color:#00897b !important}.teal-text.text-darken-1{color:#00897b !important}.teal.darken-2{background-color:#00796b !important}.teal-text.text-darken-2{color:#00796b !important}.teal.darken-3{background-color:#00695c !important}.teal-text.text-darken-3{color:#00695c !important}.teal.darken-4{background-color:#004d40 !important}.teal-text.text-darken-4{color:#004d40 !important}.teal.accent-1{background-color:#a7ffeb !important}.teal-text.text-accent-1{color:#a7ffeb !important}.teal.accent-2{background-color:#64ffda !important}.teal-text.text-accent-2{color:#64ffda !important}.teal.accent-3{background-color:#1de9b6 !important}.teal-text.text-accent-3{color:#1de9b6 !important}.teal.accent-4{background-color:#00bfa5 !important}.teal-text.text-accent-4{color:#00bfa5 !important}.green{background-color:#4CAF50 !important}.green-text{color:#4CAF50 !important}.green.lighten-5{background-color:#E8F5E9 !important}.green-text.text-lighten-5{color:#E8F5E9 !important}.green.lighten-4{background-color:#C8E6C9 !important}.green-text.text-lighten-4{color:#C8E6C9 !important}.green.lighten-3{background-color:#A5D6A7 !important}.green-text.text-lighten-3{color:#A5D6A7 !important}.green.lighten-2{background-color:#81C784 !important}.green-text.text-lighten-2{color:#81C784 !important}.green.lighten-1{background-color:#66BB6A !important}.green-text.text-lighten-1{color:#66BB6A !important}.green.darken-1{background-color:#43A047 !important}.green-text.text-darken-1{color:#43A047 !important}.green.darken-2{background-color:#388E3C !important}.green-text.text-darken-2{color:#388E3C !important}.green.darken-3{background-color:#2E7D32 !important}.green-text.text-darken-3{color:#2E7D32 !important}.green.darken-4{background-color:#1B5E20 !important}.green-text.text-darken-4{color:#1B5E20 !important}.green.accent-1{background-color:#B9F6CA !important}.green-text.text-accent-1{color:#B9F6CA !important}.green.accent-2{background-color:#69F0AE !important}.green-text.text-accent-2{color:#69F0AE !important}.green.accent-3{background-color:#00E676 !important}.green-text.text-accent-3{color:#00E676 !important}.green.accent-4{background-color:#00C853 !important}.green-text.text-accent-4{color:#00C853 !important}.light-green{background-color:#8bc34a !important}.light-green-text{color:#8bc34a !important}.light-green.lighten-5{background-color:#f1f8e9 !important}.light-green-text.text-lighten-5{color:#f1f8e9 !important}.light-green.lighten-4{background-color:#dcedc8 !important}.light-green-text.text-lighten-4{color:#dcedc8 !important}.light-green.lighten-3{background-color:#c5e1a5 !important}.light-green-text.text-lighten-3{color:#c5e1a5 !important}.light-green.lighten-2{background-color:#aed581 !important}.light-green-text.text-lighten-2{color:#aed581 !important}.light-green.lighten-1{background-color:#9ccc65 !important}.light-green-text.text-lighten-1{color:#9ccc65 !important}.light-green.darken-1{background-color:#7cb342 !important}.light-green-text.text-darken-1{color:#7cb342 !important}.light-green.darken-2{background-color:#689f38 !important}.light-green-text.text-darken-2{color:#689f38 !important}.light-green.darken-3{background-color:#558b2f !important}.light-green-text.text-darken-3{color:#558b2f !important}.light-green.darken-4{background-color:#33691e !important}.light-green-text.text-darken-4{color:#33691e !important}.light-green.accent-1{background-color:#ccff90 !important}.light-green-text.text-accent-1{color:#ccff90 !important}.light-green.accent-2{background-color:#b2ff59 !important}.light-green-text.text-accent-2{color:#b2ff59 !important}.light-green.accent-3{background-color:#76ff03 !important}.light-green-text.text-accent-3{color:#76ff03 !important}.light-green.accent-4{background-color:#64dd17 !important}.light-green-text.text-accent-4{color:#64dd17 !important}.lime{background-color:#cddc39 !important}.lime-text{color:#cddc39 !important}.lime.lighten-5{background-color:#f9fbe7 !important}.lime-text.text-lighten-5{color:#f9fbe7 !important}.lime.lighten-4{background-color:#f0f4c3 !important}.lime-text.text-lighten-4{color:#f0f4c3 !important}.lime.lighten-3{background-color:#e6ee9c !important}.lime-text.text-lighten-3{color:#e6ee9c !important}.lime.lighten-2{background-color:#dce775 !important}.lime-text.text-lighten-2{color:#dce775 !important}.lime.lighten-1{background-color:#d4e157 !important}.lime-text.text-lighten-1{color:#d4e157 !important}.lime.darken-1{background-color:#c0ca33 !important}.lime-text.text-darken-1{color:#c0ca33 !important}.lime.darken-2{background-color:#afb42b !important}.lime-text.text-darken-2{color:#afb42b !important}.lime.darken-3{background-color:#9e9d24 !important}.lime-text.text-darken-3{color:#9e9d24 !important}.lime.darken-4{background-color:#827717 !important}.lime-text.text-darken-4{color:#827717 !important}.lime.accent-1{background-color:#f4ff81 !important}.lime-text.text-accent-1{color:#f4ff81 !important}.lime.accent-2{background-color:#eeff41 !important}.lime-text.text-accent-2{color:#eeff41 !important}.lime.accent-3{background-color:#c6ff00 !important}.lime-text.text-accent-3{color:#c6ff00 !important}.lime.accent-4{background-color:#aeea00 !important}.lime-text.text-accent-4{color:#aeea00 !important}.yellow{background-color:#ffeb3b !important}.yellow-text{color:#ffeb3b !important}.yellow.lighten-5{background-color:#fffde7 !important}.yellow-text.text-lighten-5{color:#fffde7 !important}.yellow.lighten-4{background-color:#fff9c4 !important}.yellow-text.text-lighten-4{color:#fff9c4 !important}.yellow.lighten-3{background-color:#fff59d !important}.yellow-text.text-lighten-3{color:#fff59d !important}.yellow.lighten-2{background-color:#fff176 !important}.yellow-text.text-lighten-2{color:#fff176 !important}.yellow.lighten-1{background-color:#ffee58 !important}.yellow-text.text-lighten-1{color:#ffee58 !important}.yellow.darken-1{background-color:#fdd835 !important}.yellow-text.text-darken-1{color:#fdd835 !important}.yellow.darken-2{background-color:#fbc02d !important}.yellow-text.text-darken-2{color:#fbc02d !important}.yellow.darken-3{background-color:#f9a825 !important}.yellow-text.text-darken-3{color:#f9a825 !important}.yellow.darken-4{background-color:#f57f17 !important}.yellow-text.text-darken-4{color:#f57f17 !important}.yellow.accent-1{background-color:#ffff8d !important}.yellow-text.text-accent-1{color:#ffff8d !important}.yellow.accent-2{background-color:#ff0 !important}.yellow-text.text-accent-2{color:#ff0 !important}.yellow.accent-3{background-color:#ffea00 !important}.yellow-text.text-accent-3{color:#ffea00 !important}.yellow.accent-4{background-color:#ffd600 !important}.yellow-text.text-accent-4{color:#ffd600 !important}.amber{background-color:#ffc107 !important}.amber-text{color:#ffc107 !important}.amber.lighten-5{background-color:#fff8e1 !important}.amber-text.text-lighten-5{color:#fff8e1 !important}.amber.lighten-4{background-color:#ffecb3 !important}.amber-text.text-lighten-4{color:#ffecb3 !important}.amber.lighten-3{background-color:#ffe082 !important}.amber-text.text-lighten-3{color:#ffe082 !important}.amber.lighten-2{background-color:#ffd54f !important}.amber-text.text-lighten-2{color:#ffd54f !important}.amber.lighten-1{background-color:#ffca28 !important}.amber-text.text-lighten-1{color:#ffca28 !important}.amber.darken-1{background-color:#ffb300 !important}.amber-text.text-darken-1{color:#ffb300 !important}.amber.darken-2{background-color:#ffa000 !important}.amber-text.text-darken-2{color:#ffa000 !important}.amber.darken-3{background-color:#ff8f00 !important}.amber-text.text-darken-3{color:#ff8f00 !important}.amber.darken-4{background-color:#ff6f00 !important}.amber-text.text-darken-4{color:#ff6f00 !important}.amber.accent-1{background-color:#ffe57f !important}.amber-text.text-accent-1{color:#ffe57f !important}.amber.accent-2{background-color:#ffd740 !important}.amber-text.text-accent-2{color:#ffd740 !important}.amber.accent-3{background-color:#ffc400 !important}.amber-text.text-accent-3{color:#ffc400 !important}.amber.accent-4{background-color:#ffab00 !important}.amber-text.text-accent-4{color:#ffab00 !important}.orange{background-color:#ff9800 !important}.orange-text{color:#ff9800 !important}.orange.lighten-5{background-color:#fff3e0 !important}.orange-text.text-lighten-5{color:#fff3e0 !important}.orange.lighten-4{background-color:#ffe0b2 !important}.orange-text.text-lighten-4{color:#ffe0b2 !important}.orange.lighten-3{background-color:#ffcc80 !important}.orange-text.text-lighten-3{color:#ffcc80 !important}.orange.lighten-2{background-color:#ffb74d !important}.orange-text.text-lighten-2{color:#ffb74d !important}.orange.lighten-1{background-color:#ffa726 !important}.orange-text.text-lighten-1{color:#ffa726 !important}.orange.darken-1{background-color:#fb8c00 !important}.orange-text.text-darken-1{color:#fb8c00 !important}.orange.darken-2{background-color:#f57c00 !important}.orange-text.text-darken-2{color:#f57c00 !important}.orange.darken-3{background-color:#ef6c00 !important}.orange-text.text-darken-3{color:#ef6c00 !important}.orange.darken-4{background-color:#e65100 !important}.orange-text.text-darken-4{color:#e65100 !important}.orange.accent-1{background-color:#ffd180 !important}.orange-text.text-accent-1{color:#ffd180 !important}.orange.accent-2{background-color:#ffab40 !important}.orange-text.text-accent-2{color:#ffab40 !important}.orange.accent-3{background-color:#ff9100 !important}.orange-text.text-accent-3{color:#ff9100 !important}.orange.accent-4{background-color:#ff6d00 !important}.orange-text.text-accent-4{color:#ff6d00 !important}.deep-orange{background-color:#ff5722 !important}.deep-orange-text{color:#ff5722 !important}.deep-orange.lighten-5{background-color:#fbe9e7 !important}.deep-orange-text.text-lighten-5{color:#fbe9e7 !important}.deep-orange.lighten-4{background-color:#ffccbc !important}.deep-orange-text.text-lighten-4{color:#ffccbc !important}.deep-orange.lighten-3{background-color:#ffab91 !important}.deep-orange-text.text-lighten-3{color:#ffab91 !important}.deep-orange.lighten-2{background-color:#ff8a65 !important}.deep-orange-text.text-lighten-2{color:#ff8a65 !important}.deep-orange.lighten-1{background-color:#ff7043 !important}.deep-orange-text.text-lighten-1{color:#ff7043 !important}.deep-orange.darken-1{background-color:#f4511e !important}.deep-orange-text.text-darken-1{color:#f4511e !important}.deep-orange.darken-2{background-color:#e64a19 !important}.deep-orange-text.text-darken-2{color:#e64a19 !important}.deep-orange.darken-3{background-color:#d84315 !important}.deep-orange-text.text-darken-3{color:#d84315 !important}.deep-orange.darken-4{background-color:#bf360c !important}.deep-orange-text.text-darken-4{color:#bf360c !important}.deep-orange.accent-1{background-color:#ff9e80 !important}.deep-orange-text.text-accent-1{color:#ff9e80 !important}.deep-orange.accent-2{background-color:#ff6e40 !important}.deep-orange-text.text-accent-2{color:#ff6e40 !important}.deep-orange.accent-3{background-color:#ff3d00 !important}.deep-orange-text.text-accent-3{color:#ff3d00 !important}.deep-orange.accent-4{background-color:#dd2c00 !important}.deep-orange-text.text-accent-4{color:#dd2c00 !important}.brown{background-color:#795548 !important}.brown-text{color:#795548 !important}.brown.lighten-5{background-color:#efebe9 !important}.brown-text.text-lighten-5{color:#efebe9 !important}.brown.lighten-4{background-color:#d7ccc8 !important}.brown-text.text-lighten-4{color:#d7ccc8 !important}.brown.lighten-3{background-color:#bcaaa4 !important}.brown-text.text-lighten-3{color:#bcaaa4 !important}.brown.lighten-2{background-color:#a1887f !important}.brown-text.text-lighten-2{color:#a1887f !important}.brown.lighten-1{background-color:#8d6e63 !important}.brown-text.text-lighten-1{color:#8d6e63 !important}.brown.darken-1{background-color:#6d4c41 !important}.brown-text.text-darken-1{color:#6d4c41 !important}.brown.darken-2{background-color:#5d4037 !important}.brown-text.text-darken-2{color:#5d4037 !important}.brown.darken-3{background-color:#4e342e !important}.brown-text.text-darken-3{color:#4e342e !important}.brown.darken-4{background-color:#3e2723 !important}.brown-text.text-darken-4{color:#3e2723 !important}.blue-grey{background-color:#607d8b !important}.blue-grey-text{color:#607d8b !important}.blue-grey.lighten-5{background-color:#eceff1 !important}.blue-grey-text.text-lighten-5{color:#eceff1 !important}.blue-grey.lighten-4{background-color:#cfd8dc !important}.blue-grey-text.text-lighten-4{color:#cfd8dc !important}.blue-grey.lighten-3{background-color:#b0bec5 !important}.blue-grey-text.text-lighten-3{color:#b0bec5 !important}.blue-grey.lighten-2{background-color:#90a4ae !important}.blue-grey-text.text-lighten-2{color:#90a4ae !important}.blue-grey.lighten-1{background-color:#78909c !important}.blue-grey-text.text-lighten-1{color:#78909c !important}.blue-grey.darken-1{background-color:#546e7a !important}.blue-grey-text.text-darken-1{color:#546e7a !important}.blue-grey.darken-2{background-color:#455a64 !important}.blue-grey-text.text-darken-2{color:#455a64 !important}.blue-grey.darken-3{background-color:#37474f !important}.blue-grey-text.text-darken-3{color:#37474f !important}.blue-grey.darken-4{background-color:#263238 !important}.blue-grey-text.text-darken-4{color:#263238 !important}.grey{background-color:#9e9e9e !important}.grey-text{color:#9e9e9e !important}.grey.lighten-5{background-color:#fafafa !important}.grey-text.text-lighten-5{color:#fafafa !important}.grey.lighten-4{background-color:#f5f5f5 !important}.grey-text.text-lighten-4{color:#f5f5f5 !important}.grey.lighten-3{background-color:#eee !important}.grey-text.text-lighten-3{color:#eee !important}.grey.lighten-2{background-color:#e0e0e0 !important}.grey-text.text-lighten-2{color:#e0e0e0 !important}.grey.lighten-1{background-color:#bdbdbd !important}.grey-text.text-lighten-1{color:#bdbdbd !important}.grey.darken-1{background-color:#757575 !important}.grey-text.text-darken-1{color:#757575 !important}.grey.darken-2{background-color:#616161 !important}.grey-text.text-darken-2{color:#616161 !important}.grey.darken-3{background-color:#424242 !important}.grey-text.text-darken-3{color:#424242 !important}.grey.darken-4{background-color:#212121 !important}.grey-text.text-darken-4{color:#212121 !important}.black{background-color:#000 !important}.black-text{color:#000 !important}.white{background-color:#fff !important}.white-text{color:#fff !important}.transparent{background-color:rgba(0,0,0,0) !important}.transparent-text{color:rgba(0,0,0,0) !important}/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:0.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace, monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}html{-webkit-box-sizing:border-box;box-sizing:border-box}*,*:before,*:after{-webkit-box-sizing:inherit;box-sizing:inherit}button,input,optgroup,select,textarea{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}ul:not(.browser-default){padding-left:0;list-style-type:none}ul:not(.browser-default)>li{list-style-type:none}a{color:#039be5;text-decoration:none;-webkit-tap-highlight-color:transparent}.valign-wrapper{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.clearfix{clear:both}.z-depth-0{-webkit-box-shadow:none !important;box-shadow:none !important}.z-depth-1,nav,.card-panel,.card,.toast,.btn,.btn-large,.btn-small,.btn-floating,.dropdown-content,.collapsible,.sidenav{-webkit-box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2);box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2)}.z-depth-1-half,.btn:hover,.btn-large:hover,.btn-small:hover,.btn-floating:hover{-webkit-box-shadow:0 3px 3px 0 rgba(0,0,0,0.14),0 1px 7px 0 rgba(0,0,0,0.12),0 3px 1px -1px rgba(0,0,0,0.2);box-shadow:0 3px 3px 0 rgba(0,0,0,0.14),0 1px 7px 0 rgba(0,0,0,0.12),0 3px 1px -1px rgba(0,0,0,0.2)}.z-depth-2{-webkit-box-shadow:0 4px 5px 0 rgba(0,0,0,0.14),0 1px 10px 0 rgba(0,0,0,0.12),0 2px 4px -1px rgba(0,0,0,0.3);box-shadow:0 4px 5px 0 rgba(0,0,0,0.14),0 1px 10px 0 rgba(0,0,0,0.12),0 2px 4px -1px rgba(0,0,0,0.3)}.z-depth-3{-webkit-box-shadow:0 8px 17px 2px rgba(0,0,0,0.14),0 3px 14px 2px rgba(0,0,0,0.12),0 5px 5px -3px rgba(0,0,0,0.2);box-shadow:0 8px 17px 2px rgba(0,0,0,0.14),0 3px 14px 2px rgba(0,0,0,0.12),0 5px 5px -3px rgba(0,0,0,0.2)}.z-depth-4{-webkit-box-shadow:0 16px 24px 2px rgba(0,0,0,0.14),0 6px 30px 5px rgba(0,0,0,0.12),0 8px 10px -7px rgba(0,0,0,0.2);box-shadow:0 16px 24px 2px rgba(0,0,0,0.14),0 6px 30px 5px rgba(0,0,0,0.12),0 8px 10px -7px rgba(0,0,0,0.2)}.z-depth-5,.modal{-webkit-box-shadow:0 24px 38px 3px rgba(0,0,0,0.14),0 9px 46px 8px rgba(0,0,0,0.12),0 11px 15px -7px rgba(0,0,0,0.2);box-shadow:0 24px 38px 3px rgba(0,0,0,0.14),0 9px 46px 8px rgba(0,0,0,0.12),0 11px 15px -7px rgba(0,0,0,0.2)}.hoverable{-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s}.hoverable:hover{-webkit-box-shadow:0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);box-shadow:0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}.divider{height:1px;overflow:hidden;background-color:#e0e0e0}blockquote{margin:20px 0;padding-left:1.5rem;border-left:5px solid #ee6e73}i{line-height:inherit}i.left{float:left;margin-right:15px}i.right{float:right;margin-left:15px}i.tiny{font-size:1rem}i.small{font-size:2rem}i.medium{font-size:4rem}i.large{font-size:6rem}img.responsive-img,video.responsive-video{max-width:100%;height:auto}.pagination li{display:inline-block;border-radius:2px;text-align:center;vertical-align:top;height:30px}.pagination li a{color:#444;display:inline-block;font-size:1.2rem;padding:0 10px;line-height:30px}.pagination li.active a{color:#fff}.pagination li.active{background-color:#ee6e73}.pagination li.disabled a{cursor:default;color:#999}.pagination li i{font-size:2rem}.pagination li.pages ul li{display:inline-block;float:none}@media only screen and (max-width: 992px){.pagination{width:100%}.pagination li.prev,.pagination li.next{width:10%}.pagination li.pages{width:80%;overflow:hidden;white-space:nowrap}}.breadcrumb{font-size:18px;color:rgba(255,255,255,0.7)}.breadcrumb i,.breadcrumb [class^="mdi-"],.breadcrumb [class*="mdi-"],.breadcrumb i.material-icons{display:inline-block;float:left;font-size:24px}.breadcrumb:before{content:'\E5CC';color:rgba(255,255,255,0.7);vertical-align:top;display:inline-block;font-family:'Material Icons';font-weight:normal;font-style:normal;font-size:25px;margin:0 10px 0 8px;-webkit-font-smoothing:antialiased}.breadcrumb:first-child:before{display:none}.breadcrumb:last-child{color:#fff}.parallax-container{position:relative;overflow:hidden;height:500px}.parallax-container .parallax{position:absolute;top:0;left:0;right:0;bottom:0;z-index:-1}.parallax-container .parallax img{opacity:0;position:absolute;left:50%;bottom:0;min-width:100%;min-height:100%;-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);-webkit-transform:translateX(-50%);transform:translateX(-50%)}.pin-top,.pin-bottom{position:relative}.pinned{position:fixed !important}ul.staggered-list li{opacity:0}.fade-in{opacity:0;-webkit-transform-origin:0 50%;transform-origin:0 50%}@media only screen and (max-width: 600px){.hide-on-small-only,.hide-on-small-and-down{display:none !important}}@media only screen and (max-width: 992px){.hide-on-med-and-down{display:none !important}}@media only screen and (min-width: 601px){.hide-on-med-and-up{display:none !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.hide-on-med-only{display:none !important}}@media only screen and (min-width: 993px){.hide-on-large-only{display:none !important}}@media only screen and (min-width: 1201px){.hide-on-extra-large-only{display:none !important}}@media only screen and (min-width: 1201px){.show-on-extra-large{display:block !important}}@media only screen and (min-width: 993px){.show-on-large{display:block !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.show-on-medium{display:block !important}}@media only screen and (max-width: 600px){.show-on-small{display:block !important}}@media only screen and (min-width: 601px){.show-on-medium-and-up{display:block !important}}@media only screen and (max-width: 992px){.show-on-medium-and-down{display:block !important}}@media only screen and (max-width: 600px){.center-on-small-only{text-align:center}}.page-footer{padding-top:20px;color:#fff;background-color:#ee6e73}.page-footer .footer-copyright{overflow:hidden;min-height:50px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:10px 0px;color:rgba(255,255,255,0.8);background-color:rgba(51,51,51,0.08)}table,th,td{border:none}table{width:100%;display:table;border-collapse:collapse;border-spacing:0}table.striped tr{border-bottom:none}table.striped>tbody>tr:nth-child(odd){background-color:rgba(242,242,242,0.5)}table.striped>tbody>tr>td{border-radius:0}table.highlight>tbody>tr{-webkit-transition:background-color .25s ease;transition:background-color .25s ease}table.highlight>tbody>tr:hover{background-color:rgba(242,242,242,0.5)}table.centered thead tr th,table.centered tbody tr td{text-align:center}tr{border-bottom:1px solid rgba(0,0,0,0.12)}td,th{padding:15px 5px;display:table-cell;text-align:left;vertical-align:middle;border-radius:2px}@media only screen and (max-width: 992px){table.responsive-table{width:100%;border-collapse:collapse;border-spacing:0;display:block;position:relative}table.responsive-table td:empty:before{content:'\00a0'}table.responsive-table th,table.responsive-table td{margin:0;vertical-align:top}table.responsive-table th{text-align:left}table.responsive-table thead{display:block;float:left}table.responsive-table thead tr{display:block;padding:0 10px 0 0}table.responsive-table thead tr th::before{content:"\00a0"}table.responsive-table tbody{display:block;width:auto;position:relative;overflow-x:auto;white-space:nowrap}table.responsive-table tbody tr{display:inline-block;vertical-align:top}table.responsive-table th{display:block;text-align:right}table.responsive-table td{display:block;min-height:1.25em;text-align:left}table.responsive-table tr{border-bottom:none;padding:0 10px}table.responsive-table thead{border:0;border-right:1px solid rgba(0,0,0,0.12)}}.collection{margin:.5rem 0 1rem 0;border:1px solid #e0e0e0;border-radius:2px;overflow:hidden;position:relative}.collection .collection-item{background-color:#fff;line-height:1.5rem;padding:10px 20px;margin:0;border-bottom:1px solid #e0e0e0}.collection .collection-item.avatar{min-height:84px;padding-left:72px;position:relative}.collection .collection-item.avatar:not(.circle-clipper)>.circle,.collection .collection-item.avatar :not(.circle-clipper)>.circle{position:absolute;width:42px;height:42px;overflow:hidden;left:15px;display:inline-block;vertical-align:middle}.collection .collection-item.avatar i.circle{font-size:18px;line-height:42px;color:#fff;background-color:#999;text-align:center}.collection .collection-item.avatar .title{font-size:16px}.collection .collection-item.avatar p{margin:0}.collection .collection-item.avatar .secondary-content{position:absolute;top:16px;right:16px}.collection .collection-item:last-child{border-bottom:none}.collection .collection-item.active{background-color:#26a69a;color:#eafaf9}.collection .collection-item.active .secondary-content{color:#fff}.collection a.collection-item{display:block;-webkit-transition:.25s;transition:.25s;color:#26a69a}.collection a.collection-item:not(.active):hover{background-color:#ddd}.collection.with-header .collection-header{background-color:#fff;border-bottom:1px solid #e0e0e0;padding:10px 20px}.collection.with-header .collection-item{padding-left:30px}.collection.with-header .collection-item.avatar{padding-left:72px}.secondary-content{float:right;color:#26a69a}.collapsible .collection{margin:0;border:none}.video-container{position:relative;padding-bottom:56.25%;height:0;overflow:hidden}.video-container iframe,.video-container object,.video-container embed{position:absolute;top:0;left:0;width:100%;height:100%}.progress{position:relative;height:4px;display:block;width:100%;background-color:#acece6;border-radius:2px;margin:.5rem 0 1rem 0;overflow:hidden}.progress .determinate{position:absolute;top:0;left:0;bottom:0;background-color:#26a69a;-webkit-transition:width .3s linear;transition:width .3s linear}.progress .indeterminate{background-color:#26a69a}.progress .indeterminate:before{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;-webkit-animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite}.progress .indeterminate:after{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;-webkit-animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;-webkit-animation-delay:1.15s;animation-delay:1.15s}@-webkit-keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@-webkit-keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}@keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}.hide{display:none !important}.left-align{text-align:left}.right-align{text-align:right}.center,.center-align{text-align:center}.left{float:left !important}.right{float:right !important}.no-select,input[type=range],input[type=range]+.thumb{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.circle{border-radius:50%}.center-block{display:block;margin-left:auto;margin-right:auto}.truncate{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-padding{padding:0 !important}span.badge{min-width:3rem;padding:0 6px;margin-left:14px;text-align:center;font-size:1rem;line-height:22px;height:22px;color:#757575;float:right;-webkit-box-sizing:border-box;box-sizing:border-box}span.badge.new{font-weight:300;font-size:0.8rem;color:#fff;background-color:#26a69a;border-radius:2px}span.badge.new:after{content:" new"}span.badge[data-badge-caption]::after{content:" " attr(data-badge-caption)}nav ul a span.badge{display:inline-block;float:none;margin-left:4px;line-height:22px;height:22px;-webkit-font-smoothing:auto}.collection-item span.badge{margin-top:calc(.75rem - 11px)}.collapsible span.badge{margin-left:auto}.sidenav span.badge{margin-top:calc(24px - 11px)}table span.badge{display:inline-block;float:none;margin-left:auto}.material-icons{text-rendering:optimizeLegibility;-webkit-font-feature-settings:'liga';-moz-font-feature-settings:'liga';font-feature-settings:'liga'}.container{margin:0 auto;max-width:1280px;width:90%}@media only screen and (min-width: 601px){.container{width:85%}}@media only screen and (min-width: 993px){.container{width:70%}}.col .row{margin-left:-.75rem;margin-right:-.75rem}.section{padding-top:1rem;padding-bottom:1rem}.section.no-pad{padding:0}.section.no-pad-bot{padding-bottom:0}.section.no-pad-top{padding-top:0}.row{margin-left:auto;margin-right:auto;margin-bottom:20px}.row:after{content:"";display:table;clear:both}.row .col{float:left;-webkit-box-sizing:border-box;box-sizing:border-box;padding:0 .75rem;min-height:1px}.row .col[class*="push-"],.row .col[class*="pull-"]{position:relative}.row .col.s1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.s4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.s7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.s10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-s1{margin-left:8.3333333333%}.row .col.pull-s1{right:8.3333333333%}.row .col.push-s1{left:8.3333333333%}.row .col.offset-s2{margin-left:16.6666666667%}.row .col.pull-s2{right:16.6666666667%}.row .col.push-s2{left:16.6666666667%}.row .col.offset-s3{margin-left:25%}.row .col.pull-s3{right:25%}.row .col.push-s3{left:25%}.row .col.offset-s4{margin-left:33.3333333333%}.row .col.pull-s4{right:33.3333333333%}.row .col.push-s4{left:33.3333333333%}.row .col.offset-s5{margin-left:41.6666666667%}.row .col.pull-s5{right:41.6666666667%}.row .col.push-s5{left:41.6666666667%}.row .col.offset-s6{margin-left:50%}.row .col.pull-s6{right:50%}.row .col.push-s6{left:50%}.row .col.offset-s7{margin-left:58.3333333333%}.row .col.pull-s7{right:58.3333333333%}.row .col.push-s7{left:58.3333333333%}.row .col.offset-s8{margin-left:66.6666666667%}.row .col.pull-s8{right:66.6666666667%}.row .col.push-s8{left:66.6666666667%}.row .col.offset-s9{margin-left:75%}.row .col.pull-s9{right:75%}.row .col.push-s9{left:75%}.row .col.offset-s10{margin-left:83.3333333333%}.row .col.pull-s10{right:83.3333333333%}.row .col.push-s10{left:83.3333333333%}.row .col.offset-s11{margin-left:91.6666666667%}.row .col.pull-s11{right:91.6666666667%}.row .col.push-s11{left:91.6666666667%}.row .col.offset-s12{margin-left:100%}.row .col.pull-s12{right:100%}.row .col.push-s12{left:100%}@media only screen and (min-width: 601px){.row .col.m1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.m4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.m7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.m10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-m1{margin-left:8.3333333333%}.row .col.pull-m1{right:8.3333333333%}.row .col.push-m1{left:8.3333333333%}.row .col.offset-m2{margin-left:16.6666666667%}.row .col.pull-m2{right:16.6666666667%}.row .col.push-m2{left:16.6666666667%}.row .col.offset-m3{margin-left:25%}.row .col.pull-m3{right:25%}.row .col.push-m3{left:25%}.row .col.offset-m4{margin-left:33.3333333333%}.row .col.pull-m4{right:33.3333333333%}.row .col.push-m4{left:33.3333333333%}.row .col.offset-m5{margin-left:41.6666666667%}.row .col.pull-m5{right:41.6666666667%}.row .col.push-m5{left:41.6666666667%}.row .col.offset-m6{margin-left:50%}.row .col.pull-m6{right:50%}.row .col.push-m6{left:50%}.row .col.offset-m7{margin-left:58.3333333333%}.row .col.pull-m7{right:58.3333333333%}.row .col.push-m7{left:58.3333333333%}.row .col.offset-m8{margin-left:66.6666666667%}.row .col.pull-m8{right:66.6666666667%}.row .col.push-m8{left:66.6666666667%}.row .col.offset-m9{margin-left:75%}.row .col.pull-m9{right:75%}.row .col.push-m9{left:75%}.row .col.offset-m10{margin-left:83.3333333333%}.row .col.pull-m10{right:83.3333333333%}.row .col.push-m10{left:83.3333333333%}.row .col.offset-m11{margin-left:91.6666666667%}.row .col.pull-m11{right:91.6666666667%}.row .col.push-m11{left:91.6666666667%}.row .col.offset-m12{margin-left:100%}.row .col.pull-m12{right:100%}.row .col.push-m12{left:100%}}@media only screen and (min-width: 993px){.row .col.l1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.l4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.l7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.l10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-l1{margin-left:8.3333333333%}.row .col.pull-l1{right:8.3333333333%}.row .col.push-l1{left:8.3333333333%}.row .col.offset-l2{margin-left:16.6666666667%}.row .col.pull-l2{right:16.6666666667%}.row .col.push-l2{left:16.6666666667%}.row .col.offset-l3{margin-left:25%}.row .col.pull-l3{right:25%}.row .col.push-l3{left:25%}.row .col.offset-l4{margin-left:33.3333333333%}.row .col.pull-l4{right:33.3333333333%}.row .col.push-l4{left:33.3333333333%}.row .col.offset-l5{margin-left:41.6666666667%}.row .col.pull-l5{right:41.6666666667%}.row .col.push-l5{left:41.6666666667%}.row .col.offset-l6{margin-left:50%}.row .col.pull-l6{right:50%}.row .col.push-l6{left:50%}.row .col.offset-l7{margin-left:58.3333333333%}.row .col.pull-l7{right:58.3333333333%}.row .col.push-l7{left:58.3333333333%}.row .col.offset-l8{margin-left:66.6666666667%}.row .col.pull-l8{right:66.6666666667%}.row .col.push-l8{left:66.6666666667%}.row .col.offset-l9{margin-left:75%}.row .col.pull-l9{right:75%}.row .col.push-l9{left:75%}.row .col.offset-l10{margin-left:83.3333333333%}.row .col.pull-l10{right:83.3333333333%}.row .col.push-l10{left:83.3333333333%}.row .col.offset-l11{margin-left:91.6666666667%}.row .col.pull-l11{right:91.6666666667%}.row .col.push-l11{left:91.6666666667%}.row .col.offset-l12{margin-left:100%}.row .col.pull-l12{right:100%}.row .col.push-l12{left:100%}}@media only screen and (min-width: 1201px){.row .col.xl1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.xl4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.xl7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.xl10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-xl1{margin-left:8.3333333333%}.row .col.pull-xl1{right:8.3333333333%}.row .col.push-xl1{left:8.3333333333%}.row .col.offset-xl2{margin-left:16.6666666667%}.row .col.pull-xl2{right:16.6666666667%}.row .col.push-xl2{left:16.6666666667%}.row .col.offset-xl3{margin-left:25%}.row .col.pull-xl3{right:25%}.row .col.push-xl3{left:25%}.row .col.offset-xl4{margin-left:33.3333333333%}.row .col.pull-xl4{right:33.3333333333%}.row .col.push-xl4{left:33.3333333333%}.row .col.offset-xl5{margin-left:41.6666666667%}.row .col.pull-xl5{right:41.6666666667%}.row .col.push-xl5{left:41.6666666667%}.row .col.offset-xl6{margin-left:50%}.row .col.pull-xl6{right:50%}.row .col.push-xl6{left:50%}.row .col.offset-xl7{margin-left:58.3333333333%}.row .col.pull-xl7{right:58.3333333333%}.row .col.push-xl7{left:58.3333333333%}.row .col.offset-xl8{margin-left:66.6666666667%}.row .col.pull-xl8{right:66.6666666667%}.row .col.push-xl8{left:66.6666666667%}.row .col.offset-xl9{margin-left:75%}.row .col.pull-xl9{right:75%}.row .col.push-xl9{left:75%}.row .col.offset-xl10{margin-left:83.3333333333%}.row .col.pull-xl10{right:83.3333333333%}.row .col.push-xl10{left:83.3333333333%}.row .col.offset-xl11{margin-left:91.6666666667%}.row .col.pull-xl11{right:91.6666666667%}.row .col.push-xl11{left:91.6666666667%}.row .col.offset-xl12{margin-left:100%}.row .col.pull-xl12{right:100%}.row .col.push-xl12{left:100%}}nav{color:#fff;background-color:#ee6e73;width:100%;height:56px;line-height:56px}nav.nav-extended{height:auto}nav.nav-extended .nav-wrapper{min-height:56px;height:auto}nav.nav-extended .nav-content{position:relative;line-height:normal}nav a{color:#fff}nav i,nav [class^="mdi-"],nav [class*="mdi-"],nav i.material-icons{display:block;font-size:24px;height:56px;line-height:56px}nav .nav-wrapper{position:relative;height:100%}@media only screen and (min-width: 993px){nav a.sidenav-trigger{display:none}}nav .sidenav-trigger{float:left;position:relative;z-index:1;height:56px;margin:0 18px}nav .sidenav-trigger i{height:56px;line-height:56px}nav .brand-logo{position:absolute;color:#fff;display:inline-block;font-size:2.1rem;padding:0}nav .brand-logo.center{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}@media only screen and (max-width: 992px){nav .brand-logo{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}nav .brand-logo.left,nav .brand-logo.right{padding:0;-webkit-transform:none;transform:none}nav .brand-logo.left{left:0.5rem}nav .brand-logo.right{right:0.5rem;left:auto}}nav .brand-logo.right{right:0.5rem;padding:0}nav .brand-logo i,nav .brand-logo [class^="mdi-"],nav .brand-logo [class*="mdi-"],nav .brand-logo i.material-icons{float:left;margin-right:15px}nav .nav-title{display:inline-block;font-size:32px;padding:28px 0}nav ul{margin:0}nav ul li{-webkit-transition:background-color .3s;transition:background-color .3s;float:left;padding:0}nav ul li.active{background-color:rgba(0,0,0,0.1)}nav ul a{-webkit-transition:background-color .3s;transition:background-color .3s;font-size:1rem;color:#fff;display:block;padding:0 15px;cursor:pointer}nav ul a.btn,nav ul a.btn-large,nav ul a.btn-small,nav ul a.btn-large,nav ul a.btn-flat,nav ul a.btn-floating{margin-top:-2px;margin-left:15px;margin-right:15px}nav ul a.btn>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn-small>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn-flat>.material-icons,nav ul a.btn-floating>.material-icons{height:inherit;line-height:inherit}nav ul a:hover{background-color:rgba(0,0,0,0.1)}nav ul.left{float:left}nav form{height:100%}nav .input-field{margin:0;height:100%}nav .input-field input{height:100%;font-size:1.2rem;border:none;padding-left:2rem}nav .input-field input:focus,nav .input-field input[type=text]:valid,nav .input-field input[type=password]:valid,nav .input-field input[type=email]:valid,nav .input-field input[type=url]:valid,nav .input-field input[type=date]:valid{border:none;-webkit-box-shadow:none;box-shadow:none}nav .input-field label{top:0;left:0}nav .input-field label i{color:rgba(255,255,255,0.7);-webkit-transition:color .3s;transition:color .3s}nav .input-field label.active i{color:#fff}.navbar-fixed{position:relative;height:56px;z-index:997}.navbar-fixed nav{position:fixed}@media only screen and (min-width: 601px){nav.nav-extended .nav-wrapper{min-height:64px}nav,nav .nav-wrapper i,nav a.sidenav-trigger,nav a.sidenav-trigger i{height:64px;line-height:64px}.navbar-fixed{height:64px}}a{text-decoration:none}html{line-height:1.5;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-weight:normal;color:rgba(0,0,0,0.87)}@media only screen and (min-width: 0){html{font-size:14px}}@media only screen and (min-width: 992px){html{font-size:14.5px}}@media only screen and (min-width: 1200px){html{font-size:15px}}h1,h2,h3,h4,h5,h6{font-weight:400;line-height:1.3}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{font-weight:inherit}h1{font-size:4.2rem;line-height:110%;margin:2.8rem 0 1.68rem 0}h2{font-size:3.56rem;line-height:110%;margin:2.3733333333rem 0 1.424rem 0}h3{font-size:2.92rem;line-height:110%;margin:1.9466666667rem 0 1.168rem 0}h4{font-size:2.28rem;line-height:110%;margin:1.52rem 0 .912rem 0}h5{font-size:1.64rem;line-height:110%;margin:1.0933333333rem 0 .656rem 0}h6{font-size:1.15rem;line-height:110%;margin:.7666666667rem 0 .46rem 0}em{font-style:italic}strong{font-weight:500}small{font-size:75%}.light{font-weight:300}.thin{font-weight:200}@media only screen and (min-width: 360px){.flow-text{font-size:1.2rem}}@media only screen and (min-width: 390px){.flow-text{font-size:1.224rem}}@media only screen and (min-width: 420px){.flow-text{font-size:1.248rem}}@media only screen and (min-width: 450px){.flow-text{font-size:1.272rem}}@media only screen and (min-width: 480px){.flow-text{font-size:1.296rem}}@media only screen and (min-width: 510px){.flow-text{font-size:1.32rem}}@media only screen and (min-width: 540px){.flow-text{font-size:1.344rem}}@media only screen and (min-width: 570px){.flow-text{font-size:1.368rem}}@media only screen and (min-width: 600px){.flow-text{font-size:1.392rem}}@media only screen and (min-width: 630px){.flow-text{font-size:1.416rem}}@media only screen and (min-width: 660px){.flow-text{font-size:1.44rem}}@media only screen and (min-width: 690px){.flow-text{font-size:1.464rem}}@media only screen and (min-width: 720px){.flow-text{font-size:1.488rem}}@media only screen and (min-width: 750px){.flow-text{font-size:1.512rem}}@media only screen and (min-width: 780px){.flow-text{font-size:1.536rem}}@media only screen and (min-width: 810px){.flow-text{font-size:1.56rem}}@media only screen and (min-width: 840px){.flow-text{font-size:1.584rem}}@media only screen and (min-width: 870px){.flow-text{font-size:1.608rem}}@media only screen and (min-width: 900px){.flow-text{font-size:1.632rem}}@media only screen and (min-width: 930px){.flow-text{font-size:1.656rem}}@media only screen and (min-width: 960px){.flow-text{font-size:1.68rem}}@media only screen and (max-width: 360px){.flow-text{font-size:1.2rem}}.scale-transition{-webkit-transition:-webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:-webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63), -webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important}.scale-transition.scale-out{-webkit-transform:scale(0);transform:scale(0);-webkit-transition:-webkit-transform .2s !important;transition:-webkit-transform .2s !important;transition:transform .2s !important;transition:transform .2s, -webkit-transform .2s !important}.scale-transition.scale-in{-webkit-transform:scale(1);transform:scale(1)}.card-panel{-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s;padding:24px;margin:.5rem 0 1rem 0;border-radius:2px;background-color:#fff}.card{position:relative;margin:.5rem 0 1rem 0;background-color:#fff;-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s;border-radius:2px}.card .card-title{font-size:24px;font-weight:300}.card .card-title.activator{cursor:pointer}.card.small,.card.medium,.card.large{position:relative}.card.small .card-image,.card.medium .card-image,.card.large .card-image{max-height:60%;overflow:hidden}.card.small .card-image+.card-content,.card.medium .card-image+.card-content,.card.large .card-image+.card-content{max-height:40%}.card.small .card-content,.card.medium .card-content,.card.large .card-content{max-height:100%;overflow:hidden}.card.small .card-action,.card.medium .card-action,.card.large .card-action{position:absolute;bottom:0;left:0;right:0}.card.small{height:300px}.card.medium{height:400px}.card.large{height:500px}.card.horizontal{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.card.horizontal.small .card-image,.card.horizontal.medium .card-image,.card.horizontal.large .card-image{height:100%;max-height:none;overflow:visible}.card.horizontal.small .card-image img,.card.horizontal.medium .card-image img,.card.horizontal.large .card-image img{height:100%}.card.horizontal .card-image{max-width:50%}.card.horizontal .card-image img{border-radius:2px 0 0 2px;max-width:100%;width:auto}.card.horizontal .card-stacked{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;position:relative}.card.horizontal .card-stacked .card-content{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.card.sticky-action .card-action{z-index:2}.card.sticky-action .card-reveal{z-index:1;padding-bottom:64px}.card .card-image{position:relative}.card .card-image img{display:block;border-radius:2px 2px 0 0;position:relative;left:0;right:0;top:0;bottom:0;width:100%}.card .card-image .card-title{color:#fff;position:absolute;bottom:0;left:0;max-width:100%;padding:24px}.card .card-content{padding:24px;border-radius:0 0 2px 2px}.card .card-content p{margin:0}.card .card-content .card-title{display:block;line-height:32px;margin-bottom:8px}.card .card-content .card-title i{line-height:32px}.card .card-action{background-color:inherit;border-top:1px solid rgba(160,160,160,0.2);position:relative;padding:16px 24px}.card .card-action:last-child{border-radius:0 0 2px 2px}.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating){color:#ffab40;margin-right:24px;-webkit-transition:color .3s ease;transition:color .3s ease;text-transform:uppercase}.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating):hover{color:#ffd8a6}.card .card-reveal{padding:24px;position:absolute;background-color:#fff;width:100%;overflow-y:auto;left:0;top:100%;height:100%;z-index:3;display:none}.card .card-reveal .card-title{cursor:pointer;display:block}#toast-container{display:block;position:fixed;z-index:10000}@media only screen and (max-width: 600px){#toast-container{min-width:100%;bottom:0%}}@media only screen and (min-width: 601px) and (max-width: 992px){#toast-container{left:5%;bottom:7%;max-width:90%}}@media only screen and (min-width: 993px){#toast-container{top:10%;right:7%;max-width:86%}}.toast{border-radius:2px;top:35px;width:auto;margin-top:10px;position:relative;max-width:100%;height:auto;min-height:48px;line-height:1.5em;background-color:#323232;padding:10px 25px;font-size:1.1rem;font-weight:300;color:#fff;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;cursor:default}.toast .toast-action{color:#eeff41;font-weight:500;margin-right:-25px;margin-left:3rem}.toast.rounded{border-radius:24px}@media only screen and (max-width: 600px){.toast{width:100%;border-radius:0}}.tabs{position:relative;overflow-x:auto;overflow-y:hidden;height:48px;width:100%;background-color:#fff;margin:0 auto;white-space:nowrap}.tabs.tabs-transparent{background-color:transparent}.tabs.tabs-transparent .tab a,.tabs.tabs-transparent .tab.disabled a,.tabs.tabs-transparent .tab.disabled a:hover{color:rgba(255,255,255,0.7)}.tabs.tabs-transparent .tab a:hover,.tabs.tabs-transparent .tab a.active{color:#fff}.tabs.tabs-transparent .indicator{background-color:#fff}.tabs.tabs-fixed-width{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.tabs.tabs-fixed-width .tab{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.tabs .tab{display:inline-block;text-align:center;line-height:48px;height:48px;padding:0;margin:0;text-transform:uppercase}.tabs .tab a{color:rgba(238,110,115,0.7);display:block;width:100%;height:100%;padding:0 24px;font-size:14px;text-overflow:ellipsis;overflow:hidden;-webkit-transition:color .28s ease, background-color .28s ease;transition:color .28s ease, background-color .28s ease}.tabs .tab a:focus,.tabs .tab a:focus.active{background-color:rgba(246,178,181,0.2);outline:none}.tabs .tab a:hover,.tabs .tab a.active{background-color:transparent;color:#ee6e73}.tabs .tab.disabled a,.tabs .tab.disabled a:hover{color:rgba(238,110,115,0.4);cursor:default}.tabs .indicator{position:absolute;bottom:0;height:2px;background-color:#f6b2b5;will-change:left, right}@media only screen and (max-width: 992px){.tabs{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.tabs .tab{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.tabs .tab a{padding:0 12px}}.material-tooltip{padding:10px 8px;font-size:1rem;z-index:2000;background-color:transparent;border-radius:2px;color:#fff;min-height:36px;line-height:120%;opacity:0;position:absolute;text-align:center;max-width:calc(100% - 4px);overflow:hidden;left:0;top:0;pointer-events:none;visibility:hidden;background-color:#323232}.backdrop{position:absolute;opacity:0;height:7px;width:14px;border-radius:0 0 50% 50%;background-color:#323232;z-index:-1;-webkit-transform-origin:50% 0%;transform-origin:50% 0%;visibility:hidden}.btn,.btn-large,.btn-small,.btn-flat{border:none;border-radius:2px;display:inline-block;height:36px;line-height:36px;padding:0 16px;text-transform:uppercase;vertical-align:middle;-webkit-tap-highlight-color:transparent}.btn.disabled,.disabled.btn-large,.disabled.btn-small,.btn-floating.disabled,.btn-large.disabled,.btn-small.disabled,.btn-flat.disabled,.btn:disabled,.btn-large:disabled,.btn-small:disabled,.btn-floating:disabled,.btn-large:disabled,.btn-small:disabled,.btn-flat:disabled,.btn[disabled],.btn-large[disabled],.btn-small[disabled],.btn-floating[disabled],.btn-large[disabled],.btn-small[disabled],.btn-flat[disabled]{pointer-events:none;background-color:#DFDFDF !important;-webkit-box-shadow:none;box-shadow:none;color:#9F9F9F !important;cursor:default}.btn.disabled:hover,.disabled.btn-large:hover,.disabled.btn-small:hover,.btn-floating.disabled:hover,.btn-large.disabled:hover,.btn-small.disabled:hover,.btn-flat.disabled:hover,.btn:disabled:hover,.btn-large:disabled:hover,.btn-small:disabled:hover,.btn-floating:disabled:hover,.btn-large:disabled:hover,.btn-small:disabled:hover,.btn-flat:disabled:hover,.btn[disabled]:hover,.btn-large[disabled]:hover,.btn-small[disabled]:hover,.btn-floating[disabled]:hover,.btn-large[disabled]:hover,.btn-small[disabled]:hover,.btn-flat[disabled]:hover{background-color:#DFDFDF !important;color:#9F9F9F !important}.btn,.btn-large,.btn-small,.btn-floating,.btn-large,.btn-small,.btn-flat{font-size:14px;outline:0}.btn i,.btn-large i,.btn-small i,.btn-floating i,.btn-large i,.btn-small i,.btn-flat i{font-size:1.3rem;line-height:inherit}.btn:focus,.btn-large:focus,.btn-small:focus,.btn-floating:focus{background-color:#1d7d74}.btn,.btn-large,.btn-small{text-decoration:none;color:#fff;background-color:#26a69a;text-align:center;letter-spacing:.5px;-webkit-transition:background-color .2s ease-out;transition:background-color .2s ease-out;cursor:pointer}.btn:hover,.btn-large:hover,.btn-small:hover{background-color:#2bbbad}.btn-floating{display:inline-block;color:#fff;position:relative;overflow:hidden;z-index:1;width:40px;height:40px;line-height:40px;padding:0;background-color:#26a69a;border-radius:50%;-webkit-transition:background-color .3s;transition:background-color .3s;cursor:pointer;vertical-align:middle}.btn-floating:hover{background-color:#26a69a}.btn-floating:before{border-radius:0}.btn-floating.btn-large{width:56px;height:56px;padding:0}.btn-floating.btn-large.halfway-fab{bottom:-28px}.btn-floating.btn-large i{line-height:56px}.btn-floating.btn-small{width:32.4px;height:32.4px}.btn-floating.btn-small.halfway-fab{bottom:-16.2px}.btn-floating.btn-small i{line-height:32.4px}.btn-floating.halfway-fab{position:absolute;right:24px;bottom:-20px}.btn-floating.halfway-fab.left{right:auto;left:24px}.btn-floating i{width:inherit;display:inline-block;text-align:center;color:#fff;font-size:1.6rem;line-height:40px}button.btn-floating{border:none}.fixed-action-btn{position:fixed;right:23px;bottom:23px;padding-top:15px;margin-bottom:0;z-index:997}.fixed-action-btn.active ul{visibility:visible}.fixed-action-btn.direction-left,.fixed-action-btn.direction-right{padding:0 0 0 15px}.fixed-action-btn.direction-left ul,.fixed-action-btn.direction-right ul{text-align:right;right:64px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);height:100%;left:auto;width:500px}.fixed-action-btn.direction-left ul li,.fixed-action-btn.direction-right ul li{display:inline-block;margin:7.5px 15px 0 0}.fixed-action-btn.direction-right{padding:0 15px 0 0}.fixed-action-btn.direction-right ul{text-align:left;direction:rtl;left:64px;right:auto}.fixed-action-btn.direction-right ul li{margin:7.5px 0 0 15px}.fixed-action-btn.direction-bottom{padding:0 0 15px 0}.fixed-action-btn.direction-bottom ul{top:64px;bottom:auto;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:reverse;-webkit-flex-direction:column-reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.fixed-action-btn.direction-bottom ul li{margin:15px 0 0 0}.fixed-action-btn.toolbar{padding:0;height:56px}.fixed-action-btn.toolbar.active>a i{opacity:0}.fixed-action-btn.toolbar ul{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;top:0;bottom:0;z-index:1}.fixed-action-btn.toolbar ul li{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;display:inline-block;margin:0;height:100%;-webkit-transition:none;transition:none}.fixed-action-btn.toolbar ul li a{display:block;overflow:hidden;position:relative;width:100%;height:100%;background-color:transparent;-webkit-box-shadow:none;box-shadow:none;color:#fff;line-height:56px;z-index:1}.fixed-action-btn.toolbar ul li a i{line-height:inherit}.fixed-action-btn ul{left:0;right:0;text-align:center;position:absolute;bottom:64px;margin:0;visibility:hidden}.fixed-action-btn ul li{margin-bottom:15px}.fixed-action-btn ul a.btn-floating{opacity:0}.fixed-action-btn .fab-backdrop{position:absolute;top:0;left:0;z-index:-1;width:40px;height:40px;background-color:#26a69a;border-radius:50%;-webkit-transform:scale(0);transform:scale(0)}.btn-flat{-webkit-box-shadow:none;box-shadow:none;background-color:transparent;color:#343434;cursor:pointer;-webkit-transition:background-color .2s;transition:background-color .2s}.btn-flat:focus,.btn-flat:hover{-webkit-box-shadow:none;box-shadow:none}.btn-flat:focus{background-color:rgba(0,0,0,0.1)}.btn-flat.disabled,.btn-flat.btn-flat[disabled]{background-color:transparent !important;color:#b3b2b2 !important;cursor:default}.btn-large{height:54px;line-height:54px;font-size:15px;padding:0 28px}.btn-large i{font-size:1.6rem}.btn-small{height:32.4px;line-height:32.4px;font-size:13px}.btn-small i{font-size:1.2rem}.btn-block{display:block}.dropdown-content{background-color:#fff;margin:0;display:none;min-width:100px;overflow-y:auto;opacity:0;position:absolute;left:0;top:0;z-index:9999;-webkit-transform-origin:0 0;transform-origin:0 0}.dropdown-content:focus{outline:0}.dropdown-content li{clear:both;color:rgba(0,0,0,0.87);cursor:pointer;min-height:50px;line-height:1.5rem;width:100%;text-align:left}.dropdown-content li:hover,.dropdown-content li.active{background-color:#eee}.dropdown-content li:focus{outline:none}.dropdown-content li.divider{min-height:0;height:1px}.dropdown-content li>a,.dropdown-content li>span{font-size:16px;color:#26a69a;display:block;line-height:22px;padding:14px 16px}.dropdown-content li>span>label{top:1px;left:0;height:18px}.dropdown-content li>a>i{height:inherit;line-height:inherit;float:left;margin:0 24px 0 0;width:24px}body.keyboard-focused .dropdown-content li:focus{background-color:#dadada}.input-field.col .dropdown-content [type="checkbox"]+label{top:1px;left:0;height:18px;-webkit-transform:none;transform:none}.dropdown-trigger{cursor:pointer}/*!\r
+ * Waves v0.6.0\r
+ * http://fian.my.id/Waves\r
+ *\r
+ * Copyright 2014 Alfiana E. Sibuea and other contributors\r
+ * Released under the MIT license\r
+ * https://github.com/fians/Waves/blob/master/LICENSE\r
+ */.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;-webkit-transition:.3s ease-out;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,0.2);-webkit-transition:all 0.7s ease-out;transition:all 0.7s ease-out;-webkit-transition-property:opacity, -webkit-transform;transition-property:opacity, -webkit-transform;transition-property:transform, opacity;transition-property:transform, opacity, -webkit-transform;-webkit-transform:scale(0);transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:rgba(255,255,255,0.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,0.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,0.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,0.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,0.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,0.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,0.7)}.waves-effect input[type="button"],.waves-effect input[type="reset"],.waves-effect input[type="submit"]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{-webkit-transition:none !important;transition:none !important}.waves-circle{-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle, white 100%, black 100%)}.waves-input-wrapper{border-radius:0.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top, opacity}.modal:focus{outline:none}@media only screen and (max-width: 992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%;text-align:right}.modal .modal-footer .btn,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small,.modal .modal-footer .btn-flat{margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-25%;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,0.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom, opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem 0}.collapsible-header{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;cursor:pointer;-webkit-tap-highlight-color:transparent;line-height:1.5;padding:1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header:focus{outline:0}.collapsible-header i{width:2rem;font-size:1.6rem;display:inline-block;text-align:center;margin-right:1rem}.keyboard-focused .collapsible-header:focus{background-color:#eee}.collapsible-body{display:none;border-bottom:1px solid #ddd;-webkit-box-sizing:border-box;box-sizing:border-box;padding:2rem}.sidenav .collapsible,.sidenav.fixed .collapsible{border:none;-webkit-box-shadow:none;box-shadow:none}.sidenav .collapsible li,.sidenav.fixed .collapsible li{padding:0}.sidenav .collapsible-header,.sidenav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.sidenav .collapsible-header:hover,.sidenav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,0.05)}.sidenav .collapsible-header i,.sidenav.fixed .collapsible-header i{line-height:inherit}.sidenav .collapsible-body,.sidenav.fixed .collapsible-body{border:0;background-color:#fff}.sidenav .collapsible-body li a,.sidenav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;-webkit-box-shadow:none;box-shadow:none}.collapsible.popout>li{-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);margin:0 24px;-webkit-transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)}.collapsible.popout>li.active{-webkit-box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;margin-bottom:5px;margin-right:5px}.chip:focus{outline:none;background-color:#26a69a;color:#fff}.chip>img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip .close{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.chips{border:none;border-bottom:1px solid #9e9e9e;-webkit-box-shadow:none;box-shadow:none;margin:0 0 8px 0;min-height:45px;outline:none;-webkit-transition:all .3s;transition:all .3s}.chips.focus{border-bottom:1px solid #26a69a;-webkit-box-shadow:0 1px 0 0 #26a69a;box-shadow:0 1px 0 0 #26a69a}.chips:hover{cursor:text}.chips .input{background:none;border:0;color:rgba(0,0,0,0.6);display:inline-block;font-size:16px;height:3rem;line-height:32px;outline:0;margin:0;padding:0 !important;width:120px !important}.chips .input:focus{border:0 !important;-webkit-box-shadow:none !important;box-shadow:none !important}.chips .autocomplete-content{margin-top:0;margin-bottom:0}.prefix ~ .chips{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.chips:empty ~ label{font-size:0.8rem;-webkit-transform:translateY(-140%);transform:translateY(-140%)}.materialboxed{display:block;cursor:-webkit-zoom-in;cursor:zoom-in;position:relative;-webkit-transition:opacity .4s;transition:opacity .4s;-webkit-backface-visibility:hidden}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:-webkit-zoom-out;cursor:zoom-out}#materialbox-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#292929;z-index:1000;will-change:opacity}.materialbox-caption{position:fixed;display:none;color:#fff;line-height:50px;bottom:0;left:0;width:100%;text-align:center;padding:0% 15%;height:50px;z-index:1000;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #c9f3ef}button:focus{outline:none;background-color:#2ab7a9}label{font-size:.8rem;color:#9e9e9e}::-webkit-input-placeholder{color:#d1d1d1}::-moz-placeholder{color:#d1d1d1}:-ms-input-placeholder{color:#d1d1d1}::-ms-input-placeholder{color:#d1d1d1}::placeholder{color:#d1d1d1}input:not([type]),input[type=text]:not(.browser-default),input[type=password]:not(.browser-default),input[type=email]:not(.browser-default),input[type=url]:not(.browser-default),input[type=time]:not(.browser-default),input[type=date]:not(.browser-default),input[type=datetime]:not(.browser-default),input[type=datetime-local]:not(.browser-default),input[type=tel]:not(.browser-default),input[type=number]:not(.browser-default),input[type=search]:not(.browser-default),textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:content-box;box-sizing:content-box;-webkit-transition:border .3s, -webkit-box-shadow .3s;transition:border .3s, -webkit-box-shadow .3s;transition:box-shadow .3s, border .3s;transition:box-shadow .3s, border .3s, -webkit-box-shadow .3s}input:not([type]):disabled,input:not([type])[readonly="readonly"],input[type=text]:not(.browser-default):disabled,input[type=text]:not(.browser-default)[readonly="readonly"],input[type=password]:not(.browser-default):disabled,input[type=password]:not(.browser-default)[readonly="readonly"],input[type=email]:not(.browser-default):disabled,input[type=email]:not(.browser-default)[readonly="readonly"],input[type=url]:not(.browser-default):disabled,input[type=url]:not(.browser-default)[readonly="readonly"],input[type=time]:not(.browser-default):disabled,input[type=time]:not(.browser-default)[readonly="readonly"],input[type=date]:not(.browser-default):disabled,input[type=date]:not(.browser-default)[readonly="readonly"],input[type=datetime]:not(.browser-default):disabled,input[type=datetime]:not(.browser-default)[readonly="readonly"],input[type=datetime-local]:not(.browser-default):disabled,input[type=datetime-local]:not(.browser-default)[readonly="readonly"],input[type=tel]:not(.browser-default):disabled,input[type=tel]:not(.browser-default)[readonly="readonly"],input[type=number]:not(.browser-default):disabled,input[type=number]:not(.browser-default)[readonly="readonly"],input[type=search]:not(.browser-default):disabled,input[type=search]:not(.browser-default)[readonly="readonly"],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly="readonly"]{color:rgba(0,0,0,0.42);border-bottom:1px dotted rgba(0,0,0,0.42)}input:not([type]):disabled+label,input:not([type])[readonly="readonly"]+label,input[type=text]:not(.browser-default):disabled+label,input[type=text]:not(.browser-default)[readonly="readonly"]+label,input[type=password]:not(.browser-default):disabled+label,input[type=password]:not(.browser-default)[readonly="readonly"]+label,input[type=email]:not(.browser-default):disabled+label,input[type=email]:not(.browser-default)[readonly="readonly"]+label,input[type=url]:not(.browser-default):disabled+label,input[type=url]:not(.browser-default)[readonly="readonly"]+label,input[type=time]:not(.browser-default):disabled+label,input[type=time]:not(.browser-default)[readonly="readonly"]+label,input[type=date]:not(.browser-default):disabled+label,input[type=date]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime]:not(.browser-default):disabled+label,input[type=datetime]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime-local]:not(.browser-default):disabled+label,input[type=datetime-local]:not(.browser-default)[readonly="readonly"]+label,input[type=tel]:not(.browser-default):disabled+label,input[type=tel]:not(.browser-default)[readonly="readonly"]+label,input[type=number]:not(.browser-default):disabled+label,input[type=number]:not(.browser-default)[readonly="readonly"]+label,input[type=search]:not(.browser-default):disabled+label,input[type=search]:not(.browser-default)[readonly="readonly"]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly="readonly"]+label{color:rgba(0,0,0,0.42)}input:not([type]):focus:not([readonly]),input[type=text]:not(.browser-default):focus:not([readonly]),input[type=password]:not(.browser-default):focus:not([readonly]),input[type=email]:not(.browser-default):focus:not([readonly]),input[type=url]:not(.browser-default):focus:not([readonly]),input[type=time]:not(.browser-default):focus:not([readonly]),input[type=date]:not(.browser-default):focus:not([readonly]),input[type=datetime]:not(.browser-default):focus:not([readonly]),input[type=datetime-local]:not(.browser-default):focus:not([readonly]),input[type=tel]:not(.browser-default):focus:not([readonly]),input[type=number]:not(.browser-default):focus:not([readonly]),input[type=search]:not(.browser-default):focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #26a69a;-webkit-box-shadow:0 1px 0 0 #26a69a;box-shadow:0 1px 0 0 #26a69a}input:not([type]):focus:not([readonly])+label,input[type=text]:not(.browser-default):focus:not([readonly])+label,input[type=password]:not(.browser-default):focus:not([readonly])+label,input[type=email]:not(.browser-default):focus:not([readonly])+label,input[type=url]:not(.browser-default):focus:not([readonly])+label,input[type=time]:not(.browser-default):focus:not([readonly])+label,input[type=date]:not(.browser-default):focus:not([readonly])+label,input[type=datetime]:not(.browser-default):focus:not([readonly])+label,input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label,input[type=tel]:not(.browser-default):focus:not([readonly])+label,input[type=number]:not(.browser-default):focus:not([readonly])+label,input[type=search]:not(.browser-default):focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#26a69a}input:not([type]):focus.valid ~ label,input[type=text]:not(.browser-default):focus.valid ~ label,input[type=password]:not(.browser-default):focus.valid ~ label,input[type=email]:not(.browser-default):focus.valid ~ label,input[type=url]:not(.browser-default):focus.valid ~ label,input[type=time]:not(.browser-default):focus.valid ~ label,input[type=date]:not(.browser-default):focus.valid ~ label,input[type=datetime]:not(.browser-default):focus.valid ~ label,input[type=datetime-local]:not(.browser-default):focus.valid ~ label,input[type=tel]:not(.browser-default):focus.valid ~ label,input[type=number]:not(.browser-default):focus.valid ~ label,input[type=search]:not(.browser-default):focus.valid ~ label,textarea.materialize-textarea:focus.valid ~ label{color:#4CAF50}input:not([type]):focus.invalid ~ label,input[type=text]:not(.browser-default):focus.invalid ~ label,input[type=password]:not(.browser-default):focus.invalid ~ label,input[type=email]:not(.browser-default):focus.invalid ~ label,input[type=url]:not(.browser-default):focus.invalid ~ label,input[type=time]:not(.browser-default):focus.invalid ~ label,input[type=date]:not(.browser-default):focus.invalid ~ label,input[type=datetime]:not(.browser-default):focus.invalid ~ label,input[type=datetime-local]:not(.browser-default):focus.invalid ~ label,input[type=tel]:not(.browser-default):focus.invalid ~ label,input[type=number]:not(.browser-default):focus.invalid ~ label,input[type=search]:not(.browser-default):focus.invalid ~ label,textarea.materialize-textarea:focus.invalid ~ label{color:#F44336}input:not([type]).validate+label,input[type=text]:not(.browser-default).validate+label,input[type=password]:not(.browser-default).validate+label,input[type=email]:not(.browser-default).validate+label,input[type=url]:not(.browser-default).validate+label,input[type=time]:not(.browser-default).validate+label,input[type=date]:not(.browser-default).validate+label,input[type=datetime]:not(.browser-default).validate+label,input[type=datetime-local]:not(.browser-default).validate+label,input[type=tel]:not(.browser-default).validate+label,input[type=number]:not(.browser-default).validate+label,input[type=search]:not(.browser-default).validate+label,textarea.materialize-textarea.validate+label{width:100%}input.valid:not([type]),input.valid:not([type]):focus,input.valid[type=text]:not(.browser-default),input.valid[type=text]:not(.browser-default):focus,input.valid[type=password]:not(.browser-default),input.valid[type=password]:not(.browser-default):focus,input.valid[type=email]:not(.browser-default),input.valid[type=email]:not(.browser-default):focus,input.valid[type=url]:not(.browser-default),input.valid[type=url]:not(.browser-default):focus,input.valid[type=time]:not(.browser-default),input.valid[type=time]:not(.browser-default):focus,input.valid[type=date]:not(.browser-default),input.valid[type=date]:not(.browser-default):focus,input.valid[type=datetime]:not(.browser-default),input.valid[type=datetime]:not(.browser-default):focus,input.valid[type=datetime-local]:not(.browser-default),input.valid[type=datetime-local]:not(.browser-default):focus,input.valid[type=tel]:not(.browser-default),input.valid[type=tel]:not(.browser-default):focus,input.valid[type=number]:not(.browser-default),input.valid[type=number]:not(.browser-default):focus,input.valid[type=search]:not(.browser-default),input.valid[type=search]:not(.browser-default):focus,textarea.materialize-textarea.valid,textarea.materialize-textarea.valid:focus,.select-wrapper.valid>input.select-dropdown{border-bottom:1px solid #4CAF50;-webkit-box-shadow:0 1px 0 0 #4CAF50;box-shadow:0 1px 0 0 #4CAF50}input.invalid:not([type]),input.invalid:not([type]):focus,input.invalid[type=text]:not(.browser-default),input.invalid[type=text]:not(.browser-default):focus,input.invalid[type=password]:not(.browser-default),input.invalid[type=password]:not(.browser-default):focus,input.invalid[type=email]:not(.browser-default),input.invalid[type=email]:not(.browser-default):focus,input.invalid[type=url]:not(.browser-default),input.invalid[type=url]:not(.browser-default):focus,input.invalid[type=time]:not(.browser-default),input.invalid[type=time]:not(.browser-default):focus,input.invalid[type=date]:not(.browser-default),input.invalid[type=date]:not(.browser-default):focus,input.invalid[type=datetime]:not(.browser-default),input.invalid[type=datetime]:not(.browser-default):focus,input.invalid[type=datetime-local]:not(.browser-default),input.invalid[type=datetime-local]:not(.browser-default):focus,input.invalid[type=tel]:not(.browser-default),input.invalid[type=tel]:not(.browser-default):focus,input.invalid[type=number]:not(.browser-default),input.invalid[type=number]:not(.browser-default):focus,input.invalid[type=search]:not(.browser-default),input.invalid[type=search]:not(.browser-default):focus,textarea.materialize-textarea.invalid,textarea.materialize-textarea.invalid:focus,.select-wrapper.invalid>input.select-dropdown,.select-wrapper.invalid>input.select-dropdown:focus{border-bottom:1px solid #F44336;-webkit-box-shadow:0 1px 0 0 #F44336;box-shadow:0 1px 0 0 #F44336}input:not([type]).valid ~ .helper-text[data-success],input:not([type]):focus.valid ~ .helper-text[data-success],input:not([type]).invalid ~ .helper-text[data-error],input:not([type]):focus.invalid ~ .helper-text[data-error],input[type=text]:not(.browser-default).valid ~ .helper-text[data-success],input[type=text]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=text]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=text]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=password]:not(.browser-default).valid ~ .helper-text[data-success],input[type=password]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=password]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=password]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=email]:not(.browser-default).valid ~ .helper-text[data-success],input[type=email]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=email]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=email]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=url]:not(.browser-default).valid ~ .helper-text[data-success],input[type=url]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=url]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=url]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=time]:not(.browser-default).valid ~ .helper-text[data-success],input[type=time]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=time]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=time]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=date]:not(.browser-default).valid ~ .helper-text[data-success],input[type=date]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=date]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=date]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=datetime]:not(.browser-default).valid ~ .helper-text[data-success],input[type=datetime]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=datetime]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=datetime]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=datetime-local]:not(.browser-default).valid ~ .helper-text[data-success],input[type=datetime-local]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=datetime-local]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=datetime-local]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=tel]:not(.browser-default).valid ~ .helper-text[data-success],input[type=tel]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=tel]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=tel]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=number]:not(.browser-default).valid ~ .helper-text[data-success],input[type=number]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=number]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=number]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=search]:not(.browser-default).valid ~ .helper-text[data-success],input[type=search]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=search]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=search]:not(.browser-default):focus.invalid ~ .helper-text[data-error],textarea.materialize-textarea.valid ~ .helper-text[data-success],textarea.materialize-textarea:focus.valid ~ .helper-text[data-success],textarea.materialize-textarea.invalid ~ .helper-text[data-error],textarea.materialize-textarea:focus.invalid ~ .helper-text[data-error],.select-wrapper.valid .helper-text[data-success],.select-wrapper.invalid ~ .helper-text[data-error]{color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none}input:not([type]).valid ~ .helper-text:after,input:not([type]):focus.valid ~ .helper-text:after,input[type=text]:not(.browser-default).valid ~ .helper-text:after,input[type=text]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=password]:not(.browser-default).valid ~ .helper-text:after,input[type=password]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=email]:not(.browser-default).valid ~ .helper-text:after,input[type=email]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=url]:not(.browser-default).valid ~ .helper-text:after,input[type=url]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=time]:not(.browser-default).valid ~ .helper-text:after,input[type=time]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=date]:not(.browser-default).valid ~ .helper-text:after,input[type=date]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=datetime]:not(.browser-default).valid ~ .helper-text:after,input[type=datetime]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default).valid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=tel]:not(.browser-default).valid ~ .helper-text:after,input[type=tel]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=number]:not(.browser-default).valid ~ .helper-text:after,input[type=number]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=search]:not(.browser-default).valid ~ .helper-text:after,input[type=search]:not(.browser-default):focus.valid ~ .helper-text:after,textarea.materialize-textarea.valid ~ .helper-text:after,textarea.materialize-textarea:focus.valid ~ .helper-text:after,.select-wrapper.valid ~ .helper-text:after{content:attr(data-success);color:#4CAF50}input:not([type]).invalid ~ .helper-text:after,input:not([type]):focus.invalid ~ .helper-text:after,input[type=text]:not(.browser-default).invalid ~ .helper-text:after,input[type=text]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=password]:not(.browser-default).invalid ~ .helper-text:after,input[type=password]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=email]:not(.browser-default).invalid ~ .helper-text:after,input[type=email]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=url]:not(.browser-default).invalid ~ .helper-text:after,input[type=url]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=time]:not(.browser-default).invalid ~ .helper-text:after,input[type=time]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=date]:not(.browser-default).invalid ~ .helper-text:after,input[type=date]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=datetime]:not(.browser-default).invalid ~ .helper-text:after,input[type=datetime]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default).invalid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=tel]:not(.browser-default).invalid ~ .helper-text:after,input[type=tel]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=number]:not(.browser-default).invalid ~ .helper-text:after,input[type=number]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=search]:not(.browser-default).invalid ~ .helper-text:after,input[type=search]:not(.browser-default):focus.invalid ~ .helper-text:after,textarea.materialize-textarea.invalid ~ .helper-text:after,textarea.materialize-textarea:focus.invalid ~ .helper-text:after,.select-wrapper.invalid ~ .helper-text:after{content:attr(data-error);color:#F44336}input:not([type])+label:after,input[type=text]:not(.browser-default)+label:after,input[type=password]:not(.browser-default)+label:after,input[type=email]:not(.browser-default)+label:after,input[type=url]:not(.browser-default)+label:after,input[type=time]:not(.browser-default)+label:after,input[type=date]:not(.browser-default)+label:after,input[type=datetime]:not(.browser-default)+label:after,input[type=datetime-local]:not(.browser-default)+label:after,input[type=tel]:not(.browser-default)+label:after,input[type=number]:not(.browser-default)+label:after,input[type=search]:not(.browser-default)+label:after,textarea.materialize-textarea+label:after,.select-wrapper+label:after{display:block;content:"";position:absolute;top:100%;left:0;opacity:0;-webkit-transition:.2s opacity ease-out, .2s color ease-out;transition:.2s opacity ease-out, .2s color ease-out}.input-field{position:relative;margin-top:1rem;margin-bottom:1rem}.input-field.inline{display:inline-block;vertical-align:middle;margin-left:5px}.input-field.inline input,.input-field.inline .select-dropdown{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix ~ label,.input-field.col .prefix ~ .validate ~ label{width:calc(100% - 3rem - 1.5rem)}.input-field>label{color:#9e9e9e;position:absolute;top:0;left:0;font-size:1rem;cursor:text;-webkit-transition:color .2s ease-out, -webkit-transform .2s ease-out;transition:color .2s ease-out, -webkit-transform .2s ease-out;transition:transform .2s ease-out, color .2s ease-out;transition:transform .2s ease-out, color .2s ease-out, -webkit-transform .2s ease-out;-webkit-transform-origin:0% 100%;transform-origin:0% 100%;text-align:initial;-webkit-transform:translateY(12px);transform:translateY(12px)}.input-field>label:not(.label-icon).active{-webkit-transform:translateY(-14px) scale(0.8);transform:translateY(-14px) scale(0.8);-webkit-transform-origin:0 0;transform-origin:0 0}.input-field>input[type]:-webkit-autofill:not(.browser-default):not([type="search"])+label,.input-field>input[type=date]:not(.browser-default)+label,.input-field>input[type=time]:not(.browser-default)+label{-webkit-transform:translateY(-14px) scale(0.8);transform:translateY(-14px) scale(0.8);-webkit-transform-origin:0 0;transform-origin:0 0}.input-field .helper-text{position:relative;min-height:18px;display:block;font-size:12px;color:rgba(0,0,0,0.54)}.input-field .helper-text::after{opacity:1;position:absolute;top:0;left:0}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;-webkit-transition:color .2s;transition:color .2s;top:.5rem}.input-field .prefix.active{color:#26a69a}.input-field .prefix ~ input,.input-field .prefix ~ textarea,.input-field .prefix ~ label,.input-field .prefix ~ .validate ~ label,.input-field .prefix ~ .helper-text,.input-field .prefix ~ .autocomplete-content{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix ~ label{margin-left:3rem}@media only screen and (max-width: 992px){.input-field .prefix ~ input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width: 600px){.input-field .prefix ~ input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;-webkit-transition:.3s background-color;transition:.3s background-color}.nav-wrapper .input-field input[type=search]{height:inherit;padding-left:4rem;width:calc(100% - 4rem);border:0;-webkit-box-shadow:none;box-shadow:none}.input-field input[type=search]:focus:not(.browser-default){background-color:#fff;border:0;-webkit-box-shadow:none;box-shadow:none;color:#444}.input-field input[type=search]:focus:not(.browser-default)+label i,.input-field input[type=search]:focus:not(.browser-default) ~ .mdi-navigation-close,.input-field input[type=search]:focus:not(.browser-default) ~ .material-icons{color:#444}.input-field input[type=search]+.label-icon{-webkit-transform:none;transform:none;left:1rem}.input-field input[type=search] ~ .mdi-navigation-close,.input-field input[type=search] ~ .material-icons{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;-webkit-transition:.3s color;transition:.3s color}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{line-height:normal;overflow-y:hidden;padding:.8rem 0 .8rem 0;resize:none;min-height:3rem;-webkit-box-sizing:border-box;box-sizing:border-box}.hiddendiv{visibility:hidden;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem;position:absolute;top:0;z-index:-1}.autocomplete-content li .highlight{color:#444}.autocomplete-content li img{height:40px;width:40px;margin:5px 15px}.character-counter{min-height:18px}[type="radio"]:not(:checked),[type="radio"]:checked{position:absolute;opacity:0;pointer-events:none}[type="radio"]:not(:checked)+span,[type="radio"]:checked+span{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;-webkit-transition:.28s ease;transition:.28s ease;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}[type="radio"]+span:before,[type="radio"]+span:after{content:'';position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;-webkit-transition:.28s ease;transition:.28s ease}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after,[type="radio"]:checked+span:before,[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border-radius:50%}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after{border:2px solid #5a5a5a}[type="radio"]:not(:checked)+span:after{-webkit-transform:scale(0);transform:scale(0)}[type="radio"]:checked+span:before{border:2px solid transparent}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border:2px solid #26a69a}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:after{background-color:#26a69a}[type="radio"]:checked+span:after{-webkit-transform:scale(1.02);transform:scale(1.02)}[type="radio"].with-gap:checked+span:after{-webkit-transform:scale(0.5);transform:scale(0.5)}[type="radio"].tabbed:focus+span:before{-webkit-box-shadow:0 0 0 10px rgba(0,0,0,0.1);box-shadow:0 0 0 10px rgba(0,0,0,0.1)}[type="radio"].with-gap:disabled:checked+span:before{border:2px solid rgba(0,0,0,0.42)}[type="radio"].with-gap:disabled:checked+span:after{border:none;background-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before,[type="radio"]:disabled:checked+span:before{background-color:transparent;border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled+span{color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before{border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:checked+span:after{background-color:rgba(0,0,0,0.42);border-color:#949494}[type="checkbox"]:not(:checked),[type="checkbox"]:checked{position:absolute;opacity:0;pointer-events:none}[type="checkbox"]+span:not(.lever){position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}[type="checkbox"]+span:not(.lever):before,[type="checkbox"]:not(.filled-in)+span:not(.lever):after{content:'';position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #5a5a5a;border-radius:1px;margin-top:3px;-webkit-transition:.2s;transition:.2s}[type="checkbox"]:not(.filled-in)+span:not(.lever):after{border:0;-webkit-transform:scale(0);transform:scale(0)}[type="checkbox"]:not(:checked):disabled+span:not(.lever):before{border:none;background-color:rgba(0,0,0,0.42)}[type="checkbox"].tabbed:focus+span:not(.lever):after{-webkit-transform:scale(1);transform:scale(1);border:0;border-radius:50%;-webkit-box-shadow:0 0 0 10px rgba(0,0,0,0.1);box-shadow:0 0 0 10px rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.1)}[type="checkbox"]:checked+span:not(.lever):before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #26a69a;border-bottom:2px solid #26a69a;-webkit-transform:rotate(40deg);transform:rotate(40deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"]:checked:disabled+span:before{border-right:2px solid rgba(0,0,0,0.42);border-bottom:2px solid rgba(0,0,0,0.42)}[type="checkbox"]:indeterminate+span:not(.lever):before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #26a69a;border-bottom:none;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"]:indeterminate:disabled+span:not(.lever):before{border-right:2px solid rgba(0,0,0,0.42);background-color:transparent}[type="checkbox"].filled-in+span:not(.lever):after{border-radius:2px}[type="checkbox"].filled-in+span:not(.lever):before,[type="checkbox"].filled-in+span:not(.lever):after{content:'';left:0;position:absolute;-webkit-transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;z-index:1}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;-webkit-transform:rotateZ(37deg);transform:rotateZ(37deg);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):after{height:20px;width:20px;background-color:transparent;border:2px solid #5a5a5a;top:0px;z-index:0}[type="checkbox"].filled-in:checked+span:not(.lever):before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;-webkit-transform:rotateZ(37deg);transform:rotateZ(37deg);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"].filled-in:checked+span:not(.lever):after{top:0;width:20px;height:20px;border:2px solid #26a69a;background-color:#26a69a;z-index:0}[type="checkbox"].filled-in.tabbed:focus+span:not(.lever):after{border-radius:2px;border-color:#5a5a5a;background-color:rgba(0,0,0,0.1)}[type="checkbox"].filled-in.tabbed:checked:focus+span:not(.lever):after{border-radius:2px;background-color:#26a69a;border-color:#26a69a}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):before{background-color:transparent;border:2px solid transparent}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):after{border-color:transparent;background-color:#949494}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):before{background-color:transparent}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):after{background-color:#949494;border-color:#949494}.switch,.switch *{-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#84c7c1}.switch label input[type=checkbox]:checked+.lever:before,.switch label input[type=checkbox]:checked+.lever:after{left:18px}.switch label input[type=checkbox]:checked+.lever:after{background-color:#26a69a}.switch label .lever{content:"";display:inline-block;position:relative;width:36px;height:14px;background-color:rgba(0,0,0,0.38);border-radius:15px;margin-right:10px;-webkit-transition:background 0.3s ease;transition:background 0.3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:before,.switch label .lever:after{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;-webkit-transition:left 0.3s ease, background .3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease;transition:left 0.3s ease, background .3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease}.switch label .lever:before{background-color:rgba(38,166,154,0.15)}.switch label .lever:after{background-color:#F1F1F1;-webkit-box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12);box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)}input[type=checkbox]:checked:not(:disabled) ~ .lever:active::before,input[type=checkbox]:checked:not(:disabled).tabbed:focus ~ .lever::before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(38,166,154,0.15)}input[type=checkbox]:not(:disabled) ~ .lever:active:before,input[type=checkbox]:not(:disabled).tabbed:focus ~ .lever::before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(0,0,0,0.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(0,0,0,0.12)}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#949494}select{display:none}select.browser-default{display:block}select{background-color:rgba(255,255,255,0.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper.valid+label,.select-wrapper.invalid+label{width:100%;pointer-events:none}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;display:block;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;z-index:1}.select-wrapper input.select-dropdown:focus{border-bottom:1px solid #26a69a}.select-wrapper .caret{position:absolute;right:0;top:0;bottom:0;margin:auto 0;z-index:0;fill:rgba(0,0,0,0.87)}.select-wrapper+label{position:absolute;top:-26px;font-size:.8rem}select:disabled{color:rgba(0,0,0,0.42)}.select-wrapper.disabled+label{color:rgba(0,0,0,0.42)}.select-wrapper.disabled .caret{fill:rgba(0,0,0,0.42)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,0.42);cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.select-wrapper i{color:rgba(0,0,0,0.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,0.3);background-color:transparent}body.keyboard-focused .select-dropdown.dropdown-content li:focus{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li:hover{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li.selected{background-color:rgba(0,0,0,0.03)}.prefix ~ .select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix ~ label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,0.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,0.4)}.select-dropdown li.optgroup ~ li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large,.file-field .btn-small{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.file-field input[type=file]::-webkit-file-upload-button{display:none}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;top:10px;left:0;border:none;height:0;width:0;border-radius:50%;background-color:#26a69a;margin-left:7px;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#26a69a;font-size:0;-webkit-transform:rotate(45deg);transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s;-webkit-appearance:none;background-color:#26a69a;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;margin:-5px 0 0 0}.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb{-webkit-box-shadow:0 0 0 10px rgba(38,166,154,0.26);box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]{border:1px solid white}input[type=range]::-moz-range-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-moz-focus-inner{border:0}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s}.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{display:inline-block;font-weight:300;color:#757575;padding-left:16px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:15px;border-left:1px solid #ee6e73}.table-of-contents a.active{font-weight:500;padding-left:14px;border-left:2px solid #ee6e73}.sidenav{position:fixed;width:300px;left:0;top:0;margin:0;-webkit-transform:translateX(-100%);transform:translateX(-100%);height:100%;height:calc(100% + 60px);height:-moz-calc(100%);padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform:translateX(-105%);transform:translateX(-105%)}.sidenav.right-aligned{right:0;-webkit-transform:translateX(105%);transform:translateX(105%);left:auto;-webkit-transform:translateX(100%);transform:translateX(100%)}.sidenav .collapsible{margin:0}.sidenav li{float:none;line-height:48px}.sidenav li.active{background-color:rgba(0,0,0,0.05)}.sidenav li>a{color:rgba(0,0,0,0.87);display:block;font-size:14px;font-weight:500;height:48px;line-height:48px;padding:0 32px}.sidenav li>a:hover{background-color:rgba(0,0,0,0.05)}.sidenav li>a.btn,.sidenav li>a.btn-large,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating{margin:10px 15px}.sidenav li>a.btn,.sidenav li>a.btn-large,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-floating{color:#fff}.sidenav li>a.btn-flat{color:#343434}.sidenav li>a.btn:hover,.sidenav li>a.btn-large:hover,.sidenav li>a.btn-small:hover,.sidenav li>a.btn-large:hover{background-color:#2bbbad}.sidenav li>a.btn-floating:hover{background-color:#26a69a}.sidenav li>a>i,.sidenav li>a>[class^="mdi-"],.sidenav li>a li>a>[class*="mdi-"],.sidenav li>a>i.material-icons{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,0.54)}.sidenav .divider{margin:8px 0 0 0}.sidenav .subheader{cursor:initial;pointer-events:none;color:rgba(0,0,0,0.54);font-size:14px;font-weight:500;line-height:48px}.sidenav .subheader:hover{background-color:transparent}.sidenav .user-view{position:relative;padding:32px 32px 0;margin-bottom:8px}.sidenav .user-view>a{height:auto;padding:0}.sidenav .user-view>a:hover{background-color:transparent}.sidenav .user-view .background{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.sidenav .user-view .circle,.sidenav .user-view .name,.sidenav .user-view .email{display:block}.sidenav .user-view .circle{height:64px;width:64px}.sidenav .user-view .name,.sidenav .user-view .email{font-size:14px;line-height:24px}.sidenav .user-view .name{margin-top:16px;font-weight:500}.sidenav .user-view .email{padding-bottom:16px;font-weight:400}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.drag-target.right-aligned{right:0}.sidenav.sidenav-fixed{left:0;-webkit-transform:translateX(0);transform:translateX(0);position:fixed}.sidenav.sidenav-fixed.right-aligned{right:0;left:auto}@media only screen and (max-width: 992px){.sidenav.sidenav-fixed{-webkit-transform:translateX(-105%);transform:translateX(-105%)}.sidenav.sidenav-fixed.right-aligned{-webkit-transform:translateX(105%);transform:translateX(105%)}.sidenav>a{padding:0 16px}.sidenav .user-view{padding:16px 16px 0}}.sidenav .collapsible-body>ul:not(.collapsible)>li.active,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active{background-color:#ee6e73}.sidenav .collapsible-body>ul:not(.collapsible)>li.active a,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active a{color:#fff}.sidenav .collapsible-body{padding:0}.sidenav-overlay{position:fixed;top:0;left:0;right:0;opacity:0;height:120vh;background-color:rgba(0,0,0,0.5);z-index:997;display:none}.preloader-wrapper{display:inline-block;position:relative;width:50px;height:50px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(360deg)}}@keyframes container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#26a69a}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only,.active .spinner-layer.spinner-green-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg)}}@keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@-webkit-keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}@keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent !important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent !important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent !important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes left-spin{from{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes right-spin{from{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1);animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1)}@-webkit-keyframes fade-out{from{opacity:1}to{opacity:0}}@keyframes fade-out{from{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:center}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;-webkit-transition:background-color .3s;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4CAF50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;-webkit-perspective:500px;perspective:500px;-webkit-transform-style:preserve-3d;transform-style:preserve-3d;-webkit-transform-origin:0% 50%;transform-origin:0% 50%}.carousel.carousel-slider{top:0;left:0}.carousel.carousel-slider .carousel-fixed-item{position:absolute;left:0;right:0;bottom:20px;z-index:1}.carousel.carousel-slider .carousel-fixed-item.with-indicators{bottom:68px}.carousel.carousel-slider .carousel-item{width:100%;height:100%;min-height:400px;position:absolute;top:0;left:0}.carousel.carousel-slider .carousel-item h2{font-size:24px;font-weight:500;line-height:32px}.carousel.carousel-slider .carousel-item p{font-size:15px}.carousel .carousel-item{visibility:hidden;width:200px;height:200px;position:absolute;top:0;left:0}.carousel .carousel-item>img{width:100%}.carousel .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.carousel .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:8px;width:8px;margin:24px 4px;background-color:rgba(255,255,255,0.5);-webkit-transition:background-color .3s;transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:#fff}.carousel.scrolling .carousel-item .materialboxed,.carousel .carousel-item:not(.active) .materialboxed{pointer-events:none}.tap-target-wrapper{width:800px;height:800px;position:fixed;z-index:1000;visibility:hidden;-webkit-transition:visibility 0s .3s;transition:visibility 0s .3s}.tap-target-wrapper.open{visibility:visible;-webkit-transition:visibility 0s;transition:visibility 0s}.tap-target-wrapper.open .tap-target{-webkit-transform:scale(1);transform:scale(1);opacity:.95;-webkit-transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-wrapper.open .tap-target-wave::before{-webkit-transform:scale(1);transform:scale(1)}.tap-target-wrapper.open .tap-target-wave::after{visibility:visible;-webkit-animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;-webkit-transition:opacity .3s,\r visibility 0s 1s,\r -webkit-transform .3s;transition:opacity .3s,\r visibility 0s 1s,\r -webkit-transform .3s;transition:opacity .3s,\r transform .3s,\r visibility 0s 1s;transition:opacity .3s,\r transform .3s,\r visibility 0s 1s,\r -webkit-transform .3s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#ee6e73;-webkit-box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);width:100%;height:100%;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave::before,.tap-target-wave::after{content:'';display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#ffffff}.tap-target-wave::before{-webkit-transform:scale(0);transform:scale(0);-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s, -webkit-transform .3s}.tap-target-wave::after{visibility:hidden;-webkit-transition:opacity .3s,\r visibility 0s,\r -webkit-transform .3s;transition:opacity .3s,\r visibility 0s,\r -webkit-transform .3s;transition:opacity .3s,\r transform .3s,\r visibility 0s;transition:opacity .3s,\r transform .3s,\r visibility 0s,\r -webkit-transform .3s;z-index:-1}.tap-target-origin{top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);z-index:10002;position:absolute !important}.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small),.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small):hover{background:none}@media only screen and (max-width: 600px){.tap-target,.tap-target-wrapper{width:600px;height:600px}}.pulse{overflow:visible;position:relative}.pulse::before{content:'';display:block;position:absolute;width:100%;height:100%;top:0;left:0;background-color:inherit;border-radius:inherit;-webkit-transition:opacity .3s, -webkit-transform .3s;transition:opacity .3s, -webkit-transform .3s;transition:opacity .3s, transform .3s;transition:opacity .3s, transform .3s, -webkit-transform .3s;-webkit-animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;z-index:-1}@-webkit-keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}100%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}@keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}100%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}.datepicker-modal{max-width:325px;min-width:300px;max-height:none}.datepicker-container.modal-content{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:0}.datepicker-controls{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;width:280px;margin:0 auto}.datepicker-controls .selects-container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.datepicker-controls .select-wrapper input{border-bottom:none;text-align:center;margin:0}.datepicker-controls .select-wrapper input:focus{border-bottom:none}.datepicker-controls .select-wrapper .caret{display:none}.datepicker-controls .select-year input{width:50px}.datepicker-controls .select-month input{width:70px}.month-prev,.month-next{margin-top:4px;cursor:pointer;background-color:transparent;border:none}.datepicker-date-display{-webkit-box-flex:1;-webkit-flex:1 auto;-ms-flex:1 auto;flex:1 auto;background-color:#26a69a;color:#fff;padding:20px 22px;font-weight:500}.datepicker-date-display .year-text{display:block;font-size:1.5rem;line-height:25px;color:rgba(255,255,255,0.7)}.datepicker-date-display .date-text{display:block;font-size:2.8rem;line-height:47px;font-weight:500}.datepicker-calendar-container{-webkit-box-flex:2.5;-webkit-flex:2.5 auto;-ms-flex:2.5 auto;flex:2.5 auto}.datepicker-table{width:280px;font-size:1rem;margin:0 auto}.datepicker-table thead{border-bottom:none}.datepicker-table th{padding:10px 5px;text-align:center}.datepicker-table tr{border:none}.datepicker-table abbr{text-decoration:none;color:#999}.datepicker-table td{border-radius:50%;padding:0}.datepicker-table td.is-today{color:#26a69a}.datepicker-table td.is-selected{background-color:#26a69a;color:#fff}.datepicker-table td.is-outside-current-month,.datepicker-table td.is-disabled{color:rgba(0,0,0,0.3);pointer-events:none}.datepicker-day-button{background-color:transparent;border:none;line-height:38px;display:block;width:100%;border-radius:50%;padding:0 5px;cursor:pointer;color:inherit}.datepicker-day-button:focus{background-color:rgba(43,161,150,0.25)}.datepicker-footer{width:280px;margin:0 auto;padding-bottom:5px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.datepicker-cancel,.datepicker-clear,.datepicker-today,.datepicker-done{color:#26a69a;padding:0 1rem}.datepicker-clear{color:#F44336}@media only screen and (min-width: 601px){.datepicker-modal{max-width:625px}.datepicker-container.modal-content{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.datepicker-date-display{-webkit-box-flex:0;-webkit-flex:0 1 270px;-ms-flex:0 1 270px;flex:0 1 270px}.datepicker-controls,.datepicker-table,.datepicker-footer{width:320px}.datepicker-day-button{line-height:44px}}.timepicker-modal{max-width:325px;max-height:none}.timepicker-container.modal-content{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:0}.text-primary{color:#fff}.timepicker-digital-display{-webkit-box-flex:1;-webkit-flex:1 auto;-ms-flex:1 auto;flex:1 auto;background-color:#26a69a;padding:10px;font-weight:300}.timepicker-text-container{font-size:4rem;font-weight:bold;text-align:center;color:rgba(255,255,255,0.6);font-weight:400;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.timepicker-span-hours,.timepicker-span-minutes,.timepicker-span-am-pm div{cursor:pointer}.timepicker-span-hours{margin-right:3px}.timepicker-span-minutes{margin-left:3px}.timepicker-display-am-pm{font-size:1.3rem;position:absolute;right:1rem;bottom:1rem;font-weight:400}.timepicker-analog-display{-webkit-box-flex:2.5;-webkit-flex:2.5 auto;-ms-flex:2.5 auto;flex:2.5 auto}.timepicker-plate{background-color:#eee;border-radius:50%;width:270px;height:270px;overflow:visible;position:relative;margin:auto;margin-top:25px;margin-bottom:5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.timepicker-canvas,.timepicker-dial{position:absolute;left:0;right:0;top:0;bottom:0}.timepicker-minutes{visibility:hidden}.timepicker-tick{border-radius:50%;color:rgba(0,0,0,0.87);line-height:40px;text-align:center;width:40px;height:40px;position:absolute;cursor:pointer;font-size:15px}.timepicker-tick.active,.timepicker-tick:hover{background-color:rgba(38,166,154,0.25)}.timepicker-dial{-webkit-transition:opacity 350ms, -webkit-transform 350ms;transition:opacity 350ms, -webkit-transform 350ms;transition:transform 350ms, opacity 350ms;transition:transform 350ms, opacity 350ms, -webkit-transform 350ms}.timepicker-dial-out{opacity:0}.timepicker-dial-out.timepicker-hours{-webkit-transform:scale(1.1, 1.1);transform:scale(1.1, 1.1)}.timepicker-dial-out.timepicker-minutes{-webkit-transform:scale(0.8, 0.8);transform:scale(0.8, 0.8)}.timepicker-canvas{-webkit-transition:opacity 175ms;transition:opacity 175ms}.timepicker-canvas line{stroke:#26a69a;stroke-width:4;stroke-linecap:round}.timepicker-canvas-out{opacity:0.25}.timepicker-canvas-bearing{stroke:none;fill:#26a69a}.timepicker-canvas-bg{stroke:none;fill:#26a69a}.timepicker-footer{margin:0 auto;padding:5px 1rem;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.timepicker-clear{color:#F44336}.timepicker-close{color:#26a69a}.timepicker-clear,.timepicker-close{padding:0 20px}@media only screen and (min-width: 601px){.timepicker-modal{max-width:600px}.timepicker-container.modal-content{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.timepicker-text-container{top:32%}.timepicker-display-am-pm{position:relative;right:auto;bottom:auto;text-align:center;margin-top:1.2rem}}
index 66fd282..2a9b0ee 100644 (file)
@@ -8,7 +8,7 @@
 
 /* Use this next selector to style things like font-size and line-height: */
 .tooltipster-default .tooltipster-content {
-       font-family: Arial, sans-serif;
+       font-family: sans-serif;
        font-size: 14px;
        line-height: 16px;
        padding: 8px 10px;
index 0d1c89d..3f3372e 100644 (file)
@@ -786,7 +786,7 @@ body .ui-tooltip {
 /* Component containers
 ----------------------------------*/
 .ui-widget {
-       font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;
+       font-family: sans-serif;
        font-size: 1.1em;
 }
 .ui-widget .ui-widget {
@@ -796,7 +796,7 @@ body .ui-tooltip {
 .ui-widget select,
 .ui-widget textarea,
 .ui-widget button {
-       font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;
+       font-family: sans-serif;
        font-size: 1em;
 }
 .ui-widget-content {
diff --git a/css/webshop.css b/css/webshop.css
new file mode 100644 (file)
index 0000000..08bb2b8
--- /dev/null
@@ -0,0 +1,49 @@
+div.shop_table {
+  display: table;
+  background-color: #ebebeb;
+  /* padding: 15px;
+  margin: 15px; */
+}
+div.shop_table_address {
+  display: table;
+  /* padding: 15px; */
+  margin: 0px 0px 0px 15px;
+}
+div.shop_table-row {
+  display: table-row;
+}
+div.shop_table-cell {
+  display: table-cell;
+  padding: 20px,20px,20px,20px;
+}
+div.shop_table-cell_colspan {
+  flex: 2;
+  align: center;
+}
+div.shop_main{
+  width: 100%;
+  padding: 15px,15px,15px,15px;
+  margin: 15px,15px,15px,15px;
+}
+.shop_fleft {
+  width: 30%;
+  float: left;
+  border: thin solid red;
+}
+
+div.shop_table_info {
+  color: #b3b3b3;
+  width: 100%;
+  align: left;
+}
+
+.shop_transferable{
+  background:rgba(43, 208, 54, 1);
+}
+.shop_alert{
+  background:rgba(255, 0, 0, 1);
+}
+.shop_delivery{
+  background:rgba(100, 100, 100, 1);
+}
+
diff --git a/debian/.dummy b/debian/.dummy
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/debian/mkivitendo.changelog b/debian/mkivitendo.changelog
new file mode 100644 (file)
index 0000000..d84e814
--- /dev/null
@@ -0,0 +1,4 @@
+kivitendo-erp (0.1-%BUILD%) unstable; urgency=medium
+  * start of mebil mapping
+ -- Michael Wagner <info@wagnertech.de>  Tue, 08 Feb 2022 20:03:04 +0100
+
diff --git a/debian/mkivitendo.control b/debian/mkivitendo.control
new file mode 100644 (file)
index 0000000..4b2e27d
--- /dev/null
@@ -0,0 +1,13 @@
+Source: kivitendo-erp
+Section: main
+Priority: optional
+Maintainer: Michael Wagner <michael@wagnertech.de>
+Build-Depends: git,mbuild
+Package: mkivitendo
+Section: main
+Priority: optional
+Architecture: all
+Depends: kivitendo
+Description: kivitendo-ERP
+
diff --git a/debian/mkivitendo.cp b/debian/mkivitendo.cp
new file mode 100755 (executable)
index 0000000..7ee781c
--- /dev/null
@@ -0,0 +1,16 @@
+#!/bin/bash
+set -e
+
+# parameter: $1: base dir for copy (optional)
+
+. debian/setenv.sh
+mkdir -p $1/opt/mkivitendo
+
+git diff --stat release-3.6.1 | grep -v debian |grep -v "files changed"| grep -v ".dummy" |sed "s/ //" | sed "s/ .*//" >git.diff
+
+while read line
+do
+       path=${line%.*}
+       mkdir -p $1/opt/mkivitendo/$path
+       cp $line $1/opt/mkivitendo/$path/
+done <git.diff
diff --git a/debian/mkivitendo.postinst b/debian/mkivitendo.postinst
new file mode 100755 (executable)
index 0000000..ecff89e
--- /dev/null
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -ex
+
+rsync -av /opt/mkivitendo/ /opt/kivitendo-erp
+
diff --git a/debian/mkivitendo.prepare b/debian/mkivitendo.prepare
new file mode 100755 (executable)
index 0000000..4fcc49f
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/bash
+set -e
+
+# bestimme Versionsnummer aus VERSION
+base_version=$(cat VERSION)
+
+# Hänge Version aus changelog an
+vline=$(head -1 debian/$paket.changelog)
+vline=${vline%-*}
+cl_version=${vline#*(}
+
+echo "version=$base_version.$cl_version" >> debian/rules.pre
+echo "version=$base_version.$cl_version" >> debian/setenv.sh
+echo "base_version=$base_version" >> debian/setenv.sh
+
index 7d2dfb5..9b7c98b 100755 (executable)
@@ -2,16 +2,22 @@
 
 use strict;
 
-use FCGI;
+BEGIN {
+  use FindBin;
+
+  unshift(@INC, $FindBin::Bin . '/modules/override'); # Use our own versions of various modules (e.g. YAML).
+  push   (@INC, $FindBin::Bin);                       # '.' will be removed from @INC soon.
+  push   (@INC, $FindBin::Bin . '/modules/fallback'); # Only use our own versions of modules if there's no system version.
+}
+
 use SL::Dispatcher;
 use SL::FCGIFixes;
+use SL::LXDebug;
 
 our $dispatcher = SL::Dispatcher->new('FastCGI');
 $dispatcher->pre_startup_setup;
 SL::FCGIFixes::apply_fixes();
 $dispatcher->pre_startup_checks;
-
-my $request = FCGI::Request();
-$dispatcher->handle_request($request) while $request->Accept() >= 0;
+$dispatcher->handle_all_requests;
 
 1;
index 433ffe9..a6364ab 100755 (executable)
@@ -2,6 +2,13 @@
 
 use strict;
 
+BEGIN {
+  use FindBin;
+
+  unshift(@INC, $FindBin::Bin . '/modules/override'); # Use our own versions of various modules (e.g. YAML).
+  push   (@INC, $FindBin::Bin);                       # '.' will be removed from @INC soon.
+}
+
 use SL::Dispatcher;
 
 our $dispatcher = SL::Dispatcher->new('CGI');
diff --git a/doc/DATEV-2015/EXTF_Anlag-Buchungen.csv b/doc/DATEV-2015/EXTF_Anlag-Buchungen.csv
new file mode 100644 (file)
index 0000000..12d51fd
--- /dev/null
@@ -0,0 +1,3 @@
+"EXTF";510;63;"Anlagenbuchführung - Buchungssatzvorlagen";1;20150729103107277;;"RE";"Admin";"";29098;55003;20150101;4;;;"";"";;;;"";;"";;;"";;;"";""\r
+Bereich;Kontonummer;Buchungssatztyp;KontonummerSoll;KontonummerHaben;Buchungstext\r
+30;400;"N";4830;400;"Normalabschreibung"\r
diff --git a/doc/DATEV-2015/EXTF_Anlag-Filialen.csv b/doc/DATEV-2015/EXTF_Anlag-Filialen.csv
new file mode 100644 (file)
index 0000000..3f75ae9
--- /dev/null
@@ -0,0 +1,3 @@
+"EXTF";510;62;"Anlagenbuchführung - Filialen";1;20150729103107277;;"RE";"Admin";"";29098;55003;20150101;4;;;"";"";;;;"";;"";;;"";;;"";""\r
+Filialnummer;Filialbezeichnung\r
+1;"Hauptbetrieb"\r
diff --git a/doc/DATEV-2015/EXTF_Buchungsstapel.csv b/doc/DATEV-2015/EXTF_Buchungsstapel.csv
new file mode 100644 (file)
index 0000000..debb060
--- /dev/null
@@ -0,0 +1,40 @@
+"EXTF";510;21;"Buchungsstapel";7;20150729093158705;;"SV";"Admin";"";55003;63021;20160101;4;20160101;20160331;"Kasse";"";1;0;0;"EUR";;"KP";;"";;;"";"";\r
+Umsatz (ohne Soll/Haben-Kz);Soll/Haben-Kennzeichen;WKZ Umsatz;Kurs;Basis-Umsatz;WKZ Basis-Umsatz;Konto;Gegenkonto (ohne BU-Schlüssel);BU-Schlüssel;Belegdatum;Belegfeld 1;Belegfeld 2;Skonto;Buchungstext;Postensperre;Diverse Adressnummer;Geschäftspartnerbank;Sachverhalt;Zinssperre;Beleglink;Beleginfo - Art 1;Beleginfo - Inhalt 1;Beleginfo - Art 2;Beleginfo - Inhalt 2;Beleginfo - Art 3;Beleginfo - Inhalt 3;Beleginfo - Art 4;Beleginfo - Inhalt 4;Beleginfo - Art 5;Beleginfo - Inhalt 5;Beleginfo - Art 6;Beleginfo - Inhalt 6;Beleginfo - Art 7;Beleginfo - Inhalt 7;Beleginfo - Art 8;Beleginfo - Inhalt 8;KOST1 - Kostenstelle;KOST2 - Kostenstelle;Kost-Menge;EU-Land u. UStID;EU-Steuersatz;Abw. Versteuerungsart;Sachverhalt L+L;Funktionsergänzung L+L;BU 49 Hauptfunktionstyp;BU 49 Hauptfunktionsnummer;BU 49 Funktionsergänzung;Zusatzinformation - Art 1;Zusatzinformation- Inhalt 1;Zusatzinformation - Art 2;Zusatzinformation- Inhalt 2;Zusatzinformation - Art 3;Zusatzinformation- Inhalt 3;Zusatzinformation - Art 4;Zusatzinformation- Inhalt 4;Zusatzinformation - Art 5;Zusatzinformation- Inhalt 5;Zusatzinformation - Art 6;Zusatzinformation- Inhalt 6;Zusatzinformation - Art 7;Zusatzinformation- Inhalt 7;Zusatzinformation - Art 8;Zusatzinformation- Inhalt 8;Zusatzinformation - Art 9;Zusatzinformation- Inhalt 9;Zusatzinformation - Art 10;Zusatzinformation- Inhalt 10;Zusatzinformation - Art 11;Zusatzinformation- Inhalt 11;Zusatzinformation - Art 12;Zusatzinformation- Inhalt 12;Zusatzinformation - Art 13;Zusatzinformation- Inhalt 13;Zusatzinformation - Art 14;Zusatzinformation- Inhalt 14;Zusatzinformation - Art 15;Zusatzinformation- Inhalt 15;Zusatzinformation - Art 16;Zusatzinformation- Inhalt 16;Zusatzinformation - Art 17;Zusatzinformation- Inhalt 17;Zusatzinformation - Art 18;Zusatzinformation- Inhalt 18;Zusatzinformation - Art 19;Zusatzinformation- Inhalt 19;Zusatzinformation - Art 20;Zusatzinformation- Inhalt 20;Stück;Gewicht;Zahlweise;Forderungsart;Veranlagungsjahr;Zugeordnete Fälligkeit;Skontotyp;Auftragsnummer;Buchungstyp;Ust-Schlüssel (Anzahlungen);EU-Land (Anzahlungen);Sachverhalt L+L (Anzahlungen);EU-Steuersatz (Anzahlungen);Erlöskonto (Anzahlungen);Herkunft-Kz;Leerfeld;KOST-Datum;Mandatsreferenz;Skontosperre;Gesellschaftername;Beteiligtennummer;Identifikationsnummer;Zeichnernummer;Postensperre bis;Bezeichnung SoBil-Sachverhalt;Kennzeichen SoBil-Buchung;Festschreibung;Leistungsdatum;Datum Zuord.Steuerperiode\r
+100,18;"S";"";;;"";48400;8401;"";3101;"";"";;"Test Anzahlung";;"";1;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"50";"";;"";;"";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";2012;;1;"Projekt 4711";"AG";3;"";;;8070;"WK";"";;"";;"";;"";"";;"";;1;;\r
+10,00;"S";"";;;"";48220;8400;"";3103;"";"";;"Normalabschr. immater. VermG";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"90";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;10022016;\r
+64083;"S";"";;;"";4400;85;"";3101;"";"";;"Normalabschreibung Gebäude";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"50";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;0;;\r
+3584,56;"S";"";;;"";4831;100;"";3101;"";"";;"Normalabschreibung Gebäude";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"50";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;0;;\r
+3745,56;"S";"";;;"";4830;210;"";3101;"";"";;"Normalabschreibung Sachanlagen";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"50";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+531,16;"S";"";;;"";4832;320;"";3101;"";"";;"Normalabschreibung Kfz";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"20";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";0;"";;;0;"WK";"";;"";;"";;"";"";;"";;1;;\r
+4979,65;"H";"";;;"";8400;30200;"";0902;"201202010";"";;"";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"202";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+11687,62;"H";"";;;"";8120;40000;"";0902;"201202011";"";;"";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"299";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+13968,83;"H";"";;;"";8125;40100;"";1602;"201202023";"";;"";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"299";"889";5;"DE133546770";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+11807,63;"H";"";;;"";8125;40100;"";1702;"201202024";"";;"";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"299";"";;"DE133546770";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+4120,51;"H";"";;;"";8405;30100;"";1702;"201202025";"";;"";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"201";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+16585,28;"H";"";;;"";8405;10200;"";1702;"201202026";"";;"";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"202";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+15301,67;"H";"";;;"";8400;10300;"";2002;"201202027";"";;"";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"201";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+7404,94;"H";"";;;"";8400;60391;"";2102;"201202028";"";;"";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"199";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+66976,12;"H";"";;;"";8407;10400;"";2202;"201202029";"";;"";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"201";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+118,55;"H";"";;;"";1000;1369;"";0202;"4";"";;"Test-Shop";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"Beleg fehlt";"Post";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+26,87;"H";"";;;"";1000;4930;"9";0102;"";"";;"Schreibwaren";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"90";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+460,00;"S";"";;;"";1000;1360;"";0702;"";"";;"Kasseneinlage";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+124,00;"H";"CHF";1,240402;100,00;"EUR";21100;8050;"";2702;"";"";;"Umsatz Schweiz";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+70,96;"H";"";;;"";1100;4530;"9";2702;"";"";;"Diesel";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"20";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+17107,00;"S";"";;;"";31100;8125;"";2702;"201202007";"";;"Ausland EU";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"520";"45";30;"ATU36251489";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";0;"";;;0;"WK";"";;"";;"";;"";"";;"";;0;;\r
+5856,00;"H";"";;;"";1100;980;"";2802;"";"";;"Gebäudeversicherung 03/12-02/13";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+70,27;"H";"";;;"";1100;4530;"9";2802;"";"";;"Benzin";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"20";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+1763,58;"H";"";;;"";1100;4580;"9";2802;"";"";;"Leasing Van";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"20";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+1190,00;"H";"";;;"";8050;10100;"3";1403;"AR1234";"";;"Aufteilung AR ohne Automatikkonto";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+700,00;"S";"";;;"";8050;8060;"";1403;"AR1234";"";;"Aufteilung auf Erlöskonto";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+300,00;"S";"";;;"";8050;8070;"";1403;"AR1234";"";;"Aufteilung auf Erlöskonto";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+1190,00;"H";"";;;"";8400;10100;"";1403;"AR2345";"";;"Aufteilung AR mit Automatikkonto";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+600,00;"S";"";;;"";8400;8401;"40";1403;"AR2345";"";;"Auftreilung auf Erlöskonto";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+400,00;"S";"";;;"";8400;8405;"40";1403;"AR2345";"";;"Auftreilung auf Erlöskonto";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+2500,00;"S";"";;;"";10100;8400;"";0103;"AR-78/13";"160316";;"Holzlieferung";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+2500,00;"S";"";;;"";1200;10100;"";0103;"AR-78/13";"";;"Bezahlung";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+119,00;"S";"";;;"";10100;8400;"";0103;"AR-456/13";"";;"Rechnung mit Skontogewährung";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+116,62;"S";"";;;"";1200;10100;"";0103;"AR-456/13";"";2,38;"Zahlung mit 2% Skonto";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+35,00;"S";"";;;"";10001;8400;"";0103;"AR-2013";"";;"falscher Debitor";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+35,00;"H";"";;;"";10001;8400;"20";0103;"AR-2013";"";;"Berichtigung";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+35,00;"S";"";;;"";10100;8400;"";0103;"AR-2013";"";;"richtiger Debitor";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
+488,00;"H";"";;;"";980;4360;"";0101;"";"";;"Gebäudeversicherung";;"";;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"10";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;;;"WK";"";;"";;"";;"";"";;"";;1;;\r
diff --git a/doc/DATEV-2015/EXTF_Buchungstextkonstanten.csv b/doc/DATEV-2015/EXTF_Buchungstextkonstanten.csv
new file mode 100644 (file)
index 0000000..2700c8a
--- /dev/null
@@ -0,0 +1,9 @@
+"EXTF";510;67;"Buchungstextkonstanten";1;20150729093349782;;"RE";"Admin";"";29098;55003;20150101;4;;;"";"";;;;"";;"";;;"";;;"";""\r
+Nummer;Buchungstext\r
+10;"Postwertzeichen"\r
+11;"Bürobedarf"\r
+20;"Benzin"\r
+21;"Diesel"\r
+22;"Kundendienst"\r
+90;"Kasseneinlage"\r
+91;"für Kasse"\r
diff --git a/doc/DATEV-2015/EXTF_Div-Adressen.csv b/doc/DATEV-2015/EXTF_Div-Adressen.csv
new file mode 100644 (file)
index 0000000..53d3285
--- /dev/null
@@ -0,0 +1,5 @@
+"EXTF";510;48;"Diverse Adressen";2;20150729103107277;;"RE";"Admin";"";55003;63021;20150101;4;;;"";"";;;;"";;"";;"";;;"";"";\r
+Adressnummer;Konto;Anrede;Name (Adressattyp Unternehmen);Unternehmensgegenstand;Kurzbezeichnung;Name (Adressattyp natürl. Person);Vorname (Adressattyp natürl. Person);Name (Adressattyp keine Angabe);Adressattyp;Titel/Akad. Grad;Adelstitel;Namensvorsatz;Abweichende Anrede;Adressart;Straße;Postfach;Postleitzahl;Ort;Land;Versandzusatz;Adresszusatz;Abw. Zustellbezeichnung 1;Abw. Zustellbezeichnung 2;Kennz. Korrespondenzadresse;Adresse Gültig von;Adresse Gültig bis;Abweichende Anrede (Rechnungsadresse);Adressart (Rechnungsadresse);Straße (Rechnungsadresse);Postfach (Rechnungsadresse);Postleitzahl (Rechnungsadresse);Ort (Rechnungsadresse);Land (Rechnungsadresse);Versandzusatz (Rechnungsadresse);Adresszusatz (Rechnungsadresse);Abw. Zustellbezeichnung 1 (Rechnungsadresse);Abw. Zustellbezeichnung 2 (Rechnungsadresse);Adresse Gültig von (Rechnungsadresse);Adresse Gültig bis (Rechnungsadresse);Telefon;Bemerkung (Telefon);Telefon GL;Bemerkung (Telefon GL);E-Mail;Bemerkung (E-Mail);Internet;Bemerkung (Internet);Fax;Bemerkung (Fax);Sonstige;Bemerkung (Sonstige);Bankleitzahl 1;Bankbezeichnung 1;Bank-Kontonummer 1;Länderkennzeichen 1;IBAN-Nr. 1;Leerfeld;SWIFT-Code 1;Abw. Kontoinhaber 1;Kennz. Hauptbankverb. 1;Bankverb 1 Gültig von;Bankverb 1 Gültig bis;Bankleitzahl 2;Bankbezeichnung 2;Bank-Kontonummer 2;Länderkennzeichen 2;IBAN-Nr. 2;Leerfeld;SWIFT-Code 2;Abw. Kontoinhaber 2;Kennz. Hauptbankverb. 2;Bankverb 2 Gültig von;Bankverb 2 Gültig bis;Bankleitzahl 3;Bankbezeichnung 3;Bank-Kontonummer 3;Länderkennzeichen 3;IBAN-Nr. 3;Leerfeld;SWIFT-Code 3;Abw. Kontoinhaber 3;Kennz. Hauptbankverb. 3;Bankverb 3 Gültig von;Bankverb 3 Gültig bis;Bankleitzahl 4;Bankbezeichnung 4;Bank-Kontonummer 4;Länderkennzeichen 4;IBAN-Nr. 4;Leerfeld;SWIFT-Code 4;Abw. Kontoinhaber 4;Kennz. Hauptbankverb. 4;Bankverb 4 Gültig von;Bankverb 4 Gültig bis;Bankleitzahl 5;Bankbezeichnung 5;Bank-Kontonummer 5;Länderkennzeichen 5;IBAN-Nr. 5;Leerfeld;SWIFT-Code 5;Abw. Kontoinhaber 5;Kennz. Hauptbankverb. 5;Bankverb 5 Gültig von;Bankverb 5 Gültig bis;Bankleitzahl 6;Bankbezeichnung 6;Bank-Kontonummer 6;Länderkennzeichen 6;IBAN-Nr. 6;Leerfeld;SWIFT-Code 6;Abw. Kontoinhaber 6;Kennz. Hauptbankverb. 6;Bankverb 6 Gültig von;Bankverb 6 Gültig bis;Bankleitzahl 7;Bankbezeichnung 7;Bank-Kontonummer 7;Länderkennzeichen 7;IBAN-Nr. 7;Leerfeld;SWIFT-Code 7;Abw. Kontoinhaber 7;Kennz. Hauptbankverb. 7;Bankverb 7 Gültig von;Bankverb 7 Gültig bis;Bankleitzahl 8;Bankbezeichnung 8;Bank-Kontonummer 8;Länderkennzeichen 8;IBAN-Nr. 8;Leerfeld;SWIFT-Code 8;Abw. Kontoinhaber 8;Kennz. Hauptbankverb. 8;Bankverb 8 Gültig von;Bankverb 8 Gültig bis;Bankleitzahl 9;Bankbezeichnung 9;Bank-Kontonummer 9;Länderkennzeichen 9;IBAN-Nr. 9;Leerfeld;SWIFT-Code 9;Abw. Kontoinhaber 9;Kennz. Hauptbankverb. 9;Bankverb 9 Gültig von;Bankverb 9 Gültig bis;Bankleitzahl 10;Bankbezeichnung 10;Bank-Kontonummer 10;Länderkennzeichen 10;IBAN-Nr. 10;Leerfeld;SWIFT-Code 10;Abw. Kontoinhaber 10;Kennz. Hauptbankverb. 10;Bankverb 10 Gültig von;Bankverb 10 Gültig bis;Kundennummer;Ansprechpartner;Vertreter;Sachbearbeiter;Briefanrede;Grußformel;Sprache;Ausgabeziel;Indiv. Feld 1;Indiv. Feld 2;Indiv. Feld 3;Indiv. Feld 4;Indiv. Feld 5;Indiv. Feld 6;Indiv. Feld 7;Indiv. Feld 8;Indiv. Feld 9;Indiv. Feld 10;Mandatsreferenz 1;Mandatsreferenz 2;Mandatsreferenz 3;Mandatsreferenz 4;Mandatsreferenz 5;Mandatsreferenz 6;Mandatsreferenz 7;Mandatsreferenz 8;Mandatsreferenz 9;Mandatsreferenz 10;Nummer Fremdsystem\r
+"DIV500";30000;"Firma";"Testmöbel GmbH";"";"Testmöbel GmbH";"";"";"";"2";"";"";"";"";"STR";"Feldweg 28";"";"90409";"Nürnberg";"DE";"";"";"";"";1;;;"";"";"";"";"";"";"";"";"";"";"";;;"+49 911 12345678";"";"+49 911 12345678";"";"test@testmöbel.de";"";"www.testmöbel.de";"";"+49 911 987654321";"";"";"";"76069512";"Raiffbk Knoblauchsland";"1122334455";"DE";"";"0";"GENODEF1N08";"Herr Muster";"1";01012013;31122013;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"KDN 75";"Herr Müllermuster";"Frau Mustermann";"Herr Testmann";"";"Mit freundlichen Grüßen";1;3;"kein Muster";"Beispiel";"Testeingabe";"";"";"";"";"";"";"Muster vorhanden";"";"";"";"";"";"";"";"";"";"";""\r
+"DIV600";30000;"Herrn";"";"";"Mustermann";"Mustermann";"";"";"1";"Dr.";"Baron";"zu";"";"STR";"Musterweg 5";"";"90000";"Nürnberg";"DE";"Nicht nachsenden";"";"";"";1;;;"";"";"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";""\r
+"DIV700";30000;"Frau";"";"";"Testmann, Elke";"Testmann";"Elke";"";"1";"";"Landgräfin";"vom und zu";"";"STR";"Wiesenweg 125";"";"90600";"Fürth";"DE";"Bei Umzug bitte mit neuer Anschrift zurück";"";"";"";1;;;"";"";"";"";"";"";"";"";"";"";"";;;"+49 911 505050";"immer erreichbar";"+49 911 505050";"immer erreichbar";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"76000000";"BBk Nürnberg";"998877";"DE";"DE83760000000000998877";"1";"MARKDEF1760";"Frau Beispiel";"1";01012013;12122013;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"Mit den besten Empfehlungen";4;1;"nur zum Test";"nur als Beispiel";"";"nur als Beispiel";"";"nur als Beispiel";"nur zum Test";"nur zum Test";"";"nur als Beispiel";"";"";"";"";"";"";"";"";"";"";""\r
diff --git a/doc/DATEV-2015/EXTF_Sachkontobeschriftungen.csv b/doc/DATEV-2015/EXTF_Sachkontobeschriftungen.csv
new file mode 100644 (file)
index 0000000..0984bdf
--- /dev/null
@@ -0,0 +1,178 @@
+"EXTF";510;20;"Kontenbeschriftungen";2;;;"RE";"Admin";"";55003;63021;20150101;4;;;"";"";;;;"";;"";;"";;;"";"";\r
+Konto;Kontobeschriftung;SprachId\r
+27;"EDV-Software";"de-DE"\r
+35;"Geschäfts- oder Firmenwert";"de-DE"\r
+40;"Verschmorungsmehrwertt";"de-DE"\r
+44;"EDV-Software";"de-DE"\r
+65;"Grundstück Schleifmustermühle 25";"de-DE"\r
+85;"Grundstückswert bebauter Grundstücke";"de-DE"\r
+100;"Fabrikbauten";"de-DE"\r
+210;"Maschinen";"de-DE"\r
+320;"PKW";"de-DE"\r
+350;"LKW";"de-DE"\r
+400;"Betriebsausstattung";"de-DE"\r
+420;"Büroeinrichtung";"de-DE"\r
+440;"Werkzeuge";"de-DE"\r
+480;"Geringwertige Wirtschaftsgüter";"de-DE"\r
+485;"Wirtschaftsgüter Sammelposten";"de-DE"\r
+525;"Wertpapiere des Anlagevermögens";"de-DE"\r
+690;"Darlehen 1 Deutsche Bank";"de-DE"\r
+691;"Darlehen 2 Deutsche Bank";"de-DE"\r
+692;"Darlehen Postbank";"de-DE"\r
+693;"Darlehen Deutsche Bank";"de-DE"\r
+694;"Darlehen Deutsche Bank";"de-DE"\r
+695;"Ratenkredit Pritschenwagen";"de-DE"\r
+696;"Darlehen Schleifmustermühle 25";"de-DE"\r
+860;"Gewinnvortrag vor Verwendung";"de-DE"\r
+980;"Aktive Rechnungsabgrenzung";"de-DE"\r
+986;"Damnum/Disagio";"de-DE"\r
+992;"Abgrenzung unterjährige AfA für BWA";"de-DE"\r
+1000;"Kasse";"de-DE"\r
+1010;"Kasse Werkstatt";"de-DE"\r
+1100;"Postbank Nürnberg";"de-DE"\r
+1111;"Fremdwährung1";"de-DE"\r
+1200;"Deutsche Bank";"de-DE"\r
+1201;"HypoVereinsbank Nürnberg";"de-DE"\r
+1202;"Deutsche Bank";"de-DE"\r
+1203;"USD-Bank";"de-DE"\r
+1210;"Dresdner Bank";"de-DE"\r
+1220;"SchmidtBank";"de-DE"\r
+1230;"Sparkasse";"de-DE"\r
+1250;"Deutsche Bank";"de-DE"\r
+1260;"Stadtspk. Nürnberg";"de-DE"\r
+1270;"Commerzbank Nbg.";"de-DE"\r
+1290;"Finanzmittelanlagen kurzfr. Disposition";"de-DE"\r
+1361;"Geldtransit";"de-DE"\r
+1369;"Unklare Posten";"de-DE"\r
+1400;"Forderungen aus Lieferungen u.Leistung";"de-DE"\r
+1447;"Forderg. aus stfr., n. steuerbaren L+L";"de-DE"\r
+1500;"Sonstige Vermögensgegenstände";"de-DE"\r
+1540;"Steuerüberzahlungen";"de-DE"\r
+1549;"Körperschaftsteuerrückforderung";"de-DE"\r
+1571;"Abziehbare Vorsteuer 7%";"de-DE"\r
+1574;"Abziehbare Vorsteuer aus EU-Erwerb 19%";"de-DE"\r
+1576;"Abziehbare Vorsteuer 19%";"de-DE"\r
+1577;"Abziehbare Vorsteuer § 13b UStG 19%";"de-DE"\r
+1593;"Verrechnung erhaltene Anzahlungen";"de-DE"\r
+1600;"Verbindl. aus Lieferungen u. Leistungen";"de-DE"\r
+1700;"Sonstige Verbindlichkeiten";"de-DE"\r
+1710;"Erhaltene Anzahlungen";"de-DE"\r
+1718;"Erhaltene Anzahlungen 19% USt";"de-DE"\r
+1736;"Verbindl. Steuern und Abgaben";"de-DE"\r
+1741;"Verbindlichk. Lohn- und Kirchensteuer";"de-DE"\r
+1774;"Umsatzsteuer aus EU-Erwerb 19%";"de-DE"\r
+1776;"Umsatzsteuer 19%";"de-DE"\r
+1780;"Umsatzsteuervorauszahlungen";"de-DE"\r
+1781;"Umsatzsteuervorauszahlungen 1/11";"de-DE"\r
+1787;"Umsatzsteuer nach § 13b UStG 19%";"de-DE"\r
+2110;"Zinsaufwendungen f.kfr.Verbindlichkeit.";"de-DE"\r
+2120;"Zinsaufwendungen f.lfr.Verbindlichkeit.";"de-DE"\r
+2126;"Zinsen zur Finanzierung Anlagevermögen";"de-DE"\r
+2200;"Körperschaftsteuer";"de-DE"\r
+2208;"Solidaritätszuschlag";"de-DE"\r
+2315;"Abgänge Sachanlagen Restbuchwert";"de-DE"\r
+2375;"Grundsteuer";"de-DE"\r
+2381;"Spenden kulturelle Zwecke";"de-DE"\r
+2382;"Spenden mildtätige Zwecke";"de-DE"\r
+2650;"Sonstige Zinsen und ähnliche Erträge";"de-DE"\r
+2670;"Diskonterträge";"de-DE"\r
+2701;"Sonstige Erträge Mahngebühren";"de-DE"\r
+2702;"Sonstige Erträge Zinsen";"de-DE"\r
+2860;"Gewinnvortrag nach Verwendung";"de-DE"\r
+3100;"Fremdleistungen";"de-DE"\r
+3123;"Sonst. Leistung EU 19% Vorst., 19% USt";"de-DE"\r
+3300;"Wareneingang 7% Vorsteuer";"de-DE"\r
+3400;"Wareneingang Furnier";"de-DE"\r
+3401;"Wareneingang Spanplatten";"de-DE"\r
+3402;"Wareneingang Farben und Lacke";"de-DE"\r
+3405;"Wareneingang beschichtete Platten";"de-DE"\r
+3406;"Wareneingang Modellbau";"de-DE"\r
+3407;"Wareneingang Messebau";"de-DE"\r
+3409;"Sonstiger Wareneingang";"de-DE"\r
+3425;"EU-Erwerb 19% Vorsteuer und 19% USt";"de-DE"\r
+3736;"Erhaltene Skonti 19% Vorsteuer";"de-DE"\r
+3748;"Erhalt. Skonti EU-Erwerb 19% Vorst/USt";"de-DE"\r
+3961;"Bestandsveränd. Furniere";"de-DE"\r
+3962;"Bestandsveränd. Spanplatten";"de-DE"\r
+3963;"Bestandsveränd. Farben und Lacke";"de-DE"\r
+3964;"Bestandsveränd. beschichtete Platten";"de-DE"\r
+3965;"Bestandsveränd. Sonstige RHB-Stoffe";"de-DE"\r
+3971;"Bestand Furniere";"de-DE"\r
+3972;"Bestand Spanplatten";"de-DE"\r
+3973;"Bestand Farben und Lacke";"de-DE"\r
+3974;"Bestand beschichtete Platten";"de-DE"\r
+3975;"Sonstiger Bestand";"de-DE"\r
+4110;"Löhne";"de-DE"\r
+4120;"Gehälter";"de-DE"\r
+4127;"Geschäftsführergehälter";"de-DE"\r
+4130;"Gesetzliche Sozialaufwendungen";"de-DE"\r
+4138;"Beiträge zur Berufsgenossenschaft";"de-DE"\r
+4145;"Freiwillige soziale Aufwendung. LSt-pfl.";"de-DE"\r
+4149;"Pauschale Steuer für Zuschüsse";"de-DE"\r
+4175;"Fahrtkostenerstatt. Whg./Arbeitsstätte";"de-DE"\r
+4190;"Aushilfslöhne";"de-DE"\r
+4199;"Pauschale Steuer für Aushilfen";"de-DE"\r
+4200;"Raumkosten";"de-DE"\r
+4210;"Miete, unbewegliche Wirtschaftsgüter";"de-DE"\r
+4240;"Gas, Strom, Wasser";"de-DE"\r
+4250;"Reinigung";"de-DE"\r
+4260;"Instandhaltung betrieblicher Räume";"de-DE"\r
+4270;"Abgaben betrieblich genutzt. Grundbesitz";"de-DE"\r
+4280;"Sonstige Raumkosten";"de-DE"\r
+4320;"Gewerbesteuer";"de-DE"\r
+4360;"Versicherungen";"de-DE"\r
+4366;"Versicherung für Gebäude";"de-DE"\r
+4510;"Kfz-Steuern";"de-DE"\r
+4520;"Kfz-Versicherungen";"de-DE"\r
+4530;"Laufende Kfz-Betriebskosten";"de-DE"\r
+4540;"Kfz-Reparaturen";"de-DE"\r
+4580;"Sonstige Kfz-Kosten";"de-DE"\r
+4600;"Werbekosten";"de-DE"\r
+4630;"Geschenke abzugsfähig";"de-DE"\r
+4640;"Repräsentationskosten";"de-DE"\r
+4651;"abzugsfähige Bewirtungskosten";"de-DE"\r
+4654;"Nicht abzugsfähige Bewirtungskosten";"de-DE"\r
+4663;"Reisekosten Arbeitnehmer, Fahrtkosten";"de-DE"\r
+4664;"Reisekosten AN Verpfleg.mehraufwand";"de-DE"\r
+4666;"Reisekosten AN Übernachtungsaufwand";"de-DE"\r
+4710;"Verpackungsmaterial";"de-DE"\r
+4730;"Ausgangsfrachten";"de-DE"\r
+4750;"Transportversicherungen";"de-DE"\r
+4800;"Reparatur/Instandh. Anlagen u. Maschinen";"de-DE"\r
+4810;"Mietleasing bewegliche Wirtschaftsgüter";"de-DE"\r
+4822;"Abschreibung immaterielle VermG";"de-DE"\r
+4830;"Abschreibungen auf Sachanlagen";"de-DE"\r
+4831;"Abschreibungen auf Gebäude";"de-DE"\r
+4832;"Abschreibungen auf Kfz";"de-DE"\r
+4862;"Abschreibungen auf WG Sammelposten";"de-DE"\r
+4901;"Getränke für Getränkeautomat";"de-DE"\r
+4910;"Porto";"de-DE"\r
+4920;"Telefon";"de-DE"\r
+4921;"Mobilfunk";"de-DE"\r
+4930;"Bürobedarf";"de-DE"\r
+4940;"Zeitschriften, Bücher";"de-DE"\r
+4945;"Fortbildungskosten";"de-DE"\r
+4950;"Rechts- und Beratungskosten";"de-DE"\r
+4955;"Buchführungskosten";"de-DE"\r
+4969;"Aufwand Abraum-/Abfallbeseitigung";"de-DE"\r
+4970;"Nebenkosten des Geldverkehrs";"de-DE"\r
+4980;"Betriebsbedarf";"de-DE"\r
+4993;"Kalkulatorische Abschreibungen";"de-DE"\r
+8000;"indiv., Erlöse";"de-DE"\r
+8120;"Steuerfr. Erlöse Furniere Drittland";"de-DE"\r
+8125;"Steuerfr. EG-Erlöse Furniere";"de-DE"\r
+8336;"Nicht steuerbare s. Leistung § 18b UStG";"de-DE"\r
+8400;"Erlöse Furniere";"de-DE"\r
+8401;"Erlöse Kleinmöbel";"de-DE"\r
+8405;"Erlöse beschichtete Platten";"de-DE"\r
+8406;"Erlöse Modellbau";"de-DE"\r
+8407;"Erlöse Messebau";"de-DE"\r
+8611;"Verrechn. sonstige Sachbezüge 19% USt";"de-DE"\r
+8730;"Gewährte Skonti";"de-DE"\r
+8736;"Gewährte Skonti 19% USt";"de-DE"\r
+8741;"Gewährte Skonti Leistungen §13b UStG";"de-DE"\r
+8743;"Gewährte Skonti stfr. EU-Lieferung";"de-DE"\r
+8820;"Erlöse Sachanlageverkäufe 19% USt";"de-DE"\r
+9000;"Saldenvorträge Sachkonten";"de-DE"\r
+9008;"Saldenvorträge Debitoren";"de-DE"\r
+9009;"Saldenvorträge Kreditoren";"de-DE"\r
diff --git a/doc/DATEV-2015/EXTF_Stammdaten-Deb-Kred.csv b/doc/DATEV-2015/EXTF_Stammdaten-Deb-Kred.csv
new file mode 100644 (file)
index 0000000..4678566
--- /dev/null
@@ -0,0 +1,6 @@
+"EXTF";510;16;"Debitoren/Kreditoren";4;20150729093352970;;"RE";"Admin";"";55003;63021;20150101;4;;;"";"";;;;"";;"";;"";;;"";"";\r
+Konto;Name (Adressattyp Unternehmen);Unternehmensgegenstand;Name (Adressattyp natürl. Person);Vorname (Adressattyp natürl. Person);Name (Adressattyp keine Angabe);Adressattyp;Kurzbezeichnung;EU-Land;EU-UStID;Anrede;Titel/Akad. Grad;Adelstitel;Namensvorsatz;Adressart;Straße;Postfach;Postleitzahl;Ort;Land;Versandzusatz;Adresszusatz;Abweichende Anrede;Abw. Zustellbezeichnung 1;Abw. Zustellbezeichnung 2;Kennz. Korrespondenzadresse;Adresse Gültig von;Adresse Gültig bis;Telefon;Bemerkung (Telefon);Telefon GL;Bemerkung (Telefon GL);E-Mail;Bemerkung (E-Mail);Internet;Bemerkung (Internet);Fax;Bemerkung (Fax);Sonstige;Bemerkung (Sonstige);Bankleitzahl 1;Bankbezeichnung 1;Bank-Kontonummer 1;Länderkennzeichen 1;IBAN-Nr. 1;IBAN1 korrekt;SWIFT-Code 1;Abw. Kontoinhaber 1;Kennz. Hauptbankverb. 1;Bankverb 1 Gültig von;Bankverb 1 Gültig bis;Bankleitzahl 2;Bankbezeichnung 2;Bank-Kontonummer 2;Länderkennzeichen 2;IBAN-Nr. 2;IBAN2 korrekt;SWIFT-Code 2;Abw. Kontoinhaber 2;Kennz. Hauptbankverb. 2;Bankverb 2 Gültig von;Bankverb 2 Gültig bis;Bankleitzahl 3;Bankbezeichnung 3;Bank-Kontonummer 3;Länderkennzeichen 3;IBAN-Nr. 3;IBAN3 korrekt;SWIFT-Code 3;Abw. Kontoinhaber 3;Kennz. Hauptbankverb. 3;Bankverb 3 Gültig von;Bankverb 3 Gültig bis;Bankleitzahl 4;Bankbezeichnung 4;Bank-Kontonummer 4;Länderkennzeichen 4;IBAN-Nr. 4;IBAN4 korrekt;SWIFT-Code 4;Abw. Kontoinhaber 4;Kennz. Hauptbankverb. 4;Bankverb 4 Gültig von;Bankverb 4 Gültig bis;Bankleitzahl 5;Bankbezeichnung 5;Bank-Kontonummer 5;Länderkennzeichen 5;IBAN-Nr. 5;IBAN5 korrekt;SWIFT-Code 5;Abw. Kontoinhaber 5;Kennz. Hauptbankverb. 5;Bankverb 5 Gültig von;Bankverb 5 Gültig bis;Leerfeld;Briefanrede;Grußformel;Kundennummer;Steuernummer;Sprache;Ansprechpartner;Vertreter;Sachbearbeiter;Diverse-Konto;Ausgabeziel;Währungssteuerung;Kreditlimit (Debitor);Zahlungsbedingung;Fälligkeit in Tagen (Debitor);Skonto in Prozent (Debitor);Kreditoren-Ziel 1 Tg.;Kreditoren-Skonto 1 %;Kreditoren-Ziel 2 Tg.;Kreditoren-Skonto 2 %;Kreditoren-Ziel 3 Brutto Tg.;Kreditoren-Ziel 4 Tg.;Kreditoren-Skonto 4 %;Kreditoren-Ziel 5 Tg.;Kreditoren-Skonto 5 %;Mahnung;Kontoauszug;Mahntext 1;Mahntext 2;Mahntext 3;Kontoauszugstext;Mahnlimit Betrag;Mahnlimit %;Zinsberechnung;Mahnzinssatz 1;Mahnzinssatz 2;Mahnzinssatz 3;Lastschrift;Verfahren;Mandantenbank;Zahlungsträger;Indiv. Feld 1;Indiv. Feld 2;Indiv. Feld 3;Indiv. Feld 4;Indiv. Feld 5;Indiv. Feld 6;Indiv. Feld 7;Indiv. Feld 8;Indiv. Feld 9;Indiv. Feld 10;Indiv. Feld 11;Indiv. Feld 12;Indiv. Feld 13;Indiv. Feld 14;Indiv. Feld 15;Abweichende Anrede (Rechnungsadresse);Adressart (Rechnungsadresse);Straße (Rechnungsadresse);Postfach (Rechnungsadresse);Postleitzahl (Rechnungsadresse);Ort (Rechnungsadresse);Land (Rechnungsadresse);Versandzusatz (Rechnungsadresse);Adresszusatz (Rechnungsadresse);Abw. Zustellbezeichnung 1 (Rechnungsadresse);Abw. Zustellbezeichnung 2 (Rechnungsadresse);Adresse Gültig von (Rechnungsadresse);Adresse Gültig bis (Rechnungsadresse);Bankleitzahl 6;Bankbezeichnung 6;Bank-Kontonummer 6;Länderkennzeichen 6;IBAN-Nr. 6;IBAN6 korrekt;SWIFT-Code 6;Abw. Kontoinhaber 6;Kennz. Hauptbankverb. 6;Bankverb 6 Gültig von;Bankverb 6 Gültig bis;Bankleitzahl 7;Bankbezeichnung 7;Bank-Kontonummer 7;Länderkennzeichen 7;IBAN-Nr. 7;IBAN7 korrekt;SWIFT-Code 7;Abw. Kontoinhaber 7;Kennz. Hauptbankverb. 7;Bankverb 7 Gültig von;Bankverb 7 Gültig bis;Bankleitzahl 8;Bankbezeichnung 8;Bank-Kontonummer 8;Länderkennzeichen 8;IBAN-Nr. 8;IBAN8 korrekt;SWIFT-Code 8;Abw. Kontoinhaber 8;Kennz. Hauptbankverb. 8;Bankverb 8 Gültig von;Bankverb 8 Gültig bis;Bankleitzahl 9;Bankbezeichnung 9;Bank-Kontonummer 9;Länderkennzeichen 9;IBAN-Nr. 9;IBAN9 korrekt;SWIFT-Code 9;Abw. Kontoinhaber 9;Kennz. Hauptbankverb. 9;Bankverb 9 Gültig von;Bankverb 9 Gültig bis;Bankleitzahl 10;Bankbezeichnung 10;Bank-Kontonummer 10;Länderkennzeichen 10;IBAN-Nr. 10;IBAN10 korrekt;SWIFT-Code 10;Abw. Kontoinhaber 10;Kennz. Hauptbankverb. 10;Bankverb 10 Gültig von;Bankverb 10 Gültig bis;Nummer Fremdsystem;Insolvent;Mandatsreferenz 1;Mandatsreferenz 2;Mandatsreferenz 3;Mandatsreferenz 4;Mandatsreferenz 5;Mandatsreferenz 6;Mandatsreferenz 7;Mandatsreferenz 8;Mandatsreferenz 9;Mandatsreferenz 10;Verknüpftes OPOS-Konto;Mahnsperre bis;Lastschriftsperre bis;Zahlungssperre bis;Gebührenberechnung;Mahngebühr 1;Mahngebühr 2;Mahngebühr 3;Pauschalenberechnung;Verzugspauschale 1;Verzugspauschale 2;Verzugspauschale 3\r
+10000;"Möbel Testgruber";"Schreinerei";"";"";"";"2";"Möbel Testgrube";"DE";"133546770";"Firma";"";"";"";"STR";"Nelkenteststraße 125";"";"90482";"Nürnberg";"";"";"";"Firma";"";"";1;01012012;;"";"";"";"";"";"";"";"";"";"";"";"";"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"1";01012012;01012013;"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"0";01012012;01012013;"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"0";01012012;01012013;"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"0";01012012;01012013;"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"0";01012012;01012013;"";"Sehr geehrte Frau";"Hallo";"KDN 12345";"DE776655";"2";"Frau Huber";"Herr Schmid";"Frau Tester";;;"";0;0;0;0,00;0;0,00;0;0,00;0;0;0,00;0;0,00;7;;;;;;23,30;20,25;1;10,50;11,11;12,12;"7";"1";1;"9";"ind. Feld";"";"";"";"";"";"";"";"";"";"";"individuelle Beschriftung";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"50050000";"Testbank";"454545";"GB";"GB4545454545";"";"GENODEF1S12";"Herr Muster";"0";01012012;01012013;"76060618";"VR-Bank";"121212";"DE";"DE706060660125";"";"GE0987";"Herr Testmeier";"0";01012012;01012014;"";0;"1234-AB-56787";"";"";"";"";"";"";"";"";"778259637";75000;03122015;02122015;01122015;1;5,1;5,2;5,3;;0,9;0,2;0,5\r
+20000;"Einrichtungshaus Muster";"Möbelhaus";"";"";"";"2";"Muster";"";"";"Firma";"";"";"";"STR";"Feldgasse 15";"";"90409";"Nürnberg";"";"";"";"Firma";"";"";1;01012012;;"";"";"";"";"";"";"";"";"";"";"";"";"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"1";01012012;01012013;"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"0";01012012;01012013;"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"0";01012012;01012013;"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"0";01012012;01012013;"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"0";01012012;01012013;"";"Sehr geehrte Frau";"Hallo";"KDN 12345";"DE776655";"2";"Frau Huber";"Herr Schmid";"Frau Tester";;;"";0;0;0;0,00;0;0,00;0;0,00;0;0;0,00;0;0,00;7;;;;;;23,30;;;;;;"";"";0;"";"";"";"";"";"";"";"";"";"";"";"";"individuelle Beschriftung";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"50050000";"Testbank";"454545";"GB";"GB4545454545";"";"GENODEF1S12";"Herr Muster";"0";01012012;01012013;"76060618";"VR-Bank";"121212";"DE";"DE706060660125";"";"GE0987";"Herr Testmeier";"0";01012012;01012014;"";0;"";"";"";"";"";"";"";"";"";"";75000;03122015;02122015;01122015;1;5,1;5,2;5,3;;0,9;0,2;0,5\r
+30000;"";"";"Mustermeier";"Hans";"";"1";"";"";"";"Herr";"";"";"";"STR";"Musterweg 14b";"";"90489";"Nürnberg";"";"";"";"Firma";"";"";1;01012012;;"";"";"";"";"";"";"";"";"";"";"";"";"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"1";01012012;01012013;"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"0";01012012;01012013;"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"0";01012012;01012013;"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"0";01012012;01012013;"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"0";01012012;01012013;"";"Sehr geehrte Frau";"Hallo";"KDN 12345";"DE776655";"2";"Frau Huber";"Herr Schmid";"Frau Tester";;;"";0;0;0;0,00;0;0,00;0;0,00;0;0;0,00;0;0,00;7;;;;;;23,30;;;;;;"";"";0;"";"";"";"";"";"";"";"";"";"";"";"";"individuelle Beschriftung";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"50050000";"Testbank";"454545";"GB";"GB4545454545";"";"GENODEF1S12";"Herr Muster";"0";01012012;01012013;"76060618";"VR-Bank";"121212";"DE";"DE706060660125";"";"GE0987";"Herr Testmeier";"0";01012012;01012014;"";0;"";"";"";"";"";"";"";"";"";"";75000;03122015;02122015;01122015;1;5,1;5,2;5,3;;0,9;0,2;0,5\r
+40000;"";"";"Testhuber";"Susanne";"";"1";"";"";"";"Frau";"";"";"";"STR";"Beispielstr. 56";"";"90512";"Nürnberg";"";"";"";"Firma";"";"";1;01012012;;"";"";"";"";"";"";"";"";"";"";"";"";"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"1";01012012;01012013;"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"0";01012012;01012013;"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"0";01012012;01012013;"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"0";01012012;01012013;"50090500";"Sparda-Bank Hessen";"2345678";"DE";"DE49100102220002222222";"";"GENODEF1S12";"Herr Testmüller";"0";01012012;01012013;"";"Sehr geehrte Frau";"Hallo";"KDN 12345";"DE776655";"2";"Frau Huber";"Herr Schmid";"Frau Tester";;;"";0;0;0;0,00;0;0,00;0;0,00;0;0;0,00;0;0,00;7;;;;;;23,30;;;;;;"";"";0;"";"";"";"";"";"";"";"";"";"";"";"";"individuelle Beschriftung";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"";"";"";"";"";"";"";"";"";;;"50050000";"Testbank";"454545";"GB";"GB4545454545";"";"GENODEF1S12";"Herr Muster";"0";01012012;01012013;"76060618";"VR-Bank";"121212";"DE";"DE706060660125";"";"GE0987";"Herr Testmeier";"0";01012012;01012014;"";0;"";"";"";"";"";"";"";"";"";"";75000;03122015;02122015;01122015;1;5,1;5,2;5,3;;0,9;0,2;0,5\r
diff --git a/doc/DATEV-2015/EXTF_Textschluessel.csv b/doc/DATEV-2015/EXTF_Textschluessel.csv
new file mode 100644 (file)
index 0000000..d3955d3
--- /dev/null
@@ -0,0 +1,10 @@
+"EXTF";510;44;"Textschlüssel";2;20150729103107277;;"RE";"Admin";"";29098;55003;20150101;7;;;"";"";;;;"";;"";;;"";;;"";""\r
+TS-Nr.;Beschriftung;Ref.-TS;Konto Soll;Konto Haben;Sprach-ID\r
+30;"Weizen";30;5005000;4000000;"de-DE"\r
+31;"Winterweizen";30;5005000;4000000;"de-DE"\r
+32;"Dinkel";30;5005000;4000000;"de-DE"\r
+33;"Sommerweizen";33;5005000;4000000;"de-DE"\r
+34;"corn";30;5005000;4000000;"en-GB"\r
+35;"oat";35;5005000;4000000;"en-GB"\r
+38;"Energiegetreide";38;5005000;4100000;"de-DE"\r
+39;"durum wheat";39;5005000;4000000;"en-GB"
\ No newline at end of file
diff --git a/doc/DATEV-2015/EXTF_Wiederkehrende-Buchungen.csv b/doc/DATEV-2015/EXTF_Wiederkehrende-Buchungen.csv
new file mode 100644 (file)
index 0000000..2138caf
--- /dev/null
@@ -0,0 +1,4 @@
+"EXTF";510;65;"Wiederkehrende Buchungen";2;20150729093200673;;"RE";"Admin";"";55003;63021;20150101;4;;;"";"";;;;"";;"";;"";;;"";"";\r
+B1;WKZ (Umsatz);Umsatz (ohne Soll/Haben-Kz);Soll/Haben-Kennzeichen;Kurs;Basis-Umsatz;WKZ Basis-Umsatz;BU-Schlüssel;Gegenkonto (ohne BU-Schlüssel);Belegfeld1;Belegfeld2;Beginndatum;Kontonummer;Stück;Gewicht;KOST1-Kostenstelle;KOST2-Kostenstelle;KOST-Menge;Skonto;Buchungstext;Postensperre;Diverse Adressnummer;Geschäftspartnerbank;Sachverhalt;Zinssperre;Beleglink;EU-Land u. UStId;EU-Steuersatz;Abw. Versteuerungsart;Sachverhalt L+L;BU 49 Hauptfunktionstyp;BU 49 Hauptfunktionsnummer;BU 49 Funktionsergänzung;Zusatzinformation - Art 1;Zusatzinformation- Inhalt 1;Zusatzinformation - Art 2;Zusatzinformation- Inhalt 2;Zusatzinformation - Art 3;Zusatzinformation- Inhalt 3;Zusatzinformation - Art 4;Zusatzinformation- Inhalt 4;Zusatzinformation - Art 5;Zusatzinformation- Inhalt 5;Zusatzinformation - Art 6;Zusatzinformation- Inhalt 6;Zusatzinformation - Art 7;Zusatzinformation- Inhalt 7;Zusatzinformation - Art 8;Zusatzinformation- Inhalt 8;Zusatzinformation - Art 9;Zusatzinformation- Inhalt 9;Zusatzinformation - Art 10;Zusatzinformation- Inhalt 10;Zusatzinformation - Art 11;Zusatzinformation- Inhalt 11;Zusatzinformation - Art 12;Zusatzinformation- Inhalt 12;Zusatzinformation - Art 13;Zusatzinformation- Inhalt 13;Zusatzinformation - Art 14;Zusatzinformation- Inhalt 14;Zusatzinformation - Art 15;Zusatzinformation- Inhalt 15;Zusatzinformation - Art 16;Zusatzinformation- Inhalt 16;Zusatzinformation - Art 17;Zusatzinformation- Inhalt 17;Zusatzinformation - Art 18;Zusatzinformation- Inhalt 18;Zusatzinformation - Art 19;Zusatzinformation- Inhalt 19;Zusatzinformation - Art 20;Zusatzinformation- Inhalt 20;Zahlweise;Forderungsart;Veranlagungsjahr;Zugeordnete Fälligkeit;Zuletzt per;Nächste Fälligkeit;Enddatum;Zeitintervallart;Zeitabstand;Wochentag;Monat;Ordnungszahl Tag im Monat;Ordnungszahl Wochentag;EndeTyp;Gesellschaftername;Beteiligtennummer;Identifikationsnummer;Zeichnernummer;SEPA-Mandatsreferenz;Postensperre bis;KOST-Datum;Bezeichnung SoBil-Sachverhalt;Kennzeichen SoBil-Buchung\r
+2;"";488,00;"H";;;"";"";4360;"";"";01032013;980;;;"10";"";;;"Gebäudeversicherung";;"";;;;"";"";;"";;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;;01032013;01042013;01022014;"MON";1;;;1;;2;"";;"";"";"1259887";;;"";\r
+2;"";333,52;"H";;;"";"";4360;"";"";31082012;980;;;"10";"";;;"Brandversicherung";;"";;;;"";"";;"";;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;"";;;01032013;01042013;01082013;"MON";1;;;1;;2;"Mustermann";4711;"130263d";"";"77789-AB-789";25032016;23122015;"Sonderbilanz";2\r
diff --git a/doc/DATEV-2015/EXTF_Zahlungsbedingungen.csv b/doc/DATEV-2015/EXTF_Zahlungsbedingungen.csv
new file mode 100644 (file)
index 0000000..c84c647
--- /dev/null
@@ -0,0 +1,10 @@
+"EXTF";510;46;"Zahlungsbedingungen";2;20150729093357008;;"RE";"Admin";"";55003;63021;20150101;4;;;"";"";;;;"";;"";;"";;;"";"";\r
+Nummer;Bezeichnung;Fälligkeitstyp;Skonto 1%;Skonto 1 Tage;Skonto 2 %;Skonto 2 Tage;Fällig Tage;Rechnung bis / Zeitraum 1;Skonto1 Datum / Zeitraum 1;Skonto 1 Monat / Zeitraum 1;Skonto 2 Datum / Zeitraum 1;Skonto 2 Monat / Zeitraum 1;Fällig Datum / Zeitraum 1;Fällig Monat / Zeitraum 1;Rechnung bis / Zeitraum 2;Skonto1 Datum / Zeitraum 2;Skonto 1 Monat / Zeitraum 2;Skonto 2 Datum / Zeitraum 2;Skonto 2 Monat / Zeitraum 2;Fällig Datum / Zeitraum 2;Fällig Monat / Zeitraum 2;Rechnung bis / Zeitraum 3;Skonto1 Datum / Zeitraum 3;Skonto1 Monat / Zeitraum 3;Skonto 2 Datum / Zeitraum 3;Skonto 2 Monat / Zeitraum 3;Fällig Datum / Zeitraum 3;Fällig Monat / Zeitraum 3;Leerfeld;Verwendung\r
+10;"14 Tage 2% oder 30 Tage Netto";1;200;14;;;30;;;0;;0;;0;;;0;;0;;0;;;0;;0;;0;"";\r
+11;"14 Tage 3% oder 30 Tage Netto";1;300;14;200;10;30;;;0;;0;;0;;;0;;0;;0;;;0;;0;;0;"";\r
+12;"30 Tage Netto";1;300;10;;;30;;;0;;0;;0;;;0;;0;;0;;;0;;0;;0;"";\r
+13;"14 Tage 3% oder 60 Tage Netto";1;300;14;;;60;;;0;;0;;0;;;0;;0;;0;;;0;;0;;0;"";\r
+14;"10 Tage 2% oder 30 Tage Netto";1;200;10;;;30;10;20;0;;0;31;0;20;31;0;;0;10;0;31;10;0;;0;20;0;"";\r
+15;"10 Tage 3% oder 30 Tage Netto";1;300;10;;;30;;;0;;0;;0;;;0;;0;;0;;;0;;0;;0;"";\r
+16;"sofort ohne Abzug";1;;;;;;;;0;;0;;0;;;0;;0;;0;;;0;;0;;0;"";\r
+17;"Vorkasse";1;;;;;;;;0;;0;;0;;;0;;0;;0;;;0;;0;;0;"";\r
index 29b436f..6b809ee 100644 (file)
 Wichtige Hinweise zum Upgrade von älteren Versionen
 ===================================================
 
-
 ** BITTE FERTIGEN SIE VOR DEM UPGRADE EIN BACKUP IHRER DATENBANK(EN) AN! **
 
-Upgrade auf v?????
-==================
+Upgrade auf v3.6.1
+
+Das manueller Korrigieren der Steuer bei Skontoverbuchungen entfällt seit
+dieser Version, da die Steuerkorrektur automatisch gemacht wird.
+
+Ein neues Perl-Modul ist hinzugekommen, Hilfe zum Installieren bietet wie immer
+das Skript 'scripts/installation_check.pl -v'.
+
+  * IPC::Run
+
+
+Upgrade auf v3.6.0
+
+Der 'neue Auftrags-Controller' ist mittlerweile weder neu noch experimentell und
+die entsprechende Mandantenkonfiguration 'Experimentellen neuen Auftrags-Controller'
+verwenden wird bei diesem Upgrade hart auf 'Ja' gesetzt.
+Die alte, noch aktive Auftragsmaske wird in einer zukünftigen Version von kivitendo entfernt
+werden.
+
+Einige neue Perl-Module sind hinzugekommen, Hilfe zum Installieren bietet wie immer
+das Skript 'scripts/installation_check.pl -v'.
+
+  * Neue Perl Abhängigkeiten:
+
+  * Term::ReadLine::Gnu
+  * Imager::QRCode
+  * Imager
+  * REST::Client
+
+
+
+Upgrade auf v3.5.8
+
+Die API für 'Erzeugnis fertigen' wurde geändert:
+Die Einstellung der Mandantenkonfiguration für 'Zum Fertigen wird das Standardlager
+des Bestandteils verwendet, nicht das Ziellager' hat keine Auswirkung mehr.
+Falls dieser Wert auf 'Nein' steht funktioniert das Verfahren wie vorher auch.
+Falls dieser Wert auf 'Ja' steht, muss geprüft werden, ob das Verfahren noch so
+benötigt wird.
+Alternativ kann 'Erzeugnis fertigen' ab dieser Version auch Erzeugnisbestandteile aus
+fremden Lagern (nicht das Ziellager) nehmen. Dies sollte i.d.R. den Prozeß abbilden können.
+
+Die Mandantenkonfiguration 'Standard-Auslagern ohne Prüfung auf Bestand' wird bei diesem
+Versionsupgrade hart auf 'Nein' gesetzt und kann vom kivitendo Administrator selbständig
+wieder auf 'Ja' gesetzt werden. Das Verfahren wird aber prinzipiell in einer zukünftigen
+Version von kivitendo nicht mehr unterstützt werden.
+
+
+Upgrade auf v3.5.7
+  * Neue Perl Abhängigkeiten:
+
+  * Math::Round
+  * Try::Tiny
+
+
+Upgrade auf v3.5.6.1
+
+Die Abwärtskompatibilität zur Lagermengen-Berechnung in Lieferscheinen wurde
+aufgehoben. Wer nicht mit Workflows arbeitet (nicht empfohlen) muss diese
+explizit in der Mandantenkonfiguration wieder aktivieren.
+
+
+Upgrade auf v3.5.6
+
+In dieser Version sind die Mehrwertsteueranpassungen für den SKR03 und SKR04
+ab 1.7.2020 vorhanden. Wer diese Anpassungen schon manuell eingestellt hat, sollte
+die Upgrade-Skripte deaktivieren.
+Dies betrifft diese drei Skripte "sql/Pg-upgrade2/konjunkturpaket_2020*", sowie
+ferner das Entfernen der Release-Abhängigkeiten dieser Skripte:
+
+Folgende zwei Sed-Kommandos erledigen das:
+
+ sed -i 's/ignore: 0/ignore: 1/g' sql/Pg-upgrade2/konjunkturpaket_2020*
+ sed -i 's/\bkonjunktur[^ ]*//g' sql/Pg-upgrade2/release_3_5_*
+
+
+Alternativ sollten die Datenbank-Upgrade-Skripte gegen einen Testdatenbestand ausgeführt werden
+und der kivitendo-Dienstleister Ihres Vertrauens griffbereit sein.
+
+Weitere Änderungen:
+
+Für den MT940-Import erwartet kivitendo aqbanking ab Version 6.
+
+Für die Erzeugung von ZUGFeRD 2.0 fähigen PDFs wird ein aktuelles TexLive ab Version 2018 benötigt.
+Details hierzu auch in der Dokumentation (HTML oder Dokumentation.pdf).
+
+Bitte wie immer vor dem Anmelden an der Weboberfläche 'scripts/installation_check.pl -v' ausführen.
+
+Es sollten mindestens zwei Perl-Module "CAM::PDF" und "XML::LibXML" dort erscheinen, falls noch nicht installiert.
+
+Diese Version ist ferner mit Postgres Datenbanken ab Version 12 kompatibel, da die Abhängigkeit von oids entfernt wurde.
+
+Sicherheitshinweis:
+
+Für git-Installation sollte geprüft werden ob das Verzeichnis .git/ für den Webserver auslesbar ist.
+Gleiches gilt für alle Installation für den Ordner config/, der die Datei kivitendo.conf beinhaltet.
+Die Standard-Konfiguration des Apache2 Webservers sollte letzteres verhindern, aber wir weisen darauf hin
+dies einmal zu überprüfen.
+
+Ab dieser Version wird eine globale .htaccess ausgeliefert die beide Verzeichnisse mittels rewrite sichert.
+Dafür muss einmalig das Modul rewrite für den Apache, bspw. mit "a2enmode rewrite" aktiviert werden.
+Regeldetails:
+
+<IfModule mod_rewrite.c>
+  RewriteEngine On
+  RewriteRule .*(\.git|config).*$ - [F,NC]
+</IfModule>
+
+Ferner wurde ein Security-Audit der kivitendo Version 3.1 veröffentlicht.
+Hierfür empfehlen wir die Ausarbeitung eines Sicherheitskonzept mit einem kivitendo Partner Eurer Wahl.
+Falls dies nicht möglich sein sollte, weisen wir darauf hin, dass ein SQL-Backup tages- und wochenaktuell
+für einen etwaigen Restore zu Verfügung stehen sollte. Ferner besteht die Gefahr, dass angemeldete
+Benutzer Formfelder mißbrauchen können, Abhilfe schafft hier zum Beispiel der Einsatz von modsecurity unter
+Apache2 (https://doxsec.wordpress.com/2017/06/11/using-modsecurity-web-application-firewall-to-prevent-sql-injection-and-xss-using-blocking-rules/)
+
+Upgrade auf v3.5.4
+
+* Task-Server berücksichtigt Memory-Limit
+
+Falls für fgci-Prozesse ein Memory-Limit in der Konfigurationsdatei eingerichtet
+ist, wird dies nun auch vom Task-Server berücksichtigt. Dieser beendet sich bei
+Überschreitung des Limits. Deshalb muss dafür gesorgt werden, dass der
+Task-Server in diesem Fall neu gestartet wird (z.B. über den systemd-Service).
+Siehe auch aktuelle kivitendo-Dokumentation.
+
+
+Upgrade auf v3.5.3
+
+* Fallback-Module entfernt
+
+Einige Default-Module die als Fallback zu Verfügung standen, werden ab
+dieser Version nicht mehr mit ausgeliefert.
+Bitte vor dem Anmelden an der Weboberfläche 'scripts/installation_check.pl -v' ausführen
+und die entsprechenden Module installieren.
+S.a. weitere Details in der aktuellen kivitendo-Dokumentation.
 
-* Der in der Dokumentation beschriebene Mechanismus für die CGI-Anbindung
+
+Upgrade auf v3.5.1
+
+* Neue Perlabhängigkeiten
+
+* LWP::Authen::Digest für WebshopApi
+* LWP::UserAgent für WebshopApi
+
+* Zwingende Postgres Erweiterung pg_trgm(Trigram)
+
+  Die Trigramerweiterung bietet eine Ähnlichkeitsuche.
+  Diese verwendet das Shopmodul, wenn installiert, beim Bestellimport
+  um zu entscheiden ob ein Kunde neu angelegt oder als Vorschlag angezeigt wird.
+  Die Erweiterung wird bisher nur beim Ableich der Straße genutzt, da hier oft
+  unterschiedliche Schreibweisen vorhanden sind.
+  z.B Dorfstraße, Dorfstrasse, Dorfstr., Dorf Straße usw..
+  So wird vermieden, dass Kunden eventuell doppelt angelegt werden.
+
+  * Zunächst muss geprüft werden, ob die Erweiterung prinzipiell für postgres
+  vorhanden ist, dafür kann folgendes Select-Statement in template1 genutzt werden:
+
+  # select * from pg_available_extensions where name ='pg_trgm';
+
+  Sollte bei diesem Statement kein Ergebnis kommen, so muss die entsprechende
+  Erweiterung für die eigene Distribution nachinstalliert werden.
+  Bei debian/ubuntu befindet sich diese im Paket postgresql-contrib
+  und kann mit
+
+  $ apt install postgresql-contrib
+
+  installiert werden.
+
+  * Diese Erweiterung wird mit dem SQL-Updatescript sql/Pg-upgrade2/trigram_extension.sql
+  und Datenbank-Super-Benutzer Rechten automatisch installiert.
+  Dazu braucht der DatenbankSuperbenutzer "postgres" ein Passwort
+
+  su - postgres
+  psql
+  \password
+  <Eingabe passwort>
+  \q
+
+  Passwort und Benutzername können jetzt beim Anlegen einer neuen Datenbank bzw.
+  bei Updatescripten, die SuperUserRechte benötigen eingegeben werden.
+
+
+  * Änderungen DATEV-Export Format CSV
+
+  Die Felder Belegfeld2 und Buchungsbeschreibung werden nicht mehr befüllt.
+  Im KNE-Export war im Belegfeld2 die Fälligkeit der Buchung gesetzt und in
+  Buchungsbeschreibung der Kunden- oder Lieferantenname.
+  Bei nicht valider Umsatz-Steuer-Identnummer wird der Export abgelehnt.
+  Da das Feld ein Freitext-Feld und keine Validierung bei der Eingabe hat(te)
+  unternimmt kivitendo keine eigene Normalisierung,  bzw. Konvertierung
+  des Datenfelds.
+
+  Eine Bereinigung der Ust-IDs muss der kivitendo-Admin eigenverantwortlich unternehmen.
+  Hier exemplarisch ein SQL-Schnipsel zum Ersetzen der Leerzeichen in diesem Feld:
+  UPDATE customer SET ustid=REPLACE(ustid, ' ', '') WHERE ustid LIKE '% %';
+
+  Upgrade auf v3.5.0
+  ===========================
+
+  * Neue Perl Abhängigkeiten:
+
+  * File::MimeInfo - für den Dateiupload
+  * Sys::CPU
+  * Thread::Pool::Simple
+
+  * Neue externe Abhängigkeiten:
+
+  * pdfinfo
+
+  * In der Rechte-Tabelle auth.master_rights wurden alle Positionswerte mit 100
+  multipliziert, um Lücken für neue Rechte zu schaffen.
+
+  * In der Tabelle "customer" wurde die Spalte "klass" nach "pricegroup_id"
+  migriert. Bei Kunden ohne Preisgruppe ist der Datenbankwert jetzt NULL statt
+  "0". Falls Kunden per CSV-Import importiert werden muß dieses Feld in der
+  CSV-Datei ebenfalls umbenannt werden.
+
+  * Für das neue Feature Lieferantenbriefe ist die Standardvorlage für Briefe
+  (letter.tex) angepasst worden. Statt letter.customer muss der Adressat jetzt
+  aus letter.custoemr_vendor erzeugt werden.
+
+  * In der Tabelle parts wurde die Boolean-Spalte "assembly" entfernt. Zur
+  Erkennung von Waren/Dienstleistungen/Erzeugnissen gibt es nun in parts eine
+  neue Spalte part_type vom ENUM-Typ, der auf die Werte 'part', 'service',
+  'assembly' und 'assortment' beschränkt ist.
+
+  * In der Tabelle parts wurde die Spalten inventory_accno_id, expense_accno_id
+  und income_accno_id entfernt. Deren Funktionalität wurde schon lange durch
+  Buchungsgruppen ersetzt und für die Erkennung des Artikeltyps gibt es nun die
+  Spalte part_type
+
+  Upgrade auf v3.4.1
+  ==================
+
+  * Neue Druckvariante Gelangensbestätigung für Verkaufs-Aufträge
+
+  Im Standard-Vorlagensatz RB befindet sich als Vorlage die ic_supply.tex
+  als Orientierung für die Anpassung an eigene Vorlagen. Eigene Vorlagen
+  müssen entsprechend um diesen Typ für die 3.4.1 erweitert werden.
+
+  * Druckvorlagen für Briefe
+
+  Die Erzeugung der Druckausgabe für die Brieffunktion wurde auf die
+  Verwendung des Template Toolkits umgestellt. Dazu muss die verwendete
+  Druckvorlage "letter.tex" angepasst werden. Im Standard-Vorlagensatz RB ist
+  das bereits geschehen. Falls keine manuellen Änderungen an der "letter.tex"
+  aus einer vorherigen Version gemacht wurden, reicht es, diese Datei
+  ("templates/print/RB/letter.tex") in das verwendete Vorlagenverzeichnis zu
+  kopieren. Ansonsten kann diese Datei als Beispiel dienen.
+
+
+  Upgrade auf v3.4.0
+  ==================
+
+  * Neue Perl-Modul-Abhängigkeiten:
+
+  * Algorithm::CheckDigits
+  * PBKDF2::Tiny
+
+  Wie immer bitte vor dem ersten Aufrufen einmal die Pakete überprüfen:
+
+  $ scripts/installation_check.pl -ro
+
+  * Der in der Dokumentation beschriebene Mechanismus für die CGI-Anbindung
   (2.6.1 Grundkonfiguration mittels CGI) wurde geändert. Ein einfacher Alias
   auf das Programmverzeichnis funktioniert nicht mehr, und es muss immer ein
   AliasMatch auf einen dispatcher eingerichtet werden. Die Dokumentation wurde
   aktualisiert. Für Benutzer der empfohlenen FastCGI Anbindung ändert sich
   nichts.
 
-Upgrade auf v3.3.0
-==================
+  * Der Task-Server ist nun mandantenfähig. Für jeden Mandanten, für den
+  der Task-Server laufen soll, muss in der Administrationsoberfläche
+  in der Konfiguration des Mandanten hinterlegt werden, welchen
+  kivitendo-Benutzer der Task-Server nutzen soll. Ist bei einem
+  Mandanten kein Benutzer hinterlegt, so ignoriert der Task-Server
+  diesen Mandanten.
+
+  Im Gegenzug wurden die beiden Konfigurations-Einstellungen »client«
+  und »login« aus dem Abschnitt [task_server] entfernt. Der
+  Task-Server prüft beim Starten allerdings, ob diese Einstellungen
+  noch existieren und verweigert den Start mit einer hilfreichen
+  Fehlermeldung, solange sie noch vorhanden sind.
+
+  * Die Unterstützung unsicherer Passwort-Hashing-Mechanism wurde
+  entfernt. Für BenutzerInnen, die noch alte Mechanismen verwenden,
+  müssen die Passwörter einmalig in der Administrationsoberfläche
+  zurückgesetzt werden.
+
+  Dies betrifft nur Accounts, deren Passwort sich das letzte Mal vor
+  kivitendo 2.7.0 geändert hat.
+
+  Upgrade auf v3.3.0
+  ==================
 
-* Bei Upgrade von Versionen vor v.3.2.x wie immer erst die dortigen
+  * Bei Upgrade von Versionen vor v.3.2.x wie immer erst die dortigen
   Upgradehinweise beachten.
 
-* Es gibt keine neuen Perl-Modul-Abhängigkeiten.
+  * Es gibt keine neuen Perl-Modul-Abhängigkeiten.
 
-* Die alte ungepflegte Druckvorlagenvariante "Standard" wurde entfernt.
+  * Die alte ungepflegte Druckvorlagenvariante "Standard" wurde entfernt.
   Bereits verwendete Druckvorlagen, die darauf aufbauen, funktionieren
   natürlich weiterhin.
 
-* Für die Verwendung des MT940 Import Features der Bankerweiterung muß
+  * Für die Verwendung des MT940 Import Features der Bankerweiterung muß
   aqbanking installiert werden. Dies wird nur für die Konvertierung vom MT940
   ins CSV Format benötigt, das Kommandozeilentool "aqbanking-cli" befindet sich
   z.B. unter Ubuntu im Paket aqbanking-tools.
 
-Upgrade auf v3.2.0
-==================
+  Upgrade auf v3.2.0
+  ==================
 
-* Neue Perl-Modul-Abhängigkeiten:
+  * Neue Perl-Modul-Abhängigkeiten:
 
   * GD
   * HTML::Restrict
   * Image::Info
+  * List::UtilsBy
 
   Wie immer bitte vor dem ersten Aufrufen einmal die Pakete überprüfen:
 
@@ -47,42 +328,42 @@ Upgrade auf v3.2.0
   Sofern das Upgrade von einer früheren Version als 3.1.0 geschieht auch die
   Upgradehinweise der Vorversionen beachten.
 
-* Druckvorlagen auf shipto-Verwendung prüfen
+  * Druckvorlagen auf shipto-Verwendung prüfen
 
   Hier hat sich das Standardverhalten geändert und ggf. werden shipto* nicht mehr
   ausgedruckt, hier müssten die Druckvorlagen individuell angepasst werden, s.a.
   Changelog -> Verkaufsbeleg-Ausdruck.
 
-Upgrade auf v3.1.0
-==================
+  Upgrade auf v3.1.0
+  ==================
 
 
-* BEVOR ein Aufruf im Administrationsbereich erfolgt, muss zwingend der
+  * BEVOR ein Aufruf im Administrationsbereich erfolgt, muss zwingend der
   webdav Ordner im Installationspfad vorhanden sein!
-   -  mkdir webdav/
-   -  Rechte für webserver setzen ($ chmod www-data webdav/)
+  -  mkdir webdav/
+-  Rechte für webserver setzen ($ chmod www-data webdav/)
   Dieses "Feature" war in vorhergehenden Versionen optional, wird aber
   für das Upgrade auf Mandantenfähigkeit vorausgesetzt.
 
-* Neue Softwarevoraussetzungen: Perl v5.10.1 oder neuer sowie
+  * Neue Softwarevoraussetzungen: Perl v5.10.1 oder neuer sowie
   PostgreSQL 8.4 oder neuer werden zwingend vorausgesetzt. Ein Betrieb
   mit älteren Versionen ist nicht mehr möglich.
 
-* Neue Perl-Modul-Abhängigkeiten:
+  * Neue Perl-Modul-Abhängigkeiten:
 
   * File::Copy::Recursive
   * Rose::DB::Object muss v0.788 oder neuer sein (aufgrund eines Bugs
-    in besagtem Modul im Zusammenspiel mit PostgreSQL)
+      in besagtem Modul im Zusammenspiel mit PostgreSQL)
 
   Wie immer bitte vor dem ersten Aufrufen einmal die Pakete überprüfen:
 
   $ scripts/installation_check.pl -ro
 
-* Die Datenbank muss zwingend Unicode als Encoding nutzen. Daher wird
+  * Die Datenbank muss zwingend Unicode als Encoding nutzen. Daher wird
   auch die Konfigurationsvariable "system.dbcharset" nicht mehr
   unterstützt.
 
-* Einführung von Mandanten. Früher war die Konfiguration der
+  * Einführung von Mandanten. Früher war die Konfiguration der
   Datenbanken für jeden Benutzer getrennt vorzunehmen. Mit diesem
   Release wurden Mandanten eingeführt: ein Mandant bekommt einen Namen
   sowie die Datenbankkonfiguration, und Benutzer bekommen
@@ -98,7 +379,7 @@ Upgrade auf v3.1.0
   manuell anzupassen. Dazu gehören:
 
   - der Task-Server (config/kivitendo.conf)
-  - CSV-Import von der Shell aus (scripts/csv-import-from-shell.sh)
+- CSV-Import von der Shell aus (scripts/csv-import-from-shell.sh)
 
   Die folgenden Scripte sind ebenfalls betroffen, allerdings nur für
   Entwickler interessant:
@@ -106,28 +387,28 @@ Upgrade auf v3.1.0
   - scripts/dbupgrade2_tool.pl
   - scripts/rose_auto_create_model.pl
 
-* Neue Benutzerrechte
+  * Neue Benutzerrechte
 
   Diese müssen bei vorhandenen Gruppen eventuell nachgepflegt werden. Z.B. bei
   der Gruppe Vollzugriff
 
   - Stammdaten -> Kunden und Lieferanten erfassen. Alle Lieferanten bearbeiten.
-    Alle Kunden bearbeiten
+  Alle Kunden bearbeiten
   - Konfiguration -> Verändern der kivitendo-Installationseinstellungen (die
-    meisten Menüpunkte unterhalb von 'System')
+      meisten Menüpunkte unterhalb von 'System')
 
-* Die alten ungepflegten Druckvorlagenvarianten French und Service
+  * Die alten ungepflegten Druckvorlagenvarianten French und Service
   wurden entfernt.
 
-* Die HTML-Druckvorlagen der Berichte (GuV, Bilanz, SuSa, BWA, UStVA) werden
+  * Die HTML-Druckvorlagen der Berichte (GuV, Bilanz, SuSa, BWA, UStVA) werden
   jetzt alle zentral in den Webvorlagen verwaltet, es werden keine
   benutzerangepasste Versionen der Druckvorlagen im Druckvorlagenverzeichnis
   mehr unterstützt.
 
-Upgrade auf v3.0.0
-==================
+  Upgrade auf v3.0.0
+  ==================
 
-* Neue Abhängigkeiten
+  * Neue Abhängigkeiten
 
   * Clone 1.16
   * Email::MIME
@@ -142,37 +423,37 @@ Upgrade auf v3.0.0
 
   $ scripts/installation_check.pl -ro
 
-* Neue Entwicklerabhängigkeiten
+  * Neue Entwicklerabhängigkeiten
 
   * Test::Deep
   * GD 2.00
 
-* Diverse umstrittene Features zum nicht standardkonformen Umgang mit gebuchten
+  * Diverse umstrittene Features zum nicht standardkonformen Umgang mit gebuchten
   Rechnungen sind jetzt standardmässig deaktiviert und müssen unter "System" ->
   "Mandantenkonfiguration" aktiviert werden.
 
-* Die Übersetzungen "de_DE" und "fr" für die alternative deutsche Version und
+  * Die Übersetzungen "de_DE" und "fr" für die alternative deutsche Version und
   französische Version respektive wurden entfernt. Es bleiben offiziell
   unterstützte Übersetzungen in Deutsch ("de") und English ("en").
 
-* Dieses ist die letzte Version, die Perl-Versionen vor 5.10.1
+  * Dieses ist die letzte Version, die Perl-Versionen vor 5.10.1
   unterstützen wird.  Ab dem nächsten Release werden Sprachkonstrukte
   verwendet werden, die nicht mehr in 5.8 kompilieren, und Module, die
   seit v5.10.1 zu den Coremodulen gehören, werden ab dann nicht mehr
   als explizite Abhängigkeiten gelistet.
 
 
-Upgrade auf v2.7.0
-==================
+  Upgrade auf v2.7.0
+  ==================
 
-* In der Version 2.7.0 wird das XUL Menü entfernt. Alle Benutzer die das XUL
+  * In der Version 2.7.0 wird das XUL Menü entfernt. Alle Benutzer die das XUL
   Menü noch eingestellt haben, werden beim ersten Einloggen auf ein
   Kompatibilitätsmenü gesetzt. Das Javascriptmenü wurde entsprechend erweitert
   um der Funktionalität nahe zu kommen.
 
-* Das Lizenzenfeature wurde ersatzlos entfernt.
+  * Das Lizenzenfeature wurde ersatzlos entfernt.
 
-* In den LaTeX Vorlagen gilt der Befehl "pagebreak" und die dazugehörigen
+  * In den LaTeX Vorlagen gilt der Befehl "pagebreak" und die dazugehörigen
   "sumcarriedforward" und "lastpage" als deprecated und werden in einer
   kommenden Version komplett entfernt. Die Mechanik ist anfällig gegenüber
   subtilen Formatierungsfehlern bei bestimmten Zahlenformaten und ist
@@ -180,31 +461,31 @@ Upgrade auf v2.7.0
   auf einer Seite. Die Standardvorlagen sind entsprechend angepasst worden
   und müssen in der Administration neu angelegt werden.
 
-* Das Druckvorlagensystem wurde umgestellt, dadurch ist der Name "print" für
+  * Das Druckvorlagensystem wurde umgestellt, dadurch ist der Name "print" für
   Druckvorlagen jetzt reserviert. Wenn eine Ihrer Vorlagensätze "print" heisst,
   benennen Sie ihn um bevor Sie das Update starten.
 
-* Die Druckvorlagen für USTVA vor 2012 wurden entfernt und das Ausdrucken von
+  * Die Druckvorlagen für USTVA vor 2012 wurden entfernt und das Ausdrucken von
   USTVA als PDF ist deprecated. Da die Eingabe von Erklärungen als PDF nicht
   mehr gestattet ist, sollten Archivkopien der USTVA direkt bei Elster bezogen
   werden, oder auf anderem Wege erstellt werden. Der Prozess dazu wird sich in
   einer kommenden Version ändern.
 
-* Die Namen der von LaTeX generierten PDF-Dateien sind jetzt in der
+  * Die Namen der von LaTeX generierten PDF-Dateien sind jetzt in der
   eingestellten Dokumentensprache, nicht mehr in der Oberflächensprache des
   Bearbeiters.
 
-* Neue Abhängigkeiten
+  * Neue Abhängigkeiten
 
   * JSON
   * String::ShellQuote
-  * Digest::SHA (optional, empfohlen)
+* Digest::SHA (optional, empfohlen)
 
   Wie immer bitte vor dem ersten Aufrufen einmal die Pakete überprüfen:
 
   $ scripts/installation_check.pl -ro
 
-* CSV-Import wurde neu in Perl implementiert
+  * CSV-Import wurde neu in Perl implementiert
 
   Der PHP-Code wurde entfernt. Automatische Skripte, die per Aufruf von
   lxo-import/partsB.php?cron=1 die Datei parts.csv importiert haben,
@@ -212,132 +493,132 @@ Upgrade auf v2.7.0
   benutzt werden. Im Unterschied zur PHP-Version werden unbekannte Warengruppen
   nicht mehr automatisch angelegt, stattdessen bricht das Skript ab.
 
-* Rechteverwaltung
+  * Rechteverwaltung
 
   * Das Recht "Kunden und Lieferanten bearbeiten" wurde aufgespalten in zwei
-    einzelne Rechte. Ein Updatescript passt bestehende Gruppenaentsprechend an.
+  einzelne Rechte. Ein Updatescript passt bestehende Gruppenaentsprechend an.
   * Das Recht "Preise nd Rabatte bearbeiten" wurde neu eingeführt und ist
-    notwendig um in Belegen Preise ändern zu können. Es wird beim Upgrade
-    automatisch allen Benutzern erteilt.
+  notwendig um in Belegen Preise ändern zu können. Es wird beim Upgrade
+  automatisch allen Benutzern erteilt.
   * Das Recht "Administration" wurde neu eingeführt, und ist dazu da
-    administrative Tätigkeiten an der Mandantendatenbank aus einm Benutzerlogin
-    heraus durchzuführen. Es ist standardmäßig NICHT vergeben.
+  administrative Tätigkeiten an der Mandantendatenbank aus einm Benutzerlogin
+  heraus durchzuführen. Es ist standardmäßig NICHT vergeben.
   * Der Vorlageneditor wurde unter das Recht Administration gestellt, war
-    vorher Konfiguration.
+  vorher Konfiguration.
 
 
-Upgrade auf v2.6.3
-==================
+  Upgrade auf v2.6.3
+  ==================
 
-1. Mit Version 2.6.3. wurden die beiden Konfigurationsdateien
-authentication.pl und lx-erp.conf, sowie deren Varianten,
-abgeschafft. Stattdessen gibt es nun die Datei lx_office.conf, die
-aber erst neu angelegt werden muß. Als Vorlage dient hierfür die Datei
-lx_office.conf.default. Die entsprechenden Werte muß man selber neu
-konfigurieren, dies ist automatisiert zu fehleranfällig.
+  1. Mit Version 2.6.3. wurden die beiden Konfigurationsdateien
+  authentication.pl und lx-erp.conf, sowie deren Varianten,
+  abgeschafft. Stattdessen gibt es nun die Datei lx_office.conf, die
+  aber erst neu angelegt werden muß. Als Vorlage dient hierfür die Datei
+  lx_office.conf.default. Die entsprechenden Werte muß man selber neu
+  konfigurieren, dies ist automatisiert zu fehleranfällig.
 
-Nach dem Upgrade kann man sich so lange nicht anmelden, bis lx_office.conf
-angelegt und authentication.pl und lx-erp.conf gelöscht oder verschoben wurden.
+  Nach dem Upgrade kann man sich so lange nicht anmelden, bis lx_office.conf
+  angelegt und authentication.pl und lx-erp.conf gelöscht oder verschoben wurden.
 
-Es gibt keine local-Variante der lx_office.conf, arbeitet man mit git sollte
-man lx_office.conf nicht einchecken.
+  Es gibt keine local-Variante der lx_office.conf, arbeitet man mit git sollte
+  man lx_office.conf nicht einchecken.
 
-Eine etwas ausführlichere Beschreibung findet sich in Kapitel 2.3
-"Lx-Office-Konfigurationsdatei" in doc/Lx-Office-Dokumentation.pdf
+  Eine etwas ausführlichere Beschreibung findet sich in Kapitel 2.3
+  "Lx-Office-Konfigurationsdatei" in doc/Lx-Office-Dokumentation.pdf
 
-2. Eine neu hinzugekommene Komponente ist der Task-Server. Hierbei
-handelt es sich um einen Dämonen, der im Hintergrund läuft, in
-regelmäßigen Abständen nach abzuarbeitenden Aufgaben sucht und diese
-zu festgelegten Zeitpunkten abarbeitet (ähnlich wie Cron). Dieser
-Dämon wird bisher nur für die Erzeugung der wiederkehrenden Rechnungen
-benutzt, wird aber in Zukunft deutlich mehr Aufgaben übertragen
-bekommen. Die Einrichtung des Dämonen wird in der
-Installationsdokumentation im Abschnitt "Der Task-Server" beschrieben.
+  2. Eine neu hinzugekommene Komponente ist der Task-Server. Hierbei
+  handelt es sich um einen Dämonen, der im Hintergrund läuft, in
+  regelmäßigen Abständen nach abzuarbeitenden Aufgaben sucht und diese
+  zu festgelegten Zeitpunkten abarbeitet (ähnlich wie Cron). Dieser
+  Dämon wird bisher nur für die Erzeugung der wiederkehrenden Rechnungen
+  benutzt, wird aber in Zukunft deutlich mehr Aufgaben übertragen
+  bekommen. Die Einrichtung des Dämonen wird in der
+  Installationsdokumentation im Abschnitt "Der Task-Server" beschrieben.
 
-3. Mit Version 2.6.3 sind einige Abhängigkeiten von Perl-Modulen
-hinzugekommen. Bitte führen sie vor dem ersten Aufrufen der einmal
-den folgenden Befehl im Lx-Office Verzeichnis aus:
+  3. Mit Version 2.6.3 sind einige Abhängigkeiten von Perl-Modulen
+  hinzugekommen. Bitte führen sie vor dem ersten Aufrufen der einmal
+  den folgenden Befehl im Lx-Office Verzeichnis aus:
 
-$ scripts/installation_check.pl
+  $ scripts/installation_check.pl
 
-Sollten Module als fehlend markiert sein, folgen Sie bitte den Anweisungen in
-der Installationsanweisung.
+  Sollten Module als fehlend markiert sein, folgen Sie bitte den Anweisungen in
+  der Installationsanweisung.
 
-Zumindest folgende Module sind neu benötigt:
+  Zumindest folgende Module sind neu benötigt:
 
-* Config::Std
-* Params::Validate
+  * Config::Std
+  * Params::Validate
 
-4. Sollten Sie die FCGI-Version einsetzen, das Apache-Modul
-"mod_fcgid" (nicht "mod_fastcgi") benutzen und von diesem Modul die
-Version v2.6.3 oder später installiert haben, so ist außerdem wichtig,
-seinen Parameter "FcgidMaxRequestLen" deutlich zu erhöhen, weil sich
-dieser im Release mod_fcgid-Release v2.6.3 deutlich geändert
-hat. Details dazu finden sich in Kapitel 2.5.2 "Konfiguration für
-FastCGI/FCGI" in doc/Lx-Office-Dokumentation.pdf
+  4. Sollten Sie die FCGI-Version einsetzen, das Apache-Modul
+  "mod_fcgid" (nicht "mod_fastcgi") benutzen und von diesem Modul die
+  Version v2.6.3 oder später installiert haben, so ist außerdem wichtig,
+  seinen Parameter "FcgidMaxRequestLen" deutlich zu erhöhen, weil sich
+  dieser im Release mod_fcgid-Release v2.6.3 deutlich geändert
+  hat. Details dazu finden sich in Kapitel 2.5.2 "Konfiguration für
+  FastCGI/FCGI" in doc/Lx-Office-Dokumentation.pdf
 
 
-Upgrade auf v2.6.2
-==================
+  Upgrade auf v2.6.2
+  ==================
 
- Vor dem Einloggen
- -----------------
 Vor dem Einloggen
 -----------------
 
-Mit Version 2.6.2 sind einige Abhängigkeiten von Perl-Modulen hinzugekommen.
-Bitte führen sie vor dem ersten Aufrufen der einmal den folgenden Befehl im
-Lx-Office Verzeichnis aus:
+  Mit Version 2.6.2 sind einige Abhängigkeiten von Perl-Modulen hinzugekommen.
+  Bitte führen sie vor dem ersten Aufrufen der einmal den folgenden Befehl im
+  Lx-Office Verzeichnis aus:
 
-$ scripts/installation_check.pl
+  $ scripts/installation_check.pl
 
-Sollten Module als fehlend markiert sein, folgen Sie bitte den Anweisungen in
-der Installationsanweisung.
+  Sollten Module als fehlend markiert sein, folgen Sie bitte den Anweisungen in
+  der Installationsanweisung.
 
-Zumindest folgende Module sind neu benötigt:
+  Zumindest folgende Module sind neu benötigt:
 
-* Rose::Object, Rose::DB und Rose::DB::Object (die Installation von
-  Rose::DB::Object via CPAN oder den Paketmechanismus Ihrer
-  Distribution sollte für die automatische Installation der anderen
-  zwei Pakete sorgen)
+  * Rose::Object, Rose::DB und Rose::DB::Object (die Installation von
+      Rose::DB::Object via CPAN oder den Paketmechanismus Ihrer
+      Distribution sollte für die automatische Installation der anderen
+      zwei Pakete sorgen)
 
- Neue Gruppenrechte
- ------------------
 Neue Gruppenrechte
 ------------------
 
-Es wurde ein neues Recht "Druck" eingeführt. Dieses bestimmt, ob die
-Benutzerin das Menü "Druck" zu Gesicht bekommt oder nicht, unabhängig
-davon, wie die Rechte für die einzelnen Unterpunkte gesetzt sind.
+  Es wurde ein neues Recht "Druck" eingeführt. Dieses bestimmt, ob die
+  Benutzerin das Menü "Druck" zu Gesicht bekommt oder nicht, unabhängig
+  davon, wie die Rechte für die einzelnen Unterpunkte gesetzt sind.
 
-Für bereits bestehende Gruppen muss es sofern gewünscht vom
-Administrator manuell gewährt werden.
+  Für bereits bestehende Gruppen muss es sofern gewünscht vom
+  Administrator manuell gewährt werden.
 
 
-Upgrade auf v2.6.1
-==================
+  Upgrade auf v2.6.1
+  ==================
 
- Vor dem Einloggen
- -----------------
 Vor dem Einloggen
 -----------------
 
-Mit Version 2.6.1 wurden die Listen der benötigten Perl Module überarbeitet.
-Einige der vorher in den Abhängigkeiten gelisteten Module waren Coremodules
-(und damit in jeder Perldistribution vorhanden), oder ihrerseits Abhängigkeiten
-anderer benötigter Module. Durch die Überarbeitung hat sich die Liste deutlich
-geändert.
+  Mit Version 2.6.1 wurden die Listen der benötigten Perl Module überarbeitet.
+  Einige der vorher in den Abhängigkeiten gelisteten Module waren Coremodules
+  (und damit in jeder Perldistribution vorhanden), oder ihrerseits Abhängigkeiten
+  anderer benötigter Module. Durch die Überarbeitung hat sich die Liste deutlich
+  geändert.
 
-Bitte führen sie vor dem ersten Aufrufen der einmal den folgenden Befehl im
-Lx-Office Verzeichnis aus:
+  Bitte führen sie vor dem ersten Aufrufen der einmal den folgenden Befehl im
+  Lx-Office Verzeichnis aus:
 
-$ scripts/installation_check.pl
+  $ scripts/installation_check.pl
 
-Sollten Module als fehlend markiert sein, folgen Sie bitte den Anweisungen in
-der Installationsanweisung.
+  Sollten Module als fehlend markiert sein, folgen Sie bitte den Anweisungen in
+  der Installationsanweisung.
 
-Zumindest folgende Module sind neu benötigt:
+  Zumindest folgende Module sind neu benötigt:
 
-* URI
-* XML::Writer
+  * URI
+  * XML::Writer
 
- Neue Konfigurationsvariablen
- ----------------------------
 Neue Konfigurationsvariablen
 ----------------------------
 
-In der config/lx-erp.conf ist als neue Option $show_best_before hinzugekommen.
-Die Variable kontrolliert die Anzeige von Mindesthaltbarkeitsdaten. Sie ist
+  In der config/lx-erp.conf ist als neue Option $show_best_before hinzugekommen.
+  Die Variable kontrolliert die Anzeige von Mindesthaltbarkeitsdaten. Sie ist
 standardmäßig deaktiviert.
index cb62a04..88a0e01 100644 (file)
 # Veränderungen von kivitendo #
 ###############################
 
+2022-05-20 - Release 3.6.1
+
+Größere neue Features:
+
+Mittelgroße neue Features:
+
+ - Neuer Workflow Artikel->Lieferantenaufrag. Hierbei wird der gerade
+   bearbeitete Artikel gespeichert und die Lieferantenauftrags-Maske
+   geöffnet. Der Artikel ist dann in der Eingabezeile vorbelegt.
+   Sofern genau ein Lieferant beim Artikel hinterlegt ist, so wird
+   auch dieser im Lieferantenauftrag vorbelegt.
+ - In Angebot und Auftrag gibt es einen neuen Reiter für Telefonnotizen.
+   Hier können Notizen zum Beleg erfasst werden. Nach diesen lässt sich im
+   Bericht auch filtern.
+ - Neuer Filter im Auftragsbericht nach "Volltext". Hierzu werden die Texte in
+   den Feldern Bemerkungen, interne Bemerkungen, Versandort, Transportmittel,
+   Vorgangsbezeichnung, Auftragsnummer, Angebotsnummer und
+   Bestellnummer des Kunden durchsucht,
+   Zudem werden Dokumente und Anhänge zu Aufträgen im DMS durchsucht.
+   Dazu wird ein Hintgergrund-Job eingerichtet (täglich 03:20 Uhr), der die
+   Texte aus den Dokumenten extrahiert. Im Moment werden Texte aus Dokumenten
+   mit den mime-Typen 'application/pdf', 'text/html' und 'text/plain'
+   ausgelesen.
+
+Kleinere neue Features und Detailverbesserungen:
+
+ - Die Protokollierung von E-Mails in interne Bemerkungen ist deaktiviert,
+   falls der Mandant sowieso das E-Mail-Journal aktiviert hat.
+ - Steuerschlüssel 94, 19 und 18 neu angelegt und um Reverse Charge erweitert.
+   D.h. bei diesen Steuerschlüsseln
+   kann in einem netto verbuchten Kreditorenbeleg gleichzeitig Vor- und
+   Mehrwertsteuer verbucht werden. Die Steuerbuchung wird in einer separat
+   verknüpften Dialogbuchung gemacht.
+ - Im Kunden-/Lieferantenbereicht kann nach "allen Telefonnummern" gefiltert
+   werden. Hier wird in den Feldern Telefon und Fax bei Kunden und Lieferanten
+   und in weiteren Feldern bei Ansprechpersonen (Tel. 1/2, Fax, Mobil 1/2,
+   Sat. Tel, Sat. Fax, Privates Tel.) gesucht.
+ - Es gibt eine neue Schnellsuche "Alle Telefonnummern", die alle Telefonnumern
+   bei Kunden, Lieferanten und Ansprechpersonen durchsucht.
+-  Skontoautomatik bei Kontoauszug verbuchen generiert automatisch die
+   Steuerkorrektur pro Steuersatz des Belegs als verknüpfte Dialogbuchung
+-  Verknüpfte Belege auch für Dialogbuchungen (neuer Reiter)
+-  DMS: Anzeige von Versionen verbessert: Angezeigt wird immer nur die neueste
+   Version einer Datei. Weitere Versionen lassen sich durch Ausklappen
+   anzeigen. Dies gilt nun auch für die Dokument-Typen Anhänge und Bilder, bei
+   denen zuvor nur die neueste Version angezeigt wurde.
+-  Um ein ungewolltes doppeltes Buchen einer Verkaufsrechnung zu verhindern,
+   dass durch den Browser-Zurück-Knopf (und dann nochmaliges Buchen) ausgelöst
+   werden kann, kann in der Mandantenkonfiguration das Aushebeln des Browser-
+   Zurück-Knopfes bei Verkaufsrechnunghen aktiviert werden.
+   Da dadurch allerdings auch Situationen ausgehebelt werden, in denen das
+   Drücken des Zurück-Knopfes sinnvoll ist, ist dies konfigurierbar.
+-  Rechte (nur) zum Lesen von Belegen, getrennt nach Einkauf/Verkauf und
+   Angebot/Auftrag/Lieferschein/Rechnung. Wer nur das Lese-Recht hat, kann
+   Belege nicht anlegen und nicht speichern.
+-  neues Feld "Vorgangsbezeichnung" in Kreditoren-, Debitoren und Dialogbuchung.
+-  Rechnungsbericht VK und EK kann nach Steuerzone gefiltert und sortieren werden.
+-  Möglichkeit, Namen von Dateianhängen im Rechnungsbericht anzuzeigen.
+
+
+Bugfixes (Tracker: https://www.kivitendo.de/redmine):
+
+498 Angebot/Auftrags-Maske: Drucken mit nicht-änderbarer Belegnummer zeigt diese nicht an
+494 Beim Erstellen eines Auftrags via Workflow aus der Kundenmaske wird die Sprache nicht übernommen
+491 qty real nach numeric migrieren
+479 Preise neuer Auftragsconroller
+
+
+2022-03-02 - Release 3.6.0
+
+Größere neue Features:
+
+- Mobilvariante Handyfotos für Lieferscheine
+
+  Die neue mobile Variante von kivitendo kann Handyfotos an Lieferscheine hinzufügen.
+  Das Feature setzt ein mobiles Endgerät voraus, aufgrund dessen wird ein passendes
+  CSS-Design im Android-Stil geladen und über die Suche nach einem Lieferschein lassen
+  sich aufgenommen Fotos hochladen. Die Funktion benötigt ein aktiviertes DMS innerhalb
+  von kivitendo.
+
+- Lieferanten-Beistelllieferschein
+
+  Über den Lieferantenworkflow ist es jetzt möglich
+  einen Lieferantenausgangslieferschein zu erstellen (beigestellte Ware).
+  Mit diesem neuen Belegtyp können dann für einen Lieferanten Waren ausgelagert,
+  sprich mitgegeben werden. Damit kann der Anwendungsfall: Lieferant veredelt
+  eigene Erzeugnisse weiter oder erbringt Dienstleistungen mit selbst erzeugter
+  Ware abgebildet werden.
+  Dieser Belegtyp wurde vollständig unabhängig vom alten Lieferschein-Code ent-
+  wickelt (MVC Modell, wie beim neueren Auftrag) und enthält die Option
+  Belegart (Einkauf oder Verkauf) sowie Lagerrichtung (Ein- oder Auslagern) beliebig
+  zu kombinieren.
+
+- Shopware 6 Schnittstelle
+
+  kivitendo unterstützt jetzt die neuere Shopware Version 6 als Alternative
+  zum bisherigen Shopware 5 Konnektor. Die meisten Funktionen sind analog zum
+  Shopware 5 Konnektor implementiert. Admins können sich im Detail im Perl-Doc
+  über die Implementierung informieren (perldoc SL/ShopConnector/Shopware6.pm).
+
+- Anzahlungs- und Schlussrechnung konform nach deutschem Steuerrecht
+
+  Es gibt zwei neue Typen von Rechnungen, einmal den Typ Anzahlungsrechnung und den Typ Schlußrechnung.
+  Die Anzahlungsrechnung braucht keinen Vorgänger.
+  Schlußrechnung braucht immer einen Vorgänger.
+  Vorgänger für die Schlußrechnung kann eine Anzahlungsrechnung oder ein Auftrag sein.
+  Sollte der Workflow bei Anzahlungsrechnung starten, kann von der Anzahlungsrechnung aus eine
+  weitere Anzahlungsrechnung oder eine Schlußrechnung generiert werden.
+  Alternativ kann der Workflow auch mit einem Auftrag beginnen, dann muss die Schlußrechnung auch von diesem Auftrag aus erstellt werden.
+  Buchhalterische Änderungen:
+  Die Anzahlungsrechnung wird nicht auf das Standard-Ertragkonto gebucht,
+  sondern auf ein definiertes Transferkonto, ferner wird keine Mehrwertsteuer gebucht.
+  Sobald der Zahlungseingang zu dieser Anzahlungsrechnung verbucht wird (per Bankimport),
+  wird die Mehrwertsteuer entsprechend zum Zahlbetrag brutto verbucht.
+  Damit das ganze DATEV konform bleibt, wird der entsprechende netto Betrag des Zahlbetrags
+  auf ein Steuertransferkonto je nach Steuersatz verschoben.
+  Sobald die Schlußrechnung gebucht wird, werden die Verschiebungen wieder rückgängig gemacht
+  und falls die Schlußrechnung in Summe höher ist als die vorherigen Anzahlungsrechnungen wird
+  die Mehrwertsteuer anteilig gebucht.
+  Die Standard-Druckvorlage marei, enthält exemplarisch zwei neue Druckvarianten die
+  diesen Fall abbilden und somit als Orientierung für eigene Vorlagen-Anpassungen
+  dienen können.
+
+
+Mittelgroße neue Features:
+
+- In Kundenstammdaten können nun abweichende Rechnungsadressen analog zu
+  Lieferadressen verwaltet werden. Diese können in Verkaufsbelegen
+  ausgewählt werden. Sie stehen den Druckvorlagen als eigene Variablen
+  zur Verfügung.
+- Unterstützung für Schweizer QR-Rechnung mit OpenDocument Vorlagen.
+  Varianten: QR-IBAN mit QR-Referenz, IBAN ohne Referenz
+- Neuer benutzerdefinierter Variablentyp HTML-Feld
+  Der Funktionsumfang entspricht dem Editor im Langtext/Bemerkungen
+  innerhalb der Belege. Erweiterbar für alle auch bisher verwendete
+  Objekte die benutzerdefinierte Variable verwenden können (Stammdaten,
+  Projekte, usw)
+- DMS unterstützt auch Druckvarianten des Belegs
+  Bisher konnte das DMS nur die Hauptvariante des Belegtyps zuordnen,
+  jetzt wird auch bei allen bekannten Druckvariante ein entsprechend
+  eigenständiger Dokumenteneintrag, inkl. Version hinterlegt
+
+Kleinere neue Features und Detailverbesserungen:
+
+- neue Druckvorlagen-Variante "Rechnungskopie", die mit dem Druckvorlagensatz marei
+  ein Wasserzeichen "Rechnungskopie" bei Verkaufs-Rechnungen erzeugt
+- Alle HTML-Textfelder benutzen die Rechtschreibprüfung des Anwender-Browser und
+  markieren unbekannte Worte (Tippfehler) mit einer roten gewellten Linie
+- Prüfung, ob Kundenbestellnummer in Verkaufsaufträgen vorhanden ist, kann in der
+  Mandantenkonfiguration eingestellt werden
+- Optionale Warnung falls eine Verkaufsrechnung nicht aus einem Lieferschein
+  erzeugt wurde (Konfigurierbar in der Mandantenkonfiguration)
+- Die Ansicht der verknüpften Belegen kann unabhängig vom aktuellen Beleg immer
+  vom Auftrag her aufgebaut werden
+- SEPA-Überweisungen & -Bankeinzüge nutzen jetzt aktuelle Standard-Versionen, die
+  momentan von der Kreditindustrie unterstützt werden.
+- Pflichtenhefte: wenn man im Workflow vom Pflichtenheft ein neues
+  Angebot anlegt und später von diesem Angebot aus einen Auftrag, so
+  wird auch der Auftrag direkt mit dem Pflichtenheft verknüpft.
+- Pflichtenhefte: wenn in einem Auftrag, das mit einem Pflichtenheft
+  verknüpft ist, ein Projekt ausgewählt, so wird dieses Projekt auch
+  automatisch beim verknüpften Pflichtenheft eingetragen.
+- Druckvorlagen: die in Positionen verwendeten Variablen können nun
+  Platzhalter enthalten, die vom Beleg selber stammen. So könnte
+  z.B. in der Artikelbeschreibung automatisch die Rechnungsnummer
+  ersetzt werden. Beispiel: »Abrechnungszeitraum bis <%invnumber%>«
+- Verkaufs- & Einkaufsbelege: kivitendo kann so konfiguriert werden,
+  dass die Belegnummern von Belegen, die auf unserer Seite erzeugt
+  werden, nicht mehr editierbar sind. In dem Fall vergibt kivitendo
+  sie immer automatisch und zeigt sie in den Belegmasken nur noch an.
+- Warengruppe kann nun als Pflichtfeld für Artikel konfiguriert werden.
+- Das E-Mail Feld 'body' innerhalb von kivitendo unterstützt jetzt HTML-Formatierungen
+  Somit kann der Versand von wiederkehrenden Rechnungen als auch der
+  manuelle E-Mail-Versand von Belegen wie das Beleg-Bemerkungsfeld formatiert werden.
+- Für die HTML-Texte ist jetzt die Rechtschreibprüfung des Anwender-Browsers aktiviert
+- Beim E-Mail-Versand wird jetzt gewarnt, falls scheinbar keine
+  gültige E-Mail-Adresse des Empfängers existiert
+- Optionale auftragszentrische Verknüpfte Belege
+  Konfigurierbar in der Mandantenkonfiguration. Unabhängig vom
+  aktuellen Belegort werden die verknüpften Belege immer vom VK-Auftrag aufgebaut.
+- Lieferplan: Geschwindigkeitssteigerung
+- SEPA: aktuell von Kreditinstituten unterstützte Formatversionen nutzen
+- Pflichtenhefte: bei Pflichtenheft → Angebot → Auftrag auch PH mit Auftrag verknüpfen
+- Auftrag: Projekt automatisch in verknüpftem Pflichtenheft eintragen
+- Ein-/Verkauf: Belegnummern von uns erzeugter Belege nicht ändern können (Mandantenkonfig)
+  Für Belege, die auf unserer Seite erzeugt werden, kann nun verhindert
+  werden, dass die Belegnummer manuell angepasst bzw. gesetzt
+  wird. Statt dessen wird sie immer vom System beim ersten Speichern
+  vergeben und beim späteren Bearbeiten nur noch read-only angezeigt.
+- Verkaufsrechnungen direkt als Factur-X/ZUGFeRD-XML exportieren können
+- Order-Controller: Unterstützung für Drucken & E-Mailen von HTML-Vorlage
+- Der Lagerbewegungs-Import (CSV) unterstützt auch Fließkommazahlen
+- E-Mails aus kivitendo werden jetzt HTML-formatiert verschickt, mit
+  den bekannten Editiermöglichkeiten aus den Bemerkungen/Langtext
+- Bei längeren Langtexten in der Position ist jetzt ein Vergrößern des
+  Textfelds im Popup-Dialog möglich
+
+
+Bugfixes (Tracker: https://www.kivitendo.de/redmine):
+
+488 Lager ohne Lagerplatz nicht erlauben
+486 Bericht Lagerentnahme: Link zur Ware kaputt
+485 Offenen Forderungen zum Stichtag mit aktuellem Status
+484 CSV/PDF-Export Summen- und Saldenliste mit allen Konten
+
+
+2021-08-10 - Release 3.5.8
+
+Kleinere neue Features und Detailverbesserungen:
+
+- Erzeugnis fertigen, kann jetzt auf Lagerteile aus anderem Lagern zurückgreifen
+  und die Fertigung liefert keinen Fehler mehr. Einstellbar in der
+  Mandantenkonfiguration.
+- Erzeugnisse fertigen, kann auch Dienstleistungen verbrauchen, falls
+  diese ein Erzeugnisbestandteil sind. Standardmäßíg deaktiviert.
+  Aktivierbar in der Mandantenkonfiguration (Bereich Lager).
+- API- Änderung Erzeugnis fertigen nutzt jetzt SL/Helper/Inventory.pm
+- Falls der Mandant zu jeder Buchung einen Beleg hinzufügen möchte,
+  und dies in der Mandantenkonfiguration einstellt, dann öffnet sich nach
+  dem Buchen von Dialog-/Kreditoren- und EK-Rechnungs-Buchungen der
+  Dokumenten-Reiter des entsprechenden Belegs.
+  Bei dieser Einstellung gibt für Dialog- und Kreditoren-Buchungen eine
+  zweite Aktion unterhalb von "Buchen", nämlich "Buchen und neue Buchung".
+  Ist die Option ausgestellt, ist das Verhalten nach dem Buchen wie zuvor
+  und es gibt eine zweite Aktion "Buchen und Dokument hochladen", mit der
+  in den Dokumenten-Tab gesprungen werden kann.
+- Seriennummer ist jetzt ein Pflichtfeld für Lieferscheine (Einkauf und Verkauf),
+  falls die Ware im Beleg in den Stammdaten mit "Hat eine Serienummer" markiert ist.
+- Einkaufsbericht um Anzeige erstes Sollkonto erweitert
+- Einkaufsbericht um Anzeige Erfassungsdatum erweitert
+- Import der Lohnbuchhaltungsdatensätze aus DATEV Lohnbuchhaltung
+
+Bugfixes (Tracker: https://www.kivitendo.de/redmine):
+
+483 Upgrade-Skript: delete_cvars_on_trans_deletion_add_shipto löscht alle custom_variables
+
+
+2021-06-25 - Release 3.5.7
+
+Größere neue Features:
+  - Modul zur Zeiterfassung. Es ist nun möglich, auftrags-, kunden- oder
+    projektbezogen, Arbeitszeiten zu erfassen. Die erfassten Zeiten können
+    über einen Hintergrund-Job in Lieferscheine umgewandelt werden.
+
+Mittelgroße neue Features:
+
+ - Der Import von Bankauszügen im MT940-Format wurde komplett neu
+   geschrieben. Das externe Programm AQBanking wird nun nicht mehr
+   benötigt.
+ - Lupe für Projekt-Picker, über die ein Auswahl-Dialog geöffnet werden
+   kann.
+ - Verbesserungen beim Erzeugen von Mahnungen:
+   - erzeugte Dokumente werden zum Mahnlauf abgelegt
+   - erzeugte Dokumente im Dateimanagement und im WebDAV werden im
+     Bericht angezeigt
+   - erzeugte Dokumente werden erst nach der "Mahnungs-Transaktion"
+     abgelegt, wenn diese ohne Fehler verlaufen ist, sonst können
+     Dokumente ohne Mahnung abgelegt werden
+   - Fehler der Mahnläufe werden gesammelt und nach dem Mahnen in
+     einem Status-Bericht angezeigt
+   - die Verknüpfung bei der Rechnung zur Mahnung führt nicht mehr zum
+     Drucken, sondern zum Mahnbericht f. die entsprechende Mahnung
+   - der Attachment-Name der Mahnrechnung (mit den Gebühren) enthält
+     jetzt die Rechnungsnummer und nicht mehr die Mahnungsnummer
+   - Verknüpfung der Mahnungen mit E-Mail-Journal evtl. verschickter
+     Mails
+   - Anzeige der Mails im Mahnbericht
+   - DB-Trigger zum Löschen von Verknüpfungen beim Löschen einer
+     Mahnung
+ - Neuer Order-Controller: Artikel können während der Erfassung eines
+   Angebots bzw. Auftrags erfasst werden.
+ - Webshopschnittstelle
+   - Überarbeitet und verbessert. Shopstadi werden jetzt gesetzt und
+     an den Shop gemeldet
+   - Woocommerce Schnittstelle
+
+Kleinere neue Features und Detailverbesserungen:
+  - Der Status geliefert bei Aufträgen kann mit oder ohne Dienstleistungen
+    im Lieferschein berechnet werden. Einstellbar in der Mandantenkonfiguration
+    jeweils unabhängig für Einkauf und Verkauf.
+    Standardeinstellung: Dienstleistungen sind lagerbar.
+  - Gefertigte Erzeugnisse können innerhalb des Zurücklagerungszeitraums
+    wieder zerlegt werden. Die Aktion befindet sich im Lagerbuchungsbericht
+  - E-Mail-Versand: Neben dem Freitext CC-Feld kann jetzt auch ein
+    kivitendo Benutzer mittels einer Auswahlliste in CC gesetzt werden
+  - Falls der Mandant zu jeder Buchung einen Beleg hinzufügen möchte,
+    ist dies jetzt in der Mandantenkonfiguration einstellbar und falls
+    zusätzlich die DMS Funktion aktiv ist, bleibt der Bearbeiter nach
+    dem  Erfassen einer Buchung in der Maske und kann einen Beleg hinzufügen
+ - Ausgelagerte Lieferscheinen können zurückgelagerte werden insofern der
+   konfigurierbare Zurücklagerungszeitraum noch nicht überschritten ist.
+ - Angebote und Aufträge im Ein- und Verkauf können optionale Positionen enthalten.
+   Optionale Positionen werden in der zweiten Zeile der Position aktiviert.
+   Die einzelne Position wird dann berechnet und erscheint im Ausdruck mit dem
+   berechnetem Preis, die Position wird aber nicht in der Gesamtsumme des Belegs
+   aufgenommen. Dies gilt auch für die Gesamt-Marge und den Gesamt-Ertrag des Belegs.
+   Innerhalb der Druckvorlagen steht das Attribut mit <%optional%> als Variable zu Verfügung.
+   Beim Status setzen eines Auftrags (offen oder geschlossen) werden optionale Position
+   ignoriert. D.h. ein Auftrag gilt als geschlossen, wenn alle nicht optionalen
+   Positionen fakturiert worden sind.
+   Das Gleiche gilt für Lieferscheine. Sollten alles bis auf optionale Artikel
+   geliefert worden sein, gilt der Auftrag als komplett geliefert.
+   Das Attribut optional steht auch nur in den Angeboten/Aufträgen zu Verfügung.
+   Sobald über den Workflow ein neuer Beleg erstellt wird,
+   wird die vorher optionale Position zu einer normalen Position
+   und wird dann auch entsprechend bei dem Rechnungsbeleg mit fakturiert und im
+   Druckvorlagen-System entfällt das Attribut <%optional%>.
+   Entsprechend exemplarisch im aktuellen Druckvorlagensatz RB ergänzt.
+
+ - Lagerbestandsbericht: Die Resultate pro Seite können im Bericht eingestellt werden
+ - Es gibt eine PDF-Druckvorschau für die Standard-Druckvorlage bei Angeboten und
+   Aufträgen im Einkauf und Verkauf ohne ein vorheriges Dialogmenü (Druckvorlage
+   ist die Standard-Druckvorlage und Typ immer 'PDF'). Die Druckvorschau wird nicht
+   im DMS oder WebDAV archiviert, es werden aber die Pflichtfelder des Belegs überprüft.
+ - Die benutzerdefinierten Variablen für Artikel können konfigurierbar im Tab Basisdaten
+   angezeigt werden (ohne extra Klick auf einen weiteren Tab)
+ - Der Lagerbestandsbericht wurde um die Anzeige von benutzerdefinierten Variablen
+   aus dem Bereich Artikel erweitert
+ - Im Lagerjournal ist standardmäßig die Berichtsanzeige um Dokument angehakt.
+   Sollte eine Warenbewegung durch einen Lieferschein oder eine Rechnung ausgelöst
+   worden sein, wird dies jetzt direkt verlinkt dort angezeigt
+ - Projekte wurden um Dateianhänge erweitert, die dort hochgeladenen Dokumente
+   stehen beim E-Mail-Versand in allen verknüpften Belegen vorausgewählt zu
+   Verfügung
+ - Dateimanagement: In der Liste der Dateien werden Vorschaubilder angezeigt,
+   falls möglich. Diese werden beim Drüberfahren vergrößert.
+ - Dateimanagement: Dokumente können auch hochgeladen werden, dort, wo sie
+   bisher nur vom Scanner importiert werden konnten.
+ - Dateimanagement: Dokumente können auch per Drag&Drop hochgeladen werden.
+ - In der Mandantenkonfiguration ist einstellbar, ob UStID oder Steuernummer
+   für Kunden oder Lieferanten eindeutig sein sollen.
+ - Menü und Rechte für Produktivität: Zugriffskontrolle aufgeteilt und
+   Rechte unterhalb "Produktivität" als eigene Kategorie
+ - Inventur-Makse: Part-Picker sucht auch nach Lieferanten-Artikelnummer
+ - Einkaufs-/Verkaufsbelege und Buchungsmasken: Neues Feld Leistungsdatum,
+   welches die Steuerberechnung beeinflusst. I.d.R. gilt für die Steuer:
+   Leistungsdatum. Wenn leer, dann Lieferdatum; wenn leer, dann Belegdatum.
+ - Neuer Order-Controller: Unterstützung für Übersetzungen von
+   Artikeln wurde implementiert.
+ - Einkaufs-/Verkaufsbelege: die Belegsprache ist nun als Auswahl
+   direkt in der Hauptmaske vorhanden und nicht mehr in den
+   Druckeinstellungen versteckt.
+ - Einkaufsrechnungen: wenn das direkte Anlegen von Einkaufsrechnungen
+   in der Mandantenkonfiguration deaktiviert war, gab es eine
+   Fehlermeldung nach dem Verbuchen von Einkaufsrechnungen, weil auf
+   die Maske zum Erfassen einer weiteren neuen Einkaufsrechnung
+   weitergeleitet wurde.
+ - Wiederkehrende Rechnung: beim automatischen Versand erzeugter
+   Rechnungen per E-Mail können nun auch Rechnungsattribute als
+   Variablen im Betreff & Text der E-Mails genutzt werden
+   (z.B. <%invnumber%> für die Rechnungsnummer oder
+   <%transaction_description%> für die Vorgangsbezeichnung).
+ - Wiederkehrende Rechnungen: die optionale Zusammenfassungs-E-Mail
+   enthält nun auch eine Auflistung von Rechnungsnummern, für die das
+   automatische Drucken oder der automatische Versand per E-Mail
+   fehlgeschlagen ist zusammen mit der jeweils aufgetretenen
+   Fehlermeldung.
+ - Wiederkehrende Rechnungen: für die Empfangsadresse der optionalen
+   Zusammenfassungs-E-Mail kann in der Konfiguration nun anstelle
+   eines Loginnamens auch eine E-Mail-Adresse verwendet werden. Es ist
+   nicht nötig, dass diese Adresse einem der Anwenderkonten zugeordnet
+   ist.
+-  Lieferdatum und Gültigkeitsdatum können optional auch nicht mehr gesetzt werden
+
+
+Bugfixes (Tracker: https://www.kivitendo.de/redmine):
+480 Lieferscheine mit kompletter Menge im Auftrag erstellt, fehlerhaft bei Option "Lieferschein Menge muss eingelagert sein"
+478 Offene Forderungsliste per E-Mail verschicken, die Auswahl-Haken werden ignoriert
+470 OrderController: Auf Lager falsche Tausenderberechnung
+469 Lieferschein erfassen und direkt drucken: JavaScript-Fehler
+462 Part-Picker Lupen-Dialog: Ergebnisse auf weiteren Seiten (bei Paginierung) lassen sich nicht auswählen
+453 Installationsspezifische Übersetzungen (more/all) besser in Entwicklungsprozess einbinden
+441 Dialogbuchen Konten entfernen
+432 Neuer Auftragskontroller ignoriert Artikel-Übersetzungen
+431 Doppelte Steuern mit neuer DB bei SKR04
+414 Fehler beim DATEV-Export: "Unausgeglichene Buchung" bei Rechnung mit 0,00
+408 Neuer Auftragskontroller: Drucken von odt-Vorlagen geht nur mit Standardvorlage
+399 Nach dem Anlegen von Mahnungen erfolgt keine Bestätigung
+375 Keine Wiedervorlage/Historie im neuen Auftragscontroller
+319 Einkaufspreise von Waren werden im Artikel-Bericht mit 0,00 angezeigt
+317 DATEV KNE-Export komplett entfernen
+302 MT940 Import, doppelte Datensätze besser abfangen
+287 Fehlerhafte Anzeige und Vergabe von Kundennummern beim CSV-Import von Kundendaten
+237 Beim CSV-Import von KundInnen findet bei den benutzerdefinierten Variablen keine Aktualisierung bestehender Einträge statt
+ 97 Benutzer löschen unter System->Benutzer funktioniert nicht
+
+
+2020-10-02 - Release 3.5.6.1
+
+
+Mittelgroße neue Features:
+
+ - USTVA: Konjunkturpaket erwarte Pos. 35 und Pos. 36 für Voranmeldung
+ - Währung und Wechselkurs können in der (neuen/experimentellen)
+   Angebots-/Auftrags-Maske angegeben werden. Der Wechselkurs wird hier
+   pro Beleg (und nicht pro Tag) gespeichert.
+ - individuelle Lieferadresse in der (neuen/experimentellen) Angebots-/
+   Auftrags-Maske
+
+Kleinere neue Features und Detailverbesserungen:
+
+ - Beim automatischen Auslagern über die Verkaufsrechnung kann zusätzlich
+   ein Auslagern über das Attribut Seriennummer entspricht Chargennummer
+   gemacht werden. Falls die Beleg-Seriennummer nicht auslagerbar ist wird
+   eine entsprechende Fehlermeldung generiert (einstellbar in der Mandanten-
+   konfiguration).
+ - Zahlungsbedingungen auch in Ek-Rechnung angeben können
+
+
+Bugfixes (Tracker: https://www.kivitendo.de/redmine):
+
+438 individuelle Lieferadresse gerät beim Speichern durcheinander
+358 segmentation fault in DBI.so beim versenden einer Rechnung per E-Mail
+365 Neuer Order Controller "Individuelle Lieferadresse fehlt"
+ 35 Zahlungsbedingungen bei Lieferanten nicht in EK-Rechnung
+
+
+2020-07-20 - Release 3.5.6
+
+
+Mittelgroße neue Features:
+
+ - komplette Überarbeitung der Standard-LaTeX-Druckvorlagen von PeiTeX
+   S.a.: templates/print/marei/Readme.md
+
+ - Erstellung von ZUGFeRD 2.0 fähigen PDFs
+ - Verarbeitung von ZUGFeRD 2.0 kompatiblen Eingangsrechnungen über
+   Kreditorenbuchungsvorlagen
+
+ - CSV-Import für Lieferscheine
+
+Kleinere neue Features und Detailverbesserungen:
+
+ - Suche nach Erzeugnissen über die dort verbauten Artikel
+ - neues Flag "natürliche Person" bei Kunden/Lieferanten welches z.B. in den
+   Druckvorlagen für eine Weiche für die Anrede verwendet werden kann.
+ - eigene Tabellen für Anrede von Kunden/Lieferanten und Titel und Abteilung
+   von Ansprechpersonen. Auswahl in Mandantenkonfiguration, ob in den Stammdaten
+   nur eine Auswahlliste angezeigt werden soll, oder wie bisher Freitext-Feld
+   und Auswahlliste. Anrede, Titel und Abteilung können im System-Menü bearbeitet
+   werden.
+ - Kompatibel mit Postgres Version 12 (keine Abhängigkeit von oids mehr)
+ - Leistungszeitraum (Periode) durchgängig in allen Buchungsmasken verfügbar und
+   im DATEV-Export als neues Feld vorhanden
+ - Automatische Kontenrahmen-Anpassungen für Konjunkturpaket des Bundes ab 1.7.2020
+ - die Einfüge-Position beim Hinzufügen von Artikeln in der neuen Angebots-/Auftragsmaske
+   (neuer Auftrags-Controller) kann angegeben werden
+
+Administrative Änderungen
+
+  - Die zwei Perl-Module "CAM::PDF" und "XML::LibXML" werden nun benötigt.
+
+Bugfixes (Tracker: https://www.kivitendo.de/redmine):
+
+436 Kontoauszug verbuchen fehlerhafter Rechnungsbetrag 16%/19% Mehrwertsteuer
+430 Steuer erfassen wirft SQL-Bind Fehler
+428 alte/falsche Tabellen in LaTex-Vorlagen, die package filecontents u. lxtable verwenden
+266 Kontenabgleich mit Bank ist nicht Transaktionssicher
+415 Inkompatibilitäten mit postgres 12
+418 Angebote/Aufträge (alte Maske)/Lieferscheine E-Mail ohne vorher speichern kaputt
+416 Tests datev
+411 Massenerstellen Rechnungen aus Lieferscheinen: Pflege-Commit verloren gegangen
+
+
+2019-12-11 - Release 3.5.5
+
+Mittelgroße neue Features:
+
+- In den Benutzereinstellungen kann ausgewählt werden, ob der Part-Picker in
+  der neuen Angebots-/Auftragsmaske (neuer Auftrags-Controller) auch nach
+  Kunden-Artikelnummern (Verkauf) und Lieferanten-Artikelnummern (Einkauf)
+  suchen soll. Ist dieses Feature eingeschaltet, so werden auch die Kunden-
+  bzw. Lieferanten-Artikelnummern als Spalte in den Positionen angezeigt.
+
+- Part Controller - neuer Tab mit Lagerinformationen - was ist wo gelagert
+
+- Neuer Workflow Lieferantenauftrag->Kreditorenbuchung: Für jedes Aufwandskonto
+  der Positionen im Lieferantenauftrag wird eine Zeile in der Kreditorenbuchung
+  erstellt. Gebucht wird standardmäßig auf das entsprechende Aufwandskonto. In
+  der Mandantenkonfiguration kann unter Standardkonten ein Konto ausgewählt
+  werden, auf das dann alle Zeilen gebucht werden.
+  Die Steuern werden übernommen, sofern diese für das ausgewählte Aufwandskonto
+  gültig sind. Ansonsten wird die Default-Steuer für das Aufwandskonto gesetzt.
+  Der Quellauftrag wird geschlossen, wenn der Betrag aller Kreditorenbuchungen,
+  die aus Workflows aus dem Quellauftrag entstanden sind, gleich dem Betrag
+  des Quellauftrags ist.
+
+- Der Jahresabschluß wurde komplett überarbeitet, es wird nun zwischen
+  Bestands- und Erfolgskonten unterschieden und ein Gewinn- bzw. Verlustvortrag
+  übertragen.
+
+Kleinere neue Features und Detailverbesserungen:
+
+- Mahnungen nach Abteilung filtern
+
+- Anzeige einer Kundenpreisliste in den Kundenstammdaten als Reiter.
+  Hier werden die Preisgruppenpreise angezeigt, falls einem Kunden eine
+  Preisgruppe zugeordnet ist.
+
+- In der neuen Angebots-/Auftragsmaske (neuer Auftrags-Controller) kann
+  ein Update-Knopf angezeigt werden, der die Positionen aus den
+  Artikelstammdaten aktualisiert (alle oder pro Position). Aktualisiert werden
+  Preis, Beschreibung und Langtext. Das Feature kann in den
+  Benutzereinstellungen eingeschaltet werden.
+
+- In der neuen Angebots-/Auftragsmaske (neuer Auftrags-Controller) ist die
+  Artikelnummer ein Link, der die Artikelstammdaten in einem neuen Tab öffnet.
+
+- Neuer Hintergrund-Job, der die Jahreszahl in Nummernkreisen jährlich hochsetzt
+  (Einstellung und Konfiguration s.a. Kapitel 2.7.5 Exemplarische Konf. Hintergrund-Job)
+
+- Weiterleitung zur Zielseite, wenn man ausgeloggt war und sich einloggt.
+  Falls z.B. der Timeout greift, man in der noch geöffneten kivi aber etwas
+  anklickt, so wird man zur Login-Seite weitergeleitet. Vorher landete man nach
+  dem login in einem solchen Fall auf der Startseite (Logo/Version/Todo-Liste).
+  Nun gelangt man zu der Seite, die man ursprünglich angeklickt hat (nur
+  POST-Requests).
+  Das kann z.B. auch dazu verwendet werden, jmd. einen Link in der kivi (z.B. zu
+  einem Auftrag) zu schicken. Wenn derjenige nicht eingeloggt ist, gelangt er
+  nach dem Login dennoch auf die Zielseite.
+
+Bugfixes (Tracker: https://www.kivitendo.de/redmine):
+
+407 Test ./t/db_helper/with_transaction.t läuft nicht durch; Rose-Fehlermeldung nur "generic exception"
+406 abzurechnender (Netto-)Betrag bei Aufträgen rechnet falsch wenn Rechnungs-Gutschriften vorhanden sind
+379 Einkauf Lieferanten-Artikelnummer in zweiter (erster) Spalte anzeigen
+377 PartPicker-Suche im Einkauf um Hersteller-Artikelnummer erweitern
+
+
+2019-08-07 - Release 3.5.4
+
+
+Mittelgroße neue Features:
+
+- Anzeigename (Picker-Ergenisse) konfigurierbar gemacht
+  Im Moment können Kunden-, Lieferanten und Waren-Anzeige konfiguriert werden.
+  Dies kann mandantenweit in der Mandantenkonfiguration passieren und vom
+  Benutzer in den Benutzereinstellungen überschrieben werden.
+  Konfiguriert wird intern "displayable_name". Es kann sein, dass dieser auch
+  an anderen Stellen außer Picker-Ergebnissen verwendet wird.
+
+Kleinere neue Features und Detailverbesserungen:
+
+- Memory-Limits für FCGI-Prozesse werden nun auch vom Task-Server berücksichtigt.
+  Zu beachten ist, dass für einen Neustart des Task-Servers gesorgt werden muss.
+  Ist der Task-Server als systemd-Service eingerichtet, geschieht dies automatisch.
+- Bearbeiter der Mahnungen konfigurierbar gemacht (#345)
+  Entsprechend wird beim Mahnungen erzeugen auch der E-Mail-Absender inkl. Signatur gesetzt
+- Kundenstammdaten um Feld Herkunft personenbezogener Daten erweitert
+  Entsprechend der DSGVO kann hier der Erstkontakt mit dem Kunden protokolliert werden (Messe, etc)
+  Das Feld wird beim Bericht mitexportiert
+- Kundenstammdaten um Feld E-Mail Rechnungsempfänger erweitert
+  Viele Kunden besitzen für den Rechnungseingang eine generische E-Mail-Adresse, die nicht
+  mit der allgemeinen E-Mail-Adresse identisch ist. Falls dieses Feld gesetzt ist, so hat dieser
+  Wert beim manuellen E-Mail Versand der Rechnung Priorität (mandantenweit konfigurierbar).
+  Für die wiederkehrende Rechnung wird diese E-Mail-Adresse zusätzlich gesetzt.
+  In den entsprechenden vorgelagerten Masken, wird dies auch visuell angezeigt (nicht bei alter Auftragsmaske!).
+- Kundenstammdaten um Feld "Herkunft der personenbezogenen Daten" erweitert
+  Um Details zum Erstkontakt des Kunden zu erfassen.
+- Kundenstammdaten um Feld Amtsgericht erweitert
+  Falls das Feld Steuernummer mit dem Wert der Hr-Nr gefüllt wurde, wird auch das zuständige
+  Registierungs-Gericht benötigt.
+- Ansprechpartner um Feld 'Hauptansprechpartner' erweitert und exportierbar im Kundenbericht gemacht
+
+- Verkauf-Rechnungsbericht -> Nicht per E-Mail verschickte Belege anzeigen lassen
+
+- Vorauswahl bei Dateianhängen für den E-Mail-Versand von Belegen konfigurierbar gemacht (Standardmäßig angehakt)
+
+- Verbuchte Kontoauszüge können wieder rückgängig gemacht werden (Neuverbuchen ist möglich)
+
+- Verbuchen von Kontoauszügen, es können jetzt teilweise Verbuchungen gemacht werden (Belege werden nicht mehr überbucht)
+
+- Dialogbuchungen aus Kontoauszugs-Import erstellen, der Verwendungszweck wird in die Beschreibung übernommen
+
+- ungenutzte Spalte "ranking" aus Tabelle "payment_terms" entfernt
+
+
+Bugfixes (Tracker: https://www.kivitendo.de/redmine):
+
+378 Lieferantenauftrag Darstellung für Besteller optimieren
+376 Aktuelle unstable kann keine kivitendo_auth Datenbank anlegen
+371 Benutzerdefinierte Variablen nicht im Bericht Projekt enthalten, kann nicht danach gefiltert werden.
+367 Kontoauszugsimport: 2 Konten bei einer Bank. Bankgebührenbuchung wird als schon importiert makiert
+366 Zahlungsverkehr->Zahlungs-(ein/aus)gang. Bezahlen/Abgleichen mehrerer Rechnungen geht nicht mehr wie vorher.
+345 Bearbeiter bei Mahnungen
+215 Kunden als csv exportieren
+
+
+2019-01-03 - Release 3.5.3
+
+Mittelgroße neue Features:
+
+- Inventurerfassung
+
+  Portierung aus einem Kundenprojekt mit folgenden Anforderungen/Features:
+  - eigene Maske unter Lager->Inventur
+  - Anzeige des aktuellen Lagerbestands des zu erfassenden Artikels
+  - Angabe des Artikels auch über EAN
+  - Angabe eines Stichtages
+  - Vorbelegung Lager/Lagerplatz und Stichtag in Mandantenkonfiguration
+  - korrigieren des Lagerbestands entsprechend der Zählung (mit neuem
+    Transfertyp "Inventur")
+  - Speichern der gezählten Menge (auch wenn keine Korrektur des Bestands
+    stattfindet)
+  - Warnung, wenn gleicher Artikel für gleichen Lagerplatz und Stichtag schon
+    gezählt wurde und Möglichkeit die eigene gezählte Menge zu der vorhandenen
+    hinzuzuzählen oder die vorher gezählte Menge durch die eigene Menge zu
+    korrigieren
+  - Historie der Inventurerfassung des aktuellen Bearbeiters unterhalb
+    der Erfassungsmaske
+  - Bericht über Inventurerfassungen
+
+Kleinere neue Features und Detailverbesserungen:
+  - Verknüpfte Belege um die Verknüpfung von Beleg nach E-Mail-Journal erweitert.
+  - Filter nach Abteilungen für Lieferplan
+  - Eindeutigkeit bei Rechnungsnummern von Kreditoren. (Es erfolgt eine  Warnung bei Duplikaten (Überprüfung auf Lieferant mit Rechnungsnummer))
+  - Mit dem SEPA-Export verknüpfte Kreditorenbelege (Einkaufsrechnung oder Kreditorenbuchungen) können nicht mehr gelöscht oder storniert werden.
+  - Tab "Belege" beim Kunden und Lieferanten. Offene Rechnungen, Aufträge
+    werden angezeigt. Eine Umsatz und Mahnstatistik können in Tabs geöffnet
+    werden. Belegtypen Angebote, Aufträge (Kunden), bzw Preisanfragen,
+    Lieferantenaufträge (Lieferanten) sowie die Belege Rechnungen, Emails,
+    Briefe können in weiteren Tabs angezeigt werden.
+  - SEPA-Export. Überweisungsdatum vorbelegen, entweder die Fälligkeit oder falls vorhanden das Skonto-Datum.
+    Das Skonto-Datum hat Priorität vor der letzten Fälligkeit.
+    Zusätzlich kann ein Puffer in Tagen vom Zahlungsziel abgezogen werden (Standard 0). Das Verhalten muss für jeden
+    Mandanten unter Mandantenkonfiguration -> Features -> SEPA aktiv eingeschaltet werden.
+  - Schnellsuchen können auf Benutzerebene Programm->Benutzereinstellungen->Anzeigeoptionen konfiguriert werden.
+  - SelfTests erweitert: Warnungen bei sehr laxer Buchungskonfiguration und bei verwaisten abgeglichen Bank-Transaktionen
+  - DATEV-Export-Format konfigurierbar
+    In der Mandantenkonfiguration befindet sich jetzt eine Einstellung, welche die Kodierung des DATEV-Exports steuert.
+    DATEV erwartet CP1252. Kivitendo kann diese Kodierung so vom kivitendo Nutzer einfordern, alternativ nicht
+    vorhandenen Zeichen versuchen zu ersetzen oder die DATEV-Erwartung ignorieren und UTF-8 liefern.
+    Voreingestellt ist CP1252 mit Ersetzungen.
+
+Bugfixes (Tracker: https://www.kivitendo.de/redmine):
+
+361 Ware erfassen nicht möglich im leeren Mandanten
+359 get_payment_select_options_for_bank_transaction vereinfachen
+358 segmentation fault in DBI.so beim versenden einer Rechnung per E-Mail
+357 Die Generierung einer periodische Rechnung mit der Periode einmalig sollte auch den Quell-Auftrag schliessen.
+356 Bei Zuweisung von zwei Kreditorengutschrift per Kontoauszug verbuchen, wird bei der zweiten Zuweisung das Vorzeichen gedreht
+355 Kontoauszug verbuchen -> Eine Bankbewegung mit zwei Skonto Rechnungen verknüpfen geht nicht
+354 Zahlungsbedingung falsch bei Verkausf-Lieferschein nach Rechnung
+353 Preisregeln Wenn Artikel gelöscht wird
+352 Beim Drucken mehrerer Rechnung aus dem Bericht heraus wird der Rabatt falsch berechnet
+351 Order-Controller: Angebot als neu speichern erzeugt kein neues Objekt
+350 Berichte->Projektbuchungen wirft Fehler bei ausgwählten Project
+349 Normalisierung Artikelbeschreibung und Artikellangtext (Bemerkung) funkioniert nicht mehr
+348 DatevExport kommt mit bestimmten Zeichen im Buchungstext nicht klar
+347 Dateimanagement -> Erzeugte Dokumente löschen -> Nein funktioniert nicht
+344 Internal Server Error (fallback Module fehlen)
+343 Kontoauszug verbuchen Skontoautomatik bei Verkaufsrechnungen defekt
+341 Auftrag: Warnung bei aktiven wiederkehrenden Rechnungen geht nicht mehr
+337 Standardlagerplatz bei Erzeugnissen ändern wirft Presenter-Fehler
+336 Beim Drucken mehrerer Rechnung aus dem Bericht heraus mit aktiviertem DMS bricht mit Fehlermeldung ab
+335 Fehler bei den Verknüpften Belegen wenn Verknüpfungsziel Pflichtenheft
+334 Sortierung Artikelstammdaten/Preisinformationen/Verkaufspreisinformation: Kundenauftrag
+333 Bericht Lagerentnahme: Lagerplatz lässt sich nicht auswählen
+332 Bug: Bericht Lagerbestand gibt Fehler beim einschliessen leerer Lagerplätze
+329 Konto mit identischem Folgekonto führt zu Endlosschlaufe
+323 Kontoauszug verbuchen. Kombination von Rechnungen und Gutschriften nicht möglich
+316 Verknüpfte Belege erlaubt keine manuelle Verknüpfung mit Kreditorenbuchungen
+315 EAN-Feld beim Einlagern ohne Funktion
+311 Task-Server-Start beim Booten
+306 unstable: Leerzeichen beim CKEditor im Pflichtenheft
+301 SelfTest Transactions - all_passed nicht gesetzt
+292 Verkaufsbericht filtert nicht mehr nach Warengruppe
+282 Artikelliste leer, wenn in der Schnellsuche mehrere Treffer vorgeschlagen und keiner ausgewählt ist.
+281 Falsche Lagerbewegungen beim Erstellen von Erzeugnissen, wenn Bestandteile vorhanden/nicht vorhanden
+279 Datenmodell der verknüpften Belege um E-Mail Verknüpfung erweitern
+265 Kontoauszug verbuchen bei negativer Kreditorenbuchung wird das Vorzeichen bei Zahlung umgedreht
+233 Memory-Bedarf des Taskservers steigt kontinuierlich an
+151 Berichte->Pflichtenheft Fehler ab commit #c44615e
+125 Neues Datevexportformat
+90 Benutzerdefinierte Variablen von Kunden werden bei Lieferanten mit gleicher id angezeigt
+86 Kunden bzw Lieferantenliste wird von anderem Mandanten angezeigt
+82 Berechnete Preiswerte von PTC weichen von oberflächen Werten aus den Masken ab
+28 Fehler beim Hinzufügen von Artikeln zu Erzeugnissen per Artikelauswahlseite
+22 Doppelte Minuse ( --) im Kundennamen erzeugen "leider" immer check_name problem bei freitext Auswahl
+
+2017-12-12 - Release 3.5.1
+
+Größere neue Features:
+
+- WebshopApi
+
+  WebshopApi mit bisher einem Konnektor für Shopware.
+  Damit ist es möglich Bestellungen aus dem Shop abzuholen und Artikel
+  abzugleichn.
+
+Mittelgroße neue Features:
+
+- DATEV-Export überarbeitet
+
+  - Um Strukturtyp CSV-Export erweitert
+  - DATEV-Export: Kostenstellen (Kost1 und Kost2) vorbelegen
+  - DATEV-Export: Buchungssätze nach Abteilung filtern
+  - DATEV-Export: Buchungen für einen bestimmten Zeitraum ab einem
+    Buchungsdatum filtern. Z.B. wenn man einen DATEV-Export für Januar schon
+    exportiert hat, und im Juni noch ein Buchung für Januar nachbucht, kann man
+    mit "Erfassungsdatum Von: 01.06.2017" nur diese eine Buchung aus Januar
+    exportieren.
+  - Überlagerung mit Kunden- Lieferantennummer als Personenkonto möglich
+    Anstatt des Sammelkontos kann die Kunden- oder Lieferantennummer aus
+    den Stammdaten genommen werden, falls der Nummernkreis der DATEV-Konform
+    für Personenkonten entspricht.
+  - Belegfeld 2 wird nicht mehr gesetzt
+  - Buchungsbeschreibung wird nicht mehr gesetzt
+
+Kleinere neue Features und Detailverbesserungen:
+
+  - Abteilungs-Auswahl konsequent alphabetisch sortieren
+  - Buchungsvorlagen schneller über den Namen filtern (suchen).
+  - Neues Recht Erzeugnisse unabhängig vom Status editieren (default 0)
+  - SEPA-XML: alle Sonderzeichen filtern
+  - SEPA-Export: Export wieder rückgängig machen, falls noch Status offen
+  - Stammdaten -> Berichte -> Artikel: Standardlager und Lagerplatz optional anzeigen
+  - Vorbelegte Texte inkl. Ansprechpartner für den E-Mail-Versand bei allen Workflows
+    hinzugefügt.
+  - Kontoauszug verbuchen -> Buchung erstellen um Dialogbuchungen erweitert.
+    Vom Kontoimport ist es jetzt auch möglich in Vorlagen aus der Dialog-
+    Buchungsmaske zu buchen und nicht nur in Kreditorenbuchungsvorlagen
+  - Neuer Bericht: Berichtskonfigurationsübersicht
+  - Verbesserte Datumsvalidierung per Javascript
+  - Neues Benutzer-Recht Erzeugnisbestandteile editieren
+  - Dialogbuchungsvorlagen um 'Details anzeigen' erweitert
+  - Nach dem Speichern Buchungsnummer bei Debitoren/Kreditorenbuchungen und Einkaufsrechnungen
+    als Info anzeigen
+  - Bankimport: CSV- und MT940-Menüpunkte in Untermenü in »Zahlungsverkehr«
+  - Schnellsuchen - ungültige Einträge aus Stammdaten nicht mit anzeigen
+  - Workflow Lieferschein -> Rechnung. Liefertermin als Rechnungslieferdatum setzen
+  - Einkaufsrechnungen: Bearbeiter*in & Verkäufer*in mit aktueller Benutzer*in vorbelegen
+  - Bemerkungsfeldeditor (CKEditor): Größe änderbar und Buttonzeile "schwebt"
+    über dem Eingabebereich wenn im Fokus
+  - Kontenabgleich mit Bank. Hinweise auf Fehler bei nicht vorhandenem 'Abgleichen'-Knopf
+
+Administrative Änderungen
+
+  - Für die Tests müssen in der kivitendo.conf unter [testing/database]
+    Einträge für superuser_user und superuser_password gesetzt werden. Siehe
+    Beispiel in config/kivitendo.conf.default
+
+Bugfixes:
+- Bugfix #326 Das Löschen von Storno Rechnungen R(S) wirft einen SQL-Fehler
+- Bugfix #325 Rechnungen mit Zahlungsverknüpfungen können storniert werden
+- Bugfix #324 DATEV CSV-Export ggf. fehlerhaft bei Buchungen ohne Steuer
+- Bugfix #320 Stücklistenpositionen werden nicht mehr ausgedruckt
+- Bugfix #305 Kein customerpicker im Formular Rechnung erfassen
+- Bugfix #304 Datumsformat wechselt willkürlich auf Datenbank Format "YYYY-MM-DD"
+- Bugfix #303 Zahlungserinnerung PDF anhängen in E-Mail funktioniert nicht mehr
+- Bugfix #300 Kontoauszug verbuchen bei negativer Einkaufsrechnung wird das Vorzeichen bei Zahlung umgedreht
+- Bugfix #296 Verkauf -> Lieferschein erfassen erzeugt Fehlermeldung
+- Bugfix #286 DMS aktiv. Speichertyp für Belege auf 'kein' gesetzt -> Belegdruck defekt
+- Bugfix #283 Lieferwertbericht wirft Fehler
+- Bugfix #280 Drucken beim neuem OrderController geht nicht mit aktiviertem Dateimanagement und Webdav
+- Bugfix #277 Kontoauszug verbuchen. Vorschlagsliste ignoriert SEPA-Überweisungen
+- Bugfix #276 Mini-DMS Auswahl der Belege bei Lieferschein fehlt
+- Bugfix #275 Löschen von DMS-Anhängen wirft Fehler
+- Bugfix #274 Mahnungen lassen sich nicht mehr erzeugen mit aktivierten Dateimanagementfeature
+- Bugfix #270 Artikelzuweisung bzw. Zusätzliche Artikel im Pflichtenheft kaputt
+- Bugfix #265 Kontoauszug verbuchen bei negativer Kreditorenbuchung wird das Vorzeichen bei Zahlung umgedreht
+- Bugfix #264 Artikelnummer nicht mehr änderbar
+- Bugfix #263 Emailadresse der Stammdaten wird nicht mehr übernommen
+- Bugfix #8   Datumswarnung in Safari blockiert Browser
+
+
+
+2017-07-17 - Release 3.5.0
+
+große Features:
+
+- Dateiverwaltung (Mini-DMS)
+
+  parallel zum alten WebDAV gibt es nun eine Datei-Management Lösung, die
+  über eine Speichermedium unabhängige Zwischenschicht die Dateien in der
+  Datenbank verwaltet. Darunter können verschiedene Backends existieren.
+  Aktuell ist dies eine Filesystem-Struktur.
+
+  Modular können weitere Backends eingebunden werden. In Arbeit ist
+  ein Backend, das auf die alte WebDAV-Struktur zugreift.
+
+  Es gibt unterschiedliche Typen von Dateien, jedem Typ läßt sich in der
+  Mandantenkonfigurierung auf ein bestimmtes Backend zuordnen.
+
+  Aktuell gibt es die Dateitypen
+  - "documents", das sind entweder generierte, eingescannte oder hochgeladene PDF-Dateien
+  - "attachments", zusätzlich hochgeladene Dokumente, die an bestimmte ERP-Objekte angehängt werden
+  - "images", hochgeladene Bilder zu Artikeln
+
+  Daneben gibt es Dateiquellen
+  - "created" , vom System erzeugte Dokumente
+  - "uploaded", hochgeladene Dokumente
+  - "scanner1,scanner2" , von einem oder mehreren Scannern erzeugte Dateien
+  - "email",  vom Mailsystem empfangene Dateien
+
+- Artikel-Klassifizierung
+
+  Die Klassifizierung von Artikeln dient einer weiteren Gliederung um
+  zum Beispiel den Einkauf vom Verkauf zu trennen, etc.
+
+  Gekennzeichnet durch eine Beschreibung (z.B. "Einkauf") und ein Kürzel (z.B. "E")
+  Flexibel änderbar und erweiterbar.
+
+  Der Typ des Artikels und die Klassifizierung werden durch zwei
+  Buchstaben dargestellt.  Der erste Buchstabe ist eine Lokalisierung
+  des Typs des Artikels ('P','A','S') , deutsch 'W', 'E', und 'D' für
+  Ware, Erzeugnis oder Dienstleistung, ggf. weitere Typen.  Der zweite
+  Buchstabe ist eine Lokalisierung der Klassifizierungsabkürzung
+  (abbreviation).
+
+  Wenn im ERP-Dokument nach einer Artikelnummer oder Beschreibung
+  gesucht wird, diese in den Stammdaten vorhanden ist, aber der
+  Artikeltyp falsch ist, wird die Fehlermeldung "Gesuchter Artikel ist
+  nicht für den Einkauf bzw Verkauf" gemeldet.
+
+  Anpassung des CSV Imports, nun wird alternativ zur 'part_type'-Spalte
+  die 'pclass'-Spalte mit zwei bis drei Buchstaben geparsed und entsprechend
+  classification_id und part_type gesetzt.
+
+- Option "Preis separat ausweisen" als neue Artikel-Klassifizierung
+
+  Die Option ist unter Artikelklassifikation editierbar.  In Aufträgen
+  und Rechnungen werden die Zwischensummen dem Drucksystem zur
+  Verfügung gestellt. Die verwendbaren Variablen sind:
+
+  -  <%separate_XX_subtotal%>  wobei XX die Abkürzung der Klassifikation ist.
+  -  <%non_separate_subtotal%> der Rest der Positionen.
+
+  Hintergrund:
+     Preise von Artikeln wie "Verpackung" oder "Transport" müssen
+     oftmals separat ausgewiesen werden, genau so wie der reine Warenwert.
+
+- GoBD Export
+  Man kann nun einen IDEA-kompatiblen Export für Steuerprüfer exportieren.
+
+- ActionBar
+  Die Workflow-Knöpfe wurden nun in eine ActionBar-Zeile am oberen Fensterrand
+  migriert, die immer sichtbar ist, auch wenn man nach unten scrollt.
+
+- Jahresabschlußbuchungen (EB/SB)
+  Mit Saldovortrag auf die 9000er-Konten
+
+- Belegvorlagen und Entwürfe
+
+  Der bisherige Mechanismus der Entwürfe in Rechnungsbelegen, der
+  ursprünglich nur zum Zwischenspeichern gedacht war, wurde in zwei
+  Mechanismen aufgeteilt: Entwürfe und Belegvorlagen.
+
+  Die neuen Entwürfe sind nur noch zur Zwischenspeicherung
+  gedacht. Sie sind nur für die Person sichtbar, die den Entwurf
+  angelegt hat. Auch werden sie bei Abmeldung automatisch entfernt.
+
+  Die neuen Belegvorlagen hingegen sind dazu gedacht,
+  z.B. wiederkehrende Zahlungen schnell verbuchen zu können. Sie sind
+  für alle Personen sichtbar und dauerhaft vorhanden.
+
+  Beide Mechanismen sind über den »Mehr«-Button in den Belegmasken
+  erreichbar.
+
+  Weiterhin wurden diese Mechanismen so umgebaut, dass sie nun auch
+  updatesicher sind.
+
+kleinere neue Features und Detailverbesserungen:
+
+  - experimentelle Auftragsmaske als Controller ist in der Mandantenkonfiguration
+    unter "Experimentelle Features" abschaltbar
+
+  - Wiederkehrende Rechnungen können mit der Periode 'einmalig' konfiguriert werden
+
+  - Druckvorlagen Mahnungen: Bearbeiter und Verkäufer-Metadaten auch im Ausdruck zu Verfügung stellen
+
+  - PDF-Erzeugen mit Leerseiten für zweiseitiges Drucken (Installation siehe auch UPGRADE Datei)
+
+  - SEPA Überweisungen zusätzlich Kunden- oder Lieferantennummer im Verwendungszweck vorbelegen
+
+  - Dialogbuchen um WebDAV-Funktion erweitert
+
+  - Kreditorenbuchung um WebDAV-Funktion erweitert
+
+  - Verfeinerung der Rechte für Finanzbuchhaltung: Es können nun für Dialogbuchungen,
+    Debitoren- und Kreditorenbuchungen extra Rechte vergeben werden
+
+  - Weiterer Bericht in der Rubrik Lager: Lagerentnahme
+    Gibt eine Statistik über Lagerbewegungen, pro Monat/Quartal/Jahr.
+
+  - Für UStVA Voranmeldung über Elster gibt es die Anbindung über Geierlein (Installation/Config siehe Commit)
+
+  - CSV-Import von Artikel hat nun für existierende Artikel folgende Optionen:
+     1. Eigenschaften von existierenden Einträgen aktualisieren
+     2. Eigenschaften von existierenden Artikeln aktualisieren / Nicht vorhandene überspringen
+     3. Preise von vorhandenen Artikeln aktualisieren
+     4. Preise von vorhandenen Artikel aktualisieren / Nicht vorhandene überspringen
+     5. Mit neuer Artikelnummer einfügen
+     6. Eintrag überspringen
+    Zusätzlich können nun Spalten "Lager","Lagerort" als Name oder ID eingelesen werden,
+    sowie Übersetzungen z.B. als 'description_EN' oder 'description_IT'.
+    Auch cvars können als 'cvars_<name>' importiert werden.
+    Ebenfalls sind zusätzliche Bemerkungen an den einzelnen Importzeilen eingebaut.
+
+  - In der Lager-Mandantenkonfig gibt es das Feature "Zum Fertigen Standardlager des Bestandteils verwenden".
+    Statt das Ziellager des Erzeugnisses zu Verwenden, wird nun zur Prüfung der Fertigung das
+    Standardlager der einzelnen Bestandteile verwendet.
+    Hat das Bestandteil kein Standardlager, so wird das "Standard-Lager für Auslagern ohne Prüfung auf Bestand"
+    verwendet und ohne Prüfung ausgelagert. Ist dieses nicht gesetzt, wird eine Fehlermeldung erzeugt.
+
+  - Neues Recht "Verknüpfte Belege", standardmäßig erlaubt. Betrifft alle
+    Belege und die Projektstammdaten
+
+  - Briefe sind jetzt auch für Lieferanten verfügbar. Die neuen Rechte dafür
+    sind für Gruppen vergeben, die auch Einkaufsbelege bearbeiten dürfen.
+
+  - Neuer Controller für Preisgruppen, die nun sortiert und ungültig gesetzt
+    werden können.
+
+  - Neuer Bericht "Auftragsartikelsuche", um schnell Auftragspositionen aus
+    Verkaufsaufträgen finden zu können:
+    Verkauf -> Berichte -> Auftragsartikelsuche
+
+  - Part-Controller - neue Maske um Artikel anzulegen / zu bearbeiten
+    Umgestellt auf Controller, dadurch kein "Erneuern mehr". Die Bearbeitung
+    von Erzeugnisbestandteilen hat sich dadurch verändert, dies geschieht nun
+    in einem eigenen Tab.
+
+  - Neuer Artikeltyp "Sortiment" (experimentell)
+    Einem Sortiment können wie einem Erzeugnis mehrere Artikel zugeordnet
+    werden. Beim Hinzufügen eines Sortiments zu einem Beleg werden alle
+    Bestandteile des Sortiments als Einzelteile zum Beleg hinzugefügt, so als
+    ob man das manuell gemacht hätte. Der Sortimentsartikel wird ohne Preis
+    hinzugefügt und fungiert als Überschrift, und kann sogar gelöscht werden.
+    Nach dem Hinzufügen können die Einzelbestandteile auch gelöscht oder
+    verändert werden. Dadurch hat das Sortiment auch keinen festen Preis,
+    sondern der Preis im Beleg richtet sich nach dem Preis der
+    Einzelbestandteile, die je nach Kunde z.B. durch Preisgruppenpreise
+    variieren können.
+
+    Das Sortiment eignet sich z.B. als Definition von Gruppierungen von
+    Artikeln die häufig zusammen gekauft werden, z.B. ein Artikel in 10
+    Farbvariationen.
+
+    Einschränkungen: das "Auspacken" eines Sortiments beim Hinzufügen in einem
+    Beleg funktioniert derzeit nur beim neuen Auftragscontroller.
+    Auftragscontroller und Sortiment haben beide noch den Status experimentell.
+
+  - Detailverbesserung Druckvorlage RB
+    Adressfeld um Absender ergänzt. Firmenname nicht mehr aus Titlebar, sondern
+    aus der Mandantenkonfiguration nehmen. Tabelle etwas breiter gesetzt.
+    CHF als weitere Standardwährung hinzugefügt. Stempel und Unterschrift für
+    Angebot hinzugefügt.
+
+  - Projekt: unter "verknüpfte Belege" auch Belege anzeigen, wo
+    Einzelpositionen mit dem Projekt verknüpft sind, nicht nur der Beleg
+    (globalproject_id)
+
+  - Abteilungsfilter in mehr Berichten eingefügt
+
+  - Finanzübersicht: Neue Spalte »Kosten« analog zu BWA-Kosten
+
+  - Kontennachweis in den Berichten BWA und GuV/EÜR. Die Hartkodierung der
+    Kategorienamen für BWA und GuV/EÜR im Code wurde in die Datenbank verlagert.
+
+Administrative Änderungen
+
+  - Entwickler benötigen neu die zwei Perl-Module "Sys::CPU" und
+    "Thread::Pool::Simple".
+
+Bugfixes:
+
+- Bugfix #273 "Bei Schweizer Kontenplänen erscheint beim Aufruf der Maske zum Dialogbuchen eine Fehlermeldung"
+- Bugfix #268 "Schnellsuchfelder und ""Benutzer-Mandant-Abmelden""-Header wird abgeschnitten, wenn nicht alles auf einer Zeile Platz hat"
+- Bugfix #262 department in oe.pl (Angebot/Auftrag) wird nicht mehr an die Druckvorlage übergeben
+- Bugfix #258 Falscher Bearbeiter (und beim Verkauf Verkäufer) beim erstellen von neuen Verkaufs- oder Einkaufsbelegen
+- Bugfix #257 Darstellungsfehler bei Mail von Taskserverjob FailedBackgroundJobsReport
+- Bugfix #256 "Taskserver: Job bearbeiten ? ""Speichern und Ausführen"" erstellt zusätzlichen Job; diverse kleinere Probleme beim Tasklserver"
+- Bugfix #255 "Beim Erfassen von Erzeugnissen fehlt beim Hinzufügen von mehreren Artikeln die Artikelbeschreibung, der Button ""erfassen"" erzeugt eine Fehlermeldung, Eingabe von Untereinheiten wird ignoriert"
+- Bugfix #252 Hochladen von Dateianhängen gibt Fehlermeldung
+- Bugfix #250 Artikel, Dienstleistungen usw. werden nicht mehr angelegt wenn die Nummer schon in einem anderen Nummernkreis vergeben ist (das ist neu)
+- Bugfix #249 "Drucken von Rechnungen aus Liste ""Rechnungen, Gutschriften & Debitorenbuchungen"" geht nicht mit dem Dateimanagement"
+- Bugfix #245 Workflow Verkaufsrechnung -> Verkaufsauftrag fehlende Rechte
+- Bugfix #243 Kontoauszug verbuchen: Nach dem Buchen in Belegen (Vorlagen) wird beim Rücksprung die aktuelle Ansicht nicht erneuert
+- Bugfix #242 Kontoauszug verbuchen bei negativer Verkaufsrechnung dreht das Vorzeichen bei Zahlung um
+- Bugfix #238 PDFs werden nicht mehr bei wiederkehrenden Rechnung per E-Mail erzeugt/angehangen
+- Bugfix #235 Neuerfassen von Ware. Fehlermeldung, wenn man zuerst den Lieferanten eingibt
+- Bugfix #218 Benachrichtigung automatisch erstellter Rechnungen fehlerhaft
+- Bugfix #211 Sortieren und speichern commit #26dfef7da64e9712db7
+- Bugfix #208 Taskserver erzeugt immer neue session Einträge
+- Bugfix #207 Projekt in Kreditorenbuchung und Debitorenbuchung
+- Bugfix #204 Automatisches Auslagern beim Rechnung schreiben funktioniert mit Commit 8c1d5d nicht mehr
+- Bugfix #200 Rabatt mit Nachkommastellen wird abgeschnitten, beim Workflow Lieferschein -> Rechnung
+- Bugfix #194 Fehler »load_draft not defined in locale/de/all« beim Entwurfladen aus Bankauszug verbuchen
+- Bugfix #192 »Kontoauszug verbuchen« kommt mit multipler Zuweisung nicht zurecht
+- Bugfix #191 »Kontoauszug verbuchen« nutzt keine Datenbanktransaktionen
+- Bugfix #181 Storno-Rechnung als neu verwenden
+- Bugfix #180 Hänger / Verklemmung bei Benutzung von Rose und standard_dbh
+- Bugfix #164 Prüfung der Bücherkontrolle in Zahlungseingängen und Zahlungsausgängen fehlerhaft
+- Bugfix #156 Beim Erstellen einer Rechnung aus einem Lieferantenlieferschein gibt es bei manchen Lieferscheinen eine Fehlermeldung
+- Bugfix #99 Rabatt wird falsch geparsed/formatiert beim Workflow Auftrag->Angebot, Auftrag->Rechnung, Angebot->Rechnung
+
+  - Pflichtenheftmodul: Es wurde eine Fehlermeldung angezeigt, wenn im
+    rechten Teil des Fensters aktuell Textblöcke zu sehen sind,
+    während Abschnitte oder Funktionsblöcke via Drag & Drop verschoben
+    wurden.
+
+  - Das Fälligkeitsdatum wurde beim Buchen von Einkaufs- und
+    Verkaufsrechnung, bei denen eine Zahlungsbedingung ohne
+    automatische Berechnung ausgewählt war, immer auf das
+    Rechnungsdatum gesetzt, anstelle den eingetragenen Wert zu nutzen.
+
+  - Debitoren- und Kreditorenbuchungen in Fremdwährung öffnen
+
+2016-07-05 - Release 3.4.1
+
+kleinere neue Features und Detailverbesserungen:
+
+  - Erweitern der Zahlungsbedingungen um unterschiedliche Texte für
+    Angebote/Aufträge auf der einen Seite und Rechnungen auf der
+    anderen Seite.
+  - Auftrag um Druckvariante Gelangensbestätigung erweitert.
+  - Lagereingangs-Typ 'gefertigt' hinzugefügt.
+  - Fertigungsdatum von Erzeugnissen und Bestandteilen von Erzeugnissen
+    in Lagerbuchungen mitprotokollieren (Tagesdatum der Fertigung).
+  - Wiederkehrende Rechnungen können nun automatisch per E-Mail
+    verschickt werden.
+  - Die meisten Suchmasken ignorieren nun bei Teilwortsuchen führende
+    und anhängende Leerzeichen.
+  - Abteilung in Verkauf->Berichte-Rechnungen anzeigen lassen.
+  - Customer-Picker beim Projekt erzeugen eingebaut.
+  - Es gibt jetzt Schnellsuchen zu den meisten Belegen und Stammdaten in der
+    Leiste am oberen Rand, konfigurierbar in der Mandantenkonfiguration (Details s.u.)
+  - Wird in der Konfigurations-Datei (kivitendo.conf)
+    [mail_delivery]/method auf einen leeren Wert gesetzt wird jetzt der
+    Mailversand komplett ausgeschaltet, vorher wurde bei
+    Werten die nicht 'sendmail' oder 'smtp' sind SMTP als Vorauswahl benutzt.
+  - Änderungen des Verkaufspreises von Artikeln, die Bestandteile von
+    Erzeugnissen sind, haben nun keine Auswirkungen auf die
+    Verkaufspreise der Erzeugnisse mehr.
+  - Beim Kontoauszug verbuchen wird in der Übersicht der offene Rechnungsbetrag
+    als visuelle Hilfe angezeigt und zusätzlich wird nur der maximal offene
+    Rechnungsbetrag zugewiesen.
+  - Im CSV Import ist es jetzt möglich die Spalten aus der hochgeladenen Datei
+    den erwarteten Spalten zuzuordnen. Diese Zuordnung kann im Profil
+    gespeichert werden.
+  - Preishistorie der Stammdaten-Preise mitprotokollieren. Eine Übersicht
+    der Verkaufspreis-Änderungen wird zusätzlich in einem Reiter in den Stammdaten
+    angezeigt.
+  - Die nur rudimentär vorhandene Funktion: "Automatisches Erzeugen von
+    Aufträgen" (create_backorders) wurde entfernt.
+
+Schnellsuche in Bereichen:
+  - Für alle Belegtypen sowie die drei Waren-Typen gibt es Ein- und Abschaltbare
+    Schnellsuchen im Header-Bereich von kivitendo. Die Funktionsweise ist identisch
+    mit den bereits bekannten Schnellsuchen nach FiBu-Belegen und Ansprechpartnern.
+
+Brieffunktion:
+  - Beim Speichern und Drucken von Briefen werden diese im
+    WebDAV-Verzeichnis gespeichert, sofern das Feature in der
+    Mandantenkonfiguration aktiv ist.
+  - Die Weiterleitung nach dem Löschen von Briefen wurde gefixt.
+  - Das Drucken von Briefen direkt auf Drucker wurde gefixt.
+  - Die Auswahl einer Ansprechpersonen in der Brieffunktion wurde gefixt.
+  - Briefe können nun per E-Mail verschickt werden.
+  - Zum Drucken der Briefe wird jetzt das Template Toolkit
+    verwendet. Dazu muss die verwendete Briefvorlage angepasst werden
+    (siehe auch doc/UPGRADE).
+
+Bugfixes:
+
+- Bugfix #132  Verknüpfte Belege: Stornorechnung ist nicht verknüpft mit Storno
+- Bugfix #144  Problem beim Rechnungsdruck: "an invoice item may only be linked back to 1 sales delivery item, something is wrong"
+- Bugfix #150  kivitendo 3.4 - Fehler beim Öffnen von Konten
+- Bugfix #152  Fehler in TopQuickSearch
+- Bugfix #165  inventory.shippingdate wird nicht konsequent benutzt
+- Bugfix #166  Presenter Links gehen im ReportGenerator Export kaputt
 
-2015-xx-xx - Release 3.x.x
 
 Größere neue Features:
 
@@ -40,13 +1111,97 @@ Größere neue Features:
   zu Lieferscheinposition mitverfolgt. Ferner wird der Nettowarenwert für den Fall
   Hauptwährung und Netto-Auftrag berechnet.
 
+Debitorenbuchungsimport
+
+  Neuer Menüpunkt im CSV Importer. Anwendungsbeispiele:
+  * bei einer Migration zu kivitendo die offenen Posten übernehmen
+  * wenn kivitendo für die Buchhaltung benutzt wird, die Rechnungen aber mit
+    einem externen Programm erstellt werden
+
+- experimentelle Auftragsmaske als Controller
+
+  Aufträge können mit einer neuen, experimentellen Maske erfasst werden. Diese
+  Maske ist als Controller implementiert und soll nach erfolgreichen Tests die
+  alte Maske irgendwann ablösen. Es sind allerdings noch nicht alle Funktionen
+  der alten Maske implementiert (siehe auch POD in SL/Controller/Order.pm).
+  Um die neue Maske nicht im Menü zu haben, können die beiden commits
+  "Auftrags-Controller: Menüeinträge" und "Auftrags-Controller: Link zum neuen
+  Controller aus Auftragsliste (zum Testen)." bzw. deren Änderungen rückgängig
+  gemacht werden.
+
+- Der Task-Server ist nun mandantenfähig. Unbedingt die Anmerkungen in
+  doc/UPGRADE dazu lesen, da hier Änderungen in der Administationsoberfläche
+ _nötig sind.
+
 Kleinere neue Features und Detailverbesserungen:
 
+  - Neues Feld GLN bei Kunden/Lieferanten und Lieferadressen.
+
+  - IBANs werden beim Speichern auf Gültigkeit geprüft (betrifft
+    Kunden-/Lieferantenstammdaten sowie Bankkonten)
+
   - Konkurrierende Schreibprozesse beim Speichern von Belegen verhindern.
 
   - SelfTest um einen Test erweitert. Hauptbuch-Nettowert weicht vom Nebenbuch-Netto-Wert ab
     (acc_trans.amount != ar.netamount).
 
+  - Installationsbezogene Übersetzungsmöglichkeit für GUI angelegt (more_texts)
+
+  - Projekte können automatisch beim Speichern eines Verkaufsauftrags angelegt werden.
+
+  - Langtext kann in der Auswahlliste bei mehreren Treffern im Positionsbeleg  zusätzlich angezeigt werden.
+
+  - Besseren kivi-Adventssupport
+
+  - Lieferplan berücksichtigt optional die verküpften items. Lieferplan
+    funktioniert jetzt genauso wie der Lieferwertbericht über die items zu
+    items Verknüpfung. Die verbesserte Auswertung muss aber explizit im Filter
+    des Lieferplans angehakt werden.
+
+  - Projektpicker
+
+  - Brieffunktion überarbeitet: Brieftext kann jetzt den HTML-Editor benutzen
+    und Briefe können mit Belegen verknüpft werden.
+
+  - kleinere Verbesserungen beim Zahlen von Rechnungen in den Belegmasken,
+    z.B. wird das aktuelle Datum vorbelegt und man kann den Fehlbetrag
+    übernehmen
+
+  - Feature: Aufträge immer mit Projektnummer speichern
+    Konfigurierbares Feature, wo beim Speichern eines Auftrags automatisch ein Projekt
+    mit der Auftragsnummer anlegt und dem Auftrag zugewiesen wird
+
+  - Memory-Limits für FCGI-Prozesse
+    Neuer Konfigurationsparameter in der Config, wo FCGI-Prozesse beendet
+    werden, wenn sie mehr Speicher als das Limit belegen
+
+Sicherheit:
+
+  - Das sichere Passwort-Hash-Verfahren PBKDF2 wird nun unterstützt
+    und standardmäßig bei allen zukünftigen Passwortänderungen
+    benutzt.
+
+  - Die Unterstützung der unsicheren Passwort-Hashing-Mechanism crypt,
+    MD5 und SHA-1 wurde entfernt, und entsprechend gehashte Passwörter
+    wurden in der Datenbank entfernt. Für BenutzerInnen, die noch alte
+    Mechanismen verwenden, müssen die Passwörter einmalig in der
+    Administrationsoberfläche zurückgesetzt werden.
+
+Bugfixes:
+
+- Bugfix #13 Lieferplan berechnet die verschickte Menge nicht richtig bei unterschiedlichen Lieferterminen in denselben Lieferschein
+- Bugfix #83   odt-Parser erzeugt fehlerhafte Rechnung mit inkonsistenter content.xml
+- Bugfix #84   Leerer Kunde lässt sich speichern
+- Bugfix #100  Fehler bei Upgrade auf 3.3.0
+- Bugfix #109  Bei "Auslagern über Standardlagerplatz" wird delivery_order_items_stock_id in inventory nicht gesetzt
+- Bugfix #111  Liste mit Artikeln zeigt immer Preis 0 bei der Auswahl von Artikeln in Verkaufs- bzw. Einkaufsdokumenten
+- Bugfix #113  Performance Order Controller
+- Bugfix #115  Hilfelink beim Editieren der Vorlagen falsch
+- Bugfix #116  falscher Link in Kapitel 1 der Doku zum Forum
+- Bugfix #123  SuSa wirft Fehler bei Ist-Versteuerung
+- Bugfix #128  ISE durch fehlerhaften Callback nach EK Rechnungsbuchung
+- Bugfix #136  Historien Sortierung defekt
+
 2015-08-20 - Release 3.3
 
 Größere neue Features:
@@ -464,7 +1619,7 @@ Größere neue Features:
 - Partpicker für Lagereingang mit Kurzhistorie
 
 - Finanzcontrolling
-  Dieser Bericht ermöglich eine Nachkalkulation von Aufträgen, der u.a. auch die Nebenkosten
+  Dieser Bericht ermöglicht eine Nachkalkulation von Aufträgen, der u.a. auch die Nebenkosten
   berücksichtigt und dynamisch mit einem Klick
 
 - CSV-Import von Aufträgen
index 3b87ce7..8437733 100644 (file)
@@ -15,7 +15,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #
 #######################################################################
 
index 6d55ddd..1da2798 100644 (file)
@@ -2,7 +2,8 @@
 <!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
 "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
 <book id="kivitendo-documentation" lang="de">
-  <title>kivitendo 3.3.0: Installation, Konfiguration, Entwicklung</title>
+  <title>kivitendo 3.6.1: Installation, Konfiguration,
+  Entwicklung</title>
 
   <chapter id="Aktuelle-Hinweise">
     <title>Aktuelle Hinweise</title>
     <itemizedlist>
       <listitem>
         <para>im Community-Forum: <ulink
-        url="https://forum.kivitendo.org:32443">https://forum.kivitendo.org:32443</ulink></para>
+        url="https://forum.kivitendo.de">https://forum.kivitendo.de</ulink></para>
       </listitem>
+
       <listitem>
         <para>im Kunden-Forum: <ulink
         url="http://redmine.kivitendo-premium.de/projects/forum/boards/">http://redmine.kivitendo-premium.de/projects/forum/boards/</ulink></para>
       </listitem>
+
       <listitem>
-        <para>in der doc/UPGRADE Datei im doc-Verzeichnis der Installation</para>
+        <para>in der doc/UPGRADE Datei im doc-Verzeichnis der
+        Installation</para>
       </listitem>
+
       <listitem>
-        <para>Im Schulungs- und Dienstleistungsangebot der entsprechenden kivitendo-Partner: <ulink
+        <para>Im Schulungs- und Dienstleistungsangebot der entsprechenden
+        kivitendo-Partner: <ulink
         url="http://www.kivitendo.de/partner.html">http://www.kivitendo.de/partner.html</ulink></para>
       </listitem>
     </itemizedlist>
     <sect1 id="Installation-Übersicht">
       <title>Übersicht</title>
 
-      <para>
-        Die Installation von kivitendo umfasst mehrere Schritte. Die folgende Liste kann sowohl für Neulinge als auch für alte Hasen als
-        Übersicht und Stichpunktliste zum Abhaken dienen, um eine Version mit minimalen Features möglichst schnell zum Laufen zu kriegen.
-      </para>
+      <para>Die Installation von kivitendo umfasst mehrere Schritte. Die
+      folgende Liste kann sowohl für Neulinge als auch für alte Hasen als
+      Übersicht und Stichpunktliste zum Abhaken dienen, um eine Version mit
+      minimalen Features möglichst schnell zum Laufen zu kriegen.</para>
 
       <orderedlist>
-        <listitem><para><emphasis>Voraussetzungen überprüfen</emphasis>: kivitendo benötigt gewisse Ressourcen und benutzt weitere
-        Programme. Das Kapitel "<xref linkend="Benötigte-Software-und-Pakete"/>" erläutert diese. Auch die Liste der benötigten Perl-Module
-        befindet sich hier.</para></listitem>
+        <listitem>
+          <para><emphasis>Voraussetzungen überprüfen</emphasis>: kivitendo
+          benötigt gewisse Ressourcen und benutzt weitere Programme. Das
+          Kapitel "<xref linkend="Benötigte-Software-und-Pakete"/>" erläutert
+          diese. Auch die Liste der benötigten Perl-Module befindet sich
+          hier.</para>
+        </listitem>
 
-        <listitem><para><emphasis>Installation von kivitendo</emphasis>: Diese umfasst die "<xref
-        linkend="Manuelle-Installation-des-Programmpaketes"/>" sowie grundlegende Einstellungen, die der "<xref
-        linkend="config.config-file"/>" erläutert.</para></listitem>
+        <listitem>
+          <para><emphasis>Installation von kivitendo</emphasis>: Diese umfasst
+          die "<xref linkend="Manuelle-Installation-des-Programmpaketes"/>"
+          sowie grundlegende Einstellungen, die der "<xref
+          linkend="config.config-file"/>" erläutert.</para>
+        </listitem>
 
-        <listitem><para><emphasis>Konfiguration externer Programme</emphasis>: hierzu gehören die Datenbank ("<xref
-        linkend="Anpassung-der-PostgreSQL-Konfiguration"/>") und der Webserver ("<xref
-        linkend="Apache-Konfiguration"/>"). </para></listitem>
+        <listitem>
+          <para><emphasis>Konfiguration externer Programme</emphasis>: hierzu
+          gehören die Datenbank ("<xref
+          linkend="Anpassung-der-PostgreSQL-Konfiguration"/>") und der
+          Webserver ("<xref linkend="Apache-Konfiguration"/>").</para>
+        </listitem>
 
-        <listitem><para><emphasis>Benutzerinformationen speichern können</emphasis>: man benötigt mindestens eine Datenbank, in der
-        Informationen zur Authentifizierung sowie die Nutzdaten gespeichert werden. Wie man das als Administrator macht, verrät "<xref
-        linkend="Benutzerauthentifizierung-und-Administratorpasswort"/>".</para></listitem>
+        <listitem>
+          <para><emphasis>Benutzerinformationen speichern können</emphasis>:
+          man benötigt mindestens eine Datenbank, in der Informationen zur
+          Authentifizierung sowie die Nutzdaten gespeichert werden. Wie man
+          das als Administrator macht, verrät "<xref
+          linkend="Benutzerauthentifizierung-und-Administratorpasswort"/>".</para>
+        </listitem>
 
-        <listitem><para><emphasis>Benutzer, Gruppen und Datenbanken anlegen</emphasis>: wie dies alles zusammenspielt erläutert "<xref
-        linkend="Benutzer--und-Gruppenverwaltung"/>".</para></listitem>
+        <listitem>
+          <para><emphasis>Benutzer, Gruppen und Datenbanken
+          anlegen</emphasis>: wie dies alles zusammenspielt erläutert "<xref
+          linkend="Benutzer--und-Gruppenverwaltung"/>".</para>
+        </listitem>
 
-        <listitem><para><emphasis>Los geht's</emphasis>: alles soweit erledigt? Dann kann es losgehen: "<xref
-        linkend="kivitendo-ERP-verwenden"/>"</para></listitem>
+        <listitem>
+          <para><emphasis>Los geht's</emphasis>: alles soweit erledigt? Dann
+          kann es losgehen: "<xref linkend="kivitendo-ERP-verwenden"/>"</para>
+        </listitem>
       </orderedlist>
 
-      <para>
-        Alle weiteren Unterkapitel in diesem Kapitel sind ebenfalls wichtig und sollten vor einer ernsthaften Inbetriebnahme gelesen
-        werden.
-      </para>
+      <para>Alle weiteren Unterkapitel in diesem Kapitel sind ebenfalls
+      wichtig und sollten vor einer ernsthaften Inbetriebnahme gelesen
+      werden.</para>
     </sect1>
 
     <sect1 id="Benötigte-Software-und-Pakete">
         ohne große Probleme auf den derzeit aktuellen verbreiteten
         Distributionen läuft.</para>
 
-        <para>Anfang 2014 sind das folgende Systeme, von denen bekannt ist,
-        dass kivitendo auf ihnen läuft:</para>
+        <para>Mitte 2020 (ab Version 3.5.6) empfehlen wir:</para>
 
         <itemizedlist>
-
           <listitem>
             <para>Debian</para>
+
             <itemizedlist>
-               <listitem>
-                 <para>6.0 "Squeeze" (hier muss allerdings das Modul FCGI in der Version >= 0.72 compiled werden, und <literal>Rose::DB::Object</literal> ist zu alt)</para>
-               </listitem>
-               <listitem>
-                 <para>7.0 "Wheezy"</para>
-               </listitem>
+              <listitem>
+                <para>10.0 "Buster"</para>
+              </listitem>
+              <listitem>
+                <para>11.0 "Bullseye"</para>
+              </listitem>
             </itemizedlist>
           </listitem>
 
           <listitem>
-            <para>Ubuntu 12.04 LTS "Precise Pangolin", 12.10 "Quantal Quetzal", 13.04 "Precise Pangolin" und 14.04 "Trusty Tahr" LTS Alpha</para>
-          </listitem>
-
-          <listitem>
-            <para>openSUSE 12.2, 12.3 und 13.1</para>
+            <para>20.04 "Focal Fossa" LTS
+          </para>
           </listitem>
 
           <listitem>
-            <para>SuSE Linux Enterprice Server 11</para>
+            <para>openSUSE Leap 15.x und SUSE Linux Enterprise Server 15 GA</para>
           </listitem>
 
           <listitem>
-            <para>Fedora 16 bis 19</para>
+            <para>Fedora 29</para>
           </listitem>
         </itemizedlist>
       </sect2>
         <title>Benötigte Perl-Pakete installieren</title>
 
         <para>Zum Betrieb von kivitendo werden zwingend ein Webserver (meist
-        Apache) und ein Datenbankserver (PostgreSQL, mindestens v8.4)
+        Apache) und ein Datenbankserver (PostgreSQL) in einer aktuellen
+        Version (s.a. Liste der unterstützten Betriebssysteme)
         benötigt.</para>
 
-        <para>Zusätzlich benötigt kivitendo einige Perl-Pakete, die nicht Bestandteil einer Standard-Perl-Installation sind. Um zu
-        überprüfen, ob die erforderlichen Pakete installiert und aktuell genug sind, wird ein Script mitgeliefert, das wie folgt aufgerufen
-        wird:</para>
+        <para>Zusätzlich benötigt kivitendo einige Perl-Pakete, die nicht
+        Bestandteil einer Standard-Perl-Installation sind. Um zu überprüfen,
+        ob die erforderlichen Pakete installiert und aktuell genug sind, wird
+        ein Script mitgeliefert, das wie folgt aufgerufen wird:</para>
 
         <programlisting>./scripts/installation_check.pl</programlisting>
 
         <para>Die vollständige Liste der benötigten Perl-Module lautet:</para>
 
         <itemizedlist>
-          <listitem><para><literal>parent</literal> (nur bei Perl vor 5.10.1)</para></listitem>
+          <listitem>
+            <para><literal>Algorithm::CheckDigits</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Archive::Zip</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>CAM::PDF</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>CGI</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Clone</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Config::Std</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Daemon::Generic</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>DateTime</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>DateTime::Event::Cron</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>DateTime::Format::Strptime</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>DateTime::Set</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>DBI</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>DBD::Pg</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Digest::SHA</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Email::Address</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Email::MIME</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Exception::Class</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>FCGI</literal> (nicht Versionen 0.68 bis 0.71
+            inklusive; siehe <xref
+            linkend="Apache-Konfiguration.FCGI.WebserverUndPlugin"/>)</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>File::Copy::Recursive</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>File::Flock</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>File::MimeInfo</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>File::Slurp</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>GD</literal></para>
+          </listitem>
 
-          <listitem><para><literal>Archive::Zip</literal></para></listitem>
+          <listitem>
+            <para><literal>HTML::Parser</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>HTML::Restrict</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Image::Info</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Imager</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Imager::QRCode</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>IPC::Run</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>JSON</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>List::MoreUtils</literal></para>
+          </listitem>
+
+          <listitem>
+            <para><literal>List::UtilsBy</literal></para>
+          </listitem>
+
+          <listitem>
+            <para>LWP::Authen::Digest</para>
+          </listitem>
+
+          <listitem>
+            <para>LWP::UserAgent</para>
+          </listitem>
 
-          <listitem><para><literal>Config::Std</literal></para></listitem>
+          <listitem>
+            <para><literal>Net::SMTP::SSL</literal> (optional, bei
+            E-Mail-Versand über SSL; siehe Abschnitt "<xref
+            linkend="config.sending-email.smtp"/>")</para>
+          </listitem>
 
-          <listitem><para><literal>DateTime</literal></para></listitem>
+          <listitem>
+            <para><literal>Net::SSLGlue</literal> (optional, bei
+            E-Mail-Versand über TLS; siehe Abschnitt "<xref
+            linkend="config.sending-email.smtp"/>")</para>
+          </listitem>
 
-          <listitem><para><literal>DBI</literal></para></listitem>
+          <listitem>
+            <para><literal>Math::Round</literal></para>
+          </listitem>
 
-          <listitem><para><literal>DBD::Pg</literal></para></listitem>
+          <listitem>
+            <para><literal>Params::Validate</literal></para>
+          </listitem>
 
-          <listitem><para><literal>Email::Address</literal></para></listitem>
+          <listitem>
+            <para><literal>PBKDF2::Tiny</literal></para>
+          </listitem>
 
-          <listitem><para><literal>Email::MIME</literal></para></listitem>
+          <listitem>
+            <para><literal>PDF::API2</literal></para>
+          </listitem>
 
-          <listitem><para><literal>FCGI</literal> (nicht Versionen 0.68 bis 0.71 inklusive; siehe <xref linkend="Apache-Konfiguration.FCGI.WebserverUndPlugin"/>)</para></listitem>
+          <listitem>
+            <para><literal>Regexp::IPv6</literal></para>
+          </listitem>
 
-          <listitem><para><literal>File::Copy::Recursive</literal></para></listitem>
+          <listitem>
+            <para><literal>Rest::Client</literal></para>
+          </listitem>
 
-          <listitem><para><literal>JSON</literal></para></listitem>
+          <listitem>
+            <para><literal>Rose::Object</literal></para>
+          </listitem>
 
-          <listitem><para><literal>List::MoreUtils</literal></para></listitem>
+          <listitem>
+            <para><literal>Rose::DB</literal></para>
+          </listitem>
 
-          <listitem><para><literal>Net::SMTP::SSL</literal> (optional, bei E-Mail-Versand über SSL; siehe Abschnitt "<xref
-          linkend="config.sending-email.smtp"/>")</para></listitem>
+          <listitem>
+            <para><literal>Rose::DB::Object</literal> Version 0.788 oder
+            neuer</para>
+          </listitem>
 
-          <listitem><para><literal>Net::SSLGlue</literal> (optional, bei E-Mail-Versand über TLS; siehe Abschnitt "<xref
-          linkend="config.sending-email.smtp"/>")</para></listitem>
+          <listitem>
+            <para><literal>Set::Infinite</literal></para>
+          </listitem>
 
-          <listitem><para><literal>Params::Validate</literal></para></listitem>
+          <listitem>
+            <para><literal>String::ShellQuote</literal></para>
+          </listitem>
 
-          <listitem><para><literal>PDF::API2</literal></para></listitem>
+          <listitem>
+            <para><literal>Sort::Naturally</literal></para>
+          </listitem>
 
-          <listitem><para><literal>Rose::Object</literal></para></listitem>
+          <listitem>
+            <para><literal>Template</literal></para>
+          </listitem>
 
-          <listitem><para><literal>Rose::DB</literal></para></listitem>
+          <listitem>
+            <para><literal>Text::CSV_XS</literal></para>
+          </listitem>
 
-          <listitem><para><literal>Rose::DB::Object</literal> Version 0.788 oder neuer</para></listitem>
+          <listitem>
+            <para><literal>Text::Iconv</literal></para>
+          </listitem>
 
-          <listitem><para><literal>Template</literal></para></listitem>
+          <listitem>
+            <para><literal>Text::Unidecode</literal></para>
+          </listitem>
 
-          <listitem><para><literal>Text::CSV_XS</literal></para></listitem>
+          <listitem>
+            <para><literal>Try::Tiny</literal></para>
+          </listitem>
 
-          <listitem><para><literal>Text::Iconv</literal></para></listitem>
+          <listitem>
+            <para><literal>URI</literal></para>
+          </listitem>
 
-          <listitem><para><literal>URI</literal></para></listitem>
+          <listitem>
+            <para><literal>XML::Writer</literal></para>
+          </listitem>
 
-          <listitem><para><literal>XML::Writer</literal></para></listitem>
+          <listitem>
+            <para><literal>XML::LibXML</literal></para>
+          </listitem>
 
-          <listitem><para><literal>YAML</literal></para></listitem>
+          <listitem>
+            <para><literal>YAML::XS</literal> oder <literal>YAML</literal></para>
+          </listitem>
         </itemizedlist>
 
-        <para>Seit Version v3.2.0 sind die folgenden Pakete hinzugekommen: <literal>GD</literal>, <literal>HTML::Restrict</literal>, <literal>Image::Info</literal></para>
-        <para>Seit v3.0.0 sind die folgenden Pakete hinzugekommen: <literal>File::Copy::Recursive</literal>.</para>
 
-        <para>Seit v2.7.0 sind die folgenden Pakete hinzugekommen: <literal>Email::MIME</literal>, <literal>Net::SMTP::SSL</literal>,
+        <para>Seit Version größer v3.6.0 sind die folgenden Pakete hinzugekommen: <literal>IPC::Run</literal></para>
+
+        <para>Seit Version größer v3.5.8 sind die folgenden Pakete hinzugekommen: <literal>Imager</literal>, <literal>Imager::QRCode</literal>
+<literal>Rest::Client</literal><literal>Term::ReadLine::Gnu</literal></para>
+
+        <para>Seit Version größer v3.5.6 sind die folgenden Pakete hinzugekommen: <literal>Try::Tiny</literal>, <literal>Math::Round</literal></para>
+        <para>Seit Version größer v3.5.6 sind die folgenden Pakete hinzugekommen: <literal>XML::LibXML</literal>, <literal>CAM::PDF</literal></para>
+        <para>Seit Version größer v3.5.3 sind die folgenden Pakete hinzugekommen: <literal>Exception::Class</literal></para>
+
+        <para>Seit Version größer v3.5.1 sind die folgenden Pakete hinzugekommen: <literal>Set::Infinite</literal>,
+        <literal>List::UtilsBy</literal>, <literal>DateTime::Set</literal>, <literal>DateTime::Event::Cron</literal>
+        <literal>Daemon::Generic</literal>, <literal>DateTime::Event::Cron</literal>, <literal>File::Flock</literal>,
+        <literal>File::Slurp</literal></para>
+
+        <para>Seit Version größer v3.5.0 sind die folgenden Pakete
+        hinzugekommen: <literal>Text::Unidecode</literal>,
+        <literal>LWP::Authen::Digest</literal>,
+        <literal>LWP::UserAgent</literal></para>
+
+        <para>Seit Version v3.4.0 sind die folgenden Pakete hinzugekommen:
+        <literal>Algorithm::CheckDigits</literal>,
+        <literal>PBKDF2::Tiny</literal></para>
+
+        <para>Seit Version v3.2.0 sind die folgenden Pakete hinzugekommen:
+        <literal>GD</literal>, <literal>HTML::Restrict</literal>,
+        <literal>Image::Info</literal></para>
+
+        <para>Seit v3.0.0 sind die folgenden Pakete hinzugekommen:
+        <literal>File::Copy::Recursive</literal>.</para>
+
+        <para>Seit v2.7.0 sind die folgenden Pakete hinzugekommen:
+        <literal>Email::MIME</literal>, <literal>Net::SMTP::SSL</literal>,
         <literal>Net::SSLGlue</literal>.</para>
 
         <para>Gegenüber Version 2.6.0 sind zu dieser Liste 2 Pakete
 
         <sect3>
           <title>Debian und Ubuntu</title>
+          <para>Für Debian und Ubuntu stehen die meisten der benötigten
+          Pakete als Debian-Pakete zur Verfügung. Sie können mit
+          folgendem Befehl installiert werden:</para>
 
-          <para>Für Debian und Ubuntu stehen die meisten der benötigten Perl-Pakete als Debian-Pakete zur Verfügung. Sie können mit folgendem Befehl installiert werden:</para>
-
-          <programlisting>apt-get install apache2 libarchive-zip-perl libclone-perl \
+          <programlisting>apt install  apache2 libarchive-zip-perl libclone-perl \
   libconfig-std-perl libdatetime-perl libdbd-pg-perl libdbi-perl \
   libemail-address-perl  libemail-mime-perl libfcgi-perl libjson-perl \
   liblist-moreutils-perl libnet-smtp-ssl-perl libnet-sslglue-perl \
   librose-db-perl librose-object-perl libsort-naturally-perl \
   libstring-shellquote-perl libtemplate-perl libtext-csv-xs-perl \
   libtext-iconv-perl liburi-perl libxml-writer-perl libyaml-perl \
-  libimage-info-perl libgd-gd2-perl \
-  libfile-copy-recursive-perl postgresql</programlisting>
-
-          <para>Für das Paket HTML::Restrict gibt es kein Debian-Paket, dies muß per CPAN installiert werden. Unter Ubuntu funktioniert das mit:</para>
-          <programlisting>apt-get install build-essential
-cpan HTML::Restrict</programlisting>
+  libimage-info-perl libgd-gd2-perl libapache2-mod-fcgid \
+  libfile-copy-recursive-perl postgresql libalgorithm-checkdigits-perl \
+  libcrypt-pbkdf2-perl git libcgi-pm-perl libtext-unidecode-perl libwww-perl \
+  postgresql-contrib poppler-utils libhtml-restrict-perl \
+  libdatetime-set-perl libset-infinite-perl liblist-utilsby-perl \
+  libdaemon-generic-perl libfile-flock-perl libfile-slurp-perl \
+  libfile-mimeinfo-perl libpbkdf2-tiny-perl libregexp-ipv6-perl \
+  libdatetime-event-cron-perl libexception-class-perl libcam-pdf-perl \
+  libxml-libxml-perl libtry-tiny-perl libmath-round-perl \
+  libimager-perl libimager-qrcode-perl librest-client-perl libipc-run-perl
+          </programlisting>
+
+          <para>Sollten Pakete nicht zu Verfügung stehen, so können diese auch mittels CPAN installiert werden. Ferner muss für Ubuntu das Repository "Universe" aktiv sein (s.a. Anmerkungen).</para>
+          <note id="ubuntu-universe">
+            <para>Die Perl Pakete für Ubuntu befinden sich im "Universe" Repository. Falls dies nicht aktiv ist, kann dies mit folgendem Aufruf aktiviert werden:
+<programlisting>add-apt-repository universe</programlisting></para>
+          </note>
         </sect3>
 
         <sect3>
-          <title>Fedora Core</title>
+          <title>Fedora</title>
 
-          <para>Für Fedora Core stehen die meisten der benötigten Perl-Pakete als RPM-Pakete zur Verfügung. Sie können mit folgendem Befehl installiert werden:</para>
+          <para>Für Fedora stehen die meisten der benötigten Perl-Pakete als
+          RPM-Pakete zur Verfügung. Sie können mit folgendem Befehl
+          installiert werden:</para>
 
-          <programlisting>yum install httpd perl-Archive-Zip perl-Clone perl-DBD-Pg \
-  perl-DBI perl-DateTime perl-Email-Address perl-Email-MIME perl-FCGI \
-  perl-File-Copy-Recursive perl-JSON perl-List-MoreUtils perl-Net-SMTP-SSL perl-Net-SSLGlue \
-  perl-PDF-API2 perl-Params-Validate perl-Rose-DB perl-Rose-DB-Object \
+          <programlisting>dnf install httpd mod_fcgid postgresql-server postgresql-contrib\
+  perl-Algorithm-CheckDigits perl-Archive-Zip perl-CPAN perl-Class-XSAccessor \
+  perl-Clone perl-Config-Std perl-DBD-Pg perl-DBI perl-Daemon-Generic \
+  perl-DateTime perl-DateTime-Set perl-Email-Address perl-Email-MIME perl-FCGI \
+  perl-File-Copy-Recursive perl-File-Flock perl-File-MimeInfo perl-File-Slurp \
+  perl-GD perl-HTML-Restrict perl-JSON perl-List-MoreUtils perl-List-UtilsBy \
+  perl-Net-SMTP-SSL perl-Net-SSLGlue perl-PBKDF2-Tiny perl-PDF-API2 \
+  perl-Params-Validate perl-Regexp-IPv6 perl-Rose-DB perl-Rose-DB-Object \
   perl-Rose-Object perl-Sort-Naturally perl-String-ShellQuote \
-  perl-Template-Toolkit perl-Text-CSV_XS perl-Text-Iconv perl-URI \
-  perl-XML-Writer perl-YAML perl-parent postgresql-server</programlisting>
-
-          <para>Zusätzlich müssen einige Pakete aus dem CPAN installiert werden. Dazu können Sie die folgenden Befehle nutzen:</para>
-
-          <programlisting>yum install perl-CPAN
-cpan Config::Std</programlisting>
-
+  perl-Template-Toolkit perl-Text-CSV_XS perl-Text-Iconv perl-URI perl-XML-Writer \
+  perl-YAML perl-libwww-perl</programlisting>
         </sect3>
 
         <sect3>
-          <title>openSUSE</title>
-
-          <para>Für openSUSE stehen die meisten der benötigten Perl-Pakete als RPM-Pakete zur Verfügung. Sie können mit folgendem Befehl
-          installiert werden:</para>
-
-          <programlisting>zypper install apache2 perl-Archive-Zip perl-Clone \
-  perl-Config-Std perl-DBD-Pg perl-DBI perl-DateTime perl-Email-Address \
-  perl-Email-MIME perl-FastCGI perl-File-Copy-Recursive perl-JSON perl-List-MoreUtils \
-  perl-Net-SMTP-SSL perl-Net-SSLGlue perl-PDF-API2 perl-Params-Validate \
-  perl-Sort-Naturally perl-Template-Toolkit perl-Text-CSV_XS perl-Text-Iconv \
-  perl-URI perl-XML-Writer perl-YAML postgresql-server</programlisting>
-
-          <para>Zusätzlich müssen einige Pakete aus dem CPAN installiert werden. Dazu können Sie die folgenden Befehle nutzen:</para>
+          <title>openSUSE Leap 15.x und SUSE Linux Enterprise Server 15 GA</title>
+
+          <para>Für openSUSE Leap 15.x stehen die meisten der benötigten Perl-Pakete als RPM-Pakete zur Verfügung.</para>
+          <para>Damit diese installiert werden können, muß das System die erforderlichen Repositories kennen und Zugriff über das Internet darauf haben.</para>
+          <para>Daher machen wir die Repositories dem System bekannt.</para>
+          <para>Um die zusätzlichen Repositories für die Installation zur Verfügung zu stellen, kann man diese mit YaST oder auch in einem Terminal auf der Konsole bekannt geben. Wir beschränken uns hier mit der Eingabe auf der Konsole. In den allermeisten Fällen verwenden die Administratoren eine sichere SSH-Verbindung zum zu administrierenden Server.</para>
+          <para>Dazu geben wir folgenden Befehl ein:</para>
+          <programlisting>zypper addrepo -f \
+  http://download.opensuse.org/repositories/devel:languages:perl/openSUSE_Leap_15.2/ \
+  "devel:languages:perl"
+          </programlisting>
+          <programlisting>zypper addrepo -f \
+  https://download.opensuse.org/repositories/devel:languages:haskell:lts:13/\
+  openSUSE_Leap_15.0/ "devel:languages:haskell:lts:13"
+          </programlisting>
+          <programlisting>zypper addrepo -f \
+  https://download.opensuse.org/repositories/devel:languages:haskell:lts:13/\
+  openSUSE_Leap_15.0/ "devel:languages:haskell:lts:13"
+          </programlisting>
+          <para>Danach geben wir noch die beiden folgenden Befehle ein:</para>
+          <programlisting>zypper clean</programlisting>
+          <programlisting>zypper refresh</programlisting>
+          <para>Sollte zypper eine Meldung ausgeben, ob der Repositorie Key abgelehnt, nicht vertraut oder für immer akzeptiert werden soll, ist die Beantwortung durch drücken der "i" Taste am besten geeignet. Wer noch mehr über zypper erfahren möchte, kann sich einmal die zypper Hilfe anschauen.</para>
+          <programlisting>zypper --help</programlisting>
+          <note>
+          <para>Offiziell wird von openSUSE nur noch Versionen ab 15.2 unterstützt. Die SuSE Macher haben ab Version 15.x einen großen Umbau in der Verwaltung der Pakete vorgenommen, das heißt, der Paketumfang ist der SLES 15 als Programmunterbau angepasst. Dies gilt besonders der openSUSE Distribution. Es gibt ja einmal die openSUSE Distri und die Professionelle SLES Version. Dadurch sind viele Pakete aus dem ursprünglich nur für die openSUSE geltenen Repositorie enfernt worden, aber auch viele auf aktuellem Stand gehalten.</para>
+          </note>
+          <para>Ab openSUSE Leap 15.x kann man die Distribution auch als reine Text Version, also ohne KDE Oberfläche aufsetzen. Vorteil hierbei ist, dass weniger Balast und unnötige Pakte installiert werden.</para>
+          <para>Bei openSUSE Versionen bis 15.x, also 10.x, 11.x, 12.x, 13.x hatte der Administrator die Möglichkeit, bei der Installation der Distribution die KDE Oberfläche zu aktivieren. In dieser Konstellation hat man die Möglichkeit, eine VNC Verbindung vom administrativen Client zu verwenden. Ist das nicht eingerichtet, arbeitet der Admin dann direkt am Bildschirm des Servers. Nun loggen wir uns am Server direkt ein, starten Yast2 in einer Konsole wie folgt:</para>
+          <para>yast2 return.</para>
+          <para>Oder über die Menüführung wie folgt: Ein Klick auf das runde Icon, ganz links unten in der Menüleiste, dann die Maus verfahren auf System und YaST.</para>
+          <para>Im weiteren Verlauf der Installation, beschränken wir uns mit dem Installations Werkzeug zypper. Zypper ist ein Komandozeilen basiertes Installations Tool, welches bei openSUSE Standard ist. Zypper weist ein etwas eigenartiges Verhalten auf, dass sich in etwa wie folgt darstellt. Hat man die Repositories eingerichtet, kann man diese mit Yast als auch mit Zypper benutzen, setzt man einen Befehl wie etwa: zypper up ab, so findet zypper mehr neuere Programmversionen als Yast. Ich habe im allgemeinen noch keine Nachteile damit erlebt.</para>
+          <para>Programmpakete können mit folgendem Befehl installiert werden:</para>
+          <para>zypper install Paketname</para>
+          <para>Es wird empfohlen zusätzliche Pakete nicht direkt mit CPAN zu installieren, da man diese auch über andere Repositories beziehen kann, die bei openSUSE zur Verfügung stehen. Dadurch hat man den Vorteil, dass die Pakete mit YaST verwaltet werden, also wieder deinstalliert oder durch neuere ersetzt werden können. Zudem kann man auch noch eventuelle Bugs an openSUSE senden und diese dem Maintainer melden.</para>
+
+          <programlisting>zypper install perl-threads-shared ghc-pdfinfo apache2-mod_fcgid \
+  yast2-http-server postgresql-server postgresql-contrib perl-Algorithm-CheckDigits \
+  perl-Archive-Zip perl-CGI perl-CGI-Ajax perl-Clone \
+  perl-Config-Std perl-Class-XSAccessor perl-Daemon-Generic perl-DateTime \
+  perl-DateTime-Event-Cron perl-DateTime-Format-Strptime perl-DateTime-Set \
+  perl-DBI perl-DBD-Pg perl-Devel-REPL perl-FastCGI perl-Email-Address \
+  perl-Email-MIME perl-Email-MIME-ContentType perl-Email-MIME-Encodings \
+  perl-FCGI perl-File-Copy-Recursive perl-File-Flock perl-File-MimeInfo \
+  perl-File-Slurp perl-GD perl-HTML-Restrict perl-Image-Info \
+  perl-JSON perl-List-MoreUtils perl-List-UtilsBy perl-Log-Log4perl perl-Net-LDAP-Server \
+  perl-Net-SSLGlue perl-Net-SMTP-SSL perl-PBKDF2-Tiny perl-PDF-API2 \
+  perl-Params-Validate perl-Regexp-IPv6 perl-Rose-DB perl-Rose-Object \
+  perl-Rose-DB-Object perl-MooseX-Role-Cmd perl-Set-Crontab perl-Set-Infinite \
+  perl-Sort-Naturally perl-String-ShellQuote perl-Sys-CPU perl-Template-Toolkit \
+  perl-Text-CSV_XS perl-Test-Deep perl-Test-Output perl-Text-Iconv \
+  perl-Text-Unidecode perl-URI perl-URI-Find perl-XML-Writer \
+  perl-YAML perl-libwww-perl
+          </programlisting>
+
+          <para>Für die Entwickler installiert man noch die folgenden Pakete:</para>
+
+          <programlisting>zypper install ghc-mtl-devel ghc-old-locale-devel \
+  ghc-process-extras-devel ghc-rpm-macros ghc-text-devel ghc-time-devel \
+  ghc-Cabal-devel ghc-time-locale-compat-devel perl-Log-Log4perl ghc-pdfinfo \
+  ghc-pdfinfo-devel perl-Devel-REPL perl-URI-Find perl-Class-Utils \
+  perl-Error-Pure perl-File-Object perl-Readonly perl-Test-Warnings \
+  perl-Test-NoWarnings perl-Test-Deep perl-Test-Output perl-Test-Strict \
+  perl-Test-LongString perl-File-Find-Rule
+          </programlisting>
+
+          <para>Zusätzlich müssen einige Pakete für den Umgang mit Latex installiert werden. Die Latex Module barcodes sind nützliche Helfer um auch Barcodes im Dokument zu platzieren, der Vollständigkeit halber hier für die Installation mit angegeben.
+              Dazu können Sie die folgenden Befehle nutzen:</para>
+
+          <programlisting>zypper install texlive-wallpaper texlive-colortbl \
+  texlive-scrlttr2copy texlive-eurosym \
+  texlive-geometry texlive-german texlive-graphbox texlive-hyperref \
+  texlive-xifthen texlive-luainputenc texlive-lastpage texlive-ltabptch \
+  texlive-nomentbl texlive-threeparttablex texlive-substr texlive-tabulary \
+  texlive-ulem texlive-wallpaper texlive-xcolor texlive-xstring \
+  texlive-xypic texlive-mwe texlive-mweights texlive-barcodes \
+  texlive-GS1 texlive-ean texlive-makebarcode texlive-pst-barcode \
+  texlive-upca
+          </programlisting>
+
+          <para>Zusätzlich müssen einige Pakete aus dem CPAN installiert
+          werden. Dazu können Sie die folgenden Befehle anwenden:</para>
+
+          <programlisting>cpan DateTime::event::Cron DateTime::Set FCGI \
+  HTML::Restrict PBKDF2::Tiny Rose::Db::Object Set::Infinite</programlisting>
+        </sect3>
+      </sect2>
 
-          <programlisting>yum install perl-CPAN
-cpan Rose::Db::Object</programlisting>
+      <sect2>
+        <title>Andere Pakete installieren</title>
 
-        </sect3>
+        <itemizedlist>
+          <listitem>
+            <para><literal>poppler-utils</literal> 'pdfinfo' zum Erkennen der Seitenanzahl bei der PDF-Generierung</para>
+          </listitem>
+          <listitem>
+            <para><literal>Postgres Trigram-Index</literal> Für datenbankoptimierte Suchanfragen. Bspw. im Paket <literal>postgresql-contrib</literal> enthalten</para>
+          </listitem>
+        </itemizedlist>
+        <para>Debian und Ubuntu: <programlisting>apt install postgresql-contrib poppler-utils</programlisting></para>
+        <para>Fedora: <programlisting>dnf install poppler-utils postgresql-contrib</programlisting></para>
+        <para>openSUSE: <programlisting>zypper install poppler-tools</programlisting></para>
       </sect2>
     </sect1>
 
     <sect1 id="Manuelle-Installation-des-Programmpaketes"
            xreflabel="Manuelle Installation des Programmpaketes">
       <title>Manuelle Installation des Programmpaketes</title>
-      <para>Der aktuelle Stable-Release, bzw. beta Release wird bei github gehostet und kann
- <ulink url="https://github.com/kivitendo/kivitendo-erp/releases">hier</ulink> heruntergeladen werden.</para>
-      <para>Die kivitendo ERP Installationsdatei (<filename>kivitendo-erp-3.3.0.tgz</filename>) wird im Dokumentenverzeichnis des Webservers
-      (z.B.  <filename>/var/www/html/</filename>, <filename>/srv/www/htdocs</filename> oder <filename>/var/www/</filename>) entpackt:</para>
+
+      <para>Der aktuelle Stable-Release, bzw. beta Release wird bei github
+      gehostet und kann <ulink
+      url="https://github.com/kivitendo/kivitendo-erp/releases">hier</ulink>
+      heruntergeladen werden.</para>
+
+      <para>Das aktuelleste kivitendo ERP-Archiv
+      (<filename>kivitendo-erp-*.tgz</filename>) wird dann im
+      Dokumentenverzeichnis des Webservers (z.B.
+      <filename>/var/www/html/</filename>,
+      <filename>/srv/www/htdocs</filename> oder
+      <filename>/var/www/</filename>) entpackt:</para>
 
       <programlisting>cd /var/www
-tar xvzf kivitendo-erp-3.3.0.tgz</programlisting>
+tar xvzf kivitendo-erp-*.tgz</programlisting>
 
       <para>Wechseln Sie in das entpackte Verzeichnis:</para>
 
@@ -295,24 +633,80 @@ tar xvzf kivitendo-erp-3.3.0.tgz</programlisting>
       Webserverkonfiguration benutzen, um auf das tatsächliche
       Installationsverzeichnis zu verweisen.</para>
 
-      <para>Bei einer Neuinstallation von Version 3.1.0 oder später muß das WebDAV Verzeichnis derzeit manuell angelegt werden:</para>
+      <para>Bei einer Neuinstallation von Version 3.1.0 oder später muß das
+      WebDAV Verzeichnis derzeit manuell angelegt werden:</para>
 
       <programlisting>mkdir webdav</programlisting>
 
-      <para>Die Verzeichnisse <filename>users</filename>, <filename>spool</filename> und <filename>webdav</filename> müssen für den Benutzer
-      beschreibbar sein, unter dem der Webserver läuft. Die restlichen Dateien müssen für diesen Benutzer lesbar sein. Die Benutzer- und
-      Gruppennamen sind bei verschiedenen Distributionen unterschiedlich (z.B. bei Debian/Ubuntu <constant>www-data</constant>, bei Fedora
-      core <constant>apache</constant> oder bei OpenSUSE <constant>wwwrun</constant>).</para>
+      <para>Die Verzeichnisse <filename>users</filename>,
+      <filename>spool</filename> und <filename>webdav</filename> müssen für
+      den Benutzer beschreibbar sein, unter dem der Webserver läuft. Die
+      restlichen Dateien müssen für diesen Benutzer lesbar sein. Die Benutzer-
+      und Gruppennamen sind bei verschiedenen Distributionen unterschiedlich
+      (z.B. bei Debian/Ubuntu <constant>www-data</constant>, bei Fedora
+      <constant>apache</constant> oder bei openSUSE
+      <constant>wwwrun</constant>).</para>
 
       <para>Der folgende Befehl ändert den Besitzer für die oben genannten
       Verzeichnisse auf einem Debian/Ubuntu-System:</para>
 
       <programlisting>chown -R www-data users spool webdav</programlisting>
 
-      <para>Weiterhin muss der Webserver-Benutzer in den Verzeichnissen <filename>templates</filename> und <filename>users</filename>
-      Unterverzeichnisse für jeden neuen Benutzer anlegen dürfen, der in kivitendo angelegt wird:</para>
+      <para>Weiterhin muss der Webserver-Benutzer in den Verzeichnissen
+      <filename>templates</filename> und <filename>users</filename>
+      Unterverzeichnisse für jeden neuen Benutzer anlegen dürfen, der in
+      kivitendo angelegt wird:</para>
 
       <programlisting>chown www-data templates users</programlisting>
+
+      <note>
+        <para>Wir empfehlen eine Installation mittels des Versionsmanagager
+        git. Hierfür muss ein git-Client installiert sein. Damit ist man sehr
+        viel flexibler für zukünftige Upgrades. Installations-Anleitung (bitte
+        die Pfade anpassen) bspw. wie folgt: <programlisting>cd /var/www/
+git clone https://github.com/kivitendo/kivitendo-erp.git
+cd kivitendo-erp/
+git checkout `git tag -l | egrep -ve "(alpha|beta|rc)" | tail -1`</programlisting>
+        Erläuterung: Der Befehl wechselt zur letzten Stable-Version (git tag
+        -l listet alle Tags auf, das egrep schmeisst alle Einträge mit alpha,
+        beta oder rc raus und das tail gibt davon den obersten Treffer
+        zurück). Sehr sinnvoll ist es, direkt im Anschluss einen eigenen
+        Branch zu erzeugen, um bspw. seine eigenen Druckvorlagen-Anpassungen
+        damit zu verwalten. Hierfür reicht ein simples <programlisting>  git checkout -b meine_eigenen_änderungen</programlisting>
+        nach dem letzten Kommando (weiterführende Informationen <ulink
+        url="http://www-cs-students.stanford.edu/~blynn/gitmagic/index.html">
+        Git Magic</ulink>).</para>
+
+        <para>Ein beispielhafter Workflow für Druckvorlagen-Anpassungen von
+        3.4.1 nach 3.5: <programlisting>
+$ git clone https://github.com/kivitendo/kivitendo-erp.git
+$ cd kivitendo-erp/
+$ git checkout release-3.4.1                # das ist ein alter release aus dem wir starten ...
+$ git checkout -b meine_eigene_änderungen   # unser lokaler branch - unabhängig von allen anderen
+$ git add templates/mein_druck              # das sind unsere druckvorlagen inkl. produktbilder
+$ git commit -m "juhu tolle änderungen"
+
+[meine_aenderungen 1d89e41] juhu tolle ändernungen
+ 4 files changed, 380 insertions(+)
+ create mode 100644 templates/mein_druck/img/webdav/tesla.png
+ create mode 100644 templates/mein_druck/mahnung.tex
+ create mode 100644 templates/mein_druck/zahlungserinnerung_zwei.tex
+ create mode 100644 templates/mein_druck/zahlungserinnerung_zwei_invoice.tex
+
+# 5 Jahre später ...
+# webserver abschalten!
+
+$ git checkout master
+$ git pull                                  # oder git fetch und danach ein stable release tag auswählen (s.o.)
+$ git checkout meine_eigenen_änderungen
+$ git rebase master
+
+Zunächst wird der Branch zurückgespult, um Ihre Änderungen
+darauf neu anzuwenden ...
+Wende an: juhu tolle änderungen
+$ service apache2 restart                   # webserver starten!
+</programlisting></para>
+      </note>
     </sect1>
 
     <sect1 id="config.config-file">
@@ -322,10 +716,10 @@ tar xvzf kivitendo-erp-3.3.0.tgz</programlisting>
              xreflabel="Einführung in die Konfigurationsdatei">
         <title>Einführung</title>
 
-        <para>In kivitendo gibt es nur noch eine Konfigurationsdatei,
-        die benötigt wird: <filename>config/kivitendo.conf</filename> (kurz:
-        "die Hauptkonfigurationsdatei"). Diese muss bei der Erstinstallation
-        von kivitendo bzw. der Migration von älteren Versionen angelegt
+        <para>In kivitendo gibt es nur noch eine Konfigurationsdatei, die
+        benötigt wird: <filename>config/kivitendo.conf</filename> (kurz: "die
+        Hauptkonfigurationsdatei"). Diese muss bei der Erstinstallation von
+        kivitendo bzw. der Migration von älteren Versionen angelegt
         werden.</para>
 
         <para>Als Vorlage dient die Datei
@@ -341,10 +735,10 @@ tar xvzf kivitendo-erp-3.3.0.tgz</programlisting>
         abweichen.</para>
 
         <note>
-         <para>
-          Vor der Umbenennung in kivitendo hieß diese Datei noch <filename>config/lx_office.conf</filename>. Aus Gründen der Kompatibilität
-          wird diese Datei eingelesen, sofern die Datei <filename>config/kivitendo.conf</filename> nicht existiert.
-         </para>
+          <para>Vor der Umbenennung in kivitendo hieß diese Datei noch
+          <filename>config/lx_office.conf</filename>. Aus Gründen der
+          Kompatibilität wird diese Datei eingelesen, sofern die Datei
+          <filename>config/kivitendo.conf</filename> nicht existiert.</para>
         </note>
 
         <para>Diese Hauptkonfigurationsdatei ist dann eine
@@ -363,39 +757,72 @@ tar xvzf kivitendo-erp-3.3.0.tgz</programlisting>
         entsprechend kommentiert sind:</para>
 
         <itemizedlist>
-          <listitem><para><literal>authentication</literal> (siehe Abschnitt "<xref
-          linkend="Benutzerauthentifizierung-und-Administratorpasswort"/>" in diesem Kapitel)</para></listitem>
-
-          <listitem><para><literal>authentication/database</literal></para></listitem>
+          <listitem>
+            <para><literal>authentication</literal> (siehe Abschnitt "<xref
+            linkend="Benutzerauthentifizierung-und-Administratorpasswort"/>"
+            in diesem Kapitel)</para>
+          </listitem>
 
-          <listitem><para><literal>authentication/ldap</literal></para></listitem>
+          <listitem>
+            <para><literal>authentication/database</literal></para>
+          </listitem>
 
-          <listitem><para><literal>system</literal></para></listitem>
+          <listitem>
+            <para><literal>authentication/ldap</literal></para>
+          </listitem>
 
-          <listitem><para><literal>paths</literal></para></listitem>
+          <listitem>
+            <para><literal>system</literal></para>
+          </listitem>
 
-          <listitem><para><literal>mail_delivery</literal> (siehe Abschnitt "<xref linkend="config.sending-email.smtp"/>)</para></listitem>
+          <listitem>
+            <para><literal>paths</literal></para>
+          </listitem>
 
-          <listitem><para><literal>applications</literal></para></listitem>
+          <listitem>
+            <para><literal>mail_delivery</literal> (siehe Abschnitt "<xref
+            linkend="config.sending-email.smtp"/>)</para>
+          </listitem>
 
-          <listitem><para><literal>environment</literal></para></listitem>
+          <listitem>
+            <para><literal>applications</literal></para>
+          </listitem>
 
+          <listitem>
+            <para><literal>environment</literal></para>
+          </listitem>
 
-          <listitem><para><literal>print_templates</literal></para></listitem>
+          <listitem>
+            <para><literal>print_templates</literal></para>
+          </listitem>
 
-          <listitem><para><literal>task_server</literal></para></listitem>
+          <listitem>
+            <para><literal>task_server</literal></para>
+          </listitem>
 
-          <listitem><para><literal>periodic_invoices</literal></para></listitem>
+          <listitem>
+            <para><literal>periodic_invoices</literal></para>
+          </listitem>
 
-          <listitem><para><literal>self_tests</literal></para></listitem>
+          <listitem>
+            <para><literal>self_tests</literal></para>
+          </listitem>
 
-          <listitem><para><literal>console</literal></para></listitem>
+          <listitem>
+            <para><literal>console</literal></para>
+          </listitem>
 
-          <listitem><para><literal>testing</literal></para></listitem>
+          <listitem>
+            <para><literal>testing</literal></para>
+          </listitem>
 
-          <listitem><para><literal>testing/database</literal></para></listitem>
+          <listitem>
+            <para><literal>testing/database</literal></para>
+          </listitem>
 
-          <listitem><para><literal>debug</literal></para></listitem>
+          <listitem>
+            <para><literal>debug</literal></para>
+          </listitem>
         </itemizedlist>
 
         <para>Die üblicherweise wichtigsten Parameter, die am Anfang
@@ -409,29 +836,69 @@ host     = localhost
 port     = 5432
 db       = kivitendo_auth
 user     = postgres
-password =</programlisting>
-
-        <para>Nutzt man wiederkehrende Rechnungen, kann man unter
-        <varname>[periodic_invoices]</varname> den Login eines Benutzers
-        angeben, der nach Erstellung der Rechnungen eine entsprechende E-Mail
-        mit Informationen über die erstellten Rechnungen bekommt.</para>
+password =
 
-        <para>kivitendo bringt eine eigene Komponente zur zeitgesteuerten Ausführung bestimmter Aufgaben mit, den <link
-        linkend="config.task-server">Taskserver</link>. Er wird u.a. für Features wie die <link
-        linkend="features.periodic-invoices">wiederkehrenden Rechnungen</link> benötigt, erledigt aber auch andere erforderliche Aufgaben
-        und muss daher in Betrieb genommen werden. Der Taskserver benötigt zwei Konfigurationseinstellungen, die unter
-        <varname>[task_server]</varname> anzugeben sind: ein Mandant (entweder der Mandantenname oder eine Datenbank-ID, Variable
-        <varname>client</varname>), aus dem die Datenbankkonfiguration entnommen wird, sowie ein Login (Variable <varname>login</varname>)
-        eines Benutzers, der für gewisse Dinge wie die Rechnungserstellung als Verkäufer eingetragen wird.</para>
+[system]
+default_manager = german</programlisting>
 
-        <para>Für Entwickler finden sich unter <varname>[debug]</varname>
-        wichtige Funktionen, um die Fehlersuche zu erleichtern.</para>
-      </sect2>
+        <para>Für kivitendo Installationen in der Schweiz sollte hier
+        <varname>german</varname> durch <varname>swiss</varname> ersetzt
+        werden.</para>
 
-      <sect2 id="config.config-file.prior-versions">
-        <title>Versionen vor 2.6.3</title>
+        <para>Die Einstellung <varname>default_manager = swiss</varname>
+        bewirkt:</para>
 
-        <para>In älteren kivitendo Versionen gab es im Verzeichnis
+        <itemizedlist>
+          <listitem>
+            <para>Beim Erstellen einer neuen Datenbank in der kivitendo
+            Administration werden automatisch die Standard-Werte für die
+            Schweiz voreingestellt: Währung CHF, 5er-Rundung, Schweizer
+            KMU-Kontenplan, Sollversteuerung, Aufwandsmethode, Bilanzierung
+            (die Werte können aber manuell angepasst werden).</para>
+          </listitem>
+
+          <listitem>
+            <para>Einstellen der Standardkonten für Rundungserträge und
+            -aufwendungen (unter Mandantenkonfiguration → Standardkonten
+            veränderbar)</para>
+          </listitem>
+
+          <listitem>
+            <para>das verwendete Zahlenformat wird auf
+            <varname>1'000.00</varname> eingestellt (unter Programm →
+            Benutzereinstellungen veränderbar)</para>
+          </listitem>
+
+          <listitem>
+            <para>DATEV-Automatik und UStVA werden nicht angezeigt,
+            Erfolgsrechnung ersetzt GUV ( unter Mandantenkonfiguration →
+            Features veränderbar)</para>
+          </listitem>
+        </itemizedlist>
+
+        <para>Nutzt man wiederkehrende Rechnungen, kann man unter
+        <varname>[periodic_invoices]</varname> den Login eines Benutzers
+        angeben, der nach Erstellung der Rechnungen eine entsprechende E-Mail
+        mit Informationen über die erstellten Rechnungen bekommt.</para>
+
+        <para>kivitendo bringt eine eigene Komponente zur zeitgesteuerten
+        Ausführung bestimmter Aufgaben mit, den <link
+        linkend="config.task-server">Task-Server</link>. Er wird u.a. für
+        Features wie die <link
+        linkend="features.periodic-invoices">wiederkehrenden Rechnungen</link>
+        benötigt, erledigt aber auch andere erforderliche Aufgaben und muss
+        daher in Betrieb genommen werden. Seine Einrichtung wird im Abschnitt
+        <link linkend="config.task-server">Task-Server</link> genauer
+        beschrieben.</para>
+
+        <para>Für Entwickler finden sich unter <varname>[debug]</varname>
+        wichtige Funktionen, um die Fehlersuche zu erleichtern.</para>
+      </sect2>
+
+      <sect2 id="config.config-file.prior-versions">
+        <title>Versionen vor 2.6.3</title>
+
+        <para>In älteren kivitendo Versionen gab es im Verzeichnis
         <filename>config</filename> die Dateien
         <filename>authentication.pl</filename> und
         <filename>lx-erp.conf</filename>, die jeweils Perl-Dateien waren. Es
@@ -451,15 +918,29 @@ password =</programlisting>
       <title>Anpassung der PostgreSQL-Konfiguration</title>
 
       <para>PostgreSQL muss auf verschiedene Weisen angepasst werden.</para>
-
+      <para>Dies variert je nach eingesetzter Distribution, da distributionsabhängig unterschiedliche Strategien beim Upgrade der Postgres Version eingesetzt werden.
+            Als Hinweis einige Links zu den drei Distribution (Stand Dezember 2018):</para>
+          <itemizedlist>
+            <listitem>
+              <para><ulink url="https://fedoraproject.org/wiki/PostgreSQL">Fedora (Postgres-Installation unter Fedora)</ulink></para>
+            </listitem>
+            <listitem>
+              <para><ulink url="https://help.ubuntu.com/lts/serverguide/postgresql.html">Ubuntu (Infos für Postgres für die aktuelle LTS Version)</ulink></para>
+            </listitem>
+            <listitem>
+              <para><ulink url="https://de.opensuse.org/PostgreSQL">OpenSuSE (aktuell nur bis Version OpenSuSE 13 verifiziert)</ulink></para>
+            </listitem>
+          </itemizedlist>
       <sect2 id="Zeichensätze-die-Verwendung-von-UTF-8">
         <title>Zeichensätze/die Verwendung von Unicode/UTF-8</title>
 
-             <para>kivitendo setzt zwingend voraus, dass die Datenbank Unicode/UTF-8 als Encoding einsetzt. Bei aktuellen Serverinstallationen
-             braucht man hier meist nicht einzugreifen.</para>
+        <para>kivitendo setzt zwingend voraus, dass die Datenbank
+        Unicode/UTF-8 als Encoding einsetzt. Bei aktuellen
+        Serverinstallationen braucht man hier meist nicht einzugreifen.</para>
 
-        <para>Das Encoding des Datenbankservers kann überprüft werden. Ist das Encoding der Datenbank "template1" "Unicode" bzw. "UTF-8", so
-        braucht man nichts weiteres diesbezüglich unternehmen. Zum Testen:</para>
+        <para>Das Encoding des Datenbankservers kann überprüft werden. Ist das
+        Encoding der Datenbank "template1" "Unicode" bzw. "UTF-8", so braucht
+        man nichts weiteres diesbezüglich unternehmen. Zum Testen:</para>
 
         <programlisting>su postgres
 echo '\l' | psql
@@ -498,9 +979,9 @@ exit </programlisting>
         <para>In der Datei <filename>pg_hba.conf</filename>, die im gleichen
         Verzeichnis wie die <filename>postgresql.conf</filename> zu finden
         sein sollte, müssen die Berechtigungen für den Zugriff geändert
-       werden. Hier gibt es mehrere Möglichkeiten. Sinnvoll ist es nur die
-       nötigen Verbindungen immer zuzulassen, für eine lokal laufende
-       Datenbank zum Beispiel:</para>
+        werden. Hier gibt es mehrere Möglichkeiten. Sinnvoll ist es nur die
+        nötigen Verbindungen immer zuzulassen, für eine lokal laufende
+        Datenbank zum Beispiel:</para>
 
         <programlisting>local all kivitendo password
 host all kivitendo 127.0.0.1 255.255.255.255 password</programlisting>
@@ -513,19 +994,50 @@ host all kivitendo 127.0.0.1 255.255.255.255 password</programlisting>
         Unterstützung für servergespeicherte Prozeduren eingerichet werden.
         Melden Sie sich dafür als Benutzer “postgres” an der Datenbank an:
         <programlisting>su - postgres
-psql template1</programlisting>
-
-        führen Sie die folgenden Kommandos aus:</para>
+psql template1</programlisting> führen Sie die folgenden Kommandos aus:</para>
 
         <programlisting>CREATE EXTENSION IF NOT EXISTS plpgsql;
 \q</programlisting>
 
-       <note>
-        <para><literal>CREATE EXTENSION</literal> ist seit Version 9.1 die bevorzugte Syntax um die Sprache <literal>plpgsql</literal> anzulegen. In diesen Versionen ist die Extension meist auch schon vorhanden. Sollten Sie eine ältere Version von Postgres haben, benutzen Sie stattdessen den folgenden Befehl.</para>
-        <programlisting>CREATE LANGUAGE 'plpgsql';
+        <note>
+          <para><literal>CREATE EXTENSION</literal> ist seit Version 9.1 die
+          bevorzugte Syntax um die Sprache <literal>plpgsql</literal>
+          anzulegen. In diesen Versionen ist die Extension meist auch schon
+          vorhanden. Sollten Sie eine ältere Version von Postgres haben,
+          benutzen Sie stattdessen den folgenden Befehl.</para>
+
+          <programlisting>CREATE LANGUAGE 'plpgsql';
+\q</programlisting>
+        </note>
+      </sect2>
+
+      <sect2 id="Erweiterung-für-trigram">
+        <title>Erweiterung für Trigram Prozeduren</title>
+
+        <para>Ab Version 3.5.1 wird die Trigram-Index-Erweiterung benötigt.
+        Diese wird mit dem SQL-Updatescript
+        sql/Pg-upgrade2/trigram_extension.sql und Datenbank-Super-Benutzer
+        Rechten automatisch installiert. Dazu braucht der
+        DatenbankSuperbenutzer "postgres" ein Passwort.</para>
+
+        <programlisting>su - postgres
+psql
+\password postgres
+
+Eingabe Passwort
 \q</programlisting>
-       </note>
 
+        <para>Benutzername Postgres und Passwort können jetzt beim Anlegen
+        einer Datenbank bzw. bei Updatescripten, die SuperuserRechte
+        benötigen, eingegeben werden.</para>
+
+        <note>
+          <para><literal>pg_trgm</literal> ist je nach Distribution nicht im
+          Standard-Paket von Postgres enthalten. Ein <programlisting>select * from pg_available_extensions where name ='pg_trgm';</programlisting>
+          in template1 sollte entsprechend erfolgreich sein. Andernfalls muss
+          das Paket nachinstalliert werden, bspw. bei debian/ubuntu
+          <programlisting>apt install postgresql-contrib</programlisting></para>
+        </note>
       </sect2>
 
       <sect2 id="Datenbankbenutzer-anlegen">
@@ -536,16 +1048,17 @@ psql template1</programlisting>
         anlegen. Ein Beispiel, wie Sie einen neuen Benutzer anlegen
         können:</para>
 
-       <para>Die Frage, ob der neue User Superuser sein soll, können Sie mit nein
-       beantworten, genauso ist die Berechtigung neue User (Roles) zu
-       generieren nicht nötig.</para>
-             <programlisting>su - postgres
+        <para>Die Frage, ob der neue User Superuser sein soll, können Sie mit
+        nein beantworten, genauso ist die Berechtigung neue User (Roles) zu
+        generieren nicht nötig.</para>
+
+        <programlisting>su - postgres
 createuser -d -P kivitendo
 exit</programlisting>
 
         <para>Wenn Sie später einen Datenbankzugriff konfigurieren, verändern
-        Sie den evtl. voreingestellten Benutzer “postgres” auf “kivitendo” bzw.
-        den hier gewählten Benutzernamen.</para>
+        Sie den evtl. voreingestellten Benutzer “postgres” auf “kivitendo”
+        bzw. den hier gewählten Benutzernamen.</para>
       </sect2>
     </sect1>
 
@@ -558,7 +1071,7 @@ exit</programlisting>
         <note>
           <para>Für einen deutlichen Performanceschub sorgt die Ausführung
           mittels FastCGI/FCGI. Die Einrichtung wird ausführlich im Abschnitt
-          <xref linkend="Apache-Konfiguration.FCGI" /> beschrieben.</para>
+          <xref linkend="Apache-Konfiguration.FCGI"/> beschrieben.</para>
         </note>
 
         <para>Der Zugriff auf das Programmverzeichnis muss in der Apache
@@ -576,8 +1089,7 @@ Alias /kivitendo-erp/ /var/www/kivitendo-erp/
 &lt;/Directory&gt;
 
 &lt;Directory /var/www/kivitendo-erp/users&gt;
- Order Deny,Allow
- Deny from All
+ Require all granted
 &lt;/Directory&gt;</programlisting>
 
         <para>Ersetzen Sie dabei die Pfade durch diejenigen, in die Sie vorher
@@ -586,9 +1098,10 @@ Alias /kivitendo-erp/ /var/www/kivitendo-erp/
         <note>
           <para>Vor den einzelnen Optionen muss bei einigen Distributionen ein
           Plus ‘<literal>+</literal>’ gesetzt werden.</para>
-          <para>Bei einigen Distribution (Ubuntu ab 14.04, Debian ab 8.2) muss noch explizit
-          das cgi-Modul mittels <programlisting>a2enmod cgi</programlisting> aktiviert
-          werden.</para>
+
+          <para>Bei einigen Distribution (Ubuntu ab 14.04, Debian ab 8.2) muss
+          noch explizit das cgi-Modul mittels <programlisting>a2enmod cgi</programlisting>
+          aktiviert werden.</para>
         </note>
 
         <para>Auf einigen Webservern werden manchmal die Grafiken und
@@ -643,32 +1156,32 @@ Alias /kivitendo-erp/ /var/www/kivitendo-erp/
 
           <itemizedlist>
             <listitem>
-              <para>Apache 2.2.11 (Ubuntu) und mod_fcgid.</para>
+              <para>Apache 2.4.7 (Ubuntu 14.04.2 LTS) und mod_fcgid.</para>
             </listitem>
-
             <listitem>
-              <para>Apache 2.2.11 / 2.2.22 (Ubuntu) und mod_fastcgi.</para>
+              <para>Apache 2.4.18 (Ubuntu 16.04 LTS) und mod_fcgid</para>
             </listitem>
-
             <listitem>
-              <para>Apache 2.4.7 (Ubuntu 14.04.2 LTS) und mod_fcgid.</para>
+              <para>Apache 2.4.29 (Ubuntu 18.04 LTS) und mod_fcgid</para>
+            </listitem>
+             <listitem>
+              <para>Apache 2.4.41 (Ubuntu 20.04 LTS) und mod_fcgid</para>
             </listitem>
-          </itemizedlist>
 
-          <para>Dabei wird mod_fcgid empfohlen, weil mod_fastcgi seit geraumer
-          Zeit nicht mehr weiter entwickelt wird. Im Folgenden wird auf
-          mod_fastcgi nicht mehr explizit eingegangen.</para>
+          </itemizedlist>
 
           <para>Als Perl Backend wird das Modul <filename>FCGI.pm</filename>
           verwendet.</para>
 
           <warning>
-            <para>FCGI-Versionen ab 0.69 und bis zu 0.71 inklusive sind extrem strict in der Behandlung von Unicode, und verweigern
-            bestimmte Eingaben von kivitendo. Falls es Probleme mit Umlauten in Ihrer Installation gibt, muss zwingend Version 0.68 oder
-            aber Version 0.72 und neuer eingesetzt werden.</para>
+            <para>FCGI-Versionen ab 0.69 und bis zu 0.71 inklusive sind extrem
+            strict in der Behandlung von Unicode, und verweigern bestimmte
+            Eingaben von kivitendo. Falls es Probleme mit Umlauten in Ihrer
+            Installation gibt, muss zwingend Version 0.68 oder aber Version
+            0.72 und neuer eingesetzt werden.</para>
 
-            <para>Mit <ulink url="http://www.cpan.org">CPAN</ulink> lässt sie sich die Vorgängerversion wie folgt
-            installieren:</para>
+            <para>Mit <ulink url="http://www.cpan.org">CPAN</ulink> lässt sie
+            sich die Vorgängerversion wie folgt installieren:</para>
 
             <programlisting>force install M/MS/MSTROUT/FCGI-0.68.tar.gz</programlisting>
           </warning>
@@ -705,22 +1218,35 @@ Alias       /url/for/kivitendo-erp/          /path/to/kivitendo-erp/
 &lt;Directory /path/to/kivitendo-erp&gt;
   AllowOverride All
   Options ExecCGI Includes FollowSymlinks
-  Order Allow,Deny
-  Allow from All
+  Require all granted
 &lt;/Directory&gt;
 
 &lt;DirectoryMatch /path/to/kivitendo-erp/users&gt;
-  Order Deny,Allow
-  Deny from All
+Require all denied
 &lt;/DirectoryMatch&gt;</programlisting>
 
           <warning>
-            <para>Im Vergleich zu Apache 2.2 hat sich in Apache 2.4 die Syntax der Directorydirektiven verändert. Statt</para>
-              <programlisting>
+            <para>Wer einen älteren Apache als Version 2.4 im Einsatz hat,
+            muss entsprechend die Syntax der Directorydirektiven verändert.
+            Statt</para>
+
+            <programlisting>Require all granted</programlisting>
+
+            <para>muß man Folgendes einstellen:</para>
+
+            <programlisting>
   Order Allow,Deny
   Allow from All </programlisting>
-            <para> muß man jetzt Folgendes einstellen:</para>
-              <programlisting>Require all granted</programlisting>
+
+            <para>und statt</para>
+
+            <programlisting>Require all denied</programlisting>
+
+            <para>muss stehen:</para>
+
+            <programlisting>
+  Order Deny,Allow
+  Deny from All </programlisting>
           </warning>
 
           <para>Seit mod_fcgid-Version 2.3.6 gelten sehr kleine Grenzen für
@@ -729,7 +1255,6 @@ Alias       /url/for/kivitendo-erp/          /path/to/kivitendo-erp/
 
           <programlisting>FcgidMaxRequestLen 10485760</programlisting>
 
-
           <para>Das Ganze sollte dann so aussehen:</para>
 
           <programlisting>AddHandler fcgid-script .fpl
@@ -740,13 +1265,11 @@ FcgidMaxRequestLen 10485760
 &lt;Directory /path/to/kivitendo-erp&gt;
   AllowOverride All
   Options ExecCGI Includes FollowSymlinks
-  Order Allow,Deny
-  Allow from All
+  Require all granted
 &lt;/Directory&gt;
 
 &lt;DirectoryMatch /path/to/kivitendo-erp/users&gt;
-  Order Deny,Allow
-  Deny from All
+Require all denied
 &lt;/DirectoryMatch&gt;</programlisting>
 
           <para>Hierdurch wird nur ein zentraler Dispatcher gestartet. Alle
@@ -771,23 +1294,97 @@ Alias       /url/for/kivitendo-erp-fcgid/          /path/to/kivitendo-erp/</prog
           FastCGI-Version.</para>
         </sect3>
       </sect2>
+
+      <sect2>
+       <title>Authentifizierung mittels HTTP Basic Authentication</title>
+
+       <para>
+        Kivitendo unterstützt, dass Benutzerauthentifizierung über den Webserver mittels des »Basic«-HTTP-Authentifizierungs-Schema erfolgt
+        (siehe <ulink url="https://tools.ietf.org/html/rfc7617">RFC 7617</ulink>). Dazu ist es aber nötig, dass der dabei vom Client
+        mitgeschickte Header <constant>Authorization</constant> vom Webserver an Kivitendo über die Umgebungsvariable
+        <constant>HTTP_AUTHORIZATION</constant> weitergegeben wird, was standardmäßig nicht der Fall ist. Für Apache kann dies über die
+        folgende Konfigurationsoption aktiviert werden:
+       </para>
+
+       <programlisting>SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1</programlisting>
+      </sect2>
+      <sect2>
+       <title>Aktivierung von mod_rewrite/directory_match für git basierte Installationen</title>
+
+       <para>
+        Aufgrund von aktuellen (Mitte 2020) Sicherheitswarnungen für git basierte Webanwendungen ist die mitausgelieferte .htaccess
+        restriktiver geworden und verhindert somit das Auslesen von git basierten Daten.
+        Für debian/ubuntu muss das Modul mod_rewrite einmalig so aktiviert werden:
+        <programlisting>a2enmod rewrite</programlisting>
+        Alternativ und für Installationen ohne Apache ist folgender Artikel interessant:
+        <ulink url="https://www.cyberscan.io/blog/git-luecke">git-lücke</ulink>.
+        Anstelle des dort beschriebenen DirectoryMatch für Apache2 würden wir etwas weitergehend auch noch das Verzeichnis config miteinbeziehen
+        sowie ferner auch die Möglichkeit nicht ausschließen, dass es in Unterverzeichnissen auch noch .git Repositories geben kann.
+        Die Empfehlung für Apache 2.4 wäre damit:
+        <programlisting>
+        &lt;DirectoryMatch "/(\.git|config)/"&gt;
+          Require all denied
+        &lt;/DirectoryMatch&gt;</programlisting>
+       </para>
+      </sect2>
+
+
       <sect2>
         <title>Weitergehende Konfiguration</title>
-          <para>Für einen deutlichen Sicherheitsmehrwert sorgt die Ausführung von kivitendo
-          nur über https-verschlüsselten Verbindungen, sowie weiteren Zusatzmassnahmen,
-          wie beispielsweise Basic Authenticate.
-          Die Konfigurationsmöglichkeiten sprengen allerdings den Rahmen dieser Anleitung, hier ein
-          Hinweis auf einen entsprechenden <ulink
-        url="http://redmine.kivitendo-premium.de/boards/1/topics/142">Foreneintrag (Stand Sept. 2015)</ulink></para>
+
+        <para>Für einen deutlichen Sicherheitsmehrwert sorgt die Ausführung
+        von kivitendo nur über https-verschlüsselten Verbindungen, sowie
+        weiteren Zusatzmassnahmen, wie beispielsweise Basic Authenticate. Die
+        Konfigurationsmöglichkeiten sprengen allerdings den Rahmen dieser
+        Anleitung, hier ein Hinweis auf einen entsprechenden <ulink
+        url="http://redmine.kivitendo-premium.de/boards/1/topics/142">Foreneintrag
+        (Stand Sept. 2015)</ulink> und einen aktuellen (Stand Mai 2017) <ulink
+        url="https://mozilla.github.io/server-side-tls/ssl-config-generator/">
+        SSL-Konfigurations-Generator</ulink>.</para>
+      </sect2>
+      <sect2>
+        <title>Aktivierung von Apache2 modsecurity</title>
+
+        <para>Aufgrund des OpenSource Charakters ist kivitendo nicht "out of the box" sicher.
+  Organisatorisch empfehlen wir hier die enge Zusammenarbeit mit einem kivitendo Partner der auch in der
+Lage ist weiterführende Fragen in Bezug auf Datenschutz und Datensicherheit zu beantworten.
+Unabhängig davon empfehlen wir im Webserver Bereich die Aktivierung und Konfiguration des Moduls modsecurity für den Apache2, damit
+XSS und SQL-Injections verhindert werden.</para>
+<para> Als Idee hierfür sei dieser Blog-Eintrag genannt:
+<ulink url="https://doxsec.wordpress.com/2017/06/11/using-modsecurity-web-application-firewall-to-prevent-sql-injection-and-xss-using-blocking-rules/">
+        Test Apache2 modsecurity for SQL Injection</ulink>.</para>
       </sect2>
+
     </sect1>
 
     <sect1 id="config.task-server">
       <title>Der Task-Server</title>
 
-      <para>Der Task-Server ist ein Prozess, der im Hintergrund läuft, in regelmäßigen Abständen nach abzuarbeitenden Aufgaben sucht und
-      diese zu festgelegten Zeitpunkten abarbeitet (ähnlich wie Cron). Dieser Prozess wird u.a. für die Erzeugung der wiederkehrenden
-      Rechnungen und weitere essenzielle Aufgaben benutzt.</para>
+      <para>Der Task-Server ist ein Prozess, der im Hintergrund läuft, in
+      regelmäßigen Abständen nach abzuarbeitenden Aufgaben sucht und diese zu
+      festgelegten Zeitpunkten abarbeitet (ähnlich wie Cron). Dieser Prozess
+      wird u.a. für die Erzeugung der wiederkehrenden Rechnungen und weitere
+      essenzielle Aufgaben benutzt.</para>
+
+      <para>Der Task-Server muss einmalig global in der Konfigurationsdatei
+      konfiguriert werden. Danach wird er für jeden Mandanten, für den er
+      laufen soll, in der Adminsitrationsmaske eingeschaltet.</para>
+
+      <para>Beachten Sie, dass der Task-Server in den Boot-Vorgang Ihres
+      Servers integriert werden muss, damit er automatisch gestartet wird.
+      Dies kann kivitendo nicht für Sie erledigen.</para>
+
+      <para>Da der Task-Server als Perlscript läuft, wird Arbeitsspeicher, der
+      einmal benötigt wurde, nicht mehr an das Betriebssystem zurückgegeben,
+      solange der Task-Server läuft. Dies kann dazu führen, dass ein länger
+      laufender Task-Server mit der Zeit immer mehr Arbeitsspeicher für sich
+      beansprucht. Es ist deshalb sinnvoll, dass der Task-Server in
+      regelmässigen Abständen neu gestartet wird. Allerdings berücksichtigt der
+      Task-Server ein Memory-Limit, wenn dieses in der Konfigurationsdatei
+      angegeben ist. Bei Überschreiten dieses Limits beendet sich der
+      Task-Server. Sofern der Task-Server als systemd-Service mit dem
+      mitgelieferten Skript eingerichtet wurde, startet dieser danach
+      automatisch erneut.</para>
 
       <sect2 id="Konfiguration-des-Task-Servers">
         <title>Verfügbare und notwendige Konfigurationsoptionen</title>
@@ -798,28 +1395,6 @@ Alias       /url/for/kivitendo-erp-fcgid/          /path/to/kivitendo-erp/</prog
         Optionen sind:</para>
 
         <variablelist>
-          <varlistentry>
-            <term><varname>client</varname></term>
-
-            <listitem>
-              <para>Name oder Datenbank-ID eines vorhandenen kivitendo-Mandanten, der benutzt wird, um die zu verwendende
-              Datenbankverbindung auszulesen. Der Mandant muss in der Administration angelegt werden. Diese Option muss angegeben
-              werden.</para>
-
-              <para>Diese Option kam mit Release v3.x.0 hinzu und muss daher in Konfigurationen, die von älteren Versionen aktualisiert
-              wurden, ergänzt werden.</para>
-            </listitem>
-          </varlistentry>
-
-          <varlistentry>
-            <term><varname>login</varname></term>
-
-            <listitem>
-              <para>gültiger kivitendo-Benutzername, der z.B. als Verkäufer beim Erzeugen wiederkehrender Rechnungen benötigt wird. Der
-              Benutzer muss in der Administration angelegt werden. Diese Option muss angegeben werden.</para>
-            </listitem>
-          </varlistentry>
-
           <varlistentry>
             <term><varname>run_as</varname></term>
 
@@ -829,8 +1404,8 @@ Alias       /url/for/kivitendo-erp-fcgid/          /path/to/kivitendo-erp/</prog
               angegebenen Systembenutzer. Der Systembenutzer muss dieselben
               Lese- und Schreibrechte haben, wie auch der Webserverbenutzer
               (siehe see <xref
-              linkend="Manuelle-Installation-des-Programmpaketes" />). Daher
-              ist es sinnvoll, hier denselben Systembenutzer einzutragen,
+              linkend="Manuelle-Installation-des-Programmpaketes"/>). Daher
+              ist es erforderlich, hier denselben Systembenutzer einzutragen,
               unter dem auch der Webserver läuft.</para>
             </listitem>
           </varlistentry>
@@ -845,6 +1420,21 @@ Alias       /url/for/kivitendo-erp-fcgid/          /path/to/kivitendo-erp/</prog
         </variablelist>
       </sect2>
 
+      <sect2 id="Konfiguration-der-Mandanten-fuer-den-Task-Servers">
+        <title>Konfiguration der Mandanten für den Task-Server</title>
+
+        <para>Ist der Task-Server grundlegend konfiguriert, so muss
+        anschließend jeder Mandant, für den der Task-Server laufen soll,
+        einmalig konfiguriert werden. Dazu kann in der Maske zum Bearbeiten
+        von Mandanten im Administrationsbereich eine kivitendo-Benutzerkennung
+        ausgewählt werden, unter der der Task-Server seine Arbeit
+        verrichtet.</para>
+
+        <para>Ist in dieser Einstellung keine Benutzerkennung ausgewählt, so
+        wird der Task-Server für diesen Mandanten keine Aufgaben
+        ausführen.</para>
+      </sect2>
+
       <sect2 id="Einbinden-in-den-Boot-Prozess">
         <title>Automatisches Starten des Task-Servers beim Booten</title>
 
@@ -859,7 +1449,8 @@ Alias       /url/for/kivitendo-erp-fcgid/          /path/to/kivitendo-erp/</prog
         anstelle eines symbolischen Links verwendet werden können.</para>
 
         <sect3>
-          <title>SystemV-basierende Systeme (z.B. Debian, ältere OpenSUSE, ältere Fedora Core)</title>
+          <title>SystemV-basierende Systeme (z.B. ältere Debian, ältere
+          openSUSE, ältere Fedora)</title>
 
           <para>Kopieren Sie die Datei
           <filename>scripts/boot/system-v/kivitendo-task-server</filename>
@@ -873,12 +1464,11 @@ Alias       /url/for/kivitendo-erp-fcgid/          /path/to/kivitendo-erp/</prog
               <para>Debian-basierende Systeme:</para>
 
               <programlisting>update-rc.d kivitendo-task-server defaults
-# Nur bei Debian Squeeze und neuer:
 insserv kivitendo-task-server</programlisting>
             </listitem>
 
             <listitem>
-              <para>Ältere OpenSUSE und ältere Fedora Core:</para>
+              <para>Ältere openSUSE und ältere Fedora:</para>
 
               <programlisting>chkconfig --add kivitendo-task-server</programlisting>
             </listitem>
@@ -891,7 +1481,7 @@ insserv kivitendo-task-server</programlisting>
         </sect3>
 
         <sect3>
-          <title>Upstart-basierende Systeme (z.B. Ubuntu)</title>
+          <title>Upstart-basierende Systeme (z.B. Ubuntu bis 14.04)</title>
 
           <para>Kopieren Sie die Datei
           <filename>scripts/boot/upstart/kivitendo-task-server.conf</filename>
@@ -906,22 +1496,37 @@ insserv kivitendo-task-server</programlisting>
         </sect3>
 
         <sect3>
-          <title>systemd-basierende Systeme (z.B. neure OpenSUSE, neuere Fedora Core)</title>
+          <title>systemd-basierende Systeme (z.B. neure openSUSE, neuere
+          Fedora, neuere Ubuntu und neuere Debians)</title>
 
-          <para>Verlinken Sie die Datei <filename>scripts/boot/systemd/kivitendo-task-server.service</filename> nach
-          <filename>/etc/systemd/system/</filename>. Passen Sie in der kopierten Datei den Pfad zum Task-Server an (Zeile
-          <literal>ExecStart=....</literal> und <literal>ExecStop=...</literal>). Binden Sie das Script in den Boot-Prozess ein.
-          </para>
+          <para>Kopieren Sie die Datei
+          <filename>scripts/boot/systemd/kivitendo-task-server.service</filename>
+          nach <filename>/etc/systemd/system/</filename>. Passen Sie in der
+          kopierten Datei den Pfad zum Task-Server an (Zeilen
+          <literal>ExecStart=....</literal> und
+          <literal>ExecStop=...</literal>).</para>
 
-          <para>Alle hierzu benötigten Befehle sehen so aus:</para>
+          <para>Machen Sie anschließend das Script systemd bekannt, und binden
+          Sie es in den Boot-Prozess ein. Dazu führen Sie die folgenden Befehl
+          aus:</para>
 
-          <programlisting>cd /var/www/kivitendo-erp/scripts/boot/systemd
-ln -s $(pwd)/kivitendo-task-server.service /etc/systemd/system/</programlisting>
+          <programlisting>systemctl daemon-reload
+systemctl enable kivitendo-task-server.service</programlisting>
 
-          <para>Danach kann der Task-Server mit dem folgenden Befehl gestartet
-          werden:</para>
+          <para>Wenn Sie den Task-Server jetzt sofort starten möchten, anstatt
+          den Server neu zu starten, so können Sie das mit dem folgenden
+          Befehl tun:</para>
 
           <programlisting>systemctl start kivitendo-task-server.service</programlisting>
+
+          <para>Ein so eingerichteter Task-Server startet nach Beendigung
+          automatisch erneut. Das betrifft eine Beendigung über die Oberfläche,
+          eine Beendingung über die Prozesskontrolle und eine Beendigung bei
+          Überschreiten des Memory-Limits. Soll der Task-Server nicht erneut
+          starten, so können Sie ihn mit folgendem Befehl stoppen:</para>
+
+          <programlisting>systemctl stop kivitendo-task-server.service</programlisting>
+
         </sect3>
       </sect2>
 
@@ -963,23 +1568,64 @@ ln -s $(pwd)/kivitendo-task-server.service /etc/systemd/system/</programlisting>
 
         <para>Dieselben Optionen können auch für die SystemV-basierenden
         Runlevel-Scripte benutzt werden (siehe oben).</para>
-      </sect2>
-      <sect2 id="Prozesskontrolle2">
-        <title>Task-Server mit mehreren Mandanten</title>
-
-        <para>Beim Task-Server werden der zu verwendende Mandant und Login-Name des Benutzers, unter dem der Task-Server laufen soll, in die
-        Konfigurationsdatei geschrieben. Hat man mehrere Mandanten, muss man auch mehrere Konfigurationsdateien anlegen.</para>
-
-        <para>Die Konfigurationsdatei ist eine Kopie der Datei kivitendo.conf, wo in der Kategorie <varname>[task_server]</varname> die
-        gewünschten Werte für <varname>client</varname> und <varname>login</varname> eingetragen werden.</para>
 
-        <para>Der alternative Task-Server wird dann mit folgendem Befehl
-        gestartet:</para>
+        <para>Wurde der Task-Server als systemd-Service eingerichtet (s.o.),
+        so startet dieser nach Beendigung automatisch erneut.</para>
 
-        <programlisting>./scripts/task_server.pl -c config/DATEINAME.conf</programlisting>
       </sect2>
-    </sect1>
+      <sect2 id="Tasks-konfigurieren">
+        <title>Exemplarische Konfiguration eines Hintergrund-Jobs, der die Jahreszahl in allen Nummernkreisen zum Jahreswechsel erhöht</title>
 
+        <para>Hintergrund-Jobs werden über System -> Hintergrund-Jobs und Task-Server -> Aktuelle Hintergrund-Jobs anzeigen -> Aktions-Knopf 'erfassen' angelegt. </para>
+        <para>Nachdem wir über das Menü dort angelangt sind, legen wir unseren exemplarischen Hintergrund-Jobs "Erhöhung der Nummernkreise" mit folgenden Werten an:</para>
+        <itemizedlist>
+          <listitem>
+            <para><literal>Aktiv:</literal> Hier ein 'Ja' auswählen</para>
+          </listitem>
+          <listitem>
+            <para><literal>Ausführungsart:</literal> 'wiederholte Ausführung' auswählen</para>
+          </listitem>
+          <listitem>
+            <para><literal>Paketname:</literal> 'SetNumberRange' auswählen</para>
+          </listitem>
+          <listitem>
+            <para><literal>Ausführungszeitplan:</literal> Hier entsprechend Werte wie in der crontab eingeben.</para><para>Syntax:</para>
+<programlisting>* * * * *
+┬ ┬ ┬ ┬ ┬
+│ │ │ │ │
+│ │ │ │ └──── Wochentag (0-7, Sonntag ist 0 oder 7)
+│ │ │ └────── Monat (1-12)
+│ │ └──────── Tag (1-31)
+│ └────────── Stunde (0-23)
+└──────────── Minute (0-59)  </programlisting>
+            <para>Die Sterne können folgende Werte haben:</para>
+            <programlisting>
+1 2 3 4 5
+
+1 = Minute (0-59)
+2 = Stunde (0-23)
+3 = Tag (0-31)
+4 = Monat (1-12)
+5 = Wochentag (0-7, Sonntag ist 0 oder 7)
+</programlisting>
+<para>Um die Ausführung auf eine Minute vor Sylvester zu setzen, müssen die folgenden Werte eingetragen werden:</para>
+<programlisting>59 23 31 12 *</programlisting>
+          </listitem>
+          <listitem>
+            <para><literal>Daten:</literal>In diesem Feld können optionale Parameter für den Hintergrund im JSON-Format gesetzt werden. Der Hintergrund-Job <literal>SetNumberRange</literal> akzeptiert zwei Variable nämlich <literal>digit_year</literal> sowieso <literal>multiplier</literal>.</para><para> <literal>digit_year</literal> kann zwei Werte haben entweder 2 oder 4, darüber wird gesteuert ob die Jahreszahl zwei oder vierstellig kodiert wird (für 2019, dann entweder 19 oder 2019). Der Standardwert ist vierstellig.</para><para> <literal>multiplier</literal> ist ein Vielfaches von 10, darüber wird die erste Nummer im Nummernkreis (die Anzahl der Stellen) wie folgt bestimmt:</para>
+<programlisting>
+multiplier     Nummernkreis 2020
+10        ->   20200
+100       ->   202000
+1000      ->   2020000
+</programlisting>
+<para>Wir gehen jetzt beispielhaft von einer letzten Rechnungsnummer von RE2019456 aus. Demnach sollte ab Januar 2020 die erste Nummer RE2020001 sein. Da der Task auch Präfixe berücksichtigt, kann dies mit folgenden JSON-kodierten Werten umgesetzt werden:</para>
+<para><literal>Daten:</literal></para><programlisting>multiplier: 100
+digits_year: 4</programlisting>
+          </listitem>
+        </itemizedlist>
+     </sect2>
+    </sect1>
     <sect1 id="Benutzerauthentifizierung-und-Administratorpasswort">
       <title>Benutzerauthentifizierung und Administratorpasswort</title>
 
@@ -999,9 +1645,8 @@ ln -s $(pwd)/kivitendo-task-server.service /etc/systemd/system/</programlisting>
         Datenbank, in der sowohl die Benutzerinformationen als auch die Daten
         abgelegt werden.</para>
 
-        <para>Zusätzlich ermöglicht es kivitendo, dass die Benutzerpasswörter
-        entweder gegen die Authentifizierungsdatenbank oder gegen einen
-        LDAP-Server überprüft werden.</para>
+        <para>Zusätzlich ermöglicht es kivitendo, dass die Benutzerpasswörter gegen die Authentifizierungsdatenbank oder gegen einen oder
+        mehrere LDAP-Server überprüft werden.</para>
 
         <para>Welche Art der Passwortüberprüfung kivitendo benutzt und wie
         kivitendo die Authentifizierungsdatenbank erreichen kann, wird in der
@@ -1016,11 +1661,12 @@ ln -s $(pwd)/kivitendo-task-server.service /etc/systemd/system/</programlisting>
       <sect2 id="Administratorpasswort">
         <title>Administratorpasswort</title>
 
-        <para>Das Passwort, das zum Zugriff auf das Administrationsinterface von kivitendo
-        benutzt wird, wird ebenfalls in dieser Datei gespeichert. Es kann auch
-        nur dort und nicht mehr im Administrationsinterface selber geändert
-        werden. Der Parameter dazu heißt <varname>admin_password</varname> im
-        Abschnitt <varname>[authentication]</varname>.</para>
+        <para>Das Passwort, das zum Zugriff auf das Administrationsinterface
+        von kivitendo benutzt wird, wird ebenfalls in dieser Datei
+        gespeichert. Es kann auch nur dort und nicht mehr im
+        Administrationsinterface selber geändert werden. Der Parameter dazu
+        heißt <varname>admin_password</varname> im Abschnitt
+        <varname>[authentication]</varname>.</para>
       </sect2>
 
       <sect2 id="Authentifizierungsdatenbank">
@@ -1083,22 +1729,28 @@ ln -s $(pwd)/kivitendo-task-server.service /etc/systemd/system/</programlisting>
         <title>Passwortüberprüfung</title>
 
         <para>kivitendo unterstützt Passwortüberprüfung auf zwei Arten: gegen
-        die Authentifizierungsdatenbank und gegen einen externen LDAP- oder
+        die Authentifizierungsdatenbank und gegen externe LDAP- oder
         Active-Directory-Server. Welche davon benutzt wird, regelt der
         Parameter <varname>module</varname> im Abschnitt
         <varname>[authentication]</varname>.</para>
 
-        <para>Sollen die Benutzerpasswörter in der Authentifizierungsdatenbank
-        gespeichert werden, so muss der Parameter <varname>module</varname>
-        den Wert <literal>DB</literal> enthalten. In diesem Fall können sowohl
-        der Administrator als auch die Benutzer selber ihre Passwörter in
-        kivitendo ändern.</para>
+        <para>Dieser Parameter listet die zu verwendenden Authentifizierungsmodule auf. Es muss mindestens ein Modul angegeben werden, es
+        können aber auch mehrere angegeben werden. Weiterhin ist es möglich, das LDAP-Modul mehrfach zu verwenden und für jede Verwendung
+        eine unterschiedliche Konfiguration zu nutzen, z.B. um einen Fallback-Server anzugeben, der benutzt wird, sofern der Hauptserver
+        nicht erreichbar ist.</para>
+
+        <para>Sollen die Benutzerpasswörter in der Authentifizierungsdatenbank geprüft werden, so muss der Parameter
+        <varname>module</varname> das Modul <literal>DB</literal> enthalten. Sofern das Modul in der Liste enthalten ist, egal an welcher
+        Position, können sowohl der Administrator als auch die Benutzer selber ihre Passwörter in kivitendo ändern.</para>
+
+        <para>Wenn Passwörter gegen einen oder mehrere externe LDAP- oder Active-Directory-Server geprüft werden, so muss der Parameter
+        <varname>module</varname> den Wert <literal>LDAP</literal> enthalten. In diesem Fall müssen zusätzliche Informationen über den
+        LDAP-Server im Abschnitt <literal>[authentication/ldap]</literal> angegeben werden. Das Modul kann auch mehrfach angegeben werden,
+        wobei jedes Modul eine eigene Konfiguration bekommen sollte. Der Name der Konfiguration wird dabei mit einem Doppelpunkt getrennt an
+        den Modulnamen angehängt (<literal>LDAP:Name-der-Konfiguration</literal>). Der entsprechende Abschnitt in der Konfigurationsdatei
+        lautet dann <literal>[authentication/Name-der-Konfiguration]</literal>.</para>
 
-        <para>Soll hingegen ein externer LDAP- oder Active-Directory-Server
-        benutzt werden, so muss der Parameter <varname>module</varname> auf
-        <literal>LDAP</literal> gesetzt werden. In diesem Fall müssen
-        zusätzliche Informationen über den LDAP-Server im Abschnitt
-        <literal>[authentication/ldap]</literal> angegeben werden:</para>
+        <para>Die verfügbaren Parameter für die LDAP-Konfiguration lauten:</para>
 
         <variablelist>
           <varlistentry>
@@ -1129,6 +1781,17 @@ ln -s $(pwd)/kivitendo-task-server.service /etc/systemd/system/</programlisting>
             </listitem>
           </varlistentry>
 
+          <varlistentry>
+            <term><literal>verify</literal></term>
+
+            <listitem>
+              <para>Wenn Verbindungsverschlüsselung gewünscht und der Parameter <parameter>tls</parameter> gesetzt ist, so gibt dieser
+              Parameter an, ob das Serverzertifikat auf Gültigkeit geprüft wird. Mögliche Werte sind <literal>require</literal> (Zertifikat
+              wird überprüft und muss gültig sei; dies ist der Standard) und <literal>none</literal> (Zertifikat wird nicht
+              überpfüft).</para>
+            </listitem>
+          </varlistentry>
+
           <varlistentry>
             <term><literal>attribute</literal></term>
 
@@ -1178,6 +1841,14 @@ ln -s $(pwd)/kivitendo-task-server.service /etc/systemd/system/</programlisting>
               also ‘<literal>Martin Mustermann</literal>’.</para>
             </listitem>
           </varlistentry>
+
+          <varlistentry>
+            <term><literal>timeout</literal></term>
+
+            <listitem>
+              <para>Timeout beim Verbindungsversuch, bevor der Server als nicht erreichbar gilt; Standardwert: 10</para>
+            </listitem>
+          </varlistentry>
         </variablelist>
       </sect2>
 
@@ -1211,8 +1882,9 @@ ln -s $(pwd)/kivitendo-task-server.service /etc/systemd/system/</programlisting>
     <sect1 id="Benutzer--und-Gruppenverwaltung">
       <title>Mandanten-, Benutzer- und Gruppenverwaltung</title>
 
-      <para>Nach der Installation müssen Mandanten, Benutzer, Gruppen und Datenbanken angelegt werden. Dieses geschieht im
-      Administrationsmenü, das Sie unter folgender URL finden:</para>
+      <para>Nach der Installation müssen Mandanten, Benutzer, Gruppen und
+      Datenbanken angelegt werden. Dieses geschieht im Administrationsmenü,
+      das Sie unter folgender URL finden:</para>
 
       <para><ulink
       url="http://localhost/kivitendo-erp/controller.pl?action=Admin/login">http://localhost/kivitendo-erp/controller.pl?action=Admin/login</ulink></para>
@@ -1223,45 +1895,65 @@ ln -s $(pwd)/kivitendo-task-server.service /etc/systemd/system/</programlisting>
       <sect2 id="Zusammenhänge">
         <title>Zusammenhänge</title>
 
-        <para>kivitendo verwaltet zwei Sets von Daten, die je nach Einrichtung in einer oder zwei Datenbanken gespeichert werden.</para>
-
-        <para>Das erste Set besteht aus Anmeldeinformationen: welche Benutzer und Mandanten gibt es, welche Gruppen, welche BenutzerIn hat
-        Zugriff auf welche Mandanten, und welche Gruppe verfügt über welche Rechte. Diese Informationen werden in der
-        Authentifizierungsdatenbank gespeichert. Dies ist diejenige Datenbank, deren Verbindungsparameter in der Konfigurationsdatei
-        <filename>config/kivitendo.conf</filename> gespeichert werden.</para>
-
-        <para>Das zweite Set besteht aus den eigentlichen Verkehrsdaten eines Mandanten, wie beispielsweise die Stammdaten (Kunden, Lieferanten, Waren) und Belege
-        (Angebote, Lieferscheine, Rechnungen). Diese werden in einer Mandantendatenbank gespeichert. Die
-        Verbindungsinformationen einer solchen Mandantendatenbank werden im Administrationsbereich konfiguriert, indem man einen Mandanten
-        anlegt und dort die Parameter einträgt. Dabei hat jeder Mandant eine eigene Datenbank.</para>
-
-        <para>Aufgrund des Datenbankdesigns ist es für einfache Fälle möglich, die Authentifizierungsdatenbank und eine der
-        Mandantendatenbanken in ein und derselben Datenbank zu speichern. Arbeitet man hingegen mit mehr als einem Mandanten, wird
-        empfohlen, für die Authentifizierungsdatenbank eine eigene Datenbank zu verwenden, die nicht gleichzeitig für einen Mandanten
-        verwendet wird.</para>
-     </sect2>
+        <para>kivitendo verwaltet zwei Sets von Daten, die je nach Einrichtung
+        in einer oder zwei Datenbanken gespeichert werden.</para>
+
+        <para>Das erste Set besteht aus Anmeldeinformationen: welche Benutzer
+        und Mandanten gibt es, welche Gruppen, welche BenutzerIn hat Zugriff
+        auf welche Mandanten, und welche Gruppe verfügt über welche Rechte.
+        Diese Informationen werden in der Authentifizierungsdatenbank
+        gespeichert. Dies ist diejenige Datenbank, deren Verbindungsparameter
+        in der Konfigurationsdatei <filename>config/kivitendo.conf</filename>
+        gespeichert werden.</para>
+
+        <para>Das zweite Set besteht aus den eigentlichen Verkehrsdaten eines
+        Mandanten, wie beispielsweise die Stammdaten (Kunden, Lieferanten,
+        Waren) und Belege (Angebote, Lieferscheine, Rechnungen). Diese werden
+        in einer Mandantendatenbank gespeichert. Die Verbindungsinformationen
+        einer solchen Mandantendatenbank werden im Administrationsbereich
+        konfiguriert, indem man einen Mandanten anlegt und dort die Parameter
+        einträgt. Dabei hat jeder Mandant eine eigene Datenbank.</para>
+
+        <para>Aufgrund des Datenbankdesigns ist es für einfache Fälle möglich,
+        die Authentifizierungsdatenbank und eine der Mandantendatenbanken in
+        ein und derselben Datenbank zu speichern. Arbeitet man hingegen mit
+        mehr als einem Mandanten, wird empfohlen, für die
+        Authentifizierungsdatenbank eine eigene Datenbank zu verwenden, die
+        nicht gleichzeitig für einen Mandanten verwendet wird.</para>
+      </sect2>
 
       <sect2 id="Mandanten-Benutzer-Gruppen">
         <title>Mandanten, Benutzer und Gruppen</title>
 
-        <para>kivitendos Administration kennt Mandanten, Benutzer und Gruppen, die sich frei zueinander zuordnen lassen.</para>
-
-        <para>kivitendo kann mehrere Mandaten aus einer Installation heraus verwalten. Welcher Mandant benutzt wird, kann direkt beim Login
-        ausgewählt werden. Für jeden Mandanten wird ein eindeutiger Name vergeben, der beim Login angezeigt wird. Weiterhin benötigt der
-        Mandant Datenbankverbindungsparameter für seine Mandantendatenbank. Diese sollte über die <link
-        linkend="Datenbanken-anlegen">Datenbankverwaltung</link> geschehen.</para>
-
-        <para>Ein Benutzer ist eine Person, die Zugriff auf kivitendo erhalten soll. Sie erhält einen Loginnamen sowie ein
-        Passwort. Weiterhin legt der Administrator fest, an welchen Mandanten sich ein Benutzer anmelden kann, was beim Login verifiziert
-        wird.</para>
-
-        <para>Gruppen dienen dazu, Benutzern innerhalb eines Mandanten Zugriff auf bestimmte Funktionen zu geben. Einer Gruppe werden dafür
-        vom Administrator gewisse Rechte zugeordnet. Weiterhin legt der Administrator fest, für welche Mandanten eine Gruppe gilt, und
-        welche Benutzer Mitglieder in dieser Gruppe sind. Meldet sich ein Benutzer dann an einem Mandanten an, so erhält er alle Rechte von
-        allen denjenigen Gruppen, die zum Einen dem Mandanten zugeordnet sind und in denen der Benutzer zum Anderen Mitglied ist, </para>
-
-        <para>Die Reihenfolge, in der Datenbanken, Mandanten, Gruppen und Benutzer angelegt werden, kann im Prinzip beliebig gewählt
-        werden. Die folgende Reihenfolge beinhaltet die wenigsten Arbeitsschritte:</para>
+        <para>kivitendos Administration kennt Mandanten, Benutzer und Gruppen,
+        die sich frei zueinander zuordnen lassen.</para>
+
+        <para>kivitendo kann mehrere Mandaten aus einer Installation heraus
+        verwalten. Welcher Mandant benutzt wird, kann direkt beim Login
+        ausgewählt werden. Für jeden Mandanten wird ein eindeutiger Name
+        vergeben, der beim Login angezeigt wird. Weiterhin benötigt der
+        Mandant Datenbankverbindungsparameter für seine Mandantendatenbank.
+        Diese sollte über die <link
+        linkend="Datenbanken-anlegen">Datenbankverwaltung</link>
+        geschehen.</para>
+
+        <para>Ein Benutzer ist eine Person, die Zugriff auf kivitendo erhalten
+        soll. Sie erhält einen Loginnamen sowie ein Passwort. Weiterhin legt
+        der Administrator fest, an welchen Mandanten sich ein Benutzer
+        anmelden kann, was beim Login verifiziert wird.</para>
+
+        <para>Gruppen dienen dazu, Benutzern innerhalb eines Mandanten Zugriff
+        auf bestimmte Funktionen zu geben. Einer Gruppe werden dafür vom
+        Administrator gewisse Rechte zugeordnet. Weiterhin legt der
+        Administrator fest, für welche Mandanten eine Gruppe gilt, und welche
+        Benutzer Mitglieder in dieser Gruppe sind. Meldet sich ein Benutzer
+        dann an einem Mandanten an, so erhält er alle Rechte von allen
+        denjenigen Gruppen, die zum Einen dem Mandanten zugeordnet sind und in
+        denen der Benutzer zum Anderen Mitglied ist,</para>
+
+        <para>Die Reihenfolge, in der Datenbanken, Mandanten, Gruppen und
+        Benutzer angelegt werden, kann im Prinzip beliebig gewählt werden. Die
+        folgende Reihenfolge beinhaltet die wenigsten Arbeitsschritte:</para>
 
         <orderedlist numeration="arabic">
           <listitem>
@@ -1298,479 +1990,474 @@ ln -s $(pwd)/kivitendo-task-server.service /etc/systemd/system/</programlisting>
         Anlegen können Sie die verschiedenen Bereiche wählen, auf die
         Mitglieder dieser Gruppe Zugriff haben sollen.</para>
 
-        <para>Benutzergruppen werden zwar in der Authentifizierungsdatenbank gespeichert, gelten aber nicht automatisch für alle
-        Mandanten. Der Administrator legt vielmehr fest, für welche Mandanten eine Gruppe gültig ist. Dies kann entweder beim Bearbeiten der
-        Gruppe geschehen ("diese Gruppe ist gültig für Mandanten X, Y und Z"), oder aber wenn man einen Mandanten bearbeitet ("für diesen
-        Mandanten sind die Gruppen A, B und C gültig").</para>
-
-        <para>Wurden bereits Benutzer angelegt, so können hier die Mitglieder dieser Gruppe festgelegt werden ("in dieser Gruppe sind die
-        Benutzer X, Y und Z Mitglieder"). Dies kann auch nachträglich beim Bearbeiten eines Benutzers geschehen ("dieser Benutzer ist
-        Mitglied in den Gruppen A, B und C").</para>
+        <para>Benutzergruppen werden zwar in der Authentifizierungsdatenbank
+        gespeichert, gelten aber nicht automatisch für alle Mandanten. Der
+        Administrator legt vielmehr fest, für welche Mandanten eine Gruppe
+        gültig ist. Dies kann entweder beim Bearbeiten der Gruppe geschehen
+        ("diese Gruppe ist gültig für Mandanten X, Y und Z"), oder aber wenn
+        man einen Mandanten bearbeitet ("für diesen Mandanten sind die Gruppen
+        A, B und C gültig").</para>
+
+        <para>Wurden bereits Benutzer angelegt, so können hier die Mitglieder
+        dieser Gruppe festgelegt werden ("in dieser Gruppe sind die Benutzer
+        X, Y und Z Mitglieder"). Dies kann auch nachträglich beim Bearbeiten
+        eines Benutzers geschehen ("dieser Benutzer ist Mitglied in den
+        Gruppen A, B und C").</para>
       </sect2>
 
       <sect2 id="Benutzer-anlegen">
         <title>Benutzer anlegen</title>
 
-        <para>Beim Anlegen von Benutzern werden für viele Parameter Standardeinstellungen vorgenommen, die den Gepflogenheiten des deutschen
-        Raumes entsprechen.</para>
+        <para>Beim Anlegen von Benutzern werden für viele Parameter
+        Standardeinstellungen vorgenommen, die den Gepflogenheiten des
+        deutschen Raumes entsprechen.</para>
 
-        <para>Zwingend anzugeben ist der Loginname. Wenn die Passwortauthentifizierung über die Datenbank eingestellt ist, so kann hier auch
-        das Benutzerpasswort gesetzt bzw. geändert werden. Ist hingegen die LDAP-Authentifizierung aktiv, so ist das Passwort-Feld
+        <para>Zwingend anzugeben ist der Loginname. Wenn die
+        Passwortauthentifizierung über die Datenbank eingestellt ist, so kann
+        hier auch das Benutzerpasswort gesetzt bzw. geändert werden. Ist
+        hingegen die LDAP-Authentifizierung aktiv, so ist das Passwort-Feld
         deaktiviert.</para>
 
-        <para>Hat man bereits Mandanten und Gruppen angelegt, so kann hier auch konfiguriert werden, auf welche Mandanten der Benutzer
-        Zugriff hat bzw. in welchen Gruppen er Mitglied ist. Beide Zuweisungen können sowohl beim Benutzer vorgenommen werden ("dieser
-        Benutzer hat Zugriff auf Mandanten X, Y, Z" bzw. "dieser Benutzer ist Mitglied in Gruppen X, Y und Z") als auch beim Mandanten ("auf
-        diesen Mandanten haben Benutzer A, B und C Zugriff") bzw. bei der Gruppe ("in dieser Gruppe sind Benutzer A, B und C
-        Mitglieder").</para>
+        <para>Hat man bereits Mandanten und Gruppen angelegt, so kann hier
+        auch konfiguriert werden, auf welche Mandanten der Benutzer Zugriff
+        hat bzw. in welchen Gruppen er Mitglied ist. Beide Zuweisungen können
+        sowohl beim Benutzer vorgenommen werden ("dieser Benutzer hat Zugriff
+        auf Mandanten X, Y, Z" bzw. "dieser Benutzer ist Mitglied in Gruppen
+        X, Y und Z") als auch beim Mandanten ("auf diesen Mandanten haben
+        Benutzer A, B und C Zugriff") bzw. bei der Gruppe ("in dieser Gruppe
+        sind Benutzer A, B und C Mitglieder").</para>
       </sect2>
 
       <sect2 id="Mandanten-anlegen">
         <title>Mandanten anlegen</title>
 
-        <para>Ein Mandant besteht aus Administrationssicht primär aus einem eindeutigen Namen. Weiterhin wird hier hinterlegt, welche
-        Datenbank als Mandantendatenbank benutzt wird. Hier müssen die Zugriffsdaten einer der eben angelegten Datenbanken eingetragen
-        werden.</para>
-
-        <para>Hat man bereits Benutzer und Gruppen angelegt, so kann hier auch konfiguriert werden, welche Benutzer Zugriff auf den
-        Mandanten haben bzw. welche Gruppen für den Mandanten gültig sind. Beide Zuweisungen können sowohl beim Mandanten vorgenommen werden
-        ("auf diesen Mandanten haben Benutzer X, Y und Z Zugriff" bzw. "für diesen Mandanten sind die Gruppen X, Y und Z gültig") als auch
-        beim Benutzer ("dieser Benutzer hat Zugriff auf Mandanten A, B und C") bzw. bei der Gruppe ("diese Gruppe ist für Mandanten A, B und
-        C gültig").</para>
+        <para>Ein Mandant besteht aus Administrationssicht primär aus einem
+        eindeutigen Namen. Weiterhin wird hier hinterlegt, welche Datenbank
+        als Mandantendatenbank benutzt wird. Hier müssen die Zugriffsdaten
+        einer der eben angelegten Datenbanken eingetragen werden.</para>
+
+        <para>Hat man bereits Benutzer und Gruppen angelegt, so kann hier auch
+        konfiguriert werden, welche Benutzer Zugriff auf den Mandanten haben
+        bzw. welche Gruppen für den Mandanten gültig sind. Beide Zuweisungen
+        können sowohl beim Mandanten vorgenommen werden ("auf diesen Mandanten
+        haben Benutzer X, Y und Z Zugriff" bzw. "für diesen Mandanten sind die
+        Gruppen X, Y und Z gültig") als auch beim Benutzer ("dieser Benutzer
+        hat Zugriff auf Mandanten A, B und C") bzw. bei der Gruppe ("diese
+        Gruppe ist für Mandanten A, B und C gültig").</para>
       </sect2>
     </sect1>
+
     <sect1 id="Drucker--Systemverwaltung">
       <title>Drucker- und Systemverwaltung</title>
-      <para>Im Administrationsmenü gibt es ferner noch die beiden Menüpunkte Druckeradministration und System.</para>
+
+      <para>Im Administrationsmenü gibt es ferner noch die beiden Menüpunkte
+      Druckeradministration und System.</para>
+
       <sect2 id="Druckeradministration">
         <title>Druckeradministration</title>
-      <para>Unter dem Menüpunkt Druckeradministration lassen sich beliebig viele "Druckbefehle" im System verwalten. Diese Befehle werden mandantenweise
-      zugeordnet. Unter Druckerbeschreibung wird der Namen des Druckbefehls festgelegt, der dann in der Druckerauswahl des Belegs angezeigt wird.</para>
-      <para>Unter Druckbefehl definiert man den eigentlichen Druckbefehl, der direkt auf dem Webserver ausgeführt wird, bspw. 'lpr -P meinDrucker' oder ein
-      kompletter Pfad zu einem Skript (/usr/local/src/kivitendo/scripts/pdf_druck_in_verzeichnis.sh).
-      Wird ferner noch ein optionales Vorlagenkürzel verwendet, wird dieses Kürzel bei der Auswahl der Druckvorlagendatei mit einem Unterstrich ergänzt, ist
-      bspw. das Kürzel 'epson_drucker' definiert, so wird beim Ausdruck eines Angebots folgende Vorlage geparst: sales_quotation_epson_drucker.tex.</para>
-     </sect2>
+
+        <para>Unter dem Menüpunkt Druckeradministration lassen sich beliebig
+        viele "Druckbefehle" im System verwalten. Diese Befehle werden
+        mandantenweise zugeordnet. Unter Druckerbeschreibung wird der Namen
+        des Druckbefehls festgelegt, der dann in der Druckerauswahl des Belegs
+        angezeigt wird.</para>
+
+        <para>Unter Druckbefehl definiert man den eigentlichen Druckbefehl,
+        der direkt auf dem Webserver ausgeführt wird, bspw. 'lpr -P
+        meinDrucker' oder ein kompletter Pfad zu einem Skript
+        (/usr/local/src/kivitendo/scripts/pdf_druck_in_verzeichnis.sh). Wird
+        ferner noch ein optionales Vorlagenkürzel verwendet, wird dieses
+        Kürzel bei der Auswahl der Druckvorlagendatei mit einem Unterstrich
+        ergänzt, ist bspw. das Kürzel 'epson_drucker' definiert, so wird beim
+        Ausdruck eines Angebots folgende Vorlage geparst:
+        sales_quotation_epson_drucker.tex.</para>
+      </sect2>
+
       <sect2 id="System">
         <title>System sperren / entsperren</title>
 
-        <para>Unter dem Menüpunkt System gibt es den Eintrag 'Installation sperren/entsperren'. Setzt man diese Sperre so ist der Zugang zu der gesamten kivitendo Installation gesperrt.</para>
-        <para>Falls die Sperre gesetzt ist, erscheint anstelle der Anmeldemaske die Information: 'kivitendo ist momentan zwecks Wartungsarbeiten nicht zugänglich.'.
-        </para>
-        <para>Wichtig zu erwähnen ist hierbei noch, dass sich kivitendo automatisch 'sperrt', falls es bei einem Versionsupdate zu einem Datenbankfehler kam. Somit kann hier nicht aus Versehen
-        mit einem inkonsistenten Datenbestand weitergearbeitet werden.</para>
-     </sect2>
-    </sect1>
+        <para>Unter dem Menüpunkt System gibt es den Eintrag 'Installation
+        sperren/entsperren'. Setzt man diese Sperre so ist der Zugang zu der
+        gesamten kivitendo Installation gesperrt.</para>
 
-    <sect1 id="config.sending-email" xreflabel="E-Mail-Versand aus kivitendo heraus">
-      <title>E-Mail-Versand aus kivitendo heraus</title>
+        <para>Falls die Sperre gesetzt ist, erscheint anstelle der
+        Anmeldemaske die Information: 'kivitendo ist momentan zwecks
+        Wartungsarbeiten nicht zugänglich.'.</para>
 
-      <para>kivitendo kann direkt aus dem Programm heraus E-Mails versenden, z.B. um ein Angebot direkt an einen Kunden zu
-      verschicken. Damit dies funktioniert, muss eingestellt werden, über welchen Server die E-Mails verschickt werden sollen. kivitendo
-      unterstützt dabei zwei Mechanismen: Versand über einen lokalen E-Mail-Server (z.B. mit <productname>Postfix</productname> oder
-      <productname>Exim</productname>, was auch die standardmäßig aktive Methode ist) sowie Versand über einen SMTP-Server (z.B. der des
-      eigenen Internet-Providers).</para>
+        <para>Wichtig zu erwähnen ist hierbei noch, dass sich kivitendo
+        automatisch 'sperrt', falls es bei einem Versionsupdate zu einem
+        Datenbankfehler kam. Somit kann hier nicht aus Versehen mit einem
+        inkonsistenten Datenbestand weitergearbeitet werden.</para>
+      </sect2>
+    </sect1>
 
-      <para>Welche Methode und welcher Server verwendet werden, wird über die Konfigurationsdatei <filename>config/kivitendo.conf</filename>
-      festgelegt. Dort befinden sich alle Einstellungen zu diesem Thema im Abschnitt '<literal>[mail_delivery]</literal>'.</para>
+    <sect1 id="config.sending-email"
+           xreflabel="E-Mail-Versand aus kivitendo heraus">
+      <title>E-Mail-Versand aus kivitendo heraus</title>
 
-      <sect2 id="config.sending-email.sendmail" xreflabel="E-Mail-Versand über lokalen E-Mail-Server">
+      <para>kivitendo kann direkt aus dem Programm heraus E-Mails versenden,
+      z.B. um ein Angebot direkt an einen Kunden zu verschicken. Damit dies
+      funktioniert, muss eingestellt werden, über welchen Server die E-Mails
+      verschickt werden sollen. kivitendo unterstützt dabei zwei Mechanismen:
+      Versand über einen lokalen E-Mail-Server (z.B. mit
+      <productname>Postfix</productname> oder <productname>Exim</productname>,
+      was auch die standardmäßig aktive Methode ist) sowie Versand über einen
+      SMTP-Server (z.B. der des eigenen Internet-Providers).</para>
+
+      <para>Welche Methode und welcher Server verwendet werden, wird über die
+      Konfigurationsdatei <filename>config/kivitendo.conf</filename>
+      festgelegt. Dort befinden sich alle Einstellungen zu diesem Thema im
+      Abschnitt '<literal>[mail_delivery]</literal>'.</para>
+
+      <sect2 id="config.sending-email.sendmail"
+             xreflabel="E-Mail-Versand über lokalen E-Mail-Server">
         <title>Versand über lokalen E-Mail-Server</title>
 
-        <para>Diese Methode bietet sich an, wenn auf dem Server, auf dem kivitendo läuft, bereits ein funktionsfähiger E-Mail-Server wie
-        z.B. <productname>Postfix</productname>, <productname>Exim</productname> oder <productname>Sendmail</productname> läuft.</para>
+        <para>Diese Methode bietet sich an, wenn auf dem Server, auf dem
+        kivitendo läuft, bereits ein funktionsfähiger E-Mail-Server wie z.B.
+        <productname>Postfix</productname>, <productname>Exim</productname>
+        oder <productname>Sendmail</productname> läuft.</para>
 
-        <para>Um diese Methode auszuwählen, muss der Konfigurationsparameter '<literal>method = sendmail</literal>' gesetzt sein. Dies ist
+        <para>Um diese Methode auszuwählen, muss der Konfigurationsparameter
+        '<literal>method = sendmail</literal>' gesetzt sein. Dies ist
         gleichzeitig der Standardwert, falls er nicht verändert wird.</para>
 
-        <para>Um zu kontrollieren, wie das Programm zum Einliefern gestartet wird, dient der Parameter '<literal>sendmail =
-        ...</literal>'. Der Standardwert verweist auf das Programm <filename>/usr/bin/sendmail</filename>, das bei allen oben genannten
+        <para>Um zu kontrollieren, wie das Programm zum Einliefern gestartet
+        wird, dient der Parameter '<literal>sendmail = ...</literal>'. Der
+        Standardwert verweist auf das Programm
+        <filename>/usr/bin/sendmail</filename>, das bei allen oben genannten
         E-Mail-Serverprodukten für diesen Zweck funktionieren sollte.</para>
 
-        <para>Die Konfiguration des E-Mail-Servers selber würde den Rahmen dieses sprengen. Hierfür sei auf die Dokumentation des
-        E-Mail-Servers verwiesen.</para>
+        <para>Die Konfiguration des E-Mail-Servers selber würde den Rahmen
+        dieses sprengen. Hierfür sei auf die Dokumentation des E-Mail-Servers
+        verwiesen.</para>
       </sect2>
 
-      <sect2 id="config.sending-email.smtp" xreflabel="E-Mail-Versand über einen SMTP-Server">
+      <sect2 id="config.sending-email.smtp"
+             xreflabel="E-Mail-Versand über einen SMTP-Server">
         <title>Versand über einen SMTP-Server</title>
 
-        <para>Diese Methode bietet sich an, wenn kein lokaler E-Mail-Server vorhanden oder zwar einer vorhanden, dieser aber nicht
-        konfiguriert ist.</para>
+        <para>Diese Methode bietet sich an, wenn kein lokaler E-Mail-Server
+        vorhanden oder zwar einer vorhanden, dieser aber nicht konfiguriert
+        ist.</para>
 
-        <para>Um diese Methode auszuwählen, muss der Konfigurationsparameter '<literal>method = smtp</literal>' gesetzt sein. Die folgenden
+        <para>Um diese Methode auszuwählen, muss der Konfigurationsparameter
+        '<literal>method = smtp</literal>' gesetzt sein. Die folgenden
         Parameter dienen dabei der weiteren Konfiguration:</para>
 
         <variablelist>
           <varlistentry>
             <term><varname>hostname</varname></term>
 
-            <listitem><para>Name oder IP-Adresse des SMTP-Servers. Standardwert: '<literal>localhost</literal>'</para></listitem>
+            <listitem>
+              <para>Name oder IP-Adresse des SMTP-Servers. Standardwert:
+              '<literal>localhost</literal>'</para>
+            </listitem>
           </varlistentry>
 
           <varlistentry>
             <term><varname>port</varname></term>
 
-            <listitem><para>Portnummer. Der Standardwert hängt von der verwendeten Verschlüsselungsmethode ab. Gilt '<literal>security =
-            none</literal>' oder '<literal>security = tls</literal>', so ist 25 die Standardportnummer. Für '<literal>security =
-            ssl</literal>' ist 465 die Portnummer. Muss normalerweise nicht geändert werden.</para></listitem>
+            <listitem>
+              <para>Portnummer. Der Standardwert hängt von der verwendeten
+              Verschlüsselungsmethode ab. Gilt '<literal>security =
+              none</literal>' oder '<literal>security = tls</literal>', so ist
+              25 die Standardportnummer. Für '<literal>security =
+              ssl</literal>' ist 465 die Portnummer. Muss normalerweise nicht
+              geändert werden.</para>
+            </listitem>
           </varlistentry>
 
           <varlistentry>
             <term><varname>security</varname></term>
 
-            <listitem><para>Wahl der zu verwendenden Verschlüsselung der Verbindung mit dem Server. Standardwert ist
-            '<literal>none</literal>', wodurch keine Verschlüsselung verwendet wird. Mit '<literal>tls</literal>' wird TLS-Verschlüsselung
-            eingeschaltet, und mit '<literal>ssl</literal>' wird Verschlüsselung via SSL eingeschaltet. Achtung: Für
-            '<literal>tls</literal>' und '<literal>ssl</literal>' werden zusätzliche Perl-Module benötigt (siehe unten).</para></listitem>
+            <listitem>
+              <para>Wahl der zu verwendenden Verschlüsselung der Verbindung
+              mit dem Server. Standardwert ist '<literal>none</literal>',
+              wodurch keine Verschlüsselung verwendet wird. Mit
+              '<literal>tls</literal>' wird TLS-Verschlüsselung eingeschaltet,
+              und mit '<literal>ssl</literal>' wird Verschlüsselung via SSL
+              eingeschaltet. Achtung: Für '<literal>tls</literal>' und
+              '<literal>ssl</literal>' werden zusätzliche Perl-Module benötigt
+              (siehe unten).</para>
+            </listitem>
           </varlistentry>
 
           <varlistentry>
-            <term><varname>login</varname> und <varname>password</varname></term>
+            <term><varname>login</varname> und
+            <varname>password</varname></term>
 
-            <listitem><para>Falls der E-Mail-Server eine Authentifizierung verlangt, so können mit diesen zwei Parametern der Benutzername
-            und das Passwort angegeben werden. Wird Authentifizierung verwendet, so sollte aus Sicherheitsgründen auch eine Form von
-            Verschlüsselung aktiviert werden.</para></listitem>
+            <listitem>
+              <para>Falls der E-Mail-Server eine Authentifizierung verlangt,
+              so können mit diesen zwei Parametern der Benutzername und das
+              Passwort angegeben werden. Wird Authentifizierung verwendet, so
+              sollte aus Sicherheitsgründen auch eine Form von Verschlüsselung
+              aktiviert werden.</para>
+            </listitem>
           </varlistentry>
         </variablelist>
-
-        <para>Wird Verschlüsselung über TLS oder SSL aktiviert, so werden zusätzliche Perl-Module benötigt. Diese sind:</para>
-
-        <itemizedlist>
-          <listitem><para>TLS-Verschlüsselung: Modul <literal>Net::SSLGlue</literal> (Debian-Paketname
-          <literal>libnet-sslglue-perl</literal>, Fedora Core: <literal>perl-Net-SSLGlue</literal>, openSUSE:
-          <literal>perl-Net-SSLGlue</literal></para></listitem>
-
-          <listitem><para>SSL-Verschlüsselung: Modul <literal>Net::SMTP::SSL</literal> (Debian-Paketname
-          <literal>libnet-smtp-ssl-perl</literal>, Fedora Core: <literal>perl-Net-SMTP-SSL</literal>, openSUSE:
-          <literal>perl-Net-SMTP-SSL</literal></para></listitem>
-        </itemizedlist>
       </sect2>
     </sect1>
 
     <sect1 id="Drucken-mit-kivitendo">
       <title>Drucken mit kivitendo</title>
 
-      <para>Das Drucksystem von kivitendo benutzt von Haus aus LaTeX-Vorlagen.  Um drucken zu können, braucht der Server ein geeignetes
-      LaTeX System. Am einfachsten ist dazu eine <literal>texlive</literal> Installation. Unter debianoiden Betriebssystemen installiert man
-      die Pakete mit:</para>
+      <para>Das Drucksystem von kivitendo benutzt von Haus aus LaTeX-Vorlagen.
+      Um drucken zu können, braucht der Server ein geeignetes LaTeX System. Am
+      einfachsten ist dazu eine <literal>texlive</literal> Installation. Unter
+      debianoiden Betriebssystemen installiert man die Pakete mit:</para>
 
-      <para><programlisting>aptitude install texlive-base-bin texlive-latex-recommended texlive-fonts-recommended \
-  texlive-latex-extra texlive-lang-german texlive-generic-extra</programlisting></para>
+      <para><programlisting>apt install texlive-base-bin texlive-latex-recommended texlive-fonts-recommended \
+  texlive-latex-extra texlive-lang-german ghostscript</programlisting></para>
 
-      <para>TODO: RPM-Pakete.</para>
+      <para>Für Fedora benötigen Sie die folgenden Pakete:</para>
 
+      <para><programlisting>dnf install texlive-collection-latex texlive-collection-latexextra \
+  texlive-collection-latexrecommended texlive-collection-langgerman \
+  texlive-collection-langenglish</programlisting></para>
+
+      <para>Für openSUSE benötigen Sie die folgenden Pakete:</para>
+
+      <para><programlisting>zypper install texlive-collection-latex texlive-collection-latexextra \
+  texlive-collection-latexrecommended texlive-collection-langgerman \
+  texlive-collection-langenglish</programlisting></para>
+
+      <note><para>kivitendo erwartet eine aktuelle TeX Live Umgebung, um PDF/A zu erzeugen. Aktuelle Distributionen von 2020 erfüllen diese. Überprüfbar ist dies mit dem Aufruf des installation_check.pl mit Parameter -l:</para><para><programlisting>scripts/installations_check.pl -l</programlisting></para></note>
       <para>kivitendo bringt drei alternative Vorlagensätze mit:</para>
+
       <itemizedlist>
-        <listitem><para>RB</para></listitem>
-        <listitem><para>f-tex</para></listitem>
-        <listitem><para>rev-odt</para></listitem>
+        <listitem>
+          <para>RB</para>
+        </listitem>
+        <listitem>
+          <para>marei</para>
+        </listitem>
+        <listitem>
+          <para>rev-odt</para>
+        </listitem>
       </itemizedlist>
 
-      <para>Der ehemalige Druckvorlagensatz "Standard" wurde mit der Version 3.3 entfernt, da er nicht mehr gepflegt wurde.</para>
+      <para>Der ehemalige Druckvorlagensatz "f-tex" wurde mit der Version
+      3.5.6 entfernt, da er nicht mehr gepflegt wird.</para>
 
-      <sect2 id="Vorlagenverzeichnis-anlegen" xreflabel="Vorlagenverzeichnis anlegen">
+      <sect2 id="Vorlagenverzeichnis-anlegen"
+             xreflabel="Vorlagenverzeichnis anlegen">
         <title>Vorlagenverzeichnis anlegen</title>
-        <para>Es lässt sich ein initialer Vorlagensatz erstellen. Die LaTeX-System-Abhängigkeiten hierfür kann man prüfen mit:</para>
+
+        <para>Es lässt sich ein initialer Vorlagensatz erstellen. Die
+        LaTeX-System-Abhängigkeiten hierfür kann man prüfen mit:</para>
 
         <programlisting>./scripts/installation_check.pl -lv</programlisting>
 
-       <para>Der Angemeldete Benutzer muss in einer Gruppe sein, die über das
-              Recht "Konfiguration -> Mandantenverwaltung" verfügt. Siehe auch <xref linkend="Gruppen-anlegen"/>.
-        </para>
-        <para>Im Userbereich lässt sich unter:
-        "<guimenu>System</guimenu> -&gt;
-        <guisubmenu>Mandantenverwaltung</guisubmenu> -&gt; <guimenuitem>Verschiedenes</guimenuitem>" die Option
-        "Neue Druckvorlagen aus Vorlagensatz erstellen" auswählen.</para>
+        <para>Der Angemeldete Benutzer muss in einer Gruppe sein, die über das
+        Recht "Konfiguration -&gt; Mandantenverwaltung" verfügt. Siehe auch
+        <xref linkend="Gruppen-anlegen"/>.</para>
 
-        <orderedlist>
-          <listitem><para><option>Vorlagen auswählen</option>: Wählen Sie hier den Vorlagensatz aus, der kopiert werden soll
-          (<filename>RB</filename>, <filename>f-tex</filename> oder <filename>odt-rev</filename>.)</para></listitem>
-          <listitem><para><option>Neuer Name</option>: Der Verzeichnisname für den neuen Vorlagensatz. Dieser kann im Rahmen der üblichen
-          Bedingungen für Verzeichnisnamen frei gewählt werden.</para></listitem>
-        </orderedlist>
+        <para>Im Userbereich lässt sich unter: "<guimenu>System</guimenu>
+        -&gt; <guisubmenu>Mandantenverwaltung</guisubmenu> -&gt;
+        <guimenuitem>Verschiedenes</guimenuitem>" die Option "Neue
+        Druckvorlagen aus Vorlagensatz erstellen" auswählen.</para>
 
-        <para>Nach dem Speichern wird das Vorlagenverzeichnis angelegt und ist für den aktuellen Mandanten ausgewählt.
-           Der gleiche Vorlagensatz kann, wenn er mal angelegt ist, bei mehreren Mandanten verwendet werden.
-           Eventuell müssen Anpassungen (Logo, Erscheinungsbild, etc) noch vorgenommen werden. Den Ordner findet man im Dateisystem unter
-           <filename>./templates/[Neuer Name]</filename></para>
+        <orderedlist>
+          <listitem>
+            <para><option>Vorlagen auswählen</option>: Wählen Sie hier den
+            Vorlagensatz aus, der kopiert werden soll
+            (<filename>RB</filename>, <filename>marei</filename> oder
+            <filename>odt-rev</filename>.)</para>
+          </listitem>
 
+          <listitem>
+            <para><option>Neuer Name</option>: Der Verzeichnisname für den
+            neuen Vorlagensatz. Dieser kann im Rahmen der üblichen Bedingungen
+            für Verzeichnisnamen frei gewählt werden.</para>
+          </listitem>
+        </orderedlist>
 
+        <para>Nach dem Speichern wird das Vorlagenverzeichnis angelegt und ist
+        für den aktuellen Mandanten ausgewählt. Der gleiche Vorlagensatz kann,
+        wenn er mal angelegt ist, bei mehreren Mandanten verwendet werden.
+        Eventuell müssen Anpassungen (Logo, Erscheinungsbild, etc) noch
+        vorgenommen werden. Den Ordner findet man im Dateisystem unter
+        <filename>./templates/[Neuer Name]</filename></para>
       </sect2>
 
       <sect2 id="Vorlagen-RB">
         <title>Der Druckvorlagensatz RB</title>
 
-        <para>Hierbei handelt es sich um einen vollständigen LaTeX Dokumentensatz mit alternativem Design. Die odt oder html-Varianten sind nicht gepflegt.</para>
+        <para>Hierbei handelt es sich um einen vollständigen LaTeX
+        Dokumentensatz mit alternativem Design. Die odt oder html-Varianten
+        sind nicht gepflegt.</para>
+
         <para>Die konzeptionelle Idee der Vorlagen wird <ulink
-          url="http://www.kivitendo-support.de/vortraege/Lx-Office%20Anwendertreffen%20LaTeX-Druckvorlagen-Teil3-finale.pdf">hier</ulink>
-          auf Folie 5 bis 10 vorgestellt. Informationen zur Anpassung an die eigenen Firmendaten finden sich in der Datei Readme.tex im Vorlagenverzeichnis.</para>
+        url="http://www.kivitendo-support.de/vortraege/Lx-Office%20Anwendertreffen%20LaTeX-Druckvorlagen-Teil3-finale.pdf">hier</ulink>
+        auf Folie 5 bis 10 vorgestellt. Informationen zur Anpassung an die
+        eigenen Firmendaten finden sich in der Datei Readme.tex im
+        Vorlagenverzeichnis.</para>
 
         <para>Eine kurze Übersicht der Features:</para>
-          <itemizedlist>
-            <listitem><para>Mehrsprachenfähig, mit Deutscher und Englischer Übersetzung</para></listitem>
-            <listitem><para>Zentrale Konfigurationsdateien, die für alle Belege benutzt werden, z.B. für Kopf- und Fußzeilen, und Infos wie Bankdaten</para></listitem>
-            <listitem><para>mehrere vordefinierte Varianten für Logos/Hintergrundbilder</para></listitem>
-            <listitem><para>Berücksichtigung für Steuerzonen "EU mit USt-ID Nummer" oder "Außerhalb EU"</para></listitem>
-          </itemizedlist>
-
-      </sect2>
 
-      <sect2 id="f-tex">
-        <title>f-tex</title>
+        <itemizedlist>
+          <listitem>
+            <para>Mehrsprachenfähig, mit Deutscher und Englischer
+            Übersetzung</para>
+          </listitem>
 
-        <para>Ein Vorlagensatz, der in wenigen Minuten alle Dokumente zur Verfügung stellt.</para>
+          <listitem>
+            <para>Zentrale Konfigurationsdateien, die für alle Belege benutzt
+            werden, z.B. für Kopf- und Fußzeilen, und Infos wie
+            Bankdaten</para>
+          </listitem>
 
-        <sect3 id="f-tex-Feature-Übersicht">
-          <title>Feature-Übersicht</title>
-          <itemizedlist>
-            <listitem><para>Keine Redundanz. Es wird ein- und dieselbe LaTeX-Vorlage für alle briefartigen Dokumente verwendet. Also
-            Angebot, Rechnung, Proformarechnung, Lieferschein, aber eben nicht für Paketaufkleber etc.</para></listitem>
+          <listitem>
+            <para>mehrere vordefinierte Varianten für
+            Logos/Hintergrundbilder</para>
+          </listitem>
 
-            <listitem><para>Leichte Anpassung an das Firmen-Layout durch Verwendung eines Hintergrund-PDFs. Dieses kann leicht mit dem
-            eigenen Lieblingsprogramm erstellt werden (Openoffice, Inkscape, Gimp, Adobe*)</para></listitem>
+          <listitem>
+            <para>Berücksichtigung für Steuerzonen "EU mit USt-ID Nummer" oder
+            "Außerhalb EU"</para>
+          </listitem>
+        </itemizedlist>
+      </sect2>
 
-            <listitem><para>Hintergrund-PDF umschaltbar auf "nur erste Seite" (Standard) oder "alle Seiten" (Option
-            "<option>bgPdfFirstPageOnly</option>" in Datei <filename>letter.lco</filename>)</para></listitem>
+     <sect2 id="Vorlagen-rev-odt">
+        <title>Der Druckvorlagensatz rev-odt</title>
 
-            <listitem><para>Hintergrund-PDF für Ausdruck auf bereits bedrucktem Briefpapier abschaltbar. Es wird dann nur bei per E-Mail
-            versendeten Dokumenten eingebunden (Option "<option>bgPdfEmailOnly</option>" in Datei
-            <filename>letter.lco</filename>).</para></listitem>
+        <para>Hierbei handelt es sich um einen Dokumentensatz der mit
+        odt-Vorlagen erstellt wurde. Es gibt in dem Verzeichnis eine
+        Readme-Datei, die eventuell aktueller als die Dokumentation hier ist.
+        Die odt-Vorlagen in diesem Verzeichnis "rev-odt" wurden von revamp-it,
+        Zürich erstellt und werden laufend aktualisiert. Ein paar der
+        Formulierungen in den Druckvorlagen entsprechen dem Schweizer
+        Sprachgebrauch, z.B. "Offerte" oder "allfällig".</para>
+
+        <para>Hinweis zum Einsatz des Feldes "Land" bei den Stammdaten für
+        KundInnen und LieferantInnen, sowie bei Lieferadressen: Die in diesem
+        Vorlagensatz vorhandenen Vorlagen erwarten für "Land" das
+        entsprechende Kürzel, das in Adressen vor die Postleitzahl gesetzt
+        wird. Das Feld kann auch komplett leer bleiben. Wer dies anders
+        handhaben möchte, muss die Vorlagen entsprechend anpassen.</para>
+
+        <para>odt-Vorlagen können mit LibreOffice oder OpenOffice editiert und
+        den eigenen Bedürfnissen angepasst werden. Wichtig beim Editieren von
+        if-Blöcken ist, dass immer der gesamte Block überschrieben werden muss
+        und nicht nur Teile davon, da dies sonst oft zu einer odt-Datei führt,
+        die vom Parser nicht korrekt gelesen werden kann.</para>
+
+        <para>Mahnungen können unter folgenden Einschränkungen mit den
+        odt-Vorlagen im Vorlagensatz rev-odt erzeugt werden:</para>
 
-            <listitem><para>Nutzung der Layout-Funktionen von LaTeX für Seitenumbruch, Wiederholung von Kopfzeilen, Zwischensummen
-            etc. (danke an Kai-Martin Knaak für die Vorarbeit)</para></listitem>
+        <itemizedlist>
+          <listitem>
+            <para>als Druckoption steht nur 'PDF(OpenDocument/OASIS)' zur
+            Verfügung, das heisst, die Mahnungen werden als PDF-Datei
+            ausgegeben.</para>
+          </listitem>
 
-            <listitem><para>Anzeige des Empfängerlandes im Adressfeld nur, wenn es vom Land des eigenen Unternehmens abweicht (also die
-            Rechnung das Land verlässt).</para></listitem>
+          <listitem>
+            <para>für jede Rechnung muss eine eigene Mahnung erzeugt werden
+            (auch wenn bei einzelnen KundInnen mehrere überfällige Rechnungen
+            vorhanden sind).</para>
+          </listitem>
+        </itemizedlist>
 
-            <listitem><para>Multisprachfähig leicht um weitere Sprachen zu erweitern, alle Übersetzungen in der Datei
-            <filename>translatinos.tex</filename>.</para></listitem>
+        <para>Mehrere Mahnungen für eine Kundin / einen Kunden werden zu einer
+        PDF-Datei zusammengefasst</para>
 
-            <listitem><para>Auflistung von Bruttopreisen für Endverbraucher.</para></listitem>
-          </itemizedlist>
-        </sect3>
+        <para>Die Vorlagen zahlungserinnerung.odt sowie mahnung.odt sind für
+        das Erstellen einer Zahlungserinnerung bzw. Mahnung selbst vorgesehen,
+        die Vorlage mahnung_invoice.odt für das Erstellen einer Rechnung über
+        die verrechneten Mahngebühren und Verzugszinsen.</para>
 
-        <sect3 id="f-tex-Installation">
-          <title>Die Installation</title>
-          <itemizedlist>
-            <listitem><para>Vorlagenverzeichnis mit Option f-tex anlegen, siehe: <xref linkend="Vorlagenverzeichnis-anlegen"/>. Das
-            Vorlagensystem funktioniert jetzt schon, hat allerdings noch einen Beispiel-Briefkopf.</para></listitem>
+        <para>Zur Zeit gibt es in kivitendo noch keine Möglichkeit,
+        odt-Vorlagen bei Briefen und Pflichtenheften einzusetzen.
+        Entsprechende Vorlagen sind deshalb nicht vorhanden.</para>
 
-            <listitem><para>Erstelle eine pdf-Hintergrund Datei und verlinke sie nach <filename>./letter_head.pdf</filename>.</para></listitem>
-            <listitem><para>Editiere den Bereich "<option>settings</option>" in der datei <filename>letter.lco</filename>.</para></listitem>
-          </itemizedlist>
+        <para>Fehlermeldungen, Anregungen und Wünsche bitte senden an:
+        empfang@revamp-it.ch</para>
+      </sect2>
 
-          <para>oder etwas detaillierter:</para>
+      <sect2 id="allgemeine-hinweise-zu-latex">
+        <title>Allgemeine Hinweise zu LaTeX Vorlagen</title>
 
-          <para>
-            Es wird eine Datei <filename>sample.lco</filename> erstellt und diese nach <filename>letter.lco</filename> verlinkt.  Eigentlich
-            ist dies die Datei die für die firmenspezifischen Anpassungen gedacht ist.  Da die Einstiegshürde in LaTeX nicht ganz niedrig
-            ist, wird in dieser Datei auf ein Hintergrund-PDF verwiesen. Ich empfehle über dieses PDF die persönlichen Layoutanpassungen
-            vorzunehmen und <filename>sample.lco</filename> unverändert zu lassen. Die Anpassung über eine
-            <filename>*.lco</filename>-Datei, die letztlich auf <filename>letter.lco</filename> verlinkt ist ist aber auch möglich.
-          </para>
+        <para>In den allermeisten Installationen sollte das Drucken jetzt
+        schon funktionieren. Sollte ein Fehler auftreten, wirft TeX sehr lange
+        Fehlerbeschreibungen, der eigentliche Fehler ist immer die erste
+        Zeile, die mit einem Ausrufezeichen anfängt. Häufig auftretende Fehler
+        sind zum Beispiel:</para>
 
-          <para>
-            Es wird eine Datei <filename>sample_head.pdf</filename> mit ausgeliefert, diese wird nach <filename>letter_head.pdf</filename>
-            verlinkt. Damit gibt es schon mal eine funktionsfähige Vorlage. Schau Dir nach Abschluss der Installation die Datei
-            <filename>sample_head.pdf</filename> an und erstelle ein entsprechendes PDF passend zum Briefkopf Deiner Firma, diese dann im
-            Template Verzeichniss ablegen und statt <filename>sample_head.pdf</filename> nach <filename>letter_head.pdf</filename>
-            verlinken.
-          </para>
+        <itemizedlist>
+          <listitem>
+            <para>! LaTeX Error: File `eurosym.sty' not found. Die
+            entsprechende LaTeX-Bibliothek wurde nicht gefunden. Das tritt vor
+            allem bei Vorlagen aus der Community auf. Installieren Sie die
+            entsprechenden Pakete.</para>
+          </listitem>
 
-          <para>
-            Letzlich muss <filename>letter_head.pdf</filename> auf das passende Hintergrund-PDF verweisen, welches gewünschten Briefkopf
-            enthält.
-          </para>
+          <listitem>
+            <para>! Package inputenc Error: Unicode char \u8:... set up for
+            use with LaTeX. Dieser Fehler tritt auf, wenn sie versuchen mit
+            einer Standardinstallation exotische utf8 Zeichen zu drucken.
+            TeXLive unterstützt von Haus nur romanische Schriften und muss mit
+            diversen Tricks dazu gebracht werden andere Zeichen zu
+            akzeptieren. Adere TeX Systeme wie XeTeX schaffen hier
+            Abhilfe.</para>
+          </listitem>
+        </itemizedlist>
 
-          <para>
-            Es wird eine Datei <filename>mydata.tex.example</filename> ausgeliefert, die nach <filename>mytdata.tex</filename> verlinkt
-            ist. Bei verwendetem Hintergrund-PDF wird nur der Eintrag für das Land verwendet. Die Datei muss also nicht angefasst
-            werden. Die anderen Werte sind für das Modul 'lp' (Label Print in erp - zur Zeit nicht im öffentlichen Zweig).
-          </para>
-          <para>
-            Alle Anpassungen zum Briefkopf, Fusszeilen, Firmenlogos, etc.  sollten über die Hintergrund-PDF-Datei oder die
-            <filename>*.lco</filename>-Datei erfolgen.
-          </para>
-        </sect3>
-
-        <sect3 id="f-tex-Funktionsübersicht">
-          <title>f-tex Funktionsübersicht</title>
-          <para>
-            Das Konzept von kivitendo sieht vor, für jedes Dokument (Auftragsbestätigung, Lieferschein, Rechnung, etc.) eine LaTeX-Vorlage
-            vorzuhalten, dies ist sehr wartungsunfreundlich. Auch das Einlesen einer einheitlichen Quelle für den Briefkopf bringt nur
-            bedingte Vorteile, da hier leicht die Pflege der Artikel-Tabellen aus dem Ruder läuft. Bei dem vorliegenden Ansatz wird für alle
-            briefartigen Dokumente mit Artikel-Tabellen eine einheitliche LaTeX-Vorlage verwendet, welche über Codeweichen die
-            Besonderheiten der jeweiligen Dokumente berücksichtigt:
-          </para>
-
-          <itemizedlist>
-            <listitem><para>Tabellen mit oder ohne Preis</para></listitem>
-            <listitem><para>Sprache der Tabellenüberschriften etc.</para></listitem>
-            <listitem><para>Anpassung der Bezugs-Zeile (z.B. Rechnungsnummer versus Angebotsnummer)</para></listitem>
-            <listitem><para>Darstellung von Brutto oder Netto-Preisen in der Auflistung (Endverbraucher versus gewerblicher
-            Kunde)</para></listitem>
-           </itemizedlist>
-
-           <para>Nachteil:</para>
-
-           <para>
-             LaTeX hat ohnehin eine sehr steile Lehrnkurve. Die Datei <filename>letter.tex</filename> ist sehr komplex und verstärkt damit
-             diesen Effekt noch einmal erheblich.  Wer LaTeX-Erfahrung hat, oder geübt ist Scriptsparachen nachzuvollziehen kann natürlich
-             auch innerhalb der Tabellendarstellung gut persönliche Anpassungen vornehmen. Aber man kann sich hier bei Veränderungen sehr
-             schnell heftig in den Fuss schiessen.
-           </para>
-
-           <para>Wer nicht so tief in die Materie einsteigen will oder leicht zu frustrieren ist, sollte sein Hintergrund-PDF auf Basis der
-           mitglieferten Datei <filename>sample_head.pdf</filename> erstellen, und sich an der Form der dargestellten Tabellen, wie sie
-           ausgeliefert werden, erfreuen.
-           </para>
-
-           <para>Kleiner Tipp: Nicht zu viel auf einmal wollen, lieber kleine, kontinuierliche Schritte gehen.</para>
-
-        </sect3>
-
-        <sect3 id="f-tex-Bruttopreise">
-          <title>Bruttopreise für Endverbraucher</title>
-
-                <para>Der auszuweisende Bruttopreis wird innerhalb der LaTeX-Umgebung berechnet. Es gibt zwar ein Feld, um bei Aufträgen "alle
-                Preise Brutto" auszuwählen, aber:</para>
-          <itemizedlist>
-            <listitem>
-              <para>hierfür müssen die Preise auch in Brutto in der Datenbank stehen (ja - das lässt sich über die Preisgruppen und die
-              Zuordung einer Default-Preisgruppe handhaben)</para>
-            </listitem>
-            <listitem>
-              <para>man darf beim Anlegen des Vorgangs nicht vergessen, dieses Häkchen zu setzen.  (Das ist in der Praxis, wenn man sowohl
-              Endverbraucher als auch Gewerbekunden beliefert, der eigentliche Knackpunkt)</para>
-            </listitem>
-          </itemizedlist>
-
-          <para>
-            Es gibt mit f-tex eine weitere Alternative. Die Information ob Brutto oder Nettorechnung wird mit den Zahlarten
-            verknüpft. Zahlarten bei denen Rechnungen, Angebote, etc, in Brutto ausgegeben werden sollen, enden mit "_E" (für
-            Endverbraucher). Falls identische Zahlarten für Gewerbekunden und Endverbraucher vorhanden sind, legt man diese einfach doppelt
-            an (einmal mit der Namensendung "_E"). Gewinn:</para>
-
-          <itemizedlist>
-            <listitem><para>Die Entscheidung, ob Nettopreise ausgewiesen werden, ist nicht mehr fix mit einer Preisliste verbunden.</para></listitem>
-            <listitem><para>Die Default-Zahlart kann im Kundendatensatz hinterlegt werden, und man muss nicht mehr daran denken, "alle Preise
-            Netto" auszuwählen.</para></listitem>
-            <listitem><para>Die Entscheidung, ob Netto- oder Bruttopreise ausgewiesen werden, kann direkt beim Drucken revidiert werden,
-            ohne dass sich der Auftragswert ändert.</para></listitem>
-          </itemizedlist>
-        </sect3>
-
-        <sect3 id="f-tex-lieferadressen">
-          <title>Lieferadressen</title>
-
-          <para>In Lieferscheinen kommen <varname>shipto*</varname>-Variablen im Adressfeld zum Einsatz. Wenn die
-          <varname>shipto*</varname>-Variable leer ist, wird die entsprechende Adressvariable eingesetzt.  Wenn also die Lieferadresse in
-          Straße, Hausnummer und Ort abweicht, müssen auch nur diese Felder in der Lieferadresse ausgefüllt werden. Für den Firmenname wird
-          der Wert der Hauptadresse angezeigt.
-          </para>
-        </sect3>
-      </sect2>
-
-      <sect2 id="Vorlagen-rev-odt">
-        <title>Der Druckvorlagensatz rev-odt</title>
-
-        <para>Hierbei handelt es sich um einen Dokumentensatz der mit odt-Vorlagen erstellt wurde. Es gibt in dem Verzeichnis eine Readme-Datei, die eventuell aktueller als die Dokumentation hier ist.
-Die odt-Vorlagen in diesem Verzeichnis "rev-odt" wurden von revamp-it, Zürich erstellt
-und werden laufend aktualisiert. Ein paar der Formulierungen in den Druckvorlagen entsprechen dem Schweizer Sprachgebrauch, z.B. "Offerte" oder "allfällig".
-        </para>
-
-<para>
-Hinweis zum Einsatz des Feldes "Land" bei den Stammdaten für KundInnen und LieferantInnen,
-sowie bei Lieferadressen:
-Die in diesem Vorlagensatz vorhandenen Vorlagen erwarten für "Land" das entsprechende
-Kürzel, das in Adressen vor die Postleitzahl gesetzt wird.
-Das Feld kann auch komplett leer bleiben.
-Wer dies anders handhaben möchte, muss die Vorlagen entsprechend anpassen.
-</para>
-<para>
-odt-Vorlagen können mit LibreOffice oder OpenOffice editiert
-und den eigenen Bedürfnissen angepasst werden.
-Wichtig beim Editieren von if-Blöcken ist, dass immer der gesamte Block
-überschrieben werden muss und nicht nur Teile davon, da dies sonst oft
-zu einer odt-Datei führt, die vom Parser nicht korrekt gelesen werden kann.
-</para>
-<para>
-Zur Zeit gibt es in kivitendo noch keine Möglichkeit, odt-Vorlagen bei Mahnungen
-einzusetzen. Entsprechende Vorlagen sind deshalb nicht vorhanden.
-</para>
-<para>
-Inwieweit es möglich ist, für die in Version 3.2.0 neu eingeführten Pflichtenhefte
-odt-Vorlagen zu erstellen, sind wir am abklären.
-Wenn dies möglich ist, werden wir in Zukunft auch eine odt-Vorlage für Pflichtenhefte
-in diesem Vorlagensatz zur Verfügung stellen.
-</para>
-<para>
-Fehlermeldungen, Anregungen und Wünsche bitte senden an:
-empfang@revamp-it.ch
-</para>
-
-      </sect2>
-
-      <sect2 id="allgemeine-hinweise-zu-latex">
-        <title>Allgemeine Hinweise zu LaTeX Vorlagen</title>
-        <para>In den allermeisten Installationen sollte das Drucken jetzt schon
-        funktionieren. Sollte ein Fehler auftreten, wirft TeX sehr lange
-        Fehlerbeschreibungen, der eigentliche Fehler ist immer die erste Zeile,
-        die mit einem Ausrufezeichen anfängt. Häufig auftretende Fehler sind zum
-        Beispiel:</para>
-
-        <itemizedlist>
-          <listitem>
-            <para>! LaTeX Error: File `eurosym.sty' not found. Die entsprechende
-            LaTeX-Bibliothek wurde nicht gefunden. Das tritt vor allem bei
-            Vorlagen aus der Community auf. Installieren Sie die entsprechenden
-            Pakete.</para>
-          </listitem>
-          <listitem>
-            <para>! Package inputenc Error: Unicode char \u8:... set up for
-            use with LaTeX. Dieser Fehler tritt auf, wenn sie versuchen mit
-            einer Standardinstallation exotische utf8 Zeichen zu drucken.
-            TeXLive unterstützt von Haus nur romanische Schriften und muss mit
-            diversen Tricks dazu gebracht werden andere Zeichen zu akzeptieren.
-            Adere TeX Systeme wie XeTeX schaffen hier Abhilfe.</para>
-          </listitem>
-        </itemizedlist>
-
-        <para>Wird gar kein Fehler angezeigt, sondern nur der Name des Templates,
-        heißt das normalerweise, dass das LaTeX Binary nicht gefunden wurde.
-        Prüfen Sie den Namen in der Konfiguration (Standard:
+        <para>Wird gar kein Fehler angezeigt, sondern nur der Name des
+        Templates, heißt das normalerweise, dass das LaTeX Binary nicht
+        gefunden wurde. Prüfen Sie den Namen in der Konfiguration (Standard:
         <literal>pdflatex</literal>), und stellen Sie sicher, dass pdflatex
         (oder das von Ihnen verwendete System) vom Webserver ausgeführt werden
         darf.</para>
 
-        <para>Wenn sich das Problem nicht auf Grund der Ausgabe im Webbrowser verifizieren lässt:</para>
+        <para>Wenn sich das Problem nicht auf Grund der Ausgabe im Webbrowser
+        verifizieren lässt:</para>
+
         <itemizedlist>
           <listitem>
-             <para> editiere [kivitendo-home]/config/kivitendo.conf und ändere "keep_temp_files" auf 1</para>
-             <para><programlisting>keep_temp_files = 1;</programlisting></para>
+            <para>editiere [kivitendo-home]/config/kivitendo.conf und ändere
+            "keep_temp_files" auf 1</para>
+
+            <para><programlisting>keep_temp_files = 1;</programlisting></para>
           </listitem>
+
           <listitem>
-             <para>bei fastcgi oder mod_perl den Webserver neu Starten</para>
+            <para>bei fastcgi oder mod_perl den Webserver neu Starten</para>
           </listitem>
+
           <listitem>
             <para>Nochmal einen Druckversuch im Webfrontend auslösen</para>
           </listitem>
+
           <listitem>
             <para>wechsel in das users Verzeichnis von kivitendo</para>
+
             <para><programlisting>cd [kivitendo-home]/users</programlisting></para>
           </listitem>
+
           <listitem>
             <para>LaTeX Suchpfad anpassen:</para>
+
             <para><programlisting>export TEXINPUTS=".:[kivitendo-home]/templates/[aktuelles_template_verzeichniss]:"</programlisting></para>
           </listitem>
+
           <listitem>
-            <para>Finde heraus,  welche Datei kivitendo beim letzten Durchlauf erstellt hat</para>
+            <para>Finde heraus, welche Datei kivitendo beim letzten Durchlauf
+            erstellt hat</para>
+
             <para><programlisting>ls -lahtr ./1*.tex</programlisting></para>
+
             <para>Es sollte die letzte Datei ganz unten sein</para>
           </listitem>
+
           <listitem>
-            <para>für besseren Hinweis auf Fehler texdatei nochmals übersetzen</para>
+            <para>für besseren Hinweis auf Fehler texdatei nochmals
+            übersetzen</para>
+
             <para><programlisting>pdflatex ./1*.tex</programlisting></para>
+
             <para>in der *.tex datei nach dem Fehler suchen.</para>
           </listitem>
         </itemizedlist>
@@ -1781,9 +2468,9 @@ empfang@revamp-it.ch
       <title>OpenDocument-Vorlagen</title>
 
       <para>kivitendo unterstützt die Verwendung von Vorlagen im
-      OpenDocument-Format, wie es OpenOffice.org ab Version 2 erzeugt.
-      kivitendo kann dabei sowohl neue OpenDocument-Dokumente als auch aus
-      diesen direkt PDF-Dateien erzeugen. Um die Unterstützung von
+      OpenDocument-Format, wie es LibreOffice oder OpenOffice (ab Version 2)
+      erzeugen. kivitendo kann dabei sowohl neue OpenDocument-Dokumente als
+      auch aus diesen direkt PDF-Dateien erzeugen. Um die Unterstützung von
       OpenDocument-Vorlagen zu aktivieren muss in der Datei
       <filename>config/kivitendo.conf</filename> die Variable
       <literal>opendocument</literal> im Abschnitt
@@ -1792,3436 +2479,5047 @@ empfang@revamp-it.ch
 
       <para>Während die Erzeugung von reinen OpenDocument-Dateien keinerlei
       weitere Software benötigt, wird zur Umwandlung dieser Dateien in PDF
-      OpenOffice.org benötigt. Soll dieses Feature genutzt werden, so muss
-      neben OpenOffice.org ab Version 2 auch der “X virtual frame buffer”
-      (xvfb) installiert werden. Bei Debian ist er im Paket “xvfb” enthalten.
-      Andere Distributionen enthalten ihn in anderen Paketen.</para>
+      LibreOffice oder OpenOffice benötigt. Soll dieses Feature genutzt
+      werden, so muss neben LibreOffice oder OpenOffice auch der “X virtual
+      frame buffer” (xvfb) installiert werden. Bei Debian ist er im Paket
+      “xvfb” enthalten. Andere Distributionen enthalten ihn in anderen
+      Paketen.</para>
 
       <para>Nach der Installation müssen in der Datei
-      <filename>config/kivitendo.conf</filename> zwei weitere Variablen
-      angepasst werden: <literal>openofficeorg_writer</literal> muss den
-      vollständigen Pfad zur OpenOffice.org Writer-Anwendung enthalten.
-      <literal>xvfb</literal> muss den Pfad zum “X virtual frame buffer”
-      enthalten. Beide stehen im Abschnitt
-      <literal>applications</literal>.</para>
+      <filename>config/kivitendo.conf</filename> im Abschnitt
+      <literal>applications</literal> zwei weitere Variablen angepasst
+      werden:</para>
+
+      <para><literal>openofficeorg_writer</literal> muss den vollständigen
+      Pfad zu LibreOffice oder OpenOffice enthalten. Dabei dürfen keine
+      Anführungszeichen eingesetzt werden.</para>
+
+      <para>Beispiel für Debian oder Ubuntu:</para>
+
+      <programlisting>openofficeorg_writer = /usr/bin/libreoffice</programlisting>
+
+      <para><literal>xvfb</literal> muss den Pfad zum “X virtual frame buffer”
+      enthalten.</para>
 
       <para>Zusätzlich gibt es zwei verschiedene Arten, wie kivitendo mit
-      OpenOffice kommuniziert. Die erste Variante, die benutzt wird, wenn die
-      Variable <literal>$openofficeorg_daemon</literal> gesetzt ist, startet
-      ein OpenOffice, das auch nach der Umwandlung des Dokumentes gestartet
-      bleibt. Bei weiteren Umwandlungen wird dann diese laufende Instanz
-      benutzt. Der Vorteil ist, dass die Zeit zur Umwandlung deutlich
-      reduziert wird, weil nicht für jedes Dokument ein OpenOffice gestartet
-      werden muss. Der Nachteil ist, dass diese Methode Python und die
-      Python-UNO-Bindings benötigt, die Bestandteil von OpenOffice 2
-      sind.</para>
+      LibreOffice bzw. OpenOffice kommuniziert. Die erste Variante, die
+      benutzt wird, wenn die Variable <literal>$openofficeorg_daemon</literal>
+      gesetzt ist, startet ein LibreOffice oder OpenOffice, das auch nach der
+      Umwandlung des Dokumentes gestartet bleibt. Bei weiteren Umwandlungen
+      wird dann diese laufende Instanz benutzt. Der Vorteil ist, dass die Zeit
+      zur Umwandlung deutlich reduziert wird, weil nicht für jedes Dokument
+      ein LibreOffice bzw. OpenOffice gestartet werden muss. Der Nachteil ist,
+      dass diese Methode Python und die Python-UNO-Bindings benötigt, die
+      Bestandteil von LibreOffice bzw. OpenOffice sind.</para>
 
       <note>
-        <para>
-          Für die Verbindung zu OpenOffice wird normalerweise der Python-Interpreter <filename>/usr/bin/python</filename> benutzt. Sollte
-          dies nicht der richtige sein, so kann man mit zwei Konfigurationsvariablen entscheiden, welcher Python-Interpreter genutzt
-          wird. Mit der Option <literal>python_uno</literal> aus dem Abschnitt <literal>applications</literal> wird der Interpreter selber
-          festgelegt; sie steht standardmäßig auf dem eben erwähnten Wert <literal>/usr/bin/python</literal>.
-        </para>
-
-        <para>
-          Zusätzlich ist es möglich, Pfade anzugeben, in denen Python neben seinen normalen Suchpfaden ebenfalls nach Modulen gesucht wird,
-          z.B. falls sich diese in einem gesonderten OpenOffice-Verzeichnis befinden. Diese zweite Variable heißt
-          <literal>python_uno_path</literal> und befindet sich im Abschnitt <literal>environment</literal>. Sie ist standardmäßig
-          leer. Werden hier mehrere Pfade angegeben, so müssen diese durch Doppelpunkte voneinander getrennt werden. Der Inhalt wird an den
-          Python-Interpreter über die Umgebungsvariable <literal>PYTHONPATH</literal> übergeben.
-        </para>
+        <para>Für die Verbindung zu LibreOffice bzw. OpenOffice wird
+        normalerweise der Python-Interpreter
+        <filename>/usr/bin/python</filename> benutzt. Sollte dies nicht der
+        richtige sein, so kann man mit zwei Konfigurationsvariablen
+        entscheiden, welcher Python-Interpreter genutzt wird. Mit der Option
+        <literal>python_uno</literal> aus dem Abschnitt
+        <literal>applications</literal> wird der Interpreter selber
+        festgelegt; sie steht standardmäßig auf dem eben erwähnten Wert
+        <literal>/usr/bin/python</literal>.</para>
+
+        <para>Zusätzlich ist es möglich, Pfade anzugeben, in denen Python
+        neben seinen normalen Suchpfaden ebenfalls nach Modulen gesucht wird,
+        z.B. falls sich diese in einem gesonderten LibreOffice- bzw.
+        OpenOffice-Verzeichnis befinden. Diese zweite Variable heißt
+        <literal>python_uno_path</literal> und befindet sich im Abschnitt
+        <literal>environment</literal>. Sie ist standardmäßig leer. Werden
+        hier mehrere Pfade angegeben, so müssen diese durch Doppelpunkte
+        voneinander getrennt werden. Der Inhalt wird an den Python-Interpreter
+        über die Umgebungsvariable <literal>PYTHONPATH</literal>
+        übergeben.</para>
       </note>
 
       <para>Ist <literal>$openofficeorg_daemon</literal> nicht gesetzt, so
-      wird für jedes Dokument OpenOffice neu gestartet und die Konvertierung
-      mit Hilfe eines Makros durchgeführt. Dieses Makro muss in der
-      Dokumentenvorlage enthalten sein und
+      wird für jedes Dokument LibreOffice bzw. OpenOffice neu gestartet und
+      die Konvertierung mit Hilfe eines Makros durchgeführt. Dieses Makro muss
+      in der Dokumentenvorlage enthalten sein und
       “Standard.Conversion.ConvertSelfToPDF()” heißen. Die Beispielvorlage
-      ‘<literal>templates/mastertemplates/German/invoice.odt</literal>’
-      enthält ein solches Makro, das in jeder anderen Dokumentenvorlage
-      ebenfalls enthalten sein muss.</para>
-
-      <para>Als letztes muss herausgefunden werden, welchen Namen
-      OpenOffice.org Writer dem Verzeichnis mit den Benutzereinstellungen
-      gibt. Unter Debian ist dies momentan
-      <literal>~/.openoffice.org2</literal>. Sollte der Name bei Ihrer
-      OpenOffice.org-Installation anders sein, so muss das Verzeichnis
-      <literal>users/.openoffice.org2</literal> entsprechend umbenannt werden.
-      Ist der Name z.B. einfach nur <literal>.openoffice</literal>, so wäre
-      folgender Befehl auszuführen:</para>
-
-      <para><literal>mv users/.openoffice.org2
-      users/.openoffice</literal></para>
+      ‘<literal>templates/print/rev-odt/invoice.odt</literal>’ enthält ein
+      solches Makro, das in jeder anderen Dokumentenvorlage ebenfalls
+      enthalten sein muss.</para>
+
+      <para>Als letztes muss herausgefunden werden, welchen Namen OpenOffice
+      bzw. LibreOffice dem Verzeichnis mit den Benutzereinstellungen gibt.
+      Unter Debian ist dies momentan <literal>~/.config/libreoffice</literal>.
+      kivitendo verwendet das Verzeichnis
+      <literal>users/.openoffice.org2</literal>. Eventuell muss dieses
+      Verzeichnis umbenannt werden.</para>
 
       <para>Dieses Verzeichnis, wie auch das komplette
       <literal>users</literal>-Verzeichnis, muss vom Webserver beschreibbar
       sein. Dieses wurde bereits erledigt (siehe <xref
-      linkend="Manuelle-Installation-des-Programmpaketes" />), kann aber
-      erneut überprüft werden, wenn die Konvertierung nach PDF
-      fehlschlägt.</para>
-    </sect1>
+      linkend="Manuelle-Installation-des-Programmpaketes"/>), kann aber erneut
+      überprüft werden, wenn die Konvertierung nach PDF fehlschlägt.</para>
 
-    <sect1 id="config.eur">
-      <title>Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung:
-      EUR</title>
+      <sect2>
+        <title>OpenDocument (odt) Druckvorlagen mit Makros</title>
 
-      <sect2 id="config.eur.introduction"
-             xreflabel="Einführung in die Konfiguration zur EUR">
-        <title>Einführung</title>
+        <para>OpenDocument Vorlagen können Makros enthalten, welche komplexere
+        Aufgaben erfüllen.</para>
 
-        <para>kivitendo besaß bis inklusive Version 2.6.3 einen Konfigurationsparameter namens <varname>eur</varname>, der sich in der
-        Konfigurationsdatei <filename>config/kivitendo.conf</filename> (damals noch <filename>config/lx_office.conf</filename>)
-        befand. Somit galt er für alle Mandanten, die in dieser Installation benutzt wurden.</para>
+        <para>Der Vorlagensatz "rev-odt" enthält solche Vorlagen mit <emphasis
+        role="bold">Schweizer Bank-Einzahlungsscheinen (BESR)</emphasis>.
+        Diese Makros haben die Aufgabe, die in den Einzahlungsscheinen
+        benötigte Referenznummer und Kodierzeile zu erzeugen. Hier eine kurze
+        Beschreibung, wie die Makros aufgebaut sind, und was bei ihrer Nutzung
+        zu beachten ist (<emphasis role="bold">in fett sind nötige einmalige
+        Anpassungen aufgeführt</emphasis>):</para>
 
-        <para>Mit der nachfolgenden Version wurde der Parameter zum Einen in
-        die Mandantendatenbank verschoben und dabei auch gleich in drei
-        Einzelparameter aufgeteilt, mit denen sich das Verhalten genauer
-        steuern lässt.</para>
-      </sect2>
+        <sect3>
+          <title>Bezeichnung der Vorlagen</title>
 
-      <sect2 id="config.eur.parameters"
-             xreflabel="Konfigurationsparameter für EUR">
-        <title>Konfigurationsparameter</title>
+          <para>Rechnung: invoice_besr.odt, Auftrag:
+          sales_order_besr.odt</para>
+        </sect3>
 
-        <para>Es gibt drei Parameter, die die Gewinnermittlungsart,
-        Versteuerungsart und die Warenbuchungsmethode regeln:</para>
+        <sect3 id="opendocument-druckvorlagen-mit-makros.vorbereitungen">
+          <title>Vorbereitungen im Adminbereich</title>
 
-        <variablelist>
-          <varlistentry>
-            <term><varname>profit_determination</varname></term>
+          <para>Damit beim Erstellen von Rechnungen und Aufträgen neben der
+          Standardvorlage ohne Einzahlungsschein weitere Vorlagen (z.B. mit
+          Einzahlungsschein) auswählbar sind, muss für jedes Vorlagen-Suffix
+          ein Drucker eingerichtet werden:</para>
 
+          <itemizedlist>
             <listitem>
-              <para>Dieser Parameter legt die Berechnungsmethode für die
-              Gewinnermittlung fest. Er enthält entweder
-              <literal>balance</literal> für
-              Betriebsvermögensvergleich/Bilanzierung oder
-              <literal>income</literal> für die
-              Einnahmen-Überschuss-Rechnung.</para>
+              <para>Druckeradministration → Drucker hinzufügen</para>
             </listitem>
-          </varlistentry>
-
-          <varlistentry>
-            <term><varname>accounting_method</varname></term>
 
             <listitem>
-              <para>Dieser Parameter steuert die Buchungs- und
-              Berechnungsmethoden für die Versteuerungsart. Er enthält
-              entweder <literal>accrual</literal> für die Soll-Versteuerung
-              oder <literal>cash</literal> für die Ist-Versteuerung.</para>
+              <para>Mandant wählen</para>
             </listitem>
-          </varlistentry>
 
-          <varlistentry>
-            <term><varname>inventory_system</varname></term>
+            <listitem>
+              <para>Druckerbeschreibung → aussagekräftiger Text: wird in der
+              Auftrags- bzw. Rechnungsmaske als Auswahl angezeigt (z.B. mit
+              Einzahlungsschein Bank xy)</para>
+            </listitem>
 
             <listitem>
-              <para>Dieser Parameter legt die Warenbuchungsmethode fest. Er
-              enthält entweder <literal>perpetual</literal> für die
-              Bestandsmethode oder <literal>periodic</literal> für die
-              Aufwandsmethode.</para>
+              <para>Druckbefehl → beliebiger Text (hat für das Erzeugen von
+              Aufträgen oder Rechnungen als odt-Datei keine Bedeutung, darf
+              aber nicht leer sein)</para>
             </listitem>
-          </varlistentry>
-        </variablelist>
 
-        <para>Zum Vergleich der Funktionalität bis und nach 2.6.3:
-        <varname>eur</varname> = 1 bedeutete Einnahmen-Überschuss-Rechnung,
-        Ist-Versteuerung und Aufwandsmethode. <varname>eur</varname> = 0
-        bedeutete hingegen Bilanzierung, Soll-Versteuerung und
-        Bestandsmethode.</para>
+            <listitem>
+              <para>Vorlagenkürzel → besr bzw. selbst gewähltes Vorlagensuffix
+              (muss genau der Zeichenfolge entsprechen, die zwischen
+              "invoice_" bzw. "sales_order_" und ".odt" steht.)</para>
+            </listitem>
 
-        <para>Die Konfiguration "<varname>eur</varname>" unter
-        <varname>[system]</varname> in der <link
-        linkend="config.config-file">Konfigurationsdatei</link>
-        <filename>config/kivitendo.conf</filename> wird nun nicht mehr
-        benötigt und kann entfernt werden. Dies muss manuell geschehen.</para>
-      </sect2>
+            <listitem>
+              <para>speichern</para>
+            </listitem>
+          </itemizedlist>
+        </sect3>
 
-      <sect2 id="config.eur.setting-parameters">
-        <title>Festlegen der Parameter</title>
+        <sect3>
+          <title>Benutzereinstellungen</title>
 
-        <para>Beim Anlegen eines neuen Mandanten bzw. einer neuen Datenbank in
-        der Admininstration können diese Optionen nun unabhängig voneinander
-        eingestellt werden.</para>
+          <para>Wer den Ausdruck mit Einzahlungsschein als Standardeinstellung
+          im Rechnungs- bzw. Auftragsformular angezeigt haben möchte, kann
+          dies persönlich für sich bei den Benutzereinstellungen
+          konfigurieren:</para>
 
-        <para>Beim Upgrade bestehender Mandanten wird eur ausgelesen und die
-        Variablen werden so gesetzt, daß sich an der Funktionalität nichts
-        ändert.</para>
+          <itemizedlist>
+            <listitem>
+              <para>Programm → Benutzereinstellungen → Druckoptionen</para>
+            </listitem>
 
-        <para>Die aktuelle Konfiguration wird unter Nummernkreise und
-        Standardkonten unter dem neuen Punkt "Einstellungen" (read-only)
-        angezeigt. Unter <guimenu>System</guimenu>
-        -&gt; <guisubmenu>Mandantenkonfiguration</guisubmenu> können
-        die Einstellungen auch geändert werden. Dabei ist zu beachten,
-        dass eine Änderung vorhandene Daten so belässt und damit
-        evtl. die Ergebnisse verfälscht. Dies gilt vor Allem für die
-        Warenbuchungsmethode (siehe auch
-        <link linkend="config.eur.inventory-system-perpetual">
-        Bemerkungen zur Bestandsmethode</link>).</para>
-      </sect2>
+            <listitem>
+              <para>Standardvorlagenformat → OpenDocument/OASIS</para>
+            </listitem>
 
-      <sect2 id="config.eur.inventory-system-perpetual">
-        <title>Bemerkungen zur Bestandsmethode</title>
+            <listitem>
+              <para>Standardausgabekanal → Bildschirm</para>
+            </listitem>
 
-        <para>Die Bestandsmethode ist eigentlich eine sehr elegante Methode,
-        funktioniert in kivitendo aber nur unter bestimmten Bedingungen:
-        Voraussetzung ist, daß auch immer alle Einkaufsrechnungen gepflegt
-        werden, und man beim Jahreswechsel nicht mit einer leeren Datenbank
-        anfängt, da bei jedem Verkauf anhand der gesamten Rechnungshistorie
-        der Einkaufswert der Ware nach dem FIFO-Prinzip aus den
-        Einkaufsrechnungen berechnet wird.</para>
+            <listitem>
+              <para>Standarddrucker → gewünschte Druckerbeschreibung auswählen
+              (z.B. mit Einzahlungsschein Bank xy)</para>
+            </listitem>
 
-        <para>Die Bestandsmethode kann vom Prinzip her also nur funktioneren,
-        wenn man mit den Buchungen bei Null anfängt, und man kann auch nicht
-        im laufenden Betrieb von der Aufwandsmethode zur Bestandsmethode
-        wechseln.</para>
-      </sect2>
+            <listitem>
+              <para>Anzahl Kopien → leer</para>
+            </listitem>
 
-      <sect2 id="config.eur.knonw-issues">
-        <title>Bekannte Probleme</title>
+            <listitem>
+              <para>speichern</para>
+            </listitem>
+          </itemizedlist>
+        </sect3>
 
-        <para>Bei bestimmten Berichten kann man derzeit noch inviduell
-        einstellen, ob man nach Ist- oder Sollversteuerung auswertet, und es
-        werden im Code Variablen wie $accrual oder $cash gesetzt. Diese
-        Codestellen wurden noch nicht angepasst, sondern nur die, wo bisher
-        die Konfigurationsvariable
-        <varname>$::lx_office_conf{system}-&gt;{eur}</varname> ausgewertet
-        wurde.</para>
+        <sect3>
+          <title>Aufbau und nötige Anpassungen der Vorlagen</title>
 
-        <para>Es fehlen Hilfetext beim Neuanlegen eines Mandanten, was die
-        Optionen bewirken, z.B. mit zwei Standardfällen.</para>
-      </sect2>
-    </sect1>
+          <para>In der Vorlage sind als Modul "BESR" 4 Makros gespeichert, die
+          aus dem von kivitendo erzeugten odt-Dokument die korrekte
+          Referenznummer inklusive Prüfziffer sowie die Kodierzeile in
+          OCRB-Schrift erzeugen und am richtigen Ort ins Dokument
+          schreiben.</para>
 
-    <sect1 id="config.skr04-update-3804">
-      <title>SKR04 19% Umstellung für innergemeinschaftlichen Erwerb</title>
+          <itemizedlist>
+            <listitem>
+              <para>Für den Einzahlungsschein ist die letzte Seite des
+              Dokuments reserviert</para>
+            </listitem>
 
-      <sect2 id="config.skr04-update-3804.introduction">
-        <title>Einführung</title>
+            <listitem>
+              <para>Direkt über dem Einzahlungsschein enthält die Vorlage eine
+              Zeile mit folgenden Angaben (<emphasis
+              role="bold">Bank-Konto-Identifikationsnummer und
+              Postkonto-Nummer der Bank müssen gemäss Angaben der jeweiligen
+              Bank angepasst werden</emphasis>):<itemizedlist>
+                  <listitem>
+                    <para>DDDREF: 4 Werte zum Bilden der Referenznummer
+                    (jeweils durch einen Leerschlag getrennt): <itemizedlist>
+                        <listitem>
+                          <para>erster Wert: <emphasis
+                          role="bold">Bank-Konto-Identifikation</emphasis>
+                          (nur Ziffern, maximal 6), <emphasis role="bold">muss
+                          angepasst werden</emphasis>.</para>
+                        </listitem>
+
+                        <listitem>
+                          <para>zweiter Wert: &lt;%customernumber%&gt;
+                          (Kundennummer: nur Ziffern, maximal 6)</para>
+                        </listitem>
+
+                        <listitem>
+                          <para>dritter Wert: &lt;%ordnumber%&gt;
+                          (Auftragsnummer bei Auftragsvorlage
+                          sales_oder_besr.odt, sonst 0) maximal 7 Ziffern,
+                          führende Buchstaben werden vom Makro entfernt</para>
+                        </listitem>
+
+                        <listitem>
+                          <para>vierter Wert: &lt;%invnumber%&gt;
+                          (Rechnungsnummer bei Rechnungsvorlage
+                          invoice_besr.odt, sonst 0) maximal 7 Ziffern,
+                          führende Buchstaben werden vom Makro entfernt</para>
+                        </listitem>
+                      </itemizedlist></para>
+                  </listitem>
+
+                  <listitem>
+                    <para>DDDKONTO: <emphasis role="bold">Postkonto-Nummer der
+                    Bank, muss angepasst werden</emphasis>.</para>
+                  </listitem>
+
+                  <listitem>
+                    <para>DDDBETRAG: &lt;%total%&gt; Einzahlungsbetrag oder 0,
+                    falls Einzahlungsschein ohne Betrag</para>
+                  </listitem>
+
+                  <listitem>
+                    <para>DDDEND: muss am Ende der Zeile vorhanden sein</para>
+                  </listitem>
+                </itemizedlist></para>
+            </listitem>
 
-        <para>Die Umsatzsteuerumstellung auf 19% für SKR04 für die
-        Steuerschlüssel "EU ohne USt-ID Nummer" ist erst 2010 erfolgt.
-        kivitendo beinhaltet ein Upgradeskript, das das Konto 3804 automatisch
-        erstellt und die Steuereinstellungen korrekt einstellt. Hat der
-        Benutzer aber schon selber das Konto 3804 angelegt, oder gab es schon
-        Buchungen im Zeitraum nach dem 01.01.2007 auf das Konto 3803, wird das
-        Upgradeskript vorsichtshalber nicht ausgeführt, da der Benutzer sich
-        vielleicht schon selbst geholfen hat und mit seinen Änderungen
-        zufrieden ist. Die korrekten Einstellungen kann man aber auch per Hand
-        ausführen. Nachfolgend werden die entsprechenden Schritte anhand von
-        Screenshots dargestellt.</para>
+            <listitem>
+              <para><emphasis role="bold">Im Einzahlungsschein selbst müssen
+              der Name und die Adresse der Bank, die Postkonto-Nummer der
+              Bank, sowie der eigene Firmenname und die Firmenadresse
+              angepasst werden.</emphasis> Dabei ist darauf zu achten, dass
+              sich die Positionen der Postkonto-Nummern der Bank, sowie der
+              Zeichenfolgen dddfr, DDDREF1, DDDREF2, 609, DDDKODIERZEILE nicht
+              verschieben.</para>
+            </listitem>
+          </itemizedlist>
 
-        <para>Für den Fall, daß Buchungen mit der Steuerschlüssel "EU ohne
-        USt.-IdNr." nach dem 01.01.2007 erfolgt sind, ist davon auszugehen,
-        dass diese mit dem alten Umsatzsteuersatz von 16% gebucht worden sind,
-        und diese Buchungen sollten entsprechend kontrolliert werden.</para>
-      </sect2>
+          <screenshot>
+            <screeninfo>Rechnungsvorlage Schweizer Bank-Einzahlungsschein - zu
+            ändernde Einträge in rot</screeninfo>
 
-      <sect2 id="config.skr04-update-3804.create-chart">
-        <title>Konto 3804 manuell anlegen</title>
+            <mediaobject>
+              <imageobject>
+                <imagedata fileref="images/Einzahlungsschein_Makro.png"/>
+              </imageobject>
+            </mediaobject>
+          </screenshot>
+        </sect3>
 
-        <para>Die folgenden Schritte sind notwendig, um das Konto manuell
-        anzulegen und zu konfigurieren. Zuerst wird in
-        <guimenu>System</guimenu> -&gt;
-        <guisubmenu>Kontenübersicht</guisubmenu> -&gt; <guimenuitem>Konto
-        erfassen</guimenuitem> das Konto angelegt.</para>
+        <sect3>
+          <title>Auswahl der Druckvorlage in kivitendo beim Erzeugen einer
+          odt-Rechnung (analog bei Auftrag)</title>
+
+          <para>Im Fussbereich der Rechnungsmaske muss neben Rechnung,
+          OpenDocument/OASIS und Bildschirm die im Adminbereich erstellte
+          Druckerbeschreibung ausgewählt werden, falls diese nicht bereits bei
+          den Benutzereinstellungen als persönlicher Standard gewählt
+          wurde.</para>
+        </sect3>
 
-        <screenshot>
-          <screeninfo>Konto 3804 erfassen</screeninfo>
+        <sect3>
+          <title>Makroeinstellungen in LibreOffice anpassen</title>
 
-          <mediaobject>
-            <imageobject>
-              <imagedata fileref="images/skr04-update-3804/konto3804.png" />
-            </imageobject>
-          </mediaobject>
-        </screenshot>
+          <para>Falls beim Öffnen einer von kivitendo erzeugten odt-Rechnung
+          die Meldung kommt, dass Makros aus Sicherheitsgründen nicht
+          ausgeführt werden, so müssen folgende Einstellungen in LibreOffice
+          angepasst werden:</para>
 
-        <para>
-         Als Zweites muss Steuergruppe 13 für Konto 3803 angepasst werden. Dazu unter <guimenu>System</guimenu> -&gt;
-         <guisubmenu>Steuern</guisubmenu> -&gt; <guimenuitem>Bearbeiten</guimenuitem> den Eintrag mit Steuerschlüssel 13 auswählen und ihn
-         wie im folgenden Screenshot angezeigt anpassen.
-        </para>
+          <itemizedlist>
+            <listitem>
+              <para>Extras → Optionen → Sicherheit → Makrosicherheit</para>
+            </listitem>
 
-        <screenshot>
-          <screeninfo>Steuerschlüssel 13 für 3803 (16%) anpassen</screeninfo>
+            <listitem>
+              <para>Sicherheitslevel auf "Mittel" einstellen (Diese
+              Einstellung muss auf jedem Computer durchgeführt werden, mit dem
+              von kivitendo erzeugte odt-Rechnungen oder Aufträge geöffnet
+              werden.)</para>
+            </listitem>
 
-          <mediaobject>
-            <imageobject>
-              <imagedata fileref="images/skr04-update-3804/steuer3803.png" />
-            </imageobject>
-          </mediaobject>
-        </screenshot>
-
-        <para>
-         Als Drittes wird ein neuer Eintrag mit Steuerschlüssel 13 für Konto 3804 (19%) angelegt. Dazu unter <guimenu>System</guimenu> -&gt;
-         <guisubmenu>Steuern</guisubmenu> -&gt; <guimenuitem>Erfassen</guimenuitem> auswählen und die Werte aus dem Screenshot übernehmen.
-        </para>
-
-        <screenshot>
-          <screeninfo>Steuerschlüssel 13 für 3804 (19%) anlegen</screeninfo>
+            <listitem>
+              <para>Beim Öffnen einer odt-Rechnung oder eines odt-Auftrags bei
+              der entsprechenden Nachfrage "Makros ausführen"
+              auswählen.</para>
+
+              <para><emphasis role="bold">Wichtig</emphasis>: die Makros sind
+              so eingestellt, dass sie beim Öffnen der Vorlagen selbst nicht
+              ausgeführt werden. Das heisst für das Ansehen und Bearbeiten der
+              Vorlagen sind keine speziellen Einstellungen in LibreOffice
+              nötig.</para>
+            </listitem>
+          </itemizedlist>
+        </sect3>
+      </sect2>
 
-          <mediaobject>
-            <imageobject>
-              <imagedata fileref="images/skr04-update-3804/steuer3804.png" />
-            </imageobject>
-          </mediaobject>
-        </screenshot>
+      <sect2>
+        <title>Schweizer QR-Rechnung mit OpenDocument Vorlagen</title>
 
-        <para>
-         Als Nächstes sind alle Konten anzupassen, die als Steuerautomatikkonto die 3803 haben, sodass sie ab dem 1.1.2007 auch
-         Steuerautomatik auf 3804 bekommen. Dies betrifft in der Standardkonfiguration die Konten 4315 und 4726. Als Beispiel für 4315
-         müssen Sie dazu unter <guimenu>System</guimenu> -&gt; <guisubmenu>Kontenübersicht</guisubmenu> -&gt; <guimenuitem>Konten
-         anzeigen</guimenuitem> das Konto 4315 anklicken und die Einstellungen wie im Screenshot gezeigt vornehmen.
-        </para>
+        <sect3>
+          <title>Übersicht</title>
 
-        <screenshot>
-          <screeninfo>Konto 4315 anpassen</screeninfo>
+          <para>Mit der Version 3.6.0 unterstützt Kivitendo die Erstellung von
+          Schweizer QR-Rechnungen gemäss <ulink
+          url="https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-de.pdf">Swiss
+          Payment Standards, Version 2.2</ulink>. Implementiert sind hierbei die
+          Varianten:</para>
 
-          <mediaobject>
-            <imageobject>
-              <imagedata fileref="images/skr04-update-3804/konto4315.png" />
-            </imageobject>
-          </mediaobject>
-        </screenshot>
+          <itemizedlist>
+            <listitem>
+              <para><emphasis role="bold">QR-IBAN mit
+              QR-Referenz</emphasis></para>
+            </listitem>
 
-        <para>
-         Als Letztes sollte die Steuerliste unter <guimenu>System</guimenu> -&gt; <guisubmenu>Steuern</guisubmenu> -&gt;
-         <guimenuitem>Bearbeiten</guimenuitem> kontrolliert werden. Zum Vergleich der Screenshot.
-        </para>
+            <listitem>
+              <para><emphasis role="bold">IBAN ohne Referenz</emphasis></para>
+            </listitem>
+          </itemizedlist>
 
-        <screenshot>
-          <screeninfo>Steuerliste vergleichen</screeninfo>
+          <para>Der Vorlagensatz "rev-odt" enthält die Vorlage
+          <literal>invoice_qr.odt</literal>, welche für die Erstellung von
+          QR-Rechnungen vorgesehen ist. Damit diese verwendet werden kann muss
+          wie obenstehend beschrieben ein Drucker hinzugefügt werden (siehe
+          <xref linkend="opendocument-druckvorlagen-mit-makros.vorbereitungen"/>
+          ). Alternativ kann die Vorlage umbenannt werden in
+          <literal>invoice.odt</literal>.</para>
+
+          <para>Die Vorlage <literal>invoice_qr.odt</literal> kann beliebig
+          angepasst werden. Zwingend muss diese jedoch das QR-Code Platzhalter
+          Bild, als eingebettetes Bild, enthalten. Da dieses beim
+          Ausdrucken/Erzeugen der Rechnung durch das neu generierte QR-Code
+          Bild ersetzt wird.</para>
+        </sect3>
 
-          <mediaobject>
-            <imageobject>
-              <imagedata fileref="images/skr04-update-3804/steuerliste.png" />
-            </imageobject>
-          </mediaobject>
-        </screenshot>
-      </sect2>
-    </sect1>
-    <sect1 id="bilanz">
-     <title>Verhalten des Bilanzberichts</title>
-      <para>
-    Bis Version 3.0 wurde "closedto" ("Bücher schließen zum") als Grundlage für das
-    Startdatum benutzt. Schließt man die Bücher allerdings monatsweise führt dies
-    zu falschen Werten.</para>
-    <para>In der Mandantenkonfiguration kann man dieses Verhalten genau einstellen indem man:</para>
-  <itemizedlist>
-   <listitem>
-    <para>weiterhin closed_to benutzt (Default, es ändert sich nichts zu vorher)</para>
-  </listitem>
-   <listitem>
-    <para>immer den Jahresanfang nimmt (1.1. relativ zum Stichtag)</para>
-  </listitem>
-   <listitem>
-    <para>immer die letzte Eröffnungsbuchung als Startdatum nimmt</para>
-       <para>- mit Jahresanfang als Alternative wenn es keine EB-Buchungen gibt</para>
-      <para>- oder mit "alle Buchungen" als Alternative"</para>
-   </listitem>
-   <listitem>
-     <para>mit Jahresanfang als Alternative wenn es keine EB-Buchungen gibt </para>
-  </listitem>
-   <listitem>
-    <para>immer alle Buchungen seit Beginn der Datenbank nimmt</para>
-  </listitem>
-  </itemizedlist>
-  <para>
-   Folgende Hinweise zu den Optionen:
-    Das "Bücher schließen Datum" ist sinnvoll, wenn man nur komplette Jahre
-    schließt. Bei Wirtschaftsjahr = Kalendarjahr entspricht dies aber auch
-    dem Jahresanfang.
-    "Alle Buchungen" kann z.B. sinnvoll sein wenn man ohne Jahresabschluß
-    durchbucht.
-    Eröffnungsbuchung mit "alle Buchungen" als Fallback ist z.B. sinnvoll, wenn man
-    am sich Anfang des zweiten Buchungsjahres befindet, und noch keinen
-    Jahreswechsel und auch noch keine EB-Buchungen hat.
-    Bei den Optionen mit EB-Buchungen wird vorausgesetzt, daß diese immer am 1. Tag
-    des Wirtschaftsjahres gebucht werden.
-    Zur Sicherheit wird das Startdatum im Bilanzbericht jetzt zusätzlich zum
-    Stichtag mit angezeigt. Das hilft auch bei der Kontrolle für den
-    Abgleich mit der GuV.
-    </para>
-    </sect1>
-    <sect1 id="config.client">
-      <title>Einstellungen pro Mandant</title>
+        <sect3>
+          <title>Einstellungen</title>
+
+          <sect4>
+            <title>Mandantenkonfiguration</title>
+
+            <para>Unter <emphasis>System → Mandantenkonfiguration →
+            Features</emphasis>. Im Abschnitt <emphasis>Einkauf und
+            Verkauf</emphasis>, beim Punkt <emphasis>Verkaufsrechnungen mit
+            Schweizer QR-Rechnung erzeugen</emphasis>, die gewünschte Variante
+            wählen.</para>
+          </sect4>
+
+          <sect4>
+            <title>Konfiguration der Bankkonten</title>
+
+            <para>Unter <emphasis>System → Bankkonten</emphasis> muss bei
+            mindestens einem Bankkonto die Option <emphasis>Nutzung mit
+            Schweizer QR-Rechnung</emphasis> auf <emphasis
+            role="bold">Ja</emphasis> gestellt werden.</para>
+
+            <tip>
+              <para>Für die Variante <emphasis role="bold">QR-IBAN mit
+              QR-Referenz</emphasis> muss dieses Konto unter IBAN eine gültige
+              <emphasis role="bold">QR-IBAN Nummer</emphasis> enthalten. Diese
+              unterscheidet sich von der regulären IBAN.</para>
+
+              <para>Zusätzlich muss eine gültige <emphasis role="bold">Bankkonto
+              Identifikationsnummer</emphasis> angegeben werden
+              (6-stellig).</para>
+
+              <para>Diese werden von der jeweiligen Bank vergeben.</para>
+            </tip>
+
+            <para>Sind mehrere Konten ausgewählt wird das erste
+            verwendet.</para>
+          </sect4>
+
+          <sect4>
+            <title>Rechnungen ohne Betrag</title>
+
+            <para>Für Rechnungen ohne Betrag (z.B. Spenden) kann, in der
+            jeweiligen Rechnung, die Checkbox <emphasis>QR-Rechnung ohne
+            Betrag</emphasis> aktiviert werden. Diese Checkbox erscheint nur,
+            wenn QR-Rechnungen in der Mandantenkonfiguration aktiviert sind
+            (variante ausgewählt).</para>
+
+            <para>Dies wirkt sich lediglich auf den erzeugten QR-Code aus. Die
+            Vorlage muss separat angepasst und ausgewählt werden.</para>
+          </sect4>
+        </sect3>
 
-      <para>Einige Einstellungen können von einem Benutzer mit dem
-      <link linkend="Zusammenhänge">Recht</link> "Administration
-      (Für die Verwaltung der aktuellen Instanz aus einem Userlogin heraus)"
-      gemacht werden. Diese Einstellungen sind dann für die aktuellen
-      Mandanten-Datenbank gültig. Die Einstellungen sind
-      unter <guimenu>System</guimenu>
-      -&gt; <guisubmenu>Mandantenkonfiguration</guisubmenu> erreichbar.</para>
-
-      <para>Bitte beachten Sie die Hinweise zu den einzelnen
-      Einstellungen. Einige Einstellungen sollten nicht ohne Weiteres
-      im laufenden Betrieb geändert werden (siehe
-      auch <link linkend="config.eur.inventory-system-perpetual">Bemerkungen zu
-      Bestandsmethode</link>).</para>
+        <sect3>
+          <title>Adressdaten</title>
 
-      <para>Die Einstellungen <literal>show_bestbefore</literal>
-      und <literal>payments_changeable</literal> aus dem
-      Abschnitt <literal>features</literal> und die Einstellungen im
-      Abschnitt <literal>datev_check</literal> (sofern schon vorhanden)
-      der <link linkend="config.config-file">kivitendo-Konfigurationsdatei</link>
-      werden bei einem Datenbankupdate einer älteren Version automatisch
-      übernommen. Diese Einträge können danach aus der Konfigurationsdatei
-      entfernt werden.</para>
-    </sect1>
+          <para>Die Adressdaten zum Zahlungsempfänger werden aus der
+          Mandantenkonfiguration entnommen. Unter <emphasis>System →
+          Mandantenkonfiguration → Verschiedenes</emphasis>, Abschnitt
+          <emphasis>Firmenname und -adresse.</emphasis></para>
 
-    <sect1 id="kivitendo-ERP-verwenden">
-      <title>kivitendo ERP verwenden</title>
+          <para>Die Adressdaten zum Zahlungspflichtigen stammen aus den
+          Kundendaten der jeweiligen Rechnung.</para>
 
-      <para>Nach erfolgreicher Installation ist der Loginbildschirm unter
-      folgender URL erreichbar:</para>
+          <para>Die Adressen müssen inklusive Land angegeben werden. Akzeptiert
+          werden Ländername oder Ländercode, also z.B. "Schweiz" oder "CH".
+          </para>
 
-      <para><ulink
-      url="http://localhost/kivitendo-erp/login.pl">http://localhost/kivitendo-erp/login.pl</ulink></para>
+          <para>Diese können in der Vorlage mit den jeweiligen Variablen
+          eingetragen werden. Siehe auch: <xref
+          linkend="dokumentenvorlagen-und-variablen"/></para>
 
-      <para>Die Administrationsseite erreichen Sie unter:</para>
+          <para>Der erzeugte QR-Code verwendet Adress-Typ "K" (Kombinierte
+          Adressfelder, 2 Zeilen).</para>
+        </sect3>
 
-      <para><ulink
-      url="http://localhost/kivitendo-erp/controller.pl?action=Admin/login">http://localhost/kivitendo-erp/controller.pl?action=Admin/login</ulink></para>
-    </sect1>
-  </chapter>
+        <sect3>
+          <title>Referenznummer</title>
 
-  <chapter id="features" xreflabel="Features und Funktionen">
-    <title>Features und Funktionen</title>
+          <para>Die Referenznummer wird in Kivitendo erzeugt und setzt sich
+          wiefolgt zusammen:</para>
 
-    <sect1 id="features.periodic-invoices"
-           xreflabel="Wiederkehrende Rechnungen">
-      <title>Wiederkehrende Rechnungen</title>
+          <itemizedlist>
+            <listitem>
+              <para>Bankkonto Identifikationsnummer (6-stellig)</para>
+            </listitem>
 
-      <sect2 id="features.periodic-invoices.introduction"
-             xreflabel="Einführung in wiederkehrende Rechnungen">
-        <title>Einführung</title>
+            <listitem>
+              <para>Kundennummer (6-stellig, mit führenden Nullen
+              aufgefüllt)</para>
+            </listitem>
 
-        <para>Wiederkehrende Rechnungen werden als normale Aufträge definiert
-        und konfiguriert, mit allen dazugehörigen Kunden- und Artikelangaben.
-        Die konfigurierten Aufträge werden später automatisch in Rechnungen
-        umgewandelt, so als ob man den Workflow benutzen würde, und auch die
-        Auftragsnummer wird übernommen, sodass alle wiederkehrenden
-        Rechnungen, die aus einem Auftrag erstellt wurden, später leicht
-        wiederzufinden sind.</para>
-      </sect2>
+            <listitem>
+              <para>Auftragsnummer (7-stellig, mit führenden Nullen
+              aufgefüllt)</para>
+            </listitem>
 
-      <sect2 id="features.periodic-invoices.configuration"
-             xreflabel="Konfiguration von wiederkehrenden Rechnungen">
-        <title>Konfiguration</title>
+            <listitem>
+              <para>Rechnungsnummer (7-stellig, mit führenden Nullen
+              aufgefüllt)</para>
+            </listitem>
 
-        <para>Um einen Auftrag für wiederkehrende Rechnung zu konfigurieren,
-        findet sich beim Bearbeiten des Auftrags ein neuer Knopf
-        "Konfigurieren", der ein neues Fenster öffnet, in dem man die nötigen
-        Parameter einstellen kann. Hinter dem Knopf wird außerdem noch
-        angezeigt, ob der Auftrag als wiederkehrende Rechnung konfiguriert ist
-        oder nicht.</para>
+            <listitem>
+              <para>Prüfziffer (1-stellig, berechnet mittels modulo 10,
+              rekursiv)</para>
+            </listitem>
+          </itemizedlist>
 
-        <para>Folgende Parameter kann man konfigurieren:</para>
+          <para>Es sind lediglich Ziffern erlaubt. Allfällige Prefixe mit
+          Buchstaben werden entfernt und fehlende Stellen werden mit führenden
+          Nullen aufgefüllt.</para>
+        </sect3>
 
-        <variablelist>
-          <varlistentry>
-            <term>Status</term>
+        <sect3>
+          <title>Zusätzliche Variablen für Vorlage</title>
 
-            <listitem>
-              <para>Bei aktiven Rechnungen wird automatisch eine Rechnung
-              erstellt, wenn die Periodizität erreicht ist (z.B. am Anfang eines
-              neuen Monats).</para>
+          <para>Zusätzlich zu den in der Vorlage standardmässig verfügbaren
+          Variablen (siehe <xref linkend="dokumentenvorlagen-und-variablen"/>),
+          werden die folgenden Variablen erzeugt:</para>
 
-              <para>Ist ein Auftrag nicht aktiv, so werden für ihn auch keine
-              wiederkehrenden Rechnungen erzeugt. Stellt man nach längerer
-              nicht-aktiver Zeit einen Auftrag wieder auf aktiv, wird beim
-              nächsten Periodenwechsel für alle Perioden, seit der letzten
-              aktiven Periode, jeweils eine Rechnung erstellt. Möchte man dies
-              verhindern, muss man vorher das Startdatum neu setzen.</para>
+          <variablelist>
+            <varlistentry>
+              <term>ref_number_formatted</term>
 
-              <para>Für gekündigte Aufträge werden nie mehr Rechnungen
-              erstellt. Man kann sich diese Aufträge aber gesondert in den
-              Berichten anzeigen lassen.</para>
-            </listitem>
-          </varlistentry>
+              <listitem>
+                <para>Referenznummer formatiert mit Leerzeichen, z.B.: 21 00000
+                00003 13947 14300 09017</para>
+              </listitem>
+            </varlistentry>
 
-          <varlistentry>
-            <term>Periodizität</term>
+            <varlistentry>
+              <term>iban_formatted</term>
 
-            <listitem>
-              <para>Ob monatlich, quartalsweise oder jährlich auf neue
-              Rechnungen überprüft werden soll. Für jede Periode seit dem
-              Startdatum wird überprüft, ob für die Periode (beginnend immer
-              mit dem ersten Tag der Periode) schon eine Rechnung erstellt
-              wurde. Unter Umständen können bei einem Startdatum in der
-              Vergangenheit gleich mehrere Rechnungen erstellt werden.</para>
-            </listitem>
-          </varlistentry>
+              <listitem>
+                <para>IBAN formatiert mit Leerzeichen</para>
+              </listitem>
+            </varlistentry>
 
-          <varlistentry>
-            <term>Buchen auf</term>
+            <varlistentry>
+              <term>amount_formatted</term>
 
-            <listitem>
-              <para>Das Forderungskonto, in der Regel "Forderungen aus
-              Lieferungen und Leistungen". Das Gegenkonto ergibt sich aus den
-              Buchungsgruppen der betreffenden Waren.</para>
-            </listitem>
-          </varlistentry>
+              <listitem>
+                <para>Betrag formatiert mit Tausendertrennzeichen Leerschlag,
+                z.B.: 1 005.55</para>
+              </listitem>
+            </varlistentry>
+          </variablelist>
+        </sect3>
+      </sect2>
+    </sect1>
 
-          <varlistentry>
-            <term>Startdatum</term>
+    <sect1 id="nomenclature">
+      <title>Nomenklatur</title>
 
-            <listitem>
-              <para>ab welchem Datum auf Rechnungserstellung geprüft werden
-              soll</para>
-            </listitem>
-          </varlistentry>
+      <sect2 id="booking.dates">
+        <title>Datum bei Buchungen</title>
 
-          <varlistentry>
-            <term>Enddatum</term>
+        <para>Seit der Version 3.5 werden für Buchungen in kivitendo
+        einheitlich folgende Bezeichnungen verwendet:</para>
 
-            <listitem>
-              <para>ab wann keine Rechnungen mehr erstellt werden
-              sollen</para>
-            </listitem>
-          </varlistentry>
+        <itemizedlist>
+          <listitem>
+            <para><option>Erfassungsdatum</option> (en: <option>Entry
+            Date</option>, code: <option>Gldate</option>)</para>
 
-          <varlistentry>
-            <term>Automatische Verlängerung um x Monate</term>
+            <para>bezeichnet das Datum, an dem die Buchung in kivitendo
+            erfasst wurde.</para>
+          </listitem>
 
-            <listitem>
-              <para>Sollen die wiederkehrenden Rechnungen bei Erreichen des
-              eingetragenen Enddatums weiterhin erstellt werden, so kann man
-              hier die Anzahl der Monate eingeben, um die das Enddatum
-              automatisch nach hinten geschoben wird.</para>
-            </listitem>
-          </varlistentry>
+          <listitem>
+            <para><option>Buchungsdatum</option> (en: <option>Booking
+            Date</option>, code: <option>Transdate</option>)</para>
 
-          <varlistentry>
-            <term>Drucken</term>
+            <para>bezeichnet das buchhaltungstechnisch für eine Buchung
+            relevante Datum</para>
 
-            <listitem>
-              <para>Sind Drucker konfiguriert, so kann man sich die erstellten
-              Rechnungen auch gleich ausdrucken lassen.</para>
-            </listitem>
-          </varlistentry>
-        </variablelist>
+            <para>Das <option>Rechnungsdatum</option> bei Verkaufs- und
+            Einkaufsrechnungen entspricht dem Buchungsdatum. Das heisst, in
+            Berichten wie dem Buchungsjournal, in denen eine Spalte
+            <option>Buchungsdatum</option> angezeigt werden kann, erscheint
+            hier im Fall von Rechnungen das Rechnungsdatum.</para>
+          </listitem>
 
-        <para>Nach Erstellung der Rechnungen kann eine E-Mail mit
-        Informationen zu den erstellten Rechnungen verschickt werden.
-        Konfiguriert wird dies in der <link
-        linkend="config.config-file.sections-parameters">Konfigurationsdatei</link>
-        <filename>config/kivitendo.conf</filename> im Abschnitt
-        <varname>[periodic_invoices]</varname>.</para>
+          <listitem>
+            <para>Bezieht sich ein verbuchter Beleg auf einen Zeitpunkt, der
+            nicht mit dem Buchungsdatum übereinstimmt, so kann dieses Datum
+            momentan in kivitendo nur unter Bemerkungen erfasst werden.</para>
+
+            <para>Möglicherweise wird für solche Fälle in einer späteren
+            Version von kivitendo ein dritter Datumswert für Buchungen
+            erstellt. (Beispiel: Einkaufsbeleg stammt aus einem früheren Jahr,
+            das bereits buchhaltungstechnisch abgeschlossen wurde, und muss
+            deshalb später verbucht werden.)</para>
+          </listitem>
+        </itemizedlist>
       </sect2>
+    </sect1>
 
-      <sect2 id="features.periodic-invoices.variables">
-        <title>Spezielle Variablen</title>
+    <sect1 id="config.eur">
+      <title>Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung:
+      EUR</title>
 
-        <para>
-          Um die erzeugten Rechnungen individualisieren zu können, werden beim Umwandeln des Auftrags in eine Rechnung einige speziell
-          formatierte Variablen durch für die jeweils aktuelle Abrechnungsperiode gültigen Werte ersetzt. Damit ist es möglich, z.B. den
-          Abrechnungszeitraum explizit auszuweisen. Eine Variable hat dabei die Syntax <literal>&lt;%variablenname%&gt;</literal>.
-        </para>
+      <sect2 id="config.eur.introduction"
+             xreflabel="Einführung in die Konfiguration zur EUR">
+        <title>Einführung</title>
 
-        <para>
-         Sofern es sich um eine Datumsvariable handelt, kann das Ausgabeformat weiter bestimmt werden, indem an den Variablennamen
-         Formatoptionen angehängt werden. Die Syntax sieht dabei wie folgt aus: <literal>&lt;%variablenname
-         FORMAT=Formatinformation%&gt;</literal>. Die zur verfügung stehenden Formatinformationen werden unten genauer beschrieben.
-        </para>
+        <para>kivitendo besaß bis inklusive Version 2.6.3 einen
+        Konfigurationsparameter namens <varname>eur</varname>, der sich in der
+        Konfigurationsdatei <filename>config/kivitendo.conf</filename> (damals
+        noch <filename>config/lx_office.conf</filename>) befand. Somit galt er
+        für alle Mandanten, die in dieser Installation benutzt wurden.</para>
 
-        <para>
-          Diese Variablen werden in den folgenden Elementen des Auftrags ersetzt:
-        </para>
+        <para>Mit der nachfolgenden Version wurde der Parameter zum Einen in
+        die Mandantendatenbank verschoben und dabei auch gleich in drei
+        Einzelparameter aufgeteilt, mit denen sich das Verhalten genauer
+        steuern lässt.</para>
+      </sect2>
 
-        <itemizedlist>
-          <listitem><para>Bemerkungen</para></listitem>
-          <listitem><para>Interne Bemerkungen</para></listitem>
-          <listitem><para>Vorgangsbezeichnung</para></listitem>
-          <listitem><para>In den Beschreibungs- und Langtextfeldern aller Positionen</para></listitem>
-        </itemizedlist>
+      <sect2 id="config.eur.parameters"
+             xreflabel="Konfigurationsparameter für EUR">
+        <title>Konfigurationsparameter</title>
 
-        <para>Die zur Verfügung stehenden Variablen sind die Folgenden:</para>
+        <para>Es gibt drei Parameter, die die Gewinnermittlungsart,
+        Versteuerungsart und die Warenbuchungsmethode regeln:</para>
 
         <variablelist>
           <varlistentry>
-            <term><varname>&lt;%current_quarter%&gt;</varname>, <varname>&lt;%previous_quarter%&gt;</varname>, <varname>&lt;%next_quarter%&gt;</varname></term>
+            <term><varname>profit_determination</varname></term>
 
             <listitem>
-              <para>
-                Aktuelles, vorheriges und nächstes Quartal als Zahl zwischen <literal>1</literal> und <literal>4</literal>.
-              </para>
+              <para>Dieser Parameter legt die Berechnungsmethode für die
+              Gewinnermittlung fest. Er enthält entweder
+              <literal>balance</literal> für
+              Betriebsvermögensvergleich/Bilanzierung oder
+              <literal>income</literal> für die
+              Einnahmen-Überschuss-Rechnung.</para>
             </listitem>
           </varlistentry>
 
           <varlistentry>
-            <term><varname>&lt;%current_month%&gt;</varname>, <varname>&lt;%previous_month%&gt;</varname>, <varname>&lt;%next_month%&gt;</varname></term>
+            <term><varname>accounting_method</varname></term>
 
             <listitem>
-              <para>
-                Aktueller, vorheriger und nächster Monat als Zahl zwischen <literal>1</literal> und <literal>12</literal>.
-              </para>
+              <para>Dieser Parameter steuert die Buchungs- und
+              Berechnungsmethoden für die Versteuerungsart. Er enthält
+              entweder <literal>accrual</literal> für die Soll-Versteuerung
+              oder <literal>cash</literal> für die Ist-Versteuerung.</para>
             </listitem>
           </varlistentry>
 
           <varlistentry>
-            <term><varname>&lt;%current_month_long%&gt;</varname>, <varname>&lt;%previous_month_long%&gt;</varname>, <varname>&lt;%next_month_long%&gt;</varname></term>
+            <term><varname>inventory_system</varname></term>
 
             <listitem>
-              <para>
-                Aktueller, vorheriger und nächster Monat als Name (<literal>Januar</literal>, <literal>Februar</literal> etc.).
-              </para>
+              <para>Dieser Parameter legt die Warenbuchungsmethode fest. Er
+              enthält entweder <literal>perpetual</literal> für die
+              Bestandsmethode oder <literal>periodic</literal> für die
+              Aufwandsmethode.</para>
             </listitem>
           </varlistentry>
+        </variablelist>
 
-          <varlistentry>
-            <term><varname>&lt;%current_year%&gt;</varname>, <varname>&lt;%previous_year%&gt;</varname>, <varname>&lt;%next_year%&gt;</varname></term>
-
+        <para>Zum Vergleich der Funktionalität bis und nach 2.6.3:
+        <varname>eur</varname> = 1 bedeutete Einnahmen-Überschuss-Rechnung,
+        Ist-Versteuerung und Aufwandsmethode. <varname>eur</varname> = 0
+        bedeutete hingegen Bilanzierung, Soll-Versteuerung und
+        Bestandsmethode.</para>
+
+        <para>Die Konfiguration "<varname>eur</varname>" unter
+        <varname>[system]</varname> in der <link
+        linkend="config.config-file">Konfigurationsdatei</link>
+        <filename>config/kivitendo.conf</filename> wird nun nicht mehr
+        benötigt und kann entfernt werden. Dies muss manuell geschehen.</para>
+      </sect2>
+
+      <sect2 id="config.eur.setting-parameters">
+        <title>Festlegen der Parameter</title>
+
+        <para>Beim Anlegen eines neuen Mandanten bzw. einer neuen Datenbank in
+        der Admininstration können diese Optionen nun unabhängig voneinander
+        eingestellt werden.</para>
+
+        <para>Für die Schweiz sind folgende Einstellungen üblich:
+        <itemizedlist>
             <listitem>
-              <para>
-                Aktuelles, vorheriges und nächstes Jahr als vierstellige Jahreszahl (<literal>2013</literal> etc.).
-              </para>
+              <para>Sollversteuerung</para>
             </listitem>
-          </varlistentry>
 
-          <varlistentry>
-            <term><varname>&lt;%period_start_date%&gt;</varname>, <varname>&lt;%period_end_date%&gt;</varname></term>
+            <listitem>
+              <para>Aufwandsmethode</para>
+            </listitem>
 
             <listitem>
-              <para>
-                Formatiertes Datum des ersten und letzten Tages im Abrechnungszeitraum (z.B. bei quartalsweiser Abrechnung und im ersten
-                Quartal von 2013 wären dies der <literal>01.01.2013</literal> und <literal>31.03.2013</literal>).
-              </para>
+              <para>Bilanzierung</para>
             </listitem>
-          </varlistentry>
-        </variablelist>
+          </itemizedlist> Diese Einstellungen werden automatisch beim
+        Erstellen einer neuen Datenbank vorausgewählt, wenn in
+        <filename>config/kivitendo.conf</filename> unter
+        <varname>[system]</varname> <literal>default_manager = swiss</literal>
+        eingestellt ist.</para>
 
-        <para>
-         Die invidiuellen Formatinformationen bestehen aus Paaren von Prozentzeichen und einem Buchstaben, welche beide zusammen durch den
-         dazugehörigen Wert ersetzt werden. So wird z.B. <literal>%Y</literal> durch das viertstellige Jahr ersetzt. Alle möglichen
-         Platzhalter sind:
-        </para>
+        <para>Beim Upgrade bestehender Mandanten wird eur ausgelesen und die
+        Variablen werden so gesetzt, daß sich an der Funktionalität nichts
+        ändert.</para>
 
+        <para>Die aktuelle Konfiguration wird unter Nummernkreise und
+        Standardkonten unter dem neuen Punkt "Einstellungen" (read-only)
+        angezeigt. Unter <guimenu>System</guimenu> →
+        <guisubmenu>Mandantenkonfiguration</guisubmenu> können die
+        Einstellungen auch geändert werden. Dabei ist zu beachten, dass eine
+        Änderung vorhandene Daten so belässt und damit evtl. die Ergebnisse
+        verfälscht. Dies gilt vor Allem für die Warenbuchungsmethode (siehe
+        auch <link linkend="config.eur.inventory-system-perpetual">
+        Bemerkungen zur Bestandsmethode</link>).</para>
+      </sect2>
 
-        <variablelist>
-         <varlistentry>
-          <term><varname>%a</varname></term>
+      <sect2 id="config.eur.inventory-system-perpetual">
+        <title>Bemerkungen zur Bestandsmethode</title>
 
-          <listitem>
-           <para>Der abgekürzte Wochentagsname.</para>
-          </listitem>
-         </varlistentry>
+        <para>Die Bestandsmethode ist eigentlich eine sehr elegante Methode,
+        funktioniert in kivitendo aber nur unter bestimmten Bedingungen:
+        Voraussetzung ist, daß auch immer alle Einkaufsrechnungen gepflegt
+        werden, und man beim Jahreswechsel nicht mit einer leeren Datenbank
+        anfängt, da bei jedem Verkauf anhand der gesamten Rechnungshistorie
+        der Einkaufswert der Ware nach dem FIFO-Prinzip aus den
+        Einkaufsrechnungen berechnet wird.</para>
 
-         <varlistentry>
-          <term><varname>%A</varname></term>
+        <para>Die Bestandsmethode kann vom Prinzip her also nur funktioneren,
+        wenn man mit den Buchungen bei Null anfängt, und man kann auch nicht
+        im laufenden Betrieb von der Aufwandsmethode zur Bestandsmethode
+        wechseln.</para>
+      </sect2>
 
-          <listitem>
-           <para>Der ausgeschriebene Wochentagsname.</para>
-          </listitem>
-         </varlistentry>
+      <sect2 id="config.eur.knonw-issues">
+        <title>Bekannte Probleme</title>
 
-         <varlistentry>
-          <term><varname>%b</varname></term>
+        <para>Bei bestimmten Berichten kann man derzeit noch inviduell
+        einstellen, ob man nach Ist- oder Sollversteuerung auswertet, und es
+        werden im Code Variablen wie $accrual oder $cash gesetzt. Diese
+        Codestellen wurden noch nicht angepasst, sondern nur die, wo bisher
+        die Konfigurationsvariable
+        <varname>$::lx_office_conf{system}-&gt;{eur}</varname> ausgewertet
+        wurde.</para>
 
-          <listitem>
-           <para>Der abgekürzte Monatsname.</para>
-          </listitem>
-         </varlistentry>
+        <para>Es fehlen Hilfetext beim Neuanlegen eines Mandanten, was die
+        Optionen bewirken, z.B. mit zwei Standardfällen.</para>
+      </sect2>
+    </sect1>
 
-         <varlistentry>
-          <term><varname>%B</varname></term>
+    <sect1 id="config.skr04-update-3804">
+      <title>SKR04 19% Umstellung für innergemeinschaftlichen Erwerb</title>
 
-          <listitem>
-           <para>Der ausgeschriebene Monatsname.</para>
-          </listitem>
-         </varlistentry>
+      <sect2 id="config.skr04-update-3804.introduction">
+        <title>Einführung</title>
 
-         <varlistentry>
-          <term><varname>%C</varname></term>
+        <para>Die Umsatzsteuerumstellung auf 19% für SKR04 für die
+        Steuerschlüssel "EU ohne USt-ID Nummer" ist erst 2010 erfolgt.
+        kivitendo beinhaltet ein Upgradeskript, das das Konto 3804 automatisch
+        erstellt und die Steuereinstellungen korrekt einstellt. Hat der
+        Benutzer aber schon selber das Konto 3804 angelegt, oder gab es schon
+        Buchungen im Zeitraum nach dem 01.01.2007 auf das Konto 3803, wird das
+        Upgradeskript vorsichtshalber nicht ausgeführt, da der Benutzer sich
+        vielleicht schon selbst geholfen hat und mit seinen Änderungen
+        zufrieden ist. Die korrekten Einstellungen kann man aber auch per Hand
+        ausführen. Nachfolgend werden die entsprechenden Schritte anhand von
+        Screenshots dargestellt.</para>
 
-          <listitem>
-           <para>Das Jahrhundert (Jahr/100) als eine zweistellige Zahl.</para>
-          </listitem>
-         </varlistentry>
+        <para>Für den Fall, daß Buchungen mit der Steuerschlüssel "EU ohne
+        USt.-IdNr." nach dem 01.01.2007 erfolgt sind, ist davon auszugehen,
+        dass diese mit dem alten Umsatzsteuersatz von 16% gebucht worden sind,
+        und diese Buchungen sollten entsprechend kontrolliert werden.</para>
+      </sect2>
 
-         <varlistentry>
-          <term><varname>%d</varname></term>
+      <sect2 id="config.skr04-update-3804.create-chart">
+        <title>Konto 3804 manuell anlegen</title>
 
-          <listitem>
-           <para>Der Monatstag als Zahl zwischen 01 und 31.</para>
-          </listitem>
-         </varlistentry>
+        <para>Die folgenden Schritte sind notwendig, um das Konto manuell
+        anzulegen und zu konfigurieren. Zuerst wird in
+        <guimenu>System</guimenu> → <guisubmenu>Kontenübersicht</guisubmenu> →
+        <guimenuitem>Konto erfassen</guimenuitem> das Konto angelegt.</para>
 
-         <varlistentry>
-          <term><varname>%D</varname></term>
+        <screenshot>
+          <screeninfo>Konto 3804 erfassen</screeninfo>
 
-          <listitem>
-           <para>Entspricht %m/%d/%y (amerikanisches Datumsformat).</para>
-          </listitem>
-         </varlistentry>
+          <mediaobject>
+            <imageobject>
+              <imagedata fileref="images/skr04-update-3804/konto3804.png"/>
+            </imageobject>
+          </mediaobject>
+        </screenshot>
 
-         <varlistentry>
-          <term><varname>%e</varname></term>
+        <para>Als Zweites muss Steuergruppe 13 für Konto 3803 angepasst
+        werden. Dazu unter <guimenu>System</guimenu> →
+        <guisubmenu>Steuern</guisubmenu> →
+        <guimenuitem>Bearbeiten</guimenuitem> den Eintrag mit Steuerschlüssel
+        13 auswählen und ihn wie im folgenden Screenshot angezeigt
+        anpassen.</para>
 
-          <listitem>
-           <para>Wie %d (Monatstag als Zahl zwischen 1 und 31), allerdings werden führende Nullen durch Leerzeichen ersetzt.</para>
-          </listitem>
-         </varlistentry>
+        <screenshot>
+          <screeninfo>Steuerschlüssel 13 für 3803 (16%) anpassen</screeninfo>
 
-         <varlistentry>
-          <term><varname>%F</varname></term>
+          <mediaobject>
+            <imageobject>
+              <imagedata fileref="images/skr04-update-3804/steuer3803.png"/>
+            </imageobject>
+          </mediaobject>
+        </screenshot>
 
-          <listitem>
-           <para>Entspricht %Y-%m-%d (das ISO-8601-Datumsformat).</para>
-          </listitem>
-         </varlistentry>
+        <para>Als Drittes wird ein neuer Eintrag mit Steuerschlüssel 13 für
+        Konto 3804 (19%) angelegt. Dazu unter <guimenu>System</guimenu> →
+        <guisubmenu>Steuern</guisubmenu> → <guimenuitem>Erfassen</guimenuitem>
+        auswählen und die Werte aus dem Screenshot übernehmen.</para>
 
-         <varlistentry>
-          <term><varname>%j</varname></term>
+        <screenshot>
+          <screeninfo>Steuerschlüssel 13 für 3804 (19%) anlegen</screeninfo>
 
-          <listitem>
-           <para>Der Tag im Jahr als Zahl zwischen 001 und 366 inklusive.</para>
-          </listitem>
-         </varlistentry>
+          <mediaobject>
+            <imageobject>
+              <imagedata fileref="images/skr04-update-3804/steuer3804.png"/>
+            </imageobject>
+          </mediaobject>
+        </screenshot>
 
-         <varlistentry>
-          <term><varname>%m</varname></term>
+        <para>Als Nächstes sind alle Konten anzupassen, die als
+        Steuerautomatikkonto die 3803 haben, sodass sie ab dem 1.1.2007 auch
+        Steuerautomatik auf 3804 bekommen. Dies betrifft in der
+        Standardkonfiguration die Konten 4315 und 4726. Als Beispiel für 4315
+        müssen Sie dazu unter <guimenu>System</guimenu> →
+        <guisubmenu>Kontenübersicht</guisubmenu> → <guimenuitem>Konten
+        anzeigen</guimenuitem> das Konto 4315 anklicken und die Einstellungen
+        wie im Screenshot gezeigt vornehmen.</para>
 
-          <listitem>
-           <para>Der Monat als Zahl zwischen 01 und 12 inklusive.</para>
-          </listitem>
-         </varlistentry>
+        <screenshot>
+          <screeninfo>Konto 4315 anpassen</screeninfo>
 
-         <varlistentry>
-          <term><varname>%u</varname></term>
+          <mediaobject>
+            <imageobject>
+              <imagedata fileref="images/skr04-update-3804/konto4315.png"/>
+            </imageobject>
+          </mediaobject>
+        </screenshot>
 
-          <listitem>
-           <para>Der Wochentag als Zahl zwischen 1 und 7 inklusive, wobei die 1 dem Montag entspricht.</para>
-          </listitem>
-         </varlistentry>
+        <para>Als Letztes sollte die Steuerliste unter
+        <guimenu>System</guimenu> → <guisubmenu>Steuern</guisubmenu> →
+        <guimenuitem>Bearbeiten</guimenuitem> kontrolliert werden. Zum
+        Vergleich der Screenshot.</para>
 
-         <varlistentry>
-          <term><varname>%U</varname></term>
+        <screenshot>
+          <screeninfo>Steuerliste vergleichen</screeninfo>
 
-          <listitem>
-           <para>Die Wochennummer als Zahl zwischen 00 und 53 inklusive, wobei der erste Sonntag im Jahr das Startdatum von Woche 01 ist.</para>
-          </listitem>
-         </varlistentry>
+          <mediaobject>
+            <imageobject>
+              <imagedata fileref="images/skr04-update-3804/steuerliste.png"/>
+            </imageobject>
+          </mediaobject>
+        </screenshot>
+      </sect2>
+    </sect1>
 
-         <varlistentry>
-          <term><varname>%V</varname></term>
+    <sect1 id="config.bilanz">
+      <title>Verhalten des Bilanzberichts</title>
 
-          <listitem>
-           <para>Die ISO-8601:1988-Wochennummer als Zahl zwischen 01 und 53 inklusive, wobei Woche 01 die erste Woche, von der mindestens vier Tage im Jahr liegen; Montag ist erster Tag der Woche.</para>
-          </listitem>
-         </varlistentry>
+      <para>Bis Version 3.0 wurde "closedto" ("Bücher schließen zum") als
+      Grundlage für das Startdatum benutzt. Schließt man die Bücher allerdings
+      monatsweise führt dies zu falschen Werten.</para>
 
-         <varlistentry>
-          <term><varname>%w</varname></term>
+      <para>In der Mandantenkonfiguration kann man dieses Verhalten genau
+      einstellen indem man:</para>
 
-          <listitem>
-           <para>Der Wochentag als Zahl zwischen 0 und 6 inklusive, wobei die 0 dem Sonntag entspricht.</para>
-          </listitem>
-         </varlistentry>
+      <itemizedlist>
+        <listitem>
+          <para>weiterhin closed_to benutzt (Default, es ändert sich nichts zu
+          vorher)</para>
+        </listitem>
 
-         <varlistentry>
-          <term><varname>%W</varname></term>
+        <listitem>
+          <para>immer den Jahresanfang nimmt (1.1. relativ zum
+          Stichtag)</para>
+        </listitem>
 
-          <listitem>
-           <para>Die Wochennummer als Zahl zwischen 00 und 53 inklusive, wobei der erste Montag im Jahr das Startdatum von Woche 01 ist.</para>
-          </listitem>
-         </varlistentry>
+        <listitem>
+          <para>immer die letzte Eröffnungsbuchung als Startdatum nimmt</para>
 
-         <varlistentry>
-          <term><varname>%y</varname></term>
+          <para>- mit Jahresanfang als Alternative wenn es keine EB-Buchungen
+          gibt</para>
 
-          <listitem>
-           <para>Das Jahr als zweistellige Zahl zwischen 00 und 99 inklusive.</para>
-          </listitem>
-         </varlistentry>
+          <para>- oder mit "alle Buchungen" als Alternative"</para>
+        </listitem>
 
-         <varlistentry>
-          <term><varname>%Y</varname></term>
+        <listitem>
+          <para>mit Jahresanfang als Alternative wenn es keine EB-Buchungen
+          gibt</para>
+        </listitem>
 
-          <listitem>
-           <para>Das Jahr als vierstellige Zahl.</para>
-          </listitem>
-         </varlistentry>
+        <listitem>
+          <para>immer alle Buchungen seit Beginn der Datenbank nimmt</para>
+        </listitem>
+      </itemizedlist>
 
-         <varlistentry>
-          <term><varname>%%</varname></term>
+      <para>Folgende Hinweise zu den Optionen: Das "Bücher schließen Datum"
+      ist sinnvoll, wenn man nur komplette Jahre schließt. Bei Wirtschaftsjahr
+      = Kalendarjahr entspricht dies aber auch dem Jahresanfang. "Alle
+      Buchungen" kann z.B. sinnvoll sein wenn man ohne Jahresabschluß
+      durchbucht. Eröffnungsbuchung mit "alle Buchungen" als Fallback ist z.B.
+      sinnvoll, wenn man am sich Anfang des zweiten Buchungsjahres befindet,
+      und noch keinen Jahreswechsel und auch noch keine EB-Buchungen hat. Bei
+      den Optionen mit EB-Buchungen wird vorausgesetzt, daß diese immer am 1.
+      Tag des Wirtschaftsjahres gebucht werden. Zur Sicherheit wird das
+      Startdatum im Bilanzbericht jetzt zusätzlich zum Stichtag mit angezeigt.
+      Das hilft auch bei der Kontrolle für den Abgleich mit der GuV bzw.
+      Erfolgsrechnung.</para>
+    </sect1>
 
-          <listitem>
-           <para>Das Prozentzeichen selber.</para>
-          </listitem>
-         </varlistentry>
-        </variablelist>
+    <sect1 id="config.erfolgsrechnung">
+      <title>Erfolgsrechnung</title>
+
+      <para>Seit der Version 3.4.1 existiert in kivitendo der Bericht
+      <emphasis role="bold"> Erfolgsrechnung</emphasis>.</para>
+
+      <para>Die Erfolgsrechnung kann in der Mandantenkonfiguration unter
+      Features an- oder abgeschaltet werden. Mit der Einstellung
+      <varname>default_manager = swiss </varname> in der
+      <filename>config/kivitendo.conf</filename> wird beim neu Erstellen einer
+      Datenbank automatisch die Anzeige der Erfolgsrechnung im Menü
+      <guimenu>Berichte </guimenu> ausgewählt und ersetzt dort die GUV.</para>
+
+      <para>Im Gegensatz zur GUV werden bei der Erfolgsrechnung sämtliche
+      Aufwands- und Erlöskonten einzeln aufgelistet (analog zur Bilanz),
+      sortiert nach ERTRAG und AUFWAND.</para>
+
+      <para>Bei den Konteneinstellungen muss bei jedem Konto, das in der
+      Erfolgsrechnung erscheinen soll, unter <varname>Sonstige
+      Einstellungen/Erfolgsrechnung</varname> entweder
+      <literal>01.Ertrag</literal> oder <literal>06.Aufwand</literal>
+      ausgewählt werden.</para>
+
+      <para>Wird bei einem Erlöskonto <literal>06.Aufwand</literal>
+      ausgewählt, so wird dieses Konto als Aufwandsminderung unter AUFWAND
+      aufgelistet.</para>
+
+      <para>Wird bei einem Aufwandskonto <literal>01.Ertrag</literal>
+      ausgewählt, so wird dieses Konto als Ertragsminderung unter ERTRAG
+      aufgelistet.</para>
+
+      <para>Soll bei einer bereits bestehenden Buchhaltung in Zukunft
+      zusätzlich die Erfolgsrechnung als Bericht verwendet werden, so müssen
+      die Einstellungen zu allen Erlös- und Aufwandskonten unter
+      <varname>Sonstige Einstellungen/Erfolgsrechnung</varname> überprüft und
+      allenfalls neu gesetzt werden.</para>
+    </sect1>
 
-        <para>
-         Anwendungsbeispiel für die Ausgabe, von welchem Monat und Jahr bis zu welchem Monat und Jahr die aktuelle Abrechnungsperiode
-         dauert: <literal>Abrechnungszeitrum: &lt;%period_start_date FORMAT=%m/%Y%&gt; bis &lt;%period_end_date FORMAT=%m/%Y%&gt;</literal>
-        </para>
-      </sect2>
+    <sect1 id="config.rounding">
+      <title>Rundung in Verkaufsbelegen</title>
 
-      <sect2 id="features.periodic-invoices.reports">
-        <title>Auflisten</title>
+      <para>In der Schweiz hat die kleinste aktuell benutzte Münze den Wert
+      von 5 Rappen (0.05 CHF).</para>
 
-        <para>Unter Verkauf-&gt;Berichte-&gt;Aufträge finden sich zwei neue
-        Checkboxen, "Wiederkehrende Rechnungen aktiv" und "Wiederkehrende
-        Rechnungen inaktiv", mit denen man sich einen Überglick über die
-        wiederkehrenden Rechnungen verschaffen kann.</para>
-      </sect2>
+      <para>Auch wenn im elektronischen Zahlungsverkehr Beträge mit einer
+      Genauigkeit von 0.01 CHF verwendet werden können, ist es trotzdem nach
+      wie vor üblich, Rechnungen mit auf 0.05 CHF gerundeten Beträgen
+      auszustellen.</para>
 
-      <sect2 id="features.periodic-invoices.task-server">
-        <title>Erzeugung der eigentlichen Rechnungen</title>
+      <para>In kivitendo kann seit der Version 3.4.1 die Einstellung für eine
+      solche Rundung pro Mandant / Datenbank festgelegt werden.</para>
 
-        <para>Die zeitliche und periodische Überprüfung, ob eine
-        wiederkehrende Rechnung automatisch erstellt werden soll, geschieht
-        durch den <link linkend="config.task-server">Taskserver</link>, einen
-        externen Dienst, der automatisch beim Start des Servers gestartet
-        werden sollte.</para>
-      </sect2>
+      <para>Die Einstellung wird beim Erstellen der Datenbank bei
+      <literal>Genauigkeit</literal> festgelegt. Sie kann anschliessend über
+      das Webinterface von kivitendo nicht mehr verändert werden.</para>
 
-      <sect2 id="features.periodic-invoices.create-for-current-month">
-        <title>Erste Rechnung für aktuellen Monat erstellen</title>
+      <para>Abhängig vom Wert für <varname>default_manager</varname> in
+      <filename>config/kivitendo.conf</filename> werden dabei folgende Werte
+      voreingestellt:</para>
 
-        <para>Will man im laufenden Monat eine monatlich wiederkehrende
-        Rechnung inkl. des laufenden Monats starten, stellt man das Startdatum
-        auf den Monatsanfang und wartet ein paar Minuten, bis der Taskserver
-        den neu konfigurieren Auftrag erkennt und daraus eine Rechnung
-        generiert hat. Alternativ setzt man das Startdatum auf den
-        Monatsersten des Folgemonats und erstellt die erste Rechnung direkt
-        manuell über den Workflow.</para>
-      </sect2>
-    </sect1>
-    <sect1 id="features.bank"
-           xreflabel="bankerweiterung">
-      <title>Bankerweiterung</title>
+      <itemizedlist>
+        <listitem>
+          <para>0.05 (default_manager = swiss)</para>
+        </listitem>
 
-      <sect2 id="features.bank.introduction"
-             xreflabel="Einführung in die Bankerweiterung">
-        <title>Einführung</title>
+        <listitem>
+          <para>0.01 (default_manager = german)</para>
+        </listitem>
+      </itemizedlist>
 
-        <para>Die Beschreibung der Bankerweiterung befindet sich derzeit noch im Wiki und soll von dort später hierhin übernommen werden:</para>
+      <para>Der Wert wird in der Datenbank in der Tabelle <varname>defaults
+      </varname>in der Spalte <varname>precision</varname> gespeichert.</para>
 
-        <para><ulink
-        url="http://redmine.kivitendo-premium.de/projects/forum/wiki/Bankerweiterung">http://redmine.kivitendo-premium.de/projects/forum/wiki/Bankerweiterung</ulink></para>
-      </sect2>
-    </sect1>
-    <sect1 id="dokumentenvorlagen-und-variablen">
-      <title>Dokumentenvorlagen und verfügbare Variablen</title>
+      <para>In allen Verkaufsangeboten, Verkaufsaufträgen, Verkaufsrechnungen
+      und Verkaufsgutschriften wird der Endbetrag inkl. MWST gerundet, wenn
+      dieser nicht der eingestellten Genauigkeit entspricht.</para>
 
-      <sect2 id="dokumentenvorlagen-und-variablen.einführung">
-        <title>Einführung</title>
+      <para>Beim Buchen einer Verkaufsrechnung wird der Rundungsbetrag
+      automatisch auf die in der Mandantenkonfiguration festgelegten
+      Standardkonten für Rundungserträge bzw. Rundungsaufwendungen
+      gebucht.</para>
 
-        <para>Dies ist eine Auflistung der Standard-Dokumentenvorlagen und
-        aller zur Bearbeitung verfügbaren Variablen. Eine Variable wird in
-        einer Vorlage durch ihren Inhalt ersetzt, wenn sie in der Form
-        <function>&lt;%variablenname%&gt;</function> verwendet wird. Für
-        LaTeX- und HTML-Vorlagen kann man die Form dieser Tags auch verändern
-        (siehe <xref
-        linkend="dokumentenvorlagen-und-variablen.tag-style" />).</para>
+      <para>(Die berechnete MWST wird durch den Rundungsbetrag nicht mehr
+      verändert.)</para>
 
-        <para>Früher wurde hier nur über LaTeX gesprochen. Inzwischen
-        unterstützt kivitendo aber auch OpenDocument-Vorlagen. Sofern es nicht
-        ausdrücklich eingeschränkt wird, gilt das im Folgenden gesagte für
-        alle Vorlagenarten.</para>
+      <para>Die in den Druckvorlagen zur Verfügung stehenden Variablen
+      <varname>quototal</varname>, <varname>ordtotal</varname> bzw.
+      <varname>invtotal</varname> enthalten den gerundeten Betrag.</para>
 
-        <para>Insgesamt sind technisch gesehen eine ganze Menge mehr Variablen
-        verfügbar als hier aufgelistet werden. Die meisten davon können
-        allerdings innerhalb einer solchen Vorlage nicht sinnvoll verwendet
-        werden. Wenn eine Auflistung dieser Variablen gewollt ist, so kann
-        diese wie folgt erhalten werden:</para>
+      <para><emphasis role="bold">Achtung:</emphasis> Werden Verkaufsbelege in
+      anderen Währungen als der Standardwährung erstellt, so muss in kivitendo
+      ab Version 3.4.1 die Genauigkeit 0.01 verwendet werden.</para>
 
-        <itemizedlist>
-          <listitem>
-            <para><filename>SL/Form.pm</filename> öffnen und am Anfang die
-            Zeile "<command>use Data::Dumper;</command>" einfügen.</para>
-          </listitem>
+      <para>Das heisst, Firmen in der Schweiz, die teilweise
+      Verkaufsrechnungen in Euro oder anderen Währungen erstellen wollen,
+      müssen beim Erstellen der Datenbank als Genauigkeit 0.01 wählen und
+      können zur Zeit die 5er Rundung noch nicht nutzen.</para>
+    </sect1>
 
-          <listitem>
-            <para>In <filename>Form.pm</filename> die Funktion
-            <function>parse_template</function> suchen und hier die Zeile
-            <command>print(STDERR Dumper($self));</command> einfügen.</para>
-          </listitem>
+    <sect1 id="config.client">
+      <title>Einstellungen pro Mandant</title>
 
-          <listitem>
-            <para>Einmal per Browser die gewünschte Vorlage "benutzen", z.B.
-            ein PDF für eine Rechnung erzeugen.</para>
-          </listitem>
+      <para>Einige Einstellungen können von einem Benutzer mit dem <link
+      linkend="Zusammenhänge">Recht</link> "Administration (Für die Verwaltung
+      der aktuellen Instanz aus einem Userlogin heraus)" gemacht werden. Diese
+      Einstellungen sind dann für die aktuellen Mandanten-Datenbank gültig.
+      Die Einstellungen sind unter <guimenu>System</guimenu> →
+      <guisubmenu>Mandantenkonfiguration</guisubmenu> erreichbar.</para>
+
+      <para>Bitte beachten Sie die Hinweise zu den einzelnen Einstellungen.
+      Einige Einstellungen sollten nicht ohne Weiteres im laufenden Betrieb
+      geändert werden (siehe auch <link
+      linkend="config.eur.inventory-system-perpetual">Bemerkungen zu
+      Bestandsmethode</link>).</para>
 
-          <listitem>
-            <para>Im <filename>error.log</filename> Apache steht die Ausgabe
-            der Variablen <varname>$self</varname> in der Form <varname>'key'
-            =&gt; 'value',</varname>. Alle <varname>key</varname>s sind
-            verfügbar.</para>
-          </listitem>
-        </itemizedlist>
-      </sect2>
-
-      <sect2 id="dokumentenvorlagen-und-variablen.variablen-ausgeben">
-        <title>Variablen ausgeben</title>
-
-        <para>Um eine Variable auszugeben, müssen sie einfach nur zwischen die
-        Tags geschrieben werden, also z.B.
-        <varname>&lt;%variablenname%&gt;</varname>.</para>
+      <para>Die Einstellungen <literal>show_bestbefore</literal> und
+      <literal>payments_changeable</literal> aus dem Abschnitt
+      <literal>features</literal> und die Einstellungen im Abschnitt
+      <literal>datev_check</literal> (sofern schon vorhanden) der <link
+      linkend="config.config-file">kivitendo-Konfigurationsdatei</link> werden
+      bei einem Datenbankupdate einer älteren Version automatisch übernommen.
+      Diese Einträge können danach aus der Konfigurationsdatei entfernt
+      werden.</para>
+    </sect1>
 
-        <para>Optional kann man auch mit Leerzeichen getrennte Flags angeben,
-        die man aber nur selten brauchen wird. Die Syntax sieht also so aus:
-        <varname>&lt;%variablenname FLAG1 FLAG2%&gt;</varname>. Momentan
-        werden die folgenden Flags unterstützt:</para>
+    <sect1 id="kivitendo-ERP-verwenden">
+      <title>kivitendo ERP verwenden</title>
 
-        <itemizedlist>
-          <listitem>
-            <para><option>NOFORMAT</option> gilt nur für Zahlenwerte und gibt
-            den Wert ohne Formatierung, also ohne Tausendertrennzeichen mit
-            mit einem Punkt als Dezimaltrennzeichen aus. Nützlich z.B., wenn
-            damit in der Vorlage z.B. von LaTeX gerechnet werden soll.</para>
-          </listitem>
+      <para>Nach erfolgreicher Installation ist der Loginbildschirm unter
+      folgender URL erreichbar:</para>
 
-          <listitem>
-            <para><option>NOESCAPE</option> unterdrückt das Escapen von
-            Sonderzeichen für die Vorlagensprache. Wenn also in einer
-            Variablen bereits gültiger LaTeX-Code steht und dieser von LaTeX
-            auch ausgewertet und nicht wortwörtlich angezeigt werden soll, so
-            ist dieses Flag sinnvoll.</para>
-          </listitem>
-        </itemizedlist>
+      <para><ulink
+      url="http://localhost/kivitendo-erp/login.pl">http://localhost/kivitendo-erp/login.pl</ulink></para>
 
-        <para>Beispiel:</para>
+      <para>Die Administrationsseite erreichen Sie unter:</para>
 
-        <programlisting>&lt;%quototal NOFORMAT%&gt;</programlisting>
-      </sect2>
+      <para><ulink
+      url="http://localhost/kivitendo-erp/controller.pl?action=Admin/login">http://localhost/kivitendo-erp/controller.pl?action=Admin/login</ulink></para>
+    </sect1>
+  </chapter>
 
-      <sect2 id="dokumentenvorlagen-und-variablen.verwendung-in-druckbefehlen">
-        <title>Verwendung in Druckbefehlen</title>
+  <chapter id="features" xreflabel="Features und Funktionen">
+    <title>Features und Funktionen</title>
 
-        <para>In der Admininstration können Drucker definiert werden. Auch im
-        dort eingebbaren Druckbefehl können die hier aufgelisteten Variablen
-        und Kontrollstrukturen verwendet werden. Ihr Inhalt wird dabei nach
-        den Regeln der gängigen Shells formatiert, sodass Sonderzeichen wie
-        <function>`...`</function> nicht zu unerwünschtem Verhalten
-        führen.</para>
+    <sect1 id="features.periodic-invoices"
+           xreflabel="Wiederkehrende Rechnungen">
+      <title>Wiederkehrende Rechnungen</title>
 
-        <para>Dies erlaubt z.B. die Definition eines Faxes als Druckerbefehl,
-        für das die Telefonnummer eines Ansprechpartners als Teil der
-        Kommandozeile verwendet wird. Für ein fiktives Kommando könnte das
-        z.B. wie folgt aussehen:</para>
+      <sect2 id="features.periodic-invoices.introduction"
+             xreflabel="Einführung in wiederkehrende Rechnungen">
+        <title>Einführung</title>
 
-        <programlisting>send_fax --number &lt;%if cp_phone2%&gt;&lt;%cp_phone2%&gt;&lt;%else%&gt;&lt;%cp_phone1%&gt;&lt;%end%&gt;</programlisting>
+        <para>Wiederkehrende Rechnungen werden als normale Aufträge definiert
+        und konfiguriert, mit allen dazugehörigen Kunden- und Artikelangaben.
+        Die konfigurierten Aufträge werden später automatisch in Rechnungen
+        umgewandelt, so als ob man den Workflow benutzen würde, und auch die
+        Auftragsnummer wird übernommen, sodass alle wiederkehrenden
+        Rechnungen, die aus einem Auftrag erstellt wurden, später leicht
+        wiederzufinden sind.</para>
       </sect2>
 
-      <sect2 id="dokumentenvorlagen-und-variablen.tag-style"
-             xreflabel="Anfang und Ende der Tags verändern">
-        <title>Anfang und Ende der Tags verändern</title>
+      <sect2 id="features.periodic-invoices.configuration"
+             xreflabel="Konfiguration von wiederkehrenden Rechnungen">
+        <title>Konfiguration</title>
 
-        <para>Der Standardstil für Tags sieht vor, dass ein Tag mit dem
-        Kleinerzeichen und einem Prozentzeichen beginnt und mit dem
-        Prozentzeichen und dem Größerzeichen endet, beispielsweise
-        <function>&lt;%customer%&gt;</function>. Da diese Form aber z.B. in
-        LaTeX zu Problemen führen kann, weil das Prozentzeichen dort
-        Kommentare einleitet, kann pro HTML- oder LaTeX-Dokumentenvorlage der
-        Stil umgestellt werden.</para>
+        <para>Um einen Auftrag für wiederkehrende Rechnung zu konfigurieren,
+        findet sich beim Bearbeiten des Auftrags ein neuer Knopf
+        "Konfigurieren", der ein neues Fenster öffnet, in dem man die nötigen
+        Parameter einstellen kann. Hinter dem Knopf wird außerdem noch
+        angezeigt, ob der Auftrag als wiederkehrende Rechnung konfiguriert ist
+        oder nicht.</para>
 
-        <para>Dazu werden in die Datei Zeilen geschrieben, die mit dem für das
-        Format gültigen Kommentarzeichen anfangen, dann
-        <function>config:</function> enthalten, die entsprechende Option
-        setzen und bei HTML-Dokumentenvorlagen mit dem Kommentarendzeichen
-        enden. Beispiel für LaTeX:</para>
+        <para>Folgende Parameter kann man konfigurieren:</para>
 
-        <programlisting>% config: tag-style=($ $)</programlisting>
+        <variablelist>
+          <varlistentry>
+            <term>Status</term>
 
-        <para>Dies würde kivitendo dazu veranlassen, Variablen zu ersetzen,
-        wenn sie wie folgt aussehen: <function>($customer$)</function>. Das
-        äquivalente Beispiel für HTML-Dokumentenvorlagen sieht so aus:</para>
+            <listitem>
+              <para>Bei aktiven Rechnungen wird automatisch eine Rechnung
+              erstellt, wenn die Periodizität erreicht ist (z.B. am Anfang
+              eines neuen Monats).</para>
 
-        <programlisting>&lt;!-- config: tag-style=($ $) --&gt;</programlisting>
-      </sect2>
+              <para>Ist ein Auftrag nicht aktiv, so werden für ihn auch keine
+              wiederkehrenden Rechnungen erzeugt. Stellt man nach längerer
+              nicht-aktiver Zeit einen Auftrag wieder auf aktiv, wird beim
+              nächsten Periodenwechsel für alle Perioden, seit der letzten
+              aktiven Periode, jeweils eine Rechnung erstellt. Möchte man dies
+              verhindern, muss man vorher das Startdatum neu setzen.</para>
 
-      <sect2 id="dokumentenvorlagen-und-variablen.zuordnung-dateinamen">
-        <title>Zuordnung von den Dateinamen zu den Funktionen</title>
+              <para>Für gekündigte Aufträge werden nie mehr Rechnungen
+              erstellt. Man kann sich diese Aufträge aber gesondert in den
+              Berichten anzeigen lassen.</para>
+            </listitem>
+          </varlistentry>
 
-        <para>Diese folgende kurze Auflistung zeigt, welche Vorlage bei
-        welcher Funktion ausgelesen wird. Dabei ist die Dateiendung
-        "<filename>.ext</filename>" geeignet zu ersetzen:
-        "<filename>.tex</filename>" für LaTeX-Vorlagen und
-        "<filename>.odt</filename>" für OpenDocument-Vorlagen.</para>
+          <varlistentry>
+            <term>Periodizität</term>
+
+            <listitem>
+              <para>Ob monatlich, quartalsweise oder jährlich auf neue
+              Rechnungen überprüft werden soll. Für jede Periode seit dem
+              Startdatum wird überprüft, ob für die Periode (beginnend immer
+              mit dem ersten Tag der Periode) schon eine Rechnung erstellt
+              wurde. Unter Umständen können bei einem Startdatum in der
+              Vergangenheit gleich mehrere Rechnungen erstellt werden.</para>
+            </listitem>
+          </varlistentry>
 
-        <variablelist>
           <varlistentry>
-            <term><filename>bin_list.ext</filename></term>
+            <term>Buchen auf</term>
 
             <listitem>
-              <para>Lagerliste</para>
+              <para>Das Forderungskonto, in der Regel "Forderungen aus
+              Lieferungen und Leistungen". Das Gegenkonto ergibt sich aus den
+              Buchungsgruppen der betreffenden Waren.</para>
             </listitem>
           </varlistentry>
 
           <varlistentry>
-            <term><filename>check.ext</filename></term>
+            <term>Startdatum</term>
 
             <listitem>
-              <para>?</para>
+              <para>ab welchem Datum auf Rechnungserstellung geprüft werden
+              soll</para>
             </listitem>
           </varlistentry>
 
           <varlistentry>
-            <term><filename>invoice.ext</filename></term>
+            <term>Enddatum</term>
 
             <listitem>
-              <para>Rechnung</para>
+              <para>ab wann keine Rechnungen mehr erstellt werden
+              sollen</para>
             </listitem>
           </varlistentry>
 
           <varlistentry>
-            <term><filename>packing_list.ext</filename></term>
+            <term>Automatische Verlängerung um x Monate</term>
 
             <listitem>
-              <para>Packliste</para>
+              <para>Sollen die wiederkehrenden Rechnungen bei Erreichen des
+              eingetragenen Enddatums weiterhin erstellt werden, so kann man
+              hier die Anzahl der Monate eingeben, um die das Enddatum
+              automatisch nach hinten geschoben wird.</para>
             </listitem>
           </varlistentry>
 
           <varlistentry>
-            <term><filename>pick_list.ext</filename></term>
+            <term>Drucken</term>
 
             <listitem>
-              <para>Sammelliste</para>
+              <para>Sind Drucker konfiguriert, so kann man sich die erstellten
+              Rechnungen auch gleich ausdrucken lassen.</para>
             </listitem>
           </varlistentry>
+        </variablelist>
+
+        <para>Nach Erstellung der Rechnungen kann eine E-Mail mit
+        Informationen zu den erstellten Rechnungen verschickt werden.
+        Konfiguriert wird dies in der <link
+        linkend="config.config-file.sections-parameters">Konfigurationsdatei</link>
+        <filename>config/kivitendo.conf</filename> im Abschnitt
+        <varname>[periodic_invoices]</varname>.</para>
+      </sect2>
+
+      <sect2 id="features.periodic-invoices.variables">
+        <title>Spezielle Variablen</title>
+
+        <para>Um die erzeugten Rechnungen individualisieren zu können, werden
+        beim Umwandeln des Auftrags in eine Rechnung einige speziell
+        formatierte Variablen durch für die jeweils aktuelle
+        Abrechnungsperiode gültigen Werte ersetzt. Damit ist es möglich, z.B.
+        den Abrechnungszeitraum explizit auszuweisen. Eine Variable hat dabei
+        die Syntax <literal>&lt;%variablenname%&gt;</literal>.</para>
+
+        <para>Sofern es sich um eine Datumsvariable handelt, kann das
+        Ausgabeformat weiter bestimmt werden, indem an den Variablennamen
+        Formatoptionen angehängt werden. Die Syntax sieht dabei wie folgt aus:
+        <literal>&lt;%variablenname FORMAT=Formatinformation%&gt;</literal>.
+        Die zur verfügung stehenden Formatinformationen werden unten genauer
+        beschrieben.</para>
+
+        <para>Diese Variablen können auch beim automatischen Versand der
+        erzeugten Rechnungen per E-Mail genutzt werden, indem sie in den
+        Feldern für den Betreff oder die Nachricht verwendet werden.</para>
+
+        <para>Diese Variablen werden in den folgenden Elementen des Auftrags
+        ersetzt:</para>
+
+        <itemizedlist>
+          <listitem>
+            <para>Bemerkungen</para>
+          </listitem>
+
+          <listitem>
+            <para>Interne Bemerkungen</para>
+          </listitem>
+
+          <listitem>
+            <para>Vorgangsbezeichnung</para>
+          </listitem>
+
+          <listitem>
+            <para>In den Beschreibungs- und Langtextfeldern aller
+            Positionen</para>
+          </listitem>
+        </itemizedlist>
+
+        <para>Die zur Verfügung stehenden Variablen sind die Folgenden:</para>
 
+        <variablelist>
           <varlistentry>
-            <term><filename>purchase_delivery_order.ext</filename></term>
+            <term><varname>&lt;%current_quarter%&gt;</varname>,
+            <varname>&lt;%previous_quarter%&gt;</varname>,
+            <varname>&lt;%next_quarter%&gt;</varname></term>
 
             <listitem>
-              <para>Lieferschein (Einkauf)</para>
+              <para>Aktuelles, vorheriges und nächstes Quartal als Zahl
+              zwischen <literal>1</literal> und <literal>4</literal>.</para>
             </listitem>
           </varlistentry>
 
           <varlistentry>
-            <term><filename>purcharse_order.ext</filename></term>
+            <term><varname>&lt;%current_month%&gt;</varname>,
+            <varname>&lt;%previous_month%&gt;</varname>,
+            <varname>&lt;%next_month%&gt;</varname></term>
 
             <listitem>
-              <para>Bestellung an Lieferanten</para>
+              <para>Aktueller, vorheriger und nächster Monat als Zahl zwischen
+              <literal>1</literal> und <literal>12</literal>.</para>
             </listitem>
           </varlistentry>
 
           <varlistentry>
-            <term><filename>request_quotation.ext</filename></term>
+            <term><varname>&lt;%current_month_long%&gt;</varname>,
+            <varname>&lt;%previous_month_long%&gt;</varname>,
+            <varname>&lt;%next_month_long%&gt;</varname></term>
 
             <listitem>
-              <para>Anfrage an Lieferanten</para>
+              <para>Aktueller, vorheriger und nächster Monat als Name
+              (<literal>Januar</literal>, <literal>Februar</literal>
+              etc.).</para>
             </listitem>
           </varlistentry>
 
           <varlistentry>
-            <term><filename>sales_delivery_order.ext</filename></term>
+            <term><varname>&lt;%current_year%&gt;</varname>,
+            <varname>&lt;%previous_year%&gt;</varname>,
+            <varname>&lt;%next_year%&gt;</varname></term>
 
             <listitem>
-              <para>Lieferschein (Verkauf)</para>
+              <para>Aktuelles, vorheriges und nächstes Jahr als vierstellige
+              Jahreszahl (<literal>2013</literal> etc.).</para>
             </listitem>
           </varlistentry>
 
           <varlistentry>
-            <term><filename>sales_order.ext</filename></term>
+            <term><varname>&lt;%period_start_date%&gt;</varname>,
+            <varname>&lt;%period_end_date%&gt;</varname></term>
 
             <listitem>
-              <para>Bestellung</para>
+              <para>Formatiertes Datum des ersten und letzten Tages im
+              Abrechnungszeitraum (z.B. bei quartalsweiser Abrechnung und im
+              ersten Quartal von 2013 wären dies der
+              <literal>01.01.2013</literal> und
+              <literal>31.03.2013</literal>).</para>
             </listitem>
           </varlistentry>
+        </variablelist>
 
+        <para>Die invidiuellen Formatinformationen bestehen aus Paaren von
+        Prozentzeichen und einem Buchstaben, welche beide zusammen durch den
+        dazugehörigen Wert ersetzt werden. So wird z.B. <literal>%Y</literal>
+        durch das viertstellige Jahr ersetzt. Alle möglichen Platzhalter
+        sind:</para>
+
+        <variablelist>
           <varlistentry>
-            <term><filename>sales_quotation.ext</filename></term>
+            <term><varname>%a</varname></term>
 
             <listitem>
-              <para>Angebot an Kunden</para>
+              <para>Der abgekürzte Wochentagsname.</para>
             </listitem>
           </varlistentry>
 
           <varlistentry>
-            <term><filename>zahlungserinnerung.ext</filename></term>
+            <term><varname>%A</varname></term>
 
             <listitem>
-              <para>Mahnung (Dateiname im Programm konfigurierbar)</para>
+              <para>Der ausgeschriebene Wochentagsname.</para>
             </listitem>
           </varlistentry>
 
           <varlistentry>
-            <term><filename>zahlungserinnerung_invoice.ext</filename></term>
+            <term><varname>%b</varname></term>
 
             <listitem>
-              <para>Rechnung über Mahngebühren (Dateiname im Programm
-              konfigurierbar)</para>
+              <para>Der abgekürzte Monatsname.</para>
             </listitem>
           </varlistentry>
-        </variablelist>
-      </sect2>
 
-      <sect2 id="dokumentenvorlagen-und-variablen.dateinamen-erweitert">
-        <title>Sprache, Drucker und E-Mail</title>
+          <varlistentry>
+            <term><varname>%B</varname></term>
 
-        <para>Angeforderte Sprache und Druckerkürzel in den Dateinamen mit
-        eingearbeitet. So wird aus der Vorlage
-        <filename>sales_order.ext</filename> bei Sprache
-        <function>de</function> und Druckerkürzel <function>lpr2</function>
-        der Vorlagenname <filename>sales_order_de_lpr2.ext</filename>.
-        Zusätzlich können für E-Mails andere Vorlagen erstellt werden, diese
-        bekommen dann noch das Kürzel <filename>_email</filename>, der
-        vollständige Vorlagenname wäre dann
-        <filename>sales_order_email_de_lpr2.ext</filename>. In allen Fällen
-        kann eine Standarddatei <filename>default.ext</filename> hinterlegt
-        werden. Diese wird verwendet, wenn keine der anderen Varianten
-        gefunden wird.</para>
+            <listitem>
+              <para>Der ausgeschriebene Monatsname.</para>
+            </listitem>
+          </varlistentry>
 
-        <para>Die vollständige Suchreihenfolge für einen Verkaufsauftrag mit
-        der Sprache "de" und dem Drucker "lpr2", der per E-Mail im Format PDF
-        verschickt wird, ist:</para>
+          <varlistentry>
+            <term><varname>%C</varname></term>
 
-        <orderedlist>
-          <listitem>
-            <para><filename>sales_order_email_de_lpr2.tex</filename></para>
-          </listitem>
+            <listitem>
+              <para>Das Jahrhundert (Jahr/100) als eine zweistellige
+              Zahl.</para>
+            </listitem>
+          </varlistentry>
 
-          <listitem>
-            <para><filename>sales_order_de_lpr2.tex</filename></para>
-          </listitem>
+          <varlistentry>
+            <term><varname>%d</varname></term>
 
+            <listitem>
+              <para>Der Monatstag als Zahl zwischen 01 und 31.</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><varname>%D</varname></term>
+
+            <listitem>
+              <para>Entspricht %m/%d/%y (amerikanisches Datumsformat).</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><varname>%e</varname></term>
+
+            <listitem>
+              <para>Wie %d (Monatstag als Zahl zwischen 1 und 31), allerdings
+              werden führende Nullen durch Leerzeichen ersetzt.</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><varname>%F</varname></term>
+
+            <listitem>
+              <para>Entspricht %Y-%m-%d (das ISO-8601-Datumsformat).</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><varname>%j</varname></term>
+
+            <listitem>
+              <para>Der Tag im Jahr als Zahl zwischen 001 und 366
+              inklusive.</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><varname>%m</varname></term>
+
+            <listitem>
+              <para>Der Monat als Zahl zwischen 01 und 12 inklusive.</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><varname>%u</varname></term>
+
+            <listitem>
+              <para>Der Wochentag als Zahl zwischen 1 und 7 inklusive, wobei
+              die 1 dem Montag entspricht.</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><varname>%U</varname></term>
+
+            <listitem>
+              <para>Die Wochennummer als Zahl zwischen 00 und 53 inklusive,
+              wobei der erste Sonntag im Jahr das Startdatum von Woche 01
+              ist.</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><varname>%V</varname></term>
+
+            <listitem>
+              <para>Die ISO-8601:1988-Wochennummer als Zahl zwischen 01 und 53
+              inklusive, wobei Woche 01 die erste Woche, von der mindestens
+              vier Tage im Jahr liegen; Montag ist erster Tag der
+              Woche.</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><varname>%w</varname></term>
+
+            <listitem>
+              <para>Der Wochentag als Zahl zwischen 0 und 6 inklusive, wobei
+              die 0 dem Sonntag entspricht.</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><varname>%W</varname></term>
+
+            <listitem>
+              <para>Die Wochennummer als Zahl zwischen 00 und 53 inklusive,
+              wobei der erste Montag im Jahr das Startdatum von Woche 01
+              ist.</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><varname>%y</varname></term>
+
+            <listitem>
+              <para>Das Jahr als zweistellige Zahl zwischen 00 und 99
+              inklusive.</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><varname>%Y</varname></term>
+
+            <listitem>
+              <para>Das Jahr als vierstellige Zahl.</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><varname>%%</varname></term>
+
+            <listitem>
+              <para>Das Prozentzeichen selber.</para>
+            </listitem>
+          </varlistentry>
+        </variablelist>
+
+        <para>Anwendungsbeispiel für die Ausgabe, von welchem Monat und Jahr
+        bis zu welchem Monat und Jahr die aktuelle Abrechnungsperiode dauert:
+        <literal>Abrechnungszeitrum: &lt;%period_start_date FORMAT=%m/%Y%&gt;
+        bis &lt;%period_end_date FORMAT=%m/%Y%&gt;</literal></para>
+
+        <para>Beim automatischen Versand der Rechnugen via E-Mail können neben diesen speziellen Variablen auch einige Eigenschaften der
+        Rechnung selber als Variablen im Betreff &amp; dem Text der E-Mails genutzt werden. Beispiele sind
+        <varname>&lt;%invnumber%&gt;</varname> für die Rechnungsnummber oder <varname>&lt;transaction_description%&gt;</varname> für die
+        Vorgangsbezeichnung. Diese Variablen stehen beim Erzeugen der Rechnung logischerweise noch nicht zur Verfügung.</para>
+      </sect2>
+
+      <sect2 id="features.periodic-invoices.reports">
+        <title>Auflisten</title>
+
+        <para>Unter Verkauf-&gt;Berichte-&gt;Aufträge finden sich zwei neue
+        Checkboxen, "Wiederkehrende Rechnungen aktiv" und "Wiederkehrende
+        Rechnungen inaktiv", mit denen man sich einen Überglick über die
+        wiederkehrenden Rechnungen verschaffen kann.</para>
+      </sect2>
+
+      <sect2 id="features.periodic-invoices.task-server">
+        <title>Erzeugung der eigentlichen Rechnungen</title>
+
+        <para>Die zeitliche und periodische Überprüfung, ob eine
+        wiederkehrende Rechnung automatisch erstellt werden soll, geschieht
+        durch den <link linkend="config.task-server">Task-Server</link>, einen
+        externen Dienst, der automatisch beim Start des Servers gestartet
+        werden sollte.</para>
+      </sect2>
+
+      <sect2 id="features.periodic-invoices.create-for-current-month">
+        <title>Erste Rechnung für aktuellen Monat erstellen</title>
+
+        <para>Will man im laufenden Monat eine monatlich wiederkehrende
+        Rechnung inkl. des laufenden Monats starten, stellt man das Startdatum
+        auf den Monatsanfang und wartet ein paar Minuten, bis der Task-Server
+        den neu konfigurieren Auftrag erkennt und daraus eine Rechnung
+        generiert hat. Alternativ setzt man das Startdatum auf den
+        Monatsersten des Folgemonats und erstellt die erste Rechnung direkt
+        manuell über den Workflow.</para>
+      </sect2>
+    </sect1>
+
+    <sect1 id="features.bank" xreflabel="bankerweiterung">
+      <title>Bankerweiterung</title>
+
+      <sect2 id="features.bank.introduction"
+             xreflabel="Einführung in die Bankerweiterung">
+        <title>Einführung</title>
+
+        <para>Die Beschreibung der Bankerweiterung befindet sich derzeit noch
+        im Wiki und soll von dort später hierhin übernommen werden:</para>
+
+        <para><ulink
+        url="http://redmine.kivitendo-premium.de/projects/forum/wiki/Bankerweiterung">http://redmine.kivitendo-premium.de/projects/forum/wiki/Bankerweiterung</ulink></para>
+      </sect2>
+    </sect1>
+
+    <sect1 id="dokumentenvorlagen-und-variablen">
+      <title>Dokumentenvorlagen und verfügbare Variablen</title>
+
+      <sect2 id="dokumentenvorlagen-und-variablen.einführung">
+        <title>Einführung</title>
+
+        <para>Dies ist eine Auflistung der Standard-Dokumentenvorlagen und
+        aller zur Bearbeitung verfügbaren Variablen. Eine Variable wird in
+        einer Vorlage durch ihren Inhalt ersetzt, wenn sie in der Form
+        <function>&lt;%variablenname%&gt;</function> verwendet wird. Für
+        LaTeX- und HTML-Vorlagen kann man die Form dieser Tags auch verändern
+        (siehe <xref
+        linkend="dokumentenvorlagen-und-variablen.tag-style"/>).</para>
+
+        <para>kivitendo unterstützt LaTeX-, HTML- und OpenDocument-Vorlagen.
+        Sofern es nicht ausdrücklich eingeschränkt wird, gilt das im Folgenden
+        gesagte für alle Vorlagenarten.</para>
+
+        <para>Insgesamt sind technisch gesehen eine ganze Menge mehr Variablen
+        verfügbar als hier aufgelistet werden. Die meisten davon können
+        allerdings innerhalb einer solchen Vorlage nicht sinnvoll verwendet
+        werden. Wenn eine Auflistung dieser Variablen gewollt ist, so kann
+        diese wie folgt erhalten werden:</para>
+
+        <itemizedlist>
           <listitem>
-            <para><filename>sales_order.tex</filename></para>
+            <para><filename>SL/Form.pm</filename> öffnen und am Anfang die
+            Zeile "<command>use Data::Dumper;</command>" einfügen.</para>
+          </listitem>
+
+          <listitem>
+            <para>In <filename>Form.pm</filename> die Funktion
+            <function>parse_template</function> suchen und hier die Zeile
+            <command>print(STDERR Dumper($self));</command> einfügen.</para>
+          </listitem>
+
+          <listitem>
+            <para>Einmal per Browser die gewünschte Vorlage "benutzen", z.B.
+            ein PDF für eine Rechnung erzeugen.</para>
+          </listitem>
+
+          <listitem>
+            <para>Im <filename>error.log</filename> Apache steht die Ausgabe
+            der Variablen <varname>$self</varname> in der Form <varname>'key'
+            =&gt; 'value',</varname>. Alle <varname>key</varname>s sind
+            verfügbar.</para>
+          </listitem>
+        </itemizedlist>
+      </sect2>
+
+      <sect2 id="dokumentenvorlagen-und-variablen.variablen-ausgeben">
+        <title>Variablen ausgeben</title>
+
+        <para>Um eine Variable auszugeben, müssen sie einfach nur zwischen die
+        Tags geschrieben werden, also z.B.
+        <varname>&lt;%variablenname%&gt;</varname>.</para>
+
+        <para>Optional kann man auch mit Leerzeichen getrennte Flags angeben,
+        die man aber nur selten brauchen wird. Die Syntax sieht also so aus:
+        <varname>&lt;%variablenname FLAG1 FLAG2%&gt;</varname>. Momentan
+        werden die folgenden Flags unterstützt:</para>
+
+        <itemizedlist>
+          <listitem>
+            <para><option>NOFORMAT</option> gilt nur für Zahlenwerte und gibt
+            den Wert ohne Formatierung, also ohne Tausendertrennzeichen mit
+            mit einem Punkt als Dezimaltrennzeichen aus. Nützlich z.B., wenn
+            damit in der Vorlage z.B. von LaTeX gerechnet werden soll.</para>
           </listitem>
 
-          <listitem>
-            <para><filename>default.tex</filename></para>
-          </listitem>
-        </orderedlist>
+          <listitem>
+            <para><option>NOESCAPE</option> unterdrückt das Escapen von
+            Sonderzeichen für die Vorlagensprache. Wenn also in einer
+            Variablen bereits gültiger LaTeX-Code steht und dieser von LaTeX
+            auch ausgewertet und nicht wortwörtlich angezeigt werden soll, so
+            ist dieses Flag sinnvoll.</para>
+          </listitem>
+        </itemizedlist>
+
+        <para>Beispiel:</para>
+
+        <programlisting>&lt;%quototal NOFORMAT%&gt;</programlisting>
+      </sect2>
+
+      <sect2 id="dokumentenvorlagen-und-variablen.verwendung-in-druckbefehlen">
+        <title>Verwendung in Druckbefehlen</title>
+
+        <para>In der Admininstration können Drucker definiert werden. Auch im
+        dort eingebbaren Druckbefehl können die hier aufgelisteten Variablen
+        und Kontrollstrukturen verwendet werden. Ihr Inhalt wird dabei nach
+        den Regeln der gängigen Shells formatiert, sodass Sonderzeichen wie
+        <function>`...`</function> nicht zu unerwünschtem Verhalten
+        führen.</para>
+
+        <para>Dies erlaubt z.B. die Definition eines Faxes als Druckerbefehl,
+        für das die Telefonnummer eines Ansprechpartners als Teil der
+        Kommandozeile verwendet wird. Für ein fiktives Kommando könnte das
+        z.B. wie folgt aussehen:</para>
+
+        <programlisting>send_fax --number &lt;%if cp_phone2%&gt;&lt;%cp_phone2%&gt;&lt;%else%&gt;&lt;%cp_phone1%&gt;&lt;%end%&gt;</programlisting>
+      </sect2>
+
+      <sect2 id="dokumentenvorlagen-und-variablen.tag-style"
+             xreflabel="Anfang und Ende der Tags verändern">
+        <title>Anfang und Ende der Tags verändern</title>
+
+        <para>Der Standardstil für Tags sieht vor, dass ein Tag mit dem
+        Kleinerzeichen und einem Prozentzeichen beginnt und mit dem
+        Prozentzeichen und dem Größerzeichen endet, beispielsweise
+        <function>&lt;%customer%&gt;</function>. Da diese Form aber z.B. in
+        LaTeX zu Problemen führen kann, weil das Prozentzeichen dort
+        Kommentare einleitet, kann pro HTML- oder LaTeX-Dokumentenvorlage der
+        Stil umgestellt werden.</para>
+
+        <para>Dazu werden in die Datei Zeilen geschrieben, die mit dem für das
+        Format gültigen Kommentarzeichen anfangen, dann
+        <function>config:</function> enthalten, die entsprechende Option
+        setzen und bei HTML-Dokumentenvorlagen mit dem Kommentarendzeichen
+        enden. Beispiel für LaTeX:</para>
+
+        <programlisting>% config: tag-style=($ $)</programlisting>
+
+        <para>Dies würde kivitendo dazu veranlassen, Variablen zu ersetzen,
+        wenn sie wie folgt aussehen: <function>($customer$)</function>. Das
+        äquivalente Beispiel für HTML-Dokumentenvorlagen sieht so aus:</para>
+
+        <programlisting>&lt;!-- config: tag-style=($ $) --&gt;</programlisting>
+      </sect2>
+
+      <sect2 id="dokumentenvorlagen-und-variablen.zuordnung-dateinamen">
+        <title>Zuordnung von den Dateinamen zu den Funktionen</title>
+
+        <para>Diese folgende kurze Auflistung zeigt, welche Vorlage bei
+        welcher Funktion ausgelesen wird. Dabei ist die Dateiendung
+        "<filename>.ext</filename>" geeignet zu ersetzen:
+        "<filename>.tex</filename>" für LaTeX-Vorlagen und
+        "<filename>.odt</filename>" für OpenDocument-Vorlagen.</para>
+
+        <variablelist>
+          <varlistentry>
+            <term><filename>bin_list.ext</filename></term>
+
+            <listitem>
+              <para>Lagerliste</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><filename>check.ext</filename></term>
+
+            <listitem>
+              <para>?</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><filename>invoice.ext</filename></term>
+
+            <listitem>
+              <para>Rechnung</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><filename>packing_list.ext</filename></term>
+
+            <listitem>
+              <para>Packliste</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><filename>pick_list.ext</filename></term>
+
+            <listitem>
+              <para>Sammelliste</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><filename>purchase_delivery_order.ext</filename></term>
+
+            <listitem>
+              <para>Lieferschein (Einkauf)</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><filename>purcharse_order.ext</filename></term>
+
+            <listitem>
+              <para>Bestellung an Lieferanten</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><filename>request_quotation.ext</filename></term>
+
+            <listitem>
+              <para>Anfrage an Lieferanten</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><filename>sales_delivery_order.ext</filename></term>
+
+            <listitem>
+              <para>Lieferschein (Verkauf)</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><filename>sales_order.ext</filename></term>
+
+            <listitem>
+              <para>Bestellung</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><filename>sales_quotation.ext</filename></term>
+
+            <listitem>
+              <para>Angebot an Kunden</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><filename>zahlungserinnerung.ext</filename></term>
+
+            <listitem>
+              <para>Mahnung (Dateiname im Programm konfigurierbar)</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term><filename>zahlungserinnerung_invoice.ext</filename></term>
+
+            <listitem>
+              <para>Rechnung über Mahngebühren (Dateiname im Programm
+              konfigurierbar)</para>
+            </listitem>
+          </varlistentry>
+        </variablelist>
+      </sect2>
+
+      <sect2 id="dokumentenvorlagen-und-variablen.dateinamen-erweitert">
+        <title>Sprache, Drucker und E-Mail</title>
+
+        <para>Angeforderte Sprache und Druckerkürzel in den Dateinamen mit
+        eingearbeitet. So wird aus der Vorlage
+        <filename>sales_order.ext</filename> bei Sprache
+        <function>de</function> und Druckerkürzel <function>lpr2</function>
+        der Vorlagenname <filename>sales_order_de_lpr2.ext</filename>.
+        Zusätzlich können für E-Mails andere Vorlagen erstellt werden, diese
+        bekommen dann noch das Kürzel <filename>_email</filename>, der
+        vollständige Vorlagenname wäre dann
+        <filename>sales_order_email_de_lpr2.ext</filename>. In allen Fällen
+        kann eine Standarddatei <filename>default.ext</filename> hinterlegt
+        werden. Diese wird verwendet, wenn keine der anderen Varianten
+        gefunden wird.</para>
+
+        <para>Die vollständige Suchreihenfolge für einen Verkaufsauftrag mit
+        der Sprache "de" und dem Drucker "lpr2", der per E-Mail im Format PDF
+        verschickt wird, ist:</para>
+
+        <orderedlist>
+          <listitem>
+            <para><filename>sales_order_email_de_lpr2.tex</filename></para>
+          </listitem>
+
+          <listitem>
+            <para><filename>sales_order_de_lpr2.tex</filename></para>
+          </listitem>
+
+          <listitem>
+            <para><filename>sales_order.tex</filename></para>
+          </listitem>
+
+          <listitem>
+            <para><filename>default.tex</filename></para>
+          </listitem>
+        </orderedlist>
+
+        <para>Die kurzen Varianten dieser Vorlagentitel müssen dann entweder
+        Standardwerte anzeigen, oder die angeforderten Werte selbst auswerten,
+        siehe dazu <xref
+        linkend="dokumentenvorlagen-und-variablen.allgemeine-variablen.meta"/>.</para>
+      </sect2>
+
+      <sect2 id="dokumentenvorlagen-und-variablen.allgemeine-variablen">
+        <title>Allgemeine Variablen, die in allen Vorlagen vorhanden
+        sind</title>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.allgemeine-variablen.meta"
+               xreflabel="Metainformationen zur angeforderten Vorlage">
+          <title>Metainformationen zur angeforderten Vorlage</title>
+
+          <para>Diese Variablen liefern Informationen darüber welche Variante
+          einer Vorlage der Benutzer angefragt hat. Sie sind nützlich für
+          Vorlagenautoren, die aus einer zentralen Layoutvorlage die einzelnen
+          Formulare einbinden möchten.</para>
+
+          <variablelist>
+            <varlistentry>
+              <term><varname>template_meta.formname</varname></term>
+
+              <listitem>
+                <para>Basisname der Vorlage. Identisch mit der <link
+                linkend="dokumentenvorlagen-und-variablen.zuordnung-dateinamen">Zurordnung
+                zu den Dateinamen</link> ohne die Erweiterung. Ein
+                Verkaufsauftrag enthält hier
+                <constant>sales_order</constant>.</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>template_meta.language.description</varname></term>
+
+              <listitem>
+                <para>Beschreibung der verwendeten Sprache</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>template_meta.language.template_code</varname></term>
+
+              <listitem>
+                <para>Vorlagenkürzel der verwendeten Sprache, identisch mit
+                dem Kürzel das im Dateinamen verwendetet wird.</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>template_meta.language.output_numberformat</varname></term>
+
+              <listitem>
+                <para>Zahlenformat der verwendeten Sprache in der Form
+                "<constant>1.000,00</constant>". Experimentell! Nur
+                interessant für Vorlagen die mit unformatierten Werten
+                arbeiten.</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>template_meta.language.output_dateformat</varname></term>
+
+              <listitem>
+                <para>Datumsformat der verwendeten Sprache in der Form
+                "<constant>dd.mm.yyyy</constant>". Experimentell! Nur
+                interessant für Vorlagen die mit unformatierten Werten
+                arbeiten.</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>template_meta.format</varname></term>
+
+              <listitem>
+                <para>Das angeforderte Format. Kann im Moment die Werte
+                <constant>pdf</constant>, <constant>postscript</constant>,
+                <constant>html</constant>, <constant>opendocument</constant>,
+                <constant>opendocument_pdf</constant> und
+                <constant>excel</constant> enthalten.</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>template_meta.extension</varname></term>
+
+              <listitem>
+                <para>Dateierweiterung, wie im Dateinamen. Wird aus
+                <constant>format</constant> entschieden.</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>template_meta.media</varname></term>
+
+              <listitem>
+                <para>Ausgabemedium. Kann zur Zeit die Werte
+                <constant>screen</constant> für Bildschirm,
+                <constant>email</constant> für E-Mail (triggert das
+                <constant>_email</constant> Kürzel im Dateinamen),
+                <constant>printer</constant> für Drucker, und
+                <constant>queue</constant> für Warteschlange enthalten.</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>template_meta.printer.description</varname></term>
+
+              <listitem>
+                <para>Beschreibung des ausgewählten Druckers</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>template_meta.printer.template_code</varname></term>
+
+              <listitem>
+                <para>Vorlagenürzel des ausgewählten Druckers, identisch mit
+                dem Kürzel das im Dateinamen verwendetet wird.</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>template_meta.tmpfile</varname></term>
+
+              <listitem>
+                <para>Datei-Prefix für temporäre Dateien.</para>
+              </listitem>
+            </varlistentry>
+          </variablelist>
+        </sect3>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.allgemeine-variablen.kunden-lieferanten">
+          <title>Stammdaten von Kunden und Lieferanten</title>
+
+          <variablelist>
+            <varlistentry>
+              <term><varname>account_number</varname></term>
+
+              <listitem>
+                <para>Kontonummer</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>bank</varname></term>
+
+              <listitem>
+                <para>Name der Bank</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>bank_code</varname></term>
+
+              <listitem>
+                <para>Bankleitzahl</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>bic</varname></term>
+
+              <listitem>
+                <para>Bank-Identifikations-Code (Bank Identifier Code,
+                BIC)</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>business</varname></term>
+
+              <listitem>
+                <para>Kunden-/Lieferantentyp</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>city</varname></term>
+
+              <listitem>
+                <para>Stadt</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>contact</varname></term>
+
+              <listitem>
+                <para>Kontakt</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>country</varname></term>
+
+              <listitem>
+                <para>Land</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>c_vendor_id</varname></term>
+
+              <listitem>
+                <para>Lieferantennummer beim Kunden (nur Kunden)</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>v_customer_id</varname></term>
+
+              <listitem>
+                <para>Kundennummer beim Lieferanten (nur Lieferanten)</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>cp_email</varname></term>
+
+              <listitem>
+                <para>Email des Ansprechpartners</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>cp_givenname</varname></term>
+
+              <listitem>
+                <para>Vorname des Ansprechpartners</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>cp_greeting</varname></term>
+
+              <listitem>
+                <para>Anrede des Ansprechpartners</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>cp_name</varname></term>
+
+              <listitem>
+                <para>Name des Ansprechpartners</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>cp_phone1</varname></term>
+
+              <listitem>
+                <para>Telefonnummer 1 des Ansprechpartners</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>cp_phone2</varname></term>
+
+              <listitem>
+                <para>Telefonnummer 2 des Ansprechpartners</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>cp_title</varname></term>
+
+              <listitem>
+                <para>Titel des Ansprechpartners</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>creditlimit</varname></term>
+
+              <listitem>
+                <para>Kreditlimit</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>customeremail</varname></term>
+
+              <listitem>
+                <para>Email des Kunden; nur für Kunden</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>customerfax</varname></term>
+
+              <listitem>
+                <para>Faxnummer des Kunden; nur für Kunden</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>customernotes</varname></term>
+
+              <listitem>
+                <para>Bemerkungen beim Kunden; nur für Kunden</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>customernumber</varname></term>
+
+              <listitem>
+                <para>Kundennummer; nur für Kunden</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>customerphone</varname></term>
+
+              <listitem>
+                <para>Telefonnummer des Kunden; nur für Kunden</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>discount</varname></term>
+
+              <listitem>
+                <para>Rabatt</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>email</varname></term>
+
+              <listitem>
+                <para>Emailadresse</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>fax</varname></term>
+
+              <listitem>
+                <para>Faxnummer</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>gln</varname></term>
+
+              <listitem>
+                <para>GLN (Globale Lokationsnummer)</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>greeting</varname></term>
+
+              <listitem>
+                <para>Anrede</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>homepage</varname></term>
+
+              <listitem>
+                <para>Homepage</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>iban</varname></term>
+
+              <listitem>
+                <para>Internationale Kontonummer (International Bank Account
+                Number, IBAN)</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>language</varname></term>
+
+              <listitem>
+                <para>Sprache</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>name</varname></term>
+
+              <listitem>
+                <para>Firmenname</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>natural_person</varname></term>
+
+              <listitem>
+                <para>Flag "natürliche Person"; Siehe auch
+                <xref linkend="dokumentenvorlagen-und-variablen.anrede"/></para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>payment_description</varname></term>
+
+              <listitem>
+                <para>Name der Zahlart</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>payment_terms</varname></term>
+
+              <listitem>
+                <para>Zahlungskonditionen</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>phone</varname></term>
+
+              <listitem>
+                <para>Telefonnummer</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>shiptocity</varname></term>
+
+              <listitem>
+                <para>Stadt (Lieferadresse) <link
+                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>shiptocontact</varname></term>
 
-        <para>Die kurzen Varianten dieser Vorlagentitel müssen dann entweder
-        Standardwerte anzeigen, oder die angeforderten Werte selbst auswerten,
-        siehe dazu <xref
-        linkend="dokumentenvorlagen-und-variablen.allgemeine-variablen.meta" />.</para>
-      </sect2>
+              <listitem>
+                <para>Kontakt (Lieferadresse) <link
+                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
+              </listitem>
+            </varlistentry>
 
-      <sect2 id="dokumentenvorlagen-und-variablen.allgemeine-variablen">
-        <title>Allgemeine Variablen, die in allen Vorlagen vorhanden
-        sind</title>
+            <varlistentry>
+              <term><varname>shiptocountry</varname></term>
 
-        <sect3 id="dokumentenvorlagen-und-variablen.allgemeine-variablen.meta"
-               xreflabel="Metainformationen zur angeforderten Vorlage">
-          <title>Metainformationen zur angeforderten Vorlage</title>
+              <listitem>
+                <para>Land (Lieferadresse) <link
+                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
+              </listitem>
+            </varlistentry>
 
-          <para>Diese Variablen liefern Informationen darüber welche Variante
-          einer Vorlage der Benutzer angefragt hat. Sie sind nützlich für
-          Vorlagenautoren, die aus einer zentralen Layoutvorlage die einzelnen
-          Formulare einbinden möchten.</para>
+            <varlistentry>
+              <term><varname>shiptodepartment_1</varname></term>
+
+              <listitem>
+                <para>Abteilung 1 (Lieferadresse) <link
+                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
+              </listitem>
+            </varlistentry>
 
-          <variablelist>
             <varlistentry>
-              <term><varname>template_meta.formname</varname></term>
+              <term><varname>shiptodepartment_2</varname></term>
 
               <listitem>
-                <para>Basisname der Vorlage. Identisch mit der <link
-                linkend="dokumentenvorlagen-und-variablen.zuordnung-dateinamen">Zurordnung
-                zu den Dateinamen</link> ohne die Erweiterung. Ein
-                Verkaufsauftrag enthält hier
-                <constant>sales_order</constant>.</para>
+                <para>Abteilung 2 (Lieferadresse) <link
+                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>template_meta.language.description</varname></term>
+              <term><varname>shiptoemail</varname></term>
 
               <listitem>
-                <para>Beschreibung der verwendeten Sprache</para>
+                <para>Email (Lieferadresse) <link
+                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>template_meta.language.template_code</varname></term>
+              <term><varname>shiptofax</varname></term>
 
               <listitem>
-                <para>Vorlagenürzel der verwendeten Sprache, identisch mit dem
-                Kürzel das im Dateinamen verwendetet wird.</para>
+                <para>Fax (Lieferadresse) <link
+                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>template_meta.language.output_numberformat</varname></term>
+              <term><varname>shiptogln</varname></term>
 
               <listitem>
-                <para>Zahlenformat der verwendeten Sprache in der Form
-                "<constant>1.000,00</constant>". Experimentell! Nur
-                interessant für Vorlagen die mit unformatierten Werten
-                arbeiten.</para>
+                <para>GLN (Globale Lokationsnummer) (Lieferadresse) <link
+                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>template_meta.language.output_dateformat</varname></term>
+              <term><varname>shiptoname</varname></term>
 
               <listitem>
-                <para>Datumsformat der verwendeten Sprache in der Form
-                "<constant>dd.mm.yyyy</constant>". Experimentell! Nur
-                interessant für Vorlagen die mit unformatierten Werten
-                arbeiten.</para>
+                <para>Firmenname (Lieferadresse) <link
+                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>template_meta.format</varname></term>
+              <term><varname>shiptophone</varname></term>
 
               <listitem>
-                <para>Das angeforderte Format. Kann im Moment die Werte
-                <constant>pdf</constant>, <constant>postscript</constant>,
-                <constant>html</constant>, <constant>opendocument</constant>,
-                <constant>opendocument_pdf</constant> und
-                <constant>excel</constant> enthalten.</para>
+                <para>Telefonnummer (Lieferadresse) <link
+                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>template_meta.extension</varname></term>
+              <term><varname>shiptostreet</varname></term>
 
               <listitem>
-                <para>Dateierweiterung, wie im Dateinamen. Wird aus
-                <constant>format</constant> entschieden.</para>
+                <para>Straße und Hausnummer (Lieferadresse) <link
+                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>template_meta.media</varname></term>
+              <term><varname>shiptozipcode</varname></term>
 
               <listitem>
-                <para>Ausgabemedium. Kann zur Zeit die Werte
-                <constant>screen</constant> für Bildschirm,
-                <constant>email</constant> für E-Mail (triggert das
-                <constant>_email</constant> Kürzel im Dateinamen),
-                <constant>printer</constant> für Drucker, und
-                <constant>queue</constant> für Warteschlange enthalten.</para>
+                <para>Postleitzahl (Lieferadresse) <link
+                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>template_meta.printer.description</varname></term>
+              <term><varname>street</varname></term>
 
               <listitem>
-                <para>Beschreibung des ausgewählten Druckers</para>
+                <para>Straße und Hausnummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>template_meta.printer.template_code</varname></term>
+              <term><varname>taxnumber</varname></term>
 
               <listitem>
-                <para>Vorlagenürzel des ausgewählten Druckers, identisch mit
-                dem Kürzel das im Dateinamen verwendetet wird.</para>
+                <para>Steuernummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>template_meta.tmpfile</varname></term>
+              <term><varname>ustid</varname></term>
 
               <listitem>
-                <para>Datei-Prefix für temporäre Dateien.</para>
+                <para>Umsatzsteuer-Identifikationsnummer</para>
               </listitem>
             </varlistentry>
-          </variablelist>
-        </sect3>
 
-        <sect3 id="dokumentenvorlagen-und-variablen.allgemeine-variablen.kunden-lieferanten">
-          <title>Stammdaten von Kunden und Lieferanten</title>
+            <varlistentry>
+              <term><varname>vendoremail</varname></term>
+
+              <listitem>
+                <para>Email des Lieferanten; nur für Lieferanten</para>
+              </listitem>
+            </varlistentry>
 
-          <variablelist>
             <varlistentry>
-              <term><varname>account_number</varname></term>
+              <term><varname>vendorfax</varname></term>
 
               <listitem>
-                <para>Kontonummer</para>
+                <para>Faxnummer des Lieferanten; nur für Lieferanten</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>bank</varname></term>
+              <term><varname>vendornotes</varname></term>
 
               <listitem>
-                <para>Name der Bank</para>
+                <para>Bemerkungen beim Lieferanten; nur für Lieferanten</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>bank_code</varname></term>
+              <term><varname>vendornumber</varname></term>
 
               <listitem>
-                <para>Bankleitzahl</para>
+                <para>Lieferantennummer; nur für Lieferanten</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>bic</varname></term>
+              <term><varname>vendorphone</varname></term>
 
               <listitem>
-                <para>Bank-Identifikations-Code (Bank Identifier Code,
-                BIC)</para>
+                <para>Telefonnummer des Lieferanten; nur für
+                Lieferanten</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>business</varname></term>
+              <term><varname>zipcode</varname></term>
 
               <listitem>
-                <para>Kunden-/Lieferantentyp</para>
+                <para>Postleitzahl</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+
+          <note id="dokumentenvorlagen-und-variablen.anmerkung-shipto">
+            <para>Anmerkung: Sind die <varname>shipto*</varname>-Felder in den
+            Stammdaten nicht eingetragen, so haben die Variablen
+            <varname>shipto*</varname> den gleichen Wert wie die die
+            entsprechenden Variablen der Lieferdaten. Das bedeutet, dass sich
+            einige <varname>shipto*</varname>-Variablen so nicht in den
+            Stammdaten wiederfinden sondern schlicht Kopien der
+            Lieferdatenvariablen sind (z.B.
+            <varname>shiptocontact</varname>).</para>
+          </note>
+        </sect3>
 
+        <sect3 id="dokumentenvorlagen-und-variablen.allgemein-bearbeiter">
+          <title>Informationen über den Bearbeiter</title>
+
+          <variablelist>
             <varlistentry>
-              <term><varname>city</varname></term>
+              <term><varname>employee_address</varname></term>
 
               <listitem>
-                <para>Stadt</para>
+                <para>Adressfeld</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>contact</varname></term>
+              <term><varname>employee_businessnumber</varname></term>
 
               <listitem>
-                <para>Kontakt</para>
+                <para>Firmennummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>country</varname></term>
+              <term><varname>employee_company</varname></term>
 
               <listitem>
-                <para>Land</para>
+                <para>Firmenname</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>c_vendor_id</varname></term>
+              <term><varname>employee_co_ustid</varname></term>
 
               <listitem>
-                <para>Lieferantennummer beim Kunden (nur Kunden)</para>
+                <para>Usatzsteuer-Identifikationsnummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>v_customer_id</varname></term>
+              <term><varname>employee_duns</varname></term>
 
               <listitem>
-                <para>Kundennummer beim Lieferanten (nur Lieferanten)</para>
+                <para>DUNS-Nummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>cp_email</varname></term>
+              <term><varname>employee_email</varname></term>
 
               <listitem>
-                <para>Email des Ansprechpartners</para>
+                <para>Email</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>cp_givenname</varname></term>
+              <term><varname>employee_fax</varname></term>
 
               <listitem>
-                <para>Vorname des Ansprechpartners</para>
+                <para>Fax</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>cp_greeting</varname></term>
+              <term><varname>employee_name</varname></term>
 
               <listitem>
-                <para>Anrede des Ansprechpartners</para>
+                <para>voller Name</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>cp_name</varname></term>
+              <term><varname>employee_signature</varname></term>
 
               <listitem>
-                <para>Name des Ansprechpartners</para>
+                <para>Signatur</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>cp_phone1</varname></term>
+              <term><varname>employee_taxnumber</varname></term>
 
               <listitem>
-                <para>Telefonnummer 1 des Ansprechpartners</para>
+                <para>Steuernummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>cp_phone2</varname></term>
+              <term><varname>employee_tel</varname></term>
 
               <listitem>
-                <para>Telefonnummer 2 des Ansprechpartners</para>
+                <para>Telefonnummer</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+        </sect3>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.allgemein-verkaeufer">
+          <title>Informationen über den Verkäufer</title>
 
+          <variablelist>
             <varlistentry>
-              <term><varname>cp_title</varname></term>
+              <term><varname>salesman_address</varname></term>
 
               <listitem>
-                <para>Titel des Ansprechpartners</para>
+                <para>Adressfeld</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>creditlimit</varname></term>
+              <term><varname>salesman_businessnumber</varname></term>
 
               <listitem>
-                <para>Kreditlimit</para>
+                <para>Firmennummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>customeremail</varname></term>
+              <term><varname>salesman_company</varname></term>
 
               <listitem>
-                <para>Email des Kunden; nur für Kunden</para>
+                <para>Firmenname</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>customerfax</varname></term>
+              <term><varname>salesman_co_ustid</varname></term>
 
               <listitem>
-                <para>Faxnummer des Kunden; nur für Kunden</para>
+                <para>Usatzsteuer-Identifikationsnummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>customernotes</varname></term>
+              <term><varname>salesman_duns</varname></term>
 
               <listitem>
-                <para>Bemerkungen beim Kunden; nur für Kunden</para>
+                <para>DUNS-Nummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>customernumber</varname></term>
+              <term><varname>salesman_email</varname></term>
 
               <listitem>
-                <para>Kundennummer; nur für Kunden</para>
+                <para>Email</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>customerphone</varname></term>
+              <term><varname>salesman_fax</varname></term>
 
               <listitem>
-                <para>Telefonnummer des Kunden; nur für Kunden</para>
+                <para>Fax</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>discount</varname></term>
+              <term><varname>salesman_name</varname></term>
 
               <listitem>
-                <para>Rabatt</para>
+                <para>voller Name</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>email</varname></term>
+              <term><varname>salesman_signature</varname></term>
+
+              <listitem>
+                <para>Signatur</para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><varname>salesman_taxnumber</varname></term>
 
               <listitem>
-                <para>Emailadresse</para>
+                <para>Steuernummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>fax</varname></term>
+              <term><varname>salesman_tel</varname></term>
 
               <listitem>
-                <para>Faxnummer</para>
+                <para>Telefonnummer</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+        </sect3>
 
+        <sect3 id="dokumentenvorlagen-und-variablen.allgemein-steuern">
+          <title>Variablen für die einzelnen Steuern</title>
+
+          <variablelist>
             <varlistentry>
-              <term><varname>greeting</varname></term>
+              <term><varname>tax</varname></term>
 
               <listitem>
-                <para>Anrede</para>
+                <para>Steuer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>homepage</varname></term>
+              <term><varname>taxbase</varname></term>
 
               <listitem>
-                <para>Homepage</para>
+                <para>zu versteuernder Betrag</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>iban</varname></term>
+              <term><varname>taxdescription</varname></term>
 
               <listitem>
-                <para>Internationale Kontonummer (International Bank Account
-                Number, IBAN)</para>
+                <para>Name der Steuer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>language</varname></term>
+              <term><varname>taxrate</varname></term>
 
               <listitem>
-                <para>Sprache</para>
+                <para>Steuersatz</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+        </sect3>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.allgemein-lieferbedingungen">
+          <title>Variablen für Lieferbedingungen</title>
 
+          <variablelist>
             <varlistentry>
-              <term><varname>name</varname></term>
+              <term><varname>delivery_term</varname></term>
 
               <listitem>
-                <para>Firmenname</para>
+                <para>Datenbank-Objekt der Lieferbedingung</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>payment_description</varname></term>
+              <term><varname>delivery_term.description</varname></term>
 
               <listitem>
-                <para>Name der Zahlart</para>
+                <para>Beschreibung der Lieferbedingung</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>payment_terms</varname></term>
+              <term><varname>delivery_term.description_long</varname></term>
 
               <listitem>
-                <para>Zahlungskonditionen</para>
+                <para>Langtext bzw. übersetzter Langtext der
+                Lieferbedingung</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+        </sect3>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.abweichende-rechnungsadresse">
+          <title>Informationen über abweichende Rechnungsadressen (nur Verkaufsbelege)</title>
+
+          <para>
+            Abweichende Rechnungsadressen gibt es nur in Verkaufsbelegen. Die entsprechenden Variablen sind nur dann mit Inhalt gefüllt,
+            wenn im Beleg eine abweichende Rechnungsadresse ausgewählt wurde. Ob eine Adresse überhaupt ausgewählt wurde, kann über die
+            Variable <literal>billing_address_id</literal> getestet werden, die die Datenbank-ID der abweichenden Rechnungsadresse enthält,
+            wenn eine ausgewählt ist.
+          </para>
+
+          <para>
+            Die Variablennamen starten alle mit dem Präfix <literal>billing_address_</literal> und heißen anschließend so, wie ihre Pendants
+            aus der Standard-Rechnungsadresse des Kunden. Beispiel: die Postleitzahl, die in der normalen Rechnungsadresse in
+            <literal>zipcode</literal> steht, steht für die abweichende Rechnungsadresse in <literal>billing_address_zipcode</literal>.
+          </para>
+
+          <para>
+            Die folgenden Variablen stehen so zur Verfügung: <literal>billing_address_name</literal>,
+            <literal>billing_address_department_1</literal>, <literal>billing_address_department_2</literal>,
+            <literal>billing_address_contact</literal>, <literal>billing_address_street</literal>,
+            <literal>billing_address_zipcode</literal>, <literal>billing_address_city</literal>, <literal>billing_address_country</literal>,
+            <literal>billing_address_gln</literal>, <literal>billing_address_email</literal>, <literal>billing_address_phone</literal> und
+            <literal>billing_address_fax</literal>.
+          </para>
+        </sect3>
+      </sect2>
+
+      <sect2 id="dokumentenvorlagen-und-variablen.invoice">
+        <title>Variablen in Rechnungen</title>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.invoice-allgemein">
+          <title>Allgemeine Variablen</title>
 
+          <variablelist>
             <varlistentry>
-              <term><varname>phone</varname></term>
+              <term><varname>creditremaining</varname></term>
 
               <listitem>
-                <para>Telefonnummer</para>
+                <para>Verbleibender Kredit</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>shiptocity</varname></term>
+              <term><varname>currency</varname></term>
 
               <listitem>
-                <para>Stadt (Lieferadresse) <link
-                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
+                <para>Währung</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>shiptocontact</varname></term>
+              <term><varname>cusordnumber</varname></term>
 
               <listitem>
-                <para>Kontakt (Lieferadresse) <link
-                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
+                <para>Bestellnummer beim Kunden</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>shiptocountry</varname></term>
+              <term><varname>deliverydate</varname></term>
 
               <listitem>
-                <para>Land (Lieferadresse) <link
-                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
+                <para>Lieferdatum</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>shiptodepartment1</varname></term>
+              <term><varname>duedate</varname></term>
 
               <listitem>
-                <para>Abteilung 1 (Lieferadresse) <link
-                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
+                <para>Fälligkeitsdatum</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>shiptodepartment2</varname></term>
+              <term><varname>globalprojectnumber</varname></term>
 
               <listitem>
-                <para>Abteilung 2 (Lieferadresse) <link
-                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
+                <para>Projektnummer des ganzen Beleges</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>shiptoemail</varname></term>
+              <term><varname>globalprojectdescription</varname></term>
 
               <listitem>
-                <para>Email (Lieferadresse) <link
-                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
+                <para>Projekbeschreibung des ganzen Beleges</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>shiptofax</varname></term>
+              <term><varname>intnotes</varname></term>
 
               <listitem>
-                <para>Fax (Lieferadresse) <link
-                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
+                <para>Interne Bemerkungen</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>shiptoname</varname></term>
+              <term><varname>invdate</varname></term>
 
               <listitem>
-                <para>Firmenname (Lieferadresse) <link
-                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
+                <para>Rechnungsdatum</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>shiptophone</varname></term>
+              <term><varname>invnumber</varname></term>
 
               <listitem>
-                <para>Telefonnummer (Lieferadresse) <link
-                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
+                <para>Rechnungsnummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>shiptostreet</varname></term>
+              <term><varname>invtotal</varname></term>
 
               <listitem>
-                <para>Straße und Hausnummer (Lieferadresse) <link
-                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
+                <para>gesamter Rechnungsbetrag</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>shiptozipcode</varname></term>
+              <term><varname>notes</varname></term>
 
               <listitem>
-                <para>Postleitzahl (Lieferadresse) <link
-                linkend="dokumentenvorlagen-und-variablen.anmerkung-shipto">*</link></para>
+                <para>Bemerkungen der Rechnung</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>street</varname></term>
+              <term><varname>orddate</varname></term>
 
               <listitem>
-                <para>Straße und Hausnummer</para>
+                <para>Auftragsdatum</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>taxnumber</varname></term>
+              <term><varname>ordnumber</varname></term>
 
               <listitem>
-                <para>Steuernummer</para>
+                <para>Auftragsnummer, wenn die Rechnung aus einem Auftrag
+                erstellt wurde</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>ustid</varname></term>
+              <term><varname>payment_description</varname></term>
 
               <listitem>
-                <para>Umsatzsteuer-Identifikationsnummer</para>
+                <para>Name der Zahlart</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>vendoremail</varname></term>
+              <term><varname>payment_terms</varname></term>
 
               <listitem>
-                <para>Email des Lieferanten; nur für Lieferanten</para>
+                <para>Zahlungskonditionen</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>vendorfax</varname></term>
+              <term><varname>quodate</varname></term>
 
               <listitem>
-                <para>Faxnummer des Lieferanten; nur für Lieferanten</para>
+                <para>Angebotsdatum</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>vendornotes</varname></term>
+              <term><varname>quonumber</varname></term>
 
               <listitem>
-                <para>Bemerkungen beim Lieferanten; nur für Lieferanten</para>
+                <para>Angebotsnummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>vendornumber</varname></term>
+              <term><varname>rounding</varname></term>
 
               <listitem>
-                <para>Lieferantennummer; nur für Lieferanten</para>
+                <para>Betrag, um den <varname>invtotal</varname> gerundet
+                wurde (kann positiv oder negativ sein)</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>vendorphone</varname></term>
+              <term><varname>shippingpoint</varname></term>
 
               <listitem>
-                <para>Telefonnummer des Lieferanten; nur für
-                Lieferanten</para>
+                <para>Versandort</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>zipcode</varname></term>
+              <term><varname>shipvia</varname></term>
 
               <listitem>
-                <para>Postleitzahl</para>
+                <para>Transportmittel</para>
               </listitem>
             </varlistentry>
-          </variablelist>
-
-          <note id="dokumentenvorlagen-und-variablen.anmerkung-shipto">
-            <para>Anmerkung: Sind die <varname>shipto*</varname>-Felder in den
-            Stammdaten nicht eingetragen, so haben die Variablen
-            <varname>shipto*</varname> den gleichen Wert wie die die
-            entsprechenden Variablen der Lieferdaten. Das bedeutet, dass sich
-            einige <varname>shipto*</varname>-Variablen so nicht in den
-            Stammdaten wiederfinden sondern schlicht Kopien der
-            Lieferdatenvariablen sind (z.B.
-            <varname>shiptocontact</varname>).</para>
-          </note>
-        </sect3>
-
-        <sect3 id="dokumentenvorlagen-und-variablen.allgemein-bearbeiter">
-          <title>Informationen über den Bearbeiter</title>
 
-          <variablelist>
             <varlistentry>
-              <term><varname>employee_address</varname></term>
+              <term><varname>subtotal</varname></term>
 
               <listitem>
-                <para>Adressfeld</para>
+                <para>Zwischensumme aller Posten ohne Steuern</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>employee_businessnumber</varname></term>
+              <term><varname>total</varname></term>
 
               <listitem>
-                <para>Firmennummer</para>
+                <para>Restsumme der Rechnung (Summe abzüglich bereits
+                bezahlter Posten)</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>employee_company</varname></term>
+              <term><varname>transaction_description</varname></term>
 
               <listitem>
-                <para>Firmenname</para>
+                <para>Vorgangsbezeichnung</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>employee_co_ustid</varname></term>
+              <term><varname>transdate</varname></term>
 
               <listitem>
-                <para>Usatzsteuer-Identifikationsnummer</para>
+                <para>Auftragsdatum wenn die Rechnung aus einem Auftrag
+                erstellt wurde</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+        </sect3>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.invoice-posten">
+          <title>Variablen für jeden Posten auf der Rechnung</title>
 
+          <variablelist>
             <varlistentry>
-              <term><varname>employee_duns</varname></term>
+              <term><varname>bin</varname></term>
 
               <listitem>
-                <para>DUNS-Nummer</para>
+                <para>Stellage</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>employee_email</varname></term>
+              <term><varname>description</varname></term>
 
               <listitem>
-                <para>Email</para>
+                <para>Artikelbeschreibung</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>employee_fax</varname></term>
+              <term><varname>cusordnumber_oe</varname></term>
 
               <listitem>
-                <para>Fax</para>
+                <para>Bestellnummer des Kunden aus dem Auftrag, aus dem der
+                Posten ursprünglich stammt (nur Verkauf)</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>employee_name</varname></term>
+              <term><varname>discount</varname></term>
 
               <listitem>
-                <para>voller Name</para>
+                <para>Rabatt als Betrag</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>employee_signature</varname></term>
+              <term><varname>discount_sub</varname></term>
 
               <listitem>
-                <para>Signatur</para>
+                <para>Zwischensumme mit Rabatt</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>employee_taxnumber</varname></term>
+              <term><varname>donumber_do</varname></term>
 
               <listitem>
-                <para>Steuernummer</para>
+                <para>Lieferscheinnummer des Lieferscheins, aus dem die
+                Position ursprünglich stammt, wenn die Rechnung im Rahmen des
+                Workflows aus einem Lieferschein erstellt wurde.</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>employee_tel</varname></term>
+              <term><varname>drawing</varname></term>
 
               <listitem>
-                <para>Telefonnummer</para>
+                <para>Zeichnung</para>
               </listitem>
             </varlistentry>
-          </variablelist>
-        </sect3>
-
-        <sect3 id="dokumentenvorlagen-und-variablen.allgemein-verkaeufer">
-          <title>Informationen über den Verkäufer</title>
 
-          <variablelist>
             <varlistentry>
-              <term><varname>salesman_address</varname></term>
+              <term><varname>ean</varname></term>
 
               <listitem>
-                <para>Adressfeld</para>
+                <para>EAN-Code</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>salesman_businessnumber</varname></term>
+              <term><varname>image</varname></term>
 
               <listitem>
-                <para>Firmennummer</para>
+                <para>Grafik</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>salesman_company</varname></term>
+              <term><varname>linetotal</varname></term>
 
               <listitem>
-                <para>Firmenname</para>
+                <para>Zeilensumme (Anzahl * Einzelpreis)</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>salesman_co_ustid</varname></term>
+              <term><varname>longdescription</varname></term>
 
               <listitem>
-                <para>Usatzsteuer-Identifikationsnummer</para>
+                <para>Langtext, vorbelegt mit dem Feld Bemerkungen der entsprechenden Ware</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>salesman_duns</varname></term>
+              <term><varname>microfiche</varname></term>
 
               <listitem>
-                <para>DUNS-Nummer</para>
+                <para>Mikrofilm</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>salesman_email</varname></term>
+              <term><varname>netprice</varname></term>
 
               <listitem>
-                <para>Email</para>
+                <para>Alternative zu <varname>sellprice</varname>, aber
+                <varname>netprice</varname> entspricht dem effektiven
+                Einzelpreis und beinhaltet Zeilenrabatt und Preisfaktor.
+                <varname>netprice</varname> wird rückgerechnet aus Zeilensumme
+                / Menge. Diese Variable ist nützlich, wenn man den gewährten
+                Rabatt in der Druckvorlage nicht anzeigen möchte, aber Menge *
+                Einzelpreis trotzdem die angezeigte Zeilensumme ergeben soll.
+                <varname>netprice</varname> hat nichts mit Netto/Brutto im
+                Sinne von Steuern zu tun.</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>salesman_fax</varname></term>
+              <term><varname>nodiscount_linetotal</varname></term>
 
               <listitem>
-                <para>Fax</para>
+                <para>Zeilensumme ohne Rabatt</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>salesman_name</varname></term>
+              <term><varname>nodiscount_sub</varname></term>
 
               <listitem>
-                <para>voller Name</para>
+                <para>Zwischensumme ohne Rabatt</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>salesman_signature</varname></term>
+              <term><varname>number</varname></term>
 
               <listitem>
-                <para>Signatur</para>
+                <para>Artikelnummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>salesman_taxnumber</varname></term>
+              <term><varname>ordnumber_oe</varname></term>
 
               <listitem>
-                <para>Steuernummer</para>
+                <para>Auftragsnummer des Originalauftrags, aus dem der Posten
+                ursprünglich stammt. Nützlich, wenn die Rechnung aus mehreren
+                Lieferscheinen zusammengefasst wurde, oder wenn zwischendurch
+                eine Sammelauftrag aus mehreren Aufträgen erstellt wurde. In
+                letzterem Fall wird die unsprüngliche Auftragsnummer
+                angezeigt.</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>salesman_tel</varname></term>
+              <term><varname>p_discount</varname></term>
 
               <listitem>
-                <para>Telefonnummer</para>
+                <para>Rabatt in Prozent</para>
               </listitem>
             </varlistentry>
-          </variablelist>
-        </sect3>
-
-        <sect3 id="dokumentenvorlagen-und-variablen.allgemein-steuern">
-          <title>Variablen für die einzelnen Steuern</title>
 
-          <variablelist>
             <varlistentry>
-              <term><varname>tax</varname></term>
+              <term><varname>partnotes</varname></term>
 
               <listitem>
-                <para>Steuer</para>
+                <para>Die beim Artikel gespeicherten Bemerkungen</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>taxbase</varname></term>
+              <term><varname>partsgroup</varname></term>
 
               <listitem>
-                <para>zu versteuernder Betrag</para>
+                <para>Warengruppe</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>taxdescription</varname></term>
+              <term><varname>price_factor</varname></term>
 
               <listitem>
-                <para>Name der Steuer</para>
+                <para>Der Preisfaktor als Zahl, sofern einer eingestellt
+                ist</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>taxrate</varname></term>
+              <term><varname>price_factor_name</varname></term>
 
               <listitem>
-                <para>Steuersatz</para>
+                <para>Der Name des Preisfaktors, sofern einer eingestellt
+                ist</para>
               </listitem>
             </varlistentry>
-          </variablelist>
-        </sect3>
-
-        <sect3 id="dokumentenvorlagen-und-variablen.allgemein-lieferbedingungen">
-          <title>Variablen für Lieferbedingungen</title>
 
-          <variablelist>
-            <varlistentry>
-              <term><varname>delivery_term</varname></term>
-              <listitem><para>Datenbank-Objekt der Lieferbedingung</para></listitem>
-            </varlistentry>
-            <varlistentry>
-              <term><varname>delivery_term.description</varname></term>
-              <listitem><para>Beschreibung der Lieferbedingung</para></listitem>
-            </varlistentry>
             <varlistentry>
-              <term><varname>delivery_term.description_long</varname></term>
-              <listitem><para>Langtext bzw. übersetzter Langtext der Lieferbedingung</para></listitem>
-            </varlistentry>
-          </variablelist>
-        </sect3>
-      </sect2>
-
-      <sect2 id="dokumentenvorlagen-und-variablen.invoice">
-        <title>Variablen in Rechnungen</title>
+              <term><varname>projectnumber</varname></term>
 
-        <sect3 id="dokumentenvorlagen-und-variablen.invoice-allgemein">
-          <title>Allgemeine Variablen</title>
+              <listitem>
+                <para>Projektnummer</para>
+              </listitem>
+            </varlistentry>
 
-          <variablelist>
             <varlistentry>
-              <term><varname>creditremaining</varname></term>
+              <term><varname>projectdescription</varname></term>
 
               <listitem>
-                <para>Verbleibender Kredit</para>
+                <para>Projektbeschreibung</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>currency</varname></term>
+              <term><varname>qty</varname></term>
 
               <listitem>
-                <para>Währung</para>
+                <para>Anzahl</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>cusordnumber</varname></term>
+              <term><varname>reqdate</varname></term>
 
               <listitem>
-                <para>Bestellnummer beim Kunden</para>
+                <para>Lieferdatum</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>deliverydate</varname></term>
+              <term><varname>runningnumber</varname></term>
 
               <listitem>
-                <para>Lieferdatum</para>
+                <para>Position auf der Rechnung (1, 2, 3...)</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>duedate</varname></term>
+              <term><varname>sellprice</varname></term>
 
               <listitem>
-                <para>Fälligkeitsdatum</para>
+                <para>Verkaufspreis</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>globalprojectnumber</varname></term>
+              <term><varname>serialnumber</varname></term>
 
               <listitem>
-                <para>Projektnummer des ganzen Beleges</para>
+                <para>Seriennummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>globalprojectdescription</varname></term>
+              <term><varname>tax_rate</varname></term>
 
               <listitem>
-                <para>Projekbeschreibung des ganzen Beleges</para>
+                <para>Steuersatz</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>intnotes</varname></term>
+              <term><varname>transdate_do</varname></term>
 
               <listitem>
-                <para>Interne Bemerkungen</para>
+                <para>Datum des Lieferscheins, wenn die Rechnung im Rahmen des
+                Workflows aus einem Lieferschein stammte.</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>invdate</varname></term>
+              <term><varname>transdate_oe</varname></term>
 
               <listitem>
-                <para>Rechnungsdatum</para>
+                <para>Datum des Auftrags, wenn die Rechnung im Rahmen des
+                Workflows aus einem Auftrag erstellt wurde. Wenn es
+                Sammelaufträge gab wird das Datum des ursprünglichen Auftrags
+                genommen.</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>invnumber</varname></term>
+              <term><varname>transdate_quo</varname></term>
 
               <listitem>
-                <para>Rechnungsnummer</para>
+                <para>Datum des Angebots, wenn die Position im Rahmen des
+                Workflows aus einem Angebot stammte.</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>invtotal</varname></term>
+              <term><varname>unit</varname></term>
 
               <listitem>
-                <para>gesamter Rechnungsbetrag</para>
+                <para>Einheit</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>notes</varname></term>
+              <term><varname>weight</varname></term>
 
               <listitem>
-                <para>Bemerkungen der Rechnung</para>
+                <para>Gewicht</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+
+          <para>Für jeden Posten gibt es ein Unterarray mit den Informationen
+          über Lieferanten und Lieferantenartikelnummer. Diese müssen mit
+          einer <function>foreach</function>-Schleife ausgegeben werden, da
+          für jeden Artikel mehrere Lieferanteninformationen hinterlegt sein
+          können. Die Variablen dafür lauten:</para>
 
+          <variablelist>
             <varlistentry>
-              <term><varname>orddate</varname></term>
+              <term><varname>make</varname></term>
 
               <listitem>
-                <para>Auftragsdatum</para>
+                <para>Lieferant</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>ordnumber</varname></term>
+              <term><varname>model</varname></term>
 
               <listitem>
-                <para>Auftragsnummer, wenn die Rechnung aus einem Auftrag
-                erstellt wurde</para>
+                <para>Lieferantenartikelnummer</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+        </sect3>
 
+        <sect3 id="dokumentenvorlagen-und-variablen.invoice-zahlungen">
+          <title>Variablen für die einzelnen Zahlungseingänge</title>
+
+          <variablelist>
             <varlistentry>
-              <term><varname>payment_description</varname></term>
+              <term><varname>payment</varname></term>
 
               <listitem>
-                <para>Name der Zahlart</para>
+                <para>Betrag</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>payment_terms</varname></term>
+              <term><varname>paymentaccount</varname></term>
 
               <listitem>
-                <para>Zahlungskonditionen</para>
+                <para>Konto</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>quodate</varname></term>
+              <term><varname>paymentdate</varname></term>
 
               <listitem>
-                <para>Angebotsdatum</para>
+                <para>Datum</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>quonumber</varname></term>
+              <term><varname>paymentmemo</varname></term>
 
               <listitem>
-                <para>Angebotsnummer</para>
+                <para>Memo</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>shippingpoint</varname></term>
+              <term><varname>paymentsource</varname></term>
 
               <listitem>
-                <para>Versandort</para>
+                <para>Beleg</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+        </sect3>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.benutzerdefinierte-variablen-vc">
+          <title>Benutzerdefinierte Kunden- und Lieferantenvariablen</title>
+
+          <para>Die vom Benutzer definierten Variablen für Kunden und
+          Lieferanten stehen beim Ausdruck von Einkaufs- und Verkaufsbelegen
+          ebenfalls zur Verfügung. Ihre Namen setzen sich aus dem Präfix
+          <varname>vc_cvar_</varname> und dem vom Benutzer festgelegten
+          Variablennamen zusammen.</para>
+
+          <para>Beispiel: Der Benutzer hat eine Variable namens
+          <varname>number_of_employees</varname> definiert, die die Anzahl der
+          Mitarbeiter des Unternehmens enthält. Diese Variable steht dann
+          unter dem Namen <varname>vc_cvar_number_of_employees</varname> zur
+          Verfügung.</para>
+
+          <para>Die benutzerdefinierten Variablen der Lieferadressen stehen
+          unter einem ähnlichen Namensschema zur Verfügung. Hier lautet der
+          Präfix <varname>shiptocvar_</varname>.</para>
+
+          <para>Analog stehen die benutzerdefinierten Variablen für
+          Ansprechpersonen mit dem Namenspräfix <varname>cp_cvar_</varname>
+          zur Verfügung.</para>
+        </sect3>
+      </sect2>
+
+      <sect2 id="dokumentenvorlagen-und-variablen.dunning">
+        <title>Variablen in Mahnungen und Rechnungen über Mahngebühren</title>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.dunning-vorlagennamen">
+          <title>Namen der Vorlagen</title>
+
+          <para>Die Namen der Vorlagen werden im System-Menü vom Benutzer
+          eingegeben. Wird für ein Mahnlevel die Option zur automatischen
+          Erstellung einer Rechnung über die Mahngebühren und Zinsen
+          aktiviert, so wird der Name der Vorlage für diese Rechnung aus dem
+          Vorlagenname für diese Mahnstufe mit dem Zusatz
+          <constant>_invoice</constant> gebildet. Weiterhin werden die Kürzel
+          für die ausgewählte Sprache und den ausgewählten Drucker
+          angehängt.</para>
+        </sect3>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.dunning-allgemein">
+          <title>Allgemeine Variablen in Mahnungen</title>
 
+          <para>Die Variablen des Bearbeiters, bzw. Verkäufers stehen wie
+          gewohnt als <varname>employee_...</varname> bzw.
+          <varname>salesman_...</varname> zur Verfügung. Werden mehrere
+          Rechnungen in einer Mahnung zusammengefasst, so werden die Metadaten
+          (Bearbeiter, Abteilung, etc) der ersten angemahnten Rechnung im
+          Ausdruck genommen.</para>
+
+          <para>Die Adressdaten des Kunden stehen als Variablen
+          <varname>name</varname>, <varname>street</varname>,
+          <varname>zipcode</varname>, <varname>city</varname>,
+          <varname>country</varname>, <varname>department_1</varname>,
+          <varname>department_2</varname>, und <varname>email</varname> zur
+          Verfügung. Der Ansprechpartner <varname>cp_...</varname> steht auch
+          zu Verfügung, wird allerdings auch nur von der ersten angemahnten
+          Rechnung (s.o.) genommen.</para>
+
+          <para>Weitere Variablen beinhalten:</para>
+
+          <variablelist>
             <varlistentry>
-              <term><varname>shipvia</varname></term>
+              <term><varname>dunning_date</varname></term>
 
               <listitem>
-                <para>Transportmittel</para>
+                <para>Datum der Mahnung</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>subtotal</varname></term>
+              <term><varname>dunning_duedate</varname></term>
 
               <listitem>
-                <para>Zwischensumme aller Posten ohne Steuern</para>
+                <para>Fälligkeitsdatum für diese Mahhnung</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>total</varname></term>
+              <term><varname>dunning_id</varname></term>
 
               <listitem>
-                <para>Restsumme der Rechnung (Summe abzüglich bereits
-                bezahlter Posten)</para>
+                <para>Mahnungsnummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>transaction_description</varname></term>
+              <term><varname>fee</varname></term>
 
               <listitem>
-                <para>Vorgangsbezeichnung</para>
+                <para>Kumulative Mahngebühren</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>transdate</varname></term>
+              <term><varname>interest_rate</varname></term>
 
               <listitem>
-                <para>Auftragsdatum wenn die Rechnung aus einem Auftrag
-                erstellt wurde</para>
+                <para>Zinssatz per anno in Prozent</para>
               </listitem>
             </varlistentry>
-          </variablelist>
-        </sect3>
-
-        <sect3 id="dokumentenvorlagen-und-variablen.invoice-posten">
-          <title>Variablen für jeden Posten auf der Rechnung</title>
 
-          <variablelist>
             <varlistentry>
-              <term><varname>bin</varname></term>
+              <term><varname>total_amount</varname></term>
 
               <listitem>
-                <para>Stellage</para>
+                <para>Gesamter noch zu zahlender Betrag als
+                <function>fee</function> + <function>total_interest</function>
+                + <function>total_open_amount</function></para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>description</varname></term>
+              <term><varname>total_interest</varname></term>
 
               <listitem>
-                <para>Artikelbeschreibung</para>
+                <para>Zinsen per anno über alle Rechnungen</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>cusordnumber_oe</varname></term>
+              <term><varname>total_open_amount</varname></term>
 
               <listitem>
-                <para>Bestellnummer des Kunden aus dem Auftrag, aus dem der Posten ursprünglich stammt (nur Verkauf)</para>
+                <para>Summe über alle offene Beträge der Rechnungen</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+        </sect3>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.dunning-details">
+          <title>Variablen für jede gemahnte Rechnung in einer Mahnung</title>
 
+          <variablelist>
             <varlistentry>
-              <term><varname>discount</varname></term>
+              <term><varname>dn_amount</varname></term>
 
               <listitem>
-                <para>Rabatt als Betrag</para>
+                <para>Rechnungssumme (brutto)</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>discount_sub</varname></term>
+              <term><varname>dn_duedate</varname></term>
 
               <listitem>
-                <para>Zwischensumme mit Rabatt</para>
+                <para>Originales Fälligkeitsdatum der Rechnung</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>donumber_do</varname></term>
+              <term><varname>dn_dunning_date</varname></term>
 
               <listitem>
-                <para>Lieferscheinnummer des Lieferscheins, aus dem die Position ursprünglich stammt, wenn die Rechnung im Rahmen des Workflows aus einem  Lieferschein erstellt wurde.</para>
+                <para>Datum der Mahnung</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>drawing</varname></term>
+              <term><varname>dn_dunning_duedate</varname></term>
 
               <listitem>
-                <para>Zeichnung</para>
+                <para>Fälligkeitsdatum der Mahnung</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>ean</varname></term>
+              <term><varname>dn_fee</varname></term>
 
               <listitem>
-                <para>EAN-Code</para>
+                <para>Kummulative Mahngebühr</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>image</varname></term>
+              <term><varname>dn_interest</varname></term>
 
               <listitem>
-                <para>Grafik</para>
+                <para>Zinsen per anno für diese Rechnung</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>linetotal</varname></term>
+              <term><varname>dn_invnumber</varname></term>
 
               <listitem>
-                <para>Zeilensumme (Anzahl * Einzelpreis)</para>
+                <para>Rechnungsnummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>longdescription</varname></term>
+              <term><varname>dn_linetotal</varname></term>
 
               <listitem>
-                <para>Langtext</para>
+                <para>Noch zu zahlender Betrag (ergibt sich aus
+                <varname>dn_open_amount</varname> + <varname>dn_fee</varname>
+                + <varname>dn_interest</varname>)</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>microfiche</varname></term>
+              <term><varname>dn_netamount</varname></term>
 
               <listitem>
-                <para>Mikrofilm</para>
+                <para>Rechnungssumme (netto)</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>netprice</varname></term>
+              <term><varname>dn_open_amount</varname></term>
 
               <listitem>
-                <para>Alternative zu <varname>sellprice</varname>, aber <varname>netprice</varname> entspricht dem effektiven Einzelpreis und beinhaltet Zeilenrabatt und Preisfaktor. <varname>netprice</varname> wird rückgerechnet aus Zeilensumme / Menge. Diese Variable ist nützlich, wenn man den gewährten Rabatt in der Druckvorlage nicht anzeigen möchte, aber Menge * Einzelpreis trotzdem die angezeigte Zeilensumme ergeben soll. <varname>netprice</varname> hat nichts mit Netto/Brutto im Sinne von Steuern zu tun.</para>
+                <para>Offener Rechnungsbetrag</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>nodiscount_linetotal</varname></term>
+              <term><varname>dn_ordnumber</varname></term>
 
               <listitem>
-                <para>Zeilensumme ohne Rabatt</para>
+                <para>Bestellnummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>nodiscount_sub</varname></term>
+              <term><varname>dn_transdate</varname></term>
 
               <listitem>
-                <para>Zwischensumme ohne Rabatt</para>
+                <para>Rechnungsdatum</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>number</varname></term>
+              <term><varname>dn_curr</varname></term>
 
               <listitem>
-                <para>Artikelnummer</para>
+                <para>Währung, in der die Rechnung erstellt wurde. (Die
+                Rechnungsbeträge sind aber immer in der Hauptwährung)</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+        </sect3>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.dunning-invoice">
+          <title>Variablen in automatisch erzeugten Rechnungen über
+          Mahngebühren</title>
 
+          <para>Die Variablen des Verkäufers stehen wie gewohnt als
+          <varname>employee_...</varname> zur Verfügung. Die Adressdaten des
+          Kunden stehen als Variablen <varname>name</varname>,
+          <varname>street</varname>, <varname>zipcode</varname>,
+          <varname>city</varname>, <varname>country</varname>,
+          <varname>department_1</varname>, <varname>department_2</varname>,
+          und <varname>email</varname> zur Verfügung.</para>
+
+          <para>Weitere Variablen beinhalten:</para>
+
+          <variablelist>
             <varlistentry>
-              <term><varname>ordnumber_oe</varname></term>
+              <term><varname>duedate</varname></term>
 
               <listitem>
-                <para>Auftragsnummer des Originalauftrags, aus dem der Posten ursprünglich stammt. Nützlich, wenn die Rechnung aus mehreren Lieferscheinen zusammengefasst wurde, oder wenn zwischendurch eine Sammelauftrag aus mehreren Aufträgen erstellt wurde. In letzterem Fall wird die unsprüngliche Auftragsnummer angezeigt.</para>
+                <para>Fälligkeitsdatum der Rechnung</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>p_discount</varname></term>
+              <term><varname>dunning_id</varname></term>
 
               <listitem>
-                <para>Rabatt in Prozent</para>
+                <para>Mahnungsnummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>partnotes</varname></term>
+              <term><varname>fee</varname></term>
 
               <listitem>
-                <para>Die beim Artikel gespeicherten Bemerkungen</para>
+                <para>Mahngebühren</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>partsgroup</varname></term>
+              <term><varname>interest</varname></term>
 
               <listitem>
-                <para>Warengruppe</para>
+                <para>Zinsen</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>price_factor</varname></term>
+              <term><varname>invamount</varname></term>
 
               <listitem>
-                <para>Der Preisfaktor als Zahl, sofern einer eingestellt
-                ist</para>
+                <para>Rechnungssumme (ergibt sich aus <varname>fee</varname> +
+                <varname>interest</varname>)</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>price_factor_name</varname></term>
+              <term><varname>invdate</varname></term>
 
               <listitem>
-                <para>Der Name des Preisfaktors, sofern einer eingestellt
-                ist</para>
+                <para>Rechnungsdatum</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>projectnumber</varname></term>
+              <term><varname>invnumber</varname></term>
 
               <listitem>
-                <para>Projektnummer</para>
+                <para>Rechnungsnummer</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+        </sect3>
+      </sect2>
+
+      <sect2 id="dokumentenvorlagen-und-variablen.andere-vorlagen">
+        <title>Variablen in anderen Vorlagen</title>
+
+        <sect3>
+          <title>Einführung</title>
+
+          <para>Die Variablen in anderen Vorlagen sind ähnlich wie in der
+          Rechnung. Allerdings heißen die Variablen, die mit
+          <varname>inv</varname> beginnen, jetzt anders. Bei den Angeboten
+          fangen sie mit <varname>quo</varname> für "quotation" an:
+          <varname>quodate</varname> für Angebotsdatum etc. Bei Bestellungen
+          wiederum fangen sie mit <varname>ord</varname> für "order" an:
+          <varname>ordnumber</varname> für Bestellnummer etc.</para>
+
+          <para>Manche Variablen sind in anderen Vorlagen hingegen gar nicht
+          vorhanden wie z.B. die für bereits verbuchte Zahlungseingänge. Dies
+          sind Variablen, die vom Geschäftsablauf her in der entsprechenden
+          Vorlage keine Bedeutung haben oder noch nicht belegt sein
+          können.</para>
 
+          <para>Im Folgenden werden nur wichtige Unterschiede zu den Variablen
+          in Rechnungen aufgeführt.</para>
+        </sect3>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.andere-vorlagen-quotations">
+          <title>Angebote und Preisanfragen</title>
+
+          <variablelist>
             <varlistentry>
-              <term><varname>projectdescription</varname></term>
+              <term><varname>quonumber</varname></term>
 
               <listitem>
-                <para>Projektbeschreibung</para>
+                <para>Angebots- bzw. Anfragenummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>qty</varname></term>
+              <term><varname>reqdate</varname></term>
 
               <listitem>
-                <para>Anzahl</para>
+                <para>Gültigkeitsdatum (bei Angeboten) bzw. Lieferdatum (bei
+                Preisanfragen)</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>reqdate</varname></term>
+              <term><varname>transdate</varname></term>
 
               <listitem>
-                <para>Lieferdatum</para>
+                <para>Angebots- bzw. Anfragedatum</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+        </sect3>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.andere-vorlagen-orders">
+          <title>Auftragsbestätigungen und Lieferantenaufträge</title>
 
+          <variablelist>
             <varlistentry>
-              <term><varname>runningnumber</varname></term>
+              <term><varname>ordnumber</varname></term>
 
               <listitem>
-                <para>Position auf der Rechnung (1, 2, 3...)</para>
+                <para>Auftragsnummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>sellprice</varname></term>
+              <term><varname>reqdate</varname></term>
 
               <listitem>
-                <para>Verkaufspreis</para>
+                <para>Lieferdatum</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>serialnumber</varname></term>
+              <term><varname>transdate</varname></term>
 
               <listitem>
-                <para>Seriennummer</para>
+                <para>Auftragsdatum</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+        </sect3>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.andere-vorlagen-delivery-orders">
+          <title>Lieferscheine (Verkauf und Einkauf)</title>
 
+          <variablelist>
             <varlistentry>
-              <term><varname>tax_rate</varname></term>
+              <term><varname>cusordnumber</varname></term>
 
               <listitem>
-                <para>Steuersatz</para>
+                <para>Bestellnummer des Kunden (im Verkauf) bzw. Bestellnummer
+                des Lieferanten (im Einkauf)</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>transdate_do</varname></term>
+              <term><varname>donumber</varname></term>
 
               <listitem>
-                <para>Datum des Lieferscheins, wenn die Rechnung im Rahmen des Workflows aus einem Lieferschein stammte.</para>
+                <para>Lieferscheinnummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>transdate_oe</varname></term>
+              <term><varname>transdate</varname></term>
 
               <listitem>
-                <para>Datum des Auftrags, wenn die Rechnung im Rahmen des Workflows aus einem Auftrag erstellt wurde. Wenn es Sammelaufträge gab wird das Datum des ursprünglichen Auftrags genommen.</para>
+                <para>Lieferscheindatum</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+
+          <para>Für jede Position eines Lieferscheines gibt es ein Unterarray
+          mit den Informationen darüber, von welchem Lager und Lagerplatz aus
+          die Waren verschickt wurden (Verkaufslieferscheine) bzw. auf welchen
+          Lagerplatz sie eingelagert wurden. Diese müssen mittels einer
+          <function>foreach</function>-Schleife ausgegeben werden. Diese
+          Variablen sind:</para>
 
+          <variablelist>
             <varlistentry>
-              <term><varname>transdate_quo</varname></term>
+              <term><varname>si_bin</varname></term>
 
               <listitem>
-                <para>Datum des Angebots, wenn die Position im Rahmen des Workflows aus einem Angebot stammte.</para>
+                <para>Lagerplatz</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>unit</varname></term>
+              <term><varname>si_chargenumber</varname></term>
 
               <listitem>
-                <para>Einheit</para>
+                <para>Chargennummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>weight</varname></term>
+              <term><varname>si_bestbefore</varname></term>
 
               <listitem>
-                <para>Gewicht</para>
+                <para>Mindesthaltbarkeit</para>
               </listitem>
             </varlistentry>
-          </variablelist>
-
-          <para>Für jeden Posten gibt es ein Unterarray mit den Informationen
-          über Lieferanten und Lieferantenartikelnummer. Diese müssen mit
-          einer <function>foreach</function>-Schleife ausgegeben werden, da
-          für jeden Artikel mehrere Lieferanteninformationen hinterlegt sein
-          können. Die Variablen dafür lauten:</para>
 
-          <variablelist>
             <varlistentry>
-              <term><varname>make</varname></term>
+              <term><varname>si_number</varname></term>
 
               <listitem>
-                <para>Lieferant</para>
+                <para>Artikelnummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>model</varname></term>
+              <term><varname>si_qty</varname></term>
 
               <listitem>
-                <para>Lieferantenartikelnummer</para>
+                <para>Anzahl bzw. Menge</para>
               </listitem>
             </varlistentry>
-          </variablelist>
-        </sect3>
-
-        <sect3 id="dokumentenvorlagen-und-variablen.invoice-zahlungen">
-          <title>Variablen für die einzelnen Zahlungseingänge</title>
 
-          <variablelist>
             <varlistentry>
-              <term><varname>payment</varname></term>
+              <term><varname>si_runningnumber</varname></term>
 
               <listitem>
-                <para>Betrag</para>
+                <para>Positionsnummer (1, 2, 3 etc)</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>paymentaccount</varname></term>
+              <term><varname>si_unit</varname></term>
 
               <listitem>
-                <para>Konto</para>
+                <para>Einheit</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>paymentdate</varname></term>
+              <term><varname>si_warehouse</varname></term>
 
               <listitem>
-                <para>Datum</para>
+                <para>Lager</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+        </sect3>
 
+        <sect3 id="dokumentenvorlagen-und-variablen.andere-vorlagen-statement">
+          <title>Variablen für Sammelrechnung</title>
+
+          <variablelist>
             <varlistentry>
-              <term><varname>paymentmemo</varname></term>
+              <term><varname>c0total</varname></term>
 
               <listitem>
-                <para>Memo</para>
+                <para>Gesamtbetrag aller Rechnungen mit Fälligkeit &lt; 30
+                Tage</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>paymentsource</varname></term>
+              <term><varname>c30total</varname></term>
 
               <listitem>
-                <para>Beleg</para>
+                <para>Gesamtbetrag aller Rechnungen mit Fälligkeit &gt;= 30
+                und &lt; 60 Tage</para>
               </listitem>
             </varlistentry>
-          </variablelist>
-        </sect3>
 
-        <sect3 id="dokumentenvorlagen-und-variablen.benutzerdefinierte-variablen-vc">
-          <title>Benutzerdefinierte Kunden- und Lieferantenvariablen</title>
-
-          <para>Die vom Benutzer definierten Variablen für Kunden und
-          Lieferanten stehen beim Ausdruck von Einkaufs- und Verkaufsbelegen
-          ebenfalls zur Verfügung. Ihre Namen setzen sich aus dem Präfix
-          <varname>vc_cvar_</varname> und dem vom Benutzer festgelegten
-          Variablennamen zusammen.</para>
-
-          <para>Beispiel: Der Benutzer hat eine Variable namens
-          <varname>number_of_employees</varname> definiert, die die Anzahl der
-          Mitarbeiter des Unternehmens enthält. Diese Variable steht dann
-          unter dem Namen <varname>vc_cvar_number_of_employees</varname> zur
-          Verfügung.</para>
-        </sect3>
-      </sect2>
+            <varlistentry>
+              <term><varname>c60total</varname></term>
 
-      <sect2 id="dokumentenvorlagen-und-variablen.dunning">
-        <title>Variablen in Mahnungen und Rechnungen über Mahngebühren</title>
+              <listitem>
+                <para>Gesamtbetrag aller Rechnungen mit Fälligkeit &gt;= 60
+                und &lt; 90 Tage</para>
+              </listitem>
+            </varlistentry>
 
-        <sect3 id="dokumentenvorlagen-und-variablen.dunning-vorlagennamen">
-          <title>Namen der Vorlagen</title>
+            <varlistentry>
+              <term><varname>c90total</varname></term>
 
-          <para>Die Namen der Vorlagen werden im System-Menü vom Benutzer
-          eingegeben. Wird für ein Mahnlevel die Option zur automatischen
-          Erstellung einer Rechnung über die Mahngebühren und Zinsen
-          aktiviert, so wird der Name der Vorlage für diese Rechnung aus dem
-          Vorlagenname für diese Mahnstufe mit dem Zusatz
-          <constant>_invoice</constant> gebildet. Weiterhin werden die Kürzel
-          für die ausgewählte Sprache und den ausgewählten Drucker
-          angehängt.</para>
-        </sect3>
+              <listitem>
+                <para>Gesamtbetrag aller Rechnungen mit Fälligkeit &gt;= 90
+                Tage</para>
+              </listitem>
+            </varlistentry>
 
-        <sect3 id="dokumentenvorlagen-und-variablen.dunning-allgemein">
-          <title>Allgemeine Variablen in Mahnungen</title>
+            <varlistentry>
+              <term><varname>total</varname></term>
 
-          <para>Die Variablen des Verkäufers stehen wie gewohnt als
-          <varname>employee_...</varname> zur Verfügung. Die Adressdaten des
-          Kunden stehen als Variablen <varname>name</varname>,
-          <varname>street</varname>, <varname>zipcode</varname>,
-          <varname>city</varname>, <varname>country</varname>,
-          <varname>department_1</varname>, <varname>department_2</varname>,
-          und <varname>email</varname> zur Verfügung.</para>
+              <listitem>
+                <para>Gesamtbetrag aller Rechnungen</para>
+              </listitem>
+            </varlistentry>
+          </variablelist>
 
-          <para>Weitere Variablen beinhalten:</para>
+          <para>Variablen für jede Rechnungsposition in Sammelrechnung:</para>
 
           <variablelist>
             <varlistentry>
-              <term><varname>dunning_date</varname></term>
+              <term><varname>invnumber</varname></term>
 
               <listitem>
-                <para>Datum der Mahnung</para>
+                <para>Rechnungsnummer</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>dunning_duedate</varname></term>
+              <term><varname>invdate</varname></term>
 
               <listitem>
-                <para>Fälligkeitsdatum für diese Mahhnung</para>
+                <para>Rechnungsdatum</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>dunning_id</varname></term>
+              <term><varname>duedate</varname></term>
 
               <listitem>
-                <para>Mahnungsnummer</para>
+                <para>Fälligkeitsdatum</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>fee</varname></term>
+              <term><varname>amount</varname></term>
 
               <listitem>
-                <para>Kummulative Mahngebühren</para>
+                <para>Summe der Rechnung</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>interest_rate</varname></term>
+              <term><varname>open</varname></term>
 
               <listitem>
-                <para>Zinssatz per anno in Prozent</para>
+                <para>Noch offener Betrag der Rechnung</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>total_amount</varname></term>
+              <term><varname>c0</varname></term>
 
               <listitem>
-                <para>Gesamter noch zu zahlender Betrag als
-                <function>fee</function> + <function>total_interest</function>
-                + <function>total_open_amount</function></para>
+                <para>Noch offener Rechnungsbetrag mit Fälligkeit &lt; 30
+                Tage</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>total_interest</varname></term>
+              <term><varname>c30</varname></term>
 
               <listitem>
-                <para>Zinsen per anno über alle Rechnungen</para>
+                <para>Noch offener Rechnungsbetrag mit Fälligkeit &gt;= 30 und
+                &lt; 60 Tage</para>
               </listitem>
             </varlistentry>
 
             <varlistentry>
-              <term><varname>total_open_amount</varname></term>
+              <term><varname>c60</varname></term>
 
               <listitem>
-                <para>Summe über alle offene Beträge der Rechnungen</para>
+                <para>Noch offener Rechnungsbetrag mit Fälligkeit &gt;= 60 und
+                &lt; 90 Tage</para>
               </listitem>
             </varlistentry>
-          </variablelist>
-        </sect3>
-
-        <sect3 id="dokumentenvorlagen-und-variablen.dunning-details">
-          <title>Variablen für jede gemahnte Rechnung in einer Mahnung</title>
 
-          <variablelist>
             <varlistentry>
-              <term><varname>dn_amount</varname></term>
+              <term><varname>c90</varname></term>
 
               <listitem>
-                <para>Rechnungssumme (brutto)</para>
+                <para>Noch offener Rechnungsbetrag mit Fälligkeit &gt;= 90
+                Tage</para>
               </listitem>
             </varlistentry>
+          </variablelist>
+        </sect3>
+      </sect2>
+
+      <sect2 id="dokumentenvorlagen-und-variablen.bloecke">
+        <title>Blöcke, bedingte Anweisungen und Schleifen</title>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.bloecke.einfuehrung">
+          <title>Einführung</title>
+
+          <para>Der Parser kennt neben den Variablen einige weitere
+          Konstrukte, die gesondert behandelt werden. Diese sind wie
+          Variablennamen in spezieller Weise markiert:
+          <command>&lt;%anweisung%&gt; ... &lt;%end%&gt;</command></para>
+
+          <para>Anmerkung zum <command>&lt;%end%&gt;</command>: Der besseren
+          Verständlichkeit halber kann man nach dem <command>end</command>
+          noch beliebig weitere Wörter schreiben, um so zu markieren, welche
+          Anweisung (z.B. <command>if</command> oder
+          <command>foreach</command>) damit abgeschlossen wird.</para>
+
+          <para>Beispiel: Lautet der Beginn eines Blockes z.B.
+          <command>&lt;%if type == "sales_quotation"%&gt;</command>, so könnte
+          er mit <command>&lt;%end%&gt;</command> genauso abgeschlossen werden
+          wie mit <command>&lt;%end if%&gt;</command> oder auch
+          <command>&lt;%end type == "sales_quotation"%&gt;</command>.</para>
+        </sect3>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.bloecke.if">
+          <title>Der if-Block</title>
+
+          <programlisting>&lt;%if variablenname%&gt;
+...
+&lt;%end%&gt;</programlisting>
+
+          <para>Eine normale "if-then"-Bedingung. Die Zeilen zwischen dem "if"
+          und dem "end" werden nur ausgegeben, wenn die Variable
+          <varname>variablenname</varname> gesetzt und ungleich 0 ist.</para>
+
+          <para>Handelt es sich bei der benannten Variable um ein Array, also
+          um einen Variablennamen, über den man mit <command>&lt;%foreach
+          variablenname%&gt;</command> iteriert, so wird mit diesem Konstrukt
+          darauf getestet, ob das Array Elemente enthält. Somit würde im
+          folgenden Beispiel nur dann eine Liste von Zahlungseingängen samt
+          ihrer Überschrift "Zahlungseingänge" ausgegeben, wenn tatsächlich
+          welche getätigt wurden:</para>
+
+          <programlisting>&lt;%if payment%&gt;
+Zahlungseingänge:
+ &lt;%foreach payment%&gt;
+   Am &lt;%paymentdate%&gt;: &lt;%payment%&gt; €
+ &lt;%end foreach%&gt;
+&lt;%end if%&gt;</programlisting>
+
+          <para>Die Bedingung kann auch negiert werden, indem das Wort
+          <function>not</function> nach dem <filename>if</filename> verwendet
+          wird. Beispiel:</para>
+
+          <programlisting>&lt;%if not cp_greeting%&gt;
+...
+&lt;%end%&gt;</programlisting>
+
+          <para>Zusätzlich zu dem einfachen Test, ob eine Variable gesetzt ist
+          oder nicht, bietet dieser Block auch die Möglichkeit, den Inhalt
+          einer Variablen mit einer festen Zeichenkette oder einer anderen
+          Variablen zu vergleichen. Ob der Vergleich mit einer Zeichenkette
+          oder einer anderen Variablen vorgenommen wird, hängt davon ab, ob
+          die rechte Seite des Vergleichsoperators in Anführungszeichen
+          gesetzt wird (Vergleich mit Zeichenkette) oder nicht (Vergleich mit
+          anderer Variablen). Zwei Beispiele, die beide Vergleiche
+          zeigen:</para>
+
+          <programlisting>&lt;%if var1 == "Wert"%&gt;</programlisting>
+
+          <para>Testet die Variable <varname>var1</varname> auf
+          übereinstimmung mit der Zeichenkette <constant>Wert</constant>.
+          Mittels <function>!=</function> anstelle von <function>==</function>
+          würde auf Ungleichheit getestet.</para>
+
+          <programlisting>&lt;%if var1 == var2%&gt;</programlisting>
+
+          <para>Testet die Variable <varname>var1</varname> auf
+          übereinstimmung mit der Variablen <varname>var2</varname>. Mittel
+          <function>!=</function> anstelle von <function>==</function> würde
+          auf Ungleichheit getestet.</para>
+
+          <para>Erfahrere Benutzer können neben der Tests auf (Un-)Gleichheit
+          auch Tests auf Übereinstimmung mit regulären Ausdrücken ohne
+          Berücksichtung der Groß- und Kleinschreibung durchführen. Dazu dient
+          dieselbe Syntax wie oben nur mit <function>=~</function> und
+          <function>!~</function> als Vergleichsoperatoren.</para>
+
+          <para>Beispiel für einen Test, ob die Variable
+          <varname>intnotes</varname> (interne Bemerkungen) das Wort
+          <constant>schwierig</constant> enthält:</para>
+
+          <programlisting>&lt;%if intnotes =~ "schwierig"%&gt;</programlisting>
+        </sect3>
+
+        <sect3 id="dokumentenvorlagen-und-variablen.bloecke.foreach">
+          <title>Der foreach-Block</title>
+
+          <programlisting>&lt;%foreach variablenname%&gt;
+...
+&lt;%end%&gt;</programlisting>
+
+          <para>Fügt die Zeilen zwischen den beiden Anweisungen so oft ein,
+          wie das Perl-Array der Variablen <varname>variablenname</varname>
+          Elemente enthät. Dieses Konstrukt wird zur Ausgabe der einzelnen
+          Posten einer Rechnung / eines Angebots sowie zur Ausgabe der Steuern
+          benutzt. In jedem Durchlauf werden die <link
+          linkend="dokumentenvorlagen-und-variablen.invoice-posten">zeilenbezogenen
+          Variablen</link> jeweils auf den Wert für die aktuelle Position
+          gesetzt.</para>
+
+          <para>Die Syntax sieht normalerweise wie folgt aus:</para>
+
+          <programlisting>&lt;%foreach number%&gt;
+Position: &lt;%runningnumber%&gt;
+Anzahl: &lt;%qty%&gt;
+Artikelnummer: &lt;%number%&gt;
+Beschreibung: &lt;%description%&gt;
+...
+&lt;%end%&gt;</programlisting>
+
+          <para>Besonderheit in OpenDocument-Vorlagen: Tritt ein
+          <function>&lt;%foreach%&gt;</function>-Block innerhalb einer
+          Tabellenzelle auf, so wird die komplette Tabellenzeile so oft
+          wiederholt wie notwendig. Tritt er außerhalb auf, so wird nur der
+          Inhalt zwischen <function>&lt;%foreach%&gt;</function> und
+          <function>&lt;%end%&gt;</function> wiederholt, nicht aber die
+          komplette Zeile, in der er steht.</para>
+        </sect3>
+      </sect2>
+
+      <sect2 id="dokumentenvorlagen-und-variablen.markup">
+        <title>Markup-Code zur Textformatierung innerhalb von
+        Formularen</title>
+
+        <para>Wenn der Benutzer innhalb von Formularen in kivitendo Text
+        anders formatiert haben möchte, so ist dies begrenzt möglich.
+        kivitendo unterstützt die Textformatierung mit HTML-ähnlichen Tags.
+        Der Benutzer kann z.B. bei der Artikelbeschreibung auf einer Rechnung
+        Teile des Texts zwischen Start- und Endtags setzen. Dieser Teil wird
+        dann automatisch in Anweisungen für das ausgewählte Vorlagenformat
+        (HTML oder PDF über LaTeX) umgesetzt.</para>
+
+        <para>Die unterstützen Formatierungen sind:</para>
+
+        <variablelist>
+          <varlistentry>
+            <term>&lt;b&gt;Text&lt;/b&gt;</term>
+
+            <listitem>
+              <para>Text wird in Fettdruck gesetzt.</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term>&lt;i&gt;Text&lt;/i&gt;</term>
+
+            <listitem>
+              <para>Text wird kursiv gesetzt.</para>
+            </listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term>&lt;u&gt;Text&lt;/u&gt;</term>
+
+            <listitem>
+              <para>Text wird unterstrichen.</para>
+            </listitem>
+          </varlistentry>
 
-            <varlistentry>
-              <term><varname>dn_duedate</varname></term>
+          <varlistentry>
+            <term>&lt;s&gt;Text&lt;/s&gt;</term>
 
-              <listitem>
-                <para>Originales Fälligkeitsdatum der Rechnung</para>
-              </listitem>
-            </varlistentry>
+            <listitem>
+              <para>Text wird durchgestrichen. Diese Formatierung ist nicht
+              bei der Ausgabe als PDF über LaTeX verfügbar.</para>
+            </listitem>
+          </varlistentry>
 
-            <varlistentry>
-              <term><varname>dn_dunning_date</varname></term>
+          <varlistentry>
+            <term>&lt;bullet&gt;</term>
 
-              <listitem>
-                <para>Datum der Mahnung</para>
-              </listitem>
-            </varlistentry>
+            <listitem>
+              <para>Erzeugt einen ausgefüllten Kreis für Aufzählungen (siehe
+              unten).</para>
+            </listitem>
+          </varlistentry>
+        </variablelist>
 
-            <varlistentry>
-              <term><varname>dn_dunning_duedate</varname></term>
+        <para>Der Befehl <command>&lt;bullet&gt;</command> funktioniert
+        momentan auch nur in Latex-Vorlagen.</para>
+      </sect2>
 
-              <listitem>
-                <para>Fälligkeitsdatum der Mahnung</para>
-              </listitem>
-            </varlistentry>
+      <sect2 id="dokumentenvorlagen-und-variablen.anrede"
+             xreflabel="Hinweise zur Anrede">
+        <title>Hinweise zur Anrede</title>
+
+        <para>Das Flag "natürliche Person"
+        (<varname>natural_person</varname>) aus den Kunden- oder
+        Lieferantenstammdaten kann in den Druckvorlagen zusammen mit
+        dem Feld "Anrede" (<varname>greeting</varname>) z.B. dafür
+        verwendet werden, die Anrede zwischen einer allgemeinen und
+        einer persönlichen Anrede zu unterscheiden.
+        <programlisting>&lt;%if natural_person%&gt;&lt;%greeting%&gt; &lt;%name%&gt;&lt;%else%&gt;Sehr geehrte Damen und Herren&lt;%end if%&gt;</programlisting>
+        </para>
+      </sect2>
 
-            <varlistentry>
-              <term><varname>dn_fee</varname></term>
+    </sect1>
 
-              <listitem>
-                <para>Kummulative Mahngebühr</para>
-              </listitem>
-            </varlistentry>
+    <sect1 id="excel-templates">
+      <title>Excel-Vorlagen</title>
 
-            <varlistentry>
-              <term><varname>dn_interest</varname></term>
+      <sect2 id="excel-templates.summary">
+        <title>Zusammenfassung</title>
 
-              <listitem>
-                <para>Zinsen per anno für diese Rechnung</para>
-              </listitem>
-            </varlistentry>
+        <para>Dieses Dokument beschreibt den Mechanismus, mit dem
+        Exceltemplates abgearbeitet werden, und die Einschränkungen, die damit
+        einhergehen.</para>
+      </sect2>
 
-            <varlistentry>
-              <term><varname>dn_invnumber</varname></term>
+      <sect2 id="excel-templates.usage">
+        <title>Bedienung</title>
 
-              <listitem>
-                <para>Rechnungsnummer</para>
-              </listitem>
-            </varlistentry>
+        <para>Der Excel Mechanismus muss in der Konfigurationsdatei aktiviert
+        werden. Die Konfigurationsoption heißt <varname>excel_templates =
+        1</varname> im Abschnitt <varname>[print_templates]</varname>.</para>
 
-            <varlistentry>
-              <term><varname>dn_linetotal</varname></term>
+        <para>Eine Excelvorlage kann dann unter dem Namen einer beliebigen
+        anderen Vorlage mit der Endung <filename>.xls</filename> gespeichert
+        werden. In den normalen Verkaufsmasken taucht nun
+        <constant>Excel</constant> als auswählbares Format auf und kann von da
+        an wie LaTeX- oder OpenOffice-Vorlagen benutzt werden.</para>
 
-              <listitem>
-                <para>Noch zu zahlender Betrag (ergibt sich aus
-                <varname>dn_open_amount</varname> + <varname>dn_fee</varname>
-                + <varname>dn_interest</varname>)</para>
-              </listitem>
-            </varlistentry>
+        <para>Der Sonderfall der Angebote aus der Kundenmaske ist ebenfalls
+        eine Angebotsvorlage und wird unter dem internen Namen der Angebote
+        <filename>sales_quotation.xls</filename> gespeichert.</para>
+      </sect2>
 
-            <varlistentry>
-              <term><varname>dn_netamount</varname></term>
+      <sect2 id="excel-templates.syntax">
+        <title>Variablensyntax</title>
 
-              <listitem>
-                <para>Rechnungssumme (netto)</para>
-              </listitem>
-            </varlistentry>
+        <para>Einfache Syntax:
+        <command>&lt;&lt;varname&gt;&gt;</command></para>
 
-            <varlistentry>
-              <term><varname>dn_open_amount</varname></term>
+        <para>Dabei sind <constant>&lt;&lt;</constant> und
+        <constant>&gt;&gt;</constant> die Delimiter. Da Excel auf festen
+        Breiten besteht, kann der Tag künstlich verlängert werden, indem
+        weitere <constant>&lt;</constant> oder <constant>&gt;</constant>
+        eingefügt werden. Der Tag muss nicht symmetrisch sein.
+        Beispiel:</para>
 
-              <listitem>
-                <para>Offener Rechnungsbetrag</para>
-              </listitem>
-            </varlistentry>
+        <programlisting>&lt;&lt;&lt;&lt;&lt;varname&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;</programlisting>
 
-            <varlistentry>
-              <term><varname>dn_ordnumber</varname></term>
+        <para>Um die Limitierung der festen Breite zu reduzieren, können
+        weitere Variablen in einem Block interpoliert werden. Whitespace wird
+        dazwishen dann erhalten. Beispiel:</para>
 
-              <listitem>
-                <para>Bestellnummer</para>
-              </listitem>
-            </varlistentry>
+        <programlisting>&lt;&lt;&lt;&lt;&lt;varname1 varname2   varname3&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;</programlisting>
 
-            <varlistentry>
-              <term><varname>dn_transdate</varname></term>
+        <para>Die Variablen werden interpoliert, und linksbündig mit
+        Leerzeichen auf die gewünschte Länge aufgefüllt. Ist der String zu
+        lang, werden überzählige Zeichen abgeschnitten.</para>
 
-              <listitem>
-                <para>Rechnungsdatum</para>
-              </listitem>
-            </varlistentry>
+        <para>Es ist ausserdem möglich, Daten rechtsbündig darzustellen, wenn
+        der Block mit einem Leerzeichen anfängt. Beispiel:</para>
 
-            <varlistentry>
-              <term><varname>dn_curr</varname></term>
+        <programlisting>&lt;&lt;&lt;&lt;&lt;&lt;            varname&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;</programlisting>
 
-              <listitem>
-                <para>Währung, in der die Rechnung erstellt wurde. (Die
-                Rechnungsbeträge sind aber immer in der Hauptwährung)</para>
-              </listitem>
-            </varlistentry>
-          </variablelist>
-        </sect3>
+        <para>Dies würde rechtsbündig triggern. Wenn bei rechtsbündiger
+        Ausrichtung Text abgeschnitten werden muss, wird er vom linken Ende
+        entfernt.</para>
+      </sect2>
 
-        <sect3 id="dokumentenvorlagen-und-variablen.dunning-invoice">
-          <title>Variablen in automatisch erzeugten Rechnungen über
-          Mahngebühren</title>
+      <sect2 id="excel-templates.limitations">
+        <title>Einschränkungen</title>
 
-          <para>Die Variablen des Verkäufers stehen wie gewohnt als
-          <varname>employee_...</varname> zur Verfügung. Die Adressdaten des
-          Kunden stehen als Variablen <varname>name</varname>,
-          <varname>street</varname>, <varname>zipcode</varname>,
-          <varname>city</varname>, <varname>country</varname>,
-          <varname>department_1</varname>, <varname>department_2</varname>,
-          und <varname>email</varname> zur Verfügung.</para>
+        <para>Das Excelformat bis 2002 ist ein binäres Format, und kann nicht
+        mit vertretbarem Aufwand editiert werden. Der Templatemechanismus
+        beschränkt sich daher darauf, Textstellen exakt durch einen anderen
+        Text zu ersetzen.</para>
 
-          <para>Weitere Variablen beinhalten:</para>
+        <para>Aus dem gleichen Grund sind die Kontrolllstrukturen
+        <command>&lt;%if%&gt;</command> und
+        <command>&lt;%foreach%&gt;</command> nicht vorhanden. Der Delimiter
+        <constant>&lt;% %&gt;</constant> kommt in den Headerinformationen
+        evtl. vor. Deshalb wurde auf den sichereren Delimiter
+        <constant>&lt;&lt;</constant> und <constant>&gt;&gt;</constant>
+        gewechselt.</para>
+      </sect2>
+    </sect1>
 
-          <variablelist>
-            <varlistentry>
-              <term><varname>duedate</varname></term>
+    <sect1 id="features.warehouse">
+      <title>Mandantenkonfiguration Lager</title>
+
+      <para>Die Lagerverwaltung in kivitendo funktioniert standardmässig wie
+      folgt: Wird ein Lager mit einem Lagerplatz angelegt, so gibt es die
+      Möglichkeit hier über den Menüpunkt Lager entsprechende Warenbewegungen
+      durchzuführen. Ferner kann jede Position eines Lieferscheins ein-, bzw.
+      ausgelagert werden (Einkauf-, bzw. Verkauf). Es können beliebig viele
+      Lager mit beliebig vielen Lagerplätzen abgebildet werden. Die
+      Lagerbewegungen über einen Lieferschein erfolgt durch Anklicken jeder
+      Einzelposition und das Auswählen dieser Position zu einem Lager mit
+      Lagerplatz. Dieses Verfahren lässt sich schrittweise vereinfachen, je
+      nachdem wie die Einstellungen in der Mandatenkonfiguration gesetzt
+      werden.</para>
 
-              <listitem>
-                <para>Fälligkeitsdatum der Rechnung</para>
-              </listitem>
-            </varlistentry>
+      <itemizedlist>
+        <listitem>
+          <para><option>Auslagern über Standardlagerplatz</option> Hier wird
+          ein zusätzlicher Knopf (Auslagern über Standard-Lagerplatz) in dem
+          Lieferschein-Beleg hinzugefügt, der dann alle Lagerbewegungen über
+          den Standardlagerplatz (konfigurierbar pro Ware) durchführt.</para>
+        </listitem>
 
-            <varlistentry>
-              <term><varname>dunning_id</varname></term>
+        <listitem>
+          <para><option>Auslagern ohne Bestandsprüfung</option> Das obige
+          Auslagern schlägt fehl, wenn die entsprechende Menge für die
+          Lagerbewegung nicht vorhanden ist, möchte man dies auch ignorieren
+          und ggf. dann nachpflegen, so kann man eine Negativ-Warenmenge mit
+          dieser Option erlauben. Hierfür muss ein entsprechender Lagerplatz
+          (Fehlbestand, o.ä.) konfiguriert sein.</para>
+        </listitem>
+      </itemizedlist>
 
-              <listitem>
-                <para>Mahnungsnummer</para>
-              </listitem>
-            </varlistentry>
+      <para>Zusätzliche Funktionshinweise:</para>
 
-            <varlistentry>
-              <term><varname>fee</varname></term>
+      <itemizedlist>
+        <listitem>
+          <para><option>Standard-Lagerplatz</option> Ist dieser konfiguriert,
+          wird dies auch als Standard-Voreinstellung bei der Neuerfassung von
+          Stammdaten → Waren / Dienstleistung / Erzeugnis verwendet.</para>
+        </listitem>
 
-              <listitem>
-                <para>Mahngebühren</para>
-              </listitem>
-            </varlistentry>
+        <listitem>
+          <para><option>Standard-Lagerplatz verwenden, falls keiner in
+          Stammdaten definiert</option> Wird beim 'Auslagern über
+          Standardlagerplatz' keine Standardlagerplatz zu der Ware gefunden,
+          so wird mit dieser Option einfach der Standardlagerplatz
+          verwendet.</para>
+        </listitem>
+      </itemizedlist>
+    </sect1>
 
-            <varlistentry>
-              <term><varname>interest</varname></term>
+    <sect1 id="features.swiss-charts-of-accounts">
+      <title>Schweizer Kontenpläne</title>
+
+      <para>Seit der Version 3.5 stehen in kivitendo 3 Kontenpläne für den
+      Einsatz in der Schweiz zur Verfügung, einer für Firmen und
+      Organisationen, die nicht mehrwertsteuerpflichtig sind, einer für
+      Firmen, die mehrwertsteuerpflichtig sind und einer speziell für
+      Vereine.</para>
+
+      <para>Die Kontenpläne orientieren sich am in der Schweiz üblicherweise
+      verwendeten KMU-Kontenrahmen und sind mit der Revision des
+      Schweizerischen Obligationenrechts (OR) vom 1.1.2013 kompatibel,
+      insbesondere <literal>Art.957a Abs.2</literal>.</para>
+
+      <para>Beim Vereinskontenplan sind standardmässig nur die Konten 1100
+      (Debitoren CHF) und 1101 (Debitoren EUR) als Buchungskonten im Verkauf
+      sowie die Konten 2000 (Kreditoren CHF) und 2001 (Kreditoren EUR) als
+      Buchungskonten im Einkauf vorgesehen. Weitere Konten können bei Bedarf
+      in den Konto-Detaileinstellungen als Einkaufs- oder Verkaufskonten
+      konfiguriert werden.</para>
+
+      <para>Die Möglichkeit, Saldosteuersätze zu verwenden ist in der
+      aktuellen Version von kivitendo noch nicht integriert.</para>
+
+      <para>Trotzdem können auch Firmen, die per Saldosteuersatz mit der
+      Eidgenössischen Steuerverwaltung abrechnen, kivitendo bereits nutzen.
+      Dazu wird der Kontenplan mit MWST ausgewählt. Anschliessend müssen alle
+      Aufwandskonten editiert werden und dort der Steuersatz auf 0% gesetzt
+      werden.</para>
+
+      <para>So werden bei Kreditorenbuchungen keine Vorsteuern
+      verbucht.</para>
+
+      <para>Bezugssteuern für aus dem Ausland bezogene Dienstleistungen müssen
+      manuell verbucht werden.</para>
+
+      <para>Wünsche für Anpassungen an den Schweizer Kontenplänen sowie
+      Vorschläge für weitere (z.B. branchenspezifische) Kontenpläne bitte an
+      <literal>empfang@revamp-it.ch</literal> senden.</para>
+    </sect1>
 
-              <listitem>
-                <para>Zinsen</para>
-              </listitem>
-            </varlistentry>
+    <sect1 id="features.part_classification">
+      <title>Artikelklassifizierung</title>
 
-            <varlistentry>
-              <term><varname>invamount</varname></term>
+      <sect2>
+        <title>Übersicht</title>
+
+        <para>Die Klassifizierung von Artikeln dient einer weiteren
+        Gliederung, um zum Beispiel den Einkauf vom Verkauf zu trennen,
+        gekennzeichnet durch eine Beschreibung (z.B. "Einkauf") und ein Kürzel
+        (z.B. "E"). Für jede Klassifizierung besteht eine Beschreibung und
+        eine Abkürzung die normalerweise aus einem Zeichen besteht, kann aber
+        auf mehrere Zeichen erweitert werden, falls zur Unterscheidung
+        notwendig. Sinnvoll sind jedoch nur maximal 2 Zeichen.</para>
+      </sect2>
 
-              <listitem>
-                <para>Rechnungssumme (ergibt sich aus <varname>fee</varname> +
-                <varname>interest</varname>)</para>
-              </listitem>
-            </varlistentry>
+      <sect2>
+        <title>Basisklassifizierung</title>
 
-            <varlistentry>
-              <term><varname>invdate</varname></term>
+        <para>Als Basisklassifizierungen gibt es</para>
 
-              <listitem>
-                <para>Rechnungsdatum</para>
-              </listitem>
-            </varlistentry>
+        <itemizedlist>
+          <listitem>
+            <para>Einkauf</para>
+          </listitem>
 
-            <varlistentry>
-              <term><varname>invnumber</varname></term>
+          <listitem>
+            <para>Verkauf</para>
+          </listitem>
 
-              <listitem>
-                <para>Rechnungsnummer</para>
-              </listitem>
-            </varlistentry>
-          </variablelist>
-        </sect3>
-      </sect2>
+          <listitem>
+            <para>Handelsware</para>
+          </listitem>
 
-      <sect2 id="dokumentenvorlagen-und-variablen.andere-vorlagen">
-        <title>Variablen in anderen Vorlagen</title>
+          <listitem>
+            <para>Produktion</para>
+          </listitem>
 
-        <sect3>
-          <title>Einführung</title>
+          <listitem>
+            <para>- keine - (diese wird bei einer Aktualisierung für alle
+            existierenden Artikel verwendet und ist gültig für Verkauf und
+            Einkauf)</para>
+          </listitem>
+        </itemizedlist>
+
+        <para>Es können weitere Klassifizierungen angelegt werden. So kann es
+        z.B. für separat auszuweisende Artikel folgende Klassen geben:</para>
 
-          <para>Die Variablen in anderen Vorlagen sind ähnlich wie in der
-          Rechnung. Allerdings heißen die Variablen, die mit
-          <varname>inv</varname> beginnen, jetzt anders. Bei den Angeboten
-          fangen sie mit <varname>quo</varname> für "quotation" an:
-          <varname>quodate</varname> für Angebotsdatum etc. Bei Bestellungen
-          wiederum fangen sie mit <varname>ord</varname> für "order" an:
-          <varname>ordnumber</varname> für Bestellnummer etc.</para>
+        <itemizedlist>
+          <listitem>
+            <para>Lieferung (Logistik, Transport) mit Kürzel L</para>
+          </listitem>
 
-          <para>Manche Variablen sind in anderen Vorlagen hingegen gar nicht
-          vorhanden wie z.B. die für bereits verbuchte Zahlungseingänge. Dies
-          sind Variablen, die vom Geschäftsablauf her in der entsprechenden
-          Vorlage keine Bedeutung haben oder noch nicht belegt sein
-          können.</para>
+          <listitem>
+            <para>Material (Verpackungsmaterial) mit Kürzel M</para>
+          </listitem>
+        </itemizedlist>
+      </sect2>
 
-          <para>Im Folgenden werden nur wichtige Unterschiede zu den Variablen
-          in Rechnungen aufgeführt.</para>
-        </sect3>
+      <sect2>
+        <title>Attribute</title>
 
-        <sect3 id="dokumentenvorlagen-und-variablen.andere-vorlagen-quotations">
-          <title>Angebote und Preisanfragen</title>
+        <para>Bisher haben die Klassifizierungen folgende Attribute, die auch
+        alle gleichzeitg gültig sein können</para>
 
-          <variablelist>
-            <varlistentry>
-              <term><varname>quonumber</varname></term>
+        <itemizedlist>
+          <listitem>
+            <para>gültig für Verkauf - dieser Artikel kann im Verkauf genutzt
+            werden</para>
+          </listitem>
 
-              <listitem>
-                <para>Angebots- bzw. Anfragenummer</para>
-              </listitem>
-            </varlistentry>
+          <listitem>
+            <para>gültig für Einkauf - dieser Artikel kann im Einkauf genutzt
+            werden</para>
+          </listitem>
 
-            <varlistentry>
-              <term><varname>reqdate</varname></term>
+          <listitem>
+            <para>separat ausweisen - hierzu gibt es zur Dokumentengenerierung
+            (LaTeX) eine zusätzliche Variable</para>
+          </listitem>
+        </itemizedlist>
 
-              <listitem>
-                <para>Gültigkeitsdatum (bei Angeboten) bzw. Lieferdatum (bei
-                Preisanfragen)</para>
-              </listitem>
-            </varlistentry>
+        <para>Für das Attribut "separat ausweisen" stehen in den
+        LaTeX-Vorlagen die Variable <emphasis
+        role="bold">&lt;%non_separate_subtotal%&gt; </emphasis>zur Verfügung,
+        die alle nicht separat auszuweisenden Artikelkosten saldiert, sowie
+        pro separat auszuweisenden Klassifizierungen die Variable<emphasis
+        role="bold">&lt; %separate_X_subtotal%&gt;</emphasis>, wobei X das
+        Kürzel der Klassifizierung ist.</para>
+
+        <para>Im obigen Beispiel wäre das für Lieferkosten <emphasis
+        role="bold">&lt;%separate_L_subtotal%&gt;</emphasis> und für
+        Verpackungsmaterial <emphasis role="bold">
+        &lt;%separate_M_subtotal%&gt;</emphasis>.</para>
+      </sect2>
 
-            <varlistentry>
-              <term><varname>transdate</varname></term>
+      <sect2>
+        <title>Zwei-Zeichen Abkürzung</title>
 
-              <listitem>
-                <para>Angebots- bzw. Anfragedatum</para>
-              </listitem>
-            </varlistentry>
-          </variablelist>
-        </sect3>
+        <para>Der Typ des Artikels und die Klassifizierung werden durch zwei
+        Buchstaben dargestellt. Der erste Buchstabe ist eine Lokalisierung des
+        Artikel-Typs ('P','A','S'), deutsch 'W', 'E', und 'D' für Ware
+        Erzeugnis oder Dienstleistung und ggf. weiterer Typen.</para>
 
-        <sect3 id="dokumentenvorlagen-und-variablen.andere-vorlagen-orders">
-          <title>Auftragsbestätigungen und Lieferantenaufträge</title>
+        <para>Der zweite Buchstabe (und ggf. auch ein dritter, falls nötig)
+        entspricht der lokalisierten Abkürzung der Klassifizierung.</para>
 
-          <variablelist>
-            <varlistentry>
-              <term><varname>ordnumber</varname></term>
+        <para>Diese Abkürzung wird überall beim Auflisten von Artikeln zur
+        Erleichterung mit dargestellt.</para>
+      </sect2>
+    </sect1>
 
-              <listitem>
-                <para>Auftragsnummer</para>
-              </listitem>
-            </varlistentry>
+    <sect1 id="features.file_managment">
+      <title>Dateiverwaltung (Mini-DMS)</title>
 
-            <varlistentry>
-              <term><varname>reqdate</varname></term>
+      <sect2>
+        <title>Übersicht</title>
 
-              <listitem>
-                <para>Lieferdatum</para>
-              </listitem>
-            </varlistentry>
+        <para>Parallel zum alten WebDAV gibt es ein Datei-Management-System,
+        das Dateien verschiedenen Typs verwaltet. Dies können</para>
 
-            <varlistentry>
-              <term><varname>transdate</varname></term>
+        <orderedlist>
+          <listitem>
+            <para>aus ERP-Daten per LaTeX Template erzeugte
+            PDF-Dokumente,</para>
+          </listitem>
 
-              <listitem>
-                <para>Auftragsdatum</para>
-              </listitem>
-            </varlistentry>
-          </variablelist>
-        </sect3>
+          <listitem>
+            <para>zu bestimmten ERP-Daten gehörende Anhangdateien
+            unterschiedlichen Formats,</para>
+          </listitem>
 
-        <sect3 id="dokumentenvorlagen-und-variablen.andere-vorlagen-delivery-orders">
-          <title>Lieferscheine (Verkauf und Einkauf)</title>
+          <listitem>
+            <para>per Scanner eingelesene PDF-Dateien,</para>
+          </listitem>
 
-          <variablelist>
-            <varlistentry>
-              <term><varname>cusordnumber</varname></term>
+          <listitem>
+            <para>per E-Mail empfangene Dateianhänge unterschiedlichen
+            Formats,</para>
+          </listitem>
 
-              <listitem>
-                <para>Bestellnummer des Kunden (im Verkauf) bzw. Bestellnummer
-                des Lieferanten (im Einkauf)</para>
-              </listitem>
-            </varlistentry>
+          <listitem>
+            <para>sowie speziel für Artikel hochgeladene Bilder sein.</para>
+          </listitem>
+        </orderedlist>
 
-            <varlistentry>
-              <term><varname>donumber</varname></term>
+        <screenshot>
+          <screeninfo>Übersicht</screeninfo>
 
-              <listitem>
-                <para>Lieferscheinnummer</para>
-              </listitem>
-            </varlistentry>
+          <mediaobject>
+            <imageobject>
+              <imagedata contentwidth="600" fileref="images/DMS-Overview.png"/>
+            </imageobject>
+          </mediaobject>
+        </screenshot>
+      </sect2>
 
-            <varlistentry>
-              <term><varname>transdate</varname></term>
+      <sect2>
+        <title>Struktur</title>
 
-              <listitem>
-                <para>Lieferscheindatum</para>
-              </listitem>
-            </varlistentry>
-          </variablelist>
+        <para>Über eine vom Speichermedium unabhängige Zwischenschicht werden
+        die Dateien und ihre Versionen in der Datenbank verwaltet. Darunter
+        können verschiedene Implementierungen (Backends) gleichzeitig
+        existieren:</para>
 
-          <para>Für jede Position eines Lieferscheines gibt es ein Unterarray
-          mit den Informationen darüber, von welchem Lager und Lagerplatz aus
-          die Waren verschickt wurden (Verkaufslieferscheine) bzw. auf welchen
-          Lagerplatz sie eingelagert wurden. Diese müssen mittels einer
-          <function>foreach</function>-Schleife ausgegeben werden. Diese
-          Variablen sind:</para>
+        <itemizedlist>
+          <listitem>
+            <para>Dateisystem</para>
+          </listitem>
 
-          <variablelist>
-            <varlistentry>
-              <term><varname>si_bin</varname></term>
+          <listitem>
+            <para>WebDAV</para>
+          </listitem>
 
-              <listitem>
-                <para>Lagerplatz</para>
-              </listitem>
-            </varlistentry>
+          <listitem>
+            <para>Schnittstelle zu externen
+            Dokumenten-Management-Systemen</para>
+          </listitem>
 
-            <varlistentry>
-              <term><varname>si_chargenumber</varname></term>
+          <listitem>
+            <para>andere Datenbank</para>
+          </listitem>
 
-              <listitem>
-                <para>Chargennummer</para>
-              </listitem>
-            </varlistentry>
+          <listitem>
+            <para>etc ...</para>
+          </listitem>
+        </itemizedlist>
 
-            <varlistentry>
-              <term><varname>si_bestbefore</varname></term>
+        <para>Es gibt unterschiedliche Typen von Dateien. Jedem Typ läßt sich
+        in der Mandantenkonfiguration ein bestimmtes Backend zuordnen.</para>
 
-              <listitem>
-                <para>Mindesthaltbarkeit</para>
-              </listitem>
-            </varlistentry>
+        <itemizedlist>
+          <listitem>
+            <para>"document": Das sind entweder generierte, eingescannte oder
+            hochgeladene PDF-Dateien, die zu bestimmten ERP-Daten
+            (ERP-Objekte, wie z.B. Rechnung, Lieferschein) gehören.</para>
+          </listitem>
 
-            <varlistentry>
-              <term><varname>si_number</varname></term>
+          <listitem>
+            <para>"attachment": zusätzlich hochgeladene Dokumente, die an
+            bestimmte ERP-Objekte angehängt werden, z.B. technische
+            Zeichnungen, Aufmaße. Diese können auch für Artikel, Lieferanten
+            und Kunden hinterlegt sein.</para>
+          </listitem>
 
-              <listitem>
-                <para>Artikelnummer</para>
-              </listitem>
-            </varlistentry>
+          <listitem>
+            <para>"image": Bilder für Artikel. Diese können auch verkleinert
+            in einer Vorschau (Thumbnail) angezeigt werden.</para>
+          </listitem>
+        </itemizedlist>
 
-            <varlistentry>
-              <term><varname>si_qty</varname></term>
+        <para>Zusätzlich werden in der Datenbank zu den Dateien neben der
+        Zuordnung zu ERP-Objekten, Dateityp Dateinamen und Backend, in dem die
+        Datei gespeichert ist, auch die Quelle der Datei notiert:</para>
 
-              <listitem>
-                <para>Anzahl bzw. Menge</para>
-              </listitem>
-            </varlistentry>
+        <itemizedlist>
+          <listitem>
+            <para>"created": vom System erzeugte Dokumente"</para>
+          </listitem>
 
-            <varlistentry>
-              <term><varname>si_runningnumber</varname></term>
+          <listitem>
+            <para>"uploaded": hochgeladene Dokumente</para>
+          </listitem>
 
-              <listitem>
-                <para>Positionsnummer (1, 2, 3 etc)</para>
-              </listitem>
-            </varlistentry>
+          <listitem>
+            <para>"email": vom Mail-System empfangene Dateien</para>
+          </listitem>
 
-            <varlistentry>
-              <term><varname>si_unit</varname></term>
+          <listitem>
+            <para>"scanner[1]": von einem oder mehreren Scannern erzeugte
+            Dateien. Existieren mehrere Scanner, so sind diese durch
+            unterschiedliche Quellennamen zu definieren.</para>
+          </listitem>
+        </itemizedlist>
 
-              <listitem>
-                <para>Einheit</para>
-              </listitem>
-            </varlistentry>
+        <para>Je nach Dateityp sind nur bestimmte Quellen zulässig. So gibt es
+        für "attachment" und "image" nur die Quelle "uploaded". Für "document"
+        gibt es auf jeden Fall die Quelle "created". Die Quellen "scanner" und
+        "email" müssen derzeit in der Datenbank konfiguriert werden (siehe
+        <xref linkend="file_management.dbconfig"/>).</para>
+      </sect2>
 
-            <varlistentry>
-              <term><varname>si_warehouse</varname></term>
+      <sect2>
+        <title>Anwendung</title>
 
-              <listitem>
-                <para>Lager</para>
-              </listitem>
-            </varlistentry>
-          </variablelist>
-        </sect3>
+        <para>Die Daten werden bei den ERP-Objekten als extra Reiter
+        dargestellt. Eine Verkaufsrechnung z.B. hat die Reiter "Dokumente" und
+        "Dateianhänge".</para>
 
-        <sect3 id="dokumentenvorlagen-und-variablen.andere-vorlagen-statement">
-          <title>Variablen für Sammelrechnung</title>
+        <screenshot>
+          <screeninfo>Reiter "Dateianhänge"</screeninfo>
 
-          <variablelist>
-            <varlistentry>
-              <term><varname>c0total</varname></term>
+          <mediaobject>
+            <imageobject>
+              <imagedata fileref="images/DMS-Anhaenge.png" scale="50"/>
+            </imageobject>
+          </mediaobject>
+        </screenshot>
 
-              <listitem>
-                <para>Gesamtbetrag aller Rechnungen mit Fälligkeit &lt; 30
-                Tage</para>
-              </listitem>
-            </varlistentry>
+        <para>Bei den Dateianhängen wird immer nur die aktuelle Version einer
+        Datei angezeigt. Wird eine Datei mit gleichem Namen hochgeladen, so
+        wird eine neue Version der Datei erstellt. Vorher wird der Anwender
+        durch einen Dialog gefragt, ob er eine neue Version anlegen will oder
+        ob er die Datei umbenennen will, falls es eine neue Datei sein
+        soll.</para>
 
-            <varlistentry>
-              <term><varname>c30total</varname></term>
+        <screenshot>
+          <screeninfo>Reiter "Dateianhänge"</screeninfo>
 
-              <listitem>
-                <para>Gesamtbetrag aller Rechnungen mit Fälligkeit &gt;= 30
-                und &lt; 60 Tage</para>
-              </listitem>
-            </varlistentry>
+          <mediaobject>
+            <imageobject>
+              <imagedata contentwidth="40"
+                         fileref="images/DMS-Anhaenge-hochladen.png"
+                         width="100"/>
+            </imageobject>
+          </mediaobject>
+        </screenshot>
 
-            <varlistentry>
-              <term><varname>c60total</varname></term>
+        <para>Es können mehrere Dateien gleichzeitig hochgeladen werden,
+        solange in Summe die maximale Größe nicht überschritten wird (siehe
+        <xref linkend="file_management.clientconfig"/>).</para>
 
-              <listitem>
-                <para>Gesamtbetrag aller Rechnungen mit Fälligkeit &gt;= 60
-                und &lt; 90 Tage</para>
-              </listitem>
-            </varlistentry>
+        <screenshot>
+          <screeninfo>Reiter "Dokumente"</screeninfo>
+
+          <mediaobject>
+            <imageobject>
+              <imagedata fileref="images/DMS-Dokumente.png" width="500"/>
+            </imageobject>
+          </mediaobject>
+        </screenshot>
 
-            <varlistentry>
-              <term><varname>c90total</varname></term>
+        <para>Sind keine weiteren Quellen für Dokumente konfiguriert, so gibt
+        es nur "erzeugte Dokumente". Es werden alle Versionen der generierten
+        Datei angezeigt. Für Verkaufsrechnungen kommen keine anderen Quellen
+        zur Geltung. Werden entsprechend der <xref
+        linkend="file_management.dbconfig"/> zusätzliche Quellen konfiguriert,
+        so sind diese z.B. bei Einkaufsrechnungen sichtbar:</para>
 
-              <listitem>
-                <para>Gesamtbetrag aller Rechnungen mit Fälligkeit &gt;= 90
-                Tage</para>
-              </listitem>
-            </varlistentry>
+        <screenshot>
+          <screeninfo>Reiter "Dokumente"</screeninfo>
 
-            <varlistentry>
-              <term><varname>total</varname></term>
+          <mediaobject>
+            <imageobject>
+              <imagedata contentwidth="600"
+                         fileref="images/DMS-Dokumente-Scanner.png"/>
+            </imageobject>
+          </mediaobject>
+        </screenshot>
 
-              <listitem>
-                <para>Gesamtbetrag aller Rechnungen</para>
-              </listitem>
-            </varlistentry>
-          </variablelist>
+        <para>Statt des Löschens wird hier die Datei zurück zur Quelle
+        verschoben. Somit kann die Datei anschließend an ein anderes
+        ERP-Objekt angehängt werden.</para>
 
-          <para>Variablen für jede Rechnungsposition in Sammelrechnung:</para>
+        <para>Derzeit sind "Titel" und "Beschreibung" noch nicht genutzt. Sie
+        sind bisher nur bei Bildern relevant.</para>
+      </sect2>
 
-          <variablelist>
-            <varlistentry>
-              <term><varname>invnumber</varname></term>
+      <sect2>
+        <title>Konfigurierung</title>
+
+        <sect3 id="file_management.clientconfig"
+               xreflabel="Mandantenkonfigurierung">
+          <title>Mandantenkonfiguration</title>
+
+          <sect4>
+            <title>Reiter "Features"</title>
+
+            <para>Unter dem Reiter <emphasis role="bold">Features</emphasis>
+            im Abschnitt Dateimanagement ist neben dem "alten" WebDAV das
+            Dateimangement generell zu- und abschaltbar, sowie die Zuordnung
+            der Dateitypen zu Backends. Die Löschbarkeit von Dateien, sowie
+            die maximale Uploadgröße sind Backend-unabhängig</para>
+
+            <screenshot>
+              <screeninfo>Mandantenkonfig Reiter "Features"</screeninfo>
+
+              <mediaobject>
+                <imageobject>
+                  <imagedata fileref="images/DMS-ClientConfig.png" width="500"/>
+                </imageobject>
+              </mediaobject>
+            </screenshot>
+
+            <para>Die einzelnen Backends sind einzeln einschaltbar.
+            Spezifische Backend-Konfigurierungen sind hier noch
+            ergänzbar.</para>
+          </sect4>
+
+          <sect4>
+            <title>Reiter "Allgemeine Dokumentenanhänge"</title>
+
+            <para>Unter dem Reiter <emphasis role="bold">Allgemeine
+            Dokumentenanhänge</emphasis> kann für alle ERP-Dokumente (
+            Angebote, Aufträge, Lieferscheine, Rechnungen im Verkauf und
+            Einkauf ) allgemeingültige Anhänge hochgeladen werden.</para>
+
+            <screenshot>
+              <screeninfo>Mandantenkonfig Reiter "Allgemeine
+              Dokumentenanhänge"</screeninfo>
+
+              <mediaobject>
+                <imageobject>
+                  <imagedata fileref="images/DMS-Allgemeine-Dokumentenanhaenge.png"
+                             width="500"/>
+                </imageobject>
+              </mediaobject>
+            </screenshot>
+
+            <para>Diese Anhänge werden beim Generieren von PDF-Dateien an die
+            ERP-Dokumente angehängt, z.B. AGBs oder aktuelle Angebote. Es
+            werden in dem Fall die Daten kopiert, sodass an den ERP-Dokumenten
+            immer die Anhänge zum Generierungszeitpunkt eingebettet
+            sind.</para>
+          </sect4>
+        </sect3>
 
-              <listitem>
-                <para>Rechnungsnummer</para>
-              </listitem>
-            </varlistentry>
+        <sect3 id="file_management.dbconfig"
+               xreflabel="Datenbank-Konfigurierung">
+          <title>Datenbank-Konfigurierung</title>
+
+          <para>Die zusätzlichen Quellen für "email" oder ein oder mehrere
+          Scanner sind derzeit vom Administrator direkt in der
+          Datenbanktabelle "user_preferences" einzurichten. Die "value" ist im
+          JSON-Format mit den jeweiligen Werten des Verzeichnisses und der
+          Beschreibung der Quelle.</para>
+
+          <programlisting>
+ id |  login    |  namespace   | version |   key    |          value
+----+-----------+--------------+---------+----------+---------------------------
+  1 | #default# | file_sources | 0.00000 | scanner1 |
+                             {"dir":"/var/tmp/scanner1","desc":"Scanner Einkauf"}
+  2 | #default# | file_sources | 0.00000 | scanner2 |
+                             {"dir":"/var/tmp/scanner2","desc":"Scanner Verkauf"}
+  3 | #default# | file_sources | 0.00000 | emails   |
+                             {"dir":"/var/tmp/emails","desc":"Empfangene Mails" }
+          </programlisting>
+
+          <para>Es ist daran gedacht, statt dem Default-Eintrag später für
+          bestimmte Benutzer ('login') bestimmte Quellen zuzulassen. Dies wird
+          nach Bedarf implementiert.</para>
+        </sect3>
 
-            <varlistentry>
-              <term><varname>invdate</varname></term>
+        <sect3 id="file_management.kiviconfig"
+               xreflabel="kivitendo-Konfigurationsdatei">
+          <title>kivitendo-Konfigurationsdatei</title>
 
-              <listitem>
-                <para>Rechnungsdatum</para>
-              </listitem>
-            </varlistentry>
+          <para>Dort ist im Abschnitt [paths] der relative oder absolute Pfad
+          zum Dokumentenwurzelverzeichnis einzutragen. Dieser muss für den
+          Webserver schreib- und lesbar sein, jedoch nicht ausführbar.</para>
 
-            <varlistentry>
-              <term><varname>duedate</varname></term>
+          <programlisting>
+[paths]
+document_path = /var/local/kivi_documents
+          </programlisting>
 
-              <listitem>
-                <para>Fälligkeitsdatum</para>
-              </listitem>
-            </varlistentry>
+          <para>Unter diesem Wurzelverzeichnis wird pro Mandant automatisch
+          ein Unterverzeichnis mit der ID des Mandanten angelegt.</para>
+        </sect3>
+      </sect2>
+    </sect1>
 
-            <varlistentry>
-              <term><varname>amount</varname></term>
+    <sect1>
+      <title>Webshop-Api</title>
 
-              <listitem>
-                <para>Summe der Rechnung</para>
-              </listitem>
-            </varlistentry>
+      <para>Das Shopmodul bietet die Möglichkeit Onlineshopartikel und
+      Onlineshopbestellungen zu verwalten und zu bearbeiten.</para>
 
-            <varlistentry>
-              <term><varname>open</varname></term>
+      <para>Es ist Multishopfähig, d.h. Artikel können mehreren oder
+      unterschiedlichen Shops zugeordnet werden. Bestellungen können aus
+      mehreren Shops geholt werden.</para>
 
-              <listitem>
-                <para>Noch offener Betrag der Rechnung</para>
-              </listitem>
-            </varlistentry>
+      <para>Zur Zeit bietet das Modul nur einen Connector zur REST-Api von
+      Shopware. Weitere Connectoren können dazu programmiert und eingerichtet
+      werden.</para>
 
-            <varlistentry>
-              <term><varname>c0</varname></term>
+      <sect2>
+        <title>Rechte für die Webshopapi</title>
 
-              <listitem>
-                <para>Noch offener Rechnungsbetrag mit Fälligkeit &lt; 30
-                Tage</para>
-              </listitem>
-            </varlistentry>
+        <para>In der Administration können folgende Rechte vergeben
+        werden</para>
 
-            <varlistentry>
-              <term><varname>c30</varname></term>
+        <itemizedlist>
+          <listitem>
+            <para>Webshopartikel anlegen und bearbeiten</para>
+          </listitem>
 
-              <listitem>
-                <para>Noch offener Rechnungsbetrag mit Fälligkeit &gt;= 30 und
-                &lt; 60 Tage</para>
-              </listitem>
-            </varlistentry>
+          <listitem>
+            <para>Shopbestellungen holen und bearbeiten</para>
+          </listitem>
 
-            <varlistentry>
-              <term><varname>c60</varname></term>
+          <listitem>
+            <para>Shop anlegen und bearbeiten</para>
+          </listitem>
+        </itemizedlist>
+      </sect2>
 
-              <listitem>
-                <para>Noch offener Rechnungsbetrag mit Fälligkeit &gt;= 60 und
-                &lt; 90 Tage</para>
-              </listitem>
-            </varlistentry>
+      <sect2>
+        <title>Konfiguration</title>
 
-            <varlistentry>
-              <term><varname>c90</varname></term>
+        <para>Unter System-&gt;Webshops können Shops angelegt und konfiguriert
+        werden</para>
 
-              <listitem>
-                <para>Noch offener Rechnungsbetrag mit Fälligkeit &gt;= 90
-                Tage</para>
-              </listitem>
-            </varlistentry>
-          </variablelist>
-        </sect3>
+        <mediaobject>
+          <imageobject>
+            <imagedata contentdepth="500" contentwidth="700"
+                       fileref="images/Shop_Listing.png"/>
+          </imageobject>
+        </mediaobject>
       </sect2>
 
-      <sect2 id="dokumentenvorlagen-und-variablen.bloecke">
-        <title>Blöcke, bedingte Anweisungen und Schleifen</title>
+      <sect2>
+        <title>Webshopartikel</title>
 
-        <sect3 id="dokumentenvorlagen-und-variablen.bloecke.einfuehrung">
-          <title>Einführung</title>
+        <sect3>
+          <title>Shopvariablenreiter in Artikelstammdaten</title>
 
-          <para>Der Parser kennt neben den Variablen einige weitere
-          Konstrukte, die gesondert behandelt werden. Diese sind wie
-          Variablennamen in spezieller Weise markiert:
-          <command>&lt;%anweisung%&gt; ... &lt;%end%&gt;</command></para>
+          <para>Mit dem Recht "Shopartikel anlegen und bearbeiten" und des
+          Markers <emphasis role="bold">"Shopartikel" in den Basisdaten
+          </emphasis>zeigt sich der Reiter "Shopvariablen" in den
+          Artikelstammdaten. Hier können jetzt die Artikel mit
+          unterschiedlichen Beschreibung und/oder Preisen für die
+          konfigutierten Shops angelegt und bearbeitet werden. An dieser
+          Stelle können auch beliebig viele Bilder dem Shopartikel zugeordnet
+          werden. Artikelbilder gelten für alle Shops.</para>
 
-          <para>Anmerkung zum <command>&lt;%end%&gt;</command>: Der besseren
-          Verständlichkeit halber kann man nach dem <command>end</command>
-          noch beliebig weitere Wörter schreiben, um so zu markieren, welche
-          Anweisung (z.B. <command>if</command> oder
-          <command>foreach</command>) damit abgeschlossen wird.</para>
+          <mediaobject>
+            <imageobject>
+              <imagedata contentdepth="500" contentwidth="600"
+                         fileref="images/Shop_Artikel.png"/>
+            </imageobject>
+          </mediaobject>
 
-          <para>Beispiel: Lautet der Beginn eines Blockes z.B.
-          <command>&lt;%if type == "sales_quotation"%&gt;</command>, so könnte
-          er mit <command>&lt;%end%&gt;</command> genauso abgeschlossen werden
-          wie mit <command>&lt;%end if%&gt;</command> oder auch
-          <command>&lt;%end type == "sales_quotation"%&gt;</command>.</para>
+          <para>Die Artikelgruppen werden direkt vom Shopsystem geholt somit
+          ist es möglich einen Artikel auch mehreren Gruppen
+          zuzuordenen</para>
         </sect3>
 
-        <sect3 id="dokumentenvorlagen-und-variablen.bloecke.if">
-          <title>Der if-Block</title>
+        <sect3>
+          <title>Shopartikelliste</title>
 
-          <programlisting>&lt;%if variablenname%&gt;
-...
-&lt;%end%&gt;</programlisting>
+          <para>Unter dem Menu Webshop-&gt;Webshop Artikel hat man nochmal
+          eine Gesamtübersicht. Von hier aus ist es möglich Artikel im Stapel
+          unter verschiedenen Kriterien &lt;alles&gt;&lt;nur Preis&gt;&lt;nur
+          Bestand&gt;&lt;Preis und Bestand&gt; an die jeweiligen Shops
+          hochzuladen.</para>
 
-          <para>Eine normale "if-then"-Bedingung. Die Zeilen zwischen dem "if"
-          und dem "end" werden nur ausgegeben, wenn die Variable
-          <varname>variablenname</varname> gesetzt und ungleich 0 ist.</para>
+          <mediaobject>
+            <imageobject>
+              <imagedata fileref="images/Shop_Artikel_Listing.png"/>
+            </imageobject>
+          </mediaobject>
+        </sect3>
+      </sect2>
 
-          <para>Handelt es sich bei der benannten Variable um ein Array, also um einen Variablennamen, über den man mit
-          <command>&lt;%foreach variablenname%&gt;</command> iteriert, so wird mit diesem Konstrukt darauf getestet, ob das Array Elemente
-          enthält. Somit würde im folgenden Beispiel nur dann eine Liste von Zahlungseingängen samt ihrer Überschrift "Zahlungseingänge"
-          ausgegeben, wenn tatsächlich welche getätigt wurden:</para>
+      <sect2>
+        <title>Bestellimport</title>
 
-          <programlisting>&lt;%if payment%&gt;
-Zahlungseingänge:
- &lt;%foreach payment%&gt;
-   Am &lt;%paymentdate%&gt;: &lt;%payment%&gt; €
- &lt;%end foreach%&gt;
-&lt;%end if%&gt;</programlisting>
+        <para>Unter dem Menupunkt Webshop-&gt;Webshop Import öffnet sich die
+        Bestellimportsliste. Hier ist sind Möglichkeiten gegeben Neue
+        Bestellungen vom Shop abzuholen, geholte Bestellungen im Stapel oder
+        einzeln als Auftrag zu transferieren. Die Liste kann nach
+        verschiedenen Kriterien gefiltert werden.</para>
 
-          <para>Die Bedingung kann auch negiert werden, indem das Wort
-          <function>not</function> nach dem <filename>if</filename> verwendet
-          wird. Beispiel:</para>
+        <mediaobject>
+          <imageobject>
+            <imagedata fileref="images/Shop_Bestell.png"/>
+          </imageobject>
+        </mediaobject>
 
-          <programlisting>&lt;%if not cp_greeting%&gt;
-...
-&lt;%end%&gt;</programlisting>
+        <para>Bei Einträgen in der Liste.</para>
 
-          <para>Zusätzlich zu dem einfachen Test, ob eine Variable gesetzt ist
-          oder nicht, bietet dieser Block auch die Möglichkeit, den Inhalt
-          einer Variablen mit einer festen Zeichenkette oder einer anderen
-          Variablen zu vergleichen. Ob der Vergleich mit einer Zeichenkette
-          oder einer anderen Variablen vorgenommen wird, hängt davon ab, ob
-          die rechte Seite des Vergleichsoperators in Anführungszeichen
-          gesetzt wird (Vergleich mit Zeichenkette) oder nicht (Vergleich mit
-          anderer Variablen). Zwei Beispiele, die beide Vergleiche
-          zeigen:</para>
+        <itemizedlist>
+          <listitem>
+            <para>keine Kundennummer: Es gibt ähnliche Kundendatensätze und
+            der Datensatz konnte nicht eindeutig zugewiesen werden.</para>
+          </listitem>
 
-          <programlisting>&lt;%if var1 == "Wert"%&gt;</programlisting>
+          <listitem>
+            <para>Kundennummer und Rechnungen rot hinterlegt: Der Kunde hat
+            offene Posten und kann deswegen nicht im Stapel übernommen
+            werden.</para>
+          </listitem>
 
-          <para>Testet die Variable <varname>var1</varname> auf
-          übereinstimmung mit der Zeichenkette <constant>Wert</constant>.
-          Mittels <function>!=</function> anstelle von <function>==</function>
-          würde auf Ungleichheit getestet.</para>
+          <listitem>
+            <para>Rechnungsadresse grün hinterlegt: Der Kunde konnte eindeutig
+            einem Datensatz zugeordnet werden. Die Shopbestellung kann im
+            Stapel mit dem Button "Anwenden" und wenn markiert als Auftrag
+            übernommen werden.</para>
+          </listitem>
 
-          <programlisting>&lt;%if var1 == var2%&gt;</programlisting>
+          <listitem>
+            <para>Kundennummer vorhanden, aber die Checkbox "Auftrag
+            erstellen" fehlt. Der Kunde hat vermutlich eine
+            Shopauftragssperre.</para>
+          </listitem>
 
-          <para>Testet die Variable <varname>var1</varname> auf
-          übereinstimmung mit der Variablen <varname>var2</varname>. Mittel
-          <function>!=</function> anstelle von <function>==</function> würde
-          auf Ungleichheit getestet.</para>
+          <listitem>
+            <para>Lieferadresse grau hinterlegt: Optische Anzeige, dass es
+            sich um eine unterschiedliche Lieferadresse handelt.
+            Lieferadressen werden aber grundsätzlich beim Transferieren zu
+            Aufträgen mit übernommen.</para>
+          </listitem>
 
-          <para>Erfahrere Benutzer können neben der Tests auf (Un-)Gleichheit
-          auch Tests auf Übereinstimmung mit regulären Ausdrücken ohne
-          Berücksichtung der Groß- und Kleinschreibung durchführen. Dazu dient
-          dieselbe Syntax wie oben nur mit <function>=~</function> und
-          <function>!~</function> als Vergleichsoperatoren.</para>
+          <listitem>
+            <para>In der Spalte Positionen/Betrag/Versandkosten zeigt sich ein
+            tooltip zu den Positionen.</para>
+          </listitem>
+        </itemizedlist>
 
-          <para>Beispiel für einen Test, ob die Variable
-          <varname>intnotes</varname> (interne Bemerkungen) das Wort
-          <constant>schwierig</constant> enthält:</para>
+        <para>Maske Auftrag erstellen</para>
 
-          <programlisting>&lt;%if intnotes =~ "schwierig"%&gt;</programlisting>
-        </sect3>
+        <para>Viele Shopsysteme haben drei verschieden Adresstypen Kunden-,
+        Rechnungs-, und Lieferadresse, die sich auch alle unterscheiden
+        können. Diese werden im oberen Bereich angezeigt. Es ist möglich jede
+        dieser Adresse einzeln in kivitendo als Kunde zu übernehmen. Es werden
+        die Werte Formulareingabe übernommen. Es wird bei einer Änderung
+        allerdings nur diese in die kivitendo Kundenstammdaten übernommen, die
+        Shopbestellung bleibt bestehen.</para>
 
-        <sect3 id="dokumentenvorlagen-und-variablen.bloecke.foreach">
-          <title>Der foreach-Block</title>
+        <para>Mit der mittleren Adresse(Rechnungsadresse) im oberen Bereich,
+        kann ich den ausgewählten kivitendodatensatz des mittleren Bereich
+        überschreiben. Das ist sinnvoll, wenn ich erkenne, das der Kunde z.B.
+        umgezogen ist.</para>
 
-          <programlisting>&lt;%foreach variablenname%&gt;
-...
-&lt;%end%&gt;</programlisting>
+        <para>Im mittleren Bereich das Adresslisting zeigt:</para>
 
-          <para>Fügt die Zeilen zwischen den beiden Anweisungen so oft ein,
-          wie das Perl-Array der Variablen <varname>variablenname</varname>
-          Elemente enthät. Dieses Konstrukt wird zur Ausgabe der einzelnen
-          Posten einer Rechnung / eines Angebots sowie zur Ausgabe der Steuern
-          benutzt. In jedem Durchlauf werden die <link
-          linkend="dokumentenvorlagen-und-variablen.invoice-posten">zeilenbezogenen
-          Variablen</link> jeweils auf den Wert für die aktuelle Position
-          gesetzt.</para>
+        <itemizedlist>
+          <listitem>
+            <para>Rot hinterlegt: Kunde hat eine Shopauftragssperre, diese
+            muss zuerst deaktiviert werden bevor ich diesem Kunden eine
+            Shopbestellung zuordnen kann.</para>
+          </listitem>
 
-          <para>Die Syntax sieht normalerweise wie folgt aus:</para>
+          <listitem>
+            <para>Kundenname fett und rot: Hier hat der Kunde eine Bemerkung
+            in den Stammdaten. Ein Tooltip zeigt diese Bemerkung. Das kann dan
+            auch der Grund für die Auftragssperre sein.</para>
+          </listitem>
 
-          <programlisting>&lt;%foreach number%&gt;
-Position: &lt;%runningnumber%&gt;
-Anzahl: &lt;%qty%&gt;
-Artikelnummer: &lt;%number%&gt;
-Beschreibung: &lt;%description%&gt;
-...
-&lt;%end%&gt;</programlisting>
+          <listitem>
+            <para>Die Buttons "Auftrag erstellen" und "Kunde mit
+            Rechnungsadresse überschreiben" zeigen sich erst, wenn ein Kunde
+            aus dem Listing ausgewählt ist.</para>
+          </listitem>
 
-          <para>Besonderheit in OpenDocument-Vorlagen: Tritt ein
-          <function>&lt;%foreach%&gt;</function>-Block innerhalb einer
-          Tabellenzelle auf, so wird die komplette Tabellenzeile so oft
-          wiederholt wie notwendig. Tritt er außerhalb auf, so wird nur der
-          Inhalt zwischen <function>&lt;%foreach%&gt;</function> und
-          <function>&lt;%end%&gt;</function> wiederholt, nicht aber die
-          komplette Zeile, in der er steht.</para>
-        </sect3>
+          <listitem>
+            <para>Es ist aber möglich die Shopbestellung zu löschen.</para>
+          </listitem>
+
+          <listitem>
+            <para>Ist eine Bestellung schon übernommen, zeigen sich an dieser
+            Stelle, die dazugehörigen Belegverknüpfungen.</para>
+          </listitem>
+        </itemizedlist>
       </sect2>
 
-      <sect2 id="dokumentenvorlagen-und-variablen.markup">
-        <title>Markup-Code zur Textformatierung innerhalb von
-        Formularen</title>
+      <sect2>
+        <title>Mapping der Daten</title>
+
+        <para>Das Mapping der kivitendo Daten mit den Shopdaten geschieht in
+        der Datei SL/ShopConnector/&lt;SHOPCONNECTORNAME&gt;.pm
+        z.B.:SL/ShopConnector/Shopware.pm</para>
+
+        <para>In dieser Datei gibt es einen Bereich wo die Bestellpostionen,
+        die Bestellkopfdaten und die Artikeldaten gemapt werden. In dieser
+        Datei kann ein individelles Mapping dann gemacht werden. Zu Shopware
+        gibt es hier eine sehr gute Dokumentation: <ulink
+        url="https://developers.shopware.com/developers-guide/rest-api/">https://developers.shopware.com/developers-guide/rest-api/</ulink></para>
+      </sect2>
+    </sect1>
+       <sect1 id="features.zugferd">
+               <title>ZUGFeRD Rechnungen</title>
+               <sect2 id="features.zugferd.preamble">
+                       <title>Vorbedingung</title>
+         <para>
+          Für die Erstellung von ZUGFeRD PDFs wird TexLive2018 oder höher benötigt.
+         </para>
+        <note>
+         <para>
+          Wer kein TexLive2018 oder höher installieren kann, kann eine lokale Umgebung nur für kivitendo wie folgt erzeugen:
+         </para>
+        <programlisting>
+        1. Download des offiziellen Installers von https://www.tug.org/texlive/quickinstall.html
+
+        2. Installer ausführen, Standard-Ort für Installation belassen, evtl. ein paar Pakete abwählen, installieren lassen
 
-        <para>Wenn der Benutzer innhalb von Formularen in kivitendo Text
-        anders formatiert haben möchte, so ist dies begrenzt möglich.
-        kivitendo unterstützt die Textformatierung mit HTML-ähnlichen Tags.
-        Der Benutzer kann z.B. bei der Artikelbeschreibung auf einer Rechnung
-        Teile des Texts zwischen Start- und Endtags setzen. Dieser Teil wird
-        dann automatisch in Anweisungen für das ausgewählte Vorlagenformat
-        (HTML oder PDF über LaTeX) umgesetzt.</para>
+        3. Ein kleine Script »run_pdflatex.sh« anlegen, das den PATH auf das  Installationsverezichnis setzt und pdflatex ausführt:
 
-        <para>Die unterstützen Formatierungen sind:</para>
+        ------------------------------------------------------------
+        #!/bin/bash
 
-        <variablelist>
-          <varlistentry>
-            <term>&lt;b&gt;Text&lt;/b&gt;</term>
+        export PATH=/usr/local/texlive/2020/bin/x86_64-linux:$PATH
+        hash -r
 
-            <listitem>
-              <para>Text wird in Fettdruck gesetzt.</para>
-            </listitem>
-          </varlistentry>
+        exec pdflatex &quot;$@&quot;
+        ------------------------------------------------------------
 
-          <varlistentry>
-            <term>&lt;i&gt;Text&lt;/i&gt;</term>
+        4. In config/kivitendo.conf den Parameter »latex« auf den Pfad zu »run_pdflatex.sh« setzen
 
-            <listitem>
-              <para>Text wird kursiv gesetzt.</para>
-            </listitem>
-          </varlistentry>
+        5. Webserver neu starten
+        </programlisting>
+      </note>
+    </sect2>
+               <sect2 id="features.zugferd.summary">
+                       <title>Übersicht</title>
+
+                       <para>Mit der Version 3.5.6 bietet kivitendo die Möglichkeit ZUGFeRD
+                       Rechnungen zu erstellen, sowie auch  ZUGFeRD Rechnungen direkt in
+                       kivitendo einzulesen. </para>
+
+                       <para>Bei  ZUGFeRD Rechnungen handelt es sich um eine PDF Datei in
+                       der eine XML-Datei eingebettet ist. Der Aufbau der XML-Datei ist
+                       standardisiert und ermöglicht so den Austausch zwischen
+                       den verschiedenen Softwareprodukten. Kivitendo setzt mit der
+                       Version 3.5.6 den ZUGFeRD 2.1 Standard um.</para>
+
+                       <para>Weiter Details zu ZUGFeRD sind unter diesem Link zu finden:
+                       <ulink url="https://www.ferd-net.de/standards/was-ist-zugferd/index.html">https://www.ferd-net.de/standards/was-ist-zugferd/index.html</ulink>
+      </para>
+               </sect2>
 
-          <varlistentry>
-            <term>&lt;u&gt;Text&lt;/u&gt;</term>
+               <sect2 id="features.zugferd.create_zugferd_bills">
 
-            <listitem>
-              <para>Text wird unterstrichen.</para>
-            </listitem>
-          </varlistentry>
+                       <title>Erstellen von ZUGFeRD Rechnungen in Kivitendo</title>
+                       <para>Für die Erstellung von ZUGFeRD Rechnungen bedarf es in
+                       kivitendo zwei Dinge:</para>
 
-          <varlistentry>
-            <term>&lt;s&gt;Text&lt;/s&gt;</term>
+                       <orderedlist>
 
-            <listitem>
-              <para>Text wird durchgestrichen. Diese Formatierung ist nicht
-              bei der Ausgabe als PDF über LaTeX verfügbar.</para>
-            </listitem>
-          </varlistentry>
+                               <listitem>
+                                       <para>Die Erstellung muss in der Mandantenkonfiguration
+                                       aktiviert sein</para>
+                               </listitem>
 
-          <varlistentry>
-            <term>&lt;bullet&gt;</term>
+                               <listitem>
+                                       <para>Beim mindestens einem Bankkonto muss die Option
+                                       „Nutzung von ZUGFeRD“ aktiviert sein</para>
+                               </listitem>
 
-            <listitem>
-              <para>Erzeugt einen ausgefüllten Kreis für Aufzählungen (siehe
-              unten).</para>
-            </listitem>
-          </varlistentry>
-        </variablelist>
+                       </orderedlist>
+                       <sect3>
+                               <title>Mandantenkonfiguration</title>
 
-        <para>Der Befehl <command>&lt;bullet&gt;</command> funktioniert
-        momentan auch nur in Latex-Vorlagen.</para>
-      </sect2>
-    </sect1>
+                               <para>Die Einstellung für die Erstellung von ZUGFeRD Rechnungen
+                               erfolgt unter „System“ → „Mandatenkonfiguration“ → „Features“.
+                               Im Abschnitt „Einkauf und Verkauf“ finden Sie die Einstellung
+                               „Verkaufsrechnungen mit ZUGFeRD-Daten erzeugen“.
+                               Hier besteht die Auswahl zwischen:</para>
 
-    <sect1 id="excel-templates">
-      <title>Excel-Vorlagen</title>
+                               <itemizedlist>
+                                       <listitem>
+                                               <para>ZUGFeRD-Rechnungen erzeugen</para>
+                                       </listitem>
 
-      <sect2 id="excel-templates.summary">
-        <title>Zusammenfassung</title>
+                                       <listitem>
+                                               <para>ZUGFeRD-Rechnungen im Testmodus erzeugen</para>
+                                       </listitem>
 
-        <para>Dieses Dokument beschreibt den Mechanismus, mit dem
-        Exceltemplates abgearbeitet werden, und die Einschränkungen, die damit
-        einhergehen.</para>
-      </sect2>
+                                       <listitem>
+                                               <para>Keine ZUGFeRD Rechnungen erzeugen</para>
+                                       </listitem>
 
-      <sect2 id="excel-templates.usage">
-        <title>Bedienung</title>
+                               </itemizedlist>
 
-        <para>Der Excel Mechanismus muss in der Konfigurationsdatei aktiviert
-        werden. Die Konfigurationsoption heißt <varname>excel_templates =
-        1</varname> im Abschnitt <varname>[print_templates]</varname>.</para>
+                               <para>Rechnungen die als PDF erzeugt werden, werden je nach
+                               Einstellung nun im ZUGFeRD Format ausgegeben.</para>
 
-        <para>Eine Excelvorlage kann dann unter dem Namen einer beliebigen
-        anderen Vorlage mit der Endung <filename>.xls</filename> gespeichert
-        werden. In den normalen Verkaufsmasken taucht nun
-        <constant>Excel</constant> als auswählbares Format auf und kann von da
-        an wie LaTeX- oder OpenOffice-Vorlagen benutzt werden.</para>
+                       </sect3>
 
-        <para>Der Sonderfall der Angebote aus der Kundenmaske ist ebenfalls
-        eine Angebotsvorlage und wird unter dem internen Namen der Angebote
-        <filename>sales_quotation.xls</filename> gespeichert.</para>
-      </sect2>
+                       <sect3>
+                               <title>Konfiguration der Bankkonten</title>
 
-      <sect2 id="excel-templates.syntax">
-        <title>Variablensyntax</title>
+                               <para>Unter „System → Bankkonten“ muss bei mindestens einem
+                               Bankkonto die Option „Nutzung mit ZUGFeRD“ auf „Ja“ gestellt
+                               werden.</para>
+                       </sect3>
 
-        <para>Einfache Syntax:
-        <command>&lt;&lt;varname&gt;&gt;</command></para>
+               </sect2>
 
-        <para>Dabei sind <constant>&lt;&lt;</constant> und
-        <constant>&gt;&gt;</constant> die Delimiter. Da Excel auf festen
-        Breiten besteht, kann der Tag künstlich verlängert werden, indem
-        weitere <constant>&lt;</constant> oder <constant>&gt;</constant>
-        eingefügt werden. Der Tag muss nicht symmetrisch sein.
-        Beispiel:</para>
+               <sect2 id="features.zugferd.read_zugferd_bills">
 
-        <programlisting>&lt;&lt;&lt;&lt;&lt;varname&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;</programlisting>
+                       <title>Einlesen von ZUGFeRD Rechnungen in Kivitendo</title>
 
-        <para>Um die Limitierung der festen Breite zu reduzieren, können
-        weitere Variablen in einem Block interpoliert werden. Whitespace wird
-        dazwishen dann erhalten. Beispiel:</para>
+                       <para>Es lassen sich auch Rechnungen von Kreditoren, die im
+                       ZUGFeRD Format erstellt wurden, nach Kivitendo importieren.
+                       Hierfür müssen auch zwei Voraussetzungen erfüllt werden:
+                       </para>
 
-        <programlisting>&lt;&lt;&lt;&lt;&lt;varname1 varname2   varname3&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;</programlisting>
+                       <orderedlist>
 
-        <para>Die Variablen werden interpoliert, und linksbündig mit
-        Leerzeichen auf die gewünschte Länge aufgefüllt. Ist der String zu
-        lang, werden überzählige Zeichen abgeschnitten.</para>
+                               <listitem>
+                                       <para>Beim Lieferanten muss die Umsatzsteuer-ID und das
+                                       Bankkonto hinterlegt sein</para>
+                               </listitem>
 
-        <para>Es ist ausserdem möglich, Daten rechtsbündig darzustellen, wenn
-        der Block mit einem Leerzeichen anfängt. Beispiel:</para>
+                               <listitem>
+                                       <para>Für den Kreditoren muss eine Buchungsvorlage existieren.</para>
+                               </listitem>
 
-        <programlisting>&lt;&lt;&lt;&lt;&lt;&lt;            varname&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;</programlisting>
+                       </orderedlist>
 
-        <para>Dies würde rechtsbündig triggern. Wenn bei rechtsbündiger
-        Ausrichtung Text abgeschnitten werden muss, wird er vom linken Ende
-        entfernt.</para>
-      </sect2>
+                       <para>Wenn diese Voraussetzungen erfüllt sind, kann die Rechnung
+                       über „Finanzbuchhaltung“ → „Factur-X-/ZUGFeRD-Import“ über die „Durchsuchen“
+                       Schaltfläche ausgewählt werden und über die Schaltfläche „Import“
+                       eingeladen werden. Es öffnet sich daraufhin die Kreditorenbuchung.
+                       Die auslesbaren Daten aus dem eingebetteten XML der PDF Datei
+                       werden in der Kreditorenbuchung ergänzt.</para>
 
-      <sect2 id="excel-templates.limitations">
-        <title>Einschränkungen</title>
+               </sect2>
 
-        <para>Das Excelformat bis 2002 ist ein binäres Format, und kann nicht
-        mit vertretbarem Aufwand editiert werden. Der Templatemechanismus
-        beschränkt sich daher darauf, Textstellen exakt durch einen anderen
-        Text zu ersetzen.</para>
+       </sect1>
 
-        <para>Aus dem gleichen Grund sind die Kontrolllstrukturen
-        <command>&lt;%if%&gt;</command> und
-        <command>&lt;%foreach%&gt;</command> nicht vorhanden. Der Delimiter
-        <constant>&lt;% %&gt;</constant> kommt in den Headerinformationen
-        evtl. vor. Deshalb wurde auf den sichereren Delimiter
-        <constant>&lt;&lt;</constant> und <constant>&gt;&gt;</constant>
-        gewechselt.</para>
-      </sect2>
-    </sect1>
-    <sect1 id="features.warehouse">
-    <title>Mandantenkonfiguration Lager</title>
-        Die Lagerverwaltung in kivitendo funktioniert standardmässig wie folgt:
-        Wird ein Lager mit einem Lagerplatz angelegt, so gibt es die Möglichkeit hier über den
-        Menüpunkt Lager entsprechende Warenbewegungen durchzuführen. Ferner kann
-        jede Position eines Lieferscheins ein-, bzw. ausgelagert werden (Einkauf-, bzw. Verkauf).
-        Es können beliebig viele Lager mit beliebig vielen Lagerplätzen abgebildet werden.
-        Die Lagerbewegungen über einen Lieferschein erfolgt durch Anklicken jeder Einzelposition und
-        das Auswählen dieser Position zu einem Lager mit Lagerplatz.
-        Dieses Verfahren lässt sich schrittweise vereinfachen, je nachdem wie die Einstellungen in
-        der Mandatenkonfiguration gesetzt werden.
-       <itemizedlist>
-          <listitem>
-            <para><option>Auslagern über Standardlagerplatz</option> Hier wird ein zusätzlicher Knopf (Auslagern über Standard-Lagerplatz)
-            in dem Lieferschein-Beleg hinzugefügt, der dann alle Lagerbewegungen über den Standardlagerplatz (konfigurierbar pro Ware) durchführt.
-            </para>
-          </listitem>
-          <listitem>
-            <para><option>Auslagern ohne Bestandsprüfung</option>Das obige Auslagern schlägt fehl, wenn die entsprechende Menge für
-            die Lagerbewegung nicht vorhanden ist, möchte man dies auch ignorieren und ggf. dann nachpflegen, so kann man eine Negativ-Warenmenge mit dieser Option
-            erlauben. Hierfür muss ein entsprechender Lagerplatz (Fehlbestand, o.ä.) konfiguriert sein.</para>
-          </listitem>
-       </itemizedlist>
-        Zusätzliche Funktionshinweise:
-         <itemizedlist>
-          <listitem><para><option>Standard-Lagerplatz</option>Ist dieser konfiguriert, wird dies auch als Standard-Voreinstellung bei der Neuerfassung von
-          Stammdaten-> Waren / Dienstleistung / Erzeugnis verwendet.
-          </para>
-          </listitem>
-          <listitem><para><option>Standard-Lagerplatz verwenden, falls keiner in Stammdaten definiert</option>Wird beim 'Auslagern über Standardlagerplatz'
-          keine Standardlagerplatz zu der Ware gefunden, so wird mit dieser Option einfach der Standardlagerplatz verwendet.
-          </para>
-          </listitem>
-       </itemizedlist>
-    </sect1>
   </chapter>
 
   <chapter>
@@ -5504,8 +7802,8 @@ Beschreibung: &lt;%description%&gt;
             </listitem>
 
             <listitem>
-              <para>Enthält unter anderem Listenbegrenzung vclimit,
-              Datumsformat dateformat und Nummernformat numberformat</para>
+              <para>Enthält unter anderem Datumsformat dateformat und
+              Nummernformat numberformat</para>
             </listitem>
 
             <listitem>
@@ -5610,8 +7908,10 @@ $main::lxdebug-&gt;message(0, 'Wer bin ich? Kunde oder Lieferant:' . $form-&gt;{
           Geschwindigkeitsgründen nur einmal angelegt und dann nach jedem
           Request kurz resettet.</para>
 
-          <para>Dieses Objekt kapselt auch den gerade aktiven Mandanten. Dessen Einstellungen können über
-          <literal>$::auth-&gt;client</literal> abgefragt werden; Rückgabewert ist ein Hash mit den Werten aus der Tabelle
+          <para>Dieses Objekt kapselt auch den gerade aktiven Mandanten.
+          Dessen Einstellungen können über
+          <literal>$::auth-&gt;client</literal> abgefragt werden; Rückgabewert
+          ist ein Hash mit den Werten aus der Tabelle
           <literal>auth.clients</literal>.</para>
         </sect3>
 
@@ -5897,6 +8197,89 @@ file_name = /tmp/kivitendo-debug.log</programlisting>
       </sect2>
     </sect1>
 
+    <sect1 id="dev-programmatic-api-calls" xreflabel="Programmatische API-Aufrufe">
+      <title>Programmatische API-Aufrufe</title>
+
+      <sect2 id="dev-programmatic-api-calls.introduction" xreflabel="Einführung in programmatische API-Aufrufe">
+        <title>Einführung</title>
+
+        <para>
+         Es ist möglich, Funktionen in kivitendo programmatisch aus anderen Programmen aufzurufen. Dazu ist nötig, dass
+         Authentifizierungsinformationen in jedem Aufruf mitgegeben werden. Dafür gibt es zwei Methoden: die HTTP-»Basic«-Authentifizierung
+         oder die Übergabe als spziell benannte GET-Parameter. Neben den Authentifizierungsinformationen muss auch der zu verwendende Mandant
+         übergeben werden.
+        </para>
+      </sect2>
+
+      <sect2 id="dev-programmatic-api-calls.client_selection" xreflabel="Mandantenauswahl bei programmatischen API-Aufrufen">
+        <title>Wahl des Mandanten</title>
+
+        <para>
+         Der zu verwendende Mandant kann als Parameter <varname>{AUTH}client_id</varname> mit jedem Request mitgeschickt werden. Der Wert
+         muss dabei die Datenbank-ID des Mandanten sein. kivitendo prüft, ob der Account, der über die Authentifizierungsinformationen
+         übergeben wurde, Zugriff auf den angegebenen Mandanten hat.
+        </para>
+
+        <para>
+         Wird in einem Request kein Mandant mitgegeben, so wird derjenige Mandant genommen, wer als Standardmandant markiert wurde. Gibt es
+         keinen solchen, kommt es zu einer Fehlermeldung.
+        </para>
+      </sect2>
+
+      <sect2 id="dev-programmatic-api-calls.http_basic_authentication" xreflabel="Programmatische API-Aufrufe mit HTTP-»Basic« authentifizieren">
+        <title>HTTP-»Basic«-Authentifizierung</title>
+
+        <para>
+         Für diese Methode muss jedem Request der bekannte HTTP-Header <constant>Authorization</constant> mitgeschickt werden (siehe <ulink
+         url="https://tools.ietf.org/html/rfc7617">RFC 7617</ulink>). Unterstützt wird ausschließlich die »Basic«-Methode. Loginname und
+         Passwort werden bei dieser Methode durch einen Doppelpunkt getrennt und Base64-encodiert im genannten HTTP-Header übertragen.
+        </para>
+
+        <para>
+         Diese Informationen müssen einen vorhandenen Account benennen. kivitendo prüft genau wie bei Benutzung über den Webbrowser, ob
+         dieser Account Zugriff auf den Mandanten sowie auf die angeforderte Funktion hat.
+        </para>
+
+        <para>
+         Da die Logininformationen im Klartext im Request stehen, sollte der Zugriff auf kivitendo ausschließlich über HTTPS verschlüsselt
+         erfolgen.
+        </para>
+      </sect2>
+
+      <sect2 id="dev-programmatic-api-calls.authentication_via_parameters" xreflabel="Programmatische API-Aufrufe mit Parametern authentifizieren">
+        <title>Authentifizierung mit Parametern</title>
+
+        <para>
+         Für diese Methode müssen jedem Request zwei Parameter mitgegeben werden: <varname>{AUTH}login</varname> und
+         <varname>{AUTH}password</varname>. Diese Informationen müssen einen vorhandenen Account benennen. kivitendo prüft genau wie bei
+         Benutzung über den Webbrowser, ob dieser Account Zugriff auf den Mandanten sowie auf die angeforderte Funktion hat.
+        </para>
+
+        <para>
+         Da die Logininformationen im Klartext im Request stehen, sollte der Zugriff auf kivitendo ausschließlich über HTTPS verschlüsselt
+         erfolgen.
+        </para>
+
+        <note>
+         <para>
+          Die Verwendung dieser Methode ist veraltet. Statt dessen sollte die oben erwähnte HTTP-»Basic«-Authentifizierung verwendet werden.
+         </para>
+        </note>
+      </sect2>
+
+      <sect2 id="dev-programmatic-api-calls.examples">
+        <title>Beispiele</title>
+
+        <para>
+         Das folgende Beispiel nutzt das Kommandozeilenprogramm »curl« und ruft die Funktion auf, die eine vorhandene Telefonnummer in den
+         Ansprechpersonen sucht und dazu Informationen zurückliefert. Dabei wird die HTTP-»Basic«-Authentifizierung genutzt.
+        </para>
+
+        <programlisting>$ curl --silent --user 'jdoe:SecretPassword!' \
+  'https://…/controller.pl?action=PhoneNumber/look_up&amp;number=053147110815'</programlisting>
+      </sect2>
+    </sect1>
+
     <sect1 id="db-upgrade-files" xreflabel="Datenbank-Upgradedateien">
       <title>SQL-Upgradedateien</title>
 
@@ -5904,17 +8287,25 @@ file_name = /tmp/kivitendo-debug.log</programlisting>
              xreflabel="Einführung in die Datenbank-Upgradedateien">
         <title>Einführung</title>
 
-        <para>Datenbankupgrades werden über einzelne Upgrade-Scripte gesteuert, die sich im Verzeichnis <filename>sql/Pg-upgrade2</filename>
-        befinden. In diesem Verzeichnis muss pro Datenbankupgrade eine Datei existieren, die neben den eigentlich auszuführenden SQL- oder
-        Perl-Befehlen einige Kontrollinformationen enthält.</para>
-
-        <para>Kontrollinformationen definieren Abhängigkeiten und Prioritäten, sodass Datenbankscripte zwar in einer sicheren Reihenfolge
-        ausgeführt werden (z.B. darf ein <literal>ALTER TABLE</literal> erst ausgeführt werden, wenn die Tabelle mit <literal>CREATE
-        TABLE</literal> angelegt wurde), diese Reihenfolge aber so flexibel ist, dass man keine Versionsnummern braucht.</para>
-
-        <para>kivitendo merkt sich dabei, welches der Upgradescripte in <filename>sql/Pg-upgrade2</filename> bereits durchgeführt wurde und
-        führt diese nicht erneut aus. Dazu dient die Tabelle "<literal>schema_info</literal>", die bei der Anmeldung automatisch angelegt
-        wird.</para>
+        <para>Datenbankupgrades werden über einzelne Upgrade-Scripte
+        gesteuert, die sich im Verzeichnis
+        <filename>sql/Pg-upgrade2</filename> befinden. In diesem Verzeichnis
+        muss pro Datenbankupgrade eine Datei existieren, die neben den
+        eigentlich auszuführenden SQL- oder Perl-Befehlen einige
+        Kontrollinformationen enthält.</para>
+
+        <para>Kontrollinformationen definieren Abhängigkeiten und Prioritäten,
+        sodass Datenbankscripte zwar in einer sicheren Reihenfolge ausgeführt
+        werden (z.B. darf ein <literal>ALTER TABLE</literal> erst ausgeführt
+        werden, wenn die Tabelle mit <literal>CREATE TABLE</literal> angelegt
+        wurde), diese Reihenfolge aber so flexibel ist, dass man keine
+        Versionsnummern braucht.</para>
+
+        <para>kivitendo merkt sich dabei, welches der Upgradescripte in
+        <filename>sql/Pg-upgrade2</filename> bereits durchgeführt wurde und
+        führt diese nicht erneut aus. Dazu dient die Tabelle
+        "<literal>schema_info</literal>", die bei der Anmeldung automatisch
+        angelegt wird.</para>
       </sect2>
 
       <sect2 id="db-upgrade-files.format"
@@ -5963,10 +8354,14 @@ file_name = /tmp/kivitendo-debug.log</programlisting>
             <term><varname>charset</varname></term>
 
             <listitem>
-              <para>Empfohlen. Gibt den Zeichensatz an, in dem das Script geschrieben wurde, z.B. "<literal>UTF-8</literal>". Aus
-              Kompatibilitätsgründen mit alten Upgrade-Scripten wird bei Abwesenheit des Tags für SQL-Upgradedateien der Zeichensatz
-              "<literal>ISO-8859-15</literal>" angenommen. Perl-Upgradescripte hingegen müssen immer in UTF-8 encodiert sein und sollten
-              demnach auch ein "<literal>use utf8;</literal>" enthalten.</para>
+              <para>Empfohlen. Gibt den Zeichensatz an, in dem das Script
+              geschrieben wurde, z.B. "<literal>UTF-8</literal>". Aus
+              Kompatibilitätsgründen mit alten Upgrade-Scripten wird bei
+              Abwesenheit des Tags für SQL-Upgradedateien der Zeichensatz
+              "<literal>ISO-8859-15</literal>" angenommen. Perl-Upgradescripte
+              hingegen müssen immer in UTF-8 encodiert sein und sollten
+              demnach auch ein "<literal>use utf8;</literal>"
+              enthalten.</para>
             </listitem>
           </varlistentry>
 
@@ -6034,27 +8429,36 @@ file_name = /tmp/kivitendo-debug.log</programlisting>
         </variablelist>
       </sect2>
 
-      <sect2 id="db-upgrade-files.format-perl-files" xreflabel="Format von Perl-Upgradedateien">
-       <title>Format von in Perl geschriebenen Datenbankupgradescripten</title>
+      <sect2 id="db-upgrade-files.format-perl-files"
+             xreflabel="Format von Perl-Upgradedateien">
+        <title>Format von in Perl geschriebenen
+        Datenbankupgradescripten</title>
 
-       <para>In Perl geschriebene Datenbankscripte werden nicht einfach so ausgeführt sondern müssen sich an gewisse Konventionen
-       halten. Dafür bekommen sie aber auch einige Komfortfunktionen bereitgestellt.</para>
+        <para>In Perl geschriebene Datenbankscripte werden nicht einfach so
+        ausgeführt sondern müssen sich an gewisse Konventionen halten. Dafür
+        bekommen sie aber auch einige Komfortfunktionen bereitgestellt.</para>
 
-       <para>Ein Upgradescript stellt dabei eine vollständige Objektklasse dar, die vom Elternobjekt
-       "<literal>SL::DBUpgrade2::Base</literal>" erben und eine Funktion namens "<literal>run</literal>" zur Verfügung stellen muss. Das
-       Script wird ausgeführt, indem eine Instanz dieser Klasse erzeugt und darauf die erwähnte "<literal>run</literal>" aufgerufen
-       wird.</para>
+        <para>Ein Upgradescript stellt dabei eine vollständige Objektklasse
+        dar, die vom Elternobjekt "<literal>SL::DBUpgrade2::Base</literal>"
+        erben und eine Funktion namens "<literal>run</literal>" zur Verfügung
+        stellen muss. Das Script wird ausgeführt, indem eine Instanz dieser
+        Klasse erzeugt und darauf die erwähnte "<literal>run</literal>"
+        aufgerufen wird.</para>
 
-       <para>Zu beachten ist, dass sich der Paketname der Datei aus dem Wert für "<literal>@tag</literal>" ableitet. Dabei werden alle
-       Zeichen, die in Paketnamen ungültig wären (gerade Bindestriche), durch Unterstriche ersetzt. Insgesamt sieht der Paketname wie folgt
-       aus: "<literal>SL::DBUpgrade2::tag</literal>".</para>
+        <para>Zu beachten ist, dass sich der Paketname der Datei aus dem Wert
+        für "<literal>@tag</literal>" ableitet. Dabei werden alle Zeichen, die
+        in Paketnamen ungültig wären (gerade Bindestriche), durch Unterstriche
+        ersetzt. Insgesamt sieht der Paketname wie folgt aus:
+        "<literal>SL::DBUpgrade2::tag</literal>".</para>
 
-       <para>Welche Komfortfunktionen zur Verfügung stehen, erfahren Sie in der Perl-Dokumentation zum oben genannten Modul; aufzurufen mit
-       "<command>perldoc SL/DBUpgrade2/Base.pm</command>".</para>
+        <para>Welche Komfortfunktionen zur Verfügung stehen, erfahren Sie in
+        der Perl-Dokumentation zum oben genannten Modul; aufzurufen mit
+        "<command>perldoc SL/DBUpgrade2/Base.pm</command>".</para>
 
-       <para>Ein Mindestgerüst eines gültigen Perl-Upgradescriptes sieht wie folgt aus:</para>
+        <para>Ein Mindestgerüst eines gültigen Perl-Upgradescriptes sieht wie
+        folgt aus:</para>
 
-       <programlisting># @tag: beispiel-upgrade-file42
+        <programlisting># @tag: beispiel-upgrade-file42
 # @description: Ein schönes Beispielscript
 # @depends: release_3_1_0
 package SL::DBUpgrade2::beispiel_upgrade_file42;
@@ -6170,16 +8574,14 @@ sub run {
         are built. Currently the only language fully supported is German, and
         since most of the internal messages are held in English the English
         version is usable too.</para>
-
-        <para>A stub version of French is included but not functunal at this
-        point.</para>
       </sect2>
 
       <sect2 id="translations-languages.character-set"
              xreflabel="Character set">
         <title>Character set</title>
 
-        <para>All files included in a language pack must use UTF-8 as their encoding.</para>
+        <para>All files included in a language pack must use UTF-8 as their
+        encoding.</para>
       </sect2>
 
       <sect2 id="translations-languages.file-structure"
@@ -6306,9 +8708,9 @@ Template/LaTeX
 Template/OpenDocument
 filenames</programlisting>
 
-              <para>The last of which is very machine dependant. Remember that
+              <para>The last of which is very machine dependent. Remember that
               a lot of characters are forbidden by some filesystems, for
-              exmaple MS Windows doesn't like ':' in its files where Linux
+              example MS Windows doesn't like ':' in its files where Linux
               doesn't mind that. If you want the files created with your
               language pack to be portable, find all chars that could cause
               trouble.</para>
@@ -6346,6 +8748,41 @@ filenames</programlisting>
               want to keep this safe somewhere.</para>
             </listitem>
           </varlistentry>
+
+          <varlistentry>
+            <term>more/all</term>
+
+            <listitem>
+              <para>This subdir and file is not a part of the language package
+              itself.</para>
+
+              <para>If the directory more exists and contains a file called
+              all it will be parsed in addition to the mandatory all (see
+              above). The file is useful if you want to change some
+              translations for the current installation without conflicting
+              further upgrades. The file is not autogenerated and has the same
+              format as the all, but needs another key (more_texts). See the
+              german translation for an example or copy the following code:
+              <programlisting>
+#!/usr/bin/perl
+# -*- coding: utf-8; -*-
+# vim: fenc=utf-8
+
+use utf8;
+
+# These are additional texts for custom translations.
+# The format is the same as for the normal file all, only
+# with another key (more_texts instead of texts).
+# The file has the form of 'english text'  =&gt; 'foreign text',
+
+$self-&gt;{more_texts} = {
+
+  'Ship via'                    =&gt; 'Terms of delivery',
+  'Shipping Point'              =&gt; 'Delivery time',
+}
+              </programlisting></para>
+            </listitem>
+          </varlistentry>
         </variablelist>
       </sect2>
     </sect1>
@@ -6356,138 +8793,268 @@ filenames</programlisting>
       <sect2 id="devel.testsuite.intro">
         <title>Einführung</title>
 
-        <para>kivitendo enthält eine Suite für automatisierte Tests. Sie basiert auf dem Standard-Perl-Modul <literal>Test::More</literal>.</para>
+        <para>kivitendo enthält eine Suite für automatisierte Tests. Sie
+        basiert auf dem Standard-Perl-Modul
+        <literal>Test::More</literal>.</para>
 
         <para>Die grundlegenden Fakten sind:</para>
 
         <itemizedlist>
-          <listitem><para>Alle Tests liegen im Unterverzeichnis <filename>t/</filename>.</para></listitem>
+          <listitem>
+            <para>Alle Tests liegen im Unterverzeichnis
+            <filename>t/</filename>.</para>
+          </listitem>
 
-          <listitem><para>Ein Script (bzw. ein Test) in <filename>t/</filename> enthält einen oder mehrere Testfälle.</para></listitem>
+          <listitem>
+            <para>Ein Script (bzw. ein Test) in <filename>t/</filename>
+            enthält einen oder mehrere Testfälle.</para>
+          </listitem>
 
-          <listitem><para>Alle Dateinamen von Tests enden auf <literal>.t</literal>. Es sind selbstständig ausführbare Perl-Scripte.</para></listitem>
+          <listitem>
+            <para>Alle Dateinamen von Tests enden auf <literal>.t</literal>.
+            Es sind selbstständig ausführbare Perl-Scripte.</para>
+          </listitem>
 
-          <listitem><para>Die Test-Suite besteht aus der Gesamtheit aller Tests, sprich aller Scripte in <filename>t/</filename>, deren
-          Dateiname auf <literal>.t</literal> endet.</para></listitem>
+          <listitem>
+            <para>Die Test-Suite besteht aus der Gesamtheit aller Tests,
+            sprich aller Scripte in <filename>t/</filename>, deren Dateiname
+            auf <literal>.t</literal> endet.</para>
+          </listitem>
         </itemizedlist>
       </sect2>
 
       <sect2 id="devel.testsuite.prerequisites">
         <title>Voraussetzungen</title>
 
-        <para>Für die Ausführung werden neben den für kivitendo eh schon benötigten Module noch weitere Perl-Module benötigt. Diese sind:</para>
+        <para>Für die Ausführung werden neben den für kivitendo eh schon
+        benötigten Module noch weitere Perl-Module benötigt. Diese
+        sind:</para>
 
         <itemizedlist>
-          <listitem><para><literal>Test::Deep</literal> (Debian-Paketname: <literal>libtest-deep-perl</literal>; Fedora Core:
-          <literal>perl-Test-Deep</literal>; openSUSE: <literal>perl-Test-Deep</literal>)</para></listitem>
-          <listitem><para><literal>Test::Exception</literal> (Debian-Paketname: <literal>libtest-exception-perl</literal>; Fedora Core:
-          <literal>perl-Test-Exception</literal>; openSUSE: <literal>perl-Test-Exception</literal>)</para></listitem>
-          <listitem><para><literal>Test::Output</literal> (Debian-Paketname: <literal>libtest-output-perl</literal>; Fedora Core:
-          <literal>perl-Test-Output</literal>; openSUSE: <literal>perl-Test-Output</literal>)</para></listitem>
-          <listitem><para><literal>Test::Harness</literal> 3.0.0 oder höher. Dieses Modul ist ab Perl 5.10.1 Bestandteil der
-          Perl-Distribution und kann für frühere Versionen aus dem <ulink url="http://www.cpan.org">CPAN</ulink> bezogen
-          werden.</para></listitem>
-          <listitem><para><literal>LWP::Simple</literal> aus dem Paket <literal>libwww-perl</literal> (Debian-Panetname:
-          <literal>libwww-perl</literal>; Fedora Core: <literal>perl-libwww-perl</literal>; openSUSE:
-          <literal>perl-libwww-perl</literal>)</para></listitem>
-          <listitem><para><literal>URI::Find</literal> (Debian-Panetname: <literal>liburi-find-perl</literal>; Fedora Core:
-          <literal>perl-URI-Find</literal>; openSUSE: <literal>perl-URI-Find</literal>)</para></listitem>
+          <listitem>
+            <para><literal>Test::Deep</literal> (Debian-Paketname:
+            <literal>libtest-deep-perl</literal>; Fedora:
+            <literal>perl-Test-Deep</literal>; openSUSE:
+            <literal>perl-Test-Deep</literal>)</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Test::Exception</literal> (Debian-Paketname:
+            <literal>libtest-exception-perl</literal>; Fedora:
+            <literal>perl-Test-Exception</literal>; openSUSE:
+            <literal>perl-Test-Exception</literal>)</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Test::Output</literal> (Debian-Paketname:
+            <literal>libtest-output-perl</literal>; Fedora:
+            <literal>perl-Test-Output</literal>; openSUSE:
+            <literal>perl-Test-Output</literal>)</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Test::Harness</literal> 3.0.0 oder höher. Dieses
+            Modul ist ab Perl 5.10.1 Bestandteil der Perl-Distribution und
+            kann für frühere Versionen aus dem <ulink
+            url="http://www.cpan.org">CPAN</ulink> bezogen werden.</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>LWP::Simple</literal> aus dem Paket
+            <literal>libwww-perl</literal> (Debian-Panetname:
+            <literal>libwww-perl</literal>; Fedora:
+            <literal>perl-libwww-perl</literal>; openSUSE:
+            <literal>perl-libwww-perl</literal>)</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>URI::Find</literal> (Debian-Panetname:
+            <literal>liburi-find-perl</literal>; Fedora:
+            <literal>perl-URI-Find</literal>; openSUSE:
+            <literal>perl-URI-Find</literal>)</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Sys::CPU</literal> (Debian-Panetname:
+            <literal>libsys-cpu-perl</literal>; Fedora und openSUSE: nicht
+            vorhanden)</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Thread::Pool::Simple</literal> (Debian-Panetname:
+            <literal>libthread-pool-simple-perl</literal>; Fedora und
+            openSUSE: nicht vorhanden)</para>
+          </listitem>
         </itemizedlist>
 
-        <para>Weitere Voraussetzung ist, dass die Testsuite ihre eigene Datenbank anlegen kann, um Produktivdaten nicht zu gefährden. Dazu
-        müssen in der Konfigurationsdatei im Abschnit <literal>testing/database</literal> Datenbankverbindungsparameter angegeben
-        werden. Der hier angegebene Benutzer muss weiterhin das Recht haben, Datenbanken anzulegen und zu löschen.</para>
+        <para>Weitere Voraussetzung ist, dass die Testsuite ihre eigene
+        Datenbank anlegen kann, um Produktivdaten nicht zu gefährden. Dazu
+        müssen in der Konfigurationsdatei im Abschnit
+        <literal>testing/database</literal> Datenbankverbindungsparameter
+        angegeben werden. Der hier angegebene Benutzer muss weiterhin das
+        Recht haben, Datenbanken anzulegen und zu löschen.</para>
+
+        <para>Der so angegebene Benutzer muss nicht zwingend über
+        Super-User-Rechte verfügen. Allerdings gibt es einige
+        Datenbank-Upgrades, die genau diese Rechte benötigen. Für den Fall
+        kann man in diesem Konfigurationsabschnitt einen weiteren
+        Benutzeraccount angeben, der dann über Super-User-Rechte verfügt, und
+        mit dem die betroffenen Upgrades durchgeführt werden. In der
+        Beispiel-Konfigurationsdatei finden Sie die benötigten
+        Parameter.</para>
       </sect2>
 
       <sect2 id="devel.testsuite.execution">
-        <title>
-          Existierende Tests ausführen
-        </title>
+        <title>Existierende Tests ausführen</title>
 
-        <para>Es gibt mehrere Möglichkeiten zum Ausführen der Tests: entweder, man lässt alle Tests auf einmal ausführen, oder man führt
-        gezielt einzelne Scripte aus. Für beide Fälle gibt es das Helferscript <filename>t/test.pl</filename>.</para>
+        <para>Es gibt mehrere Möglichkeiten zum Ausführen der Tests: entweder,
+        man lässt alle Tests auf einmal ausführen, oder man führt gezielt
+        einzelne Scripte aus. Für beide Fälle gibt es das Helferscript
+        <filename>t/test.pl</filename>.</para>
 
-        <para>Will man die komplette Test-Suite ausführen, so muss man einfach nur <filename>t/test.pl</filename> ohne weitere Parameter aus
-        dem kivitendo-Basisverzeichnis heraus ausführen.</para>
+        <para>Will man die komplette Test-Suite ausführen, so muss man einfach
+        nur <filename>t/test.pl</filename> ohne weitere Parameter aus dem
+        kivitendo-Basisverzeichnis heraus ausführen.</para>
 
-        <para>Um einzelne Test-Scripte auszuführen, übergibt man deren Namen an <filename>t/test.pl</filename>. Beispielsweise:</para>
+        <para>Um einzelne Test-Scripte auszuführen, übergibt man deren Namen
+        an <filename>t/test.pl</filename>. Beispielsweise:</para>
 
         <programlisting>t/test.pl t/form/format_amount.t t/background_job/known_jobs.t</programlisting>
       </sect2>
 
-
       <sect2 id="devel.testsuite.meaning_of_scripts">
-        <title>
-          Bedeutung der verschiedenen Test-Scripte
-        </title>
+        <title>Bedeutung der verschiedenen Test-Scripte</title>
 
-        <para>Die Test-Suite umfasst Tests sowohl für Funktionen als auch für Programmierstil. Einige besonders zu erwähnende, weil auch
-        während der Entwicklung nützliche Tests sind:</para>
+        <para>Die Test-Suite umfasst Tests sowohl für Funktionen als auch für
+        Programmierstil. Einige besonders zu erwähnende, weil auch während der
+        Entwicklung nützliche Tests sind:</para>
 
         <itemizedlist>
-          <listitem><para><filename>t/001compile.t</filename> -- compiliert alle Quelldateien und bricht bei Fehlern sofort ab</para></listitem>
-          <listitem><para><filename>t/002goodperl.t</filename> -- überprüft alle Perl-Dateien auf Anwesenheit von '<literal>use strict</literal>'-Anweisungen</para></listitem>
-          <listitem><para><filename>t/003safesys.t</filename> -- überprüft Aufrufe von <function>system()</function> und <function>exec()</function> auf Gültigkeit</para></listitem>
-          <listitem><para><filename>t/005no_tabs.t</filename> -- überprüft, ob Dateien Tab-Zeichen enthalten</para></listitem>
-          <listitem><para><filename>t/006spelling.t</filename> -- sucht nach häufigen Rechtschreibfehlern</para></listitem>
-          <listitem><para><filename>t/011pod.t</filename> -- überprüft die Syntax von Dokumentation im POD-Format auf Gültigkeit</para></listitem>
+          <listitem>
+            <para><filename>t/001compile.t</filename> -- compiliert alle
+            Quelldateien und bricht bei Fehlern sofort ab</para>
+          </listitem>
+
+          <listitem>
+            <para><filename>t/002goodperl.t</filename> -- überprüft alle
+            Perl-Dateien auf Anwesenheit von '<literal>use
+            strict</literal>'-Anweisungen</para>
+          </listitem>
+
+          <listitem>
+            <para><filename>t/003safesys.t</filename> -- überprüft Aufrufe von
+            <function>system()</function> und <function>exec()</function> auf
+            Gültigkeit</para>
+          </listitem>
+
+          <listitem>
+            <para><filename>t/005no_tabs.t</filename> -- überprüft, ob Dateien
+            Tab-Zeichen enthalten</para>
+          </listitem>
+
+          <listitem>
+            <para><filename>t/006spelling.t</filename> -- sucht nach häufigen
+            Rechtschreibfehlern</para>
+          </listitem>
+
+          <listitem>
+            <para><filename>t/011pod.t</filename> -- überprüft die Syntax von
+            Dokumentation im POD-Format auf Gültigkeit</para>
+          </listitem>
         </itemizedlist>
 
-        <para>Weitere Test-Scripte überprüfen primär die Funktionsweise einzelner Funktionen und Module.</para>
+        <para>Weitere Test-Scripte überprüfen primär die Funktionsweise
+        einzelner Funktionen und Module.</para>
       </sect2>
 
       <sect2 id="devel.testsuite.create_new">
-        <title>
-          Neue Test-Scripte erstellen
-        </title>
+        <title>Neue Test-Scripte erstellen</title>
 
-        <para>Es wird sehr gern gesehen, wenn neue Funktionalität auch gleich mit einem Test-Script abgesichert wird. Auch bestehende
-        Funktion darf und soll ausdrücklich nachträglich mit Test-Scripten abgesichert werden.</para>
+        <para>Es wird sehr gern gesehen, wenn neue Funktionalität auch gleich
+        mit einem Test-Script abgesichert wird. Auch bestehende Funktion darf
+        und soll ausdrücklich nachträglich mit Test-Scripten abgesichert
+        werden.</para>
 
         <sect3 id="devel.testsuite.ideas_for_non_function_tests">
-          <title>
-            Ideen für neue Test-Scripte, die keine konkreten Funktionen testen
-          </title>
+          <title>Ideen für neue Test-Scripte, die keine konkreten Funktionen
+          testen</title>
 
-          <para> Ideen, die abgesehen von Funktionen noch nicht umgesetzt wurden:</para>
+          <para>Ideen, die abgesehen von Funktionen noch nicht umgesetzt
+          wurden:</para>
 
           <itemizedlist>
-            <listitem><para>Überprüfung auf fehlende symbolische Links</para></listitem>
-            <listitem><para>Suche nach Nicht-ASCII-Zeichen in Perl-Code-Dateien (mit gewissen Einschränkungen wie das Erlauben von deutschen Umlauten)</para></listitem>
-            <listitem><para>Test auf DOS-Zeilenenden (\r\n anstelle von nur \n)</para></listitem>
-            <listitem><para>Überprüfung auf Leerzeichen am Ende von Zeilen</para></listitem>
-            <listitem><para>Test, ob alle zu übersetzenden Strings in <filename>locale/de/all</filename> vorhanden sind</para></listitem>
-            <listitem><para>Test, ob alle Webseiten-Templates in <filename>templates/webpages</filename> mit vom Perl-Modul <literal>Template</literal> compiliert werden können</para></listitem>
+            <listitem>
+              <para>Überprüfung auf fehlende symbolische Links</para>
+            </listitem>
+
+            <listitem>
+              <para>Suche nach Nicht-ASCII-Zeichen in Perl-Code-Dateien (mit
+              gewissen Einschränkungen wie das Erlauben von deutschen
+              Umlauten)</para>
+            </listitem>
+
+            <listitem>
+              <para>Test auf DOS-Zeilenenden (\r\n anstelle von nur \n)</para>
+            </listitem>
+
+            <listitem>
+              <para>Überprüfung auf Leerzeichen am Ende von Zeilen</para>
+            </listitem>
+
+            <listitem>
+              <para>Test, ob alle zu übersetzenden Strings in
+              <filename>locale/de/all</filename> vorhanden sind</para>
+            </listitem>
+
+            <listitem>
+              <para>Test, ob alle Webseiten-Templates in
+              <filename>templates/webpages</filename> mit vom Perl-Modul
+              <literal>Template</literal> compiliert werden können</para>
+            </listitem>
           </itemizedlist>
         </sect3>
 
         <sect3 id="devel.testsuite.directory_and_test_names">
-          <title>
-            Konvention für Verzeichnis- und Dateinamen
-          </title>
+          <title>Konvention für Verzeichnis- und Dateinamen</title>
 
-          <para>Es gibt momentan eine wenige Richtlinien, wie Test-Scripte zu benennen sind. Bitte die folgenden Punkte als Richtlinie betrachten und ihnen soweit es geht folgen:</para>
+          <para>Es gibt momentan eine wenige Richtlinien, wie Test-Scripte zu
+          benennen sind. Bitte die folgenden Punkte als Richtlinie betrachten
+          und ihnen soweit es geht folgen:</para>
 
           <itemizedlist>
-            <listitem><para>Die Dateiendung muss <filename>.t</filename> lauten.</para></listitem>
+            <listitem>
+              <para>Die Dateiendung muss <filename>.t</filename>
+              lauten.</para>
+            </listitem>
 
-            <listitem><para>Namen sind englisch, komplett klein geschrieben und einzelne Wörter mit Unterstrichten getrennt (beispielsweise
-            <filename>bad_function_params.t</filename>).</para></listitem>
+            <listitem>
+              <para>Namen sind englisch, komplett klein geschrieben und
+              einzelne Wörter mit Unterstrichten getrennt (beispielsweise
+              <filename>bad_function_params.t</filename>).</para>
+            </listitem>
 
-            <listitem><para>Unterverzeichnisse sollten grob nach dem Themenbereich benannt sein, mit dem sich die Scripte darin befassen
-            (beispielsweise <filename>background_jobs</filename> für Tests rund um Hintergrund-Jobs).</para></listitem>
+            <listitem>
+              <para>Unterverzeichnisse sollten grob nach dem Themenbereich
+              benannt sein, mit dem sich die Scripte darin befassen
+              (beispielsweise <filename>background_jobs</filename> für Tests
+              rund um Hintergrund-Jobs).</para>
+            </listitem>
 
-            <listitem><para>Test-Scripte sollten einen überschaubaren Bereich von Funktionalität testen, der logisch zusammenhängend ist
-            (z.B. nur Tests für eine einzelne Funktion in einem Modul). Lieber mehrere Test-Scripte schreiben.</para></listitem>
+            <listitem>
+              <para>Test-Scripte sollten einen überschaubaren Bereich von
+              Funktionalität testen, der logisch zusammenhängend ist (z.B. nur
+              Tests für eine einzelne Funktion in einem Modul). Lieber mehrere
+              Test-Scripte schreiben.</para>
+            </listitem>
           </itemizedlist>
         </sect3>
 
         <sect3 id="devel.testsuite.minimal_example">
-          <title>
-            Minimales Skelett für eigene Scripte
-          </title>
+          <title>Minimales Skelett für eigene Scripte</title>
 
-          <para>Der folgenden Programmcode enthält das kleinstmögliche Testscript und kann als Ausgangspunkt für eigene Tests verwendet werden:</para>
+          <para>Der folgenden Programmcode enthält das kleinstmögliche
+          Testscript und kann als Ausgangspunkt für eigene Tests verwendet
+          werden:</para>
 
           <programlisting>use Test::More tests =&gt; 0;
 
@@ -6497,13 +9064,18 @@ use Support::TestSetup;
 
 Support::TestSetup::login();</programlisting>
 
-          <para>Wird eine vollständig initialisierte kivitendo-Umgebung benötigt (Stichwort: alle globalen Variablen wie
-          <varname>$::auth</varname>, <varname>$::form</varname> oder <varname>$::lxdebug</varname>), so muss in der Konfigurationsdatei
-          <filename>config/kivitendo.conf</filename> im Abschnitt <literal>testing.login</literal> ein gültiger Login-Name eingetragen
+          <para>Wird eine vollständig initialisierte kivitendo-Umgebung
+          benötigt (Stichwort: alle globalen Variablen wie
+          <varname>$::auth</varname>, <varname>$::form</varname> oder
+          <varname>$::lxdebug</varname>), so muss in der Konfigurationsdatei
+          <filename>config/kivitendo.conf</filename> im Abschnitt
+          <literal>testing.login</literal> ein gültiger Login-Name eingetragen
           sein. Dieser wird für die Datenbankverbindung benötigt.</para>
 
-          <para>Wir keine vollständig initialisierte Umgebung benötigt, so kann die letzte Zeile <code>Support::TestSetup::login();</code>
-          weggelassen werden, was die Ausführungszeit des Scripts leicht verringert.</para>
+          <para>Wir keine vollständig initialisierte Umgebung benötigt, so
+          kann die letzte Zeile <programlisting>Support::TestSetup::login();</programlisting>
+          weggelassen werden, was die Ausführungszeit des Scripts leicht
+          verringert.</para>
         </sect3>
       </sect2>
     </sect1>
@@ -6631,7 +9203,7 @@ map { $form-&gt;{sum} += $form-&gt;{"row_$_"} } 1..$rowcount;</programlisting>
             </listitem>
 
             <listitem>
-              <para>Ein Spezialfall ist der ternäre Oprator "?:", der am
+              <para>Ein Spezialfall ist der ternäre Operator "?:", der am
               besten in einer übersichtlichen Tabellenstruktur organisiert
               wird. Beispiel:</para>
 
@@ -6751,7 +9323,7 @@ $some_hash{42}    = 54;</programlisting>
           <para><varname>$form</varname>, <varname>$auth</varname>,
           <varname>$locale</varname>, <varname>$lxdebug</varname> und
           <varname>%myconfig</varname> werden derzeit aus dem main package
-          importiert (siehe <xref linkend="devel.globals" />. Alle anderen
+          importiert (siehe <xref linkend="devel.globals"/>. Alle anderen
           Konstrukte sollten lexikalisch lokal gehalten werden.</para>
         </listitem>
       </orderedlist>
index 60cd5dd..1e8bca3 100644 (file)
@@ -1,6 +1,9 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>Kapitel 1. Aktuelle Hinweise</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="prev" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="next" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">Kapitel 1. Aktuelle Hinweise</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="index.html">Zurück</a>&nbsp;</td><th width="60%" align="center">&nbsp;</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02.html">Weiter</a></td></tr></table><hr></div><div class="chapter" title="Kapitel 1. Aktuelle Hinweise"><div class="titlepage"><div><div><h2 class="title"><a name="Aktuelle-Hinweise"></a>Kapitel 1. Aktuelle Hinweise</h2></div></div></div><p>Aktuelle Installations- und Konfigurationshinweise gibt es:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>im Community-Forum: <a class="ulink" href="https://forum.kivitendo.org:32443" target="_top">https://forum.kivitendo.org:32443</a>
+   <title>Kapitel 1. Aktuelle Hinweise</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="prev" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="next" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">Kapitel 1. Aktuelle Hinweise</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="index.html">Zurück</a>&nbsp;</td><th width="60%" align="center">&nbsp;</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02.html">Weiter</a></td></tr></table><hr></div><div class="chapter" title="Kapitel 1. Aktuelle Hinweise"><div class="titlepage"><div><div><h2 class="title"><a name="Aktuelle-Hinweise"></a>Kapitel 1. Aktuelle Hinweise</h2></div></div></div><p>Aktuelle Installations- und Konfigurationshinweise gibt es:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>im Community-Forum: <a class="ulink" href="https://forum.kivitendo.de" target="_top">https://forum.kivitendo.de</a>
             </p></li><li class="listitem"><p>im Kunden-Forum: <a class="ulink" href="http://redmine.kivitendo-premium.de/projects/forum/boards/" target="_top">http://redmine.kivitendo-premium.de/projects/forum/boards/</a>
-            </p></li><li class="listitem"><p>in der doc/UPGRADE Datei im doc-Verzeichnis der Installation</p></li><li class="listitem"><p>Im Schulungs- und Dienstleistungsangebot der entsprechenden kivitendo-Partner: <a class="ulink" href="http://www.kivitendo.de/partner.html" target="_top">http://www.kivitendo.de/partner.html</a>
-            </p></li></ul></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="index.html">Zurück</a>&nbsp;</td><td width="20%" align="center">&nbsp;</td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">kivitendo 3.3.0: Installation, Konfiguration, Entwicklung&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;Kapitel 2. Installation und Grundkonfiguration</td></tr></table></div></body></html>
\ No newline at end of file
+            </p></li><li class="listitem"><p>in der doc/UPGRADE Datei im doc-Verzeichnis der
+        Installation</p></li><li class="listitem"><p>Im Schulungs- und Dienstleistungsangebot der entsprechenden
+        kivitendo-Partner: <a class="ulink" href="http://www.kivitendo.de/partner.html" target="_top">http://www.kivitendo.de/partner.html</a>
+            </p></li></ul></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="index.html">Zurück</a>&nbsp;</td><td width="20%" align="center">&nbsp;</td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">kivitendo 3.6.1: Installation, Konfiguration,
+  Entwicklung&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;Kapitel 2. Installation und Grundkonfiguration</td></tr></table></div></body></html>
\ No newline at end of file
index af919f3..1c494b7 100644 (file)
@@ -1,18 +1,27 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>Kapitel 2. Installation und Grundkonfiguration</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="prev" href="ch01.html" title="Kapitel 1. Aktuelle Hinweise"><link rel="next" href="ch02s02.html" title="2.2. Benötigte Software und Pakete"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">Kapitel 2. Installation und Grundkonfiguration</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch01.html">Zurück</a>&nbsp;</td><th width="60%" align="center">&nbsp;</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s02.html">Weiter</a></td></tr></table><hr></div><div class="chapter" title="Kapitel 2. Installation und Grundkonfiguration"><div class="titlepage"><div><div><h2 class="title"><a name="config"></a>Kapitel 2. Installation und Grundkonfiguration</h2></div></div></div><div class="sect1" title="2.1. Übersicht"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Installation-%C3%9Cbersicht"></a>2.1. Übersicht</h2></div></div></div><p>
-        Die Installation von kivitendo umfasst mehrere Schritte. Die folgende Liste kann sowohl für Neulinge als auch für alte Hasen als
-        Übersicht und Stichpunktliste zum Abhaken dienen, um eine Version mit minimalen Features möglichst schnell zum Laufen zu kriegen.
-      </p><div class="orderedlist"><ol class="orderedlist" type="1"><li class="listitem"><p>
-                  <span class="emphasis"><em>Voraussetzungen überprüfen</em></span>: kivitendo benötigt gewisse Ressourcen und benutzt weitere
-        Programme. Das Kapitel "<a class="xref" href="ch02s02.html" title="2.2. Benötigte Software und Pakete">Abschnitt&nbsp;2.2, „Benötigte Software und Pakete“</a>" erläutert diese. Auch die Liste der benötigten Perl-Module
-        befindet sich hier.</p></li><li class="listitem"><p>
-                  <span class="emphasis"><em>Installation von kivitendo</em></span>: Diese umfasst die "<a class="xref" href="ch02s03.html" title="2.3. Manuelle Installation des Programmpaketes">Manuelle Installation des Programmpaketes</a>" sowie grundlegende Einstellungen, die der "<a class="xref" href="ch02s04.html" title="2.4. kivitendo-Konfigurationsdatei">Abschnitt&nbsp;2.4, „kivitendo-Konfigurationsdatei“</a>" erläutert.</p></li><li class="listitem"><p>
-                  <span class="emphasis"><em>Konfiguration externer Programme</em></span>: hierzu gehören die Datenbank ("<a class="xref" href="ch02s05.html" title="2.5. Anpassung der PostgreSQL-Konfiguration">Abschnitt&nbsp;2.5, „Anpassung der PostgreSQL-Konfiguration“</a>") und der Webserver ("<a class="xref" href="ch02s06.html" title="2.6. Webserver-Konfiguration">Abschnitt&nbsp;2.6, „Webserver-Konfiguration“</a>"). </p></li><li class="listitem"><p>
-                  <span class="emphasis"><em>Benutzerinformationen speichern können</em></span>: man benötigt mindestens eine Datenbank, in der
-        Informationen zur Authentifizierung sowie die Nutzdaten gespeichert werden. Wie man das als Administrator macht, verrät "<a class="xref" href="ch02s08.html" title="2.8. Benutzerauthentifizierung und Administratorpasswort">Abschnitt&nbsp;2.8, „Benutzerauthentifizierung und Administratorpasswort“</a>".</p></li><li class="listitem"><p>
-                  <span class="emphasis"><em>Benutzer, Gruppen und Datenbanken anlegen</em></span>: wie dies alles zusammenspielt erläutert "<a class="xref" href="ch02s09.html" title="2.9. Mandanten-, Benutzer- und Gruppenverwaltung">Abschnitt&nbsp;2.9, „Mandanten-, Benutzer- und Gruppenverwaltung“</a>".</p></li><li class="listitem"><p>
-                  <span class="emphasis"><em>Los geht's</em></span>: alles soweit erledigt? Dann kann es losgehen: "<a class="xref" href="ch02s18.html" title="2.18. kivitendo ERP verwenden">Abschnitt&nbsp;2.18, „kivitendo ERP verwenden“</a>"</p></li></ol></div><p>
-        Alle weiteren Unterkapitel in diesem Kapitel sind ebenfalls wichtig und sollten vor einer ernsthaften Inbetriebnahme gelesen
-        werden.
-      </p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch01.html">Zurück</a>&nbsp;</td><td width="20%" align="center">&nbsp;</td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s02.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">Kapitel 1. Aktuelle Hinweise&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.2. Benötigte Software und Pakete</td></tr></table></div></body></html>
\ No newline at end of file
+   <title>Kapitel 2. Installation und Grundkonfiguration</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="prev" href="ch01.html" title="Kapitel 1. Aktuelle Hinweise"><link rel="next" href="ch02s02.html" title="2.2. Benötigte Software und Pakete"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">Kapitel 2. Installation und Grundkonfiguration</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch01.html">Zurück</a>&nbsp;</td><th width="60%" align="center">&nbsp;</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s02.html">Weiter</a></td></tr></table><hr></div><div class="chapter" title="Kapitel 2. Installation und Grundkonfiguration"><div class="titlepage"><div><div><h2 class="title"><a name="config"></a>Kapitel 2. Installation und Grundkonfiguration</h2></div></div></div><div class="sect1" title="2.1. Übersicht"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Installation-%C3%9Cbersicht"></a>2.1. Übersicht</h2></div></div></div><p>Die Installation von kivitendo umfasst mehrere Schritte. Die
+      folgende Liste kann sowohl für Neulinge als auch für alte Hasen als
+      Übersicht und Stichpunktliste zum Abhaken dienen, um eine Version mit
+      minimalen Features möglichst schnell zum Laufen zu kriegen.</p><div class="orderedlist"><ol class="orderedlist" type="1"><li class="listitem"><p>
+                  <span class="emphasis"><em>Voraussetzungen überprüfen</em></span>: kivitendo
+          benötigt gewisse Ressourcen und benutzt weitere Programme. Das
+          Kapitel "<a class="xref" href="ch02s02.html" title="2.2. Benötigte Software und Pakete">Abschnitt&nbsp;2.2, „Benötigte Software und Pakete“</a>" erläutert
+          diese. Auch die Liste der benötigten Perl-Module befindet sich
+          hier.</p></li><li class="listitem"><p>
+                  <span class="emphasis"><em>Installation von kivitendo</em></span>: Diese umfasst
+          die "<a class="xref" href="ch02s03.html" title="2.3. Manuelle Installation des Programmpaketes">Manuelle Installation des Programmpaketes</a>"
+          sowie grundlegende Einstellungen, die der "<a class="xref" href="ch02s04.html" title="2.4. kivitendo-Konfigurationsdatei">Abschnitt&nbsp;2.4, „kivitendo-Konfigurationsdatei“</a>" erläutert.</p></li><li class="listitem"><p>
+                  <span class="emphasis"><em>Konfiguration externer Programme</em></span>: hierzu
+          gehören die Datenbank ("<a class="xref" href="ch02s05.html" title="2.5. Anpassung der PostgreSQL-Konfiguration">Abschnitt&nbsp;2.5, „Anpassung der PostgreSQL-Konfiguration“</a>") und der
+          Webserver ("<a class="xref" href="ch02s06.html" title="2.6. Webserver-Konfiguration">Abschnitt&nbsp;2.6, „Webserver-Konfiguration“</a>").</p></li><li class="listitem"><p>
+                  <span class="emphasis"><em>Benutzerinformationen speichern können</em></span>:
+          man benötigt mindestens eine Datenbank, in der Informationen zur
+          Authentifizierung sowie die Nutzdaten gespeichert werden. Wie man
+          das als Administrator macht, verrät "<a class="xref" href="ch02s08.html" title="2.8. Benutzerauthentifizierung und Administratorpasswort">Abschnitt&nbsp;2.8, „Benutzerauthentifizierung und Administratorpasswort“</a>".</p></li><li class="listitem"><p>
+                  <span class="emphasis"><em>Benutzer, Gruppen und Datenbanken
+          anlegen</em></span>: wie dies alles zusammenspielt erläutert "<a class="xref" href="ch02s09.html" title="2.9. Mandanten-, Benutzer- und Gruppenverwaltung">Abschnitt&nbsp;2.9, „Mandanten-, Benutzer- und Gruppenverwaltung“</a>".</p></li><li class="listitem"><p>
+                  <span class="emphasis"><em>Los geht's</em></span>: alles soweit erledigt? Dann
+          kann es losgehen: "<a class="xref" href="ch02s21.html" title="2.21. kivitendo ERP verwenden">Abschnitt&nbsp;2.21, „kivitendo ERP verwenden“</a>"</p></li></ol></div><p>Alle weiteren Unterkapitel in diesem Kapitel sind ebenfalls
+      wichtig und sollten vor einer ernsthaften Inbetriebnahme gelesen
+      werden.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch01.html">Zurück</a>&nbsp;</td><td width="20%" align="center">&nbsp;</td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s02.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">Kapitel 1. Aktuelle Hinweise&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.2. Benötigte Software und Pakete</td></tr></table></div></body></html>
\ No newline at end of file
index e10f2c8..1a0ec01 100644 (file)
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.2. Benötigte Software und Pakete</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="next" href="ch02s03.html" title="2.3. Manuelle Installation des Programmpaketes"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.2. Benötigte Software und Pakete</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s03.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.2. Benötigte Software und Pakete"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Ben%C3%B6tigte-Software-und-Pakete"></a>2.2. Benötigte Software und Pakete</h2></div></div></div><div class="sect2" title="2.2.1. Betriebssystem"><div class="titlepage"><div><div><h3 class="title"><a name="Betriebssystem"></a>2.2.1. Betriebssystem</h3></div></div></div><p>kivitendo ist für Linux konzipiert, und sollte auf jedem
+   <title>2.2. Benötigte Software und Pakete</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="next" href="ch02s03.html" title="2.3. Manuelle Installation des Programmpaketes"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.2. Benötigte Software und Pakete</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s03.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.2. Benötigte Software und Pakete"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Ben%C3%B6tigte-Software-und-Pakete"></a>2.2. Benötigte Software und Pakete</h2></div></div></div><div class="sect2" title="2.2.1. Betriebssystem"><div class="titlepage"><div><div><h3 class="title"><a name="Betriebssystem"></a>2.2.1. Betriebssystem</h3></div></div></div><p>kivitendo ist für Linux konzipiert, und sollte auf jedem
         unixoiden Betriebssystem zum Laufen zu kriegen sein. Getestet ist
         diese Version im speziellen auf Debian und Ubuntu, grundsätzlich wurde
         bei der Auswahl der Pakete aber darauf Rücksicht genommen, dass es
         ohne große Probleme auf den derzeit aktuellen verbreiteten
-        Distributionen läuft.</p><p>Anfang 2014 sind das folgende Systeme, von denen bekannt ist,
-        dass kivitendo auf ihnen läuft:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Debian</p><div class="itemizedlist"><ul class="itemizedlist" type="circle"><li class="listitem"><p>6.0 "Squeeze" (hier muss allerdings das Modul FCGI in der Version &gt;= 0.72 compiled werden, und <code class="literal">Rose::DB::Object</code> ist zu alt)</p></li><li class="listitem"><p>7.0 "Wheezy"</p></li></ul></div></li><li class="listitem"><p>Ubuntu 12.04 LTS "Precise Pangolin", 12.10 "Quantal Quetzal", 13.04 "Precise Pangolin" und 14.04 "Trusty Tahr" LTS Alpha</p></li><li class="listitem"><p>openSUSE 12.2, 12.3 und 13.1</p></li><li class="listitem"><p>SuSE Linux Enterprice Server 11</p></li><li class="listitem"><p>Fedora 16 bis 19</p></li></ul></div></div><div class="sect2" title="2.2.2. Benötigte Perl-Pakete installieren"><div class="titlepage"><div><div><h3 class="title"><a name="Pakete"></a>2.2.2. Benötigte Perl-Pakete installieren</h3></div></div></div><p>Zum Betrieb von kivitendo werden zwingend ein Webserver (meist
-        Apache) und ein Datenbankserver (PostgreSQL, mindestens v8.4)
-        benötigt.</p><p>Zusätzlich benötigt kivitendo einige Perl-Pakete, die nicht Bestandteil einer Standard-Perl-Installation sind. Um zu
-        überprüfen, ob die erforderlichen Pakete installiert und aktuell genug sind, wird ein Script mitgeliefert, das wie folgt aufgerufen
-        wird:</p><pre class="programlisting">./scripts/installation_check.pl</pre><p>Die vollständige Liste der benötigten Perl-Module lautet:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
-                     <code class="literal">parent</code> (nur bei Perl vor 5.10.1)</p></li><li class="listitem"><p>
+        Distributionen läuft.</p><p>Mitte 2020 (ab Version 3.5.6) empfehlen wir:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Debian</p><div class="itemizedlist"><ul class="itemizedlist" type="circle"><li class="listitem"><p>10.0 "Buster"</p></li><li class="listitem"><p>11.0 "Bullseye"</p></li></ul></div></li><li class="listitem"><p>20.04 "Focal Fossa" LTS
+          </p></li><li class="listitem"><p>openSUSE Leap 15.x und SUSE Linux Enterprise Server 15 GA</p></li><li class="listitem"><p>Fedora 29</p></li></ul></div></div><div class="sect2" title="2.2.2. Benötigte Perl-Pakete installieren"><div class="titlepage"><div><div><h3 class="title"><a name="Pakete"></a>2.2.2. Benötigte Perl-Pakete installieren</h3></div></div></div><p>Zum Betrieb von kivitendo werden zwingend ein Webserver (meist
+        Apache) und ein Datenbankserver (PostgreSQL) in einer aktuellen
+        Version (s.a. Liste der unterstützten Betriebssysteme)
+        benötigt.</p><p>Zusätzlich benötigt kivitendo einige Perl-Pakete, die nicht
+        Bestandteil einer Standard-Perl-Installation sind. Um zu überprüfen,
+        ob die erforderlichen Pakete installiert und aktuell genug sind, wird
+        ein Script mitgeliefert, das wie folgt aufgerufen wird:</p><pre class="programlisting">./scripts/installation_check.pl</pre><p>Die vollständige Liste der benötigten Perl-Module lautet:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
+                     <code class="literal">Algorithm::CheckDigits</code>
+                  </p></li><li class="listitem"><p>
                      <code class="literal">Archive::Zip</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">CAM::PDF</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">CGI</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">Clone</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">Config::Std</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">Daemon::Generic</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">DateTime</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">DateTime::Event::Cron</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">DateTime::Format::Strptime</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">DateTime::Set</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">DBI</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">DBD::Pg</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">Digest::SHA</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">Email::Address</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">Email::MIME</code>
                   </p></li><li class="listitem"><p>
-                     <code class="literal">FCGI</code> (nicht Versionen 0.68 bis 0.71 inklusive; siehe <a class="xref" href="ch02s06.html#Apache-Konfiguration.FCGI.WebserverUndPlugin" title="2.6.2.3. Getestete Kombinationen aus Webservern und Plugin">Abschnitt&nbsp;2.6.2.3, „Getestete Kombinationen aus Webservern und Plugin“</a>)</p></li><li class="listitem"><p>
+                     <code class="literal">Exception::Class</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">FCGI</code> (nicht Versionen 0.68 bis 0.71
+            inklusive; siehe <a class="xref" href="ch02s06.html#Apache-Konfiguration.FCGI.WebserverUndPlugin" title="2.6.2.3. Getestete Kombinationen aus Webservern und Plugin">Abschnitt&nbsp;2.6.2.3, „Getestete Kombinationen aus Webservern und Plugin“</a>)</p></li><li class="listitem"><p>
                      <code class="literal">File::Copy::Recursive</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">File::Flock</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">File::MimeInfo</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">File::Slurp</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">GD</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">HTML::Parser</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">HTML::Restrict</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">Image::Info</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">Imager</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">Imager::QRCode</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">IPC::Run</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">JSON</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">List::MoreUtils</code>
                   </p></li><li class="listitem"><p>
-                     <code class="literal">Net::SMTP::SSL</code> (optional, bei E-Mail-Versand über SSL; siehe Abschnitt "<a class="xref" href="ch02s11.html#config.sending-email.smtp" title="2.11.2. Versand über einen SMTP-Server">E-Mail-Versand über einen SMTP-Server</a>")</p></li><li class="listitem"><p>
-                     <code class="literal">Net::SSLGlue</code> (optional, bei E-Mail-Versand über TLS; siehe Abschnitt "<a class="xref" href="ch02s11.html#config.sending-email.smtp" title="2.11.2. Versand über einen SMTP-Server">E-Mail-Versand über einen SMTP-Server</a>")</p></li><li class="listitem"><p>
+                     <code class="literal">List::UtilsBy</code>
+                  </p></li><li class="listitem"><p>LWP::Authen::Digest</p></li><li class="listitem"><p>LWP::UserAgent</p></li><li class="listitem"><p>
+                     <code class="literal">Net::SMTP::SSL</code> (optional, bei
+            E-Mail-Versand über SSL; siehe Abschnitt "<a class="xref" href="ch02s11.html#config.sending-email.smtp" title="2.11.2. Versand über einen SMTP-Server">E-Mail-Versand über einen SMTP-Server</a>")</p></li><li class="listitem"><p>
+                     <code class="literal">Net::SSLGlue</code> (optional, bei
+            E-Mail-Versand über TLS; siehe Abschnitt "<a class="xref" href="ch02s11.html#config.sending-email.smtp" title="2.11.2. Versand über einen SMTP-Server">E-Mail-Versand über einen SMTP-Server</a>")</p></li><li class="listitem"><p>
+                     <code class="literal">Math::Round</code>
+                  </p></li><li class="listitem"><p>
                      <code class="literal">Params::Validate</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">PBKDF2::Tiny</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">PDF::API2</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">Regexp::IPv6</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">Rest::Client</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">Rose::Object</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">Rose::DB</code>
                   </p></li><li class="listitem"><p>
-                     <code class="literal">Rose::DB::Object</code> Version 0.788 oder neuer</p></li><li class="listitem"><p>
+                     <code class="literal">Rose::DB::Object</code> Version 0.788 oder
+            neuer</p></li><li class="listitem"><p>
+                     <code class="literal">Set::Infinite</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">String::ShellQuote</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">Sort::Naturally</code>
+                  </p></li><li class="listitem"><p>
                      <code class="literal">Template</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">Text::CSV_XS</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">Text::Iconv</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">Text::Unidecode</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">Try::Tiny</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">URI</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">XML::Writer</code>
                   </p></li><li class="listitem"><p>
-                     <code class="literal">YAML</code>
-                  </p></li></ul></div><p>Seit Version v3.2.0 sind die folgenden Pakete hinzugekommen: <code class="literal">GD</code>, <code class="literal">HTML::Restrict</code>, <code class="literal">Image::Info</code>
-            </p><p>Seit v3.0.0 sind die folgenden Pakete hinzugekommen: <code class="literal">File::Copy::Recursive</code>.</p><p>Seit v2.7.0 sind die folgenden Pakete hinzugekommen: <code class="literal">Email::MIME</code>, <code class="literal">Net::SMTP::SSL</code>,
+                     <code class="literal">XML::LibXML</code>
+                  </p></li><li class="listitem"><p>
+                     <code class="literal">YAML::XS</code> oder <code class="literal">YAML</code>
+                  </p></li></ul></div><p>Seit Version größer v3.6.0 sind die folgenden Pakete hinzugekommen: <code class="literal">IPC::Run</code>
+            </p><p>Seit Version größer v3.5.8 sind die folgenden Pakete hinzugekommen: <code class="literal">Imager</code>, <code class="literal">Imager::QRCode</code>
+
+               <code class="literal">Rest::Client</code>
+               <code class="literal">Term::ReadLine::Gnu</code>
+            </p><p>Seit Version größer v3.5.6 sind die folgenden Pakete hinzugekommen: <code class="literal">Try::Tiny</code>, <code class="literal">Math::Round</code>
+            </p><p>Seit Version größer v3.5.6 sind die folgenden Pakete hinzugekommen: <code class="literal">XML::LibXML</code>, <code class="literal">CAM::PDF</code>
+            </p><p>Seit Version größer v3.5.3 sind die folgenden Pakete hinzugekommen: <code class="literal">Exception::Class</code>
+            </p><p>Seit Version größer v3.5.1 sind die folgenden Pakete hinzugekommen: <code class="literal">Set::Infinite</code>,
+        <code class="literal">List::UtilsBy</code>, <code class="literal">DateTime::Set</code>, <code class="literal">DateTime::Event::Cron</code>
+        
+               <code class="literal">Daemon::Generic</code>, <code class="literal">DateTime::Event::Cron</code>, <code class="literal">File::Flock</code>,
+        <code class="literal">File::Slurp</code>
+            </p><p>Seit Version größer v3.5.0 sind die folgenden Pakete
+        hinzugekommen: <code class="literal">Text::Unidecode</code>,
+        <code class="literal">LWP::Authen::Digest</code>,
+        <code class="literal">LWP::UserAgent</code>
+            </p><p>Seit Version v3.4.0 sind die folgenden Pakete hinzugekommen:
+        <code class="literal">Algorithm::CheckDigits</code>,
+        <code class="literal">PBKDF2::Tiny</code>
+            </p><p>Seit Version v3.2.0 sind die folgenden Pakete hinzugekommen:
+        <code class="literal">GD</code>, <code class="literal">HTML::Restrict</code>,
+        <code class="literal">Image::Info</code>
+            </p><p>Seit v3.0.0 sind die folgenden Pakete hinzugekommen:
+        <code class="literal">File::Copy::Recursive</code>.</p><p>Seit v2.7.0 sind die folgenden Pakete hinzugekommen:
+        <code class="literal">Email::MIME</code>, <code class="literal">Net::SMTP::SSL</code>,
         <code class="literal">Net::SSLGlue</code>.</p><p>Gegenüber Version 2.6.0 sind zu dieser Liste 2 Pakete
         hinzugekommen, <code class="literal">URI</code> und
         <code class="literal">XML::Writer</code> sind notwendig. Ohne startet kivitendo
         sind auch in 2.6.1 weiterhin mit ausgeliefert, wurden in einer
         zukünftigen Version aber aus dem Paket entfernt werden. Es wird
         empfohlen diese Module zusammen mit den anderen als Bibliotheken zu
-        installieren.</p><div class="sect3" title="2.2.2.1. Debian und Ubuntu"><div class="titlepage"><div><div><h4 class="title"><a name="d0e366"></a>2.2.2.1. Debian und Ubuntu</h4></div></div></div><p>Für Debian und Ubuntu stehen die meisten der benötigten Perl-Pakete als Debian-Pakete zur Verfügung. Sie können mit folgendem Befehl installiert werden:</p><pre class="programlisting">apt-get install apache2 libarchive-zip-perl libclone-perl \
+        installieren.</p><div class="sect3" title="2.2.2.1. Debian und Ubuntu"><div class="titlepage"><div><div><h4 class="title"><a name="d0e634"></a>2.2.2.1. Debian und Ubuntu</h4></div></div></div><p>Für Debian und Ubuntu stehen die meisten der benötigten
+          Pakete als Debian-Pakete zur Verfügung. Sie können mit
+          folgendem Befehl installiert werden:</p><pre class="programlisting">apt install  apache2 libarchive-zip-perl libclone-perl \
   libconfig-std-perl libdatetime-perl libdbd-pg-perl libdbi-perl \
   libemail-address-perl  libemail-mime-perl libfcgi-perl libjson-perl \
   liblist-moreutils-perl libnet-smtp-ssl-perl libnet-sslglue-perl \
   librose-db-perl librose-object-perl libsort-naturally-perl \
   libstring-shellquote-perl libtemplate-perl libtext-csv-xs-perl \
   libtext-iconv-perl liburi-perl libxml-writer-perl libyaml-perl \
-  libimage-info-perl libgd-gd2-perl \
-  libfile-copy-recursive-perl postgresql</pre><p>Für das Paket HTML::Restrict gibt es kein Debian-Paket, dies muß per CPAN installiert werden. Unter Ubuntu funktioniert das mit:</p><pre class="programlisting">apt-get install build-essential
-cpan HTML::Restrict</pre></div><div class="sect3" title="2.2.2.2. Fedora Core"><div class="titlepage"><div><div><h4 class="title"><a name="d0e377"></a>2.2.2.2. Fedora Core</h4></div></div></div><p>Für Fedora Core stehen die meisten der benötigten Perl-Pakete als RPM-Pakete zur Verfügung. Sie können mit folgendem Befehl installiert werden:</p><pre class="programlisting">yum install httpd perl-Archive-Zip perl-Clone perl-DBD-Pg \
-  perl-DBI perl-DateTime perl-Email-Address perl-Email-MIME perl-FCGI \
-  perl-File-Copy-Recursive perl-JSON perl-List-MoreUtils perl-Net-SMTP-SSL perl-Net-SSLGlue \
-  perl-PDF-API2 perl-Params-Validate perl-Rose-DB perl-Rose-DB-Object \
+  libimage-info-perl libgd-gd2-perl libapache2-mod-fcgid \
+  libfile-copy-recursive-perl postgresql libalgorithm-checkdigits-perl \
+  libcrypt-pbkdf2-perl git libcgi-pm-perl libtext-unidecode-perl libwww-perl \
+  postgresql-contrib poppler-utils libhtml-restrict-perl \
+  libdatetime-set-perl libset-infinite-perl liblist-utilsby-perl \
+  libdaemon-generic-perl libfile-flock-perl libfile-slurp-perl \
+  libfile-mimeinfo-perl libpbkdf2-tiny-perl libregexp-ipv6-perl \
+  libdatetime-event-cron-perl libexception-class-perl libcam-pdf-perl \
+  libxml-libxml-perl libtry-tiny-perl libmath-round-perl \
+  libimager-perl libimager-qrcode-perl librest-client-perl libipc-run-perl
+          </pre><p>Sollten Pakete nicht zu Verfügung stehen, so können diese auch mittels CPAN installiert werden. Ferner muss für Ubuntu das Repository "Universe" aktiv sein (s.a. Anmerkungen).</p><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left"><a name="ubuntu-universe"></a>Anmerkung</th></tr><tr><td align="left" valign="top"><p>Die Perl Pakete für Ubuntu befinden sich im "Universe" Repository. Falls dies nicht aktiv ist, kann dies mit folgendem Aufruf aktiviert werden:
+</p><pre class="programlisting">add-apt-repository universe</pre><p>
+                  </p></td></tr></table></div></div><div class="sect3" title="2.2.2.2. Fedora"><div class="titlepage"><div><div><h4 class="title"><a name="d0e649"></a>2.2.2.2. Fedora</h4></div></div></div><p>Für Fedora stehen die meisten der benötigten Perl-Pakete als
+          RPM-Pakete zur Verfügung. Sie können mit folgendem Befehl
+          installiert werden:</p><pre class="programlisting">dnf install httpd mod_fcgid postgresql-server postgresql-contrib\
+  perl-Algorithm-CheckDigits perl-Archive-Zip perl-CPAN perl-Class-XSAccessor \
+  perl-Clone perl-Config-Std perl-DBD-Pg perl-DBI perl-Daemon-Generic \
+  perl-DateTime perl-DateTime-Set perl-Email-Address perl-Email-MIME perl-FCGI \
+  perl-File-Copy-Recursive perl-File-Flock perl-File-MimeInfo perl-File-Slurp \
+  perl-GD perl-HTML-Restrict perl-JSON perl-List-MoreUtils perl-List-UtilsBy \
+  perl-Net-SMTP-SSL perl-Net-SSLGlue perl-PBKDF2-Tiny perl-PDF-API2 \
+  perl-Params-Validate perl-Regexp-IPv6 perl-Rose-DB perl-Rose-DB-Object \
   perl-Rose-Object perl-Sort-Naturally perl-String-ShellQuote \
-  perl-Template-Toolkit perl-Text-CSV_XS perl-Text-Iconv perl-URI \
-  perl-XML-Writer perl-YAML perl-parent postgresql-server</pre><p>Zusätzlich müssen einige Pakete aus dem CPAN installiert werden. Dazu können Sie die folgenden Befehle nutzen:</p><pre class="programlisting">yum install perl-CPAN
-cpan Config::Std</pre></div><div class="sect3" title="2.2.2.3. openSUSE"><div class="titlepage"><div><div><h4 class="title"><a name="d0e388"></a>2.2.2.3. openSUSE</h4></div></div></div><p>Für openSUSE stehen die meisten der benötigten Perl-Pakete als RPM-Pakete zur Verfügung. Sie können mit folgendem Befehl
-          installiert werden:</p><pre class="programlisting">zypper install apache2 perl-Archive-Zip perl-Clone \
-  perl-Config-Std perl-DBD-Pg perl-DBI perl-DateTime perl-Email-Address \
-  perl-Email-MIME perl-FastCGI perl-File-Copy-Recursive perl-JSON perl-List-MoreUtils \
-  perl-Net-SMTP-SSL perl-Net-SSLGlue perl-PDF-API2 perl-Params-Validate \
-  perl-Sort-Naturally perl-Template-Toolkit perl-Text-CSV_XS perl-Text-Iconv \
-  perl-URI perl-XML-Writer perl-YAML postgresql-server</pre><p>Zusätzlich müssen einige Pakete aus dem CPAN installiert werden. Dazu können Sie die folgenden Befehle nutzen:</p><pre class="programlisting">yum install perl-CPAN
-cpan Rose::Db::Object</pre></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s03.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">Kapitel 2. Installation und Grundkonfiguration&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.3. Manuelle Installation des Programmpaketes</td></tr></table></div></body></html>
\ No newline at end of file
+  perl-Template-Toolkit perl-Text-CSV_XS perl-Text-Iconv perl-URI perl-XML-Writer \
+  perl-YAML perl-libwww-perl</pre></div><div class="sect3" title="2.2.2.3. openSUSE Leap 15.x und SUSE Linux Enterprise Server 15 GA"><div class="titlepage"><div><div><h4 class="title"><a name="d0e656"></a>2.2.2.3. openSUSE Leap 15.x und SUSE Linux Enterprise Server 15 GA</h4></div></div></div><p>Für openSUSE Leap 15.x stehen die meisten der benötigten Perl-Pakete als RPM-Pakete zur Verfügung.</p><p>Damit diese installiert werden können, muß das System die erforderlichen Repositories kennen und Zugriff über das Internet darauf haben.</p><p>Daher machen wir die Repositories dem System bekannt.</p><p>Um die zusätzlichen Repositories für die Installation zur Verfügung zu stellen, kann man diese mit YaST oder auch in einem Terminal auf der Konsole bekannt geben. Wir beschränken uns hier mit der Eingabe auf der Konsole. In den allermeisten Fällen verwenden die Administratoren eine sichere SSH-Verbindung zum zu administrierenden Server.</p><p>Dazu geben wir folgenden Befehl ein:</p><pre class="programlisting">zypper addrepo -f \
+  http://download.opensuse.org/repositories/devel:languages:perl/openSUSE_Leap_15.2/ \
+  "devel:languages:perl"
+          </pre><pre class="programlisting">zypper addrepo -f \
+  https://download.opensuse.org/repositories/devel:languages:haskell:lts:13/\
+  openSUSE_Leap_15.0/ "devel:languages:haskell:lts:13"
+          </pre><pre class="programlisting">zypper addrepo -f \
+  https://download.opensuse.org/repositories/devel:languages:haskell:lts:13/\
+  openSUSE_Leap_15.0/ "devel:languages:haskell:lts:13"
+          </pre><p>Danach geben wir noch die beiden folgenden Befehle ein:</p><pre class="programlisting">zypper clean</pre><pre class="programlisting">zypper refresh</pre><p>Sollte zypper eine Meldung ausgeben, ob der Repositorie Key abgelehnt, nicht vertraut oder für immer akzeptiert werden soll, ist die Beantwortung durch drücken der "i" Taste am besten geeignet. Wer noch mehr über zypper erfahren möchte, kann sich einmal die zypper Hilfe anschauen.</p><pre class="programlisting">zypper --help</pre><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left">Anmerkung</th></tr><tr><td align="left" valign="top"><p>Offiziell wird von openSUSE nur noch Versionen ab 15.2 unterstützt. Die SuSE Macher haben ab Version 15.x einen großen Umbau in der Verwaltung der Pakete vorgenommen, das heißt, der Paketumfang ist der SLES 15 als Programmunterbau angepasst. Dies gilt besonders der openSUSE Distribution. Es gibt ja einmal die openSUSE Distri und die Professionelle SLES Version. Dadurch sind viele Pakete aus dem ursprünglich nur für die openSUSE geltenen Repositorie enfernt worden, aber auch viele auf aktuellem Stand gehalten.</p></td></tr></table></div><p>Ab openSUSE Leap 15.x kann man die Distribution auch als reine Text Version, also ohne KDE Oberfläche aufsetzen. Vorteil hierbei ist, dass weniger Balast und unnötige Pakte installiert werden.</p><p>Bei openSUSE Versionen bis 15.x, also 10.x, 11.x, 12.x, 13.x hatte der Administrator die Möglichkeit, bei der Installation der Distribution die KDE Oberfläche zu aktivieren. In dieser Konstellation hat man die Möglichkeit, eine VNC Verbindung vom administrativen Client zu verwenden. Ist das nicht eingerichtet, arbeitet der Admin dann direkt am Bildschirm des Servers. Nun loggen wir uns am Server direkt ein, starten Yast2 in einer Konsole wie folgt:</p><p>yast2 return.</p><p>Oder über die Menüführung wie folgt: Ein Klick auf das runde Icon, ganz links unten in der Menüleiste, dann die Maus verfahren auf System und YaST.</p><p>Im weiteren Verlauf der Installation, beschränken wir uns mit dem Installations Werkzeug zypper. Zypper ist ein Komandozeilen basiertes Installations Tool, welches bei openSUSE Standard ist. Zypper weist ein etwas eigenartiges Verhalten auf, dass sich in etwa wie folgt darstellt. Hat man die Repositories eingerichtet, kann man diese mit Yast als auch mit Zypper benutzen, setzt man einen Befehl wie etwa: zypper up ab, so findet zypper mehr neuere Programmversionen als Yast. Ich habe im allgemeinen noch keine Nachteile damit erlebt.</p><p>Programmpakete können mit folgendem Befehl installiert werden:</p><p>zypper install Paketname</p><p>Es wird empfohlen zusätzliche Pakete nicht direkt mit CPAN zu installieren, da man diese auch über andere Repositories beziehen kann, die bei openSUSE zur Verfügung stehen. Dadurch hat man den Vorteil, dass die Pakete mit YaST verwaltet werden, also wieder deinstalliert oder durch neuere ersetzt werden können. Zudem kann man auch noch eventuelle Bugs an openSUSE senden und diese dem Maintainer melden.</p><pre class="programlisting">zypper install perl-threads-shared ghc-pdfinfo apache2-mod_fcgid \
+  yast2-http-server postgresql-server postgresql-contrib perl-Algorithm-CheckDigits \
+  perl-Archive-Zip perl-CGI perl-CGI-Ajax perl-Clone \
+  perl-Config-Std perl-Class-XSAccessor perl-Daemon-Generic perl-DateTime \
+  perl-DateTime-Event-Cron perl-DateTime-Format-Strptime perl-DateTime-Set \
+  perl-DBI perl-DBD-Pg perl-Devel-REPL perl-FastCGI perl-Email-Address \
+  perl-Email-MIME perl-Email-MIME-ContentType perl-Email-MIME-Encodings \
+  perl-FCGI perl-File-Copy-Recursive perl-File-Flock perl-File-MimeInfo \
+  perl-File-Slurp perl-GD perl-HTML-Restrict perl-Image-Info \
+  perl-JSON perl-List-MoreUtils perl-List-UtilsBy perl-Log-Log4perl perl-Net-LDAP-Server \
+  perl-Net-SSLGlue perl-Net-SMTP-SSL perl-PBKDF2-Tiny perl-PDF-API2 \
+  perl-Params-Validate perl-Regexp-IPv6 perl-Rose-DB perl-Rose-Object \
+  perl-Rose-DB-Object perl-MooseX-Role-Cmd perl-Set-Crontab perl-Set-Infinite \
+  perl-Sort-Naturally perl-String-ShellQuote perl-Sys-CPU perl-Template-Toolkit \
+  perl-Text-CSV_XS perl-Test-Deep perl-Test-Output perl-Text-Iconv \
+  perl-Text-Unidecode perl-URI perl-URI-Find perl-XML-Writer \
+  perl-YAML perl-libwww-perl
+          </pre><p>Für die Entwickler installiert man noch die folgenden Pakete:</p><pre class="programlisting">zypper install ghc-mtl-devel ghc-old-locale-devel \
+  ghc-process-extras-devel ghc-rpm-macros ghc-text-devel ghc-time-devel \
+  ghc-Cabal-devel ghc-time-locale-compat-devel perl-Log-Log4perl ghc-pdfinfo \
+  ghc-pdfinfo-devel perl-Devel-REPL perl-URI-Find perl-Class-Utils \
+  perl-Error-Pure perl-File-Object perl-Readonly perl-Test-Warnings \
+  perl-Test-NoWarnings perl-Test-Deep perl-Test-Output perl-Test-Strict \
+  perl-Test-LongString perl-File-Find-Rule
+          </pre><p>Zusätzlich müssen einige Pakete für den Umgang mit Latex installiert werden. Die Latex Module barcodes sind nützliche Helfer um auch Barcodes im Dokument zu platzieren, der Vollständigkeit halber hier für die Installation mit angegeben.
+              Dazu können Sie die folgenden Befehle nutzen:</p><pre class="programlisting">zypper install texlive-wallpaper texlive-colortbl \
+  texlive-scrlttr2copy texlive-eurosym \
+  texlive-geometry texlive-german texlive-graphbox texlive-hyperref \
+  texlive-xifthen texlive-luainputenc texlive-lastpage texlive-ltabptch \
+  texlive-nomentbl texlive-threeparttablex texlive-substr texlive-tabulary \
+  texlive-ulem texlive-wallpaper texlive-xcolor texlive-xstring \
+  texlive-xypic texlive-mwe texlive-mweights texlive-barcodes \
+  texlive-GS1 texlive-ean texlive-makebarcode texlive-pst-barcode \
+  texlive-upca
+          </pre><p>Zusätzlich müssen einige Pakete aus dem CPAN installiert
+          werden. Dazu können Sie die folgenden Befehle anwenden:</p><pre class="programlisting">cpan DateTime::event::Cron DateTime::Set FCGI \
+  HTML::Restrict PBKDF2::Tiny Rose::Db::Object Set::Infinite</pre></div></div><div class="sect2" title="2.2.3. Andere Pakete installieren"><div class="titlepage"><div><div><h3 class="title"><a name="d0e718"></a>2.2.3. Andere Pakete installieren</h3></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
+                     <code class="literal">poppler-utils</code> 'pdfinfo' zum Erkennen der Seitenanzahl bei der PDF-Generierung</p></li><li class="listitem"><p>
+                     <code class="literal">Postgres Trigram-Index</code> Für datenbankoptimierte Suchanfragen. Bspw. im Paket <code class="literal">postgresql-contrib</code> enthalten</p></li></ul></div><p>Debian und Ubuntu: </p><pre class="programlisting">apt install postgresql-contrib poppler-utils</pre><p>
+            </p><p>Fedora: </p><pre class="programlisting">dnf install poppler-utils postgresql-contrib</pre><p>
+            </p><p>openSUSE: </p><pre class="programlisting">zypper install poppler-tools</pre><p>
+            </p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s03.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">Kapitel 2. Installation und Grundkonfiguration&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.3. Manuelle Installation des Programmpaketes</td></tr></table></div></body></html>
\ No newline at end of file
index 256429b..584a17a 100644 (file)
@@ -1,13 +1,68 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.3. Manuelle Installation des Programmpaketes</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s02.html" title="2.2. Benötigte Software und Pakete"><link rel="next" href="ch02s04.html" title="2.4. kivitendo-Konfigurationsdatei"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.3. Manuelle Installation des Programmpaketes</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s02.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s04.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.3. Manuelle Installation des Programmpaketes"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Manuelle-Installation-des-Programmpaketes"></a>2.3. Manuelle Installation des Programmpaketes</h2></div></div></div><p>Der aktuelle Stable-Release, bzw. beta Release wird bei github gehostet und kann
- <a class="ulink" href="https://github.com/kivitendo/kivitendo-erp/releases" target="_top">hier</a> heruntergeladen werden.</p><p>Die kivitendo ERP Installationsdatei (<code class="filename">kivitendo-erp-3.3.0.tgz</code>) wird im Dokumentenverzeichnis des Webservers
-      (z.B.  <code class="filename">/var/www/html/</code>, <code class="filename">/srv/www/htdocs</code> oder <code class="filename">/var/www/</code>) entpackt:</p><pre class="programlisting">cd /var/www
-tar xvzf kivitendo-erp-3.3.0.tgz</pre><p>Wechseln Sie in das entpackte Verzeichnis:</p><pre class="programlisting">cd kivitendo-erp</pre><p>Alternativ können Sie auch einen Alias in der
+   <title>2.3. Manuelle Installation des Programmpaketes</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s02.html" title="2.2. Benötigte Software und Pakete"><link rel="next" href="ch02s04.html" title="2.4. kivitendo-Konfigurationsdatei"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.3. Manuelle Installation des Programmpaketes</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s02.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s04.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.3. Manuelle Installation des Programmpaketes"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Manuelle-Installation-des-Programmpaketes"></a>2.3. Manuelle Installation des Programmpaketes</h2></div></div></div><p>Der aktuelle Stable-Release, bzw. beta Release wird bei github
+      gehostet und kann <a class="ulink" href="https://github.com/kivitendo/kivitendo-erp/releases" target="_top">hier</a>
+      heruntergeladen werden.</p><p>Das aktuelleste kivitendo ERP-Archiv
+      (<code class="filename">kivitendo-erp-*.tgz</code>) wird dann im
+      Dokumentenverzeichnis des Webservers (z.B.
+      <code class="filename">/var/www/html/</code>,
+      <code class="filename">/srv/www/htdocs</code> oder
+      <code class="filename">/var/www/</code>) entpackt:</p><pre class="programlisting">cd /var/www
+tar xvzf kivitendo-erp-*.tgz</pre><p>Wechseln Sie in das entpackte Verzeichnis:</p><pre class="programlisting">cd kivitendo-erp</pre><p>Alternativ können Sie auch einen Alias in der
       Webserverkonfiguration benutzen, um auf das tatsächliche
-      Installationsverzeichnis zu verweisen.</p><p>Bei einer Neuinstallation von Version 3.1.0 oder später muß das WebDAV Verzeichnis derzeit manuell angelegt werden:</p><pre class="programlisting">mkdir webdav</pre><p>Die Verzeichnisse <code class="filename">users</code>, <code class="filename">spool</code> und <code class="filename">webdav</code> müssen für den Benutzer
-      beschreibbar sein, unter dem der Webserver läuft. Die restlichen Dateien müssen für diesen Benutzer lesbar sein. Die Benutzer- und
-      Gruppennamen sind bei verschiedenen Distributionen unterschiedlich (z.B. bei Debian/Ubuntu <code class="constant">www-data</code>, bei Fedora
-      core <code class="constant">apache</code> oder bei OpenSUSE <code class="constant">wwwrun</code>).</p><p>Der folgende Befehl ändert den Besitzer für die oben genannten
-      Verzeichnisse auf einem Debian/Ubuntu-System:</p><pre class="programlisting">chown -R www-data users spool webdav</pre><p>Weiterhin muss der Webserver-Benutzer in den Verzeichnissen <code class="filename">templates</code> und <code class="filename">users</code>
-      Unterverzeichnisse für jeden neuen Benutzer anlegen dürfen, der in kivitendo angelegt wird:</p><pre class="programlisting">chown www-data templates users</pre></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s02.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s04.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.2. Benötigte Software und Pakete&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.4. kivitendo-Konfigurationsdatei</td></tr></table></div></body></html>
\ No newline at end of file
+      Installationsverzeichnis zu verweisen.</p><p>Bei einer Neuinstallation von Version 3.1.0 oder später muß das
+      WebDAV Verzeichnis derzeit manuell angelegt werden:</p><pre class="programlisting">mkdir webdav</pre><p>Die Verzeichnisse <code class="filename">users</code>,
+      <code class="filename">spool</code> und <code class="filename">webdav</code> müssen für
+      den Benutzer beschreibbar sein, unter dem der Webserver läuft. Die
+      restlichen Dateien müssen für diesen Benutzer lesbar sein. Die Benutzer-
+      und Gruppennamen sind bei verschiedenen Distributionen unterschiedlich
+      (z.B. bei Debian/Ubuntu <code class="constant">www-data</code>, bei Fedora
+      <code class="constant">apache</code> oder bei openSUSE
+      <code class="constant">wwwrun</code>).</p><p>Der folgende Befehl ändert den Besitzer für die oben genannten
+      Verzeichnisse auf einem Debian/Ubuntu-System:</p><pre class="programlisting">chown -R www-data users spool webdav</pre><p>Weiterhin muss der Webserver-Benutzer in den Verzeichnissen
+      <code class="filename">templates</code> und <code class="filename">users</code>
+      Unterverzeichnisse für jeden neuen Benutzer anlegen dürfen, der in
+      kivitendo angelegt wird:</p><pre class="programlisting">chown www-data templates users</pre><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left">Anmerkung</th></tr><tr><td align="left" valign="top"><p>Wir empfehlen eine Installation mittels des Versionsmanagager
+        git. Hierfür muss ein git-Client installiert sein. Damit ist man sehr
+        viel flexibler für zukünftige Upgrades. Installations-Anleitung (bitte
+        die Pfade anpassen) bspw. wie folgt: </p><pre class="programlisting">cd /var/www/
+git clone https://github.com/kivitendo/kivitendo-erp.git
+cd kivitendo-erp/
+git checkout `git tag -l | egrep -ve "(alpha|beta|rc)" | tail -1`</pre><p>
+        Erläuterung: Der Befehl wechselt zur letzten Stable-Version (git tag
+        -l listet alle Tags auf, das egrep schmeisst alle Einträge mit alpha,
+        beta oder rc raus und das tail gibt davon den obersten Treffer
+        zurück). Sehr sinnvoll ist es, direkt im Anschluss einen eigenen
+        Branch zu erzeugen, um bspw. seine eigenen Druckvorlagen-Anpassungen
+        damit zu verwalten. Hierfür reicht ein simples </p><pre class="programlisting">  git checkout -b meine_eigenen_änderungen</pre><p>
+        nach dem letzten Kommando (weiterführende Informationen <a class="ulink" href="http://www-cs-students.stanford.edu/~blynn/gitmagic/index.html" target="_top">
+        Git Magic</a>).</p><p>Ein beispielhafter Workflow für Druckvorlagen-Anpassungen von
+        3.4.1 nach 3.5: </p><pre class="programlisting">
+$ git clone https://github.com/kivitendo/kivitendo-erp.git
+$ cd kivitendo-erp/
+$ git checkout release-3.4.1                # das ist ein alter release aus dem wir starten ...
+$ git checkout -b meine_eigene_änderungen   # unser lokaler branch - unabhängig von allen anderen
+$ git add templates/mein_druck              # das sind unsere druckvorlagen inkl. produktbilder
+$ git commit -m "juhu tolle änderungen"
+
+[meine_aenderungen 1d89e41] juhu tolle ändernungen
+ 4 files changed, 380 insertions(+)
+ create mode 100644 templates/mein_druck/img/webdav/tesla.png
+ create mode 100644 templates/mein_druck/mahnung.tex
+ create mode 100644 templates/mein_druck/zahlungserinnerung_zwei.tex
+ create mode 100644 templates/mein_druck/zahlungserinnerung_zwei_invoice.tex
+
+# 5 Jahre später ...
+# webserver abschalten!
+
+$ git checkout master
+$ git pull                                  # oder git fetch und danach ein stable release tag auswählen (s.o.)
+$ git checkout meine_eigenen_änderungen
+$ git rebase master
+
+Zunächst wird der Branch zurückgespult, um Ihre Änderungen
+darauf neu anzuwenden ...
+Wende an: juhu tolle änderungen
+$ service apache2 restart                   # webserver starten!
+</pre><p>
+            </p></td></tr></table></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s02.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s04.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.2. Benötigte Software und Pakete&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.4. kivitendo-Konfigurationsdatei</td></tr></table></div></body></html>
\ No newline at end of file
index 119870f..7de19f2 100644 (file)
@@ -1,25 +1,26 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.4. kivitendo-Konfigurationsdatei</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s03.html" title="2.3. Manuelle Installation des Programmpaketes"><link rel="next" href="ch02s05.html" title="2.5. Anpassung der PostgreSQL-Konfiguration"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.4. kivitendo-Konfigurationsdatei</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s03.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s05.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.4. kivitendo-Konfigurationsdatei"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="config.config-file"></a>2.4. kivitendo-Konfigurationsdatei</h2></div></div></div><div class="sect2" title="2.4.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="config.config-file.introduction"></a>2.4.1. Einführung</h3></div></div></div><p>In kivitendo gibt es nur noch eine Konfigurationsdatei,
-        die benötigt wird: <code class="filename">config/kivitendo.conf</code> (kurz:
-        "die Hauptkonfigurationsdatei"). Diese muss bei der Erstinstallation
-        von kivitendo bzw. der Migration von älteren Versionen angelegt
+   <title>2.4. kivitendo-Konfigurationsdatei</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s03.html" title="2.3. Manuelle Installation des Programmpaketes"><link rel="next" href="ch02s05.html" title="2.5. Anpassung der PostgreSQL-Konfiguration"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.4. kivitendo-Konfigurationsdatei</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s03.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s05.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.4. kivitendo-Konfigurationsdatei"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="config.config-file"></a>2.4. kivitendo-Konfigurationsdatei</h2></div></div></div><div class="sect2" title="2.4.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="config.config-file.introduction"></a>2.4.1. Einführung</h3></div></div></div><p>In kivitendo gibt es nur noch eine Konfigurationsdatei, die
+        benötigt wird: <code class="filename">config/kivitendo.conf</code> (kurz: "die
+        Hauptkonfigurationsdatei"). Diese muss bei der Erstinstallation von
+        kivitendo bzw. der Migration von älteren Versionen angelegt
         werden.</p><p>Als Vorlage dient die Datei
         <code class="filename">config/kivitendo.conf.default</code> (kurz: "die
         Default-Datei"):</p><pre class="programlisting">$ cp config/kivitendo.conf.default config/kivitendo.conf</pre><p>Die Default-Datei wird immer zuerst eingelesen. Werte, die in
         der Hauptkonfigurationsdatei stehen, überschreiben die Werte aus der
         Default-Datei. Die Hauptkonfigurationsdatei muss also nur die
         Abschnitte und Werte enthalten, die von denen der Default-Datei
-        abweichen.</p><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left">Anmerkung</th></tr><tr><td align="left" valign="top"><p>
-          Vor der Umbenennung in kivitendo hieß diese Datei noch <code class="filename">config/lx_office.conf</code>. Aus Gründen der Kompatibilität
-          wird diese Datei eingelesen, sofern die Datei <code class="filename">config/kivitendo.conf</code> nicht existiert.
-         </p></td></tr></table></div><p>Diese Hauptkonfigurationsdatei ist dann eine
+        abweichen.</p><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left">Anmerkung</th></tr><tr><td align="left" valign="top"><p>Vor der Umbenennung in kivitendo hieß diese Datei noch
+          <code class="filename">config/lx_office.conf</code>. Aus Gründen der
+          Kompatibilität wird diese Datei eingelesen, sofern die Datei
+          <code class="filename">config/kivitendo.conf</code> nicht existiert.</p></td></tr></table></div><p>Diese Hauptkonfigurationsdatei ist dann eine
         installationsspezifische Datei, d.h. sie enthält bspw. lokale
         Passwörter und wird auch nicht im Versionsmanagement (git)
         verwaltet.</p><p>Die Konfiguration ist ferner serverabhängig, d.h. für alle
         Mandaten, bzw. Datenbanken gleich.</p></div><div class="sect2" title="2.4.2. Abschnitte und Parameter"><div class="titlepage"><div><div><h3 class="title"><a name="config.config-file.sections-parameters"></a>2.4.2. Abschnitte und Parameter</h3></div></div></div><p>Die Konfigurationsdatei besteht aus mehreren Teilen, die
         entsprechend kommentiert sind:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
-                     <code class="literal">authentication</code> (siehe Abschnitt "<a class="xref" href="ch02s08.html" title="2.8. Benutzerauthentifizierung und Administratorpasswort">Abschnitt&nbsp;2.8, „Benutzerauthentifizierung und Administratorpasswort“</a>" in diesem Kapitel)</p></li><li class="listitem"><p>
+                     <code class="literal">authentication</code> (siehe Abschnitt "<a class="xref" href="ch02s08.html" title="2.8. Benutzerauthentifizierung und Administratorpasswort">Abschnitt&nbsp;2.8, „Benutzerauthentifizierung und Administratorpasswort“</a>"
+            in diesem Kapitel)</p></li><li class="listitem"><p>
                      <code class="literal">authentication/database</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">authentication/ldap</code>
@@ -57,14 +58,32 @@ host     = localhost
 port     = 5432
 db       = kivitendo_auth
 user     = postgres
-password =</pre><p>Nutzt man wiederkehrende Rechnungen, kann man unter
+password =
+
+[system]
+default_manager = german</pre><p>Für kivitendo Installationen in der Schweiz sollte hier
+        <code class="varname">german</code> durch <code class="varname">swiss</code> ersetzt
+        werden.</p><p>Die Einstellung <code class="varname">default_manager = swiss</code>
+        bewirkt:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Beim Erstellen einer neuen Datenbank in der kivitendo
+            Administration werden automatisch die Standard-Werte für die
+            Schweiz voreingestellt: Währung CHF, 5er-Rundung, Schweizer
+            KMU-Kontenplan, Sollversteuerung, Aufwandsmethode, Bilanzierung
+            (die Werte können aber manuell angepasst werden).</p></li><li class="listitem"><p>Einstellen der Standardkonten für Rundungserträge und
+            -aufwendungen (unter Mandantenkonfiguration → Standardkonten
+            veränderbar)</p></li><li class="listitem"><p>das verwendete Zahlenformat wird auf
+            <code class="varname">1'000.00</code> eingestellt (unter Programm →
+            Benutzereinstellungen veränderbar)</p></li><li class="listitem"><p>DATEV-Automatik und UStVA werden nicht angezeigt,
+            Erfolgsrechnung ersetzt GUV ( unter Mandantenkonfiguration →
+            Features veränderbar)</p></li></ul></div><p>Nutzt man wiederkehrende Rechnungen, kann man unter
         <code class="varname">[periodic_invoices]</code> den Login eines Benutzers
         angeben, der nach Erstellung der Rechnungen eine entsprechende E-Mail
-        mit Informationen über die erstellten Rechnungen bekommt.</p><p>kivitendo bringt eine eigene Komponente zur zeitgesteuerten Ausführung bestimmter Aufgaben mit, den <a class="link" href="ch02s07.html" title="2.7. Der Task-Server">Taskserver</a>. Er wird u.a. für Features wie die <a class="link" href="ch03.html#features.periodic-invoices" title="3.1. Wiederkehrende Rechnungen">wiederkehrenden Rechnungen</a> benötigt, erledigt aber auch andere erforderliche Aufgaben
-        und muss daher in Betrieb genommen werden. Der Taskserver benötigt zwei Konfigurationseinstellungen, die unter
-        <code class="varname">[task_server]</code> anzugeben sind: ein Mandant (entweder der Mandantenname oder eine Datenbank-ID, Variable
-        <code class="varname">client</code>), aus dem die Datenbankkonfiguration entnommen wird, sowie ein Login (Variable <code class="varname">login</code>)
-        eines Benutzers, der für gewisse Dinge wie die Rechnungserstellung als Verkäufer eingetragen wird.</p><p>Für Entwickler finden sich unter <code class="varname">[debug]</code>
+        mit Informationen über die erstellten Rechnungen bekommt.</p><p>kivitendo bringt eine eigene Komponente zur zeitgesteuerten
+        Ausführung bestimmter Aufgaben mit, den <a class="link" href="ch02s07.html" title="2.7. Der Task-Server">Task-Server</a>. Er wird u.a. für
+        Features wie die <a class="link" href="ch03.html#features.periodic-invoices" title="3.1. Wiederkehrende Rechnungen">wiederkehrenden Rechnungen</a>
+        benötigt, erledigt aber auch andere erforderliche Aufgaben und muss
+        daher in Betrieb genommen werden. Seine Einrichtung wird im Abschnitt
+        <a class="link" href="ch02s07.html" title="2.7. Der Task-Server">Task-Server</a> genauer
+        beschrieben.</p><p>Für Entwickler finden sich unter <code class="varname">[debug]</code>
         wichtige Funktionen, um die Fehlersuche zu erleichtern.</p></div><div class="sect2" title="2.4.3. Versionen vor 2.6.3"><div class="titlepage"><div><div><h3 class="title"><a name="config.config-file.prior-versions"></a>2.4.3. Versionen vor 2.6.3</h3></div></div></div><p>In älteren kivitendo Versionen gab es im Verzeichnis
         <code class="filename">config</code> die Dateien
         <code class="filename">authentication.pl</code> und
index 759da51..c598c47 100644 (file)
@@ -1,8 +1,17 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.5. Anpassung der PostgreSQL-Konfiguration</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s04.html" title="2.4. kivitendo-Konfigurationsdatei"><link rel="next" href="ch02s06.html" title="2.6. Webserver-Konfiguration"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.5. Anpassung der PostgreSQL-Konfiguration</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s04.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s06.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.5. Anpassung der PostgreSQL-Konfiguration"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Anpassung-der-PostgreSQL-Konfiguration"></a>2.5. Anpassung der PostgreSQL-Konfiguration</h2></div></div></div><p>PostgreSQL muss auf verschiedene Weisen angepasst werden.</p><div class="sect2" title="2.5.1. Zeichensätze/die Verwendung von Unicode/UTF-8"><div class="titlepage"><div><div><h3 class="title"><a name="Zeichens%C3%A4tze-die-Verwendung-von-UTF-8"></a>2.5.1. Zeichensätze/die Verwendung von Unicode/UTF-8</h3></div></div></div><p>kivitendo setzt zwingend voraus, dass die Datenbank Unicode/UTF-8 als Encoding einsetzt. Bei aktuellen Serverinstallationen
-             braucht man hier meist nicht einzugreifen.</p><p>Das Encoding des Datenbankservers kann überprüft werden. Ist das Encoding der Datenbank "template1" "Unicode" bzw. "UTF-8", so
-        braucht man nichts weiteres diesbezüglich unternehmen. Zum Testen:</p><pre class="programlisting">su postgres
+   <title>2.5. Anpassung der PostgreSQL-Konfiguration</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s04.html" title="2.4. kivitendo-Konfigurationsdatei"><link rel="next" href="ch02s06.html" title="2.6. Webserver-Konfiguration"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.5. Anpassung der PostgreSQL-Konfiguration</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s04.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s06.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.5. Anpassung der PostgreSQL-Konfiguration"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Anpassung-der-PostgreSQL-Konfiguration"></a>2.5. Anpassung der PostgreSQL-Konfiguration</h2></div></div></div><p>PostgreSQL muss auf verschiedene Weisen angepasst werden.</p><p>Dies variert je nach eingesetzter Distribution, da distributionsabhängig unterschiedliche Strategien beim Upgrade der Postgres Version eingesetzt werden.
+            Als Hinweis einige Links zu den drei Distribution (Stand Dezember 2018):</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
+                  <a class="ulink" href="https://fedoraproject.org/wiki/PostgreSQL" target="_top">Fedora (Postgres-Installation unter Fedora)</a>
+               </p></li><li class="listitem"><p>
+                  <a class="ulink" href="https://help.ubuntu.com/lts/serverguide/postgresql.html" target="_top">Ubuntu (Infos für Postgres für die aktuelle LTS Version)</a>
+               </p></li><li class="listitem"><p>
+                  <a class="ulink" href="https://de.opensuse.org/PostgreSQL" target="_top">OpenSuSE (aktuell nur bis Version OpenSuSE 13 verifiziert)</a>
+               </p></li></ul></div><div class="sect2" title="2.5.1. Zeichensätze/die Verwendung von Unicode/UTF-8"><div class="titlepage"><div><div><h3 class="title"><a name="Zeichens%C3%A4tze-die-Verwendung-von-UTF-8"></a>2.5.1. Zeichensätze/die Verwendung von Unicode/UTF-8</h3></div></div></div><p>kivitendo setzt zwingend voraus, dass die Datenbank
+        Unicode/UTF-8 als Encoding einsetzt. Bei aktuellen
+        Serverinstallationen braucht man hier meist nicht einzugreifen.</p><p>Das Encoding des Datenbankservers kann überprüft werden. Ist das
+        Encoding der Datenbank "template1" "Unicode" bzw. "UTF-8", so braucht
+        man nichts weiteres diesbezüglich unternehmen. Zum Testen:</p><pre class="programlisting">su postgres
 echo '\l' | psql
 exit </pre><p>Andernfalls ist es notwendig, einen neuen Datenbankcluster mit
         Unicode-Encoding anzulegen und diesen zu verwenden. Unter Debian und
@@ -21,23 +30,44 @@ exit </pre><p>Andernfalls ist es notwendig, einen neuen Datenbankcluster mit
         was mit dem Wert <code class="literal">*</code> geschieht.</p><p>In der Datei <code class="filename">pg_hba.conf</code>, die im gleichen
         Verzeichnis wie die <code class="filename">postgresql.conf</code> zu finden
         sein sollte, müssen die Berechtigungen für den Zugriff geändert
-       werden. Hier gibt es mehrere Möglichkeiten. Sinnvoll ist es nur die
-       nötigen Verbindungen immer zuzulassen, für eine lokal laufende
-       Datenbank zum Beispiel:</p><pre class="programlisting">local all kivitendo password
+        werden. Hier gibt es mehrere Möglichkeiten. Sinnvoll ist es nur die
+        nötigen Verbindungen immer zuzulassen, für eine lokal laufende
+        Datenbank zum Beispiel:</p><pre class="programlisting">local all kivitendo password
 host all kivitendo 127.0.0.1 255.255.255.255 password</pre></div><div class="sect2" title="2.5.3. Erweiterung für servergespeicherte Prozeduren"><div class="titlepage"><div><div><h3 class="title"><a name="Erweiterung-f%C3%BCr-servergespeicherte-Prozeduren"></a>2.5.3. Erweiterung für servergespeicherte Prozeduren</h3></div></div></div><p>In der Datenbank <code class="literal">template1</code> muss die
         Unterstützung für servergespeicherte Prozeduren eingerichet werden.
         Melden Sie sich dafür als Benutzer “postgres” an der Datenbank an:
         </p><pre class="programlisting">su - postgres
-psql template1</pre><p>
+psql template1</pre><p> führen Sie die folgenden Kommandos aus:</p><pre class="programlisting">CREATE EXTENSION IF NOT EXISTS plpgsql;
+\q</pre><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left">Anmerkung</th></tr><tr><td align="left" valign="top"><p>
+                  <code class="literal">CREATE EXTENSION</code> ist seit Version 9.1 die
+          bevorzugte Syntax um die Sprache <code class="literal">plpgsql</code>
+          anzulegen. In diesen Versionen ist die Extension meist auch schon
+          vorhanden. Sollten Sie eine ältere Version von Postgres haben,
+          benutzen Sie stattdessen den folgenden Befehl.</p><pre class="programlisting">CREATE LANGUAGE 'plpgsql';
+\q</pre></td></tr></table></div></div><div class="sect2" title="2.5.4. Erweiterung für Trigram Prozeduren"><div class="titlepage"><div><div><h3 class="title"><a name="Erweiterung-f%C3%BCr-trigram"></a>2.5.4. Erweiterung für Trigram Prozeduren</h3></div></div></div><p>Ab Version 3.5.1 wird die Trigram-Index-Erweiterung benötigt.
+        Diese wird mit dem SQL-Updatescript
+        sql/Pg-upgrade2/trigram_extension.sql und Datenbank-Super-Benutzer
+        Rechten automatisch installiert. Dazu braucht der
+        DatenbankSuperbenutzer "postgres" ein Passwort.</p><pre class="programlisting">su - postgres
+psql
+\password postgres
 
-        führen Sie die folgenden Kommandos aus:</p><pre class="programlisting">create language 'plpgsql';
-\q</pre></div><div class="sect2" title="2.5.4. Datenbankbenutzer anlegen"><div class="titlepage"><div><div><h3 class="title"><a name="Datenbankbenutzer-anlegen"></a>2.5.4. Datenbankbenutzer anlegen</h3></div></div></div><p>Wenn Sie nicht den Datenbanksuperuser “postgres” zum Zugriff
+Eingabe Passwort
+\q</pre><p>Benutzername Postgres und Passwort können jetzt beim Anlegen
+        einer Datenbank bzw. bei Updatescripten, die SuperuserRechte
+        benötigen, eingegeben werden.</p><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left">Anmerkung</th></tr><tr><td align="left" valign="top"><p>
+                  <code class="literal">pg_trgm</code> ist je nach Distribution nicht im
+          Standard-Paket von Postgres enthalten. Ein </p><pre class="programlisting">select * from pg_available_extensions where name ='pg_trgm';</pre><p>
+          in template1 sollte entsprechend erfolgreich sein. Andernfalls muss
+          das Paket nachinstalliert werden, bspw. bei debian/ubuntu
+          </p><pre class="programlisting">apt install postgresql-contrib</pre><p>
+               </p></td></tr></table></div></div><div class="sect2" title="2.5.5. Datenbankbenutzer anlegen"><div class="titlepage"><div><div><h3 class="title"><a name="Datenbankbenutzer-anlegen"></a>2.5.5. Datenbankbenutzer anlegen</h3></div></div></div><p>Wenn Sie nicht den Datenbanksuperuser “postgres” zum Zugriff
         benutzen wollen, so sollten Sie bei PostgreSQL einen neuen Benutzer
         anlegen. Ein Beispiel, wie Sie einen neuen Benutzer anlegen
-        können:</p><p>Die Frage, ob der neue User Superuser sein soll, können Sie mit nein
-       beantworten, genauso ist die Berechtigung neue User (Roles) zu
-       generieren nicht nötig.</p><pre class="programlisting">su - postgres
+        können:</p><p>Die Frage, ob der neue User Superuser sein soll, können Sie mit
+        nein beantworten, genauso ist die Berechtigung neue User (Roles) zu
+        generieren nicht nötig.</p><pre class="programlisting">su - postgres
 createuser -d -P kivitendo
 exit</pre><p>Wenn Sie später einen Datenbankzugriff konfigurieren, verändern
-        Sie den evtl. voreingestellten Benutzer “postgres” auf “kivitendo” bzw.
-        den hier gewählten Benutzernamen.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s04.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s06.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.4. kivitendo-Konfigurationsdatei&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.6. Webserver-Konfiguration</td></tr></table></div></body></html>
\ No newline at end of file
+        Sie den evtl. voreingestellten Benutzer “postgres” auf “kivitendo”
+        bzw. den hier gewählten Benutzernamen.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s04.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s06.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.4. kivitendo-Konfigurationsdatei&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.6. Webserver-Konfiguration</td></tr></table></div></body></html>
\ No newline at end of file
index 4715710..59be848 100644 (file)
@@ -1,6 +1,6 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.6. Webserver-Konfiguration</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s05.html" title="2.5. Anpassung der PostgreSQL-Konfiguration"><link rel="next" href="ch02s07.html" title="2.7. Der Task-Server"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.6. Webserver-Konfiguration</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s05.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s07.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.6. Webserver-Konfiguration"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Apache-Konfiguration"></a>2.6. Webserver-Konfiguration</h2></div></div></div><div class="sect2" title="2.6.1. Grundkonfiguration mittels CGI"><div class="titlepage"><div><div><h3 class="title"><a name="d0e746"></a>2.6.1. Grundkonfiguration mittels CGI</h3></div></div></div><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left">Anmerkung</th></tr><tr><td align="left" valign="top"><p>Für einen deutlichen Performanceschub sorgt die Ausführung
+   <title>2.6. Webserver-Konfiguration</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s05.html" title="2.5. Anpassung der PostgreSQL-Konfiguration"><link rel="next" href="ch02s07.html" title="2.7. Der Task-Server"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.6. Webserver-Konfiguration</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s05.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s07.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.6. Webserver-Konfiguration"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Apache-Konfiguration"></a>2.6. Webserver-Konfiguration</h2></div></div></div><div class="sect2" title="2.6.1. Grundkonfiguration mittels CGI"><div class="titlepage"><div><div><h3 class="title"><a name="d0e1192"></a>2.6.1. Grundkonfiguration mittels CGI</h3></div></div></div><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left">Anmerkung</th></tr><tr><td align="left" valign="top"><p>Für einen deutlichen Performanceschub sorgt die Ausführung
           mittels FastCGI/FCGI. Die Einrichtung wird ausführlich im Abschnitt
           <a class="xref" href="ch02s06.html#Apache-Konfiguration.FCGI" title="2.6.2. Konfiguration für FastCGI/FCGI">Konfiguration für FastCGI/FCGI</a> beschrieben.</p></td></tr></table></div><p>Der Zugriff auf das Programmverzeichnis muss in der Apache
         Webserverkonfigurationsdatei <code class="literal">httpd.conf</code> eingestellt
@@ -15,13 +15,12 @@ Alias /kivitendo-erp/ /var/www/kivitendo-erp/
 &lt;/Directory&gt;
 
 &lt;Directory /var/www/kivitendo-erp/users&gt;
- Order Deny,Allow
- Deny from All
+ Require all granted
 &lt;/Directory&gt;</pre><p>Ersetzen Sie dabei die Pfade durch diejenigen, in die Sie vorher
         das kivitendo-Archiv entpacket haben.</p><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left">Anmerkung</th></tr><tr><td align="left" valign="top"><p>Vor den einzelnen Optionen muss bei einigen Distributionen ein
-          Plus ‘<code class="literal">+</code>’ gesetzt werden.</p><p>Bei einigen Distribution (Ubuntu ab 14.04, Debian ab 8.2) muss noch explizit
-          das cgi-Modul mittels </p><pre class="programlisting">a2enmod cgi</pre><p> aktiviert
-          werden.</p></td></tr></table></div><p>Auf einigen Webservern werden manchmal die Grafiken und
+          Plus ‘<code class="literal">+</code>’ gesetzt werden.</p><p>Bei einigen Distribution (Ubuntu ab 14.04, Debian ab 8.2) muss
+          noch explizit das cgi-Modul mittels </p><pre class="programlisting">a2enmod cgi</pre><p>
+          aktiviert werden.</p></td></tr></table></div><p>Auf einigen Webservern werden manchmal die Grafiken und
         Style-Sheets nicht ausgeliefert. In solchen Fällen hat es oft
         geholfen, die folgende Option in die Konfiguration aufzunehmen:</p><pre class="programlisting">EnableSendfile Off</pre></div><div class="sect2" title="2.6.2. Konfiguration für FastCGI/FCGI"><div class="titlepage"><div><div><h3 class="title"><a name="Apache-Konfiguration.FCGI"></a>2.6.2. Konfiguration für FastCGI/FCGI</h3></div></div></div><div class="sect3" title="2.6.2.1. Was ist FastCGI?"><div class="titlepage"><div><div><h4 class="title"><a name="Apache-Konfiguration.FCGI.WasIstEs"></a>2.6.2.1. Was ist FastCGI?</h4></div></div></div><p>Direkt aus <a class="ulink" href="http://de.wikipedia.org/wiki/FastCGI" target="_top">Wikipedia</a>
           kopiert:</p><p>
@@ -42,13 +41,13 @@ Alias /kivitendo-erp/ /var/www/kivitendo-erp/
           führt dazu dass ein kivitendo Aufruf der Kernmasken mittlerweile
           deutlich länger dauert als früher, und dass davon 90% für das Laden
           der Module verwendet wird.</p><p>Mit FastCGI werden nun die Module einmal geladen, und danach
-          wird nur die eigentliche Programmlogik ausgeführt.</p></div><div class="sect3" title="2.6.2.3. Getestete Kombinationen aus Webservern und Plugin"><div class="titlepage"><div><div><h4 class="title"><a name="Apache-Konfiguration.FCGI.WebserverUndPlugin"></a>2.6.2.3. Getestete Kombinationen aus Webservern und Plugin</h4></div></div></div><p>Folgende Kombinationen sind getestet:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Apache 2.2.11 (Ubuntu) und mod_fcgid.</p></li><li class="listitem"><p>Apache 2.2.11 / 2.2.22 (Ubuntu) und mod_fastcgi.</p></li><li class="listitem"><p>Apache 2.4.7 (Ubuntu 14.04.2 LTS) und mod_fcgid.</p></li></ul></div><p>Dabei wird mod_fcgid empfohlen, weil mod_fastcgi seit geraumer
-          Zeit nicht mehr weiter entwickelt wird. Im Folgenden wird auf
-          mod_fastcgi nicht mehr explizit eingegangen.</p><p>Als Perl Backend wird das Modul <code class="filename">FCGI.pm</code>
-          verwendet.</p><div class="warning" title="Warnung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Warning"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Warnung]" src="system/docbook-xsl/images/warning.png"></td><th align="left">Warnung</th></tr><tr><td align="left" valign="top"><p>FCGI-Versionen ab 0.69 und bis zu 0.71 inklusive sind extrem strict in der Behandlung von Unicode, und verweigern
-            bestimmte Eingaben von kivitendo. Falls es Probleme mit Umlauten in Ihrer Installation gibt, muss zwingend Version 0.68 oder
-            aber Version 0.72 und neuer eingesetzt werden.</p><p>Mit <a class="ulink" href="http://www.cpan.org" target="_top">CPAN</a> lässt sie sich die Vorgängerversion wie folgt
-            installieren:</p><pre class="programlisting">force install M/MS/MSTROUT/FCGI-0.68.tar.gz</pre></td></tr></table></div></div><div class="sect3" title="2.6.2.4. Konfiguration des Webservers"><div class="titlepage"><div><div><h4 class="title"><a name="Apache-Konfiguration.FCGI.Konfiguration"></a>2.6.2.4. Konfiguration des Webservers</h4></div></div></div><p>Bevor Sie versuchen, eine kivitendo Installation unter FCGI
+          wird nur die eigentliche Programmlogik ausgeführt.</p></div><div class="sect3" title="2.6.2.3. Getestete Kombinationen aus Webservern und Plugin"><div class="titlepage"><div><div><h4 class="title"><a name="Apache-Konfiguration.FCGI.WebserverUndPlugin"></a>2.6.2.3. Getestete Kombinationen aus Webservern und Plugin</h4></div></div></div><p>Folgende Kombinationen sind getestet:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Apache 2.4.7 (Ubuntu 14.04.2 LTS) und mod_fcgid.</p></li><li class="listitem"><p>Apache 2.4.18 (Ubuntu 16.04 LTS) und mod_fcgid</p></li><li class="listitem"><p>Apache 2.4.29 (Ubuntu 18.04 LTS) und mod_fcgid</p></li><li class="listitem"><p>Apache 2.4.41 (Ubuntu 20.04 LTS) und mod_fcgid</p></li></ul></div><p>Als Perl Backend wird das Modul <code class="filename">FCGI.pm</code>
+          verwendet.</p><div class="warning" title="Warnung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Warning"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Warnung]" src="system/docbook-xsl/images/warning.png"></td><th align="left">Warnung</th></tr><tr><td align="left" valign="top"><p>FCGI-Versionen ab 0.69 und bis zu 0.71 inklusive sind extrem
+            strict in der Behandlung von Unicode, und verweigern bestimmte
+            Eingaben von kivitendo. Falls es Probleme mit Umlauten in Ihrer
+            Installation gibt, muss zwingend Version 0.68 oder aber Version
+            0.72 und neuer eingesetzt werden.</p><p>Mit <a class="ulink" href="http://www.cpan.org" target="_top">CPAN</a> lässt sie
+            sich die Vorgängerversion wie folgt installieren:</p><pre class="programlisting">force install M/MS/MSTROUT/FCGI-0.68.tar.gz</pre></td></tr></table></div></div><div class="sect3" title="2.6.2.4. Konfiguration des Webservers"><div class="titlepage"><div><div><h4 class="title"><a name="Apache-Konfiguration.FCGI.Konfiguration"></a>2.6.2.4. Konfiguration des Webservers</h4></div></div></div><p>Bevor Sie versuchen, eine kivitendo Installation unter FCGI
           laufen zu lassen, empfiehlt es sich die Installation ersteinmal
           unter CGI aufzusetzen. FCGI macht es nicht einfach Fehler zu
           debuggen die beim ersten aufsetzen auftreten können. Sollte die
@@ -66,16 +65,18 @@ Alias       /url/for/kivitendo-erp/          /path/to/kivitendo-erp/
 &lt;Directory /path/to/kivitendo-erp&gt;
   AllowOverride All
   Options ExecCGI Includes FollowSymlinks
-  Order Allow,Deny
-  Allow from All
+  Require all granted
 &lt;/Directory&gt;
 
 &lt;DirectoryMatch /path/to/kivitendo-erp/users&gt;
-  Order Deny,Allow
-  Deny from All
-&lt;/DirectoryMatch&gt;</pre><div class="warning" title="Warnung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Warning"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Warnung]" src="system/docbook-xsl/images/warning.png"></td><th align="left">Warnung</th></tr><tr><td align="left" valign="top"><p>Im Vergleich zu Apache 2.2 hat sich in Apache 2.4 die Syntax der Directorydirektiven verändert. Statt</p><pre class="programlisting">
+Require all denied
+&lt;/DirectoryMatch&gt;</pre><div class="warning" title="Warnung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Warning"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Warnung]" src="system/docbook-xsl/images/warning.png"></td><th align="left">Warnung</th></tr><tr><td align="left" valign="top"><p>Wer einen älteren Apache als Version 2.4 im Einsatz hat,
+            muss entsprechend die Syntax der Directorydirektiven verändert.
+            Statt</p><pre class="programlisting">Require all granted</pre><p>muß man Folgendes einstellen:</p><pre class="programlisting">
   Order Allow,Deny
-  Allow from All </pre><p> muß man jetzt Folgendes einstellen:</p><pre class="programlisting">Require all granted</pre></td></tr></table></div><p>Seit mod_fcgid-Version 2.3.6 gelten sehr kleine Grenzen für
+  Allow from All </pre><p>und statt</p><pre class="programlisting">Require all denied</pre><p>muss stehen:</p><pre class="programlisting">
+  Order Deny,Allow
+  Deny from All </pre></td></tr></table></div><p>Seit mod_fcgid-Version 2.3.6 gelten sehr kleine Grenzen für
           die maximale Größe eines Requests. Diese sollte wie folgt
           hochgesetzt werden:</p><pre class="programlisting">FcgidMaxRequestLen 10485760</pre><p>Das Ganze sollte dann so aussehen:</p><pre class="programlisting">AddHandler fcgid-script .fpl
 AliasMatch ^/url/for/kivitendo-erp/[^/]+\.pl /path/to/kivitendo-erp/dispatcher.fpl
@@ -85,13 +86,11 @@ FcgidMaxRequestLen 10485760
 &lt;Directory /path/to/kivitendo-erp&gt;
   AllowOverride All
   Options ExecCGI Includes FollowSymlinks
-  Order Allow,Deny
-  Allow from All
+  Require all granted
 &lt;/Directory&gt;
 
 &lt;DirectoryMatch /path/to/kivitendo-erp/users&gt;
-  Order Deny,Allow
-  Deny from All
+Require all denied
 &lt;/DirectoryMatch&gt;</pre><p>Hierdurch wird nur ein zentraler Dispatcher gestartet. Alle
           Zugriffe auf die einzelnen Scripte werden auf diesen umgeleitet.
           Dadurch, dass zur Laufzeit öfter mal Scripte neu geladen werden,
@@ -105,9 +104,37 @@ AliasMatch ^/url/for/kivitendo-erp-fcgid/[^/]+\.pl /path/to/kivitendo-erp/dispat
 Alias       /url/for/kivitendo-erp-fcgid/          /path/to/kivitendo-erp/</pre><p>Dann ist unter <code class="filename">/url/for/kivitendo-erp/</code>
           die normale Version erreichbar, und unter
           <code class="constant">/url/for/kivitendo-erp-fcgid/</code> die
-          FastCGI-Version.</p></div></div><div class="sect2" title="2.6.3. Weitergehende Konfiguration"><div class="titlepage"><div><div><h3 class="title"><a name="d0e891"></a>2.6.3. Weitergehende Konfiguration</h3></div></div></div><p>Für einen deutlichen Sicherheitsmehrwert sorgt die Ausführung von kivitendo
-          nur über https-verschlüsselten Verbindungen, sowie weiteren Zusatzmassnahmen,
-          wie beispielsweise Basic Authenticate.
-          Die Konfigurationsmöglichkeiten sprengen allerdings den Rahmen dieser Anleitung, hier ein
-          Hinweis auf einen entsprechenden <a class="ulink" href="http://redmine.kivitendo-premium.de/boards/1/topics/142" target="_top">Foreneintrag (Stand Sept. 2015)</a>
-            </p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s05.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s07.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.5. Anpassung der PostgreSQL-Konfiguration&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.7. Der Task-Server</td></tr></table></div></body></html>
\ No newline at end of file
+          FastCGI-Version.</p></div></div><div class="sect2" title="2.6.3. Authentifizierung mittels HTTP Basic Authentication"><div class="titlepage"><div><div><h3 class="title"><a name="d0e1346"></a>2.6.3. Authentifizierung mittels HTTP Basic Authentication</h3></div></div></div><p>
+        Kivitendo unterstützt, dass Benutzerauthentifizierung über den Webserver mittels des »Basic«-HTTP-Authentifizierungs-Schema erfolgt
+        (siehe <a class="ulink" href="https://tools.ietf.org/html/rfc7617" target="_top">RFC 7617</a>). Dazu ist es aber nötig, dass der dabei vom Client
+        mitgeschickte Header <code class="constant">Authorization</code> vom Webserver an Kivitendo über die Umgebungsvariable
+        <code class="constant">HTTP_AUTHORIZATION</code> weitergegeben wird, was standardmäßig nicht der Fall ist. Für Apache kann dies über die
+        folgende Konfigurationsoption aktiviert werden:
+       </p><pre class="programlisting">SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1</pre></div><div class="sect2" title="2.6.4. Aktivierung von mod_rewrite/directory_match für git basierte Installationen"><div class="titlepage"><div><div><h3 class="title"><a name="d0e1362"></a>2.6.4. Aktivierung von mod_rewrite/directory_match für git basierte Installationen</h3></div></div></div><p>
+        Aufgrund von aktuellen (Mitte 2020) Sicherheitswarnungen für git basierte Webanwendungen ist die mitausgelieferte .htaccess
+        restriktiver geworden und verhindert somit das Auslesen von git basierten Daten.
+        Für debian/ubuntu muss das Modul mod_rewrite einmalig so aktiviert werden:
+        </p><pre class="programlisting">a2enmod rewrite</pre><p>
+        Alternativ und für Installationen ohne Apache ist folgender Artikel interessant:
+        <a class="ulink" href="https://www.cyberscan.io/blog/git-luecke" target="_top">git-lücke</a>.
+        Anstelle des dort beschriebenen DirectoryMatch für Apache2 würden wir etwas weitergehend auch noch das Verzeichnis config miteinbeziehen
+        sowie ferner auch die Möglichkeit nicht ausschließen, dass es in Unterverzeichnissen auch noch .git Repositories geben kann.
+        Die Empfehlung für Apache 2.4 wäre damit:
+        </p><pre class="programlisting">
+        &lt;DirectoryMatch "/(\.git|config)/"&gt;
+          Require all denied
+        &lt;/DirectoryMatch&gt;</pre><p>
+       
+            </p></div><div class="sect2" title="2.6.5. Weitergehende Konfiguration"><div class="titlepage"><div><div><h3 class="title"><a name="d0e1376"></a>2.6.5. Weitergehende Konfiguration</h3></div></div></div><p>Für einen deutlichen Sicherheitsmehrwert sorgt die Ausführung
+        von kivitendo nur über https-verschlüsselten Verbindungen, sowie
+        weiteren Zusatzmassnahmen, wie beispielsweise Basic Authenticate. Die
+        Konfigurationsmöglichkeiten sprengen allerdings den Rahmen dieser
+        Anleitung, hier ein Hinweis auf einen entsprechenden <a class="ulink" href="http://redmine.kivitendo-premium.de/boards/1/topics/142" target="_top">Foreneintrag
+        (Stand Sept. 2015)</a> und einen aktuellen (Stand Mai 2017) <a class="ulink" href="https://mozilla.github.io/server-side-tls/ssl-config-generator/" target="_top">
+        SSL-Konfigurations-Generator</a>.</p></div><div class="sect2" title="2.6.6. Aktivierung von Apache2 modsecurity"><div class="titlepage"><div><div><h3 class="title"><a name="d0e1387"></a>2.6.6. Aktivierung von Apache2 modsecurity</h3></div></div></div><p>Aufgrund des OpenSource Charakters ist kivitendo nicht "out of the box" sicher.
+  Organisatorisch empfehlen wir hier die enge Zusammenarbeit mit einem kivitendo Partner der auch in der
+Lage ist weiterführende Fragen in Bezug auf Datenschutz und Datensicherheit zu beantworten.
+Unabhängig davon empfehlen wir im Webserver Bereich die Aktivierung und Konfiguration des Moduls modsecurity für den Apache2, damit
+XSS und SQL-Injections verhindert werden.</p><p> Als Idee hierfür sei dieser Blog-Eintrag genannt:
+<a class="ulink" href="https://doxsec.wordpress.com/2017/06/11/using-modsecurity-web-application-firewall-to-prevent-sql-injection-and-xss-using-blocking-rules/" target="_top">
+        Test Apache2 modsecurity for SQL Injection</a>.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s05.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s07.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.5. Anpassung der PostgreSQL-Konfiguration&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.7. Der Task-Server</td></tr></table></div></body></html>
\ No newline at end of file
index faa7472..39312f8 100644 (file)
@@ -1,53 +1,78 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.7. Der Task-Server</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s06.html" title="2.6. Webserver-Konfiguration"><link rel="next" href="ch02s08.html" title="2.8. Benutzerauthentifizierung und Administratorpasswort"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.7. Der Task-Server</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s06.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s08.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.7. Der Task-Server"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="config.task-server"></a>2.7. Der Task-Server</h2></div></div></div><p>Der Task-Server ist ein Prozess, der im Hintergrund läuft, in regelmäßigen Abständen nach abzuarbeitenden Aufgaben sucht und
-      diese zu festgelegten Zeitpunkten abarbeitet (ähnlich wie Cron). Dieser Prozess wird u.a. für die Erzeugung der wiederkehrenden
-      Rechnungen und weitere essenzielle Aufgaben benutzt.</p><div class="sect2" title="2.7.1. Verfügbare und notwendige Konfigurationsoptionen"><div class="titlepage"><div><div><h3 class="title"><a name="Konfiguration-des-Task-Servers"></a>2.7.1. Verfügbare und notwendige Konfigurationsoptionen</h3></div></div></div><p>Die Konfiguration erfolgt über den Abschnitt
+   <title>2.7. Der Task-Server</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s06.html" title="2.6. Webserver-Konfiguration"><link rel="next" href="ch02s08.html" title="2.8. Benutzerauthentifizierung und Administratorpasswort"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.7. Der Task-Server</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s06.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s08.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.7. Der Task-Server"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="config.task-server"></a>2.7. Der Task-Server</h2></div></div></div><p>Der Task-Server ist ein Prozess, der im Hintergrund läuft, in
+      regelmäßigen Abständen nach abzuarbeitenden Aufgaben sucht und diese zu
+      festgelegten Zeitpunkten abarbeitet (ähnlich wie Cron). Dieser Prozess
+      wird u.a. für die Erzeugung der wiederkehrenden Rechnungen und weitere
+      essenzielle Aufgaben benutzt.</p><p>Der Task-Server muss einmalig global in der Konfigurationsdatei
+      konfiguriert werden. Danach wird er für jeden Mandanten, für den er
+      laufen soll, in der Adminsitrationsmaske eingeschaltet.</p><p>Beachten Sie, dass der Task-Server in den Boot-Vorgang Ihres
+      Servers integriert werden muss, damit er automatisch gestartet wird.
+      Dies kann kivitendo nicht für Sie erledigen.</p><p>Da der Task-Server als Perlscript läuft, wird Arbeitsspeicher, der
+      einmal benötigt wurde, nicht mehr an das Betriebssystem zurückgegeben,
+      solange der Task-Server läuft. Dies kann dazu führen, dass ein länger
+      laufender Task-Server mit der Zeit immer mehr Arbeitsspeicher für sich
+      beansprucht. Es ist deshalb sinnvoll, dass der Task-Server in
+      regelmässigen Abständen neu gestartet wird. Allerdings berücksichtigt der
+      Task-Server ein Memory-Limit, wenn dieses in der Konfigurationsdatei
+      angegeben ist. Bei Überschreiten dieses Limits beendet sich der
+      Task-Server. Sofern der Task-Server als systemd-Service mit dem
+      mitgelieferten Skript eingerichtet wurde, startet dieser danach
+      automatisch erneut.</p><div class="sect2" title="2.7.1. Verfügbare und notwendige Konfigurationsoptionen"><div class="titlepage"><div><div><h3 class="title"><a name="Konfiguration-des-Task-Servers"></a>2.7.1. Verfügbare und notwendige Konfigurationsoptionen</h3></div></div></div><p>Die Konfiguration erfolgt über den Abschnitt
         <code class="literal">[task_server]</code> in der Datei
         <code class="filename">config/kivitendo.conf</code>. Die dort verfügbaren
         Optionen sind:</p><div class="variablelist"><dl><dt><span class="term">
-                     <code class="varname">client</code>
-                  </span></dt><dd><p>Name oder Datenbank-ID eines vorhandenen kivitendo-Mandanten, der benutzt wird, um die zu verwendende
-              Datenbankverbindung auszulesen. Der Mandant muss in der Administration angelegt werden. Diese Option muss angegeben
-              werden.</p><p>Diese Option kam mit Release v3.x.0 hinzu und muss daher in Konfigurationen, die von älteren Versionen aktualisiert
-              wurden, ergänzt werden.</p></dd><dt><span class="term">
-                     <code class="varname">login</code>
-                  </span></dt><dd><p>gültiger kivitendo-Benutzername, der z.B. als Verkäufer beim Erzeugen wiederkehrender Rechnungen benötigt wird. Der
-              Benutzer muss in der Administration angelegt werden. Diese Option muss angegeben werden.</p></dd><dt><span class="term">
                      <code class="varname">run_as</code>
                   </span></dt><dd><p>Wird der Server vom Systembenutzer <code class="literal">root</code>
               gestartet, so wechselt er auf den mit <code class="literal">run_as</code>
               angegebenen Systembenutzer. Der Systembenutzer muss dieselben
               Lese- und Schreibrechte haben, wie auch der Webserverbenutzer
               (siehe see <a class="xref" href="ch02s03.html" title="2.3. Manuelle Installation des Programmpaketes">Manuelle Installation des Programmpaketes</a>). Daher
-              ist es sinnvoll, hier denselben Systembenutzer einzutragen,
+              ist es erforderlich, hier denselben Systembenutzer einzutragen,
               unter dem auch der Webserver läuft.</p></dd><dt><span class="term">
                      <code class="varname">debug</code>
-                  </span></dt><dd><p>Schaltet Debug-Informationen an und aus.</p></dd></dl></div></div><div class="sect2" title="2.7.2. Automatisches Starten des Task-Servers beim Booten"><div class="titlepage"><div><div><h3 class="title"><a name="Einbinden-in-den-Boot-Prozess"></a>2.7.2. Automatisches Starten des Task-Servers beim Booten</h3></div></div></div><p>Der Task-Server verhält sich von seinen Optionen her wie ein
+                  </span></dt><dd><p>Schaltet Debug-Informationen an und aus.</p></dd></dl></div></div><div class="sect2" title="2.7.2. Konfiguration der Mandanten für den Task-Server"><div class="titlepage"><div><div><h3 class="title"><a name="Konfiguration-der-Mandanten-fuer-den-Task-Servers"></a>2.7.2. Konfiguration der Mandanten für den Task-Server</h3></div></div></div><p>Ist der Task-Server grundlegend konfiguriert, so muss
+        anschließend jeder Mandant, für den der Task-Server laufen soll,
+        einmalig konfiguriert werden. Dazu kann in der Maske zum Bearbeiten
+        von Mandanten im Administrationsbereich eine kivitendo-Benutzerkennung
+        ausgewählt werden, unter der der Task-Server seine Arbeit
+        verrichtet.</p><p>Ist in dieser Einstellung keine Benutzerkennung ausgewählt, so
+        wird der Task-Server für diesen Mandanten keine Aufgaben
+        ausführen.</p></div><div class="sect2" title="2.7.3. Automatisches Starten des Task-Servers beim Booten"><div class="titlepage"><div><div><h3 class="title"><a name="Einbinden-in-den-Boot-Prozess"></a>2.7.3. Automatisches Starten des Task-Servers beim Booten</h3></div></div></div><p>Der Task-Server verhält sich von seinen Optionen her wie ein
         reguläres SystemV-kompatibles Boot-Script. Außerdem wechselt er beim
         Starten automatisch in das kivitendo-Installationsverzeichnis.</p><p>Deshalb ist es möglich, ihn durch Setzen eines symbolischen
         Links aus einem der Runlevel-Verzeichnisse heraus in den Boot-Prozess
         einzubinden. Da das bei neueren Linux-Distributionen aber nicht
         zwangsläufig funktioniert, werden auch Start-Scripte mitgeliefert, die
-        anstelle eines symbolischen Links verwendet werden können.</p><div class="sect3" title="2.7.2.1. SystemV-basierende Systeme (z.B. Debian, ältere OpenSUSE, ältere Fedora Core)"><div class="titlepage"><div><div><h4 class="title"><a name="d0e969"></a>2.7.2.1. SystemV-basierende Systeme (z.B. Debian, ältere OpenSUSE, ältere Fedora Core)</h4></div></div></div><p>Kopieren Sie die Datei
+        anstelle eines symbolischen Links verwendet werden können.</p><div class="sect3" title="2.7.3.1. SystemV-basierende Systeme (z.B. ältere Debian, ältere openSUSE, ältere Fedora)"><div class="titlepage"><div><div><h4 class="title"><a name="d0e1460"></a>2.7.3.1. SystemV-basierende Systeme (z.B. ältere Debian, ältere
+          openSUSE, ältere Fedora)</h4></div></div></div><p>Kopieren Sie die Datei
           <code class="filename">scripts/boot/system-v/kivitendo-task-server</code>
           nach <code class="filename">/etc/init.d/kivitendo-task-server</code>. Passen
           Sie in der kopierten Datei den Pfad zum Task-Server an (Zeile
           <code class="literal">DAEMON=....</code>). Binden Sie das Script in den
           Boot-Prozess ein. Dies ist distributionsabhängig:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Debian-basierende Systeme:</p><pre class="programlisting">update-rc.d kivitendo-task-server defaults
-# Nur bei Debian Squeeze und neuer:
-insserv kivitendo-task-server</pre></li><li class="listitem"><p>Ältere OpenSUSE und ältere Fedora Core:</p><pre class="programlisting">chkconfig --add kivitendo-task-server</pre></li></ul></div><p>Danach kann der Task-Server mit dem folgenden Befehl gestartet
-          werden:</p><pre class="programlisting">/etc/init.d/kivitendo-task-server start</pre></div><div class="sect3" title="2.7.2.2. Upstart-basierende Systeme (z.B. Ubuntu)"><div class="titlepage"><div><div><h4 class="title"><a name="d0e998"></a>2.7.2.2. Upstart-basierende Systeme (z.B. Ubuntu)</h4></div></div></div><p>Kopieren Sie die Datei
+insserv kivitendo-task-server</pre></li><li class="listitem"><p>Ältere openSUSE und ältere Fedora:</p><pre class="programlisting">chkconfig --add kivitendo-task-server</pre></li></ul></div><p>Danach kann der Task-Server mit dem folgenden Befehl gestartet
+          werden:</p><pre class="programlisting">/etc/init.d/kivitendo-task-server start</pre></div><div class="sect3" title="2.7.3.2. Upstart-basierende Systeme (z.B. Ubuntu bis 14.04)"><div class="titlepage"><div><div><h4 class="title"><a name="d0e1489"></a>2.7.3.2. Upstart-basierende Systeme (z.B. Ubuntu bis 14.04)</h4></div></div></div><p>Kopieren Sie die Datei
           <code class="filename">scripts/boot/upstart/kivitendo-task-server.conf</code>
           nach <code class="filename">/etc/init/kivitendo-task-server.conf</code>.
           Passen Sie in der kopierten Datei den Pfad zum Task-Server an (Zeile
           <code class="literal">exec ....</code>).</p><p>Danach kann der Task-Server mit dem folgenden Befehl gestartet
-          werden:</p><pre class="programlisting">service kivitendo-task-server start</pre></div><div class="sect3" title="2.7.2.3. systemd-basierende Systeme (z.B. neure OpenSUSE, neuere Fedora Core)"><div class="titlepage"><div><div><h4 class="title"><a name="d0e1016"></a>2.7.2.3. systemd-basierende Systeme (z.B. neure OpenSUSE, neuere Fedora Core)</h4></div></div></div><p>Verlinken Sie die Datei <code class="filename">scripts/boot/systemd/kivitendo-task-server.service</code> nach
-          <code class="filename">/etc/systemd/system/</code>. Passen Sie in der kopierten Datei den Pfad zum Task-Server an (Zeile
-          <code class="literal">ExecStart=....</code> und <code class="literal">ExecStop=...</code>). Binden Sie das Script in den Boot-Prozess ein.
-          </p><p>Alle hierzu benötigten Befehle sehen so aus:</p><pre class="programlisting">cd /var/www/kivitendo-erp/scripts/boot/systemd
-ln -s $(pwd)/kivitendo-task-server.service /etc/systemd/system/</pre><p>Danach kann der Task-Server mit dem folgenden Befehl gestartet
-          werden:</p><pre class="programlisting">systemctl start kivitendo-task-server.service</pre></div></div><div class="sect2" title="2.7.3. Wie der Task-Server gestartet und beendet wird"><div class="titlepage"><div><div><h3 class="title"><a name="Prozesskontrolle"></a>2.7.3. Wie der Task-Server gestartet und beendet wird</h3></div></div></div><p>Der Task-Server wird wie folgt kontrolliert:</p><pre class="programlisting">./scripts/task_server.pl Befehl</pre><p>
+          werden:</p><pre class="programlisting">service kivitendo-task-server start</pre></div><div class="sect3" title="2.7.3.3. systemd-basierende Systeme (z.B. neure openSUSE, neuere Fedora, neuere Ubuntu und neuere Debians)"><div class="titlepage"><div><div><h4 class="title"><a name="d0e1507"></a>2.7.3.3. systemd-basierende Systeme (z.B. neure openSUSE, neuere
+          Fedora, neuere Ubuntu und neuere Debians)</h4></div></div></div><p>Kopieren Sie die Datei
+          <code class="filename">scripts/boot/systemd/kivitendo-task-server.service</code>
+          nach <code class="filename">/etc/systemd/system/</code>. Passen Sie in der
+          kopierten Datei den Pfad zum Task-Server an (Zeilen
+          <code class="literal">ExecStart=....</code> und
+          <code class="literal">ExecStop=...</code>).</p><p>Machen Sie anschließend das Script systemd bekannt, und binden
+          Sie es in den Boot-Prozess ein. Dazu führen Sie die folgenden Befehl
+          aus:</p><pre class="programlisting">systemctl daemon-reload
+systemctl enable kivitendo-task-server.service</pre><p>Wenn Sie den Task-Server jetzt sofort starten möchten, anstatt
+          den Server neu zu starten, so können Sie das mit dem folgenden
+          Befehl tun:</p><pre class="programlisting">systemctl start kivitendo-task-server.service</pre><p>Ein so eingerichteter Task-Server startet nach Beendigung
+          automatisch erneut. Das betrifft eine Beendigung über die Oberfläche,
+          eine Beendingung über die Prozesskontrolle und eine Beendigung bei
+          Überschreiten des Memory-Limits. Soll der Task-Server nicht erneut
+          starten, so können Sie ihn mit folgendem Befehl stoppen:</p><pre class="programlisting">systemctl stop kivitendo-task-server.service</pre></div></div><div class="sect2" title="2.7.4. Wie der Task-Server gestartet und beendet wird"><div class="titlepage"><div><div><h3 class="title"><a name="Prozesskontrolle"></a>2.7.4. Wie der Task-Server gestartet und beendet wird</h3></div></div></div><p>Der Task-Server wird wie folgt kontrolliert:</p><pre class="programlisting">./scripts/task_server.pl Befehl</pre><p>
                <code class="literal">Befehl</code> ist dabei eine der folgenden
         Optionen:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
                      <code class="literal">start</code> startet eine neue Instanz des
@@ -60,7 +85,35 @@ ln -s $(pwd)/kivitendo-task-server.service /etc/systemd/system/</pre><p>Danach k
                      <code class="literal">status</code> berichtet, ob der Task-Server
             läuft.</p></li></ul></div><p>Der Task-Server wechselt beim Starten automatisch in das
         kivitendo-Installationsverzeichnis.</p><p>Dieselben Optionen können auch für die SystemV-basierenden
-        Runlevel-Scripte benutzt werden (siehe oben).</p></div><div class="sect2" title="2.7.4. Task-Server mit mehreren Mandanten"><div class="titlepage"><div><div><h3 class="title"><a name="Prozesskontrolle2"></a>2.7.4. Task-Server mit mehreren Mandanten</h3></div></div></div><p>Beim Task-Server werden der zu verwendende Mandant und Login-Name des Benutzers, unter dem der Task-Server laufen soll, in die
-        Konfigurationsdatei geschrieben. Hat man mehrere Mandanten, muss man auch mehrere Konfigurationsdateien anlegen.</p><p>Die Konfigurationsdatei ist eine Kopie der Datei kivitendo.conf, wo in der Kategorie <code class="varname">[task_server]</code> die
-        gewünschten Werte für <code class="varname">client</code> und <code class="varname">login</code> eingetragen werden.</p><p>Der alternative Task-Server wird dann mit folgendem Befehl
-        gestartet:</p><pre class="programlisting">./scripts/task_server.pl -c config/DATEINAME.conf</pre></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s06.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s08.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.6. Webserver-Konfiguration&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.8. Benutzerauthentifizierung und Administratorpasswort</td></tr></table></div></body></html>
\ No newline at end of file
+        Runlevel-Scripte benutzt werden (siehe oben).</p><p>Wurde der Task-Server als systemd-Service eingerichtet (s.o.),
+        so startet dieser nach Beendigung automatisch erneut.</p></div><div class="sect2" title="2.7.5. Exemplarische Konfiguration eines Hintergrund-Jobs, der die Jahreszahl in allen Nummernkreisen zum Jahreswechsel erhöht"><div class="titlepage"><div><div><h3 class="title"><a name="Tasks-konfigurieren"></a>2.7.5. Exemplarische Konfiguration eines Hintergrund-Jobs, der die Jahreszahl in allen Nummernkreisen zum Jahreswechsel erhöht</h3></div></div></div><p>Hintergrund-Jobs werden über System -&gt; Hintergrund-Jobs und Task-Server -&gt; Aktuelle Hintergrund-Jobs anzeigen -&gt; Aktions-Knopf 'erfassen' angelegt. </p><p>Nachdem wir über das Menü dort angelangt sind, legen wir unseren exemplarischen Hintergrund-Jobs "Erhöhung der Nummernkreise" mit folgenden Werten an:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
+                     <code class="literal">Aktiv:</code> Hier ein 'Ja' auswählen</p></li><li class="listitem"><p>
+                     <code class="literal">Ausführungsart:</code> 'wiederholte Ausführung' auswählen</p></li><li class="listitem"><p>
+                     <code class="literal">Paketname:</code> 'SetNumberRange' auswählen</p></li><li class="listitem"><p>
+                     <code class="literal">Ausführungszeitplan:</code> Hier entsprechend Werte wie in der crontab eingeben.</p><p>Syntax:</p><pre class="programlisting">* * * * *
+┬ ┬ ┬ ┬ ┬
+│ │ │ │ │
+│ │ │ │ └──── Wochentag (0-7, Sonntag ist 0 oder 7)
+│ │ │ └────── Monat (1-12)
+│ │ └──────── Tag (1-31)
+│ └────────── Stunde (0-23)
+└──────────── Minute (0-59)  </pre><p>Die Sterne können folgende Werte haben:</p><pre class="programlisting">
+1 2 3 4 5
+
+1 = Minute (0-59)
+2 = Stunde (0-23)
+3 = Tag (0-31)
+4 = Monat (1-12)
+5 = Wochentag (0-7, Sonntag ist 0 oder 7)
+</pre><p>Um die Ausführung auf eine Minute vor Sylvester zu setzen, müssen die folgenden Werte eingetragen werden:</p><pre class="programlisting">59 23 31 12 *</pre></li><li class="listitem"><p>
+                     <code class="literal">Daten:</code>In diesem Feld können optionale Parameter für den Hintergrund im JSON-Format gesetzt werden. Der Hintergrund-Job <code class="literal">SetNumberRange</code> akzeptiert zwei Variable nämlich <code class="literal">digit_year</code> sowieso <code class="literal">multiplier</code>.</p><p> 
+                     <code class="literal">digit_year</code> kann zwei Werte haben entweder 2 oder 4, darüber wird gesteuert ob die Jahreszahl zwei oder vierstellig kodiert wird (für 2019, dann entweder 19 oder 2019). Der Standardwert ist vierstellig.</p><p> 
+                     <code class="literal">multiplier</code> ist ein Vielfaches von 10, darüber wird die erste Nummer im Nummernkreis (die Anzahl der Stellen) wie folgt bestimmt:</p><pre class="programlisting">
+multiplier     Nummernkreis 2020
+10        -&gt;   20200
+100       -&gt;   202000
+1000      -&gt;   2020000
+</pre><p>Wir gehen jetzt beispielhaft von einer letzten Rechnungsnummer von RE2019456 aus. Demnach sollte ab Januar 2020 die erste Nummer RE2020001 sein. Da der Task auch Präfixe berücksichtigt, kann dies mit folgenden JSON-kodierten Werten umgesetzt werden:</p><p>
+                     <code class="literal">Daten:</code>
+                  </p><pre class="programlisting">multiplier: 100
+digits_year: 4</pre></li></ul></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s06.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s08.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.6. Webserver-Konfiguration&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.8. Benutzerauthentifizierung und Administratorpasswort</td></tr></table></div></body></html>
\ No newline at end of file
index e62e24d..d10b46c 100644 (file)
@@ -1,26 +1,26 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.8. Benutzerauthentifizierung und Administratorpasswort</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s07.html" title="2.7. Der Task-Server"><link rel="next" href="ch02s09.html" title="2.9. Mandanten-, Benutzer- und Gruppenverwaltung"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.8. Benutzerauthentifizierung und Administratorpasswort</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s07.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s09.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.8. Benutzerauthentifizierung und Administratorpasswort"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Benutzerauthentifizierung-und-Administratorpasswort"></a>2.8. Benutzerauthentifizierung und Administratorpasswort</h2></div></div></div><p>Informationen über die Einrichtung der Benutzerauthentifizierung,
+   <title>2.8. Benutzerauthentifizierung und Administratorpasswort</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s07.html" title="2.7. Der Task-Server"><link rel="next" href="ch02s09.html" title="2.9. Mandanten-, Benutzer- und Gruppenverwaltung"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.8. Benutzerauthentifizierung und Administratorpasswort</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s07.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s09.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.8. Benutzerauthentifizierung und Administratorpasswort"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Benutzerauthentifizierung-und-Administratorpasswort"></a>2.8. Benutzerauthentifizierung und Administratorpasswort</h2></div></div></div><p>Informationen über die Einrichtung der Benutzerauthentifizierung,
       über die Verwaltung von Gruppen und weitere Einstellungen</p><div class="sect2" title="2.8.1. Grundlagen zur Benutzerauthentifizierung"><div class="titlepage"><div><div><h3 class="title"><a name="Grundlagen-zur-Benutzerauthentifizierung"></a>2.8.1. Grundlagen zur Benutzerauthentifizierung</h3></div></div></div><p>kivitendo verwaltet die Benutzerinformationen in einer
         Datenbank, die im folgenden “Authentifizierungsdatenbank” genannt
         wird. Für jeden Benutzer kann dort eine eigene Datenbank für die
         eigentlichen Finanzdaten hinterlegt sein. Diese beiden Datenbanken
         können, müssen aber nicht unterschiedlich sein.</p><p>Im einfachsten Fall gibt es für kivitendo nur eine einzige
         Datenbank, in der sowohl die Benutzerinformationen als auch die Daten
-        abgelegt werden.</p><p>Zusätzlich ermöglicht es kivitendo, dass die Benutzerpasswörter
-        entweder gegen die Authentifizierungsdatenbank oder gegen einen
-        LDAP-Server überprüft werden.</p><p>Welche Art der Passwortüberprüfung kivitendo benutzt und wie
+        abgelegt werden.</p><p>Zusätzlich ermöglicht es kivitendo, dass die Benutzerpasswörter gegen die Authentifizierungsdatenbank oder gegen einen oder
+        mehrere LDAP-Server überprüft werden.</p><p>Welche Art der Passwortüberprüfung kivitendo benutzt und wie
         kivitendo die Authentifizierungsdatenbank erreichen kann, wird in der
         Konfigurationsdatei <code class="filename">config/kivitendo.conf</code>
         festgelegt. Diese muss bei der Installation und bei einem Upgrade von
         einer Version vor v2.6.0 angelegt werden. Eine
         Beispielkonfigurationsdatei
         <code class="filename">config/kivitendo.conf.default</code> existiert, die als
-        Vorlage benutzt werden kann.</p></div><div class="sect2" title="2.8.2. Administratorpasswort"><div class="titlepage"><div><div><h3 class="title"><a name="Administratorpasswort"></a>2.8.2. Administratorpasswort</h3></div></div></div><p>Das Passwort, das zum Zugriff auf das Administrationsinterface von kivitendo
-        benutzt wird, wird ebenfalls in dieser Datei gespeichert. Es kann auch
-        nur dort und nicht mehr im Administrationsinterface selber geändert
-        werden. Der Parameter dazu heißt <code class="varname">admin_password</code> im
-        Abschnitt <code class="varname">[authentication]</code>.</p></div><div class="sect2" title="2.8.3. Authentifizierungsdatenbank"><div class="titlepage"><div><div><h3 class="title"><a name="Authentifizierungsdatenbank"></a>2.8.3. Authentifizierungsdatenbank</h3></div></div></div><p>Die Verbindung zur Authentifizierungsdatenbank wird mit den
+        Vorlage benutzt werden kann.</p></div><div class="sect2" title="2.8.2. Administratorpasswort"><div class="titlepage"><div><div><h3 class="title"><a name="Administratorpasswort"></a>2.8.2. Administratorpasswort</h3></div></div></div><p>Das Passwort, das zum Zugriff auf das Administrationsinterface
+        von kivitendo benutzt wird, wird ebenfalls in dieser Datei
+        gespeichert. Es kann auch nur dort und nicht mehr im
+        Administrationsinterface selber geändert werden. Der Parameter dazu
+        heißt <code class="varname">admin_password</code> im Abschnitt
+        <code class="varname">[authentication]</code>.</p></div><div class="sect2" title="2.8.3. Authentifizierungsdatenbank"><div class="titlepage"><div><div><h3 class="title"><a name="Authentifizierungsdatenbank"></a>2.8.3. Authentifizierungsdatenbank</h3></div></div></div><p>Die Verbindung zur Authentifizierungsdatenbank wird mit den
         Parametern in <code class="varname">[authentication/database]</code>
         konfiguriert. Hier sind die folgenden Parameter anzugeben:</p><div class="variablelist"><dl><dt><span class="term">
                      <code class="literal">host</code>
                      <code class="literal">password</code>
                   </span></dt><dd><p>Das Passwort für den Datenbankbenutzer</p></dd></dl></div><p>Die Datenbank muss noch nicht existieren. kivitendo kann sie
         automatisch anlegen (mehr dazu siehe unten).</p></div><div class="sect2" title="2.8.4. Passwortüberprüfung"><div class="titlepage"><div><div><h3 class="title"><a name="Passwort%C3%BCberpr%C3%BCfung"></a>2.8.4. Passwortüberprüfung</h3></div></div></div><p>kivitendo unterstützt Passwortüberprüfung auf zwei Arten: gegen
-        die Authentifizierungsdatenbank und gegen einen externen LDAP- oder
+        die Authentifizierungsdatenbank und gegen externe LDAP- oder
         Active-Directory-Server. Welche davon benutzt wird, regelt der
         Parameter <code class="varname">module</code> im Abschnitt
-        <code class="varname">[authentication]</code>.</p><p>Sollen die Benutzerpasswörter in der Authentifizierungsdatenbank
-        gespeichert werden, so muss der Parameter <code class="varname">module</code>
-        den Wert <code class="literal">DB</code> enthalten. In diesem Fall können sowohl
-        der Administrator als auch die Benutzer selber ihre Psaswörter in
-        kivitendo ändern.</p><p>Soll hingegen ein externer LDAP- oder Active-Directory-Server
-        benutzt werden, so muss der Parameter <code class="varname">module</code> auf
-        <code class="literal">LDAP</code> gesetzt werden. In diesem Fall müssen
-        zusätzliche Informationen über den LDAP-Server im Abschnitt
-        <code class="literal">[authentication/ldap]</code> angegeben werden:</p><div class="variablelist"><dl><dt><span class="term">
+        <code class="varname">[authentication]</code>.</p><p>Dieser Parameter listet die zu verwendenden Authentifizierungsmodule auf. Es muss mindestens ein Modul angegeben werden, es
+        können aber auch mehrere angegeben werden. Weiterhin ist es möglich, das LDAP-Modul mehrfach zu verwenden und für jede Verwendung
+        eine unterschiedliche Konfiguration zu nutzen, z.B. um einen Fallback-Server anzugeben, der benutzt wird, sofern der Hauptserver
+        nicht erreichbar ist.</p><p>Sollen die Benutzerpasswörter in der Authentifizierungsdatenbank geprüft werden, so muss der Parameter
+        <code class="varname">module</code> das Modul <code class="literal">DB</code> enthalten. Sofern das Modul in der Liste enthalten ist, egal an welcher
+        Position, können sowohl der Administrator als auch die Benutzer selber ihre Passwörter in kivitendo ändern.</p><p>Wenn Passwörter gegen einen oder mehrere externe LDAP- oder Active-Directory-Server geprüft werden, so muss der Parameter
+        <code class="varname">module</code> den Wert <code class="literal">LDAP</code> enthalten. In diesem Fall müssen zusätzliche Informationen über den
+        LDAP-Server im Abschnitt <code class="literal">[authentication/ldap]</code> angegeben werden. Das Modul kann auch mehrfach angegeben werden,
+        wobei jedes Modul eine eigene Konfiguration bekommen sollte. Der Name der Konfiguration wird dabei mit einem Doppelpunkt getrennt an
+        den Modulnamen angehängt (<code class="literal">LDAP:Name-der-Konfiguration</code>). Der entsprechende Abschnitt in der Konfigurationsdatei
+        lautet dann <code class="literal">[authentication/Name-der-Konfiguration]</code>.</p><p>Die verfügbaren Parameter für die LDAP-Konfiguration lauten:</p><div class="variablelist"><dl><dt><span class="term">
                      <code class="literal">host</code>
                   </span></dt><dd><p>Der Rechnername oder die IP-Adresse des LDAP- oder
               Active-Directory-Servers. Diese Angabe ist zwingend
                   </span></dt><dd><p>Wenn Verbindungsverschlüsselung gewünscht ist, so diesen
               Wert auf ‘<code class="literal">1</code>’ setzen, andernfalls auf
               ‘<code class="literal">0</code>’ belassen</p></dd><dt><span class="term">
+                     <code class="literal">verify</code>
+                  </span></dt><dd><p>Wenn Verbindungsverschlüsselung gewünscht und der Parameter <em class="parameter"><code>tls</code></em> gesetzt ist, so gibt dieser
+              Parameter an, ob das Serverzertifikat auf Gültigkeit geprüft wird. Mögliche Werte sind <code class="literal">require</code> (Zertifikat
+              wird überprüft und muss gültig sei; dies ist der Standard) und <code class="literal">none</code> (Zertifikat wird nicht
+              überpfüft).</p></dd><dt><span class="term">
                      <code class="literal">attribute</code>
                   </span></dt><dd><p>Das LDAP-Attribut, in dem der Benutzername steht, den der
               Benutzer eingegeben hat. Für Active-Directory-Server ist dies
@@ -84,7 +91,9 @@
               z.B. ‘<code class="literal">cn=Martin
               Mustermann,cn=Users,dc=firmendomain</code>’ auch nur der
               volle Name des Benutzers eingegeben werden; in diesem Beispiel
-              also ‘<code class="literal">Martin Mustermann</code>’.</p></dd></dl></div></div><div class="sect2" title="2.8.5. Name des Session-Cookies"><div class="titlepage"><div><div><h3 class="title"><a name="Name-des-Session-Cookies"></a>2.8.5. Name des Session-Cookies</h3></div></div></div><p>Sollen auf einem Server mehrere kivitendo-Installationen
+              also ‘<code class="literal">Martin Mustermann</code>’.</p></dd><dt><span class="term">
+                     <code class="literal">timeout</code>
+                  </span></dt><dd><p>Timeout beim Verbindungsversuch, bevor der Server als nicht erreichbar gilt; Standardwert: 10</p></dd></dl></div></div><div class="sect2" title="2.8.5. Name des Session-Cookies"><div class="titlepage"><div><div><h3 class="title"><a name="Name-des-Session-Cookies"></a>2.8.5. Name des Session-Cookies</h3></div></div></div><p>Sollen auf einem Server mehrere kivitendo-Installationen
         aufgesetzt werden, so müssen die Namen der Session-Cookies für alle
         Installationen unterschiedlich sein. Der Name des Cookies wird mit dem
         Parameter <code class="varname">cookie_name</code> im Abschnitt
index aa476ee..8a718e6 100644 (file)
@@ -1,47 +1,83 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.9. Mandanten-, Benutzer- und Gruppenverwaltung</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s08.html" title="2.8. Benutzerauthentifizierung und Administratorpasswort"><link rel="next" href="ch02s10.html" title="2.10. Drucker- und Systemverwaltung"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.9. Mandanten-, Benutzer- und Gruppenverwaltung</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s08.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s10.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.9. Mandanten-, Benutzer- und Gruppenverwaltung"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Benutzer--und-Gruppenverwaltung"></a>2.9. Mandanten-, Benutzer- und Gruppenverwaltung</h2></div></div></div><p>Nach der Installation müssen Mandanten, Benutzer, Gruppen und Datenbanken angelegt werden. Dieses geschieht im
-      Administrationsmenü, das Sie unter folgender URL finden:</p><p>
+   <title>2.9. Mandanten-, Benutzer- und Gruppenverwaltung</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s08.html" title="2.8. Benutzerauthentifizierung und Administratorpasswort"><link rel="next" href="ch02s10.html" title="2.10. Drucker- und Systemverwaltung"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.9. Mandanten-, Benutzer- und Gruppenverwaltung</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s08.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s10.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.9. Mandanten-, Benutzer- und Gruppenverwaltung"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Benutzer--und-Gruppenverwaltung"></a>2.9. Mandanten-, Benutzer- und Gruppenverwaltung</h2></div></div></div><p>Nach der Installation müssen Mandanten, Benutzer, Gruppen und
+      Datenbanken angelegt werden. Dieses geschieht im Administrationsmenü,
+      das Sie unter folgender URL finden:</p><p>
             <a class="ulink" href="http://localhost/kivitendo-erp/controller.pl?action=Admin/login" target="_top">http://localhost/kivitendo-erp/controller.pl?action=Admin/login</a>
          </p><p>Verwenden Sie zur Anmeldung das Passwort, das Sie in der Datei
-      <code class="filename">config/kivitendo.conf</code> eingetragen haben.</p><div class="sect2" title="2.9.1. Zusammenhänge"><div class="titlepage"><div><div><h3 class="title"><a name="Zusammenh%C3%A4nge"></a>2.9.1. Zusammenhänge</h3></div></div></div><p>kivitendo verwaltet zwei Sets von Daten, die je nach Einrichtung in einer oder zwei Datenbanken gespeichert werden.</p><p>Das erste Set besteht aus Anmeldeinformationen: welche Benutzer und Mandanten gibt es, welche Gruppen, welche BenutzerIn hat
-        Zugriff auf welche Mandanten, und welche Gruppe verfügt über welche Rechte. Diese Informationen werden in der
-        Authentifizierungsdatenbank gespeichert. Dies ist diejenige Datenbank, deren Verbindungsparameter in der Konfigurationsdatei
-        <code class="filename">config/kivitendo.conf</code> gespeichert werden.</p><p>Das zweite Set besteht aus den eigentlichen Verkehrsdaten eines Mandanten, wie beispielsweise die Stammdaten (Kunden, Lieferanten, Waren) und Belege
-        (Angebote, Lieferscheine, Rechnungen). Diese werden in einer Mandantendatenbank gespeichert. Die
-        Verbindungsinformationen einer solchen Mandantendatenbank werden im Administrationsbereich konfiguriert, indem man einen Mandanten
-        anlegt und dort die Parameter einträgt. Dabei hat jeder Mandant eine eigene Datenbank.</p><p>Aufgrund des Datenbankdesigns ist es für einfache Fälle möglich, die Authentifizierungsdatenbank und eine der
-        Mandantendatenbanken in ein und derselben Datenbank zu speichern. Arbeitet man hingegen mit mehr als einem Mandanten, wird
-        empfohlen, für die Authentifizierungsdatenbank eine eigene Datenbank zu verwenden, die nicht gleichzeitig für einen Mandanten
-        verwendet wird.</p></div><div class="sect2" title="2.9.2. Mandanten, Benutzer und Gruppen"><div class="titlepage"><div><div><h3 class="title"><a name="Mandanten-Benutzer-Gruppen"></a>2.9.2. Mandanten, Benutzer und Gruppen</h3></div></div></div><p>kivitendos Administration kennt Mandanten, Benutzer und Gruppen, die sich frei zueinander zuordnen lassen.</p><p>kivitendo kann mehrere Mandaten aus einer Installation heraus verwalten. Welcher Mandant benutzt wird, kann direkt beim Login
-        ausgewählt werden. Für jeden Mandanten wird ein eindeutiger Name vergeben, der beim Login angezeigt wird. Weiterhin benötigt der
-        Mandant Datenbankverbindungsparameter für seine Mandantendatenbank. Diese sollte über die <a class="link" href="ch02s09.html#Datenbanken-anlegen" title="2.9.3. Datenbanken anlegen">Datenbankverwaltung</a> geschehen.</p><p>Ein Benutzer ist eine Person, die Zugriff auf kivitendo erhalten soll. Sie erhält einen Loginnamen sowie ein
-        Passwort. Weiterhin legt der Administrator fest, an welchen Mandanten sich ein Benutzer anmelden kann, was beim Login verifiziert
-        wird.</p><p>Gruppen dienen dazu, Benutzern innerhalb eines Mandanten Zugriff auf bestimmte Funktionen zu geben. Einer Gruppe werden dafür
-        vom Administrator gewisse Rechte zugeordnet. Weiterhin legt der Administrator fest, für welche Mandanten eine Gruppe gilt, und
-        welche Benutzer Mitglieder in dieser Gruppe sind. Meldet sich ein Benutzer dann an einem Mandanten an, so erhält er alle Rechte von
-        allen denjenigen Gruppen, die zum Einen dem Mandanten zugeordnet sind und in denen der Benutzer zum Anderen Mitglied ist, </p><p>Die Reihenfolge, in der Datenbanken, Mandanten, Gruppen und Benutzer angelegt werden, kann im Prinzip beliebig gewählt
-        werden. Die folgende Reihenfolge beinhaltet die wenigsten Arbeitsschritte:</p><div class="orderedlist"><ol class="orderedlist" type="1"><li class="listitem"><p>Datenbank anlegen</p></li><li class="listitem"><p>Gruppen anlegen</p></li><li class="listitem"><p>Benutzer anlegen und Gruppen als Mitglied zuordnen</p></li><li class="listitem"><p>Mandanten anlegen und Gruppen sowie Benutzer zuweisen</p></li></ol></div></div><div class="sect2" title="2.9.3. Datenbanken anlegen"><div class="titlepage"><div><div><h3 class="title"><a name="Datenbanken-anlegen"></a>2.9.3. Datenbanken anlegen</h3></div></div></div><p>Zuerst muss eine Datenbank angelegt werden. Verwenden Sie für
+      <code class="filename">config/kivitendo.conf</code> eingetragen haben.</p><div class="sect2" title="2.9.1. Zusammenhänge"><div class="titlepage"><div><div><h3 class="title"><a name="Zusammenh%C3%A4nge"></a>2.9.1. Zusammenhänge</h3></div></div></div><p>kivitendo verwaltet zwei Sets von Daten, die je nach Einrichtung
+        in einer oder zwei Datenbanken gespeichert werden.</p><p>Das erste Set besteht aus Anmeldeinformationen: welche Benutzer
+        und Mandanten gibt es, welche Gruppen, welche BenutzerIn hat Zugriff
+        auf welche Mandanten, und welche Gruppe verfügt über welche Rechte.
+        Diese Informationen werden in der Authentifizierungsdatenbank
+        gespeichert. Dies ist diejenige Datenbank, deren Verbindungsparameter
+        in der Konfigurationsdatei <code class="filename">config/kivitendo.conf</code>
+        gespeichert werden.</p><p>Das zweite Set besteht aus den eigentlichen Verkehrsdaten eines
+        Mandanten, wie beispielsweise die Stammdaten (Kunden, Lieferanten,
+        Waren) und Belege (Angebote, Lieferscheine, Rechnungen). Diese werden
+        in einer Mandantendatenbank gespeichert. Die Verbindungsinformationen
+        einer solchen Mandantendatenbank werden im Administrationsbereich
+        konfiguriert, indem man einen Mandanten anlegt und dort die Parameter
+        einträgt. Dabei hat jeder Mandant eine eigene Datenbank.</p><p>Aufgrund des Datenbankdesigns ist es für einfache Fälle möglich,
+        die Authentifizierungsdatenbank und eine der Mandantendatenbanken in
+        ein und derselben Datenbank zu speichern. Arbeitet man hingegen mit
+        mehr als einem Mandanten, wird empfohlen, für die
+        Authentifizierungsdatenbank eine eigene Datenbank zu verwenden, die
+        nicht gleichzeitig für einen Mandanten verwendet wird.</p></div><div class="sect2" title="2.9.2. Mandanten, Benutzer und Gruppen"><div class="titlepage"><div><div><h3 class="title"><a name="Mandanten-Benutzer-Gruppen"></a>2.9.2. Mandanten, Benutzer und Gruppen</h3></div></div></div><p>kivitendos Administration kennt Mandanten, Benutzer und Gruppen,
+        die sich frei zueinander zuordnen lassen.</p><p>kivitendo kann mehrere Mandaten aus einer Installation heraus
+        verwalten. Welcher Mandant benutzt wird, kann direkt beim Login
+        ausgewählt werden. Für jeden Mandanten wird ein eindeutiger Name
+        vergeben, der beim Login angezeigt wird. Weiterhin benötigt der
+        Mandant Datenbankverbindungsparameter für seine Mandantendatenbank.
+        Diese sollte über die <a class="link" href="ch02s09.html#Datenbanken-anlegen" title="2.9.3. Datenbanken anlegen">Datenbankverwaltung</a>
+        geschehen.</p><p>Ein Benutzer ist eine Person, die Zugriff auf kivitendo erhalten
+        soll. Sie erhält einen Loginnamen sowie ein Passwort. Weiterhin legt
+        der Administrator fest, an welchen Mandanten sich ein Benutzer
+        anmelden kann, was beim Login verifiziert wird.</p><p>Gruppen dienen dazu, Benutzern innerhalb eines Mandanten Zugriff
+        auf bestimmte Funktionen zu geben. Einer Gruppe werden dafür vom
+        Administrator gewisse Rechte zugeordnet. Weiterhin legt der
+        Administrator fest, für welche Mandanten eine Gruppe gilt, und welche
+        Benutzer Mitglieder in dieser Gruppe sind. Meldet sich ein Benutzer
+        dann an einem Mandanten an, so erhält er alle Rechte von allen
+        denjenigen Gruppen, die zum Einen dem Mandanten zugeordnet sind und in
+        denen der Benutzer zum Anderen Mitglied ist,</p><p>Die Reihenfolge, in der Datenbanken, Mandanten, Gruppen und
+        Benutzer angelegt werden, kann im Prinzip beliebig gewählt werden. Die
+        folgende Reihenfolge beinhaltet die wenigsten Arbeitsschritte:</p><div class="orderedlist"><ol class="orderedlist" type="1"><li class="listitem"><p>Datenbank anlegen</p></li><li class="listitem"><p>Gruppen anlegen</p></li><li class="listitem"><p>Benutzer anlegen und Gruppen als Mitglied zuordnen</p></li><li class="listitem"><p>Mandanten anlegen und Gruppen sowie Benutzer zuweisen</p></li></ol></div></div><div class="sect2" title="2.9.3. Datenbanken anlegen"><div class="titlepage"><div><div><h3 class="title"><a name="Datenbanken-anlegen"></a>2.9.3. Datenbanken anlegen</h3></div></div></div><p>Zuerst muss eine Datenbank angelegt werden. Verwenden Sie für
         den Datenbankzugriff den vorhin angelegten Benutzer (in unseren
         Beispielen ist dies ‘<code class="literal">kivitendo</code>’).</p></div><div class="sect2" title="2.9.4. Gruppen anlegen"><div class="titlepage"><div><div><h3 class="title"><a name="Gruppen-anlegen"></a>2.9.4. Gruppen anlegen</h3></div></div></div><p>Eine Gruppe wird in der Gruppenverwaltung angelegt. Ihr muss ein
         Name gegeben werden, eine Beschreibung ist hingegen optional. Nach dem
         Anlegen können Sie die verschiedenen Bereiche wählen, auf die
-        Mitglieder dieser Gruppe Zugriff haben sollen.</p><p>Benutzergruppen werden zwar in der Authentifizierungsdatenbank gespeichert, gelten aber nicht automatisch für alle
-        Mandanten. Der Administrator legt vielmehr fest, für welche Mandanten eine Gruppe gültig ist. Dies kann entweder beim Bearbeiten der
-        Gruppe geschehen ("diese Gruppe ist gültig für Mandanten X, Y und Z"), oder aber wenn man einen Mandanten bearbeitet ("für diesen
-        Mandanten sind die Gruppen A, B und C gültig").</p><p>Wurden bereits Benutzer angelegt, so können hier die Mitglieder dieser Gruppe festgelegt werden ("in dieser Gruppe sind die
-        Benutzer X, Y und Z Mitglieder"). Dies kann auch nachträglich beim Bearbeiten eines Benutzers geschehen ("dieser Benutzer ist
-        Mitglied in den Gruppen A, B und C").</p></div><div class="sect2" title="2.9.5. Benutzer anlegen"><div class="titlepage"><div><div><h3 class="title"><a name="Benutzer-anlegen"></a>2.9.5. Benutzer anlegen</h3></div></div></div><p>Beim Anlegen von Benutzern werden für viele Parameter Standardeinstellungen vorgenommen, die den Gepflogenheiten des deutschen
-        Raumes entsprechen.</p><p>Zwingend anzugeben ist der Loginname. Wenn die Passwortauthentifizierung über die Datenbank eingestellt ist, so kann hier auch
-        das Benutzerpasswort gesetzt bzw. geändert werden. Ist hingegen die LDAP-Authentifizierung aktiv, so ist das Passwort-Feld
-        deaktiviert.</p><p>Hat man bereits Mandanten und Gruppen angelegt, so kann hier auch konfiguriert werden, auf welche Mandanten der Benutzer
-        Zugriff hat bzw. in welchen Gruppen er Mitglied ist. Beide Zuweisungen können sowohl beim Benutzer vorgenommen werden ("dieser
-        Benutzer hat Zugriff auf Mandanten X, Y, Z" bzw. "dieser Benutzer ist Mitglied in Gruppen X, Y und Z") als auch beim Mandanten ("auf
-        diesen Mandanten haben Benutzer A, B und C Zugriff") bzw. bei der Gruppe ("in dieser Gruppe sind Benutzer A, B und C
-        Mitglieder").</p></div><div class="sect2" title="2.9.6. Mandanten anlegen"><div class="titlepage"><div><div><h3 class="title"><a name="Mandanten-anlegen"></a>2.9.6. Mandanten anlegen</h3></div></div></div><p>Ein Mandant besteht aus Administrationssicht primär aus einem eindeutigen Namen. Weiterhin wird hier hinterlegt, welche
-        Datenbank als Mandantendatenbank benutzt wird. Hier müssen die Zugriffsdaten einer der eben angelegten Datenbanken eingetragen
-        werden.</p><p>Hat man bereits Benutzer und Gruppen angelegt, so kann hier auch konfiguriert werden, welche Benutzer Zugriff auf den
-        Mandanten haben bzw. welche Gruppen für den Mandanten gültig sind. Beide Zuweisungen können sowohl beim Mandanten vorgenommen werden
-        ("auf diesen Mandanten haben Benutzer X, Y und Z Zugriff" bzw. "für diesen Mandanten sind die Gruppen X, Y und Z gültig") als auch
-        beim Benutzer ("dieser Benutzer hat Zugriff auf Mandanten A, B und C") bzw. bei der Gruppe ("diese Gruppe ist für Mandanten A, B und
-        C gültig").</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s08.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s10.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.8. Benutzerauthentifizierung und Administratorpasswort&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.10. Drucker- und Systemverwaltung</td></tr></table></div></body></html>
\ No newline at end of file
+        Mitglieder dieser Gruppe Zugriff haben sollen.</p><p>Benutzergruppen werden zwar in der Authentifizierungsdatenbank
+        gespeichert, gelten aber nicht automatisch für alle Mandanten. Der
+        Administrator legt vielmehr fest, für welche Mandanten eine Gruppe
+        gültig ist. Dies kann entweder beim Bearbeiten der Gruppe geschehen
+        ("diese Gruppe ist gültig für Mandanten X, Y und Z"), oder aber wenn
+        man einen Mandanten bearbeitet ("für diesen Mandanten sind die Gruppen
+        A, B und C gültig").</p><p>Wurden bereits Benutzer angelegt, so können hier die Mitglieder
+        dieser Gruppe festgelegt werden ("in dieser Gruppe sind die Benutzer
+        X, Y und Z Mitglieder"). Dies kann auch nachträglich beim Bearbeiten
+        eines Benutzers geschehen ("dieser Benutzer ist Mitglied in den
+        Gruppen A, B und C").</p></div><div class="sect2" title="2.9.5. Benutzer anlegen"><div class="titlepage"><div><div><h3 class="title"><a name="Benutzer-anlegen"></a>2.9.5. Benutzer anlegen</h3></div></div></div><p>Beim Anlegen von Benutzern werden für viele Parameter
+        Standardeinstellungen vorgenommen, die den Gepflogenheiten des
+        deutschen Raumes entsprechen.</p><p>Zwingend anzugeben ist der Loginname. Wenn die
+        Passwortauthentifizierung über die Datenbank eingestellt ist, so kann
+        hier auch das Benutzerpasswort gesetzt bzw. geändert werden. Ist
+        hingegen die LDAP-Authentifizierung aktiv, so ist das Passwort-Feld
+        deaktiviert.</p><p>Hat man bereits Mandanten und Gruppen angelegt, so kann hier
+        auch konfiguriert werden, auf welche Mandanten der Benutzer Zugriff
+        hat bzw. in welchen Gruppen er Mitglied ist. Beide Zuweisungen können
+        sowohl beim Benutzer vorgenommen werden ("dieser Benutzer hat Zugriff
+        auf Mandanten X, Y, Z" bzw. "dieser Benutzer ist Mitglied in Gruppen
+        X, Y und Z") als auch beim Mandanten ("auf diesen Mandanten haben
+        Benutzer A, B und C Zugriff") bzw. bei der Gruppe ("in dieser Gruppe
+        sind Benutzer A, B und C Mitglieder").</p></div><div class="sect2" title="2.9.6. Mandanten anlegen"><div class="titlepage"><div><div><h3 class="title"><a name="Mandanten-anlegen"></a>2.9.6. Mandanten anlegen</h3></div></div></div><p>Ein Mandant besteht aus Administrationssicht primär aus einem
+        eindeutigen Namen. Weiterhin wird hier hinterlegt, welche Datenbank
+        als Mandantendatenbank benutzt wird. Hier müssen die Zugriffsdaten
+        einer der eben angelegten Datenbanken eingetragen werden.</p><p>Hat man bereits Benutzer und Gruppen angelegt, so kann hier auch
+        konfiguriert werden, welche Benutzer Zugriff auf den Mandanten haben
+        bzw. welche Gruppen für den Mandanten gültig sind. Beide Zuweisungen
+        können sowohl beim Mandanten vorgenommen werden ("auf diesen Mandanten
+        haben Benutzer X, Y und Z Zugriff" bzw. "für diesen Mandanten sind die
+        Gruppen X, Y und Z gültig") als auch beim Benutzer ("dieser Benutzer
+        hat Zugriff auf Mandanten A, B und C") bzw. bei der Gruppe ("diese
+        Gruppe ist für Mandanten A, B und C gültig").</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s08.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s10.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.8. Benutzerauthentifizierung und Administratorpasswort&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.10. Drucker- und Systemverwaltung</td></tr></table></div></body></html>
\ No newline at end of file
index 02b859e..1127acd 100644 (file)
@@ -1,9 +1,23 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.10. Drucker- und Systemverwaltung</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s09.html" title="2.9. Mandanten-, Benutzer- und Gruppenverwaltung"><link rel="next" href="ch02s11.html" title="2.11. E-Mail-Versand aus kivitendo heraus"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.10. Drucker- und Systemverwaltung</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s09.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s11.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.10. Drucker- und Systemverwaltung"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Drucker--Systemverwaltung"></a>2.10. Drucker- und Systemverwaltung</h2></div></div></div><p>Im Administrationsmenü gibt es ferner noch die beiden Menüpunkte Druckeradministration und System.</p><div class="sect2" title="2.10.1. Druckeradministration"><div class="titlepage"><div><div><h3 class="title"><a name="Druckeradministration"></a>2.10.1. Druckeradministration</h3></div></div></div><p>Unter dem Menüpunkt Druckeradministration lassen sich beliebig viele "Druckbefehle" im System verwalten. Diese Befehle werden mandantenweise
-      zugeordnet. Unter Druckerbeschreibung wird der Namen des Druckbefehls festgelegt, der dann in der Druckerauswahl des Belegs angezeigt wird.</p><p>Unter Druckbefehl definiert man den eigentlichen Druckbefehl, der direkt auf dem Webserver ausgeführt wird, bspw. 'lpr -P meinDrucker' oder ein
-      kompletter Pfad zu einem Skript (/usr/local/src/kivitendo/scripts/pdf_druck_in_verzeichnis.sh).
-      Wird ferner noch ein optionales Vorlagenkürzel verwendet, wird dieses Kürzel bei der Auswahl der Druckvorlagendatei mit einem Unterstrich ergänzt, ist
-      bspw. das Kürzel 'epson_drucker' definiert, so wird beim Ausdruck eines Angebots folgende Vorlage geparst: sales_quotation_epson_drucker.tex.</p></div><div class="sect2" title="2.10.2. System sperren / entsperren"><div class="titlepage"><div><div><h3 class="title"><a name="System"></a>2.10.2. System sperren / entsperren</h3></div></div></div><p>Unter dem Menüpunkt System gibt es den Eintrag 'Installation sperren/entsperren'. Setzt man diese Sperre so ist der Zugang zu der gesamten kivitendo Installation gesperrt.</p><p>Falls die Sperre gesetzt ist, erscheint anstelle der Anmeldemaske die Information: 'kivitendo ist momentan zwecks Wartungsarbeiten nicht zugänglich.'.
-        </p><p>Wichtig zu erwähnen ist hierbei noch, dass sich kivitendo automatisch 'sperrt', falls es bei einem Versionsupdate zu einem Datenbankfehler kam. Somit kann hier nicht aus Versehen
-        mit einem inkonsistenten Datenbestand weitergearbeitet werden.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s09.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s11.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.9. Mandanten-, Benutzer- und Gruppenverwaltung&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.11. E-Mail-Versand aus kivitendo heraus</td></tr></table></div></body></html>
\ No newline at end of file
+   <title>2.10. Drucker- und Systemverwaltung</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s09.html" title="2.9. Mandanten-, Benutzer- und Gruppenverwaltung"><link rel="next" href="ch02s11.html" title="2.11. E-Mail-Versand aus kivitendo heraus"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.10. Drucker- und Systemverwaltung</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s09.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s11.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.10. Drucker- und Systemverwaltung"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Drucker--Systemverwaltung"></a>2.10. Drucker- und Systemverwaltung</h2></div></div></div><p>Im Administrationsmenü gibt es ferner noch die beiden Menüpunkte
+      Druckeradministration und System.</p><div class="sect2" title="2.10.1. Druckeradministration"><div class="titlepage"><div><div><h3 class="title"><a name="Druckeradministration"></a>2.10.1. Druckeradministration</h3></div></div></div><p>Unter dem Menüpunkt Druckeradministration lassen sich beliebig
+        viele "Druckbefehle" im System verwalten. Diese Befehle werden
+        mandantenweise zugeordnet. Unter Druckerbeschreibung wird der Namen
+        des Druckbefehls festgelegt, der dann in der Druckerauswahl des Belegs
+        angezeigt wird.</p><p>Unter Druckbefehl definiert man den eigentlichen Druckbefehl,
+        der direkt auf dem Webserver ausgeführt wird, bspw. 'lpr -P
+        meinDrucker' oder ein kompletter Pfad zu einem Skript
+        (/usr/local/src/kivitendo/scripts/pdf_druck_in_verzeichnis.sh). Wird
+        ferner noch ein optionales Vorlagenkürzel verwendet, wird dieses
+        Kürzel bei der Auswahl der Druckvorlagendatei mit einem Unterstrich
+        ergänzt, ist bspw. das Kürzel 'epson_drucker' definiert, so wird beim
+        Ausdruck eines Angebots folgende Vorlage geparst:
+        sales_quotation_epson_drucker.tex.</p></div><div class="sect2" title="2.10.2. System sperren / entsperren"><div class="titlepage"><div><div><h3 class="title"><a name="System"></a>2.10.2. System sperren / entsperren</h3></div></div></div><p>Unter dem Menüpunkt System gibt es den Eintrag 'Installation
+        sperren/entsperren'. Setzt man diese Sperre so ist der Zugang zu der
+        gesamten kivitendo Installation gesperrt.</p><p>Falls die Sperre gesetzt ist, erscheint anstelle der
+        Anmeldemaske die Information: 'kivitendo ist momentan zwecks
+        Wartungsarbeiten nicht zugänglich.'.</p><p>Wichtig zu erwähnen ist hierbei noch, dass sich kivitendo
+        automatisch 'sperrt', falls es bei einem Versionsupdate zu einem
+        Datenbankfehler kam. Somit kann hier nicht aus Versehen mit einem
+        inkonsistenten Datenbestand weitergearbeitet werden.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s09.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s11.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.9. Mandanten-, Benutzer- und Gruppenverwaltung&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.11. E-Mail-Versand aus kivitendo heraus</td></tr></table></div></body></html>
\ No newline at end of file
index 8dc96e7..c2ce198 100644 (file)
@@ -1,36 +1,54 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.11. E-Mail-Versand aus kivitendo heraus</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s10.html" title="2.10. Drucker- und Systemverwaltung"><link rel="next" href="ch02s12.html" title="2.12. Drucken mit kivitendo"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.11. E-Mail-Versand aus kivitendo heraus</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s10.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s12.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.11. E-Mail-Versand aus kivitendo heraus"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="config.sending-email"></a>2.11. E-Mail-Versand aus kivitendo heraus</h2></div></div></div><p>kivitendo kann direkt aus dem Programm heraus E-Mails versenden, z.B. um ein Angebot direkt an einen Kunden zu
-      verschicken. Damit dies funktioniert, muss eingestellt werden, über welchen Server die E-Mails verschickt werden sollen. kivitendo
-      unterstützt dabei zwei Mechanismen: Versand über einen lokalen E-Mail-Server (z.B. mit <span class="productname">Postfix</span>™ oder
-      <span class="productname">Exim</span>™, was auch die standardmäßig aktive Methode ist) sowie Versand über einen SMTP-Server (z.B. der des
-      eigenen Internet-Providers).</p><p>Welche Methode und welcher Server verwendet werden, wird über die Konfigurationsdatei <code class="filename">config/kivitendo.conf</code>
-      festgelegt. Dort befinden sich alle Einstellungen zu diesem Thema im Abschnitt '<code class="literal">[mail_delivery]</code>'.</p><div class="sect2" title="2.11.1. Versand über lokalen E-Mail-Server"><div class="titlepage"><div><div><h3 class="title"><a name="config.sending-email.sendmail"></a>2.11.1. Versand über lokalen E-Mail-Server</h3></div></div></div><p>Diese Methode bietet sich an, wenn auf dem Server, auf dem kivitendo läuft, bereits ein funktionsfähiger E-Mail-Server wie
-        z.B. <span class="productname">Postfix</span>™, <span class="productname">Exim</span>™ oder <span class="productname">Sendmail</span>™ läuft.</p><p>Um diese Methode auszuwählen, muss der Konfigurationsparameter '<code class="literal">method = sendmail</code>' gesetzt sein. Dies ist
-        gleichzeitig der Standardwert, falls er nicht verändert wird.</p><p>Um zu kontrollieren, wie das Programm zum Einliefern gestartet wird, dient der Parameter '<code class="literal">sendmail =
-        ...</code>'. Der Standardwert verweist auf das Programm <code class="filename">/usr/bin/sendmail</code>, das bei allen oben genannten
-        E-Mail-Serverprodukten für diesen Zweck funktionieren sollte.</p><p>Die Konfiguration des E-Mail-Servers selber würde den Rahmen dieses sprengen. Hierfür sei auf die Dokumentation des
-        E-Mail-Servers verwiesen.</p></div><div class="sect2" title="2.11.2. Versand über einen SMTP-Server"><div class="titlepage"><div><div><h3 class="title"><a name="config.sending-email.smtp"></a>2.11.2. Versand über einen SMTP-Server</h3></div></div></div><p>Diese Methode bietet sich an, wenn kein lokaler E-Mail-Server vorhanden oder zwar einer vorhanden, dieser aber nicht
-        konfiguriert ist.</p><p>Um diese Methode auszuwählen, muss der Konfigurationsparameter '<code class="literal">method = smtp</code>' gesetzt sein. Die folgenden
+   <title>2.11. E-Mail-Versand aus kivitendo heraus</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s10.html" title="2.10. Drucker- und Systemverwaltung"><link rel="next" href="ch02s12.html" title="2.12. Drucken mit kivitendo"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.11. E-Mail-Versand aus kivitendo heraus</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s10.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s12.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.11. E-Mail-Versand aus kivitendo heraus"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="config.sending-email"></a>2.11. E-Mail-Versand aus kivitendo heraus</h2></div></div></div><p>kivitendo kann direkt aus dem Programm heraus E-Mails versenden,
+      z.B. um ein Angebot direkt an einen Kunden zu verschicken. Damit dies
+      funktioniert, muss eingestellt werden, über welchen Server die E-Mails
+      verschickt werden sollen. kivitendo unterstützt dabei zwei Mechanismen:
+      Versand über einen lokalen E-Mail-Server (z.B. mit
+      <span class="productname">Postfix</span>™ oder <span class="productname">Exim</span>™,
+      was auch die standardmäßig aktive Methode ist) sowie Versand über einen
+      SMTP-Server (z.B. der des eigenen Internet-Providers).</p><p>Welche Methode und welcher Server verwendet werden, wird über die
+      Konfigurationsdatei <code class="filename">config/kivitendo.conf</code>
+      festgelegt. Dort befinden sich alle Einstellungen zu diesem Thema im
+      Abschnitt '<code class="literal">[mail_delivery]</code>'.</p><div class="sect2" title="2.11.1. Versand über lokalen E-Mail-Server"><div class="titlepage"><div><div><h3 class="title"><a name="config.sending-email.sendmail"></a>2.11.1. Versand über lokalen E-Mail-Server</h3></div></div></div><p>Diese Methode bietet sich an, wenn auf dem Server, auf dem
+        kivitendo läuft, bereits ein funktionsfähiger E-Mail-Server wie z.B.
+        <span class="productname">Postfix</span>™, <span class="productname">Exim</span>™
+        oder <span class="productname">Sendmail</span>™ läuft.</p><p>Um diese Methode auszuwählen, muss der Konfigurationsparameter
+        '<code class="literal">method = sendmail</code>' gesetzt sein. Dies ist
+        gleichzeitig der Standardwert, falls er nicht verändert wird.</p><p>Um zu kontrollieren, wie das Programm zum Einliefern gestartet
+        wird, dient der Parameter '<code class="literal">sendmail = ...</code>'. Der
+        Standardwert verweist auf das Programm
+        <code class="filename">/usr/bin/sendmail</code>, das bei allen oben genannten
+        E-Mail-Serverprodukten für diesen Zweck funktionieren sollte.</p><p>Die Konfiguration des E-Mail-Servers selber würde den Rahmen
+        dieses sprengen. Hierfür sei auf die Dokumentation des E-Mail-Servers
+        verwiesen.</p></div><div class="sect2" title="2.11.2. Versand über einen SMTP-Server"><div class="titlepage"><div><div><h3 class="title"><a name="config.sending-email.smtp"></a>2.11.2. Versand über einen SMTP-Server</h3></div></div></div><p>Diese Methode bietet sich an, wenn kein lokaler E-Mail-Server
+        vorhanden oder zwar einer vorhanden, dieser aber nicht konfiguriert
+        ist.</p><p>Um diese Methode auszuwählen, muss der Konfigurationsparameter
+        '<code class="literal">method = smtp</code>' gesetzt sein. Die folgenden
         Parameter dienen dabei der weiteren Konfiguration:</p><div class="variablelist"><dl><dt><span class="term">
                      <code class="varname">hostname</code>
-                  </span></dt><dd><p>Name oder IP-Adresse des SMTP-Servers. Standardwert: '<code class="literal">localhost</code>'</p></dd><dt><span class="term">
+                  </span></dt><dd><p>Name oder IP-Adresse des SMTP-Servers. Standardwert:
+              '<code class="literal">localhost</code>'</p></dd><dt><span class="term">
                      <code class="varname">port</code>
-                  </span></dt><dd><p>Portnummer. Der Standardwert hängt von der verwendeten Verschlüsselungsmethode ab. Gilt '<code class="literal">security =
-            none</code>' oder '<code class="literal">security = tls</code>', so ist 25 die Standardportnummer. Für '<code class="literal">security =
-            ssl</code>' ist 465 die Portnummer. Muss normalerweise nicht geändert werden.</p></dd><dt><span class="term">
+                  </span></dt><dd><p>Portnummer. Der Standardwert hängt von der verwendeten
+              Verschlüsselungsmethode ab. Gilt '<code class="literal">security =
+              none</code>' oder '<code class="literal">security = tls</code>', so ist
+              25 die Standardportnummer. Für '<code class="literal">security =
+              ssl</code>' ist 465 die Portnummer. Muss normalerweise nicht
+              geändert werden.</p></dd><dt><span class="term">
                      <code class="varname">security</code>
-                  </span></dt><dd><p>Wahl der zu verwendenden Verschlüsselung der Verbindung mit dem Server. Standardwert ist
-            '<code class="literal">none</code>', wodurch keine Verschlüsselung verwendet wird. Mit '<code class="literal">tls</code>' wird TLS-Verschlüsselung
-            eingeschaltet, und mit '<code class="literal">ssl</code>' wird Verschlüsselung via SSL eingeschaltet. Achtung: Für
-            '<code class="literal">tls</code>' und '<code class="literal">ssl</code>' werden zusätzliche Perl-Module benötigt (siehe unten).</p></dd><dt><span class="term">
-                     <code class="varname">login</code> und <code class="varname">password</code>
-                  </span></dt><dd><p>Falls der E-Mail-Server eine Authentifizierung verlangt, so können mit diesen zwei Parametern der Benutzername
-            und das Passwort angegeben werden. Wird Authentifizierung verwendet, so sollte aus Sicherheitsgründen auch eine Form von
-            Verschlüsselung aktiviert werden.</p></dd></dl></div><p>Wird Verschlüsselung über TLS oder SSL aktiviert, so werden zusätzliche Perl-Module benötigt. Diese sind:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>TLS-Verschlüsselung: Modul <code class="literal">Net::SSLGlue</code> (Debian-Paketname
-          <code class="literal">libnet-sslglue-perl</code>, Fedora Core: <code class="literal">perl-Net-SSLGlue</code>, openSUSE:
-          <code class="literal">perl-Net-SSLGlue</code>
-                  </p></li><li class="listitem"><p>SSL-Verschlüsselung: Modul <code class="literal">Net::SMTP::SSL</code> (Debian-Paketname
-          <code class="literal">libnet-smtp-ssl-perl</code>, Fedora Core: <code class="literal">perl-Net-SMTP-SSL</code>, openSUSE:
-          <code class="literal">perl-Net-SMTP-SSL</code>
-                  </p></li></ul></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s10.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s12.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.10. Drucker- und Systemverwaltung&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.12. Drucken mit kivitendo</td></tr></table></div></body></html>
\ No newline at end of file
+                  </span></dt><dd><p>Wahl der zu verwendenden Verschlüsselung der Verbindung
+              mit dem Server. Standardwert ist '<code class="literal">none</code>',
+              wodurch keine Verschlüsselung verwendet wird. Mit
+              '<code class="literal">tls</code>' wird TLS-Verschlüsselung eingeschaltet,
+              und mit '<code class="literal">ssl</code>' wird Verschlüsselung via SSL
+              eingeschaltet. Achtung: Für '<code class="literal">tls</code>' und
+              '<code class="literal">ssl</code>' werden zusätzliche Perl-Module benötigt
+              (siehe unten).</p></dd><dt><span class="term">
+                     <code class="varname">login</code> und
+            <code class="varname">password</code>
+                  </span></dt><dd><p>Falls der E-Mail-Server eine Authentifizierung verlangt,
+              so können mit diesen zwei Parametern der Benutzername und das
+              Passwort angegeben werden. Wird Authentifizierung verwendet, so
+              sollte aus Sicherheitsgründen auch eine Form von Verschlüsselung
+              aktiviert werden.</p></dd></dl></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s10.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s12.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.10. Drucker- und Systemverwaltung&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.12. Drucken mit kivitendo</td></tr></table></div></body></html>
\ No newline at end of file
index 21e5998..8c7835f 100644 (file)
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.12. Drucken mit kivitendo</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s11.html" title="2.11. E-Mail-Versand aus kivitendo heraus"><link rel="next" href="ch02s13.html" title="2.13. OpenDocument-Vorlagen"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.12. Drucken mit kivitendo</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s11.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s13.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.12. Drucken mit kivitendo"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Drucken-mit-kivitendo"></a>2.12. Drucken mit kivitendo</h2></div></div></div><p>Das Drucksystem von kivitendo benutzt von Haus aus LaTeX-Vorlagen.  Um drucken zu können, braucht der Server ein geeignetes
-      LaTeX System. Am einfachsten ist dazu eine <code class="literal">texlive</code> Installation. Unter debianoiden Betriebssystemen installiert man
-      die Pakete mit:</p><p>
-            </p><pre class="programlisting">aptitude install texlive-base-bin texlive-latex-recommended texlive-fonts-recommended \
-  texlive-latex-extra texlive-lang-german texlive-generic-extra</pre><p>
-         </p><p>TODO: RPM-Pakete.</p><p>kivitendo bringt drei alternative Vorlagensätze mit:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>RB</p></li><li class="listitem"><p>f-tex</p></li><li class="listitem"><p>rev-odt</p></li></ul></div><p>Der ehemalige Druckvorlagensatz "Standard" wurde mit der Version 3.3 entfernt, da er nicht mehr gepflegt wurde.</p><div class="sect2" title="2.12.1. Vorlagenverzeichnis anlegen"><div class="titlepage"><div><div><h3 class="title"><a name="Vorlagenverzeichnis-anlegen"></a>2.12.1. Vorlagenverzeichnis anlegen</h3></div></div></div><p>Es lässt sich ein initialer Vorlagensatz erstellen. Die LaTeX-System-Abhängigkeiten hierfür kann man prüfen mit:</p><pre class="programlisting">./scripts/installation_check.pl -lv</pre><p>Der Angemeldete Benutzer muss in einer Gruppe sein, die über das
-              Recht "Konfiguration -&gt; Mandantenverwaltung" verfügt. Siehe auch <a class="xref" href="ch02s09.html#Gruppen-anlegen" title="2.9.4. Gruppen anlegen">Abschnitt&nbsp;2.9.4, „Gruppen anlegen“</a>.
-        </p><p>Im Userbereich lässt sich unter:
-        "<span class="guimenu">System</span> -&gt;
-        <span class="guisubmenu">Mandantenverwaltung</span> -&gt; <span class="guimenuitem">Verschiedenes</span>" die Option
-        "Neue Druckvorlagen aus Vorlagensatz erstellen" auswählen.</p><div class="orderedlist"><ol class="orderedlist" type="1"><li class="listitem"><p>
-                     <code class="option">Vorlagen auswählen</code>: Wählen Sie hier den Vorlagensatz aus, der kopiert werden soll
-          (<code class="filename">RB</code>, <code class="filename">f-tex</code> oder <code class="filename">odt-rev</code>.)</p></li><li class="listitem"><p>
-                     <code class="option">Neuer Name</code>: Der Verzeichnisname für den neuen Vorlagensatz. Dieser kann im Rahmen der üblichen
-          Bedingungen für Verzeichnisnamen frei gewählt werden.</p></li></ol></div><p>Nach dem Speichern wird das Vorlagenverzeichnis angelegt und ist für den aktuellen Mandanten ausgewählt.
-           Der gleiche Vorlagensatz kann, wenn er mal angelegt ist, bei mehreren Mandanten verwendet werden.
-           Eventuell müssen Anpassungen (Logo, Erscheinungsbild, etc) noch vorgenommen werden. Den Ordner findet man im Dateisystem unter
-           <code class="filename">./templates/[Neuer Name]</code>
-            </p></div><div class="sect2" title="2.12.2. Der Druckvorlagensatz RB"><div class="titlepage"><div><div><h3 class="title"><a name="Vorlagen-RB"></a>2.12.2. Der Druckvorlagensatz RB</h3></div></div></div><p>Hierbei handelt es sich um einen vollständigen LaTeX Dokumentensatz mit alternativem Design. Die odt oder html-Varianten sind nicht gepflegt.</p><p>Die konzeptionelle Idee der Vorlagen wird <a class="ulink" href="http://www.kivitendo-support.de/vortraege/Lx-Office%20Anwendertreffen%20LaTeX-Druckvorlagen-Teil3-finale.pdf" target="_top">hier</a>
-          auf Folie 5 bis 10 vorgestellt. Informationen zur Anpassung an die eigenen Firmendaten finden sich in der Datei Readme.tex im Vorlagenverzeichnis.</p><p>Eine kurze Übersicht der Features:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Mehrsprachenfähig, mit Deutscher und Englischer Übersetzung</p></li><li class="listitem"><p>Zentrale Konfigurationsdateien, die für alle Belege benutzt werden, z.B. für Kopf- und Fußzeilen, und Infos wie Bankdaten</p></li><li class="listitem"><p>mehrere vordefinierte Varianten für Logos/Hintergrundbilder</p></li><li class="listitem"><p>Berücksichtigung für Steuerzonen "EU mit USt-ID Nummer" oder "Außerhalb EU"</p></li></ul></div></div><div class="sect2" title="2.12.3. f-tex"><div class="titlepage"><div><div><h3 class="title"><a name="f-tex"></a>2.12.3. f-tex</h3></div></div></div><p>Ein Vorlagensatz, der in wenigen Minuten alle Dokumente zur Verfügung stellt.</p><div class="sect3" title="2.12.3.1. Feature-Übersicht"><div class="titlepage"><div><div><h4 class="title"><a name="f-tex-Feature-%C3%9Cbersicht"></a>2.12.3.1. Feature-Übersicht</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Keine Redundanz. Es wird ein- und dieselbe LaTeX-Vorlage für alle briefartigen Dokumente verwendet. Also
-            Angebot, Rechnung, Proformarechnung, Lieferschein, aber eben nicht für Paketaufkleber etc.</p></li><li class="listitem"><p>Leichte Anpassung an das Firmen-Layout durch Verwendung eines Hintergrund-PDFs. Dieses kann leicht mit dem
-            eigenen Lieblingsprogramm erstellt werden (Openoffice, Inkscape, Gimp, Adobe*)</p></li><li class="listitem"><p>Hintergrund-PDF umschaltbar auf "nur erste Seite" (Standard) oder "alle Seiten" (Option
-            "<code class="option">bgPdfFirstPageOnly</code>" in Datei <code class="filename">letter.lco</code>)</p></li><li class="listitem"><p>Hintergrund-PDF für Ausdruck auf bereits bedrucktem Briefpapier abschaltbar. Es wird dann nur bei per E-Mail
-            versendeten Dokumenten eingebunden (Option "<code class="option">bgPdfEmailOnly</code>" in Datei
-            <code class="filename">letter.lco</code>).</p></li><li class="listitem"><p>Nutzung der Layout-Funktionen von LaTeX für Seitenumbruch, Wiederholung von Kopfzeilen, Zwischensummen
-            etc. (danke an Kai-Martin Knaak für die Vorarbeit)</p></li><li class="listitem"><p>Anzeige des Empfängerlandes im Adressfeld nur, wenn es vom Land des eigenen Unternehmens abweicht (also die
-            Rechnung das Land verlässt).</p></li><li class="listitem"><p>Multisprachfähig leicht um weitere Sprachen zu erweitern, alle Übersetzungen in der Datei
-            <code class="filename">translatinos.tex</code>.</p></li><li class="listitem"><p>Auflistung von Bruttopreisen für Endverbraucher.</p></li></ul></div></div><div class="sect3" title="2.12.3.2. Die Installation"><div class="titlepage"><div><div><h4 class="title"><a name="f-tex-Installation"></a>2.12.3.2. Die Installation</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Vorlagenverzeichnis mit Option f-tex anlegen, siehe: <a class="xref" href="ch02s12.html#Vorlagenverzeichnis-anlegen" title="2.12.1. Vorlagenverzeichnis anlegen">Vorlagenverzeichnis anlegen</a>. Das
-            Vorlagensystem funktioniert jetzt schon, hat allerdings noch einen Beispiel-Briefkopf.</p></li><li class="listitem"><p>Erstelle eine pdf-Hintergrund Datei und verlinke sie nach <code class="filename">./letter_head.pdf</code>.</p></li><li class="listitem"><p>Editiere den Bereich "<code class="option">settings</code>" in der datei <code class="filename">letter.lco</code>.</p></li></ul></div><p>oder etwas detaillierter:</p><p>
-            Es wird eine Datei <code class="filename">sample.lco</code> erstellt und diese nach <code class="filename">letter.lco</code> verlinkt.  Eigentlich
-            ist dies die Datei die für die firmenspezifischen Anpassungen gedacht ist.  Da die Einstiegshürde in LaTeX nicht ganz niedrig
-            ist, wird in dieser Datei auf ein Hintergrund-PDF verwiesen. Ich empfehle über dieses PDF die persönlichen Layoutanpassungen
-            vorzunehmen und <code class="filename">sample.lco</code> unverändert zu lassen. Die Anpassung über eine
-            <code class="filename">*.lco</code>-Datei, die letztlich auf <code class="filename">letter.lco</code> verlinkt ist ist aber auch möglich.
-          </p><p>
-            Es wird eine Datei <code class="filename">sample_head.pdf</code> mit ausgeliefert, diese wird nach <code class="filename">letter_head.pdf</code>
-            verlinkt. Damit gibt es schon mal eine funktionsfähige Vorlage. Schau Dir nach Abschluss der Installation die Datei
-            <code class="filename">sample_head.pdf</code> an und erstelle ein entsprechendes PDF passend zum Briefkopf Deiner Firma, diese dann im
-            Template Verzeichniss ablegen und statt <code class="filename">sample_head.pdf</code> nach <code class="filename">letter_head.pdf</code>
-            verlinken.
-          </p><p>
-            Letzlich muss <code class="filename">letter_head.pdf</code> auf das passende Hintergrund-PDF verweisen, welches gewünschten Briefkopf
-            enthält.
-          </p><p>
-            Es wird eine Datei <code class="filename">mydata.tex.example</code> ausgeliefert, die nach <code class="filename">mytdata.tex</code> verlinkt
-            ist. Bei verwendetem Hintergrund-PDF wird nur der Eintrag für das Land verwendet. Die Datei muss also nicht angefasst
-            werden. Die anderen Werte sind für das Modul 'lp' (Label Print in erp - zur Zeit nicht im öffentlichen Zweig).
-          </p><p>
-            Alle Anpassungen zum Briefkopf, Fusszeilen, Firmenlogos, etc.  sollten über die Hintergrund-PDF-Datei oder die
-            <code class="filename">*.lco</code>-Datei erfolgen.
-          </p></div><div class="sect3" title="2.12.3.3. f-tex Funktionsübersicht"><div class="titlepage"><div><div><h4 class="title"><a name="f-tex-Funktions%C3%BCbersicht"></a>2.12.3.3. f-tex Funktionsübersicht</h4></div></div></div><p>
-            Das Konzept von kivitendo sieht vor, für jedes Dokument (Auftragsbestätigung, Lieferschein, Rechnung, etc.) eine LaTeX-Vorlage
-            vorzuhalten, dies ist sehr wartungsunfreundlich. Auch das Einlesen einer einheitlichen Quelle für den Briefkopf bringt nur
-            bedingte Vorteile, da hier leicht die Pflege der Artikel-Tabellen aus dem Ruder läuft. Bei dem vorliegenden Ansatz wird für alle
-            briefartigen Dokumente mit Artikel-Tabellen eine einheitliche LaTeX-Vorlage verwendet, welche über Codeweichen die
-            Besonderheiten der jeweiligen Dokumente berücksichtigt:
-          </p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Tabellen mit oder ohne Preis</p></li><li class="listitem"><p>Sprache der Tabellenüberschriften etc.</p></li><li class="listitem"><p>Anpassung der Bezugs-Zeile (z.B. Rechnungsnummer versus Angebotsnummer)</p></li><li class="listitem"><p>Darstellung von Brutto oder Netto-Preisen in der Auflistung (Endverbraucher versus gewerblicher
-            Kunde)</p></li></ul></div><p>Nachteil:</p><p>
-             LaTeX hat ohnehin eine sehr steile Lehrnkurve. Die Datei <code class="filename">letter.tex</code> ist sehr komplex und verstärkt damit
-             diesen Effekt noch einmal erheblich.  Wer LaTeX-Erfahrung hat, oder geübt ist Scriptsparachen nachzuvollziehen kann natürlich
-             auch innerhalb der Tabellendarstellung gut persönliche Anpassungen vornehmen. Aber man kann sich hier bei Veränderungen sehr
-             schnell heftig in den Fuss schiessen.
-           </p><p>Wer nicht so tief in die Materie einsteigen will oder leicht zu frustrieren ist, sollte sein Hintergrund-PDF auf Basis der
-           mitglieferten Datei <code class="filename">sample_head.pdf</code> erstellen, und sich an der Form der dargestellten Tabellen, wie sie
-           ausgeliefert werden, erfreuen.
-           </p><p>Kleiner Tipp: Nicht zu viel auf einmal wollen, lieber kleine, kontinuierliche Schritte gehen.</p></div><div class="sect3" title="2.12.3.4. Bruttopreise für Endverbraucher"><div class="titlepage"><div><div><h4 class="title"><a name="f-tex-Bruttopreise"></a>2.12.3.4. Bruttopreise für Endverbraucher</h4></div></div></div><p>Der auszuweisende Bruttopreis wird innerhalb der LaTeX-Umgebung berechnet. Es gibt zwar ein Feld, um bei Aufträgen "alle
-                Preise Brutto" auszuwählen, aber:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>hierfür müssen die Preise auch in Brutto in der Datenbank stehen (ja - das lässt sich über die Preisgruppen und die
-              Zuordung einer Default-Preisgruppe handhaben)</p></li><li class="listitem"><p>man darf beim Anlegen des Vorgangs nicht vergessen, dieses Häkchen zu setzen.  (Das ist in der Praxis, wenn man sowohl
-              Endverbraucher als auch Gewerbekunden beliefert, der eigentliche Knackpunkt)</p></li></ul></div><p>
-            Es gibt mit f-tex eine weitere Alternative. Die Information ob Brutto oder Nettorechnung wird mit den Zahlarten
-            verknüpft. Zahlarten bei denen Rechnungen, Angebote, etc, in Brutto ausgegeben werden sollen, enden mit "_E" (für
-            Endverbraucher). Falls identische Zahlarten für Gewerbekunden und Endverbraucher vorhanden sind, legt man diese einfach doppelt
-            an (einmal mit der Namensendung "_E"). Gewinn:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Die Entscheidung, ob Nettopreise ausgewiesen werden, ist nicht mehr fix mit einer Preisliste verbunden.</p></li><li class="listitem"><p>Die Default-Zahlart kann im Kundendatensatz hinterlegt werden, und man muss nicht mehr daran denken, "alle Preise
-            Netto" auszuwählen.</p></li><li class="listitem"><p>Die Entscheidung, ob Netto- oder Bruttopreise ausgewiesen werden, kann direkt beim Drucken revidiert werden,
-            ohne dass sich der Auftragswert ändert.</p></li></ul></div></div><div class="sect3" title="2.12.3.5. Lieferadressen"><div class="titlepage"><div><div><h4 class="title"><a name="f-tex-lieferadressen"></a>2.12.3.5. Lieferadressen</h4></div></div></div><p>In Lieferscheinen kommen <code class="varname">shipto*</code>-Variablen im Adressfeld zum Einsatz. Wenn die
-          <code class="varname">shipto*</code>-Variable leer ist, wird die entsprechende Adressvariable eingesetzt.  Wenn also die Lieferadresse in
-          Straße, Hausnummer und Ort abweicht, müssen auch nur diese Felder in der Lieferadresse ausgefüllt werden. Für den Firmenname wird
-          der Wert der Hauptadresse angezeigt.
-          </p></div></div><div class="sect2" title="2.12.4. Der Druckvorlagensatz rev-odt"><div class="titlepage"><div><div><h3 class="title"><a name="Vorlagen-rev-odt"></a>2.12.4. Der Druckvorlagensatz rev-odt</h3></div></div></div><p>Hierbei handelt es sich um einen Dokumentensatz der mit odt-Vorlagen erstellt wurde. Es gibt in dem Verzeichnis eine Readme-Datei, die eventuell aktueller als die Dokumentation hier ist.
-Die odt-Vorlagen in diesem Verzeichnis "rev-odt" wurden von revamp-it, Zürich erstellt
-und werden laufend aktualisiert. Ein paar der Formulierungen in den Druckvorlagen entsprechen dem Schweizer Sprachgebrauch, z.B. "Offerte" oder "allfällig".
-        </p><p>
-Hinweis zum Einsatz des Feldes "Land" bei den Stammdaten für KundInnen und LieferantInnen,
-sowie bei Lieferadressen:
-Die in diesem Vorlagensatz vorhandenen Vorlagen erwarten für "Land" das entsprechende
-Kürzel, das in Adressen vor die Postleitzahl gesetzt wird.
-Das Feld kann auch komplett leer bleiben.
-Wer dies anders handhaben möchte, muss die Vorlagen entsprechend anpassen.
-</p><p>
-odt-Vorlagen können mit LibreOffice oder OpenOffice editiert
-und den eigenen Bedürfnissen angepasst werden.
-Wichtig beim Editieren von if-Blöcken ist, dass immer der gesamte Block
-überschrieben werden muss und nicht nur Teile davon, da dies sonst oft
-zu einer odt-Datei führt, die vom Parser nicht korrekt gelesen werden kann.
-</p><p>
-Zur Zeit gibt es in kivitendo noch keine Möglichkeit, odt-Vorlagen bei Mahnungen
-einzusetzen. Entsprechende Vorlagen sind deshalb nicht vorhanden.
-</p><p>
-Inwieweit es möglich ist, für die in Version 3.2.0 neu eingeführten Pflichtenhefte
-odt-Vorlagen zu erstellen, sind wir am abklären.
-Wenn dies möglich ist, werden wir in Zukunft auch eine odt-Vorlage für Pflichtenhefte
-in diesem Vorlagensatz zur Verfügung stellen.
-</p><p>
-Fehlermeldungen, Anregungen und Wünsche bitte senden an:
-empfang@revamp-it.ch
-</p></div><div class="sect2" title="2.12.5. Allgemeine Hinweise zu LaTeX Vorlagen"><div class="titlepage"><div><div><h3 class="title"><a name="allgemeine-hinweise-zu-latex"></a>2.12.5. Allgemeine Hinweise zu LaTeX Vorlagen</h3></div></div></div><p>In den allermeisten Installationen sollte das Drucken jetzt schon
-        funktionieren. Sollte ein Fehler auftreten, wirft TeX sehr lange
-        Fehlerbeschreibungen, der eigentliche Fehler ist immer die erste Zeile,
-        die mit einem Ausrufezeichen anfängt. Häufig auftretende Fehler sind zum
-        Beispiel:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>! LaTeX Error: File `eurosym.sty' not found. Die entsprechende
-            LaTeX-Bibliothek wurde nicht gefunden. Das tritt vor allem bei
-            Vorlagen aus der Community auf. Installieren Sie die entsprechenden
-            Pakete.</p></li><li class="listitem"><p>! Package inputenc Error: Unicode char \u8:... set up for
+   <title>2.12. Drucken mit kivitendo</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s11.html" title="2.11. E-Mail-Versand aus kivitendo heraus"><link rel="next" href="ch02s13.html" title="2.13. OpenDocument-Vorlagen"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.12. Drucken mit kivitendo</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s11.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s13.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.12. Drucken mit kivitendo"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="Drucken-mit-kivitendo"></a>2.12. Drucken mit kivitendo</h2></div></div></div><p>Das Drucksystem von kivitendo benutzt von Haus aus LaTeX-Vorlagen.
+      Um drucken zu können, braucht der Server ein geeignetes LaTeX System. Am
+      einfachsten ist dazu eine <code class="literal">texlive</code> Installation. Unter
+      debianoiden Betriebssystemen installiert man die Pakete mit:</p><p>
+            </p><pre class="programlisting">apt install texlive-base-bin texlive-latex-recommended texlive-fonts-recommended \
+  texlive-latex-extra texlive-lang-german ghostscript</pre><p>
+         </p><p>Für Fedora benötigen Sie die folgenden Pakete:</p><p>
+            </p><pre class="programlisting">dnf install texlive-collection-latex texlive-collection-latexextra \
+  texlive-collection-latexrecommended texlive-collection-langgerman \
+  texlive-collection-langenglish</pre><p>
+         </p><p>Für openSUSE benötigen Sie die folgenden Pakete:</p><p>
+            </p><pre class="programlisting">zypper install texlive-collection-latex texlive-collection-latexextra \
+  texlive-collection-latexrecommended texlive-collection-langgerman \
+  texlive-collection-langenglish</pre><p>
+         </p><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left">Anmerkung</th></tr><tr><td align="left" valign="top"><p>kivitendo erwartet eine aktuelle TeX Live Umgebung, um PDF/A zu erzeugen. Aktuelle Distributionen von 2020 erfüllen diese. Überprüfbar ist dies mit dem Aufruf des installation_check.pl mit Parameter -l:</p><p>
+               </p><pre class="programlisting">scripts/installations_check.pl -l</pre><p>
+            </p></td></tr></table></div><p>kivitendo bringt drei alternative Vorlagensätze mit:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>RB</p></li><li class="listitem"><p>marei</p></li><li class="listitem"><p>rev-odt</p></li></ul></div><p>Der ehemalige Druckvorlagensatz "f-tex" wurde mit der Version
+      3.5.6 entfernt, da er nicht mehr gepflegt wird.</p><div class="sect2" title="2.12.1. Vorlagenverzeichnis anlegen"><div class="titlepage"><div><div><h3 class="title"><a name="Vorlagenverzeichnis-anlegen"></a>2.12.1. Vorlagenverzeichnis anlegen</h3></div></div></div><p>Es lässt sich ein initialer Vorlagensatz erstellen. Die
+        LaTeX-System-Abhängigkeiten hierfür kann man prüfen mit:</p><pre class="programlisting">./scripts/installation_check.pl -lv</pre><p>Der Angemeldete Benutzer muss in einer Gruppe sein, die über das
+        Recht "Konfiguration -&gt; Mandantenverwaltung" verfügt. Siehe auch
+        <a class="xref" href="ch02s09.html#Gruppen-anlegen" title="2.9.4. Gruppen anlegen">Abschnitt&nbsp;2.9.4, „Gruppen anlegen“</a>.</p><p>Im Userbereich lässt sich unter: "<span class="guimenu">System</span>
+        -&gt; <span class="guisubmenu">Mandantenverwaltung</span> -&gt;
+        <span class="guimenuitem">Verschiedenes</span>" die Option "Neue
+        Druckvorlagen aus Vorlagensatz erstellen" auswählen.</p><div class="orderedlist"><ol class="orderedlist" type="1"><li class="listitem"><p>
+                     <code class="option">Vorlagen auswählen</code>: Wählen Sie hier den
+            Vorlagensatz aus, der kopiert werden soll
+            (<code class="filename">RB</code>, <code class="filename">marei</code> oder
+            <code class="filename">odt-rev</code>.)</p></li><li class="listitem"><p>
+                     <code class="option">Neuer Name</code>: Der Verzeichnisname für den
+            neuen Vorlagensatz. Dieser kann im Rahmen der üblichen Bedingungen
+            für Verzeichnisnamen frei gewählt werden.</p></li></ol></div><p>Nach dem Speichern wird das Vorlagenverzeichnis angelegt und ist
+        für den aktuellen Mandanten ausgewählt. Der gleiche Vorlagensatz kann,
+        wenn er mal angelegt ist, bei mehreren Mandanten verwendet werden.
+        Eventuell müssen Anpassungen (Logo, Erscheinungsbild, etc) noch
+        vorgenommen werden. Den Ordner findet man im Dateisystem unter
+        <code class="filename">./templates/[Neuer Name]</code>
+            </p></div><div class="sect2" title="2.12.2. Der Druckvorlagensatz RB"><div class="titlepage"><div><div><h3 class="title"><a name="Vorlagen-RB"></a>2.12.2. Der Druckvorlagensatz RB</h3></div></div></div><p>Hierbei handelt es sich um einen vollständigen LaTeX
+        Dokumentensatz mit alternativem Design. Die odt oder html-Varianten
+        sind nicht gepflegt.</p><p>Die konzeptionelle Idee der Vorlagen wird <a class="ulink" href="http://www.kivitendo-support.de/vortraege/Lx-Office%20Anwendertreffen%20LaTeX-Druckvorlagen-Teil3-finale.pdf" target="_top">hier</a>
+        auf Folie 5 bis 10 vorgestellt. Informationen zur Anpassung an die
+        eigenen Firmendaten finden sich in der Datei Readme.tex im
+        Vorlagenverzeichnis.</p><p>Eine kurze Übersicht der Features:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Mehrsprachenfähig, mit Deutscher und Englischer
+            Übersetzung</p></li><li class="listitem"><p>Zentrale Konfigurationsdateien, die für alle Belege benutzt
+            werden, z.B. für Kopf- und Fußzeilen, und Infos wie
+            Bankdaten</p></li><li class="listitem"><p>mehrere vordefinierte Varianten für
+            Logos/Hintergrundbilder</p></li><li class="listitem"><p>Berücksichtigung für Steuerzonen "EU mit USt-ID Nummer" oder
+            "Außerhalb EU"</p></li></ul></div></div><div class="sect2" title="2.12.3. Der Druckvorlagensatz rev-odt"><div class="titlepage"><div><div><h3 class="title"><a name="Vorlagen-rev-odt"></a>2.12.3. Der Druckvorlagensatz rev-odt</h3></div></div></div><p>Hierbei handelt es sich um einen Dokumentensatz der mit
+        odt-Vorlagen erstellt wurde. Es gibt in dem Verzeichnis eine
+        Readme-Datei, die eventuell aktueller als die Dokumentation hier ist.
+        Die odt-Vorlagen in diesem Verzeichnis "rev-odt" wurden von revamp-it,
+        Zürich erstellt und werden laufend aktualisiert. Ein paar der
+        Formulierungen in den Druckvorlagen entsprechen dem Schweizer
+        Sprachgebrauch, z.B. "Offerte" oder "allfällig".</p><p>Hinweis zum Einsatz des Feldes "Land" bei den Stammdaten für
+        KundInnen und LieferantInnen, sowie bei Lieferadressen: Die in diesem
+        Vorlagensatz vorhandenen Vorlagen erwarten für "Land" das
+        entsprechende Kürzel, das in Adressen vor die Postleitzahl gesetzt
+        wird. Das Feld kann auch komplett leer bleiben. Wer dies anders
+        handhaben möchte, muss die Vorlagen entsprechend anpassen.</p><p>odt-Vorlagen können mit LibreOffice oder OpenOffice editiert und
+        den eigenen Bedürfnissen angepasst werden. Wichtig beim Editieren von
+        if-Blöcken ist, dass immer der gesamte Block überschrieben werden muss
+        und nicht nur Teile davon, da dies sonst oft zu einer odt-Datei führt,
+        die vom Parser nicht korrekt gelesen werden kann.</p><p>Mahnungen können unter folgenden Einschränkungen mit den
+        odt-Vorlagen im Vorlagensatz rev-odt erzeugt werden:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>als Druckoption steht nur 'PDF(OpenDocument/OASIS)' zur
+            Verfügung, das heisst, die Mahnungen werden als PDF-Datei
+            ausgegeben.</p></li><li class="listitem"><p>für jede Rechnung muss eine eigene Mahnung erzeugt werden
+            (auch wenn bei einzelnen KundInnen mehrere überfällige Rechnungen
+            vorhanden sind).</p></li></ul></div><p>Mehrere Mahnungen für eine Kundin / einen Kunden werden zu einer
+        PDF-Datei zusammengefasst</p><p>Die Vorlagen zahlungserinnerung.odt sowie mahnung.odt sind für
+        das Erstellen einer Zahlungserinnerung bzw. Mahnung selbst vorgesehen,
+        die Vorlage mahnung_invoice.odt für das Erstellen einer Rechnung über
+        die verrechneten Mahngebühren und Verzugszinsen.</p><p>Zur Zeit gibt es in kivitendo noch keine Möglichkeit,
+        odt-Vorlagen bei Briefen und Pflichtenheften einzusetzen.
+        Entsprechende Vorlagen sind deshalb nicht vorhanden.</p><p>Fehlermeldungen, Anregungen und Wünsche bitte senden an:
+        empfang@revamp-it.ch</p></div><div class="sect2" title="2.12.4. Allgemeine Hinweise zu LaTeX Vorlagen"><div class="titlepage"><div><div><h3 class="title"><a name="allgemeine-hinweise-zu-latex"></a>2.12.4. Allgemeine Hinweise zu LaTeX Vorlagen</h3></div></div></div><p>In den allermeisten Installationen sollte das Drucken jetzt
+        schon funktionieren. Sollte ein Fehler auftreten, wirft TeX sehr lange
+        Fehlerbeschreibungen, der eigentliche Fehler ist immer die erste
+        Zeile, die mit einem Ausrufezeichen anfängt. Häufig auftretende Fehler
+        sind zum Beispiel:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>! LaTeX Error: File `eurosym.sty' not found. Die
+            entsprechende LaTeX-Bibliothek wurde nicht gefunden. Das tritt vor
+            allem bei Vorlagen aus der Community auf. Installieren Sie die
+            entsprechenden Pakete.</p></li><li class="listitem"><p>! Package inputenc Error: Unicode char \u8:... set up for
             use with LaTeX. Dieser Fehler tritt auf, wenn sie versuchen mit
             einer Standardinstallation exotische utf8 Zeichen zu drucken.
             TeXLive unterstützt von Haus nur romanische Schriften und muss mit
-            diversen Tricks dazu gebracht werden andere Zeichen zu akzeptieren.
-            Adere TeX Systeme wie XeTeX schaffen hier Abhilfe.</p></li></ul></div><p>Wird gar kein Fehler angezeigt, sondern nur der Name des Templates,
-        heißt das normalerweise, dass das LaTeX Binary nicht gefunden wurde.
-        Prüfen Sie den Namen in der Konfiguration (Standard:
+            diversen Tricks dazu gebracht werden andere Zeichen zu
+            akzeptieren. Adere TeX Systeme wie XeTeX schaffen hier
+            Abhilfe.</p></li></ul></div><p>Wird gar kein Fehler angezeigt, sondern nur der Name des
+        Templates, heißt das normalerweise, dass das LaTeX Binary nicht
+        gefunden wurde. Prüfen Sie den Namen in der Konfiguration (Standard:
         <code class="literal">pdflatex</code>), und stellen Sie sicher, dass pdflatex
         (oder das von Ihnen verwendete System) vom Webserver ausgeführt werden
-        darf.</p><p>Wenn sich das Problem nicht auf Grund der Ausgabe im Webbrowser verifizieren lässt:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p> editiere [kivitendo-home]/config/kivitendo.conf und ändere "keep_temp_files" auf 1</p><p>
+        darf.</p><p>Wenn sich das Problem nicht auf Grund der Ausgabe im Webbrowser
+        verifizieren lässt:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>editiere [kivitendo-home]/config/kivitendo.conf und ändere
+            "keep_temp_files" auf 1</p><p>
                      </p><pre class="programlisting">keep_temp_files = 1;</pre><p>
                   </p></li><li class="listitem"><p>bei fastcgi oder mod_perl den Webserver neu Starten</p></li><li class="listitem"><p>Nochmal einen Druckversuch im Webfrontend auslösen</p></li><li class="listitem"><p>wechsel in das users Verzeichnis von kivitendo</p><p>
                      </p><pre class="programlisting">cd [kivitendo-home]/users</pre><p>
                   </p></li><li class="listitem"><p>LaTeX Suchpfad anpassen:</p><p>
                      </p><pre class="programlisting">export TEXINPUTS=".:[kivitendo-home]/templates/[aktuelles_template_verzeichniss]:"</pre><p>
-                  </p></li><li class="listitem"><p>Finde heraus,  welche Datei kivitendo beim letzten Durchlauf erstellt hat</p><p>
+                  </p></li><li class="listitem"><p>Finde heraus, welche Datei kivitendo beim letzten Durchlauf
+            erstellt hat</p><p>
                      </p><pre class="programlisting">ls -lahtr ./1*.tex</pre><p>
-                  </p><p>Es sollte die letzte Datei ganz unten sein</p></li><li class="listitem"><p>für besseren Hinweis auf Fehler texdatei nochmals übersetzen</p><p>
+                  </p><p>Es sollte die letzte Datei ganz unten sein</p></li><li class="listitem"><p>für besseren Hinweis auf Fehler texdatei nochmals
+            übersetzen</p><p>
                      </p><pre class="programlisting">pdflatex ./1*.tex</pre><p>
                   </p><p>in der *.tex datei nach dem Fehler suchen.</p></li></ul></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s11.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s13.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.11. E-Mail-Versand aus kivitendo heraus&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.13. OpenDocument-Vorlagen</td></tr></table></div></body></html>
\ No newline at end of file
index 0c91c70..ed0cdfb 100644 (file)
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.13. OpenDocument-Vorlagen</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s12.html" title="2.12. Drucken mit kivitendo"><link rel="next" href="ch02s14.html" title="2.14. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung: EUR"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.13. OpenDocument-Vorlagen</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s12.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s14.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.13. OpenDocument-Vorlagen"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="OpenDocument-Vorlagen"></a>2.13. OpenDocument-Vorlagen</h2></div></div></div><p>kivitendo unterstützt die Verwendung von Vorlagen im
-      OpenDocument-Format, wie es OpenOffice.org ab Version 2 erzeugt.
-      kivitendo kann dabei sowohl neue OpenDocument-Dokumente als auch aus
-      diesen direkt PDF-Dateien erzeugen. Um die Unterstützung von
+   <title>2.13. OpenDocument-Vorlagen</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s12.html" title="2.12. Drucken mit kivitendo"><link rel="next" href="ch02s14.html" title="2.14. Nomenklatur"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.13. OpenDocument-Vorlagen</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s12.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s14.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.13. OpenDocument-Vorlagen"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="OpenDocument-Vorlagen"></a>2.13. OpenDocument-Vorlagen</h2></div></div></div><p>kivitendo unterstützt die Verwendung von Vorlagen im
+      OpenDocument-Format, wie es LibreOffice oder OpenOffice (ab Version 2)
+      erzeugen. kivitendo kann dabei sowohl neue OpenDocument-Dokumente als
+      auch aus diesen direkt PDF-Dateien erzeugen. Um die Unterstützung von
       OpenDocument-Vorlagen zu aktivieren muss in der Datei
       <code class="filename">config/kivitendo.conf</code> die Variable
       <code class="literal">opendocument</code> im Abschnitt
       <code class="literal">print_templates</code> auf ‘<code class="literal">1</code>’ stehen.
       Dieses ist die Standardeinstellung.</p><p>Während die Erzeugung von reinen OpenDocument-Dateien keinerlei
       weitere Software benötigt, wird zur Umwandlung dieser Dateien in PDF
-      OpenOffice.org benötigt. Soll dieses Feature genutzt werden, so muss
-      neben OpenOffice.org ab Version 2 auch der “X virtual frame buffer”
-      (xvfb) installiert werden. Bei Debian ist er im Paket “xvfb” enthalten.
-      Andere Distributionen enthalten ihn in anderen Paketen.</p><p>Nach der Installation müssen in der Datei
-      <code class="filename">config/kivitendo.conf</code> zwei weitere Variablen
-      angepasst werden: <code class="literal">openofficeorg_writer</code> muss den
-      vollständigen Pfad zur OpenOffice.org Writer-Anwendung enthalten.
-      <code class="literal">xvfb</code> muss den Pfad zum “X virtual frame buffer”
-      enthalten. Beide stehen im Abschnitt
-      <code class="literal">applications</code>.</p><p>Zusätzlich gibt es zwei verschiedene Arten, wie kivitendo mit
-      OpenOffice kommuniziert. Die erste Variante, die benutzt wird, wenn die
-      Variable <code class="literal">$openofficeorg_daemon</code> gesetzt ist, startet
-      ein OpenOffice, das auch nach der Umwandlung des Dokumentes gestartet
-      bleibt. Bei weiteren Umwandlungen wird dann diese laufende Instanz
-      benutzt. Der Vorteil ist, dass die Zeit zur Umwandlung deutlich
-      reduziert wird, weil nicht für jedes Dokument ein OpenOffice gestartet
-      werden muss. Der Nachteil ist, dass diese Methode Python und die
-      Python-UNO-Bindings benötigt, die Bestandteil von OpenOffice 2
-      sind.</p><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left">Anmerkung</th></tr><tr><td align="left" valign="top"><p>
-          Für die Verbindung zu OpenOffice wird normalerweise der Python-Interpreter <code class="filename">/usr/bin/python</code> benutzt. Sollte
-          dies nicht der richtige sein, so kann man mit zwei Konfigurationsvariablen entscheiden, welcher Python-Interpreter genutzt
-          wird. Mit der Option <code class="literal">python_uno</code> aus dem Abschnitt <code class="literal">applications</code> wird der Interpreter selber
-          festgelegt; sie steht standardmäßig auf dem eben erwähnten Wert <code class="literal">/usr/bin/python</code>.
-        </p><p>
-          Zusätzlich ist es möglich, Pfade anzugeben, in denen Python neben seinen normalen Suchpfaden ebenfalls nach Modulen gesucht wird,
-          z.B. falls sich diese in einem gesonderten OpenOffice-Verzeichnis befinden. Diese zweite Variable heißt
-          <code class="literal">python_uno_path</code> und befindet sich im Abschnitt <code class="literal">environment</code>. Sie ist standardmäßig
-          leer. Werden hier mehrere Pfade angegeben, so müssen diese durch Doppelpunkte voneinander getrennt werden. Der Inhalt wird an den
-          Python-Interpreter über die Umgebungsvariable <code class="literal">PYTHONPATH</code> übergeben.
-        </p></td></tr></table></div><p>Ist <code class="literal">$openofficeorg_daemon</code> nicht gesetzt, so
-      wird für jedes Dokument OpenOffice neu gestartet und die Konvertierung
-      mit Hilfe eines Makros durchgeführt. Dieses Makro muss in der
-      Dokumentenvorlage enthalten sein und
+      LibreOffice oder OpenOffice benötigt. Soll dieses Feature genutzt
+      werden, so muss neben LibreOffice oder OpenOffice auch der “X virtual
+      frame buffer” (xvfb) installiert werden. Bei Debian ist er im Paket
+      “xvfb” enthalten. Andere Distributionen enthalten ihn in anderen
+      Paketen.</p><p>Nach der Installation müssen in der Datei
+      <code class="filename">config/kivitendo.conf</code> im Abschnitt
+      <code class="literal">applications</code> zwei weitere Variablen angepasst
+      werden:</p><p>
+            <code class="literal">openofficeorg_writer</code> muss den vollständigen
+      Pfad zu LibreOffice oder OpenOffice enthalten. Dabei dürfen keine
+      Anführungszeichen eingesetzt werden.</p><p>Beispiel für Debian oder Ubuntu:</p><pre class="programlisting">openofficeorg_writer = /usr/bin/libreoffice</pre><p>
+            <code class="literal">xvfb</code> muss den Pfad zum “X virtual frame buffer”
+      enthalten.</p><p>Zusätzlich gibt es zwei verschiedene Arten, wie kivitendo mit
+      LibreOffice bzw. OpenOffice kommuniziert. Die erste Variante, die
+      benutzt wird, wenn die Variable <code class="literal">$openofficeorg_daemon</code>
+      gesetzt ist, startet ein LibreOffice oder OpenOffice, das auch nach der
+      Umwandlung des Dokumentes gestartet bleibt. Bei weiteren Umwandlungen
+      wird dann diese laufende Instanz benutzt. Der Vorteil ist, dass die Zeit
+      zur Umwandlung deutlich reduziert wird, weil nicht für jedes Dokument
+      ein LibreOffice bzw. OpenOffice gestartet werden muss. Der Nachteil ist,
+      dass diese Methode Python und die Python-UNO-Bindings benötigt, die
+      Bestandteil von LibreOffice bzw. OpenOffice sind.</p><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left">Anmerkung</th></tr><tr><td align="left" valign="top"><p>Für die Verbindung zu LibreOffice bzw. OpenOffice wird
+        normalerweise der Python-Interpreter
+        <code class="filename">/usr/bin/python</code> benutzt. Sollte dies nicht der
+        richtige sein, so kann man mit zwei Konfigurationsvariablen
+        entscheiden, welcher Python-Interpreter genutzt wird. Mit der Option
+        <code class="literal">python_uno</code> aus dem Abschnitt
+        <code class="literal">applications</code> wird der Interpreter selber
+        festgelegt; sie steht standardmäßig auf dem eben erwähnten Wert
+        <code class="literal">/usr/bin/python</code>.</p><p>Zusätzlich ist es möglich, Pfade anzugeben, in denen Python
+        neben seinen normalen Suchpfaden ebenfalls nach Modulen gesucht wird,
+        z.B. falls sich diese in einem gesonderten LibreOffice- bzw.
+        OpenOffice-Verzeichnis befinden. Diese zweite Variable heißt
+        <code class="literal">python_uno_path</code> und befindet sich im Abschnitt
+        <code class="literal">environment</code>. Sie ist standardmäßig leer. Werden
+        hier mehrere Pfade angegeben, so müssen diese durch Doppelpunkte
+        voneinander getrennt werden. Der Inhalt wird an den Python-Interpreter
+        über die Umgebungsvariable <code class="literal">PYTHONPATH</code>
+        übergeben.</p></td></tr></table></div><p>Ist <code class="literal">$openofficeorg_daemon</code> nicht gesetzt, so
+      wird für jedes Dokument LibreOffice bzw. OpenOffice neu gestartet und
+      die Konvertierung mit Hilfe eines Makros durchgeführt. Dieses Makro muss
+      in der Dokumentenvorlage enthalten sein und
       “Standard.Conversion.ConvertSelfToPDF()” heißen. Die Beispielvorlage
-      ‘<code class="literal">templates/mastertemplates/German/invoice.odt</code>’
-      enthält ein solches Makro, das in jeder anderen Dokumentenvorlage
-      ebenfalls enthalten sein muss.</p><p>Als letztes muss herausgefunden werden, welchen Namen
-      OpenOffice.org Writer dem Verzeichnis mit den Benutzereinstellungen
-      gibt. Unter Debian ist dies momentan
-      <code class="literal">~/.openoffice.org2</code>. Sollte der Name bei Ihrer
-      OpenOffice.org-Installation anders sein, so muss das Verzeichnis
-      <code class="literal">users/.openoffice.org2</code> entsprechend umbenannt werden.
-      Ist der Name z.B. einfach nur <code class="literal">.openoffice</code>, so wäre
-      folgender Befehl auszuführen:</p><p>
-            <code class="literal">mv users/.openoffice.org2
-      users/.openoffice</code>
-         </p><p>Dieses Verzeichnis, wie auch das komplette
+      ‘<code class="literal">templates/print/rev-odt/invoice.odt</code>’ enthält ein
+      solches Makro, das in jeder anderen Dokumentenvorlage ebenfalls
+      enthalten sein muss.</p><p>Als letztes muss herausgefunden werden, welchen Namen OpenOffice
+      bzw. LibreOffice dem Verzeichnis mit den Benutzereinstellungen gibt.
+      Unter Debian ist dies momentan <code class="literal">~/.config/libreoffice</code>.
+      kivitendo verwendet das Verzeichnis
+      <code class="literal">users/.openoffice.org2</code>. Eventuell muss dieses
+      Verzeichnis umbenannt werden.</p><p>Dieses Verzeichnis, wie auch das komplette
       <code class="literal">users</code>-Verzeichnis, muss vom Webserver beschreibbar
-      sein. Dieses wurde bereits erledigt (siehe <a class="xref" href="ch02s03.html" title="2.3. Manuelle Installation des Programmpaketes">Manuelle Installation des Programmpaketes</a>), kann aber
-      erneut überprüft werden, wenn die Konvertierung nach PDF
-      fehlschlägt.</p></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s12.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s14.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.12. Drucken mit kivitendo&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.14. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung:
-      EUR</td></tr></table></div></body></html>
\ No newline at end of file
+      sein. Dieses wurde bereits erledigt (siehe <a class="xref" href="ch02s03.html" title="2.3. Manuelle Installation des Programmpaketes">Manuelle Installation des Programmpaketes</a>), kann aber erneut
+      überprüft werden, wenn die Konvertierung nach PDF fehlschlägt.</p><div class="sect2" title="2.13.1. OpenDocument (odt) Druckvorlagen mit Makros"><div class="titlepage"><div><div><h3 class="title"><a name="d0e2489"></a>2.13.1. OpenDocument (odt) Druckvorlagen mit Makros</h3></div></div></div><p>OpenDocument Vorlagen können Makros enthalten, welche komplexere
+        Aufgaben erfüllen.</p><p>Der Vorlagensatz "rev-odt" enthält solche Vorlagen mit <span class="bold"><strong>Schweizer Bank-Einzahlungsscheinen (BESR)</strong></span>.
+        Diese Makros haben die Aufgabe, die in den Einzahlungsscheinen
+        benötigte Referenznummer und Kodierzeile zu erzeugen. Hier eine kurze
+        Beschreibung, wie die Makros aufgebaut sind, und was bei ihrer Nutzung
+        zu beachten ist (<span class="bold"><strong>in fett sind nötige einmalige
+        Anpassungen aufgeführt</strong></span>):</p><div class="sect3" title="2.13.1.1. Bezeichnung der Vorlagen"><div class="titlepage"><div><div><h4 class="title"><a name="d0e2502"></a>2.13.1.1. Bezeichnung der Vorlagen</h4></div></div></div><p>Rechnung: invoice_besr.odt, Auftrag:
+          sales_order_besr.odt</p></div><div class="sect3" title="2.13.1.2. Vorbereitungen im Adminbereich"><div class="titlepage"><div><div><h4 class="title"><a name="opendocument-druckvorlagen-mit-makros.vorbereitungen"></a>2.13.1.2. Vorbereitungen im Adminbereich</h4></div></div></div><p>Damit beim Erstellen von Rechnungen und Aufträgen neben der
+          Standardvorlage ohne Einzahlungsschein weitere Vorlagen (z.B. mit
+          Einzahlungsschein) auswählbar sind, muss für jedes Vorlagen-Suffix
+          ein Drucker eingerichtet werden:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Druckeradministration → Drucker hinzufügen</p></li><li class="listitem"><p>Mandant wählen</p></li><li class="listitem"><p>Druckerbeschreibung → aussagekräftiger Text: wird in der
+              Auftrags- bzw. Rechnungsmaske als Auswahl angezeigt (z.B. mit
+              Einzahlungsschein Bank xy)</p></li><li class="listitem"><p>Druckbefehl → beliebiger Text (hat für das Erzeugen von
+              Aufträgen oder Rechnungen als odt-Datei keine Bedeutung, darf
+              aber nicht leer sein)</p></li><li class="listitem"><p>Vorlagenkürzel → besr bzw. selbst gewähltes Vorlagensuffix
+              (muss genau der Zeichenfolge entsprechen, die zwischen
+              "invoice_" bzw. "sales_order_" und ".odt" steht.)</p></li><li class="listitem"><p>speichern</p></li></ul></div></div><div class="sect3" title="2.13.1.3. Benutzereinstellungen"><div class="titlepage"><div><div><h4 class="title"><a name="d0e2531"></a>2.13.1.3. Benutzereinstellungen</h4></div></div></div><p>Wer den Ausdruck mit Einzahlungsschein als Standardeinstellung
+          im Rechnungs- bzw. Auftragsformular angezeigt haben möchte, kann
+          dies persönlich für sich bei den Benutzereinstellungen
+          konfigurieren:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Programm → Benutzereinstellungen → Druckoptionen</p></li><li class="listitem"><p>Standardvorlagenformat → OpenDocument/OASIS</p></li><li class="listitem"><p>Standardausgabekanal → Bildschirm</p></li><li class="listitem"><p>Standarddrucker → gewünschte Druckerbeschreibung auswählen
+              (z.B. mit Einzahlungsschein Bank xy)</p></li><li class="listitem"><p>Anzahl Kopien → leer</p></li><li class="listitem"><p>speichern</p></li></ul></div></div><div class="sect3" title="2.13.1.4. Aufbau und nötige Anpassungen der Vorlagen"><div class="titlepage"><div><div><h4 class="title"><a name="d0e2555"></a>2.13.1.4. Aufbau und nötige Anpassungen der Vorlagen</h4></div></div></div><p>In der Vorlage sind als Modul "BESR" 4 Makros gespeichert, die
+          aus dem von kivitendo erzeugten odt-Dokument die korrekte
+          Referenznummer inklusive Prüfziffer sowie die Kodierzeile in
+          OCRB-Schrift erzeugen und am richtigen Ort ins Dokument
+          schreiben.</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Für den Einzahlungsschein ist die letzte Seite des
+              Dokuments reserviert</p></li><li class="listitem"><p>Direkt über dem Einzahlungsschein enthält die Vorlage eine
+              Zeile mit folgenden Angaben (<span class="bold"><strong>Bank-Konto-Identifikationsnummer und
+              Postkonto-Nummer der Bank müssen gemäss Angaben der jeweiligen
+              Bank angepasst werden</strong></span>):</p><div class="itemizedlist"><ul class="itemizedlist" type="circle"><li class="listitem"><p>DDDREF: 4 Werte zum Bilden der Referenznummer
+                    (jeweils durch einen Leerschlag getrennt): </p><div class="itemizedlist"><ul class="itemizedlist" type="square"><li class="listitem"><p>erster Wert: <span class="bold"><strong>Bank-Konto-Identifikation</strong></span>
+                          (nur Ziffern, maximal 6), <span class="bold"><strong>muss
+                          angepasst werden</strong></span>.</p></li><li class="listitem"><p>zweiter Wert: &lt;%customernumber%&gt;
+                          (Kundennummer: nur Ziffern, maximal 6)</p></li><li class="listitem"><p>dritter Wert: &lt;%ordnumber%&gt;
+                          (Auftragsnummer bei Auftragsvorlage
+                          sales_oder_besr.odt, sonst 0) maximal 7 Ziffern,
+                          führende Buchstaben werden vom Makro entfernt</p></li><li class="listitem"><p>vierter Wert: &lt;%invnumber%&gt;
+                          (Rechnungsnummer bei Rechnungsvorlage
+                          invoice_besr.odt, sonst 0) maximal 7 Ziffern,
+                          führende Buchstaben werden vom Makro entfernt</p></li></ul></div><p>
+                              </p></li><li class="listitem"><p>DDDKONTO: <span class="bold"><strong>Postkonto-Nummer der
+                    Bank, muss angepasst werden</strong></span>.</p></li><li class="listitem"><p>DDDBETRAG: &lt;%total%&gt; Einzahlungsbetrag oder 0,
+                    falls Einzahlungsschein ohne Betrag</p></li><li class="listitem"><p>DDDEND: muss am Ende der Zeile vorhanden sein</p></li></ul></div><p>
+                     </p></li><li class="listitem"><p>
+                        <span class="bold"><strong>Im Einzahlungsschein selbst müssen
+              der Name und die Adresse der Bank, die Postkonto-Nummer der
+              Bank, sowie der eigene Firmenname und die Firmenadresse
+              angepasst werden.</strong></span> Dabei ist darauf zu achten, dass
+              sich die Positionen der Postkonto-Nummern der Bank, sowie der
+              Zeichenfolgen dddfr, DDDREF1, DDDREF2, 609, DDDKODIERZEILE nicht
+              verschieben.</p></li></ul></div><div class="screenshot"><div class="mediaobject"><img src="images/Einzahlungsschein_Makro.png"></div></div></div><div class="sect3" title="2.13.1.5. Auswahl der Druckvorlage in kivitendo beim Erzeugen einer odt-Rechnung (analog bei Auftrag)"><div class="titlepage"><div><div><h4 class="title"><a name="d0e2619"></a>2.13.1.5. Auswahl der Druckvorlage in kivitendo beim Erzeugen einer
+          odt-Rechnung (analog bei Auftrag)</h4></div></div></div><p>Im Fussbereich der Rechnungsmaske muss neben Rechnung,
+          OpenDocument/OASIS und Bildschirm die im Adminbereich erstellte
+          Druckerbeschreibung ausgewählt werden, falls diese nicht bereits bei
+          den Benutzereinstellungen als persönlicher Standard gewählt
+          wurde.</p></div><div class="sect3" title="2.13.1.6. Makroeinstellungen in LibreOffice anpassen"><div class="titlepage"><div><div><h4 class="title"><a name="d0e2624"></a>2.13.1.6. Makroeinstellungen in LibreOffice anpassen</h4></div></div></div><p>Falls beim Öffnen einer von kivitendo erzeugten odt-Rechnung
+          die Meldung kommt, dass Makros aus Sicherheitsgründen nicht
+          ausgeführt werden, so müssen folgende Einstellungen in LibreOffice
+          angepasst werden:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Extras → Optionen → Sicherheit → Makrosicherheit</p></li><li class="listitem"><p>Sicherheitslevel auf "Mittel" einstellen (Diese
+              Einstellung muss auf jedem Computer durchgeführt werden, mit dem
+              von kivitendo erzeugte odt-Rechnungen oder Aufträge geöffnet
+              werden.)</p></li><li class="listitem"><p>Beim Öffnen einer odt-Rechnung oder eines odt-Auftrags bei
+              der entsprechenden Nachfrage "Makros ausführen"
+              auswählen.</p><p>
+                        <span class="bold"><strong>Wichtig</strong></span>: die Makros sind
+              so eingestellt, dass sie beim Öffnen der Vorlagen selbst nicht
+              ausgeführt werden. Das heisst für das Ansehen und Bearbeiten der
+              Vorlagen sind keine speziellen Einstellungen in LibreOffice
+              nötig.</p></li></ul></div></div></div><div class="sect2" title="2.13.2. Schweizer QR-Rechnung mit OpenDocument Vorlagen"><div class="titlepage"><div><div><h3 class="title"><a name="d0e2644"></a>2.13.2. Schweizer QR-Rechnung mit OpenDocument Vorlagen</h3></div></div></div><div class="sect3" title="2.13.2.1. Übersicht"><div class="titlepage"><div><div><h4 class="title"><a name="d0e2647"></a>2.13.2.1. Übersicht</h4></div></div></div><p>Mit der Version 3.6.0 unterstützt Kivitendo die Erstellung von
+          Schweizer QR-Rechnungen gemäss <a class="ulink" href="https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-de.pdf" target="_top">Swiss
+          Payment Standards, Version 2.2</a>. Implementiert sind hierbei die
+          Varianten:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
+                        <span class="bold"><strong>QR-IBAN mit
+              QR-Referenz</strong></span>
+                     </p></li><li class="listitem"><p>
+                        <span class="bold"><strong>IBAN ohne Referenz</strong></span>
+                     </p></li></ul></div><p>Der Vorlagensatz "rev-odt" enthält die Vorlage
+          <code class="literal">invoice_qr.odt</code>, welche für die Erstellung von
+          QR-Rechnungen vorgesehen ist. Damit diese verwendet werden kann muss
+          wie obenstehend beschrieben ein Drucker hinzugefügt werden (siehe
+          <a class="xref" href="ch02s13.html#opendocument-druckvorlagen-mit-makros.vorbereitungen" title="2.13.1.2. Vorbereitungen im Adminbereich">Abschnitt&nbsp;2.13.1.2, „Vorbereitungen im Adminbereich“</a>
+          ). Alternativ kann die Vorlage umbenannt werden in
+          <code class="literal">invoice.odt</code>.</p><p>Die Vorlage <code class="literal">invoice_qr.odt</code> kann beliebig
+          angepasst werden. Zwingend muss diese jedoch das QR-Code Platzhalter
+          Bild, als eingebettetes Bild, enthalten. Da dieses beim
+          Ausdrucken/Erzeugen der Rechnung durch das neu generierte QR-Code
+          Bild ersetzt wird.</p></div><div class="sect3" title="2.13.2.2. Einstellungen"><div class="titlepage"><div><div><h4 class="title"><a name="d0e2683"></a>2.13.2.2. Einstellungen</h4></div></div></div><div class="sect4" title="2.13.2.2.1. Mandantenkonfiguration"><div class="titlepage"><div><div><h5 class="title"><a name="d0e2686"></a>2.13.2.2.1. Mandantenkonfiguration</h5></div></div></div><p>Unter <span class="emphasis"><em>System → Mandantenkonfiguration →
+            Features</em></span>. Im Abschnitt <span class="emphasis"><em>Einkauf und
+            Verkauf</em></span>, beim Punkt <span class="emphasis"><em>Verkaufsrechnungen mit
+            Schweizer QR-Rechnung erzeugen</em></span>, die gewünschte Variante
+            wählen.</p></div><div class="sect4" title="2.13.2.2.2. Konfiguration der Bankkonten"><div class="titlepage"><div><div><h5 class="title"><a name="d0e2700"></a>2.13.2.2.2. Konfiguration der Bankkonten</h5></div></div></div><p>Unter <span class="emphasis"><em>System → Bankkonten</em></span> muss bei
+            mindestens einem Bankkonto die Option <span class="emphasis"><em>Nutzung mit
+            Schweizer QR-Rechnung</em></span> auf <span class="bold"><strong>Ja</strong></span> gestellt werden.</p><div class="tip" title="Tipp" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Tip"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Tipp]" src="system/docbook-xsl/images/tip.png"></td><th align="left">Tipp</th></tr><tr><td align="left" valign="top"><p>Für die Variante <span class="bold"><strong>QR-IBAN mit
+              QR-Referenz</strong></span> muss dieses Konto unter IBAN eine gültige
+              <span class="bold"><strong>QR-IBAN Nummer</strong></span> enthalten. Diese
+              unterscheidet sich von der regulären IBAN.</p><p>Zusätzlich muss eine gültige <span class="bold"><strong>Bankkonto
+              Identifikationsnummer</strong></span> angegeben werden
+              (6-stellig).</p><p>Diese werden von der jeweiligen Bank vergeben.</p></td></tr></table></div><p>Sind mehrere Konten ausgewählt wird das erste
+            verwendet.</p></div><div class="sect4" title="2.13.2.2.3. Rechnungen ohne Betrag"><div class="titlepage"><div><div><h5 class="title"><a name="d0e2732"></a>2.13.2.2.3. Rechnungen ohne Betrag</h5></div></div></div><p>Für Rechnungen ohne Betrag (z.B. Spenden) kann, in der
+            jeweiligen Rechnung, die Checkbox <span class="emphasis"><em>QR-Rechnung ohne
+            Betrag</em></span> aktiviert werden. Diese Checkbox erscheint nur,
+            wenn QR-Rechnungen in der Mandantenkonfiguration aktiviert sind
+            (variante ausgewählt).</p><p>Dies wirkt sich lediglich auf den erzeugten QR-Code aus. Die
+            Vorlage muss separat angepasst und ausgewählt werden.</p></div></div><div class="sect3" title="2.13.2.3. Adressdaten"><div class="titlepage"><div><div><h4 class="title"><a name="d0e2742"></a>2.13.2.3. Adressdaten</h4></div></div></div><p>Die Adressdaten zum Zahlungsempfänger werden aus der
+          Mandantenkonfiguration entnommen. Unter <span class="emphasis"><em>System →
+          Mandantenkonfiguration → Verschiedenes</em></span>, Abschnitt
+          <span class="emphasis"><em>Firmenname und -adresse.</em></span>
+               </p><p>Die Adressdaten zum Zahlungspflichtigen stammen aus den
+          Kundendaten der jeweiligen Rechnung.</p><p>Die Adressen müssen inklusive Land angegeben werden. Akzeptiert
+          werden Ländername oder Ländercode, also z.B. "Schweiz" oder "CH".
+          </p><p>Diese können in der Vorlage mit den jeweiligen Variablen
+          eingetragen werden. Siehe auch: <a class="xref" href="ch03s03.html" title="3.3. Dokumentenvorlagen und verfügbare Variablen">Abschnitt&nbsp;3.3, „Dokumentenvorlagen und verfügbare Variablen“</a>
+               </p><p>Der erzeugte QR-Code verwendet Adress-Typ "K" (Kombinierte
+          Adressfelder, 2 Zeilen).</p></div><div class="sect3" title="2.13.2.4. Referenznummer"><div class="titlepage"><div><div><h4 class="title"><a name="d0e2763"></a>2.13.2.4. Referenznummer</h4></div></div></div><p>Die Referenznummer wird in Kivitendo erzeugt und setzt sich
+          wiefolgt zusammen:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Bankkonto Identifikationsnummer (6-stellig)</p></li><li class="listitem"><p>Kundennummer (6-stellig, mit führenden Nullen
+              aufgefüllt)</p></li><li class="listitem"><p>Auftragsnummer (7-stellig, mit führenden Nullen
+              aufgefüllt)</p></li><li class="listitem"><p>Rechnungsnummer (7-stellig, mit führenden Nullen
+              aufgefüllt)</p></li><li class="listitem"><p>Prüfziffer (1-stellig, berechnet mittels modulo 10,
+              rekursiv)</p></li></ul></div><p>Es sind lediglich Ziffern erlaubt. Allfällige Prefixe mit
+          Buchstaben werden entfernt und fehlende Stellen werden mit führenden
+          Nullen aufgefüllt.</p></div><div class="sect3" title="2.13.2.5. Zusätzliche Variablen für Vorlage"><div class="titlepage"><div><div><h4 class="title"><a name="d0e2786"></a>2.13.2.5. Zusätzliche Variablen für Vorlage</h4></div></div></div><p>Zusätzlich zu den in der Vorlage standardmässig verfügbaren
+          Variablen (siehe <a class="xref" href="ch03s03.html" title="3.3. Dokumentenvorlagen und verfügbare Variablen">Abschnitt&nbsp;3.3, „Dokumentenvorlagen und verfügbare Variablen“</a>),
+          werden die folgenden Variablen erzeugt:</p><div class="variablelist"><dl><dt><span class="term">ref_number_formatted</span></dt><dd><p>Referenznummer formatiert mit Leerzeichen, z.B.: 21 00000
+                00003 13947 14300 09017</p></dd><dt><span class="term">iban_formatted</span></dt><dd><p>IBAN formatiert mit Leerzeichen</p></dd><dt><span class="term">amount_formatted</span></dt><dd><p>Betrag formatiert mit Tausendertrennzeichen Leerschlag,
+                z.B.: 1 005.55</p></dd></dl></div></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s12.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s14.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.12. Drucken mit kivitendo&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.14. Nomenklatur</td></tr></table></div></body></html>
\ No newline at end of file
index 48eb5f7..305687c 100644 (file)
@@ -1,65 +1,21 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.14. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung: EUR</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s13.html" title="2.13. OpenDocument-Vorlagen"><link rel="next" href="ch02s15.html" title="2.15. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.14. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung:
-      EUR</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s13.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s15.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.14. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung: EUR"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="config.eur"></a>2.14. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung:
-      EUR</h2></div></div></div><div class="sect2" title="2.14.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="config.eur.introduction"></a>2.14.1. Einführung</h3></div></div></div><p>kivitendo besaß bis inklusive Version 2.6.3 einen Konfigurationsparameter namens <code class="varname">eur</code>, der sich in der
-        Konfigurationsdatei <code class="filename">config/kivitendo.conf</code> (damals noch <code class="filename">config/lx_office.conf</code>)
-        befand. Somit galt er für alle Mandanten, die in dieser Installation benutzt wurden.</p><p>Mit der nachfolgenden Version wurde der Parameter zum Einen in
-        die Mandantendatenbank verschoben und dabei auch gleich in drei
-        Einzelparameter aufgeteilt, mit denen sich das Verhalten genauer
-        steuern lässt.</p></div><div class="sect2" title="2.14.2. Konfigurationsparameter"><div class="titlepage"><div><div><h3 class="title"><a name="config.eur.parameters"></a>2.14.2. Konfigurationsparameter</h3></div></div></div><p>Es gibt drei Parameter, die die Gewinnermittlungsart,
-        Versteuerungsart und die Warenbuchungsmethode regeln:</p><div class="variablelist"><dl><dt><span class="term">
-                     <code class="varname">profit_determination</code>
-                  </span></dt><dd><p>Dieser Parameter legt die Berechnungsmethode für die
-              Gewinnermittlung fest. Er enthält entweder
-              <code class="literal">balance</code> für
-              Betriebsvermögensvergleich/Bilanzierung oder
-              <code class="literal">income</code> für die
-              Einnahmen-Überschuss-Rechnung.</p></dd><dt><span class="term">
-                     <code class="varname">accounting_method</code>
-                  </span></dt><dd><p>Dieser Parameter steuert die Buchungs- und
-              Berechnungsmethoden für die Versteuerungsart. Er enthält
-              entweder <code class="literal">accrual</code> für die Soll-Versteuerung
-              oder <code class="literal">cash</code> für die Ist-Versteuerung.</p></dd><dt><span class="term">
-                     <code class="varname">inventory_system</code>
-                  </span></dt><dd><p>Dieser Parameter legt die Warenbuchungsmethode fest. Er
-              enthält entweder <code class="literal">perpetual</code> für die
-              Bestandsmethode oder <code class="literal">periodic</code> für die
-              Aufwandsmethode.</p></dd></dl></div><p>Zum Vergleich der Funktionalität bis und nach 2.6.3:
-        <code class="varname">eur</code> = 1 bedeutete Einnahmen-Überschuss-Rechnung,
-        Ist-Versteuerung und Aufwandsmethode. <code class="varname">eur</code> = 0
-        bedeutete hingegen Bilanzierung, Soll-Versteuerung und
-        Bestandsmethode.</p><p>Die Konfiguration "<code class="varname">eur</code>" unter
-        <code class="varname">[system]</code> in der <a class="link" href="ch02s04.html" title="2.4. kivitendo-Konfigurationsdatei">Konfigurationsdatei</a>
-        
-               <code class="filename">config/kivitendo.conf</code> wird nun nicht mehr
-        benötigt und kann entfernt werden. Dies muss manuell geschehen.</p></div><div class="sect2" title="2.14.3. Festlegen der Parameter"><div class="titlepage"><div><div><h3 class="title"><a name="config.eur.setting-parameters"></a>2.14.3. Festlegen der Parameter</h3></div></div></div><p>Beim Anlegen eines neuen Mandanten bzw. einer neuen Datenbank in
-        der Admininstration können diese Optionen nun unabhängig voneinander
-        eingestellt werden.</p><p>Beim Upgrade bestehender Mandanten wird eur ausgelesen und die
-        Variablen werden so gesetzt, daß sich an der Funktionalität nichts
-        ändert.</p><p>Die aktuelle Konfiguration wird unter Nummernkreise und
-        Standardkonten unter dem neuen Punkt "Einstellungen" (read-only)
-        angezeigt. Unter <span class="guimenu">System</span>
-        -&gt; <span class="guisubmenu">Mandantenkonfiguration</span> können
-        die Einstellungen auch geändert werden. Dabei ist zu beachten,
-        dass eine Änderung vorhandene Daten so belässt und damit
-        evtl. die Ergebnisse verfälscht. Dies gilt vor Allem für die
-        Warenbuchungsmethode (siehe auch
-        <a class="link" href="ch02s14.html#config.eur.inventory-system-perpetual" title="2.14.4. Bemerkungen zur Bestandsmethode">
-        Bemerkungen zur Bestandsmethode</a>).</p></div><div class="sect2" title="2.14.4. Bemerkungen zur Bestandsmethode"><div class="titlepage"><div><div><h3 class="title"><a name="config.eur.inventory-system-perpetual"></a>2.14.4. Bemerkungen zur Bestandsmethode</h3></div></div></div><p>Die Bestandsmethode ist eigentlich eine sehr elegante Methode,
-        funktioniert in kivitendo aber nur unter bestimmten Bedingungen:
-        Voraussetzung ist, daß auch immer alle Einkaufsrechnungen gepflegt
-        werden, und man beim Jahreswechsel nicht mit einer leeren Datenbank
-        anfängt, da bei jedem Verkauf anhand der gesamten Rechnungshistorie
-        der Einkaufswert der Ware nach dem FIFO-Prinzip aus den
-        Einkaufsrechnungen berechnet wird.</p><p>Die Bestandsmethode kann vom Prinzip her also nur funktioneren,
-        wenn man mit den Buchungen bei Null anfängt, und man kann auch nicht
-        im laufenden Betrieb von der Aufwandsmethode zur Bestandsmethode
-        wechseln.</p></div><div class="sect2" title="2.14.5. Bekannte Probleme"><div class="titlepage"><div><div><h3 class="title"><a name="config.eur.knonw-issues"></a>2.14.5. Bekannte Probleme</h3></div></div></div><p>Bei bestimmten Berichten kann man derzeit noch inviduell
-        einstellen, ob man nach Ist- oder Sollversteuerung auswertet, und es
-        werden im Code Variablen wie $accrual oder $cash gesetzt. Diese
-        Codestellen wurden noch nicht angepasst, sondern nur die, wo bisher
-        die Konfigurationsvariable
-        <code class="varname">$::lx_office_conf{system}-&gt;{eur}</code> ausgewertet
-        wurde.</p><p>Es fehlen Hilfetext beim Neuanlegen eines Mandanten, was die
-        Optionen bewirken, z.B. mit zwei Standardfällen.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s13.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s15.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.13. OpenDocument-Vorlagen&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.15. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb</td></tr></table></div></body></html>
\ No newline at end of file
+   <title>2.14. Nomenklatur</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s13.html" title="2.13. OpenDocument-Vorlagen"><link rel="next" href="ch02s15.html" title="2.15. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung: EUR"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.14. Nomenklatur</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s13.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s15.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.14. Nomenklatur"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="nomenclature"></a>2.14. Nomenklatur</h2></div></div></div><div class="sect2" title="2.14.1. Datum bei Buchungen"><div class="titlepage"><div><div><h3 class="title"><a name="booking.dates"></a>2.14.1. Datum bei Buchungen</h3></div></div></div><p>Seit der Version 3.5 werden für Buchungen in kivitendo
+        einheitlich folgende Bezeichnungen verwendet:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
+                     <code class="option">Erfassungsdatum</code> (en: <code class="option">Entry
+            Date</code>, code: <code class="option">Gldate</code>)</p><p>bezeichnet das Datum, an dem die Buchung in kivitendo
+            erfasst wurde.</p></li><li class="listitem"><p>
+                     <code class="option">Buchungsdatum</code> (en: <code class="option">Booking
+            Date</code>, code: <code class="option">Transdate</code>)</p><p>bezeichnet das buchhaltungstechnisch für eine Buchung
+            relevante Datum</p><p>Das <code class="option">Rechnungsdatum</code> bei Verkaufs- und
+            Einkaufsrechnungen entspricht dem Buchungsdatum. Das heisst, in
+            Berichten wie dem Buchungsjournal, in denen eine Spalte
+            <code class="option">Buchungsdatum</code> angezeigt werden kann, erscheint
+            hier im Fall von Rechnungen das Rechnungsdatum.</p></li><li class="listitem"><p>Bezieht sich ein verbuchter Beleg auf einen Zeitpunkt, der
+            nicht mit dem Buchungsdatum übereinstimmt, so kann dieses Datum
+            momentan in kivitendo nur unter Bemerkungen erfasst werden.</p><p>Möglicherweise wird für solche Fälle in einer späteren
+            Version von kivitendo ein dritter Datumswert für Buchungen
+            erstellt. (Beispiel: Einkaufsbeleg stammt aus einem früheren Jahr,
+            das bereits buchhaltungstechnisch abgeschlossen wurde, und muss
+            deshalb später verbucht werden.)</p></li></ul></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s13.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s15.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.13. OpenDocument-Vorlagen&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.15. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung:
+      EUR</td></tr></table></div></body></html>
\ No newline at end of file
index c264d4d..24f58c3 100644 (file)
@@ -1,36 +1,72 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.15. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s14.html" title="2.14. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung: EUR"><link rel="next" href="ch02s16.html" title="2.16. Verhalten des Bilanzberichts"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.15. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s14.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s16.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.15. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="config.skr04-update-3804"></a>2.15. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb</h2></div></div></div><div class="sect2" title="2.15.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="config.skr04-update-3804.introduction"></a>2.15.1. Einführung</h3></div></div></div><p>Die Umsatzsteuerumstellung auf 19% für SKR04 für die
-        Steuerschlüssel "EU ohne USt-ID Nummer" ist erst 2010 erfolgt.
-        kivitendo beinhaltet ein Upgradeskript, das das Konto 3804 automatisch
-        erstellt und die Steuereinstellungen korrekt einstellt. Hat der
-        Benutzer aber schon selber das Konto 3804 angelegt, oder gab es schon
-        Buchungen im Zeitraum nach dem 01.01.2007 auf das Konto 3803, wird das
-        Upgradeskript vorsichtshalber nicht ausgeführt, da der Benutzer sich
-        vielleicht schon selbst geholfen hat und mit seinen Änderungen
-        zufrieden ist. Die korrekten Einstellungen kann man aber auch per Hand
-        ausführen. Nachfolgend werden die entsprechenden Schritte anhand von
-        Screenshots dargestellt.</p><p>Für den Fall, daß Buchungen mit der Steuerschlüssel "EU ohne
-        USt.-IdNr." nach dem 01.01.2007 erfolgt sind, ist davon auszugehen,
-        dass diese mit dem alten Umsatzsteuersatz von 16% gebucht worden sind,
-        und diese Buchungen sollten entsprechend kontrolliert werden.</p></div><div class="sect2" title="2.15.2. Konto 3804 manuell anlegen"><div class="titlepage"><div><div><h3 class="title"><a name="config.skr04-update-3804.create-chart"></a>2.15.2. Konto 3804 manuell anlegen</h3></div></div></div><p>Die folgenden Schritte sind notwendig, um das Konto manuell
-        anzulegen und zu konfigurieren. Zuerst wird in
-        <span class="guimenu">System</span> -&gt;
-        <span class="guisubmenu">Kontenübersicht</span> -&gt; <span class="guimenuitem">Konto
-        erfassen</span> das Konto angelegt.</p><div class="screenshot"><div class="mediaobject"><img src="images/skr04-update-3804/konto3804.png"></div></div><p>
-         Als Zweites muss Steuergruppe 13 für Konto 3803 angepasst werden. Dazu unter <span class="guimenu">System</span> -&gt;
-         <span class="guisubmenu">Steuern</span> -&gt; <span class="guimenuitem">Bearbeiten</span> den Eintrag mit Steuerschlüssel 13 auswählen und ihn
-         wie im folgenden Screenshot angezeigt anpassen.
-        </p><div class="screenshot"><div class="mediaobject"><img src="images/skr04-update-3804/steuer3803.png"></div></div><p>
-         Als Drittes wird ein neuer Eintrag mit Steuerschlüssel 13 für Konto 3804 (19%) angelegt. Dazu unter <span class="guimenu">System</span> -&gt;
-         <span class="guisubmenu">Steuern</span> -&gt; <span class="guimenuitem">Erfassen</span> auswählen und die Werte aus dem Screenshot übernehmen.
-        </p><div class="screenshot"><div class="mediaobject"><img src="images/skr04-update-3804/steuer3804.png"></div></div><p>
-         Als Nächstes sind alle Konten anzupassen, die als Steuerautomatikkonto die 3803 haben, sodass sie ab dem 1.1.2007 auch
-         Steuerautomatik auf 3804 bekommen. Dies betrifft in der Standardkonfiguration die Konten 4315 und 4726. Als Beispiel für 4315
-         müssen Sie dazu unter <span class="guimenu">System</span> -&gt; <span class="guisubmenu">Kontenübersicht</span> -&gt; <span class="guimenuitem">Konten
-         anzeigen</span> das Konto 4315 anklicken und die Einstellungen wie im Screenshot gezeigt vornehmen.
-        </p><div class="screenshot"><div class="mediaobject"><img src="images/skr04-update-3804/konto4315.png"></div></div><p>
-         Als Letztes sollte die Steuerliste unter <span class="guimenu">System</span> -&gt; <span class="guisubmenu">Steuern</span> -&gt;
-         <span class="guimenuitem">Bearbeiten</span> kontrolliert werden. Zum Vergleich der Screenshot.
-        </p><div class="screenshot"><div class="mediaobject"><img src="images/skr04-update-3804/steuerliste.png"></div></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s14.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s16.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.14. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung:
-      EUR&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.16. Verhalten des Bilanzberichts</td></tr></table></div></body></html>
\ No newline at end of file
+   <title>2.15. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung: EUR</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s14.html" title="2.14. Nomenklatur"><link rel="next" href="ch02s16.html" title="2.16. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.15. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung:
+      EUR</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s14.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s16.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.15. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung: EUR"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="config.eur"></a>2.15. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung:
+      EUR</h2></div></div></div><div class="sect2" title="2.15.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="config.eur.introduction"></a>2.15.1. Einführung</h3></div></div></div><p>kivitendo besaß bis inklusive Version 2.6.3 einen
+        Konfigurationsparameter namens <code class="varname">eur</code>, der sich in der
+        Konfigurationsdatei <code class="filename">config/kivitendo.conf</code> (damals
+        noch <code class="filename">config/lx_office.conf</code>) befand. Somit galt er
+        für alle Mandanten, die in dieser Installation benutzt wurden.</p><p>Mit der nachfolgenden Version wurde der Parameter zum Einen in
+        die Mandantendatenbank verschoben und dabei auch gleich in drei
+        Einzelparameter aufgeteilt, mit denen sich das Verhalten genauer
+        steuern lässt.</p></div><div class="sect2" title="2.15.2. Konfigurationsparameter"><div class="titlepage"><div><div><h3 class="title"><a name="config.eur.parameters"></a>2.15.2. Konfigurationsparameter</h3></div></div></div><p>Es gibt drei Parameter, die die Gewinnermittlungsart,
+        Versteuerungsart und die Warenbuchungsmethode regeln:</p><div class="variablelist"><dl><dt><span class="term">
+                     <code class="varname">profit_determination</code>
+                  </span></dt><dd><p>Dieser Parameter legt die Berechnungsmethode für die
+              Gewinnermittlung fest. Er enthält entweder
+              <code class="literal">balance</code> für
+              Betriebsvermögensvergleich/Bilanzierung oder
+              <code class="literal">income</code> für die
+              Einnahmen-Überschuss-Rechnung.</p></dd><dt><span class="term">
+                     <code class="varname">accounting_method</code>
+                  </span></dt><dd><p>Dieser Parameter steuert die Buchungs- und
+              Berechnungsmethoden für die Versteuerungsart. Er enthält
+              entweder <code class="literal">accrual</code> für die Soll-Versteuerung
+              oder <code class="literal">cash</code> für die Ist-Versteuerung.</p></dd><dt><span class="term">
+                     <code class="varname">inventory_system</code>
+                  </span></dt><dd><p>Dieser Parameter legt die Warenbuchungsmethode fest. Er
+              enthält entweder <code class="literal">perpetual</code> für die
+              Bestandsmethode oder <code class="literal">periodic</code> für die
+              Aufwandsmethode.</p></dd></dl></div><p>Zum Vergleich der Funktionalität bis und nach 2.6.3:
+        <code class="varname">eur</code> = 1 bedeutete Einnahmen-Überschuss-Rechnung,
+        Ist-Versteuerung und Aufwandsmethode. <code class="varname">eur</code> = 0
+        bedeutete hingegen Bilanzierung, Soll-Versteuerung und
+        Bestandsmethode.</p><p>Die Konfiguration "<code class="varname">eur</code>" unter
+        <code class="varname">[system]</code> in der <a class="link" href="ch02s04.html" title="2.4. kivitendo-Konfigurationsdatei">Konfigurationsdatei</a>
+        
+               <code class="filename">config/kivitendo.conf</code> wird nun nicht mehr
+        benötigt und kann entfernt werden. Dies muss manuell geschehen.</p></div><div class="sect2" title="2.15.3. Festlegen der Parameter"><div class="titlepage"><div><div><h3 class="title"><a name="config.eur.setting-parameters"></a>2.15.3. Festlegen der Parameter</h3></div></div></div><p>Beim Anlegen eines neuen Mandanten bzw. einer neuen Datenbank in
+        der Admininstration können diese Optionen nun unabhängig voneinander
+        eingestellt werden.</p><p>Für die Schweiz sind folgende Einstellungen üblich:
+        </p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Sollversteuerung</p></li><li class="listitem"><p>Aufwandsmethode</p></li><li class="listitem"><p>Bilanzierung</p></li></ul></div><p> Diese Einstellungen werden automatisch beim
+        Erstellen einer neuen Datenbank vorausgewählt, wenn in
+        <code class="filename">config/kivitendo.conf</code> unter
+        <code class="varname">[system]</code> 
+               <code class="literal">default_manager = swiss</code>
+        eingestellt ist.</p><p>Beim Upgrade bestehender Mandanten wird eur ausgelesen und die
+        Variablen werden so gesetzt, daß sich an der Funktionalität nichts
+        ändert.</p><p>Die aktuelle Konfiguration wird unter Nummernkreise und
+        Standardkonten unter dem neuen Punkt "Einstellungen" (read-only)
+        angezeigt. Unter <span class="guimenu">System</span> →
+        <span class="guisubmenu">Mandantenkonfiguration</span> können die
+        Einstellungen auch geändert werden. Dabei ist zu beachten, dass eine
+        Änderung vorhandene Daten so belässt und damit evtl. die Ergebnisse
+        verfälscht. Dies gilt vor Allem für die Warenbuchungsmethode (siehe
+        auch <a class="link" href="ch02s15.html#config.eur.inventory-system-perpetual" title="2.15.4. Bemerkungen zur Bestandsmethode">
+        Bemerkungen zur Bestandsmethode</a>).</p></div><div class="sect2" title="2.15.4. Bemerkungen zur Bestandsmethode"><div class="titlepage"><div><div><h3 class="title"><a name="config.eur.inventory-system-perpetual"></a>2.15.4. Bemerkungen zur Bestandsmethode</h3></div></div></div><p>Die Bestandsmethode ist eigentlich eine sehr elegante Methode,
+        funktioniert in kivitendo aber nur unter bestimmten Bedingungen:
+        Voraussetzung ist, daß auch immer alle Einkaufsrechnungen gepflegt
+        werden, und man beim Jahreswechsel nicht mit einer leeren Datenbank
+        anfängt, da bei jedem Verkauf anhand der gesamten Rechnungshistorie
+        der Einkaufswert der Ware nach dem FIFO-Prinzip aus den
+        Einkaufsrechnungen berechnet wird.</p><p>Die Bestandsmethode kann vom Prinzip her also nur funktioneren,
+        wenn man mit den Buchungen bei Null anfängt, und man kann auch nicht
+        im laufenden Betrieb von der Aufwandsmethode zur Bestandsmethode
+        wechseln.</p></div><div class="sect2" title="2.15.5. Bekannte Probleme"><div class="titlepage"><div><div><h3 class="title"><a name="config.eur.knonw-issues"></a>2.15.5. Bekannte Probleme</h3></div></div></div><p>Bei bestimmten Berichten kann man derzeit noch inviduell
+        einstellen, ob man nach Ist- oder Sollversteuerung auswertet, und es
+        werden im Code Variablen wie $accrual oder $cash gesetzt. Diese
+        Codestellen wurden noch nicht angepasst, sondern nur die, wo bisher
+        die Konfigurationsvariable
+        <code class="varname">$::lx_office_conf{system}-&gt;{eur}</code> ausgewertet
+        wurde.</p><p>Es fehlen Hilfetext beim Neuanlegen eines Mandanten, was die
+        Optionen bewirken, z.B. mit zwei Standardfällen.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s14.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s16.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.14. Nomenklatur&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.16. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb</td></tr></table></div></body></html>
\ No newline at end of file
index 47926f2..13e79cf 100644 (file)
@@ -1,21 +1,38 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.16. Verhalten des Bilanzberichts</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s15.html" title="2.15. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb"><link rel="next" href="ch02s17.html" title="2.17. Einstellungen pro Mandant"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.16. Verhalten des Bilanzberichts</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s15.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s17.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.16. Verhalten des Bilanzberichts"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="bilanz"></a>2.16. Verhalten des Bilanzberichts</h2></div></div></div><p>
-    Bis Version 3.0 wurde "closedto" ("Bücher schließen zum") als Grundlage für das
-    Startdatum benutzt. Schließt man die Bücher allerdings monatsweise führt dies
-    zu falschen Werten.</p><p>In der Mandantenkonfiguration kann man dieses Verhalten genau einstellen indem man:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>weiterhin closed_to benutzt (Default, es ändert sich nichts zu vorher)</p></li><li class="listitem"><p>immer den Jahresanfang nimmt (1.1. relativ zum Stichtag)</p></li><li class="listitem"><p>immer die letzte Eröffnungsbuchung als Startdatum nimmt</p><p>- mit Jahresanfang als Alternative wenn es keine EB-Buchungen gibt</p><p>- oder mit "alle Buchungen" als Alternative"</p></li><li class="listitem"><p>mit Jahresanfang als Alternative wenn es keine EB-Buchungen gibt </p></li><li class="listitem"><p>immer alle Buchungen seit Beginn der Datenbank nimmt</p></li></ul></div><p>
-   Folgende Hinweise zu den Optionen:
-    Das "Bücher schließen Datum" ist sinnvoll, wenn man nur komplette Jahre
-    schließt. Bei Wirtschaftsjahr = Kalendarjahr entspricht dies aber auch
-    dem Jahresanfang.
-    "Alle Buchungen" kann z.B. sinnvoll sein wenn man ohne Jahresabschluß
-    durchbucht.
-    Eröffnungsbuchung mit "alle Buchungen" als Fallback ist z.B. sinnvoll, wenn man
-    am sich Anfang des zweiten Buchungsjahres befindet, und noch keinen
-    Jahreswechsel und auch noch keine EB-Buchungen hat.
-    Bei den Optionen mit EB-Buchungen wird vorausgesetzt, daß diese immer am 1. Tag
-    des Wirtschaftsjahres gebucht werden.
-    Zur Sicherheit wird das Startdatum im Bilanzbericht jetzt zusätzlich zum
-    Stichtag mit angezeigt. Das hilft auch bei der Kontrolle für den
-    Abgleich mit der GuV.
-    </p></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s15.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s17.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.15. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.17. Einstellungen pro Mandant</td></tr></table></div></body></html>
\ No newline at end of file
+   <title>2.16. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s15.html" title="2.15. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung: EUR"><link rel="next" href="ch02s17.html" title="2.17. Verhalten des Bilanzberichts"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.16. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s15.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s17.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.16. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="config.skr04-update-3804"></a>2.16. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb</h2></div></div></div><div class="sect2" title="2.16.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="config.skr04-update-3804.introduction"></a>2.16.1. Einführung</h3></div></div></div><p>Die Umsatzsteuerumstellung auf 19% für SKR04 für die
+        Steuerschlüssel "EU ohne USt-ID Nummer" ist erst 2010 erfolgt.
+        kivitendo beinhaltet ein Upgradeskript, das das Konto 3804 automatisch
+        erstellt und die Steuereinstellungen korrekt einstellt. Hat der
+        Benutzer aber schon selber das Konto 3804 angelegt, oder gab es schon
+        Buchungen im Zeitraum nach dem 01.01.2007 auf das Konto 3803, wird das
+        Upgradeskript vorsichtshalber nicht ausgeführt, da der Benutzer sich
+        vielleicht schon selbst geholfen hat und mit seinen Änderungen
+        zufrieden ist. Die korrekten Einstellungen kann man aber auch per Hand
+        ausführen. Nachfolgend werden die entsprechenden Schritte anhand von
+        Screenshots dargestellt.</p><p>Für den Fall, daß Buchungen mit der Steuerschlüssel "EU ohne
+        USt.-IdNr." nach dem 01.01.2007 erfolgt sind, ist davon auszugehen,
+        dass diese mit dem alten Umsatzsteuersatz von 16% gebucht worden sind,
+        und diese Buchungen sollten entsprechend kontrolliert werden.</p></div><div class="sect2" title="2.16.2. Konto 3804 manuell anlegen"><div class="titlepage"><div><div><h3 class="title"><a name="config.skr04-update-3804.create-chart"></a>2.16.2. Konto 3804 manuell anlegen</h3></div></div></div><p>Die folgenden Schritte sind notwendig, um das Konto manuell
+        anzulegen und zu konfigurieren. Zuerst wird in
+        <span class="guimenu">System</span> → <span class="guisubmenu">Kontenübersicht</span> →
+        <span class="guimenuitem">Konto erfassen</span> das Konto angelegt.</p><div class="screenshot"><div class="mediaobject"><img src="images/skr04-update-3804/konto3804.png"></div></div><p>Als Zweites muss Steuergruppe 13 für Konto 3803 angepasst
+        werden. Dazu unter <span class="guimenu">System</span> →
+        <span class="guisubmenu">Steuern</span> →
+        <span class="guimenuitem">Bearbeiten</span> den Eintrag mit Steuerschlüssel
+        13 auswählen und ihn wie im folgenden Screenshot angezeigt
+        anpassen.</p><div class="screenshot"><div class="mediaobject"><img src="images/skr04-update-3804/steuer3803.png"></div></div><p>Als Drittes wird ein neuer Eintrag mit Steuerschlüssel 13 für
+        Konto 3804 (19%) angelegt. Dazu unter <span class="guimenu">System</span> →
+        <span class="guisubmenu">Steuern</span> → <span class="guimenuitem">Erfassen</span>
+        auswählen und die Werte aus dem Screenshot übernehmen.</p><div class="screenshot"><div class="mediaobject"><img src="images/skr04-update-3804/steuer3804.png"></div></div><p>Als Nächstes sind alle Konten anzupassen, die als
+        Steuerautomatikkonto die 3803 haben, sodass sie ab dem 1.1.2007 auch
+        Steuerautomatik auf 3804 bekommen. Dies betrifft in der
+        Standardkonfiguration die Konten 4315 und 4726. Als Beispiel für 4315
+        müssen Sie dazu unter <span class="guimenu">System</span> →
+        <span class="guisubmenu">Kontenübersicht</span> → <span class="guimenuitem">Konten
+        anzeigen</span> das Konto 4315 anklicken und die Einstellungen
+        wie im Screenshot gezeigt vornehmen.</p><div class="screenshot"><div class="mediaobject"><img src="images/skr04-update-3804/konto4315.png"></div></div><p>Als Letztes sollte die Steuerliste unter
+        <span class="guimenu">System</span> → <span class="guisubmenu">Steuern</span> →
+        <span class="guimenuitem">Bearbeiten</span> kontrolliert werden. Zum
+        Vergleich der Screenshot.</p><div class="screenshot"><div class="mediaobject"><img src="images/skr04-update-3804/steuerliste.png"></div></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s15.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s17.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.15. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung:
+      EUR&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.17. Verhalten des Bilanzberichts</td></tr></table></div></body></html>
\ No newline at end of file
index 98cbc1d..a5030e6 100644 (file)
@@ -1,20 +1,21 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.17. Einstellungen pro Mandant</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s16.html" title="2.16. Verhalten des Bilanzberichts"><link rel="next" href="ch02s18.html" title="2.18. kivitendo ERP verwenden"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.17. Einstellungen pro Mandant</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s16.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s18.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.17. Einstellungen pro Mandant"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="config.client"></a>2.17. Einstellungen pro Mandant</h2></div></div></div><p>Einige Einstellungen können von einem Benutzer mit dem
-      <a class="link" href="ch02s09.html#Zusammenh%C3%A4nge" title="2.9.1. Zusammenhänge">Recht</a> "Administration
-      (Für die Verwaltung der aktuellen Instanz aus einem Userlogin heraus)"
-      gemacht werden. Diese Einstellungen sind dann für die aktuellen
-      Mandanten-Datenbank gültig. Die Einstellungen sind
-      unter <span class="guimenu">System</span>
-      -&gt; <span class="guisubmenu">Mandantenkonfiguration</span> erreichbar.</p><p>Bitte beachten Sie die Hinweise zu den einzelnen
-      Einstellungen. Einige Einstellungen sollten nicht ohne Weiteres
-      im laufenden Betrieb geändert werden (siehe
-      auch <a class="link" href="ch02s14.html#config.eur.inventory-system-perpetual" title="2.14.4. Bemerkungen zur Bestandsmethode">Bemerkungen zu
-      Bestandsmethode</a>).</p><p>Die Einstellungen <code class="literal">show_bestbefore</code>
-      und <code class="literal">payments_changeable</code> aus dem
-      Abschnitt <code class="literal">features</code> und die Einstellungen im
-      Abschnitt <code class="literal">datev_check</code> (sofern schon vorhanden)
-      der <a class="link" href="ch02s04.html" title="2.4. kivitendo-Konfigurationsdatei">kivitendo-Konfigurationsdatei</a>
-      werden bei einem Datenbankupdate einer älteren Version automatisch
-      übernommen. Diese Einträge können danach aus der Konfigurationsdatei
-      entfernt werden.</p></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s16.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s18.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.16. Verhalten des Bilanzberichts&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.18. kivitendo ERP verwenden</td></tr></table></div></body></html>
\ No newline at end of file
+   <title>2.17. Verhalten des Bilanzberichts</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s16.html" title="2.16. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb"><link rel="next" href="ch02s18.html" title="2.18. Erfolgsrechnung"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.17. Verhalten des Bilanzberichts</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s16.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s18.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.17. Verhalten des Bilanzberichts"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="config.bilanz"></a>2.17. Verhalten des Bilanzberichts</h2></div></div></div><p>Bis Version 3.0 wurde "closedto" ("Bücher schließen zum") als
+      Grundlage für das Startdatum benutzt. Schließt man die Bücher allerdings
+      monatsweise führt dies zu falschen Werten.</p><p>In der Mandantenkonfiguration kann man dieses Verhalten genau
+      einstellen indem man:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>weiterhin closed_to benutzt (Default, es ändert sich nichts zu
+          vorher)</p></li><li class="listitem"><p>immer den Jahresanfang nimmt (1.1. relativ zum
+          Stichtag)</p></li><li class="listitem"><p>immer die letzte Eröffnungsbuchung als Startdatum nimmt</p><p>- mit Jahresanfang als Alternative wenn es keine EB-Buchungen
+          gibt</p><p>- oder mit "alle Buchungen" als Alternative"</p></li><li class="listitem"><p>mit Jahresanfang als Alternative wenn es keine EB-Buchungen
+          gibt</p></li><li class="listitem"><p>immer alle Buchungen seit Beginn der Datenbank nimmt</p></li></ul></div><p>Folgende Hinweise zu den Optionen: Das "Bücher schließen Datum"
+      ist sinnvoll, wenn man nur komplette Jahre schließt. Bei Wirtschaftsjahr
+      = Kalendarjahr entspricht dies aber auch dem Jahresanfang. "Alle
+      Buchungen" kann z.B. sinnvoll sein wenn man ohne Jahresabschluß
+      durchbucht. Eröffnungsbuchung mit "alle Buchungen" als Fallback ist z.B.
+      sinnvoll, wenn man am sich Anfang des zweiten Buchungsjahres befindet,
+      und noch keinen Jahreswechsel und auch noch keine EB-Buchungen hat. Bei
+      den Optionen mit EB-Buchungen wird vorausgesetzt, daß diese immer am 1.
+      Tag des Wirtschaftsjahres gebucht werden. Zur Sicherheit wird das
+      Startdatum im Bilanzbericht jetzt zusätzlich zum Stichtag mit angezeigt.
+      Das hilft auch bei der Kontrolle für den Abgleich mit der GuV bzw.
+      Erfolgsrechnung.</p></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s16.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s18.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.16. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.18. Erfolgsrechnung</td></tr></table></div></body></html>
\ No newline at end of file
index 759144e..2641cdd 100644 (file)
@@ -1,8 +1,23 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>2.18. kivitendo ERP verwenden</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s17.html" title="2.17. Einstellungen pro Mandant"><link rel="next" href="ch03.html" title="Kapitel 3. Features und Funktionen"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.18. kivitendo ERP verwenden</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s17.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch03.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.18. kivitendo ERP verwenden"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="kivitendo-ERP-verwenden"></a>2.18. kivitendo ERP verwenden</h2></div></div></div><p>Nach erfolgreicher Installation ist der Loginbildschirm unter
-      folgender URL erreichbar:</p><p>
-            <a class="ulink" href="http://localhost/kivitendo-erp/login.pl" target="_top">http://localhost/kivitendo-erp/login.pl</a>
-         </p><p>Die Administrationsseite erreichen Sie unter:</p><p>
-            <a class="ulink" href="http://localhost/kivitendo-erp/controller.pl?action=Admin/login" target="_top">http://localhost/kivitendo-erp/controller.pl?action=Admin/login</a>
-         </p></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s17.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch03.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.17. Einstellungen pro Mandant&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;Kapitel 3. Features und Funktionen</td></tr></table></div></body></html>
\ No newline at end of file
+   <title>2.18. Erfolgsrechnung</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s17.html" title="2.17. Verhalten des Bilanzberichts"><link rel="next" href="ch02s19.html" title="2.19. Rundung in Verkaufsbelegen"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.18. Erfolgsrechnung</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s17.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s19.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.18. Erfolgsrechnung"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="config.erfolgsrechnung"></a>2.18. Erfolgsrechnung</h2></div></div></div><p>Seit der Version 3.4.1 existiert in kivitendo der Bericht
+      <span class="bold"><strong> Erfolgsrechnung</strong></span>.</p><p>Die Erfolgsrechnung kann in der Mandantenkonfiguration unter
+      Features an- oder abgeschaltet werden. Mit der Einstellung
+      <code class="varname">default_manager = swiss </code> in der
+      <code class="filename">config/kivitendo.conf</code> wird beim neu Erstellen einer
+      Datenbank automatisch die Anzeige der Erfolgsrechnung im Menü
+      <span class="guimenu">Berichte </span> ausgewählt und ersetzt dort die GUV.</p><p>Im Gegensatz zur GUV werden bei der Erfolgsrechnung sämtliche
+      Aufwands- und Erlöskonten einzeln aufgelistet (analog zur Bilanz),
+      sortiert nach ERTRAG und AUFWAND.</p><p>Bei den Konteneinstellungen muss bei jedem Konto, das in der
+      Erfolgsrechnung erscheinen soll, unter <code class="varname">Sonstige
+      Einstellungen/Erfolgsrechnung</code> entweder
+      <code class="literal">01.Ertrag</code> oder <code class="literal">06.Aufwand</code>
+      ausgewählt werden.</p><p>Wird bei einem Erlöskonto <code class="literal">06.Aufwand</code>
+      ausgewählt, so wird dieses Konto als Aufwandsminderung unter AUFWAND
+      aufgelistet.</p><p>Wird bei einem Aufwandskonto <code class="literal">01.Ertrag</code>
+      ausgewählt, so wird dieses Konto als Ertragsminderung unter ERTRAG
+      aufgelistet.</p><p>Soll bei einer bereits bestehenden Buchhaltung in Zukunft
+      zusätzlich die Erfolgsrechnung als Bericht verwendet werden, so müssen
+      die Einstellungen zu allen Erlös- und Aufwandskonten unter
+      <code class="varname">Sonstige Einstellungen/Erfolgsrechnung</code> überprüft und
+      allenfalls neu gesetzt werden.</p></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s17.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s19.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.17. Verhalten des Bilanzberichts&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.19. Rundung in Verkaufsbelegen</td></tr></table></div></body></html>
\ No newline at end of file
diff --git a/doc/html/ch02s19.html b/doc/html/ch02s19.html
new file mode 100644 (file)
index 0000000..8ef8569
--- /dev/null
@@ -0,0 +1,27 @@
+<html><head>
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+   <title>2.19. Rundung in Verkaufsbelegen</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s18.html" title="2.18. Erfolgsrechnung"><link rel="next" href="ch02s20.html" title="2.20. Einstellungen pro Mandant"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.19. Rundung in Verkaufsbelegen</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s18.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s20.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.19. Rundung in Verkaufsbelegen"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="config.rounding"></a>2.19. Rundung in Verkaufsbelegen</h2></div></div></div><p>In der Schweiz hat die kleinste aktuell benutzte Münze den Wert
+      von 5 Rappen (0.05 CHF).</p><p>Auch wenn im elektronischen Zahlungsverkehr Beträge mit einer
+      Genauigkeit von 0.01 CHF verwendet werden können, ist es trotzdem nach
+      wie vor üblich, Rechnungen mit auf 0.05 CHF gerundeten Beträgen
+      auszustellen.</p><p>In kivitendo kann seit der Version 3.4.1 die Einstellung für eine
+      solche Rundung pro Mandant / Datenbank festgelegt werden.</p><p>Die Einstellung wird beim Erstellen der Datenbank bei
+      <code class="literal">Genauigkeit</code> festgelegt. Sie kann anschliessend über
+      das Webinterface von kivitendo nicht mehr verändert werden.</p><p>Abhängig vom Wert für <code class="varname">default_manager</code> in
+      <code class="filename">config/kivitendo.conf</code> werden dabei folgende Werte
+      voreingestellt:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>0.05 (default_manager = swiss)</p></li><li class="listitem"><p>0.01 (default_manager = german)</p></li></ul></div><p>Der Wert wird in der Datenbank in der Tabelle <code class="varname">defaults
+      </code>in der Spalte <code class="varname">precision</code> gespeichert.</p><p>In allen Verkaufsangeboten, Verkaufsaufträgen, Verkaufsrechnungen
+      und Verkaufsgutschriften wird der Endbetrag inkl. MWST gerundet, wenn
+      dieser nicht der eingestellten Genauigkeit entspricht.</p><p>Beim Buchen einer Verkaufsrechnung wird der Rundungsbetrag
+      automatisch auf die in der Mandantenkonfiguration festgelegten
+      Standardkonten für Rundungserträge bzw. Rundungsaufwendungen
+      gebucht.</p><p>(Die berechnete MWST wird durch den Rundungsbetrag nicht mehr
+      verändert.)</p><p>Die in den Druckvorlagen zur Verfügung stehenden Variablen
+      <code class="varname">quototal</code>, <code class="varname">ordtotal</code> bzw.
+      <code class="varname">invtotal</code> enthalten den gerundeten Betrag.</p><p>
+            <span class="bold"><strong>Achtung:</strong></span> Werden Verkaufsbelege in
+      anderen Währungen als der Standardwährung erstellt, so muss in kivitendo
+      ab Version 3.4.1 die Genauigkeit 0.01 verwendet werden.</p><p>Das heisst, Firmen in der Schweiz, die teilweise
+      Verkaufsrechnungen in Euro oder anderen Währungen erstellen wollen,
+      müssen beim Erstellen der Datenbank als Genauigkeit 0.01 wählen und
+      können zur Zeit die 5er Rundung noch nicht nutzen.</p></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s18.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s20.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.18. Erfolgsrechnung&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.20. Einstellungen pro Mandant</td></tr></table></div></body></html>
\ No newline at end of file
diff --git a/doc/html/ch02s20.html b/doc/html/ch02s20.html
new file mode 100644 (file)
index 0000000..ac383fb
--- /dev/null
@@ -0,0 +1,16 @@
+<html><head>
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+   <title>2.20. Einstellungen pro Mandant</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s19.html" title="2.19. Rundung in Verkaufsbelegen"><link rel="next" href="ch02s21.html" title="2.21. kivitendo ERP verwenden"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.20. Einstellungen pro Mandant</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s19.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch02s21.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.20. Einstellungen pro Mandant"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="config.client"></a>2.20. Einstellungen pro Mandant</h2></div></div></div><p>Einige Einstellungen können von einem Benutzer mit dem <a class="link" href="ch02s09.html#Zusammenh%C3%A4nge" title="2.9.1. Zusammenhänge">Recht</a> "Administration (Für die Verwaltung
+      der aktuellen Instanz aus einem Userlogin heraus)" gemacht werden. Diese
+      Einstellungen sind dann für die aktuellen Mandanten-Datenbank gültig.
+      Die Einstellungen sind unter <span class="guimenu">System</span> →
+      <span class="guisubmenu">Mandantenkonfiguration</span> erreichbar.</p><p>Bitte beachten Sie die Hinweise zu den einzelnen Einstellungen.
+      Einige Einstellungen sollten nicht ohne Weiteres im laufenden Betrieb
+      geändert werden (siehe auch <a class="link" href="ch02s15.html#config.eur.inventory-system-perpetual" title="2.15.4. Bemerkungen zur Bestandsmethode">Bemerkungen zu
+      Bestandsmethode</a>).</p><p>Die Einstellungen <code class="literal">show_bestbefore</code> und
+      <code class="literal">payments_changeable</code> aus dem Abschnitt
+      <code class="literal">features</code> und die Einstellungen im Abschnitt
+      <code class="literal">datev_check</code> (sofern schon vorhanden) der <a class="link" href="ch02s04.html" title="2.4. kivitendo-Konfigurationsdatei">kivitendo-Konfigurationsdatei</a> werden
+      bei einem Datenbankupdate einer älteren Version automatisch übernommen.
+      Diese Einträge können danach aus der Konfigurationsdatei entfernt
+      werden.</p></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s19.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch02s21.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.19. Rundung in Verkaufsbelegen&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;2.21. kivitendo ERP verwenden</td></tr></table></div></body></html>
\ No newline at end of file
diff --git a/doc/html/ch02s21.html b/doc/html/ch02s21.html
new file mode 100644 (file)
index 0000000..2bad9f7
--- /dev/null
@@ -0,0 +1,8 @@
+<html><head>
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+   <title>2.21. kivitendo ERP verwenden</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch02.html" title="Kapitel 2. Installation und Grundkonfiguration"><link rel="prev" href="ch02s20.html" title="2.20. Einstellungen pro Mandant"><link rel="next" href="ch03.html" title="Kapitel 3. Features und Funktionen"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">2.21. kivitendo ERP verwenden</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s20.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 2. Installation und Grundkonfiguration</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch03.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="2.21. kivitendo ERP verwenden"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="kivitendo-ERP-verwenden"></a>2.21. kivitendo ERP verwenden</h2></div></div></div><p>Nach erfolgreicher Installation ist der Loginbildschirm unter
+      folgender URL erreichbar:</p><p>
+            <a class="ulink" href="http://localhost/kivitendo-erp/login.pl" target="_top">http://localhost/kivitendo-erp/login.pl</a>
+         </p><p>Die Administrationsseite erreichen Sie unter:</p><p>
+            <a class="ulink" href="http://localhost/kivitendo-erp/controller.pl?action=Admin/login" target="_top">http://localhost/kivitendo-erp/controller.pl?action=Admin/login</a>
+         </p></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s20.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch02.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch03.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.20. Einstellungen pro Mandant&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;Kapitel 3. Features und Funktionen</td></tr></table></div></body></html>
\ No newline at end of file
index 2c1f049..d75edb0 100644 (file)
@@ -1,6 +1,6 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>Kapitel 3. Features und Funktionen</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="prev" href="ch02s18.html" title="2.18. kivitendo ERP verwenden"><link rel="next" href="ch03s02.html" title="3.2. Bankerweiterung"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">Kapitel 3. Features und Funktionen</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s18.html">Zurück</a>&nbsp;</td><th width="60%" align="center">&nbsp;</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch03s02.html">Weiter</a></td></tr></table><hr></div><div class="chapter" title="Kapitel 3. Features und Funktionen"><div class="titlepage"><div><div><h2 class="title"><a name="features"></a>Kapitel 3. Features und Funktionen</h2></div></div></div><div class="sect1" title="3.1. Wiederkehrende Rechnungen"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="features.periodic-invoices"></a>3.1. Wiederkehrende Rechnungen</h2></div></div></div><div class="sect2" title="3.1.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="features.periodic-invoices.introduction"></a>3.1.1. Einführung</h3></div></div></div><p>Wiederkehrende Rechnungen werden als normale Aufträge definiert
+   <title>Kapitel 3. Features und Funktionen</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="prev" href="ch02s21.html" title="2.21. kivitendo ERP verwenden"><link rel="next" href="ch03s02.html" title="3.2. Bankerweiterung"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">Kapitel 3. Features und Funktionen</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch02s21.html">Zurück</a>&nbsp;</td><th width="60%" align="center">&nbsp;</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch03s02.html">Weiter</a></td></tr></table><hr></div><div class="chapter" title="Kapitel 3. Features und Funktionen"><div class="titlepage"><div><div><h2 class="title"><a name="features"></a>Kapitel 3. Features und Funktionen</h2></div></div></div><div class="sect1" title="3.1. Wiederkehrende Rechnungen"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="features.periodic-invoices"></a>3.1. Wiederkehrende Rechnungen</h2></div></div></div><div class="sect2" title="3.1.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="features.periodic-invoices.introduction"></a>3.1.1. Einführung</h3></div></div></div><p>Wiederkehrende Rechnungen werden als normale Aufträge definiert
         und konfiguriert, mit allen dazugehörigen Kunden- und Artikelangaben.
         Die konfigurierten Aufträge werden später automatisch in Rechnungen
         umgewandelt, so als ob man den Workflow benutzen würde, und auch die
@@ -12,8 +12,8 @@
         Parameter einstellen kann. Hinter dem Knopf wird außerdem noch
         angezeigt, ob der Auftrag als wiederkehrende Rechnung konfiguriert ist
         oder nicht.</p><p>Folgende Parameter kann man konfigurieren:</p><div class="variablelist"><dl><dt><span class="term">Status</span></dt><dd><p>Bei aktiven Rechnungen wird automatisch eine Rechnung
-              erstellt, wenn die Periodizität erreicht ist (z.B. am Anfang eines
-              neuen Monats).</p><p>Ist ein Auftrag nicht aktiv, so werden für ihn auch keine
+              erstellt, wenn die Periodizität erreicht ist (z.B. am Anfang
+              eines neuen Monats).</p><p>Ist ein Auftrag nicht aktiv, so werden für ihn auch keine
               wiederkehrenden Rechnungen erzeugt. Stellt man nach längerer
               nicht-aktiver Zeit einen Auftrag wieder auf aktiv, wird beim
               nächsten Periodenwechsel für alle Perioden, seit der letzten
         Konfiguriert wird dies in der <a class="link" href="ch02s04.html#config.config-file.sections-parameters" title="2.4.2. Abschnitte und Parameter">Konfigurationsdatei</a>
         
                <code class="filename">config/kivitendo.conf</code> im Abschnitt
-        <code class="varname">[periodic_invoices]</code>.</p></div><div class="sect2" title="3.1.3. Spezielle Variablen"><div class="titlepage"><div><div><h3 class="title"><a name="features.periodic-invoices.variables"></a>3.1.3. Spezielle Variablen</h3></div></div></div><p>
-          Um die erzeugten Rechnungen individualisieren zu können, werden beim Umwandeln des Auftrags in eine Rechnung einige speziell
-          formatierte Variablen durch für die jeweils aktuelle Abrechnungsperiode gültigen Werte ersetzt. Damit ist es möglich, z.B. den
-          Abrechnungszeitraum explizit auszuweisen. Eine Variable hat dabei die Syntax <code class="literal">&lt;%variablenname%&gt;</code>.
-        </p><p>
-         Sofern es sich um eine Datumsvariable handelt, kann das Ausgabeformat weiter bestimmt werden, indem an den Variablennamen
-         Formatoptionen angehängt werden. Die Syntax sieht dabei wie folgt aus: <code class="literal">&lt;%variablenname
-         FORMAT=Formatinformation%&gt;</code>. Die zur verfügung stehenden Formatinformationen werden unten genauer beschrieben.
-        </p><p>
-          Diese Variablen werden in den folgenden Elementen des Auftrags ersetzt:
-        </p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Bemerkungen</p></li><li class="listitem"><p>Interne Bemerkungen</p></li><li class="listitem"><p>Vorgangsbezeichnung</p></li><li class="listitem"><p>In den Beschreibungs- und Langtextfeldern aller Positionen</p></li></ul></div><p>Die zur Verfügung stehenden Variablen sind die Folgenden:</p><div class="variablelist"><dl><dt><span class="term">
-                     <code class="varname">&lt;%current_quarter%&gt;</code>, <code class="varname">&lt;%previous_quarter%&gt;</code>, <code class="varname">&lt;%next_quarter%&gt;</code>
-                  </span></dt><dd><p>
-                Aktuelles, vorheriges und nächstes Quartal als Zahl zwischen <code class="literal">1</code> und <code class="literal">4</code>.
-              </p></dd><dt><span class="term">
-                     <code class="varname">&lt;%current_month%&gt;</code>, <code class="varname">&lt;%previous_month%&gt;</code>, <code class="varname">&lt;%next_month%&gt;</code>
-                  </span></dt><dd><p>
-                Aktueller, vorheriger und nächster Monat als Zahl zwischen <code class="literal">1</code> und <code class="literal">12</code>.
-              </p></dd><dt><span class="term">
-                     <code class="varname">&lt;%current_month_long%&gt;</code>, <code class="varname">&lt;%previous_month_long%&gt;</code>, <code class="varname">&lt;%next_month_long%&gt;</code>
-                  </span></dt><dd><p>
-                Aktueller, vorheriger und nächster Monat als Name (<code class="literal">Januar</code>, <code class="literal">Februar</code> etc.).
-              </p></dd><dt><span class="term">
-                     <code class="varname">&lt;%current_year%&gt;</code>, <code class="varname">&lt;%previous_year%&gt;</code>, <code class="varname">&lt;%next_year%&gt;</code>
-                  </span></dt><dd><p>
-                Aktuelles, vorheriges und nächstes Jahr als vierstellige Jahreszahl (<code class="literal">2013</code> etc.).
-              </p></dd><dt><span class="term">
-                     <code class="varname">&lt;%period_start_date%&gt;</code>, <code class="varname">&lt;%period_end_date%&gt;</code>
-                  </span></dt><dd><p>
-                Formatiertes Datum des ersten und letzten Tages im Abrechnungszeitraum (z.B. bei quartalsweiser Abrechnung und im ersten
-                Quartal von 2013 wären dies der <code class="literal">01.01.2013</code> und <code class="literal">31.03.2013</code>).
-              </p></dd></dl></div><p>
-         Die invidiuellen Formatinformationen bestehen aus Paaren von Prozentzeichen und einem Buchstaben, welche beide zusammen durch den
-         dazugehörigen Wert ersetzt werden. So wird z.B. <code class="literal">%Y</code> durch das viertstellige Jahr ersetzt. Alle möglichen
-         Platzhalter sind:
-        </p><div class="variablelist"><dl><dt><span class="term">
+        <code class="varname">[periodic_invoices]</code>.</p></div><div class="sect2" title="3.1.3. Spezielle Variablen"><div class="titlepage"><div><div><h3 class="title"><a name="features.periodic-invoices.variables"></a>3.1.3. Spezielle Variablen</h3></div></div></div><p>Um die erzeugten Rechnungen individualisieren zu können, werden
+        beim Umwandeln des Auftrags in eine Rechnung einige speziell
+        formatierte Variablen durch für die jeweils aktuelle
+        Abrechnungsperiode gültigen Werte ersetzt. Damit ist es möglich, z.B.
+        den Abrechnungszeitraum explizit auszuweisen. Eine Variable hat dabei
+        die Syntax <code class="literal">&lt;%variablenname%&gt;</code>.</p><p>Sofern es sich um eine Datumsvariable handelt, kann das
+        Ausgabeformat weiter bestimmt werden, indem an den Variablennamen
+        Formatoptionen angehängt werden. Die Syntax sieht dabei wie folgt aus:
+        <code class="literal">&lt;%variablenname FORMAT=Formatinformation%&gt;</code>.
+        Die zur verfügung stehenden Formatinformationen werden unten genauer
+        beschrieben.</p><p>Diese Variablen können auch beim automatischen Versand der
+        erzeugten Rechnungen per E-Mail genutzt werden, indem sie in den
+        Feldern für den Betreff oder die Nachricht verwendet werden.</p><p>Diese Variablen werden in den folgenden Elementen des Auftrags
+        ersetzt:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Bemerkungen</p></li><li class="listitem"><p>Interne Bemerkungen</p></li><li class="listitem"><p>Vorgangsbezeichnung</p></li><li class="listitem"><p>In den Beschreibungs- und Langtextfeldern aller
+            Positionen</p></li></ul></div><p>Die zur Verfügung stehenden Variablen sind die Folgenden:</p><div class="variablelist"><dl><dt><span class="term">
+                     <code class="varname">&lt;%current_quarter%&gt;</code>,
+            <code class="varname">&lt;%previous_quarter%&gt;</code>,
+            <code class="varname">&lt;%next_quarter%&gt;</code>
+                  </span></dt><dd><p>Aktuelles, vorheriges und nächstes Quartal als Zahl
+              zwischen <code class="literal">1</code> und <code class="literal">4</code>.</p></dd><dt><span class="term">
+                     <code class="varname">&lt;%current_month%&gt;</code>,
+            <code class="varname">&lt;%previous_month%&gt;</code>,
+            <code class="varname">&lt;%next_month%&gt;</code>
+                  </span></dt><dd><p>Aktueller, vorheriger und nächster Monat als Zahl zwischen
+              <code class="literal">1</code> und <code class="literal">12</code>.</p></dd><dt><span class="term">
+                     <code class="varname">&lt;%current_month_long%&gt;</code>,
+            <code class="varname">&lt;%previous_month_long%&gt;</code>,
+            <code class="varname">&lt;%next_month_long%&gt;</code>
+                  </span></dt><dd><p>Aktueller, vorheriger und nächster Monat als Name
+              (<code class="literal">Januar</code>, <code class="literal">Februar</code>
+              etc.).</p></dd><dt><span class="term">
+                     <code class="varname">&lt;%current_year%&gt;</code>,
+            <code class="varname">&lt;%previous_year%&gt;</code>,
+            <code class="varname">&lt;%next_year%&gt;</code>
+                  </span></dt><dd><p>Aktuelles, vorheriges und nächstes Jahr als vierstellige
+              Jahreszahl (<code class="literal">2013</code> etc.).</p></dd><dt><span class="term">
+                     <code class="varname">&lt;%period_start_date%&gt;</code>,
+            <code class="varname">&lt;%period_end_date%&gt;</code>
+                  </span></dt><dd><p>Formatiertes Datum des ersten und letzten Tages im
+              Abrechnungszeitraum (z.B. bei quartalsweiser Abrechnung und im
+              ersten Quartal von 2013 wären dies der
+              <code class="literal">01.01.2013</code> und
+              <code class="literal">31.03.2013</code>).</p></dd></dl></div><p>Die invidiuellen Formatinformationen bestehen aus Paaren von
+        Prozentzeichen und einem Buchstaben, welche beide zusammen durch den
+        dazugehörigen Wert ersetzt werden. So wird z.B. <code class="literal">%Y</code>
+        durch das viertstellige Jahr ersetzt. Alle möglichen Platzhalter
+        sind:</p><div class="variablelist"><dl><dt><span class="term">
                      <code class="varname">%a</code>
                   </span></dt><dd><p>Der abgekürzte Wochentagsname.</p></dd><dt><span class="term">
                      <code class="varname">%A</code>
                      <code class="varname">%B</code>
                   </span></dt><dd><p>Der ausgeschriebene Monatsname.</p></dd><dt><span class="term">
                      <code class="varname">%C</code>
-                  </span></dt><dd><p>Das Jahrhundert (Jahr/100) als eine zweistellige Zahl.</p></dd><dt><span class="term">
+                  </span></dt><dd><p>Das Jahrhundert (Jahr/100) als eine zweistellige
+              Zahl.</p></dd><dt><span class="term">
                      <code class="varname">%d</code>
                   </span></dt><dd><p>Der Monatstag als Zahl zwischen 01 und 31.</p></dd><dt><span class="term">
                      <code class="varname">%D</code>
                   </span></dt><dd><p>Entspricht %m/%d/%y (amerikanisches Datumsformat).</p></dd><dt><span class="term">
                      <code class="varname">%e</code>
-                  </span></dt><dd><p>Wie %d (Monatstag als Zahl zwischen 1 und 31), allerdings werden führende Nullen durch Leerzeichen ersetzt.</p></dd><dt><span class="term">
+                  </span></dt><dd><p>Wie %d (Monatstag als Zahl zwischen 1 und 31), allerdings
+              werden führende Nullen durch Leerzeichen ersetzt.</p></dd><dt><span class="term">
                      <code class="varname">%F</code>
                   </span></dt><dd><p>Entspricht %Y-%m-%d (das ISO-8601-Datumsformat).</p></dd><dt><span class="term">
                      <code class="varname">%j</code>
-                  </span></dt><dd><p>Der Tag im Jahr als Zahl zwischen 001 und 366 inklusive.</p></dd><dt><span class="term">
+                  </span></dt><dd><p>Der Tag im Jahr als Zahl zwischen 001 und 366
+              inklusive.</p></dd><dt><span class="term">
                      <code class="varname">%m</code>
                   </span></dt><dd><p>Der Monat als Zahl zwischen 01 und 12 inklusive.</p></dd><dt><span class="term">
                      <code class="varname">%u</code>
-                  </span></dt><dd><p>Der Wochentag als Zahl zwischen 1 und 7 inklusive, wobei die 1 dem Montag entspricht.</p></dd><dt><span class="term">
+                  </span></dt><dd><p>Der Wochentag als Zahl zwischen 1 und 7 inklusive, wobei
+              die 1 dem Montag entspricht.</p></dd><dt><span class="term">
                      <code class="varname">%U</code>
-                  </span></dt><dd><p>Die Wochennummer als Zahl zwischen 00 und 53 inklusive, wobei der erste Sonntag im Jahr das Startdatum von Woche 01 ist.</p></dd><dt><span class="term">
+                  </span></dt><dd><p>Die Wochennummer als Zahl zwischen 00 und 53 inklusive,
+              wobei der erste Sonntag im Jahr das Startdatum von Woche 01
+              ist.</p></dd><dt><span class="term">
                      <code class="varname">%V</code>
-                  </span></dt><dd><p>Die ISO-8601:1988-Wochennummer als Zahl zwischen 01 und 53 inklusive, wobei Woche 01 die erste Woche, von der mindestens vier Tage im Jahr liegen; Montag ist erster Tag der Woche.</p></dd><dt><span class="term">
+                  </span></dt><dd><p>Die ISO-8601:1988-Wochennummer als Zahl zwischen 01 und 53
+              inklusive, wobei Woche 01 die erste Woche, von der mindestens
+              vier Tage im Jahr liegen; Montag ist erster Tag der
+              Woche.</p></dd><dt><span class="term">
                      <code class="varname">%w</code>
-                  </span></dt><dd><p>Der Wochentag als Zahl zwischen 0 und 6 inklusive, wobei die 0 dem Sonntag entspricht.</p></dd><dt><span class="term">
+                  </span></dt><dd><p>Der Wochentag als Zahl zwischen 0 und 6 inklusive, wobei
+              die 0 dem Sonntag entspricht.</p></dd><dt><span class="term">
                      <code class="varname">%W</code>
-                  </span></dt><dd><p>Die Wochennummer als Zahl zwischen 00 und 53 inklusive, wobei der erste Montag im Jahr das Startdatum von Woche 01 ist.</p></dd><dt><span class="term">
+                  </span></dt><dd><p>Die Wochennummer als Zahl zwischen 00 und 53 inklusive,
+              wobei der erste Montag im Jahr das Startdatum von Woche 01
+              ist.</p></dd><dt><span class="term">
                      <code class="varname">%y</code>
-                  </span></dt><dd><p>Das Jahr als zweistellige Zahl zwischen 00 und 99 inklusive.</p></dd><dt><span class="term">
+                  </span></dt><dd><p>Das Jahr als zweistellige Zahl zwischen 00 und 99
+              inklusive.</p></dd><dt><span class="term">
                      <code class="varname">%Y</code>
                   </span></dt><dd><p>Das Jahr als vierstellige Zahl.</p></dd><dt><span class="term">
                      <code class="varname">%%</code>
-                  </span></dt><dd><p>Das Prozentzeichen selber.</p></dd></dl></div><p>
-         Anwendungsbeispiel für die Ausgabe, von welchem Monat und Jahr bis zu welchem Monat und Jahr die aktuelle Abrechnungsperiode
-         dauert: <code class="literal">Abrechnungszeitrum: &lt;%period_start_date FORMAT=%m/%Y%&gt; bis &lt;%period_end_date FORMAT=%m/%Y%&gt;</code>
-        
-            </p></div><div class="sect2" title="3.1.4. Auflisten"><div class="titlepage"><div><div><h3 class="title"><a name="features.periodic-invoices.reports"></a>3.1.4. Auflisten</h3></div></div></div><p>Unter Verkauf-&gt;Berichte-&gt;Aufträge finden sich zwei neue
+                  </span></dt><dd><p>Das Prozentzeichen selber.</p></dd></dl></div><p>Anwendungsbeispiel für die Ausgabe, von welchem Monat und Jahr
+        bis zu welchem Monat und Jahr die aktuelle Abrechnungsperiode dauert:
+        <code class="literal">Abrechnungszeitrum: &lt;%period_start_date FORMAT=%m/%Y%&gt;
+        bis &lt;%period_end_date FORMAT=%m/%Y%&gt;</code>
+            </p><p>Beim automatischen Versand der Rechnugen via E-Mail können neben diesen speziellen Variablen auch einige Eigenschaften der
+        Rechnung selber als Variablen im Betreff &amp; dem Text der E-Mails genutzt werden. Beispiele sind
+        <code class="varname">&lt;%invnumber%&gt;</code> für die Rechnungsnummber oder <code class="varname">&lt;transaction_description%&gt;</code> für die
+        Vorgangsbezeichnung. Diese Variablen stehen beim Erzeugen der Rechnung logischerweise noch nicht zur Verfügung.</p></div><div class="sect2" title="3.1.4. Auflisten"><div class="titlepage"><div><div><h3 class="title"><a name="features.periodic-invoices.reports"></a>3.1.4. Auflisten</h3></div></div></div><p>Unter Verkauf-&gt;Berichte-&gt;Aufträge finden sich zwei neue
         Checkboxen, "Wiederkehrende Rechnungen aktiv" und "Wiederkehrende
         Rechnungen inaktiv", mit denen man sich einen Überglick über die
         wiederkehrenden Rechnungen verschaffen kann.</p></div><div class="sect2" title="3.1.5. Erzeugung der eigentlichen Rechnungen"><div class="titlepage"><div><div><h3 class="title"><a name="features.periodic-invoices.task-server"></a>3.1.5. Erzeugung der eigentlichen Rechnungen</h3></div></div></div><p>Die zeitliche und periodische Überprüfung, ob eine
         wiederkehrende Rechnung automatisch erstellt werden soll, geschieht
-        durch den <a class="link" href="ch02s07.html" title="2.7. Der Task-Server">Taskserver</a>, einen
+        durch den <a class="link" href="ch02s07.html" title="2.7. Der Task-Server">Task-Server</a>, einen
         externen Dienst, der automatisch beim Start des Servers gestartet
         werden sollte.</p></div><div class="sect2" title="3.1.6. Erste Rechnung für aktuellen Monat erstellen"><div class="titlepage"><div><div><h3 class="title"><a name="features.periodic-invoices.create-for-current-month"></a>3.1.6. Erste Rechnung für aktuellen Monat erstellen</h3></div></div></div><p>Will man im laufenden Monat eine monatlich wiederkehrende
         Rechnung inkl. des laufenden Monats starten, stellt man das Startdatum
-        auf den Monatsanfang und wartet ein paar Minuten, bis der Taskserver
+        auf den Monatsanfang und wartet ein paar Minuten, bis der Task-Server
         den neu konfigurieren Auftrag erkennt und daraus eine Rechnung
         generiert hat. Alternativ setzt man das Startdatum auf den
         Monatsersten des Folgemonats und erstellt die erste Rechnung direkt
-        manuell über den Workflow.</p></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s18.html">Zurück</a>&nbsp;</td><td width="20%" align="center">&nbsp;</td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch03s02.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.18. kivitendo ERP verwenden&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;3.2. Bankerweiterung</td></tr></table></div></body></html>
\ No newline at end of file
+        manuell über den Workflow.</p></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch02s21.html">Zurück</a>&nbsp;</td><td width="20%" align="center">&nbsp;</td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch03s02.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">2.21. kivitendo ERP verwenden&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;3.2. Bankerweiterung</td></tr></table></div></body></html>
\ No newline at end of file
index 02b0d12..1c2668d 100644 (file)
@@ -1,5 +1,6 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>3.2. Bankerweiterung</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch03.html" title="Kapitel 3. Features und Funktionen"><link rel="prev" href="ch03.html" title="Kapitel 3. Features und Funktionen"><link rel="next" href="ch03s03.html" title="3.3. Dokumentenvorlagen und verfügbare Variablen"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">3.2. Bankerweiterung</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch03.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 3. Features und Funktionen</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch03s03.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="3.2. Bankerweiterung"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="features.bank"></a>3.2. Bankerweiterung</h2></div></div></div><div class="sect2" title="3.2.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="features.bank.introduction"></a>3.2.1. Einführung</h3></div></div></div><p>Die Beschreibung der Bankerweiterung befindet sich derzeit noch im Wiki und soll von dort später hierhin übernommen werden:</p><p>
+   <title>3.2. Bankerweiterung</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch03.html" title="Kapitel 3. Features und Funktionen"><link rel="prev" href="ch03.html" title="Kapitel 3. Features und Funktionen"><link rel="next" href="ch03s03.html" title="3.3. Dokumentenvorlagen und verfügbare Variablen"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">3.2. Bankerweiterung</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch03.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 3. Features und Funktionen</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch03s03.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="3.2. Bankerweiterung"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="features.bank"></a>3.2. Bankerweiterung</h2></div></div></div><div class="sect2" title="3.2.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="features.bank.introduction"></a>3.2.1. Einführung</h3></div></div></div><p>Die Beschreibung der Bankerweiterung befindet sich derzeit noch
+        im Wiki und soll von dort später hierhin übernommen werden:</p><p>
                <a class="ulink" href="http://redmine.kivitendo-premium.de/projects/forum/wiki/Bankerweiterung" target="_top">http://redmine.kivitendo-premium.de/projects/forum/wiki/Bankerweiterung</a>
             </p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch03.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch03.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch03s03.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">Kapitel 3. Features und Funktionen&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;3.3. Dokumentenvorlagen und verfügbare Variablen</td></tr></table></div></body></html>
\ No newline at end of file
index 0cb9012..7c455f0 100644 (file)
@@ -1,14 +1,13 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>3.3. Dokumentenvorlagen und verfügbare Variablen</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch03.html" title="Kapitel 3. Features und Funktionen"><link rel="prev" href="ch03s02.html" title="3.2. Bankerweiterung"><link rel="next" href="ch03s04.html" title="3.4. Excel-Vorlagen"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">3.3. Dokumentenvorlagen und verfügbare Variablen</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch03s02.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 3. Features und Funktionen</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch03s04.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="3.3. Dokumentenvorlagen und verfügbare Variablen"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="dokumentenvorlagen-und-variablen"></a>3.3. Dokumentenvorlagen und verfügbare Variablen</h2></div></div></div><div class="sect2" title="3.3.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="dokumentenvorlagen-und-variablen.einf%C3%BChrung"></a>3.3.1. Einführung</h3></div></div></div><p>Dies ist eine Auflistung der Standard-Dokumentenvorlagen und
+   <title>3.3. Dokumentenvorlagen und verfügbare Variablen</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch03.html" title="Kapitel 3. Features und Funktionen"><link rel="prev" href="ch03s02.html" title="3.2. Bankerweiterung"><link rel="next" href="ch03s04.html" title="3.4. Excel-Vorlagen"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">3.3. Dokumentenvorlagen und verfügbare Variablen</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch03s02.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 3. Features und Funktionen</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch03s04.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="3.3. Dokumentenvorlagen und verfügbare Variablen"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="dokumentenvorlagen-und-variablen"></a>3.3. Dokumentenvorlagen und verfügbare Variablen</h2></div></div></div><div class="sect2" title="3.3.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="dokumentenvorlagen-und-variablen.einf%C3%BChrung"></a>3.3.1. Einführung</h3></div></div></div><p>Dies ist eine Auflistung der Standard-Dokumentenvorlagen und
         aller zur Bearbeitung verfügbaren Variablen. Eine Variable wird in
         einer Vorlage durch ihren Inhalt ersetzt, wenn sie in der Form
         <code class="function">&lt;%variablenname%&gt;</code> verwendet wird. Für
         LaTeX- und HTML-Vorlagen kann man die Form dieser Tags auch verändern
-        (siehe <a class="xref" href="ch03s03.html#dokumentenvorlagen-und-variablen.tag-style" title="3.3.4. Anfang und Ende der Tags verändern">Anfang und Ende der Tags verändern</a>).</p><p>Früher wurde hier nur über LaTeX gesprochen. Inzwischen
-        unterstützt kivitendo aber auch OpenDocument-Vorlagen. Sofern es nicht
-        ausdrücklich eingeschränkt wird, gilt das im Folgenden gesagte für
-        alle Vorlagenarten.</p><p>Insgesamt sind technisch gesehen eine ganze Menge mehr Variablen
+        (siehe <a class="xref" href="ch03s03.html#dokumentenvorlagen-und-variablen.tag-style" title="3.3.4. Anfang und Ende der Tags verändern">Anfang und Ende der Tags verändern</a>).</p><p>kivitendo unterstützt LaTeX-, HTML- und OpenDocument-Vorlagen.
+        Sofern es nicht ausdrücklich eingeschränkt wird, gilt das im Folgenden
+        gesagte für alle Vorlagenarten.</p><p>Insgesamt sind technisch gesehen eine ganze Menge mehr Variablen
         verfügbar als hier aufgelistet werden. Die meisten davon können
         allerdings innerhalb einer solchen Vorlage nicht sinnvoll verwendet
         werden. Wenn eine Auflistung dieser Variablen gewollt ist, so kann
                         <code class="varname">template_meta.language.description</code>
                      </span></dt><dd><p>Beschreibung der verwendeten Sprache</p></dd><dt><span class="term">
                         <code class="varname">template_meta.language.template_code</code>
-                     </span></dt><dd><p>Vorlagenürzel der verwendeten Sprache, identisch mit dem
-                Kürzel das im Dateinamen verwendetet wird.</p></dd><dt><span class="term">
+                     </span></dt><dd><p>Vorlagenkürzel der verwendeten Sprache, identisch mit
+                dem Kürzel das im Dateinamen verwendetet wird.</p></dd><dt><span class="term">
                         <code class="varname">template_meta.language.output_numberformat</code>
                      </span></dt><dd><p>Zahlenformat der verwendeten Sprache in der Form
                 "<code class="constant">1.000,00</code>". Experimentell! Nur
                      </span></dt><dd><p>Emailadresse</p></dd><dt><span class="term">
                         <code class="varname">fax</code>
                      </span></dt><dd><p>Faxnummer</p></dd><dt><span class="term">
+                        <code class="varname">gln</code>
+                     </span></dt><dd><p>GLN (Globale Lokationsnummer)</p></dd><dt><span class="term">
                         <code class="varname">greeting</code>
                      </span></dt><dd><p>Anrede</p></dd><dt><span class="term">
                         <code class="varname">homepage</code>
                      </span></dt><dd><p>Sprache</p></dd><dt><span class="term">
                         <code class="varname">name</code>
                      </span></dt><dd><p>Firmenname</p></dd><dt><span class="term">
+                        <code class="varname">natural_person</code>
+                     </span></dt><dd><p>Flag "natürliche Person"; Siehe auch
+                <a class="xref" href="ch03s03.html#dokumentenvorlagen-und-variablen.anrede" title="3.3.13. Hinweise zur Anrede">Hinweise zur Anrede</a>
+                        </p></dd><dt><span class="term">
                         <code class="varname">payment_description</code>
                      </span></dt><dd><p>Name der Zahlart</p></dd><dt><span class="term">
                         <code class="varname">payment_terms</code>
                         <code class="varname">shiptocountry</code>
                      </span></dt><dd><p>Land (Lieferadresse) <a class="link" href="ch03s03.html#dokumentenvorlagen-und-variablen.anmerkung-shipto" title="Anmerkung">*</a>
                         </p></dd><dt><span class="term">
-                        <code class="varname">shiptodepartment1</code>
+                        <code class="varname">shiptodepartment_1</code>
                      </span></dt><dd><p>Abteilung 1 (Lieferadresse) <a class="link" href="ch03s03.html#dokumentenvorlagen-und-variablen.anmerkung-shipto" title="Anmerkung">*</a>
                         </p></dd><dt><span class="term">
-                        <code class="varname">shiptodepartment2</code>
+                        <code class="varname">shiptodepartment_2</code>
                      </span></dt><dd><p>Abteilung 2 (Lieferadresse) <a class="link" href="ch03s03.html#dokumentenvorlagen-und-variablen.anmerkung-shipto" title="Anmerkung">*</a>
                         </p></dd><dt><span class="term">
                         <code class="varname">shiptoemail</code>
                         </p></dd><dt><span class="term">
                         <code class="varname">shiptofax</code>
                      </span></dt><dd><p>Fax (Lieferadresse) <a class="link" href="ch03s03.html#dokumentenvorlagen-und-variablen.anmerkung-shipto" title="Anmerkung">*</a>
+                        </p></dd><dt><span class="term">
+                        <code class="varname">shiptogln</code>
+                     </span></dt><dd><p>GLN (Globale Lokationsnummer) (Lieferadresse) <a class="link" href="ch03s03.html#dokumentenvorlagen-und-variablen.anmerkung-shipto" title="Anmerkung">*</a>
                         </p></dd><dt><span class="term">
                         <code class="varname">shiptoname</code>
                      </span></dt><dd><p>Firmenname (Lieferadresse) <a class="link" href="ch03s03.html#dokumentenvorlagen-und-variablen.anmerkung-shipto" title="Anmerkung">*</a>
                         <code class="varname">delivery_term.description</code>
                      </span></dt><dd><p>Beschreibung der Lieferbedingung</p></dd><dt><span class="term">
                         <code class="varname">delivery_term.description_long</code>
-                     </span></dt><dd><p>Langtext bzw. übersetzter Langtext der Lieferbedingung</p></dd></dl></div></div></div><div class="sect2" title="3.3.8. Variablen in Rechnungen"><div class="titlepage"><div><div><h3 class="title"><a name="dokumentenvorlagen-und-variablen.invoice"></a>3.3.8. Variablen in Rechnungen</h3></div></div></div><div class="sect3" title="3.3.8.1. Allgemeine Variablen"><div class="titlepage"><div><div><h4 class="title"><a name="dokumentenvorlagen-und-variablen.invoice-allgemein"></a>3.3.8.1. Allgemeine Variablen</h4></div></div></div><div class="variablelist"><dl><dt><span class="term">
+                     </span></dt><dd><p>Langtext bzw. übersetzter Langtext der
+                Lieferbedingung</p></dd></dl></div></div><div class="sect3" title="3.3.7.7. Informationen über abweichende Rechnungsadressen (nur Verkaufsbelege)"><div class="titlepage"><div><div><h4 class="title"><a name="dokumentenvorlagen-und-variablen.abweichende-rechnungsadresse"></a>3.3.7.7. Informationen über abweichende Rechnungsadressen (nur Verkaufsbelege)</h4></div></div></div><p>
+            Abweichende Rechnungsadressen gibt es nur in Verkaufsbelegen. Die entsprechenden Variablen sind nur dann mit Inhalt gefüllt,
+            wenn im Beleg eine abweichende Rechnungsadresse ausgewählt wurde. Ob eine Adresse überhaupt ausgewählt wurde, kann über die
+            Variable <code class="literal">billing_address_id</code> getestet werden, die die Datenbank-ID der abweichenden Rechnungsadresse enthält,
+            wenn eine ausgewählt ist.
+          </p><p>
+            Die Variablennamen starten alle mit dem Präfix <code class="literal">billing_address_</code> und heißen anschließend so, wie ihre Pendants
+            aus der Standard-Rechnungsadresse des Kunden. Beispiel: die Postleitzahl, die in der normalen Rechnungsadresse in
+            <code class="literal">zipcode</code> steht, steht für die abweichende Rechnungsadresse in <code class="literal">billing_address_zipcode</code>.
+          </p><p>
+            Die folgenden Variablen stehen so zur Verfügung: <code class="literal">billing_address_name</code>,
+            <code class="literal">billing_address_department_1</code>, <code class="literal">billing_address_department_2</code>,
+            <code class="literal">billing_address_contact</code>, <code class="literal">billing_address_street</code>,
+            <code class="literal">billing_address_zipcode</code>, <code class="literal">billing_address_city</code>, <code class="literal">billing_address_country</code>,
+            <code class="literal">billing_address_gln</code>, <code class="literal">billing_address_email</code>, <code class="literal">billing_address_phone</code> und
+            <code class="literal">billing_address_fax</code>.
+          </p></div></div><div class="sect2" title="3.3.8. Variablen in Rechnungen"><div class="titlepage"><div><div><h3 class="title"><a name="dokumentenvorlagen-und-variablen.invoice"></a>3.3.8. Variablen in Rechnungen</h3></div></div></div><div class="sect3" title="3.3.8.1. Allgemeine Variablen"><div class="titlepage"><div><div><h4 class="title"><a name="dokumentenvorlagen-und-variablen.invoice-allgemein"></a>3.3.8.1. Allgemeine Variablen</h4></div></div></div><div class="variablelist"><dl><dt><span class="term">
                         <code class="varname">creditremaining</code>
                      </span></dt><dd><p>Verbleibender Kredit</p></dd><dt><span class="term">
                         <code class="varname">currency</code>
                      </span></dt><dd><p>Angebotsdatum</p></dd><dt><span class="term">
                         <code class="varname">quonumber</code>
                      </span></dt><dd><p>Angebotsnummer</p></dd><dt><span class="term">
+                        <code class="varname">rounding</code>
+                     </span></dt><dd><p>Betrag, um den <code class="varname">invtotal</code> gerundet
+                wurde (kann positiv oder negativ sein)</p></dd><dt><span class="term">
                         <code class="varname">shippingpoint</code>
                      </span></dt><dd><p>Versandort</p></dd><dt><span class="term">
                         <code class="varname">shipvia</code>
                         <code class="varname">description</code>
                      </span></dt><dd><p>Artikelbeschreibung</p></dd><dt><span class="term">
                         <code class="varname">cusordnumber_oe</code>
-                     </span></dt><dd><p>Bestellnummer des Kunden aus dem Auftrag, aus dem der Posten ursprünglich stammt (nur Verkauf)</p></dd><dt><span class="term">
+                     </span></dt><dd><p>Bestellnummer des Kunden aus dem Auftrag, aus dem der
+                Posten ursprünglich stammt (nur Verkauf)</p></dd><dt><span class="term">
                         <code class="varname">discount</code>
                      </span></dt><dd><p>Rabatt als Betrag</p></dd><dt><span class="term">
                         <code class="varname">discount_sub</code>
                      </span></dt><dd><p>Zwischensumme mit Rabatt</p></dd><dt><span class="term">
                         <code class="varname">donumber_do</code>
-                     </span></dt><dd><p>Lieferscheinnummer des Lieferscheins, aus dem die Position ursprünglich stammt, wenn die Rechnung im Rahmen des Workflows aus einem  Lieferschein erstellt wurde.</p></dd><dt><span class="term">
+                     </span></dt><dd><p>Lieferscheinnummer des Lieferscheins, aus dem die
+                Position ursprünglich stammt, wenn die Rechnung im Rahmen des
+                Workflows aus einem Lieferschein erstellt wurde.</p></dd><dt><span class="term">
                         <code class="varname">drawing</code>
                      </span></dt><dd><p>Zeichnung</p></dd><dt><span class="term">
                         <code class="varname">ean</code>
                         <code class="varname">linetotal</code>
                      </span></dt><dd><p>Zeilensumme (Anzahl * Einzelpreis)</p></dd><dt><span class="term">
                         <code class="varname">longdescription</code>
-                     </span></dt><dd><p>Langtext</p></dd><dt><span class="term">
+                     </span></dt><dd><p>Langtext, vorbelegt mit dem Feld Bemerkungen der entsprechenden Ware</p></dd><dt><span class="term">
                         <code class="varname">microfiche</code>
                      </span></dt><dd><p>Mikrofilm</p></dd><dt><span class="term">
                         <code class="varname">netprice</code>
-                     </span></dt><dd><p>Alternative zu <code class="varname">sellprice</code>, aber <code class="varname">netprice</code> entspricht dem effektiven Einzelpreis und beinhaltet Zeilenrabatt und Preisfaktor. <code class="varname">netprice</code> wird rückgerechnet aus Zeilensumme / Menge. Diese Variable ist nützlich, wenn man den gewährten Rabatt in der Druckvorlage nicht anzeigen möchte, aber Menge * Einzelpreis trotzdem die angezeigte Zeilensumme ergeben soll. <code class="varname">netprice</code> hat nichts mit Netto/Brutto im Sinne von Steuern zu tun.</p></dd><dt><span class="term">
+                     </span></dt><dd><p>Alternative zu <code class="varname">sellprice</code>, aber
+                <code class="varname">netprice</code> entspricht dem effektiven
+                Einzelpreis und beinhaltet Zeilenrabatt und Preisfaktor.
+                <code class="varname">netprice</code> wird rückgerechnet aus Zeilensumme
+                / Menge. Diese Variable ist nützlich, wenn man den gewährten
+                Rabatt in der Druckvorlage nicht anzeigen möchte, aber Menge *
+                Einzelpreis trotzdem die angezeigte Zeilensumme ergeben soll.
+                <code class="varname">netprice</code> hat nichts mit Netto/Brutto im
+                Sinne von Steuern zu tun.</p></dd><dt><span class="term">
                         <code class="varname">nodiscount_linetotal</code>
                      </span></dt><dd><p>Zeilensumme ohne Rabatt</p></dd><dt><span class="term">
                         <code class="varname">nodiscount_sub</code>
                         <code class="varname">number</code>
                      </span></dt><dd><p>Artikelnummer</p></dd><dt><span class="term">
                         <code class="varname">ordnumber_oe</code>
-                     </span></dt><dd><p>Auftragsnummer des Originalauftrags, aus dem der Posten ursprünglich stammt. Nützlich, wenn die Rechnung aus mehreren Lieferscheinen zusammengefasst wurde, oder wenn zwischendurch eine Sammelauftrag aus mehreren Aufträgen erstellt wurde. In letzterem Fall wird die unsprüngliche Auftragsnummer angezeigt.</p></dd><dt><span class="term">
+                     </span></dt><dd><p>Auftragsnummer des Originalauftrags, aus dem der Posten
+                ursprünglich stammt. Nützlich, wenn die Rechnung aus mehreren
+                Lieferscheinen zusammengefasst wurde, oder wenn zwischendurch
+                eine Sammelauftrag aus mehreren Aufträgen erstellt wurde. In
+                letzterem Fall wird die unsprüngliche Auftragsnummer
+                angezeigt.</p></dd><dt><span class="term">
                         <code class="varname">p_discount</code>
                      </span></dt><dd><p>Rabatt in Prozent</p></dd><dt><span class="term">
                         <code class="varname">partnotes</code>
                         <code class="varname">tax_rate</code>
                      </span></dt><dd><p>Steuersatz</p></dd><dt><span class="term">
                         <code class="varname">transdate_do</code>
-                     </span></dt><dd><p>Datum des Lieferscheins, wenn die Rechnung im Rahmen des Workflows aus einem Lieferschein stammte.</p></dd><dt><span class="term">
+                     </span></dt><dd><p>Datum des Lieferscheins, wenn die Rechnung im Rahmen des
+                Workflows aus einem Lieferschein stammte.</p></dd><dt><span class="term">
                         <code class="varname">transdate_oe</code>
-                     </span></dt><dd><p>Datum des Auftrags, wenn die Rechnung im Rahmen des Workflows aus einem Auftrag erstellt wurde. Wenn es Sammelaufträge gab wird das Datum des ursprünglichen Auftrags genommen.</p></dd><dt><span class="term">
+                     </span></dt><dd><p>Datum des Auftrags, wenn die Rechnung im Rahmen des
+                Workflows aus einem Auftrag erstellt wurde. Wenn es
+                Sammelaufträge gab wird das Datum des ursprünglichen Auftrags
+                genommen.</p></dd><dt><span class="term">
                         <code class="varname">transdate_quo</code>
-                     </span></dt><dd><p>Datum des Angebots, wenn die Position im Rahmen des Workflows aus einem Angebot stammte.</p></dd><dt><span class="term">
+                     </span></dt><dd><p>Datum des Angebots, wenn die Position im Rahmen des
+                Workflows aus einem Angebot stammte.</p></dd><dt><span class="term">
                         <code class="varname">unit</code>
                      </span></dt><dd><p>Einheit</p></dd><dt><span class="term">
                         <code class="varname">weight</code>
           <code class="varname">number_of_employees</code> definiert, die die Anzahl der
           Mitarbeiter des Unternehmens enthält. Diese Variable steht dann
           unter dem Namen <code class="varname">vc_cvar_number_of_employees</code> zur
-          Verfügung.</p></div></div><div class="sect2" title="3.3.9. Variablen in Mahnungen und Rechnungen über Mahngebühren"><div class="titlepage"><div><div><h3 class="title"><a name="dokumentenvorlagen-und-variablen.dunning"></a>3.3.9. Variablen in Mahnungen und Rechnungen über Mahngebühren</h3></div></div></div><div class="sect3" title="3.3.9.1. Namen der Vorlagen"><div class="titlepage"><div><div><h4 class="title"><a name="dokumentenvorlagen-und-variablen.dunning-vorlagennamen"></a>3.3.9.1. Namen der Vorlagen</h4></div></div></div><p>Die Namen der Vorlagen werden im System-Menü vom Benutzer
+          Verfügung.</p><p>Die benutzerdefinierten Variablen der Lieferadressen stehen
+          unter einem ähnlichen Namensschema zur Verfügung. Hier lautet der
+          Präfix <code class="varname">shiptocvar_</code>.</p><p>Analog stehen die benutzerdefinierten Variablen für
+          Ansprechpersonen mit dem Namenspräfix <code class="varname">cp_cvar_</code>
+          zur Verfügung.</p></div></div><div class="sect2" title="3.3.9. Variablen in Mahnungen und Rechnungen über Mahngebühren"><div class="titlepage"><div><div><h3 class="title"><a name="dokumentenvorlagen-und-variablen.dunning"></a>3.3.9. Variablen in Mahnungen und Rechnungen über Mahngebühren</h3></div></div></div><div class="sect3" title="3.3.9.1. Namen der Vorlagen"><div class="titlepage"><div><div><h4 class="title"><a name="dokumentenvorlagen-und-variablen.dunning-vorlagennamen"></a>3.3.9.1. Namen der Vorlagen</h4></div></div></div><p>Die Namen der Vorlagen werden im System-Menü vom Benutzer
           eingegeben. Wird für ein Mahnlevel die Option zur automatischen
           Erstellung einer Rechnung über die Mahngebühren und Zinsen
           aktiviert, so wird der Name der Vorlage für diese Rechnung aus dem
           Vorlagenname für diese Mahnstufe mit dem Zusatz
           <code class="constant">_invoice</code> gebildet. Weiterhin werden die Kürzel
           für die ausgewählte Sprache und den ausgewählten Drucker
-          angehängt.</p></div><div class="sect3" title="3.3.9.2. Allgemeine Variablen in Mahnungen"><div class="titlepage"><div><div><h4 class="title"><a name="dokumentenvorlagen-und-variablen.dunning-allgemein"></a>3.3.9.2. Allgemeine Variablen in Mahnungen</h4></div></div></div><p>Die Variablen des Verkäufers stehen wie gewohnt als
-          <code class="varname">employee_...</code> zur Verfügung. Die Adressdaten des
-          Kunden stehen als Variablen <code class="varname">name</code>,
-          <code class="varname">street</code>, <code class="varname">zipcode</code>,
-          <code class="varname">city</code>, <code class="varname">country</code>,
-          <code class="varname">department_1</code>, <code class="varname">department_2</code>,
-          und <code class="varname">email</code> zur Verfügung.</p><p>Weitere Variablen beinhalten:</p><div class="variablelist"><dl><dt><span class="term">
+          angehängt.</p></div><div class="sect3" title="3.3.9.2. Allgemeine Variablen in Mahnungen"><div class="titlepage"><div><div><h4 class="title"><a name="dokumentenvorlagen-und-variablen.dunning-allgemein"></a>3.3.9.2. Allgemeine Variablen in Mahnungen</h4></div></div></div><p>Die Variablen des Bearbeiters, bzw. Verkäufers stehen wie
+          gewohnt als <code class="varname">employee_...</code> bzw.
+          <code class="varname">salesman_...</code> zur Verfügung. Werden mehrere
+          Rechnungen in einer Mahnung zusammengefasst, so werden die Metadaten
+          (Bearbeiter, Abteilung, etc) der ersten angemahnten Rechnung im
+          Ausdruck genommen.</p><p>Die Adressdaten des Kunden stehen als Variablen
+          <code class="varname">name</code>, <code class="varname">street</code>,
+          <code class="varname">zipcode</code>, <code class="varname">city</code>,
+          <code class="varname">country</code>, <code class="varname">department_1</code>,
+          <code class="varname">department_2</code>, und <code class="varname">email</code> zur
+          Verfügung. Der Ansprechpartner <code class="varname">cp_...</code> steht auch
+          zu Verfügung, wird allerdings auch nur von der ersten angemahnten
+          Rechnung (s.o.) genommen.</p><p>Weitere Variablen beinhalten:</p><div class="variablelist"><dl><dt><span class="term">
                         <code class="varname">dunning_date</code>
                      </span></dt><dd><p>Datum der Mahnung</p></dd><dt><span class="term">
                         <code class="varname">dunning_duedate</code>
                         <code class="varname">dunning_id</code>
                      </span></dt><dd><p>Mahnungsnummer</p></dd><dt><span class="term">
                         <code class="varname">fee</code>
-                     </span></dt><dd><p>Kummulative Mahngebühren</p></dd><dt><span class="term">
+                     </span></dt><dd><p>Kumulative Mahngebühren</p></dd><dt><span class="term">
                         <code class="varname">interest_rate</code>
                      </span></dt><dd><p>Zinssatz per anno in Prozent</p></dd><dt><span class="term">
                         <code class="varname">total_amount</code>
                         <code class="varname">invdate</code>
                      </span></dt><dd><p>Rechnungsdatum</p></dd><dt><span class="term">
                         <code class="varname">invnumber</code>
-                     </span></dt><dd><p>Rechnungsnummer</p></dd></dl></div></div></div><div class="sect2" title="3.3.10. Variablen in anderen Vorlagen"><div class="titlepage"><div><div><h3 class="title"><a name="dokumentenvorlagen-und-variablen.andere-vorlagen"></a>3.3.10. Variablen in anderen Vorlagen</h3></div></div></div><div class="sect3" title="3.3.10.1. Einführung"><div class="titlepage"><div><div><h4 class="title"><a name="d0e5104"></a>3.3.10.1. Einführung</h4></div></div></div><p>Die Variablen in anderen Vorlagen sind ähnlich wie in der
+                     </span></dt><dd><p>Rechnungsnummer</p></dd></dl></div></div></div><div class="sect2" title="3.3.10. Variablen in anderen Vorlagen"><div class="titlepage"><div><div><h3 class="title"><a name="dokumentenvorlagen-und-variablen.andere-vorlagen"></a>3.3.10. Variablen in anderen Vorlagen</h3></div></div></div><div class="sect3" title="3.3.10.1. Einführung"><div class="titlepage"><div><div><h4 class="title"><a name="d0e6135"></a>3.3.10.1. Einführung</h4></div></div></div><p>Die Variablen in anderen Vorlagen sind ähnlich wie in der
           Rechnung. Allerdings heißen die Variablen, die mit
           <code class="varname">inv</code> beginnen, jetzt anders. Bei den Angeboten
           fangen sie mit <code class="varname">quo</code> für "quotation" an:
 ...
 &lt;%end%&gt;</pre><p>Eine normale "if-then"-Bedingung. Die Zeilen zwischen dem "if"
           und dem "end" werden nur ausgegeben, wenn die Variable
-          <code class="varname">variablenname</code> gesetzt und ungleich 0 ist.</p><p>Handelt es sich bei der benannten Variable um ein Array, also um einen Variablennamen, über den man mit
-          <span class="command"><strong>&lt;%foreach variablenname%&gt;</strong></span> iteriert, so wird mit diesem Konstrukt darauf getestet, ob das Array Elemente
-          enthält. Somit würde im folgenden Beispiel nur dann eine Liste von Zahlungseingängen samt ihrer Überschrift "Zahlungseingänge"
-          ausgegeben, wenn tatsächlich welche getätigt wurden:</p><pre class="programlisting">&lt;%if payment%&gt;
+          <code class="varname">variablenname</code> gesetzt und ungleich 0 ist.</p><p>Handelt es sich bei der benannten Variable um ein Array, also
+          um einen Variablennamen, über den man mit <span class="command"><strong>&lt;%foreach
+          variablenname%&gt;</strong></span> iteriert, so wird mit diesem Konstrukt
+          darauf getestet, ob das Array Elemente enthält. Somit würde im
+          folgenden Beispiel nur dann eine Liste von Zahlungseingängen samt
+          ihrer Überschrift "Zahlungseingänge" ausgegeben, wenn tatsächlich
+          welche getätigt wurden:</p><pre class="programlisting">&lt;%if payment%&gt;
 Zahlungseingänge:
  &lt;%foreach payment%&gt;
    Am &lt;%paymentdate%&gt;: &lt;%payment%&gt; €
@@ -740,4 +802,12 @@ Beschreibung: &lt;%description%&gt;
         (HTML oder PDF über LaTeX) umgesetzt.</p><p>Die unterstützen Formatierungen sind:</p><div class="variablelist"><dl><dt><span class="term">&lt;b&gt;Text&lt;/b&gt;</span></dt><dd><p>Text wird in Fettdruck gesetzt.</p></dd><dt><span class="term">&lt;i&gt;Text&lt;/i&gt;</span></dt><dd><p>Text wird kursiv gesetzt.</p></dd><dt><span class="term">&lt;u&gt;Text&lt;/u&gt;</span></dt><dd><p>Text wird unterstrichen.</p></dd><dt><span class="term">&lt;s&gt;Text&lt;/s&gt;</span></dt><dd><p>Text wird durchgestrichen. Diese Formatierung ist nicht
               bei der Ausgabe als PDF über LaTeX verfügbar.</p></dd><dt><span class="term">&lt;bullet&gt;</span></dt><dd><p>Erzeugt einen ausgefüllten Kreis für Aufzählungen (siehe
               unten).</p></dd></dl></div><p>Der Befehl <span class="command"><strong>&lt;bullet&gt;</strong></span> funktioniert
-        momentan auch nur in Latex-Vorlagen.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch03s02.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch03.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch03s04.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">3.2. Bankerweiterung&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;3.4. Excel-Vorlagen</td></tr></table></div></body></html>
\ No newline at end of file
+        momentan auch nur in Latex-Vorlagen.</p></div><div class="sect2" title="3.3.13. Hinweise zur Anrede"><div class="titlepage"><div><div><h3 class="title"><a name="dokumentenvorlagen-und-variablen.anrede"></a>3.3.13. Hinweise zur Anrede</h3></div></div></div><p>Das Flag "natürliche Person"
+        (<code class="varname">natural_person</code>) aus den Kunden- oder
+        Lieferantenstammdaten kann in den Druckvorlagen zusammen mit
+        dem Feld "Anrede" (<code class="varname">greeting</code>) z.B. dafür
+        verwendet werden, die Anrede zwischen einer allgemeinen und
+        einer persönlichen Anrede zu unterscheiden.
+        </p><pre class="programlisting">&lt;%if natural_person%&gt;&lt;%greeting%&gt; &lt;%name%&gt;&lt;%else%&gt;Sehr geehrte Damen und Herren&lt;%end if%&gt;</pre><p>
+        
+            </p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch03s02.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch03.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch03s04.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">3.2. Bankerweiterung&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;3.4. Excel-Vorlagen</td></tr></table></div></body></html>
\ No newline at end of file
index bc85bbb..19a2b73 100644 (file)
@@ -1,6 +1,6 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>3.4. Excel-Vorlagen</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch03.html" title="Kapitel 3. Features und Funktionen"><link rel="prev" href="ch03s03.html" title="3.3. Dokumentenvorlagen und verfügbare Variablen"><link rel="next" href="ch03s05.html" title="3.5. Mandantenkonfiguration Lager"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">3.4. Excel-Vorlagen</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch03s03.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 3. Features und Funktionen</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch03s05.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="3.4. Excel-Vorlagen"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="excel-templates"></a>3.4. Excel-Vorlagen</h2></div></div></div><div class="sect2" title="3.4.1. Zusammenfassung"><div class="titlepage"><div><div><h3 class="title"><a name="excel-templates.summary"></a>3.4.1. Zusammenfassung</h3></div></div></div><p>Dieses Dokument beschreibt den Mechanismus, mit dem
+   <title>3.4. Excel-Vorlagen</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch03.html" title="Kapitel 3. Features und Funktionen"><link rel="prev" href="ch03s03.html" title="3.3. Dokumentenvorlagen und verfügbare Variablen"><link rel="next" href="ch03s05.html" title="3.5. Mandantenkonfiguration Lager"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">3.4. Excel-Vorlagen</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch03s03.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 3. Features und Funktionen</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch03s05.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="3.4. Excel-Vorlagen"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="excel-templates"></a>3.4. Excel-Vorlagen</h2></div></div></div><div class="sect2" title="3.4.1. Zusammenfassung"><div class="titlepage"><div><div><h3 class="title"><a name="excel-templates.summary"></a>3.4.1. Zusammenfassung</h3></div></div></div><p>Dieses Dokument beschreibt den Mechanismus, mit dem
         Exceltemplates abgearbeitet werden, und die Einschränkungen, die damit
         einhergehen.</p></div><div class="sect2" title="3.4.2. Bedienung"><div class="titlepage"><div><div><h3 class="title"><a name="excel-templates.usage"></a>3.4.2. Bedienung</h3></div></div></div><p>Der Excel Mechanismus muss in der Konfigurationsdatei aktiviert
         werden. Die Konfigurationsoption heißt <code class="varname">excel_templates =
index a92b025..a623a98 100644 (file)
@@ -1,27 +1,31 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>3.5. Mandantenkonfiguration Lager</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch03.html" title="Kapitel 3. Features und Funktionen"><link rel="prev" href="ch03s04.html" title="3.4. Excel-Vorlagen"><link rel="next" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">3.5. Mandantenkonfiguration Lager</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch03s04.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 3. Features und Funktionen</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch04.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="3.5. Mandantenkonfiguration Lager"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="features.warehouse"></a>3.5. Mandantenkonfiguration Lager</h2></div></div></div>
-        Die Lagerverwaltung in kivitendo funktioniert standardmässig wie folgt:
-        Wird ein Lager mit einem Lagerplatz angelegt, so gibt es die Möglichkeit hier über den
-        Menüpunkt Lager entsprechende Warenbewegungen durchzuführen. Ferner kann
-        jede Position eines Lieferscheins ein-, bzw. ausgelagert werden (Einkauf-, bzw. Verkauf).
-        Es können beliebig viele Lager mit beliebig vielen Lagerplätzen abgebildet werden.
-        Die Lagerbewegungen über einen Lieferschein erfolgt durch Anklicken jeder Einzelposition und
-        das Auswählen dieser Position zu einem Lager mit Lagerplatz.
-        Dieses Verfahren lässt sich schrittweise vereinfachen, je nachdem wie die Einstellungen in
-        der Mandatenkonfiguration gesetzt werden.
-       <div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
-                  <code class="option">Auslagern über Standardlagerplatz</code> Hier wird ein zusätzlicher Knopf (Auslagern über Standard-Lagerplatz)
-            in dem Lieferschein-Beleg hinzugefügt, der dann alle Lagerbewegungen über den Standardlagerplatz (konfigurierbar pro Ware) durchführt.
-            </p></li><li class="listitem"><p>
-                  <code class="option">Auslagern ohne Bestandsprüfung</code>Das obige Auslagern schlägt fehl, wenn die entsprechende Menge für
-            die Lagerbewegung nicht vorhanden ist, möchte man dies auch ignorieren und ggf. dann nachpflegen, so kann man eine Negativ-Warenmenge mit dieser Option
-            erlauben. Hierfür muss ein entsprechender Lagerplatz (Fehlbestand, o.ä.) konfiguriert sein.</p></li></ul></div>
-        Zusätzliche Funktionshinweise:
-         <div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
-                  <code class="option">Standard-Lagerplatz</code>Ist dieser konfiguriert, wird dies auch als Standard-Voreinstellung bei der Neuerfassung von
-          Stammdaten-&gt; Waren / Dienstleistung / Erzeugnis verwendet.
-          </p></li><li class="listitem"><p>
-                  <code class="option">Standard-Lagerplatz verwenden, falls keiner in Stammdaten definiert</code>Wird beim 'Auslagern über Standardlagerplatz'
-          keine Standardlagerplatz zu der Ware gefunden, so wird mit dieser Option einfach der Standardlagerplatz verwendet.
-          </p></li></ul></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch03s04.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch03.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch04.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">3.4. Excel-Vorlagen&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;Kapitel 4. Entwicklerdokumentation</td></tr></table></div></body></html>
\ No newline at end of file
+   <title>3.5. Mandantenkonfiguration Lager</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch03.html" title="Kapitel 3. Features und Funktionen"><link rel="prev" href="ch03s04.html" title="3.4. Excel-Vorlagen"><link rel="next" href="ch03s06.html" title="3.6. Schweizer Kontenpläne"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">3.5. Mandantenkonfiguration Lager</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch03s04.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 3. Features und Funktionen</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch03s06.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="3.5. Mandantenkonfiguration Lager"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="features.warehouse"></a>3.5. Mandantenkonfiguration Lager</h2></div></div></div><p>Die Lagerverwaltung in kivitendo funktioniert standardmässig wie
+      folgt: Wird ein Lager mit einem Lagerplatz angelegt, so gibt es die
+      Möglichkeit hier über den Menüpunkt Lager entsprechende Warenbewegungen
+      durchzuführen. Ferner kann jede Position eines Lieferscheins ein-, bzw.
+      ausgelagert werden (Einkauf-, bzw. Verkauf). Es können beliebig viele
+      Lager mit beliebig vielen Lagerplätzen abgebildet werden. Die
+      Lagerbewegungen über einen Lieferschein erfolgt durch Anklicken jeder
+      Einzelposition und das Auswählen dieser Position zu einem Lager mit
+      Lagerplatz. Dieses Verfahren lässt sich schrittweise vereinfachen, je
+      nachdem wie die Einstellungen in der Mandatenkonfiguration gesetzt
+      werden.</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
+                  <code class="option">Auslagern über Standardlagerplatz</code> Hier wird
+          ein zusätzlicher Knopf (Auslagern über Standard-Lagerplatz) in dem
+          Lieferschein-Beleg hinzugefügt, der dann alle Lagerbewegungen über
+          den Standardlagerplatz (konfigurierbar pro Ware) durchführt.</p></li><li class="listitem"><p>
+                  <code class="option">Auslagern ohne Bestandsprüfung</code> Das obige
+          Auslagern schlägt fehl, wenn die entsprechende Menge für die
+          Lagerbewegung nicht vorhanden ist, möchte man dies auch ignorieren
+          und ggf. dann nachpflegen, so kann man eine Negativ-Warenmenge mit
+          dieser Option erlauben. Hierfür muss ein entsprechender Lagerplatz
+          (Fehlbestand, o.ä.) konfiguriert sein.</p></li></ul></div><p>Zusätzliche Funktionshinweise:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
+                  <code class="option">Standard-Lagerplatz</code> Ist dieser konfiguriert,
+          wird dies auch als Standard-Voreinstellung bei der Neuerfassung von
+          Stammdaten → Waren / Dienstleistung / Erzeugnis verwendet.</p></li><li class="listitem"><p>
+                  <code class="option">Standard-Lagerplatz verwenden, falls keiner in
+          Stammdaten definiert</code> Wird beim 'Auslagern über
+          Standardlagerplatz' keine Standardlagerplatz zu der Ware gefunden,
+          so wird mit dieser Option einfach der Standardlagerplatz
+          verwendet.</p></li></ul></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch03s04.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch03.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch03s06.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">3.4. Excel-Vorlagen&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;3.6. Schweizer Kontenpläne</td></tr></table></div></body></html>
\ No newline at end of file
diff --git a/doc/html/ch03s06.html b/doc/html/ch03s06.html
new file mode 100644 (file)
index 0000000..5444a32
--- /dev/null
@@ -0,0 +1,24 @@
+<html><head>
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+   <title>3.6. Schweizer Kontenpläne</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch03.html" title="Kapitel 3. Features und Funktionen"><link rel="prev" href="ch03s05.html" title="3.5. Mandantenkonfiguration Lager"><link rel="next" href="ch03s07.html" title="3.7. Artikelklassifizierung"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">3.6. Schweizer Kontenpläne</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch03s05.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 3. Features und Funktionen</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch03s07.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="3.6. Schweizer Kontenpläne"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="features.swiss-charts-of-accounts"></a>3.6. Schweizer Kontenpläne</h2></div></div></div><p>Seit der Version 3.5 stehen in kivitendo 3 Kontenpläne für den
+      Einsatz in der Schweiz zur Verfügung, einer für Firmen und
+      Organisationen, die nicht mehrwertsteuerpflichtig sind, einer für
+      Firmen, die mehrwertsteuerpflichtig sind und einer speziell für
+      Vereine.</p><p>Die Kontenpläne orientieren sich am in der Schweiz üblicherweise
+      verwendeten KMU-Kontenrahmen und sind mit der Revision des
+      Schweizerischen Obligationenrechts (OR) vom 1.1.2013 kompatibel,
+      insbesondere <code class="literal">Art.957a Abs.2</code>.</p><p>Beim Vereinskontenplan sind standardmässig nur die Konten 1100
+      (Debitoren CHF) und 1101 (Debitoren EUR) als Buchungskonten im Verkauf
+      sowie die Konten 2000 (Kreditoren CHF) und 2001 (Kreditoren EUR) als
+      Buchungskonten im Einkauf vorgesehen. Weitere Konten können bei Bedarf
+      in den Konto-Detaileinstellungen als Einkaufs- oder Verkaufskonten
+      konfiguriert werden.</p><p>Die Möglichkeit, Saldosteuersätze zu verwenden ist in der
+      aktuellen Version von kivitendo noch nicht integriert.</p><p>Trotzdem können auch Firmen, die per Saldosteuersatz mit der
+      Eidgenössischen Steuerverwaltung abrechnen, kivitendo bereits nutzen.
+      Dazu wird der Kontenplan mit MWST ausgewählt. Anschliessend müssen alle
+      Aufwandskonten editiert werden und dort der Steuersatz auf 0% gesetzt
+      werden.</p><p>So werden bei Kreditorenbuchungen keine Vorsteuern
+      verbucht.</p><p>Bezugssteuern für aus dem Ausland bezogene Dienstleistungen müssen
+      manuell verbucht werden.</p><p>Wünsche für Anpassungen an den Schweizer Kontenplänen sowie
+      Vorschläge für weitere (z.B. branchenspezifische) Kontenpläne bitte an
+      <code class="literal">empfang@revamp-it.ch</code> senden.</p></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch03s05.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch03.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch03s07.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">3.5. Mandantenkonfiguration Lager&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;3.7. Artikelklassifizierung</td></tr></table></div></body></html>
\ No newline at end of file
diff --git a/doc/html/ch03s07.html b/doc/html/ch03s07.html
new file mode 100644 (file)
index 0000000..32ef9e4
--- /dev/null
@@ -0,0 +1,27 @@
+<html><head>
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+   <title>3.7. Artikelklassifizierung</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch03.html" title="Kapitel 3. Features und Funktionen"><link rel="prev" href="ch03s06.html" title="3.6. Schweizer Kontenpläne"><link rel="next" href="ch03s08.html" title="3.8. Dateiverwaltung (Mini-DMS)"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">3.7. Artikelklassifizierung</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch03s06.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 3. Features und Funktionen</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch03s08.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="3.7. Artikelklassifizierung"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="features.part_classification"></a>3.7. Artikelklassifizierung</h2></div></div></div><div class="sect2" title="3.7.1. Übersicht"><div class="titlepage"><div><div><h3 class="title"><a name="d0e6817"></a>3.7.1. Übersicht</h3></div></div></div><p>Die Klassifizierung von Artikeln dient einer weiteren
+        Gliederung, um zum Beispiel den Einkauf vom Verkauf zu trennen,
+        gekennzeichnet durch eine Beschreibung (z.B. "Einkauf") und ein Kürzel
+        (z.B. "E"). Für jede Klassifizierung besteht eine Beschreibung und
+        eine Abkürzung die normalerweise aus einem Zeichen besteht, kann aber
+        auf mehrere Zeichen erweitert werden, falls zur Unterscheidung
+        notwendig. Sinnvoll sind jedoch nur maximal 2 Zeichen.</p></div><div class="sect2" title="3.7.2. Basisklassifizierung"><div class="titlepage"><div><div><h3 class="title"><a name="d0e6822"></a>3.7.2. Basisklassifizierung</h3></div></div></div><p>Als Basisklassifizierungen gibt es</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Einkauf</p></li><li class="listitem"><p>Verkauf</p></li><li class="listitem"><p>Handelsware</p></li><li class="listitem"><p>Produktion</p></li><li class="listitem"><p>- keine - (diese wird bei einer Aktualisierung für alle
+            existierenden Artikel verwendet und ist gültig für Verkauf und
+            Einkauf)</p></li></ul></div><p>Es können weitere Klassifizierungen angelegt werden. So kann es
+        z.B. für separat auszuweisende Artikel folgende Klassen geben:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Lieferung (Logistik, Transport) mit Kürzel L</p></li><li class="listitem"><p>Material (Verpackungsmaterial) mit Kürzel M</p></li></ul></div></div><div class="sect2" title="3.7.3. Attribute"><div class="titlepage"><div><div><h3 class="title"><a name="d0e6852"></a>3.7.3. Attribute</h3></div></div></div><p>Bisher haben die Klassifizierungen folgende Attribute, die auch
+        alle gleichzeitg gültig sein können</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>gültig für Verkauf - dieser Artikel kann im Verkauf genutzt
+            werden</p></li><li class="listitem"><p>gültig für Einkauf - dieser Artikel kann im Einkauf genutzt
+            werden</p></li><li class="listitem"><p>separat ausweisen - hierzu gibt es zur Dokumentengenerierung
+            (LaTeX) eine zusätzliche Variable</p></li></ul></div><p>Für das Attribut "separat ausweisen" stehen in den
+        LaTeX-Vorlagen die Variable <span class="bold"><strong>&lt;%non_separate_subtotal%&gt; </strong></span>zur Verfügung,
+        die alle nicht separat auszuweisenden Artikelkosten saldiert, sowie
+        pro separat auszuweisenden Klassifizierungen die Variable<span class="bold"><strong>&lt; %separate_X_subtotal%&gt;</strong></span>, wobei X das
+        Kürzel der Klassifizierung ist.</p><p>Im obigen Beispiel wäre das für Lieferkosten <span class="bold"><strong>&lt;%separate_L_subtotal%&gt;</strong></span> und für
+        Verpackungsmaterial <span class="bold"><strong>
+        &lt;%separate_M_subtotal%&gt;</strong></span>.</p></div><div class="sect2" title="3.7.4. Zwei-Zeichen Abkürzung"><div class="titlepage"><div><div><h3 class="title"><a name="d0e6883"></a>3.7.4. Zwei-Zeichen Abkürzung</h3></div></div></div><p>Der Typ des Artikels und die Klassifizierung werden durch zwei
+        Buchstaben dargestellt. Der erste Buchstabe ist eine Lokalisierung des
+        Artikel-Typs ('P','A','S'), deutsch 'W', 'E', und 'D' für Ware
+        Erzeugnis oder Dienstleistung und ggf. weiterer Typen.</p><p>Der zweite Buchstabe (und ggf. auch ein dritter, falls nötig)
+        entspricht der lokalisierten Abkürzung der Klassifizierung.</p><p>Diese Abkürzung wird überall beim Auflisten von Artikeln zur
+        Erleichterung mit dargestellt.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch03s06.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch03.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch03s08.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">3.6. Schweizer Kontenpläne&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;3.8. Dateiverwaltung (Mini-DMS)</td></tr></table></div></body></html>
\ No newline at end of file
diff --git a/doc/html/ch03s08.html b/doc/html/ch03s08.html
new file mode 100644 (file)
index 0000000..17a969e
--- /dev/null
@@ -0,0 +1,76 @@
+<html><head>
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+   <title>3.8. Dateiverwaltung (Mini-DMS)</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch03.html" title="Kapitel 3. Features und Funktionen"><link rel="prev" href="ch03s07.html" title="3.7. Artikelklassifizierung"><link rel="next" href="ch03s09.html" title="3.9. Webshop-Api"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">3.8. Dateiverwaltung (Mini-DMS)</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch03s07.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 3. Features und Funktionen</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch03s09.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="3.8. Dateiverwaltung (Mini-DMS)"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="features.file_managment"></a>3.8. Dateiverwaltung (Mini-DMS)</h2></div></div></div><div class="sect2" title="3.8.1. Übersicht"><div class="titlepage"><div><div><h3 class="title"><a name="d0e6895"></a>3.8.1. Übersicht</h3></div></div></div><p>Parallel zum alten WebDAV gibt es ein Datei-Management-System,
+        das Dateien verschiedenen Typs verwaltet. Dies können</p><div class="orderedlist"><ol class="orderedlist" type="1"><li class="listitem"><p>aus ERP-Daten per LaTeX Template erzeugte
+            PDF-Dokumente,</p></li><li class="listitem"><p>zu bestimmten ERP-Daten gehörende Anhangdateien
+            unterschiedlichen Formats,</p></li><li class="listitem"><p>per Scanner eingelesene PDF-Dateien,</p></li><li class="listitem"><p>per E-Mail empfangene Dateianhänge unterschiedlichen
+            Formats,</p></li><li class="listitem"><p>sowie speziel für Artikel hochgeladene Bilder sein.</p></li></ol></div><div class="screenshot"><div class="mediaobject"><img src="images/DMS-Overview.png"></div></div></div><div class="sect2" title="3.8.2. Struktur"><div class="titlepage"><div><div><h3 class="title"><a name="d0e6922"></a>3.8.2. Struktur</h3></div></div></div><p>Über eine vom Speichermedium unabhängige Zwischenschicht werden
+        die Dateien und ihre Versionen in der Datenbank verwaltet. Darunter
+        können verschiedene Implementierungen (Backends) gleichzeitig
+        existieren:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Dateisystem</p></li><li class="listitem"><p>WebDAV</p></li><li class="listitem"><p>Schnittstelle zu externen
+            Dokumenten-Management-Systemen</p></li><li class="listitem"><p>andere Datenbank</p></li><li class="listitem"><p>etc ...</p></li></ul></div><p>Es gibt unterschiedliche Typen von Dateien. Jedem Typ läßt sich
+        in der Mandantenkonfiguration ein bestimmtes Backend zuordnen.</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>"document": Das sind entweder generierte, eingescannte oder
+            hochgeladene PDF-Dateien, die zu bestimmten ERP-Daten
+            (ERP-Objekte, wie z.B. Rechnung, Lieferschein) gehören.</p></li><li class="listitem"><p>"attachment": zusätzlich hochgeladene Dokumente, die an
+            bestimmte ERP-Objekte angehängt werden, z.B. technische
+            Zeichnungen, Aufmaße. Diese können auch für Artikel, Lieferanten
+            und Kunden hinterlegt sein.</p></li><li class="listitem"><p>"image": Bilder für Artikel. Diese können auch verkleinert
+            in einer Vorschau (Thumbnail) angezeigt werden.</p></li></ul></div><p>Zusätzlich werden in der Datenbank zu den Dateien neben der
+        Zuordnung zu ERP-Objekten, Dateityp Dateinamen und Backend, in dem die
+        Datei gespeichert ist, auch die Quelle der Datei notiert:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>"created": vom System erzeugte Dokumente"</p></li><li class="listitem"><p>"uploaded": hochgeladene Dokumente</p></li><li class="listitem"><p>"email": vom Mail-System empfangene Dateien</p></li><li class="listitem"><p>"scanner[1]": von einem oder mehreren Scannern erzeugte
+            Dateien. Existieren mehrere Scanner, so sind diese durch
+            unterschiedliche Quellennamen zu definieren.</p></li></ul></div><p>Je nach Dateityp sind nur bestimmte Quellen zulässig. So gibt es
+        für "attachment" und "image" nur die Quelle "uploaded". Für "document"
+        gibt es auf jeden Fall die Quelle "created". Die Quellen "scanner" und
+        "email" müssen derzeit in der Datenbank konfiguriert werden (siehe
+        <a class="xref" href="ch03s08.html#file_management.dbconfig" title="3.8.4.2. Datenbank-Konfigurierung">Datenbank-Konfigurierung</a>).</p></div><div class="sect2" title="3.8.3. Anwendung"><div class="titlepage"><div><div><h3 class="title"><a name="d0e6974"></a>3.8.3. Anwendung</h3></div></div></div><p>Die Daten werden bei den ERP-Objekten als extra Reiter
+        dargestellt. Eine Verkaufsrechnung z.B. hat die Reiter "Dokumente" und
+        "Dateianhänge".</p><div class="screenshot"><div class="mediaobject"><img src="images/DMS-Anhaenge.png"></div></div><p>Bei den Dateianhängen wird immer nur die aktuelle Version einer
+        Datei angezeigt. Wird eine Datei mit gleichem Namen hochgeladen, so
+        wird eine neue Version der Datei erstellt. Vorher wird der Anwender
+        durch einen Dialog gefragt, ob er eine neue Version anlegen will oder
+        ob er die Datei umbenennen will, falls es eine neue Datei sein
+        soll.</p><div class="screenshot"><div class="mediaobject"><img src="images/DMS-Anhaenge-hochladen.png"></div></div><p>Es können mehrere Dateien gleichzeitig hochgeladen werden,
+        solange in Summe die maximale Größe nicht überschritten wird (siehe
+        <a class="xref" href="ch03s08.html#file_management.clientconfig" title="3.8.4.1. Mandantenkonfiguration">Mandantenkonfigurierung</a>).</p><div class="screenshot"><div class="mediaobject"><img src="images/DMS-Dokumente.png"></div></div><p>Sind keine weiteren Quellen für Dokumente konfiguriert, so gibt
+        es nur "erzeugte Dokumente". Es werden alle Versionen der generierten
+        Datei angezeigt. Für Verkaufsrechnungen kommen keine anderen Quellen
+        zur Geltung. Werden entsprechend der <a class="xref" href="ch03s08.html#file_management.dbconfig" title="3.8.4.2. Datenbank-Konfigurierung">Datenbank-Konfigurierung</a> zusätzliche Quellen konfiguriert,
+        so sind diese z.B. bei Einkaufsrechnungen sichtbar:</p><div class="screenshot"><div class="mediaobject"><img src="images/DMS-Dokumente-Scanner.png"></div></div><p>Statt des Löschens wird hier die Datei zurück zur Quelle
+        verschoben. Somit kann die Datei anschließend an ein anderes
+        ERP-Objekt angehängt werden.</p><p>Derzeit sind "Titel" und "Beschreibung" noch nicht genutzt. Sie
+        sind bisher nur bei Bildern relevant.</p></div><div class="sect2" title="3.8.4. Konfigurierung"><div class="titlepage"><div><div><h3 class="title"><a name="d0e7017"></a>3.8.4. Konfigurierung</h3></div></div></div><div class="sect3" title="3.8.4.1. Mandantenkonfiguration"><div class="titlepage"><div><div><h4 class="title"><a name="file_management.clientconfig"></a>3.8.4.1. Mandantenkonfiguration</h4></div></div></div><div class="sect4" title="3.8.4.1.1. Reiter &#34;Features&#34;"><div class="titlepage"><div><div><h5 class="title"><a name="d0e7023"></a>3.8.4.1.1. Reiter "Features"</h5></div></div></div><p>Unter dem Reiter <span class="bold"><strong>Features</strong></span>
+            im Abschnitt Dateimanagement ist neben dem "alten" WebDAV das
+            Dateimangement generell zu- und abschaltbar, sowie die Zuordnung
+            der Dateitypen zu Backends. Die Löschbarkeit von Dateien, sowie
+            die maximale Uploadgröße sind Backend-unabhängig</p><div class="screenshot"><div class="mediaobject"><img src="images/DMS-ClientConfig.png"></div></div><p>Die einzelnen Backends sind einzeln einschaltbar.
+            Spezifische Backend-Konfigurierungen sind hier noch
+            ergänzbar.</p></div><div class="sect4" title="3.8.4.1.2. Reiter &#34;Allgemeine Dokumentenanhänge&#34;"><div class="titlepage"><div><div><h5 class="title"><a name="d0e7039"></a>3.8.4.1.2. Reiter "Allgemeine Dokumentenanhänge"</h5></div></div></div><p>Unter dem Reiter <span class="bold"><strong>Allgemeine
+            Dokumentenanhänge</strong></span> kann für alle ERP-Dokumente (
+            Angebote, Aufträge, Lieferscheine, Rechnungen im Verkauf und
+            Einkauf ) allgemeingültige Anhänge hochgeladen werden.</p><div class="screenshot"><div class="mediaobject"><img src="images/DMS-Allgemeine-Dokumentenanhaenge.png"></div></div><p>Diese Anhänge werden beim Generieren von PDF-Dateien an die
+            ERP-Dokumente angehängt, z.B. AGBs oder aktuelle Angebote. Es
+            werden in dem Fall die Daten kopiert, sodass an den ERP-Dokumenten
+            immer die Anhänge zum Generierungszeitpunkt eingebettet
+            sind.</p></div></div><div class="sect3" title="3.8.4.2. Datenbank-Konfigurierung"><div class="titlepage"><div><div><h4 class="title"><a name="file_management.dbconfig"></a>3.8.4.2. Datenbank-Konfigurierung</h4></div></div></div><p>Die zusätzlichen Quellen für "email" oder ein oder mehrere
+          Scanner sind derzeit vom Administrator direkt in der
+          Datenbanktabelle "user_preferences" einzurichten. Die "value" ist im
+          JSON-Format mit den jeweiligen Werten des Verzeichnisses und der
+          Beschreibung der Quelle.</p><pre class="programlisting">
+ id |  login    |  namespace   | version |   key    |          value
+----+-----------+--------------+---------+----------+---------------------------
+  1 | #default# | file_sources | 0.00000 | scanner1 |
+                             {"dir":"/var/tmp/scanner1","desc":"Scanner Einkauf"}
+  2 | #default# | file_sources | 0.00000 | scanner2 |
+                             {"dir":"/var/tmp/scanner2","desc":"Scanner Verkauf"}
+  3 | #default# | file_sources | 0.00000 | emails   |
+                             {"dir":"/var/tmp/emails","desc":"Empfangene Mails" }
+          </pre><p>Es ist daran gedacht, statt dem Default-Eintrag später für
+          bestimmte Benutzer ('login') bestimmte Quellen zuzulassen. Dies wird
+          nach Bedarf implementiert.</p></div><div class="sect3" title="3.8.4.3. kivitendo-Konfigurationsdatei"><div class="titlepage"><div><div><h4 class="title"><a name="file_management.kiviconfig"></a>3.8.4.3. kivitendo-Konfigurationsdatei</h4></div></div></div><p>Dort ist im Abschnitt [paths] der relative oder absolute Pfad
+          zum Dokumentenwurzelverzeichnis einzutragen. Dieser muss für den
+          Webserver schreib- und lesbar sein, jedoch nicht ausführbar.</p><pre class="programlisting">
+[paths]
+document_path = /var/local/kivi_documents
+          </pre><p>Unter diesem Wurzelverzeichnis wird pro Mandant automatisch
+          ein Unterverzeichnis mit der ID des Mandanten angelegt.</p></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch03s07.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch03.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch03s09.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">3.7. Artikelklassifizierung&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;3.9. Webshop-Api</td></tr></table></div></body></html>
\ No newline at end of file
diff --git a/doc/html/ch03s09.html b/doc/html/ch03s09.html
new file mode 100644 (file)
index 0000000..3bb78a3
--- /dev/null
@@ -0,0 +1,61 @@
+<html><head>
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+   <title>3.9. Webshop-Api</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch03.html" title="Kapitel 3. Features und Funktionen"><link rel="prev" href="ch03s08.html" title="3.8. Dateiverwaltung (Mini-DMS)"><link rel="next" href="ch03s10.html" title="3.10. ZUGFeRD Rechnungen"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">3.9. Webshop-Api</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch03s08.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 3. Features und Funktionen</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch03s10.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="3.9. Webshop-Api"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="d0e7073"></a>3.9. Webshop-Api</h2></div></div></div><p>Das Shopmodul bietet die Möglichkeit Onlineshopartikel und
+      Onlineshopbestellungen zu verwalten und zu bearbeiten.</p><p>Es ist Multishopfähig, d.h. Artikel können mehreren oder
+      unterschiedlichen Shops zugeordnet werden. Bestellungen können aus
+      mehreren Shops geholt werden.</p><p>Zur Zeit bietet das Modul nur einen Connector zur REST-Api von
+      Shopware. Weitere Connectoren können dazu programmiert und eingerichtet
+      werden.</p><div class="sect2" title="3.9.1. Rechte für die Webshopapi"><div class="titlepage"><div><div><h3 class="title"><a name="d0e7082"></a>3.9.1. Rechte für die Webshopapi</h3></div></div></div><p>In der Administration können folgende Rechte vergeben
+        werden</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Webshopartikel anlegen und bearbeiten</p></li><li class="listitem"><p>Shopbestellungen holen und bearbeiten</p></li><li class="listitem"><p>Shop anlegen und bearbeiten</p></li></ul></div></div><div class="sect2" title="3.9.2. Konfiguration"><div class="titlepage"><div><div><h3 class="title"><a name="d0e7097"></a>3.9.2. Konfiguration</h3></div></div></div><p>Unter System-&gt;Webshops können Shops angelegt und konfiguriert
+        werden</p><div class="mediaobject"><img src="images/Shop_Listing.png"></div></div><div class="sect2" title="3.9.3. Webshopartikel"><div class="titlepage"><div><div><h3 class="title"><a name="d0e7105"></a>3.9.3. Webshopartikel</h3></div></div></div><div class="sect3" title="3.9.3.1. Shopvariablenreiter in Artikelstammdaten"><div class="titlepage"><div><div><h4 class="title"><a name="d0e7108"></a>3.9.3.1. Shopvariablenreiter in Artikelstammdaten</h4></div></div></div><p>Mit dem Recht "Shopartikel anlegen und bearbeiten" und des
+          Markers <span class="bold"><strong>"Shopartikel" in den Basisdaten
+          </strong></span>zeigt sich der Reiter "Shopvariablen" in den
+          Artikelstammdaten. Hier können jetzt die Artikel mit
+          unterschiedlichen Beschreibung und/oder Preisen für die
+          konfigutierten Shops angelegt und bearbeitet werden. An dieser
+          Stelle können auch beliebig viele Bilder dem Shopartikel zugeordnet
+          werden. Artikelbilder gelten für alle Shops.</p><div class="mediaobject"><img src="images/Shop_Artikel.png"></div><p>Die Artikelgruppen werden direkt vom Shopsystem geholt somit
+          ist es möglich einen Artikel auch mehreren Gruppen
+          zuzuordenen</p></div><div class="sect3" title="3.9.3.2. Shopartikelliste"><div class="titlepage"><div><div><h4 class="title"><a name="d0e7121"></a>3.9.3.2. Shopartikelliste</h4></div></div></div><p>Unter dem Menu Webshop-&gt;Webshop Artikel hat man nochmal
+          eine Gesamtübersicht. Von hier aus ist es möglich Artikel im Stapel
+          unter verschiedenen Kriterien &lt;alles&gt;&lt;nur Preis&gt;&lt;nur
+          Bestand&gt;&lt;Preis und Bestand&gt; an die jeweiligen Shops
+          hochzuladen.</p><div class="mediaobject"><img src="images/Shop_Artikel_Listing.png"></div></div></div><div class="sect2" title="3.9.4. Bestellimport"><div class="titlepage"><div><div><h3 class="title"><a name="d0e7129"></a>3.9.4. Bestellimport</h3></div></div></div><p>Unter dem Menupunkt Webshop-&gt;Webshop Import öffnet sich die
+        Bestellimportsliste. Hier ist sind Möglichkeiten gegeben Neue
+        Bestellungen vom Shop abzuholen, geholte Bestellungen im Stapel oder
+        einzeln als Auftrag zu transferieren. Die Liste kann nach
+        verschiedenen Kriterien gefiltert werden.</p><div class="mediaobject"><img src="images/Shop_Bestell.png"></div><p>Bei Einträgen in der Liste.</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>keine Kundennummer: Es gibt ähnliche Kundendatensätze und
+            der Datensatz konnte nicht eindeutig zugewiesen werden.</p></li><li class="listitem"><p>Kundennummer und Rechnungen rot hinterlegt: Der Kunde hat
+            offene Posten und kann deswegen nicht im Stapel übernommen
+            werden.</p></li><li class="listitem"><p>Rechnungsadresse grün hinterlegt: Der Kunde konnte eindeutig
+            einem Datensatz zugeordnet werden. Die Shopbestellung kann im
+            Stapel mit dem Button "Anwenden" und wenn markiert als Auftrag
+            übernommen werden.</p></li><li class="listitem"><p>Kundennummer vorhanden, aber die Checkbox "Auftrag
+            erstellen" fehlt. Der Kunde hat vermutlich eine
+            Shopauftragssperre.</p></li><li class="listitem"><p>Lieferadresse grau hinterlegt: Optische Anzeige, dass es
+            sich um eine unterschiedliche Lieferadresse handelt.
+            Lieferadressen werden aber grundsätzlich beim Transferieren zu
+            Aufträgen mit übernommen.</p></li><li class="listitem"><p>In der Spalte Positionen/Betrag/Versandkosten zeigt sich ein
+            tooltip zu den Positionen.</p></li></ul></div><p>Maske Auftrag erstellen</p><p>Viele Shopsysteme haben drei verschieden Adresstypen Kunden-,
+        Rechnungs-, und Lieferadresse, die sich auch alle unterscheiden
+        können. Diese werden im oberen Bereich angezeigt. Es ist möglich jede
+        dieser Adresse einzeln in kivitendo als Kunde zu übernehmen. Es werden
+        die Werte Formulareingabe übernommen. Es wird bei einer Änderung
+        allerdings nur diese in die kivitendo Kundenstammdaten übernommen, die
+        Shopbestellung bleibt bestehen.</p><p>Mit der mittleren Adresse(Rechnungsadresse) im oberen Bereich,
+        kann ich den ausgewählten kivitendodatensatz des mittleren Bereich
+        überschreiben. Das ist sinnvoll, wenn ich erkenne, das der Kunde z.B.
+        umgezogen ist.</p><p>Im mittleren Bereich das Adresslisting zeigt:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Rot hinterlegt: Kunde hat eine Shopauftragssperre, diese
+            muss zuerst deaktiviert werden bevor ich diesem Kunden eine
+            Shopbestellung zuordnen kann.</p></li><li class="listitem"><p>Kundenname fett und rot: Hier hat der Kunde eine Bemerkung
+            in den Stammdaten. Ein Tooltip zeigt diese Bemerkung. Das kann dan
+            auch der Grund für die Auftragssperre sein.</p></li><li class="listitem"><p>Die Buttons "Auftrag erstellen" und "Kunde mit
+            Rechnungsadresse überschreiben" zeigen sich erst, wenn ein Kunde
+            aus dem Listing ausgewählt ist.</p></li><li class="listitem"><p>Es ist aber möglich die Shopbestellung zu löschen.</p></li><li class="listitem"><p>Ist eine Bestellung schon übernommen, zeigen sich an dieser
+            Stelle, die dazugehörigen Belegverknüpfungen.</p></li></ul></div></div><div class="sect2" title="3.9.5. Mapping der Daten"><div class="titlepage"><div><div><h3 class="title"><a name="d0e7182"></a>3.9.5. Mapping der Daten</h3></div></div></div><p>Das Mapping der kivitendo Daten mit den Shopdaten geschieht in
+        der Datei SL/ShopConnector/&lt;SHOPCONNECTORNAME&gt;.pm
+        z.B.:SL/ShopConnector/Shopware.pm</p><p>In dieser Datei gibt es einen Bereich wo die Bestellpostionen,
+        die Bestellkopfdaten und die Artikeldaten gemapt werden. In dieser
+        Datei kann ein individelles Mapping dann gemacht werden. Zu Shopware
+        gibt es hier eine sehr gute Dokumentation: <a class="ulink" href="https://developers.shopware.com/developers-guide/rest-api/" target="_top">https://developers.shopware.com/developers-guide/rest-api/</a>
+            </p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch03s08.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch03.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch03s10.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">3.8. Dateiverwaltung (Mini-DMS)&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;3.10. ZUGFeRD Rechnungen</td></tr></table></div></body></html>
\ No newline at end of file
diff --git a/doc/html/ch03s10.html b/doc/html/ch03s10.html
new file mode 100644 (file)
index 0000000..ef0de2e
--- /dev/null
@@ -0,0 +1,54 @@
+<html><head>
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+   <title>3.10. ZUGFeRD Rechnungen</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch03.html" title="Kapitel 3. Features und Funktionen"><link rel="prev" href="ch03s09.html" title="3.9. Webshop-Api"><link rel="next" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">3.10. ZUGFeRD Rechnungen</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch03s09.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 3. Features und Funktionen</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch04.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="3.10. ZUGFeRD Rechnungen"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="features.zugferd"></a>3.10. ZUGFeRD Rechnungen</h2></div></div></div><div class="sect2" title="3.10.1. Vorbedingung"><div class="titlepage"><div><div><h3 class="title"><a name="features.zugferd.preamble"></a>3.10.1. Vorbedingung</h3></div></div></div><p>
+          Für die Erstellung von ZUGFeRD PDFs wird TexLive2018 oder höher benötigt.
+         </p><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left">Anmerkung</th></tr><tr><td align="left" valign="top"><p>
+          Wer kein TexLive2018 oder höher installieren kann, kann eine lokale Umgebung nur für kivitendo wie folgt erzeugen:
+         </p><pre class="programlisting">
+        1. Download des offiziellen Installers von https://www.tug.org/texlive/quickinstall.html
+
+        2. Installer ausführen, Standard-Ort für Installation belassen, evtl. ein paar Pakete abwählen, installieren lassen
+
+        3. Ein kleine Script »run_pdflatex.sh« anlegen, das den PATH auf das  Installationsverezichnis setzt und pdflatex ausführt:
+
+        ------------------------------------------------------------
+        #!/bin/bash
+
+        export PATH=/usr/local/texlive/2020/bin/x86_64-linux:$PATH
+        hash -r
+
+        exec pdflatex "$@"
+        ------------------------------------------------------------
+
+        4. In config/kivitendo.conf den Parameter »latex« auf den Pfad zu »run_pdflatex.sh« setzen
+
+        5. Webserver neu starten
+        </pre></td></tr></table></div></div><div class="sect2" title="3.10.2. Übersicht"><div class="titlepage"><div><div><h3 class="title"><a name="features.zugferd.summary"></a>3.10.2. Übersicht</h3></div></div></div><p>Mit der Version 3.5.6 bietet kivitendo die Möglichkeit ZUGFeRD
+                       Rechnungen zu erstellen, sowie auch  ZUGFeRD Rechnungen direkt in
+                       kivitendo einzulesen. </p><p>Bei  ZUGFeRD Rechnungen handelt es sich um eine PDF Datei in
+                       der eine XML-Datei eingebettet ist. Der Aufbau der XML-Datei ist
+                       standardisiert und ermöglicht so den Austausch zwischen
+                       den verschiedenen Softwareprodukten. Kivitendo setzt mit der
+                       Version 3.5.6 den ZUGFeRD 2.1 Standard um.</p><p>Weiter Details zu ZUGFeRD sind unter diesem Link zu finden:
+                       <a class="ulink" href="https://www.ferd-net.de/standards/was-ist-zugferd/index.html" target="_top">https://www.ferd-net.de/standards/was-ist-zugferd/index.html</a>
+      
+            </p></div><div class="sect2" title="3.10.3. Erstellen von ZUGFeRD Rechnungen in Kivitendo"><div class="titlepage"><div><div><h3 class="title"><a name="features.zugferd.create_zugferd_bills"></a>3.10.3. Erstellen von ZUGFeRD Rechnungen in Kivitendo</h3></div></div></div><p>Für die Erstellung von ZUGFeRD Rechnungen bedarf es in
+                       kivitendo zwei Dinge:</p><div class="orderedlist"><ol class="orderedlist" type="1"><li class="listitem"><p>Die Erstellung muss in der Mandantenkonfiguration
+                                       aktiviert sein</p></li><li class="listitem"><p>Beim mindestens einem Bankkonto muss die Option
+                                       „Nutzung von ZUGFeRD“ aktiviert sein</p></li></ol></div><div class="sect3" title="3.10.3.1. Mandantenkonfiguration"><div class="titlepage"><div><div><h4 class="title"><a name="d0e7229"></a>3.10.3.1. Mandantenkonfiguration</h4></div></div></div><p>Die Einstellung für die Erstellung von ZUGFeRD Rechnungen
+                               erfolgt unter „System“ → „Mandatenkonfiguration“ → „Features“.
+                               Im Abschnitt „Einkauf und Verkauf“ finden Sie die Einstellung
+                               „Verkaufsrechnungen mit ZUGFeRD-Daten erzeugen“.
+                               Hier besteht die Auswahl zwischen:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>ZUGFeRD-Rechnungen erzeugen</p></li><li class="listitem"><p>ZUGFeRD-Rechnungen im Testmodus erzeugen</p></li><li class="listitem"><p>Keine ZUGFeRD Rechnungen erzeugen</p></li></ul></div><p>Rechnungen die als PDF erzeugt werden, werden je nach
+                               Einstellung nun im ZUGFeRD Format ausgegeben.</p></div><div class="sect3" title="3.10.3.2. Konfiguration der Bankkonten"><div class="titlepage"><div><div><h4 class="title"><a name="d0e7246"></a>3.10.3.2. Konfiguration der Bankkonten</h4></div></div></div><p>Unter „System → Bankkonten“ muss bei mindestens einem
+                               Bankkonto die Option „Nutzung mit ZUGFeRD“ auf „Ja“ gestellt
+                               werden.</p></div></div><div class="sect2" title="3.10.4. Einlesen von ZUGFeRD Rechnungen in Kivitendo"><div class="titlepage"><div><div><h3 class="title"><a name="features.zugferd.read_zugferd_bills"></a>3.10.4. Einlesen von ZUGFeRD Rechnungen in Kivitendo</h3></div></div></div><p>Es lassen sich auch Rechnungen von Kreditoren, die im
+                       ZUGFeRD Format erstellt wurden, nach Kivitendo importieren.
+                       Hierfür müssen auch zwei Voraussetzungen erfüllt werden:
+                       </p><div class="orderedlist"><ol class="orderedlist" type="1"><li class="listitem"><p>Beim Lieferanten muss die Umsatzsteuer-ID und das
+                                       Bankkonto hinterlegt sein</p></li><li class="listitem"><p>Für den Kreditoren muss eine Buchungsvorlage existieren.</p></li></ol></div><p>Wenn diese Voraussetzungen erfüllt sind, kann die Rechnung
+                       über „Finanzbuchhaltung“ → „Factur-X-/ZUGFeRD-Import“ über die „Durchsuchen“
+                       Schaltfläche ausgewählt werden und über die Schaltfläche „Import“
+                       eingeladen werden. Es öffnet sich daraufhin die Kreditorenbuchung.
+                       Die auslesbaren Daten aus dem eingebetteten XML der PDF Datei
+                       werden in der Kreditorenbuchung ergänzt.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch03s09.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch03.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch04.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">3.9. Webshop-Api&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;Kapitel 4. Entwicklerdokumentation</td></tr></table></div></body></html>
\ No newline at end of file
index d026ae2..6abd36c 100644 (file)
@@ -1,6 +1,6 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>Kapitel 4. Entwicklerdokumentation</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="prev" href="ch03s05.html" title="3.5. Mandantenkonfiguration Lager"><link rel="next" href="ch04s02.html" title="4.2. Entwicklung unter FastCGI"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">Kapitel 4. Entwicklerdokumentation</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch03s05.html">Zurück</a>&nbsp;</td><th width="60%" align="center">&nbsp;</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch04s02.html">Weiter</a></td></tr></table><hr></div><div class="chapter" title="Kapitel 4. Entwicklerdokumentation"><div class="titlepage"><div><div><h2 class="title"><a name="d0e5742"></a>Kapitel 4. Entwicklerdokumentation</h2></div></div></div><div class="sect1" title="4.1. Globale Variablen"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="devel.globals"></a>4.1. Globale Variablen</h2></div></div></div><div class="sect2" title="4.1.1. Wie sehen globale Variablen in Perl aus?"><div class="titlepage"><div><div><h3 class="title"><a name="d0e5748"></a>4.1.1. Wie sehen globale Variablen in Perl aus?</h3></div></div></div><p>Globale Variablen liegen in einem speziellen namespace namens
+   <title>Kapitel 4. Entwicklerdokumentation</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="prev" href="ch03s10.html" title="3.10. ZUGFeRD Rechnungen"><link rel="next" href="ch04s02.html" title="4.2. Entwicklung unter FastCGI"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">Kapitel 4. Entwicklerdokumentation</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch03s10.html">Zurück</a>&nbsp;</td><th width="60%" align="center">&nbsp;</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch04s02.html">Weiter</a></td></tr></table><hr></div><div class="chapter" title="Kapitel 4. Entwicklerdokumentation"><div class="titlepage"><div><div><h2 class="title"><a name="d0e7265"></a>Kapitel 4. Entwicklerdokumentation</h2></div></div></div><div class="sect1" title="4.1. Globale Variablen"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="devel.globals"></a>4.1. Globale Variablen</h2></div></div></div><div class="sect2" title="4.1.1. Wie sehen globale Variablen in Perl aus?"><div class="titlepage"><div><div><h3 class="title"><a name="d0e7271"></a>4.1.1. Wie sehen globale Variablen in Perl aus?</h3></div></div></div><p>Globale Variablen liegen in einem speziellen namespace namens
         "main", der von überall erreichbar ist. Darüber hinaus sind bareword
         globs global und die meisten speziellen Variablen sind...
         speziell.</p><p>Daraus ergeben sich folgende Formen:</p><div class="variablelist"><dl><dt><span class="term">
@@ -25,7 +25,7 @@
               <code class="varname">$PACKAGE::form</code>.</p></dd><dt><span class="term">
                      <code class="literal">local $form</code>
                   </span></dt><dd><p>Alle Änderungen an <code class="varname">$form</code> werden am Ende
-              des scopes zurückgesetzt</p></dd></dl></div></div><div class="sect2" title="4.1.2. Warum sind globale Variablen ein Problem?"><div class="titlepage"><div><div><h3 class="title"><a name="d0e5849"></a>4.1.2. Warum sind globale Variablen ein Problem?</h3></div></div></div><p>Das erste Problem ist <span class="productname">FCGI</span>™.</p><p>
+              des scopes zurückgesetzt</p></dd></dl></div></div><div class="sect2" title="4.1.2. Warum sind globale Variablen ein Problem?"><div class="titlepage"><div><div><h3 class="title"><a name="d0e7372"></a>4.1.2. Warum sind globale Variablen ein Problem?</h3></div></div></div><p>Das erste Problem ist <span class="productname">FCGI</span>™.</p><p>
                <span class="productname">SQL-Ledger</span>™ hat fast alles im globalen
         namespace abgelegt, und erwartet, dass es da auch wiederzufinden ist.
         Unter <span class="productname">FCGI</span>™ müssen diese Sachen aber wieder
@@ -39,7 +39,7 @@
         dies hat, seit der Einführung, u.a. schon so manche langwierige
         Bug-Suche verkürzt. Da globale Variablen aber implizit mit Package
         angegeben werden, werden die nicht geprüft, und somit kann sich
-        schnell ein Tippfehler einschleichen.</p></div><div class="sect2" title="4.1.3. Kanonische globale Variablen"><div class="titlepage"><div><div><h3 class="title"><a name="d0e5882"></a>4.1.3. Kanonische globale Variablen</h3></div></div></div><p>Um dieses Problem im Griff zu halten gibt es einige wenige
+        schnell ein Tippfehler einschleichen.</p></div><div class="sect2" title="4.1.3. Kanonische globale Variablen"><div class="titlepage"><div><div><h3 class="title"><a name="d0e7405"></a>4.1.3. Kanonische globale Variablen</h3></div></div></div><p>Um dieses Problem im Griff zu halten gibt es einige wenige
         globale Variablen, die kanonisch sind, d.h. sie haben bestimmte
         vorgegebenen Eigenschaften, und alles andere sollte anderweitig
         umhergereicht werden.</p><p>Diese Variablen sind im Moment die folgenden neun:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
@@ -62,7 +62,7 @@
                      <code class="varname">$::request</code>
                   </p></li></ul></div><p>Damit diese nicht erneut als Müllhalde missbraucht werden, im
         Folgenden eine kurze Erläuterung der bestimmten vorgegebenen
-        Eigenschaften (Konventionen):</p><div class="sect3" title="4.1.3.1. $::form"><div class="titlepage"><div><div><h4 class="title"><a name="d0e5946"></a>4.1.3.1. $::form</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Ist ein Objekt der Klasse
+        Eigenschaften (Konventionen):</p><div class="sect3" title="4.1.3.1. $::form"><div class="titlepage"><div><div><h4 class="title"><a name="d0e7469"></a>4.1.3.1. $::form</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Ist ein Objekt der Klasse
               "<code class="classname">Form</code>"</p></li><li class="listitem"><p>Wird nach jedem Request gelöscht</p></li><li class="listitem"><p>Muss auch in Tests und Konsolenscripts vorhanden
               sein.</p></li><li class="listitem"><p>Enthält am Anfang eines Requests die Requestparameter vom
               User</p></li><li class="listitem"><p>Kann zwar intern über Requestgrenzen ein Datenbankhandle
   push @{ $form-&gt;{TEMPLATE_ARRAYS}{number} },          $form-&gt;{"partnumber_$i"};
   push @{ $form-&gt;{TEMPLATE_ARRAYS}{description} },     $form-&gt;{"description_$i"};
   # ...
-}</pre></div><div class="sect3" title="4.1.3.2. %::myconfig"><div class="titlepage"><div><div><h4 class="title"><a name="d0e6030"></a>4.1.3.2. %::myconfig</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Das einzige Hash unter den globalen Variablen</p></li><li class="listitem"><p>Wird spätestens benötigt wenn auf die Datenbank
+}</pre></div><div class="sect3" title="4.1.3.2. %::myconfig"><div class="titlepage"><div><div><h4 class="title"><a name="d0e7553"></a>4.1.3.2. %::myconfig</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Das einzige Hash unter den globalen Variablen</p></li><li class="listitem"><p>Wird spätestens benötigt wenn auf die Datenbank
               zugegriffen wird</p></li><li class="listitem"><p>Wird bei jedem Request neu erstellt.</p></li><li class="listitem"><p>Enthält die Userdaten des aktuellen Logins</p></li><li class="listitem"><p>Sollte nicht ohne Filterung irgendwo gedumpt werden oder
               extern serialisiert werden, weil da auch der Datenbankzugriff
-              für diesen user drinsteht.</p></li><li class="listitem"><p>Enthält unter anderem Listenbegrenzung vclimit,
-              Datumsformat dateformat und Nummernformat numberformat</p></li><li class="listitem"><p>Enthält Datenbankzugriffinformationen</p></li></ul></div><p>
+              für diesen user drinsteht.</p></li><li class="listitem"><p>Enthält unter anderem Datumsformat dateformat und
+              Nummernformat numberformat</p></li><li class="listitem"><p>Enthält Datenbankzugriffinformationen</p></li></ul></div><p>
                   <code class="varname">%::myconfig</code> ist im Moment der Ersatz für
           ein Userobjekt. Die meisten Funktionen, die etwas anhand des
           aktuellen Users entscheiden müssen, befragen
           überwiegend die Daten, die sich unter <span class="guimenu">Programm</span>
           -&gt; <span class="guimenuitem">Einstellungen</span> befinden, bzw. die
           Informationen über den Benutzer die über die
-          Administrator-Schnittstelle eingegeben wurden.</p></div><div class="sect3" title="4.1.3.3. $::locale"><div class="titlepage"><div><div><h4 class="title"><a name="d0e6069"></a>4.1.3.3. $::locale</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Objekt der Klasse "Locale"</p></li><li class="listitem"><p>Wird pro Request erstellt</p></li><li class="listitem"><p>Muss auch für Tests und Scripte immer verfügbar
+          Administrator-Schnittstelle eingegeben wurden.</p></div><div class="sect3" title="4.1.3.3. $::locale"><div class="titlepage"><div><div><h4 class="title"><a name="d0e7592"></a>4.1.3.3. $::locale</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Objekt der Klasse "Locale"</p></li><li class="listitem"><p>Wird pro Request erstellt</p></li><li class="listitem"><p>Muss auch für Tests und Scripte immer verfügbar
               sein.</p></li><li class="listitem"><p>Cached intern über Requestgrenzen hinweg benutzte
               Locales</p></li></ul></div><p>Lokalisierung für den aktuellen User. Alle Übersetzungen,
-          Zahlen- und Datumsformatierungen laufen über dieses Objekt.</p></div><div class="sect3" title="4.1.3.4. $::lxdebug"><div class="titlepage"><div><div><h4 class="title"><a name="d0e6087"></a>4.1.3.4. $::lxdebug</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Objekt der Klasse "LXDebug"</p></li><li class="listitem"><p>Wird global gecached</p></li><li class="listitem"><p>Muss immer verfügbar sein, in nahezu allen
+          Zahlen- und Datumsformatierungen laufen über dieses Objekt.</p></div><div class="sect3" title="4.1.3.4. $::lxdebug"><div class="titlepage"><div><div><h4 class="title"><a name="d0e7610"></a>4.1.3.4. $::lxdebug</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Objekt der Klasse "LXDebug"</p></li><li class="listitem"><p>Wird global gecached</p></li><li class="listitem"><p>Muss immer verfügbar sein, in nahezu allen
               Funktionen</p></li></ul></div><p>
                   <code class="varname">$::lxdebug</code> stellt Debuggingfunktionen
           bereit, wie "<code class="function">enter_sub</code>" und
           "<code class="function">message</code>" und "<code class="function">dump</code>" mit
           denen man flott Informationen ins Log (tmp/kivitendo-debug.log)
           packen kann.</p><p>Beispielsweise so:</p><pre class="programlisting">$main::lxdebug-&gt;message(0, 'Meine Konfig:' . Dumper (%::myconfig));
-$main::lxdebug-&gt;message(0, 'Wer bin ich? Kunde oder Lieferant:' . $form-&gt;{vc});</pre></div><div class="sect3" title="4.1.3.5. $::auth"><div class="titlepage"><div><div><h4 class="title"><a name="d0e6124"></a>4.1.3.5. $::auth</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Objekt der Klasse "SL::Auth"</p></li><li class="listitem"><p>Wird global gecached</p></li><li class="listitem"><p>Hat eine permanente DB Verbindung zur Authdatenbank</p></li><li class="listitem"><p>Wird nach jedem Request resettet.</p></li></ul></div><p>
+$main::lxdebug-&gt;message(0, 'Wer bin ich? Kunde oder Lieferant:' . $form-&gt;{vc});</pre></div><div class="sect3" title="4.1.3.5. $::auth"><div class="titlepage"><div><div><h4 class="title"><a name="d0e7647"></a>4.1.3.5. $::auth</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Objekt der Klasse "SL::Auth"</p></li><li class="listitem"><p>Wird global gecached</p></li><li class="listitem"><p>Hat eine permanente DB Verbindung zur Authdatenbank</p></li><li class="listitem"><p>Wird nach jedem Request resettet.</p></li></ul></div><p>
                   <code class="varname">$::auth</code> stellt Funktionen bereit um die
           Rechte des aktuellen Users abzufragen. Obwohl diese Informationen
           vom aktuellen User abhängen wird das Objekt aus
           Geschwindigkeitsgründen nur einmal angelegt und dann nach jedem
-          Request kurz resettet.</p><p>Dieses Objekt kapselt auch den gerade aktiven Mandanten. Dessen Einstellungen können über
-          <code class="literal">$::auth-&gt;client</code> abgefragt werden; Rückgabewert ist ein Hash mit den Werten aus der Tabelle
-          <code class="literal">auth.clients</code>.</p></div><div class="sect3" title="4.1.3.6. $::lx_office_conf"><div class="titlepage"><div><div><h4 class="title"><a name="d0e6153"></a>4.1.3.6. $::lx_office_conf</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Objekt der Klasse
+          Request kurz resettet.</p><p>Dieses Objekt kapselt auch den gerade aktiven Mandanten.
+          Dessen Einstellungen können über
+          <code class="literal">$::auth-&gt;client</code> abgefragt werden; Rückgabewert
+          ist ein Hash mit den Werten aus der Tabelle
+          <code class="literal">auth.clients</code>.</p></div><div class="sect3" title="4.1.3.6. $::lx_office_conf"><div class="titlepage"><div><div><h4 class="title"><a name="d0e7676"></a>4.1.3.6. $::lx_office_conf</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Objekt der Klasse
               "<code class="classname">SL::LxOfficeConf</code>"</p></li><li class="listitem"><p>Global gecached</p></li><li class="listitem"><p>Repräsentation der
               <code class="filename">config/kivitendo.conf[.default]</code>-Dateien</p></li></ul></div><p>Globale Konfiguration. Configdateien werden zum Start gelesen
           und danach nicht mehr angefasst. Es ist derzeit nicht geplant, dass
@@ -152,16 +154,16 @@ $main::lxdebug-&gt;message(0, 'Wer bin ich? Kunde oder Lieferant:' . $form-&gt;{
 file_name = /tmp/kivitendo-debug.log</pre><p>ist der Key <code class="varname">file</code> im Programm als
           <code class="varname">$::lx_office_conf-&gt;{debug}{file}</code>
           erreichbar.</p><div class="warning" title="Warnung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Warning"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Warnung]" src="system/docbook-xsl/images/warning.png"></td><th align="left">Warnung</th></tr><tr><td align="left" valign="top"><p>Zugriff auf die Konfiguration erfolgt im Moment über
-            Hashkeys, sind also nicht gegen Tippfehler abgesichert.</p></td></tr></table></div></div><div class="sect3" title="4.1.3.7. $::instance_conf"><div class="titlepage"><div><div><h4 class="title"><a name="d0e6189"></a>4.1.3.7. $::instance_conf</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Objekt der Klasse
+            Hashkeys, sind also nicht gegen Tippfehler abgesichert.</p></td></tr></table></div></div><div class="sect3" title="4.1.3.7. $::instance_conf"><div class="titlepage"><div><div><h4 class="title"><a name="d0e7712"></a>4.1.3.7. $::instance_conf</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Objekt der Klasse
               "<code class="classname">SL::InstanceConfiguration</code>"</p></li><li class="listitem"><p>wird pro Request neu erstellt</p></li></ul></div><p>Funktioniert wie <code class="varname">$::lx_office_conf</code>,
           speichert aber Daten die von der Instanz abhängig sind. Eine Instanz
           ist hier eine Mandantendatenbank. Beispielsweise überprüft
           </p><pre class="programlisting">$::instance_conf-&gt;get_inventory_system eq 'perpetual'</pre><p>
-          ob die berüchtigte Bestandsmethode zur Anwendung kommt.</p></div><div class="sect3" title="4.1.3.8. $::dispatcher"><div class="titlepage"><div><div><h4 class="title"><a name="d0e6210"></a>4.1.3.8. $::dispatcher</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Objekt der Klasse
+          ob die berüchtigte Bestandsmethode zur Anwendung kommt.</p></div><div class="sect3" title="4.1.3.8. $::dispatcher"><div class="titlepage"><div><div><h4 class="title"><a name="d0e7733"></a>4.1.3.8. $::dispatcher</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Objekt der Klasse
               "<code class="varname">SL::Dispatcher</code>"</p></li><li class="listitem"><p>wird pro Serverprozess erstellt.</p></li><li class="listitem"><p>enthält Informationen über die technische Verbindung zum
               Server</p></li></ul></div><p>Der dritte Punkt ist auch der einzige Grund warum das Objekt
           global gespeichert wird. Wird vermutlich irgendwann in einem anderen
-          Objekt untergebracht.</p></div><div class="sect3" title="4.1.3.9. $::request"><div class="titlepage"><div><div><h4 class="title"><a name="d0e6228"></a>4.1.3.9. $::request</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Hashref (evtl später Objekt)</p></li><li class="listitem"><p>Wird pro Request neu initialisiert.</p></li><li class="listitem"><p>Keine Unterstruktur garantiert.</p></li></ul></div><p>
+          Objekt untergebracht.</p></div><div class="sect3" title="4.1.3.9. $::request"><div class="titlepage"><div><div><h4 class="title"><a name="d0e7751"></a>4.1.3.9. $::request</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Hashref (evtl später Objekt)</p></li><li class="listitem"><p>Wird pro Request neu initialisiert.</p></li><li class="listitem"><p>Keine Unterstruktur garantiert.</p></li></ul></div><p>
                   <code class="varname">$::request</code> ist ein generischer Platz um
           Daten "für den aktuellen Request" abzulegen. Sollte nicht für action
           at a distance benutzt werden, sondern um lokales memoizing zu
@@ -174,20 +176,20 @@ file_name = /tmp/kivitendo-debug.log</pre><p>ist der Key <code class="varname">f
               <code class="varname">$::request</code>
                      </p></li><li class="listitem"><p>Muss ich von anderen Teilen des Programms lesend drauf
               zugreifen? Dann <code class="varname">$::request</code>, aber Zugriff über
-              Wrappermethode</p></li></ul></div></div></div><div class="sect2" title="4.1.4. Ehemalige globale Variablen"><div class="titlepage"><div><div><h3 class="title"><a name="d0e6270"></a>4.1.4. Ehemalige globale Variablen</h3></div></div></div><p>Die folgenden Variablen waren einmal im Programm, und wurden
-        entfernt.</p><div class="sect3" title="4.1.4.1. $::cgi"><div class="titlepage"><div><div><h4 class="title"><a name="d0e6275"></a>4.1.4.1. $::cgi</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>war nötig, weil cookie Methoden nicht als
+              Wrappermethode</p></li></ul></div></div></div><div class="sect2" title="4.1.4. Ehemalige globale Variablen"><div class="titlepage"><div><div><h3 class="title"><a name="d0e7793"></a>4.1.4. Ehemalige globale Variablen</h3></div></div></div><p>Die folgenden Variablen waren einmal im Programm, und wurden
+        entfernt.</p><div class="sect3" title="4.1.4.1. $::cgi"><div class="titlepage"><div><div><h4 class="title"><a name="d0e7798"></a>4.1.4.1. $::cgi</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>war nötig, weil cookie Methoden nicht als
               Klassenfunktionen funktionieren</p></li><li class="listitem"><p>Aufruf als Klasse erzeugt Dummyobjekt was im
               Klassennamespace gehalten wird und über Requestgrenzen
               leaked</p></li><li class="listitem"><p>liegt jetzt unter
               <code class="varname">$::request-&gt;{cgi}</code>
-                     </p></li></ul></div></div><div class="sect3" title="4.1.4.2. $::all_units"><div class="titlepage"><div><div><h4 class="title"><a name="d0e6291"></a>4.1.4.2. $::all_units</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>war nötig, weil einige Funktionen in Schleifen zum Teil
+                     </p></li></ul></div></div><div class="sect3" title="4.1.4.2. $::all_units"><div class="titlepage"><div><div><h4 class="title"><a name="d0e7814"></a>4.1.4.2. $::all_units</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>war nötig, weil einige Funktionen in Schleifen zum Teil
               ein paar hundert mal pro Request eine Liste der Einheiten
               brauchen, und de als Parameter durch einen Riesenstack von
               Funktionen geschleift werden müssten.</p></li><li class="listitem"><p>Liegt jetzt unter
               <code class="varname">$::request-&gt;{cache}{all_units}</code>
                      </p></li><li class="listitem"><p>Wird nur in
               <code class="function">AM-&gt;retrieve_all_units()</code> gesetzt oder
-              gelesen.</p></li></ul></div></div><div class="sect3" title="4.1.4.3. %::called_subs"><div class="titlepage"><div><div><h4 class="title"><a name="d0e6310"></a>4.1.4.3. %::called_subs</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>wurde benutzt um callsub deep recursions
+              gelesen.</p></li></ul></div></div><div class="sect3" title="4.1.4.3. %::called_subs"><div class="titlepage"><div><div><h4 class="title"><a name="d0e7833"></a>4.1.4.3. %::called_subs</h4></div></div></div><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>wurde benutzt um callsub deep recursions
               abzufangen.</p></li><li class="listitem"><p>Wurde entfernt, weil callsub nur einen Bruchteil der
               möglichen Rekursioenen darstellt, und da nie welche
-              auftreten.</p></li><li class="listitem"><p>komplette recursion protection wurde entfernt.</p></li></ul></div></div></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch03s05.html">Zurück</a>&nbsp;</td><td width="20%" align="center">&nbsp;</td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch04s02.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">3.5. Mandantenkonfiguration Lager&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;4.2. Entwicklung unter FastCGI</td></tr></table></div></body></html>
\ No newline at end of file
+              auftreten.</p></li><li class="listitem"><p>komplette recursion protection wurde entfernt.</p></li></ul></div></div></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch03s10.html">Zurück</a>&nbsp;</td><td width="20%" align="center">&nbsp;</td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch04s02.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">3.10. ZUGFeRD Rechnungen&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;4.2. Entwicklung unter FastCGI</td></tr></table></div></body></html>
\ No newline at end of file
index 2dab81c..3c91746 100644 (file)
@@ -1,6 +1,6 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>4.2. Entwicklung unter FastCGI</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"><link rel="prev" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"><link rel="next" href="ch04s03.html" title="4.3. SQL-Upgradedateien"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">4.2. Entwicklung unter FastCGI</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch04.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 4. Entwicklerdokumentation</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch04s03.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="4.2. Entwicklung unter FastCGI"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="devel.fcgi"></a>4.2. Entwicklung unter FastCGI</h2></div></div></div><div class="sect2" title="4.2.1. Allgemeines"><div class="titlepage"><div><div><h3 class="title"><a name="devel.fcgi.general"></a>4.2.1. Allgemeines</h3></div></div></div><p>Wenn Änderungen in der Konfiguration von kivitendo gemacht
+   <title>4.2. Entwicklung unter FastCGI</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"><link rel="prev" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"><link rel="next" href="ch04s03.html" title="4.3. Programmatische API-Aufrufe"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">4.2. Entwicklung unter FastCGI</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch04.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 4. Entwicklerdokumentation</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch04s03.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="4.2. Entwicklung unter FastCGI"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="devel.fcgi"></a>4.2. Entwicklung unter FastCGI</h2></div></div></div><div class="sect2" title="4.2.1. Allgemeines"><div class="titlepage"><div><div><h3 class="title"><a name="devel.fcgi.general"></a>4.2.1. Allgemeines</h3></div></div></div><p>Wenn Änderungen in der Konfiguration von kivitendo gemacht
         werden, muss der Webserver neu gestartet werden.</p><p>Bei der Entwicklung für FastCGI ist auf ein paar Fallstricke zu
         achten. Dadurch, dass das Programm in einer Endlosschleife läuft,
         müssen folgende Aspekte beachtet werden.</p></div><div class="sect2" title="4.2.2. Programmende und Ausnahmen"><div class="titlepage"><div><div><h3 class="title"><a name="devel.fcgi.exiting"></a>4.2.2. Programmende und Ausnahmen</h3></div></div></div><p>Betrifft die Funktionen <code class="function">warn</code>,
@@ -34,4 +34,4 @@
         4GB Arbeitsspeicher und Ubuntu 9.10 eine halbe Sekunde. In der 2.6.0
         sind es je nach Menge der definierten Variablen 1-2s. Ab der
         Moose/Rose::DB Version sind es 5-6s.</p><p>Mit FastCGI ist die neuste Version auf 0,26 Sekunden selbst in
-        den kritischen Pfaden, unter 0,15 sonst.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch04.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch04.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch04s03.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">Kapitel 4. Entwicklerdokumentation&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;4.3. SQL-Upgradedateien</td></tr></table></div></body></html>
\ No newline at end of file
+        den kritischen Pfaden, unter 0,15 sonst.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch04.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch04.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch04s03.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">Kapitel 4. Entwicklerdokumentation&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;4.3. Programmatische API-Aufrufe</td></tr></table></div></body></html>
\ No newline at end of file
index 7efff6e..b05a454 100644 (file)
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>4.3. SQL-Upgradedateien</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"><link rel="prev" href="ch04s02.html" title="4.2. Entwicklung unter FastCGI"><link rel="next" href="ch04s04.html" title="4.4. Translations and languages"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">4.3. SQL-Upgradedateien</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch04s02.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 4. Entwicklerdokumentation</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch04s04.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="4.3. SQL-Upgradedateien"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="db-upgrade-files"></a>4.3. SQL-Upgradedateien</h2></div></div></div><div class="sect2" title="4.3.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="db-upgrade-files.introduction"></a>4.3.1. Einführung</h3></div></div></div><p>Datenbankupgrades werden über einzelne Upgrade-Scripte gesteuert, die sich im Verzeichnis <code class="filename">sql/Pg-upgrade2</code>
-        befinden. In diesem Verzeichnis muss pro Datenbankupgrade eine Datei existieren, die neben den eigentlich auszuführenden SQL- oder
-        Perl-Befehlen einige Kontrollinformationen enthält.</p><p>Kontrollinformationen definieren Abhängigkeiten und Prioritäten, sodass Datenbankscripte zwar in einer sicheren Reihenfolge
-        ausgeführt werden (z.B. darf ein <code class="literal">ALTER TABLE</code> erst ausgeführt werden, wenn die Tabelle mit <code class="literal">CREATE
-        TABLE</code> angelegt wurde), diese Reihenfolge aber so flexibel ist, dass man keine Versionsnummern braucht.</p><p>kivitendo merkt sich dabei, welches der Upgradescripte in <code class="filename">sql/Pg-upgrade2</code> bereits durchgeführt wurde und
-        führt diese nicht erneut aus. Dazu dient die Tabelle "<code class="literal">schema_info</code>", die bei der Anmeldung automatisch angelegt
-        wird.</p></div><div class="sect2" title="4.3.2. Format der Kontrollinformationen"><div class="titlepage"><div><div><h3 class="title"><a name="db-upgrade-files.format"></a>4.3.2. Format der Kontrollinformationen</h3></div></div></div><p>Die Kontrollinformationen sollten sich am Anfang der jeweiligen
-        Upgradedatei befinden. Jede Zeile, die Kontrollinformationen enthält,
-        hat dabei das folgende Format:</p><p>Für SQL-Upgradedateien:</p><pre class="programlisting">-- @key: value</pre><p>Für Perl-Upgradedateien:</p><pre class="programlisting"># @key: value</pre><p>Leerzeichen vor "<code class="varname">value</code>" werden
-        entfernt.</p><p>Die folgenden Schlüsselworte werden verarbeitet:</p><div class="variablelist"><dl><dt><span class="term">
-                     <code class="varname">tag</code>
-                  </span></dt><dd><p>Wird zwingend benötigt. Dies ist der "Name" des Upgrades.
-              Dieser "tag" kann von anderen Kontrolldateien in ihren
-              Abhängigkeiten verwendet werden (Schlüsselwort
-              "<code class="varname">depends</code>"). Der "tag" ist auch der Name, der
-              in der Datenbank eingetragen wird.</p><p>Normalerweise sollte die Kontrolldatei genau so heißen wie
-              der "tag", nur mit der Endung ".sql" bzw. "pl".</p><p>Ein Tag darf nur aus alphanumerischen Zeichen sowie den
-              Zeichen _ - ( ) bestehen. Insbesondere sind Leerzeichen nicht
-              erlaubt und sollten stattdessen mit Unterstrichen ersetzt
-              werden.</p></dd><dt><span class="term">
-                     <code class="varname">charset</code>
-                  </span></dt><dd><p>Empfohlen. Gibt den Zeichensatz an, in dem das Script geschrieben wurde, z.B. "<code class="literal">UTF-8</code>". Aus
-              Kompatibilitätsgründen mit alten Upgrade-Scripten wird bei Abwesenheit des Tags für SQL-Upgradedateien der Zeichensatz
-              "<code class="literal">ISO-8859-15</code>" angenommen. Perl-Upgradescripte hingegen müssen immer in UTF-8 encodiert sein und sollten
-              demnach auch ein "<code class="literal">use utf8;</code>" enthalten.</p></dd><dt><span class="term">
-                     <code class="varname">description</code>
-                  </span></dt><dd><p>Benötigt. Eine Beschreibung, was in diesem Update
-              passiert. Diese wird dem Benutzer beim eigentlichen
-              Datenbankupdate angezeigt. Während der Tag in Englisch gehalten
-              sein sollte, sollte die Beschreibung auf Deutsch
-              erfolgen.</p></dd><dt><span class="term">
-                     <code class="varname">depends</code>
-                  </span></dt><dd><p>Optional. Eine mit Leerzeichen getrennte Liste von "tags",
-              von denen dieses Upgradescript abhängt. kivitendo stellt sicher,
-              dass die in dieser Liste aufgeführten Scripte bereits
-              durchgeführt wurden, bevor dieses Script ausgeführt wird.</p><p>Abhängigkeiten werden rekursiv betrachtet. Wenn also ein
-              Script "b" existiert, das von Änderungen in "a" abhängt, und
-              eine neue Kontrolldatei für "c" erstellt wird, die von
-              Änderungen in "a" und "b" abhängt, so genügt es, in "c" nur den
-              Tag "b" als Abhängigkeit zu definieren.</p><p>Es ist nicht erlaubt, sich selbst referenzierende
-              Abhängigkeiten zu definieren (z.B. "a" -&gt; "b", "b" -&gt; "c"
-              und "c" -&gt; "a").</p></dd><dt><span class="term">
-                     <code class="varname">priority</code>
-                  </span></dt><dd><p>Optional. Ein Zahlenwert, der die Reihenfolge bestimmt, in
-              der Scripte ausgeführt werden, die die gleichen
-              Abhängigkeitstiefen besitzen. Fehlt dieser Parameter, so wird
-              der Wert 1000 benutzt.</p><p>Dies ist reine Kosmetik. Für echte Reihenfolgen muss
-              "depends" benutzt werden. kivitendo sortiert die auszuführenden
-              Scripte zuerst nach der Abhängigkeitstiefe (wenn "z" von "y"
-              abhängt und "y" von "x", so hat "z" eine Abhängigkeitstiefe von
-              2, "y" von 1 und "x" von 0. "x" würde hier zuerst ausgeführt,
-              dann "y", dann "z"), dann nach der Priorität und bei gleicher
-              Priorität alphabetisch nach dem "tag".</p></dd><dt><span class="term">
-                     <code class="varname">ignore</code>
-                  </span></dt><dd><p>Optional. Falls der Wert auf 1 (true) steht, wird das
-              Skript bei der Anmeldung ignoriert und entsprechend nicht
-              ausgeführt.</p></dd></dl></div></div><div class="sect2" title="4.3.3. Format von in Perl geschriebenen Datenbankupgradescripten"><div class="titlepage"><div><div><h3 class="title"><a name="db-upgrade-files.format-perl-files"></a>4.3.3. Format von in Perl geschriebenen Datenbankupgradescripten</h3></div></div></div><p>In Perl geschriebene Datenbankscripte werden nicht einfach so ausgeführt sondern müssen sich an gewisse Konventionen
-       halten. Dafür bekommen sie aber auch einige Komfortfunktionen bereitgestellt.</p><p>Ein Upgradescript stellt dabei eine vollständige Objektklasse dar, die vom Elternobjekt
-       "<code class="literal">SL::DBUpgrade2::Base</code>" erben und eine Funktion namens "<code class="literal">run</code>" zur Verfügung stellen muss. Das
-       Script wird ausgeführt, indem eine Instanz dieser Klasse erzeugt und darauf die erwähnte "<code class="literal">run</code>" aufgerufen
-       wird.</p><p>Zu beachten ist, dass sich der Paketname der Datei aus dem Wert für "<code class="literal">@tag</code>" ableitet. Dabei werden alle
-       Zeichen, die in Paketnamen ungültig wären (gerade Bindestriche), durch Unterstriche ersetzt. Insgesamt sieht der Paketname wie folgt
-       aus: "<code class="literal">SL::DBUpgrade2::tag</code>".</p><p>Welche Komfortfunktionen zur Verfügung stehen, erfahren Sie in der Perl-Dokumentation zum oben genannten Modul; aufzurufen mit
-       "<span class="command"><strong>perldoc SL/DBUpgrade2/Base.pm</strong></span>".</p><p>Ein Mindestgerüst eines gültigen Perl-Upgradescriptes sieht wie folgt aus:</p><pre class="programlisting"># @tag: beispiel-upgrade-file42
-# @description: Ein schönes Beispielscript
-# @depends: release_3_1_0
-package SL::DBUpgrade2::beispiel_upgrade_file42;
-
-use strict;
-use utf8;
-
-use parent qw(SL::DBUpgrade2::Base);
-
-sub run {
-  my ($self) = @_;
-
-  # hier Aktionen ausführen
-
-  return 1;
-}
-
-1;
-</pre></div><div class="sect2" title="4.3.4. Hilfsscript dbupgrade2_tool.pl"><div class="titlepage"><div><div><h3 class="title"><a name="db-upgrade-files.dbupgrade-tool"></a>4.3.4. Hilfsscript dbupgrade2_tool.pl</h3></div></div></div><p>Um die Arbeit mit den Abhängigkeiten etwas zu erleichtern,
-        existiert ein Hilfsscript namens
-        "<code class="filename">scripts/dbupgrade2_tool.pl</code>". Es muss aus dem
-        kivitendo-ERP-Basisverzeichnis heraus aufgerufen werden. Dieses Tool
-        liest alle Datenbankupgradescripte aus dem Verzeichnis
-        <code class="filename">sql/Pg-upgrade2</code> aus. Es benutzt dafür die
-        gleichen Methoden wie kivitendo selber, sodass alle Fehlersituationen
-        von der Kommandozeile überprüft werden können.</p><p>Wird dem Script kein weiterer Parameter übergeben, so wird nur
-        eine Überprüfung der Felder und Abhängigkeiten vorgenommen. Man kann
-        sich aber auch Informationen auf verschiedene Art ausgeben
-        lassen:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Listenform: "<span class="command"><strong>./scripts/dbupgrade2_tool.pl
-            --list</strong></span>"</p><p>Gibt eine Liste aller Scripte aus. Die Liste ist in der
-            Reihenfolge sortiert, in der kivitendo die Scripte ausführen
-            würde. Es werden neben der Listenposition der Tag, die
-            Abhängigkeitstiefe und die Priorität ausgegeben.</p></li><li class="listitem"><p>Baumform: "<span class="command"><strong>./scripts/dbupgrade2_tool.pl
-            --tree</strong></span>"</p><p>Listet alle Tags in Baumform basierend auf den
-            Abhängigkeiten auf. Die "Wurzelknoten" sind dabei die Scripte, von
-            denen keine anderen abhängen. Die Unterknoten sind Scripte, die
-            beim übergeordneten Script als Abhängigkeit eingetragen
-            sind.</p></li><li class="listitem"><p><a name="db-upgrade-files.dbupgrade-tool.reverse-tree"></a>Umgekehrte Baumform: "<span class="command"><strong>./scripts/dbupgrade2_tool.pl
-            --rtree</strong></span>"</p><p>Listet alle Tags in Baumform basierend auf den
-            Abhängigkeiten auf. Die "Wurzelknoten" sind dabei die Scripte mit
-            der geringsten Abhängigkeitstiefe. Die Unterknoten sind Scripte,
-            die das übergeordnete Script als Abhängigkeit eingetragen
-            haben.</p></li><li class="listitem"><p>Baumform mit Postscriptausgabe:
-            "<span class="command"><strong>./scripts/dbupgrade2_tool.pl
-            --graphviz</strong></span>"</p><p>Benötigt das Tool "<span class="command"><strong>graphviz</strong></span>", um mit
-            seiner Hilfe die <a class="link" href="ch04s03.html#db-upgrade-files.dbupgrade-tool.reverse-tree">umgekehrte
-            Baumform</a> in eine Postscriptdatei namens
-            "<code class="filename">db_dependencies.ps</code>" auszugeben. Dies ist
-            vermutlich die übersichtlichste Form, weil hierbei jeder Knoten
-            nur einmal ausgegeben wird. Bei den Textmodusbaumformen hingegen
-            können Knoten und all ihre Abhängigkeiten mehrfach ausgegeben
-            werden.</p></li><li class="listitem"><p>Scripte, von denen kein anderes Script abhängt:
-            "<span class="command"><strong>./scripts/dbupgrade2_tool.pl --nodeps</strong></span>"</p><p>Listet die Tags aller Scripte auf, von denen keine anderen
-            Scripte abhängen.</p></li></ul></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch04s02.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch04.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch04s04.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">4.2. Entwicklung unter FastCGI&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;4.4. Translations and languages</td></tr></table></div></body></html>
\ No newline at end of file
+   <title>4.3. Programmatische API-Aufrufe</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"><link rel="prev" href="ch04s02.html" title="4.2. Entwicklung unter FastCGI"><link rel="next" href="ch04s04.html" title="4.4. SQL-Upgradedateien"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">4.3. Programmatische API-Aufrufe</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch04s02.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 4. Entwicklerdokumentation</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch04s04.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="4.3. Programmatische API-Aufrufe"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="dev-programmatic-api-calls"></a>4.3. Programmatische API-Aufrufe</h2></div></div></div><div class="sect2" title="4.3.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="dev-programmatic-api-calls.introduction"></a>4.3.1. Einführung</h3></div></div></div><p>
+         Es ist möglich, Funktionen in kivitendo programmatisch aus anderen Programmen aufzurufen. Dazu ist nötig, dass
+         Authentifizierungsinformationen in jedem Aufruf mitgegeben werden. Dafür gibt es zwei Methoden: die HTTP-»Basic«-Authentifizierung
+         oder die Übergabe als spziell benannte GET-Parameter. Neben den Authentifizierungsinformationen muss auch der zu verwendende Mandant
+         übergeben werden.
+        </p></div><div class="sect2" title="4.3.2. Wahl des Mandanten"><div class="titlepage"><div><div><h3 class="title"><a name="dev-programmatic-api-calls.client_selection"></a>4.3.2. Wahl des Mandanten</h3></div></div></div><p>
+         Der zu verwendende Mandant kann als Parameter <code class="varname">{AUTH}client_id</code> mit jedem Request mitgeschickt werden. Der Wert
+         muss dabei die Datenbank-ID des Mandanten sein. kivitendo prüft, ob der Account, der über die Authentifizierungsinformationen
+         übergeben wurde, Zugriff auf den angegebenen Mandanten hat.
+        </p><p>
+         Wird in einem Request kein Mandant mitgegeben, so wird derjenige Mandant genommen, wer als Standardmandant markiert wurde. Gibt es
+         keinen solchen, kommt es zu einer Fehlermeldung.
+        </p></div><div class="sect2" title="4.3.3. HTTP-»Basic«-Authentifizierung"><div class="titlepage"><div><div><h3 class="title"><a name="dev-programmatic-api-calls.http_basic_authentication"></a>4.3.3. HTTP-»Basic«-Authentifizierung</h3></div></div></div><p>
+         Für diese Methode muss jedem Request der bekannte HTTP-Header <code class="constant">Authorization</code> mitgeschickt werden (siehe <a class="ulink" href="https://tools.ietf.org/html/rfc7617" target="_top">RFC 7617</a>). Unterstützt wird ausschließlich die »Basic«-Methode. Loginname und
+         Passwort werden bei dieser Methode durch einen Doppelpunkt getrennt und Base64-encodiert im genannten HTTP-Header übertragen.
+        </p><p>
+         Diese Informationen müssen einen vorhandenen Account benennen. kivitendo prüft genau wie bei Benutzung über den Webbrowser, ob
+         dieser Account Zugriff auf den Mandanten sowie auf die angeforderte Funktion hat.
+        </p><p>
+         Da die Logininformationen im Klartext im Request stehen, sollte der Zugriff auf kivitendo ausschließlich über HTTPS verschlüsselt
+         erfolgen.
+        </p></div><div class="sect2" title="4.3.4. Authentifizierung mit Parametern"><div class="titlepage"><div><div><h3 class="title"><a name="dev-programmatic-api-calls.authentication_via_parameters"></a>4.3.4. Authentifizierung mit Parametern</h3></div></div></div><p>
+         Für diese Methode müssen jedem Request zwei Parameter mitgegeben werden: <code class="varname">{AUTH}login</code> und
+         <code class="varname">{AUTH}password</code>. Diese Informationen müssen einen vorhandenen Account benennen. kivitendo prüft genau wie bei
+         Benutzung über den Webbrowser, ob dieser Account Zugriff auf den Mandanten sowie auf die angeforderte Funktion hat.
+        </p><p>
+         Da die Logininformationen im Klartext im Request stehen, sollte der Zugriff auf kivitendo ausschließlich über HTTPS verschlüsselt
+         erfolgen.
+        </p><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left">Anmerkung</th></tr><tr><td align="left" valign="top"><p>
+          Die Verwendung dieser Methode ist veraltet. Statt dessen sollte die oben erwähnte HTTP-»Basic«-Authentifizierung verwendet werden.
+         </p></td></tr></table></div></div><div class="sect2" title="4.3.5. Beispiele"><div class="titlepage"><div><div><h3 class="title"><a name="dev-programmatic-api-calls.examples"></a>4.3.5. Beispiele</h3></div></div></div><p>
+         Das folgende Beispiel nutzt das Kommandozeilenprogramm »curl« und ruft die Funktion auf, die eine vorhandene Telefonnummer in den
+         Ansprechpersonen sucht und dazu Informationen zurückliefert. Dabei wird die HTTP-»Basic«-Authentifizierung genutzt.
+        </p><pre class="programlisting">$ curl --silent --user 'jdoe:SecretPassword!' \
+  'https://…/controller.pl?action=PhoneNumber/look_up&amp;number=053147110815'</pre></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch04s02.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch04.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch04s04.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">4.2. Entwicklung unter FastCGI&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;4.4. SQL-Upgradedateien</td></tr></table></div></body></html>
\ No newline at end of file
index d16085f..f53c439 100644 (file)
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>4.4. Translations and languages</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"><link rel="prev" href="ch04s03.html" title="4.3. SQL-Upgradedateien"><link rel="next" href="ch04s05.html" title="4.5. Die kivitendo-Test-Suite"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">4.4. Translations and languages</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch04s03.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 4. Entwicklerdokumentation</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch04s05.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="4.4. Translations and languages"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="translations-languages"></a>4.4. Translations and languages</h2></div></div></div><div class="sect2" title="4.4.1. Introduction"><div class="titlepage"><div><div><h3 class="title"><a name="translations-languages.introduction"></a>4.4.1. Introduction</h3></div></div></div><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left">Anmerkung</th></tr><tr><td align="left" valign="top"><p>Dieser Abschnitt ist in Englisch geschrieben, um
-          internationalen Übersetzern die Arbeit zu erleichtern.</p></td></tr></table></div><p>This section describes how localization packages in kivitendo
-        are built. Currently the only language fully supported is German, and
-        since most of the internal messages are held in English the English
-        version is usable too.</p><p>A stub version of French is included but not functunal at this
-        point.</p></div><div class="sect2" title="4.4.2. Character set"><div class="titlepage"><div><div><h3 class="title"><a name="translations-languages.character-set"></a>4.4.2. Character set</h3></div></div></div><p>All files included in a language pack must use UTF-8 as their encoding.</p></div><div class="sect2" title="4.4.3. File structure"><div class="titlepage"><div><div><h3 class="title"><a name="translations-languages.file-structure"></a>4.4.3. File structure</h3></div></div></div><p>The structure of locales in kivitendo is:</p><pre class="programlisting">kivitendo/locale/&lt;langcode&gt;/</pre><p>where &lt;langcode&gt; stands for an abbreviation of the
-        language package. The builtin packages use two letter <a class="ulink" href="http://en.wikipedia.org/wiki/ISO_639-1" target="_top">ISO 639-1</a> codes,
-        but the actual name is not relevant for the program and can easily be
-        extended to <a class="ulink" href="http://en.wikipedia.org/wiki/IETF_language_tag" target="_top">IETF language
-        tags</a> (i.e. "en_GB"). In fact the original language packages
-        from SQL Ledger are named in this way.</p><p>In such a language directory the following files are
-        recognized:</p><div class="variablelist"><dl><dt><span class="term">LANGUAGE</span></dt><dd><p>This file is mandatory.</p><p>The <code class="filename">LANGUAGE</code> file contains the self
-              descripted name of the language. It should contain a native
-              representation first, and in parenthesis an english translation
-              after that. Example:</p><pre class="programlisting">Deutsch (German)</pre></dd><dt><span class="term">all</span></dt><dd><p>This file is mandatory.</p><p>The central translation file. It is essentially an inline
-              Perl script autogenerated by <span class="command"><strong>locales.pl</strong></span>. To
-              generate it, generate the directory and the two files mentioned
-              above, and execute the following command:</p><pre class="programlisting">scripts/locales.pl &lt;langcode&gt;</pre><p>Otherwise you can simply copy one of the other languages.
-              You will be told how many are missing like this:</p><pre class="programlisting">$ scripts/locales.pl en
-English - 0.6% - 2015/2028 missing</pre><p>A file named "<code class="filename">missing</code>" will be
-              generated and can be edited. You can also edit the
-              "<code class="filename">all</code>" file directly. Edit everything you
-              like to fit the target language and execute
-              <span class="command"><strong>locales.pl</strong></span> again. See how the missing words
-              get fewer.</p></dd><dt><span class="term">Num2text</span></dt><dd><p>Legacy code from SQL Ledger. It provides a means for
-              numbers to be converted into natural language, like
-              <code class="literal">1523 =&gt; one thousand five hundred twenty
-              three</code>. If you want to provide it, it must be inlinable
-              Perl code which provides a <code class="function">num2text</code> sub. If
-              an <code class="function">init</code> sub exists it will be executed
-              first.</p><p>Only used in the check and receipt printing module.</p></dd><dt><span class="term">special_chars</span></dt><dd><p>kivitendo comes with a lot of interfaces to different
-              formats, some of which are rather picky with their accepted
-              charset. The <code class="filename">special_chars</code> file contains a
-              listing of chars not suited for different file format and
-              provides substitutions. It is written in "Simple Ini" style,
-              containing a block for every file format.</p><p>First entry should be the order of substitution for
-              entries as a whitespace separated list. All entries are
-              interpolated, so <code class="literal">\n</code>, <code class="literal">\x20</code>
-              and <code class="literal">\\</code> all work.</p><p>After that every entry is a special char that should be
-              translated when writing text into such a file.</p><p>Example:</p><pre class="programlisting">[Template/XML]
-order=&amp; &lt; &gt; \n
-&amp;=&amp;amp;
-&lt;=&amp;lt;
-&gt;=&amp;gt;
-\n=&lt;br&gt;</pre><p>Note the importance of the order in this example.
-              Substituting &lt; and &gt; befor &amp; would lead to $gt; become
-              &amp;amp;gt;</p><p>For a list of valid formats, see the German
-              <code class="filename">special_chars</code> entry. As of this writing the
-              following are recognized:</p><pre class="programlisting">HTML
-URL@HTML
-Template/HTML
-Template/XML
-Template/LaTeX
-Template/OpenDocument
-filenames</pre><p>The last of which is very machine dependant. Remember that
-              a lot of characters are forbidden by some filesystems, for
-              exmaple MS Windows doesn't like ':' in its files where Linux
-              doesn't mind that. If you want the files created with your
-              language pack to be portable, find all chars that could cause
-              trouble.</p></dd><dt><span class="term">missing</span></dt><dd><p>This file is not a part of the language package
-              itself.</p><p>This is a file generated by
-              <span class="command"><strong>scripts/locales.pl</strong></span> while processing your
-              locales. It's only to have the missing entries singled out and
-              does not belong to a language package.</p></dd><dt><span class="term">lost</span></dt><dd><p>This file is not a part of the language package
-              itself.</p><p>Another file generated by
-              <span class="command"><strong>scripts/locales.pl</strong></span>. If for any reason a
-              translation does not appear anymore and can be deleted, it gets
-              moved here. The last 50 or so entries deleted are saved here in
-              case you made a typo, so that you don't have to translate
-              everything again. If a tranlsation is missing, the lost file is
-              checked first. If you maintain a language package, you might
-              want to keep this safe somewhere.</p></dd></dl></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch04s03.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch04.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch04s05.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">4.3. SQL-Upgradedateien&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;4.5. Die kivitendo-Test-Suite</td></tr></table></div></body></html>
\ No newline at end of file
+   <title>4.4. SQL-Upgradedateien</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"><link rel="prev" href="ch04s03.html" title="4.3. Programmatische API-Aufrufe"><link rel="next" href="ch04s05.html" title="4.5. Translations and languages"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">4.4. SQL-Upgradedateien</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch04s03.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 4. Entwicklerdokumentation</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch04s05.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="4.4. SQL-Upgradedateien"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="db-upgrade-files"></a>4.4. SQL-Upgradedateien</h2></div></div></div><div class="sect2" title="4.4.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="db-upgrade-files.introduction"></a>4.4.1. Einführung</h3></div></div></div><p>Datenbankupgrades werden über einzelne Upgrade-Scripte
+        gesteuert, die sich im Verzeichnis
+        <code class="filename">sql/Pg-upgrade2</code> befinden. In diesem Verzeichnis
+        muss pro Datenbankupgrade eine Datei existieren, die neben den
+        eigentlich auszuführenden SQL- oder Perl-Befehlen einige
+        Kontrollinformationen enthält.</p><p>Kontrollinformationen definieren Abhängigkeiten und Prioritäten,
+        sodass Datenbankscripte zwar in einer sicheren Reihenfolge ausgeführt
+        werden (z.B. darf ein <code class="literal">ALTER TABLE</code> erst ausgeführt
+        werden, wenn die Tabelle mit <code class="literal">CREATE TABLE</code> angelegt
+        wurde), diese Reihenfolge aber so flexibel ist, dass man keine
+        Versionsnummern braucht.</p><p>kivitendo merkt sich dabei, welches der Upgradescripte in
+        <code class="filename">sql/Pg-upgrade2</code> bereits durchgeführt wurde und
+        führt diese nicht erneut aus. Dazu dient die Tabelle
+        "<code class="literal">schema_info</code>", die bei der Anmeldung automatisch
+        angelegt wird.</p></div><div class="sect2" title="4.4.2. Format der Kontrollinformationen"><div class="titlepage"><div><div><h3 class="title"><a name="db-upgrade-files.format"></a>4.4.2. Format der Kontrollinformationen</h3></div></div></div><p>Die Kontrollinformationen sollten sich am Anfang der jeweiligen
+        Upgradedatei befinden. Jede Zeile, die Kontrollinformationen enthält,
+        hat dabei das folgende Format:</p><p>Für SQL-Upgradedateien:</p><pre class="programlisting">-- @key: value</pre><p>Für Perl-Upgradedateien:</p><pre class="programlisting"># @key: value</pre><p>Leerzeichen vor "<code class="varname">value</code>" werden
+        entfernt.</p><p>Die folgenden Schlüsselworte werden verarbeitet:</p><div class="variablelist"><dl><dt><span class="term">
+                     <code class="varname">tag</code>
+                  </span></dt><dd><p>Wird zwingend benötigt. Dies ist der "Name" des Upgrades.
+              Dieser "tag" kann von anderen Kontrolldateien in ihren
+              Abhängigkeiten verwendet werden (Schlüsselwort
+              "<code class="varname">depends</code>"). Der "tag" ist auch der Name, der
+              in der Datenbank eingetragen wird.</p><p>Normalerweise sollte die Kontrolldatei genau so heißen wie
+              der "tag", nur mit der Endung ".sql" bzw. "pl".</p><p>Ein Tag darf nur aus alphanumerischen Zeichen sowie den
+              Zeichen _ - ( ) bestehen. Insbesondere sind Leerzeichen nicht
+              erlaubt und sollten stattdessen mit Unterstrichen ersetzt
+              werden.</p></dd><dt><span class="term">
+                     <code class="varname">charset</code>
+                  </span></dt><dd><p>Empfohlen. Gibt den Zeichensatz an, in dem das Script
+              geschrieben wurde, z.B. "<code class="literal">UTF-8</code>". Aus
+              Kompatibilitätsgründen mit alten Upgrade-Scripten wird bei
+              Abwesenheit des Tags für SQL-Upgradedateien der Zeichensatz
+              "<code class="literal">ISO-8859-15</code>" angenommen. Perl-Upgradescripte
+              hingegen müssen immer in UTF-8 encodiert sein und sollten
+              demnach auch ein "<code class="literal">use utf8;</code>"
+              enthalten.</p></dd><dt><span class="term">
+                     <code class="varname">description</code>
+                  </span></dt><dd><p>Benötigt. Eine Beschreibung, was in diesem Update
+              passiert. Diese wird dem Benutzer beim eigentlichen
+              Datenbankupdate angezeigt. Während der Tag in Englisch gehalten
+              sein sollte, sollte die Beschreibung auf Deutsch
+              erfolgen.</p></dd><dt><span class="term">
+                     <code class="varname">depends</code>
+                  </span></dt><dd><p>Optional. Eine mit Leerzeichen getrennte Liste von "tags",
+              von denen dieses Upgradescript abhängt. kivitendo stellt sicher,
+              dass die in dieser Liste aufgeführten Scripte bereits
+              durchgeführt wurden, bevor dieses Script ausgeführt wird.</p><p>Abhängigkeiten werden rekursiv betrachtet. Wenn also ein
+              Script "b" existiert, das von Änderungen in "a" abhängt, und
+              eine neue Kontrolldatei für "c" erstellt wird, die von
+              Änderungen in "a" und "b" abhängt, so genügt es, in "c" nur den
+              Tag "b" als Abhängigkeit zu definieren.</p><p>Es ist nicht erlaubt, sich selbst referenzierende
+              Abhängigkeiten zu definieren (z.B. "a" -&gt; "b", "b" -&gt; "c"
+              und "c" -&gt; "a").</p></dd><dt><span class="term">
+                     <code class="varname">priority</code>
+                  </span></dt><dd><p>Optional. Ein Zahlenwert, der die Reihenfolge bestimmt, in
+              der Scripte ausgeführt werden, die die gleichen
+              Abhängigkeitstiefen besitzen. Fehlt dieser Parameter, so wird
+              der Wert 1000 benutzt.</p><p>Dies ist reine Kosmetik. Für echte Reihenfolgen muss
+              "depends" benutzt werden. kivitendo sortiert die auszuführenden
+              Scripte zuerst nach der Abhängigkeitstiefe (wenn "z" von "y"
+              abhängt und "y" von "x", so hat "z" eine Abhängigkeitstiefe von
+              2, "y" von 1 und "x" von 0. "x" würde hier zuerst ausgeführt,
+              dann "y", dann "z"), dann nach der Priorität und bei gleicher
+              Priorität alphabetisch nach dem "tag".</p></dd><dt><span class="term">
+                     <code class="varname">ignore</code>
+                  </span></dt><dd><p>Optional. Falls der Wert auf 1 (true) steht, wird das
+              Skript bei der Anmeldung ignoriert und entsprechend nicht
+              ausgeführt.</p></dd></dl></div></div><div class="sect2" title="4.4.3. Format von in Perl geschriebenen Datenbankupgradescripten"><div class="titlepage"><div><div><h3 class="title"><a name="db-upgrade-files.format-perl-files"></a>4.4.3. Format von in Perl geschriebenen
+        Datenbankupgradescripten</h3></div></div></div><p>In Perl geschriebene Datenbankscripte werden nicht einfach so
+        ausgeführt sondern müssen sich an gewisse Konventionen halten. Dafür
+        bekommen sie aber auch einige Komfortfunktionen bereitgestellt.</p><p>Ein Upgradescript stellt dabei eine vollständige Objektklasse
+        dar, die vom Elternobjekt "<code class="literal">SL::DBUpgrade2::Base</code>"
+        erben und eine Funktion namens "<code class="literal">run</code>" zur Verfügung
+        stellen muss. Das Script wird ausgeführt, indem eine Instanz dieser
+        Klasse erzeugt und darauf die erwähnte "<code class="literal">run</code>"
+        aufgerufen wird.</p><p>Zu beachten ist, dass sich der Paketname der Datei aus dem Wert
+        für "<code class="literal">@tag</code>" ableitet. Dabei werden alle Zeichen, die
+        in Paketnamen ungültig wären (gerade Bindestriche), durch Unterstriche
+        ersetzt. Insgesamt sieht der Paketname wie folgt aus:
+        "<code class="literal">SL::DBUpgrade2::tag</code>".</p><p>Welche Komfortfunktionen zur Verfügung stehen, erfahren Sie in
+        der Perl-Dokumentation zum oben genannten Modul; aufzurufen mit
+        "<span class="command"><strong>perldoc SL/DBUpgrade2/Base.pm</strong></span>".</p><p>Ein Mindestgerüst eines gültigen Perl-Upgradescriptes sieht wie
+        folgt aus:</p><pre class="programlisting"># @tag: beispiel-upgrade-file42
+# @description: Ein schönes Beispielscript
+# @depends: release_3_1_0
+package SL::DBUpgrade2::beispiel_upgrade_file42;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+sub run {
+  my ($self) = @_;
+
+  # hier Aktionen ausführen
+
+  return 1;
+}
+
+1;
+</pre></div><div class="sect2" title="4.4.4. Hilfsscript dbupgrade2_tool.pl"><div class="titlepage"><div><div><h3 class="title"><a name="db-upgrade-files.dbupgrade-tool"></a>4.4.4. Hilfsscript dbupgrade2_tool.pl</h3></div></div></div><p>Um die Arbeit mit den Abhängigkeiten etwas zu erleichtern,
+        existiert ein Hilfsscript namens
+        "<code class="filename">scripts/dbupgrade2_tool.pl</code>". Es muss aus dem
+        kivitendo-ERP-Basisverzeichnis heraus aufgerufen werden. Dieses Tool
+        liest alle Datenbankupgradescripte aus dem Verzeichnis
+        <code class="filename">sql/Pg-upgrade2</code> aus. Es benutzt dafür die
+        gleichen Methoden wie kivitendo selber, sodass alle Fehlersituationen
+        von der Kommandozeile überprüft werden können.</p><p>Wird dem Script kein weiterer Parameter übergeben, so wird nur
+        eine Überprüfung der Felder und Abhängigkeiten vorgenommen. Man kann
+        sich aber auch Informationen auf verschiedene Art ausgeben
+        lassen:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Listenform: "<span class="command"><strong>./scripts/dbupgrade2_tool.pl
+            --list</strong></span>"</p><p>Gibt eine Liste aller Scripte aus. Die Liste ist in der
+            Reihenfolge sortiert, in der kivitendo die Scripte ausführen
+            würde. Es werden neben der Listenposition der Tag, die
+            Abhängigkeitstiefe und die Priorität ausgegeben.</p></li><li class="listitem"><p>Baumform: "<span class="command"><strong>./scripts/dbupgrade2_tool.pl
+            --tree</strong></span>"</p><p>Listet alle Tags in Baumform basierend auf den
+            Abhängigkeiten auf. Die "Wurzelknoten" sind dabei die Scripte, von
+            denen keine anderen abhängen. Die Unterknoten sind Scripte, die
+            beim übergeordneten Script als Abhängigkeit eingetragen
+            sind.</p></li><li class="listitem"><p><a name="db-upgrade-files.dbupgrade-tool.reverse-tree"></a>Umgekehrte Baumform: "<span class="command"><strong>./scripts/dbupgrade2_tool.pl
+            --rtree</strong></span>"</p><p>Listet alle Tags in Baumform basierend auf den
+            Abhängigkeiten auf. Die "Wurzelknoten" sind dabei die Scripte mit
+            der geringsten Abhängigkeitstiefe. Die Unterknoten sind Scripte,
+            die das übergeordnete Script als Abhängigkeit eingetragen
+            haben.</p></li><li class="listitem"><p>Baumform mit Postscriptausgabe:
+            "<span class="command"><strong>./scripts/dbupgrade2_tool.pl
+            --graphviz</strong></span>"</p><p>Benötigt das Tool "<span class="command"><strong>graphviz</strong></span>", um mit
+            seiner Hilfe die <a class="link" href="ch04s04.html#db-upgrade-files.dbupgrade-tool.reverse-tree">umgekehrte
+            Baumform</a> in eine Postscriptdatei namens
+            "<code class="filename">db_dependencies.ps</code>" auszugeben. Dies ist
+            vermutlich die übersichtlichste Form, weil hierbei jeder Knoten
+            nur einmal ausgegeben wird. Bei den Textmodusbaumformen hingegen
+            können Knoten und all ihre Abhängigkeiten mehrfach ausgegeben
+            werden.</p></li><li class="listitem"><p>Scripte, von denen kein anderes Script abhängt:
+            "<span class="command"><strong>./scripts/dbupgrade2_tool.pl --nodeps</strong></span>"</p><p>Listet die Tags aller Scripte auf, von denen keine anderen
+            Scripte abhängen.</p></li></ul></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch04s03.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch04.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch04s05.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">4.3. Programmatische API-Aufrufe&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;4.5. Translations and languages</td></tr></table></div></body></html>
\ No newline at end of file
index 320e189..cc2d124 100644 (file)
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>4.5. Die kivitendo-Test-Suite</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"><link rel="prev" href="ch04s04.html" title="4.4. Translations and languages"><link rel="next" href="ch04s06.html" title="4.6. Stil-Richtlinien"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">4.5. Die kivitendo-Test-Suite</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch04s04.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 4. Entwicklerdokumentation</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch04s06.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="4.5. Die kivitendo-Test-Suite"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="devel.testsuite"></a>4.5. Die kivitendo-Test-Suite</h2></div></div></div><div class="sect2" title="4.5.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="devel.testsuite.intro"></a>4.5.1. Einführung</h3></div></div></div><p>kivitendo enthält eine Suite für automatisierte Tests. Sie basiert auf dem Standard-Perl-Modul <code class="literal">Test::More</code>.</p><p>Die grundlegenden Fakten sind:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Alle Tests liegen im Unterverzeichnis <code class="filename">t/</code>.</p></li><li class="listitem"><p>Ein Script (bzw. ein Test) in <code class="filename">t/</code> enthält einen oder mehrere Testfälle.</p></li><li class="listitem"><p>Alle Dateinamen von Tests enden auf <code class="literal">.t</code>. Es sind selbstständig ausführbare Perl-Scripte.</p></li><li class="listitem"><p>Die Test-Suite besteht aus der Gesamtheit aller Tests, sprich aller Scripte in <code class="filename">t/</code>, deren
-          Dateiname auf <code class="literal">.t</code> endet.</p></li></ul></div></div><div class="sect2" title="4.5.2. Voraussetzungen"><div class="titlepage"><div><div><h3 class="title"><a name="devel.testsuite.prerequisites"></a>4.5.2. Voraussetzungen</h3></div></div></div><p>Für die Ausführung werden neben den für kivitendo eh schon benötigten Module noch weitere Perl-Module benötigt. Diese sind:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
-                     <code class="literal">Test::Deep</code> (Debian-Paketname: <code class="literal">libtest-deep-perl</code>; Fedora Core:
-          <code class="literal">perl-Test-Deep</code>; openSUSE: <code class="literal">perl-Test-Deep</code>)</p></li><li class="listitem"><p>
-                     <code class="literal">Test::Exception</code> (Debian-Paketname: <code class="literal">libtest-exception-perl</code>; Fedora Core:
-          <code class="literal">perl-Test-Exception</code>; openSUSE: <code class="literal">perl-Test-Exception</code>)</p></li><li class="listitem"><p>
-                     <code class="literal">Test::Output</code> (Debian-Paketname: <code class="literal">libtest-output-perl</code>; Fedora Core:
-          <code class="literal">perl-Test-Output</code>; openSUSE: <code class="literal">perl-Test-Output</code>)</p></li><li class="listitem"><p>
-                     <code class="literal">Test::Harness</code> 3.0.0 oder höher. Dieses Modul ist ab Perl 5.10.1 Bestandteil der
-          Perl-Distribution und kann für frühere Versionen aus dem <a class="ulink" href="http://www.cpan.org" target="_top">CPAN</a> bezogen
-          werden.</p></li><li class="listitem"><p>
-                     <code class="literal">LWP::Simple</code> aus dem Paket <code class="literal">libwww-perl</code> (Debian-Panetname:
-          <code class="literal">libwww-perl</code>; Fedora Core: <code class="literal">perl-libwww-perl</code>; openSUSE:
-          <code class="literal">perl-libwww-perl</code>)</p></li><li class="listitem"><p>
-                     <code class="literal">URI::Find</code> (Debian-Panetname: <code class="literal">liburi-find-perl</code>; Fedora Core:
-          <code class="literal">perl-URI-Find</code>; openSUSE: <code class="literal">perl-URI-Find</code>)</p></li></ul></div><p>Weitere Voraussetzung ist, dass die Testsuite ihre eigene Datenbank anlegen kann, um Produktivdaten nicht zu gefährden. Dazu
-        müssen in der Konfigurationsdatei im Abschnit <code class="literal">testing/database</code> Datenbankverbindungsparameter angegeben
-        werden. Der hier angegebene Benutzer muss weiterhin das Recht haben, Datenbanken anzulegen und zu löschen.</p></div><div class="sect2" title="4.5.3. Existierende Tests ausführen"><div class="titlepage"><div><div><h3 class="title"><a name="devel.testsuite.execution"></a>4.5.3. 
-          Existierende Tests ausführen
-        </h3></div></div></div><p>Es gibt mehrere Möglichkeiten zum Ausführen der Tests: entweder, man lässt alle Tests auf einmal ausführen, oder man führt
-        gezielt einzelne Scripte aus. Für beide Fälle gibt es das Helferscript <code class="filename">t/test.pl</code>.</p><p>Will man die komplette Test-Suite ausführen, so muss man einfach nur <code class="filename">t/test.pl</code> ohne weitere Parameter aus
-        dem kivitendo-Basisverzeichnis heraus ausführen.</p><p>Um einzelne Test-Scripte auszuführen, übergibt man deren Namen an <code class="filename">t/test.pl</code>. Beispielsweise:</p><pre class="programlisting">t/test.pl t/form/format_amount.t t/background_job/known_jobs.t</pre></div><div class="sect2" title="4.5.4. Bedeutung der verschiedenen Test-Scripte"><div class="titlepage"><div><div><h3 class="title"><a name="devel.testsuite.meaning_of_scripts"></a>4.5.4. 
-          Bedeutung der verschiedenen Test-Scripte
-        </h3></div></div></div><p>Die Test-Suite umfasst Tests sowohl für Funktionen als auch für Programmierstil. Einige besonders zu erwähnende, weil auch
-        während der Entwicklung nützliche Tests sind:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
-                     <code class="filename">t/001compile.t</code> -- compiliert alle Quelldateien und bricht bei Fehlern sofort ab</p></li><li class="listitem"><p>
-                     <code class="filename">t/002goodperl.t</code> -- überprüft alle Perl-Dateien auf Anwesenheit von '<code class="literal">use strict</code>'-Anweisungen</p></li><li class="listitem"><p>
-                     <code class="filename">t/003safesys.t</code> -- überprüft Aufrufe von <code class="function">system()</code> und <code class="function">exec()</code> auf Gültigkeit</p></li><li class="listitem"><p>
-                     <code class="filename">t/005no_tabs.t</code> -- überprüft, ob Dateien Tab-Zeichen enthalten</p></li><li class="listitem"><p>
-                     <code class="filename">t/006spelling.t</code> -- sucht nach häufigen Rechtschreibfehlern</p></li><li class="listitem"><p>
-                     <code class="filename">t/011pod.t</code> -- überprüft die Syntax von Dokumentation im POD-Format auf Gültigkeit</p></li></ul></div><p>Weitere Test-Scripte überprüfen primär die Funktionsweise einzelner Funktionen und Module.</p></div><div class="sect2" title="4.5.5. Neue Test-Scripte erstellen"><div class="titlepage"><div><div><h3 class="title"><a name="devel.testsuite.create_new"></a>4.5.5. 
-          Neue Test-Scripte erstellen
-        </h3></div></div></div><p>Es wird sehr gern gesehen, wenn neue Funktionalität auch gleich mit einem Test-Script abgesichert wird. Auch bestehende
-        Funktion darf und soll ausdrücklich nachträglich mit Test-Scripten abgesichert werden.</p><div class="sect3" title="4.5.5.1. Ideen für neue Test-Scripte, die keine konkreten Funktionen testen"><div class="titlepage"><div><div><h4 class="title"><a name="devel.testsuite.ideas_for_non_function_tests"></a>4.5.5.1. 
-            Ideen für neue Test-Scripte, die keine konkreten Funktionen testen
-          </h4></div></div></div><p> Ideen, die abgesehen von Funktionen noch nicht umgesetzt wurden:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Überprüfung auf fehlende symbolische Links</p></li><li class="listitem"><p>Suche nach Nicht-ASCII-Zeichen in Perl-Code-Dateien (mit gewissen Einschränkungen wie das Erlauben von deutschen Umlauten)</p></li><li class="listitem"><p>Test auf DOS-Zeilenenden (\r\n anstelle von nur \n)</p></li><li class="listitem"><p>Überprüfung auf Leerzeichen am Ende von Zeilen</p></li><li class="listitem"><p>Test, ob alle zu übersetzenden Strings in <code class="filename">locale/de/all</code> vorhanden sind</p></li><li class="listitem"><p>Test, ob alle Webseiten-Templates in <code class="filename">templates/webpages</code> mit vom Perl-Modul <code class="literal">Template</code> compiliert werden können</p></li></ul></div></div><div class="sect3" title="4.5.5.2. Konvention für Verzeichnis- und Dateinamen"><div class="titlepage"><div><div><h4 class="title"><a name="devel.testsuite.directory_and_test_names"></a>4.5.5.2. 
-            Konvention für Verzeichnis- und Dateinamen
-          </h4></div></div></div><p>Es gibt momentan eine wenige Richtlinien, wie Test-Scripte zu benennen sind. Bitte die folgenden Punkte als Richtlinie betrachten und ihnen soweit es geht folgen:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Die Dateiendung muss <code class="filename">.t</code> lauten.</p></li><li class="listitem"><p>Namen sind englisch, komplett klein geschrieben und einzelne Wörter mit Unterstrichten getrennt (beispielsweise
-            <code class="filename">bad_function_params.t</code>).</p></li><li class="listitem"><p>Unterverzeichnisse sollten grob nach dem Themenbereich benannt sein, mit dem sich die Scripte darin befassen
-            (beispielsweise <code class="filename">background_jobs</code> für Tests rund um Hintergrund-Jobs).</p></li><li class="listitem"><p>Test-Scripte sollten einen überschaubaren Bereich von Funktionalität testen, der logisch zusammenhängend ist
-            (z.B. nur Tests für eine einzelne Funktion in einem Modul). Lieber mehrere Test-Scripte schreiben.</p></li></ul></div></div><div class="sect3" title="4.5.5.3. Minimales Skelett für eigene Scripte"><div class="titlepage"><div><div><h4 class="title"><a name="devel.testsuite.minimal_example"></a>4.5.5.3. 
-            Minimales Skelett für eigene Scripte
-          </h4></div></div></div><p>Der folgenden Programmcode enthält das kleinstmögliche Testscript und kann als Ausgangspunkt für eigene Tests verwendet werden:</p><pre class="programlisting">use Test::More tests =&gt; 0;
+   <title>4.5. Translations and languages</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"><link rel="prev" href="ch04s04.html" title="4.4. SQL-Upgradedateien"><link rel="next" href="ch04s06.html" title="4.6. Die kivitendo-Test-Suite"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">4.5. Translations and languages</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch04s04.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 4. Entwicklerdokumentation</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch04s06.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="4.5. Translations and languages"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="translations-languages"></a>4.5. Translations and languages</h2></div></div></div><div class="sect2" title="4.5.1. Introduction"><div class="titlepage"><div><div><h3 class="title"><a name="translations-languages.introduction"></a>4.5.1. Introduction</h3></div></div></div><div class="note" title="Anmerkung" style="margin-left: 0.5in; margin-right: 0.5in;"><table border="0" summary="Note"><tr><td rowspan="2" align="center" valign="top" width="25"><img alt="[Anmerkung]" src="system/docbook-xsl/images/note.png"></td><th align="left">Anmerkung</th></tr><tr><td align="left" valign="top"><p>Dieser Abschnitt ist in Englisch geschrieben, um
+          internationalen Übersetzern die Arbeit zu erleichtern.</p></td></tr></table></div><p>This section describes how localization packages in kivitendo
+        are built. Currently the only language fully supported is German, and
+        since most of the internal messages are held in English the English
+        version is usable too.</p></div><div class="sect2" title="4.5.2. Character set"><div class="titlepage"><div><div><h3 class="title"><a name="translations-languages.character-set"></a>4.5.2. Character set</h3></div></div></div><p>All files included in a language pack must use UTF-8 as their
+        encoding.</p></div><div class="sect2" title="4.5.3. File structure"><div class="titlepage"><div><div><h3 class="title"><a name="translations-languages.file-structure"></a>4.5.3. File structure</h3></div></div></div><p>The structure of locales in kivitendo is:</p><pre class="programlisting">kivitendo/locale/&lt;langcode&gt;/</pre><p>where &lt;langcode&gt; stands for an abbreviation of the
+        language package. The builtin packages use two letter <a class="ulink" href="http://en.wikipedia.org/wiki/ISO_639-1" target="_top">ISO 639-1</a> codes,
+        but the actual name is not relevant for the program and can easily be
+        extended to <a class="ulink" href="http://en.wikipedia.org/wiki/IETF_language_tag" target="_top">IETF language
+        tags</a> (i.e. "en_GB"). In fact the original language packages
+        from SQL Ledger are named in this way.</p><p>In such a language directory the following files are
+        recognized:</p><div class="variablelist"><dl><dt><span class="term">LANGUAGE</span></dt><dd><p>This file is mandatory.</p><p>The <code class="filename">LANGUAGE</code> file contains the self
+              descripted name of the language. It should contain a native
+              representation first, and in parenthesis an english translation
+              after that. Example:</p><pre class="programlisting">Deutsch (German)</pre></dd><dt><span class="term">all</span></dt><dd><p>This file is mandatory.</p><p>The central translation file. It is essentially an inline
+              Perl script autogenerated by <span class="command"><strong>locales.pl</strong></span>. To
+              generate it, generate the directory and the two files mentioned
+              above, and execute the following command:</p><pre class="programlisting">scripts/locales.pl &lt;langcode&gt;</pre><p>Otherwise you can simply copy one of the other languages.
+              You will be told how many are missing like this:</p><pre class="programlisting">$ scripts/locales.pl en
+English - 0.6% - 2015/2028 missing</pre><p>A file named "<code class="filename">missing</code>" will be
+              generated and can be edited. You can also edit the
+              "<code class="filename">all</code>" file directly. Edit everything you
+              like to fit the target language and execute
+              <span class="command"><strong>locales.pl</strong></span> again. See how the missing words
+              get fewer.</p></dd><dt><span class="term">Num2text</span></dt><dd><p>Legacy code from SQL Ledger. It provides a means for
+              numbers to be converted into natural language, like
+              <code class="literal">1523 =&gt; one thousand five hundred twenty
+              three</code>. If you want to provide it, it must be inlinable
+              Perl code which provides a <code class="function">num2text</code> sub. If
+              an <code class="function">init</code> sub exists it will be executed
+              first.</p><p>Only used in the check and receipt printing module.</p></dd><dt><span class="term">special_chars</span></dt><dd><p>kivitendo comes with a lot of interfaces to different
+              formats, some of which are rather picky with their accepted
+              charset. The <code class="filename">special_chars</code> file contains a
+              listing of chars not suited for different file format and
+              provides substitutions. It is written in "Simple Ini" style,
+              containing a block for every file format.</p><p>First entry should be the order of substitution for
+              entries as a whitespace separated list. All entries are
+              interpolated, so <code class="literal">\n</code>, <code class="literal">\x20</code>
+              and <code class="literal">\\</code> all work.</p><p>After that every entry is a special char that should be
+              translated when writing text into such a file.</p><p>Example:</p><pre class="programlisting">[Template/XML]
+order=&amp; &lt; &gt; \n
+&amp;=&amp;amp;
+&lt;=&amp;lt;
+&gt;=&amp;gt;
+\n=&lt;br&gt;</pre><p>Note the importance of the order in this example.
+              Substituting &lt; and &gt; befor &amp; would lead to $gt; become
+              &amp;amp;gt;</p><p>For a list of valid formats, see the German
+              <code class="filename">special_chars</code> entry. As of this writing the
+              following are recognized:</p><pre class="programlisting">HTML
+URL@HTML
+Template/HTML
+Template/XML
+Template/LaTeX
+Template/OpenDocument
+filenames</pre><p>The last of which is very machine dependent. Remember that
+              a lot of characters are forbidden by some filesystems, for
+              example MS Windows doesn't like ':' in its files where Linux
+              doesn't mind that. If you want the files created with your
+              language pack to be portable, find all chars that could cause
+              trouble.</p></dd><dt><span class="term">missing</span></dt><dd><p>This file is not a part of the language package
+              itself.</p><p>This is a file generated by
+              <span class="command"><strong>scripts/locales.pl</strong></span> while processing your
+              locales. It's only to have the missing entries singled out and
+              does not belong to a language package.</p></dd><dt><span class="term">lost</span></dt><dd><p>This file is not a part of the language package
+              itself.</p><p>Another file generated by
+              <span class="command"><strong>scripts/locales.pl</strong></span>. If for any reason a
+              translation does not appear anymore and can be deleted, it gets
+              moved here. The last 50 or so entries deleted are saved here in
+              case you made a typo, so that you don't have to translate
+              everything again. If a tranlsation is missing, the lost file is
+              checked first. If you maintain a language package, you might
+              want to keep this safe somewhere.</p></dd><dt><span class="term">more/all</span></dt><dd><p>This subdir and file is not a part of the language package
+              itself.</p><p>If the directory more exists and contains a file called
+              all it will be parsed in addition to the mandatory all (see
+              above). The file is useful if you want to change some
+              translations for the current installation without conflicting
+              further upgrades. The file is not autogenerated and has the same
+              format as the all, but needs another key (more_texts). See the
+              german translation for an example or copy the following code:
+              </p><pre class="programlisting">
+#!/usr/bin/perl
+# -*- coding: utf-8; -*-
+# vim: fenc=utf-8
 
-use lib 't';
+use utf8;
 
-use Support::TestSetup;
+# These are additional texts for custom translations.
+# The format is the same as for the normal file all, only
+# with another key (more_texts instead of texts).
+# The file has the form of 'english text'  =&gt; 'foreign text',
 
-Support::TestSetup::login();</pre><p>Wird eine vollständig initialisierte kivitendo-Umgebung benötigt (Stichwort: alle globalen Variablen wie
-          <code class="varname">$::auth</code>, <code class="varname">$::form</code> oder <code class="varname">$::lxdebug</code>), so muss in der Konfigurationsdatei
-          <code class="filename">config/kivitendo.conf</code> im Abschnitt <code class="literal">testing.login</code> ein gültiger Login-Name eingetragen
-          sein. Dieser wird für die Datenbankverbindung benötigt.</p><p>Wir keine vollständig initialisierte Umgebung benötigt, so kann die letzte Zeile <code class="code">Support::TestSetup::login();</code>
-          weggelassen werden, was die Ausführungszeit des Scripts leicht verringert.</p></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch04s04.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch04.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch04s06.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">4.4. Translations and languages&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;4.6. Stil-Richtlinien</td></tr></table></div></body></html>
\ No newline at end of file
+$self-&gt;{more_texts} = {
+
+  'Ship via'                    =&gt; 'Terms of delivery',
+  'Shipping Point'              =&gt; 'Delivery time',
+}
+              </pre><p>
+                     </p></dd></dl></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch04s04.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch04.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch04s06.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">4.4. SQL-Upgradedateien&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;4.6. Die kivitendo-Test-Suite</td></tr></table></div></body></html>
\ No newline at end of file
index b03ad6f..6eddda5 100644 (file)
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>4.6. Stil-Richtlinien</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"><link rel="prev" href="ch04s05.html" title="4.5. Die kivitendo-Test-Suite"><link rel="next" href="ch04s07.html" title="4.7. Dokumentation erstellen"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">4.6. Stil-Richtlinien</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch04s05.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 4. Entwicklerdokumentation</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch04s07.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="4.6. Stil-Richtlinien"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="devel.style-guide"></a>4.6. Stil-Richtlinien</h2></div></div></div><p>Die folgenden Regeln haben das Ziel, den Code möglichst gut les-
-      und wartbar zu machen. Dazu gehört zum Einen, dass der Code einheitlich
-      eingerückt ist, aber auch, dass Mehrdeutigkeit so weit es geht vermieden
-      wird (Stichworte "Klammern" oder "Hash-Keys").</p><p>Diese Regeln sind keine Schikane sondern erleichtern allen das
-      Leben!</p><p>Jeder, der einen Patch schickt, sollte seinen Code vorher
-      überprüfen. Einige der Regeln lassen sich automatisch überprüfen, andere
-      nicht.</p><div class="orderedlist"><ol class="orderedlist" type="1"><li class="listitem"><p>Es werden keine echten Tabs sondern Leerzeichen
-          verwendet.</p></li><li class="listitem"><p>Die Einrückung beträgt zwei Leerzeichen. Beispiel:</p><pre class="programlisting">foreach my $row (@data) {
-  if ($flag) {
-    # do something with $row
-  }
+   <title>4.6. Die kivitendo-Test-Suite</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"><link rel="prev" href="ch04s05.html" title="4.5. Translations and languages"><link rel="next" href="ch04s07.html" title="4.7. Stil-Richtlinien"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">4.6. Die kivitendo-Test-Suite</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch04s05.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 4. Entwicklerdokumentation</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch04s07.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="4.6. Die kivitendo-Test-Suite"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="devel.testsuite"></a>4.6. Die kivitendo-Test-Suite</h2></div></div></div><div class="sect2" title="4.6.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="devel.testsuite.intro"></a>4.6.1. Einführung</h3></div></div></div><p>kivitendo enthält eine Suite für automatisierte Tests. Sie
+        basiert auf dem Standard-Perl-Modul
+        <code class="literal">Test::More</code>.</p><p>Die grundlegenden Fakten sind:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Alle Tests liegen im Unterverzeichnis
+            <code class="filename">t/</code>.</p></li><li class="listitem"><p>Ein Script (bzw. ein Test) in <code class="filename">t/</code>
+            enthält einen oder mehrere Testfälle.</p></li><li class="listitem"><p>Alle Dateinamen von Tests enden auf <code class="literal">.t</code>.
+            Es sind selbstständig ausführbare Perl-Scripte.</p></li><li class="listitem"><p>Die Test-Suite besteht aus der Gesamtheit aller Tests,
+            sprich aller Scripte in <code class="filename">t/</code>, deren Dateiname
+            auf <code class="literal">.t</code> endet.</p></li></ul></div></div><div class="sect2" title="4.6.2. Voraussetzungen"><div class="titlepage"><div><div><h3 class="title"><a name="devel.testsuite.prerequisites"></a>4.6.2. Voraussetzungen</h3></div></div></div><p>Für die Ausführung werden neben den für kivitendo eh schon
+        benötigten Module noch weitere Perl-Module benötigt. Diese
+        sind:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
+                     <code class="literal">Test::Deep</code> (Debian-Paketname:
+            <code class="literal">libtest-deep-perl</code>; Fedora:
+            <code class="literal">perl-Test-Deep</code>; openSUSE:
+            <code class="literal">perl-Test-Deep</code>)</p></li><li class="listitem"><p>
+                     <code class="literal">Test::Exception</code> (Debian-Paketname:
+            <code class="literal">libtest-exception-perl</code>; Fedora:
+            <code class="literal">perl-Test-Exception</code>; openSUSE:
+            <code class="literal">perl-Test-Exception</code>)</p></li><li class="listitem"><p>
+                     <code class="literal">Test::Output</code> (Debian-Paketname:
+            <code class="literal">libtest-output-perl</code>; Fedora:
+            <code class="literal">perl-Test-Output</code>; openSUSE:
+            <code class="literal">perl-Test-Output</code>)</p></li><li class="listitem"><p>
+                     <code class="literal">Test::Harness</code> 3.0.0 oder höher. Dieses
+            Modul ist ab Perl 5.10.1 Bestandteil der Perl-Distribution und
+            kann für frühere Versionen aus dem <a class="ulink" href="http://www.cpan.org" target="_top">CPAN</a> bezogen werden.</p></li><li class="listitem"><p>
+                     <code class="literal">LWP::Simple</code> aus dem Paket
+            <code class="literal">libwww-perl</code> (Debian-Panetname:
+            <code class="literal">libwww-perl</code>; Fedora:
+            <code class="literal">perl-libwww-perl</code>; openSUSE:
+            <code class="literal">perl-libwww-perl</code>)</p></li><li class="listitem"><p>
+                     <code class="literal">URI::Find</code> (Debian-Panetname:
+            <code class="literal">liburi-find-perl</code>; Fedora:
+            <code class="literal">perl-URI-Find</code>; openSUSE:
+            <code class="literal">perl-URI-Find</code>)</p></li><li class="listitem"><p>
+                     <code class="literal">Sys::CPU</code> (Debian-Panetname:
+            <code class="literal">libsys-cpu-perl</code>; Fedora und openSUSE: nicht
+            vorhanden)</p></li><li class="listitem"><p>
+                     <code class="literal">Thread::Pool::Simple</code> (Debian-Panetname:
+            <code class="literal">libthread-pool-simple-perl</code>; Fedora und
+            openSUSE: nicht vorhanden)</p></li></ul></div><p>Weitere Voraussetzung ist, dass die Testsuite ihre eigene
+        Datenbank anlegen kann, um Produktivdaten nicht zu gefährden. Dazu
+        müssen in der Konfigurationsdatei im Abschnit
+        <code class="literal">testing/database</code> Datenbankverbindungsparameter
+        angegeben werden. Der hier angegebene Benutzer muss weiterhin das
+        Recht haben, Datenbanken anzulegen und zu löschen.</p><p>Der so angegebene Benutzer muss nicht zwingend über
+        Super-User-Rechte verfügen. Allerdings gibt es einige
+        Datenbank-Upgrades, die genau diese Rechte benötigen. Für den Fall
+        kann man in diesem Konfigurationsabschnitt einen weiteren
+        Benutzeraccount angeben, der dann über Super-User-Rechte verfügt, und
+        mit dem die betroffenen Upgrades durchgeführt werden. In der
+        Beispiel-Konfigurationsdatei finden Sie die benötigten
+        Parameter.</p></div><div class="sect2" title="4.6.3. Existierende Tests ausführen"><div class="titlepage"><div><div><h3 class="title"><a name="devel.testsuite.execution"></a>4.6.3. Existierende Tests ausführen</h3></div></div></div><p>Es gibt mehrere Möglichkeiten zum Ausführen der Tests: entweder,
+        man lässt alle Tests auf einmal ausführen, oder man führt gezielt
+        einzelne Scripte aus. Für beide Fälle gibt es das Helferscript
+        <code class="filename">t/test.pl</code>.</p><p>Will man die komplette Test-Suite ausführen, so muss man einfach
+        nur <code class="filename">t/test.pl</code> ohne weitere Parameter aus dem
+        kivitendo-Basisverzeichnis heraus ausführen.</p><p>Um einzelne Test-Scripte auszuführen, übergibt man deren Namen
+        an <code class="filename">t/test.pl</code>. Beispielsweise:</p><pre class="programlisting">t/test.pl t/form/format_amount.t t/background_job/known_jobs.t</pre></div><div class="sect2" title="4.6.4. Bedeutung der verschiedenen Test-Scripte"><div class="titlepage"><div><div><h3 class="title"><a name="devel.testsuite.meaning_of_scripts"></a>4.6.4. Bedeutung der verschiedenen Test-Scripte</h3></div></div></div><p>Die Test-Suite umfasst Tests sowohl für Funktionen als auch für
+        Programmierstil. Einige besonders zu erwähnende, weil auch während der
+        Entwicklung nützliche Tests sind:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
+                     <code class="filename">t/001compile.t</code> -- compiliert alle
+            Quelldateien und bricht bei Fehlern sofort ab</p></li><li class="listitem"><p>
+                     <code class="filename">t/002goodperl.t</code> -- überprüft alle
+            Perl-Dateien auf Anwesenheit von '<code class="literal">use
+            strict</code>'-Anweisungen</p></li><li class="listitem"><p>
+                     <code class="filename">t/003safesys.t</code> -- überprüft Aufrufe von
+            <code class="function">system()</code> und <code class="function">exec()</code> auf
+            Gültigkeit</p></li><li class="listitem"><p>
+                     <code class="filename">t/005no_tabs.t</code> -- überprüft, ob Dateien
+            Tab-Zeichen enthalten</p></li><li class="listitem"><p>
+                     <code class="filename">t/006spelling.t</code> -- sucht nach häufigen
+            Rechtschreibfehlern</p></li><li class="listitem"><p>
+                     <code class="filename">t/011pod.t</code> -- überprüft die Syntax von
+            Dokumentation im POD-Format auf Gültigkeit</p></li></ul></div><p>Weitere Test-Scripte überprüfen primär die Funktionsweise
+        einzelner Funktionen und Module.</p></div><div class="sect2" title="4.6.5. Neue Test-Scripte erstellen"><div class="titlepage"><div><div><h3 class="title"><a name="devel.testsuite.create_new"></a>4.6.5. Neue Test-Scripte erstellen</h3></div></div></div><p>Es wird sehr gern gesehen, wenn neue Funktionalität auch gleich
+        mit einem Test-Script abgesichert wird. Auch bestehende Funktion darf
+        und soll ausdrücklich nachträglich mit Test-Scripten abgesichert
+        werden.</p><div class="sect3" title="4.6.5.1. Ideen für neue Test-Scripte, die keine konkreten Funktionen testen"><div class="titlepage"><div><div><h4 class="title"><a name="devel.testsuite.ideas_for_non_function_tests"></a>4.6.5.1. Ideen für neue Test-Scripte, die keine konkreten Funktionen
+          testen</h4></div></div></div><p>Ideen, die abgesehen von Funktionen noch nicht umgesetzt
+          wurden:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Überprüfung auf fehlende symbolische Links</p></li><li class="listitem"><p>Suche nach Nicht-ASCII-Zeichen in Perl-Code-Dateien (mit
+              gewissen Einschränkungen wie das Erlauben von deutschen
+              Umlauten)</p></li><li class="listitem"><p>Test auf DOS-Zeilenenden (\r\n anstelle von nur \n)</p></li><li class="listitem"><p>Überprüfung auf Leerzeichen am Ende von Zeilen</p></li><li class="listitem"><p>Test, ob alle zu übersetzenden Strings in
+              <code class="filename">locale/de/all</code> vorhanden sind</p></li><li class="listitem"><p>Test, ob alle Webseiten-Templates in
+              <code class="filename">templates/webpages</code> mit vom Perl-Modul
+              <code class="literal">Template</code> compiliert werden können</p></li></ul></div></div><div class="sect3" title="4.6.5.2. Konvention für Verzeichnis- und Dateinamen"><div class="titlepage"><div><div><h4 class="title"><a name="devel.testsuite.directory_and_test_names"></a>4.6.5.2. Konvention für Verzeichnis- und Dateinamen</h4></div></div></div><p>Es gibt momentan eine wenige Richtlinien, wie Test-Scripte zu
+          benennen sind. Bitte die folgenden Punkte als Richtlinie betrachten
+          und ihnen soweit es geht folgen:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>Die Dateiendung muss <code class="filename">.t</code>
+              lauten.</p></li><li class="listitem"><p>Namen sind englisch, komplett klein geschrieben und
+              einzelne Wörter mit Unterstrichten getrennt (beispielsweise
+              <code class="filename">bad_function_params.t</code>).</p></li><li class="listitem"><p>Unterverzeichnisse sollten grob nach dem Themenbereich
+              benannt sein, mit dem sich die Scripte darin befassen
+              (beispielsweise <code class="filename">background_jobs</code> für Tests
+              rund um Hintergrund-Jobs).</p></li><li class="listitem"><p>Test-Scripte sollten einen überschaubaren Bereich von
+              Funktionalität testen, der logisch zusammenhängend ist (z.B. nur
+              Tests für eine einzelne Funktion in einem Modul). Lieber mehrere
+              Test-Scripte schreiben.</p></li></ul></div></div><div class="sect3" title="4.6.5.3. Minimales Skelett für eigene Scripte"><div class="titlepage"><div><div><h4 class="title"><a name="devel.testsuite.minimal_example"></a>4.6.5.3. Minimales Skelett für eigene Scripte</h4></div></div></div><p>Der folgenden Programmcode enthält das kleinstmögliche
+          Testscript und kann als Ausgangspunkt für eigene Tests verwendet
+          werden:</p><pre class="programlisting">use Test::More tests =&gt; 0;
 
-  if ($use_modules) {
-    $row-&gt;{modules} = MODULE-&gt;retrieve(
-      id   =&gt; $row-&gt;{id},
-      date =&gt; $use_now ? localtime() : $row-&gt;{time},
-    );
-  }
+use lib 't';
 
-  $report-&gt;add($row);
-}</pre></li><li class="listitem"><p>Öffnende geschweifte Klammern befinden sich auf der gleichen
-          Zeile wie der letzte Befehl. Beispiele:</p><pre class="programlisting">sub debug {
-  ...
-}</pre><p>oder</p><pre class="programlisting">if ($form-&gt;{item_rows} &gt; 0) {
-  ...
-}</pre></li><li class="listitem"><p>Schließende geschweifte Klammern sind so weit eingerückt wie
-          der Befehl / die öffnende schließende Klammer, die den Block
-          gestartet hat, und nicht auf der Ebene des Inhalts. Die gleichen
-          Beispiele wie bei 3. gelten.</p></li><li class="listitem"><p>Die Wörter "<code class="function">else</code>",
-          "<code class="function">elsif</code>", "<code class="function">while</code>" befinden
-          sich auf der gleichen Zeile wie schließende geschweifte Klammern.
-          Beispiele:</p><pre class="programlisting">if ($form-&gt;{sum} &gt; 1000) {
-  ...
-} elsif ($form-&gt;{sum} &gt; 0) {
-  ...
-} else {
-  ...
-}
+use Support::TestSetup;
 
-do {
-  ...
-} until ($a &gt; 0);</pre></li><li class="listitem"><p>Parameter von Funktionsaufrufen müssen mit runden Klammern
-          versehen werden. Davon nicht betroffen sind interne Perl-Funktionen,
-          und grep-ähnliche Operatoren. Beispiel:</p><pre class="programlisting">$main::lxdebug-&gt;message("Could not find file.");
-%options = map { $_ =&gt; 1 } grep { !/^#/ } @config_file;</pre></li><li class="listitem"><p>Verschiedene Klammern, Ihre Ausdrücke und Leerzeichen:</p><p>Generell gilt: Hashkeys und Arrayindices sollten nicht durch
-          Leerzeichen abgesetzt werden. Logische Klammerungen ebensowenig,
-          Blöcke schon. Beispiel:</p><pre class="programlisting">if (($form-&gt;{debug} == 1) &amp;&amp; ($form-&gt;{sum} - 100 &lt; 0)) {
-  ...
-}
-
-$array[$i + 1]             = 4;
-$form-&gt;{sum}              += $form-&gt;{"row_$i"};
-$form-&gt;{ $form-&gt;{index} } += 1;
-
-map { $form-&gt;{sum} += $form-&gt;{"row_$_"} } 1..$rowcount;</pre></li><li class="listitem"><p>Mehrzeilige Befehle</p><div class="orderedlist"><ol class="orderedlist" type="a"><li class="listitem"><p>Werden die Parameter eines Funktionsaufrufes auf mehrere
-              Zeilen aufgeteilt, so sollten diese bis zu der Spalte eingerückt
-              werden, in der die ersten Funktionsparameter in der ersten Zeile
-              stehen. Beispiel:</p><pre class="programlisting">$sth = $dbh-&gt;prepare("SELECT * FROM some_table WHERE col = ?",
-                    $form-&gt;{some_col_value});</pre></li><li class="listitem"><p>Ein Spezialfall ist der ternäre Oprator "?:", der am
-              besten in einer übersichtlichen Tabellenstruktur organisiert
-              wird. Beispiel:</p><pre class="programlisting">my $rowcount = $form-&gt;{"row_$i"} ? $i
-             : $form-&gt;{oldcount} ? $form-&gt;{oldcount} + 1
-             :                     $form-&gt;{rowcount} - $form-&gt;{rowbase};</pre></li></ol></div></li><li class="listitem"><p>Kommentare</p><div class="orderedlist"><ol class="orderedlist" type="a"><li class="listitem"><p>Kommentare, die alleine in einer Zeile stehen, sollten
-              soweit wie der Code eingerückt sein.</p></li><li class="listitem"><p>Seitliche hängende Kommentare sollten einheitlich
-              formatiert werden.</p></li><li class="listitem"><p>Sämtliche Kommentare und Sonstiges im Quellcode ist bitte
-              auf Englisch zu verfassen. So wie ich keine Lust habe,
-              französischen Quelltext zu lesen, sollte auch der kivitendo
-              Quelltext für nicht-Deutschsprachige lesbar sein.
-              Beispiel:</p><pre class="programlisting">my $found = 0;
-while (1) {
-  last if $found;
-
-  # complicated check
-  $found = 1 if //
-}
-
-$i  = 0        # initialize $i
-$n  = $i;      # save $i
-$i *= $const;  # do something crazy
-$i  = $n;      # recover $i</pre></li></ol></div></li><li class="listitem"><p>Hashkeys sollten nur in Anführungszeichen stehen, wenn die
-          Interpolation gewünscht ist. Beispiel:</p><pre class="programlisting">$form-&gt;{sum}      = 0;
-$form-&gt;{"row_$i"} = $form-&gt;{"row_$i"} - 5;
-$some_hash{42}    = 54;</pre></li><li class="listitem"><p>Die maximale Zeilenlänge ist nicht beschränkt. Zeilenlängen
-          unterhalb von 79 Zeichen helfen unter bestimmten Bedingungen, aber
-          wenn die Lesbarkeit unter kurzen Zeilen leidet (wie zum Biespiel in
-          grossen Tabellen), dann ist Lesbarkeit vorzuziehen.</p><p>Als Beispiel sei die Funktion
-          <code class="function">print_options</code> aus
-          <code class="filename">bin/mozilla/io.pl</code> angeführt.</p></li><li class="listitem"><p>Trailing Whitespace, d.h. Leerzeichen am Ende von Zeilen sind
-          unerwünscht. Sie führen zu unnötigen Whitespaceänderungen, die diffs
-          verfälschen.</p><p>Emacs und vim haben beide recht einfache Methoden zur
-          Entfernung von trailing whitespace. Emacs kennt das Kommande
-          <span class="command"><strong>nuke-trailing-whitespace</strong></span>, vim macht das gleiche
-          manuell über <code class="literal">:%s/\s\+$//e</code> Mit <code class="literal">:au
-          BufWritePre * :%s/\s\+$//e</code> wird das an Speichern
-          gebunden.</p></li><li class="listitem"><p>Es wird kein <span class="command"><strong>perltidy</strong></span> verwendet.</p><p>In der Vergangenheit wurde versucht,
-          <span class="command"><strong>perltidy</strong></span> zu verwenden, um einen einheitlichen
-          Stil zu erlangen. Es hat sich aber gezeigt, dass
-          <span class="command"><strong>perltidy</strong></span>s sehr eigenwilliges Verhalten, was
-          Zeilenumbrüche angeht, oftmals gut formatierten Code zerstört. Für
-          den Interessierten sind hier die
-          <span class="command"><strong>perltidy</strong></span>-Optionen, die grob den beschriebenen
-          Richtlinien entsprechen:</p><pre class="programlisting">-syn -i=2 -nt -pt=2 -sbt=2 -ci=2 -ibc -hsc -noll -nsts -nsfs -asc -dsm
--aws -bbc -bbs -bbb -mbl=1 -nsob -ce -nbl -nsbl -cti=0 -bbt=0 -bar -l=79
--lp -vt=1 -vtc=1</pre></li><li class="listitem"><p>
-                  <code class="varname">STDERR</code> ist tabu. Unkonditionale
-          Debugmeldungen auch.</p><p>kivitendo bietet mit dem Modul <code class="classname">LXDebug</code>
-          einen brauchbaren Trace-/Debug-Mechanismus. Es gibt also keinen
-          Grund, nach <code class="varname">STDERR</code> zu schreiben.</p><p>Die <code class="classname">LXDebug</code>-Methode
-          "<code class="function">message</code>" nimmt als ersten Paramter außerdem
-          eine Flagmaske, für die die Meldung angezeigt wird, wobei "0" immer
-          angezeigt wird. Solche Meldungen sollten nicht eingecheckt werden
-          und werden in den meisten Fällen auch vom Repository
-          zurückgewiesen.</p></li><li class="listitem"><p>Alle neuen Module müssen use strict verwenden.</p><p>
-                  <code class="varname">$form</code>, <code class="varname">$auth</code>,
-          <code class="varname">$locale</code>, <code class="varname">$lxdebug</code> und
-          <code class="varname">%myconfig</code> werden derzeit aus dem main package
-          importiert (siehe <a class="xref" href="ch04.html#devel.globals" title="4.1. Globale Variablen">Globale Variablen</a>. Alle anderen
-          Konstrukte sollten lexikalisch lokal gehalten werden.</p></li></ol></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch04s05.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch04.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch04s07.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">4.5. Die kivitendo-Test-Suite&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;4.7. Dokumentation erstellen</td></tr></table></div></body></html>
\ No newline at end of file
+Support::TestSetup::login();</pre><p>Wird eine vollständig initialisierte kivitendo-Umgebung
+          benötigt (Stichwort: alle globalen Variablen wie
+          <code class="varname">$::auth</code>, <code class="varname">$::form</code> oder
+          <code class="varname">$::lxdebug</code>), so muss in der Konfigurationsdatei
+          <code class="filename">config/kivitendo.conf</code> im Abschnitt
+          <code class="literal">testing.login</code> ein gültiger Login-Name eingetragen
+          sein. Dieser wird für die Datenbankverbindung benötigt.</p><p>Wir keine vollständig initialisierte Umgebung benötigt, so
+          kann die letzte Zeile </p><pre class="programlisting">Support::TestSetup::login();</pre><p>
+          weggelassen werden, was die Ausführungszeit des Scripts leicht
+          verringert.</p></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch04s05.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch04.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch04s07.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">4.5. Translations and languages&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;4.7. Stil-Richtlinien</td></tr></table></div></body></html>
\ No newline at end of file
index 8890372..c1e0dd8 100644 (file)
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>4.7. Dokumentation erstellen</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"><link rel="prev" href="ch04s06.html" title="4.6. Stil-Richtlinien"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">4.7. Dokumentation erstellen</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch04s06.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 4. Entwicklerdokumentation</th><td width="20%" align="right">&nbsp;</td></tr></table><hr></div><div class="sect1" title="4.7. Dokumentation erstellen"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="devel.build-doc"></a>4.7. Dokumentation erstellen</h2></div></div></div><div class="sect2" title="4.7.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="devel.build-doc.introduction"></a>4.7.1. Einführung</h3></div></div></div><p>Diese Dokumentation ist in <span class="productname">DocBook</span>™
-        XML geschrieben. Zum Bearbeiten reicht grundsätzlich ein Text-Editor.
-        Mehr Komfort bekommt man, wenn man einen dedizierten XML-fähigen
-        Editor nutzt, der spezielle Unterstützung für
-        <span class="productname">DocBook</span>™ mitbringt. Wir empfehlen dafür den
-        <a class="ulink" href="http://www.xmlmind.com/xmleditor/" target="_top">XMLmind XML
-        Editor</a>, der bei nicht kommerzieller Nutzung kostenlos
-        ist.</p></div><div class="sect2" title="4.7.2. Benötigte Software"><div class="titlepage"><div><div><h3 class="title"><a name="devel.build-doc.required-software"></a>4.7.2. Benötigte Software</h3></div></div></div><p>Bei <span class="productname">DocBook</span>™ ist Prinzip, dass
-        ausschließlich die XML-Quelldatei bearbeitet wird. Aus dieser werden
-        dann mit entsprechenden Stylesheets andere Formate wie PDF oder HTML
-        erzeugt. Bei kivitendo übernimmt diese Aufgabe das Shell-Script
-        <span class="command"><strong>scripts/build_doc.sh</strong></span>.</p><p>Das Script benötigt zur Konvertierung verschiedene
-        Softwarekomponenten, die im normalen kivitendo-Betrieb nicht benötigt
-        werden:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
-                     <a class="ulink" href="http://www.oracle.com/technetwork/java/index.html" target="_top">Java</a>
-            in einer halbwegs aktuellen Version</p></li><li class="listitem"><p>Das Java-Build-System <a class="ulink" href="http://ant.apache.org/" target="_top">Apache Ant</a>
-                  </p></li><li class="listitem"><p>Das Dokumentations-System Dobudish für
-            <span class="productname">DocBook</span>™ 4.5, eine Zusammenstellung
-            diverser Stylesheets und Grafiken zur Konvertierung von
-            <span class="productname">DocBook</span>™ XML in andere Formate. Das
-            Paket, das benötigt wird, ist zum Zeitpunkt der
-            Dokumentationserstellung
-            <code class="filename">dobudish-nojre-1.1.4.zip</code>, aus auf <a class="ulink" href="http://code.google.com/p/dobudish/downloads/list" target="_top">code.google.com</a>
-            bereitsteht.</p></li></ul></div><p>Apache Ant sowie ein dazu passendes Java Runtime Environment
-        sind auf allen gängigen Plattformen verfügbar. Beispiel für
-        Debian/Ubuntu:</p><pre class="programlisting">apt-get install ant openjdk-7-jre</pre><p>Nach dem Download von Dobudish muss Dobudish im Unterverzeichnis
-        <code class="filename">doc/build</code> entpackt werden. Beispiel unter der
-        Annahme, das <span class="productname">Dobudish</span>™ in
-        <code class="filename">$HOME/Downloads</code> heruntergeladen wurde:</p><pre class="programlisting">cd doc/build
-unzip $HOME/Downloads/dobudish-nojre-1.1.4.zip</pre></div><div class="sect2" title="4.7.3. PDFs und HTML-Seiten erstellen"><div class="titlepage"><div><div><h3 class="title"><a name="devel.build-doc.build"></a>4.7.3. PDFs und HTML-Seiten erstellen</h3></div></div></div><p>Die eigentliche Konvertierung erfolgt nach Installation der
-        benötigten Software mit einem einfachen Aufruf direkt aus dem
-        kivitendo-Installationsverzeichnis heraus:</p><pre class="programlisting">./scripts/build_doc.sh</pre></div><div class="sect2" title="4.7.4. Einchecken in das Git-Repository"><div class="titlepage"><div><div><h3 class="title"><a name="devel.build-doc.repository"></a>4.7.4. Einchecken in das Git-Repository</h3></div></div></div><p>Sowohl die XML-Datei als auch die erzeugten PDF- und
-        HTML-Dateien sind Bestandteil des Git-Repositories. Daraus folgt, dass
-        nach Änderungen am XML die PDF- und HTML-Dokumente ebenfalls gebaut
-        und alles zusammen in einem Commit eingecheckt werden sollten.</p><p>Die "<code class="filename">dobudish</code>"-Verzeichnisse bzw.
-        symbolischen Links gehören hingegen nicht in das Repository.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch04s06.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch04.html">Nach oben</a></td><td width="40%" align="right">&nbsp;</td></tr><tr><td width="40%" align="left" valign="top">4.6. Stil-Richtlinien&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;</td></tr></table></div></body></html>
\ No newline at end of file
+   <title>4.7. Stil-Richtlinien</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"><link rel="prev" href="ch04s06.html" title="4.6. Die kivitendo-Test-Suite"><link rel="next" href="ch04s08.html" title="4.8. Dokumentation erstellen"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">4.7. Stil-Richtlinien</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch04s06.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 4. Entwicklerdokumentation</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch04s08.html">Weiter</a></td></tr></table><hr></div><div class="sect1" title="4.7. Stil-Richtlinien"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="devel.style-guide"></a>4.7. Stil-Richtlinien</h2></div></div></div><p>Die folgenden Regeln haben das Ziel, den Code möglichst gut les-
+      und wartbar zu machen. Dazu gehört zum Einen, dass der Code einheitlich
+      eingerückt ist, aber auch, dass Mehrdeutigkeit so weit es geht vermieden
+      wird (Stichworte "Klammern" oder "Hash-Keys").</p><p>Diese Regeln sind keine Schikane sondern erleichtern allen das
+      Leben!</p><p>Jeder, der einen Patch schickt, sollte seinen Code vorher
+      überprüfen. Einige der Regeln lassen sich automatisch überprüfen, andere
+      nicht.</p><div class="orderedlist"><ol class="orderedlist" type="1"><li class="listitem"><p>Es werden keine echten Tabs sondern Leerzeichen
+          verwendet.</p></li><li class="listitem"><p>Die Einrückung beträgt zwei Leerzeichen. Beispiel:</p><pre class="programlisting">foreach my $row (@data) {
+  if ($flag) {
+    # do something with $row
+  }
+
+  if ($use_modules) {
+    $row-&gt;{modules} = MODULE-&gt;retrieve(
+      id   =&gt; $row-&gt;{id},
+      date =&gt; $use_now ? localtime() : $row-&gt;{time},
+    );
+  }
+
+  $report-&gt;add($row);
+}</pre></li><li class="listitem"><p>Öffnende geschweifte Klammern befinden sich auf der gleichen
+          Zeile wie der letzte Befehl. Beispiele:</p><pre class="programlisting">sub debug {
+  ...
+}</pre><p>oder</p><pre class="programlisting">if ($form-&gt;{item_rows} &gt; 0) {
+  ...
+}</pre></li><li class="listitem"><p>Schließende geschweifte Klammern sind so weit eingerückt wie
+          der Befehl / die öffnende schließende Klammer, die den Block
+          gestartet hat, und nicht auf der Ebene des Inhalts. Die gleichen
+          Beispiele wie bei 3. gelten.</p></li><li class="listitem"><p>Die Wörter "<code class="function">else</code>",
+          "<code class="function">elsif</code>", "<code class="function">while</code>" befinden
+          sich auf der gleichen Zeile wie schließende geschweifte Klammern.
+          Beispiele:</p><pre class="programlisting">if ($form-&gt;{sum} &gt; 1000) {
+  ...
+} elsif ($form-&gt;{sum} &gt; 0) {
+  ...
+} else {
+  ...
+}
+
+do {
+  ...
+} until ($a &gt; 0);</pre></li><li class="listitem"><p>Parameter von Funktionsaufrufen müssen mit runden Klammern
+          versehen werden. Davon nicht betroffen sind interne Perl-Funktionen,
+          und grep-ähnliche Operatoren. Beispiel:</p><pre class="programlisting">$main::lxdebug-&gt;message("Could not find file.");
+%options = map { $_ =&gt; 1 } grep { !/^#/ } @config_file;</pre></li><li class="listitem"><p>Verschiedene Klammern, Ihre Ausdrücke und Leerzeichen:</p><p>Generell gilt: Hashkeys und Arrayindices sollten nicht durch
+          Leerzeichen abgesetzt werden. Logische Klammerungen ebensowenig,
+          Blöcke schon. Beispiel:</p><pre class="programlisting">if (($form-&gt;{debug} == 1) &amp;&amp; ($form-&gt;{sum} - 100 &lt; 0)) {
+  ...
+}
+
+$array[$i + 1]             = 4;
+$form-&gt;{sum}              += $form-&gt;{"row_$i"};
+$form-&gt;{ $form-&gt;{index} } += 1;
+
+map { $form-&gt;{sum} += $form-&gt;{"row_$_"} } 1..$rowcount;</pre></li><li class="listitem"><p>Mehrzeilige Befehle</p><div class="orderedlist"><ol class="orderedlist" type="a"><li class="listitem"><p>Werden die Parameter eines Funktionsaufrufes auf mehrere
+              Zeilen aufgeteilt, so sollten diese bis zu der Spalte eingerückt
+              werden, in der die ersten Funktionsparameter in der ersten Zeile
+              stehen. Beispiel:</p><pre class="programlisting">$sth = $dbh-&gt;prepare("SELECT * FROM some_table WHERE col = ?",
+                    $form-&gt;{some_col_value});</pre></li><li class="listitem"><p>Ein Spezialfall ist der ternäre Operator "?:", der am
+              besten in einer übersichtlichen Tabellenstruktur organisiert
+              wird. Beispiel:</p><pre class="programlisting">my $rowcount = $form-&gt;{"row_$i"} ? $i
+             : $form-&gt;{oldcount} ? $form-&gt;{oldcount} + 1
+             :                     $form-&gt;{rowcount} - $form-&gt;{rowbase};</pre></li></ol></div></li><li class="listitem"><p>Kommentare</p><div class="orderedlist"><ol class="orderedlist" type="a"><li class="listitem"><p>Kommentare, die alleine in einer Zeile stehen, sollten
+              soweit wie der Code eingerückt sein.</p></li><li class="listitem"><p>Seitliche hängende Kommentare sollten einheitlich
+              formatiert werden.</p></li><li class="listitem"><p>Sämtliche Kommentare und Sonstiges im Quellcode ist bitte
+              auf Englisch zu verfassen. So wie ich keine Lust habe,
+              französischen Quelltext zu lesen, sollte auch der kivitendo
+              Quelltext für nicht-Deutschsprachige lesbar sein.
+              Beispiel:</p><pre class="programlisting">my $found = 0;
+while (1) {
+  last if $found;
+
+  # complicated check
+  $found = 1 if //
+}
+
+$i  = 0        # initialize $i
+$n  = $i;      # save $i
+$i *= $const;  # do something crazy
+$i  = $n;      # recover $i</pre></li></ol></div></li><li class="listitem"><p>Hashkeys sollten nur in Anführungszeichen stehen, wenn die
+          Interpolation gewünscht ist. Beispiel:</p><pre class="programlisting">$form-&gt;{sum}      = 0;
+$form-&gt;{"row_$i"} = $form-&gt;{"row_$i"} - 5;
+$some_hash{42}    = 54;</pre></li><li class="listitem"><p>Die maximale Zeilenlänge ist nicht beschränkt. Zeilenlängen
+          unterhalb von 79 Zeichen helfen unter bestimmten Bedingungen, aber
+          wenn die Lesbarkeit unter kurzen Zeilen leidet (wie zum Biespiel in
+          grossen Tabellen), dann ist Lesbarkeit vorzuziehen.</p><p>Als Beispiel sei die Funktion
+          <code class="function">print_options</code> aus
+          <code class="filename">bin/mozilla/io.pl</code> angeführt.</p></li><li class="listitem"><p>Trailing Whitespace, d.h. Leerzeichen am Ende von Zeilen sind
+          unerwünscht. Sie führen zu unnötigen Whitespaceänderungen, die diffs
+          verfälschen.</p><p>Emacs und vim haben beide recht einfache Methoden zur
+          Entfernung von trailing whitespace. Emacs kennt das Kommande
+          <span class="command"><strong>nuke-trailing-whitespace</strong></span>, vim macht das gleiche
+          manuell über <code class="literal">:%s/\s\+$//e</code> Mit <code class="literal">:au
+          BufWritePre * :%s/\s\+$//e</code> wird das an Speichern
+          gebunden.</p></li><li class="listitem"><p>Es wird kein <span class="command"><strong>perltidy</strong></span> verwendet.</p><p>In der Vergangenheit wurde versucht,
+          <span class="command"><strong>perltidy</strong></span> zu verwenden, um einen einheitlichen
+          Stil zu erlangen. Es hat sich aber gezeigt, dass
+          <span class="command"><strong>perltidy</strong></span>s sehr eigenwilliges Verhalten, was
+          Zeilenumbrüche angeht, oftmals gut formatierten Code zerstört. Für
+          den Interessierten sind hier die
+          <span class="command"><strong>perltidy</strong></span>-Optionen, die grob den beschriebenen
+          Richtlinien entsprechen:</p><pre class="programlisting">-syn -i=2 -nt -pt=2 -sbt=2 -ci=2 -ibc -hsc -noll -nsts -nsfs -asc -dsm
+-aws -bbc -bbs -bbb -mbl=1 -nsob -ce -nbl -nsbl -cti=0 -bbt=0 -bar -l=79
+-lp -vt=1 -vtc=1</pre></li><li class="listitem"><p>
+                  <code class="varname">STDERR</code> ist tabu. Unkonditionale
+          Debugmeldungen auch.</p><p>kivitendo bietet mit dem Modul <code class="classname">LXDebug</code>
+          einen brauchbaren Trace-/Debug-Mechanismus. Es gibt also keinen
+          Grund, nach <code class="varname">STDERR</code> zu schreiben.</p><p>Die <code class="classname">LXDebug</code>-Methode
+          "<code class="function">message</code>" nimmt als ersten Paramter außerdem
+          eine Flagmaske, für die die Meldung angezeigt wird, wobei "0" immer
+          angezeigt wird. Solche Meldungen sollten nicht eingecheckt werden
+          und werden in den meisten Fällen auch vom Repository
+          zurückgewiesen.</p></li><li class="listitem"><p>Alle neuen Module müssen use strict verwenden.</p><p>
+                  <code class="varname">$form</code>, <code class="varname">$auth</code>,
+          <code class="varname">$locale</code>, <code class="varname">$lxdebug</code> und
+          <code class="varname">%myconfig</code> werden derzeit aus dem main package
+          importiert (siehe <a class="xref" href="ch04.html#devel.globals" title="4.1. Globale Variablen">Globale Variablen</a>. Alle anderen
+          Konstrukte sollten lexikalisch lokal gehalten werden.</p></li></ol></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch04s06.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch04.html">Nach oben</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch04s08.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">4.6. Die kivitendo-Test-Suite&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;4.8. Dokumentation erstellen</td></tr></table></div></body></html>
\ No newline at end of file
diff --git a/doc/html/ch04s08.html b/doc/html/ch04s08.html
new file mode 100644 (file)
index 0000000..11e2252
--- /dev/null
@@ -0,0 +1,38 @@
+<html><head>
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+   <title>4.8. Dokumentation erstellen</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="ch04.html" title="Kapitel 4. Entwicklerdokumentation"><link rel="prev" href="ch04s07.html" title="4.7. Stil-Richtlinien"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">4.8. Dokumentation erstellen</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch04s07.html">Zurück</a>&nbsp;</td><th width="60%" align="center">Kapitel 4. Entwicklerdokumentation</th><td width="20%" align="right">&nbsp;</td></tr></table><hr></div><div class="sect1" title="4.8. Dokumentation erstellen"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="devel.build-doc"></a>4.8. Dokumentation erstellen</h2></div></div></div><div class="sect2" title="4.8.1. Einführung"><div class="titlepage"><div><div><h3 class="title"><a name="devel.build-doc.introduction"></a>4.8.1. Einführung</h3></div></div></div><p>Diese Dokumentation ist in <span class="productname">DocBook</span>™
+        XML geschrieben. Zum Bearbeiten reicht grundsätzlich ein Text-Editor.
+        Mehr Komfort bekommt man, wenn man einen dedizierten XML-fähigen
+        Editor nutzt, der spezielle Unterstützung für
+        <span class="productname">DocBook</span>™ mitbringt. Wir empfehlen dafür den
+        <a class="ulink" href="http://www.xmlmind.com/xmleditor/" target="_top">XMLmind XML
+        Editor</a>, der bei nicht kommerzieller Nutzung kostenlos
+        ist.</p></div><div class="sect2" title="4.8.2. Benötigte Software"><div class="titlepage"><div><div><h3 class="title"><a name="devel.build-doc.required-software"></a>4.8.2. Benötigte Software</h3></div></div></div><p>Bei <span class="productname">DocBook</span>™ ist Prinzip, dass
+        ausschließlich die XML-Quelldatei bearbeitet wird. Aus dieser werden
+        dann mit entsprechenden Stylesheets andere Formate wie PDF oder HTML
+        erzeugt. Bei kivitendo übernimmt diese Aufgabe das Shell-Script
+        <span class="command"><strong>scripts/build_doc.sh</strong></span>.</p><p>Das Script benötigt zur Konvertierung verschiedene
+        Softwarekomponenten, die im normalen kivitendo-Betrieb nicht benötigt
+        werden:</p><div class="itemizedlist"><ul class="itemizedlist" type="disc"><li class="listitem"><p>
+                     <a class="ulink" href="http://www.oracle.com/technetwork/java/index.html" target="_top">Java</a>
+            in einer halbwegs aktuellen Version</p></li><li class="listitem"><p>Das Java-Build-System <a class="ulink" href="http://ant.apache.org/" target="_top">Apache Ant</a>
+                  </p></li><li class="listitem"><p>Das Dokumentations-System Dobudish für
+            <span class="productname">DocBook</span>™ 4.5, eine Zusammenstellung
+            diverser Stylesheets und Grafiken zur Konvertierung von
+            <span class="productname">DocBook</span>™ XML in andere Formate. Das
+            Paket, das benötigt wird, ist zum Zeitpunkt der
+            Dokumentationserstellung
+            <code class="filename">dobudish-nojre-1.1.4.zip</code>, aus auf <a class="ulink" href="http://code.google.com/p/dobudish/downloads/list" target="_top">code.google.com</a>
+            bereitsteht.</p></li></ul></div><p>Apache Ant sowie ein dazu passendes Java Runtime Environment
+        sind auf allen gängigen Plattformen verfügbar. Beispiel für
+        Debian/Ubuntu:</p><pre class="programlisting">apt-get install ant openjdk-7-jre</pre><p>Nach dem Download von Dobudish muss Dobudish im Unterverzeichnis
+        <code class="filename">doc/build</code> entpackt werden. Beispiel unter der
+        Annahme, das <span class="productname">Dobudish</span>™ in
+        <code class="filename">$HOME/Downloads</code> heruntergeladen wurde:</p><pre class="programlisting">cd doc/build
+unzip $HOME/Downloads/dobudish-nojre-1.1.4.zip</pre></div><div class="sect2" title="4.8.3. PDFs und HTML-Seiten erstellen"><div class="titlepage"><div><div><h3 class="title"><a name="devel.build-doc.build"></a>4.8.3. PDFs und HTML-Seiten erstellen</h3></div></div></div><p>Die eigentliche Konvertierung erfolgt nach Installation der
+        benötigten Software mit einem einfachen Aufruf direkt aus dem
+        kivitendo-Installationsverzeichnis heraus:</p><pre class="programlisting">./scripts/build_doc.sh</pre></div><div class="sect2" title="4.8.4. Einchecken in das Git-Repository"><div class="titlepage"><div><div><h3 class="title"><a name="devel.build-doc.repository"></a>4.8.4. Einchecken in das Git-Repository</h3></div></div></div><p>Sowohl die XML-Datei als auch die erzeugten PDF- und
+        HTML-Dateien sind Bestandteil des Git-Repositories. Daraus folgt, dass
+        nach Änderungen am XML die PDF- und HTML-Dokumente ebenfalls gebaut
+        und alles zusammen in einem Commit eingecheckt werden sollten.</p><p>Die "<code class="filename">dobudish</code>"-Verzeichnisse bzw.
+        symbolischen Links gehören hingegen nicht in das Repository.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch04s07.html">Zurück</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="ch04.html">Nach oben</a></td><td width="40%" align="right">&nbsp;</td></tr><tr><td width="40%" align="left" valign="top">4.7. Stil-Richtlinien&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Zum Anfang</a></td><td width="40%" align="right" valign="top">&nbsp;</td></tr></table></div></body></html>
\ No newline at end of file
diff --git a/doc/html/images/DMS-Allgemeine-Dokumentenanhaenge.png b/doc/html/images/DMS-Allgemeine-Dokumentenanhaenge.png
new file mode 100644 (file)
index 0000000..59ad7a6
Binary files /dev/null and b/doc/html/images/DMS-Allgemeine-Dokumentenanhaenge.png differ
diff --git a/doc/html/images/DMS-Anhaenge-hochladen.png b/doc/html/images/DMS-Anhaenge-hochladen.png
new file mode 100644 (file)
index 0000000..cbaed0a
Binary files /dev/null and b/doc/html/images/DMS-Anhaenge-hochladen.png differ
diff --git a/doc/html/images/DMS-Anhaenge.png b/doc/html/images/DMS-Anhaenge.png
new file mode 100644 (file)
index 0000000..dd7ad19
Binary files /dev/null and b/doc/html/images/DMS-Anhaenge.png differ
diff --git a/doc/html/images/DMS-ClientConfig.png b/doc/html/images/DMS-ClientConfig.png
new file mode 100644 (file)
index 0000000..6bf721f
Binary files /dev/null and b/doc/html/images/DMS-ClientConfig.png differ
diff --git a/doc/html/images/DMS-Dokumente-Scanner.png b/doc/html/images/DMS-Dokumente-Scanner.png
new file mode 100644 (file)
index 0000000..9378917
Binary files /dev/null and b/doc/html/images/DMS-Dokumente-Scanner.png differ
diff --git a/doc/html/images/DMS-Dokumente.png b/doc/html/images/DMS-Dokumente.png
new file mode 100644 (file)
index 0000000..069660e
Binary files /dev/null and b/doc/html/images/DMS-Dokumente.png differ
diff --git a/doc/html/images/DMS-Overview.png b/doc/html/images/DMS-Overview.png
new file mode 100644 (file)
index 0000000..25b204e
Binary files /dev/null and b/doc/html/images/DMS-Overview.png differ
diff --git a/doc/html/images/Einzahlungsschein_Makro.png b/doc/html/images/Einzahlungsschein_Makro.png
new file mode 100644 (file)
index 0000000..4356911
Binary files /dev/null and b/doc/html/images/Einzahlungsschein_Makro.png differ
diff --git a/doc/html/images/Shop_Artikel.png b/doc/html/images/Shop_Artikel.png
new file mode 100644 (file)
index 0000000..f669ec9
Binary files /dev/null and b/doc/html/images/Shop_Artikel.png differ
diff --git a/doc/html/images/Shop_Artikel_Listing.png b/doc/html/images/Shop_Artikel_Listing.png
new file mode 100644 (file)
index 0000000..b56da6f
Binary files /dev/null and b/doc/html/images/Shop_Artikel_Listing.png differ
diff --git a/doc/html/images/Shop_Bestell.png b/doc/html/images/Shop_Bestell.png
new file mode 100644 (file)
index 0000000..ea36a3b
Binary files /dev/null and b/doc/html/images/Shop_Bestell.png differ
diff --git a/doc/html/images/Shop_Config.png b/doc/html/images/Shop_Config.png
new file mode 100644 (file)
index 0000000..47f10fb
Binary files /dev/null and b/doc/html/images/Shop_Config.png differ
diff --git a/doc/html/images/Shop_Listing.png b/doc/html/images/Shop_Listing.png
new file mode 100644 (file)
index 0000000..a33078d
Binary files /dev/null and b/doc/html/images/Shop_Listing.png differ
index aa1da1a..ce6a9ac 100644 (file)
@@ -1,12 +1,9 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>kivitendo 3.3.0: Installation, Konfiguration, Entwicklung</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><link rel="next" href="ch01.html" title="Kapitel 1. Aktuelle Hinweise"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">kivitendo 3.3.0: Installation, Konfiguration, Entwicklung</th></tr><tr><td width="20%" align="left">&nbsp;</td><th width="60%" align="center">&nbsp;</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch01.html">Weiter</a></td></tr></table><hr></div><div lang="de" class="book" title="kivitendo 3.3.0: Installation, Konfiguration, Entwicklung"><div class="titlepage"><div><div><h1 class="title"><a name="kivitendo-documentation"></a>kivitendo 3.3.0: Installation, Konfiguration, Entwicklung</h1></div></div><hr></div><div class="toc"><p><b>Inhaltsverzeichnis</b></p><dl><dt><span class="chapter"><a href="ch01.html">1. Aktuelle Hinweise</a></span></dt><dt><span class="chapter"><a href="ch02.html">2. Installation und Grundkonfiguration</a></span></dt><dd><dl><dt><span class="sect1"><a href="ch02.html#Installation-%C3%9Cbersicht">2.1. Übersicht</a></span></dt><dt><span class="sect1"><a href="ch02s02.html">2.2. Benötigte Software und Pakete</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s02.html#Betriebssystem">2.2.1. Betriebssystem</a></span></dt><dt><span class="sect2"><a href="ch02s02.html#Pakete">2.2.2. Benötigte Perl-Pakete installieren</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s03.html">2.3. Manuelle Installation des Programmpaketes</a></span></dt><dt><span class="sect1"><a href="ch02s04.html">2.4. kivitendo-Konfigurationsdatei</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s04.html#config.config-file.introduction">2.4.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch02s04.html#config.config-file.sections-parameters">2.4.2. Abschnitte und Parameter</a></span></dt><dt><span class="sect2"><a href="ch02s04.html#config.config-file.prior-versions">2.4.3. Versionen vor 2.6.3</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s05.html">2.5. Anpassung der PostgreSQL-Konfiguration</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s05.html#Zeichens%C3%A4tze-die-Verwendung-von-UTF-8">2.5.1. Zeichensätze/die Verwendung von Unicode/UTF-8</a></span></dt><dt><span class="sect2"><a href="ch02s05.html#%C3%84nderungen-an-Konfigurationsdateien">2.5.2. Änderungen an Konfigurationsdateien</a></span></dt><dt><span class="sect2"><a href="ch02s05.html#Erweiterung-f%C3%BCr-servergespeicherte-Prozeduren">2.5.3. Erweiterung für servergespeicherte Prozeduren</a></span></dt><dt><span class="sect2"><a href="ch02s05.html#Datenbankbenutzer-anlegen">2.5.4. Datenbankbenutzer anlegen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s06.html">2.6. Webserver-Konfiguration</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s06.html#d0e746">2.6.1. Grundkonfiguration mittels CGI</a></span></dt><dt><span class="sect2"><a href="ch02s06.html#Apache-Konfiguration.FCGI">2.6.2. Konfiguration für FastCGI/FCGI</a></span></dt><dt><span class="sect2"><a href="ch02s06.html#d0e891">2.6.3. Weitergehende Konfiguration</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s07.html">2.7. Der Task-Server</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s07.html#Konfiguration-des-Task-Servers">2.7.1. Verfügbare und notwendige Konfigurationsoptionen</a></span></dt><dt><span class="sect2"><a href="ch02s07.html#Einbinden-in-den-Boot-Prozess">2.7.2. Automatisches Starten des Task-Servers beim Booten</a></span></dt><dt><span class="sect2"><a href="ch02s07.html#Prozesskontrolle">2.7.3. Wie der Task-Server gestartet und beendet wird</a></span></dt><dt><span class="sect2"><a href="ch02s07.html#Prozesskontrolle2">2.7.4. Task-Server mit mehreren Mandanten</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s08.html">2.8. Benutzerauthentifizierung und Administratorpasswort</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s08.html#Grundlagen-zur-Benutzerauthentifizierung">2.8.1. Grundlagen zur Benutzerauthentifizierung</a></span></dt><dt><span class="sect2"><a href="ch02s08.html#Administratorpasswort">2.8.2. Administratorpasswort</a></span></dt><dt><span class="sect2"><a href="ch02s08.html#Authentifizierungsdatenbank">2.8.3. Authentifizierungsdatenbank</a></span></dt><dt><span class="sect2"><a href="ch02s08.html#Passwort%C3%BCberpr%C3%BCfung">2.8.4. Passwortüberprüfung</a></span></dt><dt><span class="sect2"><a href="ch02s08.html#Name-des-Session-Cookies">2.8.5. Name des Session-Cookies</a></span></dt><dt><span class="sect2"><a href="ch02s08.html#Anlegen-der-Authentifizierungsdatenbank">2.8.6. Anlegen der Authentifizierungsdatenbank</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s09.html">2.9. Mandanten-, Benutzer- und Gruppenverwaltung</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s09.html#Zusammenh%C3%A4nge">2.9.1. Zusammenhänge</a></span></dt><dt><span class="sect2"><a href="ch02s09.html#Mandanten-Benutzer-Gruppen">2.9.2. Mandanten, Benutzer und Gruppen</a></span></dt><dt><span class="sect2"><a href="ch02s09.html#Datenbanken-anlegen">2.9.3. Datenbanken anlegen</a></span></dt><dt><span class="sect2"><a href="ch02s09.html#Gruppen-anlegen">2.9.4. Gruppen anlegen</a></span></dt><dt><span class="sect2"><a href="ch02s09.html#Benutzer-anlegen">2.9.5. Benutzer anlegen</a></span></dt><dt><span class="sect2"><a href="ch02s09.html#Mandanten-anlegen">2.9.6. Mandanten anlegen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s10.html">2.10. Drucker- und Systemverwaltung</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s10.html#Druckeradministration">2.10.1. Druckeradministration</a></span></dt><dt><span class="sect2"><a href="ch02s10.html#System">2.10.2. System sperren / entsperren</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s11.html">2.11. E-Mail-Versand aus kivitendo heraus</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s11.html#config.sending-email.sendmail">2.11.1. Versand über lokalen E-Mail-Server</a></span></dt><dt><span class="sect2"><a href="ch02s11.html#config.sending-email.smtp">2.11.2. Versand über einen SMTP-Server</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s12.html">2.12. Drucken mit kivitendo</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s12.html#Vorlagenverzeichnis-anlegen">2.12.1. Vorlagenverzeichnis anlegen</a></span></dt><dt><span class="sect2"><a href="ch02s12.html#Vorlagen-RB">2.12.2. Der Druckvorlagensatz RB</a></span></dt><dt><span class="sect2"><a href="ch02s12.html#f-tex">2.12.3. f-tex</a></span></dt><dt><span class="sect2"><a href="ch02s12.html#Vorlagen-rev-odt">2.12.4. Der Druckvorlagensatz rev-odt</a></span></dt><dt><span class="sect2"><a href="ch02s12.html#allgemeine-hinweise-zu-latex">2.12.5. Allgemeine Hinweise zu LaTeX Vorlagen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s13.html">2.13. OpenDocument-Vorlagen</a></span></dt><dt><span class="sect1"><a href="ch02s14.html">2.14. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung:
-      EUR</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s14.html#config.eur.introduction">2.14.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch02s14.html#config.eur.parameters">2.14.2. Konfigurationsparameter</a></span></dt><dt><span class="sect2"><a href="ch02s14.html#config.eur.setting-parameters">2.14.3. Festlegen der Parameter</a></span></dt><dt><span class="sect2"><a href="ch02s14.html#config.eur.inventory-system-perpetual">2.14.4. Bemerkungen zur Bestandsmethode</a></span></dt><dt><span class="sect2"><a href="ch02s14.html#config.eur.knonw-issues">2.14.5. Bekannte Probleme</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s15.html">2.15. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s15.html#config.skr04-update-3804.introduction">2.15.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch02s15.html#config.skr04-update-3804.create-chart">2.15.2. Konto 3804 manuell anlegen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s16.html">2.16. Verhalten des Bilanzberichts</a></span></dt><dt><span class="sect1"><a href="ch02s17.html">2.17. Einstellungen pro Mandant</a></span></dt><dt><span class="sect1"><a href="ch02s18.html">2.18. kivitendo ERP verwenden</a></span></dt></dl></dd><dt><span class="chapter"><a href="ch03.html">3. Features und Funktionen</a></span></dt><dd><dl><dt><span class="sect1"><a href="ch03.html#features.periodic-invoices">3.1. Wiederkehrende Rechnungen</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch03.html#features.periodic-invoices.introduction">3.1.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch03.html#features.periodic-invoices.configuration">3.1.2. Konfiguration</a></span></dt><dt><span class="sect2"><a href="ch03.html#features.periodic-invoices.variables">3.1.3. Spezielle Variablen</a></span></dt><dt><span class="sect2"><a href="ch03.html#features.periodic-invoices.reports">3.1.4. Auflisten</a></span></dt><dt><span class="sect2"><a href="ch03.html#features.periodic-invoices.task-server">3.1.5. Erzeugung der eigentlichen Rechnungen</a></span></dt><dt><span class="sect2"><a href="ch03.html#features.periodic-invoices.create-for-current-month">3.1.6. Erste Rechnung für aktuellen Monat erstellen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch03s02.html">3.2. Bankerweiterung</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch03s02.html#features.bank.introduction">3.2.1. Einführung</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch03s03.html">3.3. Dokumentenvorlagen und verfügbare Variablen</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.einf%C3%BChrung">3.3.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.variablen-ausgeben">3.3.2. Variablen ausgeben</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.verwendung-in-druckbefehlen">3.3.3. Verwendung in Druckbefehlen</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.tag-style">3.3.4. Anfang und Ende der Tags verändern</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.zuordnung-dateinamen">3.3.5. Zuordnung von den Dateinamen zu den Funktionen</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.dateinamen-erweitert">3.3.6. Sprache, Drucker und E-Mail</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.allgemeine-variablen">3.3.7. Allgemeine Variablen, die in allen Vorlagen vorhanden
+   <title>kivitendo 3.6.1: Installation, Konfiguration, Entwicklung</title><link rel="stylesheet" type="text/css" href="style.css"><meta name="generator" content="DocBook XSL Stylesheets V1.76.1-RC2"><link rel="home" href="index.html" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><link rel="next" href="ch01.html" title="Kapitel 1. Aktuelle Hinweise"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">kivitendo 3.6.1: Installation, Konfiguration,
+  Entwicklung</th></tr><tr><td width="20%" align="left">&nbsp;</td><th width="60%" align="center">&nbsp;</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="ch01.html">Weiter</a></td></tr></table><hr></div><div lang="de" class="book" title="kivitendo 3.6.1: Installation, Konfiguration, Entwicklung"><div class="titlepage"><div><div><h1 class="title"><a name="kivitendo-documentation"></a>kivitendo 3.6.1: Installation, Konfiguration,
+  Entwicklung</h1></div></div><hr></div><div class="toc"><p><b>Inhaltsverzeichnis</b></p><dl><dt><span class="chapter"><a href="ch01.html">1. Aktuelle Hinweise</a></span></dt><dt><span class="chapter"><a href="ch02.html">2. Installation und Grundkonfiguration</a></span></dt><dd><dl><dt><span class="sect1"><a href="ch02.html#Installation-%C3%9Cbersicht">2.1. Übersicht</a></span></dt><dt><span class="sect1"><a href="ch02s02.html">2.2. Benötigte Software und Pakete</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s02.html#Betriebssystem">2.2.1. Betriebssystem</a></span></dt><dt><span class="sect2"><a href="ch02s02.html#Pakete">2.2.2. Benötigte Perl-Pakete installieren</a></span></dt><dt><span class="sect2"><a href="ch02s02.html#d0e718">2.2.3. Andere Pakete installieren</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s03.html">2.3. Manuelle Installation des Programmpaketes</a></span></dt><dt><span class="sect1"><a href="ch02s04.html">2.4. kivitendo-Konfigurationsdatei</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s04.html#config.config-file.introduction">2.4.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch02s04.html#config.config-file.sections-parameters">2.4.2. Abschnitte und Parameter</a></span></dt><dt><span class="sect2"><a href="ch02s04.html#config.config-file.prior-versions">2.4.3. Versionen vor 2.6.3</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s05.html">2.5. Anpassung der PostgreSQL-Konfiguration</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s05.html#Zeichens%C3%A4tze-die-Verwendung-von-UTF-8">2.5.1. Zeichensätze/die Verwendung von Unicode/UTF-8</a></span></dt><dt><span class="sect2"><a href="ch02s05.html#%C3%84nderungen-an-Konfigurationsdateien">2.5.2. Änderungen an Konfigurationsdateien</a></span></dt><dt><span class="sect2"><a href="ch02s05.html#Erweiterung-f%C3%BCr-servergespeicherte-Prozeduren">2.5.3. Erweiterung für servergespeicherte Prozeduren</a></span></dt><dt><span class="sect2"><a href="ch02s05.html#Erweiterung-f%C3%BCr-trigram">2.5.4. Erweiterung für Trigram Prozeduren</a></span></dt><dt><span class="sect2"><a href="ch02s05.html#Datenbankbenutzer-anlegen">2.5.5. Datenbankbenutzer anlegen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s06.html">2.6. Webserver-Konfiguration</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s06.html#d0e1192">2.6.1. Grundkonfiguration mittels CGI</a></span></dt><dt><span class="sect2"><a href="ch02s06.html#Apache-Konfiguration.FCGI">2.6.2. Konfiguration für FastCGI/FCGI</a></span></dt><dt><span class="sect2"><a href="ch02s06.html#d0e1346">2.6.3. Authentifizierung mittels HTTP Basic Authentication</a></span></dt><dt><span class="sect2"><a href="ch02s06.html#d0e1362">2.6.4. Aktivierung von mod_rewrite/directory_match für git basierte Installationen</a></span></dt><dt><span class="sect2"><a href="ch02s06.html#d0e1376">2.6.5. Weitergehende Konfiguration</a></span></dt><dt><span class="sect2"><a href="ch02s06.html#d0e1387">2.6.6. Aktivierung von Apache2 modsecurity</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s07.html">2.7. Der Task-Server</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s07.html#Konfiguration-des-Task-Servers">2.7.1. Verfügbare und notwendige Konfigurationsoptionen</a></span></dt><dt><span class="sect2"><a href="ch02s07.html#Konfiguration-der-Mandanten-fuer-den-Task-Servers">2.7.2. Konfiguration der Mandanten für den Task-Server</a></span></dt><dt><span class="sect2"><a href="ch02s07.html#Einbinden-in-den-Boot-Prozess">2.7.3. Automatisches Starten des Task-Servers beim Booten</a></span></dt><dt><span class="sect2"><a href="ch02s07.html#Prozesskontrolle">2.7.4. Wie der Task-Server gestartet und beendet wird</a></span></dt><dt><span class="sect2"><a href="ch02s07.html#Tasks-konfigurieren">2.7.5. Exemplarische Konfiguration eines Hintergrund-Jobs, der die Jahreszahl in allen Nummernkreisen zum Jahreswechsel erhöht</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s08.html">2.8. Benutzerauthentifizierung und Administratorpasswort</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s08.html#Grundlagen-zur-Benutzerauthentifizierung">2.8.1. Grundlagen zur Benutzerauthentifizierung</a></span></dt><dt><span class="sect2"><a href="ch02s08.html#Administratorpasswort">2.8.2. Administratorpasswort</a></span></dt><dt><span class="sect2"><a href="ch02s08.html#Authentifizierungsdatenbank">2.8.3. Authentifizierungsdatenbank</a></span></dt><dt><span class="sect2"><a href="ch02s08.html#Passwort%C3%BCberpr%C3%BCfung">2.8.4. Passwortüberprüfung</a></span></dt><dt><span class="sect2"><a href="ch02s08.html#Name-des-Session-Cookies">2.8.5. Name des Session-Cookies</a></span></dt><dt><span class="sect2"><a href="ch02s08.html#Anlegen-der-Authentifizierungsdatenbank">2.8.6. Anlegen der Authentifizierungsdatenbank</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s09.html">2.9. Mandanten-, Benutzer- und Gruppenverwaltung</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s09.html#Zusammenh%C3%A4nge">2.9.1. Zusammenhänge</a></span></dt><dt><span class="sect2"><a href="ch02s09.html#Mandanten-Benutzer-Gruppen">2.9.2. Mandanten, Benutzer und Gruppen</a></span></dt><dt><span class="sect2"><a href="ch02s09.html#Datenbanken-anlegen">2.9.3. Datenbanken anlegen</a></span></dt><dt><span class="sect2"><a href="ch02s09.html#Gruppen-anlegen">2.9.4. Gruppen anlegen</a></span></dt><dt><span class="sect2"><a href="ch02s09.html#Benutzer-anlegen">2.9.5. Benutzer anlegen</a></span></dt><dt><span class="sect2"><a href="ch02s09.html#Mandanten-anlegen">2.9.6. Mandanten anlegen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s10.html">2.10. Drucker- und Systemverwaltung</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s10.html#Druckeradministration">2.10.1. Druckeradministration</a></span></dt><dt><span class="sect2"><a href="ch02s10.html#System">2.10.2. System sperren / entsperren</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s11.html">2.11. E-Mail-Versand aus kivitendo heraus</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s11.html#config.sending-email.sendmail">2.11.1. Versand über lokalen E-Mail-Server</a></span></dt><dt><span class="sect2"><a href="ch02s11.html#config.sending-email.smtp">2.11.2. Versand über einen SMTP-Server</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s12.html">2.12. Drucken mit kivitendo</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s12.html#Vorlagenverzeichnis-anlegen">2.12.1. Vorlagenverzeichnis anlegen</a></span></dt><dt><span class="sect2"><a href="ch02s12.html#Vorlagen-RB">2.12.2. Der Druckvorlagensatz RB</a></span></dt><dt><span class="sect2"><a href="ch02s12.html#Vorlagen-rev-odt">2.12.3. Der Druckvorlagensatz rev-odt</a></span></dt><dt><span class="sect2"><a href="ch02s12.html#allgemeine-hinweise-zu-latex">2.12.4. Allgemeine Hinweise zu LaTeX Vorlagen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s13.html">2.13. OpenDocument-Vorlagen</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s13.html#d0e2489">2.13.1. OpenDocument (odt) Druckvorlagen mit Makros</a></span></dt><dt><span class="sect2"><a href="ch02s13.html#d0e2644">2.13.2. Schweizer QR-Rechnung mit OpenDocument Vorlagen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s14.html">2.14. Nomenklatur</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s14.html#booking.dates">2.14.1. Datum bei Buchungen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s15.html">2.15. Konfiguration zur Einnahmenüberschussrechnung/Bilanzierung:
+      EUR</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s15.html#config.eur.introduction">2.15.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch02s15.html#config.eur.parameters">2.15.2. Konfigurationsparameter</a></span></dt><dt><span class="sect2"><a href="ch02s15.html#config.eur.setting-parameters">2.15.3. Festlegen der Parameter</a></span></dt><dt><span class="sect2"><a href="ch02s15.html#config.eur.inventory-system-perpetual">2.15.4. Bemerkungen zur Bestandsmethode</a></span></dt><dt><span class="sect2"><a href="ch02s15.html#config.eur.knonw-issues">2.15.5. Bekannte Probleme</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s16.html">2.16. SKR04 19% Umstellung für innergemeinschaftlichen Erwerb</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch02s16.html#config.skr04-update-3804.introduction">2.16.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch02s16.html#config.skr04-update-3804.create-chart">2.16.2. Konto 3804 manuell anlegen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch02s17.html">2.17. Verhalten des Bilanzberichts</a></span></dt><dt><span class="sect1"><a href="ch02s18.html">2.18. Erfolgsrechnung</a></span></dt><dt><span class="sect1"><a href="ch02s19.html">2.19. Rundung in Verkaufsbelegen</a></span></dt><dt><span class="sect1"><a href="ch02s20.html">2.20. Einstellungen pro Mandant</a></span></dt><dt><span class="sect1"><a href="ch02s21.html">2.21. kivitendo ERP verwenden</a></span></dt></dl></dd><dt><span class="chapter"><a href="ch03.html">3. Features und Funktionen</a></span></dt><dd><dl><dt><span class="sect1"><a href="ch03.html#features.periodic-invoices">3.1. Wiederkehrende Rechnungen</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch03.html#features.periodic-invoices.introduction">3.1.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch03.html#features.periodic-invoices.configuration">3.1.2. Konfiguration</a></span></dt><dt><span class="sect2"><a href="ch03.html#features.periodic-invoices.variables">3.1.3. Spezielle Variablen</a></span></dt><dt><span class="sect2"><a href="ch03.html#features.periodic-invoices.reports">3.1.4. Auflisten</a></span></dt><dt><span class="sect2"><a href="ch03.html#features.periodic-invoices.task-server">3.1.5. Erzeugung der eigentlichen Rechnungen</a></span></dt><dt><span class="sect2"><a href="ch03.html#features.periodic-invoices.create-for-current-month">3.1.6. Erste Rechnung für aktuellen Monat erstellen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch03s02.html">3.2. Bankerweiterung</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch03s02.html#features.bank.introduction">3.2.1. Einführung</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch03s03.html">3.3. Dokumentenvorlagen und verfügbare Variablen</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.einf%C3%BChrung">3.3.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.variablen-ausgeben">3.3.2. Variablen ausgeben</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.verwendung-in-druckbefehlen">3.3.3. Verwendung in Druckbefehlen</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.tag-style">3.3.4. Anfang und Ende der Tags verändern</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.zuordnung-dateinamen">3.3.5. Zuordnung von den Dateinamen zu den Funktionen</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.dateinamen-erweitert">3.3.6. Sprache, Drucker und E-Mail</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.allgemeine-variablen">3.3.7. Allgemeine Variablen, die in allen Vorlagen vorhanden
         sind</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.invoice">3.3.8. Variablen in Rechnungen</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.dunning">3.3.9. Variablen in Mahnungen und Rechnungen über Mahngebühren</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.andere-vorlagen">3.3.10. Variablen in anderen Vorlagen</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.bloecke">3.3.11. Blöcke, bedingte Anweisungen und Schleifen</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.markup">3.3.12. Markup-Code zur Textformatierung innerhalb von
-        Formularen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch03s04.html">3.4. Excel-Vorlagen</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch03s04.html#excel-templates.summary">3.4.1. Zusammenfassung</a></span></dt><dt><span class="sect2"><a href="ch03s04.html#excel-templates.usage">3.4.2. Bedienung</a></span></dt><dt><span class="sect2"><a href="ch03s04.html#excel-templates.syntax">3.4.3. Variablensyntax</a></span></dt><dt><span class="sect2"><a href="ch03s04.html#excel-templates.limitations">3.4.4. Einschränkungen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch03s05.html">3.5. Mandantenkonfiguration Lager</a></span></dt></dl></dd><dt><span class="chapter"><a href="ch04.html">4. Entwicklerdokumentation</a></span></dt><dd><dl><dt><span class="sect1"><a href="ch04.html#devel.globals">4.1. Globale Variablen</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch04.html#d0e5748">4.1.1. Wie sehen globale Variablen in Perl aus?</a></span></dt><dt><span class="sect2"><a href="ch04.html#d0e5849">4.1.2. Warum sind globale Variablen ein Problem?</a></span></dt><dt><span class="sect2"><a href="ch04.html#d0e5882">4.1.3. Kanonische globale Variablen</a></span></dt><dt><span class="sect2"><a href="ch04.html#d0e6270">4.1.4. Ehemalige globale Variablen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch04s02.html">4.2. Entwicklung unter FastCGI</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch04s02.html#devel.fcgi.general">4.2.1. Allgemeines</a></span></dt><dt><span class="sect2"><a href="ch04s02.html#devel.fcgi.exiting">4.2.2. Programmende und Ausnahmen</a></span></dt><dt><span class="sect2"><a href="ch04s02.html#devel.fcgi.globals">4.2.3. Globale Variablen</a></span></dt><dt><span class="sect2"><a href="ch04s02.html#devel.fcgi.performance">4.2.4. Performance und Statistiken</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch04s03.html">4.3. SQL-Upgradedateien</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch04s03.html#db-upgrade-files.introduction">4.3.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch04s03.html#db-upgrade-files.format">4.3.2. Format der Kontrollinformationen</a></span></dt><dt><span class="sect2"><a href="ch04s03.html#db-upgrade-files.format-perl-files">4.3.3. Format von in Perl geschriebenen Datenbankupgradescripten</a></span></dt><dt><span class="sect2"><a href="ch04s03.html#db-upgrade-files.dbupgrade-tool">4.3.4. Hilfsscript dbupgrade2_tool.pl</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch04s04.html">4.4. Translations and languages</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch04s04.html#translations-languages.introduction">4.4.1. Introduction</a></span></dt><dt><span class="sect2"><a href="ch04s04.html#translations-languages.character-set">4.4.2. Character set</a></span></dt><dt><span class="sect2"><a href="ch04s04.html#translations-languages.file-structure">4.4.3. File structure</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch04s05.html">4.5. Die kivitendo-Test-Suite</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch04s05.html#devel.testsuite.intro">4.5.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch04s05.html#devel.testsuite.prerequisites">4.5.2. Voraussetzungen</a></span></dt><dt><span class="sect2"><a href="ch04s05.html#devel.testsuite.execution">4.5.3. 
-          Existierende Tests ausführen
-        </a></span></dt><dt><span class="sect2"><a href="ch04s05.html#devel.testsuite.meaning_of_scripts">4.5.4. 
-          Bedeutung der verschiedenen Test-Scripte
-        </a></span></dt><dt><span class="sect2"><a href="ch04s05.html#devel.testsuite.create_new">4.5.5. 
-          Neue Test-Scripte erstellen
-        </a></span></dt></dl></dd><dt><span class="sect1"><a href="ch04s06.html">4.6. Stil-Richtlinien</a></span></dt><dt><span class="sect1"><a href="ch04s07.html">4.7. Dokumentation erstellen</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch04s07.html#devel.build-doc.introduction">4.7.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch04s07.html#devel.build-doc.required-software">4.7.2. Benötigte Software</a></span></dt><dt><span class="sect2"><a href="ch04s07.html#devel.build-doc.build">4.7.3. PDFs und HTML-Seiten erstellen</a></span></dt><dt><span class="sect2"><a href="ch04s07.html#devel.build-doc.repository">4.7.4. Einchecken in das Git-Repository</a></span></dt></dl></dd></dl></dd></dl></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left">&nbsp;</td><td width="20%" align="center">&nbsp;</td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch01.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">&nbsp;</td><td width="20%" align="center">&nbsp;</td><td width="40%" align="right" valign="top">&nbsp;Kapitel 1. Aktuelle Hinweise</td></tr></table></div></body></html>
\ No newline at end of file
+        Formularen</a></span></dt><dt><span class="sect2"><a href="ch03s03.html#dokumentenvorlagen-und-variablen.anrede">3.3.13. Hinweise zur Anrede</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch03s04.html">3.4. Excel-Vorlagen</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch03s04.html#excel-templates.summary">3.4.1. Zusammenfassung</a></span></dt><dt><span class="sect2"><a href="ch03s04.html#excel-templates.usage">3.4.2. Bedienung</a></span></dt><dt><span class="sect2"><a href="ch03s04.html#excel-templates.syntax">3.4.3. Variablensyntax</a></span></dt><dt><span class="sect2"><a href="ch03s04.html#excel-templates.limitations">3.4.4. Einschränkungen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch03s05.html">3.5. Mandantenkonfiguration Lager</a></span></dt><dt><span class="sect1"><a href="ch03s06.html">3.6. Schweizer Kontenpläne</a></span></dt><dt><span class="sect1"><a href="ch03s07.html">3.7. Artikelklassifizierung</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch03s07.html#d0e6817">3.7.1. Übersicht</a></span></dt><dt><span class="sect2"><a href="ch03s07.html#d0e6822">3.7.2. Basisklassifizierung</a></span></dt><dt><span class="sect2"><a href="ch03s07.html#d0e6852">3.7.3. Attribute</a></span></dt><dt><span class="sect2"><a href="ch03s07.html#d0e6883">3.7.4. Zwei-Zeichen Abkürzung</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch03s08.html">3.8. Dateiverwaltung (Mini-DMS)</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch03s08.html#d0e6895">3.8.1. Übersicht</a></span></dt><dt><span class="sect2"><a href="ch03s08.html#d0e6922">3.8.2. Struktur</a></span></dt><dt><span class="sect2"><a href="ch03s08.html#d0e6974">3.8.3. Anwendung</a></span></dt><dt><span class="sect2"><a href="ch03s08.html#d0e7017">3.8.4. Konfigurierung</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch03s09.html">3.9. Webshop-Api</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch03s09.html#d0e7082">3.9.1. Rechte für die Webshopapi</a></span></dt><dt><span class="sect2"><a href="ch03s09.html#d0e7097">3.9.2. Konfiguration</a></span></dt><dt><span class="sect2"><a href="ch03s09.html#d0e7105">3.9.3. Webshopartikel</a></span></dt><dt><span class="sect2"><a href="ch03s09.html#d0e7129">3.9.4. Bestellimport</a></span></dt><dt><span class="sect2"><a href="ch03s09.html#d0e7182">3.9.5. Mapping der Daten</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch03s10.html">3.10. ZUGFeRD Rechnungen</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch03s10.html#features.zugferd.preamble">3.10.1. Vorbedingung</a></span></dt><dt><span class="sect2"><a href="ch03s10.html#features.zugferd.summary">3.10.2. Übersicht</a></span></dt><dt><span class="sect2"><a href="ch03s10.html#features.zugferd.create_zugferd_bills">3.10.3. Erstellen von ZUGFeRD Rechnungen in Kivitendo</a></span></dt><dt><span class="sect2"><a href="ch03s10.html#features.zugferd.read_zugferd_bills">3.10.4. Einlesen von ZUGFeRD Rechnungen in Kivitendo</a></span></dt></dl></dd></dl></dd><dt><span class="chapter"><a href="ch04.html">4. Entwicklerdokumentation</a></span></dt><dd><dl><dt><span class="sect1"><a href="ch04.html#devel.globals">4.1. Globale Variablen</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch04.html#d0e7271">4.1.1. Wie sehen globale Variablen in Perl aus?</a></span></dt><dt><span class="sect2"><a href="ch04.html#d0e7372">4.1.2. Warum sind globale Variablen ein Problem?</a></span></dt><dt><span class="sect2"><a href="ch04.html#d0e7405">4.1.3. Kanonische globale Variablen</a></span></dt><dt><span class="sect2"><a href="ch04.html#d0e7793">4.1.4. Ehemalige globale Variablen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch04s02.html">4.2. Entwicklung unter FastCGI</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch04s02.html#devel.fcgi.general">4.2.1. Allgemeines</a></span></dt><dt><span class="sect2"><a href="ch04s02.html#devel.fcgi.exiting">4.2.2. Programmende und Ausnahmen</a></span></dt><dt><span class="sect2"><a href="ch04s02.html#devel.fcgi.globals">4.2.3. Globale Variablen</a></span></dt><dt><span class="sect2"><a href="ch04s02.html#devel.fcgi.performance">4.2.4. Performance und Statistiken</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch04s03.html">4.3. Programmatische API-Aufrufe</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch04s03.html#dev-programmatic-api-calls.introduction">4.3.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch04s03.html#dev-programmatic-api-calls.client_selection">4.3.2. Wahl des Mandanten</a></span></dt><dt><span class="sect2"><a href="ch04s03.html#dev-programmatic-api-calls.http_basic_authentication">4.3.3. HTTP-»Basic«-Authentifizierung</a></span></dt><dt><span class="sect2"><a href="ch04s03.html#dev-programmatic-api-calls.authentication_via_parameters">4.3.4. Authentifizierung mit Parametern</a></span></dt><dt><span class="sect2"><a href="ch04s03.html#dev-programmatic-api-calls.examples">4.3.5. Beispiele</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch04s04.html">4.4. SQL-Upgradedateien</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch04s04.html#db-upgrade-files.introduction">4.4.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch04s04.html#db-upgrade-files.format">4.4.2. Format der Kontrollinformationen</a></span></dt><dt><span class="sect2"><a href="ch04s04.html#db-upgrade-files.format-perl-files">4.4.3. Format von in Perl geschriebenen
+        Datenbankupgradescripten</a></span></dt><dt><span class="sect2"><a href="ch04s04.html#db-upgrade-files.dbupgrade-tool">4.4.4. Hilfsscript dbupgrade2_tool.pl</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch04s05.html">4.5. Translations and languages</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch04s05.html#translations-languages.introduction">4.5.1. Introduction</a></span></dt><dt><span class="sect2"><a href="ch04s05.html#translations-languages.character-set">4.5.2. Character set</a></span></dt><dt><span class="sect2"><a href="ch04s05.html#translations-languages.file-structure">4.5.3. File structure</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch04s06.html">4.6. Die kivitendo-Test-Suite</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch04s06.html#devel.testsuite.intro">4.6.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch04s06.html#devel.testsuite.prerequisites">4.6.2. Voraussetzungen</a></span></dt><dt><span class="sect2"><a href="ch04s06.html#devel.testsuite.execution">4.6.3. Existierende Tests ausführen</a></span></dt><dt><span class="sect2"><a href="ch04s06.html#devel.testsuite.meaning_of_scripts">4.6.4. Bedeutung der verschiedenen Test-Scripte</a></span></dt><dt><span class="sect2"><a href="ch04s06.html#devel.testsuite.create_new">4.6.5. Neue Test-Scripte erstellen</a></span></dt></dl></dd><dt><span class="sect1"><a href="ch04s07.html">4.7. Stil-Richtlinien</a></span></dt><dt><span class="sect1"><a href="ch04s08.html">4.8. Dokumentation erstellen</a></span></dt><dd><dl><dt><span class="sect2"><a href="ch04s08.html#devel.build-doc.introduction">4.8.1. Einführung</a></span></dt><dt><span class="sect2"><a href="ch04s08.html#devel.build-doc.required-software">4.8.2. Benötigte Software</a></span></dt><dt><span class="sect2"><a href="ch04s08.html#devel.build-doc.build">4.8.3. PDFs und HTML-Seiten erstellen</a></span></dt><dt><span class="sect2"><a href="ch04s08.html#devel.build-doc.repository">4.8.4. Einchecken in das Git-Repository</a></span></dt></dl></dd></dl></dd></dl></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left">&nbsp;</td><td width="20%" align="center">&nbsp;</td><td width="40%" align="right">&nbsp;<a accesskey="n" href="ch01.html">Weiter</a></td></tr><tr><td width="40%" align="left" valign="top">&nbsp;</td><td width="20%" align="center">&nbsp;</td><td width="40%" align="right" valign="top">&nbsp;Kapitel 1. Aktuelle Hinweise</td></tr></table></div></body></html>
\ No newline at end of file
diff --git a/doc/html/system/docbook-xsl/images/tip.png b/doc/html/system/docbook-xsl/images/tip.png
new file mode 100644 (file)
index 0000000..5c4aab3
Binary files /dev/null and b/doc/html/system/docbook-xsl/images/tip.png differ
diff --git a/doc/images/DMS-Allgemeine-Dokumentenanhaenge.png b/doc/images/DMS-Allgemeine-Dokumentenanhaenge.png
new file mode 100644 (file)
index 0000000..59ad7a6
Binary files /dev/null and b/doc/images/DMS-Allgemeine-Dokumentenanhaenge.png differ
diff --git a/doc/images/DMS-Anhaenge-hochladen.png b/doc/images/DMS-Anhaenge-hochladen.png
new file mode 100644 (file)
index 0000000..cbaed0a
Binary files /dev/null and b/doc/images/DMS-Anhaenge-hochladen.png differ
diff --git a/doc/images/DMS-Anhaenge.png b/doc/images/DMS-Anhaenge.png
new file mode 100644 (file)
index 0000000..dd7ad19
Binary files /dev/null and b/doc/images/DMS-Anhaenge.png differ
diff --git a/doc/images/DMS-ClientConfig.png b/doc/images/DMS-ClientConfig.png
new file mode 100644 (file)
index 0000000..6bf721f
Binary files /dev/null and b/doc/images/DMS-ClientConfig.png differ
diff --git a/doc/images/DMS-Dokumente-Scanner.png b/doc/images/DMS-Dokumente-Scanner.png
new file mode 100644 (file)
index 0000000..9378917
Binary files /dev/null and b/doc/images/DMS-Dokumente-Scanner.png differ
diff --git a/doc/images/DMS-Dokumente.png b/doc/images/DMS-Dokumente.png
new file mode 100644 (file)
index 0000000..069660e
Binary files /dev/null and b/doc/images/DMS-Dokumente.png differ
diff --git a/doc/images/DMS-Overview.png b/doc/images/DMS-Overview.png
new file mode 100644 (file)
index 0000000..25b204e
Binary files /dev/null and b/doc/images/DMS-Overview.png differ
diff --git a/doc/images/Einzahlungsschein_Makro.png b/doc/images/Einzahlungsschein_Makro.png
new file mode 100644 (file)
index 0000000..4356911
Binary files /dev/null and b/doc/images/Einzahlungsschein_Makro.png differ
diff --git a/doc/images/Shop_Artikel.png b/doc/images/Shop_Artikel.png
new file mode 100644 (file)
index 0000000..f669ec9
Binary files /dev/null and b/doc/images/Shop_Artikel.png differ
diff --git a/doc/images/Shop_Artikel_Listing.png b/doc/images/Shop_Artikel_Listing.png
new file mode 100644 (file)
index 0000000..b56da6f
Binary files /dev/null and b/doc/images/Shop_Artikel_Listing.png differ
diff --git a/doc/images/Shop_Bestell.png b/doc/images/Shop_Bestell.png
new file mode 100644 (file)
index 0000000..ea36a3b
Binary files /dev/null and b/doc/images/Shop_Bestell.png differ
diff --git a/doc/images/Shop_Config.png b/doc/images/Shop_Config.png
new file mode 100644 (file)
index 0000000..47f10fb
Binary files /dev/null and b/doc/images/Shop_Config.png differ
diff --git a/doc/images/Shop_Listing.png b/doc/images/Shop_Listing.png
new file mode 100644 (file)
index 0000000..a33078d
Binary files /dev/null and b/doc/images/Shop_Listing.png differ
index 778bb01..205b043 100644 (file)
Binary files a/doc/kivitendo-Dokumentation.pdf and b/doc/kivitendo-Dokumentation.pdf differ
diff --git a/doc/modules/LICENSE.CGI-Ajax b/doc/modules/LICENSE.CGI-Ajax
deleted file mode 100644 (file)
index 9d0305b..0000000
+++ /dev/null
@@ -1,383 +0,0 @@
-Terms of Perl itself
-
-a) the GNU General Public License as published by the Free
-   Software Foundation; either version 1, or (at your option) any
-   later version, or
-b) the "Artistic License"
-
----------------------------------------------------------------------------
-
-The General Public License (GPL)
-Version 2, June 1991
-
-Copyright (C) 1989, 1991 Free Software Foundation, Inc. 675 Mass Ave,
-Cambridge, MA 02139, USA. Everyone is permitted to copy and distribute
-verbatim copies of this license document, but changing it is not allowed.
-
-Preamble
-
-The licenses for most software are designed to take away your freedom to share
-and change it. By contrast, the GNU General Public License is intended to
-guarantee your freedom to share and change free software--to make sure the
-software is free for all its users. This General Public License applies to most of
-the Free Software Foundation's software and to any other program whose
-authors commit to using it. (Some other Free Software Foundation software is
-covered by the GNU Library General Public License instead.) You can apply it to
-your programs, too.
-
-When we speak of free software, we are referring to freedom, not price. Our
-General Public Licenses are designed to make sure that you have the freedom
-to distribute copies of free software (and charge for this service if you wish), that
-you receive source code or can get it if you want it, that you can change the
-software or use pieces of it in new free programs; and that you know you can do
-these things.
-
-To protect your rights, we need to make restrictions that forbid anyone to deny
-you these rights or to ask you to surrender the rights. These restrictions
-translate to certain responsibilities for you if you distribute copies of the
-software, or if you modify it.
-
-For example, if you distribute copies of such a program, whether gratis or for a
-fee, you must give the recipients all the rights that you have. You must make
-sure that they, too, receive or can get the source code. And you must show
-them these terms so they know their rights.
-
-We protect your rights with two steps: (1) copyright the software, and (2) offer
-you this license which gives you legal permission to copy, distribute and/or
-modify the software.
-
-Also, for each author's protection and ours, we want to make certain that
-everyone understands that there is no warranty for this free software. If the
-software is modified by someone else and passed on, we want its recipients to
-know that what they have is not the original, so that any problems introduced by
-others will not reflect on the original authors' reputations.
-
-Finally, any free program is threatened constantly by software patents. We wish
-to avoid the danger that redistributors of a free program will individually obtain
-patent licenses, in effect making the program proprietary. To prevent this, we
-have made it clear that any patent must be licensed for everyone's free use or
-not licensed at all.
-
-The precise terms and conditions for copying, distribution and modification
-follow.
-
-GNU GENERAL PUBLIC LICENSE
-TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND
-MODIFICATION
-
-0. This License applies to any program or other work which contains a notice
-placed by the copyright holder saying it may be distributed under the terms of
-this General Public License. The "Program", below, refers to any such program
-or work, and a "work based on the Program" means either the Program or any
-derivative work under copyright law: that is to say, a work containing the
-Program or a portion of it, either verbatim or with modifications and/or translated
-into another language. (Hereinafter, translation is included without limitation in
-the term "modification".) Each licensee is addressed as "you".
-
-Activities other than copying, distribution and modification are not covered by
-this License; they are outside its scope. The act of running the Program is not
-restricted, and the output from the Program is covered only if its contents
-constitute a work based on the Program (independent of having been made by
-running the Program). Whether that is true depends on what the Program does.
-
-1. You may copy and distribute verbatim copies of the Program's source code as
-you receive it, in any medium, provided that you conspicuously and appropriately
-publish on each copy an appropriate copyright notice and disclaimer of warranty;
-keep intact all the notices that refer to this License and to the absence of any
-warranty; and give any other recipients of the Program a copy of this License
-along with the Program.
-
-You may charge a fee for the physical act of transferring a copy, and you may at
-your option offer warranty protection in exchange for a fee.
-
-2. You may modify your copy or copies of the Program or any portion of it, thus
-forming a work based on the Program, and copy and distribute such
-modifications or work under the terms of Section 1 above, provided that you also
-meet all of these conditions:
-
-a) You must cause the modified files to carry prominent notices stating that you
-changed the files and the date of any change.
-
-b) You must cause any work that you distribute or publish, that in whole or in
-part contains or is derived from the Program or any part thereof, to be licensed
-as a whole at no charge to all third parties under the terms of this License.
-
-c) If the modified program normally reads commands interactively when run, you
-must cause it, when started running for such interactive use in the most ordinary
-way, to print or display an announcement including an appropriate copyright
-notice and a notice that there is no warranty (or else, saying that you provide a
-warranty) and that users may redistribute the program under these conditions,
-and telling the user how to view a copy of this License. (Exception: if the
-Program itself is interactive but does not normally print such an announcement,
-your work based on the Program is not required to print an announcement.)
-
-These requirements apply to the modified work as a whole. If identifiable
-sections of that work are not derived from the Program, and can be reasonably
-considered independent and separate works in themselves, then this License,
-and its terms, do not apply to those sections when you distribute them as
-separate works. But when you distribute the same sections as part of a whole
-which is a work based on the Program, the distribution of the whole must be on
-the terms of this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest your rights to
-work written entirely by you; rather, the intent is to exercise the right to control
-the distribution of derivative or collective works based on the Program.
-
-In addition, mere aggregation of another work not based on the Program with the
-Program (or with a work based on the Program) on a volume of a storage or
-distribution medium does not bring the other work under the scope of this
-License.
-
-3. You may copy and distribute the Program (or a work based on it, under
-Section 2) in object code or executable form under the terms of Sections 1 and 2
-above provided that you also do one of the following:
-
-a) Accompany it with the complete corresponding machine-readable source
-code, which must be distributed under the terms of Sections 1 and 2 above on a
-medium customarily used for software interchange; or,
-
-b) Accompany it with a written offer, valid for at least three years, to give any
-third party, for a charge no more than your cost of physically performing source
-distribution, a complete machine-readable copy of the corresponding source
-code, to be distributed under the terms of Sections 1 and 2 above on a medium
-customarily used for software interchange; or,
-
-c) Accompany it with the information you received as to the offer to distribute
-corresponding source code. (This alternative is allowed only for noncommercial
-distribution and only if you received the program in object code or executable
-form with such an offer, in accord with Subsection b above.)
-
-The source code for a work means the preferred form of the work for making
-modifications to it. For an executable work, complete source code means all the
-source code for all modules it contains, plus any associated interface definition
-files, plus the scripts used to control compilation and installation of the
-executable. However, as a special exception, the source code distributed need
-not include anything that is normally distributed (in either source or binary form)
-with the major components (compiler, kernel, and so on) of the operating system
-on which the executable runs, unless that component itself accompanies the
-executable.
-
-If distribution of executable or object code is made by offering access to copy
-from a designated place, then offering equivalent access to copy the source
-code from the same place counts as distribution of the source code, even though
-third parties are not compelled to copy the source along with the object code.
-
-4. You may not copy, modify, sublicense, or distribute the Program except as
-expressly provided under this License. Any attempt otherwise to copy, modify,
-sublicense or distribute the Program is void, and will automatically terminate
-your rights under this License. However, parties who have received copies, or
-rights, from you under this License will not have their licenses terminated so long
-as such parties remain in full compliance.
-
-5. You are not required to accept this License, since you have not signed it.
-However, nothing else grants you permission to modify or distribute the Program
-or its derivative works. These actions are prohibited by law if you do not accept
-this License. Therefore, by modifying or distributing the Program (or any work
-based on the Program), you indicate your acceptance of this License to do so,
-and all its terms and conditions for copying, distributing or modifying the
-Program or works based on it.
-
-6. Each time you redistribute the Program (or any work based on the Program),
-the recipient automatically receives a license from the original licensor to copy,
-distribute or modify the Program subject to these terms and conditions. You
-may not impose any further restrictions on the recipients' exercise of the rights
-granted herein. You are not responsible for enforcing compliance by third parties
-to this License.
-
-7. If, as a consequence of a court judgment or allegation of patent infringement
-or for any other reason (not limited to patent issues), conditions are imposed on
-you (whether by court order, agreement or otherwise) that contradict the
-conditions of this License, they do not excuse you from the conditions of this
-License. If you cannot distribute so as to satisfy simultaneously your obligations
-under this License and any other pertinent obligations, then as a consequence
-you may not distribute the Program at all. For example, if a patent license would
-not permit royalty-free redistribution of the Program by all those who receive
-copies directly or indirectly through you, then the only way you could satisfy
-both it and this License would be to refrain entirely from distribution of the
-Program.
-
-If any portion of this section is held invalid or unenforceable under any particular
-circumstance, the balance of the section is intended to apply and the section as
-a whole is intended to apply in other circumstances.
-
-It is not the purpose of this section to induce you to infringe any patents or other
-property right claims or to contest validity of any such claims; this section has
-the sole purpose of protecting the integrity of the free software distribution
-system, which is implemented by public license practices. Many people have
-made generous contributions to the wide range of software distributed through
-that system in reliance on consistent application of that system; it is up to the
-author/donor to decide if he or she is willing to distribute software through any
-other system and a licensee cannot impose that choice.
-
-This section is intended to make thoroughly clear what is believed to be a
-consequence of the rest of this License.
-
-8. If the distribution and/or use of the Program is restricted in certain countries
-either by patents or by copyrighted interfaces, the original copyright holder who
-places the Program under this License may add an explicit geographical
-distribution limitation excluding those countries, so that distribution is permitted
-only in or among countries not thus excluded. In such case, this License
-incorporates the limitation as if written in the body of this License.
-
-9. The Free Software Foundation may publish revised and/or new versions of the
-General Public License from time to time. Such new versions will be similar in
-spirit to the present version, but may differ in detail to address new problems or
-concerns.
-
-Each version is given a distinguishing version number. If the Program specifies a
-version number of this License which applies to it and "any later version", you
-have the option of following the terms and conditions either of that version or of
-any later version published by the Free Software Foundation. If the Program does
-not specify a version number of this License, you may choose any version ever
-published by the Free Software Foundation.
-
-10. If you wish to incorporate parts of the Program into other free programs
-whose distribution conditions are different, write to the author to ask for
-permission. For software which is copyrighted by the Free Software Foundation,
-write to the Free Software Foundation; we sometimes make exceptions for this.
-Our decision will be guided by the two goals of preserving the free status of all
-derivatives of our free software and of promoting the sharing and reuse of
-software generally.
-
-NO WARRANTY
-
-11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS
-NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE
-COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM
-"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR
-IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
-ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
-PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE,
-YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
-CORRECTION.
-
-12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED
-TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY
-WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS
-PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
-ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM
-(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
-RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY
-OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS
-BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
-
-END OF TERMS AND CONDITIONS
-
-
----------------------------------------------------------------------------
-
-The Artistic License
-
-Preamble
-
-The intent of this document is to state the conditions under which a Package
-may be copied, such that the Copyright Holder maintains some semblance of
-artistic control over the development of the package, while giving the users of the
-package the right to use and distribute the Package in a more-or-less customary
-fashion, plus the right to make reasonable modifications.
-
-Definitions:
-
--    "Package" refers to the collection of files distributed by the Copyright
-     Holder, and derivatives of that collection of files created through textual
-     modification. 
--    "Standard Version" refers to such a Package if it has not been modified,
-     or has been modified in accordance with the wishes of the Copyright
-     Holder. 
--    "Copyright Holder" is whoever is named in the copyright or copyrights for
-     the package. 
--    "You" is you, if you're thinking about copying or distributing this Package.
--    "Reasonable copying fee" is whatever you can justify on the basis of
-     media cost, duplication charges, time of people involved, and so on. (You
-     will not be required to justify it to the Copyright Holder, but only to the
-     computing community at large as a market that must bear the fee.) 
--    "Freely Available" means that no fee is charged for the item itself, though
-     there may be fees involved in handling the item. It also means that
-     recipients of the item may redistribute it under the same conditions they
-     received it. 
-
-1. You may make and give away verbatim copies of the source form of the
-Standard Version of this Package without restriction, provided that you duplicate
-all of the original copyright notices and associated disclaimers.
-
-2. You may apply bug fixes, portability fixes and other modifications derived from
-the Public Domain or from the Copyright Holder. A Package modified in such a
-way shall still be considered the Standard Version.
-
-3. You may otherwise modify your copy of this Package in any way, provided
-that you insert a prominent notice in each changed file stating how and when
-you changed that file, and provided that you do at least ONE of the following:
-
-     a) place your modifications in the Public Domain or otherwise
-     make them Freely Available, such as by posting said modifications
-     to Usenet or an equivalent medium, or placing the modifications on
-     a major archive site such as ftp.uu.net, or by allowing the
-     Copyright Holder to include your modifications in the Standard
-     Version of the Package.
-
-     b) use the modified Package only within your corporation or
-     organization.
-
-     c) rename any non-standard executables so the names do not
-     conflict with standard executables, which must also be provided,
-     and provide a separate manual page for each non-standard
-     executable that clearly documents how it differs from the Standard
-     Version.
-
-     d) make other distribution arrangements with the Copyright Holder.
-
-4. You may distribute the programs of this Package in object code or executable
-form, provided that you do at least ONE of the following:
-
-     a) distribute a Standard Version of the executables and library
-     files, together with instructions (in the manual page or equivalent)
-     on where to get the Standard Version.
-
-     b) accompany the distribution with the machine-readable source of
-     the Package with your modifications.
-
-     c) accompany any non-standard executables with their
-     corresponding Standard Version executables, giving the
-     non-standard executables non-standard names, and clearly
-     documenting the differences in manual pages (or equivalent),
-     together with instructions on where to get the Standard Version.
-
-     d) make other distribution arrangements with the Copyright Holder.
-
-5. You may charge a reasonable copying fee for any distribution of this Package.
-You may charge any fee you choose for support of this Package. You may not
-charge a fee for this Package itself. However, you may distribute this Package in
-aggregate with other (possibly commercial) programs as part of a larger
-(possibly commercial) software distribution provided that you do not advertise
-this Package as a product of your own.
-
-6. The scripts and library files supplied as input to or produced as output from
-the programs of this Package do not automatically fall under the copyright of this
-Package, but belong to whomever generated them, and may be sold
-commercially, and may be aggregated with this Package.
-
-7. C or perl subroutines supplied by you and linked into this Package shall not
-be considered part of this Package.
-
-8. Aggregation of this Package with a commercial distribution is always permitted
-provided that the use of this Package is embedded; that is, when no overt attempt
-is made to make this Package's interfaces visible to the end user of the
-commercial distribution. Such use shall not be construed as a distribution of
-this Package.
-
-9. The name of the Copyright Holder may not be used to endorse or promote
-products derived from this software without specific prior written permission.
-
-10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
-IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
-WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR
-PURPOSE.
-
-The End
-
-
diff --git a/doc/modules/LICENSE.Email-Address b/doc/modules/LICENSE.Email-Address
deleted file mode 100644 (file)
index 8d38927..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-Copyright (c) 2004 Casey West.  All rights reserved.
-
-This module is free software; you can redistribute it and/or modify it
-under the same terms as Perl itself.
-
-Perl is distributed under your choice of the GNU General Public License or
-the Artistic License.
-
-The complete text of the GNU General Public License can be found in
-/usr/share/common-licenses/GPL and the Artistic Licence can be found
-in /usr/share/common-licenses/Artistic.
diff --git a/doc/modules/LICENSE.List-MoreUtils b/doc/modules/LICENSE.List-MoreUtils
deleted file mode 100644 (file)
index eb3a238..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-COPYRIGHT AND LICENSE
-
-Copyright (C) 2004-2006 by Tassilo von Parseval
-
-This library is free software; you can redistribute it and/or modify
-it under the same terms as Perl itself, either Perl version 5.8.4 or,
-at your option, any later version of Perl 5 you may have available.
diff --git a/doc/modules/LICENSE.List-UtilsBy b/doc/modules/LICENSE.List-UtilsBy
deleted file mode 100644 (file)
index 67ba0bf..0000000
+++ /dev/null
@@ -1,378 +0,0 @@
-This software is copyright (c) 2012 by Paul Evans <leonerd@leonerd.org.uk>.
-
-This is free software; you can redistribute it and/or modify it under
-the same terms as the Perl 5 programming language system itself.
-
-Terms of the Perl programming language system itself
-
-a) the GNU General Public License as published by the Free
-   Software Foundation; either version 1, or (at your option) any
-   later version, or
-b) the "Artistic License"
-
---- The GNU General Public License, Version 1, February 1989 ---
-
-This software is Copyright (c) 2012 by Paul Evans <leonerd@leonerd.org.uk>.
-
-This is free software, licensed under:
-
-  The GNU General Public License, Version 1, February 1989
-
-                    GNU GENERAL PUBLIC LICENSE
-                     Version 1, February 1989
-
- Copyright (C) 1989 Free Software Foundation, Inc.
- 51 Franklin St, Suite 500, Boston, MA  02110-1335  USA
-
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-                            Preamble
-
-  The license agreements of most software companies try to keep users
-at the mercy of those companies.  By contrast, our General Public
-License is intended to guarantee your freedom to share and change free
-software--to make sure the software is free for all its users.  The
-General Public License applies to the Free Software Foundation's
-software and to any other program whose authors commit to using it.
-You can use it for your programs, too.
-
-  When we speak of free software, we are referring to freedom, not
-price.  Specifically, the General Public License is designed to make
-sure that you have the freedom to give away or sell copies of free
-software, that you receive source code or can get it if you want it,
-that you can change the software or use pieces of it in new free
-programs; and that you know you can do these things.
-
-  To protect your rights, we need to make restrictions that forbid
-anyone to deny you these rights or to ask you to surrender the rights.
-These restrictions translate to certain responsibilities for you if you
-distribute copies of the software, or if you modify it.
-
-  For example, if you distribute copies of a such a program, whether
-gratis or for a fee, you must give the recipients all the rights that
-you have.  You must make sure that they, too, receive or can get the
-source code.  And you must tell them their rights.
-
-  We protect your rights with two steps: (1) copyright the software, and
-(2) offer you this license which gives you legal permission to copy,
-distribute and/or modify the software.
-
-  Also, for each author's protection and ours, we want to make certain
-that everyone understands that there is no warranty for this free
-software.  If the software is modified by someone else and passed on, we
-want its recipients to know that what they have is not the original, so
-that any problems introduced by others will not reflect on the original
-authors' reputations.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.
-
-                    GNU GENERAL PUBLIC LICENSE
-   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
-  0. This License Agreement applies to any program or other work which
-contains a notice placed by the copyright holder saying it may be
-distributed under the terms of this General Public License.  The
-"Program", below, refers to any such program or work, and a "work based
-on the Program" means either the Program or any work containing the
-Program or a portion of it, either verbatim or with modifications.  Each
-licensee is addressed as "you".
-
-  1. You may copy and distribute verbatim copies of the Program's source
-code as you receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice and
-disclaimer of warranty; keep intact all the notices that refer to this
-General Public License and to the absence of any warranty; and give any
-other recipients of the Program a copy of this General Public License
-along with the Program.  You may charge a fee for the physical act of
-transferring a copy.
-
-  2. You may modify your copy or copies of the Program or any portion of
-it, and copy and distribute such modifications under the terms of Paragraph
-1 above, provided that you also do the following:
-
-    a) cause the modified files to carry prominent notices stating that
-    you changed the files and the date of any change; and
-
-    b) cause the whole of any work that you distribute or publish, that
-    in whole or in part contains the Program or any part thereof, either
-    with or without modifications, to be licensed at no charge to all
-    third parties under the terms of this General Public License (except
-    that you may choose to grant warranty protection to some or all
-    third parties, at your option).
-
-    c) If the modified program normally reads commands interactively when
-    run, you must cause it, when started running for such interactive use
-    in the simplest and most usual way, to print or display an
-    announcement including an appropriate copyright notice and a notice
-    that there is no warranty (or else, saying that you provide a
-    warranty) and that users may redistribute the program under these
-    conditions, and telling the user how to view a copy of this General
-    Public License.
-
-    d) You may charge a fee for the physical act of transferring a
-    copy, and you may at your option offer warranty protection in
-    exchange for a fee.
-
-Mere aggregation of another independent work with the Program (or its
-derivative) on a volume of a storage or distribution medium does not bring
-the other work under the scope of these terms.
-
-  3. You may copy and distribute the Program (or a portion or derivative of
-it, under Paragraph 2) in object code or executable form under the terms of
-Paragraphs 1 and 2 above provided that you also do one of the following:
-
-    a) accompany it with the complete corresponding machine-readable
-    source code, which must be distributed under the terms of
-    Paragraphs 1 and 2 above; or,
-
-    b) accompany it with a written offer, valid for at least three
-    years, to give any third party free (except for a nominal charge
-    for the cost of distribution) a complete machine-readable copy of the
-    corresponding source code, to be distributed under the terms of
-    Paragraphs 1 and 2 above; or,
-
-    c) accompany it with the information you received as to where the
-    corresponding source code may be obtained.  (This alternative is
-    allowed only for noncommercial distribution and only if you
-    received the program in object code or executable form alone.)
-
-Source code for a work means the preferred form of the work for making
-modifications to it.  For an executable file, complete source code means
-all the source code for all modules it contains; but, as a special
-exception, it need not include source code for modules which are standard
-libraries that accompany the operating system on which the executable
-file runs, or for standard header files or definitions files that
-accompany that operating system.
-
-  4. You may not copy, modify, sublicense, distribute or transfer the
-Program except as expressly provided under this General Public License.
-Any attempt otherwise to copy, modify, sublicense, distribute or transfer
-the Program is void, and will automatically terminate your rights to use
-the Program under this License.  However, parties who have received
-copies, or rights to use copies, from you under this General Public
-License will not have their licenses terminated so long as such parties
-remain in full compliance.
-
-  5. By copying, distributing or modifying the Program (or any work based
-on the Program) you indicate your acceptance of this license to do so,
-and all its terms and conditions.
-
-  6. Each time you redistribute the Program (or any work based on the
-Program), the recipient automatically receives a license from the original
-licensor to copy, distribute or modify the Program subject to these
-terms and conditions.  You may not impose any further restrictions on the
-recipients' exercise of the rights granted herein.
-
-  7. The Free Software Foundation may publish revised and/or new versions
-of the General Public License from time to time.  Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-Each version is given a distinguishing version number.  If the Program
-specifies a version number of the license which applies to it and "any
-later version", you have the option of following the terms and conditions
-either of that version or of any later version published by the Free
-Software Foundation.  If the Program does not specify a version number of
-the license, you may choose any version ever published by the Free Software
-Foundation.
-
-  8. If you wish to incorporate parts of the Program into other free
-programs whose distribution conditions are different, write to the author
-to ask for permission.  For software which is copyrighted by the Free
-Software Foundation, write to the Free Software Foundation; we sometimes
-make exceptions for this.  Our decision will be guided by the two goals
-of preserving the free status of all derivatives of our free software and
-of promoting the sharing and reuse of software generally.
-
-                            NO WARRANTY
-
-  9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
-FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
-OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
-PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
-OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
-TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
-PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
-REPAIR OR CORRECTION.
-
-  10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
-REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
-INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
-OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
-TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
-YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
-PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGES.
-
-                     END OF TERMS AND CONDITIONS
-
-        Appendix: How to Apply These Terms to Your New Programs
-
-  If you develop a new program, and you want it to be of the greatest
-possible use to humanity, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these
-terms.
-
-  To do so, attach the following notices to the program.  It is safest to
-attach them to the start of each source file to most effectively convey
-the exclusion of warranty; and each file should have at least the
-"copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) 19yy  <name of author>
-
-    This program is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 1, or (at your option)
-    any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with this program; if not, write to the Free Software
-    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA  02110-1301 USA
-
-
-Also add information on how to contact you by electronic and paper mail.
-
-If the program is interactive, make it output a short notice like this
-when it starts in an interactive mode:
-
-    Gnomovision version 69, Copyright (C) 19xx name of author
-    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
-    This is free software, and you are welcome to redistribute it
-    under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the
-appropriate parts of the General Public License.  Of course, the
-commands you use may be called something other than `show w' and `show
-c'; they could even be mouse-clicks or menu items--whatever suits your
-program.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the program, if
-necessary.  Here a sample; alter the names:
-
-  Yoyodyne, Inc., hereby disclaims all copyright interest in the
-  program `Gnomovision' (a program to direct compilers to make passes
-  at assemblers) written by James Hacker.
-
-  <signature of Ty Coon>, 1 April 1989
-  Ty Coon, President of Vice
-
-That's all there is to it!
-
-
---- The Artistic License 1.0 ---
-
-This software is Copyright (c) 2012 by Paul Evans <leonerd@leonerd.org.uk>.
-
-This is free software, licensed under:
-
-  The Artistic License 1.0
-
-The Artistic License
-
-Preamble
-
-The intent of this document is to state the conditions under which a Package
-may be copied, such that the Copyright Holder maintains some semblance of
-artistic control over the development of the package, while giving the users of
-the package the right to use and distribute the Package in a more-or-less
-customary fashion, plus the right to make reasonable modifications.
-
-Definitions:
-
-  - "Package" refers to the collection of files distributed by the Copyright
-    Holder, and derivatives of that collection of files created through
-    textual modification.
-  - "Standard Version" refers to such a Package if it has not been modified,
-    or has been modified in accordance with the wishes of the Copyright
-    Holder.
-  - "Copyright Holder" is whoever is named in the copyright or copyrights for
-    the package.
-  - "You" is you, if you're thinking about copying or distributing this Package.
-  - "Reasonable copying fee" is whatever you can justify on the basis of media
-    cost, duplication charges, time of people involved, and so on. (You will
-    not be required to justify it to the Copyright Holder, but only to the
-    computing community at large as a market that must bear the fee.)
-  - "Freely Available" means that no fee is charged for the item itself, though
-    there may be fees involved in handling the item. It also means that
-    recipients of the item may redistribute it under the same conditions they
-    received it.
-
-1. You may make and give away verbatim copies of the source form of the
-Standard Version of this Package without restriction, provided that you
-duplicate all of the original copyright notices and associated disclaimers.
-
-2. You may apply bug fixes, portability fixes and other modifications derived
-from the Public Domain or from the Copyright Holder. A Package modified in such
-a way shall still be considered the Standard Version.
-
-3. You may otherwise modify your copy of this Package in any way, provided that
-you insert a prominent notice in each changed file stating how and when you
-changed that file, and provided that you do at least ONE of the following:
-
-  a) place your modifications in the Public Domain or otherwise make them
-     Freely Available, such as by posting said modifications to Usenet or an
-     equivalent medium, or placing the modifications on a major archive site
-     such as ftp.uu.net, or by allowing the Copyright Holder to include your
-     modifications in the Standard Version of the Package.
-
-  b) use the modified Package only within your corporation or organization.
-
-  c) rename any non-standard executables so the names do not conflict with
-     standard executables, which must also be provided, and provide a separate
-     manual page for each non-standard executable that clearly documents how it
-     differs from the Standard Version.
-
-  d) make other distribution arrangements with the Copyright Holder.
-
-4. You may distribute the programs of this Package in object code or executable
-form, provided that you do at least ONE of the following:
-
-  a) distribute a Standard Version of the executables and library files,
-     together with instructions (in the manual page or equivalent) on where to
-     get the Standard Version.
-
-  b) accompany the distribution with the machine-readable source of the Package
-     with your modifications.
-
-  c) accompany any non-standard executables with their corresponding Standard
-     Version executables, giving the non-standard executables non-standard
-     names, and clearly documenting the differences in manual pages (or
-     equivalent), together with instructions on where to get the Standard
-     Version.
-
-  d) make other distribution arrangements with the Copyright Holder.
-
-5. You may charge a reasonable copying fee for any distribution of this
-Package.  You may charge any fee you choose for support of this Package. You
-may not charge a fee for this Package itself. However, you may distribute this
-Package in aggregate with other (possibly commercial) programs as part of a
-larger (possibly commercial) software distribution provided that you do not
-advertise this Package as a product of your own.
-
-6. The scripts and library files supplied as input to or produced as output
-from the programs of this Package do not automatically fall under the copyright
-of this Package, but belong to whomever generated them, and may be sold
-commercially, and may be aggregated with this Package.
-
-7. C or perl subroutines supplied by you and linked into this Package shall not
-be considered part of this Package.
-
-8. The name of the Copyright Holder may not be used to endorse or promote
-products derived from this software without specific prior written permission.
-
-9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
-WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
-MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
-
-The End
diff --git a/doc/modules/README.CGI-Ajax b/doc/modules/README.CGI-Ajax
deleted file mode 100644 (file)
index 1af8860..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-pod2text CGI::Perljax.pm > README
-
-CGI::Perljax
-
-Perljax - a perl-specific system for writing AJAX- or
-DHTML-based web applications.
-
-
-Perljax provides a unique mechanism for using perl code
-asynchronously from javascript using AJAX to access user-written
-perl functions/methods. Perljax unburdens the user from having to
-write any javascript, except for having to associate an exported
-method with a document-defined event (such as onClick, onKeyUp,
-etc). Only in the more advanced implementations of a exported perl
-method would a user need to write custom javascript. Perljax supports
-methods that return single results, or multiple results to the web
-page. No other projects that we know of are like Perljax for the
-following reasons: 1. Perljax is targeted specifically for perl
-development. 2. Perljax shields the user from having to write any
-javascript at all (unless they want to).  3. The URL for the HTTP GET
-request is automatically generated based on HTML layout and events,
-and the page is then dynamically updated.  4. Perljax is not part
-of a Content Management System, or some other larger project.
-
-
-INSTALL
-
-perl Makefile.PL
-make
-make test
-make install
-
-*If you are on a windows box you should use 'nmake' rather than 'make'.
-
-Installation will place Perljax into the system perl @INC path, but it
-is important that you make sure mod_perl uses this path (which is
-mod_perl's default behavior, and also assuming you use mod_perl, and
-not just run perl as a CGI).
-
-Example scripts are provided in the source script directory, and can
-also be seen on the project's website, http://www.perljax.us.
diff --git a/doc/modules/README.File-Slurp b/doc/modules/README.File-Slurp
deleted file mode 100644 (file)
index 1a7a9d4..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-File::Slurp.pm version 0.04
-===========================
-
-This module provides subroutines to read or write entire files with a
-simple call.  It also has a subroutine for reading the list of filenames
-in a directory.
-
-In the extras/ directory you can read an article (slurp_article.pod)
-about file slurping and also run a benchmark (slurp_bench.pl) that
-compares many ways of slurping/spewing files.
-
-This module was first written and owned by David Muir Sharnoff (MUIR on
-CPAN).  I checked out his module and decided to write a new version
-which would be faster, and with many more features.  To that end, David
-graciously transfered the namespace to me.
-
-Since then, I discovered and fixed a bug in the original module's test
-script (which had only 7 tests), which is included now as t/original.t.
-This module now has 164 tests in 7 test scripts, and passes on Windows,
-Linux, Solaris and Mac OS X.
-
-There have been some comments about the somewhat unusual version number.
-The problem was that David used a future date (2004.0904) in his version
-number, and the only way I could get CPAN to index my new module was to
-make it have a version number higher than the old one, so I chose the
-9999 prefix and appended the real revision number to it.
-
-INSTALLATION
-
-To install this module type the following:
-
-   perl Makefile.PL
-   make
-   make test
-   make install
-
-COPYRIGHT AND LICENCE
-
-Copyright (C) 2003 Uri Guttman <uri@stemsystems.com>
-
-Licensed the same as Perl.
diff --git a/doc/modules/README.List-UtilsBy b/doc/modules/README.List-UtilsBy
deleted file mode 100644 (file)
index efdceb3..0000000
+++ /dev/null
@@ -1,238 +0,0 @@
-NAME
-    `List::UtilsBy' - higher-order list utility functions
-
-SYNOPSIS
-     use List::UtilsBy qw( nsort_by min_by );
-
-     use File::stat qw( stat );
-     my @files_by_age = nsort_by { stat($_)->mtime } @files;
-
-     my $shortest_name = min_by { length } @names;
-
-DESCRIPTION
-    This module provides a number of list utility functions, all of which
-    take an initial code block to control their behaviour. They are
-    variations on similar core perl or `List::Util' functions of similar
-    names, but which use the block to control their behaviour. For example,
-    the core Perl function `sort' takes a list of values and returns them,
-    sorted into order by their string value. The `sort_by' function sorts
-    them according to the string value returned by the extra function, when
-    given each value.
-
-     my @names_sorted = sort @names;
-
-     my @people_sorted = sort_by { $_->name } @people;
-
-FUNCTIONS
-  @vals = sort_by { KEYFUNC } @vals
-    Returns the list of values sorted according to the string values
-    returned by the `KEYFUNC' block or function. A typical use of this may
-    be to sort objects according to the string value of some accessor, such
-    as
-
-     sort_by { $_->name } @people
-
-    The key function is called in scalar context, being passed each value in
-    turn as both `$_' and the only argument in the parameters, `@_'. The
-    values are then sorted according to string comparisons on the values
-    returned.
-
-    This is equivalent to
-
-     sort { $a->name cmp $b->name } @people
-
-    except that it guarantees the `name' accessor will be executed only once
-    per value.
-
-    One interesting use-case is to sort strings which may have numbers
-    embedded in them "naturally", rather than lexically.
-
-     sort_by { s/(\d+)/sprintf "%09d", $1/eg; $_ } @strings
-
-    This sorts strings by generating sort keys which zero-pad the embedded
-    numbers to some level (9 digits in this case), helping to ensure the
-    lexical sort puts them in the correct order.
-
-  @vals = nsort_by { KEYFUNC } @vals
-    Similar to `sort_by' but compares its key values numerically.
-
-  @vals = rev_sort_by { KEYFUNC } @vals
-  @vals = rev_nsort_by { KEYFUNC } @vals
-    Similar to `sort_by' and `nsort_by' but returns the list in the reverse
-    order. Equivalent to
-
-     @vals = reverse sort_by { KEYFUNC } @vals
-
-    except that these functions are slightly more efficient because they
-    avoid the final `reverse' operation.
-
-  $optimal = max_by { KEYFUNC } @vals
-  @optimal = max_by { KEYFUNC } @vals
-    Returns the (first) value from `@vals' that gives the numerically
-    largest result from the key function.
-
-     my $tallest = max_by { $_->height } @people
-
-     use File::stat qw( stat );
-     my $newest = max_by { stat($_)->mtime } @files;
-
-    In scalar context, the first maximal value is returned. In list context,
-    a list of all the maximal values is returned. This may be used to obtain
-    positions other than the first, if order is significant.
-
-    If called on an empty list, an empty list is returned.
-
-    For symmetry with the `nsort_by' function, this is also provided under
-    the name `nmax_by' since it behaves numerically.
-
-  $optimal = min_by { KEYFUNC } @vals
-  @optimal = min_by { KEYFUNC } @vals
-    Similar to `max_by' but returns values which give the numerically
-    smallest result from the key function. Also provided as `nmin_by'
-
-  @vals = uniq_by { KEYFUNC } @vals
-    Returns a list of the subset of values for which the key function block
-    returns unique values. The first value yielding a particular key is
-    chosen, subsequent values are rejected.
-
-     my @some_fruit = uniq_by { $_->colour } @fruit;
-
-    To select instead the last value per key, reverse the input list. If the
-    order of the results is significant, don't forget to reverse the result
-    as well:
-
-     my @some_fruit = reverse uniq_by { $_->colour } reverse @fruit;
-
-  %parts = partition_by { KEYFUNC } @vals
-    Returns a key/value list of ARRAY refs containing all the original
-    values distributed according to the result of the key function block.
-    Each value will be an ARRAY ref containing all the values which returned
-    the string from the key function, in their original order.
-
-     my %balls_by_colour = partition_by { $_->colour } @balls;
-
-    Because the values returned by the key function are used as hash keys,
-    they ought to either be strings, or at least well-behaved as strings
-    (such as numbers, or object references which overload stringification in
-    a suitable manner).
-
-  %counts = count_by { KEYFUNC } @vals
-    Returns a key/value list of integers, giving the number of times the key
-    function block returned the key, for each value in the list.
-
-     my %count_of_balls = count_by { $_->colour } @balls;
-
-    Because the values returned by the key function are used as hash keys,
-    they ought to either be strings, or at least well-behaved as strings
-    (such as numbers, or object references which overload stringification in
-    a suitable manner).
-
-  @vals = zip_by { ITEMFUNC } \@arr0, \@arr1, \@arr2,...
-    Returns a list of each of the values returned by the function block,
-    when invoked with values from across each each of the given ARRAY
-    references. Each value in the returned list will be the result of the
-    function having been invoked with arguments at that position, from
-    across each of the arrays given.
-
-     my @transposition = zip_by { [ @_ ] } @matrix;
-
-     my @names = zip_by { "$_[1], $_[0]" } \@firstnames, \@surnames;
-
-     print zip_by { "$_[0] => $_[1]\n" } [ keys %hash ], [ values %hash ];
-
-    If some of the arrays are shorter than others, the function will behave
-    as if they had `undef' in the trailing positions. The following two
-    lines are equivalent:
-
-     zip_by { f(@_) } [ 1, 2, 3 ], [ "a", "b" ]
-     f( 1, "a" ), f( 2, "b" ), f( 3, undef )
-
-    The item function is called by `map', so if it returns a list, the
-    entire list is included in the result. This can be useful for example,
-    for generating a hash from two separate lists of keys and values
-
-     my %nums = zip_by { @_ } [qw( one two three )], [ 1, 2, 3 ];
-     # %nums = ( one => 1, two => 2, three => 3 )
-
-    (A function having this behaviour is sometimes called `zipWith', e.g. in
-    Haskell, but that name would not fit the naming scheme used by this
-    module).
-
-  $arr0, $arr1, $arr2, ... = unzip_by { ITEMFUNC } @vals
-    Returns a list of ARRAY references containing the values returned by the
-    function block, when invoked for each of the values given in the input
-    list. Each of the returned ARRAY references will contain the values
-    returned at that corresponding position by the function block. That is,
-    the first returned ARRAY reference will contain all the values returned
-    in the first position by the function block, the second will contain all
-    the values from the second position, and so on.
-
-     my ( $firstnames, $lastnames ) = unzip_by { m/^(.*?) (.*)$/ } @names;
-
-    If the function returns lists of differing lengths, the result will be
-    padded with `undef' in the missing elements.
-
-    This function is an inverse of `zip_by', if given a corresponding
-    inverse function.
-
-  @vals = extract_by { SELECTFUNC } @arr
-    Removes elements from the referenced array on which the selection
-    function returns true, and returns a list containing those elements.
-    This function is similar to `grep', except that it modifies the
-    referenced array to remove the selected values from it, leaving only the
-    unselected ones.
-
-     my @red_balls = extract_by { $_->color eq "red" } @balls;
-
-     # Now there are no red balls in the @balls array
-
-    This function modifies a real array, unlike most of the other functions
-    in this module. Because of this, it requires a real array, not just a
-    list.
-
-    This function is implemented by invoking `splice()' on the array, not by
-    constructing a new list and assigning it. One result of this is that
-    weak references will not be disturbed.
-
-     extract_by { !defined $_ } @refs;
-
-    will leave weak references weakened in the `@refs' array, whereas
-
-     @refs = grep { defined $_ } @refs;
-
-    will strengthen them all again.
-
-  @vals = weighted_shuffle_by { WEIGHTFUNC } @vals
-    Returns the list of values shuffled into a random order. The
-    randomisation is not uniform, but weighted by the value returned by the
-    `WEIGHTFUNC'. The probabilty of each item being returned first will be
-    distributed with the distribution of the weights, and so on recursively
-    for the remaining items.
-
-  @vals = bundle_by { BLOCKFUNC } $number, @vals
-    Similar to a regular `map' functional, returns a list of the values
-    returned by `BLOCKFUNC'. Values from the input list are given to the
-    block function in bundles of `$number'.
-
-    If given a list of values whose length does not evenly divide by
-    `$number', the final call will be passed fewer elements than the others.
-
-TODO
-    * XS implementations
-        These functions are currently all written in pure perl. Some at
-        least, may benefit from having XS implementations to speed up their
-        logic.
-
-    * Merge into List::Util or List::MoreUtils
-        This module shouldn't really exist. The functions should instead be
-        part of one of the existing modules that already contain many list
-        utility functions. Having Yet Another List Utilty Module just
-        worsens the problem.
-
-        I have attempted to contact the authors of both of the above
-        modules, to no avail; therefore I decided it best to write and
-        release this code here anyway so that it is at least on CPAN. Once
-        there, we can then see how best to merge it into an existing module.
-
-AUTHOR
-    Paul Evans <leonerd@leonerd.org.uk>
index c1cc623..31442a9 100644 (file)
@@ -1,39 +1,36 @@
-PDF-Table version 0.9.3
-=====================
-SOME NOTES
+# PDF::Table
 
-This module is intended for table generation using PDF::API2
-The current version is RC1 and I will apreciate any feedback.
-Developed and tested on i586 Linux SuSE 10.0 and perl, v5.8.7 built for i586-linux-thread-multi
+This module creates text blocks and tables into PDF documents using PDF::API2 Perl module.
 
-CHANGES
+The official repository for PDF::Table module collaboration:
+https://github.com/kamenov/PDF-Table.git
 
-Since version 0.02 there are many changes. 
-See the ChangeLog file or make a diff from the tools menu in CPAN
+Any patches, pull requests, issues and feedback are more than welcome.
 
-CONTACTS 
+## Installation
 
-See http://search.cpan.org/~omega/
+To install the module from CPAN, please type the following command:
 
-INSTALLATION
+```cpanm PDF::Table```
 
-To install this module type the following:
+To test or add features to this module, please type the following command:
 
-   perl Makefile.PL
-   make
-   make test
-   make install
+```cpanm .```
 
-DEPENDENCIES
+## Changes
+To see a list of changes, please do one or more of the following:
+- Read the [Changes](Changes) file
+- Review commits history on GitHub
+- Make a diff from the tools menu at CPAN
 
-This module requires these other modules and libraries:
+## Contacts 
+@deskata on Twitter 
 
-  PDF::API2
-
-COPYRIGHT AND LICENCE
-
-Put the correct copyright and licence information here.
+- Use the issue tracker on GitHub
+- See http://search.cpan.org/~omega/
+- See http://search.cpan.org/~jbazik/
 
+## License
 Copyright (C) 2006 by Daemmon Hughes
 
 Extended by Desislav Kamenov since version 0.02
@@ -41,5 +38,3 @@ Extended by Desislav Kamenov since version 0.02
 This library is free software; you can redistribute it and/or modify
 it under the same terms as Perl itself, either Perl version 5.8.7 or,
 at your option, any later version of Perl 5 you may have available.
-
-
diff --git a/doc/modules/README.Sort-Naturally b/doc/modules/README.Sort-Naturally
deleted file mode 100644 (file)
index 4fa4f1e..0000000
+++ /dev/null
@@ -1,124 +0,0 @@
-README for Sort::Naturally
-                                        Time-stamp: "2001-05-25 21:17:33 MDT"
-
-                           Sort::Naturally
-
-[extracted from the Pod...]
-
-NAME
-     Sort::Naturally -- sort lexically, but sort numeral parts
-     numerically
-
-SYNOPSIS
-       @them = nsort(qw(
-        foo12a foo12z foo13a foo 14 9x foo12 fooa foolio Foolio Foo12a
-       ));
-       print join(' ', @them), "\n";
-
-     Prints:
-
-       9x 14 foo fooa foolio Foolio foo12 foo12a Foo12a foo12z foo13a
-
-     (Or "foo12a" + "Foo12a" and "foolio" + "Foolio" and might be
-     switched, depending on your locale.)
-
-DESCRIPTION
-     This module exports two functions, nsort and ncmp; they are
-     used in implementing my idea of a "natural sorting"
-     algorithm.  Under natural sorting, numeric substrings are
-     compared numerically, and other word-characters are compared
-     lexically.
-
-     This is the way I define natural sorting:
-
-     o    Non-numeric word-character substrings are sorted
-          lexically, case-insensitively: "Foo" comes between
-          "fish" and "fowl".
-
-     o    Numeric substrings are sorted numerically:  "100" comes
-          after "20", not before.
-
-     o    \W substrings (neither words-characters nor digits) are
-          ignored.
-
-     o    Our use of \w, \d, \D, and \W is locale-sensitive:
-          Sort::Naturally uses a use locale statement.
-
-     o    When comparing two strings, where a numeric substring
-          in one place is not up against a numeric substring in
-          another, the non-numeric always comes first.  This is
-          fudged by reading pretending that the lack of a number
-          substring has the value -1, like so:
-
-            foo       =>  "foo",  -1
-            foobar    =>  "foo",  -1,  "bar"
-            foo13     =>  "foo",  13,
-            foo13xyz  =>  "foo",  13,  "xyz"
-
-          That's so that "foo" will come before "foo13", which
-          will come before "foobar".
-
-     o    The start of a string is exceptional: leading non-\W
-          (non-word, non-digit) components are are ignored, and
-          numbers come before letters.
-
-     o    I define "numeric substring" just as sequences matching
-          m/\d+/ -- scientific notation, commas, decimals, etc.,
-          are not seen.  If your data has thousands separators in
-          numbers ("20,000 Leagues Under The Sea" or "20.000
-          lieues sous les mers"), consider stripping them before
-          feeding them to nsort or ncmp.
-
-[end Pod extract]
-
-
-INSTALLATION
-
-You install Sort::Naturally, as you would install any perl module
-library, by running these commands:
-
-   perl Makefile.PL
-   make
-   make test
-   make install
-
-If you want to install a private copy of Sort::Naturally in your home
-directory, then you should try to produce the initial Makefile with
-something like this command:
-
-  perl Makefile.PL LIB=~/perl
-
-See perldoc perlmodinstall for more information on installing modules.
-
-
-DOCUMENTATION
-
-POD-format documentation is included in Naturally.pm.  POD is readable
-with the 'perldoc' utility.  See ChangeLog for recent changes.
-
-
-SUPPORT
-
-Questions, bug reports, useful code bits, and suggestions for
-Sort::Naturally should just be sent to me at sburke@cpan.org
-
-
-AVAILABILITY
-
-The latest version of Sort::Naturally is available from the
-Comprehensive Perl Archive Network (CPAN).  Visit
-<http://www.perl.com/CPAN/> to find a CPAN site near you.
-
-
-COPYRIGHT
-
-Copyright 2001, Sean M. Burke <sburke@cpan.org>, all rights
-reserved.
-
-The programs and documentation in this dist are distributed in
-the hope that they will be useful, but without any warranty; without
-even the implied warranty of merchantability or fitness for a
-particular purpose.
-
-This library is free software; you can redistribute it and/or modify
-it under the same terms as Perl itself.
diff --git a/doc/modules/README.YAML b/doc/modules/README.YAML
deleted file mode 100644 (file)
index 0fbb2fd..0000000
+++ /dev/null
@@ -1,611 +0,0 @@
-NAME
-    YAML - YAML Ain't Markup Language (tm)
-
-SYNOPSIS
-        use YAML;
-    
-        # Load a YAML stream of 3 YAML documents into Perl data structures.
-        my ($hashref, $arrayref, $string) = Load(<<'...');
-        ---
-        name: ingy
-        age: old
-        weight: heavy
-        # I should comment that I also like pink, but don't tell anybody.
-        favorite colors:
-            - red
-            - green
-            - blue
-        ---
-        - Clark Evans
-        - Oren Ben-Kiki
-        - Ingy döt Net
-        --- >
-        You probably think YAML stands for "Yet Another Markup Language". It
-        ain't! YAML is really a data serialization language. But if you want
-        to think of it as a markup, that's OK with me. A lot of people try
-        to use XML as a serialization format.
-    
-        "YAML" is catchy and fun to say. Try it. "YAML, YAML, YAML!!!"
-        ...
-    
-        # Dump the Perl data structures back into YAML.
-        print Dump($string, $arrayref, $hashref); 
-    
-        # YAML::Dump is used the same way you'd use Data::Dumper::Dumper
-        use Data::Dumper;
-        print Dumper($string, $arrayref, $hashref); 
-
-DESCRIPTION
-    The YAML.pm module implements a YAML Loader and Dumper based on the YAML
-    1.0 specification. <http://www.yaml.org/spec/>
-
-    YAML is a generic data serialization language that is optimized for
-    human readability. It can be used to express the data structures of most
-    modern programming languages. (Including Perl!!!)
-
-    For information on the YAML syntax, please refer to the YAML
-    specification.
-
-WHY YAML IS COOL
-    YAML is readable for people.
-        It makes clear sense out of complex data structures. You should find
-        that YAML is an exceptional data dumping tool. Structure is shown
-        through indentation, YAML supports recursive data, and hash keys are
-        sorted by default. In addition, YAML supports several styles of
-        scalar formatting for different types of data.
-
-    YAML is editable.
-        YAML was designed from the ground up to be an excellent syntax for
-        configuration files. Almost all programs need configuration files,
-        so why invent a new syntax for each one? And why subject users to
-        the complexities of XML or native Perl code?
-
-    YAML is multilingual.
-        Yes, YAML supports Unicode. But I'm actually referring to
-        programming languages. YAML was designed to meet the serialization
-        needs of Perl, Python, Ruby, Tcl, PHP, Javascript and Java. It was
-        also designed to be interoperable between those languages. That
-        means YAML serializations produced by Perl can be processed by
-        Python.
-
-    YAML is taint safe.
-        Using modules like Data::Dumper for serialization is fine as long as
-        you can be sure that nobody can tamper with your data files or
-        transmissions. That's because you need to use Perl's "eval()"
-        built-in to deserialize the data. Somebody could add a snippet of
-        Perl to erase your files.
-
-        YAML's parser does not need to eval anything.
-
-    YAML is full featured.
-        YAML can accurately serialize all of the common Perl data structures
-        and deserialize them again without losing data relationships.
-        Although it is not 100% perfect (no serializer is or can be
-        perfect), it fares as well as the popular current modules:
-        Data::Dumper, Storable, XML::Dumper and Data::Denter.
-
-        YAML.pm also has the ability to handle code (subroutine) references
-        and typeglobs. (Still experimental) These features are not found in
-        Perl's other serialization modules.
-
-    YAML is extensible.
-        The YAML language has been designed to be flexible enough to solve
-        it's own problems. The markup itself has 3 basic construct which
-        resemble Perl's hash, array and scalar. By default, these map to
-        their Perl equivalents. But each YAML node also supports a tagging
-        mechanism (type system) which can cause that node to be interpreted
-        in a completely different manner. That's how YAML can support object
-        serialization and oddball structures like Perl's typeglob.
-
-YAML IMPLEMENTATIONS IN PERL
-    This module, YAML.pm, is really just the interface module for YAML
-    modules written in Perl. The basic interface for YAML consists of two
-    functions: "Dump" and "Load". The real work is done by the modules
-    YAML::Dumper and YAML::Loader.
-
-    Different YAML module distributions can be created by subclassing
-    YAML.pm and YAML::Loader and YAML::Dumper. For example, YAML-Simple
-    consists of YAML::Simple YAML::Dumper::Simple and YAML::Loader::Simple.
-
-    Why would there be more than one implementation of YAML? Well, despite
-    YAML's offering of being a simple data format, YAML is actually very
-    deep and complex. Implementing the entirety of the YAML specification is
-    a daunting task.
-
-    For this reason I am currently working on 3 different YAML
-    implementations.
-
-    YAML
-        The main YAML distribution will keeping evolving to support the
-        entire YAML specification in pure Perl. This may not be the fastest
-        or most stable module though. Currently, YAML.pm has lots of known
-        bugs. It is mostly a great tool for dumping Perl data structures to
-        a readable form.
-
-    YAML::Lite
-        The point of YAML::Lite is to strip YAML down to the 90% that people
-        use most and offer that in a small, fast, stable, pure Perl form.
-        YAML::Lite will simply die when it is asked to do something it
-        can't.
-
-    YAML::Syck
-        "libsyck" is the C based YAML processing library used by the Ruby
-        programming language (and also Python, PHP and Pugs). YAML::Syck is
-        the Perl binding to "libsyck". It should be very fast, but may have
-        problems of its own. It will also require C compilation.
-
-        NOTE: Audrey Tang has actually completed this module and it works
-        great and is 10 times faster than YAML.pm.
-
-    In the future, there will likely be even more YAML modules. Remember,
-    people other than Ingy are allowed to write YAML modules!
-
-FUNCTIONAL USAGE
-    YAML is completely OO under the hood. Still it exports a few useful top
-    level functions so that it is dead simple to use. These functions just
-    do the OO stuff for you. If you want direct access to the OO API see the
-    documentation for YAML::Dumper and YAML::Loader.
-
-  Exported Functions
-    The following functions are exported by YAML.pm by default. The reason
-    they are exported is so that YAML works much like Data::Dumper. If you
-    don't want functions to be imported, just use YAML with an empty import
-    list:
-
-        use YAML ();
-
-    Dump(list-of-Perl-data-structures)
-        Turn Perl data into YAML. This function works very much like
-        Data::Dumper::Dumper(). It takes a list of Perl data strucures and
-        dumps them into a serialized form. It returns a string containing
-        the YAML stream. The structures can be references or plain scalars.
-
-    Load(string-containing-a-YAML-stream)
-        Turn YAML into Perl data. This is the opposite of Dump. Just like
-        Storable's thaw() function or the eval() function in relation to
-        Data::Dumper. It parses a string containing a valid YAML stream into
-        a list of Perl data structures.
-
-  Exportable Functions
-    These functions are not exported by default but you can request them in
-    an import list like this:
-
-        use YAML qw'freeze thaw Bless';
-
-    freeze() and thaw()
-        Aliases to Dump() and Load() for Storable fans. This will also allow
-        YAML.pm to be plugged directly into modules like POE.pm, that use
-        the freeze/thaw API for internal serialization.
-
-    DumpFile(filepath, list)
-        Writes the YAML stream to a file instead of just returning a string.
-
-    LoadFile(filepath)
-        Reads the YAML stream from a file instead of a string.
-
-    Bless(perl-node, [yaml-node | class-name])
-        Associate a normal Perl node, with a yaml node. A yaml node is an
-        object tied to the YAML::Node class. The second argument is either a
-        yaml node that you've already created or a class (package) name that
-        supports a yaml_dump() function. A yaml_dump() function should take
-        a perl node and return a yaml node. If no second argument is
-        provided, Bless will create a yaml node. This node is not returned,
-        but can be retrieved with the Blessed() function.
-
-        Here's an example of how to use Bless. Say you have a hash
-        containing three keys, but you only want to dump two of them.
-        Furthermore the keys must be dumped in a certain order. Here's how
-        you do that:
-
-            use YAML qw(Dump Bless);
-            $hash = {apple => 'good', banana => 'bad', cauliflower => 'ugly'};
-            print Dump $hash;
-            Bless($hash)->keys(['banana', 'apple']);
-            print Dump $hash;
-
-        produces:
-
-            ---
-            apple: good
-            banana: bad
-            cauliflower: ugly
-            ---
-            banana: bad
-            apple: good
-
-        Bless returns the tied part of a yaml-node, so that you can call the
-        YAML::Node methods. This is the same thing that YAML::Node::ynode()
-        returns. So another way to do the above example is:
-
-            use YAML qw(Dump Bless);
-            use YAML::Node;
-            $hash = {apple => 'good', banana => 'bad', cauliflower => 'ugly'};
-            print Dump $hash;
-            Bless($hash);
-            $ynode = ynode(Blessed($hash));
-            $ynode->keys(['banana', 'apple']);
-            print Dump $hash;
-
-        Note that Blessing a Perl data structure does not change it anyway.
-        The extra information is stored separately and looked up by the
-        Blessed node's memory address.
-
-    Blessed(perl-node)
-        Returns the yaml node that a particular perl node is associated with
-        (see above). Returns undef if the node is not (YAML) Blessed.
-
-GLOBAL OPTIONS
-    YAML options are set using a group of global variables in the YAML
-    namespace. This is similar to how Data::Dumper works.
-
-    For example, to change the indentation width, do something like:
-
-        local $YAML::Indent = 3;
-
-    The current options are:
-
-    DumperClass
-        You can override which module/class YAML uses for Dumping data.
-
-    LoaderClass
-        You can override which module/class YAML uses for Loading data.
-
-    Indent
-        This is the number of space characters to use for each indentation
-        level when doing a Dump(). The default is 2.
-
-        By the way, YAML can use any number of characters for indentation at
-        any level. So if you are editing YAML by hand feel free to do it
-        anyway that looks pleasing to you; just be consistent for a given
-        level.
-
-    SortKeys
-        Default is 1. (true)
-
-        Tells YAML.pm whether or not to sort hash keys when storing a
-        document.
-
-        YAML::Node objects can have their own sort order, which is usually
-        what you want. To override the YAML::Node order and sort the keys
-        anyway, set SortKeys to 2.
-
-    Stringify
-        Default is 0. (false)
-
-        Objects with string overloading should honor the overloading and
-        dump the stringification of themselves, rather than the actual
-        object's guts.
-
-    UseHeader
-        Default is 1. (true)
-
-        This tells YAML.pm whether to use a separator string for a Dump
-        operation. This only applies to the first document in a stream.
-        Subsequent documents must have a YAML header by definition.
-
-    UseVersion
-        Default is 0. (false)
-
-        Tells YAML.pm whether to include the YAML version on the
-        separator/header.
-
-            --- %YAML:1.0
-
-    AnchorPrefix
-        Default is ''.
-
-        Anchor names are normally numeric. YAML.pm simply starts with '1'
-        and increases by one for each new anchor. This option allows you to
-        specify a string to be prepended to each anchor number.
-
-    UseCode
-        Setting the UseCode option is a shortcut to set both the DumpCode
-        and LoadCode options at once. Setting UseCode to '1' tells YAML.pm
-        to dump Perl code references as Perl (using B::Deparse) and to load
-        them back into memory using eval(). The reason this has to be an
-        option is that using eval() to parse untrusted code is, well,
-        untrustworthy.
-
-    DumpCode
-        Determines if and how YAML.pm should serialize Perl code references.
-        By default YAML.pm will dump code references as dummy placeholders
-        (much like Data::Dumper). If DumpCode is set to '1' or 'deparse',
-        code references will be dumped as actual Perl code.
-
-        DumpCode can also be set to a subroutine reference so that you can
-        write your own serializing routine. YAML.pm passes you the code ref.
-        You pass back the serialization (as a string) and a format
-        indicator. The format indicator is a simple string like: 'deparse'
-        or 'bytecode'.
-
-    LoadCode
-        LoadCode is the opposite of DumpCode. It tells YAML if and how to
-        deserialize code references. When set to '1' or 'deparse' it will
-        use "eval()". Since this is potentially risky, only use this option
-        if you know where your YAML has been.
-
-        LoadCode can also be set to a subroutine reference so that you can
-        write your own deserializing routine. YAML.pm passes the
-        serialization (as a string) and a format indicator. You pass back
-        the code reference.
-
-    UseBlock
-        YAML.pm uses heuristics to guess which scalar style is best for a
-        given node. Sometimes you'll want all multiline scalars to use the
-        'block' style. If so, set this option to 1.
-
-        NOTE: YAML's block style is akin to Perl's here-document.
-
-    UseFold
-        If you want to force YAML to use the 'folded' style for all
-        multiline scalars, then set $UseFold to 1.
-
-        NOTE: YAML's folded style is akin to the way HTML folds text, except
-        smarter.
-
-    UseAliases
-        YAML has an alias mechanism such that any given structure in memory
-        gets serialized once. Any other references to that structure are
-        serialized only as alias markers. This is how YAML can serialize
-        duplicate and recursive structures.
-
-        Sometimes, when you KNOW that your data is nonrecursive in nature,
-        you may want to serialize such that every node is expressed in full.
-        (ie as a copy of the original). Setting $YAML::UseAliases to 0 will
-        allow you to do this. This also may result in faster processing
-        because the lookup overhead is by bypassed.
-
-        THIS OPTION CAN BE DANGEROUS. *If* your data is recursive, this
-        option *will* cause Dump() to run in an endless loop, chewing up
-        your computers memory. You have been warned.
-
-    CompressSeries
-        Default is 1.
-
-        Compresses the formatting of arrays of hashes:
-
-            -
-              foo: bar
-            - 
-              bar: foo
-
-        becomes:
-
-            - foo: bar
-            - bar: foo
-
-        Since this output is usually more desirable, this option is turned
-        on by default.
-
-YAML TERMINOLOGY
-    YAML is a full featured data serialization language, and thus has its
-    own terminology.
-
-    It is important to remember that although YAML is heavily influenced by
-    Perl and Python, it is a language in its own right, not merely just a
-    representation of Perl structures.
-
-    YAML has three constructs that are conspicuously similar to Perl's hash,
-    array, and scalar. They are called mapping, sequence, and string
-    respectively. By default, they do what you would expect. But each
-    instance may have an explicit or implicit tag (type) that makes it
-    behave differently. In this manner, YAML can be extended to represent
-    Perl's Glob or Python's tuple, or Ruby's Bigint.
-
-    stream
-        A YAML stream is the full sequence of unicode characters that a YAML
-        parser would read or a YAML emitter would write. A stream may
-        contain one or more YAML documents separated by YAML headers.
-
-            ---
-            a: mapping
-            foo: bar
-            ---
-            - a
-            - sequence
-
-    document
-        A YAML document is an independent data structure representation
-        within a stream. It is a top level node. Each document in a YAML
-        stream must begin with a YAML header line. Actually the header is
-        optional on the first document.
-
-            ---
-            This: top level mapping
-            is:
-                - a
-                - YAML
-                - document
-
-    header
-        A YAML header is a line that begins a YAML document. It consists of
-        three dashes, possibly followed by more info. Another purpose of the
-        header line is that it serves as a place to put top level tag and
-        anchor information.
-
-            --- !recursive-sequence &001
-            - * 001
-            - * 001
-
-    node
-        A YAML node is the representation of a particular data stucture.
-        Nodes may contain other nodes. (In Perl terms, nodes are like
-        scalars. Strings, arrayrefs and hashrefs. But this refers to the
-        serialized format, not the in-memory structure.)
-
-    tag This is similar to a type. It indicates how a particular YAML node
-        serialization should be transferred into or out of memory. For
-        instance a Foo::Bar object would use the tag 'perl/Foo::Bar':
-
-            - !perl/Foo::Bar
-                foo: 42
-                bar: stool
-
-    collection
-        A collection is the generic term for a YAML data grouping. YAML has
-        two types of collections: mappings and sequences. (Similar to hashes
-        and arrays)
-
-    mapping
-        A mapping is a YAML collection defined by unordered key/value pairs
-        with unique keys. By default YAML mappings are loaded into Perl
-        hashes.
-
-            a mapping:
-                foo: bar
-                two: times two is 4
-
-    sequence
-        A sequence is a YAML collection defined by an ordered list of
-        elements. By default YAML sequences are loaded into Perl arrays.
-
-            a sequence:
-                - one bourbon
-                - one scotch
-                - one beer
-
-    scalar
-        A scalar is a YAML node that is a single value. By default YAML
-        scalars are loaded into Perl scalars.
-
-            a scalar key: a scalar value
-
-        YAML has many styles for representing scalars. This is important
-        because varying data will have varying formatting requirements to
-        retain the optimum human readability.
-
-    plain scalar
-        A plain sclar is unquoted. All plain scalars are automatic
-        candidates for "implicit tagging". This means that their tag may be
-        determined automatically by examination. The typical uses for this
-        are plain alpha strings, integers, real numbers, dates, times and
-        currency.
-
-            - a plain string
-            - -42
-            - 3.1415
-            - 12:34
-            - 123 this is an error
-
-    single quoted scalar
-        This is similar to Perl's use of single quotes. It means no escaping
-        except for single quotes which are escaped by using two adjacent
-        single quotes.
-
-            - 'When I say ''\n'' I mean "backslash en"'
-
-    double quoted scalar
-        This is similar to Perl's use of double quotes. Character escaping
-        can be used.
-
-            - "This scalar\nhas two lines, and a bell -->\a"
-
-    folded scalar
-        This is a multiline scalar which begins on the next line. It is
-        indicated by a single right angle bracket. It is unescaped like the
-        single quoted scalar. Line folding is also performed.
-
-            - > 
-             This is a multiline scalar which begins on
-             the next line. It is indicated by a single
-             carat. It is unescaped like the single
-             quoted scalar. Line folding is also
-             performed.
-
-    block scalar
-        This final multiline form is akin to Perl's here-document except
-        that (as in all YAML data) scope is indicated by indentation.
-        Therefore, no ending marker is required. The data is verbatim. No
-        line folding.
-
-            - |
-                QTY  DESC          PRICE  TOTAL
-                ---  ----          -----  -----
-                  1  Foo Fighters  $19.95 $19.95
-                  2  Bar Belles    $29.95 $59.90
-
-    parser
-        A YAML processor has four stages: parse, load, dump, emit.
-
-        A parser parses a YAML stream. YAML.pm's Load() function contains a
-        parser.
-
-    loader
-        The other half of the Load() function is a loader. This takes the
-        information from the parser and loads it into a Perl data structure.
-
-    dumper
-        The Dump() function consists of a dumper and an emitter. The dumper
-        walks through each Perl data structure and gives info to the
-        emitter.
-
-    emitter
-        The emitter takes info from the dumper and turns it into a YAML
-        stream.
-
-        NOTE: In YAML.pm the parser/loader and the dumper/emitter code are
-        currently very closely tied together. In the future they may be
-        broken into separate stages.
-
-    For more information please refer to the immensely helpful YAML
-    specification available at <http://www.yaml.org/spec/>.
-
-ysh - The YAML Shell
-    The YAML distribution ships with a script called 'ysh', the YAML shell.
-    ysh provides a simple, interactive way to play with YAML. If you type in
-    Perl code, it displays the result in YAML. If you type in YAML it turns
-    it into Perl code.
-
-    To run ysh, (assuming you installed it along with YAML.pm) simply type:
-
-        ysh [options]
-
-    Please read the "ysh" documentation for the full details. There are lots
-    of options.
-
-BUGS & DEFICIENCIES
-    If you find a bug in YAML, please try to recreate it in the YAML Shell
-    with logging turned on ('ysh -L'). When you have successfully reproduced
-    the bug, please mail the LOG file to the author (ingy@cpan.org).
-
-    WARNING: This is still *ALPHA* code. Well, most of this code has been
-    around for years...
-
-    BIGGER WARNING: YAML.pm has been slow in the making, but I am committed
-    to having top notch YAML tools in the Perl world. The YAML team is close
-    to finalizing the YAML 1.1 spec. This version of YAML.pm is based off of
-    a very old pre 1.0 spec. In actuality there isn't a ton of difference,
-    and this YAML.pm is still fairly useful. Things will get much better in
-    the future.
-
-RESOURCES
-    <http://lists.sourceforge.net/lists/listinfo/yaml-core> is the mailing
-    list. This is where the language is discussed and designed.
-
-    <http://www.yaml.org> is the official YAML website.
-
-    <http://www.yaml.org/spec/> is the YAML 1.0 specification.
-
-    <http://yaml.kwiki.org> is the official YAML wiki.
-
-SEE ALSO
-    See YAML::Syck. Fast!
-
-AUTHOR
-    Ingy döt Net <ingy@cpan.org>
-
-    is resonsible for YAML.pm.
-
-    The YAML serialization language is the result of years of collaboration
-    between Oren Ben-Kiki, Clark Evans and Ingy döt Net. Several others
-    have added help along the way.
-
-COPYRIGHT
-    Copyright (c) 2005, 2006. Ingy döt Net. All rights reserved. Copyright
-    (c) 2001, 2002, 2005. Brian Ingerson. All rights reserved.
-
-    This program is free software; you can redistribute it and/or modify it
-    under the same terms as Perl itself.
-
-    See <http://www.perl.com/perl/misc/Artistic.html>
-
index 6ba651d..eb5cd2a 100644 (file)
@@ -118,7 +118,7 @@ als freundliche Checkliste zum Ausdrucken und Erweitern.
 
   3. Das Modul in SL/InstallationCheck.pm eintragen. Testen.
 
-  4. Das Modul in der Installationsanleitung eintragen.
+  4. Das Modul in der Installationsanleitung (documentation.xml) eintragen.
 
 * doc/UPGRADE doku aktualisieren und auf Vollständigkeit prüfen.
 
@@ -160,6 +160,7 @@ als freundliche Checkliste zum Ausdrucken und Erweitern.
 
 * Locales auf Vollständigkeit prüfen
 
+  $ scripts/locales.pl en
   $ scripts/locales.pl de
 
 * SL::DB::Helper::ALL auf Vollständigkeit prüfen
@@ -213,8 +214,16 @@ als freundliche Checkliste zum Ausdrucken und Erweitern.
 2. RELEASE
 ==========
 
+* VERSION auf aktuelle Version setzen
+ - Changelog auf Tagesdatum plus Versionssnummer
+ - dokumentation.xml Versionsnummer anpassen
+ - im Dokument UPGRADE Versionsnummer anpassen
+ - bei der Datei VERSION Versionsnummer anpassen
+
+
 * Annotated tag erstellen und pushen
 
+  # falls möglich den Tag mit dem Commit von VERSION setzen (Konvention)
   $ git tag -a release-3.0.0
   $ git push origin tags/release-3.0.0
 
diff --git a/image/CH-Kreuz_7mm.png b/image/CH-Kreuz_7mm.png
new file mode 100644 (file)
index 0000000..41d3c5f
Binary files /dev/null and b/image/CH-Kreuz_7mm.png differ
diff --git a/image/collapse.svg b/image/collapse.svg
new file mode 100644 (file)
index 0000000..340b0e6
--- /dev/null
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="100" height="100">
+  <path id="corner" d="M100,50 L50,50 L50,100 Z"
+        stroke-width="0" stroke="#000000" fill="true"/>
+</svg>
diff --git a/image/collapse3.gif b/image/collapse3.gif
new file mode 100644 (file)
index 0000000..9a89b24
Binary files /dev/null and b/image/collapse3.gif differ
diff --git a/image/edit-entry.png b/image/edit-entry.png
new file mode 100644 (file)
index 0000000..c8888f3
Binary files /dev/null and b/image/edit-entry.png differ
diff --git a/image/expand.svg b/image/expand.svg
new file mode 100644 (file)
index 0000000..efbd11b
--- /dev/null
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="100" height="100">
+  <path id="corner" d="M100,100 L100,50 L50,100 Z"
+        stroke-width="0" stroke="#000000" fill="true"/>
+</svg>
diff --git a/image/glass14x14.png b/image/glass14x14.png
new file mode 100644 (file)
index 0000000..e6c9d1b
Binary files /dev/null and b/image/glass14x14.png differ
diff --git a/image/gruener_punkt.gif b/image/gruener_punkt.gif
new file mode 100644 (file)
index 0000000..fe6c55d
Binary files /dev/null and b/image/gruener_punkt.gif differ
diff --git a/image/icons/16x16/wtg.png b/image/icons/16x16/wtg.png
new file mode 100644 (file)
index 0000000..0d4d690
Binary files /dev/null and b/image/icons/16x16/wtg.png differ
diff --git a/image/icons/svg/gobd.svg b/image/icons/svg/gobd.svg
new file mode 100644 (file)
index 0000000..b0af210
--- /dev/null
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   id="Layer_1"
+   x="0px"
+   y="0px"
+   width="16"
+   height="16"
+   viewBox="0 0 16 16"
+   enable-background="new 0 0 93.4 77.996"
+   xml:space="preserve"
+   inkscape:version="0.48.1 "
+   sodipodi:docname="export.svg"
+   inkscape:export-filename="O:\15-Artwork\Icons\od\erp\menuv3\SVG\16\export.png"
+   inkscape:export-xdpi="14.4"
+   inkscape:export-ydpi="14.4"><metadata
+   id="metadata13"><rdf:RDF><cc:Work
+       rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+         rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+   id="defs11" /><sodipodi:namedview
+   pagecolor="#ffffff"
+   bordercolor="#666666"
+   borderopacity="1"
+   objecttolerance="10"
+   gridtolerance="10"
+   guidetolerance="10"
+   inkscape:pageopacity="0"
+   inkscape:pageshadow="2"
+   inkscape:window-width="1920"
+   inkscape:window-height="1018"
+   id="namedview9"
+   showgrid="false"
+   inkscape:zoom="13.622241"
+   inkscape:cx="44.636133"
+   inkscape:cy="16.422306"
+   inkscape:window-x="1358"
+   inkscape:window-y="-8"
+   inkscape:window-maximized="1"
+   inkscape:current-layer="Layer_1" />
+<g
+   id="g3"
+   transform="matrix(0.16059957,0,0,0.16059957,0.5,1.7263984)"
+   style="fill:#505050;fill-opacity:1">
+       <path
+   d="m 70.028,63.936 c -1.933,0 -3.5,1.565 -3.5,3.5 v 3.56 H 7 V 28.407 h 9.445 c 1.933,0 3.5,-1.567 3.5,-3.5 0,-1.933 -1.567,-3.5 -3.5,-3.5 H 3.5 c -1.933,0 -3.5,1.567 -3.5,3.5 v 49.586 c 0,1.934 1.567,3.5 3.5,3.5 h 66.528 c 1.933,0 3.5,-1.566 3.5,-3.5 v -7.059 c 0,-1.933 -1.567,-3.498 -3.5,-3.498 z"
+   id="path5"
+   inkscape:connector-curvature="0"
+   style="fill:#505050;fill-opacity:1" />
+       <path
+   d="M 92.375,26.69 66.709,1.026 c -0.999,-1 -2.503,-1.302 -3.813,-0.758 -1.309,0.542 -2.16,1.818 -2.16,3.233 V 15.043 C 47.378,15.651 36.788,19.798 29.222,27.392 17.702,38.955 17.627,54.201 17.629,54.843 c 0.006,1.584 1.076,2.968 2.608,3.371 0.296,0.078 0.596,0.115 0.892,0.115 1.233,0 2.405,-0.654 3.039,-1.764 6.7,-11.725 23.989,-13.477 33.699,-13.477 1.074,0 2.04,0.022 2.867,0.055 V 54.83 c 0,1.416 0.853,2.691 2.16,3.233 1.312,0.544 2.814,0.241 3.813,-0.759 L 92.375,31.64 c 0.656,-0.657 1.025,-1.547 1.025,-2.475 0,-0.928 -0.369,-1.819 -1.025,-2.475 z M 67.734,46.38 v -6.514 c 0,-1.786 -1.344,-3.286 -3.118,-3.479 -0.11,-0.012 -2.75,-0.296 -6.749,-0.296 -8.239,0 -21.256,1.19 -30.941,7.671 1.396,-3.7 3.644,-7.805 7.255,-11.43 6.854,-6.879 16.966,-10.368 30.055,-10.368 1.933,0 3.5,-1.567 3.5,-3.5 V 11.95 L 84.95,29.165 67.734,46.38 z"
+   id="path7"
+   inkscape:connector-curvature="0"
+   style="fill:#505050;fill-opacity:1" />
+</g>
+</svg>
\ No newline at end of file
diff --git a/image/icons/svg/mail_journal.svg b/image/icons/svg/mail_journal.svg
new file mode 100644 (file)
index 0000000..07a2866
--- /dev/null
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   id="Layer_1"
+   x="0px"
+   y="0px"
+   width="16"
+   height="16"
+   viewBox="0 0 16 16"
+   enable-background="new 0 0 94.398 54.766"
+   xml:space="preserve"
+   inkscape:version="0.48.1 "
+   sodipodi:docname="mail.svg"
+   inkscape:export-filename="O:\15-Artwork\Icons\od\erp\menuv3\SVG\16\mail.png"
+   inkscape:export-xdpi="14.4"
+   inkscape:export-ydpi="14.4"><metadata
+   id="metadata9"><rdf:RDF><cc:Work
+       rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+         rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+   id="defs7" /><sodipodi:namedview
+   pagecolor="#ffffff"
+   bordercolor="#666666"
+   borderopacity="1"
+   objecttolerance="10"
+   gridtolerance="10"
+   guidetolerance="10"
+   inkscape:pageopacity="0"
+   inkscape:pageshadow="2"
+   inkscape:window-width="1920"
+   inkscape:window-height="1018"
+   id="namedview5"
+   showgrid="false"
+   inkscape:zoom="28.449018"
+   inkscape:cx="13.275149"
+   inkscape:cy="11.042062"
+   inkscape:window-x="1358"
+   inkscape:window-y="-8"
+   inkscape:window-maximized="1"
+   inkscape:current-layer="Layer_1" />
+<path
+   d="m 15.499029,4.4687988 c -3.18e-4,-0.024312 -0.0038,-0.048942 -0.0073,-0.073254 -0.0024,-0.017161 -0.0033,-0.035117 -0.0071,-0.051802 -0.0044,-0.020816 -0.01224,-0.040838 -0.01891,-0.061177 -0.0064,-0.019545 -0.01176,-0.039408 -0.02002,-0.05784 -0.0073,-0.016526 -0.01732,-0.031939 -0.02606,-0.048147 -0.01144,-0.020975 -0.02256,-0.042268 -0.03639,-0.061654 -0.0025,-0.00334 -0.0037,-0.00715 -0.0062,-0.010487 -0.0089,-0.011918 -0.0197,-0.021293 -0.02924,-0.032416 -0.01494,-0.017479 -0.02956,-0.035435 -0.04624,-0.051007 -0.01478,-0.014142 -0.03051,-0.026219 -0.04672,-0.038613 -0.01653,-0.012871 -0.03242,-0.025742 -0.04989,-0.037024 -0.01732,-0.010805 -0.03591,-0.020022 -0.05387,-0.029238 -0.01843,-0.00953 -0.03671,-0.018909 -0.05625,-0.026537 -0.01923,-0.00747 -0.03909,-0.012712 -0.05895,-0.018274 -0.01955,-0.0054 -0.03893,-0.011282 -0.05927,-0.014937 -0.02288,-0.00413 -0.04576,-0.0054 -0.06881,-0.00699 -0.01462,-0.00111 -0.02844,-0.00445 -0.04322,-0.00445 H 1.1356059 c -0.014619,0 -0.028126,0.00334 -0.042427,0.00429 -0.0232,0.00159 -0.046717,0.00286 -0.069599,0.00699 -0.020022,0.00365 -0.038772,0.00938 -0.0579991,0.014619 -0.0204983,0.00572 -0.0408377,0.011123 -0.0605415,0.01875 -0.0187504,0.00747 -0.0365473,0.016526 -0.0543443,0.025583 -0.0189093,0.00953 -0.0376596,0.01875 -0.0557744,0.030191 -0.0170025,0.010964 -0.0325748,0.023517 -0.048306,0.035753 -0.0165258,0.012871 -0.0330515,0.025424 -0.0484649,0.040043 -0.0163669,0.015731 -0.030668,0.033052 -0.0452869,0.050372 -0.009693,0.011282 -0.0208161,0.020816 -0.0297146,0.032893 -0.002384,0.00334 -0.003814,0.00699 -0.006197,0.010487 -0.0138244,0.019386 -0.0246297,0.04052 -0.0362295,0.061495 -0.008898,0.016208 -0.0187504,0.031621 -0.0260598,0.048147 -0.008263,0.018592 -0.0135066,0.038454 -0.0200216,0.05784 -0.006674,0.020339 -0.0143011,0.04052 -0.0189093,0.061336 C 0.512076,4.3604258 0.510964,4.3782228 0.50858,4.3955428 0.505243,4.4198548 0.501747,4.4441668 0.501271,4.4687968 0.50111231,4.4727713 0.5,4.476585 0.5,4.4805575 v 7.4311865 c 0,0.351014 0.28459255,0.635606 0.6356059,0.635606 H 14.864376 c 0.351014,0 0.635606,-0.284592 0.635606,-0.635606 V 4.4805575 c 1.59e-4,-0.00397 -7.94e-4,-0.00779 -9.53e-4,-0.011759 z M 12.924189,5.1161634 7.9999912,8.7302188 3.0759521,5.1161634 H 12.924189 z M 1.771212,11.27598 V 5.7354025 l 5.8526594,4.2957425 c 0.1120255,0.08199 0.2440727,0.12299 0.3761198,0.12299 0.1318882,0 0.2639353,-0.04131 0.3759609,-0.12299 L 14.22877,5.7354025 V 11.27598 H 1.771212 z"
+   id="path3"
+   inkscape:connector-curvature="0"
+   style="fill:#505050;fill-opacity:1" />
+</svg>
\ No newline at end of file
diff --git a/image/kivitendo_mir.png b/image/kivitendo_mir.png
new file mode 100644 (file)
index 0000000..f22f7ff
Binary files /dev/null and b/image/kivitendo_mir.png differ
diff --git a/image/kivitendo_xmas.png b/image/kivitendo_xmas.png
new file mode 100644 (file)
index 0000000..bf49826
Binary files /dev/null and b/image/kivitendo_xmas.png differ
diff --git a/image/rotate_cw.svg b/image/rotate_cw.svg
new file mode 100644 (file)
index 0000000..fc747b3
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
+"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<!-- Created with Inkscape (http://www.inkscape.org/) --><svg height="60.0000000" id="svg1" inkscape:version="0.38.1" sodipodi:docbase="/home/danny/flat/scalable/actions" sodipodi:docname="rotate_cw.svg" sodipodi:version="0.32" version="1.0" width="60.0000000" x="0" xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" y="0">
+  <metadata>
+    <rdf:RDF xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+      <cc:Work rdf:about="">
+        <dc:title>Part of the Flat Icon Collection (Wed Aug 25 23:29:46 2004)</dc:title>
+        <dc:description></dc:description>
+        <dc:subject>
+          <rdf:Bag>
+            <rdf:li>hash</rdf:li>
+            <rdf:li></rdf:li>
+            <rdf:li>action</rdf:li>
+            <rdf:li>computer</rdf:li>
+            <rdf:li>icons</rdf:li>
+            <rdf:li>theme</rdf:li>
+          </rdf:Bag>
+        </dc:subject>
+        <dc:publisher>
+          <cc:Agent rdf:about="http://www.openclipart.org">
+            <dc:title>Danny Allen</dc:title>
+          </cc:Agent>
+        </dc:publisher>
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Danny Allen</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <dc:rights>
+          <cc:Agent>
+            <dc:title>Danny Allen</dc:title>
+          </cc:Agent>
+        </dc:rights>
+        <dc:date></dc:date>
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+        <cc:license rdf:resource="http://web.resource.org/cc/PublicDomain"/>
+        <dc:language>en</dc:language>
+      </cc:Work>
+      <cc:License rdf:about="http://web.resource.org/cc/PublicDomain">
+        <cc:permits rdf:resource="http://web.resource.org/cc/Reproduction"/>
+        <cc:permits rdf:resource="http://web.resource.org/cc/Distribution"/>
+        <cc:permits rdf:resource="http://web.resource.org/cc/DerivativeWorks"/>
+      </cc:License>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:cx="33.984503" inkscape:cy="18.129014" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="685" inkscape:window-width="1016" inkscape:window-x="0" inkscape:window-y="0" inkscape:zoom="6.9465337" pagecolor="#ffffff" showguides="true" snaptoguides="true"/>
+  <defs id="defs3"/>
+  <path d="M 7.9315240,4.7740988 C 37.959278,18.463222 30.893925,41.646412 30.893925,41.867205 L 20.958272,41.867205 L 36.965712,55.225139 L 50.434041,41.756809 L 40.387993,41.756809 C 40.387993,41.756809 47.894929,9.6315299 7.9315240,4.7740988 z " id="path968" sodipodi:nodetypes="ccccccc" sodipodi:stroke-cmyk="(0 0 0 0.8)" style="font-size:12.000000;fill:#7f7f7f;fill-rule:evenodd;stroke:#333333;stroke-width:3.1249931;stroke-linecap:round;stroke-linejoin:round;" transform="translate(1.079675,0.000000)"/>
+</svg>
diff --git a/image/roter_punkt.gif b/image/roter_punkt.gif
new file mode 100644 (file)
index 0000000..2331af4
Binary files /dev/null and b/image/roter_punkt.gif differ
diff --git a/image/search.svg b/image/search.svg
new file mode 100644 (file)
index 0000000..99e99ac
--- /dev/null
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" zoomAndPan="disable" >
+  <rect id="svgEditorBackground" x="0" y="0" width="100" height="100" style="fill: none; stroke: none;"/>
+  <circle id="e2_circle" cx="60" cy="40" style="stroke-width: 3px" stroke="grey" r="25" fill="grey"/>
+  <circle id="e3_circle" cx="60" cy="40" style="stroke-width: 3px" stroke="lightgrey" r="19" fill="white"/>
+  <rect x="-25" y="47.5" style="stroke-width: 1px; vector-effect" stroke="grey" id="e4_rectangle" width="20" height="5" fill="grey" transform="matrix(1, -1, 1, 1, 0, 0)"/>
+</svg>
diff --git a/image/select-down.png b/image/select-down.png
new file mode 100644 (file)
index 0000000..cea13be
Binary files /dev/null and b/image/select-down.png differ
index 2caabf7..c3d4cbd 100644 (file)
@@ -1,4 +1,6 @@
 namespace('kivi', function(k){
+  "use strict";
+
   k.ChartPicker = function($real, options) {
     // short circuit in case someone double inits us
     if ($real.data("chart_picker"))
@@ -12,6 +14,9 @@ namespace('kivi', function(k){
       RIGHT:  39,
       PAGE_UP: 33,
       PAGE_DOWN: 34,
+      SHIFT:     16,
+      CTRL:      17,
+      ALT:       18,
     };
     var CLASSES = {
       PICKED:       'chartpicker-picked',
@@ -94,7 +99,6 @@ namespace('kivi', function(k){
       state = STATES.PICKED;
       last_real = $real.val();
       last_dummy = $dummy.val();
-      last_unverified_dummy = $dummy.val();
       $real.trigger('change');
 
       if (o.fat_set_item && item.id) {
@@ -115,10 +119,9 @@ namespace('kivi', function(k){
       if (state == STATES.PICKED) {
         annotate_state();
         return true
-      } else if (state == STATES.UNDEFINED && $dummy.val() == '')
+      } else if (state == STATES.UNDEFINED && $dummy.val() === '')
         set_item({})
       else {
-        last_unverified_dummy = $dummy.val();
         set_item({ id: last_real, name: last_dummy })
       }
       annotate_state();
@@ -127,10 +130,9 @@ namespace('kivi', function(k){
     function annotate_state () {
       if (state == STATES.PICKED)
         $dummy.removeClass(STATES.UNDEFINED).addClass(STATES.PICKED);
-      else if (state == STATES.UNDEFINED && $dummy.val() == '')
+      else if (state == STATES.UNDEFINED && $dummy.val() === '')
         $dummy.removeClass(STATES.UNDEFINED).addClass(STATES.PICKED);
       else {
-        last_unverified_dummy = $dummy.val();
         $dummy.addClass(STATES.UNDEFINED).removeClass(STATES.PICKED);
       }
     }
@@ -143,7 +145,7 @@ namespace('kivi', function(k){
         }, ajax_data(function(){ var val = $('#chart_picker_filter').val(); return val === undefined ? '' : val })),
         success: function(data){ $('#chart_picker_result').html(data) }
       });
-    };
+    }
 
     function result_timer (event) {
       if (!$('hide_chart_details').prop('checked')) {
@@ -162,7 +164,28 @@ namespace('kivi', function(k){
 
     function close_popup() {
       $('#chart_selection').dialog('close');
-    };
+    }
+
+    function handle_changed_text(callbacks) {
+      $.ajax({
+        url: 'controller.pl?action=Chart/ajax_autocomplete',
+        dataType: "json",
+        data: $.extend( ajax_data($dummy.val()), { prefer_exact: 1 } ),
+        success: function (data) {
+          if (data.length == 1) {
+            set_item(data[0]);
+            if (callbacks && callbacks.match_one) callbacks.match_one(data[0]);
+          } else if (data.length > 1) {
+            state = STATES.UNDEFINED;
+            if (callbacks && callbacks.match_many) callbacks.match_many(data);
+          } else {
+            state = STATES.UNDEFINED;
+            if (callbacks &&callbacks.match_none) callbacks.match_none();
+          }
+          annotate_state();
+        }
+      });
+    }
 
     $dummy.autocomplete({
       source: function(req, rsp) {
@@ -177,6 +200,10 @@ namespace('kivi', function(k){
       select: function(event, ui) {
         set_item(ui.item);
       },
+      search: function(event, ui) {
+        if ((event.which == KEY.SHIFT) || (event.which == KEY.CTRL) || (event.which == KEY.ALT))
+          event.preventDefault();
+      }
     });
     /*  In case users are impatient and want to skip ahead:
      *  Capture <enter> key events and check if it's a unique hit.
@@ -193,48 +220,43 @@ namespace('kivi', function(k){
     $dummy.keydown(function(event){
       if (event.which == KEY.ENTER || event.which == KEY.TAB) {
         // if string is empty assume they want to delete
-        if ($dummy.val() == '') {
+        if ($dummy.val() === '') {
           set_item({});
           return true;
         } else if (state == STATES.PICKED) {
           return true;
         }
-        if (event.which == KEY.TAB) event.preventDefault();
-        $.ajax({
-          url: 'controller.pl?action=Chart/ajax_autocomplete',
-          dataType: "json",
-          data: $.extend( ajax_data($dummy.val()), { prefer_exact: 1 } ),
-          success: function (data) {
-            if (data.length == 1) {
-              set_item(data[0]);
-              if (event.which == KEY.ENTER)
-                $('#update_button').click();
-            } else if (data.length > 1) {
-             if (event.which == KEY.ENTER)
-                open_dialog();
-            } else {
-            }
-            annotate_state();
-          }
-        });
-        if (event.which == KEY.ENTER)
+        if (event.which == KEY.TAB) {
+          event.preventDefault();
+          handle_changed_text();
+        }
+        if (event.which == KEY.ENTER) {
+          handle_changed_text({
+            match_one:  function(){$('#update_button').click();},
+            match_many: function(){open_dialog();}
+          });
           return false;
-      } else {
+        }
+      } else if ((event.which != KEY.SHIFT) && (event.which != KEY.CTRL) && (event.which != KEY.ALT)) {
         state = STATES.UNDEFINED;
       }
     });
 
+    $dummy.on('paste', function(){
+      setTimeout(function() {
+        handle_changed_text();
+      }, 1);
+    });
+
     $dummy.blur(function(){
       window.clearTimeout(timer);
       timer = window.setTimeout(annotate_state, 100);
     });
 
     // now add a picker div after the original input
-    var pcont  = $('<span>').addClass('position-absolute');
-    var picker = $('<div>');
-    $dummy.after(pcont);
-    pcont.append(picker);
-    picker.addClass('icon16 search').click(open_dialog);
+    var popup_button = $('<span>').addClass('cpc_popup_button');
+    $dummy.after(popup_button);
+    popup_button.click(open_dialog);
 
     var cp = {
       real:           function() { return $real },
diff --git a/js/autocomplete_customer.js b/js/autocomplete_customer.js
deleted file mode 100644 (file)
index e3c1c7d..0000000
+++ /dev/null
@@ -1,182 +0,0 @@
-namespace('kivi', function(k){
-  k.CustomerVendorPicker = function($real, options) {
-    // short circuit in case someone double inits us
-    if ($real.data("customer_vendor_picker"))
-      return $real.data("customer_vendor_picker");
-
-    var KEY = {
-      ESCAPE: 27,
-      ENTER:  13,
-      TAB:    9,
-      LEFT:   37,
-      RIGHT:  39,
-      PAGE_UP: 33,
-      PAGE_DOWN: 34,
-    };
-    var CLASSES = {
-      PICKED:       'customer-vendor-picker-picked',
-      UNDEFINED:    'customer-vendor-picker-undefined',
-      FAT_SET_ITEM: 'customer-vendor-picker-fat-set-item',
-    }
-    var o = $.extend({
-      limit: 20,
-      delay: 50,
-      fat_set_item: $real.hasClass(CLASSES.FAT_SET_ITEM),
-    }, options);
-    var STATES = {
-      PICKED:    CLASSES.PICKED,
-      UNDEFINED: CLASSES.UNDEFINED
-    }
-    var real_id = $real.attr('id');
-    var $dummy  = $('#' + real_id + '_name');
-    var $type   = $('#' + real_id + '_type');
-    var $unit   = $('#' + real_id + '_unit');
-    var state   = STATES.PICKED;
-    var last_real = $real.val();
-    var last_dummy = $dummy.val();
-    var timer;
-
-    function ajax_data(term) {
-      var data = {
-        'filter.all:substr:multi::ilike': term,
-        'filter.obsolete': 0,
-        current:  $real.val(),
-        type:     $type.val(),
-      };
-
-      return data;
-    }
-
-    function set_item (item) {
-      if (item.id) {
-        $real.val(item.id);
-        // autocomplete ui has name, ajax items have description
-        $dummy.val(item.name ? item.name : item.description);
-      } else {
-        $real.val('');
-        $dummy.val('');
-      }
-      state = STATES.PICKED;
-      last_real = $real.val();
-      last_dummy = $dummy.val();
-      last_unverified_dummy = $dummy.val();
-      $real.trigger('change');
-
-      if (o.fat_set_item && item.id) {
-        $.ajax({
-          url: 'controller.pl?action=CustomerVendor/show.json',
-          data: { id: item.id, db: item.type },
-          success: function(rsp) {
-            $real.trigger('set_item:CustomerVendorPicker', rsp);
-          },
-        });
-      } else {
-        $real.trigger('set_item:CustomerVendorPicker', item);
-      }
-      annotate_state();
-    }
-
-    function make_defined_state () {
-      if (state == STATES.PICKED) {
-        annotate_state();
-        return true
-      } else if (state == STATES.UNDEFINED && $dummy.val() == '')
-        set_item({})
-      else {
-        last_unverified_dummy = $dummy.val();
-        set_item({ id: last_real, name: last_dummy })
-      }
-      annotate_state();
-    }
-
-    function annotate_state () {
-      if (state == STATES.PICKED)
-        $dummy.removeClass(STATES.UNDEFINED).addClass(STATES.PICKED);
-      else if (state == STATES.UNDEFINED && $dummy.val() == '')
-        $dummy.removeClass(STATES.UNDEFINED).addClass(STATES.PICKED);
-      else {
-        last_unverified_dummy = $dummy.val();
-        $dummy.addClass(STATES.UNDEFINED).removeClass(STATES.PICKED);
-      }
-    }
-
-    $dummy.autocomplete({
-      source: function(req, rsp) {
-        $.ajax($.extend(o, {
-          url:      'controller.pl?action=CustomerVendor/ajaj_autocomplete',
-          dataType: "json",
-          data:     ajax_data(req.term),
-          success:  function (data){ rsp(data) }
-        }));
-      },
-      select: function(event, ui) {
-        set_item(ui.item);
-      },
-    });
-    /*  In case users are impatient and want to skip ahead:
-     *  Capture <enter> key events and check if it's a unique hit.
-     *  If it is, go ahead and assume it was selected. If it wasn't don't do
-     *  anything so that autocompletion kicks in.  For <tab> don't prevent
-     *  propagation. It would be nice to catch it, but javascript is too stupid
-     *  to fire a tab event later on, so we'd have to reimplement the "find
-     *  next active element in tabindex order and focus it".
-     */
-    /* note:
-     *  event.which does not contain tab events in keypressed in firefox but will report 0
-     *  chrome does not fire keypressed at all on tab or escape
-     */
-    $dummy.keydown(function(event){
-      if (event.which == KEY.ENTER || event.which == KEY.TAB) {
-        // if string is empty assume they want to delete
-        if ($dummy.val() == '') {
-          set_item({});
-          return true;
-        } else if (state == STATES.PICKED) {
-          return true;
-        }
-        if (event.which == KEY.TAB) event.preventDefault();
-        $.ajax({
-          url: 'controller.pl?action=CustomerVendor/ajaj_autocomplete',
-          dataType: "json",
-          data: $.extend( ajax_data($dummy.val()), { prefer_exact: 1 } ),
-          success: function (data) {
-            if (data.length == 1) {
-              set_item(data[0]);
-              if (event.which == KEY.ENTER)
-                $('#update_button').click();
-            } else {
-            }
-            annotate_state();
-          }
-        });
-        if (event.which == KEY.ENTER)
-          return false;
-      } else {
-        state = STATES.UNDEFINED;
-      }
-    });
-
-    $dummy.blur(function(){
-      window.clearTimeout(timer);
-      timer = window.setTimeout(annotate_state, 100);
-    });
-
-    // now add a picker div after the original input
-    var pp = {
-      real:           function() { return $real },
-      dummy:          function() { return $dummy },
-      type:           function() { return $type },
-      set_item:       set_item,
-      reset:          make_defined_state,
-      is_defined_state: function() { return state == STATES.PICKED },
-    }
-    $real.data('customer_vendor_picker', pp);
-    return pp;
-  }
-});
-
-$(function(){
-  $('input.customer_vendor_autocomplete').each(function(i,real){
-    kivi.CustomerVendorPicker($(real));
-  })
-});
diff --git a/js/autocomplete_part.js b/js/autocomplete_part.js
deleted file mode 100644 (file)
index 8c304bb..0000000
+++ /dev/null
@@ -1,269 +0,0 @@
-namespace('kivi', function(k){
-  k.PartPicker = function($real, options) {
-    // short circuit in case someone double inits us
-    if ($real.data("part_picker"))
-      return $real.data("part_picker");
-
-    var KEY = {
-      ESCAPE: 27,
-      ENTER:  13,
-      TAB:    9,
-      LEFT:   37,
-      RIGHT:  39,
-      PAGE_UP: 33,
-      PAGE_DOWN: 34,
-    };
-    var CLASSES = {
-      PICKED:       'partpicker-picked',
-      UNDEFINED:    'partpicker-undefined',
-      FAT_SET_ITEM: 'partpicker_fat_set_item',
-    }
-    var o = $.extend({
-      limit: 20,
-      delay: 50,
-      fat_set_item: $real.hasClass(CLASSES.FAT_SET_ITEM),
-    }, options);
-    var STATES = {
-      PICKED:    CLASSES.PICKED,
-      UNDEFINED: CLASSES.UNDEFINED
-    }
-    var real_id = $real.attr('id');
-    var $dummy  = $('#' + real_id + '_name');
-    var $type   = $('#' + real_id + '_type');
-    var $unit   = $('#' + real_id + '_unit');
-    var $convertible_unit = $('#' + real_id + '_convertible_unit');
-    var state   = STATES.PICKED;
-    var last_real = $real.val();
-    var last_dummy = $dummy.val();
-    var timer;
-
-    function open_dialog () {
-      k.popup_dialog({
-        url: 'controller.pl?action=Part/part_picker_search',
-        data: $.extend({
-          real_id: real_id,
-        }, ajax_data($dummy.val())),
-        id: 'part_selection',
-        dialog: {
-          title: k.t8('Part picker'),
-          width: 800,
-          height: 800,
-        }
-      });
-      window.clearTimeout(timer);
-      return true;
-    }
-
-    function ajax_data(term) {
-      var data = {
-        'filter.all:substr:multi::ilike': term,
-        'filter.obsolete': 0,
-        'filter.unit_obj.convertible_to': $convertible_unit && $convertible_unit.val() ? $convertible_unit.val() : '',
-        no_paginate:  $('#no_paginate').prop('checked') ? 1 : 0,
-        current:  $real.val(),
-      };
-
-      if ($type && $type.val())
-        data['filter.type'] = $type.val().split(',');
-
-      if ($unit && $unit.val())
-        data['filter.unit'] = $unit.val().split(',');
-
-      return data;
-    }
-
-    function set_item (item) {
-      if (item.id) {
-        $real.val(item.id);
-        // autocomplete ui has name, use the value for ajax items, which contains displayable_name
-        $dummy.val(item.name ? item.name : item.value);
-      } else {
-        $real.val('');
-        $dummy.val('');
-      }
-      state = STATES.PICKED;
-      last_real = $real.val();
-      last_dummy = $dummy.val();
-      last_unverified_dummy = $dummy.val();
-      $real.trigger('change');
-
-      if (o.fat_set_item && item.id) {
-        $.ajax({
-          url: 'controller.pl?action=Part/show.json',
-          data: { id: item.id },
-          success: function(rsp) {
-            $real.trigger('set_item:PartPicker', rsp);
-          },
-        });
-      } else {
-        $real.trigger('set_item:PartPicker', item);
-      }
-      annotate_state();
-    }
-
-    function make_defined_state () {
-      if (state == STATES.PICKED) {
-        annotate_state();
-        return true
-      } else if (state == STATES.UNDEFINED && $dummy.val() == '')
-        set_item({})
-      else {
-        last_unverified_dummy = $dummy.val();
-        set_item({ id: last_real, name: last_dummy })
-      }
-      annotate_state();
-    }
-
-    function annotate_state () {
-      if (state == STATES.PICKED)
-        $dummy.removeClass(STATES.UNDEFINED).addClass(STATES.PICKED);
-      else if (state == STATES.UNDEFINED && $dummy.val() == '')
-        $dummy.removeClass(STATES.UNDEFINED).addClass(STATES.PICKED);
-      else {
-        last_unverified_dummy = $dummy.val();
-        $dummy.addClass(STATES.UNDEFINED).removeClass(STATES.PICKED);
-      }
-    }
-
-    function update_results () {
-      $.ajax({
-        url: 'controller.pl?action=Part/part_picker_result',
-        data: $.extend({
-            'real_id': $real.val(),
-        }, ajax_data(function(){ var val = $('#part_picker_filter').val(); return val === undefined ? '' : val })),
-        success: function(data){ $('#part_picker_result').html(data) }
-      });
-    };
-
-    function result_timer (event) {
-      if (!$('no_paginate').prop('checked')) {
-        if (event.keyCode == KEY.PAGE_UP) {
-          $('#part_picker_result a.paginate-prev').click();
-          return;
-        }
-        if (event.keyCode == KEY.PAGE_DOWN) {
-          $('#part_picker_result a.paginate-next').click();
-          return;
-        }
-      }
-      window.clearTimeout(timer);
-      timer = window.setTimeout(update_results, 100);
-    }
-
-    function close_popup() {
-      $('#part_selection').dialog('close');
-    };
-
-    $dummy.autocomplete({
-      source: function(req, rsp) {
-        $.ajax($.extend(o, {
-          url:      'controller.pl?action=Part/ajax_autocomplete',
-          dataType: "json",
-          data:     ajax_data(req.term),
-          success:  function (data){ rsp(data) }
-        }));
-      },
-      select: function(event, ui) {
-        set_item(ui.item);
-      },
-    });
-    /*  In case users are impatient and want to skip ahead:
-     *  Capture <enter> key events and check if it's a unique hit.
-     *  If it is, go ahead and assume it was selected. If it wasn't don't do
-     *  anything so that autocompletion kicks in.  For <tab> don't prevent
-     *  propagation. It would be nice to catch it, but javascript is too stupid
-     *  to fire a tab event later on, so we'd have to reimplement the "find
-     *  next active element in tabindex order and focus it".
-     */
-    /* note:
-     *  event.which does not contain tab events in keypressed in firefox but will report 0
-     *  chrome does not fire keypressed at all on tab or escape
-     */
-    $dummy.keydown(function(event){
-      if (event.which == KEY.ENTER || event.which == KEY.TAB) {
-        // if string is empty assume they want to delete
-        if ($dummy.val() == '') {
-          set_item({});
-          return true;
-        } else if (state == STATES.PICKED) {
-          return true;
-        }
-        if (event.which == KEY.TAB) event.preventDefault();
-        $.ajax({
-          url: 'controller.pl?action=Part/ajax_autocomplete',
-          dataType: "json",
-          data: $.extend( ajax_data($dummy.val()), { prefer_exact: 1 } ),
-          success: function (data) {
-            if (data.length == 1) {
-              set_item(data[0]);
-              if (event.which == KEY.ENTER)
-                $('#update_button').click();
-            } else if (data.length > 1) {
-             if (event.which == KEY.ENTER)
-                open_dialog();
-            } else {
-            }
-            annotate_state();
-          }
-        });
-        if (event.which == KEY.ENTER)
-          return false;
-      } else {
-        state = STATES.UNDEFINED;
-      }
-    });
-
-    $dummy.blur(function(){
-      window.clearTimeout(timer);
-      timer = window.setTimeout(annotate_state, 100);
-    });
-
-    // now add a picker div after the original input
-    var pcont  = $('<span>').addClass('position-absolute');
-    var picker = $('<div>');
-    $dummy.after(pcont);
-    pcont.append(picker);
-    picker.addClass('icon16 search').click(open_dialog);
-
-    var pp = {
-      real:           function() { return $real },
-      dummy:          function() { return $dummy },
-      type:           function() { return $type },
-      unit:           function() { return $unit },
-      convertible_unit: function() { return $convertible_unit },
-      update_results: update_results,
-      result_timer:   result_timer,
-      set_item:       set_item,
-      reset:          make_defined_state,
-      is_defined_state: function() { return state == STATES.PICKED },
-      init_results:    function () {
-        $('div.part_picker_part').each(function(){
-          $(this).click(function(){
-            set_item({
-              id:   $(this).children('input.part_picker_id').val(),
-              name: $(this).children('input.part_picker_description').val(),
-              unit: $(this).children('input.part_picker_unit').val(),
-            });
-            close_popup();
-            $dummy.focus();
-            return true;
-          });
-        });
-        $('#part_selection').keydown(function(e){
-           if (e.which == KEY.ESCAPE) {
-             close_popup();
-             $dummy.focus();
-           }
-        });
-      }
-    }
-    $real.data('part_picker', pp);
-    return pp;
-  }
-});
-
-$(function(){
-  $('input.part_autocomplete').each(function(i,real){
-    kivi.PartPicker($(real));
-  })
-});
index c554504..6a566c4 100644 (file)
@@ -1,4 +1,6 @@
 namespace('kivi', function(k){
+  "use strict";
+
   k.ProjectPicker = function($real, options) {
     // short circuit in case someone double inits us
     if ($real.data("project_picker"))
@@ -12,6 +14,9 @@ namespace('kivi', function(k){
       RIGHT:  39,
       PAGE_UP: 33,
       PAGE_DOWN: 34,
+      SHIFT:     16,
+      CTRL:      17,
+      ALT:       18,
     };
     var CLASSES = {
       PICKED:       'projectpicker-picked',
@@ -20,7 +25,7 @@ namespace('kivi', function(k){
     var o = $.extend({
       limit: 20,
       delay: 50,
-    }, options);
+    }, $real.data('project-picker-data'), options);
     var STATES = {
       PICKED:    CLASSES.PICKED,
       UNDEFINED: CLASSES.UNDEFINED
@@ -33,16 +38,63 @@ namespace('kivi', function(k){
     var last_dummy   = $dummy.val();
     var timer;
 
+    function open_dialog () {
+      k.popup_dialog({
+        url: 'controller.pl?action=Project/project_picker_search',
+        // data that can be accessed in template project_picker_search via FORM.boss
+        data: $.extend({  // add id of part to the rest of the data in ajax_data, e.g. no_paginate, booked, ...
+          real_id: real_id,
+          select: 1,
+        }, ajax_data($dummy.val())),
+        id: 'project_selection',
+        dialog: {
+          title: k.t8('Project picker'),
+          width: 800,
+          height: 800,
+        },
+        load: function() { init_search(); }
+      });
+      window.clearTimeout(timer);
+      return true;
+    }
+
+    function init_search() {
+      $('#project_picker_filter').keypress(function(e) { result_timer(e) }).focus();
+      $('#no_paginate').change(function() { update_results() });
+      $('#project_picker_clear_filter').click(function() {
+        $('#project_picker_filter').val('').focus();
+        update_results();
+      });
+    }
+
     function ajax_data(term) {
       var data = {
         'filter.all:substr:multi::ilike': term,
-        'filter.valid': 'valid',
         no_paginate:  $('#no_paginate').prop('checked') ? 1 : 0,
         current:  $real.val(),
       };
 
-      if ($customer_id && $customer_id.val())
-        data['filter.customer_id'] = $customer_id.val().split(',');
+      if (o.customer_id)
+        data['filter.customer_id'] = o.customer_id.split(',');
+
+      if (o.active) {
+        if (o.active === 'active')   data['filter.active'] = 'active';
+        if (o.active === 'inactive') data['filter.active'] = 'inactive';
+        // both => no filter
+      } else {
+        data['filter.active'] = 'active'; // default
+      }
+
+      if (o.valid) {
+        if (o.valid === 'valid')   data['filter.valid'] = 'valid';
+        if (o.valid === 'invalid') data['filter.valid'] = 'invalid';
+        // both => no filter
+      } else {
+        data['filter.valid'] = 'valid'; // default
+      }
+
+      if (o.description_style)
+        data['description_style'] = o.description_style;
 
       return data;
     }
@@ -56,10 +108,9 @@ namespace('kivi', function(k){
         $real.val('');
         $dummy.val('');
       }
-      state                 = STATES.PICKED;
-      last_real             = $real.val();
-      last_dummy            = $dummy.val();
-      last_unverified_dummy = $dummy.val();
+      state      = STATES.PICKED;
+      last_real  = $real.val();
+      last_dummy = $dummy.val();
 
       $real.trigger('change');
       $real.trigger('set_item:ProjectPicker', item);
@@ -71,10 +122,9 @@ namespace('kivi', function(k){
       if (state == STATES.PICKED) {
         annotate_state();
         return true
-      } else if (state == STATES.UNDEFINED && $dummy.val() == '')
+      } else if (state == STATES.UNDEFINED && $dummy.val() === '')
         set_item({})
       else {
-        last_unverified_dummy = $dummy.val();
         set_item({ id: last_real, name: last_dummy })
       }
       annotate_state();
@@ -83,10 +133,9 @@ namespace('kivi', function(k){
     function annotate_state () {
       if (state == STATES.PICKED)
         $dummy.removeClass(STATES.UNDEFINED).addClass(STATES.PICKED);
-      else if (state == STATES.UNDEFINED && $dummy.val() == '')
+      else if (state == STATES.UNDEFINED && $dummy.val() === '')
         $dummy.removeClass(STATES.UNDEFINED).addClass(STATES.PICKED);
       else {
-        last_unverified_dummy = $dummy.val();
         $dummy.addClass(STATES.UNDEFINED).removeClass(STATES.PICKED);
       }
     }
@@ -97,9 +146,11 @@ namespace('kivi', function(k){
         data: $.extend({
             'real_id': $real.val(),
         }, ajax_data(function(){ var val = $('#project_picker_filter').val(); return val === undefined ? '' : val })),
-        success: function(data){ $('#project_picker_result').html(data) }
+        success: function(data){
+          $('#project_picker_result').html(data);
+        }
       });
-    };
+    }
 
     function result_timer (event) {
       if (!$('no_paginate').prop('checked')) {
@@ -116,6 +167,31 @@ namespace('kivi', function(k){
       timer = window.setTimeout(update_results, 100);
     }
 
+    function close_popup() {
+      $('#project_selection').dialog('close');
+    }
+
+    function handle_changed_text(callbacks) {
+      $.ajax({
+        url: 'controller.pl?action=Project/ajax_autocomplete',
+        dataType: "json",
+        data: $.extend( ajax_data($dummy.val()), { prefer_exact: 1 } ),
+        success: function (data) {
+          if (data.length == 1) {
+            set_item(data[0]);
+            if (callbacks && callbacks.match_one) callbacks.match_one(data[0]);
+          } else if (data.length > 1) {
+            state = STATES.UNDEFINED;
+            if (callbacks && callbacks.match_many) callbacks.match_many(data);
+          } else {
+            state = STATES.UNDEFINED;
+            if (callbacks &&callbacks.match_none) callbacks.match_none();
+          }
+          annotate_state();
+        }
+      });
+    }
+
     $dummy.autocomplete({
       source: function(req, rsp) {
         $.ajax($.extend(o, {
@@ -128,6 +204,10 @@ namespace('kivi', function(k){
       select: function(event, ui) {
         set_item(ui.item);
       },
+      search: function(event, ui) {
+        if ((event.which == KEY.SHIFT) || (event.which == KEY.CTRL) || (event.which == KEY.ALT))
+          event.preventDefault();
+      }
     });
     /*  In case users are impatient and want to skip ahead:
      *  Capture <enter> key events and check if it's a unique hit.
@@ -144,70 +224,66 @@ namespace('kivi', function(k){
     $dummy.keydown(function(event){
       if (event.which == KEY.ENTER || event.which == KEY.TAB) {
         // if string is empty assume they want to delete
-        if ($dummy.val() == '') {
+        if ($dummy.val() === '') {
           set_item({});
           return true;
         } else if (state == STATES.PICKED) {
           return true;
         }
-        if (event.which == KEY.TAB) event.preventDefault();
-        $.ajax({
-          url: 'controller.pl?action=Project/ajax_autocomplete',
-          dataType: "json",
-          data: $.extend( ajax_data($dummy.val()), { prefer_exact: 1 } ),
-          success: function (data) {
-            if (data.length == 1) {
-              set_item(data[0]);
-              if (event.which == KEY.ENTER)
-                $('#update_button').click();
-            } else {
-            }
-            annotate_state();
-          }
-        });
-        if (event.which == KEY.ENTER)
+        if (event.which == KEY.TAB) {
+          event.preventDefault();
+          handle_changed_text();
+        }
+        if (event.which == KEY.ENTER) {
+          handle_changed_text({
+            match_one:  function(){$('#update_button').click();},
+          });
           return false;
-      } else {
+        }
+      } else if ((event.which != KEY.SHIFT) && (event.which != KEY.CTRL) && (event.which != KEY.ALT)) {
         state = STATES.UNDEFINED;
       }
     });
 
+    $dummy.on('paste', function(){
+      setTimeout(function() {
+        handle_changed_text();
+      }, 1);
+    });
+
     $dummy.blur(function(){
       window.clearTimeout(timer);
       timer = window.setTimeout(annotate_state, 100);
     });
 
     // now add a picker div after the original input
-    var pcont  = $('<span>').addClass('position-absolute');
-    var picker = $('<div>');
-    $dummy.after(pcont);
-    pcont.append(picker);
-
+    var popup_button = $('<span>').addClass('ppp_popup_button');
+    $dummy.after(popup_button);
+    popup_button.click(open_dialog);
     var pp = {
       real:           function() { return $real },
       dummy:          function() { return $dummy },
-      type:           function() { return $type },
-      customer_id:    function() { return $customer_id },
       update_results: update_results,
       result_timer:   result_timer,
       set_item:       set_item,
       reset:          make_defined_state,
       is_defined_state: function() { return state == STATES.PICKED },
-      init_results:    function () {
+      init_results: function() {
         $('div.project_picker_project').each(function(){
           $(this).click(function(){
             set_item({
               id:   $(this).children('input.project_picker_id').val(),
               name: $(this).children('input.project_picker_description').val(),
             });
+            close_popup();
             $dummy.focus();
             return true;
-          });
-        });
+          });  });
         $('#project_selection').keydown(function(e){
-           if (e.which == KEY.ESCAPE) {
-             $dummy.focus();
-           }
+          if (e.which == KEY.ESCAPE) {
+            close_popup();
+            $dummy.focus();
+          }
         });
       }
     }
index a3bf3ad..ee36cf0 100644 (file)
@@ -1,20 +1,27 @@
-function calculate_qty_selection_window(input_name, alu, formel, row) {
-  var parm = centerParms(600,500) + ",width=600,height=500,status=yes,scrollbars=yes";
-  var name = document.getElementsByName(input_name)[0].value;
-  if (document.getElementsByName(alu)[0].value == "1") {
-    var action = "calculate_alu";
-    var qty = document.getElementsByName("qty_" + row)[0].value;
-    var description = document.getElementsByName("description_" + row)[0].value;
-  }  else var action = "calculate_qty";
-  url = "common.pl?" +
-    "INPUT_ENCODING=UTF-8&" +
-    "action=" + action + "&" +
-    "name=" + encodeURIComponent(name) + "&" +
-    "input_name=" + encodeURIComponent(input_name) + "&" +
-    "description=" + encodeURIComponent(description) + "&" +
-    "qty=" + encodeURIComponent(qty) + "&" +
-    "row=" + encodeURIComponent(row) + "&" +
-   "formel=" + encodeURIComponent(document.getElementsByName(formel)[0].value)
-  //alert(url);
-  window.open(url, "_new_generic", parm);
+function calculate_qty_selection_dialog(input_name, input_id, formel_name, formel_id) {
+  // The target input element is determined by it's dom id or by it's name.
+  // The formula input element (the one containing the formula) is determined by it's dom id or by it's name.
+  // If the id is not provided the name is used.
+  if (formel_id) {
+    var formel = $('#' + formel_id).val();
+  } else {
+    var formel = $('[name="' + formel_name + '"]').val();
+  }
+  var url  = "common.pl";
+  var data = {
+    action:     "calculate_qty",
+    input_name: input_name,
+    input_id:   input_id,
+    formel:     formel
+  };
+  kivi.popup_dialog({
+    id:     'calc_qty_dialog',
+    url:    url,
+    data:   data,
+    dialog: {
+      width:  500,
+      height: 400,
+      title:  kivi.t8('Please enter values'),
+    }
+  });
 }
diff --git a/js/ckeditor/CHANGES.md b/js/ckeditor/CHANGES.md
new file mode 100644 (file)
index 0000000..f9bb0a2
--- /dev/null
@@ -0,0 +1,1332 @@
+CKEditor 4 Changelog\r
+====================\r
+\r
+## CKEditor 4.7.2\r
+\r
+New Features:\r
+\r
+* [#455](https://github.com/ckeditor/ckeditor-dev/issues/455): Added [Advanced Content Filter](https://docs.ckeditor.com/#!/guide/dev_acf) integration with the [Justify](http://ckeditor.com/addon/justify) plugin.\r
+\r
+Fixed Issues:\r
+\r
+* [#663](https://github.com/ckeditor/ckeditor-dev/issues/663): [Chrome] Fixed: Clicking the scrollbar throws an `Uncaught TypeError: element.is is not a function` error.\r
+* [#520](https://github.com/ckeditor/ckeditor-dev/issues/520): Fixed: Widgets cannot be properly pasted into a table cell.\r
+* [#579](https://github.com/ckeditor/ckeditor-dev/issues/579): Fixed: Internal `cke_table-faked-selection-table` class is visible in the Stylesheet Classes field of the [Table Properties](http://ckeditor.com/addon/table) dialog.\r
+* [#545](https://github.com/ckeditor/ckeditor-dev/issues/545): [Edge] Fixed: Error thrown when pressing the [Select All](https://ckeditor.com/addon/selectall) button in [Source Mode](http://ckeditor.com/addon/sourcearea).\r
+* [#582](https://github.com/ckeditor/ckeditor-dev/issues/582): Fixed: Double slash in the path to stylesheet needed by the [Table Selection](http://ckeditor.com/addon/tableselection) plugin. Thanks to [Marius Dumitru Florea](https://github.com/mflorea)!\r
+* [#491](https://github.com/ckeditor/ckeditor-dev/issues/491): Fixed: Unnecessary dependency on the [Editor Toolbar](http://ckeditor.com/addon/toolbar) plugin inside the [Notification](http://ckeditor.com/addon/notification) plugin.\r
+* [#646](https://github.com/ckeditor/ckeditor-dev/issues/646): Fixed: Error thrown into the browser console after opening the [Styles Combo](http://ckeditor.com/addon/stylescombo) plugin menu in the editor without any selection.\r
+* [#501](https://github.com/ckeditor/ckeditor-dev/issues/501): Fixed: Double click does not open the dialog for modifying anchors inserted via the [Link](http://ckeditor.com/addon/link) plugin.\r
+* [#9780](https://dev.ckeditor.com/ticket/9780): [IE8-9] Fixed: Clicking inside an empty [read-only](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-readOnly) editor throws an error.\r
+* [#16820](https://dev.ckeditor.com/ticket/16820): [IE10] Fixed: Clicking below a single horizontal rule throws an error.\r
+* [#426](https://github.com/ckeditor/ckeditor-dev/issues/426): Fixed: The [`range.cloneContents`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.range-method-cloneContents) method selects the whole element when the selection starts at the beginning of that element.\r
+* [#644](https://github.com/ckeditor/ckeditor-dev/issues/644): Fixed: The [`range.extractContents`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.range-method-extractContents) method returns an incorrect result when multiple nodes are selected.\r
+* [#684](https://github.com/ckeditor/ckeditor-dev/issues/684): Fixed: The [`elementPath.contains`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.elementPath-method-contains) method incorrectly excludes the last element instead of root when the `fromTop` parameter is set to `true`.\r
+\r
+Other Changes:\r
+\r
+* Updated the [SCAYT](http://ckeditor.com/addon/scayt) (Spell Check As You Type) plugin:\r
+       * [#148](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/148): Fixed: SCAYT leaves underlined word after the CKEditor Replace dialog corrects it.\r
+* [#751](https://github.com/ckeditor/ckeditor-dev/issues/751): Added the [`CKEDITOR.dom.nodeList.toArray`](https://docs.ckeditor.com/#!/api/CKEDITOR.dom.nodeList-method-toArray) method which returns an array representation of a [node list](https://docs.ckeditor.com/#!/api/CKEDITOR.dom.nodeList).\r
+\r
+## CKEditor 4.7.1\r
+\r
+New Features:\r
+\r
+* Added a new Mexican Spanish localization. Thanks to [David Alexandro Rodriguez](https://www.transifex.com/user/profile/darsco16/)!\r
+* [#413](https://github.com/ckeditor/ckeditor-dev/issues/413): Added Paste as Plain Text keyboard shortcut to the [Accessibility Help](http://ckeditor.com/addon/a11yhelp) instructions.\r
+\r
+Fixed Issues:\r
+\r
+* [#515](https://github.com/ckeditor/ckeditor-dev/issues/515): [Chrome] Fixed: Mouse actions on CKEditor scrollbar throw an exception when the [Table Selection](http://ckeditor.com/addon/tableselection) plugin is loaded.\r
+* [#493](https://github.com/ckeditor/ckeditor-dev/issues/493): Fixed: Selection started from a nested table causes an error in the browser while scrolling down.\r
+* [#415](https://github.com/ckeditor/ckeditor-dev/issues/415): [Firefox] Fixed: <kbd>Enter</kbd> key breaks the table structure when pressed in a table selection.\r
+* [#457](https://github.com/ckeditor/ckeditor-dev/issues/457): Fixed: Error thrown when deleting content from the editor with no selection.\r
+* [#478](https://github.com/ckeditor/ckeditor-dev/issues/478): [Chrome] Fixed:  Error thrown by the [Enter Key](http://ckeditor.com/addon/enterkey) plugin when pressing <kbd>Enter</kbd> with no selection.\r
+* [#424](https://github.com/ckeditor/ckeditor-dev/issues/424): Fixed: Error thrown by [Tab Key Handling](http://ckeditor.com/addon/tab) and [Indent List](http://ckeditor.com/addon/indentlist) plugins when pressing <kbd>Tab</kbd> with no selection in inline editor.\r
+* [#476](https://github.com/ckeditor/ckeditor-dev/issues/476): Fixed: Anchors inserted with the [Link](http://ckeditor.com/addon/link) plugin on collapsed selection cannot be edited.\r
+* [#417](https://github.com/ckeditor/ckeditor-dev/issues/417): Fixed: The [Table Resize](http://ckeditor.com/addon/tableresize) plugin throws an error when used with a table with only header or footer rows.\r
+* [#523](https://github.com/ckeditor/ckeditor-dev/issues/523): Fixed: The [`editor.getCommandKeystroke`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-getCommandKeystroke) method does not obtain the correct keystroke.\r
+* [#534](https://github.com/ckeditor/ckeditor-dev/issues/534): [IE] Fixed: [Paste from Word](http://ckeditor.com/addon/pastefromword) does not work in Quirks Mode.\r
+* [#450](https://github.com/ckeditor/ckeditor-dev/issues/450): Fixed: [`CKEDITOR.filter`](http://docs.ckeditor.com/#!/api/CKEDITOR.filter) incorrectly transforms the `margin` CSS property.\r
+\r
+## CKEditor 4.7\r
+\r
+**Important Notes:**\r
+\r
+* [#13793](http://dev.ckeditor.com/ticket/13793): The [`embed_provider`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-embed_provider) configuration option for the [Media Embed](http://ckeditor.com/addon/embed) and [Semantic Media Embed](http://ckeditor.com/addon/embedsemantic) plugins is no longer preset by default.\r
+* The [UI Color](http://ckeditor.com/addon/uicolor) plugin now uses a custom color picker instead of the `YUI 2.7.0` library which has some known vulnerabilities (it's a security precaution, there was no security issue in CKEditor due to the way it was used).\r
+\r
+New Features:\r
+\r
+* [#16755](http://dev.ckeditor.com/ticket/16755): Added the [Table Selection](http://ckeditor.com/addon/tableselection) plugin that lets you select and manipulate an arbitrary rectangular table fragment (a few cells, a row or a column).\r
+* [#16961](http://dev.ckeditor.com/ticket/16961): Added support for pasting from Microsoft Excel.\r
+* [#13381](http://dev.ckeditor.com/ticket/13381): Dynamic code evaluation call in [`CKEDITOR.template`](http://docs.ckeditor.com/#!/api/CKEDITOR.template) removed. CKEditor can now be used with the `unsafe-eval` Content Security Policy. Thanks to [Caridy Patiño](http://caridy.name)!\r
+* [#16971](http://dev.ckeditor.com/ticket/16971): Added support for color in the `background` property containing also other styles for table cells in the [Table Tools](http://ckeditor.com/addon/tabletools) plugin.\r
+* [#16847](http://dev.ckeditor.com/ticket/16847): Added support for parsing and inlining any formatting created using the Microsoft Word style system to the [Paste from Word](http://ckeditor.com/addon/pastefromword) plugin.\r
+* [#16818](http://dev.ckeditor.com/ticket/16818): Added table cell height parsing in the [Paste from Word](http://ckeditor.com/addon/pastefromword) plugin.\r
+* [#16850](http://dev.ckeditor.com/ticket/16850): Added a new [`config.enableContextMenu`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-enableContextMenu) configuration option for enabling and disabling the [context menu](http://ckeditor.com/addon/contextmenu).\r
+* [#16937](http://dev.ckeditor.com/ticket/16937): The `command` parameter in [CKEDITOR.editor.getCommandKeystroke](http://docs.ckeditor.dev/#!/api/CKEDITOR.editor-method-getCommandKeystroke) now also accepts a command name as an argument.\r
+* [#17010](http://dev.ckeditor.com/ticket/17010): The [`CKEDITOR.dom.range.shrink`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.range-method-shrink) method now allows for skipping bogus `<br>` elements.\r
+\r
+Fixed Issues:\r
+\r
+* [#16935](http://dev.ckeditor.com/ticket/16935): [Chrome] Fixed: Blurring the editor in [Source Mode](http://ckeditor.com/addon/sourcearea) throws an error.\r
+* [#16825](http://dev.ckeditor.com/ticket/16825): [Chrome] Fixed: Error thrown when destroying a focused inline editor.\r
+* [#16857](http://dev.ckeditor.com/ticket/16857): Fixed: <kbd>Ctrl+Shift+V</kbd> blocked by [Copy Formatting](http://ckeditor.com/addon/copyformatting).\r
+* [#16845](https://dev.ckeditor.com/ticket/16845): [IE] Fixed: Cursor jumps to the top of the scrolled editor after focusing it when the [Copy Formatting](http://ckeditor.com/addon/copyformatting) plugin is enabled.\r
+* [#16786](http://dev.ckeditor.com/ticket/16786): Fixed: Added missing translations for the [Copy Formatting](http://ckeditor.com/addon/copyformatting) plugin.\r
+* [#14714](http://dev.ckeditor.com/ticket/14714): [WebKit/Blink] Fixed: Exception thrown on refocusing a blurred inline editor.\r
+* [#16913](http://dev.ckeditor.com/ticket/16913): [Firefox, IE] Fixed: [Paste as Plain Text](http://ckeditor.com/addon/pastetext) keystroke does not work.\r
+* [#16968](http://dev.ckeditor.com/ticket/16968): Fixed: [Safari] [Paste as Plain Text](http://ckeditor.com/addon/pastetext) is not handled by the editor.\r
+* [#16912](http://dev.ckeditor.com/ticket/16912): Fixed: Exception thrown when a single image is pasted using [Paste from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#16821](http://dev.ckeditor.com/ticket/16821): Fixed: Extraneous `<span>` elements with `height` style stacked when [pasting from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#16866](http://dev.ckeditor.com/ticket/16866): [IE, Edge] Fixed: Whitespaces not preserved when [pasting from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#16860](http://dev.ckeditor.com/ticket/16860): Fixed: Paragraphs which only look like lists incorrectly transformed into them when [pasting from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#16817](http://dev.ckeditor.com/ticket/16817): Fixed: When [pasting from Word](http://ckeditor.com/addon/pastefromword), paragraphs are transformed into lists with some corrupted data.\r
+* [#16833](http://dev.ckeditor.com/ticket/16833): [IE11] Fixed: Malformed list with headers [pasted from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#16826](http://dev.ckeditor.com/ticket/16826): [IE] Fixed: Superfluous paragraphs within lists [pasted from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#12465](http://dev.ckeditor.com/ticket/12465): Fixed: Cannot change the state of checkboxes or radio buttons if the properties dialog was invoked with a double-click.\r
+* [#13062](http://dev.ckeditor.com/ticket/13062): Fixed: Impossible to unlink when the caret is at the edge of the link.\r
+* [#13585](http://dev.ckeditor.com/ticket/13585): Fixed: Error when wrapping two adjacent `<div>` elements with a `<div>`.\r
+* [#16811](http://dev.ckeditor.com/ticket/16811): Fixed: Table alignment is not preserved by the [Paste from Word](http://ckeditor.com/addon/pastefromword) plugin.\r
+* [#16810](http://dev.ckeditor.com/ticket/16810): Fixed: Vertical align in tables is not supported by the [Paste from Word](http://ckeditor.com/addon/pastefromword) plugin.\r
+* [#11956](http://dev.ckeditor.com/ticket/11956): [Blink, IE] Fixed: [Link](http://ckeditor.com/addon/link) dialog does not open on a double click on the second word of the link with a background color or other styles.\r
+* [#10472](http://dev.ckeditor.com/ticket/10472): Fixed: Unable to use [Table Resize](http://ckeditor.com/addon/tableresize) on table header and footer.\r
+* [#14762](http://dev.ckeditor.com/ticket/14762): Fixed: Hovering over an empty table (without rows or cells) throws an error when the [Table Resize](http://ckeditor.com/addon/tableresize) plugin is active.\r
+* [#16777](https://dev.ckeditor.com/ticket/16777): [Edge] Fixed: The [Clipboard](http://ckeditor.com/addon/clipboard) plugin does not allow to drop widgets into the editor.\r
+* [#14894](https://dev.ckeditor.com/ticket/14894): [Chrome] Fixed: The editor scrolls to the top after focusing or when a dialog is opened.\r
+* [#14769](https://dev.ckeditor.com/ticket/14769): Fixed: URLs with '-' in host are not detected by the [Auto Link](http://ckeditor.com/addon/autolink) plugin.\r
+* [#16804](https://dev.ckeditor.com/ticket/16804): Fixed: Focus is not on the first menu item when the user opens a context menu or a drop-down list from the editor toolbar.\r
+* [#14407](https://dev.ckeditor.com/ticket/14407): [IE] Fixed: Non-editable widgets can be edited.\r
+* [#16927](https://dev.ckeditor.com/ticket/16927): Fixed: An error thrown if a bundle containing the [Color Button](http://ckeditor.com/addon/colorbutton) plugin is run in ES5 strict mode. Thanks to [Igor Rubinovich](https://github.com/IgorRubinovich)!\r
+* [#16920](http://dev.ckeditor.com/ticket/16920): Fixed: Several plugins not using the [Dialog](http://ckeditor.com/addon/dialog) plugin as a direct dependency.\r
+* [PR#336](https://github.com/ckeditor/ckeditor-dev/pull/336): Fixed: Typo in [`CKEDITOR.getCss`](http://docs.ckeditor.com/#!/api/CKEDITOR-method-getCss) API documentation. Thanks to [knusperpixel](https://github.com/knusperpixel)!\r
+* [#17027](http://dev.ckeditor.com/ticket/17027): Fixed: Command event data should be initialized as an empty object.\r
+* Fixed the behavior of HTML parser when parsing `src`/`srcdoc` attributes of the `<iframe>` element in a CKEditor setup with ACF turned off and without the [Iframe Dialog](http://ckeditor.com/addon/iframe) plugin. The issue was originally reported as a security issue by [Sriramk21](https://twitter.com/sriramk21) from Pegasystems and was later downgraded by the security team into a normal issue due to the requirement of having ACF turned off. Disabling [Advanced Content Filter](http://docs.ckeditor.com/#!/guide/dev_advanced_content_filter) is against [security best practices](http://docs.ckeditor.com/#!/guide/dev_best_practices-section-security), so the problem described above has not been considered a security issue as such.\r
+\r
+Other Changes:\r
+\r
+* Updated [SCAYT](http://ckeditor.com/addon/scayt) (Spell Check As You Type) and [WebSpellChecker](http://ckeditor.com/addon/wsc) plugins:\r
+       * Fixed: DOM Exception after clicking "Remove Language" on a selected word with enabled [Language](http://ckeditor.com/addon/language) plugin in SCAYT.\r
+* [#16958](http://dev.ckeditor.com/ticket/16958): Switched the default MathJax CDN provider for the [Mathematical Formulas](http://ckeditor.com/addon/mathjax) plugin from `cdn.mathjax.org` to [cdnjs](https://cdnjs.com/), due to closing of `cdn.mathjax.org` scheduled for April 30, 2017.\r
+* [#16954](http://dev.ckeditor.com/ticket/16954): Removed the paste dialog.\r
+* [#16982](http://dev.ckeditor.com/ticket/16982): Latest Safari now supports enhanced Clipboard API introduced in CKEditor 4.5.0.\r
+* [#17025](http://dev.ckeditor.com/ticket/17025): Updated [Bender.js](https://github.com/benderjs/benderjs) to 0.4.2.\r
+\r
+## CKEditor 4.6.2\r
+\r
+New Features:\r
+\r
+* [#16733](http://dev.ckeditor.com/ticket/16733): Added a new pastel color palette for the [Color Button](http://ckeditor.com/addon/colorbutton) plugin and a new [`config.colorButton_colorsPerRow`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-colorButton_colorsPerRow) configuration option for setting the number of rows in the color selector.\r
+* [#16752](http://dev.ckeditor.com/ticket/16752): Added a new Azerbaijani localization. Thanks to the [Azerbaijani language team](https://www.transifex.com/ckeditor/teams/11143/az/)!\r
+* [#13818](http://dev.ckeditor.com/ticket/13818): It is now possible to group [Widget](http://ckeditor.com/addon/widget) [style definitions](http://docs.ckeditor.com/#!/guide/dev_styles-section-widget-styles), so applying one style disables the other.\r
+\r
+Fixed Issues:\r
+\r
+* [#13446](http://dev.ckeditor.com/ticket/13446): [Chrome] Fixed: It is possible to type in an unfocused inline editor.\r
+* [#14856](http://dev.ckeditor.com/ticket/14856): Fixed: [Font size and font family](http://ckeditor.com/addon/font) reset each other when modified at certain positions.\r
+* [#16745](http://dev.ckeditor.com/ticket/16745): [Edge] Fixed: List items are lost when [pasted from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#16682](http://dev.ckeditor.com/ticket/16682): [Edge] Fixed: A list gets [pasted from Word](http://ckeditor.com/addon/pastefromword) as a set of paragraphs. Added the [`config.pasteFromWord_heuristicsEdgeList`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-pasteFromWord_heuristicsEdgeList) configuration option.\r
+* [#10373](http://dev.ckeditor.com/ticket/10373): Fixed: Context menu items can be dragged into the editor.\r
+* [#16728](http://dev.ckeditor.com/ticket/16728): [IE] Fixed: [Copy Formatting](http://ckeditor.com/addon/copyformatting) breaks the editor in Quirks Mode.\r
+* [#16795](http://dev.ckeditor.com/ticket/16795): [IE] Fixed: [Copy Formatting](http://ckeditor.com/addon/copyformatting) breaks the editor in Compatibility Mode.\r
+* [#16675](http://dev.ckeditor.com/ticket/16675): Fixed: Styles applied with [Copy Formatting](http://ckeditor.com/addon/copyformatting) to a single table cell are applied to the whole table.\r
+* [#16753](http://dev.ckeditor.com/ticket/16753): Fixed: [`element.setSize`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.element-method-setSize) sets incorrect editor dimensions if the border width is represented as a fraction of pixels.\r
+* [#16705](http://dev.ckeditor.com/ticket/16705): [Firefox] Fixed: Unable to paste images as Base64 strings when using [Clipboard](http://ckeditor.com/addon/clipboard).\r
+* [#14869](http://dev.ckeditor.com/ticket/14869): Fixed: JavaScript error is thrown when trying to use [Find](http://ckeditor.com/addon/find) in a [`<div>`-based editor](http://ckeditor.com/addon/divarea).\r
+\r
+## CKEditor 4.6.1\r
+\r
+New Features:\r
+\r
+* [#16639](http://dev.ckeditor.com/ticket/16639): The `callback` parameter in the [CKEDITOR.ajax.post](http://docs.ckeditor.com/#!/api/CKEDITOR.ajax-method-post) method became optional.\r
+\r
+Fixed Issues:\r
+\r
+* [#11064](http://dev.ckeditor.com/ticket/11064): [Blink, WebKit] Fixed: Cannot select all editor content when a widget or a non-editable element is the first or last element of the content. Also fixes this issue in the [Select All](http://ckeditor.com/addon/selectall) plugin.\r
+* [#14755](http://dev.ckeditor.com/ticket/14755): [Blink, WebKit, IE8] Fixed: Browser hangs when a table is inserted in the place of a selected list with an empty last item.\r
+* [#16624](http://dev.ckeditor.com/ticket/16624): Fixed: Improved the [Color Button](http://ckeditor.com/addon/colorbutton) plugin which will now normalize the CSS `background` property if it only contains a color value. This fixes missing background colors when using [Paste from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#16600](http://dev.ckeditor.com/ticket/16600): [Blink, WebKit] Fixed: Error thrown occasionally by an uninitialized editable for multiple CKEditor instances on the same page.\r
+\r
+## CKEditor 4.6\r
+\r
+New Features:\r
+\r
+* [#14569](http://dev.ckeditor.com/ticket/14569): Added a new, flat, default CKEditor skin called [Moono-Lisa](http://ckeditor.com/addon/moono-lisa). Refreshed default colors available in the [Color Button](http://ckeditor.com/addon/colorbutton) plugin ([Text Color and Background Color](http://docs.ckeditor.com/#!/guide/dev_colorbutton) feature).\r
+* [#14707](http://dev.ckeditor.com/ticket/14707): Added a new [Copy Formatting](http://ckeditor.com/addon/copyformatting) feature to enable easy copying of styles between your document parts.\r
+* Introduced the completely rewritten [Paste from Word](http://ckeditor.com/addon/pastefromword) plugin:\r
+       * Backward incompatibility: The [`config.pasteFromWordRemoveFontStyles`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-pasteFromWordRemoveFontStyles) option now defaults to `false`. This option will be deprecated in the future. Use [Advanced Content Filter](http://docs.ckeditor.com/#!/guide/dev_acf) to replicate the effect of setting it to `true`.\r
+       * Backward incompatibility: The [`config.pasteFromWordNumberedHeadingToList`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-pasteFromWordNumberedHeadingToList) and [`config.pasteFromWordRemoveStyles`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-pasteFromWordRemoveStyles) options were dropped and no longer have any effect on pasted content.\r
+       * Major improvements in preservation of list numbering, styling and indentation (nested lists with multiple levels).\r
+       * Major improvements in document structure parsing that fix plenty of issues with distorted or missing content after paste.\r
+* Added new translation: Occitan. Thanks to [Cédric Valmary](https://totenoc.eu/)!\r
+* [#10015](http://dev.ckeditor.com/ticket/10015): Keyboard shortcuts (relevant to the operating system in use) will now be displayed in tooltips and context menus.\r
+* [#13794](http://dev.ckeditor.com/ticket/13794): The [Upload Image](http://ckeditor.com/addon/uploadimage) feature now uses `uploaded.width/height` if set.\r
+* [#12541](http://dev.ckeditor.com/ticket/12541): Added the [Upload File](http://ckeditor.com/addon/uploadfile) plugin that lets you upload a file by drag&amp;dropping it into the editor content.\r
+* [#14449](http://dev.ckeditor.com/ticket/14449): Introduced the [Balloon Panel](http://ckeditor.com/addon/balloonpanel) plugin that lets you create stylish floating UI elements for the editor.\r
+* [#12077](https://dev.ckeditor.com/ticket/12077): Added support for the HTML5 `download` attribute in link (`<a>`) elements. Selecting the "Force Download" checkbox in the [Link](http://ckeditor.com/addon/link) dialog will cause the linked file to be downloaded automatically. Thanks to [sbusse](https://github.com/sbusse)!\r
+* [#13518](http://dev.ckeditor.com/ticket/13518): Introduced the [`additionalRequestParameters`](http://docs.ckeditor.com/#!/api/CKEDITOR.fileTools.uploadWidgetDefinition-property-additionalRequestParameters) property for file uploads to make it possible to send additional information about the uploaded file to the server.\r
+* [#14889](http://dev.ckeditor.com/ticket/14889): Added the [`config.image2_altRequired`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-image2_altRequired) option for the [Enhanced Image](http://ckeditor.com/addon/image2) plugin to allow making alternative text a mandatory field. Thanks to [Andrey Fedoseev](https://github.com/andreyfedoseev)!\r
+\r
+Fixed Issues:\r
+\r
+* [#9991](http://dev.ckeditor.com/ticket/9991): Fixed: [Paste from Word](http://ckeditor.com/addon/pastefromword) should only normalize input data.\r
+* [#7209](http://dev.ckeditor.com/ticket/7209): Fixed: Lists with 3 levels not [pasted from Word](http://ckeditor.com/addon/pastefromword) correctly.\r
+* [#14335](http://dev.ckeditor.com/ticket/14335): Fixed: Pasting a numbered list starting with a value different from "1" from Microsoft Word does not work correctly.\r
+* [#14542](http://dev.ckeditor.com/ticket/14542): Fixed: Copying a numbered list from Microsoft Word does not preserve list formatting.\r
+* [#14544](http://dev.ckeditor.com/ticket/14544): Fixed: Copying a nested list from Microsoft Word results in an empty list.\r
+* [#14660](http://dev.ckeditor.com/ticket/14660): Fixed: [Pasting text from  Word](http://ckeditor.com/addon/pastefromword) breaks the styling in some cases.\r
+* [#14867](http://dev.ckeditor.com/ticket/14867): [Firefox] Fixed: Text gets stripped when [pasting content from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#2507](http://dev.ckeditor.com/ticket/2507): Fixed: [Paste from Word](http://ckeditor.com/addon/pastefromword) does not detect pasting a part of a paragraph.\r
+* [#3336](http://dev.ckeditor.com/ticket/3336): Fixed: Extra blank row added on top of the content [pasted from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#6115](http://dev.ckeditor.com/ticket/6115): Fixed: When Right-to-Left text direction is applied to a table [pasted from Word](http://ckeditor.com/addon/pastefromword), borders are missing on one side.\r
+* [#6342](http://dev.ckeditor.com/ticket/6342): Fixed: [Paste from Word](http://ckeditor.com/addon/pastefromword) filters out a basic text style when it is [configured to use attributes](http://docs.ckeditor.com/#!/guide/dev_basicstyles-section-custom-basic-text-style-definition).\r
+* [#6457](http://dev.ckeditor.com/ticket/6457): [IE] Fixed: [Pasting from Word](http://ckeditor.com/addon/pastefromword) is extremely slow.\r
+* [#6789](http://dev.ckeditor.com/ticket/6789): Fixed: The `mso-list: ignore` style is not handled properly when [pasting from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#7262](http://dev.ckeditor.com/ticket/7262): Fixed: Lists in preformatted body disappear when [pasting from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#7662](http://dev.ckeditor.com/ticket/7662): [Opera] Fixed: Extra empty number/bullet shown in the editor body when editing a multi-level list [pasted from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#7807](http://dev.ckeditor.com/ticket/7807): Fixed: Last item in a list not converted to a `<li>` element after [pasting from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#7950](http://dev.ckeditor.com/ticket/7950): [IE] Fixed: Content [from Word pasted](http://ckeditor.com/addon/pastefromword) differently than in other browsers.\r
+* [#7982](http://dev.ckeditor.com/ticket/7982): Fixed: Multi-level lists get split into smaller ones when [pasting from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#8231](http://dev.ckeditor.com/ticket/8231): [WebKit, Opera] Fixed: [Paste from Word](http://ckeditor.com/addon/pastefromword) inserts empty paragraphs.\r
+* [#8266](http://dev.ckeditor.com/ticket/8266): Fixed: [Paste from Word](http://ckeditor.com/addon/pastefromword) inserts a blank line at the top.\r
+* [#8341](http://dev.ckeditor.com/ticket/8341), [#7646](http://dev.ckeditor.com/ticket/7646): Fixed: Faulty removal of empty `<span>` elements in [Paste from Word](http://ckeditor.com/addon/pastefromword) content cleanup breaking content formatting.\r
+* [#8754](http://dev.ckeditor.com/ticket/8754): [Firefox] Fixed: Incorrect pasting of multiple nested lists in [Paste from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#8983](http://dev.ckeditor.com/ticket/8983): Fixed: Alignment lost when [pasting from Word](http://ckeditor.com/addon/pastefromword) with [`config.enterMode`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-enterMode) set to [`CKEDITOR.ENTER_BR`](http://docs.ckeditor.com/#!/api/CKEDITOR-property-ENTER_BR).\r
+* [#9331](http://dev.ckeditor.com/ticket/9331): [IE] Fixed: [Pasting text from Word](http://ckeditor.com/addon/pastefromword) creates a simple Caesar cipher.\r
+* [#9422](http://dev.ckeditor.com/ticket/9422): Fixed: [Paste from Word](http://ckeditor.com/addon/pastefromword) leaves an unwanted `color:windowtext` style.\r
+* [#10011](http://dev.ckeditor.com/ticket/10011): [IE9-10] Fixed: [`config.pasteFromWordRemoveFontStyles`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-pasteFromWordRemoveFontStyles) is ignored under certain conditions.\r
+* [#10643](http://dev.ckeditor.com/ticket/10643): Fixed: Differences between using <kbd>Ctrl+V</kbd> and pasting from the [Paste from Word](http://ckeditor.com/addon/pastefromword) dialog.\r
+* [#10784](http://dev.ckeditor.com/ticket/10784): Fixed: Lines missing when [pasting from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#11294](http://dev.ckeditor.com/ticket/11294): [IE10] Fixed: Font size is not preserved when [pasting from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#11627](http://dev.ckeditor.com/ticket/11627): Fixed: Missing words when [pasting from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#12784](http://dev.ckeditor.com/ticket/12784): Fixed: Bulleted list with custom bullets gets changed to a numbered list when [pasting from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#13174](http://dev.ckeditor.com/ticket/13174): Fixed: Data loss after [pasting from Word](http://ckeditor.com/addon/pastefromword).\r
+* [#13828](http://dev.ckeditor.com/ticket/13828): Fixed: Widget classes should be added to the wrapper rather than the widget element.\r
+* [#13829](http://dev.ckeditor.com/ticket/13829): Fixed: No class in [Widget](http://ckeditor.com/addon/widget) wrapper to identify the widget type.\r
+* [#13519](http://dev.ckeditor.com/ticket/13519): Server response received when uploading files should be more flexible.\r
+\r
+Other Changes:\r
+\r
+* Updated [SCAYT](http://ckeditor.com/addon/scayt) (Spell Check As You Type) and [WebSpellChecker](http://ckeditor.com/addon/wsc) plugins:\r
+       * Support for the new default Moono-Lisa skin.\r
+       * [#121](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/121): Fixed: [Basic Styles](http://ckeditor.com/addon/basicstyles) do not work when SCAYT is enabled.\r
+       * [#125](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/125): Fixed: Inline styles are not continued when writing multiple lines of styled text with SCAYT enabled.\r
+       * [#127](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/127): Fixed: Uncaught TypeError after enabling SCAYT in the CKEditor `<div>` element.\r
+       * [#128](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/128): Fixed: Error thrown after enabling SCAYT caused by conflicts with RequireJS.\r
+\r
+## CKEditor 4.5.11\r
+\r
+**Security Updates:**\r
+\r
+* [Severity: minor] Fixed the `target="_blank"` vulnerability reported by James Gaskell.\r
+\r
+       Issue summary: If a victim had access to a spoofed version of ckeditor.com via HTTP (e.g. due to DNS spoofing, using a hacked public network or mailicious hotspot), then when using a link to the ckeditor.com website it was possible for the attacker to change the current URL of the opening page, even if the opening page was protected with SSL.\r
+\r
+  An upgrade is recommended.\r
+\r
+New Features:\r
+\r
+* [#14747](http://dev.ckeditor.com/ticket/14747): The [Enhanced Image](http://ckeditor.com/addon/image2) caption now supports the link `target` attribute.\r
+* [#7154](http://dev.ckeditor.com/ticket/7154): Added support for the "Display Text" field to the [Link](http://ckeditor.com/addon/link) dialog. Thanks to [Ryan Guill](https://github.com/ryanguill)!\r
+\r
+Fixed Issues:\r
+\r
+* [#13362](http://dev.ckeditor.com/ticket/13362): [Blink, WebKit] Fixed: Active widget element is not cached when it is losing focus and it is inside an editable element.\r
+* [#13755](http://dev.ckeditor.com/ticket/13755): [Edge] Fixed: Pasting images does not work.\r
+* [#13548](http://dev.ckeditor.com/ticket/13548): [IE] Fixed: Clicking the [elements path](http://ckeditor.com/addon/elementspath) disables Cut and Copy icons.\r
+* [#13812](http://dev.ckeditor.com/ticket/13812): Fixed: When aborting file upload the placeholder for image is left.\r
+* [#14659](http://dev.ckeditor.com/ticket/14659): [Blink] Fixed: Content scrolled to the top after closing the dialog in a [`<div>`-based editor](http://ckeditor.com/addon/divarea).\r
+* [#14825](http://dev.ckeditor.com/ticket/14825): [Edge] Fixed: Focusing the editor causes unwanted scrolling due to dropped support for the `setActive` method.\r
+\r
+## CKEditor 4.5.10\r
+\r
+Fixed Issues:\r
+\r
+* [#10750](http://dev.ckeditor.com/ticket/10750): Fixed: The editor does not escape the `font-style` family property correctly, removing quotes and whitespace from font names.\r
+* [#14413](http://dev.ckeditor.com/ticket/14413): Fixed: The [Auto Grow](http://ckeditor.com/addon/autogrow) plugin with the [`config.autoGrow_onStartup`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-autoGrow_onStartup) option set to `true` does not work properly for an editor that is not visible.\r
+* [#14451](http://dev.ckeditor.com/ticket/14451): Fixed: Numeric element ID not escaped properly. Thanks to [Jakub Chalupa](https://github.com/chaluja7)!\r
+* [#14590](http://dev.ckeditor.com/ticket/14590): Fixed: Additional line break appearing after inline elements when switching modes. Thanks to [dpidcock](https://github.com/dpidcock)!\r
+* [#14539](https://dev.ckeditor.com/ticket/14539): Fixed: JAWS reads "selected Blank" instead of "selected <widget name>" when selecting a widget.\r
+* [#14701](http://dev.ckeditor.com/ticket/14701): Fixed: More precise labels for [Enhanced Image](http://ckeditor.com/addon/image2) and [Placeholder](http://ckeditor.com/addon/placeholder) widgets.\r
+* [#14667](http://dev.ckeditor.com/ticket/14667): [IE] Fixed: Removing background color from selected text removes background color from the whole paragraph.\r
+* [#14252](http://dev.ckeditor.com/ticket/14252): [IE] Fixed: Styles drop-down list does not always reflect the current style of the text line.\r
+* [#14275](http://dev.ckeditor.com/ticket/14275): [IE9+] Fixed: `onerror` and `onload` events are not used in browsers it could have been used when loading scripts dynamically.\r
+\r
+## CKEditor 4.5.9\r
+\r
+Fixed Issues:\r
+\r
+* [#10685](http://dev.ckeditor.com/ticket/10685): Fixed: Unreadable toolbar icons after updating to the new editor version. Fixed with [6876179](https://github.com/ckeditor/ckeditor-dev/commit/6876179db4ee97e786b07b8fd72e6b4120732185) in [ckeditor-dev](https://github.com/ckeditor/ckeditor-dev) and [6c9189f4](https://github.com/ckeditor/ckeditor-presets/commit/6c9189f46392d2c126854fe8889b820b8c76d291) in [ckeditor-presets](https://github.com/ckeditor/ckeditor-presets).\r
+* [#14573](https://dev.ckeditor.com/ticket/14573): Fixed: Missing [Widget](http://ckeditor.com/addon/widget) drag handler CSS when there are multiple editor instances.\r
+* [#14620](https://dev.ckeditor.com/ticket/14620): Fixed: Setting both the `min-height` style for the `<body>` element and the `height` style for the `<html>` element breaks the [Auto Grow](http://ckeditor.com/addon/autogrow) plugin.\r
+* [#14538](http://dev.ckeditor.com/ticket/14538): Fixed: Keyboard focus goes into an embedded `<iframe>` element.\r
+* [#14602](http://dev.ckeditor.com/ticket/14602): Fixed: The [`dom.element.removeAttribute()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.element-method-removeAttribute) method does not remove all attributes if no parameter is given.\r
+* [#8679](http://dev.ckeditor.com/ticket/8679): Fixed: Better focus indication and ability to style the selected color in the [color picker dialog](http://ckeditor.com/addon/colordialog).\r
+* [#11697](http://dev.ckeditor.com/ticket/11697): Fixed: Content is replaced ignoring the letter case setting in the [Find and Replace](http://ckeditor.com/addon/find) dialog window.\r
+* [#13886](http://dev.ckeditor.com/ticket/13886): Fixed: Invalid handling of the [`CKEDITOR.style`](http://docs.ckeditor.com/#!/api/CKEDITOR.style) instance with the `styles` property by [`CKEDITOR.filter`](http://docs.ckeditor.com/#!/api/CKEDITOR.filter).\r
+* [#14535](http://dev.ckeditor.com/ticket/14535): Fixed: CSS syntax corrections. Thanks to [mdjdenormandie](https://github.com/mdjdenormandie)!\r
+\r
+## CKEditor 4.5.8\r
+\r
+New Features:\r
+\r
+* [#12440](http://dev.ckeditor.com/ticket/12440): Added the [`config.colorButton_enableAutomatic`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-colorButton_enableAutomatic) option to allow hiding the "Automatic" option in the [color picker](http://ckeditor.com/addon/colorbutton).\r
+\r
+Fixed Issues:\r
+\r
+* [#10448](http://dev.ckeditor.com/ticket/10448): Fixed: Lack of scrollbar in the [right-to-left text direction](http://ckeditor.com/addon/bidi).\r
+* [#12707](http://dev.ckeditor.com/ticket/12707): Fixed: The order of table elements does not comply with the HTML specification.\r
+* [#13756](http://dev.ckeditor.com/ticket/13756): [Edge] Fixed: Context menus are cut-off.\r
+\r
+## CKEditor 4.5.7\r
+\r
+New Features:\r
+\r
+* [#14327](http://dev.ckeditor.com/ticket/14327): Added Swiss German localization. Thanks to [Miro Grenda](https://twitter.com/mirogrenda)!\r
+\r
+Fixed Issues:\r
+\r
+* [#13816](http://dev.ckeditor.com/ticket/13816): Introduced a new strategy for Filling Character handling to avoid changes in DOM. This fixes the following issues:\r
+       * [#12727](http://dev.ckeditor.com/ticket/12727): [Blink] `IndexSizeError` when using the [Div Editing Area](http://ckeditor.com/addon/divarea) and [Content Templates](http://ckeditor.com/addon/templates) plugins.\r
+       * [#13377](http://dev.ckeditor.com/ticket/13377): [Widget](http://ckeditor.com/addon/widget) plugin issue when typing in Korean.\r
+       * [#13389](http://dev.ckeditor.com/ticket/13389): [Blink] [`editor.getData()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-getData) fails when the cursor is next to an `<hr>` tag.\r
+       * [#13513](http://dev.ckeditor.com/ticket/13513): [Blink, WebKit] [Div Editing Area](http://ckeditor.com/addon/divarea) and [`editor.getData()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-getData) throw an error when an image is the only data in the editor.\r
+* [#13884](http://dev.ckeditor.com/ticket/13884): [Firefox] Fixed: Copying and pasting a table results in just the first cell being pasted.\r
+* [#14234](http://dev.ckeditor.com/ticket/14234): Fixed: URL input field is not marked as required in the [Media Embed](http://ckeditor.com/addon/embed) dialog.\r
+\r
+## CKEditor 4.5.6\r
+\r
+New Features:\r
+\r
+* Introduced the [`CKEDITOR.tools.getCookie()`](http://docs.ckeditor.com/#!/api/CKEDITOR.tools-method-getCookie) and [`CKEDITOR.tools.setCookie()`](http://docs.ckeditor.com/#!/api/CKEDITOR.tools-method-setCookie) methods for accessing cookies.\r
+* Introduced the [`CKEDITOR.tools.getCsrfToken()`](http://docs.ckeditor.com/#!/api/CKEDITOR.tools-method-getCsrfToken) method. The CSRF token is now automatically sent by the [File Browser](http://ckeditor.com/addon/filebrowser) and [File Tools](http://ckeditor.com/addon/filetools) plugins during file uploads. The server-side upload handlers may check it and use it to additionally secure the communication.\r
+\r
+Other Changes:\r
+\r
+* Updated [SCAYT](http://ckeditor.com/addon/scayt) (Spell Check As You Type):\r
+       - New features:\r
+               - CKEditor [Language](http://ckeditor.com/addon/language) plugin support.\r
+               - CKEditor [Placeholder](http://ckeditor.com/addon/placeholder) plugin support.\r
+               - [Drag&Drop](http://sdk.ckeditor.com/samples/fileupload.html) support.\r
+               - **Experimental** [GRAYT](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-grayt_autoStartup) (Grammar As You Type) functionality.\r
+       - Fixed issues:\r
+               * [#98](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/98): SCAYT affects dialog double-click. Fixed in SCAYT core.\r
+               * [#102](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/102): SCAYT core performance enhancements.\r
+               * [#104](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/104): SCAYT's spans leak into the clipboard and after pasting.\r
+               * [#105](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/105): A JavaScript error fired in case of multiple instances of CKEditor on one page.\r
+               * [#107](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/107): SCAYT should not check non-editable parts of content.\r
+               * [#108](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/108): Latest SCAYT copies the ID of the editor element to the iframe.\r
+               * SCAYT stops working when CKEditor [Undo plugin](http://ckeditor.com/addon/undo) not enabled.\r
+               * Issue with pasting SCAYT markup in CKEditor.\r
+               * SCAYT stops working after pressing the *Cancel* button in the WSC dialog.\r
+\r
+## CKEditor 4.5.5\r
+\r
+Fixed Issues:\r
+\r
+* [#13887](https://dev.ckeditor.com/ticket/13887): Fixed: [Link](http://ckeditor.com/addon/link) plugin alters the `target` attribute value. Thanks to [SamZiemer](https://github.com/SamZiemer)!\r
+* [#12189](http://dev.ckeditor.com/ticket/12189): Fixed: The [Link](http://ckeditor.com/addon/link) plugin dialog does not display the subject of email links if the subject parameter is not lowercase.\r
+* [#9192](http://dev.ckeditor.com/ticket/9192): Fixed: An `undefined` string is appended to an email address added with the [Link](http://ckeditor.com/addon/link) plugin if subject and email body are empty and [`config.emailProtection`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-emailProtection) is set to `encode`.\r
+* [#13790](https://dev.ckeditor.com/ticket/13790): Fixed: It is not possible to destroy the editor `<iframe>` after the editor was detached from DOM. Thanks to [Stefan Rijnhart](https://github.com/StefanRijnhart)!\r
+* [#13803](https://dev.ckeditor.com/ticket/13803): Fixed: The editor cannot be destroyed before being fully initialized. Thanks to [Cyril Fluck](https://github.com/cyril-sf)!\r
+* [#13867](http://dev.ckeditor.com/ticket/13867): Fixed: CKEditor does not work when the `classList` polyfill is used.\r
+* [#13885](http://dev.ckeditor.com/ticket/13885): Fixed: [Enhanced Image](http://ckeditor.com/addon/image2) requires the [Link](http://ckeditor.com/addon/link) plugin to link an image.\r
+* [#13883](http://dev.ckeditor.com/ticket/13883): Fixed: Copying a table using the context menu strips off styles.\r
+* [#13872](http://dev.ckeditor.com/ticket/13872): Fixed: Cutting is possible in the [read-only](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-readOnly) mode.\r
+* [#12848](http://dev.ckeditor.com/ticket/12848): [Blink] Fixed: Opening the [Find and Replace](http://ckeditor.com/addon/find) dialog window in the [read-only](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-readOnly) mode throws an exception.\r
+* [#13879](http://dev.ckeditor.com/ticket/13879): Fixed: It is not possible to prevent the [`editor.drop`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-drop) event.\r
+* [#13361](http://dev.ckeditor.com/ticket/13361): Fixed: Skin images fail when the site path includes parentheses because the `background-image` path needs single quotes around the URL value.\r
+* [#13771](http://dev.ckeditor.com/ticket/13771): Fixed: The `contents.css` style is not used if the [IFrame Editing Area](http://ckeditor.com/addon/wysiwygarea) plugin is missing.\r
+* [#13782](http://dev.ckeditor.com/ticket/13782): Fixed: Unclear log messages.\r
+* [#13919](http://dev.ckeditor.com/ticket/13919): [Edge] Fixed: Browser window crashes when accessing the `isContentEditable` property of an `<input>` DOM element.\r
+\r
+Other Changes:\r
+\r
+* [#13859](http://dev.ckeditor.com/ticket/13859): Test cases created with `bender.tools.createTestsForEditors` will also receive editor bot as a second parameter.\r
+\r
+## CKEditor 4.5.4\r
+\r
+New Features:\r
+\r
+* [#13632](http://dev.ckeditor.com/ticket/13632): Introduce error logging mechanism.\r
+* [#13730](http://dev.ckeditor.com/ticket/13730): Switch to the new error logging mechanism.\r
+\r
+Fixed Issues:\r
+\r
+* [#9856](http://dev.ckeditor.com/ticket/9856): Fixed: Cannot use the native context menu together with the [Div Editing Area](http://ckeditor.com/addon/divarea) plugin. Thanks to [Mark Wade](https://github.com/mark-wade)!\r
+* [#12733](http://dev.ckeditor.com/ticket/12733): [IE9+] Fixed: Radio button `onChange` does not work. Thanks to [Iliya Kostadinov](https://github.com/iliyakostadinov)!\r
+* [#13142](http://dev.ckeditor.com/ticket/13142): [Edge] Fixed: *Ctrl+A* and then *Backspace* result in an empty `<div>` element.\r
+* [#13599](http://dev.ckeditor.com/ticket/13599): Fixed: Cross-editor drag and drop of an inline widget results in error/artifacts.\r
+* [#13640](http://dev.ckeditor.com/ticket/13640): [IE] Fixed: Dropping a widget outside the `<body>` element is not handled correctly.\r
+* [#13533](http://dev.ckeditor.com/ticket/13533): Fixed: No progress during upload.\r
+* [#13680](http://dev.ckeditor.com/ticket/13680): Fixed: The parser should allow the `<h1-6>` element to be a child of the `<summary>` element.\r
+* [#11724](http://dev.ckeditor.com/ticket/11724): [Touch devices] Fixed: Drop-downs often hide right after opening them.\r
+* [#13690](http://dev.ckeditor.com/ticket/13690): Fixed: Copying content from IE to Chrome adds an extra paragraph.\r
+* [#13284](http://dev.ckeditor.com/ticket/13284): Fixed: Cannot drag and drop a widget if the text caret is placed just after the widget instance.\r
+* [#13516](http://dev.ckeditor.com/ticket/13516): Fixed: CKEditor removes empty HTML5 anchors without the `name` attribute.\r
+* [#13765](http://dev.ckeditor.com/ticket/13765): [Safari 9] Fixed: Problems with rendering samples.\r
+\r
+Other Changes:\r
+\r
+* [#11725](http://dev.ckeditor.com/ticket/11725): Marked [`CKEDITOR.env.mobile`](http://docs.ckeditor.com/#!/api/CKEDITOR.env-property-mobile) as deprecated. The reason is that it is no longer clear what "mobile" means.\r
+* [#13737](http://dev.ckeditor.com/ticket/13737): Upgraded [Bender.js](https://github.com/benderjs/benderjs) to 0.4.1.\r
+\r
+## CKEditor 4.5.3\r
+\r
+New Features:\r
+\r
+* [#13501](http://dev.ckeditor.com/ticket/13501): Added the [`config.fileTools_defaultFileName`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-fileTools_defaultFileName) option to allow setting a default file name for paste uploads.\r
+* [#13603](http://dev.ckeditor.com/ticket/13603): Added support for uploading dropped BMP images.\r
+\r
+Fixed Issues:\r
+\r
+* [#13590](http://dev.ckeditor.com/ticket/13590): Fixed: Various issues related to the [Paste from Word](http://ckeditor.com/addon/pastefromword) feature. Fixes also:\r
+  * [#11215](http://dev.ckeditor.com/ticket/11215),\r
+  * [#8780](http://dev.ckeditor.com/ticket/8780),\r
+  * [#12762](http://dev.ckeditor.com/ticket/12762).\r
+* [#13386](http://dev.ckeditor.com/ticket/13386): [Edge] Fixed: Issues with selecting and editing images.\r
+* [#13568](http://dev.ckeditor.com/ticket/13568): Fixed: The [`editor.getSelectedHtml()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-getSelectedHtml) method returns invalid results for entire content selection.\r
+* [#13453](http://dev.ckeditor.com/ticket/13453): Fixed: Drag&drop of entire editor content throws an error.\r
+* [#13465](http://dev.ckeditor.com/ticket/13465): Fixed: Error is thrown and the widget is lost on drag&drop if it is the only content of the editor.\r
+* [#13414](http://dev.ckeditor.com/ticket/13414): Fixed: Content auto paragraphing in a nested editable despite editor configuration.\r
+* [#13429](http://dev.ckeditor.com/ticket/13429): Fixed: Incorrect selection after content insertion by the [Auto Embed](http://ckeditor.com/addon/autoembed) plugin.\r
+* [#13388](http://dev.ckeditor.com/ticket/13388): Fixed: [Table Resize](http://ckeditor.com/addon/tableresize) integration with [Undo](http://ckeditor.com/addon/undo) is broken.\r
+\r
+Other Changes:\r
+\r
+* [#13637](https://dev.ckeditor.com/ticket/13637): Several icons were refactored.\r
+* Updated [Bender.js](https://github.com/benderjs/benderjs) to 0.3.0 and introduced the ability to run tests via HTTPs ([#13265](https://dev.ckeditor.com/ticket/13265)).\r
+\r
+## CKEditor 4.5.2\r
+\r
+Fixed Issues:\r
+\r
+* [#13609](http://dev.ckeditor.com/ticket/13609): [Edge] Fixed: The browser crashes when switching to the source mode. Thanks to [Andrew Williams and Mark Smeed](http://webxsolution.com/)!\r
+* [PR#201](https://github.com/ckeditor/ckeditor-dev/pull/201): Fixed: Buttons in the toolbar configurator cause form submission. Thanks to [colemanw](https://github.com/colemanw)!\r
+* [#13422](http://dev.ckeditor.com/ticket/13422): Fixed: A monospaced font should be used in the `<textarea>` element storing editor configuration in the toolbar configurator.\r
+* [#13494](http://dev.ckeditor.com/ticket/13494): Fixed: Error thrown in the toolbar configurator if plugin requirements are not met.\r
+* [#13409](http://dev.ckeditor.com/ticket/13409): Fixed: List elements incorrectly merged when pressing *Backspace* or *Delete*.\r
+* [#13434](http://dev.ckeditor.com/ticket/13434): Fixed: Dialog state indicator broken in Right–To–Left environments.\r
+* [#13460](http://dev.ckeditor.com/ticket/13460): [IE8] Fixed: Copying inline widgets is broken when [Advanced Content Filter](http://docs.ckeditor.com/#!/guide/dev_acf) is disabled.\r
+* [#13495](http://dev.ckeditor.com/ticket/13495): [Firefox, IE] Fixed: Text is not word-wrapped in the Paste dialog window.\r
+* [#13528](http://dev.ckeditor.com/ticket/13528): [Firefox@Windows] Fixed: Content copied from Microsoft Word and other external applications is pasted as a plain text. Removed the `CKEDITOR.plugins.clipboard.isHtmlInExternalDataTransfer` property as the check must be dynamic.\r
+* [#13583](http://dev.ckeditor.com/ticket/13583): Fixed: [`DataTransfer.getData()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.clipboard.dataTransfer-method-getData) should work consistently in all browsers and should not strip valuable content. Fixed pasting tables from Microsoft Excel on Chrome.\r
+* [#13468](http://dev.ckeditor.com/ticket/13468): [IE] Fixed: Binding drag&drop `dataTransfer` does not work if `text` data was set in the meantime.\r
+* [#13451](http://dev.ckeditor.com/ticket/13451): [IE8-9] Fixed: One drag&drop operation may affect following ones.\r
+* [#13184](http://dev.ckeditor.com/ticket/13184): Fixed: Web page reloaded after a drop on editor UI.\r
+* [#13129](http://dev.ckeditor.com/ticket/13129) Fixed: Block widget blurred after a drop followed by an undo.\r
+* [#13397](http://dev.ckeditor.com/ticket/13397): Fixed: Drag&drop of a widget inside its nested widget crashes the editor.\r
+* [#13385](http://dev.ckeditor.com/ticket/13385): Fixed: [`editor.getSnapshot()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-getSnapshot) may return a non-string value.\r
+* [#13419](http://dev.ckeditor.com/ticket/13419): Fixed: The [Auto Link](http://ckeditor.com/addon/autolink) plugin does not encode double quotes in URLs.\r
+* [#13420](http://dev.ckeditor.com/ticket/13420): Fixed: The [Auto Embed](http://ckeditor.com/addon/autoembed) plugin ignores encoded characters in URL parameters.\r
+* [#13410](http://dev.ckeditor.com/ticket/13410): Fixed: Error thrown in the [Auto Embed](http://ckeditor.com/addon/autoembed) plugin when undoing right after pasting a link.\r
+* [#13566](http://dev.ckeditor.com/ticket/13566): Fixed: Suppressed notifications in the [Media Embed Base](http://ckeditor.com/addon/embedbase) plugin.\r
+* [#11616](http://dev.ckeditor.com/ticket/11616): [Chrome] Fixed: Resizing the editor while it is not displayed breaks the editable. Fixes also [#9160](http://dev.ckeditor.com/ticket/9160) and [#9715](http://dev.ckeditor.com/ticket/9715).\r
+* [#11376](http://dev.ckeditor.com/ticket/11376): [IE11] Fixed: Loss of text when pasting bulleted lists from Microsoft Word.\r
+* [#13143](http://dev.ckeditor.com/ticket/13143): [Edge] Fixed: Focus lost when opening the panel.\r
+* [#13387](http://dev.ckeditor.com/ticket/13387): [Edge] Fixed: "Permission denied" error thrown when loading the editor with developer tools open.\r
+* [#13574](http://dev.ckeditor.com/ticket/13574): [Edge] Fixed: "Permission denied" error thrown when opening editor dialog windows.\r
+* [#13441](http://dev.ckeditor.com/ticket/13441): [Edge] Fixed: The [Clipboard](http://ckeditor.com/addon/clipboard) plugin breaks the state of [Undo](http://ckeditor.com/addon/undo) commands after a paste.\r
+* [#13554](http://dev.ckeditor.com/ticket/13554): [Edge] Fixed: Paste dialog's iframe does not receive focus on show.\r
+* [#13440](http://dev.ckeditor.com/ticket/13440): [Edge] Fixed: Unable to paste a widget.\r
+\r
+Other Changes:\r
+\r
+* [#13421](http://dev.ckeditor.com/ticket/13421): UX improvements to notifications in the [Auto Embed](http://ckeditor.com/addon/autoembed) plugin.\r
+\r
+## CKEditor 4.5.1\r
+\r
+Fixed Issues:\r
+\r
+* [#13486](http://dev.ckeditor.com/ticket/13486): Fixed: The [Upload Image](http://ckeditor.com/addon/uploadimage) plugin should log an error, not throw an error when upload URL is not set.\r
+\r
+## CKEditor 4.5\r
+\r
+New Features:\r
+\r
+* [#13304](http://dev.ckeditor.com/ticket/13304): Added support for passing DOM elements to [`config.sharedSpaces`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-sharedSpaces). Thanks to [Undergrounder](https://github.com/Undergrounder)!\r
+* [#13215](http://dev.ckeditor.com/ticket/13215): Added ability to cancel fetching a resource by the Embed plugins.\r
+* [#13213](http://dev.ckeditor.com/ticket/13213): Added the [`dialog#setState()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dialog-method-setState) method and used it in the [Embed](http://ckeditor.com/addon/embed) dialog to indicate that a resource is being loaded.\r
+* [#13337](http://dev.ckeditor.com/ticket/13337): Added the [`repository.onWidget()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.repository-method-onWidget) method &mdash; a convenient way to listen to [widget](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget) events through the [repository](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.repository).\r
+* [#13214](http://dev.ckeditor.com/ticket/13214): Added support for pasting links that convert into embeddable resources on the fly.\r
+\r
+Fixed Issues:\r
+\r
+* [#13334](http://dev.ckeditor.com/ticket/13334): Fixed: Error after nesting widgets and playing with undo/redo.\r
+* [#13118](http://dev.ckeditor.com/ticket/13118): Fixed: The [`editor.getSelectedHtml()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-getSelectedHtml) method throws an error when called in the source mode.\r
+* [#13158](http://dev.ckeditor.com/ticket/13158): Fixed: Error after canceling a dialog when creating a widget.\r
+* [#13197](http://dev.ckeditor.com/ticket/13197): Fixed: Linked inline [Enhanced Image](http://ckeditor.com/addon/image2) alignment class is not transferred to the widget wrapper.\r
+* [#13199](http://dev.ckeditor.com/ticket/13199): Fixed: [Semantic Embed](http://ckeditor.com/addon/embedsemantic) does not support widget classes.\r
+* [#13003](http://dev.ckeditor.com/ticket/13003): Fixed: Anchors are uploaded when moving them by drag and drop.\r
+* [#13032](http://dev.ckeditor.com/ticket/13032): Fixed: When upload is done, notification update should be marked as important.\r
+* [#13300](http://dev.ckeditor.com/ticket/13300): Fixed: The `internalCommit` argument in the [Image](http://ckeditor.com/addon/image) dialog seems to be never used.\r
+* [#13036](http://dev.ckeditor.com/ticket/13036): Fixed: Notifications are moved 10px to the right.\r
+* [#13280](http://dev.ckeditor.com/ticket/13280): [IE8] Fixed: Undo after inline widget drag&drop throws an error.\r
+* [#13186](http://dev.ckeditor.com/ticket/13186): Fixed: Content dropped into a nested editable is not filtered by [Advanced Content Filter](http://docs.ckeditor.com/#!/guide/dev_acf).\r
+* [#13140](http://dev.ckeditor.com/ticket/13140): Fixed: Error thrown when dropping a block widget right after itself.\r
+* [#13176](http://dev.ckeditor.com/ticket/13176): [IE8] Fixed: Errors on drag&drop of embed widgets.\r
+* [#13015](http://dev.ckeditor.com/ticket/13015): Fixed: Dropping an image file on [Enhanced Image](http://ckeditor.com/addon/image2) causes a page reload.\r
+* [#13080](http://dev.ckeditor.com/ticket/13080): Fixed: Ugly notification shown when the response contains HTML content.\r
+* [#13011](http://dev.ckeditor.com/ticket/13011): [IE8] Fixed: Anchors are duplicated on drag&drop in specific locations.\r
+* [#13105](http://dev.ckeditor.com/ticket/13105): Fixed: Various issues related to [`CKEDITOR.tools.htmlEncode()`](http://docs.ckeditor.com/#!/api/CKEDITOR.tools-method-htmlEncode) and [`CKEDITOR.tools.htmlDecode()`](http://docs.ckeditor.com/#!/api/CKEDITOR.tools-method-htmlDecode) methods.\r
+* [#11976](http://dev.ckeditor.com/ticket/11976): [Chrome] Fixed: Copy&paste and drag&drop lists from Microsoft Word.\r
+* [#13128](http://dev.ckeditor.com/ticket/13128): Fixed: Various issues with cloning element IDs:\r
+  * Fixed the default behavior of [`range.cloneContents()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.range-method-cloneContents) and [`range.extractContents()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.range-method-extractContents) methods which now clone IDs similarly to their native counterparts.\r
+  * Added `cloneId` arguments to the above methods, [`range.splitBlock()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.range-method-splitBlock) and [`element.breakParent()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.element-method-breakParent). Mind the default values and special behavior in the `extractContents()` method!\r
+  * Fixed issues where IDs were lost on copy&paste and drag&drop.\r
+* Toolbar configurators:\r
+  * [#13185](http://dev.ckeditor.com/ticket/13185): Fixed: Wrong position of the suggestion box if there is not enough space below the caret.\r
+  * [#13138](http://dev.ckeditor.com/ticket/13138): Fixed: The "Toggle empty elements" button label is unclear.\r
+  * [#13136](http://dev.ckeditor.com/ticket/13136): Fixed: Autocompleter is far too intrusive.\r
+  * [#13133](http://dev.ckeditor.com/ticket/13133): Fixed: Tab leaves the editor.\r
+  * [#13173](http://dev.ckeditor.com/ticket/13173): Fixed: [`config.removeButtons`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-removeButtons) is ignored by the advanced toolbar configurator.\r
+\r
+Other Changes:\r
+\r
+* [#13119](http://dev.ckeditor.com/ticket/13119): Improved compatibility of editor skins ([Moono](http://ckeditor.com/addon/moono) and [Kama](http://ckeditor.com/addon/kama)) with external web page style sheets.\r
+* Toolbar configurators:\r
+  * [#13147](http://dev.ckeditor.com/ticket/13147): Added buttons to the sticky toolbar.\r
+  * [#13207](http://dev.ckeditor.com/ticket/13207): Used modal window to display toolbar configurator help.\r
+* [#13316](http://dev.ckeditor.com/ticket/13316): Made [`CKEDITOR.env.isCompatible`](http://docs.ckeditor.com/#!/api/CKEDITOR.env-property-isCompatible) a blacklist rather than a whitelist. More about the change in the [Browser Compatibility](http://docs.ckeditor.com/#!/guide/dev_browsers) guide.\r
+* [#13398](http://dev.ckeditor.com/ticket/13398): Renamed `CKEDITOR.fileTools.UploadsRepository` to [`CKEDITOR.fileTools.UploadRepository`](http://docs.ckeditor.com/#!/api/CKEDITOR.fileTools.uploadRepository) and changed all related properties.\r
+* [#13279](http://dev.ckeditor.com/ticket/13279): Reviewed CSS vendor prefixes.\r
+* [#13454](http://dev.ckeditor.com/ticket/13454): Removed unused `lang.image.alertUrl` token from the [Image](http://ckeditor.com/addon/image) plugin.\r
+\r
+## CKEditor 4.5 Beta\r
+\r
+New Features:\r
+\r
+* Clipboard (copy&paste, drag&drop) and file uploading features and improvements ([#11437](http://dev.ckeditor.com/ticket/11437)).\r
+\r
+  * Major features:\r
+    * Support for dropping and pasting files into the editor was introduced. Through a set of new facades for native APIs it is now possible to easily intercept and process inserted files.\r
+    * [File upload tools](http://docs.ckeditor.com/#!/api/CKEDITOR.fileTools) were introduced in order to simplify controlling the loading, uploading and handling server response, properly handle [new upload configuration](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-uploadUrl) options, etc.\r
+    * [Upload Image](http://ckeditor.com/addon/uploadimage) widget was introduced to upload dropped images. A base class for the [upload widget](http://docs.ckeditor.com/#!/api/CKEDITOR.fileTools.uploadWidgetDefinition) was exposed, too, to make it simple to create new types of upload widgets which can handle any type of dropped file, show the upload progress and update the content when the process is done. It also handles editing and undo/redo operations when a file is being uploaded and integrates with the [notification aggregator](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.notificationAggregator) to show progress and success or error.\r
+    * All drag and drop operations were integrated with the editor. All dropped content is passed through the [`editor#paste`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-paste) event and a set of new editor events was introduced &mdash; [`dragstart`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-dragstart), [`drop`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-drop), [`dragend`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-dragend).\r
+    * The [Data Transfer](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.clipboard.dataTransfer) facade was introduced to unify access to data in various types and files. [Data Transfer](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.clipboard.dataTransfer) is now always available in the [`editor#paste`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-paste) event.\r
+    * Switched from the pastebin to using the native clipboard access whenever possible. This solved many issues related to pastebin such as unnecessary scrolling or data loss. Additionally, on copy and cut from the editor the clipboard data is set. Therefore, on paste the editor has access to clean data, undisturbed by the browsers.\r
+    * Drag and drop of inline and block widgets was integrated with the standard clipboard APIs. By listening to drag events you will thus be notified about widgets, too. This opens a possibility to filter pasted and dropped widgets.\r
+    * The [`editor#paste`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-paste) event can have the `range` parameter so it is possible to change the paste position in the listener or paste in the not selectable position. Also the [`editor.insertHtml()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-insertHtml) method now accepts `range` as an additional parameter.\r
+    * [#11621](http://dev.ckeditor.com/ticket/11621): A configurable [paste filter](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-pasteFilter) was introduced. The filter is by default turned to `'semantic-content'` on Webkit and Blink for all pasted content coming from external sources because of the low quality of HTML that these engines put into the clipboard. Internal and cross-editor paste is safe due to the change explained in the previous point.\r
+\r
+  * Other changes and related fixes:\r
+    * [#12095](http://dev.ckeditor.com/ticket/12095): On drag and copy of widgets [the same method](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-getSelectedHtml) is used to get selected HTML as in the normal case. Thanks to that styles applied to inline widgets are not lost.\r
+    * [#11219](http://dev.ckeditor.com/ticket/11219): Fixed: Dragging a [captioned image](http://ckeditor.com/addon/image2) does not fire the [`editor#paste`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-paste) event.\r
+    * [#9554](http://dev.ckeditor.com/ticket/9554): [Webkit Mac] Fixed: Editor scrolls on paste.\r
+    * [#9898](http://dev.ckeditor.com/ticket/9898): [Webkit&Divarea] Fixed: Pasting causes undesirable scrolling.\r
+    * [#11993](http://dev.ckeditor.com/ticket/11993): [Chrome] Fixed: Pasting content scrolls the document.\r
+    * [#12613](http://dev.ckeditor.com/ticket/12613): Show the user that they can not drop on editor UI (toolbar, bottom bar).\r
+    * [#12851](http://dev.ckeditor.com/ticket/12851): [Blink/Webkit] Fixed: Formatting disappears when pasting content into cells.\r
+    * [#12914](http://dev.ckeditor.com/ticket/12914): Fixed: Copy/Paste of table broken in `div`-based editor.\r
+\r
+  * Browser support.<br>Browser support for related features varies significantly (see http://caniuse.com/clipboard).\r
+    * File APIs needed to operate and file upload is not supported in Internet Explorer 9 and below.\r
+    * Only Chrome and Safari on Mac OS support setting custom data items in the clipboard, so currently it is possible to recognize the origin of the copied content in these browsers only. All drag and drop operations can be identified thanks to the new Data Transfer facade.\r
+    * No Internet Explorer browser supports the standard clipboard API which results in small glitches like where only plain text can be dropped from outside the editor. Thanks to the new Data Transfer facade, internal and cross-editor drag and drop supports the full range of data.\r
+    * Direct access to clipboard could only be implemented in Chrome, Safari on Mac OS, Opera and Firefox. In other browsers the pastebin must still be used.\r
+\r
+* [#12875](http://dev.ckeditor.com/ticket/12875): Samples and toolbar configuration tools.\r
+  * The old set of samples shipped with every CKEditor package was replaced with a shiny new single-page sample. This change concluded a long term plan which started from introducing the [CKEditor SDK](http://sdk.ckeditor.com/) and [CKEditor Functionality Overview](http://docs.ckeditor.com/#!/guide/dev_features) section in the documentation which essentially redefined the old samples.\r
+  * Toolbar configurators with live previews were introduced. They will be shipped with every CKEditor package and are meant to help in configuring toolbar layouts.\r
+\r
+* [#10925](http://dev.ckeditor.com/ticket/10925): The [Media Embed](http://ckeditor.com/addon/embed) and [Semantic Media Embed](http://ckeditor.com/addon/embedsemantic) plugins were introduced. Read more about the new features in the [Embedding Content](http://docs.ckeditor.com/#!/guide/dev_media_embed) article.\r
+* [#10931](http://dev.ckeditor.com/ticket/10931): Added support for nesting widgets. It is now possible to insert one widget into another widget's nested editable. Note that unless nested editable's [allowed content](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.nestedEditable.definition-property-allowedContent) is defined precisely, starting from CKEditor 4.5 some widget buttons may become enabled. This feature is not supported in IE8. Included issues:\r
+  * [#12018](http://dev.ckeditor.com/ticket/12018): Fixed and reviewed: Nested widgets garbage collection.\r
+  * [#12024](http://dev.ckeditor.com/ticket/12024): [Firefox] Fixed: Outline is extended to the left by unpositioned drag handlers.\r
+  * [#12006](http://dev.ckeditor.com/ticket/12006): Fixed: Drag and drop of nested block widgets.\r
+  * [#12008](http://dev.ckeditor.com/ticket/12008): Fixed various cases of inserting a single non-editable element using the [`editor.insertHtml()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-insertHtml) method. Fixes pasting a widget with a nested editable inside another widget's nested editable.\r
+\r
+* Notification system:\r
+  * [#11580](http://dev.ckeditor.com/ticket/11580): Introduced the [notification system](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.notification).\r
+  * [#12810](http://dev.ckeditor.com/ticket/12810): Introduced a [notification aggregator](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.notificationAggregator) for the [notification system](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.notification) which simplifies displaying progress of many concurrent tasks.\r
+* [#11636](http://dev.ckeditor.com/ticket/11636): Introduced new, UX-focused, methods for getting selected HTML and deleting it &mdash; [`editor.getSelectedHtml()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-getSelectedHtml) and [`editor.extractSelectedHtml()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-extractSelectedHtml).\r
+* [#12416](http://dev.ckeditor.com/ticket/12416): Added the [`widget.definition.upcastPriority`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.definition-property-upcastPriority) property which gives more control over widget upcasting order to the widget author.\r
+* [#12036](http://dev.ckeditor.com/ticket/12036): Initialize the editor in [read-only](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-readOnly) mode when the `<textarea>` element has a `readonly` attribute.\r
+* [#11905](http://dev.ckeditor.com/ticket/11905): The [`resize` event](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-resize) passes the current dimensions in its data.\r
+* [#12126](http://dev.ckeditor.com/ticket/12126): Introduced [`config.image_prefillDimensions`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-image_prefillDimensions) and [`config.image2_prefillDimensions`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-image2_prefillDimensions) to make pre-filling `width` and `height` configurable for the [Enhanced Image](http://ckeditor.com/addon/image2).\r
+* [#12746](http://dev.ckeditor.com/ticket/12746): Added a new configuration option to hide the [Enhanced Image](http://ckeditor.com/addon/image2) resizer.\r
+* [#12150](http://dev.ckeditor.com/ticket/12150): Exposed the [`getNestedEditable()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget-static-method-getNestedEditable) and `is*` [widget helper](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget) functions (see the static methods).\r
+* [#12448](http://dev.ckeditor.com/ticket/12448): Introduced the [`editable.insertHtmlIntoRange`](http://docs.ckeditor.com/#!/api/CKEDITOR.editable-method-insertHtmlIntoRange) method.\r
+* [#12143](http://dev.ckeditor.com/ticket/12143): Added the [`config.floatSpacePreferRight`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-floatSpacePreferRight) configuration option that switches the alignment of the floating toolbar. Thanks to [InvisibleBacon](http://github.com/InvisibleBacon)!\r
+* [#10986](http://dev.ckeditor.com/ticket/10986): Added support for changing dialog input and textarea text directions by using the *Shift+Alt+Home/End* keystrokes. The direction is stored in the value of the input by prepending the [`\u202A`](http://unicode.org/cldr/utility/character.jsp?a=202A) or [`\u202B`](http://unicode.org/cldr/utility/character.jsp?a=202B) marker to it. Read more in the [documentation](http://docs.ckeditor.com/#!/api/CKEDITOR.dialog.definition.textInput-property-bidi). Thanks to [edithkk](https://github.com/edithkk)!\r
+* [#12770](http://dev.ckeditor.com/ticket/12770): Added support for passing [widget](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget)'s startup data as a widget command's argument. Thanks to [Rebrov Boris](https://github.com/zipp3r) and [Tieme van Veen](https://github.com/tiemevanveen)!\r
+* [#11583](http://dev.ckeditor.com/ticket/11583): Added support for the HTML5 `required` attribute in various form elements. Thanks to [Steven Busse](https://github.com/sbusse)!\r
+\r
+Changes:\r
+\r
+* [#12858](http://dev.ckeditor.com/ticket/12858): Basic [Spartan](http://blogs.windows.com/bloggingwindows/2015/03/30/introducing-project-spartan-the-new-browser-built-for-windows-10/) browser compatibility. Full compatibility will be introduced later, because at the moment Spartan is still too unstable to be used for tests and we see many changes from version to version.\r
+* [#12948](http://dev.ckeditor.com/ticket/12948): The [`config.mathJaxLibrary`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-mathJaxLib) option does not default to the MathJax CDN any more. It needs to be configured to enable the [Mathematical Formulas](http://ckeditor.com/addon/mathjax) plugin now.\r
+* [#13069](http://dev.ckeditor.com/ticket/13069): Fixed inconsistencies between [`editable.insertHtml()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editable-method-insertElement) and [`editable.insertElement()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editable-method-insertElement) when the `range` parameter is used. Now, the `editor.insertElement()` method works on a higher level, which means that it saves undo snapshots and sets the selection after insertion. Use the [`editable.insertElementIntoRange()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editable-method-insertElementIntoRange) method directly for the pre 4.5 behavior of `editable.insertElement()`.\r
+* [#12870](http://dev.ckeditor.com/ticket/12870): Use [`editor.showNotification()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-showNotification) instead of `alert()` directly whenever possible. When the [Notification plugin](http://ckeditor.com/addon/notification) is loaded, the notification system is used automatically. Otherwise, the native `alert()` is displayed.\r
+* [#8024](http://dev.ckeditor.com/ticket/8024): Swapped behavior of the Split Cell Vertically and Horizontally features of the [Table Tools](http://ckeditor.com/addon/tabletools) plugin to be more intuitive. Thanks to [kevinisagit](https://github.com/kevinisagit)!\r
+* [#10903](http://dev.ckeditor.com/ticket/10903): Performance improvements for the [`dom.element.addClass()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.element-method-addClass), [`dom.element.removeClass()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.element-method-removeClass) and [`dom.element.hasClass()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.element-method-hasClass) methods. Note: The previous implementation allowed passing multiple classes to `addClass()` although it was only a side effect of that implementation. The new implementation does not allow this.\r
+* [#11856](http://dev.ckeditor.com/ticket/11856): The jQuery adapter throws a meaningful error if CKEditor or jQuery are not loaded.\r
+\r
+Fixed issues:\r
+\r
+* [#11586](http://dev.ckeditor.com/ticket/11586): Fixed: [`range.cloneContents()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.range-method-cloneContents) should not change the DOM in order not to affect selection.\r
+* [#12148](http://dev.ckeditor.com/ticket/12148): Fixed: [`dom.element.getChild()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.element-method-getChild) should not modify a passed array.\r
+* [#12503](http://dev.ckeditor.com/ticket/12503): [Blink/Webkit] Fixed: Incorrect result of Select All and *Backspace* or *Delete*.\r
+* [#13001](http://dev.ckeditor.com/ticket/13001): [Firefox] Fixed: The `<br />` filler is placed in the wrong position by the [`range.fixBlock()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.range-method-fixBlock) method due to quirky Firefox behavior.\r
+* [#13101](http://dev.ckeditor.com/ticket/13101): [IE8] Fixed: Colons are prepended to HTML5 element names when cloning them.\r
+\r
+## CKEditor 4.4.8\r
+\r
+**Security Updates:**\r
+\r
+* Fixed XSS vulnerability in the HTML parser reported by [Dheeraj Joshi](https://twitter.com/dheerajhere) and [Prem Kumar](https://twitter.com/iAmPr3m).\r
+\r
+       Issue summary: It was possible to execute XSS inside CKEditor after persuading the victim to: (i) switch CKEditor to source mode, then (ii) paste a specially crafted HTML code, prepared by the attacker, into the opened CKEditor source area, and (iii) switch back to WYSIWYG mode.\r
+\r
+**An upgrade is highly recommended!**\r
+\r
+Fixed Issues:\r
+\r
+* [#12899](http://dev.ckeditor.com/ticket/12899): Fixed: Corrected wrong tag ending for horizontal box definition in the [Dialog User Interface](http://ckeditor.com/addon/dialogui) plugin. Thanks to [mizafish](https://github.com/mizafish)!\r
+* [#13254](http://dev.ckeditor.com/ticket/13254): Fixed: Cannot outdent block after indent when using the [Div Editing Area](http://ckeditor.com/addon/divarea) plugin. Thanks to [Jonathan Cottrill](https://github.com/jcttrll)!\r
+* [#13268](http://dev.ckeditor.com/ticket/13268): Fixed: Documentation for [`CKEDITOR.dom.text`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.text) is incorrect. Thanks to [Ben Kiefer](https://github.com/benkiefer)!\r
+* [#12739](http://dev.ckeditor.com/ticket/12739): Fixed: Link loses inline styles when edited without the [Advanced Tab for Dialogs](http://ckeditor.com/addon/dialogadvtab) plugin. Thanks to [Віталій Крутько](https://github.com/asmforce)!\r
+* [#13292](http://dev.ckeditor.com/ticket/13292): Fixed: Protection pattern does not work in attribute in self-closing elements with no space before `/>`. Thanks to [Віталій Крутько](https://github.com/asmforce)!\r
+* [PR#192](https://github.com/ckeditor/ckeditor-dev/pull/192): Fixed: Variable name typo in the [Dialog User Interface](http://ckeditor.com/addon/dialogui) plugin which caused [`CKEDITOR.ui.dialog.radio`](http://docs.ckeditor.com/#!/api/CKEDITOR.ui.dialog.radio) validation to not work. Thanks to [Florian Ludwig](https://github.com/FlorianLudwig)!\r
+* [#13232](http://dev.ckeditor.com/ticket/13232): [Safari] Fixed: The [`element.appendText()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.element-method-appendText) method does not work properly for empty elements.\r
+* [#13233](http://dev.ckeditor.com/ticket/13233): Fixed: [HTMLDataProcessor](http://docs.ckeditor.com/#!/api/CKEDITOR.htmlDataProcessor) can process `foo:href` attributes.\r
+* [#12796](http://dev.ckeditor.com/ticket/12796): Fixed: The [Indent List](http://ckeditor.com/addon/indentlist) plugin unwraps parent `<li>` elements. Thanks to [Andrew Stucki](https://github.com/andrewstucki)!\r
+* [#12885](http://dev.ckeditor.com/ticket/12885): Added missing [`editor.getData()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-getData) parameter documentation.\r
+* [#11982](http://dev.ckeditor.com/ticket/11982): Fixed: Bullet added in a wrong position after the *Enter* key is pressed in a nested list.\r
+* [#13027](http://dev.ckeditor.com/ticket/13027): Fixed: Keyboard navigation in dialog windows with multiple tabs not following IBM CI 162 instructions or [ARIA Authoring Practices](http://www.w3.org/TR/2013/WD-wai-aria-practices-20130307/#tabpanel).\r
+* [#12256](http://dev.ckeditor.com/ticket/12256): Fixed: Basic styles classes are lost when pasting from Microsoft Word if [basic styles](http://ckeditor.com/addon/basicstyles) were configured to use classes.\r
+* [#12729](http://dev.ckeditor.com/ticket/12729): Fixed: Incorrect structure created when merging a block into a list item on *Backspace* and *Delete*.\r
+* [#13031](http://dev.ckeditor.com/ticket/13031): [Firefox] Fixed: No more line breaks in source view since Firefox 36.\r
+* [#13131](http://dev.ckeditor.com/ticket/13131): Fixed: The [Code Snippet](http://ckeditor.com/addon/codesnippet) plugin cannot be used without the [IFrame Editing Area](http://ckeditor.com/addon/wysiwygarea) plugin.\r
+* [#9086](http://dev.ckeditor.com/ticket/9086): Fixed: Invalid ARIA property used on paste area `<iframe>`.\r
+* [#13164](http://dev.ckeditor.com/ticket/13164): Fixed: Error when inserting a hidden field.\r
+* [#13155](http://dev.ckeditor.com/ticket/13155): Fixed: Incorrect [Line Utilities](http://ckeditor.com/addon/lineutils) positioning when `<body>` has a margin.\r
+* [#13351](http://dev.ckeditor.com/ticket/13351): Fixed: Link lost when editing a linked image with the Link tab disabled. This also fixed a bug when inserting an image into a fully selected link would throw an error ([#12847](https://dev.ckeditor.com/ticket/12847)).\r
+* [#13344](http://dev.ckeditor.com/ticket/13344): [WebKit/Blink] Fixed: It is possible to remove or change editor content in [read-only mode](http://docs.ckeditor.com/#!/guide/dev_readonly).\r
+\r
+Other Changes:\r
+\r
+* [#12844](http://dev.ckeditor.com/ticket/12844) and [#13103](http://dev.ckeditor.com/ticket/13103): Upgraded the [testing environment](http://docs.ckeditor.com/#!/guide/dev_tests) to [Bender.js](https://github.com/benderjs/benderjs) `0.2.3`.\r
+* [#12930](http://dev.ckeditor.com/ticket/12930): Because of licensing issues, `truncated-mathjax/` is now removed from the `tests/` directory. Now `bender.config.mathJaxLibPath` must be configured manually in order to run [Mathematical Formulas](http://ckeditor.com/addon/mathjax) plugin tests.\r
+* [#13266](http://dev.ckeditor.com/ticket/13266): Added more shades of gray in the [Color Dialog](http://ckeditor.com/addon/colordialog) window. Thanks to [mizafish](https://github.com/mizafish)!\r
+\r
+\r
+## CKEditor 4.4.7\r
+\r
+Fixed Issues:\r
+\r
+* [#12825](http://dev.ckeditor.com/ticket/12825): Fixed: Preventing the [Table Resize](http://ckeditor.com/addon/tableresize) plugin from operating on elements outside the editor. Thanks to [Paul Martin](https://github.com/Paul-Martin)!\r
+* [#12157](http://dev.ckeditor.com/ticket/12157): Fixed: Lost text formatting on pressing *Tab* when the [`config.tabSpaces`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-tabSpaces) configuration option value was greater than zero.\r
+* [#12777](http://dev.ckeditor.com/ticket/12777): Fixed: The `table-layout` CSS property should be reset by skins. Thanks to [vita10gy](https://github.com/vita10gy)!\r
+* [#12812](http://dev.ckeditor.com/ticket/12812): Fixed: An uncaught security exception is thrown when [Line Utilities](http://ckeditor.com/addon/lineutils) are used in an inline editor loaded in a cross-domain `iframe`. Thanks to [Vitaliy Zurian](https://github.com/thecatontheflat)!\r
+* [#12735](http://dev.ckeditor.com/ticket/12735): Fixed: [`config.fillEmptyBlocks`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-fillEmptyBlocks) should only apply when outputting data.\r
+* [#10032](http://dev.ckeditor.com/ticket/10032): Fixed: [Paste from Word](http://ckeditor.com/addon/pastefromword) filter is executed for every paste after using the button.\r
+* [#12597](http://dev.ckeditor.com/ticket/12597): [Blink/WebKit] Fixed: Multi-byte Japanese characters entry not working properly after *Shift+Enter*.\r
+* [#12387](http://dev.ckeditor.com/ticket/12387): Fixed: An error is thrown if a skin does not have the [`chameleon`](http://docs.ckeditor.com/#!/api/CKEDITOR.skin-method-chameleon) property defined and [`config.uiColor`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-uiColor) is defined.\r
+* [#12747](http://dev.ckeditor.com/ticket/12747): [IE8-10] Fixed: Opening a drop-down for a specific selection when the editor is maximized results in incorrect drop-down panel position.\r
+* [#12850](http://dev.ckeditor.com/ticket/12850): [IEQM] Fixed: An error is thrown after focusing the editor.\r
+\r
+## CKEditor 4.4.6\r
+\r
+**Security Updates:**\r
+\r
+* Fixed XSS vulnerability in the HTML parser reported by [Maco Cortes](https://www.facebook.com/Maaacoooo).\r
+\r
+       Issue summary: It was possible to execute XSS inside CKEditor after persuading the victim to: (i) switch CKEditor to source mode, then (ii) paste a specially crafted HTML code, prepared by the attacker, into the opened CKEditor source area, and (iii) switch back to WYSIWYG mode.\r
+\r
+**An upgrade is highly recommended!**\r
+\r
+New Features:\r
+\r
+* [#12501](http://dev.ckeditor.com/ticket/12501): Allowed dashes in element names in the [string format of allowed content rules](http://docs.ckeditor.com/#!/guide/dev_allowed_content_rules-section-string-format).\r
+* [#12550](http://dev.ckeditor.com/ticket/12550): Added the `<main>` element to the [`CKEDITOR.dtd`](http://docs.ckeditor.com/#!/api/CKEDITOR.dtd).\r
+\r
+Fixed Issues:\r
+\r
+* [#12506](http://dev.ckeditor.com/ticket/12506): [Safari] Fixed: Cannot paste into inline editor if the page has `user-select: none` style. Thanks to [shaohua](https://github.com/shaohua)!\r
+* [#12683](http://dev.ckeditor.com/ticket/12683): Fixed: [Filter](http://docs.ckeditor.com/#!/guide/dev_acf) fails to remove custom tags. Thanks to [timselier](https://github.com/timselier)!\r
+* [#12489](http://dev.ckeditor.com/ticket/12489) and [#12491](http://dev.ckeditor.com/ticket/12491): Fixed: Various issues related to restoring the selection after performing operations on filler character. See the [fixed cases](http://dev.ckeditor.com/ticket/12491#comment:4).\r
+* [#12621](http://dev.ckeditor.com/ticket/12621): Fixed: Cannot remove inline styles (bold, italic, etc.) in empty lines.\r
+* [#12630](http://dev.ckeditor.com/ticket/12630): [Chrome] Fixed: Selection is placed outside the paragraph when the [New Page](http://ckeditor.com/addon/newpage) button is clicked. This patch significantly simplified the way how the initial selection (a selection after the content of the editable is overwritten) is being fixed. That might have fixed many related scenarios in all browsers.\r
+* [#11647](http://dev.ckeditor.com/ticket/11647): Fixed: The [`editor.blur`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-blur) event is not fired on first blur after initializing the inline editor on an already focused element.\r
+* [#12601](http://dev.ckeditor.com/ticket/12601): Fixed: [Strikethrough](http://ckeditor.com/addon/basicstyles) button tooltip spelling.\r
+* [#12546](http://dev.ckeditor.com/ticket/12546): Fixed: The Preview tab in the [Document Properties](http://ckeditor.com/addon/docprops) dialog window is always disabled.\r
+* [#12300](http://dev.ckeditor.com/ticket/12300): Fixed: The [`editor.change`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-change) event fired on first navigation key press after typing.\r
+* [#12141](http://dev.ckeditor.com/ticket/12141): Fixed: List items are lost when indenting a list item with content wrapped with a block element.\r
+* [#12515](http://dev.ckeditor.com/ticket/12515): Fixed: Cursor is in the wrong position when undoing after adding an image and typing some text.\r
+* [#12484](http://dev.ckeditor.com/ticket/12484): [Blink/WebKit] Fixed: DOM is changed outside the editor area in a certain case.\r
+* [#12688](http://dev.ckeditor.com/ticket/12688): Improved the tests of the [styles system](http://docs.ckeditor.com/#!/api/CKEDITOR.style) and fixed two minor issues.\r
+* [#12403](http://dev.ckeditor.com/ticket/12403): Fixed: Changing the [font](http://ckeditor.com/addon/font) style should not lead to nesting it in the previous style element.\r
+* [#12609](http://dev.ckeditor.com/ticket/12609): Fixed: Incorrect `config.magicline_putEverywhere` name used for a [Magic Line](http://ckeditor.com/addon/magicline) all-encompassing [`config.magicline_everywhere`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-magicline_everywhere) configuration option.\r
+\r
+\r
+## CKEditor 4.4.5\r
+\r
+New Features:\r
+\r
+* [#12279](http://dev.ckeditor.com/ticket/12279): Added a possibility to pass a custom evaluator to [`node.getAscendant()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.node-method-getAscendant).\r
+\r
+Fixed Issues:\r
+\r
+* [#12423](http://dev.ckeditor.com/ticket/12423): [Safari7.1+] Fixed: *Enter* key moved cursor to a strange position.\r
+* [#12381](http://dev.ckeditor.com/ticket/12381): [iOS] Fixed: Selection issue. Thanks to [Remiremi](https://github.com/Remiremi)!\r
+* [#10804](http://dev.ckeditor.com/ticket/10804): Fixed: `CKEDITOR_GETURL` is not used with some plugins where it should be used. Thanks to [Thomas Andraschko](https://github.com/tandraschko)!\r
+* [#9137](http://dev.ckeditor.com/ticket/9137): Fixed: The `<base>` tag is not created when `<head>` has an attribute. Thanks to [naoki.fujikawa](https://github.com/naoki-fujikawa)!\r
+* [#12377](http://dev.ckeditor.com/ticket/12377): Fixed: Errors thrown in the [Image](http://ckeditor.com/addon/image) plugin when removing preview from the dialog window definition. Thanks to [Axinet](https://github.com/Axinet)!\r
+* [#12162](http://dev.ckeditor.com/ticket/12162): Fixed: Auto paragraphing and *Enter* key in nested editables.\r
+* [#12315](http://dev.ckeditor.com/ticket/12315): Fixed: Marked [`config.autoParagraph`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-autoParagraph) as deprecated.\r
+* [#12113](http://dev.ckeditor.com/ticket/12113): Fixed: A [code snippet](http://ckeditor.com/addon/codesnippet) should be presented in the [elements path](http://ckeditor.com/addon/elementspath) as "code snippet" (translatable).\r
+* [#12311](http://dev.ckeditor.com/ticket/12311): Fixed: [Remove Format](http://ckeditor.com/addon/removeformat) should also remove `<cite>` elements.\r
+* [#12261](http://dev.ckeditor.com/ticket/12261): Fixed: Filter has to be destroyed and removed from [`CKEDITOR.filter.instances`](http://docs.ckeditor.com/#!/api/CKEDITOR.filter-static-property-instances) on editor destroy.\r
+* [#12398](http://dev.ckeditor.com/ticket/12398): Fixed: [Maximize](http://ckeditor.com/addon/maximize) does not work on an instance without a [title](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-title).\r
+* [#12097](http://dev.ckeditor.com/ticket/12097): Fixed: JAWS not reading the number of options correctly in the [Text Color and Background Color](http://ckeditor.com/addon/colorbutton) button menu.\r
+* [#12411](http://dev.ckeditor.com/ticket/12411): Fixed: [Page Break](http://ckeditor.com/addon/pagebreak) used directly in the editable breaks the editor.\r
+* [#12354](http://dev.ckeditor.com/ticket/12354): Fixed: Various issues in undo manager when holding keys.\r
+* [#12324](http://dev.ckeditor.com/ticket/12324): [IE8] Fixed: Undo steps are not recorded when changing the caret position by clicking below the body.\r
+* [#12332](http://dev.ckeditor.com/ticket/12332): Fixed: Lowered DOM events listeners' priorities in undo manager in order to avoid ambiguity.\r
+* [#12402](http://dev.ckeditor.com/ticket/12402): [Blink] Fixed: Workaround for Blink bug with `document.title` which breaks updating title in the full HTML mode.\r
+* [#12338](http://dev.ckeditor.com/ticket/12338): Fixed: The CKEditor package contains unoptimized images.\r
+\r
+\r
+## CKEditor 4.4.4\r
+\r
+Fixed Issues:\r
+\r
+* [#12268](http://dev.ckeditor.com/ticket/12268): Cleanup of [UI Color](http://ckeditor.com/addon/uicolor) YUI styles. Thanks to [CasherWest](https://github.com/CasherWest)!\r
+* [#12263](http://dev.ckeditor.com/ticket/12263): Fixed: [Paste from Word](http://ckeditor.com/addon/pastefromword) filter does not properly normalize semicolons style text. Thanks to [Alin Purcaru](https://github.com/mesmerizero)!\r
+* [#12243](http://dev.ckeditor.com/ticket/12243): Fixed: Text formatting lost when pasting from Word. Thanks to [Alin Purcaru](https://github.com/mesmerizero)!\r
+* [#111739](http://dev.ckeditor.com/ticket/11739): Fixed: `keypress` listeners should not be used in the undo manager. A complete rewrite of keyboard handling in the undo manager was made. Numerous smaller issues were fixed, among others:\r
+  * [#10926](http://dev.ckeditor.com/ticket/10926): [Chrome@Android] Fixed: Typing does not record snapshots and does not fire the [`editor.change`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-change) event.\r
+  * [#11611](http://dev.ckeditor.com/ticket/11611): [Firefox] Fixed: The [`editor.change`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-change) event is fired when pressing Arrow keys.\r
+  * [#12219](http://dev.ckeditor.com/ticket/12219): [Safari] Fixed: Some modifications of the [`UndoManager.locked`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.undo.UndoManager-property-locked) property violate strict mode in the [Undo](http://ckeditor.com/addon/undo) plugin.\r
+* [#10916](http://dev.ckeditor.com/ticket/10916): Fixed: [Magic Line](http://ckeditor.com/addon/magicline) icon in Right-To-Left environments.\r
+* [#11970](http://dev.ckeditor.com/ticket/11970): [IE] Fixed: CKEditor `paste` event is not fired when pasting with *Shift+Ins*.\r
+* [#12111](http://dev.ckeditor.com/ticket/12111): Fixed: Linked image attributes are not read when opening the image dialog window by doubleclicking.\r
+* [#10030](http://dev.ckeditor.com/ticket/10030): [IE] Fixed: Prevented "Unspecified Error" thrown in various cases when IE8-9 does not allow access to `document.activeElement`.\r
+* [#12273](http://dev.ckeditor.com/ticket/12273): Fixed: Applying block style in a description list breaks it.\r
+* [#12218](http://dev.ckeditor.com/ticket/12218): Fixed: Minor syntax issue in CSS files.\r
+* [#12178](http://dev.ckeditor.com/ticket/12178): [Blink/WebKit] Fixed: Iterator does not return the block if the selection is located at the end of it.\r
+* [#12185](http://dev.ckeditor.com/ticket/12185): [IE9QM] Fixed: Error thrown when moving the mouse over focused editor's scrollbar.\r
+* [#12215](http://dev.ckeditor.com/ticket/12215): Fixed: Basepath resolution does not recognize semicolon as a query separator.\r
+* [#12135](http://dev.ckeditor.com/ticket/12135): Fixed: [Remove Format](http://ckeditor.com/addon/removeformat) does not work on widgets.\r
+* [#12298](http://dev.ckeditor.com/ticket/12298): [IE11] Fixed: Clicking below `<body>` in Compatibility Mode will no longer reset selection to the first line.\r
+* [#12204](http://dev.ckeditor.com/ticket/12204): Fixed: Editor's voice label is not affected by [`config.title`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-title).\r
+* [#11915](http://dev.ckeditor.com/ticket/11915): Fixed: With [SCAYT](http://ckeditor.com/addon/scayt) enabled, cursor moves to the beginning of the first highlighted, misspelled word after typing or pasting into the editor.\r
+* [SCAYT](https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/69): Fixed: Error thrown in the console after enabling [SCAYT](http://ckeditor.com/addon/scayt) and trying to add a new image.\r
+\r
+\r
+Other Changes:\r
+\r
+* [#12296](http://dev.ckeditor.com/ticket/12296): Merged `benderjs-ckeditor` into the main CKEditor repository.\r
+\r
+## CKEditor 4.4.3\r
+\r
+**Security Updates:**\r
+\r
+* Fixed XSS vulnerability in the Preview plugin reported by Mario Heiderich of [Cure53](https://cure53.de/).\r
+\r
+**An upgrade is highly recommended!**\r
+\r
+New Features:\r
+\r
+* [#12164](http://dev.ckeditor.com/ticket/12164): Added the "Justify" option to the "Horizontal Alignment" drop-down in the Table Cell Properties dialog window.\r
+\r
+Fixed Issues:\r
+\r
+* [#12110](http://dev.ckeditor.com/ticket/12110): Fixed: Editor crash after deleting a table. Thanks to [Alin Purcaru](https://github.com/mesmerizero)!\r
+* [#11897](http://dev.ckeditor.com/ticket/11897): Fixed: *Enter* key used in an empty list item creates a new line instead of breaking the list. Thanks to [noam-si](https://github.com/noam-si)!\r
+* [#12140](http://dev.ckeditor.com/ticket/12140): Fixed: Double-clicking linked widgets opens two dialog windows.\r
+* [#12132](http://dev.ckeditor.com/ticket/12132): Fixed: Image is inserted with `width` and `height` styles even when they are not allowed.\r
+* [#9317](http://dev.ckeditor.com/ticket/9317): [IE] Fixed: [`config.disableObjectResizing`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-disableObjectResizing) does not work on IE. **Note**: We were not able to fix this issue on IE11+ because necessary events stopped working. See a [last resort workaround](http://dev.ckeditor.com/ticket/9317#comment:16) and make sure to [support our complaint to Microsoft](https://connect.microsoft.com/IE/feedback/details/742593/please-respect-execcommand-enableobjectresizing-in-contenteditable-elements).\r
+* [#9638](http://dev.ckeditor.com/ticket/9638): Fixed: There should be no information about accessibility help available under the *Alt+0* keyboard shortcut if the [Accessibility Help](http://ckeditor.com/addon/a11yhelp) plugin is not available.\r
+* [#8117](http://dev.ckeditor.com/ticket/8117) and [#9186](http://dev.ckeditor.com/ticket/9186): Fixed: In HTML5 `<meta>` tags should be allowed everywhere, including inside the `<body>` element.\r
+* [#10422](http://dev.ckeditor.com/ticket/10422): Fixed: [`config.fillEmptyBlocks`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-fillEmptyBlocks) not working properly if a function is specified.\r
+\r
+## CKEditor 4.4.2\r
+\r
+Important Notes:\r
+\r
+* The CKEditor testing environment is now publicly available. Read more about how to set up the environment and execute tests in the [CKEditor Testing Environment](http://docs.ckeditor.com/#!/guide/dev_tests) guide.\r
+       Please note that the [`tests/`](https://github.com/ckeditor/ckeditor-dev/tree/master/tests) directory which contains editor tests is not available in release packages. It can only be found in the development version of CKEditor on [GitHub](https://github.com/ckeditor/ckeditor-dev/).\r
+\r
+New Features:\r
+\r
+* [#11909](http://dev.ckeditor.com/ticket/11909): Introduced a parameter to prevent the [`editor.setData()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-setData) method from recording undo snapshots.\r
+\r
+Fixed Issues:\r
+\r
+* [#11757](http://dev.ckeditor.com/ticket/11757): Fixed: Imperfections in the [Moono](http://ckeditor.com/addon/moono) skin. Thanks to [danyaPostfactum](https://github.com/danyaPostfactum)!\r
+* [#10091](http://dev.ckeditor.com/ticket/10091): Blockquote should be treated like an object by the styles system. Thanks to [dan-james-deeson](https://github.com/dan-james-deeson)!\r
+* [#11478](http://dev.ckeditor.com/ticket/11478): Fixed: Issue with passing jQuery objects to [adapter](http://docs.ckeditor.com/#!/guide/dev_jquery) configuration.\r
+* [#10867](http://dev.ckeditor.com/ticket/10867): Fixed: Issue with setting encoded URI as image link.\r
+* [#11983](http://dev.ckeditor.com/ticket/11983): Fixed: Clicking a nested widget does not focus it. Additionally, performance of the [`widget.repository.getByElement()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.repository-method-getByElement) method was improved.\r
+* [#12000](http://dev.ckeditor.com/ticket/12000): Fixed: Nested widgets should be initialized on [`editor.setData()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-setData) and [`nestedEditable.setData()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.nestedEditable-method-setData).\r
+* [#12022](http://dev.ckeditor.com/ticket/12022): Fixed: Outer widget's drag handler is not created at all if it has any nested widgets inside.\r
+* [#11960](http://dev.ckeditor.com/ticket/11960): [Blink/WebKit] Fixed: The caret should be scrolled into view on *Backspace* and *Delete* (covers only the merging blocks case).\r
+* [#11306](http://dev.ckeditor.com/ticket/11306): [OSX][Blink/WebKit] Fixed: No widget entries in the context menu on widget right-click.\r
+* [#11957](http://dev.ckeditor.com/ticket/11957): Fixed: Alignment labels in the [Enhanced Image](http://ckeditor.com/addon/image2) dialog window are not translated.\r
+* [#11980](http://dev.ckeditor.com/ticket/11980): [Blink/WebKit] Fixed: `<span>` elements created when joining adjacent elements (non-collapsed selection).\r
+* [#12009](http://dev.ckeditor.com/ticket/12009): [Nested widgets] Integration with the [Magic Line](http://ckeditor.com/addon/magicline) plugin.\r
+* [#11387](http://dev.ckeditor.com/ticket/11387): Fixed: `role="radiogroup"` should be applied only to radio inputs' container.\r
+* [#7975](http://dev.ckeditor.com/ticket/7975): [IE8] Fixed: Errors when trying to select an empty table cell.\r
+* [#11947](http://dev.ckeditor.com/ticket/11947): [Firefox+IE11] Fixed: *Shift+Enter* in lists produces two line breaks.\r
+* [#11972](http://dev.ckeditor.com/ticket/11972): Fixed: Feature detection in the [`element.setText()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.element-method-setText) method should not trigger the layout engine.\r
+* [#7634](http://dev.ckeditor.com/ticket/7634): Fixed: The [Flash Dialog](http://ckeditor.com/addon/flash) plugin omits the `allowFullScreen` parameter in the editor data if set to `true`.\r
+* [#11910](http://dev.ckeditor.com/ticket/11910): Fixed: [Enhanced Image](http://ckeditor.com/addon/image2) does not take [`config.baseHref`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-baseHref) into account when updating image dimensions.\r
+* [#11753](http://dev.ckeditor.com/ticket/11753): Fixed: Wrong [`checkDirty()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-checkDirty) method value after focusing or blurring a widget.\r
+* [#11830](http://dev.ckeditor.com/ticket/11830): Fixed: Impossible to pass some arguments to [CKBuilder](https://github.com/ckeditor/ckbuilder) when using the `/dev/builder/build.sh` script.\r
+* [#11945](http://dev.ckeditor.com/ticket/11945): Fixed: [Form Elements](http://ckeditor.com/addon/forms) plugin should not change a core method.\r
+* [#11384](http://dev.ckeditor.com/ticket/11384): [IE9+] Fixed: `IndexSizeError` thrown when pasting into a non-empty selection anchored in one text node.\r
+\r
+## CKEditor 4.4.1\r
+\r
+New Features:\r
+\r
+* [#9661](http://dev.ckeditor.com/ticket/9661): Added the option to [configure](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-linkJavaScriptLinksAllowed) anchor tags with JavaScript code in the `href` attribute.\r
+\r
+Fixed Issues:\r
+\r
+* [#11861](http://dev.ckeditor.com/ticket/11861): [WebKit/Blink] Fixed: Span elements created while joining adjacent elements. **Note:** This patch only covers cases when *Backspace* or *Delete* is pressed on a collapsed (empty) selection. The remaining case, with a non-empty selection, will be fixed in the next release.\r
+* [#10714](http://dev.ckeditor.com/ticket/10714): [iOS] Fixed: Selection and drop-downs are broken if a touch event listener is used due to a [WebKit bug](https://bugs.webkit.org/show_bug.cgi?id=128924). Thanks to [Arty Gus](https://github.com/artygus)!\r
+* [#11911](http://dev.ckeditor.com/ticket/11911): Fixed setting the `dir` attribute for a preloaded language in [CKEDITOR.lang](http://docs.ckeditor.com/#!/api/CKEDITOR.lang). Thanks to [Akash Mohapatra](https://github.com/akashmohapatra)!\r
+* [#11926](http://dev.ckeditor.com/ticket/11926): Fixed: [Code Snippet](http://ckeditor.com/addon/codesnippet) does not decode HTML entities when loading code from the `<code>` element.\r
+* [#11223](http://dev.ckeditor.com/ticket/11223): Fixed: Issue when [Protected Source](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-protectedSource) was not working in the `<title>` element.\r
+* [#11859](http://dev.ckeditor.com/ticket/11859): Fixed: Removed the [Source Dialog](http://ckeditor.com/addon/sourcedialog) plugin dependency from the [Code Snippet](http://ckeditor.com/addon/codesnippet) sample.\r
+* [#11754](http://dev.ckeditor.com/ticket/11754): [Chrome] Fixed: Infinite loop when content includes not closed attributes.\r
+* [#11848](http://dev.ckeditor.com/ticket/11848): [IE] Fixed: [`editor.insertElement()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-insertElement) throwing an exception when there was no selection in the editor.\r
+* [#11801](http://dev.ckeditor.com/ticket/11801): Fixed: Editor anchors unavailable when linking the [Enhanced Image](http://ckeditor.com/addon/image2) widget.\r
+* [#11626](http://dev.ckeditor.com/ticket/11626): Fixed: [Table Resize](http://ckeditor.com/addon/tableresize) sets invalid column width.\r
+* [#11872](http://dev.ckeditor.com/ticket/11872): Made [`element.addClass()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.element-method-addClass) chainable symmetrically to [`element.removeClass()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.element-method-removeClass).\r
+* [#11813](http://dev.ckeditor.com/ticket/11813): Fixed: Link lost while pasting a captioned image and restoring an undo snapshot ([Enhanced Image](http://ckeditor.com/addon/image2)).\r
+* [#11814](http://dev.ckeditor.com/ticket/11814): Fixed: _Link_ and _Unlink_ entries persistently displayed in the [Enhanced Image](http://ckeditor.com/addon/image2) context menu.\r
+* [#11839](http://dev.ckeditor.com/ticket/11839): [IE9] Fixed: The caret jumps out of the editable area when resizing the editor in the source mode.\r
+* [#11822](http://dev.ckeditor.com/ticket/11822): [WebKit] Fixed: Editing anchors by double-click is broken in some cases.\r
+* [#11823](http://dev.ckeditor.com/ticket/11823): [IE8] Fixed: [Table Resize](http://ckeditor.com/addon/tableresize) throws an error over scrollbar.\r
+* [#11788](http://dev.ckeditor.com/ticket/11788): Fixed: It is not possible to change the language back to _Not set_ in the [Code Snippet](http://ckeditor.com/addon/codesnippet) dialog window.\r
+* [#11788](http://dev.ckeditor.com/ticket/11788): Fixed: [Filter](http://docs.ckeditor.com/#!/api/CKEDITOR.htmlParser.filter) rules are not applied inside elements with the `contenteditable` attribute set to `true`.\r
+* [#11798](http://dev.ckeditor.com/ticket/11798): Fixed: Inserting a non-editable element inside a table cell breaks the table.\r
+* [#11793](http://dev.ckeditor.com/ticket/11793): Fixed: Drop-down is not "on" when clicking it while the editor is blurred.\r
+* [#11850](http://dev.ckeditor.com/ticket/11850): Fixed: Fake objects with the `contenteditable` attribute set to `false` are not downcasted properly.\r
+* [#11811](http://dev.ckeditor.com/ticket/11811): Fixed: Widget's data is not encoded correctly when passed to an attribute.\r
+* [#11777](http://dev.ckeditor.com/ticket/11777): Fixed encoding ampersand in the [Mathematical Formulas](http://ckeditor.com/addon/mathjax) plugin.\r
+* [#11880](http://dev.ckeditor.com/ticket/11880): [IE8-9] Fixed: Linked image has a default thick border.\r
+\r
+Other Changes:\r
+\r
+* [#11807](http://dev.ckeditor.com/ticket/11807): Updated jQuery version used in the sample to 1.11.0 and tested CKEditor jQuery Adapter with version 1.11.0 and 2.1.0.\r
+* [#9504](http://dev.ckeditor.com/ticket/9504): Stopped using deprecated `attribute.specified` in all browsers except Internet Explorer.\r
+* [#11809](http://dev.ckeditor.com/ticket/11809): Changed tab size in `<pre>` to 4 spaces.\r
+\r
+## CKEditor 4.4\r
+\r
+**Important Notes:**\r
+\r
+* Marked the [`editor.beforePaste`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-beforePaste) event as deprecated.\r
+* The default class of captioned images has changed to `image` (was: `caption`). Please note that once edited in CKEditor 4.4+, all existing images of the `caption` class (`<figure class="caption">`) will be [filtered out](http://docs.ckeditor.com/#!/guide/dev_advanced_content_filter) unless the [`config.image2_captionedClass`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-image2_captionedClass) option is set to `caption`. For backward compatibility (i.e. when upgrading), it is highly recommended to use this setting, which also helps prevent CSS conflicts, etc. This does not apply to new CKEditor integrations.\r
+* Widgets without defined buttons are no longer registered automatically to the [Advanced Content Filter](http://docs.ckeditor.com/#!/guide/dev_advanced_content_filter). Before CKEditor 4.4 widgets were registered to the ACF which was an incorrect behavior ([#11567](http://dev.ckeditor.com/ticket/11567)). This change should not have any impact on standard scenarios, but if your button does not execute the widget command, you need to set [`allowedContent`](http://docs.ckeditor.com/#!/api/CKEDITOR.feature-property-allowedContent) and [`requiredContent`](http://docs.ckeditor.com/#!/api/CKEDITOR.feature-property-requiredContent) properties for it manually, because the editor will not be able to find them.\r
+* The [Show Borders](http://ckeditor.com/addon/showborders) plugin was added to the Standard installation package in order to ensure that unstyled tables are still visible for the user ([#11665](http://dev.ckeditor.com/ticket/11665)).\r
+* Since CKEditor 4.4 the editor instance should be passed to [`CKEDITOR.style`](http://docs.ckeditor.com/#!/api/CKEDITOR.style) methods to ensure full compatibility with other features (e.g. applying styles to widgets requires that). We ensured backward compatibility though, so the [`CKEDITOR.style`](http://docs.ckeditor.com/#!/api/CKEDITOR.style) will work even when the editor instance is not provided.\r
+\r
+New Features:\r
+\r
+* [#11297](http://dev.ckeditor.com/ticket/11297): Styles can now be applied to widgets. The definition of a style which can be applied to a specific widget must contain two additional properties &mdash; `type` and `widget`. Read more in the [Widget Styles](http://docs.ckeditor.com/#!/guide/dev_styles-section-widget-styles) section of the "Syles Drop-down" guide. Note that by default, widgets support only classes and no other attributes or styles. Related changes and features:\r
+  * Introduced the [`CKEDITOR.style.addCustomHandler()`](http://docs.ckeditor.com/#!/api/CKEDITOR.style-static-method-addCustomHandler) method for registering custom style handlers.\r
+  * The [`CKEDITOR.style.apply()`](http://docs.ckeditor.com/#!/api/CKEDITOR.style-method-apply) and [`CKEDITOR.style.remove()`](http://docs.ckeditor.com/#!/api/CKEDITOR.style-method-remove) methods are now called with an editor instance instead of the document so they can be reused by the [`CKEDITOR.editor.applyStyle()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-applyStyle) and [`CKEDITOR.editor.removeStyle()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-removeStyle) methods. Backward compatibility was preserved, but from CKEditor 4.4 it is highly recommended to pass an editor instead of a document to these methods.\r
+  * Many new methods and properties were introduced in the [Widget API](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget) to make the handling of styles by widgets fully customizable. See: [`widget.definition.styleableElements`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.definition-property-styleableElements), [`widget.definition.styleToAllowedContentRule`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.definition-property-styleToAllowedContentRules), [`widget.addClass()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget-method-addClass), [`widget.removeClass()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget-method-removeClass), [`widget.getClasses()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget-method-getClasses), [`widget.hasClass()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget-method-hasClass), [`widget.applyStyle()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget-method-applyStyle), [`widget.removeStyle()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget-method-removeStyle), [`widget.checkStyleActive()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget-method-checkStyleActive).\r
+  * Integration with the [Allowed Content Filter](http://docs.ckeditor.com/#!/guide/dev_advanced_content_filter) required an introduction of the [`CKEDITOR.style.toAllowedContent()`](http://docs.ckeditor.com/#!/api/CKEDITOR.style-method-toAllowedContentRules) method which can be implemented by the custom style handler and if exists, it is used by the [`CKEDITOR.filter`](http://docs.ckeditor.com/#!/api/CKEDITOR.filter) to translate a style to [allowed content rules](http://docs.ckeditor.com/#!/api/CKEDITOR.filter.allowedContentRules).\r
+* [#11300](http://dev.ckeditor.com/ticket/11300): Various changes in the [Enhanced Image](http://ckeditor.com/addon/image2) plugin:\r
+  * Introduced the [`config.image2_captionedClass`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-image2_captionedClass) option to configure the class of captioned images.\r
+  * Introduced the [`config.image2_alignClasses`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-image2_alignClasses) option to configure the way images are aligned with CSS classes.\r
+  If this setting is defined, the editor produces classes instead of inline styles for aligned images.\r
+  * Default image caption can be translated (customized) with the `editor.lang.image2.captionPlaceholder` string.\r
+* [#11341](http://dev.ckeditor.com/ticket/11341): [Enhanced Image](http://ckeditor.com/addon/image2) plugin: It is now possible to add a link to any image type.\r
+* [#10202](http://dev.ckeditor.com/ticket/10202): Introduced wildcard support in the [Allowed Content Rules](http://docs.ckeditor.com/#!/guide/dev_allowed_content_rules) format.\r
+* [#10276](http://dev.ckeditor.com/ticket/10276): Introduced blacklisting in the [Allowed Content Filter](http://docs.ckeditor.com/#!/guide/dev_advanced_content_filter).\r
+* [#10480](http://dev.ckeditor.com/ticket/10480): Introduced code snippets with code highlighting. There are two versions available so far &mdash; the default [Code Snippet](http://ckeditor.com/addon/codesnippet) which uses the [highlight.js](http://highlightjs.org) library and the [Code Snippet GeSHi](http://ckeditor.com/addon/codesnippetgeshi) which uses the [GeSHi](http://qbnz.com/highlighter/) library.\r
+* [#11737](http://dev.ckeditor.com/ticket/11737): Introduced an option to prevent [filtering](http://docs.ckeditor.com/#!/guide/dev_advanced_content_filter) of an element that matches custom criteria (see [`filter.addElementCallback()`](http://docs.ckeditor.com/#!/api/CKEDITOR.filter-method-addElementCallback)).\r
+* [#11532](http://dev.ckeditor.com/ticket/11532): Introduced the [`editor.addContentsCss()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-addContentsCss) method that can be used for [adding custom CSS files](http://docs.ckeditor.com/#!/guide/plugin_sdk_styles).\r
+* [#11536](http://dev.ckeditor.com/ticket/11536): Added the [`CKEDITOR.tools.htmlDecode()`](http://docs.ckeditor.com/#!/api/CKEDITOR.tools-method-htmlDecode) method for decoding HTML entities.\r
+* [#11225](http://dev.ckeditor.com/ticket/11225): Introduced the [`CKEDITOR.tools.transparentImageData`](http://docs.ckeditor.com/#!/api/CKEDITOR.tools-property-transparentImageData) property which contains transparent image data to be used in CSS or as image source.\r
+\r
+Other Changes:\r
+\r
+* [#11377](http://dev.ckeditor.com/ticket/11377): Unified internal representation of empty anchors using the [fake objects](http://ckeditor.com/addon/fakeobjects).\r
+* [#11422](http://dev.ckeditor.com/ticket/11422): Removed Firefox 3.x, Internet Explorer 6 and Opera 12.x leftovers in code.\r
+* [#5217](http://dev.ckeditor.com/ticket/5217): Setting data (including switching between modes) creates a new undo snapshot. Besides that:\r
+  * Introduced the [`editable.status`](http://docs.ckeditor.com/#!/api/CKEDITOR.editable-property-status) property.\r
+  * Introduced a new `forceUpdate` option for the [`editor.lockSnapshot`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-lockSnapshot) event.\r
+  * Fixed: Selection not being unlocked in inline editor after setting data ([#11500](http://dev.ckeditor.com/ticket/11500)).\r
+* The [WebSpellChecker](http://ckeditor.com/addon/wsc) plugin was updated to the latest version.\r
+\r
+Fixed Issues:\r
+\r
+* [#10190](http://dev.ckeditor.com/ticket/10190): Fixed: Removing block style with [`editor.removeStyle()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-removeStyle) should result in a paragraph and not a div.\r
+* [#11727](http://dev.ckeditor.com/ticket/11727): Fixed: The editor tries to select a non-editable image which was clicked.\r
+\r
+## CKEditor 4.3.5\r
+\r
+New Features:\r
+\r
+* Added new translation: Tatar.\r
+\r
+Fixed Issues:\r
+\r
+* [#11677](http://dev.ckeditor.com/ticket/11677): Fixed: Undo/Redo keystrokes are blocked in the source mode.\r
+* [#11717](http://dev.ckeditor.com/ticket/11717): [Document Properties](http://ckeditor.com/addon/docprops) plugin requires the [Color Dialog](http://ckeditor.com/addon/colordialog) plugin to work.\r
+\r
+## CKEditor 4.3.4\r
+\r
+Fixed Issues:\r
+\r
+* [#11597](http://dev.ckeditor.com/ticket/11597): [IE11] Fixed: Error thrown when trying to open the [preview](http://ckeditor.com/addon/preview) using the keyboard.\r
+* [#11544](http://dev.ckeditor.com/ticket/11544): [Placeholders](http://ckeditor.com/addon/placeholder) will no longer be upcasted in parents not accepting `<span>` elements.\r
+* [#8663](http://dev.ckeditor.com/ticket/8663): Fixed [`element.renameNode()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.element-method-renameNode) not clearing the [`element.getName()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.element-method-getName) cache.\r
+* [#11574](http://dev.ckeditor.com/ticket/11574): Fixed: *Backspace* destroying the DOM structure if an inline editable is placed in a list item.\r
+* [#11603](http://dev.ckeditor.com/ticket/11603): Fixed: [Table Resize](http://ckeditor.com/addon/tableresize) attaches to tables outside the editable.\r
+* [#9205](http://dev.ckeditor.com/ticket/9205), [#7805](http://dev.ckeditor.com/ticket/7805), [#8216](http://dev.ckeditor.com/ticket/8216): Fixed: `{cke_protected_1}` appearing in data in various cases where HTML comments are placed next to `"` or `'`.\r
+* [#11635](http://dev.ckeditor.com/ticket/11635): Fixed: Some attributes are not protected before the content is passed through the fix bin.\r
+* [#11660](http://dev.ckeditor.com/ticket/11660): [IE] Fixed: Table content is lost when some extra markup is inside the table.\r
+* [#11641](http://dev.ckeditor.com/ticket/11641): Fixed: Switching between modes in the classic editor removes content styles for the inline editor.\r
+* [#11568](http://dev.ckeditor.com/ticket/11568): Fixed: [Styles](http://ckeditor.com/addon/stylescombo) drop-down list is not enabled on selection change.\r
+\r
+## CKEditor 4.3.3\r
+\r
+Fixed Issues:\r
+\r
+* [#11500](http://dev.ckeditor.com/ticket/11500): [WebKit/Blink] Fixed: Selection lost when setting data in another inline editor. Additionally, [`selection.removeAllRanges()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.selection-method-removeAllRanges) is now scoped to selection's [root](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.selection-property-root).\r
+* [#11104](http://dev.ckeditor.com/ticket/11104): [IE] Fixed: Various issues with scrolling and selection when focusing widgets.\r
+* [#11487](http://dev.ckeditor.com/ticket/11487): Moving mouse over the [Enhanced Image](http://ckeditor.com/addon/image2) widget will no longer change the value returned by the [`editor.checkDirty()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-checkDirty) method.\r
+* [#8673](http://dev.ckeditor.com/ticket/8673): [WebKit] Fixed: Cannot select and remove the [Page Break](http://ckeditor.com/addon/pagebreak).\r
+* [#11413](http://dev.ckeditor.com/ticket/11413): Fixed: Incorrect [`editor.execCommand()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-execCommand) behavior.\r
+* [#11438](http://dev.ckeditor.com/ticket/11438): Splitting table cells vertically is no longer changing table structure.\r
+* [#8899](http://dev.ckeditor.com/ticket/8899): Fixed: Links in the [About CKEditor](http://ckeditor.com/addon/about) dialog window now open in a new browser window or tab.\r
+* [#11490](http://dev.ckeditor.com/ticket/11490): Fixed: [Menu button](http://ckeditor.com/addon/menubutton) panel not showing in the source mode.\r
+* [#11417](http://dev.ckeditor.com/ticket/11417): The [`widget.doubleclick`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget-event-doubleclick) event is not canceled anymore after editing was triggered.\r
+* [#11253](http://dev.ckeditor.com/ticket/11253): [IE] Fixed: Clipped upload button in the [Enhanced Image](http://ckeditor.com/addon/image2) dialog window.\r
+* [#11359](http://dev.ckeditor.com/ticket/11359): Standardized the way anchors are discovered by the [Link](http://ckeditor.com/addon/link) plugin.\r
+* [#11058](http://dev.ckeditor.com/ticket/11058): [IE8] Fixed: Error when deleting a table row.\r
+* [#11508](http://dev.ckeditor.com/ticket/11508): Fixed: [`htmlDataProcessor`](http://docs.ckeditor.com/#!/api/CKEDITOR.htmlDataProcessor) discovering protected attributes within other attributes' values.\r
+* [#11533](http://dev.ckeditor.com/ticket/11533): Widgets: Avoid recurring upcasts if the DOM structure was modified during an upcast.\r
+* [#11400](http://dev.ckeditor.com/ticket/11400): Fixed: The [`domObject.removeAllListeners()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.domObject-method-removeAllListeners) method does not remove custom listeners completely.\r
+* [#11493](http://dev.ckeditor.com/ticket/11493): Fixed: The [`selection.getRanges()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.selection-method-getRanges) method does not override cached ranges when used with the `onlyEditables` argument.\r
+* [#11390](http://dev.ckeditor.com/ticket/11390): [IE] All [XML](http://ckeditor.com/addon/xml) plugin [methods](http://docs.ckeditor.com/#!/api/CKEDITOR.xml) now work in IE10+.\r
+* [#11542](http://dev.ckeditor.com/ticket/11542): [IE11] Fixed: Blurry toolbar icons when Right-to-Left UI language is set.\r
+* [#11504](http://dev.ckeditor.com/ticket/11504): Fixed: When [`config.fullPage`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-fullPage) is set to `true`, entities are not encoded in editor output.\r
+* [#11004](http://dev.ckeditor.com/ticket/11004): Integrated [Enhanced Image](http://ckeditor.com/addon/image2) dialog window with [Advanced Content Filter](http://docs.ckeditor.com/#!/guide/dev_advanced_content_filter).\r
+* [#11439](http://dev.ckeditor.com/ticket/11439): Fixed: Properties get cloned in the Cell Properties dialog window if multiple cells are selected.\r
+\r
+## CKEditor 4.3.2\r
+\r
+Fixed Issues:\r
+\r
+* [#11331](http://dev.ckeditor.com/ticket/11331): A menu button will have a changed label when selected instead of using the `aria-pressed` attribute.\r
+* [#11177](http://dev.ckeditor.com/ticket/11177): Widget drag handler improvements:\r
+  * [#11176](http://dev.ckeditor.com/ticket/11176): Fixed: Initial position is not updated when the widget data object is empty.\r
+  * [#11001](http://dev.ckeditor.com/ticket/11001): Fixed: Multiple synchronous layout recalculations are caused by initial drag handler positioning causing performance issues.\r
+  * [#11161](http://dev.ckeditor.com/ticket/11161): Fixed: Drag handler is not repositioned in various situations.\r
+  * [#11281](http://dev.ckeditor.com/ticket/11281): Fixed: Drag handler and mask are duplicated after widget reinitialization.\r
+* [#11207](http://dev.ckeditor.com/ticket/11207): [Firefox] Fixed: Misplaced [Enhanced Image](http://ckeditor.com/addon/image2) resizer in the inline editor.\r
+* [#11102](http://dev.ckeditor.com/ticket/11102): `CKEDITOR.template` improvements:\r
+  * [#11102](http://dev.ckeditor.com/ticket/11102): Added newline character support.\r
+  * [#11216](http://dev.ckeditor.com/ticket/11216): Added "\\'" substring support.\r
+* [#11121](http://dev.ckeditor.com/ticket/11121): [Firefox] Fixed: High Contrast mode is enabled when the editor is loaded in a hidden iframe.\r
+* [#11350](http://dev.ckeditor.com/ticket/11350): The default value of [`config.contentsCss`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-contentsCss) is affected by [`CKEDITOR.getUrl()`](http://docs.ckeditor.com/#!/api/CKEDITOR-method-getUrl).\r
+* [#11097](http://dev.ckeditor.com/ticket/11097): Improved the [Autogrow](http://ckeditor.com/addon/autogrow) plugin performance when dealing with very big tables.\r
+* [#11290](http://dev.ckeditor.com/ticket/11290): Removed redundant code in the [Source Dialog](http://ckeditor.com/addon/sourcedialog) plugin.\r
+* [#11133](http://dev.ckeditor.com/ticket/11133): [Page Break](http://ckeditor.com/addon/pagebreak) becomes editable if pasted.\r
+* [#11126](http://dev.ckeditor.com/ticket/11126): Fixed: Native Undo executed once the bottom of the snapshot stack is reached.\r
+* [#11131](http://dev.ckeditor.com/ticket/11131): [Div Editing Area](http://ckeditor.com/addon/divarea): Fixed: Error thrown when switching to source mode if the selection was in widget's nested editable.\r
+* [#11139](http://dev.ckeditor.com/ticket/11139): [Div Editing Area](http://ckeditor.com/addon/divarea): Fixed: Elements Path is not cleared after switching to source mode.\r
+* [#10778](http://dev.ckeditor.com/ticket/10778): Fixed a bug with range enlargement. The range no longer expands to visible whitespace.\r
+* [#11146](http://dev.ckeditor.com/ticket/11146): [IE] Fixed: Preview window switches Internet Explorer to Quirks Mode.\r
+* [#10762](http://dev.ckeditor.com/ticket/10762): [IE] Fixed: JavaScript code displayed in preview window's URL bar.\r
+* [#11186](http://dev.ckeditor.com/ticket/11186): Introduced the [`widgets.repository.addUpcastCallback()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.repository-method-addUpcastCallback) method that allows to block upcasting given element to a widget.\r
+* [#11307](http://dev.ckeditor.com/ticket/11307): Fixed: Paste as Plain Text conflict with the [MooTools](http://mootools.net) library.\r
+* [#11140](http://dev.ckeditor.com/ticket/11140): [IE11] Fixed: Anchors are not draggable.\r
+* [#11379](http://dev.ckeditor.com/ticket/11379): Changed default contents `line-height` to unitless values to avoid huge text overlapping (like in [#9696](http://dev.ckeditor.com/ticket/9696)).\r
+* [#10787](http://dev.ckeditor.com/ticket/10787): [Firefox] Fixed: Broken replacement of text while pasting into `div`-based editor.\r
+* [#10884](http://dev.ckeditor.com/ticket/10884): Widgets integration with the [Show Blocks](http://ckeditor.com/addon/showblocks) plugin.\r
+* [#11021](http://dev.ckeditor.com/ticket/11021): Fixed: An error thrown when selecting entire editable contents while fake selection is on.\r
+* [#11086](http://dev.ckeditor.com/ticket/11086): [IE8] Re-enable inline widgets drag&drop in Internet Explorer 8.\r
+* [#11372](http://dev.ckeditor.com/ticket/11372): Widgets: Special characters encoded twice in nested editables.\r
+* [#10068](http://dev.ckeditor.com/ticket/10068): Fixed: Support for protocol-relative URLs.\r
+* [#11283](http://dev.ckeditor.com/ticket/11283): [Enhanced Image](http://ckeditor.com/addon/image2): A `<div>` element with `text-align: center` and an image inside is not recognised correctly.\r
+* [#11196](http://dev.ckeditor.com/ticket/11196): [Accessibility Instructions](http://ckeditor.com/addon/a11yhelp): Allowed additional keyboard button labels to be translated in the dialog window.\r
+\r
+## CKEditor 4.3.1\r
+\r
+**Important Notes:**\r
+\r
+* To match the naming convention, the `language` button is now `Language` ([#11201](http://dev.ckeditor.com/ticket/11201)).\r
+* [Enhanced Image](http://ckeditor.com/addon/image2) button, context menu, command, and icon names match those of the [Image](http://ckeditor.com/addon/image) plugin ([#11222](http://dev.ckeditor.com/ticket/11222)).\r
+\r
+Fixed Issues:\r
+\r
+* [#11244](http://dev.ckeditor.com/ticket/11244): Changed: The [`widget.repository.checkWidgets()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.repository-method-checkWidgets) method now fires the [`widget.repository.checkWidgets`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.repository-event-checkWidgets) event, so from CKEditor 4.3.1 it is preferred to use the method rather than fire the event.\r
+* [#11171](http://dev.ckeditor.com/ticket/11171): Fixed: [`editor.insertElement()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-insertElement) and [`editor.insertText()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-insertText) methods do not call the [`widget.repository.checkWidgets()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.repository-method-checkWidgets) method.\r
+* [#11085](http://dev.ckeditor.com/ticket/11085): [IE8] Replaced preview generated by the [Mathematical Formulas](http://ckeditor.com/addon/mathjax) widget with a placeholder.\r
+* [#11044](http://dev.ckeditor.com/ticket/11044): Enhanced WAI-ARIA support for the [Language](http://ckeditor.com/addon/language) plugin drop-down menu.\r
+* [#11075](http://dev.ckeditor.com/ticket/11075): With drop-down menu button focused, pressing the *Down Arrow* key will now open the menu and focus its first option.\r
+* [#11165](http://dev.ckeditor.com/ticket/11165): Fixed: The [File Browser](http://ckeditor.com/addon/filebrowser) plugin cannot be removed from the editor.\r
+* [#11159](http://dev.ckeditor.com/ticket/11159): [IE9-10] [Enhanced Image](http://ckeditor.com/addon/image2): Fixed buggy discovery of image dimensions.\r
+* [#11101](http://dev.ckeditor.com/ticket/11101): Drop-down lists no longer break when given double quotes.\r
+* [#11077](http://dev.ckeditor.com/ticket/11077): [Enhanced Image](http://ckeditor.com/addon/image2): Empty undo step recorded when resizing the image.\r
+* [#10853](http://dev.ckeditor.com/ticket/10853): [Enhanced Image](http://ckeditor.com/addon/image2): Widget has paragraph wrapper when de-captioning unaligned image.\r
+* [#11198](http://dev.ckeditor.com/ticket/11198): Widgets: Drag handler is not fully visible when an inline widget is in a heading.\r
+* [#11132](http://dev.ckeditor.com/ticket/11132): [Firefox] Fixed: Caret is lost after drag and drop of an inline widget.\r
+* [#11182](http://dev.ckeditor.com/ticket/11182): [IE10-11] Fixed: Editor crashes (IE11) or works with minor issues (IE10) if a page is loaded in Quirks Mode. See [`env.quirks`](http://docs.ckeditor.com/#!/api/CKEDITOR.env-property-quirks) for more details.\r
+* [#11204](http://dev.ckeditor.com/ticket/11204): Added `figure` and `figcaption` styles to the `contents.css` file so [Enhanced Image](http://ckeditor.com/addon/image2) looks nicer.\r
+* [#11202](http://dev.ckeditor.com/ticket/11202): Fixed: No newline in [BBCode](http://ckeditor.com/addon/bbcode) mode.\r
+* [#10890](http://dev.ckeditor.com/ticket/10890): Fixed: Error thrown when pressing the *Delete* key in a list item.\r
+* [#10055](http://dev.ckeditor.com/ticket/10055): [IE8-10] Fixed: *Delete* pressed on a selected image causes the browser to go back.\r
+* [#11183](http://dev.ckeditor.com/ticket/11183): Fixed: Inserting a horizontal rule or a table in multiple row selection causes a browser crash. Additionally, the [`editor.insertElement()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-insertElement) method does not insert the element into every range of a selection any more.\r
+* [#11042](http://dev.ckeditor.com/ticket/11042): Fixed: Selection made on an element containing a non-editable element was not auto faked.\r
+* [#11125](http://dev.ckeditor.com/ticket/11125): Fixed: Keyboard navigation through menu and drop-down items will now cycle.\r
+* [#11011](http://dev.ckeditor.com/ticket/11011): Fixed: The [`editor.applyStyle()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-applyStyle) method removes attributes from nested elements.\r
+* [#11179](http://dev.ckeditor.com/ticket/11179): Fixed: [`editor.destroy()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-destroy) does not cleanup content generated by the [Table Resize](http://ckeditor.com/addon/tableresize) plugin for inline editors.\r
+* [#11237](http://dev.ckeditor.com/ticket/11237): Fixed: Table border attribute value is deleted when pasting content from Microsoft Word.\r
+* [#11250](http://dev.ckeditor.com/ticket/11250): Fixed: HTML entities inside the `<textarea>` element are not encoded.\r
+* [#11260](http://dev.ckeditor.com/ticket/11260): Fixed: Initially disabled buttons are not read by JAWS as disabled.\r
+* [#11200](http://dev.ckeditor.com/ticket/11200):  Added [Clipboard](http://ckeditor.com/addon/clipboard) plugin as a dependency for [Widget](http://ckeditor.com/addon/widget) to fix drag and drop.\r
+\r
+## CKEditor 4.3\r
+\r
+New Features:\r
+\r
+* [#10612](http://dev.ckeditor.com/ticket/10612): Internet Explorer 11 support.\r
+* [#10869](http://dev.ckeditor.com/ticket/10869): Widgets: Added better integration with the [Elements Path](http://ckeditor.com/addon/elementspath) plugin.\r
+* [#10886](http://dev.ckeditor.com/ticket/10886): Widgets: Added tooltip to the drag handle.\r
+* [#10933](http://dev.ckeditor.com/ticket/10933): Widgets: Introduced drag and drop of block widgets with the [Line Utilities](http://ckeditor.com/addon/lineutils) plugin.\r
+* [#10936](http://dev.ckeditor.com/ticket/10936): Widget System changes for easier integration with other dialog systems.\r
+* [#10895](http://dev.ckeditor.com/ticket/10895): [Enhanced Image](http://ckeditor.com/addon/image2): Added file browser integration.\r
+* [#11002](http://dev.ckeditor.com/ticket/11002): Added the [`draggable`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.definition-property-draggable) option to disable drag and drop support for widgets.\r
+* [#10937](http://dev.ckeditor.com/ticket/10937): [Mathematical Formulas](http://ckeditor.com/addon/mathjax) widget improvements:\r
+  * loading indicator ([#10948](http://dev.ckeditor.com/ticket/10948)),\r
+  * applying paragraph changes (like font color change) to iframe ([#10841](http://dev.ckeditor.com/ticket/10841)),\r
+  * Firefox and IE9 clipboard fixes ([#10857](http://dev.ckeditor.com/ticket/10857)),\r
+  * fixing same origin policy issue ([#10840](http://dev.ckeditor.com/ticket/10840)),\r
+  * fixing undo bugs ([#10842](http://dev.ckeditor.com/ticket/10842), [#10930](http://dev.ckeditor.com/ticket/10930)),\r
+  * fixing other minor bugs.\r
+* [#10862](http://dev.ckeditor.com/ticket/10862): [Placeholder](http://ckeditor.com/addon/placeholder) plugin was rewritten as a widget.\r
+* [#10822](http://dev.ckeditor.com/ticket/10822): Added styles system integration with non-editable elements (for example widgets) and their nested editables. Styles cannot change non-editable content and are applied in nested editable only if allowed by its type and content filter.\r
+* [#10856](http://dev.ckeditor.com/ticket/10856): Menu buttons will now toggle the visibility of their panels when clicked multiple times. [Language](http://ckeditor.com/addon/language) plugin fixes: Added active language highlighting, added an option to remove the language.\r
+* [#10028](http://dev.ckeditor.com/ticket/10028): New [`config.dialog_noConfirmCancel`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-dialog_noConfirmCancel) configuration option that eliminates the need to confirm closing of a dialog window when the user changed any of its fields.\r
+* [#10848](http://dev.ckeditor.com/ticket/10848): Integrate remaining plugins ([Styles](http://ckeditor.com/addon/stylescombo), [Format](http://ckeditor.com/addon/format), [Font](http://ckeditor.com/addon/font), [Color Button](http://ckeditor.com/addon/colorbutton), [Language](http://ckeditor.com/addon/language) and [Indent](http://ckeditor.com/addon/indent)) with [active filter](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-activeFilter).\r
+* [#10855](http://dev.ckeditor.com/ticket/10855): Change the extension of emoticons in the [BBCode](http://ckeditor.com/addon/bbcode) sample from GIF to PNG.\r
+\r
+Fixed Issues:\r
+\r
+* [#10831](http://dev.ckeditor.com/ticket/10831): [Enhanced Image](http://ckeditor.com/addon/image2): Merged `image2inline` and `image2block` into one `image2` widget.\r
+* [#10835](http://dev.ckeditor.com/ticket/10835): [Enhanced Image](http://ckeditor.com/addon/image2): Improved visibility of the resize handle.\r
+* [#10836](http://dev.ckeditor.com/ticket/10836): [Enhanced Image](http://ckeditor.com/addon/image2): Preserve custom mouse cursor while resizing the image.\r
+* [#10939](http://dev.ckeditor.com/ticket/10939): [Firefox] [Enhanced Image](http://ckeditor.com/addon/image2): hovering the image causes it to change.\r
+* [#10866](http://dev.ckeditor.com/ticket/10866): Fixed: Broken *Tab* key navigation in the [Enhanced Image](http://ckeditor.com/addon/image2) dialog window.\r
+* [#10833](http://dev.ckeditor.com/ticket/10833): Fixed: *Lock ratio* option should be on by default in the [Enhanced Image](http://ckeditor.com/addon/image2) dialog window.\r
+* [#10881](http://dev.ckeditor.com/ticket/10881): Various improvements to *Enter* key behavior in nested editables.\r
+* [#10879](http://dev.ckeditor.com/ticket/10879): [Remove Format](http://ckeditor.com/addon/removeformat) should not leak from a nested editable.\r
+* [#10877](http://dev.ckeditor.com/ticket/10877): Fixed: [WebSpellChecker](http://ckeditor.com/addon/wsc) fails to apply changes if a nested editable was focused.\r
+* [#10877](http://dev.ckeditor.com/ticket/10877): Fixed: [SCAYT](http://ckeditor.com/addon/wsc) blocks typing in nested editables.\r
+* [#11079](http://dev.ckeditor.com/ticket/11079): Add button icons to the [Placeholder](http://ckeditor.com/addon/placeholder) sample.\r
+* [#10870](http://dev.ckeditor.com/ticket/10870): The `paste` command is no longer being disabled when the clipboard is empty.\r
+* [#10854](http://dev.ckeditor.com/ticket/10854): Fixed: Firefox prepends `<br>` to `<body>`, so it is stripped by the HTML data processor.\r
+* [#10823](http://dev.ckeditor.com/ticket/10823): Fixed: [Link](http://ckeditor.com/addon/link) plugin does not work with non-editable content.\r
+* [#10828](http://dev.ckeditor.com/ticket/10828): [Magic Line](http://ckeditor.com/addon/magicline) integration with the Widget System.\r
+* [#10865](http://dev.ckeditor.com/ticket/10865): Improved hiding copybin, so copying widgets works smoothly.\r
+* [#11066](http://dev.ckeditor.com/ticket/11066): Widget's private parts use CSS reset.\r
+* [#11027](http://dev.ckeditor.com/ticket/11027): Fixed: Block commands break on widgets; added the [`contentDomInvalidated`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-contentDomInvalidated) event.\r
+* [#10430](http://dev.ckeditor.com/ticket/10430): Resolve dependence of the [Image](http://ckeditor.com/addon/image) plugin on the [Form Elements](http://ckeditor.com/addon/forms) plugin.\r
+* [#10911](http://dev.ckeditor.com/ticket/10911): Fixed: Browser *Alt* hotkeys will no longer be blocked while a widget is focused.\r
+* [#11082](http://dev.ckeditor.com/ticket/11082): Fixed: Selected widget is not copied or cut when using toolbar buttons or context menu.\r
+* [#11083](http://dev.ckeditor.com/ticket/11083): Fixed list and div element application to block widgets.\r
+* [#10887](http://dev.ckeditor.com/ticket/10887): Internet Explorer 8 compatibility issues related to the Widget System.\r
+* [#11074](http://dev.ckeditor.com/ticket/11074): Temporarily disabled inline widget drag and drop, because of seriously buggy native `range#moveToPoint` method.\r
+* [#11098](http://dev.ckeditor.com/ticket/11098): Fixed: Wrong selection position after undoing widget drag and drop.\r
+* [#11110](http://dev.ckeditor.com/ticket/11110): Fixed: IFrame and Flash objects are being incorrectly pasted in certain conditions.\r
+* [#11129](http://dev.ckeditor.com/ticket/11129): Page break is lost when loading data.\r
+* [#11123](http://dev.ckeditor.com/ticket/11123): [Firefox] Widget is destroyed after being dragged outside of `<body>`.\r
+* [#11124](http://dev.ckeditor.com/ticket/11124): Fixed the [Elements Path](http://ckeditor.com/addon/elementspath) in an editor using the [Div Editing Area](http://ckeditor.com/addon/divarea).\r
+\r
+## CKEditor 4.3 Beta\r
+\r
+New Features:\r
+\r
+* [#9764](http://dev.ckeditor.com/ticket/9764): Widget System.\r
+  * [Widget plugin](http://ckeditor.com/addon/widget) introducing the [Widget API](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget).\r
+  * New [`editor.enterMode`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-enterMode) and [`editor.shiftEnterMode`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-shiftEnterMode) properties &ndash; normalized versions of [`config.enterMode`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-enterMode) and [`config.shiftEnterMode`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-shiftEnterMode).\r
+  * Dynamic editor settings. Starting from CKEditor 4.3 Beta, *Enter* mode values and [content filter](http://docs.ckeditor.com/#!/guide/dev_advanced_content_filter) instances may be changed dynamically (for example when the caret was placed in an element in which editor features should be adjusted). When you are implementing a new editor feature, you should base its behavior on [dynamic](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-activeEnterMode) or [static](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-enterMode) *Enter* mode values depending on whether this feature works in selection context or globally on editor content.\r
+      * Dynamic *Enter* mode values &ndash; [`editor.setActiveEnterMode()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-setActiveEnterMode) method, [`editor.activeEnterModeChange`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-activeEnterModeChange) event, and two properties: [`editor.activeEnterMode`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-activeEnterMode) and [`editor.activeShiftEnterMode`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-activeShiftEnterMode).\r
+      * Dynamic content filter instances &ndash; [`editor.setActiveFilter()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-setActiveFilter) method, [`editor.activeFilterChange`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-activeFilterChange) event, and [`editor.activeFilter`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-activeFilter) property.\r
+  * "Fake" selection was introduced. It makes it possible to virtually select any element when the real selection remains hidden. See the  [`selection.fake()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.selection-method-fake) method.\r
+  * Default [`htmlParser.filter`](http://docs.ckeditor.com/#!/api/CKEDITOR.htmlParser.filter) rules are not applied to non-editable elements (elements with `contenteditable` attribute set to `false` and their descendants) anymore. To add a rule which will be applied to all elements you need to pass an additional argument to the [`filter.addRules()`](http://docs.ckeditor.com/#!/api/CKEDITOR.htmlParser.filter-method-addRules) method.\r
+  * Dozens of new methods were introduced &ndash; most interesting ones:\r
+      * [`document.find()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.document-method-find),\r
+      * [`document.findOne()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.document-method-findOne),\r
+      * [`editable.insertElementIntoRange()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editable-method-insertElementIntoRange),\r
+      * [`range.moveToClosestEditablePosition()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.range-method-moveToClosestEditablePosition),\r
+      * New methods for [`htmlParser.node`](http://docs.ckeditor.com/#!/api/CKEDITOR.htmlParser.node) and [`htmlParser.element`](http://docs.ckeditor.com/#!/api/CKEDITOR.htmlParser.element).\r
+* [#10659](http://dev.ckeditor.com/ticket/10659): New [Enhanced Image](http://ckeditor.com/addon/image2) plugin that introduces a widget with integrated image captions, an option to center images, and dynamic "click and drag" resizing.\r
+* [#10664](http://dev.ckeditor.com/ticket/10664): New [Mathematical Formulas](http://ckeditor.com/addon/mathjax) plugin that introduces the MathJax widget.\r
+* [#7987](https://dev.ckeditor.com/ticket/7987): New [Language](http://ckeditor.com/addon/language) plugin that implements Language toolbar button to support [WCAG 3.1.2 Language of Parts](http://www.w3.org/TR/UNDERSTANDING-WCAG20/meaning-other-lang-id.html).\r
+* [#10708](http://dev.ckeditor.com/ticket/10708): New [smileys](http://ckeditor.com/addon/smiley).\r
+\r
+## CKEditor 4.2.3\r
+\r
+Fixed Issues:\r
+\r
+* [#10994](http://dev.ckeditor.com/ticket/10994): Fixed: Loading external jQuery library when opening the [jQuery Adapter](http://docs.ckeditor.com/#!/guide/dev_jquery) sample directly from file.\r
+* [#10975](http://dev.ckeditor.com/ticket/10975): [IE] Fixed: Error thrown while opening the color palette.\r
+* [#9929](http://dev.ckeditor.com/ticket/9929): [Blink/WebKit] Fixed: A non-breaking space is created once a character is deleted and a regular space is typed.\r
+* [#10963](http://dev.ckeditor.com/ticket/10963): Fixed: JAWS issue with the keyboard shortcut for [Magic Line](http://ckeditor.com/addon/magicline).\r
+* [#11096](http://dev.ckeditor.com/ticket/11096): Fixed: TypeError: Object has no method 'is'.\r
+\r
+## CKEditor 4.2.2\r
+\r
+Fixed Issues:\r
+\r
+* [#9314](http://dev.ckeditor.com/ticket/9314): Fixed: Incorrect error message on closing a dialog window without saving changs.\r
+* [#10308](http://dev.ckeditor.com/ticket/10308): [IE10] Fixed: Unspecified error when deleting a row.\r
+* [#10945](http://dev.ckeditor.com/ticket/10945): [Chrome] Fixed: Clicking with a mouse inside the editor does not show the caret.\r
+* [#10912](http://dev.ckeditor.com/ticket/10912): Prevent default action when content of a non-editable link is clicked.\r
+* [#10913](http://dev.ckeditor.com/ticket/10913): Fixed [`CKEDITOR.plugins.addExternal()`](http://docs.ckeditor.com/#!/api/CKEDITOR.resourceManager-method-addExternal) not handling paths including file name specified.\r
+* [#10666](http://dev.ckeditor.com/ticket/10666): Fixed [`CKEDITOR.tools.isArray()`](http://docs.ckeditor.com/#!/api/CKEDITOR.tools-method-isArray) not working cross frame.\r
+* [#10910](http://dev.ckeditor.com/ticket/10910): [IE9] Fixed JavaScript error thrown in Compatibility Mode when clicking and/or typing in the editing area.\r
+* [#10868](http://dev.ckeditor.com/ticket/10868): [IE8] Prevent the browser from crashing when applying the Inline Quotation style.\r
+* [#10915](http://dev.ckeditor.com/ticket/10915): Fixed: Invalid CSS filter in the Kama skin.\r
+* [#10914](http://dev.ckeditor.com/ticket/10914): Plugins [Indent List](http://ckeditor.com/addon/indentlist) and [Indent Block](http://ckeditor.com/addon/indentblock) are now included in the build configuration.\r
+* [#10812](http://dev.ckeditor.com/ticket/10812): Fixed [`range.createBookmark2()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dom.range-method-createBookmark2) incorrectly normalizing offsets. This bug was causing many issues: [#10850](http://dev.ckeditor.com/ticket/10850), [#10842](http://dev.ckeditor.com/ticket/10842).\r
+* [#10951](http://dev.ckeditor.com/ticket/10951): Reviewed and optimized focus handling on panels (combo, menu buttons, color buttons, and context menu) to enhance accessibility. Fixed [#10705](http://dev.ckeditor.com/ticket/10705), [#10706](http://dev.ckeditor.com/ticket/10706) and [#10707](http://dev.ckeditor.com/ticket/10707).\r
+* [#10704](http://dev.ckeditor.com/ticket/10704): Fixed a JAWS issue with the Select Color dialog window title not being announced.\r
+* [#10753](http://dev.ckeditor.com/ticket/10753): The floating toolbar in inline instances now has a dedicated accessibility label.\r
+\r
+## CKEditor 4.2.1\r
+\r
+Fixed Issues:\r
+\r
+* [#10301](http://dev.ckeditor.com/ticket/10301): [IE9-10] Undo fails after 3+ consecutive paste actions with a JavaScript error.\r
+* [#10689](http://dev.ckeditor.com/ticket/10689): Save toolbar button saves only the first editor instance.\r
+* [#10368](http://dev.ckeditor.com/ticket/10368): Move language reading direction definition (`dir`) from main language file to core.\r
+* [#9330](http://dev.ckeditor.com/ticket/9330): Fixed pasting anchors from MS Word.\r
+* [#8103](http://dev.ckeditor.com/ticket/8103): Fixed pasting nested lists from MS Word.\r
+* [#9958](http://dev.ckeditor.com/ticket/9958): [IE9] Pressing the "OK" button will trigger the `onbeforeunload` event in the popup dialog.\r
+* [#10662](http://dev.ckeditor.com/ticket/10662): Fixed styles from the Styles drop-down list not registering to the ACF in case when the [Shared Spaces plugin](http://ckeditor.com/addon/sharedspace) is used.\r
+* [#9654](http://dev.ckeditor.com/ticket/9654): Problems with Internet Explorer 10 Quirks Mode.\r
+* [#9816](http://dev.ckeditor.com/ticket/9816): Floating toolbar does not reposition vertically in several cases.\r
+* [#10646](http://dev.ckeditor.com/ticket/10646): Removing a selected sublist or nested table with *Backspace/Delete* removes the parent element.\r
+* [#10623](http://dev.ckeditor.com/ticket/10623): [WebKit] Page is scrolled when opening a drop-down list.\r
+* [#10004](http://dev.ckeditor.com/ticket/10004): [ChromeVox] Button names are not announced.\r
+* [#10731](http://dev.ckeditor.com/ticket/10731): [WebSpellChecker](http://ckeditor.com/addon/wsc) plugin breaks cloning of editor configuration.\r
+* It is now possible to set per instance [WebSpellChecker](http://ckeditor.com/addon/wsc) plugin configuration instead of setting the configuration globally.\r
+\r
+## CKEditor 4.2\r
+\r
+**Important Notes:**\r
+\r
+* Dropped compatibility support for Internet Explorer 7 and Firefox 3.6.\r
+\r
+* Both the Basic and the Standard distribution packages will not contain the new [Indent Block](http://ckeditor.com/addon/indentblock) plugin. Because of this the [Advanced Content Filter](http://docs.ckeditor.com/#!/guide/dev_advanced_content_filter) might remove block indentations from existing contents. If you want to prevent this, either [add an appropriate ACF rule to your filter](http://docs.ckeditor.com/#!/guide/dev_allowed_content_rules) or create a custom build based on the Basic/Standard package and add the Indent Block plugin in [CKBuilder](http://ckeditor.com/builder).\r
+\r
+New Features:\r
+\r
+* [#10027](http://dev.ckeditor.com/ticket/10027): Separated list and block indentation into two plugins: [Indent List](http://ckeditor.com/addon/indentlist) and [Indent Block](http://ckeditor.com/addon/indentblock).\r
+* [#8244](http://dev.ckeditor.com/ticket/8244): Use *(Shift+)Tab* to indent and outdent lists.\r
+* [#10281](http://dev.ckeditor.com/ticket/10281): The [jQuery Adapter](http://docs.ckeditor.com/#!/guide/dev_jquery) is now available. Several jQuery-related issues fixed: [#8261](http://dev.ckeditor.com/ticket/8261), [#9077](http://dev.ckeditor.com/ticket/9077), [#8710](http://dev.ckeditor.com/ticket/8710), [#8530](http://dev.ckeditor.com/ticket/8530), [#9019](http://dev.ckeditor.com/ticket/9019), [#6181](http://dev.ckeditor.com/ticket/6181), [#7876](http://dev.ckeditor.com/ticket/7876), [#6906](http://dev.ckeditor.com/ticket/6906).\r
+* [#10042](http://dev.ckeditor.com/ticket/10042): Introduced [`config.title`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-title) setting to change the human-readable title of the editor.\r
+* [#9794](http://dev.ckeditor.com/ticket/9794): Added [`editor.change`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-change) event.\r
+* [#9923](http://dev.ckeditor.com/ticket/9923): HiDPI support in the editor UI. HiDPI icons for [Moono skin](http://ckeditor.com/addon/moono) added.\r
+* [#8031](http://dev.ckeditor.com/ticket/8031): Handle `required` attributes on `<textarea>` elements &mdash; introduced [`editor.required`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-required) event.\r
+* [#10280](http://dev.ckeditor.com/ticket/10280): Ability to replace `<textarea>` elements with the inline editor.\r
+\r
+Fixed Issues:\r
+\r
+* [#10599](http://dev.ckeditor.com/ticket/10599): [Indent](http://ckeditor.com/addon/indent) plugin is no longer required by the [List](http://ckeditor.com/addon/list) plugin.\r
+* [#10370](http://dev.ckeditor.com/ticket/10370): Inconsistency in data events between framed and inline editors.\r
+* [#10438](http://dev.ckeditor.com/ticket/10438): [FF, IE] No selection is done on an editable element on executing [`editor.setData()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-setData).\r
+\r
+## CKEditor 4.1.3\r
+\r
+New Features:\r
+\r
+* Added new translation: Indonesian.\r
+\r
+Fixed Issues:\r
+\r
+* [#10644](http://dev.ckeditor.com/ticket/10644): Fixed a critical bug when pasting plain text in Blink-based browsers.\r
+* [#5189](http://dev.ckeditor.com/ticket/5189): [Find/Replace](http://ckeditor.com/addon/find) dialog window: rename "Cancel" button to "Close".\r
+* [#10562](http://dev.ckeditor.com/ticket/10562): [Housekeeping] Unified CSS gradient filter formats in the [Moono](http://ckeditor.com/addon/moono) skin.\r
+* [#10537](http://dev.ckeditor.com/ticket/10537): Advanced Content Filter should register a default rule for [`config.shiftEnterMode`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-shiftEnterMode).\r
+* [#10610](http://dev.ckeditor.com/ticket/10610): [`CKEDITOR.dialog.addIframe()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dialog-static-method-addIframe) incorrectly sets the iframe size in dialog windows.\r
+\r
+## CKEditor 4.1.2\r
+\r
+New Features:\r
+\r
+* Added new translation: Sinhala.\r
+\r
+Fixed Issues:\r
+\r
+* [#10339](http://dev.ckeditor.com/ticket/10339): Fixed: Error thrown when inserted data was totally stripped out after filtering and processing.\r
+* [#10298](http://dev.ckeditor.com/ticket/10298): Fixed: Data processor breaks attributes containing protected parts.\r
+* [#10367](http://dev.ckeditor.com/ticket/10367): Fixed: [`editable.insertText()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editable-method-insertText) loses characters when `RegExp` replace controls are being inserted.\r
+* [#10165](http://dev.ckeditor.com/ticket/10165): [IE] Access denied error when `document.domain` has been altered.\r
+* [#9761](http://dev.ckeditor.com/ticket/9761): Update the *Backspace* key state in [`keystrokeHandler.blockedKeystrokes`](http://docs.ckeditor.com/#!/api/CKEDITOR.keystrokeHandler-property-blockedKeystrokes) when calling [`editor.setReadOnly()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-setReadOnly).\r
+* [#6504](http://dev.ckeditor.com/ticket/6504): Fixed: Race condition while loading several [`config.customConfig`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-customConfig) files.\r
+* [#10146](http://dev.ckeditor.com/ticket/10146): [Firefox] Empty lines are being removed while [`config.enterMode`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-enterMode) is [`CKEDITOR.ENTER_BR`](http://docs.ckeditor.com/#!/api/CKEDITOR-property-ENTER_BR).\r
+* [#10360](http://dev.ckeditor.com/ticket/10360): Fixed: ARIA `role="application"` should not be used for dialog windows.\r
+* [#10361](http://dev.ckeditor.com/ticket/10361): Fixed: ARIA `role="application"` should not be used for floating panels.\r
+* [#10510](http://dev.ckeditor.com/ticket/10510): Introduced unique voice labels to differentiate between different editor instances.\r
+* [#9945](http://dev.ckeditor.com/ticket/9945): [iOS] Scrolling not possible on iPad.\r
+* [#10389](http://dev.ckeditor.com/ticket/10389): Fixed: Invalid HTML in the "Text and Table" template.\r
+* [WebSpellChecker](http://ckeditor.com/addon/wsc) plugin user interface was changed to match CKEditor 4 style.\r
+\r
+## CKEditor 4.1.1\r
+\r
+New Features:\r
+\r
+* Added new translation: Albanian.\r
+\r
+Fixed Issues:\r
+\r
+* [#10172](http://dev.ckeditor.com/ticket/10172): Pressing *Delete* or *Backspace* in an empty table cell moves the cursor to the next/previous cell.\r
+* [#10219](http://dev.ckeditor.com/ticket/10219): Error thrown when destroying an editor instance in parallel with a `mouseup` event.\r
+* [#10265](http://dev.ckeditor.com/ticket/10265): Wrong loop type in the [File Browser](http://ckeditor.com/addon/filebrowser) plugin.\r
+* [#10249](http://dev.ckeditor.com/ticket/10249): Wrong undo/redo states at start.\r
+* [#10268](http://dev.ckeditor.com/ticket/10268): [Show Blocks](http://ckeditor.com/addon/showblocks) does not recover after switching to Source view.\r
+* [#9995](http://dev.ckeditor.com/ticket/9995): HTML code in the `<textarea>` should not be modified by the [`htmlDataProcessor`](http://docs.ckeditor.com/#!/api/CKEDITOR.htmlDataProcessor).\r
+* [#10320](http://dev.ckeditor.com/ticket/10320): [Justify](http://ckeditor.com/addon/justify) plugin should add elements to Advanced Content Filter based on current [Enter mode](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-enterMode).\r
+* [#10260](http://dev.ckeditor.com/ticket/10260): Fixed: Advanced Content Filter blocks [`tabSpaces`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-tabSpaces). Unified `data-cke-*` attributes filtering.\r
+* [#10315](http://dev.ckeditor.com/ticket/10315): [WebKit] [Undo manager](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.undo.UndoManager) should not record snapshots after a filling character was added/removed.\r
+* [#10291](http://dev.ckeditor.com/ticket/10291): [WebKit] Space after a filling character should be secured.\r
+* [#10330](http://dev.ckeditor.com/ticket/10330): [WebKit] The filling character is not removed on `keydown` in specific cases.\r
+* [#10285](http://dev.ckeditor.com/ticket/10285): Fixed: Styled text pasted from MS Word causes an infinite loop.\r
+* [#10131](http://dev.ckeditor.com/ticket/10131): Fixed: [`undoManager.update()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.undo.UndoManager-method-update) does not refresh the command state.\r
+* [#10337](http://dev.ckeditor.com/ticket/10337): Fixed: Unable to remove `<s>` using [Remove Format](http://ckeditor.com/addon/removeformat).\r
+\r
+## CKEditor 4.1\r
+\r
+Fixed Issues:\r
+\r
+* [#10192](http://dev.ckeditor.com/ticket/10192): Closing lists with the *Enter* key does not work with [Advanced Content Filter](http://docs.ckeditor.com/#!/guide/dev_advanced_content_filter) in several cases.\r
+* [#10191](http://dev.ckeditor.com/ticket/10191): Fixed allowed content rules unification, so the [`filter.allowedContent`](http://docs.ckeditor.com/#!/api/CKEDITOR.filter-property-allowedContent) property always contains rules in the same format.\r
+* [#10224](http://dev.ckeditor.com/ticket/10224): Advanced Content Filter does not remove non-empty `<a>` elements anymore.\r
+* Minor issues in plugin integration with Advanced Content Filter:\r
+  * [#10166](http://dev.ckeditor.com/ticket/10166): Added transformation from the `align` attribute to `float` style to preserve backward compatibility after the introduction of Advanced Content Filter.\r
+  * [#10195](http://dev.ckeditor.com/ticket/10195): [Image](http://ckeditor.com/addon/image) plugin no longer registers rules for links to Advanced Content Filter.\r
+  * [#10213](http://dev.ckeditor.com/ticket/10213): [Justify](http://ckeditor.com/addon/justify) plugin is now correctly registering rules to Advanced Content Filter when [`config.justifyClasses`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-justifyClasses) is defined.\r
+\r
+## CKEditor 4.1 RC\r
+\r
+New Features:\r
+\r
+* [#9829](http://dev.ckeditor.com/ticket/9829): Advanced Content Filter - data and features activation based on editor configuration.\r
+\r
+  Brand new data filtering system that works in 2 modes:\r
+\r
+  * Based on loaded features (toolbar items, plugins) - the data will be filtered according to what the editor in its\r
+  current configuration can handle.\r
+  * Based on [`config.allowedContent`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-allowedContent) rules - the data\r
+  will be filtered and the editor features (toolbar items, commands, keystrokes) will be enabled if they are allowed.\r
+\r
+  See the `datafiltering.html` sample, [guides](http://docs.ckeditor.com/#!/guide/dev_advanced_content_filter) and [`CKEDITOR.filter` API documentation](http://docs.ckeditor.com/#!/api/CKEDITOR.filter).\r
+* [#9387](http://dev.ckeditor.com/ticket/9387): Reintroduced [Shared Spaces](http://ckeditor.com/addon/sharedspace) - the ability to display toolbar and bottom editor space in selected locations and to share them by different editor instances.\r
+* [#9907](http://dev.ckeditor.com/ticket/9907): Added the [`contentPreview`](http://docs.ckeditor.com/#!/api/CKEDITOR-event-contentPreview) event for preview data manipulation.\r
+* [#9713](http://dev.ckeditor.com/ticket/9713): Introduced the [Source Dialog](http://ckeditor.com/addon/sourcedialog) plugin that brings raw HTML editing for inline editor instances.\r
+* Included in [#9829](http://dev.ckeditor.com/ticket/9829): Introduced new events, [`toHtml`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-toHtml) and [`toDataFormat`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-toDataFormat), allowing for better integration with data processing.\r
+* [#9981](http://dev.ckeditor.com/ticket/9981): Added ability to filter [`htmlParser.fragment`](http://docs.ckeditor.com/#!/api/CKEDITOR.htmlParser.fragment), [`htmlParser.element`](http://docs.ckeditor.com/#!/api/CKEDITOR.htmlParser.element) etc. by many [`htmlParser.filter`](http://docs.ckeditor.com/#!/api/CKEDITOR.htmlParser.filter)s before writing structure to an HTML string.\r
+* Included in [#10103](http://dev.ckeditor.com/ticket/10103):\r
+  * Introduced the [`editor.status`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-status) property to make it easier to check the current status of the editor.\r
+  * Default [`command`](http://docs.ckeditor.com/#!/api/CKEDITOR.command) state is now [`CKEDITOR.TRISTATE_DISABLE`](http://docs.ckeditor.com/#!/api/CKEDITOR-property-TRISTATE_DISABLED). It will be activated on [`editor.instanceReady`](http://docs.ckeditor.com/#!/api/CKEDITOR-event-instanceReady) or immediately after being added if the editor is already initialized.\r
+* [#9796](http://dev.ckeditor.com/ticket/9796): Introduced `<s>` as a default tag for strikethrough, which replaces obsolete `<strike>` in HTML5.\r
+\r
+## CKEditor 4.0.3\r
+\r
+Fixed Issues:\r
+\r
+* [#10196](http://dev.ckeditor.com/ticket/10196): Fixed context menus not opening with keyboard shortcuts when [Autogrow](http://ckeditor.com/addon/autogrow) is enabled.\r
+* [#10212](http://dev.ckeditor.com/ticket/10212): [IE7-10] Undo command throws errors after multiple switches between Source and WYSIWYG view.\r
+* [#10219](http://dev.ckeditor.com/ticket/10219): [Inline editor] Error thrown after calling [`editor.destroy()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-destroy).\r
+\r
+## CKEditor 4.0.2\r
+\r
+Fixed Issues:\r
+\r
+* [#9779](http://dev.ckeditor.com/ticket/9779): Fixed overriding [`CKEDITOR.getUrl()`](http://docs.ckeditor.com/#!/api/CKEDITOR-method-getUrl) with `CKEDITOR_GETURL`.\r
+* [#9772](http://dev.ckeditor.com/ticket/9772): Custom buttons in the dialog window footer have different look and size ([Moono](http://ckeditor.com/addon/moono), [Kama](http://ckeditor.com/addon/kama) skins).\r
+* [#9029](http://dev.ckeditor.com/ticket/9029): Custom styles added with the [`stylesSet.add()`](http://docs.ckeditor.com/#!/api/CKEDITOR.stylesSet-method-add) are displayed in the wrong order.\r
+* [#9887](http://dev.ckeditor.com/ticket/9887): Disable [Magic Line](http://ckeditor.com/addon/magicline) when [`editor.readOnly`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-property-readOnly) is set.\r
+* [#9882](http://dev.ckeditor.com/ticket/9882): Fixed empty document title on [`editor.getData()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-getData) if set via the Document Properties dialog window.\r
+* [#9773](http://dev.ckeditor.com/ticket/9773): Fixed rendering problems with selection fields in the Kama skin.\r
+* [#9851](http://dev.ckeditor.com/ticket/9851): The [`selectionChange`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-event-selectionChange) event is not fired when mouse selection ended outside editable.\r
+* [#9903](http://dev.ckeditor.com/ticket/9903): [Inline editor] Bad positioning of floating space with page horizontal scroll.\r
+* [#9872](http://dev.ckeditor.com/ticket/9872): [`editor.checkDirty()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-checkDirty) returns `true` when called onload. Removed the obsolete `editor.mayBeDirty` flag.\r
+* [#9893](http://dev.ckeditor.com/ticket/9893): [IE] Fixed broken toolbar when editing mixed direction content in Quirks mode.\r
+* [#9845](http://dev.ckeditor.com/ticket/9845): Fixed TAB navigation in the [Link](http://ckeditor.com/addon/link) dialog window when the Anchor option is used and no anchors are available.\r
+* [#9883](http://dev.ckeditor.com/ticket/9883): Maximizing was making the entire page editable with [divarea](http://ckeditor.com/addon/divarea)-based editors.\r
+* [#9940](http://dev.ckeditor.com/ticket/9940): [Firefox] Navigating back to a page with the editor was making the entire page editable.\r
+* [#9966](http://dev.ckeditor.com/ticket/9966): Fixed: Unable to type square brackets with French keyboard layout. Changed [Magic Line](http://ckeditor.com/addon/magicline) keystrokes.\r
+* [#9507](http://dev.ckeditor.com/ticket/9507): [Firefox] Selection is moved before editable position when the editor is focused for the first time.\r
+* [#9947](http://dev.ckeditor.com/ticket/9947): [WebKit] Editor overflows parent container in some edge cases.\r
+* [#10105](http://dev.ckeditor.com/ticket/10105): Fixed: Broken [sourcearea](http://ckeditor.com/addon/sourcearea) view when an RTL language is set.\r
+* [#10123](http://dev.ckeditor.com/ticket/10123): [WebKit] Fixed: Several dialog windows have broken layout since the latest WebKit release.\r
+* [#10152](http://dev.ckeditor.com/ticket/10152): Fixed: Invalid ARIA property used on menu items.\r
+\r
+## CKEditor 4.0.1.1\r
+\r
+Fixed Issues:\r
+\r
+* Security update: Added protection against XSS attack and possible path disclosure in the PHP sample.\r
+\r
+## CKEditor 4.0.1\r
+\r
+Fixed Issues:\r
+\r
+* [#9655](http://dev.ckeditor.com/ticket/9655): Support for IE Quirks Mode in the new [Moono skin](http://ckeditor.com/addon/moono).\r
+* Accessibility issues (mainly in inline editor): [#9364](http://dev.ckeditor.com/ticket/9364), [#9368](http://dev.ckeditor.com/ticket/9368), [#9369](http://dev.ckeditor.com/ticket/9369), [#9370](http://dev.ckeditor.com/ticket/9370), [#9541](http://dev.ckeditor.com/ticket/9541), [#9543](http://dev.ckeditor.com/ticket/9543), [#9841](http://dev.ckeditor.com/ticket/9841), [#9844](http://dev.ckeditor.com/ticket/9844).\r
+* [Magic Line](http://ckeditor.com/addon/magicline) plugin:\r
+    * [#9481](http://dev.ckeditor.com/ticket/9481): Added accessibility support for Magic Line.\r
+    * [#9509](http://dev.ckeditor.com/ticket/9509): Added Magic Line support for forms.\r
+    * [#9573](http://dev.ckeditor.com/ticket/9573): Magic Line does not disappear on `mouseout` in a specific case.\r
+* [#9754](http://dev.ckeditor.com/ticket/9754): [WebKit] Cutting & pasting simple unformatted text generates an inline wrapper in WebKit browsers.\r
+* [#9456](http://dev.ckeditor.com/ticket/9456): [Chrome] Properly paste bullet list style from MS Word.\r
+* [#9699](http://dev.ckeditor.com/ticket/9699), [#9758](http://dev.ckeditor.com/ticket/9758): Improved selection locking when selecting by dragging.\r
+* Context menu:\r
+    * [#9712](http://dev.ckeditor.com/ticket/9712): Opening the context menu destroys editor focus.\r
+    * [#9366](http://dev.ckeditor.com/ticket/9366): Context menu should be displayed over the floating toolbar.\r
+    * [#9706](http://dev.ckeditor.com/ticket/9706): Context menu generates a JavaScript error in inline mode when the editor is attached to a header element.\r
+* [#9800](http://dev.ckeditor.com/ticket/9800): Hide float panel when resizing the window.\r
+* [#9721](http://dev.ckeditor.com/ticket/9721): Padding in content of div-based editor puts the editing area under the bottom UI space.\r
+* [#9528](http://dev.ckeditor.com/ticket/9528): Host page `box-sizing` style should not influence the editor UI elements.\r
+* [#9503](http://dev.ckeditor.com/ticket/9503): [Form Elements](http://ckeditor.com/addon/forms) plugin adds context menu listeners only on supported input types. Added support for `tel`, `email`, `search` and `url` input types.\r
+* [#9769](http://dev.ckeditor.com/ticket/9769): Improved floating toolbar positioning in a narrow window.\r
+* [#9875](http://dev.ckeditor.com/ticket/9875): Table dialog window does not populate width correctly.\r
+* [#8675](http://dev.ckeditor.com/ticket/8675): Deleting cells in a nested table removes the outer table cell.\r
+* [#9815](http://dev.ckeditor.com/ticket/9815): Cannot edit dialog window fields in an editor initialized in the jQuery UI modal dialog.\r
+* [#8888](http://dev.ckeditor.com/ticket/8888): CKEditor dialog windows do not show completely in a small window.\r
+* [#9360](http://dev.ckeditor.com/ticket/9360): [Inline editor] Blocks shown for a `<div>` element stay permanently even after the user exits editing the `<div>`.\r
+* [#9531](http://dev.ckeditor.com/ticket/9531): [Firefox & Inline editor] Toolbar is lost when closing the Format drop-down list by clicking its button.\r
+* [#9553](http://dev.ckeditor.com/ticket/9553): Table width incorrectly set when the `border-width` style is specified.\r
+* [#9594](http://dev.ckeditor.com/ticket/9594): Cannot tab past CKEditor when it is in read-only mode.\r
+* [#9658](http://dev.ckeditor.com/ticket/9658): [IE9] Justify not working on selected images.\r
+* [#9686](http://dev.ckeditor.com/ticket/9686): Added missing contents styles for `<pre>` elements.\r
+* [#9709](http://dev.ckeditor.com/ticket/9709): [Paste from Word](http://ckeditor.com/addon/pastefromword) should not depend on configuration from other styles.\r
+* [#9726](http://dev.ckeditor.com/ticket/9726): Removed [Color Dialog](http://ckeditor.com/addon/colordialog) plugin dependency from [Table Tools](http://ckeditor.com/addon/tabletools).\r
+* [#9765](http://dev.ckeditor.com/ticket/9765): Toolbar Collapse command documented incorrectly in the [Accessibility Instructions](http://ckeditor.com/addon/a11yhelp) dialog window.\r
+* [#9771](http://dev.ckeditor.com/ticket/9771): [WebKit & Opera] Fixed scrolling issues when pasting.\r
+* [#9787](http://dev.ckeditor.com/ticket/9787): [IE9] `onChange` is not fired for checkboxes in dialogs.\r
+* [#9842](http://dev.ckeditor.com/ticket/9842): [Firefox 17] When opening a toolbar menu for the first time and pressing the *Down Arrow* key, focus goes to the next toolbar button instead of the menu options.\r
+* [#9847](http://dev.ckeditor.com/ticket/9847): [Elements Path](http://ckeditor.com/addon/elementspath) should not be initialized in the inline editor.\r
+* [#9853](http://dev.ckeditor.com/ticket/9853): [`editor.addRemoveFormatFilter()`](http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-addRemoveFormatFilter) is exposed before it really works.\r
+* [#8893](http://dev.ckeditor.com/ticket/8893): Value of the [`pasteFromWordCleanupFile`](http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-pasteFromWordCleanupFile) configuration option is now taken from the instance configuration.\r
+* [#9693](http://dev.ckeditor.com/ticket/9693): Removed "Live Preview" checkbox from UI color picker.\r
+\r
+\r
+## CKEditor 4.0\r
+\r
+The first stable release of the new CKEditor 4 code line.\r
+\r
+The CKEditor JavaScript API has been kept compatible with CKEditor 4, whenever\r
+possible. The list of relevant changes can be found in the [API Changes page of\r
+the CKEditor 4 documentation][1].\r
+\r
+[1]: http://docs.ckeditor.com/#!/guide/dev_api_changes "API Changes"\r
diff --git a/js/ckeditor/LICENSE.md b/js/ckeditor/LICENSE.md
new file mode 100644 (file)
index 0000000..044e7da
--- /dev/null
@@ -0,0 +1,1420 @@
+Software License Agreement\r
+==========================\r
+\r
+CKEditor - The text editor for Internet - http://ckeditor.com\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+\r
+Licensed under the terms of any of the following licenses at your\r
+choice:\r
+\r
+ - GNU General Public License Version 2 or later (the "GPL")\r
+   http://www.gnu.org/licenses/gpl.html\r
+   (See Appendix A)\r
+\r
+ - GNU Lesser General Public License Version 2.1 or later (the "LGPL")\r
+   http://www.gnu.org/licenses/lgpl.html\r
+   (See Appendix B)\r
+\r
+ - Mozilla Public License Version 1.1 or later (the "MPL")\r
+   http://www.mozilla.org/MPL/MPL-1.1.html\r
+   (See Appendix C)\r
+\r
+You are not required to, but if you want to explicitly declare the\r
+license you have chosen to be bound to when using, reproducing,\r
+modifying and distributing this software, just include a text file\r
+titled "legal.txt" in your version of this software, indicating your\r
+license choice. In any case, your choice will not restrict any\r
+recipient of your version of this software to use, reproduce, modify\r
+and distribute this software under any of the above licenses.\r
+\r
+Sources of Intellectual Property Included in CKEditor\r
+-----------------------------------------------------\r
+\r
+Where not otherwise indicated, all CKEditor content is authored by\r
+CKSource engineers and consists of CKSource-owned intellectual\r
+property. In some specific instances, CKEditor will incorporate work\r
+done by developers outside of CKSource with their express permission.\r
+\r
+The following libraries are included in CKEditor under the MIT license (see Appendix D):\r
+\r
+* CKSource Samples Framework (included in the samples) - Copyright (c) 2014-2017, CKSource - Frederico Knabben.\r
+* PicoModal (included in `samples/js/sf.js`) - Copyright (c) 2012 James Frasca.\r
+* CodeMirror (included in the samples) - Copyright (C) 2014 by Marijn Haverbeke <marijnh@gmail.com> and others.\r
+\r
+Parts of code taken from the following libraries are included in CKEditor under the MIT license (see Appendix D):\r
+\r
+* jQuery (inspired the domReady function, ckeditor_base.js) - Copyright (c) 2011 John Resig, http://jquery.com/\r
+\r
+The following libraries are included in CKEditor under the SIL Open Font License, Version 1.1 (see Appendix E):\r
+\r
+* Font Awesome (included in the toolbar configurator) - Copyright (C) 2012 by Dave Gandy.\r
+\r
+The following libraries are included in CKEditor under the BSD-3 License (see Appendix F):\r
+\r
+* highlight.js (included in the `codesnippet` plugin) - Copyright (c) 2006, Ivan Sagalaev.\r
+* YUI Library (included in the `uicolor` plugin) - Copyright (c) 2009, Yahoo! Inc.\r
+\r
+\r
+Trademarks\r
+----------\r
+\r
+CKEditor is a trademark of CKSource - Frederico Knabben. All other brand\r
+and product names are trademarks, registered trademarks or service\r
+marks of their respective holders.\r
+\r
+---\r
+\r
+Appendix A: The GPL License\r
+---------------------------\r
+\r
+```\r
+GNU GENERAL PUBLIC LICENSE\r
+Version 2, June 1991\r
+\r
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,\r
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\r
+ Everyone is permitted to copy and distribute verbatim copies\r
+ of this license document, but changing it is not allowed.\r
+\r
+Preamble\r
+\r
+  The licenses for most software are designed to take away your\r
+freedom to share and change it.  By contrast, the GNU General Public\r
+License is intended to guarantee your freedom to share and change free\r
+software-to make sure the software is free for all its users.  This\r
+General Public License applies to most of the Free Software\r
+Foundation's software and to any other program whose authors commit to\r
+using it.  (Some other Free Software Foundation software is covered by\r
+the GNU Lesser General Public License instead.)  You can apply it to\r
+your programs, too.\r
+\r
+  When we speak of free software, we are referring to freedom, not\r
+price.  Our General Public Licenses are designed to make sure that you\r
+have the freedom to distribute copies of free software (and charge for\r
+this service if you wish), that you receive source code or can get it\r
+if you want it, that you can change the software or use pieces of it\r
+in new free programs; and that you know you can do these things.\r
+\r
+  To protect your rights, we need to make restrictions that forbid\r
+anyone to deny you these rights or to ask you to surrender the rights.\r
+These restrictions translate to certain responsibilities for you if you\r
+distribute copies of the software, or if you modify it.\r
+\r
+  For example, if you distribute copies of such a program, whether\r
+gratis or for a fee, you must give the recipients all the rights that\r
+you have.  You must make sure that they, too, receive or can get the\r
+source code.  And you must show them these terms so they know their\r
+rights.\r
+\r
+  We protect your rights with two steps: (1) copyright the software, and\r
+(2) offer you this license which gives you legal permission to copy,\r
+distribute and/or modify the software.\r
+\r
+  Also, for each author's protection and ours, we want to make certain\r
+that everyone understands that there is no warranty for this free\r
+software.  If the software is modified by someone else and passed on, we\r
+want its recipients to know that what they have is not the original, so\r
+that any problems introduced by others will not reflect on the original\r
+authors' reputations.\r
+\r
+  Finally, any free program is threatened constantly by software\r
+patents.  We wish to avoid the danger that redistributors of a free\r
+program will individually obtain patent licenses, in effect making the\r
+program proprietary.  To prevent this, we have made it clear that any\r
+patent must be licensed for everyone's free use or not licensed at all.\r
+\r
+  The precise terms and conditions for copying, distribution and\r
+modification follow.\r
+\r
+GNU GENERAL PUBLIC LICENSE\r
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\r
+\r
+  0. This License applies to any program or other work which contains\r
+a notice placed by the copyright holder saying it may be distributed\r
+under the terms of this General Public License.  The "Program", below,\r
+refers to any such program or work, and a "work based on the Program"\r
+means either the Program or any derivative work under copyright law:\r
+that is to say, a work containing the Program or a portion of it,\r
+either verbatim or with modifications and/or translated into another\r
+language.  (Hereinafter, translation is included without limitation in\r
+the term "modification".)  Each licensee is addressed as "you".\r
+\r
+Activities other than copying, distribution and modification are not\r
+covered by this License; they are outside its scope.  The act of\r
+running the Program is not restricted, and the output from the Program\r
+is covered only if its contents constitute a work based on the\r
+Program (independent of having been made by running the Program).\r
+Whether that is true depends on what the Program does.\r
+\r
+  1. You may copy and distribute verbatim copies of the Program's\r
+source code as you receive it, in any medium, provided that you\r
+conspicuously and appropriately publish on each copy an appropriate\r
+copyright notice and disclaimer of warranty; keep intact all the\r
+notices that refer to this License and to the absence of any warranty;\r
+and give any other recipients of the Program a copy of this License\r
+along with the Program.\r
+\r
+You may charge a fee for the physical act of transferring a copy, and\r
+you may at your option offer warranty protection in exchange for a fee.\r
+\r
+  2. You may modify your copy or copies of the Program or any portion\r
+of it, thus forming a work based on the Program, and copy and\r
+distribute such modifications or work under the terms of Section 1\r
+above, provided that you also meet all of these conditions:\r
+\r
+    a) You must cause the modified files to carry prominent notices\r
+    stating that you changed the files and the date of any change.\r
+\r
+    b) You must cause any work that you distribute or publish, that in\r
+    whole or in part contains or is derived from the Program or any\r
+    part thereof, to be licensed as a whole at no charge to all third\r
+    parties under the terms of this License.\r
+\r
+    c) If the modified program normally reads commands interactively\r
+    when run, you must cause it, when started running for such\r
+    interactive use in the most ordinary way, to print or display an\r
+    announcement including an appropriate copyright notice and a\r
+    notice that there is no warranty (or else, saying that you provide\r
+    a warranty) and that users may redistribute the program under\r
+    these conditions, and telling the user how to view a copy of this\r
+    License.  (Exception: if the Program itself is interactive but\r
+    does not normally print such an announcement, your work based on\r
+    the Program is not required to print an announcement.)\r
+\r
+These requirements apply to the modified work as a whole.  If\r
+identifiable sections of that work are not derived from the Program,\r
+and can be reasonably considered independent and separate works in\r
+themselves, then this License, and its terms, do not apply to those\r
+sections when you distribute them as separate works.  But when you\r
+distribute the same sections as part of a whole which is a work based\r
+on the Program, the distribution of the whole must be on the terms of\r
+this License, whose permissions for other licensees extend to the\r
+entire whole, and thus to each and every part regardless of who wrote it.\r
+\r
+Thus, it is not the intent of this section to claim rights or contest\r
+your rights to work written entirely by you; rather, the intent is to\r
+exercise the right to control the distribution of derivative or\r
+collective works based on the Program.\r
+\r
+In addition, mere aggregation of another work not based on the Program\r
+with the Program (or with a work based on the Program) on a volume of\r
+a storage or distribution medium does not bring the other work under\r
+the scope of this License.\r
+\r
+  3. You may copy and distribute the Program (or a work based on it,\r
+under Section 2) in object code or executable form under the terms of\r
+Sections 1 and 2 above provided that you also do one of the following:\r
+\r
+    a) Accompany it with the complete corresponding machine-readable\r
+    source code, which must be distributed under the terms of Sections\r
+    1 and 2 above on a medium customarily used for software interchange; or,\r
+\r
+    b) Accompany it with a written offer, valid for at least three\r
+    years, to give any third party, for a charge no more than your\r
+    cost of physically performing source distribution, a complete\r
+    machine-readable copy of the corresponding source code, to be\r
+    distributed under the terms of Sections 1 and 2 above on a medium\r
+    customarily used for software interchange; or,\r
+\r
+    c) Accompany it with the information you received as to the offer\r
+    to distribute corresponding source code.  (This alternative is\r
+    allowed only for noncommercial distribution and only if you\r
+    received the program in object code or executable form with such\r
+    an offer, in accord with Subsection b above.)\r
+\r
+The source code for a work means the preferred form of the work for\r
+making modifications to it.  For an executable work, complete source\r
+code means all the source code for all modules it contains, plus any\r
+associated interface definition files, plus the scripts used to\r
+control compilation and installation of the executable.  However, as a\r
+special exception, the source code distributed need not include\r
+anything that is normally distributed (in either source or binary\r
+form) with the major components (compiler, kernel, and so on) of the\r
+operating system on which the executable runs, unless that component\r
+itself accompanies the executable.\r
+\r
+If distribution of executable or object code is made by offering\r
+access to copy from a designated place, then offering equivalent\r
+access to copy the source code from the same place counts as\r
+distribution of the source code, even though third parties are not\r
+compelled to copy the source along with the object code.\r
+\r
+  4. You may not copy, modify, sublicense, or distribute the Program\r
+except as expressly provided under this License.  Any attempt\r
+otherwise to copy, modify, sublicense or distribute the Program is\r
+void, and will automatically terminate your rights under this License.\r
+However, parties who have received copies, or rights, from you under\r
+this License will not have their licenses terminated so long as such\r
+parties remain in full compliance.\r
+\r
+  5. You are not required to accept this License, since you have not\r
+signed it.  However, nothing else grants you permission to modify or\r
+distribute the Program or its derivative works.  These actions are\r
+prohibited by law if you do not accept this License.  Therefore, by\r
+modifying or distributing the Program (or any work based on the\r
+Program), you indicate your acceptance of this License to do so, and\r
+all its terms and conditions for copying, distributing or modifying\r
+the Program or works based on it.\r
+\r
+  6. Each time you redistribute the Program (or any work based on the\r
+Program), the recipient automatically receives a license from the\r
+original licensor to copy, distribute or modify the Program subject to\r
+these terms and conditions.  You may not impose any further\r
+restrictions on the recipients' exercise of the rights granted herein.\r
+You are not responsible for enforcing compliance by third parties to\r
+this License.\r
+\r
+  7. If, as a consequence of a court judgment or allegation of patent\r
+infringement or for any other reason (not limited to patent issues),\r
+conditions are imposed on you (whether by court order, agreement or\r
+otherwise) that contradict the conditions of this License, they do not\r
+excuse you from the conditions of this License.  If you cannot\r
+distribute so as to satisfy simultaneously your obligations under this\r
+License and any other pertinent obligations, then as a consequence you\r
+may not distribute the Program at all.  For example, if a patent\r
+license would not permit royalty-free redistribution of the Program by\r
+all those who receive copies directly or indirectly through you, then\r
+the only way you could satisfy both it and this License would be to\r
+refrain entirely from distribution of the Program.\r
+\r
+If any portion of this section is held invalid or unenforceable under\r
+any particular circumstance, the balance of the section is intended to\r
+apply and the section as a whole is intended to apply in other\r
+circumstances.\r
+\r
+It is not the purpose of this section to induce you to infringe any\r
+patents or other property right claims or to contest validity of any\r
+such claims; this section has the sole purpose of protecting the\r
+integrity of the free software distribution system, which is\r
+implemented by public license practices.  Many people have made\r
+generous contributions to the wide range of software distributed\r
+through that system in reliance on consistent application of that\r
+system; it is up to the author/donor to decide if he or she is willing\r
+to distribute software through any other system and a licensee cannot\r
+impose that choice.\r
+\r
+This section is intended to make thoroughly clear what is believed to\r
+be a consequence of the rest of this License.\r
+\r
+  8. If the distribution and/or use of the Program is restricted in\r
+certain countries either by patents or by copyrighted interfaces, the\r
+original copyright holder who places the Program under this License\r
+may add an explicit geographical distribution limitation excluding\r
+those countries, so that distribution is permitted only in or among\r
+countries not thus excluded.  In such case, this License incorporates\r
+the limitation as if written in the body of this License.\r
+\r
+  9. The Free Software Foundation may publish revised and/or new versions\r
+of the General Public License from time to time.  Such new versions will\r
+be similar in spirit to the present version, but may differ in detail to\r
+address new problems or concerns.\r
+\r
+Each version is given a distinguishing version number.  If the Program\r
+specifies a version number of this License which applies to it and "any\r
+later version", you have the option of following the terms and conditions\r
+either of that version or of any later version published by the Free\r
+Software Foundation.  If the Program does not specify a version number of\r
+this License, you may choose any version ever published by the Free Software\r
+Foundation.\r
+\r
+  10. If you wish to incorporate parts of the Program into other free\r
+programs whose distribution conditions are different, write to the author\r
+to ask for permission.  For software which is copyrighted by the Free\r
+Software Foundation, write to the Free Software Foundation; we sometimes\r
+make exceptions for this.  Our decision will be guided by the two goals\r
+of preserving the free status of all derivatives of our free software and\r
+of promoting the sharing and reuse of software generally.\r
+\r
+NO WARRANTY\r
+\r
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY\r
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN\r
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES\r
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED\r
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\r
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS\r
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE\r
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,\r
+REPAIR OR CORRECTION.\r
+\r
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\r
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR\r
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,\r
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING\r
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED\r
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY\r
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\r
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\r
+POSSIBILITY OF SUCH DAMAGES.\r
+\r
+END OF TERMS AND CONDITIONS\r
+```\r
+\r
+Appendix B: The LGPL License\r
+----------------------------\r
+\r
+```\r
+GNU LESSER GENERAL PUBLIC LICENSE\r
+Version 2.1, February 1999\r
+\r
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.\r
+     59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r
+ Everyone is permitted to copy and distribute verbatim copies\r
+ of this license document, but changing it is not allowed.\r
+\r
+[This is the first released version of the Lesser GPL.  It also counts\r
+ as the successor of the GNU Library Public License, version 2, hence\r
+ the version number 2.1.]\r
+\r
+Preamble\r
+\r
+  The licenses for most software are designed to take away your\r
+freedom to share and change it.  By contrast, the GNU General Public\r
+Licenses are intended to guarantee your freedom to share and change\r
+free software-to make sure the software is free for all its users.\r
+\r
+  This license, the Lesser General Public License, applies to some\r
+specially designated software packages-typically libraries-of the\r
+Free Software Foundation and other authors who decide to use it.  You\r
+can use it too, but we suggest you first think carefully about whether\r
+this license or the ordinary General Public License is the better\r
+strategy to use in any particular case, based on the explanations below.\r
+\r
+  When we speak of free software, we are referring to freedom of use,\r
+not price.  Our General Public Licenses are designed to make sure that\r
+you have the freedom to distribute copies of free software (and charge\r
+for this service if you wish); that you receive source code or can get\r
+it if you want it; that you can change the software and use pieces of\r
+it in new free programs; and that you are informed that you can do\r
+these things.\r
+\r
+  To protect your rights, we need to make restrictions that forbid\r
+distributors to deny you these rights or to ask you to surrender these\r
+rights.  These restrictions translate to certain responsibilities for\r
+you if you distribute copies of the library or if you modify it.\r
+\r
+  For example, if you distribute copies of the library, whether gratis\r
+or for a fee, you must give the recipients all the rights that we gave\r
+you.  You must make sure that they, too, receive or can get the source\r
+code.  If you link other code with the library, you must provide\r
+complete object files to the recipients, so that they can relink them\r
+with the library after making changes to the library and recompiling\r
+it.  And you must show them these terms so they know their rights.\r
+\r
+  We protect your rights with a two-step method: (1) we copyright the\r
+library, and (2) we offer you this license, which gives you legal\r
+permission to copy, distribute and/or modify the library.\r
+\r
+  To protect each distributor, we want to make it very clear that\r
+there is no warranty for the free library.  Also, if the library is\r
+modified by someone else and passed on, the recipients should know\r
+that what they have is not the original version, so that the original\r
+author's reputation will not be affected by problems that might be\r
+introduced by others.\r
+\r
+  Finally, software patents pose a constant threat to the existence of\r
+any free program.  We wish to make sure that a company cannot\r
+effectively restrict the users of a free program by obtaining a\r
+restrictive license from a patent holder.  Therefore, we insist that\r
+any patent license obtained for a version of the library must be\r
+consistent with the full freedom of use specified in this license.\r
+\r
+  Most GNU software, including some libraries, is covered by the\r
+ordinary GNU General Public License.  This license, the GNU Lesser\r
+General Public License, applies to certain designated libraries, and\r
+is quite different from the ordinary General Public License.  We use\r
+this license for certain libraries in order to permit linking those\r
+libraries into non-free programs.\r
+\r
+  When a program is linked with a library, whether statically or using\r
+a shared library, the combination of the two is legally speaking a\r
+combined work, a derivative of the original library.  The ordinary\r
+General Public License therefore permits such linking only if the\r
+entire combination fits its criteria of freedom.  The Lesser General\r
+Public License permits more lax criteria for linking other code with\r
+the library.\r
+\r
+  We call this license the "Lesser" General Public License because it\r
+does Less to protect the user's freedom than the ordinary General\r
+Public License.  It also provides other free software developers Less\r
+of an advantage over competing non-free programs.  These disadvantages\r
+are the reason we use the ordinary General Public License for many\r
+libraries.  However, the Lesser license provides advantages in certain\r
+special circumstances.\r
+\r
+  For example, on rare occasions, there may be a special need to\r
+encourage the widest possible use of a certain library, so that it becomes\r
+a de-facto standard.  To achieve this, non-free programs must be\r
+allowed to use the library.  A more frequent case is that a free\r
+library does the same job as widely used non-free libraries.  In this\r
+case, there is little to gain by limiting the free library to free\r
+software only, so we use the Lesser General Public License.\r
+\r
+  In other cases, permission to use a particular library in non-free\r
+programs enables a greater number of people to use a large body of\r
+free software.  For example, permission to use the GNU C Library in\r
+non-free programs enables many more people to use the whole GNU\r
+operating system, as well as its variant, the GNU/Linux operating\r
+system.\r
+\r
+  Although the Lesser General Public License is Less protective of the\r
+users' freedom, it does ensure that the user of a program that is\r
+linked with the Library has the freedom and the wherewithal to run\r
+that program using a modified version of the Library.\r
+\r
+  The precise terms and conditions for copying, distribution and\r
+modification follow.  Pay close attention to the difference between a\r
+"work based on the library" and a "work that uses the library".  The\r
+former contains code derived from the library, whereas the latter must\r
+be combined with the library in order to run.\r
+\r
+GNU LESSER GENERAL PUBLIC LICENSE\r
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\r
+\r
+  0. This License Agreement applies to any software library or other\r
+program which contains a notice placed by the copyright holder or\r
+other authorized party saying it may be distributed under the terms of\r
+this Lesser General Public License (also called "this License").\r
+Each licensee is addressed as "you".\r
+\r
+  A "library" means a collection of software functions and/or data\r
+prepared so as to be conveniently linked with application programs\r
+(which use some of those functions and data) to form executables.\r
+\r
+  The "Library", below, refers to any such software library or work\r
+which has been distributed under these terms.  A "work based on the\r
+Library" means either the Library or any derivative work under\r
+copyright law: that is to say, a work containing the Library or a\r
+portion of it, either verbatim or with modifications and/or translated\r
+straightforwardly into another language.  (Hereinafter, translation is\r
+included without limitation in the term "modification".)\r
+\r
+  "Source code" for a work means the preferred form of the work for\r
+making modifications to it.  For a library, complete source code means\r
+all the source code for all modules it contains, plus any associated\r
+interface definition files, plus the scripts used to control compilation\r
+and installation of the library.\r
+\r
+  Activities other than copying, distribution and modification are not\r
+covered by this License; they are outside its scope.  The act of\r
+running a program using the Library is not restricted, and output from\r
+such a program is covered only if its contents constitute a work based\r
+on the Library (independent of the use of the Library in a tool for\r
+writing it).  Whether that is true depends on what the Library does\r
+and what the program that uses the Library does.\r
+\r
+  1. You may copy and distribute verbatim copies of the Library's\r
+complete source code as you receive it, in any medium, provided that\r
+you conspicuously and appropriately publish on each copy an\r
+appropriate copyright notice and disclaimer of warranty; keep intact\r
+all the notices that refer to this License and to the absence of any\r
+warranty; and distribute a copy of this License along with the\r
+Library.\r
+\r
+  You may charge a fee for the physical act of transferring a copy,\r
+and you may at your option offer warranty protection in exchange for a\r
+fee.\r
+\r
+  2. You may modify your copy or copies of the Library or any portion\r
+of it, thus forming a work based on the Library, and copy and\r
+distribute such modifications or work under the terms of Section 1\r
+above, provided that you also meet all of these conditions:\r
+\r
+    a) The modified work must itself be a software library.\r
+\r
+    b) You must cause the files modified to carry prominent notices\r
+    stating that you changed the files and the date of any change.\r
+\r
+    c) You must cause the whole of the work to be licensed at no\r
+    charge to all third parties under the terms of this License.\r
+\r
+    d) If a facility in the modified Library refers to a function or a\r
+    table of data to be supplied by an application program that uses\r
+    the facility, other than as an argument passed when the facility\r
+    is invoked, then you must make a good faith effort to ensure that,\r
+    in the event an application does not supply such function or\r
+    table, the facility still operates, and performs whatever part of\r
+    its purpose remains meaningful.\r
+\r
+    (For example, a function in a library to compute square roots has\r
+    a purpose that is entirely well-defined independent of the\r
+    application.  Therefore, Subsection 2d requires that any\r
+    application-supplied function or table used by this function must\r
+    be optional: if the application does not supply it, the square\r
+    root function must still compute square roots.)\r
+\r
+These requirements apply to the modified work as a whole.  If\r
+identifiable sections of that work are not derived from the Library,\r
+and can be reasonably considered independent and separate works in\r
+themselves, then this License, and its terms, do not apply to those\r
+sections when you distribute them as separate works.  But when you\r
+distribute the same sections as part of a whole which is a work based\r
+on the Library, the distribution of the whole must be on the terms of\r
+this License, whose permissions for other licensees extend to the\r
+entire whole, and thus to each and every part regardless of who wrote\r
+it.\r
+\r
+Thus, it is not the intent of this section to claim rights or contest\r
+your rights to work written entirely by you; rather, the intent is to\r
+exercise the right to control the distribution of derivative or\r
+collective works based on the Library.\r
+\r
+In addition, mere aggregation of another work not based on the Library\r
+with the Library (or with a work based on the Library) on a volume of\r
+a storage or distribution medium does not bring the other work under\r
+the scope of this License.\r
+\r
+  3. You may opt to apply the terms of the ordinary GNU General Public\r
+License instead of this License to a given copy of the Library.  To do\r
+this, you must alter all the notices that refer to this License, so\r
+that they refer to the ordinary GNU General Public License, version 2,\r
+instead of to this License.  (If a newer version than version 2 of the\r
+ordinary GNU General Public License has appeared, then you can specify\r
+that version instead if you wish.)  Do not make any other change in\r
+these notices.\r
+\r
+  Once this change is made in a given copy, it is irreversible for\r
+that copy, so the ordinary GNU General Public License applies to all\r
+subsequent copies and derivative works made from that copy.\r
+\r
+  This option is useful when you wish to copy part of the code of\r
+the Library into a program that is not a library.\r
+\r
+  4. You may copy and distribute the Library (or a portion or\r
+derivative of it, under Section 2) in object code or executable form\r
+under the terms of Sections 1 and 2 above provided that you accompany\r
+it with the complete corresponding machine-readable source code, which\r
+must be distributed under the terms of Sections 1 and 2 above on a\r
+medium customarily used for software interchange.\r
+\r
+  If distribution of object code is made by offering access to copy\r
+from a designated place, then offering equivalent access to copy the\r
+source code from the same place satisfies the requirement to\r
+distribute the source code, even though third parties are not\r
+compelled to copy the source along with the object code.\r
+\r
+  5. A program that contains no derivative of any portion of the\r
+Library, but is designed to work with the Library by being compiled or\r
+linked with it, is called a "work that uses the Library".  Such a\r
+work, in isolation, is not a derivative work of the Library, and\r
+therefore falls outside the scope of this License.\r
+\r
+  However, linking a "work that uses the Library" with the Library\r
+creates an executable that is a derivative of the Library (because it\r
+contains portions of the Library), rather than a "work that uses the\r
+library".  The executable is therefore covered by this License.\r
+Section 6 states terms for distribution of such executables.\r
+\r
+  When a "work that uses the Library" uses material from a header file\r
+that is part of the Library, the object code for the work may be a\r
+derivative work of the Library even though the source code is not.\r
+Whether this is true is especially significant if the work can be\r
+linked without the Library, or if the work is itself a library.  The\r
+threshold for this to be true is not precisely defined by law.\r
+\r
+  If such an object file uses only numerical parameters, data\r
+structure layouts and accessors, and small macros and small inline\r
+functions (ten lines or less in length), then the use of the object\r
+file is unrestricted, regardless of whether it is legally a derivative\r
+work.  (Executables containing this object code plus portions of the\r
+Library will still fall under Section 6.)\r
+\r
+  Otherwise, if the work is a derivative of the Library, you may\r
+distribute the object code for the work under the terms of Section 6.\r
+Any executables containing that work also fall under Section 6,\r
+whether or not they are linked directly with the Library itself.\r
+\r
+  6. As an exception to the Sections above, you may also combine or\r
+link a "work that uses the Library" with the Library to produce a\r
+work containing portions of the Library, and distribute that work\r
+under terms of your choice, provided that the terms permit\r
+modification of the work for the customer's own use and reverse\r
+engineering for debugging such modifications.\r
+\r
+  You must give prominent notice with each copy of the work that the\r
+Library is used in it and that the Library and its use are covered by\r
+this License.  You must supply a copy of this License.  If the work\r
+during execution displays copyright notices, you must include the\r
+copyright notice for the Library among them, as well as a reference\r
+directing the user to the copy of this License.  Also, you must do one\r
+of these things:\r
+\r
+    a) Accompany the work with the complete corresponding\r
+    machine-readable source code for the Library including whatever\r
+    changes were used in the work (which must be distributed under\r
+    Sections 1 and 2 above); and, if the work is an executable linked\r
+    with the Library, with the complete machine-readable "work that\r
+    uses the Library", as object code and/or source code, so that the\r
+    user can modify the Library and then relink to produce a modified\r
+    executable containing the modified Library.  (It is understood\r
+    that the user who changes the contents of definitions files in the\r
+    Library will not necessarily be able to recompile the application\r
+    to use the modified definitions.)\r
+\r
+    b) Use a suitable shared library mechanism for linking with the\r
+    Library.  A suitable mechanism is one that (1) uses at run time a\r
+    copy of the library already present on the user's computer system,\r
+    rather than copying library functions into the executable, and (2)\r
+    will operate properly with a modified version of the library, if\r
+    the user installs one, as long as the modified version is\r
+    interface-compatible with the version that the work was made with.\r
+\r
+    c) Accompany the work with a written offer, valid for at\r
+    least three years, to give the same user the materials\r
+    specified in Subsection 6a, above, for a charge no more\r
+    than the cost of performing this distribution.\r
+\r
+    d) If distribution of the work is made by offering access to copy\r
+    from a designated place, offer equivalent access to copy the above\r
+    specified materials from the same place.\r
+\r
+    e) Verify that the user has already received a copy of these\r
+    materials or that you have already sent this user a copy.\r
+\r
+  For an executable, the required form of the "work that uses the\r
+Library" must include any data and utility programs needed for\r
+reproducing the executable from it.  However, as a special exception,\r
+the materials to be distributed need not include anything that is\r
+normally distributed (in either source or binary form) with the major\r
+components (compiler, kernel, and so on) of the operating system on\r
+which the executable runs, unless that component itself accompanies\r
+the executable.\r
+\r
+  It may happen that this requirement contradicts the license\r
+restrictions of other proprietary libraries that do not normally\r
+accompany the operating system.  Such a contradiction means you cannot\r
+use both them and the Library together in an executable that you\r
+distribute.\r
+\r
+  7. You may place library facilities that are a work based on the\r
+Library side-by-side in a single library together with other library\r
+facilities not covered by this License, and distribute such a combined\r
+library, provided that the separate distribution of the work based on\r
+the Library and of the other library facilities is otherwise\r
+permitted, and provided that you do these two things:\r
+\r
+    a) Accompany the combined library with a copy of the same work\r
+    based on the Library, uncombined with any other library\r
+    facilities.  This must be distributed under the terms of the\r
+    Sections above.\r
+\r
+    b) Give prominent notice with the combined library of the fact\r
+    that part of it is a work based on the Library, and explaining\r
+    where to find the accompanying uncombined form of the same work.\r
+\r
+  8. You may not copy, modify, sublicense, link with, or distribute\r
+the Library except as expressly provided under this License.  Any\r
+attempt otherwise to copy, modify, sublicense, link with, or\r
+distribute the Library is void, and will automatically terminate your\r
+rights under this License.  However, parties who have received copies,\r
+or rights, from you under this License will not have their licenses\r
+terminated so long as such parties remain in full compliance.\r
+\r
+  9. You are not required to accept this License, since you have not\r
+signed it.  However, nothing else grants you permission to modify or\r
+distribute the Library or its derivative works.  These actions are\r
+prohibited by law if you do not accept this License.  Therefore, by\r
+modifying or distributing the Library (or any work based on the\r
+Library), you indicate your acceptance of this License to do so, and\r
+all its terms and conditions for copying, distributing or modifying\r
+the Library or works based on it.\r
+\r
+  10. Each time you redistribute the Library (or any work based on the\r
+Library), the recipient automatically receives a license from the\r
+original licensor to copy, distribute, link with or modify the Library\r
+subject to these terms and conditions.  You may not impose any further\r
+restrictions on the recipients' exercise of the rights granted herein.\r
+You are not responsible for enforcing compliance by third parties with\r
+this License.\r
+\r
+  11. If, as a consequence of a court judgment or allegation of patent\r
+infringement or for any other reason (not limited to patent issues),\r
+conditions are imposed on you (whether by court order, agreement or\r
+otherwise) that contradict the conditions of this License, they do not\r
+excuse you from the conditions of this License.  If you cannot\r
+distribute so as to satisfy simultaneously your obligations under this\r
+License and any other pertinent obligations, then as a consequence you\r
+may not distribute the Library at all.  For example, if a patent\r
+license would not permit royalty-free redistribution of the Library by\r
+all those who receive copies directly or indirectly through you, then\r
+the only way you could satisfy both it and this License would be to\r
+refrain entirely from distribution of the Library.\r
+\r
+If any portion of this section is held invalid or unenforceable under any\r
+particular circumstance, the balance of the section is intended to apply,\r
+and the section as a whole is intended to apply in other circumstances.\r
+\r
+It is not the purpose of this section to induce you to infringe any\r
+patents or other property right claims or to contest validity of any\r
+such claims; this section has the sole purpose of protecting the\r
+integrity of the free software distribution system which is\r
+implemented by public license practices.  Many people have made\r
+generous contributions to the wide range of software distributed\r
+through that system in reliance on consistent application of that\r
+system; it is up to the author/donor to decide if he or she is willing\r
+to distribute software through any other system and a licensee cannot\r
+impose that choice.\r
+\r
+This section is intended to make thoroughly clear what is believed to\r
+be a consequence of the rest of this License.\r
+\r
+  12. If the distribution and/or use of the Library is restricted in\r
+certain countries either by patents or by copyrighted interfaces, the\r
+original copyright holder who places the Library under this License may add\r
+an explicit geographical distribution limitation excluding those countries,\r
+so that distribution is permitted only in or among countries not thus\r
+excluded.  In such case, this License incorporates the limitation as if\r
+written in the body of this License.\r
+\r
+  13. The Free Software Foundation may publish revised and/or new\r
+versions of the Lesser General Public License from time to time.\r
+Such new versions will be similar in spirit to the present version,\r
+but may differ in detail to address new problems or concerns.\r
+\r
+Each version is given a distinguishing version number.  If the Library\r
+specifies a version number of this License which applies to it and\r
+"any later version", you have the option of following the terms and\r
+conditions either of that version or of any later version published by\r
+the Free Software Foundation.  If the Library does not specify a\r
+license version number, you may choose any version ever published by\r
+the Free Software Foundation.\r
+\r
+  14. If you wish to incorporate parts of the Library into other free\r
+programs whose distribution conditions are incompatible with these,\r
+write to the author to ask for permission.  For software which is\r
+copyrighted by the Free Software Foundation, write to the Free\r
+Software Foundation; we sometimes make exceptions for this.  Our\r
+decision will be guided by the two goals of preserving the free status\r
+of all derivatives of our free software and of promoting the sharing\r
+and reuse of software generally.\r
+\r
+NO WARRANTY\r
+\r
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\r
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\r
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\r
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY\r
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\r
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\r
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\r
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\r
+\r
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\r
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\r
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\r
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\r
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\r
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\r
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\r
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\r
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\r
+DAMAGES.\r
+\r
+END OF TERMS AND CONDITIONS\r
+```\r
+\r
+Appendix C: The MPL License\r
+---------------------------\r
+\r
+```\r
+MOZILLA PUBLIC LICENSE\r
+Version 1.1\r
+\r
+1. Definitions.\r
+\r
+     1.0.1. "Commercial Use" means distribution or otherwise making the\r
+     Covered Code available to a third party.\r
+\r
+     1.1. "Contributor" means each entity that creates or contributes to\r
+     the creation of Modifications.\r
+\r
+     1.2. "Contributor Version" means the combination of the Original\r
+     Code, prior Modifications used by a Contributor, and the Modifications\r
+     made by that particular Contributor.\r
+\r
+     1.3. "Covered Code" means the Original Code or Modifications or the\r
+     combination of the Original Code and Modifications, in each case\r
+     including portions thereof.\r
+\r
+     1.4. "Electronic Distribution Mechanism" means a mechanism generally\r
+     accepted in the software development community for the electronic\r
+     transfer of data.\r
+\r
+     1.5. "Executable" means Covered Code in any form other than Source\r
+     Code.\r
+\r
+     1.6. "Initial Developer" means the individual or entity identified\r
+     as the Initial Developer in the Source Code notice required by Exhibit\r
+     A.\r
+\r
+     1.7. "Larger Work" means a work which combines Covered Code or\r
+     portions thereof with code not governed by the terms of this License.\r
+\r
+     1.8. "License" means this document.\r
+\r
+     1.8.1. "Licensable" means having the right to grant, to the maximum\r
+     extent possible, whether at the time of the initial grant or\r
+     subsequently acquired, any and all of the rights conveyed herein.\r
+\r
+     1.9. "Modifications" means any addition to or deletion from the\r
+     substance or structure of either the Original Code or any previous\r
+     Modifications. When Covered Code is released as a series of files, a\r
+     Modification is:\r
+          A. Any addition to or deletion from the contents of a file\r
+          containing Original Code or previous Modifications.\r
+\r
+          B. Any new file that contains any part of the Original Code or\r
+          previous Modifications.\r
+\r
+     1.10. "Original Code" means Source Code of computer software code\r
+     which is described in the Source Code notice required by Exhibit A as\r
+     Original Code, and which, at the time of its release under this\r
+     License is not already Covered Code governed by this License.\r
+\r
+     1.10.1. "Patent Claims" means any patent claim(s), now owned or\r
+     hereafter acquired, including without limitation,  method, process,\r
+     and apparatus claims, in any patent Licensable by grantor.\r
+\r
+     1.11. "Source Code" means the preferred form of the Covered Code for\r
+     making modifications to it, including all modules it contains, plus\r
+     any associated interface definition files, scripts used to control\r
+     compilation and installation of an Executable, or source code\r
+     differential comparisons against either the Original Code or another\r
+     well known, available Covered Code of the Contributor's choice. The\r
+     Source Code can be in a compressed or archival form, provided the\r
+     appropriate decompression or de-archiving software is widely available\r
+     for no charge.\r
+\r
+     1.12. "You" (or "Your")  means an individual or a legal entity\r
+     exercising rights under, and complying with all of the terms of, this\r
+     License or a future version of this License issued under Section 6.1.\r
+     For legal entities, "You" includes any entity which controls, is\r
+     controlled by, or is under common control with You. For purposes of\r
+     this definition, "control" means (a) the power, direct or indirect,\r
+     to cause the direction or management of such entity, whether by\r
+     contract or otherwise, or (b) ownership of more than fifty percent\r
+     (50%) of the outstanding shares or beneficial ownership of such\r
+     entity.\r
+\r
+2. Source Code License.\r
+\r
+     2.1. The Initial Developer Grant.\r
+     The Initial Developer hereby grants You a world-wide, royalty-free,\r
+     non-exclusive license, subject to third party intellectual property\r
+     claims:\r
+          (a)  under intellectual property rights (other than patent or\r
+          trademark) Licensable by Initial Developer to use, reproduce,\r
+          modify, display, perform, sublicense and distribute the Original\r
+          Code (or portions thereof) with or without Modifications, and/or\r
+          as part of a Larger Work; and\r
+\r
+          (b) under Patents Claims infringed by the making, using or\r
+          selling of Original Code, to make, have made, use, practice,\r
+          sell, and offer for sale, and/or otherwise dispose of the\r
+          Original Code (or portions thereof).\r
+\r
+          (c) the licenses granted in this Section 2.1(a) and (b) are\r
+          effective on the date Initial Developer first distributes\r
+          Original Code under the terms of this License.\r
+\r
+          (d) Notwithstanding Section 2.1(b) above, no patent license is\r
+          granted: 1) for code that You delete from the Original Code; 2)\r
+          separate from the Original Code;  or 3) for infringements caused\r
+          by: i) the modification of the Original Code or ii) the\r
+          combination of the Original Code with other software or devices.\r
+\r
+     2.2. Contributor Grant.\r
+     Subject to third party intellectual property claims, each Contributor\r
+     hereby grants You a world-wide, royalty-free, non-exclusive license\r
+\r
+          (a)  under intellectual property rights (other than patent or\r
+          trademark) Licensable by Contributor, to use, reproduce, modify,\r
+          display, perform, sublicense and distribute the Modifications\r
+          created by such Contributor (or portions thereof) either on an\r
+          unmodified basis, with other Modifications, as Covered Code\r
+          and/or as part of a Larger Work; and\r
+\r
+          (b) under Patent Claims infringed by the making, using, or\r
+          selling of  Modifications made by that Contributor either alone\r
+          and/or in combination with its Contributor Version (or portions\r
+          of such combination), to make, use, sell, offer for sale, have\r
+          made, and/or otherwise dispose of: 1) Modifications made by that\r
+          Contributor (or portions thereof); and 2) the combination of\r
+          Modifications made by that Contributor with its Contributor\r
+          Version (or portions of such combination).\r
+\r
+          (c) the licenses granted in Sections 2.2(a) and 2.2(b) are\r
+          effective on the date Contributor first makes Commercial Use of\r
+          the Covered Code.\r
+\r
+          (d)    Notwithstanding Section 2.2(b) above, no patent license is\r
+          granted: 1) for any code that Contributor has deleted from the\r
+          Contributor Version; 2)  separate from the Contributor Version;\r
+          3)  for infringements caused by: i) third party modifications of\r
+          Contributor Version or ii)  the combination of Modifications made\r
+          by that Contributor with other software  (except as part of the\r
+          Contributor Version) or other devices; or 4) under Patent Claims\r
+          infringed by Covered Code in the absence of Modifications made by\r
+          that Contributor.\r
+\r
+3. Distribution Obligations.\r
+\r
+     3.1. Application of License.\r
+     The Modifications which You create or to which You contribute are\r
+     governed by the terms of this License, including without limitation\r
+     Section 2.2. The Source Code version of Covered Code may be\r
+     distributed only under the terms of this License or a future version\r
+     of this License released under Section 6.1, and You must include a\r
+     copy of this License with every copy of the Source Code You\r
+     distribute. You may not offer or impose any terms on any Source Code\r
+     version that alters or restricts the applicable version of this\r
+     License or the recipients' rights hereunder. However, You may include\r
+     an additional document offering the additional rights described in\r
+     Section 3.5.\r
+\r
+     3.2. Availability of Source Code.\r
+     Any Modification which You create or to which You contribute must be\r
+     made available in Source Code form under the terms of this License\r
+     either on the same media as an Executable version or via an accepted\r
+     Electronic Distribution Mechanism to anyone to whom you made an\r
+     Executable version available; and if made available via Electronic\r
+     Distribution Mechanism, must remain available for at least twelve (12)\r
+     months after the date it initially became available, or at least six\r
+     (6) months after a subsequent version of that particular Modification\r
+     has been made available to such recipients. You are responsible for\r
+     ensuring that the Source Code version remains available even if the\r
+     Electronic Distribution Mechanism is maintained by a third party.\r
+\r
+     3.3. Description of Modifications.\r
+     You must cause all Covered Code to which You contribute to contain a\r
+     file documenting the changes You made to create that Covered Code and\r
+     the date of any change. You must include a prominent statement that\r
+     the Modification is derived, directly or indirectly, from Original\r
+     Code provided by the Initial Developer and including the name of the\r
+     Initial Developer in (a) the Source Code, and (b) in any notice in an\r
+     Executable version or related documentation in which You describe the\r
+     origin or ownership of the Covered Code.\r
+\r
+     3.4. Intellectual Property Matters\r
+          (a) Third Party Claims.\r
+          If Contributor has knowledge that a license under a third party's\r
+          intellectual property rights is required to exercise the rights\r
+          granted by such Contributor under Sections 2.1 or 2.2,\r
+          Contributor must include a text file with the Source Code\r
+          distribution titled "LEGAL" which describes the claim and the\r
+          party making the claim in sufficient detail that a recipient will\r
+          know whom to contact. If Contributor obtains such knowledge after\r
+          the Modification is made available as described in Section 3.2,\r
+          Contributor shall promptly modify the LEGAL file in all copies\r
+          Contributor makes available thereafter and shall take other steps\r
+          (such as notifying appropriate mailing lists or newsgroups)\r
+          reasonably calculated to inform those who received the Covered\r
+          Code that new knowledge has been obtained.\r
+\r
+          (b) Contributor APIs.\r
+          If Contributor's Modifications include an application programming\r
+          interface and Contributor has knowledge of patent licenses which\r
+          are reasonably necessary to implement that API, Contributor must\r
+          also include this information in the LEGAL file.\r
+\r
+               (c)    Representations.\r
+          Contributor represents that, except as disclosed pursuant to\r
+          Section 3.4(a) above, Contributor believes that Contributor's\r
+          Modifications are Contributor's original creation(s) and/or\r
+          Contributor has sufficient rights to grant the rights conveyed by\r
+          this License.\r
+\r
+     3.5. Required Notices.\r
+     You must duplicate the notice in Exhibit A in each file of the Source\r
+     Code.  If it is not possible to put such notice in a particular Source\r
+     Code file due to its structure, then You must include such notice in a\r
+     location (such as a relevant directory) where a user would be likely\r
+     to look for such a notice.  If You created one or more Modification(s)\r
+     You may add your name as a Contributor to the notice described in\r
+     Exhibit A.  You must also duplicate this License in any documentation\r
+     for the Source Code where You describe recipients' rights or ownership\r
+     rights relating to Covered Code.  You may choose to offer, and to\r
+     charge a fee for, warranty, support, indemnity or liability\r
+     obligations to one or more recipients of Covered Code. However, You\r
+     may do so only on Your own behalf, and not on behalf of the Initial\r
+     Developer or any Contributor. You must make it absolutely clear than\r
+     any such warranty, support, indemnity or liability obligation is\r
+     offered by You alone, and You hereby agree to indemnify the Initial\r
+     Developer and every Contributor for any liability incurred by the\r
+     Initial Developer or such Contributor as a result of warranty,\r
+     support, indemnity or liability terms You offer.\r
+\r
+     3.6. Distribution of Executable Versions.\r
+     You may distribute Covered Code in Executable form only if the\r
+     requirements of Section 3.1-3.5 have been met for that Covered Code,\r
+     and if You include a notice stating that the Source Code version of\r
+     the Covered Code is available under the terms of this License,\r
+     including a description of how and where You have fulfilled the\r
+     obligations of Section 3.2. The notice must be conspicuously included\r
+     in any notice in an Executable version, related documentation or\r
+     collateral in which You describe recipients' rights relating to the\r
+     Covered Code. You may distribute the Executable version of Covered\r
+     Code or ownership rights under a license of Your choice, which may\r
+     contain terms different from this License, provided that You are in\r
+     compliance with the terms of this License and that the license for the\r
+     Executable version does not attempt to limit or alter the recipient's\r
+     rights in the Source Code version from the rights set forth in this\r
+     License. If You distribute the Executable version under a different\r
+     license You must make it absolutely clear that any terms which differ\r
+     from this License are offered by You alone, not by the Initial\r
+     Developer or any Contributor. You hereby agree to indemnify the\r
+     Initial Developer and every Contributor for any liability incurred by\r
+     the Initial Developer or such Contributor as a result of any such\r
+     terms You offer.\r
+\r
+     3.7. Larger Works.\r
+     You may create a Larger Work by combining Covered Code with other code\r
+     not governed by the terms of this License and distribute the Larger\r
+     Work as a single product. In such a case, You must make sure the\r
+     requirements of this License are fulfilled for the Covered Code.\r
+\r
+4. Inability to Comply Due to Statute or Regulation.\r
+\r
+     If it is impossible for You to comply with any of the terms of this\r
+     License with respect to some or all of the Covered Code due to\r
+     statute, judicial order, or regulation then You must: (a) comply with\r
+     the terms of this License to the maximum extent possible; and (b)\r
+     describe the limitations and the code they affect. Such description\r
+     must be included in the LEGAL file described in Section 3.4 and must\r
+     be included with all distributions of the Source Code. Except to the\r
+     extent prohibited by statute or regulation, such description must be\r
+     sufficiently detailed for a recipient of ordinary skill to be able to\r
+     understand it.\r
+\r
+5. Application of this License.\r
+\r
+     This License applies to code to which the Initial Developer has\r
+     attached the notice in Exhibit A and to related Covered Code.\r
+\r
+6. Versions of the License.\r
+\r
+     6.1. New Versions.\r
+     Netscape Communications Corporation ("Netscape") may publish revised\r
+     and/or new versions of the License from time to time. Each version\r
+     will be given a distinguishing version number.\r
+\r
+     6.2. Effect of New Versions.\r
+     Once Covered Code has been published under a particular version of the\r
+     License, You may always continue to use it under the terms of that\r
+     version. You may also choose to use such Covered Code under the terms\r
+     of any subsequent version of the License published by Netscape. No one\r
+     other than Netscape has the right to modify the terms applicable to\r
+     Covered Code created under this License.\r
+\r
+     6.3. Derivative Works.\r
+     If You create or use a modified version of this License (which you may\r
+     only do in order to apply it to code which is not already Covered Code\r
+     governed by this License), You must (a) rename Your license so that\r
+     the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",\r
+     "MPL", "NPL" or any confusingly similar phrase do not appear in your\r
+     license (except to note that your license differs from this License)\r
+     and (b) otherwise make it clear that Your version of the license\r
+     contains terms which differ from the Mozilla Public License and\r
+     Netscape Public License. (Filling in the name of the Initial\r
+     Developer, Original Code or Contributor in the notice described in\r
+     Exhibit A shall not of themselves be deemed to be modifications of\r
+     this License.)\r
+\r
+7. DISCLAIMER OF WARRANTY.\r
+\r
+     COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,\r
+     WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,\r
+     WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF\r
+     DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.\r
+     THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE\r
+     IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,\r
+     YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE\r
+     COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER\r
+     OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF\r
+     ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.\r
+\r
+8. TERMINATION.\r
+\r
+     8.1.  This License and the rights granted hereunder will terminate\r
+     automatically if You fail to comply with terms herein and fail to cure\r
+     such breach within 30 days of becoming aware of the breach. All\r
+     sublicenses to the Covered Code which are properly granted shall\r
+     survive any termination of this License. Provisions which, by their\r
+     nature, must remain in effect beyond the termination of this License\r
+     shall survive.\r
+\r
+     8.2.  If You initiate litigation by asserting a patent infringement\r
+     claim (excluding declatory judgment actions) against Initial Developer\r
+     or a Contributor (the Initial Developer or Contributor against whom\r
+     You file such action is referred to as "Participant")  alleging that:\r
+\r
+     (a)  such Participant's Contributor Version directly or indirectly\r
+     infringes any patent, then any and all rights granted by such\r
+     Participant to You under Sections 2.1 and/or 2.2 of this License\r
+     shall, upon 60 days notice from Participant terminate prospectively,\r
+     unless if within 60 days after receipt of notice You either: (i)\r
+     agree in writing to pay Participant a mutually agreeable reasonable\r
+     royalty for Your past and future use of Modifications made by such\r
+     Participant, or (ii) withdraw Your litigation claim with respect to\r
+     the Contributor Version against such Participant.  If within 60 days\r
+     of notice, a reasonable royalty and payment arrangement are not\r
+     mutually agreed upon in writing by the parties or the litigation claim\r
+     is not withdrawn, the rights granted by Participant to You under\r
+     Sections 2.1 and/or 2.2 automatically terminate at the expiration of\r
+     the 60 day notice period specified above.\r
+\r
+     (b)  any software, hardware, or device, other than such Participant's\r
+     Contributor Version, directly or indirectly infringes any patent, then\r
+     any rights granted to You by such Participant under Sections 2.1(b)\r
+     and 2.2(b) are revoked effective as of the date You first made, used,\r
+     sold, distributed, or had made, Modifications made by that\r
+     Participant.\r
+\r
+     8.3.  If You assert a patent infringement claim against Participant\r
+     alleging that such Participant's Contributor Version directly or\r
+     indirectly infringes any patent where such claim is resolved (such as\r
+     by license or settlement) prior to the initiation of patent\r
+     infringement litigation, then the reasonable value of the licenses\r
+     granted by such Participant under Sections 2.1 or 2.2 shall be taken\r
+     into account in determining the amount or value of any payment or\r
+     license.\r
+\r
+     8.4.  In the event of termination under Sections 8.1 or 8.2 above,\r
+     all end user license agreements (excluding distributors and resellers)\r
+     which have been validly granted by You or any distributor hereunder\r
+     prior to termination shall survive termination.\r
+\r
+9. LIMITATION OF LIABILITY.\r
+\r
+     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT\r
+     (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL\r
+     DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,\r
+     OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR\r
+     ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY\r
+     CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,\r
+     WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER\r
+     COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN\r
+     INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF\r
+     LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY\r
+     RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW\r
+     PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE\r
+     EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO\r
+     THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.\r
+\r
+10. U.S. GOVERNMENT END USERS.\r
+\r
+     The Covered Code is a "commercial item," as that term is defined in\r
+     48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer\r
+     software" and "commercial computer software documentation," as such\r
+     terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48\r
+     C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),\r
+     all U.S. Government End Users acquire Covered Code with only those\r
+     rights set forth herein.\r
+\r
+11. MISCELLANEOUS.\r
+\r
+     This License represents the complete agreement concerning subject\r
+     matter hereof. If any provision of this License is held to be\r
+     unenforceable, such provision shall be reformed only to the extent\r
+     necessary to make it enforceable. This License shall be governed by\r
+     California law provisions (except to the extent applicable law, if\r
+     any, provides otherwise), excluding its conflict-of-law provisions.\r
+     With respect to disputes in which at least one party is a citizen of,\r
+     or an entity chartered or registered to do business in the United\r
+     States of America, any litigation relating to this License shall be\r
+     subject to the jurisdiction of the Federal Courts of the Northern\r
+     District of California, with venue lying in Santa Clara County,\r
+     California, with the losing party responsible for costs, including\r
+     without limitation, court costs and reasonable attorneys' fees and\r
+     expenses. The application of the United Nations Convention on\r
+     Contracts for the International Sale of Goods is expressly excluded.\r
+     Any law or regulation which provides that the language of a contract\r
+     shall be construed against the drafter shall not apply to this\r
+     License.\r
+\r
+12. RESPONSIBILITY FOR CLAIMS.\r
+\r
+     As between Initial Developer and the Contributors, each party is\r
+     responsible for claims and damages arising, directly or indirectly,\r
+     out of its utilization of rights under this License and You agree to\r
+     work with Initial Developer and Contributors to distribute such\r
+     responsibility on an equitable basis. Nothing herein is intended or\r
+     shall be deemed to constitute any admission of liability.\r
+\r
+13. MULTIPLE-LICENSED CODE.\r
+\r
+     Initial Developer may designate portions of the Covered Code as\r
+     "Multiple-Licensed".  "Multiple-Licensed" means that the Initial\r
+     Developer permits you to utilize portions of the Covered Code under\r
+     Your choice of the NPL or the alternative licenses, if any, specified\r
+     by the Initial Developer in the file described in Exhibit A.\r
+\r
+EXHIBIT A -Mozilla Public License.\r
+\r
+     ``The contents of this file are subject to the Mozilla Public License\r
+     Version 1.1 (the "License"); you may not use this file except in\r
+     compliance with the License. You may obtain a copy of the License at\r
+     http://www.mozilla.org/MPL/\r
+\r
+     Software distributed under the License is distributed on an "AS IS"\r
+     basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the\r
+     License for the specific language governing rights and limitations\r
+     under the License.\r
+\r
+     The Original Code is ______________________________________.\r
+\r
+     The Initial Developer of the Original Code is ________________________.\r
+     Portions created by ______________________ are Copyright (C) ______\r
+     _______________________. All Rights Reserved.\r
+\r
+     Contributor(s): ______________________________________.\r
+\r
+     Alternatively, the contents of this file may be used under the terms\r
+     of the _____ license (the  "[___] License"), in which case the\r
+     provisions of [______] License are applicable instead of those\r
+     above.  If you wish to allow use of your version of this file only\r
+     under the terms of the [____] License and not to allow others to use\r
+     your version of this file under the MPL, indicate your decision by\r
+     deleting  the provisions above and replace  them with the notice and\r
+     other provisions required by the [___] License.  If you do not delete\r
+     the provisions above, a recipient may use your version of this file\r
+     under either the MPL or the [___] License."\r
+\r
+     [NOTE: The text of this Exhibit A may differ slightly from the text of\r
+     the notices in the Source Code files of the Original Code. You should\r
+     use the text of this Exhibit A rather than the text found in the\r
+     Original Code Source Code for Your Modifications.]\r
+```\r
+\r
+Appendix D: The MIT License\r
+---------------------------\r
+\r
+```\r
+The MIT License (MIT)\r
+\r
+Permission is hereby granted, free of charge, to any person obtaining a copy\r
+of this software and associated documentation files (the "Software"), to deal\r
+in the Software without restriction, including without limitation the rights\r
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r
+copies of the Software, and to permit persons to whom the Software is\r
+furnished to do so, subject to the following conditions:\r
+\r
+The above copyright notice and this permission notice shall be included in\r
+all copies or substantial portions of the Software.\r
+\r
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\r
+THE SOFTWARE.\r
+```\r
+\r
+Appendix E: The SIL Open Font License Version 1.1\r
+---------------------------------------------\r
+\r
+```\r
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r
+-----------------------------------------------------------\r
+\r
+PREAMBLE\r
+The goals of the Open Font License (OFL) are to stimulate worldwide\r
+development of collaborative font projects, to support the font creation\r
+efforts of academic and linguistic communities, and to provide a free and\r
+open framework in which fonts may be shared and improved in partnership\r
+with others.\r
+\r
+The OFL allows the licensed fonts to be used, studied, modified and\r
+redistributed freely as long as they are not sold by themselves. The\r
+fonts, including any derivative works, can be bundled, embedded,\r
+redistributed and/or sold with any software provided that any reserved\r
+names are not used by derivative works. The fonts and derivatives,\r
+however, cannot be released under any other type of license. The\r
+requirement for fonts to remain under this license does not apply\r
+to any document created using the fonts or their derivatives.\r
+\r
+DEFINITIONS\r
+"Font Software" refers to the set of files released by the Copyright\r
+Holder(s) under this license and clearly marked as such. This may\r
+include source files, build scripts and documentation.\r
+\r
+"Reserved Font Name" refers to any names specified as such after the\r
+copyright statement(s).\r
+\r
+"Original Version" refers to the collection of Font Software components as\r
+distributed by the Copyright Holder(s).\r
+\r
+"Modified Version" refers to any derivative made by adding to, deleting,\r
+or substituting -- in part or in whole -- any of the components of the\r
+Original Version, by changing formats or by porting the Font Software to a\r
+new environment.\r
+\r
+"Author" refers to any designer, engineer, programmer, technical\r
+writer or other person who contributed to the Font Software.\r
+\r
+PERMISSION & CONDITIONS\r
+Permission is hereby granted, free of charge, to any person obtaining\r
+a copy of the Font Software, to use, study, copy, merge, embed, modify,\r
+redistribute, and sell modified and unmodified copies of the Font\r
+Software, subject to the following conditions:\r
+\r
+1) Neither the Font Software nor any of its individual components,\r
+in Original or Modified Versions, may be sold by itself.\r
+\r
+2) Original or Modified Versions of the Font Software may be bundled,\r
+redistributed and/or sold with any software, provided that each copy\r
+contains the above copyright notice and this license. These can be\r
+included either as stand-alone text files, human-readable headers or\r
+in the appropriate machine-readable metadata fields within text or\r
+binary files as long as those fields can be easily viewed by the user.\r
+\r
+3) No Modified Version of the Font Software may use the Reserved Font\r
+Name(s) unless explicit written permission is granted by the corresponding\r
+Copyright Holder. This restriction only applies to the primary font name as\r
+presented to the users.\r
+\r
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r
+Software shall not be used to promote, endorse or advertise any\r
+Modified Version, except to acknowledge the contribution(s) of the\r
+Copyright Holder(s) and the Author(s) or with their explicit written\r
+permission.\r
+\r
+5) The Font Software, modified or unmodified, in part or in whole,\r
+must be distributed entirely under this license, and must not be\r
+distributed under any other license. The requirement for fonts to\r
+remain under this license does not apply to any document created\r
+using the Font Software.\r
+\r
+TERMINATION\r
+This license becomes null and void if any of the above conditions are\r
+not met.\r
+\r
+DISCLAIMER\r
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,\r
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r
+OTHER DEALINGS IN THE FONT SOFTWARE.\r
+```\r
+\r
+Appendix F: The BSD-3 License\r
+-----------------------------\r
+\r
+```\r
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\r
+\r
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\r
+\r
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\r
+\r
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.\r
+\r
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
+```\r
+\r
diff --git a/js/ckeditor/README.md b/js/ckeditor/README.md
new file mode 100644 (file)
index 0000000..d18d4a1
--- /dev/null
@@ -0,0 +1,39 @@
+CKEditor 4
+==========
+
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+http://ckeditor.com - See LICENSE.md for license information.
+
+CKEditor is a text editor to be used inside web pages. It's not a replacement
+for desktop text editors like Word or OpenOffice, but a component to be used as
+part of web applications and websites.
+
+## Documentation
+
+The full editor documentation is available online at the following address:
+http://docs.ckeditor.com
+
+## Installation
+
+Installing CKEditor is an easy task. Just follow these simple steps:
+
+ 1. **Download** the latest version from the CKEditor website:
+    http://ckeditor.com. You should have already completed this step, but be
+    sure you have the very latest version.
+ 2. **Extract** (decompress) the downloaded file into the root of your website.
+
+**Note:** CKEditor is by default installed in the `ckeditor` folder. You can
+place the files in whichever you want though.
+
+## Checking Your Installation
+
+The editor comes with a few sample pages that can be used to verify that
+installation proceeded properly. Take a look at the `samples` directory.
+
+To test your installation, just call the following page at your website:
+
+       http://<your site>/<CKEditor installation path>/samples/index.html
+
+For example:
+
+       http://www.example.com/ckeditor/samples/index.html
index 2851ea4..86cb458 100644 (file)
@@ -1,10 +1,10 @@
 /*
- Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
+ Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  For licensing, see LICENSE.md or http://ckeditor.com/license
 */
-(function(a){CKEDITOR.config.jqueryOverrideVal="undefined"==typeof CKEDITOR.config.jqueryOverrideVal?!0:CKEDITOR.config.jqueryOverrideVal;"undefined"!=typeof a&&(a.extend(a.fn,{ckeditorGet:function(){var a=this.eq(0).data("ckeditorInstance");if(!a)throw"CKEditor is not initialized yet, use ckeditor() with a callback.";return a},ckeditor:function(g,d){if(!CKEDITOR.env.isCompatible)throw Error("The environment is incompatible.");if(!a.isFunction(g))var k=d,d=g,g=k;var i=[],d=d||{};this.each(function(){var b=
-a(this),c=b.data("ckeditorInstance"),f=b.data("_ckeditorInstanceLock"),h=this,j=new a.Deferred;i.push(j.promise());if(c&&!f)g&&g.apply(c,[this]),j.resolve();else if(f)c.once("instanceReady",function(){setTimeout(function(){c.element?(c.element.$==h&&g&&g.apply(c,[h]),j.resolve()):setTimeout(arguments.callee,100)},0)},null,null,9999);else{if(d.autoUpdateElement||"undefined"==typeof d.autoUpdateElement&&CKEDITOR.config.autoUpdateElement)d.autoUpdateElementJquery=!0;d.autoUpdateElement=!1;b.data("_ckeditorInstanceLock",
-!0);c=a(this).is("textarea")?CKEDITOR.replace(h,d):CKEDITOR.inline(h,d);b.data("ckeditorInstance",c);c.on("instanceReady",function(d){var e=d.editor;setTimeout(function(){if(e.element){d.removeListener();e.on("dataReady",function(){b.trigger("dataReady.ckeditor",[e])});e.on("setData",function(a){b.trigger("setData.ckeditor",[e,a.data])});e.on("getData",function(a){b.trigger("getData.ckeditor",[e,a.data])},999);e.on("destroy",function(){b.trigger("destroy.ckeditor",[e])});e.on("save",function(){a(h.form).submit();
-return!1},null,null,20);if(e.config.autoUpdateElementJquery&&b.is("textarea")&&a(h.form).length){var c=function(){b.ckeditor(function(){e.updateElement()})};a(h.form).submit(c);a(h.form).bind("form-pre-serialize",c);b.bind("destroy.ckeditor",function(){a(h.form).unbind("submit",c);a(h.form).unbind("form-pre-serialize",c)})}e.on("destroy",function(){b.removeData("ckeditorInstance")});b.removeData("_ckeditorInstanceLock");b.trigger("instanceReady.ckeditor",[e]);g&&g.apply(e,[h]);j.resolve()}else setTimeout(arguments.callee,
-100)},0)},null,null,9999)}});var f=new a.Deferred;this.promise=f.promise();a.when.apply(this,i).then(function(){f.resolve()});this.editor=this.eq(0).data("ckeditorInstance");return this}}),CKEDITOR.config.jqueryOverrideVal&&(a.fn.val=CKEDITOR.tools.override(a.fn.val,function(g){return function(d){if(arguments.length){var k=this,i=[],f=this.each(function(){var b=a(this),c=b.data("ckeditorInstance");if(b.is("textarea")&&c){var f=new a.Deferred;c.setData(d,function(){f.resolve()});i.push(f.promise());
-return!0}return g.call(b,d)});if(i.length){var b=new a.Deferred;a.when.apply(this,i).done(function(){b.resolveWith(k)});return b.promise()}return f}var f=a(this).eq(0),c=f.data("ckeditorInstance");return f.is("textarea")&&c?c.getData():g.call(f)}})))})(window.jQuery);
\ No newline at end of file
+(function(a){if("undefined"==typeof a)throw Error("jQuery should be loaded before CKEditor jQuery adapter.");if("undefined"==typeof CKEDITOR)throw Error("CKEditor should be loaded before CKEditor jQuery adapter.");CKEDITOR.config.jqueryOverrideVal="undefined"==typeof CKEDITOR.config.jqueryOverrideVal?!0:CKEDITOR.config.jqueryOverrideVal;a.extend(a.fn,{ckeditorGet:function(){var a=this.eq(0).data("ckeditorInstance");if(!a)throw"CKEditor is not initialized yet, use ckeditor() with a callback.";return a},
+ckeditor:function(g,d){if(!CKEDITOR.env.isCompatible)throw Error("The environment is incompatible.");if(!a.isFunction(g)){var m=d;d=g;g=m}var k=[];d=d||{};this.each(function(){var b=a(this),c=b.data("ckeditorInstance"),f=b.data("_ckeditorInstanceLock"),h=this,l=new a.Deferred;k.push(l.promise());if(c&&!f)g&&g.apply(c,[this]),l.resolve();else if(f)c.once("instanceReady",function(){setTimeout(function(){c.element?(c.element.$==h&&g&&g.apply(c,[h]),l.resolve()):setTimeout(arguments.callee,100)},0)},
+null,null,9999);else{if(d.autoUpdateElement||"undefined"==typeof d.autoUpdateElement&&CKEDITOR.config.autoUpdateElement)d.autoUpdateElementJquery=!0;d.autoUpdateElement=!1;b.data("_ckeditorInstanceLock",!0);c=a(this).is("textarea")?CKEDITOR.replace(h,d):CKEDITOR.inline(h,d);b.data("ckeditorInstance",c);c.on("instanceReady",function(d){var e=d.editor;setTimeout(function(){if(e.element){d.removeListener();e.on("dataReady",function(){b.trigger("dataReady.ckeditor",[e])});e.on("setData",function(a){b.trigger("setData.ckeditor",
+[e,a.data])});e.on("getData",function(a){b.trigger("getData.ckeditor",[e,a.data])},999);e.on("destroy",function(){b.trigger("destroy.ckeditor",[e])});e.on("save",function(){a(h.form).submit();return!1},null,null,20);if(e.config.autoUpdateElementJquery&&b.is("textarea")&&a(h.form).length){var c=function(){b.ckeditor(function(){e.updateElement()})};a(h.form).submit(c);a(h.form).bind("form-pre-serialize",c);b.bind("destroy.ckeditor",function(){a(h.form).unbind("submit",c);a(h.form).unbind("form-pre-serialize",
+c)})}e.on("destroy",function(){b.removeData("ckeditorInstance")});b.removeData("_ckeditorInstanceLock");b.trigger("instanceReady.ckeditor",[e]);g&&g.apply(e,[h]);l.resolve()}else setTimeout(arguments.callee,100)},0)},null,null,9999)}});var f=new a.Deferred;this.promise=f.promise();a.when.apply(this,k).then(function(){f.resolve()});this.editor=this.eq(0).data("ckeditorInstance");return this}});CKEDITOR.config.jqueryOverrideVal&&(a.fn.val=CKEDITOR.tools.override(a.fn.val,function(g){return function(d){if(arguments.length){var m=
+this,k=[],f=this.each(function(){var b=a(this),c=b.data("ckeditorInstance");if(b.is("textarea")&&c){var f=new a.Deferred;c.setData(d,function(){f.resolve()});k.push(f.promise());return!0}return g.call(b,d)});if(k.length){var b=new a.Deferred;a.when.apply(this,k).done(function(){b.resolveWith(m)});return b.promise()}return f}var f=a(this).eq(0),c=f.data("ckeditorInstance");return f.is("textarea")&&c?c.getData():g.call(f)}}))})(window.jQuery);
\ No newline at end of file
index 43fb935..0e3b826 100644 (file)
@@ -1,5 +1,5 @@
 /**\r
- * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.\r
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
  * For licensing, see LICENSE.md or http://ckeditor.com/license\r
  */\r
 \r
  * (1) http://ckeditor.com/builder\r
  *     Visit online builder to build CKEditor from scratch.\r
  *\r
- * (2) http://ckeditor.com/builder/fa2b75439d6d70ccf0c31610888271cc\r
+ * (2) http://ckeditor.com/builder/43e1bb7eecc49e498b91b6045b267c42\r
  *     Visit online builder to build CKEditor, starting with the same setup as before.\r
  *\r
- * (3) http://ckeditor.com/builder/download/fa2b75439d6d70ccf0c31610888271cc\r
+ * (3) http://ckeditor.com/builder/download/43e1bb7eecc49e498b91b6045b267c42\r
  *     Straight download link to the latest version of CKEditor (Optimized) with the same setup as before.\r
  *\r
  * NOTE:\r
  */\r
 \r
 var CKBUILDER_CONFIG = {\r
-       skin: 'moono',\r
-       preset: 'full',\r
-       ignore: [\r
-               'dev',\r
-               '.gitignore',\r
-               '.gitattributes',\r
-               'README.md',\r
-               '.mailmap'\r
-       ],\r
+       skin: 'moono-lisa',\r
+       preset: 'standard',\r
+       ignore: [
+               '.DS_Store',
+               '.bender',
+               '.editorconfig',
+               '.gitattributes',
+               '.gitignore',
+               '.idea',
+               '.jscsrc',
+               '.jshintignore',
+               '.jshintrc',
+               '.mailmap',
+               '.travis.yml',
+               'bender-err.log',
+               'bender-out.log',
+               'bender.ci.js',
+               'bender.js',
+               'dev',
+               'gruntfile.js',
+               'less',
+               'node_modules',
+               'package.json',
+               'tests'
+       ],
        plugins : {
-               'about' : 1,
                'basicstyles' : 1,
                'clipboard' : 1,
                'enterkey' : 1,
@@ -46,10 +61,8 @@ var CKBUILDER_CONFIG = {
                'link' : 1,
                'list' : 1,
                'removeformat' : 1,
-               'resize' : 1,
-               'toolbar' : 1,
-               'undo' : 1,
-               'wysiwygarea' : 1
+               'sourcedialog' : 1,
+               'toolbar' : 1
        },
        languages : {
                'de' : 1,
index 741e470..068941c 100644 (file)
 /*
-Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
-For licensing, see LICENSE.html or http://ckeditor.com/license
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.md or http://ckeditor.com/license
 */
-(function(){if(window.CKEDITOR&&window.CKEDITOR.dom)return;window.CKEDITOR||(window.CKEDITOR=function(){var a={timestamp:"DBAA",version:"4.3.1",revision:"3ecd0b8",rnd:Math.floor(900*Math.random())+100,_:{pending:[]},status:"unloaded",basePath:function(){var b=window.CKEDITOR_BASEPATH||"";if(!b)for(var d=document.getElementsByTagName("script"),a=0;a<d.length;a++){var e=d[a].src.match(/(^|.*[\\\/])ckeditor(?:_basic)?(?:_source)?.js(?:\?.*)?$/i);if(e){b=e[1];break}}-1==b.indexOf(":/")&&(b=0===b.indexOf("/")?location.href.match(/^.*?:\/\/[^\/]*/)[0]+b:location.href.match(/^[^\?]*\/(?:)/)[0]+
-b);if(!b)throw'The CKEditor installation path could not be automatically detected. Please set the global variable "CKEDITOR_BASEPATH" before creating editor instances.';return b}(),getUrl:function(b){-1==b.indexOf(":/")&&0!==b.indexOf("/")&&(b=this.basePath+b);this.timestamp&&("/"!=b.charAt(b.length-1)&&!/[&?]t=/.test(b))&&(b+=(0<=b.indexOf("?")?"&":"?")+"t="+this.timestamp);return b},domReady:function(){function b(){try{document.addEventListener?(document.removeEventListener("DOMContentLoaded",b,
-!1),d()):document.attachEvent&&"complete"===document.readyState&&(document.detachEvent("onreadystatechange",b),d())}catch(a){}}function d(){for(var d;d=a.shift();)d()}var a=[];return function(d){a.push(d);"complete"===document.readyState&&setTimeout(b,1);if(1==a.length)if(document.addEventListener)document.addEventListener("DOMContentLoaded",b,!1),window.addEventListener("load",b,!1);else if(document.attachEvent){document.attachEvent("onreadystatechange",b);window.attachEvent("onload",b);d=!1;try{d=
-!window.frameElement}catch(e){}if(document.documentElement.doScroll&&d){var i=function(){try{document.documentElement.doScroll("left")}catch(d){setTimeout(i,1);return}b()};i()}}}}()},e=window.CKEDITOR_GETURL;if(e){var b=a.getUrl;a.getUrl=function(c){return e.call(a,c)||b.call(a,c)}}return a}());
-CKEDITOR.event||(CKEDITOR.event=function(){},CKEDITOR.event.implementOn=function(a){var e=CKEDITOR.event.prototype,b;for(b in e)a[b]==void 0&&(a[b]=e[b])},CKEDITOR.event.prototype=function(){function a(a){var d=e(this);return d[a]||(d[a]=new b(a))}var e=function(b){b=b.getPrivate&&b.getPrivate()||b._||(b._={});return b.events||(b.events={})},b=function(b){this.name=b;this.listeners=[]};b.prototype={getListenerIndex:function(b){for(var d=0,a=this.listeners;d<a.length;d++)if(a[d].fn==b)return d;return-1}};
-return{define:function(b,d){var h=a.call(this,b);CKEDITOR.tools.extend(h,d,true)},on:function(b,d,h,e,n){function i(a,f,o,q){a={name:b,sender:this,editor:a,data:f,listenerData:e,stop:o,cancel:q,removeListener:j};return d.call(h,a)===false?false:a.data}function j(){q.removeListener(b,d)}var o=a.call(this,b);if(o.getListenerIndex(d)<0){o=o.listeners;h||(h=this);isNaN(n)&&(n=10);var q=this;i.fn=d;i.priority=n;for(var s=o.length-1;s>=0;s--)if(o[s].priority<=n){o.splice(s+1,0,i);return{removeListener:j}}o.unshift(i)}return{removeListener:j}},
-once:function(){var b=arguments[1];arguments[1]=function(d){d.removeListener();return b.apply(this,arguments)};return this.on.apply(this,arguments)},capture:function(){CKEDITOR.event.useCapture=1;var b=this.on.apply(this,arguments);CKEDITOR.event.useCapture=0;return b},fire:function(){var b=0,d=function(){b=1},a=0,g=function(){a=1};return function(n,i,j){var o=e(this)[n],n=b,q=a;b=a=0;if(o){var s=o.listeners;if(s.length)for(var s=s.slice(0),u,f=0;f<s.length;f++){if(o.errorProof)try{u=s[f].call(this,
-j,i,d,g)}catch(p){}else u=s[f].call(this,j,i,d,g);u===false?a=1:typeof u!="undefined"&&(i=u);if(b||a)break}}i=a?false:typeof i=="undefined"?true:i;b=n;a=q;return i}}(),fireOnce:function(b,d,a){d=this.fire(b,d,a);delete e(this)[b];return d},removeListener:function(b,d){var a=e(this)[b];if(a){var g=a.getListenerIndex(d);g>=0&&a.listeners.splice(g,1)}},removeAllListeners:function(){var b=e(this),d;for(d in b)delete b[d]},hasListeners:function(b){return(b=e(this)[b])&&b.listeners.length>0}}}());
-CKEDITOR.editor||(CKEDITOR.editor=function(){CKEDITOR._.pending.push([this,arguments]);CKEDITOR.event.call(this)},CKEDITOR.editor.prototype.fire=function(a,e){a in{instanceReady:1,loaded:1}&&(this[a]=true);return CKEDITOR.event.prototype.fire.call(this,a,e,this)},CKEDITOR.editor.prototype.fireOnce=function(a,e){a in{instanceReady:1,loaded:1}&&(this[a]=true);return CKEDITOR.event.prototype.fireOnce.call(this,a,e,this)},CKEDITOR.event.implementOn(CKEDITOR.editor.prototype));
-CKEDITOR.env||(CKEDITOR.env=function(){var a=navigator.userAgent.toLowerCase(),e=window.opera,b={ie:a.indexOf("trident/")>-1,opera:!!e&&e.version,webkit:a.indexOf(" applewebkit/")>-1,air:a.indexOf(" adobeair/")>-1,mac:a.indexOf("macintosh")>-1,quirks:document.compatMode=="BackCompat"&&(!document.documentMode||document.documentMode<10),mobile:a.indexOf("mobile")>-1,iOS:/(ipad|iphone|ipod)/.test(a),isCustomDomain:function(){if(!this.ie)return false;var d=document.domain,b=window.location.hostname;return d!=
-b&&d!="["+b+"]"},secure:location.protocol=="https:"};b.gecko=navigator.product=="Gecko"&&!b.webkit&&!b.opera&&!b.ie;if(b.webkit)a.indexOf("chrome")>-1?b.chrome=true:b.safari=true;var c=0;if(b.ie){c=b.quirks||!document.documentMode?parseFloat(a.match(/msie (\d+)/)[1]):document.documentMode;b.ie9Compat=c==9;b.ie8Compat=c==8;b.ie7Compat=c==7;b.ie6Compat=c<7||b.quirks}if(b.gecko){var d=a.match(/rv:([\d\.]+)/);if(d){d=d[1].split(".");c=d[0]*1E4+(d[1]||0)*100+(d[2]||0)*1}}b.opera&&(c=parseFloat(e.version()));
-b.air&&(c=parseFloat(a.match(/ adobeair\/(\d+)/)[1]));b.webkit&&(c=parseFloat(a.match(/ applewebkit\/(\d+)/)[1]));b.version=c;b.isCompatible=b.iOS&&c>=534||!b.mobile&&(b.ie&&c>6||b.gecko&&c>=10801||b.opera&&c>=9.5||b.air&&c>=1||b.webkit&&c>=522||false);b.hidpi=window.devicePixelRatio>=2;b.needsBrFiller=b.gecko||b.webkit||b.ie&&c>10;b.needsNbspFiller=b.ie&&c<11;b.cssClass="cke_browser_"+(b.ie?"ie":b.gecko?"gecko":b.opera?"opera":b.webkit?"webkit":"unknown");if(b.quirks)b.cssClass=b.cssClass+" cke_browser_quirks";
-if(b.ie){b.cssClass=b.cssClass+(" cke_browser_ie"+(b.quirks||b.version<7?"6":b.version));if(b.quirks)b.cssClass=b.cssClass+" cke_browser_iequirks"}if(b.gecko)if(c<10900)b.cssClass=b.cssClass+" cke_browser_gecko18";else if(c<=11E3)b.cssClass=b.cssClass+" cke_browser_gecko19";if(b.air)b.cssClass=b.cssClass+" cke_browser_air";if(b.iOS)b.cssClass=b.cssClass+" cke_browser_ios";if(b.hidpi)b.cssClass=b.cssClass+" cke_hidpi";return b}());
-"unloaded"==CKEDITOR.status&&function(){CKEDITOR.event.implementOn(CKEDITOR);CKEDITOR.loadFullCore=function(){if(CKEDITOR.status!="basic_ready")CKEDITOR.loadFullCore._load=1;else{delete CKEDITOR.loadFullCore;var a=document.createElement("script");a.type="text/javascript";a.src=CKEDITOR.basePath+"ckeditor.js";document.getElementsByTagName("head")[0].appendChild(a)}};CKEDITOR.loadFullCoreTimeout=0;CKEDITOR.add=function(a){(this._.pending||(this._.pending=[])).push(a)};(function(){CKEDITOR.domReady(function(){var a=
-CKEDITOR.loadFullCore,e=CKEDITOR.loadFullCoreTimeout;if(a){CKEDITOR.status="basic_ready";a&&a._load?a():e&&setTimeout(function(){CKEDITOR.loadFullCore&&CKEDITOR.loadFullCore()},e*1E3)}})})();CKEDITOR.status="basic_loaded"}();CKEDITOR.dom={};
-(function(){var a=[],e=CKEDITOR.env.gecko?"-moz-":CKEDITOR.env.webkit?"-webkit-":CKEDITOR.env.opera?"-o-":CKEDITOR.env.ie?"-ms-":"";CKEDITOR.on("reset",function(){a=[]});CKEDITOR.tools={arrayCompare:function(b,a){if(!b&&!a)return true;if(!b||!a||b.length!=a.length)return false;for(var d=0;d<b.length;d++)if(b[d]!=a[d])return false;return true},clone:function(b){var a;if(b&&b instanceof Array){a=[];for(var d=0;d<b.length;d++)a[d]=CKEDITOR.tools.clone(b[d]);return a}if(b===null||typeof b!="object"||
-b instanceof String||b instanceof Number||b instanceof Boolean||b instanceof Date||b instanceof RegExp)return b;a=new b.constructor;for(d in b)a[d]=CKEDITOR.tools.clone(b[d]);return a},capitalize:function(b,a){return b.charAt(0).toUpperCase()+(a?b.slice(1):b.slice(1).toLowerCase())},extend:function(b){var a=arguments.length,d,h;if(typeof(d=arguments[a-1])=="boolean")a--;else if(typeof(d=arguments[a-2])=="boolean"){h=arguments[a-1];a=a-2}for(var e=1;e<a;e++){var n=arguments[e],i;for(i in n)if(d===
-true||b[i]==void 0)if(!h||i in h)b[i]=n[i]}return b},prototypedCopy:function(b){var a=function(){};a.prototype=b;return new a},copy:function(b){var a={},d;for(d in b)a[d]=b[d];return a},isArray:function(b){return Object.prototype.toString.call(b)=="[object Array]"},isEmpty:function(b){for(var a in b)if(b.hasOwnProperty(a))return false;return true},cssVendorPrefix:function(b,a,d){if(d)return e+b+":"+a+";"+b+":"+a;d={};d[b]=a;d[e+b]=a;return d},cssStyleToDomStyle:function(){var b=document.createElement("div").style,
-a=typeof b.cssFloat!="undefined"?"cssFloat":typeof b.styleFloat!="undefined"?"styleFloat":"float";return function(d){return d=="float"?a:d.replace(/-./g,function(d){return d.substr(1).toUpperCase()})}}(),buildStyleHtml:function(b){for(var b=[].concat(b),a,d=[],h=0;h<b.length;h++)if(a=b[h])/@import|[{}]/.test(a)?d.push("<style>"+a+"</style>"):d.push('<link type="text/css" rel=stylesheet href="'+a+'">');return d.join("")},htmlEncode:function(b){return(""+b).replace(/&/g,"&amp;").replace(/>/g,"&gt;").replace(/</g,
-"&lt;")},htmlEncodeAttr:function(b){return b.replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;")},htmlDecodeAttr:function(b){return b.replace(/&quot;/g,'"').replace(/&lt;/g,"<").replace(/&gt;/g,">")},getNextNumber:function(){var b=0;return function(){return++b}}(),getNextId:function(){return"cke_"+this.getNextNumber()},override:function(b,a){var d=a(b);d.prototype=b.prototype;return d},setTimeout:function(b,a,d,h,e){e||(e=window);d||(d=e);return e.setTimeout(function(){h?b.apply(d,[].concat(h)):
-b.apply(d)},a||0)},trim:function(){var b=/(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g;return function(a){return a.replace(b,"")}}(),ltrim:function(){var b=/^[ \t\n\r]+/g;return function(a){return a.replace(b,"")}}(),rtrim:function(){var b=/[ \t\n\r]+$/g;return function(a){return a.replace(b,"")}}(),indexOf:function(b,a){if(typeof a=="function")for(var d=0,h=b.length;d<h;d++){if(a(b[d]))return d}else{if(b.indexOf)return b.indexOf(a);d=0;for(h=b.length;d<h;d++)if(b[d]===a)return d}return-1},search:function(b,
-a){var d=CKEDITOR.tools.indexOf(b,a);return d>=0?b[d]:null},bind:function(b,a){return function(){return b.apply(a,arguments)}},createClass:function(b){var a=b.$,d=b.base,h=b.privates||b._,e=b.proto,b=b.statics;!a&&(a=function(){d&&this.base.apply(this,arguments)});if(h)var n=a,a=function(){var d=this._||(this._={}),a;for(a in h){var b=h[a];d[a]=typeof b=="function"?CKEDITOR.tools.bind(b,this):b}n.apply(this,arguments)};if(d){a.prototype=this.prototypedCopy(d.prototype);a.prototype.constructor=a;a.base=
-d;a.baseProto=d.prototype;a.prototype.base=function(){this.base=d.prototype.base;d.apply(this,arguments);this.base=arguments.callee}}e&&this.extend(a.prototype,e,true);b&&this.extend(a,b,true);return a},addFunction:function(b,c){return a.push(function(){return b.apply(c||this,arguments)})-1},removeFunction:function(b){a[b]=null},callFunction:function(b){var c=a[b];return c&&c.apply(window,Array.prototype.slice.call(arguments,1))},cssLength:function(){var a=/^-?\d+\.?\d*px$/,c;return function(d){c=
-CKEDITOR.tools.trim(d+"")+"px";return a.test(c)?c:d||""}}(),convertToPx:function(){var a;return function(c){if(!a){a=CKEDITOR.dom.element.createFromHtml('<div style="position:absolute;left:-9999px;top:-9999px;margin:0px;padding:0px;border:0px;"></div>',CKEDITOR.document);CKEDITOR.document.getBody().append(a)}if(!/%$/.test(c)){a.setStyle("width",c);return a.$.clientWidth}return c}}(),repeat:function(a,c){return Array(c+1).join(a)},tryThese:function(){for(var a,c=0,d=arguments.length;c<d;c++){var h=
-arguments[c];try{a=h();break}catch(e){}}return a},genKey:function(){return Array.prototype.slice.call(arguments).join("-")},defer:function(a){return function(){var c=arguments,d=this;window.setTimeout(function(){a.apply(d,c)},0)}},normalizeCssText:function(a,c){var d=[],h,e=CKEDITOR.tools.parseCssText(a,true,c);for(h in e)d.push(h+":"+e[h]);d.sort();return d.length?d.join(";")+";":""},convertRgbToHex:function(a){return a.replace(/(?:rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\))/gi,function(a,d,b,e){a=
-[d,b,e];for(d=0;d<3;d++)a[d]=("0"+parseInt(a[d],10).toString(16)).slice(-2);return"#"+a.join("")})},parseCssText:function(a,c,d){var h={};if(d){d=new CKEDITOR.dom.element("span");d.setAttribute("style",a);a=CKEDITOR.tools.convertRgbToHex(d.getAttribute("style")||"")}if(!a||a==";")return h;a.replace(/&quot;/g,'"').replace(/\s*([^:;\s]+)\s*:\s*([^;]+)\s*(?=;|$)/g,function(a,d,b){if(c){d=d.toLowerCase();d=="font-family"&&(b=b.toLowerCase().replace(/["']/g,"").replace(/\s*,\s*/g,","));b=CKEDITOR.tools.trim(b)}h[d]=
-b});return h},writeCssText:function(a,c){var d,h=[];for(d in a)h.push(d+":"+a[d]);c&&h.sort();return h.join("; ")},objectCompare:function(a,c,d){var h;if(!a&&!c)return true;if(!a||!c)return false;for(h in a)if(a[h]!=c[h])return false;if(!d)for(h in c)if(a[h]!=c[h])return false;return true},objectKeys:function(a){var c=[],d;for(d in a)c.push(d);return c},convertArrayToObject:function(a,c){var d={};arguments.length==1&&(c=true);for(var h=0,e=a.length;h<e;++h)d[a[h]]=c;return d},fixDomain:function(){for(var a;;)try{a=
-window.parent.document.domain;break}catch(c){a=a?a.replace(/.+?(?:\.|$)/,""):document.domain;if(!a)break;document.domain=a}return!!a},eventsBuffer:function(a,c){function d(){e=(new Date).getTime();h=false;c()}var h,e=0;return{input:function(){if(!h){var c=(new Date).getTime()-e;c<a?h=setTimeout(d,a-c):d()}},reset:function(){h&&clearTimeout(h);h=e=0}}},enableHtml5Elements:function(a,c){for(var d=["abbr","article","aside","audio","bdi","canvas","data","datalist","details","figcaption","figure","footer",
-"header","hgroup","mark","meter","nav","output","progress","section","summary","time","video"],h=d.length,e;h--;){e=a.createElement(d[h]);c&&a.appendChild(e)}}}})();
-CKEDITOR.dtd=function(){var a=CKEDITOR.tools.extend,e=function(a,d){for(var b=CKEDITOR.tools.clone(a),h=1;h<arguments.length;h++){var d=arguments[h],c;for(c in d)delete b[c]}return b},b={},c={},d={address:1,article:1,aside:1,blockquote:1,details:1,div:1,dl:1,fieldset:1,figure:1,footer:1,form:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,header:1,hgroup:1,hr:1,menu:1,nav:1,ol:1,p:1,pre:1,section:1,table:1,ul:1},h={command:1,link:1,meta:1,noscript:1,script:1,style:1},g={},n={"#":1},i={center:1,dir:1,noframes:1};
-a(b,{a:1,abbr:1,area:1,audio:1,b:1,bdi:1,bdo:1,br:1,button:1,canvas:1,cite:1,code:1,command:1,datalist:1,del:1,dfn:1,em:1,embed:1,i:1,iframe:1,img:1,input:1,ins:1,kbd:1,keygen:1,label:1,map:1,mark:1,meter:1,noscript:1,object:1,output:1,progress:1,q:1,ruby:1,s:1,samp:1,script:1,select:1,small:1,span:1,strong:1,sub:1,sup:1,textarea:1,time:1,u:1,"var":1,video:1,wbr:1},n,{acronym:1,applet:1,basefont:1,big:1,font:1,isindex:1,strike:1,style:1,tt:1});a(c,d,b,i);e={a:e(b,{a:1,button:1}),abbr:b,address:c,
-area:g,article:a({style:1},c),aside:a({style:1},c),audio:a({source:1,track:1},c),b:b,base:g,bdi:b,bdo:b,blockquote:c,body:c,br:g,button:e(b,{a:1,button:1}),canvas:b,caption:c,cite:b,code:b,col:g,colgroup:{col:1},command:g,datalist:a({option:1},b),dd:c,del:b,details:a({summary:1},c),dfn:b,div:a({style:1},c),dl:{dt:1,dd:1},dt:c,em:b,embed:g,fieldset:a({legend:1},c),figcaption:c,figure:a({figcaption:1},c),footer:c,form:c,h1:b,h2:b,h3:b,h4:b,h5:b,h6:b,head:a({title:1,base:1},h),header:c,hgroup:{h1:1,
-h2:1,h3:1,h4:1,h5:1,h6:1},hr:g,html:a({head:1,body:1},c,h),i:b,iframe:n,img:g,input:g,ins:b,kbd:b,keygen:g,label:b,legend:b,li:c,link:g,map:c,mark:b,menu:a({li:1},c),meta:g,meter:e(b,{meter:1}),nav:c,noscript:a({link:1,meta:1,style:1},b),object:a({param:1},b),ol:{li:1},optgroup:{option:1},option:n,output:b,p:b,param:g,pre:b,progress:e(b,{progress:1}),q:b,rp:b,rt:b,ruby:a({rp:1,rt:1},b),s:b,samp:b,script:n,section:a({style:1},c),select:{optgroup:1,option:1},small:b,source:g,span:b,strong:b,style:n,
-sub:b,summary:b,sup:b,table:{caption:1,colgroup:1,thead:1,tfoot:1,tbody:1,tr:1},tbody:{tr:1},td:c,textarea:n,tfoot:{tr:1},th:c,thead:{tr:1},time:e(b,{time:1}),title:n,tr:{th:1,td:1},track:g,u:b,ul:{li:1},"var":b,video:a({source:1,track:1},c),wbr:g,acronym:b,applet:a({param:1},c),basefont:g,big:b,center:c,dialog:g,dir:{li:1},font:b,isindex:g,noframes:c,strike:b,tt:b};a(e,{$block:a({audio:1,dd:1,dt:1,figcaption:1,li:1,video:1},d,i),$blockLimit:{article:1,aside:1,audio:1,body:1,caption:1,details:1,dir:1,
-div:1,dl:1,fieldset:1,figcaption:1,figure:1,footer:1,form:1,header:1,hgroup:1,menu:1,nav:1,ol:1,section:1,table:1,td:1,th:1,tr:1,ul:1,video:1},$cdata:{script:1,style:1},$editable:{address:1,article:1,aside:1,blockquote:1,body:1,details:1,div:1,fieldset:1,figcaption:1,footer:1,form:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,header:1,hgroup:1,nav:1,p:1,pre:1,section:1},$empty:{area:1,base:1,basefont:1,br:1,col:1,command:1,dialog:1,embed:1,hr:1,img:1,input:1,isindex:1,keygen:1,link:1,meta:1,param:1,source:1,track:1,
-wbr:1},$inline:b,$list:{dl:1,ol:1,ul:1},$listItem:{dd:1,dt:1,li:1},$nonBodyContent:a({body:1,head:1,html:1},e.head),$nonEditable:{applet:1,audio:1,button:1,embed:1,iframe:1,map:1,object:1,option:1,param:1,script:1,textarea:1,video:1},$object:{applet:1,audio:1,button:1,hr:1,iframe:1,img:1,input:1,object:1,select:1,table:1,textarea:1,video:1},$removeEmpty:{abbr:1,acronym:1,b:1,bdi:1,bdo:1,big:1,cite:1,code:1,del:1,dfn:1,em:1,font:1,i:1,ins:1,label:1,kbd:1,mark:1,meter:1,output:1,q:1,ruby:1,s:1,samp:1,
-small:1,span:1,strike:1,strong:1,sub:1,sup:1,time:1,tt:1,u:1,"var":1},$tabIndex:{a:1,area:1,button:1,input:1,object:1,select:1,textarea:1},$tableContent:{caption:1,col:1,colgroup:1,tbody:1,td:1,tfoot:1,th:1,thead:1,tr:1},$transparent:{a:1,audio:1,canvas:1,del:1,ins:1,map:1,noscript:1,object:1,video:1},$intermediate:{caption:1,colgroup:1,dd:1,dt:1,figcaption:1,legend:1,li:1,optgroup:1,option:1,rp:1,rt:1,summary:1,tbody:1,td:1,tfoot:1,th:1,thead:1,tr:1}});return e}();
+(function(){if(window.CKEDITOR&&window.CKEDITOR.dom)return;window.CKEDITOR||(window.CKEDITOR=function(){var a=/(^|.*[\\\/])ckeditor\.js(?:\?.*|;.*)?$/i,d={timestamp:"H7HD",version:"4.7.2",revision:"c9b79c9",rnd:Math.floor(900*Math.random())+100,_:{pending:[],basePathSrcPattern:a},status:"unloaded",basePath:function(){var b=window.CKEDITOR_BASEPATH||"";if(!b)for(var c=document.getElementsByTagName("script"),d=0;d<c.length;d++){var k=c[d].src.match(a);if(k){b=k[1];break}}-1==b.indexOf(":/")&&"//"!=b.slice(0,2)&&(b=0===b.indexOf("/")?location.href.match(/^.*?:\/\/[^\/]*/)[0]+
+b:location.href.match(/^[^\?]*\/(?:)/)[0]+b);if(!b)throw'The CKEditor installation path could not be automatically detected. Please set the global variable "CKEDITOR_BASEPATH" before creating editor instances.';return b}(),getUrl:function(a){-1==a.indexOf(":/")&&0!==a.indexOf("/")&&(a=this.basePath+a);this.timestamp&&"/"!=a.charAt(a.length-1)&&!/[&?]t=/.test(a)&&(a+=(0<=a.indexOf("?")?"\x26":"?")+"t\x3d"+this.timestamp);return a},domReady:function(){function a(){try{document.addEventListener?(document.removeEventListener("DOMContentLoaded",
+a,!1),b()):document.attachEvent&&"complete"===document.readyState&&(document.detachEvent("onreadystatechange",a),b())}catch(c){}}function b(){for(var a;a=c.shift();)a()}var c=[];return function(b){function n(){try{document.documentElement.doScroll("left")}catch(f){setTimeout(n,1);return}a()}c.push(b);"complete"===document.readyState&&setTimeout(a,1);if(1==c.length)if(document.addEventListener)document.addEventListener("DOMContentLoaded",a,!1),window.addEventListener("load",a,!1);else if(document.attachEvent){document.attachEvent("onreadystatechange",
+a);window.attachEvent("onload",a);b=!1;try{b=!window.frameElement}catch(u){}document.documentElement.doScroll&&b&&n()}}}()},b=window.CKEDITOR_GETURL;if(b){var c=d.getUrl;d.getUrl=function(a){return b.call(d,a)||c.call(d,a)}}return d}());
+CKEDITOR.event||(CKEDITOR.event=function(){},CKEDITOR.event.implementOn=function(a){var d=CKEDITOR.event.prototype,b;for(b in d)null==a[b]&&(a[b]=d[b])},CKEDITOR.event.prototype=function(){function a(a){var e=d(this);return e[a]||(e[a]=new b(a))}var d=function(a){a=a.getPrivate&&a.getPrivate()||a._||(a._={});return a.events||(a.events={})},b=function(a){this.name=a;this.listeners=[]};b.prototype={getListenerIndex:function(a){for(var b=0,d=this.listeners;b<d.length;b++)if(d[b].fn==a)return b;return-1}};
+return{define:function(b,d){var g=a.call(this,b);CKEDITOR.tools.extend(g,d,!0)},on:function(b,d,g,h,k){function n(a,f,D,k){a={name:b,sender:this,editor:a,data:f,listenerData:h,stop:D,cancel:k,removeListener:u};return!1===d.call(g,a)?!1:a.data}function u(){D.removeListener(b,d)}var f=a.call(this,b);if(0>f.getListenerIndex(d)){f=f.listeners;g||(g=this);isNaN(k)&&(k=10);var D=this;n.fn=d;n.priority=k;for(var w=f.length-1;0<=w;w--)if(f[w].priority<=k)return f.splice(w+1,0,n),{removeListener:u};f.unshift(n)}return{removeListener:u}},
+once:function(){var a=Array.prototype.slice.call(arguments),b=a[1];a[1]=function(a){a.removeListener();return b.apply(this,arguments)};return this.on.apply(this,a)},capture:function(){CKEDITOR.event.useCapture=1;var a=this.on.apply(this,arguments);CKEDITOR.event.useCapture=0;return a},fire:function(){var a=0,b=function(){a=1},g=0,h=function(){g=1};return function(k,n,u){var f=d(this)[k];k=a;var D=g;a=g=0;if(f){var w=f.listeners;if(w.length)for(var w=w.slice(0),A,F=0;F<w.length;F++){if(f.errorProof)try{A=
+w[F].call(this,u,n,b,h)}catch(x){}else A=w[F].call(this,u,n,b,h);!1===A?g=1:"undefined"!=typeof A&&(n=A);if(a||g)break}}n=g?!1:"undefined"==typeof n?!0:n;a=k;g=D;return n}}(),fireOnce:function(a,b,g){b=this.fire(a,b,g);delete d(this)[a];return b},removeListener:function(a,b){var g=d(this)[a];if(g){var h=g.getListenerIndex(b);0<=h&&g.listeners.splice(h,1)}},removeAllListeners:function(){var a=d(this),b;for(b in a)delete a[b]},hasListeners:function(a){return(a=d(this)[a])&&0<a.listeners.length}}}());
+CKEDITOR.editor||(CKEDITOR.editor=function(){CKEDITOR._.pending.push([this,arguments]);CKEDITOR.event.call(this)},CKEDITOR.editor.prototype.fire=function(a,d){a in{instanceReady:1,loaded:1}&&(this[a]=!0);return CKEDITOR.event.prototype.fire.call(this,a,d,this)},CKEDITOR.editor.prototype.fireOnce=function(a,d){a in{instanceReady:1,loaded:1}&&(this[a]=!0);return CKEDITOR.event.prototype.fireOnce.call(this,a,d,this)},CKEDITOR.event.implementOn(CKEDITOR.editor.prototype));
+CKEDITOR.env||(CKEDITOR.env=function(){var a=navigator.userAgent.toLowerCase(),d=a.match(/edge[ \/](\d+.?\d*)/),b=-1<a.indexOf("trident/"),b=!(!d&&!b),b={ie:b,edge:!!d,webkit:!b&&-1<a.indexOf(" applewebkit/"),air:-1<a.indexOf(" adobeair/"),mac:-1<a.indexOf("macintosh"),quirks:"BackCompat"==document.compatMode&&(!document.documentMode||10>document.documentMode),mobile:-1<a.indexOf("mobile"),iOS:/(ipad|iphone|ipod)/.test(a),isCustomDomain:function(){if(!this.ie)return!1;var a=document.domain,b=window.location.hostname;
+return a!=b&&a!="["+b+"]"},secure:"https:"==location.protocol};b.gecko="Gecko"==navigator.product&&!b.webkit&&!b.ie;b.webkit&&(-1<a.indexOf("chrome")?b.chrome=!0:b.safari=!0);var c=0;b.ie&&(c=d?parseFloat(d[1]):b.quirks||!document.documentMode?parseFloat(a.match(/msie (\d+)/)[1]):document.documentMode,b.ie9Compat=9==c,b.ie8Compat=8==c,b.ie7Compat=7==c,b.ie6Compat=7>c||b.quirks);b.gecko&&(d=a.match(/rv:([\d\.]+)/))&&(d=d[1].split("."),c=1E4*d[0]+100*(d[1]||0)+1*(d[2]||0));b.air&&(c=parseFloat(a.match(/ adobeair\/(\d+)/)[1]));
+b.webkit&&(c=parseFloat(a.match(/ applewebkit\/(\d+)/)[1]));b.version=c;b.isCompatible=!(b.ie&&7>c)&&!(b.gecko&&4E4>c)&&!(b.webkit&&534>c);b.hidpi=2<=window.devicePixelRatio;b.needsBrFiller=b.gecko||b.webkit||b.ie&&10<c;b.needsNbspFiller=b.ie&&11>c;b.cssClass="cke_browser_"+(b.ie?"ie":b.gecko?"gecko":b.webkit?"webkit":"unknown");b.quirks&&(b.cssClass+=" cke_browser_quirks");b.ie&&(b.cssClass+=" cke_browser_ie"+(b.quirks?"6 cke_browser_iequirks":b.version));b.air&&(b.cssClass+=" cke_browser_air");
+b.iOS&&(b.cssClass+=" cke_browser_ios");b.hidpi&&(b.cssClass+=" cke_hidpi");return b}());
+"unloaded"==CKEDITOR.status&&function(){CKEDITOR.event.implementOn(CKEDITOR);CKEDITOR.loadFullCore=function(){if("basic_ready"!=CKEDITOR.status)CKEDITOR.loadFullCore._load=1;else{delete CKEDITOR.loadFullCore;var a=document.createElement("script");a.type="text/javascript";a.src=CKEDITOR.basePath+"ckeditor.js";document.getElementsByTagName("head")[0].appendChild(a)}};CKEDITOR.loadFullCoreTimeout=0;CKEDITOR.add=function(a){(this._.pending||(this._.pending=[])).push(a)};(function(){CKEDITOR.domReady(function(){var a=
+CKEDITOR.loadFullCore,d=CKEDITOR.loadFullCoreTimeout;a&&(CKEDITOR.status="basic_ready",a&&a._load?a():d&&setTimeout(function(){CKEDITOR.loadFullCore&&CKEDITOR.loadFullCore()},1E3*d))})})();CKEDITOR.status="basic_loaded"}();"use strict";CKEDITOR.VERBOSITY_WARN=1;CKEDITOR.VERBOSITY_ERROR=2;CKEDITOR.verbosity=CKEDITOR.VERBOSITY_WARN|CKEDITOR.VERBOSITY_ERROR;CKEDITOR.warn=function(a,d){CKEDITOR.verbosity&CKEDITOR.VERBOSITY_WARN&&CKEDITOR.fire("log",{type:"warn",errorCode:a,additionalData:d})};
+CKEDITOR.error=function(a,d){CKEDITOR.verbosity&CKEDITOR.VERBOSITY_ERROR&&CKEDITOR.fire("log",{type:"error",errorCode:a,additionalData:d})};
+CKEDITOR.on("log",function(a){if(window.console&&window.console.log){var d=console[a.data.type]?a.data.type:"log",b=a.data.errorCode;if(a=a.data.additionalData)console[d]("[CKEDITOR] Error code: "+b+".",a);else console[d]("[CKEDITOR] Error code: "+b+".");console[d]("[CKEDITOR] For more information about this error go to http://docs.ckeditor.com/#!/guide/dev_errors-section-"+b)}},null,null,999);CKEDITOR.dom={};
+(function(){var a=[],d=CKEDITOR.env.gecko?"-moz-":CKEDITOR.env.webkit?"-webkit-":CKEDITOR.env.ie?"-ms-":"",b=/&/g,c=/>/g,e=/</g,g=/"/g,h=/&(lt|gt|amp|quot|nbsp|shy|#\d{1,5});/g,k={lt:"\x3c",gt:"\x3e",amp:"\x26",quot:'"',nbsp:" ",shy:"­"},n=function(a,f){return"#"==f[0]?String.fromCharCode(parseInt(f.slice(1),10)):k[f]};CKEDITOR.on("reset",function(){a=[]});CKEDITOR.tools={arrayCompare:function(a,f){if(!a&&!f)return!0;if(!a||!f||a.length!=f.length)return!1;for(var b=0;b<a.length;b++)if(a[b]!=f[b])return!1;
+return!0},getIndex:function(a,f){for(var b=0;b<a.length;++b)if(f(a[b]))return b;return-1},clone:function(a){var f;if(a&&a instanceof Array){f=[];for(var b=0;b<a.length;b++)f[b]=CKEDITOR.tools.clone(a[b]);return f}if(null===a||"object"!=typeof a||a instanceof String||a instanceof Number||a instanceof Boolean||a instanceof Date||a instanceof RegExp||a.nodeType||a.window===a)return a;f=new a.constructor;for(b in a)f[b]=CKEDITOR.tools.clone(a[b]);return f},capitalize:function(a,f){return a.charAt(0).toUpperCase()+
+(f?a.slice(1):a.slice(1).toLowerCase())},extend:function(a){var f=arguments.length,b,c;"boolean"==typeof(b=arguments[f-1])?f--:"boolean"==typeof(b=arguments[f-2])&&(c=arguments[f-1],f-=2);for(var k=1;k<f;k++){var d=arguments[k],n;for(n in d)if(!0===b||null==a[n])if(!c||n in c)a[n]=d[n]}return a},prototypedCopy:function(a){var f=function(){};f.prototype=a;return new f},copy:function(a){var f={},b;for(b in a)f[b]=a[b];return f},isArray:function(a){return"[object Array]"==Object.prototype.toString.call(a)},
+isEmpty:function(a){for(var f in a)if(a.hasOwnProperty(f))return!1;return!0},cssVendorPrefix:function(a,f,b){if(b)return d+a+":"+f+";"+a+":"+f;b={};b[a]=f;b[d+a]=f;return b},cssStyleToDomStyle:function(){var a=document.createElement("div").style,f="undefined"!=typeof a.cssFloat?"cssFloat":"undefined"!=typeof a.styleFloat?"styleFloat":"float";return function(a){return"float"==a?f:a.replace(/-./g,function(a){return a.substr(1).toUpperCase()})}}(),buildStyleHtml:function(a){a=[].concat(a);for(var f,
+b=[],c=0;c<a.length;c++)if(f=a[c])/@import|[{}]/.test(f)?b.push("\x3cstyle\x3e"+f+"\x3c/style\x3e"):b.push('\x3clink type\x3d"text/css" rel\x3dstylesheet href\x3d"'+f+'"\x3e');return b.join("")},htmlEncode:function(a){return void 0===a||null===a?"":String(a).replace(b,"\x26amp;").replace(c,"\x26gt;").replace(e,"\x26lt;")},htmlDecode:function(a){return a.replace(h,n)},htmlEncodeAttr:function(a){return CKEDITOR.tools.htmlEncode(a).replace(g,"\x26quot;")},htmlDecodeAttr:function(a){return CKEDITOR.tools.htmlDecode(a)},
+transformPlainTextToHtml:function(a,f){var b=f==CKEDITOR.ENTER_BR,c=this.htmlEncode(a.replace(/\r\n/g,"\n")),c=c.replace(/\t/g,"\x26nbsp;\x26nbsp; \x26nbsp;"),k=f==CKEDITOR.ENTER_P?"p":"div";if(!b){var d=/\n{2}/g;if(d.test(c))var n="\x3c"+k+"\x3e",m="\x3c/"+k+"\x3e",c=n+c.replace(d,function(){return m+n})+m}c=c.replace(/\n/g,"\x3cbr\x3e");b||(c=c.replace(new RegExp("\x3cbr\x3e(?\x3d\x3c/"+k+"\x3e)"),function(a){return CKEDITOR.tools.repeat(a,2)}));c=c.replace(/^ | $/g,"\x26nbsp;");return c=c.replace(/(>|\s) /g,
+function(a,f){return f+"\x26nbsp;"}).replace(/ (?=<)/g,"\x26nbsp;")},getNextNumber:function(){var a=0;return function(){return++a}}(),getNextId:function(){return"cke_"+this.getNextNumber()},getUniqueId:function(){for(var a="e",f=0;8>f;f++)a+=Math.floor(65536*(1+Math.random())).toString(16).substring(1);return a},override:function(a,f){var b=f(a);b.prototype=a.prototype;return b},setTimeout:function(a,f,b,c,k){k||(k=window);b||(b=k);return k.setTimeout(function(){c?a.apply(b,[].concat(c)):a.apply(b)},
+f||0)},trim:function(){var a=/(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g;return function(f){return f.replace(a,"")}}(),ltrim:function(){var a=/^[ \t\n\r]+/g;return function(f){return f.replace(a,"")}}(),rtrim:function(){var a=/[ \t\n\r]+$/g;return function(f){return f.replace(a,"")}}(),indexOf:function(a,f){if("function"==typeof f)for(var b=0,c=a.length;b<c;b++){if(f(a[b]))return b}else{if(a.indexOf)return a.indexOf(f);b=0;for(c=a.length;b<c;b++)if(a[b]===f)return b}return-1},search:function(a,f){var b=CKEDITOR.tools.indexOf(a,
+f);return 0<=b?a[b]:null},bind:function(a,f){return function(){return a.apply(f,arguments)}},createClass:function(a){var f=a.$,b=a.base,c=a.privates||a._,k=a.proto;a=a.statics;!f&&(f=function(){b&&this.base.apply(this,arguments)});if(c)var d=f,f=function(){var a=this._||(this._={}),f;for(f in c){var b=c[f];a[f]="function"==typeof b?CKEDITOR.tools.bind(b,this):b}d.apply(this,arguments)};b&&(f.prototype=this.prototypedCopy(b.prototype),f.prototype.constructor=f,f.base=b,f.baseProto=b.prototype,f.prototype.base=
+function(){this.base=b.prototype.base;b.apply(this,arguments);this.base=arguments.callee});k&&this.extend(f.prototype,k,!0);a&&this.extend(f,a,!0);return f},addFunction:function(b,f){return a.push(function(){return b.apply(f||this,arguments)})-1},removeFunction:function(b){a[b]=null},callFunction:function(b){var f=a[b];return f&&f.apply(window,Array.prototype.slice.call(arguments,1))},cssLength:function(){var a=/^-?\d+\.?\d*px$/,f;return function(b){f=CKEDITOR.tools.trim(b+"")+"px";return a.test(f)?
+f:b||""}}(),convertToPx:function(){var a;return function(f){a||(a=CKEDITOR.dom.element.createFromHtml('\x3cdiv style\x3d"position:absolute;left:-9999px;top:-9999px;margin:0px;padding:0px;border:0px;"\x3e\x3c/div\x3e',CKEDITOR.document),CKEDITOR.document.getBody().append(a));return/%$/.test(f)?f:(a.setStyle("width",f),a.$.clientWidth)}}(),repeat:function(a,f){return Array(f+1).join(a)},tryThese:function(){for(var a,f=0,b=arguments.length;f<b;f++){var c=arguments[f];try{a=c();break}catch(k){}}return a},
+genKey:function(){return Array.prototype.slice.call(arguments).join("-")},defer:function(a){return function(){var f=arguments,b=this;window.setTimeout(function(){a.apply(b,f)},0)}},normalizeCssText:function(a,f){var b=[],c,k=CKEDITOR.tools.parseCssText(a,!0,f);for(c in k)b.push(c+":"+k[c]);b.sort();return b.length?b.join(";")+";":""},convertRgbToHex:function(a){return a.replace(/(?:rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\))/gi,function(a,b,c,k){a=[b,c,k];for(b=0;3>b;b++)a[b]=("0"+parseInt(a[b],10).toString(16)).slice(-2);
+return"#"+a.join("")})},normalizeHex:function(a){return a.replace(/#(([0-9a-f]{3}){1,2})($|;|\s+)/gi,function(a,b,c,k){a=b.toLowerCase();3==a.length&&(a=a.split(""),a=[a[0],a[0],a[1],a[1],a[2],a[2]].join(""));return"#"+a+k})},parseCssText:function(a,f,b){var c={};b&&(a=(new CKEDITOR.dom.element("span")).setAttribute("style",a).getAttribute("style")||"");a&&(a=CKEDITOR.tools.normalizeHex(CKEDITOR.tools.convertRgbToHex(a)));if(!a||";"==a)return c;a.replace(/&quot;/g,'"').replace(/\s*([^:;\s]+)\s*:\s*([^;]+)\s*(?=;|$)/g,
+function(a,b,k){f&&(b=b.toLowerCase(),"font-family"==b&&(k=k.replace(/\s*,\s*/g,",")),k=CKEDITOR.tools.trim(k));c[b]=k});return c},writeCssText:function(a,f){var b,c=[];for(b in a)c.push(b+":"+a[b]);f&&c.sort();return c.join("; ")},objectCompare:function(a,f,b){var c;if(!a&&!f)return!0;if(!a||!f)return!1;for(c in a)if(a[c]!=f[c])return!1;if(!b)for(c in f)if(a[c]!=f[c])return!1;return!0},objectKeys:function(a){var f=[],b;for(b in a)f.push(b);return f},convertArrayToObject:function(a,b){var c={};1==
+arguments.length&&(b=!0);for(var k=0,d=a.length;k<d;++k)c[a[k]]=b;return c},fixDomain:function(){for(var a;;)try{a=window.parent.document.domain;break}catch(b){a=a?a.replace(/.+?(?:\.|$)/,""):document.domain;if(!a)break;document.domain=a}return!!a},eventsBuffer:function(a,b,c){function k(){n=(new Date).getTime();d=!1;c?b.call(c):b()}var d,n=0;return{input:function(){if(!d){var b=(new Date).getTime()-n;b<a?d=setTimeout(k,a-b):k()}},reset:function(){d&&clearTimeout(d);d=n=0}}},enableHtml5Elements:function(a,
+b){for(var c="abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup main mark meter nav output progress section summary time video".split(" "),k=c.length,d;k--;)d=a.createElement(c[k]),b&&a.appendChild(d)},checkIfAnyArrayItemMatches:function(a,b){for(var c=0,k=a.length;c<k;++c)if(a[c].match(b))return!0;return!1},checkIfAnyObjectPropertyMatches:function(a,b){for(var c in a)if(c.match(b))return!0;return!1},keystrokeToString:function(a,b){var c=b&16711680,k=
+b&65535,d=CKEDITOR.env.mac,n=[],e=[];c&CKEDITOR.CTRL&&(n.push(d?"⌘":a[17]),e.push(d?a[224]:a[17]));c&CKEDITOR.ALT&&(n.push(d?"⌥":a[18]),e.push(a[18]));c&CKEDITOR.SHIFT&&(n.push(d?"⇧":a[16]),e.push(a[16]));k&&(a[k]?(n.push(a[k]),e.push(a[k])):(n.push(String.fromCharCode(k)),e.push(String.fromCharCode(k))));return{display:n.join("+"),aria:e.join("+")}},transparentImageData:"data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw\x3d\x3d",getCookie:function(a){a=a.toLowerCase();
+for(var b=document.cookie.split(";"),c,k,d=0;d<b.length;d++)if(c=b[d].split("\x3d"),k=decodeURIComponent(CKEDITOR.tools.trim(c[0]).toLowerCase()),k===a)return decodeURIComponent(1<c.length?c[1]:"");return null},setCookie:function(a,b){document.cookie=encodeURIComponent(a)+"\x3d"+encodeURIComponent(b)+";path\x3d/"},getCsrfToken:function(){var a=CKEDITOR.tools.getCookie("ckCsrfToken");if(!a||40!=a.length){var a=[],b="";if(window.crypto&&window.crypto.getRandomValues)a=new Uint8Array(40),window.crypto.getRandomValues(a);
+else for(var c=0;40>c;c++)a.push(Math.floor(256*Math.random()));for(c=0;c<a.length;c++)var k="abcdefghijklmnopqrstuvwxyz0123456789".charAt(a[c]%36),b=b+(.5<Math.random()?k.toUpperCase():k);a=b;CKEDITOR.tools.setCookie("ckCsrfToken",a)}return a},escapeCss:function(a){return a?window.CSS&&CSS.escape?CSS.escape(a):isNaN(parseInt(a.charAt(0),10))?a:"\\3"+a.charAt(0)+" "+a.substring(1,a.length):""},style:{parse:{_colors:{aliceblue:"#F0F8FF",antiquewhite:"#FAEBD7",aqua:"#00FFFF",aquamarine:"#7FFFD4",azure:"#F0FFFF",
+beige:"#F5F5DC",bisque:"#FFE4C4",black:"#000000",blanchedalmond:"#FFEBCD",blue:"#0000FF",blueviolet:"#8A2BE2",brown:"#A52A2A",burlywood:"#DEB887",cadetblue:"#5F9EA0",chartreuse:"#7FFF00",chocolate:"#D2691E",coral:"#FF7F50",cornflowerblue:"#6495ED",cornsilk:"#FFF8DC",crimson:"#DC143C",cyan:"#00FFFF",darkblue:"#00008B",darkcyan:"#008B8B",darkgoldenrod:"#B8860B",darkgray:"#A9A9A9",darkgreen:"#006400",darkgrey:"#A9A9A9",darkkhaki:"#BDB76B",darkmagenta:"#8B008B",darkolivegreen:"#556B2F",darkorange:"#FF8C00",
+darkorchid:"#9932CC",darkred:"#8B0000",darksalmon:"#E9967A",darkseagreen:"#8FBC8F",darkslateblue:"#483D8B",darkslategray:"#2F4F4F",darkslategrey:"#2F4F4F",darkturquoise:"#00CED1",darkviolet:"#9400D3",deeppink:"#FF1493",deepskyblue:"#00BFFF",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1E90FF",firebrick:"#B22222",floralwhite:"#FFFAF0",forestgreen:"#228B22",fuchsia:"#FF00FF",gainsboro:"#DCDCDC",ghostwhite:"#F8F8FF",gold:"#FFD700",goldenrod:"#DAA520",gray:"#808080",green:"#008000",greenyellow:"#ADFF2F",
+grey:"#808080",honeydew:"#F0FFF0",hotpink:"#FF69B4",indianred:"#CD5C5C",indigo:"#4B0082",ivory:"#FFFFF0",khaki:"#F0E68C",lavender:"#E6E6FA",lavenderblush:"#FFF0F5",lawngreen:"#7CFC00",lemonchiffon:"#FFFACD",lightblue:"#ADD8E6",lightcoral:"#F08080",lightcyan:"#E0FFFF",lightgoldenrodyellow:"#FAFAD2",lightgray:"#D3D3D3",lightgreen:"#90EE90",lightgrey:"#D3D3D3",lightpink:"#FFB6C1",lightsalmon:"#FFA07A",lightseagreen:"#20B2AA",lightskyblue:"#87CEFA",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#B0C4DE",
+lightyellow:"#FFFFE0",lime:"#00FF00",limegreen:"#32CD32",linen:"#FAF0E6",magenta:"#FF00FF",maroon:"#800000",mediumaquamarine:"#66CDAA",mediumblue:"#0000CD",mediumorchid:"#BA55D3",mediumpurple:"#9370DB",mediumseagreen:"#3CB371",mediumslateblue:"#7B68EE",mediumspringgreen:"#00FA9A",mediumturquoise:"#48D1CC",mediumvioletred:"#C71585",midnightblue:"#191970",mintcream:"#F5FFFA",mistyrose:"#FFE4E1",moccasin:"#FFE4B5",navajowhite:"#FFDEAD",navy:"#000080",oldlace:"#FDF5E6",olive:"#808000",olivedrab:"#6B8E23",
+orange:"#FFA500",orangered:"#FF4500",orchid:"#DA70D6",palegoldenrod:"#EEE8AA",palegreen:"#98FB98",paleturquoise:"#AFEEEE",palevioletred:"#DB7093",papayawhip:"#FFEFD5",peachpuff:"#FFDAB9",peru:"#CD853F",pink:"#FFC0CB",plum:"#DDA0DD",powderblue:"#B0E0E6",purple:"#800080",rebeccapurple:"#663399",red:"#FF0000",rosybrown:"#BC8F8F",royalblue:"#4169E1",saddlebrown:"#8B4513",salmon:"#FA8072",sandybrown:"#F4A460",seagreen:"#2E8B57",seashell:"#FFF5EE",sienna:"#A0522D",silver:"#C0C0C0",skyblue:"#87CEEB",slateblue:"#6A5ACD",
+slategray:"#708090",slategrey:"#708090",snow:"#FFFAFA",springgreen:"#00FF7F",steelblue:"#4682B4",tan:"#D2B48C",teal:"#008080",thistle:"#D8BFD8",tomato:"#FF6347",turquoise:"#40E0D0",violet:"#EE82EE",wheat:"#F5DEB3",white:"#FFFFFF",whitesmoke:"#F5F5F5",yellow:"#FFFF00",yellowgreen:"#9ACD32"},_rgbaRegExp:/rgba?\(\s*\d+%?\s*,\s*\d+%?\s*,\s*\d+%?\s*(?:,\s*[0-9.]+\s*)?\)/gi,_hslaRegExp:/hsla?\(\s*[0-9.]+\s*,\s*\d+%\s*,\s*\d+%\s*(?:,\s*[0-9.]+\s*)?\)/gi,background:function(a){var b={},c=this._findColor(a);
+c.length&&(b.color=c[0],CKEDITOR.tools.array.forEach(c,function(b){a=a.replace(b,"")}));if(a=CKEDITOR.tools.trim(a))b.unprocessed=a;return b},margin:function(a){function b(a){c.top=k[a[0]];c.right=k[a[1]];c.bottom=k[a[2]];c.left=k[a[3]]}var c={},k=a.match(/(?:\-?[\.\d]+(?:%|\w*)|auto|inherit|initial|unset)/g)||["0px"];switch(k.length){case 1:b([0,0,0,0]);break;case 2:b([0,1,0,1]);break;case 3:b([0,1,2,1]);break;case 4:b([0,1,2,3])}return c},_findColor:function(a){var b=[],c=CKEDITOR.tools.array,b=
+b.concat(a.match(this._rgbaRegExp)||[]),b=b.concat(a.match(this._hslaRegExp)||[]);return b=b.concat(c.filter(a.split(/\s+/),function(a){return a.match(/^\#[a-f0-9]{3}(?:[a-f0-9]{3})?$/gi)?!0:a.toLowerCase()in CKEDITOR.tools.style.parse._colors}))}}},array:{filter:function(a,b,c){var k=[];this.forEach(a,function(d,n){b.call(c,d,n,a)&&k.push(d)});return k},forEach:function(a,b,c){var k=a.length,d;for(d=0;d<k;d++)b.call(c,a[d],d,a)},map:function(a,b,c){for(var k=[],d=0;d<a.length;d++)k.push(b.call(c,
+a[d],d,a));return k},reduce:function(a,b,c,k){for(var d=0;d<a.length;d++)c=b.call(k,c,a[d],d,a);return c}},object:{findKey:function(a,b){if("object"!==typeof a)return null;for(var c in a)if(a[c]===b)return c;return null}}};CKEDITOR.tools.array.indexOf=CKEDITOR.tools.indexOf;CKEDITOR.tools.array.isArray=CKEDITOR.tools.isArray})();
+CKEDITOR.dtd=function(){var a=CKEDITOR.tools.extend,d=function(a,b){for(var c=CKEDITOR.tools.clone(a),k=1;k<arguments.length;k++){b=arguments[k];for(var d in b)delete c[d]}return c},b={},c={},e={address:1,article:1,aside:1,blockquote:1,details:1,div:1,dl:1,fieldset:1,figure:1,footer:1,form:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,header:1,hgroup:1,hr:1,main:1,menu:1,nav:1,ol:1,p:1,pre:1,section:1,table:1,ul:1},g={command:1,link:1,meta:1,noscript:1,script:1,style:1},h={},k={"#":1},n={center:1,dir:1,noframes:1};
+a(b,{a:1,abbr:1,area:1,audio:1,b:1,bdi:1,bdo:1,br:1,button:1,canvas:1,cite:1,code:1,command:1,datalist:1,del:1,dfn:1,em:1,embed:1,i:1,iframe:1,img:1,input:1,ins:1,kbd:1,keygen:1,label:1,map:1,mark:1,meter:1,noscript:1,object:1,output:1,progress:1,q:1,ruby:1,s:1,samp:1,script:1,select:1,small:1,span:1,strong:1,sub:1,sup:1,textarea:1,time:1,u:1,"var":1,video:1,wbr:1},k,{acronym:1,applet:1,basefont:1,big:1,font:1,isindex:1,strike:1,style:1,tt:1});a(c,e,b,n);d={a:d(b,{a:1,button:1}),abbr:b,address:c,
+area:h,article:c,aside:c,audio:a({source:1,track:1},c),b:b,base:h,bdi:b,bdo:b,blockquote:c,body:c,br:h,button:d(b,{a:1,button:1}),canvas:b,caption:c,cite:b,code:b,col:h,colgroup:{col:1},command:h,datalist:a({option:1},b),dd:c,del:b,details:a({summary:1},c),dfn:b,div:c,dl:{dt:1,dd:1},dt:c,em:b,embed:h,fieldset:a({legend:1},c),figcaption:c,figure:a({figcaption:1},c),footer:c,form:c,h1:b,h2:b,h3:b,h4:b,h5:b,h6:b,head:a({title:1,base:1},g),header:c,hgroup:{h1:1,h2:1,h3:1,h4:1,h5:1,h6:1},hr:h,html:a({head:1,
+body:1},c,g),i:b,iframe:k,img:h,input:h,ins:b,kbd:b,keygen:h,label:b,legend:b,li:c,link:h,main:c,map:c,mark:b,menu:a({li:1},c),meta:h,meter:d(b,{meter:1}),nav:c,noscript:a({link:1,meta:1,style:1},b),object:a({param:1},b),ol:{li:1},optgroup:{option:1},option:k,output:b,p:b,param:h,pre:b,progress:d(b,{progress:1}),q:b,rp:b,rt:b,ruby:a({rp:1,rt:1},b),s:b,samp:b,script:k,section:c,select:{optgroup:1,option:1},small:b,source:h,span:b,strong:b,style:k,sub:b,summary:a({h1:1,h2:1,h3:1,h4:1,h5:1,h6:1},b),
+sup:b,table:{caption:1,colgroup:1,thead:1,tfoot:1,tbody:1,tr:1},tbody:{tr:1},td:c,textarea:k,tfoot:{tr:1},th:c,thead:{tr:1},time:d(b,{time:1}),title:k,tr:{th:1,td:1},track:h,u:b,ul:{li:1},"var":b,video:a({source:1,track:1},c),wbr:h,acronym:b,applet:a({param:1},c),basefont:h,big:b,center:c,dialog:h,dir:{li:1},font:b,isindex:h,noframes:c,strike:b,tt:b};a(d,{$block:a({audio:1,dd:1,dt:1,figcaption:1,li:1,video:1},e,n),$blockLimit:{article:1,aside:1,audio:1,body:1,caption:1,details:1,dir:1,div:1,dl:1,
+fieldset:1,figcaption:1,figure:1,footer:1,form:1,header:1,hgroup:1,main:1,menu:1,nav:1,ol:1,section:1,table:1,td:1,th:1,tr:1,ul:1,video:1},$cdata:{script:1,style:1},$editable:{address:1,article:1,aside:1,blockquote:1,body:1,details:1,div:1,fieldset:1,figcaption:1,footer:1,form:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,header:1,hgroup:1,main:1,nav:1,p:1,pre:1,section:1},$empty:{area:1,base:1,basefont:1,br:1,col:1,command:1,dialog:1,embed:1,hr:1,img:1,input:1,isindex:1,keygen:1,link:1,meta:1,param:1,source:1,
+track:1,wbr:1},$inline:b,$list:{dl:1,ol:1,ul:1},$listItem:{dd:1,dt:1,li:1},$nonBodyContent:a({body:1,head:1,html:1},d.head),$nonEditable:{applet:1,audio:1,button:1,embed:1,iframe:1,map:1,object:1,option:1,param:1,script:1,textarea:1,video:1},$object:{applet:1,audio:1,button:1,hr:1,iframe:1,img:1,input:1,object:1,select:1,table:1,textarea:1,video:1},$removeEmpty:{abbr:1,acronym:1,b:1,bdi:1,bdo:1,big:1,cite:1,code:1,del:1,dfn:1,em:1,font:1,i:1,ins:1,label:1,kbd:1,mark:1,meter:1,output:1,q:1,ruby:1,
+s:1,samp:1,small:1,span:1,strike:1,strong:1,sub:1,sup:1,time:1,tt:1,u:1,"var":1},$tabIndex:{a:1,area:1,button:1,input:1,object:1,select:1,textarea:1},$tableContent:{caption:1,col:1,colgroup:1,tbody:1,td:1,tfoot:1,th:1,thead:1,tr:1},$transparent:{a:1,audio:1,canvas:1,del:1,ins:1,map:1,noscript:1,object:1,video:1},$intermediate:{caption:1,colgroup:1,dd:1,dt:1,figcaption:1,legend:1,li:1,optgroup:1,option:1,rp:1,rt:1,summary:1,tbody:1,td:1,tfoot:1,th:1,thead:1,tr:1}});return d}();
 CKEDITOR.dom.event=function(a){this.$=a};
-CKEDITOR.dom.event.prototype={getKey:function(){return this.$.keyCode||this.$.which},getKeystroke:function(){var a=this.getKey();if(this.$.ctrlKey||this.$.metaKey)a=a+CKEDITOR.CTRL;this.$.shiftKey&&(a=a+CKEDITOR.SHIFT);this.$.altKey&&(a=a+CKEDITOR.ALT);return a},preventDefault:function(a){var e=this.$;e.preventDefault?e.preventDefault():e.returnValue=false;a&&this.stopPropagation()},stopPropagation:function(){var a=this.$;a.stopPropagation?a.stopPropagation():a.cancelBubble=true},getTarget:function(){var a=
+CKEDITOR.dom.event.prototype={getKey:function(){return this.$.keyCode||this.$.which},getKeystroke:function(){var a=this.getKey();if(this.$.ctrlKey||this.$.metaKey)a+=CKEDITOR.CTRL;this.$.shiftKey&&(a+=CKEDITOR.SHIFT);this.$.altKey&&(a+=CKEDITOR.ALT);return a},preventDefault:function(a){var d=this.$;d.preventDefault?d.preventDefault():d.returnValue=!1;a&&this.stopPropagation()},stopPropagation:function(){var a=this.$;a.stopPropagation?a.stopPropagation():a.cancelBubble=!0},getTarget:function(){var a=
 this.$.target||this.$.srcElement;return a?new CKEDITOR.dom.node(a):null},getPhase:function(){return this.$.eventPhase||2},getPageOffset:function(){var a=this.getTarget().getDocument().$;return{x:this.$.pageX||this.$.clientX+(a.documentElement.scrollLeft||a.body.scrollLeft),y:this.$.pageY||this.$.clientY+(a.documentElement.scrollTop||a.body.scrollTop)}}};CKEDITOR.CTRL=1114112;CKEDITOR.SHIFT=2228224;CKEDITOR.ALT=4456448;CKEDITOR.EVENT_PHASE_CAPTURING=1;CKEDITOR.EVENT_PHASE_AT_TARGET=2;
-CKEDITOR.EVENT_PHASE_BUBBLING=3;CKEDITOR.dom.domObject=function(a){if(a)this.$=a};
-CKEDITOR.dom.domObject.prototype=function(){var a=function(a,b){return function(c){typeof CKEDITOR!="undefined"&&a.fire(b,new CKEDITOR.dom.event(c))}};return{getPrivate:function(){var a;if(!(a=this.getCustomData("_")))this.setCustomData("_",a={});return a},on:function(e){var b=this.getCustomData("_cke_nativeListeners");if(!b){b={};this.setCustomData("_cke_nativeListeners",b)}if(!b[e]){b=b[e]=a(this,e);this.$.addEventListener?this.$.addEventListener(e,b,!!CKEDITOR.event.useCapture):this.$.attachEvent&&
-this.$.attachEvent("on"+e,b)}return CKEDITOR.event.prototype.on.apply(this,arguments)},removeListener:function(a){CKEDITOR.event.prototype.removeListener.apply(this,arguments);if(!this.hasListeners(a)){var b=this.getCustomData("_cke_nativeListeners"),c=b&&b[a];if(c){this.$.removeEventListener?this.$.removeEventListener(a,c,false):this.$.detachEvent&&this.$.detachEvent("on"+a,c);delete b[a]}}},removeAllListeners:function(){var a=this.getCustomData("_cke_nativeListeners"),b;for(b in a){var c=a[b];this.$.detachEvent?
-this.$.detachEvent("on"+b,c):this.$.removeEventListener&&this.$.removeEventListener(b,c,false);delete a[b]}}}}();
-(function(a){var e={};CKEDITOR.on("reset",function(){e={}});a.equals=function(a){try{return a&&a.$===this.$}catch(c){return false}};a.setCustomData=function(a,c){var d=this.getUniqueId();(e[d]||(e[d]={}))[a]=c;return this};a.getCustomData=function(a){var c=this.$["data-cke-expando"];return(c=c&&e[c])&&a in c?c[a]:null};a.removeCustomData=function(a){var c=this.$["data-cke-expando"],c=c&&e[c],d,h;if(c){d=c[a];h=a in c;delete c[a]}return h?d:null};a.clearCustomData=function(){this.removeAllListeners();
-var a=this.$["data-cke-expando"];a&&delete e[a]};a.getUniqueId=function(){return this.$["data-cke-expando"]||(this.$["data-cke-expando"]=CKEDITOR.tools.getNextNumber())};CKEDITOR.event.implementOn(a)})(CKEDITOR.dom.domObject.prototype);
+CKEDITOR.EVENT_PHASE_BUBBLING=3;CKEDITOR.dom.domObject=function(a){a&&(this.$=a)};
+CKEDITOR.dom.domObject.prototype=function(){var a=function(a,b){return function(c){"undefined"!=typeof CKEDITOR&&a.fire(b,new CKEDITOR.dom.event(c))}};return{getPrivate:function(){var a;(a=this.getCustomData("_"))||this.setCustomData("_",a={});return a},on:function(d){var b=this.getCustomData("_cke_nativeListeners");b||(b={},this.setCustomData("_cke_nativeListeners",b));b[d]||(b=b[d]=a(this,d),this.$.addEventListener?this.$.addEventListener(d,b,!!CKEDITOR.event.useCapture):this.$.attachEvent&&this.$.attachEvent("on"+
+d,b));return CKEDITOR.event.prototype.on.apply(this,arguments)},removeListener:function(a){CKEDITOR.event.prototype.removeListener.apply(this,arguments);if(!this.hasListeners(a)){var b=this.getCustomData("_cke_nativeListeners"),c=b&&b[a];c&&(this.$.removeEventListener?this.$.removeEventListener(a,c,!1):this.$.detachEvent&&this.$.detachEvent("on"+a,c),delete b[a])}},removeAllListeners:function(){var a=this.getCustomData("_cke_nativeListeners"),b;for(b in a){var c=a[b];this.$.detachEvent?this.$.detachEvent("on"+
+b,c):this.$.removeEventListener&&this.$.removeEventListener(b,c,!1);delete a[b]}CKEDITOR.event.prototype.removeAllListeners.call(this)}}}();
+(function(a){var d={};CKEDITOR.on("reset",function(){d={}});a.equals=function(a){try{return a&&a.$===this.$}catch(c){return!1}};a.setCustomData=function(a,c){var e=this.getUniqueId();(d[e]||(d[e]={}))[a]=c;return this};a.getCustomData=function(a){var c=this.$["data-cke-expando"];return(c=c&&d[c])&&a in c?c[a]:null};a.removeCustomData=function(a){var c=this.$["data-cke-expando"],c=c&&d[c],e,g;c&&(e=c[a],g=a in c,delete c[a]);return g?e:null};a.clearCustomData=function(){this.removeAllListeners();var a=
+this.$["data-cke-expando"];a&&delete d[a]};a.getUniqueId=function(){return this.$["data-cke-expando"]||(this.$["data-cke-expando"]=CKEDITOR.tools.getNextNumber())};CKEDITOR.event.implementOn(a)})(CKEDITOR.dom.domObject.prototype);
 CKEDITOR.dom.node=function(a){return a?new CKEDITOR.dom[a.nodeType==CKEDITOR.NODE_DOCUMENT?"document":a.nodeType==CKEDITOR.NODE_ELEMENT?"element":a.nodeType==CKEDITOR.NODE_TEXT?"text":a.nodeType==CKEDITOR.NODE_COMMENT?"comment":a.nodeType==CKEDITOR.NODE_DOCUMENT_FRAGMENT?"documentFragment":"domObject"](a):this};CKEDITOR.dom.node.prototype=new CKEDITOR.dom.domObject;CKEDITOR.NODE_ELEMENT=1;CKEDITOR.NODE_DOCUMENT=9;CKEDITOR.NODE_TEXT=3;CKEDITOR.NODE_COMMENT=8;CKEDITOR.NODE_DOCUMENT_FRAGMENT=11;
 CKEDITOR.POSITION_IDENTICAL=0;CKEDITOR.POSITION_DISCONNECTED=1;CKEDITOR.POSITION_FOLLOWING=2;CKEDITOR.POSITION_PRECEDING=4;CKEDITOR.POSITION_IS_CONTAINED=8;CKEDITOR.POSITION_CONTAINS=16;
-CKEDITOR.tools.extend(CKEDITOR.dom.node.prototype,{appendTo:function(a,e){a.append(this,e);return a},clone:function(a,e){var b=this.$.cloneNode(a),c=function(d){d["data-cke-expando"]&&(d["data-cke-expando"]=false);if(d.nodeType==CKEDITOR.NODE_ELEMENT){e||d.removeAttribute("id",false);if(a)for(var d=d.childNodes,b=0;b<d.length;b++)c(d[b])}};c(b);return new CKEDITOR.dom.node(b)},hasPrevious:function(){return!!this.$.previousSibling},hasNext:function(){return!!this.$.nextSibling},insertAfter:function(a){a.$.parentNode.insertBefore(this.$,
-a.$.nextSibling);return a},insertBefore:function(a){a.$.parentNode.insertBefore(this.$,a.$);return a},insertBeforeMe:function(a){this.$.parentNode.insertBefore(a.$,this.$);return a},getAddress:function(a){for(var e=[],b=this.getDocument().$.documentElement,c=this.$;c&&c!=b;){var d=c.parentNode;d&&e.unshift(this.getIndex.call({$:c},a));c=d}return e},getDocument:function(){return new CKEDITOR.dom.document(this.$.ownerDocument||this.$.parentNode.ownerDocument)},getIndex:function(a){var e=this.$,b=-1,
-c;if(!this.$.parentNode)return b;do if(!a||!(e!=this.$&&e.nodeType==CKEDITOR.NODE_TEXT&&(c||!e.nodeValue))){b++;c=e.nodeType==CKEDITOR.NODE_TEXT}while(e=e.previousSibling);return b},getNextSourceNode:function(a,e,b){if(b&&!b.call)var c=b,b=function(a){return!a.equals(c)};var a=!a&&this.getFirst&&this.getFirst(),d;if(!a){if(this.type==CKEDITOR.NODE_ELEMENT&&b&&b(this,true)===false)return null;a=this.getNext()}for(;!a&&(d=(d||this).getParent());){if(b&&b(d,true)===false)return null;a=d.getNext()}return!a||
-b&&b(a)===false?null:e&&e!=a.type?a.getNextSourceNode(false,e,b):a},getPreviousSourceNode:function(a,e,b){if(b&&!b.call)var c=b,b=function(a){return!a.equals(c)};var a=!a&&this.getLast&&this.getLast(),d;if(!a){if(this.type==CKEDITOR.NODE_ELEMENT&&b&&b(this,true)===false)return null;a=this.getPrevious()}for(;!a&&(d=(d||this).getParent());){if(b&&b(d,true)===false)return null;a=d.getPrevious()}return!a||b&&b(a)===false?null:e&&a.type!=e?a.getPreviousSourceNode(false,e,b):a},getPrevious:function(a){var e=
-this.$,b;do b=(e=e.previousSibling)&&e.nodeType!=10&&new CKEDITOR.dom.node(e);while(b&&a&&!a(b));return b},getNext:function(a){var e=this.$,b;do b=(e=e.nextSibling)&&new CKEDITOR.dom.node(e);while(b&&a&&!a(b));return b},getParent:function(a){var e=this.$.parentNode;return e&&(e.nodeType==CKEDITOR.NODE_ELEMENT||a&&e.nodeType==CKEDITOR.NODE_DOCUMENT_FRAGMENT)?new CKEDITOR.dom.node(e):null},getParents:function(a){var e=this,b=[];do b[a?"push":"unshift"](e);while(e=e.getParent());return b},getCommonAncestor:function(a){if(a.equals(this))return this;
-if(a.contains&&a.contains(this))return a;var e=this.contains?this:this.getParent();do if(e.contains(a))return e;while(e=e.getParent());return null},getPosition:function(a){var e=this.$,b=a.$;if(e.compareDocumentPosition)return e.compareDocumentPosition(b);if(e==b)return CKEDITOR.POSITION_IDENTICAL;if(this.type==CKEDITOR.NODE_ELEMENT&&a.type==CKEDITOR.NODE_ELEMENT){if(e.contains){if(e.contains(b))return CKEDITOR.POSITION_CONTAINS+CKEDITOR.POSITION_PRECEDING;if(b.contains(e))return CKEDITOR.POSITION_IS_CONTAINED+
-CKEDITOR.POSITION_FOLLOWING}if("sourceIndex"in e)return e.sourceIndex<0||b.sourceIndex<0?CKEDITOR.POSITION_DISCONNECTED:e.sourceIndex<b.sourceIndex?CKEDITOR.POSITION_PRECEDING:CKEDITOR.POSITION_FOLLOWING}for(var e=this.getAddress(),a=a.getAddress(),b=Math.min(e.length,a.length),c=0;c<=b-1;c++)if(e[c]!=a[c]){if(c<b)return e[c]<a[c]?CKEDITOR.POSITION_PRECEDING:CKEDITOR.POSITION_FOLLOWING;break}return e.length<a.length?CKEDITOR.POSITION_CONTAINS+CKEDITOR.POSITION_PRECEDING:CKEDITOR.POSITION_IS_CONTAINED+
-CKEDITOR.POSITION_FOLLOWING},getAscendant:function(a,e){var b=this.$,c;if(!e)b=b.parentNode;for(;b;){if(b.nodeName&&(c=b.nodeName.toLowerCase(),typeof a=="string"?c==a:c in a))return new CKEDITOR.dom.node(b);try{b=b.parentNode}catch(d){b=null}}return null},hasAscendant:function(a,e){var b=this.$;if(!e)b=b.parentNode;for(;b;){if(b.nodeName&&b.nodeName.toLowerCase()==a)return true;b=b.parentNode}return false},move:function(a,e){a.append(this.remove(),e)},remove:function(a){var e=this.$,b=e.parentNode;
-if(b){if(a)for(;a=e.firstChild;)b.insertBefore(e.removeChild(a),e);b.removeChild(e)}return this},replace:function(a){this.insertBefore(a);a.remove()},trim:function(){this.ltrim();this.rtrim()},ltrim:function(){for(var a;this.getFirst&&(a=this.getFirst());){if(a.type==CKEDITOR.NODE_TEXT){var e=CKEDITOR.tools.ltrim(a.getText()),b=a.getLength();if(e){if(e.length<b){a.split(b-e.length);this.$.removeChild(this.$.firstChild)}}else{a.remove();continue}}break}},rtrim:function(){for(var a;this.getLast&&(a=
-this.getLast());){if(a.type==CKEDITOR.NODE_TEXT){var e=CKEDITOR.tools.rtrim(a.getText()),b=a.getLength();if(e){if(e.length<b){a.split(e.length);this.$.lastChild.parentNode.removeChild(this.$.lastChild)}}else{a.remove();continue}}break}if(CKEDITOR.env.needsBrFiller)(a=this.$.lastChild)&&(a.type==1&&a.nodeName.toLowerCase()=="br")&&a.parentNode.removeChild(a)},isReadOnly:function(){var a=this;this.type!=CKEDITOR.NODE_ELEMENT&&(a=this.getParent());if(a&&typeof a.$.isContentEditable!="undefined")return!(a.$.isContentEditable||
-a.data("cke-editable"));for(;a;){if(a.data("cke-editable"))break;if(a.getAttribute("contentEditable")=="false")return true;if(a.getAttribute("contentEditable")=="true")break;a=a.getParent()}return!a}});CKEDITOR.dom.window=function(a){CKEDITOR.dom.domObject.call(this,a)};CKEDITOR.dom.window.prototype=new CKEDITOR.dom.domObject;
-CKEDITOR.tools.extend(CKEDITOR.dom.window.prototype,{focus:function(){this.$.focus()},getViewPaneSize:function(){var a=this.$.document,e=a.compatMode=="CSS1Compat";return{width:(e?a.documentElement.clientWidth:a.body.clientWidth)||0,height:(e?a.documentElement.clientHeight:a.body.clientHeight)||0}},getScrollPosition:function(){var a=this.$;if("pageXOffset"in a)return{x:a.pageXOffset||0,y:a.pageYOffset||0};a=a.document;return{x:a.documentElement.scrollLeft||a.body.scrollLeft||0,y:a.documentElement.scrollTop||
+CKEDITOR.tools.extend(CKEDITOR.dom.node.prototype,{appendTo:function(a,d){a.append(this,d);return a},clone:function(a,d){function b(c){c["data-cke-expando"]&&(c["data-cke-expando"]=!1);if(c.nodeType==CKEDITOR.NODE_ELEMENT||c.nodeType==CKEDITOR.NODE_DOCUMENT_FRAGMENT)if(d||c.nodeType!=CKEDITOR.NODE_ELEMENT||c.removeAttribute("id",!1),a){c=c.childNodes;for(var e=0;e<c.length;e++)b(c[e])}}function c(b){if(b.type==CKEDITOR.NODE_ELEMENT||b.type==CKEDITOR.NODE_DOCUMENT_FRAGMENT){if(b.type!=CKEDITOR.NODE_DOCUMENT_FRAGMENT){var d=
+b.getName();":"==d[0]&&b.renameNode(d.substring(1))}if(a)for(d=0;d<b.getChildCount();d++)c(b.getChild(d))}}var e=this.$.cloneNode(a);b(e);e=new CKEDITOR.dom.node(e);CKEDITOR.env.ie&&9>CKEDITOR.env.version&&(this.type==CKEDITOR.NODE_ELEMENT||this.type==CKEDITOR.NODE_DOCUMENT_FRAGMENT)&&c(e);return e},hasPrevious:function(){return!!this.$.previousSibling},hasNext:function(){return!!this.$.nextSibling},insertAfter:function(a){a.$.parentNode.insertBefore(this.$,a.$.nextSibling);return a},insertBefore:function(a){a.$.parentNode.insertBefore(this.$,
+a.$);return a},insertBeforeMe:function(a){this.$.parentNode.insertBefore(a.$,this.$);return a},getAddress:function(a){for(var d=[],b=this.getDocument().$.documentElement,c=this.$;c&&c!=b;){var e=c.parentNode;e&&d.unshift(this.getIndex.call({$:c},a));c=e}return d},getDocument:function(){return new CKEDITOR.dom.document(this.$.ownerDocument||this.$.parentNode.ownerDocument)},getIndex:function(a){function d(a,c){var n=c?a.nextSibling:a.previousSibling;return n&&n.nodeType==CKEDITOR.NODE_TEXT?b(n)?d(n,
+c):n:null}function b(a){return!a.nodeValue||a.nodeValue==CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE}var c=this.$,e=-1,g;if(!this.$.parentNode||a&&c.nodeType==CKEDITOR.NODE_TEXT&&b(c)&&!d(c)&&!d(c,!0))return-1;do a&&c!=this.$&&c.nodeType==CKEDITOR.NODE_TEXT&&(g||b(c))||(e++,g=c.nodeType==CKEDITOR.NODE_TEXT);while(c=c.previousSibling);return e},getNextSourceNode:function(a,d,b){if(b&&!b.call){var c=b;b=function(a){return!a.equals(c)}}a=!a&&this.getFirst&&this.getFirst();var e;if(!a){if(this.type==
+CKEDITOR.NODE_ELEMENT&&b&&!1===b(this,!0))return null;a=this.getNext()}for(;!a&&(e=(e||this).getParent());){if(b&&!1===b(e,!0))return null;a=e.getNext()}return!a||b&&!1===b(a)?null:d&&d!=a.type?a.getNextSourceNode(!1,d,b):a},getPreviousSourceNode:function(a,d,b){if(b&&!b.call){var c=b;b=function(a){return!a.equals(c)}}a=!a&&this.getLast&&this.getLast();var e;if(!a){if(this.type==CKEDITOR.NODE_ELEMENT&&b&&!1===b(this,!0))return null;a=this.getPrevious()}for(;!a&&(e=(e||this).getParent());){if(b&&!1===
+b(e,!0))return null;a=e.getPrevious()}return!a||b&&!1===b(a)?null:d&&a.type!=d?a.getPreviousSourceNode(!1,d,b):a},getPrevious:function(a){var d=this.$,b;do b=(d=d.previousSibling)&&10!=d.nodeType&&new CKEDITOR.dom.node(d);while(b&&a&&!a(b));return b},getNext:function(a){var d=this.$,b;do b=(d=d.nextSibling)&&new CKEDITOR.dom.node(d);while(b&&a&&!a(b));return b},getParent:function(a){var d=this.$.parentNode;return d&&(d.nodeType==CKEDITOR.NODE_ELEMENT||a&&d.nodeType==CKEDITOR.NODE_DOCUMENT_FRAGMENT)?
+new CKEDITOR.dom.node(d):null},getParents:function(a){var d=this,b=[];do b[a?"push":"unshift"](d);while(d=d.getParent());return b},getCommonAncestor:function(a){if(a.equals(this))return this;if(a.contains&&a.contains(this))return a;var d=this.contains?this:this.getParent();do if(d.contains(a))return d;while(d=d.getParent());return null},getPosition:function(a){var d=this.$,b=a.$;if(d.compareDocumentPosition)return d.compareDocumentPosition(b);if(d==b)return CKEDITOR.POSITION_IDENTICAL;if(this.type==
+CKEDITOR.NODE_ELEMENT&&a.type==CKEDITOR.NODE_ELEMENT){if(d.contains){if(d.contains(b))return CKEDITOR.POSITION_CONTAINS+CKEDITOR.POSITION_PRECEDING;if(b.contains(d))return CKEDITOR.POSITION_IS_CONTAINED+CKEDITOR.POSITION_FOLLOWING}if("sourceIndex"in d)return 0>d.sourceIndex||0>b.sourceIndex?CKEDITOR.POSITION_DISCONNECTED:d.sourceIndex<b.sourceIndex?CKEDITOR.POSITION_PRECEDING:CKEDITOR.POSITION_FOLLOWING}d=this.getAddress();a=a.getAddress();for(var b=Math.min(d.length,a.length),c=0;c<b;c++)if(d[c]!=
+a[c])return d[c]<a[c]?CKEDITOR.POSITION_PRECEDING:CKEDITOR.POSITION_FOLLOWING;return d.length<a.length?CKEDITOR.POSITION_CONTAINS+CKEDITOR.POSITION_PRECEDING:CKEDITOR.POSITION_IS_CONTAINED+CKEDITOR.POSITION_FOLLOWING},getAscendant:function(a,d){var b=this.$,c,e;d||(b=b.parentNode);"function"==typeof a?(e=!0,c=a):(e=!1,c=function(b){b="string"==typeof b.nodeName?b.nodeName.toLowerCase():"";return"string"==typeof a?b==a:b in a});for(;b;){if(c(e?new CKEDITOR.dom.node(b):b))return new CKEDITOR.dom.node(b);
+try{b=b.parentNode}catch(g){b=null}}return null},hasAscendant:function(a,d){var b=this.$;d||(b=b.parentNode);for(;b;){if(b.nodeName&&b.nodeName.toLowerCase()==a)return!0;b=b.parentNode}return!1},move:function(a,d){a.append(this.remove(),d)},remove:function(a){var d=this.$,b=d.parentNode;if(b){if(a)for(;a=d.firstChild;)b.insertBefore(d.removeChild(a),d);b.removeChild(d)}return this},replace:function(a){this.insertBefore(a);a.remove()},trim:function(){this.ltrim();this.rtrim()},ltrim:function(){for(var a;this.getFirst&&
+(a=this.getFirst());){if(a.type==CKEDITOR.NODE_TEXT){var d=CKEDITOR.tools.ltrim(a.getText()),b=a.getLength();if(d)d.length<b&&(a.split(b-d.length),this.$.removeChild(this.$.firstChild));else{a.remove();continue}}break}},rtrim:function(){for(var a;this.getLast&&(a=this.getLast());){if(a.type==CKEDITOR.NODE_TEXT){var d=CKEDITOR.tools.rtrim(a.getText()),b=a.getLength();if(d)d.length<b&&(a.split(d.length),this.$.lastChild.parentNode.removeChild(this.$.lastChild));else{a.remove();continue}}break}CKEDITOR.env.needsBrFiller&&
+(a=this.$.lastChild)&&1==a.type&&"br"==a.nodeName.toLowerCase()&&a.parentNode.removeChild(a)},isReadOnly:function(a){var d=this;this.type!=CKEDITOR.NODE_ELEMENT&&(d=this.getParent());CKEDITOR.env.edge&&d&&d.is("textarea","input")&&(a=!0);if(!a&&d&&"undefined"!=typeof d.$.isContentEditable)return!(d.$.isContentEditable||d.data("cke-editable"));for(;d;){if(d.data("cke-editable"))return!1;if(d.hasAttribute("contenteditable"))return"false"==d.getAttribute("contenteditable");d=d.getParent()}return!0}});
+CKEDITOR.dom.window=function(a){CKEDITOR.dom.domObject.call(this,a)};CKEDITOR.dom.window.prototype=new CKEDITOR.dom.domObject;
+CKEDITOR.tools.extend(CKEDITOR.dom.window.prototype,{focus:function(){this.$.focus()},getViewPaneSize:function(){var a=this.$.document,d="CSS1Compat"==a.compatMode;return{width:(d?a.documentElement.clientWidth:a.body.clientWidth)||0,height:(d?a.documentElement.clientHeight:a.body.clientHeight)||0}},getScrollPosition:function(){var a=this.$;if("pageXOffset"in a)return{x:a.pageXOffset||0,y:a.pageYOffset||0};a=a.document;return{x:a.documentElement.scrollLeft||a.body.scrollLeft||0,y:a.documentElement.scrollTop||
 a.body.scrollTop||0}},getFrame:function(){var a=this.$.frameElement;return a?new CKEDITOR.dom.element.get(a):null}});CKEDITOR.dom.document=function(a){CKEDITOR.dom.domObject.call(this,a)};CKEDITOR.dom.document.prototype=new CKEDITOR.dom.domObject;
-CKEDITOR.tools.extend(CKEDITOR.dom.document.prototype,{type:CKEDITOR.NODE_DOCUMENT,appendStyleSheet:function(a){if(this.$.createStyleSheet)this.$.createStyleSheet(a);else{var e=new CKEDITOR.dom.element("link");e.setAttributes({rel:"stylesheet",type:"text/css",href:a});this.getHead().append(e)}},appendStyleText:function(a){if(this.$.createStyleSheet){var e=this.$.createStyleSheet("");e.cssText=a}else{var b=new CKEDITOR.dom.element("style",this);b.append(new CKEDITOR.dom.text(a,this));this.getHead().append(b)}return e||
-b.$.sheet},createElement:function(a,e){var b=new CKEDITOR.dom.element(a,this);if(e){e.attributes&&b.setAttributes(e.attributes);e.styles&&b.setStyles(e.styles)}return b},createText:function(a){return new CKEDITOR.dom.text(a,this)},focus:function(){this.getWindow().focus()},getActive:function(){return new CKEDITOR.dom.element(this.$.activeElement)},getById:function(a){return(a=this.$.getElementById(a))?new CKEDITOR.dom.element(a):null},getByAddress:function(a,e){for(var b=this.$.documentElement,c=
-0;b&&c<a.length;c++){var d=a[c];if(e)for(var h=-1,g=0;g<b.childNodes.length;g++){var n=b.childNodes[g];if(!(e===true&&n.nodeType==3&&n.previousSibling&&n.previousSibling.nodeType==3)){h++;if(h==d){b=n;break}}}else b=b.childNodes[d]}return b?new CKEDITOR.dom.node(b):null},getElementsByTag:function(a,e){if((!CKEDITOR.env.ie||document.documentMode>8)&&e)a=e+":"+a;return new CKEDITOR.dom.nodeList(this.$.getElementsByTagName(a))},getHead:function(){var a=this.$.getElementsByTagName("head")[0];return a=
-a?new CKEDITOR.dom.element(a):this.getDocumentElement().append(new CKEDITOR.dom.element("head"),true)},getBody:function(){return new CKEDITOR.dom.element(this.$.body)},getDocumentElement:function(){return new CKEDITOR.dom.element(this.$.documentElement)},getWindow:function(){return new CKEDITOR.dom.window(this.$.parentWindow||this.$.defaultView)},write:function(a){this.$.open("text/html","replace");CKEDITOR.env.ie&&(a=a.replace(/(?:^\s*<!DOCTYPE[^>]*?>)|^/i,'$&\n<script data-cke-temp="1">('+CKEDITOR.tools.fixDomain+
-")();<\/script>"));this.$.write(a);this.$.close()},find:function(a){return new CKEDITOR.dom.nodeList(this.$.querySelectorAll(a))},findOne:function(a){return(a=this.$.querySelector(a))?new CKEDITOR.dom.element(a):null},_getHtml5ShivFrag:function(){var a=this.getCustomData("html5ShivFrag");if(!a){a=this.$.createDocumentFragment();CKEDITOR.tools.enableHtml5Elements(a,true);this.setCustomData("html5ShivFrag",a)}return a}});CKEDITOR.dom.nodeList=function(a){this.$=a};
-CKEDITOR.dom.nodeList.prototype={count:function(){return this.$.length},getItem:function(a){if(a<0||a>=this.$.length)return null;return(a=this.$[a])?new CKEDITOR.dom.node(a):null}};CKEDITOR.dom.element=function(a,e){typeof a=="string"&&(a=(e?e.$:document).createElement(a));CKEDITOR.dom.domObject.call(this,a)};CKEDITOR.dom.element.get=function(a){return(a=typeof a=="string"?document.getElementById(a)||document.getElementsByName(a)[0]:a)&&(a.$?a:new CKEDITOR.dom.element(a))};
-CKEDITOR.dom.element.prototype=new CKEDITOR.dom.node;CKEDITOR.dom.element.createFromHtml=function(a,e){var b=new CKEDITOR.dom.element("div",e);b.setHtml(a);return b.getFirst().remove()};
-CKEDITOR.dom.element.setMarker=function(a,e,b,c){var d=e.getCustomData("list_marker_id")||e.setCustomData("list_marker_id",CKEDITOR.tools.getNextNumber()).getCustomData("list_marker_id"),h=e.getCustomData("list_marker_names")||e.setCustomData("list_marker_names",{}).getCustomData("list_marker_names");a[d]=e;h[b]=1;return e.setCustomData(b,c)};CKEDITOR.dom.element.clearAllMarkers=function(a){for(var e in a)CKEDITOR.dom.element.clearMarkers(a,a[e],1)};
-CKEDITOR.dom.element.clearMarkers=function(a,e,b){var c=e.getCustomData("list_marker_names"),d=e.getCustomData("list_marker_id"),h;for(h in c)e.removeCustomData(h);e.removeCustomData("list_marker_names");if(b){e.removeCustomData("list_marker_id");delete a[d]}};
-(function(){function a(a){var b=true;if(!a.$.id){a.$.id="cke_tmp_"+CKEDITOR.tools.getNextNumber();b=false}return function(){b||a.removeAttribute("id")}}function e(a,b){return"#"+a.$.id+" "+b.split(/,\s*/).join(", #"+a.$.id+" ")}function b(a){for(var b=0,e=0,n=c[a].length;e<n;e++)b=b+(parseInt(this.getComputedStyle(c[a][e])||0,10)||0);return b}CKEDITOR.tools.extend(CKEDITOR.dom.element.prototype,{type:CKEDITOR.NODE_ELEMENT,addClass:function(a){var b=this.$.className;b&&(RegExp("(?:^|\\s)"+a+"(?:\\s|$)",
-"").test(b)||(b=b+(" "+a)));this.$.className=b||a},removeClass:function(a){var b=this.getAttribute("class");if(b){a=RegExp("(?:^|\\s+)"+a+"(?=\\s|$)","i");if(a.test(b))(b=b.replace(a,"").replace(/^\s+/,""))?this.setAttribute("class",b):this.removeAttribute("class")}return this},hasClass:function(a){return RegExp("(?:^|\\s+)"+a+"(?=\\s|$)","").test(this.getAttribute("class"))},append:function(a,b){typeof a=="string"&&(a=this.getDocument().createElement(a));b?this.$.insertBefore(a.$,this.$.firstChild):
-this.$.appendChild(a.$);return a},appendHtml:function(a){if(this.$.childNodes.length){var b=new CKEDITOR.dom.element("div",this.getDocument());b.setHtml(a);b.moveChildren(this)}else this.setHtml(a)},appendText:function(a){this.$.text!=void 0?this.$.text=this.$.text+a:this.append(new CKEDITOR.dom.text(a))},appendBogus:function(a){if(a||CKEDITOR.env.needsBrFiller||CKEDITOR.env.opera){for(a=this.getLast();a&&a.type==CKEDITOR.NODE_TEXT&&!CKEDITOR.tools.rtrim(a.getText());)a=a.getPrevious();if(!a||!a.is||
-!a.is("br")){a=CKEDITOR.env.opera?this.getDocument().createText(""):this.getDocument().createElement("br");CKEDITOR.env.gecko&&a.setAttribute("type","_moz");this.append(a)}}},breakParent:function(a){var b=new CKEDITOR.dom.range(this.getDocument());b.setStartAfter(this);b.setEndAfter(a);a=b.extractContents();b.insertNode(this.remove());a.insertAfterNode(this)},contains:CKEDITOR.env.ie||CKEDITOR.env.webkit?function(a){var b=this.$;return a.type!=CKEDITOR.NODE_ELEMENT?b.contains(a.getParent().$):b!=
-a.$&&b.contains(a.$)}:function(a){return!!(this.$.compareDocumentPosition(a.$)&16)},focus:function(){function a(){try{this.$.focus()}catch(b){}}return function(b){b?CKEDITOR.tools.setTimeout(a,100,this):a.call(this)}}(),getHtml:function(){var a=this.$.innerHTML;return CKEDITOR.env.ie?a.replace(/<\?[^>]*>/g,""):a},getOuterHtml:function(){if(this.$.outerHTML)return this.$.outerHTML.replace(/<\?[^>]*>/,"");var a=this.$.ownerDocument.createElement("div");a.appendChild(this.$.cloneNode(true));return a.innerHTML},
-getClientRect:function(){var a=CKEDITOR.tools.extend({},this.$.getBoundingClientRect());!a.width&&(a.width=a.right-a.left);!a.height&&(a.height=a.bottom-a.top);return a},setHtml:CKEDITOR.env.ie&&CKEDITOR.env.version<9?function(a){try{var b=this.$;if(this.getParent())return b.innerHTML=a;var c=this.getDocument()._getHtml5ShivFrag();c.appendChild(b);b.innerHTML=a;c.removeChild(b);return a}catch(e){this.$.innerHTML="";b=new CKEDITOR.dom.element("body",this.getDocument());b.$.innerHTML=a;for(b=b.getChildren();b.count();)this.append(b.getItem(0));
-return a}}:function(a){return this.$.innerHTML=a},setText:function(a){CKEDITOR.dom.element.prototype.setText=this.$.innerText!=void 0?function(a){return this.$.innerText=a}:function(a){return this.$.textContent=a};return this.setText(a)},getAttribute:function(){var a=function(a){return this.$.getAttribute(a,2)};return CKEDITOR.env.ie&&(CKEDITOR.env.ie7Compat||CKEDITOR.env.ie6Compat)?function(a){switch(a){case "class":a="className";break;case "http-equiv":a="httpEquiv";break;case "name":return this.$.name;
-case "tabindex":a=this.$.getAttribute(a,2);a!==0&&this.$.tabIndex===0&&(a=null);return a;case "checked":a=this.$.attributes.getNamedItem(a);return(a.specified?a.nodeValue:this.$.checked)?"checked":null;case "hspace":case "value":return this.$[a];case "style":return this.$.style.cssText;case "contenteditable":case "contentEditable":return this.$.attributes.getNamedItem("contentEditable").specified?this.$.getAttribute("contentEditable"):null}return this.$.getAttribute(a,2)}:a}(),getChildren:function(){return new CKEDITOR.dom.nodeList(this.$.childNodes)},
-getComputedStyle:CKEDITOR.env.ie?function(a){return this.$.currentStyle[CKEDITOR.tools.cssStyleToDomStyle(a)]}:function(a){var b=this.getWindow().$.getComputedStyle(this.$,null);return b?b.getPropertyValue(a):""},getDtd:function(){var a=CKEDITOR.dtd[this.getName()];this.getDtd=function(){return a};return a},getElementsByTag:CKEDITOR.dom.document.prototype.getElementsByTag,getTabIndex:CKEDITOR.env.ie?function(){var a=this.$.tabIndex;a===0&&(!CKEDITOR.dtd.$tabIndex[this.getName()]&&parseInt(this.getAttribute("tabindex"),
-10)!==0)&&(a=-1);return a}:CKEDITOR.env.webkit?function(){var a=this.$.tabIndex;if(a==void 0){a=parseInt(this.getAttribute("tabindex"),10);isNaN(a)&&(a=-1)}return a}:function(){return this.$.tabIndex},getText:function(){return this.$.textContent||this.$.innerText||""},getWindow:function(){return this.getDocument().getWindow()},getId:function(){return this.$.id||null},getNameAtt:function(){return this.$.name||null},getName:function(){var a=this.$.nodeName.toLowerCase();if(CKEDITOR.env.ie&&!(document.documentMode>
-8)){var b=this.$.scopeName;b!="HTML"&&(a=b.toLowerCase()+":"+a)}return(this.getName=function(){return a})()},getValue:function(){return this.$.value},getFirst:function(a){var b=this.$.firstChild;(b=b&&new CKEDITOR.dom.node(b))&&(a&&!a(b))&&(b=b.getNext(a));return b},getLast:function(a){var b=this.$.lastChild;(b=b&&new CKEDITOR.dom.node(b))&&(a&&!a(b))&&(b=b.getPrevious(a));return b},getStyle:function(a){return this.$.style[CKEDITOR.tools.cssStyleToDomStyle(a)]},is:function(){var a=this.getName();
-if(typeof arguments[0]=="object")return!!arguments[0][a];for(var b=0;b<arguments.length;b++)if(arguments[b]==a)return true;return false},isEditable:function(a){var b=this.getName();if(this.isReadOnly()||this.getComputedStyle("display")=="none"||this.getComputedStyle("visibility")=="hidden"||CKEDITOR.dtd.$nonEditable[b]||CKEDITOR.dtd.$empty[b]||this.is("a")&&(this.data("cke-saved-name")||this.hasAttribute("name"))&&!this.getChildCount())return false;if(a!==false){a=CKEDITOR.dtd[b]||CKEDITOR.dtd.span;
-return!(!a||!a["#"])}return true},isIdentical:function(a){var b=this.clone(0,1),a=a.clone(0,1);b.removeAttributes(["_moz_dirty","data-cke-expando","data-cke-saved-href","data-cke-saved-name"]);a.removeAttributes(["_moz_dirty","data-cke-expando","data-cke-saved-href","data-cke-saved-name"]);if(b.$.isEqualNode){b.$.style.cssText=CKEDITOR.tools.normalizeCssText(b.$.style.cssText);a.$.style.cssText=CKEDITOR.tools.normalizeCssText(a.$.style.cssText);return b.$.isEqualNode(a.$)}b=b.getOuterHtml();a=a.getOuterHtml();
-if(CKEDITOR.env.ie&&CKEDITOR.env.version<9&&this.is("a")){var c=this.getParent();if(c.type==CKEDITOR.NODE_ELEMENT){c=c.clone();c.setHtml(b);b=c.getHtml();c.setHtml(a);a=c.getHtml()}}return b==a},isVisible:function(){var a=(this.$.offsetHeight||this.$.offsetWidth)&&this.getComputedStyle("visibility")!="hidden",b,c;if(a&&(CKEDITOR.env.webkit||CKEDITOR.env.opera)){b=this.getWindow();if(!b.equals(CKEDITOR.document.getWindow())&&(c=b.$.frameElement))a=(new CKEDITOR.dom.element(c)).isVisible()}return!!a},
-isEmptyInlineRemoveable:function(){if(!CKEDITOR.dtd.$removeEmpty[this.getName()])return false;for(var a=this.getChildren(),b=0,c=a.count();b<c;b++){var e=a.getItem(b);if(!(e.type==CKEDITOR.NODE_ELEMENT&&e.data("cke-bookmark"))&&(e.type==CKEDITOR.NODE_ELEMENT&&!e.isEmptyInlineRemoveable()||e.type==CKEDITOR.NODE_TEXT&&CKEDITOR.tools.trim(e.getText())))return false}return true},hasAttributes:CKEDITOR.env.ie&&(CKEDITOR.env.ie7Compat||CKEDITOR.env.ie6Compat)?function(){for(var a=this.$.attributes,b=0;b<
-a.length;b++){var c=a[b];switch(c.nodeName){case "class":if(this.getAttribute("class"))return true;case "data-cke-expando":continue;default:if(c.specified)return true}}return false}:function(){var a=this.$.attributes,b=a.length,c={"data-cke-expando":1,_moz_dirty:1};return b>0&&(b>2||!c[a[0].nodeName]||b==2&&!c[a[1].nodeName])},hasAttribute:function(){function a(b){b=this.$.attributes.getNamedItem(b);return!(!b||!b.specified)}return CKEDITOR.env.ie&&CKEDITOR.env.version<8?function(b){return b=="name"?
-!!this.$.name:a.call(this,b)}:a}(),hide:function(){this.setStyle("display","none")},moveChildren:function(a,b){var c=this.$,a=a.$;if(c!=a){var e;if(b)for(;e=c.lastChild;)a.insertBefore(c.removeChild(e),a.firstChild);else for(;e=c.firstChild;)a.appendChild(c.removeChild(e))}},mergeSiblings:function(){function a(b,d,c){if(d&&d.type==CKEDITOR.NODE_ELEMENT){for(var e=[];d.data("cke-bookmark")||d.isEmptyInlineRemoveable();){e.push(d);d=c?d.getNext():d.getPrevious();if(!d||d.type!=CKEDITOR.NODE_ELEMENT)return}if(b.isIdentical(d)){for(var j=
-c?b.getLast():b.getFirst();e.length;)e.shift().move(b,!c);d.moveChildren(b,!c);d.remove();j&&j.type==CKEDITOR.NODE_ELEMENT&&j.mergeSiblings()}}}return function(b){if(b===false||CKEDITOR.dtd.$removeEmpty[this.getName()]||this.is("a")){a(this,this.getNext(),true);a(this,this.getPrevious())}}}(),show:function(){this.setStyles({display:"",visibility:""})},setAttribute:function(){var a=function(a,b){this.$.setAttribute(a,b);return this};return CKEDITOR.env.ie&&(CKEDITOR.env.ie7Compat||CKEDITOR.env.ie6Compat)?
-function(b,c){b=="class"?this.$.className=c:b=="style"?this.$.style.cssText=c:b=="tabindex"?this.$.tabIndex=c:b=="checked"?this.$.checked=c:b=="contenteditable"?a.call(this,"contentEditable",c):a.apply(this,arguments);return this}:CKEDITOR.env.ie8Compat&&CKEDITOR.env.secure?function(b,c){if(b=="src"&&c.match(/^http:\/\//))try{a.apply(this,arguments)}catch(e){}else a.apply(this,arguments);return this}:a}(),setAttributes:function(a){for(var b in a)this.setAttribute(b,a[b]);return this},setValue:function(a){this.$.value=
-a;return this},removeAttribute:function(){var a=function(a){this.$.removeAttribute(a)};return CKEDITOR.env.ie&&(CKEDITOR.env.ie7Compat||CKEDITOR.env.ie6Compat)?function(a){a=="class"?a="className":a=="tabindex"?a="tabIndex":a=="contenteditable"&&(a="contentEditable");this.$.removeAttribute(a)}:a}(),removeAttributes:function(a){if(CKEDITOR.tools.isArray(a))for(var b=0;b<a.length;b++)this.removeAttribute(a[b]);else for(b in a)a.hasOwnProperty(b)&&this.removeAttribute(b)},removeStyle:function(a){var b=
-this.$.style;if(!b.removeProperty&&(a=="border"||a=="margin"||a=="padding")){var c=["top","left","right","bottom"],e;a=="border"&&(e=["color","style","width"]);for(var b=[],i=0;i<c.length;i++)if(e)for(var j=0;j<e.length;j++)b.push([a,c[i],e[j]].join("-"));else b.push([a,c[i]].join("-"));for(a=0;a<b.length;a++)this.removeStyle(b[a])}else{b.removeProperty?b.removeProperty(a):b.removeAttribute(CKEDITOR.tools.cssStyleToDomStyle(a));this.$.style.cssText||this.removeAttribute("style")}},setStyle:function(a,
-b){this.$.style[CKEDITOR.tools.cssStyleToDomStyle(a)]=b;return this},setStyles:function(a){for(var b in a)this.setStyle(b,a[b]);return this},setOpacity:function(a){if(CKEDITOR.env.ie&&CKEDITOR.env.version<9){a=Math.round(a*100);this.setStyle("filter",a>=100?"":"progid:DXImageTransform.Microsoft.Alpha(opacity="+a+")")}else this.setStyle("opacity",a)},unselectable:function(){this.setStyles(CKEDITOR.tools.cssVendorPrefix("user-select","none"));if(CKEDITOR.env.ie||CKEDITOR.env.opera){this.setAttribute("unselectable",
-"on");for(var a,b=this.getElementsByTag("*"),c=0,e=b.count();c<e;c++){a=b.getItem(c);a.setAttribute("unselectable","on")}}},getPositionedAncestor:function(){for(var a=this;a.getName()!="html";){if(a.getComputedStyle("position")!="static")return a;a=a.getParent()}return null},getDocumentPosition:function(a){var b=0,c=0,e=this.getDocument(),i=e.getBody(),j=e.$.compatMode=="BackCompat";if(document.documentElement.getBoundingClientRect){var o=this.$.getBoundingClientRect(),q=e.$.documentElement,s=q.clientTop||
-i.$.clientTop||0,u=q.clientLeft||i.$.clientLeft||0,f=true;if(CKEDITOR.env.ie){f=e.getDocumentElement().contains(this);e=e.getBody().contains(this);f=j&&e||!j&&f}if(f){b=o.left+(!j&&q.scrollLeft||i.$.scrollLeft);b=b-u;c=o.top+(!j&&q.scrollTop||i.$.scrollTop);c=c-s}}else{i=this;for(e=null;i&&!(i.getName()=="body"||i.getName()=="html");){b=b+(i.$.offsetLeft-i.$.scrollLeft);c=c+(i.$.offsetTop-i.$.scrollTop);if(!i.equals(this)){b=b+(i.$.clientLeft||0);c=c+(i.$.clientTop||0)}for(;e&&!e.equals(i);){b=b-
-e.$.scrollLeft;c=c-e.$.scrollTop;e=e.getParent()}e=i;i=(o=i.$.offsetParent)?new CKEDITOR.dom.element(o):null}}if(a){i=this.getWindow();e=a.getWindow();if(!i.equals(e)&&i.$.frameElement){a=(new CKEDITOR.dom.element(i.$.frameElement)).getDocumentPosition(a);b=b+a.x;c=c+a.y}}if(!document.documentElement.getBoundingClientRect&&CKEDITOR.env.gecko&&!j){b=b+(this.$.clientLeft?1:0);c=c+(this.$.clientTop?1:0)}return{x:b,y:c}},scrollIntoView:function(a){var b=this.getParent();if(b){do{(b.$.clientWidth&&b.$.clientWidth<
-b.$.scrollWidth||b.$.clientHeight&&b.$.clientHeight<b.$.scrollHeight)&&!b.is("body")&&this.scrollIntoParent(b,a,1);if(b.is("html")){var c=b.getWindow();try{var e=c.$.frameElement;e&&(b=new CKEDITOR.dom.element(e))}catch(i){}}}while(b=b.getParent())}},scrollIntoParent:function(a,b,c){var e,i,j,o;function q(b,f){if(/body|html/.test(a.getName()))a.getWindow().$.scrollBy(b,f);else{a.$.scrollLeft=a.$.scrollLeft+b;a.$.scrollTop=a.$.scrollTop+f}}function s(a,b){var d={x:0,y:0};if(!a.is(f?"body":"html")){var c=
-a.$.getBoundingClientRect();d.x=c.left;d.y=c.top}c=a.getWindow();if(!c.equals(b)){c=s(CKEDITOR.dom.element.get(c.$.frameElement),b);d.x=d.x+c.x;d.y=d.y+c.y}return d}function u(a,b){return parseInt(a.getComputedStyle("margin-"+b)||0,10)||0}!a&&(a=this.getWindow());j=a.getDocument();var f=j.$.compatMode=="BackCompat";a instanceof CKEDITOR.dom.window&&(a=f?j.getBody():j.getDocumentElement());j=a.getWindow();i=s(this,j);var p=s(a,j),y=this.$.offsetHeight;e=this.$.offsetWidth;var m=a.$.clientHeight,k=
-a.$.clientWidth;j=i.x-u(this,"left")-p.x||0;o=i.y-u(this,"top")-p.y||0;e=i.x+e+u(this,"right")-(p.x+k)||0;i=i.y+y+u(this,"bottom")-(p.y+m)||0;if(o<0||i>0)q(0,b===true?o:b===false?i:o<0?o:i);if(c&&(j<0||e>0))q(j<0?j:e,0)},setState:function(a,b,c){b=b||"cke";switch(a){case CKEDITOR.TRISTATE_ON:this.addClass(b+"_on");this.removeClass(b+"_off");this.removeClass(b+"_disabled");c&&this.setAttribute("aria-pressed",true);c&&this.removeAttribute("aria-disabled");break;case CKEDITOR.TRISTATE_DISABLED:this.addClass(b+
-"_disabled");this.removeClass(b+"_off");this.removeClass(b+"_on");c&&this.setAttribute("aria-disabled",true);c&&this.removeAttribute("aria-pressed");break;default:this.addClass(b+"_off");this.removeClass(b+"_on");this.removeClass(b+"_disabled");c&&this.removeAttribute("aria-pressed");c&&this.removeAttribute("aria-disabled")}},getFrameDocument:function(){var a=this.$;try{a.contentWindow.document}catch(b){a.src=a.src}return a&&new CKEDITOR.dom.document(a.contentWindow.document)},copyAttributes:function(a,
-b){for(var c=this.$.attributes,b=b||{},e=0;e<c.length;e++){var i=c[e],j=i.nodeName.toLowerCase(),o;if(!(j in b))if(j=="checked"&&(o=this.getAttribute(j)))a.setAttribute(j,o);else if(i.specified||CKEDITOR.env.ie&&i.nodeValue&&j=="value"){o=this.getAttribute(j);if(o===null)o=i.nodeValue;a.setAttribute(j,o)}}if(this.$.style.cssText!=="")a.$.style.cssText=this.$.style.cssText},renameNode:function(a){if(this.getName()!=a){var b=this.getDocument(),a=new CKEDITOR.dom.element(a,b);this.copyAttributes(a);
-this.moveChildren(a);this.getParent()&&this.$.parentNode.replaceChild(a.$,this.$);a.$["data-cke-expando"]=this.$["data-cke-expando"];this.$=a.$}},getChild:function(){function a(b,c){var d=b.childNodes;if(c>=0&&c<d.length)return d[c]}return function(b){var c=this.$;if(b.slice)for(;b.length>0&&c;)c=a(c,b.shift());else c=a(c,b);return c?new CKEDITOR.dom.node(c):null}}(),getChildCount:function(){return this.$.childNodes.length},disableContextMenu:function(){this.on("contextmenu",function(a){a.data.getTarget().hasClass("cke_enable_context_menu")||
-a.data.preventDefault()})},getDirection:function(a){return a?this.getComputedStyle("direction")||this.getDirection()||this.getParent()&&this.getParent().getDirection(1)||this.getDocument().$.dir||"ltr":this.getStyle("direction")||this.getAttribute("dir")},data:function(a,b){a="data-"+a;if(b===void 0)return this.getAttribute(a);b===false?this.removeAttribute(a):this.setAttribute(a,b);return null},getEditor:function(){var a=CKEDITOR.instances,b,c;for(b in a){c=a[b];if(c.element.equals(this)&&c.elementMode!=
-CKEDITOR.ELEMENT_MODE_APPENDTO)return c}return null},find:function(b){var c=a(this),b=new CKEDITOR.dom.nodeList(this.$.querySelectorAll(e(this,b)));c();return b},findOne:function(b){var c=a(this),b=this.$.querySelector(e(this,b));c();return b?new CKEDITOR.dom.element(b):null},forEach:function(a,b,c){if(!c&&(!b||this.type==b))var e=a(this);if(e!==false)for(var c=this.getChildren(),i=0;i<c.count();i++){e=c.getItem(i);e.type==CKEDITOR.NODE_ELEMENT?e.forEach(a,b):(!b||e.type==b)&&a(e)}}});var c={width:["border-left-width",
-"border-right-width","padding-left","padding-right"],height:["border-top-width","border-bottom-width","padding-top","padding-bottom"]};CKEDITOR.dom.element.prototype.setSize=function(a,c,e){if(typeof c=="number"){if(e&&(!CKEDITOR.env.ie||!CKEDITOR.env.quirks))c=c-b.call(this,a);this.setStyle(a,c+"px")}};CKEDITOR.dom.element.prototype.getSize=function(a,c){var e=Math.max(this.$["offset"+CKEDITOR.tools.capitalize(a)],this.$["client"+CKEDITOR.tools.capitalize(a)])||0;c&&(e=e-b.call(this,a));return e}})();
-CKEDITOR.dom.documentFragment=function(a){a=a||CKEDITOR.document;this.$=a.type==CKEDITOR.NODE_DOCUMENT?a.$.createDocumentFragment():a};
-CKEDITOR.tools.extend(CKEDITOR.dom.documentFragment.prototype,CKEDITOR.dom.element.prototype,{type:CKEDITOR.NODE_DOCUMENT_FRAGMENT,insertAfterNode:function(a){a=a.$;a.parentNode.insertBefore(this.$,a.nextSibling)}},!0,{append:1,appendBogus:1,getFirst:1,getLast:1,getParent:1,getNext:1,getPrevious:1,appendTo:1,moveChildren:1,insertBefore:1,insertAfterNode:1,replace:1,trim:1,type:1,ltrim:1,rtrim:1,getDocument:1,getChildCount:1,getChild:1,getChildren:1});
-(function(){function a(a,b){var c=this.range;if(this._.end)return null;if(!this._.start){this._.start=1;if(c.collapsed){this.end();return null}c.optimize()}var f,d=c.startContainer;f=c.endContainer;var o=c.startOffset,e=c.endOffset,k,l=this.guard,t=this.type,h=a?"getPreviousSourceNode":"getNextSourceNode";if(!a&&!this._.guardLTR){var r=f.type==CKEDITOR.NODE_ELEMENT?f:f.getParent(),i=f.type==CKEDITOR.NODE_ELEMENT?f.getChild(e):f.getNext();this._.guardLTR=function(a,b){return(!b||!r.equals(a))&&(!i||
-!a.equals(i))&&(a.type!=CKEDITOR.NODE_ELEMENT||!b||!a.equals(c.root))}}if(a&&!this._.guardRTL){var g=d.type==CKEDITOR.NODE_ELEMENT?d:d.getParent(),j=d.type==CKEDITOR.NODE_ELEMENT?o?d.getChild(o-1):null:d.getPrevious();this._.guardRTL=function(a,b){return(!b||!g.equals(a))&&(!j||!a.equals(j))&&(a.type!=CKEDITOR.NODE_ELEMENT||!b||!a.equals(c.root))}}var n=a?this._.guardRTL:this._.guardLTR;k=l?function(a,b){return n(a,b)===false?false:l(a,b)}:n;if(this.current)f=this.current[h](false,t,k);else{if(a)f.type==
-CKEDITOR.NODE_ELEMENT&&(f=e>0?f.getChild(e-1):k(f,true)===false?null:f.getPreviousSourceNode(true,t,k));else{f=d;if(f.type==CKEDITOR.NODE_ELEMENT&&!(f=f.getChild(o)))f=k(d,true)===false?null:d.getNextSourceNode(true,t,k)}f&&k(f)===false&&(f=null)}for(;f&&!this._.end;){this.current=f;if(!this.evaluator||this.evaluator(f)!==false){if(!b)return f}else if(b&&this.evaluator)return false;f=f[h](false,t,k)}this.end();return this.current=null}function e(b){for(var c,d=null;c=a.call(this,b);)d=c;return d}
-function b(a){if(j(a))return false;if(a.type==CKEDITOR.NODE_TEXT)return true;if(a.type==CKEDITOR.NODE_ELEMENT){if(a.is(CKEDITOR.dtd.$inline)||a.getAttribute("contenteditable")=="false")return true;var b;if(b=!CKEDITOR.env.needsBrFiller)if(b=a.is(o))a:{b=0;for(var c=a.getChildCount();b<c;++b)if(!j(a.getChild(b))){b=false;break a}b=true}if(b)return true}return false}CKEDITOR.dom.walker=CKEDITOR.tools.createClass({$:function(a){this.range=a;this._={}},proto:{end:function(){this._.end=1},next:function(){return a.call(this)},
-previous:function(){return a.call(this,1)},checkForward:function(){return a.call(this,0,1)!==false},checkBackward:function(){return a.call(this,1,1)!==false},lastForward:function(){return e.call(this)},lastBackward:function(){return e.call(this,1)},reset:function(){delete this.current;this._={}}}});var c={block:1,"list-item":1,table:1,"table-row-group":1,"table-header-group":1,"table-footer-group":1,"table-row":1,"table-column-group":1,"table-column":1,"table-cell":1,"table-caption":1},d={absolute:1,
-fixed:1};CKEDITOR.dom.element.prototype.isBlockBoundary=function(a){return this.getComputedStyle("float")=="none"&&!(this.getComputedStyle("position")in d)&&c[this.getComputedStyle("display")]?true:!!(this.is(CKEDITOR.dtd.$block)||a&&this.is(a))};CKEDITOR.dom.walker.blockBoundary=function(a){return function(b){return!(b.type==CKEDITOR.NODE_ELEMENT&&b.isBlockBoundary(a))}};CKEDITOR.dom.walker.listItemBoundary=function(){return this.blockBoundary({br:1})};CKEDITOR.dom.walker.bookmark=function(a,b){function c(a){return a&&
-a.getName&&a.getName()=="span"&&a.data("cke-bookmark")}return function(f){var d,o;d=f&&f.type!=CKEDITOR.NODE_ELEMENT&&(o=f.getParent())&&c(o);d=a?d:d||c(f);return!!(b^d)}};CKEDITOR.dom.walker.whitespaces=function(a){return function(b){var c;b&&b.type==CKEDITOR.NODE_TEXT&&(c=!CKEDITOR.tools.trim(b.getText())||CKEDITOR.env.webkit&&b.getText()=="​");return!!(a^c)}};CKEDITOR.dom.walker.invisible=function(a){var b=CKEDITOR.dom.walker.whitespaces();return function(c){if(b(c))c=1;else{c.type==CKEDITOR.NODE_TEXT&&
-(c=c.getParent());c=!c.$.offsetHeight}return!!(a^c)}};CKEDITOR.dom.walker.nodeType=function(a,b){return function(c){return!!(b^c.type==a)}};CKEDITOR.dom.walker.bogus=function(a){function b(a){return!g(a)&&!n(a)}return function(c){var f=CKEDITOR.env.needsBrFiller?c.is&&c.is("br"):c.getText&&h.test(c.getText());if(f){f=c.getParent();c=c.getNext(b);f=f.isBlockBoundary()&&(!c||c.type==CKEDITOR.NODE_ELEMENT&&c.isBlockBoundary())}return!!(a^f)}};CKEDITOR.dom.walker.temp=function(a){return function(b){b.type!=
-CKEDITOR.NODE_ELEMENT&&(b=b.getParent());b=b&&b.hasAttribute("data-cke-temp");return!!(a^b)}};var h=/^[\t\r\n ]*(?:&nbsp;|\xa0)$/,g=CKEDITOR.dom.walker.whitespaces(),n=CKEDITOR.dom.walker.bookmark(),i=CKEDITOR.dom.walker.temp();CKEDITOR.dom.walker.ignored=function(a){return function(b){b=g(b)||n(b)||i(b);return!!(a^b)}};var j=CKEDITOR.dom.walker.ignored(),o=function(a){var b={},c;for(c in a)CKEDITOR.dtd[c]["#"]&&(b[c]=1);return b}(CKEDITOR.dtd.$block);CKEDITOR.dom.walker.editable=function(a){return function(c){return!!(a^
-b(c))}};CKEDITOR.dom.element.prototype.getBogus=function(){var a=this;do a=a.getPreviousSourceNode();while(n(a)||g(a)||a.type==CKEDITOR.NODE_ELEMENT&&a.is(CKEDITOR.dtd.$inline)&&!a.is(CKEDITOR.dtd.$empty));return a&&(CKEDITOR.env.needsBrFiller?a.is&&a.is("br"):a.getText&&h.test(a.getText()))?a:false}})();
-CKEDITOR.dom.range=function(a){this.endOffset=this.endContainer=this.startOffset=this.startContainer=null;this.collapsed=true;var e=a instanceof CKEDITOR.dom.document;this.document=e?a:a.getDocument();this.root=e?a.getBody():a};
-(function(){function a(){var a=false,b=CKEDITOR.dom.walker.whitespaces(),c=CKEDITOR.dom.walker.bookmark(true),d=CKEDITOR.dom.walker.bogus();return function(f){if(c(f)||b(f))return true;if(d(f)&&!a)return a=true;return f.type==CKEDITOR.NODE_TEXT&&(f.hasAscendant("pre")||CKEDITOR.tools.trim(f.getText()).length)||f.type==CKEDITOR.NODE_ELEMENT&&!f.is(h)?false:true}}function e(a){var b=CKEDITOR.dom.walker.whitespaces(),c=CKEDITOR.dom.walker.bookmark(1);return function(d){return c(d)||b(d)?true:!a&&g(d)||
-d.type==CKEDITOR.NODE_ELEMENT&&d.is(CKEDITOR.dtd.$removeEmpty)}}function b(a){return function(){var b;return this[a?"getPreviousNode":"getNextNode"](function(a){!b&&j(a)&&(b=a);return i(a)&&!(g(a)&&a.equals(b))})}}var c=function(a){a.collapsed=a.startContainer&&a.endContainer&&a.startContainer.equals(a.endContainer)&&a.startOffset==a.endOffset},d=function(a,b,c,d){a.optimizeBookmark();var f=a.startContainer,e=a.endContainer,h=a.startOffset,m=a.endOffset,k,l;if(e.type==CKEDITOR.NODE_TEXT)e=e.split(m);
-else if(e.getChildCount()>0)if(m>=e.getChildCount()){e=e.append(a.document.createText(""));l=true}else e=e.getChild(m);if(f.type==CKEDITOR.NODE_TEXT){f.split(h);f.equals(e)&&(e=f.getNext())}else if(h)if(h>=f.getChildCount()){f=f.append(a.document.createText(""));k=true}else f=f.getChild(h).getPrevious();else{f=f.append(a.document.createText(""),1);k=true}var h=f.getParents(),m=e.getParents(),t,i,r;for(t=0;t<h.length;t++){i=h[t];r=m[t];if(!i.equals(r))break}for(var g=c,j,n,E,z=t;z<h.length;z++){j=
-h[z];g&&!j.equals(f)&&(n=g.append(j.clone()));for(j=j.getNext();j;){if(j.equals(m[z])||j.equals(e))break;E=j.getNext();if(b==2)g.append(j.clone(true));else{j.remove();b==1&&g.append(j)}j=E}g&&(g=n)}g=c;for(c=t;c<m.length;c++){j=m[c];b>0&&!j.equals(e)&&(n=g.append(j.clone()));if(!h[c]||j.$.parentNode!=h[c].$.parentNode)for(j=j.getPrevious();j;){if(j.equals(h[c])||j.equals(f))break;E=j.getPrevious();if(b==2)g.$.insertBefore(j.$.cloneNode(true),g.$.firstChild);else{j.remove();b==1&&g.$.insertBefore(j.$,
-g.$.firstChild)}j=E}g&&(g=n)}if(b==2){i=a.startContainer;if(i.type==CKEDITOR.NODE_TEXT){i.$.data=i.$.data+i.$.nextSibling.data;i.$.parentNode.removeChild(i.$.nextSibling)}a=a.endContainer;if(a.type==CKEDITOR.NODE_TEXT&&a.$.nextSibling){a.$.data=a.$.data+a.$.nextSibling.data;a.$.parentNode.removeChild(a.$.nextSibling)}}else{if(i&&r&&(f.$.parentNode!=i.$.parentNode||e.$.parentNode!=r.$.parentNode)){b=r.getIndex();k&&r.$.parentNode==f.$.parentNode&&b--;if(d&&i.type==CKEDITOR.NODE_ELEMENT){d=CKEDITOR.dom.element.createFromHtml('<span data-cke-bookmark="1" style="display:none">&nbsp;</span>',
-a.document);d.insertAfter(i);i.mergeSiblings(false);a.moveToBookmark({startNode:d})}else a.setStart(r.getParent(),b)}a.collapse(true)}k&&f.remove();l&&e.$.parentNode&&e.remove()},h={abbr:1,acronym:1,b:1,bdo:1,big:1,cite:1,code:1,del:1,dfn:1,em:1,font:1,i:1,ins:1,label:1,kbd:1,q:1,samp:1,small:1,span:1,strike:1,strong:1,sub:1,sup:1,tt:1,u:1,"var":1},g=CKEDITOR.dom.walker.bogus(),n=/^[\t\r\n ]*(?:&nbsp;|\xa0)$/,i=CKEDITOR.dom.walker.editable(),j=CKEDITOR.dom.walker.ignored(true);CKEDITOR.dom.range.prototype=
-{clone:function(){var a=new CKEDITOR.dom.range(this.root);a.startContainer=this.startContainer;a.startOffset=this.startOffset;a.endContainer=this.endContainer;a.endOffset=this.endOffset;a.collapsed=this.collapsed;return a},collapse:function(a){if(a){this.endContainer=this.startContainer;this.endOffset=this.startOffset}else{this.startContainer=this.endContainer;this.startOffset=this.endOffset}this.collapsed=true},cloneContents:function(){var a=new CKEDITOR.dom.documentFragment(this.document);this.collapsed||
-d(this,2,a);return a},deleteContents:function(a){this.collapsed||d(this,0,null,a)},extractContents:function(a){var b=new CKEDITOR.dom.documentFragment(this.document);this.collapsed||d(this,1,b,a);return b},createBookmark:function(a){var b,c,d,f,e=this.collapsed;b=this.document.createElement("span");b.data("cke-bookmark",1);b.setStyle("display","none");b.setHtml("&nbsp;");if(a){d="cke_bm_"+CKEDITOR.tools.getNextNumber();b.setAttribute("id",d+(e?"C":"S"))}if(!e){c=b.clone();c.setHtml("&nbsp;");a&&c.setAttribute("id",
-d+"E");f=this.clone();f.collapse();f.insertNode(c)}f=this.clone();f.collapse(true);f.insertNode(b);if(c){this.setStartAfter(b);this.setEndBefore(c)}else this.moveToPosition(b,CKEDITOR.POSITION_AFTER_END);return{startNode:a?d+(e?"C":"S"):b,endNode:a?d+"E":c,serializable:a,collapsed:e}},createBookmark2:function(){function a(b){var c=b.container,d=b.offset,f;f=c;var e=d;f=f.type!=CKEDITOR.NODE_ELEMENT||e===0||e==f.getChildCount()?0:f.getChild(e-1).type==CKEDITOR.NODE_TEXT&&f.getChild(e).type==CKEDITOR.NODE_TEXT;
-if(f){c=c.getChild(d-1);d=c.getLength()}c.type==CKEDITOR.NODE_ELEMENT&&d>1&&(d=c.getChild(d-1).getIndex(true)+1);if(c.type==CKEDITOR.NODE_TEXT){f=c;for(e=0;(f=f.getPrevious())&&f.type==CKEDITOR.NODE_TEXT;)e=e+f.getLength();d=d+e}b.container=c;b.offset=d}return function(b){var c=this.collapsed,d={container:this.startContainer,offset:this.startOffset},f={container:this.endContainer,offset:this.endOffset};if(b){a(d);c||a(f)}return{start:d.container.getAddress(b),end:c?null:f.container.getAddress(b),
-startOffset:d.offset,endOffset:f.offset,normalized:b,collapsed:c,is2:true}}}(),moveToBookmark:function(a){if(a.is2){var b=this.document.getByAddress(a.start,a.normalized),c=a.startOffset,d=a.end&&this.document.getByAddress(a.end,a.normalized),a=a.endOffset;this.setStart(b,c);d?this.setEnd(d,a):this.collapse(true)}else{b=(c=a.serializable)?this.document.getById(a.startNode):a.startNode;a=c?this.document.getById(a.endNode):a.endNode;this.setStartBefore(b);b.remove();if(a){this.setEndBefore(a);a.remove()}else this.collapse(true)}},
-getBoundaryNodes:function(){var a=this.startContainer,b=this.endContainer,c=this.startOffset,d=this.endOffset,f;if(a.type==CKEDITOR.NODE_ELEMENT){f=a.getChildCount();if(f>c)a=a.getChild(c);else if(f<1)a=a.getPreviousSourceNode();else{for(a=a.$;a.lastChild;)a=a.lastChild;a=new CKEDITOR.dom.node(a);a=a.getNextSourceNode()||a}}if(b.type==CKEDITOR.NODE_ELEMENT){f=b.getChildCount();if(f>d)b=b.getChild(d).getPreviousSourceNode(true);else if(f<1)b=b.getPreviousSourceNode();else{for(b=b.$;b.lastChild;)b=
-b.lastChild;b=new CKEDITOR.dom.node(b)}}a.getPosition(b)&CKEDITOR.POSITION_FOLLOWING&&(a=b);return{startNode:a,endNode:b}},getCommonAncestor:function(a,b){var c=this.startContainer,d=this.endContainer,c=c.equals(d)?a&&c.type==CKEDITOR.NODE_ELEMENT&&this.startOffset==this.endOffset-1?c.getChild(this.startOffset):c:c.getCommonAncestor(d);return b&&!c.is?c.getParent():c},optimize:function(){var a=this.startContainer,b=this.startOffset;a.type!=CKEDITOR.NODE_ELEMENT&&(b?b>=a.getLength()&&this.setStartAfter(a):
-this.setStartBefore(a));a=this.endContainer;b=this.endOffset;a.type!=CKEDITOR.NODE_ELEMENT&&(b?b>=a.getLength()&&this.setEndAfter(a):this.setEndBefore(a))},optimizeBookmark:function(){var a=this.startContainer,b=this.endContainer;a.is&&(a.is("span")&&a.data("cke-bookmark"))&&this.setStartAt(a,CKEDITOR.POSITION_BEFORE_START);b&&(b.is&&b.is("span")&&b.data("cke-bookmark"))&&this.setEndAt(b,CKEDITOR.POSITION_AFTER_END)},trim:function(a,b){var c=this.startContainer,d=this.startOffset,f=this.collapsed;
-if((!a||f)&&c&&c.type==CKEDITOR.NODE_TEXT){if(d)if(d>=c.getLength()){d=c.getIndex()+1;c=c.getParent()}else{var e=c.split(d),d=c.getIndex()+1,c=c.getParent();if(this.startContainer.equals(this.endContainer))this.setEnd(e,this.endOffset-this.startOffset);else if(c.equals(this.endContainer))this.endOffset=this.endOffset+1}else{d=c.getIndex();c=c.getParent()}this.setStart(c,d);if(f){this.collapse(true);return}}c=this.endContainer;d=this.endOffset;if(!b&&!f&&c&&c.type==CKEDITOR.NODE_TEXT){if(d){d>=c.getLength()||
-c.split(d);d=c.getIndex()+1}else d=c.getIndex();c=c.getParent();this.setEnd(c,d)}},enlarge:function(a,b){function c(a){return a&&a.type==CKEDITOR.NODE_ELEMENT&&a.hasAttribute("contenteditable")?null:a}switch(a){case CKEDITOR.ENLARGE_INLINE:var d=1;case CKEDITOR.ENLARGE_ELEMENT:if(this.collapsed)break;var f=this.getCommonAncestor(),e=this.root,h,m,k,l,t,i=false,r,j;r=this.startContainer;j=this.startOffset;if(r.type==CKEDITOR.NODE_TEXT){if(j){r=!CKEDITOR.tools.trim(r.substring(0,j)).length&&r;i=!!r}if(r&&
-!(l=r.getPrevious()))k=r.getParent()}else{j&&(l=r.getChild(j-1)||r.getLast());l||(k=r)}for(k=c(k);k||l;){if(k&&!l){!t&&k.equals(f)&&(t=true);if(d?k.isBlockBoundary():!e.contains(k))break;if(!i||k.getComputedStyle("display")!="inline"){i=false;t?h=k:this.setStartBefore(k)}l=k.getPrevious()}for(;l;){r=false;if(l.type==CKEDITOR.NODE_COMMENT)l=l.getPrevious();else{if(l.type==CKEDITOR.NODE_TEXT){j=l.getText();/[^\s\ufeff]/.test(j)&&(l=null);r=/[\s\ufeff]$/.test(j)}else if((l.$.offsetWidth>0||b&&l.is("br"))&&
-!l.data("cke-bookmark"))if(i&&CKEDITOR.dtd.$removeEmpty[l.getName()]){j=l.getText();if(/[^\s\ufeff]/.test(j))l=null;else for(var g=l.$.getElementsByTagName("*"),n=0,E;E=g[n++];)if(!CKEDITOR.dtd.$removeEmpty[E.nodeName.toLowerCase()]){l=null;break}l&&(r=!!j.length)}else l=null;r&&(i?t?h=k:k&&this.setStartBefore(k):i=true);if(l){r=l.getPrevious();if(!k&&!r){k=l;l=null;break}l=r}else k=null}}k&&(k=c(k.getParent()))}r=this.endContainer;j=this.endOffset;k=l=null;t=i=false;if(r.type==CKEDITOR.NODE_TEXT){r=
-!CKEDITOR.tools.trim(r.substring(j)).length&&r;i=!(r&&r.getLength());if(r&&!(l=r.getNext()))k=r.getParent()}else(l=r.getChild(j))||(k=r);for(;k||l;){if(k&&!l){!t&&k.equals(f)&&(t=true);if(d?k.isBlockBoundary():!e.contains(k))break;if(!i||k.getComputedStyle("display")!="inline"){i=false;t?m=k:k&&this.setEndAfter(k)}l=k.getNext()}for(;l;){r=false;if(l.type==CKEDITOR.NODE_TEXT){j=l.getText();/[^\s\ufeff]/.test(j)&&(l=null);r=/^[\s\ufeff]/.test(j)}else if(l.type==CKEDITOR.NODE_ELEMENT){if((l.$.offsetWidth>
-0||b&&l.is("br"))&&!l.data("cke-bookmark"))if(i&&CKEDITOR.dtd.$removeEmpty[l.getName()]){j=l.getText();if(/[^\s\ufeff]/.test(j))l=null;else{g=l.$.getElementsByTagName("*");for(n=0;E=g[n++];)if(!CKEDITOR.dtd.$removeEmpty[E.nodeName.toLowerCase()]){l=null;break}}l&&(r=!!j.length)}else l=null}else r=1;r&&i&&(t?m=k:this.setEndAfter(k));if(l){r=l.getNext();if(!k&&!r){k=l;l=null;break}l=r}else k=null}k&&(k=c(k.getParent()))}if(h&&m){f=h.contains(m)?m:h;this.setStartBefore(f);this.setEndAfter(f)}break;case CKEDITOR.ENLARGE_BLOCK_CONTENTS:case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:k=
-new CKEDITOR.dom.range(this.root);e=this.root;k.setStartAt(e,CKEDITOR.POSITION_AFTER_START);k.setEnd(this.startContainer,this.startOffset);k=new CKEDITOR.dom.walker(k);var z,M,Q=CKEDITOR.dom.walker.blockBoundary(a==CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS?{br:1}:null),v=null,w=function(a){if(a.type==CKEDITOR.NODE_ELEMENT&&a.getAttribute("contenteditable")=="false")if(v){if(v.equals(a)){v=null;return}}else v=a;else if(v)return;var b=Q(a);b||(z=a);return b},d=function(a){var b=w(a);!b&&(a.is&&a.is("br"))&&
-(M=a);return b};k.guard=w;k=k.lastBackward();z=z||e;this.setStartAt(z,!z.is("br")&&(!k&&this.checkStartOfBlock()||k&&z.contains(k))?CKEDITOR.POSITION_AFTER_START:CKEDITOR.POSITION_AFTER_END);if(a==CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS){k=this.clone();k=new CKEDITOR.dom.walker(k);var H=CKEDITOR.dom.walker.whitespaces(),C=CKEDITOR.dom.walker.bookmark();k.evaluator=function(a){return!H(a)&&!C(a)};if((k=k.previous())&&k.type==CKEDITOR.NODE_ELEMENT&&k.is("br"))break}k=this.clone();k.collapse();k.setEndAt(e,
-CKEDITOR.POSITION_BEFORE_END);k=new CKEDITOR.dom.walker(k);k.guard=a==CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS?d:w;z=null;k=k.lastForward();z=z||e;this.setEndAt(z,!k&&this.checkEndOfBlock()||k&&z.contains(k)?CKEDITOR.POSITION_BEFORE_END:CKEDITOR.POSITION_BEFORE_START);M&&this.setEndAfter(M)}},shrink:function(a,b,c){if(!this.collapsed){var a=a||CKEDITOR.SHRINK_TEXT,d=this.clone(),f=this.startContainer,e=this.endContainer,h=this.startOffset,m=this.endOffset,k=1,l=1;if(f&&f.type==CKEDITOR.NODE_TEXT)if(h)if(h>=
-f.getLength())d.setStartAfter(f);else{d.setStartBefore(f);k=0}else d.setStartBefore(f);if(e&&e.type==CKEDITOR.NODE_TEXT)if(m)if(m>=e.getLength())d.setEndAfter(e);else{d.setEndAfter(e);l=0}else d.setEndBefore(e);var d=new CKEDITOR.dom.walker(d),t=CKEDITOR.dom.walker.bookmark();d.evaluator=function(b){return b.type==(a==CKEDITOR.SHRINK_ELEMENT?CKEDITOR.NODE_ELEMENT:CKEDITOR.NODE_TEXT)};var j;d.guard=function(b,d){if(t(b))return true;if(a==CKEDITOR.SHRINK_ELEMENT&&b.type==CKEDITOR.NODE_TEXT||d&&b.equals(j)||
-c===false&&b.type==CKEDITOR.NODE_ELEMENT&&b.isBlockBoundary()||b.type==CKEDITOR.NODE_ELEMENT&&b.hasAttribute("contenteditable"))return false;!d&&b.type==CKEDITOR.NODE_ELEMENT&&(j=b);return true};if(k)(f=d[a==CKEDITOR.SHRINK_ELEMENT?"lastForward":"next"]())&&this.setStartAt(f,b?CKEDITOR.POSITION_AFTER_START:CKEDITOR.POSITION_BEFORE_START);if(l){d.reset();(d=d[a==CKEDITOR.SHRINK_ELEMENT?"lastBackward":"previous"]())&&this.setEndAt(d,b?CKEDITOR.POSITION_BEFORE_END:CKEDITOR.POSITION_AFTER_END)}return!(!k&&
-!l)}},insertNode:function(a){this.optimizeBookmark();this.trim(false,true);var b=this.startContainer,c=b.getChild(this.startOffset);c?a.insertBefore(c):b.append(a);a.getParent()&&a.getParent().equals(this.endContainer)&&this.endOffset++;this.setStartBefore(a)},moveToPosition:function(a,b){this.setStartAt(a,b);this.collapse(true)},moveToRange:function(a){this.setStart(a.startContainer,a.startOffset);this.setEnd(a.endContainer,a.endOffset)},selectNodeContents:function(a){this.setStart(a,0);this.setEnd(a,
-a.type==CKEDITOR.NODE_TEXT?a.getLength():a.getChildCount())},setStart:function(a,b){if(a.type==CKEDITOR.NODE_ELEMENT&&CKEDITOR.dtd.$empty[a.getName()]){b=a.getIndex();a=a.getParent()}this.startContainer=a;this.startOffset=b;if(!this.endContainer){this.endContainer=a;this.endOffset=b}c(this)},setEnd:function(a,b){if(a.type==CKEDITOR.NODE_ELEMENT&&CKEDITOR.dtd.$empty[a.getName()]){b=a.getIndex()+1;a=a.getParent()}this.endContainer=a;this.endOffset=b;if(!this.startContainer){this.startContainer=a;this.startOffset=
-b}c(this)},setStartAfter:function(a){this.setStart(a.getParent(),a.getIndex()+1)},setStartBefore:function(a){this.setStart(a.getParent(),a.getIndex())},setEndAfter:function(a){this.setEnd(a.getParent(),a.getIndex()+1)},setEndBefore:function(a){this.setEnd(a.getParent(),a.getIndex())},setStartAt:function(a,b){switch(b){case CKEDITOR.POSITION_AFTER_START:this.setStart(a,0);break;case CKEDITOR.POSITION_BEFORE_END:a.type==CKEDITOR.NODE_TEXT?this.setStart(a,a.getLength()):this.setStart(a,a.getChildCount());
-break;case CKEDITOR.POSITION_BEFORE_START:this.setStartBefore(a);break;case CKEDITOR.POSITION_AFTER_END:this.setStartAfter(a)}c(this)},setEndAt:function(a,b){switch(b){case CKEDITOR.POSITION_AFTER_START:this.setEnd(a,0);break;case CKEDITOR.POSITION_BEFORE_END:a.type==CKEDITOR.NODE_TEXT?this.setEnd(a,a.getLength()):this.setEnd(a,a.getChildCount());break;case CKEDITOR.POSITION_BEFORE_START:this.setEndBefore(a);break;case CKEDITOR.POSITION_AFTER_END:this.setEndAfter(a)}c(this)},fixBlock:function(a,b){var c=
-this.createBookmark(),d=this.document.createElement(b);this.collapse(a);this.enlarge(CKEDITOR.ENLARGE_BLOCK_CONTENTS);this.extractContents().appendTo(d);d.trim();d.appendBogus();this.insertNode(d);this.moveToBookmark(c);return d},splitBlock:function(a){var b=new CKEDITOR.dom.elementPath(this.startContainer,this.root),c=new CKEDITOR.dom.elementPath(this.endContainer,this.root),d=b.block,f=c.block,e=null;if(!b.blockLimit.equals(c.blockLimit))return null;if(a!="br"){if(!d){d=this.fixBlock(true,a);f=
-(new CKEDITOR.dom.elementPath(this.endContainer,this.root)).block}f||(f=this.fixBlock(false,a))}a=d&&this.checkStartOfBlock();b=f&&this.checkEndOfBlock();this.deleteContents();if(d&&d.equals(f))if(b){e=new CKEDITOR.dom.elementPath(this.startContainer,this.root);this.moveToPosition(f,CKEDITOR.POSITION_AFTER_END);f=null}else if(a){e=new CKEDITOR.dom.elementPath(this.startContainer,this.root);this.moveToPosition(d,CKEDITOR.POSITION_BEFORE_START);d=null}else{f=this.splitElement(d);d.is("ul","ol")||d.appendBogus()}return{previousBlock:d,
-nextBlock:f,wasStartOfBlock:a,wasEndOfBlock:b,elementPath:e}},splitElement:function(a){if(!this.collapsed)return null;this.setEndAt(a,CKEDITOR.POSITION_BEFORE_END);var b=this.extractContents(),c=a.clone(false);b.appendTo(c);c.insertAfter(a);this.moveToPosition(a,CKEDITOR.POSITION_AFTER_END);return c},removeEmptyBlocksAtEnd:function(){function a(d){return function(a){return b(a)||(c(a)||a.type==CKEDITOR.NODE_ELEMENT&&a.isEmptyInlineRemoveable())||d.is("table")&&a.is("caption")?false:true}}var b=CKEDITOR.dom.walker.whitespaces(),
-c=CKEDITOR.dom.walker.bookmark(false);return function(b){for(var c=this.createBookmark(),d=this[b?"endPath":"startPath"](),e=d.block||d.blockLimit,m;e&&!e.equals(d.root)&&!e.getFirst(a(e));){m=e.getParent();this[b?"setEndAt":"setStartAt"](e,CKEDITOR.POSITION_AFTER_END);e.remove(1);e=m}this.moveToBookmark(c)}}(),startPath:function(){return new CKEDITOR.dom.elementPath(this.startContainer,this.root)},endPath:function(){return new CKEDITOR.dom.elementPath(this.endContainer,this.root)},checkBoundaryOfElement:function(a,
-b){var c=b==CKEDITOR.START,d=this.clone();d.collapse(c);d[c?"setStartAt":"setEndAt"](a,c?CKEDITOR.POSITION_AFTER_START:CKEDITOR.POSITION_BEFORE_END);d=new CKEDITOR.dom.walker(d);d.evaluator=e(c);return d[c?"checkBackward":"checkForward"]()},checkStartOfBlock:function(){var b=this.startContainer,c=this.startOffset;if(CKEDITOR.env.ie&&c&&b.type==CKEDITOR.NODE_TEXT){b=CKEDITOR.tools.ltrim(b.substring(0,c));n.test(b)&&this.trim(0,1)}this.trim();b=new CKEDITOR.dom.elementPath(this.startContainer,this.root);
-c=this.clone();c.collapse(true);c.setStartAt(b.block||b.blockLimit,CKEDITOR.POSITION_AFTER_START);b=new CKEDITOR.dom.walker(c);b.evaluator=a();return b.checkBackward()},checkEndOfBlock:function(){var b=this.endContainer,c=this.endOffset;if(CKEDITOR.env.ie&&b.type==CKEDITOR.NODE_TEXT){b=CKEDITOR.tools.rtrim(b.substring(c));n.test(b)&&this.trim(1,0)}this.trim();b=new CKEDITOR.dom.elementPath(this.endContainer,this.root);c=this.clone();c.collapse(false);c.setEndAt(b.block||b.blockLimit,CKEDITOR.POSITION_BEFORE_END);
-b=new CKEDITOR.dom.walker(c);b.evaluator=a();return b.checkForward()},getPreviousNode:function(a,b,c){var d=this.clone();d.collapse(1);d.setStartAt(c||this.root,CKEDITOR.POSITION_AFTER_START);c=new CKEDITOR.dom.walker(d);c.evaluator=a;c.guard=b;return c.previous()},getNextNode:function(a,b,c){var d=this.clone();d.collapse();d.setEndAt(c||this.root,CKEDITOR.POSITION_BEFORE_END);c=new CKEDITOR.dom.walker(d);c.evaluator=a;c.guard=b;return c.next()},checkReadOnly:function(){function a(b,c){for(;b;){if(b.type==
-CKEDITOR.NODE_ELEMENT){if(b.getAttribute("contentEditable")=="false"&&!b.data("cke-editable"))return 0;if(b.is("html")||b.getAttribute("contentEditable")=="true"&&(b.contains(c)||b.equals(c)))break}b=b.getParent()}return 1}return function(){var b=this.startContainer,c=this.endContainer;return!(a(b,c)&&a(c,b))}}(),moveToElementEditablePosition:function(a,b){if(a.type==CKEDITOR.NODE_ELEMENT&&!a.isEditable(false)){this.moveToPosition(a,b?CKEDITOR.POSITION_AFTER_END:CKEDITOR.POSITION_BEFORE_START);return true}for(var c=
-0;a;){if(a.type==CKEDITOR.NODE_TEXT){b&&this.endContainer&&this.checkEndOfBlock()&&n.test(a.getText())?this.moveToPosition(a,CKEDITOR.POSITION_BEFORE_START):this.moveToPosition(a,b?CKEDITOR.POSITION_AFTER_END:CKEDITOR.POSITION_BEFORE_START);c=1;break}if(a.type==CKEDITOR.NODE_ELEMENT)if(a.isEditable()){this.moveToPosition(a,b?CKEDITOR.POSITION_BEFORE_END:CKEDITOR.POSITION_AFTER_START);c=1}else if(b&&a.is("br")&&this.endContainer&&this.checkEndOfBlock())this.moveToPosition(a,CKEDITOR.POSITION_BEFORE_START);
-else if(a.getAttribute("contenteditable")=="false"&&a.is(CKEDITOR.dtd.$block)){this.setStartBefore(a);this.setEndAfter(a);return true}var d=a,f=c,e=void 0;d.type==CKEDITOR.NODE_ELEMENT&&d.isEditable(false)&&(e=d[b?"getLast":"getFirst"](j));!f&&!e&&(e=d[b?"getPrevious":"getNext"](j));a=e}return!!c},moveToClosestEditablePosition:function(a,b){var c=new CKEDITOR.dom.range(this.root),d=0,f,e=[CKEDITOR.POSITION_AFTER_END,CKEDITOR.POSITION_BEFORE_START];c.moveToPosition(a,e[b?0:1]);if(a.is(CKEDITOR.dtd.$block)){if(f=
-c[b?"getNextEditableNode":"getPreviousEditableNode"]()){d=1;if(f.type==CKEDITOR.NODE_ELEMENT&&f.is(CKEDITOR.dtd.$block)&&f.getAttribute("contenteditable")=="false"){c.setStartAt(f,CKEDITOR.POSITION_BEFORE_START);c.setEndAt(f,CKEDITOR.POSITION_AFTER_END)}else c.moveToPosition(f,e[b?1:0])}}else d=1;d&&this.moveToRange(c);return!!d},moveToElementEditStart:function(a){return this.moveToElementEditablePosition(a)},moveToElementEditEnd:function(a){return this.moveToElementEditablePosition(a,true)},getEnclosedNode:function(){var a=
-this.clone();a.optimize();if(a.startContainer.type!=CKEDITOR.NODE_ELEMENT||a.endContainer.type!=CKEDITOR.NODE_ELEMENT)return null;var a=new CKEDITOR.dom.walker(a),b=CKEDITOR.dom.walker.bookmark(false,true),c=CKEDITOR.dom.walker.whitespaces(true);a.evaluator=function(a){return c(a)&&b(a)};var d=a.next();a.reset();return d&&d.equals(a.previous())?d:null},getTouchedStartNode:function(){var a=this.startContainer;return this.collapsed||a.type!=CKEDITOR.NODE_ELEMENT?a:a.getChild(this.startOffset)||a},getTouchedEndNode:function(){var a=
-this.endContainer;return this.collapsed||a.type!=CKEDITOR.NODE_ELEMENT?a:a.getChild(this.endOffset-1)||a},getNextEditableNode:b(),getPreviousEditableNode:b(1),scrollIntoView:function(){var a=new CKEDITOR.dom.element.createFromHtml("<span>&nbsp;</span>",this.document),b,c,d,f=this.clone();f.optimize();if(d=f.startContainer.type==CKEDITOR.NODE_TEXT){c=f.startContainer.getText();b=f.startContainer.split(f.startOffset);a.insertAfter(f.startContainer)}else f.insertNode(a);a.scrollIntoView();if(d){f.startContainer.setText(c);
-b.remove()}a.remove()}}})();CKEDITOR.POSITION_AFTER_START=1;CKEDITOR.POSITION_BEFORE_END=2;CKEDITOR.POSITION_BEFORE_START=3;CKEDITOR.POSITION_AFTER_END=4;CKEDITOR.ENLARGE_ELEMENT=1;CKEDITOR.ENLARGE_BLOCK_CONTENTS=2;CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS=3;CKEDITOR.ENLARGE_INLINE=4;CKEDITOR.START=1;CKEDITOR.END=2;CKEDITOR.SHRINK_ELEMENT=1;CKEDITOR.SHRINK_TEXT=2;"use strict";
-(function(){function a(a){if(!(arguments.length<1)){this.range=a;this.forceBrBreak=0;this.enlargeBr=1;this.enforceRealBlocks=0;this._||(this._={})}}function e(a,b,c){for(a=a.getNextSourceNode(b,null,c);!h(a);)a=a.getNextSourceNode(b,null,c);return a}function b(a){var b=[];a.forEach(function(a){if(a.getAttribute("contenteditable")=="true"){b.push(a);return false}},CKEDITOR.NODE_ELEMENT,true);return b}function c(a,d,e,h){a:{h==void 0&&(h=b(e));for(var g;g=h.shift();)if(g.getDtd().p){h={element:g,remaining:h};
-break a}h=null}if(!h)return 0;if((g=CKEDITOR.filter.instances[h.element.data("cke-filter")])&&!g.check(d))return c(a,d,e,h.remaining);d=new CKEDITOR.dom.range(h.element);d.selectNodeContents(h.element);d=d.createIterator();d.enlargeBr=a.enlargeBr;d.enforceRealBlocks=a.enforceRealBlocks;d.activeFilter=d.filter=g;a._.nestedEditable={element:h.element,container:e,remaining:h.remaining,iterator:d};return 1}var d=/^[\r\n\t ]+$/,h=CKEDITOR.dom.walker.bookmark(false,true),g=CKEDITOR.dom.walker.whitespaces(true),
-n=function(a){return h(a)&&g(a)};a.prototype={getNextParagraph:function(a){var b,g,q,s,u,a=a||"p";if(this._.nestedEditable){if(b=this._.nestedEditable.iterator.getNextParagraph(a)){this.activeFilter=this._.nestedEditable.iterator.activeFilter;return b}this.activeFilter=this.filter;if(c(this,a,this._.nestedEditable.container,this._.nestedEditable.remaining)){this.activeFilter=this._.nestedEditable.iterator.activeFilter;return this._.nestedEditable.iterator.getNextParagraph(a)}this._.nestedEditable=
-null}if(!this.range.root.getDtd()[a])return null;if(!this._.started){var f=this.range.clone();f.shrink(CKEDITOR.SHRINK_ELEMENT,true);g=f.endContainer.hasAscendant("pre",true)||f.startContainer.hasAscendant("pre",true);f.enlarge(this.forceBrBreak&&!g||!this.enlargeBr?CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:CKEDITOR.ENLARGE_BLOCK_CONTENTS);if(!f.collapsed){g=new CKEDITOR.dom.walker(f.clone());var p=CKEDITOR.dom.walker.bookmark(true,true);g.evaluator=p;this._.nextNode=g.next();g=new CKEDITOR.dom.walker(f.clone());
-g.evaluator=p;g=g.previous();this._.lastNode=g.getNextSourceNode(true);if(this._.lastNode&&this._.lastNode.type==CKEDITOR.NODE_TEXT&&!CKEDITOR.tools.trim(this._.lastNode.getText())&&this._.lastNode.getParent().isBlockBoundary()){p=this.range.clone();p.moveToPosition(this._.lastNode,CKEDITOR.POSITION_AFTER_END);if(p.checkEndOfBlock()){p=new CKEDITOR.dom.elementPath(p.endContainer,p.root);this._.lastNode=(p.block||p.blockLimit).getNextSourceNode(true)}}if(!this._.lastNode||!f.root.contains(this._.lastNode)){this._.lastNode=
-this._.docEndMarker=f.document.createText("");this._.lastNode.insertAfter(g)}f=null}this._.started=1;g=f}p=this._.nextNode;f=this._.lastNode;for(this._.nextNode=null;p;){var y=0,m=p.hasAscendant("pre"),k=p.type!=CKEDITOR.NODE_ELEMENT,l=0;if(k)p.type==CKEDITOR.NODE_TEXT&&d.test(p.getText())&&(k=0);else{var t=p.getName();if(CKEDITOR.dtd.$block[t]&&p.getAttribute("contenteditable")=="false"){b=p;c(this,a,b);break}else if(p.isBlockBoundary(this.forceBrBreak&&!m&&{br:1})){if(t=="br")k=1;else if(!g&&!p.getChildCount()&&
-t!="hr"){b=p;q=p.equals(f);break}if(g){g.setEndAt(p,CKEDITOR.POSITION_BEFORE_START);if(t!="br")this._.nextNode=p}y=1}else{if(p.getFirst()){if(!g){g=this.range.clone();g.setStartAt(p,CKEDITOR.POSITION_BEFORE_START)}p=p.getFirst();continue}k=1}}if(k&&!g){g=this.range.clone();g.setStartAt(p,CKEDITOR.POSITION_BEFORE_START)}q=(!y||k)&&p.equals(f);if(g&&!y)for(;!p.getNext(n)&&!q;){t=p.getParent();if(t.isBlockBoundary(this.forceBrBreak&&!m&&{br:1})){y=1;k=0;q||t.equals(f);g.setEndAt(t,CKEDITOR.POSITION_BEFORE_END);
-break}p=t;k=1;q=p.equals(f);l=1}k&&g.setEndAt(p,CKEDITOR.POSITION_AFTER_END);p=e(p,l,f);if((q=!p)||y&&g)break}if(!b){if(!g){this._.docEndMarker&&this._.docEndMarker.remove();return this._.nextNode=null}b=new CKEDITOR.dom.elementPath(g.startContainer,g.root);p=b.blockLimit;y={div:1,th:1,td:1};b=b.block;if(!b&&p&&!this.enforceRealBlocks&&y[p.getName()]&&g.checkStartOfBlock()&&g.checkEndOfBlock()&&!p.equals(g.root))b=p;else if(!b||this.enforceRealBlocks&&b.getName()=="li"){b=this.range.document.createElement(a);
-g.extractContents().appendTo(b);b.trim();g.insertNode(b);s=u=true}else if(b.getName()!="li"){if(!g.checkStartOfBlock()||!g.checkEndOfBlock()){b=b.clone(false);g.extractContents().appendTo(b);b.trim();u=g.splitBlock();s=!u.wasStartOfBlock;u=!u.wasEndOfBlock;g.insertNode(b)}}else if(!q)this._.nextNode=b.equals(f)?null:e(g.getBoundaryNodes().endNode,1,f)}if(s)(s=b.getPrevious())&&s.type==CKEDITOR.NODE_ELEMENT&&(s.getName()=="br"?s.remove():s.getLast()&&s.getLast().$.nodeName.toLowerCase()=="br"&&s.getLast().remove());
-if(u)(s=b.getLast())&&s.type==CKEDITOR.NODE_ELEMENT&&s.getName()=="br"&&(!CKEDITOR.env.needsBrFiller||s.getPrevious(h)||s.getNext(h))&&s.remove();if(!this._.nextNode)this._.nextNode=q||b.equals(f)||!f?null:e(b,1,f);return b}};CKEDITOR.dom.range.prototype.createIterator=function(){return new a(this)}})();
-CKEDITOR.command=function(a,e){this.uiItems=[];this.exec=function(b){if(this.state==CKEDITOR.TRISTATE_DISABLED||!this.checkAllowed())return false;this.editorFocus&&a.focus();return this.fire("exec")===false?true:e.exec.call(this,a,b)!==false};this.refresh=function(a,b){if(!this.readOnly&&a.readOnly)return true;if(this.context&&!b.isContextFor(this.context)){this.disable();return true}if(!this.checkAllowed(true)){this.disable();return true}this.startDisabled||this.enable();this.modes&&!this.modes[a.mode]&&
-this.disable();return this.fire("refresh",{editor:a,path:b})===false?true:e.refresh&&e.refresh.apply(this,arguments)!==false};var b;this.checkAllowed=function(c){return!c&&typeof b=="boolean"?b:b=a.activeFilter.checkFeature(this)};CKEDITOR.tools.extend(this,e,{modes:{wysiwyg:1},editorFocus:1,contextSensitive:!!e.context,state:CKEDITOR.TRISTATE_DISABLED});CKEDITOR.event.call(this)};
-CKEDITOR.command.prototype={enable:function(){this.state==CKEDITOR.TRISTATE_DISABLED&&this.checkAllowed()&&this.setState(!this.preserveState||typeof this.previousState=="undefined"?CKEDITOR.TRISTATE_OFF:this.previousState)},disable:function(){this.setState(CKEDITOR.TRISTATE_DISABLED)},setState:function(a){if(this.state==a||a!=CKEDITOR.TRISTATE_DISABLED&&!this.checkAllowed())return false;this.previousState=this.state;this.state=a;this.fire("state");return true},toggleState:function(){this.state==CKEDITOR.TRISTATE_OFF?
+CKEDITOR.tools.extend(CKEDITOR.dom.document.prototype,{type:CKEDITOR.NODE_DOCUMENT,appendStyleSheet:function(a){if(this.$.createStyleSheet)this.$.createStyleSheet(a);else{var d=new CKEDITOR.dom.element("link");d.setAttributes({rel:"stylesheet",type:"text/css",href:a});this.getHead().append(d)}},appendStyleText:function(a){if(this.$.createStyleSheet){var d=this.$.createStyleSheet("");d.cssText=a}else{var b=new CKEDITOR.dom.element("style",this);b.append(new CKEDITOR.dom.text(a,this));this.getHead().append(b)}return d||
+b.$.sheet},createElement:function(a,d){var b=new CKEDITOR.dom.element(a,this);d&&(d.attributes&&b.setAttributes(d.attributes),d.styles&&b.setStyles(d.styles));return b},createText:function(a){return new CKEDITOR.dom.text(a,this)},focus:function(){this.getWindow().focus()},getActive:function(){var a;try{a=this.$.activeElement}catch(d){return null}return new CKEDITOR.dom.element(a)},getById:function(a){return(a=this.$.getElementById(a))?new CKEDITOR.dom.element(a):null},getByAddress:function(a,d){for(var b=
+this.$.documentElement,c=0;b&&c<a.length;c++){var e=a[c];if(d)for(var g=-1,h=0;h<b.childNodes.length;h++){var k=b.childNodes[h];if(!0!==d||3!=k.nodeType||!k.previousSibling||3!=k.previousSibling.nodeType)if(g++,g==e){b=k;break}}else b=b.childNodes[e]}return b?new CKEDITOR.dom.node(b):null},getElementsByTag:function(a,d){CKEDITOR.env.ie&&8>=document.documentMode||!d||(a=d+":"+a);return new CKEDITOR.dom.nodeList(this.$.getElementsByTagName(a))},getHead:function(){var a=this.$.getElementsByTagName("head")[0];
+return a=a?new CKEDITOR.dom.element(a):this.getDocumentElement().append(new CKEDITOR.dom.element("head"),!0)},getBody:function(){return new CKEDITOR.dom.element(this.$.body)},getDocumentElement:function(){return new CKEDITOR.dom.element(this.$.documentElement)},getWindow:function(){return new CKEDITOR.dom.window(this.$.parentWindow||this.$.defaultView)},write:function(a){this.$.open("text/html","replace");CKEDITOR.env.ie&&(a=a.replace(/(?:^\s*<!DOCTYPE[^>]*?>)|^/i,'$\x26\n\x3cscript data-cke-temp\x3d"1"\x3e('+
+CKEDITOR.tools.fixDomain+")();\x3c/script\x3e"));this.$.write(a);this.$.close()},find:function(a){return new CKEDITOR.dom.nodeList(this.$.querySelectorAll(a))},findOne:function(a){return(a=this.$.querySelector(a))?new CKEDITOR.dom.element(a):null},_getHtml5ShivFrag:function(){var a=this.getCustomData("html5ShivFrag");a||(a=this.$.createDocumentFragment(),CKEDITOR.tools.enableHtml5Elements(a,!0),this.setCustomData("html5ShivFrag",a));return a}});CKEDITOR.dom.nodeList=function(a){this.$=a};
+CKEDITOR.dom.nodeList.prototype={count:function(){return this.$.length},getItem:function(a){return 0>a||a>=this.$.length?null:(a=this.$[a])?new CKEDITOR.dom.node(a):null},toArray:function(){return CKEDITOR.tools.array.map(this.$,function(a){return new CKEDITOR.dom.node(a)})}};CKEDITOR.dom.element=function(a,d){"string"==typeof a&&(a=(d?d.$:document).createElement(a));CKEDITOR.dom.domObject.call(this,a)};
+CKEDITOR.dom.element.get=function(a){return(a="string"==typeof a?document.getElementById(a)||document.getElementsByName(a)[0]:a)&&(a.$?a:new CKEDITOR.dom.element(a))};CKEDITOR.dom.element.prototype=new CKEDITOR.dom.node;CKEDITOR.dom.element.createFromHtml=function(a,d){var b=new CKEDITOR.dom.element("div",d);b.setHtml(a);return b.getFirst().remove()};
+CKEDITOR.dom.element.setMarker=function(a,d,b,c){var e=d.getCustomData("list_marker_id")||d.setCustomData("list_marker_id",CKEDITOR.tools.getNextNumber()).getCustomData("list_marker_id"),g=d.getCustomData("list_marker_names")||d.setCustomData("list_marker_names",{}).getCustomData("list_marker_names");a[e]=d;g[b]=1;return d.setCustomData(b,c)};CKEDITOR.dom.element.clearAllMarkers=function(a){for(var d in a)CKEDITOR.dom.element.clearMarkers(a,a[d],1)};
+CKEDITOR.dom.element.clearMarkers=function(a,d,b){var c=d.getCustomData("list_marker_names"),e=d.getCustomData("list_marker_id"),g;for(g in c)d.removeCustomData(g);d.removeCustomData("list_marker_names");b&&(d.removeCustomData("list_marker_id"),delete a[e])};
+(function(){function a(a,b){return-1<(" "+a+" ").replace(g," ").indexOf(" "+b+" ")}function d(a){var b=!0;a.$.id||(a.$.id="cke_tmp_"+CKEDITOR.tools.getNextNumber(),b=!1);return function(){b||a.removeAttribute("id")}}function b(a,b){var c=CKEDITOR.tools.escapeCss(a.$.id);return"#"+c+" "+b.split(/,\s*/).join(", #"+c+" ")}function c(a){for(var b=0,c=0,f=h[a].length;c<f;c++)b+=parseFloat(this.getComputedStyle(h[a][c])||0,10)||0;return b}var e=document.createElement("_").classList,e="undefined"!==typeof e&&
+null!==String(e.add).match(/\[Native code\]/gi),g=/[\n\t\r]/g;CKEDITOR.tools.extend(CKEDITOR.dom.element.prototype,{type:CKEDITOR.NODE_ELEMENT,addClass:e?function(a){this.$.classList.add(a);return this}:function(b){var c=this.$.className;c&&(a(c,b)||(c+=" "+b));this.$.className=c||b;return this},removeClass:e?function(a){var b=this.$;b.classList.remove(a);b.className||b.removeAttribute("class");return this}:function(b){var c=this.getAttribute("class");c&&a(c,b)&&((c=c.replace(new RegExp("(?:^|\\s+)"+
+b+"(?\x3d\\s|$)"),"").replace(/^\s+/,""))?this.setAttribute("class",c):this.removeAttribute("class"));return this},hasClass:function(b){return a(this.$.className,b)},append:function(a,b){"string"==typeof a&&(a=this.getDocument().createElement(a));b?this.$.insertBefore(a.$,this.$.firstChild):this.$.appendChild(a.$);return a},appendHtml:function(a){if(this.$.childNodes.length){var b=new CKEDITOR.dom.element("div",this.getDocument());b.setHtml(a);b.moveChildren(this)}else this.setHtml(a)},appendText:function(a){null!=
+this.$.text&&CKEDITOR.env.ie&&9>CKEDITOR.env.version?this.$.text+=a:this.append(new CKEDITOR.dom.text(a))},appendBogus:function(a){if(a||CKEDITOR.env.needsBrFiller){for(a=this.getLast();a&&a.type==CKEDITOR.NODE_TEXT&&!CKEDITOR.tools.rtrim(a.getText());)a=a.getPrevious();a&&a.is&&a.is("br")||(a=this.getDocument().createElement("br"),CKEDITOR.env.gecko&&a.setAttribute("type","_moz"),this.append(a))}},breakParent:function(a,b){var c=new CKEDITOR.dom.range(this.getDocument());c.setStartAfter(this);c.setEndAfter(a);
+var f=c.extractContents(!1,b||!1),d;c.insertNode(this.remove());if(CKEDITOR.env.ie&&!CKEDITOR.env.edge){for(c=new CKEDITOR.dom.element("div");d=f.getFirst();)d.$.style.backgroundColor&&(d.$.style.backgroundColor=d.$.style.backgroundColor),c.append(d);c.insertAfter(this);c.remove(!0)}else f.insertAfterNode(this)},contains:document.compareDocumentPosition?function(a){return!!(this.$.compareDocumentPosition(a.$)&16)}:function(a){var b=this.$;return a.type!=CKEDITOR.NODE_ELEMENT?b.contains(a.getParent().$):
+b!=a.$&&b.contains(a.$)},focus:function(){function a(){try{this.$.focus()}catch(b){}}return function(b){b?CKEDITOR.tools.setTimeout(a,100,this):a.call(this)}}(),getHtml:function(){var a=this.$.innerHTML;return CKEDITOR.env.ie?a.replace(/<\?[^>]*>/g,""):a},getOuterHtml:function(){if(this.$.outerHTML)return this.$.outerHTML.replace(/<\?[^>]*>/,"");var a=this.$.ownerDocument.createElement("div");a.appendChild(this.$.cloneNode(!0));return a.innerHTML},getClientRect:function(){var a=CKEDITOR.tools.extend({},
+this.$.getBoundingClientRect());!a.width&&(a.width=a.right-a.left);!a.height&&(a.height=a.bottom-a.top);return a},setHtml:CKEDITOR.env.ie&&9>CKEDITOR.env.version?function(a){try{var b=this.$;if(this.getParent())return b.innerHTML=a;var c=this.getDocument()._getHtml5ShivFrag();c.appendChild(b);b.innerHTML=a;c.removeChild(b);return a}catch(f){this.$.innerHTML="";b=new CKEDITOR.dom.element("body",this.getDocument());b.$.innerHTML=a;for(b=b.getChildren();b.count();)this.append(b.getItem(0));return a}}:
+function(a){return this.$.innerHTML=a},setText:function(){var a=document.createElement("p");a.innerHTML="x";a=a.textContent;return function(b){this.$[a?"textContent":"innerText"]=b}}(),getAttribute:function(){var a=function(a){return this.$.getAttribute(a,2)};return CKEDITOR.env.ie&&(CKEDITOR.env.ie7Compat||CKEDITOR.env.quirks)?function(a){switch(a){case "class":a="className";break;case "http-equiv":a="httpEquiv";break;case "name":return this.$.name;case "tabindex":return a=this.$.getAttribute(a,
+2),0!==a&&0===this.$.tabIndex&&(a=null),a;case "checked":return a=this.$.attributes.getNamedItem(a),(a.specified?a.nodeValue:this.$.checked)?"checked":null;case "hspace":case "value":return this.$[a];case "style":return this.$.style.cssText;case "contenteditable":case "contentEditable":return this.$.attributes.getNamedItem("contentEditable").specified?this.$.getAttribute("contentEditable"):null}return this.$.getAttribute(a,2)}:a}(),getAttributes:function(a){var b={},c=this.$.attributes,f;a=CKEDITOR.tools.isArray(a)?
+a:[];for(f=0;f<c.length;f++)-1===CKEDITOR.tools.indexOf(a,c[f].name)&&(b[c[f].name]=c[f].value);return b},getChildren:function(){return new CKEDITOR.dom.nodeList(this.$.childNodes)},getComputedStyle:document.defaultView&&document.defaultView.getComputedStyle?function(a){var b=this.getWindow().$.getComputedStyle(this.$,null);return b?b.getPropertyValue(a):""}:function(a){return this.$.currentStyle[CKEDITOR.tools.cssStyleToDomStyle(a)]},getDtd:function(){var a=CKEDITOR.dtd[this.getName()];this.getDtd=
+function(){return a};return a},getElementsByTag:CKEDITOR.dom.document.prototype.getElementsByTag,getTabIndex:function(){var a=this.$.tabIndex;return 0!==a||CKEDITOR.dtd.$tabIndex[this.getName()]||0===parseInt(this.getAttribute("tabindex"),10)?a:-1},getText:function(){return this.$.textContent||this.$.innerText||""},getWindow:function(){return this.getDocument().getWindow()},getId:function(){return this.$.id||null},getNameAtt:function(){return this.$.name||null},getName:function(){var a=this.$.nodeName.toLowerCase();
+if(CKEDITOR.env.ie&&8>=document.documentMode){var b=this.$.scopeName;"HTML"!=b&&(a=b.toLowerCase()+":"+a)}this.getName=function(){return a};return this.getName()},getValue:function(){return this.$.value},getFirst:function(a){var b=this.$.firstChild;(b=b&&new CKEDITOR.dom.node(b))&&a&&!a(b)&&(b=b.getNext(a));return b},getLast:function(a){var b=this.$.lastChild;(b=b&&new CKEDITOR.dom.node(b))&&a&&!a(b)&&(b=b.getPrevious(a));return b},getStyle:function(a){return this.$.style[CKEDITOR.tools.cssStyleToDomStyle(a)]},
+is:function(){var a=this.getName();if("object"==typeof arguments[0])return!!arguments[0][a];for(var b=0;b<arguments.length;b++)if(arguments[b]==a)return!0;return!1},isEditable:function(a){var b=this.getName();return this.isReadOnly()||"none"==this.getComputedStyle("display")||"hidden"==this.getComputedStyle("visibility")||CKEDITOR.dtd.$nonEditable[b]||CKEDITOR.dtd.$empty[b]||this.is("a")&&(this.data("cke-saved-name")||this.hasAttribute("name"))&&!this.getChildCount()?!1:!1!==a?(a=CKEDITOR.dtd[b]||
+CKEDITOR.dtd.span,!(!a||!a["#"])):!0},isIdentical:function(a){var b=this.clone(0,1);a=a.clone(0,1);b.removeAttributes(["_moz_dirty","data-cke-expando","data-cke-saved-href","data-cke-saved-name"]);a.removeAttributes(["_moz_dirty","data-cke-expando","data-cke-saved-href","data-cke-saved-name"]);if(b.$.isEqualNode)return b.$.style.cssText=CKEDITOR.tools.normalizeCssText(b.$.style.cssText),a.$.style.cssText=CKEDITOR.tools.normalizeCssText(a.$.style.cssText),b.$.isEqualNode(a.$);b=b.getOuterHtml();a=
+a.getOuterHtml();if(CKEDITOR.env.ie&&9>CKEDITOR.env.version&&this.is("a")){var c=this.getParent();c.type==CKEDITOR.NODE_ELEMENT&&(c=c.clone(),c.setHtml(b),b=c.getHtml(),c.setHtml(a),a=c.getHtml())}return b==a},isVisible:function(){var a=(this.$.offsetHeight||this.$.offsetWidth)&&"hidden"!=this.getComputedStyle("visibility"),b,c;a&&CKEDITOR.env.webkit&&(b=this.getWindow(),!b.equals(CKEDITOR.document.getWindow())&&(c=b.$.frameElement)&&(a=(new CKEDITOR.dom.element(c)).isVisible()));return!!a},isEmptyInlineRemoveable:function(){if(!CKEDITOR.dtd.$removeEmpty[this.getName()])return!1;
+for(var a=this.getChildren(),b=0,c=a.count();b<c;b++){var f=a.getItem(b);if(f.type!=CKEDITOR.NODE_ELEMENT||!f.data("cke-bookmark"))if(f.type==CKEDITOR.NODE_ELEMENT&&!f.isEmptyInlineRemoveable()||f.type==CKEDITOR.NODE_TEXT&&CKEDITOR.tools.trim(f.getText()))return!1}return!0},hasAttributes:CKEDITOR.env.ie&&(CKEDITOR.env.ie7Compat||CKEDITOR.env.quirks)?function(){for(var a=this.$.attributes,b=0;b<a.length;b++){var c=a[b];switch(c.nodeName){case "class":if(this.getAttribute("class"))return!0;case "data-cke-expando":continue;
+default:if(c.specified)return!0}}return!1}:function(){var a=this.$.attributes,b=a.length,c={"data-cke-expando":1,_moz_dirty:1};return 0<b&&(2<b||!c[a[0].nodeName]||2==b&&!c[a[1].nodeName])},hasAttribute:function(){function a(b){var c=this.$.attributes.getNamedItem(b);if("input"==this.getName())switch(b){case "class":return 0<this.$.className.length;case "checked":return!!this.$.checked;case "value":return b=this.getAttribute("type"),"checkbox"==b||"radio"==b?"on"!=this.$.value:!!this.$.value}return c?
+c.specified:!1}return CKEDITOR.env.ie?8>CKEDITOR.env.version?function(b){return"name"==b?!!this.$.name:a.call(this,b)}:a:function(a){return!!this.$.attributes.getNamedItem(a)}}(),hide:function(){this.setStyle("display","none")},moveChildren:function(a,b){var c=this.$;a=a.$;if(c!=a){var f;if(b)for(;f=c.lastChild;)a.insertBefore(c.removeChild(f),a.firstChild);else for(;f=c.firstChild;)a.appendChild(c.removeChild(f))}},mergeSiblings:function(){function a(b,c,f){if(c&&c.type==CKEDITOR.NODE_ELEMENT){for(var d=
+[];c.data("cke-bookmark")||c.isEmptyInlineRemoveable();)if(d.push(c),c=f?c.getNext():c.getPrevious(),!c||c.type!=CKEDITOR.NODE_ELEMENT)return;if(b.isIdentical(c)){for(var k=f?b.getLast():b.getFirst();d.length;)d.shift().move(b,!f);c.moveChildren(b,!f);c.remove();k&&k.type==CKEDITOR.NODE_ELEMENT&&k.mergeSiblings()}}}return function(b){if(!1===b||CKEDITOR.dtd.$removeEmpty[this.getName()]||this.is("a"))a(this,this.getNext(),!0),a(this,this.getPrevious())}}(),show:function(){this.setStyles({display:"",
+visibility:""})},setAttribute:function(){var a=function(a,b){this.$.setAttribute(a,b);return this};return CKEDITOR.env.ie&&(CKEDITOR.env.ie7Compat||CKEDITOR.env.quirks)?function(b,c){"class"==b?this.$.className=c:"style"==b?this.$.style.cssText=c:"tabindex"==b?this.$.tabIndex=c:"checked"==b?this.$.checked=c:"contenteditable"==b?a.call(this,"contentEditable",c):a.apply(this,arguments);return this}:CKEDITOR.env.ie8Compat&&CKEDITOR.env.secure?function(b,c){if("src"==b&&c.match(/^http:\/\//))try{a.apply(this,
+arguments)}catch(f){}else a.apply(this,arguments);return this}:a}(),setAttributes:function(a){for(var b in a)this.setAttribute(b,a[b]);return this},setValue:function(a){this.$.value=a;return this},removeAttribute:function(){var a=function(a){this.$.removeAttribute(a)};return CKEDITOR.env.ie&&(CKEDITOR.env.ie7Compat||CKEDITOR.env.quirks)?function(a){"class"==a?a="className":"tabindex"==a?a="tabIndex":"contenteditable"==a&&(a="contentEditable");this.$.removeAttribute(a)}:a}(),removeAttributes:function(a){if(CKEDITOR.tools.isArray(a))for(var b=
+0;b<a.length;b++)this.removeAttribute(a[b]);else for(b in a=a||this.getAttributes(),a)a.hasOwnProperty(b)&&this.removeAttribute(b)},removeStyle:function(a){var b=this.$.style;if(b.removeProperty||"border"!=a&&"margin"!=a&&"padding"!=a)b.removeProperty?b.removeProperty(a):b.removeAttribute(CKEDITOR.tools.cssStyleToDomStyle(a)),this.$.style.cssText||this.removeAttribute("style");else{var c=["top","left","right","bottom"],f;"border"==a&&(f=["color","style","width"]);for(var b=[],d=0;d<c.length;d++)if(f)for(var w=
+0;w<f.length;w++)b.push([a,c[d],f[w]].join("-"));else b.push([a,c[d]].join("-"));for(a=0;a<b.length;a++)this.removeStyle(b[a])}},setStyle:function(a,b){this.$.style[CKEDITOR.tools.cssStyleToDomStyle(a)]=b;return this},setStyles:function(a){for(var b in a)this.setStyle(b,a[b]);return this},setOpacity:function(a){CKEDITOR.env.ie&&9>CKEDITOR.env.version?(a=Math.round(100*a),this.setStyle("filter",100<=a?"":"progid:DXImageTransform.Microsoft.Alpha(opacity\x3d"+a+")")):this.setStyle("opacity",a)},unselectable:function(){this.setStyles(CKEDITOR.tools.cssVendorPrefix("user-select",
+"none"));if(CKEDITOR.env.ie){this.setAttribute("unselectable","on");for(var a,b=this.getElementsByTag("*"),c=0,f=b.count();c<f;c++)a=b.getItem(c),a.setAttribute("unselectable","on")}},getPositionedAncestor:function(){for(var a=this;"html"!=a.getName();){if("static"!=a.getComputedStyle("position"))return a;a=a.getParent()}return null},getDocumentPosition:function(a){var b=0,c=0,f=this.getDocument(),d=f.getBody(),w="BackCompat"==f.$.compatMode;if(document.documentElement.getBoundingClientRect&&(CKEDITOR.env.ie?
+8!==CKEDITOR.env.version:1)){var e=this.$.getBoundingClientRect(),g=f.$.documentElement,x=g.clientTop||d.$.clientTop||0,m=g.clientLeft||d.$.clientLeft||0,h=!0;CKEDITOR.env.ie&&(h=f.getDocumentElement().contains(this),f=f.getBody().contains(this),h=w&&f||!w&&h);h&&(CKEDITOR.env.webkit||CKEDITOR.env.ie&&12<=CKEDITOR.env.version?(b=d.$.scrollLeft||g.scrollLeft,c=d.$.scrollTop||g.scrollTop):(c=w?d.$:g,b=c.scrollLeft,c=c.scrollTop),b=e.left+b-m,c=e.top+c-x)}else for(x=this,m=null;x&&"body"!=x.getName()&&
+"html"!=x.getName();){b+=x.$.offsetLeft-x.$.scrollLeft;c+=x.$.offsetTop-x.$.scrollTop;x.equals(this)||(b+=x.$.clientLeft||0,c+=x.$.clientTop||0);for(;m&&!m.equals(x);)b-=m.$.scrollLeft,c-=m.$.scrollTop,m=m.getParent();m=x;x=(e=x.$.offsetParent)?new CKEDITOR.dom.element(e):null}a&&(e=this.getWindow(),x=a.getWindow(),!e.equals(x)&&e.$.frameElement&&(a=(new CKEDITOR.dom.element(e.$.frameElement)).getDocumentPosition(a),b+=a.x,c+=a.y));document.documentElement.getBoundingClientRect||!CKEDITOR.env.gecko||
+w||(b+=this.$.clientLeft?1:0,c+=this.$.clientTop?1:0);return{x:b,y:c}},scrollIntoView:function(a){var b=this.getParent();if(b){do if((b.$.clientWidth&&b.$.clientWidth<b.$.scrollWidth||b.$.clientHeight&&b.$.clientHeight<b.$.scrollHeight)&&!b.is("body")&&this.scrollIntoParent(b,a,1),b.is("html")){var c=b.getWindow();try{var f=c.$.frameElement;f&&(b=new CKEDITOR.dom.element(f))}catch(d){}}while(b=b.getParent())}},scrollIntoParent:function(a,b,c){var f,d,w,e;function g(b,c){/body|html/.test(a.getName())?
+a.getWindow().$.scrollBy(b,c):(a.$.scrollLeft+=b,a.$.scrollTop+=c)}function x(a,b){var c={x:0,y:0};if(!a.is(h?"body":"html")){var f=a.$.getBoundingClientRect();c.x=f.left;c.y=f.top}f=a.getWindow();f.equals(b)||(f=x(CKEDITOR.dom.element.get(f.$.frameElement),b),c.x+=f.x,c.y+=f.y);return c}function m(a,b){return parseInt(a.getComputedStyle("margin-"+b)||0,10)||0}!a&&(a=this.getWindow());w=a.getDocument();var h="BackCompat"==w.$.compatMode;a instanceof CKEDITOR.dom.window&&(a=h?w.getBody():w.getDocumentElement());
+CKEDITOR.env.webkit&&(w=this.getEditor(!1))&&(w._.previousScrollTop=null);w=a.getWindow();d=x(this,w);var z=x(a,w),I=this.$.offsetHeight;f=this.$.offsetWidth;var l=a.$.clientHeight,t=a.$.clientWidth;w=d.x-m(this,"left")-z.x||0;e=d.y-m(this,"top")-z.y||0;f=d.x+f+m(this,"right")-(z.x+t)||0;d=d.y+I+m(this,"bottom")-(z.y+l)||0;(0>e||0<d)&&g(0,!0===b?e:!1===b?d:0>e?e:d);c&&(0>w||0<f)&&g(0>w?w:f,0)},setState:function(a,b,c){b=b||"cke";switch(a){case CKEDITOR.TRISTATE_ON:this.addClass(b+"_on");this.removeClass(b+
+"_off");this.removeClass(b+"_disabled");c&&this.setAttribute("aria-pressed",!0);c&&this.removeAttribute("aria-disabled");break;case CKEDITOR.TRISTATE_DISABLED:this.addClass(b+"_disabled");this.removeClass(b+"_off");this.removeClass(b+"_on");c&&this.setAttribute("aria-disabled",!0);c&&this.removeAttribute("aria-pressed");break;default:this.addClass(b+"_off"),this.removeClass(b+"_on"),this.removeClass(b+"_disabled"),c&&this.removeAttribute("aria-pressed"),c&&this.removeAttribute("aria-disabled")}},
+getFrameDocument:function(){var a=this.$;try{a.contentWindow.document}catch(b){a.src=a.src}return a&&new CKEDITOR.dom.document(a.contentWindow.document)},copyAttributes:function(a,b){var c=this.$.attributes;b=b||{};for(var f=0;f<c.length;f++){var d=c[f],w=d.nodeName.toLowerCase(),e;if(!(w in b))if("checked"==w&&(e=this.getAttribute(w)))a.setAttribute(w,e);else if(!CKEDITOR.env.ie||this.hasAttribute(w))e=this.getAttribute(w),null===e&&(e=d.nodeValue),a.setAttribute(w,e)}""!==this.$.style.cssText&&
+(a.$.style.cssText=this.$.style.cssText)},renameNode:function(a){if(this.getName()!=a){var b=this.getDocument();a=new CKEDITOR.dom.element(a,b);this.copyAttributes(a);this.moveChildren(a);this.getParent(!0)&&this.$.parentNode.replaceChild(a.$,this.$);a.$["data-cke-expando"]=this.$["data-cke-expando"];this.$=a.$;delete this.getName}},getChild:function(){function a(b,c){var f=b.childNodes;if(0<=c&&c<f.length)return f[c]}return function(b){var c=this.$;if(b.slice)for(b=b.slice();0<b.length&&c;)c=a(c,
+b.shift());else c=a(c,b);return c?new CKEDITOR.dom.node(c):null}}(),getChildCount:function(){return this.$.childNodes.length},disableContextMenu:function(){function a(b){return b.type==CKEDITOR.NODE_ELEMENT&&b.hasClass("cke_enable_context_menu")}this.on("contextmenu",function(b){b.data.getTarget().getAscendant(a,!0)||b.data.preventDefault()})},getDirection:function(a){return a?this.getComputedStyle("direction")||this.getDirection()||this.getParent()&&this.getParent().getDirection(1)||this.getDocument().$.dir||
+"ltr":this.getStyle("direction")||this.getAttribute("dir")},data:function(a,b){a="data-"+a;if(void 0===b)return this.getAttribute(a);!1===b?this.removeAttribute(a):this.setAttribute(a,b);return null},getEditor:function(a){var b=CKEDITOR.instances,c,f,d;a=a||void 0===a;for(c in b)if(f=b[c],f.element.equals(this)&&f.elementMode!=CKEDITOR.ELEMENT_MODE_APPENDTO||!a&&(d=f.editable())&&(d.equals(this)||d.contains(this)))return f;return null},find:function(a){var c=d(this);a=new CKEDITOR.dom.nodeList(this.$.querySelectorAll(b(this,
+a)));c();return a},findOne:function(a){var c=d(this);a=this.$.querySelector(b(this,a));c();return a?new CKEDITOR.dom.element(a):null},forEach:function(a,b,c){if(!(c||b&&this.type!=b))var f=a(this);if(!1!==f){c=this.getChildren();for(var d=0;d<c.count();d++)f=c.getItem(d),f.type==CKEDITOR.NODE_ELEMENT?f.forEach(a,b):b&&f.type!=b||a(f)}}});var h={width:["border-left-width","border-right-width","padding-left","padding-right"],height:["border-top-width","border-bottom-width","padding-top","padding-bottom"]};
+CKEDITOR.dom.element.prototype.setSize=function(a,b,d){"number"==typeof b&&(!d||CKEDITOR.env.ie&&CKEDITOR.env.quirks||(b-=c.call(this,a)),this.setStyle(a,b+"px"))};CKEDITOR.dom.element.prototype.getSize=function(a,b){var d=Math.max(this.$["offset"+CKEDITOR.tools.capitalize(a)],this.$["client"+CKEDITOR.tools.capitalize(a)])||0;b&&(d-=c.call(this,a));return d}})();CKEDITOR.dom.documentFragment=function(a){a=a||CKEDITOR.document;this.$=a.type==CKEDITOR.NODE_DOCUMENT?a.$.createDocumentFragment():a};
+CKEDITOR.tools.extend(CKEDITOR.dom.documentFragment.prototype,CKEDITOR.dom.element.prototype,{type:CKEDITOR.NODE_DOCUMENT_FRAGMENT,insertAfterNode:function(a){a=a.$;a.parentNode.insertBefore(this.$,a.nextSibling)},getHtml:function(){var a=new CKEDITOR.dom.element("div");this.clone(1,1).appendTo(a);return a.getHtml().replace(/\s*data-cke-expando=".*?"/g,"")}},!0,{append:1,appendBogus:1,clone:1,getFirst:1,getHtml:1,getLast:1,getParent:1,getNext:1,getPrevious:1,appendTo:1,moveChildren:1,insertBefore:1,
+insertAfterNode:1,replace:1,trim:1,type:1,ltrim:1,rtrim:1,getDocument:1,getChildCount:1,getChild:1,getChildren:1});
+(function(){function a(a,b){var c=this.range;if(this._.end)return null;if(!this._.start){this._.start=1;if(c.collapsed)return this.end(),null;c.optimize()}var f,d=c.startContainer;f=c.endContainer;var e=c.startOffset,D=c.endOffset,g,l=this.guard,t=this.type,J=a?"getPreviousSourceNode":"getNextSourceNode";if(!a&&!this._.guardLTR){var H=f.type==CKEDITOR.NODE_ELEMENT?f:f.getParent(),k=f.type==CKEDITOR.NODE_ELEMENT?f.getChild(D):f.getNext();this._.guardLTR=function(a,b){return(!b||!H.equals(a))&&(!k||
+!a.equals(k))&&(a.type!=CKEDITOR.NODE_ELEMENT||!b||!a.equals(c.root))}}if(a&&!this._.guardRTL){var q=d.type==CKEDITOR.NODE_ELEMENT?d:d.getParent(),y=d.type==CKEDITOR.NODE_ELEMENT?e?d.getChild(e-1):null:d.getPrevious();this._.guardRTL=function(a,b){return(!b||!q.equals(a))&&(!y||!a.equals(y))&&(a.type!=CKEDITOR.NODE_ELEMENT||!b||!a.equals(c.root))}}var v=a?this._.guardRTL:this._.guardLTR;g=l?function(a,b){return!1===v(a,b)?!1:l(a,b)}:v;this.current?f=this.current[J](!1,t,g):(a?f.type==CKEDITOR.NODE_ELEMENT&&
+(f=0<D?f.getChild(D-1):!1===g(f,!0)?null:f.getPreviousSourceNode(!0,t,g)):(f=d,f.type==CKEDITOR.NODE_ELEMENT&&((f=f.getChild(e))||(f=!1===g(d,!0)?null:d.getNextSourceNode(!0,t,g)))),f&&!1===g(f)&&(f=null));for(;f&&!this._.end;){this.current=f;if(!this.evaluator||!1!==this.evaluator(f)){if(!b)return f}else if(b&&this.evaluator)return!1;f=f[J](!1,t,g)}this.end();return this.current=null}function d(b){for(var c,f=null;c=a.call(this,b);)f=c;return f}CKEDITOR.dom.walker=CKEDITOR.tools.createClass({$:function(a){this.range=
+a;this._={}},proto:{end:function(){this._.end=1},next:function(){return a.call(this)},previous:function(){return a.call(this,1)},checkForward:function(){return!1!==a.call(this,0,1)},checkBackward:function(){return!1!==a.call(this,1,1)},lastForward:function(){return d.call(this)},lastBackward:function(){return d.call(this,1)},reset:function(){delete this.current;this._={}}}});var b={block:1,"list-item":1,table:1,"table-row-group":1,"table-header-group":1,"table-footer-group":1,"table-row":1,"table-column-group":1,
+"table-column":1,"table-cell":1,"table-caption":1},c={absolute:1,fixed:1};CKEDITOR.dom.element.prototype.isBlockBoundary=function(a){return"none"!=this.getComputedStyle("float")||this.getComputedStyle("position")in c||!b[this.getComputedStyle("display")]?!!(this.is(CKEDITOR.dtd.$block)||a&&this.is(a)):!0};CKEDITOR.dom.walker.blockBoundary=function(a){return function(b){return!(b.type==CKEDITOR.NODE_ELEMENT&&b.isBlockBoundary(a))}};CKEDITOR.dom.walker.listItemBoundary=function(){return this.blockBoundary({br:1})};
+CKEDITOR.dom.walker.bookmark=function(a,b){function c(a){return a&&a.getName&&"span"==a.getName()&&a.data("cke-bookmark")}return function(f){var d,e;d=f&&f.type!=CKEDITOR.NODE_ELEMENT&&(e=f.getParent())&&c(e);d=a?d:d||c(f);return!!(b^d)}};CKEDITOR.dom.walker.whitespaces=function(a){return function(b){var c;b&&b.type==CKEDITOR.NODE_TEXT&&(c=!CKEDITOR.tools.trim(b.getText())||CKEDITOR.env.webkit&&b.getText()==CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE);return!!(a^c)}};CKEDITOR.dom.walker.invisible=
+function(a){var b=CKEDITOR.dom.walker.whitespaces(),c=CKEDITOR.env.webkit?1:0;return function(f){b(f)?f=1:(f.type==CKEDITOR.NODE_TEXT&&(f=f.getParent()),f=f.$.offsetWidth<=c);return!!(a^f)}};CKEDITOR.dom.walker.nodeType=function(a,b){return function(c){return!!(b^c.type==a)}};CKEDITOR.dom.walker.bogus=function(a){function b(a){return!g(a)&&!h(a)}return function(c){var f=CKEDITOR.env.needsBrFiller?c.is&&c.is("br"):c.getText&&e.test(c.getText());f&&(f=c.getParent(),c=c.getNext(b),f=f.isBlockBoundary()&&
+(!c||c.type==CKEDITOR.NODE_ELEMENT&&c.isBlockBoundary()));return!!(a^f)}};CKEDITOR.dom.walker.temp=function(a){return function(b){b.type!=CKEDITOR.NODE_ELEMENT&&(b=b.getParent());b=b&&b.hasAttribute("data-cke-temp");return!!(a^b)}};var e=/^[\t\r\n ]*(?:&nbsp;|\xa0)$/,g=CKEDITOR.dom.walker.whitespaces(),h=CKEDITOR.dom.walker.bookmark(),k=CKEDITOR.dom.walker.temp(),n=function(a){return h(a)||g(a)||a.type==CKEDITOR.NODE_ELEMENT&&a.is(CKEDITOR.dtd.$inline)&&!a.is(CKEDITOR.dtd.$empty)};CKEDITOR.dom.walker.ignored=
+function(a){return function(b){b=g(b)||h(b)||k(b);return!!(a^b)}};var u=CKEDITOR.dom.walker.ignored();CKEDITOR.dom.walker.empty=function(a){return function(b){for(var c=0,f=b.getChildCount();c<f;++c)if(!u(b.getChild(c)))return!!a;return!a}};var f=CKEDITOR.dom.walker.empty(),D=CKEDITOR.dom.walker.validEmptyBlockContainers=CKEDITOR.tools.extend(function(a){var b={},c;for(c in a)CKEDITOR.dtd[c]["#"]&&(b[c]=1);return b}(CKEDITOR.dtd.$block),{caption:1,td:1,th:1});CKEDITOR.dom.walker.editable=function(a){return function(b){b=
+u(b)?!1:b.type==CKEDITOR.NODE_TEXT||b.type==CKEDITOR.NODE_ELEMENT&&(b.is(CKEDITOR.dtd.$inline)||b.is("hr")||"false"==b.getAttribute("contenteditable")||!CKEDITOR.env.needsBrFiller&&b.is(D)&&f(b))?!0:!1;return!!(a^b)}};CKEDITOR.dom.element.prototype.getBogus=function(){var a=this;do a=a.getPreviousSourceNode();while(n(a));return a&&(CKEDITOR.env.needsBrFiller?a.is&&a.is("br"):a.getText&&e.test(a.getText()))?a:!1}})();
+CKEDITOR.dom.range=function(a){this.endOffset=this.endContainer=this.startOffset=this.startContainer=null;this.collapsed=!0;var d=a instanceof CKEDITOR.dom.document;this.document=d?a:a.getDocument();this.root=d?a.getBody():a};
+(function(){function a(a){a.collapsed=a.startContainer&&a.endContainer&&a.startContainer.equals(a.endContainer)&&a.startOffset==a.endOffset}function d(a,b,c,d,e){function g(a,b,c,f){var d=c?a.getPrevious():a.getNext();if(f&&h)return d;l||f?b.append(a.clone(!0,e),c):(a.remove(),u&&b.append(a,c));return d}function m(){var a,b,c,f=Math.min(O.length,p.length);for(a=0;a<f;a++)if(b=O[a],c=p[a],!b.equals(c))return a;return a-1}function k(){var b=C-1,c=v&&B&&!t.equals(J);b<r-1||b<G-1||c?(c?a.moveToPosition(J,
+CKEDITOR.POSITION_BEFORE_START):G==b+1&&y?a.moveToPosition(p[b],CKEDITOR.POSITION_BEFORE_END):a.moveToPosition(p[b+1],CKEDITOR.POSITION_BEFORE_START),d&&(b=O[b+1])&&b.type==CKEDITOR.NODE_ELEMENT&&(c=CKEDITOR.dom.element.createFromHtml('\x3cspan data-cke-bookmark\x3d"1" style\x3d"display:none"\x3e\x26nbsp;\x3c/span\x3e',a.document),c.insertAfter(b),b.mergeSiblings(!1),a.moveToBookmark({startNode:c}))):a.collapse(!0)}a.optimizeBookmark();var h=0===b,u=1==b,l=2==b;b=l||u;var t=a.startContainer,J=a.endContainer,
+H=a.startOffset,E=a.endOffset,q,y,v,B,L,n;if(l&&J.type==CKEDITOR.NODE_TEXT&&(t.equals(J)||t.type===CKEDITOR.NODE_ELEMENT&&t.getFirst().equals(J)))c.append(a.document.createText(J.substring(H,E)));else{J.type==CKEDITOR.NODE_TEXT?l?n=!0:J=J.split(E):0<J.getChildCount()?E>=J.getChildCount()?(J=J.getChild(E-1),y=!0):J=J.getChild(E):B=y=!0;t.type==CKEDITOR.NODE_TEXT?l?L=!0:t.split(H):0<t.getChildCount()?0===H?(t=t.getChild(H),q=!0):t=t.getChild(H-1):v=q=!0;for(var O=t.getParents(),p=J.getParents(),C=m(),
+r=O.length-1,G=p.length-1,N=c,P,Y,V,ea=-1,Q=C;Q<=r;Q++){Y=O[Q];V=Y.getNext();for(Q!=r||Y.equals(p[Q])&&r<G?b&&(P=N.append(Y.clone(0,e))):q?g(Y,N,!1,v):L&&N.append(a.document.createText(Y.substring(H)));V;){if(V.equals(p[Q])){ea=Q;break}V=g(V,N)}N=P}N=c;for(Q=C;Q<=G;Q++)if(c=p[Q],V=c.getPrevious(),c.equals(O[Q]))b&&(N=N.getChild(0));else{Q!=G||c.equals(O[Q])&&G<r?b&&(P=N.append(c.clone(0,e))):y?g(c,N,!1,B):n&&N.append(a.document.createText(c.substring(0,E)));if(Q>ea)for(;V;)V=g(V,N,!0);N=P}l||k()}}
+function b(){var a=!1,b=CKEDITOR.dom.walker.whitespaces(),c=CKEDITOR.dom.walker.bookmark(!0),d=CKEDITOR.dom.walker.bogus();return function(e){return c(e)||b(e)?!0:d(e)&&!a?a=!0:e.type==CKEDITOR.NODE_TEXT&&(e.hasAscendant("pre")||CKEDITOR.tools.trim(e.getText()).length)||e.type==CKEDITOR.NODE_ELEMENT&&!e.is(g)?!1:!0}}function c(a){var b=CKEDITOR.dom.walker.whitespaces(),c=CKEDITOR.dom.walker.bookmark(1);return function(d){return c(d)||b(d)?!0:!a&&h(d)||d.type==CKEDITOR.NODE_ELEMENT&&d.is(CKEDITOR.dtd.$removeEmpty)}}
+function e(a){return function(){var b;return this[a?"getPreviousNode":"getNextNode"](function(a){!b&&u(a)&&(b=a);return n(a)&&!(h(a)&&a.equals(b))})}}var g={abbr:1,acronym:1,b:1,bdo:1,big:1,cite:1,code:1,del:1,dfn:1,em:1,font:1,i:1,ins:1,label:1,kbd:1,q:1,samp:1,small:1,span:1,strike:1,strong:1,sub:1,sup:1,tt:1,u:1,"var":1},h=CKEDITOR.dom.walker.bogus(),k=/^[\t\r\n ]*(?:&nbsp;|\xa0)$/,n=CKEDITOR.dom.walker.editable(),u=CKEDITOR.dom.walker.ignored(!0);CKEDITOR.dom.range.prototype={clone:function(){var a=
+new CKEDITOR.dom.range(this.root);a._setStartContainer(this.startContainer);a.startOffset=this.startOffset;a._setEndContainer(this.endContainer);a.endOffset=this.endOffset;a.collapsed=this.collapsed;return a},collapse:function(a){a?(this._setEndContainer(this.startContainer),this.endOffset=this.startOffset):(this._setStartContainer(this.endContainer),this.startOffset=this.endOffset);this.collapsed=!0},cloneContents:function(a){var b=new CKEDITOR.dom.documentFragment(this.document);this.collapsed||
+d(this,2,b,!1,"undefined"==typeof a?!0:a);return b},deleteContents:function(a){this.collapsed||d(this,0,null,a)},extractContents:function(a,b){var c=new CKEDITOR.dom.documentFragment(this.document);this.collapsed||d(this,1,c,a,"undefined"==typeof b?!0:b);return c},createBookmark:function(a){var b,c,d,e,g=this.collapsed;b=this.document.createElement("span");b.data("cke-bookmark",1);b.setStyle("display","none");b.setHtml("\x26nbsp;");a&&(d="cke_bm_"+CKEDITOR.tools.getNextNumber(),b.setAttribute("id",
+d+(g?"C":"S")));g||(c=b.clone(),c.setHtml("\x26nbsp;"),a&&c.setAttribute("id",d+"E"),e=this.clone(),e.collapse(),e.insertNode(c));e=this.clone();e.collapse(!0);e.insertNode(b);c?(this.setStartAfter(b),this.setEndBefore(c)):this.moveToPosition(b,CKEDITOR.POSITION_AFTER_END);return{startNode:a?d+(g?"C":"S"):b,endNode:a?d+"E":c,serializable:a,collapsed:g}},createBookmark2:function(){function a(b){var f=b.container,d=b.offset,e;e=f;var g=d;e=e.type!=CKEDITOR.NODE_ELEMENT||0===g||g==e.getChildCount()?
+0:e.getChild(g-1).type==CKEDITOR.NODE_TEXT&&e.getChild(g).type==CKEDITOR.NODE_TEXT;e&&(f=f.getChild(d-1),d=f.getLength());if(f.type==CKEDITOR.NODE_ELEMENT&&0<d){a:{for(e=f;d--;)if(g=e.getChild(d).getIndex(!0),0<=g){d=g;break a}d=-1}d+=1}if(f.type==CKEDITOR.NODE_TEXT){e=f;for(g=0;(e=e.getPrevious())&&e.type==CKEDITOR.NODE_TEXT;)g+=e.getText().replace(CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE,"").length;e=g;f.getText()?d+=e:(g=f.getPrevious(c),e?(d=e,f=g?g.getNext():f.getParent().getFirst()):(f=
+f.getParent(),d=g?g.getIndex(!0)+1:0))}b.container=f;b.offset=d}function b(a,c){var f=c.getCustomData("cke-fillingChar");if(f){var d=a.container;f.equals(d)&&(a.offset-=CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE.length,0>=a.offset&&(a.offset=d.getIndex(),a.container=d.getParent()))}}var c=CKEDITOR.dom.walker.nodeType(CKEDITOR.NODE_TEXT,!0);return function(c){var d=this.collapsed,e={container:this.startContainer,offset:this.startOffset},m={container:this.endContainer,offset:this.endOffset};c&&(a(e),
+b(e,this.root),d||(a(m),b(m,this.root)));return{start:e.container.getAddress(c),end:d?null:m.container.getAddress(c),startOffset:e.offset,endOffset:m.offset,normalized:c,collapsed:d,is2:!0}}}(),moveToBookmark:function(a){if(a.is2){var b=this.document.getByAddress(a.start,a.normalized),c=a.startOffset,d=a.end&&this.document.getByAddress(a.end,a.normalized);a=a.endOffset;this.setStart(b,c);d?this.setEnd(d,a):this.collapse(!0)}else b=(c=a.serializable)?this.document.getById(a.startNode):a.startNode,
+a=c?this.document.getById(a.endNode):a.endNode,this.setStartBefore(b),b.remove(),a?(this.setEndBefore(a),a.remove()):this.collapse(!0)},getBoundaryNodes:function(){var a=this.startContainer,b=this.endContainer,c=this.startOffset,d=this.endOffset,e;if(a.type==CKEDITOR.NODE_ELEMENT)if(e=a.getChildCount(),e>c)a=a.getChild(c);else if(1>e)a=a.getPreviousSourceNode();else{for(a=a.$;a.lastChild;)a=a.lastChild;a=new CKEDITOR.dom.node(a);a=a.getNextSourceNode()||a}if(b.type==CKEDITOR.NODE_ELEMENT)if(e=b.getChildCount(),
+e>d)b=b.getChild(d).getPreviousSourceNode(!0);else if(1>e)b=b.getPreviousSourceNode();else{for(b=b.$;b.lastChild;)b=b.lastChild;b=new CKEDITOR.dom.node(b)}a.getPosition(b)&CKEDITOR.POSITION_FOLLOWING&&(a=b);return{startNode:a,endNode:b}},getCommonAncestor:function(a,b){var c=this.startContainer,d=this.endContainer,c=c.equals(d)?a&&c.type==CKEDITOR.NODE_ELEMENT&&this.startOffset==this.endOffset-1?c.getChild(this.startOffset):c:c.getCommonAncestor(d);return b&&!c.is?c.getParent():c},optimize:function(){var a=
+this.startContainer,b=this.startOffset;a.type!=CKEDITOR.NODE_ELEMENT&&(b?b>=a.getLength()&&this.setStartAfter(a):this.setStartBefore(a));a=this.endContainer;b=this.endOffset;a.type!=CKEDITOR.NODE_ELEMENT&&(b?b>=a.getLength()&&this.setEndAfter(a):this.setEndBefore(a))},optimizeBookmark:function(){var a=this.startContainer,b=this.endContainer;a.is&&a.is("span")&&a.data("cke-bookmark")&&this.setStartAt(a,CKEDITOR.POSITION_BEFORE_START);b&&b.is&&b.is("span")&&b.data("cke-bookmark")&&this.setEndAt(b,CKEDITOR.POSITION_AFTER_END)},
+trim:function(a,b){var c=this.startContainer,d=this.startOffset,e=this.collapsed;if((!a||e)&&c&&c.type==CKEDITOR.NODE_TEXT){if(d)if(d>=c.getLength())d=c.getIndex()+1,c=c.getParent();else{var g=c.split(d),d=c.getIndex()+1,c=c.getParent();this.startContainer.equals(this.endContainer)?this.setEnd(g,this.endOffset-this.startOffset):c.equals(this.endContainer)&&(this.endOffset+=1)}else d=c.getIndex(),c=c.getParent();this.setStart(c,d);if(e){this.collapse(!0);return}}c=this.endContainer;d=this.endOffset;
+b||e||!c||c.type!=CKEDITOR.NODE_TEXT||(d?(d>=c.getLength()||c.split(d),d=c.getIndex()+1):d=c.getIndex(),c=c.getParent(),this.setEnd(c,d))},enlarge:function(a,b){function c(a){return a&&a.type==CKEDITOR.NODE_ELEMENT&&a.hasAttribute("contenteditable")?null:a}var d=new RegExp(/[^\s\ufeff]/);switch(a){case CKEDITOR.ENLARGE_INLINE:var e=1;case CKEDITOR.ENLARGE_ELEMENT:var g=function(a,b){var c=new CKEDITOR.dom.range(k);c.setStart(a,b);c.setEndAt(k,CKEDITOR.POSITION_BEFORE_END);var c=new CKEDITOR.dom.walker(c),
+f;for(c.guard=function(a){return!(a.type==CKEDITOR.NODE_ELEMENT&&a.isBlockBoundary())};f=c.next();){if(f.type!=CKEDITOR.NODE_TEXT)return!1;q=f!=a?f.getText():f.substring(b);if(d.test(q))return!1}return!0};if(this.collapsed)break;var m=this.getCommonAncestor(),k=this.root,h,u,l,t,J,H=!1,E,q;E=this.startContainer;var y=this.startOffset;E.type==CKEDITOR.NODE_TEXT?(y&&(E=!CKEDITOR.tools.trim(E.substring(0,y)).length&&E,H=!!E),E&&((t=E.getPrevious())||(l=E.getParent()))):(y&&(t=E.getChild(y-1)||E.getLast()),
+t||(l=E));for(l=c(l);l||t;){if(l&&!t){!J&&l.equals(m)&&(J=!0);if(e?l.isBlockBoundary():!k.contains(l))break;H&&"inline"==l.getComputedStyle("display")||(H=!1,J?h=l:this.setStartBefore(l));t=l.getPrevious()}for(;t;)if(E=!1,t.type==CKEDITOR.NODE_COMMENT)t=t.getPrevious();else{if(t.type==CKEDITOR.NODE_TEXT)q=t.getText(),d.test(q)&&(t=null),E=/[\s\ufeff]$/.test(q);else if((t.$.offsetWidth>(CKEDITOR.env.webkit?1:0)||b&&t.is("br"))&&!t.data("cke-bookmark"))if(H&&CKEDITOR.dtd.$removeEmpty[t.getName()]){q=
+t.getText();if(d.test(q))t=null;else for(var y=t.$.getElementsByTagName("*"),v=0,B;B=y[v++];)if(!CKEDITOR.dtd.$removeEmpty[B.nodeName.toLowerCase()]){t=null;break}t&&(E=!!q.length)}else t=null;E&&(H?J?h=l:l&&this.setStartBefore(l):H=!0);if(t){E=t.getPrevious();if(!l&&!E){l=t;t=null;break}t=E}else l=null}l&&(l=c(l.getParent()))}E=this.endContainer;y=this.endOffset;l=t=null;J=H=!1;E.type==CKEDITOR.NODE_TEXT?CKEDITOR.tools.trim(E.substring(y)).length?H=!0:(H=!E.getLength(),y==E.getLength()?(t=E.getNext())||
+(l=E.getParent()):g(E,y)&&(l=E.getParent())):(t=E.getChild(y))||(l=E);for(;l||t;){if(l&&!t){!J&&l.equals(m)&&(J=!0);if(e?l.isBlockBoundary():!k.contains(l))break;H&&"inline"==l.getComputedStyle("display")||(H=!1,J?u=l:l&&this.setEndAfter(l));t=l.getNext()}for(;t;){E=!1;if(t.type==CKEDITOR.NODE_TEXT)q=t.getText(),g(t,0)||(t=null),E=/^[\s\ufeff]/.test(q);else if(t.type==CKEDITOR.NODE_ELEMENT){if((0<t.$.offsetWidth||b&&t.is("br"))&&!t.data("cke-bookmark"))if(H&&CKEDITOR.dtd.$removeEmpty[t.getName()]){q=
+t.getText();if(d.test(q))t=null;else for(y=t.$.getElementsByTagName("*"),v=0;B=y[v++];)if(!CKEDITOR.dtd.$removeEmpty[B.nodeName.toLowerCase()]){t=null;break}t&&(E=!!q.length)}else t=null}else E=1;E&&H&&(J?u=l:this.setEndAfter(l));if(t){E=t.getNext();if(!l&&!E){l=t;t=null;break}t=E}else l=null}l&&(l=c(l.getParent()))}h&&u&&(m=h.contains(u)?u:h,this.setStartBefore(m),this.setEndAfter(m));break;case CKEDITOR.ENLARGE_BLOCK_CONTENTS:case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:l=new CKEDITOR.dom.range(this.root);
+k=this.root;l.setStartAt(k,CKEDITOR.POSITION_AFTER_START);l.setEnd(this.startContainer,this.startOffset);l=new CKEDITOR.dom.walker(l);var L,n,O=CKEDITOR.dom.walker.blockBoundary(a==CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS?{br:1}:null),p=null,C=function(a){if(a.type==CKEDITOR.NODE_ELEMENT&&"false"==a.getAttribute("contenteditable"))if(p){if(p.equals(a)){p=null;return}}else p=a;else if(p)return;var b=O(a);b||(L=a);return b},e=function(a){var b=C(a);!b&&a.is&&a.is("br")&&(n=a);return b};l.guard=C;l=l.lastBackward();
+L=L||k;this.setStartAt(L,!L.is("br")&&(!l&&this.checkStartOfBlock()||l&&L.contains(l))?CKEDITOR.POSITION_AFTER_START:CKEDITOR.POSITION_AFTER_END);if(a==CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS){l=this.clone();l=new CKEDITOR.dom.walker(l);var r=CKEDITOR.dom.walker.whitespaces(),G=CKEDITOR.dom.walker.bookmark();l.evaluator=function(a){return!r(a)&&!G(a)};if((l=l.previous())&&l.type==CKEDITOR.NODE_ELEMENT&&l.is("br"))break}l=this.clone();l.collapse();l.setEndAt(k,CKEDITOR.POSITION_BEFORE_END);l=new CKEDITOR.dom.walker(l);
+l.guard=a==CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS?e:C;L=p=n=null;l=l.lastForward();L=L||k;this.setEndAt(L,!l&&this.checkEndOfBlock()||l&&L.contains(l)?CKEDITOR.POSITION_BEFORE_END:CKEDITOR.POSITION_BEFORE_START);n&&this.setEndAfter(n)}},shrink:function(a,b,c){var d="boolean"===typeof c?c:c&&"boolean"===typeof c.shrinkOnBlockBoundary?c.shrinkOnBlockBoundary:!0,e=c&&c.skipBogus;if(!this.collapsed){a=a||CKEDITOR.SHRINK_TEXT;var g=this.clone(),m=this.startContainer,k=this.endContainer,h=this.startOffset,
+u=this.endOffset,l=c=1;m&&m.type==CKEDITOR.NODE_TEXT&&(h?h>=m.getLength()?g.setStartAfter(m):(g.setStartBefore(m),c=0):g.setStartBefore(m));k&&k.type==CKEDITOR.NODE_TEXT&&(u?u>=k.getLength()?g.setEndAfter(k):(g.setEndAfter(k),l=0):g.setEndBefore(k));var g=new CKEDITOR.dom.walker(g),t=CKEDITOR.dom.walker.bookmark(),J=CKEDITOR.dom.walker.bogus();g.evaluator=function(b){return b.type==(a==CKEDITOR.SHRINK_ELEMENT?CKEDITOR.NODE_ELEMENT:CKEDITOR.NODE_TEXT)};var H;g.guard=function(b,c){if(e&&J(b)||t(b))return!0;
+if(a==CKEDITOR.SHRINK_ELEMENT&&b.type==CKEDITOR.NODE_TEXT||c&&b.equals(H)||!1===d&&b.type==CKEDITOR.NODE_ELEMENT&&b.isBlockBoundary()||b.type==CKEDITOR.NODE_ELEMENT&&b.hasAttribute("contenteditable"))return!1;c||b.type!=CKEDITOR.NODE_ELEMENT||(H=b);return!0};c&&(m=g[a==CKEDITOR.SHRINK_ELEMENT?"lastForward":"next"]())&&this.setStartAt(m,b?CKEDITOR.POSITION_AFTER_START:CKEDITOR.POSITION_BEFORE_START);l&&(g.reset(),(g=g[a==CKEDITOR.SHRINK_ELEMENT?"lastBackward":"previous"]())&&this.setEndAt(g,b?CKEDITOR.POSITION_BEFORE_END:
+CKEDITOR.POSITION_AFTER_END));return!(!c&&!l)}},insertNode:function(a){this.optimizeBookmark();this.trim(!1,!0);var b=this.startContainer,c=b.getChild(this.startOffset);c?a.insertBefore(c):b.append(a);a.getParent()&&a.getParent().equals(this.endContainer)&&this.endOffset++;this.setStartBefore(a)},moveToPosition:function(a,b){this.setStartAt(a,b);this.collapse(!0)},moveToRange:function(a){this.setStart(a.startContainer,a.startOffset);this.setEnd(a.endContainer,a.endOffset)},selectNodeContents:function(a){this.setStart(a,
+0);this.setEnd(a,a.type==CKEDITOR.NODE_TEXT?a.getLength():a.getChildCount())},setStart:function(b,c){b.type==CKEDITOR.NODE_ELEMENT&&CKEDITOR.dtd.$empty[b.getName()]&&(c=b.getIndex(),b=b.getParent());this._setStartContainer(b);this.startOffset=c;this.endContainer||(this._setEndContainer(b),this.endOffset=c);a(this)},setEnd:function(b,c){b.type==CKEDITOR.NODE_ELEMENT&&CKEDITOR.dtd.$empty[b.getName()]&&(c=b.getIndex()+1,b=b.getParent());this._setEndContainer(b);this.endOffset=c;this.startContainer||
+(this._setStartContainer(b),this.startOffset=c);a(this)},setStartAfter:function(a){this.setStart(a.getParent(),a.getIndex()+1)},setStartBefore:function(a){this.setStart(a.getParent(),a.getIndex())},setEndAfter:function(a){this.setEnd(a.getParent(),a.getIndex()+1)},setEndBefore:function(a){this.setEnd(a.getParent(),a.getIndex())},setStartAt:function(b,c){switch(c){case CKEDITOR.POSITION_AFTER_START:this.setStart(b,0);break;case CKEDITOR.POSITION_BEFORE_END:b.type==CKEDITOR.NODE_TEXT?this.setStart(b,
+b.getLength()):this.setStart(b,b.getChildCount());break;case CKEDITOR.POSITION_BEFORE_START:this.setStartBefore(b);break;case CKEDITOR.POSITION_AFTER_END:this.setStartAfter(b)}a(this)},setEndAt:function(b,c){switch(c){case CKEDITOR.POSITION_AFTER_START:this.setEnd(b,0);break;case CKEDITOR.POSITION_BEFORE_END:b.type==CKEDITOR.NODE_TEXT?this.setEnd(b,b.getLength()):this.setEnd(b,b.getChildCount());break;case CKEDITOR.POSITION_BEFORE_START:this.setEndBefore(b);break;case CKEDITOR.POSITION_AFTER_END:this.setEndAfter(b)}a(this)},
+fixBlock:function(a,b){var c=this.createBookmark(),d=this.document.createElement(b);this.collapse(a);this.enlarge(CKEDITOR.ENLARGE_BLOCK_CONTENTS);this.extractContents().appendTo(d);d.trim();this.insertNode(d);var e=d.getBogus();e&&e.remove();d.appendBogus();this.moveToBookmark(c);return d},splitBlock:function(a,b){var c=new CKEDITOR.dom.elementPath(this.startContainer,this.root),d=new CKEDITOR.dom.elementPath(this.endContainer,this.root),e=c.block,g=d.block,m=null;if(!c.blockLimit.equals(d.blockLimit))return null;
+"br"!=a&&(e||(e=this.fixBlock(!0,a),g=(new CKEDITOR.dom.elementPath(this.endContainer,this.root)).block),g||(g=this.fixBlock(!1,a)));c=e&&this.checkStartOfBlock();d=g&&this.checkEndOfBlock();this.deleteContents();e&&e.equals(g)&&(d?(m=new CKEDITOR.dom.elementPath(this.startContainer,this.root),this.moveToPosition(g,CKEDITOR.POSITION_AFTER_END),g=null):c?(m=new CKEDITOR.dom.elementPath(this.startContainer,this.root),this.moveToPosition(e,CKEDITOR.POSITION_BEFORE_START),e=null):(g=this.splitElement(e,
+b||!1),e.is("ul","ol")||e.appendBogus()));return{previousBlock:e,nextBlock:g,wasStartOfBlock:c,wasEndOfBlock:d,elementPath:m}},splitElement:function(a,b){if(!this.collapsed)return null;this.setEndAt(a,CKEDITOR.POSITION_BEFORE_END);var c=this.extractContents(!1,b||!1),d=a.clone(!1,b||!1);c.appendTo(d);d.insertAfter(a);this.moveToPosition(a,CKEDITOR.POSITION_AFTER_END);return d},removeEmptyBlocksAtEnd:function(){function a(d){return function(a){return b(a)||c(a)||a.type==CKEDITOR.NODE_ELEMENT&&a.isEmptyInlineRemoveable()||
+d.is("table")&&a.is("caption")?!1:!0}}var b=CKEDITOR.dom.walker.whitespaces(),c=CKEDITOR.dom.walker.bookmark(!1);return function(b){for(var c=this.createBookmark(),d=this[b?"endPath":"startPath"](),e=d.block||d.blockLimit,g;e&&!e.equals(d.root)&&!e.getFirst(a(e));)g=e.getParent(),this[b?"setEndAt":"setStartAt"](e,CKEDITOR.POSITION_AFTER_END),e.remove(1),e=g;this.moveToBookmark(c)}}(),startPath:function(){return new CKEDITOR.dom.elementPath(this.startContainer,this.root)},endPath:function(){return new CKEDITOR.dom.elementPath(this.endContainer,
+this.root)},checkBoundaryOfElement:function(a,b){var d=b==CKEDITOR.START,e=this.clone();e.collapse(d);e[d?"setStartAt":"setEndAt"](a,d?CKEDITOR.POSITION_AFTER_START:CKEDITOR.POSITION_BEFORE_END);e=new CKEDITOR.dom.walker(e);e.evaluator=c(d);return e[d?"checkBackward":"checkForward"]()},checkStartOfBlock:function(){var a=this.startContainer,c=this.startOffset;CKEDITOR.env.ie&&c&&a.type==CKEDITOR.NODE_TEXT&&(a=CKEDITOR.tools.ltrim(a.substring(0,c)),k.test(a)&&this.trim(0,1));this.trim();a=new CKEDITOR.dom.elementPath(this.startContainer,
+this.root);c=this.clone();c.collapse(!0);c.setStartAt(a.block||a.blockLimit,CKEDITOR.POSITION_AFTER_START);a=new CKEDITOR.dom.walker(c);a.evaluator=b();return a.checkBackward()},checkEndOfBlock:function(){var a=this.endContainer,c=this.endOffset;CKEDITOR.env.ie&&a.type==CKEDITOR.NODE_TEXT&&(a=CKEDITOR.tools.rtrim(a.substring(c)),k.test(a)&&this.trim(1,0));this.trim();a=new CKEDITOR.dom.elementPath(this.endContainer,this.root);c=this.clone();c.collapse(!1);c.setEndAt(a.block||a.blockLimit,CKEDITOR.POSITION_BEFORE_END);
+a=new CKEDITOR.dom.walker(c);a.evaluator=b();return a.checkForward()},getPreviousNode:function(a,b,c){var d=this.clone();d.collapse(1);d.setStartAt(c||this.root,CKEDITOR.POSITION_AFTER_START);c=new CKEDITOR.dom.walker(d);c.evaluator=a;c.guard=b;return c.previous()},getNextNode:function(a,b,c){var d=this.clone();d.collapse();d.setEndAt(c||this.root,CKEDITOR.POSITION_BEFORE_END);c=new CKEDITOR.dom.walker(d);c.evaluator=a;c.guard=b;return c.next()},checkReadOnly:function(){function a(b,c){for(;b;){if(b.type==
+CKEDITOR.NODE_ELEMENT){if("false"==b.getAttribute("contentEditable")&&!b.data("cke-editable"))return 0;if(b.is("html")||"true"==b.getAttribute("contentEditable")&&(b.contains(c)||b.equals(c)))break}b=b.getParent()}return 1}return function(){var b=this.startContainer,c=this.endContainer;return!(a(b,c)&&a(c,b))}}(),moveToElementEditablePosition:function(a,b){if(a.type==CKEDITOR.NODE_ELEMENT&&!a.isEditable(!1))return this.moveToPosition(a,b?CKEDITOR.POSITION_AFTER_END:CKEDITOR.POSITION_BEFORE_START),
+!0;for(var c=0;a;){if(a.type==CKEDITOR.NODE_TEXT){b&&this.endContainer&&this.checkEndOfBlock()&&k.test(a.getText())?this.moveToPosition(a,CKEDITOR.POSITION_BEFORE_START):this.moveToPosition(a,b?CKEDITOR.POSITION_AFTER_END:CKEDITOR.POSITION_BEFORE_START);c=1;break}if(a.type==CKEDITOR.NODE_ELEMENT)if(a.isEditable())this.moveToPosition(a,b?CKEDITOR.POSITION_BEFORE_END:CKEDITOR.POSITION_AFTER_START),c=1;else if(b&&a.is("br")&&this.endContainer&&this.checkEndOfBlock())this.moveToPosition(a,CKEDITOR.POSITION_BEFORE_START);
+else if("false"==a.getAttribute("contenteditable")&&a.is(CKEDITOR.dtd.$block))return this.setStartBefore(a),this.setEndAfter(a),!0;var d=a,e=c,g=void 0;d.type==CKEDITOR.NODE_ELEMENT&&d.isEditable(!1)&&(g=d[b?"getLast":"getFirst"](u));e||g||(g=d[b?"getPrevious":"getNext"](u));a=g}return!!c},moveToClosestEditablePosition:function(a,b){var c,d=0,e,g,m=[CKEDITOR.POSITION_AFTER_END,CKEDITOR.POSITION_BEFORE_START];a?(c=new CKEDITOR.dom.range(this.root),c.moveToPosition(a,m[b?0:1])):c=this.clone();if(a&&
+!a.is(CKEDITOR.dtd.$block))d=1;else if(e=c[b?"getNextEditableNode":"getPreviousEditableNode"]())d=1,(g=e.type==CKEDITOR.NODE_ELEMENT)&&e.is(CKEDITOR.dtd.$block)&&"false"==e.getAttribute("contenteditable")?(c.setStartAt(e,CKEDITOR.POSITION_BEFORE_START),c.setEndAt(e,CKEDITOR.POSITION_AFTER_END)):!CKEDITOR.env.needsBrFiller&&g&&e.is(CKEDITOR.dom.walker.validEmptyBlockContainers)?(c.setEnd(e,0),c.collapse()):c.moveToPosition(e,m[b?1:0]);d&&this.moveToRange(c);return!!d},moveToElementEditStart:function(a){return this.moveToElementEditablePosition(a)},
+moveToElementEditEnd:function(a){return this.moveToElementEditablePosition(a,!0)},getEnclosedNode:function(){var a=this.clone();a.optimize();if(a.startContainer.type!=CKEDITOR.NODE_ELEMENT||a.endContainer.type!=CKEDITOR.NODE_ELEMENT)return null;var a=new CKEDITOR.dom.walker(a),b=CKEDITOR.dom.walker.bookmark(!1,!0),c=CKEDITOR.dom.walker.whitespaces(!0);a.evaluator=function(a){return c(a)&&b(a)};var d=a.next();a.reset();return d&&d.equals(a.previous())?d:null},getTouchedStartNode:function(){var a=this.startContainer;
+return this.collapsed||a.type!=CKEDITOR.NODE_ELEMENT?a:a.getChild(this.startOffset)||a},getTouchedEndNode:function(){var a=this.endContainer;return this.collapsed||a.type!=CKEDITOR.NODE_ELEMENT?a:a.getChild(this.endOffset-1)||a},getNextEditableNode:e(),getPreviousEditableNode:e(1),_getTableElement:function(a){a=a||{td:1,th:1,tr:1,tbody:1,thead:1,tfoot:1,table:1};var b=this.startContainer,c=this.endContainer,d=b.getAscendant("table",!0),e=c.getAscendant("table",!0);return CKEDITOR.env.safari&&d&&c.equals(this.root)?
+b.getAscendant(a,!0):this.getEnclosedNode()?this.getEnclosedNode().getAscendant(a,!0):d&&e&&(d.equals(e)||d.contains(e)||e.contains(d))?b.getAscendant(a,!0):null},scrollIntoView:function(){var a=new CKEDITOR.dom.element.createFromHtml("\x3cspan\x3e\x26nbsp;\x3c/span\x3e",this.document),b,c,d,e=this.clone();e.optimize();(d=e.startContainer.type==CKEDITOR.NODE_TEXT)?(c=e.startContainer.getText(),b=e.startContainer.split(e.startOffset),a.insertAfter(e.startContainer)):e.insertNode(a);a.scrollIntoView();
+d&&(e.startContainer.setText(c),b.remove());a.remove()},_setStartContainer:function(a){this.startContainer=a},_setEndContainer:function(a){this.endContainer=a},_find:function(a,b){var c=this.getCommonAncestor(),d=this.getBoundaryNodes(),e=[],g,m,k,h;if(c&&c.find)for(m=c.find(a),g=0;g<m.count();g++)if(c=m.getItem(g),b||!c.isReadOnly())k=c.getPosition(d.startNode)&CKEDITOR.POSITION_FOLLOWING||d.startNode.equals(c),h=c.getPosition(d.endNode)&CKEDITOR.POSITION_PRECEDING+CKEDITOR.POSITION_IS_CONTAINED||
+d.endNode.equals(c),k&&h&&e.push(c);return e}};CKEDITOR.dom.range.mergeRanges=function(a){return CKEDITOR.tools.array.reduce(a,function(a,b){var c=a[a.length-1],d=!1;b=b.clone();b.enlarge(CKEDITOR.ENLARGE_ELEMENT);if(c){var e=new CKEDITOR.dom.range(b.root),d=new CKEDITOR.dom.walker(e),f=CKEDITOR.dom.walker.whitespaces();e.setStart(c.endContainer,c.endOffset);e.setEnd(b.startContainer,b.startOffset);for(e=d.next();f(e)||b.endContainer.equals(e);)e=d.next();d=!e}d?c.setEnd(b.endContainer,b.endOffset):
+a.push(b);return a},[])}})();CKEDITOR.POSITION_AFTER_START=1;CKEDITOR.POSITION_BEFORE_END=2;CKEDITOR.POSITION_BEFORE_START=3;CKEDITOR.POSITION_AFTER_END=4;CKEDITOR.ENLARGE_ELEMENT=1;CKEDITOR.ENLARGE_BLOCK_CONTENTS=2;CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS=3;CKEDITOR.ENLARGE_INLINE=4;CKEDITOR.START=1;CKEDITOR.END=2;CKEDITOR.SHRINK_ELEMENT=1;CKEDITOR.SHRINK_TEXT=2;"use strict";
+(function(){function a(a){1>arguments.length||(this.range=a,this.forceBrBreak=0,this.enlargeBr=1,this.enforceRealBlocks=0,this._||(this._={}))}function d(a){var b=[];a.forEach(function(a){if("true"==a.getAttribute("contenteditable"))return b.push(a),!1},CKEDITOR.NODE_ELEMENT,!0);return b}function b(a,c,e,g){a:{null==g&&(g=d(e));for(var k;k=g.shift();)if(k.getDtd().p){g={element:k,remaining:g};break a}g=null}if(!g)return 0;if((k=CKEDITOR.filter.instances[g.element.data("cke-filter")])&&!k.check(c))return b(a,
+c,e,g.remaining);c=new CKEDITOR.dom.range(g.element);c.selectNodeContents(g.element);c=c.createIterator();c.enlargeBr=a.enlargeBr;c.enforceRealBlocks=a.enforceRealBlocks;c.activeFilter=c.filter=k;a._.nestedEditable={element:g.element,container:e,remaining:g.remaining,iterator:c};return 1}function c(a,b,c){if(!b)return!1;a=a.clone();a.collapse(!c);return a.checkBoundaryOfElement(b,c?CKEDITOR.START:CKEDITOR.END)}var e=/^[\r\n\t ]+$/,g=CKEDITOR.dom.walker.bookmark(!1,!0),h=CKEDITOR.dom.walker.whitespaces(!0),
+k=function(a){return g(a)&&h(a)},n={dd:1,dt:1,li:1};a.prototype={getNextParagraph:function(a){var d,h,w,A,F;a=a||"p";if(this._.nestedEditable){if(d=this._.nestedEditable.iterator.getNextParagraph(a))return this.activeFilter=this._.nestedEditable.iterator.activeFilter,d;this.activeFilter=this.filter;if(b(this,a,this._.nestedEditable.container,this._.nestedEditable.remaining))return this.activeFilter=this._.nestedEditable.iterator.activeFilter,this._.nestedEditable.iterator.getNextParagraph(a);this._.nestedEditable=
+null}if(!this.range.root.getDtd()[a])return null;if(!this._.started){var x=this.range.clone();h=x.startPath();var m=x.endPath(),K=!x.collapsed&&c(x,h.block),z=!x.collapsed&&c(x,m.block,1);x.shrink(CKEDITOR.SHRINK_ELEMENT,!0);K&&x.setStartAt(h.block,CKEDITOR.POSITION_BEFORE_END);z&&x.setEndAt(m.block,CKEDITOR.POSITION_AFTER_START);h=x.endContainer.hasAscendant("pre",!0)||x.startContainer.hasAscendant("pre",!0);x.enlarge(this.forceBrBreak&&!h||!this.enlargeBr?CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:CKEDITOR.ENLARGE_BLOCK_CONTENTS);
+x.collapsed||(h=new CKEDITOR.dom.walker(x.clone()),m=CKEDITOR.dom.walker.bookmark(!0,!0),h.evaluator=m,this._.nextNode=h.next(),h=new CKEDITOR.dom.walker(x.clone()),h.evaluator=m,h=h.previous(),this._.lastNode=h.getNextSourceNode(!0,null,x.root),this._.lastNode&&this._.lastNode.type==CKEDITOR.NODE_TEXT&&!CKEDITOR.tools.trim(this._.lastNode.getText())&&this._.lastNode.getParent().isBlockBoundary()&&(m=this.range.clone(),m.moveToPosition(this._.lastNode,CKEDITOR.POSITION_AFTER_END),m.checkEndOfBlock()&&
+(m=new CKEDITOR.dom.elementPath(m.endContainer,m.root),this._.lastNode=(m.block||m.blockLimit).getNextSourceNode(!0))),this._.lastNode&&x.root.contains(this._.lastNode)||(this._.lastNode=this._.docEndMarker=x.document.createText(""),this._.lastNode.insertAfter(h)),x=null);this._.started=1;h=x}m=this._.nextNode;x=this._.lastNode;for(this._.nextNode=null;m;){var K=0,z=m.hasAscendant("pre"),I=m.type!=CKEDITOR.NODE_ELEMENT,l=0;if(I)m.type==CKEDITOR.NODE_TEXT&&e.test(m.getText())&&(I=0);else{var t=m.getName();
+if(CKEDITOR.dtd.$block[t]&&"false"==m.getAttribute("contenteditable")){d=m;b(this,a,d);break}else if(m.isBlockBoundary(this.forceBrBreak&&!z&&{br:1})){if("br"==t)I=1;else if(!h&&!m.getChildCount()&&"hr"!=t){d=m;w=m.equals(x);break}h&&(h.setEndAt(m,CKEDITOR.POSITION_BEFORE_START),"br"!=t&&(this._.nextNode=m));K=1}else{if(m.getFirst()){h||(h=this.range.clone(),h.setStartAt(m,CKEDITOR.POSITION_BEFORE_START));m=m.getFirst();continue}I=1}}I&&!h&&(h=this.range.clone(),h.setStartAt(m,CKEDITOR.POSITION_BEFORE_START));
+w=(!K||I)&&m.equals(x);if(h&&!K)for(;!m.getNext(k)&&!w;){t=m.getParent();if(t.isBlockBoundary(this.forceBrBreak&&!z&&{br:1})){K=1;I=0;w||t.equals(x);h.setEndAt(t,CKEDITOR.POSITION_BEFORE_END);break}m=t;I=1;w=m.equals(x);l=1}I&&h.setEndAt(m,CKEDITOR.POSITION_AFTER_END);m=this._getNextSourceNode(m,l,x);if((w=!m)||K&&h)break}if(!d){if(!h)return this._.docEndMarker&&this._.docEndMarker.remove(),this._.nextNode=null;d=new CKEDITOR.dom.elementPath(h.startContainer,h.root);m=d.blockLimit;K={div:1,th:1,td:1};
+d=d.block;!d&&m&&!this.enforceRealBlocks&&K[m.getName()]&&h.checkStartOfBlock()&&h.checkEndOfBlock()&&!m.equals(h.root)?d=m:!d||this.enforceRealBlocks&&d.is(n)?(d=this.range.document.createElement(a),h.extractContents().appendTo(d),d.trim(),h.insertNode(d),A=F=!0):"li"!=d.getName()?h.checkStartOfBlock()&&h.checkEndOfBlock()||(d=d.clone(!1),h.extractContents().appendTo(d),d.trim(),F=h.splitBlock(),A=!F.wasStartOfBlock,F=!F.wasEndOfBlock,h.insertNode(d)):w||(this._.nextNode=d.equals(x)?null:this._getNextSourceNode(h.getBoundaryNodes().endNode,
+1,x))}A&&(A=d.getPrevious())&&A.type==CKEDITOR.NODE_ELEMENT&&("br"==A.getName()?A.remove():A.getLast()&&"br"==A.getLast().$.nodeName.toLowerCase()&&A.getLast().remove());F&&(A=d.getLast())&&A.type==CKEDITOR.NODE_ELEMENT&&"br"==A.getName()&&(!CKEDITOR.env.needsBrFiller||A.getPrevious(g)||A.getNext(g))&&A.remove();this._.nextNode||(this._.nextNode=w||d.equals(x)||!x?null:this._getNextSourceNode(d,1,x));return d},_getNextSourceNode:function(a,b,c){function d(a){return!(a.equals(c)||a.equals(e))}var e=
+this.range.root;for(a=a.getNextSourceNode(b,null,d);!g(a);)a=a.getNextSourceNode(b,null,d);return a}};CKEDITOR.dom.range.prototype.createIterator=function(){return new a(this)}})();
+CKEDITOR.command=function(a,d){this.uiItems=[];this.exec=function(b){if(this.state==CKEDITOR.TRISTATE_DISABLED||!this.checkAllowed())return!1;this.editorFocus&&a.focus();return!1===this.fire("exec")?!0:!1!==d.exec.call(this,a,b)};this.refresh=function(a,b){if(!this.readOnly&&a.readOnly)return!0;if(this.context&&!b.isContextFor(this.context)||!this.checkAllowed(!0))return this.disable(),!0;this.startDisabled||this.enable();this.modes&&!this.modes[a.mode]&&this.disable();return!1===this.fire("refresh",
+{editor:a,path:b})?!0:d.refresh&&!1!==d.refresh.apply(this,arguments)};var b;this.checkAllowed=function(c){return c||"boolean"!=typeof b?b=a.activeFilter.checkFeature(this):b};CKEDITOR.tools.extend(this,d,{modes:{wysiwyg:1},editorFocus:1,contextSensitive:!!d.context,state:CKEDITOR.TRISTATE_DISABLED});CKEDITOR.event.call(this)};
+CKEDITOR.command.prototype={enable:function(){this.state==CKEDITOR.TRISTATE_DISABLED&&this.checkAllowed()&&this.setState(this.preserveState&&"undefined"!=typeof this.previousState?this.previousState:CKEDITOR.TRISTATE_OFF)},disable:function(){this.setState(CKEDITOR.TRISTATE_DISABLED)},setState:function(a){if(this.state==a||a!=CKEDITOR.TRISTATE_DISABLED&&!this.checkAllowed())return!1;this.previousState=this.state;this.state=a;this.fire("state");return!0},toggleState:function(){this.state==CKEDITOR.TRISTATE_OFF?
 this.setState(CKEDITOR.TRISTATE_ON):this.state==CKEDITOR.TRISTATE_ON&&this.setState(CKEDITOR.TRISTATE_OFF)}};CKEDITOR.event.implementOn(CKEDITOR.command.prototype);CKEDITOR.ENTER_P=1;CKEDITOR.ENTER_BR=2;CKEDITOR.ENTER_DIV=3;
-CKEDITOR.config={customConfig:"config.js",autoUpdateElement:!0,language:"",defaultLanguage:"en",contentsLangDirection:"",enterMode:CKEDITOR.ENTER_P,forceEnterMode:!1,shiftEnterMode:CKEDITOR.ENTER_BR,docType:"<!DOCTYPE html>",bodyId:"",bodyClass:"",fullPage:!1,height:200,extraPlugins:"",removePlugins:"",protectedSource:[],tabIndex:0,width:"",baseFloatZIndex:1E4,blockedKeystrokes:[CKEDITOR.CTRL+66,CKEDITOR.CTRL+73,CKEDITOR.CTRL+85]};
-(function(){function a(a,b,c,f,k){var m=b.name;if((f||typeof a.elements!="function"||a.elements(m))&&(!a.match||a.match(b))){if(f=!k){a:if(a.nothingRequired)f=true;else{if(k=a.requiredClasses){m=b.classes;for(f=0;f<k.length;++f)if(CKEDITOR.tools.indexOf(m,k[f])==-1){f=false;break a}}f=d(b.styles,a.requiredStyles)&&d(b.attributes,a.requiredAttributes)}f=!f}if(!f){if(!a.propertiesOnly)c.valid=true;if(!c.allAttributes)c.allAttributes=e(a.attributes,b.attributes,c.validAttributes);if(!c.allStyles)c.allStyles=
-e(a.styles,b.styles,c.validStyles);if(!c.allClasses){a=a.classes;b=b.classes;f=c.validClasses;if(a)if(a===true)b=true;else{for(var k=0,m=b.length,l;k<m;++k){l=b[k];f[l]||(f[l]=a(l))}b=false}else b=false;c.allClasses=b}}}}function e(a,b,c){if(!a)return false;if(a===true)return true;for(var d in b)c[d]||(c[d]=a(d,b[d]));return false}function b(a,b){if(!a)return false;if(a===true)return a;if(typeof a=="string"){a=r(a);return a=="*"?true:CKEDITOR.tools.convertArrayToObject(a.split(b))}if(CKEDITOR.tools.isArray(a))return a.length?
-CKEDITOR.tools.convertArrayToObject(a):false;var c={},d=0,f;for(f in a){c[f]=a[f];d++}return d?c:false}function c(b){if(b._.filterFunction)return b._.filterFunction;var c=/^cke:(object|embed|param)$/,d=/^(object|embed|param)$/;return b._.filterFunction=function(f,e,k,m,l,h,t){var g=f.name,r,y=false;if(l)f.name=g=g.replace(c,"$1");if(k=k&&k[g]){i(f);for(g=0;g<k.length;++g)p(b,f,k[g]);j(f)}if(e){var g=f.name,k=e.elements[g],x=e.generic,e={valid:false,validAttributes:{},validClasses:{},validStyles:{},
-allAttributes:false,allClasses:false,allStyles:false};if(!k&&!x){m.push(f);return true}i(f);if(k){g=0;for(r=k.length;g<r;++g)a(k[g],f,e,true,h)}if(x){g=0;for(r=x.length;g<r;++g)a(x[g],f,e,false,h)}if(!e.valid){m.push(f);return true}h=e.validAttributes;g=e.validStyles;k=e.validClasses;r=f.attributes;var x=f.styles,n=r["class"],L=r.style,A,z,E=[],I=[],s=/^data-cke-/,q=false;delete r.style;delete r["class"];if(!e.allAttributes)for(A in r)if(!h[A])if(s.test(A)){if(A!=(z=A.replace(/^data-cke-saved-/,""))&&
-!h[z]){delete r[A];q=true}}else{delete r[A];q=true}if(e.allStyles){if(L)r.style=L}else{for(A in x)g[A]?E.push(A+":"+x[A]):q=true;if(E.length)r.style=E.sort().join("; ")}if(e.allClasses)n&&(r["class"]=n);else{for(A in k)k[A]&&I.push(A);I.length&&(r["class"]=I.sort().join(" "));n&&I.length<n.split(/\s+/).length&&(q=true)}q&&(y=true);if(!t&&!o(f)){m.push(f);return true}}if(l)f.name=f.name.replace(d,"cke:$1");return y}}function d(a,b){if(!b)return true;for(var c=0;c<b.length;++c)if(!(b[c]in a))return false;
-return true}function h(a){if(!a)return{};for(var a=a.split(/\s*,\s*/).sort(),b={};a.length;)b[a.shift()]=L;return b}function g(a){for(var b,c,d,f,e={},k=1,a=r(a);b=a.match(z);){if(c=b[2]){d=n(c,"styles");f=n(c,"attrs");c=n(c,"classes")}else d=f=c=null;e["$"+k++]={elements:b[1],classes:c,styles:d,attributes:f};a=a.slice(b[0].length)}return e}function n(a,b){var c=a.match(M[b]);return c?r(c[1]):null}function i(a){if(!a.styles)a.styles=CKEDITOR.tools.parseCssText(a.attributes.style||"",1);if(!a.classes)a.classes=
-a.attributes["class"]?a.attributes["class"].split(/\s+/):[]}function j(a){var b=a.attributes,c;delete b.style;delete b["class"];if(c=CKEDITOR.tools.writeCssText(a.styles,true))b.style=c;a.classes.length&&(b["class"]=a.classes.sort().join(" "))}function o(a){switch(a.name){case "a":if(!a.children.length&&!a.attributes.name)return false;break;case "img":if(!a.attributes.src)return false}return true}function q(a){return!a?false:a===true?true:function(b){return b in a}}function s(){return new CKEDITOR.htmlParser.element("br")}
-function u(a){return a.type==CKEDITOR.NODE_ELEMENT&&(a.name=="br"||t.$block[a.name])}function f(a,b,c){var d=a.name;if(t.$empty[d]||!a.children.length)if(d=="hr"&&b=="br")a.replaceWith(s());else{a.parent&&c.push({check:"it",el:a.parent});a.remove()}else if(t.$block[d]||d=="tr")if(b=="br"){if(a.previous&&!u(a.previous)){b=s();b.insertBefore(a)}if(a.next&&!u(a.next)){b=s();b.insertAfter(a)}a.replaceWithChildren()}else{var d=a.children,f;b:{f=t[b];for(var e=0,k=d.length,m;e<k;++e){m=d[e];if(m.type==
-CKEDITOR.NODE_ELEMENT&&!f[m.name]){f=false;break b}}f=true}if(f){a.name=b;a.attributes={};c.push({check:"parent-down",el:a})}else{f=a.parent;for(var e=f.type==CKEDITOR.NODE_DOCUMENT_FRAGMENT||f.name=="body",l,k=d.length;k>0;){m=d[--k];if(e&&(m.type==CKEDITOR.NODE_TEXT||m.type==CKEDITOR.NODE_ELEMENT&&t.$inline[m.name])){if(!l){l=new CKEDITOR.htmlParser.element(b);l.insertAfter(a);c.push({check:"parent-down",el:l})}l.add(m,0)}else{l=null;m.insertAfter(a);f.type!=CKEDITOR.NODE_DOCUMENT_FRAGMENT&&(m.type==
-CKEDITOR.NODE_ELEMENT&&!t[f.name][m.name])&&c.push({check:"el-up",el:m})}}a.remove()}}else if(d=="style")a.remove();else{a.parent&&c.push({check:"it",el:a.parent});a.replaceWithChildren()}}function p(a,b,c){var d,f;for(d=0;d<c.length;++d){f=c[d];if((!f.check||a.check(f.check,false))&&(!f.left||f.left(b))){f.right(b,Q);break}}}function y(a,b){var c=b.getDefinition(),d=c.attributes,f=c.styles,e,k,m,l;if(a.name!=c.element)return false;for(e in d)if(e=="class"){c=d[e].split(/\s+/);for(m=a.classes.join("|");l=
-c.pop();)if(m.indexOf(l)==-1)return false}else if(a.attributes[e]!=d[e])return false;for(k in f)if(a.styles[k]!=f[k])return false;return true}function m(a,b){var c,d;if(typeof a=="string")c=a;else if(a instanceof CKEDITOR.style)d=a;else{c=a[0];d=a[1]}return[{element:c,left:d,right:function(a,c){c.transform(a,b)}}]}function k(a){return function(b){return y(b,a)}}function l(a){return function(b,c){c[a](b)}}var t=CKEDITOR.dtd,x=CKEDITOR.tools.copy,r=CKEDITOR.tools.trim,L="cke-test",A=["","p","br","div"];
-CKEDITOR.filter=function(a){this.allowedContent=[];this.disabled=false;this.editor=null;this.id=CKEDITOR.tools.getNextNumber();this._={rules:{},transformations:{},cachedTests:{}};CKEDITOR.filter.instances[this.id]=this;if(a instanceof CKEDITOR.editor){a=this.editor=a;this.customConfig=true;var b=a.config.allowedContent;if(b===true)this.disabled=true;else{if(!b)this.customConfig=false;this.allow(b,"config",1);this.allow(a.config.extraAllowedContent,"extra",1);this.allow(A[a.enterMode]+" "+A[a.shiftEnterMode],
-"default",1)}}else{this.customConfig=false;this.allow(a,"default",1)}};CKEDITOR.filter.instances={};CKEDITOR.filter.prototype={allow:function(a,c,d){if(this.disabled||this.customConfig&&!d||!a)return false;this._.cachedChecks={};var f,e;if(typeof a=="string")a=g(a);else if(a instanceof CKEDITOR.style){e=a.getDefinition();d={};a=e.attributes;d[e.element]=e={styles:e.styles,requiredStyles:e.styles&&CKEDITOR.tools.objectKeys(e.styles)};if(a){a=x(a);e.classes=a["class"]?a["class"].split(/\s+/):null;e.requiredClasses=
-e.classes;delete a["class"];e.attributes=a;e.requiredAttributes=a&&CKEDITOR.tools.objectKeys(a)}a=d}else if(CKEDITOR.tools.isArray(a)){for(f=0;f<a.length;++f)e=this.allow(a[f],c,d);return e}var k,d=[];for(k in a){e=a[k];e=typeof e=="boolean"?{}:typeof e=="function"?{match:e}:x(e);if(k.charAt(0)!="$")e.elements=k;if(c)e.featureName=c.toLowerCase();var m=e;m.elements=b(m.elements,/\s+/)||null;m.propertiesOnly=m.propertiesOnly||m.elements===true;var l=/\s*,\s*/,h=void 0;for(h in I){m[h]=b(m[h],l)||null;
-var p=m,t=E[h],r=b(m[E[h]],l),i=m[h],j=[],y=true,n=void 0;r?y=false:r={};for(n in i)if(n.charAt(0)=="!"){n=n.slice(1);j.push(n);r[n]=true;y=false}for(;n=j.pop();){i[n]=i["!"+n];delete i["!"+n]}p[t]=(y?false:r)||null}m.match=m.match||null;this.allowedContent.push(e);d.push(e)}c=this._.rules;k=c.elements||{};a=c.generic||[];e=0;for(m=d.length;e<m;++e){l=x(d[e]);h=l.classes===true||l.styles===true||l.attributes===true;p=l;t=void 0;for(t in I)p[t]=q(p[t]);r=true;for(t in E){t=E[t];p[t]=CKEDITOR.tools.objectKeys(p[t]);
-p[t]&&(r=false)}p.nothingRequired=r;if(l.elements===true||l.elements===null){l.elements=q(l.elements);a[h?"unshift":"push"](l)}else{p=l.elements;delete l.elements;for(f in p)if(k[f])k[f][h?"unshift":"push"](l);else k[f]=[l]}}c.elements=k;c.generic=a.length?a:null;return true},applyTo:function(a,b,d,e){if(this.disabled)return false;var k=[],m=!d&&this._.rules,l=this._.transformations,h=c(this),p=this.editor&&this.editor.config.protectedSource,g=false;a.forEach(function(a){if(a.type==CKEDITOR.NODE_ELEMENT){if(a.attributes["data-cke-filter"]==
-"off")return false;if(!b||!(a.name=="span"&&~CKEDITOR.tools.objectKeys(a.attributes).join("|").indexOf("data-cke-")))h(a,m,l,k,b)&&(g=true)}else if(a.type==CKEDITOR.NODE_COMMENT&&a.value.match(/^\{cke_protected\}(?!\{C\})/)){var c;a:{var d=decodeURIComponent(a.value.replace(/^\{cke_protected\}/,""));c=[];var f,e,C;if(p)for(e=0;e<p.length;++e)if((C=d.match(p[e]))&&C[0].length==d.length){c=true;break a}d=CKEDITOR.htmlParser.fragment.fromHtml(d);d.children.length==1&&(f=d.children[0]).type==CKEDITOR.NODE_ELEMENT&&
-h(f,m,l,c,b);c=!c.length}c||k.push(a)}},null,true);k.length&&(g=true);for(var r,a=[],e=A[e||(this.editor?this.editor.enterMode:CKEDITOR.ENTER_P)];d=k.pop();)d.type==CKEDITOR.NODE_ELEMENT?f(d,e,a):d.remove();for(;r=a.pop();){d=r.el;if(d.parent)switch(r.check){case "it":t.$removeEmpty[d.name]&&!d.children.length?f(d,e,a):o(d)||f(d,e,a);break;case "el-up":d.parent.type!=CKEDITOR.NODE_DOCUMENT_FRAGMENT&&!t[d.parent.name][d.name]&&f(d,e,a);break;case "parent-down":d.parent.type!=CKEDITOR.NODE_DOCUMENT_FRAGMENT&&
-!t[d.parent.name][d.name]&&f(d.parent,e,a)}}return g},checkFeature:function(a){if(this.disabled||!a)return true;a.toFeature&&(a=a.toFeature(this.editor));return!a.requiredContent||this.check(a.requiredContent)},disable:function(){this.disabled=true},addContentForms:function(a){if(!this.disabled&&a){var b,c,d=[],f;for(b=0;b<a.length&&!f;++b){c=a[b];if((typeof c=="string"||c instanceof CKEDITOR.style)&&this.check(c))f=c}if(f){for(b=0;b<a.length;++b)d.push(m(a[b],f));this.addTransformations(d)}}},addFeature:function(a){if(this.disabled||
-!a)return true;a.toFeature&&(a=a.toFeature(this.editor));this.allow(a.allowedContent,a.name);this.addTransformations(a.contentTransformations);this.addContentForms(a.contentForms);return this.customConfig&&a.requiredContent?this.check(a.requiredContent):true},addTransformations:function(a){var b,c;if(!this.disabled&&a){var d=this._.transformations,f;for(f=0;f<a.length;++f){b=a[f];var e=void 0,m=void 0,h=void 0,p=void 0,t=void 0,g=void 0;c=[];for(m=0;m<b.length;++m){h=b[m];if(typeof h=="string"){h=
-h.split(/\s*:\s*/);p=h[0];t=null;g=h[1]}else{p=h.check;t=h.left;g=h.right}if(!e){e=h;e=e.element?e.element:p?p.match(/^([a-z0-9]+)/i)[0]:e.left.getDefinition().element}t instanceof CKEDITOR.style&&(t=k(t));c.push({check:p==e?null:p,left:t,right:typeof g=="string"?l(g):g})}b=e;d[b]||(d[b]=[]);d[b].push(c)}}},check:function(a,b,d){if(this.disabled)return true;if(CKEDITOR.tools.isArray(a)){for(var f=a.length;f--;)if(this.check(a[f],b,d))return true;return false}var e,k;if(typeof a=="string"){k=a+"<"+
-(b===false?"0":"1")+(d?"1":"0")+">";if(k in this._.cachedChecks)return this._.cachedChecks[k];f=g(a).$1;e=f.styles;var m=f.classes;f.name=f.elements;f.classes=m=m?m.split(/\s*,\s*/):[];f.styles=h(e);f.attributes=h(f.attributes);f.children=[];m.length&&(f.attributes["class"]=m.join(" "));if(e)f.attributes.style=CKEDITOR.tools.writeCssText(f.styles);e=f}else{f=a.getDefinition();e=f.styles;m=f.attributes||{};if(e){e=x(e);m.style=CKEDITOR.tools.writeCssText(e,true)}else e={};e={name:f.element,attributes:m,
-classes:m["class"]?m["class"].split(/\s+/):[],styles:e,children:[]}}var m=CKEDITOR.tools.clone(e),l=[],t;if(b!==false&&(t=this._.transformations[e.name])){for(f=0;f<t.length;++f)p(this,e,t[f]);j(e)}c(this)(m,this._.rules,b===false?false:this._.transformations,l,false,!d,!d);b=l.length>0?false:CKEDITOR.tools.objectCompare(e.attributes,m.attributes,true)?true:false;typeof a=="string"&&(this._.cachedChecks[k]=b);return b},getAllowedEnterMode:function(){var a=["p","div","br"],b={p:CKEDITOR.ENTER_P,div:CKEDITOR.ENTER_DIV,
-br:CKEDITOR.ENTER_BR};return function(c,d){var f=a.slice(),e;if(this.check(A[c]))return c;for(d||(f=f.reverse());e=f.pop();)if(this.check(e))return b[e];return CKEDITOR.ENTER_BR}}()};var I={styles:1,attributes:1,classes:1},E={styles:"requiredStyles",attributes:"requiredAttributes",classes:"requiredClasses"},z=/^([a-z0-9*\s]+)((?:\s*\{[!\w\-,\s\*]+\}\s*|\s*\[[!\w\-,\s\*]+\]\s*|\s*\([!\w\-,\s\*]+\)\s*){0,3})(?:;\s*|$)/i,M={styles:/{([^}]+)}/,attrs:/\[([^\]]+)\]/,classes:/\(([^\)]+)\)/},Q=CKEDITOR.filter.transformationsTools=
-{sizeToStyle:function(a){this.lengthToStyle(a,"width");this.lengthToStyle(a,"height")},sizeToAttribute:function(a){this.lengthToAttribute(a,"width");this.lengthToAttribute(a,"height")},lengthToStyle:function(a,b,c){c=c||b;if(!(c in a.styles)){var d=a.attributes[b];if(d){/^\d+$/.test(d)&&(d=d+"px");a.styles[c]=d}}delete a.attributes[b]},lengthToAttribute:function(a,b,c){c=c||b;if(!(c in a.attributes)){var d=a.styles[b],f=d&&d.match(/^(\d+)(?:\.\d*)?px$/);f?a.attributes[c]=f[1]:d==L&&(a.attributes[c]=
-L)}delete a.styles[b]},alignmentToStyle:function(a){if(!("float"in a.styles)){var b=a.attributes.align;if(b=="left"||b=="right")a.styles["float"]=b}delete a.attributes.align},alignmentToAttribute:function(a){if(!("align"in a.attributes)){var b=a.styles["float"];if(b=="left"||b=="right")a.attributes.align=b}delete a.styles["float"]},matchesStyle:y,transform:function(a,b){if(typeof b=="string")a.name=b;else{var c=b.getDefinition(),d=c.styles,f=c.attributes,e,k,m,l;a.name=c.element;for(e in f)if(e==
-"class"){c=a.classes.join("|");for(m=f[e].split(/\s+/);l=m.pop();)c.indexOf(l)==-1&&a.classes.push(l)}else a.attributes[e]=f[e];for(k in d)a.styles[k]=d[k]}}}})();
-(function(){CKEDITOR.focusManager=function(a){if(a.focusManager)return a.focusManager;this.hasFocus=false;this.currentActive=null;this._={editor:a};return this};CKEDITOR.focusManager._={blurDelay:200};CKEDITOR.focusManager.prototype={focus:function(a){this._.timer&&clearTimeout(this._.timer);if(a)this.currentActive=a;if(!this.hasFocus&&!this._.locked){(a=CKEDITOR.currentInstance)&&a.focusManager.blur(1);this.hasFocus=true;(a=this._.editor.container)&&a.addClass("cke_focus");this._.editor.fire("focus")}},
-lock:function(){this._.locked=1},unlock:function(){delete this._.locked},blur:function(a){function e(){if(this.hasFocus){this.hasFocus=false;var a=this._.editor.container;a&&a.removeClass("cke_focus");this._.editor.fire("blur")}}if(!this._.locked){this._.timer&&clearTimeout(this._.timer);var b=CKEDITOR.focusManager._.blurDelay;a||!b?e.call(this):this._.timer=CKEDITOR.tools.setTimeout(function(){delete this._.timer;e.call(this)},b,this)}},add:function(a,e){var b=a.getCustomData("focusmanager");if(!b||
-b!=this){b&&b.remove(a);var b="focus",c="blur";if(e)if(CKEDITOR.env.ie){b="focusin";c="focusout"}else CKEDITOR.event.useCapture=1;var d={blur:function(){a.equals(this.currentActive)&&this.blur()},focus:function(){this.focus(a)}};a.on(b,d.focus,this);a.on(c,d.blur,this);if(e)CKEDITOR.event.useCapture=0;a.setCustomData("focusmanager",this);a.setCustomData("focusmanager_handlers",d)}},remove:function(a){a.removeCustomData("focusmanager");var e=a.removeCustomData("focusmanager_handlers");a.removeListener("blur",
-e.blur);a.removeListener("focus",e.focus)}}})();CKEDITOR.keystrokeHandler=function(a){if(a.keystrokeHandler)return a.keystrokeHandler;this.keystrokes={};this.blockedKeystrokes={};this._={editor:a};return this};
-(function(){var a,e=function(b){var b=b.data,d=b.getKeystroke(),e=this.keystrokes[d],g=this._.editor;a=g.fire("key",{keyCode:d})===false;if(!a){e&&(a=g.execCommand(e,{from:"keystrokeHandler"})!==false);a||(a=!!this.blockedKeystrokes[d])}a&&b.preventDefault(true);return!a},b=function(b){if(a){a=false;b.data.preventDefault(true)}};CKEDITOR.keystrokeHandler.prototype={attach:function(a){a.on("keydown",e,this);if(CKEDITOR.env.opera||CKEDITOR.env.gecko&&CKEDITOR.env.mac)a.on("keypress",b,this)}}})();
-(function(){CKEDITOR.lang={languages:{af:1,ar:1,bg:1,bn:1,bs:1,ca:1,cs:1,cy:1,da:1,de:1,el:1,"en-au":1,"en-ca":1,"en-gb":1,en:1,eo:1,es:1,et:1,eu:1,fa:1,fi:1,fo:1,"fr-ca":1,fr:1,gl:1,gu:1,he:1,hi:1,hr:1,hu:1,id:1,is:1,it:1,ja:1,ka:1,km:1,ko:1,ku:1,lt:1,lv:1,mk:1,mn:1,ms:1,nb:1,nl:1,no:1,pl:1,"pt-br":1,pt:1,ro:1,ru:1,si:1,sk:1,sl:1,sq:1,"sr-latn":1,sr:1,sv:1,th:1,tr:1,ug:1,uk:1,vi:1,"zh-cn":1,zh:1},rtl:{ar:1,fa:1,he:1,ku:1,ug:1},load:function(a,e,b){if(!a||!CKEDITOR.lang.languages[a])a=this.detect(e,
-a);this[a]?b(a,this[a]):CKEDITOR.scriptLoader.load(CKEDITOR.getUrl("lang/"+a+".js"),function(){this[a].dir=this.rtl[a]?"rtl":"ltr";b(a,this[a])},this)},detect:function(a,e){var b=this.languages,e=e||navigator.userLanguage||navigator.language||a,c=e.toLowerCase().match(/([a-z]+)(?:-([a-z]+))?/),d=c[1],c=c[2];b[d+"-"+c]?d=d+"-"+c:b[d]||(d=null);CKEDITOR.lang.detect=d?function(){return d}:function(a){return a};return d||a}}})();
-CKEDITOR.scriptLoader=function(){var a={},e={};return{load:function(b,c,d,h){var g=typeof b=="string";g&&(b=[b]);d||(d=CKEDITOR);var n=b.length,i=[],j=[],o=function(a){c&&(g?c.call(d,a):c.call(d,i,j))};if(n===0)o(true);else{var q=function(a,b){(b?i:j).push(a);if(--n<=0){h&&CKEDITOR.document.getDocumentElement().removeStyle("cursor");o(b)}},s=function(b,c){a[b]=1;var d=e[b];delete e[b];for(var f=0;f<d.length;f++)d[f](b,c)},u=function(b){if(a[b])q(b,true);else{var d=e[b]||(e[b]=[]);d.push(q);if(!(d.length>
-1)){var f=new CKEDITOR.dom.element("script");f.setAttributes({type:"text/javascript",src:b});if(c)if(CKEDITOR.env.ie&&CKEDITOR.env.version<11)f.$.onreadystatechange=function(){if(f.$.readyState=="loaded"||f.$.readyState=="complete"){f.$.onreadystatechange=null;s(b,true)}};else{f.$.onload=function(){setTimeout(function(){s(b,true)},0)};f.$.onerror=function(){s(b,false)}}f.appendTo(CKEDITOR.document.getHead())}}};h&&CKEDITOR.document.getDocumentElement().setStyle("cursor","wait");for(var f=0;f<n;f++)u(b[f])}},
-queue:function(){function a(){var b;(b=c[0])&&this.load(b.scriptUrl,b.callback,CKEDITOR,0)}var c=[];return function(d,e){var g=this;c.push({scriptUrl:d,callback:function(){e&&e.apply(this,arguments);c.shift();a.call(g)}});c.length==1&&a.call(this)}}()}}();CKEDITOR.resourceManager=function(a,e){this.basePath=a;this.fileName=e;this.registered={};this.loaded={};this.externals={};this._={waitingList:{}}};
-CKEDITOR.resourceManager.prototype={add:function(a,e){if(this.registered[a])throw'[CKEDITOR.resourceManager.add] The resource name "'+a+'" is already registered.';var b=this.registered[a]=e||{};b.name=a;b.path=this.getPath(a);CKEDITOR.fire(a+CKEDITOR.tools.capitalize(this.fileName)+"Ready",b);return this.get(a)},get:function(a){return this.registered[a]||null},getPath:function(a){var e=this.externals[a];return CKEDITOR.getUrl(e&&e.dir||this.basePath+a+"/")},getFilePath:function(a){var e=this.externals[a];
-return CKEDITOR.getUrl(this.getPath(a)+(e?e.file:this.fileName+".js"))},addExternal:function(a,e,b){for(var a=a.split(","),c=0;c<a.length;c++){var d=a[c];b||(e=e.replace(/[^\/]+$/,function(a){b=a;return""}));this.externals[d]={dir:e,file:b||this.fileName+".js"}}},load:function(a,e,b){CKEDITOR.tools.isArray(a)||(a=a?[a]:[]);for(var c=this.loaded,d=this.registered,h=[],g={},n={},i=0;i<a.length;i++){var j=a[i];if(j)if(!c[j]&&!d[j]){var o=this.getFilePath(j);h.push(o);o in g||(g[o]=[]);g[o].push(j)}else n[j]=
-this.get(j)}CKEDITOR.scriptLoader.load(h,function(a,d){if(d.length)throw'[CKEDITOR.resourceManager.load] Resource name "'+g[d[0]].join(",")+'" was not found at "'+d[0]+'".';for(var h=0;h<a.length;h++)for(var f=g[a[h]],p=0;p<f.length;p++){var i=f[p];n[i]=this.get(i);c[i]=1}e.call(b,n)},this)}};CKEDITOR.plugins=new CKEDITOR.resourceManager("plugins/","plugin");
-CKEDITOR.plugins.load=CKEDITOR.tools.override(CKEDITOR.plugins.load,function(a){var e={};return function(b,c,d){var h={},g=function(b){a.call(this,b,function(a){CKEDITOR.tools.extend(h,a);var b=[],n;for(n in a){var q=a[n],s=q&&q.requires;if(!e[n]){if(q.icons)for(var u=q.icons.split(","),f=u.length;f--;)CKEDITOR.skin.addIcon(u[f],q.path+"icons/"+(CKEDITOR.env.hidpi&&q.hidpi?"hidpi/":"")+u[f]+".png");e[n]=1}if(s){s.split&&(s=s.split(","));for(q=0;q<s.length;q++)h[s[q]]||b.push(s[q])}}if(b.length)g.call(this,
-b);else{for(n in h){q=h[n];if(q.onLoad&&!q.onLoad._called){q.onLoad()===false&&delete h[n];q.onLoad._called=1}}c&&c.call(d||window,h)}},this)};g.call(this,b)}});CKEDITOR.plugins.setLang=function(a,e,b){var c=this.get(a),a=c.langEntries||(c.langEntries={}),c=c.lang||(c.lang=[]);c.split&&(c=c.split(","));CKEDITOR.tools.indexOf(c,e)==-1&&c.push(e);a[e]=b};CKEDITOR.ui=function(a){if(a.ui)return a.ui;this.items={};this.instances={};this.editor=a;this._={handlers:{}};return this};
-CKEDITOR.ui.prototype={add:function(a,e,b){b.name=a.toLowerCase();var c=this.items[a]={type:e,command:b.command||null,args:Array.prototype.slice.call(arguments,2)};CKEDITOR.tools.extend(c,b)},get:function(a){return this.instances[a]},create:function(a){var e=this.items[a],b=e&&this._.handlers[e.type],c=e&&e.command&&this.editor.getCommand(e.command),b=b&&b.create.apply(this,e.args);this.instances[a]=b;c&&c.uiItems.push(b);if(b&&!b.type)b.type=e.type;return b},addHandler:function(a,e){this._.handlers[a]=
-e},space:function(a){return CKEDITOR.document.getById(this.spaceId(a))},spaceId:function(a){return this.editor.id+"_"+a}};CKEDITOR.event.implementOn(CKEDITOR.ui);
-(function(){function a(a,c,h){CKEDITOR.event.call(this);a=a&&CKEDITOR.tools.clone(a);if(c!==void 0){if(c instanceof CKEDITOR.dom.element){if(!h)throw Error("One of the element modes must be specified.");}else throw Error("Expect element of type CKEDITOR.dom.element.");if(CKEDITOR.env.ie&&CKEDITOR.env.quirks&&h==CKEDITOR.ELEMENT_MODE_INLINE)throw Error("Inline element mode is not supported on IE quirks.");if(!(h==CKEDITOR.ELEMENT_MODE_INLINE?c.is(CKEDITOR.dtd.$editable)||c.is("textarea"):h==CKEDITOR.ELEMENT_MODE_REPLACE?
-!c.is(CKEDITOR.dtd.$nonBodyContent):1))throw Error('The specified element mode is not supported on element: "'+c.getName()+'".');this.element=c;this.elementMode=h;this.name=this.elementMode!=CKEDITOR.ELEMENT_MODE_APPENDTO&&(c.getId()||c.getNameAtt())}else this.elementMode=CKEDITOR.ELEMENT_MODE_NONE;this._={};this.commands={};this.templates={};this.name=this.name||e();this.id=CKEDITOR.tools.getNextId();this.status="unloaded";this.config=CKEDITOR.tools.prototypedCopy(CKEDITOR.config);this.ui=new CKEDITOR.ui(this);
-this.focusManager=new CKEDITOR.focusManager(this);this.keystrokeHandler=new CKEDITOR.keystrokeHandler(this);this.on("readOnly",b);this.on("selectionChange",function(a){d(this,a.data.path)});this.on("activeFilterChange",function(){d(this,this.elementPath(),true)});this.on("mode",b);this.on("instanceReady",function(){this.config.startupFocus&&this.focus()});CKEDITOR.fire("instanceCreated",null,this);CKEDITOR.add(this);CKEDITOR.tools.setTimeout(function(){g(this,a)},0,this)}function e(){do var a="editor"+
-++s;while(CKEDITOR.instances[a]);return a}function b(){var a=this.commands,b;for(b in a)c(this,a[b])}function c(a,b){b[b.startDisabled?"disable":a.readOnly&&!b.readOnly?"disable":b.modes[a.mode]?"enable":"disable"]()}function d(a,b,c){if(b){var d,e,l=a.commands;for(e in l){d=l[e];(c||d.contextSensitive)&&d.refresh(a,b)}}}function h(a){var b=a.config.customConfig;if(!b)return false;var b=CKEDITOR.getUrl(b),c=u[b]||(u[b]={});if(c.fn){c.fn.call(a,a.config);(CKEDITOR.getUrl(a.config.customConfig)==b||
-!h(a))&&a.fireOnce("customConfigLoaded")}else CKEDITOR.scriptLoader.queue(b,function(){c.fn=CKEDITOR.editorConfig?CKEDITOR.editorConfig:function(){};h(a)});return true}function g(a,b){a.on("customConfigLoaded",function(){if(b){if(b.on)for(var c in b.on)a.on(c,b.on[c]);CKEDITOR.tools.extend(a.config,b,true);delete a.config.on}c=a.config;a.readOnly=!(!c.readOnly&&!(a.elementMode==CKEDITOR.ELEMENT_MODE_INLINE?a.element.is("textarea")?a.element.hasAttribute("disabled"):a.element.isReadOnly():a.elementMode==
-CKEDITOR.ELEMENT_MODE_REPLACE&&a.element.hasAttribute("disabled")));a.blockless=a.elementMode==CKEDITOR.ELEMENT_MODE_INLINE?!(a.element.is("textarea")||CKEDITOR.dtd[a.element.getName()].p):false;a.tabIndex=c.tabIndex||a.element&&a.element.getAttribute("tabindex")||0;a.activeEnterMode=a.enterMode=a.blockless?CKEDITOR.ENTER_BR:c.enterMode;a.activeShiftEnterMode=a.shiftEnterMode=a.blockless?CKEDITOR.ENTER_BR:c.shiftEnterMode;if(c.skin)CKEDITOR.skinName=c.skin;a.fireOnce("configLoaded");a.dataProcessor=
-new CKEDITOR.htmlDataProcessor(a);a.filter=a.activeFilter=new CKEDITOR.filter(a);n(a)});if(b&&b.customConfig!=void 0)a.config.customConfig=b.customConfig;h(a)||a.fireOnce("customConfigLoaded")}function n(a){CKEDITOR.skin.loadPart("editor",function(){i(a)})}function i(a){CKEDITOR.lang.load(a.config.language,a.config.defaultLanguage,function(b,c){var d=a.config.title;a.langCode=b;a.lang=CKEDITOR.tools.prototypedCopy(c);a.title=typeof d=="string"||d===false?d:[a.lang.editor,a.name].join(", ");if(CKEDITOR.env.gecko&&
-CKEDITOR.env.version<10900&&a.lang.dir=="rtl")a.lang.dir="ltr";if(!a.config.contentsLangDirection)a.config.contentsLangDirection=a.elementMode==CKEDITOR.ELEMENT_MODE_INLINE?a.element.getDirection(1):a.lang.dir;a.fire("langLoaded");j(a)})}function j(a){a.getStylesSet(function(b){a.once("loaded",function(){a.fire("stylesSet",{styles:b})},null,null,1);o(a)})}function o(a){var b=a.config,c=b.plugins,d=b.extraPlugins,e=b.removePlugins;if(d)var l=RegExp("(?:^|,)(?:"+d.replace(/\s*,\s*/g,"|")+")(?=,|$)",
-"g"),c=c.replace(l,""),c=c+(","+d);if(e)var h=RegExp("(?:^|,)(?:"+e.replace(/\s*,\s*/g,"|")+")(?=,|$)","g"),c=c.replace(h,"");CKEDITOR.env.air&&(c=c+",adobeair");CKEDITOR.plugins.load(c.split(","),function(c){var d=[],e=[],k=[];a.plugins=c;for(var l in c){var m=c[l],g=m.lang,i=null,j=m.requires,v;CKEDITOR.tools.isArray(j)&&(j=j.join(","));if(j&&(v=j.match(h)))for(;j=v.pop();)CKEDITOR.tools.setTimeout(function(a,b){throw Error('Plugin "'+a.replace(",","")+'" cannot be removed from the plugins list, because it\'s required by "'+
-b+'" plugin.');},0,null,[j,l]);if(g&&!a.lang[l]){g.split&&(g=g.split(","));if(CKEDITOR.tools.indexOf(g,a.langCode)>=0)i=a.langCode;else{i=a.langCode.replace(/-.*/,"");i=i!=a.langCode&&CKEDITOR.tools.indexOf(g,i)>=0?i:CKEDITOR.tools.indexOf(g,"en")>=0?"en":g[0]}if(!m.langEntries||!m.langEntries[i])k.push(CKEDITOR.getUrl(m.path+"lang/"+i+".js"));else{a.lang[l]=m.langEntries[i];i=null}}e.push(i);d.push(m)}CKEDITOR.scriptLoader.load(k,function(){for(var c=["beforeInit","init","afterInit"],k=0;k<c.length;k++)for(var m=
-0;m<d.length;m++){var l=d[m];k===0&&(e[m]&&l.lang&&l.langEntries)&&(a.lang[l.name]=l.langEntries[e[m]]);if(l[c[k]])l[c[k]](a)}a.fireOnce("pluginsLoaded");b.keystrokes&&a.setKeystroke(a.config.keystrokes);for(m=0;m<a.config.blockedKeystrokes.length;m++)a.keystrokeHandler.blockedKeystrokes[a.config.blockedKeystrokes[m]]=1;a.status="loaded";a.fireOnce("loaded");CKEDITOR.fire("instanceLoaded",null,a)})})}function q(){var a=this.element;if(a&&this.elementMode!=CKEDITOR.ELEMENT_MODE_APPENDTO){var b=this.getData();
-this.config.htmlEncodeOutput&&(b=CKEDITOR.tools.htmlEncode(b));a.is("textarea")?a.setValue(b):a.setHtml(b);return true}return false}a.prototype=CKEDITOR.editor.prototype;CKEDITOR.editor=a;var s=0,u={};CKEDITOR.tools.extend(CKEDITOR.editor.prototype,{addCommand:function(a,b){b.name=a.toLowerCase();var d=new CKEDITOR.command(this,b);this.mode&&c(this,d);return this.commands[a]=d},_attachToForm:function(){var a=this,b=a.element,c=new CKEDITOR.dom.element(b.$.form);if(b.is("textarea")&&c){var d=function(c){a.updateElement();
-a._.required&&(!b.getValue()&&a.fire("required")===false)&&c.data.preventDefault()};c.on("submit",d);if(c.$.submit&&c.$.submit.call&&c.$.submit.apply)c.$.submit=CKEDITOR.tools.override(c.$.submit,function(a){return function(){d();a.apply?a.apply(this):a()}});a.on("destroy",function(){c.removeListener("submit",d)})}},destroy:function(a){this.fire("beforeDestroy");!a&&q.call(this);this.editable(null);this.status="destroyed";this.fire("destroy");this.removeAllListeners();CKEDITOR.remove(this);CKEDITOR.fire("instanceDestroyed",
-null,this)},elementPath:function(a){return(a=a||this.getSelection().getStartElement())?new CKEDITOR.dom.elementPath(a,this.editable()):null},createRange:function(){var a=this.editable();return a?new CKEDITOR.dom.range(a):null},execCommand:function(a,b){var c=this.getCommand(a),d={name:a,commandData:b,command:c};if(c&&c.state!=CKEDITOR.TRISTATE_DISABLED&&this.fire("beforeCommandExec",d)!==true){d.returnValue=c.exec(d.commandData);if(!c.async&&this.fire("afterCommandExec",d)!==true)return d.returnValue}return false},
-getCommand:function(a){return this.commands[a]},getData:function(a){!a&&this.fire("beforeGetData");var b=this._.data;if(typeof b!="string")b=(b=this.element)&&this.elementMode==CKEDITOR.ELEMENT_MODE_REPLACE?b.is("textarea")?b.getValue():b.getHtml():"";b={dataValue:b};!a&&this.fire("getData",b);return b.dataValue},getSnapshot:function(){var a=this.fire("getSnapshot");if(typeof a!="string"){var b=this.element;b&&this.elementMode==CKEDITOR.ELEMENT_MODE_REPLACE&&(a=b.is("textarea")?b.getValue():b.getHtml())}return a},
-loadSnapshot:function(a){this.fire("loadSnapshot",a)},setData:function(a,b,c){if(b)this.on("dataReady",function(a){a.removeListener();b.call(a.editor)});a={dataValue:a};!c&&this.fire("setData",a);this._.data=a.dataValue;!c&&this.fire("afterSetData",a)},setReadOnly:function(a){a=a==void 0||a;if(this.readOnly!=a){this.readOnly=a;this.keystrokeHandler.blockedKeystrokes[8]=+a;this.editable().setReadOnly(a);this.fire("readOnly")}},insertHtml:function(a,b){this.fire("insertHtml",{dataValue:a,mode:b})},
-insertText:function(a){this.fire("insertText",a)},insertElement:function(a){this.fire("insertElement",a)},focus:function(){this.fire("beforeFocus")},checkDirty:function(){return this.status=="ready"&&this._.previousValue!==this.getSnapshot()},resetDirty:function(){this._.previousValue=this.getSnapshot()},updateElement:function(){return q.call(this)},setKeystroke:function(){for(var a=this.keystrokeHandler.keystrokes,b=CKEDITOR.tools.isArray(arguments[0])?arguments[0]:[[].slice.call(arguments,0)],c,
-d,e=b.length;e--;){c=b[e];d=0;if(CKEDITOR.tools.isArray(c)){d=c[1];c=c[0]}d?a[c]=d:delete a[c]}},addFeature:function(a){return this.filter.addFeature(a)},setActiveFilter:function(a){if(!a)a=this.filter;if(this.activeFilter!==a){this.activeFilter=a;this.fire("activeFilterChange");a===this.filter?this.setActiveEnterMode(null,null):this.setActiveEnterMode(a.getAllowedEnterMode(this.enterMode),a.getAllowedEnterMode(this.shiftEnterMode,true))}},setActiveEnterMode:function(a,b){a=a?this.blockless?CKEDITOR.ENTER_BR:
-a:this.enterMode;b=b?this.blockless?CKEDITOR.ENTER_BR:b:this.shiftEnterMode;if(this.activeEnterMode!=a||this.activeShiftEnterMode!=b){this.activeEnterMode=a;this.activeShiftEnterMode=b;this.fire("activeEnterModeChange")}}})})();CKEDITOR.ELEMENT_MODE_NONE=0;CKEDITOR.ELEMENT_MODE_REPLACE=1;CKEDITOR.ELEMENT_MODE_APPENDTO=2;CKEDITOR.ELEMENT_MODE_INLINE=3;
-CKEDITOR.htmlParser=function(){this._={htmlPartsRegex:RegExp("<(?:(?:\\/([^>]+)>)|(?:!--([\\S|\\s]*?)--\>)|(?:([^\\s>]+)\\s*((?:(?:\"[^\"]*\")|(?:'[^']*')|[^\"'>])*)\\/?>))","g")}};
-(function(){var a=/([\w\-:.]+)(?:(?:\s*=\s*(?:(?:"([^"]*)")|(?:'([^']*)')|([^\s>]+)))|(?=\s|$))/g,e={checked:1,compact:1,declare:1,defer:1,disabled:1,ismap:1,multiple:1,nohref:1,noresize:1,noshade:1,nowrap:1,readonly:1,selected:1};CKEDITOR.htmlParser.prototype={onTagOpen:function(){},onTagClose:function(){},onText:function(){},onCDATA:function(){},onComment:function(){},parse:function(b){for(var c,d,h=0,g;c=this._.htmlPartsRegex.exec(b);){d=c.index;if(d>h){h=b.substring(h,d);if(g)g.push(h);else this.onText(h)}h=
-this._.htmlPartsRegex.lastIndex;if(d=c[1]){d=d.toLowerCase();if(g&&CKEDITOR.dtd.$cdata[d]){this.onCDATA(g.join(""));g=null}if(!g){this.onTagClose(d);continue}}if(g)g.push(c[0]);else if(d=c[3]){d=d.toLowerCase();if(!/="/.test(d)){var n={},i;c=c[4];var j=!!(c&&c.charAt(c.length-1)=="/");if(c)for(;i=a.exec(c);){var o=i[1].toLowerCase();i=i[2]||i[3]||i[4]||"";n[o]=!i&&e[o]?o:CKEDITOR.tools.htmlDecodeAttr(i)}this.onTagOpen(d,n,j);!g&&CKEDITOR.dtd.$cdata[d]&&(g=[])}}else if(d=c[2])this.onComment(d)}if(b.length>
-h)this.onText(b.substring(h,b.length))}}})();
-CKEDITOR.htmlParser.basicWriter=CKEDITOR.tools.createClass({$:function(){this._={output:[]}},proto:{openTag:function(a){this._.output.push("<",a)},openTagClose:function(a,e){e?this._.output.push(" />"):this._.output.push(">")},attribute:function(a,e){typeof e=="string"&&(e=CKEDITOR.tools.htmlEncodeAttr(e));this._.output.push(" ",a,'="',e,'"')},closeTag:function(a){this._.output.push("</",a,">")},text:function(a){this._.output.push(a)},comment:function(a){this._.output.push("<\!--",a,"--\>")},write:function(a){this._.output.push(a)},
-reset:function(){this._.output=[];this._.indent=false},getHtml:function(a){var e=this._.output.join("");a&&this.reset();return e}}});"use strict";
-(function(){CKEDITOR.htmlParser.node=function(){};CKEDITOR.htmlParser.node.prototype={remove:function(){var a=this.parent.children,e=CKEDITOR.tools.indexOf(a,this),b=this.previous,c=this.next;b&&(b.next=c);c&&(c.previous=b);a.splice(e,1);this.parent=null},replaceWith:function(a){var e=this.parent.children,b=CKEDITOR.tools.indexOf(e,this),c=a.previous=this.previous,d=a.next=this.next;c&&(c.next=a);d&&(d.previous=a);e[b]=a;a.parent=this.parent;this.parent=null},insertAfter:function(a){var e=a.parent.children,
-b=CKEDITOR.tools.indexOf(e,a),c=a.next;e.splice(b+1,0,this);this.next=a.next;this.previous=a;a.next=this;c&&(c.previous=this);this.parent=a.parent},insertBefore:function(a){var e=a.parent.children,b=CKEDITOR.tools.indexOf(e,a);e.splice(b,0,this);this.next=a;(this.previous=a.previous)&&(a.previous.next=this);a.previous=this;this.parent=a.parent},getAscendant:function(a){var e=typeof a=="function"?a:typeof a=="string"?function(b){return b.name==a}:function(b){return b.name in a},b=this.parent;for(;b&&
-b.type==CKEDITOR.NODE_ELEMENT;){if(e(b))return b;b=b.parent}return null},wrapWith:function(a){this.replaceWith(a);a.add(this);return a},getIndex:function(){return CKEDITOR.tools.indexOf(this.parent.children,this)},getFilterContext:function(a){return a||{}}}})();"use strict";CKEDITOR.htmlParser.comment=function(a){this.value=a;this._={isBlockLike:false}};
-CKEDITOR.htmlParser.comment.prototype=CKEDITOR.tools.extend(new CKEDITOR.htmlParser.node,{type:CKEDITOR.NODE_COMMENT,filter:function(a,e){var b=this.value;if(!(b=a.onComment(e,b,this))){this.remove();return false}if(typeof b!="string"){this.replaceWith(b);return false}this.value=b;return true},writeHtml:function(a,e){e&&this.filter(e);a.comment(this.value)}});"use strict";
-(function(){CKEDITOR.htmlParser.text=function(a){this.value=a;this._={isBlockLike:false}};CKEDITOR.htmlParser.text.prototype=CKEDITOR.tools.extend(new CKEDITOR.htmlParser.node,{type:CKEDITOR.NODE_TEXT,filter:function(a,e){if(!(this.value=a.onText(e,this.value,this))){this.remove();return false}},writeHtml:function(a,e){e&&this.filter(e);a.text(this.value)}})})();"use strict";
-(function(){CKEDITOR.htmlParser.cdata=function(a){this.value=a};CKEDITOR.htmlParser.cdata.prototype=CKEDITOR.tools.extend(new CKEDITOR.htmlParser.node,{type:CKEDITOR.NODE_TEXT,filter:function(){},writeHtml:function(a){a.write(this.value)}})})();"use strict";CKEDITOR.htmlParser.fragment=function(){this.children=[];this.parent=null;this._={isBlockLike:true,hasInlineStarted:false}};
-(function(){function a(a){return a.attributes["data-cke-survive"]?false:a.name=="a"&&a.attributes.href||CKEDITOR.dtd.$removeEmpty[a.name]}var e=CKEDITOR.tools.extend({table:1,ul:1,ol:1,dl:1},CKEDITOR.dtd.table,CKEDITOR.dtd.ul,CKEDITOR.dtd.ol,CKEDITOR.dtd.dl),b={ol:1,ul:1},c=CKEDITOR.tools.extend({},{html:1},CKEDITOR.dtd.html,CKEDITOR.dtd.body,CKEDITOR.dtd.head,{style:1,script:1});CKEDITOR.htmlParser.fragment.fromHtml=function(d,h,g){function n(a){var b;if(p.length>0)for(var c=0;c<p.length;c++){var d=
-p[c],e=d.name,f=CKEDITOR.dtd[e],k=m.name&&CKEDITOR.dtd[m.name];if((!k||k[e])&&(!a||!f||f[a]||!CKEDITOR.dtd[a])){if(!b){i();b=1}d=d.clone();d.parent=m;m=d;p.splice(c,1);c--}else if(e==m.name){o(m,m.parent,1);c--}}}function i(){for(;y.length;)o(y.shift(),m)}function j(a){if(a._.isBlockLike&&a.name!="pre"&&a.name!="textarea"){var b=a.children.length,c=a.children[b-1],d;if(c&&c.type==CKEDITOR.NODE_TEXT)(d=CKEDITOR.tools.rtrim(c.value))?c.value=d:a.children.length=b-1}}function o(b,c,d){var c=c||m||f,
-e=m;if(b.previous===void 0){if(q(c,b)){m=c;u.onTagOpen(g,{});b.returnPoint=c=m}j(b);(!a(b)||b.children.length)&&c.add(b);b.name=="pre"&&(l=false);b.name=="textarea"&&(k=false)}if(b.returnPoint){m=b.returnPoint;delete b.returnPoint}else m=d?c:e}function q(a,b){if((a==f||a.name=="body")&&g&&(!a.name||CKEDITOR.dtd[a.name][g])){var c,d;return(c=b.attributes&&(d=b.attributes["data-cke-real-element-type"])?d:b.name)&&c in CKEDITOR.dtd.$inline&&!(c in CKEDITOR.dtd.head)&&!b.isOrphan||b.type==CKEDITOR.NODE_TEXT}}
-function s(a,b){return a in CKEDITOR.dtd.$listItem||a in CKEDITOR.dtd.$tableContent?a==b||a=="dt"&&b=="dd"||a=="dd"&&b=="dt":false}var u=new CKEDITOR.htmlParser,f=h instanceof CKEDITOR.htmlParser.element?h:typeof h=="string"?new CKEDITOR.htmlParser.element(h):new CKEDITOR.htmlParser.fragment,p=[],y=[],m=f,k=f.name=="textarea",l=f.name=="pre";u.onTagOpen=function(d,f,h,g){f=new CKEDITOR.htmlParser.element(d,f);if(f.isUnknown&&h)f.isEmpty=true;f.isOptionalClose=g;if(a(f))p.push(f);else{if(d=="pre")l=
-true;else{if(d=="br"&&l){m.add(new CKEDITOR.htmlParser.text("\n"));return}d=="textarea"&&(k=true)}if(d=="br")y.push(f);else{for(;;){g=(h=m.name)?CKEDITOR.dtd[h]||(m._.isBlockLike?CKEDITOR.dtd.div:CKEDITOR.dtd.span):c;if(!f.isUnknown&&!m.isUnknown&&!g[d])if(m.isOptionalClose)u.onTagClose(h);else if(d in b&&h in b){h=m.children;(h=h[h.length-1])&&h.name=="li"||o(h=new CKEDITOR.htmlParser.element("li"),m);!f.returnPoint&&(f.returnPoint=m);m=h}else if(d in CKEDITOR.dtd.$listItem&&!s(d,h))u.onTagOpen(d==
-"li"?"ul":"dl",{},0,1);else if(h in e&&!s(d,h)){!f.returnPoint&&(f.returnPoint=m);m=m.parent}else{h in CKEDITOR.dtd.$inline&&p.unshift(m);if(m.parent)o(m,m.parent,1);else{f.isOrphan=1;break}}else break}n(d);i();f.parent=m;f.isEmpty?o(f):m=f}}};u.onTagClose=function(a){for(var b=p.length-1;b>=0;b--)if(a==p[b].name){p.splice(b,1);return}for(var c=[],d=[],e=m;e!=f&&e.name!=a;){e._.isBlockLike||d.unshift(e);c.push(e);e=e.returnPoint||e.parent}if(e!=f){for(b=0;b<c.length;b++){var k=c[b];o(k,k.parent)}m=
-e;e._.isBlockLike&&i();o(e,e.parent);if(e==m)m=m.parent;p=p.concat(d)}a=="body"&&(g=false)};u.onText=function(a){if((!m._.hasInlineStarted||y.length)&&!l&&!k){a=CKEDITOR.tools.ltrim(a);if(a.length===0)return}var d=m.name,f=d?CKEDITOR.dtd[d]||(m._.isBlockLike?CKEDITOR.dtd.div:CKEDITOR.dtd.span):c;if(!k&&!f["#"]&&d in e){u.onTagOpen(d in b?"li":d=="dl"?"dd":d=="table"?"tr":d=="tr"?"td":"");u.onText(a)}else{i();n();!l&&!k&&(a=a.replace(/[\t\r\n ]{2,}|[\t\r\n]/g," "));a=new CKEDITOR.htmlParser.text(a);
-if(q(m,a))this.onTagOpen(g,{},0,1);m.add(a)}};u.onCDATA=function(a){m.add(new CKEDITOR.htmlParser.cdata(a))};u.onComment=function(a){i();n();m.add(new CKEDITOR.htmlParser.comment(a))};u.parse(d);for(i();m!=f;)o(m,m.parent,1);j(f);return f};CKEDITOR.htmlParser.fragment.prototype={type:CKEDITOR.NODE_DOCUMENT_FRAGMENT,add:function(a,b){isNaN(b)&&(b=this.children.length);var c=b>0?this.children[b-1]:null;if(c){if(a._.isBlockLike&&c.type==CKEDITOR.NODE_TEXT){c.value=CKEDITOR.tools.rtrim(c.value);if(c.value.length===
-0){this.children.pop();this.add(a);return}}c.next=a}a.previous=c;a.parent=this;this.children.splice(b,0,a);if(!this._.hasInlineStarted)this._.hasInlineStarted=a.type==CKEDITOR.NODE_TEXT||a.type==CKEDITOR.NODE_ELEMENT&&!a._.isBlockLike},filter:function(a,b){b=this.getFilterContext(b);a.onRoot(b,this);this.filterChildren(a,false,b)},filterChildren:function(a,b,c){if(this.childrenFilteredBy!=a.id){c=this.getFilterContext(c);if(b&&!this.parent)a.onRoot(c,this);this.childrenFilteredBy=a.id;for(b=0;b<this.children.length;b++)this.children[b].filter(a,
-c)===false&&b--}},writeHtml:function(a,b){b&&this.filter(b);this.writeChildrenHtml(a)},writeChildrenHtml:function(a,b,c){var e=this.getFilterContext();if(c&&!this.parent&&b)b.onRoot(e,this);b&&this.filterChildren(b,false,e);b=0;c=this.children;for(e=c.length;b<e;b++)c[b].writeHtml(a)},forEach:function(a,b,c){if(!c&&(!b||this.type==b))var e=a(this);if(e!==false)for(var c=this.children,i=0;i<c.length;i++){e=c[i];e.type==CKEDITOR.NODE_ELEMENT?e.forEach(a,b):(!b||e.type==b)&&a(e)}},getFilterContext:function(a){return a||
+CKEDITOR.config={customConfig:"config.js",autoUpdateElement:!0,language:"",defaultLanguage:"en",contentsLangDirection:"",enterMode:CKEDITOR.ENTER_P,forceEnterMode:!1,shiftEnterMode:CKEDITOR.ENTER_BR,docType:"\x3c!DOCTYPE html\x3e",bodyId:"",bodyClass:"",fullPage:!1,height:200,contentsCss:CKEDITOR.getUrl("contents.css"),extraPlugins:"",removePlugins:"",protectedSource:[],tabIndex:0,width:"",baseFloatZIndex:1E4,blockedKeystrokes:[CKEDITOR.CTRL+66,CKEDITOR.CTRL+73,CKEDITOR.CTRL+85]};
+(function(){function a(a,b,c,d,e){var l,f;a=[];for(l in b){f=b[l];f="boolean"==typeof f?{}:"function"==typeof f?{match:f}:v(f);"$"!=l.charAt(0)&&(f.elements=l);c&&(f.featureName=c.toLowerCase());var r=f;r.elements=h(r.elements,/\s+/)||null;r.propertiesOnly=r.propertiesOnly||!0===r.elements;var q=/\s*,\s*/,g=void 0;for(g in W){r[g]=h(r[g],q)||null;var p=r,t=O[g],m=h(r[O[g]],q),G=r[g],y=[],B=!0,C=void 0;m?B=!1:m={};for(C in G)"!"==C.charAt(0)&&(C=C.slice(1),y.push(C),m[C]=!0,B=!1);for(;C=y.pop();)G[C]=
+G["!"+C],delete G["!"+C];p[t]=(B?!1:m)||null}r.match=r.match||null;d.push(f);a.push(f)}b=e.elements;e=e.generic;var k;c=0;for(d=a.length;c<d;++c){l=v(a[c]);f=!0===l.classes||!0===l.styles||!0===l.attributes;r=l;g=t=q=void 0;for(q in W)r[q]=K(r[q]);p=!0;for(g in O){q=O[g];t=r[q];m=[];G=void 0;for(G in t)-1<G.indexOf("*")?m.push(new RegExp("^"+G.replace(/\*/g,".*")+"$")):m.push(G);t=m;t.length&&(r[q]=t,p=!1)}r.nothingRequired=p;r.noProperties=!(r.attributes||r.classes||r.styles);if(!0===l.elements||
+null===l.elements)e[f?"unshift":"push"](l);else for(k in r=l.elements,delete l.elements,r)if(b[k])b[k][f?"unshift":"push"](l);else b[k]=[l]}}function d(a,c,d,e){if(!a.match||a.match(c))if(e||k(a,c))if(a.propertiesOnly||(d.valid=!0),d.allAttributes||(d.allAttributes=b(a.attributes,c.attributes,d.validAttributes)),d.allStyles||(d.allStyles=b(a.styles,c.styles,d.validStyles)),!d.allClasses){a=a.classes;c=c.classes;e=d.validClasses;if(a)if(!0===a)a=!0;else{for(var l=0,f=c.length,r;l<f;++l)r=c[l],e[r]||
+(e[r]=a(r));a=!1}else a=!1;d.allClasses=a}}function b(a,b,c){if(!a)return!1;if(!0===a)return!0;for(var d in b)c[d]||(c[d]=a(d));return!1}function c(a,b,c){if(!a.match||a.match(b)){if(a.noProperties)return!1;c.hadInvalidAttribute=e(a.attributes,b.attributes)||c.hadInvalidAttribute;c.hadInvalidStyle=e(a.styles,b.styles)||c.hadInvalidStyle;a=a.classes;b=b.classes;if(a){for(var d=!1,l=!0===a,r=b.length;r--;)if(l||a(b[r]))b.splice(r,1),d=!0;a=d}else a=!1;c.hadInvalidClass=a||c.hadInvalidClass}}function e(a,
+b){if(!a)return!1;var c=!1,d=!0===a,e;for(e in b)if(d||a(e))delete b[e],c=!0;return c}function g(a,b,c){if(a.disabled||a.customConfig&&!c||!b)return!1;a._.cachedChecks={};return!0}function h(a,b){if(!a)return!1;if(!0===a)return a;if("string"==typeof a)return a=B(a),"*"==a?!0:CKEDITOR.tools.convertArrayToObject(a.split(b));if(CKEDITOR.tools.isArray(a))return a.length?CKEDITOR.tools.convertArrayToObject(a):!1;var c={},d=0,e;for(e in a)c[e]=a[e],d++;return d?c:!1}function k(a,b){if(a.nothingRequired)return!0;
+var c,d,e,l;if(e=a.requiredClasses)for(l=b.classes,c=0;c<e.length;++c)if(d=e[c],"string"==typeof d){if(-1==CKEDITOR.tools.indexOf(l,d))return!1}else if(!CKEDITOR.tools.checkIfAnyArrayItemMatches(l,d))return!1;return n(b.styles,a.requiredStyles)&&n(b.attributes,a.requiredAttributes)}function n(a,b){if(!b)return!0;for(var c=0,d;c<b.length;++c)if(d=b[c],"string"==typeof d){if(!(d in a))return!1}else if(!CKEDITOR.tools.checkIfAnyObjectPropertyMatches(a,d))return!1;return!0}function u(a){if(!a)return{};
+a=a.split(/\s*,\s*/).sort();for(var b={};a.length;)b[a.shift()]="cke-test";return b}function f(a){var b,c,d,e,l={},r=1;for(a=B(a);b=a.match(p);)(c=b[2])?(d=D(c,"styles"),e=D(c,"attrs"),c=D(c,"classes")):d=e=c=null,l["$"+r++]={elements:b[1],classes:c,styles:d,attributes:e},a=a.slice(b[0].length);return l}function D(a,b){var c=a.match(C[b]);return c?B(c[1]):null}function w(a){var b=a.styleBackup=a.attributes.style,c=a.classBackup=a.attributes["class"];a.styles||(a.styles=CKEDITOR.tools.parseCssText(b||
+"",1));a.classes||(a.classes=c?c.split(/\s+/):[])}function A(a,b,e,l){var f=0,q;l.toHtml&&(b.name=b.name.replace(r,"$1"));if(l.doCallbacks&&a.elementCallbacks){a:{q=a.elementCallbacks;for(var v=0,g=q.length,p;v<g;++v)if(p=q[v](b)){q=p;break a}q=void 0}if(q)return q}if(l.doTransform&&(q=a._.transformations[b.name])){w(b);for(v=0;v<q.length;++v)t(a,b,q[v]);x(b)}if(l.doFilter){a:{v=b.name;g=a._;a=g.allowedRules.elements[v];q=g.allowedRules.generic;v=g.disallowedRules.elements[v];g=g.disallowedRules.generic;
+p=l.skipRequired;var y={valid:!1,validAttributes:{},validClasses:{},validStyles:{},allAttributes:!1,allClasses:!1,allStyles:!1,hadInvalidAttribute:!1,hadInvalidClass:!1,hadInvalidStyle:!1},B,C;if(a||q){w(b);if(v)for(B=0,C=v.length;B<C;++B)if(!1===c(v[B],b,y)){a=null;break a}if(g)for(B=0,C=g.length;B<C;++B)c(g[B],b,y);if(a)for(B=0,C=a.length;B<C;++B)d(a[B],b,y,p);if(q)for(B=0,C=q.length;B<C;++B)d(q[B],b,y,p);a=y}else a=null}if(!a||!a.valid)return e.push(b),1;C=a.validAttributes;var h=a.validStyles;
+q=a.validClasses;var v=b.attributes,k=b.styles,g=b.classes;p=b.classBackup;var J=b.styleBackup,H,N,E=[],y=[],O=/^data-cke-/;B=!1;delete v.style;delete v["class"];delete b.classBackup;delete b.styleBackup;if(!a.allAttributes)for(H in v)C[H]||(O.test(H)?H==(N=H.replace(/^data-cke-saved-/,""))||C[N]||(delete v[H],B=!0):(delete v[H],B=!0));if(!a.allStyles||a.hadInvalidStyle){for(H in k)a.allStyles||h[H]?E.push(H+":"+k[H]):B=!0;E.length&&(v.style=E.sort().join("; "))}else J&&(v.style=J);if(!a.allClasses||
+a.hadInvalidClass){for(H=0;H<g.length;++H)(a.allClasses||q[g[H]])&&y.push(g[H]);y.length&&(v["class"]=y.sort().join(" "));p&&y.length<p.split(/\s+/).length&&(B=!0)}else p&&(v["class"]=p);B&&(f=1);if(!l.skipFinalValidation&&!m(b))return e.push(b),1}l.toHtml&&(b.name=b.name.replace(G,"cke:$1"));return f}function F(a){var b=[],c;for(c in a)-1<c.indexOf("*")&&b.push(c.replace(/\*/g,".*"));return b.length?new RegExp("^(?:"+b.join("|")+")$"):null}function x(a){var b=a.attributes,c;delete b.style;delete b["class"];
+if(c=CKEDITOR.tools.writeCssText(a.styles,!0))b.style=c;a.classes.length&&(b["class"]=a.classes.sort().join(" "))}function m(a){switch(a.name){case "a":if(!(a.children.length||a.attributes.name||a.attributes.id))return!1;break;case "img":if(!a.attributes.src)return!1}return!0}function K(a){if(!a)return!1;if(!0===a)return!0;var b=F(a);return function(c){return c in a||b&&c.match(b)}}function z(){return new CKEDITOR.htmlParser.element("br")}function I(a){return a.type==CKEDITOR.NODE_ELEMENT&&("br"==
+a.name||y.$block[a.name])}function l(a,b,c){var d=a.name;if(y.$empty[d]||!a.children.length)"hr"==d&&"br"==b?a.replaceWith(z()):(a.parent&&c.push({check:"it",el:a.parent}),a.remove());else if(y.$block[d]||"tr"==d)if("br"==b)a.previous&&!I(a.previous)&&(b=z(),b.insertBefore(a)),a.next&&!I(a.next)&&(b=z(),b.insertAfter(a)),a.replaceWithChildren();else{var d=a.children,e;b:{e=y[b];for(var l=0,r=d.length,f;l<r;++l)if(f=d[l],f.type==CKEDITOR.NODE_ELEMENT&&!e[f.name]){e=!1;break b}e=!0}if(e)a.name=b,a.attributes=
+{},c.push({check:"parent-down",el:a});else{e=a.parent;for(var l=e.type==CKEDITOR.NODE_DOCUMENT_FRAGMENT||"body"==e.name,v,q,r=d.length;0<r;)f=d[--r],l&&(f.type==CKEDITOR.NODE_TEXT||f.type==CKEDITOR.NODE_ELEMENT&&y.$inline[f.name])?(v||(v=new CKEDITOR.htmlParser.element(b),v.insertAfter(a),c.push({check:"parent-down",el:v})),v.add(f,0)):(v=null,q=y[e.name]||y.span,f.insertAfter(a),e.type==CKEDITOR.NODE_DOCUMENT_FRAGMENT||f.type!=CKEDITOR.NODE_ELEMENT||q[f.name]||c.push({check:"el-up",el:f}));a.remove()}}else d in
+{style:1,script:1}?a.remove():(a.parent&&c.push({check:"it",el:a.parent}),a.replaceWithChildren())}function t(a,b,c){var d,e;for(d=0;d<c.length;++d)if(e=c[d],!(e.check&&!a.check(e.check,!1)||e.left&&!e.left(b))){e.right(b,N);break}}function J(a,b){var c=b.getDefinition(),d=c.attributes,e=c.styles,l,f,r,v;if(a.name!=c.element)return!1;for(l in d)if("class"==l)for(c=d[l].split(/\s+/),r=a.classes.join("|");v=c.pop();){if(-1==r.indexOf(v))return!1}else if(a.attributes[l]!=d[l])return!1;for(f in e)if(a.styles[f]!=
+e[f])return!1;return!0}function H(a,b){var c,d;"string"==typeof a?c=a:a instanceof CKEDITOR.style?d=a:(c=a[0],d=a[1]);return[{element:c,left:d,right:function(a,c){c.transform(a,b)}}]}function E(a){return function(b){return J(b,a)}}function q(a){return function(b,c){c[a](b)}}var y=CKEDITOR.dtd,v=CKEDITOR.tools.copy,B=CKEDITOR.tools.trim,L=["","p","br","div"];CKEDITOR.FILTER_SKIP_TREE=2;CKEDITOR.filter=function(a){this.allowedContent=[];this.disallowedContent=[];this.elementCallbacks=null;this.disabled=
+!1;this.editor=null;this.id=CKEDITOR.tools.getNextNumber();this._={allowedRules:{elements:{},generic:[]},disallowedRules:{elements:{},generic:[]},transformations:{},cachedTests:{}};CKEDITOR.filter.instances[this.id]=this;if(a instanceof CKEDITOR.editor){a=this.editor=a;this.customConfig=!0;var b=a.config.allowedContent;!0===b?this.disabled=!0:(b||(this.customConfig=!1),this.allow(b,"config",1),this.allow(a.config.extraAllowedContent,"extra",1),this.allow(L[a.enterMode]+" "+L[a.shiftEnterMode],"default",
+1),this.disallow(a.config.disallowedContent))}else this.customConfig=!1,this.allow(a,"default",1)};CKEDITOR.filter.instances={};CKEDITOR.filter.prototype={allow:function(b,c,d){if(!g(this,b,d))return!1;var e,l;if("string"==typeof b)b=f(b);else if(b instanceof CKEDITOR.style){if(b.toAllowedContentRules)return this.allow(b.toAllowedContentRules(this.editor),c,d);e=b.getDefinition();b={};d=e.attributes;b[e.element]=e={styles:e.styles,requiredStyles:e.styles&&CKEDITOR.tools.objectKeys(e.styles)};d&&(d=
+v(d),e.classes=d["class"]?d["class"].split(/\s+/):null,e.requiredClasses=e.classes,delete d["class"],e.attributes=d,e.requiredAttributes=d&&CKEDITOR.tools.objectKeys(d))}else if(CKEDITOR.tools.isArray(b)){for(e=0;e<b.length;++e)l=this.allow(b[e],c,d);return l}a(this,b,c,this.allowedContent,this._.allowedRules);return!0},applyTo:function(a,b,c,d){if(this.disabled)return!1;var e=this,f=[],r=this.editor&&this.editor.config.protectedSource,v,q=!1,g={doFilter:!c,doTransform:!0,doCallbacks:!0,toHtml:b};
+a.forEach(function(a){if(a.type==CKEDITOR.NODE_ELEMENT){if("off"==a.attributes["data-cke-filter"])return!1;if(!b||"span"!=a.name||!~CKEDITOR.tools.objectKeys(a.attributes).join("|").indexOf("data-cke-"))if(v=A(e,a,f,g),v&1)q=!0;else if(v&2)return!1}else if(a.type==CKEDITOR.NODE_COMMENT&&a.value.match(/^\{cke_protected\}(?!\{C\})/)){var c;a:{var d=decodeURIComponent(a.value.replace(/^\{cke_protected\}/,""));c=[];var l,p,t;if(r)for(p=0;p<r.length;++p)if((t=d.match(r[p]))&&t[0].length==d.length){c=!0;
+break a}d=CKEDITOR.htmlParser.fragment.fromHtml(d);1==d.children.length&&(l=d.children[0]).type==CKEDITOR.NODE_ELEMENT&&A(e,l,c,g);c=!c.length}c||f.push(a)}},null,!0);f.length&&(q=!0);var p;a=[];d=L[d||(this.editor?this.editor.enterMode:CKEDITOR.ENTER_P)];for(var t;c=f.pop();)c.type==CKEDITOR.NODE_ELEMENT?l(c,d,a):c.remove();for(;p=a.pop();)if(c=p.el,c.parent)switch(t=y[c.parent.name]||y.span,p.check){case "it":y.$removeEmpty[c.name]&&!c.children.length?l(c,d,a):m(c)||l(c,d,a);break;case "el-up":c.parent.type==
+CKEDITOR.NODE_DOCUMENT_FRAGMENT||t[c.name]||l(c,d,a);break;case "parent-down":c.parent.type==CKEDITOR.NODE_DOCUMENT_FRAGMENT||t[c.name]||l(c.parent,d,a)}return q},checkFeature:function(a){if(this.disabled||!a)return!0;a.toFeature&&(a=a.toFeature(this.editor));return!a.requiredContent||this.check(a.requiredContent)},disable:function(){this.disabled=!0},disallow:function(b){if(!g(this,b,!0))return!1;"string"==typeof b&&(b=f(b));a(this,b,null,this.disallowedContent,this._.disallowedRules);return!0},
+addContentForms:function(a){if(!this.disabled&&a){var b,c,d=[],e;for(b=0;b<a.length&&!e;++b)c=a[b],("string"==typeof c||c instanceof CKEDITOR.style)&&this.check(c)&&(e=c);if(e){for(b=0;b<a.length;++b)d.push(H(a[b],e));this.addTransformations(d)}}},addElementCallback:function(a){this.elementCallbacks||(this.elementCallbacks=[]);this.elementCallbacks.push(a)},addFeature:function(a){if(this.disabled||!a)return!0;a.toFeature&&(a=a.toFeature(this.editor));this.allow(a.allowedContent,a.name);this.addTransformations(a.contentTransformations);
+this.addContentForms(a.contentForms);return a.requiredContent&&(this.customConfig||this.disallowedContent.length)?this.check(a.requiredContent):!0},addTransformations:function(a){var b,c;if(!this.disabled&&a){var d=this._.transformations,e;for(e=0;e<a.length;++e){b=a[e];var l=void 0,f=void 0,r=void 0,v=void 0,g=void 0,p=void 0;c=[];for(f=0;f<b.length;++f)r=b[f],"string"==typeof r?(r=r.split(/\s*:\s*/),v=r[0],g=null,p=r[1]):(v=r.check,g=r.left,p=r.right),l||(l=r,l=l.element?l.element:v?v.match(/^([a-z0-9]+)/i)[0]:
+l.left.getDefinition().element),g instanceof CKEDITOR.style&&(g=E(g)),c.push({check:v==l?null:v,left:g,right:"string"==typeof p?q(p):p});b=l;d[b]||(d[b]=[]);d[b].push(c)}}},check:function(a,b,c){if(this.disabled)return!0;if(CKEDITOR.tools.isArray(a)){for(var d=a.length;d--;)if(this.check(a[d],b,c))return!0;return!1}var e,l;if("string"==typeof a){l=a+"\x3c"+(!1===b?"0":"1")+(c?"1":"0")+"\x3e";if(l in this._.cachedChecks)return this._.cachedChecks[l];d=f(a).$1;e=d.styles;var r=d.classes;d.name=d.elements;
+d.classes=r=r?r.split(/\s*,\s*/):[];d.styles=u(e);d.attributes=u(d.attributes);d.children=[];r.length&&(d.attributes["class"]=r.join(" "));e&&(d.attributes.style=CKEDITOR.tools.writeCssText(d.styles));e=d}else d=a.getDefinition(),e=d.styles,r=d.attributes||{},e&&!CKEDITOR.tools.isEmpty(e)?(e=v(e),r.style=CKEDITOR.tools.writeCssText(e,!0)):e={},e={name:d.element,attributes:r,classes:r["class"]?r["class"].split(/\s+/):[],styles:e,children:[]};var r=CKEDITOR.tools.clone(e),q=[],g;if(!1!==b&&(g=this._.transformations[e.name])){for(d=
+0;d<g.length;++d)t(this,e,g[d]);x(e)}A(this,r,q,{doFilter:!0,doTransform:!1!==b,skipRequired:!c,skipFinalValidation:!c});b=0<q.length?!1:CKEDITOR.tools.objectCompare(e.attributes,r.attributes,!0)?!0:!1;"string"==typeof a&&(this._.cachedChecks[l]=b);return b},getAllowedEnterMode:function(){var a=["p","div","br"],b={p:CKEDITOR.ENTER_P,div:CKEDITOR.ENTER_DIV,br:CKEDITOR.ENTER_BR};return function(c,d){var e=a.slice(),l;if(this.check(L[c]))return c;for(d||(e=e.reverse());l=e.pop();)if(this.check(l))return b[l];
+return CKEDITOR.ENTER_BR}}(),destroy:function(){delete CKEDITOR.filter.instances[this.id];delete this._;delete this.allowedContent;delete this.disallowedContent}};var W={styles:1,attributes:1,classes:1},O={styles:"requiredStyles",attributes:"requiredAttributes",classes:"requiredClasses"},p=/^([a-z0-9\-*\s]+)((?:\s*\{[!\w\-,\s\*]+\}\s*|\s*\[[!\w\-,\s\*]+\]\s*|\s*\([!\w\-,\s\*]+\)\s*){0,3})(?:;\s*|$)/i,C={styles:/{([^}]+)}/,attrs:/\[([^\]]+)\]/,classes:/\(([^\)]+)\)/},r=/^cke:(object|embed|param)$/,
+G=/^(object|embed|param)$/,N;N=CKEDITOR.filter.transformationsTools={sizeToStyle:function(a){this.lengthToStyle(a,"width");this.lengthToStyle(a,"height")},sizeToAttribute:function(a){this.lengthToAttribute(a,"width");this.lengthToAttribute(a,"height")},lengthToStyle:function(a,b,c){c=c||b;if(!(c in a.styles)){var d=a.attributes[b];d&&(/^\d+$/.test(d)&&(d+="px"),a.styles[c]=d)}delete a.attributes[b]},lengthToAttribute:function(a,b,c){c=c||b;if(!(c in a.attributes)){var d=a.styles[b],e=d&&d.match(/^(\d+)(?:\.\d*)?px$/);
+e?a.attributes[c]=e[1]:"cke-test"==d&&(a.attributes[c]="cke-test")}delete a.styles[b]},alignmentToStyle:function(a){if(!("float"in a.styles)){var b=a.attributes.align;if("left"==b||"right"==b)a.styles["float"]=b}delete a.attributes.align},alignmentToAttribute:function(a){if(!("align"in a.attributes)){var b=a.styles["float"];if("left"==b||"right"==b)a.attributes.align=b}delete a.styles["float"]},splitBorderShorthand:function(a){function b(d){a.styles["border-top-width"]=c[d[0]];a.styles["border-right-width"]=
+c[d[1]];a.styles["border-bottom-width"]=c[d[2]];a.styles["border-left-width"]=c[d[3]]}if(a.styles.border){var c=a.styles.border.match(/([\.\d]+\w+)/g)||["0px"];switch(c.length){case 1:a.styles["border-width"]=c[0];break;case 2:b([0,1,0,1]);break;case 3:b([0,1,2,1]);break;case 4:b([0,1,2,3])}a.styles["border-style"]=a.styles["border-style"]||(a.styles.border.match(/(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset|initial|inherit)/)||[])[0];a.styles["border-style"]||delete a.styles["border-style"];
+delete a.styles.border}},listTypeToStyle:function(a){if(a.attributes.type)switch(a.attributes.type){case "a":a.styles["list-style-type"]="lower-alpha";break;case "A":a.styles["list-style-type"]="upper-alpha";break;case "i":a.styles["list-style-type"]="lower-roman";break;case "I":a.styles["list-style-type"]="upper-roman";break;case "1":a.styles["list-style-type"]="decimal";break;default:a.styles["list-style-type"]=a.attributes.type}},splitMarginShorthand:function(a){function b(d){a.styles["margin-top"]=
+c[d[0]];a.styles["margin-right"]=c[d[1]];a.styles["margin-bottom"]=c[d[2]];a.styles["margin-left"]=c[d[3]]}if(a.styles.margin){var c=a.styles.margin.match(/(\-?[\.\d]+\w+)/g)||["0px"];switch(c.length){case 1:b([0,0,0,0]);break;case 2:b([0,1,0,1]);break;case 3:b([0,1,2,1]);break;case 4:b([0,1,2,3])}delete a.styles.margin}},matchesStyle:J,transform:function(a,b){if("string"==typeof b)a.name=b;else{var c=b.getDefinition(),d=c.styles,e=c.attributes,l,r,f,v;a.name=c.element;for(l in e)if("class"==l)for(c=
+a.classes.join("|"),f=e[l].split(/\s+/);v=f.pop();)-1==c.indexOf(v)&&a.classes.push(v);else a.attributes[l]=e[l];for(r in d)a.styles[r]=d[r]}}}})();
+(function(){CKEDITOR.focusManager=function(a){if(a.focusManager)return a.focusManager;this.hasFocus=!1;this.currentActive=null;this._={editor:a};return this};CKEDITOR.focusManager._={blurDelay:200};CKEDITOR.focusManager.prototype={focus:function(a){this._.timer&&clearTimeout(this._.timer);a&&(this.currentActive=a);this.hasFocus||this._.locked||((a=CKEDITOR.currentInstance)&&a.focusManager.blur(1),this.hasFocus=!0,(a=this._.editor.container)&&a.addClass("cke_focus"),this._.editor.fire("focus"))},lock:function(){this._.locked=
+1},unlock:function(){delete this._.locked},blur:function(a){function d(){if(this.hasFocus){this.hasFocus=!1;var a=this._.editor.container;a&&a.removeClass("cke_focus");this._.editor.fire("blur")}}if(!this._.locked){this._.timer&&clearTimeout(this._.timer);var b=CKEDITOR.focusManager._.blurDelay;a||!b?d.call(this):this._.timer=CKEDITOR.tools.setTimeout(function(){delete this._.timer;d.call(this)},b,this)}},add:function(a,d){var b=a.getCustomData("focusmanager");if(!b||b!=this){b&&b.remove(a);var b=
+"focus",c="blur";d&&(CKEDITOR.env.ie?(b="focusin",c="focusout"):CKEDITOR.event.useCapture=1);var e={blur:function(){a.equals(this.currentActive)&&this.blur()},focus:function(){this.focus(a)}};a.on(b,e.focus,this);a.on(c,e.blur,this);d&&(CKEDITOR.event.useCapture=0);a.setCustomData("focusmanager",this);a.setCustomData("focusmanager_handlers",e)}},remove:function(a){a.removeCustomData("focusmanager");var d=a.removeCustomData("focusmanager_handlers");a.removeListener("blur",d.blur);a.removeListener("focus",
+d.focus)}}})();CKEDITOR.keystrokeHandler=function(a){if(a.keystrokeHandler)return a.keystrokeHandler;this.keystrokes={};this.blockedKeystrokes={};this._={editor:a};return this};
+(function(){var a,d=function(b){b=b.data;var d=b.getKeystroke(),g=this.keystrokes[d],h=this._.editor;a=!1===h.fire("key",{keyCode:d,domEvent:b});a||(g&&(a=!1!==h.execCommand(g,{from:"keystrokeHandler"})),a||(a=!!this.blockedKeystrokes[d]));a&&b.preventDefault(!0);return!a},b=function(b){a&&(a=!1,b.data.preventDefault(!0))};CKEDITOR.keystrokeHandler.prototype={attach:function(a){a.on("keydown",d,this);if(CKEDITOR.env.gecko&&CKEDITOR.env.mac)a.on("keypress",b,this)}}})();
+(function(){CKEDITOR.lang={languages:{af:1,ar:1,az:1,bg:1,bn:1,bs:1,ca:1,cs:1,cy:1,da:1,de:1,"de-ch":1,el:1,"en-au":1,"en-ca":1,"en-gb":1,en:1,eo:1,es:1,"es-mx":1,et:1,eu:1,fa:1,fi:1,fo:1,"fr-ca":1,fr:1,gl:1,gu:1,he:1,hi:1,hr:1,hu:1,id:1,is:1,it:1,ja:1,ka:1,km:1,ko:1,ku:1,lt:1,lv:1,mk:1,mn:1,ms:1,nb:1,nl:1,no:1,oc:1,pl:1,"pt-br":1,pt:1,ro:1,ru:1,si:1,sk:1,sl:1,sq:1,"sr-latn":1,sr:1,sv:1,th:1,tr:1,tt:1,ug:1,uk:1,vi:1,"zh-cn":1,zh:1},rtl:{ar:1,fa:1,he:1,ku:1,ug:1},load:function(a,d,b){a&&CKEDITOR.lang.languages[a]||
+(a=this.detect(d,a));var c=this;d=function(){c[a].dir=c.rtl[a]?"rtl":"ltr";b(a,c[a])};this[a]?d():CKEDITOR.scriptLoader.load(CKEDITOR.getUrl("lang/"+a+".js"),d,this)},detect:function(a,d){var b=this.languages;d=d||navigator.userLanguage||navigator.language||a;var c=d.toLowerCase().match(/([a-z]+)(?:-([a-z]+))?/),e=c[1],c=c[2];b[e+"-"+c]?e=e+"-"+c:b[e]||(e=null);CKEDITOR.lang.detect=e?function(){return e}:function(a){return a};return e||a}}})();
+CKEDITOR.scriptLoader=function(){var a={},d={};return{load:function(b,c,e,g){var h="string"==typeof b;h&&(b=[b]);e||(e=CKEDITOR);var k=b.length,n=[],u=[],f=function(a){c&&(h?c.call(e,a):c.call(e,n,u))};if(0===k)f(!0);else{var D=function(a,b){(b?n:u).push(a);0>=--k&&(g&&CKEDITOR.document.getDocumentElement().removeStyle("cursor"),f(b))},w=function(b,c){a[b]=1;var e=d[b];delete d[b];for(var f=0;f<e.length;f++)e[f](b,c)},A=function(b){if(a[b])D(b,!0);else{var e=d[b]||(d[b]=[]);e.push(D);if(!(1<e.length)){var f=
+new CKEDITOR.dom.element("script");f.setAttributes({type:"text/javascript",src:b});c&&(CKEDITOR.env.ie&&(8>=CKEDITOR.env.version||CKEDITOR.env.ie9Compat)?f.$.onreadystatechange=function(){if("loaded"==f.$.readyState||"complete"==f.$.readyState)f.$.onreadystatechange=null,w(b,!0)}:(f.$.onload=function(){setTimeout(function(){w(b,!0)},0)},f.$.onerror=function(){w(b,!1)}));f.appendTo(CKEDITOR.document.getHead())}}};g&&CKEDITOR.document.getDocumentElement().setStyle("cursor","wait");for(var F=0;F<k;F++)A(b[F])}},
+queue:function(){function a(){var b;(b=c[0])&&this.load(b.scriptUrl,b.callback,CKEDITOR,0)}var c=[];return function(d,g){var h=this;c.push({scriptUrl:d,callback:function(){g&&g.apply(this,arguments);c.shift();a.call(h)}});1==c.length&&a.call(this)}}()}}();CKEDITOR.resourceManager=function(a,d){this.basePath=a;this.fileName=d;this.registered={};this.loaded={};this.externals={};this._={waitingList:{}}};
+CKEDITOR.resourceManager.prototype={add:function(a,d){if(this.registered[a])throw Error('[CKEDITOR.resourceManager.add] The resource name "'+a+'" is already registered.');var b=this.registered[a]=d||{};b.name=a;b.path=this.getPath(a);CKEDITOR.fire(a+CKEDITOR.tools.capitalize(this.fileName)+"Ready",b);return this.get(a)},get:function(a){return this.registered[a]||null},getPath:function(a){var d=this.externals[a];return CKEDITOR.getUrl(d&&d.dir||this.basePath+a+"/")},getFilePath:function(a){var d=this.externals[a];
+return CKEDITOR.getUrl(this.getPath(a)+(d?d.file:this.fileName+".js"))},addExternal:function(a,d,b){a=a.split(",");for(var c=0;c<a.length;c++){var e=a[c];b||(d=d.replace(/[^\/]+$/,function(a){b=a;return""}));this.externals[e]={dir:d,file:b||this.fileName+".js"}}},load:function(a,d,b){CKEDITOR.tools.isArray(a)||(a=a?[a]:[]);for(var c=this.loaded,e=this.registered,g=[],h={},k={},n=0;n<a.length;n++){var u=a[n];if(u)if(c[u]||e[u])k[u]=this.get(u);else{var f=this.getFilePath(u);g.push(f);f in h||(h[f]=
+[]);h[f].push(u)}}CKEDITOR.scriptLoader.load(g,function(a,e){if(e.length)throw Error('[CKEDITOR.resourceManager.load] Resource name "'+h[e[0]].join(",")+'" was not found at "'+e[0]+'".');for(var f=0;f<a.length;f++)for(var g=h[a[f]],n=0;n<g.length;n++){var m=g[n];k[m]=this.get(m);c[m]=1}d.call(b,k)},this)}};CKEDITOR.plugins=new CKEDITOR.resourceManager("plugins/","plugin");
+CKEDITOR.plugins.load=CKEDITOR.tools.override(CKEDITOR.plugins.load,function(a){var d={};return function(b,c,e){var g={},h=function(b){a.call(this,b,function(a){CKEDITOR.tools.extend(g,a);var b=[],f;for(f in a){var k=a[f],w=k&&k.requires;if(!d[f]){if(k.icons)for(var A=k.icons.split(","),F=A.length;F--;)CKEDITOR.skin.addIcon(A[F],k.path+"icons/"+(CKEDITOR.env.hidpi&&k.hidpi?"hidpi/":"")+A[F]+".png");d[f]=1}if(w)for(w.split&&(w=w.split(",")),k=0;k<w.length;k++)g[w[k]]||b.push(w[k])}if(b.length)h.call(this,
+b);else{for(f in g)k=g[f],k.onLoad&&!k.onLoad._called&&(!1===k.onLoad()&&delete g[f],k.onLoad._called=1);c&&c.call(e||window,g)}},this)};h.call(this,b)}});CKEDITOR.plugins.setLang=function(a,d,b){var c=this.get(a);a=c.langEntries||(c.langEntries={});c=c.lang||(c.lang=[]);c.split&&(c=c.split(","));-1==CKEDITOR.tools.indexOf(c,d)&&c.push(d);a[d]=b};CKEDITOR.ui=function(a){if(a.ui)return a.ui;this.items={};this.instances={};this.editor=a;this._={handlers:{}};return this};
+CKEDITOR.ui.prototype={add:function(a,d,b){b.name=a.toLowerCase();var c=this.items[a]={type:d,command:b.command||null,args:Array.prototype.slice.call(arguments,2)};CKEDITOR.tools.extend(c,b)},get:function(a){return this.instances[a]},create:function(a){var d=this.items[a],b=d&&this._.handlers[d.type],c=d&&d.command&&this.editor.getCommand(d.command),b=b&&b.create.apply(this,d.args);this.instances[a]=b;c&&c.uiItems.push(b);b&&!b.type&&(b.type=d.type);return b},addHandler:function(a,d){this._.handlers[a]=
+d},space:function(a){return CKEDITOR.document.getById(this.spaceId(a))},spaceId:function(a){return this.editor.id+"_"+a}};CKEDITOR.event.implementOn(CKEDITOR.ui);
+(function(){function a(a,e,f){CKEDITOR.event.call(this);a=a&&CKEDITOR.tools.clone(a);if(void 0!==e){if(!(e instanceof CKEDITOR.dom.element))throw Error("Expect element of type CKEDITOR.dom.element.");if(!f)throw Error("One of the element modes must be specified.");if(CKEDITOR.env.ie&&CKEDITOR.env.quirks&&f==CKEDITOR.ELEMENT_MODE_INLINE)throw Error("Inline element mode is not supported on IE quirks.");if(!b(e,f))throw Error('The specified element mode is not supported on element: "'+e.getName()+'".');
+this.element=e;this.elementMode=f;this.name=this.elementMode!=CKEDITOR.ELEMENT_MODE_APPENDTO&&(e.getId()||e.getNameAtt())}else this.elementMode=CKEDITOR.ELEMENT_MODE_NONE;this._={};this.commands={};this.templates={};this.name=this.name||d();this.id=CKEDITOR.tools.getNextId();this.status="unloaded";this.config=CKEDITOR.tools.prototypedCopy(CKEDITOR.config);this.ui=new CKEDITOR.ui(this);this.focusManager=new CKEDITOR.focusManager(this);this.keystrokeHandler=new CKEDITOR.keystrokeHandler(this);this.on("readOnly",
+c);this.on("selectionChange",function(a){g(this,a.data.path)});this.on("activeFilterChange",function(){g(this,this.elementPath(),!0)});this.on("mode",c);this.on("instanceReady",function(){this.config.startupFocus&&this.focus()});CKEDITOR.fire("instanceCreated",null,this);CKEDITOR.add(this);CKEDITOR.tools.setTimeout(function(){"destroyed"!==this.status?k(this,a):CKEDITOR.warn("editor-incorrect-destroy")},0,this)}function d(){do var a="editor"+ ++F;while(CKEDITOR.instances[a]);return a}function b(a,
+b){return b==CKEDITOR.ELEMENT_MODE_INLINE?a.is(CKEDITOR.dtd.$editable)||a.is("textarea"):b==CKEDITOR.ELEMENT_MODE_REPLACE?!a.is(CKEDITOR.dtd.$nonBodyContent):1}function c(){var a=this.commands,b;for(b in a)e(this,a[b])}function e(a,b){b[b.startDisabled?"disable":a.readOnly&&!b.readOnly?"disable":b.modes[a.mode]?"enable":"disable"]()}function g(a,b,c){if(b){var d,e,f=a.commands;for(e in f)d=f[e],(c||d.contextSensitive)&&d.refresh(a,b)}}function h(a){var b=a.config.customConfig;if(!b)return!1;var b=
+CKEDITOR.getUrl(b),c=x[b]||(x[b]={});c.fn?(c.fn.call(a,a.config),CKEDITOR.getUrl(a.config.customConfig)!=b&&h(a)||a.fireOnce("customConfigLoaded")):CKEDITOR.scriptLoader.queue(b,function(){c.fn=CKEDITOR.editorConfig?CKEDITOR.editorConfig:function(){};h(a)});return!0}function k(a,b){a.on("customConfigLoaded",function(){if(b){if(b.on)for(var c in b.on)a.on(c,b.on[c]);CKEDITOR.tools.extend(a.config,b,!0);delete a.config.on}c=a.config;a.readOnly=c.readOnly?!0:a.elementMode==CKEDITOR.ELEMENT_MODE_INLINE?
+a.element.is("textarea")?a.element.hasAttribute("disabled")||a.element.hasAttribute("readonly"):a.element.isReadOnly():a.elementMode==CKEDITOR.ELEMENT_MODE_REPLACE?a.element.hasAttribute("disabled")||a.element.hasAttribute("readonly"):!1;a.blockless=a.elementMode==CKEDITOR.ELEMENT_MODE_INLINE?!(a.element.is("textarea")||CKEDITOR.dtd[a.element.getName()].p):!1;a.tabIndex=c.tabIndex||a.element&&a.element.getAttribute("tabindex")||0;a.activeEnterMode=a.enterMode=a.blockless?CKEDITOR.ENTER_BR:c.enterMode;
+a.activeShiftEnterMode=a.shiftEnterMode=a.blockless?CKEDITOR.ENTER_BR:c.shiftEnterMode;c.skin&&(CKEDITOR.skinName=c.skin);a.fireOnce("configLoaded");a.dataProcessor=new CKEDITOR.htmlDataProcessor(a);a.filter=a.activeFilter=new CKEDITOR.filter(a);n(a)});b&&null!=b.customConfig&&(a.config.customConfig=b.customConfig);h(a)||a.fireOnce("customConfigLoaded")}function n(a){CKEDITOR.skin.loadPart("editor",function(){u(a)})}function u(a){CKEDITOR.lang.load(a.config.language,a.config.defaultLanguage,function(b,
+c){var d=a.config.title;a.langCode=b;a.lang=CKEDITOR.tools.prototypedCopy(c);a.title="string"==typeof d||!1===d?d:[a.lang.editor,a.name].join(", ");a.config.contentsLangDirection||(a.config.contentsLangDirection=a.elementMode==CKEDITOR.ELEMENT_MODE_INLINE?a.element.getDirection(1):a.lang.dir);a.fire("langLoaded");f(a)})}function f(a){a.getStylesSet(function(b){a.once("loaded",function(){a.fire("stylesSet",{styles:b})},null,null,1);D(a)})}function D(a){var b=a.config,c=b.plugins,d=b.extraPlugins,e=
+b.removePlugins;if(d)var f=new RegExp("(?:^|,)(?:"+d.replace(/\s*,\s*/g,"|")+")(?\x3d,|$)","g"),c=c.replace(f,""),c=c+(","+d);if(e)var g=new RegExp("(?:^|,)(?:"+e.replace(/\s*,\s*/g,"|")+")(?\x3d,|$)","g"),c=c.replace(g,"");CKEDITOR.env.air&&(c+=",adobeair");CKEDITOR.plugins.load(c.split(","),function(c){var d=[],e=[],f=[];a.plugins=c;for(var l in c){var t=c[l],k=t.lang,h=null,O=t.requires,p;CKEDITOR.tools.isArray(O)&&(O=O.join(","));if(O&&(p=O.match(g)))for(;O=p.pop();)CKEDITOR.error("editor-plugin-required",
+{plugin:O.replace(",",""),requiredBy:l});k&&!a.lang[l]&&(k.split&&(k=k.split(",")),0<=CKEDITOR.tools.indexOf(k,a.langCode)?h=a.langCode:(h=a.langCode.replace(/-.*/,""),h=h!=a.langCode&&0<=CKEDITOR.tools.indexOf(k,h)?h:0<=CKEDITOR.tools.indexOf(k,"en")?"en":k[0]),t.langEntries&&t.langEntries[h]?(a.lang[l]=t.langEntries[h],h=null):f.push(CKEDITOR.getUrl(t.path+"lang/"+h+".js")));e.push(h);d.push(t)}CKEDITOR.scriptLoader.load(f,function(){for(var c=["beforeInit","init","afterInit"],f=0;f<c.length;f++)for(var l=
+0;l<d.length;l++){var v=d[l];0===f&&e[l]&&v.lang&&v.langEntries&&(a.lang[v.name]=v.langEntries[e[l]]);if(v[c[f]])v[c[f]](a)}a.fireOnce("pluginsLoaded");b.keystrokes&&a.setKeystroke(a.config.keystrokes);for(l=0;l<a.config.blockedKeystrokes.length;l++)a.keystrokeHandler.blockedKeystrokes[a.config.blockedKeystrokes[l]]=1;a.status="loaded";a.fireOnce("loaded");CKEDITOR.fire("instanceLoaded",null,a)})})}function w(){var a=this.element;if(a&&this.elementMode!=CKEDITOR.ELEMENT_MODE_APPENDTO){var b=this.getData();
+this.config.htmlEncodeOutput&&(b=CKEDITOR.tools.htmlEncode(b));a.is("textarea")?a.setValue(b):a.setHtml(b);return!0}return!1}function A(a,b){function c(a){var b=a.startContainer,d=a.endContainer;return b.is&&(b.is("tr")||b.is("td")&&b.equals(d)&&a.endOffset===b.getChildCount())?!0:!1}function d(a){var b=a.startContainer;return b.is("tr")?a.cloneContents():b.clone(!0)}for(var e=new CKEDITOR.dom.documentFragment,f,g,k,h=0;h<a.length;h++){var q=a[h],y=q.startContainer.getAscendant("tr",!0);c(q)?(f||
+(f=y.getAscendant("table").clone(),f.append(y.getAscendant({thead:1,tbody:1,tfoot:1}).clone()),e.append(f),f=f.findOne("thead, tbody, tfoot")),g&&g.equals(y)||(g=y,k=y.clone(),f.append(k)),k.append(d(q))):e.append(q.cloneContents())}return f?e:b.getHtmlFromRange(a[0])}a.prototype=CKEDITOR.editor.prototype;CKEDITOR.editor=a;var F=0,x={};CKEDITOR.tools.extend(CKEDITOR.editor.prototype,{addCommand:function(a,b){b.name=a.toLowerCase();var c=new CKEDITOR.command(this,b);this.mode&&e(this,c);return this.commands[a]=
+c},_attachToForm:function(){function a(b){c.updateElement();c._.required&&!d.getValue()&&!1===c.fire("required")&&b.data.preventDefault()}function b(a){return!!(a&&a.call&&a.apply)}var c=this,d=c.element,e=new CKEDITOR.dom.element(d.$.form);d.is("textarea")&&e&&(e.on("submit",a),b(e.$.submit)&&(e.$.submit=CKEDITOR.tools.override(e.$.submit,function(b){return function(){a();b.apply?b.apply(this):b()}})),c.on("destroy",function(){e.removeListener("submit",a)}))},destroy:function(a){this.fire("beforeDestroy");
+!a&&w.call(this);this.editable(null);this.filter&&(this.filter.destroy(),delete this.filter);delete this.activeFilter;this.status="destroyed";this.fire("destroy");this.removeAllListeners();CKEDITOR.remove(this);CKEDITOR.fire("instanceDestroyed",null,this)},elementPath:function(a){if(!a){a=this.getSelection();if(!a)return null;a=a.getStartElement()}return a?new CKEDITOR.dom.elementPath(a,this.editable()):null},createRange:function(){var a=this.editable();return a?new CKEDITOR.dom.range(a):null},execCommand:function(a,
+b){var c=this.getCommand(a),d={name:a,commandData:b||{},command:c};return c&&c.state!=CKEDITOR.TRISTATE_DISABLED&&!1!==this.fire("beforeCommandExec",d)&&(d.returnValue=c.exec(d.commandData),!c.async&&!1!==this.fire("afterCommandExec",d))?d.returnValue:!1},getCommand:function(a){return this.commands[a]},getData:function(a){!a&&this.fire("beforeGetData");var b=this._.data;"string"!=typeof b&&(b=(b=this.element)&&this.elementMode==CKEDITOR.ELEMENT_MODE_REPLACE?b.is("textarea")?b.getValue():b.getHtml():
+"");b={dataValue:b};!a&&this.fire("getData",b);return b.dataValue},getSnapshot:function(){var a=this.fire("getSnapshot");"string"!=typeof a&&(a=(a=this.element)&&this.elementMode==CKEDITOR.ELEMENT_MODE_REPLACE?a.is("textarea")?a.getValue():a.getHtml():"");return a},loadSnapshot:function(a){this.fire("loadSnapshot",a)},setData:function(a,b,c){var d=!0,e=b;b&&"object"==typeof b&&(c=b.internal,e=b.callback,d=!b.noSnapshot);!c&&d&&this.fire("saveSnapshot");if(e||!c)this.once("dataReady",function(a){!c&&
+d&&this.fire("saveSnapshot");e&&e.call(a.editor)});a={dataValue:a};!c&&this.fire("setData",a);this._.data=a.dataValue;!c&&this.fire("afterSetData",a)},setReadOnly:function(a){a=null==a||a;this.readOnly!=a&&(this.readOnly=a,this.keystrokeHandler.blockedKeystrokes[8]=+a,this.editable().setReadOnly(a),this.fire("readOnly"))},insertHtml:function(a,b,c){this.fire("insertHtml",{dataValue:a,mode:b,range:c})},insertText:function(a){this.fire("insertText",a)},insertElement:function(a){this.fire("insertElement",
+a)},getSelectedHtml:function(a){var b=this.editable(),c=this.getSelection(),c=c&&c.getRanges();if(!b||!c||0===c.length)return null;b=A(c,b);return a?b.getHtml():b},extractSelectedHtml:function(a,b){var c=this.editable(),d=this.getSelection().getRanges(),e=new CKEDITOR.dom.documentFragment,f;if(!c||0===d.length)return null;for(f=0;f<d.length;f++)e.append(c.extractHtmlFromRange(d[f],b));b||this.getSelection().selectRanges([d[0]]);return a?e.getHtml():e},focus:function(){this.fire("beforeFocus")},checkDirty:function(){return"ready"==
+this.status&&this._.previousValue!==this.getSnapshot()},resetDirty:function(){this._.previousValue=this.getSnapshot()},updateElement:function(){return w.call(this)},setKeystroke:function(){for(var a=this.keystrokeHandler.keystrokes,b=CKEDITOR.tools.isArray(arguments[0])?arguments[0]:[[].slice.call(arguments,0)],c,d,e=b.length;e--;)c=b[e],d=0,CKEDITOR.tools.isArray(c)&&(d=c[1],c=c[0]),d?a[c]=d:delete a[c]},getCommandKeystroke:function(a){if(a="string"===typeof a?this.getCommand(a):a){var b=CKEDITOR.tools.object.findKey(this.commands,
+a),c=this.keystrokeHandler.keystrokes,d;if(a.fakeKeystroke)return a.fakeKeystroke;for(d in c)if(c.hasOwnProperty(d)&&c[d]==b)return d}return null},addFeature:function(a){return this.filter.addFeature(a)},setActiveFilter:function(a){a||(a=this.filter);this.activeFilter!==a&&(this.activeFilter=a,this.fire("activeFilterChange"),a===this.filter?this.setActiveEnterMode(null,null):this.setActiveEnterMode(a.getAllowedEnterMode(this.enterMode),a.getAllowedEnterMode(this.shiftEnterMode,!0)))},setActiveEnterMode:function(a,
+b){a=a?this.blockless?CKEDITOR.ENTER_BR:a:this.enterMode;b=b?this.blockless?CKEDITOR.ENTER_BR:b:this.shiftEnterMode;if(this.activeEnterMode!=a||this.activeShiftEnterMode!=b)this.activeEnterMode=a,this.activeShiftEnterMode=b,this.fire("activeEnterModeChange")},showNotification:function(a){alert(a)}})})();CKEDITOR.ELEMENT_MODE_NONE=0;CKEDITOR.ELEMENT_MODE_REPLACE=1;CKEDITOR.ELEMENT_MODE_APPENDTO=2;CKEDITOR.ELEMENT_MODE_INLINE=3;CKEDITOR.htmlParser=function(){this._={htmlPartsRegex:/<(?:(?:\/([^>]+)>)|(?:!--([\S|\s]*?)--\x3e)|(?:([^\/\s>]+)((?:\s+[\w\-:.]+(?:\s*=\s*?(?:(?:"[^"]*")|(?:'[^']*')|[^\s"'\/>]+))?)*)[\S\s]*?(\/?)>))/g}};
+(function(){var a=/([\w\-:.]+)(?:(?:\s*=\s*(?:(?:"([^"]*)")|(?:'([^']*)')|([^\s>]+)))|(?=\s|$))/g,d={checked:1,compact:1,declare:1,defer:1,disabled:1,ismap:1,multiple:1,nohref:1,noresize:1,noshade:1,nowrap:1,readonly:1,selected:1};CKEDITOR.htmlParser.prototype={onTagOpen:function(){},onTagClose:function(){},onText:function(){},onCDATA:function(){},onComment:function(){},parse:function(b){for(var c,e,g=0,h;c=this._.htmlPartsRegex.exec(b);){e=c.index;if(e>g)if(g=b.substring(g,e),h)h.push(g);else this.onText(g);
+g=this._.htmlPartsRegex.lastIndex;if(e=c[1])if(e=e.toLowerCase(),h&&CKEDITOR.dtd.$cdata[e]&&(this.onCDATA(h.join("")),h=null),!h){this.onTagClose(e);continue}if(h)h.push(c[0]);else if(e=c[3]){if(e=e.toLowerCase(),!/="/.test(e)){var k={},n,u=c[4];c=!!c[5];if(u)for(;n=a.exec(u);){var f=n[1].toLowerCase();n=n[2]||n[3]||n[4]||"";k[f]=!n&&d[f]?f:CKEDITOR.tools.htmlDecodeAttr(n)}this.onTagOpen(e,k,c);!h&&CKEDITOR.dtd.$cdata[e]&&(h=[])}}else if(e=c[2])this.onComment(e)}if(b.length>g)this.onText(b.substring(g,
+b.length))}}})();
+CKEDITOR.htmlParser.basicWriter=CKEDITOR.tools.createClass({$:function(){this._={output:[]}},proto:{openTag:function(a){this._.output.push("\x3c",a)},openTagClose:function(a,d){d?this._.output.push(" /\x3e"):this._.output.push("\x3e")},attribute:function(a,d){"string"==typeof d&&(d=CKEDITOR.tools.htmlEncodeAttr(d));this._.output.push(" ",a,'\x3d"',d,'"')},closeTag:function(a){this._.output.push("\x3c/",a,"\x3e")},text:function(a){this._.output.push(a)},comment:function(a){this._.output.push("\x3c!--",a,
+"--\x3e")},write:function(a){this._.output.push(a)},reset:function(){this._.output=[];this._.indent=!1},getHtml:function(a){var d=this._.output.join("");a&&this.reset();return d}}});"use strict";
+(function(){CKEDITOR.htmlParser.node=function(){};CKEDITOR.htmlParser.node.prototype={remove:function(){var a=this.parent.children,d=CKEDITOR.tools.indexOf(a,this),b=this.previous,c=this.next;b&&(b.next=c);c&&(c.previous=b);a.splice(d,1);this.parent=null},replaceWith:function(a){var d=this.parent.children,b=CKEDITOR.tools.indexOf(d,this),c=a.previous=this.previous,e=a.next=this.next;c&&(c.next=a);e&&(e.previous=a);d[b]=a;a.parent=this.parent;this.parent=null},insertAfter:function(a){var d=a.parent.children,
+b=CKEDITOR.tools.indexOf(d,a),c=a.next;d.splice(b+1,0,this);this.next=a.next;this.previous=a;a.next=this;c&&(c.previous=this);this.parent=a.parent},insertBefore:function(a){var d=a.parent.children,b=CKEDITOR.tools.indexOf(d,a);d.splice(b,0,this);this.next=a;(this.previous=a.previous)&&(a.previous.next=this);a.previous=this;this.parent=a.parent},getAscendant:function(a){var d="function"==typeof a?a:"string"==typeof a?function(b){return b.name==a}:function(b){return b.name in a},b=this.parent;for(;b&&
+b.type==CKEDITOR.NODE_ELEMENT;){if(d(b))return b;b=b.parent}return null},wrapWith:function(a){this.replaceWith(a);a.add(this);return a},getIndex:function(){return CKEDITOR.tools.indexOf(this.parent.children,this)},getFilterContext:function(a){return a||{}}}})();"use strict";CKEDITOR.htmlParser.comment=function(a){this.value=a;this._={isBlockLike:!1}};
+CKEDITOR.htmlParser.comment.prototype=CKEDITOR.tools.extend(new CKEDITOR.htmlParser.node,{type:CKEDITOR.NODE_COMMENT,filter:function(a,d){var b=this.value;if(!(b=a.onComment(d,b,this)))return this.remove(),!1;if("string"!=typeof b)return this.replaceWith(b),!1;this.value=b;return!0},writeHtml:function(a,d){d&&this.filter(d);a.comment(this.value)}});"use strict";
+(function(){CKEDITOR.htmlParser.text=function(a){this.value=a;this._={isBlockLike:!1}};CKEDITOR.htmlParser.text.prototype=CKEDITOR.tools.extend(new CKEDITOR.htmlParser.node,{type:CKEDITOR.NODE_TEXT,filter:function(a,d){if(!(this.value=a.onText(d,this.value,this)))return this.remove(),!1},writeHtml:function(a,d){d&&this.filter(d);a.text(this.value)}})})();"use strict";
+(function(){CKEDITOR.htmlParser.cdata=function(a){this.value=a};CKEDITOR.htmlParser.cdata.prototype=CKEDITOR.tools.extend(new CKEDITOR.htmlParser.node,{type:CKEDITOR.NODE_TEXT,filter:function(){},writeHtml:function(a){a.write(this.value)}})})();"use strict";CKEDITOR.htmlParser.fragment=function(){this.children=[];this.parent=null;this._={isBlockLike:!0,hasInlineStarted:!1}};
+(function(){function a(a){return a.attributes["data-cke-survive"]?!1:"a"==a.name&&a.attributes.href||CKEDITOR.dtd.$removeEmpty[a.name]}var d=CKEDITOR.tools.extend({table:1,ul:1,ol:1,dl:1},CKEDITOR.dtd.table,CKEDITOR.dtd.ul,CKEDITOR.dtd.ol,CKEDITOR.dtd.dl),b={ol:1,ul:1},c=CKEDITOR.tools.extend({},{html:1},CKEDITOR.dtd.html,CKEDITOR.dtd.body,CKEDITOR.dtd.head,{style:1,script:1}),e={ul:"li",ol:"li",dl:"dd",table:"tbody",tbody:"tr",thead:"tr",tfoot:"tr",tr:"td"};CKEDITOR.htmlParser.fragment.fromHtml=
+function(g,h,k){function n(a){var b;if(0<m.length)for(var c=0;c<m.length;c++){var d=m[c],e=d.name,f=CKEDITOR.dtd[e],l=z.name&&CKEDITOR.dtd[z.name];l&&!l[e]||a&&f&&!f[a]&&CKEDITOR.dtd[a]?e==z.name&&(D(z,z.parent,1),c--):(b||(u(),b=1),d=d.clone(),d.parent=z,z=d,m.splice(c,1),c--)}}function u(){for(;K.length;)D(K.shift(),z)}function f(a){if(a._.isBlockLike&&"pre"!=a.name&&"textarea"!=a.name){var b=a.children.length,c=a.children[b-1],d;c&&c.type==CKEDITOR.NODE_TEXT&&((d=CKEDITOR.tools.rtrim(c.value))?
+c.value=d:a.children.length=b-1)}}function D(b,c,d){c=c||z||x;var e=z;void 0===b.previous&&(w(c,b)&&(z=c,F.onTagOpen(k,{}),b.returnPoint=c=z),f(b),a(b)&&!b.children.length||c.add(b),"pre"==b.name&&(l=!1),"textarea"==b.name&&(I=!1));b.returnPoint?(z=b.returnPoint,delete b.returnPoint):z=d?c:e}function w(a,b){if((a==x||"body"==a.name)&&k&&(!a.name||CKEDITOR.dtd[a.name][k])){var c,d;return(c=b.attributes&&(d=b.attributes["data-cke-real-element-type"])?d:b.name)&&c in CKEDITOR.dtd.$inline&&!(c in CKEDITOR.dtd.head)&&
+!b.isOrphan||b.type==CKEDITOR.NODE_TEXT}}function A(a,b){return a in CKEDITOR.dtd.$listItem||a in CKEDITOR.dtd.$tableContent?a==b||"dt"==a&&"dd"==b||"dd"==a&&"dt"==b:!1}var F=new CKEDITOR.htmlParser,x=h instanceof CKEDITOR.htmlParser.element?h:"string"==typeof h?new CKEDITOR.htmlParser.element(h):new CKEDITOR.htmlParser.fragment,m=[],K=[],z=x,I="textarea"==x.name,l="pre"==x.name;F.onTagOpen=function(e,f,g,k){f=new CKEDITOR.htmlParser.element(e,f);f.isUnknown&&g&&(f.isEmpty=!0);f.isOptionalClose=k;
+if(a(f))m.push(f);else{if("pre"==e)l=!0;else{if("br"==e&&l){z.add(new CKEDITOR.htmlParser.text("\n"));return}"textarea"==e&&(I=!0)}if("br"==e)K.push(f);else{for(;!(k=(g=z.name)?CKEDITOR.dtd[g]||(z._.isBlockLike?CKEDITOR.dtd.div:CKEDITOR.dtd.span):c,f.isUnknown||z.isUnknown||k[e]);)if(z.isOptionalClose)F.onTagClose(g);else if(e in b&&g in b)g=z.children,(g=g[g.length-1])&&"li"==g.name||D(g=new CKEDITOR.htmlParser.element("li"),z),!f.returnPoint&&(f.returnPoint=z),z=g;else if(e in CKEDITOR.dtd.$listItem&&
+!A(e,g))F.onTagOpen("li"==e?"ul":"dl",{},0,1);else if(g in d&&!A(e,g))!f.returnPoint&&(f.returnPoint=z),z=z.parent;else if(g in CKEDITOR.dtd.$inline&&m.unshift(z),z.parent)D(z,z.parent,1);else{f.isOrphan=1;break}n(e);u();f.parent=z;f.isEmpty?D(f):z=f}}};F.onTagClose=function(a){for(var b=m.length-1;0<=b;b--)if(a==m[b].name){m.splice(b,1);return}for(var c=[],d=[],e=z;e!=x&&e.name!=a;)e._.isBlockLike||d.unshift(e),c.push(e),e=e.returnPoint||e.parent;if(e!=x){for(b=0;b<c.length;b++){var f=c[b];D(f,f.parent)}z=
+e;e._.isBlockLike&&u();D(e,e.parent);e==z&&(z=z.parent);m=m.concat(d)}"body"==a&&(k=!1)};F.onText=function(a){if(!(z._.hasInlineStarted&&!K.length||l||I)&&(a=CKEDITOR.tools.ltrim(a),0===a.length))return;var b=z.name,f=b?CKEDITOR.dtd[b]||(z._.isBlockLike?CKEDITOR.dtd.div:CKEDITOR.dtd.span):c;if(!I&&!f["#"]&&b in d)F.onTagOpen(e[b]||""),F.onText(a);else{u();n();l||I||(a=a.replace(/[\t\r\n ]{2,}|[\t\r\n]/g," "));a=new CKEDITOR.htmlParser.text(a);if(w(z,a))this.onTagOpen(k,{},0,1);z.add(a)}};F.onCDATA=
+function(a){z.add(new CKEDITOR.htmlParser.cdata(a))};F.onComment=function(a){u();n();z.add(new CKEDITOR.htmlParser.comment(a))};F.parse(g);for(u();z!=x;)D(z,z.parent,1);f(x);return x};CKEDITOR.htmlParser.fragment.prototype={type:CKEDITOR.NODE_DOCUMENT_FRAGMENT,add:function(a,b){isNaN(b)&&(b=this.children.length);var c=0<b?this.children[b-1]:null;if(c){if(a._.isBlockLike&&c.type==CKEDITOR.NODE_TEXT&&(c.value=CKEDITOR.tools.rtrim(c.value),0===c.value.length)){this.children.pop();this.add(a);return}c.next=
+a}a.previous=c;a.parent=this;this.children.splice(b,0,a);this._.hasInlineStarted||(this._.hasInlineStarted=a.type==CKEDITOR.NODE_TEXT||a.type==CKEDITOR.NODE_ELEMENT&&!a._.isBlockLike)},filter:function(a,b){b=this.getFilterContext(b);a.onRoot(b,this);this.filterChildren(a,!1,b)},filterChildren:function(a,b,c){if(this.childrenFilteredBy!=a.id){c=this.getFilterContext(c);if(b&&!this.parent)a.onRoot(c,this);this.childrenFilteredBy=a.id;for(b=0;b<this.children.length;b++)!1===this.children[b].filter(a,
+c)&&b--}},writeHtml:function(a,b){b&&this.filter(b);this.writeChildrenHtml(a)},writeChildrenHtml:function(a,b,c){var d=this.getFilterContext();if(c&&!this.parent&&b)b.onRoot(d,this);b&&this.filterChildren(b,!1,d);b=0;c=this.children;for(d=c.length;b<d;b++)c[b].writeHtml(a)},forEach:function(a,b,c){if(!(c||b&&this.type!=b))var d=a(this);if(!1!==d){c=this.children;for(var e=0;e<c.length;e++)d=c[e],d.type==CKEDITOR.NODE_ELEMENT?d.forEach(a,b):b&&d.type!=b||a(d)}},getFilterContext:function(a){return a||
 {}}}})();"use strict";
-(function(){function a(){this.rules=[]}function e(b,c,d,e){var g,n;for(g in c){(n=b[g])||(n=b[g]=new a);n.add(c[g],d,e)}}CKEDITOR.htmlParser.filter=CKEDITOR.tools.createClass({$:function(b){this.id=CKEDITOR.tools.getNextNumber();this.elementNameRules=new a;this.attributeNameRules=new a;this.elementsRules={};this.attributesRules={};this.textRules=new a;this.commentRules=new a;this.rootRules=new a;b&&this.addRules(b,10)},proto:{addRules:function(a,c){var d;if(typeof c=="number")d=c;else if(c&&"priority"in
-c)d=c.priority;typeof d!="number"&&(d=10);typeof c!="object"&&(c={});a.elementNames&&this.elementNameRules.addMany(a.elementNames,d,c);a.attributeNames&&this.attributeNameRules.addMany(a.attributeNames,d,c);a.elements&&e(this.elementsRules,a.elements,d,c);a.attributes&&e(this.attributesRules,a.attributes,d,c);a.text&&this.textRules.add(a.text,d,c);a.comment&&this.commentRules.add(a.comment,d,c);a.root&&this.rootRules.add(a.root,d,c)},applyTo:function(a){a.filter(this)},onElementName:function(a,c){return this.elementNameRules.execOnName(a,
-c)},onAttributeName:function(a,c){return this.attributeNameRules.execOnName(a,c)},onText:function(a,c){return this.textRules.exec(a,c)},onComment:function(a,c,d){return this.commentRules.exec(a,c,d)},onRoot:function(a,c){return this.rootRules.exec(a,c)},onElement:function(a,c){for(var d=[this.elementsRules["^"],this.elementsRules[c.name],this.elementsRules.$],e,g=0;g<3;g++)if(e=d[g]){e=e.exec(a,c,this);if(e===false)return null;if(e&&e!=c)return this.onNode(a,e);if(c.parent&&!c.name)break}return c},
-onNode:function(a,c){var d=c.type;return d==CKEDITOR.NODE_ELEMENT?this.onElement(a,c):d==CKEDITOR.NODE_TEXT?new CKEDITOR.htmlParser.text(this.onText(a,c.value)):d==CKEDITOR.NODE_COMMENT?new CKEDITOR.htmlParser.comment(this.onComment(a,c.value)):null},onAttribute:function(a,c,d,e){return(d=this.attributesRules[d])?d.exec(a,e,c,this):e}}});CKEDITOR.htmlParser.filterRulesGroup=a;a.prototype={add:function(a,c,d){this.rules.splice(this.findIndex(c),0,{value:a,priority:c,options:d})},addMany:function(a,
-c,d){for(var e=[this.findIndex(c),0],g=0,n=a.length;g<n;g++)e.push({value:a[g],priority:c,options:d});this.rules.splice.apply(this.rules,e)},findIndex:function(a){for(var c=this.rules,d=c.length-1;d>=0&&a<c[d].priority;)d--;return d+1},exec:function(a,c){var d=c instanceof CKEDITOR.htmlParser.node||c instanceof CKEDITOR.htmlParser.fragment,e=Array.prototype.slice.call(arguments,1),g=this.rules,n=g.length,i,j,o,q;for(q=0;q<n;q++){if(d){i=c.type;j=c.name}o=g[q];if(!a.nonEditable||o.options.applyToAll){o=
-o.value.apply(null,e);if(o===false||d&&o&&(o.name!=j||o.type!=i))return o;o!=void 0&&(e[0]=c=o)}}return c},execOnName:function(a,c){for(var d=0,e=this.rules,g=e.length,n;c&&d<g;d++){n=e[d];if(!a.nonEditable||n.options.applyToAll)c=c.replace(n.value[0],n.value[1])}return c}}})();
-(function(){function a(a,e){function f(a){return a||CKEDITOR.env.needsNbspFiller?new CKEDITOR.htmlParser.text(" "):new CKEDITOR.htmlParser.element("br",{"data-cke-bogus":1})}function l(a,d){return function(e){if(e.type!=CKEDITOR.NODE_DOCUMENT_FRAGMENT){var k=[],l=b(e),g,C;if(l)for(m(l,1)&&k.push(l);l;){if(h(l)&&(g=c(l))&&m(g))if((C=c(g))&&!h(C))k.push(g);else{f(v).insertAfter(g);g.remove()}l=l.previous}for(l=0;l<k.length;l++)k[l].remove();if(k=CKEDITOR.env.opera&&!a||(typeof d=="function"?d(e)!==
-false:d))if(!v&&!CKEDITOR.env.needsBrFiller&&e.type==CKEDITOR.NODE_DOCUMENT_FRAGMENT)k=false;else if(!v&&!CKEDITOR.env.needsBrFiller&&(document.documentMode>7||e.name in CKEDITOR.dtd.tr||e.name in CKEDITOR.dtd.$listItem))k=false;else{k=b(e);k=!k||e.name=="form"&&k.name=="input"}k&&e.add(f(a))}}}function m(a,b){if((!v||CKEDITOR.env.needsBrFiller)&&a.type==CKEDITOR.NODE_ELEMENT&&a.name=="br"&&!a.attributes["data-cke-eol"])return true;var c;if(a.type==CKEDITOR.NODE_TEXT&&(c=a.value.match(y))){if(c.index){(new CKEDITOR.htmlParser.text(a.value.substring(0,
-c.index))).insertBefore(a);a.value=c[0]}if(!CKEDITOR.env.needsBrFiller&&v&&(!b||a.parent.name in i))return true;if(!v)if((c=a.previous)&&c.name=="br"||!c||h(c))return true}return false}var r={elements:{}},v=e=="html",i=CKEDITOR.tools.extend({},t),p;for(p in i)"#"in k[p]||delete i[p];for(p in i)r.elements[p]=l(v,a.config.fillEmptyBlocks!==false);r.root=l(v);r.elements.br=function(a){return function(b){if(b.parent.type!=CKEDITOR.NODE_DOCUMENT_FRAGMENT){var e=b.attributes;if("data-cke-bogus"in e||"data-cke-eol"in
-e)delete e["data-cke-bogus"];else{for(e=b.next;e&&d(e);)e=e.next;var k=c(b);!e&&h(b.parent)?g(b.parent,f(a)):h(e)&&(k&&!h(k))&&f(a).insertBefore(e)}}}}(v);return r}function e(a,b){return a!=CKEDITOR.ENTER_BR&&b!==false?a==CKEDITOR.ENTER_DIV?"div":"p":false}function b(a){for(a=a.children[a.children.length-1];a&&d(a);)a=a.previous;return a}function c(a){for(a=a.previous;a&&d(a);)a=a.previous;return a}function d(a){return a.type==CKEDITOR.NODE_TEXT&&!CKEDITOR.tools.trim(a.value)||a.type==CKEDITOR.NODE_ELEMENT&&
-a.attributes["data-cke-bookmark"]}function h(a){return a&&(a.type==CKEDITOR.NODE_ELEMENT&&a.name in t||a.type==CKEDITOR.NODE_DOCUMENT_FRAGMENT)}function g(a,b){var c=a.children[a.children.length-1];a.children.push(b);b.parent=a;if(c){c.next=b;b.previous=c}}function n(a){a=a.attributes;a.contenteditable!="false"&&(a["data-cke-editable"]=a.contenteditable?"true":1);a.contenteditable="false"}function i(a){a=a.attributes;switch(a["data-cke-editable"]){case "true":a.contenteditable="true";break;case "1":delete a.contenteditable}}
-function j(a){return a.replace(I,function(a,b,c){return"<"+b+c.replace(E,function(a,b){if(!/^on/.test(b)&&c.indexOf("data-cke-saved-"+b)==-1){a=a.slice(1);return" data-cke-saved-"+a+" data-cke-"+CKEDITOR.rnd+"-"+a}return a})+">"})}function o(a,b){return a.replace(b,function(a,b,c){a.indexOf("<textarea")===0&&(a=b+u(c).replace(/</g,"&lt;").replace(/>/g,"&gt;")+"</textarea>");return"<cke:encoded>"+encodeURIComponent(a)+"</cke:encoded>"})}function q(a){return a.replace(Q,function(a,b){return decodeURIComponent(b)})}
-function s(a){return a.replace(/<\!--(?!{cke_protected})[\s\S]+?--\>/g,function(a){return"<\!--"+m+"{C}"+encodeURIComponent(a).replace(/--/g,"%2D%2D")+"--\>"})}function u(a){return a.replace(/<\!--\{cke_protected\}\{C\}([\s\S]+?)--\>/g,function(a,b){return decodeURIComponent(b)})}function f(a,b){var c=b._.dataStore;return a.replace(/<\!--\{cke_protected\}([\s\S]+?)--\>/g,function(a,b){return decodeURIComponent(b)}).replace(/\{cke_protected_(\d+)\}/g,function(a,b){return c&&c[b]||""})}function p(a,
-b){for(var c=[],d=b.config.protectedSource,e=b._.dataStore||(b._.dataStore={id:1}),f=/<\!--\{cke_temp(comment)?\}(\d*?)--\>/g,d=[/<script[\s\S]*?<\/script>/gi,/<noscript[\s\S]*?<\/noscript>/gi].concat(d),a=a.replace(/<\!--[\s\S]*?--\>/g,function(a){return"<\!--{cke_tempcomment}"+(c.push(a)-1)+"--\>"}),k=0;k<d.length;k++)a=a.replace(d[k],function(a){a=a.replace(f,function(a,b,d){return c[d]});return/cke_temp(comment)?/.test(a)?a:"<\!--{cke_temp}"+(c.push(a)-1)+"--\>"});a=a.replace(f,function(a,b,d){return"<\!--"+
-m+(b?"{C}":"")+encodeURIComponent(c[d]).replace(/--/g,"%2D%2D")+"--\>"});return a.replace(/(['"]).*?\1/g,function(a){return a.replace(/<\!--\{cke_protected\}([\s\S]+?)--\>/g,function(a,b){e[e.id]=decodeURIComponent(b);return"{cke_protected_"+e.id++ +"}"})})}CKEDITOR.htmlDataProcessor=function(b){var c,d,k=this;this.editor=b;this.dataFilter=c=new CKEDITOR.htmlParser.filter;this.htmlFilter=d=new CKEDITOR.htmlParser.filter;this.writer=new CKEDITOR.htmlParser.basicWriter;c.addRules(x);c.addRules(r,{applyToAll:true});
-c.addRules(a(b,"data"),{applyToAll:true});d.addRules(L);d.addRules(A,{applyToAll:true});d.addRules(a(b,"html"),{applyToAll:true});b.on("toHtml",function(a){var a=a.data,c=a.dataValue,c=p(c,b),c=o(c,M),c=j(c),c=o(c,z),c=c.replace(v,"$1cke:$2"),c=c.replace(H,"<cke:$1$2></cke:$1>"),c=CKEDITOR.env.opera?c:c.replace(/(<pre\b[^>]*>)(\r\n|\n)/g,"$1$2$2"),d=a.context||b.editable().getName(),f;if(CKEDITOR.env.ie&&CKEDITOR.env.version<9&&d=="pre"){d="div";c="<pre>"+c+"</pre>";f=1}d=b.document.createElement(d);
-d.setHtml("a"+c);c=d.getHtml().substr(1);c=c.replace(RegExp(" data-cke-"+CKEDITOR.rnd+"-","ig")," ");f&&(c=c.replace(/^<pre>|<\/pre>$/gi,""));c=c.replace(w,"$1$2");c=q(c);c=u(c);a.dataValue=CKEDITOR.htmlParser.fragment.fromHtml(c,a.context,a.fixForBody===false?false:e(a.enterMode,b.config.autoParagraph))},null,null,5);b.on("toHtml",function(a){a.data.filter.applyTo(a.data.dataValue,true,a.data.dontFilter,a.data.enterMode)&&b.fire("dataFiltered")},null,null,6);b.on("toHtml",function(a){a.data.dataValue.filterChildren(k.dataFilter,
-true)},null,null,10);b.on("toHtml",function(a){var a=a.data,b=a.dataValue,c=new CKEDITOR.htmlParser.basicWriter;b.writeChildrenHtml(c);b=c.getHtml(true);a.dataValue=s(b)},null,null,15);b.on("toDataFormat",function(a){var c=a.data.dataValue;a.data.enterMode!=CKEDITOR.ENTER_BR&&(c=c.replace(/^<br *\/?>/i,""));a.data.dataValue=CKEDITOR.htmlParser.fragment.fromHtml(c,a.data.context,e(a.data.enterMode,b.config.autoParagraph))},null,null,5);b.on("toDataFormat",function(a){a.data.dataValue.filterChildren(k.htmlFilter,
-true)},null,null,10);b.on("toDataFormat",function(a){a.data.filter.applyTo(a.data.dataValue,false,true)},null,null,11);b.on("toDataFormat",function(a){var c=a.data.dataValue,d=k.writer;d.reset();c.writeChildrenHtml(d);c=d.getHtml(true);c=u(c);c=f(c,b);a.data.dataValue=c},null,null,15)};CKEDITOR.htmlDataProcessor.prototype={toHtml:function(a,b,c,d){var e=this.editor,f,k,l;if(b&&typeof b=="object"){f=b.context;c=b.fixForBody;d=b.dontFilter;k=b.filter;l=b.enterMode}else f=b;!f&&f!==null&&(f=e.editable().getName());
-return e.fire("toHtml",{dataValue:a,context:f,fixForBody:c,dontFilter:d,filter:k||e.filter,enterMode:l||e.enterMode}).dataValue},toDataFormat:function(a,b){var c,d,e;if(b){c=b.context;d=b.filter;e=b.enterMode}!c&&c!==null&&(c=this.editor.editable().getName());return this.editor.fire("toDataFormat",{dataValue:a,filter:d||this.editor.filter,context:c,enterMode:e||this.editor.enterMode}).dataValue}};var y=/(?:&nbsp;|\xa0)$/,m="{cke_protected}",k=CKEDITOR.dtd,l=["caption","colgroup","col","thead","tfoot",
-"tbody"],t=CKEDITOR.tools.extend({},k.$blockLimit,k.$block),x={elements:{input:n,textarea:n}},r={attributeNames:[[/^on/,"data-cke-pa-on"],[/^data-cke-expando$/,""]]},L={elements:{embed:function(a){var b=a.parent;if(b&&b.name=="object"){var c=b.attributes.width,b=b.attributes.height;if(c)a.attributes.width=c;if(b)a.attributes.height=b}},a:function(a){if(!a.children.length&&!a.attributes.name&&!a.attributes["data-cke-saved-name"])return false}}},A={elementNames:[[/^cke:/,""],[/^\?xml:namespace$/,""]],
-attributeNames:[[/^data-cke-(saved|pa)-/,""],[/^data-cke-.*/,""],["hidefocus",""]],elements:{$:function(a){var b=a.attributes;if(b){if(b["data-cke-temp"])return false;for(var c=["name","href","src"],d,e=0;e<c.length;e++){d="data-cke-saved-"+c[e];d in b&&delete b[c[e]]}}return a},table:function(a){a.children.slice(0).sort(function(a,b){var c,d;if(a.type==CKEDITOR.NODE_ELEMENT&&b.type==a.type){c=CKEDITOR.tools.indexOf(l,a.name);d=CKEDITOR.tools.indexOf(l,b.name)}if(!(c>-1&&d>-1&&c!=d)){c=a.parent?a.getIndex():
--1;d=b.parent?b.getIndex():-1}return c>d?1:-1})},param:function(a){a.children=[];a.isEmpty=true;return a},span:function(a){a.attributes["class"]=="Apple-style-span"&&delete a.name},html:function(a){delete a.attributes.contenteditable;delete a.attributes["class"]},body:function(a){delete a.attributes.spellcheck;delete a.attributes.contenteditable},style:function(a){var b=a.children[0];if(b&&b.value)b.value=CKEDITOR.tools.trim(b.value);if(!a.attributes.type)a.attributes.type="text/css"},title:function(a){var b=
-a.children[0];!b&&g(a,b=new CKEDITOR.htmlParser.text);b.value=a.attributes["data-cke-title"]||""},input:i,textarea:i},attributes:{"class":function(a){return CKEDITOR.tools.ltrim(a.replace(/(?:^|\s+)cke_[^\s]*/g,""))||false}}};if(CKEDITOR.env.ie)A.attributes.style=function(a){return a.replace(/(^|;)([^\:]+)/g,function(a){return a.toLowerCase()})};var I=/<(a|area|img|input|source)\b([^>]*)>/gi,E=/\s(on\w+|href|src|name)\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|(?:[^ "'>]+))/gi,z=/(?:<style(?=[ >])[^>]*>[\s\S]*?<\/style>)|(?:<(:?link|meta|base)[^>]*>)/gi,
-M=/(<textarea(?=[ >])[^>]*>)([\s\S]*?)(?:<\/textarea>)/gi,Q=/<cke:encoded>([^<]*)<\/cke:encoded>/gi,v=/(<\/?)((?:object|embed|param|html|body|head|title)[^>]*>)/gi,w=/(<\/?)cke:((?:html|body|head|title)[^>]*>)/gi,H=/<cke:(param|embed)([^>]*?)\/?>(?!\s*<\/cke:\1)/gi})();"use strict";
-CKEDITOR.htmlParser.element=function(a,e){this.name=a;this.attributes=e||{};this.children=[];var b=a||"",c=b.match(/^cke:(.*)/);c&&(b=c[1]);b=!(!CKEDITOR.dtd.$nonBodyContent[b]&&!CKEDITOR.dtd.$block[b]&&!CKEDITOR.dtd.$listItem[b]&&!CKEDITOR.dtd.$tableContent[b]&&!(CKEDITOR.dtd.$nonEditable[b]||b=="br"));this.isEmpty=!!CKEDITOR.dtd.$empty[a];this.isUnknown=!CKEDITOR.dtd[a];this._={isBlockLike:b,hasInlineStarted:this.isEmpty||!b}};
-CKEDITOR.htmlParser.cssStyle=function(a){var e={};((a instanceof CKEDITOR.htmlParser.element?a.attributes.style:a)||"").replace(/&quot;/g,'"').replace(/\s*([^ :;]+)\s*:\s*([^;]+)\s*(?=;|$)/g,function(a,c,d){c=="font-family"&&(d=d.replace(/["']/g,""));e[c.toLowerCase()]=d});return{rules:e,populate:function(a){var c=this.toString();if(c)a instanceof CKEDITOR.dom.element?a.setAttribute("style",c):a instanceof CKEDITOR.htmlParser.element?a.attributes.style=c:a.style=c},toString:function(){var a=[],c;
-for(c in e)e[c]&&a.push(c,":",e[c],";");return a.join("")}}};
-(function(){function a(a){return function(b){return b.type==CKEDITOR.NODE_ELEMENT&&(typeof a=="string"?b.name==a:b.name in a)}}var e=function(a,b){a=a[0];b=b[0];return a<b?-1:a>b?1:0},b=CKEDITOR.htmlParser.fragment.prototype;CKEDITOR.htmlParser.element.prototype=CKEDITOR.tools.extend(new CKEDITOR.htmlParser.node,{type:CKEDITOR.NODE_ELEMENT,add:b.add,clone:function(){return new CKEDITOR.htmlParser.element(this.name,this.attributes)},filter:function(a,b){var e=this,g,n,b=e.getFilterContext(b);if(b.off)return true;
-if(!e.parent)a.onRoot(b,e);for(;;){g=e.name;if(!(n=a.onElementName(b,g))){this.remove();return false}e.name=n;if(!(e=a.onElement(b,e))){this.remove();return false}if(e!==this){this.replaceWith(e);return false}if(e.name==g)break;if(e.type!=CKEDITOR.NODE_ELEMENT){this.replaceWith(e);return false}if(!e.name){this.replaceWithChildren();return false}}g=e.attributes;var i,j;for(i in g){j=i;for(n=g[i];;)if(j=a.onAttributeName(b,i))if(j!=i){delete g[i];i=j}else break;else{delete g[i];break}j&&((n=a.onAttribute(b,
-e,j,n))===false?delete g[j]:g[j]=n)}e.isEmpty||this.filterChildren(a,false,b);return true},filterChildren:b.filterChildren,writeHtml:function(a,b){b&&this.filter(b);var h=this.name,g=[],n=this.attributes,i,j;a.openTag(h,n);for(i in n)g.push([i,n[i]]);a.sortAttributes&&g.sort(e);i=0;for(j=g.length;i<j;i++){n=g[i];a.attribute(n[0],n[1])}a.openTagClose(h,this.isEmpty);this.writeChildrenHtml(a);this.isEmpty||a.closeTag(h)},writeChildrenHtml:b.writeChildrenHtml,replaceWithChildren:function(){for(var a=
-this.children,b=a.length;b;)a[--b].insertAfter(this);this.remove()},forEach:b.forEach,getFirst:function(b){if(!b)return this.children.length?this.children[0]:null;typeof b!="function"&&(b=a(b));for(var d=0,e=this.children.length;d<e;++d)if(b(this.children[d]))return this.children[d];return null},getHtml:function(){var a=new CKEDITOR.htmlParser.basicWriter;this.writeChildrenHtml(a);return a.getHtml()},setHtml:function(a){for(var a=this.children=CKEDITOR.htmlParser.fragment.fromHtml(a).children,b=0,
-e=a.length;b<e;++b)a[b].parent=this},getOuterHtml:function(){var a=new CKEDITOR.htmlParser.basicWriter;this.writeHtml(a);return a.getHtml()},split:function(a){for(var b=this.children.splice(a,this.children.length-a),e=this.clone(),g=0;g<b.length;++g)b[g].parent=e;e.children=b;if(b[0])b[0].previous=null;if(a>0)this.children[a-1].next=null;this.parent.add(e,this.getIndex()+1);return e},removeClass:function(a){var b=this.attributes["class"];if(b)(b=CKEDITOR.tools.trim(b.replace(RegExp("(?:\\s+|^)"+a+
-"(?:\\s+|$)")," ")))?this.attributes["class"]=b:delete this.attributes["class"]},hasClass:function(a){var b=this.attributes["class"];return!b?false:RegExp("(?:^|\\s)"+a+"(?=\\s|$)").test(b)},getFilterContext:function(a){var b=[];a||(a={off:false,nonEditable:false});!a.off&&this.attributes["data-cke-processor"]=="off"&&b.push("off",true);!a.nonEditable&&this.attributes.contenteditable=="false"&&b.push("nonEditable",true);if(b.length)for(var a=CKEDITOR.tools.copy(a),e=0;e<b.length;e=e+2)a[b[e]]=b[e+
-1];return a}},true)})();(function(){var a={};CKEDITOR.template=function(e){if(a[e])this.output=a[e];else{var b=e.replace(/'/g,"\\'").replace(/{([^}]+)}/g,function(a,b){return"',data['"+b+"']==undefined?'{"+b+"}':data['"+b+"'],'"});this.output=a[e]=Function("data","buffer","return buffer?buffer.push('"+b+"'):['"+b+"'].join('');")}}})();delete CKEDITOR.loadFullCore;CKEDITOR.instances={};CKEDITOR.document=new CKEDITOR.dom.document(document);
-CKEDITOR.add=function(a){CKEDITOR.instances[a.name]=a;a.on("focus",function(){if(CKEDITOR.currentInstance!=a){CKEDITOR.currentInstance=a;CKEDITOR.fire("currentInstance")}});a.on("blur",function(){if(CKEDITOR.currentInstance==a){CKEDITOR.currentInstance=null;CKEDITOR.fire("currentInstance")}});CKEDITOR.fire("instance",null,a)};CKEDITOR.remove=function(a){delete CKEDITOR.instances[a.name]};
-(function(){var a={};CKEDITOR.addTemplate=function(e,b){var c=a[e];if(c)return c;c={name:e,source:b};CKEDITOR.fire("template",c);return a[e]=new CKEDITOR.template(c.source)};CKEDITOR.getTemplate=function(e){return a[e]}})();(function(){var a=[];CKEDITOR.addCss=function(e){a.push(e)};CKEDITOR.getCss=function(){return a.join("\n")}})();CKEDITOR.on("instanceDestroyed",function(){CKEDITOR.tools.isEmpty(this.instances)&&CKEDITOR.fire("reset")});CKEDITOR.TRISTATE_ON=1;CKEDITOR.TRISTATE_OFF=2;
+(function(){function a(){this.rules=[]}function d(b,c,d,g){var h,k;for(h in c)(k=b[h])||(k=b[h]=new a),k.add(c[h],d,g)}CKEDITOR.htmlParser.filter=CKEDITOR.tools.createClass({$:function(b){this.id=CKEDITOR.tools.getNextNumber();this.elementNameRules=new a;this.attributeNameRules=new a;this.elementsRules={};this.attributesRules={};this.textRules=new a;this.commentRules=new a;this.rootRules=new a;b&&this.addRules(b,10)},proto:{addRules:function(a,c){var e;"number"==typeof c?e=c:c&&"priority"in c&&(e=
+c.priority);"number"!=typeof e&&(e=10);"object"!=typeof c&&(c={});a.elementNames&&this.elementNameRules.addMany(a.elementNames,e,c);a.attributeNames&&this.attributeNameRules.addMany(a.attributeNames,e,c);a.elements&&d(this.elementsRules,a.elements,e,c);a.attributes&&d(this.attributesRules,a.attributes,e,c);a.text&&this.textRules.add(a.text,e,c);a.comment&&this.commentRules.add(a.comment,e,c);a.root&&this.rootRules.add(a.root,e,c)},applyTo:function(a){a.filter(this)},onElementName:function(a,c){return this.elementNameRules.execOnName(a,
+c)},onAttributeName:function(a,c){return this.attributeNameRules.execOnName(a,c)},onText:function(a,c,d){return this.textRules.exec(a,c,d)},onComment:function(a,c,d){return this.commentRules.exec(a,c,d)},onRoot:function(a,c){return this.rootRules.exec(a,c)},onElement:function(a,c){for(var d=[this.elementsRules["^"],this.elementsRules[c.name],this.elementsRules.$],g,h=0;3>h;h++)if(g=d[h]){g=g.exec(a,c,this);if(!1===g)return null;if(g&&g!=c)return this.onNode(a,g);if(c.parent&&!c.name)break}return c},
+onNode:function(a,c){var d=c.type;return d==CKEDITOR.NODE_ELEMENT?this.onElement(a,c):d==CKEDITOR.NODE_TEXT?new CKEDITOR.htmlParser.text(this.onText(a,c.value)):d==CKEDITOR.NODE_COMMENT?new CKEDITOR.htmlParser.comment(this.onComment(a,c.value)):null},onAttribute:function(a,c,d,g){return(d=this.attributesRules[d])?d.exec(a,g,c,this):g}}});CKEDITOR.htmlParser.filterRulesGroup=a;a.prototype={add:function(a,c,d){this.rules.splice(this.findIndex(c),0,{value:a,priority:c,options:d})},addMany:function(a,
+c,d){for(var g=[this.findIndex(c),0],h=0,k=a.length;h<k;h++)g.push({value:a[h],priority:c,options:d});this.rules.splice.apply(this.rules,g)},findIndex:function(a){for(var c=this.rules,d=c.length-1;0<=d&&a<c[d].priority;)d--;return d+1},exec:function(a,c){var d=c instanceof CKEDITOR.htmlParser.node||c instanceof CKEDITOR.htmlParser.fragment,g=Array.prototype.slice.call(arguments,1),h=this.rules,k=h.length,n,u,f,D;for(D=0;D<k;D++)if(d&&(n=c.type,u=c.name),f=h[D],!(a.nonEditable&&!f.options.applyToAll||
+a.nestedEditable&&f.options.excludeNestedEditable)){f=f.value.apply(null,g);if(!1===f||d&&f&&(f.name!=u||f.type!=n))return f;null!=f&&(g[0]=c=f)}return c},execOnName:function(a,c){for(var d=0,g=this.rules,h=g.length,k;c&&d<h;d++)k=g[d],a.nonEditable&&!k.options.applyToAll||a.nestedEditable&&k.options.excludeNestedEditable||(c=c.replace(k.value[0],k.value[1]));return c}}})();
+(function(){function a(a,d){function f(a){return a||CKEDITOR.env.needsNbspFiller?new CKEDITOR.htmlParser.text(" "):new CKEDITOR.htmlParser.element("br",{"data-cke-bogus":1})}function v(a,d){return function(e){if(e.type!=CKEDITOR.NODE_DOCUMENT_FRAGMENT){var l=[],r=b(e),v,p;if(r)for(q(r,1)&&l.push(r);r;)g(r)&&(v=c(r))&&q(v)&&((p=c(v))&&!g(p)?l.push(v):(f(y).insertAfter(v),v.remove())),r=r.previous;for(r=0;r<l.length;r++)l[r].remove();if(l=!a||!1!==("function"==typeof d?d(e):d))y||CKEDITOR.env.needsBrFiller||
+e.type!=CKEDITOR.NODE_DOCUMENT_FRAGMENT?y||CKEDITOR.env.needsBrFiller||!(7<document.documentMode||e.name in CKEDITOR.dtd.tr||e.name in CKEDITOR.dtd.$listItem)?(l=b(e),l=!l||"form"==e.name&&"input"==l.name):l=!1:l=!1;l&&e.add(f(a))}}}function q(a,b){if((!y||CKEDITOR.env.needsBrFiller)&&a.type==CKEDITOR.NODE_ELEMENT&&"br"==a.name&&!a.attributes["data-cke-eol"])return!0;var c;return a.type==CKEDITOR.NODE_TEXT&&(c=a.value.match(m))&&(c.index&&((new CKEDITOR.htmlParser.text(a.value.substring(0,c.index))).insertBefore(a),
+a.value=c[0]),!CKEDITOR.env.needsBrFiller&&y&&(!b||a.parent.name in B)||!y&&((c=a.previous)&&"br"==c.name||!c||g(c)))?!0:!1}var p={elements:{}},y="html"==d,B=CKEDITOR.tools.extend({},l),C;for(C in B)"#"in z[C]||delete B[C];for(C in B)p.elements[C]=v(y,a.config.fillEmptyBlocks);p.root=v(y,!1);p.elements.br=function(a){return function(b){if(b.parent.type!=CKEDITOR.NODE_DOCUMENT_FRAGMENT){var d=b.attributes;if("data-cke-bogus"in d||"data-cke-eol"in d)delete d["data-cke-bogus"];else{for(d=b.next;d&&e(d);)d=
+d.next;var l=c(b);!d&&g(b.parent)?h(b.parent,f(a)):g(d)&&l&&!g(l)&&f(a).insertBefore(d)}}}}(y);return p}function d(a,b){return a!=CKEDITOR.ENTER_BR&&!1!==b?a==CKEDITOR.ENTER_DIV?"div":"p":!1}function b(a){for(a=a.children[a.children.length-1];a&&e(a);)a=a.previous;return a}function c(a){for(a=a.previous;a&&e(a);)a=a.previous;return a}function e(a){return a.type==CKEDITOR.NODE_TEXT&&!CKEDITOR.tools.trim(a.value)||a.type==CKEDITOR.NODE_ELEMENT&&a.attributes["data-cke-bookmark"]}function g(a){return a&&
+(a.type==CKEDITOR.NODE_ELEMENT&&a.name in l||a.type==CKEDITOR.NODE_DOCUMENT_FRAGMENT)}function h(a,b){var c=a.children[a.children.length-1];a.children.push(b);b.parent=a;c&&(c.next=b,b.previous=c)}function k(a){a=a.attributes;"false"!=a.contenteditable&&(a["data-cke-editable"]=a.contenteditable?"true":1);a.contenteditable="false"}function n(a){a=a.attributes;switch(a["data-cke-editable"]){case "true":a.contenteditable="true";break;case "1":delete a.contenteditable}}function u(a){return a.replace(q,
+function(a,b,c){return"\x3c"+b+c.replace(y,function(a,b){return v.test(b)&&-1==c.indexOf("data-cke-saved-"+b)?" data-cke-saved-"+a+" data-cke-"+CKEDITOR.rnd+"-"+a:a})+"\x3e"})}function f(a,b){return a.replace(b,function(a,b,c){0===a.indexOf("\x3ctextarea")&&(a=b+A(c).replace(/</g,"\x26lt;").replace(/>/g,"\x26gt;")+"\x3c/textarea\x3e");return"\x3ccke:encoded\x3e"+encodeURIComponent(a)+"\x3c/cke:encoded\x3e"})}function D(a){return a.replace(W,function(a,b){return decodeURIComponent(b)})}function w(a){return a.replace(/\x3c!--(?!{cke_protected})[\s\S]+?--\x3e/g,
+function(a){return"\x3c!--"+K+"{C}"+encodeURIComponent(a).replace(/--/g,"%2D%2D")+"--\x3e"})}function A(a){return a.replace(/\x3c!--\{cke_protected\}\{C\}([\s\S]+?)--\x3e/g,function(a,b){return decodeURIComponent(b)})}function F(a,b){var c=b._.dataStore;return a.replace(/\x3c!--\{cke_protected\}([\s\S]+?)--\x3e/g,function(a,b){return decodeURIComponent(b)}).replace(/\{cke_protected_(\d+)\}/g,function(a,b){return c&&c[b]||""})}function x(a,b){var c=[],d=b.config.protectedSource,e=b._.dataStore||(b._.dataStore=
+{id:1}),f=/<\!--\{cke_temp(comment)?\}(\d*?)--\x3e/g,d=[/<script[\s\S]*?(<\/script>|$)/gi,/<noscript[\s\S]*?<\/noscript>/gi,/<meta[\s\S]*?\/?>/gi].concat(d);a=a.replace(/\x3c!--[\s\S]*?--\x3e/g,function(a){return"\x3c!--{cke_tempcomment}"+(c.push(a)-1)+"--\x3e"});for(var l=0;l<d.length;l++)a=a.replace(d[l],function(a){a=a.replace(f,function(a,b,d){return c[d]});return/cke_temp(comment)?/.test(a)?a:"\x3c!--{cke_temp}"+(c.push(a)-1)+"--\x3e"});a=a.replace(f,function(a,b,d){return"\x3c!--"+K+(b?"{C}":
+"")+encodeURIComponent(c[d]).replace(/--/g,"%2D%2D")+"--\x3e"});a=a.replace(/<\w+(?:\s+(?:(?:[^\s=>]+\s*=\s*(?:[^'"\s>]+|'[^']*'|"[^"]*"))|[^\s=\/>]+))+\s*\/?>/g,function(a){return a.replace(/\x3c!--\{cke_protected\}([^>]*)--\x3e/g,function(a,b){e[e.id]=decodeURIComponent(b);return"{cke_protected_"+e.id++ +"}"})});return a=a.replace(/<(title|iframe|textarea)([^>]*)>([\s\S]*?)<\/\1>/g,function(a,c,d,e){return"\x3c"+c+d+"\x3e"+F(A(e),b)+"\x3c/"+c+"\x3e"})}CKEDITOR.htmlDataProcessor=function(b){var c,
+e,l=this;this.editor=b;this.dataFilter=c=new CKEDITOR.htmlParser.filter;this.htmlFilter=e=new CKEDITOR.htmlParser.filter;this.writer=new CKEDITOR.htmlParser.basicWriter;c.addRules(t);c.addRules(J,{applyToAll:!0});c.addRules(a(b,"data"),{applyToAll:!0});e.addRules(H);e.addRules(E,{applyToAll:!0});e.addRules(a(b,"html"),{applyToAll:!0});b.on("toHtml",function(a){a=a.data;var c=a.dataValue,e,c=x(c,b),c=f(c,L),c=u(c),c=f(c,B),c=c.replace(O,"$1cke:$2"),c=c.replace(C,"\x3ccke:$1$2\x3e\x3c/cke:$1\x3e"),
+c=c.replace(/(<pre\b[^>]*>)(\r\n|\n)/g,"$1$2$2"),c=c.replace(/([^a-z0-9<\-])(on\w{3,})(?!>)/gi,"$1data-cke-"+CKEDITOR.rnd+"-$2");e=a.context||b.editable().getName();var l;CKEDITOR.env.ie&&9>CKEDITOR.env.version&&"pre"==e&&(e="div",c="\x3cpre\x3e"+c+"\x3c/pre\x3e",l=1);e=b.document.createElement(e);e.setHtml("a"+c);c=e.getHtml().substr(1);c=c.replace(new RegExp("data-cke-"+CKEDITOR.rnd+"-","ig"),"");l&&(c=c.replace(/^<pre>|<\/pre>$/gi,""));c=c.replace(p,"$1$2");c=D(c);c=A(c);e=!1===a.fixForBody?!1:
+d(a.enterMode,b.config.autoParagraph);c=CKEDITOR.htmlParser.fragment.fromHtml(c,a.context,e);e&&(l=c,!l.children.length&&CKEDITOR.dtd[l.name][e]&&(e=new CKEDITOR.htmlParser.element(e),l.add(e)));a.dataValue=c},null,null,5);b.on("toHtml",function(a){a.data.filter.applyTo(a.data.dataValue,!0,a.data.dontFilter,a.data.enterMode)&&b.fire("dataFiltered")},null,null,6);b.on("toHtml",function(a){a.data.dataValue.filterChildren(l.dataFilter,!0)},null,null,10);b.on("toHtml",function(a){a=a.data;var b=a.dataValue,
+c=new CKEDITOR.htmlParser.basicWriter;b.writeChildrenHtml(c);b=c.getHtml(!0);a.dataValue=w(b)},null,null,15);b.on("toDataFormat",function(a){var c=a.data.dataValue;a.data.enterMode!=CKEDITOR.ENTER_BR&&(c=c.replace(/^<br *\/?>/i,""));a.data.dataValue=CKEDITOR.htmlParser.fragment.fromHtml(c,a.data.context,d(a.data.enterMode,b.config.autoParagraph))},null,null,5);b.on("toDataFormat",function(a){a.data.dataValue.filterChildren(l.htmlFilter,!0)},null,null,10);b.on("toDataFormat",function(a){a.data.filter.applyTo(a.data.dataValue,
+!1,!0)},null,null,11);b.on("toDataFormat",function(a){var c=a.data.dataValue,d=l.writer;d.reset();c.writeChildrenHtml(d);c=d.getHtml(!0);c=A(c);c=F(c,b);a.data.dataValue=c},null,null,15)};CKEDITOR.htmlDataProcessor.prototype={toHtml:function(a,b,c,d){var e=this.editor,f,l,v,g;b&&"object"==typeof b?(f=b.context,c=b.fixForBody,d=b.dontFilter,l=b.filter,v=b.enterMode,g=b.protectedWhitespaces):f=b;f||null===f||(f=e.editable().getName());return e.fire("toHtml",{dataValue:a,context:f,fixForBody:c,dontFilter:d,
+filter:l||e.filter,enterMode:v||e.enterMode,protectedWhitespaces:g}).dataValue},toDataFormat:function(a,b){var c,d,e;b&&(c=b.context,d=b.filter,e=b.enterMode);c||null===c||(c=this.editor.editable().getName());return this.editor.fire("toDataFormat",{dataValue:a,filter:d||this.editor.filter,context:c,enterMode:e||this.editor.enterMode}).dataValue}};var m=/(?:&nbsp;|\xa0)$/,K="{cke_protected}",z=CKEDITOR.dtd,I="caption colgroup col thead tfoot tbody".split(" "),l=CKEDITOR.tools.extend({},z.$blockLimit,
+z.$block),t={elements:{input:k,textarea:k}},J={attributeNames:[[/^on/,"data-cke-pa-on"],[/^srcdoc/,"data-cke-pa-srcdoc"],[/^data-cke-expando$/,""]],elements:{iframe:function(a){if(a.attributes&&a.attributes.src){var b=a.attributes.src.toLowerCase().replace(/[^a-z]/gi,"");if(0===b.indexOf("javascript")||0===b.indexOf("data"))a.attributes["data-cke-pa-src"]=a.attributes.src,delete a.attributes.src}}}},H={elements:{embed:function(a){var b=a.parent;if(b&&"object"==b.name){var c=b.attributes.width,b=b.attributes.height;
+c&&(a.attributes.width=c);b&&(a.attributes.height=b)}},a:function(a){var b=a.attributes;if(!(a.children.length||b.name||b.id||a.attributes["data-cke-saved-name"]))return!1}}},E={elementNames:[[/^cke:/,""],[/^\?xml:namespace$/,""]],attributeNames:[[/^data-cke-(saved|pa)-/,""],[/^data-cke-.*/,""],["hidefocus",""]],elements:{$:function(a){var b=a.attributes;if(b){if(b["data-cke-temp"])return!1;for(var c=["name","href","src"],d,e=0;e<c.length;e++)d="data-cke-saved-"+c[e],d in b&&delete b[c[e]]}return a},
+table:function(a){a.children.slice(0).sort(function(a,b){var c,d;a.type==CKEDITOR.NODE_ELEMENT&&b.type==a.type&&(c=CKEDITOR.tools.indexOf(I,a.name),d=CKEDITOR.tools.indexOf(I,b.name));-1<c&&-1<d&&c!=d||(c=a.parent?a.getIndex():-1,d=b.parent?b.getIndex():-1);return c>d?1:-1})},param:function(a){a.children=[];a.isEmpty=!0;return a},span:function(a){"Apple-style-span"==a.attributes["class"]&&delete a.name},html:function(a){delete a.attributes.contenteditable;delete a.attributes["class"]},body:function(a){delete a.attributes.spellcheck;
+delete a.attributes.contenteditable},style:function(a){var b=a.children[0];b&&b.value&&(b.value=CKEDITOR.tools.trim(b.value));a.attributes.type||(a.attributes.type="text/css")},title:function(a){var b=a.children[0];!b&&h(a,b=new CKEDITOR.htmlParser.text);b.value=a.attributes["data-cke-title"]||""},input:n,textarea:n},attributes:{"class":function(a){return CKEDITOR.tools.ltrim(a.replace(/(?:^|\s+)cke_[^\s]*/g,""))||!1}}};CKEDITOR.env.ie&&(E.attributes.style=function(a){return a.replace(/(^|;)([^\:]+)/g,
+function(a){return a.toLowerCase()})});var q=/<(a|area|img|input|source)\b([^>]*)>/gi,y=/([\w-:]+)\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|(?:[^ "'>]+))/gi,v=/^(href|src|name)$/i,B=/(?:<style(?=[ >])[^>]*>[\s\S]*?<\/style>)|(?:<(:?link|meta|base)[^>]*>)/gi,L=/(<textarea(?=[ >])[^>]*>)([\s\S]*?)(?:<\/textarea>)/gi,W=/<cke:encoded>([^<]*)<\/cke:encoded>/gi,O=/(<\/?)((?:object|embed|param|html|body|head|title)[^>]*>)/gi,p=/(<\/?)cke:((?:html|body|head|title)[^>]*>)/gi,C=/<cke:(param|embed)([^>]*?)\/?>(?!\s*<\/cke:\1)/gi})();
+"use strict";CKEDITOR.htmlParser.element=function(a,d){this.name=a;this.attributes=d||{};this.children=[];var b=a||"",c=b.match(/^cke:(.*)/);c&&(b=c[1]);b=!!(CKEDITOR.dtd.$nonBodyContent[b]||CKEDITOR.dtd.$block[b]||CKEDITOR.dtd.$listItem[b]||CKEDITOR.dtd.$tableContent[b]||CKEDITOR.dtd.$nonEditable[b]||"br"==b);this.isEmpty=!!CKEDITOR.dtd.$empty[a];this.isUnknown=!CKEDITOR.dtd[a];this._={isBlockLike:b,hasInlineStarted:this.isEmpty||!b}};
+CKEDITOR.htmlParser.cssStyle=function(a){var d={};((a instanceof CKEDITOR.htmlParser.element?a.attributes.style:a)||"").replace(/&quot;/g,'"').replace(/\s*([^ :;]+)\s*:\s*([^;]+)\s*(?=;|$)/g,function(a,c,e){"font-family"==c&&(e=e.replace(/["']/g,""));d[c.toLowerCase()]=e});return{rules:d,populate:function(a){var c=this.toString();c&&(a instanceof CKEDITOR.dom.element?a.setAttribute("style",c):a instanceof CKEDITOR.htmlParser.element?a.attributes.style=c:a.style=c)},toString:function(){var a=[],c;
+for(c in d)d[c]&&a.push(c,":",d[c],";");return a.join("")}}};
+(function(){function a(a){return function(b){return b.type==CKEDITOR.NODE_ELEMENT&&("string"==typeof a?b.name==a:b.name in a)}}var d=function(a,b){a=a[0];b=b[0];return a<b?-1:a>b?1:0},b=CKEDITOR.htmlParser.fragment.prototype;CKEDITOR.htmlParser.element.prototype=CKEDITOR.tools.extend(new CKEDITOR.htmlParser.node,{type:CKEDITOR.NODE_ELEMENT,add:b.add,clone:function(){return new CKEDITOR.htmlParser.element(this.name,this.attributes)},filter:function(a,b){var d=this,h,k;b=d.getFilterContext(b);if(b.off)return!0;
+if(!d.parent)a.onRoot(b,d);for(;;){h=d.name;if(!(k=a.onElementName(b,h)))return this.remove(),!1;d.name=k;if(!(d=a.onElement(b,d)))return this.remove(),!1;if(d!==this)return this.replaceWith(d),!1;if(d.name==h)break;if(d.type!=CKEDITOR.NODE_ELEMENT)return this.replaceWith(d),!1;if(!d.name)return this.replaceWithChildren(),!1}h=d.attributes;var n,u;for(n in h){for(k=h[n];;)if(u=a.onAttributeName(b,n))if(u!=n)delete h[n],n=u;else break;else{delete h[n];break}u&&(!1===(k=a.onAttribute(b,d,u,k))?delete h[u]:
+h[u]=k)}d.isEmpty||this.filterChildren(a,!1,b);return!0},filterChildren:b.filterChildren,writeHtml:function(a,b){b&&this.filter(b);var g=this.name,h=[],k=this.attributes,n,u;a.openTag(g,k);for(n in k)h.push([n,k[n]]);a.sortAttributes&&h.sort(d);n=0;for(u=h.length;n<u;n++)k=h[n],a.attribute(k[0],k[1]);a.openTagClose(g,this.isEmpty);this.writeChildrenHtml(a);this.isEmpty||a.closeTag(g)},writeChildrenHtml:b.writeChildrenHtml,replaceWithChildren:function(){for(var a=this.children,b=a.length;b;)a[--b].insertAfter(this);
+this.remove()},forEach:b.forEach,getFirst:function(b){if(!b)return this.children.length?this.children[0]:null;"function"!=typeof b&&(b=a(b));for(var d=0,g=this.children.length;d<g;++d)if(b(this.children[d]))return this.children[d];return null},getHtml:function(){var a=new CKEDITOR.htmlParser.basicWriter;this.writeChildrenHtml(a);return a.getHtml()},setHtml:function(a){a=this.children=CKEDITOR.htmlParser.fragment.fromHtml(a).children;for(var b=0,d=a.length;b<d;++b)a[b].parent=this},getOuterHtml:function(){var a=
+new CKEDITOR.htmlParser.basicWriter;this.writeHtml(a);return a.getHtml()},split:function(a){for(var b=this.children.splice(a,this.children.length-a),d=this.clone(),h=0;h<b.length;++h)b[h].parent=d;d.children=b;b[0]&&(b[0].previous=null);0<a&&(this.children[a-1].next=null);this.parent.add(d,this.getIndex()+1);return d},find:function(a,b){void 0===b&&(b=!1);var d=[],h;for(h=0;h<this.children.length;h++){var k=this.children[h];"function"==typeof a&&a(k)?d.push(k):"string"==typeof a&&k.name===a&&d.push(k);
+b&&k.find&&(d=d.concat(k.find(a,b)))}return d},addClass:function(a){if(!this.hasClass(a)){var b=this.attributes["class"]||"";this.attributes["class"]=b+(b?" ":"")+a}},removeClass:function(a){var b=this.attributes["class"];b&&((b=CKEDITOR.tools.trim(b.replace(new RegExp("(?:\\s+|^)"+a+"(?:\\s+|$)")," ")))?this.attributes["class"]=b:delete this.attributes["class"])},hasClass:function(a){var b=this.attributes["class"];return b?(new RegExp("(?:^|\\s)"+a+"(?\x3d\\s|$)")).test(b):!1},getFilterContext:function(a){var b=
+[];a||(a={off:!1,nonEditable:!1,nestedEditable:!1});a.off||"off"!=this.attributes["data-cke-processor"]||b.push("off",!0);a.nonEditable||"false"!=this.attributes.contenteditable?a.nonEditable&&!a.nestedEditable&&"true"==this.attributes.contenteditable&&b.push("nestedEditable",!0):b.push("nonEditable",!0);if(b.length){a=CKEDITOR.tools.copy(a);for(var d=0;d<b.length;d+=2)a[b[d]]=b[d+1]}return a}},!0)})();
+(function(){var a=/{([^}]+)}/g;CKEDITOR.template=function(a){this.source=String(a)};CKEDITOR.template.prototype.output=function(d,b){var c=this.source.replace(a,function(a,b){return void 0!==d[b]?d[b]:a});return b?b.push(c):c}})();delete CKEDITOR.loadFullCore;CKEDITOR.instances={};CKEDITOR.document=new CKEDITOR.dom.document(document);
+CKEDITOR.add=function(a){CKEDITOR.instances[a.name]=a;a.on("focus",function(){CKEDITOR.currentInstance!=a&&(CKEDITOR.currentInstance=a,CKEDITOR.fire("currentInstance"))});a.on("blur",function(){CKEDITOR.currentInstance==a&&(CKEDITOR.currentInstance=null,CKEDITOR.fire("currentInstance"))});CKEDITOR.fire("instance",null,a)};CKEDITOR.remove=function(a){delete CKEDITOR.instances[a.name]};
+(function(){var a={};CKEDITOR.addTemplate=function(d,b){var c=a[d];if(c)return c;c={name:d,source:b};CKEDITOR.fire("template",c);return a[d]=new CKEDITOR.template(c.source)};CKEDITOR.getTemplate=function(d){return a[d]}})();(function(){var a=[];CKEDITOR.addCss=function(d){a.push(d)};CKEDITOR.getCss=function(){return a.join("\n")}})();CKEDITOR.on("instanceDestroyed",function(){CKEDITOR.tools.isEmpty(this.instances)&&CKEDITOR.fire("reset")});CKEDITOR.TRISTATE_ON=1;CKEDITOR.TRISTATE_OFF=2;
 CKEDITOR.TRISTATE_DISABLED=0;
-(function(){CKEDITOR.inline=function(a,e){if(!CKEDITOR.env.isCompatible)return null;a=CKEDITOR.dom.element.get(a);if(a.getEditor())throw'The editor instance "'+a.getEditor().name+'" is already attached to the provided element.';var b=new CKEDITOR.editor(e,a,CKEDITOR.ELEMENT_MODE_INLINE),c=a.is("textarea")?a:null;if(c){b.setData(c.getValue(),null,true);a=CKEDITOR.dom.element.createFromHtml('<div contenteditable="'+!!b.readOnly+'" class="cke_textarea_inline">'+c.getValue()+"</div>",CKEDITOR.document);
-a.insertAfter(c);c.hide();c.$.form&&b._attachToForm()}else b.setData(a.getHtml(),null,true);b.on("loaded",function(){b.fire("uiReady");b.editable(a);b.container=a;b.setData(b.getData(1));b.resetDirty();b.fire("contentDom");b.mode="wysiwyg";b.fire("mode");b.status="ready";b.fireOnce("instanceReady");CKEDITOR.fire("instanceReady",null,b)},null,null,1E4);b.on("destroy",function(){if(c){b.container.clearCustomData();b.container.remove();c.show()}b.element.clearCustomData();delete b.element});return b};
-CKEDITOR.inlineAll=function(){var a,e,b;for(b in CKEDITOR.dtd.$editable)for(var c=CKEDITOR.document.getElementsByTag(b),d=0,h=c.count();d<h;d++){a=c.getItem(d);if(a.getAttribute("contenteditable")=="true"){e={element:a,config:{}};CKEDITOR.fire("inline",e)!==false&&CKEDITOR.inline(a,e.config)}}};CKEDITOR.domReady(function(){!CKEDITOR.disableAutoInline&&CKEDITOR.inlineAll()})})();CKEDITOR.replaceClass="ckeditor";
-(function(){function a(a,c,g,n){if(!CKEDITOR.env.isCompatible)return null;a=CKEDITOR.dom.element.get(a);if(a.getEditor())throw'The editor instance "'+a.getEditor().name+'" is already attached to the provided element.';var i=new CKEDITOR.editor(c,a,n);if(n==CKEDITOR.ELEMENT_MODE_REPLACE){a.setStyle("visibility","hidden");i._.required=a.hasAttribute("required");a.removeAttribute("required")}g&&i.setData(g,null,true);i.on("loaded",function(){b(i);n==CKEDITOR.ELEMENT_MODE_REPLACE&&(i.config.autoUpdateElement&&
-a.$.form)&&i._attachToForm();i.setMode(i.config.startupMode,function(){i.resetDirty();i.status="ready";i.fireOnce("instanceReady");CKEDITOR.fire("instanceReady",null,i)})});i.on("destroy",e);return i}function e(){var a=this.container,b=this.element;if(a){a.clearCustomData();a.remove()}if(b){b.clearCustomData();if(this.elementMode==CKEDITOR.ELEMENT_MODE_REPLACE){b.show();this._.required&&b.setAttribute("required","required")}delete this.element}}function b(a){var b=a.name,e=a.element,n=a.elementMode,
-i=a.fire("uiSpace",{space:"top",html:""}).html,j=a.fire("uiSpace",{space:"bottom",html:""}).html;c||(c=CKEDITOR.addTemplate("maincontainer",'<{outerEl} id="cke_{name}" class="{id} cke cke_reset cke_chrome cke_editor_{name} cke_{langDir} '+CKEDITOR.env.cssClass+'"  dir="{langDir}" lang="{langCode}" role="application" aria-labelledby="cke_{name}_arialbl"><span id="cke_{name}_arialbl" class="cke_voice_label">{voiceLabel}</span><{outerEl} class="cke_inner cke_reset" role="presentation">{topHtml}<{outerEl} id="{contentId}" class="cke_contents cke_reset" role="presentation"></{outerEl}>{bottomHtml}</{outerEl}></{outerEl}>'));
-b=CKEDITOR.dom.element.createFromHtml(c.output({id:a.id,name:b,langDir:a.lang.dir,langCode:a.langCode,voiceLabel:[a.lang.editor,a.name].join(", "),topHtml:i?'<span id="'+a.ui.spaceId("top")+'" class="cke_top cke_reset_all" role="presentation" style="height:auto">'+i+"</span>":"",contentId:a.ui.spaceId("contents"),bottomHtml:j?'<span id="'+a.ui.spaceId("bottom")+'" class="cke_bottom cke_reset_all" role="presentation">'+j+"</span>":"",outerEl:CKEDITOR.env.ie?"span":"div"}));if(n==CKEDITOR.ELEMENT_MODE_REPLACE){e.hide();
-b.insertAfter(e)}else e.append(b);a.container=b;i&&a.ui.space("top").unselectable();j&&a.ui.space("bottom").unselectable();e=a.config.width;n=a.config.height;e&&b.setStyle("width",CKEDITOR.tools.cssLength(e));n&&a.ui.space("contents").setStyle("height",CKEDITOR.tools.cssLength(n));b.disableContextMenu();CKEDITOR.env.webkit&&b.on("focus",function(){a.focus()});a.fireOnce("uiReady")}CKEDITOR.replace=function(b,c){return a(b,c,null,CKEDITOR.ELEMENT_MODE_REPLACE)};CKEDITOR.appendTo=function(b,c,e){return a(b,
-c,e,CKEDITOR.ELEMENT_MODE_APPENDTO)};CKEDITOR.replaceAll=function(){for(var a=document.getElementsByTagName("textarea"),b=0;b<a.length;b++){var c=null,e=a[b];if(e.name||e.id){if(typeof arguments[0]=="string"){if(!RegExp("(?:^|\\s)"+arguments[0]+"(?:$|\\s)").test(e.className))continue}else if(typeof arguments[0]=="function"){c={};if(arguments[0](e,c)===false)continue}this.replace(e,c)}}};CKEDITOR.editor.prototype.addMode=function(a,b){(this._.modes||(this._.modes={}))[a]=b};CKEDITOR.editor.prototype.setMode=
-function(a,b){var c=this,e=this._.modes;if(!(a==c.mode||!e||!e[a])){c.fire("beforeSetMode",a);if(c.mode){var i=c.checkDirty();c._.previousMode=c.mode;c.fire("beforeModeUnload");c.editable(0);c.ui.space("contents").setHtml("");c.mode=""}this._.modes[a](function(){c.mode=a;i!==void 0&&!i&&c.resetDirty();setTimeout(function(){c.fire("mode");b&&b.call(c)},0)})}};CKEDITOR.editor.prototype.resize=function(a,b,c,e){var i=this.container,j=this.ui.space("contents"),o=CKEDITOR.env.webkit&&this.document&&this.document.getWindow().$.frameElement,
-e=e?i.getChild(1):i;e.setSize("width",a,true);o&&(o.style.width="1%");j.setStyle("height",Math.max(b-(c?0:(e.$.offsetHeight||0)-(j.$.clientHeight||0)),0)+"px");o&&(o.style.width="100%");this.fire("resize")};CKEDITOR.editor.prototype.getResizable=function(a){return a?this.ui.space("contents"):this.container};var c;CKEDITOR.domReady(function(){CKEDITOR.replaceClass&&CKEDITOR.replaceAll(CKEDITOR.replaceClass)})})();CKEDITOR.config.startupMode="wysiwyg";
-(function(){function a(a){var b=a.editor,d=a.data.path,m=d.blockLimit,k=a.data.selection,l=k.getRanges()[0],g;if(CKEDITOR.env.gecko||CKEDITOR.env.ie&&CKEDITOR.env.needsBrFiller)if(k=e(k,d)){k.appendBogus();g=CKEDITOR.env.ie}if(b.config.autoParagraph!==false&&b.activeEnterMode!=CKEDITOR.ENTER_BR&&b.editable().equals(m)&&!d.block&&l.collapsed&&!l.getCommonAncestor().isReadOnly()){d=l.clone();d.enlarge(CKEDITOR.ENLARGE_BLOCK_CONTENTS);m=new CKEDITOR.dom.walker(d);m.guard=function(a){return!c(a)||a.type==
-CKEDITOR.NODE_COMMENT||a.isReadOnly()};if(!m.checkForward()||d.checkStartOfBlock()&&d.checkEndOfBlock()){b=l.fixBlock(true,b.activeEnterMode==CKEDITOR.ENTER_DIV?"div":"p");if(!CKEDITOR.env.needsBrFiller)(b=b.getFirst(c))&&(b.type==CKEDITOR.NODE_TEXT&&CKEDITOR.tools.trim(b.getText()).match(/^(?:&nbsp;|\xa0)$/))&&b.remove();g=1;a.cancel()}}g&&l.select()}function e(a,b){if(a.isFake)return 0;var e=b.block||b.blockLimit,d=e&&e.getLast(c);if(e&&e.isBlockBoundary()&&(!d||!(d.type==CKEDITOR.NODE_ELEMENT&&
-d.isBlockBoundary()))&&!e.is("pre")&&!e.getBogus())return e}function b(a){var b=a.data.getTarget();if(b.is("input")){b=b.getAttribute("type");(b=="submit"||b=="reset")&&a.data.preventDefault()}}function c(a){return o(a)&&q(a)}function d(a,b){return function(c){var e=CKEDITOR.dom.element.get(c.data.$.toElement||c.data.$.fromElement||c.data.$.relatedTarget);(!e||!b.equals(e)&&!b.contains(e))&&a.call(this,c)}}function h(a){var b,e=a.getRanges()[0],d=a.root,k={table:1,ul:1,ol:1,dl:1};if(e.startPath().contains(k)){var a=
-function(a){return function(e,d){d&&(e.type==CKEDITOR.NODE_ELEMENT&&e.is(k))&&(b=e);if(!d&&c(e)&&(!a||!i(e)))return false}},l=e.clone();l.collapse(1);l.setStartAt(d,CKEDITOR.POSITION_AFTER_START);d=new CKEDITOR.dom.walker(l);d.guard=a();d.checkBackward();if(b){l=e.clone();l.collapse();l.setEndAt(b,CKEDITOR.POSITION_AFTER_END);d=new CKEDITOR.dom.walker(l);d.guard=a(true);b=false;d.checkForward();return b}}return null}function g(a){a.editor.focus();a.editor.fire("saveSnapshot")}function n(a,b){var c=
-a.editor;!b&&c.getSelection().scrollIntoView();setTimeout(function(){c.fire("saveSnapshot")},0)}CKEDITOR.editable=CKEDITOR.tools.createClass({base:CKEDITOR.dom.element,$:function(a,b){this.base(b.$||b);this.editor=a;this.hasFocus=false;this.setup()},proto:{focus:function(){var a;if(CKEDITOR.env.webkit&&!this.hasFocus){a=this.editor._.previousActive||this.getDocument().getActive();if(this.contains(a)){a.focus();return}}try{this.$[CKEDITOR.env.ie&&this.getDocument().equals(CKEDITOR.document)?"setActive":
-"focus"]()}catch(b){if(!CKEDITOR.env.ie)throw b;}if(CKEDITOR.env.safari&&!this.isInline()){a=CKEDITOR.document.getActive();a.equals(this.getWindow().getFrame())||this.getWindow().focus()}},on:function(a,b){var c=Array.prototype.slice.call(arguments,0);if(CKEDITOR.env.ie&&/^focus|blur$/.exec(a)){a=a=="focus"?"focusin":"focusout";b=d(b,this);c[0]=a;c[1]=b}return CKEDITOR.dom.element.prototype.on.apply(this,c)},attachListener:function(a,b,c,e,d,l){!this._.listeners&&(this._.listeners=[]);var g=Array.prototype.slice.call(arguments,
-1),g=a.on.apply(a,g);this._.listeners.push(g);return g},clearListeners:function(){var a=this._.listeners;try{for(;a.length;)a.pop().removeListener()}catch(b){}},restoreAttrs:function(){var a=this._.attrChanges,b,c;for(c in a)if(a.hasOwnProperty(c)){b=a[c];b!==null?this.setAttribute(c,b):this.removeAttribute(c)}},attachClass:function(a){var b=this.getCustomData("classes");if(!this.hasClass(a)){!b&&(b=[]);b.push(a);this.setCustomData("classes",b);this.addClass(a)}},changeAttr:function(a,b){var c=this.getAttribute(a);
-if(b!==c){!this._.attrChanges&&(this._.attrChanges={});a in this._.attrChanges||(this._.attrChanges[a]=c);this.setAttribute(a,b)}},insertHtml:function(a,b){g(this);s(this,b||"html",a)},insertText:function(a){g(this);var b=this.editor,c=b.getSelection().getStartElement().hasAscendant("pre",true)?CKEDITOR.ENTER_BR:b.activeEnterMode,b=c==CKEDITOR.ENTER_BR,e=CKEDITOR.tools,a=e.htmlEncode(a.replace(/\r\n/g,"\n")),a=a.replace(/\t/g,"&nbsp;&nbsp; &nbsp;"),c=c==CKEDITOR.ENTER_P?"p":"div";if(!b){var d=/\n{2}/g;
-if(d.test(a))var l="<"+c+">",h="</"+c+">",a=l+a.replace(d,function(){return h+l})+h}a=a.replace(/\n/g,"<br>");b||(a=a.replace(RegExp("<br>(?=</"+c+">)"),function(a){return e.repeat(a,2)}));a=a.replace(/^ | $/g,"&nbsp;");a=a.replace(/(>|\s) /g,function(a,b){return b+"&nbsp;"}).replace(/ (?=<)/g,"&nbsp;");s(this,"text",a)},insertElement:function(a,b){b?this.insertElementIntoRange(a,b):this.insertElementIntoSelection(a)},insertElementIntoRange:function(a,b){var c=this.editor,e=c.config.enterMode,d=a.getName(),
-l=CKEDITOR.dtd.$block[d];if(b.checkReadOnly())return false;b.deleteContents(1);b.startContainer.type==CKEDITOR.NODE_ELEMENT&&b.startContainer.is({tr:1,table:1,tbody:1,thead:1,tfoot:1})&&u(b);var g,h;if(l)for(;(g=b.getCommonAncestor(0,1))&&(h=CKEDITOR.dtd[g.getName()])&&(!h||!h[d]);)if(g.getName()in CKEDITOR.dtd.span)b.splitElement(g);else if(b.checkStartOfBlock()&&b.checkEndOfBlock()){b.setStartBefore(g);b.collapse(true);g.remove()}else b.splitBlock(e==CKEDITOR.ENTER_DIV?"div":"p",c.editable());b.insertNode(a);
-return true},insertElementIntoSelection:function(a){var b=this.editor,e=b.activeEnterMode,b=b.getSelection(),d=b.getRanges()[0],k=a.getName(),k=CKEDITOR.dtd.$block[k];g(this);if(this.insertElementIntoRange(a,d)){d.moveToPosition(a,CKEDITOR.POSITION_AFTER_END);if(k)if((k=a.getNext(function(a){return c(a)&&!i(a)}))&&k.type==CKEDITOR.NODE_ELEMENT&&k.is(CKEDITOR.dtd.$block))k.getDtd()["#"]?d.moveToElementEditStart(k):d.moveToElementEditEnd(a);else if(!k&&e!=CKEDITOR.ENTER_BR){k=d.fixBlock(true,e==CKEDITOR.ENTER_DIV?
-"div":"p");d.moveToElementEditStart(k)}}b.selectRanges([d]);n(this,CKEDITOR.env.opera)},setData:function(a,b){b||(a=this.editor.dataProcessor.toHtml(a));this.setHtml(a);this.editor.fire("dataReady")},getData:function(a){var b=this.getHtml();a||(b=this.editor.dataProcessor.toDataFormat(b));return b},setReadOnly:function(a){this.setAttribute("contenteditable",!a)},detach:function(){this.removeClass("cke_editable");var a=this.editor;this._.detach();delete a.document;delete a.window},isInline:function(){return this.getDocument().equals(CKEDITOR.document)},
-setup:function(){var a=this.editor;this.attachListener(a,"beforeGetData",function(){var b=this.getData();this.is("textarea")||a.config.ignoreEmptyParagraph!==false&&(b=b.replace(j,function(a,b){return b}));a.setData(b,null,1)},this);this.attachListener(a,"getSnapshot",function(a){a.data=this.getData(1)},this);this.attachListener(a,"afterSetData",function(){this.setData(a.getData(1))},this);this.attachListener(a,"loadSnapshot",function(a){this.setData(a.data,1)},this);this.attachListener(a,"beforeFocus",
-function(){var b=a.getSelection();(b=b&&b.getNative())&&b.type=="Control"||this.focus()},this);this.attachListener(a,"insertHtml",function(a){this.insertHtml(a.data.dataValue,a.data.mode)},this);this.attachListener(a,"insertElement",function(a){this.insertElement(a.data)},this);this.attachListener(a,"insertText",function(a){this.insertText(a.data)},this);this.setReadOnly(a.readOnly);this.attachClass("cke_editable");this.attachClass(a.elementMode==CKEDITOR.ELEMENT_MODE_INLINE?"cke_editable_inline":
-a.elementMode==CKEDITOR.ELEMENT_MODE_REPLACE||a.elementMode==CKEDITOR.ELEMENT_MODE_APPENDTO?"cke_editable_themed":"");this.attachClass("cke_contents_"+a.config.contentsLangDirection);a.keystrokeHandler.blockedKeystrokes[8]=+a.readOnly;a.keystrokeHandler.attach(this);this.on("blur",function(a){CKEDITOR.env.opera&&CKEDITOR.document.getActive().equals(this.isInline()?this:this.getWindow().getFrame())?a.cancel():this.hasFocus=false},null,null,-1);this.on("focus",function(){this.hasFocus=true},null,null,
--1);a.focusManager.add(this);if(this.equals(CKEDITOR.document.getActive())){this.hasFocus=true;a.once("contentDom",function(){a.focusManager.focus()})}this.isInline()&&this.changeAttr("tabindex",a.tabIndex);if(!this.is("textarea")){a.document=this.getDocument();a.window=this.getWindow();var e=a.document;this.changeAttr("spellcheck",!a.config.disableNativeSpellChecker);var d=a.config.contentsLangDirection;this.getDirection(1)!=d&&this.changeAttr("dir",d);var m=CKEDITOR.getCss();if(m){d=e.getHead();
-if(!d.getCustomData("stylesheet")){m=e.appendStyleText(m);m=new CKEDITOR.dom.element(m.ownerNode||m.owningElement);d.setCustomData("stylesheet",m);m.data("cke-temp",1)}}d=e.getCustomData("stylesheet_ref")||0;e.setCustomData("stylesheet_ref",d+1);this.setCustomData("cke_includeReadonly",!a.config.disableReadonlyStyling);this.attachListener(this,"click",function(a){var a=a.data,b=(new CKEDITOR.dom.elementPath(a.getTarget(),this)).contains("a");b&&(a.$.button!=2&&b.isReadOnly())&&a.preventDefault()});
-var k={8:1,46:1};this.attachListener(a,"key",function(b){if(a.readOnly)return true;var c=b.data.keyCode,e;if(c in k){var b=a.getSelection(),d,m=b.getRanges()[0],g=m.startPath(),i,j,n,c=c==8;if(CKEDITOR.env.ie&&CKEDITOR.env.version<11&&(d=b.getSelectedElement())||(d=h(b))){a.fire("saveSnapshot");m.moveToPosition(d,CKEDITOR.POSITION_BEFORE_START);d.remove();m.select();a.fire("saveSnapshot");e=1}else if(m.collapsed)if((i=g.block)&&(n=i[c?"getPrevious":"getNext"](o))&&n.type==CKEDITOR.NODE_ELEMENT&&n.is("table")&&
-m[c?"checkStartOfBlock":"checkEndOfBlock"]()){a.fire("saveSnapshot");m[c?"checkEndOfBlock":"checkStartOfBlock"]()&&i.remove();m["moveToElementEdit"+(c?"End":"Start")](n);m.select();a.fire("saveSnapshot");e=1}else if(g.blockLimit&&g.blockLimit.is("td")&&(j=g.blockLimit.getAscendant("table"))&&m.checkBoundaryOfElement(j,c?CKEDITOR.START:CKEDITOR.END)&&(n=j[c?"getPrevious":"getNext"](o))){a.fire("saveSnapshot");m["moveToElementEdit"+(c?"End":"Start")](n);m.checkStartOfBlock()&&m.checkEndOfBlock()?n.remove():
-m.select();a.fire("saveSnapshot");e=1}else if((j=g.contains(["td","th","caption"]))&&m.checkBoundaryOfElement(j,c?CKEDITOR.START:CKEDITOR.END))e=1}return!e});a.blockless&&(CKEDITOR.env.ie&&CKEDITOR.env.needsBrFiller)&&this.attachListener(this,"keyup",function(b){if(b.data.getKeystroke()in k&&!this.getFirst(c)){this.appendBogus();b=a.createRange();b.moveToPosition(this,CKEDITOR.POSITION_AFTER_START);b.select()}});this.attachListener(this,"dblclick",function(b){if(a.readOnly)return false;b={element:b.data.getTarget()};
-a.fire("doubleclick",b)});CKEDITOR.env.ie&&this.attachListener(this,"click",b);!CKEDITOR.env.ie&&!CKEDITOR.env.opera&&this.attachListener(this,"mousedown",function(b){var c=b.data.getTarget();if(c.is("img","hr","input","textarea","select")){a.getSelection().selectElement(c);c.is("input","textarea","select")&&b.data.preventDefault()}});CKEDITOR.env.gecko&&this.attachListener(this,"mouseup",function(b){if(b.data.$.button==2){b=b.data.getTarget();if(!b.getOuterHtml().replace(j,"")){var c=a.createRange();
-c.moveToElementEditStart(b);c.select(true)}}});if(CKEDITOR.env.webkit){this.attachListener(this,"click",function(a){a.data.getTarget().is("input","select")&&a.data.preventDefault()});this.attachListener(this,"mouseup",function(a){a.data.getTarget().is("input","textarea")&&a.data.preventDefault()})}}}},_:{detach:function(){this.editor.setData(this.editor.getData(),0,1);this.clearListeners();this.restoreAttrs();var a;if(a=this.removeCustomData("classes"))for(;a.length;)this.removeClass(a.pop());a=this.getDocument();
-var b=a.getHead();if(b.getCustomData("stylesheet")){var c=a.getCustomData("stylesheet_ref");if(--c)a.setCustomData("stylesheet_ref",c);else{a.removeCustomData("stylesheet_ref");b.removeCustomData("stylesheet").remove()}}delete this.editor}}});CKEDITOR.editor.prototype.editable=function(a){var b=this._.editable;if(b&&a)return 0;if(arguments.length)b=this._.editable=a?a instanceof CKEDITOR.editable?a:new CKEDITOR.editable(this,a):(b&&b.detach(),null);return b};var i=CKEDITOR.dom.walker.bogus(),j=/(^|<body\b[^>]*>)\s*<(p|div|address|h\d|center|pre)[^>]*>\s*(?:<br[^>]*>|&nbsp;|\u00A0|&#160;)?\s*(:?<\/\2>)?\s*(?=$|<\/body>)/gi,
-o=CKEDITOR.dom.walker.whitespaces(true),q=CKEDITOR.dom.walker.bookmark(false,true);CKEDITOR.on("instanceLoaded",function(b){var c=b.editor;c.on("insertElement",function(a){a=a.data;if(a.type==CKEDITOR.NODE_ELEMENT&&(a.is("input")||a.is("textarea"))){a.getAttribute("contentEditable")!="false"&&a.data("cke-editable",a.hasAttribute("contenteditable")?"true":"1");a.setAttribute("contentEditable",false)}});c.on("selectionChange",function(b){if(!c.readOnly){var e=c.getSelection();if(e&&!e.isLocked){e=c.checkDirty();
-c.fire("lockSnapshot");a(b);c.fire("unlockSnapshot");!e&&c.resetDirty()}}})});CKEDITOR.on("instanceCreated",function(a){var b=a.editor;b.on("mode",function(){var a=b.editable();if(a&&a.isInline()){var c=b.title;a.changeAttr("role","textbox");a.changeAttr("aria-label",c);c&&a.changeAttr("title",c);if(c=this.ui.space(this.elementMode==CKEDITOR.ELEMENT_MODE_INLINE?"top":"contents")){var e=CKEDITOR.tools.getNextId(),d=CKEDITOR.dom.element.createFromHtml('<span id="'+e+'" class="cke_voice_label">'+this.lang.common.editorHelp+
-"</span>");c.append(d);a.changeAttr("aria-describedby",e)}}})});CKEDITOR.addCss(".cke_editable{cursor:text}.cke_editable img,.cke_editable input,.cke_editable textarea{cursor:default}");var s=function(){function a(b){return b.type==CKEDITOR.NODE_ELEMENT}function b(c,e){var d,k,m,l,h=[],i=e.range.startContainer;d=e.range.startPath();for(var i=g[i.getName()],r=0,j=c.getChildren(),n=j.count(),o=-1,q=-1,x=0,s=d.contains(g.$list);r<n;++r){d=j.getItem(r);if(a(d)){m=d.getName();if(s&&m in CKEDITOR.dtd.$list)h=
-h.concat(b(d,e));else{l=!!i[m];if(m=="br"&&d.data("cke-eol")&&(!r||r==n-1)){x=(k=r?h[r-1].node:j.getItem(r+1))&&(!a(k)||!k.is("br"));k=k&&a(k)&&g.$block[k.getName()]}o==-1&&!l&&(o=r);l||(q=r);h.push({isElement:1,isLineBreak:x,isBlock:d.isBlockBoundary(),hasBlockSibling:k,node:d,name:m,allowed:l});k=x=0}}else h.push({isElement:0,node:d,allowed:1})}if(o>-1)h[o].firstNotAllowed=1;if(q>-1)h[q].lastNotAllowed=1;return h}function e(b,c){var d=[],k=b.getChildren(),m=k.count(),l,h=0,r=g[c],i=!b.is(g.$inline)||
-b.is("br");for(i&&d.push(" ");h<m;h++){l=k.getItem(h);a(l)&&!l.is(r)?d=d.concat(e(l,c)):d.push(l)}i&&d.push(" ");return d}function d(b){return b&&a(b)&&(b.is(g.$removeEmpty)||b.is("a")&&!b.isBlockBoundary())}function k(b,c,e,d){var l=b.clone(),m,g;l.setEndAt(c,CKEDITOR.POSITION_BEFORE_END);if((m=(new CKEDITOR.dom.walker(l)).next())&&a(m)&&h[m.getName()]&&(g=m.getPrevious())&&a(g)&&!g.getParent().equals(b.startContainer)&&e.contains(g)&&d.contains(m)&&m.isIdentical(g)){m.moveChildren(g);m.remove();
-k(b,c,e,d)}}function l(b,c){function e(b,c){if(c.isBlock&&c.isElement&&!c.node.is("br")&&a(b)&&b.is("br")){b.remove();return 1}}var d=c.endContainer.getChild(c.endOffset),k=c.endContainer.getChild(c.endOffset-1);d&&e(d,b[b.length-1]);if(k&&e(k,b[0])){c.setEnd(c.endContainer,c.endOffset-1);c.collapse()}}var g=CKEDITOR.dtd,h={p:1,div:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,ul:1,ol:1,li:1,pre:1,dl:1,blockquote:1},r={p:1,div:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1},i=CKEDITOR.tools.extend({},g.$inline);delete i.br;return function(h,
-j,o){var q=h.editor;h.getDocument();var x=q.getSelection().getRanges()[0],s=false;if(j=="unfiltered_html"){j="html";s=true}if(!x.checkReadOnly()){var v=(new CKEDITOR.dom.elementPath(x.startContainer,x.root)).blockLimit||x.root,j={type:j,dontFilter:s,editable:h,editor:q,range:x,blockLimit:v,mergeCandidates:[],zombies:[]},q=j.range,s=j.mergeCandidates,w,H,C,D;if(j.type=="text"&&q.shrink(CKEDITOR.SHRINK_ELEMENT,true,false)){w=CKEDITOR.dom.element.createFromHtml("<span>&nbsp;</span>",q.document);q.insertNode(w);
-q.setStartAfter(w)}H=new CKEDITOR.dom.elementPath(q.startContainer);j.endPath=C=new CKEDITOR.dom.elementPath(q.endContainer);if(!q.collapsed){var v=C.block||C.blockLimit,X=q.getCommonAncestor();v&&(!v.equals(X)&&!v.contains(X)&&q.checkEndOfBlock())&&j.zombies.push(v);q.deleteContents()}for(;(D=a(q.startContainer)&&q.startContainer.getChild(q.startOffset-1))&&a(D)&&D.isBlockBoundary()&&H.contains(D);)q.moveToPosition(D,CKEDITOR.POSITION_BEFORE_END);k(q,j.blockLimit,H,C);if(w){q.setEndBefore(w);q.collapse();
-w.remove()}w=q.startPath();if(v=w.contains(d,false,1)){q.splitElement(v);j.inlineStylesRoot=v;j.inlineStylesPeak=w.lastElement}w=q.createBookmark();(v=w.startNode.getPrevious(c))&&a(v)&&d(v)&&s.push(v);(v=w.startNode.getNext(c))&&a(v)&&d(v)&&s.push(v);for(v=w.startNode;(v=v.getParent())&&d(v);)s.push(v);q.moveToBookmark(w);if(w=o){w=j.range;if(j.type=="text"&&j.inlineStylesRoot){D=j.inlineStylesPeak;q=D.getDocument().createText("{cke-peak}");for(s=j.inlineStylesRoot.getParent();!D.equals(s);){q=q.appendTo(D.clone());
-D=D.getParent()}o=q.getOuterHtml().split("{cke-peak}").join(o)}D=j.blockLimit.getName();if(/^\s+|\s+$/.test(o)&&"span"in CKEDITOR.dtd[D])var u='<span data-cke-marker="1">&nbsp;</span>',o=u+o+u;o=j.editor.dataProcessor.toHtml(o,{context:null,fixForBody:false,dontFilter:j.dontFilter,filter:j.editor.activeFilter,enterMode:j.editor.activeEnterMode});D=w.document.createElement("body");D.setHtml(o);if(u){D.getFirst().remove();D.getLast().remove()}if((u=w.startPath().block)&&!(u.getChildCount()==1&&u.getBogus()))a:{var F;
-if(D.getChildCount()==1&&a(F=D.getFirst())&&F.is(r)){u=F.getElementsByTag("*");w=0;for(s=u.count();w<s;w++){q=u.getItem(w);if(!q.is(i))break a}F.moveChildren(F.getParent(1));F.remove()}}j.dataWrapper=D;w=o}if(w){F=j.range;var u=F.document,B,o=j.blockLimit;w=0;var J;D=[];var G,O,s=q=0,K,R;H=F.startContainer;var v=j.endPath.elements[0],S;C=v.getPosition(H);X=!!v.getCommonAncestor(H)&&C!=CKEDITOR.POSITION_IDENTICAL&&!(C&CKEDITOR.POSITION_CONTAINS+CKEDITOR.POSITION_IS_CONTAINED);H=b(j.dataWrapper,j);
-for(l(H,F);w<H.length;w++){C=H[w];if(B=C.isLineBreak){B=F;K=o;var N=void 0,U=void 0;if(C.hasBlockSibling)B=1;else{N=B.startContainer.getAscendant(g.$block,1);if(!N||!N.is({div:1,p:1}))B=0;else{U=N.getPosition(K);if(U==CKEDITOR.POSITION_IDENTICAL||U==CKEDITOR.POSITION_CONTAINS)B=0;else{K=B.splitElement(N);B.moveToPosition(K,CKEDITOR.POSITION_AFTER_START);B=1}}}}if(B)s=w>0;else{B=F.startPath();if(!C.isBlock&&j.editor.config.autoParagraph!==false&&(j.editor.activeEnterMode!=CKEDITOR.ENTER_BR&&j.editor.editable().equals(B.blockLimit)&&
-!B.block)&&(O=j.editor.activeEnterMode!=CKEDITOR.ENTER_BR&&j.editor.config.autoParagraph!==false?j.editor.activeEnterMode==CKEDITOR.ENTER_DIV?"div":"p":false)){O=u.createElement(O);O.appendBogus();F.insertNode(O);CKEDITOR.env.needsBrFiller&&(J=O.getBogus())&&J.remove();F.moveToPosition(O,CKEDITOR.POSITION_BEFORE_END)}if((B=F.startPath().block)&&!B.equals(G)){if(J=B.getBogus()){J.remove();D.push(B)}G=B}C.firstNotAllowed&&(q=1);if(q&&C.isElement){B=F.startContainer;for(K=null;B&&!g[B.getName()][C.name];){if(B.equals(o)){B=
-null;break}K=B;B=B.getParent()}if(B){if(K){R=F.splitElement(K);j.zombies.push(R);j.zombies.push(K)}}else{K=o.getName();S=!w;B=w==H.length-1;K=e(C.node,K);for(var N=[],U=K.length,Y=0,$=void 0,aa=0,ba=-1;Y<U;Y++){$=K[Y];if($==" "){if(!aa&&(!S||Y)){N.push(new CKEDITOR.dom.text(" "));ba=N.length}aa=1}else{N.push($);aa=0}}B&&ba==N.length&&N.pop();S=N}}if(S){for(;B=S.pop();)F.insertNode(B);S=0}else F.insertNode(C.node);if(C.lastNotAllowed&&w<H.length-1){(R=X?v:R)&&F.setEndAt(R,CKEDITOR.POSITION_AFTER_START);
-q=0}F.collapse()}}j.dontMoveCaret=s;j.bogusNeededBlocks=D}J=j.range;var V;R=j.bogusNeededBlocks;for(S=J.createBookmark();G=j.zombies.pop();)if(G.getParent()){O=J.clone();O.moveToElementEditStart(G);O.removeEmptyBlocksAtEnd()}if(R)for(;G=R.pop();)CKEDITOR.env.needsBrFiller?G.appendBogus():G.append(J.document.createText(" "));for(;G=j.mergeCandidates.pop();)G.mergeSiblings();J.moveToBookmark(S);if(!j.dontMoveCaret){for(G=a(J.startContainer)&&J.startContainer.getChild(J.startOffset-1);G&&a(G)&&!G.is(g.$empty);){if(G.isBlockBoundary())J.moveToPosition(G,
-CKEDITOR.POSITION_BEFORE_END);else{if(d(G)&&G.getHtml().match(/(\s|&nbsp;)$/g)){V=null;break}V=J.clone();V.moveToPosition(G,CKEDITOR.POSITION_BEFORE_END)}G=G.getLast(c)}V&&J.moveToRange(V)}x.select();n(h)}}}(),u=function(){function a(b){b=new CKEDITOR.dom.walker(b);b.guard=function(a,b){if(b)return false;if(a.type==CKEDITOR.NODE_ELEMENT)return a.is(CKEDITOR.dtd.$tableContent)};b.evaluator=function(a){return a.type==CKEDITOR.NODE_ELEMENT};return b}function b(a,c,e){c=a.getDocument().createElement(c);
-a.append(c,e);return c}function c(a){var b=a.count(),e;for(b;b-- >0;){e=a.getItem(b);if(!CKEDITOR.tools.trim(e.getHtml())){e.appendBogus();CKEDITOR.env.ie&&(CKEDITOR.env.version<9&&e.getChildCount())&&e.getFirst().remove()}}}return function(e){var d=e.startContainer,l=d.getAscendant("table",1),g=false;c(l.getElementsByTag("td"));c(l.getElementsByTag("th"));l=e.clone();l.setStart(d,0);l=a(l).lastBackward();if(!l){l=e.clone();l.setEndAt(d,CKEDITOR.POSITION_BEFORE_END);l=a(l).lastForward();g=true}l||
-(l=d);if(l.is("table")){e.setStartAt(l,CKEDITOR.POSITION_BEFORE_START);e.collapse(true);l.remove()}else{l.is({tbody:1,thead:1,tfoot:1})&&(l=b(l,"tr",g));l.is("tr")&&(l=b(l,l.getParent().is("thead")?"th":"td",g));(d=l.getBogus())&&d.remove();e.moveToPosition(l,g?CKEDITOR.POSITION_AFTER_START:CKEDITOR.POSITION_BEFORE_END)}}}()})();
-(function(){function a(){var a=this._.fakeSelection,b;if(a){b=this.getSelection(1);if(!b||!b.isHidden()){a.reset();a=0}}if(!a){a=b||this.getSelection(1);if(!a||a.getType()==CKEDITOR.SELECTION_NONE)return}this.fire("selectionCheck",a);b=this.elementPath();if(!b.compare(this._.selectionPreviousPath)){if(CKEDITOR.env.webkit)this._.previousActive=this.document.getActive();this._.selectionPreviousPath=b;this.fire("selectionChange",{selection:a,path:b})}}function e(){q=true;if(!o){b.call(this);o=CKEDITOR.tools.setTimeout(b,
-200,this)}}function b(){o=null;if(q){CKEDITOR.tools.setTimeout(a,0,this);q=false}}function c(a){function b(c,e){return!c||c.type==CKEDITOR.NODE_TEXT?false:a.clone()["moveToElementEdit"+(e?"End":"Start")](c)}if(!(a.root instanceof CKEDITOR.editable))return false;var c=a.startContainer,e=a.getPreviousNode(s,null,c),d=a.getNextNode(s,null,c);return b(e)||b(d,1)||!e&&!d&&!(c.type==CKEDITOR.NODE_ELEMENT&&c.isBlockBoundary()&&c.getBogus())?true:false}function d(a){return a.getCustomData("cke-fillingChar")}
-function h(a,b){var c=a&&a.removeCustomData("cke-fillingChar");if(c){if(b!==false){var e,d=a.getDocument().getSelection().getNative(),f=d&&d.type!="None"&&d.getRangeAt(0);if(c.getLength()>1&&f&&f.intersectsNode(c.$)){e=[d.anchorOffset,d.focusOffset];f=d.focusNode==c.$&&d.focusOffset>0;d.anchorNode==c.$&&d.anchorOffset>0&&e[0]--;f&&e[1]--;var h;f=d;if(!f.isCollapsed){h=f.getRangeAt(0);h.setStart(f.anchorNode,f.anchorOffset);h.setEnd(f.focusNode,f.focusOffset);h=h.collapsed}h&&e.unshift(e.pop())}}c.setText(g(c.getText()));
-if(e){c=d.getRangeAt(0);c.setStart(c.startContainer,e[0]);c.setEnd(c.startContainer,e[1]);d.removeAllRanges();d.addRange(c)}}}function g(a){return a.replace(/\u200B( )?/g,function(a){return a[1]?" ":""})}function n(a,b,c){var e=a.on("focus",function(a){a.cancel()},null,null,-100);if(CKEDITOR.env.ie)var d=a.getDocument().on("selectionchange",function(a){a.cancel()},null,null,-100);else{var f=new CKEDITOR.dom.range(a);f.moveToElementEditStart(a);var g=a.getDocument().$.createRange();g.setStart(f.startContainer.$,
-f.startOffset);g.collapse(1);b.removeAllRanges();b.addRange(g)}c&&a.focus();e.removeListener();d&&d.removeListener()}function i(a){var b=CKEDITOR.dom.element.createFromHtml('<div data-cke-hidden-sel="1" data-cke-temp="1" style="'+(CKEDITOR.env.ie?"display:none":"position:fixed;top:0;left:-1000px")+'">&nbsp;</div>',a.document);a.fire("lockSnapshot");a.editable().append(b);var c=a.getSelection(),e=a.createRange(),d=c.root.on("selectionchange",function(a){a.cancel()},null,null,0);e.setStartAt(b,CKEDITOR.POSITION_AFTER_START);
-e.setEndAt(b,CKEDITOR.POSITION_BEFORE_END);c.selectRanges([e]);d.removeListener();a.fire("unlockSnapshot");a._.hiddenSelectionContainer=b}function j(a){var b={37:1,39:1,8:1,46:1};return function(c){var e=c.data.getKeystroke();if(b[e]){var d=a.getSelection().getRanges(),f=d[0];if(d.length==1&&f.collapsed)if((e=f[e<38?"getPreviousEditableNode":"getNextEditableNode"]())&&e.type==CKEDITOR.NODE_ELEMENT&&e.getAttribute("contenteditable")=="false"){a.getSelection().fake(e);c.data.preventDefault();c.cancel()}}}}
-var o,q,s=CKEDITOR.dom.walker.invisible(1),u=function(){function a(b){return function(a){var c=a.editor.createRange();c.moveToClosestEditablePosition(a.selected,b)&&a.editor.getSelection().selectRanges([c]);return false}}function b(a){return function(b){var c=b.editor,e=c.createRange(),d;if(!(d=e.moveToClosestEditablePosition(b.selected,a)))d=e.moveToClosestEditablePosition(b.selected,!a);d&&c.getSelection().selectRanges([e]);c.fire("saveSnapshot");b.selected.remove();if(!d){e.moveToElementEditablePosition(c.editable());
-c.getSelection().selectRanges([e])}c.fire("saveSnapshot");return false}}var c=a(),e=a(1);return{37:c,38:c,39:e,40:e,8:b(),46:b(1)}}();CKEDITOR.on("instanceCreated",function(b){function c(){var a=d.getSelection();a&&a.removeAllRanges()}var d=b.editor;d.on("contentDom",function(){var b=d.document,c=CKEDITOR.document,k=d.editable(),m=b.getBody(),g=b.getDocumentElement(),i=k.isInline(),o,n;CKEDITOR.env.gecko&&k.attachListener(k,"focus",function(a){a.removeListener();if(o!==0)if((a=d.getSelection().getNative())&&
-a.isCollapsed&&a.anchorNode==k.$){a=d.createRange();a.moveToElementEditStart(k);a.select()}},null,null,-2);k.attachListener(k,CKEDITOR.env.webkit?"DOMFocusIn":"focus",function(){o&&CKEDITOR.env.webkit&&(o=d._.previousActive&&d._.previousActive.equals(b.getActive()));d.unlockSelection(o);o=0},null,null,-1);k.attachListener(k,"mousedown",function(){o=0});if(CKEDITOR.env.ie||CKEDITOR.env.opera||i){var q=function(){n=new CKEDITOR.dom.selection(d.getSelection());n.lock()};f?k.attachListener(k,"beforedeactivate",
-q,null,null,-1):k.attachListener(d,"selectionCheck",q,null,null,-1);k.attachListener(k,CKEDITOR.env.webkit?"DOMFocusOut":"blur",function(){d.lockSelection(n);o=1},null,null,-1);k.attachListener(k,"mousedown",function(){o=0})}if(CKEDITOR.env.ie&&!i){var p;k.attachListener(k,"mousedown",function(a){if(a.data.$.button==2){a=d.document.getSelection();if(!a||a.getType()==CKEDITOR.SELECTION_NONE)p=d.window.getScrollPosition()}});k.attachListener(k,"mouseup",function(a){if(a.data.$.button==2&&p){d.document.$.documentElement.scrollLeft=
-p.x;d.document.$.documentElement.scrollTop=p.y}p=null});if(b.$.compatMode!="BackCompat"){if(CKEDITOR.env.ie7Compat||CKEDITOR.env.ie6Compat)g.on("mousedown",function(a){function b(a){a=a.data.$;if(e){var c=m.$.createTextRange();try{c.moveToPoint(a.x,a.y)}catch(d){}e.setEndPoint(f.compareEndPoints("StartToStart",c)<0?"EndToEnd":"StartToStart",c);e.select()}}function d(){g.removeListener("mousemove",b);c.removeListener("mouseup",d);g.removeListener("mouseup",d);e.select()}a=a.data;if(a.getTarget().is("html")&&
-a.$.y<g.$.clientHeight&&a.$.x<g.$.clientWidth){var e=m.$.createTextRange();try{e.moveToPoint(a.$.x,a.$.y)}catch(k){}var f=e.duplicate();g.on("mousemove",b);c.on("mouseup",d);g.on("mouseup",d)}});if(CKEDITOR.env.version>7&&CKEDITOR.env.version<11){g.on("mousedown",function(a){if(a.data.getTarget().is("html")){c.on("mouseup",v);g.on("mouseup",v)}});var v=function(){c.removeListener("mouseup",v);g.removeListener("mouseup",v);var a=CKEDITOR.document.$.selection,d=a.createRange();a.type!="None"&&d.parentElement().ownerDocument==
-b.$&&d.select()}}}}k.attachListener(k,"selectionchange",a,d);k.attachListener(k,"keyup",e,d);k.attachListener(k,CKEDITOR.env.webkit?"DOMFocusIn":"focus",function(){d.forceNextSelectionCheck();d.selectionChange(1)});if(i?CKEDITOR.env.webkit||CKEDITOR.env.gecko:CKEDITOR.env.opera){var w;k.attachListener(k,"mousedown",function(){w=1});k.attachListener(b.getDocumentElement(),"mouseup",function(){w&&e.call(d);w=0})}else k.attachListener(CKEDITOR.env.ie?k:b.getDocumentElement(),"mouseup",e,d);CKEDITOR.env.webkit&&
-k.attachListener(b,"keydown",function(a){switch(a.data.getKey()){case 13:case 33:case 34:case 35:case 36:case 37:case 39:case 8:case 45:case 46:h(k)}},null,null,-1);k.attachListener(k,"keydown",j(d),null,null,-1)});d.on("contentDomUnload",d.forceNextSelectionCheck,d);d.on("dataReady",function(){delete d._.fakeSelection;delete d._.hiddenSelectionContainer;d.selectionChange(1)});d.on("loadSnapshot",function(){var a=d.editable().getLast(function(a){return a.type==CKEDITOR.NODE_ELEMENT});a&&a.hasAttribute("data-cke-hidden-sel")&&
-a.remove()},null,null,100);CKEDITOR.env.ie9Compat&&d.on("beforeDestroy",c,null,null,9);CKEDITOR.env.webkit&&d.on("setData",c);d.on("contentDomUnload",function(){d.unlockSelection()});d.on("key",function(a){if(d.mode=="wysiwyg"){var b=d.getSelection();if(b.isFake){var c=u[a.data.keyCode];if(c)return c({editor:d,selected:b.getSelectedElement(),selection:b,keyEvent:a})}}})});CKEDITOR.on("instanceReady",function(a){var b=a.editor;if(CKEDITOR.env.webkit){b.on("selectionChange",function(){var a=b.editable(),
-c=d(a);c&&(c.getCustomData("ready")?h(a):c.setCustomData("ready",1))},null,null,-1);b.on("beforeSetMode",function(){h(b.editable())},null,null,-1);var c,e,a=function(){var a=b.editable();if(a)if(a=d(a)){var f=b.document.$.defaultView.getSelection();f.type=="Caret"&&f.anchorNode==a.$&&(e=1);c=a.getText();a.setText(g(c))}},f=function(){var a=b.editable();if(a)if(a=d(a)){a.setText(c);if(e){b.document.$.defaultView.getSelection().setPosition(a.$,a.getLength());e=0}}};b.on("beforeUndoImage",a);b.on("afterUndoImage",
-f);b.on("beforeGetData",a,null,null,0);b.on("getData",f)}});CKEDITOR.editor.prototype.selectionChange=function(b){(b?a:e).call(this)};CKEDITOR.editor.prototype.getSelection=function(a){if((this._.savedSelection||this._.fakeSelection)&&!a)return this._.savedSelection||this._.fakeSelection;return(a=this.editable())&&this.mode=="wysiwyg"?new CKEDITOR.dom.selection(a):null};CKEDITOR.editor.prototype.lockSelection=function(a){a=a||this.getSelection(1);if(a.getType()!=CKEDITOR.SELECTION_NONE){!a.isLocked&&
-a.lock();this._.savedSelection=a;return true}return false};CKEDITOR.editor.prototype.unlockSelection=function(a){var b=this._.savedSelection;if(b){b.unlock(a);delete this._.savedSelection;return true}return false};CKEDITOR.editor.prototype.forceNextSelectionCheck=function(){delete this._.selectionPreviousPath};CKEDITOR.dom.document.prototype.getSelection=function(){return new CKEDITOR.dom.selection(this)};CKEDITOR.dom.range.prototype.select=function(){var a=this.root instanceof CKEDITOR.editable?
-this.root.editor.getSelection():new CKEDITOR.dom.selection(this.root);a.selectRanges([this]);return a};CKEDITOR.SELECTION_NONE=1;CKEDITOR.SELECTION_TEXT=2;CKEDITOR.SELECTION_ELEMENT=3;var f=typeof window.getSelection!="function",p=1;CKEDITOR.dom.selection=function(a){if(a instanceof CKEDITOR.dom.selection)var b=a,a=a.root;var c=a instanceof CKEDITOR.dom.element;this.rev=b?b.rev:p++;this.document=a instanceof CKEDITOR.dom.document?a:a.getDocument();this.root=a=c?a:this.document.getBody();this.isLocked=
-0;this._={cache:{}};if(b){CKEDITOR.tools.extend(this._.cache,b._.cache);this.isFake=b.isFake;this.isLocked=b.isLocked;return this}b=f?this.document.$.selection:this.document.getWindow().$.getSelection();if(CKEDITOR.env.webkit)(b.type=="None"&&this.document.getActive().equals(a)||b.type=="Caret"&&b.anchorNode.nodeType==CKEDITOR.NODE_DOCUMENT)&&n(a,b);else if(CKEDITOR.env.gecko)b&&(this.document.getActive().equals(a)&&b.anchorNode&&b.anchorNode.nodeType==CKEDITOR.NODE_DOCUMENT)&&n(a,b,true);else if(CKEDITOR.env.ie){var d;
-try{d=this.document.getActive()}catch(e){}if(f)b.type=="None"&&(d&&d.equals(this.document.getDocumentElement()))&&n(a,null,true);else{(b=b&&b.anchorNode)&&(b=new CKEDITOR.dom.node(b));d&&(d.equals(this.document.getDocumentElement())&&b&&(a.equals(b)||a.contains(b)))&&n(a,null,true)}}d=this.getNative();var g,h;if(d)if(d.getRangeAt)g=(h=d.rangeCount&&d.getRangeAt(0))&&new CKEDITOR.dom.node(h.commonAncestorContainer);else{try{h=d.createRange()}catch(j){}g=h&&CKEDITOR.dom.element.get(h.item&&h.item(0)||
-h.parentElement())}if(!g||!(g.type==CKEDITOR.NODE_ELEMENT||g.type==CKEDITOR.NODE_TEXT)||!this.root.equals(g)&&!this.root.contains(g)){this._.cache.type=CKEDITOR.SELECTION_NONE;this._.cache.startElement=null;this._.cache.selectedElement=null;this._.cache.selectedText="";this._.cache.ranges=new CKEDITOR.dom.rangeList}return this};var y={img:1,hr:1,li:1,table:1,tr:1,td:1,th:1,embed:1,object:1,ol:1,ul:1,a:1,input:1,form:1,select:1,textarea:1,button:1,fieldset:1,thead:1,tfoot:1};CKEDITOR.dom.selection.prototype=
-{getNative:function(){return this._.cache.nativeSel!==void 0?this._.cache.nativeSel:this._.cache.nativeSel=f?this.document.$.selection:this.document.getWindow().$.getSelection()},getType:f?function(){var a=this._.cache;if(a.type)return a.type;var b=CKEDITOR.SELECTION_NONE;try{var c=this.getNative(),d=c.type;if(d=="Text")b=CKEDITOR.SELECTION_TEXT;if(d=="Control")b=CKEDITOR.SELECTION_ELEMENT;if(c.createRange().parentElement())b=CKEDITOR.SELECTION_TEXT}catch(e){}return a.type=b}:function(){var a=this._.cache;
-if(a.type)return a.type;var b=CKEDITOR.SELECTION_TEXT,c=this.getNative();if(!c||!c.rangeCount)b=CKEDITOR.SELECTION_NONE;else if(c.rangeCount==1){var c=c.getRangeAt(0),d=c.startContainer;if(d==c.endContainer&&d.nodeType==1&&c.endOffset-c.startOffset==1&&y[d.childNodes[c.startOffset].nodeName.toLowerCase()])b=CKEDITOR.SELECTION_ELEMENT}return a.type=b},getRanges:function(){var a=f?function(){function a(b){return(new CKEDITOR.dom.node(b)).getIndex()}var b=function(b,c){b=b.duplicate();b.collapse(c);
-var d=b.parentElement();if(!d.hasChildNodes())return{container:d,offset:0};for(var e=d.children,f,g,h=b.duplicate(),l=0,m=e.length-1,j=-1,i,o;l<=m;){j=Math.floor((l+m)/2);f=e[j];h.moveToElementText(f);i=h.compareEndPoints("StartToStart",b);if(i>0)m=j-1;else if(i<0)l=j+1;else return{container:d,offset:a(f)}}if(j==-1||j==e.length-1&&i<0){h.moveToElementText(d);h.setEndPoint("StartToStart",b);h=h.text.replace(/(\r\n|\r)/g,"\n").length;e=d.childNodes;if(!h){f=e[e.length-1];return f.nodeType!=CKEDITOR.NODE_TEXT?
-{container:d,offset:e.length}:{container:f,offset:f.nodeValue.length}}for(d=e.length;h>0&&d>0;){g=e[--d];if(g.nodeType==CKEDITOR.NODE_TEXT){o=g;h=h-g.nodeValue.length}}return{container:o,offset:-h}}h.collapse(i>0?true:false);h.setEndPoint(i>0?"StartToStart":"EndToStart",b);h=h.text.replace(/(\r\n|\r)/g,"\n").length;if(!h)return{container:d,offset:a(f)+(i>0?0:1)};for(;h>0;)try{g=f[i>0?"previousSibling":"nextSibling"];if(g.nodeType==CKEDITOR.NODE_TEXT){h=h-g.nodeValue.length;o=g}f=g}catch(n){return{container:d,
-offset:a(f)}}return{container:o,offset:i>0?-h:o.nodeValue.length+h}};return function(){var a=this.getNative(),c=a&&a.createRange(),d=this.getType();if(!a)return[];if(d==CKEDITOR.SELECTION_TEXT){a=new CKEDITOR.dom.range(this.root);d=b(c,true);a.setStart(new CKEDITOR.dom.node(d.container),d.offset);d=b(c);a.setEnd(new CKEDITOR.dom.node(d.container),d.offset);a.endContainer.getPosition(a.startContainer)&CKEDITOR.POSITION_PRECEDING&&a.endOffset<=a.startContainer.getIndex()&&a.collapse();return[a]}if(d==
-CKEDITOR.SELECTION_ELEMENT){for(var d=[],e=0;e<c.length;e++){for(var f=c.item(e),k=f.parentNode,g=0,a=new CKEDITOR.dom.range(this.root);g<k.childNodes.length&&k.childNodes[g]!=f;g++);a.setStart(new CKEDITOR.dom.node(k),g);a.setEnd(new CKEDITOR.dom.node(k),g+1);d.push(a)}return d}return[]}}():function(){var a=[],b,c=this.getNative();if(!c)return a;for(var d=0;d<c.rangeCount;d++){var e=c.getRangeAt(d);b=new CKEDITOR.dom.range(this.root);b.setStart(new CKEDITOR.dom.node(e.startContainer),e.startOffset);
-b.setEnd(new CKEDITOR.dom.node(e.endContainer),e.endOffset);a.push(b)}return a};return function(b){var c=this._.cache;if(c.ranges&&!b)return c.ranges;if(!c.ranges)c.ranges=new CKEDITOR.dom.rangeList(a.call(this));if(b)for(var d=c.ranges,e=0;e<d.length;e++){var f=d[e];f.getCommonAncestor().isReadOnly()&&d.splice(e,1);if(!f.collapsed){if(f.startContainer.isReadOnly())for(var b=f.startContainer,g;b;){if((g=b.type==CKEDITOR.NODE_ELEMENT)&&b.is("body")||!b.isReadOnly())break;g&&b.getAttribute("contentEditable")==
-"false"&&f.setStartAfter(b);b=b.getParent()}b=f.startContainer;g=f.endContainer;var h=f.startOffset,j=f.endOffset,i=f.clone();b&&b.type==CKEDITOR.NODE_TEXT&&(h>=b.getLength()?i.setStartAfter(b):i.setStartBefore(b));g&&g.type==CKEDITOR.NODE_TEXT&&(j?i.setEndAfter(g):i.setEndBefore(g));b=new CKEDITOR.dom.walker(i);b.evaluator=function(a){if(a.type==CKEDITOR.NODE_ELEMENT&&a.isReadOnly()){var b=f.clone();f.setEndBefore(a);f.collapsed&&d.splice(e--,1);if(!(a.getPosition(i.endContainer)&CKEDITOR.POSITION_CONTAINS)){b.setStartAfter(a);
-b.collapsed||d.splice(e+1,0,b)}return true}return false};b.next()}}return c.ranges}}(),getStartElement:function(){var a=this._.cache;if(a.startElement!==void 0)return a.startElement;var b;switch(this.getType()){case CKEDITOR.SELECTION_ELEMENT:return this.getSelectedElement();case CKEDITOR.SELECTION_TEXT:var c=this.getRanges()[0];if(c){if(c.collapsed){b=c.startContainer;b.type!=CKEDITOR.NODE_ELEMENT&&(b=b.getParent())}else{for(c.optimize();;){b=c.startContainer;if(c.startOffset==(b.getChildCount?b.getChildCount():
-b.getLength())&&!b.isBlockBoundary())c.setStartAfter(b);else break}b=c.startContainer;if(b.type!=CKEDITOR.NODE_ELEMENT)return b.getParent();b=b.getChild(c.startOffset);if(!b||b.type!=CKEDITOR.NODE_ELEMENT)b=c.startContainer;else for(c=b.getFirst();c&&c.type==CKEDITOR.NODE_ELEMENT;){b=c;c=c.getFirst()}}b=b.$}}return a.startElement=b?new CKEDITOR.dom.element(b):null},getSelectedElement:function(){var a=this._.cache;if(a.selectedElement!==void 0)return a.selectedElement;var b=this,c=CKEDITOR.tools.tryThese(function(){return b.getNative().createRange().item(0)},
-function(){for(var a=b.getRanges()[0].clone(),c,d,e=2;e&&(!(c=a.getEnclosedNode())||!(c.type==CKEDITOR.NODE_ELEMENT&&y[c.getName()]&&(d=c)));e--)a.shrink(CKEDITOR.SHRINK_ELEMENT);return d&&d.$});return a.selectedElement=c?new CKEDITOR.dom.element(c):null},getSelectedText:function(){var a=this._.cache;if(a.selectedText!==void 0)return a.selectedText;var b=this.getNative(),b=f?b.type=="Control"?"":b.createRange().text:b.toString();return a.selectedText=b},lock:function(){this.getRanges();this.getStartElement();
-this.getSelectedElement();this.getSelectedText();this._.cache.nativeSel=null;this.isLocked=1},unlock:function(a){if(this.isLocked){if(a)var b=this.getSelectedElement(),c=!b&&this.getRanges(),d=this.isFake;this.isLocked=0;this.reset();if(a)(a=b||c[0]&&c[0].getCommonAncestor())&&a.getAscendant("body",1)&&(d?this.fake(b):b?this.selectElement(b):this.selectRanges(c))}},reset:function(){this._.cache={};this.isFake=0;var a=this.root.editor;if(a&&a._.fakeSelection&&this.rev==a._.fakeSelection.rev){delete a._.fakeSelection;
-var b=a._.hiddenSelectionContainer;if(b){a.fire("lockSnapshot");b.remove();a.fire("unlockSnapshot")}delete a._.hiddenSelectionContainer}this.rev=p++},selectElement:function(a){var b=new CKEDITOR.dom.range(this.root);b.setStartBefore(a);b.setEndAfter(a);this.selectRanges([b])},selectRanges:function(a){this.reset();if(a.length)if(this.isLocked){var b=CKEDITOR.document.getActive();this.unlock();this.selectRanges(a);this.lock();!b.equals(this.root)&&b.focus()}else{a:{var d,e;if(a.length==1&&!(e=a[0]).collapsed&&
-(b=e.getEnclosedNode())&&b.type==CKEDITOR.NODE_ELEMENT){e=e.clone();e.shrink(CKEDITOR.SHRINK_ELEMENT,true);if((d=e.getEnclosedNode())&&d.type==CKEDITOR.NODE_ELEMENT)b=d;if(b.getAttribute("contenteditable")=="false")break a}b=void 0}if(b)this.fake(b);else{if(f){e=CKEDITOR.dom.walker.whitespaces(true);d=/\ufeff|\u00a0/;var g={table:1,tbody:1,tr:1};if(a.length>1){b=a[a.length-1];a[0].setEnd(b.endContainer,b.endOffset)}var b=a[0],a=b.collapsed,j,i,o,n=b.getEnclosedNode();if(n&&n.type==CKEDITOR.NODE_ELEMENT&&
-n.getName()in y&&(!n.is("a")||!n.getText()))try{o=n.$.createControlRange();o.addElement(n.$);o.select();return}catch(q){}(b.startContainer.type==CKEDITOR.NODE_ELEMENT&&b.startContainer.getName()in g||b.endContainer.type==CKEDITOR.NODE_ELEMENT&&b.endContainer.getName()in g)&&b.shrink(CKEDITOR.NODE_ELEMENT,true);o=b.createBookmark();var g=o.startNode,p;if(!a)p=o.endNode;o=b.document.$.body.createTextRange();o.moveToElementText(g.$);o.moveStart("character",1);if(p){d=b.document.$.body.createTextRange();
-d.moveToElementText(p.$);o.setEndPoint("EndToEnd",d);o.moveEnd("character",-1)}else{j=g.getNext(e);i=g.hasAscendant("pre");j=!(j&&j.getText&&j.getText().match(d))&&(i||!g.hasPrevious()||g.getPrevious().is&&g.getPrevious().is("br"));i=b.document.createElement("span");i.setHtml("&#65279;");i.insertBefore(g);j&&b.document.createText("").insertBefore(g)}b.setStartBefore(g);g.remove();if(a){if(j){o.moveStart("character",-1);o.select();b.document.$.selection.clear()}else o.select();b.moveToPosition(i,
-CKEDITOR.POSITION_BEFORE_START);i.remove()}else{b.setEndBefore(p);p.remove();o.select()}}else{p=this.getNative();if(!p)return;if(CKEDITOR.env.opera){b=this.document.$.createRange();b.selectNodeContents(this.root.$);p.addRange(b)}this.removeAllRanges();for(o=0;o<a.length;o++){if(o<a.length-1){b=a[o];j=a[o+1];d=b.clone();d.setStart(b.endContainer,b.endOffset);d.setEnd(j.startContainer,j.startOffset);if(!d.collapsed){d.shrink(CKEDITOR.NODE_ELEMENT,true);i=d.getCommonAncestor();d=d.getEnclosedNode();
-if(i.isReadOnly()||d&&d.isReadOnly()){j.setStart(b.startContainer,b.startOffset);a.splice(o--,1);continue}}}b=a[o];i=this.document.$.createRange();j=b.startContainer;if(CKEDITOR.env.opera&&b.collapsed&&j.type==CKEDITOR.NODE_ELEMENT){d=j.getChild(b.startOffset-1);e=j.getChild(b.startOffset);if(!d&&!e&&j.is(CKEDITOR.dtd.$removeEmpty)||d&&d.type==CKEDITOR.NODE_ELEMENT||e&&e.type==CKEDITOR.NODE_ELEMENT){b.insertNode(this.document.createText(""));b.collapse(1)}}if(b.collapsed&&CKEDITOR.env.webkit&&c(b)){j=
-this.root;h(j,false);d=j.getDocument().createText("​");j.setCustomData("cke-fillingChar",d);b.insertNode(d);if((j=d.getNext())&&!d.getPrevious()&&j.type==CKEDITOR.NODE_ELEMENT&&j.getName()=="br"){h(this.root);b.moveToPosition(j,CKEDITOR.POSITION_BEFORE_START)}else b.moveToPosition(d,CKEDITOR.POSITION_AFTER_END)}i.setStart(b.startContainer.$,b.startOffset);try{i.setEnd(b.endContainer.$,b.endOffset)}catch(s){if(s.toString().indexOf("NS_ERROR_ILLEGAL_VALUE")>=0){b.collapse(1);i.setEnd(b.endContainer.$,
-b.endOffset)}else throw s;}p.addRange(i)}}this.reset();this.root.fire("selectionchange")}}},fake:function(a){var b=this.root.editor;this.reset();i(b);var c=this._.cache,d=new CKEDITOR.dom.range(this.root);d.setStartBefore(a);d.setEndAfter(a);c.ranges=new CKEDITOR.dom.rangeList(d);c.selectedElement=c.startElement=a;c.type=CKEDITOR.SELECTION_ELEMENT;c.selectedText=c.nativeSel=null;this.isFake=1;this.rev=p++;b._.fakeSelection=this;this.root.fire("selectionchange")},isHidden:function(){var a=this.getCommonAncestor();
-a&&a.type==CKEDITOR.NODE_TEXT&&(a=a.getParent());return!(!a||!a.data("cke-hidden-sel"))},createBookmarks:function(a){a=this.getRanges().createBookmarks(a);this.isFake&&(a.isFake=1);return a},createBookmarks2:function(a){a=this.getRanges().createBookmarks2(a);this.isFake&&(a.isFake=1);return a},selectBookmarks:function(a){for(var b=[],c=0;c<a.length;c++){var d=new CKEDITOR.dom.range(this.root);d.moveToBookmark(a[c]);b.push(d)}a.isFake?this.fake(b[0].getEnclosedNode()):this.selectRanges(b);return this},
-getCommonAncestor:function(){var a=this.getRanges();return!a.length?null:a[0].startContainer.getCommonAncestor(a[a.length-1].endContainer)},scrollIntoView:function(){this.type!=CKEDITOR.SELECTION_NONE&&this.getRanges()[0].scrollIntoView()},removeAllRanges:function(){var a=this.getNative();try{a&&a[f?"empty":"removeAllRanges"]()}catch(b){}this.reset()}}})();"use strict";
-CKEDITOR.editor.prototype.attachStyleStateChange=function(a,e){var b=this._.styleStateChangeCallbacks;if(!b){b=this._.styleStateChangeCallbacks=[];this.on("selectionChange",function(a){for(var d=0;d<b.length;d++){var e=b[d],g=e.style.checkActive(a.data.path)?CKEDITOR.TRISTATE_ON:CKEDITOR.TRISTATE_OFF;e.fn.call(this,g)}})}b.push({style:a,fn:e})};CKEDITOR.STYLE_BLOCK=1;CKEDITOR.STYLE_INLINE=2;CKEDITOR.STYLE_OBJECT=3;
-(function(){function a(a,b){for(var c,d;a=a.getParent();){if(a.equals(b))break;if(a.getAttribute("data-nostyle"))c=a;else if(!d){var e=a.getAttribute("contentEditable");e=="false"?c=a:e=="true"&&(d=1)}}return c}function e(b){var d=b.document;if(b.collapsed){d=y(this,d);b.insertNode(d);b.moveToPosition(d,CKEDITOR.POSITION_BEFORE_END)}else{var f=this.element,g=this._.definition,h,j=g.ignoreReadonly,i=j||g.includeReadonly;i==void 0&&(i=b.root.getCustomData("cke_includeReadonly"));var k=CKEDITOR.dtd[f];
-if(!k){h=true;k=CKEDITOR.dtd.span}b.enlarge(CKEDITOR.ENLARGE_INLINE,1);b.trim();var l=b.createBookmark(),m=l.startNode,o=l.endNode,n=m,q;if(!j){var p=b.getCommonAncestor(),j=a(m,p),p=a(o,p);j&&(n=j.getNextSourceNode(true));p&&(o=p)}for(n.getPosition(o)==CKEDITOR.POSITION_FOLLOWING&&(n=0);n;){j=false;if(n.equals(o)){n=null;j=true}else{var s=n.type==CKEDITOR.NODE_ELEMENT?n.getName():null,p=s&&n.getAttribute("contentEditable")=="false",r=s&&n.getAttribute("data-nostyle");if(s&&n.data("cke-bookmark")){n=
-n.getNextSourceNode(true);continue}if(p&&i&&CKEDITOR.dtd.$block[s])for(var t=n,x=c(t),z=void 0,A=x.length,I=0,t=A&&new CKEDITOR.dom.range(t.getDocument());I<A;++I){var z=x[I],L=CKEDITOR.filter.instances[z.data("cke-filter")];if(L?L.check(this):1){t.selectNodeContents(z);e.call(this,t)}}x=s?!k[s]||r?0:p&&!i?0:(n.getPosition(o)|M)==M&&(!g.childRule||g.childRule(n)):1;if(x)if((x=n.getParent())&&((x.getDtd()||CKEDITOR.dtd.span)[f]||h)&&(!g.parentRule||g.parentRule(x))){if(!q&&(!s||!CKEDITOR.dtd.$removeEmpty[s]||
-(n.getPosition(o)|M)==M)){q=b.clone();q.setStartBefore(n)}s=n.type;if(s==CKEDITOR.NODE_TEXT||p||s==CKEDITOR.NODE_ELEMENT&&!n.getChildCount()){for(var s=n,W;(j=!s.getNext(E))&&(W=s.getParent(),k[W.getName()])&&(W.getPosition(m)|Q)==Q&&(!g.childRule||g.childRule(W));)s=W;q.setEndAfter(s)}}else j=true;else j=true;n=n.getNextSourceNode(r||p)}if(j&&q&&!q.collapsed){for(var j=y(this,d),p=j.hasAttributes(),r=q.getCommonAncestor(),s={},x={},z={},A={},T,P,Z;j&&r;){if(r.getName()==f){for(T in g.attributes)if(!A[T]&&
-(Z=r.getAttribute(P)))j.getAttribute(T)==Z?x[T]=1:A[T]=1;for(P in g.styles)if(!z[P]&&(Z=r.getStyle(P)))j.getStyle(P)==Z?s[P]=1:z[P]=1}r=r.getParent()}for(T in x)j.removeAttribute(T);for(P in s)j.removeStyle(P);p&&!j.hasAttributes()&&(j=null);if(j){q.extractContents().appendTo(j);q.insertNode(j);u.call(this,j);j.mergeSiblings();CKEDITOR.env.ie||j.$.normalize()}else{j=new CKEDITOR.dom.element("span");q.extractContents().appendTo(j);q.insertNode(j);u.call(this,j);j.remove(true)}q=null}}b.moveToBookmark(l);
-b.shrink(CKEDITOR.SHRINK_TEXT);b.shrink(CKEDITOR.NODE_ELEMENT,true)}}function b(a){function b(){for(var a=new CKEDITOR.dom.elementPath(d.getParent()),c=new CKEDITOR.dom.elementPath(k.getParent()),e=null,f=null,g=0;g<a.elements.length;g++){var h=a.elements[g];if(h==a.block||h==a.blockLimit)break;m.checkElementRemovable(h)&&(e=h)}for(g=0;g<c.elements.length;g++){h=c.elements[g];if(h==c.block||h==c.blockLimit)break;m.checkElementRemovable(h)&&(f=h)}f&&k.breakParent(f);e&&d.breakParent(e)}a.enlarge(CKEDITOR.ENLARGE_INLINE,
-1);var c=a.createBookmark(),d=c.startNode;if(a.collapsed){for(var e=new CKEDITOR.dom.elementPath(d.getParent(),a.root),g,j=0,h;j<e.elements.length&&(h=e.elements[j]);j++){if(h==e.block||h==e.blockLimit)break;if(this.checkElementRemovable(h)){var i;if(a.collapsed&&(a.checkBoundaryOfElement(h,CKEDITOR.END)||(i=a.checkBoundaryOfElement(h,CKEDITOR.START)))){g=h;g.match=i?"start":"end"}else{h.mergeSiblings();h.is(this.element)?s.call(this,h):f(h,l(this)[h.getName()])}}}if(g){h=d;for(j=0;;j++){i=e.elements[j];
-if(i.equals(g))break;else if(i.match)continue;else i=i.clone();i.append(h);h=i}h[g.match=="start"?"insertBefore":"insertAfter"](g)}}else{var k=c.endNode,m=this;b();for(e=d;!e.equals(k);){g=e.getNextSourceNode();if(e.type==CKEDITOR.NODE_ELEMENT&&this.checkElementRemovable(e)){e.getName()==this.element?s.call(this,e):f(e,l(this)[e.getName()]);if(g.type==CKEDITOR.NODE_ELEMENT&&g.contains(d)){b();g=d.getNext()}}e=g}}a.moveToBookmark(c);a.shrink(CKEDITOR.NODE_ELEMENT,true)}function c(a){var b=[];a.forEach(function(a){if(a.getAttribute("contenteditable")==
-"true"){b.push(a);return false}},CKEDITOR.NODE_ELEMENT,true);return b}function d(a){var b=a.getEnclosedNode()||a.getCommonAncestor(false,true);(a=(new CKEDITOR.dom.elementPath(b,a.root)).contains(this.element,1))&&!a.isReadOnly()&&m(a,this)}function h(a){var b=a.getCommonAncestor(true,true);if(a=(new CKEDITOR.dom.elementPath(b,a.root)).contains(this.element,1)){var b=this._.definition,c=b.attributes;if(c)for(var d in c)a.removeAttribute(d,c[d]);if(b.styles)for(var e in b.styles)b.styles.hasOwnProperty(e)&&
-a.removeStyle(e)}}function g(a){var b=a.createBookmark(true),c=a.createIterator();c.enforceRealBlocks=true;if(this._.enterMode)c.enlargeBr=this._.enterMode!=CKEDITOR.ENTER_BR;for(var d,e=a.document,f;d=c.getNextParagraph();)if(!d.isReadOnly()&&(c.activeFilter?c.activeFilter.check(this):1)){f=y(this,e,d);i(d,f)}a.moveToBookmark(b)}function n(a){var b=a.createBookmark(1),c=a.createIterator();c.enforceRealBlocks=true;c.enlargeBr=this._.enterMode!=CKEDITOR.ENTER_BR;for(var d,e;d=c.getNextParagraph();)if(this.checkElementRemovable(d))if(d.is("pre")){(e=
-this._.enterMode==CKEDITOR.ENTER_BR?null:a.document.createElement(this._.enterMode==CKEDITOR.ENTER_P?"p":"div"))&&d.copyAttributes(e);i(d,e)}else s.call(this,d);a.moveToBookmark(b)}function i(a,b){var c=!b;if(c){b=a.getDocument().createElement("div");a.copyAttributes(b)}var d=b&&b.is("pre"),e=a.is("pre"),f=!d&&e;if(d&&!e){e=b;(f=a.getBogus())&&f.remove();f=a.getHtml();f=o(f,/(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g,"");f=f.replace(/[ \t\r\n]*(<br[^>]*>)[ \t\r\n]*/gi,"$1");f=f.replace(/([ \t\n\r]+|&nbsp;)/g,
-" ");f=f.replace(/<br\b[^>]*>/gi,"\n");if(CKEDITOR.env.ie){var g=a.getDocument().createElement("div");g.append(e);e.$.outerHTML="<pre>"+f+"</pre>";e.copyAttributes(g.getFirst());e=g.getFirst().remove()}else e.setHtml(f);b=e}else f?b=q(c?[a.getHtml()]:j(a),b):a.moveChildren(b);b.replace(a);if(d){var c=b,h;if((h=c.getPrevious(z))&&h.type==CKEDITOR.NODE_ELEMENT&&h.is("pre")){d=o(h.getHtml(),/\n$/,"")+"\n\n"+o(c.getHtml(),/^\n/,"");CKEDITOR.env.ie?c.$.outerHTML="<pre>"+d+"</pre>":c.setHtml(d);h.remove()}}else c&&
-p(b)}function j(a){a.getName();var b=[];o(a.getOuterHtml(),/(\S\s*)\n(?:\s|(<span[^>]+data-cke-bookmark.*?\/span>))*\n(?!$)/gi,function(a,b,c){return b+"</pre>"+c+"<pre>"}).replace(/<pre\b.*?>([\s\S]*?)<\/pre>/gi,function(a,c){b.push(c)});return b}function o(a,b,c){var d="",e="",a=a.replace(/(^<span[^>]+data-cke-bookmark.*?\/span>)|(<span[^>]+data-cke-bookmark.*?\/span>$)/gi,function(a,b,c){b&&(d=b);c&&(e=c);return""});return d+a.replace(b,c)+e}function q(a,b){var c;a.length>1&&(c=new CKEDITOR.dom.documentFragment(b.getDocument()));
-for(var d=0;d<a.length;d++){var e=a[d],e=e.replace(/(\r\n|\r)/g,"\n"),e=o(e,/^[ \t]*\n/,""),e=o(e,/\n$/,""),e=o(e,/^[ \t]+|[ \t]+$/g,function(a,b){return a.length==1?"&nbsp;":b?" "+CKEDITOR.tools.repeat("&nbsp;",a.length-1):CKEDITOR.tools.repeat("&nbsp;",a.length-1)+" "}),e=e.replace(/\n/g,"<br>"),e=e.replace(/[ \t]{2,}/g,function(a){return CKEDITOR.tools.repeat("&nbsp;",a.length-1)+" "});if(c){var f=b.clone();f.setHtml(e);c.append(f)}else b.setHtml(e)}return c||b}function s(a,b){var c=this._.definition,
-d=c.attributes,c=c.styles,e=l(this)[a.getName()],g=CKEDITOR.tools.isEmpty(d)&&CKEDITOR.tools.isEmpty(c),h;for(h in d)if(!((h=="class"||this._.definition.fullMatch)&&a.getAttribute(h)!=t(h,d[h]))&&!(b&&h.slice(0,5)=="data-")){g=a.hasAttribute(h);a.removeAttribute(h)}for(var j in c)if(!(this._.definition.fullMatch&&a.getStyle(j)!=t(j,c[j],true))){g=g||!!a.getStyle(j);a.removeStyle(j)}f(a,e,r[a.getName()]);g&&(this._.definition.alwaysRemoveElement?p(a,1):!CKEDITOR.dtd.$block[a.getName()]||this._.enterMode==
-CKEDITOR.ENTER_BR&&!a.hasAttributes()?p(a):a.renameNode(this._.enterMode==CKEDITOR.ENTER_P?"p":"div"))}function u(a){for(var b=l(this),c=a.getElementsByTag(this.element),d,e=c.count();--e>=0;){d=c.getItem(e);d.isReadOnly()||s.call(this,d,true)}for(var g in b)if(g!=this.element){c=a.getElementsByTag(g);for(e=c.count()-1;e>=0;e--){d=c.getItem(e);d.isReadOnly()||f(d,b[g])}}}function f(a,b,c){if(b=b&&b.attributes)for(var d=0;d<b.length;d++){var e=b[d][0],f;if(f=a.getAttribute(e)){var g=b[d][1];(g===null||
-g.test&&g.test(f)||typeof g=="string"&&f==g)&&a.removeAttribute(e)}}c||p(a)}function p(a,b){if(!a.hasAttributes()||b)if(CKEDITOR.dtd.$block[a.getName()]){var c=a.getPrevious(z),d=a.getNext(z);c&&(c.type==CKEDITOR.NODE_TEXT||!c.isBlockBoundary({br:1}))&&a.append("br",1);d&&(d.type==CKEDITOR.NODE_TEXT||!d.isBlockBoundary({br:1}))&&a.append("br");a.remove(true)}else{c=a.getFirst();d=a.getLast();a.remove(true);if(c){c.type==CKEDITOR.NODE_ELEMENT&&c.mergeSiblings();d&&(!c.equals(d)&&d.type==CKEDITOR.NODE_ELEMENT)&&
-d.mergeSiblings()}}}function y(a,b,c){var d;d=a.element;d=="*"&&(d="span");d=new CKEDITOR.dom.element(d,b);c&&c.copyAttributes(d);d=m(d,a);b.getCustomData("doc_processing_style")&&d.hasAttribute("id")?d.removeAttribute("id"):b.setCustomData("doc_processing_style",1);return d}function m(a,b){var c=b._.definition,d=c.attributes,c=CKEDITOR.style.getStyleText(c);if(d)for(var e in d)a.setAttribute(e,d[e]);c&&a.setAttribute("style",c);return a}function k(a,b){for(var c in a)a[c]=a[c].replace(I,function(a,
-c){return b[c]})}function l(a){if(a._.overrides)return a._.overrides;var b=a._.overrides={},c=a._.definition.overrides;if(c){CKEDITOR.tools.isArray(c)||(c=[c]);for(var d=0;d<c.length;d++){var e=c[d],f,g;if(typeof e=="string")f=e.toLowerCase();else{f=e.element?e.element.toLowerCase():a.element;g=e.attributes}e=b[f]||(b[f]={});if(g){var e=e.attributes=e.attributes||[],h;for(h in g)e.push([h.toLowerCase(),g[h]])}}}return b}function t(a,b,c){var d=new CKEDITOR.dom.element("span");d[c?"setStyle":"setAttribute"](a,
-b);return d[c?"getStyle":"getAttribute"](a)}function x(a,b){for(var c=a.document,d=a.getRanges(),e=b?this.removeFromRange:this.applyToRange,f,g=d.createIterator();f=g.getNextRange();)e.call(this,f);a.selectRanges(d);c.removeCustomData("doc_processing_style")}var r={address:1,div:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,p:1,pre:1,section:1,header:1,footer:1,nav:1,article:1,aside:1,figure:1,dialog:1,hgroup:1,time:1,meter:1,menu:1,command:1,keygen:1,output:1,progress:1,details:1,datagrid:1,datalist:1},L={a:1,
-embed:1,hr:1,img:1,li:1,object:1,ol:1,table:1,td:1,tr:1,th:1,ul:1,dl:1,dt:1,dd:1,form:1,audio:1,video:1},A=/\s*(?:;\s*|$)/,I=/#\((.+?)\)/g,E=CKEDITOR.dom.walker.bookmark(0,1),z=CKEDITOR.dom.walker.whitespaces(1);CKEDITOR.style=function(a,b){var c=a.attributes;if(c&&c.style){a.styles=CKEDITOR.tools.extend({},a.styles,CKEDITOR.tools.parseCssText(c.style));delete c.style}if(b){a=CKEDITOR.tools.clone(a);k(a.attributes,b);k(a.styles,b)}c=this.element=a.element?typeof a.element=="string"?a.element.toLowerCase():
-a.element:"*";this.type=a.type||(r[c]?CKEDITOR.STYLE_BLOCK:L[c]?CKEDITOR.STYLE_OBJECT:CKEDITOR.STYLE_INLINE);if(typeof this.element=="object")this.type=CKEDITOR.STYLE_OBJECT;this._={definition:a}};CKEDITOR.editor.prototype.applyStyle=function(a){a.checkApplicable(this.elementPath())&&x.call(a,this.getSelection())};CKEDITOR.editor.prototype.removeStyle=function(a){a.checkApplicable(this.elementPath())&&x.call(a,this.getSelection(),1)};CKEDITOR.style.prototype={apply:function(a){x.call(this,a.getSelection())},
-remove:function(a){x.call(this,a.getSelection(),1)},applyToRange:function(a){return(this.applyToRange=this.type==CKEDITOR.STYLE_INLINE?e:this.type==CKEDITOR.STYLE_BLOCK?g:this.type==CKEDITOR.STYLE_OBJECT?d:null).call(this,a)},removeFromRange:function(a){return(this.removeFromRange=this.type==CKEDITOR.STYLE_INLINE?b:this.type==CKEDITOR.STYLE_BLOCK?n:this.type==CKEDITOR.STYLE_OBJECT?h:null).call(this,a)},applyToObject:function(a){m(a,this)},checkActive:function(a){switch(this.type){case CKEDITOR.STYLE_BLOCK:return this.checkElementRemovable(a.block||
-a.blockLimit,true);case CKEDITOR.STYLE_OBJECT:case CKEDITOR.STYLE_INLINE:for(var b=a.elements,c=0,d;c<b.length;c++){d=b[c];if(!(this.type==CKEDITOR.STYLE_INLINE&&(d==a.block||d==a.blockLimit))){if(this.type==CKEDITOR.STYLE_OBJECT){var e=d.getName();if(!(typeof this.element=="string"?e==this.element:e in this.element))continue}if(this.checkElementRemovable(d,true))return true}}}return false},checkApplicable:function(a,b){if(b&&!b.check(this))return false;switch(this.type){case CKEDITOR.STYLE_OBJECT:return!!a.contains(this.element);
-case CKEDITOR.STYLE_BLOCK:return!!a.blockLimit.getDtd()[this.element]}return true},checkElementMatch:function(a,b){var c=this._.definition;if(!a||!c.ignoreReadonly&&a.isReadOnly())return false;var d=a.getName();if(typeof this.element=="string"?d==this.element:d in this.element){if(!b&&!a.hasAttributes())return true;if(d=c._AC)c=d;else{var d={},e=0,f=c.attributes;if(f)for(var g in f){e++;d[g]=f[g]}if(g=CKEDITOR.style.getStyleText(c)){d.style||e++;d.style=g}d._length=e;c=c._AC=d}if(c._length){for(var h in c)if(h!=
-"_length"){e=a.getAttribute(h)||"";if(h=="style")a:{d=c[h];typeof d=="string"&&(d=CKEDITOR.tools.parseCssText(d));typeof e=="string"&&(e=CKEDITOR.tools.parseCssText(e,true));g=void 0;for(g in d)if(!(g in e&&(e[g]==d[g]||d[g]=="inherit"||e[g]=="inherit"))){d=false;break a}d=true}else d=c[h]==e;if(d){if(!b)return true}else if(b)return false}if(b)return true}else return true}return false},checkElementRemovable:function(a,b){if(this.checkElementMatch(a,b))return true;var c=l(this)[a.getName()];if(c){var d;
-if(!(c=c.attributes))return true;for(var e=0;e<c.length;e++){d=c[e][0];if(d=a.getAttribute(d)){var f=c[e][1];if(f===null||typeof f=="string"&&d==f||f.test(d))return true}}}return false},buildPreview:function(a){var b=this._.definition,c=[],d=b.element;d=="bdo"&&(d="span");var c=["<",d],e=b.attributes;if(e)for(var f in e)c.push(" ",f,'="',e[f],'"');(e=CKEDITOR.style.getStyleText(b))&&c.push(' style="',e,'"');c.push(">",a||b.name,"</",d,">");return c.join("")},getDefinition:function(){return this._.definition}};
-CKEDITOR.style.getStyleText=function(a){var b=a._ST;if(b)return b;var b=a.styles,c=a.attributes&&a.attributes.style||"",d="";c.length&&(c=c.replace(A,";"));for(var e in b){var f=b[e],g=(e+":"+f).replace(A,";");f=="inherit"?d=d+g:c=c+g}c.length&&(c=CKEDITOR.tools.normalizeCssText(c,true));return a._ST=c+d};var M=CKEDITOR.POSITION_PRECEDING|CKEDITOR.POSITION_IDENTICAL|CKEDITOR.POSITION_IS_CONTAINED,Q=CKEDITOR.POSITION_FOLLOWING|CKEDITOR.POSITION_IDENTICAL|CKEDITOR.POSITION_IS_CONTAINED})();
-CKEDITOR.styleCommand=function(a,e){this.requiredContent=this.allowedContent=this.style=a;CKEDITOR.tools.extend(this,e,true)};CKEDITOR.styleCommand.prototype.exec=function(a){a.focus();this.state==CKEDITOR.TRISTATE_OFF?a.applyStyle(this.style):this.state==CKEDITOR.TRISTATE_ON&&a.removeStyle(this.style)};CKEDITOR.stylesSet=new CKEDITOR.resourceManager("","stylesSet");CKEDITOR.addStylesSet=CKEDITOR.tools.bind(CKEDITOR.stylesSet.add,CKEDITOR.stylesSet);
-CKEDITOR.loadStylesSet=function(a,e,b){CKEDITOR.stylesSet.addExternal(a,e,"");CKEDITOR.stylesSet.load(a,b)};
-CKEDITOR.editor.prototype.getStylesSet=function(a){if(this._.stylesDefinitions)a(this._.stylesDefinitions);else{var e=this,b=e.config.stylesCombo_stylesSet||e.config.stylesSet;if(b===false)a(null);else if(b instanceof Array){e._.stylesDefinitions=b;a(b)}else{b||(b="default");var b=b.split(":"),c=b[0];CKEDITOR.stylesSet.addExternal(c,b[1]?b.slice(1).join(":"):CKEDITOR.getUrl("styles.js"),"");CKEDITOR.stylesSet.load(c,function(b){e._.stylesDefinitions=b[c];a(e._.stylesDefinitions)})}}};
-CKEDITOR.dom.comment=function(a,e){typeof a=="string"&&(a=(e?e.$:document).createComment(a));CKEDITOR.dom.domObject.call(this,a)};CKEDITOR.dom.comment.prototype=new CKEDITOR.dom.node;CKEDITOR.tools.extend(CKEDITOR.dom.comment.prototype,{type:CKEDITOR.NODE_COMMENT,getOuterHtml:function(){return"<\!--"+this.$.nodeValue+"--\>"}});"use strict";
-(function(){var a={},e={},b;for(b in CKEDITOR.dtd.$blockLimit)b in CKEDITOR.dtd.$list||(a[b]=1);for(b in CKEDITOR.dtd.$block)b in CKEDITOR.dtd.$blockLimit||b in CKEDITOR.dtd.$empty||(e[b]=1);CKEDITOR.dom.elementPath=function(b,d){var h=null,g=null,n=[],i=b,j,d=d||b.getDocument().getBody();do if(i.type==CKEDITOR.NODE_ELEMENT){n.push(i);if(!this.lastElement){this.lastElement=i;if(i.is(CKEDITOR.dtd.$object)||i.getAttribute("contenteditable")=="false")continue}if(i.equals(d))break;if(!g){j=i.getName();
-i.getAttribute("contenteditable")=="true"?g=i:!h&&e[j]&&(h=i);if(a[j]){var o;if(o=!h){if(j=j=="div"){a:{j=i.getChildren();o=0;for(var q=j.count();o<q;o++){var s=j.getItem(o);if(s.type==CKEDITOR.NODE_ELEMENT&&CKEDITOR.dtd.$block[s.getName()]){j=true;break a}}j=false}j=!j}o=j}o?h=i:g=i}}}while(i=i.getParent());g||(g=d);this.block=h;this.blockLimit=g;this.root=d;this.elements=n}})();
-CKEDITOR.dom.elementPath.prototype={compare:function(a){var e=this.elements,a=a&&a.elements;if(!a||e.length!=a.length)return false;for(var b=0;b<e.length;b++)if(!e[b].equals(a[b]))return false;return true},contains:function(a,e,b){var c;typeof a=="string"&&(c=function(b){return b.getName()==a});a instanceof CKEDITOR.dom.element?c=function(b){return b.equals(a)}:CKEDITOR.tools.isArray(a)?c=function(b){return CKEDITOR.tools.indexOf(a,b.getName())>-1}:typeof a=="function"?c=a:typeof a=="object"&&(c=
-function(b){return b.getName()in a});var d=this.elements,h=d.length;e&&h--;if(b){d=Array.prototype.slice.call(d,0);d.reverse()}for(e=0;e<h;e++)if(c(d[e]))return d[e];return null},isContextFor:function(a){var e;if(a in CKEDITOR.dtd.$block){e=this.contains(CKEDITOR.dtd.$intermediate)||this.root.equals(this.block)&&this.block||this.blockLimit;return!!e.getDtd()[a]}return true},direction:function(){return(this.block||this.blockLimit||this.root).getDirection(1)}};
-CKEDITOR.dom.text=function(a,e){typeof a=="string"&&(a=(e?e.$:document).createTextNode(a));this.$=a};CKEDITOR.dom.text.prototype=new CKEDITOR.dom.node;
-CKEDITOR.tools.extend(CKEDITOR.dom.text.prototype,{type:CKEDITOR.NODE_TEXT,getLength:function(){return this.$.nodeValue.length},getText:function(){return this.$.nodeValue},setText:function(a){this.$.nodeValue=a},split:function(a){var e=this.$.parentNode,b=e.childNodes.length,c=this.getLength(),d=this.getDocument(),h=new CKEDITOR.dom.text(this.$.splitText(a),d);if(e.childNodes.length==b)if(a>=c){h=d.createText("");h.insertAfter(this)}else{a=d.createText("");a.insertAfter(h);a.remove()}return h},substring:function(a,
-e){return typeof e!="number"?this.$.nodeValue.substr(a):this.$.nodeValue.substring(a,e)}});
-(function(){function a(a,c,d){var e=a.serializable,g=c[d?"endContainer":"startContainer"],n=d?"endOffset":"startOffset",i=e?c.document.getById(a.startNode):a.startNode,a=e?c.document.getById(a.endNode):a.endNode;if(g.equals(i.getPrevious())){c.startOffset=c.startOffset-g.getLength()-a.getPrevious().getLength();g=a.getNext()}else if(g.equals(a.getPrevious())){c.startOffset=c.startOffset-g.getLength();g=a.getNext()}g.equals(i.getParent())&&c[n]++;g.equals(a.getParent())&&c[n]++;c[d?"endContainer":"startContainer"]=
-g;return c}CKEDITOR.dom.rangeList=function(a){if(a instanceof CKEDITOR.dom.rangeList)return a;a?a instanceof CKEDITOR.dom.range&&(a=[a]):a=[];return CKEDITOR.tools.extend(a,e)};var e={createIterator:function(){var a=this,c=CKEDITOR.dom.walker.bookmark(),d=[],e;return{getNextRange:function(g){e=e==void 0?0:e+1;var n=a[e];if(n&&a.length>1){if(!e)for(var i=a.length-1;i>=0;i--)d.unshift(a[i].createBookmark(true));if(g)for(var j=0;a[e+j+1];){for(var o=n.document,g=0,i=o.getById(d[j].endNode),o=o.getById(d[j+
-1].startNode);;){i=i.getNextSourceNode(false);if(o.equals(i))g=1;else if(c(i)||i.type==CKEDITOR.NODE_ELEMENT&&i.isBlockBoundary())continue;break}if(!g)break;j++}for(n.moveToBookmark(d.shift());j--;){i=a[++e];i.moveToBookmark(d.shift());n.setEnd(i.endContainer,i.endOffset)}}return n}}},createBookmarks:function(b){for(var c=[],d,e=0;e<this.length;e++){c.push(d=this[e].createBookmark(b,true));for(var g=e+1;g<this.length;g++){this[g]=a(d,this[g]);this[g]=a(d,this[g],true)}}return c},createBookmarks2:function(a){for(var c=
-[],d=0;d<this.length;d++)c.push(this[d].createBookmark2(a));return c},moveToBookmarks:function(a){for(var c=0;c<this.length;c++)this[c].moveToBookmark(a[c])}}})();
-(function(){function a(){return CKEDITOR.getUrl(CKEDITOR.skinName.split(",")[1]||"skins/"+CKEDITOR.skinName.split(",")[0]+"/")}function e(b){var c=CKEDITOR.skin["ua_"+b],d=CKEDITOR.env;if(c)for(var c=c.split(",").sort(function(a,b){return a>b?-1:1}),e=0,g;e<c.length;e++){g=c[e];if(d.ie&&(g.replace(/^ie/,"")==d.version||d.quirks&&g=="iequirks"))g="ie";if(d[g]){b=b+("_"+c[e]);break}}return CKEDITOR.getUrl(a()+b+".css")}function b(a,b){if(!h[a]){CKEDITOR.document.appendStyleSheet(e(a));h[a]=1}b&&b()}
-function c(a){var b=a.getById(g);if(!b){b=a.getHead().append("style");b.setAttribute("id",g);b.setAttribute("type","text/css")}return b}function d(a,b,c){var d,e,f;if(CKEDITOR.env.webkit){b=b.split("}").slice(0,-1);for(e=0;e<b.length;e++)b[e]=b[e].split("{")}for(var g=0;g<a.length;g++)if(CKEDITOR.env.webkit)for(e=0;e<b.length;e++){f=b[e][1];for(d=0;d<c.length;d++)f=f.replace(c[d][0],c[d][1]);a[g].$.sheet.addRule(b[e][0],f)}else{f=b;for(d=0;d<c.length;d++)f=f.replace(c[d][0],c[d][1]);CKEDITOR.env.ie&&
-CKEDITOR.env.version<11?a[g].$.styleSheet.cssText=a[g].$.styleSheet.cssText+f:a[g].$.innerHTML=a[g].$.innerHTML+f}}var h={};CKEDITOR.skin={path:a,loadPart:function(c,d){CKEDITOR.skin.name!=CKEDITOR.skinName.split(",")[0]?CKEDITOR.scriptLoader.load(CKEDITOR.getUrl(a()+"skin.js"),function(){b(c,d)}):b(c,d)},getPath:function(a){return CKEDITOR.getUrl(e(a))},icons:{},addIcon:function(a,b,c,d){a=a.toLowerCase();this.icons[a]||(this.icons[a]={path:b,offset:c||0,bgsize:d||"16px"})},getIconStyle:function(a,
-b,c,d,e){var f;if(a){a=a.toLowerCase();b&&(f=this.icons[a+"-rtl"]);f||(f=this.icons[a])}a=c||f&&f.path||"";d=d||f&&f.offset;e=e||f&&f.bgsize||"16px";return a&&"background-image:url("+CKEDITOR.getUrl(a)+");background-position:0 "+d+"px;background-size:"+e+";"}};CKEDITOR.tools.extend(CKEDITOR.editor.prototype,{getUiColor:function(){return this.uiColor},setUiColor:function(a){var b=c(CKEDITOR.document);return(this.setUiColor=function(a){var c=CKEDITOR.skin.chameleon,e=[[i,a]];this.uiColor=a;d([b],c(this,
-"editor"),e);d(n,c(this,"panel"),e)}).call(this,a)}});var g="cke_ui_color",n=[],i=/\$color/g;CKEDITOR.on("instanceLoaded",function(a){if(!CKEDITOR.env.ie||!CKEDITOR.env.quirks){var b=a.editor,a=function(a){a=(a.data[0]||a.data).element.getElementsByTag("iframe").getItem(0).getFrameDocument();if(!a.getById("cke_ui_color")){a=c(a);n.push(a);var e=b.getUiColor();e&&d([a],CKEDITOR.skin.chameleon(b,"panel"),[[i,e]])}};b.on("panelShow",a);b.on("menuShow",a);b.config.uiColor&&b.setUiColor(b.config.uiColor)}})})();
-(function(){if(CKEDITOR.env.webkit)CKEDITOR.env.hc=false;else{var a=CKEDITOR.dom.element.createFromHtml('<div style="width:0px;height:0px;position:absolute;left:-10000px;border: 1px solid;border-color: red blue;"></div>',CKEDITOR.document);a.appendTo(CKEDITOR.document.getHead());try{CKEDITOR.env.hc=a.getComputedStyle("border-top-color")==a.getComputedStyle("border-right-color")}catch(e){CKEDITOR.env.hc=false}a.remove()}if(CKEDITOR.env.hc)CKEDITOR.env.cssClass=CKEDITOR.env.cssClass+" cke_hc";CKEDITOR.document.appendStyleText(".cke{visibility:hidden;}");
-CKEDITOR.status="loaded";CKEDITOR.fireOnce("loaded");if(a=CKEDITOR._.pending){delete CKEDITOR._.pending;for(var b=0;b<a.length;b++){CKEDITOR.editor.prototype.constructor.apply(a[b][0],a[b][1]);CKEDITOR.add(a[b][0])}}})();/*
- Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
+(function(){CKEDITOR.inline=function(a,d){if(!CKEDITOR.env.isCompatible)return null;a=CKEDITOR.dom.element.get(a);if(a.getEditor())throw'The editor instance "'+a.getEditor().name+'" is already attached to the provided element.';var b=new CKEDITOR.editor(d,a,CKEDITOR.ELEMENT_MODE_INLINE),c=a.is("textarea")?a:null;c?(b.setData(c.getValue(),null,!0),a=CKEDITOR.dom.element.createFromHtml('\x3cdiv contenteditable\x3d"'+!!b.readOnly+'" class\x3d"cke_textarea_inline"\x3e'+c.getValue()+"\x3c/div\x3e",CKEDITOR.document),(d.width&&a.setStyle('width',CKEDITOR.tools.cssLength(d.width))),(d.height&&a.setStyle('height',CKEDITOR.tools.cssLength(d.height))),
+a.insertAfter(c),c.hide(),c.$.form&&b._attachToForm()):b.setData(a.getHtml(),null,!0);b.on("loaded",function(){b.fire("uiReady");b.editable(a);b.container=a;b.ui.contentsElement=a;b.setData(b.getData(1));b.resetDirty();b.fire("contentDom");b.mode="wysiwyg";b.fire("mode");b.status="ready";b.fireOnce("instanceReady");CKEDITOR.fire("instanceReady",null,b)},null,null,1E4);b.on("destroy",function(){c&&(b.container.clearCustomData(),b.container.remove(),c.show());b.element.clearCustomData();delete b.element});
+return b};CKEDITOR.inlineAll=function(){var a,d,b;for(b in CKEDITOR.dtd.$editable)for(var c=CKEDITOR.document.getElementsByTag(b),e=0,g=c.count();e<g;e++)a=c.getItem(e),"true"==a.getAttribute("contenteditable")&&(d={element:a,config:{}},!1!==CKEDITOR.fire("inline",d)&&CKEDITOR.inline(a,d.config))};CKEDITOR.domReady(function(){!CKEDITOR.disableAutoInline&&CKEDITOR.inlineAll()})})();CKEDITOR.replaceClass="ckeditor";
+(function(){function a(a,e,g,h){if(!CKEDITOR.env.isCompatible)return null;a=CKEDITOR.dom.element.get(a);if(a.getEditor())throw'The editor instance "'+a.getEditor().name+'" is already attached to the provided element.';var k=new CKEDITOR.editor(e,a,h);h==CKEDITOR.ELEMENT_MODE_REPLACE&&(a.setStyle("visibility","hidden"),k._.required=a.hasAttribute("required"),a.removeAttribute("required"));g&&k.setData(g,null,!0);k.on("loaded",function(){b(k);h==CKEDITOR.ELEMENT_MODE_REPLACE&&k.config.autoUpdateElement&&
+a.$.form&&k._attachToForm();k.setMode(k.config.startupMode,function(){k.resetDirty();k.status="ready";k.fireOnce("instanceReady");CKEDITOR.fire("instanceReady",null,k)})});k.on("destroy",d);return k}function d(){var a=this.container,b=this.element;a&&(a.clearCustomData(),a.remove());b&&(b.clearCustomData(),this.elementMode==CKEDITOR.ELEMENT_MODE_REPLACE&&(b.show(),this._.required&&b.setAttribute("required","required")),delete this.element)}function b(a){var b=a.name,d=a.element,h=a.elementMode,k=
+a.fire("uiSpace",{space:"top",html:""}).html,n=a.fire("uiSpace",{space:"bottom",html:""}).html,u=new CKEDITOR.template('\x3c{outerEl} id\x3d"cke_{name}" class\x3d"{id} cke cke_reset cke_chrome cke_editor_{name} cke_{langDir} '+CKEDITOR.env.cssClass+'"  dir\x3d"{langDir}" lang\x3d"{langCode}" role\x3d"application"'+(a.title?' aria-labelledby\x3d"cke_{name}_arialbl"':"")+"\x3e"+(a.title?'\x3cspan id\x3d"cke_{name}_arialbl" class\x3d"cke_voice_label"\x3e{voiceLabel}\x3c/span\x3e':"")+'\x3c{outerEl} class\x3d"cke_inner cke_reset" role\x3d"presentation"\x3e{topHtml}\x3c{outerEl} id\x3d"{contentId}" class\x3d"cke_contents cke_reset" role\x3d"presentation"\x3e\x3c/{outerEl}\x3e{bottomHtml}\x3c/{outerEl}\x3e\x3c/{outerEl}\x3e'),
+b=CKEDITOR.dom.element.createFromHtml(u.output({id:a.id,name:b,langDir:a.lang.dir,langCode:a.langCode,voiceLabel:a.title,topHtml:k?'\x3cspan id\x3d"'+a.ui.spaceId("top")+'" class\x3d"cke_top cke_reset_all" role\x3d"presentation" style\x3d"height:auto"\x3e'+k+"\x3c/span\x3e":"",contentId:a.ui.spaceId("contents"),bottomHtml:n?'\x3cspan id\x3d"'+a.ui.spaceId("bottom")+'" class\x3d"cke_bottom cke_reset_all" role\x3d"presentation"\x3e'+n+"\x3c/span\x3e":"",outerEl:CKEDITOR.env.ie?"span":"div"}));h==CKEDITOR.ELEMENT_MODE_REPLACE?
+(d.hide(),b.insertAfter(d)):d.append(b);a.container=b;a.ui.contentsElement=a.ui.space("contents");k&&a.ui.space("top").unselectable();n&&a.ui.space("bottom").unselectable();d=a.config.width;h=a.config.height;d&&b.setStyle("width",CKEDITOR.tools.cssLength(d));h&&a.ui.space("contents").setStyle("height",CKEDITOR.tools.cssLength(h));b.disableContextMenu();CKEDITOR.env.webkit&&b.on("focus",function(){a.focus()});a.fireOnce("uiReady")}CKEDITOR.replace=function(b,d){return a(b,d,null,CKEDITOR.ELEMENT_MODE_REPLACE)};
+CKEDITOR.appendTo=function(b,d,g){return a(b,d,g,CKEDITOR.ELEMENT_MODE_APPENDTO)};CKEDITOR.replaceAll=function(){for(var a=document.getElementsByTagName("textarea"),b=0;b<a.length;b++){var d=null,h=a[b];if(h.name||h.id){if("string"==typeof arguments[0]){if(!(new RegExp("(?:^|\\s)"+arguments[0]+"(?:$|\\s)")).test(h.className))continue}else if("function"==typeof arguments[0]&&(d={},!1===arguments[0](h,d)))continue;this.replace(h,d)}}};CKEDITOR.editor.prototype.addMode=function(a,b){(this._.modes||(this._.modes=
+{}))[a]=b};CKEDITOR.editor.prototype.setMode=function(a,b){var d=this,h=this._.modes;if(a!=d.mode&&h&&h[a]){d.fire("beforeSetMode",a);if(d.mode){var k=d.checkDirty(),h=d._.previousModeData,n,u=0;d.fire("beforeModeUnload");d.editable(0);d._.previousMode=d.mode;d._.previousModeData=n=d.getData(1);"source"==d.mode&&h==n&&(d.fire("lockSnapshot",{forceUpdate:!0}),u=1);d.ui.space("contents").setHtml("");d.mode=""}else d._.previousModeData=d.getData(1);this._.modes[a](function(){d.mode=a;void 0!==k&&!k&&
+d.resetDirty();u?d.fire("unlockSnapshot"):"wysiwyg"==a&&d.fire("saveSnapshot");setTimeout(function(){d.fire("mode");b&&b.call(d)},0)})}};CKEDITOR.editor.prototype.resize=function(a,b,d,h){var k=this.container,n=this.ui.space("contents"),u=CKEDITOR.env.webkit&&this.document&&this.document.getWindow().$.frameElement;h=h?this.container.getFirst(function(a){return a.type==CKEDITOR.NODE_ELEMENT&&a.hasClass("cke_inner")}):k;h.setSize("width",a,!0);u&&(u.style.width="1%");var f=(h.$.offsetHeight||0)-(n.$.clientHeight||
+0),k=Math.max(b-(d?0:f),0);b=d?b+f:b;n.setStyle("height",k+"px");u&&(u.style.width="100%");this.fire("resize",{outerHeight:b,contentsHeight:k,outerWidth:a||h.getSize("width")})};CKEDITOR.editor.prototype.getResizable=function(a){return a?this.ui.space("contents"):this.container};CKEDITOR.domReady(function(){CKEDITOR.replaceClass&&CKEDITOR.replaceAll(CKEDITOR.replaceClass)})})();CKEDITOR.config.startupMode="wysiwyg";
+(function(){function a(a){var b=a.editor,e=a.data.path,f=e.blockLimit,g=a.data.selection,q=g.getRanges()[0],y;if(CKEDITOR.env.gecko||CKEDITOR.env.ie&&CKEDITOR.env.needsBrFiller)if(g=d(g,e))g.appendBogus(),y=CKEDITOR.env.ie;h(b,e.block,f)&&q.collapsed&&!q.getCommonAncestor().isReadOnly()&&(e=q.clone(),e.enlarge(CKEDITOR.ENLARGE_BLOCK_CONTENTS),f=new CKEDITOR.dom.walker(e),f.guard=function(a){return!c(a)||a.type==CKEDITOR.NODE_COMMENT||a.isReadOnly()},!f.checkForward()||e.checkStartOfBlock()&&e.checkEndOfBlock())&&
+(b=q.fixBlock(!0,b.activeEnterMode==CKEDITOR.ENTER_DIV?"div":"p"),CKEDITOR.env.needsBrFiller||(b=b.getFirst(c))&&b.type==CKEDITOR.NODE_TEXT&&CKEDITOR.tools.trim(b.getText()).match(/^(?:&nbsp;|\xa0)$/)&&b.remove(),y=1,a.cancel());y&&q.select()}function d(a,b){if(a.isFake)return 0;var d=b.block||b.blockLimit,e=d&&d.getLast(c);if(!(!d||!d.isBlockBoundary()||e&&e.type==CKEDITOR.NODE_ELEMENT&&e.isBlockBoundary()||d.is("pre")||d.getBogus()))return d}function b(a){var b=a.data.getTarget();b.is("input")&&
+(b=b.getAttribute("type"),"submit"!=b&&"reset"!=b||a.data.preventDefault())}function c(a){return f(a)&&D(a)}function e(a,b){return function(c){var d=c.data.$.toElement||c.data.$.fromElement||c.data.$.relatedTarget;(d=d&&d.nodeType==CKEDITOR.NODE_ELEMENT?new CKEDITOR.dom.element(d):null)&&(b.equals(d)||b.contains(d))||a.call(this,c)}}function g(a){function b(a){return function(b,e){e&&b.type==CKEDITOR.NODE_ELEMENT&&b.is(f)&&(d=b);if(!(e||!c(b)||a&&A(b)))return!1}}var d,e=a.getRanges()[0];a=a.root;
+var f={table:1,ul:1,ol:1,dl:1};if(e.startPath().contains(f)){var q=e.clone();q.collapse(1);q.setStartAt(a,CKEDITOR.POSITION_AFTER_START);a=new CKEDITOR.dom.walker(q);a.guard=b();a.checkBackward();if(d)return q=e.clone(),q.collapse(),q.setEndAt(d,CKEDITOR.POSITION_AFTER_END),a=new CKEDITOR.dom.walker(q),a.guard=b(!0),d=!1,a.checkForward(),d}return null}function h(a,b,c){return!1!==a.config.autoParagraph&&a.activeEnterMode!=CKEDITOR.ENTER_BR&&(a.editable().equals(c)&&!b||b&&"true"==b.getAttribute("contenteditable"))}
+function k(a){return a.activeEnterMode!=CKEDITOR.ENTER_BR&&!1!==a.config.autoParagraph?a.activeEnterMode==CKEDITOR.ENTER_DIV?"div":"p":!1}function n(a){var b=a.editor;b.getSelection().scrollIntoView();setTimeout(function(){b.fire("saveSnapshot")},0)}function u(a,b,c){var d=a.getCommonAncestor(b);for(b=a=c?b:a;(a=a.getParent())&&!d.equals(a)&&1==a.getChildCount();)b=a;b.remove()}var f,D,w,A,F,x,m,K,z,I;CKEDITOR.editable=CKEDITOR.tools.createClass({base:CKEDITOR.dom.element,$:function(a,b){this.base(b.$||
+b);this.editor=a;this.status="unloaded";this.hasFocus=!1;this.setup()},proto:{focus:function(){var a;if(CKEDITOR.env.webkit&&!this.hasFocus&&(a=this.editor._.previousActive||this.getDocument().getActive(),this.contains(a))){a.focus();return}CKEDITOR.env.edge&&14<CKEDITOR.env.version&&!this.hasFocus&&this.getDocument().equals(CKEDITOR.document)&&(this.editor._.previousScrollTop=this.$.scrollTop);try{if(!CKEDITOR.env.ie||CKEDITOR.env.edge&&14<CKEDITOR.env.version||!this.getDocument().equals(CKEDITOR.document))if(CKEDITOR.env.chrome){var b=
+this.$.scrollTop;this.$.focus();this.$.scrollTop=b}else this.$.focus();else this.$.setActive()}catch(c){if(!CKEDITOR.env.ie)throw c;}CKEDITOR.env.safari&&!this.isInline()&&(a=CKEDITOR.document.getActive(),a.equals(this.getWindow().getFrame())||this.getWindow().focus())},on:function(a,b){var c=Array.prototype.slice.call(arguments,0);CKEDITOR.env.ie&&/^focus|blur$/.exec(a)&&(a="focus"==a?"focusin":"focusout",b=e(b,this),c[0]=a,c[1]=b);return CKEDITOR.dom.element.prototype.on.apply(this,c)},attachListener:function(a){!this._.listeners&&
+(this._.listeners=[]);var b=Array.prototype.slice.call(arguments,1),b=a.on.apply(a,b);this._.listeners.push(b);return b},clearListeners:function(){var a=this._.listeners;try{for(;a.length;)a.pop().removeListener()}catch(b){}},restoreAttrs:function(){var a=this._.attrChanges,b,c;for(c in a)a.hasOwnProperty(c)&&(b=a[c],null!==b?this.setAttribute(c,b):this.removeAttribute(c))},attachClass:function(a){var b=this.getCustomData("classes");this.hasClass(a)||(!b&&(b=[]),b.push(a),this.setCustomData("classes",
+b),this.addClass(a))},changeAttr:function(a,b){var c=this.getAttribute(a);b!==c&&(!this._.attrChanges&&(this._.attrChanges={}),a in this._.attrChanges||(this._.attrChanges[a]=c),this.setAttribute(a,b))},insertText:function(a){this.editor.focus();this.insertHtml(this.transformPlainTextToHtml(a),"text")},transformPlainTextToHtml:function(a){var b=this.editor.getSelection().getStartElement().hasAscendant("pre",!0)?CKEDITOR.ENTER_BR:this.editor.activeEnterMode;return CKEDITOR.tools.transformPlainTextToHtml(a,
+b)},insertHtml:function(a,b,c){var d=this.editor;d.focus();d.fire("saveSnapshot");c||(c=d.getSelection().getRanges()[0]);x(this,b||"html",a,c);c.select();n(this);this.editor.fire("afterInsertHtml",{})},insertHtmlIntoRange:function(a,b,c){x(this,c||"html",a,b);this.editor.fire("afterInsertHtml",{intoRange:b})},insertElement:function(a,b){var d=this.editor;d.focus();d.fire("saveSnapshot");var e=d.activeEnterMode,d=d.getSelection(),f=a.getName(),f=CKEDITOR.dtd.$block[f];b||(b=d.getRanges()[0]);this.insertElementIntoRange(a,
+b)&&(b.moveToPosition(a,CKEDITOR.POSITION_AFTER_END),f&&((f=a.getNext(function(a){return c(a)&&!A(a)}))&&f.type==CKEDITOR.NODE_ELEMENT&&f.is(CKEDITOR.dtd.$block)?f.getDtd()["#"]?b.moveToElementEditStart(f):b.moveToElementEditEnd(a):f||e==CKEDITOR.ENTER_BR||(f=b.fixBlock(!0,e==CKEDITOR.ENTER_DIV?"div":"p"),b.moveToElementEditStart(f))));d.selectRanges([b]);n(this)},insertElementIntoSelection:function(a){this.insertElement(a)},insertElementIntoRange:function(a,b){var c=this.editor,d=c.config.enterMode,
+e=a.getName(),f=CKEDITOR.dtd.$block[e];if(b.checkReadOnly())return!1;b.deleteContents(1);b.startContainer.type==CKEDITOR.NODE_ELEMENT&&(b.startContainer.is({tr:1,table:1,tbody:1,thead:1,tfoot:1})?m(b):b.startContainer.is(CKEDITOR.dtd.$list)&&K(b));var g,v;if(f)for(;(g=b.getCommonAncestor(0,1))&&(v=CKEDITOR.dtd[g.getName()])&&(!v||!v[e]);)g.getName()in CKEDITOR.dtd.span?b.splitElement(g):b.checkStartOfBlock()&&b.checkEndOfBlock()?(b.setStartBefore(g),b.collapse(!0),g.remove()):b.splitBlock(d==CKEDITOR.ENTER_DIV?
+"div":"p",c.editable());b.insertNode(a);return!0},setData:function(a,b){b||(a=this.editor.dataProcessor.toHtml(a));this.setHtml(a);this.fixInitialSelection();"unloaded"==this.status&&(this.status="ready");this.editor.fire("dataReady")},getData:function(a){var b=this.getHtml();a||(b=this.editor.dataProcessor.toDataFormat(b));return b},setReadOnly:function(a){this.setAttribute("contenteditable",!a)},detach:function(){this.removeClass("cke_editable");this.status="detached";var a=this.editor;this._.detach();
+delete a.document;delete a.window},isInline:function(){return this.getDocument().equals(CKEDITOR.document)},fixInitialSelection:function(){function a(){var b=c.getDocument().$,d=b.getSelection(),e;a:if(d.anchorNode&&d.anchorNode==c.$)e=!0;else{if(CKEDITOR.env.webkit&&(e=c.getDocument().getActive())&&e.equals(c)&&!d.anchorNode){e=!0;break a}e=void 0}e&&(e=new CKEDITOR.dom.range(c),e.moveToElementEditStart(c),b=b.createRange(),b.setStart(e.startContainer.$,e.startOffset),b.collapse(!0),d.removeAllRanges(),
+d.addRange(b))}function b(){var a=c.getDocument().$,d=a.selection,e=c.getDocument().getActive();"None"==d.type&&e.equals(c)&&(d=new CKEDITOR.dom.range(c),a=a.body.createTextRange(),d.moveToElementEditStart(c),d=d.startContainer,d.type!=CKEDITOR.NODE_ELEMENT&&(d=d.getParent()),a.moveToElementText(d.$),a.collapse(!0),a.select())}var c=this;if(CKEDITOR.env.ie&&(9>CKEDITOR.env.version||CKEDITOR.env.quirks))this.hasFocus&&(this.focus(),b());else if(this.hasFocus)this.focus(),a();else this.once("focus",
+function(){a()},null,null,-999)},getHtmlFromRange:function(a){if(a.collapsed)return new CKEDITOR.dom.documentFragment(a.document);a={doc:this.getDocument(),range:a.clone()};z.eol.detect(a,this);z.bogus.exclude(a);z.cell.shrink(a);a.fragment=a.range.cloneContents();z.tree.rebuild(a,this);z.eol.fix(a,this);return new CKEDITOR.dom.documentFragment(a.fragment.$)},extractHtmlFromRange:function(a,b){var c=I,d={range:a,doc:a.document},e=this.getHtmlFromRange(a);if(a.collapsed)return a.optimize(),e;a.enlarge(CKEDITOR.ENLARGE_INLINE,
+1);c.table.detectPurge(d);d.bookmark=a.createBookmark();delete d.range;var f=this.editor.createRange();f.moveToPosition(d.bookmark.startNode,CKEDITOR.POSITION_BEFORE_START);d.targetBookmark=f.createBookmark();c.list.detectMerge(d,this);c.table.detectRanges(d,this);c.block.detectMerge(d,this);d.tableContentsRanges?(c.table.deleteRanges(d),a.moveToBookmark(d.bookmark),d.range=a):(a.moveToBookmark(d.bookmark),d.range=a,a.extractContents(c.detectExtractMerge(d)));a.moveToBookmark(d.targetBookmark);a.optimize();
+c.fixUneditableRangePosition(a);c.list.merge(d,this);c.table.purge(d,this);c.block.merge(d,this);if(b){c=a.startPath();if(d=a.checkStartOfBlock()&&a.checkEndOfBlock()&&c.block&&!a.root.equals(c.block)){a:{var d=c.block.getElementsByTag("span"),f=0,g;if(d)for(;g=d.getItem(f++);)if(!D(g)){d=!0;break a}d=!1}d=!d}d&&(a.moveToPosition(c.block,CKEDITOR.POSITION_BEFORE_START),c.block.remove())}else c.autoParagraph(this.editor,a),w(a.startContainer)&&a.startContainer.appendBogus();a.startContainer.mergeSiblings();
+return e},setup:function(){var a=this.editor;this.attachListener(a,"beforeGetData",function(){var b=this.getData();this.is("textarea")||!1!==a.config.ignoreEmptyParagraph&&(b=b.replace(F,function(a,b){return b}));a.setData(b,null,1)},this);this.attachListener(a,"getSnapshot",function(a){a.data=this.getData(1)},this);this.attachListener(a,"afterSetData",function(){this.setData(a.getData(1))},this);this.attachListener(a,"loadSnapshot",function(a){this.setData(a.data,1)},this);this.attachListener(a,
+"beforeFocus",function(){var b=a.getSelection();(b=b&&b.getNative())&&"Control"==b.type||this.focus()},this);this.attachListener(a,"insertHtml",function(a){this.insertHtml(a.data.dataValue,a.data.mode,a.data.range)},this);this.attachListener(a,"insertElement",function(a){this.insertElement(a.data)},this);this.attachListener(a,"insertText",function(a){this.insertText(a.data)},this);this.setReadOnly(a.readOnly);this.attachClass("cke_editable");a.elementMode==CKEDITOR.ELEMENT_MODE_INLINE?this.attachClass("cke_editable_inline"):
+a.elementMode!=CKEDITOR.ELEMENT_MODE_REPLACE&&a.elementMode!=CKEDITOR.ELEMENT_MODE_APPENDTO||this.attachClass("cke_editable_themed");this.attachClass("cke_contents_"+a.config.contentsLangDirection);a.keystrokeHandler.blockedKeystrokes[8]=+a.readOnly;a.keystrokeHandler.attach(this);this.on("blur",function(){this.hasFocus=!1},null,null,-1);this.on("focus",function(){this.hasFocus=!0},null,null,-1);if(CKEDITOR.env.webkit)this.on("scroll",function(){a._.previousScrollTop=a.editable().$.scrollTop},null,
+null,-1);if(CKEDITOR.env.edge&&14<CKEDITOR.env.version){var d=function(){var b=a.editable();null!=a._.previousScrollTop&&b.getDocument().equals(CKEDITOR.document)&&(b.$.scrollTop=a._.previousScrollTop,a._.previousScrollTop=null,this.removeListener("scroll",d))};this.on("scroll",d)}a.focusManager.add(this);this.equals(CKEDITOR.document.getActive())&&(this.hasFocus=!0,a.once("contentDom",function(){a.focusManager.focus(this)},this));this.isInline()&&this.changeAttr("tabindex",a.tabIndex);if(!this.is("textarea")){a.document=
+this.getDocument();a.window=this.getWindow();var e=a.document;this.changeAttr("spellcheck",!a.config.disableNativeSpellChecker);var k=a.config.contentsLangDirection;this.getDirection(1)!=k&&this.changeAttr("dir",k);var h=CKEDITOR.getCss();if(h){var k=e.getHead(),q=k.getCustomData("stylesheet");q?h!=q.getText()&&(CKEDITOR.env.ie&&9>CKEDITOR.env.version?q.$.styleSheet.cssText=h:q.setText(h)):(h=e.appendStyleText(h),h=new CKEDITOR.dom.element(h.ownerNode||h.owningElement),k.setCustomData("stylesheet",
+h),h.data("cke-temp",1))}k=e.getCustomData("stylesheet_ref")||0;e.setCustomData("stylesheet_ref",k+1);this.setCustomData("cke_includeReadonly",!a.config.disableReadonlyStyling);this.attachListener(this,"click",function(a){a=a.data;var b=(new CKEDITOR.dom.elementPath(a.getTarget(),this)).contains("a");b&&2!=a.$.button&&b.isReadOnly()&&a.preventDefault()});var y={8:1,46:1};this.attachListener(a,"key",function(b){if(a.readOnly)return!0;var c=b.data.domEvent.getKey(),d;b=a.getSelection();if(0!==b.getRanges().length){if(c in
+y){var e,q=b.getRanges()[0],p=q.startPath(),C,r,k,c=8==c;CKEDITOR.env.ie&&11>CKEDITOR.env.version&&(e=b.getSelectedElement())||(e=g(b))?(a.fire("saveSnapshot"),q.moveToPosition(e,CKEDITOR.POSITION_BEFORE_START),e.remove(),q.select(),a.fire("saveSnapshot"),d=1):q.collapsed&&((C=p.block)&&(k=C[c?"getPrevious":"getNext"](f))&&k.type==CKEDITOR.NODE_ELEMENT&&k.is("table")&&q[c?"checkStartOfBlock":"checkEndOfBlock"]()?(a.fire("saveSnapshot"),q[c?"checkEndOfBlock":"checkStartOfBlock"]()&&C.remove(),q["moveToElementEdit"+
+(c?"End":"Start")](k),q.select(),a.fire("saveSnapshot"),d=1):p.blockLimit&&p.blockLimit.is("td")&&(r=p.blockLimit.getAscendant("table"))&&q.checkBoundaryOfElement(r,c?CKEDITOR.START:CKEDITOR.END)&&(k=r[c?"getPrevious":"getNext"](f))?(a.fire("saveSnapshot"),q["moveToElementEdit"+(c?"End":"Start")](k),q.checkStartOfBlock()&&q.checkEndOfBlock()?k.remove():q.select(),a.fire("saveSnapshot"),d=1):(r=p.contains(["td","th","caption"]))&&q.checkBoundaryOfElement(r,c?CKEDITOR.START:CKEDITOR.END)&&(d=1))}return!d}});
+a.blockless&&CKEDITOR.env.ie&&CKEDITOR.env.needsBrFiller&&this.attachListener(this,"keyup",function(b){b.data.getKeystroke()in y&&!this.getFirst(c)&&(this.appendBogus(),b=a.createRange(),b.moveToPosition(this,CKEDITOR.POSITION_AFTER_START),b.select())});this.attachListener(this,"dblclick",function(b){if(a.readOnly)return!1;b={element:b.data.getTarget()};a.fire("doubleclick",b)});CKEDITOR.env.ie&&this.attachListener(this,"click",b);CKEDITOR.env.ie&&!CKEDITOR.env.edge||this.attachListener(this,"mousedown",
+function(b){var c=b.data.getTarget();c.is("img","hr","input","textarea","select")&&!c.isReadOnly()&&(a.getSelection().selectElement(c),c.is("input","textarea","select")&&b.data.preventDefault())});CKEDITOR.env.edge&&this.attachListener(this,"mouseup",function(b){(b=b.data.getTarget())&&b.is("img")&&a.getSelection().selectElement(b)});CKEDITOR.env.gecko&&this.attachListener(this,"mouseup",function(b){if(2==b.data.$.button&&(b=b.data.getTarget(),!b.getOuterHtml().replace(F,""))){var c=a.createRange();
+c.moveToElementEditStart(b);c.select(!0)}});CKEDITOR.env.webkit&&(this.attachListener(this,"click",function(a){a.data.getTarget().is("input","select")&&a.data.preventDefault()}),this.attachListener(this,"mouseup",function(a){a.data.getTarget().is("input","textarea")&&a.data.preventDefault()}));CKEDITOR.env.webkit&&this.attachListener(a,"key",function(b){if(a.readOnly)return!0;var c=b.data.domEvent.getKey();if(c in y&&(b=a.getSelection(),0!==b.getRanges().length)){var c=8==c,d=b.getRanges()[0];b=d.startPath();
+if(d.collapsed)a:{var e=b.block;if(e&&d[c?"checkStartOfBlock":"checkEndOfBlock"]()&&d.moveToClosestEditablePosition(e,!c)&&d.collapsed){if(d.startContainer.type==CKEDITOR.NODE_ELEMENT){var f=d.startContainer.getChild(d.startOffset-(c?1:0));if(f&&f.type==CKEDITOR.NODE_ELEMENT&&f.is("hr")){a.fire("saveSnapshot");f.remove();b=!0;break a}}d=d.startPath().block;if(!d||d&&d.contains(e))b=void 0;else{a.fire("saveSnapshot");var p;(p=(c?d:e).getBogus())&&p.remove();p=a.getSelection();f=p.createBookmarks();
+(c?e:d).moveChildren(c?d:e,!1);b.lastElement.mergeSiblings();u(e,d,!c);p.selectBookmarks(f);b=!0}}else b=!1}else c=d,p=b.block,d=c.endPath().block,p&&d&&!p.equals(d)?(a.fire("saveSnapshot"),(e=p.getBogus())&&e.remove(),c.enlarge(CKEDITOR.ENLARGE_INLINE),c.deleteContents(),d.getParent()&&(d.moveChildren(p,!1),b.lastElement.mergeSiblings(),u(p,d,!0)),c=a.getSelection().getRanges()[0],c.collapse(1),c.optimize(),""===c.startContainer.getHtml()&&c.startContainer.appendBogus(),c.select(),b=!0):b=!1;if(!b)return;
+a.getSelection().scrollIntoView();a.fire("saveSnapshot");return!1}},this,null,100)}}},_:{detach:function(){this.editor.setData(this.editor.getData(),0,1);this.clearListeners();this.restoreAttrs();var a;if(a=this.removeCustomData("classes"))for(;a.length;)this.removeClass(a.pop());if(!this.is("textarea")){a=this.getDocument();var b=a.getHead();if(b.getCustomData("stylesheet")){var c=a.getCustomData("stylesheet_ref");--c?a.setCustomData("stylesheet_ref",c):(a.removeCustomData("stylesheet_ref"),b.removeCustomData("stylesheet").remove())}}this.editor.fire("contentDomUnload");
+delete this.editor}}});CKEDITOR.editor.prototype.editable=function(a){var b=this._.editable;if(b&&a)return 0;arguments.length&&(b=this._.editable=a?a instanceof CKEDITOR.editable?a:new CKEDITOR.editable(this,a):(b&&b.detach(),null));return b};CKEDITOR.on("instanceLoaded",function(b){var c=b.editor;c.on("insertElement",function(a){a=a.data;a.type==CKEDITOR.NODE_ELEMENT&&(a.is("input")||a.is("textarea"))&&("false"!=a.getAttribute("contentEditable")&&a.data("cke-editable",a.hasAttribute("contenteditable")?
+"true":"1"),a.setAttribute("contentEditable",!1))});c.on("selectionChange",function(b){if(!c.readOnly){var d=c.getSelection();d&&!d.isLocked&&(d=c.checkDirty(),c.fire("lockSnapshot"),a(b),c.fire("unlockSnapshot"),!d&&c.resetDirty())}})});CKEDITOR.on("instanceCreated",function(a){var b=a.editor;b.on("mode",function(){var a=b.editable();if(a&&a.isInline()){var c=b.title;a.changeAttr("role","textbox");a.changeAttr("aria-label",c);c&&a.changeAttr("title",c);var d=b.fire("ariaEditorHelpLabel",{}).label;
+if(d&&(c=this.ui.space(this.elementMode==CKEDITOR.ELEMENT_MODE_INLINE?"top":"contents"))){var e=CKEDITOR.tools.getNextId(),d=CKEDITOR.dom.element.createFromHtml('\x3cspan id\x3d"'+e+'" class\x3d"cke_voice_label"\x3e'+d+"\x3c/span\x3e");c.append(d);a.changeAttr("aria-describedby",e)}}})});CKEDITOR.addCss(".cke_editable{cursor:text}.cke_editable img,.cke_editable input,.cke_editable textarea{cursor:default}");f=CKEDITOR.dom.walker.whitespaces(!0);D=CKEDITOR.dom.walker.bookmark(!1,!0);w=CKEDITOR.dom.walker.empty();
+A=CKEDITOR.dom.walker.bogus();F=/(^|<body\b[^>]*>)\s*<(p|div|address|h\d|center|pre)[^>]*>\s*(?:<br[^>]*>|&nbsp;|\u00A0|&#160;)?\s*(:?<\/\2>)?\s*(?=$|<\/body>)/gi;x=function(){function a(b){return b.type==CKEDITOR.NODE_ELEMENT}function b(c,d){var e,f,q,g,y=[],k=d.range.startContainer;e=d.range.startPath();for(var k=v[k.getName()],h=0,B=c.getChildren(),n=B.count(),L=-1,u=-1,E=0,H=e.contains(v.$list);h<n;++h)e=B.getItem(h),a(e)?(q=e.getName(),H&&q in CKEDITOR.dtd.$list?y=y.concat(b(e,d)):(g=!!k[q],
+"br"!=q||!e.data("cke-eol")||h&&h!=n-1||(E=(f=h?y[h-1].node:B.getItem(h+1))&&(!a(f)||!f.is("br")),f=f&&a(f)&&v.$block[f.getName()]),-1!=L||g||(L=h),g||(u=h),y.push({isElement:1,isLineBreak:E,isBlock:e.isBlockBoundary(),hasBlockSibling:f,node:e,name:q,allowed:g}),f=E=0)):y.push({isElement:0,node:e,allowed:1});-1<L&&(y[L].firstNotAllowed=1);-1<u&&(y[u].lastNotAllowed=1);return y}function d(b,c){var e=[],f=b.getChildren(),q=f.count(),g,y=0,k=v[c],h=!b.is(v.$inline)||b.is("br");for(h&&e.push(" ");y<q;y++)g=
+f.getItem(y),a(g)&&!g.is(k)?e=e.concat(d(g,c)):e.push(g);h&&e.push(" ");return e}function e(b){return a(b.startContainer)&&b.startContainer.getChild(b.startOffset-1)}function f(b){return b&&a(b)&&(b.is(v.$removeEmpty)||b.is("a")&&!b.isBlockBoundary())}function q(b,c,d,e){var f=b.clone(),g,v;f.setEndAt(c,CKEDITOR.POSITION_BEFORE_END);(g=(new CKEDITOR.dom.walker(f)).next())&&a(g)&&B[g.getName()]&&(v=g.getPrevious())&&a(v)&&!v.getParent().equals(b.startContainer)&&d.contains(v)&&e.contains(g)&&g.isIdentical(v)&&
+(g.moveChildren(v),g.remove(),q(b,c,d,e))}function g(b,c){function d(b,c){if(c.isBlock&&c.isElement&&!c.node.is("br")&&a(b)&&b.is("br"))return b.remove(),1}var e=c.endContainer.getChild(c.endOffset),f=c.endContainer.getChild(c.endOffset-1);e&&d(e,b[b.length-1]);f&&d(f,b[0])&&(c.setEnd(c.endContainer,c.endOffset-1),c.collapse())}var v=CKEDITOR.dtd,B={p:1,div:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,ul:1,ol:1,li:1,pre:1,dl:1,blockquote:1},n={p:1,div:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1},u=CKEDITOR.tools.extend({},
+v.$inline);delete u.br;return function(B,p,C,r){var G=B.editor,N=!1;"unfiltered_html"==p&&(p="html",N=!0);if(!r.checkReadOnly()){var P=(new CKEDITOR.dom.elementPath(r.startContainer,r.root)).blockLimit||r.root;B={type:p,dontFilter:N,editable:B,editor:G,range:r,blockLimit:P,mergeCandidates:[],zombies:[]};p=B.range;r=B.mergeCandidates;var m,w;"text"==B.type&&p.shrink(CKEDITOR.SHRINK_ELEMENT,!0,!1)&&(m=CKEDITOR.dom.element.createFromHtml("\x3cspan\x3e\x26nbsp;\x3c/span\x3e",p.document),p.insertNode(m),
+p.setStartAfter(m));N=new CKEDITOR.dom.elementPath(p.startContainer);B.endPath=P=new CKEDITOR.dom.elementPath(p.endContainer);if(!p.collapsed){var G=P.block||P.blockLimit,x=p.getCommonAncestor();G&&!G.equals(x)&&!G.contains(x)&&p.checkEndOfBlock()&&B.zombies.push(G);p.deleteContents()}for(;(w=e(p))&&a(w)&&w.isBlockBoundary()&&N.contains(w);)p.moveToPosition(w,CKEDITOR.POSITION_BEFORE_END);q(p,B.blockLimit,N,P);m&&(p.setEndBefore(m),p.collapse(),m.remove());m=p.startPath();if(G=m.contains(f,!1,1))p.splitElement(G),
+B.inlineStylesRoot=G,B.inlineStylesPeak=m.lastElement;m=p.createBookmark();(G=m.startNode.getPrevious(c))&&a(G)&&f(G)&&r.push(G);(G=m.startNode.getNext(c))&&a(G)&&f(G)&&r.push(G);for(G=m.startNode;(G=G.getParent())&&f(G);)r.push(G);p.moveToBookmark(m);if(m=C){m=B.range;if("text"==B.type&&B.inlineStylesRoot){w=B.inlineStylesPeak;p=w.getDocument().createText("{cke-peak}");for(r=B.inlineStylesRoot.getParent();!w.equals(r);)p=p.appendTo(w.clone()),w=w.getParent();C=p.getOuterHtml().split("{cke-peak}").join(C)}w=
+B.blockLimit.getName();if(/^\s+|\s+$/.test(C)&&"span"in CKEDITOR.dtd[w]){var D='\x3cspan data-cke-marker\x3d"1"\x3e\x26nbsp;\x3c/span\x3e';C=D+C+D}C=B.editor.dataProcessor.toHtml(C,{context:null,fixForBody:!1,protectedWhitespaces:!!D,dontFilter:B.dontFilter,filter:B.editor.activeFilter,enterMode:B.editor.activeEnterMode});w=m.document.createElement("body");w.setHtml(C);D&&(w.getFirst().remove(),w.getLast().remove());if((D=m.startPath().block)&&(1!=D.getChildCount()||!D.getBogus()))a:{var A;if(1==
+w.getChildCount()&&a(A=w.getFirst())&&A.is(n)&&!A.hasAttribute("contenteditable")){D=A.getElementsByTag("*");m=0;for(r=D.count();m<r;m++)if(p=D.getItem(m),!p.is(u))break a;A.moveChildren(A.getParent(1));A.remove()}}B.dataWrapper=w;m=C}if(m){A=B.range;m=A.document;var M;w=B.blockLimit;r=0;var z,D=[],F,T;C=G=0;var I,K;p=A.startContainer;var N=B.endPath.elements[0],X,P=N.getPosition(p),x=!!N.getCommonAncestor(p)&&P!=CKEDITOR.POSITION_IDENTICAL&&!(P&CKEDITOR.POSITION_CONTAINS+CKEDITOR.POSITION_IS_CONTAINED);
+p=b(B.dataWrapper,B);for(g(p,A);r<p.length;r++){P=p[r];if(M=P.isLineBreak){M=A;I=w;var S=void 0,Z=void 0;P.hasBlockSibling?M=1:(S=M.startContainer.getAscendant(v.$block,1))&&S.is({div:1,p:1})?(Z=S.getPosition(I),Z==CKEDITOR.POSITION_IDENTICAL||Z==CKEDITOR.POSITION_CONTAINS?M=0:(I=M.splitElement(S),M.moveToPosition(I,CKEDITOR.POSITION_AFTER_START),M=1)):M=0}if(M)C=0<r;else{M=A.startPath();!P.isBlock&&h(B.editor,M.block,M.blockLimit)&&(T=k(B.editor))&&(T=m.createElement(T),T.appendBogus(),A.insertNode(T),
+CKEDITOR.env.needsBrFiller&&(z=T.getBogus())&&z.remove(),A.moveToPosition(T,CKEDITOR.POSITION_BEFORE_END));if((M=A.startPath().block)&&!M.equals(F)){if(z=M.getBogus())z.remove(),D.push(M);F=M}P.firstNotAllowed&&(G=1);if(G&&P.isElement){M=A.startContainer;for(I=null;M&&!v[M.getName()][P.name];){if(M.equals(w)){M=null;break}I=M;M=M.getParent()}if(M)I&&(K=A.splitElement(I),B.zombies.push(K),B.zombies.push(I));else{I=w.getName();X=!r;M=r==p.length-1;I=d(P.node,I);for(var S=[],Z=I.length,aa=0,ca=void 0,
+da=0,U=-1;aa<Z;aa++)ca=I[aa]," "==ca?(da||X&&!aa||(S.push(new CKEDITOR.dom.text(" ")),U=S.length),da=1):(S.push(ca),da=0);M&&U==S.length&&S.pop();X=S}}if(X){for(;M=X.pop();)A.insertNode(M);X=0}else A.insertNode(P.node);P.lastNotAllowed&&r<p.length-1&&((K=x?N:K)&&A.setEndAt(K,CKEDITOR.POSITION_AFTER_START),G=0);A.collapse()}}1!=p.length?z=!1:(z=p[0],z=z.isElement&&"false"==z.node.getAttribute("contenteditable"));z&&(C=!0,M=p[0].node,A.setStartAt(M,CKEDITOR.POSITION_BEFORE_START),A.setEndAt(M,CKEDITOR.POSITION_AFTER_END));
+B.dontMoveCaret=C;B.bogusNeededBlocks=D}z=B.range;var R;K=B.bogusNeededBlocks;for(X=z.createBookmark();F=B.zombies.pop();)F.getParent()&&(T=z.clone(),T.moveToElementEditStart(F),T.removeEmptyBlocksAtEnd());if(K)for(;F=K.pop();)CKEDITOR.env.needsBrFiller?F.appendBogus():F.append(z.document.createText(" "));for(;F=B.mergeCandidates.pop();)F.mergeSiblings();z.moveToBookmark(X);if(!B.dontMoveCaret){for(F=e(z);F&&a(F)&&!F.is(v.$empty);){if(F.isBlockBoundary())z.moveToPosition(F,CKEDITOR.POSITION_BEFORE_END);
+else{if(f(F)&&F.getHtml().match(/(\s|&nbsp;)$/g)){R=null;break}R=z.clone();R.moveToPosition(F,CKEDITOR.POSITION_BEFORE_END)}F=F.getLast(c)}R&&z.moveToRange(R)}}}}();m=function(){function a(b){b=new CKEDITOR.dom.walker(b);b.guard=function(a,b){if(b)return!1;if(a.type==CKEDITOR.NODE_ELEMENT)return a.is(CKEDITOR.dtd.$tableContent)};b.evaluator=function(a){return a.type==CKEDITOR.NODE_ELEMENT};return b}function b(a,c,d){c=a.getDocument().createElement(c);a.append(c,d);return c}function c(a){var b=a.count(),
+d;for(b;0<b--;)d=a.getItem(b),CKEDITOR.tools.trim(d.getHtml())||(d.appendBogus(),CKEDITOR.env.ie&&9>CKEDITOR.env.version&&d.getChildCount()&&d.getFirst().remove())}return function(d){var e=d.startContainer,f=e.getAscendant("table",1),g=!1;c(f.getElementsByTag("td"));c(f.getElementsByTag("th"));f=d.clone();f.setStart(e,0);f=a(f).lastBackward();f||(f=d.clone(),f.setEndAt(e,CKEDITOR.POSITION_BEFORE_END),f=a(f).lastForward(),g=!0);f||(f=e);f.is("table")?(d.setStartAt(f,CKEDITOR.POSITION_BEFORE_START),
+d.collapse(!0),f.remove()):(f.is({tbody:1,thead:1,tfoot:1})&&(f=b(f,"tr",g)),f.is("tr")&&(f=b(f,f.getParent().is("thead")?"th":"td",g)),(e=f.getBogus())&&e.remove(),d.moveToPosition(f,g?CKEDITOR.POSITION_AFTER_START:CKEDITOR.POSITION_BEFORE_END))}}();K=function(){function a(b){b=new CKEDITOR.dom.walker(b);b.guard=function(a,b){if(b)return!1;if(a.type==CKEDITOR.NODE_ELEMENT)return a.is(CKEDITOR.dtd.$list)||a.is(CKEDITOR.dtd.$listItem)};b.evaluator=function(a){return a.type==CKEDITOR.NODE_ELEMENT&&
+a.is(CKEDITOR.dtd.$listItem)};return b}return function(b){var c=b.startContainer,d=!1,e;e=b.clone();e.setStart(c,0);e=a(e).lastBackward();e||(e=b.clone(),e.setEndAt(c,CKEDITOR.POSITION_BEFORE_END),e=a(e).lastForward(),d=!0);e||(e=c);e.is(CKEDITOR.dtd.$list)?(b.setStartAt(e,CKEDITOR.POSITION_BEFORE_START),b.collapse(!0),e.remove()):((c=e.getBogus())&&c.remove(),b.moveToPosition(e,d?CKEDITOR.POSITION_AFTER_START:CKEDITOR.POSITION_BEFORE_END),b.select())}}();z={eol:{detect:function(a,b){var c=a.range,
+d=c.clone(),e=c.clone(),f=new CKEDITOR.dom.elementPath(c.startContainer,b),g=new CKEDITOR.dom.elementPath(c.endContainer,b);d.collapse(1);e.collapse();f.block&&d.checkBoundaryOfElement(f.block,CKEDITOR.END)&&(c.setStartAfter(f.block),a.prependEolBr=1);g.block&&e.checkBoundaryOfElement(g.block,CKEDITOR.START)&&(c.setEndBefore(g.block),a.appendEolBr=1)},fix:function(a,b){var c=b.getDocument(),d;a.appendEolBr&&(d=this.createEolBr(c),a.fragment.append(d));!a.prependEolBr||d&&!d.getPrevious()||a.fragment.append(this.createEolBr(c),
+1)},createEolBr:function(a){return a.createElement("br",{attributes:{"data-cke-eol":1}})}},bogus:{exclude:function(a){var b=a.range.getBoundaryNodes(),c=b.startNode,b=b.endNode;!b||!A(b)||c&&c.equals(b)||a.range.setEndBefore(b)}},tree:{rebuild:function(a,b){var c=a.range,d=c.getCommonAncestor(),e=new CKEDITOR.dom.elementPath(d,b),f=new CKEDITOR.dom.elementPath(c.startContainer,b),c=new CKEDITOR.dom.elementPath(c.endContainer,b),g;d.type==CKEDITOR.NODE_TEXT&&(d=d.getParent());if(e.blockLimit.is({tr:1,
+table:1})){var v=e.contains("table").getParent();g=function(a){return!a.equals(v)}}else if(e.block&&e.block.is(CKEDITOR.dtd.$listItem)&&(f=f.contains(CKEDITOR.dtd.$list),c=c.contains(CKEDITOR.dtd.$list),!f.equals(c))){var k=e.contains(CKEDITOR.dtd.$list).getParent();g=function(a){return!a.equals(k)}}g||(g=function(a){return!a.equals(e.block)&&!a.equals(e.blockLimit)});this.rebuildFragment(a,b,d,g)},rebuildFragment:function(a,b,c,d){for(var e;c&&!c.equals(b)&&d(c);)e=c.clone(0,1),a.fragment.appendTo(e),
+a.fragment=e,c=c.getParent()}},cell:{shrink:function(a){a=a.range;var b=a.startContainer,c=a.endContainer,d=a.startOffset,e=a.endOffset;b.type==CKEDITOR.NODE_ELEMENT&&b.equals(c)&&b.is("tr")&&++d==e&&a.shrink(CKEDITOR.SHRINK_TEXT)}}};I=function(){function a(b,c){var d=b.getParent();if(d.is(CKEDITOR.dtd.$inline))b[c?"insertBefore":"insertAfter"](d)}function b(c,d,e){a(d);a(e,1);for(var f;f=e.getNext();)f.insertAfter(d),d=f;w(c)&&c.remove()}function c(a,b){var d=new CKEDITOR.dom.range(a);d.setStartAfter(b.startNode);
+d.setEndBefore(b.endNode);return d}return{list:{detectMerge:function(a,b){var d=c(b,a.bookmark),e=d.startPath(),f=d.endPath(),g=e.contains(CKEDITOR.dtd.$list),k=f.contains(CKEDITOR.dtd.$list);a.mergeList=g&&k&&g.getParent().equals(k.getParent())&&!g.equals(k);a.mergeListItems=e.block&&f.block&&e.block.is(CKEDITOR.dtd.$listItem)&&f.block.is(CKEDITOR.dtd.$listItem);if(a.mergeList||a.mergeListItems)d=d.clone(),d.setStartBefore(a.bookmark.startNode),d.setEndAfter(a.bookmark.endNode),a.mergeListBookmark=
+d.createBookmark()},merge:function(a,c){if(a.mergeListBookmark){var d=a.mergeListBookmark.startNode,e=a.mergeListBookmark.endNode,f=new CKEDITOR.dom.elementPath(d,c),g=new CKEDITOR.dom.elementPath(e,c);if(a.mergeList){var k=f.contains(CKEDITOR.dtd.$list),h=g.contains(CKEDITOR.dtd.$list);k.equals(h)||(h.moveChildren(k),h.remove())}a.mergeListItems&&(f=f.contains(CKEDITOR.dtd.$listItem),g=g.contains(CKEDITOR.dtd.$listItem),f.equals(g)||b(g,d,e));d.remove();e.remove()}}},block:{detectMerge:function(a,
+b){if(!a.tableContentsRanges&&!a.mergeListBookmark){var c=new CKEDITOR.dom.range(b);c.setStartBefore(a.bookmark.startNode);c.setEndAfter(a.bookmark.endNode);a.mergeBlockBookmark=c.createBookmark()}},merge:function(a,c){if(a.mergeBlockBookmark&&!a.purgeTableBookmark){var d=a.mergeBlockBookmark.startNode,e=a.mergeBlockBookmark.endNode,f=new CKEDITOR.dom.elementPath(d,c),g=new CKEDITOR.dom.elementPath(e,c),f=f.block,g=g.block;f&&g&&!f.equals(g)&&b(g,d,e);d.remove();e.remove()}}},table:function(){function a(c){var e=
+[],f,g=new CKEDITOR.dom.walker(c),k=c.startPath().contains(d),h=c.endPath().contains(d),p={};g.guard=function(a,g){if(a.type==CKEDITOR.NODE_ELEMENT){var l="visited_"+(g?"out":"in");if(a.getCustomData(l))return;CKEDITOR.dom.element.setMarker(p,a,l,1)}if(g&&k&&a.equals(k))f=c.clone(),f.setEndAt(k,CKEDITOR.POSITION_BEFORE_END),e.push(f);else if(!g&&h&&a.equals(h))f=c.clone(),f.setStartAt(h,CKEDITOR.POSITION_AFTER_START),e.push(f);else{if(l=!g)l=a.type==CKEDITOR.NODE_ELEMENT&&a.is(d)&&(!k||b(a,k))&&(!h||
+b(a,h));l&&(f=c.clone(),f.selectNodeContents(a),e.push(f))}};g.lastForward();CKEDITOR.dom.element.clearAllMarkers(p);return e}function b(a,c){var d=CKEDITOR.POSITION_CONTAINS+CKEDITOR.POSITION_IS_CONTAINED,e=a.getPosition(c);return e===CKEDITOR.POSITION_IDENTICAL?!1:0===(e&d)}var d={td:1,th:1,caption:1};return{detectPurge:function(a){var b=a.range,c=b.clone();c.enlarge(CKEDITOR.ENLARGE_ELEMENT);var c=new CKEDITOR.dom.walker(c),e=0;c.evaluator=function(a){a.type==CKEDITOR.NODE_ELEMENT&&a.is(d)&&++e};
+c.checkForward();if(1<e){var c=b.startPath().contains("table"),f=b.endPath().contains("table");c&&f&&b.checkBoundaryOfElement(c,CKEDITOR.START)&&b.checkBoundaryOfElement(f,CKEDITOR.END)&&(b=a.range.clone(),b.setStartBefore(c),b.setEndAfter(f),a.purgeTableBookmark=b.createBookmark())}},detectRanges:function(e,f){var g=c(f,e.bookmark),k=g.clone(),h,l,p=g.getCommonAncestor();p.is(CKEDITOR.dtd.$tableContent)&&!p.is(d)&&(p=p.getAscendant("table",!0));l=p;p=new CKEDITOR.dom.elementPath(g.startContainer,
+l);l=new CKEDITOR.dom.elementPath(g.endContainer,l);p=p.contains("table");l=l.contains("table");if(p||l)p&&l&&b(p,l)?(e.tableSurroundingRange=k,k.setStartAt(p,CKEDITOR.POSITION_AFTER_END),k.setEndAt(l,CKEDITOR.POSITION_BEFORE_START),k=g.clone(),k.setEndAt(p,CKEDITOR.POSITION_AFTER_END),h=g.clone(),h.setStartAt(l,CKEDITOR.POSITION_BEFORE_START),h=a(k).concat(a(h))):p?l||(e.tableSurroundingRange=k,k.setStartAt(p,CKEDITOR.POSITION_AFTER_END),g.setEndAt(p,CKEDITOR.POSITION_AFTER_END)):(e.tableSurroundingRange=
+k,k.setEndAt(l,CKEDITOR.POSITION_BEFORE_START),g.setStartAt(l,CKEDITOR.POSITION_AFTER_START)),e.tableContentsRanges=h?h:a(g)},deleteRanges:function(a){for(var b;b=a.tableContentsRanges.pop();)b.extractContents(),w(b.startContainer)&&b.startContainer.appendBogus();a.tableSurroundingRange&&a.tableSurroundingRange.extractContents()},purge:function(a){if(a.purgeTableBookmark){var b=a.doc,c=a.range.clone(),b=b.createElement("p");b.insertBefore(a.purgeTableBookmark.startNode);c.moveToBookmark(a.purgeTableBookmark);
+c.deleteContents();a.range.moveToPosition(b,CKEDITOR.POSITION_AFTER_START)}}}}(),detectExtractMerge:function(a){return!(a.range.startPath().contains(CKEDITOR.dtd.$listItem)&&a.range.endPath().contains(CKEDITOR.dtd.$listItem))},fixUneditableRangePosition:function(a){a.startContainer.getDtd()["#"]||a.moveToClosestEditablePosition(null,!0)},autoParagraph:function(a,b){var c=b.startPath(),d;h(a,c.block,c.blockLimit)&&(d=k(a))&&(d=b.document.createElement(d),d.appendBogus(),b.insertNode(d),b.moveToPosition(d,
+CKEDITOR.POSITION_AFTER_START))}}}()})();
+(function(){function a(a,b){if(0===a.length)return!1;var c,d;if((c=!b&&1===a.length)&&!(c=a[0].collapsed)){var e=a[0];c=e.startContainer.getAscendant({td:1,th:1},!0);var f=e.endContainer.getAscendant({td:1,th:1},!0);d=CKEDITOR.tools.trim;c&&c.equals(f)&&!c.findOne("td, th, tr, tbody, table")?(e=e.cloneContents(),c=e.getFirst()?d(e.getFirst().getText())!==d(c.getText()):!0):c=!1}if(c)return!1;for(d=0;d<a.length;d++)if(c=a[d]._getTableElement(),!c)return!1;return!0}function d(a){function b(a){a=a.find("td, th");
+var c=[],d;for(d=0;d<a.count();d++)c.push(a.getItem(d));return c}var c=[],d,e;for(e=0;e<a.length;e++)d=a[e]._getTableElement(),d.is&&d.is({td:1,th:1})?c.push(d):c=c.concat(b(d));return c}function b(a){a=d(a);var b="",c=[],e,f;for(f=0;f<a.length;f++)e&&!e.equals(a[f].getAscendant("tr"))?(b+=c.join("\t")+"\n",e=a[f].getAscendant("tr"),c=[]):0===f&&(e=a[f].getAscendant("tr")),c.push(a[f].getText());return b+=c.join("\t")}function c(a){var c=this.root.editor,d=c.getSelection(1);this.reset();I=!0;d.root.once("selectionchange",
+function(a){a.cancel()},null,null,0);d.selectRanges([a[0]]);d=this._.cache;d.ranges=new CKEDITOR.dom.rangeList(a);d.type=CKEDITOR.SELECTION_TEXT;d.selectedElement=a[0]._getTableElement();d.selectedText=b(a);d.nativeSel=null;this.isFake=1;this.rev=m++;c._.fakeSelection=this;I=!1;this.root.fire("selectionchange")}function e(){var b=this._.fakeSelection,c;if(b){c=this.getSelection(1);var d;if(!(d=!c)&&(d=!c.isHidden())){d=b;var e=c.getRanges(),f=d.getRanges(),g=e.length&&e[0]._getTableElement()&&e[0]._getTableElement().getAscendant("table",
+!0),k=f.length&&f[0]._getTableElement()&&f[0]._getTableElement().getAscendant("table",!0),p=1===e.length&&e[0]._getTableElement()&&e[0]._getTableElement().is("table"),h=1===f.length&&f[0]._getTableElement()&&f[0]._getTableElement().is("table"),r=1===e.length&&e[0].collapsed,f=a(e,!!CKEDITOR.env.webkit)&&a(f);g=g&&k?g.equals(k)||k.contains(g):!1;g&&(r||f)?(p&&!h&&d.selectRanges(e),d=!0):d=!1;d=!d}d&&(b.reset(),b=0)}if(!b&&(b=c||this.getSelection(1),!b||b.getType()==CKEDITOR.SELECTION_NONE))return;
+this.fire("selectionCheck",b);c=this.elementPath();c.compare(this._.selectionPreviousPath)||(d=this._.selectionPreviousPath&&this._.selectionPreviousPath.blockLimit.equals(c.blockLimit),CKEDITOR.env.webkit&&!d&&(this._.previousActive=this.document.getActive()),this._.selectionPreviousPath=c,this.fire("selectionChange",{selection:b,path:c}))}function g(){t=!0;l||(h.call(this),l=CKEDITOR.tools.setTimeout(h,200,this))}function h(){l=null;t&&(CKEDITOR.tools.setTimeout(e,0,this),t=!1)}function k(a){return J(a)||
+a.type==CKEDITOR.NODE_ELEMENT&&!a.is(CKEDITOR.dtd.$empty)?!0:!1}function n(a){function b(c,d){return c&&c.type!=CKEDITOR.NODE_TEXT?a.clone()["moveToElementEdit"+(d?"End":"Start")](c):!1}if(!(a.root instanceof CKEDITOR.editable))return!1;var c=a.startContainer,d=a.getPreviousNode(k,null,c),e=a.getNextNode(k,null,c);return b(d)||b(e,1)||!(d||e||c.type==CKEDITOR.NODE_ELEMENT&&c.isBlockBoundary()&&c.getBogus())?!0:!1}function u(a){f(a,!1);var b=a.getDocument().createText(K);a.setCustomData("cke-fillingChar",
+b);return b}function f(a,b){var c=a&&a.removeCustomData("cke-fillingChar");if(c){if(!1!==b){var d=a.getDocument().getSelection().getNative(),e=d&&"None"!=d.type&&d.getRangeAt(0),f=K.length;if(c.getLength()>f&&e&&e.intersectsNode(c.$)){var g=[{node:d.anchorNode,offset:d.anchorOffset},{node:d.focusNode,offset:d.focusOffset}];d.anchorNode==c.$&&d.anchorOffset>f&&(g[0].offset-=f);d.focusNode==c.$&&d.focusOffset>f&&(g[1].offset-=f)}}c.setText(D(c.getText(),1));g&&(c=a.getDocument().$,d=c.getSelection(),
+c=c.createRange(),c.setStart(g[0].node,g[0].offset),c.collapse(!0),d.removeAllRanges(),d.addRange(c),d.extend(g[1].node,g[1].offset))}}function D(a,b){return b?a.replace(z,function(a,b){return b?" ":""}):a.replace(K,"")}function w(a,b){var c=CKEDITOR.dom.element.createFromHtml('\x3cdiv data-cke-hidden-sel\x3d"1" data-cke-temp\x3d"1" style\x3d"'+(CKEDITOR.env.ie&&14>CKEDITOR.env.version?"display:none":"position:fixed;top:0;left:-1000px")+'"\x3e'+(b||"\x26nbsp;")+"\x3c/div\x3e",a.document);a.fire("lockSnapshot");
+a.editable().append(c);var d=a.getSelection(1),e=a.createRange(),f=d.root.on("selectionchange",function(a){a.cancel()},null,null,0);e.setStartAt(c,CKEDITOR.POSITION_AFTER_START);e.setEndAt(c,CKEDITOR.POSITION_BEFORE_END);d.selectRanges([e]);f.removeListener();a.fire("unlockSnapshot");a._.hiddenSelectionContainer=c}function A(a){var b={37:1,39:1,8:1,46:1};return function(c){var d=c.data.getKeystroke();if(b[d]){var e=a.getSelection().getRanges(),f=e[0];1==e.length&&f.collapsed&&(d=f[38>d?"getPreviousEditableNode":
+"getNextEditableNode"]())&&d.type==CKEDITOR.NODE_ELEMENT&&"false"==d.getAttribute("contenteditable")&&(a.getSelection().fake(d),c.data.preventDefault(),c.cancel())}}}function F(a){for(var b=0;b<a.length;b++){var c=a[b];c.getCommonAncestor().isReadOnly()&&a.splice(b,1);if(!c.collapsed){if(c.startContainer.isReadOnly())for(var d=c.startContainer,e;d&&!((e=d.type==CKEDITOR.NODE_ELEMENT)&&d.is("body")||!d.isReadOnly());)e&&"false"==d.getAttribute("contentEditable")&&c.setStartAfter(d),d=d.getParent();
+d=c.startContainer;e=c.endContainer;var f=c.startOffset,g=c.endOffset,p=c.clone();d&&d.type==CKEDITOR.NODE_TEXT&&(f>=d.getLength()?p.setStartAfter(d):p.setStartBefore(d));e&&e.type==CKEDITOR.NODE_TEXT&&(g?p.setEndAfter(e):p.setEndBefore(e));d=new CKEDITOR.dom.walker(p);d.evaluator=function(d){if(d.type==CKEDITOR.NODE_ELEMENT&&d.isReadOnly()){var e=c.clone();c.setEndBefore(d);c.collapsed&&a.splice(b--,1);d.getPosition(p.endContainer)&CKEDITOR.POSITION_CONTAINS||(e.setStartAfter(d),e.collapsed||a.splice(b+
+1,0,e));return!0}return!1};d.next()}}return a}var x="function"!=typeof window.getSelection,m=1,K=CKEDITOR.tools.repeat("​",7),z=new RegExp(K+"( )?","g"),I,l,t,J=CKEDITOR.dom.walker.invisible(1),H=function(){function a(b){return function(a){var c=a.editor.createRange();c.moveToClosestEditablePosition(a.selected,b)&&a.editor.getSelection().selectRanges([c]);return!1}}function b(a){return function(b){var c=b.editor,d=c.createRange(),e;(e=d.moveToClosestEditablePosition(b.selected,a))||(e=d.moveToClosestEditablePosition(b.selected,
+!a));e&&c.getSelection().selectRanges([d]);c.fire("saveSnapshot");b.selected.remove();e||(d.moveToElementEditablePosition(c.editable()),c.getSelection().selectRanges([d]));c.fire("saveSnapshot");return!1}}var c=a(),d=a(1);return{37:c,38:c,39:d,40:d,8:b(),46:b(1)}}();CKEDITOR.on("instanceCreated",function(a){function b(){var a=c.getSelection();a&&a.removeAllRanges()}var c=a.editor;c.on("contentDom",function(){function a(){n=new CKEDITOR.dom.selection(c.getSelection());n.lock()}function b(){p.removeListener("mouseup",
+b);l.removeListener("mouseup",b);var a=CKEDITOR.document.$.selection,c=a.createRange();"None"!=a.type&&c.parentElement()&&c.parentElement().ownerDocument==k.$&&c.select()}function d(a){if(CKEDITOR.env.ie){var b=(a=a.getRanges()[0])?a.startContainer.getAscendant(function(a){return a.type==CKEDITOR.NODE_ELEMENT&&("false"==a.getAttribute("contenteditable")||"true"==a.getAttribute("contenteditable"))},!0):null;return a&&"false"==b.getAttribute("contenteditable")&&b}}var k=c.document,p=CKEDITOR.document,
+h=c.editable(),r=k.getBody(),l=k.getDocumentElement(),q=h.isInline(),y,n;CKEDITOR.env.gecko&&h.attachListener(h,"focus",function(a){a.removeListener();0!==y&&(a=c.getSelection().getNative())&&a.isCollapsed&&a.anchorNode==h.$&&(a=c.createRange(),a.moveToElementEditStart(h),a.select())},null,null,-2);h.attachListener(h,CKEDITOR.env.webkit?"DOMFocusIn":"focus",function(){y&&CKEDITOR.env.webkit&&(y=c._.previousActive&&c._.previousActive.equals(k.getActive()))&&null!=c._.previousScrollTop&&c._.previousScrollTop!=
+h.$.scrollTop&&(h.$.scrollTop=c._.previousScrollTop);c.unlockSelection(y);y=0},null,null,-1);h.attachListener(h,"mousedown",function(){y=0});if(CKEDITOR.env.ie||q)x?h.attachListener(h,"beforedeactivate",a,null,null,-1):h.attachListener(c,"selectionCheck",a,null,null,-1),h.attachListener(h,CKEDITOR.env.webkit?"DOMFocusOut":"blur",function(){c.lockSelection(n);y=1},null,null,-1),h.attachListener(h,"mousedown",function(){y=0});if(CKEDITOR.env.ie&&!q){var m;h.attachListener(h,"mousedown",function(a){2==
+a.data.$.button&&((a=c.document.getSelection())&&a.getType()!=CKEDITOR.SELECTION_NONE||(m=c.window.getScrollPosition()))});h.attachListener(h,"mouseup",function(a){2==a.data.$.button&&m&&(c.document.$.documentElement.scrollLeft=m.x,c.document.$.documentElement.scrollTop=m.y);m=null});if("BackCompat"!=k.$.compatMode){if(CKEDITOR.env.ie7Compat||CKEDITOR.env.ie6Compat){var u,w;l.on("mousedown",function(a){function b(a){a=a.data.$;if(u){var c=r.$.createTextRange();try{c.moveToPoint(a.clientX,a.clientY)}catch(d){}u.setEndPoint(0>
+w.compareEndPoints("StartToStart",c)?"EndToEnd":"StartToStart",c);u.select()}}function c(){l.removeListener("mousemove",b);p.removeListener("mouseup",c);l.removeListener("mouseup",c);u.select()}a=a.data;if(a.getTarget().is("html")&&a.$.y<l.$.clientHeight&&a.$.x<l.$.clientWidth){u=r.$.createTextRange();try{u.moveToPoint(a.$.clientX,a.$.clientY)}catch(d){}w=u.duplicate();l.on("mousemove",b);p.on("mouseup",c);l.on("mouseup",c)}})}if(7<CKEDITOR.env.version&&11>CKEDITOR.env.version)l.on("mousedown",function(a){a.data.getTarget().is("html")&&
+(p.on("mouseup",b),l.on("mouseup",b))})}}h.attachListener(h,"selectionchange",e,c);h.attachListener(h,"keyup",g,c);h.attachListener(h,"keydown",function(a){var b=this.getSelection(1);d(b)&&(b.selectElement(d(b)),a.data.preventDefault())},c);h.attachListener(h,CKEDITOR.env.webkit?"DOMFocusIn":"focus",function(){c.forceNextSelectionCheck();c.selectionChange(1)});if(q&&(CKEDITOR.env.webkit||CKEDITOR.env.gecko)){var t;h.attachListener(h,"mousedown",function(){t=1});h.attachListener(k.getDocumentElement(),
+"mouseup",function(){t&&g.call(c);t=0})}else h.attachListener(CKEDITOR.env.ie?h:k.getDocumentElement(),"mouseup",g,c);CKEDITOR.env.webkit&&h.attachListener(k,"keydown",function(a){switch(a.data.getKey()){case 13:case 33:case 34:case 35:case 36:case 37:case 39:case 8:case 45:case 46:h.hasFocus&&f(h)}},null,null,-1);h.attachListener(h,"keydown",A(c),null,null,-1)});c.on("setData",function(){c.unlockSelection();CKEDITOR.env.webkit&&b()});c.on("contentDomUnload",function(){c.unlockSelection()});if(CKEDITOR.env.ie9Compat)c.on("beforeDestroy",
+b,null,null,9);c.on("dataReady",function(){delete c._.fakeSelection;delete c._.hiddenSelectionContainer;c.selectionChange(1)});c.on("loadSnapshot",function(){var a=CKEDITOR.dom.walker.nodeType(CKEDITOR.NODE_ELEMENT),b=c.editable().getLast(a);b&&b.hasAttribute("data-cke-hidden-sel")&&(b.remove(),CKEDITOR.env.gecko&&(a=c.editable().getFirst(a))&&a.is("br")&&a.getAttribute("_moz_editor_bogus_node")&&a.remove())},null,null,100);c.on("key",function(a){if("wysiwyg"==c.mode){var b=c.getSelection();if(b.isFake){var d=
+H[a.data.keyCode];if(d)return d({editor:c,selected:b.getSelectedElement(),selection:b,keyEvent:a})}}})});if(CKEDITOR.env.webkit)CKEDITOR.on("instanceReady",function(a){var b=a.editor;b.on("selectionChange",function(){var a=b.editable(),c=a.getCustomData("cke-fillingChar");c&&(c.getCustomData("ready")?(f(a),a.editor.fire("selectionCheck")):c.setCustomData("ready",1))},null,null,-1);b.on("beforeSetMode",function(){f(b.editable())},null,null,-1);b.on("getSnapshot",function(a){a.data&&(a.data=D(a.data))},
+b,null,20);b.on("toDataFormat",function(a){a.data.dataValue=D(a.data.dataValue)},null,null,0)});CKEDITOR.editor.prototype.selectionChange=function(a){(a?e:g).call(this)};CKEDITOR.editor.prototype.getSelection=function(a){return!this._.savedSelection&&!this._.fakeSelection||a?(a=this.editable())&&"wysiwyg"==this.mode?new CKEDITOR.dom.selection(a):null:this._.savedSelection||this._.fakeSelection};CKEDITOR.editor.prototype.lockSelection=function(a){a=a||this.getSelection(1);return a.getType()!=CKEDITOR.SELECTION_NONE?
+(!a.isLocked&&a.lock(),this._.savedSelection=a,!0):!1};CKEDITOR.editor.prototype.unlockSelection=function(a){var b=this._.savedSelection;return b?(b.unlock(a),delete this._.savedSelection,!0):!1};CKEDITOR.editor.prototype.forceNextSelectionCheck=function(){delete this._.selectionPreviousPath};CKEDITOR.dom.document.prototype.getSelection=function(){return new CKEDITOR.dom.selection(this)};CKEDITOR.dom.range.prototype.select=function(){var a=this.root instanceof CKEDITOR.editable?this.root.editor.getSelection():
+new CKEDITOR.dom.selection(this.root);a.selectRanges([this]);return a};CKEDITOR.SELECTION_NONE=1;CKEDITOR.SELECTION_TEXT=2;CKEDITOR.SELECTION_ELEMENT=3;CKEDITOR.dom.selection=function(a){if(a instanceof CKEDITOR.dom.selection){var b=a;a=a.root}var c=a instanceof CKEDITOR.dom.element;this.rev=b?b.rev:m++;this.document=a instanceof CKEDITOR.dom.document?a:a.getDocument();this.root=c?a:this.document.getBody();this.isLocked=0;this._={cache:{}};if(b)return CKEDITOR.tools.extend(this._.cache,b._.cache),
+this.isFake=b.isFake,this.isLocked=b.isLocked,this;a=this.getNative();var d,e;if(a)if(a.getRangeAt)d=(e=a.rangeCount&&a.getRangeAt(0))&&new CKEDITOR.dom.node(e.commonAncestorContainer);else{try{e=a.createRange()}catch(f){}d=e&&CKEDITOR.dom.element.get(e.item&&e.item(0)||e.parentElement())}if(!d||d.type!=CKEDITOR.NODE_ELEMENT&&d.type!=CKEDITOR.NODE_TEXT||!this.root.equals(d)&&!this.root.contains(d))this._.cache.type=CKEDITOR.SELECTION_NONE,this._.cache.startElement=null,this._.cache.selectedElement=
+null,this._.cache.selectedText="",this._.cache.ranges=new CKEDITOR.dom.rangeList;return this};var E={img:1,hr:1,li:1,table:1,tr:1,td:1,th:1,embed:1,object:1,ol:1,ul:1,a:1,input:1,form:1,select:1,textarea:1,button:1,fieldset:1,thead:1,tfoot:1};CKEDITOR.tools.extend(CKEDITOR.dom.selection,{_removeFillingCharSequenceString:D,_createFillingCharSequenceNode:u,FILLING_CHAR_SEQUENCE:K});CKEDITOR.dom.selection.prototype={getNative:function(){return void 0!==this._.cache.nativeSel?this._.cache.nativeSel:this._.cache.nativeSel=
+x?this.document.$.selection:this.document.getWindow().$.getSelection()},getType:x?function(){var a=this._.cache;if(a.type)return a.type;var b=CKEDITOR.SELECTION_NONE;try{var c=this.getNative(),d=c.type;"Text"==d&&(b=CKEDITOR.SELECTION_TEXT);"Control"==d&&(b=CKEDITOR.SELECTION_ELEMENT);c.createRange().parentElement()&&(b=CKEDITOR.SELECTION_TEXT)}catch(e){}return a.type=b}:function(){var a=this._.cache;if(a.type)return a.type;var b=CKEDITOR.SELECTION_TEXT,c=this.getNative();if(!c||!c.rangeCount)b=CKEDITOR.SELECTION_NONE;
+else if(1==c.rangeCount){var c=c.getRangeAt(0),d=c.startContainer;d==c.endContainer&&1==d.nodeType&&1==c.endOffset-c.startOffset&&E[d.childNodes[c.startOffset].nodeName.toLowerCase()]&&(b=CKEDITOR.SELECTION_ELEMENT)}return a.type=b},getRanges:function(){var a=x?function(){function a(b){return(new CKEDITOR.dom.node(b)).getIndex()}var b=function(b,c){b=b.duplicate();b.collapse(c);var d=b.parentElement();if(!d.hasChildNodes())return{container:d,offset:0};for(var e=d.children,f,g,h=b.duplicate(),k=0,
+l=e.length-1,q=-1,n,m;k<=l;)if(q=Math.floor((k+l)/2),f=e[q],h.moveToElementText(f),n=h.compareEndPoints("StartToStart",b),0<n)l=q-1;else if(0>n)k=q+1;else return{container:d,offset:a(f)};if(-1==q||q==e.length-1&&0>n){h.moveToElementText(d);h.setEndPoint("StartToStart",b);h=h.text.replace(/(\r\n|\r)/g,"\n").length;e=d.childNodes;if(!h)return f=e[e.length-1],f.nodeType!=CKEDITOR.NODE_TEXT?{container:d,offset:e.length}:{container:f,offset:f.nodeValue.length};for(d=e.length;0<h&&0<d;)g=e[--d],g.nodeType==
+CKEDITOR.NODE_TEXT&&(m=g,h-=g.nodeValue.length);return{container:m,offset:-h}}h.collapse(0<n?!0:!1);h.setEndPoint(0<n?"StartToStart":"EndToStart",b);h=h.text.replace(/(\r\n|\r)/g,"\n").length;if(!h)return{container:d,offset:a(f)+(0<n?0:1)};for(;0<h;)try{g=f[0<n?"previousSibling":"nextSibling"],g.nodeType==CKEDITOR.NODE_TEXT&&(h-=g.nodeValue.length,m=g),f=g}catch(u){return{container:d,offset:a(f)}}return{container:m,offset:0<n?-h:m.nodeValue.length+h}};return function(){var a=this.getNative(),c=a&&
+a.createRange(),d=this.getType();if(!a)return[];if(d==CKEDITOR.SELECTION_TEXT)return a=new CKEDITOR.dom.range(this.root),d=b(c,!0),a.setStart(new CKEDITOR.dom.node(d.container),d.offset),d=b(c),a.setEnd(new CKEDITOR.dom.node(d.container),d.offset),a.endContainer.getPosition(a.startContainer)&CKEDITOR.POSITION_PRECEDING&&a.endOffset<=a.startContainer.getIndex()&&a.collapse(),[a];if(d==CKEDITOR.SELECTION_ELEMENT){for(var d=[],e=0;e<c.length;e++){for(var f=c.item(e),g=f.parentNode,h=0,a=new CKEDITOR.dom.range(this.root);h<
+g.childNodes.length&&g.childNodes[h]!=f;h++);a.setStart(new CKEDITOR.dom.node(g),h);a.setEnd(new CKEDITOR.dom.node(g),h+1);d.push(a)}return d}return[]}}():function(){var a=[],b,c=this.getNative();if(!c)return a;for(var d=0;d<c.rangeCount;d++){var e=c.getRangeAt(d);b=new CKEDITOR.dom.range(this.root);b.setStart(new CKEDITOR.dom.node(e.startContainer),e.startOffset);b.setEnd(new CKEDITOR.dom.node(e.endContainer),e.endOffset);a.push(b)}return a};return function(b){var c=this._.cache,d=c.ranges;d||(c.ranges=
+d=new CKEDITOR.dom.rangeList(a.call(this)));return b?F(new CKEDITOR.dom.rangeList(d.slice())):d}}(),getStartElement:function(){var a=this._.cache;if(void 0!==a.startElement)return a.startElement;var b;switch(this.getType()){case CKEDITOR.SELECTION_ELEMENT:return this.getSelectedElement();case CKEDITOR.SELECTION_TEXT:var c=this.getRanges()[0];if(c){if(c.collapsed)b=c.startContainer,b.type!=CKEDITOR.NODE_ELEMENT&&(b=b.getParent());else{for(c.optimize();b=c.startContainer,c.startOffset==(b.getChildCount?
+b.getChildCount():b.getLength())&&!b.isBlockBoundary();)c.setStartAfter(b);b=c.startContainer;if(b.type!=CKEDITOR.NODE_ELEMENT)return b.getParent();if((b=b.getChild(c.startOffset))&&b.type==CKEDITOR.NODE_ELEMENT)for(c=b.getFirst();c&&c.type==CKEDITOR.NODE_ELEMENT;)b=c,c=c.getFirst();else b=c.startContainer}b=b.$}}return a.startElement=b?new CKEDITOR.dom.element(b):null},getSelectedElement:function(){var a=this._.cache;if(void 0!==a.selectedElement)return a.selectedElement;var b=this,c=CKEDITOR.tools.tryThese(function(){return b.getNative().createRange().item(0)},
+function(){for(var a=b.getRanges()[0].clone(),c,d,e=2;e&&!((c=a.getEnclosedNode())&&c.type==CKEDITOR.NODE_ELEMENT&&E[c.getName()]&&(d=c));e--)a.shrink(CKEDITOR.SHRINK_ELEMENT);return d&&d.$});return a.selectedElement=c?new CKEDITOR.dom.element(c):null},getSelectedText:function(){var a=this._.cache;if(void 0!==a.selectedText)return a.selectedText;var b=this.getNative(),b=x?"Control"==b.type?"":b.createRange().text:b.toString();return a.selectedText=b},lock:function(){this.getRanges();this.getStartElement();
+this.getSelectedElement();this.getSelectedText();this._.cache.nativeSel=null;this.isLocked=1},unlock:function(b){if(this.isLocked){if(b)var d=this.getSelectedElement(),e=this.getRanges(),f=this.isFake;this.isLocked=0;this.reset();b&&(b=d||e[0]&&e[0].getCommonAncestor())&&b.getAscendant("body",1)&&(a(e)?c.call(this,e):f?this.fake(d):d?this.selectElement(d):this.selectRanges(e))}},reset:function(){this._.cache={};this.isFake=0;var a=this.root.editor;if(a&&a._.fakeSelection)if(this.rev==a._.fakeSelection.rev){delete a._.fakeSelection;
+var b=a._.hiddenSelectionContainer;if(b){var c=a.checkDirty();a.fire("lockSnapshot");b.remove();a.fire("unlockSnapshot");!c&&a.resetDirty()}delete a._.hiddenSelectionContainer}else CKEDITOR.warn("selection-fake-reset");this.rev=m++},selectElement:function(a){var b=new CKEDITOR.dom.range(this.root);b.setStartBefore(a);b.setEndAfter(a);this.selectRanges([b])},selectRanges:function(b){var d=this.root.editor,e=d&&d._.hiddenSelectionContainer;this.reset();if(e)for(var e=this.root,g,h=0;h<b.length;++h)g=
+b[h],g.endContainer.equals(e)&&(g.endOffset=Math.min(g.endOffset,e.getChildCount()));if(b.length)if(this.isLocked){var k=CKEDITOR.document.getActive();this.unlock();this.selectRanges(b);this.lock();k&&!k.equals(this.root)&&k.focus()}else{var l;a:{var p,C;if(1==b.length&&!(C=b[0]).collapsed&&(l=C.getEnclosedNode())&&l.type==CKEDITOR.NODE_ELEMENT&&(C=C.clone(),C.shrink(CKEDITOR.SHRINK_ELEMENT,!0),(p=C.getEnclosedNode())&&p.type==CKEDITOR.NODE_ELEMENT&&(l=p),"false"==l.getAttribute("contenteditable")))break a;
+l=void 0}if(l)this.fake(l);else if(d&&d.plugins.tableselection&&CKEDITOR.plugins.tableselection.isSupportedEnvironment&&a(b)&&!I)c.call(this,b);else{if(x){p=CKEDITOR.dom.walker.whitespaces(!0);l=/\ufeff|\u00a0/;C={table:1,tbody:1,tr:1};1<b.length&&(d=b[b.length-1],b[0].setEnd(d.endContainer,d.endOffset));d=b[0];b=d.collapsed;var r,G,m;if((e=d.getEnclosedNode())&&e.type==CKEDITOR.NODE_ELEMENT&&e.getName()in E&&(!e.is("a")||!e.getText()))try{m=e.$.createControlRange();m.addElement(e.$);m.select();return}catch(w){}if(d.startContainer.type==
+CKEDITOR.NODE_ELEMENT&&d.startContainer.getName()in C||d.endContainer.type==CKEDITOR.NODE_ELEMENT&&d.endContainer.getName()in C)d.shrink(CKEDITOR.NODE_ELEMENT,!0),b=d.collapsed;m=d.createBookmark();C=m.startNode;b||(k=m.endNode);m=d.document.$.body.createTextRange();m.moveToElementText(C.$);m.moveStart("character",1);k?(l=d.document.$.body.createTextRange(),l.moveToElementText(k.$),m.setEndPoint("EndToEnd",l),m.moveEnd("character",-1)):(r=C.getNext(p),G=C.hasAscendant("pre"),r=!(r&&r.getText&&r.getText().match(l))&&
+(G||!C.hasPrevious()||C.getPrevious().is&&C.getPrevious().is("br")),G=d.document.createElement("span"),G.setHtml("\x26#65279;"),G.insertBefore(C),r&&d.document.createText("").insertBefore(C));d.setStartBefore(C);C.remove();b?(r?(m.moveStart("character",-1),m.select(),d.document.$.selection.clear()):m.select(),d.moveToPosition(G,CKEDITOR.POSITION_BEFORE_START),G.remove()):(d.setEndBefore(k),k.remove(),m.select())}else{k=this.getNative();if(!k)return;this.removeAllRanges();for(m=0;m<b.length;m++){if(m<
+b.length-1&&(r=b[m],G=b[m+1],l=r.clone(),l.setStart(r.endContainer,r.endOffset),l.setEnd(G.startContainer,G.startOffset),!l.collapsed&&(l.shrink(CKEDITOR.NODE_ELEMENT,!0),d=l.getCommonAncestor(),l=l.getEnclosedNode(),d.isReadOnly()||l&&l.isReadOnly()))){G.setStart(r.startContainer,r.startOffset);b.splice(m--,1);continue}d=b[m];G=this.document.$.createRange();d.collapsed&&CKEDITOR.env.webkit&&n(d)&&(l=u(this.root),d.insertNode(l),(r=l.getNext())&&!l.getPrevious()&&r.type==CKEDITOR.NODE_ELEMENT&&"br"==
+r.getName()?(f(this.root),d.moveToPosition(r,CKEDITOR.POSITION_BEFORE_START)):d.moveToPosition(l,CKEDITOR.POSITION_AFTER_END));G.setStart(d.startContainer.$,d.startOffset);try{G.setEnd(d.endContainer.$,d.endOffset)}catch(t){if(0<=t.toString().indexOf("NS_ERROR_ILLEGAL_VALUE"))d.collapse(1),G.setEnd(d.endContainer.$,d.endOffset);else throw t;}k.addRange(G)}}this.reset();this.root.fire("selectionchange")}}},fake:function(a,b){var c=this.root.editor;void 0===b&&a.hasAttribute("aria-label")&&(b=a.getAttribute("aria-label"));
+this.reset();w(c,b);var d=this._.cache,e=new CKEDITOR.dom.range(this.root);e.setStartBefore(a);e.setEndAfter(a);d.ranges=new CKEDITOR.dom.rangeList(e);d.selectedElement=d.startElement=a;d.type=CKEDITOR.SELECTION_ELEMENT;d.selectedText=d.nativeSel=null;this.isFake=1;this.rev=m++;c._.fakeSelection=this;this.root.fire("selectionchange")},isHidden:function(){var a=this.getCommonAncestor();a&&a.type==CKEDITOR.NODE_TEXT&&(a=a.getParent());return!(!a||!a.data("cke-hidden-sel"))},isInTable:function(b){return a(this.getRanges(),
+b)},createBookmarks:function(a){a=this.getRanges().createBookmarks(a);this.isFake&&(a.isFake=1);return a},createBookmarks2:function(a){a=this.getRanges().createBookmarks2(a);this.isFake&&(a.isFake=1);return a},selectBookmarks:function(b){for(var c=[],d,e=0;e<b.length;e++){var f=new CKEDITOR.dom.range(this.root);f.moveToBookmark(b[e]);c.push(f)}b.isFake&&(d=a(c)?c[0]._getTableElement():c[0].getEnclosedNode(),d&&d.type==CKEDITOR.NODE_ELEMENT||(CKEDITOR.warn("selection-not-fake"),b.isFake=0));b.isFake&&
+!a(c)?this.fake(d):this.selectRanges(c);return this},getCommonAncestor:function(){var a=this.getRanges();return a.length?a[0].startContainer.getCommonAncestor(a[a.length-1].endContainer):null},scrollIntoView:function(){this.type!=CKEDITOR.SELECTION_NONE&&this.getRanges()[0].scrollIntoView()},removeAllRanges:function(){if(this.getType()!=CKEDITOR.SELECTION_NONE){var a=this.getNative();try{a&&a[x?"empty":"removeAllRanges"]()}catch(b){}this.reset()}}}})();"use strict";CKEDITOR.STYLE_BLOCK=1;
+CKEDITOR.STYLE_INLINE=2;CKEDITOR.STYLE_OBJECT=3;
+(function(){function a(a,b){for(var c,d;(a=a.getParent())&&!a.equals(b);)if(a.getAttribute("data-nostyle"))c=a;else if(!d){var e=a.getAttribute("contentEditable");"false"==e?c=a:"true"==e&&(d=1)}return c}function d(a,b,c,d){return(a.getPosition(b)|d)==d&&(!c.childRule||c.childRule(a))}function b(c){var f=c.document;if(c.collapsed)f=K(this,f),c.insertNode(f),c.moveToPosition(f,CKEDITOR.POSITION_BEFORE_END);else{var g=this.element,h=this._.definition,k,l=h.ignoreReadonly,m=l||h.includeReadonly;null==
+m&&(m=c.root.getCustomData("cke_includeReadonly"));var n=CKEDITOR.dtd[g];n||(k=!0,n=CKEDITOR.dtd.span);c.enlarge(CKEDITOR.ENLARGE_INLINE,1);c.trim();var u=c.createBookmark(),w=u.startNode,t=u.endNode,q=w,v;if(!l){var x=c.getCommonAncestor(),l=a(w,x),x=a(t,x);l&&(q=l.getNextSourceNode(!0));x&&(t=x)}for(q.getPosition(t)==CKEDITOR.POSITION_FOLLOWING&&(q=0);q;){l=!1;if(q.equals(t))q=null,l=!0;else{var A=q.type==CKEDITOR.NODE_ELEMENT?q.getName():null,x=A&&"false"==q.getAttribute("contentEditable"),D=A&&
+q.getAttribute("data-nostyle");if(A&&q.data("cke-bookmark")){q=q.getNextSourceNode(!0);continue}if(x&&m&&CKEDITOR.dtd.$block[A])for(var z=q,y=e(z),E=void 0,H=y.length,I=0,z=H&&new CKEDITOR.dom.range(z.getDocument());I<H;++I){var E=y[I],J=CKEDITOR.filter.instances[E.data("cke-filter")];if(J?J.check(this):1)z.selectNodeContents(E),b.call(this,z)}y=A?!n[A]||D?0:x&&!m?0:d(q,t,h,W):1;if(y)if(E=q.getParent(),y=h,H=g,I=k,!E||!(E.getDtd()||CKEDITOR.dtd.span)[H]&&!I||y.parentRule&&!y.parentRule(E))l=!0;else{if(v||
+A&&CKEDITOR.dtd.$removeEmpty[A]&&(q.getPosition(t)|W)!=W||(v=c.clone(),v.setStartBefore(q)),A=q.type,A==CKEDITOR.NODE_TEXT||x||A==CKEDITOR.NODE_ELEMENT&&!q.getChildCount()){for(var A=q,L;(l=!A.getNext(B))&&(L=A.getParent(),n[L.getName()])&&d(L,w,h,O);)A=L;v.setEndAfter(A)}}else l=!0;q=q.getNextSourceNode(D||x)}if(l&&v&&!v.collapsed){for(var l=K(this,f),x=l.hasAttributes(),D=v.getCommonAncestor(),A={},y={},E={},H={},U,R,ba;l&&D;){if(D.getName()==g){for(U in h.attributes)!H[U]&&(ba=D.getAttribute(R))&&
+(l.getAttribute(U)==ba?y[U]=1:H[U]=1);for(R in h.styles)!E[R]&&(ba=D.getStyle(R))&&(l.getStyle(R)==ba?A[R]=1:E[R]=1)}D=D.getParent()}for(U in y)l.removeAttribute(U);for(R in A)l.removeStyle(R);x&&!l.hasAttributes()&&(l=null);l?(v.extractContents().appendTo(l),v.insertNode(l),F.call(this,l),l.mergeSiblings(),CKEDITOR.env.ie||l.$.normalize()):(l=new CKEDITOR.dom.element("span"),v.extractContents().appendTo(l),v.insertNode(l),F.call(this,l),l.remove(!0));v=null}}c.moveToBookmark(u);c.shrink(CKEDITOR.SHRINK_TEXT);
+c.shrink(CKEDITOR.NODE_ELEMENT,!0)}}function c(a){function b(){for(var a=new CKEDITOR.dom.elementPath(d.getParent()),c=new CKEDITOR.dom.elementPath(n.getParent()),e=null,f=null,g=0;g<a.elements.length;g++){var h=a.elements[g];if(h==a.block||h==a.blockLimit)break;u.checkElementRemovable(h,!0)&&(e=h)}for(g=0;g<c.elements.length;g++){h=c.elements[g];if(h==c.block||h==c.blockLimit)break;u.checkElementRemovable(h,!0)&&(f=h)}f&&n.breakParent(f);e&&d.breakParent(e)}a.enlarge(CKEDITOR.ENLARGE_INLINE,1);var c=
+a.createBookmark(),d=c.startNode,e=this._.definition.alwaysRemoveElement;if(a.collapsed){for(var f=new CKEDITOR.dom.elementPath(d.getParent(),a.root),g,h=0,k;h<f.elements.length&&(k=f.elements[h])&&k!=f.block&&k!=f.blockLimit;h++)if(this.checkElementRemovable(k)){var m;!e&&a.collapsed&&(a.checkBoundaryOfElement(k,CKEDITOR.END)||(m=a.checkBoundaryOfElement(k,CKEDITOR.START)))?(g=k,g.match=m?"start":"end"):(k.mergeSiblings(),k.is(this.element)?A.call(this,k):x(k,l(this)[k.getName()]))}if(g){e=d;for(h=
+0;;h++){k=f.elements[h];if(k.equals(g))break;else if(k.match)continue;else k=k.clone();k.append(e);e=k}e["start"==g.match?"insertBefore":"insertAfter"](g)}}else{var n=c.endNode,u=this;b();for(f=d;!f.equals(n);)g=f.getNextSourceNode(),f.type==CKEDITOR.NODE_ELEMENT&&this.checkElementRemovable(f)&&(f.getName()==this.element?A.call(this,f):x(f,l(this)[f.getName()]),g.type==CKEDITOR.NODE_ELEMENT&&g.contains(d)&&(b(),g=d.getNext())),f=g}a.moveToBookmark(c);a.shrink(CKEDITOR.NODE_ELEMENT,!0)}function e(a){var b=
+[];a.forEach(function(a){if("true"==a.getAttribute("contenteditable"))return b.push(a),!1},CKEDITOR.NODE_ELEMENT,!0);return b}function g(a){var b=a.getEnclosedNode()||a.getCommonAncestor(!1,!0);(a=(new CKEDITOR.dom.elementPath(b,a.root)).contains(this.element,1))&&!a.isReadOnly()&&z(a,this)}function h(a){var b=a.getCommonAncestor(!0,!0);if(a=(new CKEDITOR.dom.elementPath(b,a.root)).contains(this.element,1)){var b=this._.definition,c=b.attributes;if(c)for(var d in c)a.removeAttribute(d,c[d]);if(b.styles)for(var e in b.styles)b.styles.hasOwnProperty(e)&&
+a.removeStyle(e)}}function k(a){var b=a.createBookmark(!0),c=a.createIterator();c.enforceRealBlocks=!0;this._.enterMode&&(c.enlargeBr=this._.enterMode!=CKEDITOR.ENTER_BR);for(var d,e=a.document,f;d=c.getNextParagraph();)!d.isReadOnly()&&(c.activeFilter?c.activeFilter.check(this):1)&&(f=K(this,e,d),u(d,f));a.moveToBookmark(b)}function n(a){var b=a.createBookmark(1),c=a.createIterator();c.enforceRealBlocks=!0;c.enlargeBr=this._.enterMode!=CKEDITOR.ENTER_BR;for(var d,e;d=c.getNextParagraph();)this.checkElementRemovable(d)&&
+(d.is("pre")?((e=this._.enterMode==CKEDITOR.ENTER_BR?null:a.document.createElement(this._.enterMode==CKEDITOR.ENTER_P?"p":"div"))&&d.copyAttributes(e),u(d,e)):A.call(this,d));a.moveToBookmark(b)}function u(a,b){var c=!b;c&&(b=a.getDocument().createElement("div"),a.copyAttributes(b));var d=b&&b.is("pre"),e=a.is("pre"),g=!d&&e;if(d&&!e){e=b;(g=a.getBogus())&&g.remove();g=a.getHtml();g=D(g,/(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g,"");g=g.replace(/[ \t\r\n]*(<br[^>]*>)[ \t\r\n]*/gi,"$1");g=g.replace(/([ \t\n\r]+|&nbsp;)/g,
+" ");g=g.replace(/<br\b[^>]*>/gi,"\n");if(CKEDITOR.env.ie){var h=a.getDocument().createElement("div");h.append(e);e.$.outerHTML="\x3cpre\x3e"+g+"\x3c/pre\x3e";e.copyAttributes(h.getFirst());e=h.getFirst().remove()}else e.setHtml(g);b=e}else g?b=w(c?[a.getHtml()]:f(a),b):a.moveChildren(b);b.replace(a);if(d){var c=b,k;(k=c.getPrevious(L))&&k.type==CKEDITOR.NODE_ELEMENT&&k.is("pre")&&(d=D(k.getHtml(),/\n$/,"")+"\n\n"+D(c.getHtml(),/^\n/,""),CKEDITOR.env.ie?c.$.outerHTML="\x3cpre\x3e"+d+"\x3c/pre\x3e":
+c.setHtml(d),k.remove())}else c&&m(b)}function f(a){var b=[];D(a.getOuterHtml(),/(\S\s*)\n(?:\s|(<span[^>]+data-cke-bookmark.*?\/span>))*\n(?!$)/gi,function(a,b,c){return b+"\x3c/pre\x3e"+c+"\x3cpre\x3e"}).replace(/<pre\b.*?>([\s\S]*?)<\/pre>/gi,function(a,c){b.push(c)});return b}function D(a,b,c){var d="",e="";a=a.replace(/(^<span[^>]+data-cke-bookmark.*?\/span>)|(<span[^>]+data-cke-bookmark.*?\/span>$)/gi,function(a,b,c){b&&(d=b);c&&(e=c);return""});return d+a.replace(b,c)+e}function w(a,b){var c;
+1<a.length&&(c=new CKEDITOR.dom.documentFragment(b.getDocument()));for(var d=0;d<a.length;d++){var e=a[d],e=e.replace(/(\r\n|\r)/g,"\n"),e=D(e,/^[ \t]*\n/,""),e=D(e,/\n$/,""),e=D(e,/^[ \t]+|[ \t]+$/g,function(a,b){return 1==a.length?"\x26nbsp;":b?" "+CKEDITOR.tools.repeat("\x26nbsp;",a.length-1):CKEDITOR.tools.repeat("\x26nbsp;",a.length-1)+" "}),e=e.replace(/\n/g,"\x3cbr\x3e"),e=e.replace(/[ \t]{2,}/g,function(a){return CKEDITOR.tools.repeat("\x26nbsp;",a.length-1)+" "});if(c){var f=b.clone();f.setHtml(e);
+c.append(f)}else b.setHtml(e)}return c||b}function A(a,b){var c=this._.definition,d=c.attributes,c=c.styles,e=l(this)[a.getName()],f=CKEDITOR.tools.isEmpty(d)&&CKEDITOR.tools.isEmpty(c),g;for(g in d)if("class"!=g&&!this._.definition.fullMatch||a.getAttribute(g)==t(g,d[g]))b&&"data-"==g.slice(0,5)||(f=a.hasAttribute(g),a.removeAttribute(g));for(var h in c)this._.definition.fullMatch&&a.getStyle(h)!=t(h,c[h],!0)||(f=f||!!a.getStyle(h),a.removeStyle(h));x(a,e,E[a.getName()]);f&&(this._.definition.alwaysRemoveElement?
+m(a,1):!CKEDITOR.dtd.$block[a.getName()]||this._.enterMode==CKEDITOR.ENTER_BR&&!a.hasAttributes()?m(a):a.renameNode(this._.enterMode==CKEDITOR.ENTER_P?"p":"div"))}function F(a){for(var b=l(this),c=a.getElementsByTag(this.element),d,e=c.count();0<=--e;)d=c.getItem(e),d.isReadOnly()||A.call(this,d,!0);for(var f in b)if(f!=this.element)for(c=a.getElementsByTag(f),e=c.count()-1;0<=e;e--)d=c.getItem(e),d.isReadOnly()||x(d,b[f])}function x(a,b,c){if(b=b&&b.attributes)for(var d=0;d<b.length;d++){var e=b[d][0],
+f;if(f=a.getAttribute(e)){var g=b[d][1];(null===g||g.test&&g.test(f)||"string"==typeof g&&f==g)&&a.removeAttribute(e)}}c||m(a)}function m(a,b){if(!a.hasAttributes()||b)if(CKEDITOR.dtd.$block[a.getName()]){var c=a.getPrevious(L),d=a.getNext(L);!c||c.type!=CKEDITOR.NODE_TEXT&&c.isBlockBoundary({br:1})||a.append("br",1);!d||d.type!=CKEDITOR.NODE_TEXT&&d.isBlockBoundary({br:1})||a.append("br");a.remove(!0)}else c=a.getFirst(),d=a.getLast(),a.remove(!0),c&&(c.type==CKEDITOR.NODE_ELEMENT&&c.mergeSiblings(),
+d&&!c.equals(d)&&d.type==CKEDITOR.NODE_ELEMENT&&d.mergeSiblings())}function K(a,b,c){var d;d=a.element;"*"==d&&(d="span");d=new CKEDITOR.dom.element(d,b);c&&c.copyAttributes(d);d=z(d,a);b.getCustomData("doc_processing_style")&&d.hasAttribute("id")?d.removeAttribute("id"):b.setCustomData("doc_processing_style",1);return d}function z(a,b){var c=b._.definition,d=c.attributes,c=CKEDITOR.style.getStyleText(c);if(d)for(var e in d)a.setAttribute(e,d[e]);c&&a.setAttribute("style",c);return a}function I(a,
+b){for(var c in a)a[c]=a[c].replace(v,function(a,c){return b[c]})}function l(a){if(a._.overrides)return a._.overrides;var b=a._.overrides={},c=a._.definition.overrides;if(c){CKEDITOR.tools.isArray(c)||(c=[c]);for(var d=0;d<c.length;d++){var e=c[d],f,g;"string"==typeof e?f=e.toLowerCase():(f=e.element?e.element.toLowerCase():a.element,g=e.attributes);e=b[f]||(b[f]={});if(g){var e=e.attributes=e.attributes||[],h;for(h in g)e.push([h.toLowerCase(),g[h]])}}}return b}function t(a,b,c){var d=new CKEDITOR.dom.element("span");
+d[c?"setStyle":"setAttribute"](a,b);return d[c?"getStyle":"getAttribute"](a)}function J(a,b){function c(a,b){return"font-family"==b.toLowerCase()?a.replace(/["']/g,""):a}"string"==typeof a&&(a=CKEDITOR.tools.parseCssText(a));"string"==typeof b&&(b=CKEDITOR.tools.parseCssText(b,!0));for(var d in a)if(!(d in b)||c(b[d],d)!=c(a[d],d)&&"inherit"!=a[d]&&"inherit"!=b[d])return!1;return!0}function H(a,b,c){var d=a.document,e=a.getRanges();b=b?this.removeFromRange:this.applyToRange;var f,g;if(a.isFake&&a.isInTable())for(f=
+[],g=0;g<e.length;g++)f.push(e[g].clone());for(var h=e.createIterator();g=h.getNextRange();)b.call(this,g,c);a.selectRanges(f||e);d.removeCustomData("doc_processing_style")}var E={address:1,div:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,p:1,pre:1,section:1,header:1,footer:1,nav:1,article:1,aside:1,figure:1,dialog:1,hgroup:1,time:1,meter:1,menu:1,command:1,keygen:1,output:1,progress:1,details:1,datagrid:1,datalist:1},q={a:1,blockquote:1,embed:1,hr:1,img:1,li:1,object:1,ol:1,table:1,td:1,tr:1,th:1,ul:1,dl:1,dt:1,
+dd:1,form:1,audio:1,video:1},y=/\s*(?:;\s*|$)/,v=/#\((.+?)\)/g,B=CKEDITOR.dom.walker.bookmark(0,1),L=CKEDITOR.dom.walker.whitespaces(1);CKEDITOR.style=function(a,b){if("string"==typeof a.type)return new CKEDITOR.style.customHandlers[a.type](a);var c=a.attributes;c&&c.style&&(a.styles=CKEDITOR.tools.extend({},a.styles,CKEDITOR.tools.parseCssText(c.style)),delete c.style);b&&(a=CKEDITOR.tools.clone(a),I(a.attributes,b),I(a.styles,b));c=this.element=a.element?"string"==typeof a.element?a.element.toLowerCase():
+a.element:"*";this.type=a.type||(E[c]?CKEDITOR.STYLE_BLOCK:q[c]?CKEDITOR.STYLE_OBJECT:CKEDITOR.STYLE_INLINE);"object"==typeof this.element&&(this.type=CKEDITOR.STYLE_OBJECT);this._={definition:a}};CKEDITOR.style.prototype={apply:function(a){if(a instanceof CKEDITOR.dom.document)return H.call(this,a.getSelection());if(this.checkApplicable(a.elementPath(),a)){var b=this._.enterMode;b||(this._.enterMode=a.activeEnterMode);H.call(this,a.getSelection(),0,a);this._.enterMode=b}},remove:function(a){if(a instanceof
+CKEDITOR.dom.document)return H.call(this,a.getSelection(),1);if(this.checkApplicable(a.elementPath(),a)){var b=this._.enterMode;b||(this._.enterMode=a.activeEnterMode);H.call(this,a.getSelection(),1,a);this._.enterMode=b}},applyToRange:function(a){this.applyToRange=this.type==CKEDITOR.STYLE_INLINE?b:this.type==CKEDITOR.STYLE_BLOCK?k:this.type==CKEDITOR.STYLE_OBJECT?g:null;return this.applyToRange(a)},removeFromRange:function(a){this.removeFromRange=this.type==CKEDITOR.STYLE_INLINE?c:this.type==CKEDITOR.STYLE_BLOCK?
+n:this.type==CKEDITOR.STYLE_OBJECT?h:null;return this.removeFromRange(a)},applyToObject:function(a){z(a,this)},checkActive:function(a,b){switch(this.type){case CKEDITOR.STYLE_BLOCK:return this.checkElementRemovable(a.block||a.blockLimit,!0,b);case CKEDITOR.STYLE_OBJECT:case CKEDITOR.STYLE_INLINE:for(var c=a.elements,d=0,e;d<c.length;d++)if(e=c[d],this.type!=CKEDITOR.STYLE_INLINE||e!=a.block&&e!=a.blockLimit){if(this.type==CKEDITOR.STYLE_OBJECT){var f=e.getName();if(!("string"==typeof this.element?
+f==this.element:f in this.element))continue}if(this.checkElementRemovable(e,!0,b))return!0}}return!1},checkApplicable:function(a,b,c){b&&b instanceof CKEDITOR.filter&&(c=b);if(c&&!c.check(this))return!1;switch(this.type){case CKEDITOR.STYLE_OBJECT:return!!a.contains(this.element);case CKEDITOR.STYLE_BLOCK:return!!a.blockLimit.getDtd()[this.element]}return!0},checkElementMatch:function(a,b){var c=this._.definition;if(!a||!c.ignoreReadonly&&a.isReadOnly())return!1;var d=a.getName();if("string"==typeof this.element?
+d==this.element:d in this.element){if(!b&&!a.hasAttributes())return!0;if(d=c._AC)c=d;else{var d={},e=0,f=c.attributes;if(f)for(var g in f)e++,d[g]=f[g];if(g=CKEDITOR.style.getStyleText(c))d.style||e++,d.style=g;d._length=e;c=c._AC=d}if(c._length){for(var h in c)if("_length"!=h)if(d=a.getAttribute(h)||"","style"==h?J(c[h],d):c[h]==d){if(!b)return!0}else if(b)return!1;if(b)return!0}else return!0}return!1},checkElementRemovable:function(a,b,c){if(this.checkElementMatch(a,b,c))return!0;if(b=l(this)[a.getName()]){var d;
+if(!(b=b.attributes))return!0;for(c=0;c<b.length;c++)if(d=b[c][0],d=a.getAttribute(d)){var e=b[c][1];if(null===e)return!0;if("string"==typeof e){if(d==e)return!0}else if(e.test(d))return!0}}return!1},buildPreview:function(a){var b=this._.definition,c=[],d=b.element;"bdo"==d&&(d="span");var c=["\x3c",d],e=b.attributes;if(e)for(var f in e)c.push(" ",f,'\x3d"',e[f],'"');(e=CKEDITOR.style.getStyleText(b))&&c.push(' style\x3d"',e,'"');c.push("\x3e",a||b.name,"\x3c/",d,"\x3e");return c.join("")},getDefinition:function(){return this._.definition}};
+CKEDITOR.style.getStyleText=function(a){var b=a._ST;if(b)return b;var b=a.styles,c=a.attributes&&a.attributes.style||"",d="";c.length&&(c=c.replace(y,";"));for(var e in b){var f=b[e],g=(e+":"+f).replace(y,";");"inherit"==f?d+=g:c+=g}c.length&&(c=CKEDITOR.tools.normalizeCssText(c,!0));return a._ST=c+d};CKEDITOR.style.customHandlers={};CKEDITOR.style.addCustomHandler=function(a){var b=function(a){this._={definition:a};this.setup&&this.setup(a)};b.prototype=CKEDITOR.tools.extend(CKEDITOR.tools.prototypedCopy(CKEDITOR.style.prototype),
+{assignedTo:CKEDITOR.STYLE_OBJECT},a,!0);return this.customHandlers[a.type]=b};var W=CKEDITOR.POSITION_PRECEDING|CKEDITOR.POSITION_IDENTICAL|CKEDITOR.POSITION_IS_CONTAINED,O=CKEDITOR.POSITION_FOLLOWING|CKEDITOR.POSITION_IDENTICAL|CKEDITOR.POSITION_IS_CONTAINED})();CKEDITOR.styleCommand=function(a,d){this.requiredContent=this.allowedContent=this.style=a;CKEDITOR.tools.extend(this,d,!0)};
+CKEDITOR.styleCommand.prototype.exec=function(a){a.focus();this.state==CKEDITOR.TRISTATE_OFF?a.applyStyle(this.style):this.state==CKEDITOR.TRISTATE_ON&&a.removeStyle(this.style)};CKEDITOR.stylesSet=new CKEDITOR.resourceManager("","stylesSet");CKEDITOR.addStylesSet=CKEDITOR.tools.bind(CKEDITOR.stylesSet.add,CKEDITOR.stylesSet);CKEDITOR.loadStylesSet=function(a,d,b){CKEDITOR.stylesSet.addExternal(a,d,"");CKEDITOR.stylesSet.load(a,b)};
+CKEDITOR.tools.extend(CKEDITOR.editor.prototype,{attachStyleStateChange:function(a,d){var b=this._.styleStateChangeCallbacks;b||(b=this._.styleStateChangeCallbacks=[],this.on("selectionChange",function(a){for(var d=0;d<b.length;d++){var g=b[d],h=g.style.checkActive(a.data.path,this)?CKEDITOR.TRISTATE_ON:CKEDITOR.TRISTATE_OFF;g.fn.call(this,h)}}));b.push({style:a,fn:d})},applyStyle:function(a){a.apply(this)},removeStyle:function(a){a.remove(this)},getStylesSet:function(a){if(this._.stylesDefinitions)a(this._.stylesDefinitions);
+else{var d=this,b=d.config.stylesCombo_stylesSet||d.config.stylesSet;if(!1===b)a(null);else if(b instanceof Array)d._.stylesDefinitions=b,a(b);else{b||(b="default");var b=b.split(":"),c=b[0];CKEDITOR.stylesSet.addExternal(c,b[1]?b.slice(1).join(":"):CKEDITOR.getUrl("styles.js"),"");CKEDITOR.stylesSet.load(c,function(b){d._.stylesDefinitions=b[c];a(d._.stylesDefinitions)})}}}});
+CKEDITOR.dom.comment=function(a,d){"string"==typeof a&&(a=(d?d.$:document).createComment(a));CKEDITOR.dom.domObject.call(this,a)};CKEDITOR.dom.comment.prototype=new CKEDITOR.dom.node;CKEDITOR.tools.extend(CKEDITOR.dom.comment.prototype,{type:CKEDITOR.NODE_COMMENT,getOuterHtml:function(){return"\x3c!--"+this.$.nodeValue+"--\x3e"}});"use strict";
+(function(){var a={},d={},b;for(b in CKEDITOR.dtd.$blockLimit)b in CKEDITOR.dtd.$list||(a[b]=1);for(b in CKEDITOR.dtd.$block)b in CKEDITOR.dtd.$blockLimit||b in CKEDITOR.dtd.$empty||(d[b]=1);CKEDITOR.dom.elementPath=function(b,e){var g=null,h=null,k=[],n=b,u;e=e||b.getDocument().getBody();n||(n=e);do if(n.type==CKEDITOR.NODE_ELEMENT){k.push(n);if(!this.lastElement&&(this.lastElement=n,n.is(CKEDITOR.dtd.$object)||"false"==n.getAttribute("contenteditable")))continue;if(n.equals(e))break;if(!h&&(u=n.getName(),
+"true"==n.getAttribute("contenteditable")?h=n:!g&&d[u]&&(g=n),a[u])){if(u=!g&&"div"==u){a:{u=n.getChildren();for(var f=0,D=u.count();f<D;f++){var w=u.getItem(f);if(w.type==CKEDITOR.NODE_ELEMENT&&CKEDITOR.dtd.$block[w.getName()]){u=!0;break a}}u=!1}u=!u}u?g=n:h=n}}while(n=n.getParent());h||(h=e);this.block=g;this.blockLimit=h;this.root=e;this.elements=k}})();
+CKEDITOR.dom.elementPath.prototype={compare:function(a){var d=this.elements;a=a&&a.elements;if(!a||d.length!=a.length)return!1;for(var b=0;b<d.length;b++)if(!d[b].equals(a[b]))return!1;return!0},contains:function(a,d,b){var c=0,e;"string"==typeof a&&(e=function(b){return b.getName()==a});a instanceof CKEDITOR.dom.element?e=function(b){return b.equals(a)}:CKEDITOR.tools.isArray(a)?e=function(b){return-1<CKEDITOR.tools.indexOf(a,b.getName())}:"function"==typeof a?e=a:"object"==typeof a&&(e=function(b){return b.getName()in
+a});var g=this.elements,h=g.length;d&&(b?c+=1:--h);b&&(g=Array.prototype.slice.call(g,0),g.reverse());for(;c<h;c++)if(e(g[c]))return g[c];return null},isContextFor:function(a){var d;return a in CKEDITOR.dtd.$block?(d=this.contains(CKEDITOR.dtd.$intermediate)||this.root.equals(this.block)&&this.block||this.blockLimit,!!d.getDtd()[a]):!0},direction:function(){return(this.block||this.blockLimit||this.root).getDirection(1)}};
+CKEDITOR.dom.text=function(a,d){"string"==typeof a&&(a=(d?d.$:document).createTextNode(a));this.$=a};CKEDITOR.dom.text.prototype=new CKEDITOR.dom.node;
+CKEDITOR.tools.extend(CKEDITOR.dom.text.prototype,{type:CKEDITOR.NODE_TEXT,getLength:function(){return this.$.nodeValue.length},getText:function(){return this.$.nodeValue},setText:function(a){this.$.nodeValue=a},split:function(a){var d=this.$.parentNode,b=d.childNodes.length,c=this.getLength(),e=this.getDocument(),g=new CKEDITOR.dom.text(this.$.splitText(a),e);d.childNodes.length==b&&(a>=c?(g=e.createText(""),g.insertAfter(this)):(a=e.createText(""),a.insertAfter(g),a.remove()));return g},substring:function(a,
+d){return"number"!=typeof d?this.$.nodeValue.substr(a):this.$.nodeValue.substring(a,d)}});
+(function(){function a(a,c,d){var g=a.serializable,h=c[d?"endContainer":"startContainer"],k=d?"endOffset":"startOffset",n=g?c.document.getById(a.startNode):a.startNode;a=g?c.document.getById(a.endNode):a.endNode;h.equals(n.getPrevious())?(c.startOffset=c.startOffset-h.getLength()-a.getPrevious().getLength(),h=a.getNext()):h.equals(a.getPrevious())&&(c.startOffset-=h.getLength(),h=a.getNext());h.equals(n.getParent())&&c[k]++;h.equals(a.getParent())&&c[k]++;c[d?"endContainer":"startContainer"]=h;return c}
+CKEDITOR.dom.rangeList=function(a){if(a instanceof CKEDITOR.dom.rangeList)return a;a?a instanceof CKEDITOR.dom.range&&(a=[a]):a=[];return CKEDITOR.tools.extend(a,d)};var d={createIterator:function(){var a=this,c=CKEDITOR.dom.walker.bookmark(),d=[],g;return{getNextRange:function(h){g=void 0===g?0:g+1;var k=a[g];if(k&&1<a.length){if(!g)for(var n=a.length-1;0<=n;n--)d.unshift(a[n].createBookmark(!0));if(h)for(var u=0;a[g+u+1];){var f=k.document;h=0;n=f.getById(d[u].endNode);for(f=f.getById(d[u+1].startNode);;){n=
+n.getNextSourceNode(!1);if(f.equals(n))h=1;else if(c(n)||n.type==CKEDITOR.NODE_ELEMENT&&n.isBlockBoundary())continue;break}if(!h)break;u++}for(k.moveToBookmark(d.shift());u--;)n=a[++g],n.moveToBookmark(d.shift()),k.setEnd(n.endContainer,n.endOffset)}return k}}},createBookmarks:function(b){for(var c=[],d,g=0;g<this.length;g++){c.push(d=this[g].createBookmark(b,!0));for(var h=g+1;h<this.length;h++)this[h]=a(d,this[h]),this[h]=a(d,this[h],!0)}return c},createBookmarks2:function(a){for(var c=[],d=0;d<
+this.length;d++)c.push(this[d].createBookmark2(a));return c},moveToBookmarks:function(a){for(var c=0;c<this.length;c++)this[c].moveToBookmark(a[c])}}})();
+(function(){function a(){return CKEDITOR.getUrl(CKEDITOR.skinName.split(",")[1]||"skins/"+CKEDITOR.skinName.split(",")[0]+"/")}function d(b){var c=CKEDITOR.skin["ua_"+b],d=CKEDITOR.env;if(c)for(var c=c.split(",").sort(function(a,b){return a>b?-1:1}),e=0,g;e<c.length;e++)if(g=c[e],d.ie&&(g.replace(/^ie/,"")==d.version||d.quirks&&"iequirks"==g)&&(g="ie"),d[g]){b+="_"+c[e];break}return CKEDITOR.getUrl(a()+b+".css")}function b(a,b){g[a]||(CKEDITOR.document.appendStyleSheet(d(a)),g[a]=1);b&&b()}function c(a){var b=
+a.getById(h);b||(b=a.getHead().append("style"),b.setAttribute("id",h),b.setAttribute("type","text/css"));return b}function e(a,b,c){var d,e,g;if(CKEDITOR.env.webkit)for(b=b.split("}").slice(0,-1),e=0;e<b.length;e++)b[e]=b[e].split("{");for(var h=0;h<a.length;h++)if(CKEDITOR.env.webkit)for(e=0;e<b.length;e++){g=b[e][1];for(d=0;d<c.length;d++)g=g.replace(c[d][0],c[d][1]);a[h].$.sheet.addRule(b[e][0],g)}else{g=b;for(d=0;d<c.length;d++)g=g.replace(c[d][0],c[d][1]);CKEDITOR.env.ie&&11>CKEDITOR.env.version?
+a[h].$.styleSheet.cssText+=g:a[h].$.innerHTML+=g}}var g={};CKEDITOR.skin={path:a,loadPart:function(c,d){CKEDITOR.skin.name!=CKEDITOR.skinName.split(",")[0]?CKEDITOR.scriptLoader.load(CKEDITOR.getUrl(a()+"skin.js"),function(){b(c,d)}):b(c,d)},getPath:function(a){return CKEDITOR.getUrl(d(a))},icons:{},addIcon:function(a,b,c,d){a=a.toLowerCase();this.icons[a]||(this.icons[a]={path:b,offset:c||0,bgsize:d||"16px"})},getIconStyle:function(a,b,c,d,e){var g;a&&(a=a.toLowerCase(),b&&(g=this.icons[a+"-rtl"]),
+g||(g=this.icons[a]));a=c||g&&g.path||"";d=d||g&&g.offset;e=e||g&&g.bgsize||"16px";a&&(a=a.replace(/'/g,"\\'"));return a&&"background-image:url('"+CKEDITOR.getUrl(a)+"');background-position:0 "+d+"px;background-size:"+e+";"}};CKEDITOR.tools.extend(CKEDITOR.editor.prototype,{getUiColor:function(){return this.uiColor},setUiColor:function(a){var b=c(CKEDITOR.document);return(this.setUiColor=function(a){this.uiColor=a;var c=CKEDITOR.skin.chameleon,d="",g="";"function"==typeof c&&(d=c(this,"editor"),g=
+c(this,"panel"));a=[[n,a]];e([b],d,a);e(k,g,a)}).call(this,a)}});var h="cke_ui_color",k=[],n=/\$color/g;CKEDITOR.on("instanceLoaded",function(a){if(!CKEDITOR.env.ie||!CKEDITOR.env.quirks){var b=a.editor;a=function(a){a=(a.data[0]||a.data).element.getElementsByTag("iframe").getItem(0).getFrameDocument();if(!a.getById("cke_ui_color")){a=c(a);k.push(a);var d=b.getUiColor();d&&e([a],CKEDITOR.skin.chameleon(b,"panel"),[[n,d]])}};b.on("panelShow",a);b.on("menuShow",a);b.config.uiColor&&b.setUiColor(b.config.uiColor)}})})();
+(function(){if(CKEDITOR.env.webkit)CKEDITOR.env.hc=!1;else{var a=CKEDITOR.dom.element.createFromHtml('\x3cdiv style\x3d"width:0;height:0;position:absolute;left:-10000px;border:1px solid;border-color:red blue"\x3e\x3c/div\x3e',CKEDITOR.document);a.appendTo(CKEDITOR.document.getHead());try{var d=a.getComputedStyle("border-top-color"),b=a.getComputedStyle("border-right-color");CKEDITOR.env.hc=!(!d||d!=b)}catch(c){CKEDITOR.env.hc=!1}a.remove()}CKEDITOR.env.hc&&(CKEDITOR.env.cssClass+=" cke_hc");CKEDITOR.document.appendStyleText(".cke{visibility:hidden;}");
+CKEDITOR.status="loaded";CKEDITOR.fireOnce("loaded");if(a=CKEDITOR._.pending)for(delete CKEDITOR._.pending,d=0;d<a.length;d++)CKEDITOR.editor.prototype.constructor.apply(a[d][0],a[d][1]),CKEDITOR.add(a[d][0])})();/*
+ Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  For licensing, see LICENSE.md or http://ckeditor.com/license
 */
-CKEDITOR.skin.name="moono";CKEDITOR.skin.ua_editor="ie,iequirks,ie7,ie8,gecko";CKEDITOR.skin.ua_dialog="ie,iequirks,ie7,ie8,opera";
-CKEDITOR.skin.chameleon=function(){var b=function(){return function(b,e){for(var a=b.match(/[^#]./g),c=0;3>c;c++){var f=a,h=c,d;d=parseInt(a[c],16);d=("0"+(0>e?0|d*(1+e):0|d+(255-d)*e).toString(16)).slice(-2);f[h]=d}return"#"+a.join("")}}(),c=function(){var b=new CKEDITOR.template("background:#{to};background-image:-webkit-gradient(linear,lefttop,leftbottom,from({from}),to({to}));background-image:-moz-linear-gradient(top,{from},{to});background-image:-webkit-linear-gradient(top,{from},{to});background-image:-o-linear-gradient(top,{from},{to});background-image:-ms-linear-gradient(top,{from},{to});background-image:linear-gradient(top,{from},{to});filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='{from}',endColorstr='{to}');");return function(c,
-a){return b.output({from:c,to:a})}}(),f={editor:new CKEDITOR.template("{id}.cke_chrome [border-color:{defaultBorder};] {id} .cke_top [ {defaultGradient}border-bottom-color:{defaultBorder};] {id} .cke_bottom [{defaultGradient}border-top-color:{defaultBorder};] {id} .cke_resizer [border-right-color:{ckeResizer}] {id} .cke_dialog_title [{defaultGradient}border-bottom-color:{defaultBorder};] {id} .cke_dialog_footer [{defaultGradient}outline-color:{defaultBorder};border-top-color:{defaultBorder};] {id} .cke_dialog_tab [{lightGradient}border-color:{defaultBorder};] {id} .cke_dialog_tab:hover [{mediumGradient}] {id} .cke_dialog_contents [border-top-color:{defaultBorder};] {id} .cke_dialog_tab_selected, {id} .cke_dialog_tab_selected:hover [background:{dialogTabSelected};border-bottom-color:{dialogTabSelectedBorder};] {id} .cke_dialog_body [background:{dialogBody};border-color:{defaultBorder};] {id} .cke_toolgroup [{lightGradient}border-color:{defaultBorder};] {id} a.cke_button_off:hover, {id} a.cke_button_off:focus, {id} a.cke_button_off:active [{mediumGradient}] {id} .cke_button_on [{ckeButtonOn}] {id} .cke_toolbar_separator [background-color: {ckeToolbarSeparator};] {id} .cke_combo_button [border-color:{defaultBorder};{lightGradient}] {id} a.cke_combo_button:hover, {id} a.cke_combo_button:focus, {id} .cke_combo_on a.cke_combo_button [border-color:{defaultBorder};{mediumGradient}] {id} .cke_path_item [color:{elementsPathColor};] {id} a.cke_path_item:hover, {id} a.cke_path_item:focus, {id} a.cke_path_item:active [background-color:{elementsPathBg};] {id}.cke_panel [border-color:{defaultBorder};] "),
-panel:new CKEDITOR.template(".cke_panel_grouptitle [{lightGradient}border-color:{defaultBorder};] .cke_menubutton_icon [background-color:{menubuttonIcon};] .cke_menubutton:hover .cke_menubutton_icon, .cke_menubutton:focus .cke_menubutton_icon, .cke_menubutton:active .cke_menubutton_icon [background-color:{menubuttonIconHover};] .cke_menuseparator [background-color:{menubuttonIcon};] a:hover.cke_colorbox, a:focus.cke_colorbox, a:active.cke_colorbox [border-color:{defaultBorder};] a:hover.cke_colorauto, a:hover.cke_colormore, a:focus.cke_colorauto, a:focus.cke_colormore, a:active.cke_colorauto, a:active.cke_colormore [background-color:{ckeColorauto};border-color:{defaultBorder};] ")};
-return function(g,e){var a=g.uiColor,a={id:"."+g.id,defaultBorder:b(a,-0.1),defaultGradient:c(b(a,0.9),a),lightGradient:c(b(a,1),b(a,0.7)),mediumGradient:c(b(a,0.8),b(a,0.5)),ckeButtonOn:c(b(a,0.6),b(a,0.7)),ckeResizer:b(a,-0.4),ckeToolbarSeparator:b(a,0.5),ckeColorauto:b(a,0.8),dialogBody:b(a,0.7),dialogTabSelected:c("#FFFFFF","#FFFFFF"),dialogTabSelectedBorder:"#FFF",elementsPathColor:b(a,-0.6),elementsPathBg:a,menubuttonIcon:b(a,0.5),menubuttonIconHover:b(a,0.3)};return f[e].output(a).replace(/\[/g,
-"{").replace(/\]/g,"}")}}();CKEDITOR.plugins.add("dialogui",{onLoad:function(){var i=function(b){this._||(this._={});this._["default"]=this._.initValue=b["default"]||"";this._.required=b.required||!1;for(var a=[this._],d=1;d<arguments.length;d++)a.push(arguments[d]);a.push(!0);CKEDITOR.tools.extend.apply(CKEDITOR.tools,a);return this._},r={build:function(b,a,d){return new CKEDITOR.ui.dialog.textInput(b,a,d)}},l={build:function(b,a,d){return new CKEDITOR.ui.dialog[a.type](b,a,d)}},n={isChanged:function(){return this.getValue()!=
-this.getInitValue()},reset:function(b){this.setValue(this.getInitValue(),b)},setInitValue:function(){this._.initValue=this.getValue()},resetInitValue:function(){this._.initValue=this._["default"]},getInitValue:function(){return this._.initValue}},o=CKEDITOR.tools.extend({},CKEDITOR.ui.dialog.uiElement.prototype.eventProcessors,{onChange:function(b,a){this._.domOnChangeRegistered||(b.on("load",function(){this.getInputElement().on("change",function(){b.parts.dialog.isVisible()&&this.fire("change",{value:this.getValue()})},
-this)},this),this._.domOnChangeRegistered=!0);this.on("change",a)}},!0),s=/^on([A-Z]\w+)/,p=function(b){for(var a in b)(s.test(a)||"title"==a||"type"==a)&&delete b[a];return b};CKEDITOR.tools.extend(CKEDITOR.ui.dialog,{labeledElement:function(b,a,d,e){if(!(4>arguments.length)){var c=i.call(this,a);c.labelId=CKEDITOR.tools.getNextId()+"_label";this._.children=[];CKEDITOR.ui.dialog.uiElement.call(this,b,a,d,"div",null,{role:"presentation"},function(){var f=[],d=a.required?" cke_required":"";"horizontal"!=
-a.labelLayout?f.push('<label class="cke_dialog_ui_labeled_label'+d+'" ',' id="'+c.labelId+'"',c.inputId?' for="'+c.inputId+'"':"",(a.labelStyle?' style="'+a.labelStyle+'"':"")+">",a.label,"</label>",'<div class="cke_dialog_ui_labeled_content"',a.controlStyle?' style="'+a.controlStyle+'"':"",' role="radiogroup" aria-labelledby="'+c.labelId+'">',e.call(this,b,a),"</div>"):(d={type:"hbox",widths:a.widths,padding:0,children:[{type:"html",html:'<label class="cke_dialog_ui_labeled_label'+d+'" id="'+c.labelId+
-'" for="'+c.inputId+'"'+(a.labelStyle?' style="'+a.labelStyle+'"':"")+">"+CKEDITOR.tools.htmlEncode(a.label)+"</span>"},{type:"html",html:'<span class="cke_dialog_ui_labeled_content"'+(a.controlStyle?' style="'+a.controlStyle+'"':"")+">"+e.call(this,b,a)+"</span>"}]},CKEDITOR.dialog._.uiElementBuilders.hbox.build(b,d,f));return f.join("")})}},textInput:function(b,a,d){if(!(3>arguments.length)){i.call(this,a);var e=this._.inputId=CKEDITOR.tools.getNextId()+"_textInput",c={"class":"cke_dialog_ui_input_"+
-a.type,id:e,type:a.type};a.validate&&(this.validate=a.validate);a.maxLength&&(c.maxlength=a.maxLength);a.size&&(c.size=a.size);a.inputStyle&&(c.style=a.inputStyle);var f=this,h=!1;b.on("load",function(){f.getInputElement().on("keydown",function(a){a.data.getKeystroke()==13&&(h=true)});f.getInputElement().on("keyup",function(a){if(a.data.getKeystroke()==13&&h){b.getButton("ok")&&setTimeout(function(){b.getButton("ok").click()},0);h=false}},null,null,1E3)});CKEDITOR.ui.dialog.labeledElement.call(this,
-b,a,d,function(){var b=['<div class="cke_dialog_ui_input_',a.type,'" role="presentation"'];a.width&&b.push('style="width:'+a.width+'" ');b.push("><input ");c["aria-labelledby"]=this._.labelId;this._.required&&(c["aria-required"]=this._.required);for(var d in c)b.push(d+'="'+c[d]+'" ');b.push(" /></div>");return b.join("")})}},textarea:function(b,a,d){if(!(3>arguments.length)){i.call(this,a);var e=this,c=this._.inputId=CKEDITOR.tools.getNextId()+"_textarea",f={};a.validate&&(this.validate=a.validate);
-f.rows=a.rows||5;f.cols=a.cols||20;f["class"]="cke_dialog_ui_input_textarea "+(a["class"]||"");"undefined"!=typeof a.inputStyle&&(f.style=a.inputStyle);a.dir&&(f.dir=a.dir);CKEDITOR.ui.dialog.labeledElement.call(this,b,a,d,function(){f["aria-labelledby"]=this._.labelId;this._.required&&(f["aria-required"]=this._.required);var a=['<div class="cke_dialog_ui_input_textarea" role="presentation"><textarea id="',c,'" '],b;for(b in f)a.push(b+'="'+CKEDITOR.tools.htmlEncode(f[b])+'" ');a.push(">",CKEDITOR.tools.htmlEncode(e._["default"]),
-"</textarea></div>");return a.join("")})}},checkbox:function(b,a,d){if(!(3>arguments.length)){var e=i.call(this,a,{"default":!!a["default"]});a.validate&&(this.validate=a.validate);CKEDITOR.ui.dialog.uiElement.call(this,b,a,d,"span",null,null,function(){var c=CKEDITOR.tools.extend({},a,{id:a.id?a.id+"_checkbox":CKEDITOR.tools.getNextId()+"_checkbox"},true),d=[],h=CKEDITOR.tools.getNextId()+"_label",g={"class":"cke_dialog_ui_checkbox_input",type:"checkbox","aria-labelledby":h};p(c);if(a["default"])g.checked=
-"checked";if(typeof c.inputStyle!="undefined")c.style=c.inputStyle;e.checkbox=new CKEDITOR.ui.dialog.uiElement(b,c,d,"input",null,g);d.push(' <label id="',h,'" for="',g.id,'"'+(a.labelStyle?' style="'+a.labelStyle+'"':"")+">",CKEDITOR.tools.htmlEncode(a.label),"</label>");return d.join("")})}},radio:function(b,a,d){if(!(3>arguments.length)){i.call(this,a);this._["default"]||(this._["default"]=this._.initValue=a.items[0][1]);a.validate&&(this.validate=a.valdiate);var e=[],c=this;CKEDITOR.ui.dialog.labeledElement.call(this,
-b,a,d,function(){for(var d=[],h=[],g=(a.id?a.id:CKEDITOR.tools.getNextId())+"_radio",k=0;k<a.items.length;k++){var j=a.items[k],i=j[2]!==void 0?j[2]:j[0],l=j[1]!==void 0?j[1]:j[0],m=CKEDITOR.tools.getNextId()+"_radio_input",n=m+"_label",m=CKEDITOR.tools.extend({},a,{id:m,title:null,type:null},true),i=CKEDITOR.tools.extend({},m,{title:i},true),o={type:"radio","class":"cke_dialog_ui_radio_input",name:g,value:l,"aria-labelledby":n},q=[];if(c._["default"]==l)o.checked="checked";p(m);p(i);if(typeof m.inputStyle!=
-"undefined")m.style=m.inputStyle;m.keyboardFocusable=true;e.push(new CKEDITOR.ui.dialog.uiElement(b,m,q,"input",null,o));q.push(" ");new CKEDITOR.ui.dialog.uiElement(b,i,q,"label",null,{id:n,"for":o.id},j[0]);d.push(q.join(""))}new CKEDITOR.ui.dialog.hbox(b,e,d,h);return h.join("")});this._.children=e}},button:function(b,a,d){if(arguments.length){"function"==typeof a&&(a=a(b.getParentEditor()));i.call(this,a,{disabled:a.disabled||!1});CKEDITOR.event.implementOn(this);var e=this;b.on("load",function(){var a=
-this.getElement();(function(){a.on("click",function(a){e.click();a.data.preventDefault()});a.on("keydown",function(a){a.data.getKeystroke()in{32:1}&&(e.click(),a.data.preventDefault())})})();a.unselectable()},this);var c=CKEDITOR.tools.extend({},a);delete c.style;var f=CKEDITOR.tools.getNextId()+"_label";CKEDITOR.ui.dialog.uiElement.call(this,b,c,d,"a",null,{style:a.style,href:"javascript:void(0)",title:a.label,hidefocus:"true","class":a["class"],role:"button","aria-labelledby":f},'<span id="'+f+
-'" class="cke_dialog_ui_button">'+CKEDITOR.tools.htmlEncode(a.label)+"</span>")}},select:function(b,a,d){if(!(3>arguments.length)){var e=i.call(this,a);a.validate&&(this.validate=a.validate);e.inputId=CKEDITOR.tools.getNextId()+"_select";CKEDITOR.ui.dialog.labeledElement.call(this,b,a,d,function(){var c=CKEDITOR.tools.extend({},a,{id:a.id?a.id+"_select":CKEDITOR.tools.getNextId()+"_select"},true),d=[],h=[],g={id:e.inputId,"class":"cke_dialog_ui_input_select","aria-labelledby":this._.labelId};d.push('<div class="cke_dialog_ui_input_',
-a.type,'" role="presentation"');a.width&&d.push('style="width:'+a.width+'" ');d.push(">");if(a.size!=void 0)g.size=a.size;if(a.multiple!=void 0)g.multiple=a.multiple;p(c);for(var k=0,j;k<a.items.length&&(j=a.items[k]);k++)h.push('<option value="',CKEDITOR.tools.htmlEncode(j[1]!==void 0?j[1]:j[0]).replace(/"/g,"&quot;"),'" /> ',CKEDITOR.tools.htmlEncode(j[0]));if(typeof c.inputStyle!="undefined")c.style=c.inputStyle;e.select=new CKEDITOR.ui.dialog.uiElement(b,c,d,"select",null,g,h.join(""));d.push("</div>");
-return d.join("")})}},file:function(b,a,d){if(!(3>arguments.length)){void 0===a["default"]&&(a["default"]="");var e=CKEDITOR.tools.extend(i.call(this,a),{definition:a,buttons:[]});a.validate&&(this.validate=a.validate);b.on("load",function(){CKEDITOR.document.getById(e.frameId).getParent().addClass("cke_dialog_ui_input_file")});CKEDITOR.ui.dialog.labeledElement.call(this,b,a,d,function(){e.frameId=CKEDITOR.tools.getNextId()+"_fileInput";var b=['<iframe frameborder="0" allowtransparency="0" class="cke_dialog_ui_input_file" role="presentation" id="',
-e.frameId,'" title="',a.label,'" src="javascript:void('];b.push(CKEDITOR.env.ie?"(function(){"+encodeURIComponent("document.open();("+CKEDITOR.tools.fixDomain+")();document.close();")+"})()":"0");b.push(')"></iframe>');return b.join("")})}},fileButton:function(b,a,d){if(!(3>arguments.length)){i.call(this,a);var e=this;a.validate&&(this.validate=a.validate);var c=CKEDITOR.tools.extend({},a),f=c.onClick;c.className=(c.className?c.className+" ":"")+"cke_dialog_ui_button";c.onClick=function(c){var d=
-a["for"];if(!f||f.call(this,c)!==false){b.getContentElement(d[0],d[1]).submit();this.disable()}};b.on("load",function(){b.getContentElement(a["for"][0],a["for"][1])._.buttons.push(e)});CKEDITOR.ui.dialog.button.call(this,b,c,d)}},html:function(){var b=/^\s*<[\w:]+\s+([^>]*)?>/,a=/^(\s*<[\w:]+(?:\s+[^>]*)?)((?:.|\r|\n)+)$/,d=/\/$/;return function(e,c,f){if(!(3>arguments.length)){var h=[],g=c.html;"<"!=g.charAt(0)&&(g="<span>"+g+"</span>");var k=c.focus;if(k){var j=this.focus;this.focus=function(){("function"==
-typeof k?k:j).call(this);this.fire("focus")};c.isFocusable&&(this.isFocusable=this.isFocusable);this.keyboardFocusable=!0}CKEDITOR.ui.dialog.uiElement.call(this,e,c,h,"span",null,null,"");h=h.join("").match(b);g=g.match(a)||["","",""];d.test(g[1])&&(g[1]=g[1].slice(0,-1),g[2]="/"+g[2]);f.push([g[1]," ",h[1]||"",g[2]].join(""))}}}(),fieldset:function(b,a,d,e,c){var f=c.label;this._={children:a};CKEDITOR.ui.dialog.uiElement.call(this,b,c,e,"fieldset",null,null,function(){var a=[];f&&a.push("<legend"+
-(c.labelStyle?' style="'+c.labelStyle+'"':"")+">"+f+"</legend>");for(var b=0;b<d.length;b++)a.push(d[b]);return a.join("")})}},!0);CKEDITOR.ui.dialog.html.prototype=new CKEDITOR.ui.dialog.uiElement;CKEDITOR.ui.dialog.labeledElement.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.uiElement,{setLabel:function(b){var a=CKEDITOR.document.getById(this._.labelId);1>a.getChildCount()?(new CKEDITOR.dom.text(b,CKEDITOR.document)).appendTo(a):a.getChild(0).$.nodeValue=b;return this},getLabel:function(){var b=
-CKEDITOR.document.getById(this._.labelId);return!b||1>b.getChildCount()?"":b.getChild(0).getText()},eventProcessors:o},!0);CKEDITOR.ui.dialog.button.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.uiElement,{click:function(){return!this._.disabled?this.fire("click",{dialog:this._.dialog}):!1},enable:function(){this._.disabled=!1;var b=this.getElement();b&&b.removeClass("cke_disabled")},disable:function(){this._.disabled=!0;this.getElement().addClass("cke_disabled")},isVisible:function(){return this.getElement().getFirst().isVisible()},
-isEnabled:function(){return!this._.disabled},eventProcessors:CKEDITOR.tools.extend({},CKEDITOR.ui.dialog.uiElement.prototype.eventProcessors,{onClick:function(b,a){this.on("click",function(){a.apply(this,arguments)})}},!0),accessKeyUp:function(){this.click()},accessKeyDown:function(){this.focus()},keyboardFocusable:!0},!0);CKEDITOR.ui.dialog.textInput.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.labeledElement,{getInputElement:function(){return CKEDITOR.document.getById(this._.inputId)},
-focus:function(){var b=this.selectParentTab();setTimeout(function(){var a=b.getInputElement();a&&a.$.focus()},0)},select:function(){var b=this.selectParentTab();setTimeout(function(){var a=b.getInputElement();a&&(a.$.focus(),a.$.select())},0)},accessKeyUp:function(){this.select()},setValue:function(b){!b&&(b="");return CKEDITOR.ui.dialog.uiElement.prototype.setValue.apply(this,arguments)},keyboardFocusable:!0},n,!0);CKEDITOR.ui.dialog.textarea.prototype=new CKEDITOR.ui.dialog.textInput;CKEDITOR.ui.dialog.select.prototype=
-CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.labeledElement,{getInputElement:function(){return this._.select.getElement()},add:function(b,a,d){var e=new CKEDITOR.dom.element("option",this.getDialog().getParentEditor().document),c=this.getInputElement().$;e.$.text=b;e.$.value=void 0===a||null===a?b:a;void 0===d||null===d?CKEDITOR.env.ie?c.add(e.$):c.add(e.$,null):c.add(e.$,d);return this},remove:function(b){this.getInputElement().$.remove(b);return this},clear:function(){for(var b=this.getInputElement().$;0<
-b.length;)b.remove(0);return this},keyboardFocusable:!0},n,!0);CKEDITOR.ui.dialog.checkbox.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.uiElement,{getInputElement:function(){return this._.checkbox.getElement()},setValue:function(b,a){this.getInputElement().$.checked=b;!a&&this.fire("change",{value:b})},getValue:function(){return this.getInputElement().$.checked},accessKeyUp:function(){this.setValue(!this.getValue())},eventProcessors:{onChange:function(b,a){if(!CKEDITOR.env.ie||8<CKEDITOR.env.version)return o.onChange.apply(this,
-arguments);b.on("load",function(){var a=this._.checkbox.getElement();a.on("propertychange",function(b){b=b.data.$;"checked"==b.propertyName&&this.fire("change",{value:a.$.checked})},this)},this);this.on("change",a);return null}},keyboardFocusable:!0},n,!0);CKEDITOR.ui.dialog.radio.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.uiElement,{setValue:function(b,a){for(var d=this._.children,e,c=0;c<d.length&&(e=d[c]);c++)e.getElement().$.checked=e.getValue()==b;!a&&this.fire("change",{value:b})},
-getValue:function(){for(var b=this._.children,a=0;a<b.length;a++)if(b[a].getElement().$.checked)return b[a].getValue();return null},accessKeyUp:function(){var b=this._.children,a;for(a=0;a<b.length;a++)if(b[a].getElement().$.checked){b[a].getElement().focus();return}b[0].getElement().focus()},eventProcessors:{onChange:function(b,a){if(CKEDITOR.env.ie)b.on("load",function(){for(var a=this._.children,b=this,c=0;c<a.length;c++)a[c].getElement().on("propertychange",function(a){a=a.data.$;"checked"==a.propertyName&&
-this.$.checked&&b.fire("change",{value:this.getAttribute("value")})})},this),this.on("change",a);else return o.onChange.apply(this,arguments);return null}}},n,!0);CKEDITOR.ui.dialog.file.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.labeledElement,n,{getInputElement:function(){var b=CKEDITOR.document.getById(this._.frameId).getFrameDocument();return 0<b.$.forms.length?new CKEDITOR.dom.element(b.$.forms[0].elements[0]):this.getElement()},submit:function(){this.getInputElement().getParent().$.submit();
-return this},getAction:function(){return this.getInputElement().getParent().$.action},registerEvents:function(b){var a=/^on([A-Z]\w+)/,d,e=function(a,b,c,d){a.on("formLoaded",function(){a.getInputElement().on(c,d,a)})},c;for(c in b)if(d=c.match(a))this.eventProcessors[c]?this.eventProcessors[c].call(this,this._.dialog,b[c]):e(this,this._.dialog,d[1].toLowerCase(),b[c]);return this},reset:function(){function b(){d.$.open();var b="";e.size&&(b=e.size-(CKEDITOR.env.ie?7:0));var i=a.frameId+"_input";
-d.$.write(['<html dir="'+g+'" lang="'+k+'"><head><title></title></head><body style="margin: 0; overflow: hidden; background: transparent;">','<form enctype="multipart/form-data" method="POST" dir="'+g+'" lang="'+k+'" action="',CKEDITOR.tools.htmlEncode(e.action),'"><label id="',a.labelId,'" for="',i,'" style="display:none">',CKEDITOR.tools.htmlEncode(e.label),'</label><input id="',i,'" aria-labelledby="',a.labelId,'" type="file" name="',CKEDITOR.tools.htmlEncode(e.id||"cke_upload"),'" size="',CKEDITOR.tools.htmlEncode(0<
-b?b:""),'" /></form></body></html><script>',CKEDITOR.env.ie?"("+CKEDITOR.tools.fixDomain+")();":"","window.parent.CKEDITOR.tools.callFunction("+f+");","window.onbeforeunload = function() {window.parent.CKEDITOR.tools.callFunction("+h+")}","<\/script>"].join(""));d.$.close();for(b=0;b<c.length;b++)c[b].enable()}var a=this._,d=CKEDITOR.document.getById(a.frameId).getFrameDocument(),e=a.definition,c=a.buttons,f=this.formLoadedNumber,h=this.formUnloadNumber,g=a.dialog._.editor.lang.dir,k=a.dialog._.editor.langCode;
-f||(f=this.formLoadedNumber=CKEDITOR.tools.addFunction(function(){this.fire("formLoaded")},this),h=this.formUnloadNumber=CKEDITOR.tools.addFunction(function(){this.getInputElement().clearCustomData()},this),this.getDialog()._.editor.on("destroy",function(){CKEDITOR.tools.removeFunction(f);CKEDITOR.tools.removeFunction(h)}));CKEDITOR.env.gecko?setTimeout(b,500):b()},getValue:function(){return this.getInputElement().$.value||""},setInitValue:function(){this._.initValue=""},eventProcessors:{onChange:function(b,
-a){this._.domOnChangeRegistered||(this.on("formLoaded",function(){this.getInputElement().on("change",function(){this.fire("change",{value:this.getValue()})},this)},this),this._.domOnChangeRegistered=!0);this.on("change",a)}},keyboardFocusable:!0},!0);CKEDITOR.ui.dialog.fileButton.prototype=new CKEDITOR.ui.dialog.button;CKEDITOR.ui.dialog.fieldset.prototype=CKEDITOR.tools.clone(CKEDITOR.ui.dialog.hbox.prototype);CKEDITOR.dialog.addUIElement("text",r);CKEDITOR.dialog.addUIElement("password",r);CKEDITOR.dialog.addUIElement("textarea",
-l);CKEDITOR.dialog.addUIElement("checkbox",l);CKEDITOR.dialog.addUIElement("radio",l);CKEDITOR.dialog.addUIElement("button",l);CKEDITOR.dialog.addUIElement("select",l);CKEDITOR.dialog.addUIElement("file",l);CKEDITOR.dialog.addUIElement("fileButton",l);CKEDITOR.dialog.addUIElement("html",l);CKEDITOR.dialog.addUIElement("fieldset",{build:function(b,a,d){for(var e=a.children,c,f=[],h=[],g=0;g<e.length&&(c=e[g]);g++){var i=[];f.push(i);h.push(CKEDITOR.dialog._.uiElementBuilders[c.type].build(b,c,i))}return new CKEDITOR.ui.dialog[a.type](b,
-h,f,d,a)}})}});CKEDITOR.DIALOG_RESIZE_NONE=0;CKEDITOR.DIALOG_RESIZE_WIDTH=1;CKEDITOR.DIALOG_RESIZE_HEIGHT=2;CKEDITOR.DIALOG_RESIZE_BOTH=3;
-(function(){function t(){for(var a=this._.tabIdList.length,b=CKEDITOR.tools.indexOf(this._.tabIdList,this._.currentTabId)+a,c=b-1;c>b-a;c--)if(this._.tabs[this._.tabIdList[c%a]][0].$.offsetHeight)return this._.tabIdList[c%a];return null}function u(){for(var a=this._.tabIdList.length,b=CKEDITOR.tools.indexOf(this._.tabIdList,this._.currentTabId),c=b+1;c<b+a;c++)if(this._.tabs[this._.tabIdList[c%a]][0].$.offsetHeight)return this._.tabIdList[c%a];return null}function G(a,b){for(var c=a.$.getElementsByTagName("input"),
-e=0,d=c.length;e<d;e++){var g=new CKEDITOR.dom.element(c[e]);"text"==g.getAttribute("type").toLowerCase()&&(b?(g.setAttribute("value",g.getCustomData("fake_value")||""),g.removeCustomData("fake_value")):(g.setCustomData("fake_value",g.getAttribute("value")),g.setAttribute("value","")))}}function P(a,b){var c=this.getInputElement();c&&(a?c.removeAttribute("aria-invalid"):c.setAttribute("aria-invalid",!0));a||(this.select?this.select():this.focus());b&&alert(b);this.fire("validated",{valid:a,msg:b})}
-function Q(){var a=this.getInputElement();a&&a.removeAttribute("aria-invalid")}function R(a){var a=CKEDITOR.dom.element.createFromHtml(CKEDITOR.addTemplate("dialog",S).output({id:CKEDITOR.tools.getNextNumber(),editorId:a.id,langDir:a.lang.dir,langCode:a.langCode,editorDialogClass:"cke_editor_"+a.name.replace(/\./g,"\\.")+"_dialog",closeTitle:a.lang.common.close,hidpi:CKEDITOR.env.hidpi?"cke_hidpi":""})),b=a.getChild([0,0,0,0,0]),c=b.getChild(0),e=b.getChild(1);if(CKEDITOR.env.ie&&!CKEDITOR.env.ie6Compat){var d=
-"javascript:void(function(){"+encodeURIComponent("document.open();("+CKEDITOR.tools.fixDomain+")();document.close();")+"}())";CKEDITOR.dom.element.createFromHtml('<iframe frameBorder="0" class="cke_iframe_shim" src="'+d+'" tabIndex="-1"></iframe>').appendTo(b.getParent())}c.unselectable();e.unselectable();return{element:a,parts:{dialog:a.getChild(0),title:c,close:e,tabs:b.getChild(2),contents:b.getChild([3,0,0,0]),footer:b.getChild([3,0,1,0])}}}function H(a,b,c){this.element=b;this.focusIndex=c;this.tabIndex=
-0;this.isFocusable=function(){return!b.getAttribute("disabled")&&b.isVisible()};this.focus=function(){a._.currentFocusIndex=this.focusIndex;this.element.focus()};b.on("keydown",function(a){a.data.getKeystroke()in{32:1,13:1}&&this.fire("click")});b.on("focus",function(){this.fire("mouseover")});b.on("blur",function(){this.fire("mouseout")})}function T(a){function b(){a.layout()}var c=CKEDITOR.document.getWindow();c.on("resize",b);a.on("hide",function(){c.removeListener("resize",b)})}function I(a,b){this._=
-{dialog:a};CKEDITOR.tools.extend(this,b)}function U(a){function b(b){var c=a.getSize(),h=CKEDITOR.document.getWindow().getViewPaneSize(),o=b.data.$.screenX,j=b.data.$.screenY,n=o-e.x,m=j-e.y;e={x:o,y:j};d.x+=n;d.y+=m;a.move(d.x+i[3]<f?-i[3]:d.x-i[1]>h.width-c.width-f?h.width-c.width+("rtl"==g.lang.dir?0:i[1]):d.x,d.y+i[0]<f?-i[0]:d.y-i[2]>h.height-c.height-f?h.height-c.height+i[2]:d.y,1);b.data.preventDefault()}function c(){CKEDITOR.document.removeListener("mousemove",b);CKEDITOR.document.removeListener("mouseup",
-c);if(CKEDITOR.env.ie6Compat){var a=q.getChild(0).getFrameDocument();a.removeListener("mousemove",b);a.removeListener("mouseup",c)}}var e=null,d=null;a.getElement().getFirst();var g=a.getParentEditor(),f=g.config.dialog_magnetDistance,i=CKEDITOR.skin.margins||[0,0,0,0];"undefined"==typeof f&&(f=20);a.parts.title.on("mousedown",function(f){e={x:f.data.$.screenX,y:f.data.$.screenY};CKEDITOR.document.on("mousemove",b);CKEDITOR.document.on("mouseup",c);d=a.getPosition();if(CKEDITOR.env.ie6Compat){var k=
-q.getChild(0).getFrameDocument();k.on("mousemove",b);k.on("mouseup",c)}f.data.preventDefault()},a)}function V(a){var b,c;function e(d){var e="rtl"==i.lang.dir,j=o.width,C=o.height,D=j+(d.data.$.screenX-b)*(e?-1:1)*(a._.moved?1:2),n=C+(d.data.$.screenY-c)*(a._.moved?1:2),x=a._.element.getFirst(),x=e&&x.getComputedStyle("right"),y=a.getPosition();y.y+n>h.height&&(n=h.height-y.y);if((e?x:y.x)+D>h.width)D=h.width-(e?x:y.x);if(f==CKEDITOR.DIALOG_RESIZE_WIDTH||f==CKEDITOR.DIALOG_RESIZE_BOTH)j=Math.max(g.minWidth||
-0,D-l);if(f==CKEDITOR.DIALOG_RESIZE_HEIGHT||f==CKEDITOR.DIALOG_RESIZE_BOTH)C=Math.max(g.minHeight||0,n-k);a.resize(j,C);a._.moved||a.layout();d.data.preventDefault()}function d(){CKEDITOR.document.removeListener("mouseup",d);CKEDITOR.document.removeListener("mousemove",e);j&&(j.remove(),j=null);if(CKEDITOR.env.ie6Compat){var a=q.getChild(0).getFrameDocument();a.removeListener("mouseup",d);a.removeListener("mousemove",e)}}var g=a.definition,f=g.resizable;if(f!=CKEDITOR.DIALOG_RESIZE_NONE){var i=a.getParentEditor(),
-l,k,h,o,j,n=CKEDITOR.tools.addFunction(function(f){o=a.getSize();var g=a.parts.contents;g.$.getElementsByTagName("iframe").length&&(j=CKEDITOR.dom.element.createFromHtml('<div class="cke_dialog_resize_cover" style="height: 100%; position: absolute; width: 100%;"></div>'),g.append(j));k=o.height-a.parts.contents.getSize("height",!(CKEDITOR.env.gecko||CKEDITOR.env.opera||CKEDITOR.env.ie&&CKEDITOR.env.quirks));l=o.width-a.parts.contents.getSize("width",1);b=f.screenX;c=f.screenY;h=CKEDITOR.document.getWindow().getViewPaneSize();
-CKEDITOR.document.on("mousemove",e);CKEDITOR.document.on("mouseup",d);CKEDITOR.env.ie6Compat&&(g=q.getChild(0).getFrameDocument(),g.on("mousemove",e),g.on("mouseup",d));f.preventDefault&&f.preventDefault()});a.on("load",function(){var b="";f==CKEDITOR.DIALOG_RESIZE_WIDTH?b=" cke_resizer_horizontal":f==CKEDITOR.DIALOG_RESIZE_HEIGHT&&(b=" cke_resizer_vertical");b=CKEDITOR.dom.element.createFromHtml('<div class="cke_resizer'+b+" cke_resizer_"+i.lang.dir+'" title="'+CKEDITOR.tools.htmlEncode(i.lang.common.resize)+
-'" onmousedown="CKEDITOR.tools.callFunction('+n+', event )">'+("ltr"==i.lang.dir?"◢":"◣")+"</div>");a.parts.footer.append(b,1)});i.on("destroy",function(){CKEDITOR.tools.removeFunction(n)})}}function E(a){a.data.preventDefault(1)}function J(a){var b=CKEDITOR.document.getWindow(),c=a.config,e=c.dialog_backgroundCoverColor||"white",d=c.dialog_backgroundCoverOpacity,g=c.baseFloatZIndex,c=CKEDITOR.tools.genKey(e,d,g),f=w[c];f?f.show():(g=['<div tabIndex="-1" style="position: ',CKEDITOR.env.ie6Compat?
-"absolute":"fixed","; z-index: ",g,"; top: 0px; left: 0px; ",!CKEDITOR.env.ie6Compat?"background-color: "+e:"",'" class="cke_dialog_background_cover">'],CKEDITOR.env.ie6Compat&&(e="<html><body style=\\'background-color:"+e+";\\'></body></html>",g.push('<iframe hidefocus="true" frameborder="0" id="cke_dialog_background_iframe" src="javascript:'),g.push("void((function(){"+encodeURIComponent("document.open();("+CKEDITOR.tools.fixDomain+")();document.write( '"+e+"' );document.close();")+"})())"),g.push('" style="position:absolute;left:0;top:0;width:100%;height: 100%;filter: progid:DXImageTransform.Microsoft.Alpha(opacity=0)"></iframe>')),
-g.push("</div>"),f=CKEDITOR.dom.element.createFromHtml(g.join("")),f.setOpacity(void 0!=d?d:0.5),f.on("keydown",E),f.on("keypress",E),f.on("keyup",E),f.appendTo(CKEDITOR.document.getBody()),w[c]=f);a.focusManager.add(f);q=f;var a=function(){var a=b.getViewPaneSize();f.setStyles({width:a.width+"px",height:a.height+"px"})},i=function(){var a=b.getScrollPosition(),c=CKEDITOR.dialog._.currentTop;f.setStyles({left:a.x+"px",top:a.y+"px"});if(c){do{a=c.getPosition();c.move(a.x,a.y)}while(c=c._.parentDialog)
-}};F=a;b.on("resize",a);a();(!CKEDITOR.env.mac||!CKEDITOR.env.webkit)&&f.focus();if(CKEDITOR.env.ie6Compat){var l=function(){i();arguments.callee.prevScrollHandler.apply(this,arguments)};b.$.setTimeout(function(){l.prevScrollHandler=window.onscroll||function(){};window.onscroll=l},0);i()}}function K(a){q&&(a.focusManager.remove(q),a=CKEDITOR.document.getWindow(),q.hide(),a.removeListener("resize",F),CKEDITOR.env.ie6Compat&&a.$.setTimeout(function(){window.onscroll=window.onscroll&&window.onscroll.prevScrollHandler||
-null},0),F=null)}var r=CKEDITOR.tools.cssLength,S='<div class="cke_reset_all {editorId} {editorDialogClass} {hidpi}" dir="{langDir}" lang="{langCode}" role="dialog" aria-labelledby="cke_dialog_title_{id}"><table class="cke_dialog '+CKEDITOR.env.cssClass+' cke_{langDir}" style="position:absolute" role="presentation"><tr><td role="presentation"><div class="cke_dialog_body" role="presentation"><div id="cke_dialog_title_{id}" class="cke_dialog_title" role="presentation"></div><a id="cke_dialog_close_button_{id}" class="cke_dialog_close_button" href="javascript:void(0)" title="{closeTitle}" role="button"><span class="cke_label">X</span></a><div id="cke_dialog_tabs_{id}" class="cke_dialog_tabs" role="tablist"></div><table class="cke_dialog_contents" role="presentation"><tr><td id="cke_dialog_contents_{id}" class="cke_dialog_contents_body" role="presentation"></td></tr><tr><td id="cke_dialog_footer_{id}" class="cke_dialog_footer" role="presentation"></td></tr></table></div></td></tr></table></div>';
-CKEDITOR.dialog=function(a,b){function c(){var a=m._.focusList;a.sort(function(a,b){return a.tabIndex!=b.tabIndex?b.tabIndex-a.tabIndex:a.focusIndex-b.focusIndex});for(var b=a.length,c=0;c<b;c++)a[c].focusIndex=c}function e(a){var b=m._.focusList,a=a||0;if(!(1>b.length)){var c=m._.currentFocusIndex;try{b[c].getInputElement().$.blur()}catch(f){}for(var d=c=(c+a+b.length)%b.length;a&&!b[d].isFocusable()&&!(d=(d+a+b.length)%b.length,d==c););b[d].focus();"text"==b[d].type&&b[d].select()}}function d(b){if(m==
-CKEDITOR.dialog._.currentTop){var c=b.data.getKeystroke(),d="rtl"==a.lang.dir;o=j=0;if(9==c||c==CKEDITOR.SHIFT+9)c=c==CKEDITOR.SHIFT+9,m._.tabBarMode?(c=c?t.call(m):u.call(m),m.selectPage(c),m._.tabs[c][0].focus()):e(c?-1:1),o=1;else if(c==CKEDITOR.ALT+121&&!m._.tabBarMode&&1<m.getPageCount())m._.tabBarMode=!0,m._.tabs[m._.currentTabId][0].focus(),o=1;else if((37==c||39==c)&&m._.tabBarMode)c=c==(d?39:37)?t.call(m):u.call(m),m.selectPage(c),m._.tabs[c][0].focus(),o=1;else if((13==c||32==c)&&m._.tabBarMode)this.selectPage(this._.currentTabId),
-this._.tabBarMode=!1,this._.currentFocusIndex=-1,e(1),o=1;else if(13==c){c=b.data.getTarget();if(!c.is("a","button","select","textarea")&&(!c.is("input")||"button"!=c.$.type))(c=this.getButton("ok"))&&CKEDITOR.tools.setTimeout(c.click,0,c),o=1;j=1}else if(27==c)(c=this.getButton("cancel"))?CKEDITOR.tools.setTimeout(c.click,0,c):!1!==this.fire("cancel",{hide:!0}).hide&&this.hide(),j=1;else return;g(b)}}function g(a){o?a.data.preventDefault(1):j&&a.data.stopPropagation()}var f=CKEDITOR.dialog._.dialogDefinitions[b],
-i=CKEDITOR.tools.clone(W),l=a.config.dialog_buttonsOrder||"OS",k=a.lang.dir,h={},o,j;("OS"==l&&CKEDITOR.env.mac||"rtl"==l&&"ltr"==k||"ltr"==l&&"rtl"==k)&&i.buttons.reverse();f=CKEDITOR.tools.extend(f(a),i);f=CKEDITOR.tools.clone(f);f=new L(this,f);i=R(a);this._={editor:a,element:i.element,name:b,contentSize:{width:0,height:0},size:{width:0,height:0},contents:{},buttons:{},accessKeyMap:{},tabs:{},tabIdList:[],currentTabId:null,currentTabIndex:null,pageCount:0,lastTab:null,tabBarMode:!1,focusList:[],
-currentFocusIndex:0,hasFocus:!1};this.parts=i.parts;CKEDITOR.tools.setTimeout(function(){a.fire("ariaWidget",this.parts.contents)},0,this);i={position:CKEDITOR.env.ie6Compat?"absolute":"fixed",top:0,visibility:"hidden"};i["rtl"==k?"right":"left"]=0;this.parts.dialog.setStyles(i);CKEDITOR.event.call(this);this.definition=f=CKEDITOR.fire("dialogDefinition",{name:b,definition:f},a).definition;if(!("removeDialogTabs"in a._)&&a.config.removeDialogTabs){i=a.config.removeDialogTabs.split(";");for(k=0;k<
-i.length;k++)if(l=i[k].split(":"),2==l.length){var n=l[0];h[n]||(h[n]=[]);h[n].push(l[1])}a._.removeDialogTabs=h}if(a._.removeDialogTabs&&(h=a._.removeDialogTabs[b]))for(k=0;k<h.length;k++)f.removeContents(h[k]);if(f.onLoad)this.on("load",f.onLoad);if(f.onShow)this.on("show",f.onShow);if(f.onHide)this.on("hide",f.onHide);if(f.onOk)this.on("ok",function(b){a.fire("saveSnapshot");setTimeout(function(){a.fire("saveSnapshot")},0);!1===f.onOk.call(this,b)&&(b.data.hide=!1)});if(f.onCancel)this.on("cancel",
-function(a){!1===f.onCancel.call(this,a)&&(a.data.hide=!1)});var m=this,p=function(a){var b=m._.contents,c=!1,d;for(d in b)for(var f in b[d])if(c=a.call(this,b[d][f]))return};this.on("ok",function(a){p(function(b){if(b.validate){var c=b.validate(this),d="string"==typeof c||!1===c;d&&(a.data.hide=!1,a.stop());P.call(b,!d,"string"==typeof c?c:void 0);return d}})},this,null,0);this.on("cancel",function(b){p(function(c){if(c.isChanged())return!a.config.dialog_noConfirmCancel&&!confirm(a.lang.common.confirmCancel)&&
-(b.data.hide=!1),!0})},this,null,0);this.parts.close.on("click",function(a){!1!==this.fire("cancel",{hide:!0}).hide&&this.hide();a.data.preventDefault()},this);this.changeFocus=e;var v=this._.element;a.focusManager.add(v,1);this.on("show",function(){v.on("keydown",d,this);if(CKEDITOR.env.opera||CKEDITOR.env.gecko)v.on("keypress",g,this)});this.on("hide",function(){v.removeListener("keydown",d);(CKEDITOR.env.opera||CKEDITOR.env.gecko)&&v.removeListener("keypress",g);p(function(a){Q.apply(a)})});this.on("iframeAdded",
-function(a){(new CKEDITOR.dom.document(a.data.iframe.$.contentWindow.document)).on("keydown",d,this,null,0)});this.on("show",function(){c();if(a.config.dialog_startupFocusTab&&1<m._.pageCount)m._.tabBarMode=!0,m._.tabs[m._.currentTabId][0].focus();else if(!this._.hasFocus)if(this._.currentFocusIndex=-1,f.onFocus){var b=f.onFocus.call(this);b&&b.focus()}else e(1)},this,null,4294967295);if(CKEDITOR.env.ie6Compat)this.on("load",function(){var a=this.getElement(),b=a.getFirst();b.remove();b.appendTo(a)},
-this);U(this);V(this);(new CKEDITOR.dom.text(f.title,CKEDITOR.document)).appendTo(this.parts.title);for(k=0;k<f.contents.length;k++)(h=f.contents[k])&&this.addPage(h);this.parts.tabs.on("click",function(a){var b=a.data.getTarget();b.hasClass("cke_dialog_tab")&&(b=b.$.id,this.selectPage(b.substring(4,b.lastIndexOf("_"))),this._.tabBarMode&&(this._.tabBarMode=!1,this._.currentFocusIndex=-1,e(1)),a.data.preventDefault())},this);k=[];h=CKEDITOR.dialog._.uiElementBuilders.hbox.build(this,{type:"hbox",
-className:"cke_dialog_footer_buttons",widths:[],children:f.buttons},k).getChild();this.parts.footer.setHtml(k.join(""));for(k=0;k<h.length;k++)this._.buttons[h[k].id]=h[k]};CKEDITOR.dialog.prototype={destroy:function(){this.hide();this._.element.remove()},resize:function(){return function(a,b){if(!this._.contentSize||!(this._.contentSize.width==a&&this._.contentSize.height==b))CKEDITOR.dialog.fire("resize",{dialog:this,width:a,height:b},this._.editor),this.fire("resize",{width:a,height:b},this._.editor),
-this.parts.contents.setStyles({width:a+"px",height:b+"px"}),"rtl"==this._.editor.lang.dir&&this._.position&&(this._.position.x=CKEDITOR.document.getWindow().getViewPaneSize().width-this._.contentSize.width-parseInt(this._.element.getFirst().getStyle("right"),10)),this._.contentSize={width:a,height:b}}}(),getSize:function(){var a=this._.element.getFirst();return{width:a.$.offsetWidth||0,height:a.$.offsetHeight||0}},move:function(a,b,c){var e=this._.element.getFirst(),d="rtl"==this._.editor.lang.dir,
-g="fixed"==e.getComputedStyle("position");CKEDITOR.env.ie&&e.setStyle("zoom","100%");if(!g||!this._.position||!(this._.position.x==a&&this._.position.y==b))this._.position={x:a,y:b},g||(g=CKEDITOR.document.getWindow().getScrollPosition(),a+=g.x,b+=g.y),d&&(g=this.getSize(),a=CKEDITOR.document.getWindow().getViewPaneSize().width-g.width-a),b={top:(0<b?b:0)+"px"},b[d?"right":"left"]=(0<a?a:0)+"px",e.setStyles(b),c&&(this._.moved=1)},getPosition:function(){return CKEDITOR.tools.extend({},this._.position)},
-show:function(){var a=this._.element,b=this.definition;!a.getParent()||!a.getParent().equals(CKEDITOR.document.getBody())?a.appendTo(CKEDITOR.document.getBody()):a.setStyle("display","block");if(CKEDITOR.env.gecko&&10900>CKEDITOR.env.version){var c=this.parts.dialog;c.setStyle("position","absolute");setTimeout(function(){c.setStyle("position","fixed")},0)}this.resize(this._.contentSize&&this._.contentSize.width||b.width||b.minWidth,this._.contentSize&&this._.contentSize.height||b.height||b.minHeight);
-this.reset();this.selectPage(this.definition.contents[0].id);null===CKEDITOR.dialog._.currentZIndex&&(CKEDITOR.dialog._.currentZIndex=this._.editor.config.baseFloatZIndex);this._.element.getFirst().setStyle("z-index",CKEDITOR.dialog._.currentZIndex+=10);null===CKEDITOR.dialog._.currentTop?(CKEDITOR.dialog._.currentTop=this,this._.parentDialog=null,J(this._.editor)):(this._.parentDialog=CKEDITOR.dialog._.currentTop,this._.parentDialog.getElement().getFirst().$.style.zIndex-=Math.floor(this._.editor.config.baseFloatZIndex/
-2),CKEDITOR.dialog._.currentTop=this);a.on("keydown",M);a.on(CKEDITOR.env.opera?"keypress":"keyup",N);this._.hasFocus=!1;for(var e in b.contents)if(b.contents[e]){var a=b.contents[e],d=this._.tabs[a.id],g=a.requiredContent,f=0;if(d){for(var i in this._.contents[a.id]){var l=this._.contents[a.id][i];"hbox"==l.type||("vbox"==l.type||!l.getInputElement())||(l.requiredContent&&!this._.editor.activeFilter.check(l.requiredContent)?l.disable():(l.enable(),f++))}!f||g&&!this._.editor.activeFilter.check(g)?
-d[0].addClass("cke_dialog_tab_disabled"):d[0].removeClass("cke_dialog_tab_disabled")}}CKEDITOR.tools.setTimeout(function(){this.layout();T(this);this.parts.dialog.setStyle("visibility","");this.fireOnce("load",{});CKEDITOR.ui.fire("ready",this);this.fire("show",{});this._.editor.fire("dialogShow",this);this._.parentDialog||this._.editor.focusManager.lock();this.foreach(function(a){a.setInitValue&&a.setInitValue()})},100,this)},layout:function(){var a=this.parts.dialog,b=this.getSize(),c=CKEDITOR.document.getWindow().getViewPaneSize(),
-e=(c.width-b.width)/2,d=(c.height-b.height)/2;CKEDITOR.env.ie6Compat||(b.height+(0<d?d:0)>c.height||b.width+(0<e?e:0)>c.width?a.setStyle("position","absolute"):a.setStyle("position","fixed"));this.move(this._.moved?this._.position.x:e,this._.moved?this._.position.y:d)},foreach:function(a){for(var b in this._.contents)for(var c in this._.contents[b])a.call(this,this._.contents[b][c]);return this},reset:function(){var a=function(a){a.reset&&a.reset(1)};return function(){this.foreach(a);return this}}(),
-setupContent:function(){var a=arguments;this.foreach(function(b){b.setup&&b.setup.apply(b,a)})},commitContent:function(){var a=arguments;this.foreach(function(b){CKEDITOR.env.ie&&this._.currentFocusIndex==b.focusIndex&&b.getInputElement().$.blur();b.commit&&b.commit.apply(b,a)})},hide:function(){if(this.parts.dialog.isVisible()){this.fire("hide",{});this._.editor.fire("dialogHide",this);this.selectPage(this._.tabIdList[0]);var a=this._.element;a.setStyle("display","none");this.parts.dialog.setStyle("visibility",
-"hidden");for(X(this);CKEDITOR.dialog._.currentTop!=this;)CKEDITOR.dialog._.currentTop.hide();if(this._.parentDialog){var b=this._.parentDialog.getElement().getFirst();b.setStyle("z-index",parseInt(b.$.style.zIndex,10)+Math.floor(this._.editor.config.baseFloatZIndex/2))}else K(this._.editor);if(CKEDITOR.dialog._.currentTop=this._.parentDialog)CKEDITOR.dialog._.currentZIndex-=10;else{CKEDITOR.dialog._.currentZIndex=null;a.removeListener("keydown",M);a.removeListener(CKEDITOR.env.opera?"keypress":"keyup",
-N);var c=this._.editor;c.focus();setTimeout(function(){c.focusManager.unlock()},0)}delete this._.parentDialog;this.foreach(function(a){a.resetInitValue&&a.resetInitValue()})}},addPage:function(a){if(!a.requiredContent||this._.editor.filter.check(a.requiredContent)){for(var b=[],c=a.label?' title="'+CKEDITOR.tools.htmlEncode(a.label)+'"':"",e=CKEDITOR.dialog._.uiElementBuilders.vbox.build(this,{type:"vbox",className:"cke_dialog_page_contents",children:a.elements,expand:!!a.expand,padding:a.padding,
-style:a.style||"width: 100%;"},b),d=this._.contents[a.id]={},g=e.getChild(),f=0;e=g.shift();)!e.notAllowed&&("hbox"!=e.type&&"vbox"!=e.type)&&f++,d[e.id]=e,"function"==typeof e.getChild&&g.push.apply(g,e.getChild());f||(a.hidden=!0);b=CKEDITOR.dom.element.createFromHtml(b.join(""));b.setAttribute("role","tabpanel");e=CKEDITOR.env;d="cke_"+a.id+"_"+CKEDITOR.tools.getNextNumber();c=CKEDITOR.dom.element.createFromHtml(['<a class="cke_dialog_tab"',0<this._.pageCount?" cke_last":"cke_first",c,a.hidden?
-' style="display:none"':"",' id="',d,'"',e.gecko&&10900<=e.version&&!e.hc?"":' href="javascript:void(0)"',' tabIndex="-1" hidefocus="true" role="tab">',a.label,"</a>"].join(""));b.setAttribute("aria-labelledby",d);this._.tabs[a.id]=[c,b];this._.tabIdList.push(a.id);!a.hidden&&this._.pageCount++;this._.lastTab=c;this.updateStyle();b.setAttribute("name",a.id);b.appendTo(this.parts.contents);c.unselectable();this.parts.tabs.append(c);a.accessKey&&(O(this,this,"CTRL+"+a.accessKey,Y,Z),this._.accessKeyMap["CTRL+"+
-a.accessKey]=a.id)}},selectPage:function(a){if(this._.currentTabId!=a&&!this._.tabs[a][0].hasClass("cke_dialog_tab_disabled")&&!0!==this.fire("selectPage",{page:a,currentPage:this._.currentTabId})){for(var b in this._.tabs){var c=this._.tabs[b][0],e=this._.tabs[b][1];b!=a&&(c.removeClass("cke_dialog_tab_selected"),e.hide());e.setAttribute("aria-hidden",b!=a)}var d=this._.tabs[a];d[0].addClass("cke_dialog_tab_selected");CKEDITOR.env.ie6Compat||CKEDITOR.env.ie7Compat?(G(d[1]),d[1].show(),setTimeout(function(){G(d[1],
-1)},0)):d[1].show();this._.currentTabId=a;this._.currentTabIndex=CKEDITOR.tools.indexOf(this._.tabIdList,a)}},updateStyle:function(){this.parts.dialog[(1===this._.pageCount?"add":"remove")+"Class"]("cke_single_page")},hidePage:function(a){var b=this._.tabs[a]&&this._.tabs[a][0];b&&(1!=this._.pageCount&&b.isVisible())&&(a==this._.currentTabId&&this.selectPage(t.call(this)),b.hide(),this._.pageCount--,this.updateStyle())},showPage:function(a){if(a=this._.tabs[a]&&this._.tabs[a][0])a.show(),this._.pageCount++,
-this.updateStyle()},getElement:function(){return this._.element},getName:function(){return this._.name},getContentElement:function(a,b){var c=this._.contents[a];return c&&c[b]},getValueOf:function(a,b){return this.getContentElement(a,b).getValue()},setValueOf:function(a,b,c){return this.getContentElement(a,b).setValue(c)},getButton:function(a){return this._.buttons[a]},click:function(a){return this._.buttons[a].click()},disableButton:function(a){return this._.buttons[a].disable()},enableButton:function(a){return this._.buttons[a].enable()},
-getPageCount:function(){return this._.pageCount},getParentEditor:function(){return this._.editor},getSelectedElement:function(){return this.getParentEditor().getSelection().getSelectedElement()},addFocusable:function(a,b){if("undefined"==typeof b)b=this._.focusList.length,this._.focusList.push(new H(this,a,b));else{this._.focusList.splice(b,0,new H(this,a,b));for(var c=b+1;c<this._.focusList.length;c++)this._.focusList[c].focusIndex++}}};CKEDITOR.tools.extend(CKEDITOR.dialog,{add:function(a,b){if(!this._.dialogDefinitions[a]||
-"function"==typeof b)this._.dialogDefinitions[a]=b},exists:function(a){return!!this._.dialogDefinitions[a]},getCurrent:function(){return CKEDITOR.dialog._.currentTop},isTabEnabled:function(a,b,c){a=a.config.removeDialogTabs;return!(a&&a.match(RegExp("(?:^|;)"+b+":"+c+"(?:$|;)","i")))},okButton:function(){var a=function(a,c){c=c||{};return CKEDITOR.tools.extend({id:"ok",type:"button",label:a.lang.common.ok,"class":"cke_dialog_ui_button_ok",onClick:function(a){a=a.data.dialog;!1!==a.fire("ok",{hide:!0}).hide&&
-a.hide()}},c,!0)};a.type="button";a.override=function(b){return CKEDITOR.tools.extend(function(c){return a(c,b)},{type:"button"},!0)};return a}(),cancelButton:function(){var a=function(a,c){c=c||{};return CKEDITOR.tools.extend({id:"cancel",type:"button",label:a.lang.common.cancel,"class":"cke_dialog_ui_button_cancel",onClick:function(a){a=a.data.dialog;!1!==a.fire("cancel",{hide:!0}).hide&&a.hide()}},c,!0)};a.type="button";a.override=function(b){return CKEDITOR.tools.extend(function(c){return a(c,
-b)},{type:"button"},!0)};return a}(),addUIElement:function(a,b){this._.uiElementBuilders[a]=b}});CKEDITOR.dialog._={uiElementBuilders:{},dialogDefinitions:{},currentTop:null,currentZIndex:null};CKEDITOR.event.implementOn(CKEDITOR.dialog);CKEDITOR.event.implementOn(CKEDITOR.dialog.prototype);var W={resizable:CKEDITOR.DIALOG_RESIZE_BOTH,minWidth:600,minHeight:400,buttons:[CKEDITOR.dialog.okButton,CKEDITOR.dialog.cancelButton]},z=function(a,b,c){for(var e=0,d;d=a[e];e++)if(d.id==b||c&&d[c]&&(d=z(d[c],
-b,c)))return d;return null},A=function(a,b,c,e,d){if(c){for(var g=0,f;f=a[g];g++){if(f.id==c)return a.splice(g,0,b),b;if(e&&f[e]&&(f=A(f[e],b,c,e,!0)))return f}if(d)return null}a.push(b);return b},B=function(a,b,c){for(var e=0,d;d=a[e];e++){if(d.id==b)return a.splice(e,1);if(c&&d[c]&&(d=B(d[c],b,c)))return d}return null},L=function(a,b){this.dialog=a;for(var c=b.contents,e=0,d;d=c[e];e++)c[e]=d&&new I(a,d);CKEDITOR.tools.extend(this,b)};L.prototype={getContents:function(a){return z(this.contents,
-a)},getButton:function(a){return z(this.buttons,a)},addContents:function(a,b){return A(this.contents,a,b)},addButton:function(a,b){return A(this.buttons,a,b)},removeContents:function(a){B(this.contents,a)},removeButton:function(a){B(this.buttons,a)}};I.prototype={get:function(a){return z(this.elements,a,"children")},add:function(a,b){return A(this.elements,a,b,"children")},remove:function(a){B(this.elements,a,"children")}};var F,w={},q,s={},M=function(a){var b=a.data.$.ctrlKey||a.data.$.metaKey,c=
-a.data.$.altKey,e=a.data.$.shiftKey,d=String.fromCharCode(a.data.$.keyCode);if((b=s[(b?"CTRL+":"")+(c?"ALT+":"")+(e?"SHIFT+":"")+d])&&b.length)b=b[b.length-1],b.keydown&&b.keydown.call(b.uiElement,b.dialog,b.key),a.data.preventDefault()},N=function(a){var b=a.data.$.ctrlKey||a.data.$.metaKey,c=a.data.$.altKey,e=a.data.$.shiftKey,d=String.fromCharCode(a.data.$.keyCode);if((b=s[(b?"CTRL+":"")+(c?"ALT+":"")+(e?"SHIFT+":"")+d])&&b.length)b=b[b.length-1],b.keyup&&(b.keyup.call(b.uiElement,b.dialog,b.key),
-a.data.preventDefault())},O=function(a,b,c,e,d){(s[c]||(s[c]=[])).push({uiElement:a,dialog:b,key:c,keyup:d||a.accessKeyUp,keydown:e||a.accessKeyDown})},X=function(a){for(var b in s){for(var c=s[b],e=c.length-1;0<=e;e--)(c[e].dialog==a||c[e].uiElement==a)&&c.splice(e,1);0===c.length&&delete s[b]}},Z=function(a,b){a._.accessKeyMap[b]&&a.selectPage(a._.accessKeyMap[b])},Y=function(){};(function(){CKEDITOR.ui.dialog={uiElement:function(a,b,c,e,d,g,f){if(!(4>arguments.length)){var i=(e.call?e(b):e)||"div",
-l=["<",i," "],k=(d&&d.call?d(b):d)||{},h=(g&&g.call?g(b):g)||{},o=(f&&f.call?f.call(this,a,b):f)||"",j=this.domId=h.id||CKEDITOR.tools.getNextId()+"_uiElement";this.id=b.id;b.requiredContent&&!a.getParentEditor().filter.check(b.requiredContent)&&(k.display="none",this.notAllowed=!0);h.id=j;var n={};b.type&&(n["cke_dialog_ui_"+b.type]=1);b.className&&(n[b.className]=1);b.disabled&&(n.cke_disabled=1);for(var m=h["class"]&&h["class"].split?h["class"].split(" "):[],j=0;j<m.length;j++)m[j]&&(n[m[j]]=1);
-m=[];for(j in n)m.push(j);h["class"]=m.join(" ");b.title&&(h.title=b.title);n=(b.style||"").split(";");b.align&&(m=b.align,k["margin-left"]="left"==m?0:"auto",k["margin-right"]="right"==m?0:"auto");for(j in k)n.push(j+":"+k[j]);b.hidden&&n.push("display:none");for(j=n.length-1;0<=j;j--)""===n[j]&&n.splice(j,1);0<n.length&&(h.style=(h.style?h.style+"; ":"")+n.join("; "));for(j in h)l.push(j+'="'+CKEDITOR.tools.htmlEncode(h[j])+'" ');l.push(">",o,"</",i,">");c.push(l.join(""));(this._||(this._={})).dialog=
-a;"boolean"==typeof b.isChanged&&(this.isChanged=function(){return b.isChanged});"function"==typeof b.isChanged&&(this.isChanged=b.isChanged);"function"==typeof b.setValue&&(this.setValue=CKEDITOR.tools.override(this.setValue,function(a){return function(c){a.call(this,b.setValue.call(this,c))}}));"function"==typeof b.getValue&&(this.getValue=CKEDITOR.tools.override(this.getValue,function(a){return function(){return b.getValue.call(this,a.call(this))}}));CKEDITOR.event.implementOn(this);this.registerEvents(b);
-this.accessKeyUp&&(this.accessKeyDown&&b.accessKey)&&O(this,a,"CTRL+"+b.accessKey);var p=this;a.on("load",function(){var b=p.getInputElement();if(b){var c=p.type in{checkbox:1,ratio:1}&&CKEDITOR.env.ie&&CKEDITOR.env.version<8?"cke_dialog_ui_focused":"";b.on("focus",function(){a._.tabBarMode=false;a._.hasFocus=true;p.fire("focus");c&&this.addClass(c)});b.on("blur",function(){p.fire("blur");c&&this.removeClass(c)})}});CKEDITOR.tools.extend(this,b);this.keyboardFocusable&&(this.tabIndex=b.tabIndex||
-0,this.focusIndex=a._.focusList.push(this)-1,this.on("focus",function(){a._.currentFocusIndex=p.focusIndex}))}},hbox:function(a,b,c,e,d){if(!(4>arguments.length)){this._||(this._={});var g=this._.children=b,f=d&&d.widths||null,i=d&&d.height||null,l,k={role:"presentation"};d&&d.align&&(k.align=d.align);CKEDITOR.ui.dialog.uiElement.call(this,a,d||{type:"hbox"},e,"table",{},k,function(){var a=['<tbody><tr class="cke_dialog_ui_hbox">'];for(l=0;l<c.length;l++){var b="cke_dialog_ui_hbox_child",e=[];0===
-l&&(b="cke_dialog_ui_hbox_first");l==c.length-1&&(b="cke_dialog_ui_hbox_last");a.push('<td class="',b,'" role="presentation" ');f?f[l]&&e.push("width:"+r(f[l])):e.push("width:"+Math.floor(100/c.length)+"%");i&&e.push("height:"+r(i));d&&void 0!=d.padding&&e.push("padding:"+r(d.padding));CKEDITOR.env.ie&&(CKEDITOR.env.quirks&&g[l].align)&&e.push("text-align:"+g[l].align);0<e.length&&a.push('style="'+e.join("; ")+'" ');a.push(">",c[l],"</td>")}a.push("</tr></tbody>");return a.join("")})}},vbox:function(a,
-b,c,e,d){if(!(3>arguments.length)){this._||(this._={});var g=this._.children=b,f=d&&d.width||null,i=d&&d.heights||null;CKEDITOR.ui.dialog.uiElement.call(this,a,d||{type:"vbox"},e,"div",null,{role:"presentation"},function(){var b=['<table role="presentation" cellspacing="0" border="0" '];b.push('style="');d&&d.expand&&b.push("height:100%;");b.push("width:"+r(f||"100%"),";");CKEDITOR.env.webkit&&b.push("float:none;");b.push('"');b.push('align="',CKEDITOR.tools.htmlEncode(d&&d.align||("ltr"==a.getParentEditor().lang.dir?
-"left":"right")),'" ');b.push("><tbody>");for(var e=0;e<c.length;e++){var h=[];b.push('<tr><td role="presentation" ');f&&h.push("width:"+r(f||"100%"));i?h.push("height:"+r(i[e])):d&&d.expand&&h.push("height:"+Math.floor(100/c.length)+"%");d&&void 0!=d.padding&&h.push("padding:"+r(d.padding));CKEDITOR.env.ie&&(CKEDITOR.env.quirks&&g[e].align)&&h.push("text-align:"+g[e].align);0<h.length&&b.push('style="',h.join("; "),'" ');b.push(' class="cke_dialog_ui_vbox_child">',c[e],"</td></tr>")}b.push("</tbody></table>");
-return b.join("")})}}}})();CKEDITOR.ui.dialog.uiElement.prototype={getElement:function(){return CKEDITOR.document.getById(this.domId)},getInputElement:function(){return this.getElement()},getDialog:function(){return this._.dialog},setValue:function(a,b){this.getInputElement().setValue(a);!b&&this.fire("change",{value:a});return this},getValue:function(){return this.getInputElement().getValue()},isChanged:function(){return!1},selectParentTab:function(){for(var a=this.getInputElement();(a=a.getParent())&&
--1==a.$.className.search("cke_dialog_page_contents"););if(!a)return this;a=a.getAttribute("name");this._.dialog._.currentTabId!=a&&this._.dialog.selectPage(a);return this},focus:function(){this.selectParentTab().getInputElement().focus();return this},registerEvents:function(a){var b=/^on([A-Z]\w+)/,c,e=function(a,b,c,d){b.on("load",function(){a.getInputElement().on(c,d,a)})},d;for(d in a)if(c=d.match(b))this.eventProcessors[d]?this.eventProcessors[d].call(this,this._.dialog,a[d]):e(this,this._.dialog,
-c[1].toLowerCase(),a[d]);return this},eventProcessors:{onLoad:function(a,b){a.on("load",b,this)},onShow:function(a,b){a.on("show",b,this)},onHide:function(a,b){a.on("hide",b,this)}},accessKeyDown:function(){this.focus()},accessKeyUp:function(){},disable:function(){var a=this.getElement();this.getInputElement().setAttribute("disabled","true");a.addClass("cke_disabled")},enable:function(){var a=this.getElement();this.getInputElement().removeAttribute("disabled");a.removeClass("cke_disabled")},isEnabled:function(){return!this.getElement().hasClass("cke_disabled")},
-isVisible:function(){return this.getInputElement().isVisible()},isFocusable:function(){return!this.isEnabled()||!this.isVisible()?!1:!0}};CKEDITOR.ui.dialog.hbox.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.uiElement,{getChild:function(a){if(1>arguments.length)return this._.children.concat();a.splice||(a=[a]);return 2>a.length?this._.children[a[0]]:this._.children[a[0]]&&this._.children[a[0]].getChild?this._.children[a[0]].getChild(a.slice(1,a.length)):null}},!0);CKEDITOR.ui.dialog.vbox.prototype=
-new CKEDITOR.ui.dialog.hbox;(function(){var a={build:function(a,c,e){for(var d=c.children,g,f=[],i=[],l=0;l<d.length&&(g=d[l]);l++){var k=[];f.push(k);i.push(CKEDITOR.dialog._.uiElementBuilders[g.type].build(a,g,k))}return new CKEDITOR.ui.dialog[c.type](a,i,f,e,c)}};CKEDITOR.dialog.addUIElement("hbox",a);CKEDITOR.dialog.addUIElement("vbox",a)})();CKEDITOR.dialogCommand=function(a,b){this.dialogName=a;CKEDITOR.tools.extend(this,b,!0)};CKEDITOR.dialogCommand.prototype={exec:function(a){CKEDITOR.env.opera?
-CKEDITOR.tools.setTimeout(function(){a.openDialog(this.dialogName)},0,this):a.openDialog(this.dialogName)},canUndo:!1,editorFocus:1};(function(){var a=/^([a]|[^a])+$/,b=/^\d*$/,c=/^\d*(?:\.\d+)?$/,e=/^(((\d*(\.\d+))|(\d*))(px|\%)?)?$/,d=/^(((\d*(\.\d+))|(\d*))(px|em|ex|in|cm|mm|pt|pc|\%)?)?$/i,g=/^(\s*[\w-]+\s*:\s*[^:;]+(?:;|$))*$/;CKEDITOR.VALIDATE_OR=1;CKEDITOR.VALIDATE_AND=2;CKEDITOR.dialog.validate={functions:function(){var a=arguments;return function(){var b=this&&this.getValue?this.getValue():
-a[0],c=void 0,d=CKEDITOR.VALIDATE_AND,e=[],g;for(g=0;g<a.length;g++)if("function"==typeof a[g])e.push(a[g]);else break;g<a.length&&"string"==typeof a[g]&&(c=a[g],g++);g<a.length&&"number"==typeof a[g]&&(d=a[g]);var j=d==CKEDITOR.VALIDATE_AND?!0:!1;for(g=0;g<e.length;g++)j=d==CKEDITOR.VALIDATE_AND?j&&e[g](b):j||e[g](b);return!j?c:!0}},regex:function(a,b){return function(c){c=this&&this.getValue?this.getValue():c;return!a.test(c)?b:!0}},notEmpty:function(b){return this.regex(a,b)},integer:function(a){return this.regex(b,
-a)},number:function(a){return this.regex(c,a)},cssLength:function(a){return this.functions(function(a){return d.test(CKEDITOR.tools.trim(a))},a)},htmlLength:function(a){return this.functions(function(a){return e.test(CKEDITOR.tools.trim(a))},a)},inlineStyle:function(a){return this.functions(function(a){return g.test(CKEDITOR.tools.trim(a))},a)},equals:function(a,b){return this.functions(function(b){return b==a},b)},notEqual:function(a,b){return this.functions(function(b){return b!=a},b)}};CKEDITOR.on("instanceDestroyed",
-function(a){if(CKEDITOR.tools.isEmpty(CKEDITOR.instances)){for(var b;b=CKEDITOR.dialog._.currentTop;)b.hide();for(var c in w)w[c].remove();w={}}var a=a.editor._.storedDialogs,d;for(d in a)a[d].destroy()})})();CKEDITOR.tools.extend(CKEDITOR.editor.prototype,{openDialog:function(a,b){var c=null,e=CKEDITOR.dialog._.dialogDefinitions[a];null===CKEDITOR.dialog._.currentTop&&J(this);if("function"==typeof e)c=this._.storedDialogs||(this._.storedDialogs={}),c=c[a]||(c[a]=new CKEDITOR.dialog(this,a)),b&&b.call(c,
-c),c.show();else{if("failed"==e)throw K(this),Error('[CKEDITOR.dialog.openDialog] Dialog "'+a+'" failed when loading definition.');"string"==typeof e&&CKEDITOR.scriptLoader.load(CKEDITOR.getUrl(e),function(){"function"!=typeof CKEDITOR.dialog._.dialogDefinitions[a]&&(CKEDITOR.dialog._.dialogDefinitions[a]="failed");this.openDialog(a,b)},this,0,1)}CKEDITOR.skin.loadPart("dialog");return c}})})();
-CKEDITOR.plugins.add("dialog",{requires:"dialogui",init:function(t){t.on("doubleclick",function(u){u.data.dialog&&t.openDialog(u.data.dialog)},null,null,999)}});CKEDITOR.plugins.add("about",{requires:"dialog",init:function(a){var b=a.addCommand("about",new CKEDITOR.dialogCommand("about"));b.modes={wysiwyg:1,source:1};b.canUndo=!1;b.readOnly=1;a.ui.addButton&&a.ui.addButton("About",{label:a.lang.about.title,command:"about",toolbar:"about"});CKEDITOR.dialog.add("about",this.path+"dialogs/about.js")}});CKEDITOR.plugins.add("basicstyles",{init:function(c){var e=0,d=function(g,d,b,a){if(a){var a=new CKEDITOR.style(a),f=h[b];f.unshift(a);c.attachStyleStateChange(a,function(a){!c.readOnly&&c.getCommand(b).setState(a)});c.addCommand(b,new CKEDITOR.styleCommand(a,{contentForms:f}));c.ui.addButton&&c.ui.addButton(g,{label:d,command:b,toolbar:"basicstyles,"+(e+=10)})}},h={bold:["strong","b",["span",function(a){a=a.styles["font-weight"];return"bold"==a||700<=+a}]],italic:["em","i",["span",function(a){return"italic"==
+CKEDITOR.skin.name="moono-lisa";CKEDITOR.skin.ua_editor="ie,iequirks,ie8,gecko";CKEDITOR.skin.ua_dialog="ie,iequirks,ie8";
+CKEDITOR.skin.chameleon=function(){var b=function(){return function(b,d){for(var a=b.match(/[^#]./g),e=0;3>e;e++){var f=e,c;c=parseInt(a[e],16);c=("0"+(0>d?0|c*(1+d):0|c+(255-c)*d).toString(16)).slice(-2);a[f]=c}return"#"+a.join("")}}(),f={editor:new CKEDITOR.template("{id}.cke_chrome [border-color:{defaultBorder};] {id} .cke_top [ background-color:{defaultBackground};border-bottom-color:{defaultBorder};] {id} .cke_bottom [background-color:{defaultBackground};border-top-color:{defaultBorder};] {id} .cke_resizer [border-right-color:{ckeResizer}] {id} .cke_dialog_title [background-color:{defaultBackground};border-bottom-color:{defaultBorder};] {id} .cke_dialog_footer [background-color:{defaultBackground};outline-color:{defaultBorder};] {id} .cke_dialog_tab [background-color:{dialogTab};border-color:{defaultBorder};] {id} .cke_dialog_tab:hover [background-color:{lightBackground};] {id} .cke_dialog_contents [border-top-color:{defaultBorder};] {id} .cke_dialog_tab_selected, {id} .cke_dialog_tab_selected:hover [background:{dialogTabSelected};border-bottom-color:{dialogTabSelectedBorder};] {id} .cke_dialog_body [background:{dialogBody};border-color:{defaultBorder};] {id} a.cke_button_off:hover,{id} a.cke_button_off:focus,{id} a.cke_button_off:active [background-color:{darkBackground};border-color:{toolbarElementsBorder};] {id} .cke_button_on [background-color:{ckeButtonOn};border-color:{toolbarElementsBorder};] {id} .cke_toolbar_separator,{id} .cke_toolgroup a.cke_button:last-child:after,{id} .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after [background-color: {toolbarElementsBorder};border-color: {toolbarElementsBorder};] {id} a.cke_combo_button:hover,{id} a.cke_combo_button:focus,{id} .cke_combo_on a.cke_combo_button [border-color:{toolbarElementsBorder};background-color:{darkBackground};] {id} .cke_combo:after [border-color:{toolbarElementsBorder};] {id} .cke_path_item [color:{elementsPathColor};] {id} a.cke_path_item:hover,{id} a.cke_path_item:focus,{id} a.cke_path_item:active [background-color:{darkBackground};] {id}.cke_panel [border-color:{defaultBorder};] "),panel:new CKEDITOR.template(".cke_panel_grouptitle [background-color:{lightBackground};border-color:{defaultBorder};] .cke_menubutton_icon [background-color:{menubuttonIcon};] .cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active [background-color:{menubuttonHover};] .cke_menubutton:hover .cke_menubutton_icon, .cke_menubutton:focus .cke_menubutton_icon, .cke_menubutton:active .cke_menubutton_icon [background-color:{menubuttonIconHover};] .cke_menubutton_disabled:hover .cke_menubutton_icon,.cke_menubutton_disabled:focus .cke_menubutton_icon,.cke_menubutton_disabled:active .cke_menubutton_icon [background-color:{menubuttonIcon};] .cke_menuseparator [background-color:{menubuttonIcon};] a:hover.cke_colorbox, a:active.cke_colorbox [border-color:{defaultBorder};] a:hover.cke_colorauto, a:hover.cke_colormore, a:active.cke_colorauto, a:active.cke_colormore [background-color:{ckeColorauto};border-color:{defaultBorder};] ")};
+return function(g,d){var a=b(g.uiColor,.4),a={id:"."+g.id,defaultBorder:b(a,-.2),toolbarElementsBorder:b(a,-.25),defaultBackground:a,lightBackground:b(a,.8),darkBackground:b(a,-.15),ckeButtonOn:b(a,.4),ckeResizer:b(a,-.4),ckeColorauto:b(a,.8),dialogBody:b(a,.7),dialogTab:b(a,.65),dialogTabSelected:"#FFF",dialogTabSelectedBorder:"#FFF",elementsPathColor:b(a,-.6),menubuttonHover:b(a,.1),menubuttonIcon:b(a,.5),menubuttonIconHover:b(a,.3)};return f[d].output(a).replace(/\[/g,"{").replace(/\]/g,"}")}}();CKEDITOR.plugins.add("basicstyles",{init:function(c){var e=0,d=function(g,d,b,a){if(a){a=new CKEDITOR.style(a);var f=h[b];f.unshift(a);c.attachStyleStateChange(a,function(a){!c.readOnly&&c.getCommand(b).setState(a)});c.addCommand(b,new CKEDITOR.styleCommand(a,{contentForms:f}));c.ui.addButton&&c.ui.addButton(g,{label:d,command:b,toolbar:"basicstyles,"+(e+=10)})}},h={bold:["strong","b",["span",function(a){a=a.styles["font-weight"];return"bold"==a||700<=+a}]],italic:["em","i",["span",function(a){return"italic"==
 a.styles["font-style"]}]],underline:["u",["span",function(a){return"underline"==a.styles["text-decoration"]}]],strike:["s","strike",["span",function(a){return"line-through"==a.styles["text-decoration"]}]],subscript:["sub"],superscript:["sup"]},b=c.config,a=c.lang.basicstyles;d("Bold",a.bold,"bold",b.coreStyles_bold);d("Italic",a.italic,"italic",b.coreStyles_italic);d("Underline",a.underline,"underline",b.coreStyles_underline);d("Strike",a.strike,"strike",b.coreStyles_strike);d("Subscript",a.subscript,
 "subscript",b.coreStyles_subscript);d("Superscript",a.superscript,"superscript",b.coreStyles_superscript);c.setKeystroke([[CKEDITOR.CTRL+66,"bold"],[CKEDITOR.CTRL+73,"italic"],[CKEDITOR.CTRL+85,"underline"]])}});CKEDITOR.config.coreStyles_bold={element:"strong",overrides:"b"};CKEDITOR.config.coreStyles_italic={element:"em",overrides:"i"};CKEDITOR.config.coreStyles_underline={element:"u"};CKEDITOR.config.coreStyles_strike={element:"s",overrides:"strike"};CKEDITOR.config.coreStyles_subscript={element:"sub"};
-CKEDITOR.config.coreStyles_superscript={element:"sup"};(function(){function w(b){function a(){var e=b.editable();e.on(q,function(b){(!CKEDITOR.env.ie||!n)&&u(b)});CKEDITOR.env.ie&&e.on("paste",function(e){r||(f(),e.data.preventDefault(),u(e),l("paste")||b.openDialog("paste"))});CKEDITOR.env.ie&&(e.on("contextmenu",h,null,null,0),e.on("beforepaste",function(b){b.data&&!b.data.$.ctrlKey&&h()},null,null,0));e.on("beforecut",function(){!n&&i(b)});var a;e.attachListener(CKEDITOR.env.ie?e:b.document.getDocumentElement(),"mouseup",function(){a=setTimeout(function(){s()},
-0)});b.on("destroy",function(){clearTimeout(a)});e.on("keyup",s)}function c(e){return{type:e,canUndo:"cut"==e,startDisabled:!0,exec:function(){"cut"==this.type&&i();var e;var a=this.type;if(CKEDITOR.env.ie)e=l(a);else try{e=b.document.$.execCommand(a,!1,null)}catch(d){e=!1}e||alert(b.lang.clipboard[this.type+"Error"]);return e}}}function d(){return{canUndo:!1,async:!0,exec:function(b,a){var d=function(a,d){a&&g(a.type,a.dataValue,!!d);b.fire("afterCommandExec",{name:"paste",command:c,returnValue:!!a})},
-c=this;"string"==typeof a?d({type:"auto",dataValue:a},1):b.getClipboardData(d)}}}function f(){r=1;setTimeout(function(){r=0},100)}function h(){n=1;setTimeout(function(){n=0},10)}function l(e){var a=b.document,d=a.getBody(),c=!1,i=function(){c=!0};d.on(e,i);(7<CKEDITOR.env.version?a.$:a.$.selection.createRange()).execCommand(e);d.removeListener(e,i);return c}function g(e,a,d){e={type:e};if(d&&!b.fire("beforePaste",e)||!a)return!1;e.dataValue=a;return b.fire("paste",e)}function i(){if(CKEDITOR.env.ie&&
-!CKEDITOR.env.quirks){var e=b.getSelection(),a,d,c;if(e.getType()==CKEDITOR.SELECTION_ELEMENT&&(a=e.getSelectedElement()))d=e.getRanges()[0],c=b.document.createText(""),c.insertBefore(a),d.setStartBefore(c),d.setEndAfter(a),e.selectRanges([d]),setTimeout(function(){a.getParent()&&(c.remove(),e.selectElement(a))},0)}}function k(a,d){var c=b.document,i=b.editable(),k=function(b){b.cancel()},f=CKEDITOR.env.gecko&&10902>=CKEDITOR.env.version,h;if(!c.getById("cke_pastebin")){var o=b.getSelection(),v=o.createBookmarks(),
-j=new CKEDITOR.dom.element((CKEDITOR.env.webkit||i.is("body"))&&!CKEDITOR.env.ie&&!CKEDITOR.env.opera?"body":"div",c);j.setAttributes({id:"cke_pastebin","data-cke-temp":"1"});CKEDITOR.env.opera&&j.appendBogus();var g=0,c=c.getWindow();f?(j.insertAfter(v[0].startNode),j.setStyle("display","inline")):(CKEDITOR.env.webkit?(i.append(j),j.addClass("cke_editable"),i.is("body")||(f="static"!=i.getComputedStyle("position")?i:CKEDITOR.dom.element.get(i.$.offsetParent),g=f.getDocumentPosition().y)):i.getAscendant(CKEDITOR.env.ie||
-CKEDITOR.env.opera?"body":"html",1).append(j),j.setStyles({position:"absolute",top:c.getScrollPosition().y-g+10+"px",width:"1px",height:Math.max(1,c.getViewPaneSize().height-20)+"px",overflow:"hidden",margin:0,padding:0}));(f=j.getParent().isReadOnly())?(j.setOpacity(0),j.setAttribute("contenteditable",!0)):j.setStyle("ltr"==b.config.contentsLangDirection?"left":"right","-1000px");b.on("selectionChange",k,null,null,0);CKEDITOR.env.webkit&&(h=i.once("blur",k,null,null,-100));f&&j.focus();f=new CKEDITOR.dom.range(j);
-f.selectNodeContents(j);var l=f.select();CKEDITOR.env.ie&&(h=i.once("blur",function(){b.lockSelection(l)}));var m=CKEDITOR.document.getWindow().getScrollPosition().y;setTimeout(function(){if(CKEDITOR.env.webkit||CKEDITOR.env.opera)CKEDITOR.document[CKEDITOR.env.webkit?"getBody":"getDocumentElement"]().$.scrollTop=m;h&&h.removeListener();CKEDITOR.env.ie&&i.focus();o.selectBookmarks(v);j.remove();var a;if(CKEDITOR.env.webkit&&(a=j.getFirst())&&a.is&&a.hasClass("Apple-style-span"))j=a;b.removeListener("selectionChange",
-k);d(j.getHtml())},0)}}function o(){if(CKEDITOR.env.ie){b.focus();f();var a=b.focusManager;a.lock();if(b.editable().fire(q)&&!l("paste"))return a.unlock(),!1;a.unlock()}else try{if(b.editable().fire(q)&&!b.document.$.execCommand("Paste",!1,null))throw 0;}catch(d){return!1}return!0}function p(a){if("wysiwyg"==b.mode)switch(a.data.keyCode){case CKEDITOR.CTRL+86:case CKEDITOR.SHIFT+45:a=b.editable();f();!CKEDITOR.env.ie&&a.fire("beforepaste");(CKEDITOR.env.opera||CKEDITOR.env.gecko&&10900>CKEDITOR.env.version)&&
-a.fire("paste");break;case CKEDITOR.CTRL+88:case CKEDITOR.SHIFT+46:b.fire("saveSnapshot"),setTimeout(function(){b.fire("saveSnapshot")},0)}}function u(a){var d={type:"auto"},c=b.fire("beforePaste",d);k(a,function(b){b=b.replace(/<span[^>]+data-cke-bookmark[^<]*?<\/span>/ig,"");c&&g(d.type,b,0,1)})}function s(){if("wysiwyg"==b.mode){var a=m("paste");b.getCommand("cut").setState(m("cut"));b.getCommand("copy").setState(m("copy"));b.getCommand("paste").setState(a);b.fire("pasteState",a)}}function m(a){if(t&&
-a in{paste:1,cut:1})return CKEDITOR.TRISTATE_DISABLED;if("paste"==a)return CKEDITOR.TRISTATE_OFF;var a=b.getSelection(),d=a.getRanges();return a.getType()==CKEDITOR.SELECTION_NONE||1==d.length&&d[0].collapsed?CKEDITOR.TRISTATE_DISABLED:CKEDITOR.TRISTATE_OFF}var n=0,r=0,t=0,q=CKEDITOR.env.ie?"beforepaste":"paste";(function(){b.on("key",p);b.on("contentDom",a);b.on("selectionChange",function(a){t=a.data.selection.getRanges()[0].checkReadOnly();s()});b.contextMenu&&b.contextMenu.addListener(function(a,
-b){t=b.getRanges()[0].checkReadOnly();return{cut:m("cut"),copy:m("copy"),paste:m("paste")}})})();(function(){function a(d,c,i,e,f){var k=b.lang.clipboard[c];b.addCommand(c,i);b.ui.addButton&&b.ui.addButton(d,{label:k,command:c,toolbar:"clipboard,"+e});b.addMenuItems&&b.addMenuItem(c,{label:k,command:c,group:"clipboard",order:f})}a("Cut","cut",c("cut"),10,1);a("Copy","copy",c("copy"),20,4);a("Paste","paste",d(),30,8)})();b.getClipboardData=function(a,d){function c(a){a.removeListener();a.cancel();
-d(a.data)}function i(a){a.removeListener();a.cancel();g=!0;d({type:h,dataValue:a.data})}function f(){this.customTitle=a&&a.title}var k=!1,h="auto",g=!1;d||(d=a,a=null);b.on("paste",c,null,null,0);b.on("beforePaste",function(a){a.removeListener();k=true;h=a.data.type},null,null,1E3);!1===o()&&(b.removeListener("paste",c),k&&b.fire("pasteDialog",f)?(b.on("pasteDialogCommit",i),b.on("dialogHide",function(a){a.removeListener();a.data.removeListener("pasteDialogCommit",i);setTimeout(function(){g||d(null)},
-10)})):d(null))}}function x(b){if(CKEDITOR.env.webkit){if(!b.match(/^[^<]*$/g)&&!b.match(/^(<div><br( ?\/)?><\/div>|<div>[^<]*<\/div>)*$/gi))return"html"}else if(CKEDITOR.env.ie){if(!b.match(/^([^<]|<br( ?\/)?>)*$/gi)&&!b.match(/^(<p>([^<]|<br( ?\/)?>)*<\/p>|(\r\n))*$/gi))return"html"}else if(CKEDITOR.env.gecko||CKEDITOR.env.opera){if(!b.match(/^([^<]|<br( ?\/)?>)*$/gi))return"html"}else return"html";return"htmlifiedtext"}function y(b,a){function c(a){return CKEDITOR.tools.repeat("</p><p>",~~(a/2))+
-(1==a%2?"<br>":"")}a=a.replace(/\s+/g," ").replace(/> +</g,"><").replace(/<br ?\/>/gi,"<br>");a=a.replace(/<\/?[A-Z]+>/g,function(a){return a.toLowerCase()});if(a.match(/^[^<]$/))return a;CKEDITOR.env.webkit&&-1<a.indexOf("<div>")&&(a=a.replace(/^(<div>(<br>|)<\/div>)(?!$|(<div>(<br>|)<\/div>))/g,"<br>").replace(/^(<div>(<br>|)<\/div>){2}(?!$)/g,"<div></div>"),a.match(/<div>(<br>|)<\/div>/)&&(a="<p>"+a.replace(/(<div>(<br>|)<\/div>)+/g,function(a){return c(a.split("</div><div>").length+1)})+"</p>"),
-a=a.replace(/<\/div><div>/g,"<br>"),a=a.replace(/<\/?div>/g,""));if((CKEDITOR.env.gecko||CKEDITOR.env.opera)&&b.enterMode!=CKEDITOR.ENTER_BR)CKEDITOR.env.gecko&&(a=a.replace(/^<br><br>$/,"<br>")),-1<a.indexOf("<br><br>")&&(a="<p>"+a.replace(/(<br>){2,}/g,function(a){return c(a.length/4)})+"</p>");return p(b,a)}function z(){var b=new CKEDITOR.htmlParser.filter,a={blockquote:1,dl:1,fieldset:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,ol:1,p:1,table:1,ul:1},c=CKEDITOR.tools.extend({br:0},CKEDITOR.dtd.$inline),d=
-{p:1,br:1,"cke:br":1},f=CKEDITOR.dtd,h=CKEDITOR.tools.extend({area:1,basefont:1,embed:1,iframe:1,map:1,object:1,param:1},CKEDITOR.dtd.$nonBodyContent,CKEDITOR.dtd.$cdata),l=function(a){delete a.name;a.add(new CKEDITOR.htmlParser.text(" "))},g=function(a){for(var b=a,c;(b=b.next)&&b.name&&b.name.match(/^h\d$/);){c=new CKEDITOR.htmlParser.element("cke:br");c.isEmpty=!0;for(a.add(c);c=b.children.shift();)a.add(c)}};b.addRules({elements:{h1:g,h2:g,h3:g,h4:g,h5:g,h6:g,img:function(a){var a=CKEDITOR.tools.trim(a.attributes.alt||
-""),b=" ";a&&!a.match(/(^http|\.(jpe?g|gif|png))/i)&&(b=" ["+a+"] ");return new CKEDITOR.htmlParser.text(b)},td:l,th:l,$:function(b){var k=b.name,g;if(h[k])return!1;b.attributes=[];if("br"==k)return b;if(a[k])b.name="p";else if(c[k])delete b.name;else if(f[k]){g=new CKEDITOR.htmlParser.element("cke:br");g.isEmpty=!0;if(CKEDITOR.dtd.$empty[k])return g;b.add(g,0);g=g.clone();g.isEmpty=!0;b.add(g);delete b.name}d[b.name]||delete b.name;return b}}},{applyToAll:!0});return b}function A(b,a,c){var a=new CKEDITOR.htmlParser.fragment.fromHtml(a),
-d=new CKEDITOR.htmlParser.basicWriter;a.writeHtml(d,c);var a=d.getHtml(),a=a.replace(/\s*(<\/?[a-z:]+ ?\/?>)\s*/g,"$1").replace(/(<cke:br \/>){2,}/g,"<cke:br />").replace(/(<cke:br \/>)(<\/?p>|<br \/>)/g,"$2").replace(/(<\/?p>|<br \/>)(<cke:br \/>)/g,"$1").replace(/<(cke:)?br( \/)?>/g,"<br>").replace(/<p><\/p>/g,""),f=0,a=a.replace(/<\/?p>/g,function(a){if("<p>"==a){if(1<++f)return"</p><p>"}else if(0<--f)return"</p><p>";return a}).replace(/<p><\/p>/g,"");return p(b,a)}function p(b,a){b.enterMode==
-CKEDITOR.ENTER_BR?a=a.replace(/(<\/p><p>)+/g,function(a){return CKEDITOR.tools.repeat("<br>",2*(a.length/7))}).replace(/<\/?p>/g,""):b.enterMode==CKEDITOR.ENTER_DIV&&(a=a.replace(/<(\/)?p>/g,"<$1div>"));return a}CKEDITOR.plugins.add("clipboard",{requires:"dialog",init:function(b){var a;w(b);CKEDITOR.dialog.add("paste",CKEDITOR.getUrl(this.path+"dialogs/paste.js"));b.on("paste",function(a){var b=a.data.dataValue,f=CKEDITOR.dtd.$block;-1<b.indexOf("Apple-")&&(b=b.replace(/<span class="Apple-converted-space">&nbsp;<\/span>/gi,
-" "),"html"!=a.data.type&&(b=b.replace(/<span class="Apple-tab-span"[^>]*>([^<]*)<\/span>/gi,function(a,b){return b.replace(/\t/g,"&nbsp;&nbsp; &nbsp;")})),-1<b.indexOf('<br class="Apple-interchange-newline">')&&(a.data.startsWithEOL=1,a.data.preSniffing="html",b=b.replace(/<br class="Apple-interchange-newline">/,"")),b=b.replace(/(<[^>]+) class="Apple-[^"]*"/gi,"$1"));if(b.match(/^<[^<]+cke_(editable|contents)/i)){var h,l,g=new CKEDITOR.dom.element("div");for(g.setHtml(b);1==g.getChildCount()&&(h=
-g.getFirst())&&h.type==CKEDITOR.NODE_ELEMENT&&(h.hasClass("cke_editable")||h.hasClass("cke_contents"));)g=l=h;l&&(b=l.getHtml().replace(/<br>$/i,""))}CKEDITOR.env.ie?b=b.replace(/^&nbsp;(?: |\r\n)?<(\w+)/g,function(b,d){if(d.toLowerCase()in f){a.data.preSniffing="html";return"<"+d}return b}):CKEDITOR.env.webkit?b=b.replace(/<\/(\w+)><div><br><\/div>$/,function(b,d){if(d in f){a.data.endsWithEOL=1;return"</"+d+">"}return b}):CKEDITOR.env.gecko&&(b=b.replace(/(\s)<br>$/,"$1"));a.data.dataValue=b},null,
-null,3);b.on("paste",function(c){var c=c.data,d=c.type,f=c.dataValue,h,l=b.config.clipboard_defaultContentType||"html";h="html"==d||"html"==c.preSniffing?"html":x(f);"htmlifiedtext"==h?f=y(b.config,f):"text"==d&&"html"==h&&(f=A(b.config,f,a||(a=z(b))));c.startsWithEOL&&(f='<br data-cke-eol="1">'+f);c.endsWithEOL&&(f+='<br data-cke-eol="1">');"auto"==d&&(d="html"==h||"html"==l?"html":"text");c.type=d;c.dataValue=f;delete c.preSniffing;delete c.startsWithEOL;delete c.endsWithEOL},null,null,6);b.on("paste",
-function(a){a=a.data;b.insertHtml(a.dataValue,a.type);setTimeout(function(){b.fire("afterPaste")},0)},null,null,1E3);b.on("pasteDialog",function(a){setTimeout(function(){b.openDialog("paste",a.data)},0)})}})})();(function(){var c='<a id="{id}" class="cke_button cke_button__{name} cke_button_{state} {cls}"'+(CKEDITOR.env.gecko&&10900<=CKEDITOR.env.version&&!CKEDITOR.env.hc?"":" href=\"javascript:void('{titleJs}')\"")+' title="{title}" tabindex="-1" hidefocus="true" role="button" aria-labelledby="{id}_label" aria-haspopup="{hasArrow}" aria-disabled="{ariaDisabled}"';if(CKEDITOR.env.opera||CKEDITOR.env.gecko&&CKEDITOR.env.mac)c+=' onkeypress="return false;"';CKEDITOR.env.gecko&&(c+=' onblur="this.style.cssText = this.style.cssText;"');
-var c=c+(' onkeydown="return CKEDITOR.tools.callFunction({keydownFn},event);" onfocus="return CKEDITOR.tools.callFunction({focusFn},event);"  onmousedown="return CKEDITOR.tools.callFunction({mousedownFn},event);" '+(CKEDITOR.env.ie?'onclick="return false;" onmouseup':"onclick")+'="CKEDITOR.tools.callFunction({clickFn},this);return false;"><span class="cke_button_icon cke_button__{iconName}_icon" style="{style}"'),c=c+'>&nbsp;</span><span id="{id}_label" class="cke_button_label cke_button__{name}_label" aria-hidden="false">{label}</span>{arrowHtml}</a>',
-m=CKEDITOR.addTemplate("buttonArrow",'<span class="cke_button_arrow">'+(CKEDITOR.env.hc?"&#9660;":"")+"</span>"),n=CKEDITOR.addTemplate("button",c);CKEDITOR.plugins.add("button",{beforeInit:function(a){a.ui.addHandler(CKEDITOR.UI_BUTTON,CKEDITOR.ui.button.handler)}});CKEDITOR.UI_BUTTON="button";CKEDITOR.ui.button=function(a){CKEDITOR.tools.extend(this,a,{title:a.label,click:a.click||function(b){b.execCommand(a.command)}});this._={}};CKEDITOR.ui.button.handler={create:function(a){return new CKEDITOR.ui.button(a)}};
-CKEDITOR.ui.button.prototype={render:function(a,b){var c=CKEDITOR.env,i=this._.id=CKEDITOR.tools.getNextId(),f="",e=this.command,l;this._.editor=a;var d={id:i,button:this,editor:a,focus:function(){CKEDITOR.document.getById(i).focus()},execute:function(){this.button.click(a)},attach:function(a){this.button.attach(a)}},o=CKEDITOR.tools.addFunction(function(a){if(d.onkey)return a=new CKEDITOR.dom.event(a),!1!==d.onkey(d,a.getKeystroke())}),p=CKEDITOR.tools.addFunction(function(a){var b;d.onfocus&&(b=
-!1!==d.onfocus(d,new CKEDITOR.dom.event(a)));CKEDITOR.env.gecko&&10900>CKEDITOR.env.version&&a.preventBubble();return b}),j=0,q=CKEDITOR.tools.addFunction(function(){if(CKEDITOR.env.opera){var b=a.editable();b.isInline()&&b.hasFocus&&(a.lockSelection(),j=1)}});d.clickFn=l=CKEDITOR.tools.addFunction(function(){j&&(a.unlockSelection(1),j=0);d.execute()});if(this.modes){var k={},g=function(){var b=a.mode;b&&(b=this.modes[b]?void 0!=k[b]?k[b]:CKEDITOR.TRISTATE_OFF:CKEDITOR.TRISTATE_DISABLED,b=a.readOnly&&
-!this.readOnly?CKEDITOR.TRISTATE_DISABLED:b,this.setState(b),this.refresh&&this.refresh())};a.on("beforeModeUnload",function(){a.mode&&this._.state!=CKEDITOR.TRISTATE_DISABLED&&(k[a.mode]=this._.state)},this);a.on("activeFilterChange",g,this);a.on("mode",g,this);!this.readOnly&&a.on("readOnly",g,this)}else if(e&&(e=a.getCommand(e)))e.on("state",function(){this.setState(e.state)},this),f+=e.state==CKEDITOR.TRISTATE_ON?"on":e.state==CKEDITOR.TRISTATE_DISABLED?"disabled":"off";if(this.directional)a.on("contentDirChanged",
-function(b){var c=CKEDITOR.document.getById(this._.id),d=c.getFirst(),b=b.data;b!=a.lang.dir?c.addClass("cke_"+b):c.removeClass("cke_ltr").removeClass("cke_rtl");d.setAttribute("style",CKEDITOR.skin.getIconStyle(h,"rtl"==b,this.icon,this.iconOffset))},this);e||(f+="off");var h=g=this.name||this.command;this.icon&&!/\./.test(this.icon)&&(h=this.icon,this.icon=null);c={id:i,name:g,iconName:h,label:this.label,cls:this.className||"",state:f,ariaDisabled:"disabled"==f?"true":"false",title:this.title,titleJs:c.gecko&&
-10900<=c.version&&!c.hc?"":(this.title||"").replace("'",""),hasArrow:this.hasArrow?"true":"false",keydownFn:o,mousedownFn:q,focusFn:p,clickFn:l,style:CKEDITOR.skin.getIconStyle(h,"rtl"==a.lang.dir,this.icon,this.iconOffset),arrowHtml:this.hasArrow?m.output():""};n.output(c,b);if(this.onRender)this.onRender();return d},setState:function(a){if(this._.state==a)return!1;this._.state=a;var b=CKEDITOR.document.getById(this._.id);return b?(b.setState(a,"cke_button"),a==CKEDITOR.TRISTATE_DISABLED?b.setAttribute("aria-disabled",
-!0):b.removeAttribute("aria-disabled"),a==CKEDITOR.TRISTATE_ON?b.setAttribute("aria-pressed",!0):b.removeAttribute("aria-pressed"),!0):!1},getState:function(){return this._.state},toFeature:function(a){if(this._.feature)return this._.feature;var b=this;!this.allowedContent&&(!this.requiredContent&&this.command)&&(b=a.getCommand(this.command)||b);return this._.feature=b}};CKEDITOR.ui.prototype.addButton=function(a,b){this.add(a,CKEDITOR.UI_BUTTON,b)}})();(function(){function w(a){function d(){for(var b=g(),e=CKEDITOR.tools.clone(a.config.toolbarGroups)||n(a),f=0;f<e.length;f++){var k=e[f];if("/"!=k){"string"==typeof k&&(k=e[f]={name:k});var i,d=k.groups;if(d)for(var h=0;h<d.length;h++)i=d[h],(i=b[i])&&c(k,i);(i=b[k.name])&&c(k,i)}}return e}function g(){var b={},c,f,e;for(c in a.ui.items)f=a.ui.items[c],e=f.toolbar||"others",e=e.split(","),f=e[0],e=parseInt(e[1]||-1,10),b[f]||(b[f]=[]),b[f].push({name:c,order:e});for(f in b)b[f]=b[f].sort(function(b,
-a){return b.order==a.order?0:0>a.order?-1:0>b.order?1:b.order<a.order?-1:1});return b}function c(c,e){if(e.length){c.items?c.items.push(a.ui.create("-")):c.items=[];for(var f;f=e.shift();)if(f="string"==typeof f?f:f.name,!b||-1==CKEDITOR.tools.indexOf(b,f))(f=a.ui.create(f))&&a.addFeature(f)&&c.items.push(f)}}function h(b){var a=[],e,d,h;for(e=0;e<b.length;++e)d=b[e],h={},"/"==d?a.push(d):CKEDITOR.tools.isArray(d)?(c(h,CKEDITOR.tools.clone(d)),a.push(h)):d.items&&(c(h,CKEDITOR.tools.clone(d.items)),
-h.name=d.name,a.push(h));return a}var b=a.config.removeButtons,b=b&&b.split(","),e=a.config.toolbar;"string"==typeof e&&(e=a.config["toolbar_"+e]);return a.toolbar=e?h(e):d()}function n(a){return a._.toolbarGroups||(a._.toolbarGroups=[{name:"document",groups:["mode","document","doctools"]},{name:"clipboard",groups:["clipboard","undo"]},{name:"editing",groups:["find","selection","spellchecker"]},{name:"forms"},"/",{name:"basicstyles",groups:["basicstyles","cleanup"]},{name:"paragraph",groups:["list",
-"indent","blocks","align","bidi"]},{name:"links"},{name:"insert"},"/",{name:"styles"},{name:"colors"},{name:"tools"},{name:"others"},{name:"about"}])}var u=function(){this.toolbars=[];this.focusCommandExecuted=!1};u.prototype.focus=function(){for(var a=0,d;d=this.toolbars[a++];)for(var g=0,c;c=d.items[g++];)if(c.focus){c.focus();return}};var x={modes:{wysiwyg:1,source:1},readOnly:1,exec:function(a){a.toolbox&&(a.toolbox.focusCommandExecuted=!0,CKEDITOR.env.ie||CKEDITOR.env.air?setTimeout(function(){a.toolbox.focus()},
-100):a.toolbox.focus())}};CKEDITOR.plugins.add("toolbar",{requires:"button",init:function(a){var d,g=function(c,h){var b,e="rtl"==a.lang.dir,j=a.config.toolbarGroupCycling,o=e?37:39,e=e?39:37,j=void 0===j||j;switch(h){case 9:case CKEDITOR.SHIFT+9:for(;!b||!b.items.length;)if(b=9==h?(b?b.next:c.toolbar.next)||a.toolbox.toolbars[0]:(b?b.previous:c.toolbar.previous)||a.toolbox.toolbars[a.toolbox.toolbars.length-1],b.items.length)for(c=b.items[d?b.items.length-1:0];c&&!c.focus;)(c=d?c.previous:c.next)||
-(b=0);c&&c.focus();return!1;case o:b=c;do b=b.next,!b&&j&&(b=c.toolbar.items[0]);while(b&&!b.focus);b?b.focus():g(c,9);return!1;case 40:return c.button&&c.button.hasArrow?(a.once("panelShow",function(b){b.data._.panel._.currentBlock.onKeyDown(40)}),c.execute()):g(c,40==h?o:e),!1;case e:case 38:b=c;do b=b.previous,!b&&j&&(b=c.toolbar.items[c.toolbar.items.length-1]);while(b&&!b.focus);b?b.focus():(d=1,g(c,CKEDITOR.SHIFT+9),d=0);return!1;case 27:return a.focus(),!1;case 13:case 32:return c.execute(),
-!1}return!0};a.on("uiSpace",function(c){if(c.data.space==a.config.toolbarLocation){c.removeListener();a.toolbox=new u;var d=CKEDITOR.tools.getNextId(),b=['<span id="',d,'" class="cke_voice_label">',a.lang.toolbar.toolbars,"</span>",'<span id="'+a.ui.spaceId("toolbox")+'" class="cke_toolbox" role="group" aria-labelledby="',d,'" onmousedown="return false;">'],d=!1!==a.config.toolbarStartupExpanded,e,j;a.config.toolbarCanCollapse&&a.elementMode!=CKEDITOR.ELEMENT_MODE_INLINE&&b.push('<span class="cke_toolbox_main"'+
-(d?">":' style="display:none">'));for(var o=a.toolbox.toolbars,f=w(a),k=0;k<f.length;k++){var i,l=0,r,m=f[k],s;if(m)if(e&&(b.push("</span>"),j=e=0),"/"===m)b.push('<span class="cke_toolbar_break"></span>');else{s=m.items||m;for(var t=0;t<s.length;t++){var p=s[t],n;if(p)if(p.type==CKEDITOR.UI_SEPARATOR)j=e&&p;else{n=!1!==p.canGroup;if(!l){i=CKEDITOR.tools.getNextId();l={id:i,items:[]};r=m.name&&(a.lang.toolbar.toolbarGroups[m.name]||m.name);b.push('<span id="',i,'" class="cke_toolbar"',r?' aria-labelledby="'+
-i+'_label"':"",' role="toolbar">');r&&b.push('<span id="',i,'_label" class="cke_voice_label">',r,"</span>");b.push('<span class="cke_toolbar_start"></span>');var q=o.push(l)-1;0<q&&(l.previous=o[q-1],l.previous.next=l)}n?e||(b.push('<span class="cke_toolgroup" role="presentation">'),e=1):e&&(b.push("</span>"),e=0);i=function(c){c=c.render(a,b);q=l.items.push(c)-1;if(q>0){c.previous=l.items[q-1];c.previous.next=c}c.toolbar=l;c.onkey=g;c.onfocus=function(){a.toolbox.focusCommandExecuted||a.focus()}};
-j&&(i(j),j=0);i(p)}}e&&(b.push("</span>"),j=e=0);l&&b.push('<span class="cke_toolbar_end"></span></span>')}}a.config.toolbarCanCollapse&&b.push("</span>");if(a.config.toolbarCanCollapse&&a.elementMode!=CKEDITOR.ELEMENT_MODE_INLINE){var v=CKEDITOR.tools.addFunction(function(){a.execCommand("toolbarCollapse")});a.on("destroy",function(){CKEDITOR.tools.removeFunction(v)});a.addCommand("toolbarCollapse",{readOnly:1,exec:function(b){var a=b.ui.space("toolbar_collapser"),c=a.getPrevious(),e=b.ui.space("contents"),
-d=c.getParent(),f=parseInt(e.$.style.height,10),h=d.$.offsetHeight,g=a.hasClass("cke_toolbox_collapser_min");g?(c.show(),a.removeClass("cke_toolbox_collapser_min"),a.setAttribute("title",b.lang.toolbar.toolbarCollapse)):(c.hide(),a.addClass("cke_toolbox_collapser_min"),a.setAttribute("title",b.lang.toolbar.toolbarExpand));a.getFirst().setText(g?"▲":"◀");e.setStyle("height",f-(d.$.offsetHeight-h)+"px");b.fire("resize")},modes:{wysiwyg:1,source:1}});a.setKeystroke(CKEDITOR.ALT+(CKEDITOR.env.ie||CKEDITOR.env.webkit?
-189:109),"toolbarCollapse");b.push('<a title="'+(d?a.lang.toolbar.toolbarCollapse:a.lang.toolbar.toolbarExpand)+'" id="'+a.ui.spaceId("toolbar_collapser")+'" tabIndex="-1" class="cke_toolbox_collapser');d||b.push(" cke_toolbox_collapser_min");b.push('" onclick="CKEDITOR.tools.callFunction('+v+')">','<span class="cke_arrow">&#9650;</span>',"</a>")}b.push("</span>");c.data.html+=b.join("")}});a.on("destroy",function(){if(this.toolbox){var a,d=0,b,e,g;for(a=this.toolbox.toolbars;d<a.length;d++){e=a[d].items;
-for(b=0;b<e.length;b++)g=e[b],g.clickFn&&CKEDITOR.tools.removeFunction(g.clickFn),g.keyDownFn&&CKEDITOR.tools.removeFunction(g.keyDownFn)}}});a.on("uiReady",function(){var c=a.ui.space("toolbox");c&&a.focusManager.add(c,1)});a.addCommand("toolbarFocus",x);a.setKeystroke(CKEDITOR.ALT+121,"toolbarFocus");a.ui.add("-",CKEDITOR.UI_SEPARATOR,{});a.ui.addHandler(CKEDITOR.UI_SEPARATOR,{create:function(){return{render:function(a,d){d.push('<span class="cke_toolbar_separator" role="separator"></span>');return{}}}}})}});
-CKEDITOR.ui.prototype.addToolbarGroup=function(a,d,g){var c=n(this.editor),h=0===d,b={name:a};if(g){if(g=CKEDITOR.tools.search(c,function(a){return a.name==g})){!g.groups&&(g.groups=[]);if(d&&(d=CKEDITOR.tools.indexOf(g.groups,d),0<=d)){g.groups.splice(d+1,0,a);return}h?g.groups.splice(0,0,a):g.groups.push(a);return}d=null}d&&(d=CKEDITOR.tools.indexOf(c,function(a){return a.name==d}));h?c.splice(0,0,a):"number"==typeof d?c.splice(d+1,0,b):c.push(a)}})();CKEDITOR.UI_SEPARATOR="separator";
-CKEDITOR.config.toolbarLocation="top";(function(){function l(e,c,b){b=e.config.forceEnterMode||b;"wysiwyg"==e.mode&&(c||(c=e.activeEnterMode),e.elementPath().isContextFor("p")||(c=CKEDITOR.ENTER_BR,b=1),e.fire("saveSnapshot"),c==CKEDITOR.ENTER_BR?o(e,c,null,b):p(e,c,null,b),e.fire("saveSnapshot"))}function q(e){for(var e=e.getSelection().getRanges(!0),c=e.length-1;0<c;c--)e[c].deleteContents();return e[0]}CKEDITOR.plugins.add("enterkey",{init:function(e){e.addCommand("enter",{modes:{wysiwyg:1},editorFocus:!1,exec:function(c){l(c)}});
-e.addCommand("shiftEnter",{modes:{wysiwyg:1},editorFocus:!1,exec:function(c){l(c,c.activeShiftEnterMode,1)}});e.setKeystroke([[13,"enter"],[CKEDITOR.SHIFT+13,"shiftEnter"]])}});var t=CKEDITOR.dom.walker.whitespaces(),u=CKEDITOR.dom.walker.bookmark();CKEDITOR.plugins.enterkey={enterBlock:function(e,c,b,i){if(b=b||q(e)){var f=b.document,j=b.checkStartOfBlock(),h=b.checkEndOfBlock(),a=e.elementPath(b.startContainer).block,k=c==CKEDITOR.ENTER_DIV?"div":"p",d;if(j&&h){if(a&&(a.is("li")||a.getParent().is("li"))){b=
-a.getParent();d=b.getParent();var i=!a.hasPrevious(),m=!a.hasNext(),k=e.getSelection(),g=k.createBookmarks(),j=a.getDirection(1),h=a.getAttribute("class"),n=a.getAttribute("style"),l=d.getDirection(1)!=j,e=e.enterMode!=CKEDITOR.ENTER_BR||l||n||h;if(d.is("li"))if(i||m)a[i?"insertBefore":"insertAfter"](d);else a.breakParent(d);else{if(e)if(d=f.createElement(c==CKEDITOR.ENTER_P?"p":"div"),l&&d.setAttribute("dir",j),n&&d.setAttribute("style",n),h&&d.setAttribute("class",h),a.moveChildren(d),i||m)d[i?
-"insertBefore":"insertAfter"](b);else a.breakParent(b),d.insertAfter(b);else if(a.appendBogus(!0),i||m)for(;f=a[i?"getFirst":"getLast"]();)f[i?"insertBefore":"insertAfter"](b);else for(a.breakParent(b);f=a.getLast();)f.insertAfter(b);a.remove()}k.selectBookmarks(g);return}if(a&&a.getParent().is("blockquote")){a.breakParent(a.getParent());a.getPrevious().getFirst(CKEDITOR.dom.walker.invisible(1))||a.getPrevious().remove();a.getNext().getFirst(CKEDITOR.dom.walker.invisible(1))||a.getNext().remove();
-b.moveToElementEditStart(a);b.select();return}}else if(a&&a.is("pre")&&!h){o(e,c,b,i);return}if(h=b.splitBlock(k)){c=h.previousBlock;a=h.nextBlock;e=h.wasStartOfBlock;j=h.wasEndOfBlock;if(a)g=a.getParent(),g.is("li")&&(a.breakParent(g),a.move(a.getNext(),1));else if(c&&(g=c.getParent())&&g.is("li"))c.breakParent(g),g=c.getNext(),b.moveToElementEditStart(g),c.move(c.getPrevious());if(!e&&!j)a.is("li")&&(d=b.clone(),d.selectNodeContents(a),d=new CKEDITOR.dom.walker(d),d.evaluator=function(a){return!(u(a)||
-t(a)||a.type==CKEDITOR.NODE_ELEMENT&&a.getName()in CKEDITOR.dtd.$inline&&!(a.getName()in CKEDITOR.dtd.$empty))},(g=d.next())&&(g.type==CKEDITOR.NODE_ELEMENT&&g.is("ul","ol"))&&(CKEDITOR.env.needsBrFiller?f.createElement("br"):f.createText(" ")).insertBefore(g)),a&&b.moveToElementEditStart(a);else{if(c){if(c.is("li")||!r.test(c.getName())&&!c.is("pre"))d=c.clone()}else a&&(d=a.clone());d?i&&!d.is("li")&&d.renameNode(k):g&&g.is("li")?d=g:(d=f.createElement(k),c&&(m=c.getDirection())&&d.setAttribute("dir",
-m));if(f=h.elementPath){i=0;for(k=f.elements.length;i<k;i++){g=f.elements[i];if(g.equals(f.block)||g.equals(f.blockLimit))break;CKEDITOR.dtd.$removeEmpty[g.getName()]&&(g=g.clone(),d.moveChildren(g),d.append(g))}}d.appendBogus();d.getParent()||b.insertNode(d);d.is("li")&&d.removeAttribute("value");if(CKEDITOR.env.ie&&e&&(!j||!c.getChildCount()))b.moveToElementEditStart(j?c:d),b.select();b.moveToElementEditStart(e&&!j?a:d)}b.select();b.scrollIntoView()}}},enterBr:function(e,c,b,i){if(b=b||q(e)){var f=
-b.document,j=b.checkEndOfBlock(),h=new CKEDITOR.dom.elementPath(e.getSelection().getStartElement()),a=h.block,h=a&&h.block.getName();!i&&"li"==h?p(e,c,b,i):(!i&&j&&r.test(h)?(j=a.getDirection())?(f=f.createElement("div"),f.setAttribute("dir",j),f.insertAfter(a),b.setStart(f,0)):(f.createElement("br").insertAfter(a),CKEDITOR.env.gecko&&f.createText("").insertAfter(a),b.setStartAt(a.getNext(),CKEDITOR.env.ie?CKEDITOR.POSITION_BEFORE_START:CKEDITOR.POSITION_AFTER_START)):(a="pre"==h&&CKEDITOR.env.ie&&
-8>CKEDITOR.env.version?f.createText("\r"):f.createElement("br"),b.deleteContents(),b.insertNode(a),CKEDITOR.env.needsBrFiller?(f.createText("").insertAfter(a),j&&a.getParent().appendBogus(),a.getNext().$.nodeValue="",b.setStartAt(a.getNext(),CKEDITOR.POSITION_AFTER_START)):b.setStartAt(a,CKEDITOR.POSITION_AFTER_END)),b.collapse(!0),b.select(),b.scrollIntoView())}}};var s=CKEDITOR.plugins.enterkey,o=s.enterBr,p=s.enterBlock,r=/^h[1-6]$/})();(function(){function j(a,b){var d={},e=[],f={nbsp:" ",shy:"­",gt:">",lt:"<",amp:"&",apos:"'",quot:'"'},a=a.replace(/\b(nbsp|shy|gt|lt|amp|apos|quot)(?:,|$)/g,function(a,h){var c=b?"&"+h+";":f[h];d[c]=b?f[h]:"&"+h+";";e.push(c);return""});if(!b&&a){var a=a.split(","),c=document.createElement("div"),g;c.innerHTML="&"+a.join(";&")+";";g=c.innerHTML;c=null;for(c=0;c<g.length;c++){var i=g.charAt(c);d[i]="&"+a[c]+";";e.push(i)}}d.regex=e.join(b?"|":"");return d}CKEDITOR.plugins.add("entities",{afterInit:function(a){var b=
-a.config;if(a=(a=a.dataProcessor)&&a.htmlFilter){var d=[];!1!==b.basicEntities&&d.push("nbsp,gt,lt,amp");b.entities&&(d.length&&d.push("quot,iexcl,cent,pound,curren,yen,brvbar,sect,uml,copy,ordf,laquo,not,shy,reg,macr,deg,plusmn,sup2,sup3,acute,micro,para,middot,cedil,sup1,ordm,raquo,frac14,frac12,frac34,iquest,times,divide,fnof,bull,hellip,prime,Prime,oline,frasl,weierp,image,real,trade,alefsym,larr,uarr,rarr,darr,harr,crarr,lArr,uArr,rArr,dArr,hArr,forall,part,exist,empty,nabla,isin,notin,ni,prod,sum,minus,lowast,radic,prop,infin,ang,and,or,cap,cup,int,there4,sim,cong,asymp,ne,equiv,le,ge,sub,sup,nsub,sube,supe,oplus,otimes,perp,sdot,lceil,rceil,lfloor,rfloor,lang,rang,loz,spades,clubs,hearts,diams,circ,tilde,ensp,emsp,thinsp,zwnj,zwj,lrm,rlm,ndash,mdash,lsquo,rsquo,sbquo,ldquo,rdquo,bdquo,dagger,Dagger,permil,lsaquo,rsaquo,euro"),
-b.entities_latin&&d.push("Agrave,Aacute,Acirc,Atilde,Auml,Aring,AElig,Ccedil,Egrave,Eacute,Ecirc,Euml,Igrave,Iacute,Icirc,Iuml,ETH,Ntilde,Ograve,Oacute,Ocirc,Otilde,Ouml,Oslash,Ugrave,Uacute,Ucirc,Uuml,Yacute,THORN,szlig,agrave,aacute,acirc,atilde,auml,aring,aelig,ccedil,egrave,eacute,ecirc,euml,igrave,iacute,icirc,iuml,eth,ntilde,ograve,oacute,ocirc,otilde,ouml,oslash,ugrave,uacute,ucirc,uuml,yacute,thorn,yuml,OElig,oelig,Scaron,scaron,Yuml"),b.entities_greek&&d.push("Alpha,Beta,Gamma,Delta,Epsilon,Zeta,Eta,Theta,Iota,Kappa,Lambda,Mu,Nu,Xi,Omicron,Pi,Rho,Sigma,Tau,Upsilon,Phi,Chi,Psi,Omega,alpha,beta,gamma,delta,epsilon,zeta,eta,theta,iota,kappa,lambda,mu,nu,xi,omicron,pi,rho,sigmaf,sigma,tau,upsilon,phi,chi,psi,omega,thetasym,upsih,piv"),
-b.entities_additional&&d.push(b.entities_additional));var e=j(d.join(",")),f=e.regex?"["+e.regex+"]":"a^";delete e.regex;b.entities&&b.entities_processNumerical&&(f="[^ -~]|"+f);var f=RegExp(f,"g"),c=function(a){return b.entities_processNumerical=="force"||!e[a]?"&#"+a.charCodeAt(0)+";":e[a]},g=j("nbsp,gt,lt,amp,shy",!0),i=RegExp(g.regex,"g"),k=function(a){return g[a]};a.addRules({text:function(a){return a.replace(i,k).replace(f,c)}},{applyToAll:!0})}}})})();CKEDITOR.config.basicEntities=!0;
-CKEDITOR.config.entities=!0;CKEDITOR.config.entities_latin=!0;CKEDITOR.config.entities_greek=!0;CKEDITOR.config.entities_additional="#39";(function(){function q(a){var i=a.config,l=a.fire("uiSpace",{space:"top",html:""}).html,o=function(){function f(a,c,e){b.setStyle(c,t(e));b.setStyle("position",a)}function e(a){var b=r.getDocumentPosition();switch(a){case "top":f("absolute","top",b.y-m-n);break;case "pin":f("fixed","top",q);break;case "bottom":f("absolute","top",b.y+(c.height||c.bottom-c.top)+n)}j=a}var j,r,k,c,h,m,s,l=i.floatSpaceDockedOffsetX||0,n=i.floatSpaceDockedOffsetY||0,p=i.floatSpacePinnedOffsetX||0,q=i.floatSpacePinnedOffsetY||
-0;return function(d){if(r=a.editable())if(d&&"focus"==d.name&&b.show(),b.removeStyle("left"),b.removeStyle("right"),k=b.getClientRect(),c=r.getClientRect(),h=g.getViewPaneSize(),m=k.height,s="pageXOffset"in g.$?g.$.pageXOffset:CKEDITOR.document.$.documentElement.scrollLeft,j){m+n<=c.top?e("top"):m+n>h.height-c.bottom?e("pin"):e("bottom");var d=h.width/2,d=0<c.left&&c.right<h.width&&c.width>k.width?"rtl"==a.config.contentsLangDirection?"right":"left":d-c.left>c.right-d?"left":"right",f;k.width>h.width?
-(d="left",f=0):(f="left"==d?0<c.left?c.left:0:c.right<h.width?h.width-c.right:0,f+k.width>h.width&&(d="left"==d?"right":"left",f=0));b.setStyle(d,t(("pin"==j?p:l)+f+("pin"==j?0:"left"==d?s:-s)))}else j="pin",e("pin"),o(d)}}();if(l){var b=CKEDITOR.document.getBody().append(CKEDITOR.dom.element.createFromHtml(u.output({content:l,id:a.id,langDir:a.lang.dir,langCode:a.langCode,name:a.name,style:"display:none;z-index:"+(i.baseFloatZIndex-1),topId:a.ui.spaceId("top"),voiceLabel:a.lang.editorPanel+", "+
-a.name}))),p=CKEDITOR.tools.eventsBuffer(500,o),e=CKEDITOR.tools.eventsBuffer(100,o);b.unselectable();b.on("mousedown",function(a){a=a.data;a.getTarget().hasAscendant("a",1)||a.preventDefault()});a.on("focus",function(b){o(b);a.on("change",p.input);g.on("scroll",e.input);g.on("resize",e.input)});a.on("blur",function(){b.hide();a.removeListener("change",p.input);g.removeListener("scroll",e.input);g.removeListener("resize",e.input)});a.on("destroy",function(){g.removeListener("scroll",e.input);g.removeListener("resize",
-e.input);b.clearCustomData();b.remove()});a.focusManager.hasFocus&&b.show();a.focusManager.add(b,1)}}var u=CKEDITOR.addTemplate("floatcontainer",'<div id="cke_{name}" class="cke {id} cke_reset_all cke_chrome cke_editor_{name} cke_float cke_{langDir} '+CKEDITOR.env.cssClass+'" dir="{langDir}" title="'+(CKEDITOR.env.gecko?" ":"")+'" lang="{langCode}" role="application" style="{style}" aria-labelledby="cke_{name}_arialbl"><span id="cke_{name}_arialbl" class="cke_voice_label">{voiceLabel}</span><div class="cke_inner"><div id="{topId}" class="cke_top" role="presentation">{content}</div></div></div>'),
-g=CKEDITOR.document.getWindow(),t=CKEDITOR.tools.cssLength;CKEDITOR.plugins.add("floatingspace",{init:function(a){a.on("loaded",function(){q(this)},null,null,20)}})})();(function(){var b={canUndo:!1,exec:function(a){var b=a.document.createElement("hr");a.insertElement(b)},allowedContent:"hr",requiredContent:"hr"};CKEDITOR.plugins.add("horizontalrule",{init:function(a){a.blockless||(a.addCommand("horizontalrule",b),a.ui.addButton&&a.ui.addButton("HorizontalRule",{label:a.lang.horizontalrule.toolbar,command:"horizontalrule",toolbar:"insert,40"}))}})})();(function(){function k(a){var d=this.editor,b=a.document,c=b.body;(a=b.getElementById("cke_actscrpt"))&&a.parentNode.removeChild(a);(a=b.getElementById("cke_shimscrpt"))&&a.parentNode.removeChild(a);CKEDITOR.env.gecko&&(c.contentEditable=!1,2E4>CKEDITOR.env.version&&(c.innerHTML=c.innerHTML.replace(/^.*<\!-- cke-content-start --\>/,""),setTimeout(function(){var a=new CKEDITOR.dom.range(new CKEDITOR.dom.document(b));a.setStart(new CKEDITOR.dom.node(c),0);d.getSelection().selectRanges([a])},0)));c.contentEditable=
-!0;CKEDITOR.env.ie&&(c.hideFocus=!0,c.disabled=!0,c.removeAttribute("disabled"));delete this._.isLoadingData;this.$=c;b=new CKEDITOR.dom.document(b);this.setup();CKEDITOR.env.ie&&(b.getDocumentElement().addClass(b.$.compatMode),d.config.enterMode!=CKEDITOR.ENTER_P&&this.attachListener(b,"selectionchange",function(){var a=b.getBody(),c=d.getSelection(),e=c&&c.getRanges()[0];e&&(a.getHtml().match(/^<p>(?:&nbsp;|<br>)<\/p>$/i)&&e.startContainer.equals(a))&&setTimeout(function(){e=d.getSelection().getRanges()[0];
-if(!e.startContainer.equals("body")){a.getFirst().remove(1);e.moveToElementEditEnd(a);e.select()}},0)}));if(CKEDITOR.env.webkit||CKEDITOR.env.ie&&10<CKEDITOR.env.version)b.getDocumentElement().on("mousedown",function(a){a.data.getTarget().is("html")&&setTimeout(function(){d.editable().focus()})});try{d.document.$.execCommand("2D-position",!1,!0)}catch(e){}try{d.document.$.execCommand("enableInlineTableEditing",!1,!d.config.disableNativeTableHandles)}catch(g){}if(d.config.disableObjectResizing)try{this.getDocument().$.execCommand("enableObjectResizing",
-!1,!1)}catch(f){this.attachListener(this,CKEDITOR.env.ie?"resizestart":"resize",function(a){a.data.preventDefault()})}(CKEDITOR.env.gecko||CKEDITOR.env.ie&&"CSS1Compat"==d.document.$.compatMode)&&this.attachListener(this,"keydown",function(a){var b=a.data.getKeystroke();if(b==33||b==34)if(CKEDITOR.env.ie)setTimeout(function(){d.getSelection().scrollIntoView()},0);else if(d.window.$.innerHeight>this.$.offsetHeight){var c=d.createRange();c[b==33?"moveToElementEditStart":"moveToElementEditEnd"](this);
-c.select();a.data.preventDefault()}});CKEDITOR.env.ie&&this.attachListener(b,"blur",function(){try{b.$.selection.empty()}catch(a){}});d.document.getElementsByTag("title").getItem(0).data("cke-title",d.document.$.title);CKEDITOR.env.ie&&(d.document.$.title=this._.docTitle);CKEDITOR.tools.setTimeout(function(){d.fire("contentDom");if(this._.isPendingFocus){d.focus();this._.isPendingFocus=false}setTimeout(function(){d.fire("dataReady")},0);CKEDITOR.env.ie&&setTimeout(function(){if(d.document){var a=
-d.document.$.body;a.runtimeStyle.marginBottom="0px";a.runtimeStyle.marginBottom=""}},1E3)},0,this)}function l(){var a=[];if(8<=CKEDITOR.document.$.documentMode){a.push("html.CSS1Compat [contenteditable=false]{min-height:0 !important}");var d=[],b;for(b in CKEDITOR.dtd.$removeEmpty)d.push("html.CSS1Compat "+b+"[contenteditable=false]");a.push(d.join(",")+"{display:inline-block}")}else CKEDITOR.env.gecko&&(a.push("html{height:100% !important}"),a.push("img:-moz-broken{-moz-force-broken-image-icon:1;min-width:24px;min-height:24px}"));
-a.push("html{cursor:text;*cursor:auto}");a.push("img,input,textarea{cursor:default}");return a.join("\n")}CKEDITOR.plugins.add("wysiwygarea",{init:function(a){a.config.fullPage&&a.addFeature({allowedContent:"html head title; style [media,type]; body (*)[id]; meta link [*]",requiredContent:"body"});a.addMode("wysiwyg",function(d){function b(b){b&&b.removeListener();a.editable(new j(a,e.$.contentWindow.document.body));a.setData(a.getData(1),d)}var c="document.open();"+(CKEDITOR.env.ie?"("+CKEDITOR.tools.fixDomain+
-")();":"")+"document.close();",c=CKEDITOR.env.air?"javascript:void(0)":CKEDITOR.env.ie?"javascript:void(function(){"+encodeURIComponent(c)+"}())":"",e=CKEDITOR.dom.element.createFromHtml('<iframe src="'+c+'" frameBorder="0"></iframe>');e.setStyles({width:"100%",height:"100%"});e.addClass("cke_wysiwyg_frame cke_reset");var g=a.ui.space("contents");g.append(e);if(c=CKEDITOR.env.ie||CKEDITOR.env.gecko)e.on("load",b);var f=a.title,h=a.lang.common.editorHelp;f&&(CKEDITOR.env.ie&&(f+=", "+h),e.setAttribute("title",
-f));var f=CKEDITOR.tools.getNextId(),i=CKEDITOR.dom.element.createFromHtml('<span id="'+f+'" class="cke_voice_label">'+h+"</span>");g.append(i,1);a.on("beforeModeUnload",function(a){a.removeListener();i.remove()});e.setAttributes({"aria-describedby":f,tabIndex:a.tabIndex,allowTransparency:"true"});!c&&b();CKEDITOR.env.webkit&&(c=function(){g.setStyle("width","100%");e.hide();e.setSize("width",g.getSize("width"));g.removeStyle("width");e.show()},e.setCustomData("onResize",c),CKEDITOR.document.getWindow().on("resize",
-c));a.fire("ariaWidget",e)})}});var j=CKEDITOR.tools.createClass({$:function(a){this.base.apply(this,arguments);this._.frameLoadedHandler=CKEDITOR.tools.addFunction(function(a){CKEDITOR.tools.setTimeout(k,0,this,a)},this);this._.docTitle=this.getWindow().getFrame().getAttribute("title")},base:CKEDITOR.editable,proto:{setData:function(a,d){var b=this.editor;if(d)this.setHtml(a),b.fire("dataReady");else{this._.isLoadingData=!0;b._.dataStore={id:1};var c=b.config,e=c.fullPage,g=c.docType,f=CKEDITOR.tools.buildStyleHtml(l()).replace(/<style>/,
-'<style data-cke-temp="1">');e||(f+=CKEDITOR.tools.buildStyleHtml(b.config.contentsCss));var h=c.baseHref?'<base href="'+c.baseHref+'" data-cke-temp="1" />':"";e&&(a=a.replace(/<!DOCTYPE[^>]*>/i,function(a){b.docType=g=a;return""}).replace(/<\?xml\s[^\?]*\?>/i,function(a){b.xmlDeclaration=a;return""}));a=b.dataProcessor.toHtml(a);e?(/<body[\s|>]/.test(a)||(a="<body>"+a),/<html[\s|>]/.test(a)||(a="<html>"+a+"</html>"),/<head[\s|>]/.test(a)?/<title[\s|>]/.test(a)||(a=a.replace(/<head[^>]*>/,"$&<title></title>")):
-a=a.replace(/<html[^>]*>/,"$&<head><title></title></head>"),h&&(a=a.replace(/<head>/,"$&"+h)),a=a.replace(/<\/head\s*>/,f+"$&"),a=g+a):a=c.docType+'<html dir="'+c.contentsLangDirection+'" lang="'+(c.contentsLanguage||b.langCode)+'"><head><title>'+this._.docTitle+"</title>"+h+f+"</head><body"+(c.bodyId?' id="'+c.bodyId+'"':"")+(c.bodyClass?' class="'+c.bodyClass+'"':"")+">"+a+"</body></html>";CKEDITOR.env.gecko&&(a=a.replace(/<body/,'<body contenteditable="true" '),2E4>CKEDITOR.env.version&&(a=a.replace(/<body[^>]*>/,
-"$&<\!-- cke-content-start --\>")));c='<script id="cke_actscrpt" type="text/javascript"'+(CKEDITOR.env.ie?' defer="defer" ':"")+">var wasLoaded=0;function onload(){if(!wasLoaded)window.parent.CKEDITOR.tools.callFunction("+this._.frameLoadedHandler+",window);wasLoaded=1;}"+(CKEDITOR.env.ie?"onload();":'document.addEventListener("DOMContentLoaded", onload, false );')+"<\/script>";CKEDITOR.env.ie&&9>CKEDITOR.env.version&&(c+='<script id="cke_shimscrpt">window.parent.CKEDITOR.tools.enableHtml5Elements(document)<\/script>');
-a=a.replace(/(?=\s*<\/(:?head)>)/,c);this.clearCustomData();this.clearListeners();b.fire("contentDomUnload");var i=this.getDocument();try{i.write(a)}catch(j){setTimeout(function(){i.write(a)},0)}}},getData:function(a){if(a)return this.getHtml();var a=this.editor,d=a.config,b=d.fullPage,c=b&&a.docType,e=b&&a.xmlDeclaration,g=this.getDocument(),b=b?g.getDocumentElement().getOuterHtml():g.getBody().getHtml();CKEDITOR.env.gecko&&d.enterMode!=CKEDITOR.ENTER_BR&&(b=b.replace(/<br>(?=\s*(:?$|<\/body>))/,
-""));b=a.dataProcessor.toDataFormat(b);e&&(b=e+"\n"+b);c&&(b=c+"\n"+b);return b},focus:function(){this._.isLoadingData?this._.isPendingFocus=!0:j.baseProto.focus.call(this)},detach:function(){var a=this.editor,d=a.document,b=a.window.getFrame();j.baseProto.detach.call(this);this.clearCustomData();d.getDocumentElement().clearCustomData();b.clearCustomData();CKEDITOR.tools.removeFunction(this._.frameLoadedHandler);(d=b.removeCustomData("onResize"))&&d.removeListener();a.fire("contentDomUnload");b.remove()}}})})();
-CKEDITOR.config.disableObjectResizing=!1;CKEDITOR.config.disableNativeTableHandles=!0;CKEDITOR.config.disableNativeSpellChecker=!0;CKEDITOR.config.contentsCss=CKEDITOR.basePath+"contents.css";(function(){function k(a,b){var e,f;b.on("refresh",function(a){var b=[i],c;for(c in a.data.states)b.push(a.data.states[c]);this.setState(CKEDITOR.tools.search(b,m)?m:i)},b,null,100);b.on("exec",function(b){e=a.getSelection();f=e.createBookmarks(1);b.data||(b.data={});b.data.done=!1},b,null,0);b.on("exec",function(){a.forceNextSelectionCheck();e.selectBookmarks(f)},b,null,100)}var i=CKEDITOR.TRISTATE_DISABLED,m=CKEDITOR.TRISTATE_OFF;CKEDITOR.plugins.add("indent",{init:function(a){var b=CKEDITOR.plugins.indent.genericDefinition;
-k(a,a.addCommand("indent",new b(!0)));k(a,a.addCommand("outdent",new b));a.ui.addButton&&(a.ui.addButton("Indent",{label:a.lang.indent.indent,command:"indent",directional:!0,toolbar:"indent,20"}),a.ui.addButton("Outdent",{label:a.lang.indent.outdent,command:"outdent",directional:!0,toolbar:"indent,10"}));a.on("dirChanged",function(b){var f=a.createRange(),j=b.data.node;f.setStartBefore(j);f.setEndAfter(j);for(var l=new CKEDITOR.dom.walker(f),c;c=l.next();)if(c.type==CKEDITOR.NODE_ELEMENT)if(!c.equals(j)&&
-c.getDirection()){f.setStartAfter(c);l=new CKEDITOR.dom.walker(f)}else{var d=a.config.indentClasses;if(d)for(var g=b.data.dir=="ltr"?["_rtl",""]:["","_rtl"],h=0;h<d.length;h++)if(c.hasClass(d[h]+g[0])){c.removeClass(d[h]+g[0]);c.addClass(d[h]+g[1])}d=c.getStyle("margin-right");g=c.getStyle("margin-left");d?c.setStyle("margin-left",d):c.removeStyle("margin-left");g?c.setStyle("margin-right",g):c.removeStyle("margin-right")}})}});CKEDITOR.plugins.indent={genericDefinition:function(a){this.isIndent=
-!!a;this.startDisabled=!this.isIndent},specificDefinition:function(a,b,e){this.name=b;this.editor=a;this.jobs={};this.enterBr=a.config.enterMode==CKEDITOR.ENTER_BR;this.isIndent=!!e;this.relatedGlobal=e?"indent":"outdent";this.indentKey=e?9:CKEDITOR.SHIFT+9;this.database={}},registerCommands:function(a,b){a.on("pluginsLoaded",function(){for(var a in b)(function(a,b){var e=a.getCommand(b.relatedGlobal),c;for(c in b.jobs)e.on("exec",function(d){d.data.done||(a.fire("lockSnapshot"),b.execJob(a,c)&&(d.data.done=
-!0),a.fire("unlockSnapshot"),CKEDITOR.dom.element.clearAllMarkers(b.database))},this,null,c),e.on("refresh",function(d){d.data.states||(d.data.states={});d.data.states[b.name+"@"+c]=b.refreshJob(a,c,d.data.path)},this,null,c);a.addFeature(b)})(this,b[a])})}};CKEDITOR.plugins.indent.genericDefinition.prototype={context:"p",exec:function(){}};CKEDITOR.plugins.indent.specificDefinition.prototype={execJob:function(a,b){var e=this.jobs[b];if(e.state!=i)return e.exec.call(this,a)},refreshJob:function(a,
-b,e){b=this.jobs[b];b.state=a.activeFilter.checkFeature(this)?b.refresh.call(this,a,e):i;return b.state},getContext:function(a){return a.contains(this.context)}}})();(function(){function s(e){function g(b){for(var f=d.startContainer,a=d.endContainer;f&&!f.getParent().equals(b);)f=f.getParent();for(;a&&!a.getParent().equals(b);)a=a.getParent();if(!f||!a)return!1;for(var h=f,f=[],c=!1;!c;)h.equals(a)&&(c=!0),f.push(h),h=h.getNext();if(1>f.length)return!1;h=b.getParents(!0);for(a=0;a<h.length;a++)if(h[a].getName&&k[h[a].getName()]){b=h[a];break}for(var h=n.isIndent?1:-1,a=f[0],f=f[f.length-1],c=CKEDITOR.plugins.list.listToArray(b,o),g=c[f.getCustomData("listarray_index")].indent,
-a=a.getCustomData("listarray_index");a<=f.getCustomData("listarray_index");a++)if(c[a].indent+=h,0<h){var l=c[a].parent;c[a].parent=new CKEDITOR.dom.element(l.getName(),l.getDocument())}for(a=f.getCustomData("listarray_index")+1;a<c.length&&c[a].indent>g;a++)c[a].indent+=h;f=CKEDITOR.plugins.list.arrayToList(c,o,null,e.config.enterMode,b.getDirection());if(!n.isIndent){var i;if((i=b.getParent())&&i.is("li"))for(var h=f.listNode.getChildren(),m=[],j,a=h.count()-1;0<=a;a--)(j=h.getItem(a))&&(j.is&&
-j.is("li"))&&m.push(j)}f&&f.listNode.replace(b);if(m&&m.length)for(a=0;a<m.length;a++){for(j=b=m[a];(j=j.getNext())&&j.is&&j.getName()in k;)CKEDITOR.env.needsNbspFiller&&!b.getFirst(t)&&b.append(d.document.createText(" ")),b.append(j);b.insertAfter(i)}f&&e.fire("contentDomInvalidated");return!0}for(var n=this,o=this.database,k=this.context,l=e.getSelection(),l=(l&&l.getRanges()).createIterator(),d;d=l.getNextRange();){for(var b=d.getCommonAncestor();b&&!(b.type==CKEDITOR.NODE_ELEMENT&&k[b.getName()]);)b=
-b.getParent();b||(b=d.startPath().contains(k))&&d.setEndAt(b,CKEDITOR.POSITION_BEFORE_END);if(!b){var c=d.getEnclosedNode();c&&(c.type==CKEDITOR.NODE_ELEMENT&&c.getName()in k)&&(d.setStartAt(c,CKEDITOR.POSITION_AFTER_START),d.setEndAt(c,CKEDITOR.POSITION_BEFORE_END),b=c)}b&&(d.startContainer.type==CKEDITOR.NODE_ELEMENT&&d.startContainer.getName()in k)&&(c=new CKEDITOR.dom.walker(d),c.evaluator=i,d.startContainer=c.next());b&&(d.endContainer.type==CKEDITOR.NODE_ELEMENT&&d.endContainer.getName()in k)&&
-(c=new CKEDITOR.dom.walker(d),c.evaluator=i,d.endContainer=c.previous());if(b)return g(b)}return 0}function p(e,g){g||(g=e.contains(this.context));return g&&e.block&&e.block.equals(g.getFirst(i))}function i(e){return e.type==CKEDITOR.NODE_ELEMENT&&e.is("li")}function t(e){return u(e)&&v(e)}var u=CKEDITOR.dom.walker.whitespaces(!0),v=CKEDITOR.dom.walker.bookmark(!1,!0),q=CKEDITOR.TRISTATE_DISABLED,r=CKEDITOR.TRISTATE_OFF;CKEDITOR.plugins.add("indentlist",{requires:"indent",init:function(e){function g(e,
-g){i.specificDefinition.apply(this,arguments);this.requiredContent=["ul","ol"];e.on("key",function(g){if("wysiwyg"==e.mode&&g.data.keyCode==this.indentKey){var d=this.getContext(e.elementPath());if(d&&(!this.isIndent||!p.call(this,e.elementPath(),d)))e.execCommand(this.relatedGlobal),g.cancel()}},this);this.jobs[this.isIndent?10:30]={refresh:this.isIndent?function(e,d){var b=this.getContext(d),c=p.call(this,d,b);return!b||!this.isIndent||c?q:r}:function(e,d){return!this.getContext(d)||this.isIndent?
-q:r},exec:CKEDITOR.tools.bind(s,this)}}var i=CKEDITOR.plugins.indent;i.registerCommands(e,{indentlist:new g(e,"indentlist",!0),outdentlist:new g(e,"outdentlist")});CKEDITOR.tools.extend(g.prototype,i.specificDefinition.prototype,{context:{ol:1,ul:1}})}})})();(function(){function g(a,b){var c=j.exec(a),d=j.exec(b);if(c){if(!c[2]&&"px"==d[2])return d[1];if("px"==c[2]&&!d[2])return d[1]+"px"}return b}var i=CKEDITOR.htmlParser.cssStyle,h=CKEDITOR.tools.cssLength,j=/^((?:\d*(?:\.\d+))|(?:\d+))(.*)?$/i,l={elements:{$:function(a){var b=a.attributes;if((b=(b=(b=b&&b["data-cke-realelement"])&&new CKEDITOR.htmlParser.fragment.fromHtml(decodeURIComponent(b)))&&b.children[0])&&a.attributes["data-cke-resizable"]){var c=(new i(a)).rules,a=b.attributes,d=c.width,c=
-c.height;d&&(a.width=g(a.width,d));c&&(a.height=g(a.height,c))}return b}}},k=CKEDITOR.plugins.add("fakeobjects",{init:function(a){a.filter.allow("img[!data-cke-realelement,src,alt,title](*){*}","fakeobjects")},afterInit:function(a){(a=(a=a.dataProcessor)&&a.htmlFilter)&&a.addRules(l)}});CKEDITOR.editor.prototype.createFakeElement=function(a,b,c,d){var e=this.lang.fakeobjects,e=e[c]||e.unknown,b={"class":b,"data-cke-realelement":encodeURIComponent(a.getOuterHtml()),"data-cke-real-node-type":a.type,
-alt:e,title:e,align:a.getAttribute("align")||""};CKEDITOR.env.hc||(b.src=CKEDITOR.getUrl(k.path+"images/spacer.gif"));c&&(b["data-cke-real-element-type"]=c);d&&(b["data-cke-resizable"]=d,c=new i,d=a.getAttribute("width"),a=a.getAttribute("height"),d&&(c.rules.width=h(d)),a&&(c.rules.height=h(a)),c.populate(b));return this.document.createElement("img",{attributes:b})};CKEDITOR.editor.prototype.createFakeParserElement=function(a,b,c,d){var e=this.lang.fakeobjects,e=e[c]||e.unknown,f;f=new CKEDITOR.htmlParser.basicWriter;
-a.writeHtml(f);f=f.getHtml();b={"class":b,"data-cke-realelement":encodeURIComponent(f),"data-cke-real-node-type":a.type,alt:e,title:e,align:a.attributes.align||""};CKEDITOR.env.hc||(b.src=CKEDITOR.getUrl(k.path+"images/spacer.gif"));c&&(b["data-cke-real-element-type"]=c);d&&(b["data-cke-resizable"]=d,d=a.attributes,a=new i,c=d.width,d=d.height,void 0!=c&&(a.rules.width=h(c)),void 0!=d&&(a.rules.height=h(d)),a.populate(b));return new CKEDITOR.htmlParser.element("img",b)};CKEDITOR.editor.prototype.restoreRealElement=
-function(a){if(a.data("cke-real-node-type")!=CKEDITOR.NODE_ELEMENT)return null;var b=CKEDITOR.dom.element.createFromHtml(decodeURIComponent(a.data("cke-realelement")),this.document);if(a.data("cke-resizable")){var c=a.getStyle("width"),a=a.getStyle("height");c&&b.setAttribute("width",g(b.getAttribute("width"),c));a&&b.setAttribute("height",g(b.getAttribute("height"),a))}return b}})();CKEDITOR.plugins.add("link",{requires:"dialog,fakeobjects",onLoad:function(){function b(b){return d.replace(/%1/g,"rtl"==b?"right":"left").replace(/%2/g,"cke_contents_"+b)}var a="background:url("+CKEDITOR.getUrl(this.path+"images"+(CKEDITOR.env.hidpi?"/hidpi":"")+"/anchor.png")+") no-repeat %1 center;border:1px dotted #00f;background-size:16px;",d=".%2 a.cke_anchor,.%2 a.cke_anchor_empty,.cke_editable.%2 a[name],.cke_editable.%2 a[data-cke-saved-name]{"+a+"padding-%1:18px;cursor:auto;}"+(CKEDITOR.plugins.link.synAnchorSelector?
-"a.cke_anchor_empty{display:inline-block;}":"")+".%2 img.cke_anchor{"+a+"width:16px;min-height:15px;height:1.15em;vertical-align:"+(CKEDITOR.env.opera?"middle":"text-bottom")+";}";CKEDITOR.addCss(b("ltr")+b("rtl"))},init:function(b){var a="a[!href]";CKEDITOR.dialog.isTabEnabled(b,"link","advanced")&&(a=a.replace("]",",accesskey,charset,dir,id,lang,name,rel,tabindex,title,type]{*}(*)"));CKEDITOR.dialog.isTabEnabled(b,"link","target")&&(a=a.replace("]",",target,onclick]"));b.addCommand("link",new CKEDITOR.dialogCommand("link",
-{allowedContent:a,requiredContent:"a[href]"}));b.addCommand("anchor",new CKEDITOR.dialogCommand("anchor",{allowedContent:"a[!name,id]",requiredContent:"a[name]"}));b.addCommand("unlink",new CKEDITOR.unlinkCommand);b.addCommand("removeAnchor",new CKEDITOR.removeAnchorCommand);b.setKeystroke(CKEDITOR.CTRL+76,"link");b.ui.addButton&&(b.ui.addButton("Link",{label:b.lang.link.toolbar,command:"link",toolbar:"links,10"}),b.ui.addButton("Unlink",{label:b.lang.link.unlink,command:"unlink",toolbar:"links,20"}),
-b.ui.addButton("Anchor",{label:b.lang.link.anchor.toolbar,command:"anchor",toolbar:"links,30"}));CKEDITOR.dialog.add("link",this.path+"dialogs/link.js");CKEDITOR.dialog.add("anchor",this.path+"dialogs/anchor.js");b.on("doubleclick",function(a){var c=CKEDITOR.plugins.link.getSelectedLink(b)||a.data.element;if(!c.isReadOnly())if(c.is("a")){a.data.dialog=c.getAttribute("name")&&(!c.getAttribute("href")||!c.getChildCount())?"anchor":"link";b.getSelection().selectElement(c)}else if(CKEDITOR.plugins.link.tryRestoreFakeAnchor(b,
-c))a.data.dialog="anchor"});b.addMenuItems&&b.addMenuItems({anchor:{label:b.lang.link.anchor.menu,command:"anchor",group:"anchor",order:1},removeAnchor:{label:b.lang.link.anchor.remove,command:"removeAnchor",group:"anchor",order:5},link:{label:b.lang.link.menu,command:"link",group:"link",order:1},unlink:{label:b.lang.link.unlink,command:"unlink",group:"link",order:5}});b.contextMenu&&b.contextMenu.addListener(function(a){if(!a||a.isReadOnly())return null;a=CKEDITOR.plugins.link.tryRestoreFakeAnchor(b,
-a);if(!a&&!(a=CKEDITOR.plugins.link.getSelectedLink(b)))return null;var c={};a.getAttribute("href")&&a.getChildCount()&&(c={link:CKEDITOR.TRISTATE_OFF,unlink:CKEDITOR.TRISTATE_OFF});if(a&&a.hasAttribute("name"))c.anchor=c.removeAnchor=CKEDITOR.TRISTATE_OFF;return c})},afterInit:function(b){var a=b.dataProcessor,d=a&&a.dataFilter,a=a&&a.htmlFilter,c=b._.elementsPath&&b._.elementsPath.filters;d&&d.addRules({elements:{a:function(a){var c=a.attributes;if(!c.name)return null;var d=!a.children.length;if(CKEDITOR.plugins.link.synAnchorSelector){var a=
-d?"cke_anchor_empty":"cke_anchor",e=c["class"];if(c.name&&(!e||0>e.indexOf(a)))c["class"]=(e||"")+" "+a;d&&CKEDITOR.plugins.link.emptyAnchorFix&&(c.contenteditable="false",c["data-cke-editable"]=1)}else if(CKEDITOR.plugins.link.fakeAnchor&&d)return b.createFakeParserElement(a,"cke_anchor","anchor");return null}}});CKEDITOR.plugins.link.emptyAnchorFix&&a&&a.addRules({elements:{a:function(a){delete a.attributes.contenteditable}}});c&&c.push(function(a,c){if("a"==c&&(CKEDITOR.plugins.link.tryRestoreFakeAnchor(b,
-a)||a.getAttribute("name")&&(!a.getAttribute("href")||!a.getChildCount())))return"anchor"})}});
-CKEDITOR.plugins.link={getSelectedLink:function(b){var a=b.getSelection(),d=a.getSelectedElement();return d&&d.is("a")?d:(a=a.getRanges()[0])?(a.shrink(CKEDITOR.SHRINK_TEXT),b.elementPath(a.getCommonAncestor()).contains("a",1)):null},fakeAnchor:CKEDITOR.env.opera||CKEDITOR.env.webkit,synAnchorSelector:CKEDITOR.env.ie&&11>CKEDITOR.env.version,emptyAnchorFix:CKEDITOR.env.ie&&8>CKEDITOR.env.version,tryRestoreFakeAnchor:function(b,a){if(a&&a.data("cke-real-element-type")&&"anchor"==a.data("cke-real-element-type")){var d=
-b.restoreRealElement(a);if(d.data("cke-saved-name"))return d}}};CKEDITOR.unlinkCommand=function(){};
-CKEDITOR.unlinkCommand.prototype={exec:function(b){var a=new CKEDITOR.style({element:"a",type:CKEDITOR.STYLE_INLINE,alwaysRemoveElement:1});b.removeStyle(a)},refresh:function(b,a){var d=a.lastElement&&a.lastElement.getAscendant("a",!0);d&&"a"==d.getName()&&d.getAttribute("href")&&d.getChildCount()?this.setState(CKEDITOR.TRISTATE_OFF):this.setState(CKEDITOR.TRISTATE_DISABLED)},contextSensitive:1,startDisabled:1,requiredContent:"a[href]"};CKEDITOR.removeAnchorCommand=function(){};
-CKEDITOR.removeAnchorCommand.prototype={exec:function(b){var a=b.getSelection(),d=a.createBookmarks(),c;if(a&&(c=a.getSelectedElement())&&(CKEDITOR.plugins.link.fakeAnchor&&!c.getChildCount()?CKEDITOR.plugins.link.tryRestoreFakeAnchor(b,c):c.is("a")))c.remove(1);else if(c=CKEDITOR.plugins.link.getSelectedLink(b))c.hasAttribute("href")?(c.removeAttributes({name:1,"data-cke-saved-name":1}),c.removeClass("cke_anchor")):c.remove(1);a.selectBookmarks(d)},requiredContent:"a[name]"};
-CKEDITOR.tools.extend(CKEDITOR.config,{linkShowAdvancedTab:!0,linkShowTargetTab:!0});(function(){function E(c,k,e){function b(b){if((d=a[b?"getFirst":"getLast"]())&&(!d.is||!d.isBlockBoundary())&&(m=k.root[b?"getPrevious":"getNext"](CKEDITOR.dom.walker.invisible(!0)))&&(!m.is||!m.isBlockBoundary({br:1})))c.document.createElement("br")[b?"insertBefore":"insertAfter"](d)}for(var j=CKEDITOR.plugins.list.listToArray(k.root,e),g=[],h=0;h<k.contents.length;h++){var f=k.contents[h];if((f=f.getAscendant("li",!0))&&!f.getCustomData("list_item_processed"))g.push(f),CKEDITOR.dom.element.setMarker(e,
-f,"list_item_processed",!0)}f=null;for(h=0;h<g.length;h++)f=g[h].getCustomData("listarray_index"),j[f].indent=-1;for(h=f+1;h<j.length;h++)if(j[h].indent>j[h-1].indent+1){g=j[h-1].indent+1-j[h].indent;for(f=j[h].indent;j[h]&&j[h].indent>=f;)j[h].indent+=g,h++;h--}var a=CKEDITOR.plugins.list.arrayToList(j,e,null,c.config.enterMode,k.root.getAttribute("dir")).listNode,d,m;b(!0);b();a.replace(k.root);c.fire("contentDomInvalidated")}function x(c,k){this.name=c;this.context=this.type=k;this.allowedContent=
-k+" li";this.requiredContent=k}function A(c,k,e,b){for(var j,g;j=c[b?"getLast":"getFirst"](F);)(g=j.getDirection(1))!==k.getDirection(1)&&j.setAttribute("dir",g),j.remove(),e?j[b?"insertBefore":"insertAfter"](e):k.append(j,b)}function B(c){var k;(k=function(e){var b=c[e?"getPrevious":"getNext"](q);b&&(b.type==CKEDITOR.NODE_ELEMENT&&b.is(c.getName()))&&(A(c,b,null,!e),c.remove(),c=b)})();k(1)}function C(c){return c.type==CKEDITOR.NODE_ELEMENT&&(c.getName()in CKEDITOR.dtd.$block||c.getName()in CKEDITOR.dtd.$listItem)&&
-CKEDITOR.dtd[c.getName()]["#"]}function y(c,k,e){c.fire("saveSnapshot");e.enlarge(CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS);var b=e.extractContents();k.trim(!1,!0);var j=k.createBookmark(),g=new CKEDITOR.dom.elementPath(k.startContainer),h=g.block,g=g.lastElement.getAscendant("li",1)||h,f=new CKEDITOR.dom.elementPath(e.startContainer),a=f.contains(CKEDITOR.dtd.$listItem),f=f.contains(CKEDITOR.dtd.$list);h?(h=h.getBogus())&&h.remove():f&&(h=f.getPrevious(q))&&v(h)&&h.remove();(h=b.getLast())&&(h.type==
-CKEDITOR.NODE_ELEMENT&&h.is("br"))&&h.remove();(h=k.startContainer.getChild(k.startOffset))?b.insertBefore(h):k.startContainer.append(b);if(a&&(b=w(a)))g.contains(a)?(A(b,a.getParent(),a),b.remove()):g.append(b);for(;e.checkStartOfBlock()&&e.checkEndOfBlock();){f=e.startPath();b=f.block;if(!b)break;b.is("li")&&(g=b.getParent(),b.equals(g.getLast(q))&&b.equals(g.getFirst(q))&&(b=g));e.moveToPosition(b,CKEDITOR.POSITION_BEFORE_START);b.remove()}e=e.clone();b=c.editable();e.setEndAt(b,CKEDITOR.POSITION_BEFORE_END);
-e=new CKEDITOR.dom.walker(e);e.evaluator=function(a){return q(a)&&!v(a)};(e=e.next())&&(e.type==CKEDITOR.NODE_ELEMENT&&e.getName()in CKEDITOR.dtd.$list)&&B(e);k.moveToBookmark(j);k.select();c.fire("saveSnapshot")}function w(c){return(c=c.getLast(q))&&c.type==CKEDITOR.NODE_ELEMENT&&c.getName()in r?c:null}var r={ol:1,ul:1},G=CKEDITOR.dom.walker.whitespaces(),D=CKEDITOR.dom.walker.bookmark(),q=function(c){return!(G(c)||D(c))},v=CKEDITOR.dom.walker.bogus();CKEDITOR.plugins.list={listToArray:function(c,
-k,e,b,j){if(!r[c.getName()])return[];b||(b=0);e||(e=[]);for(var g=0,h=c.getChildCount();g<h;g++){var f=c.getChild(g);f.type==CKEDITOR.NODE_ELEMENT&&f.getName()in CKEDITOR.dtd.$list&&CKEDITOR.plugins.list.listToArray(f,k,e,b+1);if("li"==f.$.nodeName.toLowerCase()){var a={parent:c,indent:b,element:f,contents:[]};j?a.grandparent=j:(a.grandparent=c.getParent(),a.grandparent&&"li"==a.grandparent.$.nodeName.toLowerCase()&&(a.grandparent=a.grandparent.getParent()));k&&CKEDITOR.dom.element.setMarker(k,f,
-"listarray_index",e.length);e.push(a);for(var d=0,m=f.getChildCount(),i;d<m;d++)i=f.getChild(d),i.type==CKEDITOR.NODE_ELEMENT&&r[i.getName()]?CKEDITOR.plugins.list.listToArray(i,k,e,b+1,a.grandparent):a.contents.push(i)}}return e},arrayToList:function(c,k,e,b,j){e||(e=0);if(!c||c.length<e+1)return null;for(var g,h=c[e].parent.getDocument(),f=new CKEDITOR.dom.documentFragment(h),a=null,d=e,m=Math.max(c[e].indent,0),i=null,n,l,p=b==CKEDITOR.ENTER_P?"p":"div";;){var o=c[d];g=o.grandparent;n=o.element.getDirection(1);
-if(o.indent==m){if(!a||c[d].parent.getName()!=a.getName())a=c[d].parent.clone(!1,1),j&&a.setAttribute("dir",j),f.append(a);i=a.append(o.element.clone(0,1));n!=a.getDirection(1)&&i.setAttribute("dir",n);for(g=0;g<o.contents.length;g++)i.append(o.contents[g].clone(1,1));d++}else if(o.indent==Math.max(m,0)+1)o=c[d-1].element.getDirection(1),d=CKEDITOR.plugins.list.arrayToList(c,null,d,b,o!=n?n:null),!i.getChildCount()&&(CKEDITOR.env.needsNbspFiller&&!(7<h.$.documentMode))&&i.append(h.createText(" ")),
-i.append(d.listNode),d=d.nextIndex;else if(-1==o.indent&&!e&&g){r[g.getName()]?(i=o.element.clone(!1,!0),n!=g.getDirection(1)&&i.setAttribute("dir",n)):i=new CKEDITOR.dom.documentFragment(h);var a=g.getDirection(1)!=n,u=o.element,z=u.getAttribute("class"),v=u.getAttribute("style"),w=i.type==CKEDITOR.NODE_DOCUMENT_FRAGMENT&&(b!=CKEDITOR.ENTER_BR||a||v||z),s,x=o.contents.length,t;for(g=0;g<x;g++)if(s=o.contents[g],D(s)&&1<x)w?t=s.clone(1,1):i.append(s.clone(1,1));else if(s.type==CKEDITOR.NODE_ELEMENT&&
-s.isBlockBoundary()){a&&!s.getDirection()&&s.setAttribute("dir",n);l=s;var y=u.getAttribute("style");y&&l.setAttribute("style",y.replace(/([^;])$/,"$1;")+(l.getAttribute("style")||""));z&&s.addClass(z);l=null;t&&(i.append(t),t=null);i.append(s.clone(1,1))}else w?(l||(l=h.createElement(p),i.append(l),a&&l.setAttribute("dir",n)),v&&l.setAttribute("style",v),z&&l.setAttribute("class",z),t&&(l.append(t),t=null),l.append(s.clone(1,1))):i.append(s.clone(1,1));t&&((l||i).append(t),t=null);i.type==CKEDITOR.NODE_DOCUMENT_FRAGMENT&&
-d!=c.length-1&&(CKEDITOR.env.needsBrFiller&&(n=i.getLast())&&(n.type==CKEDITOR.NODE_ELEMENT&&n.is("br"))&&n.remove(),n=i.getLast(q),(!n||!(n.type==CKEDITOR.NODE_ELEMENT&&n.is(CKEDITOR.dtd.$block)))&&i.append(h.createElement("br")));n=i.$.nodeName.toLowerCase();("div"==n||"p"==n)&&i.appendBogus();f.append(i);a=null;d++}else return null;l=null;if(c.length<=d||Math.max(c[d].indent,0)<m)break}if(k)for(c=f.getFirst();c;){if(c.type==CKEDITOR.NODE_ELEMENT&&(CKEDITOR.dom.element.clearMarkers(k,c),c.getName()in
-CKEDITOR.dtd.$listItem&&(e=c,h=j=b=void 0,b=e.getDirection()))){for(j=e.getParent();j&&!(h=j.getDirection());)j=j.getParent();b==h&&e.removeAttribute("dir")}c=c.getNextSourceNode()}return{listNode:f,nextIndex:d}}};var H=/^h[1-6]$/,F=CKEDITOR.dom.walker.nodeType(CKEDITOR.NODE_ELEMENT);x.prototype={exec:function(c){this.refresh(c,c.elementPath());var k=c.config,e=c.getSelection(),b=e&&e.getRanges();if(this.state==CKEDITOR.TRISTATE_OFF){var j=c.editable();if(j.getFirst(q)){var g=1==b.length&&b[0];(k=
-g&&g.getEnclosedNode())&&(k.is&&this.type==k.getName())&&this.setState(CKEDITOR.TRISTATE_ON)}else k.enterMode==CKEDITOR.ENTER_BR?j.appendBogus():b[0].fixBlock(1,k.enterMode==CKEDITOR.ENTER_P?"p":"div"),e.selectRanges(b)}for(var k=e.createBookmarks(!0),j=[],h={},b=b.createIterator(),f=0;(g=b.getNextRange())&&++f;){var a=g.getBoundaryNodes(),d=a.startNode,m=a.endNode;d.type==CKEDITOR.NODE_ELEMENT&&"td"==d.getName()&&g.setStartAt(a.startNode,CKEDITOR.POSITION_AFTER_START);m.type==CKEDITOR.NODE_ELEMENT&&
-"td"==m.getName()&&g.setEndAt(a.endNode,CKEDITOR.POSITION_BEFORE_END);g=g.createIterator();for(g.forceBrBreak=this.state==CKEDITOR.TRISTATE_OFF;a=g.getNextParagraph();)if(!a.getCustomData("list_block")){CKEDITOR.dom.element.setMarker(h,a,"list_block",1);for(var i=c.elementPath(a),d=i.elements,m=0,i=i.blockLimit,n,l=d.length-1;0<=l&&(n=d[l]);l--)if(r[n.getName()]&&i.contains(n)){i.removeCustomData("list_group_object_"+f);(d=n.getCustomData("list_group_object"))?d.contents.push(a):(d={root:n,contents:[a]},
-j.push(d),CKEDITOR.dom.element.setMarker(h,n,"list_group_object",d));m=1;break}m||(m=i,m.getCustomData("list_group_object_"+f)?m.getCustomData("list_group_object_"+f).contents.push(a):(d={root:m,contents:[a]},CKEDITOR.dom.element.setMarker(h,m,"list_group_object_"+f,d),j.push(d)))}}for(n=[];0<j.length;)if(d=j.shift(),this.state==CKEDITOR.TRISTATE_OFF)if(r[d.root.getName()]){b=c;f=d;d=h;g=n;m=CKEDITOR.plugins.list.listToArray(f.root,d);i=[];for(a=0;a<f.contents.length;a++)if(l=f.contents[a],(l=l.getAscendant("li",
-!0))&&!l.getCustomData("list_item_processed"))i.push(l),CKEDITOR.dom.element.setMarker(d,l,"list_item_processed",!0);for(var l=f.root.getDocument(),p=void 0,o=void 0,a=0;a<i.length;a++){var u=i[a].getCustomData("listarray_index"),p=m[u].parent;p.is(this.type)||(o=l.createElement(this.type),p.copyAttributes(o,{start:1,type:1}),o.removeStyle("list-style-type"),m[u].parent=o)}d=CKEDITOR.plugins.list.arrayToList(m,d,null,b.config.enterMode);m=void 0;i=d.listNode.getChildCount();for(a=0;a<i&&(m=d.listNode.getChild(a));a++)m.getName()==
-this.type&&g.push(m);d.listNode.replace(f.root);b.fire("contentDomInvalidated")}else{m=c;a=d;g=n;i=a.contents;b=a.root.getDocument();f=[];1==i.length&&i[0].equals(a.root)&&(d=b.createElement("div"),i[0].moveChildren&&i[0].moveChildren(d),i[0].append(d),i[0]=d);a=a.contents[0].getParent();for(l=0;l<i.length;l++)a=a.getCommonAncestor(i[l].getParent());p=m.config.useComputedState;m=d=void 0;p=void 0===p||p;for(l=0;l<i.length;l++)for(o=i[l];u=o.getParent();){if(u.equals(a)){f.push(o);!m&&o.getDirection()&&
-(m=1);o=o.getDirection(p);null!==d&&(d=d&&d!=o?null:o);break}o=u}if(!(1>f.length)){i=f[f.length-1].getNext();l=b.createElement(this.type);g.push(l);for(p=g=void 0;f.length;)g=f.shift(),p=b.createElement("li"),g.is("pre")||H.test(g.getName())||"false"==g.getAttribute("contenteditable")?g.appendTo(p):(g.copyAttributes(p),d&&g.getDirection()&&(p.removeStyle("direction"),p.removeAttribute("dir")),g.moveChildren(p),g.remove()),p.appendTo(l);d&&m&&l.setAttribute("dir",d);i?l.insertBefore(i):l.appendTo(a)}}else this.state==
-CKEDITOR.TRISTATE_ON&&r[d.root.getName()]&&E.call(this,c,d,h);for(l=0;l<n.length;l++)B(n[l]);CKEDITOR.dom.element.clearAllMarkers(h);e.selectBookmarks(k);c.focus()},refresh:function(c,k){var e=k.contains(r,1),b=k.blockLimit||k.root;e&&b.contains(e)?this.setState(e.is(this.type)?CKEDITOR.TRISTATE_ON:CKEDITOR.TRISTATE_OFF):this.setState(CKEDITOR.TRISTATE_OFF)}};CKEDITOR.plugins.add("list",{requires:"indentlist",init:function(c){c.blockless||(c.addCommand("numberedlist",new x("numberedlist","ol")),c.addCommand("bulletedlist",
-new x("bulletedlist","ul")),c.ui.addButton&&(c.ui.addButton("NumberedList",{label:c.lang.list.numberedlist,command:"numberedlist",directional:!0,toolbar:"list,10"}),c.ui.addButton("BulletedList",{label:c.lang.list.bulletedlist,command:"bulletedlist",directional:!0,toolbar:"list,20"})),c.on("key",function(k){var e=k.data.keyCode;if(c.mode=="wysiwyg"&&e in{8:1,46:1}){var b=c.getSelection().getRanges()[0],j=b&&b.startPath();if(b&&b.collapsed){var j=new CKEDITOR.dom.elementPath(b.startContainer),g=e==
-8,h=c.editable(),f=new CKEDITOR.dom.walker(b.clone());f.evaluator=function(a){return q(a)&&!v(a)};f.guard=function(a,b){return!(b&&a.type==CKEDITOR.NODE_ELEMENT&&a.is("table"))};e=b.clone();if(g){var a,d;if((a=j.contains(r))&&b.checkBoundaryOfElement(a,CKEDITOR.START)&&(a=a.getParent())&&a.is("li")&&(a=w(a))){d=a;a=a.getPrevious(q);e.moveToPosition(a&&v(a)?a:d,CKEDITOR.POSITION_BEFORE_START)}else{f.range.setStartAt(h,CKEDITOR.POSITION_AFTER_START);f.range.setEnd(b.startContainer,b.startOffset);if((a=
-f.previous())&&a.type==CKEDITOR.NODE_ELEMENT&&(a.getName()in r||a.is("li"))){if(!a.is("li")){f.range.selectNodeContents(a);f.reset();f.evaluator=C;a=f.previous()}d=a;e.moveToElementEditEnd(d)}}if(d){y(c,e,b);k.cancel()}else if((e=j.contains(r))&&b.checkBoundaryOfElement(e,CKEDITOR.START)){d=e.getFirst(q);if(b.checkBoundaryOfElement(d,CKEDITOR.START)){a=e.getPrevious(q);if(w(d)){if(a){b.moveToElementEditEnd(a);b.select()}}else c.execCommand("outdent");k.cancel()}}}else if(d=j.contains("li")){f.range.setEndAt(h,
-CKEDITOR.POSITION_BEFORE_END);h=(j=d.getLast(q))&&C(j)?j:d;d=0;if((a=f.next())&&a.type==CKEDITOR.NODE_ELEMENT&&a.getName()in r&&a.equals(j)){d=1;a=f.next()}else b.checkBoundaryOfElement(h,CKEDITOR.END)&&(d=1);if(d&&a){b=b.clone();b.moveToElementEditStart(a);y(c,e,b);k.cancel()}}else{f.range.setEndAt(h,CKEDITOR.POSITION_BEFORE_END);if((a=f.next())&&a.type==CKEDITOR.NODE_ELEMENT&&a.is(r)){a=a.getFirst(q);if(j.block&&b.checkStartOfBlock()&&b.checkEndOfBlock()){j.block.remove();b.moveToElementEditStart(a);
-b.select()}else if(w(a)){b.moveToElementEditStart(a);b.select()}else{b=b.clone();b.moveToElementEditStart(a);y(c,e,b)}k.cancel()}}setTimeout(function(){c.selectionChange(1)})}}}))}})})();CKEDITOR.plugins.add("removeformat",{init:function(a){a.addCommand("removeFormat",CKEDITOR.plugins.removeformat.commands.removeformat);a.ui.addButton&&a.ui.addButton("RemoveFormat",{label:a.lang.removeformat.toolbar,command:"removeFormat",toolbar:"cleanup,10"})}});
-CKEDITOR.plugins.removeformat={commands:{removeformat:{exec:function(a){for(var h=a._.removeFormatRegex||(a._.removeFormatRegex=RegExp("^(?:"+a.config.removeFormatTags.replace(/,/g,"|")+")$","i")),e=a._.removeAttributes||(a._.removeAttributes=a.config.removeFormatAttributes.split(",")),f=CKEDITOR.plugins.removeformat.filter,k=a.getSelection().getRanges(1),l=k.createIterator(),c;c=l.getNextRange();){c.collapsed||c.enlarge(CKEDITOR.ENLARGE_ELEMENT);var i=c.createBookmark(),b=i.startNode,j=i.endNode,
-d=function(b){for(var c=a.elementPath(b),e=c.elements,d=1,g;(g=e[d])&&!g.equals(c.block)&&!g.equals(c.blockLimit);d++)h.test(g.getName())&&f(a,g)&&b.breakParent(g)};d(b);if(j){d(j);for(b=b.getNextSourceNode(!0,CKEDITOR.NODE_ELEMENT);b&&!b.equals(j);)d=b.getNextSourceNode(!1,CKEDITOR.NODE_ELEMENT),!("img"==b.getName()&&b.data("cke-realelement"))&&f(a,b)&&(h.test(b.getName())?b.remove(1):(b.removeAttributes(e),a.fire("removeFormatCleanup",b))),b=d}c.moveToBookmark(i)}a.forceNextSelectionCheck();a.getSelection().selectRanges(k)}}},
-filter:function(a,h){for(var e=a._.removeFormatFilters||[],f=0;f<e.length;f++)if(!1===e[f](h))return!1;return!0}};CKEDITOR.editor.prototype.addRemoveFormatFilter=function(a){this._.removeFormatFilters||(this._.removeFormatFilters=[]);this._.removeFormatFilters.push(a)};CKEDITOR.config.removeFormatTags="b,big,code,del,dfn,em,font,i,ins,kbd,q,s,samp,small,span,strike,strong,sub,sup,tt,u,var";CKEDITOR.config.removeFormatAttributes="class,style,lang,width,height,align,hspace,valign";(function(){function g(a){this.editor=a;this.reset()}CKEDITOR.plugins.add("undo",{init:function(a){function c(a){b.enabled&&!1!==a.data.command.canUndo&&b.save()}function d(){b.enabled=a.readOnly?!1:"wysiwyg"==a.mode;b.onChange()}var b=a.undoManager=new g(a),e=a.addCommand("undo",{exec:function(){b.undo()&&(a.selectionChange(),this.fire("afterUndo"))},startDisabled:!0,canUndo:!1}),f=a.addCommand("redo",{exec:function(){b.redo()&&(a.selectionChange(),this.fire("afterRedo"))},startDisabled:!0,canUndo:!1});
-a.setKeystroke([[CKEDITOR.CTRL+90,"undo"],[CKEDITOR.CTRL+89,"redo"],[CKEDITOR.CTRL+CKEDITOR.SHIFT+90,"redo"]]);b.onChange=function(){e.setState(b.undoable()?CKEDITOR.TRISTATE_OFF:CKEDITOR.TRISTATE_DISABLED);f.setState(b.redoable()?CKEDITOR.TRISTATE_OFF:CKEDITOR.TRISTATE_DISABLED)};a.on("beforeCommandExec",c);a.on("afterCommandExec",c);a.on("saveSnapshot",function(a){b.save(a.data&&a.data.contentOnly)});a.on("contentDom",function(){a.editable().on("keydown",function(a){a=a.data.getKey();(8==a||46==
-a)&&b.type(a,0)});a.editable().on("keypress",function(a){b.type(a.data.getKey(),1)})});a.on("beforeModeUnload",function(){"wysiwyg"==a.mode&&b.save(!0)});a.on("mode",d);a.on("readOnly",d);a.ui.addButton&&(a.ui.addButton("Undo",{label:a.lang.undo.undo,command:"undo",toolbar:"undo,10"}),a.ui.addButton("Redo",{label:a.lang.undo.redo,command:"redo",toolbar:"undo,20"}));a.resetUndo=function(){b.reset();a.fire("saveSnapshot")};a.on("updateSnapshot",function(){b.currentImage&&b.update()});a.on("lockSnapshot",
-function(a){b.lock(a.data&&a.data.dontUpdate)});a.on("unlockSnapshot",b.unlock,b)}});CKEDITOR.plugins.undo={};var f=CKEDITOR.plugins.undo.Image=function(a,c){this.editor=a;a.fire("beforeUndoImage");var d=a.getSnapshot();CKEDITOR.env.ie&&d&&(d=d.replace(/\s+data-cke-expando=".*?"/g,""));this.contents=d;c||(this.bookmarks=(d=d&&a.getSelection())&&d.createBookmarks2(!0));a.fire("afterUndoImage")},h=/\b(?:href|src|name)="[^"]*?"/gi;f.prototype={equalsContent:function(a){var c=this.contents,a=a.contents;
-if(CKEDITOR.env.ie&&(CKEDITOR.env.ie7Compat||CKEDITOR.env.ie6Compat))c=c.replace(h,""),a=a.replace(h,"");return c!=a?!1:!0},equalsSelection:function(a){var c=this.bookmarks,a=a.bookmarks;if(c||a){if(!c||!a||c.length!=a.length)return!1;for(var d=0;d<c.length;d++){var b=c[d],e=a[d];if(b.startOffset!=e.startOffset||b.endOffset!=e.endOffset||!CKEDITOR.tools.arrayCompare(b.start,e.start)||!CKEDITOR.tools.arrayCompare(b.end,e.end))return!1}}return!0}};g.prototype={type:function(a,c){var d=!c&&a!=this.lastKeystroke,
-b=this.editor;if(!this.typing||c&&!this.wasCharacter||d){var e=new f(b),g=this.snapshots.length;CKEDITOR.tools.setTimeout(function(){var a=b.getSnapshot();CKEDITOR.env.ie&&(a=a.replace(/\s+data-cke-expando=".*?"/g,""));e.contents!=a&&g==this.snapshots.length&&(this.typing=!0,this.save(!1,e,!1)||this.snapshots.splice(this.index+1,this.snapshots.length-this.index-1),this.hasUndo=!0,this.hasRedo=!1,this.modifiersCount=this.typesCount=1,this.onChange())},0,this)}this.lastKeystroke=a;(this.wasCharacter=
-c)?(this.modifiersCount=0,this.typesCount++,25<this.typesCount?(this.save(!1,null,!1),this.typesCount=1):setTimeout(function(){b.fire("change")},0)):(this.typesCount=0,this.modifiersCount++,25<this.modifiersCount?(this.save(!1,null,!1),this.modifiersCount=1):setTimeout(function(){b.fire("change")},0))},reset:function(){this.lastKeystroke=0;this.snapshots=[];this.index=-1;this.limit=this.editor.config.undoStackSize||20;this.currentImage=null;this.hasRedo=this.hasUndo=!1;this.locked=null;this.resetType()},
-resetType:function(){this.typing=!1;delete this.lastKeystroke;this.modifiersCount=this.typesCount=0},fireChange:function(){this.hasUndo=!!this.getNextImage(!0);this.hasRedo=!!this.getNextImage(!1);this.resetType();this.onChange()},save:function(a,c,d){if(this.locked)return!1;var b=this.snapshots;c||(c=new f(this.editor));if(!1===c.contents)return!1;if(this.currentImage)if(c.equalsContent(this.currentImage)){if(a||c.equalsSelection(this.currentImage))return!1}else this.editor.fire("change");b.splice(this.index+
-1,b.length-this.index-1);b.length==this.limit&&b.shift();this.index=b.push(c)-1;this.currentImage=c;!1!==d&&this.fireChange();return!0},restoreImage:function(a){var c=this.editor,d;a.bookmarks&&(c.focus(),d=c.getSelection());this.locked=1;this.editor.loadSnapshot(a.contents);a.bookmarks?d.selectBookmarks(a.bookmarks):CKEDITOR.env.ie&&(d=this.editor.document.getBody().$.createTextRange(),d.collapse(!0),d.select());this.locked=0;this.index=a.index;this.currentImage=this.snapshots[this.index];this.update();
-this.fireChange();c.fire("change")},getNextImage:function(a){var c=this.snapshots,d=this.currentImage,b;if(d)if(a)for(b=this.index-1;0<=b;b--){if(a=c[b],!d.equalsContent(a))return a.index=b,a}else for(b=this.index+1;b<c.length;b++)if(a=c[b],!d.equalsContent(a))return a.index=b,a;return null},redoable:function(){return this.enabled&&this.hasRedo},undoable:function(){return this.enabled&&this.hasUndo},undo:function(){if(this.undoable()){this.save(!0);var a=this.getNextImage(!0);if(a)return this.restoreImage(a),
-!0}return!1},redo:function(){if(this.redoable()&&(this.save(!0),this.redoable())){var a=this.getNextImage(!1);if(a)return this.restoreImage(a),!0}return!1},update:function(a){if(!this.locked){a||(a=new f(this.editor));for(var c=this.index,d=this.snapshots;0<c&&this.currentImage.equalsContent(d[c-1]);)c-=1;d.splice(c,this.index-c+1,a);this.index=c;this.currentImage=a}},lock:function(a){this.locked?this.locked.level++:a?this.locked={level:1}:(a=new f(this.editor,!0),this.locked={update:this.currentImage&&
-this.currentImage.equalsContent(a)?a:null,level:1})},unlock:function(){if(this.locked&&!--this.locked.level){var a=this.locked.update,c=a&&new f(this.editor,!0);this.locked=null;a&&!a.equalsContent(c)&&this.update()}}}})();CKEDITOR.plugins.add("resize",{init:function(b){var f,g,n,o,a=b.config,q=b.ui.spaceId("resizer"),h=b.element?b.element.getDirection(1):"ltr";!a.resize_dir&&(a.resize_dir="vertical");void 0==a.resize_maxWidth&&(a.resize_maxWidth=3E3);void 0==a.resize_maxHeight&&(a.resize_maxHeight=3E3);void 0==a.resize_minWidth&&(a.resize_minWidth=750);void 0==a.resize_minHeight&&(a.resize_minHeight=250);if(!1!==a.resize_enabled){var c=null,i=("both"==a.resize_dir||"horizontal"==a.resize_dir)&&a.resize_minWidth!=a.resize_maxWidth,
-l=("both"==a.resize_dir||"vertical"==a.resize_dir)&&a.resize_minHeight!=a.resize_maxHeight,j=function(d){var e=f,m=g,c=e+(d.data.$.screenX-n)*("rtl"==h?-1:1),d=m+(d.data.$.screenY-o);i&&(e=Math.max(a.resize_minWidth,Math.min(c,a.resize_maxWidth)));l&&(m=Math.max(a.resize_minHeight,Math.min(d,a.resize_maxHeight)));b.resize(i?e:null,m)},k=function(){CKEDITOR.document.removeListener("mousemove",j);CKEDITOR.document.removeListener("mouseup",k);b.document&&(b.document.removeListener("mousemove",j),b.document.removeListener("mouseup",
-k))},p=CKEDITOR.tools.addFunction(function(d){c||(c=b.getResizable());f=c.$.offsetWidth||0;g=c.$.offsetHeight||0;n=d.screenX;o=d.screenY;a.resize_minWidth>f&&(a.resize_minWidth=f);a.resize_minHeight>g&&(a.resize_minHeight=g);CKEDITOR.document.on("mousemove",j);CKEDITOR.document.on("mouseup",k);b.document&&(b.document.on("mousemove",j),b.document.on("mouseup",k));d.preventDefault&&d.preventDefault()});b.on("destroy",function(){CKEDITOR.tools.removeFunction(p)});b.on("uiSpace",function(a){if("bottom"==
-a.data.space){var e="";i&&!l&&(e=" cke_resizer_horizontal");!i&&l&&(e=" cke_resizer_vertical");var c='<span id="'+q+'" class="cke_resizer'+e+" cke_resizer_"+h+'" title="'+CKEDITOR.tools.htmlEncode(b.lang.common.resize)+'" onmousedown="CKEDITOR.tools.callFunction('+p+', event)">'+("ltr"==h?"◢":"◣")+"</span>";"ltr"==h&&"ltr"==e?a.data.html+=c:a.data.html=c+a.data.html}},b,null,100);b.on("maximize",function(a){b.ui.space("resizer")[a.data==CKEDITOR.TRISTATE_ON?"hide":"show"]()})}}});CKEDITOR.config.plugins='dialogui,dialog,about,basicstyles,clipboard,button,toolbar,enterkey,entities,floatingspace,horizontalrule,wysiwygarea,indent,indentlist,fakeobjects,link,list,removeformat,undo,resize';CKEDITOR.config.skin='moono';(function() {var setIcons = function(icons, strip) {var path = CKEDITOR.getUrl( 'plugins/' + strip );icons = icons.split( ',' );for ( var i = 0; i < icons.length; i++ )CKEDITOR.skin.icons[ icons[ i ] ] = { path: path, offset: -icons[ ++i ], bgsize : icons[ ++i ] };};if (CKEDITOR.env.hidpi) setIcons('about,0,,bold,24,,italic,48,,strike,72,,subscript,96,,superscript,120,,underline,144,,copy-rtl,168,,copy,192,,cut-rtl,216,,cut,240,,paste-rtl,264,,paste,288,,horizontalrule,312,,indent-rtl,336,,indent,360,,outdent-rtl,384,,outdent,408,,anchor-rtl,432,,anchor,456,,link,480,,unlink,504,,bulletedlist-rtl,528,,bulletedlist,552,,numberedlist-rtl,576,,numberedlist,600,,removeformat,624,,redo-rtl,648,,redo,672,,undo-rtl,696,,undo,720,','icons_hidpi.png');else setIcons('about,0,auto,bold,24,auto,italic,48,auto,strike,72,auto,subscript,96,auto,superscript,120,auto,underline,144,auto,copy-rtl,168,auto,copy,192,auto,cut-rtl,216,auto,cut,240,auto,paste-rtl,264,auto,paste,288,auto,horizontalrule,312,auto,indent-rtl,336,auto,indent,360,auto,outdent-rtl,384,auto,outdent,408,auto,anchor-rtl,432,auto,anchor,456,auto,link,480,auto,unlink,504,auto,bulletedlist-rtl,528,auto,bulletedlist,552,auto,numberedlist-rtl,576,auto,numberedlist,600,auto,removeformat,624,auto,redo-rtl,648,auto,redo,672,auto,undo-rtl,696,auto,undo,720,auto','icons.png');})();CKEDITOR.lang.languages={"en":1,"de":1};}());
\ No newline at end of file
+CKEDITOR.config.coreStyles_superscript={element:"sup"};CKEDITOR.plugins.add("notification",{init:function(b){function a(b){var a=new CKEDITOR.dom.element("div");a.setStyles({position:"fixed","margin-left":"-9999px"});a.setAttributes({"aria-live":"assertive","aria-atomic":"true"});a.setText(b);CKEDITOR.document.getBody().append(a);setTimeout(function(){a.remove()},100)}b._.notificationArea=new Area(b);b.showNotification=function(a,d,e){var f,l;"progress"==d?f=e:l=e;a=new CKEDITOR.plugins.notification(b,{message:a,type:d,progress:f,duration:l});a.show();
+return a};b.on("key",function(c){if(27==c.data.keyCode){var d=b._.notificationArea.notifications;d.length&&(a(b.lang.notification.closed),d[d.length-1].hide(),c.cancel())}})}});function Notification(b,a){CKEDITOR.tools.extend(this,a,{editor:b,id:"cke-"+CKEDITOR.tools.getUniqueId(),area:b._.notificationArea});a.type||(this.type="info");this.element=this._createElement();b.plugins.clipboard&&CKEDITOR.plugins.clipboard.preventDefaultDropOnElement(this.element)}
+Notification.prototype={show:function(){!1!==this.editor.fire("notificationShow",{notification:this})&&(this.area.add(this),this._hideAfterTimeout())},update:function(b){var a=!0;!1===this.editor.fire("notificationUpdate",{notification:this,options:b})&&(a=!1);var c=this.element,d=c.findOne(".cke_notification_message"),e=c.findOne(".cke_notification_progress"),f=b.type;c.removeAttribute("role");b.progress&&"progress"!=this.type&&(f="progress");f&&(c.removeClass(this._getClass()),c.removeAttribute("aria-label"),
+this.type=f,c.addClass(this._getClass()),c.setAttribute("aria-label",this.type),"progress"!=this.type||e?"progress"!=this.type&&e&&e.remove():(e=this._createProgressElement(),e.insertBefore(d)));void 0!==b.message&&(this.message=b.message,d.setHtml(this.message));void 0!==b.progress&&(this.progress=b.progress,e&&e.setStyle("width",this._getPercentageProgress()));a&&b.important&&(c.setAttribute("role","alert"),this.isVisible()||this.area.add(this));this.duration=b.duration;this._hideAfterTimeout()},
+hide:function(){!1!==this.editor.fire("notificationHide",{notification:this})&&this.area.remove(this)},isVisible:function(){return 0<=CKEDITOR.tools.indexOf(this.area.notifications,this)},_createElement:function(){var b=this,a,c,d=this.editor.lang.common.close;a=new CKEDITOR.dom.element("div");a.addClass("cke_notification");a.addClass(this._getClass());a.setAttributes({id:this.id,role:"alert","aria-label":this.type});"progress"==this.type&&a.append(this._createProgressElement());c=new CKEDITOR.dom.element("p");
+c.addClass("cke_notification_message");c.setHtml(this.message);a.append(c);c=CKEDITOR.dom.element.createFromHtml('\x3ca class\x3d"cke_notification_close" href\x3d"javascript:void(0)" title\x3d"'+d+'" role\x3d"button" tabindex\x3d"-1"\x3e\x3cspan class\x3d"cke_label"\x3eX\x3c/span\x3e\x3c/a\x3e');a.append(c);c.on("click",function(){b.editor.focus();b.hide()});return a},_getClass:function(){return"progress"==this.type?"cke_notification_info":"cke_notification_"+this.type},_createProgressElement:function(){var b=
+new CKEDITOR.dom.element("span");b.addClass("cke_notification_progress");b.setStyle("width",this._getPercentageProgress());return b},_getPercentageProgress:function(){return Math.round(100*(this.progress||0))+"%"},_hideAfterTimeout:function(){var b=this,a;this._hideTimeoutId&&clearTimeout(this._hideTimeoutId);if("number"==typeof this.duration)a=this.duration;else if("info"==this.type||"success"==this.type)a="number"==typeof this.editor.config.notification_duration?this.editor.config.notification_duration:
+5E3;a&&(b._hideTimeoutId=setTimeout(function(){b.hide()},a))}};function Area(b){var a=this;this.editor=b;this.notifications=[];this.element=this._createElement();this._uiBuffer=CKEDITOR.tools.eventsBuffer(10,this._layout,this);this._changeBuffer=CKEDITOR.tools.eventsBuffer(500,this._layout,this);b.on("destroy",function(){a._removeListeners();a.element.remove()})}
+Area.prototype={add:function(b){this.notifications.push(b);this.element.append(b.element);1==this.element.getChildCount()&&(CKEDITOR.document.getBody().append(this.element),this._attachListeners());this._layout()},remove:function(b){var a=CKEDITOR.tools.indexOf(this.notifications,b);0>a||(this.notifications.splice(a,1),b.element.remove(),this.element.getChildCount()||(this._removeListeners(),this.element.remove()))},_createElement:function(){var b=this.editor,a=b.config,c=new CKEDITOR.dom.element("div");
+c.addClass("cke_notifications_area");c.setAttribute("id","cke_notifications_area_"+b.name);c.setStyle("z-index",a.baseFloatZIndex-2);return c},_attachListeners:function(){var b=CKEDITOR.document.getWindow(),a=this.editor;b.on("scroll",this._uiBuffer.input);b.on("resize",this._uiBuffer.input);a.on("change",this._changeBuffer.input);a.on("floatingSpaceLayout",this._layout,this,null,20);a.on("blur",this._layout,this,null,20)},_removeListeners:function(){var b=CKEDITOR.document.getWindow(),a=this.editor;
+b.removeListener("scroll",this._uiBuffer.input);b.removeListener("resize",this._uiBuffer.input);a.removeListener("change",this._changeBuffer.input);a.removeListener("floatingSpaceLayout",this._layout);a.removeListener("blur",this._layout)},_layout:function(){function b(){a.setStyle("left",k(n+d.width-g-h))}var a=this.element,c=this.editor,d=c.ui.contentsElement.getClientRect(),e=c.ui.contentsElement.getDocumentPosition(),f,l,r=a.getClientRect(),m,g=this._notificationWidth,h=this._notificationMargin;
+m=CKEDITOR.document.getWindow();var p=m.getScrollPosition(),q=m.getViewPaneSize(),t=CKEDITOR.document.getBody(),u=t.getDocumentPosition(),k=CKEDITOR.tools.cssLength;g&&h||(m=this.element.getChild(0),g=this._notificationWidth=m.getClientRect().width,h=this._notificationMargin=parseInt(m.getComputedStyle("margin-left"),10)+parseInt(m.getComputedStyle("margin-right"),10));c.toolbar&&(f=c.ui.space("top"),l=f.getClientRect());f&&f.isVisible()&&l.bottom>d.top&&l.bottom<d.bottom-r.height?a.setStyles({position:"fixed",
+top:k(l.bottom)}):0<d.top?a.setStyles({position:"absolute",top:k(e.y)}):e.y+d.height-r.height>p.y?a.setStyles({position:"fixed",top:0}):a.setStyles({position:"absolute",top:k(e.y+d.height-r.height)});var n="fixed"==a.getStyle("position")?d.left:"static"!=t.getComputedStyle("position")?e.x-u.x:e.x;d.width<g+h?e.x+g+h>p.x+q.width?b():a.setStyle("left",k(n)):e.x+g+h>p.x+q.width?a.setStyle("left",k(n)):e.x+d.width/2+g/2+h>p.x+q.width?a.setStyle("left",k(n-e.x+p.x+q.width-g-h)):0>d.left+d.width-g-h?b():
+0>d.left+d.width/2-g/2?a.setStyle("left",k(n-e.x+p.x)):a.setStyle("left",k(n+d.width/2-g/2-h/2))}};CKEDITOR.plugins.notification=Notification;(function(){var c='\x3ca id\x3d"{id}" class\x3d"cke_button cke_button__{name} cke_button_{state} {cls}"'+(CKEDITOR.env.gecko&&!CKEDITOR.env.hc?"":" href\x3d\"javascript:void('{titleJs}')\"")+' title\x3d"{title}" tabindex\x3d"-1" hidefocus\x3d"true" role\x3d"button" aria-labelledby\x3d"{id}_label" aria-describedby\x3d"{id}_description" aria-haspopup\x3d"{hasArrow}" aria-disabled\x3d"{ariaDisabled}"';CKEDITOR.env.gecko&&CKEDITOR.env.mac&&(c+=' onkeypress\x3d"return false;"');CKEDITOR.env.gecko&&(c+=
+' onblur\x3d"this.style.cssText \x3d this.style.cssText;"');var c=c+(' onkeydown\x3d"return CKEDITOR.tools.callFunction({keydownFn},event);" onfocus\x3d"return CKEDITOR.tools.callFunction({focusFn},event);" '+(CKEDITOR.env.ie?'onclick\x3d"return false;" onmouseup':"onclick")+'\x3d"CKEDITOR.tools.callFunction({clickFn},this);return false;"\x3e\x3cspan class\x3d"cke_button_icon cke_button__{iconName}_icon" style\x3d"{style}"'),c=c+'\x3e\x26nbsp;\x3c/span\x3e\x3cspan id\x3d"{id}_label" class\x3d"cke_button_label cke_button__{name}_label" aria-hidden\x3d"false"\x3e{label}\x3c/span\x3e\x3cspan id\x3d"{id}_description" class\x3d"cke_button_label" aria-hidden\x3d"false"\x3e{ariaShortcut}\x3c/span\x3e{arrowHtml}\x3c/a\x3e',
+t=CKEDITOR.addTemplate("buttonArrow",'\x3cspan class\x3d"cke_button_arrow"\x3e'+(CKEDITOR.env.hc?"\x26#9660;":"")+"\x3c/span\x3e"),u=CKEDITOR.addTemplate("button",c);CKEDITOR.plugins.add("button",{beforeInit:function(a){a.ui.addHandler(CKEDITOR.UI_BUTTON,CKEDITOR.ui.button.handler)}});CKEDITOR.UI_BUTTON="button";CKEDITOR.ui.button=function(a){CKEDITOR.tools.extend(this,a,{title:a.label,click:a.click||function(b){b.execCommand(a.command)}});this._={}};CKEDITOR.ui.button.handler={create:function(a){return new CKEDITOR.ui.button(a)}};
+CKEDITOR.ui.button.prototype={render:function(a,b){function c(){var f=a.mode;f&&(f=this.modes[f]?void 0!==m[f]?m[f]:CKEDITOR.TRISTATE_OFF:CKEDITOR.TRISTATE_DISABLED,f=a.readOnly&&!this.readOnly?CKEDITOR.TRISTATE_DISABLED:f,this.setState(f),this.refresh&&this.refresh())}var n=CKEDITOR.env,p=this._.id=CKEDITOR.tools.getNextId(),g="",d=this.command,q,k,h;this._.editor=a;var e={id:p,button:this,editor:a,focus:function(){CKEDITOR.document.getById(p).focus()},execute:function(){this.button.click(a)},attach:function(a){this.button.attach(a)}},
+v=CKEDITOR.tools.addFunction(function(a){if(e.onkey)return a=new CKEDITOR.dom.event(a),!1!==e.onkey(e,a.getKeystroke())}),w=CKEDITOR.tools.addFunction(function(a){var b;e.onfocus&&(b=!1!==e.onfocus(e,new CKEDITOR.dom.event(a)));return b}),r=0;e.clickFn=q=CKEDITOR.tools.addFunction(function(){r&&(a.unlockSelection(1),r=0);e.execute();n.iOS&&a.focus()});if(this.modes){var m={};a.on("beforeModeUnload",function(){a.mode&&this._.state!=CKEDITOR.TRISTATE_DISABLED&&(m[a.mode]=this._.state)},this);a.on("activeFilterChange",
+c,this);a.on("mode",c,this);!this.readOnly&&a.on("readOnly",c,this)}else d&&(d=a.getCommand(d))&&(d.on("state",function(){this.setState(d.state)},this),g+=d.state==CKEDITOR.TRISTATE_ON?"on":d.state==CKEDITOR.TRISTATE_DISABLED?"disabled":"off");if(this.directional)a.on("contentDirChanged",function(b){var c=CKEDITOR.document.getById(this._.id),d=c.getFirst();b=b.data;b!=a.lang.dir?c.addClass("cke_"+b):c.removeClass("cke_ltr").removeClass("cke_rtl");d.setAttribute("style",CKEDITOR.skin.getIconStyle(l,
+"rtl"==b,this.icon,this.iconOffset))},this);d?(k=a.getCommandKeystroke(d))&&(h=CKEDITOR.tools.keystrokeToString(a.lang.common.keyboard,k)):g+="off";var l=k=this.name||this.command;this.icon&&!/\./.test(this.icon)&&(l=this.icon,this.icon=null);g={id:p,name:k,iconName:l,label:this.label,cls:this.className||"",state:g,ariaDisabled:"disabled"==g?"true":"false",title:this.title+(h?" ("+h.display+")":""),ariaShortcut:h?a.lang.common.keyboardShortcut+" "+h.aria:"",titleJs:n.gecko&&!n.hc?"":(this.title||
+"").replace("'",""),hasArrow:this.hasArrow?"true":"false",keydownFn:v,focusFn:w,clickFn:q,style:CKEDITOR.skin.getIconStyle(l,"rtl"==a.lang.dir,this.icon,this.iconOffset),arrowHtml:this.hasArrow?t.output():""};u.output(g,b);if(this.onRender)this.onRender();return e},setState:function(a){if(this._.state==a)return!1;this._.state=a;var b=CKEDITOR.document.getById(this._.id);return b?(b.setState(a,"cke_button"),a==CKEDITOR.TRISTATE_DISABLED?b.setAttribute("aria-disabled",!0):b.removeAttribute("aria-disabled"),
+this.hasArrow?(a=a==CKEDITOR.TRISTATE_ON?this._.editor.lang.button.selectedLabel.replace(/%1/g,this.label):this.label,CKEDITOR.document.getById(this._.id+"_label").setText(a)):a==CKEDITOR.TRISTATE_ON?b.setAttribute("aria-pressed",!0):b.removeAttribute("aria-pressed"),!0):!1},getState:function(){return this._.state},toFeature:function(a){if(this._.feature)return this._.feature;var b=this;this.allowedContent||this.requiredContent||!this.command||(b=a.getCommand(this.command)||b);return this._.feature=
+b}};CKEDITOR.ui.prototype.addButton=function(a,b){this.add(a,CKEDITOR.UI_BUTTON,b)}})();(function(){function D(a){function d(){for(var b=f(),e=CKEDITOR.tools.clone(a.config.toolbarGroups)||v(a),n=0;n<e.length;n++){var g=e[n];if("/"!=g){"string"==typeof g&&(g=e[n]={name:g});var l,d=g.groups;if(d)for(var h=0;h<d.length;h++)l=d[h],(l=b[l])&&c(g,l);(l=b[g.name])&&c(g,l)}}return e}function f(){var b={},c,e,g;for(c in a.ui.items)e=a.ui.items[c],g=e.toolbar||"others",g=g.split(","),e=g[0],g=parseInt(g[1]||-1,10),b[e]||(b[e]=[]),b[e].push({name:c,order:g});for(e in b)b[e]=b[e].sort(function(b,
+a){return b.order==a.order?0:0>a.order?-1:0>b.order?1:b.order<a.order?-1:1});return b}function c(c,e){if(e.length){c.items?c.items.push(a.ui.create("-")):c.items=[];for(var d;d=e.shift();)d="string"==typeof d?d:d.name,b&&-1!=CKEDITOR.tools.indexOf(b,d)||(d=a.ui.create(d))&&a.addFeature(d)&&c.items.push(d)}}function h(b){var a=[],e,d,h;for(e=0;e<b.length;++e)d=b[e],h={},"/"==d?a.push(d):CKEDITOR.tools.isArray(d)?(c(h,CKEDITOR.tools.clone(d)),a.push(h)):d.items&&(c(h,CKEDITOR.tools.clone(d.items)),
+h.name=d.name,a.push(h));return a}var b=a.config.removeButtons,b=b&&b.split(","),e=a.config.toolbar;"string"==typeof e&&(e=a.config["toolbar_"+e]);return a.toolbar=e?h(e):d()}function v(a){return a._.toolbarGroups||(a._.toolbarGroups=[{name:"document",groups:["mode","document","doctools"]},{name:"clipboard",groups:["clipboard","undo"]},{name:"editing",groups:["find","selection","spellchecker"]},{name:"forms"},"/",{name:"basicstyles",groups:["basicstyles","cleanup"]},{name:"paragraph",groups:["list",
+"indent","blocks","align","bidi"]},{name:"links"},{name:"insert"},"/",{name:"styles"},{name:"colors"},{name:"tools"},{name:"others"},{name:"about"}])}var z=function(){this.toolbars=[];this.focusCommandExecuted=!1};z.prototype.focus=function(){for(var a=0,d;d=this.toolbars[a++];)for(var f=0,c;c=d.items[f++];)if(c.focus){c.focus();return}};var E={modes:{wysiwyg:1,source:1},readOnly:1,exec:function(a){a.toolbox&&(a.toolbox.focusCommandExecuted=!0,CKEDITOR.env.ie||CKEDITOR.env.air?setTimeout(function(){a.toolbox.focus()},
+100):a.toolbox.focus())}};CKEDITOR.plugins.add("toolbar",{requires:"button",init:function(a){var d,f=function(c,h){var b,e="rtl"==a.lang.dir,k=a.config.toolbarGroupCycling,q=e?37:39,e=e?39:37,k=void 0===k||k;switch(h){case 9:case CKEDITOR.SHIFT+9:for(;!b||!b.items.length;)if(b=9==h?(b?b.next:c.toolbar.next)||a.toolbox.toolbars[0]:(b?b.previous:c.toolbar.previous)||a.toolbox.toolbars[a.toolbox.toolbars.length-1],b.items.length)for(c=b.items[d?b.items.length-1:0];c&&!c.focus;)(c=d?c.previous:c.next)||
+(b=0);c&&c.focus();return!1;case q:b=c;do b=b.next,!b&&k&&(b=c.toolbar.items[0]);while(b&&!b.focus);b?b.focus():f(c,9);return!1;case 40:return c.button&&c.button.hasArrow?c.execute():f(c,40==h?q:e),!1;case e:case 38:b=c;do b=b.previous,!b&&k&&(b=c.toolbar.items[c.toolbar.items.length-1]);while(b&&!b.focus);b?b.focus():(d=1,f(c,CKEDITOR.SHIFT+9),d=0);return!1;case 27:return a.focus(),!1;case 13:case 32:return c.execute(),!1}return!0};a.on("uiSpace",function(c){if(c.data.space==a.config.toolbarLocation){c.removeListener();
+a.toolbox=new z;var d=CKEDITOR.tools.getNextId(),b=['\x3cspan id\x3d"',d,'" class\x3d"cke_voice_label"\x3e',a.lang.toolbar.toolbars,"\x3c/span\x3e",'\x3cspan id\x3d"'+a.ui.spaceId("toolbox")+'" class\x3d"cke_toolbox" role\x3d"group" aria-labelledby\x3d"',d,'" onmousedown\x3d"return false;"\x3e'],d=!1!==a.config.toolbarStartupExpanded,e,k;a.config.toolbarCanCollapse&&a.elementMode!=CKEDITOR.ELEMENT_MODE_INLINE&&b.push('\x3cspan class\x3d"cke_toolbox_main"'+(d?"\x3e":' style\x3d"display:none"\x3e'));
+for(var q=a.toolbox.toolbars,n=D(a),g=n.length,l=0;l<g;l++){var r,m=0,w,p=n[l],v="/"!==p&&("/"===n[l+1]||l==g-1),x;if(p)if(e&&(b.push("\x3c/span\x3e"),k=e=0),"/"===p)b.push('\x3cspan class\x3d"cke_toolbar_break"\x3e\x3c/span\x3e');else{x=p.items||p;for(var y=0;y<x.length;y++){var t=x[y],A;if(t){var B=function(c){c=c.render(a,b);u=m.items.push(c)-1;0<u&&(c.previous=m.items[u-1],c.previous.next=c);c.toolbar=m;c.onkey=f;c.onfocus=function(){a.toolbox.focusCommandExecuted||a.focus()}};if(t.type==CKEDITOR.UI_SEPARATOR)k=
+e&&t;else{A=!1!==t.canGroup;if(!m){r=CKEDITOR.tools.getNextId();m={id:r,items:[]};w=p.name&&(a.lang.toolbar.toolbarGroups[p.name]||p.name);b.push('\x3cspan id\x3d"',r,'" class\x3d"cke_toolbar'+(v?' cke_toolbar_last"':'"'),w?' aria-labelledby\x3d"'+r+'_label"':"",' role\x3d"toolbar"\x3e');w&&b.push('\x3cspan id\x3d"',r,'_label" class\x3d"cke_voice_label"\x3e',w,"\x3c/span\x3e");b.push('\x3cspan class\x3d"cke_toolbar_start"\x3e\x3c/span\x3e');var u=q.push(m)-1;0<u&&(m.previous=q[u-1],m.previous.next=
+m)}A?e||(b.push('\x3cspan class\x3d"cke_toolgroup" role\x3d"presentation"\x3e'),e=1):e&&(b.push("\x3c/span\x3e"),e=0);k&&(B(k),k=0);B(t)}}}e&&(b.push("\x3c/span\x3e"),k=e=0);m&&b.push('\x3cspan class\x3d"cke_toolbar_end"\x3e\x3c/span\x3e\x3c/span\x3e')}}a.config.toolbarCanCollapse&&b.push("\x3c/span\x3e");if(a.config.toolbarCanCollapse&&a.elementMode!=CKEDITOR.ELEMENT_MODE_INLINE){var C=CKEDITOR.tools.addFunction(function(){a.execCommand("toolbarCollapse")});a.on("destroy",function(){CKEDITOR.tools.removeFunction(C)});
+a.addCommand("toolbarCollapse",{readOnly:1,exec:function(b){var a=b.ui.space("toolbar_collapser"),c=a.getPrevious(),d=b.ui.space("contents"),e=c.getParent(),h=parseInt(d.$.style.height,10),g=e.$.offsetHeight,f=a.hasClass("cke_toolbox_collapser_min");f?(c.show(),a.removeClass("cke_toolbox_collapser_min"),a.setAttribute("title",b.lang.toolbar.toolbarCollapse)):(c.hide(),a.addClass("cke_toolbox_collapser_min"),a.setAttribute("title",b.lang.toolbar.toolbarExpand));a.getFirst().setText(f?"▲":"◀");d.setStyle("height",
+h-(e.$.offsetHeight-g)+"px");b.fire("resize",{outerHeight:b.container.$.offsetHeight,contentsHeight:d.$.offsetHeight,outerWidth:b.container.$.offsetWidth})},modes:{wysiwyg:1,source:1}});a.setKeystroke(CKEDITOR.ALT+(CKEDITOR.env.ie||CKEDITOR.env.webkit?189:109),"toolbarCollapse");b.push('\x3ca title\x3d"'+(d?a.lang.toolbar.toolbarCollapse:a.lang.toolbar.toolbarExpand)+'" id\x3d"'+a.ui.spaceId("toolbar_collapser")+'" tabIndex\x3d"-1" class\x3d"cke_toolbox_collapser');d||b.push(" cke_toolbox_collapser_min");
+b.push('" onclick\x3d"CKEDITOR.tools.callFunction('+C+')"\x3e','\x3cspan class\x3d"cke_arrow"\x3e\x26#9650;\x3c/span\x3e',"\x3c/a\x3e")}b.push("\x3c/span\x3e");c.data.html+=b.join("")}});a.on("destroy",function(){if(this.toolbox){var a,d=0,b,e,f;for(a=this.toolbox.toolbars;d<a.length;d++)for(e=a[d].items,b=0;b<e.length;b++)f=e[b],f.clickFn&&CKEDITOR.tools.removeFunction(f.clickFn),f.keyDownFn&&CKEDITOR.tools.removeFunction(f.keyDownFn)}});a.on("uiReady",function(){var c=a.ui.space("toolbox");c&&a.focusManager.add(c,
+1)});a.addCommand("toolbarFocus",E);a.setKeystroke(CKEDITOR.ALT+121,"toolbarFocus");a.ui.add("-",CKEDITOR.UI_SEPARATOR,{});a.ui.addHandler(CKEDITOR.UI_SEPARATOR,{create:function(){return{render:function(a,d){d.push('\x3cspan class\x3d"cke_toolbar_separator" role\x3d"separator"\x3e\x3c/span\x3e');return{}}}}})}});CKEDITOR.ui.prototype.addToolbarGroup=function(a,d,f){var c=v(this.editor),h=0===d,b={name:a};if(f){if(f=CKEDITOR.tools.search(c,function(a){return a.name==f})){!f.groups&&(f.groups=[]);if(d&&
+(d=CKEDITOR.tools.indexOf(f.groups,d),0<=d)){f.groups.splice(d+1,0,a);return}h?f.groups.splice(0,0,a):f.groups.push(a);return}d=null}d&&(d=CKEDITOR.tools.indexOf(c,function(a){return a.name==d}));h?c.splice(0,0,a):"number"==typeof d?c.splice(d+1,0,b):c.push(a)}})();CKEDITOR.UI_SEPARATOR="separator";CKEDITOR.config.toolbarLocation="top";(function(){function q(b,a,c){a.type||(a.type="auto");if(c&&!1===b.fire("beforePaste",a)||!a.dataValue&&a.dataTransfer.isEmpty())return!1;a.dataValue||(a.dataValue="");if(CKEDITOR.env.gecko&&"drop"==a.method&&b.toolbox)b.once("afterPaste",function(){b.toolbox.focus()});return b.fire("paste",a)}function y(b){function a(){var a=b.editable();if(CKEDITOR.plugins.clipboard.isCustomCopyCutSupported){var c=function(a){b.readOnly&&"cut"==a.name||m.initPasteDataTransfer(a,b);a.data.preventDefault()};a.on("copy",
+c);a.on("cut",c);a.on("cut",function(){b.readOnly||b.extractSelectedHtml()},null,null,999)}a.on(m.mainPasteEvent,function(b){"beforepaste"==m.mainPasteEvent&&n||t(b)});"beforepaste"==m.mainPasteEvent&&(a.on("paste",function(b){u||(f(),b.data.preventDefault(),t(b),e("paste"))}),a.on("contextmenu",g,null,null,0),a.on("beforepaste",function(b){!b.data||b.data.$.ctrlKey||b.data.$.shiftKey||g()},null,null,0));a.on("beforecut",function(){!n&&h(b)});var d;a.attachListener(CKEDITOR.env.ie?a:b.document.getDocumentElement(),
+"mouseup",function(){d=setTimeout(function(){r()},0)});b.on("destroy",function(){clearTimeout(d)});a.on("keyup",r)}function c(a){return{type:a,canUndo:"cut"==a,startDisabled:!0,fakeKeystroke:"cut"==a?CKEDITOR.CTRL+88:CKEDITOR.CTRL+67,exec:function(){"cut"==this.type&&h();var a;var c=this.type;if(CKEDITOR.env.ie)a=e(c);else try{a=b.document.$.execCommand(c,!1,null)}catch(d){a=!1}a||b.showNotification(b.lang.clipboard[this.type+"Error"]);return a}}}function d(){return{canUndo:!1,async:!0,fakeKeystroke:CKEDITOR.CTRL+
+86,exec:function(b,a){function c(a,g){g="undefined"!==typeof g?g:!0;a?(a.method="paste",a.dataTransfer||(a.dataTransfer=m.initPasteDataTransfer()),q(b,a,g)):d&&b.showNotification(l,"info",b.config.clipboard_notificationDuration);b.fire("afterCommandExec",{name:"paste",command:e,returnValue:!!a})}a="undefined"!==typeof a&&null!==a?a:{};var e=this,d="undefined"!==typeof a.notification?a.notification:!0,g=a.type,h=CKEDITOR.tools.keystrokeToString(b.lang.common.keyboard,b.getCommandKeystroke(this)),l=
+"string"===typeof d?d:b.lang.clipboard.pasteNotification.replace(/%1/,'\x3ckbd aria-label\x3d"'+h.aria+'"\x3e'+h.display+"\x3c/kbd\x3e"),h="string"===typeof a?a:a.dataValue;g?b._.nextPasteType=g:delete b._.nextPasteType;"string"===typeof h?c({dataValue:h}):b.getClipboardData(c)}}}function f(){u=1;setTimeout(function(){u=0},100)}function g(){n=1;setTimeout(function(){n=0},10)}function e(a){var c=b.document,d=c.getBody(),e=!1,g=function(){e=!0};d.on(a,g);7<CKEDITOR.env.version?c.$.execCommand(a):c.$.selection.createRange().execCommand(a);
+d.removeListener(a,g);return e}function h(){if(CKEDITOR.env.ie&&!CKEDITOR.env.quirks){var a=b.getSelection(),c,d,e;a.getType()==CKEDITOR.SELECTION_ELEMENT&&(c=a.getSelectedElement())&&(d=a.getRanges()[0],e=b.document.createText(""),e.insertBefore(c),d.setStartBefore(e),d.setEndAfter(c),a.selectRanges([d]),setTimeout(function(){c.getParent()&&(e.remove(),a.selectElement(c))},0))}}function k(a,c){var d=b.document,e=b.editable(),g=function(b){b.cancel()},h;if(!d.getById("cke_pastebin")){var l=b.getSelection(),
+t=l.createBookmarks();CKEDITOR.env.ie&&l.root.fire("selectionchange");var f=new CKEDITOR.dom.element(!CKEDITOR.env.webkit&&!e.is("body")||CKEDITOR.env.ie?"div":"body",d);f.setAttributes({id:"cke_pastebin","data-cke-temp":"1"});var k=0,d=d.getWindow();CKEDITOR.env.webkit?(e.append(f),f.addClass("cke_editable"),e.is("body")||(k="static"!=e.getComputedStyle("position")?e:CKEDITOR.dom.element.get(e.$.offsetParent),k=k.getDocumentPosition().y)):e.getAscendant(CKEDITOR.env.ie?"body":"html",1).append(f);
+f.setStyles({position:"absolute",top:d.getScrollPosition().y-k+10+"px",width:"1px",height:Math.max(1,d.getViewPaneSize().height-20)+"px",overflow:"hidden",margin:0,padding:0});CKEDITOR.env.safari&&f.setStyles(CKEDITOR.tools.cssVendorPrefix("user-select","text"));(k=f.getParent().isReadOnly())?(f.setOpacity(0),f.setAttribute("contenteditable",!0)):f.setStyle("ltr"==b.config.contentsLangDirection?"left":"right","-10000px");b.on("selectionChange",g,null,null,0);if(CKEDITOR.env.webkit||CKEDITOR.env.gecko)h=
+e.once("blur",g,null,null,-100);k&&f.focus();k=new CKEDITOR.dom.range(f);k.selectNodeContents(f);var r=k.select();CKEDITOR.env.ie&&(h=e.once("blur",function(){b.lockSelection(r)}));var p=CKEDITOR.document.getWindow().getScrollPosition().y;setTimeout(function(){CKEDITOR.env.webkit&&(CKEDITOR.document.getBody().$.scrollTop=p);h&&h.removeListener();CKEDITOR.env.ie&&e.focus();l.selectBookmarks(t);f.remove();var a;CKEDITOR.env.webkit&&(a=f.getFirst())&&a.is&&a.hasClass("Apple-style-span")&&(f=a);b.removeListener("selectionChange",
+g);c(f.getHtml())},0)}}function v(){if("paste"==m.mainPasteEvent)return b.fire("beforePaste",{type:"auto",method:"paste"}),!1;b.focus();f();var a=b.focusManager;a.lock();if(b.editable().fire(m.mainPasteEvent)&&!e("paste"))return a.unlock(),!1;a.unlock();return!0}function l(a){if("wysiwyg"==b.mode)switch(a.data.keyCode){case CKEDITOR.CTRL+86:case CKEDITOR.SHIFT+45:a=b.editable();f();"paste"==m.mainPasteEvent&&a.fire("beforepaste");break;case CKEDITOR.CTRL+88:case CKEDITOR.SHIFT+46:b.fire("saveSnapshot"),
+setTimeout(function(){b.fire("saveSnapshot")},50)}}function t(a){var c={type:"auto",method:"paste",dataTransfer:m.initPasteDataTransfer(a)};c.dataTransfer.cacheData();var e=!1!==b.fire("beforePaste",c);e&&m.canClipboardApiBeTrusted(c.dataTransfer,b)?(a.data.preventDefault(),setTimeout(function(){q(b,c)},0)):k(a,function(a){c.dataValue=a.replace(/<span[^>]+data-cke-bookmark[^<]*?<\/span>/ig,"");e&&q(b,c)})}function r(){if("wysiwyg"==b.mode){var a=p("paste");b.getCommand("cut").setState(p("cut"));b.getCommand("copy").setState(p("copy"));
+b.getCommand("paste").setState(a);b.fire("pasteState",a)}}function p(a){if(w&&a in{paste:1,cut:1})return CKEDITOR.TRISTATE_DISABLED;if("paste"==a)return CKEDITOR.TRISTATE_OFF;a=b.getSelection();var c=a.getRanges();return a.getType()==CKEDITOR.SELECTION_NONE||1==c.length&&c[0].collapsed?CKEDITOR.TRISTATE_DISABLED:CKEDITOR.TRISTATE_OFF}var m=CKEDITOR.plugins.clipboard,n=0,u=0,w=0;(function(){b.on("key",l);b.on("contentDom",a);b.on("selectionChange",function(b){w=b.data.selection.getRanges()[0].checkReadOnly();
+r()});b.contextMenu&&b.contextMenu.addListener(function(b,a){w=a.getRanges()[0].checkReadOnly();return{cut:p("cut"),copy:p("copy"),paste:p("paste")}})})();(function(){function a(c,e,d,g,h){var l=b.lang.clipboard[e];b.addCommand(e,d);b.ui.addButton&&b.ui.addButton(c,{label:l,command:e,toolbar:"clipboard,"+g});b.addMenuItems&&b.addMenuItem(e,{label:l,command:e,group:"clipboard",order:h})}a("Cut","cut",c("cut"),10,1);a("Copy","copy",c("copy"),20,4);a("Paste","paste",d(),30,8)})();b.getClipboardData=
+function(a,c){function e(b){b.removeListener();b.cancel();c(b.data)}c||(c=a,a=null);b.on("paste",e,null,null,0);!1===v()&&(b.removeListener("paste",e),c(null))}}function z(b){if(CKEDITOR.env.webkit){if(!b.match(/^[^<]*$/g)&&!b.match(/^(<div><br( ?\/)?><\/div>|<div>[^<]*<\/div>)*$/gi))return"html"}else if(CKEDITOR.env.ie){if(!b.match(/^([^<]|<br( ?\/)?>)*$/gi)&&!b.match(/^(<p>([^<]|<br( ?\/)?>)*<\/p>|(\r\n))*$/gi))return"html"}else if(CKEDITOR.env.gecko){if(!b.match(/^([^<]|<br( ?\/)?>)*$/gi))return"html"}else return"html";
+return"htmlifiedtext"}function A(b,a){function c(b){return CKEDITOR.tools.repeat("\x3c/p\x3e\x3cp\x3e",~~(b/2))+(1==b%2?"\x3cbr\x3e":"")}a=a.replace(/\s+/g," ").replace(/> +</g,"\x3e\x3c").replace(/<br ?\/>/gi,"\x3cbr\x3e");a=a.replace(/<\/?[A-Z]+>/g,function(b){return b.toLowerCase()});if(a.match(/^[^<]$/))return a;CKEDITOR.env.webkit&&-1<a.indexOf("\x3cdiv\x3e")&&(a=a.replace(/^(<div>(<br>|)<\/div>)(?!$|(<div>(<br>|)<\/div>))/g,"\x3cbr\x3e").replace(/^(<div>(<br>|)<\/div>){2}(?!$)/g,"\x3cdiv\x3e\x3c/div\x3e"),
+a.match(/<div>(<br>|)<\/div>/)&&(a="\x3cp\x3e"+a.replace(/(<div>(<br>|)<\/div>)+/g,function(b){return c(b.split("\x3c/div\x3e\x3cdiv\x3e").length+1)})+"\x3c/p\x3e"),a=a.replace(/<\/div><div>/g,"\x3cbr\x3e"),a=a.replace(/<\/?div>/g,""));CKEDITOR.env.gecko&&b.enterMode!=CKEDITOR.ENTER_BR&&(CKEDITOR.env.gecko&&(a=a.replace(/^<br><br>$/,"\x3cbr\x3e")),-1<a.indexOf("\x3cbr\x3e\x3cbr\x3e")&&(a="\x3cp\x3e"+a.replace(/(<br>){2,}/g,function(b){return c(b.length/4)})+"\x3c/p\x3e"));return B(b,a)}function C(){function b(){var b=
+{},a;for(a in CKEDITOR.dtd)"$"!=a.charAt(0)&&"div"!=a&&"span"!=a&&(b[a]=1);return b}var a={};return{get:function(c){return"plain-text"==c?a.plainText||(a.plainText=new CKEDITOR.filter("br")):"semantic-content"==c?((c=a.semanticContent)||(c=new CKEDITOR.filter,c.allow({$1:{elements:b(),attributes:!0,styles:!1,classes:!1}}),c=a.semanticContent=c),c):c?new CKEDITOR.filter(c):null}}}function x(b,a,c){a=CKEDITOR.htmlParser.fragment.fromHtml(a);var d=new CKEDITOR.htmlParser.basicWriter;c.applyTo(a,!0,!1,
+b.activeEnterMode);a.writeHtml(d);return d.getHtml()}function B(b,a){b.enterMode==CKEDITOR.ENTER_BR?a=a.replace(/(<\/p><p>)+/g,function(b){return CKEDITOR.tools.repeat("\x3cbr\x3e",b.length/7*2)}).replace(/<\/?p>/g,""):b.enterMode==CKEDITOR.ENTER_DIV&&(a=a.replace(/<(\/)?p>/g,"\x3c$1div\x3e"));return a}function D(b){b.data.preventDefault();b.data.$.dataTransfer.dropEffect="none"}function E(b){var a=CKEDITOR.plugins.clipboard;b.on("contentDom",function(){function c(a,c,e){c.select();q(b,{dataTransfer:e,
+method:"drop"},1);e.sourceEditor.fire("saveSnapshot");e.sourceEditor.editable().extractHtmlFromRange(a);e.sourceEditor.getSelection().selectRanges([a]);e.sourceEditor.fire("saveSnapshot")}function d(c,e){c.select();q(b,{dataTransfer:e,method:"drop"},1);a.resetDragDataTransfer()}function f(a,c,e){var g={$:a.data.$,target:a.data.getTarget()};c&&(g.dragRange=c);e&&(g.dropRange=e);!1===b.fire(a.name,g)&&a.data.preventDefault()}function g(b){b.type!=CKEDITOR.NODE_ELEMENT&&(b=b.getParent());return b.getChildCount()}
+var e=b.editable(),h=CKEDITOR.plugins.clipboard.getDropTarget(b),k=b.ui.space("top"),v=b.ui.space("bottom");a.preventDefaultDropOnElement(k);a.preventDefaultDropOnElement(v);e.attachListener(h,"dragstart",f);e.attachListener(b,"dragstart",a.resetDragDataTransfer,a,null,1);e.attachListener(b,"dragstart",function(c){a.initDragDataTransfer(c,b)},null,null,2);e.attachListener(b,"dragstart",function(){var c=a.dragRange=b.getSelection().getRanges()[0];CKEDITOR.env.ie&&10>CKEDITOR.env.version&&(a.dragStartContainerChildCount=
+c?g(c.startContainer):null,a.dragEndContainerChildCount=c?g(c.endContainer):null)},null,null,100);e.attachListener(h,"dragend",f);e.attachListener(b,"dragend",a.initDragDataTransfer,a,null,1);e.attachListener(b,"dragend",a.resetDragDataTransfer,a,null,100);e.attachListener(h,"dragover",function(b){if(CKEDITOR.env.edge)b.data.preventDefault();else{var a=b.data.getTarget();a&&a.is&&a.is("html")?b.data.preventDefault():CKEDITOR.env.ie&&CKEDITOR.plugins.clipboard.isFileApiSupported&&b.data.$.dataTransfer.types.contains("Files")&&
+b.data.preventDefault()}});e.attachListener(h,"drop",function(c){if(!c.data.$.defaultPrevented){c.data.preventDefault();var e=c.data.getTarget();if(!e.isReadOnly()||e.type==CKEDITOR.NODE_ELEMENT&&e.is("html")){var e=a.getRangeAtDropPosition(c,b),g=a.dragRange;e&&f(c,g,e)}}},null,null,9999);e.attachListener(b,"drop",a.initDragDataTransfer,a,null,1);e.attachListener(b,"drop",function(e){if(e=e.data){var g=e.dropRange,h=e.dragRange,f=e.dataTransfer;f.getTransferType(b)==CKEDITOR.DATA_TRANSFER_INTERNAL?
+setTimeout(function(){a.internalDrop(h,g,f,b)},0):f.getTransferType(b)==CKEDITOR.DATA_TRANSFER_CROSS_EDITORS?c(h,g,f):d(g,f)}},null,null,9999)})}CKEDITOR.plugins.add("clipboard",{requires:"notification,toolbar",init:function(b){var a,c=C();b.config.forcePasteAsPlainText?a="plain-text":b.config.pasteFilter?a=b.config.pasteFilter:!CKEDITOR.env.webkit||"pasteFilter"in b.config||(a="semantic-content");b.pasteFilter=c.get(a);y(b);E(b);if(CKEDITOR.env.gecko){var d=["image/png","image/jpeg","image/gif"],
+f;b.on("paste",function(a){var c=a.data,h=c.dataTransfer;if(!c.dataValue&&"paste"==c.method&&h&&1==h.getFilesCount()&&f!=h.id&&(h=h.getFile(0),-1!=CKEDITOR.tools.indexOf(d,h.type))){var k=new FileReader;k.addEventListener("load",function(){a.data.dataValue='\x3cimg src\x3d"'+k.result+'" /\x3e';b.fire("paste",a.data)},!1);k.addEventListener("abort",function(){b.fire("paste",a.data)},!1);k.addEventListener("error",function(){b.fire("paste",a.data)},!1);k.readAsDataURL(h);f=c.dataTransfer.id;a.stop()}},
+null,null,1)}b.on("paste",function(a){a.data.dataTransfer||(a.data.dataTransfer=new CKEDITOR.plugins.clipboard.dataTransfer);if(!a.data.dataValue){var c=a.data.dataTransfer,d=c.getData("text/html");if(d)a.data.dataValue=d,a.data.type="html";else if(d=c.getData("text/plain"))a.data.dataValue=b.editable().transformPlainTextToHtml(d),a.data.type="text"}},null,null,1);b.on("paste",function(b){var a=b.data.dataValue,c=CKEDITOR.dtd.$block;-1<a.indexOf("Apple-")&&(a=a.replace(/<span class="Apple-converted-space">&nbsp;<\/span>/gi,
+" "),"html"!=b.data.type&&(a=a.replace(/<span class="Apple-tab-span"[^>]*>([^<]*)<\/span>/gi,function(a,b){return b.replace(/\t/g,"\x26nbsp;\x26nbsp; \x26nbsp;")})),-1<a.indexOf('\x3cbr class\x3d"Apple-interchange-newline"\x3e')&&(b.data.startsWithEOL=1,b.data.preSniffing="html",a=a.replace(/<br class="Apple-interchange-newline">/,"")),a=a.replace(/(<[^>]+) class="Apple-[^"]*"/gi,"$1"));if(a.match(/^<[^<]+cke_(editable|contents)/i)){var d,f,l=new CKEDITOR.dom.element("div");for(l.setHtml(a);1==l.getChildCount()&&
+(d=l.getFirst())&&d.type==CKEDITOR.NODE_ELEMENT&&(d.hasClass("cke_editable")||d.hasClass("cke_contents"));)l=f=d;f&&(a=f.getHtml().replace(/<br>$/i,""))}CKEDITOR.env.ie?a=a.replace(/^&nbsp;(?: |\r\n)?<(\w+)/g,function(a,e){return e.toLowerCase()in c?(b.data.preSniffing="html","\x3c"+e):a}):CKEDITOR.env.webkit?a=a.replace(/<\/(\w+)><div><br><\/div>$/,function(a,e){return e in c?(b.data.endsWithEOL=1,"\x3c/"+e+"\x3e"):a}):CKEDITOR.env.gecko&&(a=a.replace(/(\s)<br>$/,"$1"));b.data.dataValue=a},null,
+null,3);b.on("paste",function(a){a=a.data;var e=b._.nextPasteType||a.type,d=a.dataValue,f,n=b.config.clipboard_defaultContentType||"html",l=a.dataTransfer.getTransferType(b);f="html"==e||"html"==a.preSniffing?"html":z(d);delete b._.nextPasteType;"htmlifiedtext"==f&&(d=A(b.config,d));"text"==e&&"html"==f?d=x(b,d,c.get("plain-text")):l==CKEDITOR.DATA_TRANSFER_EXTERNAL&&b.pasteFilter&&!a.dontFilter&&(d=x(b,d,b.pasteFilter));a.startsWithEOL&&(d='\x3cbr data-cke-eol\x3d"1"\x3e'+d);a.endsWithEOL&&(d+='\x3cbr data-cke-eol\x3d"1"\x3e');
+"auto"==e&&(e="html"==f||"html"==n?"html":"text");a.type=e;a.dataValue=d;delete a.preSniffing;delete a.startsWithEOL;delete a.endsWithEOL},null,null,6);b.on("paste",function(a){a=a.data;a.dataValue&&(b.insertHtml(a.dataValue,a.type,a.range),setTimeout(function(){b.fire("afterPaste")},0))},null,null,1E3)}});CKEDITOR.plugins.clipboard={isCustomCopyCutSupported:!CKEDITOR.env.ie&&!CKEDITOR.env.iOS,isCustomDataTypesSupported:!CKEDITOR.env.ie,isFileApiSupported:!CKEDITOR.env.ie||9<CKEDITOR.env.version,
+mainPasteEvent:CKEDITOR.env.ie&&!CKEDITOR.env.edge?"beforepaste":"paste",canClipboardApiBeTrusted:function(b,a){return b.getTransferType(a)!=CKEDITOR.DATA_TRANSFER_EXTERNAL||CKEDITOR.env.chrome&&!b.isEmpty()||CKEDITOR.env.gecko&&(b.getData("text/html")||b.getFilesCount())||CKEDITOR.env.safari&&603<=CKEDITOR.env.version&&!CKEDITOR.env.iOS?!0:!1},getDropTarget:function(b){var a=b.editable();return CKEDITOR.env.ie&&9>CKEDITOR.env.version||a.isInline()?a:b.document},fixSplitNodesAfterDrop:function(b,
+a,c,d){function f(b,c,d){var f=b;f.type==CKEDITOR.NODE_TEXT&&(f=b.getParent());if(f.equals(c)&&d!=c.getChildCount())return b=a.startContainer.getChild(a.startOffset-1),c=a.startContainer.getChild(a.startOffset),b&&b.type==CKEDITOR.NODE_TEXT&&c&&c.type==CKEDITOR.NODE_TEXT&&(d=b.getLength(),b.setText(b.getText()+c.getText()),c.remove(),a.setStart(b,d),a.collapse(!0)),!0}var g=a.startContainer;"number"==typeof d&&"number"==typeof c&&g.type==CKEDITOR.NODE_ELEMENT&&(f(b.startContainer,g,c)||f(b.endContainer,
+g,d))},isDropRangeAffectedByDragRange:function(b,a){var c=a.startContainer,d=a.endOffset;return b.endContainer.equals(c)&&b.endOffset<=d||b.startContainer.getParent().equals(c)&&b.startContainer.getIndex()<d||b.endContainer.getParent().equals(c)&&b.endContainer.getIndex()<d?!0:!1},internalDrop:function(b,a,c,d){var f=CKEDITOR.plugins.clipboard,g=d.editable(),e,h;d.fire("saveSnapshot");d.fire("lockSnapshot",{dontUpdate:1});CKEDITOR.env.ie&&10>CKEDITOR.env.version&&this.fixSplitNodesAfterDrop(b,a,f.dragStartContainerChildCount,
+f.dragEndContainerChildCount);(h=this.isDropRangeAffectedByDragRange(b,a))||(e=b.createBookmark(!1));f=a.clone().createBookmark(!1);h&&(e=b.createBookmark(!1));b=e.startNode;a=e.endNode;h=f.startNode;a&&b.getPosition(h)&CKEDITOR.POSITION_PRECEDING&&a.getPosition(h)&CKEDITOR.POSITION_FOLLOWING&&h.insertBefore(b);b=d.createRange();b.moveToBookmark(e);g.extractHtmlFromRange(b,1);a=d.createRange();a.moveToBookmark(f);q(d,{dataTransfer:c,method:"drop",range:a},1);d.fire("unlockSnapshot")},getRangeAtDropPosition:function(b,
+a){var c=b.data.$,d=c.clientX,f=c.clientY,g=a.getSelection(!0).getRanges()[0],e=a.createRange();if(b.data.testRange)return b.data.testRange;if(document.caretRangeFromPoint&&a.document.$.caretRangeFromPoint(d,f))c=a.document.$.caretRangeFromPoint(d,f),e.setStart(CKEDITOR.dom.node(c.startContainer),c.startOffset),e.collapse(!0);else if(c.rangeParent)e.setStart(CKEDITOR.dom.node(c.rangeParent),c.rangeOffset),e.collapse(!0);else{if(CKEDITOR.env.ie&&8<CKEDITOR.env.version&&g&&a.editable().hasFocus)return g;
+if(document.body.createTextRange){a.focus();c=a.document.getBody().$.createTextRange();try{for(var h=!1,k=0;20>k&&!h;k++){if(!h)try{c.moveToPoint(d,f-k),h=!0}catch(n){}if(!h)try{c.moveToPoint(d,f+k),h=!0}catch(l){}}if(h){var t="cke-temp-"+(new Date).getTime();c.pasteHTML('\x3cspan id\x3d"'+t+'"\x3e​\x3c/span\x3e');var r=a.document.getById(t);e.moveToPosition(r,CKEDITOR.POSITION_BEFORE_START);r.remove()}else{var p=a.document.$.elementFromPoint(d,f),m=new CKEDITOR.dom.element(p),q;if(m.equals(a.editable())||
+"html"==m.getName())return g&&g.startContainer&&!g.startContainer.equals(a.editable())?g:null;q=m.getClientRect();d<q.left?e.setStartAt(m,CKEDITOR.POSITION_AFTER_START):e.setStartAt(m,CKEDITOR.POSITION_BEFORE_END);e.collapse(!0)}}catch(u){return null}}else return null}return e},initDragDataTransfer:function(b,a){var c=b.data.$?b.data.$.dataTransfer:null,d=new this.dataTransfer(c,a);c?this.dragData&&d.id==this.dragData.id?d=this.dragData:this.dragData=d:this.dragData?d=this.dragData:this.dragData=
+d;b.data.dataTransfer=d},resetDragDataTransfer:function(){this.dragData=null},initPasteDataTransfer:function(b,a){if(this.isCustomCopyCutSupported){if(b&&b.data&&b.data.$){var c=new this.dataTransfer(b.data.$.clipboardData,a);this.copyCutData&&c.id==this.copyCutData.id?(c=this.copyCutData,c.$=b.data.$.clipboardData):this.copyCutData=c;return c}return new this.dataTransfer(null,a)}return new this.dataTransfer(CKEDITOR.env.edge&&b&&b.data.$&&b.data.$.clipboardData||null,a)},preventDefaultDropOnElement:function(b){b&&
+b.on("dragover",D)}};var n=CKEDITOR.plugins.clipboard.isCustomDataTypesSupported?"cke/id":"Text";CKEDITOR.plugins.clipboard.dataTransfer=function(b,a){b&&(this.$=b);this._={metaRegExp:/^<meta.*?>/i,bodyRegExp:/<body(?:[\s\S]*?)>([\s\S]*)<\/body>/i,fragmentRegExp:/\x3c!--(?:Start|End)Fragment--\x3e/g,data:{},files:[],normalizeType:function(a){a=a.toLowerCase();return"text"==a||"text/plain"==a?"Text":"url"==a?"URL":a}};this.id=this.getData(n);this.id||(this.id="Text"==n?"":"cke-"+CKEDITOR.tools.getUniqueId());
+if("Text"!=n)try{this.$.setData(n,this.id)}catch(c){}a&&(this.sourceEditor=a,this.setData("text/html",a.getSelectedHtml(1)),"Text"==n||this.getData("text/plain")||this.setData("text/plain",a.getSelection().getSelectedText()))};CKEDITOR.DATA_TRANSFER_INTERNAL=1;CKEDITOR.DATA_TRANSFER_CROSS_EDITORS=2;CKEDITOR.DATA_TRANSFER_EXTERNAL=3;CKEDITOR.plugins.clipboard.dataTransfer.prototype={getData:function(b,a){b=this._.normalizeType(b);var c=this._.data[b],d;if(void 0===c||null===c||""===c)try{c=this.$.getData(b)}catch(f){}if(void 0===
+c||null===c||""===c)c="";"text/html"!=b||a?"Text"==b&&CKEDITOR.env.gecko&&this.getFilesCount()&&"file://"==c.substring(0,7)&&(c=""):(c=c.replace(this._.metaRegExp,""),(d=this._.bodyRegExp.exec(c))&&d.length&&(c=d[1],c=c.replace(this._.fragmentRegExp,"")));"string"===typeof c&&(d=c.indexOf("\x3c/html\x3e"),c=-1!==d?c.substring(0,d+7):c);return c},setData:function(b,a){b=this._.normalizeType(b);this._.data[b]=a;if(CKEDITOR.plugins.clipboard.isCustomDataTypesSupported||"URL"==b||"Text"==b){"Text"==n&&
+"Text"==b&&(this.id=a);try{this.$.setData(b,a)}catch(c){}}},getTransferType:function(b){return this.sourceEditor?this.sourceEditor==b?CKEDITOR.DATA_TRANSFER_INTERNAL:CKEDITOR.DATA_TRANSFER_CROSS_EDITORS:CKEDITOR.DATA_TRANSFER_EXTERNAL},cacheData:function(){function b(b){b=a._.normalizeType(b);var c=a.getData(b,!0);c&&(a._.data[b]=c)}if(this.$){var a=this,c,d;if(CKEDITOR.plugins.clipboard.isCustomDataTypesSupported){if(this.$.types)for(c=0;c<this.$.types.length;c++)b(this.$.types[c])}else b("Text"),
+b("URL");d=this._getImageFromClipboard();if(this.$&&this.$.files||d){this._.files=[];if(this.$.files&&this.$.files.length)for(c=0;c<this.$.files.length;c++)this._.files.push(this.$.files[c]);0===this._.files.length&&d&&this._.files.push(d)}}},getFilesCount:function(){return this._.files.length?this._.files.length:this.$&&this.$.files&&this.$.files.length?this.$.files.length:this._getImageFromClipboard()?1:0},getFile:function(b){return this._.files.length?this._.files[b]:this.$&&this.$.files&&this.$.files.length?
+this.$.files[b]:0===b?this._getImageFromClipboard():void 0},isEmpty:function(){var b={},a;if(this.getFilesCount())return!1;for(a in this._.data)b[a]=1;if(this.$)if(CKEDITOR.plugins.clipboard.isCustomDataTypesSupported){if(this.$.types)for(var c=0;c<this.$.types.length;c++)b[this.$.types[c]]=1}else b.Text=1,b.URL=1;"Text"!=n&&(b[n]=0);for(a in b)if(b[a]&&""!==this.getData(a))return!1;return!0},_getImageFromClipboard:function(){var b;if(this.$&&this.$.items&&this.$.items[0])try{if((b=this.$.items[0].getAsFile())&&
+b.type)return b}catch(a){}}}})();CKEDITOR.config.clipboard_notificationDuration=1E4;(function(){function q(b,d,a){a=b.config.forceEnterMode||a;if("wysiwyg"==b.mode){d||(d=b.activeEnterMode);var h=b.elementPath();h&&!h.isContextFor("p")&&(d=CKEDITOR.ENTER_BR,a=1);b.fire("saveSnapshot");d==CKEDITOR.ENTER_BR?t(b,d,null,a):u(b,d,null,a);b.fire("saveSnapshot")}}function v(b){b=b.getSelection().getRanges(!0);for(var d=b.length-1;0<d;d--)b[d].deleteContents();return b[0]}function y(b){var d=b.startContainer.getAscendant(function(a){return a.type==CKEDITOR.NODE_ELEMENT&&"true"==a.getAttribute("contenteditable")},
+!0);if(b.root.equals(d))return b;d=new CKEDITOR.dom.range(d);d.moveToRange(b);return d}CKEDITOR.plugins.add("enterkey",{init:function(b){b.addCommand("enter",{modes:{wysiwyg:1},editorFocus:!1,exec:function(b){q(b)}});b.addCommand("shiftEnter",{modes:{wysiwyg:1},editorFocus:!1,exec:function(b){q(b,b.activeShiftEnterMode,1)}});b.setKeystroke([[13,"enter"],[CKEDITOR.SHIFT+13,"shiftEnter"]])}});var z=CKEDITOR.dom.walker.whitespaces(),A=CKEDITOR.dom.walker.bookmark();CKEDITOR.plugins.enterkey={enterBlock:function(b,
+d,a,h){if(a=a||v(b)){a=y(a);var f=a.document,k=a.checkStartOfBlock(),m=a.checkEndOfBlock(),l=b.elementPath(a.startContainer),c=l.block,n=d==CKEDITOR.ENTER_DIV?"div":"p",e;if(k&&m){if(c&&(c.is("li")||c.getParent().is("li"))){c.is("li")||(c=c.getParent());a=c.getParent();e=a.getParent();h=!c.hasPrevious();var p=!c.hasNext(),n=b.getSelection(),g=n.createBookmarks(),k=c.getDirection(1),m=c.getAttribute("class"),r=c.getAttribute("style"),q=e.getDirection(1)!=k;b=b.enterMode!=CKEDITOR.ENTER_BR||q||r||m;
+if(e.is("li"))h||p?(h&&p&&a.remove(),c[p?"insertAfter":"insertBefore"](e)):c.breakParent(e);else{if(b)if(l.block.is("li")?(e=f.createElement(d==CKEDITOR.ENTER_P?"p":"div"),q&&e.setAttribute("dir",k),r&&e.setAttribute("style",r),m&&e.setAttribute("class",m),c.moveChildren(e)):e=l.block,h||p)e[h?"insertBefore":"insertAfter"](a);else c.breakParent(a),e.insertAfter(a);else if(c.appendBogus(!0),h||p)for(;f=c[h?"getFirst":"getLast"]();)f[h?"insertBefore":"insertAfter"](a);else for(c.breakParent(a);f=c.getLast();)f.insertAfter(a);
+c.remove()}n.selectBookmarks(g);return}if(c&&c.getParent().is("blockquote")){c.breakParent(c.getParent());c.getPrevious().getFirst(CKEDITOR.dom.walker.invisible(1))||c.getPrevious().remove();c.getNext().getFirst(CKEDITOR.dom.walker.invisible(1))||c.getNext().remove();a.moveToElementEditStart(c);a.select();return}}else if(c&&c.is("pre")&&!m){t(b,d,a,h);return}if(k=a.splitBlock(n)){d=k.previousBlock;c=k.nextBlock;l=k.wasStartOfBlock;b=k.wasEndOfBlock;c?(g=c.getParent(),g.is("li")&&(c.breakParent(g),
+c.move(c.getNext(),1))):d&&(g=d.getParent())&&g.is("li")&&(d.breakParent(g),g=d.getNext(),a.moveToElementEditStart(g),d.move(d.getPrevious()));if(l||b){if(d){if(d.is("li")||!w.test(d.getName())&&!d.is("pre"))e=d.clone()}else c&&(e=c.clone());e?h&&!e.is("li")&&e.renameNode(n):g&&g.is("li")?e=g:(e=f.createElement(n),d&&(p=d.getDirection())&&e.setAttribute("dir",p));if(f=k.elementPath)for(h=0,n=f.elements.length;h<n;h++){g=f.elements[h];if(g.equals(f.block)||g.equals(f.blockLimit))break;CKEDITOR.dtd.$removeEmpty[g.getName()]&&
+(g=g.clone(),e.moveChildren(g),e.append(g))}e.appendBogus();e.getParent()||a.insertNode(e);e.is("li")&&e.removeAttribute("value");!CKEDITOR.env.ie||!l||b&&d.getChildCount()||(a.moveToElementEditStart(b?d:e),a.select());a.moveToElementEditStart(l&&!b?c:e)}else c.is("li")&&(e=a.clone(),e.selectNodeContents(c),e=new CKEDITOR.dom.walker(e),e.evaluator=function(a){return!(A(a)||z(a)||a.type==CKEDITOR.NODE_ELEMENT&&a.getName()in CKEDITOR.dtd.$inline&&!(a.getName()in CKEDITOR.dtd.$empty))},(g=e.next())&&
+g.type==CKEDITOR.NODE_ELEMENT&&g.is("ul","ol")&&(CKEDITOR.env.needsBrFiller?f.createElement("br"):f.createText(" ")).insertBefore(g)),c&&a.moveToElementEditStart(c);a.select();a.scrollIntoView()}}},enterBr:function(b,d,a,h){if(a=a||v(b)){var f=a.document,k=a.checkEndOfBlock(),m=new CKEDITOR.dom.elementPath(b.getSelection().getStartElement()),l=m.block,c=l&&m.block.getName();h||"li"!=c?(!h&&k&&w.test(c)?(k=l.getDirection())?(f=f.createElement("div"),f.setAttribute("dir",k),f.insertAfter(l),a.setStart(f,
+0)):(f.createElement("br").insertAfter(l),CKEDITOR.env.gecko&&f.createText("").insertAfter(l),a.setStartAt(l.getNext(),CKEDITOR.env.ie?CKEDITOR.POSITION_BEFORE_START:CKEDITOR.POSITION_AFTER_START)):(b="pre"==c&&CKEDITOR.env.ie&&8>CKEDITOR.env.version?f.createText("\r"):f.createElement("br"),a.deleteContents(),a.insertNode(b),CKEDITOR.env.needsBrFiller?(f.createText("").insertAfter(b),k&&(l||m.blockLimit).appendBogus(),b.getNext().$.nodeValue="",a.setStartAt(b.getNext(),CKEDITOR.POSITION_AFTER_START)):
+a.setStartAt(b,CKEDITOR.POSITION_AFTER_END)),a.collapse(!0),a.select(),a.scrollIntoView()):u(b,d,a,h)}}};var x=CKEDITOR.plugins.enterkey,t=x.enterBr,u=x.enterBlock,w=/^h[1-6]$/})();(function(){function k(b,f){var g={},c=[],e={nbsp:" ",shy:"­",gt:"\x3e",lt:"\x3c",amp:"\x26",apos:"'",quot:'"'};b=b.replace(/\b(nbsp|shy|gt|lt|amp|apos|quot)(?:,|$)/g,function(b,a){var d=f?"\x26"+a+";":e[a];g[d]=f?e[a]:"\x26"+a+";";c.push(d);return""});if(!f&&b){b=b.split(",");var a=document.createElement("div"),d;a.innerHTML="\x26"+b.join(";\x26")+";";d=a.innerHTML;a=null;for(a=0;a<d.length;a++){var h=d.charAt(a);g[h]="\x26"+b[a]+";";c.push(h)}}g.regex=c.join(f?"|":"");return g}CKEDITOR.plugins.add("entities",
+{afterInit:function(b){function f(a){return h[a]}function g(b){return"force"!=c.entities_processNumerical&&a[b]?a[b]:"\x26#"+b.charCodeAt(0)+";"}var c=b.config;if(b=(b=b.dataProcessor)&&b.htmlFilter){var e=[];!1!==c.basicEntities&&e.push("nbsp,gt,lt,amp");c.entities&&(e.length&&e.push("quot,iexcl,cent,pound,curren,yen,brvbar,sect,uml,copy,ordf,laquo,not,shy,reg,macr,deg,plusmn,sup2,sup3,acute,micro,para,middot,cedil,sup1,ordm,raquo,frac14,frac12,frac34,iquest,times,divide,fnof,bull,hellip,prime,Prime,oline,frasl,weierp,image,real,trade,alefsym,larr,uarr,rarr,darr,harr,crarr,lArr,uArr,rArr,dArr,hArr,forall,part,exist,empty,nabla,isin,notin,ni,prod,sum,minus,lowast,radic,prop,infin,ang,and,or,cap,cup,int,there4,sim,cong,asymp,ne,equiv,le,ge,sub,sup,nsub,sube,supe,oplus,otimes,perp,sdot,lceil,rceil,lfloor,rfloor,lang,rang,loz,spades,clubs,hearts,diams,circ,tilde,ensp,emsp,thinsp,zwnj,zwj,lrm,rlm,ndash,mdash,lsquo,rsquo,sbquo,ldquo,rdquo,bdquo,dagger,Dagger,permil,lsaquo,rsaquo,euro"),
+c.entities_latin&&e.push("Agrave,Aacute,Acirc,Atilde,Auml,Aring,AElig,Ccedil,Egrave,Eacute,Ecirc,Euml,Igrave,Iacute,Icirc,Iuml,ETH,Ntilde,Ograve,Oacute,Ocirc,Otilde,Ouml,Oslash,Ugrave,Uacute,Ucirc,Uuml,Yacute,THORN,szlig,agrave,aacute,acirc,atilde,auml,aring,aelig,ccedil,egrave,eacute,ecirc,euml,igrave,iacute,icirc,iuml,eth,ntilde,ograve,oacute,ocirc,otilde,ouml,oslash,ugrave,uacute,ucirc,uuml,yacute,thorn,yuml,OElig,oelig,Scaron,scaron,Yuml"),c.entities_greek&&e.push("Alpha,Beta,Gamma,Delta,Epsilon,Zeta,Eta,Theta,Iota,Kappa,Lambda,Mu,Nu,Xi,Omicron,Pi,Rho,Sigma,Tau,Upsilon,Phi,Chi,Psi,Omega,alpha,beta,gamma,delta,epsilon,zeta,eta,theta,iota,kappa,lambda,mu,nu,xi,omicron,pi,rho,sigmaf,sigma,tau,upsilon,phi,chi,psi,omega,thetasym,upsih,piv"),
+c.entities_additional&&e.push(c.entities_additional));var a=k(e.join(",")),d=a.regex?"["+a.regex+"]":"a^";delete a.regex;c.entities&&c.entities_processNumerical&&(d="[^ -~]|"+d);var d=new RegExp(d,"g"),h=k("nbsp,gt,lt,amp,shy",!0),l=new RegExp(h.regex,"g");b.addRules({text:function(a){return a.replace(l,f).replace(d,g)}},{applyToAll:!0,excludeNestedEditable:!0})}}})})();CKEDITOR.config.basicEntities=!0;CKEDITOR.config.entities=!0;CKEDITOR.config.entities_latin=!0;CKEDITOR.config.entities_greek=!0;
+CKEDITOR.config.entities_additional="#39";(function(){function k(a){var l=a.config,p=a.fire("uiSpace",{space:"top",html:""}).html,
+el=(function(e){var p=e.$.getAttribute("position"),s=p==="absolute",o="overflow";return e.getParents().filter(function(q){var c=window.getComputedStyle(q.$);if(s&&c.position==="static")return false;return(/(auto|scroll)/).test(c[o]+c[o+"-y"]+c[o+"-x"] )})})(a.element),
+t=function(){function f(a,c,e){b.setStyle(c,w(e));b.setStyle("position",a)}function e(a){var b=k.getDocumentPosition();switch(a){case "top":f("absolute","top",b.y-q-r);break;case "pin":f("fixed","top",x);break;case "bottom":f("absolute","top",b.y+(c.height||c.bottom-c.top)+r)}m=a}var m,k,n,c,h,q,v,p=l.floatSpaceDockedOffsetX||0,r=l.floatSpaceDockedOffsetY||0,u=l.floatSpacePinnedOffsetX||0,x=l.floatSpacePinnedOffsetY||
+window.innerHeight-28;return function(d){if(k=a.editable()){var f=d&&"focus"==d.name;f&&b.show();a.fire("floatingSpaceLayout",{show:f});b.removeStyle("left");b.removeStyle("right");n=b.getClientRect();c=k.getClientRect();h=g.getViewPaneSize();q=n.height;v="pageXOffset"in g.$?g.$.pageXOffset:CKEDITOR.document.$.documentElement.scrollLeft;m?(q+r<=c.top-77?e("top"):q+r>h.height-c.bottom?e("pin"):e("bottom"),d=h.width/2,d=l.floatSpacePreferRight?"right":0<c.left&&c.right<h.width&&c.width>n.width?"rtl"==l.contentsLangDirection?
+"right":"left":d-c.left>c.right-d?"left":"right",n.width>h.width?(d="left",f=0):(f="left"==d?0<c.left?c.left:0:c.right<h.width?h.width-c.right:0,f+n.width>h.width&&(d="left"==d?"right":"left",f=0)),b.setStyle(d,w(("pin"==m?u:p)+f+("pin"==m?0:"left"==d?v:-v)))):(m="pin",e("pin"),t(d))}}}();if(p){var k=new CKEDITOR.template('\x3cdiv id\x3d"cke_{name}" class\x3d"cke {id} cke_reset_all cke_chrome cke_editor_{name} cke_float cke_{langDir} '+CKEDITOR.env.cssClass+'" dir\x3d"{langDir}" title\x3d"'+(CKEDITOR.env.gecko?
+" ":"")+'" lang\x3d"{langCode}" role\x3d"application" style\x3d"{style}"'+(a.title?' aria-labelledby\x3d"cke_{name}_arialbl"':" ")+"\x3e"+(a.title?'\x3cspan id\x3d"cke_{name}_arialbl" class\x3d"cke_voice_label"\x3e{voiceLabel}\x3c/span\x3e':" ")+'\x3cdiv class\x3d"cke_inner"\x3e\x3cdiv id\x3d"{topId}" class\x3d"cke_top" role\x3d"presentation"\x3e{content}\x3c/div\x3e\x3c/div\x3e\x3c/div\x3e'),b=CKEDITOR.document.getBody().append(CKEDITOR.dom.element.createFromHtml(k.output({content:p,id:a.id,langDir:a.lang.dir,
+langCode:a.langCode,name:a.name,style:"display:none;z-index:"+(l.baseFloatZIndex-1),topId:a.ui.spaceId("top"),voiceLabel:a.title}))),u=CKEDITOR.tools.eventsBuffer(500,t),e=CKEDITOR.tools.eventsBuffer(10,t);b.unselectable();b.on("mousedown",function(a){a=a.data;a.getTarget().hasAscendant("a",1)||a.preventDefault()});a.on("focus",function(b){t(b);a.on("change",u.input);g.on("scroll",e.input);el.forEach(function(i){i.on("scroll",e.input)});g.on("resize",e.input)});a.on("blur",function(){b.hide();a.removeListener("change",u.input);g.removeListener("scroll",
+e.input);el.forEach(function(i){i.removeListener("scroll",e.input)});
+g.removeListener("resize",e.input)});a.on("destroy",function(){g.removeListener("scroll",e.input);el.forEach(function(i){i.removeListener("scroll",e.input)});g.removeListener("resize",e.input);b.clearCustomData();b.remove()});a.focusManager.hasFocus&&b.show();a.focusManager.add(b,1)}}var g=CKEDITOR.document.getWindow(),w=CKEDITOR.tools.cssLength;CKEDITOR.plugins.add("floatingspace",{init:function(a){a.on("loaded",function(){k(this)},null,null,20)}})})();(function(){var b={canUndo:!1,exec:function(a){var b=a.document.createElement("hr");a.insertElement(b)},allowedContent:"hr",requiredContent:"hr"};CKEDITOR.plugins.add("horizontalrule",{init:function(a){a.blockless||(a.addCommand("horizontalrule",b),a.ui.addButton&&a.ui.addButton("HorizontalRule",{label:a.lang.horizontalrule.toolbar,command:"horizontalrule",toolbar:"insert,40"}))}})})();(function(){function m(a,b){var e,f;b.on("refresh",function(a){var b=[k],c;for(c in a.data.states)b.push(a.data.states[c]);this.setState(CKEDITOR.tools.search(b,p)?p:k)},b,null,100);b.on("exec",function(b){e=a.getSelection();f=e.createBookmarks(1);b.data||(b.data={});b.data.done=!1},b,null,0);b.on("exec",function(){a.forceNextSelectionCheck();e.selectBookmarks(f)},b,null,100)}var k=CKEDITOR.TRISTATE_DISABLED,p=CKEDITOR.TRISTATE_OFF;CKEDITOR.plugins.add("indent",{init:function(a){var b=CKEDITOR.plugins.indent.genericDefinition;
+m(a,a.addCommand("indent",new b(!0)));m(a,a.addCommand("outdent",new b));a.ui.addButton&&(a.ui.addButton("Indent",{label:a.lang.indent.indent,command:"indent",directional:!0,toolbar:"indent,20"}),a.ui.addButton("Outdent",{label:a.lang.indent.outdent,command:"outdent",directional:!0,toolbar:"indent,10"}));a.on("dirChanged",function(b){var f=a.createRange(),l=b.data.node;f.setStartBefore(l);f.setEndAfter(l);for(var n=new CKEDITOR.dom.walker(f),c;c=n.next();)if(c.type==CKEDITOR.NODE_ELEMENT)if(!c.equals(l)&&
+c.getDirection())f.setStartAfter(c),n=new CKEDITOR.dom.walker(f);else{var d=a.config.indentClasses;if(d)for(var g="ltr"==b.data.dir?["_rtl",""]:["","_rtl"],h=0;h<d.length;h++)c.hasClass(d[h]+g[0])&&(c.removeClass(d[h]+g[0]),c.addClass(d[h]+g[1]));d=c.getStyle("margin-right");g=c.getStyle("margin-left");d?c.setStyle("margin-left",d):c.removeStyle("margin-left");g?c.setStyle("margin-right",g):c.removeStyle("margin-right")}})}});CKEDITOR.plugins.indent={genericDefinition:function(a){this.isIndent=!!a;
+this.startDisabled=!this.isIndent},specificDefinition:function(a,b,e){this.name=b;this.editor=a;this.jobs={};this.enterBr=a.config.enterMode==CKEDITOR.ENTER_BR;this.isIndent=!!e;this.relatedGlobal=e?"indent":"outdent";this.indentKey=e?9:CKEDITOR.SHIFT+9;this.database={}},registerCommands:function(a,b){a.on("pluginsLoaded",function(){for(var a in b)(function(a,b){var e=a.getCommand(b.relatedGlobal),c;for(c in b.jobs)e.on("exec",function(d){d.data.done||(a.fire("lockSnapshot"),b.execJob(a,c)&&(d.data.done=
+!0),a.fire("unlockSnapshot"),CKEDITOR.dom.element.clearAllMarkers(b.database))},this,null,c),e.on("refresh",function(d){d.data.states||(d.data.states={});d.data.states[b.name+"@"+c]=b.refreshJob(a,c,d.data.path)},this,null,c);a.addFeature(b)})(this,b[a])})}};CKEDITOR.plugins.indent.genericDefinition.prototype={context:"p",exec:function(){}};CKEDITOR.plugins.indent.specificDefinition.prototype={execJob:function(a,b){var e=this.jobs[b];if(e.state!=k)return e.exec.call(this,a)},refreshJob:function(a,
+b,e){b=this.jobs[b];a.activeFilter.checkFeature(this)?b.state=b.refresh.call(this,a,e):b.state=k;return b.state},getContext:function(a){return a.contains(this.context)}}})();(function(){function w(d){function f(b){for(var e=c.startContainer,a=c.endContainer;e&&!e.getParent().equals(b);)e=e.getParent();for(;a&&!a.getParent().equals(b);)a=a.getParent();if(!e||!a)return!1;for(var g=e,e=[],k=!1;!k;)g.equals(a)&&(k=!0),e.push(g),g=g.getNext();if(1>e.length)return!1;g=b.getParents(!0);for(a=0;a<g.length;a++)if(g[a].getName&&p[g[a].getName()]){b=g[a];break}for(var g=l.isIndent?1:-1,a=e[0],e=e[e.length-1],k=CKEDITOR.plugins.list.listToArray(b,q),h=k[e.getCustomData("listarray_index")].indent,
+a=a.getCustomData("listarray_index");a<=e.getCustomData("listarray_index");a++)if(k[a].indent+=g,0<g){var n=k[a].parent;k[a].parent=new CKEDITOR.dom.element(n.getName(),n.getDocument())}for(a=e.getCustomData("listarray_index")+1;a<k.length&&k[a].indent>h;a++)k[a].indent+=g;e=CKEDITOR.plugins.list.arrayToList(k,q,null,d.config.enterMode,b.getDirection());if(!l.isIndent){var f;if((f=b.getParent())&&f.is("li"))for(var g=e.listNode.getChildren(),r=[],m,a=g.count()-1;0<=a;a--)(m=g.getItem(a))&&m.is&&m.is("li")&&
+r.push(m)}e&&e.listNode.replace(b);if(r&&r.length)for(a=0;a<r.length;a++){for(m=b=r[a];(m=m.getNext())&&m.is&&m.getName()in p;)CKEDITOR.env.needsNbspFiller&&!b.getFirst(x)&&b.append(c.document.createText(" ")),b.append(m);b.insertAfter(f)}e&&d.fire("contentDomInvalidated");return!0}for(var l=this,q=this.database,p=this.context,c,n=d.getSelection(),n=(n&&n.getRanges()).createIterator();c=n.getNextRange();){for(var b=c.getCommonAncestor();b&&(b.type!=CKEDITOR.NODE_ELEMENT||!p[b.getName()]);){if(d.editable().equals(b)){b=
+!1;break}b=b.getParent()}b||(b=c.startPath().contains(p))&&c.setEndAt(b,CKEDITOR.POSITION_BEFORE_END);if(!b){var h=c.getEnclosedNode();h&&h.type==CKEDITOR.NODE_ELEMENT&&h.getName()in p&&(c.setStartAt(h,CKEDITOR.POSITION_AFTER_START),c.setEndAt(h,CKEDITOR.POSITION_BEFORE_END),b=h)}b&&c.startContainer.type==CKEDITOR.NODE_ELEMENT&&c.startContainer.getName()in p&&(h=new CKEDITOR.dom.walker(c),h.evaluator=t,c.startContainer=h.next());b&&c.endContainer.type==CKEDITOR.NODE_ELEMENT&&c.endContainer.getName()in
+p&&(h=new CKEDITOR.dom.walker(c),h.evaluator=t,c.endContainer=h.previous());if(b)return f(b)}return 0}function t(d){return d.type==CKEDITOR.NODE_ELEMENT&&d.is("li")}function x(d){return y(d)&&z(d)}var y=CKEDITOR.dom.walker.whitespaces(!0),z=CKEDITOR.dom.walker.bookmark(!1,!0),u=CKEDITOR.TRISTATE_DISABLED,v=CKEDITOR.TRISTATE_OFF;CKEDITOR.plugins.add("indentlist",{requires:"indent",init:function(d){function f(d){l.specificDefinition.apply(this,arguments);this.requiredContent=["ul","ol"];d.on("key",
+function(f){var c=d.elementPath();if("wysiwyg"==d.mode&&f.data.keyCode==this.indentKey&&c){var n=this.getContext(c);!n||this.isIndent&&CKEDITOR.plugins.indentList.firstItemInPath(this.context,c,n)||(d.execCommand(this.relatedGlobal),f.cancel())}},this);this.jobs[this.isIndent?10:30]={refresh:this.isIndent?function(d,c){var f=this.getContext(c),b=CKEDITOR.plugins.indentList.firstItemInPath(this.context,c,f);return f&&this.isIndent&&!b?v:u}:function(d,c){return!this.getContext(c)||this.isIndent?u:v},
+exec:CKEDITOR.tools.bind(w,this)}}var l=CKEDITOR.plugins.indent;l.registerCommands(d,{indentlist:new f(d,"indentlist",!0),outdentlist:new f(d,"outdentlist")});CKEDITOR.tools.extend(f.prototype,l.specificDefinition.prototype,{context:{ol:1,ul:1}})}});CKEDITOR.plugins.indentList={};CKEDITOR.plugins.indentList.firstItemInPath=function(d,f,l){var q=f.contains(t);l||(l=f.contains(d));return l&&q&&q.equals(l.getFirst(t))}})();CKEDITOR.plugins.add("dialogui",{onLoad:function(){var h=function(b){this._||(this._={});this._["default"]=this._.initValue=b["default"]||"";this._.required=b.required||!1;for(var a=[this._],d=1;d<arguments.length;d++)a.push(arguments[d]);a.push(!0);CKEDITOR.tools.extend.apply(CKEDITOR.tools,a);return this._},v={build:function(b,a,d){return new CKEDITOR.ui.dialog.textInput(b,a,d)}},n={build:function(b,a,d){return new CKEDITOR.ui.dialog[a.type](b,a,d)}},q={isChanged:function(){return this.getValue()!=
+this.getInitValue()},reset:function(b){this.setValue(this.getInitValue(),b)},setInitValue:function(){this._.initValue=this.getValue()},resetInitValue:function(){this._.initValue=this._["default"]},getInitValue:function(){return this._.initValue}},r=CKEDITOR.tools.extend({},CKEDITOR.ui.dialog.uiElement.prototype.eventProcessors,{onChange:function(b,a){this._.domOnChangeRegistered||(b.on("load",function(){this.getInputElement().on("change",function(){b.parts.dialog.isVisible()&&this.fire("change",{value:this.getValue()})},
+this)},this),this._.domOnChangeRegistered=!0);this.on("change",a)}},!0),x=/^on([A-Z]\w+)/,t=function(b){for(var a in b)(x.test(a)||"title"==a||"type"==a)&&delete b[a];return b},w=function(b){b=b.data.getKeystroke();b==CKEDITOR.SHIFT+CKEDITOR.ALT+36?this.setDirectionMarker("ltr"):b==CKEDITOR.SHIFT+CKEDITOR.ALT+35&&this.setDirectionMarker("rtl")};CKEDITOR.tools.extend(CKEDITOR.ui.dialog,{labeledElement:function(b,a,d,f){if(!(4>arguments.length)){var c=h.call(this,a);c.labelId=CKEDITOR.tools.getNextId()+
+"_label";this._.children=[];var e={role:a.role||"presentation"};a.includeLabel&&(e["aria-labelledby"]=c.labelId);CKEDITOR.ui.dialog.uiElement.call(this,b,a,d,"div",null,e,function(){var e=[],g=a.required?" cke_required":"";"horizontal"!=a.labelLayout?e.push('\x3clabel class\x3d"cke_dialog_ui_labeled_label'+g+'" ',' id\x3d"'+c.labelId+'"',c.inputId?' for\x3d"'+c.inputId+'"':"",(a.labelStyle?' style\x3d"'+a.labelStyle+'"':"")+"\x3e",a.label,"\x3c/label\x3e",'\x3cdiv class\x3d"cke_dialog_ui_labeled_content"',
+a.controlStyle?' style\x3d"'+a.controlStyle+'"':"",' role\x3d"presentation"\x3e',f.call(this,b,a),"\x3c/div\x3e"):(g={type:"hbox",widths:a.widths,padding:0,children:[{type:"html",html:'\x3clabel class\x3d"cke_dialog_ui_labeled_label'+g+'" id\x3d"'+c.labelId+'" for\x3d"'+c.inputId+'"'+(a.labelStyle?' style\x3d"'+a.labelStyle+'"':"")+"\x3e"+CKEDITOR.tools.htmlEncode(a.label)+"\x3c/label\x3e"},{type:"html",html:'\x3cspan class\x3d"cke_dialog_ui_labeled_content"'+(a.controlStyle?' style\x3d"'+a.controlStyle+
+'"':"")+"\x3e"+f.call(this,b,a)+"\x3c/span\x3e"}]},CKEDITOR.dialog._.uiElementBuilders.hbox.build(b,g,e));return e.join("")})}},textInput:function(b,a,d){if(!(3>arguments.length)){h.call(this,a);var f=this._.inputId=CKEDITOR.tools.getNextId()+"_textInput",c={"class":"cke_dialog_ui_input_"+a.type,id:f,type:a.type};a.validate&&(this.validate=a.validate);a.maxLength&&(c.maxlength=a.maxLength);a.size&&(c.size=a.size);a.inputStyle&&(c.style=a.inputStyle);var e=this,m=!1;b.on("load",function(){e.getInputElement().on("keydown",
+function(a){13==a.data.getKeystroke()&&(m=!0)});e.getInputElement().on("keyup",function(a){13==a.data.getKeystroke()&&m&&(b.getButton("ok")&&setTimeout(function(){b.getButton("ok").click()},0),m=!1);e.bidi&&w.call(e,a)},null,null,1E3)});CKEDITOR.ui.dialog.labeledElement.call(this,b,a,d,function(){var b=['\x3cdiv class\x3d"cke_dialog_ui_input_',a.type,'" role\x3d"presentation"'];a.width&&b.push('style\x3d"width:'+a.width+'" ');b.push("\x3e\x3cinput ");c["aria-labelledby"]=this._.labelId;this._.required&&
+(c["aria-required"]=this._.required);for(var e in c)b.push(e+'\x3d"'+c[e]+'" ');b.push(" /\x3e\x3c/div\x3e");return b.join("")})}},textarea:function(b,a,d){if(!(3>arguments.length)){h.call(this,a);var f=this,c=this._.inputId=CKEDITOR.tools.getNextId()+"_textarea",e={};a.validate&&(this.validate=a.validate);e.rows=a.rows||5;e.cols=a.cols||20;e["class"]="cke_dialog_ui_input_textarea "+(a["class"]||"");"undefined"!=typeof a.inputStyle&&(e.style=a.inputStyle);a.dir&&(e.dir=a.dir);if(f.bidi)b.on("load",
+function(){f.getInputElement().on("keyup",w)},f);CKEDITOR.ui.dialog.labeledElement.call(this,b,a,d,function(){e["aria-labelledby"]=this._.labelId;this._.required&&(e["aria-required"]=this._.required);var a=['\x3cdiv class\x3d"cke_dialog_ui_input_textarea" role\x3d"presentation"\x3e\x3ctextarea id\x3d"',c,'" '],b;for(b in e)a.push(b+'\x3d"'+CKEDITOR.tools.htmlEncode(e[b])+'" ');a.push("\x3e",CKEDITOR.tools.htmlEncode(f._["default"]),"\x3c/textarea\x3e\x3c/div\x3e");return a.join("")})}},checkbox:function(b,
+a,d){if(!(3>arguments.length)){var f=h.call(this,a,{"default":!!a["default"]});a.validate&&(this.validate=a.validate);CKEDITOR.ui.dialog.uiElement.call(this,b,a,d,"span",null,null,function(){var c=CKEDITOR.tools.extend({},a,{id:a.id?a.id+"_checkbox":CKEDITOR.tools.getNextId()+"_checkbox"},!0),e=[],d=CKEDITOR.tools.getNextId()+"_label",g={"class":"cke_dialog_ui_checkbox_input",type:"checkbox","aria-labelledby":d};t(c);a["default"]&&(g.checked="checked");"undefined"!=typeof c.inputStyle&&(c.style=c.inputStyle);
+f.checkbox=new CKEDITOR.ui.dialog.uiElement(b,c,e,"input",null,g);e.push(' \x3clabel id\x3d"',d,'" for\x3d"',g.id,'"'+(a.labelStyle?' style\x3d"'+a.labelStyle+'"':"")+"\x3e",CKEDITOR.tools.htmlEncode(a.label),"\x3c/label\x3e");return e.join("")})}},radio:function(b,a,d){if(!(3>arguments.length)){h.call(this,a);this._["default"]||(this._["default"]=this._.initValue=a.items[0][1]);a.validate&&(this.validate=a.validate);var f=[],c=this;a.role="radiogroup";a.includeLabel=!0;CKEDITOR.ui.dialog.labeledElement.call(this,
+b,a,d,function(){for(var e=[],d=[],g=(a.id?a.id:CKEDITOR.tools.getNextId())+"_radio",k=0;k<a.items.length;k++){var l=a.items[k],h=void 0!==l[2]?l[2]:l[0],n=void 0!==l[1]?l[1]:l[0],p=CKEDITOR.tools.getNextId()+"_radio_input",q=p+"_label",p=CKEDITOR.tools.extend({},a,{id:p,title:null,type:null},!0),h=CKEDITOR.tools.extend({},p,{title:h},!0),r={type:"radio","class":"cke_dialog_ui_radio_input",name:g,value:n,"aria-labelledby":q},u=[];c._["default"]==n&&(r.checked="checked");t(p);t(h);"undefined"!=typeof p.inputStyle&&
+(p.style=p.inputStyle);p.keyboardFocusable=!0;f.push(new CKEDITOR.ui.dialog.uiElement(b,p,u,"input",null,r));u.push(" ");new CKEDITOR.ui.dialog.uiElement(b,h,u,"label",null,{id:q,"for":r.id},l[0]);e.push(u.join(""))}new CKEDITOR.ui.dialog.hbox(b,f,e,d);return d.join("")});this._.children=f}},button:function(b,a,d){if(arguments.length){"function"==typeof a&&(a=a(b.getParentEditor()));h.call(this,a,{disabled:a.disabled||!1});CKEDITOR.event.implementOn(this);var f=this;b.on("load",function(){var a=this.getElement();
+(function(){a.on("click",function(a){f.click();a.data.preventDefault()});a.on("keydown",function(a){a.data.getKeystroke()in{32:1}&&(f.click(),a.data.preventDefault())})})();a.unselectable()},this);var c=CKEDITOR.tools.extend({},a);delete c.style;var e=CKEDITOR.tools.getNextId()+"_label";CKEDITOR.ui.dialog.uiElement.call(this,b,c,d,"a",null,{style:a.style,href:"javascript:void(0)",title:a.label,hidefocus:"true","class":a["class"],role:"button","aria-labelledby":e},'\x3cspan id\x3d"'+e+'" class\x3d"cke_dialog_ui_button"\x3e'+
+CKEDITOR.tools.htmlEncode(a.label)+"\x3c/span\x3e")}},select:function(b,a,d){if(!(3>arguments.length)){var f=h.call(this,a);a.validate&&(this.validate=a.validate);f.inputId=CKEDITOR.tools.getNextId()+"_select";CKEDITOR.ui.dialog.labeledElement.call(this,b,a,d,function(){var c=CKEDITOR.tools.extend({},a,{id:a.id?a.id+"_select":CKEDITOR.tools.getNextId()+"_select"},!0),e=[],d=[],g={id:f.inputId,"class":"cke_dialog_ui_input_select","aria-labelledby":this._.labelId};e.push('\x3cdiv class\x3d"cke_dialog_ui_input_',
+a.type,'" role\x3d"presentation"');a.width&&e.push('style\x3d"width:'+a.width+'" ');e.push("\x3e");void 0!==a.size&&(g.size=a.size);void 0!==a.multiple&&(g.multiple=a.multiple);t(c);for(var k=0,l;k<a.items.length&&(l=a.items[k]);k++)d.push('\x3coption value\x3d"',CKEDITOR.tools.htmlEncode(void 0!==l[1]?l[1]:l[0]).replace(/"/g,"\x26quot;"),'" /\x3e ',CKEDITOR.tools.htmlEncode(l[0]));"undefined"!=typeof c.inputStyle&&(c.style=c.inputStyle);f.select=new CKEDITOR.ui.dialog.uiElement(b,c,e,"select",null,
+g,d.join(""));e.push("\x3c/div\x3e");return e.join("")})}},file:function(b,a,d){if(!(3>arguments.length)){void 0===a["default"]&&(a["default"]="");var f=CKEDITOR.tools.extend(h.call(this,a),{definition:a,buttons:[]});a.validate&&(this.validate=a.validate);b.on("load",function(){CKEDITOR.document.getById(f.frameId).getParent().addClass("cke_dialog_ui_input_file")});CKEDITOR.ui.dialog.labeledElement.call(this,b,a,d,function(){f.frameId=CKEDITOR.tools.getNextId()+"_fileInput";var b=['\x3ciframe frameborder\x3d"0" allowtransparency\x3d"0" class\x3d"cke_dialog_ui_input_file" role\x3d"presentation" id\x3d"',
+f.frameId,'" title\x3d"',a.label,'" src\x3d"javascript:void('];b.push(CKEDITOR.env.ie?"(function(){"+encodeURIComponent("document.open();("+CKEDITOR.tools.fixDomain+")();document.close();")+"})()":"0");b.push(')"\x3e\x3c/iframe\x3e');return b.join("")})}},fileButton:function(b,a,d){var f=this;if(!(3>arguments.length)){h.call(this,a);a.validate&&(this.validate=a.validate);var c=CKEDITOR.tools.extend({},a),e=c.onClick;c.className=(c.className?c.className+" ":"")+"cke_dialog_ui_button";c.onClick=function(c){var d=
+a["for"];e&&!1===e.call(this,c)||(b.getContentElement(d[0],d[1]).submit(),this.disable())};b.on("load",function(){b.getContentElement(a["for"][0],a["for"][1])._.buttons.push(f)});CKEDITOR.ui.dialog.button.call(this,b,c,d)}},html:function(){var b=/^\s*<[\w:]+\s+([^>]*)?>/,a=/^(\s*<[\w:]+(?:\s+[^>]*)?)((?:.|\r|\n)+)$/,d=/\/$/;return function(f,c,e){if(!(3>arguments.length)){var m=[],g=c.html;"\x3c"!=g.charAt(0)&&(g="\x3cspan\x3e"+g+"\x3c/span\x3e");var k=c.focus;if(k){var l=this.focus;this.focus=function(){("function"==
+typeof k?k:l).call(this);this.fire("focus")};c.isFocusable&&(this.isFocusable=this.isFocusable);this.keyboardFocusable=!0}CKEDITOR.ui.dialog.uiElement.call(this,f,c,m,"span",null,null,"");m=m.join("").match(b);g=g.match(a)||["","",""];d.test(g[1])&&(g[1]=g[1].slice(0,-1),g[2]="/"+g[2]);e.push([g[1]," ",m[1]||"",g[2]].join(""))}}}(),fieldset:function(b,a,d,f,c){var e=c.label;this._={children:a};CKEDITOR.ui.dialog.uiElement.call(this,b,c,f,"fieldset",null,null,function(){var a=[];e&&a.push("\x3clegend"+
+(c.labelStyle?' style\x3d"'+c.labelStyle+'"':"")+"\x3e"+e+"\x3c/legend\x3e");for(var b=0;b<d.length;b++)a.push(d[b]);return a.join("")})}},!0);CKEDITOR.ui.dialog.html.prototype=new CKEDITOR.ui.dialog.uiElement;CKEDITOR.ui.dialog.labeledElement.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.uiElement,{setLabel:function(b){var a=CKEDITOR.document.getById(this._.labelId);1>a.getChildCount()?(new CKEDITOR.dom.text(b,CKEDITOR.document)).appendTo(a):a.getChild(0).$.nodeValue=b;return this},getLabel:function(){var b=
+CKEDITOR.document.getById(this._.labelId);return!b||1>b.getChildCount()?"":b.getChild(0).getText()},eventProcessors:r},!0);CKEDITOR.ui.dialog.button.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.uiElement,{click:function(){return this._.disabled?!1:this.fire("click",{dialog:this._.dialog})},enable:function(){this._.disabled=!1;var b=this.getElement();b&&b.removeClass("cke_disabled")},disable:function(){this._.disabled=!0;this.getElement().addClass("cke_disabled")},isVisible:function(){return this.getElement().getFirst().isVisible()},
+isEnabled:function(){return!this._.disabled},eventProcessors:CKEDITOR.tools.extend({},CKEDITOR.ui.dialog.uiElement.prototype.eventProcessors,{onClick:function(b,a){this.on("click",function(){a.apply(this,arguments)})}},!0),accessKeyUp:function(){this.click()},accessKeyDown:function(){this.focus()},keyboardFocusable:!0},!0);CKEDITOR.ui.dialog.textInput.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.labeledElement,{getInputElement:function(){return CKEDITOR.document.getById(this._.inputId)},
+focus:function(){var b=this.selectParentTab();setTimeout(function(){var a=b.getInputElement();a&&a.$.focus()},0)},select:function(){var b=this.selectParentTab();setTimeout(function(){var a=b.getInputElement();a&&(a.$.focus(),a.$.select())},0)},accessKeyUp:function(){this.select()},setValue:function(b){if(this.bidi){var a=b&&b.charAt(0);(a="‪"==a?"ltr":"‫"==a?"rtl":null)&&(b=b.slice(1));this.setDirectionMarker(a)}b||(b="");return CKEDITOR.ui.dialog.uiElement.prototype.setValue.apply(this,arguments)},
+getValue:function(){var b=CKEDITOR.ui.dialog.uiElement.prototype.getValue.call(this);if(this.bidi&&b){var a=this.getDirectionMarker();a&&(b=("ltr"==a?"‪":"‫")+b)}return b},setDirectionMarker:function(b){var a=this.getInputElement();b?a.setAttributes({dir:b,"data-cke-dir-marker":b}):this.getDirectionMarker()&&a.removeAttributes(["dir","data-cke-dir-marker"])},getDirectionMarker:function(){return this.getInputElement().data("cke-dir-marker")},keyboardFocusable:!0},q,!0);CKEDITOR.ui.dialog.textarea.prototype=
+new CKEDITOR.ui.dialog.textInput;CKEDITOR.ui.dialog.select.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.labeledElement,{getInputElement:function(){return this._.select.getElement()},add:function(b,a,d){var f=new CKEDITOR.dom.element("option",this.getDialog().getParentEditor().document),c=this.getInputElement().$;f.$.text=b;f.$.value=void 0===a||null===a?b:a;void 0===d||null===d?CKEDITOR.env.ie?c.add(f.$):c.add(f.$,null):c.add(f.$,d);return this},remove:function(b){this.getInputElement().$.remove(b);
+return this},clear:function(){for(var b=this.getInputElement().$;0<b.length;)b.remove(0);return this},keyboardFocusable:!0},q,!0);CKEDITOR.ui.dialog.checkbox.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.uiElement,{getInputElement:function(){return this._.checkbox.getElement()},setValue:function(b,a){this.getInputElement().$.checked=b;!a&&this.fire("change",{value:b})},getValue:function(){return this.getInputElement().$.checked},accessKeyUp:function(){this.setValue(!this.getValue())},eventProcessors:{onChange:function(b,
+a){if(!CKEDITOR.env.ie||8<CKEDITOR.env.version)return r.onChange.apply(this,arguments);b.on("load",function(){var a=this._.checkbox.getElement();a.on("propertychange",function(b){b=b.data.$;"checked"==b.propertyName&&this.fire("change",{value:a.$.checked})},this)},this);this.on("change",a);return null}},keyboardFocusable:!0},q,!0);CKEDITOR.ui.dialog.radio.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.uiElement,{setValue:function(b,a){for(var d=this._.children,f,c=0;c<d.length&&(f=d[c]);c++)f.getElement().$.checked=
+f.getValue()==b;!a&&this.fire("change",{value:b})},getValue:function(){for(var b=this._.children,a=0;a<b.length;a++)if(b[a].getElement().$.checked)return b[a].getValue();return null},accessKeyUp:function(){var b=this._.children,a;for(a=0;a<b.length;a++)if(b[a].getElement().$.checked){b[a].getElement().focus();return}b[0].getElement().focus()},eventProcessors:{onChange:function(b,a){if(!CKEDITOR.env.ie||8<CKEDITOR.env.version)return r.onChange.apply(this,arguments);b.on("load",function(){for(var a=
+this._.children,b=this,c=0;c<a.length;c++)a[c].getElement().on("propertychange",function(a){a=a.data.$;"checked"==a.propertyName&&this.$.checked&&b.fire("change",{value:this.getAttribute("value")})})},this);this.on("change",a);return null}}},q,!0);CKEDITOR.ui.dialog.file.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.labeledElement,q,{getInputElement:function(){var b=CKEDITOR.document.getById(this._.frameId).getFrameDocument();return 0<b.$.forms.length?new CKEDITOR.dom.element(b.$.forms[0].elements[0]):
+this.getElement()},submit:function(){this.getInputElement().getParent().$.submit();return this},getAction:function(){return this.getInputElement().getParent().$.action},registerEvents:function(b){var a=/^on([A-Z]\w+)/,d,f=function(a,b,c,d){a.on("formLoaded",function(){a.getInputElement().on(c,d,a)})},c;for(c in b)if(d=c.match(a))this.eventProcessors[c]?this.eventProcessors[c].call(this,this._.dialog,b[c]):f(this,this._.dialog,d[1].toLowerCase(),b[c]);return this},reset:function(){function b(){d.$.open();
+var b="";f.size&&(b=f.size-(CKEDITOR.env.ie?7:0));var h=a.frameId+"_input";d.$.write(['\x3chtml dir\x3d"'+g+'" lang\x3d"'+k+'"\x3e\x3chead\x3e\x3ctitle\x3e\x3c/title\x3e\x3c/head\x3e\x3cbody style\x3d"margin: 0; overflow: hidden; background: transparent;"\x3e','\x3cform enctype\x3d"multipart/form-data" method\x3d"POST" dir\x3d"'+g+'" lang\x3d"'+k+'" action\x3d"',CKEDITOR.tools.htmlEncode(f.action),'"\x3e\x3clabel id\x3d"',a.labelId,'" for\x3d"',h,'" style\x3d"display:none"\x3e',CKEDITOR.tools.htmlEncode(f.label),
+'\x3c/label\x3e\x3cinput style\x3d"width:100%" id\x3d"',h,'" aria-labelledby\x3d"',a.labelId,'" type\x3d"file" name\x3d"',CKEDITOR.tools.htmlEncode(f.id||"cke_upload"),'" size\x3d"',CKEDITOR.tools.htmlEncode(0<b?b:""),'" /\x3e\x3c/form\x3e\x3c/body\x3e\x3c/html\x3e\x3cscript\x3e',CKEDITOR.env.ie?"("+CKEDITOR.tools.fixDomain+")();":"","window.parent.CKEDITOR.tools.callFunction("+e+");","window.onbeforeunload \x3d function() {window.parent.CKEDITOR.tools.callFunction("+m+")}","\x3c/script\x3e"].join(""));
+d.$.close();for(b=0;b<c.length;b++)c[b].enable()}var a=this._,d=CKEDITOR.document.getById(a.frameId).getFrameDocument(),f=a.definition,c=a.buttons,e=this.formLoadedNumber,m=this.formUnloadNumber,g=a.dialog._.editor.lang.dir,k=a.dialog._.editor.langCode;e||(e=this.formLoadedNumber=CKEDITOR.tools.addFunction(function(){this.fire("formLoaded")},this),m=this.formUnloadNumber=CKEDITOR.tools.addFunction(function(){this.getInputElement().clearCustomData()},this),this.getDialog()._.editor.on("destroy",function(){CKEDITOR.tools.removeFunction(e);
+CKEDITOR.tools.removeFunction(m)}));CKEDITOR.env.gecko?setTimeout(b,500):b()},getValue:function(){return this.getInputElement().$.value||""},setInitValue:function(){this._.initValue=""},eventProcessors:{onChange:function(b,a){this._.domOnChangeRegistered||(this.on("formLoaded",function(){this.getInputElement().on("change",function(){this.fire("change",{value:this.getValue()})},this)},this),this._.domOnChangeRegistered=!0);this.on("change",a)}},keyboardFocusable:!0},!0);CKEDITOR.ui.dialog.fileButton.prototype=
+new CKEDITOR.ui.dialog.button;CKEDITOR.ui.dialog.fieldset.prototype=CKEDITOR.tools.clone(CKEDITOR.ui.dialog.hbox.prototype);CKEDITOR.dialog.addUIElement("text",v);CKEDITOR.dialog.addUIElement("password",v);CKEDITOR.dialog.addUIElement("textarea",n);CKEDITOR.dialog.addUIElement("checkbox",n);CKEDITOR.dialog.addUIElement("radio",n);CKEDITOR.dialog.addUIElement("button",n);CKEDITOR.dialog.addUIElement("select",n);CKEDITOR.dialog.addUIElement("file",n);CKEDITOR.dialog.addUIElement("fileButton",n);CKEDITOR.dialog.addUIElement("html",
+n);CKEDITOR.dialog.addUIElement("fieldset",{build:function(b,a,d){for(var f=a.children,c,e=[],h=[],g=0;g<f.length&&(c=f[g]);g++){var k=[];e.push(k);h.push(CKEDITOR.dialog._.uiElementBuilders[c.type].build(b,c,k))}return new CKEDITOR.ui.dialog[a.type](b,h,e,d,a)}})}});CKEDITOR.DIALOG_RESIZE_NONE=0;CKEDITOR.DIALOG_RESIZE_WIDTH=1;CKEDITOR.DIALOG_RESIZE_HEIGHT=2;CKEDITOR.DIALOG_RESIZE_BOTH=3;CKEDITOR.DIALOG_STATE_IDLE=1;CKEDITOR.DIALOG_STATE_BUSY=2;
+(function(){function x(){for(var a=this._.tabIdList.length,b=CKEDITOR.tools.indexOf(this._.tabIdList,this._.currentTabId)+a,c=b-1;c>b-a;c--)if(this._.tabs[this._.tabIdList[c%a]][0].$.offsetHeight)return this._.tabIdList[c%a];return null}function A(){for(var a=this._.tabIdList.length,b=CKEDITOR.tools.indexOf(this._.tabIdList,this._.currentTabId),c=b+1;c<b+a;c++)if(this._.tabs[this._.tabIdList[c%a]][0].$.offsetHeight)return this._.tabIdList[c%a];return null}function K(a,b){for(var c=a.$.getElementsByTagName("input"),
+e=0,d=c.length;e<d;e++){var f=new CKEDITOR.dom.element(c[e]);"text"==f.getAttribute("type").toLowerCase()&&(b?(f.setAttribute("value",f.getCustomData("fake_value")||""),f.removeCustomData("fake_value")):(f.setCustomData("fake_value",f.getAttribute("value")),f.setAttribute("value","")))}}function T(a,b){var c=this.getInputElement();c&&(a?c.removeAttribute("aria-invalid"):c.setAttribute("aria-invalid",!0));a||(this.select?this.select():this.focus());b&&alert(b);this.fire("validated",{valid:a,msg:b})}
+function U(){var a=this.getInputElement();a&&a.removeAttribute("aria-invalid")}function V(a){var b=CKEDITOR.dom.element.createFromHtml(CKEDITOR.addTemplate("dialog",W).output({id:CKEDITOR.tools.getNextNumber(),editorId:a.id,langDir:a.lang.dir,langCode:a.langCode,editorDialogClass:"cke_editor_"+a.name.replace(/\./g,"\\.")+"_dialog",closeTitle:a.lang.common.close,hidpi:CKEDITOR.env.hidpi?"cke_hidpi":""})),c=b.getChild([0,0,0,0,0]),e=c.getChild(0),d=c.getChild(1);a.plugins.clipboard&&CKEDITOR.plugins.clipboard.preventDefaultDropOnElement(c);
+!CKEDITOR.env.ie||CKEDITOR.env.quirks||CKEDITOR.env.edge||(a="javascript:void(function(){"+encodeURIComponent("document.open();("+CKEDITOR.tools.fixDomain+")();document.close();")+"}())",CKEDITOR.dom.element.createFromHtml('\x3ciframe frameBorder\x3d"0" class\x3d"cke_iframe_shim" src\x3d"'+a+'" tabIndex\x3d"-1"\x3e\x3c/iframe\x3e').appendTo(c.getParent()));e.unselectable();d.unselectable();return{element:b,parts:{dialog:b.getChild(0),title:e,close:d,tabs:c.getChild(2),contents:c.getChild([3,0,0,0]),
+footer:c.getChild([3,0,1,0])}}}function L(a,b,c){this.element=b;this.focusIndex=c;this.tabIndex=0;this.isFocusable=function(){return!b.getAttribute("disabled")&&b.isVisible()};this.focus=function(){a._.currentFocusIndex=this.focusIndex;this.element.focus()};b.on("keydown",function(a){a.data.getKeystroke()in{32:1,13:1}&&this.fire("click")});b.on("focus",function(){this.fire("mouseover")});b.on("blur",function(){this.fire("mouseout")})}function X(a){function b(){a.layout()}var c=CKEDITOR.document.getWindow();
+c.on("resize",b);a.on("hide",function(){c.removeListener("resize",b)})}function M(a,b){this._={dialog:a};CKEDITOR.tools.extend(this,b)}function Y(a){function b(b){var c=a.getSize(),k=CKEDITOR.document.getWindow().getViewPaneSize(),q=b.data.$.screenX,n=b.data.$.screenY,r=q-e.x,l=n-e.y;e={x:q,y:n};d.x+=r;d.y+=l;a.move(d.x+h[3]<g?-h[3]:d.x-h[1]>k.width-c.width-g?k.width-c.width+("rtl"==f.lang.dir?0:h[1]):d.x,d.y+h[0]<g?-h[0]:d.y-h[2]>k.height-c.height-g?k.height-c.height+h[2]:d.y,1);b.data.preventDefault()}
+function c(){CKEDITOR.document.removeListener("mousemove",b);CKEDITOR.document.removeListener("mouseup",c);if(CKEDITOR.env.ie6Compat){var a=u.getChild(0).getFrameDocument();a.removeListener("mousemove",b);a.removeListener("mouseup",c)}}var e=null,d=null,f=a.getParentEditor(),g=f.config.dialog_magnetDistance,h=CKEDITOR.skin.margins||[0,0,0,0];"undefined"==typeof g&&(g=20);a.parts.title.on("mousedown",function(g){e={x:g.data.$.screenX,y:g.data.$.screenY};CKEDITOR.document.on("mousemove",b);CKEDITOR.document.on("mouseup",
+c);d=a.getPosition();if(CKEDITOR.env.ie6Compat){var f=u.getChild(0).getFrameDocument();f.on("mousemove",b);f.on("mouseup",c)}g.data.preventDefault()},a)}function Z(a){function b(b){var c="rtl"==f.lang.dir,n=k.width,q=k.height,G=n+(b.data.$.screenX-m.x)*(c?-1:1)*(a._.moved?1:2),H=q+(b.data.$.screenY-m.y)*(a._.moved?1:2),B=a._.element.getFirst(),B=c&&B.getComputedStyle("right"),C=a.getPosition();C.y+H>p.height&&(H=p.height-C.y);(c?B:C.x)+G>p.width&&(G=p.width-(c?B:C.x));if(d==CKEDITOR.DIALOG_RESIZE_WIDTH||
+d==CKEDITOR.DIALOG_RESIZE_BOTH)n=Math.max(e.minWidth||0,G-g);if(d==CKEDITOR.DIALOG_RESIZE_HEIGHT||d==CKEDITOR.DIALOG_RESIZE_BOTH)q=Math.max(e.minHeight||0,H-h);a.resize(n,q);a._.moved||a.layout();b.data.preventDefault()}function c(){CKEDITOR.document.removeListener("mouseup",c);CKEDITOR.document.removeListener("mousemove",b);q&&(q.remove(),q=null);if(CKEDITOR.env.ie6Compat){var a=u.getChild(0).getFrameDocument();a.removeListener("mouseup",c);a.removeListener("mousemove",b)}}var e=a.definition,d=e.resizable;
+if(d!=CKEDITOR.DIALOG_RESIZE_NONE){var f=a.getParentEditor(),g,h,p,m,k,q,n=CKEDITOR.tools.addFunction(function(d){k=a.getSize();var e=a.parts.contents;e.$.getElementsByTagName("iframe").length&&(q=CKEDITOR.dom.element.createFromHtml('\x3cdiv class\x3d"cke_dialog_resize_cover" style\x3d"height: 100%; position: absolute; width: 100%;"\x3e\x3c/div\x3e'),e.append(q));h=k.height-a.parts.contents.getSize("height",!(CKEDITOR.env.gecko||CKEDITOR.env.ie&&CKEDITOR.env.quirks));g=k.width-a.parts.contents.getSize("width",
+1);m={x:d.screenX,y:d.screenY};p=CKEDITOR.document.getWindow().getViewPaneSize();CKEDITOR.document.on("mousemove",b);CKEDITOR.document.on("mouseup",c);CKEDITOR.env.ie6Compat&&(e=u.getChild(0).getFrameDocument(),e.on("mousemove",b),e.on("mouseup",c));d.preventDefault&&d.preventDefault()});a.on("load",function(){var b="";d==CKEDITOR.DIALOG_RESIZE_WIDTH?b=" cke_resizer_horizontal":d==CKEDITOR.DIALOG_RESIZE_HEIGHT&&(b=" cke_resizer_vertical");b=CKEDITOR.dom.element.createFromHtml('\x3cdiv class\x3d"cke_resizer'+
+b+" cke_resizer_"+f.lang.dir+'" title\x3d"'+CKEDITOR.tools.htmlEncode(f.lang.common.resize)+'" onmousedown\x3d"CKEDITOR.tools.callFunction('+n+', event )"\x3e'+("ltr"==f.lang.dir?"◢":"◣")+"\x3c/div\x3e");a.parts.footer.append(b,1)});f.on("destroy",function(){CKEDITOR.tools.removeFunction(n)})}}function I(a){a.data.preventDefault(1)}function N(a){var b=CKEDITOR.document.getWindow(),c=a.config,e=CKEDITOR.skinName||a.config.skin,d=c.dialog_backgroundCoverColor||("moono-lisa"==e?"black":"white"),e=c.dialog_backgroundCoverOpacity,
+f=c.baseFloatZIndex,c=CKEDITOR.tools.genKey(d,e,f),g=z[c];g?g.show():(f=['\x3cdiv tabIndex\x3d"-1" style\x3d"position: ',CKEDITOR.env.ie6Compat?"absolute":"fixed","; z-index: ",f,"; top: 0px; left: 0px; ",CKEDITOR.env.ie6Compat?"":"background-color: "+d,'" class\x3d"cke_dialog_background_cover"\x3e'],CKEDITOR.env.ie6Compat&&(d="\x3chtml\x3e\x3cbody style\x3d\\'background-color:"+d+";\\'\x3e\x3c/body\x3e\x3c/html\x3e",f.push('\x3ciframe hidefocus\x3d"true" frameborder\x3d"0" id\x3d"cke_dialog_background_iframe" src\x3d"javascript:'),
+f.push("void((function(){"+encodeURIComponent("document.open();("+CKEDITOR.tools.fixDomain+")();document.write( '"+d+"' );document.close();")+"})())"),f.push('" style\x3d"position:absolute;left:0;top:0;width:100%;height: 100%;filter: progid:DXImageTransform.Microsoft.Alpha(opacity\x3d0)"\x3e\x3c/iframe\x3e')),f.push("\x3c/div\x3e"),g=CKEDITOR.dom.element.createFromHtml(f.join("")),g.setOpacity(void 0!==e?e:.5),g.on("keydown",I),g.on("keypress",I),g.on("keyup",I),g.appendTo(CKEDITOR.document.getBody()),
+z[c]=g);a.focusManager.add(g);u=g;a=function(){var a=b.getViewPaneSize();g.setStyles({width:a.width+"px",height:a.height+"px"})};var h=function(){var a=b.getScrollPosition(),c=CKEDITOR.dialog._.currentTop;g.setStyles({left:a.x+"px",top:a.y+"px"});if(c){do a=c.getPosition(),c.move(a.x,a.y);while(c=c._.parentDialog)}};J=a;b.on("resize",a);a();CKEDITOR.env.mac&&CKEDITOR.env.webkit||g.focus();if(CKEDITOR.env.ie6Compat){var p=function(){h();arguments.callee.prevScrollHandler.apply(this,arguments)};b.$.setTimeout(function(){p.prevScrollHandler=
+window.onscroll||function(){};window.onscroll=p},0);h()}}function O(a){u&&(a.focusManager.remove(u),a=CKEDITOR.document.getWindow(),u.hide(),a.removeListener("resize",J),CKEDITOR.env.ie6Compat&&a.$.setTimeout(function(){window.onscroll=window.onscroll&&window.onscroll.prevScrollHandler||null},0),J=null)}var v=CKEDITOR.tools.cssLength,W='\x3cdiv class\x3d"cke_reset_all {editorId} {editorDialogClass} {hidpi}" dir\x3d"{langDir}" lang\x3d"{langCode}" role\x3d"dialog" aria-labelledby\x3d"cke_dialog_title_{id}"\x3e\x3ctable class\x3d"cke_dialog '+
+CKEDITOR.env.cssClass+' cke_{langDir}" style\x3d"position:absolute" role\x3d"presentation"\x3e\x3ctr\x3e\x3ctd role\x3d"presentation"\x3e\x3cdiv class\x3d"cke_dialog_body" role\x3d"presentation"\x3e\x3cdiv id\x3d"cke_dialog_title_{id}" class\x3d"cke_dialog_title" role\x3d"presentation"\x3e\x3c/div\x3e\x3ca id\x3d"cke_dialog_close_button_{id}" class\x3d"cke_dialog_close_button" href\x3d"javascript:void(0)" title\x3d"{closeTitle}" role\x3d"button"\x3e\x3cspan class\x3d"cke_label"\x3eX\x3c/span\x3e\x3c/a\x3e\x3cdiv id\x3d"cke_dialog_tabs_{id}" class\x3d"cke_dialog_tabs" role\x3d"tablist"\x3e\x3c/div\x3e\x3ctable class\x3d"cke_dialog_contents" role\x3d"presentation"\x3e\x3ctr\x3e\x3ctd id\x3d"cke_dialog_contents_{id}" class\x3d"cke_dialog_contents_body" role\x3d"presentation"\x3e\x3c/td\x3e\x3c/tr\x3e\x3ctr\x3e\x3ctd id\x3d"cke_dialog_footer_{id}" class\x3d"cke_dialog_footer" role\x3d"presentation"\x3e\x3c/td\x3e\x3c/tr\x3e\x3c/table\x3e\x3c/div\x3e\x3c/td\x3e\x3c/tr\x3e\x3c/table\x3e\x3c/div\x3e';
+CKEDITOR.dialog=function(a,b){function c(){var a=l._.focusList;a.sort(function(a,b){return a.tabIndex!=b.tabIndex?b.tabIndex-a.tabIndex:a.focusIndex-b.focusIndex});for(var b=a.length,c=0;c<b;c++)a[c].focusIndex=c}function e(a){var b=l._.focusList;a=a||0;if(!(1>b.length)){var c=l._.currentFocusIndex;l._.tabBarMode&&0>a&&(c=0);try{b[c].getInputElement().$.blur()}catch(d){}var e=c,g=1<l._.pageCount;do{e+=a;if(g&&!l._.tabBarMode&&(e==b.length||-1==e)){l._.tabBarMode=!0;l._.tabs[l._.currentTabId][0].focus();
+l._.currentFocusIndex=-1;return}e=(e+b.length)%b.length;if(e==c)break}while(a&&!b[e].isFocusable());b[e].focus();"text"==b[e].type&&b[e].select()}}function d(b){if(l==CKEDITOR.dialog._.currentTop){var c=b.data.getKeystroke(),d="rtl"==a.lang.dir,g=[37,38,39,40];q=n=0;if(9==c||c==CKEDITOR.SHIFT+9)e(c==CKEDITOR.SHIFT+9?-1:1),q=1;else if(c==CKEDITOR.ALT+121&&!l._.tabBarMode&&1<l.getPageCount())l._.tabBarMode=!0,l._.tabs[l._.currentTabId][0].focus(),l._.currentFocusIndex=-1,q=1;else if(-1!=CKEDITOR.tools.indexOf(g,
+c)&&l._.tabBarMode)c=-1!=CKEDITOR.tools.indexOf([d?39:37,38],c)?x.call(l):A.call(l),l.selectPage(c),l._.tabs[c][0].focus(),q=1;else if(13!=c&&32!=c||!l._.tabBarMode)if(13==c)c=b.data.getTarget(),c.is("a","button","select","textarea")||c.is("input")&&"button"==c.$.type||((c=this.getButton("ok"))&&CKEDITOR.tools.setTimeout(c.click,0,c),q=1),n=1;else if(27==c)(c=this.getButton("cancel"))?CKEDITOR.tools.setTimeout(c.click,0,c):!1!==this.fire("cancel",{hide:!0}).hide&&this.hide(),n=1;else return;else this.selectPage(this._.currentTabId),
+this._.tabBarMode=!1,this._.currentFocusIndex=-1,e(1),q=1;f(b)}}function f(a){q?a.data.preventDefault(1):n&&a.data.stopPropagation()}var g=CKEDITOR.dialog._.dialogDefinitions[b],h=CKEDITOR.tools.clone(aa),p=a.config.dialog_buttonsOrder||"OS",m=a.lang.dir,k={},q,n;("OS"==p&&CKEDITOR.env.mac||"rtl"==p&&"ltr"==m||"ltr"==p&&"rtl"==m)&&h.buttons.reverse();g=CKEDITOR.tools.extend(g(a),h);g=CKEDITOR.tools.clone(g);g=new P(this,g);h=V(a);this._={editor:a,element:h.element,name:b,contentSize:{width:0,height:0},
+size:{width:0,height:0},contents:{},buttons:{},accessKeyMap:{},tabs:{},tabIdList:[],currentTabId:null,currentTabIndex:null,pageCount:0,lastTab:null,tabBarMode:!1,focusList:[],currentFocusIndex:0,hasFocus:!1};this.parts=h.parts;CKEDITOR.tools.setTimeout(function(){a.fire("ariaWidget",this.parts.contents)},0,this);h={position:CKEDITOR.env.ie6Compat?"absolute":"fixed",top:0,visibility:"hidden"};h["rtl"==m?"right":"left"]=0;this.parts.dialog.setStyles(h);CKEDITOR.event.call(this);this.definition=g=CKEDITOR.fire("dialogDefinition",
+{name:b,definition:g},a).definition;if(!("removeDialogTabs"in a._)&&a.config.removeDialogTabs){h=a.config.removeDialogTabs.split(";");for(m=0;m<h.length;m++)if(p=h[m].split(":"),2==p.length){var r=p[0];k[r]||(k[r]=[]);k[r].push(p[1])}a._.removeDialogTabs=k}if(a._.removeDialogTabs&&(k=a._.removeDialogTabs[b]))for(m=0;m<k.length;m++)g.removeContents(k[m]);if(g.onLoad)this.on("load",g.onLoad);if(g.onShow)this.on("show",g.onShow);if(g.onHide)this.on("hide",g.onHide);if(g.onOk)this.on("ok",function(b){a.fire("saveSnapshot");
+setTimeout(function(){a.fire("saveSnapshot")},0);!1===g.onOk.call(this,b)&&(b.data.hide=!1)});this.state=CKEDITOR.DIALOG_STATE_IDLE;if(g.onCancel)this.on("cancel",function(a){!1===g.onCancel.call(this,a)&&(a.data.hide=!1)});var l=this,t=function(a){var b=l._.contents,c=!1,d;for(d in b)for(var e in b[d])if(c=a.call(this,b[d][e]))return};this.on("ok",function(a){t(function(b){if(b.validate){var c=b.validate(this),d="string"==typeof c||!1===c;d&&(a.data.hide=!1,a.stop());T.call(b,!d,"string"==typeof c?
+c:void 0);return d}})},this,null,0);this.on("cancel",function(b){t(function(c){if(c.isChanged())return a.config.dialog_noConfirmCancel||confirm(a.lang.common.confirmCancel)||(b.data.hide=!1),!0})},this,null,0);this.parts.close.on("click",function(a){!1!==this.fire("cancel",{hide:!0}).hide&&this.hide();a.data.preventDefault()},this);this.changeFocus=e;var y=this._.element;a.focusManager.add(y,1);this.on("show",function(){y.on("keydown",d,this);if(CKEDITOR.env.gecko)y.on("keypress",f,this)});this.on("hide",
+function(){y.removeListener("keydown",d);CKEDITOR.env.gecko&&y.removeListener("keypress",f);t(function(a){U.apply(a)})});this.on("iframeAdded",function(a){(new CKEDITOR.dom.document(a.data.iframe.$.contentWindow.document)).on("keydown",d,this,null,0)});this.on("show",function(){c();var b=1<l._.pageCount;a.config.dialog_startupFocusTab&&b?(l._.tabBarMode=!0,l._.tabs[l._.currentTabId][0].focus(),l._.currentFocusIndex=-1):this._.hasFocus||(this._.currentFocusIndex=b?-1:this._.focusList.length-1,g.onFocus?
+(b=g.onFocus.call(this))&&b.focus():e(1))},this,null,4294967295);if(CKEDITOR.env.ie6Compat)this.on("load",function(){var a=this.getElement(),b=a.getFirst();b.remove();b.appendTo(a)},this);Y(this);Z(this);(new CKEDITOR.dom.text(g.title,CKEDITOR.document)).appendTo(this.parts.title);for(m=0;m<g.contents.length;m++)(k=g.contents[m])&&this.addPage(k);this.parts.tabs.on("click",function(a){var b=a.data.getTarget();b.hasClass("cke_dialog_tab")&&(b=b.$.id,this.selectPage(b.substring(4,b.lastIndexOf("_"))),
+this._.tabBarMode&&(this._.tabBarMode=!1,this._.currentFocusIndex=-1,e(1)),a.data.preventDefault())},this);m=[];k=CKEDITOR.dialog._.uiElementBuilders.hbox.build(this,{type:"hbox",className:"cke_dialog_footer_buttons",widths:[],children:g.buttons},m).getChild();this.parts.footer.setHtml(m.join(""));for(m=0;m<k.length;m++)this._.buttons[k[m].id]=k[m]};CKEDITOR.dialog.prototype={destroy:function(){this.hide();this._.element.remove()},resize:function(){return function(a,b){this._.contentSize&&this._.contentSize.width==
+a&&this._.contentSize.height==b||(CKEDITOR.dialog.fire("resize",{dialog:this,width:a,height:b},this._.editor),this.fire("resize",{width:a,height:b},this._.editor),this.parts.contents.setStyles({width:a+"px",height:b+"px"}),"rtl"==this._.editor.lang.dir&&this._.position&&(this._.position.x=CKEDITOR.document.getWindow().getViewPaneSize().width-this._.contentSize.width-parseInt(this._.element.getFirst().getStyle("right"),10)),this._.contentSize={width:a,height:b})}}(),getSize:function(){var a=this._.element.getFirst();
+return{width:a.$.offsetWidth||0,height:a.$.offsetHeight||0}},move:function(a,b,c){var e=this._.element.getFirst(),d="rtl"==this._.editor.lang.dir,f="fixed"==e.getComputedStyle("position");CKEDITOR.env.ie&&e.setStyle("zoom","100%");f&&this._.position&&this._.position.x==a&&this._.position.y==b||(this._.position={x:a,y:b},f||(f=CKEDITOR.document.getWindow().getScrollPosition(),a+=f.x,b+=f.y),d&&(f=this.getSize(),a=CKEDITOR.document.getWindow().getViewPaneSize().width-f.width-a),b={top:(0<b?b:0)+"px"},
+b[d?"right":"left"]=(0<a?a:0)+"px",e.setStyles(b),c&&(this._.moved=1))},getPosition:function(){return CKEDITOR.tools.extend({},this._.position)},show:function(){var a=this._.element,b=this.definition;a.getParent()&&a.getParent().equals(CKEDITOR.document.getBody())?a.setStyle("display","block"):a.appendTo(CKEDITOR.document.getBody());this.resize(this._.contentSize&&this._.contentSize.width||b.width||b.minWidth,this._.contentSize&&this._.contentSize.height||b.height||b.minHeight);this.reset();this.selectPage(this.definition.contents[0].id);
+null===CKEDITOR.dialog._.currentZIndex&&(CKEDITOR.dialog._.currentZIndex=this._.editor.config.baseFloatZIndex);this._.element.getFirst().setStyle("z-index",CKEDITOR.dialog._.currentZIndex+=10);null===CKEDITOR.dialog._.currentTop?(CKEDITOR.dialog._.currentTop=this,this._.parentDialog=null,N(this._.editor)):(this._.parentDialog=CKEDITOR.dialog._.currentTop,this._.parentDialog.getElement().getFirst().$.style.zIndex-=Math.floor(this._.editor.config.baseFloatZIndex/2),CKEDITOR.dialog._.currentTop=this);
+a.on("keydown",Q);a.on("keyup",R);this._.hasFocus=!1;for(var c in b.contents)if(b.contents[c]){var a=b.contents[c],e=this._.tabs[a.id],d=a.requiredContent,f=0;if(e){for(var g in this._.contents[a.id]){var h=this._.contents[a.id][g];"hbox"!=h.type&&"vbox"!=h.type&&h.getInputElement()&&(h.requiredContent&&!this._.editor.activeFilter.check(h.requiredContent)?h.disable():(h.enable(),f++))}!f||d&&!this._.editor.activeFilter.check(d)?e[0].addClass("cke_dialog_tab_disabled"):e[0].removeClass("cke_dialog_tab_disabled")}}CKEDITOR.tools.setTimeout(function(){this.layout();
+X(this);this.parts.dialog.setStyle("visibility","");this.fireOnce("load",{});CKEDITOR.ui.fire("ready",this);this.fire("show",{});this._.editor.fire("dialogShow",this);this._.parentDialog||this._.editor.focusManager.lock();this.foreach(function(a){a.setInitValue&&a.setInitValue()})},100,this)},layout:function(){var a=this.parts.dialog,b=this.getSize(),c=CKEDITOR.document.getWindow().getViewPaneSize(),e=(c.width-b.width)/2,d=(c.height-b.height)/2;CKEDITOR.env.ie6Compat||(b.height+(0<d?d:0)>c.height||
+b.width+(0<e?e:0)>c.width?a.setStyle("position","absolute"):a.setStyle("position","fixed"));this.move(this._.moved?this._.position.x:e,this._.moved?this._.position.y:d)},foreach:function(a){for(var b in this._.contents)for(var c in this._.contents[b])a.call(this,this._.contents[b][c]);return this},reset:function(){var a=function(a){a.reset&&a.reset(1)};return function(){this.foreach(a);return this}}(),setupContent:function(){var a=arguments;this.foreach(function(b){b.setup&&b.setup.apply(b,a)})},
+commitContent:function(){var a=arguments;this.foreach(function(b){CKEDITOR.env.ie&&this._.currentFocusIndex==b.focusIndex&&b.getInputElement().$.blur();b.commit&&b.commit.apply(b,a)})},hide:function(){if(this.parts.dialog.isVisible()){this.fire("hide",{});this._.editor.fire("dialogHide",this);this.selectPage(this._.tabIdList[0]);var a=this._.element;a.setStyle("display","none");this.parts.dialog.setStyle("visibility","hidden");for(ba(this);CKEDITOR.dialog._.currentTop!=this;)CKEDITOR.dialog._.currentTop.hide();
+if(this._.parentDialog){var b=this._.parentDialog.getElement().getFirst();b.setStyle("z-index",parseInt(b.$.style.zIndex,10)+Math.floor(this._.editor.config.baseFloatZIndex/2))}else O(this._.editor);if(CKEDITOR.dialog._.currentTop=this._.parentDialog)CKEDITOR.dialog._.currentZIndex-=10;else{CKEDITOR.dialog._.currentZIndex=null;a.removeListener("keydown",Q);a.removeListener("keyup",R);var c=this._.editor;c.focus();setTimeout(function(){c.focusManager.unlock();CKEDITOR.env.iOS&&c.window.focus()},0)}delete this._.parentDialog;
+this.foreach(function(a){a.resetInitValue&&a.resetInitValue()});this.setState(CKEDITOR.DIALOG_STATE_IDLE)}},addPage:function(a){if(!a.requiredContent||this._.editor.filter.check(a.requiredContent)){for(var b=[],c=a.label?' title\x3d"'+CKEDITOR.tools.htmlEncode(a.label)+'"':"",e=CKEDITOR.dialog._.uiElementBuilders.vbox.build(this,{type:"vbox",className:"cke_dialog_page_contents",children:a.elements,expand:!!a.expand,padding:a.padding,style:a.style||"width: 100%;"},b),d=this._.contents[a.id]={},f=e.getChild(),
+g=0;e=f.shift();)e.notAllowed||"hbox"==e.type||"vbox"==e.type||g++,d[e.id]=e,"function"==typeof e.getChild&&f.push.apply(f,e.getChild());g||(a.hidden=!0);b=CKEDITOR.dom.element.createFromHtml(b.join(""));b.setAttribute("role","tabpanel");e=CKEDITOR.env;d="cke_"+a.id+"_"+CKEDITOR.tools.getNextNumber();c=CKEDITOR.dom.element.createFromHtml(['\x3ca class\x3d"cke_dialog_tab"',0<this._.pageCount?" cke_last":"cke_first",c,a.hidden?' style\x3d"display:none"':"",' id\x3d"',d,'"',e.gecko&&!e.hc?"":' href\x3d"javascript:void(0)"',
+' tabIndex\x3d"-1" hidefocus\x3d"true" role\x3d"tab"\x3e',a.label,"\x3c/a\x3e"].join(""));b.setAttribute("aria-labelledby",d);this._.tabs[a.id]=[c,b];this._.tabIdList.push(a.id);!a.hidden&&this._.pageCount++;this._.lastTab=c;this.updateStyle();b.setAttribute("name",a.id);b.appendTo(this.parts.contents);c.unselectable();this.parts.tabs.append(c);a.accessKey&&(S(this,this,"CTRL+"+a.accessKey,ca,da),this._.accessKeyMap["CTRL+"+a.accessKey]=a.id)}},selectPage:function(a){if(this._.currentTabId!=a&&!this._.tabs[a][0].hasClass("cke_dialog_tab_disabled")&&
+!1!==this.fire("selectPage",{page:a,currentPage:this._.currentTabId})){for(var b in this._.tabs){var c=this._.tabs[b][0],e=this._.tabs[b][1];b!=a&&(c.removeClass("cke_dialog_tab_selected"),e.hide());e.setAttribute("aria-hidden",b!=a)}var d=this._.tabs[a];d[0].addClass("cke_dialog_tab_selected");CKEDITOR.env.ie6Compat||CKEDITOR.env.ie7Compat?(K(d[1]),d[1].show(),setTimeout(function(){K(d[1],1)},0)):d[1].show();this._.currentTabId=a;this._.currentTabIndex=CKEDITOR.tools.indexOf(this._.tabIdList,a)}},
+updateStyle:function(){this.parts.dialog[(1===this._.pageCount?"add":"remove")+"Class"]("cke_single_page")},hidePage:function(a){var b=this._.tabs[a]&&this._.tabs[a][0];b&&1!=this._.pageCount&&b.isVisible()&&(a==this._.currentTabId&&this.selectPage(x.call(this)),b.hide(),this._.pageCount--,this.updateStyle())},showPage:function(a){if(a=this._.tabs[a]&&this._.tabs[a][0])a.show(),this._.pageCount++,this.updateStyle()},getElement:function(){return this._.element},getName:function(){return this._.name},
+getContentElement:function(a,b){var c=this._.contents[a];return c&&c[b]},getValueOf:function(a,b){return this.getContentElement(a,b).getValue()},setValueOf:function(a,b,c){return this.getContentElement(a,b).setValue(c)},getButton:function(a){return this._.buttons[a]},click:function(a){return this._.buttons[a].click()},disableButton:function(a){return this._.buttons[a].disable()},enableButton:function(a){return this._.buttons[a].enable()},getPageCount:function(){return this._.pageCount},getParentEditor:function(){return this._.editor},
+getSelectedElement:function(){return this.getParentEditor().getSelection().getSelectedElement()},addFocusable:function(a,b){if("undefined"==typeof b)b=this._.focusList.length,this._.focusList.push(new L(this,a,b));else{this._.focusList.splice(b,0,new L(this,a,b));for(var c=b+1;c<this._.focusList.length;c++)this._.focusList[c].focusIndex++}},setState:function(a){if(this.state!=a){this.state=a;if(a==CKEDITOR.DIALOG_STATE_BUSY){if(!this.parts.spinner){var b=this.getParentEditor().lang.dir,c={attributes:{"class":"cke_dialog_spinner"},
+styles:{"float":"rtl"==b?"right":"left"}};c.styles["margin-"+("rtl"==b?"left":"right")]="8px";this.parts.spinner=CKEDITOR.document.createElement("div",c);this.parts.spinner.setHtml("\x26#8987;");this.parts.spinner.appendTo(this.parts.title,1)}this.parts.spinner.show();this.getButton("ok").disable()}else a==CKEDITOR.DIALOG_STATE_IDLE&&(this.parts.spinner&&this.parts.spinner.hide(),this.getButton("ok").enable());this.fire("state",a)}}};CKEDITOR.tools.extend(CKEDITOR.dialog,{add:function(a,b){this._.dialogDefinitions[a]&&
+"function"!=typeof b||(this._.dialogDefinitions[a]=b)},exists:function(a){return!!this._.dialogDefinitions[a]},getCurrent:function(){return CKEDITOR.dialog._.currentTop},isTabEnabled:function(a,b,c){a=a.config.removeDialogTabs;return!(a&&a.match(new RegExp("(?:^|;)"+b+":"+c+"(?:$|;)","i")))},okButton:function(){var a=function(a,c){c=c||{};return CKEDITOR.tools.extend({id:"ok",type:"button",label:a.lang.common.ok,"class":"cke_dialog_ui_button_ok",onClick:function(a){a=a.data.dialog;!1!==a.fire("ok",
+{hide:!0}).hide&&a.hide()}},c,!0)};a.type="button";a.override=function(b){return CKEDITOR.tools.extend(function(c){return a(c,b)},{type:"button"},!0)};return a}(),cancelButton:function(){var a=function(a,c){c=c||{};return CKEDITOR.tools.extend({id:"cancel",type:"button",label:a.lang.common.cancel,"class":"cke_dialog_ui_button_cancel",onClick:function(a){a=a.data.dialog;!1!==a.fire("cancel",{hide:!0}).hide&&a.hide()}},c,!0)};a.type="button";a.override=function(b){return CKEDITOR.tools.extend(function(c){return a(c,
+b)},{type:"button"},!0)};return a}(),addUIElement:function(a,b){this._.uiElementBuilders[a]=b}});CKEDITOR.dialog._={uiElementBuilders:{},dialogDefinitions:{},currentTop:null,currentZIndex:null};CKEDITOR.event.implementOn(CKEDITOR.dialog);CKEDITOR.event.implementOn(CKEDITOR.dialog.prototype);var aa={resizable:CKEDITOR.DIALOG_RESIZE_BOTH,minWidth:600,minHeight:400,buttons:[CKEDITOR.dialog.okButton,CKEDITOR.dialog.cancelButton]},D=function(a,b,c){for(var e=0,d;d=a[e];e++)if(d.id==b||c&&d[c]&&(d=D(d[c],
+b,c)))return d;return null},E=function(a,b,c,e,d){if(c){for(var f=0,g;g=a[f];f++){if(g.id==c)return a.splice(f,0,b),b;if(e&&g[e]&&(g=E(g[e],b,c,e,!0)))return g}if(d)return null}a.push(b);return b},F=function(a,b,c){for(var e=0,d;d=a[e];e++){if(d.id==b)return a.splice(e,1);if(c&&d[c]&&(d=F(d[c],b,c)))return d}return null},P=function(a,b){this.dialog=a;for(var c=b.contents,e=0,d;d=c[e];e++)c[e]=d&&new M(a,d);CKEDITOR.tools.extend(this,b)};P.prototype={getContents:function(a){return D(this.contents,
+a)},getButton:function(a){return D(this.buttons,a)},addContents:function(a,b){return E(this.contents,a,b)},addButton:function(a,b){return E(this.buttons,a,b)},removeContents:function(a){F(this.contents,a)},removeButton:function(a){F(this.buttons,a)}};M.prototype={get:function(a){return D(this.elements,a,"children")},add:function(a,b){return E(this.elements,a,b,"children")},remove:function(a){F(this.elements,a,"children")}};var J,z={},u,w={},Q=function(a){var b=a.data.$.ctrlKey||a.data.$.metaKey,c=
+a.data.$.altKey,e=a.data.$.shiftKey,d=String.fromCharCode(a.data.$.keyCode);(b=w[(b?"CTRL+":"")+(c?"ALT+":"")+(e?"SHIFT+":"")+d])&&b.length&&(b=b[b.length-1],b.keydown&&b.keydown.call(b.uiElement,b.dialog,b.key),a.data.preventDefault())},R=function(a){var b=a.data.$.ctrlKey||a.data.$.metaKey,c=a.data.$.altKey,e=a.data.$.shiftKey,d=String.fromCharCode(a.data.$.keyCode);(b=w[(b?"CTRL+":"")+(c?"ALT+":"")+(e?"SHIFT+":"")+d])&&b.length&&(b=b[b.length-1],b.keyup&&(b.keyup.call(b.uiElement,b.dialog,b.key),
+a.data.preventDefault()))},S=function(a,b,c,e,d){(w[c]||(w[c]=[])).push({uiElement:a,dialog:b,key:c,keyup:d||a.accessKeyUp,keydown:e||a.accessKeyDown})},ba=function(a){for(var b in w){for(var c=w[b],e=c.length-1;0<=e;e--)c[e].dialog!=a&&c[e].uiElement!=a||c.splice(e,1);0===c.length&&delete w[b]}},da=function(a,b){a._.accessKeyMap[b]&&a.selectPage(a._.accessKeyMap[b])},ca=function(){};(function(){CKEDITOR.ui.dialog={uiElement:function(a,b,c,e,d,f,g){if(!(4>arguments.length)){var h=(e.call?e(b):e)||
+"div",p=["\x3c",h," "],m=(d&&d.call?d(b):d)||{},k=(f&&f.call?f(b):f)||{},q=(g&&g.call?g.call(this,a,b):g)||"",n=this.domId=k.id||CKEDITOR.tools.getNextId()+"_uiElement";b.requiredContent&&!a.getParentEditor().filter.check(b.requiredContent)&&(m.display="none",this.notAllowed=!0);k.id=n;var r={};b.type&&(r["cke_dialog_ui_"+b.type]=1);b.className&&(r[b.className]=1);b.disabled&&(r.cke_disabled=1);for(var l=k["class"]&&k["class"].split?k["class"].split(" "):[],n=0;n<l.length;n++)l[n]&&(r[l[n]]=1);l=
+[];for(n in r)l.push(n);k["class"]=l.join(" ");b.title&&(k.title=b.title);r=(b.style||"").split(";");b.align&&(l=b.align,m["margin-left"]="left"==l?0:"auto",m["margin-right"]="right"==l?0:"auto");for(n in m)r.push(n+":"+m[n]);b.hidden&&r.push("display:none");for(n=r.length-1;0<=n;n--)""===r[n]&&r.splice(n,1);0<r.length&&(k.style=(k.style?k.style+"; ":"")+r.join("; "));for(n in k)p.push(n+'\x3d"'+CKEDITOR.tools.htmlEncode(k[n])+'" ');p.push("\x3e",q,"\x3c/",h,"\x3e");c.push(p.join(""));(this._||(this._=
+{})).dialog=a;"boolean"==typeof b.isChanged&&(this.isChanged=function(){return b.isChanged});"function"==typeof b.isChanged&&(this.isChanged=b.isChanged);"function"==typeof b.setValue&&(this.setValue=CKEDITOR.tools.override(this.setValue,function(a){return function(c){a.call(this,b.setValue.call(this,c))}}));"function"==typeof b.getValue&&(this.getValue=CKEDITOR.tools.override(this.getValue,function(a){return function(){return b.getValue.call(this,a.call(this))}}));CKEDITOR.event.implementOn(this);
+this.registerEvents(b);this.accessKeyUp&&this.accessKeyDown&&b.accessKey&&S(this,a,"CTRL+"+b.accessKey);var t=this;a.on("load",function(){var b=t.getInputElement();if(b){var c=t.type in{checkbox:1,ratio:1}&&CKEDITOR.env.ie&&8>CKEDITOR.env.version?"cke_dialog_ui_focused":"";b.on("focus",function(){a._.tabBarMode=!1;a._.hasFocus=!0;t.fire("focus");c&&this.addClass(c)});b.on("blur",function(){t.fire("blur");c&&this.removeClass(c)})}});CKEDITOR.tools.extend(this,b);this.keyboardFocusable&&(this.tabIndex=
+b.tabIndex||0,this.focusIndex=a._.focusList.push(this)-1,this.on("focus",function(){a._.currentFocusIndex=t.focusIndex}))}},hbox:function(a,b,c,e,d){if(!(4>arguments.length)){this._||(this._={});var f=this._.children=b,g=d&&d.widths||null,h=d&&d.height||null,p,m={role:"presentation"};d&&d.align&&(m.align=d.align);CKEDITOR.ui.dialog.uiElement.call(this,a,d||{type:"hbox"},e,"table",{},m,function(){var a=['\x3ctbody\x3e\x3ctr class\x3d"cke_dialog_ui_hbox"\x3e'];for(p=0;p<c.length;p++){var b="cke_dialog_ui_hbox_child",
+e=[];0===p&&(b="cke_dialog_ui_hbox_first");p==c.length-1&&(b="cke_dialog_ui_hbox_last");a.push('\x3ctd class\x3d"',b,'" role\x3d"presentation" ');g?g[p]&&e.push("width:"+v(g[p])):e.push("width:"+Math.floor(100/c.length)+"%");h&&e.push("height:"+v(h));d&&void 0!==d.padding&&e.push("padding:"+v(d.padding));CKEDITOR.env.ie&&CKEDITOR.env.quirks&&f[p].align&&e.push("text-align:"+f[p].align);0<e.length&&a.push('style\x3d"'+e.join("; ")+'" ');a.push("\x3e",c[p],"\x3c/td\x3e")}a.push("\x3c/tr\x3e\x3c/tbody\x3e");
+return a.join("")})}},vbox:function(a,b,c,e,d){if(!(3>arguments.length)){this._||(this._={});var f=this._.children=b,g=d&&d.width||null,h=d&&d.heights||null;CKEDITOR.ui.dialog.uiElement.call(this,a,d||{type:"vbox"},e,"div",null,{role:"presentation"},function(){var b=['\x3ctable role\x3d"presentation" cellspacing\x3d"0" border\x3d"0" '];b.push('style\x3d"');d&&d.expand&&b.push("height:100%;");b.push("width:"+v(g||"100%"),";");CKEDITOR.env.webkit&&b.push("float:none;");b.push('"');b.push('align\x3d"',
+CKEDITOR.tools.htmlEncode(d&&d.align||("ltr"==a.getParentEditor().lang.dir?"left":"right")),'" ');b.push("\x3e\x3ctbody\x3e");for(var e=0;e<c.length;e++){var k=[];b.push('\x3ctr\x3e\x3ctd role\x3d"presentation" ');g&&k.push("width:"+v(g||"100%"));h?k.push("height:"+v(h[e])):d&&d.expand&&k.push("height:"+Math.floor(100/c.length)+"%");d&&void 0!==d.padding&&k.push("padding:"+v(d.padding));CKEDITOR.env.ie&&CKEDITOR.env.quirks&&f[e].align&&k.push("text-align:"+f[e].align);0<k.length&&b.push('style\x3d"',
+k.join("; "),'" ');b.push(' class\x3d"cke_dialog_ui_vbox_child"\x3e',c[e],"\x3c/td\x3e\x3c/tr\x3e")}b.push("\x3c/tbody\x3e\x3c/table\x3e");return b.join("")})}}}})();CKEDITOR.ui.dialog.uiElement.prototype={getElement:function(){return CKEDITOR.document.getById(this.domId)},getInputElement:function(){return this.getElement()},getDialog:function(){return this._.dialog},setValue:function(a,b){this.getInputElement().setValue(a);!b&&this.fire("change",{value:a});return this},getValue:function(){return this.getInputElement().getValue()},
+isChanged:function(){return!1},selectParentTab:function(){for(var a=this.getInputElement();(a=a.getParent())&&-1==a.$.className.search("cke_dialog_page_contents"););if(!a)return this;a=a.getAttribute("name");this._.dialog._.currentTabId!=a&&this._.dialog.selectPage(a);return this},focus:function(){this.selectParentTab().getInputElement().focus();return this},registerEvents:function(a){var b=/^on([A-Z]\w+)/,c,e=function(a,b,c,d){b.on("load",function(){a.getInputElement().on(c,d,a)})},d;for(d in a)if(c=
+d.match(b))this.eventProcessors[d]?this.eventProcessors[d].call(this,this._.dialog,a[d]):e(this,this._.dialog,c[1].toLowerCase(),a[d]);return this},eventProcessors:{onLoad:function(a,b){a.on("load",b,this)},onShow:function(a,b){a.on("show",b,this)},onHide:function(a,b){a.on("hide",b,this)}},accessKeyDown:function(){this.focus()},accessKeyUp:function(){},disable:function(){var a=this.getElement();this.getInputElement().setAttribute("disabled","true");a.addClass("cke_disabled")},enable:function(){var a=
+this.getElement();this.getInputElement().removeAttribute("disabled");a.removeClass("cke_disabled")},isEnabled:function(){return!this.getElement().hasClass("cke_disabled")},isVisible:function(){return this.getInputElement().isVisible()},isFocusable:function(){return this.isEnabled()&&this.isVisible()?!0:!1}};CKEDITOR.ui.dialog.hbox.prototype=CKEDITOR.tools.extend(new CKEDITOR.ui.dialog.uiElement,{getChild:function(a){if(1>arguments.length)return this._.children.concat();a.splice||(a=[a]);return 2>
+a.length?this._.children[a[0]]:this._.children[a[0]]&&this._.children[a[0]].getChild?this._.children[a[0]].getChild(a.slice(1,a.length)):null}},!0);CKEDITOR.ui.dialog.vbox.prototype=new CKEDITOR.ui.dialog.hbox;(function(){var a={build:function(a,c,e){for(var d=c.children,f,g=[],h=[],p=0;p<d.length&&(f=d[p]);p++){var m=[];g.push(m);h.push(CKEDITOR.dialog._.uiElementBuilders[f.type].build(a,f,m))}return new CKEDITOR.ui.dialog[c.type](a,h,g,e,c)}};CKEDITOR.dialog.addUIElement("hbox",a);CKEDITOR.dialog.addUIElement("vbox",
+a)})();CKEDITOR.dialogCommand=function(a,b){this.dialogName=a;CKEDITOR.tools.extend(this,b,!0)};CKEDITOR.dialogCommand.prototype={exec:function(a){a.openDialog(this.dialogName)},canUndo:!1,editorFocus:1};(function(){var a=/^([a]|[^a])+$/,b=/^\d*$/,c=/^\d*(?:\.\d+)?$/,e=/^(((\d*(\.\d+))|(\d*))(px|\%)?)?$/,d=/^(((\d*(\.\d+))|(\d*))(px|em|ex|in|cm|mm|pt|pc|\%)?)?$/i,f=/^(\s*[\w-]+\s*:\s*[^:;]+(?:;|$))*$/;CKEDITOR.VALIDATE_OR=1;CKEDITOR.VALIDATE_AND=2;CKEDITOR.dialog.validate={functions:function(){var a=
+arguments;return function(){var b=this&&this.getValue?this.getValue():a[0],c,d=CKEDITOR.VALIDATE_AND,e=[],f;for(f=0;f<a.length;f++)if("function"==typeof a[f])e.push(a[f]);else break;f<a.length&&"string"==typeof a[f]&&(c=a[f],f++);f<a.length&&"number"==typeof a[f]&&(d=a[f]);var n=d==CKEDITOR.VALIDATE_AND?!0:!1;for(f=0;f<e.length;f++)n=d==CKEDITOR.VALIDATE_AND?n&&e[f](b):n||e[f](b);return n?!0:c}},regex:function(a,b){return function(c){c=this&&this.getValue?this.getValue():c;return a.test(c)?!0:b}},
+notEmpty:function(b){return this.regex(a,b)},integer:function(a){return this.regex(b,a)},number:function(a){return this.regex(c,a)},cssLength:function(a){return this.functions(function(a){return d.test(CKEDITOR.tools.trim(a))},a)},htmlLength:function(a){return this.functions(function(a){return e.test(CKEDITOR.tools.trim(a))},a)},inlineStyle:function(a){return this.functions(function(a){return f.test(CKEDITOR.tools.trim(a))},a)},equals:function(a,b){return this.functions(function(b){return b==a},b)},
+notEqual:function(a,b){return this.functions(function(b){return b!=a},b)}};CKEDITOR.on("instanceDestroyed",function(a){if(CKEDITOR.tools.isEmpty(CKEDITOR.instances)){for(var b;b=CKEDITOR.dialog._.currentTop;)b.hide();for(var c in z)z[c].remove();z={}}a=a.editor._.storedDialogs;for(var d in a)a[d].destroy()})})();CKEDITOR.tools.extend(CKEDITOR.editor.prototype,{openDialog:function(a,b){var c=null,e=CKEDITOR.dialog._.dialogDefinitions[a];null===CKEDITOR.dialog._.currentTop&&N(this);if("function"==typeof e)c=
+this._.storedDialogs||(this._.storedDialogs={}),c=c[a]||(c[a]=new CKEDITOR.dialog(this,a)),b&&b.call(c,c),c.show();else{if("failed"==e)throw O(this),Error('[CKEDITOR.dialog.openDialog] Dialog "'+a+'" failed when loading definition.');"string"==typeof e&&CKEDITOR.scriptLoader.load(CKEDITOR.getUrl(e),function(){"function"!=typeof CKEDITOR.dialog._.dialogDefinitions[a]&&(CKEDITOR.dialog._.dialogDefinitions[a]="failed");this.openDialog(a,b)},this,0,1)}CKEDITOR.skin.loadPart("dialog");return c}})})();
+CKEDITOR.plugins.add("dialog",{requires:"dialogui",init:function(x){x.on("doubleclick",function(A){A.data.dialog&&x.openDialog(A.data.dialog)},null,null,999)}});(function(){function g(a,b){var c=l.exec(a),d=l.exec(b);if(c){if(!c[2]&&"px"==d[2])return d[1];if("px"==c[2]&&!d[2])return d[1]+"px"}return b}var k=CKEDITOR.htmlParser.cssStyle,h=CKEDITOR.tools.cssLength,l=/^((?:\d*(?:\.\d+))|(?:\d+))(.*)?$/i,m={elements:{$:function(a){var b=a.attributes;if((b=(b=(b=b&&b["data-cke-realelement"])&&new CKEDITOR.htmlParser.fragment.fromHtml(decodeURIComponent(b)))&&b.children[0])&&a.attributes["data-cke-resizable"]){var c=(new k(a)).rules;a=b.attributes;var d=c.width,
+c=c.height;d&&(a.width=g(a.width,d));c&&(a.height=g(a.height,c))}return b}}};CKEDITOR.plugins.add("fakeobjects",{init:function(a){a.filter.allow("img[!data-cke-realelement,src,alt,title](*){*}","fakeobjects")},afterInit:function(a){(a=(a=a.dataProcessor)&&a.htmlFilter)&&a.addRules(m,{applyToAll:!0})}});CKEDITOR.editor.prototype.createFakeElement=function(a,b,c,d){var e=this.lang.fakeobjects,e=e[c]||e.unknown;b={"class":b,"data-cke-realelement":encodeURIComponent(a.getOuterHtml()),"data-cke-real-node-type":a.type,
+alt:e,title:e,align:a.getAttribute("align")||""};CKEDITOR.env.hc||(b.src=CKEDITOR.tools.transparentImageData);c&&(b["data-cke-real-element-type"]=c);d&&(b["data-cke-resizable"]=d,c=new k,d=a.getAttribute("width"),a=a.getAttribute("height"),d&&(c.rules.width=h(d)),a&&(c.rules.height=h(a)),c.populate(b));return this.document.createElement("img",{attributes:b})};CKEDITOR.editor.prototype.createFakeParserElement=function(a,b,c,d){var e=this.lang.fakeobjects,e=e[c]||e.unknown,f;f=new CKEDITOR.htmlParser.basicWriter;
+a.writeHtml(f);f=f.getHtml();b={"class":b,"data-cke-realelement":encodeURIComponent(f),"data-cke-real-node-type":a.type,alt:e,title:e,align:a.attributes.align||""};CKEDITOR.env.hc||(b.src=CKEDITOR.tools.transparentImageData);c&&(b["data-cke-real-element-type"]=c);d&&(b["data-cke-resizable"]=d,d=a.attributes,a=new k,c=d.width,d=d.height,void 0!==c&&(a.rules.width=h(c)),void 0!==d&&(a.rules.height=h(d)),a.populate(b));return new CKEDITOR.htmlParser.element("img",b)};CKEDITOR.editor.prototype.restoreRealElement=
+function(a){if(a.data("cke-real-node-type")!=CKEDITOR.NODE_ELEMENT)return null;var b=CKEDITOR.dom.element.createFromHtml(decodeURIComponent(a.data("cke-realelement")),this.document);if(a.data("cke-resizable")){var c=a.getStyle("width");a=a.getStyle("height");c&&b.setAttribute("width",g(b.getAttribute("width"),c));a&&b.setAttribute("height",g(b.getAttribute("height"),a))}return b}})();(function(){function p(c){return c.replace(/'/g,"\\$\x26")}function q(c){for(var b,a=c.length,d=[],f=0;f<a;f++)b=c.charCodeAt(f),d.push(b);return"String.fromCharCode("+d.join(",")+")"}function r(c,b){var a=c.plugins.link,d=a.compiledProtectionFunction.params,f,e;e=[a.compiledProtectionFunction.name,"("];for(var g=0;g<d.length;g++)a=d[g].toLowerCase(),f=b[a],0<g&&e.push(","),e.push("'",f?p(encodeURIComponent(b[a])):"","'");e.push(")");return e.join("")}function n(c){c=c.config.emailProtection||"";
+var b;c&&"encode"!=c&&(b={},c.replace(/^([^(]+)\(([^)]+)\)$/,function(a,c,f){b.name=c;b.params=[];f.replace(/[^,\s]+/g,function(a){b.params.push(a)})}));return b}CKEDITOR.plugins.add("link",{requires:"dialog,fakeobjects",onLoad:function(){function c(b){return a.replace(/%1/g,"rtl"==b?"right":"left").replace(/%2/g,"cke_contents_"+b)}var b="background:url("+CKEDITOR.getUrl(this.path+"images"+(CKEDITOR.env.hidpi?"/hidpi":"")+"/anchor.png")+") no-repeat %1 center;border:1px dotted #00f;background-size:16px;",
+a=".%2 a.cke_anchor,.%2 a.cke_anchor_empty,.cke_editable.%2 a[name],.cke_editable.%2 a[data-cke-saved-name]{"+b+"padding-%1:18px;cursor:auto;}.%2 img.cke_anchor{"+b+"width:16px;min-height:15px;height:1.15em;vertical-align:text-bottom;}";CKEDITOR.addCss(c("ltr")+c("rtl"))},init:function(c){var b="a[!href]";CKEDITOR.dialog.isTabEnabled(c,"link","advanced")&&(b=b.replace("]",",accesskey,charset,dir,id,lang,name,rel,tabindex,title,type,download]{*}(*)"));CKEDITOR.dialog.isTabEnabled(c,"link","target")&&
+(b=b.replace("]",",target,onclick]"));c.addCommand("link",new CKEDITOR.dialogCommand("link",{allowedContent:b,requiredContent:"a[href]"}));c.addCommand("anchor",new CKEDITOR.dialogCommand("anchor",{allowedContent:"a[!name,id]",requiredContent:"a[name]"}));c.addCommand("unlink",new CKEDITOR.unlinkCommand);c.addCommand("removeAnchor",new CKEDITOR.removeAnchorCommand);c.setKeystroke(CKEDITOR.CTRL+76,"link");c.ui.addButton&&(c.ui.addButton("Link",{label:c.lang.link.toolbar,command:"link",toolbar:"links,10"}),
+c.ui.addButton("Unlink",{label:c.lang.link.unlink,command:"unlink",toolbar:"links,20"}),c.ui.addButton("Anchor",{label:c.lang.link.anchor.toolbar,command:"anchor",toolbar:"links,30"}));CKEDITOR.dialog.add("link",this.path+"dialogs/link.js");CKEDITOR.dialog.add("anchor",this.path+"dialogs/anchor.js");c.on("doubleclick",function(a){var b=a.data.element.getAscendant({a:1,img:1},!0);b&&!b.isReadOnly()&&(b.is("a")?(a.data.dialog=!b.getAttribute("name")||b.getAttribute("href")&&b.getChildCount()?"link":
+"anchor",a.data.link=b):CKEDITOR.plugins.link.tryRestoreFakeAnchor(c,b)&&(a.data.dialog="anchor"))},null,null,0);c.on("doubleclick",function(a){a.data.dialog in{link:1,anchor:1}&&a.data.link&&c.getSelection().selectElement(a.data.link)},null,null,20);c.addMenuItems&&c.addMenuItems({anchor:{label:c.lang.link.anchor.menu,command:"anchor",group:"anchor",order:1},removeAnchor:{label:c.lang.link.anchor.remove,command:"removeAnchor",group:"anchor",order:5},link:{label:c.lang.link.menu,command:"link",group:"link",
+order:1},unlink:{label:c.lang.link.unlink,command:"unlink",group:"link",order:5}});c.contextMenu&&c.contextMenu.addListener(function(a){if(!a||a.isReadOnly())return null;a=CKEDITOR.plugins.link.tryRestoreFakeAnchor(c,a);if(!a&&!(a=CKEDITOR.plugins.link.getSelectedLink(c)))return null;var b={};a.getAttribute("href")&&a.getChildCount()&&(b={link:CKEDITOR.TRISTATE_OFF,unlink:CKEDITOR.TRISTATE_OFF});a&&a.hasAttribute("name")&&(b.anchor=b.removeAnchor=CKEDITOR.TRISTATE_OFF);return b});this.compiledProtectionFunction=
+n(c)},afterInit:function(c){c.dataProcessor.dataFilter.addRules({elements:{a:function(a){return a.attributes.name?a.children.length?null:c.createFakeParserElement(a,"cke_anchor","anchor"):null}}});var b=c._.elementsPath&&c._.elementsPath.filters;b&&b.push(function(a,b){if("a"==b&&(CKEDITOR.plugins.link.tryRestoreFakeAnchor(c,a)||a.getAttribute("name")&&(!a.getAttribute("href")||!a.getChildCount())))return"anchor"})}});var t=/^javascript:/,u=/^mailto:([^?]+)(?:\?(.+))?$/,v=/subject=([^;?:@&=$,\/]*)/i,
+w=/body=([^;?:@&=$,\/]*)/i,x=/^#(.*)$/,y=/^((?:http|https|ftp|news):\/\/)?(.*)$/,z=/^(_(?:self|top|parent|blank))$/,A=/^javascript:void\(location\.href='mailto:'\+String\.fromCharCode\(([^)]+)\)(?:\+'(.*)')?\)$/,B=/^javascript:([^(]+)\(([^)]+)\)$/,C=/\s*window.open\(\s*this\.href\s*,\s*(?:'([^']*)'|null)\s*,\s*'([^']*)'\s*\)\s*;\s*return\s*false;*\s*/,D=/(?:^|,)([^=]+)=(\d+|yes|no)/gi,m={id:"advId",dir:"advLangDir",accessKey:"advAccessKey",name:"advName",lang:"advLangCode",tabindex:"advTabIndex",
+title:"advTitle",type:"advContentType","class":"advCSSClasses",charset:"advCharset",style:"advStyles",rel:"advRel"};CKEDITOR.plugins.link={getSelectedLink:function(c,b){var a=c.getSelection(),d=a.getSelectedElement(),f=a.getRanges(),e=[],g;if(!b&&d&&d.is("a"))return d;for(d=0;d<f.length;d++)if(g=a.getRanges()[d],g.shrink(CKEDITOR.SHRINK_TEXT,!1,{skipBogus:!0}),(g=c.elementPath(g.getCommonAncestor()).contains("a",1))&&b)e.push(g);else if(g)return g;return b?e:null},getEditorAnchors:function(c){for(var b=
+c.editable(),a=b.isInline()&&!c.plugins.divarea?c.document:b,b=a.getElementsByTag("a"),a=a.getElementsByTag("img"),d=[],f=0,e;e=b.getItem(f++);)(e.data("cke-saved-name")||e.hasAttribute("name"))&&d.push({name:e.data("cke-saved-name")||e.getAttribute("name"),id:e.getAttribute("id")});for(f=0;e=a.getItem(f++);)(e=this.tryRestoreFakeAnchor(c,e))&&d.push({name:e.getAttribute("name"),id:e.getAttribute("id")});return d},fakeAnchor:!0,tryRestoreFakeAnchor:function(c,b){if(b&&b.data("cke-real-element-type")&&
+"anchor"==b.data("cke-real-element-type")){var a=c.restoreRealElement(b);if(a.data("cke-saved-name"))return a}},parseLinkAttributes:function(c,b){var a=b&&(b.data("cke-saved-href")||b.getAttribute("href"))||"",d=c.plugins.link.compiledProtectionFunction,f=c.config.emailProtection,e,g={};a.match(t)&&("encode"==f?a=a.replace(A,function(a,b,c){c=c||"";return"mailto:"+String.fromCharCode.apply(String,b.split(","))+c.replace(/\\'/g,"'")}):f&&a.replace(B,function(a,b,c){if(b==d.name){g.type="email";a=g.email=
+{};b=/(^')|('$)/g;c=c.match(/[^,\s]+/g);for(var e=c.length,f,h,k=0;k<e;k++)f=decodeURIComponent,h=c[k].replace(b,"").replace(/\\'/g,"'"),h=f(h),f=d.params[k].toLowerCase(),a[f]=h;a.address=[a.name,a.domain].join("@")}}));if(!g.type)if(f=a.match(x))g.type="anchor",g.anchor={},g.anchor.name=g.anchor.id=f[1];else if(f=a.match(u)){e=a.match(v);a=a.match(w);g.type="email";var k=g.email={};k.address=f[1];e&&(k.subject=decodeURIComponent(e[1]));a&&(k.body=decodeURIComponent(a[1]))}else a&&(e=a.match(y))&&
+(g.type="url",g.url={},g.url.protocol=e[1],g.url.url=e[2]);if(b){if(a=b.getAttribute("target"))g.target={type:a.match(z)?a:"frame",name:a};else if(a=(a=b.data("cke-pa-onclick")||b.getAttribute("onclick"))&&a.match(C))for(g.target={type:"popup",name:a[1]};f=D.exec(a[2]);)"yes"!=f[2]&&"1"!=f[2]||f[1]in{height:1,width:1,top:1,left:1}?isFinite(f[2])&&(g.target[f[1]]=f[2]):g.target[f[1]]=!0;null!==b.getAttribute("download")&&(g.download=!0);var a={},h;for(h in m)(f=b.getAttribute(h))&&(a[m[h]]=f);if(h=
+b.data("cke-saved-name")||a.advName)a.advName=h;CKEDITOR.tools.isEmpty(a)||(g.advanced=a)}return g},getLinkAttributes:function(c,b){var a=c.config.emailProtection||"",d={};switch(b.type){case "url":var a=b.url&&void 0!==b.url.protocol?b.url.protocol:"http://",f=b.url&&CKEDITOR.tools.trim(b.url.url)||"";d["data-cke-saved-href"]=0===f.indexOf("/")?f:a+f;break;case "anchor":a=b.anchor&&b.anchor.id;d["data-cke-saved-href"]="#"+(b.anchor&&b.anchor.name||a||"");break;case "email":var e=b.email,f=e.address;
+switch(a){case "":case "encode":var g=encodeURIComponent(e.subject||""),k=encodeURIComponent(e.body||""),e=[];g&&e.push("subject\x3d"+g);k&&e.push("body\x3d"+k);e=e.length?"?"+e.join("\x26"):"";"encode"==a?(a=["javascript:void(location.href\x3d'mailto:'+",q(f)],e&&a.push("+'",p(e),"'"),a.push(")")):a=["mailto:",f,e];break;default:a=f.split("@",2),e.name=a[0],e.domain=a[1],a=["javascript:",r(c,e)]}d["data-cke-saved-href"]=a.join("")}if(b.target)if("popup"==b.target.type){for(var a=["window.open(this.href, '",
+b.target.name||"","', '"],h="resizable status location toolbar menubar fullscreen scrollbars dependent".split(" "),f=h.length,g=function(a){b.target[a]&&h.push(a+"\x3d"+b.target[a])},e=0;e<f;e++)h[e]+=b.target[h[e]]?"\x3dyes":"\x3dno";g("width");g("left");g("height");g("top");a.push(h.join(","),"'); return false;");d["data-cke-pa-onclick"]=a.join("")}else"notSet"!=b.target.type&&b.target.name&&(d.target=b.target.name);b.download&&(d.download="");if(b.advanced){for(var l in m)(a=b.advanced[m[l]])&&
+(d[l]=a);d.name&&(d["data-cke-saved-name"]=d.name)}d["data-cke-saved-href"]&&(d.href=d["data-cke-saved-href"]);l={target:1,onclick:1,"data-cke-pa-onclick":1,"data-cke-saved-name":1,download:1};b.advanced&&CKEDITOR.tools.extend(l,m);for(var n in d)delete l[n];return{set:d,removed:CKEDITOR.tools.objectKeys(l)}},showDisplayTextForElement:function(c,b){var a={img:1,table:1,tbody:1,thead:1,tfoot:1,input:1,select:1,textarea:1},d=b.getSelection();return b.widgets&&b.widgets.focused||d&&1<d.getRanges().length?
+!1:!c||!c.getName||!c.is(a)}};CKEDITOR.unlinkCommand=function(){};CKEDITOR.unlinkCommand.prototype={exec:function(c){if(CKEDITOR.env.ie){var b=c.getSelection().getRanges()[0],a=b.getPreviousEditableNode()&&b.getPreviousEditableNode().getAscendant("a",!0)||b.getNextEditableNode()&&b.getNextEditableNode().getAscendant("a",!0),d;b.collapsed&&a&&(d=b.createBookmark(),b.selectNodeContents(a),b.select())}a=new CKEDITOR.style({element:"a",type:CKEDITOR.STYLE_INLINE,alwaysRemoveElement:1});c.removeStyle(a);
+d&&(b.moveToBookmark(d),b.select())},refresh:function(c,b){var a=b.lastElement&&b.lastElement.getAscendant("a",!0);a&&"a"==a.getName()&&a.getAttribute("href")&&a.getChildCount()?this.setState(CKEDITOR.TRISTATE_OFF):this.setState(CKEDITOR.TRISTATE_DISABLED)},contextSensitive:1,startDisabled:1,requiredContent:"a[href]",editorFocus:1};CKEDITOR.removeAnchorCommand=function(){};CKEDITOR.removeAnchorCommand.prototype={exec:function(c){var b=c.getSelection(),a=b.createBookmarks(),d;if(b&&(d=b.getSelectedElement())&&
+(d.getChildCount()?d.is("a"):CKEDITOR.plugins.link.tryRestoreFakeAnchor(c,d)))d.remove(1);else if(d=CKEDITOR.plugins.link.getSelectedLink(c))d.hasAttribute("href")?(d.removeAttributes({name:1,"data-cke-saved-name":1}),d.removeClass("cke_anchor")):d.remove(1);b.selectBookmarks(a)},requiredContent:"a[name]"};CKEDITOR.tools.extend(CKEDITOR.config,{linkShowAdvancedTab:!0,linkShowTargetTab:!0})})();(function(){function I(b,m,e){function c(c){if(!(!(a=d[c?"getFirst":"getLast"]())||a.is&&a.isBlockBoundary()||!(p=m.root[c?"getPrevious":"getNext"](CKEDITOR.dom.walker.invisible(!0)))||p.is&&p.isBlockBoundary({br:1})))b.document.createElement("br")[c?"insertBefore":"insertAfter"](a)}for(var f=CKEDITOR.plugins.list.listToArray(m.root,e),g=[],k=0;k<m.contents.length;k++){var h=m.contents[k];(h=h.getAscendant("li",!0))&&!h.getCustomData("list_item_processed")&&(g.push(h),CKEDITOR.dom.element.setMarker(e,
+h,"list_item_processed",!0))}h=null;for(k=0;k<g.length;k++)h=g[k].getCustomData("listarray_index"),f[h].indent=-1;for(k=h+1;k<f.length;k++)if(f[k].indent>f[k-1].indent+1){g=f[k-1].indent+1-f[k].indent;for(h=f[k].indent;f[k]&&f[k].indent>=h;)f[k].indent+=g,k++;k--}var d=CKEDITOR.plugins.list.arrayToList(f,e,null,b.config.enterMode,m.root.getAttribute("dir")).listNode,a,p;c(!0);c();d.replace(m.root);b.fire("contentDomInvalidated")}function B(b,m){this.name=b;this.context=this.type=m;this.allowedContent=
+m+" li";this.requiredContent=m}function E(b,m,e,c){for(var f,g;f=b[c?"getLast":"getFirst"](J);)(g=f.getDirection(1))!==m.getDirection(1)&&f.setAttribute("dir",g),f.remove(),e?f[c?"insertBefore":"insertAfter"](e):m.append(f,c)}function F(b){function m(e){var c=b[e?"getPrevious":"getNext"](u);c&&c.type==CKEDITOR.NODE_ELEMENT&&c.is(b.getName())&&(E(b,c,null,!e),b.remove(),b=c)}m();m(1)}function G(b){return b.type==CKEDITOR.NODE_ELEMENT&&(b.getName()in CKEDITOR.dtd.$block||b.getName()in CKEDITOR.dtd.$listItem)&&
+CKEDITOR.dtd[b.getName()]["#"]}function C(b,m,e){b.fire("saveSnapshot");e.enlarge(CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS);var c=e.extractContents();m.trim(!1,!0);var f=m.createBookmark(),g=new CKEDITOR.dom.elementPath(m.startContainer),k=g.block,g=g.lastElement.getAscendant("li",1)||k,h=new CKEDITOR.dom.elementPath(e.startContainer),d=h.contains(CKEDITOR.dtd.$listItem),h=h.contains(CKEDITOR.dtd.$list);k?(k=k.getBogus())&&k.remove():h&&(k=h.getPrevious(u))&&z(k)&&k.remove();(k=c.getLast())&&k.type==CKEDITOR.NODE_ELEMENT&&
+k.is("br")&&k.remove();(k=m.startContainer.getChild(m.startOffset))?c.insertBefore(k):m.startContainer.append(c);d&&(c=A(d))&&(g.contains(d)?(E(c,d.getParent(),d),c.remove()):g.append(c));for(;e.checkStartOfBlock()&&e.checkEndOfBlock();){h=e.startPath();c=h.block;if(!c)break;c.is("li")&&(g=c.getParent(),c.equals(g.getLast(u))&&c.equals(g.getFirst(u))&&(c=g));e.moveToPosition(c,CKEDITOR.POSITION_BEFORE_START);c.remove()}e=e.clone();c=b.editable();e.setEndAt(c,CKEDITOR.POSITION_BEFORE_END);e=new CKEDITOR.dom.walker(e);
+e.evaluator=function(a){return u(a)&&!z(a)};(e=e.next())&&e.type==CKEDITOR.NODE_ELEMENT&&e.getName()in CKEDITOR.dtd.$list&&F(e);m.moveToBookmark(f);m.select();b.fire("saveSnapshot")}function A(b){return(b=b.getLast(u))&&b.type==CKEDITOR.NODE_ELEMENT&&b.getName()in v?b:null}var v={ol:1,ul:1},K=CKEDITOR.dom.walker.whitespaces(),H=CKEDITOR.dom.walker.bookmark(),u=function(b){return!(K(b)||H(b))},z=CKEDITOR.dom.walker.bogus();CKEDITOR.plugins.list={listToArray:function(b,m,e,c,f){if(!v[b.getName()])return[];
+c||(c=0);e||(e=[]);for(var g=0,k=b.getChildCount();g<k;g++){var h=b.getChild(g);h.type==CKEDITOR.NODE_ELEMENT&&h.getName()in CKEDITOR.dtd.$list&&CKEDITOR.plugins.list.listToArray(h,m,e,c+1);if("li"==h.$.nodeName.toLowerCase()){var d={parent:b,indent:c,element:h,contents:[]};f?d.grandparent=f:(d.grandparent=b.getParent(),d.grandparent&&"li"==d.grandparent.$.nodeName.toLowerCase()&&(d.grandparent=d.grandparent.getParent()));m&&CKEDITOR.dom.element.setMarker(m,h,"listarray_index",e.length);e.push(d);
+for(var a=0,p=h.getChildCount(),l;a<p;a++)l=h.getChild(a),l.type==CKEDITOR.NODE_ELEMENT&&v[l.getName()]?CKEDITOR.plugins.list.listToArray(l,m,e,c+1,d.grandparent):d.contents.push(l)}}return e},arrayToList:function(b,m,e,c,f){e||(e=0);if(!b||b.length<e+1)return null;for(var g,k=b[e].parent.getDocument(),h=new CKEDITOR.dom.documentFragment(k),d=null,a=e,p=Math.max(b[e].indent,0),l=null,q,n,t=c==CKEDITOR.ENTER_P?"p":"div";;){var r=b[a];g=r.grandparent;q=r.element.getDirection(1);if(r.indent==p){d&&b[a].parent.getName()==
+d.getName()||(d=b[a].parent.clone(!1,1),f&&d.setAttribute("dir",f),h.append(d));l=d.append(r.element.clone(0,1));q!=d.getDirection(1)&&l.setAttribute("dir",q);for(g=0;g<r.contents.length;g++)l.append(r.contents[g].clone(1,1));a++}else if(r.indent==Math.max(p,0)+1)r=b[a-1].element.getDirection(1),a=CKEDITOR.plugins.list.arrayToList(b,null,a,c,r!=q?q:null),!l.getChildCount()&&CKEDITOR.env.needsNbspFiller&&7>=k.$.documentMode&&l.append(k.createText(" ")),l.append(a.listNode),a=a.nextIndex;else if(-1==
+r.indent&&!e&&g){v[g.getName()]?(l=r.element.clone(!1,!0),q!=g.getDirection(1)&&l.setAttribute("dir",q)):l=new CKEDITOR.dom.documentFragment(k);var d=g.getDirection(1)!=q,y=r.element,D=y.getAttribute("class"),z=y.getAttribute("style"),A=l.type==CKEDITOR.NODE_DOCUMENT_FRAGMENT&&(c!=CKEDITOR.ENTER_BR||d||z||D),w,B=r.contents.length,x;for(g=0;g<B;g++)if(w=r.contents[g],H(w)&&1<B)A?x=w.clone(1,1):l.append(w.clone(1,1));else if(w.type==CKEDITOR.NODE_ELEMENT&&w.isBlockBoundary()){d&&!w.getDirection()&&
+w.setAttribute("dir",q);n=w;var C=y.getAttribute("style");C&&n.setAttribute("style",C.replace(/([^;])$/,"$1;")+(n.getAttribute("style")||""));D&&w.addClass(D);n=null;x&&(l.append(x),x=null);l.append(w.clone(1,1))}else A?(n||(n=k.createElement(t),l.append(n),d&&n.setAttribute("dir",q)),z&&n.setAttribute("style",z),D&&n.setAttribute("class",D),x&&(n.append(x),x=null),n.append(w.clone(1,1))):l.append(w.clone(1,1));x&&((n||l).append(x),x=null);l.type==CKEDITOR.NODE_DOCUMENT_FRAGMENT&&a!=b.length-1&&(CKEDITOR.env.needsBrFiller&&
+(q=l.getLast())&&q.type==CKEDITOR.NODE_ELEMENT&&q.is("br")&&q.remove(),(q=l.getLast(u))&&q.type==CKEDITOR.NODE_ELEMENT&&q.is(CKEDITOR.dtd.$block)||l.append(k.createElement("br")));q=l.$.nodeName.toLowerCase();"div"!=q&&"p"!=q||l.appendBogus();h.append(l);d=null;a++}else return null;n=null;if(b.length<=a||Math.max(b[a].indent,0)<p)break}if(m)for(b=h.getFirst();b;){if(b.type==CKEDITOR.NODE_ELEMENT&&(CKEDITOR.dom.element.clearMarkers(m,b),b.getName()in CKEDITOR.dtd.$listItem&&(e=b,k=f=c=void 0,c=e.getDirection()))){for(f=
+e.getParent();f&&!(k=f.getDirection());)f=f.getParent();c==k&&e.removeAttribute("dir")}b=b.getNextSourceNode()}return{listNode:h,nextIndex:a}}};var L=/^h[1-6]$/,J=CKEDITOR.dom.walker.nodeType(CKEDITOR.NODE_ELEMENT);B.prototype={exec:function(b){this.refresh(b,b.elementPath());var m=b.config,e=b.getSelection(),c=e&&e.getRanges();if(this.state==CKEDITOR.TRISTATE_OFF){var f=b.editable();if(f.getFirst(u)){var g=1==c.length&&c[0];(m=g&&g.getEnclosedNode())&&m.is&&this.type==m.getName()&&this.setState(CKEDITOR.TRISTATE_ON)}else m.enterMode==
+CKEDITOR.ENTER_BR?f.appendBogus():c[0].fixBlock(1,m.enterMode==CKEDITOR.ENTER_P?"p":"div"),e.selectRanges(c)}for(var m=e.createBookmarks(!0),f=[],k={},c=c.createIterator(),h=0;(g=c.getNextRange())&&++h;){var d=g.getBoundaryNodes(),a=d.startNode,p=d.endNode;a.type==CKEDITOR.NODE_ELEMENT&&"td"==a.getName()&&g.setStartAt(d.startNode,CKEDITOR.POSITION_AFTER_START);p.type==CKEDITOR.NODE_ELEMENT&&"td"==p.getName()&&g.setEndAt(d.endNode,CKEDITOR.POSITION_BEFORE_END);g=g.createIterator();for(g.forceBrBreak=
+this.state==CKEDITOR.TRISTATE_OFF;d=g.getNextParagraph();)if(!d.getCustomData("list_block")){CKEDITOR.dom.element.setMarker(k,d,"list_block",1);for(var l=b.elementPath(d),a=l.elements,p=0,l=l.blockLimit,q,n=a.length-1;0<=n&&(q=a[n]);n--)if(v[q.getName()]&&l.contains(q)){l.removeCustomData("list_group_object_"+h);(a=q.getCustomData("list_group_object"))?a.contents.push(d):(a={root:q,contents:[d]},f.push(a),CKEDITOR.dom.element.setMarker(k,q,"list_group_object",a));p=1;break}p||(p=l,p.getCustomData("list_group_object_"+
+h)?p.getCustomData("list_group_object_"+h).contents.push(d):(a={root:p,contents:[d]},CKEDITOR.dom.element.setMarker(k,p,"list_group_object_"+h,a),f.push(a)))}}for(q=[];0<f.length;)if(a=f.shift(),this.state==CKEDITOR.TRISTATE_OFF)if(v[a.root.getName()]){c=b;h=a;a=k;g=q;p=CKEDITOR.plugins.list.listToArray(h.root,a);l=[];for(d=0;d<h.contents.length;d++)n=h.contents[d],(n=n.getAscendant("li",!0))&&!n.getCustomData("list_item_processed")&&(l.push(n),CKEDITOR.dom.element.setMarker(a,n,"list_item_processed",
+!0));for(var n=h.root.getDocument(),t=void 0,r=void 0,d=0;d<l.length;d++){var y=l[d].getCustomData("listarray_index"),t=p[y].parent;t.is(this.type)||(r=n.createElement(this.type),t.copyAttributes(r,{start:1,type:1}),r.removeStyle("list-style-type"),p[y].parent=r)}a=CKEDITOR.plugins.list.arrayToList(p,a,null,c.config.enterMode);p=void 0;l=a.listNode.getChildCount();for(d=0;d<l&&(p=a.listNode.getChild(d));d++)p.getName()==this.type&&g.push(p);a.listNode.replace(h.root);c.fire("contentDomInvalidated")}else{p=
+b;g=a;d=q;l=g.contents;c=g.root.getDocument();h=[];1==l.length&&l[0].equals(g.root)&&(a=c.createElement("div"),l[0].moveChildren&&l[0].moveChildren(a),l[0].append(a),l[0]=a);g=g.contents[0].getParent();for(n=0;n<l.length;n++)g=g.getCommonAncestor(l[n].getParent());t=p.config.useComputedState;p=a=void 0;t=void 0===t||t;for(n=0;n<l.length;n++)for(r=l[n];y=r.getParent();){if(y.equals(g)){h.push(r);!p&&r.getDirection()&&(p=1);r=r.getDirection(t);null!==a&&(a=a&&a!=r?null:r);break}r=y}if(!(1>h.length)){l=
+h[h.length-1].getNext();n=c.createElement(this.type);d.push(n);for(t=d=void 0;h.length;)d=h.shift(),t=c.createElement("li"),r=d,r.is("pre")||L.test(r.getName())||"false"==r.getAttribute("contenteditable")?d.appendTo(t):(d.copyAttributes(t),a&&d.getDirection()&&(t.removeStyle("direction"),t.removeAttribute("dir")),d.moveChildren(t),d.remove()),t.appendTo(n);a&&p&&n.setAttribute("dir",a);l?n.insertBefore(l):n.appendTo(g)}}else this.state==CKEDITOR.TRISTATE_ON&&v[a.root.getName()]&&I.call(this,b,a,k);
+for(n=0;n<q.length;n++)F(q[n]);CKEDITOR.dom.element.clearAllMarkers(k);e.selectBookmarks(m);b.focus()},refresh:function(b,m){var e=m.contains(v,1),c=m.blockLimit||m.root;e&&c.contains(e)?this.setState(e.is(this.type)?CKEDITOR.TRISTATE_ON:CKEDITOR.TRISTATE_OFF):this.setState(CKEDITOR.TRISTATE_OFF)}};CKEDITOR.plugins.add("list",{requires:"indentlist",init:function(b){b.blockless||(b.addCommand("numberedlist",new B("numberedlist","ol")),b.addCommand("bulletedlist",new B("bulletedlist","ul")),b.ui.addButton&&
+(b.ui.addButton("NumberedList",{label:b.lang.list.numberedlist,command:"numberedlist",directional:!0,toolbar:"list,10"}),b.ui.addButton("BulletedList",{label:b.lang.list.bulletedlist,command:"bulletedlist",directional:!0,toolbar:"list,20"})),b.on("key",function(m){var e=m.data.domEvent.getKey(),c;if("wysiwyg"==b.mode&&e in{8:1,46:1}){var f=b.getSelection().getRanges()[0],g=f&&f.startPath();if(f&&f.collapsed){var k=8==e,h=b.editable(),d=new CKEDITOR.dom.walker(f.clone());d.evaluator=function(a){return u(a)&&
+!z(a)};d.guard=function(a,b){return!(b&&a.type==CKEDITOR.NODE_ELEMENT&&a.is("table"))};e=f.clone();if(k){var a;(a=g.contains(v))&&f.checkBoundaryOfElement(a,CKEDITOR.START)&&(a=a.getParent())&&a.is("li")&&(a=A(a))?(c=a,a=a.getPrevious(u),e.moveToPosition(a&&z(a)?a:c,CKEDITOR.POSITION_BEFORE_START)):(d.range.setStartAt(h,CKEDITOR.POSITION_AFTER_START),d.range.setEnd(f.startContainer,f.startOffset),(a=d.previous())&&a.type==CKEDITOR.NODE_ELEMENT&&(a.getName()in v||a.is("li"))&&(a.is("li")||(d.range.selectNodeContents(a),
+d.reset(),d.evaluator=G,a=d.previous()),c=a,e.moveToElementEditEnd(c),e.moveToPosition(e.endPath().block,CKEDITOR.POSITION_BEFORE_END)));if(c)C(b,e,f),m.cancel();else{var p=g.contains(v);p&&f.checkBoundaryOfElement(p,CKEDITOR.START)&&(c=p.getFirst(u),f.checkBoundaryOfElement(c,CKEDITOR.START)&&(a=p.getPrevious(u),A(c)?a&&(f.moveToElementEditEnd(a),f.select()):b.execCommand("outdent"),m.cancel()))}}else if(c=g.contains("li")){if(d.range.setEndAt(h,CKEDITOR.POSITION_BEFORE_END),k=(h=c.getLast(u))&&
+G(h)?h:c,g=0,(a=d.next())&&a.type==CKEDITOR.NODE_ELEMENT&&a.getName()in v&&a.equals(h)?(g=1,a=d.next()):f.checkBoundaryOfElement(k,CKEDITOR.END)&&(g=2),g&&a){f=f.clone();f.moveToElementEditStart(a);if(1==g&&(e.optimize(),!e.startContainer.equals(c))){for(c=e.startContainer;c.is(CKEDITOR.dtd.$inline);)p=c,c=c.getParent();p&&e.moveToPosition(p,CKEDITOR.POSITION_AFTER_END)}2==g&&(e.moveToPosition(e.endPath().block,CKEDITOR.POSITION_BEFORE_END),f.endPath().block&&f.moveToPosition(f.endPath().block,CKEDITOR.POSITION_AFTER_START));
+C(b,e,f);m.cancel()}}else d.range.setEndAt(h,CKEDITOR.POSITION_BEFORE_END),(a=d.next())&&a.type==CKEDITOR.NODE_ELEMENT&&a.is(v)&&(a=a.getFirst(u),g.block&&f.checkStartOfBlock()&&f.checkEndOfBlock()?(g.block.remove(),f.moveToElementEditStart(a),f.select()):A(a)?(f.moveToElementEditStart(a),f.select()):(f=f.clone(),f.moveToElementEditStart(a),C(b,e,f)),m.cancel());setTimeout(function(){b.selectionChange(1)})}}}))}})})();CKEDITOR.plugins.add("removeformat",{init:function(a){a.addCommand("removeFormat",CKEDITOR.plugins.removeformat.commands.removeformat);a.ui.addButton&&a.ui.addButton("RemoveFormat",{label:a.lang.removeformat.toolbar,command:"removeFormat",toolbar:"cleanup,10"})}});
+CKEDITOR.plugins.removeformat={commands:{removeformat:{exec:function(a){for(var h=a._.removeFormatRegex||(a._.removeFormatRegex=new RegExp("^(?:"+a.config.removeFormatTags.replace(/,/g,"|")+")$","i")),e=a._.removeAttributes||(a._.removeAttributes=a.config.removeFormatAttributes.split(",")),f=CKEDITOR.plugins.removeformat.filter,m=a.getSelection().getRanges(),n=m.createIterator(),p=function(a){return a.type==CKEDITOR.NODE_ELEMENT},c;c=n.getNextRange();){c.collapsed||c.enlarge(CKEDITOR.ENLARGE_ELEMENT);
+var l=c.createBookmark(),b=l.startNode,d=l.endNode,k=function(b){for(var c=a.elementPath(b),e=c.elements,d=1,g;(g=e[d])&&!g.equals(c.block)&&!g.equals(c.blockLimit);d++)h.test(g.getName())&&f(a,g)&&b.breakParent(g)};k(b);if(d)for(k(d),b=b.getNextSourceNode(!0,CKEDITOR.NODE_ELEMENT);b&&!b.equals(d);)if(b.isReadOnly()){if(b.getPosition(d)&CKEDITOR.POSITION_CONTAINS)break;b=b.getNext(p)}else k=b.getNextSourceNode(!1,CKEDITOR.NODE_ELEMENT),"img"==b.getName()&&b.data("cke-realelement")||!f(a,b)||(h.test(b.getName())?
+b.remove(1):(b.removeAttributes(e),a.fire("removeFormatCleanup",b))),b=k;c.moveToBookmark(l)}a.forceNextSelectionCheck();a.getSelection().selectRanges(m)}}},filter:function(a,h){for(var e=a._.removeFormatFilters||[],f=0;f<e.length;f++)if(!1===e[f](h))return!1;return!0}};CKEDITOR.editor.prototype.addRemoveFormatFilter=function(a){this._.removeFormatFilters||(this._.removeFormatFilters=[]);this._.removeFormatFilters.push(a)};CKEDITOR.config.removeFormatTags="b,big,cite,code,del,dfn,em,font,i,ins,kbd,q,s,samp,small,span,strike,strong,sub,sup,tt,u,var";
+CKEDITOR.config.removeFormatAttributes="class,style,lang,width,height,align,hspace,valign";CKEDITOR.plugins.add("sourcedialog",{requires:"dialog",init:function(a){a.addCommand("sourcedialog",new CKEDITOR.dialogCommand("sourcedialog"));CKEDITOR.dialog.add("sourcedialog",this.path+"dialogs/sourcedialog.js");a.ui.addButton&&a.ui.addButton("Sourcedialog",{label:a.lang.sourcedialog.toolbar,command:"sourcedialog",toolbar:"mode,10"})}});CKEDITOR.config.plugins='basicstyles,notification,button,toolbar,clipboard,enterkey,entities,floatingspace,horizontalrule,indent,indentlist,dialogui,dialog,fakeobjects,link,list,removeformat,sourcedialog';CKEDITOR.config.skin='moono-lisa';(function() {var setIcons = function(icons, strip) {var path = CKEDITOR.getUrl( 'plugins/' + strip );icons = icons.split( ',' );for ( var i = 0; i < icons.length; i++ )CKEDITOR.skin.icons[ icons[ i ] ] = { path: path, offset: -icons[ ++i ], bgsize : icons[ ++i ] };};if (CKEDITOR.env.hidpi) setIcons('bold,0,,italic,24,,strike,48,,subscript,72,,superscript,96,,underline,120,,copy-rtl,144,,copy,168,,cut-rtl,192,,cut,216,,paste-rtl,240,,paste,264,,horizontalrule,288,,indent-rtl,312,,indent,336,,outdent-rtl,360,,outdent,384,,anchor-rtl,408,,anchor,432,,link,456,,unlink,480,,bulletedlist-rtl,504,,bulletedlist,528,,numberedlist-rtl,552,,numberedlist,576,,removeformat,600,,sourcedialog-rtl,624,,sourcedialog,648,','icons_hidpi.png');else setIcons('bold,0,auto,italic,24,auto,strike,48,auto,subscript,72,auto,superscript,96,auto,underline,120,auto,copy-rtl,144,auto,copy,168,auto,cut-rtl,192,auto,cut,216,auto,paste-rtl,240,auto,paste,264,auto,horizontalrule,288,auto,indent-rtl,312,auto,indent,336,auto,outdent-rtl,360,auto,outdent,384,auto,anchor-rtl,408,auto,anchor,432,auto,link,456,auto,unlink,480,auto,bulletedlist-rtl,504,auto,bulletedlist,528,auto,numberedlist-rtl,552,auto,numberedlist,576,auto,removeformat,600,auto,sourcedialog-rtl,624,auto,sourcedialog,648,auto','icons.png');})();CKEDITOR.lang.languages={"en":1,"de":1};}());
index 8e304f9..13c398a 100644 (file)
@@ -1,10 +1,9 @@
 /**
- * @license Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or http://ckeditor.com/license
  */
 
 CKEDITOR.editorConfig = function( config ) {
-       // Define changes to default configuration here. For example:
-       // config.language = 'fr';
-       // config.uiColor = '#AADC6E';
+       // Simplify the dialog windows.
+       config.removeDialogTabs = 'image:advanced;link:advanced';
 };
index cd23f24..920f2ca 100644 (file)
@@ -1,5 +1,5 @@
 /*\r
-Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
 For licensing, see LICENSE.md or http://ckeditor.com/license\r
 */\r
 \r
@@ -21,7 +21,10 @@ body
 .cke_editable\r
 {\r
        font-size: 13px;\r
-       line-height: 1.6em;\r
+       line-height: 1.6;\r
+\r
+       /* Fix for missing scrollbars with RTL texts. (#10488) */\r
+       word-wrap: break-word;\r
 }\r
 \r
 blockquote\r
@@ -64,7 +67,7 @@ ol,ul,dl
 h1,h2,h3,h4,h5,h6\r
 {\r
        font-weight: normal;\r
-       line-height: 1.2em;\r
+       line-height: 1.2;\r
 }\r
 \r
 hr\r
@@ -93,6 +96,8 @@ pre
 {\r
        white-space: pre-wrap; /* CSS 2.1 */\r
        word-wrap: break-word; /* IE7 */\r
+       -moz-tab-size: 4;\r
+       tab-size: 4;\r
 }\r
 \r
 .marker\r
@@ -102,7 +107,7 @@ pre
 \r
 span[lang]\r
 {\r
-   font-style: italic;\r
+       font-style: italic;\r
 }\r
 \r
 figure\r
@@ -113,11 +118,91 @@ figure
        background: rgba(0,0,0,0.05);\r
        padding: 10px;\r
        margin: 10px 20px;\r
-       display: block; /* For IE8 */\r
+       display: inline-block;\r
 }\r
 \r
-figure figcaption\r
+figure figcaption\r
 {\r
        text-align: center;\r
        display: block; /* For IE8 */\r
 }\r
+\r
+a > img {\r
+       padding: 1px;\r
+       margin: 1px;\r
+       border: none;\r
+       outline: 1px solid #0782C1;\r
+}\r
+\r
+/* Widget Styles */\r
+.code-featured\r
+{\r
+       border: 5px solid red;\r
+}\r
+\r
+.math-featured\r
+{\r
+       padding: 20px;\r
+       box-shadow: 0 0 2px rgba(200, 0, 0, 1);\r
+       background-color: rgba(255, 0, 0, 0.05);\r
+       margin: 10px;\r
+}\r
+\r
+.image-clean\r
+{\r
+       border: 0;\r
+       background: none;\r
+       padding: 0;\r
+}\r
+\r
+.image-clean > figcaption\r
+{\r
+       font-size: .9em;\r
+       text-align: right;\r
+}\r
+\r
+.image-grayscale\r
+{\r
+       background-color: white;\r
+       color: #666;\r
+}\r
+\r
+.image-grayscale img, img.image-grayscale\r
+{\r
+       filter: grayscale(100%);\r
+}\r
+\r
+.embed-240p\r
+{\r
+       max-width: 426px;\r
+       max-height: 240px;\r
+       margin:0 auto;\r
+}\r
+\r
+.embed-360p\r
+{\r
+       max-width: 640px;\r
+       max-height: 360px;\r
+       margin:0 auto;\r
+}\r
+\r
+.embed-480p\r
+{\r
+       max-width: 854px;\r
+       max-height: 480px;\r
+       margin:0 auto;\r
+}\r
+\r
+.embed-720p\r
+{\r
+       max-width: 1280px;\r
+       max-height: 720px;\r
+       margin:0 auto;\r
+}\r
+\r
+.embed-1080p\r
+{\r
+       max-width: 1920px;\r
+       max-height: 1080px;\r
+       margin:0 auto;\r
+}\r
index 872365c..cf8a6d9 100644 (file)
@@ -1,5 +1,5 @@
 /*\r
-Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.\r
-For licensing, see LICENSE.html or http://ckeditor.com/license\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
 */
-CKEDITOR.lang['de']={"editor":"WYSIWYG-Editor","editorPanel":"WYSIWYG-Editor-Leiste","common":{"editorHelp":"Drücken Sie ALT 0 für Hilfe","browseServer":"Server durchsuchen","url":"URL","protocol":"Protokoll","upload":"Hochladen","uploadSubmit":"Zum Server senden","image":"Bild","flash":"Flash","form":"Formular","checkbox":"Checkbox","radio":"Radiobutton","textField":"Textfeld einzeilig","textarea":"Textfeld mehrzeilig","hiddenField":"Verstecktes Feld","button":"Klickbutton","select":"Auswahlfeld","imageButton":"Bildbutton","notSet":"<nichts>","id":"ID","name":"Name","langDir":"Schreibrichtung","langDirLtr":"Links nach Rechts (LTR)","langDirRtl":"Rechts nach Links (RTL)","langCode":"Sprachenkürzel","longDescr":"Langform URL","cssClass":"Stylesheet Klasse","advisoryTitle":"Titel Beschreibung","cssStyle":"Style","ok":"OK","cancel":"Abbrechen","close":"Schließen","preview":"Vorschau","resize":"Zum Vergrößern ziehen","generalTab":"Allgemein","advancedTab":"Erweitert","validateNumberFailed":"Dieser Wert ist keine Nummer.","confirmNewPage":"Alle nicht gespeicherten Änderungen gehen verlohren. Sind Sie sicher die neue Seite zu laden?","confirmCancel":"Einige Optionen wurden geändert. Wollen Sie den Dialog dennoch schließen?","options":"Optionen","target":"Zielseite","targetNew":"Neues Fenster (_blank)","targetTop":"Oberstes Fenster (_top)","targetSelf":"Gleiches Fenster (_self)","targetParent":"Oberes Fenster (_parent)","langDirLTR":"Links nach Rechts (LNR)","langDirRTL":"Rechts nach Links (RNL)","styles":"Style","cssClasses":"Stylesheet Klasse","width":"Breite","height":"Höhe","align":"Ausrichtung","alignLeft":"Links","alignRight":"Rechts","alignCenter":"Zentriert","alignTop":"Oben","alignMiddle":"Mitte","alignBottom":"Unten","invalidValue":"Ungültiger Wert.","invalidHeight":"Höhe muss eine Zahl sein.","invalidWidth":"Breite muss eine Zahl sein.","invalidCssLength":"Wert spezifiziert für \"%1\" Feld muss ein positiver numerischer Wert sein mit oder ohne korrekte CSS Messeinheit (px, %, in, cm, mm, em, ex, pt oder pc).","invalidHtmlLength":"Wert spezifiziert für \"%1\" Feld muss ein positiver numerischer Wert sein mit oder ohne korrekte HTML Messeinheit (px oder %).","invalidInlineStyle":"Wert spezifiziert für inline Stilart muss enthalten ein oder mehr Tupels mit dem Format \"Name : Wert\" getrennt mit Semikolons.","cssLengthTooltip":"Gebe eine Zahl ein für ein Wert in pixels oder eine Zahl mit einer korrekten CSS Messeinheit (px, %, in, cm, mm, em, ex, pt oder pc).","unavailable":"%1<span class=\"cke_accessibility\">, nicht verfügbar</span>"},"about":{"copy":"Copyright &copy; $1. Alle Rechte vorbehalten.","dlgTitle":"Über CKEditor","help":"Prüfe $1 für Hilfe.","moreInfo":"Für Informationen über unsere Lizenzbestimmungen besuchen sie bitte unsere Webseite:","title":"Über CKEditor","userGuide":"CKEditor Benutzerhandbuch"},"basicstyles":{"bold":"Fett","italic":"Kursiv","strike":"Durchgestrichen","subscript":"Tiefgestellt","superscript":"Hochgestellt","underline":"Unterstrichen"},"clipboard":{"copy":"Kopieren","copyError":"Die Sicherheitseinstellungen Ihres Browsers lassen es nicht zu, den Text automatisch kopieren. Bitte benutzen Sie die System-Zwischenablage über STRG-C (kopieren).","cut":"Ausschneiden","cutError":"Die Sicherheitseinstellungen Ihres Browsers lassen es nicht zu, den Text automatisch auszuschneiden. Bitte benutzen Sie die System-Zwischenablage über STRG-X (ausschneiden) und STRG-V (einfügen).","paste":"Einfügen","pasteArea":"Einfügebereich","pasteMsg":"Bitte fügen Sie den Text in der folgenden Box über die Tastatur (mit <STRONG>Strg+V</STRONG>) ein und bestätigen Sie mit <STRONG>OK</STRONG>.","securityMsg":"Aufgrund von Sicherheitsbeschränkungen Ihres Browsers kann der Editor nicht direkt auf die Zwischenablage zugreifen. Bitte fügen Sie den Inhalt erneut in diesem Fenster ein.","title":"Einfügen"},"toolbar":{"toolbarCollapse":"Symbolleiste einklappen","toolbarExpand":"Symbolleiste ausklappen","toolbarGroups":{"document":"Dokument","clipboard":"Zwischenablage/Rückgängig","editing":"Editieren","forms":"Formularen","basicstyles":"Grundstile","paragraph":"Absatz","links":"Links","insert":"Einfügen","styles":"Stile","colors":"Farben","tools":"Werkzeuge"},"toolbars":"Editor Symbolleisten"},"horizontalrule":{"toolbar":"Horizontale Linie einfügen"},"indent":{"indent":"Einzug erhöhen","outdent":"Einzug verringern"},"fakeobjects":{"anchor":"Anker","flash":"Flash Animation","hiddenfield":"Verstecktes Feld","iframe":"IFrame","unknown":"Unbekanntes Objekt"},"link":{"acccessKey":"Zugriffstaste","advanced":"Erweitert","advisoryContentType":"Inhaltstyp","advisoryTitle":"Titel Beschreibung","anchor":{"toolbar":"Anker einfügen/editieren","menu":"Anker-Eigenschaften","title":"Anker-Eigenschaften","name":"Anker Name","errorName":"Bitte geben Sie den Namen des Ankers ein","remove":"Anker entfernen"},"anchorId":"nach Element Id","anchorName":"nach Anker Name","charset":"Ziel-Zeichensatz","cssClasses":"Stylesheet Klasse","emailAddress":"E-Mail Adresse","emailBody":"Nachrichtentext","emailSubject":"Betreffzeile","id":"Id","info":"Link-Info","langCode":"Sprachenkürzel","langDir":"Schreibrichtung","langDirLTR":"Links nach Rechts (LTR)","langDirRTL":"Rechts nach Links (RTL)","menu":"Link editieren","name":"Name","noAnchors":"(keine Anker im Dokument vorhanden)","noEmail":"Bitte geben Sie e-Mail Adresse an","noUrl":"Bitte geben Sie die Link-URL an","other":"<andere>","popupDependent":"Abhängig (Netscape)","popupFeatures":"Pop-up Fenster-Eigenschaften","popupFullScreen":"Vollbild (IE)","popupLeft":"Linke Position","popupLocationBar":"Adress-Leiste","popupMenuBar":"Menü-Leiste","popupResizable":"Größe änderbar","popupScrollBars":"Rollbalken","popupStatusBar":"Statusleiste","popupToolbar":"Symbolleiste","popupTop":"Obere Position","rel":"Beziehung","selectAnchor":"Anker auswählen","styles":"Style","tabIndex":"Tab-Index","target":"Zielseite","targetFrame":"<Frame>","targetFrameName":"Ziel-Fenster-Name","targetPopup":"<Pop-up Fenster>","targetPopupName":"Pop-up Fenster-Name","title":"Link","toAnchor":"Anker in dieser Seite","toEmail":"E-Mail","toUrl":"URL","toolbar":"Link einfügen/editieren","type":"Link-Typ","unlink":"Link entfernen","upload":"Hochladen"},"list":{"bulletedlist":"Liste","numberedlist":"Nummerierte Liste"},"removeformat":{"toolbar":"Formatierungen entfernen"},"undo":{"redo":"Wiederherstellen","undo":"Rückgängig"}};
\ No newline at end of file
+CKEDITOR.lang['de']={"editor":"WYSIWYG-Editor","editorPanel":"WYSIWYG-Editor-Leiste","common":{"editorHelp":"Drücken Sie ALT 0 für Hilfe","browseServer":"Server durchsuchen","url":"URL","protocol":"Protokoll","upload":"Hochladen","uploadSubmit":"Zum Server senden","image":"Bild","flash":"Flash","form":"Formular","checkbox":"Kontrollbox","radio":"Optionsfeld","textField":"Textfeld","textarea":"Textfeld","hiddenField":"Verstecktes Feld","button":"Schaltfläche","select":"Auswahlfeld","imageButton":"Bildschaltfläche","notSet":"<nicht festgelegt>","id":"Kennung","name":"Name","langDir":"Schreibrichtung","langDirLtr":"Links nach Rechts (LTR)","langDirRtl":"Rechts nach Links (RTL)","langCode":"Sprachcode","longDescr":"Langbeschreibungs-URL","cssClass":"Formatvorlagenklassen","advisoryTitle":"Titel Beschreibung","cssStyle":"Stil","ok":"OK","cancel":"Abbrechen","close":"Schließen","preview":"Vorschau","resize":"Größe ändern","generalTab":"Allgemein","advancedTab":"Erweitert","validateNumberFailed":"Dieser Wert ist keine Nummer.","confirmNewPage":"Alle nicht gespeicherten Änderungen gehen verloren. Sind Sie sicher die neue Seite zu laden?","confirmCancel":"Einige Optionen wurden geändert. Wollen Sie den Dialog dennoch schließen?","options":"Optionen","target":"Zielseite","targetNew":"Neues Fenster (_blank)","targetTop":"Oberstes Fenster (_top)","targetSelf":"Gleiches Fenster (_self)","targetParent":"Oberes Fenster (_parent)","langDirLTR":"Links nach Rechts (LNR)","langDirRTL":"Rechts nach Links (RNL)","styles":"Style","cssClasses":"Stylesheet Klasse","width":"Breite","height":"Höhe","align":"Ausrichtung","alignLeft":"Links","alignRight":"Rechts","alignCenter":"Zentriert","alignJustify":"Blocksatz","alignTop":"Oben","alignMiddle":"Mitte","alignBottom":"Unten","alignNone":"Keine","invalidValue":"Ungültiger Wert.","invalidHeight":"Höhe muss eine Zahl sein.","invalidWidth":"Breite muss eine Zahl sein.","invalidCssLength":"Wert spezifiziert für \"%1\" Feld muss ein positiver numerischer Wert sein mit oder ohne korrekte CSS Messeinheit (px, %, in, cm, mm, em, ex, pt oder pc).","invalidHtmlLength":"Wert spezifiziert für \"%1\" Feld muss ein positiver numerischer Wert sein mit oder ohne korrekte HTML Messeinheit (px oder %).","invalidInlineStyle":"Wert spezifiziert für inline Stilart muss enthalten ein oder mehr Tupels mit dem Format \"Name : Wert\" getrennt mit Semikolons.","cssLengthTooltip":"Gebe eine Zahl ein für ein Wert in pixels oder eine Zahl mit einer korrekten CSS Messeinheit (px, %, in, cm, mm, em, ex, pt oder pc).","unavailable":"%1<span class=\"cke_accessibility\">, nicht verfügbar</span>","keyboard":{"8":"Rücktaste","13":"Eingabe","16":"Umschalt","17":"Strg","18":"Alt","32":"Leer","35":"Ende","36":"Pos1","46":"Entfernen","224":"Befehl"},"keyboardShortcut":"Tastaturkürzel"},"basicstyles":{"bold":"Fett","italic":"Kursiv","strike":"Durchgestrichen","subscript":"Tiefgestellt","superscript":"Hochgestellt","underline":"Unterstrichen"},"notification":{"closed":"Benachrichtigung geschlossen."},"button":{"selectedLabel":"%1 (Ausgewählt)"},"toolbar":{"toolbarCollapse":"Werkzeugleiste einklappen","toolbarExpand":"Werkzeugleiste ausklappen","toolbarGroups":{"document":"Dokument","clipboard":"Zwischenablage/Rückgängig","editing":"Editieren","forms":"Formulare","basicstyles":"Grundstile","paragraph":"Absatz","links":"Links","insert":"Einfügen","styles":"Stile","colors":"Farben","tools":"Werkzeuge"},"toolbars":"Editor Werkzeugleisten"},"clipboard":{"copy":"Kopieren","copyError":"Die Sicherheitseinstellungen Ihres Browsers lassen es nicht zu, den Text automatisch kopieren. Bitte benutzen Sie die System-Zwischenablage über STRG-C (kopieren).","cut":"Ausschneiden","cutError":"Die Sicherheitseinstellungen Ihres Browsers lassen es nicht zu, den Text automatisch auszuschneiden. Bitte benutzen Sie die System-Zwischenablage über STRG-X (ausschneiden) und STRG-V (einfügen).","paste":"Einfügen","pasteNotification":"Ihr Browser verhindert das Einfügen über diesen Weg. Zum einfügen drücken Sie %1."},"horizontalrule":{"toolbar":"Horizontale Linie einfügen"},"indent":{"indent":"Einzug erhöhen","outdent":"Einzug verringern"},"fakeobjects":{"anchor":"Anker","flash":"Flash-Animation","hiddenfield":"Verstecktes Feld","iframe":"IFrame","unknown":"Unbekanntes Objekt"},"link":{"acccessKey":"Zugriffstaste","advanced":"Erweitert","advisoryContentType":"Inhaltstyp","advisoryTitle":"Titel Beschreibung","anchor":{"toolbar":"Anker","menu":"Anker bearbeiten","title":"Ankereigenschaften","name":"Ankername","errorName":"Bitte geben Sie den Namen des Ankers ein","remove":"Anker entfernen"},"anchorId":"Nach Elementkennung","anchorName":"Nach Ankername","charset":"Verknüpfter Ressourcenzeichensatz","cssClasses":"Formatvorlagenklasse","download":"Herunterladen erzwingen","displayText":"Anzeigetext","emailAddress":"E-Mail-Adresse","emailBody":"Nachrichtentext","emailSubject":"Betreffzeile","id":"Kennung","info":"Linkinfo","langCode":"Sprachcode","langDir":"Schreibrichtung","langDirLTR":"Links nach Rechts (LTR)","langDirRTL":"Rechts nach Links (RTL)","menu":"Link bearbeiten","name":"Name","noAnchors":"(Keine Anker im Dokument vorhanden)","noEmail":"Bitte geben Sie E-Mail-Adresse an","noUrl":"Bitte geben Sie die Link-URL an","other":"<andere>","popupDependent":"Abhängig (Netscape)","popupFeatures":"Pop-up Fenstereigenschaften","popupFullScreen":"Vollbild (IE)","popupLeft":"Linke Position","popupLocationBar":"Adressleiste","popupMenuBar":"Menüleiste","popupResizable":"Größe änderbar","popupScrollBars":"Rollbalken","popupStatusBar":"Statusleiste","popupToolbar":"Werkzeugleiste","popupTop":"Obere Position","rel":"Beziehung","selectAnchor":"Anker auswählen","styles":"Style","tabIndex":"Tab-Index","target":"Zielseite","targetFrame":"<Frame>","targetFrameName":"Ziel-Fenster-Name","targetPopup":"<Pop-up Fenster>","targetPopupName":"Pop-up Fenster-Name","title":"Link","toAnchor":"Anker in dieser Seite","toEmail":"E-Mail","toUrl":"URL","toolbar":"Link einfügen/editieren","type":"Link-Typ","unlink":"Link entfernen","upload":"Hochladen"},"list":{"bulletedlist":"Liste","numberedlist":"Nummerierte Liste einfügen/entfernen"},"removeformat":{"toolbar":"Formatierung entfernen"},"sourcedialog":{"toolbar":"Quellcode","title":"Quellcode"}};
\ No newline at end of file
index 5eccdf4..897fa47 100644 (file)
@@ -1,5 +1,5 @@
 /*\r
-Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.\r
-For licensing, see LICENSE.html or http://ckeditor.com/license\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
 */
-CKEDITOR.lang['en']={"editor":"Rich Text Editor","editorPanel":"Rich Text Editor panel","common":{"editorHelp":"Press ALT 0 for help","browseServer":"Browse Server","url":"URL","protocol":"Protocol","upload":"Upload","uploadSubmit":"Send it to the Server","image":"Image","flash":"Flash","form":"Form","checkbox":"Checkbox","radio":"Radio Button","textField":"Text Field","textarea":"Textarea","hiddenField":"Hidden Field","button":"Button","select":"Selection Field","imageButton":"Image Button","notSet":"<not set>","id":"Id","name":"Name","langDir":"Language Direction","langDirLtr":"Left to Right (LTR)","langDirRtl":"Right to Left (RTL)","langCode":"Language Code","longDescr":"Long Description URL","cssClass":"Stylesheet Classes","advisoryTitle":"Advisory Title","cssStyle":"Style","ok":"OK","cancel":"Cancel","close":"Close","preview":"Preview","resize":"Resize","generalTab":"General","advancedTab":"Advanced","validateNumberFailed":"This value is not a number.","confirmNewPage":"Any unsaved changes to this content will be lost. Are you sure you want to load new page?","confirmCancel":"You have changed some options. Are you sure you want to close the dialog window?","options":"Options","target":"Target","targetNew":"New Window (_blank)","targetTop":"Topmost Window (_top)","targetSelf":"Same Window (_self)","targetParent":"Parent Window (_parent)","langDirLTR":"Left to Right (LTR)","langDirRTL":"Right to Left (RTL)","styles":"Style","cssClasses":"Stylesheet Classes","width":"Width","height":"Height","align":"Alignment","alignLeft":"Left","alignRight":"Right","alignCenter":"Center","alignTop":"Top","alignMiddle":"Middle","alignBottom":"Bottom","invalidValue":"Invalid value.","invalidHeight":"Height must be a number.","invalidWidth":"Width must be a number.","invalidCssLength":"Value specified for the \"%1\" field must be a positive number with or without a valid CSS measurement unit (px, %, in, cm, mm, em, ex, pt, or pc).","invalidHtmlLength":"Value specified for the \"%1\" field must be a positive number with or without a valid HTML measurement unit (px or %).","invalidInlineStyle":"Value specified for the inline style must consist of one or more tuples with the format of \"name : value\", separated by semi-colons.","cssLengthTooltip":"Enter a number for a value in pixels or a number with a valid CSS unit (px, %, in, cm, mm, em, ex, pt, or pc).","unavailable":"%1<span class=\"cke_accessibility\">, unavailable</span>"},"about":{"copy":"Copyright &copy; $1. All rights reserved.","dlgTitle":"About CKEditor","help":"Check $1 for help.","moreInfo":"For licensing information please visit our web site:","title":"About CKEditor","userGuide":"CKEditor User's Guide"},"basicstyles":{"bold":"Bold","italic":"Italic","strike":"Strike Through","subscript":"Subscript","superscript":"Superscript","underline":"Underline"},"clipboard":{"copy":"Copy","copyError":"Your browser security settings don't permit the editor to automatically execute copying operations. Please use the keyboard for that (Ctrl/Cmd+C).","cut":"Cut","cutError":"Your browser security settings don't permit the editor to automatically execute cutting operations. Please use the keyboard for that (Ctrl/Cmd+X).","paste":"Paste","pasteArea":"Paste Area","pasteMsg":"Please paste inside the following box using the keyboard (<strong>Ctrl/Cmd+V</strong>) and hit OK","securityMsg":"Because of your browser security settings, the editor is not able to access your clipboard data directly. You are required to paste it again in this window.","title":"Paste"},"toolbar":{"toolbarCollapse":"Collapse Toolbar","toolbarExpand":"Expand Toolbar","toolbarGroups":{"document":"Document","clipboard":"Clipboard/Undo","editing":"Editing","forms":"Forms","basicstyles":"Basic Styles","paragraph":"Paragraph","links":"Links","insert":"Insert","styles":"Styles","colors":"Colors","tools":"Tools"},"toolbars":"Editor toolbars"},"horizontalrule":{"toolbar":"Insert Horizontal Line"},"indent":{"indent":"Increase Indent","outdent":"Decrease Indent"},"fakeobjects":{"anchor":"Anchor","flash":"Flash Animation","hiddenfield":"Hidden Field","iframe":"IFrame","unknown":"Unknown Object"},"link":{"acccessKey":"Access Key","advanced":"Advanced","advisoryContentType":"Advisory Content Type","advisoryTitle":"Advisory Title","anchor":{"toolbar":"Anchor","menu":"Edit Anchor","title":"Anchor Properties","name":"Anchor Name","errorName":"Please type the anchor name","remove":"Remove Anchor"},"anchorId":"By Element Id","anchorName":"By Anchor Name","charset":"Linked Resource Charset","cssClasses":"Stylesheet Classes","emailAddress":"E-Mail Address","emailBody":"Message Body","emailSubject":"Message Subject","id":"Id","info":"Link Info","langCode":"Language Code","langDir":"Language Direction","langDirLTR":"Left to Right (LTR)","langDirRTL":"Right to Left (RTL)","menu":"Edit Link","name":"Name","noAnchors":"(No anchors available in the document)","noEmail":"Please type the e-mail address","noUrl":"Please type the link URL","other":"<other>","popupDependent":"Dependent (Netscape)","popupFeatures":"Popup Window Features","popupFullScreen":"Full Screen (IE)","popupLeft":"Left Position","popupLocationBar":"Location Bar","popupMenuBar":"Menu Bar","popupResizable":"Resizable","popupScrollBars":"Scroll Bars","popupStatusBar":"Status Bar","popupToolbar":"Toolbar","popupTop":"Top Position","rel":"Relationship","selectAnchor":"Select an Anchor","styles":"Style","tabIndex":"Tab Index","target":"Target","targetFrame":"<frame>","targetFrameName":"Target Frame Name","targetPopup":"<popup window>","targetPopupName":"Popup Window Name","title":"Link","toAnchor":"Link to anchor in the text","toEmail":"E-mail","toUrl":"URL","toolbar":"Link","type":"Link Type","unlink":"Unlink","upload":"Upload"},"list":{"bulletedlist":"Insert/Remove Bulleted List","numberedlist":"Insert/Remove Numbered List"},"removeformat":{"toolbar":"Remove Format"},"undo":{"redo":"Redo","undo":"Undo"}};
\ No newline at end of file
+CKEDITOR.lang['en']={"editor":"Rich Text Editor","editorPanel":"Rich Text Editor panel","common":{"editorHelp":"Press ALT 0 for help","browseServer":"Browse Server","url":"URL","protocol":"Protocol","upload":"Upload","uploadSubmit":"Send it to the Server","image":"Image","flash":"Flash","form":"Form","checkbox":"Checkbox","radio":"Radio Button","textField":"Text Field","textarea":"Textarea","hiddenField":"Hidden Field","button":"Button","select":"Selection Field","imageButton":"Image Button","notSet":"<not set>","id":"Id","name":"Name","langDir":"Language Direction","langDirLtr":"Left to Right (LTR)","langDirRtl":"Right to Left (RTL)","langCode":"Language Code","longDescr":"Long Description URL","cssClass":"Stylesheet Classes","advisoryTitle":"Advisory Title","cssStyle":"Style","ok":"OK","cancel":"Cancel","close":"Close","preview":"Preview","resize":"Resize","generalTab":"General","advancedTab":"Advanced","validateNumberFailed":"This value is not a number.","confirmNewPage":"Any unsaved changes to this content will be lost. Are you sure you want to load new page?","confirmCancel":"You have changed some options. Are you sure you want to close the dialog window?","options":"Options","target":"Target","targetNew":"New Window (_blank)","targetTop":"Topmost Window (_top)","targetSelf":"Same Window (_self)","targetParent":"Parent Window (_parent)","langDirLTR":"Left to Right (LTR)","langDirRTL":"Right to Left (RTL)","styles":"Style","cssClasses":"Stylesheet Classes","width":"Width","height":"Height","align":"Alignment","alignLeft":"Left","alignRight":"Right","alignCenter":"Center","alignJustify":"Justify","alignTop":"Top","alignMiddle":"Middle","alignBottom":"Bottom","alignNone":"None","invalidValue":"Invalid value.","invalidHeight":"Height must be a number.","invalidWidth":"Width must be a number.","invalidCssLength":"Value specified for the \"%1\" field must be a positive number with or without a valid CSS measurement unit (px, %, in, cm, mm, em, ex, pt, or pc).","invalidHtmlLength":"Value specified for the \"%1\" field must be a positive number with or without a valid HTML measurement unit (px or %).","invalidInlineStyle":"Value specified for the inline style must consist of one or more tuples with the format of \"name : value\", separated by semi-colons.","cssLengthTooltip":"Enter a number for a value in pixels or a number with a valid CSS unit (px, %, in, cm, mm, em, ex, pt, or pc).","unavailable":"%1<span class=\"cke_accessibility\">, unavailable</span>","keyboard":{"8":"Backspace","13":"Enter","16":"Shift","17":"Ctrl","18":"Alt","32":"Space","35":"End","36":"Home","46":"Delete","224":"Command"},"keyboardShortcut":"Keyboard shortcut"},"basicstyles":{"bold":"Bold","italic":"Italic","strike":"Strikethrough","subscript":"Subscript","superscript":"Superscript","underline":"Underline"},"notification":{"closed":"Notification closed."},"button":{"selectedLabel":"%1 (Selected)"},"toolbar":{"toolbarCollapse":"Collapse Toolbar","toolbarExpand":"Expand Toolbar","toolbarGroups":{"document":"Document","clipboard":"Clipboard/Undo","editing":"Editing","forms":"Forms","basicstyles":"Basic Styles","paragraph":"Paragraph","links":"Links","insert":"Insert","styles":"Styles","colors":"Colors","tools":"Tools"},"toolbars":"Editor toolbars"},"clipboard":{"copy":"Copy","copyError":"Your browser security settings don't permit the editor to automatically execute copying operations. Please use the keyboard for that (Ctrl/Cmd+C).","cut":"Cut","cutError":"Your browser security settings don't permit the editor to automatically execute cutting operations. Please use the keyboard for that (Ctrl/Cmd+X).","paste":"Paste","pasteNotification":"Your browser doesn't allow you to paste this way. Press %1 to paste."},"horizontalrule":{"toolbar":"Insert Horizontal Line"},"indent":{"indent":"Increase Indent","outdent":"Decrease Indent"},"fakeobjects":{"anchor":"Anchor","flash":"Flash Animation","hiddenfield":"Hidden Field","iframe":"IFrame","unknown":"Unknown Object"},"link":{"acccessKey":"Access Key","advanced":"Advanced","advisoryContentType":"Advisory Content Type","advisoryTitle":"Advisory Title","anchor":{"toolbar":"Anchor","menu":"Edit Anchor","title":"Anchor Properties","name":"Anchor Name","errorName":"Please type the anchor name","remove":"Remove Anchor"},"anchorId":"By Element Id","anchorName":"By Anchor Name","charset":"Linked Resource Charset","cssClasses":"Stylesheet Classes","download":"Force Download","displayText":"Display Text","emailAddress":"E-Mail Address","emailBody":"Message Body","emailSubject":"Message Subject","id":"Id","info":"Link Info","langCode":"Language Code","langDir":"Language Direction","langDirLTR":"Left to Right (LTR)","langDirRTL":"Right to Left (RTL)","menu":"Edit Link","name":"Name","noAnchors":"(No anchors available in the document)","noEmail":"Please type the e-mail address","noUrl":"Please type the link URL","other":"<other>","popupDependent":"Dependent (Netscape)","popupFeatures":"Popup Window Features","popupFullScreen":"Full Screen (IE)","popupLeft":"Left Position","popupLocationBar":"Location Bar","popupMenuBar":"Menu Bar","popupResizable":"Resizable","popupScrollBars":"Scroll Bars","popupStatusBar":"Status Bar","popupToolbar":"Toolbar","popupTop":"Top Position","rel":"Relationship","selectAnchor":"Select an Anchor","styles":"Style","tabIndex":"Tab Index","target":"Target","targetFrame":"<frame>","targetFrameName":"Target Frame Name","targetPopup":"<popup window>","targetPopupName":"Popup Window Name","title":"Link","toAnchor":"Link to anchor in the text","toEmail":"E-mail","toUrl":"URL","toolbar":"Link","type":"Link Type","unlink":"Unlink","upload":"Upload"},"list":{"bulletedlist":"Insert/Remove Bulleted List","numberedlist":"Insert/Remove Numbered List"},"removeformat":{"toolbar":"Remove Format"},"sourcedialog":{"toolbar":"Source","title":"Source"}};
\ No newline at end of file
diff --git a/js/ckeditor/plugins/about/dialogs/about.js b/js/ckeditor/plugins/about/dialogs/about.js
deleted file mode 100644 (file)
index d9b8194..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-/*
- Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
- For licensing, see LICENSE.md or http://ckeditor.com/license
-*/
-CKEDITOR.dialog.add("about",function(a){var a=a.lang.about,b=CKEDITOR.plugins.get("about").path+"dialogs/"+(CKEDITOR.env.hidpi?"hidpi/":"")+"logo_ckeditor.png";return{title:CKEDITOR.env.ie?a.dlgTitle:a.title,minWidth:390,minHeight:230,contents:[{id:"tab1",label:"",title:"",expand:!0,padding:0,elements:[{type:"html",html:'<style type="text/css">.cke_about_container{color:#000 !important;padding:10px 10px 0;margin-top:5px}.cke_about_container p{margin: 0 0 10px;}.cke_about_container .cke_about_logo{height:81px;background-color:#fff;background-image:url('+
-b+");"+(CKEDITOR.env.hidpi?"background-size:163px 58px;":"")+'background-position:center; background-repeat:no-repeat;margin-bottom:10px;}.cke_about_container a{cursor:pointer !important;color:#00B2CE !important;text-decoration:underline !important;}</style><div class="cke_about_container"><div class="cke_about_logo"></div><p>CKEditor '+CKEDITOR.version+" (revision "+CKEDITOR.revision+')<br><a href="http://ckeditor.com/">http://ckeditor.com</a></p><p>'+a.help.replace("$1",'<a href="http://docs.ckeditor.com/user">'+
-a.userGuide+"</a>")+"</p><p>"+a.moreInfo+'<br><a href="http://ckeditor.com/about/license">http://ckeditor.com/about/license</a></p><p>'+a.copy.replace("$1",'<a href="http://cksource.com/">CKSource</a> - Frederico Knabben')+"</p></div>"}]}],buttons:[CKEDITOR.dialog.cancelButton]}});
\ No newline at end of file
diff --git a/js/ckeditor/plugins/about/dialogs/hidpi/logo_ckeditor.png b/js/ckeditor/plugins/about/dialogs/hidpi/logo_ckeditor.png
deleted file mode 100644 (file)
index 10cc736..0000000
Binary files a/js/ckeditor/plugins/about/dialogs/hidpi/logo_ckeditor.png and /dev/null differ
diff --git a/js/ckeditor/plugins/about/dialogs/logo_ckeditor.png b/js/ckeditor/plugins/about/dialogs/logo_ckeditor.png
deleted file mode 100644 (file)
index f186eb8..0000000
Binary files a/js/ckeditor/plugins/about/dialogs/logo_ckeditor.png and /dev/null differ
diff --git a/js/ckeditor/plugins/clipboard/dialogs/paste.js b/js/ckeditor/plugins/clipboard/dialogs/paste.js
deleted file mode 100644 (file)
index 0c7e256..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
- Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
- For licensing, see LICENSE.md or http://ckeditor.com/license
-*/
-CKEDITOR.dialog.add("paste",function(c){function h(a){var b=new CKEDITOR.dom.document(a.document),f=b.getBody(),d=b.getById("cke_actscrpt");d&&d.remove();f.setAttribute("contenteditable",!0);if(CKEDITOR.env.ie&&8>CKEDITOR.env.version)b.getWindow().on("blur",function(){b.$.selection.empty()});b.on("keydown",function(a){var a=a.data,b;switch(a.getKeystroke()){case 27:this.hide();b=1;break;case 9:case CKEDITOR.SHIFT+9:this.changeFocus(1),b=1}b&&a.preventDefault()},this);c.fire("ariaWidget",new CKEDITOR.dom.element(a.frameElement));
-b.getWindow().getFrame().removeCustomData("pendingFocus")&&f.focus()}var e=c.lang.clipboard;c.on("pasteDialogCommit",function(a){a.data&&c.fire("paste",{type:"auto",dataValue:a.data})},null,null,1E3);return{title:e.title,minWidth:CKEDITOR.env.ie&&CKEDITOR.env.quirks?370:350,minHeight:CKEDITOR.env.quirks?250:245,onShow:function(){this.parts.dialog.$.offsetHeight;this.setupContent();this.parts.title.setHtml(this.customTitle||e.title);this.customTitle=null},onLoad:function(){(CKEDITOR.env.ie7Compat||
-CKEDITOR.env.ie6Compat)&&"rtl"==c.lang.dir&&this.parts.contents.setStyle("overflow","hidden")},onOk:function(){this.commitContent()},contents:[{id:"general",label:c.lang.common.generalTab,elements:[{type:"html",id:"securityMsg",html:'<div style="white-space:normal;width:340px">'+e.securityMsg+"</div>"},{type:"html",id:"pasteMsg",html:'<div style="white-space:normal;width:340px">'+e.pasteMsg+"</div>"},{type:"html",id:"editing_area",style:"width:100%;height:100%",html:"",focus:function(){var a=this.getInputElement(),
-b=a.getFrameDocument().getBody();!b||b.isReadOnly()?a.setCustomData("pendingFocus",1):b.focus()},setup:function(){var a=this.getDialog(),b='<html dir="'+c.config.contentsLangDirection+'" lang="'+(c.config.contentsLanguage||c.langCode)+'"><head><style>body{margin:3px;height:95%}</style></head><body><script id="cke_actscrpt" type="text/javascript">window.parent.CKEDITOR.tools.callFunction('+CKEDITOR.tools.addFunction(h,a)+",this);<\/script></body></html>",f=CKEDITOR.env.air?"javascript:void(0)":CKEDITOR.env.ie?
-"javascript:void((function(){"+encodeURIComponent("document.open();("+CKEDITOR.tools.fixDomain+")();document.close();")+'})())"':"",d=CKEDITOR.dom.element.createFromHtml('<iframe class="cke_pasteframe" frameborder="0"  allowTransparency="true" src="'+f+'" role="region" aria-label="'+e.pasteArea+'" aria-describedby="'+a.getContentElement("general","pasteMsg").domId+'" aria-multiple="true"></iframe>');d.on("load",function(a){a.removeListener();a=d.getFrameDocument();a.write(b);c.focusManager.add(a.getBody());
-CKEDITOR.env.air&&h.call(this,a.getWindow().$)},a);d.setCustomData("dialog",a);a=this.getElement();a.setHtml("");a.append(d);if(CKEDITOR.env.ie){var g=CKEDITOR.dom.element.createFromHtml('<span tabindex="-1" style="position:absolute" role="presentation"></span>');g.on("focus",function(){setTimeout(function(){d.$.contentWindow.focus()})});a.append(g);this.focus=function(){g.focus();this.fire("focus")}}this.getInputElement=function(){return d};CKEDITOR.env.ie&&(a.setStyle("display","block"),a.setStyle("height",
-d.$.offsetHeight+2+"px"))},commit:function(){var a=this.getDialog().getParentEditor(),b=this.getInputElement().getFrameDocument().getBody(),c=b.getBogus(),d;c&&c.remove();d=b.getHtml();setTimeout(function(){a.fire("pasteDialogCommit",d)},0)}}]}]}});
\ No newline at end of file
diff --git a/js/ckeditor/plugins/codemirror/css/codemirror.min.css b/js/ckeditor/plugins/codemirror/css/codemirror.min.css
new file mode 100644 (file)
index 0000000..dfd7af0
--- /dev/null
@@ -0,0 +1 @@
+.CodeMirror{font-family:monospace;height:300px;color:black}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{background-color:white}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:black}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid black;border-right:0;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:-20px;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:blue}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:bold}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3,.cm-s-default .cm-type{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta{color:#555}.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-s-default .cm-error{color:#f00}.cm-invalidchar{color:#f00}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:white}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-vscrollbar,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:none!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:transparent;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:0}.CodeMirror-scroll,.CodeMirror-sizer,.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0}.CodeMirror{font:13px/1.4em monospace;text-align:left}.CodeMirror .activeline{background:#e8f2ff}.CodeMirror .CodeMirror-foldmarker{color:blue;-ms-text-shadow:#b9f 1px 1px 2px,#b9f -1px -1px 2px,#b9f 1px -1px 2px,#b9f -1px 1px 2px;-webkit-text-shadow:#b9f 1px 1px 2px,#b9f -1px -1px 2px,#b9f 1px -1px 2px,#b9f -1px 1px 2px;text-shadow:#b9f 1px 1px 2px,#b9f -1px -1px 2px,#b9f 1px -1px 2px,#b9f -1px 1px 2px;font-family:arial;line-height:.3;cursor:pointer}.CodeMirror-matchingtag{background:#ff9600;background:rgba(255,150,0,0.3)}.searchCodeButton span,.autoFormat span,.CommentSelectedRange span,.UncommentSelectedRange span{width:16px;height:16px;margin-left:6px}.searchCodeButton span{background:url("../icons/searchcode.png") no-repeat}.autoFormat span{background:url("../icons/autoformat.png") no-repeat}.CommentSelectedRange span{background:url("../icons/commentselectedrange.png") no-repeat}.UncommentSelectedRange span{background:url("../icons/uncommentselectedrange.png") no-repeat}.cke_reset_all .CodeMirror-scroll *{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.cke_reset_all .cm-s-cobalt *,.cke_reset_all .cm-s-erlang-dark *,.cke_reset_all .cm-s-lesser-dark *,.cke_reset_all .cm-s-monokai *,.cke_reset_all .cm-s-night *,.cke_reset_all .cm-s-rubyblue *,.cke_reset_all .cm-s-twilight *,.cke_reset_all .cm-s-xq-dark *,.cke_reset_all .cm-s-base16-dark *,.cke_reset_all .cm-s-3024-night *,.cke_reset_all .cm-s-the-matrix *,.cke_reset_all .cm-s-paraiso-dark *,.cke_reset_all .cm-s-paraiso-light *{color:inherit;font:inherit}.cm-s-cobalt .CodeMirror-selected{background:#b36539!important}.cm-s-erlang-dark .CodeMirror-selected{background:#b36539!important}.cm-s-lesser-dark .CodeMirror-selected{background:#45443b!important}.cm-s-monokai .CodeMirror-selected{background:#49483e!important}.cm-s-night .CodeMirror-selected{background:#447!important}.cm-s-rubyblue .CodeMirror-selected{background:#38566f!important}.cm-s-twilight .CodeMirror-selected{background:#323232!important}.cm-s-xq-dark .CodeMirror-selected{background:#a8f!important}.cm-s-the-matrix .CodeMirror-selected{background:#494949!important}.cm-s-mbo .CodeMirror-selected{background:#716c62!important}.cm-s-blackboard .activeline,.cm-s-cobalt .activeline,.cm-s-erlang-dark .activeline,.cm-s-lesser-dark .activeline,.cm-s-monokai .activeline,.cm-s-night .activeline,.cm-s-rubyblue .activeline,.cm-s-vibrant-ink .activeline,.cm-s-xq-dark .activeline,.cm-s-base16-dark .activeline,.cm-s-3024-night .activeline,.cm-s-paraiso-light .activeline,.cm-s-paraiso-dark .activeline,.cm-s-pastel-on-dark .activeline{background:#757575}.cm-s-pastel-on-dark .activeline{background:#404040}.cm-s-mbo .activeline{background:#716c62}.cm-s-twilight .activeline{background:#494949}.cm-s-the-matrix .activeline{background:#060}.CodeMirror-focused .cm-matchhighlight{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);background-position:bottom;background-repeat:repeat-x}.CodeMirror-hints{position:absolute;z-index:10;overflow:hidden;list-style:none;margin:0;padding:2px;-webkit-box-shadow:2px 3px 5px #000;-ms-box-shadow:2px 3px 5px #000;box-shadow:2px 3px 5px #000;border-radius:3px;border:1px solid silver;background:white;font-size:90%;font-family:monospace;max-height:20em;overflow-y:auto}.CodeMirror-hint{margin:0;padding:0 4px;border-radius:2px;max-width:19em;overflow:hidden;white-space:pre;color:black;cursor:pointer}.CodeMirror-hint-active{background:#08f;color:white}.cm-trailingspace{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QUXCToH00Y1UgAAACFJREFUCNdjPMDBUc/AwNDAAAFMTAwMDA0OP34wQgX/AQBYgwYEx4f9lQAAAABJRU5ErkJggg==);background-position:bottom left;background-repeat:repeat-x}.CodeMirror-dialog{position:absolute;left:0;right:0;background:inherit;z-index:15;padding:.1em .8em;overflow:hidden;color:inherit}.CodeMirror-dialog-top{border-bottom:1px solid #eee;top:0}.CodeMirror-dialog-bottom{border-top:1px solid #eee;bottom:0}.CodeMirror-dialog input{border:0;outline:0;background:transparent;width:20em;color:inherit;font-family:monospace}.CodeMirror-dialog button{font-size:70%}.CodeMirror-foldmarker{color:blue;text-shadow:#b9f 1px 1px 2px,#b9f -1px -1px 2px,#b9f 1px -1px 2px,#b9f -1px 1px 2px;font-family:arial;line-height:.3;cursor:pointer}.CodeMirror-foldgutter{width:.7em}.CodeMirror-foldgutter-open,.CodeMirror-foldgutter-folded{cursor:pointer}.CodeMirror-foldgutter-open:after{content:"\25BE"}.CodeMirror-foldgutter-folded:after{content:"\25B8"}\r
diff --git a/js/ckeditor/plugins/codemirror/js/beautify.min.js b/js/ckeditor/plugins/codemirror/js/beautify.min.js
new file mode 100644 (file)
index 0000000..33cdfb6
--- /dev/null
@@ -0,0 +1,75 @@
+!function(){function f(a,l){for(var d=0;d<l.length;d+=1)if(l[d]===a)return!0;return!1}function x(a,l){return(new I(a,l)).beautify()}function I(r,l){function w(c,b){var e=0;return c&&(e=c.indentation_level,!g.just_added_newline()&&c.line_indent_level>e&&(e=c.line_indent_level)),{mode:b,parent:c,last_text:c?c.last_text:"",last_word:c?c.last_word:"",declaration_statement:!1,declaration_assignment:!1,multiline_frame:!1,if_block:!1,else_block:!1,do_block:!1,do_while:!1,in_case_statement:!1,in_case:!1,
+case_body:!1,indentation_level:e,line_indent_level:c?c.line_indent_level:e,start_line_index:g.get_line_number(),ternary_depth:0}}function p(b){var e=b.newlines;if(m.keep_array_indentation&&c.mode===a.ArrayLiteral)for(g=0;g<e;g+=1)q(0<g);else if(m.max_preserve_newlines&&e>m.max_preserve_newlines&&(e=m.max_preserve_newlines),m.preserve_newlines&&1<b.newlines){q();for(var g=1;g<e;g+=1)q(!0)}h=b;O[h.type]()}function B(c){(c=void 0!==c&&c,g.just_added_newline())||(m.preserve_newlines&&h.wanted_newline||
+c?q(!1,!0):m.wrap_line_length&&g.current_line.get_character_count()+h.text.length+(g.space_before_token?1:0)>=m.wrap_line_length&&q(!1,!0))}function q(b,e){if(!e&&";"!==c.last_text&&","!==c.last_text&&"\x3d"!==c.last_text&&"TK_OPERATOR"!==k)for(;c.mode===a.Statement&&!c.if_block&&!c.do_block;)z();g.add_new_line(b)&&(c.multiline_frame=!0)}function K(){g.just_added_newline()&&(m.keep_array_indentation&&c.mode===a.ArrayLiteral&&h.wanted_newline?(g.current_line.push(h.whitespace_before),g.space_before_token=
+!1):g.set_indent(c.indentation_level)&&(c.line_indent_level=c.indentation_level))}function t(c){if(g.raw)return void g.add_raw_token(h);m.comma_first&&"TK_COMMA"===k&&g.just_added_newline()&&","===g.previous_line.last()&&(g.previous_line.pop(),K(),g.add_token(","),g.space_before_token=!0);c=c||h.text;K();g.add_token(c)}function D(b){c?(N.push(c),e=c):e=w(null,b);c=w(e,b)}function E(c){return f(c,[a.Expression,a.ForInitializer,a.Conditional])}function z(){0<N.length&&(e=c,c=N.pop(),e.mode===a.Statement&&
+g.remove_redundant_indentation(e))}function C(){return c.parent.mode===a.ObjectLiteral&&c.mode===a.Statement&&(":"===c.last_text&&0===c.ternary_depth||"TK_RESERVED"===k&&f(c.last_text,["get","set"]))}function b(){var b;if(b=!!("TK_RESERVED"===k&&f(c.last_text,["var","let","const"])&&"TK_WORD"===h.type||"TK_RESERVED"===k&&"do"===c.last_text||"TK_RESERVED"===k&&"return"===c.last_text&&!h.wanted_newline||"TK_RESERVED"===k&&"else"===c.last_text&&("TK_RESERVED"!==h.type||"if"!==h.text)||"TK_END_EXPR"===
+k&&(e.mode===a.ForInitializer||e.mode===a.Conditional)||"TK_WORD"===k&&c.mode===a.BlockStatement&&!c.in_case&&"--"!==h.text&&"++"!==h.text&&"function"!==G&&"TK_WORD"!==h.type&&"TK_RESERVED"!==h.type||c.mode===a.ObjectLiteral&&(":"===c.last_text&&0===c.ternary_depth||"TK_RESERVED"===k&&f(c.last_text,["get","set"]))))D(a.Statement),c.indentation_level+=1,b=("TK_RESERVED"===k&&f(c.last_text,["var","let","const"])&&"TK_WORD"===h.type&&(c.declaration_statement=!0),C()||B("TK_RESERVED"===h.type&&f(h.text,
+["do","for","if","while"])),!0);return b}function u(c){return f(c,"case return do if throw else".split(" "))}function y(c){c=L+(c||0);return 0>c||c>=x.length?null:x[c]}function F(){("TK_RESERVED"===h.type&&c.mode!==a.ObjectLiteral&&f(h.text,["set","get"])&&(h.type="TK_WORD"),"TK_RESERVED"===h.type&&c.mode===a.ObjectLiteral)&&":"==y(1).text&&(h.type="TK_WORD");if(b()||!h.wanted_newline||E(c.mode)||"TK_OPERATOR"===k&&"--"!==c.last_text&&"++"!==c.last_text||"TK_EQUALS"===k||!m.preserve_newlines&&"TK_RESERVED"===
+k&&f(c.last_text,["var","let","const","set","get"])||q(),c.do_block&&!c.do_while){if("TK_RESERVED"===h.type&&"while"===h.text)return g.space_before_token=!0,t(),g.space_before_token=!0,void(c.do_while=!0);q();c.do_block=!1}if(c.if_block)if(c.else_block||"TK_RESERVED"!==h.type||"else"!==h.text){for(;c.mode===a.Statement;)z();c.if_block=!1;c.else_block=!1}else c.else_block=!0;if("TK_RESERVED"===h.type&&("case"===h.text||"default"===h.text&&c.in_case_statement)){q();if(c.case_body||m.jslint_happy)0<
+c.indentation_level&&(!c.parent||c.indentation_level>c.parent.indentation_level)&&--c.indentation_level,c.case_body=!1;return t(),c.in_case=!0,void(c.in_case_statement=!0)}if("TK_RESERVED"===h.type&&"function"===h.text&&((f(c.last_text,["}",";"])||g.just_added_newline()&&!f(c.last_text,["[","{",":","\x3d",","]))&&(g.just_added_blankline()||h.comments_before.length||(q(),q(!0))),"TK_RESERVED"===k||"TK_WORD"===k?"TK_RESERVED"===k&&f(c.last_text,"get set new return export async".split(" "))?g.space_before_token=
+!0:"TK_RESERVED"===k&&"default"===c.last_text&&"export"===G?g.space_before_token=!0:q():"TK_OPERATOR"===k||"\x3d"===c.last_text?g.space_before_token=!0:(c.multiline_frame||!E(c.mode)&&c.mode!==a.ArrayLiteral)&&q()),"TK_COMMA"!==k&&"TK_START_EXPR"!==k&&"TK_EQUALS"!==k&&"TK_OPERATOR"!==k||C()||B(),"TK_RESERVED"===h.type&&f(h.text,["function","get","set"]))return t(),void(c.last_word=h.text);(v="NONE","TK_END_BLOCK"===k?"TK_RESERVED"===h.type&&f(h.text,["else","catch","finally"])?"expand"===m.brace_style||
+"end-expand"===m.brace_style||"none"===m.brace_style&&h.wanted_newline?v="NEWLINE":(v="SPACE",g.space_before_token=!0):v="NEWLINE":"TK_SEMICOLON"===k&&c.mode===a.BlockStatement?v="NEWLINE":"TK_SEMICOLON"===k&&E(c.mode)?v="SPACE":"TK_STRING"===k?v="NEWLINE":"TK_RESERVED"===k||"TK_WORD"===k||"*"===c.last_text&&"function"===G?v="SPACE":"TK_START_BLOCK"===k?v="NEWLINE":"TK_END_EXPR"===k&&(g.space_before_token=!0,v="NEWLINE"),"TK_RESERVED"===h.type&&f(h.text,H.line_starters)&&")"!==c.last_text&&(v="else"===
+c.last_text||"export"===c.last_text?"SPACE":"NEWLINE"),"TK_RESERVED"===h.type&&f(h.text,["else","catch","finally"]))?"TK_END_BLOCK"!==k||"expand"===m.brace_style||"end-expand"===m.brace_style||"none"===m.brace_style&&h.wanted_newline?q():(g.trim(!0),"}"!==g.current_line.last()&&q(),g.space_before_token=!0):"NEWLINE"===v?"TK_RESERVED"===k&&u(c.last_text)?g.space_before_token=!0:"TK_END_EXPR"!==k?"TK_START_EXPR"===k&&"TK_RESERVED"===h.type&&f(h.text,["var","let","const"])||":"===c.last_text||("TK_RESERVED"===
+h.type&&"if"===h.text&&"else"===c.last_text?g.space_before_token=!0:q()):"TK_RESERVED"===h.type&&f(h.text,H.line_starters)&&")"!==c.last_text&&q():c.multiline_frame&&c.mode===a.ArrayLiteral&&","===c.last_text&&"}"===G?q():"SPACE"===v&&(g.space_before_token=!0);t();c.last_word=h.text;"TK_RESERVED"===h.type&&"do"===h.text&&(c.do_block=!0);"TK_RESERVED"===h.type&&"if"===h.text&&(c.if_block=!0)}var g,L,H,h,k,G,n,c,e,N,v,O,m,x=[],J="";O={TK_START_EXPR:function(){b();var e=a.Expression;if("["===h.text){if("TK_WORD"===
+k||")"===c.last_text)return"TK_RESERVED"===k&&f(c.last_text,H.line_starters)&&(g.space_before_token=!0),D(e),t(),c.indentation_level+=1,void(m.space_in_paren&&(g.space_before_token=!0));e=a.ArrayLiteral;c.mode===a.ArrayLiteral&&("["!==c.last_text&&(","!==c.last_text||"]"!==G&&"}"!==G)||m.keep_array_indentation||q())}else"TK_RESERVED"===k&&"for"===c.last_text?e=a.ForInitializer:"TK_RESERVED"===k&&f(c.last_text,["if","while"])&&(e=a.Conditional);";"===c.last_text||"TK_START_BLOCK"===k?q():"TK_END_EXPR"===
+k||"TK_START_EXPR"===k||"TK_END_BLOCK"===k||"."===c.last_text?B(h.wanted_newline):"TK_RESERVED"===k&&"("===h.text||"TK_WORD"===k||"TK_OPERATOR"===k?"TK_RESERVED"===k&&("function"===c.last_word||"typeof"===c.last_word)||"*"===c.last_text&&"function"===G?m.space_after_anon_function&&(g.space_before_token=!0):"TK_RESERVED"!==k||!f(c.last_text,H.line_starters)&&"catch"!==c.last_text||m.space_before_conditional&&(g.space_before_token=!0):g.space_before_token=!0;"("===h.text&&"TK_RESERVED"===k&&"await"===
+c.last_word&&(g.space_before_token=!0);"("===h.text&&("TK_EQUALS"!==k&&"TK_OPERATOR"!==k||C()||B());D(e);t();m.space_in_paren&&(g.space_before_token=!0);c.indentation_level+=1},TK_END_EXPR:function(){for(;c.mode===a.Statement;)z();c.multiline_frame&&B("]"===h.text&&c.mode===a.ArrayLiteral&&!m.keep_array_indentation);m.space_in_paren&&("TK_START_EXPR"!==k||m.space_in_empty_paren?g.space_before_token=!0:(g.trim(),g.space_before_token=!1));"]"===h.text&&m.keep_array_indentation?(t(),z()):(z(),t());g.remove_redundant_indentation(e);
+c.do_while&&e.mode===a.Conditional&&(e.mode=a.Expression,c.do_block=!1,c.do_while=!1)},TK_START_BLOCK:function(){var b=y(1),r=y(2);D(r&&(":"===r.text&&f(b.type,["TK_STRING","TK_WORD","TK_RESERVED"])||f(b.text,["get","set"])&&f(r.type,["TK_WORD","TK_RESERVED"]))?f(G,["class","interface"])?a.BlockStatement:a.ObjectLiteral:a.BlockStatement);b=!b.comments_before.length&&"}"===b.text&&"function"===c.last_word&&"TK_END_EXPR"===k;"expand"===m.brace_style||"none"===m.brace_style&&h.wanted_newline?"TK_OPERATOR"!==
+k&&(b||"TK_EQUALS"===k||"TK_RESERVED"===k&&u(c.last_text)&&"else"!==c.last_text)?g.space_before_token=!0:q(!1,!0):"TK_OPERATOR"!==k&&"TK_START_EXPR"!==k?"TK_START_BLOCK"===k?q():g.space_before_token=!0:e.mode===a.ArrayLiteral&&","===c.last_text&&("}"===G?g.space_before_token=!0:q());t();c.indentation_level+=1},TK_END_BLOCK:function(){for(;c.mode===a.Statement;)z();var b="TK_START_BLOCK"===k;"expand"===m.brace_style?b||q():b||(c.mode===a.ArrayLiteral&&m.keep_array_indentation?(m.keep_array_indentation=
+!1,q(),m.keep_array_indentation=!0):q());z();t()},TK_WORD:F,TK_RESERVED:F,TK_SEMICOLON:function(){for(b()&&(g.space_before_token=!1);c.mode===a.Statement&&!c.if_block&&!c.do_block;)z();t()},TK_STRING:function(){b()?g.space_before_token=!0:"TK_RESERVED"===k||"TK_WORD"===k?g.space_before_token=!0:"TK_COMMA"===k||"TK_START_EXPR"===k||"TK_EQUALS"===k||"TK_OPERATOR"===k?C()||B():q();t()},TK_EQUALS:function(){b();c.declaration_statement&&(c.declaration_assignment=!0);g.space_before_token=!0;t();g.space_before_token=
+!0},TK_OPERATOR:function(){if(b(),"TK_RESERVED"===k&&u(c.last_text))return g.space_before_token=!0,void t();if("*"===h.text&&"TK_DOT"===k)return void t();if(":"===h.text&&c.in_case)return c.case_body=!0,c.indentation_level+=1,t(),q(),void(c.in_case=!1);if("::"===h.text)return void t();"TK_OPERATOR"===k&&B();var e=!0,y=!0;f(h.text,["--","++","!","~"])||f(h.text,["-","+"])&&(f(k,["TK_START_BLOCK","TK_START_EXPR","TK_EQUALS","TK_OPERATOR"])||f(c.last_text,H.line_starters)||","===c.last_text)?(e=!1,y=
+!1,!h.wanted_newline||"--"!==h.text&&"++"!==h.text||q(!1,!0),";"===c.last_text&&E(c.mode)&&(e=!0),"TK_RESERVED"===k?e=!0:"TK_END_EXPR"===k?e=!("]"===c.last_text&&("--"===h.text||"++"===h.text)):"TK_OPERATOR"===k&&(e=f(h.text,["--","-","++","+"])&&f(c.last_text,["--","-","++","+"]),f(h.text,["+","-"])&&f(c.last_text,["--","++"])&&(y=!0)),c.mode!==a.BlockStatement&&c.mode!==a.Statement||"{"!==c.last_text&&";"!==c.last_text||q()):":"===h.text?0===c.ternary_depth?e=!1:--c.ternary_depth:"?"===h.text?c.ternary_depth+=
+1:"*"===h.text&&"TK_RESERVED"===k&&"function"===c.last_text&&(e=!1,y=!1);g.space_before_token=g.space_before_token||e;t();g.space_before_token=y},TK_COMMA:function(){if(c.declaration_statement)return E(c.parent.mode)&&(c.declaration_assignment=!1),t(),void(c.declaration_assignment?(c.declaration_assignment=!1,q(!1,!0)):(g.space_before_token=!0,m.comma_first&&B()));t();c.mode===a.ObjectLiteral||c.mode===a.Statement&&c.parent.mode===a.ObjectLiteral?(c.mode===a.Statement&&z(),q()):(g.space_before_token=
+!0,m.comma_first&&B())},TK_BLOCK_COMMENT:function(){if(g.raw)return g.add_raw_token(h),void(h.directives&&"end"===h.directives.preserve&&(m.test_output_raw||(g.raw=!1)));if(h.directives)return q(!1,!0),t(),"start"===h.directives.preserve&&(g.raw=!0),void q(!1,!0);if(!A.newline.test(h.text)&&!h.wanted_newline)return g.space_before_token=!0,t(),void(g.space_before_token=!0);var c,b;b=h.text;b=b.replace(/\x0d/g,"");for(var e=[],a=b.indexOf("\n");-1!==a;)e.push(b.substring(0,a)),b=b.substring(a+1),a=
+b.indexOf("\n");b=(b.length&&e.push(b),e);var a=e=!1,y=h.whitespace_before,r=y.length;q(!1,!0);if(1<b.length){var k;a:{k=b.slice(1);for(var F=0;F<k.length;F++)if("*"!==k[F].replace(/^\s+|\s+$/g,"").charAt(0)){k=!1;break a}k=!0}if(k)e=!0;else{a:{k=b.slice(1);for(var F=0,l=k.length;F<l;F++)if((c=k[F])&&0!==c.indexOf(y)){c=!1;break a}c=!0}c&&(a=!0)}}t(b[0]);for(c=1;c<b.length;c++)q(!1,!0),e?t(" "+b[c].replace(/^\s+/g,"")):a&&b[c].length>r?t(b[c].substring(r)):g.add_token(b[c]);q(!1,!0)},TK_COMMENT:function(){h.wanted_newline?
+q(!1,!0):g.trim(!0);g.space_before_token=!0;t();q(!1,!0)},TK_DOT:function(){b();"TK_RESERVED"===k&&u(c.last_text)?g.space_before_token=!0:B(")"===c.last_text&&m.break_chained_methods);t()},TK_UNKNOWN:function(){t();"\n"===h.text[h.text.length-1]&&q()},TK_EOF:function(){for(;c.mode===a.Statement;)z()}};l=l||{};m={};void 0!==l.braces_on_own_line&&(m.brace_style=l.braces_on_own_line?"expand":"collapse");m.brace_style=l.brace_style?l.brace_style:m.brace_style?m.brace_style:"collapse";"expand-strict"===
+m.brace_style&&(m.brace_style="expand");m.indent_size=l.indent_size?parseInt(l.indent_size,10):4;m.indent_char=l.indent_char?l.indent_char:" ";m.eol=l.eol?l.eol:"\n";m.preserve_newlines=void 0===l.preserve_newlines||l.preserve_newlines;m.break_chained_methods=void 0!==l.break_chained_methods&&l.break_chained_methods;m.max_preserve_newlines=void 0===l.max_preserve_newlines?0:parseInt(l.max_preserve_newlines,10);m.space_in_paren=void 0!==l.space_in_paren&&l.space_in_paren;m.space_in_empty_paren=void 0!==
+l.space_in_empty_paren&&l.space_in_empty_paren;m.jslint_happy=void 0!==l.jslint_happy&&l.jslint_happy;m.space_after_anon_function=void 0!==l.space_after_anon_function&&l.space_after_anon_function;m.keep_array_indentation=void 0!==l.keep_array_indentation&&l.keep_array_indentation;m.space_before_conditional=void 0===l.space_before_conditional||l.space_before_conditional;m.unescape_strings=void 0!==l.unescape_strings&&l.unescape_strings;m.wrap_line_length=void 0===l.wrap_line_length?0:parseInt(l.wrap_line_length,
+10);m.e4x=void 0!==l.e4x&&l.e4x;m.end_with_newline=void 0!==l.end_with_newline&&l.end_with_newline;m.comma_first=void 0!==l.comma_first&&l.comma_first;m.test_output_raw=void 0!==l.test_output_raw&&l.test_output_raw;m.jslint_happy&&(m.space_after_anon_function=!0);l.indent_with_tabs&&(m.indent_char="\t",m.indent_size=1);m.eol=m.eol.replace(/\\r/,"\r").replace(/\\n/,"\n");for(n="";0<m.indent_size;)n+=m.indent_char,--m.indent_size;var M=0;if(r&&r.length){for(;" "===r.charAt(M)||"\t"===r.charAt(M);)J+=
+r.charAt(M),M+=1;r=r.substring(M)}k="TK_START_BLOCK";G="";g=new d(n,J);g.raw=m.test_output_raw;N=[];D(a.BlockStatement);this.beautify=function(){var b,e;H=new P(r,m,n);x=H.tokenize();for(L=0;b=y();){for(var a=0;a<b.comments_before.length;a++)p(b.comments_before[a]);p(b);G=c.last_text;k=b.type;c.last_text=b.text;L+=1}return e=g.get_code(),m.end_with_newline&&(e+="\n"),"\n"!=m.eol&&(e=e.replace(/[\n]/g,m.eol)),e}}function K(a){var l=0,d=-1,p=[],f=!0;this.set_indent=function(p){l=a.baseIndentLength+
+p*a.indent_length;d=p};this.get_character_count=function(){return l};this.is_empty=function(){return f};this.last=function(){return this._empty?null:p[p.length-1]};this.push=function(a){p.push(a);l+=a.length;f=!1};this.pop=function(){var a=null;return f||(a=p.pop(),l-=a.length,f=0===p.length),a};this.remove_indent=function(){0<d&&(--d,l-=a.indent_length)};this.trim=function(){for(;" "===this.last();)p.pop(),--l;f=0===p.length};this.toString=function(){var l="";return this._empty||(0<=d&&(l=a.indent_cache[d]),
+l+=p.join("")),l}}function d(r,l){l=l||"";this.indent_cache=[l];this.baseIndentLength=l.length;this.indent_length=r.length;this.raw=!1;var d=[];this.baseIndentString=l;this.indent_string=r;this.current_line=this.previous_line=null;this.space_before_token=!1;this.add_outputline=function(){this.previous_line=this.current_line;this.current_line=new K(this);d.push(this.current_line)};this.add_outputline();this.get_line_number=function(){return d.length};this.add_new_line=function(a){return(1!==this.get_line_number()||
+!this.just_added_newline())&&!(!a&&this.just_added_newline())&&(this.raw||this.add_outputline(),!0)};this.get_code=function(){return d.join("\n").replace(/[\r\n\t ]+$/,"")};this.set_indent=function(a){if(1<d.length){for(;a>=this.indent_cache.length;)this.indent_cache.push(this.indent_cache[this.indent_cache.length-1]+this.indent_string);return this.current_line.set_indent(a),!0}return this.current_line.set_indent(0),!1};this.add_raw_token=function(a){for(var r=0;r<a.newlines;r++)this.add_outputline();
+this.current_line.push(a.whitespace_before);this.current_line.push(a.text);this.space_before_token=!1};this.add_token=function(a){this.add_space_before_token();this.current_line.push(a)};this.add_space_before_token=function(){this.space_before_token&&!this.just_added_newline()&&this.current_line.push(" ");this.space_before_token=!1};this.remove_redundant_indentation=function(r){if(!r.multiline_frame&&r.mode!==a.ForInitializer&&r.mode!==a.Conditional){r=r.start_line_index;for(var l=d.length;r<l;)d[r].remove_indent(),
+r++}};this.trim=function(a){a=void 0!==a&&a;for(this.current_line.trim(r,l);a&&1<d.length&&this.current_line.is_empty();)d.pop(),this.current_line=d[d.length-1],this.current_line.trim();this.previous_line=1<d.length?d[d.length-2]:null};this.just_added_newline=function(){return this.current_line.is_empty()};this.just_added_blankline=function(){return this.just_added_newline()?1===d.length?!0:d[d.length-2].is_empty():!1}}function P(a,l,d){function p(){var d,n=[];if(D=0,E="",b>=u)return["","TK_EOF"];
+var c;c=C.length?C[C.length-1]:new J("TK_START_BLOCK","{");var e=a.charAt(b);for(b+=1;f(e,K);){if(A.newline.test(e)?"\n"===e&&"\r"===a.charAt(b-2)||(D+=1,n=[]):n.push(e),b>=u)return["","TK_EOF"];e=a.charAt(b);b+=1}if(n.length&&(E=n.join("")),q.test(e)){c=n=!0;var p=q;for("0"===e&&b<u&&/[Xx]/.test(a.charAt(b))?(n=!1,c=!1,e+=a.charAt(b),b+=1,p=x):(e="",--b);b<u&&p.test(a.charAt(b));)e+=a.charAt(b),b+=1,n&&b<u&&"."===a.charAt(b)&&(e+=a.charAt(b),b+=1,n=!1),c&&b<u&&/[Ee]/.test(a.charAt(b))&&(e+=a.charAt(b),
+b+=1,b<u&&/[+-]/.test(a.charAt(b))&&(e+=a.charAt(b),b+=1),c=!1,n=!1);return[e,"TK_WORD"]}if(A.isIdentifierStart(a.charCodeAt(b-1))){if(b<u)for(;A.isIdentifierChar(a.charCodeAt(b))&&(e+=a.charAt(b),(b+=1)!==u););return"TK_DOT"===c.type||"TK_RESERVED"===c.type&&f(c.text,["set","get"])||!f(e,y)?[e,"TK_WORD"]:"in"===e?[e,"TK_OPERATOR"]:[e,"TK_RESERVED"]}if("("===e||"["===e)return[e,"TK_START_EXPR"];if(")"===e||"]"===e)return[e,"TK_END_EXPR"];if("{"===e)return[e,"TK_START_BLOCK"];if("}"===e)return[e,"TK_END_BLOCK"];
+if(";"===e)return[e,"TK_SEMICOLON"];if("/"===e){n="";if("*"===a.charAt(b)){b+=1;F.lastIndex=b;e=F.exec(a);n="/*"+e[0];b+=e[0].length;c=n;if(c.match(L)){p={};H.lastIndex=0;for(d=H.exec(c);d;)p[d[1]]=d[2],d=H.exec(c);c=p}else c=null;return c&&"start"===c.ignore&&(h.lastIndex=b,e=h.exec(a),n+=e[0],b+=e[0].length),n=n.replace(A.lineBreak,"\n"),[n,"TK_BLOCK_COMMENT",c]}if("/"===a.charAt(b))return b+=1,g.lastIndex=b,e=g.exec(a),n="//"+e[0],b+=e[0].length,[n,"TK_COMMENT"]}if("`"===e||"'"===e||'"'===e||("/"===
+e||l.e4x&&"\x3c"===e&&a.slice(b-1).match(/^<([-a-zA-Z:0-9_.]+|{[^{}]*}|!\[CDATA\[[\s\S]*?\]\])(\s+[-a-zA-Z:0-9_.]+\s*=\s*('[^']*'|"[^"]*"|{.*?}))*\s*(\/?)\s*>/))&&("TK_RESERVED"===c.type&&f(c.text,"return case throw else do typeof yield".split(" "))||"TK_END_EXPR"===c.type&&")"===c.text&&c.parent&&"TK_RESERVED"===c.parent.type&&f(c.parent.text,["if","while","for"])||f(c.type,"TK_COMMENT TK_START_EXPR TK_START_BLOCK TK_END_BLOCK TK_OPERATOR TK_EQUALS TK_EOF TK_SEMICOLON TK_COMMA".split(" ")))){var n=
+e,v=c=!1;if(d=e,"/"===n)for(e=!1;b<u&&(c||e||a.charAt(b)!==n)&&!A.newline.test(a.charAt(b));)d+=a.charAt(b),c?c=!1:(c="\\"===a.charAt(b),"["===a.charAt(b)?e=!0:"]"===a.charAt(b)&&(e=!1)),b+=1;else if(l.e4x&&"\x3c"===n){if(c=/<(\/?)([-a-zA-Z:0-9_.]+|{[^{}]*}|!\[CDATA\[[\s\S]*?\]\])(\s+[-a-zA-Z:0-9_.]+\s*=\s*('[^']*'|"[^"]*"|{.*?}))*\s*(\/?)\s*>/g,e=a.slice(b-1),(p=c.exec(e))&&0===p.index){n=p[2];for(d=0;p;){var v=!!p[1],w=p[2],m=!!p[p.length-1]||"![CDATA["===w.slice(0,8);if(w!==n||m||(v?--d:++d),0>=
+d)break;p=c.exec(e)}n=p?p.index+p[0].length:e.length;return e=e.slice(0,n),b+=n-1,e=e.replace(A.lineBreak,"\n"),[e,"TK_STRING"]}}else for(;b<u&&(c||a.charAt(b)!==n&&("`"===n||!A.newline.test(a.charAt(b))));)(c||"`"===n)&&A.newline.test(a.charAt(b))?("\r"===a.charAt(b)&&"\n"===a.charAt(b+1)&&(b+=1),d+="\n"):d+=a.charAt(b),c?("x"!==a.charAt(b)&&"u"!==a.charAt(b)||(v=!0),c=!1):c="\\"===a.charAt(b),b+=1;if(v&&l.unescape_strings){a:{e=d;p=!1;d="";v=0;w="";for(m=0;p||v<e.length;)if(c=e.charAt(v),v++,p){if(p=
+!1,"x"===c)w=e.substr(v,2),v+=2;else{if("u"!==c){d+="\\"+c;continue}w=e.substr(v,4);v+=4}if(!w.match(/^[0123456789abcdefABCDEF]+$/))break a;if(0<=(m=parseInt(w,16))&&32>m)d+="x"===c?"\\x"+w:"\\u"+w;else if(34===m||39===m||92===m)d+="\\"+String.fromCharCode(m);else{if("x"===c&&126<m&&255>=m)break a;d+=String.fromCharCode(m)}}else"\\"===c?p=!0:d+=c;e=d}d=e}if(b<u&&a.charAt(b)===n&&(d+=n,b+=1,"/"===n))for(;b<u&&A.isIdentifierStart(a.charCodeAt(b));)d+=a.charAt(b),b+=1;return[d,"TK_STRING"]}if("#"===
+e){if(0===C.length&&"!"===a.charAt(b)){for(d=e;b<u&&"\n"!==e;)e=a.charAt(b),d+=e,b+=1;return[d.replace(/^\s+|\s+$/g,"")+"\n","TK_UNKNOWN"]}n="#";if(b<u&&q.test(a.charAt(b))){do e=a.charAt(b),n+=e,b+=1;while(b<u&&"#"!==e&&"\x3d"!==e);return"#"===e||("["===a.charAt(b)&&"]"===a.charAt(b+1)?(n+="[]",b+=2):"{"===a.charAt(b)&&"}"===a.charAt(b+1)&&(n+="{}",b+=2)),[n,"TK_WORD"]}}if("\x3c"===e&&("?"===a.charAt(b)||"%"===a.charAt(b))&&(k.lastIndex=b-1,n=k.exec(a)))return e=n[0],b+=e.length-1,e=e.replace(A.lineBreak,
+"\n"),[e,"TK_STRING"];if("\x3c"===e&&"\x3c!--"===a.substring(b-1,b+3)){b+=3;for(e="\x3c!--";!A.newline.test(a.charAt(b))&&b<u;)e+=a.charAt(b),b++;return z=!0,[e,"TK_COMMENT"]}if("-"===e&&z&&"--\x3e"===a.substring(b-1,b+2))return z=!1,b+=2,["--\x3e","TK_COMMENT"];if("."===e)return[e,"TK_DOT"];if(f(e,t)){for(;b<u&&f(e+a.charAt(b),t)&&(e+=a.charAt(b),!((b+=1)>=u)););return","===e?[e,"TK_COMMA"]:"\x3d"===e?[e,"TK_EQUALS"]:[e,"TK_OPERATOR"]}return[e,"TK_UNKNOWN"]}var K=["\n","\r","\t"," "],q=/[0-9]/,x=
+/[0123456789abcdefABCDEF]/,t="+ - * / % \x26 ++ -- \x3d +\x3d -\x3d *\x3d /\x3d %\x3d \x3d\x3d \x3d\x3d\x3d !\x3d !\x3d\x3d \x3e \x3c \x3e\x3d \x3c\x3d \x3e\x3e \x3c\x3c \x3e\x3e\x3e \x3e\x3e\x3e\x3d \x3e\x3e\x3d \x3c\x3c\x3d \x26\x26 \x26\x3d | || ! ~ , : ? ^ ^\x3d |\x3d :: \x3d\x3e \x3c%\x3d \x3c% %\x3e \x3c?\x3d \x3c? ?\x3e".split(" ");this.line_starters="continue try throw return var let const if switch case default for while break function import export".split(" ");var D,E,z,C,b,u,y=this.line_starters.concat("do in else get set new catch finally typeof yield async await".split(" ")),
+F=/([\s\S]*?)((?:\*\/)|$)/g,g=/([^\n\r\u2028\u2029]*)/g,L=/\/\* beautify( \w+[:]\w+)+ \*\//g,H=/ (\w+)[:](\w+)/g,h=/([\s\S]*?)((?:\/\*\sbeautify\signore:end\s\*\/)|$)/g,k=/((<\?php|<\?=)[\s\S]*?\?>)|(<%[\s\S]*?%>)/g;this.tokenize=function(){u=a.length;b=0;z=!1;C=[];for(var g,d,c,e=null,h=[],y=[];!d||"TK_EOF"!==d.type;){c=p();for(g=new J(c[1],c[0],D,E);"TK_COMMENT"===g.type||"TK_BLOCK_COMMENT"===g.type||"TK_UNKNOWN"===g.type;)"TK_BLOCK_COMMENT"===g.type&&(g.directives=c[2]),y.push(g),c=p(),g=new J(c[1],
+c[0],D,E);y.length&&(g.comments_before=y,y=[]);"TK_START_BLOCK"===g.type||"TK_START_EXPR"===g.type?(g.parent=d,h.push(e),e=g):("TK_END_BLOCK"===g.type||"TK_END_EXPR"===g.type)&&e&&("]"===g.text&&"["===e.text||")"===g.text&&"("===e.text||"}"===g.text&&"{"===e.text)&&(g.parent=e.parent,e=h.pop());C.push(g);d=g}return C}}var A={};!function(a){var d=RegExp("[ªµºÀ-ÖØ-öø-ˁˆ-ˑˠ-ˤˬˮͰ-ʹͶͷͺ-ͽΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ҁҊ-ԧԱ-Ֆՙա-ևא-תװ-ײؠ-يٮٯٱ-ۓەۥۦۮۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪߴߵߺࠀ-ࠕࠚࠤࠨࡀ-ࡘࢠࢢ-ࢬऄ-हऽॐक़-ॡॱ-ॷॹ-ॿঅ-ঌএঐও-নপ-রলশ-হঽৎড়ঢ়য়-ৡৰৱਅ-ਊਏਐਓ-ਨਪ-ਰਲਲ਼ਵਸ਼ਸਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલળવ-હઽૐૠૡଅ-ଌଏଐଓ-ନପ-ରଲଳଵ-ହଽଡ଼ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கஙசஜஞடணதந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-ళవ-హఽౘౙౠౡಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೞೠೡೱೲഅ-ഌഎ-ഐഒ-ഺഽൎൠൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะาำเ-ๆກຂຄງຈຊຍດ-ທນ-ຟມ-ຣລວສຫອ-ະາຳຽເ-ໄໆໜ-ໟༀཀ-ཇཉ-ཬྈ-ྌက-ဪဿၐ-ၕၚ-ၝၡၥၦၮ-ၰၵ-ႁႎႠ-ჅჇჍა-ჺჼ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏼᐁ-ᙬᙯ-ᙿᚁ-ᚚᚠ-ᛪᛮ-ᛰᜀ-ᜌᜎ-ᜑᜠ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៗៜᠠ-ᡷᢀ-ᢨᢪᢰ-ᣵᤀ-ᤜᥐ-ᥭᥰ-ᥴᦀ-ᦫᧁ-ᧇᨀ-ᨖᨠ-ᩔᪧᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮᮯᮺ-ᯥᰀ-ᰣᱍ-ᱏᱚ-ᱽᳩ-ᳬᳮ-ᳱᳵᳶᴀ-ᶿḀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼⁱⁿₐ-ₜℂℇℊ-ℓℕℙ-ℝℤΩℨK-ℭℯ-ℹℼ-ℿⅅ-ⅉⅎⅠ-ↈⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳮⳲⳳⴀ-ⴥⴧⴭⴰ-ⵧⵯⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞⸯ々-〇〡-〩〱-〵〸-〼ぁ-ゖゝ-ゟァ-ヺー-ヿㄅ-ㄭㄱ-ㆎㆠ-ㆺㇰ-ㇿ㐀-䶵一-鿌ꀀ-ꒌꓐ-ꓽꔀ-ꘌꘐ-ꘟꘪꘫꙀ-ꙮꙿ-ꚗꚠ-ꛯꜗ-ꜟꜢ-ꞈꞋ-ꞎꞐ-ꞓꞠ-Ɦꟸ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꣲ-ꣷꣻꤊ-ꤥꤰ-ꥆꥠ-ꥼꦄ-ꦲꧏꨀ-ꨨꩀ-ꩂꩄ-ꩋꩠ-ꩶꩺꪀ-ꪯꪱꪵꪶꪹ-ꪽꫀꫂꫛ-ꫝꫠ-ꫪꫲ-ꫴꬁ-ꬆꬉ-ꬎꬑ-ꬖꬠ-ꬦꬨ-ꬮꯀ-ꯢ가-힣ힰ-ퟆퟋ-ퟻ豈-舘並-龎ff-stﬓ-ﬗיִײַ-ﬨשׁ-זּטּ-לּמּנּסּףּפּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼA-Za-zヲ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ]"),
+f=RegExp("[ªµºÀ-ÖØ-öø-ˁˆ-ˑˠ-ˤˬˮͰ-ʹͶͷͺ-ͽΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ҁҊ-ԧԱ-Ֆՙա-ևא-תװ-ײؠ-يٮٯٱ-ۓەۥۦۮۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪߴߵߺࠀ-ࠕࠚࠤࠨࡀ-ࡘࢠࢢ-ࢬऄ-हऽॐक़-ॡॱ-ॷॹ-ॿঅ-ঌএঐও-নপ-রলশ-হঽৎড়ঢ়য়-ৡৰৱਅ-ਊਏਐਓ-ਨਪ-ਰਲਲ਼ਵਸ਼ਸਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલળવ-હઽૐૠૡଅ-ଌଏଐଓ-ନପ-ରଲଳଵ-ହଽଡ଼ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கஙசஜஞடணதந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-ళవ-హఽౘౙౠౡಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೞೠೡೱೲഅ-ഌഎ-ഐഒ-ഺഽൎൠൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะาำเ-ๆກຂຄງຈຊຍດ-ທນ-ຟມ-ຣລວສຫອ-ະາຳຽເ-ໄໆໜ-ໟༀཀ-ཇཉ-ཬྈ-ྌက-ဪဿၐ-ၕၚ-ၝၡၥၦၮ-ၰၵ-ႁႎႠ-ჅჇჍა-ჺჼ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏼᐁ-ᙬᙯ-ᙿᚁ-ᚚᚠ-ᛪᛮ-ᛰᜀ-ᜌᜎ-ᜑᜠ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៗៜᠠ-ᡷᢀ-ᢨᢪᢰ-ᣵᤀ-ᤜᥐ-ᥭᥰ-ᥴᦀ-ᦫᧁ-ᧇᨀ-ᨖᨠ-ᩔᪧᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮᮯᮺ-ᯥᰀ-ᰣᱍ-ᱏᱚ-ᱽᳩ-ᳬᳮ-ᳱᳵᳶᴀ-ᶿḀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼⁱⁿₐ-ₜℂℇℊ-ℓℕℙ-ℝℤΩℨK-ℭℯ-ℹℼ-ℿⅅ-ⅉⅎⅠ-ↈⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳮⳲⳳⴀ-ⴥⴧⴭⴰ-ⵧⵯⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞⸯ々-〇〡-〩〱-〵〸-〼ぁ-ゖゝ-ゟァ-ヺー-ヿㄅ-ㄭㄱ-ㆎㆠ-ㆺㇰ-ㇿ㐀-䶵一-鿌ꀀ-ꒌꓐ-ꓽꔀ-ꘌꘐ-ꘟꘪꘫꙀ-ꙮꙿ-ꚗꚠ-ꛯꜗ-ꜟꜢ-ꞈꞋ-ꞎꞐ-ꞓꞠ-Ɦꟸ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꣲ-ꣷꣻꤊ-ꤥꤰ-ꥆꥠ-ꥼꦄ-ꦲꧏꨀ-ꨨꩀ-ꩂꩄ-ꩋꩠ-ꩶꩺꪀ-ꪯꪱꪵꪶꪹ-ꪽꫀꫂꫛ-ꫝꫠ-ꫪꫲ-ꫴꬁ-ꬆꬉ-ꬎꬑ-ꬖꬠ-ꬦꬨ-ꬮꯀ-ꯢ가-힣ힰ-ퟆퟋ-ퟻ豈-舘並-龎ff-stﬓ-ﬗיִײַ-ﬨשׁ-זּטּ-לּמּנּסּףּפּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼA-Za-zヲ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ̀-ͯ҃-֑҇-ׇֽֿׁׂׅׄؐ-ؚؠ-ىٲ-ۓۧ-ۨۻ-ۼܰ-݊ࠀ-ࠔࠛ-ࠣࠥ-ࠧࠩ-࠭ࡀ-ࡗࣤ-ࣾऀ-ःऺ-़ा-ॏ॑-ॗॢ-ॣ०-९ঁ-ঃ়া-ৄেৈৗয়-ৠਁ-ਃ਼ਾ-ੂੇੈੋ-੍ੑ੦-ੱੵઁ-ઃ઼ા-ૅે-ૉો-્ૢ-ૣ૦-૯ଁ-ଃ଼ା-ୄେୈୋ-୍ୖୗୟ-ୠ୦-୯ஂா-ூெ-ைொ-்ௗ௦-௯ఁ-ఃె-ైొ-్ౕౖౢ-ౣ౦-౯ಂಃ಼ಾ-ೄೆ-ೈೊ-್ೕೖೢ-ೣ೦-೯ംഃെ-ൈൗൢ-ൣ൦-൯ංඃ්ා-ුූෘ-ෟෲෳิ-ฺเ-ๅ๐-๙ິ-ູ່-ໍ໐-໙༘༙༠-༩༹༵༷ཁ-ཇཱ-྄྆-྇ྍ-ྗྙ-ྼ࿆က-ဩ၀-၉ၧ-ၭၱ-ၴႂ-ႍႏ-ႝ፝-፟ᜎ-ᜐᜠ-ᜰᝀ-ᝐᝲᝳក-ឲ៝០-៩᠋-᠍᠐-᠙ᤠ-ᤫᤰ-᤻ᥑ-ᥭᦰ-ᧀᧈ-ᧉ᧐-᧙ᨀ-ᨕᨠ-ᩓ᩠-᩿᩼-᪉᪐-᪙ᭆ-ᭋ᭐-᭙᭫-᭳᮰-᮹᯦-᯳ᰀ-ᰢ᱀-᱉ᱛ-ᱽ᳐-᳒ᴀ-ᶾḁ-ἕ‌‍‿⁀⁔⃐-⃥⃜⃡-⃰ⶁ-ⶖⷠ-ⷿ〡-〨゙゚Ꙁ-ꙭꙴ-꙽ꚟ꛰-꛱ꟸ-ꠀ꠆ꠋꠣ-ꠧꢀ-ꢁꢴ-꣄꣐-꣙ꣳ-ꣷ꤀-꤉ꤦ-꤭ꤰ-ꥅꦀ-ꦃ꦳-꧀ꨀ-ꨧꩀ-ꩁꩌ-ꩍ꩐-꩙ꩻꫠ-ꫩꫲ-ꫳꯀ-ꯡ꯬꯭꯰-꯹ﬠ-ﬨ︀-️︠-︦︳︴﹍-﹏0-9_]");
+a.newline=/[\n\r\u2028\u2029]/;a.lineBreak=/\r\n|[\n\r\u2028\u2029]/g;a.isIdentifierStart=function(a){return 65>a?36===a:91>a||(97>a?95===a:123>a||170<=a&&d.test(String.fromCharCode(a)))};a.isIdentifierChar=function(a){return 48>a?36===a:58>a||!(65>a)&&(91>a||(97>a?95===a:123>a||170<=a&&f.test(String.fromCharCode(a))))}}(A);var a={BlockStatement:"BlockStatement",Statement:"Statement",ObjectLiteral:"ObjectLiteral",ArrayLiteral:"ArrayLiteral",ForInitializer:"ForInitializer",Conditional:"Conditional",
+Expression:"Expression"},J=function(a,d,f,p,K,q){this.type=a;this.text=d;this.comments_before=[];this.newlines=f||0;this.wanted_newline=0<f;this.whitespace_before=p||"";this.directives=this.parent=null};"function"==typeof define&&define.amd?define("beautify.js",[],function(){return{js_beautify:x}}):"undefined"!=typeof exports?exports.js_beautify=x:"undefined"!=typeof window?window.js_beautify=x:"undefined"!=typeof global&&(global.js_beautify=x)}();
+(function(){function f(f,d,x,A){var a,J,r,l,w,p,B,q,I,t,D,E,z,C,b;d=d||{};void 0!==d.wrap_line_length&&0!==parseInt(d.wrap_line_length,10)||void 0===d.max_char||0===parseInt(d.max_char,10)||(d.wrap_line_length=d.max_char);J=void 0!==d.indent_inner_html&&d.indent_inner_html;r=void 0===d.indent_size?4:parseInt(d.indent_size,10);l=void 0===d.indent_char?" ":d.indent_char;p=void 0===d.brace_style?"collapse":d.brace_style;w=0===parseInt(d.wrap_line_length,10)?32786:parseInt(d.wrap_line_length||250,10);
+B=d.unformatted||"a span img bdo em strong dfn code samp kbd var cite abbr acronym q sub sup tt i b big small u s strike font ins del pre address dt h1 h2 h3 h4 h5 h6".split(" ");I=(q=void 0===d.preserve_newlines||d.preserve_newlines)?isNaN(parseInt(d.max_preserve_newlines,10))?32786:parseInt(d.max_preserve_newlines,10):0;t=void 0!==d.indent_handlebars&&d.indent_handlebars;D=void 0===d.wrap_attributes?"auto":d.wrap_attributes;E=void 0===d.wrap_attributes_indent_size?r:parseInt(d.wrap_attributes_indent_size,
+10)||r;z=void 0!==d.end_with_newline&&d.end_with_newline;C="object"==typeof d.extra_liners&&d.extra_liners?d.extra_liners.concat():"string"==typeof d.extra_liners?d.extra_liners.split(","):["head","body","/html"];b=d.eol?d.eol:"\n";d.indent_with_tabs&&(l="\t",r=1);b=b.replace(/\\r/,"\r").replace(/\\n/,"\n");a=new function(){return this.pos=0,this.token="",this.current_mode="CONTENT",this.tags={parent:"parent1",parentcount:1,parent1:""},this.tag_type="",this.token_text=this.last_token=this.last_text=
+this.token_type="",this.newlines=0,this.indent_content=J,this.Utils={whitespace:["\n","\r","\t"," "],single_token:"br input link meta source !doctype basefont base area hr wbr param img isindex embed".split(" "),extra_liners:C,in_array:function(a,b){for(var g=0;g<b.length;g++)if(a===b[g])return!0;return!1}},this.is_whitespace=function(a){for(;0<a.length;a++)if(!this.Utils.in_array(a.charAt(0),this.Utils.whitespace))return!1;return!0},this.traverse_whitespace=function(){var a="";if(a=this.input.charAt(this.pos),
+this.Utils.in_array(a,this.Utils.whitespace)){for(this.newlines=0;this.Utils.in_array(a,this.Utils.whitespace);)q&&"\n"===a&&this.newlines<=I&&(this.newlines+=1),this.pos++,a=this.input.charAt(this.pos);return!0}return!1},this.space_or_wrap=function(a){this.line_char_count>=this.wrap_line_length?(this.print_newline(!1,a),this.print_indentation(a)):(this.line_char_count++,a.push(" "))},this.get_content=function(){for(var a="",b=[];"\x3c"!==this.input.charAt(this.pos);){if(this.pos>=this.input.length)return b.length?
+b.join(""):["","TK_EOF"];if(this.traverse_whitespace())this.space_or_wrap(b);else{if(t){a=this.input.substr(this.pos,3);if("{{#"===a||"{{/"===a)break;if("{{!"===a)return[this.get_tag(),"TK_TAG_HANDLEBARS_COMMENT"];if("{{"===this.input.substr(this.pos,2)&&"{{else}}"===this.get_tag(!0))break}a=this.input.charAt(this.pos);this.pos++;this.line_char_count++;b.push(a)}}return b.length?b.join(""):""},this.get_contents_to=function(a){if(this.pos===this.input.length)return["","TK_EOF"];var b="";a=new RegExp("\x3c/"+
+a+"\\s*\x3e","igm");a.lastIndex=this.pos;a=(a=a.exec(this.input))?a.index:this.input.length;return this.pos<a&&(b=this.input.substring(this.pos,a),this.pos=a),b},this.record_tag=function(a){this.tags[a+"count"]?(this.tags[a+"count"]++,this.tags[a+this.tags[a+"count"]]=this.indent_level):(this.tags[a+"count"]=1,this.tags[a+this.tags[a+"count"]]=this.indent_level);this.tags[a+this.tags[a+"count"]+"parent"]=this.tags.parent;this.tags.parent=a+this.tags[a+"count"]},this.retrieve_tag=function(a){if(this.tags[a+
+"count"]){for(var b=this.tags.parent;b&&a+this.tags[a+"count"]!==b;)b=this.tags[b+"parent"];b&&(this.indent_level=this.tags[a+this.tags[a+"count"]],this.tags.parent=this.tags[b+"parent"]);delete this.tags[a+this.tags[a+"count"]+"parent"];delete this.tags[a+this.tags[a+"count"]];1===this.tags[a+"count"]?delete this.tags[a+"count"]:this.tags[a+"count"]--}},this.indent_to_tag=function(a){if(this.tags[a+"count"]){for(var b=this.tags.parent;b&&a+this.tags[a+"count"]!==b;)b=this.tags[b+"parent"];b&&(this.indent_level=
+this.tags[a+this.tags[a+"count"]])}},this.get_tag=function(a){var b,g,d="",f=[],h="",k=!1,p=!0,n=this.pos,c=this.line_char_count;a=void 0!==a&&a;do{if(this.pos>=this.input.length)return a&&(this.pos=n,this.line_char_count=c),f.length?f.join(""):["","TK_EOF"];if(d=this.input.charAt(this.pos),this.pos++,this.Utils.in_array(d,this.Utils.whitespace))k=!0;else{if("'"!==d&&'"'!==d||(d+=this.get_unformatted(d),k=!0),"\x3d"===d&&(k=!1),f.length&&"\x3d"!==f[f.length-1]&&"\x3e"!==d&&k){if(this.space_or_wrap(f),
+k=!1,!p&&"force"===D&&"/"!==d){this.print_newline(!0,f);this.print_indentation(f);for(var e=0;e<E;e++)f.push(l)}for(e=0;e<f.length;e++)if(" "===f[e]){p=!1;break}}if(t&&"\x3c"===g&&"{{"===d+this.input.charAt(this.pos)&&(d+=this.get_unformatted("}}"),f.length&&" "!==f[f.length-1]&&"\x3c"!==f[f.length-1]&&(d=" "+d),k=!0),"\x3c"!==d||g||(b=this.pos-1,g="\x3c"),t&&!g&&2<=f.length&&"{"===f[f.length-1]&&"{"===f[f.length-2]&&(b="#"===d||"/"===d||"!"===d?this.pos-3:this.pos-2,g="{"),this.line_char_count++,
+f.push(d),f[1]&&("!"===f[1]||"?"===f[1]||"%"===f[1])){f=[this.get_comment(b)];break}if(t&&f[1]&&"{"===f[1]&&f[2]&&"!"===f[2]){f=[this.get_comment(b)];break}if(t&&"{"===g&&2<f.length&&"}"===f[f.length-2]&&"}"===f[f.length-1])break}}while("\x3e"!==d);b=f.join("");g=-1!==b.indexOf(" ")?b.indexOf(" "):"{"===b.charAt(0)?b.indexOf("}"):b.indexOf("\x3e");d="\x3c"!==b.charAt(0)&&t?"#"===b.charAt(2)?3:2:1;g=b.substring(d,g).toLowerCase();return"/"===b.charAt(b.length-2)||this.Utils.in_array(g,this.Utils.single_token)?
+a||(this.tag_type="SINGLE"):t&&"{"===b.charAt(0)&&"else"===g?a||(this.indent_to_tag("if"),this.tag_type="HANDLEBARS_ELSE",this.indent_content=!0,this.traverse_whitespace()):this.is_unformatted(g,B)?(h=this.get_unformatted("\x3c/"+g+"\x3e",b),f.push(h),this.pos-1,this.tag_type="SINGLE"):"script"===g&&(-1===b.search("type")||-1<b.search("type")&&-1<b.search(/\b(text|application)\/(x-)?(javascript|ecmascript|jscript|livescript)/))?a||(this.record_tag(g),this.tag_type="SCRIPT"):"style"===g&&(-1===b.search("type")||
+-1<b.search("type")&&-1<b.search("text/css"))?a||(this.record_tag(g),this.tag_type="STYLE"):"!"===g.charAt(0)?a||(this.tag_type="SINGLE",this.traverse_whitespace()):a||("/"===g.charAt(0)?(this.retrieve_tag(g.substring(1)),this.tag_type="END"):(this.record_tag(g),"html"!==g.toLowerCase()&&(this.indent_content=!0),this.tag_type="START"),this.traverse_whitespace()&&this.space_or_wrap(f),this.Utils.in_array(g,this.Utils.extra_liners)&&(this.print_newline(!1,this.output),this.output.length&&"\n"!==this.output[this.output.length-
+2]&&this.print_newline(!0,this.output))),a&&(this.pos=n,this.line_char_count=c),f.join("")},this.get_comment=function(a){var b="",d="\x3e",f=!1;this.pos=a;input_char=this.input.charAt(this.pos);for(this.pos++;this.pos<=this.input.length&&(b+=input_char,b.charAt(b.length-1)!==d.charAt(d.length-1)||-1===b.indexOf(d));)!f&&10>b.length&&(0===b.indexOf("\x3c![if")?(d="\x3c![endif]\x3e",f=!0):0===b.indexOf("\x3c![cdata[")?(d="]]\x3e",f=!0):0===b.indexOf("\x3c![")?(d="]\x3e",f=!0):0===b.indexOf("\x3c!--")?
+(d="--\x3e",f=!0):0===b.indexOf("{{!")?(d="}}",f=!0):0===b.indexOf("\x3c?")?(d="?\x3e",f=!0):0===b.indexOf("\x3c%")&&(d="%\x3e",f=!0)),input_char=this.input.charAt(this.pos),this.pos++;return b},this.get_unformatted=function(a,b){if(b&&-1!==b.toLowerCase().indexOf(a))return"";var d="",f="",l=0,h=!0;do{if(this.pos>=this.input.length)break;if(d=this.input.charAt(this.pos),this.pos++,this.Utils.in_array(d,this.Utils.whitespace)){if(!h){this.line_char_count--;continue}if("\n"===d||"\r"===d){f+="\n";this.line_char_count=
+0;continue}}f+=d;this.line_char_count++;h=!0;t&&"{"===d&&f.length&&"{"===f.charAt(f.length-2)&&(f+=this.get_unformatted("}}"),l=f.length)}while(-1===f.toLowerCase().indexOf(a,l));return f},this.get_token=function(){var a;if("TK_TAG_SCRIPT"===this.last_token||"TK_TAG_STYLE"===this.last_token){var b=this.last_token.substr(7);return a=this.get_contents_to(b),"string"!=typeof a?a:[a,"TK_"+b]}if("CONTENT"===this.current_mode)return a=this.get_content(),"string"!=typeof a?a:[a,"TK_CONTENT"];if("TAG"===
+this.current_mode)return"string"!=typeof(a=this.get_tag())?a:[a,"TK_TAG_"+this.tag_type]},this.get_full_indent=function(a){return a=this.indent_level+a||0,1>a?"":Array(a+1).join(this.indent_string)},this.is_unformatted=function(a,b){if(!this.Utils.in_array(a,b))return!1;if("a"!==a.toLowerCase()||!this.Utils.in_array("a",b))return!0;var d=(this.get_tag(!0)||"").match(/^\s*<\s*\/?([a-z]*)\s*[^>]*>\s*$/);return!(d&&!this.Utils.in_array(d,b))},this.printer=function(a,b,d,f,l){this.input=a||"";this.input=
+this.input.replace(/\r\n|[\r\u2028\u2029]/g,"\n");this.output=[];this.indent_character=b;this.indent_string="";this.indent_size=d;this.brace_style=l;this.indent_level=0;this.wrap_line_length=f;for(a=this.line_char_count=0;a<this.indent_size;a++)this.indent_string+=this.indent_character;this.print_newline=function(a,b){this.line_char_count=0;b&&b.length&&(a||"\n"!==b[b.length-1])&&("\n"!==b[b.length-1]&&(b[b.length-1]=b[b.length-1].replace(/\s+$/g,"")),b.push("\n"))};this.print_indentation=function(a){for(var b=
+0;b<this.indent_level;b++)a.push(this.indent_string),this.line_char_count+=this.indent_string.length};this.print_token=function(a){this.is_whitespace(a)&&!this.output.length||((a||""!==a)&&this.output.length&&"\n"===this.output[this.output.length-1]&&(this.print_indentation(this.output),a=a.replace(/^\s+/g,"")),this.print_token_raw(a))};this.print_token_raw=function(a){0<this.newlines&&(a=a.replace(/\s+$/g,""));a&&""!==a&&(1<a.length&&"\n"===a.charAt(a.length-1)?(this.output.push(a.slice(0,-1)),this.print_newline(!1,
+this.output)):this.output.push(a));for(a=0;a<this.newlines;a++)this.print_newline(0<a,this.output);this.newlines=0};this.indent=function(){this.indent_level++};this.unindent=function(){0<this.indent_level&&this.indent_level--}},this};for(a.printer(f,l,r,w,p);;){f=a.get_token();if(a.token_text=f[0],a.token_type=f[1],"TK_EOF"===a.token_type)break;switch(a.token_type){case "TK_TAG_START":a.print_newline(!1,a.output);a.print_token(a.token_text);a.indent_content&&(a.indent(),a.indent_content=!1);a.current_mode=
+"CONTENT";break;case "TK_TAG_STYLE":case "TK_TAG_SCRIPT":a.print_newline(!1,a.output);a.print_token(a.token_text);a.current_mode="CONTENT";break;case "TK_TAG_END":"TK_CONTENT"===a.last_token&&""===a.last_text&&(f=a.token_text.match(/\w+/)[0],r=null,a.output.length&&(r=a.output[a.output.length-1].match(/(?:<|{{#)\s*(\w+)/)),(null===r||r[1]!==f&&!a.Utils.in_array(r[1],B))&&a.print_newline(!1,a.output));a.print_token(a.token_text);a.current_mode="CONTENT";break;case "TK_TAG_SINGLE":(f=a.token_text.match(/^\s*<([a-z-]+)/i))&&
+a.Utils.in_array(f[1],B)||a.print_newline(!1,a.output);a.print_token(a.token_text);a.current_mode="CONTENT";break;case "TK_TAG_HANDLEBARS_ELSE":a.print_token(a.token_text);a.indent_content&&(a.indent(),a.indent_content=!1);a.current_mode="CONTENT";break;case "TK_TAG_HANDLEBARS_COMMENT":case "TK_CONTENT":a.print_token(a.token_text);a.current_mode="TAG";break;case "TK_STYLE":case "TK_SCRIPT":if(""!==a.token_text){a.print_newline(!1,a.output);var u;f=a.token_text;w=1;"TK_SCRIPT"===a.token_type?u="function"==
+typeof x&&x:"TK_STYLE"===a.token_type&&(u="function"==typeof A&&A);"keep"===d.indent_scripts?w=0:"separate"===d.indent_scripts&&(w=-a.indent_level);r=a.get_full_indent(w);u?(w=function(){this.eol="\n"},w.prototype=d,w=new w,f=u(f.replace(/^\s*/,r),w)):(p=f.match(/^\s*/)[0].match(/[^\n\r]*$/)[0].split(a.indent_string).length-1,w=a.get_full_indent(w-p),f=f.replace(/^\s*/,r).replace(/\r\n|\r|\n/g,"\n"+w).replace(/\s+$/,""));f&&(a.print_token_raw(f),a.print_newline(!0,a.output))}a.current_mode="TAG";
+break;default:""!==a.token_text&&a.print_token(a.token_text)}a.last_token=a.token_type;a.last_text=a.token_text}d=a.output.join("").replace(/[\r\n\t ]+$/,"");return z&&(d+="\n"),"\n"!=b&&(d=d.replace(/[\n]/g,b)),d}if("function"==typeof define&&define.amd)define("beautify-html.js",["require","./beautify","./beautify-css"],function(x){var d=x("./beautify"),I=x("./beautify-css");return{html_beautify:function(x,a){return f(x,a,d.js_beautify,I.css_beautify)}}});else if("undefined"!=typeof exports){var x=
+require("./beautify.js"),I=require("./beautify-css.js");exports.html_beautify=function(K,d){return f(K,d,x.js_beautify,I.css_beautify)}}else"undefined"!=typeof window?window.html_beautify=function(x,d){return f(x,d,window.js_beautify,window.css_beautify)}:"undefined"!=typeof global&&(global.html_beautify=function(x,d){return f(x,d,global.js_beautify,global.css_beautify)})})();
+(function(f){"function"==typeof f.define&&(f.define("beautify",["beautify.js"],function(f){return f}),f.define("beautify-css",[],function(){return{css_beautify:void 0}}),f.define("beautifyModule",["beautify","beautify-html.js"],function(x,I){f.js_beautify=x.js_beautify;f.html_beautify=I.html_beautify}))})(this);
\ No newline at end of file
diff --git a/js/ckeditor/plugins/codemirror/js/codemirror.addons.min.js b/js/ckeditor/plugins/codemirror/js/codemirror.addons.min.js
new file mode 100644 (file)
index 0000000..3fe1403
--- /dev/null
@@ -0,0 +1,103 @@
+!function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/comment/continuecomment.js",["../../lib/codemirror"],a):a(CodeMirror)}(function(a){function m(b){if(b.getOption("disableInput"))return a.Pass;for(var d,c=b.listSelections(),r=[],q=0;q<c.length;q++){var n=c[q].head,p=b.getTokenAt(n);if("comment"!=p.type)return a.Pass;var e=a.innerMode(b.getMode(),p.state).mode;if(d){if(d!=e)return a.Pass}else d=e;e=null;
+if(d.blockCommentStart&&d.blockCommentContinue){var l,f=p.string.indexOf(d.blockCommentEnd),k=b.getRange(a.Pos(n.line,0),a.Pos(n.line,p.end));if(!(-1!=f&&f==p.string.length-d.blockCommentEnd.length&&n.ch>=f))if(0==p.string.indexOf(d.blockCommentStart)){if(e=k.slice(0,p.start),!/^\s*$/.test(e))for(e="",f=0;f<p.start;++f)e+=" "}else-1!=(l=k.indexOf(d.blockCommentContinue))&&l+d.blockCommentContinue.length>p.start&&/^\s*$/.test(k.slice(0,l))&&(e=k.slice(0,l));null!=e&&(e+=d.blockCommentContinue)}null==
+e&&d.lineComment&&t(b)&&(n=b.getLine(n.line),l=n.indexOf(d.lineComment),-1<l&&(e=n.slice(0,l),/\S/.test(e)?e=null:e+=d.lineComment+n.slice(l+d.lineComment.length).match(/^\s*/)[0]));if(null==e)return a.Pass;r[q]="\n"+e}b.operation(function(){for(var f=c.length-1;0<=f;f--)b.replaceRange(r[f],c[f].from(),c[f].to(),"+insert")})}function t(b){b=b.getOption("continueComments");return!b||"object"!=typeof b||!1!==b.continueLineComment}for(var g=["clike","css","javascript"],c=0;c<g.length;++c)a.extendMode(g[c],
+{blockCommentContinue:" * "});a.defineOption("continueComments",null,function(b,d,c){if(c&&c!=a.Init&&b.removeKeyMap("continueComment"),d)c="Enter","string"==typeof d?c=d:"object"==typeof d&&d.key&&(c=d.key),d={name:"continueComment"},d[c]=m,b.addKeyMap(d)})});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/edit/closebrackets.js",["../../lib/codemirror"],a):a(CodeMirror)})(function(a){function m(l,f){return"pairs"==f&&"string"==typeof l?l:"object"==typeof l&&null!=l[f]?l[f]:r[f]}function t(l){var f=l.state.closeBrackets;return!f||f.override?f:l.getModeAt(l.getCursor()).closeBrackets||f}function g(l,f){var k=t(l);if(!k||l.getOption("disableInput"))return a.Pass;
+var b=m(k,"pairs"),e=b.indexOf(f);if(-1==e)return a.Pass;for(var p,k=m(k,"triples"),n=b.charAt(e+1)==f,r=l.listSelections(),g=0==e%2,C=0;C<r.length;C++){var w;w=r[C];var u=w.head,y=l.getRange(u,q(u.line,u.ch+1));if(g&&!w.empty())w="surround";else if(!n&&g||y!=f)if(n&&1<u.ch&&0<=k.indexOf(f)&&l.getRange(q(u.line,u.ch-2),u)==f+f&&(2>=u.ch||l.getRange(q(u.line,u.ch-3),q(u.line,u.ch-2))!=f))w="addFour";else{if(n){if(a.isWordChar(y)||!d(l,u,f))return a.Pass}else if(!g||l.getLine(u.line).length!=u.ch&&
+!c(y,b)&&!/\s/.test(y))return a.Pass;w="both"}else w=n&&h(l,u)?"both":0<=k.indexOf(f)&&l.getRange(u,q(u.line,u.ch+3))==f+f+f?"skipThree":"skip";if(p){if(p!=w)return a.Pass}else p=w}var z=e%2?b.charAt(e-1):f,D=e%2?f:b.charAt(e+1);l.operation(function(){if("skip"==p)l.execCommand("goCharRight");else if("skipThree"==p)for(var f=0;3>f;f++)l.execCommand("goCharRight");else if("surround"==p){for(var k=l.getSelections(),f=0;f<k.length;f++)k[f]=z+k[f]+D;l.replaceSelections(k,"around");k=l.listSelections().slice();
+for(f=0;f<k.length;f++){var e=k,b=f,d;d=k[f];var c=0<a.cmpPos(d.anchor,d.head);d={anchor:new q(d.anchor.line,d.anchor.ch+(c?-1:1)),head:new q(d.head.line,d.head.ch+(c?1:-1))};e[b]=d}l.setSelections(k)}else"both"==p?(l.replaceSelection(z+D,null),l.triggerElectric(z+D),l.execCommand("goCharLeft")):"addFour"==p&&(l.replaceSelection(z+z+z+z,"before"),l.execCommand("goCharRight"))})}function c(l,f){var k=f.lastIndexOf(l);return-1<k&&1==k%2}function b(l,f){var k=l.getRange(q(f.line,f.ch-1),q(f.line,f.ch+
+1));return 2==k.length?k:null}function d(l,f,k){var e=l.getLine(f.line),b=l.getTokenAt(f);if(/\bstring2?\b/.test(b.type)||h(l,f))return!1;k=new a.StringStream(e.slice(0,f.ch)+k+e.slice(f.ch),4);for(k.pos=k.start=b.start;;){e=l.getMode().token(k,b.state);if(k.pos>=f.ch+1)return/\bstring2?\b/.test(e);k.start=k.pos}}function h(l,f){var k=l.getTokenAt(q(f.line,f.ch+1));return/\bstring/.test(k.type)&&k.start==f.ch}var r={pairs:"()[]{}''\"\"",triples:"",explode:"[]{}"},q=a.Pos;a.defineOption("autoCloseBrackets",
+!1,function(l,f,k){k&&k!=a.Init&&(l.removeKeyMap(p),l.state.closeBrackets=null);f&&(l.state.closeBrackets=f,l.addKeyMap(p))});for(var n=r.pairs+"`",p={Backspace:function(l){var f=t(l);if(!f||l.getOption("disableInput"))return a.Pass;for(var k=m(f,"pairs"),f=l.listSelections(),e=0;e<f.length;e++){if(!f[e].empty())return a.Pass;var d=b(l,f[e].head);if(!d||0!=k.indexOf(d)%2)return a.Pass}for(e=f.length-1;0<=e;e--)k=f[e].head,l.replaceRange("",q(k.line,k.ch-1),q(k.line,k.ch+1),"+delete")},Enter:function(e){var f=
+t(e),f=f&&m(f,"explode");if(!f||e.getOption("disableInput"))return a.Pass;for(var k=e.listSelections(),d=0;d<k.length;d++){if(!k[d].empty())return a.Pass;var c=b(e,k[d].head);if(!c||0!=f.indexOf(c)%2)return a.Pass}e.operation(function(){e.replaceSelection("\n\n",null);e.execCommand("goCharLeft");k=e.listSelections();for(var f=0;f<k.length;f++){var b=k[f].head.line;e.indentLine(b,null,!0);e.indentLine(b+1,null,!0)}})}},e=0;e<n.length;e++)p["'"+n.charAt(e)+"'"]=function(e){return function(f){return g(f,
+e)}}(n.charAt(e))});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/fold/xml-fold",["../../lib/codemirror"],a):a(CodeMirror)})(function(a){function m(e,b,f,k){this.line=b;this.ch=f;this.cm=e;this.text=e.getLine(b);this.min=k?Math.max(k.from,e.firstLine()):e.firstLine();this.max=k?Math.min(k.to-1,e.lastLine()):e.lastLine()}function t(e,b){var f=e.cm.getTokenTypeAt(n(e.line,b));return f&&/\btag\b/.test(f)}function g(e){if(!(e.line>=
+e.max))return e.ch=0,e.text=e.cm.getLine(++e.line),!0}function c(e){if(!(e.line<=e.min))return e.text=e.cm.getLine(--e.line),e.ch=e.text.length,!0}function b(e){for(;;){var b=e.text.indexOf("\x3e",e.ch);if(-1==b){if(g(e))continue;break}if(t(e,b+1)){var f=e.text.lastIndexOf("/",b),f=-1<f&&!/\S/.test(e.text.slice(f+1,b));return e.ch=b+1,f?"selfClose":"regular"}e.ch=b+1}}function d(e){for(;;){var b=e.ch?e.text.lastIndexOf("\x3c",e.ch-1):-1;if(-1==b){if(c(e))continue;break}if(t(e,b+1)){p.lastIndex=b;
+e.ch=b;var f=p.exec(e.text);if(f&&f.index==b)return f}else e.ch=b}}function h(e){for(;;){p.lastIndex=e.ch;var b=p.exec(e.text);if(!b){if(g(e))continue;break}if(t(e,b.index+1))return e.ch=b.index+b[0].length,b;e.ch=b.index+1}}function r(e,d){for(var f=[];;){var k,a=h(e),c=e.line,p=e.ch-(a?a[0].length:0);if(!a||!(k=b(e)))break;if("selfClose"!=k)if(a[1]){for(var q=f.length-1;0<=q;--q)if(f[q]==a[2]){f.length=q;break}if(0>q&&(!d||d==a[2]))return{tag:a[2],from:n(c,p),to:n(e.line,e.ch)}}else f.push(a[2])}}
+function q(b,a){for(var f=[];;){var k;a:for(k=b;;){var p=k.ch?k.text.lastIndexOf("\x3e",k.ch-1):-1;if(-1==p){if(c(k))continue;k=void 0;break a}if(t(k,p+1)){var h=k.text.lastIndexOf("/",p),h=-1<h&&!/\S/.test(k.text.slice(h+1,p));k=(k.ch=p+1,h?"selfClose":"regular");break a}k.ch=p}if(!k)break;if("selfClose"!=k){k=b.line;p=b.ch;h=d(b);if(!h)break;if(h[1])f.push(h[2]);else{for(var q=f.length-1;0<=q;--q)if(f[q]==h[2]){f.length=q;break}if(0>q&&(!a||a==h[2]))return{tag:h[2],from:n(b.line,b.ch),to:n(k,p)}}}else d(b)}}
+var n=a.Pos,p=RegExp("\x3c(/?)([A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD][A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD-:.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040]*)","g");a.registerHelper("fold","xml",function(e,a){for(var f=new m(e,a.line,0);;){var k,
+d=h(f);if(!d||f.line!=a.line||!(k=b(f)))break;if(!d[1]&&"selfClose"!=k)return k=n(f.line,f.ch),(f=r(f,d[2]))&&{from:k,to:f.from}}});a.findMatchingTag=function(e,a,f){var k=new m(e,a.line,a.ch,f);if(-1!=k.text.indexOf("\x3e")||-1!=k.text.indexOf("\x3c")){var c=b(k),p=c&&n(k.line,k.ch),h=c&&d(k);if(c&&h&&!(0<(k.line-a.line||k.ch-a.ch)))return a={from:n(k.line,k.ch),to:p,tag:h[2]},"selfClose"==c?{open:a,close:null,at:"open"}:h[1]?{open:q(k,h[2]),close:a,at:"close"}:(k=new m(e,p.line,p.ch,f),{open:a,
+close:r(k,h[2]),at:"open"})}};a.findEnclosingTag=function(b,a,f,k){for(var d=new m(b,a.line,a.ch,f);;){var c=q(d,k);if(!c)break;var p=new m(b,a.line,a.ch,f);if(p=r(p,c.tag))return{open:c,close:p}}};a.scanForClosingTag=function(b,a,f,k){return r(new m(b,a.line,a.ch,k?{from:0,to:k}:null),f)}});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror"),require("../fold/xml-fold")):"function"==typeof define&&define.amd?define("addon/edit/closetag.js",["../../lib/codemirror","../fold/xml-fold"],a):a(CodeMirror)})(function(a){function m(h){if(h.getOption("disableInput"))return a.Pass;for(var r=h.listSelections(),q=[],n=0;n<r.length;n++){if(!r[n].empty())return a.Pass;var p=r[n].head,e=h.getTokenAt(p),l=a.innerMode(h.getMode(),e.state),f=l.state;if("xml"!=
+l.mode.name||!f.tagName)return a.Pass;var k=h.getOption("autoCloseTags"),v="html"==l.mode.configuration,l="object"==typeof k&&k.dontCloseTags||v&&b,v="object"==typeof k&&k.indentTags||v&&d,k=f.tagName;e.end>p.ch&&(k=k.slice(0,k.length-e.end+p.ch));var t=k.toLowerCase();if(!k||"string"==e.type&&(e.end!=p.ch||!/[\"\']/.test(e.string.charAt(e.string.length-1))||1==e.string.length)||"tag"==e.type&&"closeTag"==f.type||e.string.indexOf("/")==e.string.length-1||l&&-1<g(l,t)||c(h,k,p,f,!0))return a.Pass;
+e=v&&-1<g(v,t);q[n]={indent:e,text:"\x3e"+(e?"\n\n":"")+"\x3c/"+k+"\x3e",newPos:e?a.Pos(p.line+1,0):a.Pos(p.line,p.ch+1)}}for(n=r.length-1;0<=n;n--)p=q[n],h.replaceRange(p.text,r[n].head,r[n].anchor,"+insert"),e=h.listSelections().slice(0),e[n]={head:p.newPos,anchor:p.newPos},h.setSelections(e),p.indent&&(h.indentLine(p.newPos.line,null,!0),h.indentLine(p.newPos.line+1,null,!0))}function t(b,d){for(var q=b.listSelections(),n=[],p=d?"/":"\x3c/",e=0;e<q.length;e++){if(!q[e].empty())return a.Pass;var l=
+q[e].head,f=b.getTokenAt(l),k=a.innerMode(b.getMode(),f.state),g=k.state;if(d&&("string"==f.type||"\x3c"!=f.string.charAt(0)||f.start!=l.ch-1))return a.Pass;if("xml"!=k.mode.name)if("htmlmixed"==b.getMode().name&&"javascript"==k.mode.name)k=p+"script";else{if("htmlmixed"!=b.getMode().name||"css"!=k.mode.name)return a.Pass;k=p+"style"}else{if(!g.context||!g.context.tagName||c(b,g.context.tagName,l,g))return a.Pass;k=p+g.context.tagName}"\x3e"!=b.getLine(l.line).charAt(f.end)&&(k+="\x3e");n[e]=k}b.replaceSelections(n);
+q=b.listSelections();for(e=0;e<q.length;e++)(e==q.length-1||q[e].head.line<q[e+1].head.line)&&b.indentLine(q[e].head.line)}function g(b,a){if(b.indexOf)return b.indexOf(a);for(var d=0,c=b.length;d<c;++d)if(b[d]==a)return d;return-1}function c(b,d,c,n,p){if(!a.scanForClosingTag)return!1;var e=Math.min(b.lastLine()+1,c.line+500);c=a.scanForClosingTag(b,c,null,e);if(!c||c.tag!=d)return!1;n=n.context;for(p=p?1:0;n&&n.tagName==d;n=n.prev)++p;c=c.to;for(n=1;n<p;n++){c=a.scanForClosingTag(b,c,null,e);if(!c||
+c.tag!=d)return!1;c=c.to}return!0}a.defineOption("autoCloseTags",!1,function(b,d,c){if(c!=a.Init&&c&&b.removeKeyMap("autoCloseTags"),d)c={name:"autoCloseTags"},("object"!=typeof d||d.whenClosing)&&(c["'/'"]=function(b){return b.getOption("disableInput")?a.Pass:t(b,!0)}),("object"!=typeof d||d.whenOpening)&&(c["'\x3e'"]=function(b){return m(b)}),b.addKeyMap(c)});var b="area base br col command embed hr img input keygen link meta param source track wbr".split(" "),d="applet blockquote body button div dl fieldset form frameset h1 h2 h3 h4 h5 h6 head html iframe layer legend object ol p select table ul".split(" ");
+a.commands.closeTag=function(b){return t(b)}});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/edit/matchbrackets.js",["../../lib/codemirror"],a):a(CodeMirror)})(function(a){function m(b,a,c){var e=b.getLineHandle(a.line),l=a.ch-1,f=c&&c.afterCursor;null==f&&(f=/(^| )cm-fat-cursor($| )/.test(b.getWrapperElement().className));e=!f&&0<=l&&h[e.text.charAt(l)]||h[e.text.charAt(++l)];if(!e)return null;f="\x3e"==e.charAt(1)?1:-1;if(c&&c.strict&&0<f!=
+(l==a.ch))return null;var k=b.getTokenTypeAt(d(a.line,l+1));b=t(b,d(a.line,l+(0<f?1:0)),f,k||null,c);return null==b?null:{from:d(a.line,l),to:b&&b.pos,match:b&&b.ch==e.charAt(0),forward:0<f}}function t(b,a,c,e,l){var f=l&&l.maxScanLineLength||1E4,k=l&&l.maxScanLines||1E3,g=[];l=l&&l.bracketRegex?l.bracketRegex:/[(){}[\]]/;for(var k=0<c?Math.min(a.line+k,b.lastLine()+1):Math.max(b.firstLine()-1,a.line-k),r=a.line;r!=k;r+=c){var t=b.getLine(r);if(t){var m=0<c?0:t.length-1,E=0<c?t.length:-1;if(!(t.length>
+f))for(r==a.line&&(m=a.ch-(0>c?1:0));m!=E;m+=c){var A=t.charAt(m);if(l.test(A)&&(void 0===e||b.getTokenTypeAt(d(r,m+1))==e))if("\x3e"==h[A].charAt(1)==0<c)g.push(A);else{if(!g.length)return{pos:d(r,m),ch:A};g.pop()}}}}return r-c!=(0<c?b.lastLine():b.firstLine())&&null}function g(a,c,p){for(var e=a.state.matchBrackets.maxHighlightLineLength||1E3,l=[],f=a.listSelections(),k=0;k<f.length;k++){var h=f[k].empty()&&m(a,f[k].head,p);if(h&&a.getLine(h.from.line).length<=e){var g=h.match?"CodeMirror-matchingbracket":
+"CodeMirror-nonmatchingbracket";l.push(a.markText(h.from,d(h.from.line,h.from.ch+1),{className:g}));h.to&&a.getLine(h.to.line).length<=e&&l.push(a.markText(h.to,d(h.to.line,h.to.ch+1),{className:g}))}}if(l.length){b&&a.state.focused&&a.focus();p=function(){a.operation(function(){for(var b=0;b<l.length;b++)l[b].clear()})};if(!c)return p;setTimeout(p,800)}}function c(b){b.operation(function(){r&&(r(),r=null);r=g(b,!1,b.state.matchBrackets)})}var b=/MSIE \d/.test(navigator.userAgent)&&(null==document.documentMode||
+8>document.documentMode),d=a.Pos,h={"(":")\x3e",")":"(\x3c","[":"]\x3e","]":"[\x3c","{":"}\x3e","}":"{\x3c"},r=null;a.defineOption("matchBrackets",!1,function(b,d,p){p&&p!=a.Init&&(b.off("cursorActivity",c),r&&(r(),r=null));d&&(b.state.matchBrackets="object"==typeof d?d:{},b.on("cursorActivity",c))});a.defineExtension("matchBrackets",function(){g(this,!0)});a.defineExtension("findMatchingBracket",function(b,a,c){return(c||"boolean"==typeof a)&&(c?(c.strict=a,a=c):a=a?{strict:!0}:null),m(this,b,a)});
+a.defineExtension("scanForBracket",function(b,a,c,e){return t(this,b,a,c,e)})});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror"),require("../fold/xml-fold")):"function"==typeof define&&define.amd?define("addon/edit/matchtags.js",["../../lib/codemirror","../fold/xml-fold"],a):a(CodeMirror)})(function(a){function m(a){a.state.tagHit&&a.state.tagHit.clear();a.state.tagOther&&a.state.tagOther.clear();a.state.tagHit=a.state.tagOther=null}function t(c){c.state.failedTagMatch=!1;c.operation(function(){if(m(c),!c.somethingSelected()){var b=
+c.getCursor(),d=c.getViewport();d.from=Math.min(d.from,b.line);d.to=Math.max(b.line+1,d.to);if(b=a.findMatchingTag(c,b,d))c.state.matchBothTags&&(d="open"==b.at?b.open:b.close)&&(c.state.tagHit=c.markText(d.from,d.to,{className:"CodeMirror-matchingtag"})),(b="close"==b.at?b.open:b.close)?c.state.tagOther=c.markText(b.from,b.to,{className:"CodeMirror-matchingtag"}):c.state.failedTagMatch=!0}})}function g(a){a.state.failedTagMatch&&t(a)}a.defineOption("matchTags",!1,function(c,b,d){d&&d!=a.Init&&(c.off("cursorActivity",
+t),c.off("viewportChange",g),m(c));b&&(c.state.matchBothTags="object"==typeof b&&b.bothTags,c.on("cursorActivity",t),c.on("viewportChange",g),t(c))});a.commands.toMatchingTag=function(c){var b=a.findMatchingTag(c,c.getCursor());b&&(b="close"==b.at?b.open:b.close)&&c.extendSelection(b.to,b.from)}});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/edit/trailingspace.js",["../../lib/codemirror"],a):a(CodeMirror)})(function(a){a.defineOption("showTrailingSpace",!1,function(m,t,g){g==a.Init&&(g=!1);g&&!t?m.removeOverlay("trailingspace"):!g&&t&&m.addOverlay({token:function(a){for(var b=a.string.length,d=b;d&&/\s/.test(a.string.charAt(d-1));--d);return d>a.pos?(a.pos=d,null):(a.pos=b,"trailingspace")},
+name:"trailingspace"})})});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/fold/foldcode",["../../lib/codemirror"],a):a(CodeMirror)})(function(a){function m(b,d,c,r){function q(f){var a=n(b,d);if(!a||a.to.line-a.from.line<p)return null;for(var e=b.findMarksAt(a.from),c=0;c<e.length;++c)if(e[c].__isFold&&"fold"!==r){if(!f)return null;a.cleared=!0;e[c].clear()}return a}if(c&&c.call){var n=c;c=null}else n=g(b,c,"rangeFinder");
+"number"==typeof d&&(d=a.Pos(d,0));var p=g(b,c,"minFoldSize"),e=q(!0);if(g(b,c,"scanUp"))for(;!e&&d.line>b.firstLine();)d=a.Pos(d.line-1,0),e=q(!1);if(e&&!e.cleared&&"unfold"!==r){var l=t(b,c);a.on(l,"mousedown",function(b){f.clear();a.e_preventDefault(b)});var f=b.markText(e.from,e.to,{replacedWith:l,clearOnEnter:g(b,c,"clearOnEnter"),__isFold:!0});f.on("clear",function(f,c){a.signal(b,"unfold",b,f,c)});a.signal(b,"fold",b,e.from,e.to)}}function t(b,a){var c=g(b,a,"widget");if("string"==typeof c){var r=
+document.createTextNode(c),c=document.createElement("span");c.appendChild(r);c.className="CodeMirror-foldmarker"}else c&&(c=c.cloneNode(!0));return c}function g(b,a,h){return a&&void 0!==a[h]?a[h]:(b=b.options.foldOptions)&&void 0!==b[h]?b[h]:c[h]}a.newFoldFunction=function(b,a){return function(c,g){m(c,g,{rangeFinder:b,widget:a})}};a.defineExtension("foldCode",function(b,a,c){m(this,b,a,c)});a.defineExtension("isFolded",function(b){b=this.findMarksAt(b);for(var a=0;a<b.length;++a)if(b[a].__isFold)return!0});
+a.commands.toggleFold=function(b){b.foldCode(b.getCursor())};a.commands.fold=function(b){b.foldCode(b.getCursor(),null,"fold")};a.commands.unfold=function(b){b.foldCode(b.getCursor(),null,"unfold")};a.commands.foldAll=function(b){b.operation(function(){for(var c=b.firstLine(),h=b.lastLine();c<=h;c++)b.foldCode(a.Pos(c,0),null,"fold")})};a.commands.unfoldAll=function(b){b.operation(function(){for(var c=b.firstLine(),h=b.lastLine();c<=h;c++)b.foldCode(a.Pos(c,0),null,"unfold")})};a.registerHelper("fold",
+"combine",function(){var b=Array.prototype.slice.call(arguments,0);return function(a,c){for(var g=0;g<b.length;++g){var t=b[g](a,c);if(t)return t}}});a.registerHelper("fold","auto",function(b,a){for(var c=b.getHelpers(a,"fold"),g=0;g<c.length;g++){var t=c[g](b,a);if(t)return t}});var c={rangeFinder:a.fold.auto,widget:"↔",minFoldSize:0,scanUp:!1,clearOnEnter:!0};a.defineOption("foldOptions",null);a.defineExtension("foldOption",function(b,a){return g(this,b,a)})});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror"),require("./foldcode")):"function"==typeof define&&define.amd?define("addon/fold/foldgutter.js",["../../lib/codemirror","./foldcode"],a):a(CodeMirror)})(function(a){function m(b){this.options=b;this.from=this.to=0}function t(b,a){for(var c=b.findMarks(n(a,0),n(a+1,0)),f=0;f<c.length;++f)if(c[f].__isFold&&c[f].find().from.line==a)return c[f]}function g(b){if("string"==typeof b){var a=document.createElement("div");
+return a.className=b+" CodeMirror-guttermarker-subtle",a}return b.cloneNode(!0)}function c(b,a,c){var f=b.state.foldGutter.options,k=a,d=b.foldOption(f,"minFoldSize"),h=b.foldOption(f,"rangeFinder");b.eachLine(a,c,function(a){var c=null;if(t(b,k))c=g(f.indicatorFolded);else{var e=n(k,0);(e=h&&h(b,e))&&e.to.line-e.from.line>=d&&(c=g(f.indicatorOpen))}b.setGutterMarker(a,f.gutter,c);++k})}function b(b){var a=b.getViewport(),d=b.state.foldGutter;d&&(b.operation(function(){c(b,a.from,a.to)}),d.from=a.from,
+d.to=a.to)}function d(b,a,c){var f=b.state.foldGutter;f&&(f=f.options,c==f.gutter&&((c=t(b,a))?c.clear():b.foldCode(n(a,0),f.rangeFinder)))}function h(a){var c=a.state.foldGutter;if(c){var d=c.options;c.from=c.to=0;clearTimeout(c.changeUpdate);c.changeUpdate=setTimeout(function(){b(a)},d.foldOnChangeTimeSpan||600)}}function r(a){var e=a.state.foldGutter;if(e){var d=e.options;clearTimeout(e.changeUpdate);e.changeUpdate=setTimeout(function(){var f=a.getViewport();e.from==e.to||20<f.from-e.to||20<e.from-
+f.to?b(a):a.operation(function(){f.from<e.from&&(c(a,f.from,e.from),e.from=f.from);f.to>e.to&&(c(a,e.to,f.to),e.to=f.to)})},d.updateViewportTimeSpan||400)}}function q(b,a){var d=b.state.foldGutter;if(d){var f=a.line;f>=d.from&&f<d.to&&c(b,f,f+1)}}a.defineOption("foldGutter",!1,function(c,e,l){l&&l!=a.Init&&(c.clearGutter(c.state.foldGutter.options.gutter),c.state.foldGutter=null,c.off("gutterClick",d),c.off("change",h),c.off("viewportChange",r),c.off("fold",q),c.off("unfold",q),c.off("swapDoc",h));
+if(e){l=c.state;var f=e;e=(!0===f&&(f={}),null==f.gutter&&(f.gutter="CodeMirror-foldgutter"),null==f.indicatorOpen&&(f.indicatorOpen="CodeMirror-foldgutter-open"),null==f.indicatorFolded&&(f.indicatorFolded="CodeMirror-foldgutter-folded"),f);l.foldGutter=new m(e);b(c);c.on("gutterClick",d);c.on("change",h);c.on("viewportChange",r);c.on("fold",q);c.on("unfold",q);c.on("swapDoc",h)}});var n=a.Pos});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/fold/brace-fold.js",["../../lib/codemirror"],a):a(CodeMirror)})(function(a){a.registerHelper("fold","brace",function(m,t){function g(f){for(var k=t.ch,e=0;;)if(k=0>=k?-1:d.lastIndexOf(f,k-1),-1!=k){if(1==e&&k<t.ch)break;if(c=m.getTokenTypeAt(a.Pos(b,k+1)),!/^(comment|string)/.test(c))return k+1;--k}else{if(1==e)break;e=1;k=d.length}}var c,b=t.line,d=
+m.getLine(b),h="{",r="}",q=g("{");if(null==q&&(h="[",r="]",q=g("[")),null!=q){var n,p,e=1,l=m.lastLine(),f=b;a:for(;f<=l;++f)for(var k=m.getLine(f),v=f==b?q:0;;){var x=k.indexOf(h,v),B=k.indexOf(r,v);if(0>x&&(x=k.length),0>B&&(B=k.length),(v=Math.min(x,B))==k.length)break;if(m.getTokenTypeAt(a.Pos(f,v+1))==c)if(v==x)++e;else if(!--e){n=f;p=v;break a}++v}if(null!=n&&(b!=n||p!=q))return{from:a.Pos(b,q),to:a.Pos(n,p)}}});a.registerHelper("fold","import",function(m,t){function g(b){if(b<m.firstLine()||
+b>m.lastLine())return null;var c=m.getTokenAt(a.Pos(b,1));if(/\S/.test(c.string)||(c=m.getTokenAt(a.Pos(b,c.end+1))),"keyword"!=c.type||"import"!=c.string)return null;var d=b;for(b=Math.min(m.lastLine(),b+10);d<=b;++d){var g=m.getLine(d).indexOf(";");if(-1!=g)return{startCh:c.end,end:a.Pos(d,g)}}}var c,b=t.line,d=g(b);if(!d||g(b-1)||(c=g(b-2))&&c.end.line==b-1)return null;for(c=d.end;;){var h=g(c.line+1);if(null==h)break;c=h.end}return{from:m.clipPos(a.Pos(b,d.startCh+1)),to:c}});a.registerHelper("fold",
+"include",function(m,t){function g(b){if(b<m.firstLine()||b>m.lastLine())return null;var c=m.getTokenAt(a.Pos(b,1));return/\S/.test(c.string)||(c=m.getTokenAt(a.Pos(b,c.end+1))),"meta"==c.type&&"#include"==c.string.slice(0,8)?c.start+8:void 0}var c=t.line,b=g(c);if(null==b||null!=g(c-1))return null;for(var d=c;null!=g(d+1);)++d;return{from:a.Pos(c,b+1),to:m.clipPos(a.Pos(d))}})});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/fold/comment-fold.js",["../../lib/codemirror"],a):a(CodeMirror)})(function(a){a.registerGlobalHelper("fold","comment",function(a){return a.blockCommentStart&&a.blockCommentEnd},function(m,t){var g=m.getModeAt(t),c=g.blockCommentStart,g=g.blockCommentEnd;if(c&&g){var b,d=t.line;b=m.getLine(d);for(var h=t.ch,r=0;;)if(h=0>=h?-1:b.lastIndexOf(c,h-1),-1!=
+h){if(1==r&&h<t.ch)return;if(/comment/.test(m.getTokenTypeAt(a.Pos(d,h+1)))&&(0==h||b.slice(h-g.length,h)==g||!/comment/.test(m.getTokenTypeAt(a.Pos(d,h))))){b=h+c.length;break}--h}else{if(1==r)return;r=1;h=b.length}var q,n,r=1,h=m.lastLine(),p=d;a:for(;p<=h;++p)for(var e=m.getLine(p),l=p==d?b:0;;){var f=e.indexOf(c,l),k=e.indexOf(g,l);if(0>f&&(f=e.length),0>k&&(k=e.length),(l=Math.min(f,k))==e.length)break;if(l==f)++r;else if(!--r){q=p;n=l;break a}++l}if(null!=q&&(d!=q||n!=b))return{from:a.Pos(d,
+b),to:a.Pos(q,n)}}})});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/fold/indent-fold.js",["../../lib/codemirror"],a):a(CodeMirror)})(function(a){function m(t,g){var c=t.getLine(g),b=c.search(/\S/);return-1==b||/\bcomment\b/.test(t.getTokenTypeAt(a.Pos(g,b+1)))?-1:a.countColumn(c,null,t.getOption("tabSize"))}a.registerHelper("fold","indent",function(t,g){var c=m(t,g.line);if(!(0>c)){for(var b=null,d=g.line+1,h=t.lastLine();d<=
+h;++d){var r=m(t,d);if(-1!=r){if(!(r>c))break;b=d}}return b?{from:a.Pos(g.line,t.getLine(g.line).length),to:a.Pos(b,t.getLine(b).length)}:void 0}})});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/format/autoFormatAll.js",["../../lib/codemirror"],a):a(CodeMirror)})(function(a){a.defineExtension("autoFormatAll",function(m,t){for(var g=this,c=g.getMode(),b=g.getRange(m,t).split("\n"),d=a.copyState(c,g.getTokenAt(m).state),h=g.getOption("tabSize"),r="",q=0,n=0==m.ch,p=0;p<b.length;++p){for(var e=new a.StringStream(b[p],h);!e.eol();){var l=a.innerMode(c,
+d),f=c.token(e,d),k=e.current();e.start=e.pos;n&&!/\S/.test(k)||(r+=k,n=!1);!n&&l.mode.newlineAfterToken&&l.mode.newlineAfterToken(f,k,e.string.slice(e.pos)||b[p+1]||"",l.state)&&(r+="\n",n=!0,++q)}!e.pos&&c.blankLine&&c.blankLine(d);!n&&p<b.length-1&&(r+="\n",n=!0,++q)}g.operation(function(){g.replaceRange(r,m,t);for(var b=m.line+1,a=m.line+q;b<=a;++b)g.indentLine(b,"smart");g.setCursor({line:0,ch:0})})})});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/format/formatting.js",["../../lib/codemirror"],a):a(CodeMirror)})(function(a){function m(a){for(var g=[/for\s*?\((.*?)\)/g,/&#?[a-z0-9]+;[\s\S]/g,/\"(.*?)((\")|$)/g,/\/\*(.*?)(\*\/|$)/g,/^\/\/.*/g],c=[],b=0;b<g.length;b++)for(var d=0;d<a.length;){var h=a.substr(d).match(g[b]);if(null==h)break;c.push({start:d+h.index,end:d+h.index+h[0].length});d+=h.index+
+Math.max(1,h[0].length)}return c.sort(function(b,a){return b.start-a.start}),c}a.extendMode("css",{commentStart:"/*",commentEnd:"*/",newlineAfterToken:function(a,g){return/^[;{}]$/.test(g)}});a.extendMode("javascript",{commentStart:"/*",commentEnd:"*/",wordWrapChars:[";","\\{","\\}"],autoFormatLineBreaks:function(a){var g=0,c=this.jsonMode?function(b){return b.replace(/([,{])/g,"$1\n").replace(/}/g,"\n}")}:function(b){return b.replace(/(;|\{|\})([^\r\n;])/g,"$1\n$2")},b=m(a),d="";if(null!=b){for(var h=
+0;h<b.length;h++)b[h].start>g&&(d+=c(a.substring(g,b[h].start)),g=b[h].start),b[h].start<=g&&b[h].end>=g&&(d+=a.substring(g,b[h].end),g=b[h].end);g<a.length&&(d+=c(a.substr(g)))}else d=c(a);return d.replace(/^\n*|\n*$/,"")}});a.extendMode("xml",{commentStart:"\x3c!--",commentEnd:"--\x3e",noBreak:!1,noBreakEmpty:null,tagType:"",tagName:"",isXML:!1,newlineAfterToken:function(a,g,c,b){b=!1;var d=null,h="";if(this.isXML="xml"==this.configuration,"comment"==a||/\x3c!--/.test(c))return!1;if("tag"==a){0==
+g.indexOf("\x3c")&&0==!g.indexOf("\x3c/")&&(this.tagType="open",d=g.match(/^<\s*?([\w]+?)$/i),this.tagName=null!=d?d[1]:"",h=this.tagName.toLowerCase(),-1!="|label|li|option|textarea|title|a|b|bdi|bdo|big|center|cite|del|em|font|i|img|ins|s|small|span|strike|strong|sub|sup|u|".indexOf("|"+h+"|")&&(this.noBreak=!0));if(0==g.indexOf("\x3e")&&"open"==this.tagType)return this.tagType="",RegExp("^"+(this.isXML?"[^\x3c]*?":"")+"\x3c/s*?"+this.tagName+"s*?\x3e","i").test(c)?(this.noBreak=!1,this.isXML||
+(this.tagName=""),!1):(b=this.noBreak,this.noBreak=!1,!b);if(0==g.indexOf("\x3c/")&&(this.tagType="close",d=g.match(/^<\/\s*?([\w]+?)$/i),null!=d&&(h=d[1].toLowerCase()),-1!="|a|b|bdi|bdo|big|center|cite|del|em|font|i|img|ins|s|small|span|strike|strong|sub|sup|u|".indexOf("|"+h+"|")&&(this.noBreak=!0)),0==g.indexOf("\x3e")&&"close"==this.tagType)return this.tagType="",0==c.indexOf("\x3c")&&(d=c.match(/^<\/?\s*?([\w]+?)(\s|>)/i),h=null!=d?d[1].toLowerCase():"",-1=="|label|li|option|textarea|title|a|b|bdi|bdo|big|center|cite|del|em|font|i|img|ins|s|small|span|strike|strong|sub|sup|u|".indexOf("|"+
+h+"|"))?(this.noBreak=!1,!0):(b=this.noBreak,this.noBreak=!1,!b)}return 0==c.indexOf("\x3c")&&(this.noBreak=!1,this.isXML&&""!=this.tagName?(this.tagName="",!1):(d=c.match(/^<\/?\s*?([\w]+?)(\s|>)/i),h=null!=d?d[1].toLowerCase():"",-1=="|label|li|option|textarea|title|a|b|bdi|bdo|big|center|cite|del|em|font|i|img|ins|s|small|span|strike|strong|sub|sup|u|".indexOf("|"+h+"|")))}});a.defineExtension("commentRange",function(t,g,c){var b=this,d=a.innerMode(b.getMode(),b.getTokenAt(g).state).mode;b.operation(function(){if(t)b.replaceRange(d.commentEnd,
+c),b.replaceRange(d.commentStart,g),b.setSelection(g,{line:c.line,ch:c.ch+d.commentStart.length+d.commentEnd.length}),g.line==c.line&&g.ch==c.ch&&b.setCursor(g.line,g.ch+d.commentStart.length);else{var a=b.getRange(g,c),r=a.indexOf(d.commentStart),q=a.lastIndexOf(d.commentEnd);-1<r&&-1<q&&q>r&&(a=a.substr(0,r)+a.substring(r+d.commentStart.length,q)+a.substr(q+d.commentEnd.length));b.replaceRange(a,g,c);b.setSelection(g,{line:c.line,ch:c.ch-d.commentStart.length-d.commentEnd.length})}})});a.defineExtension("autoIndentRange",
+function(a,g){var c=this;this.operation(function(){for(var b=a.line;b<=g.line;b++)c.indentLine(b,"smart")})});a.defineExtension("autoFormatRange",function(t,g){for(var c=this,b=c.getMode(),d=c.getRange(t,g).split("\n"),h=a.copyState(b,c.getTokenAt(t).state),r=c.getOption("tabSize"),q="",n=0,p=0==t.ch,e=0;e<d.length;++e){for(var l=new a.StringStream(d[e],r);!l.eol();){var f=a.innerMode(b,h),k=b.token(l,h),m=l.current();l.start=l.pos;p&&!/\S/.test(m)||(q+=m,p=!1);!p&&f.mode.newlineAfterToken&&f.mode.newlineAfterToken(k,
+m,l.string.slice(l.pos)||d[e+1]||"",f.state)&&(q+="\n",p=!0,++n)}!l.pos&&b.blankLine&&b.blankLine(h);!p&&e<d.length-1&&(q+="\n",p=!0,++n)}c.operation(function(){c.replaceRange(q,t,g);for(var b=t.line+1,a=t.line+n;b<=a;++b)c.indentLine(b,"smart");c.setSelection(t,c.getCursor(!1))})})});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/selection/active-line.js",["../../lib/codemirror"],a):a(CodeMirror)})(function(a){function m(a){for(var c=0;c<a.state.activeLines.length;c++)a.removeLineClass(a.state.activeLines[c],"wrap",b),a.removeLineClass(a.state.activeLines[c],"background",d),a.removeLineClass(a.state.activeLines[c],"gutter",h)}function t(b,a){if(b.length!=a.length)return!1;for(var c=
+0;c<b.length;c++)if(b[c]!=a[c])return!1;return!0}function g(a,c){for(var g=[],p=0;p<c.length;p++){var e=c[p],l=a.getOption("styleActiveLine");if("object"==typeof l&&l.nonEmpty?e.anchor.line==e.head.line:e.empty())e=a.getLineHandleVisualStart(e.head.line),g[g.length-1]!=e&&g.push(e)}t(a.state.activeLines,g)||a.operation(function(){m(a);for(var c=0;c<g.length;c++)a.addLineClass(g[c],"wrap",b),a.addLineClass(g[c],"background",d),a.addLineClass(g[c],"gutter",h);a.state.activeLines=g})}function c(b,a){g(b,
+a.ranges)}var b="CodeMirror-activeline",d="CodeMirror-activeline-background",h="CodeMirror-activeline-gutter";a.defineOption("styleActiveLine",!1,function(b,d,h){h=h!=a.Init&&h;d!=h&&(h&&(b.off("beforeSelectionChange",c),m(b),delete b.state.activeLines),d&&(b.state.activeLines=[],g(b,b.listSelections()),b.on("beforeSelectionChange",c)))})});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/search/searchcursor",["../../lib/codemirror"],a):a(CodeMirror)})(function(a){function m(b){if(!b.global){var a=b.flags;b=new RegExp(b.source,(null!=a?a:(b.ignoreCase?"i":"")+(b.global?"g":"")+(b.multiline?"m":""))+"g")}return b}function t(b,a,c){a=m(a);var e=c.line,d=c.ch;for(c=b.lastLine();e<=c;e++,d=0)if(a.lastIndex=d,d=b.getLine(e),d=a.exec(d))return{from:l(e,
+d.index),to:l(e,d.index+d[0].length),match:d}}function g(b,a,c){if(!/\\s|\\n|\n|\\W|\\D|\[\^/.test(a.source))return t(b,a,c);a=m(a);for(var d,e=1,g=c.line,h=b.lastLine();g<=h;){for(var p=0;p<e;p++){var n=b.getLine(g++);d=null==d?n:d+"\n"+n}e*=2;a.lastIndex=c.ch;if(p=a.exec(d))return a=d.slice(0,p.index).split("\n"),b=p[0].split("\n"),c=c.line+a.length-1,a=a[a.length-1].length,{from:l(c,a),to:l(c+b.length-1,1==b.length?a+b[0].length:b[b.length-1].length),match:p}}}function c(b,a){for(var c,d=0;;){a.lastIndex=
+d;var e=a.exec(b);if(!e||(c=e,(d=c.index+(c[0].length||1))==b.length))return c}}function b(b,a,d){a=m(a);var e=d.line,g=d.ch;for(d=b.firstLine();e>=d;e--,g=-1){var h=b.getLine(e);-1<g&&(h=h.slice(0,g));if(g=c(h,a))return{from:l(e,g.index),to:l(e,g.index+g[0].length),match:g}}}function d(b,a,d){a=m(a);for(var e,g=1,h=d.line,p=b.firstLine();h>=p;){for(var n=0;n<g;n++){var t=b.getLine(h--);e=null==e?t.slice(0,d.ch):t+"\n"+e}g*=2;if(n=c(e,a))return a=e.slice(0,n.index).split("\n"),b=n[0].split("\n"),
+h+=a.length,a=a[a.length-1].length,{from:l(h,a),to:l(h+b.length-1,1==b.length?a+b[0].length:b[b.length-1].length),match:n}}}function h(b,a,c,d){if(b.length==a.length)return c;var e=0;for(a=c+Math.max(0,b.length-a.length);;){if(e==a)return e;var g=e+a>>1,l=d(b.slice(0,g)).length;if(l==c)return g;l>c?a=g:e=g+1}}function r(b,a,c,d){if(!a.length)return null;d=d?p:e;a=d(a).split(/\r|\n\r?/);var g=c.line;c=c.ch;var n=b.lastLine()+1-a.length;a:for(;g<=n;g++,c=0){var t=b.getLine(g).slice(c),m=d(t);if(1==
+a.length){var q=m.indexOf(a[0]);if(-1==q)continue a;h(t,m,q,d);return{from:l(g,h(t,m,q,d)+c),to:l(g,h(t,m,q+a[0].length,d)+c)}}q=m.length-a[0].length;if(m.slice(q)==a[0]){for(var r=1;r<a.length-1;r++)if(d(b.getLine(g+r))!=a[r])continue a;var r=b.getLine(g+a.length-1),u=d(r),y=a[a.length-1];if(r.slice(0,y.length)==y)return{from:l(g,h(t,m,q,d)+c),to:l(g+a.length-1,h(r,u,y.length,d))}}}}function q(a,b,c,d){if(!b.length)return null;d=d?p:e;b=d(b).split(/\r|\n\r?/);var g=c.line,n=c.ch,t=a.firstLine()-
+1+b.length;a:for(;g>=t;g--,n=-1){var m=a.getLine(g);-1<n&&(m=m.slice(0,n));n=d(m);if(1==b.length){c=n.lastIndexOf(b[0]);if(-1==c)continue a;return{from:l(g,h(m,n,c,d)),to:l(g,h(m,n,c+b[0].length,d))}}var q=b[b.length-1];if(n.slice(0,q.length)==q){var r=1;for(c=g-b.length+1;r<b.length-1;r++)if(d(a.getLine(c+r))!=b[r])continue a;c=a.getLine(g+1-b.length);r=d(c);if(r.slice(r.length-b[0].length)==b[0])return{from:l(g+1-b.length,h(c,r,c.length-b[0].length,d)),to:l(g,h(m,n,q.length,d))}}}}function n(a,
+c,e,h){this.atOccurrence=!1;this.doc=a;e=e?a.clipPos(e):l(0,0);this.pos={from:e,to:e};var n;"object"==typeof h?n=h.caseFold:(n=h,h=null);"string"==typeof c?(null==n&&(n=!1),this.matches=function(b,d){return(b?q:r)(a,c,d,n)}):(c=m(c),h&&!1===h.multiline?this.matches=function(d,e){return(d?b:t)(a,c,e)}:this.matches=function(b,e){return(b?d:g)(a,c,e)})}var p,e,l=a.Pos;String.prototype.normalize?(p=function(b){return b.normalize("NFD").toLowerCase()},e=function(b){return b.normalize("NFD")}):(p=function(b){return b.toLowerCase()},
+e=function(b){return b});n.prototype={findNext:function(){return this.find(!1)},findPrevious:function(){return this.find(!0)},find:function(b){for(var c=this.matches(b,this.doc.clipPos(b?this.pos.from:this.pos.to));c&&0==a.cmpPos(c.from,c.to);)b?c.from.ch?c.from=l(c.from.line,c.from.ch-1):c=c.from.line==this.doc.firstLine()?null:this.matches(b,this.doc.clipPos(l(c.from.line-1))):c.to.ch<this.doc.getLine(c.to.line).length?c.to=l(c.to.line,c.to.ch+1):c=c.to.line==this.doc.lastLine()?null:this.matches(b,
+l(c.to.line+1,0));if(c)return this.pos=c,this.atOccurrence=!0,this.pos.match||!0;b=l(b?this.doc.firstLine():this.doc.lastLine()+1,0);return this.pos={from:b,to:b},this.atOccurrence=!1},from:function(){if(this.atOccurrence)return this.pos.from},to:function(){if(this.atOccurrence)return this.pos.to},replace:function(b,c){if(this.atOccurrence){var d=a.splitLines(b);this.doc.replaceRange(d,this.pos.from,this.pos.to,c);this.pos.to=l(this.pos.from.line+d.length-1,d[d.length-1].length+(1==d.length?this.pos.from.ch:
+0))}}};a.defineExtension("getSearchCursor",function(b,a,c){return new n(this.doc,b,a,c)});a.defineDocExtension("getSearchCursor",function(b,a,c){return new n(this,b,a,c)});a.defineExtension("selectMatches",function(b,c){for(var d=[],e=this.getSearchCursor(b,this.getCursor("from"),c);e.findNext()&&!(0<a.cmpPos(e.to(),this.getCursor("to")));)d.push({anchor:e.from(),head:e.to()});d.length&&this.setSelections(d,0)})});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/scroll/annotatescrollbar",["../../lib/codemirror"],a):a(CodeMirror)})(function(a){function m(a,g){function c(a){clearTimeout(b.doRedraw);b.doRedraw=setTimeout(function(){b.redraw()},a)}this.cm=a;this.options=g;this.buttonHeight=g.scrollButtonHeight||a.getOption("scrollButtonHeight");this.annotations=[];this.doRedraw=this.doUpdate=null;this.div=a.getWrapperElement().appendChild(document.createElement("div"));
+this.div.style.cssText="position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none";this.computeScale();var b=this;a.on("refresh",this.resizeHandler=function(){clearTimeout(b.doUpdate);b.doUpdate=setTimeout(function(){b.computeScale()&&c(20)},100)});a.on("markerAdded",this.resizeHandler);a.on("markerCleared",this.resizeHandler);!1!==g.listenForChanges&&a.on("change",this.changeHandler=function(){c(250)})}a.defineExtension("annotateScrollbar",function(a){return"string"==typeof a&&(a={className:a}),
+new m(this,a)});a.defineOption("scrollButtonHeight",0);m.prototype.computeScale=function(){var a=this.cm,a=(a.getWrapperElement().clientHeight-a.display.barHeight-2*this.buttonHeight)/a.getScrollerElement().scrollHeight;if(a!=this.hScale)return this.hScale=a,!0};m.prototype.update=function(a){this.annotations=a;this.redraw()};m.prototype.redraw=function(a){function g(a,b){return q!=a.line&&(q=a.line,n=c.getLineHandle(q)),n.widgets&&n.widgets.length||h&&n.height>m?c.charCoords(a,"local")[b?"top":"bottom"]:
+c.heightAtLine(n,"local")+(b?0:n.height)}!1!==a&&this.computeScale();var c=this.cm;a=this.hScale;var b=document.createDocumentFragment(),d=this.annotations,h=c.getOption("lineWrapping"),m=h&&1.5*c.defaultTextHeight(),q=null,n=null,p=c.lastLine();if(c.display.barWidth)for(var e,l=0;l<d.length;l++){var f=d[l];if(!(f.to.line>p)){for(var k=e||g(f.from,!0)*a,v=g(f.to,!1)*a;l<d.length-1&&!(d[l+1].to.line>p)&&!((e=g(d[l+1].from,!0)*a)>v+.9);)f=d[++l],v=g(f.to,!1)*a;if(v!=k){var v=Math.max(v-k,3),x=b.appendChild(document.createElement("div"));
+x.style.cssText="position: absolute; right: 0px; width: "+Math.max(c.display.barWidth-1,2)+"px; top: "+(k+this.buttonHeight)+"px; height: "+v+"px";x.className=this.options.className;f.id&&x.setAttribute("annotation-id",f.id)}}}this.div.textContent="";this.div.appendChild(b)};m.prototype.clear=function(){this.cm.off("refresh",this.resizeHandler);this.cm.off("markerAdded",this.resizeHandler);this.cm.off("markerCleared",this.resizeHandler);this.changeHandler&&this.cm.off("change",this.changeHandler);
+this.div.parentNode.removeChild(this.div)}});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror"),require("./searchcursor"),require("../scroll/annotatescrollbar")):"function"==typeof define&&define.amd?define("addon/search/matchesonscrollbar",["../../lib/codemirror","./searchcursor","../scroll/annotatescrollbar"],a):a(CodeMirror)})(function(a){function m(a,c,b,d){this.cm=a;this.options=d;var h={listenForChanges:!1},m;for(m in d)h[m]=d[m];h.className||(h.className="CodeMirror-search-match");this.annotation=
+a.annotateScrollbar(h);this.query=c;this.caseFold=b;this.gap={from:a.firstLine(),to:a.lastLine()+1};this.matches=[];this.update=null;this.findMatches();this.annotation.update(this.matches);var q=this;a.on("change",this.changeHandler=function(a,b){q.onChange(b)})}function t(a,c,b){return a<=c?a:Math.max(c,a+b)}a.defineExtension("showMatchesOnScrollbar",function(a,c,b){return"string"==typeof b&&(b={className:b}),b||(b={}),new m(this,a,c,b)});m.prototype.findMatches=function(){if(this.gap){for(var g=
+0;g<this.matches.length;g++){var c=this.matches[g];if(c.from.line>=this.gap.to)break;c.to.line>=this.gap.from&&this.matches.splice(g--,1)}for(var b=this.cm.getSearchCursor(this.query,a.Pos(this.gap.from,0),this.caseFold),d=this.options&&this.options.maxMatches||1E3;b.findNext();){c={from:b.from(),to:b.to()};if(c.from.line>=this.gap.to)break;if(this.matches.splice(g++,0,c),this.matches.length>d)break}this.gap=null}};m.prototype.onChange=function(g){var c=g.from.line,b=a.changeEnd(g).line,d=b-g.to.line;
+if(this.gap?(this.gap.from=Math.min(t(this.gap.from,c,d),g.from.line),this.gap.to=Math.max(t(this.gap.to,c,d),g.from.line)):this.gap={from:g.from.line,to:b+1},d)for(g=0;g<this.matches.length;g++){var b=this.matches[g],h=t(b.from.line,c,d);h!=b.from.line&&(b.from=a.Pos(h,b.from.ch));h=t(b.to.line,c,d);h!=b.to.line&&(b.to=a.Pos(h,b.to.ch))}clearTimeout(this.update);var m=this;this.update=setTimeout(function(){m.updateAfterChange()},250)};m.prototype.updateAfterChange=function(){this.findMatches();this.annotation.update(this.matches)};
+m.prototype.clear=function(){this.cm.off("change",this.changeHandler);this.annotation.clear()}});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror"),require("./matchesonscrollbar")):"function"==typeof define&&define.amd?define("addon/search/match-highlighter.js",["../../lib/codemirror","./matchesonscrollbar"],a):a(CodeMirror)})(function(a){function m(a){this.options={};for(var b in q)this.options[b]=(a&&a.hasOwnProperty(b)?a:q)[b];this.matchesonscroll=this.overlay=this.timeout=null;this.active=!1}function t(a){var b=a.state.matchHighlighter;(b.active||
+a.hasFocus())&&c(a,b)}function g(a){var b=a.state.matchHighlighter;b.active||(b.active=!0,c(a,b))}function c(a,b){clearTimeout(b.timeout);b.timeout=setTimeout(function(){h(a)},b.options.delay)}function b(a,b,c,d){var f=a.state.matchHighlighter;if(a.addOverlay(f.overlay=r(b,c,d)),f.options.annotateScrollbar&&a.showMatchesOnScrollbar)f.matchesonscroll=a.showMatchesOnScrollbar(c?new RegExp("\\b"+b+"\\b"):b,!1,{className:"CodeMirror-selection-highlight-scrollbar"})}function d(a){var b=a.state.matchHighlighter;
+b.overlay&&(a.removeOverlay(b.overlay),b.overlay=null,b.matchesonscroll&&(b.matchesonscroll.clear(),b.matchesonscroll=null))}function h(a){a.operation(function(){var c=a.state.matchHighlighter;if(d(a),!a.somethingSelected()&&c.options.showToken){for(var e=!0===c.options.showToken?/[\w$]/:c.options.showToken,g=a.getCursor(),f=a.getLine(g.line),h=g=g.ch;g&&e.test(f.charAt(g-1));)--g;for(;h<f.length&&e.test(f.charAt(h));)++h;return void(g<h&&b(a,f.slice(g,h),e,c.options.style))}e=a.getCursor("from");
+f=a.getCursor("to");if((g=e.line==f.line)&&!(g=!c.options.wordsOnly))a:if(null!==a.getRange(e,f).match(/^\w+$/)){if(0<e.ch&&(g={line:e.line,ch:e.ch-1},g=a.getRange(g,e),null===g.match(/\W/))){g=!1;break a}if(f.ch<a.getLine(e.line).length&&(g={line:f.line,ch:f.ch+1},g=a.getRange(f,g),null===g.match(/\W/))){g=!1;break a}g=!0}else g=!1;g&&(e=a.getRange(e,f),c.options.trim&&(e=e.replace(/^\s+|\s+$/g,"")),e.length>=c.options.minChars&&b(a,e,!1,c.options.style))})}function r(a,b,c){return{token:function(d){var f;
+if(f=d.match(a))(f=!b)||(f=!(d.start&&b.test(d.string.charAt(d.start-1))||d.pos!=d.string.length&&b.test(d.string.charAt(d.pos))));if(f)return c;d.next();d.skipTo(a.charAt(0))||d.skipToEnd()}}}var q={style:"matchhighlight",minChars:2,delay:100,wordsOnly:!1,annotateScrollbar:!1,showToken:!1,trim:!0};a.defineOption("highlightSelectionMatches",!1,function(b,c,e){if(e&&e!=a.Init&&(d(b),clearTimeout(b.state.matchHighlighter.timeout),b.state.matchHighlighter=null,b.off("cursorActivity",t),b.off("focus",
+g)),c)c=b.state.matchHighlighter=new m(c),b.hasFocus()?(c.active=!0,h(b)):b.on("focus",g),b.on("cursorActivity",t)})});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/mode/multiplex.js",["../../lib/codemirror"],a):a(CodeMirror)})(function(a){a.multiplexingMode=function(m){function t(a,b,d,g){return"string"==typeof b?(d=a.indexOf(b,d),g&&-1<d?d+b.length:d):(b=b.exec(d?a.slice(d):a))?b.index+d+(g?b[0].length:0):-1}var g=Array.prototype.slice.call(arguments,1);return{startState:function(){return{outer:a.startState(m),
+innerActive:null,inner:null}},copyState:function(c){return{outer:a.copyState(m,c.outer),innerActive:c.innerActive,inner:c.innerActive&&a.copyState(c.innerActive.mode,c.inner)}},token:function(c,b){if(b.innerActive){var d=b.innerActive,h=c.string;if(!d.close&&c.sol())return b.innerActive=b.inner=null,this.token(c,b);var r=d.close?t(h,d.close,c.pos,d.parseDelimiters):-1;if(r==c.pos&&!d.parseDelimiters)return c.match(d.close),b.innerActive=b.inner=null,d.delimStyle&&d.delimStyle+" "+d.delimStyle+"-close";
+-1<r&&(c.string=h.slice(0,r));var q=d.mode.token(c,b.inner);return-1<r&&(c.string=h),r==c.pos&&d.parseDelimiters&&(b.innerActive=b.inner=null),d.innerStyle&&(q=q?q+" "+d.innerStyle:d.innerStyle),q}d=1/0;h=c.string;for(q=0;q<g.length;++q){var n=g[q],r=t(h,n.open,c.pos);if(r==c.pos)return n.parseDelimiters||c.match(n.open),b.innerActive=n,b.inner=a.startState(n.mode,m.indent?m.indent(b.outer,""):0),n.delimStyle&&n.delimStyle+" "+n.delimStyle+"-open";-1!=r&&r<d&&(d=r)}d!=1/0&&(c.string=h.slice(0,d));
+r=m.token(c,b.outer);return d!=1/0&&(c.string=h),r},indent:function(c,b){var d=c.innerActive?c.innerActive.mode:m;return d.indent?d.indent(c.innerActive?c.inner:c.outer,b):a.Pass},blankLine:function(c){var b=c.innerActive?c.innerActive.mode:m;if(b.blankLine&&b.blankLine(c.innerActive?c.inner:c.outer),c.innerActive)"\n"===c.innerActive.close&&(c.innerActive=c.inner=null);else for(var d=0;d<g.length;++d){var h=g[d];"\n"===h.open&&(c.innerActive=h,c.inner=a.startState(h.mode,b.indent?b.indent(c.outer,
+""):0))}},electricChars:m.electricChars,innerMode:function(a){return a.inner?{state:a.inner,mode:a.innerActive.mode}:{state:a.outer,mode:m}}}}});
+(function(a){"function"==typeof a.define&&a.define("addons","addon/comment/continuecomment.js addon/edit/closebrackets.js addon/edit/closetag.js addon/edit/matchbrackets.js addon/edit/matchtags.js addon/edit/trailingspace.js addon/fold/foldgutter.js addon/fold/brace-fold.js addon/fold/comment-fold.js addon/fold/indent-fold.js addon/format/autoFormatAll.js addon/format/formatting.js addon/selection/active-line.js addon/search/match-highlighter.js addon/mode/multiplex.js".split(" "),function(){})})(this);
\ No newline at end of file
diff --git a/js/ckeditor/plugins/codemirror/js/codemirror.addons.search.min.js b/js/ckeditor/plugins/codemirror/js/codemirror.addons.search.min.js
new file mode 100644 (file)
index 0000000..5f906a0
--- /dev/null
@@ -0,0 +1,26 @@
+!function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/search/searchcursor",["../../lib/codemirror"],a):a(CodeMirror)}(function(a){function x(d){if(!d.global){var b=d.flags;d=new RegExp(d.source,(null!=b?b:(d.ignoreCase?"i":"")+(d.global?"g":"")+(d.multiline?"m":""))+"g")}return d}function z(d,b,e){b=x(b);var k=e.line,f=e.ch;for(e=d.lastLine();k<=e;k++,f=0)if(b.lastIndex=f,f=d.getLine(k),f=b.exec(f))return{from:m(k,
+f.index),to:m(k,f.index+f[0].length),match:f}}function q(d,b,e){if(!/\\s|\\n|\n|\\W|\\D|\[\^/.test(b.source))return z(d,b,e);b=x(b);for(var k,f=1,a=e.line,c=d.lastLine();a<=c;){for(var h=0;h<f;h++){var t=d.getLine(a++);k=null==k?t:k+"\n"+t}f*=2;b.lastIndex=e.ch;if(h=b.exec(k))return b=k.slice(0,h.index).split("\n"),d=h[0].split("\n"),e=e.line+b.length-1,b=b[b.length-1].length,{from:m(e,b),to:m(e+d.length-1,1==d.length?b+d[0].length:d[d.length-1].length),match:h}}}function p(d,b){for(var e,k=0;;){b.lastIndex=
+k;var f=b.exec(d);if(!f||(e=f,(k=e.index+(e[0].length||1))==d.length))return e}}function c(d,b,e){b=x(b);var k=e.line,f=e.ch;for(e=d.firstLine();k>=e;k--,f=-1){var a=d.getLine(k);-1<f&&(a=a.slice(0,f));if(f=p(a,b))return{from:m(k,f.index),to:m(k,f.index+f[0].length),match:f}}}function g(d,b,e){b=x(b);for(var k,a=1,c=e.line,n=d.firstLine();c>=n;){for(var h=0;h<a;h++){var t=d.getLine(c--);k=null==k?t.slice(0,e.ch):t+"\n"+k}a*=2;if(h=p(k,b))return b=k.slice(0,h.index).split("\n"),d=h[0].split("\n"),
+c+=b.length,b=b[b.length-1].length,{from:m(c,b),to:m(c+d.length-1,1==d.length?b+d[0].length:d[d.length-1].length),match:h}}}function l(d,b,e,a){if(d.length==b.length)return e;var f=0;for(b=e+Math.max(0,d.length-b.length);;){if(f==b)return f;var c=f+b>>1,m=a(d.slice(0,c)).length;if(m==e)return c;m>e?b=c:f=c+1}}function r(d,b,e,a){if(!b.length)return null;a=a?n:y;b=a(b).split(/\r|\n\r?/);var f=e.line;e=e.ch;var c=d.lastLine()+1-b.length;a:for(;f<=c;f++,e=0){var g=d.getLine(f).slice(e),h=a(g);if(1==
+b.length){var t=h.indexOf(b[0]);if(-1==t)continue a;l(g,h,t,a);return{from:m(f,l(g,h,t,a)+e),to:m(f,l(g,h,t+b[0].length,a)+e)}}t=h.length-b[0].length;if(h.slice(t)==b[0]){for(var u=1;u<b.length-1;u++)if(a(d.getLine(f+u))!=b[u])continue a;var u=d.getLine(f+b.length-1),E=a(u),A=b[b.length-1];if(u.slice(0,A.length)==A)return{from:m(f,l(g,h,t,a)+e),to:m(f+b.length-1,l(u,E,A.length,a))}}}}function v(d,b,a,c){if(!b.length)return null;c=c?n:y;b=c(b).split(/\r|\n\r?/);var f=a.line,g=a.ch,q=d.firstLine()-
+1+b.length;a:for(;f>=q;f--,g=-1){var h=d.getLine(f);-1<g&&(h=h.slice(0,g));g=c(h);if(1==b.length){a=g.lastIndexOf(b[0]);if(-1==a)continue a;return{from:m(f,l(h,g,a,c)),to:m(f,l(h,g,a+b[0].length,c))}}var t=b[b.length-1];if(g.slice(0,t.length)==t){var u=1;for(a=f-b.length+1;u<b.length-1;u++)if(c(d.getLine(a+u))!=b[u])continue a;a=d.getLine(f+1-b.length);u=c(a);if(u.slice(u.length-b[0].length)==b[0])return{from:m(f+1-b.length,l(a,u,a.length-b[0].length,c)),to:m(f,l(h,g,t.length,c))}}}}function w(a,
+b,e,k){this.atOccurrence=!1;this.doc=a;e=e?a.clipPos(e):m(0,0);this.pos={from:e,to:e};var f;"object"==typeof k?f=k.caseFold:(f=k,k=null);"string"==typeof b?(null==f&&(f=!1),this.matches=function(c,e){return(c?v:r)(a,b,e,f)}):(b=x(b),k&&!1===k.multiline?this.matches=function(e,f){return(e?c:z)(a,b,f)}:this.matches=function(c,e){return(c?g:q)(a,b,e)})}var n,y,m=a.Pos;String.prototype.normalize?(n=function(a){return a.normalize("NFD").toLowerCase()},y=function(a){return a.normalize("NFD")}):(n=function(a){return a.toLowerCase()},
+y=function(a){return a});w.prototype={findNext:function(){return this.find(!1)},findPrevious:function(){return this.find(!0)},find:function(d){for(var b=this.matches(d,this.doc.clipPos(d?this.pos.from:this.pos.to));b&&0==a.cmpPos(b.from,b.to);)d?b.from.ch?b.from=m(b.from.line,b.from.ch-1):b=b.from.line==this.doc.firstLine()?null:this.matches(d,this.doc.clipPos(m(b.from.line-1))):b.to.ch<this.doc.getLine(b.to.line).length?b.to=m(b.to.line,b.to.ch+1):b=b.to.line==this.doc.lastLine()?null:this.matches(d,
+m(b.to.line+1,0));if(b)return this.pos=b,this.atOccurrence=!0,this.pos.match||!0;d=m(d?this.doc.firstLine():this.doc.lastLine()+1,0);return this.pos={from:d,to:d},this.atOccurrence=!1},from:function(){if(this.atOccurrence)return this.pos.from},to:function(){if(this.atOccurrence)return this.pos.to},replace:function(d,b){if(this.atOccurrence){var c=a.splitLines(d);this.doc.replaceRange(c,this.pos.from,this.pos.to,b);this.pos.to=m(this.pos.from.line+c.length-1,c[c.length-1].length+(1==c.length?this.pos.from.ch:
+0))}}};a.defineExtension("getSearchCursor",function(a,b,c){return new w(this.doc,a,b,c)});a.defineDocExtension("getSearchCursor",function(a,b,c){return new w(this,a,b,c)});a.defineExtension("selectMatches",function(c,b){for(var e=[],k=this.getSearchCursor(c,this.getCursor("from"),b);k.findNext()&&!(0<a.cmpPos(k.to(),this.getCursor("to")));)e.push({anchor:k.from(),head:k.to()});e.length&&this.setSelections(e,0)})});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/dialog/dialog",["../../lib/codemirror"],a):a(CodeMirror)})(function(a){function x(a,p,c){var g;return g=a.getWrapperElement().appendChild(document.createElement("div")),g.className=c?"CodeMirror-dialog CodeMirror-dialog-bottom":"CodeMirror-dialog CodeMirror-dialog-top","string"==typeof p?g.innerHTML=p:g.appendChild(p),g}function z(a,p){a.state.currentNotificationClose&&
+a.state.currentNotificationClose();a.state.currentNotificationClose=p}a.defineExtension("openDialog",function(q,p,c){function g(a){"string"==typeof a?n.value=a:v||(v=!0,r.parentNode.removeChild(r),w.focus(),c.onClose&&c.onClose(r))}c||(c={});z(this,null);var l,r=x(this,q,c.bottom),v=!1,w=this,n=r.getElementsByTagName("input")[0];return n?(n.focus(),c.value&&(n.value=c.value,!1!==c.selectValueOnOpen&&n.select()),c.onInput&&a.on(n,"input",function(a){c.onInput(a,n.value,g)}),c.onKeyUp&&a.on(n,"keyup",
+function(a){c.onKeyUp(a,n.value,g)}),a.on(n,"keydown",function(l){c&&c.onKeyDown&&c.onKeyDown(l,n.value,g)||((27==l.keyCode||!1!==c.closeOnEnter&&13==l.keyCode)&&(n.blur(),a.e_stop(l),g()),13==l.keyCode&&p(n.value,l))}),!1!==c.closeOnBlur&&a.on(n,"blur",g)):(l=r.getElementsByTagName("button")[0])&&(a.on(l,"click",function(){g();w.focus()}),!1!==c.closeOnBlur&&a.on(l,"blur",g),l.focus()),g});a.defineExtension("openConfirm",function(q,p,c){function g(){r||(r=!0,l.parentNode.removeChild(l),v.focus())}
+z(this,null);var l=x(this,q,c&&c.bottom);q=l.getElementsByTagName("button");var r=!1,v=this,w=1;q[0].focus();for(c=0;c<q.length;++c){var n=q[c];!function(c){a.on(n,"click",function(l){a.e_preventDefault(l);g();c&&c(v)})}(p[c]);a.on(n,"blur",function(){--w;setTimeout(function(){0>=w&&g()},200)});a.on(n,"focus",function(){++w})}});a.defineExtension("openNotification",function(q,p){function c(){r||(r=!0,clearTimeout(g),l.parentNode.removeChild(l))}z(this,c);var g,l=x(this,q,p&&p.bottom),r=!1,v=p&&void 0!==
+p.duration?p.duration:5E3;return a.on(l,"click",function(g){a.e_preventDefault(g);c()}),v&&(g=setTimeout(c,v)),c})});
+(function(a){"object"==typeof exports&&"object"==typeof module?a(require("../../lib/codemirror"),require("./searchcursor"),require("../dialog/dialog")):"function"==typeof define&&define.amd?define("addon/search/search.js",["../../lib/codemirror","./searchcursor","../dialog/dialog"],a):a(CodeMirror)})(function(a){function x(h,a){return"string"==typeof h?h=new RegExp(h.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$\x26"),a?"gi":"g"):h.global||(h=new RegExp(h.source,h.ignoreCase?"gi":"g")),{token:function(a){h.lastIndex=
+a.pos;var b=h.exec(a.string);if(b&&b.index==a.pos)return a.pos+=b[0].length||1,"searching";b?a.pos=b.index:a.skipToEnd()}}}function z(){this.overlay=this.posFrom=this.posTo=this.lastQuery=this.query=null}function q(h){return h.state.search||(h.state.search=new z)}function p(h){return"string"==typeof h&&h==h.toLowerCase()}function c(h,a,b){return h.getSearchCursor(a,b,{caseFold:p(a),multiline:!0})}function g(h,a,b,c,f){h.openDialog(a,c,{value:b,selectValueOnOpen:!0,closeOnEnter:!1,onClose:function(){d(h)},
+onKeyDown:f})}function l(h,a,b,c,d){h.openDialog?h.openDialog(a,d,{value:c,selectValueOnOpen:!0}):d(prompt(b,c))}function r(a,b,c,d){a.openConfirm?a.openConfirm(b,d):confirm(c)&&d[0]()}function v(a){return a.replace(/\\(.)/g,function(a,h){return"n"==h?"\n":"r"==h?"\r":h})}function w(a){var b=a.match(/^\/(.*)\/([a-z]*)$/);if(b)try{a=new RegExp(b[1],-1==b[2].indexOf("i")?"":"i")}catch(c){}else a=v(a);return("string"==typeof a?""==a:a.test(""))&&(a=/x^/),a}function n(a,b,c){b.queryText=c;b.query=w(c);
+a.removeOverlay(b.overlay,p(b.query));b.overlay=x(b.query,p(b.query));a.addOverlay(b.overlay);a.showMatchesOnScrollbar&&(b.annotate&&(b.annotate.clear(),b.annotate=null),b.annotate=a.showMatchesOnScrollbar(b.query,p(b.query)))}function y(b,c,d,f){var e=q(b);if(e.query)return m(b,c);var p=b.getSelection()||e.lastQuery;if(d&&b.openDialog){var B=null,r=function(c,d){a.e_stop(d);c&&(c!=e.queryText&&(n(b,e,c),e.posFrom=e.posTo=b.getCursor()),B&&(B.style.opacity=1),m(b,d.shiftKey,function(a,c){var d;3>
+c.line&&document.querySelector&&(d=b.display.wrapper.querySelector(".CodeMirror-dialog"))&&d.getBoundingClientRect().bottom-4>b.cursorCoords(c,"window").top&&((B=d).style.opacity=.4)}))};g(b,k,p,r,function(c,d){var e=a.keyName(c),f=a.keyMap[b.getOption("keyMap")][e];f||(f=b.getOption("extraKeys")[e]);"findNext"==f||"findPrev"==f||"findPersistentNext"==f||"findPersistentPrev"==f?(a.e_stop(c),n(b,q(b),d),b.execCommand(f)):"find"!=f&&"findPersistent"!=f||(a.e_stop(c),r(d,c))});f&&p&&(n(b,e,p),m(b,c))}else l(b,
+k,"Search for:",p,function(a){a&&!e.query&&b.operation(function(){n(b,e,a);e.posFrom=e.posTo=b.getCursor();m(b,c)})})}function m(b,d,f){b.operation(function(){var e=q(b),g=c(b,e.query,d?e.posFrom:e.posTo);(g.find(d)||(g=c(b,e.query,d?a.Pos(b.lastLine()):a.Pos(b.firstLine(),0)),g.find(d)))&&(b.setSelection(g.from(),g.to()),b.scrollIntoView({from:g.from(),to:g.to()},20),e.posFrom=g.from(),e.posTo=g.to(),f&&f(g.from(),g.to()))})}function d(b){b.operation(function(){var a=q(b);(a.lastQuery=a.query)&&
+(a.query=a.queryText=null,b.removeOverlay(a.overlay),a.annotate&&(a.annotate.clear(),a.annotate=null))})}function b(b,a,d){b.operation(function(){for(var e=c(b,a);e.findNext();)if("string"!=typeof a){var f=b.getRange(e.from(),e.to()).match(a);e.replace(d.replace(/\$(\d)/g,function(b,a){return f[a]}))}else e.replace(d)})}function e(a,e){if(!a.getOption("readOnly")){var g=a.getSelection()||q(a).lastQuery,k='\x3cspan class\x3d"CodeMirror-search-label"\x3e'+(e?"Replace all:":"Replace:")+"\x3c/span\x3e";
+l(a,k+f,k,g,function(f){f&&(f=w(f),l(a,C,"Replace with:","",function(g){if(g=v(g),e)b(a,f,g);else{d(a);var k=c(a,f,a.getCursor("from")),l=function(){var d,e=k.from();!(d=k.findNext())&&(k=c(a,f),!(d=k.findNext())||e&&k.from().line==e.line&&k.from().ch==e.ch)||(a.setSelection(k.from(),k.to()),a.scrollIntoView({from:k.from(),to:k.to()}),r(a,D,"Replace?",[function(){m(d)},l,function(){b(a,f,g)}]))},m=function(a){k.replace("string"==typeof f?g:g.replace(/\$(\d)/g,function(b,c){return a[c]}));l()};l()}}))})}}
+var k='\x3cspan class\x3d"CodeMirror-search-label"\x3eSearch:\x3c/span\x3e \x3cinput type\x3d"text" style\x3d"width: 10em" class\x3d"CodeMirror-search-field"/\x3e \x3cspan style\x3d"color: #888" class\x3d"CodeMirror-search-hint"\x3e(Use /re/ syntax for regexp search)\x3c/span\x3e',f=' \x3cinput type\x3d"text" style\x3d"width: 10em" class\x3d"CodeMirror-search-field"/\x3e \x3cspan style\x3d"color: #888" class\x3d"CodeMirror-search-hint"\x3e(Use /re/ syntax for regexp search)\x3c/span\x3e',C='\x3cspan class\x3d"CodeMirror-search-label"\x3eWith:\x3c/span\x3e \x3cinput type\x3d"text" style\x3d"width: 10em" class\x3d"CodeMirror-search-field"/\x3e',
+D='\x3cspan class\x3d"CodeMirror-search-label"\x3eReplace?\x3c/span\x3e \x3cbutton\x3eYes\x3c/button\x3e \x3cbutton\x3eNo\x3c/button\x3e \x3cbutton\x3eAll\x3c/button\x3e \x3cbutton\x3eStop\x3c/button\x3e';a.commands.find=function(a){d(a);y(a)};a.commands.findPersistent=function(a){d(a);y(a,!1,!0)};a.commands.findPersistentNext=function(a){y(a,!1,!0,!0)};a.commands.findPersistentPrev=function(a){y(a,!0,!0,!0)};a.commands.findNext=y;a.commands.findPrev=function(a){y(a,!0)};a.commands.clearSearch=d;
+a.commands.replace=e;a.commands.replaceAll=function(a){e(a,!0)}});(function(a){"function"==typeof a.define&&a.define("addonSearch",["addon/search/search.js"],function(){})})(this);
\ No newline at end of file
diff --git a/js/ckeditor/plugins/codemirror/js/codemirror.min.js b/js/ckeditor/plugins/codemirror/js/codemirror.min.js
new file mode 100644 (file)
index 0000000..2f07d97
--- /dev/null
@@ -0,0 +1,315 @@
+!function(ga,X){"object"==typeof exports&&"undefined"!=typeof module?module.exports=X():"function"==typeof define&&define.amd?define("codemirror.js",X):ga.CodeMirror=X()}(this,function(){function ga(a){return new RegExp("(^|\\s)"+a+"(?:$|\\s)\\s*")}function X(a){for(var b=a.childNodes.length;0<b;--b)a.removeChild(a.firstChild);return a}function Z(a,b){return X(a).appendChild(b)}function r(a,b,c,d){a=document.createElement(a);if(c&&(a.className=c),d&&(a.style.cssText=d),"string"==typeof b)a.appendChild(document.createTextNode(b));
+else if(b)for(c=0;c<b.length;++c)a.appendChild(b[c]);return a}function Za(a,b,c,d){a=r(a,b,c,d);return a.setAttribute("role","presentation"),a}function va(a,b){if(3==b.nodeType&&(b=b.parentNode),a.contains)return a.contains(b);do if(11==b.nodeType&&(b=b.host),b==a)return!0;while(b=b.parentNode)}function qa(){var a;try{a=document.activeElement}catch(b){a=document.body||null}for(;a&&a.shadowRoot&&a.shadowRoot.activeElement;)a=a.shadowRoot.activeElement;return a}function Ea(a,b){var c=a.className;ga(b).test(c)||
+(a.className+=(c?" ":"")+b)}function Jc(a,b){for(var c=a.split(" "),d=0;d<c.length;d++)c[d]&&!ga(c[d]).test(b)&&(b+=" "+c[d]);return b}function Kc(a){var b=Array.prototype.slice.call(arguments,1);return function(){return a.apply(null,b)}}function Fa(a,b,c){b||(b={});for(var d in a)!a.hasOwnProperty(d)||!1===c&&b.hasOwnProperty(d)||(b[d]=a[d]);return b}function ea(a,b,c,d,e){null==b&&-1==(b=a.search(/[^\s\u00a0]/))&&(b=a.length);d=d||0;for(e=e||0;;){var f=a.indexOf("\t",d);if(0>f||f>=b)return e+(b-
+d);e+=f-d;e+=c-e%c;d=f+1}}function N(a,b){for(var c=0;c<a.length;++c)if(a[c]==b)return c;return-1}function Lc(a,b,c){for(var d=0,e=0;;){var f=a.indexOf("\t",d);-1==f&&(f=a.length);var g=f-d;if(f==a.length||e+g>=b)return d+Math.min(g,b-e);if(e+=f-d,e+=c-e%c,d=f+1,e>=b)return d}}function Mc(a){for(;dc.length<=a;)dc.push(z(dc)+" ");return dc[a]}function z(a){return a[a.length-1]}function ec(a,b){for(var c=[],d=0;d<a.length;d++)c[d]=b(a[d],d);return c}function bg(a,b,c){for(var d=0,e=c(b);d<a.length&&
+c(a[d])<=e;)d++;a.splice(d,0,b)}function Qd(){}function Rd(a,b){var c;return Object.create?c=Object.create(a):(Qd.prototype=a,c=new Qd),b&&Fa(b,c),c}function Nc(a){return/\w/.test(a)||"\80"<a&&(a.toUpperCase()!=a.toLowerCase()||cg.test(a))}function fc(a,b){return b?!!(-1<b.source.indexOf("\\w")&&Nc(a))||b.test(a):Nc(a)}function Sd(a){for(var b in a)if(a.hasOwnProperty(b)&&a[b])return!1;return!0}function Oc(a){return 768<=a.charCodeAt(0)&&dg.test(a)}function Td(a,b,c){for(;(0>c?0<b:b<a.length)&&Oc(a.charAt(b));)b+=
+c;return b}function gc(a,b,c){for(;;){if(1>=Math.abs(b-c))return a(b)?b:c;var d=Math.floor((b+c)/2);a(d)?c=d:b=d}}function eg(a,b,c){this.input=c;this.scrollbarFiller=r("div",null,"CodeMirror-scrollbar-filler");this.scrollbarFiller.setAttribute("cm-not-content","true");this.gutterFiller=r("div",null,"CodeMirror-gutter-filler");this.gutterFiller.setAttribute("cm-not-content","true");this.lineDiv=Za("div",null,"CodeMirror-code");this.selectionDiv=r("div",null,null,"position: relative; z-index: 1");
+this.cursorDiv=r("div",null,"CodeMirror-cursors");this.measure=r("div",null,"CodeMirror-measure");this.lineMeasure=r("div",null,"CodeMirror-measure");this.lineSpace=Za("div",[this.measure,this.lineMeasure,this.selectionDiv,this.cursorDiv,this.lineDiv],null,"position: relative; outline: none");var d=Za("div",[this.lineSpace],"CodeMirror-lines");this.mover=r("div",[d],null,"position: relative");this.sizer=r("div",[this.mover],"CodeMirror-sizer");this.sizerWidth=null;this.heightForcer=r("div",null,null,
+"position: absolute; height: "+Ud+"px; width: 1px;");this.gutters=r("div",null,"CodeMirror-gutters");this.lineGutter=null;this.scroller=r("div",[this.sizer,this.heightForcer,this.gutters],"CodeMirror-scroll");this.scroller.setAttribute("tabIndex","-1");this.wrapper=r("div",[this.scrollbarFiller,this.gutterFiller,this.scroller],"CodeMirror");C&&8>B&&(this.gutters.style.zIndex=-1,this.scroller.style.paddingRight=0);R||wa&&rb||(this.scroller.draggable=!0);a&&(a.appendChild?a.appendChild(this.wrapper):
+a(this.wrapper));this.reportedViewFrom=this.reportedViewTo=this.viewFrom=this.viewTo=b.first;this.view=[];this.externalMeasured=this.renderedView=null;this.lastWrapHeight=this.lastWrapWidth=this.viewOffset=0;this.updateLineNumbers=null;this.nativeBarWidth=this.barHeight=this.barWidth=0;this.scrollbarsClipped=!1;this.lineNumWidth=this.lineNumInnerWidth=this.lineNumChars=null;this.alignWidgets=!1;this.maxLine=this.cachedCharWidth=this.cachedTextHeight=this.cachedPaddingH=null;this.maxLineLength=0;this.maxLineChanged=
+!1;this.wheelDX=this.wheelDY=this.wheelStartX=this.wheelStartY=null;this.shift=!1;this.activeTouch=this.selForContextMenu=null;c.init(this)}function w(a,b){if(0>(b-=a.first)||b>=a.size)throw Error("There is no line "+(b+a.first)+" in the document.");for(var c=a;!c.lines;)for(var d=0;;++d){var e=c.children[d],f=e.chunkSize();if(b<f){c=e;break}b-=f}return c.lines[b]}function Ga(a,b,c){var d=[],e=b.line;return a.iter(b.line,c.line+1,function(a){a=a.text;e==c.line&&(a=a.slice(0,c.ch));e==b.line&&(a=a.slice(b.ch));
+d.push(a);++e}),d}function Pc(a,b,c){var d=[];return a.iter(b,c,function(a){d.push(a.text)}),d}function la(a,b){var c=b-a.height;if(c)for(var d=a;d;d=d.parent)d.height+=c}function D(a){if(null==a.parent)return null;var b=a.parent;a=N(b.lines,a);for(var c=b.parent;c;b=c,c=c.parent)for(var d=0;c.children[d]!=b;++d)a+=c.children[d].chunkSize();return a+b.first}function Ha(a,b){var c=a.first;a:do{for(var d=0;d<a.children.length;++d){var e=a.children[d],f=e.height;if(b<f){a=e;continue a}b-=f;c+=e.chunkSize()}return c}while(!a.lines);
+for(d=0;d<a.lines.length;++d){e=a.lines[d].height;if(b<e)break;b-=e}return c+d}function sb(a,b){return b>=a.first&&b<a.first+a.size}function Qc(a,b){return String(a.lineNumberFormatter(b+a.firstLineNumber))}function m(a,b,c){if(void 0===c&&(c=null),!(this instanceof m))return new m(a,b,c);this.line=a;this.ch=b;this.sticky=c}function x(a,b){return a.line-b.line||a.ch-b.ch}function Rc(a,b){return a.sticky==b.sticky&&0==x(a,b)}function Sc(a){return m(a.line,a.ch)}function hc(a,b){return 0>x(a,b)?b:a}
+function ic(a,b){return 0>x(a,b)?a:b}function v(a,b){if(b.line<a.first)return m(a.first,0);var c=a.first+a.size-1;if(b.line>c)c=m(c,w(a,c).text.length);else var c=w(a,b.line).text.length,d=b.ch,c=null==d||d>c?m(b.line,c):0>d?m(b.line,0):b;return c}function Vd(a,b){for(var c=[],d=0;d<b.length;d++)c[d]=v(a,b[d]);return c}function jc(a,b,c){this.marker=a;this.from=b;this.to=c}function tb(a,b){if(a)for(var c=0;c<a.length;++c){var d=a[c];if(d.marker==b)return d}}function Tc(a,b){if(b.full)return null;
+var c=sb(a,b.from.line)&&w(a,b.from.line).markedSpans,d=sb(a,b.to.line)&&w(a,b.to.line).markedSpans;if(!c&&!d)return null;var e=b.from.ch,f=b.to.ch,g=0==x(b.from,b.to),h;if(c)for(var k=0;k<c.length;++k){var l=c[k],n=l.marker;if(null==l.from||(n.inclusiveLeft?l.from<=e:l.from<e)||!(l.from!=e||"bookmark"!=n.type||g&&l.marker.insertLeft)){var q=null==l.to||(n.inclusiveRight?l.to>=e:l.to>e);(h||(h=[])).push(new jc(n,l.from,q?null:l.to))}}var c=h,p;if(d)for(h=0;h<d.length;++h)if(k=d[h],l=k.marker,null==
+k.to||(l.inclusiveRight?k.to>=f:k.to>f)||k.from==f&&"bookmark"==l.type&&(!g||k.marker.insertLeft))n=null==k.from||(l.inclusiveLeft?k.from<=f:k.from<f),(p||(p=[])).push(new jc(l,n?null:k.from-f,null==k.to?null:k.to-f));d=p;f=1==b.text.length;g=z(b.text).length+(f?e:0);if(c)for(p=0;p<c.length;++p)h=c[p],null==h.to&&((k=tb(d,h.marker))?f&&(h.to=null==k.to?null:k.to+g):h.to=e);if(d)for(e=0;e<d.length;++e)p=d[e],(null!=p.to&&(p.to+=g),null==p.from)?tb(c,p.marker)||(p.from=g,f&&(c||(c=[])).push(p)):(p.from+=
+g,f&&(c||(c=[])).push(p));c&&(c=Wd(c));d&&d!=c&&(d=Wd(d));e=[c];if(!f){var u,f=b.text.length-2;if(0<f&&c)for(g=0;g<c.length;++g)null==c[g].to&&(u||(u=[])).push(new jc(c[g].marker,null,null));for(c=0;c<f;++c)e.push(u);e.push(d)}return e}function Wd(a){for(var b=0;b<a.length;++b){var c=a[b];null!=c.from&&c.from==c.to&&!1!==c.marker.clearWhenEmpty&&a.splice(b--,1)}return a.length?a:null}function fg(a,b,c){var d=null;if(a.iter(b.line,c.line+1,function(a){if(a.markedSpans)for(var b=0;b<a.markedSpans.length;++b){var c=
+a.markedSpans[b].marker;!c.readOnly||d&&-1!=N(d,c)||(d||(d=[])).push(c)}}),!d)return null;a=[{from:b,to:c}];for(b=0;b<d.length;++b){c=d[b];for(var e=c.find(0),f=0;f<a.length;++f){var g=a[f];if(!(0>x(g.to,e.from)||0<x(g.from,e.to))){var h=[f,1],k=x(g.from,e.from),l=x(g.to,e.to);(0>k||!c.inclusiveLeft&&!k)&&h.push({from:g.from,to:e.from});(0<l||!c.inclusiveRight&&!l)&&h.push({from:e.to,to:g.to});a.splice.apply(a,h);f+=h.length-3}}}return a}function Xd(a){var b=a.markedSpans;if(b){for(var c=0;c<b.length;++c)b[c].marker.detachLine(a);
+a.markedSpans=null}}function Yd(a,b){if(b){for(var c=0;c<b.length;++c)b[c].marker.attachLine(a);a.markedSpans=b}}function Zd(a,b){var c=a.lines.length-b.lines.length;if(0!=c)return c;var c=a.find(),d=b.find(),e=x(c.from,d.from)||(a.inclusiveLeft?-1:0)-(b.inclusiveLeft?-1:0);return e?-e:x(c.to,d.to)||(a.inclusiveRight?1:0)-(b.inclusiveRight?1:0)||b.id-a.id}function Ia(a,b){var c,d=xa&&a.markedSpans;if(d)for(var e=void 0,f=0;f<d.length;++f)e=d[f],e.marker.collapsed&&null==(b?e.from:e.to)&&(!c||0>Zd(c,
+e.marker))&&(c=e.marker);return c}function $d(a,b,c,d,e){a=w(a,b);if(a=xa&&a.markedSpans)for(b=0;b<a.length;++b){var f=a[b];if(f.marker.collapsed){var g=f.marker.find(0),h=x(g.from,c)||(f.marker.inclusiveLeft?-1:0)-(e.inclusiveLeft?-1:0),k=x(g.to,d)||(f.marker.inclusiveRight?1:0)-(e.inclusiveRight?1:0);if(!(0<=h&&0>=k||0>=h&&0<=k)&&(0>=h&&(f.marker.inclusiveRight&&e.inclusiveLeft?0<=x(g.to,c):0<x(g.to,c))||0<=h&&(f.marker.inclusiveRight&&e.inclusiveLeft?0>=x(g.from,d):0>x(g.from,d))))return!0}}}function ma(a){for(var b;b=
+Ia(a,!0);)a=b.find(-1,!0).line;return a}function Uc(a,b){var c=w(a,b),d=ma(c);return c==d?b:D(d)}function ae(a,b){if(b>a.lastLine())return b;var c,d=w(a,b);if(!Ja(a,d))return b;for(;c=Ia(d,!1);)d=c.find(1,!0).line;return D(d)+1}function Ja(a,b){var c=xa&&b.markedSpans;if(c)for(var d=void 0,e=0;e<c.length;++e)if(d=c[e],d.marker.collapsed)if(null==d.from||!d.marker.widgetNode&&0==d.from&&d.marker.inclusiveLeft&&Vc(a,b,d))return!0}function Vc(a,b,c){if(null==c.to)return b=c.marker.find(1,!0),Vc(a,b.line,
+tb(b.line.markedSpans,c.marker));if(c.marker.inclusiveRight&&c.to==b.text.length)return!0;for(var d=void 0,e=0;e<b.markedSpans.length;++e)if(d=b.markedSpans[e],d.marker.collapsed&&!d.marker.widgetNode&&d.from==c.to&&(null==d.to||d.to!=c.from)&&(d.marker.inclusiveLeft||c.marker.inclusiveRight)&&Vc(a,b,d))return!0}function na(a){a=ma(a);for(var b=0,c=a.parent,d=0;d<c.lines.length;++d){var e=c.lines[d];if(e==a)break;b+=e.height}for(a=c.parent;a;c=a,a=c.parent)for(d=0;d<a.children.length;++d){e=a.children[d];
+if(e==c)break;b+=e.height}return b}function kc(a){if(0==a.height)return 0;for(var b,c=a.text.length,d=a;b=Ia(d,!0);)b=b.find(0,!0),d=b.from.line,c+=b.from.ch-b.to.ch;for(d=a;b=Ia(d,!1);)a=b.find(0,!0),c-=d.text.length-a.from.ch,d=a.to.line,c+=d.text.length-a.to.ch;return c}function Wc(a){var b=a.display;a=a.doc;b.maxLine=w(a,a.first);b.maxLineLength=kc(b.maxLine);b.maxLineChanged=!0;a.iter(function(a){var d=kc(a);d>b.maxLineLength&&(b.maxLineLength=d,b.maxLine=a)})}function gg(a,b,c,d){if(!a)return d(b,
+c,"ltr");for(var e=!1,f=0;f<a.length;++f){var g=a[f];(g.from<c&&g.to>b||b==c&&g.to==b)&&(d(Math.max(g.from,b),Math.min(g.to,c),1==g.level?"rtl":"ltr"),e=!0)}e||d(b,c,"ltr")}function Xc(a,b,c){var d;ub=null;for(var e=0;e<a.length;++e){var f=a[e];if(f.from<b&&f.to>b)return e;f.to==b&&(f.from!=f.to&&"before"==c?d=e:ub=e);f.from==b&&(f.from!=f.to&&"before"!=c?d=e:ub=e)}return null!=d?d:ub}function ya(a,b){var c=a.order;return null==c&&(c=a.order=hg(a.text,b)),c}function Yc(a,b,c){b=Td(a.text,b+c,c);return 0>
+b||b>a.text.length?null:b}function Zc(a,b,c){a=Yc(a,b.ch,c);return null==a?null:new m(b.line,a,0>c?"after":"before")}function $c(a,b,c,d,e){if(a&&(a=ya(c,b.doc.direction))){var f=0>e?z(a):a[0],g=0>e==(1==f.level)?"after":"before";if(0<f.level){var h=$a(b,c);a=0>e?c.text.length-1:0;var k=ra(b,h,a).top;a=gc(function(a){return ra(b,h,a).top==k},0>e==(1==f.level)?f.from:f.to-1,a);"before"==g&&(a=Yc(c,a,1))}else a=0>e?f.to:f.from;return new m(d,a,g)}return new m(d,0>e?c.text.length:0,0>e?"before":"after")}
+function be(a,b,c,d){var e=ya(b,a.doc.direction);if(!e)return Zc(b,c,d);c.ch>=b.text.length?(c.ch=b.text.length,c.sticky="before"):0>=c.ch&&(c.ch=0,c.sticky="after");var f=Xc(e,c.ch,c.sticky),g=e[f];if("ltr"==a.doc.direction&&0==g.level%2&&(0<d?g.to>c.ch:g.from<c.ch))return Zc(b,c,d);var h,k=function(a,d){return Yc(b,a instanceof m?a.ch:a,d)},l=function(d){return a.options.lineWrapping?(h=h||$a(a,b),ce(a,b,h,Ka(a,b,ra(a,h,d),"line").top)):{begin:0,end:b.text.length}},n=l("before"==c.sticky?k(c,-1):
+c.ch);if("rtl"==a.doc.direction||1==g.level){var q=1==g.level==0>d,p=k(c,q?1:-1);if(null!=p&&(q?p<=g.to&&p<=n.end:p>=g.from&&p>=n.begin))return new m(c.line,p,q?"before":"after")}g=function(a,b,d){for(;0<=a&&a<e.length;a+=b){var f=e[a],g=0<b==(1!=f.level),h=g?d.begin:k(d.end,-1);if(f.from<=h&&h<f.to||(h=g?f.from:k(f.to,-1),d.begin<=h&&h<d.end))return a=h,g?new m(c.line,k(a,1),"before"):new m(c.line,a,"after")}};if(f=g(f+d,d,n))return f;n=0<d?n.end:k(n.begin,-1);return null==n||0<d&&n==b.text.length||
+!(f=g(0<d?0:e.length-1,d,l(n)))?null:f}function aa(a,b,c){if(a.removeEventListener)a.removeEventListener(b,c,!1);else if(a.detachEvent)a.detachEvent("on"+b,c);else{var d=(a=a._handlers)&&a[b];d&&(c=N(d,c),-1<c&&(a[b]=d.slice(0,c).concat(d.slice(c+1))))}}function F(a,b){var c=a._handlers&&a._handlers[b]||lc;if(c.length)for(var d=Array.prototype.slice.call(arguments,2),e=0;e<c.length;++e)c[e].apply(null,d)}function K(a,b,c){return"string"==typeof b&&(b={type:b,preventDefault:function(){this.defaultPrevented=
+!0}}),F(a,c||b.type,a,b),ad(b)||b.codemirrorIgnore}function de(a){var b=a._handlers&&a._handlers.cursorActivity;if(b){a=a.curOp.cursorActivityHandlers||(a.curOp.cursorActivityHandlers=[]);for(var c=0;c<b.length;++c)-1==N(a,b[c])&&a.push(b[c])}}function fa(a,b){return 0<(a._handlers&&a._handlers[b]||lc).length}function ab(a){a.prototype.on=function(a,c){t(this,a,c)};a.prototype.off=function(a,c){aa(this,a,c)}}function S(a){a.preventDefault?a.preventDefault():a.returnValue=!1}function ee(a){a.stopPropagation?
+a.stopPropagation():a.cancelBubble=!0}function ad(a){return null!=a.defaultPrevented?a.defaultPrevented:0==a.returnValue}function vb(a){S(a);ee(a)}function fe(a){var b=a.which;return null==b&&(1&a.button?b=1:2&a.button?b=3:4&a.button&&(b=2)),ha&&a.ctrlKey&&1==b&&(b=3),b}function ig(a){if(null==bd){var b=r("span","​");Z(a,r("span",[b,document.createTextNode("x")]));0!=a.firstChild.offsetHeight&&(bd=1>=b.offsetWidth&&2<b.offsetHeight&&!(C&&8>B))}a=bd?r("span","​"):r("span"," ",null,"display: inline-block; width: 1px; margin-right: -1px");
+return a.setAttribute("cm-text",""),a}function jg(a,b){2<arguments.length&&(b.dependencies=Array.prototype.slice.call(arguments,2));cd[a]=b}function mc(a){if("string"==typeof a&&bb.hasOwnProperty(a))a=bb[a];else if(a&&"string"==typeof a.name&&bb.hasOwnProperty(a.name)){var b=bb[a.name];"string"==typeof b&&(b={name:b});a=Rd(b,a);a.name=b.name}else{if("string"==typeof a&&/^[\w\-]+\/[\w\-]+\+xml$/.test(a))return mc("application/xml");if("string"==typeof a&&/^[\w\-]+\/[\w\-]+\+json$/.test(a))return mc("application/json")}return"string"==
+typeof a?{name:a}:a||{name:"null"}}function dd(a,b){b=mc(b);var c=cd[b.name];if(!c)return dd(a,"text/plain");c=c(a,b);if(cb.hasOwnProperty(b.name)){var d=cb[b.name],e;for(e in d)d.hasOwnProperty(e)&&(c.hasOwnProperty(e)&&(c["_"+e]=c[e]),c[e]=d[e])}if(c.name=b.name,b.helperType&&(c.helperType=b.helperType),b.modeProps)for(var f in b.modeProps)c[f]=b.modeProps[f];return c}function kg(a,b){Fa(b,cb.hasOwnProperty(a)?cb[a]:cb[a]={})}function La(a,b){if(!0===b)return b;if(a.copyState)return a.copyState(b);
+var c={},d;for(d in b){var e=b[d];e instanceof Array&&(e=e.concat([]));c[d]=e}return c}function ed(a,b){for(var c;a.innerMode&&(c=a.innerMode(b))&&c.mode!=a;)b=c.state,a=c.mode;return c||{mode:a,state:b}}function ge(a,b,c){return!a.startState||a.startState(b,c)}function he(a,b,c,d){var e=[a.state.modeGen],f={};ie(a,b.text,a.doc.mode,c,function(a,b){return e.push(a,b)},f,d);d=c.state;for(var g=0;g<a.state.overlays.length;++g)!function(d){var g=a.state.overlays[d],l=1,n=0;c.state=!0;ie(a,b.text,g.mode,
+c,function(a,b){for(var d=l;n<a;){var c=e[l];c>a&&e.splice(l,1,a,e[l+1],c);l+=2;n=Math.min(a,c)}if(b)if(g.opaque)e.splice(d,l-d,a,"overlay "+b),l=d+2;else for(;d<l;d+=2)c=e[d+1],e[d+1]=(c?c+" ":"")+"overlay "+b},f)}(g);return c.state=d,{styles:e,classes:f.bgClass||f.textClass?f:null}}function je(a,b,c){if(!b.styles||b.styles[0]!=a.state.modeGen){var d=wb(a,D(b)),e=b.text.length>a.options.maxHighlightLength&&La(a.doc.mode,d.state),f=he(a,b,d);e&&(d.state=e);b.stateAfter=d.save(!e);b.styles=f.styles;
+f.classes?b.styleClasses=f.classes:b.styleClasses&&(b.styleClasses=null);c===a.doc.highlightFrontier&&(a.doc.modeFrontier=Math.max(a.doc.modeFrontier,++a.doc.highlightFrontier))}return b.styles}function wb(a,b,c){var d=a.doc,e=a.display;if(!d.mode.startState)return new sa(d,!0,b);var f=lg(a,b,c),g=f>d.first&&w(d,f-1).stateAfter,h=g?sa.fromSaved(d,g,f):new sa(d,ge(d.mode),f);return d.iter(f,b,function(d){fd(a,d.text,h);var c=h.line;d.stateAfter=c==b-1||0==c%5||c>=e.viewFrom&&c<e.viewTo?h.save():null;
+h.nextLine()}),c&&(d.modeFrontier=h.line),h}function fd(a,b,c,d){var e=a.doc.mode;a=new H(b,a.options.tabSize,c);a.start=a.pos=d||0;for(""==b&&ke(e,c.state);!a.eol();)gd(e,a,c.state),a.start=a.pos}function ke(a,b){if(a.blankLine)return a.blankLine(b);if(a.innerMode){var c=ed(a,b);return c.mode.blankLine?c.mode.blankLine(c.state):void 0}}function gd(a,b,c,d){for(var e=0;10>e;e++){d&&(d[0]=ed(a,c).mode);var f=a.token(b,c);if(b.pos>b.start)return f}throw Error("Mode "+a.name+" failed to advance stream.");
+}function le(a,b,c,d){var e,f=a.doc,g=f.mode;b=v(f,b);var h,k=w(f,b.line);c=wb(a,b.line,c);a=new H(k.text,a.options.tabSize,c);for(d&&(h=[]);(d||a.pos<b.ch)&&!a.eol();)a.start=a.pos,e=gd(g,a,c.state),d&&h.push(new me(a,e,La(f.mode,c.state)));return d?h:new me(a,e,c.state)}function ne(a,b){if(a)for(;;){var c=a.match(/(?:^|\s+)line-(background-)?(\S+)/);if(!c)break;a=a.slice(0,c.index)+a.slice(c.index+c[0].length);var d=c[1]?"bgClass":"textClass";null==b[d]?b[d]=c[2]:(new RegExp("(?:^|s)"+c[2]+"(?:$|s)")).test(b[d])||
+(b[d]+=" "+c[2])}return a}function ie(a,b,c,d,e,f,g){var h=c.flattenSpans;null==h&&(h=a.options.flattenSpans);var k,l=0,n=null,q=new H(b,a.options.tabSize,d),p=a.options.addModeClass&&[null];for(""==b&&ne(ke(c,d.state),f);!q.eol();){if(q.pos>a.options.maxHighlightLength?(h=!1,g&&fd(a,b,d,q.pos),q.pos=b.length,k=null):k=ne(gd(c,q,d.state,p),f),p){var u=p[0].name;u&&(k="m-"+(k?u+" "+k:u))}if(!h||n!=k){for(;l<q.start;)l=Math.min(q.start,l+5E3),e(l,n);n=k}q.start=q.pos}for(;l<q.pos;)a=Math.min(q.pos,
+l+5E3),e(a,n),l=a}function lg(a,b,c){for(var d,e,f=a.doc,g=c?-1:b-(a.doc.mode.innerMode?1E3:100);b>g;--b){if(b<=f.first)return f.first;var h=w(f,b-1),k=h.stateAfter;if(k&&(!c||b+(k instanceof nc?k.lookAhead:0)<=f.modeFrontier))return b;h=ea(h.text,null,a.options.tabSize);(null==e||d>h)&&(e=b-1,d=h)}return e}function mg(a,b){if(a.modeFrontier=Math.min(a.modeFrontier,b),!(a.highlightFrontier<b-10)){for(var c=a.first,d=b-1;d>c;d--){var e=w(a,d).stateAfter;if(e&&(!(e instanceof nc)||d+e.lookAhead<b)){c=
+d+1;break}}a.highlightFrontier=Math.min(a.highlightFrontier,c)}}function oe(a,b){if(!a||/^\s*$/.test(a))return null;var c=b.addModeClass?ng:og;return c[a]||(c[a]=a.replace(/\S+/g,"cm-$\x26"))}function pe(a,b){var c=Za("span",null,null,R?"padding-right: .1px":null),c={pre:Za("pre",[c],"CodeMirror-line"),content:c,col:0,pos:0,cm:a,trailingSpace:!1,splitSpaces:(C||R)&&a.getOption("lineWrapping")};b.measure={};for(var d=0;d<=(b.rest?b.rest.length:0);d++){var e=d?b.rest[d-1]:b.line,f=void 0;c.pos=0;c.addToken=
+pg;var g;g=a.display.measure;if(null!=hd)g=hd;else{var h=Z(g,document.createTextNode("AخA")),k=db(h,0,1).getBoundingClientRect(),h=db(h,1,2).getBoundingClientRect();g=(X(g),!(!k||k.left==k.right)&&(hd=3>h.right-k.right))}g&&(f=ya(e,a.doc.direction))&&(c.addToken=qg(c.addToken,f));c.map=[];a:{f=c;g=je(a,e,b!=a.display.externalMeasured&&D(e));var l=e.markedSpans,k=e.text,h=0;if(l)for(var n=void 0,q=void 0,p=void 0,u=void 0,O=void 0,m=void 0,J=void 0,w=k.length,G=0,r=1,t="",x=0;;){if(x==G){for(var p=
+u=O=m=q="",J=null,x=1/0,v=[],z=void 0,A=0;A<l.length;++A){var y=l[A],B=y.marker;"bookmark"==B.type&&y.from==G&&B.widgetNode?v.push(B):y.from<=G&&(null==y.to||y.to>G||B.collapsed&&y.to==G&&y.from==G)?(null!=y.to&&y.to!=G&&x>y.to&&(x=y.to,u=""),B.className&&(p+=" "+B.className),B.css&&(q=(q?q+";":"")+B.css),B.startStyle&&y.from==G&&(O+=" "+B.startStyle),B.endStyle&&y.to==x&&(z||(z=[])).push(B.endStyle,y.to),B.title&&!m&&(m=B.title),B.collapsed&&(!J||0>Zd(J.marker,B))&&(J=y)):y.from>G&&x>y.from&&(x=
+y.from)}if(z)for(A=0;A<z.length;A+=2)z[A+1]==x&&(u+=" "+z[A]);if(!J||J.from==G)for(z=0;z<v.length;++z)qe(f,0,v[z]);if(J&&(J.from||0)==G){if(qe(f,(null==J.to?w+1:J.to)-G,J.marker,null==J.from),null==J.to)break a;J.to==G&&(J=!1)}}if(G>=w)break;for(v=Math.min(w,x);;){if(t){z=G+t.length;J||(A=z>v?t.slice(0,v-G):t,f.addToken(f,A,n?n+p:p,O,G+A.length==x?u:"",m,q));if(z>=v){t=t.slice(v-G);G=v;break}G=z;O=""}t=k.slice(h,h=g[r++]);n=oe(g[r++],f.cm.options)}}else for(l=1;l<g.length;l+=2)f.addToken(f,k.slice(h,
+h=g[l]),oe(g[l+1],f.cm.options))}e.styleClasses&&(e.styleClasses.bgClass&&(c.bgClass=Jc(e.styleClasses.bgClass,c.bgClass||"")),e.styleClasses.textClass&&(c.textClass=Jc(e.styleClasses.textClass,c.textClass||"")));0==c.map.length&&c.map.push(0,0,c.content.appendChild(ig(a.display.measure)));0==d?(b.measure.map=c.map,b.measure.cache={}):((b.measure.maps||(b.measure.maps=[])).push(c.map),(b.measure.caches||(b.measure.caches=[])).push({}))}R&&(d=c.content.lastChild,(/\bcm-tab\b/.test(d.className)||d.querySelector&&
+d.querySelector(".cm-tab"))&&(c.content.className="cm-tab-wrap-hack"));return F(a,"renderLine",a,b.line,c.pre),c.pre.className&&(c.textClass=Jc(c.pre.className,c.textClass||"")),c}function rg(a){var b=r("span","•","cm-invalidchar");return b.title="\\u"+a.charCodeAt(0).toString(16),b.setAttribute("aria-label",b.title),b}function pg(a,b,c,d,e,f,g){if(b){var h;if(a.splitSpaces)if(h=a.trailingSpace,1<b.length&&!/  /.test(b))h=b;else{for(var k="",l=0;l<b.length;l++){var n=b.charAt(l);" "!=n||!h||l!=b.length-
+1&&32!=b.charCodeAt(l+1)||(n=" ");k+=n;h=" "==n}h=k}else h=b;k=h;l=a.cm.state.specialChars;n=!1;if(l.test(b)){h=document.createDocumentFragment();for(var q=0;;){l.lastIndex=q;var p=l.exec(b),u=p?p.index-q:b.length-q;if(u){var O=document.createTextNode(k.slice(q,q+u));C&&9>B?h.appendChild(r("span",[O])):h.appendChild(O);a.map.push(a.pos,a.pos+u,O);a.col+=u;a.pos+=u}if(!p)break;q+=u+1;u=void 0;"\t"==p[0]?(p=a.cm.options.tabSize,p-=a.col%p,u=h.appendChild(r("span",Mc(p),"cm-tab")),u.setAttribute("role",
+"presentation"),u.setAttribute("cm-text","\t"),a.col+=p):"\r"==p[0]||"\n"==p[0]?(u=h.appendChild(r("span","\r"==p[0]?"␍":"␤","cm-invalidchar")),u.setAttribute("cm-text",p[0]),a.col+=1):(u=a.cm.options.specialCharPlaceholder(p[0]),u.setAttribute("cm-text",p[0]),C&&9>B?h.appendChild(r("span",[u])):h.appendChild(u),a.col+=1);a.map.push(a.pos,a.pos+1,u);a.pos++}}else a.col+=b.length,h=document.createTextNode(k),a.map.push(a.pos,a.pos+b.length,h),C&&9>B&&(n=!0),a.pos+=b.length;if(a.trailingSpace=32==k.charCodeAt(b.length-
+1),c||d||e||n||g)return b=c||"",d&&(b+=d),e&&(b+=e),d=r("span",[h],b,g),f&&(d.title=f),a.content.appendChild(d);a.content.appendChild(h)}}function qg(a,b){return function(c,d,e,f,g,h,k){e=e?e+" cm-force-border":"cm-force-border";for(var l=c.pos,n=l+d.length;;){for(var q=void 0,p=0;p<b.length&&(q=b[p],!(q.to>l&&q.from<=l));p++);if(q.to>=n)return a(c,d,e,f,g,h,k);a(c,d.slice(0,q.to-l),e,f,null,h,k);f=null;d=d.slice(q.to-l);l=q.to}}}function qe(a,b,c,d){var e=!d&&c.widgetNode;e&&a.map.push(a.pos,a.pos+
+b,e);!d&&a.cm.display.input.needsContentAttribute&&(e||(e=a.content.appendChild(document.createElement("span"))),e.setAttribute("cm-marker",c.id));e&&(a.cm.display.input.setUneditable(e),a.content.appendChild(e));a.pos+=b;a.trailingSpace=!1}function re(a,b,c){for(var d=this.line=b,e;d=Ia(d,!1);)d=d.find(1,!0).line,(e||(e=[])).push(d);this.size=(this.rest=e)?D(z(this.rest))-c+1:1;this.node=this.text=null;this.hidden=Ja(a,b)}function oc(a,b,c){var d,e=[];for(d=b;d<c;)b=new re(a.doc,w(a.doc,d),d),d+=
+b.size,e.push(b);return e}function sg(a,b){var c=a.ownsGroup;if(c)try{var d=c.delayedCallbacks,e=0;do{for(;e<d.length;e++)d[e].call(null);for(var f=0;f<c.ops.length;f++){var g=c.ops[f];if(g.cursorActivityHandlers)for(;g.cursorActivityCalled<g.cursorActivityHandlers.length;)g.cursorActivityHandlers[g.cursorActivityCalled++].call(null,g.cm)}}while(e<d.length)}finally{eb=null,b(c)}}function P(a,b){var c=a._handlers&&a._handlers[b]||lc;if(c.length){var d,e=Array.prototype.slice.call(arguments,2);eb?d=
+eb.delayedCallbacks:xb?d=xb:(d=xb=[],setTimeout(tg,0));for(var f=0;f<c.length;++f)!function(a){d.push(function(){return c[a].apply(null,e)})}(f)}}function tg(){var a=xb;xb=null;for(var b=0;b<a.length;++b)a[b]()}function se(a,b,c,d){for(var e=0;e<b.changes.length;e++){var f=b.changes[e];if("text"==f){var f=a,g=b,h=g.text.className,k=te(f,g);g.text==g.node&&(g.node=k.pre);g.text.parentNode.replaceChild(k.pre,g.text);g.text=k.pre;k.bgClass!=g.bgClass||k.textClass!=g.textClass?(g.bgClass=k.bgClass,g.textClass=
+k.textClass,id(f,g)):h&&(g.text.className=h)}else if("gutter"==f)ue(a,b,c,d);else if("class"==f)id(a,b);else if("widget"==f){f=a;g=b;h=d;g.alignable&&(g.alignable=null);for(var k=g.node.firstChild,l=void 0;k;k=l)l=k.nextSibling,"CodeMirror-linewidget"==k.className&&g.node.removeChild(k);ve(f,g,h)}}b.changes=null}function yb(a){return a.node==a.text&&(a.node=r("div",null,null,"position: relative"),a.text.parentNode&&a.text.parentNode.replaceChild(a.node,a.text),a.node.appendChild(a.text),C&&8>B&&(a.node.style.zIndex=
+2)),a.node}function te(a,b){var c=a.display.externalMeasured;return c&&c.line==b.line?(a.display.externalMeasured=null,b.measure=c.measure,c.built):pe(a,b)}function id(a,b){var c=b.bgClass?b.bgClass+" "+(b.line.bgClass||""):b.line.bgClass;if(c&&(c+=" CodeMirror-linebackground"),b.background)c?b.background.className=c:(b.background.parentNode.removeChild(b.background),b.background=null);else if(c){var d=yb(b);b.background=d.insertBefore(r("div",null,c),d.firstChild);a.display.input.setUneditable(b.background)}b.line.wrapClass?
+yb(b).className=b.line.wrapClass:b.node!=b.text&&(b.node.className="");b.text.className=(b.textClass?b.textClass+" "+(b.line.textClass||""):b.line.textClass)||""}function ue(a,b,c,d){if(b.gutter&&(b.node.removeChild(b.gutter),b.gutter=null),b.gutterBackground&&(b.node.removeChild(b.gutterBackground),b.gutterBackground=null),b.line.gutterClass){var e=yb(b);b.gutterBackground=r("div",null,"CodeMirror-gutter-background "+b.line.gutterClass,"left: "+(a.options.fixedGutter?d.fixedPos:-d.gutterTotalWidth)+
+"px; width: "+d.gutterTotalWidth+"px");a.display.input.setUneditable(b.gutterBackground);e.insertBefore(b.gutterBackground,b.text)}e=b.line.gutterMarkers;if(a.options.lineNumbers||e){var f=yb(b),g=b.gutter=r("div",null,"CodeMirror-gutter-wrapper","left: "+(a.options.fixedGutter?d.fixedPos:-d.gutterTotalWidth)+"px");if(a.display.input.setUneditable(g),f.insertBefore(g,b.text),b.line.gutterClass&&(g.className+=" "+b.line.gutterClass),!a.options.lineNumbers||e&&e["CodeMirror-linenumbers"]||(b.lineNumber=
+g.appendChild(r("div",Qc(a.options,c),"CodeMirror-linenumber CodeMirror-gutter-elt","left: "+d.gutterLeft["CodeMirror-linenumbers"]+"px; width: "+a.display.lineNumInnerWidth+"px"))),e)for(b=0;b<a.options.gutters.length;++b)c=a.options.gutters[b],(f=e.hasOwnProperty(c)&&e[c])&&g.appendChild(r("div",[f],"CodeMirror-gutter-elt","left: "+d.gutterLeft[c]+"px; width: "+d.gutterWidth[c]+"px"))}}function ug(a,b,c,d){var e=te(a,b);return b.text=b.node=e.pre,e.bgClass&&(b.bgClass=e.bgClass),e.textClass&&(b.textClass=
+e.textClass),id(a,b),ue(a,b,c,d),ve(a,b,d),b.node}function ve(a,b,c){if(we(a,b.line,b,c,!0),b.rest)for(var d=0;d<b.rest.length;d++)we(a,b.rest[d],b,c,!1)}function we(a,b,c,d,e){if(b.widgets){var f=yb(c),g=0;for(b=b.widgets;g<b.length;++g){var h=b[g],k=r("div",[h.node],"CodeMirror-linewidget");h.handleMouseEvents||k.setAttribute("cm-ignore-events","true");var l=h,n=k,q=d;if(l.noHScroll){(c.alignable||(c.alignable=[])).push(n);var p=q.wrapperWidth;n.style.left=q.fixedPos+"px";l.coverGutter||(p-=q.gutterTotalWidth,
+n.style.paddingLeft=q.gutterTotalWidth+"px");n.style.width=p+"px"}l.coverGutter&&(n.style.zIndex=5,n.style.position="relative",l.noHScroll||(n.style.marginLeft=-q.gutterTotalWidth+"px"));a.display.input.setUneditable(k);e&&h.above?f.insertBefore(k,c.gutter||c.text):f.appendChild(k);P(h,"redraw")}}}function zb(a){if(null!=a.height)return a.height;var b=a.doc.cm;if(!b)return 0;if(!va(document.body,a.node)){var c="position: relative;";a.coverGutter&&(c+="margin-left: -"+b.display.gutters.offsetWidth+
+"px;");a.noHScroll&&(c+="width: "+b.display.wrapper.clientWidth+"px;");Z(b.display.measure,r("div",[a.node],null,c))}return a.height=a.node.parentNode.offsetHeight}function ta(a,b){for(var c=b.target||b.srcElement;c!=a.wrapper;c=c.parentNode)if(!c||1==c.nodeType&&"true"==c.getAttribute("cm-ignore-events")||c.parentNode==a.sizer&&c!=a.mover)return!0}function jd(a){return a.mover.offsetHeight-a.lineSpace.offsetHeight}function xe(a){if(a.cachedPaddingH)return a.cachedPaddingH;var b=Z(a.measure,r("pre",
+"x")),b=window.getComputedStyle?window.getComputedStyle(b):b.currentStyle,b={left:parseInt(b.paddingLeft),right:parseInt(b.paddingRight)};return isNaN(b.left)||isNaN(b.right)||(a.cachedPaddingH=b),b}function oa(a){return Ud-a.display.nativeBarWidth}function Ma(a){return a.display.scroller.clientWidth-oa(a)-a.display.barWidth}function kd(a){return a.display.scroller.clientHeight-oa(a)-a.display.barHeight}function ye(a,b,c){if(a.line==b)return{map:a.measure.map,cache:a.measure.cache};for(var d=0;d<
+a.rest.length;d++)if(a.rest[d]==b)return{map:a.measure.maps[d],cache:a.measure.caches[d]};for(b=0;b<a.rest.length;b++)if(D(a.rest[b])>c)return{map:a.measure.maps[b],cache:a.measure.caches[b],before:!0}}function ld(a,b){if(b>=a.display.viewFrom&&b<a.display.viewTo)return a.display.view[Na(a,b)];var c=a.display.externalMeasured;return c&&b>=c.lineN&&b<c.lineN+c.size?c:void 0}function $a(a,b){var c=D(b),d=ld(a,c);d&&!d.text?d=null:d&&d.changes&&(se(a,d,c,md(a)),a.curOp.forceUpdate=!0);if(!d){var e;e=
+ma(b);d=D(e);e=a.display.externalMeasured=new re(a.doc,e,d);e.lineN=d;d=e.built=pe(a,e);d=(e.text=d.pre,Z(a.display.lineMeasure,d.pre),e)}c=ye(d,b,c);return{line:b,view:d,rect:null,map:c.map,cache:c.cache,before:c.before,hasHeights:!1}}function ra(a,b,c,d,e){b.before&&(c=-1);var f=c+(d||"");if(b.cache.hasOwnProperty(f))a=b.cache[f];else{b.rect||(b.rect=b.view.text.getBoundingClientRect());if(!b.hasHeights){var g=b.view,h=b.rect,k=a.options.lineWrapping,l=k&&Ma(a);if(!g.measure.heights||k&&g.measure.width!=
+l){var n=g.measure.heights=[];if(k)for(g.measure.width=l,g=g.text.firstChild.getClientRects(),k=0;k<g.length-1;k++){var l=g[k],q=g[k+1];2<Math.abs(l.bottom-q.bottom)&&n.push((l.bottom+q.top)/2-h.top)}n.push(h.bottom-h.top)}b.hasHeights=!0}var n=d,p,g=ze(b.map,c,n);d=g.node;h=g.start;k=g.end;c=g.collapse;if(3==d.nodeType){for(var u=0;4>u;u++){for(;h&&Oc(b.line.text.charAt(g.coverStart+h));)--h;for(;g.coverStart+k<g.coverEnd&&Oc(b.line.text.charAt(g.coverStart+k));)++k;if(C&&9>B&&0==h&&k==g.coverEnd-
+g.coverStart)k=d.parentNode.getBoundingClientRect();else{k=db(d,h,k).getClientRects();l=Ae;if("left"==n)for(q=0;q<k.length&&(l=k[q]).left==l.right;q++);else for(q=k.length-1;0<=q&&(l=k[q]).left==l.right;q--);k=l}if(p=k,p.left||p.right||0==h)break;k=h;--h;c="right"}C&&11>B&&((u=!window.screen||null==screen.logicalXDPI||screen.logicalXDPI==screen.deviceXDPI)||(null!=nd?u=nd:(n=Z(a.display.measure,r("span","x")),u=n.getBoundingClientRect(),n=db(n,0,1).getBoundingClientRect(),u=nd=1<Math.abs(u.left-n.left)),
+u=!u),u||(u=screen.logicalXDPI/screen.deviceXDPI,n=screen.logicalYDPI/screen.deviceYDPI,p={left:p.left*u,right:p.right*u,top:p.top*n,bottom:p.bottom*n}))}else 0<h&&(c=n="right"),p=a.options.lineWrapping&&1<(u=d.getClientRects()).length?u["right"==n?u.length-1:0]:d.getBoundingClientRect();!(C&&9>B)||h||p&&(p.left||p.right)||(p=(p=d.parentNode.getClientRects()[0])?{left:p.left,right:p.left+Ab(a.display),top:p.top,bottom:p.bottom}:Ae);d=p.top-b.rect.top;h=p.bottom-b.rect.top;u=(d+h)/2;n=b.view.measure.heights;
+for(g=0;g<n.length-1&&!(u<n[g]);g++);c={left:("right"==c?p.right:p.left)-b.rect.left,right:("left"==c?p.left:p.right)-b.rect.left,top:g?n[g-1]:0,bottom:n[g]};a=(p.left||p.right||(c.bogus=!0),a.options.singleCursorHeightPerLine||(c.rtop=d,c.rbottom=h),c);a.bogus||(b.cache[f]=a)}return{left:a.left,right:a.right,top:e?a.rtop:a.top,bottom:e?a.rbottom:a.bottom}}function ze(a,b,c){for(var d,e,f,g,h,k,l=0;l<a.length;l+=3)if(h=a[l],k=a[l+1],b<h?(e=0,f=1,g="left"):b<k?(e=b-h,f=e+1):(l==a.length-3||b==k&&a[l+
+3]>b)&&(f=k-h,e=f-1,b>=k&&(g="right")),null!=e){if(d=a[l+2],h==k&&c==(d.insertLeft?"left":"right")&&(g=c),"left"==c&&0==e)for(;l&&a[l-2]==a[l-3]&&a[l-1].insertLeft;)d=a[2+(l-=3)],g="left";if("right"==c&&e==k-h)for(;l<a.length-3&&a[l+3]==a[l+4]&&!a[l+5].insertLeft;)d=a[(l+=3)+2],g="right";break}return{node:d,start:e,end:f,collapse:g,coverStart:h,coverEnd:k}}function Be(a){if(a.measure&&(a.measure.cache={},a.measure.heights=null,a.rest))for(var b=0;b<a.rest.length;b++)a.measure.caches[b]={}}function Ce(a){a.display.externalMeasure=
+null;X(a.display.lineMeasure);for(var b=0;b<a.display.view.length;b++)Be(a.display.view[b])}function Bb(a){Ce(a);a.display.cachedCharWidth=a.display.cachedTextHeight=a.display.cachedPaddingH=null;a.options.lineWrapping||(a.display.maxLineChanged=!0);a.display.lineNumChars=null}function De(){return pc&&qc?-(document.body.getBoundingClientRect().left-parseInt(getComputedStyle(document.body).marginLeft)):window.pageXOffset||(document.documentElement||document.body).scrollLeft}function Ee(){return pc&&
+qc?-(document.body.getBoundingClientRect().top-parseInt(getComputedStyle(document.body).marginTop)):window.pageYOffset||(document.documentElement||document.body).scrollTop}function Ka(a,b,c,d,e){if(!e&&b.widgets)for(e=0;e<b.widgets.length;++e)if(b.widgets[e].above){var f=zb(b.widgets[e]);c.top+=f;c.bottom+=f}if("line"==d)return c;d||(d="local");b=na(b);if("local"==d?b+=a.display.lineSpace.offsetTop:b-=a.display.viewOffset,"page"==d||"window"==d)a=a.display.lineSpace.getBoundingClientRect(),b+=a.top+
+("window"==d?0:Ee()),d=a.left+("window"==d?0:De()),c.left+=d,c.right+=d;return c.top+=b,c.bottom+=b,c}function Fe(a,b,c){if("div"==c)return b;var d=b.left;b=b.top;"page"==c?(d-=De(),b-=Ee()):"local"!=c&&c||(c=a.display.sizer.getBoundingClientRect(),d+=c.left,b+=c.top);a=a.display.lineSpace.getBoundingClientRect();return{left:d-a.left,top:b-a.top}}function rc(a,b,c,d,e){d||(d=w(a.doc,b.line));var f=d;b=b.ch;d=ra(a,$a(a,d),b,e);return Ka(a,f,d,c)}function ia(a,b,c,d,e,f){function g(b,g){var h=ra(a,
+e,b,g?"right":"left",f);return g?h.left=h.right:h.right=h.left,Ka(a,d,h,c)}function h(a,b,d){return g(d?a-1:a,0!=k[b].level%2!=d)}d=d||w(a.doc,b.line);e||(e=$a(a,d));var k=ya(d,a.doc.direction),l=b.ch;b=b.sticky;if(l>=d.text.length?(l=d.text.length,b="before"):0>=l&&(l=0,b="after"),!k)return g("before"==b?l-1:l,"before"==b);var n=Xc(k,l,b),q=ub,n=h(l,n,"before"==b);return null!=q&&(n.other=h(l,q,"before"!=b)),n}function Ge(a,b){var c=0;b=v(a.doc,b);a.options.lineWrapping||(c=Ab(a.display)*b.ch);var d=
+w(a.doc,b.line),e=na(d)+a.display.lineSpace.offsetTop;return{left:c,right:c,top:e,bottom:e+d.height}}function od(a,b,c){var d=a.doc;if(0>(c+=a.display.viewOffset))return a=m(d.first,0,null),a.xRel=-1,a.outside=!0,a;var e=Ha(d,c),f=d.first+d.size-1;if(e>f)return a=w(d,f).text.length,a=m(d.first+d.size-1,a,null),a.xRel=1,a.outside=!0,a;0>b&&(b=0);for(f=w(d,e);;){d=vg(a,f,e,b,c);f=(e=Ia(f,!1))&&e.find(0,!0);if(!e||!(d.ch>f.from.ch||d.ch==f.from.ch&&0<d.xRel))return d;e=D(f=f.to.line)}}function ce(a,
+b,c,d){var e=b.text.length,f=gc(function(e){return Ka(a,b,ra(a,c,e-1),"line").bottom<=d},e,0);return e=gc(function(e){return Ka(a,b,ra(a,c,e),"line").top>d},f,e),{begin:f,end:e}}function vg(a,b,c,d,e){e-=na(b);var f=0,g=b.text.length,h=$a(a,b);if(ya(b,a.doc.direction)){if(a.options.lineWrapping){var k;k=ce(a,b,h,e);f=k.begin;g=k.end}c=new m(c,Math.floor(f+(g-f)/2));var l;k=ia(a,c,"line",b,h).left;var n=k<d?1:-1,q=k-d,p=Math.ceil((g-f)/4);a:do{k=q;l=c;for(var u=0;u<p;++u){var O=c;if(null==(c=be(a,
+b,c,n))||c.ch<f||g<=("before"==c.sticky?c.ch-1:c.ch)){c=O;break a}}if(q=ia(a,c,"line",b,h).left-d,1<p)n=Math.abs(q-k)/p,p=Math.min(p,Math.ceil(Math.abs(q)/n)),n=0>q?1:-1}while(0!=q&&(1<p||0>n!=0>q&&Math.abs(q)<=Math.abs(k)));if(Math.abs(q)>Math.abs(k)){if(0>q==0>k)throw Error("Broke out of infinite loop in coordsCharInner");c=l}}else f=gc(function(c){var f=Ka(a,b,ra(a,h,c),"line");return f.top>e?(g=Math.min(c,g),!0):!(f.bottom<=e)&&(f.left>d||!(f.right<d)&&d-f.left<f.right-d)},f,g),f=Td(b.text,f,
+1),c=new m(c,f,f==g?"before":"after");f=ia(a,c,"line",b,h);return(e<f.top||f.bottom<e)&&(c.outside=!0),c.xRel=d<f.left?-1:d>f.right?1:0,c}function Oa(a){if(null!=a.cachedTextHeight)return a.cachedTextHeight;if(null==Pa){Pa=r("pre");for(var b=0;49>b;++b)Pa.appendChild(document.createTextNode("x")),Pa.appendChild(r("br"));Pa.appendChild(document.createTextNode("x"))}Z(a.measure,Pa);b=Pa.offsetHeight/50;return 3<b&&(a.cachedTextHeight=b),X(a.measure),b||1}function Ab(a){if(null!=a.cachedCharWidth)return a.cachedCharWidth;
+var b=r("span","xxxxxxxxxx"),c=r("pre",[b]);Z(a.measure,c);b=b.getBoundingClientRect();b=(b.right-b.left)/10;return 2<b&&(a.cachedCharWidth=b),b||10}function md(a){for(var b=a.display,c={},d={},e=b.gutters.clientLeft,f=b.gutters.firstChild,g=0;f;f=f.nextSibling,++g)c[a.options.gutters[g]]=f.offsetLeft+f.clientLeft+e,d[a.options.gutters[g]]=f.clientWidth;return{fixedPos:pd(b),gutterTotalWidth:b.gutters.offsetWidth,gutterLeft:c,gutterWidth:d,wrapperWidth:b.wrapper.clientWidth}}function pd(a){return a.scroller.getBoundingClientRect().left-
+a.sizer.getBoundingClientRect().left}function He(a){var b=Oa(a.display),c=a.options.lineWrapping,d=c&&Math.max(5,a.display.scroller.clientWidth/Ab(a.display)-3);return function(e){if(Ja(a.doc,e))return 0;var f=0;if(e.widgets)for(var g=0;g<e.widgets.length;g++)e.widgets[g].height&&(f+=e.widgets[g].height);return c?f+(Math.ceil(e.text.length/d)||1)*b:f+b}}function qd(a){var b=a.doc,c=He(a);b.iter(function(a){var b=c(a);b!=a.height&&la(a,b)})}function Qa(a,b,c,d){var e=a.display;if(!c&&"true"==(b.target||
+b.srcElement).getAttribute("cm-not-content"))return null;var f,g;c=e.lineSpace.getBoundingClientRect();try{f=b.clientX-c.left,g=b.clientY-c.top}catch(h){return null}var k;b=od(a,f,g);d&&1==b.xRel&&(k=w(a.doc,b.line).text).length==b.ch&&(d=ea(k,k.length,a.options.tabSize)-k.length,b=m(b.line,Math.max(0,Math.round((f-xe(a.display).left)/Ab(a.display))-d)));return b}function Na(a,b){if(b>=a.display.viewTo||0>(b-=a.display.viewFrom))return null;for(var c=a.display.view,d=0;d<c.length;d++)if(0>(b-=c[d].size))return d}
+function Cb(a){a.display.input.showSelection(a.display.input.prepareSelection())}function Ie(a,b){for(var c=a.doc,d={},e=d.cursors=document.createDocumentFragment(),f=d.selection=document.createDocumentFragment(),g=0;g<c.sel.ranges.length;g++)if(!1!==b||g!=c.sel.primIndex){var h=c.sel.ranges[g];if(!(h.from().line>=a.display.viewTo||h.to().line<a.display.viewFrom)){var k=h.empty();(k||a.options.showCursorWhenSelecting)&&Je(a,h.head,e);k||wg(a,h,f)}}return d}function Je(a,b,c){b=ia(a,b,"div",null,null,
+!a.options.singleCursorHeightPerLine);var d=c.appendChild(r("div"," ","CodeMirror-cursor"));if(d.style.left=b.left+"px",d.style.top=b.top+"px",d.style.height=Math.max(0,b.bottom-b.top)*a.options.cursorHeight+"px",b.other)a=c.appendChild(r("div"," ","CodeMirror-cursor CodeMirror-secondarycursor")),a.style.display="",a.style.left=b.other.left+"px",a.style.top=b.other.top+"px",a.style.height=.85*(b.other.bottom-b.other.top)+"px"}function wg(a,b,c){function d(a,b,d,c){0>b&&(b=0);b=Math.round(b);c=Math.round(c);
+h.appendChild(r("div",null,"CodeMirror-selected","position: absolute; left: "+a+"px;\n                             top: "+b+"px; width: "+(null==d?n-a:d)+"px;\n                             height: "+(c-b)+"px"))}function e(b,c,e){var f,h,k=w(g,b),q=k.text.length;return gg(ya(k,g.direction),c||0,null==e?q:e,function(g,w,t){var r,x,v=rc(a,m(b,g),"div",k,"left");if(g==w)r=v,t=x=v.left;else{if(r=rc(a,m(b,w-1),"div",k,"right"),"rtl"==t)t=v,v=r,r=t;t=v.left;x=r.right}null==c&&0==g&&(t=l);3<r.top-v.top&&
+(d(t,v.top,null,v.bottom),t=l,v.bottom<r.top&&d(t,v.bottom,null,r.top));null==e&&w==q&&(x=n);(!f||v.top<f.top||v.top==f.top&&v.left<f.left)&&(f=v);(!h||r.bottom>h.bottom||r.bottom==h.bottom&&r.right>h.right)&&(h=r);t<l+1&&(t=l);d(t,r.top,x-t,r.bottom)}),{start:f,end:h}}var f=a.display,g=a.doc,h=document.createDocumentFragment(),k=xe(a.display),l=k.left,n=Math.max(f.sizerWidth,Ma(a)-f.sizer.offsetLeft)-k.right,f=b.from();b=b.to();if(f.line==b.line)e(f.line,f.ch,b.ch);else{var q=w(g,f.line),k=w(g,b.line),
+k=ma(q)==ma(k),f=e(f.line,f.ch,k?q.text.length+1:null).end;b=e(b.line,k?0:null,b.ch).start;k&&(f.top<b.top-2?(d(f.right,f.top,null,f.bottom),d(l,b.top,b.left,b.bottom)):d(f.right,f.top,b.left-f.right,f.bottom));f.bottom<b.top&&d(l,f.bottom,null,b.top)}c.appendChild(h)}function rd(a){if(a.state.focused){var b=a.display;clearInterval(b.blinker);var c=!0;b.cursorDiv.style.visibility="";0<a.options.cursorBlinkRate?b.blinker=setInterval(function(){return b.cursorDiv.style.visibility=(c=!c)?"":"hidden"},
+a.options.cursorBlinkRate):0>a.options.cursorBlinkRate&&(b.cursorDiv.style.visibility="hidden")}}function Ke(a){a.state.focused||(a.display.input.focus(),sd(a))}function Le(a){a.state.delayingBlurEvent=!0;setTimeout(function(){a.state.delayingBlurEvent&&(a.state.delayingBlurEvent=!1,Db(a))},100)}function sd(a,b){a.state.delayingBlurEvent&&(a.state.delayingBlurEvent=!1);"nocursor"!=a.options.readOnly&&(a.state.focused||(F(a,"focus",a,b),a.state.focused=!0,Ea(a.display.wrapper,"CodeMirror-focused"),
+a.curOp||a.display.selForContextMenu==a.doc.sel||(a.display.input.reset(),R&&setTimeout(function(){return a.display.input.reset(!0)},20)),a.display.input.receivedFocus()),rd(a))}function Db(a,b){a.state.delayingBlurEvent||(a.state.focused&&(F(a,"blur",a,b),a.state.focused=!1,Ra(a.display.wrapper,"CodeMirror-focused")),clearInterval(a.display.blinker),setTimeout(function(){a.state.focused||(a.display.shift=!1)},150))}function sc(a){a=a.display;for(var b=a.lineDiv.offsetTop,c=0;c<a.view.length;c++){var d=
+a.view[c],e=void 0;if(!d.hidden){if(C&&8>B)var f=d.node.offsetTop+d.node.offsetHeight,e=f-b,b=f;else e=d.node.getBoundingClientRect(),e=e.bottom-e.top;f=d.line.height-e;if(2>e&&(e=Oa(a)),(.005<f||-.005>f)&&(la(d.line,e),Me(d.line),d.rest))for(e=0;e<d.rest.length;e++)Me(d.rest[e])}}}function Me(a){if(a.widgets)for(var b=0;b<a.widgets.length;++b)a.widgets[b].height=a.widgets[b].node.parentNode.offsetHeight}function td(a,b,c){var d=c&&null!=c.top?Math.max(0,c.top):a.scroller.scrollTop,d=Math.floor(d-
+a.lineSpace.offsetTop),e=c&&null!=c.bottom?c.bottom:d+a.wrapper.clientHeight,d=Ha(b,d),e=Ha(b,e);if(c&&c.ensure){var f=c.ensure.from.line;c=c.ensure.to.line;f<d?(d=f,e=Ha(b,na(w(b,f))+a.wrapper.clientHeight)):Math.min(c,b.lastLine())>=e&&(d=Ha(b,na(w(b,c))-a.wrapper.clientHeight),e=c)}return{from:d,to:Math.max(e,d+1)}}function Ne(a){var b=a.display,c=b.view;if(b.alignWidgets||b.gutters.firstChild&&a.options.fixedGutter){for(var d=pd(b)-b.scroller.scrollLeft+a.doc.scrollLeft,e=b.gutters.offsetWidth,
+f=d+"px",g=0;g<c.length;g++)if(!c[g].hidden){a.options.fixedGutter&&(c[g].gutter&&(c[g].gutter.style.left=f),c[g].gutterBackground&&(c[g].gutterBackground.style.left=f));var h=c[g].alignable;if(h)for(var k=0;k<h.length;k++)h[k].style.left=f}a.options.fixedGutter&&(b.gutters.style.left=d+e+"px")}}function Oe(a){if(!a.options.lineNumbers)return!1;var b=a.doc,b=Qc(a.options,b.first+b.size-1),c=a.display;if(b.length!=c.lineNumChars){var d=c.measure.appendChild(r("div",[r("div",b)],"CodeMirror-linenumber CodeMirror-gutter-elt")),
+e=d.firstChild.offsetWidth,d=d.offsetWidth-e;return c.lineGutter.style.width="",c.lineNumInnerWidth=Math.max(e,c.lineGutter.offsetWidth-d)+1,c.lineNumWidth=c.lineNumInnerWidth+d,c.lineNumChars=c.lineNumInnerWidth?b.length:-1,c.lineGutter.style.width=c.lineNumWidth+"px",ud(a),!0}return!1}function vd(a,b){var c=a.display,d=Oa(a.display);0>b.top&&(b.top=0);var e=a.curOp&&null!=a.curOp.scrollTop?a.curOp.scrollTop:c.scroller.scrollTop,f=kd(a),g={};b.bottom-b.top>f&&(b.bottom=b.top+f);var h=a.doc.height+
+jd(c),k=b.top<d,d=b.bottom>h-d;b.top<e?g.scrollTop=k?0:b.top:b.bottom>e+f&&(f=Math.min(b.top,(d?h:b.bottom)-f),f!=e&&(g.scrollTop=f));e=a.curOp&&null!=a.curOp.scrollLeft?a.curOp.scrollLeft:c.scroller.scrollLeft;c=Ma(a)-(a.options.fixedGutter?c.gutters.offsetWidth:0);f=b.right-b.left>c;return f&&(b.right=b.left+c),10>b.left?g.scrollLeft=0:b.left<e?g.scrollLeft=Math.max(0,b.left-(f?0:10)):b.right>c+e-3&&(g.scrollLeft=b.right+(f?0:10)-c),g}function tc(a,b){null!=b&&(uc(a),a.curOp.scrollTop=(null==a.curOp.scrollTop?
+a.doc.scrollTop:a.curOp.scrollTop)+b)}function fb(a){uc(a);var b=a.getCursor();a.curOp.scrollToPos={from:b,to:b,margin:a.options.cursorScrollMargin}}function Eb(a,b,c){null==b&&null==c||uc(a);null!=b&&(a.curOp.scrollLeft=b);null!=c&&(a.curOp.scrollTop=c)}function uc(a){var b=a.curOp.scrollToPos;b&&(a.curOp.scrollToPos=null,Pe(a,Ge(a,b.from),Ge(a,b.to),b.margin))}function Pe(a,b,c,d){b=vd(a,{left:Math.min(b.left,c.left),top:Math.min(b.top,c.top)-d,right:Math.max(b.right,c.right),bottom:Math.max(b.bottom,
+c.bottom)+d});Eb(a,b.scrollLeft,b.scrollTop)}function Fb(a,b){2>Math.abs(a.doc.scrollTop-b)||(wa||wd(a,{top:b}),Qe(a,b,!0),wa&&wd(a),Gb(a,100))}function Qe(a,b,c){b=Math.min(a.display.scroller.scrollHeight-a.display.scroller.clientHeight,b);(a.display.scroller.scrollTop!=b||c)&&(a.doc.scrollTop=b,a.display.scrollbars.setScrollTop(b),a.display.scroller.scrollTop!=b&&(a.display.scroller.scrollTop=b))}function Sa(a,b,c,d){b=Math.min(b,a.display.scroller.scrollWidth-a.display.scroller.clientWidth);(c?
+b==a.doc.scrollLeft:2>Math.abs(a.doc.scrollLeft-b))&&!d||(a.doc.scrollLeft=b,Ne(a),a.display.scroller.scrollLeft!=b&&(a.display.scroller.scrollLeft=b),a.display.scrollbars.setScrollLeft(b))}function Hb(a){var b=a.display,c=b.gutters.offsetWidth,d=Math.round(a.doc.height+jd(a.display));return{clientHeight:b.scroller.clientHeight,viewHeight:b.wrapper.clientHeight,scrollWidth:b.scroller.scrollWidth,clientWidth:b.scroller.clientWidth,viewWidth:b.wrapper.clientWidth,barLeft:a.options.fixedGutter?c:0,docHeight:d,
+scrollHeight:d+oa(a)+b.barHeight,nativeBarWidth:b.nativeBarWidth,gutterWidth:c}}function gb(a,b){b||(b=Hb(a));var c=a.display.barWidth,d=a.display.barHeight;Re(a,b);for(var e=0;4>e&&c!=a.display.barWidth||d!=a.display.barHeight;e++)c!=a.display.barWidth&&a.options.lineWrapping&&sc(a),Re(a,Hb(a)),c=a.display.barWidth,d=a.display.barHeight}function Re(a,b){var c=a.display,d=c.scrollbars.update(b);c.sizer.style.paddingRight=(c.barWidth=d.right)+"px";c.sizer.style.paddingBottom=(c.barHeight=d.bottom)+
+"px";c.heightForcer.style.borderBottom=d.bottom+"px solid transparent";d.right&&d.bottom?(c.scrollbarFiller.style.display="block",c.scrollbarFiller.style.height=d.bottom+"px",c.scrollbarFiller.style.width=d.right+"px"):c.scrollbarFiller.style.display="";d.bottom&&a.options.coverGutterNextToScrollbar&&a.options.fixedGutter?(c.gutterFiller.style.display="block",c.gutterFiller.style.height=d.bottom+"px",c.gutterFiller.style.width=b.gutterWidth+"px"):c.gutterFiller.style.display=""}function Se(a){a.display.scrollbars&&
+(a.display.scrollbars.clear(),a.display.scrollbars.addClass&&Ra(a.display.wrapper,a.display.scrollbars.addClass));a.display.scrollbars=new Te[a.options.scrollbarStyle](function(b){a.display.wrapper.insertBefore(b,a.display.scrollbarFiller);t(b,"mousedown",function(){a.state.focused&&setTimeout(function(){return a.display.input.focus()},0)});b.setAttribute("cm-not-content","true")},function(b,c){"horizontal"==c?Sa(a,b):Fb(a,b)},a);a.display.scrollbars.addClass&&Ea(a.display.wrapper,a.display.scrollbars.addClass)}
+function Ta(a){a.curOp={cm:a,viewChanged:!1,startHeight:a.doc.height,forceUpdate:!1,updateInput:null,typing:!1,changeObjs:null,cursorActivityHandlers:null,cursorActivityCalled:0,selectionChanged:!1,updateMaxLine:!1,scrollLeft:null,scrollTop:null,scrollToPos:null,focus:!1,id:++xg};a=a.curOp;eb?eb.ops.push(a):a.ownsGroup=eb={ops:[a],delayedCallbacks:[]}}function Ua(a){sg(a.curOp,function(a){for(var c=0;c<a.ops.length;c++)a.ops[c].cm.curOp=null;a=a.ops;for(c=0;c<a.length;c++){var d=a[c],e=d.cm,f=e.display,
+g=e.display;!g.scrollbarsClipped&&g.scroller.offsetWidth&&(g.nativeBarWidth=g.scroller.offsetWidth-g.scroller.clientWidth,g.heightForcer.style.height=oa(e)+"px",g.sizer.style.marginBottom=-g.nativeBarWidth+"px",g.sizer.style.borderRightWidth=oa(e)+"px",g.scrollbarsClipped=!0);d.updateMaxLine&&Wc(e);d.mustUpdate=d.viewChanged||d.forceUpdate||null!=d.scrollTop||d.scrollToPos&&(d.scrollToPos.from.line<f.viewFrom||d.scrollToPos.to.line>=f.viewTo)||f.maxLineChanged&&e.options.lineWrapping;d.update=d.mustUpdate&&
+new vc(e,d.mustUpdate&&{top:d.scrollTop,ensure:d.scrollToPos},d.forceUpdate)}for(c=0;c<a.length;c++)d=a[c],d.updatedDisplay=d.mustUpdate&&xd(d.cm,d.update);for(c=0;c<a.length;c++)d=a[c],e=d.cm,f=e.display,d.updatedDisplay&&sc(e),d.barMeasure=Hb(e),f.maxLineChanged&&!e.options.lineWrapping&&(g=void 0,g=f.maxLine.text.length,g=ra(e,$a(e,f.maxLine),g,void 0),d.adjustWidthTo=g.left+3,e.display.sizerWidth=d.adjustWidthTo,d.barMeasure.scrollWidth=Math.max(f.scroller.clientWidth,f.sizer.offsetLeft+d.adjustWidthTo+
+oa(e)+e.display.barWidth),d.maxScrollLeft=Math.max(0,f.sizer.offsetLeft+d.adjustWidthTo-Ma(e))),(d.updatedDisplay||d.selectionChanged)&&(d.preparedSelection=f.input.prepareSelection(d.focus));for(c=0;c<a.length;c++)d=a[c],e=d.cm,null!=d.adjustWidthTo&&(e.display.sizer.style.minWidth=d.adjustWidthTo+"px",d.maxScrollLeft<e.doc.scrollLeft&&Sa(e,Math.min(e.display.scroller.scrollLeft,d.maxScrollLeft),!0),e.display.maxLineChanged=!1),f=d.focus&&d.focus==qa()&&(!document.hasFocus||document.hasFocus()),
+d.preparedSelection&&e.display.input.showSelection(d.preparedSelection,f),(d.updatedDisplay||d.startHeight!=e.doc.height)&&gb(e,d.barMeasure),d.updatedDisplay&&yd(e,d.barMeasure),d.selectionChanged&&rd(e),e.state.focused&&d.updateInput&&e.display.input.reset(d.typing),f&&Ke(d.cm);for(c=0;c<a.length;c++){d=a[c];e=d.cm;f=e.display;g=e.doc;if(d.updatedDisplay&&Ue(e,d.update),null==f.wheelStartX||null==d.scrollTop&&null==d.scrollLeft&&!d.scrollToPos||(f.wheelStartX=f.wheelStartY=null),null!=d.scrollTop&&
+Qe(e,d.scrollTop,d.forceScroll),null!=d.scrollLeft&&Sa(e,d.scrollLeft,!0,!0),d.scrollToPos){var h=v(g,d.scrollToPos.from),k=v(g,d.scrollToPos.to),l=d.scrollToPos.margin;null==l&&(l=0);var n=void 0;e.options.lineWrapping||h!=k||(h=h.ch?m(h.line,"before"==h.sticky?h.ch-1:h.ch,"after"):h,k="before"==h.sticky?m(h.line,h.ch+1,"before"):h);for(var q=0;5>q;q++){var p=!1,n=ia(e,h),u=k&&k!=h?ia(e,k):n,n={left:Math.min(n.left,u.left),top:Math.min(n.top,u.top)-l,right:Math.max(n.left,u.left),bottom:Math.max(n.bottom,
+u.bottom)+l},u=vd(e,n),O=e.doc.scrollTop,t=e.doc.scrollLeft;if(null!=u.scrollTop&&(Fb(e,u.scrollTop),1<Math.abs(e.doc.scrollTop-O)&&(p=!0)),null!=u.scrollLeft&&(Sa(e,u.scrollLeft),1<Math.abs(e.doc.scrollLeft-t)&&(p=!0)),!p)break}k=n;K(e,"scrollCursorIntoView")||(l=e.display,q=l.sizer.getBoundingClientRect(),h=null,(0>k.top+q.top?h=!0:k.bottom+q.top>(window.innerHeight||document.documentElement.clientHeight)&&(h=!1),null==h||yg)||(k=r("div","​",null,"position: absolute;\n                         top: "+
+(k.top-l.viewOffset-e.display.lineSpace.offsetTop)+"px;\n                         height: "+(k.bottom-k.top+oa(e)+l.barHeight)+"px;\n                         left: "+k.left+"px; width: "+Math.max(2,k.right-k.left)+"px;"),e.display.lineSpace.appendChild(k),k.scrollIntoView(h),e.display.lineSpace.removeChild(k)))}k=d.maybeHiddenMarkers;h=d.maybeUnhiddenMarkers;if(k)for(l=0;l<k.length;++l)k[l].lines.length||F(k[l],"hide");if(h)for(k=0;k<h.length;++k)h[k].lines.length&&F(h[k],"unhide");f.wrapper.offsetHeight&&
+(g.scrollTop=e.display.scroller.scrollTop);d.changeObjs&&F(e,"changes",e,d.changeObjs);d.update&&d.update.finish()}})}function Y(a,b){if(a.curOp)return b();Ta(a);try{return b()}finally{Ua(a)}}function L(a,b){return function(){if(a.curOp)return b.apply(a,arguments);Ta(a);try{return b.apply(a,arguments)}finally{Ua(a)}}}function T(a){return function(){if(this.curOp)return a.apply(this,arguments);Ta(this);try{return a.apply(this,arguments)}finally{Ua(this)}}}function M(a){return function(){var b=this.cm;
+if(!b||b.curOp)return a.apply(this,arguments);Ta(b);try{return a.apply(this,arguments)}finally{Ua(b)}}}function V(a,b,c,d){null==b&&(b=a.doc.first);null==c&&(c=a.doc.first+a.doc.size);d||(d=0);var e=a.display;if(d&&c<e.viewTo&&(null==e.updateLineNumbers||e.updateLineNumbers>b)&&(e.updateLineNumbers=b),a.curOp.viewChanged=!0,b>=e.viewTo)xa&&Uc(a.doc,b)<e.viewTo&&za(a);else if(c<=e.viewFrom)xa&&ae(a.doc,c+d)>e.viewFrom?za(a):(e.viewFrom+=d,e.viewTo+=d);else if(b<=e.viewFrom&&c>=e.viewTo)za(a);else if(b<=
+e.viewFrom){var f=wc(a,c,c+d,1);f?(e.view=e.view.slice(f.index),e.viewFrom=f.lineN,e.viewTo+=d):za(a)}else if(c>=e.viewTo)(f=wc(a,b,b,-1))?(e.view=e.view.slice(0,f.index),e.viewTo=f.lineN):za(a);else{var f=wc(a,b,b,-1),g=wc(a,c,c+d,1);f&&g?(e.view=e.view.slice(0,f.index).concat(oc(a,f.lineN,g.lineN)).concat(e.view.slice(g.index)),e.viewTo+=d):za(a)}(a=e.externalMeasured)&&(c<a.lineN?a.lineN+=d:b<a.lineN+a.size&&(e.externalMeasured=null))}function Aa(a,b,c){a.curOp.viewChanged=!0;var d=a.display,e=
+a.display.externalMeasured;(e&&b>=e.lineN&&b<e.lineN+e.size&&(d.externalMeasured=null),b<d.viewFrom||b>=d.viewTo)||(a=d.view[Na(a,b)],null!=a.node&&(a=a.changes||(a.changes=[]),-1==N(a,c)&&a.push(c)))}function za(a){a.display.viewFrom=a.display.viewTo=a.doc.first;a.display.view=[];a.display.viewOffset=0}function wc(a,b,c,d){var e=Na(a,b),f=a.display.view;if(!xa||c==a.doc.first+a.doc.size)return{index:e,lineN:c};for(var g=a.display.viewFrom,h=0;h<e;h++)g+=f[h].size;if(g!=b){if(0<d){if(e==f.length-
+1)return null;b=g+f[e].size-b;e++}else b=g-b;c+=b}for(;Uc(a.doc,c)!=c;){if(e==(0>d?0:f.length-1))return null;c+=d*f[e-(0>d?1:0)].size;e+=d}return{index:e,lineN:c}}function Ve(a){a=a.display.view;for(var b=0,c=0;c<a.length;c++){var d=a[c];d.hidden||d.node&&!d.changes||++b}return b}function Gb(a,b){a.doc.highlightFrontier<a.display.viewTo&&a.state.highlight.set(b,Kc(zg,a))}function zg(a){var b=a.doc;if(!(b.highlightFrontier>=a.display.viewTo)){var c=+new Date+a.options.workTime,d=wb(a,b.highlightFrontier),
+e=[];b.iter(d.line,Math.min(b.first+b.size,a.display.viewTo+500),function(f){if(d.line>=a.display.viewFrom){var g=f.styles,h=f.text.length>a.options.maxHighlightLength?La(b.mode,d.state):null,k=he(a,f,d,!0);h&&(d.state=h);f.styles=k.styles;h=f.styleClasses;(k=k.classes)?f.styleClasses=k:h&&(f.styleClasses=null);k=!g||g.length!=f.styles.length||h!=k&&(!h||!k||h.bgClass!=k.bgClass||h.textClass!=k.textClass);for(h=0;!k&&h<g.length;++h)k=g[h]!=f.styles[h];k&&e.push(d.line);f.stateAfter=d.save()}else f.text.length<=
+a.options.maxHighlightLength&&fd(a,f.text,d),f.stateAfter=0==d.line%5?d.save():null;d.nextLine();if(+new Date>c)return Gb(a,a.options.workDelay),!0});b.highlightFrontier=d.line;b.modeFrontier=Math.max(b.modeFrontier,d.line);e.length&&Y(a,function(){for(var b=0;b<e.length;b++)Aa(a,e[b],"text")})}}function xd(a,b){var c=a.display,d=a.doc;if(b.editorIsHidden)return za(a),!1;if(!b.force&&b.visible.from>=c.viewFrom&&b.visible.to<=c.viewTo&&(null==c.updateLineNumbers||c.updateLineNumbers>=c.viewTo)&&c.renderedView==
+c.view&&0==Ve(a))return!1;Oe(a)&&(za(a),b.dims=md(a));var e=d.first+d.size,f=Math.max(b.visible.from-a.options.viewportMargin,d.first),g=Math.min(e,b.visible.to+a.options.viewportMargin);c.viewFrom<f&&20>f-c.viewFrom&&(f=Math.max(d.first,c.viewFrom));c.viewTo>g&&20>c.viewTo-g&&(g=Math.min(e,c.viewTo));xa&&(f=Uc(a.doc,f),g=ae(a.doc,g));d=f!=c.viewFrom||g!=c.viewTo||c.lastWrapHeight!=b.wrapperHeight||c.lastWrapWidth!=b.wrapperWidth;e=a.display;0==e.view.length||f>=e.viewTo||g<=e.viewFrom?(e.view=oc(a,
+f,g),e.viewFrom=f):(e.viewFrom>f?e.view=oc(a,f,e.viewFrom).concat(e.view):e.viewFrom<f&&(e.view=e.view.slice(Na(a,f))),e.viewFrom=f,e.viewTo<g?e.view=e.view.concat(oc(a,e.viewTo,g)):e.viewTo>g&&(e.view=e.view.slice(0,Na(a,g))));e.viewTo=g;c.viewOffset=na(w(a.doc,c.viewFrom));a.display.mover.style.top=c.viewOffset+"px";g=Ve(a);if(!d&&0==g&&!b.force&&c.renderedView==c.view&&(null==c.updateLineNumbers||c.updateLineNumbers>=c.viewTo))return!1;a.hasFocus()?f=null:(f=qa())&&va(a.display.lineDiv,f)?(f={activeElt:f},
+window.getSelection&&(e=window.getSelection(),e.anchorNode&&e.extend&&va(a.display.lineDiv,e.anchorNode)&&(f.anchorNode=e.anchorNode,f.anchorOffset=e.anchorOffset,f.focusNode=e.focusNode,f.focusOffset=e.focusOffset))):f=null;4<g&&(c.lineDiv.style.display="none");Ag(a,c.updateLineNumbers,b.dims);4<g&&(c.lineDiv.style.display="");c.renderedView=c.view;(g=f)&&g.activeElt&&g.activeElt!=qa()&&(g.activeElt.focus(),g.anchorNode&&va(document.body,g.anchorNode)&&va(document.body,g.focusNode))&&(f=window.getSelection(),
+e=document.createRange(),e.setEnd(g.anchorNode,g.anchorOffset),e.collapse(!1),f.removeAllRanges(),f.addRange(e),f.extend(g.focusNode,g.focusOffset));return X(c.cursorDiv),X(c.selectionDiv),c.gutters.style.height=c.sizer.style.minHeight=0,d&&(c.lastWrapHeight=b.wrapperHeight,c.lastWrapWidth=b.wrapperWidth,Gb(a,400)),c.updateLineNumbers=null,!0}function Ue(a,b){for(var c=b.viewport,d=!0;(d&&a.options.lineWrapping&&b.oldDisplayWidth!=Ma(a)||(c&&null!=c.top&&(c={top:Math.min(a.doc.height+jd(a.display)-
+kd(a),c.top)}),b.visible=td(a.display,a.doc,c),!(b.visible.from>=a.display.viewFrom&&b.visible.to<=a.display.viewTo)))&&xd(a,b);d=!1)sc(a),d=Hb(a),Cb(a),gb(a,d),yd(a,d),b.force=!1;b.signal(a,"update",a);a.display.viewFrom==a.display.reportedViewFrom&&a.display.viewTo==a.display.reportedViewTo||(b.signal(a,"viewportChange",a,a.display.viewFrom,a.display.viewTo),a.display.reportedViewFrom=a.display.viewFrom,a.display.reportedViewTo=a.display.viewTo)}function wd(a,b){var c=new vc(a,b);if(xd(a,c)){sc(a);
+Ue(a,c);var d=Hb(a);Cb(a);gb(a,d);yd(a,d);c.finish()}}function Ag(a,b,c){function d(b){var d=b.nextSibling;return R&&ha&&a.display.currentWheelTarget==b?b.style.display="none":b.parentNode.removeChild(b),d}for(var e=a.display,f=a.options.lineNumbers,g=e.lineDiv,h=g.firstChild,k=e.view,e=e.viewFrom,l=0;l<k.length;l++){var n=k[l];if(!n.hidden)if(n.node&&n.node.parentNode==g){for(;h!=n.node;)h=d(h);h=f&&null!=b&&b<=e&&n.lineNumber;n.changes&&(-1<N(n.changes,"gutter")&&(h=!1),se(a,n,e,c));h&&(X(n.lineNumber),
+n.lineNumber.appendChild(document.createTextNode(Qc(a.options,e))));h=n.node.nextSibling}else{var q=ug(a,n,e,c);g.insertBefore(q,h)}e+=n.size}for(;h;)h=d(h)}function ud(a){a.display.sizer.style.marginLeft=a.display.gutters.offsetWidth+"px"}function yd(a,b){a.display.sizer.style.minHeight=b.docHeight+"px";a.display.heightForcer.style.top=b.docHeight+"px";a.display.gutters.style.height=b.docHeight+a.display.barHeight+oa(a)+"px"}function We(a){var b=a.display.gutters,c=a.options.gutters;X(b);for(var d=
+0;d<c.length;++d){var e=c[d],f=b.appendChild(r("div",null,"CodeMirror-gutter "+e));"CodeMirror-linenumbers"==e&&(a.display.lineGutter=f,f.style.width=(a.display.lineNumWidth||1)+"px")}b.style.display=d?"":"none";ud(a)}function zd(a){var b=N(a.gutters,"CodeMirror-linenumbers");-1==b&&a.lineNumbers?a.gutters=a.gutters.concat(["CodeMirror-linenumbers"]):-1<b&&!a.lineNumbers&&(a.gutters=a.gutters.slice(0),a.gutters.splice(b,1))}function Xe(a){var b=a.wheelDeltaX,c=a.wheelDeltaY;return null==b&&a.detail&&
+a.axis==a.HORIZONTAL_AXIS&&(b=a.detail),null==c&&a.detail&&a.axis==a.VERTICAL_AXIS?c=a.detail:null==c&&(c=a.wheelDelta),{x:b,y:c}}function Bg(a){a=Xe(a);return a.x*=ba,a.y*=ba,a}function Ye(a,b){var c=Xe(b),d=c.x,c=c.y,e=a.display,f=e.scroller,g=f.scrollWidth>f.clientWidth,h=f.scrollHeight>f.clientHeight;if(d&&g||c&&h){if(c&&ha&&R){var g=b.target,k=e.view;a:for(;g!=f;g=g.parentNode)for(var l=0;l<k.length;l++)if(k[l].node==g){a.display.currentWheelTarget=g;break a}}if(d&&!wa&&!ja&&null!=ba)return c&&
+h&&Fb(a,Math.max(0,f.scrollTop+c*ba)),Sa(a,Math.max(0,f.scrollLeft+d*ba)),(!c||c&&h)&&S(b),void(e.wheelStartX=null);c&&null!=ba&&(h=c*ba,g=a.doc.scrollTop,k=g+e.wrapper.clientHeight,0>h?g=Math.max(0,g+h-50):k=Math.min(a.doc.height,k+h+50),wd(a,{top:g,bottom:k}));20>xc&&(null==e.wheelStartX?(e.wheelStartX=f.scrollLeft,e.wheelStartY=f.scrollTop,e.wheelDX=d,e.wheelDY=c,setTimeout(function(){if(null!=e.wheelStartX){var a=f.scrollLeft-e.wheelStartX,b=f.scrollTop-e.wheelStartY,a=b&&e.wheelDY&&b/e.wheelDY||
+a&&e.wheelDX&&a/e.wheelDX;e.wheelStartX=e.wheelStartY=null;a&&(ba=(ba*xc+a)/(xc+1),++xc)}},200)):(e.wheelDX+=d,e.wheelDY+=c))}}function ka(a,b){var c=a[b];a.sort(function(a,b){return x(a.from(),b.from())});b=N(a,c);for(c=1;c<a.length;c++){var d=a[c],e=a[c-1];if(0<=x(e.to(),d.from())){var f=ic(e.from(),d.from()),g=hc(e.to(),d.to()),d=e.empty()?d.from()==d.head:e.from()==e.head;c<=b&&--b;a.splice(--c,2,new A(d?g:f,d?f:g))}}return new ca(a,b)}function ua(a,b){return new ca([new A(a,b||a)],0)}function Ba(a){return a.text?
+m(a.from.line+a.text.length-1,z(a.text).length+(1==a.text.length?a.from.ch:0)):a.to}function Ze(a,b){if(0>x(a,b.from))return a;if(0>=x(a,b.to))return Ba(b);var c=a.line+b.text.length-(b.to.line-b.from.line)-1,d=a.ch;return a.line==b.to.line&&(d+=Ba(b).ch-b.to.ch),m(c,d)}function Ad(a,b){for(var c=[],d=0;d<a.sel.ranges.length;d++){var e=a.sel.ranges[d];c.push(new A(Ze(e.anchor,b),Ze(e.head,b)))}return ka(c,a.sel.primIndex)}function $e(a,b,c){return a.line==b.line?m(c.line,a.ch-b.ch+c.ch):m(c.line+
+(a.line-b.line),a.ch)}function Bd(a){a.doc.mode=dd(a.options,a.doc.modeOption);Ib(a)}function Ib(a){a.doc.iter(function(a){a.stateAfter&&(a.stateAfter=null);a.styles&&(a.styles=null)});a.doc.modeFrontier=a.doc.highlightFrontier=a.doc.first;Gb(a,100);a.state.modeGen++;a.curOp&&V(a)}function af(a,b){return 0==b.from.ch&&0==b.to.ch&&""==z(b.text)&&(!a.cm||a.cm.options.wholeLineUpdateBefore)}function Cd(a,b,c,d){function e(a,c,e){a.text=c;a.stateAfter&&(a.stateAfter=null);a.styles&&(a.styles=null);null!=
+a.order&&(a.order=null);Xd(a);Yd(a,e);c=d?d(a):1;c!=a.height&&la(a,c);P(a,"change",a,b)}function f(a,b){for(var e=[],f=a;f<b;++f)e.push(new hb(k[f],c?c[f]:null,d));return e}var g=b.from,h=b.to,k=b.text,l=w(a,g.line),n=w(a,h.line),q=z(k),p=c?c[k.length-1]:null,u=h.line-g.line;b.full?(a.insert(0,f(0,k.length)),a.remove(k.length,a.size-k.length)):af(a,b)?(h=f(0,k.length-1),e(n,n.text,p),u&&a.remove(g.line,u),h.length&&a.insert(g.line,h)):l==n?1==k.length?e(l,l.text.slice(0,g.ch)+q+l.text.slice(h.ch),
+p):(u=f(1,k.length-1),u.push(new hb(q+l.text.slice(h.ch),p,d)),e(l,l.text.slice(0,g.ch)+k[0],c?c[0]:null),a.insert(g.line+1,u)):1==k.length?(e(l,l.text.slice(0,g.ch)+k[0]+n.text.slice(h.ch),c?c[0]:null),a.remove(g.line+1,u)):(e(l,l.text.slice(0,g.ch)+k[0],c?c[0]:null),e(n,q+n.text.slice(h.ch),p),p=f(1,k.length-1),1<u&&a.remove(g.line+1,u-1),a.insert(g.line+1,p));P(a,"change",a,b)}function Va(a,b,c){function d(a,f,g){if(a.linked)for(var h=0;h<a.linked.length;++h){var k=a.linked[h];if(k.doc!=f){var l=
+g&&k.sharedHist;c&&!l||(b(k.doc,l),d(k.doc,a,l))}}}d(a,null,!0)}function bf(a,b){if(b.cm)throw Error("This document is already in use.");a.doc=b;b.cm=a;qd(a);Bd(a);cf(a);a.options.lineWrapping||Wc(a);a.options.mode=b.modeOption;V(a)}function cf(a){("rtl"==a.doc.direction?Ea:Ra)(a.display.lineDiv,"CodeMirror-rtl")}function Cg(a){Y(a,function(){cf(a);V(a)})}function yc(a){this.done=[];this.undone=[];this.undoDepth=1/0;this.lastModTime=this.lastSelTime=0;this.lastOrigin=this.lastSelOrigin=this.lastOp=
+this.lastSelOp=null;this.generation=this.maxGeneration=a||1}function Dd(a,b){var c={from:Sc(b.from),to:Ba(b),text:Ga(a,b.from,b.to)};return df(a,c,b.from.line,b.to.line+1),Va(a,function(a){return df(a,c,b.from.line,b.to.line+1)},!0),c}function ef(a){for(;a.length&&z(a).ranges;)a.pop()}function ff(a,b,c,d){var e=a.history;e.undone.length=0;var f,g,h=+new Date,k;if(k=e.lastOp==d||e.lastOrigin==b.origin&&b.origin&&("+"==b.origin.charAt(0)&&a.cm&&e.lastModTime>h-a.cm.options.historyEventDelay||"*"==b.origin.charAt(0)))k=
+f=e.lastOp==d?(ef(e.done),z(e.done)):e.done.length&&!z(e.done).ranges?z(e.done):1<e.done.length&&!e.done[e.done.length-2].ranges?(e.done.pop(),z(e.done)):void 0;if(k)g=z(f.changes),0==x(b.from,b.to)&&0==x(b.from,g.to)?g.to=Ba(b):f.changes.push(Dd(a,b));else for((f=z(e.done))&&f.ranges||zc(a.sel,e.done),f={changes:[Dd(a,b)],generation:e.generation},e.done.push(f);e.done.length>e.undoDepth;)e.done.shift(),e.done[0].ranges||e.done.shift();e.done.push(c);e.generation=++e.maxGeneration;e.lastModTime=e.lastSelTime=
+h;e.lastOp=e.lastSelOp=d;e.lastOrigin=e.lastSelOrigin=b.origin;g||F(a,"historyAdded")}function zc(a,b){var c=z(b);c&&c.ranges&&c.equals(a)||b.push(a)}function df(a,b,c,d){var e=b["spans_"+a.id],f=0;a.iter(Math.max(a.first,c),Math.min(a.first+a.size,d),function(d){d.markedSpans&&((e||(e=b["spans_"+a.id]={}))[f]=d.markedSpans);++f})}function Dg(a){if(!a)return null;for(var b,c=0;c<a.length;++c)a[c].marker.explicitlyCleared?b||(b=a.slice(0,c)):b&&b.push(a[c]);return b?b.length?b:null:a}function gf(a,
+b){var c;if(c=b["spans_"+a.id]){for(var d=[],e=0;e<b.text.length;++e)d.push(Dg(c[e]));c=d}else c=null;d=Tc(a,b);if(!c)return d;if(!d)return c;for(e=0;e<c.length;++e){var f=c[e],g=d[e];if(f&&g){var h=0;a:for(;h<g.length;++h){for(var k=g[h],l=0;l<f.length;++l)if(f[l].marker==k.marker)continue a;f.push(k)}}else g&&(c[e]=g)}return c}function ib(a,b,c){for(var d=[],e=0;e<a.length;++e){var f=a[e];if(f.ranges)d.push(c?ca.prototype.deepCopy.call(f):f);else{var f=f.changes,g=[];d.push({changes:g});for(var h=
+0;h<f.length;++h){var k=f[h],l=void 0;if(g.push({from:k.from,to:k.to,text:k.text}),b)for(var n in k)(l=n.match(/^spans_(\d+)$/))&&-1<N(b,Number(l[1]))&&(z(g)[n]=k[n],delete k[n])}}}return d}function Ed(a,b,c,d){return d?(a=a.anchor,c&&(d=0>x(b,a),d!=0>x(c,a)?(a=b,b=c):d!=0>x(b,c)&&(b=c)),new A(a,b)):new A(c||b,b)}function Ac(a,b,c,d,e){null==e&&(e=a.cm&&(a.cm.display.shift||a.extend));Q(a,new ca([Ed(a.sel.primary(),b,c,e)],0),d)}function hf(a,b,c){for(var d=[],e=a.cm&&(a.cm.display.shift||a.extend),
+f=0;f<a.sel.ranges.length;f++)d[f]=Ed(a.sel.ranges[f],b[f],null,e);Q(a,ka(d,a.sel.primIndex),c)}function Fd(a,b,c,d){var e=a.sel.ranges.slice(0);e[b]=c;Q(a,ka(e,a.sel.primIndex),d)}function Eg(a,b,c){c={ranges:b.ranges,update:function(b){this.ranges=[];for(var c=0;c<b.length;c++)this.ranges[c]=new A(v(a,b[c].anchor),v(a,b[c].head))},origin:c&&c.origin};return F(a,"beforeSelectionChange",a,c),a.cm&&F(a.cm,"beforeSelectionChange",a.cm,c),c.ranges!=b.ranges?ka(c.ranges,c.ranges.length-1):b}function jf(a,
+b,c){var d=a.history.done,e=z(d);e&&e.ranges?(d[d.length-1]=b,Bc(a,b,c)):Q(a,b,c)}function Q(a,b,c){Bc(a,b,c);b=a.sel;var d=a.cm?a.cm.curOp.id:NaN,e=a.history,f=c&&c.origin,g;if(!(g=d==e.lastSelOp)&&(g=f&&e.lastSelOrigin==f)&&!(g=e.lastModTime==e.lastSelTime&&e.lastOrigin==f)){g=z(e.done);var h=f.charAt(0);g="*"==h||"+"==h&&g.ranges.length==b.ranges.length&&g.somethingSelected()==b.somethingSelected()&&new Date-a.history.lastSelTime<=(a.cm?a.cm.options.historyEventDelay:500)}g?e.done[e.done.length-
+1]=b:zc(b,e.done);e.lastSelTime=+new Date;e.lastSelOrigin=f;e.lastSelOp=d;c&&!1!==c.clearRedo&&ef(e.undone)}function Bc(a,b,c){(fa(a,"beforeSelectionChange")||a.cm&&fa(a.cm,"beforeSelectionChange"))&&(b=Eg(a,b,c));kf(a,lf(a,b,c&&c.bias||(0>x(b.primary().head,a.sel.primary().head)?-1:1),!0));c&&!1===c.scroll||!a.cm||fb(a.cm)}function kf(a,b){b.equals(a.sel)||(a.sel=b,a.cm&&(a.cm.curOp.updateInput=a.cm.curOp.selectionChanged=!0,de(a.cm)),P(a,"cursorActivity",a))}function mf(a){kf(a,lf(a,a.sel,null,
+!1))}function lf(a,b,c,d){for(var e,f=0;f<b.ranges.length;f++){var g=b.ranges[f],h=b.ranges.length==a.sel.ranges.length&&a.sel.ranges[f],k=Gd(a,g.anchor,h&&h.anchor,c,d),h=Gd(a,g.head,h&&h.head,c,d);(e||k!=g.anchor||h!=g.head)&&(e||(e=b.ranges.slice(0,f)),e[f]=new A(k,h))}return e?ka(e,b.primIndex):b}function jb(a,b,c,d,e){var f=w(a,b.line);if(f.markedSpans)for(var g=0;g<f.markedSpans.length;++g){var h=f.markedSpans[g],k=h.marker;if((null==h.from||(k.inclusiveLeft?h.from<=b.ch:h.from<b.ch))&&(null==
+h.to||(k.inclusiveRight?h.to>=b.ch:h.to>b.ch))){if(e&&(F(k,"beforeCursorEnter"),k.explicitlyCleared)){if(f.markedSpans){--g;continue}break}if(k.atomic){if(c&&(g=k.find(0>d?1:-1),h=void 0,(0>d?k.inclusiveRight:k.inclusiveLeft)&&(g=nf(a,g,-d,g&&g.line==b.line?f:null)),g&&g.line==b.line&&(h=x(g,c))&&(0>d?0>h:0<h)))return jb(a,g,b,d,e);c=k.find(0>d?-1:1);return(0>d?k.inclusiveLeft:k.inclusiveRight)&&(c=nf(a,c,d,c.line==b.line?f:null)),c?jb(a,c,b,d,e):null}}}return b}function Gd(a,b,c,d,e){d=d||1;return jb(a,
+b,c,d,e)||!e&&jb(a,b,c,d,!0)||jb(a,b,c,-d,e)||!e&&jb(a,b,c,-d,!0)||(a.cantEdit=!0,m(a.first,0))}function nf(a,b,c,d){return 0>c&&0==b.ch?b.line>a.first?v(a,m(b.line-1)):null:0<c&&b.ch==(d||w(a,b.line)).text.length?b.line<a.first+a.size-1?m(b.line+1,0):null:new m(b.line,b.ch+c)}function of(a){a.setSelection(m(a.firstLine(),0),m(a.lastLine()),pa)}function pf(a,b,c){var d={canceled:!1,from:b.from,to:b.to,text:b.text,origin:b.origin,cancel:function(){return d.canceled=!0}};return c&&(d.update=function(b,
+c,g,h){b&&(d.from=v(a,b));c&&(d.to=v(a,c));g&&(d.text=g);void 0!==h&&(d.origin=h)}),F(a,"beforeChange",a,d),a.cm&&F(a.cm,"beforeChange",a.cm,d),d.canceled?null:{from:d.from,to:d.to,text:d.text,origin:d.origin}}function kb(a,b,c){if(a.cm){if(!a.cm.curOp)return L(a.cm,kb)(a,b,c);if(a.cm.state.suppressEdits)return}if(!(fa(a,"beforeChange")||a.cm&&fa(a.cm,"beforeChange"))||(b=pf(a,b,!0)))if(c=qf&&!c&&fg(a,b.from,b.to))for(var d=c.length-1;0<=d;--d)rf(a,{from:c[d].from,to:c[d].to,text:d?[""]:b.text});
+else rf(a,b)}function rf(a,b){if(1!=b.text.length||""!=b.text[0]||0!=x(b.from,b.to)){var c=Ad(a,b);ff(a,b,c,a.cm?a.cm.curOp.id:NaN);Jb(a,b,c,Tc(a,b));var d=[];Va(a,function(a,c){c||-1!=N(d,a.history)||(sf(a.history,b),d.push(a.history));Jb(a,b,null,Tc(a,b))})}}function Cc(a,b,c){if(!a.cm||!a.cm.state.suppressEdits||c){for(var d,e=a.history,f=a.sel,g="undo"==b?e.done:e.undone,h="undo"==b?e.undone:e.done,k=0;k<g.length&&(d=g[k],c?!d.ranges||d.equals(a.sel):d.ranges);k++);if(k!=g.length){for(e.lastOrigin=
+e.lastSelOrigin=null;d=g.pop(),d.ranges;){if(zc(d,h),c&&!d.equals(a.sel))return void Q(a,d,{clearRedo:!1});f=d}var l=[];zc(f,h);h.push({changes:l,generation:e.generation});e.generation=d.generation||++e.maxGeneration;var n=fa(a,"beforeChange")||a.cm&&fa(a.cm,"beforeChange");for(c=d.changes.length-1;0<=c;--c)if(e=function(c){var e=d.changes[c];if(e.origin=b,n&&!pf(a,e,!1))return g.length=0,{};l.push(Dd(a,e));var f=c?Ad(a,e):z(g);Jb(a,e,f,gf(a,e));!c&&a.cm&&a.cm.scrollIntoView({from:e.from,to:Ba(e)});
+var h=[];Va(a,function(a,b){b||-1!=N(h,a.history)||(sf(a.history,e),h.push(a.history));Jb(a,e,null,gf(a,e))})}(c))return e.v}}}function tf(a,b){if(0!=b&&(a.first+=b,a.sel=new ca(ec(a.sel.ranges,function(a){return new A(m(a.anchor.line+b,a.anchor.ch),m(a.head.line+b,a.head.ch))}),a.sel.primIndex),a.cm)){V(a.cm,a.first,a.first-b,b);for(var c=a.cm.display,d=c.viewFrom;d<c.viewTo;d++)Aa(a.cm,d,"gutter")}}function Jb(a,b,c,d){if(a.cm&&!a.cm.curOp)return L(a.cm,Jb)(a,b,c,d);if(b.to.line<a.first)return void tf(a,
+b.text.length-1-(b.to.line-b.from.line));if(!(b.from.line>a.lastLine())){if(b.from.line<a.first){var e=b.text.length-1-(a.first-b.from.line);tf(a,e);b={from:m(a.first,0),to:m(b.to.line+e,b.to.ch),text:[z(b.text)],origin:b.origin}}e=a.lastLine();b.to.line>e&&(b={from:b.from,to:m(e,w(a,e).text.length),text:[b.text[0]],origin:b.origin});b.removed=Ga(a,b.from,b.to);c||(c=Ad(a,b));a.cm?Fg(a.cm,b,d):Cd(a,b,d);Bc(a,c,pa)}}function Fg(a,b,c){var d=a.doc,e=a.display,f=b.from,g=b.to,h=!1,k=f.line;a.options.lineWrapping||
+(k=D(ma(w(d,f.line))),d.iter(k,g.line+1,function(a){if(a==e.maxLine)return h=!0,!0}));-1<d.sel.contains(b.from,b.to)&&de(a);Cd(d,b,c,He(a));a.options.lineWrapping||(d.iter(k,f.line+b.text.length,function(a){var b=kc(a);b>e.maxLineLength&&(e.maxLine=a,e.maxLineLength=b,e.maxLineChanged=!0,h=!1)}),h&&(a.curOp.updateMaxLine=!0));mg(d,f.line);Gb(a,400);c=b.text.length-(g.line-f.line)-1;b.full?V(a):f.line!=g.line||1!=b.text.length||af(a.doc,b)?V(a,f.line,g.line+1,c):Aa(a,f.line,"text");c=fa(a,"changes");
+if((d=fa(a,"change"))||c)b={from:f,to:g,text:b.text,removed:b.removed,origin:b.origin},d&&P(a,"change",a,b),c&&(a.curOp.changeObjs||(a.curOp.changeObjs=[])).push(b);a.display.selForContextMenu=null}function lb(a,b,c,d,e){if(d||(d=c),0>x(d,c)){var f=d;d=c;c=f}"string"==typeof b&&(b=a.splitLines(b));kb(a,{from:c,to:d,text:b,origin:e})}function uf(a,b,c,d){for(var e=0;e<a.length;++e){var f=a[e],g=!0;if(f.ranges)for(f.copied||(f=a[e]=f.deepCopy(),f.copied=!0),g=0;g<f.ranges.length;g++){var h=f.ranges[g].anchor,
+k=b;c<h.line?h.line+=d:k<h.line&&(h.line=k,h.ch=0);h=f.ranges[g].head;k=b;c<h.line?h.line+=d:k<h.line&&(h.line=k,h.ch=0)}else{for(h=0;h<f.changes.length;++h)if(k=f.changes[h],c<k.from.line)k.from=m(k.from.line+d,k.from.ch),k.to=m(k.to.line+d,k.to.ch);else if(b<=k.to.line){g=!1;break}g||(a.splice(0,e+1),e=0)}}}function sf(a,b){var c=b.from.line,d=b.to.line,e=b.text.length-(d-c)-1;uf(a.done,c,d,e);uf(a.undone,c,d,e)}function Kb(a,b,c,d){var e=b,f=b;return"number"==typeof b?f=w(a,Math.max(a.first,Math.min(b,
+a.first+a.size-1))):e=D(b),null==e?null:(d(f,e)&&a.cm&&Aa(a.cm,e,c),f)}function Lb(a){this.lines=a;this.parent=null;for(var b=0,c=0;c<a.length;++c)a[c].parent=this,b+=a[c].height;this.height=b}function Mb(a){this.children=a;for(var b=0,c=0,d=0;d<a.length;++d){var e=a[d],b=b+e.chunkSize(),c=c+e.height;e.parent=this}this.size=b;this.height=c;this.parent=null}function Gg(a,b,c,d){var e=new Nb(a,c,d),f=a.cm;return f&&e.noHScroll&&(f.display.alignWidgets=!0),Kb(a,b,"widget",function(b){var c=b.widgets||
+(b.widgets=[]);if(null==e.insertAt?c.push(e):c.splice(Math.min(c.length-1,Math.max(0,e.insertAt)),0,e),e.line=b,f&&!Ja(a,b))c=na(b)<a.scrollTop,la(b,b.height+zb(e)),c&&tc(f,e.height),f.curOp.forceUpdate=!0;return!0}),P(f,"lineWidgetAdded",f,e,"number"==typeof b?b:D(b)),e}function mb(a,b,c,d,e){if(d&&d.shared)return Hg(a,b,c,d,e);if(a.cm&&!a.cm.curOp)return L(a.cm,mb)(a,b,c,d,e);var f=new Ca(a,e);e=x(b,c);if(d&&Fa(d,f,!1),0<e||0==e&&!1!==f.clearWhenEmpty)return f;if(f.replacedWith&&(f.collapsed=!0,
+f.widgetNode=Za("span",[f.replacedWith],"CodeMirror-widget"),d.handleMouseEvents||f.widgetNode.setAttribute("cm-ignore-events","true"),d.insertLeft&&(f.widgetNode.insertLeft=!0)),f.collapsed){if($d(a,b.line,b,c,f)||b.line!=c.line&&$d(a,c.line,b,c,f))throw Error("Inserting collapsed marker partially overlapping an existing one");xa=!0}f.addToHistory&&ff(a,{from:b,to:c,origin:"markText"},a.sel,NaN);var g,h=b.line,k=a.cm;a.iter(h,c.line+1,function(a){k&&f.collapsed&&!k.options.lineWrapping&&ma(a)==k.display.maxLine&&
+(g=!0);f.collapsed&&h!=b.line&&la(a,0);var d=new jc(f,h==b.line?b.ch:null,h==c.line?c.ch:null);a.markedSpans=a.markedSpans?a.markedSpans.concat([d]):[d];d.marker.attachLine(a);++h});f.collapsed&&a.iter(b.line,c.line+1,function(b){Ja(a,b)&&la(b,0)});f.clearOnEnter&&t(f,"beforeCursorEnter",function(){return f.clear()});f.readOnly&&(qf=!0,(a.history.done.length||a.history.undone.length)&&a.clearHistory());if(f.collapsed&&(f.id=++vf,f.atomic=!0),k){if(g&&(k.curOp.updateMaxLine=!0),f.collapsed)V(k,b.line,
+c.line+1);else if(f.className||f.title||f.startStyle||f.endStyle||f.css)for(d=b.line;d<=c.line;d++)Aa(k,d,"text");f.atomic&&mf(k.doc);P(k,"markerAdded",k,f)}return f}function Hg(a,b,c,d,e){d=Fa(d);d.shared=!1;var f=[mb(a,b,c,d,e)],g=f[0],h=d.widgetNode;return Va(a,function(a){h&&(d.widgetNode=h.cloneNode(!0));f.push(mb(a,v(a,b),v(a,c),d,e));for(var l=0;l<a.linked.length;++l)if(a.linked[l].isParent)return;g=z(f)}),new Ob(f,g)}function wf(a){return a.findMarks(m(a.first,0),a.clipPos(m(a.lastLine())),
+function(a){return a.parent})}function Ig(a){for(var b=0;b<a.length;b++)!function(b){b=a[b];var d=[b.primary.doc];Va(b.primary.doc,function(a){return d.push(a)});for(var e=0;e<b.markers.length;e++){var f=b.markers[e];-1==N(d,f.doc)&&(f.parent=null,b.markers.splice(e--,1))}}(b)}function Jg(a){var b=this;if(xf(b),!K(b,a)&&!ta(b.display,a)){S(a);C&&(yf=+new Date);var c=Qa(b,a,!0),d=a.dataTransfer.files;if(c&&!b.isReadOnly())if(d&&d.length&&window.FileReader&&window.File)for(var e=d.length,f=Array(e),
+g=0,h=0;h<e;++h)!function(a,d){if(!b.options.allowDropFileTypes||-1!=N(b.options.allowDropFileTypes,a.type)){var h=new FileReader;h.onload=L(b,function(){var a=h.result;if(/[\x00-\x08\x0e-\x1f]{2}/.test(a)&&(a=""),f[d]=a,++g==e)c=v(b.doc,c),a={from:c,to:c,text:b.doc.splitLines(f.join(b.doc.lineSeparator())),origin:"paste"},kb(b.doc,a),jf(b.doc,ua(c,Ba(a)))});h.readAsText(a)}}(d[h],h);else{if(b.state.draggingText&&-1<b.doc.sel.contains(c))return b.state.draggingText(a),void setTimeout(function(){return b.display.input.focus()},
+20);try{if(h=a.dataTransfer.getData("Text")){var k;if(b.state.draggingText&&!b.state.draggingText.copy&&(k=b.listSelections()),Bc(b.doc,ua(c,c)),k)for(d=0;d<k.length;++d)lb(b.doc,"",k[d].anchor,k[d].head,"drag");b.replaceSelection(h,"around","paste");b.display.input.focus()}}catch(l){}}}}function xf(a){a.display.dragCursor&&(a.display.lineSpace.removeChild(a.display.dragCursor),a.display.dragCursor=null)}function zf(a){if(document.getElementsByClassName)for(var b=document.getElementsByClassName("CodeMirror"),
+c=0;c<b.length;c++){var d=b[c].CodeMirror;d&&a(d)}}function Kg(){var a;t(window,"resize",function(){null==a&&(a=setTimeout(function(){a=null;zf(Lg)},100))});t(window,"blur",function(){return zf(Db)})}function Lg(a){var b=a.display;b.lastWrapHeight==b.wrapper.clientHeight&&b.lastWrapWidth==b.wrapper.clientWidth||(b.cachedCharWidth=b.cachedTextHeight=b.cachedPaddingH=null,b.scrollbarsClipped=!1,a.setSize())}function Mg(a){var b=a.split(/-(?!$)/);a=b[b.length-1];for(var c,d,e,f,g=0;g<b.length-1;g++){var h=
+b[g];if(/^(cmd|meta|m)$/i.test(h))f=!0;else if(/^a(lt)?$/i.test(h))c=!0;else if(/^(c|ctrl|control)$/i.test(h))d=!0;else{if(!/^s(hift)?$/i.test(h))throw Error("Unrecognized modifier name: "+h);e=!0}}return c&&(a="Alt-"+a),d&&(a="Ctrl-"+a),f&&(a="Cmd-"+a),e&&(a="Shift-"+a),a}function Ng(a){var b={},c;for(c in a)if(a.hasOwnProperty(c)){var d=a[c];if(!/^(name|fallthrough|(de|at)tach)$/.test(c)){if("..."!=d)for(var e=ec(c.split(" "),Mg),f=0;f<e.length;f++){var g=void 0,h=void 0;f==e.length-1?(h=e.join(" "),
+g=d):(h=e.slice(0,f+1).join(" "),g="...");var k=b[h];if(k){if(k!=g)throw Error("Inconsistent bindings for "+h);}else b[h]=g}delete a[c]}}for(var l in b)a[l]=b[l];return a}function nb(a,b,c,d){b=Dc(b);var e=b.call?b.call(a,d):b[a];if(!1===e)return"nothing";if("..."===e)return"multi";if(null!=e&&c(e))return"handled";if(b.fallthrough){if("[object Array]"!=Object.prototype.toString.call(b.fallthrough))return nb(a,b.fallthrough,c,d);for(e=0;e<b.fallthrough.length;e++){var f=nb(a,b.fallthrough[e],c,d);
+if(f)return f}}}function Af(a){a="string"==typeof a?a:Da[a.keyCode];return"Ctrl"==a||"Alt"==a||"Shift"==a||"Mod"==a}function Bf(a,b,c){var d=a;return b.altKey&&"Alt"!=d&&(a="Alt-"+a),(Cf?b.metaKey:b.ctrlKey)&&"Ctrl"!=d&&(a="Ctrl-"+a),(Cf?b.ctrlKey:b.metaKey)&&"Cmd"!=d&&(a="Cmd-"+a),!c&&b.shiftKey&&"Shift"!=d&&(a="Shift-"+a),a}function Df(a,b){if(ja&&34==a.keyCode&&a["char"])return!1;var c=Da[a.keyCode];return null!=c&&!a.altGraphKey&&Bf(c,a,b)}function Dc(a){return"string"==typeof a?Pb[a]:a}function ob(a,
+b){for(var c=a.doc.sel.ranges,d=[],e=0;e<c.length;e++){for(var f=b(c[e]);d.length&&0>=x(f.from,z(d).to);){var g=d.pop();if(0>x(g.from,f.from)){f.from=g.from;break}}d.push(f)}Y(a,function(){for(var b=d.length-1;0<=b;b--)lb(a.doc,"",d[b].from,d[b].to,"+delete");fb(a)})}function Ef(a,b){var c=w(a.doc,b),d=ma(c);return d!=c&&(b=D(d)),$c(!0,a,d,b,1)}function Ff(a,b){var c=Ef(a,b.line),d=w(a.doc,c.line),e=ya(d,a.doc.direction);return e&&0!=e[0].level?c:(d=Math.max(0,d.text.search(/\S/)),m(c.line,b.line==
+c.line&&b.ch<=d&&b.ch?0:d,c.sticky))}function Ec(a,b,c){if("string"==typeof b&&!(b=Qb[b]))return!1;a.display.input.ensurePolled();var d=a.display.shift,e=!1;try{a.isReadOnly()&&(a.state.suppressEdits=!0),c&&(a.display.shift=!1),e=b(a)!=Fc}finally{a.display.shift=d,a.state.suppressEdits=!1}return e}function Og(a,b,c){for(var d=0;d<a.state.keyMaps.length;d++){var e=nb(b,a.state.keyMaps[d],c,a);if(e)return e}return a.options.extraKeys&&nb(b,a.options.extraKeys,c,a)||nb(b,a.options.keyMap,c,a)}function Rb(a,
+b,c,d){var e=a.state.keySeq;if(e){if(Af(b))return"handled";Pg.set(50,function(){a.state.keySeq==e&&(a.state.keySeq=null,a.display.input.reset())});b=e+" "+b}d=Og(a,b,d);return"multi"==d&&(a.state.keySeq=b),"handled"==d&&P(a,"keyHandled",a,b,c),"handled"!=d&&"multi"!=d||(S(c),rd(a)),e&&!d&&/\'$/.test(b)?(S(c),!0):!!d}function Gf(a,b){var c=Df(b,!0);return!!c&&(b.shiftKey&&!a.state.keySeq?Rb(a,"Shift-"+c,b,function(b){return Ec(a,b,!0)})||Rb(a,c,b,function(b){if("string"==typeof b?/^go[A-Z]/.test(b):
+b.motion)return Ec(a,b)}):Rb(a,c,b,function(b){return Ec(a,b)}))}function Qg(a,b,c){return Rb(a,"'"+c+"'",b,function(b){return Ec(a,b,!0)})}function Hf(a){if(this.curOp.focus=qa(),!K(this,a)){C&&11>B&&27==a.keyCode&&(a.returnValue=!1);var b=a.keyCode;this.display.shift=16==b||a.shiftKey;var c=Gf(this,a);ja&&(Hd=c?b:null,!c&&88==b&&!Rg&&(ha?a.metaKey:a.ctrlKey)&&this.replaceSelection("",null,"cut"));18!=b||/\bCodeMirror-crosshair\b/.test(this.display.lineDiv.className)||Sg(this)}}function Sg(a){function b(a){18!=
+a.keyCode&&a.altKey||(Ra(c,"CodeMirror-crosshair"),aa(document,"keyup",b),aa(document,"mouseover",b))}var c=a.display.lineDiv;Ea(c,"CodeMirror-crosshair");t(document,"keyup",b);t(document,"mouseover",b)}function If(a){16==a.keyCode&&(this.doc.sel.shift=!1);K(this,a)}function Jf(a){if(!(ta(this.display,a)||K(this,a)||a.ctrlKey&&!a.altKey||ha&&a.metaKey)){var b=a.keyCode,c=a.charCode;if(ja&&b==Hd)return Hd=null,void S(a);ja&&(!a.which||10>a.which)&&Gf(this,a)||(b=String.fromCharCode(null==c?b:c),"\b"!=
+b&&(Qg(this,a,b)||this.display.input.onKeyPress(a)))}}function Tg(a,b){var c=+new Date;return Sb&&Sb.compare(c,a,b)?(Tb=Sb=null,"triple"):Tb&&Tb.compare(c,a,b)?(Sb=new Id(c,a,b),Tb=null,"double"):(Tb=new Id(c,a,b),Sb=null,"single")}function Kf(a){var b=this.display;if(!(K(this,a)||b.activeTouch&&b.input.supportsTouch())){if(b.input.ensurePolled(),b.shift=a.shiftKey,ta(b,a))return void(R||(b.scroller.draggable=!1,setTimeout(function(){return b.scroller.draggable=!0},100)));if(!Jd(this,a,"gutterClick",
+!0)){var c=Qa(this,a),d=fe(a),e=c?Tg(c,d):"single";window.focus();1==d&&this.state.selectingText&&this.state.selectingText(a);c&&Ug(this,d,c,e,a)||(1==d?c?Vg(this,c,e,a):(a.target||a.srcElement)==b.scroller&&S(a):2==d?(c&&Ac(this.doc,c),setTimeout(function(){return b.input.focus()},20)):3==d&&(Kd?Lf(this,a):Le(this)))}}}function Ug(a,b,c,d,e){var f="Click";return"double"==d?f="Double"+f:"triple"==d&&(f="Triple"+f),f=(1==b?"Left":2==b?"Middle":"Right")+f,Rb(a,Bf(f,e),e,function(b){if("string"==typeof b&&
+(b=Qb[b]),!b)return!1;var d=!1;try{a.isReadOnly()&&(a.state.suppressEdits=!0),d=b(a,c)!=Fc}finally{a.state.suppressEdits=!1}return d})}function Vg(a,b,c,d){C?setTimeout(Kc(Ke,a),0):a.curOp.focus=qa();var e,f;f=(f=a.getOption("configureMouse"))?f(a,c,d):{};null==f.unit&&(f.unit=(Wg?d.shiftKey&&d.metaKey:d.altKey)?"rectangle":"single"==c?"char":"double"==c?"word":"line");f=((null==f.extend||a.doc.extend)&&(f.extend=a.doc.extend||d.shiftKey),null==f.addNew&&(f.addNew=ha?d.metaKey:d.ctrlKey),null==f.moveOnDrag&&
+(f.moveOnDrag=!(ha?d.altKey:d.ctrlKey)),f);var g=a.doc.sel;a.options.dragDrop&&Xg&&!a.isReadOnly()&&"single"==c&&-1<(e=g.contains(b))&&(0>x((e=g.ranges[e]).from(),b)||0<b.xRel)&&(0<x(e.to(),b)||0>b.xRel)?Yg(a,d,b,f):Zg(a,d,b,f)}function Yg(a,b,c,d){var e=a.display,f=!1,g=L(a,function(b){R&&(e.scroller.draggable=!1);a.state.draggingText=!1;aa(document,"mouseup",g);aa(document,"mousemove",h);aa(e.scroller,"dragstart",k);aa(e.scroller,"drop",g);f||(S(b),d.addNew||Ac(a.doc,c,null,null,d.extend),R||C&&
+9==B?setTimeout(function(){document.body.focus();e.input.focus()},20):e.input.focus())}),h=function(a){f=f||10<=Math.abs(b.clientX-a.clientX)+Math.abs(b.clientY-a.clientY)},k=function(){return f=!0};R&&(e.scroller.draggable=!0);a.state.draggingText=g;g.copy=!d.moveOnDrag;e.scroller.dragDrop&&e.scroller.dragDrop();t(document,"mouseup",g);t(document,"mousemove",h);t(e.scroller,"dragstart",k);t(e.scroller,"drop",g);Le(a);setTimeout(function(){return e.input.focus()},20)}function Mf(a,b,c){if("char"==
+c)return new A(b,b);if("word"==c)return a.findWordAt(b);if("line"==c)return new A(m(b.line,0),v(a.doc,m(b.line+1,0)));a=c(a,b);return new A(a.from,a.to)}function Zg(a,b,c,d){function e(b){if(0!=x(u,b))if(u=b,"rectangle"==d.unit){for(var e=[],f=a.options.tabSize,g=ea(w(k,c.line).text,c.ch,f),h=ea(w(k,b.line).text,b.ch,f),p=Math.min(g,h),g=Math.max(g,h),h=Math.min(c.line,b.line),O=Math.min(a.lastLine(),Math.max(c.line,b.line));h<=O;h++){var r=w(k,h).text,t=Lc(r,p,f);p==g?e.push(new A(m(h,t),m(h,t))):
+r.length>t&&e.push(new A(m(h,t),m(h,Lc(r,g,f))))}e.length||e.push(new A(c,c));Q(k,ka(q.ranges.slice(0,n).concat(e),n),{origin:"*mouse",scroll:!1});a.scrollIntoView(b)}else f=l,p=Mf(a,b,d.unit),b=f.anchor,0<x(p.anchor,b)?(e=p.head,b=ic(f.from(),p.anchor)):(e=p.anchor,b=hc(f.to(),p.head)),f=q.ranges.slice(0),f[n]=new A(v(k,b),e),Q(k,ka(f,n),Ld)}function f(b){var c=++r,g=Qa(a,b,!0,"rectangle"==d.unit);if(g)if(0!=x(g,u)){a.curOp.focus=qa();e(g);var l=td(h,k);(g.line>=l.to||g.line<l.from)&&setTimeout(L(a,
+function(){r==c&&f(b)}),150)}else{var n=b.clientY<O.top?-20:b.clientY>O.bottom?20:0;n&&setTimeout(L(a,function(){r==c&&(h.scroller.scrollTop+=n,f(b))}),50)}}function g(b){a.state.selectingText=!1;r=1/0;S(b);h.input.focus();aa(document,"mousemove",J);aa(document,"mouseup",y);k.history.lastSelOrigin=null}var h=a.display,k=a.doc;S(b);var l,n,q=k.sel,p=q.ranges;(d.addNew&&!d.extend?(n=k.sel.contains(c),l=-1<n?p[n]:new A(c,c)):(l=k.sel.primary(),n=k.sel.primIndex),"rectangle"==d.unit)?(d.addNew||(l=new A(c,
+c)),c=Qa(a,b,!0,!0),n=-1):(b=Mf(a,c,d.unit),l=d.extend?Ed(l,b.anchor,b.head,d.extend):b);d.addNew?-1==n?(n=p.length,Q(k,ka(p.concat([l]),n),{scroll:!1,origin:"*mouse"})):1<p.length&&p[n].empty()&&"char"==d.unit&&!d.extend?(Q(k,ka(p.slice(0,n).concat(p.slice(n+1)),0),{scroll:!1,origin:"*mouse"}),q=k.sel):Fd(k,n,l,Ld):(n=0,Q(k,new ca([l],0),Ld),q=k.sel);var u=c,O=h.wrapper.getBoundingClientRect(),r=0,J=L(a,function(a){fe(a)?f(a):g(a)}),y=L(a,g);a.state.selectingText=y;t(document,"mousemove",J);t(document,
+"mouseup",y)}function Jd(a,b,c,d){var e,f;try{e=b.clientX,f=b.clientY}catch(g){return!1}if(e>=Math.floor(a.display.gutters.getBoundingClientRect().right))return!1;d&&S(b);d=a.display;var h=d.lineDiv.getBoundingClientRect();if(f>h.bottom||!fa(a,c))return ad(b);f-=h.top-d.viewOffset;for(h=0;h<a.options.gutters.length;++h){var k=d.gutters.childNodes[h];if(k&&k.getBoundingClientRect().right>=e)return F(a,c,a,Ha(a.doc,f),a.options.gutters[h],b),ad(b)}}function Lf(a,b){var c;(c=ta(a.display,b))||(c=!!fa(a,
+"gutterContextMenu")&&Jd(a,b,"gutterContextMenu",!1));c||K(a,b,"contextmenu")||a.display.input.onContextMenu(b)}function Nf(a){a.display.wrapper.className=a.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+a.options.theme.replace(/(^|\s)\s*/g," cm-s-");Bb(a)}function Ub(a){We(a);V(a);Ne(a)}function $g(a,b,c){!b!=!(c&&c!=pb)&&(c=a.display.dragFunctions,b=b?t:aa,b(a.display.scroller,"dragstart",c.start),b(a.display.scroller,"dragenter",c.enter),b(a.display.scroller,"dragover",c.over),b(a.display.scroller,
+"dragleave",c.leave),b(a.display.scroller,"drop",c.drop))}function ah(a){a.options.lineWrapping?(Ea(a.display.wrapper,"CodeMirror-wrap"),a.display.sizer.style.minWidth="",a.display.sizerWidth=null):(Ra(a.display.wrapper,"CodeMirror-wrap"),Wc(a));qd(a);V(a);Bb(a);setTimeout(function(){return gb(a)},100)}function E(a,b){var c=this;if(!(this instanceof E))return new E(a,b);this.options=b=b?Fa(b):{};Fa(Of,b,!1);zd(b);var d=b.value;"string"==typeof d&&(d=new W(d,b.mode,null,b.lineSeparator,b.direction));
+this.doc=d;var e=new E.inputStyles[b.inputStyle](this),e=this.display=new eg(a,d,e);e.wrapper.CodeMirror=this;We(this);Nf(this);b.lineWrapping&&(this.display.wrapper.className+=" CodeMirror-wrap");Se(this);this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,delayingBlurEvent:!1,focused:!1,suppressEdits:!1,pasteIncoming:!1,cutIncoming:!1,selectingText:!1,draggingText:!1,highlight:new Wa,keySeq:null,specialChars:null};b.autofocus&&!rb&&e.input.focus();C&&11>B&&setTimeout(function(){return c.display.input.reset(!0)},
+20);bh(this);Pf||(Kg(),Pf=!0);Ta(this);this.curOp.forceUpdate=!0;bf(this,d);b.autofocus&&!rb||this.hasFocus()?setTimeout(Kc(sd,this),20):Db(this);for(var f in Gc)Gc.hasOwnProperty(f)&&Gc[f](c,b[f],pb);Oe(this);b.finishInit&&b.finishInit(this);for(d=0;d<Md.length;++d)Md[d](c);Ua(this);R&&b.lineWrapping&&"optimizelegibility"==getComputedStyle(e.lineDiv).textRendering&&(e.lineDiv.style.textRendering="auto")}function bh(a){function b(){d.activeTouch&&(e=setTimeout(function(){return d.activeTouch=null},
+1E3),f=d.activeTouch,f.end=+new Date)}function c(a,b){if(null==b.left)return!0;var c=b.left-a.left,d=b.top-a.top;return 400<c*c+d*d}var d=a.display;t(d.scroller,"mousedown",L(a,Kf));C&&11>B?t(d.scroller,"dblclick",L(a,function(b){if(!K(a,b)){var c=Qa(a,b);!c||Jd(a,b,"gutterClick",!0)||ta(a.display,b)||(S(b),b=a.findWordAt(c),Ac(a.doc,b.anchor,b.head))}})):t(d.scroller,"dblclick",function(b){return K(a,b)||S(b)});Kd||t(d.scroller,"contextmenu",function(b){return Lf(a,b)});var e,f={end:0};t(d.scroller,
+"touchstart",function(b){var c;if(c=!K(a,b))1!=b.touches.length?c=!1:(c=b.touches[0],c=1>=c.radiusX&&1>=c.radiusY),c=!c;c&&(d.input.ensurePolled(),clearTimeout(e),c=+new Date,d.activeTouch={start:c,moved:!1,prev:300>=c-f.end?f:null},1==b.touches.length&&(d.activeTouch.left=b.touches[0].pageX,d.activeTouch.top=b.touches[0].pageY))});t(d.scroller,"touchmove",function(){d.activeTouch&&(d.activeTouch.moved=!0)});t(d.scroller,"touchend",function(e){var f=d.activeTouch;if(f&&!ta(d,e)&&null!=f.left&&!f.moved&&
+300>new Date-f.start){var g=a.coordsChar(d.activeTouch,"page"),f=!f.prev||c(f,f.prev)?new A(g,g):!f.prev.prev||c(f,f.prev.prev)?a.findWordAt(g):new A(m(g.line,0),v(a.doc,m(g.line+1,0)));a.setSelection(f.anchor,f.head);a.focus();S(e)}b()});t(d.scroller,"touchcancel",b);t(d.scroller,"scroll",function(){d.scroller.clientHeight&&(Fb(a,d.scroller.scrollTop),Sa(a,d.scroller.scrollLeft,!0),F(a,"scroll",a))});t(d.scroller,"mousewheel",function(b){return Ye(a,b)});t(d.scroller,"DOMMouseScroll",function(b){return Ye(a,
+b)});t(d.wrapper,"scroll",function(){return d.wrapper.scrollTop=d.wrapper.scrollLeft=0});d.dragFunctions={enter:function(b){K(a,b)||vb(b)},over:function(b){if(!K(a,b)){var c=Qa(a,b);if(c){var d=document.createDocumentFragment();Je(a,c,d);a.display.dragCursor||(a.display.dragCursor=r("div",null,"CodeMirror-cursors CodeMirror-dragcursors"),a.display.lineSpace.insertBefore(a.display.dragCursor,a.display.cursorDiv));Z(a.display.dragCursor,d)}vb(b)}},start:function(b){if(C&&(!a.state.draggingText||100>
++new Date-yf))b=void vb(b);else{if(!K(a,b)&&!ta(a.display,b)&&(b.dataTransfer.setData("Text",a.getSelection()),b.dataTransfer.effectAllowed="copyMove",b.dataTransfer.setDragImage&&!Qf)){var c=r("img",null,null,"position: fixed; left: 0; top: 0;");c.src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw\x3d\x3d";ja&&(c.width=c.height=1,a.display.wrapper.appendChild(c),c._top=c.offsetTop);b.dataTransfer.setDragImage(c,0,0);ja&&c.parentNode.removeChild(c)}b=void 0}return b},drop:L(a,
+Jg),leave:function(b){K(a,b)||xf(a)}};var g=d.input.getField();t(g,"keyup",function(b){return If.call(a,b)});t(g,"keydown",L(a,Hf));t(g,"keypress",L(a,Jf));t(g,"focus",function(b){return sd(a,b)});t(g,"blur",function(b){return Db(a,b)})}function Vb(a,b,c,d){var e,f=a.doc;null==c&&(c="add");"smart"==c&&(f.mode.indent?e=wb(a,b).state:c="prev");var g=a.options.tabSize,h=w(f,b),k=ea(h.text,null,g);h.stateAfter&&(h.stateAfter=null);var l,n=h.text.match(/^\s*/)[0];if(d||/\S/.test(h.text)){if("smart"==c&&
+((l=f.mode.indent(e,h.text.slice(n.length),h.text))==Fc||150<l)){if(!d)return;c="prev"}}else l=0,c="not";"prev"==c?l=b>f.first?ea(w(f,b-1).text,null,g):0:"add"==c?l=k+a.options.indentUnit:"subtract"==c?l=k-a.options.indentUnit:"number"==typeof c&&(l=k+c);l=Math.max(0,l);c="";d=0;if(a.options.indentWithTabs)for(a=Math.floor(l/g);a;--a)d+=g,c+="\t";if(d<l&&(c+=Mc(l-d)),c!=n)return lb(f,c,m(b,0),m(b,n.length),"+input"),h.stateAfter=null,!0;for(g=0;g<f.sel.ranges.length;g++)if(h=f.sel.ranges[g],h.head.line==
+b&&h.head.ch<n.length){b=m(b,n.length);Fd(f,g,new A(b,b));break}}function Rf(a){da=a}function Nd(a,b,c,d,e){var f=a.doc;a.display.shift=!1;d||(d=f.sel);var g=a.state.pasteIncoming||"paste"==e,h=Od(b),k=null;if(g&&1<d.ranges.length)if(da&&da.text.join("\n")==b){if(0==d.ranges.length%da.text.length)for(var k=[],l=0;l<da.text.length;l++)k.push(f.splitLines(da.text[l]))}else h.length==d.ranges.length&&a.options.pasteLinesPerSelection&&(k=ec(h,function(a){return[a]}));for(var n,l=d.ranges.length-1;0<=
+l;l--){n=d.ranges[l];var q=n.from(),p=n.to();n.empty()&&(c&&0<c?q=m(q.line,q.ch-c):a.state.overwrite&&!g?p=m(p.line,Math.min(w(f,p.line).text.length,p.ch+z(h).length)):da&&da.lineWise&&da.text.join("\n")==b&&(q=p=m(q.line,0)));n=a.curOp.updateInput;q={from:q,to:p,text:k?k[l%k.length]:h,origin:e||(g?"paste":a.state.cutIncoming?"cut":"+input")};kb(a.doc,q);P(a,"inputRead",a,q)}b&&!g&&Sf(a,b);fb(a);a.curOp.updateInput=n;a.curOp.typing=!0;a.state.pasteIncoming=a.state.cutIncoming=!1}function Tf(a,b){var c=
+a.clipboardData&&a.clipboardData.getData("Text");if(c)return a.preventDefault(),b.isReadOnly()||b.options.disableInput||Y(b,function(){return Nd(b,c,0,null,"paste")}),!0}function Sf(a,b){if(a.options.electricChars&&a.options.smartIndent)for(var c=a.doc.sel,d=c.ranges.length-1;0<=d;d--){var e=c.ranges[d];if(!(100<e.head.ch||d&&c.ranges[d-1].head.line==e.head.line)){var f=a.getModeAt(e.head),g=!1;if(f.electricChars)for(var h=0;h<f.electricChars.length;h++){if(-1<b.indexOf(f.electricChars.charAt(h))){g=
+Vb(a,e.head.line,"smart");break}}else f.electricInput&&f.electricInput.test(w(a.doc,e.head.line).text.slice(0,e.head.ch))&&(g=Vb(a,e.head.line,"smart"));g&&P(a,"electricInput",a,e.head.line)}}}function Uf(a){for(var b=[],c=[],d=0;d<a.doc.sel.ranges.length;d++){var e=a.doc.sel.ranges[d].head.line,e={anchor:m(e,0),head:m(e+1,0)};c.push(e);b.push(a.getRange(e.anchor,e.head))}return{text:b,ranges:c}}function Vf(a,b){a.setAttribute("autocorrect","off");a.setAttribute("autocapitalize","off");a.setAttribute("spellcheck",
+!!b)}function Wf(){var a=r("textarea",null,null,"position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; outline: none"),b=r("div",[a],null,"overflow: hidden; position: relative; width: 3px; height: 0px;");return R?a.style.width="1000px":a.setAttribute("wrap","off"),Wb&&(a.style.border="1px solid black"),Vf(a),b}function Pd(a,b,c,d,e){function f(d){var f;if(null==(f=e?be(a.cm,k,b,c):Zc(k,b,c))){d||(d=b.line+c,d=!(!(d<a.first||d>=a.first+a.size)&&(b=new m(d,b.ch,b.sticky),k=w(a,d))));
+if(d)return!1;b=$c(e,a.cm,k,b.line,c)}else b=f;return!0}var g=b,h=c,k=w(a,b.line);if("char"==d)f();else if("column"==d)f(!0);else if("word"==d||"group"==d){var l=null;d="group"==d;for(var n=a.cm&&a.cm.getHelper(b,"wordChars"),q=!0;!(0>c)||f(!q);q=!1){var p=k.text.charAt(b.ch)||"\n",p=fc(p,n)?"w":d&&"\n"==p?"n":!d||/\s/.test(p)?null:"p";if(!d||q||p||(p="s"),l&&l!=p){0>c&&(c=1,f(),b.sticky="after");break}if(p&&(l=p),0<c&&!f(!q))break}}h=Gd(a,b,g,h,!0);return Rc(g,h)&&(h.hitSide=!0),h}function Xf(a,
+b,c,d){var e,f=a.doc,g=b.left;"page"==d?(d=Math.min(a.display.wrapper.clientHeight,window.innerHeight||document.documentElement.clientHeight),d=Math.max(d-.5*Oa(a.display),3),e=(0<c?b.bottom:b.top)+c*d):"line"==d&&(e=0<c?b.bottom+3:b.top-3);for(var h;h=od(a,g,e),h.outside;){if(0>c?0>=e:e>=f.height){h.hitSide=!0;break}e+=5*c}return h}function Yf(a,b){var c=ld(a,b.line);if(!c||c.hidden)return null;var d=w(a.doc,b.line),c=ye(c,d,b.line),d=ya(d,a.doc.direction),e="left";d&&(e=Xc(d,b.ch)%2?"right":"left");
+c=ze(c.map,b.ch,e);return c.offset="right"==c.collapse?c.end:c.start,c}function ch(a){for(;a;a=a.parentNode)if(/CodeMirror-gutter-wrapper/.test(a.className))return!0;return!1}function qb(a,b){return b&&(a.bad=!0),a}function dh(a,b,c,d,e){function f(a){return function(b){return b.id==a}}function g(){l&&(k+=n,l=!1)}function h(b){if(1==b.nodeType){var c=b.getAttribute("cm-text");if(null!=c)(b=c||b.textContent.replace(/\u200b/g,""))&&(g(),k+=b);else{var u;if(c=b.getAttribute("cm-marker"))b=a.findMarks(m(d,
+0),m(e+1,0),f(+c)),b.length&&(u=b[0].find())&&(b=Ga(a.doc,u.from,u.to).join(n))&&(g(),k+=b);else if("false"!=b.getAttribute("contenteditable")){(u=/^(pre|div|p)$/i.test(b.nodeName))&&g();for(c=0;c<b.childNodes.length;c++)h(b.childNodes[c]);u&&(l=!0)}}}else 3==b.nodeType&&(b=b.nodeValue)&&(g(),k+=b)}for(var k="",l=!1,n=a.doc.lineSeparator();h(b),b!=c;)b=b.nextSibling;return k}function Hc(a,b,c){var d;if(b==a.display.lineDiv){if(!(d=a.display.lineDiv.childNodes[c]))return qb(a.clipPos(m(a.display.viewTo-
+1)),!0);b=null;c=0}else for(d=b;;d=d.parentNode){if(!d||d==a.display.lineDiv)return null;if(d.parentNode&&d.parentNode==a.display.lineDiv)break}for(var e=0;e<a.display.view.length;e++){var f=a.display.view[e];if(f.node==d)return eh(f,b,c)}}function eh(a,b,c){function d(b,c,d){for(var e=-1;e<(l?l.length:0);e++)for(var f=0>e?k.map:l[e],g=0;g<f.length;g+=3){var h=f[g+2];if(h==b||h==c)return c=D(0>e?a.line:a.rest[e]),e=f[g]+d,(0>d||h!=b)&&(e=f[g+(d?1:0)]),m(c,e)}}var e=a.text.firstChild,f=!1;if(!b||!va(e,
+b))return qb(m(D(a.line),0),!0);if(b==e&&(f=!0,b=e.childNodes[c],c=0,!b))return c=a.rest?z(a.rest):a.line,qb(m(D(c),c.text.length),f);var g=3==b.nodeType?b:null,h=b;for(g||1!=b.childNodes.length||3!=b.firstChild.nodeType||(g=b.firstChild,c&&(c=g.nodeValue.length));h.parentNode!=e;)h=h.parentNode;var k=a.measure,l=k.maps;if(b=d(g,h,c))return qb(b,f);e=h.nextSibling;for(g=g?g.nodeValue.length-c:0;e;e=e.nextSibling){if(b=d(e,e.firstChild,0))return qb(m(b.line,b.ch-g),f);g+=e.textContent.length}for(h=
+h.previousSibling;h;h=h.previousSibling){if(b=d(h,h.firstChild,-1))return qb(m(b.line,b.ch+c),f);c+=h.textContent.length}}var U=navigator.userAgent,Zf=navigator.platform,wa=/gecko\/\d/i.test(U),$f=/MSIE \d/.test(U),ag=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(U),Xb=/Edge\/(\d+)/.exec(U),C=$f||ag||Xb,B=C&&($f?document.documentMode||6:+(Xb||ag)[1]),R=!Xb&&/WebKit\//.test(U),fh=R&&/Qt\/\d+\.\d+/.test(U),pc=!Xb&&/Chrome\//.test(U),ja=/Opera\//.test(U),Qf=/Apple Computer/.test(navigator.vendor),gh=
+/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(U),yg=/PhantomJS/.test(U),Wb=!Xb&&/AppleWebKit/.test(U)&&/Mobile\/\w+/.test(U),qc=/Android/.test(U),rb=Wb||qc||/webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(U),ha=Wb||/Mac/.test(Zf),Wg=/\bCrOS\b/.test(U),hh=/win/i.test(Zf),Xa=ja&&U.match(/Version\/(\d*\.\d*)/);Xa&&(Xa=Number(Xa[1]));Xa&&15<=Xa&&(ja=!1,R=!0);var db,Cf=ha&&(fh||ja&&(null==Xa||12.11>Xa)),Kd=wa||C&&9<=B,Ra=function(a,b){var c=a.className,d=ga(b).exec(c);if(d){var e=c.slice(d.index+d[0].length);
+a.className=c.slice(0,d.index)+(e?d[1]+e:"")}};db=document.createRange?function(a,b,c,d){var e=document.createRange();return e.setEnd(d||a,c),e.setStart(a,b),e}:function(a,b,c){var d=document.body.createTextRange();try{d.moveToElementText(a.parentNode)}catch(e){return d}return d.collapse(!0),d.moveEnd("character",c),d.moveStart("character",b),d};var Yb=function(a){a.select()};Wb?Yb=function(a){a.selectionStart=0;a.selectionEnd=a.value.length}:C&&(Yb=function(a){try{a.select()}catch(b){}});var Wa=
+function(){this.id=null};Wa.prototype.set=function(a,b){clearTimeout(this.id);this.id=setTimeout(b,a)};var bd,hd,Ud=30,Fc={toString:function(){return"CodeMirror.Pass"}},pa={scroll:!1},Ld={origin:"*mouse"},Zb={origin:"+move"},dc=[""],cg=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/,dg=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/,
+qf=!1,xa=!1,ub=null,hg=function(){function a(a){return 247>=a?c.charAt(a):1424<=a&&1524>=a?"R":1536<=a&&1785>=a?d.charAt(a-1536):1774<=a&&2220>=a?"r":8192<=a&&8203>=a?"w":8204==a?"b":"L"}function b(a,b,c){this.level=a;this.from=b;this.to=c}var c="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN",d="nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111",
+e=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,f=/[stwN]/,g=/[LRr]/,h=/[Lb1n]/,k=/[1n]/;return function(c,d){var q="ltr"==d?"L":"R";if(0==c.length||"ltr"==d&&!e.test(c))return!1;for(var p=c.length,u=[],m=0;m<p;++m)u.push(a(c.charCodeAt(m)));for(var m=0,r=q;m<p;++m){var t=u[m];"m"==t?u[m]=r:r=t}m=0;for(r=q;m<p;++m)t=u[m],"1"==t&&"r"==r?u[m]="n":g.test(t)&&(r=t,"r"==t&&(u[m]="R"));m=1;for(r=u[0];m<p-1;++m)t=u[m],"+"==t&&"1"==r&&"1"==u[m+1]?u[m]="1":","!=t||r!=u[m+1]||"1"!=r&&"n"!=r||(u[m]=r),r=t;for(m=
+0;m<p;++m)if(r=u[m],","==r)u[m]="N";else if("%"==r){r=void 0;for(r=m+1;r<p&&"%"==u[r];++r);for(t=m&&"!"==u[m-1]||r<p&&"1"==u[r]?"1":"N";m<r;++m)u[m]=t;m=r-1}m=0;for(r=q;m<p;++m)t=u[m],"L"==r&&"1"==t?u[m]="L":g.test(t)&&(r=t);for(r=0;r<p;++r)if(f.test(u[r])){m=void 0;for(m=r+1;m<p&&f.test(u[m]);++m);t="L"==(r?u[r-1]:q);for(t=t==("L"==(m<p?u[m]:q))?t?"L":"R":q;r<m;++r)u[r]=t;r=m-1}for(var v,q=[],m=0;m<p;)if(h.test(u[m])){r=m;for(++m;m<p&&h.test(u[m]);++m);q.push(new b(0,r,m))}else{var w=m,r=q.length;
+for(++m;m<p&&"L"!=u[m];++m);for(t=w;t<m;)if(k.test(u[t])){w<t&&q.splice(r,0,new b(1,w,t));w=t;for(++t;t<m&&k.test(u[t]);++t);q.splice(r,0,new b(2,w,t));w=t}else++t;w<m&&q.splice(r,0,new b(1,w,m))}return 1==q[0].level&&(v=c.match(/^\s+/))&&(q[0].from=v[0].length,q.unshift(new b(0,0,v[0].length))),1==z(q).level&&(v=c.match(/\s+$/))&&(z(q).to-=v[0].length,q.push(new b(0,p-v[0].length,p))),"rtl"==d?q.reverse():q}}(),lc=[],t=function(a,b,c){a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent?a.attachEvent("on"+
+b,c):(a=a._handlers||(a._handlers={}),a[b]=(a[b]||lc).concat(c))},Xg=function(){if(C&&9>B)return!1;var a=r("div");return"draggable"in a||"dragDrop"in a}(),Od=3!="\n\nb".split(/\n/).length?function(a){for(var b=0,c=[],d=a.length;b<=d;){var e=a.indexOf("\n",b);-1==e&&(e=a.length);var f=a.slice(b,"\r"==a.charAt(e-1)?e-1:e),g=f.indexOf("\r");-1!=g?(c.push(f.slice(0,g)),b+=g+1):(c.push(f),b=e+1)}return c}:function(a){return a.split(/\r\n?|\n/)},ih=window.getSelection?function(a){try{return a.selectionStart!=
+a.selectionEnd}catch(b){return!1}}:function(a){var b;try{b=a.ownerDocument.selection.createRange()}catch(c){}return!(!b||b.parentElement()!=a)&&0!=b.compareEndPoints("StartToEnd",b)},Rg=function(){var a=r("div");return"oncopy"in a||(a.setAttribute("oncopy","return;"),"function"==typeof a.oncopy)}(),nd=null,cd={},bb={},cb={},H=function(a,b,c){this.pos=this.start=0;this.string=a;this.tabSize=b||8;this.lineStart=this.lastColumnPos=this.lastColumnValue=0;this.lineOracle=c};H.prototype.eol=function(){return this.pos>=
+this.string.length};H.prototype.sol=function(){return this.pos==this.lineStart};H.prototype.peek=function(){return this.string.charAt(this.pos)||void 0};H.prototype.next=function(){if(this.pos<this.string.length)return this.string.charAt(this.pos++)};H.prototype.eat=function(a){var b=this.string.charAt(this.pos);if("string"==typeof a?b==a:b&&(a.test?a.test(b):a(b)))return++this.pos,b};H.prototype.eatWhile=function(a){for(var b=this.pos;this.eat(a););return this.pos>b};H.prototype.eatSpace=function(){for(var a=
+this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>a};H.prototype.skipToEnd=function(){this.pos=this.string.length};H.prototype.skipTo=function(a){a=this.string.indexOf(a,this.pos);if(-1<a)return this.pos=a,!0};H.prototype.backUp=function(a){this.pos-=a};H.prototype.column=function(){return this.lastColumnPos<this.start&&(this.lastColumnValue=ea(this.string,this.start,this.tabSize,this.lastColumnPos,this.lastColumnValue),this.lastColumnPos=this.start),this.lastColumnValue-
+(this.lineStart?ea(this.string,this.lineStart,this.tabSize):0)};H.prototype.indentation=function(){return ea(this.string,null,this.tabSize)-(this.lineStart?ea(this.string,this.lineStart,this.tabSize):0)};H.prototype.match=function(a,b,c){if("string"!=typeof a)return(a=this.string.slice(this.pos).match(a))&&0<a.index?null:(a&&!1!==b&&(this.pos+=a[0].length),a);var d=function(a){return c?a.toLowerCase():a};if(d(this.string.substr(this.pos,a.length))==d(a))return!1!==b&&(this.pos+=a.length),!0};H.prototype.current=
+function(){return this.string.slice(this.start,this.pos)};H.prototype.hideFirstChars=function(a,b){this.lineStart+=a;try{return b()}finally{this.lineStart-=a}};H.prototype.lookAhead=function(a){var b=this.lineOracle;return b&&b.lookAhead(a)};var nc=function(a,b){this.state=a;this.lookAhead=b},sa=function(a,b,c,d){this.state=b;this.doc=a;this.line=c;this.maxLookAhead=d||0};sa.prototype.lookAhead=function(a){var b=this.doc.getLine(this.line+a);return null!=b&&a>this.maxLookAhead&&(this.maxLookAhead=
+a),b};sa.prototype.nextLine=function(){this.line++;0<this.maxLookAhead&&this.maxLookAhead--};sa.fromSaved=function(a,b,c){return b instanceof nc?new sa(a,La(a.mode,b.state),c,b.lookAhead):new sa(a,La(a.mode,b),c)};sa.prototype.save=function(a){a=!1!==a?La(this.doc.mode,this.state):this.state;return 0<this.maxLookAhead?new nc(a,this.maxLookAhead):a};var me=function(a,b,c){this.start=a.start;this.end=a.pos;this.string=a.current();this.type=b||null;this.state=c},hb=function(a,b,c){this.text=a;Yd(this,
+b);this.height=c?c(this):1};hb.prototype.lineNo=function(){return D(this)};ab(hb);var Pa,og={},ng={},eb=null,xb=null,Ae={left:0,right:0,top:0,bottom:0},Ya=function(a,b,c){this.cm=c;var d=this.vert=r("div",[r("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),e=this.horiz=r("div",[r("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");a(d);a(e);t(d,"scroll",function(){d.clientHeight&&b(d.scrollTop,"vertical")});t(e,"scroll",function(){e.clientWidth&&b(e.scrollLeft,"horizontal")});
+this.checkedZeroWidth=!1;C&&8>B&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")};Ya.prototype.update=function(a){var b=a.scrollWidth>a.clientWidth+1,c=a.scrollHeight>a.clientHeight+1,d=a.nativeBarWidth;c?(this.vert.style.display="block",this.vert.style.bottom=b?d+"px":"0",this.vert.firstChild.style.height=Math.max(0,a.scrollHeight-a.clientHeight+(a.viewHeight-(b?d:0)))+"px"):(this.vert.style.display="",this.vert.firstChild.style.height="0");b?(this.horiz.style.display="block",this.horiz.style.right=
+c?d+"px":"0",this.horiz.style.left=a.barLeft+"px",this.horiz.firstChild.style.width=Math.max(0,a.scrollWidth-a.clientWidth+(a.viewWidth-a.barLeft-(c?d:0)))+"px"):(this.horiz.style.display="",this.horiz.firstChild.style.width="0");return!this.checkedZeroWidth&&0<a.clientHeight&&(0==d&&this.zeroWidthHack(),this.checkedZeroWidth=!0),{right:c?d:0,bottom:b?d:0}};Ya.prototype.setScrollLeft=function(a){this.horiz.scrollLeft!=a&&(this.horiz.scrollLeft=a);this.disableHoriz&&this.enableZeroWidthBar(this.horiz,
+this.disableHoriz,"horiz")};Ya.prototype.setScrollTop=function(a){this.vert.scrollTop!=a&&(this.vert.scrollTop=a);this.disableVert&&this.enableZeroWidthBar(this.vert,this.disableVert,"vert")};Ya.prototype.zeroWidthHack=function(){this.horiz.style.height=this.vert.style.width=ha&&!gh?"12px":"18px";this.horiz.style.pointerEvents=this.vert.style.pointerEvents="none";this.disableHoriz=new Wa;this.disableVert=new Wa};Ya.prototype.enableZeroWidthBar=function(a,b,c){function d(){var e=a.getBoundingClientRect();
+("vert"==c?document.elementFromPoint(e.right-1,(e.top+e.bottom)/2):document.elementFromPoint((e.right+e.left)/2,e.bottom-1))!=a?a.style.pointerEvents="none":b.set(1E3,d)}a.style.pointerEvents="auto";b.set(1E3,d)};Ya.prototype.clear=function(){var a=this.horiz.parentNode;a.removeChild(this.horiz);a.removeChild(this.vert)};var $b=function(){};$b.prototype.update=function(){return{bottom:0,right:0}};$b.prototype.setScrollLeft=function(){};$b.prototype.setScrollTop=function(){};$b.prototype.clear=function(){};
+var Te={"native":Ya,"null":$b},xg=0,vc=function(a,b,c){var d=a.display;this.viewport=b;this.visible=td(d,a.doc,b);this.editorIsHidden=!d.wrapper.offsetWidth;this.wrapperHeight=d.wrapper.clientHeight;this.wrapperWidth=d.wrapper.clientWidth;this.oldDisplayWidth=Ma(a);this.force=c;this.dims=md(a);this.events=[]};vc.prototype.signal=function(a,b){fa(a,b)&&this.events.push(arguments)};vc.prototype.finish=function(){for(var a=0;a<this.events.length;a++)F.apply(null,this.events[a])};var xc=0,ba=null;C?ba=
+-.53:wa?ba=15:pc?ba=-.7:Qf&&(ba=-1/3);var ca=function(a,b){this.ranges=a;this.primIndex=b};ca.prototype.primary=function(){return this.ranges[this.primIndex]};ca.prototype.equals=function(a){if(a==this)return!0;if(a.primIndex!=this.primIndex||a.ranges.length!=this.ranges.length)return!1;for(var b=0;b<this.ranges.length;b++){var c=this.ranges[b],d=a.ranges[b];if(!Rc(c.anchor,d.anchor)||!Rc(c.head,d.head))return!1}return!0};ca.prototype.deepCopy=function(){for(var a=[],b=0;b<this.ranges.length;b++)a[b]=
+new A(Sc(this.ranges[b].anchor),Sc(this.ranges[b].head));return new ca(a,this.primIndex)};ca.prototype.somethingSelected=function(){for(var a=0;a<this.ranges.length;a++)if(!this.ranges[a].empty())return!0;return!1};ca.prototype.contains=function(a,b){b||(b=a);for(var c=0;c<this.ranges.length;c++){var d=this.ranges[c];if(0<=x(b,d.from())&&0>=x(a,d.to()))return c}return-1};var A=function(a,b){this.anchor=a;this.head=b};A.prototype.from=function(){return ic(this.anchor,this.head)};A.prototype.to=function(){return hc(this.anchor,
+this.head)};A.prototype.empty=function(){return this.head.line==this.anchor.line&&this.head.ch==this.anchor.ch};Lb.prototype={chunkSize:function(){return this.lines.length},removeInner:function(a,b){for(var c=a,d=a+b;c<d;++c){var e=this.lines[c];this.height-=e.height;var f=e;f.parent=null;Xd(f);P(e,"delete")}this.lines.splice(a,b)},collapse:function(a){a.push.apply(a,this.lines)},insertInner:function(a,b,c){this.height+=c;this.lines=this.lines.slice(0,a).concat(b).concat(this.lines.slice(a));for(a=
+0;a<b.length;++a)b[a].parent=this},iterN:function(a,b,c){for(b=a+b;a<b;++a)if(c(this.lines[a]))return!0}};Mb.prototype={chunkSize:function(){return this.size},removeInner:function(a,b){this.size-=b;for(var c=0;c<this.children.length;++c){var d=this.children[c],e=d.chunkSize();if(a<e){var f=Math.min(b,e-a),g=d.height;if(d.removeInner(a,f),this.height-=g-d.height,e==f&&(this.children.splice(c--,1),d.parent=null),0==(b-=f))break;a=0}else a-=e}25>this.size-b&&(1<this.children.length||!(this.children[0]instanceof
+Lb))&&(c=[],this.collapse(c),this.children=[new Lb(c)],this.children[0].parent=this)},collapse:function(a){for(var b=0;b<this.children.length;++b)this.children[b].collapse(a)},insertInner:function(a,b,c){this.size+=b.length;this.height+=c;for(var d=0;d<this.children.length;++d){var e=this.children[d],f=e.chunkSize();if(a<=f){if(e.insertInner(a,b,c),e.lines&&50<e.lines.length){for(b=a=e.lines.length%25+25;b<e.lines.length;)c=new Lb(e.lines.slice(b,b+=25)),e.height-=c.height,this.children.splice(++d,
+0,c),c.parent=this;e.lines=e.lines.slice(0,a);this.maybeSpill()}break}a-=f}},maybeSpill:function(){if(!(10>=this.children.length)){var a=this;do{var b=a.children.splice(a.children.length-5,5),b=new Mb(b);if(a.parent){a.size-=b.size;a.height-=b.height;var c=N(a.parent.children,a);a.parent.children.splice(c+1,0,b)}else c=new Mb(a.children),c.parent=a,a.children=[c,b],a=c;b.parent=a.parent}while(10<a.children.length);a.parent.maybeSpill()}},iterN:function(a,b,c){for(var d=0;d<this.children.length;++d){var e=
+this.children[d],f=e.chunkSize();if(a<f){f=Math.min(b,f-a);if(e.iterN(a,f,c))return!0;if(0==(b-=f))break;a=0}else a-=f}}};var Nb=function(a,b,c){if(c)for(var d in c)c.hasOwnProperty(d)&&(this[d]=c[d]);this.doc=a;this.node=b};Nb.prototype.clear=function(){var a=this.doc.cm,b=this.line.widgets,c=this.line,d=D(c);if(null!=d&&b){for(var e=0;e<b.length;++e)b[e]==this&&b.splice(e--,1);b.length||(c.widgets=null);var f=zb(this);la(c,Math.max(0,c.height-f));a&&(Y(a,function(){var b=-f;na(c)<(a.curOp&&a.curOp.scrollTop||
+a.doc.scrollTop)&&tc(a,b);Aa(a,d,"widget")}),P(a,"lineWidgetCleared",a,this,d))}};Nb.prototype.changed=function(){var a=this,b=this.height,c=this.doc.cm,d=this.line;this.height=null;var e=zb(this)-b;e&&(la(d,d.height+e),c&&Y(c,function(){c.curOp.forceUpdate=!0;na(d)<(c.curOp&&c.curOp.scrollTop||c.doc.scrollTop)&&tc(c,e);P(c,"lineWidgetChanged",c,a,D(d))}))};ab(Nb);var vf=0,Ca=function(a,b){this.lines=[];this.type=b;this.doc=a;this.id=++vf};Ca.prototype.clear=function(){if(!this.explicitlyCleared){var a=
+this.doc.cm,b=a&&!a.curOp;if(b&&Ta(a),fa(this,"clear")){var c=this.find();c&&P(this,"clear",c.from,c.to)}for(var d=c=null,e=0;e<this.lines.length;++e){var f=this.lines[e],g=tb(f.markedSpans,this);a&&!this.collapsed?Aa(a,D(f),"text"):a&&(null!=g.to&&(d=D(f)),null!=g.from&&(c=D(f)));for(var h=f,k=f.markedSpans,l=g,n=void 0,m=0;m<k.length;++m)k[m]!=l&&(n||(n=[])).push(k[m]);h.markedSpans=n;null==g.from&&this.collapsed&&!Ja(this.doc,f)&&a&&la(f,Oa(a.display))}if(a&&this.collapsed&&!a.options.lineWrapping)for(e=
+0;e<this.lines.length;++e)f=ma(this.lines[e]),g=kc(f),g>a.display.maxLineLength&&(a.display.maxLine=f,a.display.maxLineLength=g,a.display.maxLineChanged=!0);null!=c&&a&&this.collapsed&&V(a,c,d+1);this.lines.length=0;this.explicitlyCleared=!0;this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,a&&mf(a.doc));a&&P(a,"markerCleared",a,this,c,d);b&&Ua(a);this.parent&&this.parent.clear()}};Ca.prototype.find=function(a,b){null==a&&"bookmark"==this.type&&(a=1);for(var c,d,e=0;e<this.lines.length;++e){var f=
+this.lines[e],g=tb(f.markedSpans,this);if(null!=g.from&&(c=m(b?f:D(f),g.from),-1==a))return c;if(null!=g.to&&(d=m(b?f:D(f),g.to),1==a))return d}return c&&{from:c,to:d}};Ca.prototype.changed=function(){var a=this,b=this.find(-1,!0),c=this,d=this.doc.cm;b&&d&&Y(d,function(){var e=b.line,f=D(b.line),f=ld(d,f);(f&&(Be(f),d.curOp.selectionChanged=d.curOp.forceUpdate=!0),d.curOp.updateMaxLine=!0,Ja(c.doc,e)||null==c.height)||(f=c.height,c.height=null,(f=zb(c)-f)&&la(e,e.height+f));P(d,"markerChanged",d,
+a)})};Ca.prototype.attachLine=function(a){if(!this.lines.length&&this.doc.cm){var b=this.doc.cm.curOp;b.maybeHiddenMarkers&&-1!=N(b.maybeHiddenMarkers,this)||(b.maybeUnhiddenMarkers||(b.maybeUnhiddenMarkers=[])).push(this)}this.lines.push(a)};Ca.prototype.detachLine=function(a){if(this.lines.splice(N(this.lines,a),1),!this.lines.length&&this.doc.cm)a=this.doc.cm.curOp,(a.maybeHiddenMarkers||(a.maybeHiddenMarkers=[])).push(this)};ab(Ca);var Ob=function(a,b){this.markers=a;this.primary=b;for(var c=
+0;c<a.length;++c)a[c].parent=this};Ob.prototype.clear=function(){if(!this.explicitlyCleared){this.explicitlyCleared=!0;for(var a=0;a<this.markers.length;++a)this.markers[a].clear();P(this,"clear")}};Ob.prototype.find=function(a,b){return this.primary.find(a,b)};ab(Ob);var jh=0,W=function(a,b,c,d,e){if(!(this instanceof W))return new W(a,b,c,d,e);null==c&&(c=0);Mb.call(this,[new Lb([new hb("",null)])]);this.first=c;this.scrollTop=this.scrollLeft=0;this.cantEdit=!1;this.cleanGeneration=1;this.modeFrontier=
+this.highlightFrontier=c;c=m(c,0);this.sel=ua(c);this.history=new yc(null);this.id=++jh;this.modeOption=b;this.lineSep=d;this.direction="rtl"==e?"rtl":"ltr";this.extend=!1;"string"==typeof a&&(a=this.splitLines(a));Cd(this,{from:c,to:c,text:a});Q(this,ua(c),pa)};W.prototype=Rd(Mb.prototype,{constructor:W,iter:function(a,b,c){c?this.iterN(a-this.first,b-a,c):this.iterN(this.first,this.first+this.size,a)},insert:function(a,b){for(var c=0,d=0;d<b.length;++d)c+=b[d].height;this.insertInner(a-this.first,
+b,c)},remove:function(a,b){this.removeInner(a-this.first,b)},getValue:function(a){var b=Pc(this,this.first,this.first+this.size);return!1===a?b:b.join(a||this.lineSeparator())},setValue:M(function(a){var b=m(this.first,0),c=this.first+this.size-1;kb(this,{from:b,to:m(c,w(this,c).text.length),text:this.splitLines(a),origin:"setValue",full:!0},!0);this.cm&&Eb(this.cm,0,0);Q(this,ua(b),pa)}),replaceRange:function(a,b,c,d){b=v(this,b);c=c?v(this,c):b;lb(this,a,b,c,d)},getRange:function(a,b,c){a=Ga(this,
+v(this,a),v(this,b));return!1===c?a:a.join(c||this.lineSeparator())},getLine:function(a){return(a=this.getLineHandle(a))&&a.text},getLineHandle:function(a){if(sb(this,a))return w(this,a)},getLineNumber:function(a){return D(a)},getLineHandleVisualStart:function(a){return"number"==typeof a&&(a=w(this,a)),ma(a)},lineCount:function(){return this.size},firstLine:function(){return this.first},lastLine:function(){return this.first+this.size-1},clipPos:function(a){return v(this,a)},getCursor:function(a){var b=
+this.sel.primary();return null==a||"head"==a?b.head:"anchor"==a?b.anchor:"end"==a||"to"==a||!1===a?b.to():b.from()},listSelections:function(){return this.sel.ranges},somethingSelected:function(){return this.sel.somethingSelected()},setCursor:M(function(a,b,c){a=v(this,"number"==typeof a?m(a,b||0):a);Q(this,ua(a,null),c)}),setSelection:M(function(a,b,c){var d=v(this,a);a=v(this,b||a);Q(this,ua(d,a),c)}),extendSelection:M(function(a,b,c){Ac(this,v(this,a),b&&v(this,b),c)}),extendSelections:M(function(a,
+b){hf(this,Vd(this,a),b)}),extendSelectionsBy:M(function(a,b){hf(this,Vd(this,ec(this.sel.ranges,a)),b)}),setSelections:M(function(a,b,c){if(a.length){for(var d=[],e=0;e<a.length;e++)d[e]=new A(v(this,a[e].anchor),v(this,a[e].head));null==b&&(b=Math.min(a.length-1,this.sel.primIndex));Q(this,ka(d,b),c)}}),addSelection:M(function(a,b,c){var d=this.sel.ranges.slice(0);d.push(new A(v(this,a),v(this,b||a)));Q(this,ka(d,d.length-1),c)}),getSelection:function(a){for(var b,c=this.sel.ranges,d=0;d<c.length;d++){var e=
+Ga(this,c[d].from(),c[d].to());b=b?b.concat(e):e}return!1===a?b:b.join(a||this.lineSeparator())},getSelections:function(a){for(var b=[],c=this.sel.ranges,d=0;d<c.length;d++){var e=Ga(this,c[d].from(),c[d].to());!1!==a&&(e=e.join(a||this.lineSeparator()));b[d]=e}return b},replaceSelection:function(a,b,c){for(var d=[],e=0;e<this.sel.ranges.length;e++)d[e]=a;this.replaceSelections(d,b,c||"+input")},replaceSelections:M(function(a,b,c){for(var d=[],e=this.sel,f=0;f<e.ranges.length;f++){var g=e.ranges[f];
+d[f]={from:g.from(),to:g.to(),text:this.splitLines(a[f]),origin:c}}if(a=b&&"end"!=b){a=[];e=c=m(this.first,0);for(f=0;f<d.length;f++){var h=d[f],g=$e(h.from,c,e),k=$e(Ba(h),c,e);(c=h.to,e=k,"around"==b)?(h=this.sel.ranges[f],h=0>x(h.head,h.anchor),a[f]=new A(h?k:g,h?g:k)):a[f]=new A(g,g)}a=new ca(a,this.sel.primIndex)}b=a;for(a=d.length-1;0<=a;a--)kb(this,d[a]);b?jf(this,b):this.cm&&fb(this.cm)}),undo:M(function(){Cc(this,"undo")}),redo:M(function(){Cc(this,"redo")}),undoSelection:M(function(){Cc(this,
+"undo",!0)}),redoSelection:M(function(){Cc(this,"redo",!0)}),setExtending:function(a){this.extend=a},getExtending:function(){return this.extend},historySize:function(){for(var a=this.history,b=0,c=0,d=0;d<a.done.length;d++)a.done[d].ranges||++b;for(d=0;d<a.undone.length;d++)a.undone[d].ranges||++c;return{undo:b,redo:c}},clearHistory:function(){this.history=new yc(this.history.maxGeneration)},markClean:function(){this.cleanGeneration=this.changeGeneration(!0)},changeGeneration:function(a){return a&&
+(this.history.lastOp=this.history.lastSelOp=this.history.lastOrigin=null),this.history.generation},isClean:function(a){return this.history.generation==(a||this.cleanGeneration)},getHistory:function(){return{done:ib(this.history.done),undone:ib(this.history.undone)}},setHistory:function(a){var b=this.history=new yc(this.history.maxGeneration);b.done=ib(a.done.slice(0),null,!0);b.undone=ib(a.undone.slice(0),null,!0)},setGutterMarker:M(function(a,b,c){return Kb(this,a,"gutter",function(a){var e=a.gutterMarkers||
+(a.gutterMarkers={});return e[b]=c,!c&&Sd(e)&&(a.gutterMarkers=null),!0})}),clearGutter:M(function(a){var b=this;this.iter(function(c){c.gutterMarkers&&c.gutterMarkers[a]&&Kb(b,c,"gutter",function(){return c.gutterMarkers[a]=null,Sd(c.gutterMarkers)&&(c.gutterMarkers=null),!0})})}),lineInfo:function(a){var b;if("number"==typeof a){if(!(sb(this,a)&&(b=a,a=w(this,a))))return null}else if(null==(b=D(a)))return null;return{line:b,handle:a,text:a.text,gutterMarkers:a.gutterMarkers,textClass:a.textClass,
+bgClass:a.bgClass,wrapClass:a.wrapClass,widgets:a.widgets}},addLineClass:M(function(a,b,c){return Kb(this,a,"gutter"==b?"gutter":"class",function(a){var e="text"==b?"textClass":"background"==b?"bgClass":"gutter"==b?"gutterClass":"wrapClass";if(a[e]){if(ga(c).test(a[e]))return!1;a[e]+=" "+c}else a[e]=c;return!0})}),removeLineClass:M(function(a,b,c){return Kb(this,a,"gutter"==b?"gutter":"class",function(a){var e="text"==b?"textClass":"background"==b?"bgClass":"gutter"==b?"gutterClass":"wrapClass",f=
+a[e];if(!f)return!1;if(null==c)a[e]=null;else{var g=f.match(ga(c));if(!g)return!1;var h=g.index+g[0].length;a[e]=f.slice(0,g.index)+(g.index&&h!=f.length?" ":"")+f.slice(h)||null}return!0})}),addLineWidget:M(function(a,b,c){return Gg(this,a,b,c)}),removeLineWidget:function(a){a.clear()},markText:function(a,b,c){return mb(this,v(this,a),v(this,b),c,c&&c.type||"range")},setBookmark:function(a,b){var c={replacedWith:b&&(null==b.nodeType?b.widget:b),insertLeft:b&&b.insertLeft,clearWhenEmpty:!1,shared:b&&
+b.shared,handleMouseEvents:b&&b.handleMouseEvents};return a=v(this,a),mb(this,a,a,c,"bookmark")},findMarksAt:function(a){a=v(this,a);var b=[],c=w(this,a.line).markedSpans;if(c)for(var d=0;d<c.length;++d){var e=c[d];(null==e.from||e.from<=a.ch)&&(null==e.to||e.to>=a.ch)&&b.push(e.marker.parent||e.marker)}return b},findMarks:function(a,b,c){a=v(this,a);b=v(this,b);var d=[],e=a.line;return this.iter(a.line,b.line+1,function(f){if(f=f.markedSpans)for(var g=0;g<f.length;g++){var h=f[g];null!=h.to&&e==
+a.line&&a.ch>=h.to||null==h.from&&e!=a.line||null!=h.from&&e==b.line&&h.from>=b.ch||c&&!c(h.marker)||d.push(h.marker.parent||h.marker)}++e}),d},getAllMarks:function(){var a=[];return this.iter(function(b){if(b=b.markedSpans)for(var c=0;c<b.length;++c)null!=b[c].from&&a.push(b[c].marker)}),a},posFromIndex:function(a){var b,c=this.first,d=this.lineSeparator().length;return this.iter(function(e){e=e.text.length+d;if(e>a)return b=a,!0;a-=e;++c}),v(this,m(c,b))},indexFromPos:function(a){a=v(this,a);var b=
+a.ch;if(a.line<this.first||0>a.ch)return 0;var c=this.lineSeparator().length;return this.iter(this.first,a.line,function(a){b+=a.text.length+c}),b},copy:function(a){var b=new W(Pc(this,this.first,this.first+this.size),this.modeOption,this.first,this.lineSep,this.direction);return b.scrollTop=this.scrollTop,b.scrollLeft=this.scrollLeft,b.sel=this.sel,b.extend=!1,a&&(b.history.undoDepth=this.history.undoDepth,b.setHistory(this.getHistory())),b},linkedDoc:function(a){a||(a={});var b=this.first,c=this.first+
+this.size;null!=a.from&&a.from>b&&(b=a.from);null!=a.to&&a.to<c&&(c=a.to);b=new W(Pc(this,b,c),a.mode||this.modeOption,b,this.lineSep,this.direction);a.sharedHist&&(b.history=this.history);(this.linked||(this.linked=[])).push({doc:b,sharedHist:a.sharedHist});b.linked=[{doc:this,isParent:!0,sharedHist:a.sharedHist}];a=wf(this);for(c=0;c<a.length;c++){var d=a[c],e=d.find(),f=b.clipPos(e.from),e=b.clipPos(e.to);x(f,e)&&(f=mb(b,f,e,d.primary,d.primary.type),d.markers.push(f),f.parent=d)}return b},unlinkDoc:function(a){if(a instanceof
+E&&(a=a.doc),this.linked)for(var b=0;b<this.linked.length;++b)if(this.linked[b].doc==a){this.linked.splice(b,1);a.unlinkDoc(this);Ig(wf(this));break}if(a.history==this.history){var c=[a.id];Va(a,function(a){return c.push(a.id)},!0);a.history=new yc(null);a.history.done=ib(this.history.done,c);a.history.undone=ib(this.history.undone,c)}},iterLinkedDocs:function(a){Va(this,a)},getMode:function(){return this.mode},getEditor:function(){return this.cm},splitLines:function(a){return this.lineSep?a.split(this.lineSep):
+Od(a)},lineSeparator:function(){return this.lineSep||"\n"},setDirection:M(function(a){"rtl"!=a&&(a="ltr");a!=this.direction&&(this.direction=a,this.iter(function(a){return a.order=null}),this.cm&&Cg(this.cm))})});W.prototype.eachLine=W.prototype.iter;for(var yf=0,Pf=!1,Da={3:"Enter",8:"Backspace",9:"Tab",13:"Enter",16:"Shift",17:"Ctrl",18:"Alt",19:"Pause",20:"CapsLock",27:"Esc",32:"Space",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"Left",38:"Up",39:"Right",40:"Down",44:"PrintScrn",45:"Insert",
+46:"Delete",59:";",61:"\x3d",91:"Mod",92:"Mod",93:"Mod",106:"*",107:"\x3d",109:"-",110:".",111:"/",127:"Delete",173:"-",186:";",187:"\x3d",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'",63232:"Up",63233:"Down",63234:"Left",63235:"Right",63272:"Delete",63273:"Home",63275:"End",63276:"PageUp",63277:"PageDown",63302:"Insert"},ac=0;10>ac;ac++)Da[ac+48]=Da[ac+96]=String(ac);for(var Ic=65;90>=Ic;Ic++)Da[Ic]=String.fromCharCode(Ic);for(var bc=1;12>=bc;bc++)Da[bc+111]=Da[bc+63235]=
+"F"+bc;var Pb={basic:{Left:"goCharLeft",Right:"goCharRight",Up:"goLineUp",Down:"goLineDown",End:"goLineEnd",Home:"goLineStartSmart",PageUp:"goPageUp",PageDown:"goPageDown",Delete:"delCharAfter",Backspace:"delCharBefore","Shift-Backspace":"delCharBefore",Tab:"defaultTab","Shift-Tab":"indentAuto",Enter:"newlineAndIndent",Insert:"toggleOverwrite",Esc:"singleSelection"},pcDefault:{"Ctrl-A":"selectAll","Ctrl-D":"deleteLine","Ctrl-Z":"undo","Shift-Ctrl-Z":"redo","Ctrl-Y":"redo","Ctrl-Home":"goDocStart",
+"Ctrl-End":"goDocEnd","Ctrl-Up":"goLineUp","Ctrl-Down":"goLineDown","Ctrl-Left":"goGroupLeft","Ctrl-Right":"goGroupRight","Alt-Left":"goLineStart","Alt-Right":"goLineEnd","Ctrl-Backspace":"delGroupBefore","Ctrl-Delete":"delGroupAfter","Ctrl-S":"save","Ctrl-F":"find","Ctrl-G":"findNext","Shift-Ctrl-G":"findPrev","Shift-Ctrl-F":"replace","Shift-Ctrl-R":"replaceAll","Ctrl-[":"indentLess","Ctrl-]":"indentMore","Ctrl-U":"undoSelection","Shift-Ctrl-U":"redoSelection","Alt-U":"redoSelection",fallthrough:"basic"},
+emacsy:{"Ctrl-F":"goCharRight","Ctrl-B":"goCharLeft","Ctrl-P":"goLineUp","Ctrl-N":"goLineDown","Alt-F":"goWordRight","Alt-B":"goWordLeft","Ctrl-A":"goLineStart","Ctrl-E":"goLineEnd","Ctrl-V":"goPageDown","Shift-Ctrl-V":"goPageUp","Ctrl-D":"delCharAfter","Ctrl-H":"delCharBefore","Alt-D":"delWordAfter","Alt-Backspace":"delWordBefore","Ctrl-K":"killLine","Ctrl-T":"transposeChars","Ctrl-O":"openLine"},macDefault:{"Cmd-A":"selectAll","Cmd-D":"deleteLine","Cmd-Z":"undo","Shift-Cmd-Z":"redo","Cmd-Y":"redo",
+"Cmd-Home":"goDocStart","Cmd-Up":"goDocStart","Cmd-End":"goDocEnd","Cmd-Down":"goDocEnd","Alt-Left":"goGroupLeft","Alt-Right":"goGroupRight","Cmd-Left":"goLineLeft","Cmd-Right":"goLineRight","Alt-Backspace":"delGroupBefore","Ctrl-Alt-Backspace":"delGroupAfter","Alt-Delete":"delGroupAfter","Cmd-S":"save","Cmd-F":"find","Cmd-G":"findNext","Shift-Cmd-G":"findPrev","Cmd-Alt-F":"replace","Shift-Cmd-Alt-F":"replaceAll","Cmd-[":"indentLess","Cmd-]":"indentMore","Cmd-Backspace":"delWrappedLineLeft","Cmd-Delete":"delWrappedLineRight",
+"Cmd-U":"undoSelection","Shift-Cmd-U":"redoSelection","Ctrl-Up":"goDocStart","Ctrl-Down":"goDocEnd",fallthrough:["basic","emacsy"]}};Pb["default"]=ha?Pb.macDefault:Pb.pcDefault;var Qb={selectAll:of,singleSelection:function(a){return a.setSelection(a.getCursor("anchor"),a.getCursor("head"),pa)},killLine:function(a){return ob(a,function(b){if(b.empty()){var c=w(a.doc,b.head.line).text.length;return b.head.ch==c&&b.head.line<a.lastLine()?{from:b.head,to:m(b.head.line+1,0)}:{from:b.head,to:m(b.head.line,
+c)}}return{from:b.from(),to:b.to()}})},deleteLine:function(a){return ob(a,function(b){return{from:m(b.from().line,0),to:v(a.doc,m(b.to().line+1,0))}})},delLineLeft:function(a){return ob(a,function(a){return{from:m(a.from().line,0),to:a.from()}})},delWrappedLineLeft:function(a){return ob(a,function(b){var c=a.charCoords(b.head,"div").top+5;return{from:a.coordsChar({left:0,top:c},"div"),to:b.from()}})},delWrappedLineRight:function(a){return ob(a,function(b){var c=a.charCoords(b.head,"div").top+5,c=
+a.coordsChar({left:a.display.lineDiv.offsetWidth+100,top:c},"div");return{from:b.from(),to:c}})},undo:function(a){return a.undo()},redo:function(a){return a.redo()},undoSelection:function(a){return a.undoSelection()},redoSelection:function(a){return a.redoSelection()},goDocStart:function(a){return a.extendSelection(m(a.firstLine(),0))},goDocEnd:function(a){return a.extendSelection(m(a.lastLine()))},goLineStart:function(a){return a.extendSelectionsBy(function(b){return Ef(a,b.head.line)},{origin:"+move",
+bias:1})},goLineStartSmart:function(a){return a.extendSelectionsBy(function(b){return Ff(a,b.head)},{origin:"+move",bias:1})},goLineEnd:function(a){return a.extendSelectionsBy(function(b){b=b.head.line;var c=w(a.doc,b),d;d=c;for(var e;e=Ia(d,!1);)d=e.find(1,!0).line;return d!=c&&(b=D(d)),$c(!0,a,c,b,-1)},{origin:"+move",bias:-1})},goLineRight:function(a){return a.extendSelectionsBy(function(b){b=a.cursorCoords(b.head,"div").top+5;return a.coordsChar({left:a.display.lineDiv.offsetWidth+100,top:b},
+"div")},Zb)},goLineLeft:function(a){return a.extendSelectionsBy(function(b){b=a.cursorCoords(b.head,"div").top+5;return a.coordsChar({left:0,top:b},"div")},Zb)},goLineLeftSmart:function(a){return a.extendSelectionsBy(function(b){var c=a.cursorCoords(b.head,"div").top+5,c=a.coordsChar({left:0,top:c},"div");return c.ch<a.getLine(c.line).search(/\S/)?Ff(a,b.head):c},Zb)},goLineUp:function(a){return a.moveV(-1,"line")},goLineDown:function(a){return a.moveV(1,"line")},goPageUp:function(a){return a.moveV(-1,
+"page")},goPageDown:function(a){return a.moveV(1,"page")},goCharLeft:function(a){return a.moveH(-1,"char")},goCharRight:function(a){return a.moveH(1,"char")},goColumnLeft:function(a){return a.moveH(-1,"column")},goColumnRight:function(a){return a.moveH(1,"column")},goWordLeft:function(a){return a.moveH(-1,"word")},goGroupRight:function(a){return a.moveH(1,"group")},goGroupLeft:function(a){return a.moveH(-1,"group")},goWordRight:function(a){return a.moveH(1,"word")},delCharBefore:function(a){return a.deleteH(-1,
+"char")},delCharAfter:function(a){return a.deleteH(1,"char")},delWordBefore:function(a){return a.deleteH(-1,"word")},delWordAfter:function(a){return a.deleteH(1,"word")},delGroupBefore:function(a){return a.deleteH(-1,"group")},delGroupAfter:function(a){return a.deleteH(1,"group")},indentAuto:function(a){return a.indentSelection("smart")},indentMore:function(a){return a.indentSelection("add")},indentLess:function(a){return a.indentSelection("subtract")},insertTab:function(a){return a.replaceSelection("\t")},
+insertSoftTab:function(a){for(var b=[],c=a.listSelections(),d=a.options.tabSize,e=0;e<c.length;e++){var f=c[e].from(),f=ea(a.getLine(f.line),f.ch,d);b.push(Mc(d-f%d))}a.replaceSelections(b)},defaultTab:function(a){a.somethingSelected()?a.indentSelection("add"):a.execCommand("insertTab")},transposeChars:function(a){return Y(a,function(){for(var b=a.listSelections(),c=[],d=0;d<b.length;d++)if(b[d].empty()){var e=b[d].head,f=w(a.doc,e.line).text;if(f)if(e.ch==f.length&&(e=new m(e.line,e.ch-1)),0<e.ch)e=
+new m(e.line,e.ch+1),a.replaceRange(f.charAt(e.ch-1)+f.charAt(e.ch-2),m(e.line,e.ch-2),e,"+transpose");else if(e.line>a.doc.first){var g=w(a.doc,e.line-1).text;g&&(e=new m(e.line,1),a.replaceRange(f.charAt(0)+a.doc.lineSeparator()+g.charAt(g.length-1),m(e.line-1,g.length-1),e,"+transpose"))}c.push(new A(e,e))}a.setSelections(c)})},newlineAndIndent:function(a){return Y(a,function(){for(var b=a.listSelections(),c=b.length-1;0<=c;c--)a.replaceRange(a.doc.lineSeparator(),b[c].anchor,b[c].head,"+input");
+b=a.listSelections();for(c=0;c<b.length;c++)a.indentLine(b[c].from().line,null,!0);fb(a)})},openLine:function(a){return a.replaceSelection("\n","start")},toggleOverwrite:function(a){return a.toggleOverwrite()}},Pg=new Wa,Hd=null,Id=function(a,b,c){this.time=a;this.pos=b;this.button=c};Id.prototype.compare=function(a,b,c){return this.time+400>a&&0==x(b,this.pos)&&c==this.button};var Tb,Sb,pb={toString:function(){return"CodeMirror.Init"}},Of={},Gc={};E.defaults=Of;E.optionHandlers=Gc;var Md=[];E.defineInitHook=
+function(a){return Md.push(a)};var da=null,y=function(a){this.cm=a;this.lastAnchorNode=this.lastAnchorOffset=this.lastFocusNode=this.lastFocusOffset=null;this.polling=new Wa;this.composing=null;this.gracePeriod=!1;this.readDOMTimeout=null};y.prototype.init=function(a){function b(a){if(!K(e,a)){if(e.somethingSelected())Rf({lineWise:!1,text:e.getSelections()}),"cut"==a.type&&e.replaceSelection("",null,"cut");else{if(!e.options.lineWiseCopyCut)return;var b=Uf(e);Rf({lineWise:!0,text:b.text});"cut"==
+a.type&&e.operation(function(){e.setSelections(b.ranges,0,pa);e.replaceSelection("",null,"cut")})}if(a.clipboardData){a.clipboardData.clearData();var c=da.text.join("\n");if(a.clipboardData.setData("Text",c),a.clipboardData.getData("Text")==c)return void a.preventDefault()}var l=Wf();a=l.firstChild;e.display.lineSpace.insertBefore(l,e.display.lineSpace.firstChild);a.value=da.text.join("\n");var n=document.activeElement;Yb(a);setTimeout(function(){e.display.lineSpace.removeChild(l);n.focus();n==f&&
+d.showPrimarySelection()},50)}}var c=this,d=this,e=d.cm,f=d.div=a.lineDiv;Vf(f,e.options.spellcheck);t(f,"paste",function(a){K(e,a)||Tf(a,e)||11>=B&&setTimeout(L(e,function(){return c.updateFromDOM()}),20)});t(f,"compositionstart",function(a){c.composing={data:a.data,done:!1}});t(f,"compositionupdate",function(a){c.composing||(c.composing={data:a.data,done:!1})});t(f,"compositionend",function(a){c.composing&&(a.data!=c.composing.data&&c.readFromDOMSoon(),c.composing.done=!0)});t(f,"touchstart",function(){return d.forceCompositionEnd()});
+t(f,"input",function(){c.composing||c.readFromDOMSoon()});t(f,"copy",b);t(f,"cut",b)};y.prototype.prepareSelection=function(){var a=Ie(this.cm,!1);return a.focus=this.cm.state.focused,a};y.prototype.showSelection=function(a,b){a&&this.cm.display.view.length&&((a.focus||b)&&this.showPrimarySelection(),this.showMultipleSelections(a))};y.prototype.showPrimarySelection=function(){var a=window.getSelection(),b=this.cm,c=b.doc.sel.primary(),d=c.from(),c=c.to();if(b.display.viewTo==b.display.viewFrom||d.line>=
+b.display.viewTo||c.line<b.display.viewFrom)return void a.removeAllRanges();var e=Hc(b,a.anchorNode,a.anchorOffset),f=Hc(b,a.focusNode,a.focusOffset);if(!e||e.bad||!f||f.bad||0!=x(ic(e,f),d)||0!=x(hc(e,f),c)){e=b.display.view;d=d.line>=b.display.viewFrom&&Yf(b,d)||{node:e[0].measure.map[2],offset:0};c=c.line<b.display.viewTo&&Yf(b,c);c||(c=e[e.length-1].measure,c=c.maps?c.maps[c.maps.length-1]:c.map,c={node:c[c.length-1],offset:c[c.length-2]-c[c.length-3]});if(!d||!c)return void a.removeAllRanges();
+var g,e=a.rangeCount&&a.getRangeAt(0);try{g=db(d.node,d.offset,c.offset,c.node)}catch(h){}g&&(!wa&&b.state.focused?(a.collapse(d.node,d.offset),g.collapsed||(a.removeAllRanges(),a.addRange(g))):(a.removeAllRanges(),a.addRange(g)),e&&null==a.anchorNode?a.addRange(e):wa&&this.startGracePeriod());this.rememberSelection()}};y.prototype.startGracePeriod=function(){var a=this;clearTimeout(this.gracePeriod);this.gracePeriod=setTimeout(function(){a.gracePeriod=!1;a.selectionChanged()&&a.cm.operation(function(){return a.cm.curOp.selectionChanged=
+!0})},20)};y.prototype.showMultipleSelections=function(a){Z(this.cm.display.cursorDiv,a.cursors);Z(this.cm.display.selectionDiv,a.selection)};y.prototype.rememberSelection=function(){var a=window.getSelection();this.lastAnchorNode=a.anchorNode;this.lastAnchorOffset=a.anchorOffset;this.lastFocusNode=a.focusNode;this.lastFocusOffset=a.focusOffset};y.prototype.selectionInEditor=function(){var a=window.getSelection();if(!a.rangeCount)return!1;a=a.getRangeAt(0).commonAncestorContainer;return va(this.div,
+a)};y.prototype.focus=function(){"nocursor"!=this.cm.options.readOnly&&(this.selectionInEditor()||this.showSelection(this.prepareSelection(),!0),this.div.focus())};y.prototype.blur=function(){this.div.blur()};y.prototype.getField=function(){return this.div};y.prototype.supportsTouch=function(){return!0};y.prototype.receivedFocus=function(){function a(){b.cm.state.focused&&(b.pollSelection(),b.polling.set(b.cm.options.pollInterval,a))}var b=this;this.selectionInEditor()?this.pollSelection():Y(this.cm,
+function(){return b.cm.curOp.selectionChanged=!0});this.polling.set(this.cm.options.pollInterval,a)};y.prototype.selectionChanged=function(){var a=window.getSelection();return a.anchorNode!=this.lastAnchorNode||a.anchorOffset!=this.lastAnchorOffset||a.focusNode!=this.lastFocusNode||a.focusOffset!=this.lastFocusOffset};y.prototype.pollSelection=function(){if(null==this.readDOMTimeout&&!this.gracePeriod&&this.selectionChanged()){var a=window.getSelection(),b=this.cm;if(qc&&pc&&this.cm.options.gutters.length&&
+ch(a.anchorNode))return this.cm.triggerOnKeyDown({type:"keydown",keyCode:8,preventDefault:Math.abs}),this.blur(),void this.focus();if(!this.composing){this.rememberSelection();var c=Hc(b,a.anchorNode,a.anchorOffset),d=Hc(b,a.focusNode,a.focusOffset);c&&d&&Y(b,function(){Q(b.doc,ua(c,d),pa);(c.bad||d.bad)&&(b.curOp.selectionChanged=!0)})}}};y.prototype.pollContent=function(){null!=this.readDOMTimeout&&(clearTimeout(this.readDOMTimeout),this.readDOMTimeout=null);var a=this.cm,b=a.display,c=a.doc.sel.primary(),
+d=c.from(),c=c.to();if(0==d.ch&&d.line>a.firstLine()&&(d=m(d.line-1,w(a.doc,d.line-1).length)),c.ch==w(a.doc,c.line).text.length&&c.line<a.lastLine()&&(c=m(c.line+1,0)),d.line<b.viewFrom||c.line>b.viewTo-1)return!1;var e,f,g;d.line==b.viewFrom||0==(e=Na(a,d.line))?(f=D(b.view[0].line),g=b.view[0].node):(f=D(b.view[e].line),g=b.view[e-1].node.nextSibling);var h,k;e=Na(a,c.line);if(e==b.view.length-1?(h=b.viewTo-1,k=b.lineDiv.lastChild):(h=D(b.view[e+1].line)-1,k=b.view[e+1].node.previousSibling),!g)return!1;
+b=a.doc.splitLines(dh(a,g,k,f,h));for(g=Ga(a.doc,m(f,0),m(h,w(a.doc,h).text.length));1<b.length&&1<g.length;)if(z(b)==z(g))b.pop(),g.pop(),h--;else{if(b[0]!=g[0])break;b.shift();g.shift();f++}k=e=0;for(var c=b[0],l=g[0],n=Math.min(c.length,l.length);e<n&&c.charCodeAt(e)==l.charCodeAt(e);)++e;c=z(b);l=z(g);for(n=Math.min(c.length-(1==b.length?e:0),l.length-(1==g.length?e:0));k<n&&c.charCodeAt(c.length-k-1)==l.charCodeAt(l.length-k-1);)++k;if(1==b.length&&1==g.length&&f==d.line)for(;e&&e>d.ch&&c.charCodeAt(c.length-
+k-1)==l.charCodeAt(l.length-k-1);)e--,k++;b[b.length-1]=c.slice(0,c.length-k).replace(/^\u200b+/,"");b[0]=b[0].slice(e).replace(/\u200b+$/,"");d=m(f,e);h=m(h,g.length?z(g).length-k:0);return 1<b.length||b[0]||x(d,h)?(lb(a.doc,b,d,h,"+input"),!0):void 0};y.prototype.ensurePolled=function(){this.forceCompositionEnd()};y.prototype.reset=function(){this.forceCompositionEnd()};y.prototype.forceCompositionEnd=function(){this.composing&&(clearTimeout(this.readDOMTimeout),this.composing=null,this.updateFromDOM(),
+this.div.blur(),this.div.focus())};y.prototype.readFromDOMSoon=function(){var a=this;null==this.readDOMTimeout&&(this.readDOMTimeout=setTimeout(function(){if(a.readDOMTimeout=null,a.composing){if(!a.composing.done)return;a.composing=null}a.updateFromDOM()},80))};y.prototype.updateFromDOM=function(){var a=this;!this.cm.isReadOnly()&&this.pollContent()||Y(this.cm,function(){return V(a.cm)})};y.prototype.setUneditable=function(a){a.contentEditable="false"};y.prototype.onKeyPress=function(a){0!=a.charCode&&
+(a.preventDefault(),this.cm.isReadOnly()||L(this.cm,Nd)(this.cm,String.fromCharCode(null==a.charCode?a.keyCode:a.charCode),0))};y.prototype.readOnlyChanged=function(a){this.div.contentEditable=String("nocursor"!=a)};y.prototype.onContextMenu=function(){};y.prototype.resetPosition=function(){};y.prototype.needsContentAttribute=!0;var I=function(a){this.cm=a;this.prevInput="";this.pollingFast=!1;this.polling=new Wa;this.hasSelection=!1;this.composing=null};I.prototype.init=function(a){function b(a){if(!K(e,
+a)){if(e.somethingSelected())da={lineWise:!1,text:e.getSelections()};else{if(!e.options.lineWiseCopyCut)return;var b=Uf(e);da={lineWise:!0,text:b.text};"cut"==a.type?e.setSelections(b.ranges,null,pa):(d.prevInput="",g.value=b.text.join("\n"),Yb(g))}"cut"==a.type&&(e.state.cutIncoming=!0)}}var c=this,d=this,e=this.cm,f=this.wrapper=Wf(),g=this.textarea=f.firstChild;a.wrapper.insertBefore(f,a.wrapper.firstChild);Wb&&(g.style.width="0px");t(g,"input",function(){C&&9<=B&&c.hasSelection&&(c.hasSelection=
+null);d.poll()});t(g,"paste",function(a){K(e,a)||Tf(a,e)||(e.state.pasteIncoming=!0,d.fastPoll())});t(g,"cut",b);t(g,"copy",b);t(a.scroller,"paste",function(b){ta(a,b)||K(e,b)||(e.state.pasteIncoming=!0,d.focus())});t(a.lineSpace,"selectstart",function(b){ta(a,b)||S(b)});t(g,"compositionstart",function(){var a=e.getCursor("from");d.composing&&d.composing.range.clear();d.composing={start:a,range:e.markText(a,e.getCursor("to"),{className:"CodeMirror-composing"})}});t(g,"compositionend",function(){d.composing&&
+(d.poll(),d.composing.range.clear(),d.composing=null)})};I.prototype.prepareSelection=function(){var a=this.cm,b=a.display,c=a.doc,d=Ie(a);if(a.options.moveInputWithCursor){var a=ia(a,c.sel.primary().head,"div"),c=b.wrapper.getBoundingClientRect(),e=b.lineDiv.getBoundingClientRect();d.teTop=Math.max(0,Math.min(b.wrapper.clientHeight-10,a.top+e.top-c.top));d.teLeft=Math.max(0,Math.min(b.wrapper.clientWidth-10,a.left+e.left-c.left))}return d};I.prototype.showSelection=function(a){var b=this.cm.display;
+Z(b.cursorDiv,a.cursors);Z(b.selectionDiv,a.selection);null!=a.teTop&&(this.wrapper.style.top=a.teTop+"px",this.wrapper.style.left=a.teLeft+"px")};I.prototype.reset=function(a){if(!this.contextMenuPending&&!this.composing){var b=this.cm;b.somethingSelected()?(this.prevInput="",a=b.getSelection(),this.textarea.value=a,b.state.focused&&Yb(this.textarea),C&&9<=B&&(this.hasSelection=a)):a||(this.prevInput=this.textarea.value="",C&&9<=B&&(this.hasSelection=null))}};I.prototype.getField=function(){return this.textarea};
+I.prototype.supportsTouch=function(){return!1};I.prototype.focus=function(){if("nocursor"!=this.cm.options.readOnly&&(!rb||qa()!=this.textarea))try{this.textarea.focus()}catch(a){}};I.prototype.blur=function(){this.textarea.blur()};I.prototype.resetPosition=function(){this.wrapper.style.top=this.wrapper.style.left=0};I.prototype.receivedFocus=function(){this.slowPoll()};I.prototype.slowPoll=function(){var a=this;this.pollingFast||this.polling.set(this.cm.options.pollInterval,function(){a.poll();a.cm.state.focused&&
+a.slowPoll()})};I.prototype.fastPoll=function(){function a(){c.poll()||b?(c.pollingFast=!1,c.slowPoll()):(b=!0,c.polling.set(60,a))}var b=!1,c=this;c.pollingFast=!0;c.polling.set(20,a)};I.prototype.poll=function(){var a=this,b=this.cm,c=this.textarea,d=this.prevInput;if(this.contextMenuPending||!b.state.focused||ih(c)&&!d&&!this.composing||b.isReadOnly()||b.options.disableInput||b.state.keySeq)return!1;var e=c.value;if(e==d&&!b.somethingSelected())return!1;if(C&&9<=B&&this.hasSelection===e||ha&&/[\uf700-\uf7ff]/.test(e))return b.display.input.reset(),
+!1;if(b.doc.sel==b.display.selForContextMenu){var f=e.charCodeAt(0);if(8203!=f||d||(d="​"),8666==f)return this.reset(),this.cm.execCommand("undo")}for(var g=0,f=Math.min(d.length,e.length);g<f&&d.charCodeAt(g)==e.charCodeAt(g);)++g;return Y(b,function(){Nd(b,e.slice(g),d.length-g,null,a.composing?"*compose":null);1E3<e.length||-1<e.indexOf("\n")?c.value=a.prevInput="":a.prevInput=e;a.composing&&(a.composing.range.clear(),a.composing.range=b.markText(a.composing.start,b.getCursor("to"),{className:"CodeMirror-composing"}))}),
+!0};I.prototype.ensurePolled=function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)};I.prototype.onKeyPress=function(){C&&9<=B&&(this.hasSelection=null);this.fastPoll()};I.prototype.onContextMenu=function(a){function b(){if(null!=g.selectionStart){var a=e.somethingSelected(),b="​"+(a?g.value:"");g.value="⇚";g.value=b;d.prevInput=a?"":"​";g.selectionStart=1;g.selectionEnd=b.length;f.selForContextMenu=e.doc.sel}}function c(){if(d.contextMenuPending=!1,d.wrapper.style.cssText=n,g.style.cssText=
+l,C&&9>B&&f.scrollbars.setScrollTop(f.scroller.scrollTop=k),null!=g.selectionStart){(!C||C&&9>B)&&b();var a=0,c=function(){f.selForContextMenu==e.doc.sel&&0==g.selectionStart&&0<g.selectionEnd&&"​"==d.prevInput?L(e,of)(e):10>a++?f.detectingSelectAll=setTimeout(c,500):(f.selForContextMenu=null,f.input.reset())};f.detectingSelectAll=setTimeout(c,200)}}var d=this,e=d.cm,f=e.display,g=d.textarea,h=Qa(e,a),k=f.scroller.scrollTop;if(h&&!ja){e.options.resetSelectionOnContextMenu&&-1==e.doc.sel.contains(h)&&
+L(e,Q)(e.doc,ua(h),pa);var l=g.style.cssText,n=d.wrapper.style.cssText;d.wrapper.style.cssText="position: absolute";h=d.wrapper.getBoundingClientRect();g.style.cssText="position: absolute; width: 30px; height: 30px;\n      top: "+(a.clientY-h.top-5)+"px; left: "+(a.clientX-h.left-5)+"px;\n      z-index: 1000; background: "+(C?"rgba(255, 255, 255, .05)":"transparent")+";\n      outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity\x3d5);";var m;if(R&&
+(m=window.scrollY),f.input.focus(),R&&window.scrollTo(null,m),f.input.reset(),e.somethingSelected()||(g.value=d.prevInput=" "),d.contextMenuPending=!0,f.selForContextMenu=e.doc.sel,clearTimeout(f.detectingSelectAll),C&&9<=B&&b(),Kd){vb(a);var p=function(){aa(window,"mouseup",p);setTimeout(c,20)};t(window,"mouseup",p)}else setTimeout(c,50)}};I.prototype.readOnlyChanged=function(a){a||this.reset();this.textarea.disabled="nocursor"==a};I.prototype.setUneditable=function(){};I.prototype.needsContentAttribute=
+!1;(function(a){function b(b,e,f,g){a.defaults[b]=e;f&&(c[b]=g?function(a,b,c){c!=pb&&f(a,b,c)}:f)}var c=a.optionHandlers;a.defineOption=b;a.Init=pb;b("value","",function(a,b){return a.setValue(b)},!0);b("mode",null,function(a,b){a.doc.modeOption=b;Bd(a)},!0);b("indentUnit",2,Bd,!0);b("indentWithTabs",!1);b("smartIndent",!0);b("tabSize",4,function(a){Ib(a);Bb(a);V(a)},!0);b("lineSeparator",null,function(a,b){if(a.doc.lineSep=b,b){var c=[],g=a.doc.first;a.doc.iter(function(a){for(var d=0;;){var h=
+a.text.indexOf(b,d);if(-1==h)break;d=h+b.length;c.push(m(g,h))}g++});for(var h=c.length-1;0<=h;h--)lb(a.doc,b,c[h],m(c[h].line,c[h].ch+b.length))}});b("specialChars",/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff]/g,function(a,b,c){a.state.specialChars=new RegExp(b.source+(b.test("\t")?"":"|\t"),"g");c!=pb&&a.refresh()});b("specialCharPlaceholder",rg,function(a){return a.refresh()},!0);b("electricChars",!0);b("inputStyle",rb?"contenteditable":"textarea",function(){throw Error("inputStyle can not (yet) be changed in a running editor");
+},!0);b("spellcheck",!1,function(a,b){return a.getInputField().spellcheck=b},!0);b("rtlMoveVisually",!hh);b("wholeLineUpdateBefore",!0);b("theme","default",function(a){Nf(a);Ub(a)},!0);b("keyMap","default",function(a,b,c){b=Dc(b);(c=c!=pb&&Dc(c))&&c.detach&&c.detach(a,b);b.attach&&b.attach(a,c||null)});b("extraKeys",null);b("configureMouse",null);b("lineWrapping",!1,ah,!0);b("gutters",[],function(a){zd(a.options);Ub(a)},!0);b("fixedGutter",!0,function(a,b){a.display.gutters.style.left=b?pd(a.display)+
+"px":"0";a.refresh()},!0);b("coverGutterNextToScrollbar",!1,function(a){return gb(a)},!0);b("scrollbarStyle","native",function(a){Se(a);gb(a);a.display.scrollbars.setScrollTop(a.doc.scrollTop);a.display.scrollbars.setScrollLeft(a.doc.scrollLeft)},!0);b("lineNumbers",!1,function(a){zd(a.options);Ub(a)},!0);b("firstLineNumber",1,Ub,!0);b("lineNumberFormatter",function(a){return a},Ub,!0);b("showCursorWhenSelecting",!1,Cb,!0);b("resetSelectionOnContextMenu",!0);b("lineWiseCopyCut",!0);b("pasteLinesPerSelection",
+!0);b("readOnly",!1,function(a,b){"nocursor"==b&&(Db(a),a.display.input.blur());a.display.input.readOnlyChanged(b)});b("disableInput",!1,function(a,b){b||a.display.input.reset()},!0);b("dragDrop",!0,$g);b("allowDropFileTypes",null);b("cursorBlinkRate",530);b("cursorScrollMargin",0);b("cursorHeight",1,Cb,!0);b("singleCursorHeightPerLine",!0,Cb,!0);b("workTime",100);b("workDelay",100);b("flattenSpans",!0,Ib,!0);b("addModeClass",!1,Ib,!0);b("pollInterval",100);b("undoDepth",200,function(a,b){return a.doc.history.undoDepth=
+b});b("historyEventDelay",1250);b("viewportMargin",10,function(a){return a.refresh()},!0);b("maxHighlightLength",1E4,Ib,!0);b("moveInputWithCursor",!0,function(a,b){b||a.display.input.resetPosition()});b("tabindex",null,function(a,b){return a.display.input.getField().tabIndex=b||""});b("autofocus",null);b("direction","ltr",function(a,b){return a.doc.setDirection(b)},!0)})(E);(function(a){var b=a.optionHandlers,c=a.helpers={};a.prototype={constructor:a,focus:function(){window.focus();this.display.input.focus()},
+setOption:function(a,c){var f=this.options,g=f[a];f[a]==c&&"mode"!=a||(f[a]=c,b.hasOwnProperty(a)&&L(this,b[a])(this,c,g),F(this,"optionChange",this,a))},getOption:function(a){return this.options[a]},getDoc:function(){return this.doc},addKeyMap:function(a,b){this.state.keyMaps[b?"push":"unshift"](Dc(a))},removeKeyMap:function(a){for(var b=this.state.keyMaps,c=0;c<b.length;++c)if(b[c]==a||b[c].name==a)return b.splice(c,1),!0},addOverlay:T(function(b,c){var f=b.token?b:a.getMode(this.options,b);if(f.startState)throw Error("Overlays may not be stateful.");
+bg(this.state.overlays,{mode:f,modeSpec:b,opaque:c&&c.opaque,priority:c&&c.priority||0},function(a){return a.priority});this.state.modeGen++;V(this)}),removeOverlay:T(function(a){for(var b=this.state.overlays,c=0;c<b.length;++c){var g=b[c].modeSpec;if(g==a||"string"==typeof a&&g.name==a)return b.splice(c,1),this.state.modeGen++,void V(this)}}),indentLine:T(function(a,b,c){"string"!=typeof b&&"number"!=typeof b&&(b=null==b?this.options.smartIndent?"smart":"prev":b?"add":"subtract");sb(this.doc,a)&&
+Vb(this,a,b,c)}),indentSelection:T(function(a){for(var b=this.doc.sel.ranges,c=-1,g=0;g<b.length;g++){var h=b[g];if(h.empty())h.head.line>c&&(Vb(this,h.head.line,a,!0),c=h.head.line,g==this.doc.sel.primIndex&&fb(this));else{for(var k=h.from(),h=h.to(),l=Math.max(c,k.line),c=Math.min(this.lastLine(),h.line-(h.ch?0:1))+1,h=l;h<c;++h)Vb(this,h,a);h=this.doc.sel.ranges;0==k.ch&&b.length==h.length&&0<h[g].from().ch&&Fd(this.doc,g,new A(k,h[g].to()),pa)}}}),getTokenAt:function(a,b){return le(this,a,b)},
+getLineTokens:function(a,b){return le(this,m(a),b,!0)},getTokenTypeAt:function(a){a=v(this.doc,a);var b;b=je(this,w(this.doc,a.line));var c=0,g=(b.length-1)/2;a=a.ch;if(0==a)b=b[2];else for(;;){var h=c+g>>1;if((h?b[2*h-1]:0)>=a)g=h;else{if(!(b[2*h+1]<a)){b=b[2*h+2];break}c=h+1}}c=b?b.indexOf("overlay "):-1;return 0>c?b:0==c?null:b.slice(0,c-1)},getModeAt:function(b){var c=this.doc.mode;return c.innerMode?a.innerMode(c,this.getTokenAt(b).state).mode:c},getHelper:function(a,b){return this.getHelpers(a,
+b)[0]},getHelpers:function(a,b){var f=[];if(!c.hasOwnProperty(b))return f;var g=c[b],h=this.getModeAt(a);if("string"==typeof h[b])g[h[b]]&&f.push(g[h[b]]);else if(h[b])for(var k=0;k<h[b].length;k++){var l=g[h[b][k]];l&&f.push(l)}else h.helperType&&g[h.helperType]?f.push(g[h.helperType]):g[h.name]&&f.push(g[h.name]);for(k=0;k<g._global.length;k++)l=g._global[k],l.pred(h,this)&&-1==N(f,l.val)&&f.push(l.val);return f},getStateAfter:function(a,b){var c=this.doc;return a=Math.max(c.first,Math.min(null==
+a?c.first+c.size-1:a,c.first+c.size-1)),wb(this,a+1,b).state},cursorCoords:function(a,b){var c,g=this.doc.sel.primary();return c=null==a?g.head:"object"==typeof a?v(this.doc,a):a?g.from():g.to(),ia(this,c,b||"page")},charCoords:function(a,b){return rc(this,v(this.doc,a),b||"page")},coordsChar:function(a,b){return a=Fe(this,a,b||"page"),od(this,a.left,a.top)},lineAtHeight:function(a,b){return a=Fe(this,{top:a,left:0},b||"page").top,Ha(this.doc,a+this.display.viewOffset)},heightAtLine:function(a,b,
+c){var g=!1;if("number"==typeof a){var h=this.doc.first+this.doc.size-1;a<this.doc.first?a=this.doc.first:a>h&&(a=h,g=!0);a=w(this.doc,a)}return Ka(this,a,{top:0,left:0},b||"page",c||g).top+(g?this.doc.height-na(a):0)},defaultTextHeight:function(){return Oa(this.display)},defaultCharWidth:function(){return Ab(this.display)},getViewport:function(){return{from:this.display.viewFrom,to:this.display.viewTo}},addWidget:function(a,b,c,g,h){var k=this.display;a=ia(this,v(this.doc,a));var l=a.bottom,n=a.left;
+if(b.style.position="absolute",b.setAttribute("cm-ignore-events","true"),this.display.input.setUneditable(b),k.sizer.appendChild(b),"over"==g)l=a.top;else if("above"==g||"near"==g){var m=Math.max(k.wrapper.clientHeight,this.doc.height),p=Math.max(k.sizer.clientWidth,k.lineSpace.clientWidth);("above"==g||a.bottom+b.offsetHeight>m)&&a.top>b.offsetHeight?l=a.top-b.offsetHeight:a.bottom+b.offsetHeight<=m&&(l=a.bottom);n+b.offsetWidth>p&&(n=p-b.offsetWidth)}b.style.top=l+"px";b.style.left=b.style.right=
+"";"right"==h?(n=k.sizer.clientWidth-b.offsetWidth,b.style.right="0px"):("left"==h?n=0:"middle"==h&&(n=(k.sizer.clientWidth-b.offsetWidth)/2),b.style.left=n+"px");c&&(a=vd(this,{left:n,top:l,right:n+b.offsetWidth,bottom:l+b.offsetHeight}),null!=a.scrollTop&&Fb(this,a.scrollTop),null!=a.scrollLeft&&Sa(this,a.scrollLeft))},triggerOnKeyDown:T(Hf),triggerOnKeyPress:T(Jf),triggerOnKeyUp:If,triggerOnMouseDown:T(Kf),execCommand:function(a){if(Qb.hasOwnProperty(a))return Qb[a].call(null,this)},triggerElectric:T(function(a){Sf(this,
+a)}),findPosH:function(a,b,c,g){var h=1;0>b&&(h=-1,b=-b);a=v(this.doc,a);for(var k=0;k<b&&(a=Pd(this.doc,a,h,c,g),!a.hitSide);++k);return a},moveH:T(function(a,b){var c=this;this.extendSelectionsBy(function(g){return c.display.shift||c.doc.extend||g.empty()?Pd(c.doc,g.head,a,b,c.options.rtlMoveVisually):0>a?g.from():g.to()},Zb)}),deleteH:T(function(a,b){var c=this.doc;this.doc.sel.somethingSelected()?c.replaceSelection("",null,"+delete"):ob(this,function(g){var h=Pd(c,g.head,a,b,!1);return 0>a?{from:h,
+to:g.head}:{from:g.head,to:h}})}),findPosV:function(a,b,c,g){var h=1;0>b&&(h=-1,b=-b);a=v(this.doc,a);for(var k=0;k<b;++k){var l=ia(this,a,"div");if(null==g?g=l.left:l.left=g,a=Xf(this,l,h,c),a.hitSide)break}return a},moveV:T(function(a,b){var c=this,g=this.doc,h=[],k=!this.display.shift&&!g.extend&&g.sel.somethingSelected();if(g.extendSelectionsBy(function(l){if(k)return 0>a?l.from():l.to();var m=ia(c,l.head,"div");null!=l.goalColumn&&(m.left=l.goalColumn);h.push(m.left);var p=Xf(c,m,a,b);return"page"==
+b&&l==g.sel.primary()&&tc(c,rc(c,p,"div").top-m.top),p},Zb),h.length)for(var l=0;l<g.sel.ranges.length;l++)g.sel.ranges[l].goalColumn=h[l]}),findWordAt:function(a){var b=w(this.doc,a.line).text,c=a.ch,g=a.ch;if(b){var h=this.getHelper(a,"wordChars");"before"!=a.sticky&&g!=b.length||!c?++g:--c;for(var k=b.charAt(c),k=fc(k,h)?function(a){return fc(a,h)}:/\s/.test(k)?function(a){return/\s/.test(a)}:function(a){return!/\s/.test(a)&&!fc(a)};0<c&&k(b.charAt(c-1));)--c;for(;g<b.length&&k(b.charAt(g));)++g}return new A(m(a.line,
+c),m(a.line,g))},toggleOverwrite:function(a){null!=a&&a==this.state.overwrite||((this.state.overwrite=!this.state.overwrite)?Ea(this.display.cursorDiv,"CodeMirror-overwrite"):Ra(this.display.cursorDiv,"CodeMirror-overwrite"),F(this,"overwriteToggle",this,this.state.overwrite))},hasFocus:function(){return this.display.input.getField()==qa()},isReadOnly:function(){return!(!this.options.readOnly&&!this.doc.cantEdit)},scrollTo:T(function(a,b){Eb(this,a,b)}),getScrollInfo:function(){var a=this.display.scroller;
+return{left:a.scrollLeft,top:a.scrollTop,height:a.scrollHeight-oa(this)-this.display.barHeight,width:a.scrollWidth-oa(this)-this.display.barWidth,clientHeight:kd(this),clientWidth:Ma(this)}},scrollIntoView:T(function(a,b){null==a?(a={from:this.doc.sel.primary().head,to:null},null==b&&(b=this.options.cursorScrollMargin)):"number"==typeof a?a={from:m(a,0),to:null}:null==a.from&&(a={from:a,to:null});a.to||(a.to=a.from);a.margin=b||0;if(null!=a.from.line){var c=a;uc(this);this.curOp.scrollToPos=c}else Pe(this,
+a.from,a.to,a.margin)}),setSize:T(function(a,b){var c=this,g=function(a){return"number"==typeof a||/^\d+$/.test(String(a))?a+"px":a};null!=a&&(this.display.wrapper.style.width=g(a));null!=b&&(this.display.wrapper.style.height=g(b));this.options.lineWrapping&&Ce(this);var h=this.display.viewFrom;this.doc.iter(h,this.display.viewTo,function(a){if(a.widgets)for(var b=0;b<a.widgets.length;b++)if(a.widgets[b].noHScroll){Aa(c,h,"widget");break}++h});this.curOp.forceUpdate=!0;F(this,"refresh",this)}),operation:function(a){return Y(this,
+a)},startOperation:function(){return Ta(this)},endOperation:function(){return Ua(this)},refresh:T(function(){var a=this.display.cachedTextHeight;V(this);this.curOp.forceUpdate=!0;Bb(this);Eb(this,this.doc.scrollLeft,this.doc.scrollTop);ud(this);(null==a||.5<Math.abs(a-Oa(this.display)))&&qd(this);F(this,"refresh",this)}),swapDoc:T(function(a){var b=this.doc;return b.cm=null,bf(this,a),Bb(this),this.display.input.reset(),Eb(this,a.scrollLeft,a.scrollTop),this.curOp.forceScroll=!0,P(this,"swapDoc",
+this,b),b}),getInputField:function(){return this.display.input.getField()},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}};ab(a);a.registerHelper=function(b,e,f){c.hasOwnProperty(b)||(c[b]=a[b]={_global:[]});c[b][e]=f};a.registerGlobalHelper=function(b,e,f,g){a.registerHelper(b,e,g);c[b]._global.push({pred:f,val:g})}})(E);var kh="iter insert remove copy getEditor constructor".split(" "),
+cc;for(cc in W.prototype)W.prototype.hasOwnProperty(cc)&&0>N(kh,cc)&&(E.prototype[cc]=function(a){return function(){return a.apply(this.doc,arguments)}}(W.prototype[cc]));return ab(W),E.inputStyles={textarea:I,contenteditable:y},E.defineMode=function(a){E.defaults.mode||"null"==a||(E.defaults.mode=a);jg.apply(this,arguments)},E.defineMIME=function(a,b){bb[a]=b},E.defineMode("null",function(){return{token:function(a){return a.skipToEnd()}}}),E.defineMIME("text/plain","null"),E.defineExtension=function(a,
+b){E.prototype[a]=b},E.defineDocExtension=function(a,b){W.prototype[a]=b},E.fromTextArea=function(a,b){function c(){a.value=k.getValue()}if(b=b?Fa(b):{},b.value=a.value,!b.tabindex&&a.tabIndex&&(b.tabindex=a.tabIndex),!b.placeholder&&a.placeholder&&(b.placeholder=a.placeholder),null==b.autofocus){var d=qa();b.autofocus=d==a||null!=a.getAttribute("autofocus")&&d==document.body}var e;if(a.form&&(t(a.form,"submit",c),!b.leaveSubmitMethodAlone)){var f=a.form;e=f.submit;try{var g=f.submit=function(){c();
+f.submit=e;f.submit();f.submit=g}}catch(h){}}b.finishInit=function(b){b.save=c;b.getTextArea=function(){return a};b.toTextArea=function(){b.toTextArea=isNaN;c();a.parentNode.removeChild(b.getWrapperElement());a.style.display="";a.form&&(aa(a.form,"submit",c),"function"==typeof a.form.submit&&(a.form.submit=e))}};a.style.display="none";var k=E(function(b){return a.parentNode.insertBefore(b,a.nextSibling)},b);return k},function(a){a.off=aa;a.on=t;a.wheelEventPixels=Bg;a.Doc=W;a.splitLines=Od;a.countColumn=
+ea;a.findColumn=Lc;a.isWordChar=Nc;a.Pass=Fc;a.signal=F;a.Line=hb;a.changeEnd=Ba;a.scrollbarModel=Te;a.Pos=m;a.cmpPos=x;a.modes=cd;a.mimeModes=bb;a.resolveMode=mc;a.getMode=dd;a.modeExtensions=cb;a.extendMode=kg;a.copyState=La;a.startState=ge;a.innerMode=ed;a.commands=Qb;a.keyMap=Pb;a.keyName=Df;a.isModifierKey=Af;a.lookupKey=nb;a.normalizeKeyMap=Ng;a.StringStream=H;a.SharedTextMarker=Ob;a.TextMarker=Ca;a.LineWidget=Nb;a.e_preventDefault=S;a.e_stopPropagation=ee;a.e_stop=vb;a.addClass=Ea;a.contains=
+va;a.rmClass=Ra;a.keyNames=Da}(E),E.version="5.28.0",E});(function(ga){"function"==typeof ga.define&&ga.define("core",["codemirror.js"],function(X){ga.CodeMirror=X})})(this);
\ No newline at end of file
diff --git a/js/ckeditor/plugins/codemirror/js/codemirror.mode.bbcode.min.js b/js/ckeditor/plugins/codemirror/js/codemirror.mode.bbcode.min.js
new file mode 100644 (file)
index 0000000..b881206
--- /dev/null
@@ -0,0 +1,4 @@
+CodeMirror.defineMode("bbcode",function(h){var k,l,m;k="b i u s img quote code list table  tr td size color url";l="* :-) hr cut";h.hasOwnProperty("bbCodeTags")&&(k=h.bbCodeTags);h.hasOwnProperty("bbCodeUnaryTags")&&(l=h.bbCodeUnaryTags);var c={cont:function(a,b){m=b;return a},escapeRegEx:function(a){return a.replace(/([\:\-\)\(\*\+\?\[\]])/g,"\\$1")}},n=/[a-zA-Z0-9_]/,p=/['"]/,q=new RegExp("(?:"+c.escapeRegEx(k).split(" ").join("|")+")"),r=new RegExp("(?:"+c.escapeRegEx(l).split(" ").join("|")+")"),
+f={tokenizer:function(a,b){if(a.eatSpace())return null;if(a.match("[",!0))return b.tokenize=f.bbcode,c.cont("tag","startTag");a.next();return null},inAttribute:function(a){return function(b,c){for(var d=null,g=null;!b.eol();){g=b.peek();if(b.next()==a&&"\\"!==d){c.tokenize=f.bbcode;break}d=g}return"string"}},bbcode:function(a,b){if(a.match("]",!0))return b.tokenize=f.tokenizer,c.cont("tag",null);if(a.match("[",!0))return c.cont("tag","startTag");var e=a.next();if(p.test(e))return b.tokenize=f.inAttribute(e),
+c.cont("string","string");if(/\d/.test(e))return a.eatWhile(/\d/),c.cont("number","number");if("whitespace"==b.last)return a.eatWhile(n),c.cont("attribute","modifier");if("property"==b.last)return a.eatWhile(n),c.cont("property",null);if(/\s/.test(e))return m="whitespace",null;var d="";"/"!=e&&(d+=e);for(var g=null;g=a.eat(n);)d+=g;return r.test(d)?c.cont("atom","atom"):q.test(d)?c.cont("keyword","keyword"):/\s/.test(e)?null:c.cont("tag","tag")}};return{startState:function(){return{tokenize:f.tokenizer,
+mode:"bbcode",last:null}},token:function(a,b){var c=b.tokenize(a,b);b.last=m;return c},electricChars:""}});CodeMirror.defineMIME("text/x-bbcode","bbcode");
\ No newline at end of file
diff --git a/js/ckeditor/plugins/codemirror/js/codemirror.mode.bbcodemixed.min.js b/js/ckeditor/plugins/codemirror/js/codemirror.mode.bbcodemixed.min.js
new file mode 100644 (file)
index 0000000..4234575
--- /dev/null
@@ -0,0 +1,76 @@
+(function(g){"object"==typeof exports&&"object"==typeof module?g(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],g):g(CodeMirror)})(function(g){var z={autoSelfClosers:{area:!0,base:!0,br:!0,col:!0,command:!0,embed:!0,frame:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0,menuitem:!0},implicitlyClosed:{dd:!0,li:!0,optgroup:!0,option:!0,p:!0,rp:!0,rt:!0,tbody:!0,td:!0,tfoot:!0,th:!0,tr:!0},contextGrabbers:{dd:{dd:!0,
+dt:!0},dt:{dd:!0,dt:!0},li:{li:!0},option:{option:!0,optgroup:!0},optgroup:{optgroup:!0},p:{address:!0,article:!0,aside:!0,blockquote:!0,dir:!0,div:!0,dl:!0,fieldset:!0,footer:!0,form:!0,h1:!0,h2:!0,h3:!0,h4:!0,h5:!0,h6:!0,header:!0,hgroup:!0,hr:!0,menu:!0,nav:!0,ol:!0,p:!0,pre:!0,section:!0,table:!0,ul:!0},rp:{rp:!0,rt:!0},rt:{rp:!0,rt:!0},tbody:{tbody:!0,tfoot:!0},td:{td:!0,th:!0},tfoot:{tbody:!0},th:{td:!0,th:!0},thead:{tbody:!0,tfoot:!0},tr:{tr:!0}},doNotIndent:{pre:!0},allowUnquoted:!0,allowMissing:!0,
+caseFold:!0},C={autoSelfClosers:{},implicitlyClosed:{},contextGrabbers:{},doNotIndent:{},allowUnquoted:!1,allowMissing:!1,caseFold:!1};g.defineMode("xml",function(r,n){function B(a,f){function d(d){f.tokenize=d;return d(a,f)}var c=a.next();if("\x3c"==c){if(a.eat("!"))return a.eat("[")?a.match("CDATA[")?d(A("atom","]]\x3e")):null:a.match("--")?d(A("comment","--\x3e")):a.match("DOCTYPE",!0,!0)?(a.eatWhile(/[\w\._\-]/),d(H(1))):null;if(a.eat("?"))return a.eatWhile(/[\w\._\-]/),f.tokenize=A("meta","?\x3e"),
+"meta";M=a.eat("/")?"closeTag":"openTag";f.tokenize=G;return"tag bracket"}if("\x26"==c)return(a.eat("#")?a.eat("x")?a.eatWhile(/[a-fA-F\d]/)&&a.eat(";"):a.eatWhile(/[\d]/)&&a.eat(";"):a.eatWhile(/[\w\.\-:]/)&&a.eat(";"))?"atom":"error";a.eatWhile(/[^&<]/);return null}function G(a,f){var d=a.next();if("\x3e"==d||"/"==d&&a.eat("\x3e"))return f.tokenize=B,M="\x3e"==d?"endTag":"selfcloseTag","tag bracket";if("\x3d"==d)return M="equals",null;if("\x3c"==d)return f.tokenize=B,f.state=c,f.tagName=f.tagStart=
+null,(d=f.tokenize(a,f))?d+" tag error":"tag error";if(/[\'\"]/.test(d))return f.tokenize=J(d),f.stringStartCol=a.column(),f.tokenize(a,f);a.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/);return"word"}function J(a){var f=function(d,f){for(;!d.eol();)if(d.next()==a){f.tokenize=G;break}return"string"};f.isInAttribute=!0;return f}function A(a,f){return function(d,c){for(;!d.eol();){if(d.match(f)){c.tokenize=B;break}d.next()}return a}}function H(a){return function(f,d){for(var c;null!=(c=f.next());){if("\x3c"==
+c)return d.tokenize=H(a+1),d.tokenize(f,d);if("\x3e"==c)if(1==a){d.tokenize=B;break}else return d.tokenize=H(a-1),d.tokenize(f,d)}return"meta"}}function w(a,f,d){this.prev=a.context;this.tagName=f;this.indent=a.indented;this.startOfLine=d;if(F.doNotIndent.hasOwnProperty(f)||a.context&&a.context.noIndent)this.noIndent=!0}function h(a){a.context&&(a.context=a.context.prev)}function a(a,f){for(var d;a.context;){d=a.context.tagName;if(!F.contextGrabbers.hasOwnProperty(d)||!F.contextGrabbers[d].hasOwnProperty(f))break;
+h(a)}}function c(a,f,d){return"openTag"==a?(d.tagStart=f.column(),K):"closeTag"==a?x:c}function K(a,f,d){if("word"==a)return d.tagName=f.current(),E="tag",l;E="error";return K}function x(a,f,d){if("word"==a){a=f.current();d.context&&d.context.tagName!=a&&F.implicitlyClosed.hasOwnProperty(d.context.tagName)&&h(d);if(d.context&&d.context.tagName==a||!1===F.matchClosing)return E="tag",t;E="tag error";return m}E="error";return m}function t(a,f,d){if("endTag"!=a)return E="error",t;h(d);return c}function m(a,
+f,d){E="error";return t(a,f,d)}function l(R,f,d){if("word"==R)return E="attribute",D;if("endTag"==R||"selfcloseTag"==R){f=d.tagName;var g=d.tagStart;d.tagName=d.tagStart=null;"selfcloseTag"==R||F.autoSelfClosers.hasOwnProperty(f)?a(d,f):(a(d,f),d.context=new w(d,f,g==d.indented));return c}E="error";return l}function D(a,f,d){if("equals"==a)return u;F.allowMissing||(E="error");return l(a,f,d)}function u(a,f,d){if("string"==a)return L;if("word"==a&&F.allowUnquoted)return E="string",l;E="error";return l(a,
+f,d)}function L(a,f,d){return"string"==a?L:l(a,f,d)}var V=r.indentUnit,F={},Y=n.htmlMode?z:C,S;for(S in Y)F[S]=Y[S];for(S in n)F[S]=n[S];var M,E;B.isInText=!0;return{startState:function(a){var f={tokenize:B,state:c,indented:a||0,tagName:null,tagStart:null,context:null};null!=a&&(f.baseIndent=a);return f},token:function(a,f){!f.tagName&&a.sol()&&(f.indented=a.indentation());if(a.eatSpace())return null;M=null;var d=f.tokenize(a,f);(d||M)&&"comment"!=d&&(E=null,f.state=f.state(M||d,a,f),E&&(d="error"==
+E?d+" error":E));return d},indent:function(a,f,d){var c=a.context;if(a.tokenize.isInAttribute)return a.tagStart==a.indented?a.stringStartCol+1:a.indented+V;if(c&&c.noIndent)return g.Pass;if(a.tokenize!=G&&a.tokenize!=B)return d?d.match(/^(\s*)/)[0].length:0;if(a.tagName)return!1!==F.multilineTagIndentPastTag?a.tagStart+a.tagName.length+2:a.tagStart+V*(F.multilineTagIndentFactor||1);if(F.alignCDATA&&/<!\[CDATA\[/.test(f))return 0;if((f=f&&/^<(\/)?([\w_:\.-]*)/.exec(f))&&f[1])for(;c;)if(c.tagName==
+f[2]){c=c.prev;break}else if(F.implicitlyClosed.hasOwnProperty(c.tagName))c=c.prev;else break;else if(f)for(;c;)if((d=F.contextGrabbers[c.tagName])&&d.hasOwnProperty(f[2]))c=c.prev;else break;for(;c&&c.prev&&!c.startOfLine;)c=c.prev;return c?c.indent+V:a.baseIndent||0},electricInput:/<\/[\s\w:]+>$/,blockCommentStart:"\x3c!--",blockCommentEnd:"--\x3e",configuration:F.htmlMode?"html":"xml",helperType:F.htmlMode?"html":"xml",skipAttribute:function(a){a.state==u&&(a.state=l)}}});g.defineMIME("text/xml",
+"xml");g.defineMIME("application/xml","xml");g.mimeModes.hasOwnProperty("text/html")||g.defineMIME("text/html",{name:"xml",htmlMode:!0})});
+(function(g){"object"==typeof exports&&"object"==typeof module?g(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],g):g(CodeMirror)})(function(g){function z(g,r,n){return/^(?:operator|sof|keyword c|case|new|export|default|[\[{}\(,;:]|=>)$/.test(r.lastType)||"quasi"==r.lastType&&/\{\s*$/.test(g.string.slice(0,g.pos-(n||0)))}g.defineMode("javascript",function(C,r){function n(p,a,e){da=p;fa=e;return a}function B(a,e){var b=a.next();if('"'==b||"'"==
+b)return e.tokenize=G(b),e.tokenize(a,e);if("."==b&&a.match(/^\d+(?:[eE][+\-]?\d+)?/))return n("number","number");if("."==b&&a.match(".."))return n("spread","meta");if(/[\[\]{}\(\),;\:\.]/.test(b))return n(b);if("\x3d"==b&&a.eat("\x3e"))return n("\x3d\x3e","operator");if("0"==b&&a.eat(/x/i))return a.eatWhile(/[\da-f]/i),n("number","number");if("0"==b&&a.eat(/o/i))return a.eatWhile(/[0-7]/i),n("number","number");if("0"==b&&a.eat(/b/i))return a.eatWhile(/[01]/i),n("number","number");if(/\d/.test(b))return a.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/),
+n("number","number");if("/"==b){if(a.eat("*"))return e.tokenize=J,J(a,e);if(a.eat("/"))return a.skipToEnd(),n("comment","comment");if(z(a,e,1)){a:for(var b=!1,v,c=!1;null!=(v=a.next());){if(!b){if("/"==v&&!c)break a;"["==v?c=!0:c&&"]"==v&&(c=!1)}b=!b&&"\\"==v}a.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/);return n("regexp","string-2")}a.eatWhile(ga);return n("operator","operator",a.current())}if("`"==b)return e.tokenize=A,A(a,e);if("#"==b)return a.skipToEnd(),n("error","error");if(ga.test(b))return"\x3e"==
+b&&e.lexical&&"\x3e"==e.lexical.type||a.eatWhile(ga),n("operator","operator",a.current());if(ma.test(b)){a.eatWhile(ma);b=a.current();if("."!=e.lastType){if(sa.propertyIsEnumerable(b))return v=sa[b],n(v.type,v.style,b);if("async"==b&&a.match(/^\s*[\(\w]/,!1))return n("async","keyword",b)}return n("variable","variable",b)}}function G(a){return function(e,b){var v=!1,c;if(ha&&"@"==e.peek()&&e.match(Aa))return b.tokenize=B,n("jsonld-keyword","meta");for(;null!=(c=e.next())&&(c!=a||v);)v=!v&&"\\"==c;
+v||(b.tokenize=B);return n("string","string")}}function J(a,e){for(var b=!1,v;v=a.next();){if("/"==v&&b){e.tokenize=B;break}b="*"==v}return n("comment","comment")}function A(a,e){for(var b=!1,v;null!=(v=a.next());){if(!b&&("`"==v||"$"==v&&a.eat("{"))){e.tokenize=B;break}b=!b&&"\\"==v}return n("quasi","string-2",a.current())}function H(a,e){e.fatArrowAt&&(e.fatArrowAt=null);var b=a.string.indexOf("\x3d\x3e",a.start);if(!(0>b)){if(O){var v=/:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(a.string.slice(a.start,
+b));v&&(b=v.index)}for(var v=0,c=!1,b=b-1;0<=b;--b){var d=a.string.charAt(b),q="([{}])".indexOf(d);if(0<=q&&3>q){if(!v){++b;break}if(0==--v){"("==d&&(c=!0);break}}else if(3<=q&&6>q)++v;else if(ma.test(d))c=!0;else{if(/["'\/]/.test(d))return;if(c&&!v){++b;break}}}c&&!v&&(e.fatArrowAt=b)}}function w(a,e,b,c,d,q){this.indented=a;this.column=e;this.type=b;this.prev=d;this.info=q;null!=c&&(this.align=c)}function h(){for(var a=arguments.length-1;0<=a;a--)k.cc.push(arguments[a])}function a(){h.apply(null,
+arguments);return!0}function c(a){function e(b){for(;b;b=b.next)if(b.name==a)return!0;return!1}var b=k.state;k.marked="def";b.context?e(b.localVars)||(b.localVars={name:a,next:b.localVars}):!e(b.globalVars)&&r.globalVars&&(b.globalVars={name:a,next:b.globalVars})}function K(){k.state.context={prev:k.state.context,vars:k.state.localVars};k.state.localVars=Ba}function x(){k.state.localVars=k.state.context.vars;k.state.context=k.state.context.prev}function t(a,e){var b=function(){var b=k.state,c=b.indented;
+if("stat"==b.lexical.type)c=b.lexical.indented;else for(var d=b.lexical;d&&")"==d.type&&d.align;d=d.prev)c=d.indented;b.lexical=new w(c,k.stream.column(),a,null,b.lexical,e)};b.lex=!0;return b}function m(){var a=k.state;a.lexical.prev&&(")"==a.lexical.type&&(a.indented=a.lexical.indented),a.lexical=a.lexical.prev)}function l(p){function b(e){return e==p?a():";"==p?h():a(b)}return b}function D(p,b){return"var"==p?a(t("vardef",b.length),N,l(";"),m):"keyword a"==p?a(t("form"),V,D,m):"keyword b"==p?a(t("form"),
+D,m):"{"==p?a(t("}"),ia,m):";"==p?a():"if"==p?("else"==k.state.lexical.info&&k.state.cc[k.state.cc.length-1]==m&&k.state.cc.pop()(),a(t("form"),V,D,m,ta)):"function"==p?a(P):"for"==p?a(t("form"),Ca,D,m):"variable"==p?O&&"type"==b?(k.marked="keyword",a(I,l("operator"),I,l(";"))):a(t("stat"),Da):"switch"==p?a(t("form"),V,l("{"),t("}","switch"),ia,m,m):"case"==p?a(u,l(":")):"default"==p?a(l(":")):"catch"==p?a(t("form"),K,l("("),na,l(")"),D,m,x):"class"==p?a(t("form"),ua,m):"export"==p?a(t("stat"),Ea,
+m):"import"==p?a(t("stat"),Fa,m):"module"==p?a(t("form"),e,l("{"),t("}"),ia,m,m):"async"==p?a(D):"@"==b?a(u,D):h(t("stat"),u,l(";"),m)}function u(a){return F(a,!1)}function L(a){return F(a,!0)}function V(p){return"("!=p?h():a(t(")"),u,l(")"),m)}function F(p,b){if(k.state.fatArrowAt==k.stream.start){var c=b?ra:d;if("("==p)return a(K,t(")"),Q(e,")"),m,l("\x3d\x3e"),c,x);if("variable"==p)return h(K,e,l("\x3d\x3e"),c,x)}c=b?E:M;return Ga.hasOwnProperty(p)?a(c):"function"==p?a(P,c):"class"==p?a(t("form"),
+Ha,m):"keyword c"==p||"async"==p?a(b?S:Y):"("==p?a(t(")"),Y,l(")"),m,c):"operator"==p||"spread"==p?a(b?L:u):"["==p?a(t("]"),Ia,m,c):"{"==p?ea(oa,"}",null,c):"quasi"==p?h(R,c):"new"==p?a(za(b)):a()}function Y(a){return a.match(/[;\}\)\],]/)?h():h(u)}function S(a){return a.match(/[;\}\)\],]/)?h():h(L)}function M(p,b){return","==p?a(u):E(p,b,!1)}function E(b,e,c){var v=0==c?M:E,q=0==c?u:L;if("\x3d\x3e"==b)return a(K,c?ra:d,x);if("operator"==b)return/\+\+|--/.test(e)?a(v):"?"==e?a(u,l(":"),q):a(q);if("quasi"==
+b)return h(R,v);if(";"!=b){if("("==b)return ea(L,")","call",v);if("."==b)return a(Ja,v);if("["==b)return a(t("]"),Y,l("]"),m,v);if(O&&"as"==e)return k.marked="keyword",a(I,v)}}function R(b,e){return"quasi"!=b?h():"${"!=e.slice(e.length-2)?a(R):a(u,f)}function f(b){if("}"==b)return k.marked="string-2",k.state.tokenize=A,a(R)}function d(a){H(k.stream,k.state);return h("{"==a?D:u)}function ra(a){H(k.stream,k.state);return h("{"==a?D:L)}function za(b){return function(e){return"."==e?a(b?Ka:La):h(b?L:
+u)}}function La(b,e){if("target"==e)return k.marked="keyword",a(M)}function Ka(b,e){if("target"==e)return k.marked="keyword",a(E)}function Da(b){return":"==b?a(m,D):h(M,l(";"),m)}function Ja(b){if("variable"==b)return k.marked="property",a()}function oa(b,e){if("async"==b)return k.marked="property",a(oa);if("variable"==b||"keyword"==k.style)return k.marked="property","get"==e||"set"==e?a(Ma):a(W);if("number"==b||"string"==b)return k.marked=ha?"property":k.style+" property",a(W);if("jsonld-keyword"==
+b)return a(W);if("modifier"==b)return a(oa);if("["==b)return a(u,l("]"),W);if("spread"==b)return a(u,W);if(":"==b)return h(W)}function Ma(b){if("variable"!=b)return h(W);k.marked="property";return a(P)}function W(b){if(":"==b)return a(L);if("("==b)return h(P)}function Q(b,e,c){function d(q,f){if(c?-1<c.indexOf(q):","==q){var g=k.state.lexical;"call"==g.info&&(g.pos=(g.pos||0)+1);return a(function(a,c){return a==e||c==e?h():h(b)},d)}return q==e||f==e?a():a(l(e))}return function(c,q){return c==e||q==
+e?a():h(b,d)}}function ea(b,e,c){for(var d=3;d<arguments.length;d++)k.cc.push(arguments[d]);return a(t(e,c),Q(b,e),m)}function ia(b){return"}"==b?a():h(D,ia)}function T(b,e){if(O){if(":"==b)return a(I);if("?"==e)return a(T)}}function I(b){if("variable"==b)return k.marked="type",a(y);if("string"==b||"number"==b||"atom"==b)return a(y);if("{"==b)return a(t("}"),Q(Z,"}",",;"),m,y);if("("==b)return a(Q(aa,")"),U)}function U(b){if("\x3d\x3e"==b)return a(I)}function Z(b,e){if("variable"==b||"keyword"==k.style)return k.marked=
+"property",a(Z);if("?"==e)return a(Z);if(":"==b)return a(I);if("["==b)return a(u,T,l("]"),Z)}function aa(b){if("variable"==b)return a(aa);if(":"==b)return a(I)}function y(b,e){if("\x3c"==e)return a(t("\x3e"),Q(I,"\x3e"),m,y);if("|"==e||"."==b)return a(I);if("["==b)return a(l("]"),y);if("extends"==e)return a(I)}function N(){return h(e,T,b,Na)}function e(b,d){if("modifier"==b)return a(e);if("variable"==b)return c(d),a();if("spread"==b)return a(e);if("["==b)return ea(e,"]");if("{"==b)return ea(q,"}")}
+function q(p,d){if("variable"==p&&!k.stream.match(/^\s*:/,!1))return c(d),a(b);"variable"==p&&(k.marked="property");return"spread"==p?a(e):"}"==p?h():a(l(":"),e,b)}function b(b,e){if("\x3d"==e)return a(L)}function Na(b){if(","==b)return a(N)}function ta(b,e){if("keyword b"==b&&"else"==e)return a(t("form","else"),D,m)}function Ca(b){if("("==b)return a(t(")"),Oa,l(")"),m)}function Oa(b){return"var"==b?a(N,l(";"),ja):";"==b?a(ja):"variable"==b?a(Pa):h(u,l(";"),ja)}function Pa(b,e){return"in"==e||"of"==
+e?(k.marked="keyword",a(u)):a(M,ja)}function ja(b,e){return";"==b?a(va):"in"==e||"of"==e?(k.marked="keyword",a(u)):h(u,l(";"),va)}function va(b){")"!=b&&a(u)}function P(b,e){if("*"==e)return k.marked="keyword",a(P);if("variable"==b)return c(e),a(P);if("("==b)return a(K,t(")"),Q(na,")"),m,T,D,x);if(O&&"\x3c"==e)return a(t("\x3e"),Q(I,"\x3e"),m,P)}function na(c){return"spread"==c?a(na):h(e,T,b)}function Ha(a,b){return"variable"==a?ua(a,b):ka(a,b)}function ua(b,e){if("variable"==b)return c(e),a(ka)}
+function ka(b,e){if("\x3c"==e)return a(t("\x3e"),Q(I,"\x3e"),m,ka);if("extends"==e||"implements"==e||O&&","==b)return a(O?I:u,ka);if("{"==b)return a(t("}"),X,m)}function X(b,e){if("variable"==b||"keyword"==k.style){if(("async"==e||"static"==e||"get"==e||"set"==e||O&&("public"==e||"private"==e||"protected"==e||"readonly"==e||"abstract"==e))&&k.stream.match(/^\s+[\w$\xa1-\uffff]/,!1))return k.marked="keyword",a(X);k.marked="property";return a(O?pa:P,X)}if("["==b)return a(u,l("]"),O?pa:P,X);if("*"==
+e)return k.marked="keyword",a(X);if(";"==b)return a(X);if("}"==b)return a();if("@"==e)return a(u,X)}function pa(e,c){return"?"==c?a(pa):":"==e?a(I,b):"\x3d"==c?a(L):h(P)}function Ea(b,e){return"*"==e?(k.marked="keyword",a(qa,l(";"))):"default"==e?(k.marked="keyword",a(u,l(";"))):"{"==b?a(Q(wa,"}"),qa,l(";")):h(D)}function wa(b,e){if("as"==e)return k.marked="keyword",a(l("variable"));if("variable"==b)return h(L,wa)}function Fa(b){return"string"==b?a():h(la,xa,qa)}function la(b,e){if("{"==b)return ea(la,
+"}");"variable"==b&&c(e);"*"==e&&(k.marked="keyword");return a(Qa)}function xa(b){if(","==b)return a(la,xa)}function Qa(b,e){if("as"==e)return k.marked="keyword",a(la)}function qa(b,e){if("from"==e)return k.marked="keyword",a(u)}function Ia(b){return"]"==b?a():h(Q(L,"]"))}var ba=C.indentUnit,ya=r.statementIndent,ha=r.jsonld,ca=r.json||ha,O=r.typescript,ma=r.wordCharacters||/[\w$\xa1-\uffff]/,sa=function(){function a(b){return{type:b,style:"keyword"}}var b=a("keyword a"),e=a("keyword b"),c=a("keyword c"),
+d=a("operator"),q={type:"atom",style:"atom"},b={"if":a("if"),"while":b,"with":b,"else":e,"do":e,"try":e,"finally":e,"return":c,"break":c,"continue":c,"new":a("new"),"delete":c,"throw":c,"debugger":c,"var":a("var"),"const":a("var"),let:a("var"),"function":a("function"),"catch":a("catch"),"for":a("for"),"switch":a("switch"),"case":a("case"),"default":a("default"),"in":d,"typeof":d,"instanceof":d,"true":q,"false":q,"null":q,undefined:q,NaN:q,Infinity:q,"this":a("this"),"class":a("class"),"super":a("atom"),
+yield:c,"export":a("export"),"import":a("import"),"extends":c,await:c};if(O){var e={type:"variable",style:"type"},c={"interface":a("class"),"implements":c,namespace:c,module:a("module"),"enum":a("module"),"public":a("modifier"),"private":a("modifier"),"protected":a("modifier"),"abstract":a("modifier"),string:e,number:e,"boolean":e,any:e},f;for(f in c)b[f]=c[f]}return b}(),ga=/[+\-*&%=<>!?|~^@]/,Aa=/^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/,da,fa,Ga={atom:!0,
+number:!0,variable:!0,string:!0,regexp:!0,"this":!0,"jsonld-keyword":!0},k={state:null,column:null,marked:null,cc:null},Ba={name:"this",next:{name:"arguments"}};m.lex=!0;return{startState:function(a){a={tokenize:B,lastType:"sof",cc:[],lexical:new w((a||0)-ba,0,"block",!1),localVars:r.localVars,context:r.localVars&&{vars:r.localVars},indented:a||0};r.globalVars&&"object"==typeof r.globalVars&&(a.globalVars=r.globalVars);return a},token:function(a,b){a.sol()&&(b.lexical.hasOwnProperty("align")||(b.lexical.align=
+!1),b.indented=a.indentation(),H(a,b));if(b.tokenize!=J&&a.eatSpace())return null;var e=b.tokenize(a,b);if("comment"==da)return e;b.lastType="operator"!=da||"++"!=fa&&"--"!=fa?da:"incdec";a:{var c=da,d=fa,q=b.cc;k.state=b;k.stream=a;k.marked=null;k.cc=q;k.style=e;b.lexical.hasOwnProperty("align")||(b.lexical.align=!0);for(;;)if((q.length?q.pop():ca?u:D)(c,d)){for(;q.length&&q[q.length-1].lex;)q.pop()();if(k.marked){e=k.marked;break a}if(c="variable"==c)b:{for(c=b.localVars;c;c=c.next)if(c.name==d){c=
+!0;break b}for(q=b.context;q;q=q.prev)for(c=q.vars;c;c=c.next)if(c.name==d){c=!0;break b}c=void 0}if(c){e="variable-2";break a}break a}}return e},indent:function(a,b){if(a.tokenize==J)return g.Pass;if(a.tokenize!=B)return 0;var e=b&&b.charAt(0),c=a.lexical,q;if(!/^\s*else\b/.test(b))for(var d=a.cc.length-1;0<=d;--d){var f=a.cc[d];if(f==m)c=c.prev;else if(f!=ta)break}for(;!("stat"!=c.type&&"form"!=c.type||"}"!=e&&(!(q=a.cc[a.cc.length-1])||q!=M&&q!=E||/^[,\.=+\-*:?[\(]/.test(b)));)c=c.prev;ya&&")"==
+c.type&&"stat"==c.prev.type&&(c=c.prev);q=c.type;d=e==q;return"vardef"==q?c.indented+("operator"==a.lastType||","==a.lastType?c.info+1:0):"form"==q&&"{"==e?c.indented:"form"==q?c.indented+ba:"stat"==q?(e=c.indented,c="operator"==a.lastType||","==a.lastType||ga.test(b.charAt(0))||/[,.]/.test(b.charAt(0)),e+(c?ya||ba:0)):"switch"!=c.info||d||0==r.doubleIndentSwitch?c.align?c.column+(d?0:1):c.indented+(d?0:ba):c.indented+(/^(?:case|default)\b/.test(b)?ba:2*ba)},electricInput:/^\s*(?:case .*?:|default:|\{|\})$/,
+blockCommentStart:ca?null:"/*",blockCommentEnd:ca?null:"*/",lineComment:ca?null:"//",fold:"brace",closeBrackets:"()[]{}''\"\"``",helperType:ca?"json":"javascript",jsonldMode:ha,jsonMode:ca,expressionAllowed:z,skipExpression:function(a){var b=a.cc[a.cc.length-1];b!=u&&b!=L||a.cc.pop()}}});g.registerHelper("wordChars","javascript",/[\w$]/);g.defineMIME("text/javascript","javascript");g.defineMIME("text/ecmascript","javascript");g.defineMIME("application/javascript","javascript");g.defineMIME("application/x-javascript",
+"javascript");g.defineMIME("application/ecmascript","javascript");g.defineMIME("application/json",{name:"javascript",json:!0});g.defineMIME("application/x-json",{name:"javascript",json:!0});g.defineMIME("application/ld+json",{name:"javascript",jsonld:!0});g.defineMIME("text/typescript",{name:"javascript",typescript:!0});g.defineMIME("application/typescript",{name:"javascript",typescript:!0})});
+(function(g){"object"==typeof exports&&"object"==typeof module?g(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],g):g(CodeMirror)})(function(g){function z(a){for(var c={},g=0;g<a.length;++g)c[a[g].toLowerCase()]=!0;return c}function C(a,c){for(var g=!1,h;null!=(h=a.next());){if(g&&"/"==h){c.tokenize=null;break}g="*"==h}return["comment","comment"]}g.defineMode("css",function(a,c){function h(a,c){aa=c;return a}function K(a,c){var b=a.next();if(w[b]){var d=
+w[b](a,c);if(!1!==d)return d}if("@"==b)return a.eatWhile(/[\w\\\-]/),h("def",a.current());if("\x3d"==b||("~"==b||"|"==b)&&a.eat("\x3d"))return h(null,"compare");if('"'==b||"'"==b)return c.tokenize=l(b),c.tokenize(a,c);if("#"==b)return a.eatWhile(/[\w\\\-]/),h("atom","hash");if("!"==b)return a.match(/^\s*\w*/),h("keyword","important");if(/\d/.test(b)||"."==b&&a.eat(/\d/))return a.eatWhile(/[\w.%]/),h("number","unit");if("-"===b){if(/[\d.]/.test(a.peek()))return a.eatWhile(/[\w.%]/),h("number","unit");
+if(a.match(/^-[\w\\\-]+/))return a.eatWhile(/[\w\\\-]/),a.match(/^\s*:/,!1)?h("variable-2","variable-definition"):h("variable-2","variable");if(a.match(/^\w+-/))return h("meta","meta")}else return/[,+>*\/]/.test(b)?h(null,"select-op"):"."==b&&a.match(/^-?[_a-z][_a-z0-9-]*/i)?h("qualifier","qualifier"):/[:;{}\[\]\(\)]/.test(b)?h(null,b):"u"==b&&a.match(/rl(-prefix)?\(/)||"d"==b&&a.match("omain(")||"r"==b&&a.match("egexp(")?(a.backUp(1),c.tokenize=m,h("property","word")):/[\w\\\-]/.test(b)?(a.eatWhile(/[\w\\\-]/),
+h("property","word")):h(null,null)}function l(a){return function(c,b){for(var d=!1,f;null!=(f=c.next());){if(f==a&&!d){")"==a&&c.backUp(1);break}d=!d&&"\\"==f}if(f==a||!d&&")"!=a)b.tokenize=null;return h("string","string")}}function m(a,c){a.next();a.match(/\s*[\"\')]/,!1)?c.tokenize=null:c.tokenize=l(")");return h(null,"(")}function t(a,c,b){this.type=a;this.indent=c;this.prev=b}function x(a,c,b,d){a.context=new t(b,c.indentation()+(!1===d?0:A),a.context);return b}function f(a){a.context.prev&&(a.context=
+a.context.prev);return a.context.type}function d(a,c,b,d){for(d=d||1;0<d;d--)b.context=b.context.prev;return N[b.context.type](a,c,b)}function u(a){a=a.current().toLowerCase();y=I.hasOwnProperty(a)?"atom":T.hasOwnProperty(a)?"keyword":"variable"}var n=c.inline;c.propertyKeywords||(c=g.resolveMode("text/css"));var A=a.indentUnit,w=c.tokenHooks,r=c.documentTypes||{},H=c.mediaTypes||{},J=c.mediaFeatures||{},z=c.mediaValueKeywords||{},B=c.propertyKeywords||{},D=c.nonStandardPropertyKeywords||{},C=c.fontProperties||
+{},G=c.counterDescriptors||{},T=c.colorKeywords||{},I=c.valueKeywords||{},U=c.allowNested,Z=!0===c.supportsAtComponent,aa,y,N={top:function(a,c,b){if("{"==a)return x(b,c,"block");if("}"==a&&b.context.prev)return f(b);if(Z&&/@component/.test(a))return x(b,c,"atComponentBlock");if(/^@(-moz-)?document$/.test(a))return x(b,c,"documentTypes");if(/^@(media|supports|(-moz-)?document|import)$/.test(a))return x(b,c,"atBlock");if(/^@(font-face|counter-style)/.test(a))return b.stateArg=a,"restricted_atBlock_before";
+if(/^@(-(moz|ms|o|webkit)-)?keyframes$/.test(a))return"keyframes";if(a&&"@"==a.charAt(0))return x(b,c,"at");if("hash"==a)y="builtin";else if("word"==a)y="tag";else{if("variable-definition"==a)return"maybeprop";if("interpolation"==a)return x(b,c,"interpolation");if(":"==a)return"pseudo";if(U&&"("==a)return x(b,c,"parens")}return b.context.type},block:function(a,c,b){if("word"==a){a=c.current().toLowerCase();if(B.hasOwnProperty(a))return y="property","maybeprop";if(D.hasOwnProperty(a))return y="string-2",
+"maybeprop";if(U)return y=c.match(/^\s*:(?:\s|$)/,!1)?"property":"tag","block";y+=" error";return"maybeprop"}if("meta"==a)return"block";if(U||"hash"!=a&&"qualifier"!=a)return N.top(a,c,b);y="error";return"block"},maybeprop:function(a,c,b){return":"==a?x(b,c,"prop"):N[b.context.type](a,c,b)},prop:function(a,c,b){if(";"==a)return f(b);if("{"==a&&U)return x(b,c,"propBlock");if("}"==a||"{"==a)return d(a,c,b);if("("==a)return x(b,c,"parens");if("hash"==a&&!/^#([0-9a-fA-f]{3,4}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(c.current()))y+=
+" error";else if("word"==a)u(c);else if("interpolation"==a)return x(b,c,"interpolation");return"prop"},propBlock:function(a,c,b){return"}"==a?f(b):"word"==a?(y="property","maybeprop"):b.context.type},parens:function(a,c,b){if("{"==a||"}"==a)return d(a,c,b);if(")"==a)return f(b);if("("==a)return x(b,c,"parens");if("interpolation"==a)return x(b,c,"interpolation");"word"==a&&u(c);return"parens"},pseudo:function(a,c,b){return"meta"==a?"pseudo":"word"==a?(y="variable-3",b.context.type):N[b.context.type](a,
+c,b)},documentTypes:function(a,c,b){return"word"==a&&r.hasOwnProperty(c.current())?(y="tag",b.context.type):N.atBlock(a,c,b)},atBlock:function(a,c,b){if("("==a)return x(b,c,"atBlock_parens");if("}"==a||";"==a)return d(a,c,b);if("{"==a)return f(b)&&x(b,c,U?"block":"top");if("interpolation"==a)return x(b,c,"interpolation");"word"==a&&(a=c.current().toLowerCase(),y="only"==a||"not"==a||"and"==a||"or"==a?"keyword":H.hasOwnProperty(a)?"attribute":J.hasOwnProperty(a)?"property":z.hasOwnProperty(a)?"keyword":
+B.hasOwnProperty(a)?"property":D.hasOwnProperty(a)?"string-2":I.hasOwnProperty(a)?"atom":T.hasOwnProperty(a)?"keyword":"error");return b.context.type},atComponentBlock:function(a,c,b){if("}"==a)return d(a,c,b);if("{"==a)return f(b)&&x(b,c,U?"block":"top",!1);"word"==a&&(y="error");return b.context.type},atBlock_parens:function(a,c,b){return")"==a?f(b):"{"==a||"}"==a?d(a,c,b,2):N.atBlock(a,c,b)},restricted_atBlock_before:function(a,c,b){return"{"==a?x(b,c,"restricted_atBlock"):"word"==a&&"@counter-style"==
+b.stateArg?(y="variable","restricted_atBlock_before"):N[b.context.type](a,c,b)},restricted_atBlock:function(a,c,b){return"}"==a?(b.stateArg=null,f(b)):"word"==a?(y="@font-face"==b.stateArg&&!C.hasOwnProperty(c.current().toLowerCase())||"@counter-style"==b.stateArg&&!G.hasOwnProperty(c.current().toLowerCase())?"error":"property","maybeprop"):"restricted_atBlock"},keyframes:function(a,c,b){return"word"==a?(y="variable","keyframes"):"{"==a?x(b,c,"top"):N[b.context.type](a,c,b)},at:function(a,c,b){if(";"==
+a)return f(b);if("{"==a||"}"==a)return d(a,c,b);"word"==a?y="tag":"hash"==a&&(y="builtin");return"at"},interpolation:function(a,c,b){if("}"==a)return f(b);if("{"==a||";"==a)return d(a,c,b);"word"==a?y="variable":"variable"!=a&&"("!=a&&")"!=a&&(y="error");return"interpolation"}};return{startState:function(a){return{tokenize:null,state:n?"block":"top",stateArg:null,context:new t(n?"block":"top",a||0,null)}},token:function(a,c){if(!c.tokenize&&a.eatSpace())return null;var b=(c.tokenize||K)(a,c);b&&"object"==
+typeof b&&(aa=b[1],b=b[0]);y=b;c.state=N[c.state](aa,a,c);return y},indent:function(a,c){var b=a.context,d=c&&c.charAt(0),f=b.indent;"prop"!=b.type||"}"!=d&&")"!=d||(b=b.prev);if(b.prev)if("}"==d&&("block"==b.type||"top"==b.type||"interpolation"==b.type||"restricted_atBlock"==b.type))b=b.prev,f=b.indent;else if(")"==d&&("parens"==b.type||"atBlock_parens"==b.type)||"{"==d&&("at"==b.type||"atBlock"==b.type))f=Math.max(0,b.indent-A);return f},electricChars:"}",blockCommentStart:"/*",blockCommentEnd:"*/",
+lineComment:c.lineComment,fold:"brace"}});var r=["domain","regexp","url","url-prefix"],n=z(r),B="all aural braille handheld print projection screen tty tv embossed".split(" "),G=z(B),J="width min-width max-width height min-height max-height device-width min-device-width max-device-width device-height min-device-height max-device-height aspect-ratio min-aspect-ratio max-aspect-ratio device-aspect-ratio min-device-aspect-ratio max-device-aspect-ratio color min-color max-color color-index min-color-index max-color-index monochrome min-monochrome max-monochrome resolution min-resolution max-resolution scan grid orientation device-pixel-ratio min-device-pixel-ratio max-device-pixel-ratio pointer any-pointer hover any-hover".split(" "),
+A=z(J),H="landscape portrait none coarse fine on-demand hover interlace progressive".split(" "),w=z(H),h="align-content align-items align-self alignment-adjust alignment-baseline anchor-point animation animation-delay animation-direction animation-duration animation-fill-mode animation-iteration-count animation-name animation-play-state animation-timing-function appearance azimuth backface-visibility background background-attachment background-blend-mode background-clip background-color background-image background-origin background-position background-repeat background-size baseline-shift binding bleed bookmark-label bookmark-level bookmark-state bookmark-target border border-bottom border-bottom-color border-bottom-left-radius border-bottom-right-radius border-bottom-style border-bottom-width border-collapse border-color border-image border-image-outset border-image-repeat border-image-slice border-image-source border-image-width border-left border-left-color border-left-style border-left-width border-radius border-right border-right-color border-right-style border-right-width border-spacing border-style border-top border-top-color border-top-left-radius border-top-right-radius border-top-style border-top-width border-width bottom box-decoration-break box-shadow box-sizing break-after break-before break-inside caption-side caret-color clear clip color color-profile column-count column-fill column-gap column-rule column-rule-color column-rule-style column-rule-width column-span column-width columns content counter-increment counter-reset crop cue cue-after cue-before cursor direction display dominant-baseline drop-initial-after-adjust drop-initial-after-align drop-initial-before-adjust drop-initial-before-align drop-initial-size drop-initial-value elevation empty-cells fit fit-position flex flex-basis flex-direction flex-flow flex-grow flex-shrink flex-wrap float float-offset flow-from flow-into font font-feature-settings font-family font-kerning font-language-override font-size font-size-adjust font-stretch font-style font-synthesis font-variant font-variant-alternates font-variant-caps font-variant-east-asian font-variant-ligatures font-variant-numeric font-variant-position font-weight grid grid-area grid-auto-columns grid-auto-flow grid-auto-rows grid-column grid-column-end grid-column-gap grid-column-start grid-gap grid-row grid-row-end grid-row-gap grid-row-start grid-template grid-template-areas grid-template-columns grid-template-rows hanging-punctuation height hyphens icon image-orientation image-rendering image-resolution inline-box-align justify-content justify-items justify-self left letter-spacing line-break line-height line-stacking line-stacking-ruby line-stacking-shift line-stacking-strategy list-style list-style-image list-style-position list-style-type margin margin-bottom margin-left margin-right margin-top marks marquee-direction marquee-loop marquee-play-count marquee-speed marquee-style max-height max-width min-height min-width move-to nav-down nav-index nav-left nav-right nav-up object-fit object-position opacity order orphans outline outline-color outline-offset outline-style outline-width overflow overflow-style overflow-wrap overflow-x overflow-y padding padding-bottom padding-left padding-right padding-top page page-break-after page-break-before page-break-inside page-policy pause pause-after pause-before perspective perspective-origin pitch pitch-range place-content place-items place-self play-during position presentation-level punctuation-trim quotes region-break-after region-break-before region-break-inside region-fragment rendering-intent resize rest rest-after rest-before richness right rotation rotation-point ruby-align ruby-overhang ruby-position ruby-span shape-image-threshold shape-inside shape-margin shape-outside size speak speak-as speak-header speak-numeral speak-punctuation speech-rate stress string-set tab-size table-layout target target-name target-new target-position text-align text-align-last text-decoration text-decoration-color text-decoration-line text-decoration-skip text-decoration-style text-emphasis text-emphasis-color text-emphasis-position text-emphasis-style text-height text-indent text-justify text-outline text-overflow text-shadow text-size-adjust text-space-collapse text-transform text-underline-position text-wrap top transform transform-origin transform-style transition transition-delay transition-duration transition-property transition-timing-function unicode-bidi user-select vertical-align visibility voice-balance voice-duration voice-family voice-pitch voice-range voice-rate voice-stress voice-volume volume white-space widows width will-change word-break word-spacing word-wrap z-index clip-path clip-rule mask enable-background filter flood-color flood-opacity lighting-color stop-color stop-opacity pointer-events color-interpolation color-interpolation-filters color-rendering fill fill-opacity fill-rule image-rendering marker marker-end marker-mid marker-start shape-rendering stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-rendering baseline-shift dominant-baseline glyph-orientation-horizontal glyph-orientation-vertical text-anchor writing-mode".split(" "),
+a=z(h),c="scrollbar-arrow-color scrollbar-base-color scrollbar-dark-shadow-color scrollbar-face-color scrollbar-highlight-color scrollbar-shadow-color scrollbar-3d-light-color scrollbar-track-color shape-inside searchfield-cancel-button searchfield-decoration searchfield-results-button searchfield-results-decoration zoom".split(" "),K=z(c),x=z("font-family src unicode-range font-variant font-feature-settings font-stretch font-weight font-style".split(" ")),t=z("additive-symbols fallback negative pad prefix range speak-as suffix symbols system".split(" ")),
+m="aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray grey green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen".split(" "),
+l=z(m),D="above absolute activeborder additive activecaption afar after-white-space ahead alias all all-scroll alphabetic alternate always amharic amharic-abegede antialiased appworkspace arabic-indic armenian asterisks attr auto auto-flow avoid avoid-column avoid-page avoid-region background backwards baseline below bidi-override binary bengali blink block block-axis bold bolder border border-box both bottom break break-all break-word bullets button button-bevel buttonface buttonhighlight buttonshadow buttontext calc cambodian capitalize caps-lock-indicator caption captiontext caret cell center checkbox circle cjk-decimal cjk-earthly-branch cjk-heavenly-stem cjk-ideographic clear clip close-quote col-resize collapse color color-burn color-dodge column column-reverse compact condensed contain content contents content-box context-menu continuous copy counter counters cover crop cross crosshair currentcolor cursive cyclic darken dashed decimal decimal-leading-zero default default-button dense destination-atop destination-in destination-out destination-over devanagari difference disc discard disclosure-closed disclosure-open document dot-dash dot-dot-dash dotted double down e-resize ease ease-in ease-in-out ease-out element ellipse ellipsis embed end ethiopic ethiopic-abegede ethiopic-abegede-am-et ethiopic-abegede-gez ethiopic-abegede-ti-er ethiopic-abegede-ti-et ethiopic-halehame-aa-er ethiopic-halehame-aa-et ethiopic-halehame-am-et ethiopic-halehame-gez ethiopic-halehame-om-et ethiopic-halehame-sid-et ethiopic-halehame-so-et ethiopic-halehame-ti-er ethiopic-halehame-ti-et ethiopic-halehame-tig ethiopic-numeric ew-resize exclusion expanded extends extra-condensed extra-expanded fantasy fast fill fixed flat flex flex-end flex-start footnotes forwards from geometricPrecision georgian graytext grid groove gujarati gurmukhi hand hangul hangul-consonant hard-light hebrew help hidden hide higher highlight highlighttext hiragana hiragana-iroha horizontal hsl hsla hue icon ignore inactiveborder inactivecaption inactivecaptiontext infinite infobackground infotext inherit initial inline inline-axis inline-block inline-flex inline-grid inline-table inset inside intrinsic invert italic japanese-formal japanese-informal justify kannada katakana katakana-iroha keep-all khmer korean-hangul-formal korean-hanja-formal korean-hanja-informal landscape lao large larger left level lighter lighten line-through linear linear-gradient lines list-item listbox listitem local logical loud lower lower-alpha lower-armenian lower-greek lower-hexadecimal lower-latin lower-norwegian lower-roman lowercase ltr luminosity malayalam match matrix matrix3d media-controls-background media-current-time-display media-fullscreen-button media-mute-button media-play-button media-return-to-realtime-button media-rewind-button media-seek-back-button media-seek-forward-button media-slider media-sliderthumb media-time-remaining-display media-volume-slider media-volume-slider-container media-volume-sliderthumb medium menu menulist menulist-button menulist-text menulist-textfield menutext message-box middle min-intrinsic mix mongolian monospace move multiple multiply myanmar n-resize narrower ne-resize nesw-resize no-close-quote no-drop no-open-quote no-repeat none normal not-allowed nowrap ns-resize numbers numeric nw-resize nwse-resize oblique octal opacity open-quote optimizeLegibility optimizeSpeed oriya oromo outset outside outside-shape overlay overline padding padding-box painted page paused persian perspective plus-darker plus-lighter pointer polygon portrait pre pre-line pre-wrap preserve-3d progress push-button radial-gradient radio read-only read-write read-write-plaintext-only rectangle region relative repeat repeating-linear-gradient repeating-radial-gradient repeat-x repeat-y reset reverse rgb rgba ridge right rotate rotate3d rotateX rotateY rotateZ round row row-resize row-reverse rtl run-in running s-resize sans-serif saturation scale scale3d scaleX scaleY scaleZ screen scroll scrollbar scroll-position se-resize searchfield searchfield-cancel-button searchfield-decoration searchfield-results-button searchfield-results-decoration self-start self-end semi-condensed semi-expanded separate serif show sidama simp-chinese-formal simp-chinese-informal single skew skewX skewY skip-white-space slide slider-horizontal slider-vertical sliderthumb-horizontal sliderthumb-vertical slow small small-caps small-caption smaller soft-light solid somali source-atop source-in source-out source-over space space-around space-between space-evenly spell-out square square-button start static status-bar stretch stroke sub subpixel-antialiased super sw-resize symbolic symbols system-ui table table-caption table-cell table-column table-column-group table-footer-group table-header-group table-row table-row-group tamil telugu text text-bottom text-top textarea textfield thai thick thin threeddarkshadow threedface threedhighlight threedlightshadow threedshadow tibetan tigre tigrinya-er tigrinya-er-abegede tigrinya-et tigrinya-et-abegede to top trad-chinese-formal trad-chinese-informal transform translate translate3d translateX translateY translateZ transparent ultra-condensed ultra-expanded underline unset up upper-alpha upper-armenian upper-greek upper-hexadecimal upper-latin upper-norwegian upper-roman uppercase urdu url var vertical vertical-text visible visibleFill visiblePainted visibleStroke visual w-resize wait wave wider window windowframe windowtext words wrap wrap-reverse x-large x-small xor xx-large xx-small".split(" "),
+u=z(D),r=r.concat(B).concat(J).concat(H).concat(h).concat(c).concat(m).concat(D);g.registerHelper("hintWords","css",r);g.defineMIME("text/css",{documentTypes:n,mediaTypes:G,mediaFeatures:A,mediaValueKeywords:w,propertyKeywords:a,nonStandardPropertyKeywords:K,fontProperties:x,counterDescriptors:t,colorKeywords:l,valueKeywords:u,tokenHooks:{"/":function(a,c){if(!a.eat("*"))return!1;c.tokenize=C;return C(a,c)}},name:"css"});g.defineMIME("text/x-scss",{mediaTypes:G,mediaFeatures:A,mediaValueKeywords:w,
+propertyKeywords:a,nonStandardPropertyKeywords:K,colorKeywords:l,valueKeywords:u,fontProperties:x,allowNested:!0,lineComment:"//",tokenHooks:{"/":function(a,c){return a.eat("/")?(a.skipToEnd(),["comment","comment"]):a.eat("*")?(c.tokenize=C,C(a,c)):["operator","operator"]},":":function(a){return a.match(/\s*\{/,!1)?[null,null]:!1},$:function(a){a.match(/^[\w-]+/);return a.match(/^\s*:/,!1)?["variable-2","variable-definition"]:["variable-2","variable"]},"#":function(a){return a.eat("{")?[null,"interpolation"]:
+!1}},name:"css",helperType:"scss"});g.defineMIME("text/x-less",{mediaTypes:G,mediaFeatures:A,mediaValueKeywords:w,propertyKeywords:a,nonStandardPropertyKeywords:K,colorKeywords:l,valueKeywords:u,fontProperties:x,allowNested:!0,lineComment:"//",tokenHooks:{"/":function(a,c){return a.eat("/")?(a.skipToEnd(),["comment","comment"]):a.eat("*")?(c.tokenize=C,C(a,c)):["operator","operator"]},"@":function(a){if(a.eat("{"))return[null,"interpolation"];if(a.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/,
+!1))return!1;a.eatWhile(/[\w\\\-]/);return a.match(/^\s*:/,!1)?["variable-2","variable-definition"]:["variable-2","variable"]},"\x26":function(){return["atom","atom"]}},name:"css",helperType:"less"});g.defineMIME("text/x-gss",{documentTypes:n,mediaTypes:G,mediaFeatures:A,propertyKeywords:a,nonStandardPropertyKeywords:K,fontProperties:x,counterDescriptors:t,colorKeywords:l,valueKeywords:u,supportsAtComponent:!0,tokenHooks:{"/":function(a,c){if(!a.eat("*"))return!1;c.tokenize=C;return C(a,c)}},name:"css",
+helperType:"gss"})});
+(function(g){"object"==typeof exports&&"object"==typeof module?g(require("../../lib/codemirror"),require("../xml/xml"),require("../javascript/javascript"),require("../css/css")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../xml/xml","../javascript/javascript","../css/css"],g):g(CodeMirror)})(function(g){function z(g){var n=G[g];return n?n:G[g]=new RegExp("\\s+"+g+"\\s*\x3d\\s*('|\")?([^'\"]+)('|\")?\\s*")}function C(g,n){var r=g.match(z(n));return r?/^\s*(.*?)\s*$/.exec(r[2])[1]:
+""}function r(g,n){for(var r in g)for(var w=n[r]||(n[r]=[]),h=g[r],a=h.length-1;0<=a;a--)w.unshift(h[a])}function n(g,n){for(var r=0;r<g.length;r++){var w=g[r];if(!w[0]||w[1].test(C(n,w[0])))return w[2]}}var B={script:[["lang",/(javascript|babel)/i,"javascript"],["type",/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i,"javascript"],["type",/./,"text/plain"],[null,null,"javascript"]],style:[["lang",/^css$/i,"css"],["type",/^(text\/)?(x-)?(stylesheet|css)$/i,"css"],["type",/./,"text/plain"],
+[null,null,"css"]]},G={};g.defineMode("htmlmixed",function(z,A){function C(a,c){var t=w.token(a,c.htmlState),m=/\btag\b/.test(t),l;if(m&&!/[<>\s\/]/.test(a.current())&&(l=c.htmlState.tagName&&c.htmlState.tagName.toLowerCase())&&h.hasOwnProperty(l))c.inTag=l+" ";else if(c.inTag&&m&&/>$/.test(a.current())){m=/^([\S]+) (.*)/.exec(c.inTag);c.inTag=null;l="\x3e"==a.current()&&n(h[m[1]],m[2]);l=g.getMode(z,l);var r=new RegExp("^\x3c/s*"+m[1]+"s*\x3e","i"),u=new RegExp("\x3c/s*"+m[1]+"s*\x3e","i");c.token=
+function(a,c){if(a.match(r,!1))return c.token=C,c.localState=c.localMode=null;var g=c.localMode.token(a,c.localState),h=a.current(),l=h.search(u);-1<l?a.backUp(h.length-l):h.match(/<\/?$/)&&(a.backUp(h.length),a.match(u,!1)||a.match(h));return g};c.localMode=l;c.localState=g.startState(l,w.indent(c.htmlState,""))}else c.inTag&&(c.inTag+=a.current(),a.eol()&&(c.inTag+=" "));return t}var w=g.getMode(z,{name:"xml",htmlMode:!0,multilineTagIndentFactor:A.multilineTagIndentFactor,multilineTagIndentPastTag:A.multilineTagIndentPastTag}),
+h={},a=A&&A.tags,c=A&&A.scriptTypes;r(B,h);a&&r(a,h);if(c)for(a=c.length-1;0<=a;a--)h.script.unshift(["type",c[a].matches,c[a].mode]);return{startState:function(){var a=g.startState(w);return{token:C,inTag:null,localMode:null,localState:null,htmlState:a}},copyState:function(a){var c;a.localState&&(c=g.copyState(a.localMode,a.localState));return{token:a.token,inTag:a.inTag,localMode:a.localMode,localState:c,htmlState:g.copyState(w,a.htmlState)}},token:function(a,c){return c.token(a,c)},indent:function(a,
+c,h){return!a.localMode||/^\s*<\//.test(c)?w.indent(a.htmlState,c):a.localMode.indent?a.localMode.indent(a.localState,c,h):g.Pass},innerMode:function(a){return{state:a.localState||a.htmlState,mode:a.localMode||w}}}},"xml","javascript","css");g.defineMIME("text/html","htmlmixed")});
+CodeMirror.defineMode("bbcodemixed",function(g){var z,C,r,n,B,G,J;function A(a){return a.replace(/([\[\]\.\-\+\<\>\?\:\(\)\{\}])/g,"\\$1")}var H,w=CodeMirror.getMode(g,"htmlmixed"),h=CodeMirror.getMode(g,"bbcode");J="literal";g.hasOwnProperty("bbCodeLiteral")&&(J=g.bbCodeLiteral);r=/.*\[/;n=/[^\<\>]*\[/;B=new RegExp(A("["+J+"]"));G=new RegExp(A("[/"+J+"]"));H={chain:function(a,c,g){c.tokenize=g;return g(a,c)},cleanChain:function(a,c,g){c.tokenize=null;c.localState=null;c.localMode=null;return"string"==
+typeof g?g?g:null:g(a,c)},maybeBackup:function(a,c,g){c=A(c);var h=a.current(),n=h.search(c);-1<n?a.backUp(h.length-n):h.match(/<\/?$/)&&(a.backUp(h.length),a.match(c,!1)||a.match(h[0]));return g}};z=function(a,c){return!c.inLiteral&&a.match(n,!1)&&null===c.htmlMixedState.htmlState.tagName||!c.inLiteral&&a.match("[",!1)?(c.tokenize=C,c.localMode=h,c.localState=h.startState(w.indent(c.htmlMixedState,"")),H.maybeBackup(a,"[",h.token(a,c.localState))):w.token(a,c.htmlMixedState)};C=function(a,c){return a.match("]",
+!1)?(a.eat("]"),c.tokenize=z,c.localMode=w,c.localState=c.htmlMixedState,"tag"):H.maybeBackup(a,"]",h.token(a,c.localState))};return{startState:function(){var a=w.startState();return{token:z,localMode:null,localState:null,htmlMixedState:a,tokenize:null,inLiteral:!1}},copyState:function(a){var c=null,g=a.tokenize||a.token;a.localState&&(c=CodeMirror.copyState(g!=z?h:w,a.localState));return{token:a.token,tokenize:a.tokenize,localMode:a.localMode,localState:c,htmlMixedState:CodeMirror.copyState(w,a.htmlMixedState),
+inLiteral:a.inLiteral}},token:function(a,c){if(a.match("[",!1)){if(!c.inLiteral&&a.match(B,!0))return c.inLiteral=!0,"keyword";if(c.inLiteral&&a.match(G,!0))return c.inLiteral=!1,"keyword"}c.inLiteral&&c.localState!=c.htmlMixedState&&(c.tokenize=z,c.localMode=w,c.localState=c.htmlMixedState);return(c.tokenize||c.token)(a,c)},indent:function(a,c){return a.localMode==h||a.inLiteral&&!a.localMode||r.test(c)?CodeMirror.Pass:w.indent(a.htmlMixedState,c)},innerMode:function(a){return{state:a.localState||
+a.htmlMixedState,mode:a.localMode||w}}}},"xml","javascript","css");CodeMirror.defineMIME("text/x-bbcode","bbcodemixed");
\ No newline at end of file
diff --git a/js/ckeditor/plugins/codemirror/js/codemirror.mode.htmlmixed.min.js b/js/ckeditor/plugins/codemirror/js/codemirror.mode.htmlmixed.min.js
new file mode 100644 (file)
index 0000000..eb0fa7c
--- /dev/null
@@ -0,0 +1,77 @@
+!function(d){"object"==typeof exports&&"object"==typeof module?d(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("mode/xml/xml",["../../lib/codemirror"],d):d(CodeMirror)}(function(d){var p={autoSelfClosers:{area:!0,base:!0,br:!0,col:!0,command:!0,embed:!0,frame:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0,menuitem:!0},implicitlyClosed:{dd:!0,li:!0,optgroup:!0,option:!0,p:!0,rp:!0,rt:!0,tbody:!0,td:!0,tfoot:!0,th:!0,tr:!0},contextGrabbers:{dd:{dd:!0,
+dt:!0},dt:{dd:!0,dt:!0},li:{li:!0},option:{option:!0,optgroup:!0},optgroup:{optgroup:!0},p:{address:!0,article:!0,aside:!0,blockquote:!0,dir:!0,div:!0,dl:!0,fieldset:!0,footer:!0,form:!0,h1:!0,h2:!0,h3:!0,h4:!0,h5:!0,h6:!0,header:!0,hgroup:!0,hr:!0,menu:!0,nav:!0,ol:!0,p:!0,pre:!0,section:!0,table:!0,ul:!0},rp:{rp:!0,rt:!0},rt:{rp:!0,rt:!0},tbody:{tbody:!0,tfoot:!0},td:{td:!0,th:!0},tfoot:{tbody:!0},th:{td:!0,th:!0},thead:{tbody:!0,tfoot:!0},tr:{tr:!0}},doNotIndent:{pre:!0},allowUnquoted:!0,allowMissing:!0,
+caseFold:!0},B={autoSelfClosers:{},implicitlyClosed:{},contextGrabbers:{},doNotIndent:{},allowUnquoted:!1,allowMissing:!1,caseFold:!1};d.defineMode("xml",function(x,b){function n(a,b){function H(H){return b.tokenize=H,H(a,b)}var d=a.next();if("\x3c"==d)return a.eat("!")?a.eat("[")?a.match("CDATA[")?H(k("atom","]]\x3e")):null:a.match("--")?H(k("comment","--\x3e")):a.match("DOCTYPE",!0,!0)?(a.eatWhile(/[\w\._\-]/),H(y(1))):null:a.eat("?")?(a.eatWhile(/[\w\._\-]/),b.tokenize=k("meta","?\x3e"),"meta"):
+(K=a.eat("/")?"closeTag":"openTag",b.tokenize=q,"tag bracket");if("\x26"==d){var m;return m=a.eat("#")?a.eat("x")?a.eatWhile(/[a-fA-F\d]/)&&a.eat(";"):a.eatWhile(/[\d]/)&&a.eat(";"):a.eatWhile(/[\w\.\-:]/)&&a.eat(";"),m?"atom":"error"}return a.eatWhile(/[^&<]/),null}function q(a,b){var H=a.next();return"\x3e"==H||"/"==H&&a.eat("\x3e")?(b.tokenize=n,K="\x3e"==H?"endTag":"selfcloseTag","tag bracket"):"\x3d"==H?(K="equals",null):"\x3c"==H?(b.tokenize=n,b.state=E,b.tagName=b.tagStart=null,(H=b.tokenize(a,
+b))?H+" tag error":"tag error"):/[\'\"]/.test(H)?(b.tokenize=A(H),b.stringStartCol=a.column(),b.tokenize(a,b)):(a.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/),"word")}function A(a){var b=function(b,d){for(;!b.eol();)if(b.next()==a){d.tokenize=q;break}return"string"};return b.isInAttribute=!0,b}function k(a,b){return function(d,m){for(;!d.eol();){if(d.match(b)){m.tokenize=n;break}d.next()}return a}}function y(a){return function(b,d){for(var m;null!=(m=b.next());){if("\x3c"==m)return d.tokenize=
+y(a+1),d.tokenize(b,d);if("\x3e"==m){if(1==a){d.tokenize=n;break}return d.tokenize=y(a-1),d.tokenize(b,d)}}return"meta"}}function C(a,b,d){this.prev=a.context;this.tagName=b;this.indent=a.indented;this.startOfLine=d;(G.doNotIndent.hasOwnProperty(b)||a.context&&a.context.noIndent)&&(this.noIndent=!0)}function g(a){a.context&&(a.context=a.context.prev)}function a(a,b){for(var d;a.context&&(d=a.context.tagName,G.contextGrabbers.hasOwnProperty(d)&&G.contextGrabbers[d].hasOwnProperty(b));)g(a)}function E(a,
+b,d){return"openTag"==a?(d.tagStart=b.column(),w):"closeTag"==a?J:E}function w(a,b,d){return"word"==a?(d.tagName=b.current(),F="tag",h):(F="error",w)}function J(a,b,d){return"word"==a?(a=b.current(),d.context&&d.context.tagName!=a&&G.implicitlyClosed.hasOwnProperty(d.context.tagName)&&g(d),d.context&&d.context.tagName==a||!1===G.matchClosing?(F="tag",v):(F="tag error",r)):(F="error",r)}function v(a,b,d){return"endTag"!=a?(F="error",v):(g(d),E)}function r(a,b,d){return F="error",v(a,b,d)}function h(b,
+d,m){if("word"==b)return F="attribute",D;if("endTag"==b||"selfcloseTag"==b){d=m.tagName;var q=m.tagStart;return m.tagName=m.tagStart=null,"selfcloseTag"==b||G.autoSelfClosers.hasOwnProperty(d)?a(m,d):(a(m,d),m.context=new C(m,d,q==m.indented)),E}return F="error",h}function D(a,b,d){return"equals"==a?u:(G.allowMissing||(F="error"),h(a,b,d))}function u(a,b,d){return"string"==a?m:"word"==a&&G.allowUnquoted?(F="string",h):(F="error",h(a,b,d))}function m(a,b,d){return"string"==a?m:h(a,b,d)}var R=x.indentUnit,
+G={},W=b.htmlMode?p:B,P;for(P in W)G[P]=W[P];for(P in b)G[P]=b[P];var K,F;return n.isInText=!0,{startState:function(a){var b={tokenize:n,state:E,indented:a||0,tagName:null,tagStart:null,context:null};return null!=a&&(b.baseIndent=a),b},token:function(a,b){if(!b.tagName&&a.sol()&&(b.indented=a.indentation()),a.eatSpace())return null;K=null;var d=b.tokenize(a,b);return(d||K)&&"comment"!=d&&(F=null,b.state=b.state(K||d,a,b),F&&(d="error"==F?d+" error":F)),d},indent:function(a,b,m){var g=a.context;if(a.tokenize.isInAttribute)return a.tagStart==
+a.indented?a.stringStartCol+1:a.indented+R;if(g&&g.noIndent)return d.Pass;if(a.tokenize!=q&&a.tokenize!=n)return m?m.match(/^(\s*)/)[0].length:0;if(a.tagName)return!1!==G.multilineTagIndentPastTag?a.tagStart+a.tagName.length+2:a.tagStart+R*(G.multilineTagIndentFactor||1);if(G.alignCDATA&&/<!\[CDATA\[/.test(b))return 0;if((b=b&&/^<(\/)?([\w_:\.-]*)/.exec(b))&&b[1])for(;g;){if(g.tagName==b[2]){g=g.prev;break}if(!G.implicitlyClosed.hasOwnProperty(g.tagName))break;g=g.prev}else if(b)for(;g;){m=G.contextGrabbers[g.tagName];
+if(!m||!m.hasOwnProperty(b[2]))break;g=g.prev}for(;g&&g.prev&&!g.startOfLine;)g=g.prev;return g?g.indent+R:a.baseIndent||0},electricInput:/<\/[\s\w:]+>$/,blockCommentStart:"\x3c!--",blockCommentEnd:"--\x3e",configuration:G.htmlMode?"html":"xml",helperType:G.htmlMode?"html":"xml",skipAttribute:function(a){a.state==u&&(a.state=h)}}});d.defineMIME("text/xml","xml");d.defineMIME("application/xml","xml");d.mimeModes.hasOwnProperty("text/html")||d.defineMIME("text/html",{name:"xml",htmlMode:!0})});
+(function(d){"object"==typeof exports&&"object"==typeof module?d(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("mode/javascript/javascript",["../../lib/codemirror"],d):d(CodeMirror)})(function(d){function p(d,p,b){return/^(?:operator|sof|keyword c|case|new|export|default|[\[{}\(,;:]|=>)$/.test(p.lastType)||"quasi"==p.lastType&&/\{\s*$/.test(d.string.slice(0,d.pos-(b||0)))}d.defineMode("javascript",function(B,x){function b(f,a,c){return aa=f,ca=c,a}function n(f,a){var c=
+f.next();if('"'==c||"'"==c)return a.tokenize=q(c),a.tokenize(f,a);if("."==c&&f.match(/^\d+(?:[eE][+\-]?\d+)?/))return b("number","number");if("."==c&&f.match(".."))return b("spread","meta");if(/[\[\]{}\(\),;\:\.]/.test(c))return b(c);if("\x3d"==c&&f.eat("\x3e"))return b("\x3d\x3e","operator");if("0"==c&&f.eat(/x/i))return f.eatWhile(/[\da-f]/i),b("number","number");if("0"==c&&f.eat(/o/i))return f.eatWhile(/[0-7]/i),b("number","number");if("0"==c&&f.eat(/b/i))return f.eatWhile(/[01]/i),b("number",
+"number");if(/\d/.test(c))return f.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/),b("number","number");if("/"==c){if(f.eat("*"))c=(a.tokenize=A,A(f,a));else if(f.eat("/"))c=(f.skipToEnd(),b("comment","comment"));else if(p(f,a,1)){a:for(var e=!1,d=!1;null!=(c=f.next());){if(!e){if("/"==c&&!d)break a;"["==c?d=!0:d&&"]"==c&&(d=!1)}e=!e&&"\\"==c}c=(f.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/),b("regexp","string-2"))}else c=(f.eatWhile(da),b("operator","operator",f.current()));return c}if("`"==c)return a.tokenize=
+k,k(f,a);if("#"==c)return f.skipToEnd(),b("error","error");if(da.test(c))return"\x3e"==c&&a.lexical&&"\x3e"==a.lexical.type||f.eatWhile(da),b("operator","operator",f.current());if(ka.test(c)){f.eatWhile(ka);c=f.current();if("."!=a.lastType){if(qa.propertyIsEnumerable(c))return e=qa[c],b(e.type,e.style,c);if("async"==c&&f.match(/^\s*[\(\w]/,!1))return b("async","keyword",c)}return b("variable","variable",c)}}function q(f){return function(a,c){var e,d=!1;if(ea&&"@"==a.peek()&&a.match(Aa))return c.tokenize=
+n,b("jsonld-keyword","meta");for(;null!=(e=a.next())&&(e!=f||d);)d=!d&&"\\"==e;return d||(c.tokenize=n),b("string","string")}}function A(f,a){for(var c,e=!1;c=f.next();){if("/"==c&&e){a.tokenize=n;break}e="*"==c}return b("comment","comment")}function k(f,a){for(var c,e=!1;null!=(c=f.next());){if(!e&&("`"==c||"$"==c&&f.eat("{"))){a.tokenize=n;break}e=!e&&"\\"==c}return b("quasi","string-2",f.current())}function y(f,a){a.fatArrowAt&&(a.fatArrowAt=null);var c=f.string.indexOf("\x3d\x3e",f.start);if(!(0>
+c)){if(L){var e=/:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(f.string.slice(f.start,c));e&&(c=e.index)}for(var e=0,b=!1,c=c-1;0<=c;--c){var d=f.string.charAt(c),t=Ba.indexOf(d);if(0<=t&&3>t){if(!e){++c;break}if(0==--e){"("==d&&(b=!0);break}}else if(3<=t&&6>t)++e;else if(ka.test(d))b=!0;else{if(/["'\/]/.test(d))return;if(b&&!e){++c;break}}}b&&!e&&(a.fatArrowAt=c)}}function C(f,a,c,e,b,d){this.indented=f;this.column=a;this.type=c;this.prev=b;this.info=d;null!=e&&(this.align=e)}function g(){for(var f=
+arguments.length-1;0<=f;f--)l.cc.push(arguments[f])}function a(){return g.apply(null,arguments),!0}function E(f){function a(c){for(;c;c=c.next)if(c.name==f)return!0;return!1}var c=l.state;(l.marked="def",c.context)?a(c.localVars)||(c.localVars={name:f,next:c.localVars}):a(c.globalVars)||x.globalVars&&(c.globalVars={name:f,next:c.globalVars})}function w(){l.state.context={prev:l.state.context,vars:l.state.localVars};l.state.localVars=Ca}function J(){l.state.localVars=l.state.context.vars;l.state.context=
+l.state.context.prev}function v(f,a){var c=function(){var c=l.state,e=c.indented;if("stat"==c.lexical.type)e=c.lexical.indented;else for(var b=c.lexical;b&&")"==b.type&&b.align;b=b.prev)e=b.indented;c.lexical=new C(e,l.stream.column(),f,null,c.lexical,a)};return c.lex=!0,c}function r(){var f=l.state;f.lexical.prev&&(")"==f.lexical.type&&(f.indented=f.lexical.indented),f.lexical=f.lexical.prev)}function h(f){function c(e){return e==f?a():";"==f?g():a(c)}return c}function D(f,e){return"var"==f?a(v("vardef",
+e.length),z,h(";"),r):"keyword a"==f?a(v("form"),R,D,r):"keyword b"==f?a(v("form"),D,r):"{"==f?a(v("}"),X,r):";"==f?a():"if"==f?("else"==l.state.lexical.info&&l.state.cc[l.state.cc.length-1]==r&&l.state.cc.pop()(),a(v("form"),R,D,r,ra)):"function"==f?a(N):"for"==f?a(v("form"),Da,D,r):"variable"==f?L&&"type"==e?(l.marked="keyword",a(I,h("operator"),I,h(";"))):a(v("stat"),Ea):"switch"==f?a(v("form"),R,h("{"),v("}","switch"),X,r,r):"case"==f?a(u,h(":")):"default"==f?a(h(":")):"catch"==f?a(v("form"),
+w,h("("),la,h(")"),D,r,J):"class"==f?a(v("form"),sa,r):"export"==f?a(v("stat"),Fa,r):"import"==f?a(v("stat"),Ga,r):"module"==f?a(v("form"),c,h("{"),v("}"),X,r,r):"async"==f?a(D):"@"==e?a(u,D):g(v("stat"),u,h(";"),r)}function u(f){return G(f,!1)}function m(f){return G(f,!0)}function R(f){return"("!=f?g():a(v(")"),u,h(")"),r)}function G(f,e){if(l.state.fatArrowAt==l.stream.start){var b=e?pa:H;if("("==f)return a(w,v(")"),O(c,")"),r,h("\x3d\x3e"),b,J);if("variable"==f)return g(w,c,h("\x3d\x3e"),b,J)}b=
+e?F:K;return Ha.hasOwnProperty(f)?a(b):"function"==f?a(N,b):"class"==f?a(v("form"),Ia,r):"keyword c"==f||"async"==f?a(e?P:W):"("==f?a(v(")"),W,h(")"),r,b):"operator"==f||"spread"==f?a(e?m:u):"["==f?a(v("]"),Ja,r,b):"{"==f?Q(ma,"}",null,b):"quasi"==f?g(ba,b):"new"==f?a(za(e)):a()}function W(f){return f.match(/[;\}\)\],]/)?g():g(u)}function P(f){return f.match(/[;\}\)\],]/)?g():g(m)}function K(f,c){return","==f?a(u):F(f,c,!1)}function F(f,c,e){var b=0==e?K:F,d=0==e?u:m;return"\x3d\x3e"==f?a(w,e?pa:
+H,J):"operator"==f?/\+\+|--/.test(c)?a(b):"?"==c?a(u,h(":"),d):a(d):"quasi"==f?g(ba,b):";"!=f?"("==f?Q(m,")","call",b):"."==f?a(Ka,b):"["==f?a(v("]"),W,h("]"),r,b):L&&"as"==c?(l.marked="keyword",a(I,b)):void 0:void 0}function ba(f,c){return"quasi"!=f?g():"${"!=c.slice(c.length-2)?a(ba):a(u,xa)}function xa(f){if("}"==f)return l.marked="string-2",l.state.tokenize=k,a(ba)}function H(f){return y(l.stream,l.state),g("{"==f?D:u)}function pa(f){return y(l.stream,l.state),g("{"==f?D:m)}function za(f){return function(c){return"."==
+c?a(f?La:ya):g(f?m:u)}}function ya(f,c){if("target"==c)return l.marked="keyword",a(K)}function La(f,c){if("target"==c)return l.marked="keyword",a(F)}function Ea(f){return":"==f?a(r,D):g(K,h(";"),r)}function Ka(f){if("variable"==f)return l.marked="property",a()}function ma(f,c){return"async"==f?(l.marked="property",a(ma)):"variable"==f||"keyword"==l.style?(l.marked="property",a("get"==c||"set"==c?Ma:S)):"number"==f||"string"==f?(l.marked=ea?"property":l.style+" property",a(S)):"jsonld-keyword"==f?
+a(S):"modifier"==f?a(ma):"["==f?a(u,h("]"),S):"spread"==f?a(u,S):":"==f?g(S):void 0}function Ma(f){return"variable"!=f?g(S):(l.marked="property",a(N))}function S(f){return":"==f?a(m):"("==f?g(N):void 0}function O(f,c,e){function b(d,t){if(e?-1<e.indexOf(d):","==d){var m=l.state.lexical;return"call"==m.info&&(m.pos=(m.pos||0)+1),a(function(a,e){return a==c||e==c?g():g(f)},b)}return d==c||t==c?a():a(h(c))}return function(e,d){return e==c||d==c?a():g(f,b)}}function Q(f,c,e){for(var b=3;b<arguments.length;b++)l.cc.push(arguments[b]);
+return a(v(c,e),O(f,c),r)}function X(f){return"}"==f?a():g(D,X)}function T(f,c){if(L){if(":"==f)return a(I);if("?"==c)return a(T)}}function I(c){return"variable"==c?(l.marked="type",a(U)):"string"==c||"number"==c||"atom"==c?a(U):"{"==c?a(v("}"),O(M,"}",",;"),r,U):"("==c?a(O(fa,")"),ga):void 0}function ga(c){if("\x3d\x3e"==c)return a(I)}function M(c,e){return"variable"==c||"keyword"==l.style?(l.marked="property",a(M)):"?"==e?a(M):":"==c?a(I):"["==c?a(u,T,h("]"),M):void 0}function fa(c){return"variable"==
+c?a(fa):":"==c?a(I):void 0}function U(c,e){return"\x3c"==e?a(v("\x3e"),O(I,"\x3e"),r,U):"|"==e||"."==c?a(I):"["==c?a(h("]"),U):"extends"==e?a(I):void 0}function z(){return g(c,T,e,Na)}function c(f,e){return"modifier"==f?a(c):"variable"==f?(E(e),a()):"spread"==f?a(c):"["==f?Q(c,"]"):"{"==f?Q(t,"}"):void 0}function t(f,b){return"variable"!=f||l.stream.match(/^\s*:/,!1)?("variable"==f&&(l.marked="property"),"spread"==f?a(c):"}"==f?g():a(h(":"),c,e)):(E(b),a(e))}function e(c,e){if("\x3d"==e)return a(m)}
+function Na(c){if(","==c)return a(z)}function ra(c,e){if("keyword b"==c&&"else"==e)return a(v("form","else"),D,r)}function Da(c){if("("==c)return a(v(")"),Oa,h(")"),r)}function Oa(c){return"var"==c?a(z,h(";"),ha):";"==c?a(ha):"variable"==c?a(Pa):g(u,h(";"),ha)}function Pa(c,e){return"in"==e||"of"==e?(l.marked="keyword",a(u)):a(K,ha)}function ha(c,e){return";"==c?a(ta):"in"==e||"of"==e?(l.marked="keyword",a(u)):g(u,h(";"),ta)}function ta(c){")"!=c&&a(u)}function N(c,e){return"*"==e?(l.marked="keyword",
+a(N)):"variable"==c?(E(e),a(N)):"("==c?a(w,v(")"),O(la,")"),r,T,D,J):L&&"\x3c"==e?a(v("\x3e"),O(I,"\x3e"),r,N):void 0}function la(f){return"spread"==f?a(la):g(c,T,e)}function Ia(c,a){return"variable"==c?sa(c,a):ia(c,a)}function sa(c,e){if("variable"==c)return E(e),a(ia)}function ia(c,e){return"\x3c"==e?a(v("\x3e"),O(I,"\x3e"),r,ia):"extends"==e||"implements"==e||L&&","==c?a(L?I:u,ia):"{"==c?a(v("}"),V,r):void 0}function V(c,e){return"variable"==c||"keyword"==l.style?("async"==e||"static"==e||"get"==
+e||"set"==e||L&&("public"==e||"private"==e||"protected"==e||"readonly"==e||"abstract"==e))&&l.stream.match(/^\s+[\w$\xa1-\uffff]/,!1)?(l.marked="keyword",a(V)):(l.marked="property",a(L?na:N,V)):"["==c?a(u,h("]"),L?na:N,V):"*"==e?(l.marked="keyword",a(V)):";"==c?a(V):"}"==c?a():"@"==e?a(u,V):void 0}function na(c,b){return"?"==b?a(na):":"==c?a(I,e):"\x3d"==b?a(m):g(N)}function Fa(c,e){return"*"==e?(l.marked="keyword",a(oa,h(";"))):"default"==e?(l.marked="keyword",a(u,h(";"))):"{"==c?a(O(ua,"}"),oa,
+h(";")):g(D)}function ua(c,e){return"as"==e?(l.marked="keyword",a(h("variable"))):"variable"==c?g(m,ua):void 0}function Ga(c){return"string"==c?a():g(ja,va,oa)}function ja(c,e){return"{"==c?Q(ja,"}"):("variable"==c&&E(e),"*"==e&&(l.marked="keyword"),a(Qa))}function va(c){if(","==c)return a(ja,va)}function Qa(c,e){if("as"==e)return l.marked="keyword",a(ja)}function oa(c,e){if("from"==e)return l.marked="keyword",a(u)}function Ja(c){return"]"==c?a():g(O(m,"]"))}var aa,ca,Y=B.indentUnit,wa=x.statementIndent,
+ea=x.jsonld,Z=x.json||ea,L=x.typescript,ka=x.wordCharacters||/[\w$\xa1-\uffff]/,qa=function(){function c(a){return{type:a,style:"keyword"}}var a=c("keyword a"),e=c("keyword b"),b=c("keyword c"),d=c("operator"),t={type:"atom",style:"atom"},a={"if":c("if"),"while":a,"with":a,"else":e,"do":e,"try":e,"finally":e,"return":b,"break":b,"continue":b,"new":c("new"),"delete":b,"throw":b,"debugger":b,"var":c("var"),"const":c("var"),let:c("var"),"function":c("function"),"catch":c("catch"),"for":c("for"),"switch":c("switch"),
+"case":c("case"),"default":c("default"),"in":d,"typeof":d,"instanceof":d,"true":t,"false":t,"null":t,undefined:t,NaN:t,Infinity:t,"this":c("this"),"class":c("class"),"super":c("atom"),yield:b,"export":c("export"),"import":c("import"),"extends":b,await:b};if(L){var e={type:"variable",style:"type"},b={"interface":c("class"),"implements":b,namespace:b,module:c("module"),"enum":c("module"),"public":c("modifier"),"private":c("modifier"),"protected":c("modifier"),"abstract":c("modifier"),string:e,number:e,
+"boolean":e,any:e},m;for(m in b)a[m]=b[m]}return a}(),da=/[+\-*&%=<>!?|~^@]/,Aa=/^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/,Ba="([{}])",Ha={atom:!0,number:!0,variable:!0,string:!0,regexp:!0,"this":!0,"jsonld-keyword":!0},l={state:null,column:null,marked:null,cc:null},Ca={name:"this",next:{name:"arguments"}};return r.lex=!0,{startState:function(c){c={tokenize:n,lastType:"sof",cc:[],lexical:new C((c||0)-Y,0,"block",!1),localVars:x.localVars,context:x.localVars&&
+{vars:x.localVars},indented:c||0};return x.globalVars&&"object"==typeof x.globalVars&&(c.globalVars=x.globalVars),c},token:function(c,a){if(c.sol()&&(a.lexical.hasOwnProperty("align")||(a.lexical.align=!1),a.indented=c.indentation(),y(c,a)),a.tokenize!=A&&c.eatSpace())return null;var e=a.tokenize(c,a);if("comment"!=aa){a.lastType="operator"!=aa||"++"!=ca&&"--"!=ca?aa:"incdec";a:{var b=aa,d=ca,t=a.cc;l.state=a;l.stream=c;l.marked=null;l.cc=t;l.style=e;for(a.lexical.hasOwnProperty("align")||(a.lexical.align=
+!0);;)if((t.length?t.pop():Z?u:D)(b,d)){for(;t.length&&t[t.length-1].lex;)t.pop()();if(l.marked)e=l.marked;else{if(b="variable"==b)b:{for(b=a.localVars;b;b=b.next)if(b.name==d){b=!0;break b}for(t=a.context;t;t=t.prev)for(b=t.vars;b;b=b.next)if(b.name==d){b=!0;break b}b=void 0}e=b?"variable-2":e}break a}}}return e},indent:function(c,a){if(c.tokenize==A)return d.Pass;if(c.tokenize!=n)return 0;var e,b=a&&a.charAt(0),t=c.lexical;if(!/^\s*else\b/.test(a))for(var m=c.cc.length-1;0<=m;--m){var g=c.cc[m];
+if(g==r)t=t.prev;else if(g!=ra)break}for(;!("stat"!=t.type&&"form"!=t.type||"}"!=b&&(!(e=c.cc[c.cc.length-1])||e!=K&&e!=F||/^[,\.=+\-*:?[\(]/.test(a)));)t=t.prev;wa&&")"==t.type&&"stat"==t.prev.type&&(t=t.prev);e=t.type;m=b==e;"vardef"==e?b=t.indented+("operator"==c.lastType||","==c.lastType?t.info+1:0):"form"==e&&"{"==b?b=t.indented:"form"==e?b=t.indented+Y:"stat"==e?(b=t.indented,t="operator"==c.lastType||","==c.lastType||da.test(a.charAt(0))||/[,.]/.test(a.charAt(0)),b+=t?wa||Y:0):b="switch"!=
+t.info||m||0==x.doubleIndentSwitch?t.align?t.column+(m?0:1):t.indented+(m?0:Y):t.indented+(/^(?:case|default)\b/.test(a)?Y:2*Y);return b},electricInput:/^\s*(?:case .*?:|default:|\{|\})$/,blockCommentStart:Z?null:"/*",blockCommentEnd:Z?null:"*/",lineComment:Z?null:"//",fold:"brace",closeBrackets:"()[]{}''\"\"``",helperType:Z?"json":"javascript",jsonldMode:ea,jsonMode:Z,expressionAllowed:p,skipExpression:function(c){var a=c.cc[c.cc.length-1];a!=u&&a!=m||c.cc.pop()}}});d.registerHelper("wordChars",
+"javascript",/[\w$]/);d.defineMIME("text/javascript","javascript");d.defineMIME("text/ecmascript","javascript");d.defineMIME("application/javascript","javascript");d.defineMIME("application/x-javascript","javascript");d.defineMIME("application/ecmascript","javascript");d.defineMIME("application/json",{name:"javascript",json:!0});d.defineMIME("application/x-json",{name:"javascript",json:!0});d.defineMIME("application/ld+json",{name:"javascript",jsonld:!0});d.defineMIME("text/typescript",{name:"javascript",
+typescript:!0});d.defineMIME("application/typescript",{name:"javascript",typescript:!0})});
+(function(d){"object"==typeof exports&&"object"==typeof module?d(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("mode/css/css",["../../lib/codemirror"],d):d(CodeMirror)})(function(d){function p(a){for(var b={},d=0;d<a.length;++d)b[a[d].toLowerCase()]=!0;return b}function B(a,b){for(var d,g=!1;null!=(d=a.next());){if(g&&"/"==d){b.tokenize=null;break}g="*"==d}return["comment","comment"]}d.defineMode("css",function(a,b){function g(c,a){var e=c.next();if(y[e]){var b=y[e](c,
+a);if(!1!==b)return b}"@"==e?(c.eatWhile(/[\w\\\-]/),e=(w=c.current(),"def")):e="\x3d"==e||("~"==e||"|"==e)&&c.eat("\x3d")?(w="compare",null):'"'==e||"'"==e?(a.tokenize=q(e),a.tokenize(c,a)):"#"==e?(c.eatWhile(/[\w\\\-]/),w="hash","atom"):"!"==e?(c.match(/^\s*\w*/),w="important","keyword"):/\d/.test(e)||"."==e&&c.eat(/\d/)?(c.eatWhile(/[\w.%]/),w="unit","number"):"-"!==e?/[,+>*\/]/.test(e)?(w="select-op",null):"."==e&&c.match(/^-?[_a-z][_a-z0-9-]*/i)?(w="qualifier","qualifier"):/[:;{}\[\]\(\)]/.test(e)?
+(w=e,null):"u"==e&&c.match(/rl(-prefix)?\(/)||"d"==e&&c.match("omain(")||"r"==e&&c.match("egexp(")?(c.backUp(1),a.tokenize=n,w="word","property"):/[\w\\\-]/.test(e)?(c.eatWhile(/[\w\\\-]/),w="word","property"):(w=null,null):/[\d.]/.test(c.peek())?(c.eatWhile(/[\w.%]/),w="unit","number"):c.match(/^-[\w\\\-]+/)?(c.eatWhile(/[\w\\\-]/),c.match(/^\s*:/,!1)?(w="variable-definition","variable-2"):(w="variable","variable-2")):c.match(/^\w+-/)?(w="meta","meta"):void 0;return e}function q(c){return function(a,
+e){for(var b,d=!1;null!=(b=a.next());){if(b==c&&!d){")"==c&&a.backUp(1);break}d=!d&&"\\"==b}return(b==c||!d&&")"!=c)&&(e.tokenize=null),w="string","string"}}function n(c,a){return c.next(),c.match(/\s*[\"\')]/,!1)?a.tokenize=null:a.tokenize=q(")"),w="(",null}function r(c,a,e){this.type=c;this.indent=a;this.prev=e}function h(c,a,e,b){return c.context=new r(e,a.indentation()+(!1===b?0:p),c.context),e}function u(c){return c.context.prev&&(c.context=c.context.prev),c.context.type}function E(c,a,e,b){for(b=
+b||1;0<b;b--)e.context=e.context.prev;return z[e.context.type](c,a,e)}function v(c){c=c.current().toLowerCase();k=ga.hasOwnProperty(c)?"atom":I.hasOwnProperty(c)?"keyword":"variable"}var A=b.inline;b.propertyKeywords||(b=d.resolveMode("text/css"));var w,k,p=a.indentUnit,y=b.tokenHooks,x=b.documentTypes||{},C=b.mediaTypes||{},B=b.mediaFeatures||{},J=b.mediaValueKeywords||{},D=b.propertyKeywords||{},Q=b.nonStandardPropertyKeywords||{},X=b.fontProperties||{},T=b.counterDescriptors||{},I=b.colorKeywords||
+{},ga=b.valueKeywords||{},M=b.allowNested,fa=b.lineComment,U=!0===b.supportsAtComponent,z={};return z.top=function(c,a,b){if("{"==c)return h(b,a,"block");if("}"==c&&b.context.prev)return u(b);if(U&&/@component/.test(c))return h(b,a,"atComponentBlock");if(/^@(-moz-)?document$/.test(c))return h(b,a,"documentTypes");if(/^@(media|supports|(-moz-)?document|import)$/.test(c))return h(b,a,"atBlock");if(/^@(font-face|counter-style)/.test(c))return b.stateArg=c,"restricted_atBlock_before";if(/^@(-(moz|ms|o|webkit)-)?keyframes$/.test(c))return"keyframes";
+if(c&&"@"==c.charAt(0))return h(b,a,"at");if("hash"==c)k="builtin";else if("word"==c)k="tag";else{if("variable-definition"==c)return"maybeprop";if("interpolation"==c)return h(b,a,"interpolation");if(":"==c)return"pseudo";if(M&&"("==c)return h(b,a,"parens")}return b.context.type},z.block=function(c,a,b){return"word"==c?(c=a.current().toLowerCase(),D.hasOwnProperty(c)?(k="property","maybeprop"):Q.hasOwnProperty(c)?(k="string-2","maybeprop"):M?(k=a.match(/^\s*:(?:\s|$)/,!1)?"property":"tag","block"):
+(k+=" error","maybeprop")):"meta"==c?"block":M||"hash"!=c&&"qualifier"!=c?z.top(c,a,b):(k="error","block")},z.maybeprop=function(c,a,b){return":"==c?h(b,a,"prop"):z[b.context.type](c,a,b)},z.prop=function(c,a,b){if(";"==c)return u(b);if("{"==c&&M)return h(b,a,"propBlock");if("}"==c||"{"==c)return E(c,a,b);if("("==c)return h(b,a,"parens");if("hash"!=c||/^#([0-9a-fA-f]{3,4}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(a.current()))if("word"==c)v(a);else{if("interpolation"==c)return h(b,a,"interpolation")}else k+=
+" error";return"prop"},z.propBlock=function(c,a,b){return"}"==c?u(b):"word"==c?(k="property","maybeprop"):b.context.type},z.parens=function(c,a,b){return"{"==c||"}"==c?E(c,a,b):")"==c?u(b):"("==c?h(b,a,"parens"):"interpolation"==c?h(b,a,"interpolation"):("word"==c&&v(a),"parens")},z.pseudo=function(c,a,b){return"meta"==c?"pseudo":"word"==c?(k="variable-3",b.context.type):z[b.context.type](c,a,b)},z.documentTypes=function(c,a,b){return"word"==c&&x.hasOwnProperty(a.current())?(k="tag",b.context.type):
+z.atBlock(c,a,b)},z.atBlock=function(c,a,b){if("("==c)return h(b,a,"atBlock_parens");if("}"==c||";"==c)return E(c,a,b);if("{"==c)return u(b)&&h(b,a,M?"block":"top");if("interpolation"==c)return h(b,a,"interpolation");"word"==c&&(c=a.current().toLowerCase(),k="only"==c||"not"==c||"and"==c||"or"==c?"keyword":C.hasOwnProperty(c)?"attribute":B.hasOwnProperty(c)?"property":J.hasOwnProperty(c)?"keyword":D.hasOwnProperty(c)?"property":Q.hasOwnProperty(c)?"string-2":ga.hasOwnProperty(c)?"atom":I.hasOwnProperty(c)?
+"keyword":"error");return b.context.type},z.atComponentBlock=function(c,a,b){return"}"==c?E(c,a,b):"{"==c?u(b)&&h(b,a,M?"block":"top",!1):("word"==c&&(k="error"),b.context.type)},z.atBlock_parens=function(c,a,b){return")"==c?u(b):"{"==c||"}"==c?E(c,a,b,2):z.atBlock(c,a,b)},z.restricted_atBlock_before=function(c,a,b){return"{"==c?h(b,a,"restricted_atBlock"):"word"==c&&"@counter-style"==b.stateArg?(k="variable","restricted_atBlock_before"):z[b.context.type](c,a,b)},z.restricted_atBlock=function(c,a,
+b){return"}"==c?(b.stateArg=null,u(b)):"word"==c?(k="@font-face"==b.stateArg&&!X.hasOwnProperty(a.current().toLowerCase())||"@counter-style"==b.stateArg&&!T.hasOwnProperty(a.current().toLowerCase())?"error":"property","maybeprop"):"restricted_atBlock"},z.keyframes=function(c,a,b){return"word"==c?(k="variable","keyframes"):"{"==c?h(b,a,"top"):z[b.context.type](c,a,b)},z.at=function(c,a,b){return";"==c?u(b):"{"==c||"}"==c?E(c,a,b):("word"==c?k="tag":"hash"==c&&(k="builtin"),"at")},z.interpolation=function(c,
+a,b){return"}"==c?u(b):"{"==c||";"==c?E(c,a,b):("word"==c?k="variable":"variable"!=c&&"("!=c&&")"!=c&&(k="error"),"interpolation")},{startState:function(c){return{tokenize:null,state:A?"block":"top",stateArg:null,context:new r(A?"block":"top",c||0,null)}},token:function(c,a){if(!a.tokenize&&c.eatSpace())return null;var b=(a.tokenize||g)(c,a);return b&&"object"==typeof b&&(w=b[1],b=b[0]),k=b,a.state=z[a.state](w,c,a),k},indent:function(c,a){var b=c.context,d=a&&a.charAt(0),m=b.indent;return"prop"!=
+b.type||"}"!=d&&")"!=d||(b=b.prev),b.prev&&("}"!=d||"block"!=b.type&&"top"!=b.type&&"interpolation"!=b.type&&"restricted_atBlock"!=b.type?(")"!=d||"parens"!=b.type&&"atBlock_parens"!=b.type)&&("{"!=d||"at"!=b.type&&"atBlock"!=b.type)||(m=Math.max(0,b.indent-p)):(b=b.prev,m=b.indent)),m},electricChars:"}",blockCommentStart:"/*",blockCommentEnd:"*/",lineComment:fa,fold:"brace"}});var x=["domain","regexp","url","url-prefix"],b=p(x),n="all aural braille handheld print projection screen tty tv embossed".split(" "),
+q=p(n),A="width min-width max-width height min-height max-height device-width min-device-width max-device-width device-height min-device-height max-device-height aspect-ratio min-aspect-ratio max-aspect-ratio device-aspect-ratio min-device-aspect-ratio max-device-aspect-ratio color min-color max-color color-index min-color-index max-color-index monochrome min-monochrome max-monochrome resolution min-resolution max-resolution scan grid orientation device-pixel-ratio min-device-pixel-ratio max-device-pixel-ratio pointer any-pointer hover any-hover".split(" "),
+k=p(A),y="landscape portrait none coarse fine on-demand hover interlace progressive".split(" "),C=p(y),g="align-content align-items align-self alignment-adjust alignment-baseline anchor-point animation animation-delay animation-direction animation-duration animation-fill-mode animation-iteration-count animation-name animation-play-state animation-timing-function appearance azimuth backface-visibility background background-attachment background-blend-mode background-clip background-color background-image background-origin background-position background-repeat background-size baseline-shift binding bleed bookmark-label bookmark-level bookmark-state bookmark-target border border-bottom border-bottom-color border-bottom-left-radius border-bottom-right-radius border-bottom-style border-bottom-width border-collapse border-color border-image border-image-outset border-image-repeat border-image-slice border-image-source border-image-width border-left border-left-color border-left-style border-left-width border-radius border-right border-right-color border-right-style border-right-width border-spacing border-style border-top border-top-color border-top-left-radius border-top-right-radius border-top-style border-top-width border-width bottom box-decoration-break box-shadow box-sizing break-after break-before break-inside caption-side caret-color clear clip color color-profile column-count column-fill column-gap column-rule column-rule-color column-rule-style column-rule-width column-span column-width columns content counter-increment counter-reset crop cue cue-after cue-before cursor direction display dominant-baseline drop-initial-after-adjust drop-initial-after-align drop-initial-before-adjust drop-initial-before-align drop-initial-size drop-initial-value elevation empty-cells fit fit-position flex flex-basis flex-direction flex-flow flex-grow flex-shrink flex-wrap float float-offset flow-from flow-into font font-feature-settings font-family font-kerning font-language-override font-size font-size-adjust font-stretch font-style font-synthesis font-variant font-variant-alternates font-variant-caps font-variant-east-asian font-variant-ligatures font-variant-numeric font-variant-position font-weight grid grid-area grid-auto-columns grid-auto-flow grid-auto-rows grid-column grid-column-end grid-column-gap grid-column-start grid-gap grid-row grid-row-end grid-row-gap grid-row-start grid-template grid-template-areas grid-template-columns grid-template-rows hanging-punctuation height hyphens icon image-orientation image-rendering image-resolution inline-box-align justify-content justify-items justify-self left letter-spacing line-break line-height line-stacking line-stacking-ruby line-stacking-shift line-stacking-strategy list-style list-style-image list-style-position list-style-type margin margin-bottom margin-left margin-right margin-top marks marquee-direction marquee-loop marquee-play-count marquee-speed marquee-style max-height max-width min-height min-width move-to nav-down nav-index nav-left nav-right nav-up object-fit object-position opacity order orphans outline outline-color outline-offset outline-style outline-width overflow overflow-style overflow-wrap overflow-x overflow-y padding padding-bottom padding-left padding-right padding-top page page-break-after page-break-before page-break-inside page-policy pause pause-after pause-before perspective perspective-origin pitch pitch-range place-content place-items place-self play-during position presentation-level punctuation-trim quotes region-break-after region-break-before region-break-inside region-fragment rendering-intent resize rest rest-after rest-before richness right rotation rotation-point ruby-align ruby-overhang ruby-position ruby-span shape-image-threshold shape-inside shape-margin shape-outside size speak speak-as speak-header speak-numeral speak-punctuation speech-rate stress string-set tab-size table-layout target target-name target-new target-position text-align text-align-last text-decoration text-decoration-color text-decoration-line text-decoration-skip text-decoration-style text-emphasis text-emphasis-color text-emphasis-position text-emphasis-style text-height text-indent text-justify text-outline text-overflow text-shadow text-size-adjust text-space-collapse text-transform text-underline-position text-wrap top transform transform-origin transform-style transition transition-delay transition-duration transition-property transition-timing-function unicode-bidi user-select vertical-align visibility voice-balance voice-duration voice-family voice-pitch voice-range voice-rate voice-stress voice-volume volume white-space widows width will-change word-break word-spacing word-wrap z-index clip-path clip-rule mask enable-background filter flood-color flood-opacity lighting-color stop-color stop-opacity pointer-events color-interpolation color-interpolation-filters color-rendering fill fill-opacity fill-rule image-rendering marker marker-end marker-mid marker-start shape-rendering stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-rendering baseline-shift dominant-baseline glyph-orientation-horizontal glyph-orientation-vertical text-anchor writing-mode".split(" "),
+a=p(g),E="scrollbar-arrow-color scrollbar-base-color scrollbar-dark-shadow-color scrollbar-face-color scrollbar-highlight-color scrollbar-shadow-color scrollbar-3d-light-color scrollbar-track-color shape-inside searchfield-cancel-button searchfield-decoration searchfield-results-button searchfield-results-decoration zoom".split(" "),w=p(E),J=p("font-family src unicode-range font-variant font-feature-settings font-stretch font-weight font-style".split(" ")),v=p("additive-symbols fallback negative pad prefix range speak-as suffix symbols system".split(" ")),
+r="aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray grey green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen".split(" "),
+h=p(r),D="above absolute activeborder additive activecaption afar after-white-space ahead alias all all-scroll alphabetic alternate always amharic amharic-abegede antialiased appworkspace arabic-indic armenian asterisks attr auto auto-flow avoid avoid-column avoid-page avoid-region background backwards baseline below bidi-override binary bengali blink block block-axis bold bolder border border-box both bottom break break-all break-word bullets button button-bevel buttonface buttonhighlight buttonshadow buttontext calc cambodian capitalize caps-lock-indicator caption captiontext caret cell center checkbox circle cjk-decimal cjk-earthly-branch cjk-heavenly-stem cjk-ideographic clear clip close-quote col-resize collapse color color-burn color-dodge column column-reverse compact condensed contain content contents content-box context-menu continuous copy counter counters cover crop cross crosshair currentcolor cursive cyclic darken dashed decimal decimal-leading-zero default default-button dense destination-atop destination-in destination-out destination-over devanagari difference disc discard disclosure-closed disclosure-open document dot-dash dot-dot-dash dotted double down e-resize ease ease-in ease-in-out ease-out element ellipse ellipsis embed end ethiopic ethiopic-abegede ethiopic-abegede-am-et ethiopic-abegede-gez ethiopic-abegede-ti-er ethiopic-abegede-ti-et ethiopic-halehame-aa-er ethiopic-halehame-aa-et ethiopic-halehame-am-et ethiopic-halehame-gez ethiopic-halehame-om-et ethiopic-halehame-sid-et ethiopic-halehame-so-et ethiopic-halehame-ti-er ethiopic-halehame-ti-et ethiopic-halehame-tig ethiopic-numeric ew-resize exclusion expanded extends extra-condensed extra-expanded fantasy fast fill fixed flat flex flex-end flex-start footnotes forwards from geometricPrecision georgian graytext grid groove gujarati gurmukhi hand hangul hangul-consonant hard-light hebrew help hidden hide higher highlight highlighttext hiragana hiragana-iroha horizontal hsl hsla hue icon ignore inactiveborder inactivecaption inactivecaptiontext infinite infobackground infotext inherit initial inline inline-axis inline-block inline-flex inline-grid inline-table inset inside intrinsic invert italic japanese-formal japanese-informal justify kannada katakana katakana-iroha keep-all khmer korean-hangul-formal korean-hanja-formal korean-hanja-informal landscape lao large larger left level lighter lighten line-through linear linear-gradient lines list-item listbox listitem local logical loud lower lower-alpha lower-armenian lower-greek lower-hexadecimal lower-latin lower-norwegian lower-roman lowercase ltr luminosity malayalam match matrix matrix3d media-controls-background media-current-time-display media-fullscreen-button media-mute-button media-play-button media-return-to-realtime-button media-rewind-button media-seek-back-button media-seek-forward-button media-slider media-sliderthumb media-time-remaining-display media-volume-slider media-volume-slider-container media-volume-sliderthumb medium menu menulist menulist-button menulist-text menulist-textfield menutext message-box middle min-intrinsic mix mongolian monospace move multiple multiply myanmar n-resize narrower ne-resize nesw-resize no-close-quote no-drop no-open-quote no-repeat none normal not-allowed nowrap ns-resize numbers numeric nw-resize nwse-resize oblique octal opacity open-quote optimizeLegibility optimizeSpeed oriya oromo outset outside outside-shape overlay overline padding padding-box painted page paused persian perspective plus-darker plus-lighter pointer polygon portrait pre pre-line pre-wrap preserve-3d progress push-button radial-gradient radio read-only read-write read-write-plaintext-only rectangle region relative repeat repeating-linear-gradient repeating-radial-gradient repeat-x repeat-y reset reverse rgb rgba ridge right rotate rotate3d rotateX rotateY rotateZ round row row-resize row-reverse rtl run-in running s-resize sans-serif saturation scale scale3d scaleX scaleY scaleZ screen scroll scrollbar scroll-position se-resize searchfield searchfield-cancel-button searchfield-decoration searchfield-results-button searchfield-results-decoration self-start self-end semi-condensed semi-expanded separate serif show sidama simp-chinese-formal simp-chinese-informal single skew skewX skewY skip-white-space slide slider-horizontal slider-vertical sliderthumb-horizontal sliderthumb-vertical slow small small-caps small-caption smaller soft-light solid somali source-atop source-in source-out source-over space space-around space-between space-evenly spell-out square square-button start static status-bar stretch stroke sub subpixel-antialiased super sw-resize symbolic symbols system-ui table table-caption table-cell table-column table-column-group table-footer-group table-header-group table-row table-row-group tamil telugu text text-bottom text-top textarea textfield thai thick thin threeddarkshadow threedface threedhighlight threedlightshadow threedshadow tibetan tigre tigrinya-er tigrinya-er-abegede tigrinya-et tigrinya-et-abegede to top trad-chinese-formal trad-chinese-informal transform translate translate3d translateX translateY translateZ transparent ultra-condensed ultra-expanded underline unset up upper-alpha upper-armenian upper-greek upper-hexadecimal upper-latin upper-norwegian upper-roman uppercase urdu url var vertical vertical-text visible visibleFill visiblePainted visibleStroke visual w-resize wait wave wider window windowframe windowtext words wrap wrap-reverse x-large x-small xor xx-large xx-small".split(" "),
+u=p(D),x=x.concat(n).concat(A).concat(y).concat(g).concat(E).concat(r).concat(D);d.registerHelper("hintWords","css",x);d.defineMIME("text/css",{documentTypes:b,mediaTypes:q,mediaFeatures:k,mediaValueKeywords:C,propertyKeywords:a,nonStandardPropertyKeywords:w,fontProperties:J,counterDescriptors:v,colorKeywords:h,valueKeywords:u,tokenHooks:{"/":function(a,b){return!!a.eat("*")&&(b.tokenize=B,B(a,b))}},name:"css"});d.defineMIME("text/x-scss",{mediaTypes:q,mediaFeatures:k,mediaValueKeywords:C,propertyKeywords:a,
+nonStandardPropertyKeywords:w,colorKeywords:h,valueKeywords:u,fontProperties:J,allowNested:!0,lineComment:"//",tokenHooks:{"/":function(a,b){return a.eat("/")?(a.skipToEnd(),["comment","comment"]):a.eat("*")?(b.tokenize=B,B(a,b)):["operator","operator"]},":":function(a){return!!a.match(/\s*\{/,!1)&&[null,null]},$:function(a){return a.match(/^[\w-]+/),a.match(/^\s*:/,!1)?["variable-2","variable-definition"]:["variable-2","variable"]},"#":function(a){return!!a.eat("{")&&[null,"interpolation"]}},name:"css",
+helperType:"scss"});d.defineMIME("text/x-less",{mediaTypes:q,mediaFeatures:k,mediaValueKeywords:C,propertyKeywords:a,nonStandardPropertyKeywords:w,colorKeywords:h,valueKeywords:u,fontProperties:J,allowNested:!0,lineComment:"//",tokenHooks:{"/":function(a,b){return a.eat("/")?(a.skipToEnd(),["comment","comment"]):a.eat("*")?(b.tokenize=B,B(a,b)):["operator","operator"]},"@":function(a){return a.eat("{")?[null,"interpolation"]:!a.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/,
+!1)&&(a.eatWhile(/[\w\\\-]/),a.match(/^\s*:/,!1)?["variable-2","variable-definition"]:["variable-2","variable"])},"\x26":function(){return["atom","atom"]}},name:"css",helperType:"less"});d.defineMIME("text/x-gss",{documentTypes:b,mediaTypes:q,mediaFeatures:k,propertyKeywords:a,nonStandardPropertyKeywords:w,fontProperties:J,counterDescriptors:v,colorKeywords:h,valueKeywords:u,supportsAtComponent:!0,tokenHooks:{"/":function(a,b){return!!a.eat("*")&&(b.tokenize=B,B(a,b))}},name:"css",helperType:"gss"})});
+(function(d){"object"==typeof exports&&"object"==typeof module?d(require("../../lib/codemirror"),require("../xml/xml"),require("../javascript/javascript"),require("../css/css")):"function"==typeof define&&define.amd?define("mode/htmlmixed/htmlmixed",["../../lib/codemirror","../xml/xml","../javascript/javascript","../css/css"],d):d(CodeMirror)})(function(d){function p(b,d){var k=b.match(n[d]||(n[d]=new RegExp("\\s+"+d+"\\s*\x3d\\s*('|\")?([^'\"]+)('|\")?\\s*")));return k?/^\s*(.*?)\s*$/.exec(k[2])[1]:
+""}function B(b,d){for(var k in b)for(var n=d[k]||(d[k]=[]),p=b[k],g=p.length-1;0<=g;g--)n.unshift(p[g])}function x(b,d){for(var k=0;k<b.length;k++){var n=b[k];if(!n[0]||n[1].test(p(d,n[0])))return n[2]}}var b={script:[["lang",/(javascript|babel)/i,"javascript"],["type",/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i,"javascript"],["type",/./,"text/plain"],[null,null,"javascript"]],style:[["lang",/^css$/i,"css"],["type",/^(text\/)?(x-)?(stylesheet|css)$/i,"css"],["type",/./,"text/plain"],
+[null,null,"css"]]},n={};d.defineMode("htmlmixed",function(n,p){function k(a,b){var g,p=y.token(a,b.htmlState),r=/\btag\b/.test(p);if(r&&!/[<>\s\/]/.test(a.current())&&(g=b.htmlState.tagName&&b.htmlState.tagName.toLowerCase())&&C.hasOwnProperty(g))b.inTag=g+" ";else if(b.inTag&&r&&/>$/.test(a.current())){g=/^([\S]+) (.*)/.exec(b.inTag);b.inTag=null;var r="\x3e"==a.current()&&x(C[g[1]],g[2]),r=d.getMode(n,r),h=new RegExp("^\x3c/s*"+g[1]+"s*\x3e","i"),A=new RegExp("\x3c/s*"+g[1]+"s*\x3e","i");b.token=
+function(a,b){var d;if(a.match(h,!1))d=(b.token=k,b.localState=b.localMode=null,null);else{d=b.localMode.token(a,b.localState);var g=a.current(),n=g.search(A);d=(-1<n?a.backUp(g.length-n):g.match(/<\/?$/)&&(a.backUp(g.length),a.match(A,!1)||a.match(g)),d)}return d};b.localMode=r;b.localState=d.startState(r,y.indent(b.htmlState,""))}else b.inTag&&(b.inTag+=a.current(),a.eol()&&(b.inTag+=" "));return p}var y=d.getMode(n,{name:"xml",htmlMode:!0,multilineTagIndentFactor:p.multilineTagIndentFactor,multilineTagIndentPastTag:p.multilineTagIndentPastTag}),
+C={},g=p&&p.tags,a=p&&p.scriptTypes;if(B(b,C),g&&B(g,C),a)for(g=a.length-1;0<=g;g--)C.script.unshift(["type",a[g].matches,a[g].mode]);return{startState:function(){return{token:k,inTag:null,localMode:null,localState:null,htmlState:d.startState(y)}},copyState:function(a){var b;return a.localState&&(b=d.copyState(a.localMode,a.localState)),{token:a.token,inTag:a.inTag,localMode:a.localMode,localState:b,htmlState:d.copyState(y,a.htmlState)}},token:function(a,b){return b.token(a,b)},indent:function(a,
+b,g){return!a.localMode||/^\s*<\//.test(b)?y.indent(a.htmlState,b):a.localMode.indent?a.localMode.indent(a.localState,b,g):d.Pass},innerMode:function(a){return{state:a.localState||a.htmlState,mode:a.localMode||y}}}},"xml","javascript","css");d.defineMIME("text/html","htmlmixed")});
+(function(d){"object"==typeof exports&&"object"==typeof module?d(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/mode/multiplex",["../../lib/codemirror"],d):d(CodeMirror)})(function(d){d.multiplexingMode=function(p){function B(b,d,q,p){return"string"==typeof d?(q=b.indexOf(d,q),p&&-1<q?q+d.length:q):(d=d.exec(q?b.slice(q):b))?d.index+q+(p?d[0].length:0):-1}var x=Array.prototype.slice.call(arguments,1);return{startState:function(){return{outer:d.startState(p),innerActive:null,
+inner:null}},copyState:function(b){return{outer:d.copyState(p,b.outer),innerActive:b.innerActive,inner:b.innerActive&&d.copyState(b.innerActive.mode,b.inner)}},token:function(b,n){if(n.innerActive){var q=n.innerActive,A=b.string;if(!q.close&&b.sol())return n.innerActive=n.inner=null,this.token(b,n);var k=q.close?B(A,q.close,b.pos,q.parseDelimiters):-1;if(k==b.pos&&!q.parseDelimiters)return b.match(q.close),n.innerActive=n.inner=null,q.delimStyle&&q.delimStyle+" "+q.delimStyle+"-close";-1<k&&(b.string=
+A.slice(0,k));var y=q.mode.token(b,n.inner);return-1<k&&(b.string=A),k==b.pos&&q.parseDelimiters&&(n.innerActive=n.inner=null),q.innerStyle&&(y=y?y+" "+q.innerStyle:q.innerStyle),y}q=1/0;A=b.string;for(y=0;y<x.length;++y){var C=x[y],k=B(A,C.open,b.pos);if(k==b.pos)return C.parseDelimiters||b.match(C.open),n.innerActive=C,n.inner=d.startState(C.mode,p.indent?p.indent(n.outer,""):0),C.delimStyle&&C.delimStyle+" "+C.delimStyle+"-open";-1!=k&&k<q&&(q=k)}q!=1/0&&(b.string=A.slice(0,q));k=p.token(b,n.outer);
+return q!=1/0&&(b.string=A),k},indent:function(b,n){var q=b.innerActive?b.innerActive.mode:p;return q.indent?q.indent(b.innerActive?b.inner:b.outer,n):d.Pass},blankLine:function(b){var n=b.innerActive?b.innerActive.mode:p;if(n.blankLine&&n.blankLine(b.innerActive?b.inner:b.outer),b.innerActive)"\n"===b.innerActive.close&&(b.innerActive=b.inner=null);else for(var q=0;q<x.length;++q){var A=x[q];"\n"===A.open&&(b.innerActive=A,b.inner=d.startState(A.mode,n.indent?n.indent(b.outer,""):0))}},electricChars:p.electricChars,
+innerMode:function(b){return b.inner?{state:b.inner,mode:b.innerActive.mode}:{state:b.outer,mode:p}}}}});
+(function(d){"object"==typeof exports&&"object"==typeof module?d(require("../../lib/codemirror"),require("../htmlmixed/htmlmixed"),require("../../addon/mode/multiplex")):"function"==typeof define&&define.amd?define("mode/htmlembedded/htmlembedded.js",["../../lib/codemirror","../htmlmixed/htmlmixed","../../addon/mode/multiplex"],d):d(CodeMirror)})(function(d){d.defineMode("htmlembedded",function(p,B){return d.multiplexingMode(d.getMode(p,"htmlmixed"),{open:B.open||B.scriptStartRegex||"\x3c%",close:B.close||
+B.scriptEndRegex||"%\x3e",mode:d.getMode(p,B.scriptingModeSpec)})},"htmlmixed");d.defineMIME("application/x-ejs",{name:"htmlembedded",scriptingModeSpec:"javascript"});d.defineMIME("application/x-aspx",{name:"htmlembedded",scriptingModeSpec:"text/x-csharp"});d.defineMIME("application/x-jsp",{name:"htmlembedded",scriptingModeSpec:"text/x-java"});d.defineMIME("application/x-erb",{name:"htmlembedded",scriptingModeSpec:"ruby"})});
+(function(d){"function"==typeof define&&define("modeHtml",["mode/htmlembedded/htmlembedded.js"],function(){})})();
\ No newline at end of file
diff --git a/js/ckeditor/plugins/codemirror/js/codemirror.mode.javascript.min.js b/js/ckeditor/plugins/codemirror/js/codemirror.mode.javascript.min.js
new file mode 100644 (file)
index 0000000..4b4ff98
--- /dev/null
@@ -0,0 +1,29 @@
+!function(p){"object"==typeof exports&&"object"==typeof module?p(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("mode/javascript/javascript.js",["../../lib/codemirror"],p):p(CodeMirror)}(function(p){function ka(p,r,q){return/^(?:operator|sof|keyword c|case|new|export|default|[\[{}\(,;:]|=>)$/.test(r.lastType)||"quasi"==r.lastType&&/\{\s*$/.test(p.string.slice(0,p.pos-(q||0)))}p.defineMode("javascript",function(xa,r){function q(a,c,b){return H=a,Q=b,c}function D(a,c){var b=
+a.next();if('"'==b||"'"==b)return c.tokenize=ya(b),c.tokenize(a,c);if("."==b&&a.match(/^\d+(?:[eE][+\-]?\d+)?/))return q("number","number");if("."==b&&a.match(".."))return q("spread","meta");if(/[\[\]{}\(\),;\:\.]/.test(b))return q(b);if("\x3d"==b&&a.eat("\x3e"))return q("\x3d\x3e","operator");if("0"==b&&a.eat(/x/i))return a.eatWhile(/[\da-f]/i),q("number","number");if("0"==b&&a.eat(/o/i))return a.eatWhile(/[0-7]/i),q("number","number");if("0"==b&&a.eat(/b/i))return a.eatWhile(/[01]/i),q("number",
+"number");if(/\d/.test(b))return a.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/),q("number","number");if("/"==b){if(a.eat("*"))b=(c.tokenize=R,R(a,c));else if(a.eat("/"))b=(a.skipToEnd(),q("comment","comment"));else if(ka(a,c,1)){a:for(var e=!1,d=!1;null!=(b=a.next());){if(!e){if("/"==b&&!d)break a;"["==b?d=!0:d&&"]"==b&&(d=!1)}e=!e&&"\\"==b}b=(a.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/),q("regexp","string-2"))}else b=(a.eatWhile(S),q("operator","operator",a.current()));return b}if("`"==b)return c.tokenize=
+aa,aa(a,c);if("#"==b)return a.skipToEnd(),q("error","error");if(S.test(b))return"\x3e"==b&&c.lexical&&"\x3e"==c.lexical.type||a.eatWhile(S),q("operator","operator",a.current());if(ba.test(b)){a.eatWhile(ba);b=a.current();if("."!=c.lastType){if(la.propertyIsEnumerable(b))return e=la[b],q(e.type,e.style,b);if("async"==b&&a.match(/^\s*[\(\w]/,!1))return q("async","keyword",b)}return q("variable","variable",b)}}function ya(a){return function(c,b){var e,d=!1;if(T&&"@"==c.peek()&&c.match(za))return b.tokenize=
+D,q("jsonld-keyword","meta");for(;null!=(e=c.next())&&(e!=a||d);)d=!d&&"\\"==e;return d||(b.tokenize=D),q("string","string")}}function R(a,b){for(var f,e=!1;f=a.next();){if("/"==f&&e){b.tokenize=D;break}e="*"==f}return q("comment","comment")}function aa(a,b){for(var f,e=!1;null!=(f=a.next());){if(!e&&("`"==f||"$"==f&&a.eat("{"))){b.tokenize=D;break}e=!e&&"\\"==f}return q("quasi","string-2",a.current())}function ca(a,b){b.fatArrowAt&&(b.fatArrowAt=null);var f=a.string.indexOf("\x3d\x3e",a.start);if(!(0>
+f)){if(w){var e=/:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(a.string.slice(a.start,f));e&&(f=e.index)}for(var e=0,d=!1,f=f-1;0<=f;--f){var k=a.string.charAt(f),h=Aa.indexOf(k);if(0<=h&&3>h){if(!e){++f;break}if(0==--e){"("==k&&(d=!0);break}}else if(3<=h&&6>h)++e;else if(ba.test(k))d=!0;else{if(/["'\/]/.test(k))return;if(d&&!e){++f;break}}}d&&!e&&(b.fatArrowAt=f)}}function ma(a,b,d,e,A,k){this.indented=a;this.column=b;this.type=d;this.prev=A;this.info=k;null!=e&&(this.align=e)}function g(){for(var a=
+arguments.length-1;0<=a;a--)d.cc.push(arguments[a])}function b(){return g.apply(null,arguments),!0}function I(a){function b(c){for(;c;c=c.next)if(c.name==a)return!0;return!1}var f=d.state;(d.marked="def",f.context)?b(f.localVars)||(f.localVars={name:a,next:f.localVars}):b(f.globalVars)||r.globalVars&&(f.globalVars={name:a,next:f.globalVars})}function J(){d.state.context={prev:d.state.context,vars:d.state.localVars};d.state.localVars=Ba}function K(){d.state.localVars=d.state.context.vars;d.state.context=
+d.state.context.prev}function l(a,b){var f=function(){var e=d.state,f=e.indented;if("stat"==e.lexical.type)f=e.lexical.indented;else for(var k=e.lexical;k&&")"==k.type&&k.align;k=k.prev)f=k.indented;e.lexical=new ma(f,d.stream.column(),a,null,e.lexical,b)};return f.lex=!0,f}function h(){var a=d.state;a.lexical.prev&&(")"==a.lexical.type&&(a.indented=a.lexical.indented),a.lexical=a.lexical.prev)}function m(a){function c(d){return d==a?b():";"==a?g():b(c)}return c}function t(a,c){return"var"==a?b(l("vardef",
+c.length),da,m(";"),h):"keyword a"==a?b(l("form"),ea,t,h):"keyword b"==a?b(l("form"),t,h):"{"==a?b(l("}"),U,h):";"==a?b():"if"==a?("else"==d.state.lexical.info&&d.state.cc[d.state.cc.length-1]==h&&d.state.cc.pop()(),b(l("form"),ea,t,h,na)):"function"==a?b(x):"for"==a?b(l("form"),Ca,t,h):"variable"==a?w&&"type"==c?(d.marked="keyword",b(u,m("operator"),u,m(";"))):b(l("stat"),Da):"switch"==a?b(l("form"),ea,m("{"),l("}","switch"),U,h,h):"case"==a?b(n,m(":")):"default"==a?b(m(":")):"catch"==a?b(l("form"),
+J,m("("),fa,m(")"),t,h,K):"class"==a?b(l("form"),oa,h):"export"==a?b(l("stat"),Ea,h):"import"==a?b(l("stat"),Fa,h):"module"==a?b(l("form"),y,m("{"),l("}"),U,h,h):"async"==a?b(t):"@"==c?b(n,t):g(l("stat"),n,m(";"),h)}function n(a){return pa(a,!1)}function v(a){return pa(a,!0)}function ea(a){return"("!=a?g():b(l(")"),n,m(")"),h)}function pa(a,c){if(d.state.fatArrowAt==d.stream.start){var f=c?qa:ra;if("("==a)return b(J,l(")"),z(y,")"),h,m("\x3d\x3e"),f,K);if("variable"==a)return g(J,y,m("\x3d\x3e"),
+f,K)}f=c?L:E;return Ga.hasOwnProperty(a)?b(f):"function"==a?b(x,f):"class"==a?b(l("form"),Ha,h):"keyword c"==a||"async"==a?b(c?Ia:ga):"("==a?b(l(")"),ga,m(")"),h,f):"operator"==a||"spread"==a?b(c?v:n):"["==a?b(l("]"),Ja,h,f):"{"==a?M(ha,"}",null,f):"quasi"==a?g(V,f):"new"==a?b(Ka(c)):b()}function ga(a){return a.match(/[;\}\)\],]/)?g():g(n)}function Ia(a){return a.match(/[;\}\)\],]/)?g():g(v)}function E(a,c){return","==a?b(n):L(a,c,!1)}function L(a,c,f){var e=0==f?E:L,A=0==f?n:v;return"\x3d\x3e"==
+a?b(J,f?qa:ra,K):"operator"==a?/\+\+|--/.test(c)?b(e):"?"==c?b(n,m(":"),A):b(A):"quasi"==a?g(V,e):";"!=a?"("==a?M(v,")","call",e):"."==a?b(La,e):"["==a?b(l("]"),ga,m("]"),h,e):w&&"as"==c?(d.marked="keyword",b(u,e)):void 0:void 0}function V(a,c){return"quasi"!=a?g():"${"!=c.slice(c.length-2)?b(V):b(n,Ma)}function Ma(a){if("}"==a)return d.marked="string-2",d.state.tokenize=aa,b(V)}function ra(a){return ca(d.stream,d.state),g("{"==a?t:n)}function qa(a){return ca(d.stream,d.state),g("{"==a?t:v)}function Ka(a){return function(c){return"."==
+c?b(a?Na:Oa):g(a?v:n)}}function Oa(a,c){if("target"==c)return d.marked="keyword",b(E)}function Na(a,c){if("target"==c)return d.marked="keyword",b(L)}function Da(a){return":"==a?b(h,t):g(E,m(";"),h)}function La(a){if("variable"==a)return d.marked="property",b()}function ha(a,c){return"async"==a?(d.marked="property",b(ha)):"variable"==a||"keyword"==d.style?(d.marked="property",b("get"==c||"set"==c?Pa:B)):"number"==a||"string"==a?(d.marked=T?"property":d.style+" property",b(B)):"jsonld-keyword"==a?b(B):
+"modifier"==a?b(ha):"["==a?b(n,m("]"),B):"spread"==a?b(n,B):":"==a?g(B):void 0}function Pa(a){return"variable"!=a?g(B):(d.marked="property",b(x))}function B(a){return":"==a?b(v):"("==a?g(x):void 0}function z(a,c,f){function e(A,k){if(f?-1<f.indexOf(A):","==A){var h=d.state.lexical;return"call"==h.info&&(h.pos=(h.pos||0)+1),b(function(b,e){return b==c||e==c?g():g(a)},e)}return A==c||k==c?b():b(m(c))}return function(d,f){return d==c||f==c?b():g(a,e)}}function M(a,c,f){for(var e=3;e<arguments.length;e++)d.cc.push(arguments[e]);
+return b(l(c,f),z(a,c),h)}function U(a){return"}"==a?b():g(t,U)}function N(a,c){if(w){if(":"==a)return b(u);if("?"==c)return b(N)}}function u(a){return"variable"==a?(d.marked="type",b(O)):"string"==a||"number"==a||"atom"==a?b(O):"{"==a?b(l("}"),z(W,"}",",;"),h,O):"("==a?b(z(sa,")"),Qa):void 0}function Qa(a){if("\x3d\x3e"==a)return b(u)}function W(a,c){return"variable"==a||"keyword"==d.style?(d.marked="property",b(W)):"?"==c?b(W):":"==a?b(u):"["==a?b(n,N,m("]"),W):void 0}function sa(a){return"variable"==
+a?b(sa):":"==a?b(u):void 0}function O(a,c){return"\x3c"==c?b(l("\x3e"),z(u,"\x3e"),h,O):"|"==c||"."==a?b(u):"["==a?b(m("]"),O):"extends"==c?b(u):void 0}function da(){return g(y,N,P,Ra)}function y(a,c){return"modifier"==a?b(y):"variable"==a?(I(c),b()):"spread"==a?b(y):"["==a?M(y,"]"):"{"==a?M(Sa,"}"):void 0}function Sa(a,c){return"variable"!=a||d.stream.match(/^\s*:/,!1)?("variable"==a&&(d.marked="property"),"spread"==a?b(y):"}"==a?g():b(m(":"),y,P)):(I(c),b(P))}function P(a,c){if("\x3d"==c)return b(v)}
+function Ra(a){if(","==a)return b(da)}function na(a,c){if("keyword b"==a&&"else"==c)return b(l("form","else"),t,h)}function Ca(a){if("("==a)return b(l(")"),Ta,m(")"),h)}function Ta(a){return"var"==a?b(da,m(";"),X):";"==a?b(X):"variable"==a?b(Ua):g(n,m(";"),X)}function Ua(a,c){return"in"==c||"of"==c?(d.marked="keyword",b(n)):b(E,X)}function X(a,c){return";"==a?b(ta):"in"==c||"of"==c?(d.marked="keyword",b(n)):g(n,m(";"),ta)}function ta(a){")"!=a&&b(n)}function x(a,c){return"*"==c?(d.marked="keyword",
+b(x)):"variable"==a?(I(c),b(x)):"("==a?b(J,l(")"),z(fa,")"),h,N,t,K):w&&"\x3c"==c?b(l("\x3e"),z(u,"\x3e"),h,x):void 0}function fa(a){return"spread"==a?b(fa):g(y,N,P)}function Ha(a,b){return"variable"==a?oa(a,b):Y(a,b)}function oa(a,c){if("variable"==a)return I(c),b(Y)}function Y(a,c){return"\x3c"==c?b(l("\x3e"),z(u,"\x3e"),h,Y):"extends"==c||"implements"==c||w&&","==a?b(w?u:n,Y):"{"==a?b(l("}"),C,h):void 0}function C(a,c){return"variable"==a||"keyword"==d.style?("async"==c||"static"==c||"get"==c||
+"set"==c||w&&("public"==c||"private"==c||"protected"==c||"readonly"==c||"abstract"==c))&&d.stream.match(/^\s+[\w$\xa1-\uffff]/,!1)?(d.marked="keyword",b(C)):(d.marked="property",b(w?ia:x,C)):"["==a?b(n,m("]"),w?ia:x,C):"*"==c?(d.marked="keyword",b(C)):";"==a?b(C):"}"==a?b():"@"==c?b(n,C):void 0}function ia(a,c){return"?"==c?b(ia):":"==a?b(u,P):"\x3d"==c?b(v):g(x)}function Ea(a,c){return"*"==c?(d.marked="keyword",b(ja,m(";"))):"default"==c?(d.marked="keyword",b(n,m(";"))):"{"==a?b(z(ua,"}"),ja,m(";")):
+g(t)}function ua(a,c){return"as"==c?(d.marked="keyword",b(m("variable"))):"variable"==a?g(v,ua):void 0}function Fa(a){return"string"==a?b():g(Z,va,ja)}function Z(a,c){return"{"==a?M(Z,"}"):("variable"==a&&I(c),"*"==c&&(d.marked="keyword"),b(Va))}function va(a){if(","==a)return b(Z,va)}function Va(a,c){if("as"==c)return d.marked="keyword",b(Z)}function ja(a,c){if("from"==c)return d.marked="keyword",b(n)}function Ja(a){return"]"==a?b():g(z(v,"]"))}var H,Q,F=xa.indentUnit,wa=r.statementIndent,T=r.jsonld,
+G=r.json||T,w=r.typescript,ba=r.wordCharacters||/[\w$\xa1-\uffff]/,la=function(){function a(a){return{type:a,style:"keyword"}}var b=a("keyword a"),d=a("keyword b"),e=a("keyword c"),h=a("operator"),k={type:"atom",style:"atom"},b={"if":a("if"),"while":b,"with":b,"else":d,"do":d,"try":d,"finally":d,"return":e,"break":e,"continue":e,"new":a("new"),"delete":e,"throw":e,"debugger":e,"var":a("var"),"const":a("var"),let:a("var"),"function":a("function"),"catch":a("catch"),"for":a("for"),"switch":a("switch"),
+"case":a("case"),"default":a("default"),"in":h,"typeof":h,"instanceof":h,"true":k,"false":k,"null":k,undefined:k,NaN:k,Infinity:k,"this":a("this"),"class":a("class"),"super":a("atom"),yield:e,"export":a("export"),"import":a("import"),"extends":e,await:e};if(w){var d={type:"variable",style:"type"},e={"interface":a("class"),"implements":e,namespace:e,module:a("module"),"enum":a("module"),"public":a("modifier"),"private":a("modifier"),"protected":a("modifier"),"abstract":a("modifier"),string:d,number:d,
+"boolean":d,any:d},g;for(g in e)b[g]=e[g]}return b}(),S=/[+\-*&%=<>!?|~^@]/,za=/^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/,Aa="([{}])",Ga={atom:!0,number:!0,variable:!0,string:!0,regexp:!0,"this":!0,"jsonld-keyword":!0},d={state:null,column:null,marked:null,cc:null},Ba={name:"this",next:{name:"arguments"}};return h.lex=!0,{startState:function(a){a={tokenize:D,lastType:"sof",cc:[],lexical:new ma((a||0)-F,0,"block",!1),localVars:r.localVars,context:r.localVars&&
+{vars:r.localVars},indented:a||0};return r.globalVars&&"object"==typeof r.globalVars&&(a.globalVars=r.globalVars),a},token:function(a,b){if(a.sol()&&(b.lexical.hasOwnProperty("align")||(b.lexical.align=!1),b.indented=a.indentation(),ca(a,b)),b.tokenize!=R&&a.eatSpace())return null;var f=b.tokenize(a,b);if("comment"!=H){b.lastType="operator"!=H||"++"!=Q&&"--"!=Q?H:"incdec";a:{var e=H,h=Q,k=b.cc;d.state=b;d.stream=a;d.marked=null;d.cc=k;d.style=f;for(b.lexical.hasOwnProperty("align")||(b.lexical.align=
+!0);;)if((k.length?k.pop():G?n:t)(e,h)){for(;k.length&&k[k.length-1].lex;)k.pop()();if(d.marked)f=d.marked;else{if(e="variable"==e)b:{for(e=b.localVars;e;e=e.next)if(e.name==h){e=!0;break b}for(k=b.context;k;k=k.prev)for(e=k.vars;e;e=e.next)if(e.name==h){e=!0;break b}e=void 0}f=e?"variable-2":f}break a}}}return f},indent:function(a,b){if(a.tokenize==R)return p.Pass;if(a.tokenize!=D)return 0;var d,e=b&&b.charAt(0),g=a.lexical;if(!/^\s*else\b/.test(b))for(var k=a.cc.length-1;0<=k;--k){var l=a.cc[k];
+if(l==h)g=g.prev;else if(l!=na)break}for(;!("stat"!=g.type&&"form"!=g.type||"}"!=e&&(!(d=a.cc[a.cc.length-1])||d!=E&&d!=L||/^[,\.=+\-*:?[\(]/.test(b)));)g=g.prev;wa&&")"==g.type&&"stat"==g.prev.type&&(g=g.prev);d=g.type;k=e==d;"vardef"==d?e=g.indented+("operator"==a.lastType||","==a.lastType?g.info+1:0):"form"==d&&"{"==e?e=g.indented:"form"==d?e=g.indented+F:"stat"==d?(e=g.indented,g="operator"==a.lastType||","==a.lastType||S.test(b.charAt(0))||/[,.]/.test(b.charAt(0)),e+=g?wa||F:0):e="switch"!=g.info||
+k||0==r.doubleIndentSwitch?g.align?g.column+(k?0:1):g.indented+(k?0:F):g.indented+(/^(?:case|default)\b/.test(b)?F:2*F);return e},electricInput:/^\s*(?:case .*?:|default:|\{|\})$/,blockCommentStart:G?null:"/*",blockCommentEnd:G?null:"*/",lineComment:G?null:"//",fold:"brace",closeBrackets:"()[]{}''\"\"``",helperType:G?"json":"javascript",jsonldMode:T,jsonMode:G,expressionAllowed:ka,skipExpression:function(a){var b=a.cc[a.cc.length-1];b!=n&&b!=v||a.cc.pop()}}});p.registerHelper("wordChars","javascript",
+/[\w$]/);p.defineMIME("text/javascript","javascript");p.defineMIME("text/ecmascript","javascript");p.defineMIME("application/javascript","javascript");p.defineMIME("application/x-javascript","javascript");p.defineMIME("application/ecmascript","javascript");p.defineMIME("application/json",{name:"javascript",json:!0});p.defineMIME("application/x-json",{name:"javascript",json:!0});p.defineMIME("application/ld+json",{name:"javascript",jsonld:!0});p.defineMIME("text/typescript",{name:"javascript",typescript:!0});
+p.defineMIME("application/typescript",{name:"javascript",typescript:!0})});(function(p){"function"==typeof p.define&&p.define("modeJs",["mode/javascript/javascript.js"],function(){})})(this);
\ No newline at end of file
diff --git a/js/ckeditor/plugins/codemirror/js/codemirror.mode.php.min.js b/js/ckeditor/plugins/codemirror/js/codemirror.mode.php.min.js
new file mode 100644 (file)
index 0000000..6005e39
--- /dev/null
@@ -0,0 +1,113 @@
+!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("mode/xml/xml",["../../lib/codemirror"],e):e(CodeMirror)}(function(e){var A={autoSelfClosers:{area:!0,base:!0,br:!0,col:!0,command:!0,embed:!0,frame:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0,menuitem:!0},implicitlyClosed:{dd:!0,li:!0,optgroup:!0,option:!0,p:!0,rp:!0,rt:!0,tbody:!0,td:!0,tfoot:!0,th:!0,tr:!0},contextGrabbers:{dd:{dd:!0,
+dt:!0},dt:{dd:!0,dt:!0},li:{li:!0},option:{option:!0,optgroup:!0},optgroup:{optgroup:!0},p:{address:!0,article:!0,aside:!0,blockquote:!0,dir:!0,div:!0,dl:!0,fieldset:!0,footer:!0,form:!0,h1:!0,h2:!0,h3:!0,h4:!0,h5:!0,h6:!0,header:!0,hgroup:!0,hr:!0,menu:!0,nav:!0,ol:!0,p:!0,pre:!0,section:!0,table:!0,ul:!0},rp:{rp:!0,rt:!0},rt:{rp:!0,rt:!0},tbody:{tbody:!0,tfoot:!0},td:{td:!0,th:!0},tfoot:{tbody:!0},th:{td:!0,th:!0},thead:{tbody:!0,tfoot:!0},tr:{tr:!0}},doNotIndent:{pre:!0},allowUnquoted:!0,allowMissing:!0,
+caseFold:!0},C={autoSelfClosers:{},implicitlyClosed:{},contextGrabbers:{},doNotIndent:{},allowUnquoted:!1,allowMissing:!1,caseFold:!1};e.defineMode("xml",function(w,x){function f(b,c){function a(g){return c.tokenize=g,g(b,c)}var g=b.next();if("\x3c"==g)return b.eat("!")?b.eat("[")?b.match("CDATA[")?a(u("atom","]]\x3e")):null:b.match("--")?a(u("comment","--\x3e")):b.match("DOCTYPE",!0,!0)?(b.eatWhile(/[\w\._\-]/),a(n(1))):null:b.eat("?")?(b.eatWhile(/[\w\._\-]/),c.tokenize=u("meta","?\x3e"),"meta"):
+(I=b.eat("/")?"closeTag":"openTag",c.tokenize=d,"tag bracket");if("\x26"==g){var e;return e=b.eat("#")?b.eat("x")?b.eatWhile(/[a-fA-F\d]/)&&b.eat(";"):b.eatWhile(/[\d]/)&&b.eat(";"):b.eatWhile(/[\w\.\-:]/)&&b.eat(";"),e?"atom":"error"}return b.eatWhile(/[^&<]/),null}function d(b,c){var a=b.next();return"\x3e"==a||"/"==a&&b.eat("\x3e")?(c.tokenize=f,I="\x3e"==a?"endTag":"selfcloseTag","tag bracket"):"\x3d"==a?(I="equals",null):"\x3c"==a?(c.tokenize=f,c.state=D,c.tagName=c.tagStart=null,(a=c.tokenize(b,
+c))?a+" tag error":"tag error"):/[\'\"]/.test(a)?(c.tokenize=t(a),c.stringStartCol=b.column(),c.tokenize(b,c)):(b.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/),"word")}function t(b){var c=function(c,a){for(;!c.eol();)if(c.next()==b){a.tokenize=d;break}return"string"};return c.isInAttribute=!0,c}function u(b,c){return function(a,g){for(;!a.eol();){if(a.match(c)){g.tokenize=f;break}a.next()}return b}}function n(b){return function(c,a){for(var g;null!=(g=c.next());){if("\x3c"==g)return a.tokenize=
+n(b+1),a.tokenize(c,a);if("\x3e"==g){if(1==b){a.tokenize=f;break}return a.tokenize=n(b-1),a.tokenize(c,a)}}return"meta"}}function y(b,c,a){this.prev=b.context;this.tagName=c;this.indent=b.indented;this.startOfLine=a;(F.doNotIndent.hasOwnProperty(c)||b.context&&b.context.noIndent)&&(this.noIndent=!0)}function m(b){b.context&&(b.context=b.context.prev)}function c(b,c){for(var a;b.context&&(a=b.context.tagName,F.contextGrabbers.hasOwnProperty(a)&&F.contextGrabbers[a].hasOwnProperty(c));)m(b)}function D(b,
+c,a){return"openTag"==b?(a.tagStart=c.column(),z):"closeTag"==b?H:D}function z(b,c,a){return"word"==b?(a.tagName=c.current(),E="tag",k):(E="error",z)}function H(b,c,a){return"word"==b?(b=c.current(),a.context&&a.context.tagName!=b&&F.implicitlyClosed.hasOwnProperty(a.context.tagName)&&m(a),a.context&&a.context.tagName==b||!1===F.matchClosing?(E="tag",v):(E="tag error",p)):(E="error",p)}function v(b,a,c){return"endTag"!=b?(E="error",v):(m(c),D)}function p(b,a,c){return E="error",v(b,a,c)}function k(b,
+g,d){if("word"==b)return E="attribute",a;if("endTag"==b||"selfcloseTag"==b){g=d.tagName;var e=d.tagStart;return d.tagName=d.tagStart=null,"selfcloseTag"==b||F.autoSelfClosers.hasOwnProperty(g)?c(d,g):(c(d,g),d.context=new y(d,g,e==d.indented)),D}return E="error",k}function a(a,c,g){return"equals"==a?b:(F.allowMissing||(E="error"),k(a,c,g))}function b(b,a,c){return"string"==b?g:"word"==b&&F.allowUnquoted?(E="string",k):(E="error",k(b,a,c))}function g(b,a,c){return"string"==b?g:k(b,a,c)}var T=w.indentUnit,
+F={},W=x.htmlMode?A:C,Q;for(Q in W)F[Q]=W[Q];for(Q in x)F[Q]=x[Q];var I,E;return f.isInText=!0,{startState:function(b){var a={tokenize:f,state:D,indented:b||0,tagName:null,tagStart:null,context:null};return null!=b&&(a.baseIndent=b),a},token:function(b,a){if(!a.tagName&&b.sol()&&(a.indented=b.indentation()),b.eatSpace())return null;I=null;var c=a.tokenize(b,a);return(c||I)&&"comment"!=c&&(E=null,a.state=a.state(I||c,b,a),E&&(c="error"==E?c+" error":E)),c},indent:function(b,a,c){var g=b.context;if(b.tokenize.isInAttribute)return b.tagStart==
+b.indented?b.stringStartCol+1:b.indented+T;if(g&&g.noIndent)return e.Pass;if(b.tokenize!=d&&b.tokenize!=f)return c?c.match(/^(\s*)/)[0].length:0;if(b.tagName)return!1!==F.multilineTagIndentPastTag?b.tagStart+b.tagName.length+2:b.tagStart+T*(F.multilineTagIndentFactor||1);if(F.alignCDATA&&/<!\[CDATA\[/.test(a))return 0;if((a=a&&/^<(\/)?([\w_:\.-]*)/.exec(a))&&a[1])for(;g;){if(g.tagName==a[2]){g=g.prev;break}if(!F.implicitlyClosed.hasOwnProperty(g.tagName))break;g=g.prev}else if(a)for(;g;){c=F.contextGrabbers[g.tagName];
+if(!c||!c.hasOwnProperty(a[2]))break;g=g.prev}for(;g&&g.prev&&!g.startOfLine;)g=g.prev;return g?g.indent+T:b.baseIndent||0},electricInput:/<\/[\s\w:]+>$/,blockCommentStart:"\x3c!--",blockCommentEnd:"--\x3e",configuration:F.htmlMode?"html":"xml",helperType:F.htmlMode?"html":"xml",skipAttribute:function(a){a.state==b&&(a.state=k)}}});e.defineMIME("text/xml","xml");e.defineMIME("application/xml","xml");e.mimeModes.hasOwnProperty("text/html")||e.defineMIME("text/html",{name:"xml",htmlMode:!0})});
+(function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("mode/javascript/javascript",["../../lib/codemirror"],e):e(CodeMirror)})(function(e){function A(e,w,x){return/^(?:operator|sof|keyword c|case|new|export|default|[\[{}\(,;:]|=>)$/.test(w.lastType)||"quasi"==w.lastType&&/\{\s*$/.test(e.string.slice(0,e.pos-(x||0)))}e.defineMode("javascript",function(C,w){function x(q,b,a){return Z=q,da=a,b}function f(q,b){var a=
+q.next();if('"'==a||"'"==a)return b.tokenize=d(a),b.tokenize(q,b);if("."==a&&q.match(/^\d+(?:[eE][+\-]?\d+)?/))return x("number","number");if("."==a&&q.match(".."))return x("spread","meta");if(/[\[\]{}\(\),;\:\.]/.test(a))return x(a);if("\x3d"==a&&q.eat("\x3e"))return x("\x3d\x3e","operator");if("0"==a&&q.eat(/x/i))return q.eatWhile(/[\da-f]/i),x("number","number");if("0"==a&&q.eat(/o/i))return q.eatWhile(/[0-7]/i),x("number","number");if("0"==a&&q.eat(/b/i))return q.eatWhile(/[01]/i),x("number",
+"number");if(/\d/.test(a))return q.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/),x("number","number");if("/"==a){if(q.eat("*"))a=(b.tokenize=t,t(q,b));else if(q.eat("/"))a=(q.skipToEnd(),x("comment","comment"));else if(A(q,b,1)){a:for(var c=!1,h=!1;null!=(a=q.next());){if(!c){if("/"==a&&!h)break a;"["==a?h=!0:h&&"]"==a&&(h=!1)}c=!c&&"\\"==a}a=(q.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/),x("regexp","string-2"))}else a=(q.eatWhile(ea),x("operator","operator",q.current()));return a}if("`"==a)return b.tokenize=
+u,u(q,b);if("#"==a)return q.skipToEnd(),x("error","error");if(ea.test(a))return"\x3e"==a&&b.lexical&&"\x3e"==b.lexical.type||q.eatWhile(ea),x("operator","operator",q.current());if(la.test(a)){q.eatWhile(la);a=q.current();if("."!=b.lastType){if(ua.propertyIsEnumerable(a))return c=ua[a],x(c.type,c.style,a);if("async"==a&&q.match(/^\s*[\(\w]/,!1))return x("async","keyword",a)}return x("variable","variable",a)}}function d(q){return function(b,a){var c,h=!1;if(fa&&"@"==b.peek()&&b.match(Da))return a.tokenize=
+f,x("jsonld-keyword","meta");for(;null!=(c=b.next())&&(c!=q||h);)h=!h&&"\\"==c;return h||(a.tokenize=f),x("string","string")}}function t(q,b){for(var a,c=!1;a=q.next();){if("/"==a&&c){b.tokenize=f;break}c="*"==a}return x("comment","comment")}function u(q,b){for(var a,c=!1;null!=(a=q.next());){if(!c&&("`"==a||"$"==a&&q.eat("{"))){b.tokenize=f;break}c=!c&&"\\"==a}return x("quasi","string-2",q.current())}function n(q,a){a.fatArrowAt&&(a.fatArrowAt=null);var b=q.string.indexOf("\x3d\x3e",q.start);if(!(0>
+b)){if(K){var c=/:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(q.string.slice(q.start,b));c&&(b=c.index)}for(var c=0,h=!1,b=b-1;0<=b;--b){var g=q.string.charAt(b),l=Ea.indexOf(g);if(0<=l&&3>l){if(!c){++b;break}if(0==--c){"("==g&&(h=!0);break}}else if(3<=l&&6>l)++c;else if(la.test(g))h=!0;else{if(/["'\/]/.test(g))return;if(h&&!c){++b;break}}}h&&!c&&(a.fatArrowAt=b)}}function y(q,b,a,c,h,g){this.indented=q;this.column=b;this.type=a;this.prev=h;this.info=g;null!=c&&(this.align=c)}function m(){for(var q=
+arguments.length-1;0<=q;q--)r.cc.push(arguments[q])}function c(){return m.apply(null,arguments),!0}function D(q){function b(a){for(;a;a=a.next)if(a.name==q)return!0;return!1}var a=r.state;(r.marked="def",a.context)?b(a.localVars)||(a.localVars={name:q,next:a.localVars}):b(a.globalVars)||w.globalVars&&(a.globalVars={name:q,next:a.globalVars})}function z(){r.state.context={prev:r.state.context,vars:r.state.localVars};r.state.localVars=Fa}function H(){r.state.localVars=r.state.context.vars;r.state.context=
+r.state.context.prev}function v(q,a){var b=function(){var b=r.state,c=b.indented;if("stat"==b.lexical.type)c=b.lexical.indented;else for(var h=b.lexical;h&&")"==h.type&&h.align;h=h.prev)c=h.indented;b.lexical=new y(c,r.stream.column(),q,null,b.lexical,a)};return b.lex=!0,b}function p(){var b=r.state;b.lexical.prev&&(")"==b.lexical.type&&(b.indented=b.lexical.indented),b.lexical=b.lexical.prev)}function k(b){function a(h){return h==b?c():";"==b?m():c(a)}return a}function a(q,g){return"var"==q?c(v("vardef",
+g.length),B,k(";"),p):"keyword a"==q?c(v("form"),T,a,p):"keyword b"==q?c(v("form"),a,p):"{"==q?c(v("}"),R,p):";"==q?c():"if"==q?("else"==r.state.lexical.info&&r.state.cc[r.state.cc.length-1]==p&&r.state.cc.pop()(),c(v("form"),T,a,p,va)):"function"==q?c(M):"for"==q?c(v("form"),Ga,a,p):"variable"==q?K&&"type"==g?(r.marked="keyword",c(G,k("operator"),G,k(";"))):c(v("stat"),ma):"switch"==q?c(v("form"),T,k("{"),v("}","switch"),R,p,p):"case"==q?c(b,k(":")):"default"==q?c(k(":")):"catch"==q?c(v("form"),
+z,k("("),na,k(")"),a,p,H):"class"==q?c(v("form"),wa,p):"export"==q?c(v("stat"),Ha,p):"import"==q?c(v("stat"),Ia,p):"module"==q?c(v("form"),h,k("{"),v("}"),R,p,p):"async"==q?c(a):"@"==g?c(b,a):m(v("stat"),b,k(";"),p)}function b(b){return F(b,!1)}function g(b){return F(b,!0)}function T(a){return"("!=a?m():c(v(")"),b,k(")"),p)}function F(a,l){if(r.state.fatArrowAt==r.stream.start){var d=l?ta:sa;if("("==a)return c(z,v(")"),J(h,")"),p,k("\x3d\x3e"),d,H);if("variable"==a)return m(z,h,k("\x3d\x3e"),d,H)}d=
+l?E:I;return Ja.hasOwnProperty(a)?c(d):"function"==a?c(M,d):"class"==a?c(v("form"),Ka,p):"keyword c"==a||"async"==a?c(l?Q:W):"("==a?c(v(")"),W,k(")"),p,d):"operator"==a||"spread"==a?c(l?g:b):"["==a?c(v("]"),La,p,d):"{"==a?N(aa,"}",null,d):"quasi"==a?m(ba,d):"new"==a?c(Ca(l)):c()}function W(a){return a.match(/[;\}\)\],]/)?m():m(b)}function Q(a){return a.match(/[;\}\)\],]/)?m():m(g)}function I(a,h){return","==a?c(b):E(a,h,!1)}function E(a,h,l){var d=0==l?I:E,e=0==l?b:g;return"\x3d\x3e"==a?c(z,l?ta:
+sa,H):"operator"==a?/\+\+|--/.test(h)?c(d):"?"==h?c(b,k(":"),e):c(e):"quasi"==a?m(ba,d):";"!=a?"("==a?N(g,")","call",d):"."==a?c(S,d):"["==a?c(v("]"),W,k("]"),p,d):K&&"as"==h?(r.marked="keyword",c(G,d)):void 0:void 0}function ba(a,h){return"quasi"!=a?m():"${"!=h.slice(h.length-2)?c(ba):c(b,Ba)}function Ba(a){if("}"==a)return r.marked="string-2",r.state.tokenize=u,c(ba)}function sa(q){return n(r.stream,r.state),m("{"==q?a:b)}function ta(b){return n(r.stream,r.state),m("{"==b?a:g)}function Ca(a){return function(h){return"."==
+h?c(a?oa:ca):m(a?g:b)}}function ca(a,b){if("target"==b)return r.marked="keyword",c(I)}function oa(a,b){if("target"==b)return r.marked="keyword",c(E)}function ma(b){return":"==b?c(p,a):m(I,k(";"),p)}function S(a){if("variable"==a)return r.marked="property",c()}function aa(a,h){return"async"==a?(r.marked="property",c(aa)):"variable"==a||"keyword"==r.style?(r.marked="property",c("get"==h||"set"==h?pa:O)):"number"==a||"string"==a?(r.marked=fa?"property":r.style+" property",c(O)):"jsonld-keyword"==a?c(O):
+"modifier"==a?c(aa):"["==a?c(b,k("]"),O):"spread"==a?c(b,O):":"==a?m(O):void 0}function pa(a){return"variable"!=a?m(O):(r.marked="property",c(M))}function O(a){return":"==a?c(g):"("==a?m(M):void 0}function J(a,b,h){function g(l,d){if(h?-1<h.indexOf(l):","==l){var e=r.state.lexical;return"call"==e.info&&(e.pos=(e.pos||0)+1),c(function(c,h){return c==b||h==b?m():m(a)},g)}return l==b||d==b?c():c(k(b))}return function(h,l){return h==b||l==b?c():m(a,g)}}function N(a,b,h){for(var g=3;g<arguments.length;g++)r.cc.push(arguments[g]);
+return c(v(b,h),J(a,b),p)}function R(b){return"}"==b?c():m(a,R)}function P(a,b){if(K){if(":"==a)return c(G);if("?"==b)return c(P)}}function G(a){return"variable"==a?(r.marked="type",c(U)):"string"==a||"number"==a||"atom"==a?c(U):"{"==a?c(v("}"),J(L,"}",",;"),p,U):"("==a?c(J(ga,")"),ha):void 0}function ha(a){if("\x3d\x3e"==a)return c(G)}function L(a,h){return"variable"==a||"keyword"==r.style?(r.marked="property",c(L)):"?"==h?c(L):":"==a?c(G):"["==a?c(b,P,k("]"),L):void 0}function ga(a){return"variable"==
+a?c(ga):":"==a?c(G):void 0}function U(a,b){return"\x3c"==b?c(v("\x3e"),J(G,"\x3e"),p,U):"|"==b||"."==a?c(G):"["==a?c(k("]"),U):"extends"==b?c(G):void 0}function B(){return m(h,P,l,Ma)}function h(a,b){return"modifier"==a?c(h):"variable"==a?(D(b),c()):"spread"==a?c(h):"["==a?N(h,"]"):"{"==a?N(Na,"}"):void 0}function Na(a,b){return"variable"!=a||r.stream.match(/^\s*:/,!1)?("variable"==a&&(r.marked="property"),"spread"==a?c(h):"}"==a?m():c(k(":"),h,l)):(D(b),c(l))}function l(a,b){if("\x3d"==b)return c(g)}
+function Ma(a){if(","==a)return c(B)}function va(b,h){if("keyword b"==b&&"else"==h)return c(v("form","else"),a,p)}function Ga(a){if("("==a)return c(v(")"),Oa,k(")"),p)}function Oa(a){return"var"==a?c(B,k(";"),ia):";"==a?c(ia):"variable"==a?c(Pa):m(b,k(";"),ia)}function Pa(a,h){return"in"==h||"of"==h?(r.marked="keyword",c(b)):c(I,ia)}function ia(a,h){return";"==a?c(xa):"in"==h||"of"==h?(r.marked="keyword",c(b)):m(b,k(";"),xa)}function xa(a){")"!=a&&c(b)}function M(b,h){return"*"==h?(r.marked="keyword",
+c(M)):"variable"==b?(D(h),c(M)):"("==b?c(z,v(")"),J(na,")"),p,P,a,H):K&&"\x3c"==h?c(v("\x3e"),J(G,"\x3e"),p,M):void 0}function na(a){return"spread"==a?c(na):m(h,P,l)}function Ka(a,b){return"variable"==a?wa(a,b):ja(a,b)}function wa(a,b){if("variable"==a)return D(b),c(ja)}function ja(a,h){return"\x3c"==h?c(v("\x3e"),J(G,"\x3e"),p,ja):"extends"==h||"implements"==h||K&&","==a?c(K?G:b,ja):"{"==a?c(v("}"),V,p):void 0}function V(a,h){return"variable"==a||"keyword"==r.style?("async"==h||"static"==h||"get"==
+h||"set"==h||K&&("public"==h||"private"==h||"protected"==h||"readonly"==h||"abstract"==h))&&r.stream.match(/^\s+[\w$\xa1-\uffff]/,!1)?(r.marked="keyword",c(V)):(r.marked="property",c(K?qa:M,V)):"["==a?c(b,k("]"),K?qa:M,V):"*"==h?(r.marked="keyword",c(V)):";"==a?c(V):"}"==a?c():"@"==h?c(b,V):void 0}function qa(a,b){return"?"==b?c(qa):":"==a?c(G,l):"\x3d"==b?c(g):m(M)}function Ha(h,g){return"*"==g?(r.marked="keyword",c(ra,k(";"))):"default"==g?(r.marked="keyword",c(b,k(";"))):"{"==h?c(J(ya,"}"),ra,
+k(";")):m(a)}function ya(a,b){return"as"==b?(r.marked="keyword",c(k("variable"))):"variable"==a?m(g,ya):void 0}function Ia(a){return"string"==a?c():m(ka,za,ra)}function ka(a,b){return"{"==a?N(ka,"}"):("variable"==a&&D(b),"*"==b&&(r.marked="keyword"),c(Qa))}function za(a){if(","==a)return c(ka,za)}function Qa(a,b){if("as"==b)return r.marked="keyword",c(ka)}function ra(a,h){if("from"==h)return r.marked="keyword",c(b)}function La(a){return"]"==a?c():m(J(g,"]"))}var Z,da,X=C.indentUnit,Aa=w.statementIndent,
+fa=w.jsonld,Y=w.json||fa,K=w.typescript,la=w.wordCharacters||/[\w$\xa1-\uffff]/,ua=function(){function a(b){return{type:b,style:"keyword"}}var b=a("keyword a"),h=a("keyword b"),c=a("keyword c"),g=a("operator"),l={type:"atom",style:"atom"},b={"if":a("if"),"while":b,"with":b,"else":h,"do":h,"try":h,"finally":h,"return":c,"break":c,"continue":c,"new":a("new"),"delete":c,"throw":c,"debugger":c,"var":a("var"),"const":a("var"),let:a("var"),"function":a("function"),"catch":a("catch"),"for":a("for"),"switch":a("switch"),
+"case":a("case"),"default":a("default"),"in":g,"typeof":g,"instanceof":g,"true":l,"false":l,"null":l,undefined:l,NaN:l,Infinity:l,"this":a("this"),"class":a("class"),"super":a("atom"),yield:c,"export":a("export"),"import":a("import"),"extends":c,await:c};if(K){var h={type:"variable",style:"type"},c={"interface":a("class"),"implements":c,namespace:c,module:a("module"),"enum":a("module"),"public":a("modifier"),"private":a("modifier"),"protected":a("modifier"),"abstract":a("modifier"),string:h,number:h,
+"boolean":h,any:h},d;for(d in c)b[d]=c[d]}return b}(),ea=/[+\-*&%=<>!?|~^@]/,Da=/^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/,Ea="([{}])",Ja={atom:!0,number:!0,variable:!0,string:!0,regexp:!0,"this":!0,"jsonld-keyword":!0},r={state:null,column:null,marked:null,cc:null},Fa={name:"this",next:{name:"arguments"}};return p.lex=!0,{startState:function(a){a={tokenize:f,lastType:"sof",cc:[],lexical:new y((a||0)-X,0,"block",!1),localVars:w.localVars,context:w.localVars&&
+{vars:w.localVars},indented:a||0};return w.globalVars&&"object"==typeof w.globalVars&&(a.globalVars=w.globalVars),a},token:function(h,c){if(h.sol()&&(c.lexical.hasOwnProperty("align")||(c.lexical.align=!1),c.indented=h.indentation(),n(h,c)),c.tokenize!=t&&h.eatSpace())return null;var g=c.tokenize(h,c);if("comment"!=Z){c.lastType="operator"!=Z||"++"!=da&&"--"!=da?Z:"incdec";a:{var l=Z,d=da,e=c.cc;r.state=c;r.stream=h;r.marked=null;r.cc=e;r.style=g;for(c.lexical.hasOwnProperty("align")||(c.lexical.align=
+!0);;)if((e.length?e.pop():Y?b:a)(l,d)){for(;e.length&&e[e.length-1].lex;)e.pop()();if(r.marked)g=r.marked;else{if(l="variable"==l)b:{for(l=c.localVars;l;l=l.next)if(l.name==d){l=!0;break b}for(e=c.context;e;e=e.prev)for(l=e.vars;l;l=l.next)if(l.name==d){l=!0;break b}l=void 0}g=l?"variable-2":g}break a}}}return g},indent:function(a,b){if(a.tokenize==t)return e.Pass;if(a.tokenize!=f)return 0;var h,c=b&&b.charAt(0),l=a.lexical;if(!/^\s*else\b/.test(b))for(var g=a.cc.length-1;0<=g;--g){var d=a.cc[g];
+if(d==p)l=l.prev;else if(d!=va)break}for(;!("stat"!=l.type&&"form"!=l.type||"}"!=c&&(!(h=a.cc[a.cc.length-1])||h!=I&&h!=E||/^[,\.=+\-*:?[\(]/.test(b)));)l=l.prev;Aa&&")"==l.type&&"stat"==l.prev.type&&(l=l.prev);h=l.type;g=c==h;"vardef"==h?c=l.indented+("operator"==a.lastType||","==a.lastType?l.info+1:0):"form"==h&&"{"==c?c=l.indented:"form"==h?c=l.indented+X:"stat"==h?(c=l.indented,l="operator"==a.lastType||","==a.lastType||ea.test(b.charAt(0))||/[,.]/.test(b.charAt(0)),c+=l?Aa||X:0):c="switch"!=
+l.info||g||0==w.doubleIndentSwitch?l.align?l.column+(g?0:1):l.indented+(g?0:X):l.indented+(/^(?:case|default)\b/.test(b)?X:2*X);return c},electricInput:/^\s*(?:case .*?:|default:|\{|\})$/,blockCommentStart:Y?null:"/*",blockCommentEnd:Y?null:"*/",lineComment:Y?null:"//",fold:"brace",closeBrackets:"()[]{}''\"\"``",helperType:Y?"json":"javascript",jsonldMode:fa,jsonMode:Y,expressionAllowed:A,skipExpression:function(a){var h=a.cc[a.cc.length-1];h!=b&&h!=g||a.cc.pop()}}});e.registerHelper("wordChars",
+"javascript",/[\w$]/);e.defineMIME("text/javascript","javascript");e.defineMIME("text/ecmascript","javascript");e.defineMIME("application/javascript","javascript");e.defineMIME("application/x-javascript","javascript");e.defineMIME("application/ecmascript","javascript");e.defineMIME("application/json",{name:"javascript",json:!0});e.defineMIME("application/x-json",{name:"javascript",json:!0});e.defineMIME("application/ld+json",{name:"javascript",jsonld:!0});e.defineMIME("text/typescript",{name:"javascript",
+typescript:!0});e.defineMIME("application/typescript",{name:"javascript",typescript:!0})});
+(function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("mode/css/css",["../../lib/codemirror"],e):e(CodeMirror)})(function(e){function A(a){for(var b={},c=0;c<a.length;++c)b[a[c].toLowerCase()]=!0;return b}function C(a,b){for(var c,d=!1;null!=(c=a.next());){if(d&&"/"==c){b.tokenize=null;break}d="*"==c}return["comment","comment"]}e.defineMode("css",function(a,b){function c(a,b){var l=a.next();if(v[l]){var g=v[l](a,
+b);if(!1!==g)return g}"@"==l?(a.eatWhile(/[\w\\\-]/),l=(p=a.current(),"def")):l="\x3d"==l||("~"==l||"|"==l)&&a.eat("\x3d")?(p="compare",null):'"'==l||"'"==l?(b.tokenize=d(l),b.tokenize(a,b)):"#"==l?(a.eatWhile(/[\w\\\-]/),p="hash","atom"):"!"==l?(a.match(/^\s*\w*/),p="important","keyword"):/\d/.test(l)||"."==l&&a.eat(/\d/)?(a.eatWhile(/[\w.%]/),p="unit","number"):"-"!==l?/[,+>*\/]/.test(l)?(p="select-op",null):"."==l&&a.match(/^-?[_a-z][_a-z0-9-]*/i)?(p="qualifier","qualifier"):/[:;{}\[\]\(\)]/.test(l)?
+(p=l,null):"u"==l&&a.match(/rl(-prefix)?\(/)||"d"==l&&a.match("omain(")||"r"==l&&a.match("egexp(")?(a.backUp(1),b.tokenize=y,p="word","property"):/[\w\\\-]/.test(l)?(a.eatWhile(/[\w\\\-]/),p="word","property"):(p=null,null):/[\d.]/.test(a.peek())?(a.eatWhile(/[\w.%]/),p="unit","number"):a.match(/^-[\w\\\-]+/)?(a.eatWhile(/[\w\\\-]/),a.match(/^\s*:/,!1)?(p="variable-definition","variable-2"):(p="variable","variable-2")):a.match(/^\w+-/)?(p="meta","meta"):void 0;return l}function d(a){return function(b,
+c){for(var g,d=!1;null!=(g=b.next());){if(g==a&&!d){")"==a&&b.backUp(1);break}d=!d&&"\\"==g}return(g==a||!d&&")"!=a)&&(c.tokenize=null),p="string","string"}}function y(a,b){return a.next(),a.match(/\s*[\"\')]/,!1)?b.tokenize=null:b.tokenize=d(")"),p="(",null}function m(a,b,c){this.type=a;this.indent=b;this.prev=c}function n(a,b,c,g){return a.context=new m(c,b.indentation()+(!1===g?0:D),a.context),c}function t(a){return a.context.prev&&(a.context=a.context.prev),a.context.type}function f(a,b,c,g){for(g=
+g||1;0<g;g--)c.context=c.context.prev;return B[c.context.type](a,b,c)}function u(a){a=a.current().toLowerCase();k=ha.hasOwnProperty(a)?"atom":G.hasOwnProperty(a)?"keyword":"variable"}var z=b.inline;b.propertyKeywords||(b=e.resolveMode("text/css"));var p,k,D=a.indentUnit,v=b.tokenHooks,w=b.documentTypes||{},x=b.mediaTypes||{},A=b.mediaFeatures||{},H=b.mediaValueKeywords||{},C=b.propertyKeywords||{},N=b.nonStandardPropertyKeywords||{},R=b.fontProperties||{},P=b.counterDescriptors||{},G=b.colorKeywords||
+{},ha=b.valueKeywords||{},L=b.allowNested,ga=b.lineComment,U=!0===b.supportsAtComponent,B={};return B.top=function(a,b,c){if("{"==a)return n(c,b,"block");if("}"==a&&c.context.prev)return t(c);if(U&&/@component/.test(a))return n(c,b,"atComponentBlock");if(/^@(-moz-)?document$/.test(a))return n(c,b,"documentTypes");if(/^@(media|supports|(-moz-)?document|import)$/.test(a))return n(c,b,"atBlock");if(/^@(font-face|counter-style)/.test(a))return c.stateArg=a,"restricted_atBlock_before";if(/^@(-(moz|ms|o|webkit)-)?keyframes$/.test(a))return"keyframes";
+if(a&&"@"==a.charAt(0))return n(c,b,"at");if("hash"==a)k="builtin";else if("word"==a)k="tag";else{if("variable-definition"==a)return"maybeprop";if("interpolation"==a)return n(c,b,"interpolation");if(":"==a)return"pseudo";if(L&&"("==a)return n(c,b,"parens")}return c.context.type},B.block=function(a,b,c){return"word"==a?(a=b.current().toLowerCase(),C.hasOwnProperty(a)?(k="property","maybeprop"):N.hasOwnProperty(a)?(k="string-2","maybeprop"):L?(k=b.match(/^\s*:(?:\s|$)/,!1)?"property":"tag","block"):
+(k+=" error","maybeprop")):"meta"==a?"block":L||"hash"!=a&&"qualifier"!=a?B.top(a,b,c):(k="error","block")},B.maybeprop=function(a,b,c){return":"==a?n(c,b,"prop"):B[c.context.type](a,b,c)},B.prop=function(a,b,c){if(";"==a)return t(c);if("{"==a&&L)return n(c,b,"propBlock");if("}"==a||"{"==a)return f(a,b,c);if("("==a)return n(c,b,"parens");if("hash"!=a||/^#([0-9a-fA-f]{3,4}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(b.current()))if("word"==a)u(b);else{if("interpolation"==a)return n(c,b,"interpolation")}else k+=
+" error";return"prop"},B.propBlock=function(a,b,c){return"}"==a?t(c):"word"==a?(k="property","maybeprop"):c.context.type},B.parens=function(a,b,c){return"{"==a||"}"==a?f(a,b,c):")"==a?t(c):"("==a?n(c,b,"parens"):"interpolation"==a?n(c,b,"interpolation"):("word"==a&&u(b),"parens")},B.pseudo=function(a,b,c){return"meta"==a?"pseudo":"word"==a?(k="variable-3",c.context.type):B[c.context.type](a,b,c)},B.documentTypes=function(a,b,c){return"word"==a&&w.hasOwnProperty(b.current())?(k="tag",c.context.type):
+B.atBlock(a,b,c)},B.atBlock=function(a,b,c){if("("==a)return n(c,b,"atBlock_parens");if("}"==a||";"==a)return f(a,b,c);if("{"==a)return t(c)&&n(c,b,L?"block":"top");if("interpolation"==a)return n(c,b,"interpolation");"word"==a&&(a=b.current().toLowerCase(),k="only"==a||"not"==a||"and"==a||"or"==a?"keyword":x.hasOwnProperty(a)?"attribute":A.hasOwnProperty(a)?"property":H.hasOwnProperty(a)?"keyword":C.hasOwnProperty(a)?"property":N.hasOwnProperty(a)?"string-2":ha.hasOwnProperty(a)?"atom":G.hasOwnProperty(a)?
+"keyword":"error");return c.context.type},B.atComponentBlock=function(a,b,c){return"}"==a?f(a,b,c):"{"==a?t(c)&&n(c,b,L?"block":"top",!1):("word"==a&&(k="error"),c.context.type)},B.atBlock_parens=function(a,b,c){return")"==a?t(c):"{"==a||"}"==a?f(a,b,c,2):B.atBlock(a,b,c)},B.restricted_atBlock_before=function(a,b,c){return"{"==a?n(c,b,"restricted_atBlock"):"word"==a&&"@counter-style"==c.stateArg?(k="variable","restricted_atBlock_before"):B[c.context.type](a,b,c)},B.restricted_atBlock=function(a,b,
+c){return"}"==a?(c.stateArg=null,t(c)):"word"==a?(k="@font-face"==c.stateArg&&!R.hasOwnProperty(b.current().toLowerCase())||"@counter-style"==c.stateArg&&!P.hasOwnProperty(b.current().toLowerCase())?"error":"property","maybeprop"):"restricted_atBlock"},B.keyframes=function(a,b,c){return"word"==a?(k="variable","keyframes"):"{"==a?n(c,b,"top"):B[c.context.type](a,b,c)},B.at=function(a,b,c){return";"==a?t(c):"{"==a||"}"==a?f(a,b,c):("word"==a?k="tag":"hash"==a&&(k="builtin"),"at")},B.interpolation=function(a,
+b,c){return"}"==a?t(c):"{"==a||";"==a?f(a,b,c):("word"==a?k="variable":"variable"!=a&&"("!=a&&")"!=a&&(k="error"),"interpolation")},{startState:function(a){return{tokenize:null,state:z?"block":"top",stateArg:null,context:new m(z?"block":"top",a||0,null)}},token:function(a,b){if(!b.tokenize&&a.eatSpace())return null;var g=(b.tokenize||c)(a,b);return g&&"object"==typeof g&&(p=g[1],g=g[0]),k=g,b.state=B[b.state](p,a,b),k},indent:function(a,b){var c=a.context,g=b&&b.charAt(0),d=c.indent;return"prop"!=
+c.type||"}"!=g&&")"!=g||(c=c.prev),c.prev&&("}"!=g||"block"!=c.type&&"top"!=c.type&&"interpolation"!=c.type&&"restricted_atBlock"!=c.type?(")"!=g||"parens"!=c.type&&"atBlock_parens"!=c.type)&&("{"!=g||"at"!=c.type&&"atBlock"!=c.type)||(d=Math.max(0,c.indent-D)):(c=c.prev,d=c.indent)),d},electricChars:"}",blockCommentStart:"/*",blockCommentEnd:"*/",lineComment:ga,fold:"brace"}});var w=["domain","regexp","url","url-prefix"],x=A(w),f="all aural braille handheld print projection screen tty tv embossed".split(" "),
+d=A(f),t="width min-width max-width height min-height max-height device-width min-device-width max-device-width device-height min-device-height max-device-height aspect-ratio min-aspect-ratio max-aspect-ratio device-aspect-ratio min-device-aspect-ratio max-device-aspect-ratio color min-color max-color color-index min-color-index max-color-index monochrome min-monochrome max-monochrome resolution min-resolution max-resolution scan grid orientation device-pixel-ratio min-device-pixel-ratio max-device-pixel-ratio pointer any-pointer hover any-hover".split(" "),
+u=A(t),n="landscape portrait none coarse fine on-demand hover interlace progressive".split(" "),y=A(n),m="align-content align-items align-self alignment-adjust alignment-baseline anchor-point animation animation-delay animation-direction animation-duration animation-fill-mode animation-iteration-count animation-name animation-play-state animation-timing-function appearance azimuth backface-visibility background background-attachment background-blend-mode background-clip background-color background-image background-origin background-position background-repeat background-size baseline-shift binding bleed bookmark-label bookmark-level bookmark-state bookmark-target border border-bottom border-bottom-color border-bottom-left-radius border-bottom-right-radius border-bottom-style border-bottom-width border-collapse border-color border-image border-image-outset border-image-repeat border-image-slice border-image-source border-image-width border-left border-left-color border-left-style border-left-width border-radius border-right border-right-color border-right-style border-right-width border-spacing border-style border-top border-top-color border-top-left-radius border-top-right-radius border-top-style border-top-width border-width bottom box-decoration-break box-shadow box-sizing break-after break-before break-inside caption-side caret-color clear clip color color-profile column-count column-fill column-gap column-rule column-rule-color column-rule-style column-rule-width column-span column-width columns content counter-increment counter-reset crop cue cue-after cue-before cursor direction display dominant-baseline drop-initial-after-adjust drop-initial-after-align drop-initial-before-adjust drop-initial-before-align drop-initial-size drop-initial-value elevation empty-cells fit fit-position flex flex-basis flex-direction flex-flow flex-grow flex-shrink flex-wrap float float-offset flow-from flow-into font font-feature-settings font-family font-kerning font-language-override font-size font-size-adjust font-stretch font-style font-synthesis font-variant font-variant-alternates font-variant-caps font-variant-east-asian font-variant-ligatures font-variant-numeric font-variant-position font-weight grid grid-area grid-auto-columns grid-auto-flow grid-auto-rows grid-column grid-column-end grid-column-gap grid-column-start grid-gap grid-row grid-row-end grid-row-gap grid-row-start grid-template grid-template-areas grid-template-columns grid-template-rows hanging-punctuation height hyphens icon image-orientation image-rendering image-resolution inline-box-align justify-content justify-items justify-self left letter-spacing line-break line-height line-stacking line-stacking-ruby line-stacking-shift line-stacking-strategy list-style list-style-image list-style-position list-style-type margin margin-bottom margin-left margin-right margin-top marks marquee-direction marquee-loop marquee-play-count marquee-speed marquee-style max-height max-width min-height min-width move-to nav-down nav-index nav-left nav-right nav-up object-fit object-position opacity order orphans outline outline-color outline-offset outline-style outline-width overflow overflow-style overflow-wrap overflow-x overflow-y padding padding-bottom padding-left padding-right padding-top page page-break-after page-break-before page-break-inside page-policy pause pause-after pause-before perspective perspective-origin pitch pitch-range place-content place-items place-self play-during position presentation-level punctuation-trim quotes region-break-after region-break-before region-break-inside region-fragment rendering-intent resize rest rest-after rest-before richness right rotation rotation-point ruby-align ruby-overhang ruby-position ruby-span shape-image-threshold shape-inside shape-margin shape-outside size speak speak-as speak-header speak-numeral speak-punctuation speech-rate stress string-set tab-size table-layout target target-name target-new target-position text-align text-align-last text-decoration text-decoration-color text-decoration-line text-decoration-skip text-decoration-style text-emphasis text-emphasis-color text-emphasis-position text-emphasis-style text-height text-indent text-justify text-outline text-overflow text-shadow text-size-adjust text-space-collapse text-transform text-underline-position text-wrap top transform transform-origin transform-style transition transition-delay transition-duration transition-property transition-timing-function unicode-bidi user-select vertical-align visibility voice-balance voice-duration voice-family voice-pitch voice-range voice-rate voice-stress voice-volume volume white-space widows width will-change word-break word-spacing word-wrap z-index clip-path clip-rule mask enable-background filter flood-color flood-opacity lighting-color stop-color stop-opacity pointer-events color-interpolation color-interpolation-filters color-rendering fill fill-opacity fill-rule image-rendering marker marker-end marker-mid marker-start shape-rendering stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-rendering baseline-shift dominant-baseline glyph-orientation-horizontal glyph-orientation-vertical text-anchor writing-mode".split(" "),
+c=A(m),D="scrollbar-arrow-color scrollbar-base-color scrollbar-dark-shadow-color scrollbar-face-color scrollbar-highlight-color scrollbar-shadow-color scrollbar-3d-light-color scrollbar-track-color shape-inside searchfield-cancel-button searchfield-decoration searchfield-results-button searchfield-results-decoration zoom".split(" "),z=A(D),H=A("font-family src unicode-range font-variant font-feature-settings font-stretch font-weight font-style".split(" ")),v=A("additive-symbols fallback negative pad prefix range speak-as suffix symbols system".split(" ")),
+p="aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray grey green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen".split(" "),
+k=A(p),a="above absolute activeborder additive activecaption afar after-white-space ahead alias all all-scroll alphabetic alternate always amharic amharic-abegede antialiased appworkspace arabic-indic armenian asterisks attr auto auto-flow avoid avoid-column avoid-page avoid-region background backwards baseline below bidi-override binary bengali blink block block-axis bold bolder border border-box both bottom break break-all break-word bullets button button-bevel buttonface buttonhighlight buttonshadow buttontext calc cambodian capitalize caps-lock-indicator caption captiontext caret cell center checkbox circle cjk-decimal cjk-earthly-branch cjk-heavenly-stem cjk-ideographic clear clip close-quote col-resize collapse color color-burn color-dodge column column-reverse compact condensed contain content contents content-box context-menu continuous copy counter counters cover crop cross crosshair currentcolor cursive cyclic darken dashed decimal decimal-leading-zero default default-button dense destination-atop destination-in destination-out destination-over devanagari difference disc discard disclosure-closed disclosure-open document dot-dash dot-dot-dash dotted double down e-resize ease ease-in ease-in-out ease-out element ellipse ellipsis embed end ethiopic ethiopic-abegede ethiopic-abegede-am-et ethiopic-abegede-gez ethiopic-abegede-ti-er ethiopic-abegede-ti-et ethiopic-halehame-aa-er ethiopic-halehame-aa-et ethiopic-halehame-am-et ethiopic-halehame-gez ethiopic-halehame-om-et ethiopic-halehame-sid-et ethiopic-halehame-so-et ethiopic-halehame-ti-er ethiopic-halehame-ti-et ethiopic-halehame-tig ethiopic-numeric ew-resize exclusion expanded extends extra-condensed extra-expanded fantasy fast fill fixed flat flex flex-end flex-start footnotes forwards from geometricPrecision georgian graytext grid groove gujarati gurmukhi hand hangul hangul-consonant hard-light hebrew help hidden hide higher highlight highlighttext hiragana hiragana-iroha horizontal hsl hsla hue icon ignore inactiveborder inactivecaption inactivecaptiontext infinite infobackground infotext inherit initial inline inline-axis inline-block inline-flex inline-grid inline-table inset inside intrinsic invert italic japanese-formal japanese-informal justify kannada katakana katakana-iroha keep-all khmer korean-hangul-formal korean-hanja-formal korean-hanja-informal landscape lao large larger left level lighter lighten line-through linear linear-gradient lines list-item listbox listitem local logical loud lower lower-alpha lower-armenian lower-greek lower-hexadecimal lower-latin lower-norwegian lower-roman lowercase ltr luminosity malayalam match matrix matrix3d media-controls-background media-current-time-display media-fullscreen-button media-mute-button media-play-button media-return-to-realtime-button media-rewind-button media-seek-back-button media-seek-forward-button media-slider media-sliderthumb media-time-remaining-display media-volume-slider media-volume-slider-container media-volume-sliderthumb medium menu menulist menulist-button menulist-text menulist-textfield menutext message-box middle min-intrinsic mix mongolian monospace move multiple multiply myanmar n-resize narrower ne-resize nesw-resize no-close-quote no-drop no-open-quote no-repeat none normal not-allowed nowrap ns-resize numbers numeric nw-resize nwse-resize oblique octal opacity open-quote optimizeLegibility optimizeSpeed oriya oromo outset outside outside-shape overlay overline padding padding-box painted page paused persian perspective plus-darker plus-lighter pointer polygon portrait pre pre-line pre-wrap preserve-3d progress push-button radial-gradient radio read-only read-write read-write-plaintext-only rectangle region relative repeat repeating-linear-gradient repeating-radial-gradient repeat-x repeat-y reset reverse rgb rgba ridge right rotate rotate3d rotateX rotateY rotateZ round row row-resize row-reverse rtl run-in running s-resize sans-serif saturation scale scale3d scaleX scaleY scaleZ screen scroll scrollbar scroll-position se-resize searchfield searchfield-cancel-button searchfield-decoration searchfield-results-button searchfield-results-decoration self-start self-end semi-condensed semi-expanded separate serif show sidama simp-chinese-formal simp-chinese-informal single skew skewX skewY skip-white-space slide slider-horizontal slider-vertical sliderthumb-horizontal sliderthumb-vertical slow small small-caps small-caption smaller soft-light solid somali source-atop source-in source-out source-over space space-around space-between space-evenly spell-out square square-button start static status-bar stretch stroke sub subpixel-antialiased super sw-resize symbolic symbols system-ui table table-caption table-cell table-column table-column-group table-footer-group table-header-group table-row table-row-group tamil telugu text text-bottom text-top textarea textfield thai thick thin threeddarkshadow threedface threedhighlight threedlightshadow threedshadow tibetan tigre tigrinya-er tigrinya-er-abegede tigrinya-et tigrinya-et-abegede to top trad-chinese-formal trad-chinese-informal transform translate translate3d translateX translateY translateZ transparent ultra-condensed ultra-expanded underline unset up upper-alpha upper-armenian upper-greek upper-hexadecimal upper-latin upper-norwegian upper-roman uppercase urdu url var vertical vertical-text visible visibleFill visiblePainted visibleStroke visual w-resize wait wave wider window windowframe windowtext words wrap wrap-reverse x-large x-small xor xx-large xx-small".split(" "),
+b=A(a),w=w.concat(f).concat(t).concat(n).concat(m).concat(D).concat(p).concat(a);e.registerHelper("hintWords","css",w);e.defineMIME("text/css",{documentTypes:x,mediaTypes:d,mediaFeatures:u,mediaValueKeywords:y,propertyKeywords:c,nonStandardPropertyKeywords:z,fontProperties:H,counterDescriptors:v,colorKeywords:k,valueKeywords:b,tokenHooks:{"/":function(a,b){return!!a.eat("*")&&(b.tokenize=C,C(a,b))}},name:"css"});e.defineMIME("text/x-scss",{mediaTypes:d,mediaFeatures:u,mediaValueKeywords:y,propertyKeywords:c,
+nonStandardPropertyKeywords:z,colorKeywords:k,valueKeywords:b,fontProperties:H,allowNested:!0,lineComment:"//",tokenHooks:{"/":function(a,b){return a.eat("/")?(a.skipToEnd(),["comment","comment"]):a.eat("*")?(b.tokenize=C,C(a,b)):["operator","operator"]},":":function(a){return!!a.match(/\s*\{/,!1)&&[null,null]},$:function(a){return a.match(/^[\w-]+/),a.match(/^\s*:/,!1)?["variable-2","variable-definition"]:["variable-2","variable"]},"#":function(a){return!!a.eat("{")&&[null,"interpolation"]}},name:"css",
+helperType:"scss"});e.defineMIME("text/x-less",{mediaTypes:d,mediaFeatures:u,mediaValueKeywords:y,propertyKeywords:c,nonStandardPropertyKeywords:z,colorKeywords:k,valueKeywords:b,fontProperties:H,allowNested:!0,lineComment:"//",tokenHooks:{"/":function(a,b){return a.eat("/")?(a.skipToEnd(),["comment","comment"]):a.eat("*")?(b.tokenize=C,C(a,b)):["operator","operator"]},"@":function(a){return a.eat("{")?[null,"interpolation"]:!a.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/,
+!1)&&(a.eatWhile(/[\w\\\-]/),a.match(/^\s*:/,!1)?["variable-2","variable-definition"]:["variable-2","variable"])},"\x26":function(){return["atom","atom"]}},name:"css",helperType:"less"});e.defineMIME("text/x-gss",{documentTypes:x,mediaTypes:d,mediaFeatures:u,propertyKeywords:c,nonStandardPropertyKeywords:z,fontProperties:H,counterDescriptors:v,colorKeywords:k,valueKeywords:b,supportsAtComponent:!0,tokenHooks:{"/":function(a,b){return!!a.eat("*")&&(b.tokenize=C,C(a,b))}},name:"css",helperType:"gss"})});
+(function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror"),require("../xml/xml"),require("../javascript/javascript"),require("../css/css")):"function"==typeof define&&define.amd?define("mode/htmlmixed/htmlmixed",["../../lib/codemirror","../xml/xml","../javascript/javascript","../css/css"],e):e(CodeMirror)})(function(e){function A(d,e){var u=d.match(f[e]||(f[e]=new RegExp("\\s+"+e+"\\s*\x3d\\s*('|\")?([^'\"]+)('|\")?\\s*")));return u?/^\s*(.*?)\s*$/.exec(u[2])[1]:
+""}function C(d,e){for(var f in d)for(var n=e[f]||(e[f]=[]),y=d[f],m=y.length-1;0<=m;m--)n.unshift(y[m])}function w(d,e){for(var f=0;f<d.length;f++){var n=d[f];if(!n[0]||n[1].test(A(e,n[0])))return n[2]}}var x={script:[["lang",/(javascript|babel)/i,"javascript"],["type",/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i,"javascript"],["type",/./,"text/plain"],[null,null,"javascript"]],style:[["lang",/^css$/i,"css"],["type",/^(text\/)?(x-)?(stylesheet|css)$/i,"css"],["type",/./,"text/plain"],
+[null,null,"css"]]},f={};e.defineMode("htmlmixed",function(d,f){function u(c,f){var m,t=n.token(c,f.htmlState),p=/\btag\b/.test(t);if(p&&!/[<>\s\/]/.test(c.current())&&(m=f.htmlState.tagName&&f.htmlState.tagName.toLowerCase())&&y.hasOwnProperty(m))f.inTag=m+" ";else if(f.inTag&&p&&/>$/.test(c.current())){m=/^([\S]+) (.*)/.exec(f.inTag);f.inTag=null;var p="\x3e"==c.current()&&w(y[m[1]],m[2]),p=e.getMode(d,p),k=new RegExp("^\x3c/s*"+m[1]+"s*\x3e","i"),a=new RegExp("\x3c/s*"+m[1]+"s*\x3e","i");f.token=
+function(b,c){var d;if(b.match(k,!1))d=(c.token=u,c.localState=c.localMode=null,null);else{d=c.localMode.token(b,c.localState);var e=b.current(),f=e.search(a);d=(-1<f?b.backUp(e.length-f):e.match(/<\/?$/)&&(b.backUp(e.length),b.match(a,!1)||b.match(e)),d)}return d};f.localMode=p;f.localState=e.startState(p,n.indent(f.htmlState,""))}else f.inTag&&(f.inTag+=c.current(),c.eol()&&(f.inTag+=" "));return t}var n=e.getMode(d,{name:"xml",htmlMode:!0,multilineTagIndentFactor:f.multilineTagIndentFactor,multilineTagIndentPastTag:f.multilineTagIndentPastTag}),
+y={},m=f&&f.tags,c=f&&f.scriptTypes;if(C(x,y),m&&C(m,y),c)for(m=c.length-1;0<=m;m--)y.script.unshift(["type",c[m].matches,c[m].mode]);return{startState:function(){return{token:u,inTag:null,localMode:null,localState:null,htmlState:e.startState(n)}},copyState:function(c){var d;return c.localState&&(d=e.copyState(c.localMode,c.localState)),{token:c.token,inTag:c.inTag,localMode:c.localMode,localState:d,htmlState:e.copyState(n,c.htmlState)}},token:function(c,d){return d.token(c,d)},indent:function(c,
+d,f){return!c.localMode||/^\s*<\//.test(d)?n.indent(c.htmlState,d):c.localMode.indent?c.localMode.indent(c.localState,d,f):e.Pass},innerMode:function(c){return{state:c.localState||c.htmlState,mode:c.localMode||n}}}},"xml","javascript","css");e.defineMIME("text/html","htmlmixed")});
+(function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("mode/clike/clike",["../../lib/codemirror"],e):e(CodeMirror)})(function(e){function A(a,b,c,d,e,f){this.indented=a;this.column=b;this.type=c;this.info=d;this.align=e;this.prev=f}function C(a,b,c,d){var e=a.indented;return a.context&&"statement"==a.context.type&&"statement"!=c&&(e=a.context.indented),a.context=new A(e,b,c,d,null,a.context)}function w(a){var b=
+a.context.type;return")"!=b&&"]"!=b&&"}"!=b||(a.indented=a.context.indented),a.context=a.context.prev}function x(a,b,c){return"variable"==b.prevToken||"type"==b.prevToken||!!/\S(?:[^- ]>|[*\]])\s*$|\*$/.test(a.string.slice(0,c))||!(!b.typeAtEndOfLine||a.column()!=a.indentation())||void 0}function f(a){for(;;){if(!a||"top"==a.type)return!0;if("}"==a.type&&"namespace"!=a.prev.info)return!1;a=a.prev}}function d(a){var b={};a=a.split(" ");for(var c=0;c<a.length;++c)b[a[c]]=!0;return b}function t(a,b){return"function"==
+typeof a?a(b):a.propertyIsEnumerable(b)}function u(a,b){if(!b.startOfLine)return!1;for(var c,d=null;c=a.peek();){if("\\"==c&&a.match(/^.$/)){d=u;break}if("/"==c&&a.match(/^\/[\/\*]/,!1))break;a.next()}return b.tokenize=d,"meta"}function n(a,b){return"type"==b.prevToken&&"type"}function y(a){return a.eatWhile(/[\w\.']/),"number"}function m(a,b){if(a.backUp(1),a.match(/(R|u8R|uR|UR|LR)/)){var c=a.match(/"([^\s\\()]{0,16})\(/);return!!c&&(b.cpp11RawStringDelim=c[1],b.tokenize=D,D(a,b))}return a.match(/(u8|u|U|L)/)?
+!!a.match(/["']/,!1)&&"string":(a.next(),!1)}function c(a,b){for(var c;null!=(c=a.next());)if('"'==c&&!a.eat('"')){b.tokenize=null;break}return"string"}function D(a,b){var c=b.cpp11RawStringDelim.replace(/[^\w\s]/g,"\\$\x26");return a.match(new RegExp(".*?\\)"+c+'"'))?b.tokenize=null:a.skipToEnd(),"string"}function z(a,b){function c(a){if(a)for(var b in a)a.hasOwnProperty(b)&&d.push(b)}"string"==typeof a&&(a=[a]);var d=[];c(b.keywords);c(b.types);c(b.builtin);c(b.atoms);d.length&&(b.helperType=a[0],
+e.registerHelper("hintWords",a[0],d));for(var f=0;f<a.length;++f)e.defineMIME(a[f],b)}function H(a,b){for(var c=!1;!a.eol();){if(!c&&a.match('"""')){b.tokenize=null;break}c="\\"==a.next()&&!c}return"string"}function v(a){return function(b,c){for(var d,e=!1,f=!1;!b.eol();){if(!a&&!e&&b.match('"')){f=!0;break}if(a&&b.match('"""')){f=!0;break}d=b.next();!e&&"$"==d&&b.match("{")&&b.skipTo("}");e=!e&&"\\"==d&&!a}return!f&&a||(c.tokenize=null),"string"}}function p(a){return function(b,c){for(var d,e=!1,
+f=!1;!b.eol();){if(!e&&b.match('"')&&("single"==a||b.match('""'))){f=!0;break}if(!e&&b.match("``")){k=p(a);f=!0;break}d=b.next();e="single"==a&&!e&&"\\"==d}return f&&(c.tokenize=null),"string"}}e.defineMode("clike",function(a,b){function c(a,b){var e=a.next();if(S[e]){var g=S[e](a,b);if(!1!==g)return g}if('"'==e||"'"==e)return b.tokenize=d(e),b.tokenize(a,b);if(J.test(e))return k=e,null;if(N.test(e)){if(a.backUp(1),a.match(R))return"number";a.next()}if("/"==e){if(a.eat("*"))return b.tokenize=n,n(a,
+b);if(a.eat("/"))return a.skipToEnd(),"comment"}if(P.test(e)){for(;!a.match(/^\/[\/*]/,!1)&&a.eat(P););return"operator"}if(a.eatWhile(G),O)for(;a.match(O);)a.eatWhile(G);e=a.current();return t(z,e)?(t(ca,e)&&(k="newstatement"),t(oa,e)&&(y=!0),"keyword"):t(D,e)?"type":t(H,e)?(t(ca,e)&&(k="newstatement"),"builtin"):t(ma,e)?"atom":"variable"}function d(a){return function(b,c){for(var d,e=!1,g=!1;null!=(d=b.next());){if(d==a&&!e){g=!0;break}e=!e&&"\\"==d}return(g||!e&&!aa)&&(c.tokenize=null),"string"}}
+function n(a,b){for(var c,d=!1;c=a.next();){if("/"==c&&d){b.tokenize=null;break}d="*"==c}return"comment"}function m(a,c){b.typeFirstDefinitions&&a.eol()&&f(c.context)&&(c.typeAtEndOfLine=x(a,c,a.pos))}var k,y,p=a.indentUnit,u=b.statementIndentUnit||p,v=b.dontAlignCalls,z=b.keywords||{},D=b.types||{},H=b.builtin||{},ca=b.blockKeywords||{},oa=b.defKeywords||{},ma=b.atoms||{},S=b.hooks||{},aa=b.multiLineStrings,pa=!1!==b.indentStatements,O=b.namespaceSeparator,J=b.isPunctuationChar||/[\[\]{}\(\),;\:\.]/,
+N=b.numberStart||/[\d\.]/,R=b.number||/^(?:0x[a-f\d]+|0b[01]+|(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)(u|ll?|l|f)?/i,P=b.isOperatorChar||/[+\-*&%=<>!?|\/]/,G=b.isIdentifierChar||/[\w\$_\xa1-\uffff]/;return{startState:function(a){return{tokenize:null,context:new A((a||0)-p,0,"top",null,!1),indented:0,startOfLine:!0,prevToken:null}},token:function(a,d){var e=d.context;if(a.sol()&&(null==e.align&&(e.align=!1),d.indented=a.indentation(),d.startOfLine=!0),a.eatSpace())return m(a,d),null;k=y=null;var n=(d.tokenize||
+c)(a,d);if("comment"==n||"meta"==n)return n;if(null==e.align&&(e.align=!0),";"==k||":"==k||","==k&&a.match(/^\s*(?:\/\/.*)?$/,!1))for(;"statement"==d.context.type;)w(d);else if("{"==k)C(d,a.column(),"}");else if("["==k)C(d,a.column(),"]");else if("("==k)C(d,a.column(),")");else if("}"==k){for(;"statement"==e.type;)e=w(d);for("}"==e.type&&(e=w(d));"statement"==e.type;)e=w(d)}else k==e.type?w(d):pa&&(("}"==e.type||"top"==e.type)&&";"!=k||"statement"==e.type&&"newstatement"==k)&&C(d,a.column(),"statement",
+a.current());if("variable"==n&&("def"==d.prevToken||b.typeFirstDefinitions&&x(a,d,a.start)&&f(d.context)&&a.match(/^\s*\(/,!1))&&(n="def"),S.token)e=S.token(a,d,n),void 0!==e&&(n=e);return"def"==n&&!1===b.styleDefs&&(n="variable"),d.startOfLine=!1,d.prevToken=y?"def":n||k,m(a,d),n},indent:function(a,d){if(a.tokenize!=c&&null!=a.tokenize||a.typeAtEndOfLine)return e.Pass;var f=a.context,n=d&&d.charAt(0);if("statement"==f.type&&"}"==n&&(f=f.prev),b.dontIndentStatements)for(;"statement"==f.type&&b.dontIndentStatements.test(f.info);)f=
+f.prev;if(S.indent){var k=S.indent(a,f,d);if("number"==typeof k)return k}var k=n==f.type,h=f.prev&&"switch"==f.prev.info;if(b.allmanIndentation&&/[{(]/.test(n)){for(;"top"!=f.type&&"}"!=f.type;)f=f.prev;return f.indented}return"statement"==f.type?f.indented+("{"==n?0:u):!f.align||v&&")"==f.type?")"!=f.type||k?f.indented+(k?0:p)+(k||!h||/^(?:case|default)\b/.test(d)?0:p):f.indented+u:f.column+(k?0:1)},electricInput:!1!==b.indentSwitch?/^\s*(?:case .*?:|default:|\{\}?|\})$/:/^\s*[{}]$/,blockCommentStart:"/*",
+blockCommentEnd:"*/",lineComment:"//",fold:"brace"}});z(["text/x-csrc","text/x-c","text/x-chdr"],{name:"clike",keywords:d("auto if break case register continue return default do sizeof static else struct switch extern typedef union for goto while enum const volatile"),types:d("int long char short double float unsigned signed void size_t ptrdiff_t bool _Complex _Bool float_t double_t intptr_t intmax_t int8_t int16_t int32_t int64_t uintptr_t uintmax_t uint8_t uint16_t uint32_t uint64_t"),blockKeywords:d("case do else for if switch while struct"),
+defKeywords:d("struct"),typeFirstDefinitions:!0,atoms:d("null true false"),hooks:{"#":u,"*":n},modeProps:{fold:["brace","include"]}});z(["text/x-c++src","text/x-c++hdr"],{name:"clike",keywords:d("auto if break case register continue return default do sizeof static else struct switch extern typedef union for goto while enum const volatile asm dynamic_cast namespace reinterpret_cast try explicit new static_cast typeid catch operator template typename class friend private this using const_cast inline public throw virtual delete mutable protected alignas alignof constexpr decltype nullptr noexcept thread_local final static_assert override"),
+types:d("int long char short double float unsigned signed void size_t ptrdiff_t bool wchar_t"),blockKeywords:d("catch class do else finally for if struct switch try while"),defKeywords:d("class namespace struct enum union"),typeFirstDefinitions:!0,atoms:d("true false null"),dontIndentStatements:/^template$/,isIdentifierChar:/[\w\$_~\xa1-\uffff]/,hooks:{"#":u,"*":n,u:m,U:m,L:m,R:m,0:y,1:y,2:y,3:y,4:y,5:y,6:y,7:y,8:y,9:y,token:function(a,b,c){if(b="variable"==c&&"("==a.peek()&&(";"==b.prevToken||null==
+b.prevToken||"}"==b.prevToken))a=a.current(),b=(a=/(\w+)::~?(\w+)$/.exec(a))&&a[1]==a[2];if(b)return"def"}},namespaceSeparator:"::",modeProps:{fold:["brace","include"]}});z("text/x-java",{name:"clike",keywords:d("abstract assert break case catch class const continue default do else enum extends final finally float for goto if implements import instanceof interface native new package private protected public return static strictfp super switch synchronized this throw throws transient try volatile while @interface"),
+types:d("byte short int long float double boolean char void Boolean Byte Character Double Float Integer Long Number Object Short String StringBuffer StringBuilder Void"),blockKeywords:d("catch class do else finally for if switch try while"),defKeywords:d("class interface package enum @interface"),typeFirstDefinitions:!0,atoms:d("true false null"),number:/^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+\.?\d*|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i,hooks:{"@":function(a){return!a.match("interface",!1)&&(a.eatWhile(/[\w\$_]/),
+"meta")}},modeProps:{fold:["brace","import"]}});z("text/x-csharp",{name:"clike",keywords:d("abstract as async await base break case catch checked class const continue default delegate do else enum event explicit extern finally fixed for foreach goto if implicit in interface internal is lock namespace new operator out override params private protected public readonly ref return sealed sizeof stackalloc static struct switch this throw try typeof unchecked unsafe using virtual void volatile while add alias ascending descending dynamic from get global group into join let orderby partial remove select set value var yield"),
+types:d("Action Boolean Byte Char DateTime DateTimeOffset Decimal Double Func Guid Int16 Int32 Int64 Object SByte Single String Task TimeSpan UInt16 UInt32 UInt64 bool byte char decimal double short int long object sbyte float string ushort uint ulong"),blockKeywords:d("catch class do else finally for foreach if struct switch try while"),defKeywords:d("class interface namespace struct var"),typeFirstDefinitions:!0,atoms:d("true false null"),hooks:{"@":function(a,b){return a.eat('"')?(b.tokenize=c,
+c(a,b)):(a.eatWhile(/[\w\$_]/),"meta")}}});z("text/x-scala",{name:"clike",keywords:d("abstract case catch class def do else extends final finally for forSome if implicit import lazy match new null object override package private protected return sealed super this throw trait try type val var while with yield _ assert assume require print println printf readLine readBoolean readByte readShort readChar readInt readLong readFloat readDouble"),types:d("AnyVal App Application Array BufferedIterator BigDecimal BigInt Char Console Either Enumeration Equiv Error Exception Fractional Function IndexedSeq Int Integral Iterable Iterator List Map Numeric Nil NotNull Option Ordered Ordering PartialFunction PartialOrdering Product Proxy Range Responder Seq Serializable Set Specializable Stream StringBuilder StringContext Symbol Throwable Traversable TraversableOnce Tuple Unit Vector Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable Compiler Double Exception Float Integer Long Math Number Object Package Pair Process Runtime Runnable SecurityManager Short StackTraceElement StrictMath String StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void"),
+multiLineStrings:!0,blockKeywords:d("catch class enum do else finally for forSome if match switch try while"),defKeywords:d("class enum def object package trait type val var"),atoms:d("true false null"),indentStatements:!1,indentSwitch:!1,isOperatorChar:/[+\-*&%=<>!?|\/#:@]/,hooks:{"@":function(a){return a.eatWhile(/[\w\$_]/),"meta"},'"':function(a,b){return!!a.match('""')&&(b.tokenize=H,b.tokenize(a,b))},"'":function(a){return a.eatWhile(/[\w\$_\xa1-\uffff]/),"atom"},"\x3d":function(a,b){var c=b.context;
+return!("}"!=c.type||!c.align||!a.eat("\x3e"))&&(b.context=new A(c.indented,c.column,c.type,c.info,null,c.prev),"operator")}},modeProps:{closeBrackets:{triples:'"'}}});z("text/x-kotlin",{name:"clike",keywords:d("package as typealias class interface this super val var fun for is in This throw return break continue object if else while do try when !in !is as? file import where by get set abstract enum open inner override private public internal protected catch finally out final vararg reified dynamic companion constructor init sealed field property receiver param sparam lateinit data inline noinline tailrec external annotation crossinline const operator infix suspend"),
+types:d("Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable Compiler Double Exception Float Integer Long Math Number Object Package Pair Process Runtime Runnable SecurityManager Short StackTraceElement StrictMath String StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void"),intendSwitch:!1,indentStatements:!1,multiLineStrings:!0,number:/^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+\.?\d*|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i,blockKeywords:d("catch class do else finally for if where try while enum"),
+defKeywords:d("class val var object package interface fun"),atoms:d("true false null this"),hooks:{'"':function(a,b){return b.tokenize=v(a.match('""')),b.tokenize(a,b)}},modeProps:{closeBrackets:{triples:'"'}}});z(["x-shader/x-vertex","x-shader/x-fragment"],{name:"clike",keywords:d("sampler1D sampler2D sampler3D samplerCube sampler1DShadow sampler2DShadow const attribute uniform varying break continue discard return for while do if else struct in out inout"),types:d("float int bool void vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 mat2 mat3 mat4"),
+blockKeywords:d("for while do if else struct"),builtin:d("radians degrees sin cos tan asin acos atan pow exp log exp2 sqrt inversesqrt abs sign floor ceil fract mod min max clamp mix step smoothstep length distance dot cross normalize ftransform faceforward reflect refract matrixCompMult lessThan lessThanEqual greaterThan greaterThanEqual equal notEqual any all not texture1D texture1DProj texture1DLod texture1DProjLod texture2D texture2DProj texture2DLod texture2DProjLod texture3D texture3DProj texture3DLod texture3DProjLod textureCube textureCubeLod shadow1D shadow2D shadow1DProj shadow2DProj shadow1DLod shadow2DLod shadow1DProjLod shadow2DProjLod dFdx dFdy fwidth noise1 noise2 noise3 noise4"),
+atoms:d("true false gl_FragColor gl_SecondaryColor gl_Normal gl_Vertex gl_MultiTexCoord0 gl_MultiTexCoord1 gl_MultiTexCoord2 gl_MultiTexCoord3 gl_MultiTexCoord4 gl_MultiTexCoord5 gl_MultiTexCoord6 gl_MultiTexCoord7 gl_FogCoord gl_PointCoord gl_Position gl_PointSize gl_ClipVertex gl_FrontColor gl_BackColor gl_FrontSecondaryColor gl_BackSecondaryColor gl_TexCoord gl_FogFragCoord gl_FragCoord gl_FrontFacing gl_FragData gl_FragDepth gl_ModelViewMatrix gl_ProjectionMatrix gl_ModelViewProjectionMatrix gl_TextureMatrix gl_NormalMatrix gl_ModelViewMatrixInverse gl_ProjectionMatrixInverse gl_ModelViewProjectionMatrixInverse gl_TexureMatrixTranspose gl_ModelViewMatrixInverseTranspose gl_ProjectionMatrixInverseTranspose gl_ModelViewProjectionMatrixInverseTranspose gl_TextureMatrixInverseTranspose gl_NormalScale gl_DepthRange gl_ClipPlane gl_Point gl_FrontMaterial gl_BackMaterial gl_LightSource gl_LightModel gl_FrontLightModelProduct gl_BackLightModelProduct gl_TextureColor gl_EyePlaneS gl_EyePlaneT gl_EyePlaneR gl_EyePlaneQ gl_FogParameters gl_MaxLights gl_MaxClipPlanes gl_MaxTextureUnits gl_MaxTextureCoords gl_MaxVertexAttribs gl_MaxVertexUniformComponents gl_MaxVaryingFloats gl_MaxVertexTextureImageUnits gl_MaxTextureImageUnits gl_MaxFragmentUniformComponents gl_MaxCombineTextureImageUnits gl_MaxDrawBuffers"),
+indentSwitch:!1,hooks:{"#":u},modeProps:{fold:["brace","include"]}});z("text/x-nesc",{name:"clike",keywords:d("auto if break case register continue return default do sizeof static else struct switch extern typedef union for goto while enum const volatileas atomic async call command component components configuration event generic implementation includes interface module new norace nx_struct nx_union post provides signal task uses abstract extends"),types:d("int long char short double float unsigned signed void size_t ptrdiff_t"),
+blockKeywords:d("case do else for if switch while struct"),atoms:d("null true false"),hooks:{"#":u},modeProps:{fold:["brace","include"]}});z("text/x-objectivec",{name:"clike",keywords:d("auto if break case register continue return default do sizeof static else struct switch extern typedef union for goto while enum const volatileinline restrict _Bool _Complex _Imaginary BOOL Class bycopy byref id IMP in inout nil oneway out Protocol SEL self super atomic nonatomic retain copy readwrite readonly"),
+types:d("int long char short double float unsigned signed void size_t ptrdiff_t"),atoms:d("YES NO NULL NILL ON OFF true false"),hooks:{"@":function(a){return a.eatWhile(/[\w\$]/),"keyword"},"#":u,indent:function(a,b,c){if("statement"==b.type&&/^@\w/.test(c))return b.indented}},modeProps:{fold:"brace"}});z("text/x-squirrel",{name:"clike",keywords:d("base break clone continue const default delete enum extends function in class foreach local resume return this throw typeof yield constructor instanceof static"),
+types:d("int long char short double float unsigned signed void size_t ptrdiff_t"),blockKeywords:d("case catch class else for foreach if switch try while"),defKeywords:d("function local class"),typeFirstDefinitions:!0,atoms:d("true false null"),hooks:{"#":u},modeProps:{fold:["brace","include"]}});var k=null;z("text/x-ceylon",{name:"clike",keywords:d("abstracts alias assembly assert assign break case catch class continue dynamic else exists extends finally for function given if import in interface is let module new nonempty object of out outer package return satisfies super switch then this throw try value void while"),
+types:function(a){a=a.charAt(0);return a===a.toUpperCase()&&a!==a.toLowerCase()},blockKeywords:d("case catch class dynamic else finally for function if interface module new object switch try while"),defKeywords:d("class dynamic function interface module object package value"),builtin:d("abstract actual aliased annotation by default deprecated doc final formal late license native optional sealed see serializable shared suppressWarnings tagged throws variable"),isPunctuationChar:/[\[\]{}\(\),;\:\.`]/,
+isOperatorChar:/[+\-*&%=<>!?|^~:\/]/,numberStart:/[\d#$]/,number:/^(?:#[\da-fA-F_]+|\$[01_]+|[\d_]+[kMGTPmunpf]?|[\d_]+\.[\d_]+(?:[eE][-+]?\d+|[kMGTPmunpf]|)|)/i,multiLineStrings:!0,typeFirstDefinitions:!0,atoms:d("true false null larger smaller equal empty finished"),indentSwitch:!1,styleDefs:!1,hooks:{"@":function(a){return a.eatWhile(/[\w\$_]/),"meta"},'"':function(a,b){return b.tokenize=p(a.match('""')?"triple":"single"),b.tokenize(a,b)},"`":function(a,b){return!(!k||!a.match("`"))&&(b.tokenize=
+k,k=null,b.tokenize(a,b))},"'":function(a){return a.eatWhile(/[\w\$_\xa1-\uffff]/),"atom"},token:function(a,b,c){if(("variable"==c||"type"==c)&&"."==b.prevToken)return"variable-2"}},modeProps:{fold:["brace","import"],closeBrackets:{triples:'"'}}})});
+(function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror"),require("../htmlmixed/htmlmixed"),require("../clike/clike")):"function"==typeof define&&define.amd?define("mode/php/php.js",["../../lib/codemirror","../htmlmixed/htmlmixed","../clike/clike"],e):e(CodeMirror)})(function(e){function A(e){var d={};e=e.split(" ");for(var t=0;t<e.length;++t)d[e[t]]=!0;return d}function C(e,d,t){return 0==e.length?w(d):function(u,n){for(var y=e[0],m=0;m<y.length;m++)if(u.match(y[m][0]))return n.tokenize=
+C(e.slice(1),d),y[m][1];return n.tokenize=w(d,t),"string"}}function w(e,d){return function(t,u){var n;if(!1!==d&&t.match("${",!1)||t.match("{$",!1))n=(u.tokenize=null,"string");else if(!1!==d&&t.match(/^\$[a-zA-Z_][a-zA-Z0-9_]*/))n=(t.match("[",!1)&&(u.tokenize=C([[["[",null]],[[/\d[\w\.]*/,"number"],[/\$[a-zA-Z_][a-zA-Z0-9_]*/,"variable-2"],[/[\w\$]+/,"variable"]],[["]",null]]],e,d)),t.match(/\-\>\w/,!1)&&(u.tokenize=C([[["-\x3e",null]],[[/[\w]+/,"variable"]]],e,d)),"variable-2");else{for(n=!1;!t.eol()&&
+(n||!1===d||!t.match("{$",!1)&&!t.match(/^(\$[a-zA-Z_][a-zA-Z0-9_]*|\$\{)/,!1));){if(!n&&t.match(e)){u.tokenize=null;u.tokStack.pop();u.tokStack.pop();break}n="\\"==t.next()&&!n}n="string"}return n}}e.registerHelper("hintWords","php","abstract and array as break case catch class clone const continue declare default do else elseif enddeclare endfor endforeach endif endswitch endwhile extends final for foreach function global goto if implements interface instanceof namespace new or private protected public static switch throw trait try use var while xor die echo empty exit eval include include_once isset list require require_once return print unset __halt_compiler self static parent yield insteadof finally true false null TRUE FALSE NULL __CLASS__ __DIR__ __FILE__ __LINE__ __METHOD__ __FUNCTION__ __NAMESPACE__ __TRAIT__ func_num_args func_get_arg func_get_args strlen strcmp strncmp strcasecmp strncasecmp each error_reporting define defined trigger_error user_error set_error_handler restore_error_handler get_declared_classes get_loaded_extensions extension_loaded get_extension_funcs debug_backtrace constant bin2hex hex2bin sleep usleep time mktime gmmktime strftime gmstrftime strtotime date gmdate getdate localtime checkdate flush wordwrap htmlspecialchars htmlentities html_entity_decode md5 md5_file crc32 getimagesize image_type_to_mime_type phpinfo phpversion phpcredits strnatcmp strnatcasecmp substr_count strspn strcspn strtok strtoupper strtolower strpos strrpos strrev hebrev hebrevc nl2br basename dirname pathinfo stripslashes stripcslashes strstr stristr strrchr str_shuffle str_word_count strcoll substr substr_replace quotemeta ucfirst ucwords strtr addslashes addcslashes rtrim str_replace str_repeat count_chars chunk_split trim ltrim strip_tags similar_text explode implode setlocale localeconv parse_str str_pad chop strchr sprintf printf vprintf vsprintf sscanf fscanf parse_url urlencode urldecode rawurlencode rawurldecode readlink linkinfo link unlink exec system escapeshellcmd escapeshellarg passthru shell_exec proc_open proc_close rand srand getrandmax mt_rand mt_srand mt_getrandmax base64_decode base64_encode abs ceil floor round is_finite is_nan is_infinite bindec hexdec octdec decbin decoct dechex base_convert number_format fmod ip2long long2ip getenv putenv getopt microtime gettimeofday getrusage uniqid quoted_printable_decode set_time_limit get_cfg_var magic_quotes_runtime set_magic_quotes_runtime get_magic_quotes_gpc get_magic_quotes_runtime import_request_variables error_log serialize unserialize memory_get_usage var_dump var_export debug_zval_dump print_r highlight_file show_source highlight_string ini_get ini_get_all ini_set ini_alter ini_restore get_include_path set_include_path restore_include_path setcookie header headers_sent connection_aborted connection_status ignore_user_abort parse_ini_file is_uploaded_file move_uploaded_file intval floatval doubleval strval gettype settype is_null is_resource is_bool is_long is_float is_int is_integer is_double is_real is_numeric is_string is_array is_object is_scalar ereg ereg_replace eregi eregi_replace split spliti join sql_regcase dl pclose popen readfile rewind rmdir umask fclose feof fgetc fgets fgetss fread fopen fpassthru ftruncate fstat fseek ftell fflush fwrite fputs mkdir rename copy tempnam tmpfile file file_get_contents file_put_contents stream_select stream_context_create stream_context_set_params stream_context_set_option stream_context_get_options stream_filter_prepend stream_filter_append fgetcsv flock get_meta_tags stream_set_write_buffer set_file_buffer set_socket_blocking stream_set_blocking socket_set_blocking stream_get_meta_data stream_register_wrapper stream_wrapper_register stream_set_timeout socket_set_timeout socket_get_status realpath fnmatch fsockopen pfsockopen pack unpack get_browser crypt opendir closedir chdir getcwd rewinddir readdir dir glob fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype file_exists is_writable is_writeable is_readable is_executable is_file is_dir is_link stat lstat chown touch clearstatcache mail ob_start ob_flush ob_clean ob_end_flush ob_end_clean ob_get_flush ob_get_clean ob_get_length ob_get_level ob_get_status ob_get_contents ob_implicit_flush ob_list_handlers ksort krsort natsort natcasesort asort arsort sort rsort usort uasort uksort shuffle array_walk count end prev next reset current key min max in_array array_search extract compact array_fill range array_multisort array_push array_pop array_shift array_unshift array_splice array_slice array_merge array_merge_recursive array_keys array_values array_count_values array_reverse array_reduce array_pad array_flip array_change_key_case array_rand array_unique array_intersect array_intersect_assoc array_diff array_diff_assoc array_sum array_filter array_map array_chunk array_key_exists array_intersect_key array_combine array_column pos sizeof key_exists assert assert_options version_compare ftok str_rot13 aggregate session_name session_module_name session_save_path session_id session_regenerate_id session_decode session_register session_unregister session_is_registered session_encode session_start session_destroy session_unset session_set_save_handler session_cache_limiter session_cache_expire session_set_cookie_params session_get_cookie_params session_write_close preg_match preg_match_all preg_replace preg_replace_callback preg_split preg_quote preg_grep overload ctype_alnum ctype_alpha ctype_cntrl ctype_digit ctype_lower ctype_graph ctype_print ctype_punct ctype_space ctype_upper ctype_xdigit virtual apache_request_headers apache_note apache_lookup_uri apache_child_terminate apache_setenv apache_response_headers apache_get_version getallheaders mysql_connect mysql_pconnect mysql_close mysql_select_db mysql_create_db mysql_drop_db mysql_query mysql_unbuffered_query mysql_db_query mysql_list_dbs mysql_list_tables mysql_list_fields mysql_list_processes mysql_error mysql_errno mysql_affected_rows mysql_insert_id mysql_result mysql_num_rows mysql_num_fields mysql_fetch_row mysql_fetch_array mysql_fetch_assoc mysql_fetch_object mysql_data_seek mysql_fetch_lengths mysql_fetch_field mysql_field_seek mysql_free_result mysql_field_name mysql_field_table mysql_field_len mysql_field_type mysql_field_flags mysql_escape_string mysql_real_escape_string mysql_stat mysql_thread_id mysql_client_encoding mysql_get_client_info mysql_get_host_info mysql_get_proto_info mysql_get_server_info mysql_info mysql mysql_fieldname mysql_fieldtable mysql_fieldlen mysql_fieldtype mysql_fieldflags mysql_selectdb mysql_createdb mysql_dropdb mysql_freeresult mysql_numfields mysql_numrows mysql_listdbs mysql_listtables mysql_listfields mysql_db_name mysql_dbname mysql_tablename mysql_table_name pg_connect pg_pconnect pg_close pg_connection_status pg_connection_busy pg_connection_reset pg_host pg_dbname pg_port pg_tty pg_options pg_ping pg_query pg_send_query pg_cancel_query pg_fetch_result pg_fetch_row pg_fetch_assoc pg_fetch_array pg_fetch_object pg_fetch_all pg_affected_rows pg_get_result pg_result_seek pg_result_status pg_free_result pg_last_oid pg_num_rows pg_num_fields pg_field_name pg_field_num pg_field_size pg_field_type pg_field_prtlen pg_field_is_null pg_get_notify pg_get_pid pg_result_error pg_last_error pg_last_notice pg_put_line pg_end_copy pg_copy_to pg_copy_from pg_trace pg_untrace pg_lo_create pg_lo_unlink pg_lo_open pg_lo_close pg_lo_read pg_lo_write pg_lo_read_all pg_lo_import pg_lo_export pg_lo_seek pg_lo_tell pg_escape_string pg_escape_bytea pg_unescape_bytea pg_client_encoding pg_set_client_encoding pg_meta_data pg_convert pg_insert pg_update pg_delete pg_select pg_exec pg_getlastoid pg_cmdtuples pg_errormessage pg_numrows pg_numfields pg_fieldname pg_fieldsize pg_fieldtype pg_fieldnum pg_fieldprtlen pg_fieldisnull pg_freeresult pg_result pg_loreadall pg_locreate pg_lounlink pg_loopen pg_loclose pg_loread pg_lowrite pg_loimport pg_loexport http_response_code get_declared_traits getimagesizefromstring socket_import_stream stream_set_chunk_size trait_exists header_register_callback class_uses session_status session_register_shutdown echo print global static exit array empty eval isset unset die include require include_once require_once json_decode json_encode json_last_error json_last_error_msg curl_close curl_copy_handle curl_errno curl_error curl_escape curl_exec curl_file_create curl_getinfo curl_init curl_multi_add_handle curl_multi_close curl_multi_exec curl_multi_getcontent curl_multi_info_read curl_multi_init curl_multi_remove_handle curl_multi_select curl_multi_setopt curl_multi_strerror curl_pause curl_reset curl_setopt_array curl_setopt curl_share_close curl_share_init curl_share_setopt curl_strerror curl_unescape curl_version mysqli_affected_rows mysqli_autocommit mysqli_change_user mysqli_character_set_name mysqli_close mysqli_commit mysqli_connect_errno mysqli_connect_error mysqli_connect mysqli_data_seek mysqli_debug mysqli_dump_debug_info mysqli_errno mysqli_error_list mysqli_error mysqli_fetch_all mysqli_fetch_array mysqli_fetch_assoc mysqli_fetch_field_direct mysqli_fetch_field mysqli_fetch_fields mysqli_fetch_lengths mysqli_fetch_object mysqli_fetch_row mysqli_field_count mysqli_field_seek mysqli_field_tell mysqli_free_result mysqli_get_charset mysqli_get_client_info mysqli_get_client_stats mysqli_get_client_version mysqli_get_connection_stats mysqli_get_host_info mysqli_get_proto_info mysqli_get_server_info mysqli_get_server_version mysqli_info mysqli_init mysqli_insert_id mysqli_kill mysqli_more_results mysqli_multi_query mysqli_next_result mysqli_num_fields mysqli_num_rows mysqli_options mysqli_ping mysqli_prepare mysqli_query mysqli_real_connect mysqli_real_escape_string mysqli_real_query mysqli_reap_async_query mysqli_refresh mysqli_rollback mysqli_select_db mysqli_set_charset mysqli_set_local_infile_default mysqli_set_local_infile_handler mysqli_sqlstate mysqli_ssl_set mysqli_stat mysqli_stmt_init mysqli_store_result mysqli_thread_id mysqli_thread_safe mysqli_use_result mysqli_warning_count".split(" "));
+e.registerHelper("wordChars","php",/[\w$]/);var x={name:"clike",helperType:"php",keywords:A("abstract and array as break case catch class clone const continue declare default do else elseif enddeclare endfor endforeach endif endswitch endwhile extends final for foreach function global goto if implements interface instanceof namespace new or private protected public static switch throw trait try use var while xor die echo empty exit eval include include_once isset list require require_once return print unset __halt_compiler self static parent yield insteadof finally"),
+blockKeywords:A("catch do else elseif for foreach if switch try while finally"),defKeywords:A("class function interface namespace trait"),atoms:A("true false null TRUE FALSE NULL __CLASS__ __DIR__ __FILE__ __LINE__ __METHOD__ __FUNCTION__ __NAMESPACE__ __TRAIT__"),builtin:A("func_num_args func_get_arg func_get_args strlen strcmp strncmp strcasecmp strncasecmp each error_reporting define defined trigger_error user_error set_error_handler restore_error_handler get_declared_classes get_loaded_extensions extension_loaded get_extension_funcs debug_backtrace constant bin2hex hex2bin sleep usleep time mktime gmmktime strftime gmstrftime strtotime date gmdate getdate localtime checkdate flush wordwrap htmlspecialchars htmlentities html_entity_decode md5 md5_file crc32 getimagesize image_type_to_mime_type phpinfo phpversion phpcredits strnatcmp strnatcasecmp substr_count strspn strcspn strtok strtoupper strtolower strpos strrpos strrev hebrev hebrevc nl2br basename dirname pathinfo stripslashes stripcslashes strstr stristr strrchr str_shuffle str_word_count strcoll substr substr_replace quotemeta ucfirst ucwords strtr addslashes addcslashes rtrim str_replace str_repeat count_chars chunk_split trim ltrim strip_tags similar_text explode implode setlocale localeconv parse_str str_pad chop strchr sprintf printf vprintf vsprintf sscanf fscanf parse_url urlencode urldecode rawurlencode rawurldecode readlink linkinfo link unlink exec system escapeshellcmd escapeshellarg passthru shell_exec proc_open proc_close rand srand getrandmax mt_rand mt_srand mt_getrandmax base64_decode base64_encode abs ceil floor round is_finite is_nan is_infinite bindec hexdec octdec decbin decoct dechex base_convert number_format fmod ip2long long2ip getenv putenv getopt microtime gettimeofday getrusage uniqid quoted_printable_decode set_time_limit get_cfg_var magic_quotes_runtime set_magic_quotes_runtime get_magic_quotes_gpc get_magic_quotes_runtime import_request_variables error_log serialize unserialize memory_get_usage var_dump var_export debug_zval_dump print_r highlight_file show_source highlight_string ini_get ini_get_all ini_set ini_alter ini_restore get_include_path set_include_path restore_include_path setcookie header headers_sent connection_aborted connection_status ignore_user_abort parse_ini_file is_uploaded_file move_uploaded_file intval floatval doubleval strval gettype settype is_null is_resource is_bool is_long is_float is_int is_integer is_double is_real is_numeric is_string is_array is_object is_scalar ereg ereg_replace eregi eregi_replace split spliti join sql_regcase dl pclose popen readfile rewind rmdir umask fclose feof fgetc fgets fgetss fread fopen fpassthru ftruncate fstat fseek ftell fflush fwrite fputs mkdir rename copy tempnam tmpfile file file_get_contents file_put_contents stream_select stream_context_create stream_context_set_params stream_context_set_option stream_context_get_options stream_filter_prepend stream_filter_append fgetcsv flock get_meta_tags stream_set_write_buffer set_file_buffer set_socket_blocking stream_set_blocking socket_set_blocking stream_get_meta_data stream_register_wrapper stream_wrapper_register stream_set_timeout socket_set_timeout socket_get_status realpath fnmatch fsockopen pfsockopen pack unpack get_browser crypt opendir closedir chdir getcwd rewinddir readdir dir glob fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype file_exists is_writable is_writeable is_readable is_executable is_file is_dir is_link stat lstat chown touch clearstatcache mail ob_start ob_flush ob_clean ob_end_flush ob_end_clean ob_get_flush ob_get_clean ob_get_length ob_get_level ob_get_status ob_get_contents ob_implicit_flush ob_list_handlers ksort krsort natsort natcasesort asort arsort sort rsort usort uasort uksort shuffle array_walk count end prev next reset current key min max in_array array_search extract compact array_fill range array_multisort array_push array_pop array_shift array_unshift array_splice array_slice array_merge array_merge_recursive array_keys array_values array_count_values array_reverse array_reduce array_pad array_flip array_change_key_case array_rand array_unique array_intersect array_intersect_assoc array_diff array_diff_assoc array_sum array_filter array_map array_chunk array_key_exists array_intersect_key array_combine array_column pos sizeof key_exists assert assert_options version_compare ftok str_rot13 aggregate session_name session_module_name session_save_path session_id session_regenerate_id session_decode session_register session_unregister session_is_registered session_encode session_start session_destroy session_unset session_set_save_handler session_cache_limiter session_cache_expire session_set_cookie_params session_get_cookie_params session_write_close preg_match preg_match_all preg_replace preg_replace_callback preg_split preg_quote preg_grep overload ctype_alnum ctype_alpha ctype_cntrl ctype_digit ctype_lower ctype_graph ctype_print ctype_punct ctype_space ctype_upper ctype_xdigit virtual apache_request_headers apache_note apache_lookup_uri apache_child_terminate apache_setenv apache_response_headers apache_get_version getallheaders mysql_connect mysql_pconnect mysql_close mysql_select_db mysql_create_db mysql_drop_db mysql_query mysql_unbuffered_query mysql_db_query mysql_list_dbs mysql_list_tables mysql_list_fields mysql_list_processes mysql_error mysql_errno mysql_affected_rows mysql_insert_id mysql_result mysql_num_rows mysql_num_fields mysql_fetch_row mysql_fetch_array mysql_fetch_assoc mysql_fetch_object mysql_data_seek mysql_fetch_lengths mysql_fetch_field mysql_field_seek mysql_free_result mysql_field_name mysql_field_table mysql_field_len mysql_field_type mysql_field_flags mysql_escape_string mysql_real_escape_string mysql_stat mysql_thread_id mysql_client_encoding mysql_get_client_info mysql_get_host_info mysql_get_proto_info mysql_get_server_info mysql_info mysql mysql_fieldname mysql_fieldtable mysql_fieldlen mysql_fieldtype mysql_fieldflags mysql_selectdb mysql_createdb mysql_dropdb mysql_freeresult mysql_numfields mysql_numrows mysql_listdbs mysql_listtables mysql_listfields mysql_db_name mysql_dbname mysql_tablename mysql_table_name pg_connect pg_pconnect pg_close pg_connection_status pg_connection_busy pg_connection_reset pg_host pg_dbname pg_port pg_tty pg_options pg_ping pg_query pg_send_query pg_cancel_query pg_fetch_result pg_fetch_row pg_fetch_assoc pg_fetch_array pg_fetch_object pg_fetch_all pg_affected_rows pg_get_result pg_result_seek pg_result_status pg_free_result pg_last_oid pg_num_rows pg_num_fields pg_field_name pg_field_num pg_field_size pg_field_type pg_field_prtlen pg_field_is_null pg_get_notify pg_get_pid pg_result_error pg_last_error pg_last_notice pg_put_line pg_end_copy pg_copy_to pg_copy_from pg_trace pg_untrace pg_lo_create pg_lo_unlink pg_lo_open pg_lo_close pg_lo_read pg_lo_write pg_lo_read_all pg_lo_import pg_lo_export pg_lo_seek pg_lo_tell pg_escape_string pg_escape_bytea pg_unescape_bytea pg_client_encoding pg_set_client_encoding pg_meta_data pg_convert pg_insert pg_update pg_delete pg_select pg_exec pg_getlastoid pg_cmdtuples pg_errormessage pg_numrows pg_numfields pg_fieldname pg_fieldsize pg_fieldtype pg_fieldnum pg_fieldprtlen pg_fieldisnull pg_freeresult pg_result pg_loreadall pg_locreate pg_lounlink pg_loopen pg_loclose pg_loread pg_lowrite pg_loimport pg_loexport http_response_code get_declared_traits getimagesizefromstring socket_import_stream stream_set_chunk_size trait_exists header_register_callback class_uses session_status session_register_shutdown echo print global static exit array empty eval isset unset die include require include_once require_once json_decode json_encode json_last_error json_last_error_msg curl_close curl_copy_handle curl_errno curl_error curl_escape curl_exec curl_file_create curl_getinfo curl_init curl_multi_add_handle curl_multi_close curl_multi_exec curl_multi_getcontent curl_multi_info_read curl_multi_init curl_multi_remove_handle curl_multi_select curl_multi_setopt curl_multi_strerror curl_pause curl_reset curl_setopt_array curl_setopt curl_share_close curl_share_init curl_share_setopt curl_strerror curl_unescape curl_version mysqli_affected_rows mysqli_autocommit mysqli_change_user mysqli_character_set_name mysqli_close mysqli_commit mysqli_connect_errno mysqli_connect_error mysqli_connect mysqli_data_seek mysqli_debug mysqli_dump_debug_info mysqli_errno mysqli_error_list mysqli_error mysqli_fetch_all mysqli_fetch_array mysqli_fetch_assoc mysqli_fetch_field_direct mysqli_fetch_field mysqli_fetch_fields mysqli_fetch_lengths mysqli_fetch_object mysqli_fetch_row mysqli_field_count mysqli_field_seek mysqli_field_tell mysqli_free_result mysqli_get_charset mysqli_get_client_info mysqli_get_client_stats mysqli_get_client_version mysqli_get_connection_stats mysqli_get_host_info mysqli_get_proto_info mysqli_get_server_info mysqli_get_server_version mysqli_info mysqli_init mysqli_insert_id mysqli_kill mysqli_more_results mysqli_multi_query mysqli_next_result mysqli_num_fields mysqli_num_rows mysqli_options mysqli_ping mysqli_prepare mysqli_query mysqli_real_connect mysqli_real_escape_string mysqli_real_query mysqli_reap_async_query mysqli_refresh mysqli_rollback mysqli_select_db mysqli_set_charset mysqli_set_local_infile_default mysqli_set_local_infile_handler mysqli_sqlstate mysqli_ssl_set mysqli_stat mysqli_stmt_init mysqli_store_result mysqli_thread_id mysqli_thread_safe mysqli_use_result mysqli_warning_count"),
+multiLineStrings:!0,hooks:{$:function(e){return e.eatWhile(/[\w\$_]/),"variable-2"},"\x3c":function(e,d){var t;if(t=e.match(/<<\s*/)){var u=e.eat(/['"]/);e.eatWhile(/[\w\.]/);t=e.current().slice(t[0].length+(u?2:1));if(u&&e.eat(u),t)return(d.tokStack||(d.tokStack=[])).push(t,0),d.tokenize=w(t,"'"!=u),"string"}return!1},"#":function(e){for(;!e.eol()&&!e.match("?\x3e",!1);)e.next();return"comment"},"/":function(e){if(e.eat("/")){for(;!e.eol()&&!e.match("?\x3e",!1);)e.next();return"comment"}return!1},
+'"':function(e,d){return(d.tokStack||(d.tokStack=[])).push('"',0),d.tokenize=w('"'),"string"},"{":function(e,d){return d.tokStack&&d.tokStack.length&&d.tokStack[d.tokStack.length-1]++,!1},"}":function(e,d){return d.tokStack&&0<d.tokStack.length&&!--d.tokStack[d.tokStack.length-1]&&(d.tokenize=w(d.tokStack[d.tokStack.length-2])),!1}}};e.defineMode("php",function(f,d){var t=e.getMode(f,"text/html"),u=e.getMode(f,x);return{startState:function(){var n=e.startState(t),f=d.startOpen?e.startState(u):null;
+return{html:n,php:f,curMode:d.startOpen?u:t,curState:d.startOpen?f:n,pending:null}},copyState:function(d){var f,m=e.copyState(t,d.html),c=d.php,c=c&&e.copyState(u,c);return f=d.curMode==t?m:c,{html:m,php:c,curMode:d.curMode,curState:f,pending:d.pending}},token:function(d,f){var m=f.curMode==u;if(d.sol()&&f.pending&&'"'!=f.pending&&"'"!=f.pending&&(f.pending=null),m)return m&&null==f.php.tokenize&&d.match("?\x3e")?(f.curMode=t,f.curState=f.html,f.php.context.prev||(f.php=null),"meta"):u.token(d,f.curState);
+if(d.match(/^<\?\w*/))return f.curMode=u,f.php||(f.php=e.startState(u,t.indent(f.html,""))),f.curState=f.php,"meta";if('"'==f.pending||"'"==f.pending){for(;!d.eol()&&d.next()!=f.pending;);m="string"}else f.pending&&d.pos<f.pending.end?(d.pos=f.pending.end,m=f.pending.style):m=t.token(d,f.curState);f.pending&&(f.pending=null);var c,w=d.current(),x=w.search(/<\?/);return-1!=x&&("string"==m&&(c=w.match(/[\'\"]$/))&&!/\?>/.test(w)?f.pending=c[0]:f.pending={end:d.pos,style:m},d.backUp(w.length-x)),m},
+indent:function(d,e){return d.curMode!=u&&/^\s*<\//.test(e)||d.curMode==u&&/^\?>/.test(e)?t.indent(d.html,e):d.curMode.indent(d.curState,e)},blockCommentStart:"/*",blockCommentEnd:"*/",lineComment:"//",innerMode:function(d){return{state:d.curState,mode:d.curMode}}}},"htmlmixed","clike");e.defineMIME("application/x-httpd-php","php");e.defineMIME("application/x-httpd-php-open",{name:"php",startOpen:!0});e.defineMIME("text/x-php",x)});
+(function(e){"function"==typeof e.define&&e.define("modePHP",["mode/php/php.js"],function(){})})(this);
\ No newline at end of file
diff --git a/js/ckeditor/plugins/codemirror/js/codemirror.mode.twig.min.js b/js/ckeditor/plugins/codemirror/js/codemirror.mode.twig.min.js
new file mode 100644 (file)
index 0000000..0b98b10
--- /dev/null
@@ -0,0 +1,10 @@
+!function(f){"object"==typeof exports&&"object"==typeof module?f(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define("addon/mode/multiplex",["../../lib/codemirror"],f):f(CodeMirror)}(function(f){f.multiplexingMode=function(h){function k(a,d,c,b){return"string"==typeof d?(c=a.indexOf(d,c),b&&-1<c?c+d.length:c):(d=d.exec(c?a.slice(c):a))?d.index+c+(b?d[0].length:0):-1}var l=Array.prototype.slice.call(arguments,1);return{startState:function(){return{outer:f.startState(h),innerActive:null,
+inner:null}},copyState:function(a){return{outer:f.copyState(h,a.outer),innerActive:a.innerActive,inner:a.innerActive&&f.copyState(a.innerActive.mode,a.inner)}},token:function(a,d){if(d.innerActive){var c=d.innerActive,b=a.string;if(!c.close&&a.sol())return d.innerActive=d.inner=null,this.token(a,d);var e=c.close?k(b,c.close,a.pos,c.parseDelimiters):-1;if(e==a.pos&&!c.parseDelimiters)return a.match(c.close),d.innerActive=d.inner=null,c.delimStyle&&c.delimStyle+" "+c.delimStyle+"-close";-1<e&&(a.string=
+b.slice(0,e));var g=c.mode.token(a,d.inner);return-1<e&&(a.string=b),e==a.pos&&c.parseDelimiters&&(d.innerActive=d.inner=null),c.innerStyle&&(g=g?g+" "+c.innerStyle:c.innerStyle),g}c=1/0;b=a.string;for(g=0;g<l.length;++g){var m=l[g],e=k(b,m.open,a.pos);if(e==a.pos)return m.parseDelimiters||a.match(m.open),d.innerActive=m,d.inner=f.startState(m.mode,h.indent?h.indent(d.outer,""):0),m.delimStyle&&m.delimStyle+" "+m.delimStyle+"-open";-1!=e&&e<c&&(c=e)}c!=1/0&&(a.string=b.slice(0,c));e=h.token(a,d.outer);
+return c!=1/0&&(a.string=b),e},indent:function(a,d){var c=a.innerActive?a.innerActive.mode:h;return c.indent?c.indent(a.innerActive?a.inner:a.outer,d):f.Pass},blankLine:function(a){var d=a.innerActive?a.innerActive.mode:h;if(d.blankLine&&d.blankLine(a.innerActive?a.inner:a.outer),a.innerActive)"\n"===a.innerActive.close&&(a.innerActive=a.inner=null);else for(var c=0;c<l.length;++c){var b=l[c];"\n"===b.open&&(a.innerActive=b,a.inner=f.startState(b.mode,d.indent?d.indent(a.outer,""):0))}},electricChars:h.electricChars,
+innerMode:function(a){return a.inner?{state:a.inner,mode:a.innerActive.mode}:{state:a.outer,mode:h}}}}});
+(function(f){"object"==typeof exports&&"object"==typeof module?f(require("../../lib/codemirror"),require("../../addon/mode/multiplex")):"function"==typeof define&&define.amd?define("mode/twig/twig.js",["../../lib/codemirror","../../addon/mode/multiplex"],f):f(CodeMirror)})(function(f){f.defineMode("twig:inner",function(){function f(b,e){var g=b.peek();if(e.incomment)return b.skipTo("#}")?(b.eatWhile(/\#|}/),e.incomment=!1):b.skipToEnd(),"comment";if(e.intag){if(e.operator){if(e.operator=!1,b.match(d))return"atom";
+if(b.match(c))return"number"}if(e.sign){if(e.sign=!1,b.match(d))return"atom";if(b.match(c))return"number"}if(e.instring)return g==e.instring&&(e.instring=!1),b.next(),"string";if("'"==g||'"'==g)return e.instring=g,b.next(),"string";if(b.match(e.intag+"}")||b.eat("-")&&b.match(e.intag+"}"))return e.intag=!1,"tag";if(b.match(l))return e.operator=!0,"operator";if(b.match(a))e.sign=!0;else if(b.eat(" ")||b.sol()){if(b.match(k))return"keyword";if(b.match(d))return"atom";if(b.match(c))return"number";b.sol()&&
+b.next()}else b.next();return"variable"}if(b.eat("{")){if(b.eat("#"))return e.incomment=!0,b.skipTo("#}")?(b.eatWhile(/\#|}/),e.incomment=!1):b.skipToEnd(),"comment";if(g=b.eat(/\{|%/))return e.intag=g,"{"==g&&(e.intag="}"),b.eat("-"),"tag"}b.next()}var k="and as autoescape endautoescape block do endblock else elseif extends for endfor embed endembed filter endfilter flush from if endif in is include import not or set spaceless endspaceless with endwith trans endtrans blocktrans endblocktrans macro endmacro use verbatim endverbatim".split(" "),
+l=/^[+\-*&%=<>!?|~^]/,a=/^[:\[\(\{]/,d="true;false;null;empty;defined;divisibleby;divisible by;even;odd;iterable;sameas;same as".split(";"),c=/^(\d[+\-\*\/])?\d+(\.\d+)?/;return k=new RegExp("(("+k.join(")|(")+"))\\b"),d=new RegExp("(("+d.join(")|(")+"))\\b"),{startState:function(){return{}},token:function(a,c){return f(a,c)}}});f.defineMode("twig",function(h,k){var l=f.getMode(h,"twig:inner");return k&&k.base?f.multiplexingMode(f.getMode(h,k.base),{open:/\{[{#%]/,close:/[}#%]\}/,mode:l,parseDelimiters:!0}):
+l});f.defineMIME("text/x-twig","twig")});(function(f){"function"==typeof f.define&&f.define("modeTwig",["mode/twig/twig.js"],function(){})})(this);
\ No newline at end of file
diff --git a/js/ckeditor/plugins/codemirror/theme/3024-day.css b/js/ckeditor/plugins/codemirror/theme/3024-day.css
new file mode 100644 (file)
index 0000000..5232267
--- /dev/null
@@ -0,0 +1,41 @@
+/*\r
+\r
+    Name:       3024 day\r
+    Author:     Jan T. Sott (http://github.com/idleberg)\r
+\r
+    CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)\r
+    Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)\r
+\r
+*/\r
+\r
+.cm-s-3024-day.CodeMirror { background: #f7f7f7; color: #3a3432; }\r
+.cm-s-3024-day div.CodeMirror-selected { background: #d6d5d4; }\r
+\r
+.cm-s-3024-day .CodeMirror-line::selection, .cm-s-3024-day .CodeMirror-line > span::selection, .cm-s-3024-day .CodeMirror-line > span > span::selection { background: #d6d5d4; }\r
+.cm-s-3024-day .CodeMirror-line::-moz-selection, .cm-s-3024-day .CodeMirror-line > span::-moz-selection, .cm-s-3024-day .CodeMirror-line > span > span::selection { background: #d9d9d9; }\r
+\r
+.cm-s-3024-day .CodeMirror-gutters { background: #f7f7f7; border-right: 0px; }\r
+.cm-s-3024-day .CodeMirror-guttermarker { color: #db2d20; }\r
+.cm-s-3024-day .CodeMirror-guttermarker-subtle { color: #807d7c; }\r
+.cm-s-3024-day .CodeMirror-linenumber { color: #807d7c; }\r
+\r
+.cm-s-3024-day .CodeMirror-cursor { border-left: 1px solid #5c5855; }\r
+\r
+.cm-s-3024-day span.cm-comment { color: #cdab53; }\r
+.cm-s-3024-day span.cm-atom { color: #a16a94; }\r
+.cm-s-3024-day span.cm-number { color: #a16a94; }\r
+\r
+.cm-s-3024-day span.cm-property, .cm-s-3024-day span.cm-attribute { color: #01a252; }\r
+.cm-s-3024-day span.cm-keyword { color: #db2d20; }\r
+.cm-s-3024-day span.cm-string { color: #fded02; }\r
+\r
+.cm-s-3024-day span.cm-variable { color: #01a252; }\r
+.cm-s-3024-day span.cm-variable-2 { color: #01a0e4; }\r
+.cm-s-3024-day span.cm-def { color: #e8bbd0; }\r
+.cm-s-3024-day span.cm-bracket { color: #3a3432; }\r
+.cm-s-3024-day span.cm-tag { color: #db2d20; }\r
+.cm-s-3024-day span.cm-link { color: #a16a94; }\r
+.cm-s-3024-day span.cm-error { background: #db2d20; color: #5c5855; }\r
+\r
+.cm-s-3024-day .CodeMirror-activeline-background { background: #e8f2ff; }\r
+.cm-s-3024-day .CodeMirror-matchingbracket { text-decoration: underline; color: #a16a94 !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/3024-night.css b/js/ckeditor/plugins/codemirror/theme/3024-night.css
new file mode 100644 (file)
index 0000000..bd0b64d
--- /dev/null
@@ -0,0 +1,39 @@
+/*\r
+\r
+    Name:       3024 night\r
+    Author:     Jan T. Sott (http://github.com/idleberg)\r
+\r
+    CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)\r
+    Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)\r
+\r
+*/\r
+\r
+.cm-s-3024-night.CodeMirror { background: #090300; color: #d6d5d4; }\r
+.cm-s-3024-night div.CodeMirror-selected { background: #3a3432; }\r
+.cm-s-3024-night .CodeMirror-line::selection, .cm-s-3024-night .CodeMirror-line > span::selection, .cm-s-3024-night .CodeMirror-line > span > span::selection { background: rgba(58, 52, 50, .99); }\r
+.cm-s-3024-night .CodeMirror-line::-moz-selection, .cm-s-3024-night .CodeMirror-line > span::-moz-selection, .cm-s-3024-night .CodeMirror-line > span > span::-moz-selection { background: rgba(58, 52, 50, .99); }\r
+.cm-s-3024-night .CodeMirror-gutters { background: #090300; border-right: 0px; }\r
+.cm-s-3024-night .CodeMirror-guttermarker { color: #db2d20; }\r
+.cm-s-3024-night .CodeMirror-guttermarker-subtle { color: #5c5855; }\r
+.cm-s-3024-night .CodeMirror-linenumber { color: #5c5855; }\r
+\r
+.cm-s-3024-night .CodeMirror-cursor { border-left: 1px solid #807d7c; }\r
+\r
+.cm-s-3024-night span.cm-comment { color: #cdab53; }\r
+.cm-s-3024-night span.cm-atom { color: #a16a94; }\r
+.cm-s-3024-night span.cm-number { color: #a16a94; }\r
+\r
+.cm-s-3024-night span.cm-property, .cm-s-3024-night span.cm-attribute { color: #01a252; }\r
+.cm-s-3024-night span.cm-keyword { color: #db2d20; }\r
+.cm-s-3024-night span.cm-string { color: #fded02; }\r
+\r
+.cm-s-3024-night span.cm-variable { color: #01a252; }\r
+.cm-s-3024-night span.cm-variable-2 { color: #01a0e4; }\r
+.cm-s-3024-night span.cm-def { color: #e8bbd0; }\r
+.cm-s-3024-night span.cm-bracket { color: #d6d5d4; }\r
+.cm-s-3024-night span.cm-tag { color: #db2d20; }\r
+.cm-s-3024-night span.cm-link { color: #a16a94; }\r
+.cm-s-3024-night span.cm-error { background: #db2d20; color: #807d7c; }\r
+\r
+.cm-s-3024-night .CodeMirror-activeline-background { background: #2F2F2F; }\r
+.cm-s-3024-night .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/abcdef.css b/js/ckeditor/plugins/codemirror/theme/abcdef.css
new file mode 100644 (file)
index 0000000..748540b
--- /dev/null
@@ -0,0 +1,32 @@
+.cm-s-abcdef.CodeMirror { background: #0f0f0f; color: #defdef; }\r
+.cm-s-abcdef div.CodeMirror-selected { background: #515151; }\r
+.cm-s-abcdef .CodeMirror-line::selection, .cm-s-abcdef .CodeMirror-line > span::selection, .cm-s-abcdef .CodeMirror-line > span > span::selection { background: rgba(56, 56, 56, 0.99); }\r
+.cm-s-abcdef .CodeMirror-line::-moz-selection, .cm-s-abcdef .CodeMirror-line > span::-moz-selection, .cm-s-abcdef .CodeMirror-line > span > span::-moz-selection { background: rgba(56, 56, 56, 0.99); }\r
+.cm-s-abcdef .CodeMirror-gutters { background: #555; border-right: 2px solid #314151; }\r
+.cm-s-abcdef .CodeMirror-guttermarker { color: #222; }\r
+.cm-s-abcdef .CodeMirror-guttermarker-subtle { color: azure; }\r
+.cm-s-abcdef .CodeMirror-linenumber { color: #FFFFFF; }\r
+.cm-s-abcdef .CodeMirror-cursor { border-left: 1px solid #00FF00; }\r
+\r
+.cm-s-abcdef span.cm-keyword { color: darkgoldenrod; font-weight: bold; }\r
+.cm-s-abcdef span.cm-atom { color: #77F; }\r
+.cm-s-abcdef span.cm-number { color: violet; }\r
+.cm-s-abcdef span.cm-def { color: #fffabc; }\r
+.cm-s-abcdef span.cm-variable { color: #abcdef; }\r
+.cm-s-abcdef span.cm-variable-2 { color: #cacbcc; }\r
+.cm-s-abcdef span.cm-variable-3, .cm-s-abcdef span.cm-type { color: #def; }\r
+.cm-s-abcdef span.cm-property { color: #fedcba; }\r
+.cm-s-abcdef span.cm-operator { color: #ff0; }\r
+.cm-s-abcdef span.cm-comment { color: #7a7b7c; font-style: italic;}\r
+.cm-s-abcdef span.cm-string { color: #2b4; }\r
+.cm-s-abcdef span.cm-meta { color: #C9F; }\r
+.cm-s-abcdef span.cm-qualifier { color: #FFF700; }\r
+.cm-s-abcdef span.cm-builtin { color: #30aabc; }\r
+.cm-s-abcdef span.cm-bracket { color: #8a8a8a; }\r
+.cm-s-abcdef span.cm-tag { color: #FFDD44; }\r
+.cm-s-abcdef span.cm-attribute { color: #DDFF00; }\r
+.cm-s-abcdef span.cm-error { color: #FF0000; }\r
+.cm-s-abcdef span.cm-header { color: aquamarine; font-weight: bold; }\r
+.cm-s-abcdef span.cm-link { color: blueviolet; }\r
+\r
+.cm-s-abcdef .CodeMirror-activeline-background { background: #314151; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/ambiance-mobile.css b/js/ckeditor/plugins/codemirror/theme/ambiance-mobile.css
new file mode 100644 (file)
index 0000000..374818a
--- /dev/null
@@ -0,0 +1,5 @@
+.cm-s-ambiance.CodeMirror {\r
+  -webkit-box-shadow: none;\r
+  -moz-box-shadow: none;\r
+  box-shadow: none;\r
+}\r
diff --git a/js/ckeditor/plugins/codemirror/theme/ambiance.css b/js/ckeditor/plugins/codemirror/theme/ambiance.css
new file mode 100644 (file)
index 0000000..4d25e3f
--- /dev/null
@@ -0,0 +1,74 @@
+/* ambiance theme for codemirror */\r
+\r
+/* Color scheme */\r
+\r
+.cm-s-ambiance .cm-header { color: blue; }\r
+.cm-s-ambiance .cm-quote { color: #24C2C7; }\r
+\r
+.cm-s-ambiance .cm-keyword { color: #cda869; }\r
+.cm-s-ambiance .cm-atom { color: #CF7EA9; }\r
+.cm-s-ambiance .cm-number { color: #78CF8A; }\r
+.cm-s-ambiance .cm-def { color: #aac6e3; }\r
+.cm-s-ambiance .cm-variable { color: #ffb795; }\r
+.cm-s-ambiance .cm-variable-2 { color: #eed1b3; }\r
+.cm-s-ambiance .cm-variable-3, .cm-s-ambiance .cm-type { color: #faded3; }\r
+.cm-s-ambiance .cm-property { color: #eed1b3; }\r
+.cm-s-ambiance .cm-operator { color: #fa8d6a; }\r
+.cm-s-ambiance .cm-comment { color: #555; font-style:italic; }\r
+.cm-s-ambiance .cm-string { color: #8f9d6a; }\r
+.cm-s-ambiance .cm-string-2 { color: #9d937c; }\r
+.cm-s-ambiance .cm-meta { color: #D2A8A1; }\r
+.cm-s-ambiance .cm-qualifier { color: yellow; }\r
+.cm-s-ambiance .cm-builtin { color: #9999cc; }\r
+.cm-s-ambiance .cm-bracket { color: #24C2C7; }\r
+.cm-s-ambiance .cm-tag { color: #fee4ff; }\r
+.cm-s-ambiance .cm-attribute { color: #9B859D; }\r
+.cm-s-ambiance .cm-hr { color: pink; }\r
+.cm-s-ambiance .cm-link { color: #F4C20B; }\r
+.cm-s-ambiance .cm-special { color: #FF9D00; }\r
+.cm-s-ambiance .cm-error { color: #AF2018; }\r
+\r
+.cm-s-ambiance .CodeMirror-matchingbracket { color: #0f0; }\r
+.cm-s-ambiance .CodeMirror-nonmatchingbracket { color: #f22; }\r
+\r
+.cm-s-ambiance div.CodeMirror-selected { background: rgba(255, 255, 255, 0.15); }\r
+.cm-s-ambiance.CodeMirror-focused div.CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\r
+.cm-s-ambiance .CodeMirror-line::selection, .cm-s-ambiance .CodeMirror-line > span::selection, .cm-s-ambiance .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\r
+.cm-s-ambiance .CodeMirror-line::-moz-selection, .cm-s-ambiance .CodeMirror-line > span::-moz-selection, .cm-s-ambiance .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\r
+\r
+/* Editor styling */\r
+\r
+.cm-s-ambiance.CodeMirror {\r
+  line-height: 1.40em;\r
+  color: #E6E1DC;\r
+  background-color: #202020;\r
+  -webkit-box-shadow: inset 0 0 10px black;\r
+  -moz-box-shadow: inset 0 0 10px black;\r
+  box-shadow: inset 0 0 10px black;\r
+}\r
+\r
+.cm-s-ambiance .CodeMirror-gutters {\r
+  background: #3D3D3D;\r
+  border-right: 1px solid #4D4D4D;\r
+  box-shadow: 0 10px 20px black;\r
+}\r
+\r
+.cm-s-ambiance .CodeMirror-linenumber {\r
+  text-shadow: 0px 1px 1px #4d4d4d;\r
+  color: #111;\r
+  padding: 0 5px;\r
+}\r
+\r
+.cm-s-ambiance .CodeMirror-guttermarker { color: #aaa; }\r
+.cm-s-ambiance .CodeMirror-guttermarker-subtle { color: #111; }\r
+\r
+.cm-s-ambiance .CodeMirror-cursor { border-left: 1px solid #7991E8; }\r
+\r
+.cm-s-ambiance .CodeMirror-activeline-background {\r
+  background: none repeat scroll 0% 0% rgba(255, 255, 255, 0.031);\r
+}\r
+\r
+.cm-s-ambiance.CodeMirror,\r
+.cm-s-ambiance .CodeMirror-gutters {\r
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAQAAAAHUWYVAABFFUlEQVQYGbzBCeDVU/74/6fj9HIcx/FRHx9JCFmzMyGRURhLZIkUsoeRfUjS2FNDtr6WkMhO9sm+S8maJfu+Jcsg+/o/c+Z4z/t97/vezy3z+z8ekGlnYICG/o7gdk+wmSHZ1z4pJItqapjoKXWahm8NmV6eOTbWUOp6/6a/XIg6GQqmenJ2lDHyvCFZ2cBDbmtHA043VFhHwXxClWmeYAdLhV00Bd85go8VmaFCkbVkzlQENzfBDZ5gtN7HwF0KDrTwJ0dypSOzpaKCMwQHKTIreYIxlmhXTzTWkVm+LTynZhiSBT3RZQ7aGfjGEd3qyXQ1FDymqbKxpspERQN2MiRjNZlFFQXfCNFm9nM1zpAsoYjmtRTc5ajwuaXc5xrWskT97RaKzAGe5ARHhVUsDbjKklziiX5WROcJwSNCNI+9w1Jwv4Zb2r7lCMZ4oq5C0EdTx+2GzNuKpJ+iFf38JEWkHJn9DNF7mmBDITrWEg0VWL3pHU20tSZnuqWu+R3BtYa8XxV1HO7GyD32UkOpL/yDloINFTmvtId+nmAjxRw40VMwVKiwrKLE4bK5UOVntYwhOcSSXKrJHKPJedocpGjVz/ZMIbnYUPB10/eKCrs5apqpgVmWzBYWpmtKHecJPjaUuEgRDDaU0oZghCJ6zNMQ5ZhDYx05r5v2muQdM0EILtXUsaKiQX9WMEUotagQzFbUNN6NUPC2nm5pxEWGCjMc3GdJHjSU2kORLK/JGSrkfGEIjncU/CYUnOipoYemwj8tST9NsJmB7TUVXtbUtXATJVZXBMvYeTXJfobgJUPmGMP/yFaWonaa6BcFO3nqcIqCozSZoZoSr1g4zJOzuyGnxTEX3lUEJ7WcZgme8ddaWvWJo2AJR9DZU3CUIbhCSG6ybSwN6qtJVnCU2svDTP2ZInOw2cBTrqtQahtNZn9NcJ4l2NaSmSkkP1noZWnVwkLmdUPOwLZEwy2Z3S3R+4rIG9hcbpPXHFVWcQdZkn2FOta3cKWQnNRC5g1LsJah4GCzSVsKnCOY5OAFRTBekyyryeyilhFKva75r4Mc0aWanGEaThcy31s439KKxTzJYY5WTHPU1FtIHjQU3Oip4xlNzj/lBw23dYZVliQa7WAXf4shetcQfatI+jWRDBPmyNeW6A1P5kdDgyYJlba0BIM8BZu1JfrFwItyjcAMR3K0BWOIrtMEXyhyrlVEx3ui5dUBjmB/Q3CXW85R4mBD0s7B+4q5tKUjOlb9qqmhi5AZ6GFIC5HXtOobdYGlVdMVbNJ8toNTFcHxnoL+muBagcctjWnbNMuR00uI7nQESwg5q2qqrKWIfrNUmeQocY6HuyxJV02wj36w00yhpmUFenv4p6fUkZYqLyuinx2RGOjhCXYyJF84oiU00YMOOhhquNdfbOB7gU88pY4xJO8LVdp6/q2voeB4R04vIdhSE40xZObx1HGGJ/ja0LBthFInKaLPPFzuCaYaoj8JjPME8yoyxo6zlBqkiUZYgq00OYMswbWO5NGmq+xhipxHLRW29ARjNKXO0wRnear8XSg4XFPLKEPUS1GqvyLwiuBUoa7zpZ0l5xxFwWmWZC1H5h5FwU8eQ7K+g8UcVY6TMQreVQT/8uQ8Z+ALIXnSEa2pYZQneE9RZbSBNYXfWYJzW/h/4j4Dp1tYVcFIC5019Vyi4ThPqSFCzjGWaHQTBU8q6vrVwgxP9Lkm840imWKpcLCjYTtrKuwvsKSnrvHCXGkSMk9p6lhckfRpIeis+N2PiszT+mFLspyGleUhDwcLrZqmyeylxwjBcKHEapqkmyangyLZRVOijwOtCY5SsG5zL0OwlCJ4y5KznF3EUNDDrinwiyLZRzOXtlBbK5ITHFGLp8Q0R6ab6mS7enI2cFrxOyHvOCFaT1HThS1krjCwqWeurCkk+willhCC+RSZnRXBiZaC5RXRIZYKp2lyfrHwiKPKR0JDzrdU2EFgpidawlFDR6FgXUMNa+g1FY3bUQh2cLCwosRdnuQTS/S+JVrGLeWIvtQUvONJxlqSQYYKpwoN2kaocLjdVsis4Mk80ESF2YpSkzwldjHkjFCUutI/r+EHDU8oCs6yzL3PhWiEooZdFMkymlas4AcI3KmoMMNSQ3tHzjGWCrcJJdYyZC7QFGwjRL9p+MrRkAGWzIaWCn9W0F3TsK01c2ZvQw0byvxuQU0r1lM0qJO7wW0kRIMdDTtXEdzi4VIh+EoIHm0mWtAtpCixlabgn83fKTI7anJe9ST7WIK1DMGpQmYeA58ImV6ezOGOzK2Kgq01pd60cKWiUi9Lievb/0vIDPHQ05Kzt4ddPckQBQtoaurjyHnek/nKzpQLrVgKPjIkh2v4uyezpv+Xoo7fPFXaGFp1vaLKxQ4uUpQQS5VuQs7BCq4xRJv7fwpVvvFEB3j+620haOuocqMhWd6TTPAEx+mdFNGHdranFe95WrWmIvlY4F1Dle2ECgc6cto7SryuqGGGha0tFQ5V53migUKmg6XKAo4qS3mik+0OZpAhOLeZKicacgaYcyx5hypYQE02ZA4xi/pNhOQxR4klNKyqacj+mpxnLTnnGSo85++3ZCZq6lrZkXlGEX3o+C9FieccJbZWVFjC0Yo1FZnJhoYMFoI1hEZ9r6hwg75HwzBNhbZCdJEfJwTPGzJvaKImw1yYX1HDAmpXR+ZJQ/SmgqMNVQb5vgamGwLtt7VwvP7Qk1xpiM5x5Cyv93E06MZmgs0Nya2azIKOYKCGBQQW97RmhKNKF02JZqHEJ4o58qp7X5EcZmc56trXEqzjCBZ1MFGR87Ql2tSTs6CGxS05PTzRQorkbw7aKoKXFDXsYW42VJih/q+FP2BdTzDTwVqOYB13liM50vG7wy28qagyuIXMeQI/Oqq8bcn5wJI50xH00CRntyfpL1T4hydYpoXgNiFzoIUTDZnLNRzh4TBHwbYGDvZkxmlyJloyr6tRihpeUG94GnKtIznREF0tzJG/OOr73JBcrSh1k6WuTprgLU+mnSGnv6Zge0NNz+kTDdH8nuAuTdJDCNb21LCiIuqlYbqGzT3RAoZofQfjFazkqeNWdYaGvYTM001EW2oKPvVk1ldUGSgUtHFwjKM1h9jnFcmy5lChoLNaQMGGDsYbKixlaMBmmsx1QjCfflwTfO/gckW0ruZ3jugKR3R5W9hGUWqCgxuFgsuaCHorotGKzGaeZB9DMsaTnKCpMtwTvOzhYk0rdrArKCqcaWmVk1+F372ur1YkKxgatI8Qfe1gIX9wE9FgS8ESmuABIXnRUbCapcKe+nO7slClSZFzpV/LkLncEb1qiO42fS3R855Su2mCLh62t1SYZZYVmKwIHjREF2uihTzB20JOkz7dkxzYQnK0UOU494wh+VWRc6Un2kpTaVgLDFEkJ/uhzRcI0YKGgpGWOlocBU/a4fKoJ/pEaNV6jip3+Es9VXY078rGnmAdf7t9ylPXS34RBSuYPs1UecZTU78WanhBCHpZ5sAoTz0LGZKjPf9TRypqWEiTvOFglL1fCEY3wY/++rbk7C8bWebA6p6om6PgOL2kp44TFJlVNBXae2rqqdZztOJpT87GQsE9jqCPIe9VReZuQ/CIgacsyZdCpIScSYqcZk8r+nsyCzhyfhOqHGOIvrLknC8wTpFcaYiGC/RU1NRbUeUpocQOnkRpGOrIOcNRx+1uA0UrzhSSt+VyS3SJpnFWkzNDqOFGIWcfR86DnmARTQ1HKIL33ExPiemeOhYSSjzlSUZZuE4TveoJLnBUOFof6KiysCbnAEcZgcUNTDOwkqWu3RWtmGpZwlHhJENdZ3miGz0lJlsKnjbwqSHQjpxnFDlTLLwqJPMZMjd7KrzkSG7VsxXBZE+F8YZkb01Oe00yyRK9psh5SYh29ySPKBo2ylNht7ZkZnsKenjKNJu9PNEyZpaCHv4Kt6RQsLvAVp7M9kIimmCUwGeWqLMmGuIotYMmWNpSahkhZw9FqZsVnKJhsjAHvtHMsTM9fCI06Dx/u3vfUXCqfsKRc4oFY2jMsoo/7DJDwZ1CsIKnJu+J9ldkpmiCxQx1rWjI+T9FwcWWzOuaYH0Hj7klNRVWEQpmaqosakiGNTFHdjS/qnUdmf0NJW5xsL0HhimCCZZSRzmSPTXJQ4aaztAwtZnoabebJ+htCaZ7Cm535ByoqXKbX1WRc4Eh2MkRXWzImVc96Cj4VdOKVxR84VdQsIUM8Psoou2byVHyZFuq7O8otbSQ2UAoeEWTudATLGSpZzVLlXVkPU2Jc+27lsw2jmg5T5VhbeE3BT083K9WsTTkFU/Osi0rC5lRlpwRHUiesNS0sOvmqGML1aRbPAxTJD9ZKtxuob+hhl8cwYGWpJ8nub7t5p6coYbMovZ1BTdaKn1jYD6h4GFDNFyT/Kqe1XCXphXHOKLZmuRSRdBPEfVUXQzJm5YGPGGJdvAEr7hHNdGZnuBvrpciGmopOLf5N0uVMy0FfYToJk90uUCbJupaVpO53UJXR2bVpoU00V2KOo4zMFrBd0Jtz2pa0clT5Q5L8IpQ177mWQejPMEJhuQjS10ref6HHjdEhy1P1EYR7GtO0uSsKJQYLiTnG1rVScj5lyazpqWGl5uBbRWl7m6ixGOOnEsMJR7z8J0n6KMnCdxhiNYQCoZ6CmYLnO8omC3MkW3bktlPmEt/VQQHejL3+dOE5FlPdK/Mq8hZxxJtLyRrepLThYKbLZxkSb5W52vYxNOaOxUF0yxMUPwBTYqCzy01XayYK0sJyWBLqX0MwU5CzoymRzV0EjjeUeLgDpTo6ij42ZAzvD01dHUUTPLU96MdLbBME8nFBn7zJCMtJcZokn8YoqU0FS5WFKyniHobguMcmW8N0XkWZjkyN3hqOMtS08r+/xTBwpZSZ3qiVRX8SzMHHjfUNFjgHEPmY9PL3ykEzxkSre/1ZD6z/NuznuB0RcE1TWTm9zRgfUWVJiG6yrzgmWPXC8EAR4Wxhlad0ZbgQyEz3pG5RVEwwDJH2mgKpjcTiCOzn1lfUWANFbZ2BA8balnEweJC9J0iuaeZoI+ippFCztEKVvckR2iice1JvhVytrQwUAZpgsubCPaU7xUe9vWnaOpaSBEspalykhC9bUlOMpT42ZHca6hyrqKmw/wMR8H5ZmdFoBVJb03O4UL0tSNnvIeRmkrLWqrs78gcrEn2tpcboh0UPOW3UUR9PMk4T4nnNKWmCjlrefhCwxRNztfmIQVdDElvS4m1/WuOujoZCs5XVOjtKPGokJzsYCtFYoWonSPT21DheU/wWhM19FcElwqNGOsp9Q8N/cwXaiND1MmeL1Q5XROtYYgGeFq1aTMsoMmcrKjQrOFQTQ1fmBYhmW6o8Jkjc7iDJRTBIo5kgJD5yMEYA3srCg7VFKwiVJkmRCc5ohGOKhsYMn/XBLdo5taZjlb9YAlGWRimqbCsoY7HFAXLa5I1HPRxMMsQDHFkWtRNniqT9UEeNjcE7RUlrCJ4R2CSJuqlKHWvJXjAUNcITYkenuBRB84TbeepcqTj3zZyFJzgYQdHnqfgI0ddUwS6GqWpsKWhjq9cV0vBAEMN2znq+EBfIWT+pClYw5xsTlJU6GeIBsjGmmANTzJZiIYpgrM0Oa8ZMjd7NP87jxhqGOhJlnQtjuQpB+8aEE00wZFznSJPyHxgH3HkPOsJFvYk8zqCHzTs1BYOa4J3PFU+UVRZxlHDM4YavlNUuMoRveiZA2d7grMNc2g+RbSCEKzmgYsUmWmazFJyoiOZ4KnyhKOGRzWJa0+moyV4TVHDzn51Awtqaphfk/lRQ08FX1iiqxTB/kLwd0VynKfEvI6cd4XMV5bMhZ7gZUWVzYQ6Nm2BYzxJbw3bGthEUUMfgbGeorae6DxHtJoZ6alhZ0+ytiVoK1R4z5PTrOECT/SugseEOlb1MMNR4VRNcJy+V1Hg9ONClSZFZjdHlc6W6FBLdJja2MC5hhpu0DBYEY1TFGwiFAxRRCsYkiM9JRb0JNMVkW6CZYT/2EiTGWmo8k+h4FhDNE7BvppoTSFnmCV5xZKzvcCdDo7VVPnIU+I+Rc68juApC90MwcFCsJ5hDqxgScYKreruyQwTqrzoqDCmhWi4IbhB0Yrt3RGa6GfDv52rKXWhh28dyZaWUvcZeMTBaZoSGyiCtRU5J8iviioHaErs7Jkj61syVzTTgOcUOQ8buFBTYWdL5g3T4qlpe0+wvD63heAXRfCCIed9RbCsp2CiI7raUOYOTU13N8PNHvpaGvayo4a3LLT1lDrVEPT2zLUlheB1R+ZTRfKWJ+dcocLJfi11vyJ51lLqJ0WD7tRwryezjiV5W28uJO9qykzX8JDe2lHl/9oyBwa2UMfOngpXCixvKdXTk3wrsKmiVYdZIqsoWEERjbcUNDuiaQomGoIbFdEHmsyWnuR+IeriKDVLnlawlyNHKwKlSU631PKep8J4Q+ayjkSLKYLhalNHlYvttb6fHm0p6OApsZ4l2VfdqZkjuysy6ysKLlckf1KUutCTs39bmCgEyyoasIWlVaMF7mgmWtBT8Kol5xpH9IGllo8cJdopcvZ2sImlDmMIbtDk3KIpeNiS08lQw11NFPTwVFlPP6pJ2gvRfI7gQUfmNAtf6Gs0wQxDsKGlVBdF8rCa3jzdwMaGHOsItrZk7hAyOzpK9VS06j5F49b0VNGOOfKs3lDToMsMBe9ZWtHFEgxTJLs7qrygKZjUnmCYoeAqeU6jqWuLJup4WghOdvCYJnrSkSzoyRkm5M2StQwVltPkfCAk58tET/CSg+8MUecmotMEnhBKfWBIZsg2ihruMJQaoIm+tkTLKEqspMh00w95gvFCQRtDwTT1gVDDSEVdlwqZfxoQRbK0g+tbiBZxzKlpnpypejdDwTaeOvorMk/IJE10h9CqRe28hhLbe0pMsdSwv4ZbhKivo2BjDWfL8UKJgeavwlwb5KlwhyE4u4XkGE2ytZCznKLCDZZq42VzT8HLCrpruFbIfOIINmh/qCdZ1ZBc65kLHR1Bkyf5zn6pN3SvGKIlFNGplhrO9QSXanLOMQTLCa0YJCRrCZm/CZmrLTm7WzCK4GJDiWUdFeYx1LCFg3NMd0XmCuF3Y5rITLDUsYS9zoHVzwnJoYpSTQoObyEzr4cFBNqYTopoaU/wkyLZ2lPhX/5Y95ulxGTV7KjhWrOZgl8MyUUafjYraNjNU1N3IWcjT5WzWqjwtoarHSUObGYO3GCJZpsBlnJGPd6ZYLyl1GdCA2625IwwJDP8GUKymbzuyPlZlvTUsaUh5zFDhRWFzPKKZLAlWdcQbObgF9tOqOsmB1dqcqYJmWstFbZRRI9poolmqiLnU0POvxScpah2iSL5UJNzgScY5+AuIbpO0YD3NCW+dLMszFSdFCWGqG6eVq2uYVNDdICGD6W7EPRWZEY5gpsE9rUkS3mijzzJnm6UpUFXG1hCUeVoS5WfNcFpblELL2qqrCvMvRfd45oalvKU2tiQ6ePJOVMRXase9iTtLJztPxJKLWpo2CRDcJwn2sWSLKIO1WQWNTCvpVUvOZhgSC40JD0dOctaSqzkCRbXsKlb11Oip6PCJ0IwSJM31j3akRxlP7Rwn6aGaUL0qiLnJkvB3xWZ2+Q1TfCwpQH3G0o92UzmX4o/oJNQMMSQc547wVHhdk+VCw01DFYEnTxzZKAm74QmeNNR1w6WzEhNK15VJzuCdxQ53dRUDws5KvwgBMOEgpcVNe0hZI6RXT1Jd0cyj5nsaEAHgVmGaJIlWdsc5Ui2ElrRR6jrRAttNMEAIWrTDFubkZaok7/AkzfIwfuWVq0jHzuCK4QabtLUMVPB3kJ0oyHTSVFlqMALilJf2Rf8k5aaHtMfayocLBS8L89oKoxpJvnAkDPa0qp5DAUTHKWmCcnthlou8iCKaFFLHWcINd1nyIwXqrSxMNmSs6KmoL2QrKuWtlQ5V0120xQ5vRyZS1rgFkWwhiOwiuQbR0OOVhQM9iS3tiXp4RawRPMp5tDletOOBL95MpM01dZTBM9pkn5qF010rIeHFcFZhmSGpYpTsI6nwhqe5C9ynhlpp5ophuRb6WcJFldkVnVEwwxVfrVkvnWUuNLCg5bgboFHPDlDPDmnK7hUrWiIbjadDclujlZcaokOFup4Ri1kacV6jmrrK1hN9bGwpKEBQ4Q6DvIUXOmo6U5LqQM6EPyiKNjVkPnJkDPNEaxhiFay5ExW1NXVUGqcpYYdPcGiCq7z/TSlbhL4pplWXKd7NZO5QQFrefhRQW/NHOsqcIglc4UhWklR8K0QzbAw08CBDnpbgqXdeD/QUsM4RZXDFBW6WJKe/mFPdH0LtBgiq57wFLzlyQzz82qYx5D5WJP5yVJDW01BfyHnS6HKO/reZqId1WGa4Hkh2kWodJ8i6KoIPlAj2hPt76CzXsVR6koPRzWTfKqIentatYpQw2me4AA3y1Kind3SwoOKZDcFXTwl9tWU6mfgRk9d71sKtlNwrjnYw5tC5n5LdKiGry3JKNlHEd3oaMCFHrazBPMp/uNJ+V7IudcSbeOIdjUEdwl0VHCOZo5t6YluEuaC9mQeMgSfOyKnYGFHcIeQ84yQWbuJYJpZw5CzglDH7gKnWqqM9ZTaXcN0TeYhR84eQtJT76JJ1lREe7WnnvsMmRc9FQ7SBBM9mV3lCUdmHk/S2RAMt0QjFNFqQpWjDPQ01DXWUdDBkXziKPjGEP3VP+zIWU2t7im41FOloyWzn/L6dkUy3VLDaZ6appgDLHPjJEsyvJngWEPUyVBiAaHCTEXwrLvSEbV1e1gKJniicWorC1MUrVjB3uDhJE/wgSOzk1DXpk0k73qCM8xw2UvD5kJmDUfOomqMpWCkJRlvKXGmoeBm18USjVIk04SClxTB6YrgLAPLWYK9HLUt5cmc0vYES8GnTeRc6skZbQkWdxRsIcyBRzx1DbTk9FbU0caTPOgJHhJKnOGIVhQqvKmo0llRw9sabrZkDtdg3PqaKi9oatjY8B+G371paMg6+mZFNNtQ04mWBq3rYLOmtWWQp8KJnpy9DdFensyjdqZ+yY40VJlH8wcdLzC8PZnvHMFUTZUrDTkLyQaGus5X5LzpYAf3i+e/ZlhqGqWhh6Ou6xTR9Z6oi5AZZtp7Mj2EEm8oSpxiYZCHU/1fbGdNNNRRoZMhmilEb2gqHOEJDtXkHK/JnG6IrvbPCwV3NhONVdS1thBMs1T4QOBcTWa2IzhMk2nW5Kyn9tXUtpv9RsG2msxk+ZsQzRQacJncpgke0+T8y5Fzj8BiGo7XlJjaTIlpQs7KFjpqGnKuoyEPeIKnFMkZHvopgh81ySxNFWvJWcKRs70j2FOT012IllEEO1n4pD1513Yg2ssQPOThOkvyrqHUdEXOSEsihmBbTbKX1kLBPWqWkLOqJbjB3GBIZmoa8qWl4CG/iZ7oiA72ZL7TJNeZUY7kFQftDcHHluBzRbCegzMtrRjVQpX2lgoPKKLJAkcbMl01XK2p7yhL8pCBbQ3BN2avJgKvttcrWDK3CiUOVxQ8ZP+pqXKyIxnmBymCg5vJjNfkPK4+c8cIfK8ocVt7kmfd/I5SR1hKvCzUtb+lhgc00ZaO6CyhIQP1Uv4yIZjload72PXX0OIJvnFU+0Zf6MhsJwTfW0r0UwQfW4LNLZl5HK261JCZ4qnBaAreVAS3WrjV0LBnNDUNNDToCEeFfwgcb4gOEqLRhirWkexrCEYKVV711DLYEE1XBEsp5tpTGjorkomKYF9FDXv7fR3BGwbettSxnyL53MBPjsxDZjMh+VUW9NRxq1DhVk+FSxQcaGjV9Pawv6eGByw5qzoy7xk4RsOShqjJwWKe/1pEEfzkobeD/dQJmpqedcyBTy2sr4nGNRH0c0SPWTLrqAc0OQcb/gemKgqucQT7ySWKCn2EUotoCvpZct7RO2sy/QW0IWcXd7pQRQyZVwT2USRO87uhjioTLKV2brpMUcMQRbKH/N2T+UlTpaMls6cmc6CCNy3JdYYSUzzJQ4oSD3oKLncULOiJvjBEC2oqnCJkJluCYy2ZQ5so9YYlZ1VLlQU1mXEW1jZERwj/MUSRc24TdexlqLKfQBtDTScJUV8FszXBEY5ktpD5Ur9hYB4Nb1iikw3JoYpkKX+RodRKFt53MMuRnKSpY31PwYaGaILh3wxJGz9TkTPEETxoCWZrgvOlmyMzxFEwVJE5xZKzvyJ4WxEc16Gd4Xe3Weq4XH2jKRikqOkGQ87hQnC7wBmGYLAnesX3M+S87eFATauuN+Qcrh7xIxXJbUIdMw3JGE3ylCWzrieaqCn4zhGM19TQ3z1oH1AX+pWEqIc7wNGAkULBo/ZxRaV9NNyh4Br3rCHZzbzmSfawBL0dNRwpW1kK9mxPXR9povcdrGSZK9c2k0xwFGzjuniCtRSZCZ6ccZ7gaktmgAOtKbG/JnOkJrjcQTdFMsxRQ2cLY3WTIrlCw1eWKn8R6pvt4GFDso3QoL4a3nLk3G6JrtME3dSenpx7PNFTmga0EaJTLQ061sEeQoWXhSo9LTXsaSjoJQRXeZLtDclbCrYzfzHHeaKjHCVOUkQHO3JeEepr56mhiyaYYKjjNU+Fed1wS5VlhWSqI/hYUdDOkaxiKehoyOnrCV5yBHtbWFqTHCCwtpDcYolesVR5yUzTZBb3RNMd0d6WP+SvhuBmRcGxnuQzT95IC285cr41cLGQ6aJJhmi4TMGempxeimBRQw1tFKV+8jd6KuzoSTqqDxzRtpZkurvKEHxlqXKRIjjfUNNXQsNOsRScoWFLT+YeRZVD3GRN0MdQcKqQjHDMrdGGVu3iYJpQx3WGUvfbmxwFfR20WBq0oYY7LMFhhgYtr8jpaEnaOzjawWWaTP8mMr0t/EPDPoqcnxTBI5o58L7uoWnMrpoqPwgVrlAUWE+V+TQl9rawoyP6QGAlQw2TPRX+YSkxyBC8Z6jhHkXBgQL7WII3DVFnRfCrBfxewv9D6xsyjys4VkhWb9pUU627JllV0YDNHMku/ldNMMXDEo4aFnAkk4U6frNEU4XgZUPmEKHUl44KrzmYamjAbh0JFvGnaTLPu1s9jPCwjFpYiN7z1DTOk/nc07CfDFzmCf7i+bfNHXhDtLeBXzTBT5rkMvWOIxpl4EMh2LGJBu2syDnAEx2naEhHDWMMzPZEhygyS1mS5RTJr5ZkoKbEUoYqr2kqdDUE8ztK7OaIntJkFrIECwv8LJTaVx5XJE86go8dFeZ3FN3rjabCAYpoYEeC9zzJVULBbmZhDyd7ko09ydpNZ3nm2Kee4FPPXHnYEF1nqOFEC08LUVcDvYXkJHW8gTaKCk9YGOeIJhqiE4ToPEepdp7IWFjdwnWaufGMwJJCMtUTTBBK9BGCOy2tGGrJTHIwyEOzp6aPzNMOtlZkDvcEWpP5SVNhfkvDxhmSazTJXYrM9U1E0xwFVwqZQwzJxw6+kGGGUj2FglGGmnb1/G51udRSMNlTw6GGnCcUwVcOpmsqTHa06o72sw1RL02p9z0VbnMLOaIX3QKaYKSCFQzBKEUNHTSc48k53RH9wxGMtpQa5KjjW0W0n6XCCCG4yxNNdhQ4R4l1Ff+2sSd6UFHiIEOyqqFgT01mEUMD+joy75jPhOA+oVVLm309FR4yVOlp4RhLiScNmSmaYF5Pw0STrOIoWMSR2UkRXOMp+M4SHW8o8Zoi6OZgjKOaFar8zZDzkWzvKOjkKBjmCXby8JahhjXULY4KlzgKLvAwxVGhvyd4zxB1d9T0piazmKLCVZY5sKiD0y2ZSYrkUEPUbIk+dlQ4SJHTR50k1DPaUWIdTZW9NJwnJMOECgd7ou/MnppMJ02O1VT4Wsh85MnZzcFTngpXGKo84qmwgKbCL/orR/SzJ2crA+t6Mp94KvxJUeIbT3CQu1uIdlQEOzlKfS3UMcrTiFmOuroocrZrT2AcmamOKg8YomeEKm/rlT2sociMaybaUlFhuqHCM2qIJ+rg4EcDFymiDSxzaHdPcpE62pD5kyM5SBMoA1PaUtfIthS85ig1VPiPPYXgYEMNk4Qq7TXBgo7oT57gPUdwgCHzhIVFPFU6OYJzHAX9m5oNrVjeE61miDrqQ4VSa1oiURTsKHC0IfjNwU2WzK6eqK8jWln4g15TVBnqmDteCJ501PGAocJhhqjZdtBEB6lnhLreFJKxmlKbeGrqLiSThVIbCdGzloasa6lpMQXHCME2boLpJgT7yWaemu6wBONbqGNVRS0PKIL7LckbjmQtR7K8I5qtqel+T/ChJTNIKLjdUMNIRyvOEko9YYl2cwQveBikCNawJKcLBbc7+JM92mysNvd/Fqp8a0k6CNEe7cnZrxlW0wQXaXjaktnRwNOGZKYiONwS7a1JVheq3WgJHlQUGKHKmp4KAxXR/ULURcNgoa4zhKSLpZR3kxRRb0NmD0OFn+UCS7CzI1nbP6+o4x47QZE5xRCt3ZagnYcvmpYQktXdk5YKXTzBC57kKEe0VVuiSYqapssMS3C9p2CKkHOg8B8Pa8p5atrIw3qezIWanMGa5HRDNF6RM9wcacl0N+Q8Z8hsIkSnaIIdHRUOEebAPy1zbCkhM062FCJtif7PU+UtoVXzWKqM1PxXO8cfdruhFQ/a6x3JKYagvVDhQEtNiyiiSQ7OsuRsZUku0CRNDs4Sog6KKjsZgk2bYJqijgsEenoKeniinRXBn/U3lgpPdyDZynQx8IiioMnCep5Ky8mjGs6Wty0l1hUQTcNWswS3WRp2kCNZwJG8omG8JphPUaFbC8lEfabwP7VtM9yoaNCAjpR41VNhrD9LkbN722v0CoZMByFzhaW+MyzRYEWFDQwN2M4/JiT76PuljT3VU/A36eaIThb+R9oZGOAJ9tewkgGvqOMNRWYjT/Cwu99Q8LqDE4TgbLWxJ1jaDDAERsFOFrobgjUsBScaguXU8kKm2RL19tRypSHnHNlHiIZqgufs4opgQdVdwxBNNFBR6kVFqb8ogimOzB6a6HTzrlDHEpYaxjiiA4TMQobkDg2vejjfwJGWmnbVFAw3H3hq2NyQfG7hz4aC+w3BbwbesG0swYayvpAs6++Ri1Vfzx93mFChvyN5xVHTS+0p9aqCAxyZ6ZacZyw5+7uuQkFPR9DDk9NOiE7X1PCYJVjVUqq7JlrHwWALF5nfHNGjApdpqgzx5OwilDhCiDYTgnc9waGW4BdLNNUQvOtpzDOWHDH8D7TR/A/85KljEQu3NREc4Pl/6B1Hhc8Umb5CsKMmGC9EPcxoT2amwHNCmeOEnOPbklnMkbOgIvO5UMOpQrS9UGVdt6iH/fURjhI/WOpaW9OKLYRod6HCUEdOX000wpDZQ6hwg6LgZfOqo1RfT/CrJzjekXOGhpc1VW71ZLbXyyp+93ILbC1kPtIEYx0FIx1VDrLoVzXRKRYWk809yYlC9ImcrinxtabKnzRJk3lAU1OLEN1j2zrYzr2myHRXJFf4h4QKT1qSTzTB5+ZNTzTRkAxX8FcLV2uS8eoQQ2aAkFzvCM72sJIcJET3WPjRk5wi32uSS9rfZajpWEvj9hW42F4o5NytSXYy8IKHay10VYdrcl4SkqscrXpMwyGOgtkajheSxdQqmpxP1L3t4R5PqasFnrQEjytq6qgp9Y09Qx9o4S1FzhUCn1kyHSzBWLemoSGvOqLNhZyBjmCaAUYpMgt4Ck7wBBMMwWKWgjsUwTaGVsxWC1mYoKiyqqeGKYqonSIRQ3KIkHO0pmAxTdBHkbOvfllfr+AA+7gnc50huVKYK393FOyg7rbPO/izI7hE4CnHHHnJ0ogNPRUGeUpsrZZTBJcrovUcJe51BPsr6GkJdhCCsZ6aTtMEb2pqWkqeVtDXE/QVggsU/Nl86d9RMF3DxvZTA58agu810RWawCiSzzXBeU3MMW9oyJUedvNEvQyNu1f10BSMddR1vaLCYpYa/mGocLSiYDcLbQz8aMn5iyF4xBNMs1P0QEOV7o5gaWGuzSeLue4tt3ro7y4Tgm4G/mopdZgl6q0o6KzJWE3mMksNr3r+a6CbT8g5wZNzT9O7fi/zpaOmnz3BRoqos+tv9zMbdpxsqDBOEewtJLt7cg5wtKKbvldpSzRRCD43VFheCI7yZLppggMVBS/KMAdHODJvOwq2NQSbKKKPLdFWQs7Fqo+mpl01JXYRgq8dnGLhTiFzqmWsUMdpllZdbKlyvSdYxhI9YghOtxR8LgSLWHK62mGGVoxzBE8LNWzqH9CUesQzFy5RQzTc56mhi6fgXEWwpKfE5Z7M05ZgZUPmo6auiv8YKzDYwWBLMErIbKHJvOwIrvEdhOBcQ9JdU1NHQ7CXn2XIDFBKU2WAgcX9UAUzDXWd5alwuyJ41Z9rjKLCL4aCp4WarhPm2rH+SaHUYE001JDZ2ZAzXPjdMpZWvC9wmqIB2lLhQ01D5jO06hghWMndbM7yRJMsoCj1vYbnFQVrW9jak3OlEJ3s/96+p33dEPRV5GxiqaGjIthUU6FFEZyqCa5qJrpBdzSw95IUnOPIrCUUjRZQFrbw5PR0R1qiYx3cb6nrWUMrBmmiBQxVHtTew5ICP/ip6g4hed/Akob/32wvBHsIOX83cI8hGeNeNPCIkPmXe8fPKx84OMSRM1MTdXSwjCZ4S30jVGhvqTRak/OVhgGazHuOCud5onEO1lJr6ecVyaOK6H7zqlBlIaHE0oroCgfvGJIdPcmfLNGLjpz7hZwZQpUbFME0A1cIJa7VNORkgfsMBatbKgwwJM9bSvQXeNOvbIjelg6WWvo5kvbKaJJNHexkKNHL9xRyFlH8Ti2riB5wVPhUk7nGkJnoCe428LR/wRGdYIlmWebCyxou1rCk4g/ShugBDX0V0ZQWkh0dOVsagkM0yV6OoLd5ye+pRlsCr0n+KiQrGuq5yJDzrTAXHtLUMduTDBVKrSm3eHL+6ijxhFDX9Z5gVU/wliHYTMiMFpKLNMEywu80wd3meoFmt6VbRMPenhrOc6DVe4pgXU8DnnHakLOIIrlF4FZPIw6R+zxBP0dyq6OOZ4Q5sLKCcz084ok+VsMMyQhNZmmBgX5xIXOEJTmi7VsGTvMTNdHHhpzdbE8Du2oKxgvBqQKdDDnTFOylCFaxR1syz2iqrOI/FEpNc3C6f11/7+ASS6l2inq2ciTrCCzgyemrCL5SVPjQkdPZUmGy2c9Sw9FtR1sS30RmsKPCS4rkIC/2U0MduwucYolGaPjKEyhzmiPYXagyWbYz8LWBDdzRimAXzxx4z8K9hpzlhLq+NiQ97HuKorMUfK/OVvC2JfiHUPCQI/q7J2gjK+tTDNxkCc4TMssqCs4TGtLVwQihyoAWgj9bosU80XGW6Ac9TJGziaUh5+hnFcHOnlaM1iRn29NaqGENTTTSUHCH2tWTeV0osUhH6psuVLjRUmGWhm6OZEshGeNowABHcJ2Bpy2ZszRcKkRXd2QuKVEeXnbfaEq825FguqfgfE2whlChSRMdron+LATTPQ2Z369t4B9C5gs/ylzv+CMmepIDPclFQl13W0rspPd1JOcbghGOEutqCv5qacURQl3dDKyvyJlqKXGPgcM9FfawJAMVmdcspcYKOZc4GjDYkFlK05olNMHyHn4zFNykyOxt99RkHlfwmiHo60l2EKI+mhreEKp080Tbug08BVPcgoqC5zWt+NLDTZ7oNSF51N1qie7Va3uCCwyZbkINf/NED6jzOsBdZjFN8oqG3wxVunqCSYYKf3EdhJyf9YWGf7tRU2oH3VHgPr1fe5J9hOgHd7xQ0y7qBwXr23aGErP0cm64JVjZwsOGqL+mhNgZmhJLW2oY4UhedsyBgzrCKrq7BmcpNVhR6jBPq64Vgi+kn6XE68pp8J5/+0wRHGOpsKenQn9DZntPzjRLZpDAdD2fnSgkG9tmIXnUwQ6WVighs7Yi2MxQ0N3CqYaCXkJ0oyOztMDJjmSSpcpvlrk0RMMOjmArQ04PRV1DO1FwhCVaUVPpKUM03JK5SxPsIWRu8/CGHi8UHChiqGFDTbSRJWeYUDDcH6vJWUxR4k1FXbMUwV6e4AJFXS8oMqsZKqzvYQ9DDQdZckY4aGsIhtlubbd2r3j4QBMoTamdPZk7O/Bf62lacZwneNjQoGcdVU7zJOd7ghsUHOkosagic6cnWc8+4gg285R6zZP5s1/LUbCKIznTwK36PkdwlOrl4U1LwfdCCa+IrvFkmgw1PCAUXKWo0sURXWcI2muKJlgyFzhynCY4RBOsqCjoI1R5zREco0n2Vt09BQtYSizgKNHfUmUrQ5UOCh51BFcLmY7umhYqXKQomOop8bUnWNNQcIiBcYaC6xzMNOS8JQQfeqKBmmglB+97ok/lfk3ygaHSyZaCRTzRxQo6GzLfa2jWBPepw+UmT7SQEJyiyRkhBLMVOfcoMjcK0eZChfUNzFAUzCsEN5vP/X1uP/n/aoMX+K+nw/Hjr/9xOo7j7Pju61tLcgvJpTWXNbfN5jLpi6VfCOviTktKlFusQixdEKWmEBUKNaIpjZRSSOXSgzaaKLdabrm1/9nZ+/f+vd/vz/v9+Xy+zZ7PRorYoZqyLrCwQdEAixxVOEXNNnjX2nUSRlkqGmWowk8lxR50JPy9Bo6qJXaXwNvREBvnThPEPrewryLhcAnj5WE15Fqi8W7R1sAuEu86S4ENikItFN4xkv9Af4nXSnUVcLiA9xzesFpivRRVeFKtsMRaKBhuSbjOELnAUtlSQUpXgdfB4Z1oSbnFEetbQ0IrAe+Y+pqnDcEJFj6S8LDZzZHwY4e3XONNlARraomNEt2bkvGsosA3ioyHm+6jCMbI59wqt4eeara28IzEmyPgoRaUOEDhTVdEJhmCoTWfC0p8aNkCp0oYqih2iqGi4yXeMkOsn4LdLLnmKfh/YogjNsPebeFGR4m9BJHLzB61XQ3BtpISfS2FugsK9FAtLWX1dCRcrCnUp44CNzuCowUZmxSRgYaE6Za0W2u/E7CVXCiI/UOR8aAm1+OSyE3mOUcwyc1zBBeoX1kiKy0Zfxck1Gsyulti11i83QTBF5Kg3pDQThFMVHiPSlK+0cSedng/VaS8bOZbtsBcTcZAR8JP5KeqQ1OYKAi20njdNNRpgnsU//K+JnaXJaGTomr7aYIphoRn9aeShJWKEq9LcozSF7QleEfDI5LYm5bgVkFkRwVDBCVu0DDIkGupo8TZBq+/pMQURYErJQmPKGKjNDkWOLx7Jd5QizdUweIaKrlP7SwJDhZvONjLkOsBBX9UpGxnydhXkfBLQ8IxgojQbLFnJf81JytSljclYYyEFyx0kVBvKWOFJmONpshGAcsduQY5giVNCV51eOdJYo/pLhbvM0uDHSevNKRcrKZIqnCtJeEsO95RoqcgGK4ocZcho1tTYtcZvH41pNQ7vA0WrhIfOSraIIntIAi+NXWCErdbkvrWwjRLrt0NKUdL6KSOscTOdMSOUtBHwL6OLA0vNSdynaWQEnCpIvKaIrJJEbvHkmuNhn6OjM8VkSGSqn1uYJCGHnq9I3aLhNME3t6GjIkO7xrNFumpyTNX/NrwX7CrIRiqqWijI9JO4d1iieykyfiposQIQ8YjjsjlBh6oHWbwRjgYJQn2NgSnNycmJAk3NiXhx44Sxykihxm8ybUwT1OVKySc7vi3OXVkdBJ4AyXBeksDXG0IhgtYY0lY5ahCD0ehborIk5aUWRJviMA7Xt5kyRjonrXENkm8yYqgs8VzgrJmClK20uMM3jRJ0FiQICQF9hdETlLQWRIb5ki6WDfWRPobvO6a4GP5mcOrNzDFELtTkONLh9dXE8xypEg7z8A9jkhrQ6Fhjlg/QVktJXxt4WXzT/03Q8IaQWSqIuEvloQ2mqC9Jfi7wRul4RX3pSPlzpoVlmCtI2jvKHCFhjcM3sN6lqF6HxnKelLjXWbwrpR4xzuCrTUZx2qq9oAh8p6ixCUGr78g8oyjRAtB5CZFwi80VerVpI0h+IeBxa6Zg6kWvpDHaioYYuEsRbDC3eOmC2JvGYLeioxGknL2UATNJN6hmtj1DlpLvDVmocYbrGCVJKOrg4X6DgddLA203BKMFngdJJFtFd7vJLm6KEpc5yjQrkk7M80SGe34X24nSex1Ra5Omgb71JKyg8SrU3i/kARKwWpH0kOGhKkObyfd0ZGjvyXlAkVZ4xRbYJ2irFMkFY1SwyWxr2oo4zlNiV+7zmaweFpT4kR3kaDAFW6xpSqzJay05FtYR4HmZhc9UxKbbfF2V8RG1MBmSaE+kmC6JnaRXK9gsiXhJHl/U0qM0WTcbyhwkYIvFGwjSbjfwhiJt8ZSQU+Bd5+marPMOkVkD0muxYLIfEuhh60x/J92itguihJSEMySVPQnTewnEm+620rTQEMsOfo4/kP/0ARvWjitlpSX7GxBgcMEsd3EEeYWvdytd+Saawi6aCIj1CkGb6Aj9rwhx16Cf3vAwFy5pyLhVonXzy51FDpdEblbkdJbUcEPDEFzQ8qNmhzzLTmmKWKbFCXeEuRabp6rxbvAtLF442QjQ+wEA9eL1xSR7Q0JXzlSHjJ4exq89yR0laScJ/FW6z4a73pFMEfDiRZvuvijIt86RaSFOl01riV2mD1UEvxGk/Geg5aWwGki1zgKPG9J2U8PEg8qYvMsZeytiTRXBMslCU8JSlxi8EabjwUldlDNLfzTUmCgxWsjqWCOHavYAqsknKFIO0yQ61VL5AVFxk6WhEaCAkdJgt9aSkzXlKNX2jEa79waYuc7gq0N3GDJGCBhoiTXUEPsdknCUE1CK0fwsiaylSF2uiDyO4XX3pFhNd7R4itFGc0k/ElBZwWvq+GC6szVeEoS/MZ+qylwpKNKv9Z469UOjqCjwlusicyTxG6VpNxcQ8IncoR4RhLbR+NdpGGmJWOcIzJGUuKPGpQg8rrG21dOMqQssJQ4RxH5jaUqnZuQ0F4Q+cjxLwPtpZbIAk3QTJHQWBE5S1BokoVtDd6lhqr9UpHSUxMcIYl9pojsb8h4SBOsMQcqvOWC2E8EVehqiJ1hrrAEbQxeK0NGZ0Gkq+guSRgniM23bIHVkqwx4hiHd7smaOyglyIyQuM978j4VS08J/A2G1KeMBRo4fBaSNhKUEZfQewVQ/C1I+MgfbEleEzCUw7mKXI0M3hd1EESVji8x5uQ41nxs1q4RMJCCXs7Iq9acpxn22oSDnQ/sJTxsCbHIYZiLyhY05TY0ZLIOQrGaSJDDN4t8pVaIrsqqFdEegtizc1iTew5Q4ayBDMUsQMkXocaYkc0hZua412siZ1rSXlR460zRJ5SlHGe5j801RLMlJTxtaOM3Q1pvxJ45zUlWFD7rsAbpfEm1JHxG0eh8w2R7QQVzBUw28FhFp5QZzq8t2rx2joqulYTWSuJdTYfWwqMFMcovFmSyJPNyLhE4E10pHzYjOC3huArRa571ZsGajQpQx38SBP5pyZB6lMU3khDnp0MBV51BE9o2E+TY5Ml2E8S7C0o6w1xvCZjf0HkVEHCzFoyNmqC+9wdcqN+Tp7jSDheE9ws8Y5V0NJCn2bk2tqSY4okdrEhx1iDN8cSudwepWmAGXKcJXK65H9to8jYQRH7SBF01ESUJdd0TayVInaWhLkOjlXE5irKGOnI6GSWGCJa482zBI9rCr0jyTVcEuzriC1vcr6mwFGSiqy5zMwxBH/TJHwjSPhL8+01kaaSUuMFKTcLEvaUePcrSmwn8DZrgikWb7CGPxkSjhQwrRk57tctmxLsb9sZvL9LSlyuSLlWkqOjwduo8b6Uv1DkmudIeFF2dHCgxVtk8dpIvHpBxhEOdhKk7OLIUSdJ+cSRY57B+0DgGUUlNfpthTfGkauzxrvTsUUaCVhlKeteTXCoJDCa2NOKhOmC4G1H8JBd4OBZReSRGkqcb/CO1PyLJTLB4j1q8JYaIutEjSLX8YKM+a6phdMsdLFUoV5RTm9JSkuDN8WcIon0NZMNZWh1q8C7SJEwV5HxrmnnTrf3KoJBlmCYI2ilSLlfEvlE4011NNgjgthzEua0oKK7JLE7HZHlEl60BLMVFewg4EWNt0ThrVNEVkkiTwpKXSWJzdRENgvKGq4IhjsiezgSFtsfCUq8qki5S1LRQeYQQ4nemmCkImWMw3tFUoUBZk4NOeZYEp4XRKTGa6wJjrWNHBVJR4m3FCnbuD6aak2WsMTh3SZImGCIPKNgsDpVwnsa70K31lCFJZYcwwSMFcQulGTsZuEaSdBXkPGZhu0FsdUO73RHjq8MPGGIfaGIbVTk6iuI3GFgucHrIQkmWSJdBd7BBu+uOryWAhY7+Lki9rK5wtEQzWwvtbqGhIMFwWRJsElsY4m9IIg9L6lCX0VklaPAYkfkZEGDnOWowlBJjtMUkcGK4Lg6EtoZInMUBVYLgn0UsdmCyCz7gIGHFfk+k1QwTh5We7A9x+IdJ6CvIkEagms0hR50eH9UnTQJ+2oiKyVlLFUE+8gBGu8MQ3CppUHesnjTHN4QB/UGPhCTHLFPHMFrCqa73gqObUJGa03wgbhHkrCfpEpzNLE7JDS25FMKhlhKKWKfCgqstLCPu1zBXy0J2ztwjtixBu8UTRn9LVtkmCN2iyFhtME70JHRQ1KVZXqKI/KNIKYMCYs1GUMEKbM1bKOI9LDXC7zbHS+bt+1MTWS9odA9DtrYtpbImQJ2VHh/lisEwaHqUk1kjKTAKknkBEXkbkdMGwq0dnhzLJF3NJH3JVwrqOB4Sca2hti75nmJN0WzxS6UxDYoEpxpa4htVlRjkYE7DZGzJVU72uC9IyhQL4i8YfGWSYLLNcHXloyz7QhNifmKSE9JgfGmuyLhc403Xm9vqcp6gXe3xuuv8F6VJNxkyTHEkHG2g0aKXL0MsXc1bGfgas2//dCONXiNLCX+5mB7eZIl1kHh7ajwpikyzlUUWOVOsjSQlsS+M0R+pPje/dzBXRZGO0rMtgQrLLG9VSu9n6CMXS3BhwYmSoIBhsjNBmZbgusE9BCPCP5triU4VhNbJfE+swSP27aayE8tuTpYYjtrYjMVGZdp2NpS1s6aBnKSHDsbKuplKbHM4a0wMFd/5/DmGyKrJSUaW4IBrqUhx0vyfzTBBLPIUcnZdrAkNsKR0sWRspumSns6Ch0v/qqIbBYUWKvPU/CFoyrDJGwSNFhbA/MlzKqjrO80hRbpKx0Jewsi/STftwGSlKc1JZyAzx05dhLEdnfQvhZOqiHWWEAHC7+30FuRcZUgaO5gpaIK+xsiHRUsqaPElTV40xQZQ107Q9BZE1nryDVGU9ZSQ47bmhBpLcYpUt7S+xuK/FiT8qKjwXYw5ypS2iuCv7q1gtgjhuBuB8LCFY5cUuCNtsQOFcT+4Ih9JX+k8Ea6v0iCIRZOtCT0Et00JW5UeC85Cg0ScK0k411HcG1zKtre3SeITBRk7WfwDhEvaYLTHP9le0m8By0JDwn4TlLW/aJOvGHxdjYUes+ScZigCkYQdNdEOhkiezgShqkx8ueKjI8lDfK2oNiOFvrZH1hS+tk7NV7nOmLHicGWEgubkXKdwdtZknCLJXaCpkrjZBtLZFsDP9CdxWsSr05Sxl6CMmoFbCOgryX40uDtamB7SVmXW4Ihlgpmq+00tBKUUa83WbjLUNkzDmY7cow1JDygyPGlhgGKYKz4vcV7QBNbJIgM11TUqZaMdwTeSguH6rOaw1JRKzaaGyxVm2EJ/uCIrVWUcZUkcp2grMsEjK+DMwS59jQk3Kd6SEq1d0S6uVmO4Bc1lDXTUcHjluCXEq+1OlBDj1pi9zgiXxnKuE0SqTXwhqbETW6RggMEnGl/q49UT2iCzgJvRwVXS2K/d6+ZkyUl7jawSVLit46EwxVljDZwoSQ20sDBihztHfk2yA8NVZghiXwrYHQdfKAOtzsayjhY9bY0yE2CWEeJ9xfzO423xhL5syS2TFJofO2pboHob0nY4GiAgRrvGQEDa/FWSsoaaYl0syRsEt3kWoH3B01shCXhTUWe9w3Bt44SC9QCh3eShQctwbaK2ApLroGCMlZrYqvlY3qYhM0aXpFkPOuoqJ3Dm6fxXrGwVF9gCWZagjPqznfkuMKQ8DPTQRO8ZqG1hPGKEm9IgpGW4DZDgTNriTxvFiq+Lz+0cKfp4wj6OCK9JSnzNSn9LFU7UhKZZMnYwcJ8s8yRsECScK4j5UOB95HFO0CzhY4xJxuCix0lDlEUeMdS6EZBkTsUkZ4K74dugyTXS7aNgL8aqjDfkCE0ZbwkCXpaWCKhl8P7VD5jxykivSyxyZrYERbe168LYu9ZYh86IkscgVLE7tWPKmJv11CgoyJltMEbrohtVAQfO4ImltiHEroYEs7RxAarVpY8AwXMcMReFOTYWe5iiLRQxJ5Q8DtJ8LQhWOhIeFESPGsILhbNDRljNbHzNRlTFbk2S3L0NOS6V1KFJYKUbSTcIIhM0wQ/s2TM0SRMNcQmSap3jCH4yhJZKSkwyRHpYYgsFeQ4U7xoCB7VVOExhXepo9ABBsYbvGWKXPME3lyH95YioZ0gssQRWWbI+FaSMkXijZXwgiTlYdPdkNLaETxlyDVIwqeaEus0aTcYcg0RVOkpR3CSJqIddK+90JCxzsDVloyrFd5ZAr4TBKfaWa6boEA7C7s6EpYaeFPjveooY72mjIccLHJ9HUwVlDhKkmutJDJBwnp1rvulJZggKDRfbXAkvC/4l3ozQOG9a8lxjx0i7nV4jSXc7vhe3OwIxjgSHjdEhhsif9YkPGlus3iLFDnWOFhtCZbJg0UbQcIaR67JjthoCyMEZRwhiXWyxO5QxI6w5NhT4U1WsJvDO60J34fW9hwzwlKij6ZAW9ne4L0s8C6XeBMEkd/LQy1VucBRot6QMlbivaBhoBgjqGiCJNhsqVp/S2SsG6DIONCR0dXhvWbJ+MRRZJkkuEjgDXJjFQW6SSL7GXK8Z2CZg7cVsbWGoKmEpzQ5elpiy8Ryg7dMkLLUEauzeO86CuwlSOlgYLojZWeJ9xM3S1PWfEfKl5ISLQ0MEKR8YOB2QfCxJBjrKPCN4f9MkaSsqoVXJBmP7EpFZ9UQfOoOFwSzBN4MQ8LsGrymlipcJQhmy0GaQjPqCHaXRwuCZwRbqK2Fg9wlClZqYicrIgMdZfxTQ0c7TBIbrChxmuzoKG8XRaSrIhhiyNFJkrC7oIAWMEOQa5aBekPCRknCo4IKPrYkvCDI8aYmY7WFtprgekcJZ3oLIqssCSMtFbQTJKwXYy3BY5oCh2iKPCpJOE+zRdpYgi6O2KmOAgvVCYaU4ySRek1sgyFhJ403QFHiVEmJHwtybO1gs8Hr5+BETQX3War0qZngYGgtVZtoqd6vFSk/UwdZElYqyjrF4HXUeFspIi9IGKf4j92pKGAdCYMVsbcV3kRF0N+R8LUd5PCsIGWoxDtBkCI0nKofdJQxT+LtZflvuc8Q3CjwWkq8KwUpHzkK/NmSsclCL0nseQdj5FRH5CNHSgtLiW80Of5HU9Hhlsga9bnBq3fEVltKfO5IaSTmGjjc4J0otcP7QsJUSQM8pEj5/wCuUuC2DWz8AAAAAElFTkSuQmCC");\r
+}\r
diff --git a/js/ckeditor/plugins/codemirror/theme/base16-dark.css b/js/ckeditor/plugins/codemirror/theme/base16-dark.css
new file mode 100644 (file)
index 0000000..c35a38d
--- /dev/null
@@ -0,0 +1,38 @@
+/*\r
+\r
+    Name:       Base16 Default Dark\r
+    Author:     Chris Kempson (http://chriskempson.com)\r
+\r
+    CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)\r
+    Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)\r
+\r
+*/\r
+\r
+.cm-s-base16-dark.CodeMirror { background: #151515; color: #e0e0e0; }\r
+.cm-s-base16-dark div.CodeMirror-selected { background: #303030; }\r
+.cm-s-base16-dark .CodeMirror-line::selection, .cm-s-base16-dark .CodeMirror-line > span::selection, .cm-s-base16-dark .CodeMirror-line > span > span::selection { background: rgba(48, 48, 48, .99); }\r
+.cm-s-base16-dark .CodeMirror-line::-moz-selection, .cm-s-base16-dark .CodeMirror-line > span::-moz-selection, .cm-s-base16-dark .CodeMirror-line > span > span::-moz-selection { background: rgba(48, 48, 48, .99); }\r
+.cm-s-base16-dark .CodeMirror-gutters { background: #151515; border-right: 0px; }\r
+.cm-s-base16-dark .CodeMirror-guttermarker { color: #ac4142; }\r
+.cm-s-base16-dark .CodeMirror-guttermarker-subtle { color: #505050; }\r
+.cm-s-base16-dark .CodeMirror-linenumber { color: #505050; }\r
+.cm-s-base16-dark .CodeMirror-cursor { border-left: 1px solid #b0b0b0; }\r
+\r
+.cm-s-base16-dark span.cm-comment { color: #8f5536; }\r
+.cm-s-base16-dark span.cm-atom { color: #aa759f; }\r
+.cm-s-base16-dark span.cm-number { color: #aa759f; }\r
+\r
+.cm-s-base16-dark span.cm-property, .cm-s-base16-dark span.cm-attribute { color: #90a959; }\r
+.cm-s-base16-dark span.cm-keyword { color: #ac4142; }\r
+.cm-s-base16-dark span.cm-string { color: #f4bf75; }\r
+\r
+.cm-s-base16-dark span.cm-variable { color: #90a959; }\r
+.cm-s-base16-dark span.cm-variable-2 { color: #6a9fb5; }\r
+.cm-s-base16-dark span.cm-def { color: #d28445; }\r
+.cm-s-base16-dark span.cm-bracket { color: #e0e0e0; }\r
+.cm-s-base16-dark span.cm-tag { color: #ac4142; }\r
+.cm-s-base16-dark span.cm-link { color: #aa759f; }\r
+.cm-s-base16-dark span.cm-error { background: #ac4142; color: #b0b0b0; }\r
+\r
+.cm-s-base16-dark .CodeMirror-activeline-background { background: #202020; }\r
+.cm-s-base16-dark .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/base16-light.css b/js/ckeditor/plugins/codemirror/theme/base16-light.css
new file mode 100644 (file)
index 0000000..5561f8c
--- /dev/null
@@ -0,0 +1,38 @@
+/*\r
+\r
+    Name:       Base16 Default Light\r
+    Author:     Chris Kempson (http://chriskempson.com)\r
+\r
+    CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)\r
+    Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)\r
+\r
+*/\r
+\r
+.cm-s-base16-light.CodeMirror { background: #f5f5f5; color: #202020; }\r
+.cm-s-base16-light div.CodeMirror-selected { background: #e0e0e0; }\r
+.cm-s-base16-light .CodeMirror-line::selection, .cm-s-base16-light .CodeMirror-line > span::selection, .cm-s-base16-light .CodeMirror-line > span > span::selection { background: #e0e0e0; }\r
+.cm-s-base16-light .CodeMirror-line::-moz-selection, .cm-s-base16-light .CodeMirror-line > span::-moz-selection, .cm-s-base16-light .CodeMirror-line > span > span::-moz-selection { background: #e0e0e0; }\r
+.cm-s-base16-light .CodeMirror-gutters { background: #f5f5f5; border-right: 0px; }\r
+.cm-s-base16-light .CodeMirror-guttermarker { color: #ac4142; }\r
+.cm-s-base16-light .CodeMirror-guttermarker-subtle { color: #b0b0b0; }\r
+.cm-s-base16-light .CodeMirror-linenumber { color: #b0b0b0; }\r
+.cm-s-base16-light .CodeMirror-cursor { border-left: 1px solid #505050; }\r
+\r
+.cm-s-base16-light span.cm-comment { color: #8f5536; }\r
+.cm-s-base16-light span.cm-atom { color: #aa759f; }\r
+.cm-s-base16-light span.cm-number { color: #aa759f; }\r
+\r
+.cm-s-base16-light span.cm-property, .cm-s-base16-light span.cm-attribute { color: #90a959; }\r
+.cm-s-base16-light span.cm-keyword { color: #ac4142; }\r
+.cm-s-base16-light span.cm-string { color: #f4bf75; }\r
+\r
+.cm-s-base16-light span.cm-variable { color: #90a959; }\r
+.cm-s-base16-light span.cm-variable-2 { color: #6a9fb5; }\r
+.cm-s-base16-light span.cm-def { color: #d28445; }\r
+.cm-s-base16-light span.cm-bracket { color: #202020; }\r
+.cm-s-base16-light span.cm-tag { color: #ac4142; }\r
+.cm-s-base16-light span.cm-link { color: #aa759f; }\r
+.cm-s-base16-light span.cm-error { background: #ac4142; color: #505050; }\r
+\r
+.cm-s-base16-light .CodeMirror-activeline-background { background: #DDDCDC; }\r
+.cm-s-base16-light .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/bespin.css b/js/ckeditor/plugins/codemirror/theme/bespin.css
new file mode 100644 (file)
index 0000000..becfad9
--- /dev/null
@@ -0,0 +1,34 @@
+/*\r
+\r
+    Name:       Bespin\r
+    Author:     Mozilla / Jan T. Sott\r
+\r
+    CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)\r
+    Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)\r
+\r
+*/\r
+\r
+.cm-s-bespin.CodeMirror {background: #28211c; color: #9d9b97;}\r
+.cm-s-bespin div.CodeMirror-selected {background: #36312e !important;}\r
+.cm-s-bespin .CodeMirror-gutters {background: #28211c; border-right: 0px;}\r
+.cm-s-bespin .CodeMirror-linenumber {color: #666666;}\r
+.cm-s-bespin .CodeMirror-cursor {border-left: 1px solid #797977 !important;}\r
+\r
+.cm-s-bespin span.cm-comment {color: #937121;}\r
+.cm-s-bespin span.cm-atom {color: #9b859d;}\r
+.cm-s-bespin span.cm-number {color: #9b859d;}\r
+\r
+.cm-s-bespin span.cm-property, .cm-s-bespin span.cm-attribute {color: #54be0d;}\r
+.cm-s-bespin span.cm-keyword {color: #cf6a4c;}\r
+.cm-s-bespin span.cm-string {color: #f9ee98;}\r
+\r
+.cm-s-bespin span.cm-variable {color: #54be0d;}\r
+.cm-s-bespin span.cm-variable-2 {color: #5ea6ea;}\r
+.cm-s-bespin span.cm-def {color: #cf7d34;}\r
+.cm-s-bespin span.cm-error {background: #cf6a4c; color: #797977;}\r
+.cm-s-bespin span.cm-bracket {color: #9d9b97;}\r
+.cm-s-bespin span.cm-tag {color: #cf6a4c;}\r
+.cm-s-bespin span.cm-link {color: #9b859d;}\r
+\r
+.cm-s-bespin .CodeMirror-matchingbracket { text-decoration: underline; color: white !important;}\r
+.cm-s-bespin .CodeMirror-activeline-background { background: #404040; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/blackboard.css b/js/ckeditor/plugins/codemirror/theme/blackboard.css
new file mode 100644 (file)
index 0000000..0fa55e7
--- /dev/null
@@ -0,0 +1,32 @@
+/* Port of TextMate's Blackboard theme */\r
+\r
+.cm-s-blackboard.CodeMirror { background: #0C1021; color: #F8F8F8; }\r
+.cm-s-blackboard div.CodeMirror-selected { background: #253B76; }\r
+.cm-s-blackboard .CodeMirror-line::selection, .cm-s-blackboard .CodeMirror-line > span::selection, .cm-s-blackboard .CodeMirror-line > span > span::selection { background: rgba(37, 59, 118, .99); }\r
+.cm-s-blackboard .CodeMirror-line::-moz-selection, .cm-s-blackboard .CodeMirror-line > span::-moz-selection, .cm-s-blackboard .CodeMirror-line > span > span::-moz-selection { background: rgba(37, 59, 118, .99); }\r
+.cm-s-blackboard .CodeMirror-gutters { background: #0C1021; border-right: 0; }\r
+.cm-s-blackboard .CodeMirror-guttermarker { color: #FBDE2D; }\r
+.cm-s-blackboard .CodeMirror-guttermarker-subtle { color: #888; }\r
+.cm-s-blackboard .CodeMirror-linenumber { color: #888; }\r
+.cm-s-blackboard .CodeMirror-cursor { border-left: 1px solid #A7A7A7; }\r
+\r
+.cm-s-blackboard .cm-keyword { color: #FBDE2D; }\r
+.cm-s-blackboard .cm-atom { color: #D8FA3C; }\r
+.cm-s-blackboard .cm-number { color: #D8FA3C; }\r
+.cm-s-blackboard .cm-def { color: #8DA6CE; }\r
+.cm-s-blackboard .cm-variable { color: #FF6400; }\r
+.cm-s-blackboard .cm-operator { color: #FBDE2D; }\r
+.cm-s-blackboard .cm-comment { color: #AEAEAE; }\r
+.cm-s-blackboard .cm-string { color: #61CE3C; }\r
+.cm-s-blackboard .cm-string-2 { color: #61CE3C; }\r
+.cm-s-blackboard .cm-meta { color: #D8FA3C; }\r
+.cm-s-blackboard .cm-builtin { color: #8DA6CE; }\r
+.cm-s-blackboard .cm-tag { color: #8DA6CE; }\r
+.cm-s-blackboard .cm-attribute { color: #8DA6CE; }\r
+.cm-s-blackboard .cm-header { color: #FF6400; }\r
+.cm-s-blackboard .cm-hr { color: #AEAEAE; }\r
+.cm-s-blackboard .cm-link { color: #8DA6CE; }\r
+.cm-s-blackboard .cm-error { background: #9D1E15; color: #F8F8F8; }\r
+\r
+.cm-s-blackboard .CodeMirror-activeline-background { background: #3C3636; }\r
+.cm-s-blackboard .CodeMirror-matchingbracket { outline:1px solid grey;color:white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/cobalt.css b/js/ckeditor/plugins/codemirror/theme/cobalt.css
new file mode 100644 (file)
index 0000000..bf24bd2
--- /dev/null
@@ -0,0 +1,25 @@
+.cm-s-cobalt.CodeMirror { background: #002240; color: white; }\r
+.cm-s-cobalt div.CodeMirror-selected { background: #b36539; }\r
+.cm-s-cobalt .CodeMirror-line::selection, .cm-s-cobalt .CodeMirror-line > span::selection, .cm-s-cobalt .CodeMirror-line > span > span::selection { background: rgba(179, 101, 57, .99); }\r
+.cm-s-cobalt .CodeMirror-line::-moz-selection, .cm-s-cobalt .CodeMirror-line > span::-moz-selection, .cm-s-cobalt .CodeMirror-line > span > span::-moz-selection { background: rgba(179, 101, 57, .99); }\r
+.cm-s-cobalt .CodeMirror-gutters { background: #002240; border-right: 1px solid #aaa; }\r
+.cm-s-cobalt .CodeMirror-guttermarker { color: #ffee80; }\r
+.cm-s-cobalt .CodeMirror-guttermarker-subtle { color: #d0d0d0; }\r
+.cm-s-cobalt .CodeMirror-linenumber { color: #d0d0d0; }\r
+.cm-s-cobalt .CodeMirror-cursor { border-left: 1px solid white; }\r
+\r
+.cm-s-cobalt span.cm-comment { color: #08f; }\r
+.cm-s-cobalt span.cm-atom { color: #845dc4; }\r
+.cm-s-cobalt span.cm-number, .cm-s-cobalt span.cm-attribute { color: #ff80e1; }\r
+.cm-s-cobalt span.cm-keyword { color: #ffee80; }\r
+.cm-s-cobalt span.cm-string { color: #3ad900; }\r
+.cm-s-cobalt span.cm-meta { color: #ff9d00; }\r
+.cm-s-cobalt span.cm-variable-2, .cm-s-cobalt span.cm-tag { color: #9effff; }\r
+.cm-s-cobalt span.cm-variable-3, .cm-s-cobalt span.cm-def, .cm-s-cobalt .cm-type { color: white; }\r
+.cm-s-cobalt span.cm-bracket { color: #d8d8d8; }\r
+.cm-s-cobalt span.cm-builtin, .cm-s-cobalt span.cm-special { color: #ff9e59; }\r
+.cm-s-cobalt span.cm-link { color: #845dc4; }\r
+.cm-s-cobalt span.cm-error { color: #9d1e15; }\r
+\r
+.cm-s-cobalt .CodeMirror-activeline-background { background: #002D57; }\r
+.cm-s-cobalt .CodeMirror-matchingbracket { outline:1px solid grey;color:white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/colorforth.css b/js/ckeditor/plugins/codemirror/theme/colorforth.css
new file mode 100644 (file)
index 0000000..a096504
--- /dev/null
@@ -0,0 +1,33 @@
+.cm-s-colorforth.CodeMirror { background: #000000; color: #f8f8f8; }\r
+.cm-s-colorforth .CodeMirror-gutters { background: #0a001f; border-right: 1px solid #aaa; }\r
+.cm-s-colorforth .CodeMirror-guttermarker { color: #FFBD40; }\r
+.cm-s-colorforth .CodeMirror-guttermarker-subtle { color: #78846f; }\r
+.cm-s-colorforth .CodeMirror-linenumber { color: #bababa; }\r
+.cm-s-colorforth .CodeMirror-cursor { border-left: 1px solid white; }\r
+\r
+.cm-s-colorforth span.cm-comment     { color: #ededed; }\r
+.cm-s-colorforth span.cm-def         { color: #ff1c1c; font-weight:bold; }\r
+.cm-s-colorforth span.cm-keyword     { color: #ffd900; }\r
+.cm-s-colorforth span.cm-builtin     { color: #00d95a; }\r
+.cm-s-colorforth span.cm-variable    { color: #73ff00; }\r
+.cm-s-colorforth span.cm-string      { color: #007bff; }\r
+.cm-s-colorforth span.cm-number      { color: #00c4ff; }\r
+.cm-s-colorforth span.cm-atom        { color: #606060; }\r
+\r
+.cm-s-colorforth span.cm-variable-2  { color: #EEE; }\r
+.cm-s-colorforth span.cm-variable-3, .cm-s-colorforth span.cm-type { color: #DDD; }\r
+.cm-s-colorforth span.cm-property    {}\r
+.cm-s-colorforth span.cm-operator    {}\r
+\r
+.cm-s-colorforth span.cm-meta        { color: yellow; }\r
+.cm-s-colorforth span.cm-qualifier   { color: #FFF700; }\r
+.cm-s-colorforth span.cm-bracket     { color: #cc7; }\r
+.cm-s-colorforth span.cm-tag         { color: #FFBD40; }\r
+.cm-s-colorforth span.cm-attribute   { color: #FFF700; }\r
+.cm-s-colorforth span.cm-error       { color: #f00; }\r
+\r
+.cm-s-colorforth div.CodeMirror-selected { background: #333d53; }\r
+\r
+.cm-s-colorforth span.cm-compilation { background: rgba(255, 255, 255, 0.12); }\r
+\r
+.cm-s-colorforth .CodeMirror-activeline-background { background: #253540; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/dracula.css b/js/ckeditor/plugins/codemirror/theme/dracula.css
new file mode 100644 (file)
index 0000000..cc41554
--- /dev/null
@@ -0,0 +1,40 @@
+/*\r
+\r
+    Name:       dracula\r
+    Author:     Michael Kaminsky (http://github.com/mkaminsky11)\r
+\r
+    Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\r
+\r
+*/\r
+\r
+\r
+.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\r
+  background-color: #282a36 !important;\r
+  color: #f8f8f2 !important;\r
+  border: none;\r
+}\r
+.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\r
+.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\r
+.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\r
+.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\r
+.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\r
+.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\r
+.cm-s-dracula span.cm-comment { color: #6272a4; }\r
+.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\r
+.cm-s-dracula span.cm-number { color: #bd93f9; }\r
+.cm-s-dracula span.cm-variable { color: #50fa7b; }\r
+.cm-s-dracula span.cm-variable-2 { color: white; }\r
+.cm-s-dracula span.cm-def { color: #50fa7b; }\r
+.cm-s-dracula span.cm-operator { color: #ff79c6; }\r
+.cm-s-dracula span.cm-keyword { color: #ff79c6; }\r
+.cm-s-dracula span.cm-atom { color: #bd93f9; }\r
+.cm-s-dracula span.cm-meta { color: #f8f8f2; }\r
+.cm-s-dracula span.cm-tag { color: #ff79c6; }\r
+.cm-s-dracula span.cm-attribute { color: #50fa7b; }\r
+.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\r
+.cm-s-dracula span.cm-property { color: #66d9ef; }\r
+.cm-s-dracula span.cm-builtin { color: #50fa7b; }\r
+.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\r
+\r
+.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\r
+.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/duotone-dark.css b/js/ckeditor/plugins/codemirror/theme/duotone-dark.css
new file mode 100644 (file)
index 0000000..a493eb6
--- /dev/null
@@ -0,0 +1,35 @@
+/*\r
+Name:   DuoTone-Dark\r
+Author: by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes)\r
+\r
+CodeMirror template by Jan T. Sott (https://github.com/idleberg), adapted by Bram de Haan (https://github.com/atelierbram/)\r
+*/\r
+\r
+.cm-s-duotone-dark.CodeMirror { background: #2a2734; color: #6c6783; }\r
+.cm-s-duotone-dark div.CodeMirror-selected { background: #545167!important; }\r
+.cm-s-duotone-dark .CodeMirror-gutters { background: #2a2734; border-right: 0px; }\r
+.cm-s-duotone-dark .CodeMirror-linenumber { color: #545167; }\r
+\r
+/* begin cursor */\r
+.cm-s-duotone-dark .CodeMirror-cursor { border-left: 1px solid #ffad5c; /* border-left: 1px solid #ffad5c80; */ border-right: .5em solid #ffad5c; /* border-right: .5em solid #ffad5c80; */ opacity: .5; }\r
+.cm-s-duotone-dark .CodeMirror-activeline-background { background: #363342; /* background: #36334280;  */ opacity: .5;}\r
+.cm-s-duotone-dark .cm-fat-cursor .CodeMirror-cursor { background: #ffad5c; /* background: #ffad5c80; */ opacity: .5;}\r
+/* end cursor */\r
+\r
+.cm-s-duotone-dark span.cm-atom, .cm-s-duotone-dark span.cm-number, .cm-s-duotone-dark span.cm-keyword, .cm-s-duotone-dark span.cm-variable, .cm-s-duotone-dark span.cm-attribute, .cm-s-duotone-dark span.cm-quote, .cm-s-duotone-dark span.cm-hr, .cm-s-duotone-dark span.cm-link { color: #ffcc99; }\r
+\r
+.cm-s-duotone-dark span.cm-property { color: #9a86fd; }\r
+.cm-s-duotone-dark span.cm-punctuation, .cm-s-duotone-dark span.cm-unit, .cm-s-duotone-dark span.cm-negative { color: #e09142; }\r
+.cm-s-duotone-dark span.cm-string { color: #ffb870; }\r
+.cm-s-duotone-dark span.cm-operator { color: #ffad5c; }\r
+.cm-s-duotone-dark span.cm-positive { color: #6a51e6; }\r
+\r
+.cm-s-duotone-dark span.cm-variable-2, .cm-s-duotone-dark span.cm-variable-3, .cm-s-duotone-dark span.cm-type, .cm-s-duotone-dark span.cm-string-2, .cm-s-duotone-dark span.cm-url { color: #7a63ee; }\r
+.cm-s-duotone-dark span.cm-def, .cm-s-duotone-dark span.cm-tag, .cm-s-duotone-dark span.cm-builtin, .cm-s-duotone-dark span.cm-qualifier, .cm-s-duotone-dark span.cm-header, .cm-s-duotone-dark span.cm-em { color: #eeebff; }\r
+.cm-s-duotone-dark span.cm-bracket, .cm-s-duotone-dark span.cm-comment { color: #6c6783; }\r
+\r
+/* using #f00 red for errors, don't think any of the colorscheme variables will stand out enough, ... maybe by giving it a background-color ... */\r
+.cm-s-duotone-dark span.cm-error, .cm-s-duotone-dark span.cm-invalidchar { color: #f00; }\r
+\r
+.cm-s-duotone-dark span.cm-header { font-weight: normal; }\r
+.cm-s-duotone-dark .CodeMirror-matchingbracket { text-decoration: underline; color: #eeebff !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/duotone-light.css b/js/ckeditor/plugins/codemirror/theme/duotone-light.css
new file mode 100644 (file)
index 0000000..68db16e
--- /dev/null
@@ -0,0 +1,35 @@
+/*\r
+Name:   DuoTone-Light\r
+Author: by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes)\r
+\r
+CodeMirror template by Jan T. Sott (https://github.com/idleberg), adapted by Bram de Haan (https://github.com/atelierbram/)\r
+*/\r
+\r
+.cm-s-duotone-light.CodeMirror { background: #faf8f5; color: #b29762; }\r
+.cm-s-duotone-light div.CodeMirror-selected { background: #e3dcce !important; }\r
+.cm-s-duotone-light .CodeMirror-gutters { background: #faf8f5; border-right: 0px; }\r
+.cm-s-duotone-light .CodeMirror-linenumber { color: #cdc4b1; }\r
+\r
+/* begin cursor */\r
+.cm-s-duotone-light .CodeMirror-cursor { border-left: 1px solid #93abdc; /* border-left: 1px solid #93abdc80; */ border-right: .5em solid #93abdc; /* border-right: .5em solid #93abdc80; */ opacity: .5; }\r
+.cm-s-duotone-light .CodeMirror-activeline-background { background: #e3dcce;  /* background: #e3dcce80; */ opacity: .5; }\r
+.cm-s-duotone-light .cm-fat-cursor .CodeMirror-cursor { background: #93abdc; /* #93abdc80; */ opacity: .5; }\r
+/* end cursor */\r
+\r
+.cm-s-duotone-light span.cm-atom, .cm-s-duotone-light span.cm-number, .cm-s-duotone-light span.cm-keyword, .cm-s-duotone-light span.cm-variable, .cm-s-duotone-light span.cm-attribute, .cm-s-duotone-light span.cm-quote, .cm-s-duotone-light-light span.cm-hr, .cm-s-duotone-light-light span.cm-link { color: #063289; }\r
+\r
+.cm-s-duotone-light span.cm-property { color: #b29762; }\r
+.cm-s-duotone-light span.cm-punctuation, .cm-s-duotone-light span.cm-unit, .cm-s-duotone-light span.cm-negative { color: #063289; }\r
+.cm-s-duotone-light span.cm-string, .cm-s-duotone-light span.cm-operator { color: #1659df; }\r
+.cm-s-duotone-light span.cm-positive { color: #896724; }\r
+\r
+.cm-s-duotone-light span.cm-variable-2, .cm-s-duotone-light span.cm-variable-3, .cm-s-duotone-light span.cm-type, .cm-s-duotone-light span.cm-string-2, .cm-s-duotone-light span.cm-url { color: #896724; }\r
+.cm-s-duotone-light span.cm-def, .cm-s-duotone-light span.cm-tag, .cm-s-duotone-light span.cm-builtin, .cm-s-duotone-light span.cm-qualifier, .cm-s-duotone-light span.cm-header, .cm-s-duotone-light span.cm-em { color: #2d2006; }\r
+.cm-s-duotone-light span.cm-bracket, .cm-s-duotone-light span.cm-comment { color: #b6ad9a; }\r
+\r
+/* using #f00 red for errors, don't think any of the colorscheme variables will stand out enough, ... maybe by giving it a background-color ... */\r
+/* .cm-s-duotone-light span.cm-error { background: #896724; color: #728fcb; } */\r
+.cm-s-duotone-light span.cm-error, .cm-s-duotone-light span.cm-invalidchar { color: #f00; }\r
+\r
+.cm-s-duotone-light span.cm-header { font-weight: normal; }\r
+.cm-s-duotone-light .CodeMirror-matchingbracket { text-decoration: underline; color: #faf8f5 !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/eclipse.css b/js/ckeditor/plugins/codemirror/theme/eclipse.css
new file mode 100644 (file)
index 0000000..30fe9d1
--- /dev/null
@@ -0,0 +1,23 @@
+.cm-s-eclipse span.cm-meta { color: #FF1717; }\r
+.cm-s-eclipse span.cm-keyword { line-height: 1em; font-weight: bold; color: #7F0055; }\r
+.cm-s-eclipse span.cm-atom { color: #219; }\r
+.cm-s-eclipse span.cm-number { color: #164; }\r
+.cm-s-eclipse span.cm-def { color: #00f; }\r
+.cm-s-eclipse span.cm-variable { color: black; }\r
+.cm-s-eclipse span.cm-variable-2 { color: #0000C0; }\r
+.cm-s-eclipse span.cm-variable-3, .cm-s-eclipse span.cm-type { color: #0000C0; }\r
+.cm-s-eclipse span.cm-property { color: black; }\r
+.cm-s-eclipse span.cm-operator { color: black; }\r
+.cm-s-eclipse span.cm-comment { color: #3F7F5F; }\r
+.cm-s-eclipse span.cm-string { color: #2A00FF; }\r
+.cm-s-eclipse span.cm-string-2 { color: #f50; }\r
+.cm-s-eclipse span.cm-qualifier { color: #555; }\r
+.cm-s-eclipse span.cm-builtin { color: #30a; }\r
+.cm-s-eclipse span.cm-bracket { color: #cc7; }\r
+.cm-s-eclipse span.cm-tag { color: #170; }\r
+.cm-s-eclipse span.cm-attribute { color: #00c; }\r
+.cm-s-eclipse span.cm-link { color: #219; }\r
+.cm-s-eclipse span.cm-error { color: #f00; }\r
+\r
+.cm-s-eclipse .CodeMirror-activeline-background { background: #e8f2ff; }\r
+.cm-s-eclipse .CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/elegant.css b/js/ckeditor/plugins/codemirror/theme/elegant.css
new file mode 100644 (file)
index 0000000..a5929c1
--- /dev/null
@@ -0,0 +1,13 @@
+.cm-s-elegant span.cm-number, .cm-s-elegant span.cm-string, .cm-s-elegant span.cm-atom { color: #762; }\r
+.cm-s-elegant span.cm-comment { color: #262; font-style: italic; line-height: 1em; }\r
+.cm-s-elegant span.cm-meta { color: #555; font-style: italic; line-height: 1em; }\r
+.cm-s-elegant span.cm-variable { color: black; }\r
+.cm-s-elegant span.cm-variable-2 { color: #b11; }\r
+.cm-s-elegant span.cm-qualifier { color: #555; }\r
+.cm-s-elegant span.cm-keyword { color: #730; }\r
+.cm-s-elegant span.cm-builtin { color: #30a; }\r
+.cm-s-elegant span.cm-link { color: #762; }\r
+.cm-s-elegant span.cm-error { background-color: #fdd; }\r
+\r
+.cm-s-elegant .CodeMirror-activeline-background { background: #e8f2ff; }\r
+.cm-s-elegant .CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/erlang-dark.css b/js/ckeditor/plugins/codemirror/theme/erlang-dark.css
new file mode 100644 (file)
index 0000000..35e8796
--- /dev/null
@@ -0,0 +1,34 @@
+.cm-s-erlang-dark.CodeMirror { background: #002240; color: white; }\r
+.cm-s-erlang-dark div.CodeMirror-selected { background: #b36539; }\r
+.cm-s-erlang-dark .CodeMirror-line::selection, .cm-s-erlang-dark .CodeMirror-line > span::selection, .cm-s-erlang-dark .CodeMirror-line > span > span::selection { background: rgba(179, 101, 57, .99); }\r
+.cm-s-erlang-dark .CodeMirror-line::-moz-selection, .cm-s-erlang-dark .CodeMirror-line > span::-moz-selection, .cm-s-erlang-dark .CodeMirror-line > span > span::-moz-selection { background: rgba(179, 101, 57, .99); }\r
+.cm-s-erlang-dark .CodeMirror-gutters { background: #002240; border-right: 1px solid #aaa; }\r
+.cm-s-erlang-dark .CodeMirror-guttermarker { color: white; }\r
+.cm-s-erlang-dark .CodeMirror-guttermarker-subtle { color: #d0d0d0; }\r
+.cm-s-erlang-dark .CodeMirror-linenumber { color: #d0d0d0; }\r
+.cm-s-erlang-dark .CodeMirror-cursor { border-left: 1px solid white; }\r
+\r
+.cm-s-erlang-dark span.cm-quote      { color: #ccc; }\r
+.cm-s-erlang-dark span.cm-atom       { color: #f133f1; }\r
+.cm-s-erlang-dark span.cm-attribute  { color: #ff80e1; }\r
+.cm-s-erlang-dark span.cm-bracket    { color: #ff9d00; }\r
+.cm-s-erlang-dark span.cm-builtin    { color: #eaa; }\r
+.cm-s-erlang-dark span.cm-comment    { color: #77f; }\r
+.cm-s-erlang-dark span.cm-def        { color: #e7a; }\r
+.cm-s-erlang-dark span.cm-keyword    { color: #ffee80; }\r
+.cm-s-erlang-dark span.cm-meta       { color: #50fefe; }\r
+.cm-s-erlang-dark span.cm-number     { color: #ffd0d0; }\r
+.cm-s-erlang-dark span.cm-operator   { color: #d55; }\r
+.cm-s-erlang-dark span.cm-property   { color: #ccc; }\r
+.cm-s-erlang-dark span.cm-qualifier  { color: #ccc; }\r
+.cm-s-erlang-dark span.cm-special    { color: #ffbbbb; }\r
+.cm-s-erlang-dark span.cm-string     { color: #3ad900; }\r
+.cm-s-erlang-dark span.cm-string-2   { color: #ccc; }\r
+.cm-s-erlang-dark span.cm-tag        { color: #9effff; }\r
+.cm-s-erlang-dark span.cm-variable   { color: #50fe50; }\r
+.cm-s-erlang-dark span.cm-variable-2 { color: #e0e; }\r
+.cm-s-erlang-dark span.cm-variable-3, .cm-s-erlang-dark span.cm-type { color: #ccc; }\r
+.cm-s-erlang-dark span.cm-error      { color: #9d1e15; }\r
+\r
+.cm-s-erlang-dark .CodeMirror-activeline-background { background: #013461; }\r
+.cm-s-erlang-dark .CodeMirror-matchingbracket { outline:1px solid grey; color:white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/hopscotch.css b/js/ckeditor/plugins/codemirror/theme/hopscotch.css
new file mode 100644 (file)
index 0000000..c40094d
--- /dev/null
@@ -0,0 +1,34 @@
+/*\r
+\r
+    Name:       Hopscotch\r
+    Author:     Jan T. Sott\r
+\r
+    CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)\r
+    Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)\r
+\r
+*/\r
+\r
+.cm-s-hopscotch.CodeMirror {background: #322931; color: #d5d3d5;}\r
+.cm-s-hopscotch div.CodeMirror-selected {background: #433b42 !important;}\r
+.cm-s-hopscotch .CodeMirror-gutters {background: #322931; border-right: 0px;}\r
+.cm-s-hopscotch .CodeMirror-linenumber {color: #797379;}\r
+.cm-s-hopscotch .CodeMirror-cursor {border-left: 1px solid #989498 !important;}\r
+\r
+.cm-s-hopscotch span.cm-comment {color: #b33508;}\r
+.cm-s-hopscotch span.cm-atom {color: #c85e7c;}\r
+.cm-s-hopscotch span.cm-number {color: #c85e7c;}\r
+\r
+.cm-s-hopscotch span.cm-property, .cm-s-hopscotch span.cm-attribute {color: #8fc13e;}\r
+.cm-s-hopscotch span.cm-keyword {color: #dd464c;}\r
+.cm-s-hopscotch span.cm-string {color: #fdcc59;}\r
+\r
+.cm-s-hopscotch span.cm-variable {color: #8fc13e;}\r
+.cm-s-hopscotch span.cm-variable-2 {color: #1290bf;}\r
+.cm-s-hopscotch span.cm-def {color: #fd8b19;}\r
+.cm-s-hopscotch span.cm-error {background: #dd464c; color: #989498;}\r
+.cm-s-hopscotch span.cm-bracket {color: #d5d3d5;}\r
+.cm-s-hopscotch span.cm-tag {color: #dd464c;}\r
+.cm-s-hopscotch span.cm-link {color: #c85e7c;}\r
+\r
+.cm-s-hopscotch .CodeMirror-matchingbracket { text-decoration: underline; color: white !important;}\r
+.cm-s-hopscotch .CodeMirror-activeline-background { background: #302020; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/icecoder.css b/js/ckeditor/plugins/codemirror/theme/icecoder.css
new file mode 100644 (file)
index 0000000..3056733
--- /dev/null
@@ -0,0 +1,43 @@
+/*\r
+ICEcoder default theme by Matt Pass, used in code editor available at https://icecoder.net\r
+*/\r
+\r
+.cm-s-icecoder { color: #666; background: #1d1d1b; }\r
+\r
+.cm-s-icecoder span.cm-keyword { color: #eee; font-weight:bold; }  /* off-white 1 */\r
+.cm-s-icecoder span.cm-atom { color: #e1c76e; }                    /* yellow */\r
+.cm-s-icecoder span.cm-number { color: #6cb5d9; }                  /* blue */\r
+.cm-s-icecoder span.cm-def { color: #b9ca4a; }                     /* green */\r
+\r
+.cm-s-icecoder span.cm-variable { color: #6cb5d9; }                /* blue */\r
+.cm-s-icecoder span.cm-variable-2 { color: #cc1e5c; }              /* pink */\r
+.cm-s-icecoder span.cm-variable-3, .cm-s-icecoder span.cm-type { color: #f9602c; } /* orange */\r
+\r
+.cm-s-icecoder span.cm-property { color: #eee; }                   /* off-white 1 */\r
+.cm-s-icecoder span.cm-operator { color: #9179bb; }                /* purple */\r
+.cm-s-icecoder span.cm-comment { color: #97a3aa; }                 /* grey-blue */\r
+\r
+.cm-s-icecoder span.cm-string { color: #b9ca4a; }                  /* green */\r
+.cm-s-icecoder span.cm-string-2 { color: #6cb5d9; }                /* blue */\r
+\r
+.cm-s-icecoder span.cm-meta { color: #555; }                       /* grey */\r
+\r
+.cm-s-icecoder span.cm-qualifier { color: #555; }                  /* grey */\r
+.cm-s-icecoder span.cm-builtin { color: #214e7b; }                 /* bright blue */\r
+.cm-s-icecoder span.cm-bracket { color: #cc7; }                    /* grey-yellow */\r
+\r
+.cm-s-icecoder span.cm-tag { color: #e8e8e8; }                     /* off-white 2 */\r
+.cm-s-icecoder span.cm-attribute { color: #099; }                  /* teal */\r
+\r
+.cm-s-icecoder span.cm-header { color: #6a0d6a; }                  /* purple-pink */\r
+.cm-s-icecoder span.cm-quote { color: #186718; }                   /* dark green */\r
+.cm-s-icecoder span.cm-hr { color: #888; }                         /* mid-grey */\r
+.cm-s-icecoder span.cm-link { color: #e1c76e; }                    /* yellow */\r
+.cm-s-icecoder span.cm-error { color: #d00; }                      /* red */\r
+\r
+.cm-s-icecoder .CodeMirror-cursor { border-left: 1px solid white; }\r
+.cm-s-icecoder div.CodeMirror-selected { color: #fff; background: #037; }\r
+.cm-s-icecoder .CodeMirror-gutters { background: #1d1d1b; min-width: 41px; border-right: 0; }\r
+.cm-s-icecoder .CodeMirror-linenumber { color: #555; cursor: default; }\r
+.cm-s-icecoder .CodeMirror-matchingbracket { color: #fff !important; background: #555 !important; }\r
+.cm-s-icecoder .CodeMirror-activeline-background { background: #000; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/isotope.css b/js/ckeditor/plugins/codemirror/theme/isotope.css
new file mode 100644 (file)
index 0000000..a9642ac
--- /dev/null
@@ -0,0 +1,34 @@
+/*\r
+\r
+    Name:       Isotope\r
+    Author:     David Desandro / Jan T. Sott\r
+\r
+    CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)\r
+    Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)\r
+\r
+*/\r
+\r
+.cm-s-isotope.CodeMirror {background: #000000; color: #e0e0e0;}\r
+.cm-s-isotope div.CodeMirror-selected {background: #404040 !important;}\r
+.cm-s-isotope .CodeMirror-gutters {background: #000000; border-right: 0px;}\r
+.cm-s-isotope .CodeMirror-linenumber {color: #808080;}\r
+.cm-s-isotope .CodeMirror-cursor {border-left: 1px solid #c0c0c0 !important;}\r
+\r
+.cm-s-isotope span.cm-comment {color: #3300ff;}\r
+.cm-s-isotope span.cm-atom {color: #cc00ff;}\r
+.cm-s-isotope span.cm-number {color: #cc00ff;}\r
+\r
+.cm-s-isotope span.cm-property, .cm-s-isotope span.cm-attribute {color: #33ff00;}\r
+.cm-s-isotope span.cm-keyword {color: #ff0000;}\r
+.cm-s-isotope span.cm-string {color: #ff0099;}\r
+\r
+.cm-s-isotope span.cm-variable {color: #33ff00;}\r
+.cm-s-isotope span.cm-variable-2 {color: #0066ff;}\r
+.cm-s-isotope span.cm-def {color: #ff9900;}\r
+.cm-s-isotope span.cm-error {background: #ff0000; color: #c0c0c0;}\r
+.cm-s-isotope span.cm-bracket {color: #e0e0e0;}\r
+.cm-s-isotope span.cm-tag {color: #ff0000;}\r
+.cm-s-isotope span.cm-link {color: #cc00ff;}\r
+\r
+.cm-s-isotope .CodeMirror-matchingbracket { text-decoration: underline; color: white !important;}\r
+.cm-s-isotope .CodeMirror-activeline-background { background: #202020; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/lesser-dark.css b/js/ckeditor/plugins/codemirror/theme/lesser-dark.css
new file mode 100644 (file)
index 0000000..303e232
--- /dev/null
@@ -0,0 +1,47 @@
+/*\r
+http://lesscss.org/ dark theme\r
+Ported to CodeMirror by Peter Kroon\r
+*/\r
+.cm-s-lesser-dark {\r
+  line-height: 1.3em;\r
+}\r
+.cm-s-lesser-dark.CodeMirror { background: #262626; color: #EBEFE7; text-shadow: 0 -1px 1px #262626; }\r
+.cm-s-lesser-dark div.CodeMirror-selected { background: #45443B; } /* 33322B*/\r
+.cm-s-lesser-dark .CodeMirror-line::selection, .cm-s-lesser-dark .CodeMirror-line > span::selection, .cm-s-lesser-dark .CodeMirror-line > span > span::selection { background: rgba(69, 68, 59, .99); }\r
+.cm-s-lesser-dark .CodeMirror-line::-moz-selection, .cm-s-lesser-dark .CodeMirror-line > span::-moz-selection, .cm-s-lesser-dark .CodeMirror-line > span > span::-moz-selection { background: rgba(69, 68, 59, .99); }\r
+.cm-s-lesser-dark .CodeMirror-cursor { border-left: 1px solid white; }\r
+.cm-s-lesser-dark pre { padding: 0 8px; }/*editable code holder*/\r
+\r
+.cm-s-lesser-dark.CodeMirror span.CodeMirror-matchingbracket { color: #7EFC7E; }/*65FC65*/\r
+\r
+.cm-s-lesser-dark .CodeMirror-gutters { background: #262626; border-right:1px solid #aaa; }\r
+.cm-s-lesser-dark .CodeMirror-guttermarker { color: #599eff; }\r
+.cm-s-lesser-dark .CodeMirror-guttermarker-subtle { color: #777; }\r
+.cm-s-lesser-dark .CodeMirror-linenumber { color: #777; }\r
+\r
+.cm-s-lesser-dark span.cm-header { color: #a0a; }\r
+.cm-s-lesser-dark span.cm-quote { color: #090; }\r
+.cm-s-lesser-dark span.cm-keyword { color: #599eff; }\r
+.cm-s-lesser-dark span.cm-atom { color: #C2B470; }\r
+.cm-s-lesser-dark span.cm-number { color: #B35E4D; }\r
+.cm-s-lesser-dark span.cm-def { color: white; }\r
+.cm-s-lesser-dark span.cm-variable { color:#D9BF8C; }\r
+.cm-s-lesser-dark span.cm-variable-2 { color: #669199; }\r
+.cm-s-lesser-dark span.cm-variable-3, .cm-s-lesser-dark span.cm-type { color: white; }\r
+.cm-s-lesser-dark span.cm-property { color: #92A75C; }\r
+.cm-s-lesser-dark span.cm-operator { color: #92A75C; }\r
+.cm-s-lesser-dark span.cm-comment { color: #666; }\r
+.cm-s-lesser-dark span.cm-string { color: #BCD279; }\r
+.cm-s-lesser-dark span.cm-string-2 { color: #f50; }\r
+.cm-s-lesser-dark span.cm-meta { color: #738C73; }\r
+.cm-s-lesser-dark span.cm-qualifier { color: #555; }\r
+.cm-s-lesser-dark span.cm-builtin { color: #ff9e59; }\r
+.cm-s-lesser-dark span.cm-bracket { color: #EBEFE7; }\r
+.cm-s-lesser-dark span.cm-tag { color: #669199; }\r
+.cm-s-lesser-dark span.cm-attribute { color: #00c; }\r
+.cm-s-lesser-dark span.cm-hr { color: #999; }\r
+.cm-s-lesser-dark span.cm-link { color: #00c; }\r
+.cm-s-lesser-dark span.cm-error { color: #9d1e15; }\r
+\r
+.cm-s-lesser-dark .CodeMirror-activeline-background { background: #3C3A3A; }\r
+.cm-s-lesser-dark .CodeMirror-matchingbracket { outline:1px solid grey; color:white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/liquibyte.css b/js/ckeditor/plugins/codemirror/theme/liquibyte.css
new file mode 100644 (file)
index 0000000..fa164f6
--- /dev/null
@@ -0,0 +1,95 @@
+.cm-s-liquibyte.CodeMirror {\r
+       background-color: #000;\r
+       color: #fff;\r
+       line-height: 1.2em;\r
+       font-size: 1em;\r
+}\r
+.cm-s-liquibyte .CodeMirror-focused .cm-matchhighlight {\r
+       text-decoration: underline;\r
+       text-decoration-color: #0f0;\r
+       text-decoration-style: wavy;\r
+}\r
+.cm-s-liquibyte .cm-trailingspace {\r
+       text-decoration: line-through;\r
+       text-decoration-color: #f00;\r
+       text-decoration-style: dotted;\r
+}\r
+.cm-s-liquibyte .cm-tab {\r
+       text-decoration: line-through;\r
+       text-decoration-color: #404040;\r
+       text-decoration-style: dotted;\r
+}\r
+.cm-s-liquibyte .CodeMirror-gutters { background-color: #262626; border-right: 1px solid #505050; padding-right: 0.8em; }\r
+.cm-s-liquibyte .CodeMirror-gutter-elt div { font-size: 1.2em; }\r
+.cm-s-liquibyte .CodeMirror-guttermarker {  }\r
+.cm-s-liquibyte .CodeMirror-guttermarker-subtle {  }\r
+.cm-s-liquibyte .CodeMirror-linenumber { color: #606060; padding-left: 0; }\r
+.cm-s-liquibyte .CodeMirror-cursor { border-left: 1px solid #eee; }\r
+\r
+.cm-s-liquibyte span.cm-comment     { color: #008000; }\r
+.cm-s-liquibyte span.cm-def         { color: #ffaf40; font-weight: bold; }\r
+.cm-s-liquibyte span.cm-keyword     { color: #c080ff; font-weight: bold; }\r
+.cm-s-liquibyte span.cm-builtin     { color: #ffaf40; font-weight: bold; }\r
+.cm-s-liquibyte span.cm-variable    { color: #5967ff; font-weight: bold; }\r
+.cm-s-liquibyte span.cm-string      { color: #ff8000; }\r
+.cm-s-liquibyte span.cm-number      { color: #0f0; font-weight: bold; }\r
+.cm-s-liquibyte span.cm-atom        { color: #bf3030; font-weight: bold; }\r
+\r
+.cm-s-liquibyte span.cm-variable-2  { color: #007f7f; font-weight: bold; }\r
+.cm-s-liquibyte span.cm-variable-3, .cm-s-liquibyte span.cm-type { color: #c080ff; font-weight: bold; }\r
+.cm-s-liquibyte span.cm-property    { color: #999; font-weight: bold; }\r
+.cm-s-liquibyte span.cm-operator    { color: #fff; }\r
+\r
+.cm-s-liquibyte span.cm-meta        { color: #0f0; }\r
+.cm-s-liquibyte span.cm-qualifier   { color: #fff700; font-weight: bold; }\r
+.cm-s-liquibyte span.cm-bracket     { color: #cc7; }\r
+.cm-s-liquibyte span.cm-tag         { color: #ff0; font-weight: bold; }\r
+.cm-s-liquibyte span.cm-attribute   { color: #c080ff; font-weight: bold; }\r
+.cm-s-liquibyte span.cm-error       { color: #f00; }\r
+\r
+.cm-s-liquibyte div.CodeMirror-selected { background-color: rgba(255, 0, 0, 0.25); }\r
+\r
+.cm-s-liquibyte span.cm-compilation { background-color: rgba(255, 255, 255, 0.12); }\r
+\r
+.cm-s-liquibyte .CodeMirror-activeline-background { background-color: rgba(0, 255, 0, 0.15); }\r
+\r
+/* Default styles for common addons */\r
+.cm-s-liquibyte .CodeMirror span.CodeMirror-matchingbracket { color: #0f0; font-weight: bold; }\r
+.cm-s-liquibyte .CodeMirror span.CodeMirror-nonmatchingbracket { color: #f00; font-weight: bold; }\r
+.CodeMirror-matchingtag { background-color: rgba(150, 255, 0, .3); }\r
+/* Scrollbars */\r
+/* Simple */\r
+.cm-s-liquibyte div.CodeMirror-simplescroll-horizontal div:hover, .cm-s-liquibyte div.CodeMirror-simplescroll-vertical div:hover {\r
+       background-color: rgba(80, 80, 80, .7);\r
+}\r
+.cm-s-liquibyte div.CodeMirror-simplescroll-horizontal div, .cm-s-liquibyte div.CodeMirror-simplescroll-vertical div {\r
+       background-color: rgba(80, 80, 80, .3);\r
+       border: 1px solid #404040;\r
+       border-radius: 5px;\r
+}\r
+.cm-s-liquibyte div.CodeMirror-simplescroll-vertical div {\r
+       border-top: 1px solid #404040;\r
+       border-bottom: 1px solid #404040;\r
+}\r
+.cm-s-liquibyte div.CodeMirror-simplescroll-horizontal div {\r
+       border-left: 1px solid #404040;\r
+       border-right: 1px solid #404040;\r
+}\r
+.cm-s-liquibyte div.CodeMirror-simplescroll-vertical {\r
+       background-color: #262626;\r
+}\r
+.cm-s-liquibyte div.CodeMirror-simplescroll-horizontal {\r
+       background-color: #262626;\r
+       border-top: 1px solid #404040;\r
+}\r
+/* Overlay */\r
+.cm-s-liquibyte div.CodeMirror-overlayscroll-horizontal div, div.CodeMirror-overlayscroll-vertical div {\r
+       background-color: #404040;\r
+       border-radius: 5px;\r
+}\r
+.cm-s-liquibyte div.CodeMirror-overlayscroll-vertical div {\r
+       border: 1px solid #404040;\r
+}\r
+.cm-s-liquibyte div.CodeMirror-overlayscroll-horizontal div {\r
+       border: 1px solid #404040;\r
+}\r
diff --git a/js/ckeditor/plugins/codemirror/theme/material.css b/js/ckeditor/plugins/codemirror/theme/material.css
new file mode 100644 (file)
index 0000000..634a01a
--- /dev/null
@@ -0,0 +1,53 @@
+/*\r
+\r
+    Name:       material\r
+    Author:     Michael Kaminsky (http://github.com/mkaminsky11)\r
+\r
+    Original material color scheme by Mattia Astorino (https://github.com/equinusocio/material-theme)\r
+\r
+*/\r
+\r
+.cm-s-material.CodeMirror {\r
+  background-color: #263238;\r
+  color: rgba(233, 237, 237, 1);\r
+}\r
+.cm-s-material .CodeMirror-gutters {\r
+  background: #263238;\r
+  color: rgb(83,127,126);\r
+  border: none;\r
+}\r
+.cm-s-material .CodeMirror-guttermarker, .cm-s-material .CodeMirror-guttermarker-subtle, .cm-s-material .CodeMirror-linenumber { color: rgb(83,127,126); }\r
+.cm-s-material .CodeMirror-cursor { border-left: 1px solid #f8f8f0; }\r
+.cm-s-material div.CodeMirror-selected { background: rgba(255, 255, 255, 0.15); }\r
+.cm-s-material.CodeMirror-focused div.CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\r
+.cm-s-material .CodeMirror-line::selection, .cm-s-material .CodeMirror-line > span::selection, .cm-s-material .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\r
+.cm-s-material .CodeMirror-line::-moz-selection, .cm-s-material .CodeMirror-line > span::-moz-selection, .cm-s-material .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\r
+\r
+.cm-s-material .CodeMirror-activeline-background { background: rgba(0, 0, 0, 0); }\r
+.cm-s-material .cm-keyword { color: rgba(199, 146, 234, 1); }\r
+.cm-s-material .cm-operator { color: rgba(233, 237, 237, 1); }\r
+.cm-s-material .cm-variable-2 { color: #80CBC4; }\r
+.cm-s-material .cm-variable-3, .cm-s-material .cm-type { color: #82B1FF; }\r
+.cm-s-material .cm-builtin { color: #DECB6B; }\r
+.cm-s-material .cm-atom { color: #F77669; }\r
+.cm-s-material .cm-number { color: #F77669; }\r
+.cm-s-material .cm-def { color: rgba(233, 237, 237, 1); }\r
+.cm-s-material .cm-string { color: #C3E88D; }\r
+.cm-s-material .cm-string-2 { color: #80CBC4; }\r
+.cm-s-material .cm-comment { color: #546E7A; }\r
+.cm-s-material .cm-variable { color: #82B1FF; }\r
+.cm-s-material .cm-tag { color: #80CBC4; }\r
+.cm-s-material .cm-meta { color: #80CBC4; }\r
+.cm-s-material .cm-attribute { color: #FFCB6B; }\r
+.cm-s-material .cm-property { color: #80CBAE; }\r
+.cm-s-material .cm-qualifier { color: #DECB6B; }\r
+.cm-s-material .cm-variable-3, .cm-s-material .cm-type { color: #DECB6B; }\r
+.cm-s-material .cm-tag { color: rgba(255, 83, 112, 1); }\r
+.cm-s-material .cm-error {\r
+  color: rgba(255, 255, 255, 1.0);\r
+  background-color: #EC5F67;\r
+}\r
+.cm-s-material .CodeMirror-matchingbracket {\r
+  text-decoration: underline;\r
+  color: white !important;\r
+}\r
diff --git a/js/ckeditor/plugins/codemirror/theme/mbo.css b/js/ckeditor/plugins/codemirror/theme/mbo.css
new file mode 100644 (file)
index 0000000..a64c0fd
--- /dev/null
@@ -0,0 +1,37 @@
+/****************************************************************/\r
+/*   Based on mbonaci's Brackets mbo theme                      */\r
+/*   https://github.com/mbonaci/global/blob/master/Mbo.tmTheme  */\r
+/*   Create your own: http://tmtheme-editor.herokuapp.com       */\r
+/****************************************************************/\r
+\r
+.cm-s-mbo.CodeMirror { background: #2c2c2c; color: #ffffec; }\r
+.cm-s-mbo div.CodeMirror-selected { background: #716C62; }\r
+.cm-s-mbo .CodeMirror-line::selection, .cm-s-mbo .CodeMirror-line > span::selection, .cm-s-mbo .CodeMirror-line > span > span::selection { background: rgba(113, 108, 98, .99); }\r
+.cm-s-mbo .CodeMirror-line::-moz-selection, .cm-s-mbo .CodeMirror-line > span::-moz-selection, .cm-s-mbo .CodeMirror-line > span > span::-moz-selection { background: rgba(113, 108, 98, .99); }\r
+.cm-s-mbo .CodeMirror-gutters { background: #4e4e4e; border-right: 0px; }\r
+.cm-s-mbo .CodeMirror-guttermarker { color: white; }\r
+.cm-s-mbo .CodeMirror-guttermarker-subtle { color: grey; }\r
+.cm-s-mbo .CodeMirror-linenumber { color: #dadada; }\r
+.cm-s-mbo .CodeMirror-cursor { border-left: 1px solid #ffffec; }\r
+\r
+.cm-s-mbo span.cm-comment { color: #95958a; }\r
+.cm-s-mbo span.cm-atom { color: #00a8c6; }\r
+.cm-s-mbo span.cm-number { color: #00a8c6; }\r
+\r
+.cm-s-mbo span.cm-property, .cm-s-mbo span.cm-attribute { color: #9ddfe9; }\r
+.cm-s-mbo span.cm-keyword { color: #ffb928; }\r
+.cm-s-mbo span.cm-string { color: #ffcf6c; }\r
+.cm-s-mbo span.cm-string.cm-property { color: #ffffec; }\r
+\r
+.cm-s-mbo span.cm-variable { color: #ffffec; }\r
+.cm-s-mbo span.cm-variable-2 { color: #00a8c6; }\r
+.cm-s-mbo span.cm-def { color: #ffffec; }\r
+.cm-s-mbo span.cm-bracket { color: #fffffc; font-weight: bold; }\r
+.cm-s-mbo span.cm-tag { color: #9ddfe9; }\r
+.cm-s-mbo span.cm-link { color: #f54b07; }\r
+.cm-s-mbo span.cm-error { border-bottom: #636363; color: #ffffec; }\r
+.cm-s-mbo span.cm-qualifier { color: #ffffec; }\r
+\r
+.cm-s-mbo .CodeMirror-activeline-background { background: #494b41; }\r
+.cm-s-mbo .CodeMirror-matchingbracket { color: #ffb928 !important; }\r
+.cm-s-mbo .CodeMirror-matchingtag { background: rgba(255, 255, 255, .37); }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/mdn-like.css b/js/ckeditor/plugins/codemirror/theme/mdn-like.css
new file mode 100644 (file)
index 0000000..c8c6de0
--- /dev/null
@@ -0,0 +1,46 @@
+/*\r
+  MDN-LIKE Theme - Mozilla\r
+  Ported to CodeMirror by Peter Kroon <plakroon@gmail.com>\r
+  Report bugs/issues here: https://github.com/codemirror/CodeMirror/issues\r
+  GitHub: @peterkroon\r
+\r
+  The mdn-like theme is inspired on the displayed code examples at: https://developer.mozilla.org/en-US/docs/Web/CSS/animation\r
+\r
+*/\r
+.cm-s-mdn-like.CodeMirror { color: #999; background-color: #fff; }\r
+.cm-s-mdn-like div.CodeMirror-selected { background: #cfc; }\r
+.cm-s-mdn-like .CodeMirror-line::selection, .cm-s-mdn-like .CodeMirror-line > span::selection, .cm-s-mdn-like .CodeMirror-line > span > span::selection { background: #cfc; }\r
+.cm-s-mdn-like .CodeMirror-line::-moz-selection, .cm-s-mdn-like .CodeMirror-line > span::-moz-selection, .cm-s-mdn-like .CodeMirror-line > span > span::-moz-selection { background: #cfc; }\r
+\r
+.cm-s-mdn-like .CodeMirror-gutters { background: #f8f8f8; border-left: 6px solid rgba(0,83,159,0.65); color: #333; }\r
+.cm-s-mdn-like .CodeMirror-linenumber { color: #aaa; padding-left: 8px; }\r
+.cm-s-mdn-like .CodeMirror-cursor { border-left: 2px solid #222; }\r
+\r
+.cm-s-mdn-like .cm-keyword { color: #6262FF; }\r
+.cm-s-mdn-like .cm-atom { color: #F90; }\r
+.cm-s-mdn-like .cm-number { color:  #ca7841; }\r
+.cm-s-mdn-like .cm-def { color: #8DA6CE; }\r
+.cm-s-mdn-like span.cm-variable-2, .cm-s-mdn-like span.cm-tag { color: #690; }\r
+.cm-s-mdn-like span.cm-variable-3, .cm-s-mdn-like span.cm-def, .cm-s-mdn-like span.cm-type { color: #07a; }\r
+\r
+.cm-s-mdn-like .cm-variable { color: #07a; }\r
+.cm-s-mdn-like .cm-property { color: #905; }\r
+.cm-s-mdn-like .cm-qualifier { color: #690; }\r
+\r
+.cm-s-mdn-like .cm-operator { color: #cda869; }\r
+.cm-s-mdn-like .cm-comment { color:#777; font-weight:normal; }\r
+.cm-s-mdn-like .cm-string { color:#07a; font-style:italic; }\r
+.cm-s-mdn-like .cm-string-2 { color:#bd6b18; } /*?*/\r
+.cm-s-mdn-like .cm-meta { color: #000; } /*?*/\r
+.cm-s-mdn-like .cm-builtin { color: #9B7536; } /*?*/\r
+.cm-s-mdn-like .cm-tag { color: #997643; }\r
+.cm-s-mdn-like .cm-attribute { color: #d6bb6d; } /*?*/\r
+.cm-s-mdn-like .cm-header { color: #FF6400; }\r
+.cm-s-mdn-like .cm-hr { color: #AEAEAE; }\r
+.cm-s-mdn-like .cm-link { color:#ad9361; font-style:italic; text-decoration:none; }\r
+.cm-s-mdn-like .cm-error { border-bottom: 1px solid red; }\r
+\r
+div.cm-s-mdn-like .CodeMirror-activeline-background { background: #efefff; }\r
+div.cm-s-mdn-like span.CodeMirror-matchingbracket { outline:1px solid grey; color: inherit; }\r
+\r
+.cm-s-mdn-like.CodeMirror { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFcAAAAyCAYAAAAp8UeFAAAHvklEQVR42s2b63bcNgyEQZCSHCdt2vd/0tWF7I+Q6XgMXiTtuvU5Pl57ZQKkKHzEAOtF5KeIJBGJ8uvL599FRFREZhFx8DeXv8trn68RuGaC8TRfo3SNp9dlDDHedyLyTUTeRWStXKPZrjtpZxaRw5hPqozRs1N8/enzIiQRWcCgy4MUA0f+XWliDhyL8Lfyvx7ei/Ae3iQFHyw7U/59pQVIMEEPEz0G7XiwdRjzSfC3UTtz9vchIntxvry5iMgfIhJoEflOz2CQr3F5h/HfeFe+GTdLaKcu9L8LTeQb/R/7GgbsfKedyNdoHsN31uRPWrfZ5wsj/NzzRQHuToIdU3ahwnsKPxXCjJITuOsi7XLc7SG/v5GdALs7wf8JjTFiB5+QvTEfRyGOfX3Lrx8wxyQi3sNq46O7QahQiCsRFgqddjBouVEHOKDgXAQHD9gJCr5sMKkEdjwsarG/ww3BMHBU7OBjXnzdyY7SfCxf5/z6ATccrwlKuwC/jhznnPF4CgVzhhVf4xp2EixcBActO75iZ8/fM9zAs2OMzKdslgXWJ9XG8PQoOAMA5fGcsvORgv0doBXyHrCwfLJAOwo71QLNkb8n2Pl6EWiR7OCibtkPaz4Kc/0NNAze2gju3zOwekALDaCFPI5vjPFmgGY5AZqyGEvH1x7QfIb8YtxMnA/b+QQ0aQDAwc6JMFg8CbQZ4qoYEEHbRwNojuK3EHwd7VALSgq+MNDKzfT58T8qdpADrgW0GmgcAS1lhzztJmkAzcPNOQbsWEALBDSlMKUG0Eq4CLAQWvEVQ9WU57gZJwZtgPO3r9oBTQ9WO8TjqXINx8R0EYpiZEUWOF3FxkbJkgU9B2f41YBrIj5ZfsQa0M5kTgiAAqM3ShXLgu8XMqcrQBvJ0CL5pnTsfMB13oB8athpAq2XOQmcGmoACCLydx7nToa23ATaSIY2ichfOdPTGxlasXMLaL0MLZAOwAKIM+y8CmicobGdCcbbK9DzN+yYGVoNNI5iUKTMyYOjPse4A8SM1MmcXgU0toOq1yO/v8FOxlASyc7TgeYaAMBJHcY1CcCwGI/TK4AmDbDyKYBBtFUkRwto8gygiQEaByFgJ00BH2M8JWwQS1nafDXQCidWyOI8AcjDCSjCLk8ngObuAm3JAHAdubAmOaK06V8MNEsKPJOhobSprwQa6gD7DclRQdqcwL4zxqgBrQcabUiBLclRDKAlWp+etPkBaNMA0AKlrHwTdEByZAA4GM+SNluSY6wAzcMNewxmgig5Ks0nkrSpBvSaQHMdKTBAnLojOdYyGpQ254602ZILPdTD1hdlggdIm74jbTp8vDwF5ZYUeLWGJpWsh6XNyXgcYwVoJQTEhhTYkxzZjiU5npU2TaB979TQehlaAVq4kaGpiPwwwLkYUuBbQwocyQTv1tA0+1UFWoJF3iv1oq+qoSk8EQdJmwHkziIF7oOZk14EGitibAdjLYYK78H5vZOhtWpoI0ATGHs0Q8OMb4Ey+2bU2UYztCtA0wFAs7TplGLRVQCcqaFdGSPCeTI1QNIC52iWNzof6Uib7xjEp07mNNoUYmVosVItHrHzRlLgBn9LFyRHaQCtVUMbtTNhoXWiTOO9k/V8BdAc1Oq0ArSQs6/5SU0hckNy9NnXqQY0PGYo5dWJ7nINaN6o958FWin27aBaWRka1r5myvLOAm0j30eBJqCxHLReVclxhxOEN2JfDWjxBtAC7MIH1fVaGdoOp4qJYDgKtKPSFNID2gSnGldrCqkFZ+5UeQXQBIRrSwocbdZYQT/2LwRahBPBXoHrB8nxaGROST62DKUbQOMMzZIC9abkuELfQzQALWTnDNAm8KHWFOJgJ5+SHIvTPcmx1xQyZRhNL5Qci689aXMEaN/uNIWkEwDAvFpOZmgsBaaGnbs1NPa1Jm32gBZAIh1pCtG7TSH4aE0y1uVY4uqoFPisGlpP2rSA5qTecWn5agK6BzSpgAyD+wFaqhnYoSZ1Vwr8CmlTQbrcO3ZaX0NAEyMbYaAlyquFoLKK3SPby9CeVUPThrSJmkCAE0CrKUQadi4DrdSlWhmah0YL9z9vClH59YGbHx1J8VZTyAjQepJjmXwAKTDQI3omc3p1U4gDUf6RfcdYfrUp5ClAi2J3Ba6UOXGo+K+bQrjjssitG2SJzshaLwMtXgRagUNpYYoVkMSBLM+9GGiJZMvduG6DRZ4qc04DMPtQQxOjEtACmhO7K1AbNbQDEggZyJwscFpAGwENhoBeUwh3bWolhe8BTYVKxQEWrSUn/uhcM5KhvUu/+eQu0Lzhi+VrK0PrZZNDQKs9cpYUuFYgMVpD4/NxenJTiMCNqdUEUf1qZWjppLT5qSkkUZbCwkbZMSuVnu80hfSkzRbQeqCZSAh6huR4VtoM2gHAlLf72smuWgE+VV7XpE25Ab2WFDgyhnSuKbs4GuGzCjR+tIoUuMFg3kgcWKLTwRqanJQ2W00hAsenfaApRC42hbCvK1SlE0HtE9BGgneJO+ELamitD1YjjOYnNYVcraGhtKkW0EqVVeDx733I2NH581k1NNxNLG0i0IJ8/NjVaOZ0tYZ2Vtr0Xv7tPV3hkWp9EFkgS/J0vosngTaSoaG06WHi+xObQkaAdlbanP8B2+2l0f90LmUAAAAASUVORK5CYII=); }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/midnight.css b/js/ckeditor/plugins/codemirror/theme/midnight.css
new file mode 100644 (file)
index 0000000..d9bdc2c
--- /dev/null
@@ -0,0 +1,43 @@
+/* Based on the theme at http://bonsaiden.github.com/JavaScript-Garden */\r
+\r
+/*<!--match-->*/\r
+.cm-s-midnight span.CodeMirror-matchhighlight { background: #494949; }\r
+.cm-s-midnight.CodeMirror-focused span.CodeMirror-matchhighlight { background: #314D67 !important; }\r
+\r
+/*<!--activeline-->*/\r
+.cm-s-midnight .CodeMirror-activeline-background { background: #253540; }\r
+\r
+.cm-s-midnight.CodeMirror {\r
+    background: #0F192A;\r
+    color: #D1EDFF;\r
+}\r
+\r
+.cm-s-midnight div.CodeMirror-selected { background: #314D67; }\r
+.cm-s-midnight .CodeMirror-line::selection, .cm-s-midnight .CodeMirror-line > span::selection, .cm-s-midnight .CodeMirror-line > span > span::selection { background: rgba(49, 77, 103, .99); }\r
+.cm-s-midnight .CodeMirror-line::-moz-selection, .cm-s-midnight .CodeMirror-line > span::-moz-selection, .cm-s-midnight .CodeMirror-line > span > span::-moz-selection { background: rgba(49, 77, 103, .99); }\r
+.cm-s-midnight .CodeMirror-gutters { background: #0F192A; border-right: 1px solid; }\r
+.cm-s-midnight .CodeMirror-guttermarker { color: white; }\r
+.cm-s-midnight .CodeMirror-guttermarker-subtle { color: #d0d0d0; }\r
+.cm-s-midnight .CodeMirror-linenumber { color: #D0D0D0; }\r
+.cm-s-midnight .CodeMirror-cursor { border-left: 1px solid #F8F8F0; }\r
+\r
+.cm-s-midnight span.cm-comment { color: #428BDD; }\r
+.cm-s-midnight span.cm-atom { color: #AE81FF; }\r
+.cm-s-midnight span.cm-number { color: #D1EDFF; }\r
+\r
+.cm-s-midnight span.cm-property, .cm-s-midnight span.cm-attribute { color: #A6E22E; }\r
+.cm-s-midnight span.cm-keyword { color: #E83737; }\r
+.cm-s-midnight span.cm-string { color: #1DC116; }\r
+\r
+.cm-s-midnight span.cm-variable { color: #FFAA3E; }\r
+.cm-s-midnight span.cm-variable-2 { color: #FFAA3E; }\r
+.cm-s-midnight span.cm-def { color: #4DD; }\r
+.cm-s-midnight span.cm-bracket { color: #D1EDFF; }\r
+.cm-s-midnight span.cm-tag { color: #449; }\r
+.cm-s-midnight span.cm-link { color: #AE81FF; }\r
+.cm-s-midnight span.cm-error { background: #F92672; color: #F8F8F0; }\r
+\r
+.cm-s-midnight .CodeMirror-matchingbracket {\r
+  text-decoration: underline;\r
+  color: white !important;\r
+}\r
diff --git a/js/ckeditor/plugins/codemirror/theme/monokai.css b/js/ckeditor/plugins/codemirror/theme/monokai.css
new file mode 100644 (file)
index 0000000..69be556
--- /dev/null
@@ -0,0 +1,36 @@
+/* Based on Sublime Text's Monokai theme */\r
+\r
+.cm-s-monokai.CodeMirror { background: #272822; color: #f8f8f2; }\r
+.cm-s-monokai div.CodeMirror-selected { background: #49483E; }\r
+.cm-s-monokai .CodeMirror-line::selection, .cm-s-monokai .CodeMirror-line > span::selection, .cm-s-monokai .CodeMirror-line > span > span::selection { background: rgba(73, 72, 62, .99); }\r
+.cm-s-monokai .CodeMirror-line::-moz-selection, .cm-s-monokai .CodeMirror-line > span::-moz-selection, .cm-s-monokai .CodeMirror-line > span > span::-moz-selection { background: rgba(73, 72, 62, .99); }\r
+.cm-s-monokai .CodeMirror-gutters { background: #272822; border-right: 0px; }\r
+.cm-s-monokai .CodeMirror-guttermarker { color: white; }\r
+.cm-s-monokai .CodeMirror-guttermarker-subtle { color: #d0d0d0; }\r
+.cm-s-monokai .CodeMirror-linenumber { color: #d0d0d0; }\r
+.cm-s-monokai .CodeMirror-cursor { border-left: 1px solid #f8f8f0; }\r
+\r
+.cm-s-monokai span.cm-comment { color: #75715e; }\r
+.cm-s-monokai span.cm-atom { color: #ae81ff; }\r
+.cm-s-monokai span.cm-number { color: #ae81ff; }\r
+\r
+.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute { color: #a6e22e; }\r
+.cm-s-monokai span.cm-keyword { color: #f92672; }\r
+.cm-s-monokai span.cm-builtin { color: #66d9ef; }\r
+.cm-s-monokai span.cm-string { color: #e6db74; }\r
+\r
+.cm-s-monokai span.cm-variable { color: #f8f8f2; }\r
+.cm-s-monokai span.cm-variable-2 { color: #9effff; }\r
+.cm-s-monokai span.cm-variable-3, .cm-s-monokai span.cm-type { color: #66d9ef; }\r
+.cm-s-monokai span.cm-def { color: #fd971f; }\r
+.cm-s-monokai span.cm-bracket { color: #f8f8f2; }\r
+.cm-s-monokai span.cm-tag { color: #f92672; }\r
+.cm-s-monokai span.cm-header { color: #ae81ff; }\r
+.cm-s-monokai span.cm-link { color: #ae81ff; }\r
+.cm-s-monokai span.cm-error { background: #f92672; color: #f8f8f0; }\r
+\r
+.cm-s-monokai .CodeMirror-activeline-background { background: #373831; }\r
+.cm-s-monokai .CodeMirror-matchingbracket {\r
+  text-decoration: underline;\r
+  color: white !important;\r
+}\r
diff --git a/js/ckeditor/plugins/codemirror/theme/neat.css b/js/ckeditor/plugins/codemirror/theme/neat.css
new file mode 100644 (file)
index 0000000..d454fbb
--- /dev/null
@@ -0,0 +1,12 @@
+.cm-s-neat span.cm-comment { color: #a86; }\r
+.cm-s-neat span.cm-keyword { line-height: 1em; font-weight: bold; color: blue; }\r
+.cm-s-neat span.cm-string { color: #a22; }\r
+.cm-s-neat span.cm-builtin { line-height: 1em; font-weight: bold; color: #077; }\r
+.cm-s-neat span.cm-special { line-height: 1em; font-weight: bold; color: #0aa; }\r
+.cm-s-neat span.cm-variable { color: black; }\r
+.cm-s-neat span.cm-number, .cm-s-neat span.cm-atom { color: #3a3; }\r
+.cm-s-neat span.cm-meta { color: #555; }\r
+.cm-s-neat span.cm-link { color: #3a3; }\r
+\r
+.cm-s-neat .CodeMirror-activeline-background { background: #e8f2ff; }\r
+.cm-s-neat .CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/neo.css b/js/ckeditor/plugins/codemirror/theme/neo.css
new file mode 100644 (file)
index 0000000..42a0a49
--- /dev/null
@@ -0,0 +1,43 @@
+/* neo theme for codemirror */\r
+\r
+/* Color scheme */\r
+\r
+.cm-s-neo.CodeMirror {\r
+  background-color:#ffffff;\r
+  color:#2e383c;\r
+  line-height:1.4375;\r
+}\r
+.cm-s-neo .cm-comment { color:#75787b; }\r
+.cm-s-neo .cm-keyword, .cm-s-neo .cm-property { color:#1d75b3; }\r
+.cm-s-neo .cm-atom,.cm-s-neo .cm-number { color:#75438a; }\r
+.cm-s-neo .cm-node,.cm-s-neo .cm-tag { color:#9c3328; }\r
+.cm-s-neo .cm-string { color:#b35e14; }\r
+.cm-s-neo .cm-variable,.cm-s-neo .cm-qualifier { color:#047d65; }\r
+\r
+\r
+/* Editor styling */\r
+\r
+.cm-s-neo pre {\r
+  padding:0;\r
+}\r
+\r
+.cm-s-neo .CodeMirror-gutters {\r
+  border:none;\r
+  border-right:10px solid transparent;\r
+  background-color:transparent;\r
+}\r
+\r
+.cm-s-neo .CodeMirror-linenumber {\r
+  padding:0;\r
+  color:#e0e2e5;\r
+}\r
+\r
+.cm-s-neo .CodeMirror-guttermarker { color: #1d75b3; }\r
+.cm-s-neo .CodeMirror-guttermarker-subtle { color: #e0e2e5; }\r
+\r
+.cm-s-neo .CodeMirror-cursor {\r
+  width: auto;\r
+  border: 0;\r
+  background: rgba(155,157,162,0.37);\r
+  z-index: 1;\r
+}\r
diff --git a/js/ckeditor/plugins/codemirror/theme/night.css b/js/ckeditor/plugins/codemirror/theme/night.css
new file mode 100644 (file)
index 0000000..07925c5
--- /dev/null
@@ -0,0 +1,27 @@
+/* Loosely based on the Midnight Textmate theme */\r
+\r
+.cm-s-night.CodeMirror { background: #0a001f; color: #f8f8f8; }\r
+.cm-s-night div.CodeMirror-selected { background: #447; }\r
+.cm-s-night .CodeMirror-line::selection, .cm-s-night .CodeMirror-line > span::selection, .cm-s-night .CodeMirror-line > span > span::selection { background: rgba(68, 68, 119, .99); }\r
+.cm-s-night .CodeMirror-line::-moz-selection, .cm-s-night .CodeMirror-line > span::-moz-selection, .cm-s-night .CodeMirror-line > span > span::-moz-selection { background: rgba(68, 68, 119, .99); }\r
+.cm-s-night .CodeMirror-gutters { background: #0a001f; border-right: 1px solid #aaa; }\r
+.cm-s-night .CodeMirror-guttermarker { color: white; }\r
+.cm-s-night .CodeMirror-guttermarker-subtle { color: #bbb; }\r
+.cm-s-night .CodeMirror-linenumber { color: #f8f8f8; }\r
+.cm-s-night .CodeMirror-cursor { border-left: 1px solid white; }\r
+\r
+.cm-s-night span.cm-comment { color: #8900d1; }\r
+.cm-s-night span.cm-atom { color: #845dc4; }\r
+.cm-s-night span.cm-number, .cm-s-night span.cm-attribute { color: #ffd500; }\r
+.cm-s-night span.cm-keyword { color: #599eff; }\r
+.cm-s-night span.cm-string { color: #37f14a; }\r
+.cm-s-night span.cm-meta { color: #7678e2; }\r
+.cm-s-night span.cm-variable-2, .cm-s-night span.cm-tag { color: #99b2ff; }\r
+.cm-s-night span.cm-variable-3, .cm-s-night span.cm-def, .cm-s-night span.cm-type { color: white; }\r
+.cm-s-night span.cm-bracket { color: #8da6ce; }\r
+.cm-s-night span.cm-builtin, .cm-s-night span.cm-special { color: #ff9e59; }\r
+.cm-s-night span.cm-link { color: #845dc4; }\r
+.cm-s-night span.cm-error { color: #9d1e15; }\r
+\r
+.cm-s-night .CodeMirror-activeline-background { background: #1C005A; }\r
+.cm-s-night .CodeMirror-matchingbracket { outline:1px solid grey; color:white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/panda-syntax.css b/js/ckeditor/plugins/codemirror/theme/panda-syntax.css
new file mode 100644 (file)
index 0000000..e9da8ca
--- /dev/null
@@ -0,0 +1,85 @@
+/*\r
+       Name:       Panda Syntax\r
+       Author:     Siamak Mokhtari (http://github.com/siamak/)\r
+       CodeMirror template by Siamak Mokhtari (https://github.com/siamak/atom-panda-syntax)\r
+*/\r
+.cm-s-panda-syntax {\r
+       background: #292A2B;\r
+       color: #E6E6E6;\r
+       line-height: 1.5;\r
+       font-family: 'Operator Mono', 'Source Sans Pro', Menlo, Monaco, Consolas, Courier New, monospace;\r
+}\r
+.cm-s-panda-syntax .CodeMirror-cursor { border-color: #ff2c6d; }\r
+.cm-s-panda-syntax .CodeMirror-activeline-background {\r
+       background: rgba(99, 123, 156, 0.1);\r
+}\r
+.cm-s-panda-syntax .CodeMirror-selected {\r
+       background: #FFF;\r
+}\r
+.cm-s-panda-syntax .cm-comment {\r
+       font-style: italic;\r
+       color: #676B79;\r
+}\r
+.cm-s-panda-syntax .cm-operator {\r
+       color: #f3f3f3;\r
+}\r
+.cm-s-panda-syntax .cm-string {\r
+       color: #19F9D8;\r
+}\r
+.cm-s-panda-syntax .cm-string-2 {\r
+    color: #FFB86C;\r
+}\r
+\r
+.cm-s-panda-syntax .cm-tag {\r
+       color: #ff2c6d;\r
+}\r
+.cm-s-panda-syntax .cm-meta {\r
+       color: #b084eb;\r
+}\r
+\r
+.cm-s-panda-syntax .cm-number {\r
+       color: #FFB86C;\r
+}\r
+.cm-s-panda-syntax .cm-atom {\r
+       color: #ff2c6d;\r
+}\r
+.cm-s-panda-syntax .cm-keyword {\r
+       color: #FF75B5;\r
+}\r
+.cm-s-panda-syntax .cm-variable {\r
+       color: #ffb86c;\r
+}\r
+.cm-s-panda-syntax .cm-variable-2 {\r
+       color: #ff9ac1;\r
+}\r
+.cm-s-panda-syntax .cm-variable-3, .cm-s-panda-syntax .cm-type {\r
+       color: #ff9ac1;\r
+}\r
+\r
+.cm-s-panda-syntax .cm-def {\r
+       color: #e6e6e6;\r
+}\r
+.cm-s-panda-syntax .cm-property {\r
+       color: #f3f3f3;\r
+}\r
+.cm-s-panda-syntax .cm-unit {\r
+    color: #ffb86c;\r
+}\r
+\r
+.cm-s-panda-syntax .cm-attribute {\r
+    color: #ffb86c;\r
+}\r
+\r
+.cm-s-panda-syntax .CodeMirror-matchingbracket {\r
+    border-bottom: 1px dotted #19F9D8;\r
+    padding-bottom: 2px;\r
+    color: #e6e6e6;\r
+}\r
+.cm-s-panda-syntax .CodeMirror-gutters {\r
+    background: #292a2b;\r
+    border-right-color: rgba(255, 255, 255, 0.1);\r
+}\r
+.cm-s-panda-syntax .CodeMirror-linenumber {\r
+    color: #e6e6e6;\r
+    opacity: 0.6;\r
+}\r
diff --git a/js/ckeditor/plugins/codemirror/theme/paraiso-dark.css b/js/ckeditor/plugins/codemirror/theme/paraiso-dark.css
new file mode 100644 (file)
index 0000000..25eb926
--- /dev/null
@@ -0,0 +1,38 @@
+/*\r
+\r
+    Name:       Paraíso (Dark)\r
+    Author:     Jan T. Sott\r
+\r
+    Color scheme by Jan T. Sott (https://github.com/idleberg/Paraiso-CodeMirror)\r
+    Inspired by the art of Rubens LP (http://www.rubenslp.com.br)\r
+\r
+*/\r
+\r
+.cm-s-paraiso-dark.CodeMirror { background: #2f1e2e; color: #b9b6b0; }\r
+.cm-s-paraiso-dark div.CodeMirror-selected { background: #41323f; }\r
+.cm-s-paraiso-dark .CodeMirror-line::selection, .cm-s-paraiso-dark .CodeMirror-line > span::selection, .cm-s-paraiso-dark .CodeMirror-line > span > span::selection { background: rgba(65, 50, 63, .99); }\r
+.cm-s-paraiso-dark .CodeMirror-line::-moz-selection, .cm-s-paraiso-dark .CodeMirror-line > span::-moz-selection, .cm-s-paraiso-dark .CodeMirror-line > span > span::-moz-selection { background: rgba(65, 50, 63, .99); }\r
+.cm-s-paraiso-dark .CodeMirror-gutters { background: #2f1e2e; border-right: 0px; }\r
+.cm-s-paraiso-dark .CodeMirror-guttermarker { color: #ef6155; }\r
+.cm-s-paraiso-dark .CodeMirror-guttermarker-subtle { color: #776e71; }\r
+.cm-s-paraiso-dark .CodeMirror-linenumber { color: #776e71; }\r
+.cm-s-paraiso-dark .CodeMirror-cursor { border-left: 1px solid #8d8687; }\r
+\r
+.cm-s-paraiso-dark span.cm-comment { color: #e96ba8; }\r
+.cm-s-paraiso-dark span.cm-atom { color: #815ba4; }\r
+.cm-s-paraiso-dark span.cm-number { color: #815ba4; }\r
+\r
+.cm-s-paraiso-dark span.cm-property, .cm-s-paraiso-dark span.cm-attribute { color: #48b685; }\r
+.cm-s-paraiso-dark span.cm-keyword { color: #ef6155; }\r
+.cm-s-paraiso-dark span.cm-string { color: #fec418; }\r
+\r
+.cm-s-paraiso-dark span.cm-variable { color: #48b685; }\r
+.cm-s-paraiso-dark span.cm-variable-2 { color: #06b6ef; }\r
+.cm-s-paraiso-dark span.cm-def { color: #f99b15; }\r
+.cm-s-paraiso-dark span.cm-bracket { color: #b9b6b0; }\r
+.cm-s-paraiso-dark span.cm-tag { color: #ef6155; }\r
+.cm-s-paraiso-dark span.cm-link { color: #815ba4; }\r
+.cm-s-paraiso-dark span.cm-error { background: #ef6155; color: #8d8687; }\r
+\r
+.cm-s-paraiso-dark .CodeMirror-activeline-background { background: #4D344A; }\r
+.cm-s-paraiso-dark .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/paraiso-light.css b/js/ckeditor/plugins/codemirror/theme/paraiso-light.css
new file mode 100644 (file)
index 0000000..479792e
--- /dev/null
@@ -0,0 +1,38 @@
+/*\r
+\r
+    Name:       Paraíso (Light)\r
+    Author:     Jan T. Sott\r
+\r
+    Color scheme by Jan T. Sott (https://github.com/idleberg/Paraiso-CodeMirror)\r
+    Inspired by the art of Rubens LP (http://www.rubenslp.com.br)\r
+\r
+*/\r
+\r
+.cm-s-paraiso-light.CodeMirror { background: #e7e9db; color: #41323f; }\r
+.cm-s-paraiso-light div.CodeMirror-selected { background: #b9b6b0; }\r
+.cm-s-paraiso-light .CodeMirror-line::selection, .cm-s-paraiso-light .CodeMirror-line > span::selection, .cm-s-paraiso-light .CodeMirror-line > span > span::selection { background: #b9b6b0; }\r
+.cm-s-paraiso-light .CodeMirror-line::-moz-selection, .cm-s-paraiso-light .CodeMirror-line > span::-moz-selection, .cm-s-paraiso-light .CodeMirror-line > span > span::-moz-selection { background: #b9b6b0; }\r
+.cm-s-paraiso-light .CodeMirror-gutters { background: #e7e9db; border-right: 0px; }\r
+.cm-s-paraiso-light .CodeMirror-guttermarker { color: black; }\r
+.cm-s-paraiso-light .CodeMirror-guttermarker-subtle { color: #8d8687; }\r
+.cm-s-paraiso-light .CodeMirror-linenumber { color: #8d8687; }\r
+.cm-s-paraiso-light .CodeMirror-cursor { border-left: 1px solid #776e71; }\r
+\r
+.cm-s-paraiso-light span.cm-comment { color: #e96ba8; }\r
+.cm-s-paraiso-light span.cm-atom { color: #815ba4; }\r
+.cm-s-paraiso-light span.cm-number { color: #815ba4; }\r
+\r
+.cm-s-paraiso-light span.cm-property, .cm-s-paraiso-light span.cm-attribute { color: #48b685; }\r
+.cm-s-paraiso-light span.cm-keyword { color: #ef6155; }\r
+.cm-s-paraiso-light span.cm-string { color: #fec418; }\r
+\r
+.cm-s-paraiso-light span.cm-variable { color: #48b685; }\r
+.cm-s-paraiso-light span.cm-variable-2 { color: #06b6ef; }\r
+.cm-s-paraiso-light span.cm-def { color: #f99b15; }\r
+.cm-s-paraiso-light span.cm-bracket { color: #41323f; }\r
+.cm-s-paraiso-light span.cm-tag { color: #ef6155; }\r
+.cm-s-paraiso-light span.cm-link { color: #815ba4; }\r
+.cm-s-paraiso-light span.cm-error { background: #ef6155; color: #776e71; }\r
+\r
+.cm-s-paraiso-light .CodeMirror-activeline-background { background: #CFD1C4; }\r
+.cm-s-paraiso-light .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/pastel-on-dark.css b/js/ckeditor/plugins/codemirror/theme/pastel-on-dark.css
new file mode 100644 (file)
index 0000000..9fcfea8
--- /dev/null
@@ -0,0 +1,52 @@
+/**\r
+ * Pastel On Dark theme ported from ACE editor\r
+ * @license MIT\r
+ * @copyright AtomicPages LLC 2014\r
+ * @author Dennis Thompson, AtomicPages LLC\r
+ * @version 1.1\r
+ * @source https://github.com/atomicpages/codemirror-pastel-on-dark-theme\r
+ */\r
+\r
+.cm-s-pastel-on-dark.CodeMirror {\r
+       background: #2c2827;\r
+       color: #8F938F;\r
+       line-height: 1.5;\r
+}\r
+.cm-s-pastel-on-dark div.CodeMirror-selected { background: rgba(221,240,255,0.2); }\r
+.cm-s-pastel-on-dark .CodeMirror-line::selection, .cm-s-pastel-on-dark .CodeMirror-line > span::selection, .cm-s-pastel-on-dark .CodeMirror-line > span > span::selection { background: rgba(221,240,255,0.2); }\r
+.cm-s-pastel-on-dark .CodeMirror-line::-moz-selection, .cm-s-pastel-on-dark .CodeMirror-line > span::-moz-selection, .cm-s-pastel-on-dark .CodeMirror-line > span > span::-moz-selection { background: rgba(221,240,255,0.2); }\r
+\r
+.cm-s-pastel-on-dark .CodeMirror-gutters {\r
+       background: #34302f;\r
+       border-right: 0px;\r
+       padding: 0 3px;\r
+}\r
+.cm-s-pastel-on-dark .CodeMirror-guttermarker { color: white; }\r
+.cm-s-pastel-on-dark .CodeMirror-guttermarker-subtle { color: #8F938F; }\r
+.cm-s-pastel-on-dark .CodeMirror-linenumber { color: #8F938F; }\r
+.cm-s-pastel-on-dark .CodeMirror-cursor { border-left: 1px solid #A7A7A7; }\r
+.cm-s-pastel-on-dark span.cm-comment { color: #A6C6FF; }\r
+.cm-s-pastel-on-dark span.cm-atom { color: #DE8E30; }\r
+.cm-s-pastel-on-dark span.cm-number { color: #CCCCCC; }\r
+.cm-s-pastel-on-dark span.cm-property { color: #8F938F; }\r
+.cm-s-pastel-on-dark span.cm-attribute { color: #a6e22e; }\r
+.cm-s-pastel-on-dark span.cm-keyword { color: #AEB2F8; }\r
+.cm-s-pastel-on-dark span.cm-string { color: #66A968; }\r
+.cm-s-pastel-on-dark span.cm-variable { color: #AEB2F8; }\r
+.cm-s-pastel-on-dark span.cm-variable-2 { color: #BEBF55; }\r
+.cm-s-pastel-on-dark span.cm-variable-3, .cm-s-pastel-on-dark span.cm-type { color: #DE8E30; }\r
+.cm-s-pastel-on-dark span.cm-def { color: #757aD8; }\r
+.cm-s-pastel-on-dark span.cm-bracket { color: #f8f8f2; }\r
+.cm-s-pastel-on-dark span.cm-tag { color: #C1C144; }\r
+.cm-s-pastel-on-dark span.cm-link { color: #ae81ff; }\r
+.cm-s-pastel-on-dark span.cm-qualifier,.cm-s-pastel-on-dark span.cm-builtin { color: #C1C144; }\r
+.cm-s-pastel-on-dark span.cm-error {\r
+       background: #757aD8;\r
+       color: #f8f8f0;\r
+}\r
+.cm-s-pastel-on-dark .CodeMirror-activeline-background { background: rgba(255, 255, 255, 0.031); }\r
+.cm-s-pastel-on-dark .CodeMirror-matchingbracket {\r
+       border: 1px solid rgba(255,255,255,0.25);\r
+       color: #8F938F !important;\r
+       margin: -1px -1px 0 -1px;\r
+}\r
diff --git a/js/ckeditor/plugins/codemirror/theme/railscasts.css b/js/ckeditor/plugins/codemirror/theme/railscasts.css
new file mode 100644 (file)
index 0000000..2336e7c
--- /dev/null
@@ -0,0 +1,34 @@
+/*\r
+\r
+    Name:       Railscasts\r
+    Author:     Ryan Bates (http://railscasts.com)\r
+\r
+    CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)\r
+    Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)\r
+\r
+*/\r
+\r
+.cm-s-railscasts.CodeMirror {background: #2b2b2b; color: #f4f1ed;}\r
+.cm-s-railscasts div.CodeMirror-selected {background: #272935 !important;}\r
+.cm-s-railscasts .CodeMirror-gutters {background: #2b2b2b; border-right: 0px;}\r
+.cm-s-railscasts .CodeMirror-linenumber {color: #5a647e;}\r
+.cm-s-railscasts .CodeMirror-cursor {border-left: 1px solid #d4cfc9 !important;}\r
+\r
+.cm-s-railscasts span.cm-comment {color: #bc9458;}\r
+.cm-s-railscasts span.cm-atom {color: #b6b3eb;}\r
+.cm-s-railscasts span.cm-number {color: #b6b3eb;}\r
+\r
+.cm-s-railscasts span.cm-property, .cm-s-railscasts span.cm-attribute {color: #a5c261;}\r
+.cm-s-railscasts span.cm-keyword {color: #da4939;}\r
+.cm-s-railscasts span.cm-string {color: #ffc66d;}\r
+\r
+.cm-s-railscasts span.cm-variable {color: #a5c261;}\r
+.cm-s-railscasts span.cm-variable-2 {color: #6d9cbe;}\r
+.cm-s-railscasts span.cm-def {color: #cc7833;}\r
+.cm-s-railscasts span.cm-error {background: #da4939; color: #d4cfc9;}\r
+.cm-s-railscasts span.cm-bracket {color: #f4f1ed;}\r
+.cm-s-railscasts span.cm-tag {color: #da4939;}\r
+.cm-s-railscasts span.cm-link {color: #b6b3eb;}\r
+\r
+.cm-s-railscasts .CodeMirror-matchingbracket { text-decoration: underline; color: white !important;}\r
+.cm-s-railscasts .CodeMirror-activeline-background { background: #303040; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/rubyblue.css b/js/ckeditor/plugins/codemirror/theme/rubyblue.css
new file mode 100644 (file)
index 0000000..8d4c17f
--- /dev/null
@@ -0,0 +1,25 @@
+.cm-s-rubyblue.CodeMirror { background: #112435; color: white; }\r
+.cm-s-rubyblue div.CodeMirror-selected { background: #38566F; }\r
+.cm-s-rubyblue .CodeMirror-line::selection, .cm-s-rubyblue .CodeMirror-line > span::selection, .cm-s-rubyblue .CodeMirror-line > span > span::selection { background: rgba(56, 86, 111, 0.99); }\r
+.cm-s-rubyblue .CodeMirror-line::-moz-selection, .cm-s-rubyblue .CodeMirror-line > span::-moz-selection, .cm-s-rubyblue .CodeMirror-line > span > span::-moz-selection { background: rgba(56, 86, 111, 0.99); }\r
+.cm-s-rubyblue .CodeMirror-gutters { background: #1F4661; border-right: 7px solid #3E7087; }\r
+.cm-s-rubyblue .CodeMirror-guttermarker { color: white; }\r
+.cm-s-rubyblue .CodeMirror-guttermarker-subtle { color: #3E7087; }\r
+.cm-s-rubyblue .CodeMirror-linenumber { color: white; }\r
+.cm-s-rubyblue .CodeMirror-cursor { border-left: 1px solid white; }\r
+\r
+.cm-s-rubyblue span.cm-comment { color: #999; font-style:italic; line-height: 1em; }\r
+.cm-s-rubyblue span.cm-atom { color: #F4C20B; }\r
+.cm-s-rubyblue span.cm-number, .cm-s-rubyblue span.cm-attribute { color: #82C6E0; }\r
+.cm-s-rubyblue span.cm-keyword { color: #F0F; }\r
+.cm-s-rubyblue span.cm-string { color: #F08047; }\r
+.cm-s-rubyblue span.cm-meta { color: #F0F; }\r
+.cm-s-rubyblue span.cm-variable-2, .cm-s-rubyblue span.cm-tag { color: #7BD827; }\r
+.cm-s-rubyblue span.cm-variable-3, .cm-s-rubyblue span.cm-def, .cm-s-rubyblue span.cm-type { color: white; }\r
+.cm-s-rubyblue span.cm-bracket { color: #F0F; }\r
+.cm-s-rubyblue span.cm-link { color: #F4C20B; }\r
+.cm-s-rubyblue span.CodeMirror-matchingbracket { color:#F0F !important; }\r
+.cm-s-rubyblue span.cm-builtin, .cm-s-rubyblue span.cm-special { color: #FF9D00; }\r
+.cm-s-rubyblue span.cm-error { color: #AF2018; }\r
+\r
+.cm-s-rubyblue .CodeMirror-activeline-background { background: #173047; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/seti.css b/js/ckeditor/plugins/codemirror/theme/seti.css
new file mode 100644 (file)
index 0000000..400f0a5
--- /dev/null
@@ -0,0 +1,44 @@
+/*\r
+\r
+    Name:       seti\r
+    Author:     Michael Kaminsky (http://github.com/mkaminsky11)\r
+\r
+    Original seti color scheme by Jesse Weed (https://github.com/jesseweed/seti-syntax)\r
+\r
+*/\r
+\r
+\r
+.cm-s-seti.CodeMirror {\r
+  background-color: #151718 !important;\r
+  color: #CFD2D1 !important;\r
+  border: none;\r
+}\r
+.cm-s-seti .CodeMirror-gutters {\r
+  color: #404b53;\r
+  background-color: #0E1112;\r
+  border: none;\r
+}\r
+.cm-s-seti .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\r
+.cm-s-seti .CodeMirror-linenumber { color: #6D8A88; }\r
+.cm-s-seti.CodeMirror-focused div.CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\r
+.cm-s-seti .CodeMirror-line::selection, .cm-s-seti .CodeMirror-line > span::selection, .cm-s-seti .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\r
+.cm-s-seti .CodeMirror-line::-moz-selection, .cm-s-seti .CodeMirror-line > span::-moz-selection, .cm-s-seti .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\r
+.cm-s-seti span.cm-comment { color: #41535b; }\r
+.cm-s-seti span.cm-string, .cm-s-seti span.cm-string-2 { color: #55b5db; }\r
+.cm-s-seti span.cm-number { color: #cd3f45; }\r
+.cm-s-seti span.cm-variable { color: #55b5db; }\r
+.cm-s-seti span.cm-variable-2 { color: #a074c4; }\r
+.cm-s-seti span.cm-def { color: #55b5db; }\r
+.cm-s-seti span.cm-keyword { color: #ff79c6; }\r
+.cm-s-seti span.cm-operator { color: #9fca56; }\r
+.cm-s-seti span.cm-keyword { color: #e6cd69; }\r
+.cm-s-seti span.cm-atom { color: #cd3f45; }\r
+.cm-s-seti span.cm-meta { color: #55b5db; }\r
+.cm-s-seti span.cm-tag { color: #55b5db; }\r
+.cm-s-seti span.cm-attribute { color: #9fca56; }\r
+.cm-s-seti span.cm-qualifier { color: #9fca56; }\r
+.cm-s-seti span.cm-property { color: #a074c4; }\r
+.cm-s-seti span.cm-variable-3, .cm-s-seti span.cm-type { color: #9fca56; }\r
+.cm-s-seti span.cm-builtin { color: #9fca56; }\r
+.cm-s-seti .CodeMirror-activeline-background { background: #101213; }\r
+.cm-s-seti .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/solarized.css b/js/ckeditor/plugins/codemirror/theme/solarized.css
new file mode 100644 (file)
index 0000000..44777cb
--- /dev/null
@@ -0,0 +1,169 @@
+/*\r
+Solarized theme for code-mirror\r
+http://ethanschoonover.com/solarized\r
+*/\r
+\r
+/*\r
+Solarized color palette\r
+http://ethanschoonover.com/solarized/img/solarized-palette.png\r
+*/\r
+\r
+.solarized.base03 { color: #002b36; }\r
+.solarized.base02 { color: #073642; }\r
+.solarized.base01 { color: #586e75; }\r
+.solarized.base00 { color: #657b83; }\r
+.solarized.base0 { color: #839496; }\r
+.solarized.base1 { color: #93a1a1; }\r
+.solarized.base2 { color: #eee8d5; }\r
+.solarized.base3  { color: #fdf6e3; }\r
+.solarized.solar-yellow  { color: #b58900; }\r
+.solarized.solar-orange  { color: #cb4b16; }\r
+.solarized.solar-red { color: #dc322f; }\r
+.solarized.solar-magenta { color: #d33682; }\r
+.solarized.solar-violet  { color: #6c71c4; }\r
+.solarized.solar-blue { color: #268bd2; }\r
+.solarized.solar-cyan { color: #2aa198; }\r
+.solarized.solar-green { color: #859900; }\r
+\r
+/* Color scheme for code-mirror */\r
+\r
+.cm-s-solarized {\r
+  line-height: 1.45em;\r
+  color-profile: sRGB;\r
+  rendering-intent: auto;\r
+}\r
+.cm-s-solarized.cm-s-dark {\r
+  color: #839496;\r
+  background-color: #002b36;\r
+  text-shadow: #002b36 0 1px;\r
+}\r
+.cm-s-solarized.cm-s-light {\r
+  background-color: #fdf6e3;\r
+  color: #657b83;\r
+  text-shadow: #eee8d5 0 1px;\r
+}\r
+\r
+.cm-s-solarized .CodeMirror-widget {\r
+  text-shadow: none;\r
+}\r
+\r
+.cm-s-solarized .cm-header { color: #586e75; }\r
+.cm-s-solarized .cm-quote { color: #93a1a1; }\r
+\r
+.cm-s-solarized .cm-keyword { color: #cb4b16; }\r
+.cm-s-solarized .cm-atom { color: #d33682; }\r
+.cm-s-solarized .cm-number { color: #d33682; }\r
+.cm-s-solarized .cm-def { color: #2aa198; }\r
+\r
+.cm-s-solarized .cm-variable { color: #839496; }\r
+.cm-s-solarized .cm-variable-2 { color: #b58900; }\r
+.cm-s-solarized .cm-variable-3, .cm-s-solarized .cm-type { color: #6c71c4; }\r
+\r
+.cm-s-solarized .cm-property { color: #2aa198; }\r
+.cm-s-solarized .cm-operator { color: #6c71c4; }\r
+\r
+.cm-s-solarized .cm-comment { color: #586e75; font-style:italic; }\r
+\r
+.cm-s-solarized .cm-string { color: #859900; }\r
+.cm-s-solarized .cm-string-2 { color: #b58900; }\r
+\r
+.cm-s-solarized .cm-meta { color: #859900; }\r
+.cm-s-solarized .cm-qualifier { color: #b58900; }\r
+.cm-s-solarized .cm-builtin { color: #d33682; }\r
+.cm-s-solarized .cm-bracket { color: #cb4b16; }\r
+.cm-s-solarized .CodeMirror-matchingbracket { color: #859900; }\r
+.cm-s-solarized .CodeMirror-nonmatchingbracket { color: #dc322f; }\r
+.cm-s-solarized .cm-tag { color: #93a1a1; }\r
+.cm-s-solarized .cm-attribute { color: #2aa198; }\r
+.cm-s-solarized .cm-hr {\r
+  color: transparent;\r
+  border-top: 1px solid #586e75;\r
+  display: block;\r
+}\r
+.cm-s-solarized .cm-link { color: #93a1a1; cursor: pointer; }\r
+.cm-s-solarized .cm-special { color: #6c71c4; }\r
+.cm-s-solarized .cm-em {\r
+  color: #999;\r
+  text-decoration: underline;\r
+  text-decoration-style: dotted;\r
+}\r
+.cm-s-solarized .cm-strong { color: #eee; }\r
+.cm-s-solarized .cm-error,\r
+.cm-s-solarized .cm-invalidchar {\r
+  color: #586e75;\r
+  border-bottom: 1px dotted #dc322f;\r
+}\r
+\r
+.cm-s-solarized.cm-s-dark div.CodeMirror-selected { background: #073642; }\r
+.cm-s-solarized.cm-s-dark.CodeMirror ::selection { background: rgba(7, 54, 66, 0.99); }\r
+.cm-s-solarized.cm-s-dark .CodeMirror-line::-moz-selection, .cm-s-dark .CodeMirror-line > span::-moz-selection, .cm-s-dark .CodeMirror-line > span > span::-moz-selection { background: rgba(7, 54, 66, 0.99); }\r
+\r
+.cm-s-solarized.cm-s-light div.CodeMirror-selected { background: #eee8d5; }\r
+.cm-s-solarized.cm-s-light .CodeMirror-line::selection, .cm-s-light .CodeMirror-line > span::selection, .cm-s-light .CodeMirror-line > span > span::selection { background: #eee8d5; }\r
+.cm-s-solarized.cm-s-light .CodeMirror-line::-moz-selection, .cm-s-ligh .CodeMirror-line > span::-moz-selection, .cm-s-ligh .CodeMirror-line > span > span::-moz-selection { background: #eee8d5; }\r
+\r
+/* Editor styling */\r
+\r
+\r
+\r
+/* Little shadow on the view-port of the buffer view */\r
+.cm-s-solarized.CodeMirror {\r
+  -moz-box-shadow: inset 7px 0 12px -6px #000;\r
+  -webkit-box-shadow: inset 7px 0 12px -6px #000;\r
+  box-shadow: inset 7px 0 12px -6px #000;\r
+}\r
+\r
+/* Remove gutter border */\r
+.cm-s-solarized .CodeMirror-gutters {\r
+  border-right: 0;\r
+}\r
+\r
+/* Gutter colors and line number styling based of color scheme (dark / light) */\r
+\r
+/* Dark */\r
+.cm-s-solarized.cm-s-dark .CodeMirror-gutters {\r
+  background-color: #073642;\r
+}\r
+\r
+.cm-s-solarized.cm-s-dark .CodeMirror-linenumber {\r
+  color: #586e75;\r
+  text-shadow: #021014 0 -1px;\r
+}\r
+\r
+/* Light */\r
+.cm-s-solarized.cm-s-light .CodeMirror-gutters {\r
+  background-color: #eee8d5;\r
+}\r
+\r
+.cm-s-solarized.cm-s-light .CodeMirror-linenumber {\r
+  color: #839496;\r
+}\r
+\r
+/* Common */\r
+.cm-s-solarized .CodeMirror-linenumber {\r
+  padding: 0 5px;\r
+}\r
+.cm-s-solarized .CodeMirror-guttermarker-subtle { color: #586e75; }\r
+.cm-s-solarized.cm-s-dark .CodeMirror-guttermarker { color: #ddd; }\r
+.cm-s-solarized.cm-s-light .CodeMirror-guttermarker { color: #cb4b16; }\r
+\r
+.cm-s-solarized .CodeMirror-gutter .CodeMirror-gutter-text {\r
+  color: #586e75;\r
+}\r
+\r
+/* Cursor */\r
+.cm-s-solarized .CodeMirror-cursor { border-left: 1px solid #819090; }\r
+\r
+/* Fat cursor */\r
+.cm-s-solarized.cm-s-light.cm-fat-cursor .CodeMirror-cursor { background: #77ee77; }\r
+.cm-s-solarized.cm-s-light .cm-animate-fat-cursor { background-color: #77ee77; }\r
+.cm-s-solarized.cm-s-dark.cm-fat-cursor .CodeMirror-cursor { background: #586e75; }\r
+.cm-s-solarized.cm-s-dark .cm-animate-fat-cursor { background-color: #586e75; }\r
+\r
+/* Active line */\r
+.cm-s-solarized.cm-s-dark .CodeMirror-activeline-background {\r
+  background: rgba(255, 255, 255, 0.06);\r
+}\r
+.cm-s-solarized.cm-s-light .CodeMirror-activeline-background {\r
+  background: rgba(0, 0, 0, 0.06);\r
+}\r
diff --git a/js/ckeditor/plugins/codemirror/theme/the-matrix.css b/js/ckeditor/plugins/codemirror/theme/the-matrix.css
new file mode 100644 (file)
index 0000000..2106c19
--- /dev/null
@@ -0,0 +1,30 @@
+.cm-s-the-matrix.CodeMirror { background: #000000; color: #00FF00; }\r
+.cm-s-the-matrix div.CodeMirror-selected { background: #2D2D2D; }\r
+.cm-s-the-matrix .CodeMirror-line::selection, .cm-s-the-matrix .CodeMirror-line > span::selection, .cm-s-the-matrix .CodeMirror-line > span > span::selection { background: rgba(45, 45, 45, 0.99); }\r
+.cm-s-the-matrix .CodeMirror-line::-moz-selection, .cm-s-the-matrix .CodeMirror-line > span::-moz-selection, .cm-s-the-matrix .CodeMirror-line > span > span::-moz-selection { background: rgba(45, 45, 45, 0.99); }\r
+.cm-s-the-matrix .CodeMirror-gutters { background: #060; border-right: 2px solid #00FF00; }\r
+.cm-s-the-matrix .CodeMirror-guttermarker { color: #0f0; }\r
+.cm-s-the-matrix .CodeMirror-guttermarker-subtle { color: white; }\r
+.cm-s-the-matrix .CodeMirror-linenumber { color: #FFFFFF; }\r
+.cm-s-the-matrix .CodeMirror-cursor { border-left: 1px solid #00FF00; }\r
+\r
+.cm-s-the-matrix span.cm-keyword { color: #008803; font-weight: bold; }\r
+.cm-s-the-matrix span.cm-atom { color: #3FF; }\r
+.cm-s-the-matrix span.cm-number { color: #FFB94F; }\r
+.cm-s-the-matrix span.cm-def { color: #99C; }\r
+.cm-s-the-matrix span.cm-variable { color: #F6C; }\r
+.cm-s-the-matrix span.cm-variable-2 { color: #C6F; }\r
+.cm-s-the-matrix span.cm-variable-3, .cm-s-the-matrix span.cm-type { color: #96F; }\r
+.cm-s-the-matrix span.cm-property { color: #62FFA0; }\r
+.cm-s-the-matrix span.cm-operator { color: #999; }\r
+.cm-s-the-matrix span.cm-comment { color: #CCCCCC; }\r
+.cm-s-the-matrix span.cm-string { color: #39C; }\r
+.cm-s-the-matrix span.cm-meta { color: #C9F; }\r
+.cm-s-the-matrix span.cm-qualifier { color: #FFF700; }\r
+.cm-s-the-matrix span.cm-builtin { color: #30a; }\r
+.cm-s-the-matrix span.cm-bracket { color: #cc7; }\r
+.cm-s-the-matrix span.cm-tag { color: #FFBD40; }\r
+.cm-s-the-matrix span.cm-attribute { color: #FFF700; }\r
+.cm-s-the-matrix span.cm-error { color: #FF0000; }\r
+\r
+.cm-s-the-matrix .CodeMirror-activeline-background { background: #040; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/tomorrow-night-bright.css b/js/ckeditor/plugins/codemirror/theme/tomorrow-night-bright.css
new file mode 100644 (file)
index 0000000..5d567c7
--- /dev/null
@@ -0,0 +1,35 @@
+/*\r
+\r
+    Name:       Tomorrow Night - Bright\r
+    Author:     Chris Kempson\r
+\r
+    Port done by Gerard Braad <me@gbraad.nl>\r
+\r
+*/\r
+\r
+.cm-s-tomorrow-night-bright.CodeMirror { background: #000000; color: #eaeaea; }\r
+.cm-s-tomorrow-night-bright div.CodeMirror-selected { background: #424242; }\r
+.cm-s-tomorrow-night-bright .CodeMirror-gutters { background: #000000; border-right: 0px; }\r
+.cm-s-tomorrow-night-bright .CodeMirror-guttermarker { color: #e78c45; }\r
+.cm-s-tomorrow-night-bright .CodeMirror-guttermarker-subtle { color: #777; }\r
+.cm-s-tomorrow-night-bright .CodeMirror-linenumber { color: #424242; }\r
+.cm-s-tomorrow-night-bright .CodeMirror-cursor { border-left: 1px solid #6A6A6A; }\r
+\r
+.cm-s-tomorrow-night-bright span.cm-comment { color: #d27b53; }\r
+.cm-s-tomorrow-night-bright span.cm-atom { color: #a16a94; }\r
+.cm-s-tomorrow-night-bright span.cm-number { color: #a16a94; }\r
+\r
+.cm-s-tomorrow-night-bright span.cm-property, .cm-s-tomorrow-night-bright span.cm-attribute { color: #99cc99; }\r
+.cm-s-tomorrow-night-bright span.cm-keyword { color: #d54e53; }\r
+.cm-s-tomorrow-night-bright span.cm-string { color: #e7c547; }\r
+\r
+.cm-s-tomorrow-night-bright span.cm-variable { color: #b9ca4a; }\r
+.cm-s-tomorrow-night-bright span.cm-variable-2 { color: #7aa6da; }\r
+.cm-s-tomorrow-night-bright span.cm-def { color: #e78c45; }\r
+.cm-s-tomorrow-night-bright span.cm-bracket { color: #eaeaea; }\r
+.cm-s-tomorrow-night-bright span.cm-tag { color: #d54e53; }\r
+.cm-s-tomorrow-night-bright span.cm-link { color: #a16a94; }\r
+.cm-s-tomorrow-night-bright span.cm-error { background: #d54e53; color: #6A6A6A; }\r
+\r
+.cm-s-tomorrow-night-bright .CodeMirror-activeline-background { background: #2a2a2a; }\r
+.cm-s-tomorrow-night-bright .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/tomorrow-night-eighties.css b/js/ckeditor/plugins/codemirror/theme/tomorrow-night-eighties.css
new file mode 100644 (file)
index 0000000..0eef9fe
--- /dev/null
@@ -0,0 +1,38 @@
+/*\r
+\r
+    Name:       Tomorrow Night - Eighties\r
+    Author:     Chris Kempson\r
+\r
+    CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)\r
+    Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)\r
+\r
+*/\r
+\r
+.cm-s-tomorrow-night-eighties.CodeMirror { background: #000000; color: #CCCCCC; }\r
+.cm-s-tomorrow-night-eighties div.CodeMirror-selected { background: #2D2D2D; }\r
+.cm-s-tomorrow-night-eighties .CodeMirror-line::selection, .cm-s-tomorrow-night-eighties .CodeMirror-line > span::selection, .cm-s-tomorrow-night-eighties .CodeMirror-line > span > span::selection { background: rgba(45, 45, 45, 0.99); }\r
+.cm-s-tomorrow-night-eighties .CodeMirror-line::-moz-selection, .cm-s-tomorrow-night-eighties .CodeMirror-line > span::-moz-selection, .cm-s-tomorrow-night-eighties .CodeMirror-line > span > span::-moz-selection { background: rgba(45, 45, 45, 0.99); }\r
+.cm-s-tomorrow-night-eighties .CodeMirror-gutters { background: #000000; border-right: 0px; }\r
+.cm-s-tomorrow-night-eighties .CodeMirror-guttermarker { color: #f2777a; }\r
+.cm-s-tomorrow-night-eighties .CodeMirror-guttermarker-subtle { color: #777; }\r
+.cm-s-tomorrow-night-eighties .CodeMirror-linenumber { color: #515151; }\r
+.cm-s-tomorrow-night-eighties .CodeMirror-cursor { border-left: 1px solid #6A6A6A; }\r
+\r
+.cm-s-tomorrow-night-eighties span.cm-comment { color: #d27b53; }\r
+.cm-s-tomorrow-night-eighties span.cm-atom { color: #a16a94; }\r
+.cm-s-tomorrow-night-eighties span.cm-number { color: #a16a94; }\r
+\r
+.cm-s-tomorrow-night-eighties span.cm-property, .cm-s-tomorrow-night-eighties span.cm-attribute { color: #99cc99; }\r
+.cm-s-tomorrow-night-eighties span.cm-keyword { color: #f2777a; }\r
+.cm-s-tomorrow-night-eighties span.cm-string { color: #ffcc66; }\r
+\r
+.cm-s-tomorrow-night-eighties span.cm-variable { color: #99cc99; }\r
+.cm-s-tomorrow-night-eighties span.cm-variable-2 { color: #6699cc; }\r
+.cm-s-tomorrow-night-eighties span.cm-def { color: #f99157; }\r
+.cm-s-tomorrow-night-eighties span.cm-bracket { color: #CCCCCC; }\r
+.cm-s-tomorrow-night-eighties span.cm-tag { color: #f2777a; }\r
+.cm-s-tomorrow-night-eighties span.cm-link { color: #a16a94; }\r
+.cm-s-tomorrow-night-eighties span.cm-error { background: #f2777a; color: #6A6A6A; }\r
+\r
+.cm-s-tomorrow-night-eighties .CodeMirror-activeline-background { background: #343600; }\r
+.cm-s-tomorrow-night-eighties .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/ttcn.css b/js/ckeditor/plugins/codemirror/theme/ttcn.css
new file mode 100644 (file)
index 0000000..69c9a09
--- /dev/null
@@ -0,0 +1,64 @@
+.cm-s-ttcn .cm-quote { color: #090; }\r
+.cm-s-ttcn .cm-negative { color: #d44; }\r
+.cm-s-ttcn .cm-positive { color: #292; }\r
+.cm-s-ttcn .cm-header, .cm-strong { font-weight: bold; }\r
+.cm-s-ttcn .cm-em { font-style: italic; }\r
+.cm-s-ttcn .cm-link { text-decoration: underline; }\r
+.cm-s-ttcn .cm-strikethrough { text-decoration: line-through; }\r
+.cm-s-ttcn .cm-header { color: #00f; font-weight: bold; }\r
+\r
+.cm-s-ttcn .cm-atom { color: #219; }\r
+.cm-s-ttcn .cm-attribute { color: #00c; }\r
+.cm-s-ttcn .cm-bracket { color: #997; }\r
+.cm-s-ttcn .cm-comment { color: #333333; }\r
+.cm-s-ttcn .cm-def { color: #00f; }\r
+.cm-s-ttcn .cm-em { font-style: italic; }\r
+.cm-s-ttcn .cm-error { color: #f00; }\r
+.cm-s-ttcn .cm-hr { color: #999; }\r
+.cm-s-ttcn .cm-invalidchar { color: #f00; }\r
+.cm-s-ttcn .cm-keyword { font-weight:bold; }\r
+.cm-s-ttcn .cm-link { color: #00c; text-decoration: underline; }\r
+.cm-s-ttcn .cm-meta { color: #555; }\r
+.cm-s-ttcn .cm-negative { color: #d44; }\r
+.cm-s-ttcn .cm-positive { color: #292; }\r
+.cm-s-ttcn .cm-qualifier { color: #555; }\r
+.cm-s-ttcn .cm-strikethrough { text-decoration: line-through; }\r
+.cm-s-ttcn .cm-string { color: #006400; }\r
+.cm-s-ttcn .cm-string-2 { color: #f50; }\r
+.cm-s-ttcn .cm-strong { font-weight: bold; }\r
+.cm-s-ttcn .cm-tag { color: #170; }\r
+.cm-s-ttcn .cm-variable { color: #8B2252; }\r
+.cm-s-ttcn .cm-variable-2 { color: #05a; }\r
+.cm-s-ttcn .cm-variable-3, .cm-s-ttcn .cm-type { color: #085; }\r
+\r
+.cm-s-ttcn .cm-invalidchar { color: #f00; }\r
+\r
+/* ASN */\r
+.cm-s-ttcn .cm-accessTypes,\r
+.cm-s-ttcn .cm-compareTypes { color: #27408B; }\r
+.cm-s-ttcn .cm-cmipVerbs { color: #8B2252; }\r
+.cm-s-ttcn .cm-modifier { color:#D2691E; }\r
+.cm-s-ttcn .cm-status { color:#8B4545; }\r
+.cm-s-ttcn .cm-storage { color:#A020F0; }\r
+.cm-s-ttcn .cm-tags { color:#006400; }\r
+\r
+/* CFG */\r
+.cm-s-ttcn .cm-externalCommands { color: #8B4545; font-weight:bold; }\r
+.cm-s-ttcn .cm-fileNCtrlMaskOptions,\r
+.cm-s-ttcn .cm-sectionTitle { color: #2E8B57; font-weight:bold; }\r
+\r
+/* TTCN */\r
+.cm-s-ttcn .cm-booleanConsts,\r
+.cm-s-ttcn .cm-otherConsts,\r
+.cm-s-ttcn .cm-verdictConsts { color: #006400; }\r
+.cm-s-ttcn .cm-configOps,\r
+.cm-s-ttcn .cm-functionOps,\r
+.cm-s-ttcn .cm-portOps,\r
+.cm-s-ttcn .cm-sutOps,\r
+.cm-s-ttcn .cm-timerOps,\r
+.cm-s-ttcn .cm-verdictOps { color: #0000FF; }\r
+.cm-s-ttcn .cm-preprocessor,\r
+.cm-s-ttcn .cm-templateMatch,\r
+.cm-s-ttcn .cm-ttcn3Macros { color: #27408B; }\r
+.cm-s-ttcn .cm-types { color: #A52A2A; font-weight:bold; }\r
+.cm-s-ttcn .cm-visibilityModifiers { font-weight:bold; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/twilight.css b/js/ckeditor/plugins/codemirror/theme/twilight.css
new file mode 100644 (file)
index 0000000..8e6cb74
--- /dev/null
@@ -0,0 +1,32 @@
+.cm-s-twilight.CodeMirror { background: #141414; color: #f7f7f7; } /**/\r
+.cm-s-twilight div.CodeMirror-selected { background: #323232; } /**/\r
+.cm-s-twilight .CodeMirror-line::selection, .cm-s-twilight .CodeMirror-line > span::selection, .cm-s-twilight .CodeMirror-line > span > span::selection { background: rgba(50, 50, 50, 0.99); }\r
+.cm-s-twilight .CodeMirror-line::-moz-selection, .cm-s-twilight .CodeMirror-line > span::-moz-selection, .cm-s-twilight .CodeMirror-line > span > span::-moz-selection { background: rgba(50, 50, 50, 0.99); }\r
+\r
+.cm-s-twilight .CodeMirror-gutters { background: #222; border-right: 1px solid #aaa; }\r
+.cm-s-twilight .CodeMirror-guttermarker { color: white; }\r
+.cm-s-twilight .CodeMirror-guttermarker-subtle { color: #aaa; }\r
+.cm-s-twilight .CodeMirror-linenumber { color: #aaa; }\r
+.cm-s-twilight .CodeMirror-cursor { border-left: 1px solid white; }\r
+\r
+.cm-s-twilight .cm-keyword { color: #f9ee98; } /**/\r
+.cm-s-twilight .cm-atom { color: #FC0; }\r
+.cm-s-twilight .cm-number { color:  #ca7841; } /**/\r
+.cm-s-twilight .cm-def { color: #8DA6CE; }\r
+.cm-s-twilight span.cm-variable-2, .cm-s-twilight span.cm-tag { color: #607392; } /**/\r
+.cm-s-twilight span.cm-variable-3, .cm-s-twilight span.cm-def, .cm-s-twilight span.cm-type { color: #607392; } /**/\r
+.cm-s-twilight .cm-operator { color: #cda869; } /**/\r
+.cm-s-twilight .cm-comment { color:#777; font-style:italic; font-weight:normal; } /**/\r
+.cm-s-twilight .cm-string { color:#8f9d6a; font-style:italic; } /**/\r
+.cm-s-twilight .cm-string-2 { color:#bd6b18; } /*?*/\r
+.cm-s-twilight .cm-meta { background-color:#141414; color:#f7f7f7; } /*?*/\r
+.cm-s-twilight .cm-builtin { color: #cda869; } /*?*/\r
+.cm-s-twilight .cm-tag { color: #997643; } /**/\r
+.cm-s-twilight .cm-attribute { color: #d6bb6d; } /*?*/\r
+.cm-s-twilight .cm-header { color: #FF6400; }\r
+.cm-s-twilight .cm-hr { color: #AEAEAE; }\r
+.cm-s-twilight .cm-link { color:#ad9361; font-style:italic; text-decoration:none; } /**/\r
+.cm-s-twilight .cm-error { border-bottom: 1px solid red; }\r
+\r
+.cm-s-twilight .CodeMirror-activeline-background { background: #27282E; }\r
+.cm-s-twilight .CodeMirror-matchingbracket { outline:1px solid grey; color:white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/vibrant-ink.css b/js/ckeditor/plugins/codemirror/theme/vibrant-ink.css
new file mode 100644 (file)
index 0000000..e900195
--- /dev/null
@@ -0,0 +1,34 @@
+/* Taken from the popular Visual Studio Vibrant Ink Schema */\r
+\r
+.cm-s-vibrant-ink.CodeMirror { background: black; color: white; }\r
+.cm-s-vibrant-ink div.CodeMirror-selected { background: #35493c; }\r
+.cm-s-vibrant-ink .CodeMirror-line::selection, .cm-s-vibrant-ink .CodeMirror-line > span::selection, .cm-s-vibrant-ink .CodeMirror-line > span > span::selection { background: rgba(53, 73, 60, 0.99); }\r
+.cm-s-vibrant-ink .CodeMirror-line::-moz-selection, .cm-s-vibrant-ink .CodeMirror-line > span::-moz-selection, .cm-s-vibrant-ink .CodeMirror-line > span > span::-moz-selection { background: rgba(53, 73, 60, 0.99); }\r
+\r
+.cm-s-vibrant-ink .CodeMirror-gutters { background: #002240; border-right: 1px solid #aaa; }\r
+.cm-s-vibrant-ink .CodeMirror-guttermarker { color: white; }\r
+.cm-s-vibrant-ink .CodeMirror-guttermarker-subtle { color: #d0d0d0; }\r
+.cm-s-vibrant-ink .CodeMirror-linenumber { color: #d0d0d0; }\r
+.cm-s-vibrant-ink .CodeMirror-cursor { border-left: 1px solid white; }\r
+\r
+.cm-s-vibrant-ink .cm-keyword { color: #CC7832; }\r
+.cm-s-vibrant-ink .cm-atom { color: #FC0; }\r
+.cm-s-vibrant-ink .cm-number { color:  #FFEE98; }\r
+.cm-s-vibrant-ink .cm-def { color: #8DA6CE; }\r
+.cm-s-vibrant-ink span.cm-variable-2, .cm-s-vibrant span.cm-tag { color: #FFC66D; }\r
+.cm-s-vibrant-ink span.cm-variable-3, .cm-s-vibrant span.cm-def, .cm-s-vibrant span.cm-type { color: #FFC66D; }\r
+.cm-s-vibrant-ink .cm-operator { color: #888; }\r
+.cm-s-vibrant-ink .cm-comment { color: gray; font-weight: bold; }\r
+.cm-s-vibrant-ink .cm-string { color:  #A5C25C; }\r
+.cm-s-vibrant-ink .cm-string-2 { color: red; }\r
+.cm-s-vibrant-ink .cm-meta { color: #D8FA3C; }\r
+.cm-s-vibrant-ink .cm-builtin { color: #8DA6CE; }\r
+.cm-s-vibrant-ink .cm-tag { color: #8DA6CE; }\r
+.cm-s-vibrant-ink .cm-attribute { color: #8DA6CE; }\r
+.cm-s-vibrant-ink .cm-header { color: #FF6400; }\r
+.cm-s-vibrant-ink .cm-hr { color: #AEAEAE; }\r
+.cm-s-vibrant-ink .cm-link { color: blue; }\r
+.cm-s-vibrant-ink .cm-error { border-bottom: 1px solid red; }\r
+\r
+.cm-s-vibrant-ink .CodeMirror-activeline-background { background: #27282E; }\r
+.cm-s-vibrant-ink .CodeMirror-matchingbracket { outline:1px solid grey; color:white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/xq-dark.css b/js/ckeditor/plugins/codemirror/theme/xq-dark.css
new file mode 100644 (file)
index 0000000..039b5a0
--- /dev/null
@@ -0,0 +1,53 @@
+/*\r
+Copyright (C) 2011 by MarkLogic Corporation\r
+Author: Mike Brevoort <mike@brevoort.com>\r
+\r
+Permission is hereby granted, free of charge, to any person obtaining a copy\r
+of this software and associated documentation files (the "Software"), to deal\r
+in the Software without restriction, including without limitation the rights\r
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r
+copies of the Software, and to permit persons to whom the Software is\r
+furnished to do so, subject to the following conditions:\r
+\r
+The above copyright notice and this permission notice shall be included in\r
+all copies or substantial portions of the Software.\r
+\r
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\r
+THE SOFTWARE.\r
+*/\r
+.cm-s-xq-dark.CodeMirror { background: #0a001f; color: #f8f8f8; }\r
+.cm-s-xq-dark div.CodeMirror-selected { background: #27007A; }\r
+.cm-s-xq-dark .CodeMirror-line::selection, .cm-s-xq-dark .CodeMirror-line > span::selection, .cm-s-xq-dark .CodeMirror-line > span > span::selection { background: rgba(39, 0, 122, 0.99); }\r
+.cm-s-xq-dark .CodeMirror-line::-moz-selection, .cm-s-xq-dark .CodeMirror-line > span::-moz-selection, .cm-s-xq-dark .CodeMirror-line > span > span::-moz-selection { background: rgba(39, 0, 122, 0.99); }\r
+.cm-s-xq-dark .CodeMirror-gutters { background: #0a001f; border-right: 1px solid #aaa; }\r
+.cm-s-xq-dark .CodeMirror-guttermarker { color: #FFBD40; }\r
+.cm-s-xq-dark .CodeMirror-guttermarker-subtle { color: #f8f8f8; }\r
+.cm-s-xq-dark .CodeMirror-linenumber { color: #f8f8f8; }\r
+.cm-s-xq-dark .CodeMirror-cursor { border-left: 1px solid white; }\r
+\r
+.cm-s-xq-dark span.cm-keyword { color: #FFBD40; }\r
+.cm-s-xq-dark span.cm-atom { color: #6C8CD5; }\r
+.cm-s-xq-dark span.cm-number { color: #164; }\r
+.cm-s-xq-dark span.cm-def { color: #FFF; text-decoration:underline; }\r
+.cm-s-xq-dark span.cm-variable { color: #FFF; }\r
+.cm-s-xq-dark span.cm-variable-2 { color: #EEE; }\r
+.cm-s-xq-dark span.cm-variable-3, .cm-s-xq-dark span.cm-type { color: #DDD; }\r
+.cm-s-xq-dark span.cm-property {}\r
+.cm-s-xq-dark span.cm-operator {}\r
+.cm-s-xq-dark span.cm-comment { color: gray; }\r
+.cm-s-xq-dark span.cm-string { color: #9FEE00; }\r
+.cm-s-xq-dark span.cm-meta { color: yellow; }\r
+.cm-s-xq-dark span.cm-qualifier { color: #FFF700; }\r
+.cm-s-xq-dark span.cm-builtin { color: #30a; }\r
+.cm-s-xq-dark span.cm-bracket { color: #cc7; }\r
+.cm-s-xq-dark span.cm-tag { color: #FFBD40; }\r
+.cm-s-xq-dark span.cm-attribute { color: #FFF700; }\r
+.cm-s-xq-dark span.cm-error { color: #f00; }\r
+\r
+.cm-s-xq-dark .CodeMirror-activeline-background { background: #27282E; }\r
+.cm-s-xq-dark .CodeMirror-matchingbracket { outline:1px solid grey; color:white !important; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/xq-light.css b/js/ckeditor/plugins/codemirror/theme/xq-light.css
new file mode 100644 (file)
index 0000000..1c6c63b
--- /dev/null
@@ -0,0 +1,43 @@
+/*\r
+Copyright (C) 2011 by MarkLogic Corporation\r
+Author: Mike Brevoort <mike@brevoort.com>\r
+\r
+Permission is hereby granted, free of charge, to any person obtaining a copy\r
+of this software and associated documentation files (the "Software"), to deal\r
+in the Software without restriction, including without limitation the rights\r
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r
+copies of the Software, and to permit persons to whom the Software is\r
+furnished to do so, subject to the following conditions:\r
+\r
+The above copyright notice and this permission notice shall be included in\r
+all copies or substantial portions of the Software.\r
+\r
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\r
+THE SOFTWARE.\r
+*/\r
+.cm-s-xq-light span.cm-keyword { line-height: 1em; font-weight: bold; color: #5A5CAD; }\r
+.cm-s-xq-light span.cm-atom { color: #6C8CD5; }\r
+.cm-s-xq-light span.cm-number { color: #164; }\r
+.cm-s-xq-light span.cm-def { text-decoration:underline; }\r
+.cm-s-xq-light span.cm-variable { color: black; }\r
+.cm-s-xq-light span.cm-variable-2 { color:black; }\r
+.cm-s-xq-light span.cm-variable-3, .cm-s-xq-light span.cm-type { color: black; }\r
+.cm-s-xq-light span.cm-property {}\r
+.cm-s-xq-light span.cm-operator {}\r
+.cm-s-xq-light span.cm-comment { color: #0080FF; font-style: italic; }\r
+.cm-s-xq-light span.cm-string { color: red; }\r
+.cm-s-xq-light span.cm-meta { color: yellow; }\r
+.cm-s-xq-light span.cm-qualifier { color: grey; }\r
+.cm-s-xq-light span.cm-builtin { color: #7EA656; }\r
+.cm-s-xq-light span.cm-bracket { color: #cc7; }\r
+.cm-s-xq-light span.cm-tag { color: #3F7F7F; }\r
+.cm-s-xq-light span.cm-attribute { color: #7F007F; }\r
+.cm-s-xq-light span.cm-error { color: #f00; }\r
+\r
+.cm-s-xq-light .CodeMirror-activeline-background { background: #e8f2ff; }\r
+.cm-s-xq-light .CodeMirror-matchingbracket { outline:1px solid grey;color:black !important;background:yellow; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/yeti.css b/js/ckeditor/plugins/codemirror/theme/yeti.css
new file mode 100644 (file)
index 0000000..1edc1dd
--- /dev/null
@@ -0,0 +1,44 @@
+/*\r
+\r
+    Name:       yeti\r
+    Author:     Michael Kaminsky (http://github.com/mkaminsky11)\r
+\r
+    Original yeti color scheme by Jesse Weed (https://github.com/jesseweed/yeti-syntax)\r
+\r
+*/\r
+\r
+\r
+.cm-s-yeti.CodeMirror {\r
+  background-color: #ECEAE8 !important;\r
+  color: #d1c9c0 !important;\r
+  border: none;\r
+}\r
+\r
+.cm-s-yeti .CodeMirror-gutters {\r
+  color: #adaba6;\r
+  background-color: #E5E1DB;\r
+  border: none;\r
+}\r
+.cm-s-yeti .CodeMirror-cursor { border-left: solid thin #d1c9c0; }\r
+.cm-s-yeti .CodeMirror-linenumber { color: #adaba6; }\r
+.cm-s-yeti.CodeMirror-focused div.CodeMirror-selected { background: #DCD8D2; }\r
+.cm-s-yeti .CodeMirror-line::selection, .cm-s-yeti .CodeMirror-line > span::selection, .cm-s-yeti .CodeMirror-line > span > span::selection { background: #DCD8D2; }\r
+.cm-s-yeti .CodeMirror-line::-moz-selection, .cm-s-yeti .CodeMirror-line > span::-moz-selection, .cm-s-yeti .CodeMirror-line > span > span::-moz-selection { background: #DCD8D2; }\r
+.cm-s-yeti span.cm-comment { color: #d4c8be; }\r
+.cm-s-yeti span.cm-string, .cm-s-yeti span.cm-string-2 { color: #96c0d8; }\r
+.cm-s-yeti span.cm-number { color: #a074c4; }\r
+.cm-s-yeti span.cm-variable { color: #55b5db; }\r
+.cm-s-yeti span.cm-variable-2 { color: #a074c4; }\r
+.cm-s-yeti span.cm-def { color: #55b5db; }\r
+.cm-s-yeti span.cm-operator { color: #9fb96e; }\r
+.cm-s-yeti span.cm-keyword { color: #9fb96e; }\r
+.cm-s-yeti span.cm-atom { color: #a074c4; }\r
+.cm-s-yeti span.cm-meta { color: #96c0d8; }\r
+.cm-s-yeti span.cm-tag { color: #96c0d8; }\r
+.cm-s-yeti span.cm-attribute { color: #9fb96e; }\r
+.cm-s-yeti span.cm-qualifier { color: #96c0d8; }\r
+.cm-s-yeti span.cm-property { color: #a074c4; }\r
+.cm-s-yeti span.cm-builtin { color: #a074c4; }\r
+.cm-s-yeti span.cm-variable-3, .cm-s-yeti span.cm-type { color: #96c0d8; }\r
+.cm-s-yeti .CodeMirror-activeline-background { background: #E7E4E0; }\r
+.cm-s-yeti .CodeMirror-matchingbracket { text-decoration: underline; }\r
diff --git a/js/ckeditor/plugins/codemirror/theme/zenburn.css b/js/ckeditor/plugins/codemirror/theme/zenburn.css
new file mode 100644 (file)
index 0000000..69cecea
--- /dev/null
@@ -0,0 +1,37 @@
+/**\r
+ * "\r
+ *  Using Zenburn color palette from the Emacs Zenburn Theme\r
+ *  https://github.com/bbatsov/zenburn-emacs/blob/master/zenburn-theme.el\r
+ *\r
+ *  Also using parts of https://github.com/xavi/coderay-lighttable-theme\r
+ * "\r
+ * From: https://github.com/wisenomad/zenburn-lighttable-theme/blob/master/zenburn.css\r
+ */\r
+\r
+.cm-s-zenburn .CodeMirror-gutters { background: #3f3f3f !important; }\r
+.cm-s-zenburn .CodeMirror-foldgutter-open, .CodeMirror-foldgutter-folded { color: #999; }\r
+.cm-s-zenburn .CodeMirror-cursor { border-left: 1px solid white; }\r
+.cm-s-zenburn { background-color: #3f3f3f; color: #dcdccc; }\r
+.cm-s-zenburn span.cm-builtin { color: #dcdccc; font-weight: bold; }\r
+.cm-s-zenburn span.cm-comment { color: #7f9f7f; }\r
+.cm-s-zenburn span.cm-keyword { color: #f0dfaf; font-weight: bold; }\r
+.cm-s-zenburn span.cm-atom { color: #bfebbf; }\r
+.cm-s-zenburn span.cm-def { color: #dcdccc; }\r
+.cm-s-zenburn span.cm-variable { color: #dfaf8f; }\r
+.cm-s-zenburn span.cm-variable-2 { color: #dcdccc; }\r
+.cm-s-zenburn span.cm-string { color: #cc9393; }\r
+.cm-s-zenburn span.cm-string-2 { color: #cc9393; }\r
+.cm-s-zenburn span.cm-number { color: #dcdccc; }\r
+.cm-s-zenburn span.cm-tag { color: #93e0e3; }\r
+.cm-s-zenburn span.cm-property { color: #dfaf8f; }\r
+.cm-s-zenburn span.cm-attribute { color: #dfaf8f; }\r
+.cm-s-zenburn span.cm-qualifier { color: #7cb8bb; }\r
+.cm-s-zenburn span.cm-meta { color: #f0dfaf; }\r
+.cm-s-zenburn span.cm-header { color: #f0efd0; }\r
+.cm-s-zenburn span.cm-operator { color: #f0efd0; }\r
+.cm-s-zenburn span.CodeMirror-matchingbracket { box-sizing: border-box; background: transparent; border-bottom: 1px solid; }\r
+.cm-s-zenburn span.CodeMirror-nonmatchingbracket { border-bottom: 1px solid; background: none; }\r
+.cm-s-zenburn .CodeMirror-activeline { background: #000000; }\r
+.cm-s-zenburn .CodeMirror-activeline-background { background: #000000; }\r
+.cm-s-zenburn div.CodeMirror-selected { background: #545454; }\r
+.cm-s-zenburn .CodeMirror-focused div.CodeMirror-selected { background: #4f4f4f; }\r
index 2186b29..5c35976 100644 (file)
@@ -1,4 +1,4 @@
 /*
- Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
+ Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  For licensing, see LICENSE.md or http://ckeditor.com/license
 */
diff --git a/js/ckeditor/plugins/fakeobjects/images/spacer.gif b/js/ckeditor/plugins/fakeobjects/images/spacer.gif
deleted file mode 100644 (file)
index 5bfd67a..0000000
Binary files a/js/ckeditor/plugins/fakeobjects/images/spacer.gif and /dev/null differ
index 59a7ad3..ad22906 100644 (file)
Binary files a/js/ckeditor/plugins/icons.png and b/js/ckeditor/plugins/icons.png differ
index 14a9b43..7c76e52 100644 (file)
Binary files a/js/ckeditor/plugins/icons_hidpi.png and b/js/ckeditor/plugins/icons_hidpi.png differ
diff --git a/js/ckeditor/plugins/inline_resize/plugin.js b/js/ckeditor/plugins/inline_resize/plugin.js
new file mode 100644 (file)
index 0000000..eefac2e
--- /dev/null
@@ -0,0 +1,165 @@
+( function() {
+  var win      = CKEDITOR.document.getWindow(),
+      pixelate = CKEDITOR.tools.cssLength;
+
+  CKEDITOR.plugins.add('inline_resize', {
+    init: function( editor ) {
+      var config = editor.config;
+
+      editor.on('loaded', function() {
+        attach(editor);
+      }, null, null, 20); // same priority as floatspace so that both get initialized
+    }
+  });
+
+  function parentScroll(e) {
+    var position = e.$.getAttribute("position"),
+        excludeStaticParent = position === "absolute";
+    return e.getParents().filter( function(parent) {
+      var style = window.getComputedStyle(parent.$);
+      if ( excludeStaticParent && style.position === "static" )
+        return false;
+      return (/(auto|scroll)/).test( style['overflow'] + style["overflow-y"] + style["overflow-x"] );
+    });
+  };
+
+  function parentDialog(e) {
+    return e.getParents().filter( function(parent) {
+      return parent.$.classList.contains("ui-dialog-content")
+    })[0];
+  };
+
+  function attach( editor ) {
+    var config = editor.config,
+        parent = parentScroll(editor.element),
+        divelt = editor.element.getNext(function(elt){return elt.getAttribute("class") === "cke_textarea_inline"}),
+        inline = editor.element.$.classList.contains("texteditor-in-dialog");
+
+    var dialog;
+
+    if (inline) {
+      dialog = parentDialog(divelt);
+    }
+
+    var resize = function (width, height) {
+      var editable;
+      if ( !( editable = editor.editable() ) )
+        return;
+
+      editable.setStyle('width',  pixelate(width));
+      editable.setStyle('height', pixelate(height));
+    }
+
+    var layout = function ( evt ) {
+      var editable;
+      if ( !( editable = editor.editable() ) )
+          return;
+
+      // Show up the space on focus gain.
+      if (  evt && evt.name == 'focus' )
+        float_space.show();
+
+      var editorPos  = editable.getDocumentPosition();
+      var editorRect = editable.getClientRect();
+      var floatRect  = float_space.getClientRect();
+      var viewRect   = win.getViewPaneSize();
+
+      float_space.setStyle( 'position', 'absolute' );
+      if (inline) {
+        var dialogPos = dialog.getDocumentPosition();
+        float_space.setStyle( 'top',  pixelate( editorPos.y - dialogPos.y + editorRect.height - floatRect.height + 1 ) );
+
+        //float_space.setStyle( 'left', pixelate( editorPos.x - dialogPos.x + editorRect.width  - floatRect.width ) );
+        // floatRect.width seems to be far to high on first dialog popup
+        float_space.setStyle( 'left', pixelate( editorPos.x - dialogPos.x + editorRect.width  - 11 ) );
+      } else {
+        float_space.setStyle( 'top',    pixelate( editorPos.y + editorRect.height - floatRect.height + 1) );
+        float_space.setStyle( 'right',  pixelate( viewRect.width - editorRect.right ) );
+      }
+    };
+
+    var float_html  = '<div class="cke_editor_inline_resize_button">\u25E2</div>'; // class so that csss can overrise content and style
+    var float_space = inline ? divelt.getParent().append( CKEDITOR.dom.element.createFromHtml( float_html ))
+                             : CKEDITOR.document.getBody().append( CKEDITOR.dom.element.createFromHtml( float_html ));
+
+    var drag_handler = function( evt ) {
+      var width  = startSize.width  + evt.data.$.screenX - origin.x,
+          height = startSize.height + evt.data.$.screenY - origin.y;
+
+      width  = Math.max( config.resize_minWidth  || 200, Math.min( width  || 0, config.resize_maxWidth  || 9000) );
+      height = Math.max( config.resize_minHeight || 75,  Math.min( height || 0, config.resize_maxHeight || 9000) );
+
+      resize( width, height );
+      layout();
+    };
+
+    var drag_end_handler = function() {
+      CKEDITOR.document.removeListener( 'mousemove', drag_handler );
+      CKEDITOR.document.removeListener( 'mouseup',   drag_end_handler );
+
+      if ( editor.document ) {
+        editor.document.removeListener( 'mousemove', drag_handler );
+        editor.document.removeListener( 'mouseup',   drag_end_handler );
+      }
+    }
+
+    var mousedown_fn =  function( evt ) {
+      var editable;
+      if ( !( editable = editor.editable() ) )
+        return;
+
+      var editorRect = editable.getClientRect();
+      startSize      = { width: editorRect.width, height: editorRect.height };
+      origin         = { x: evt.data.$.screenX, y: evt.data.$.screenY };
+
+      if (config.resize_minWidth  > startSize.width)   config.resize_minWidth = startSize.width;
+      if (config.resize_minHeight > startSize.height) config.resize_minHeight = startSize.height;
+
+      CKEDITOR.document.on( 'mousemove', drag_handler );
+      CKEDITOR.document.on( 'mouseup',   drag_end_handler );
+
+      if ( editor.document ) {
+        editor.document.on( 'mousemove', drag_handler );
+        editor.document.on( 'mouseup',   drag_end_handler );
+      }
+
+      evt.data.$.preventDefault();
+    };
+
+    float_space.setStyle( 'overflow', 'hidden' );
+    float_space.setStyle( 'cursor', 'se-resize' )
+    float_space.on('mousedown', mousedown_fn);
+    float_space.unselectable();
+    float_space.hide();
+
+    editor.on( 'focus', function( evt ) {
+      layout( evt );
+    } );
+
+    parent.forEach(function(e){
+      e.on('scroll', function (evt) { layout(evt) });
+    });
+
+    editor.on( 'blur', function() {
+      float_space.hide();
+    } );
+
+    editor.on( 'destroy', function() {
+      float_space.remove();
+    } );
+
+    if ( editor.focusManager.hasFocus )
+      float_space.show();
+
+    editor.focusManager.add( float_space, 1 );
+  }
+
+})();
+
+/*
+  TODO
+   * ltr support
+   * textarea/div mode safe, currently simply assumes that textarea inline is used
+   * positioning of resize handle is not browser zomm safe
+   * positioning of resize handle in dialog with scroll bars is broken
+*/
index 562417a..50677e1 100644 (file)
@@ -1,8 +1,8 @@
 /*
- Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
+ Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  For licensing, see LICENSE.md or http://ckeditor.com/license
 */
-CKEDITOR.dialog.add("anchor",function(c){var d=function(a){this._.selectedElement=a;this.setValueOf("info","txtName",a.data("cke-saved-name")||"")};return{title:c.lang.link.anchor.title,minWidth:300,minHeight:60,onOk:function(){var a=CKEDITOR.tools.trim(this.getValueOf("info","txtName")),a={id:a,name:a,"data-cke-saved-name":a};if(this._.selectedElement)this._.selectedElement.data("cke-realelement")?(a=c.document.createElement("a",{attributes:a}),c.createFakeElement(a,"cke_anchor","anchor").replace(this._.selectedElement)):
-this._.selectedElement.setAttributes(a);else{var b=c.getSelection(),b=b&&b.getRanges()[0];b.collapsed?(CKEDITOR.plugins.link.synAnchorSelector&&(a["class"]="cke_anchor_empty"),CKEDITOR.plugins.link.emptyAnchorFix&&(a.contenteditable="false",a["data-cke-editable"]=1),a=c.document.createElement("a",{attributes:a}),CKEDITOR.plugins.link.fakeAnchor&&(a=c.createFakeElement(a,"cke_anchor","anchor")),b.insertNode(a)):(CKEDITOR.env.ie&&9>CKEDITOR.env.version&&(a["class"]="cke_anchor"),a=new CKEDITOR.style({element:"a",
-attributes:a}),a.type=CKEDITOR.STYLE_INLINE,c.applyStyle(a))}},onHide:function(){delete this._.selectedElement},onShow:function(){var a=c.getSelection(),b=a.getSelectedElement();if(b)CKEDITOR.plugins.link.fakeAnchor?((a=CKEDITOR.plugins.link.tryRestoreFakeAnchor(c,b))&&d.call(this,a),this._.selectedElement=b):b.is("a")&&b.hasAttribute("name")&&d.call(this,b);else if(b=CKEDITOR.plugins.link.getSelectedLink(c))d.call(this,b),a.selectElement(b);this.getContentElement("info","txtName").focus()},contents:[{id:"info",
-label:c.lang.link.anchor.title,accessKey:"I",elements:[{type:"text",id:"txtName",label:c.lang.link.anchor.name,required:!0,validate:function(){return!this.getValue()?(alert(c.lang.link.anchor.errorName),!1):!0}}]}]}});
\ No newline at end of file
+CKEDITOR.dialog.add("anchor",function(c){function e(b,a){return b.createFakeElement(b.document.createElement("a",{attributes:a}),"cke_anchor","anchor")}return{title:c.lang.link.anchor.title,minWidth:300,minHeight:60,onOk:function(){var b=CKEDITOR.tools.trim(this.getValueOf("info","txtName")),a={id:b,name:b,"data-cke-saved-name":b};this._.selectedElement?this._.selectedElement.data("cke-realelement")?(b=e(c,a),b.replace(this._.selectedElement),CKEDITOR.env.ie&&c.getSelection().selectElement(b)):this._.selectedElement.setAttributes(a):
+(b=(b=c.getSelection())&&b.getRanges()[0],b.collapsed?(a=e(c,a),b.insertNode(a)):(CKEDITOR.env.ie&&9>CKEDITOR.env.version&&(a["class"]="cke_anchor"),a=new CKEDITOR.style({element:"a",attributes:a}),a.type=CKEDITOR.STYLE_INLINE,a.applyToRange(b)))},onHide:function(){delete this._.selectedElement},onShow:function(){var b=c.getSelection(),a;a=b.getRanges()[0];var d=b.getSelectedElement();a.shrink(CKEDITOR.SHRINK_ELEMENT);a=(d=a.getEnclosedNode())&&d.type===CKEDITOR.NODE_ELEMENT&&("anchor"===d.data("cke-real-element-type")||
+d.is("a"))?d:void 0;var f=(d=a&&a.data("cke-realelement"))?CKEDITOR.plugins.link.tryRestoreFakeAnchor(c,a):CKEDITOR.plugins.link.getSelectedLink(c);if(f){this._.selectedElement=f;var e=f.data("cke-saved-name");this.setValueOf("info","txtName",e||"");!d&&b.selectElement(f);a&&(this._.selectedElement=a)}this.getContentElement("info","txtName").focus()},contents:[{id:"info",label:c.lang.link.anchor.title,accessKey:"I",elements:[{type:"text",id:"txtName",label:c.lang.link.anchor.name,required:!0,validate:function(){return this.getValue()?
+!0:(alert(c.lang.link.anchor.errorName),!1)}}]}]}});
\ No newline at end of file
index 37085d3..e8581a7 100644 (file)
@@ -1,37 +1,28 @@
 /*
- Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
+ Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  For licensing, see LICENSE.md or http://ckeditor.com/license
 */
-CKEDITOR.dialog.add("link",function(n){var p,q;function r(a){return a.replace(/'/g,"\\$&")}function t(a){var g,c=p,d,e;g=[q,"("];for(var b=0;b<c.length;b++)d=c[b].toLowerCase(),e=a[d],0<b&&g.push(","),g.push("'",e?r(encodeURIComponent(a[d])):"","'");g.push(")");return g.join("")}function u(a){for(var g,c=a.length,d=[],e=0;e<c;e++)g=a.charCodeAt(e),d.push(g);return"String.fromCharCode("+d.join(",")+")"}function v(a){return(a=a.getAttribute("class"))?a.replace(/\s*(?:cke_anchor_empty|cke_anchor)(?:\s*$)?/g,
-""):""}var w=CKEDITOR.plugins.link,s=function(){var a=this.getDialog(),g=a.getContentElement("target","popupFeatures"),a=a.getContentElement("target","linkTargetName"),c=this.getValue();if(g&&a)switch(g=g.getElement(),g.hide(),a.setValue(""),c){case "frame":a.setLabel(n.lang.link.targetFrameName);a.getElement().show();break;case "popup":g.show();a.setLabel(n.lang.link.targetPopupName);a.getElement().show();break;default:a.setValue(c),a.getElement().hide()}},x=/^javascript:/,y=/^mailto:([^?]+)(?:\?(.+))?$/,
-z=/subject=([^;?:@&=$,\/]*)/,A=/body=([^;?:@&=$,\/]*)/,B=/^#(.*)$/,C=/^((?:http|https|ftp|news):\/\/)?(.*)$/,D=/^(_(?:self|top|parent|blank))$/,E=/^javascript:void\(location\.href='mailto:'\+String\.fromCharCode\(([^)]+)\)(?:\+'(.*)')?\)$/,F=/^javascript:([^(]+)\(([^)]+)\)$/,G=/\s*window.open\(\s*this\.href\s*,\s*(?:'([^']*)'|null)\s*,\s*'([^']*)'\s*\)\s*;\s*return\s*false;*\s*/,H=/(?:^|,)([^=]+)=(\d+|yes|no)/gi,I=function(a,g){var c=g&&(g.data("cke-saved-href")||g.getAttribute("href"))||"",d,e,b=
-{};c.match(x)&&("encode"==o?c=c.replace(E,function(a,c,b){return"mailto:"+String.fromCharCode.apply(String,c.split(","))+(b&&b.replace(/\\'/g,"'"))}):o&&c.replace(F,function(a,c,d){if(c==q){b.type="email";for(var a=b.email={},c=/(^')|('$)/g,d=d.match(/[^,\s]+/g),e=d.length,g,f,h=0;h<e;h++)g=decodeURIComponent,f=d[h].replace(c,"").replace(/\\'/g,"'"),f=g(f),g=p[h].toLowerCase(),a[g]=f;a.address=[a.name,a.domain].join("@")}}));if(!b.type)if(d=c.match(B))b.type="anchor",b.anchor={},b.anchor.name=b.anchor.id=
-d[1];else if(d=c.match(y)){e=c.match(z);c=c.match(A);b.type="email";var f=b.email={};f.address=d[1];e&&(f.subject=decodeURIComponent(e[1]));c&&(f.body=decodeURIComponent(c[1]))}else c&&(e=c.match(C))?(b.type="url",b.url={},b.url.protocol=e[1],b.url.url=e[2]):b.type="url";if(g){d=g.getAttribute("target");b.target={};b.adv={};if(d)d.match(D)?b.target.type=b.target.name=d:(b.target.type="frame",b.target.name=d);else if(d=(d=g.data("cke-pa-onclick")||g.getAttribute("onclick"))&&d.match(G)){b.target.type=
-"popup";for(b.target.name=d[1];c=H.exec(d[2]);)("yes"==c[2]||"1"==c[2])&&!(c[1]in{height:1,width:1,top:1,left:1})?b.target[c[1]]=!0:isFinite(c[2])&&(b.target[c[1]]=c[2])}d=function(a,c){var d=g.getAttribute(c);null!==d&&(b.adv[a]=d||"")};d("advId","id");d("advLangDir","dir");d("advAccessKey","accessKey");b.adv.advName=g.data("cke-saved-name")||g.getAttribute("name")||"";d("advLangCode","lang");d("advTabIndex","tabindex");d("advTitle","title");d("advContentType","type");CKEDITOR.plugins.link.synAnchorSelector?
-b.adv.advCSSClasses=v(g):d("advCSSClasses","class");d("advCharset","charset");d("advStyles","style");d("advRel","rel")}d=b.anchors=[];var h;if(CKEDITOR.plugins.link.emptyAnchorFix){f=a.document.getElementsByTag("a");c=0;for(e=f.count();c<e;c++)if(h=f.getItem(c),h.data("cke-saved-name")||h.hasAttribute("name"))d.push({name:h.data("cke-saved-name")||h.getAttribute("name"),id:h.getAttribute("id")})}else{f=new CKEDITOR.dom.nodeList(a.document.$.anchors);c=0;for(e=f.count();c<e;c++)h=f.getItem(c),d[c]=
-{name:h.getAttribute("name"),id:h.getAttribute("id")}}if(CKEDITOR.plugins.link.fakeAnchor){f=a.document.getElementsByTag("img");c=0;for(e=f.count();c<e;c++)(h=CKEDITOR.plugins.link.tryRestoreFakeAnchor(a,f.getItem(c)))&&d.push({name:h.getAttribute("name"),id:h.getAttribute("id")})}this._.selectedElement=g;return b},j=function(a){a.target&&this.setValue(a.target[this.id]||"")},k=function(a){a.adv&&this.setValue(a.adv[this.id]||"")},l=function(a){a.target||(a.target={});a.target[this.id]=this.getValue()||
-""},m=function(a){a.adv||(a.adv={});a.adv[this.id]=this.getValue()||""},o=n.config.emailProtection||"";o&&"encode"!=o&&(q=p=void 0,o.replace(/^([^(]+)\(([^)]+)\)$/,function(a,b,c){q=b;p=[];c.replace(/[^,\s]+/g,function(a){p.push(a)})}));var i=n.lang.common,b=n.lang.link;return{title:b.title,minWidth:350,minHeight:230,contents:[{id:"info",label:b.info,title:b.info,elements:[{id:"linkType",type:"select",label:b.type,"default":"url",items:[[b.toUrl,"url"],[b.toAnchor,"anchor"],[b.toEmail,"email"]],onChange:function(){var a=
-this.getDialog(),b=["urlOptions","anchorOptions","emailOptions"],c=this.getValue(),d=a.definition.getContents("upload"),d=d&&d.hidden;if(c=="url"){n.config.linkShowTargetTab&&a.showPage("target");d||a.showPage("upload")}else{a.hidePage("target");d||a.hidePage("upload")}for(d=0;d<b.length;d++){var e=a.getContentElement("info",b[d]);if(e){e=e.getElement().getParent().getParent();b[d]==c+"Options"?e.show():e.hide()}}a.layout()},setup:function(a){a.type&&this.setValue(a.type)},commit:function(a){a.type=
-this.getValue()}},{type:"vbox",id:"urlOptions",children:[{type:"hbox",widths:["25%","75%"],children:[{id:"protocol",type:"select",label:i.protocol,"default":"http://",items:[["http://‎","http://"],["https://‎","https://"],["ftp://‎","ftp://"],["news://‎","news://"],[b.other,""]],setup:function(a){a.url&&this.setValue(a.url.protocol||"")},commit:function(a){if(!a.url)a.url={};a.url.protocol=this.getValue()}},{type:"text",id:"url",label:i.url,required:!0,onLoad:function(){this.allowOnChange=true},onKeyUp:function(){this.allowOnChange=
-false;var a=this.getDialog().getContentElement("info","protocol"),b=this.getValue(),c=/^((javascript:)|[#\/\.\?])/i,d=/^(http|https|ftp|news):\/\/(?=.)/i.exec(b);if(d){this.setValue(b.substr(d[0].length));a.setValue(d[0].toLowerCase())}else c.test(b)&&a.setValue("");this.allowOnChange=true},onChange:function(){if(this.allowOnChange)this.onKeyUp()},validate:function(){var a=this.getDialog();if(a.getContentElement("info","linkType")&&a.getValueOf("info","linkType")!="url")return true;if(/javascript\:/.test(this.getValue())){alert(i.invalidValue);
-return false}return this.getDialog().fakeObj?true:CKEDITOR.dialog.validate.notEmpty(b.noUrl).apply(this)},setup:function(a){this.allowOnChange=false;a.url&&this.setValue(a.url.url);this.allowOnChange=true},commit:function(a){this.onChange();if(!a.url)a.url={};a.url.url=this.getValue();this.allowOnChange=false}}],setup:function(){this.getDialog().getContentElement("info","linkType")||this.getElement().show()}},{type:"button",id:"browse",hidden:"true",filebrowser:"info:url",label:i.browseServer}]},
-{type:"vbox",id:"anchorOptions",width:260,align:"center",padding:0,children:[{type:"fieldset",id:"selectAnchorText",label:b.selectAnchor,setup:function(a){a.anchors.length>0?this.getElement().show():this.getElement().hide()},children:[{type:"hbox",id:"selectAnchor",children:[{type:"select",id:"anchorName","default":"",label:b.anchorName,style:"width: 100%;",items:[[""]],setup:function(a){this.clear();this.add("");for(var b=0;b<a.anchors.length;b++)a.anchors[b].name&&this.add(a.anchors[b].name);a.anchor&&
-this.setValue(a.anchor.name);(a=this.getDialog().getContentElement("info","linkType"))&&a.getValue()=="email"&&this.focus()},commit:function(a){if(!a.anchor)a.anchor={};a.anchor.name=this.getValue()}},{type:"select",id:"anchorId","default":"",label:b.anchorId,style:"width: 100%;",items:[[""]],setup:function(a){this.clear();this.add("");for(var b=0;b<a.anchors.length;b++)a.anchors[b].id&&this.add(a.anchors[b].id);a.anchor&&this.setValue(a.anchor.id)},commit:function(a){if(!a.anchor)a.anchor={};a.anchor.id=
-this.getValue()}}],setup:function(a){a.anchors.length>0?this.getElement().show():this.getElement().hide()}}]},{type:"html",id:"noAnchors",style:"text-align: center;",html:'<div role="note" tabIndex="-1">'+CKEDITOR.tools.htmlEncode(b.noAnchors)+"</div>",focus:!0,setup:function(a){a.anchors.length<1?this.getElement().show():this.getElement().hide()}}],setup:function(){this.getDialog().getContentElement("info","linkType")||this.getElement().hide()}},{type:"vbox",id:"emailOptions",padding:1,children:[{type:"text",
-id:"emailAddress",label:b.emailAddress,required:!0,validate:function(){var a=this.getDialog();return!a.getContentElement("info","linkType")||a.getValueOf("info","linkType")!="email"?true:CKEDITOR.dialog.validate.notEmpty(b.noEmail).apply(this)},setup:function(a){a.email&&this.setValue(a.email.address);(a=this.getDialog().getContentElement("info","linkType"))&&a.getValue()=="email"&&this.select()},commit:function(a){if(!a.email)a.email={};a.email.address=this.getValue()}},{type:"text",id:"emailSubject",
-label:b.emailSubject,setup:function(a){a.email&&this.setValue(a.email.subject)},commit:function(a){if(!a.email)a.email={};a.email.subject=this.getValue()}},{type:"textarea",id:"emailBody",label:b.emailBody,rows:3,"default":"",setup:function(a){a.email&&this.setValue(a.email.body)},commit:function(a){if(!a.email)a.email={};a.email.body=this.getValue()}}],setup:function(){this.getDialog().getContentElement("info","linkType")||this.getElement().hide()}}]},{id:"target",requiredContent:"a[target]",label:b.target,
-title:b.target,elements:[{type:"hbox",widths:["50%","50%"],children:[{type:"select",id:"linkTargetType",label:i.target,"default":"notSet",style:"width : 100%;",items:[[i.notSet,"notSet"],[b.targetFrame,"frame"],[b.targetPopup,"popup"],[i.targetNew,"_blank"],[i.targetTop,"_top"],[i.targetSelf,"_self"],[i.targetParent,"_parent"]],onChange:s,setup:function(a){a.target&&this.setValue(a.target.type||"notSet");s.call(this)},commit:function(a){if(!a.target)a.target={};a.target.type=this.getValue()}},{type:"text",
-id:"linkTargetName",label:b.targetFrameName,"default":"",setup:function(a){a.target&&this.setValue(a.target.name)},commit:function(a){if(!a.target)a.target={};a.target.name=this.getValue().replace(/\W/gi,"")}}]},{type:"vbox",width:"100%",align:"center",padding:2,id:"popupFeatures",children:[{type:"fieldset",label:b.popupFeatures,children:[{type:"hbox",children:[{type:"checkbox",id:"resizable",label:b.popupResizable,setup:j,commit:l},{type:"checkbox",id:"status",label:b.popupStatusBar,setup:j,commit:l}]},
-{type:"hbox",children:[{type:"checkbox",id:"location",label:b.popupLocationBar,setup:j,commit:l},{type:"checkbox",id:"toolbar",label:b.popupToolbar,setup:j,commit:l}]},{type:"hbox",children:[{type:"checkbox",id:"menubar",label:b.popupMenuBar,setup:j,commit:l},{type:"checkbox",id:"fullscreen",label:b.popupFullScreen,setup:j,commit:l}]},{type:"hbox",children:[{type:"checkbox",id:"scrollbars",label:b.popupScrollBars,setup:j,commit:l},{type:"checkbox",id:"dependent",label:b.popupDependent,setup:j,commit:l}]},
-{type:"hbox",children:[{type:"text",widths:["50%","50%"],labelLayout:"horizontal",label:i.width,id:"width",setup:j,commit:l},{type:"text",labelLayout:"horizontal",widths:["50%","50%"],label:b.popupLeft,id:"left",setup:j,commit:l}]},{type:"hbox",children:[{type:"text",labelLayout:"horizontal",widths:["50%","50%"],label:i.height,id:"height",setup:j,commit:l},{type:"text",labelLayout:"horizontal",label:b.popupTop,widths:["50%","50%"],id:"top",setup:j,commit:l}]}]}]}]},{id:"upload",label:b.upload,title:b.upload,
-hidden:!0,filebrowser:"uploadButton",elements:[{type:"file",id:"upload",label:i.upload,style:"height:40px",size:29},{type:"fileButton",id:"uploadButton",label:i.uploadSubmit,filebrowser:"info:url","for":["upload","upload"]}]},{id:"advanced",label:b.advanced,title:b.advanced,elements:[{type:"vbox",padding:1,children:[{type:"hbox",widths:["45%","35%","20%"],children:[{type:"text",id:"advId",requiredContent:"a[id]",label:b.id,setup:k,commit:m},{type:"select",id:"advLangDir",requiredContent:"a[dir]",
-label:b.langDir,"default":"",style:"width:110px",items:[[i.notSet,""],[b.langDirLTR,"ltr"],[b.langDirRTL,"rtl"]],setup:k,commit:m},{type:"text",id:"advAccessKey",requiredContent:"a[accesskey]",width:"80px",label:b.acccessKey,maxLength:1,setup:k,commit:m}]},{type:"hbox",widths:["45%","35%","20%"],children:[{type:"text",label:b.name,id:"advName",requiredContent:"a[name]",setup:k,commit:m},{type:"text",label:b.langCode,id:"advLangCode",requiredContent:"a[lang]",width:"110px","default":"",setup:k,commit:m},
-{type:"text",label:b.tabIndex,id:"advTabIndex",requiredContent:"a[tabindex]",width:"80px",maxLength:5,setup:k,commit:m}]}]},{type:"vbox",padding:1,children:[{type:"hbox",widths:["45%","55%"],children:[{type:"text",label:b.advisoryTitle,requiredContent:"a[title]","default":"",id:"advTitle",setup:k,commit:m},{type:"text",label:b.advisoryContentType,requiredContent:"a[type]","default":"",id:"advContentType",setup:k,commit:m}]},{type:"hbox",widths:["45%","55%"],children:[{type:"text",label:b.cssClasses,
-requiredContent:"a(cke-xyz)","default":"",id:"advCSSClasses",setup:k,commit:m},{type:"text",label:b.charset,requiredContent:"a[charset]","default":"",id:"advCharset",setup:k,commit:m}]},{type:"hbox",widths:["45%","55%"],children:[{type:"text",label:b.rel,requiredContent:"a[rel]","default":"",id:"advRel",setup:k,commit:m},{type:"text",label:b.styles,requiredContent:"a{cke-xyz}","default":"",id:"advStyles",validate:CKEDITOR.dialog.validate.inlineStyle(n.lang.common.invalidInlineStyle),setup:k,commit:m}]}]}]}],
-onShow:function(){var a=this.getParentEditor(),b=a.getSelection(),c=null;(c=w.getSelectedLink(a))&&c.hasAttribute("href")?b.getSelectedElement()||b.selectElement(c):c=null;this.setupContent(I.apply(this,[a,c]))},onOk:function(){var a={},b=[],c={},d=this.getParentEditor();this.commitContent(c);switch(c.type||"url"){case "url":var e=c.url&&c.url.protocol!=void 0?c.url.protocol:"http://",i=c.url&&CKEDITOR.tools.trim(c.url.url)||"";a["data-cke-saved-href"]=i.indexOf("/")===0?i:e+i;break;case "anchor":e=
-c.anchor&&c.anchor.id;a["data-cke-saved-href"]="#"+(c.anchor&&c.anchor.name||e||"");break;case "email":var f=c.email,e=f.address;switch(o){case "":case "encode":var i=encodeURIComponent(f.subject||""),h=encodeURIComponent(f.body||""),f=[];i&&f.push("subject="+i);h&&f.push("body="+h);f=f.length?"?"+f.join("&"):"";if(o=="encode"){e=["javascript:void(location.href='mailto:'+",u(e)];f&&e.push("+'",r(f),"'");e.push(")")}else e=["mailto:",e,f];break;default:e=e.split("@",2);f.name=e[0];f.domain=e[1];e=
-["javascript:",t(f)]}a["data-cke-saved-href"]=e.join("")}if(c.target)if(c.target.type=="popup"){for(var e=["window.open(this.href, '",c.target.name||"","', '"],j=["resizable","status","location","toolbar","menubar","fullscreen","scrollbars","dependent"],i=j.length,f=function(a){c.target[a]&&j.push(a+"="+c.target[a])},h=0;h<i;h++)j[h]=j[h]+(c.target[j[h]]?"=yes":"=no");f("width");f("left");f("height");f("top");e.push(j.join(","),"'); return false;");a["data-cke-pa-onclick"]=e.join("");b.push("target")}else{c.target.type!=
-"notSet"&&c.target.name?a.target=c.target.name:b.push("target");b.push("data-cke-pa-onclick","onclick")}if(c.adv){e=function(d,e){var f=c.adv[d];f?a[e]=f:b.push(e)};e("advId","id");e("advLangDir","dir");e("advAccessKey","accessKey");c.adv.advName?a.name=a["data-cke-saved-name"]=c.adv.advName:b=b.concat(["data-cke-saved-name","name"]);e("advLangCode","lang");e("advTabIndex","tabindex");e("advTitle","title");e("advContentType","type");e("advCSSClasses","class");e("advCharset","charset");e("advStyles",
-"style");e("advRel","rel")}e=d.getSelection();a.href=a["data-cke-saved-href"];if(this._.selectedElement){d=this._.selectedElement;i=d.data("cke-saved-href");f=d.getHtml();d.setAttributes(a);d.removeAttributes(b);c.adv&&(c.adv.advName&&CKEDITOR.plugins.link.synAnchorSelector)&&d.addClass(d.getChildCount()?"cke_anchor":"cke_anchor_empty");if(i==f||c.type=="email"&&f.indexOf("@")!=-1){d.setHtml(c.type=="email"?c.email.address:a["data-cke-saved-href"]);e.selectElement(d)}delete this._.selectedElement}else{e=
-e.getRanges()[0];if(e.collapsed){d=new CKEDITOR.dom.text(c.type=="email"?c.email.address:a["data-cke-saved-href"],d.document);e.insertNode(d);e.selectNodeContents(d)}d=new CKEDITOR.style({element:"a",attributes:a});d.type=CKEDITOR.STYLE_INLINE;d.applyToRange(e);e.select()}},onLoad:function(){n.config.linkShowAdvancedTab||this.hidePage("advanced");n.config.linkShowTargetTab||this.hidePage("target")},onFocus:function(){var a=this.getContentElement("info","linkType");if(a&&a.getValue()=="url"){a=this.getContentElement("info",
-"url");a.select()}}}});
\ No newline at end of file
+(function(){CKEDITOR.dialog.add("link",function(c){function t(a,b){var c=a.createRange();c.setStartBefore(b);c.setEndAfter(b);return c}var n=CKEDITOR.plugins.link,q,r=function(){var a=this.getDialog(),b=a.getContentElement("target","popupFeatures"),a=a.getContentElement("target","linkTargetName"),p=this.getValue();if(b&&a)switch(b=b.getElement(),b.hide(),a.setValue(""),p){case "frame":a.setLabel(c.lang.link.targetFrameName);a.getElement().show();break;case "popup":b.show();a.setLabel(c.lang.link.targetPopupName);
+a.getElement().show();break;default:a.setValue(p),a.getElement().hide()}},l=function(a){a.target&&this.setValue(a.target[this.id]||"")},e=function(a){a.advanced&&this.setValue(a.advanced[this.id]||"")},k=function(a){a.target||(a.target={});a.target[this.id]=this.getValue()||""},m=function(a){a.advanced||(a.advanced={});a.advanced[this.id]=this.getValue()||""},g=c.lang.common,b=c.lang.link,d;return{title:b.title,minWidth:"moono-lisa"==(CKEDITOR.skinName||c.config.skin)?450:350,minHeight:240,contents:[{id:"info",
+label:b.info,title:b.info,elements:[{type:"text",id:"linkDisplayText",label:b.displayText,setup:function(){this.enable();this.setValue(c.getSelection().getSelectedText());q=this.getValue()},commit:function(a){a.linkText=this.isEnabled()?this.getValue():""}},{id:"linkType",type:"select",label:b.type,"default":"url",items:[[b.toUrl,"url"],[b.toAnchor,"anchor"],[b.toEmail,"email"]],onChange:function(){var a=this.getDialog(),b=["urlOptions","anchorOptions","emailOptions"],p=this.getValue(),f=a.definition.getContents("upload"),
+f=f&&f.hidden;"url"==p?(c.config.linkShowTargetTab&&a.showPage("target"),f||a.showPage("upload")):(a.hidePage("target"),f||a.hidePage("upload"));for(f=0;f<b.length;f++){var h=a.getContentElement("info",b[f]);h&&(h=h.getElement().getParent().getParent(),b[f]==p+"Options"?h.show():h.hide())}a.layout()},setup:function(a){this.setValue(a.type||"url")},commit:function(a){a.type=this.getValue()}},{type:"vbox",id:"urlOptions",children:[{type:"hbox",widths:["25%","75%"],children:[{id:"protocol",type:"select",
+label:g.protocol,"default":"http://",items:[["http://‎","http://"],["https://‎","https://"],["ftp://‎","ftp://"],["news://‎","news://"],[b.other,""]],setup:function(a){a.url&&this.setValue(a.url.protocol||"")},commit:function(a){a.url||(a.url={});a.url.protocol=this.getValue()}},{type:"text",id:"url",label:g.url,required:!0,onLoad:function(){this.allowOnChange=!0},onKeyUp:function(){this.allowOnChange=!1;var a=this.getDialog().getContentElement("info","protocol"),b=this.getValue(),c=/^((javascript:)|[#\/\.\?])/i,
+f=/^(http|https|ftp|news):\/\/(?=.)/i.exec(b);f?(this.setValue(b.substr(f[0].length)),a.setValue(f[0].toLowerCase())):c.test(b)&&a.setValue("");this.allowOnChange=!0},onChange:function(){if(this.allowOnChange)this.onKeyUp()},validate:function(){var a=this.getDialog();return a.getContentElement("info","linkType")&&"url"!=a.getValueOf("info","linkType")?!0:!c.config.linkJavaScriptLinksAllowed&&/javascript\:/.test(this.getValue())?(alert(g.invalidValue),!1):this.getDialog().fakeObj?!0:CKEDITOR.dialog.validate.notEmpty(b.noUrl).apply(this)},
+setup:function(a){this.allowOnChange=!1;a.url&&this.setValue(a.url.url);this.allowOnChange=!0},commit:function(a){this.onChange();a.url||(a.url={});a.url.url=this.getValue();this.allowOnChange=!1}}],setup:function(){this.getDialog().getContentElement("info","linkType")||this.getElement().show()}},{type:"button",id:"browse",hidden:"true",filebrowser:"info:url",label:g.browseServer}]},{type:"vbox",id:"anchorOptions",width:260,align:"center",padding:0,children:[{type:"fieldset",id:"selectAnchorText",
+label:b.selectAnchor,setup:function(){d=n.getEditorAnchors(c);this.getElement()[d&&d.length?"show":"hide"]()},children:[{type:"hbox",id:"selectAnchor",children:[{type:"select",id:"anchorName","default":"",label:b.anchorName,style:"width: 100%;",items:[[""]],setup:function(a){this.clear();this.add("");if(d)for(var b=0;b<d.length;b++)d[b].name&&this.add(d[b].name);a.anchor&&this.setValue(a.anchor.name);(a=this.getDialog().getContentElement("info","linkType"))&&"email"==a.getValue()&&this.focus()},commit:function(a){a.anchor||
+(a.anchor={});a.anchor.name=this.getValue()}},{type:"select",id:"anchorId","default":"",label:b.anchorId,style:"width: 100%;",items:[[""]],setup:function(a){this.clear();this.add("");if(d)for(var b=0;b<d.length;b++)d[b].id&&this.add(d[b].id);a.anchor&&this.setValue(a.anchor.id)},commit:function(a){a.anchor||(a.anchor={});a.anchor.id=this.getValue()}}],setup:function(){this.getElement()[d&&d.length?"show":"hide"]()}}]},{type:"html",id:"noAnchors",style:"text-align: center;",html:'\x3cdiv role\x3d"note" tabIndex\x3d"-1"\x3e'+
+CKEDITOR.tools.htmlEncode(b.noAnchors)+"\x3c/div\x3e",focus:!0,setup:function(){this.getElement()[d&&d.length?"hide":"show"]()}}],setup:function(){this.getDialog().getContentElement("info","linkType")||this.getElement().hide()}},{type:"vbox",id:"emailOptions",padding:1,children:[{type:"text",id:"emailAddress",label:b.emailAddress,required:!0,validate:function(){var a=this.getDialog();return a.getContentElement("info","linkType")&&"email"==a.getValueOf("info","linkType")?CKEDITOR.dialog.validate.notEmpty(b.noEmail).apply(this):
+!0},setup:function(a){a.email&&this.setValue(a.email.address);(a=this.getDialog().getContentElement("info","linkType"))&&"email"==a.getValue()&&this.select()},commit:function(a){a.email||(a.email={});a.email.address=this.getValue()}},{type:"text",id:"emailSubject",label:b.emailSubject,setup:function(a){a.email&&this.setValue(a.email.subject)},commit:function(a){a.email||(a.email={});a.email.subject=this.getValue()}},{type:"textarea",id:"emailBody",label:b.emailBody,rows:3,"default":"",setup:function(a){a.email&&
+this.setValue(a.email.body)},commit:function(a){a.email||(a.email={});a.email.body=this.getValue()}}],setup:function(){this.getDialog().getContentElement("info","linkType")||this.getElement().hide()}}]},{id:"target",requiredContent:"a[target]",label:b.target,title:b.target,elements:[{type:"hbox",widths:["50%","50%"],children:[{type:"select",id:"linkTargetType",label:g.target,"default":"notSet",style:"width : 100%;",items:[[g.notSet,"notSet"],[b.targetFrame,"frame"],[b.targetPopup,"popup"],[g.targetNew,
+"_blank"],[g.targetTop,"_top"],[g.targetSelf,"_self"],[g.targetParent,"_parent"]],onChange:r,setup:function(a){a.target&&this.setValue(a.target.type||"notSet");r.call(this)},commit:function(a){a.target||(a.target={});a.target.type=this.getValue()}},{type:"text",id:"linkTargetName",label:b.targetFrameName,"default":"",setup:function(a){a.target&&this.setValue(a.target.name)},commit:function(a){a.target||(a.target={});a.target.name=this.getValue().replace(/([^\x00-\x7F]|\s)/gi,"")}}]},{type:"vbox",
+width:"100%",align:"center",padding:2,id:"popupFeatures",children:[{type:"fieldset",label:b.popupFeatures,children:[{type:"hbox",children:[{type:"checkbox",id:"resizable",label:b.popupResizable,setup:l,commit:k},{type:"checkbox",id:"status",label:b.popupStatusBar,setup:l,commit:k}]},{type:"hbox",children:[{type:"checkbox",id:"location",label:b.popupLocationBar,setup:l,commit:k},{type:"checkbox",id:"toolbar",label:b.popupToolbar,setup:l,commit:k}]},{type:"hbox",children:[{type:"checkbox",id:"menubar",
+label:b.popupMenuBar,setup:l,commit:k},{type:"checkbox",id:"fullscreen",label:b.popupFullScreen,setup:l,commit:k}]},{type:"hbox",children:[{type:"checkbox",id:"scrollbars",label:b.popupScrollBars,setup:l,commit:k},{type:"checkbox",id:"dependent",label:b.popupDependent,setup:l,commit:k}]},{type:"hbox",children:[{type:"text",widths:["50%","50%"],labelLayout:"horizontal",label:g.width,id:"width",setup:l,commit:k},{type:"text",labelLayout:"horizontal",widths:["50%","50%"],label:b.popupLeft,id:"left",
+setup:l,commit:k}]},{type:"hbox",children:[{type:"text",labelLayout:"horizontal",widths:["50%","50%"],label:g.height,id:"height",setup:l,commit:k},{type:"text",labelLayout:"horizontal",label:b.popupTop,widths:["50%","50%"],id:"top",setup:l,commit:k}]}]}]}]},{id:"upload",label:b.upload,title:b.upload,hidden:!0,filebrowser:"uploadButton",elements:[{type:"file",id:"upload",label:g.upload,style:"height:40px",size:29},{type:"fileButton",id:"uploadButton",label:g.uploadSubmit,filebrowser:"info:url","for":["upload",
+"upload"]}]},{id:"advanced",label:b.advanced,title:b.advanced,elements:[{type:"vbox",padding:1,children:[{type:"hbox",widths:["45%","35%","20%"],children:[{type:"text",id:"advId",requiredContent:"a[id]",label:b.id,setup:e,commit:m},{type:"select",id:"advLangDir",requiredContent:"a[dir]",label:b.langDir,"default":"",style:"width:110px",items:[[g.notSet,""],[b.langDirLTR,"ltr"],[b.langDirRTL,"rtl"]],setup:e,commit:m},{type:"text",id:"advAccessKey",requiredContent:"a[accesskey]",width:"80px",label:b.acccessKey,
+maxLength:1,setup:e,commit:m}]},{type:"hbox",widths:["45%","35%","20%"],children:[{type:"text",label:b.name,id:"advName",requiredContent:"a[name]",setup:e,commit:m},{type:"text",label:b.langCode,id:"advLangCode",requiredContent:"a[lang]",width:"110px","default":"",setup:e,commit:m},{type:"text",label:b.tabIndex,id:"advTabIndex",requiredContent:"a[tabindex]",width:"80px",maxLength:5,setup:e,commit:m}]}]},{type:"vbox",padding:1,children:[{type:"hbox",widths:["45%","55%"],children:[{type:"text",label:b.advisoryTitle,
+requiredContent:"a[title]","default":"",id:"advTitle",setup:e,commit:m},{type:"text",label:b.advisoryContentType,requiredContent:"a[type]","default":"",id:"advContentType",setup:e,commit:m}]},{type:"hbox",widths:["45%","55%"],children:[{type:"text",label:b.cssClasses,requiredContent:"a(cke-xyz)","default":"",id:"advCSSClasses",setup:e,commit:m},{type:"text",label:b.charset,requiredContent:"a[charset]","default":"",id:"advCharset",setup:e,commit:m}]},{type:"hbox",widths:["45%","55%"],children:[{type:"text",
+label:b.rel,requiredContent:"a[rel]","default":"",id:"advRel",setup:e,commit:m},{type:"text",label:b.styles,requiredContent:"a{cke-xyz}","default":"",id:"advStyles",validate:CKEDITOR.dialog.validate.inlineStyle(c.lang.common.invalidInlineStyle),setup:e,commit:m}]},{type:"hbox",widths:["45%","55%"],children:[{type:"checkbox",id:"download",requiredContent:"a[download]",label:b.download,setup:function(a){void 0!==a.download&&this.setValue("checked","checked")},commit:function(a){this.getValue()&&(a.download=
+this.getValue())}}]}]}]}],onShow:function(){var a=this.getParentEditor(),b=a.getSelection(),c=this.getContentElement("info","linkDisplayText").getElement().getParent().getParent(),f=n.getSelectedLink(a,!0),h=f[0]||null;h&&h.hasAttribute("href")&&(b.getSelectedElement()||b.isInTable()||b.selectElement(h));b=n.parseLinkAttributes(a,h);1>=f.length&&n.showDisplayTextForElement(h,a)?c.show():c.hide();this._.selectedElements=f;this.setupContent(b)},onOk:function(){var a={};this.commitContent(a);if(this._.selectedElements.length){var b=
+this._.selectedElements,g=n.getLinkAttributes(c,a),f=[],h,d,l,e,k;for(k=0;k<b.length;k++){h=b[k];d=h.data("cke-saved-href");l=h.getHtml();h.setAttributes(g.set);h.removeAttributes(g.removed);if(a.linkText&&q!=a.linkText)e=a.linkText;else if(d==l||"email"==a.type&&-1!=l.indexOf("@"))e="email"==a.type?a.email.address:g.set["data-cke-saved-href"];e&&h.setText(e);f.push(t(c,h))}c.getSelection().selectRanges(f);delete this._.selectedElements}else{b=n.getLinkAttributes(c,a);g=c.getSelection().getRanges();
+f=new CKEDITOR.style({element:"a",attributes:b.set});h=[];f.type=CKEDITOR.STYLE_INLINE;for(l=0;l<g.length;l++){d=g[l];d.collapsed?(e=new CKEDITOR.dom.text(a.linkText||("email"==a.type?a.email.address:b.set["data-cke-saved-href"]),c.document),d.insertNode(e),d.selectNodeContents(e)):q!==a.linkText&&(e=new CKEDITOR.dom.text(a.linkText,c.document),d.shrink(CKEDITOR.SHRINK_TEXT),c.editable().extractHtmlFromRange(d),d.insertNode(e));e=d._find("a");for(k=0;k<e.length;k++)e[k].remove(!0);f.applyToRange(d,
+c);h.push(d)}c.getSelection().selectRanges(h)}},onLoad:function(){c.config.linkShowAdvancedTab||this.hidePage("advanced");c.config.linkShowTargetTab||this.hidePage("target")},onFocus:function(){var a=this.getContentElement("info","linkType");a&&"url"==a.getValue()&&(a=this.getContentElement("info","url"),a.select())}}})})();
\ No newline at end of file
index c946ba5..d94adb4 100644 (file)
Binary files a/js/ckeditor/plugins/link/images/anchor.png and b/js/ckeditor/plugins/link/images/anchor.png differ
index 908b9fa..186c3e9 100644 (file)
Binary files a/js/ckeditor/plugins/link/images/hidpi/anchor.png and b/js/ckeditor/plugins/link/images/hidpi/anchor.png differ
diff --git a/js/ckeditor/plugins/sourcedialog/dialogs/sourcedialog.js b/js/ckeditor/plugins/sourcedialog/dialogs/sourcedialog.js
new file mode 100644 (file)
index 0000000..048361c
--- /dev/null
@@ -0,0 +1,6 @@
+/*
+ Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ For licensing, see LICENSE.md or http://ckeditor.com/license
+*/
+CKEDITOR.dialog.add("sourcedialog",function(a){var b=CKEDITOR.document.getWindow().getViewPaneSize(),e=Math.min(b.width-70,800),b=b.height/1.5,d;return{title:a.lang.sourcedialog.title,minWidth:100,minHeight:100,onShow:function(){this.setValueOf("main","data",d=a.getData())},onOk:function(){function b(f,c){a.focus();a.setData(c,function(){f.hide();var b=a.createRange();b.moveToElementEditStart(a.editable());b.select()})}return function(){var a=this.getValueOf("main","data").replace(/\r/g,""),c=this;
+if(a===d)return!0;setTimeout(function(){b(c,a)});return!1}}(),contents:[{id:"main",label:a.lang.sourcedialog.title,elements:[{type:"textarea",id:"data",dir:"ltr",inputStyle:"cursor:auto;width:"+e+"px;height:"+b+"px;tab-size:4;text-align:left;","class":"cke_source"}]}]}});
\ No newline at end of file
diff --git a/js/ckeditor/samples/css/samples.css b/js/ckeditor/samples/css/samples.css
new file mode 100644 (file)
index 0000000..f1973d0
--- /dev/null
@@ -0,0 +1,1632 @@
+/**\r
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+ * For licensing, see LICENSE.md or http://ckeditor.com/license\r
+ */\r
+@media (max-width: 900px) {\r
+  .global-is-mobile-hidden {\r
+    display: none !important;\r
+  }\r
+}\r
+article,\r
+aside,\r
+details,\r
+figcaption,\r
+figure,\r
+footer,\r
+header,\r
+hgroup,\r
+main,\r
+menu,\r
+nav,\r
+section {\r
+  display: block;\r
+}\r
+body,\r
+html {\r
+  margin: 0;\r
+  padding: 0;\r
+  font: 16px / 1.8 Arial, 'Helvetica Neue', Helvetica, sans-serif;\r
+  font-weight: 300;\r
+  color: #575757;\r
+}\r
+.grid-width-10 {\r
+  width: 10%;\r
+}\r
+.grid-width-20 {\r
+  width: 20%;\r
+}\r
+.grid-width-30 {\r
+  width: 30%;\r
+}\r
+.grid-width-40 {\r
+  width: 40%;\r
+}\r
+.grid-width-50 {\r
+  width: 50%;\r
+}\r
+.grid-width-60 {\r
+  width: 60%;\r
+}\r
+.grid-width-70 {\r
+  width: 70%;\r
+}\r
+.grid-width-80 {\r
+  width: 80%;\r
+}\r
+.grid-width-90 {\r
+  width: 90%;\r
+}\r
+.grid-width-100 {\r
+  width: 100%;\r
+}\r
+@media (max-width: 900px) {\r
+  .grid-width-10,\r
+  .grid-width-20,\r
+  .grid-width-30,\r
+  .grid-width-40,\r
+  .grid-width-50,\r
+  .grid-width-60,\r
+  .grid-width-70,\r
+  .grid-width-80,\r
+  .grid-width-90,\r
+  .grid-width-100 {\r
+    width: 100%;\r
+  }\r
+}\r
+*[class*="grid-width"] {\r
+  -webkit-box-sizing: border-box;\r
+  -moz-box-sizing: border-box;\r
+  box-sizing: border-box;\r
+  padding-left: 4%;\r
+  padding-right: 4%;\r
+  float: left;\r
+}\r
+*[class*="grid-width"]:after,\r
+.grid-container:after,\r
+*[class*="grid-width"]:before,\r
+.grid-container:before {\r
+  content: '';\r
+  display: block;\r
+  overflow: hidden;\r
+  visibility: hidden;\r
+  font-size: 0;\r
+  line-height: 0;\r
+  width: 0;\r
+  height: 0;\r
+}\r
+*[class*="grid-width"]:after,\r
+.grid-container:after {\r
+  clear: both;\r
+}\r
+.grid-container {\r
+  -webkit-box-sizing: border-box;\r
+  -moz-box-sizing: border-box;\r
+  box-sizing: border-box;\r
+  margin-left: auto;\r
+  margin-right: auto;\r
+}\r
+.grid-container-nested *[class*="grid-width"]:first-child {\r
+  padding-left: 0;\r
+}\r
+.grid-container-nested *[class*="grid-width"]:last-child {\r
+  padding-right: 0;\r
+}\r
+@media (max-width: 900px) {\r
+  .grid-container-nested *[class*="grid-width"]:first-child {\r
+    padding-left: 4%;\r
+  }\r
+  .grid-container-nested *[class*="grid-width"]:last-child {\r
+    padding-right: 4%;\r
+  }\r
+}\r
+.header-a {\r
+  min-height: 140px;\r
+  overflow: hidden;\r
+}\r
+.header-a .header-a-logo {\r
+  margin: 40px 0 0;\r
+}\r
+@media (max-width: 900px) {\r
+  .header-a .header-a-logo {\r
+    text-align: center;\r
+  }\r
+}\r
+.header-a .header-a-logo img {\r
+  border: transparent;\r
+}\r
+.navigation-a {\r
+  height: 30px;\r
+  background: #3D3D3D;\r
+  position: absolute;\r
+  left: 0;\r
+  right: 0;\r
+  top: 0;\r
+  padding: 0;\r
+  overflow: hidden;\r
+}\r
+@media (max-width: 900px) {\r
+  .navigation-a {\r
+    text-align: center;\r
+  }\r
+}\r
+.navigation-a ul {\r
+  list-style: none;\r
+  margin: 0;\r
+  overflow: hidden;\r
+}\r
+.navigation-a ul li,\r
+.navigation-a ul li a {\r
+  display: inline-block;\r
+}\r
+@media (max-width: 900px) {\r
+  .navigation-a ul {\r
+    width: auto;\r
+    text-overflow: ellipsis;\r
+    white-space: nowrap;\r
+    display: inline-block;\r
+    float: none;\r
+  }\r
+  .navigation-a ul:before,\r
+  .navigation-a ul:after {\r
+    display: none;\r
+  }\r
+}\r
+.navigation-a ul.navigation-a-left {\r
+  text-align: left;\r
+}\r
+@media (max-width: 900px) {\r
+  .navigation-a ul.navigation-a-left {\r
+    padding-right: 0;\r
+  }\r
+}\r
+.navigation-a ul.navigation-a-right {\r
+  text-align: right;\r
+}\r
+@media (max-width: 900px) {\r
+  .navigation-a ul.navigation-a-right {\r
+    padding-left: 23px;\r
+  }\r
+}\r
+.navigation-a ul li + li {\r
+  margin-left: 23px;\r
+}\r
+.navigation-a ul li a {\r
+  font-size: 10px;\r
+  font-size: 0.625rem;\r
+  line-height: 18px;\r
+  line-height: 1.13rem;\r
+  line-height: 30px;\r
+  float: left;\r
+  color: #ddd;\r
+  font-weight: bold;\r
+  text-decoration: none;\r
+  text-transform: uppercase;\r
+}\r
+.navigation-a ul li a:hover {\r
+  cursor: pointer;\r
+  color: #fff;\r
+}\r
+.icon-navigation-a-github:before,\r
+.icon-navigation-a-github:after {\r
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAAXNSR0IArs4c6QAAAa9JREFUOBGNlM8rRGEUht0pDGosjKYZpUSIkuwsiCaxUEqK2VOUBcrWv2BjxUJho6wsLLDzY2fhD5iR5NeOcJvIjOfM3O927m3mmlPPnPec835nZprvjlVVJvL5fCOjMWiDCLzCLVxZlpUj/x8saYV9+IZS8UJzFWoCt2GYgk+oJG4wJUouZDANv5VsUZ47dNSzkEYHfIDEHixDWgoiB/rTHlPPwBNInPmXHRb7hdeUDFG10AN1Th1Fd5mD6BMwMVnoUyVA3t3EkjkQlDFfmwPkc7NsQTXf0bGgJWaGb16dk18+EmLYawzkC+6Q3KdK4kiZqtGdskx/kmdlCJS86RuGrDLFZJmtGi1KB0q+VhOGsDLZsiyjGsOY4qoOkrO+YUauwCDoOKWo9xk9JfM+MPdSzqZdA8UlyDO3AvKLPsIG9LsmBHUKduEHdCy6PrpJZyKXdwKMOemaissOHJ9O9xTeh57GluMYIsehWy8STW/d8ZhkI0b9PjFasA1fsAOb0KCN1PLXYyKLGNdzj2YYArnZDyDRrA3Ua4UuDzd5QM/KaoxhmAO5Om5Qt8OI2/CJP6MVa1dvltQ5AAAAAElFTkSuQmCC");\r
+}\r
+.navigation-b {\r
+  text-align: right;\r
+  margin: 52px 0 0;\r
+  overflow: visible;\r
+}\r
+@media (max-width: 900px) {\r
+  .navigation-b {\r
+    text-align: center;\r
+    margin-top: 20px;\r
+    padding: 0;\r
+  }\r
+}\r
+.navigation-b ul {\r
+  padding: 0;\r
+  list-style: none;\r
+  margin: 0;\r
+  overflow: visible;\r
+}\r
+.navigation-b ul li,\r
+.navigation-b ul li a {\r
+  display: inline-block;\r
+}\r
+@media (max-width: 900px) {\r
+  .navigation-b ul {\r
+    display: table;\r
+    width: 100%;\r
+    padding-bottom: 1.5em;\r
+  }\r
+}\r
+@media (max-width: 900px) {\r
+  .navigation-b ul li {\r
+    display: table-row;\r
+  }\r
+}\r
+.navigation-b ul li + li {\r
+  margin-left: 20px;\r
+}\r
+@media (max-width: 900px) {\r
+  .navigation-b ul li + li {\r
+    margin-left: 0;\r
+  }\r
+}\r
+.navigation-b ul li a {\r
+  -webkit-box-sizing: border-box;\r
+  -moz-box-sizing: border-box;\r
+  box-sizing: border-box;\r
+  text-transform: uppercase;\r
+  text-decoration: none;\r
+  outline: none;\r
+}\r
+@media (max-width: 900px) {\r
+  .navigation-b ul li a {\r
+    width: 100%;\r
+    -webkit-border-radius: 0;\r
+    -webkit-background-clip: padding-box;\r
+    -moz-border-radius: 0;\r
+    -moz-background-clip: padding;\r
+    border-radius: 0;\r
+    background-clip: padding-box;\r
+  }\r
+}\r
+.footer-a {\r
+  font-size: 13px;\r
+  font-size: 0.8125rem;\r
+  line-height: 23.4px;\r
+  line-height: 1.46rem;\r
+  padding-top: 2.25em;\r
+  padding-bottom: 2.25em;\r
+  overflow: hidden;\r
+  color: #8a8a8a;\r
+}\r
+.footer-a a {\r
+  color: #27C0D8;\r
+  text-decoration: none;\r
+  border-bottom: 1px dotted #27C0D8;\r
+}\r
+.footer-a a:hover {\r
+  color: #23adc2;\r
+}\r
+.footer-a p {\r
+  margin: 0;\r
+  display: inline-block;\r
+  text-align: center;\r
+}\r
+.content {\r
+  font-size: 14px;\r
+  font-size: 0.875rem;\r
+  line-height: 25.2px;\r
+  line-height: 1.57rem;\r
+  overflow: hidden;\r
+  padding-top: 1.5em;\r
+  padding-bottom: 1.5em;\r
+}\r
+.content p {\r
+  margin: 0.75em 0;\r
+}\r
+.content ul,\r
+.content ol,\r
+.content pre,\r
+.content blockquote,\r
+.content textarea:not([class^="cke"]),\r
+.content .cke {\r
+  margin: 1.875em 0;\r
+}\r
+.content code,\r
+.content kbd {\r
+  -webkit-border-radius: 3px;\r
+  -webkit-background-clip: padding-box;\r
+  -moz-border-radius: 3px;\r
+  -moz-background-clip: padding;\r
+  border-radius: 3px;\r
+  background-clip: padding-box;\r
+  padding: 3px 4px;\r
+}\r
+.content pre,\r
+.content code,\r
+.content kbd,\r
+.content blockquote {\r
+  background: #f5f5f5;\r
+}\r
+.content blockquote,\r
+.content pre {\r
+  background: none;\r
+  border-left: 4px solid #27C0D8;\r
+  padding: 1.5em 2.25em;\r
+}\r
+.content p a,\r
+.content ul a,\r
+.content ol a,\r
+.content blockquote a,\r
+.content h1 a,\r
+.content h2 a,\r
+.content h3 a,\r
+.content h4 a,\r
+.content h5 a {\r
+  color: #27C0D8;\r
+  text-decoration: none;\r
+  border-bottom: 1px dotted #27C0D8;\r
+}\r
+.content p a:hover,\r
+.content ul a:hover,\r
+.content ol a:hover,\r
+.content blockquote a:hover,\r
+.content h1 a:hover,\r
+.content h2 a:hover,\r
+.content h3 a:hover,\r
+.content h4 a:hover,\r
+.content h5 a:hover {\r
+  color: #23adc2;\r
+}\r
+.content h1,\r
+.content h2,\r
+.content h3,\r
+.content h4,\r
+.content h5 {\r
+  color: #000;\r
+  font-weight: 100;\r
+}\r
+.content h1 code,\r
+.content h2 code,\r
+.content h3 code,\r
+.content h4 code,\r
+.content h5 code,\r
+.content h1 kbd,\r
+.content h2 kbd,\r
+.content h3 kbd,\r
+.content h4 kbd,\r
+.content h5 kbd {\r
+  font-size: inherit;\r
+}\r
+.content h1 a.content-heading-anchor,\r
+.content h2 a.content-heading-anchor,\r
+.content h3 a.content-heading-anchor,\r
+.content h4 a.content-heading-anchor,\r
+.content h5 a.content-heading-anchor {\r
+  font-weight: 100;\r
+  vertical-align: middle;\r
+  opacity: 0;\r
+  border: 0;\r
+}\r
+.content h1:hover a.content-heading-anchor,\r
+.content h2:hover a.content-heading-anchor,\r
+.content h3:hover a.content-heading-anchor,\r
+.content h4:hover a.content-heading-anchor,\r
+.content h5:hover a.content-heading-anchor {\r
+  opacity: 1;\r
+}\r
+.content h1:target a,\r
+.content h2:target a,\r
+.content h3:target a,\r
+.content h4:target a,\r
+.content h5:target a {\r
+  -webkit-animation: targetLinkOpacity 0.5s linear alternate;\r
+  -moz-animation: targetLinkOpacity 0.5s linear alternate;\r
+  -o-animation: targetLinkOpacity 0.5s linear alternate;\r
+  animation: targetLinkOpacity 0.5s linear alternate;\r
+  opacity: 1;\r
+}\r
+.content input,\r
+.content select,\r
+.content textarea:not([class^="cke"]) {\r
+  -webkit-border-radius: 3px;\r
+  -webkit-background-clip: padding-box;\r
+  -moz-border-radius: 3px;\r
+  -moz-background-clip: padding;\r
+  border-radius: 3px;\r
+  background-clip: padding-box;\r
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.08);\r
+  -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.08);\r
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.08);\r
+  font: inherit;\r
+  color: inherit;\r
+  border: 1px solid #D9D9D9;\r
+  padding: .2em .5em;\r
+}\r
+.content input:focus,\r
+.content select:focus,\r
+.content textarea:not([class^="cke"]):focus {\r
+  border-color: #66afe9;\r
+  outline: 0;\r
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.08), 0 0 8px #93c6ef;\r
+  -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.08), 0 0 8px #93c6ef;\r
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.08), 0 0 8px #93c6ef;\r
+}\r
+.content abbr {\r
+  border-bottom: 1px dotted #666;\r
+  cursor: pointer;\r
+}\r
+.content blockquote {\r
+  font-style: italic;\r
+  font-family: Georgia, Times, "Times New Roman", serif;\r
+  font-size: 16px;\r
+  font-size: 1rem;\r
+  line-height: 28.8px;\r
+  line-height: 1.8rem;\r
+}\r
+.content em {\r
+  font-style: italic;\r
+}\r
+.content h1 {\r
+  font-size: 36px;\r
+  font-size: 2.25rem;\r
+  line-height: 64.8px;\r
+  line-height: 4.05rem;\r
+  margin: 1.125em 0 0;\r
+}\r
+.content h2 {\r
+  font-size: 27.2px;\r
+  font-size: 1.7rem;\r
+  line-height: 48.96px;\r
+  line-height: 3.06rem;\r
+  margin: 0.9em 0 0;\r
+}\r
+.content h3 {\r
+  font-size: 24px;\r
+  font-size: 1.5rem;\r
+  line-height: 43.2px;\r
+  line-height: 2.7rem;\r
+  font-weight: 500;\r
+  margin: 0.75em 0 0;\r
+}\r
+.content h4 {\r
+  font-size: 19.2px;\r
+  font-size: 1.2rem;\r
+  line-height: 34.56px;\r
+  line-height: 2.16rem;\r
+  font-weight: 500;\r
+  margin: 0.75em 0 0;\r
+}\r
+.content h5 {\r
+  font-size: 17.6px;\r
+  font-size: 1.1rem;\r
+  line-height: 31.68px;\r
+  line-height: 1.98rem;\r
+  font-weight: 500;\r
+  margin: 0.75em 0 0;\r
+}\r
+.content hr {\r
+  border: 0;\r
+  border-top: 4px solid #D9D9D9;\r
+  margin: 1.5em 0;\r
+}\r
+.content input[type="text"] {\r
+  height: 1.8em;\r
+  line-height: 1.8em;\r
+}\r
+.content input[type="button"] {\r
+  -webkit-appearance: button;\r
+  -moz-appearance: button;\r
+  appearance: button;\r
+}\r
+.content kbd {\r
+  font-size: 12px;\r
+  font-size: 0.75rem;\r
+  line-height: 21.6px;\r
+  line-height: 1.35rem;\r
+  font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;\r
+  padding: 2px 6px;\r
+  -webkit-box-shadow: 0 0 4px #fff inset, 0 2px 0 #D9D9D9;\r
+  -moz-box-shadow: 0 0 4px #fff inset, 0 2px 0 #D9D9D9;\r
+  box-shadow: 0 0 4px #fff inset, 0 2px 0 #D9D9D9;\r
+}\r
+.content p img {\r
+  vertical-align: middle;\r
+}\r
+.content p pre {\r
+  padding: 1.5em;\r
+}\r
+.content pre {\r
+  padding: 0;\r
+  border: 0;\r
+  tab-size: 4;\r
+  -o-tab-size: 4;\r
+  -moz-tab-size: 4;\r
+}\r
+.content pre,\r
+.content code {\r
+  font-size: 11.89px;\r
+  font-size: 0.743rem;\r
+  line-height: 21.4px;\r
+  line-height: 1.34rem;\r
+  font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif;\r
+}\r
+.content pre a,\r
+.content code a {\r
+  border: 0;\r
+}\r
+.content pre code {\r
+  padding: 0.75em;\r
+  display: block;\r
+}\r
+.content strong {\r
+  color: #000;\r
+}\r
+.content ul ul,\r
+.content ol ul,\r
+.content ul ol,\r
+.content ol ol {\r
+  margin: 0.75em 0;\r
+}\r
+.content ul li,\r
+.content ol li {\r
+  font-size: 14px;\r
+  font-size: 0.875rem;\r
+  line-height: 30.24px;\r
+  line-height: 1.89rem;\r
+}\r
+.content textarea:not([class^="cke"]) {\r
+  width: 100%;\r
+}\r
+.content div.todo {\r
+  border: 2px dotted #444;\r
+  padding: 10px;\r
+  margin: 60px 0 10px 0;\r
+  /* Remove me some day */\r
+}\r
+.content div.todo:before {\r
+  content: "TODO";\r
+  font-weight: bold;\r
+}\r
+body a.button-a,\r
+body button.button-a,\r
+body input.button-a {\r
+  -webkit-border-radius: 3px;\r
+  -webkit-background-clip: padding-box;\r
+  -moz-border-radius: 3px;\r
+  -moz-background-clip: padding;\r
+  border-radius: 3px;\r
+  background-clip: padding-box;\r
+  font-size: 14px;\r
+  font-size: 0.875rem;\r
+  line-height: 25.2px;\r
+  line-height: 1.57rem;\r
+  height: 36px;\r
+  line-height: 36px;\r
+  padding: 0 1.1em;\r
+  font-weight: 700;\r
+  color: #3e3e3e;\r
+  white-space: nowrap;\r
+  text-decoration: none;\r
+  display: inline-block;\r
+  cursor: pointer;\r
+  border: 0;\r
+  vertical-align: middle;\r
+  margin: 1px 0;\r
+  background: transparent;\r
+}\r
+body a.button-a.icon-pos-left,\r
+body button.button-a.icon-pos-left,\r
+body input.button-a.icon-pos-left {\r
+  padding-left: .8em;\r
+}\r
+body a.button-a.icon-pos-right,\r
+body button.button-a.icon-pos-right,\r
+body input.button-a.icon-pos-right {\r
+  padding-right: .8em;\r
+}\r
+body a.button-a.button-a-no-text,\r
+body button.button-a.button-a-no-text,\r
+body input.button-a.button-a-no-text {\r
+  -webkit-border-radius: 100px;\r
+  -webkit-background-clip: padding-box;\r
+  -moz-border-radius: 100px;\r
+  -moz-background-clip: padding;\r
+  border-radius: 100px;\r
+  background-clip: padding-box;\r
+  width: 36px;\r
+  padding: 0;\r
+  text-indent: -999px;\r
+  overflow: hidden;\r
+  position: relative;\r
+  text-align: center;\r
+}\r
+body a.button-a.button-a-no-text:before,\r
+body button.button-a.button-a-no-text:before,\r
+body input.button-a.button-a-no-text:before {\r
+  position: absolute;\r
+  left: 50%;\r
+  top: 50%;\r
+  margin: -9px 0 0 -9px;\r
+}\r
+@media (max-width: 900px) {\r
+  body a.button-a.button-a-mobile-collapsed,\r
+  body button.button-a.button-a-mobile-collapsed,\r
+  body input.button-a.button-a-mobile-collapsed {\r
+    -webkit-border-radius: 100px;\r
+    -webkit-background-clip: padding-box;\r
+    -moz-border-radius: 100px;\r
+    -moz-background-clip: padding;\r
+    border-radius: 100px;\r
+    background-clip: padding-box;\r
+    width: 36px;\r
+    padding: 0;\r
+    text-indent: -999px;\r
+    overflow: hidden;\r
+    position: relative;\r
+    text-align: center;\r
+  }\r
+  body a.button-a.button-a-mobile-collapsed:before,\r
+  body button.button-a.button-a-mobile-collapsed:before,\r
+  body input.button-a.button-a-mobile-collapsed:before {\r
+    position: absolute;\r
+    left: 50%;\r
+    top: 50%;\r
+    margin: -9px 0 0 -9px;\r
+  }\r
+  body a.button-a.button-a-mobile-collapsed:before,\r
+  body button.button-a.button-a-mobile-collapsed:before,\r
+  body input.button-a.button-a-mobile-collapsed:before {\r
+    position: absolute;\r
+    left: 50%;\r
+    top: 50%;\r
+    margin: -9px 0 0 -9px;\r
+  }\r
+}\r
+body a.button-a:active,\r
+body button.button-a:active,\r
+body input.button-a:active,\r
+body a.button-a:hover,\r
+body button.button-a:hover,\r
+body input.button-a:hover {\r
+  color: #fff;\r
+  background: #23adc2;\r
+}\r
+body a.button-a:focus,\r
+body button.button-a:focus,\r
+body input.button-a:focus {\r
+  border-color: #66afe9;\r
+  outline: 0;\r
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px #93c6ef;\r
+  -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px #93c6ef;\r
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px #93c6ef;\r
+}\r
+body a.button-a-soft,\r
+body button.button-a-soft,\r
+body input.button-a-soft {\r
+  background: #e7e7e7;\r
+}\r
+body a.button-a-soft:active,\r
+body button.button-a-soft:active,\r
+body input.button-a-soft:active,\r
+body a.button-a-soft:hover,\r
+body button.button-a-soft:hover,\r
+body input.button-a-soft:hover {\r
+  color: #3e3e3e;\r
+  background: #cecece;\r
+}\r
+body a.button-a-background,\r
+body button.button-a-background,\r
+body input.button-a-background,\r
+body a.navigation-b ul li a:hover,\r
+body button.navigation-b ul li a:hover,\r
+body input.navigation-b ul li a:hover {\r
+  color: #fff;\r
+  background: #27C0D8;\r
+}\r
+body a.button-a-background:active,\r
+body button.button-a-background:active,\r
+body input.button-a-background:active,\r
+body a.button-a-background:hover,\r
+body button.button-a-background:hover,\r
+body input.button-a-background:hover,\r
+body a.navigation-b ul li a:hover:active,\r
+body button.navigation-b ul li a:hover:active,\r
+body input.navigation-b ul li a:hover:active,\r
+body a.navigation-b ul li a:hover:hover,\r
+body button.navigation-b ul li a:hover:hover,\r
+body input.navigation-b ul li a:hover:hover {\r
+  color: #fff;\r
+  background: #23adc2;\r
+}\r
+.balloon-a {\r
+  font-size: 12px;\r
+  font-size: 0.75rem;\r
+  line-height: 21.6px;\r
+  line-height: 1.35rem;\r
+  -webkit-border-radius: 3px;\r
+  -webkit-background-clip: padding-box;\r
+  -moz-border-radius: 3px;\r
+  -moz-background-clip: padding;\r
+  border-radius: 3px;\r
+  background-clip: padding-box;\r
+  border-bottom: 3px solid #d4d4d4;\r
+  background: #ebebeb;\r
+  display: inline-block;\r
+  white-space: nowrap;\r
+  padding: .4em 1.2em .2em;\r
+  font-weight: 700;\r
+  position: relative;\r
+  z-index: 1000;\r
+  text-transform: none;\r
+  color: #575757;\r
+}\r
+.balloon-a:hover {\r
+  color: #575757;\r
+}\r
+.balloon-a:before {\r
+  content: '';\r
+  width: 0;\r
+  height: 0;\r
+  border-style: solid;\r
+  position: absolute;\r
+}\r
+.balloon-a-ne:before,\r
+.balloon-a-nw:before {\r
+  top: -13px;\r
+  border-width: 0 9px 15.6px 9px;\r
+  border-color: transparent transparent #ebebeb transparent;\r
+}\r
+.balloon-a-se:before,\r
+.balloon-a-sw:before {\r
+  bottom: -13px;\r
+  border-width: 15.6px 9px 0 9px;\r
+  border-color: #ebebeb transparent transparent transparent;\r
+}\r
+.balloon-a-nw:before,\r
+.balloon-a-sw:before {\r
+  left: 20px;\r
+}\r
+.balloon-a-ne:before,\r
+.balloon-a-se:before {\r
+  right: 20px;\r
+}\r
+.icon-pos-left:before,\r
+.icon-pos-right:after {\r
+  content: '';\r
+  display: inline-block;\r
+  width: 18px;\r
+  height: 18px;\r
+  vertical-align: middle;\r
+  background-repeat: no-repeat;\r
+}\r
+.icon-pos-left:before {\r
+  margin-right: 10px;\r
+}\r
+.icon-pos-right:after {\r
+  margin-left: 10px;\r
+}\r
+.icon-download:before,\r
+.icon-download:after {\r
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAQFJREFUOBGtVDESgjAQBGfobHwE/AIa/AN/8EEWfMWGZ+gDaG2ws8BdyY13SRgGcGducre3WQ5NSJIIxnGsES3ijhhcMCdXR7ZYCqIc0SGWQE1ud7sKjRLxXHJQfWpLYwaCk6wxET/u+U2GIngd8yRViINau28bBH/YAGqvSQPhRNQHqBqj3FY0NKq27TW7qhSTDaCOhkaRAj7Hmm8S4V+c6C+gUa+crsizuWmoc70MKbWCnqPy2GvcUJxE4a/sIajRaGkU+/sf4IuISQGePR/T/QMbHEhwPLVnMWPuOCwGnWg41dwVeaN3ccHch70idIRi/6WV0WC2/zMiZm661R+2DxyEdjTuST3mAAAAAElFTkSuQmCC");\r
+}\r
+.icon-question-mark:before,\r
+.icon-question-mark:after {\r
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAUhJREFUOBGllLFOAkEQhjk0WthT2JFA7Czsqc7OxFLewEeAZ/AVbO0tTLTSBKhstTBUNkYLEoVAbD2//zILe5e9uwCT/JnZmX/+m83ebq0WsCRJYnANxmBhUKxcHGjJpiC1wQBUmTjtbLetKHTAT5WCVxe3kxEjoUmKRL6pvYEZyJt6VpOxCG3nmfyx+yJxBM7BFPg2SDlkTv2sxZqi4YnUvfgswI9FuHAkzz9EUTTRmqYeTifXsvoj/s9i57oi6ljz9kviFdyBCbgHe+rCn4C8jVXQ18rshuKOiTSIXwLkRZWQTurARJrE7wERpea7kD7BkcgB+yB3CFGlPmgqCNiXhEagSGif2qU1Ln8FW/tupK3pXhXZrWNDuCoikY/rHPMT5KFr2MAPTSM90rIrUjJIeq1WV0RTwN7+0rrtILb9M+LEbLq1H7Z/Ea3+RvBddl0AAAAASUVORK5CYII=");\r
+}\r
+.icon-close:before,\r
+.icon-close:after {\r
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAUlJREFUOBGllDFuwzAMRe3Cd+jYKUCzd/XkDtm9dsoVOuUqBnqBoodwgBwiW8ZsXTIWqPu+Iia0LMAoTOBbJEV+UZTkosjIMAwN6MARXCKky9dkUsYuglagB3OimNU4O1pM1OB7jsHNK7YekeFQJZ5kj/0LcnLA+RMnlHOvDMNv5wO7BFuQkn3hq0ALjKwPVeF4BSaqpLRy0T1ZIHFz75bE2BR8dBImqmBrwRplg09QmR/9GZyBSadAHauXCZkRROKURLlHEemepJIlIyhHotzLg1/N6erTxtmmvqA8muHGIbc1rTBqrEuwnqWnGbbmmz0hwaHtvM2QhWbrXZnosvnTWWPrdCY9w7cDJtf3h9VHjy5Zq9UZ08beyJh7Aicg6W/VYvgnIjJdNn9PMIOITJWcgnV9VvcnEitY/mitNFZZ/hsxsljdv39sfybRQ4R/kU0MAAAAAElFTkSuQmCC");\r
+}\r
+.ie8 .switch > * {\r
+  vertical-align: middle;\r
+}\r
+.ie8 .switch input[type="radio"] {\r
+  margin: 0 0.25em;\r
+  display: inline-block;\r
+}\r
+.ie8 .switch label {\r
+  margin-left: 0 !important;\r
+  margin-right: 0 !important;\r
+}\r
+.ie8 .switch label[data-for="1"] {\r
+  float: left;\r
+}\r
+.ie8 .switch label[data-for="2"] {\r
+  float: right;\r
+}\r
+.ie8 .switch .switch-inner {\r
+  display: none;\r
+}\r
+.switch {\r
+  font-size: 14px;\r
+  font-size: 0.875rem;\r
+  line-height: 25.2px;\r
+  line-height: 1.57rem;\r
+  font-weight: bold;\r
+  background-color: #27C0D8;\r
+  overflow: hidden;\r
+  display: inline-block;\r
+  padding: 0.75em 0.25em;\r
+  color: #fff;\r
+  -webkit-border-radius: 3px;\r
+  -webkit-background-clip: padding-box;\r
+  -moz-border-radius: 3px;\r
+  -moz-background-clip: padding;\r
+  border-radius: 3px;\r
+  background-clip: padding-box;\r
+  position: relative;\r
+}\r
+.switch input[type="radio"] {\r
+  display: none;\r
+}\r
+.switch label {\r
+  position: relative;\r
+  z-index: 2;\r
+  float: left;\r
+  cursor: pointer;\r
+  padding: 0 0.75em;\r
+}\r
+.switch label:hover {\r
+  text-decoration: underline;\r
+}\r
+.switch .switch-inner {\r
+  float: left;\r
+  background-color: #FFF;\r
+  height: 1.5em;\r
+  width: 4.125em;\r
+  padding: 2px;\r
+  margin: 0 0.25em;\r
+  -webkit-border-radius: 5.5px;\r
+  -webkit-background-clip: padding-box;\r
+  -moz-border-radius: 5.5px;\r
+  -moz-background-clip: padding;\r
+  border-radius: 5.5px;\r
+  background-clip: padding-box;\r
+}\r
+.switch .switch-inner .handler {\r
+  overflow: hidden;\r
+  position: relative;\r
+  display: block;\r
+  height: 1.5em;\r
+  width: 1.5em;\r
+  background: #25b4cb;\r
+  -webkit-border-radius: 4.5px;\r
+  -webkit-background-clip: padding-box;\r
+  -moz-border-radius: 4.5px;\r
+  -moz-background-clip: padding;\r
+  border-radius: 4.5px;\r
+  background-clip: padding-box;\r
+}\r
+.switch .switch-inner .handler:before {\r
+  content: '';\r
+  display: block;\r
+  position: absolute;\r
+  top: 0;\r
+  right: 0;\r
+  bottom: 3px;\r
+  left: 0;\r
+  background-color: #34c4da;\r
+  -webkit-border-bottom-left-radius: 4.5px;\r
+  -moz-border-radius-bottomleft: 4.5px;\r
+  border-bottom-left-radius: 4.5px;\r
+  -webkit-border-bottom-right-radius: 4.5px;\r
+  -webkit-background-clip: padding-box;\r
+  -moz-border-radius-bottomright: 4.5px;\r
+  -moz-background-clip: padding;\r
+  border-bottom-right-radius: 4.5px;\r
+  background-clip: padding-box;\r
+}\r
+.switch:hover .switch-inner .handler:before {\r
+  background: #45c9dd;\r
+}\r
+.switch input[data-num="2"]:checked ~ .switch-inner > .handler {\r
+  margin-left: auto;\r
+}\r
+.switch input[data-num="2"]:checked ~ label[data-for="1"] {\r
+  padding-right: 5.125em;\r
+  margin-right: -4.375em;\r
+}\r
+.switch input[data-num="1"]:checked ~ label[data-for="2"] {\r
+  padding-left: 5.125em;\r
+  margin-left: -4.375em;\r
+}\r
+.toggler {\r
+  -webkit-user-select: none;\r
+  -moz-user-select: none;\r
+  -ms-user-select: none;\r
+  user-select: none;\r
+}\r
+.toggler label {\r
+  cursor: pointer;\r
+}\r
+.toggler [data-collapse] {\r
+  display: inherit;\r
+}\r
+.toggler [data-expand] {\r
+  display: none;\r
+}\r
+.toggler.collapsed [data-collapse] {\r
+  display: none;\r
+}\r
+.toggler.collapsed [data-expand] {\r
+  display: inherit;\r
+}\r
+.toggler-container {\r
+  overflow: hidden;\r
+}\r
+.toggler-container.collapsed {\r
+  height: 0;\r
+}\r
+.icon-toggler-expanded:before,\r
+.icon-toggler-collapsed:before,\r
+.icon-toggler-expanded:after,\r
+.icon-toggler-collapsed:after {\r
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAByCAYAAABeOoENAAAAAXNSR0IArs4c6QAAAbxJREFUaAXtmT1KBEEQhRdFQdBEMfQEBoaGopl3MfECXsFERLyBh/AUIuwJDEUQM//eB11Dz1A1uzotGFTBY2rr58306+kNpmazP7Z98V8Kj8JrAT4xcgttXRVXwofwFYAcNdS6RuJegOBTuBUOhc0CfGLkqKHWJeMuFDwJJ0Jk5Kihlp6esW4embuNkVgTNdTS09MMEbkDj76sUUsPvZ2xIwTRATsQuBuxGsTIYdSSo7cztpggwprdyKlJ8ImZUUuM3s48ol1lXwQjwydm5hINl2bF53KMCL82d2mR2GvqnBfg1+aKPbb9p+oGtYXbT1GTFxKiZkfEyHgy7x0y0clR454zSGpDMzaA3fzV30hNln4qkAqkAqlAKpAKpAKpQCqQCqQCqUAqkAqkAqlAKpAKpAKpQCrw3xWY/GGcz++TP9U3Gx40GWdEAxabXA33NBywRCOfdzFcCztDJv12Rz7REMpmIc9qPBNWK0J3COWNxegxIrs+KHZcyHpjsZUSXPaypcLtseJFS3tT84WwUZG4S4vEZkl3wl5FYK4rdrT9R9Y1uIbbT12TFxKiZkfEyCYfWojMJv+NGNGPr99GI9DP7P9TCgAAAABJRU5ErkJggg==");\r
+}\r
+.icon-toggler-expanded.icon-light:before,\r
+.icon-toggler-collapsed.icon-light:before,\r
+.icon-toggler-expanded.icon-light:after,\r
+.icon-toggler-collapsed.icon-light:after {\r
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAByCAYAAABeOoENAAAAAXNSR0IArs4c6QAAAcVJREFUaAXtmT9KA1EQxhMlASE2SkpPkCJlStHOu3gGwRPYBAm5gYfwFBKwtrARRAh26vr7ljfx7TrLChtBcAa+zOSbPy/7vcTC6fV+04qimIArsALrBMXiJq1nUzQEc/AOmkw51QzdgUqAWyD7AEswA6MExeKUk6n2+zBInSJ7BKfuaZDKpRpcMa/UQUgTfWSd1jjEmlSTatXzpRlvJKJsacVtXrVlB72bWgjdiGwmEj8FOq1u4qapRprJVvkgXbFsZCTxomSqL4ssr0uQrY3TJ/AGjeFfVJlM8diaiCuDdlLiIfmNcP1+/wnu0hoVJ84oq7XeUhNXbE4dgPuEgU2Qh3PFbrx+Gs6E2hD/+tMJ3b+QadB2fiLZsG4/2poG3f6M5MMiDgVCgVAgFAgFQoFQIBQIBUKBUCAUCAVCgVAgFAgFQoFQIBQIBf66AiwLuv1jnAH/Zb/Go5abq/qdwvsLFhJNK583ctfg0Bnmrnwq+zVrYoDZM8E52M1yP9uvqcGmZP6O+CTl3LWYHdTm9yk4aCzilLZHe6XmAuzZEGL30ZrEpr64AUc2wDycK7a7X6P42BpzD+9fv4pIxn4tWznnwm0r/gQpiG1tFshTowAAAABJRU5ErkJggg==");\r
+}\r
+.icon-toggler-expanded:before,\r
+.icon-toggler-expanded:after {\r
+  background-position: top left;\r
+}\r
+.icon-toggler-collapsed:before,\r
+.icon-toggler-collapsed:after {\r
+  background-position: bottom left;\r
+}\r
+.modal {\r
+  padding: 20px;\r
+  border-radius: 3px;\r
+  background-color: white;\r
+  max-width: 700px;\r
+  -webkit-box-sizing: border-box;\r
+  -moz-box-sizing: border-box;\r
+  box-sizing: border-box;\r
+  width: 80% !important;\r
+  top: 50% !important;\r
+  -webkit-transform: translate(-50%, -50%) !important;\r
+  -moz-transform: translate(-50%, -50%) !important;\r
+  -o-transform: translate(-50%, -50%) !important;\r
+  -ms-transform: translate(-50%, -50%) !important;\r
+  transform: translate(-50%, -50%) !important;\r
+}\r
+.modal-close {\r
+  -webkit-border-radius: 100px;\r
+  -webkit-background-clip: padding-box;\r
+  -moz-border-radius: 100px;\r
+  -moz-background-clip: padding;\r
+  border-radius: 100px;\r
+  background-clip: padding-box;\r
+  cursor: pointer;\r
+  height: 18px;\r
+  width: 18px;\r
+  position: absolute;\r
+  top: 10px;\r
+  right: 10px;\r
+  font-size: 17px;\r
+  text-align: center;\r
+  line-height: 19px;\r
+  background: #cccccc;\r
+}\r
+main .grid-container,\r
+header .grid-container,\r
+.navigation-a > div,\r
+footer > div {\r
+  max-width: 968px;\r
+}\r
+.header-a {\r
+  margin-top: 30px;\r
+}\r
+.footer-a {\r
+  border-top: 1px solid #D9D9D9;\r
+}\r
+.adjoined-top {\r
+  background-color: #27C0D8;\r
+  color: #fff;\r
+}\r
+.adjoined-top .content h1,\r
+.adjoined-top .content h2,\r
+.adjoined-top .content h3,\r
+.adjoined-top .content h4,\r
+.adjoined-top .content h5 {\r
+  color: #fff;\r
+}\r
+.adjoined-top .content p {\r
+  font-size: 18px;\r
+  font-size: 1.125rem;\r
+  line-height: 32.4px;\r
+  line-height: 2.02rem;\r
+  font-weight: 100;\r
+}\r
+.adjoined-top .content p a {\r
+  text-decoration: none;\r
+  border-bottom: 1px dotted #fff;\r
+  color: inherit;\r
+}\r
+.adjoined-top .content p a:hover {\r
+  color: #e6e6e6;\r
+}\r
+.adjoined-top .content button {\r
+  color: #fff;\r
+}\r
+.adjoined-top .content strong {\r
+  color: #fff;\r
+}\r
+.adjoined-top .content code {\r
+  font-size: inherit;\r
+  color: #27C0D8;\r
+}\r
+.adjoined-bottom {\r
+  position: relative;\r
+}\r
+.adjoined-bottom:before {\r
+  z-index: -1;\r
+  content: '';\r
+  background: #27C0D8;\r
+  position: absolute;\r
+  top: 0;\r
+  left: 0;\r
+  right: 0;\r
+  height: 50%;\r
+}\r
+main .grid-container,\r
+header .grid-container,\r
+.navigation-a > div,\r
+footer > div {\r
+  max-width: 1052px;\r
+}\r
+main .grid-container.freed-width {\r
+  max-width: none;\r
+}\r
+.switch {\r
+  background: #25b4cb;\r
+  float: right;\r
+  overflow: visible;\r
+}\r
+.switch .balloon-a {\r
+  position: absolute;\r
+  top: -40px;\r
+  right: 50%;\r
+  margin-right: -15px;\r
+  background: #FFEFC1;\r
+  border-bottom-color: #DCDCA4;\r
+}\r
+.switch .balloon-a:before {\r
+  border-color: #FFEFC1 transparent transparent transparent;\r
+}\r
+#toolbar .editors-container {\r
+  overflow: hidden;\r
+  height: 0;\r
+  transition: height 200ms;\r
+}\r
+#toolbar .editors-container.active {\r
+  height: auto;\r
+}\r
+#main #editor {\r
+  background: #FFF;\r
+  padding: 2% 4%;\r
+  border: dashed 5px #27C0D8;\r
+}\r
+#main .adjoined-top:before {\r
+  height: 335px;\r
+}\r
+#toolbar .adjoined-top:before {\r
+  height: 219px;\r
+}\r
+#toolbar .adjoined-top .grid-container-nested {\r
+  height: 147px;\r
+}\r
+.content .grid-switch-magic {\r
+  margin: 3.5em 0 0;\r
+}\r
+#info-box {\r
+  padding-bottom: 0;\r
+}\r
+#info-box > div {\r
+  width: 100%;\r
+  text-align: right;\r
+}\r
+#info-box > div .toggler {\r
+  padding-right: 0;\r
+}\r
+#info-box > div .toggler:hover {\r
+  background: transparent;\r
+  color: #000;\r
+}\r
+#info-box > div .toggler:hover > label {\r
+  text-decoration: underline;\r
+}\r
+#info-box > div h2 {\r
+  float: left;\r
+  margin-top: 0;\r
+}\r
+#info-box > div#instructions-container {\r
+  text-align: left;\r
+}\r
+#toolbarModifierWrapper {\r
+  overflow: hidden;\r
+  height: 0;\r
+  opacity: 0;\r
+  transition: height 200ms;\r
+}\r
+#toolbarModifierWrapper.active {\r
+  height: auto;\r
+  opacity: 1;\r
+}\r
+header {\r
+  overflow: visible;\r
+}\r
+header div.grid-container {\r
+  overflow: visible;\r
+}\r
+header .navigation-b {\r
+  overflow: visible;\r
+}\r
+header .navigation-b ul {\r
+  overflow: visible;\r
+}\r
+header .navigation-b a {\r
+  position: relative;\r
+}\r
+header .balloon-a {\r
+  position: absolute;\r
+  top: 48px;\r
+  left: 50%;\r
+  margin-left: -35px;\r
+}\r
+@media (max-width: 1140px) {\r
+  header .balloon-a {\r
+    left: auto;\r
+    margin-left: auto;\r
+    right: 50%;\r
+    margin-right: -35px;\r
+  }\r
+  header .balloon-a:before {\r
+    left: auto;\r
+    right: 22px;\r
+  }\r
+}\r
+@media (max-width: 900px) {\r
+  header .balloon-a {\r
+    display: none;\r
+  }\r
+}\r
+#toolbar .cke_toolbar {\r
+  pointer-events: none;\r
+  -webkit-user-select: none;\r
+  -moz-user-select: none;\r
+  -ms-user-select: none;\r
+  user-select: none;\r
+  cursor: default;\r
+}\r
+.some-toolbar-active .cke_toolbar {\r
+  zoom: 1;\r
+  filter: alpha(opacity=50);\r
+  -webkit-opacity: 0.5;\r
+  -moz-opacity: 0.5;\r
+  opacity: 0.5;\r
+}\r
+.cke_toolbar.active {\r
+  position: relative;\r
+  zoom: 1;\r
+  filter: alpha(opacity=100);\r
+  -webkit-opacity: 1;\r
+  -moz-opacity: 1;\r
+  opacity: 1;\r
+}\r
+.cke_toolbar.active:after {\r
+  content: '';\r
+  display: block;\r
+  position: absolute;\r
+  top: 0;\r
+  right: 6px;\r
+  bottom: 5px;\r
+  left: 0;\r
+  -webkit-border-radius: 5px;\r
+  -webkit-background-clip: padding-box;\r
+  -moz-border-radius: 5px;\r
+  -moz-background-clip: padding;\r
+  border-radius: 5px;\r
+  background-clip: padding-box;\r
+  -webkit-box-shadow: 0px 0px 15px 3px #fff4b0;\r
+  -moz-box-shadow: 0px 0px 15px 3px #fff4b0;\r
+  box-shadow: 0px 0px 15px 3px #fff4b0;\r
+}\r
+.cke_toolbar.active .cke_toolgroup {\r
+  -webkit-box-shadow: none;\r
+  -moz-box-shadow: none;\r
+  box-shadow: none;\r
+  border-color: #e3c300;\r
+}\r
+.cke_toolbar.active .cke_combo,\r
+.cke_toolbar.active .cke_toolgroup {\r
+  position: relative;\r
+  z-index: 2;\r
+}\r
+.cke_toolbar.active .cke_combo_button {\r
+  -webkit-box-shadow: none;\r
+  -moz-box-shadow: none;\r
+  box-shadow: none;\r
+}\r
+.unselectable {\r
+  -webkit-user-select: none;\r
+  -moz-user-select: none;\r
+  -ms-user-select: none;\r
+  user-select: none;\r
+}\r
+.toolbar {\r
+  padding: 5px 0;\r
+  margin-bottom: 2.4em;\r
+  overflow: hidden;\r
+  background: #fff;\r
+}\r
+.toolbar button.button-a.cke_button {\r
+  cursor: pointer;\r
+  display: inline-block;\r
+  padding: 4px 6px;\r
+  outline: 0;\r
+  border: 1px solid #a6a6a6;\r
+}\r
+.toolbar button.button-a.hidden {\r
+  display: none;\r
+}\r
+.toolbar button.button-a.left {\r
+  float: left;\r
+  margin-right: 8px;\r
+}\r
+.toolbar button.button-a.right {\r
+  float: right;\r
+  margin-left: 8px;\r
+}\r
+.toolbar button.button-a .highlight {\r
+  color: #ffefc1;\r
+}\r
+.configContainer.hidden,\r
+.toolbarModifier.hidden,\r
+.toolbarModifier-hints.hidden {\r
+  display: none;\r
+}\r
+.toolbarModifier :focus,\r
+.toolbar button:focus,\r
+.configContainer textarea.configCode:focus {\r
+  outline: none;\r
+}\r
+div.toolbarModifier {\r
+  padding: 0;\r
+  overflow: hidden;\r
+  width: 100%;\r
+  position: relative;\r
+  display: table;\r
+  border-collapse: collapse;\r
+}\r
+div.toolbarModifier ::-moz-focus-inner {\r
+  border: 0;\r
+}\r
+div.toolbarModifier .empty {\r
+  display: none;\r
+}\r
+div.toolbarModifier.empty-visible .empty {\r
+  display: table-row;\r
+  zoom: 1;\r
+  filter: alpha(opacity=60);\r
+  -webkit-opacity: 0.6;\r
+  -moz-opacity: 0.6;\r
+  opacity: 0.6;\r
+}\r
+div.toolbarModifier .empty > p {\r
+  line-height: 31px;\r
+}\r
+div.toolbarModifier > ul {\r
+  padding: 0;\r
+  margin: 0;\r
+  border-top: 1px solid #ccc;\r
+  width: 100%;\r
+}\r
+div.toolbarModifier > ul[data-type="table-header"] {\r
+  display: table-header-group;\r
+}\r
+div.toolbarModifier > ul[data-type="table-body"] {\r
+  display: table-row-group;\r
+}\r
+div.toolbarModifier > ul p {\r
+  padding: 0;\r
+  margin: 0;\r
+}\r
+div.toolbarModifier > ul > li {\r
+  display: table-row;\r
+}\r
+div.toolbarModifier > ul > li[data-type="header"] {\r
+  font-weight: bold;\r
+  user-select: none;\r
+  cursor: default;\r
+}\r
+div.toolbarModifier > ul > li[data-type="group"],\r
+div.toolbarModifier > ul > li[data-type="separator"] {\r
+  border-bottom: 1px solid #ccc;\r
+}\r
+div.toolbarModifier > ul > li[data-type="subgroup"] {\r
+  border-top: 1px solid #eee;\r
+}\r
+div.toolbarModifier > ul > li[data-type="subgroup"]:first-child {\r
+  border-top: none;\r
+}\r
+div.toolbarModifier > ul > li[data-type="group"].active,\r
+div.toolbarModifier > ul > li[data-type="group"]:hover,\r
+div.toolbarModifier > ul > li[data-type="separator"].active,\r
+div.toolbarModifier > ul > li[data-type="separator"]:hover {\r
+  overflow: hidden;\r
+  z-index: 2;\r
+}\r
+div.toolbarModifier > ul > li[data-type="group"].active,\r
+div.toolbarModifier > ul > li[data-type="separator"].active,\r
+div.toolbarModifier > ul > li[data-type="group"].active:hover,\r
+div.toolbarModifier > ul > li[data-type="separator"].active:hover {\r
+  background: #f0fafb;\r
+}\r
+div.toolbarModifier > ul > li[data-type="group"]:hover,\r
+div.toolbarModifier > ul > li[data-type="separator"]:hover {\r
+  background: #fffbe3;\r
+}\r
+div.toolbarModifier > ul > li[data-type="separator"] {\r
+  background: #f5f5f5;\r
+}\r
+div.toolbarModifier > ul > li[data-type="separator"]:after {\r
+  content: '';\r
+  width: 100%;\r
+}\r
+div.toolbarModifier > ul > li[data-type="separator"] > p {\r
+  padding: 2px 5px;\r
+}\r
+div.toolbarModifier > ul > li > p,\r
+div.toolbarModifier > ul > li > ul {\r
+  display: table-cell;\r
+  vertical-align: middle;\r
+}\r
+div.toolbarModifier > ul > li p {\r
+  padding-left: 5px;\r
+  min-width: 200px;\r
+}\r
+div.toolbarModifier > ul > li p span {\r
+  white-space: nowrap;\r
+  cursor: default;\r
+}\r
+div.toolbarModifier > ul > li p span button {\r
+  font-size: 12.666px;\r
+  margin-right: 5px;\r
+  cursor: pointer;\r
+  background: #fff;\r
+  -webkit-border-radius: 5px;\r
+  -webkit-background-clip: padding-box;\r
+  -moz-border-radius: 5px;\r
+  -moz-background-clip: padding;\r
+  border-radius: 5px;\r
+  background-clip: padding-box;\r
+  border: 1px solid #bbb;\r
+  padding: 0 7px;\r
+  line-height: 12px;\r
+  height: 20px;\r
+}\r
+div.toolbarModifier > ul > li p span button:not(.disabled):hover,\r
+div.toolbarModifier > ul > li p span button:not(.disabled):focus {\r
+  color: #fff;\r
+  background-color: #454545;\r
+  border-color: transparent;\r
+}\r
+div.toolbarModifier > ul > li p span button.move.disabled {\r
+  cursor: default;\r
+  zoom: 1;\r
+  filter: alpha(opacity=20);\r
+  -webkit-opacity: 0.2;\r
+  -moz-opacity: 0.2;\r
+  opacity: 0.2;\r
+}\r
+div.toolbarModifier > ul > li ul {\r
+  border-collapse: collapse;\r
+  padding: 0;\r
+  width: 100%;\r
+}\r
+div.toolbarModifier > ul > li ul li {\r
+  display: table-row;\r
+  list-style-type: none;\r
+  line-height: 1;\r
+}\r
+div.toolbarModifier > ul > li ul li[data-type="subgroup"] {\r
+  border-top: 1px solid #ddd;\r
+}\r
+div.toolbarModifier > ul > li ul li[data-type="subgroup"]:first-child {\r
+  border-top: 0;\r
+}\r
+div.toolbarModifier > ul > li ul li[data-type="subgroup"] [data-type="button"] {\r
+  -webkit-border-radius: 3px;\r
+  -webkit-background-clip: padding-box;\r
+  -moz-border-radius: 3px;\r
+  -moz-background-clip: padding;\r
+  border-radius: 3px;\r
+  background-clip: padding-box;\r
+  padding: 0 2px;\r
+}\r
+div.toolbarModifier > ul > li ul li[data-type="subgroup"] [data-type="button"]:focus {\r
+  background: rgba(0, 0, 0, 0.04);\r
+}\r
+div.toolbarModifier > ul > li ul li[data-type="subgroup"] [data-type="button"] input {\r
+  vertical-align: middle;\r
+}\r
+div.toolbarModifier > ul > li ul li > p,\r
+div.toolbarModifier > ul > li ul li > ul {\r
+  display: table-cell;\r
+  vertical-align: middle;\r
+}\r
+div.toolbarModifier > ul > li ul li ul {\r
+  padding: 0;\r
+}\r
+div.toolbarModifier > ul > li ul li ul li {\r
+  padding: 0;\r
+  display: inline-block;\r
+  cursor: pointer;\r
+  margin: 2px 5px 2px 0;\r
+}\r
+div.toolbarModifier > ul > li ul li ul li .cke_combo_text {\r
+  cursor: pointer;\r
+  white-space: nowrap;\r
+}\r
+div.toolbarModifier > ul > li ul li ul li .cke_toolgroup,\r
+div.toolbarModifier > ul > li ul li ul li .cke_combo_button {\r
+  cursor: pointer;\r
+  margin: 0;\r
+  vertical-align: middle;\r
+  border: 1px solid #ddd;\r
+  font-size: 11.41px;\r
+  font-size: 0.713rem;\r
+  line-height: 20.54px;\r
+  line-height: 1.28rem;\r
+}\r
+div.toolbarModifier > .codemirror-wrapper {\r
+  overflow-y: auto;\r
+}\r
+div.toolbarModifier-hints {\r
+  float: right;\r
+  width: 350px;\r
+  min-width: 150px;\r
+  overflow-y: auto;\r
+  margin-left: 1.5em;\r
+}\r
+div.toolbarModifier-hints h3 {\r
+  font-size: 18.08px;\r
+  font-size: 1.13rem;\r
+  line-height: 32.54px;\r
+  line-height: 2.03rem;\r
+  padding: 0.36em 1.5em;\r
+  background: #f5f5f5;\r
+  border-bottom: 1px solid #ddd;\r
+  margin-top: 0;\r
+  margin-bottom: 1.2em;\r
+}\r
+div.toolbarModifier-hints dl {\r
+  margin-bottom: 1.2em;\r
+  overflow: hidden;\r
+}\r
+div.toolbarModifier-hints dl .list-header {\r
+  font-weight: bold;\r
+  border: 0;\r
+  padding-bottom: 0.6em;\r
+}\r
+div.toolbarModifier-hints dl > p {\r
+  text-align: center;\r
+}\r
+div.toolbarModifier-hints dl dt {\r
+  float: left;\r
+  width: 9em;\r
+  clear: both;\r
+  text-align: right;\r
+  border-top: 1px solid #ddd;\r
+  padding-left: 1.5em;\r
+  padding-right: .1em;\r
+  -webkit-box-sizing: border-box;\r
+  -moz-box-sizing: border-box;\r
+  box-sizing: border-box;\r
+}\r
+div.toolbarModifier-hints dl dt code {\r
+  background: none;\r
+  border: none;\r
+  vertical-align: middle;\r
+}\r
+div.toolbarModifier-hints dl dd {\r
+  margin-left: 10em;\r
+  clear: right;\r
+  padding-right: 1.5em;\r
+}\r
+div.toolbarModifier-hints dl dd code {\r
+  line-height: 2.2em;\r
+}\r
+div.toolbarModifier-hints dl dd:after {\r
+  content: '\00a0';\r
+  display: block;\r
+  clear: left;\r
+  float: right;\r
+  height: 0;\r
+  width: 0;\r
+}\r
+.toolbarModifier-hints,\r
+.configContainer textarea.configCode,\r
+.CodeMirror {\r
+  -webkit-border-radius: 3px;\r
+  -webkit-background-clip: padding-box;\r
+  -moz-border-radius: 3px;\r
+  -moz-background-clip: padding;\r
+  border-radius: 3px;\r
+  background-clip: padding-box;\r
+  border: 1px solid #ccc;\r
+  font-size: 13.01px;\r
+  font-size: 0.813rem;\r
+  line-height: 23.42px;\r
+  line-height: 1.46rem;\r
+}\r
+.configContainer textarea.configCode,\r
+.CodeMirror pre,\r
+.CodeMirror-linenumber {\r
+  font-size: 13.01px;\r
+  font-size: 0.813rem;\r
+  line-height: 23.42px;\r
+  line-height: 1.46rem;\r
+  font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif;\r
+}\r
+.CodeMirror pre {\r
+  border: none;\r
+  padding: 0;\r
+  margin: 0;\r
+}\r
+.configContainer textarea.configCode {\r
+  -webkit-box-sizing: border-box;\r
+  -moz-box-sizing: border-box;\r
+  box-sizing: border-box;\r
+  color: #575757;\r
+  padding: 10px;\r
+  width: 100%;\r
+  min-height: 500px;\r
+  margin: 0;\r
+  resize: none;\r
+  outline: none;\r
+  -moz-tab-size: 4;\r
+  tab-size: 4;\r
+  white-space: pre;\r
+  word-wrap: normal;\r
+  overflow: auto;\r
+}\r
+.CodeMirror-hints.toolbar-modifier {\r
+  padding: 0;\r
+  color: #575757;\r
+  font-size: 14px;\r
+  font-size: 0.875rem;\r
+  line-height: 25.2px;\r
+  line-height: 1.57rem;\r
+  font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif;\r
+}\r
+.CodeMirror-hints.toolbar-modifier .CodeMirror-hint-active {\r
+  color: #575757;\r
+  background: #f0fafb;\r
+}\r
+.CodeMirror-hints.toolbar-modifier > li:hover {\r
+  background: #fffbe3;\r
+}\r
+/* Text modifier */\r
+#toolbarModifierWrapper {\r
+  margin-bottom: 1.2em;\r
+}\r
+#toolbarModifierWrapper .invalid .CodeMirror {\r
+  background: #fff8f8;\r
+  border-color: red;\r
+}\r
+#toolbarModifierWrapper .CodeMirror {\r
+  height: auto;\r
+  padding: 0 0.6em;\r
+}\r
+.staticContainer {\r
+  position: fixed;\r
+  top: 0;\r
+  width: 100%;\r
+  z-index: 10;\r
+}\r
+.staticContainer > .grid-container {\r
+  max-width: 1052px;\r
+}\r
+.staticContainer > .grid-container .inner {\r
+  background: #fff;\r
+}\r
+.staticContainer > .grid-container .inner .toolbar {\r
+  margin-bottom: 0;\r
+}\r
+#help {\r
+  position: relative;\r
+  top: -15px;\r
+  left: -5px;\r
+}\r
+#help-content {\r
+  display: none;\r
+}\r
+/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL25vZGVfbW9kdWxlcy9ja3NvdXJjZS1zYW1wbGVzLWZyYW1ld29yay9jb21wb25lbnRzL2dsb2JhbC9nbG9iYWwubGVzcyIsIi4uLy4uL25vZGVfbW9kdWxlcy9ja3NvdXJjZS1zYW1wbGVzLWZyYW1ld29yay9jb21wb25lbnRzL2NvcmUvY29yZS5sZXNzIiwiLi4vLi4vbm9kZV9tb2R1bGVzL2Nrc291cmNlLXNhbXBsZXMtZnJhbWV3b3JrL2NvbXBvbmVudHMvZ3JpZC9ncmlkLmxlc3MiLCIuLi8uLi9ub2RlX21vZHVsZXMvY2tzb3VyY2Utc2FtcGxlcy1mcmFtZXdvcmsvbm9kZV9tb2R1bGVzL2xlc3NoYXQvYnVpbGQvbGVzc2hhdC5sZXNzIiwiLi4vLi4vbm9kZV9tb2R1bGVzL2Nrc291cmNlLXNhbXBsZXMtZnJhbWV3b3JrL2NvbXBvbmVudHMvaGVhZGVyLWEvaGVhZGVyLWEubGVzcyIsIi4uLy4uL25vZGVfbW9kdWxlcy9ja3NvdXJjZS1zYW1wbGVzLWZyYW1ld29yay9jb21wb25lbnRzL25hdmlnYXRpb24tYS9uYXZpZ2F0aW9uLWEubGVzcyIsIi4uLy4uL25vZGVfbW9kdWxlcy9ja3NvdXJjZS1zYW1wbGVzLWZyYW1ld29yay9jb21wb25lbnRzL25hdmlnYXRpb24tYi9uYXZpZ2F0aW9uLWIubGVzcyIsIi4uLy4uL25vZGVfbW9kdWxlcy9ja3NvdXJjZS1zYW1wbGVzLWZyYW1ld29yay9jb21wb25lbnRzL2Zvb3Rlci1hL2Zvb3Rlci1hLmxlc3MiLCIuLi8uLi9ub2RlX21vZHVsZXMvY2tzb3VyY2Utc2FtcGxlcy1mcmFtZXdvcmsvY29tcG9uZW50cy9jb250ZW50L2NvbnRlbnQubGVzcyIsIi4uLy4uL25vZGVfbW9kdWxlcy9ja3NvdXJjZS1zYW1wbGVzLWZyYW1ld29yay9jb21wb25lbnRzL2J1dHRvbi1hL2J1dHRvbi1hLmxlc3MiLCIuLi8uLi9ub2RlX21vZHVsZXMvY2tzb3VyY2Utc2FtcGxlcy1mcmFtZXdvcmsvY29tcG9uZW50cy9iYWxsb29uLWEvYmFsbG9vbi1hLmxlc3MiLCIuLi8uLi9ub2RlX21vZHVsZXMvY2tzb3VyY2Utc2FtcGxlcy1mcmFtZXdvcmsvY29tcG9uZW50cy9pY29uL2ljb24ubGVzcyIsIi4uLy4uL25vZGVfbW9kdWxlcy9ja3NvdXJjZS1zYW1wbGVzLWZyYW1ld29yay9jb21wb25lbnRzL3N3aXRjaC9zd2l0Y2gubGVzcyIsIi4uLy4uL25vZGVfbW9kdWxlcy9ja3NvdXJjZS1zYW1wbGVzLWZyYW1ld29yay9jb21wb25lbnRzL3RvZ2dsZXIvdG9nZ2xlci5sZXNzIiwiLi4vLi4vbm9kZV9tb2R1bGVzL2Nrc291cmNlLXNhbXBsZXMtZnJhbWV3b3JrL2NvbXBvbmVudHMvbW9kYWwvbW9kYWwubGVzcyIsIi4uLy4uL25vZGVfbW9kdWxlcy9ja3NvdXJjZS1zYW1wbGVzLWZyYW1ld29yay9jb21wb25lbnRzL2Jhc2ljc2FtcGxlL2NvcmUubGVzcyIsIi4uLy4uL25vZGVfbW9kdWxlcy9ja3NvdXJjZS1zYW1wbGVzLWZyYW1ld29yay9jb21wb25lbnRzL2Jhc2ljc2FtcGxlL2Fkam9pbmVkLmxlc3MiLCIuLi8uLi9zYW1wbGVzL2xlc3MvY3VzdG9tLmxlc3MiLCIuLi8uLi9zYW1wbGVzL3Rvb2xiYXJjb25maWd1cmF0b3IvbGVzcy90b29sYmFybW9kaWZpZXIubGVzcyIsIi4uLy4uL3NhbXBsZXMvdG9vbGJhcmNvbmZpZ3VyYXRvci9sZXNzL2Jhc2UubGVzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7O0FBc0RBLFFBSGlDO0VBeUNoQztJQUNDLHdCQUFBOzs7QUMxRkY7QUFBUztBQUFPO0FBQVM7QUFBWTtBQUFRO0FBQVE7QUFBUTtBQUFRO0FBQU07QUFBTTtBQUFLO0VBQ3JGLGNBQUE7O0FBR0Q7QUFBTTtFQUNMLFNBQUE7RUFDQSxVQUFBO0VBQ0Esd0JETitCLHVDQ00vQjtFQUNBLGdCQUFBO0VBQ0EsY0FBQTs7QUNIQSxZQUFZO0VBQ1gsVUFBQTs7QUFERCxZQUFZO0VBQ1gsVUFBQTs7QUFERCxZQUFZO0VBQ1gsVUFBQTs7QUFERCxZQUFZO0VBQ1gsVUFBQTs7QUFERCxZQUFZO0VBQ1gsVUFBQTs7QUFERCxZQUFZO0VBQ1gsVUFBQTs7QUFERCxZQUFZO0VBQ1gsVUFBQTs7QUFERCxZQUFZO0VBQ1gsVUFBQTs7QUFERCxZQUFZO0VBQ1gsVUFBQTs7QUFERCxZQUFZO0VBQ1gsV0FBQTs7QUY0Q0YsUUFIaUM7RUVqQ2hDO0VBS0MsWUFBWTtFQUFaLFlBQVk7RUFBWixZQUFZO0VBQVosWUFBWTtFQUFaLFlBQVk7RUFBWixZQUFZO0VBQVosWUFBWTtFQUFaLFlBQVk7RUFBWixZQUFZO0lBSlosV0FBQTs7O0FBYUYsQ0FBQztFQ3FSQyw4QkFBQTtFQUNBLDJCQUFBO0VBQ0Esc0JBQUE7RURyUkQsZ0JBQUE7RUFDQSxpQkFBQTtFQUNBLFdBQUE7O0FBSUEsQ0FEQSxxQkFDQztBQUFELGVBQUM7QUFBUSxDQURULHFCQUNVO0FBQUQsZUFBQztFQUNULFNBQVMsRUFBVDtFQUNBLGNBQUE7RUFDQSxnQkFBQTtFQUNBLGtCQUFBO0VBQ0EsWUFBQTtFQUNBLGNBQUE7RUFDQSxRQUFBO0VBQ0EsU0FBQTs7QUFLRCxDQURBLHFCQUNDO0FBQUQsZUFBQztFQUNBLFdBQUE7O0FBSUY7RUMyUEUsOEJBQUE7RUFDQSwyQkFBQTtFQUNBLHNCQUFBO0VEM1BELGlCQUFBO0VBQ0Esa0JBQUE7O0FBS0Msc0JBREQsRUFBQyxxQkFDQztFQUNBLGVBQUE7O0FBR0Qsc0JBTEQsRUFBQyxxQkFLQztFQUNBLGdCQUFBOztBRmpCSCxRQUhpQztFRTBCOUIsc0JBREQsRUFBQyxxQkFDQztJQUNBLGdCQUFBOztFQUdELHNCQUxELEVBQUMscUJBS0M7SUFDQSxpQkFBQTs7O0FFN0VKO0VBQ0MsaUJBQUE7RUFHQSxnQkFBQTs7QUFKRCxTQU1DO0VBQ0MsZ0JBQUE7O0FKMENGLFFBSGlDO0VBNkNqQyxTSXJGQztJQUlFLGtCQUFBOzs7QUFWSCxTQU1DLGVBT0M7RUFDQyxtQkFBQTs7QUNWSDtFQUNDLFlBQUE7RUFDQSxtQkFBQTtFQUNBLGtCQUFBO0VBQ0EsT0FBQTtFQUNBLFFBQUE7RUFDQSxNQUFBO0VBQ0EsVUFBQTtFQUNBLGdCQUFBOztBTHFDRCxRQUhpQztFQTZDakM7SUs1RUUsa0JBQUE7OztBQVhGLGFBY0M7RUFDQyxnQkFBQTtFQUNBLFNBQUE7RUFDQSxnQkFBQTs7QUFqQkYsYUFjQyxHQUtDO0FBbkJGLGFBY0MsR0FLSyxHQUFHO0VBQ04scUJBQUE7O0FMeUJILFFBSGlDO0VBNkNqQyxhS3pFQztJQVVFLFdBQUE7SUFDQSx1QkFBQTtJQUNBLG1CQUFBO0lBQ0EscUJBQUE7SUFDQSxXQUFBOztFQUVBLGFBaEJGLEdBZ0JHO0VBQVMsYUFoQlosR0FnQmE7SUFDVixhQUFBOzs7QUFLRCxhQXRCRixHQXFCRSxhQUNDO0VBQ0EsZ0JBQUE7O0FMUUosUUFIaUM7RUE2Q2pDLGFLekVDLEdBcUJFLGFBQ0M7SUFJQyxnQkFBQTs7O0FBSUYsYUE5QkYsR0FxQkUsYUFTQztFQUNBLGlCQUFBOztBTEFKLFFBSGlDO0VBNkNqQyxhS3pFQyxHQXFCRSxhQVNDO0lBSUMsa0JBQUE7OztBQU1GLGFBeENGLEdBdUNDLEdBQ0c7RUFDRCxpQkFBQTs7QUF2REosYUFjQyxHQXVDQyxHQUtDO0VMeENGLGVBQUE7RUFDQSxtQkFBQTtFQUNBLGlCQUFBO0VBQ0Esb0JBQUE7RUt1Q0csaUJBQUE7RUFDQSxXQUFBO0VBQ0EsV0FBQTtFQUNBLGlCQUFBO0VBQ0EscUJBQUE7RUFDQSx5QkFBQTs7QUFFQSxhQXJESCxHQXVDQyxHQUtDLEVBU0U7RUFDQSxlQUFBO0VBQ0EsV0FBQTs7QUFRSix5QkFBQztBQUFTLHlCQUFDO0VBQ1Ysc0JBQWtCLHFyQkFBbEI7O0FDcEZGO0VBQ0MsaUJBQUE7RUFDQSxnQkFBQTtFQUNBLGlCQUFBOztBTmdERCxRQUhpQztFQTZDakM7SU12RkUsa0JBQUE7SUFDQSxnQkFBQTtJQUdBLFVBQUE7OztBQVZGLGFBYUM7RUFDQyxVQUFBO0VBQ0EsZ0JBQUE7RUFDQSxTQUFBO0VBQ0EsaUJBQUE7O0FBakJGLGFBYUMsR0FNQztBQW5CRixhQWFDLEdBTUssR0FBRztFQUNOLHFCQUFBOztBTitCSCxRQUhpQztFQTZDakMsYU1oRkM7SUFXRSxjQUFBO0lBQ0EsV0FBQTtJQUNBLHFCQUFBOzs7QU55QkgsUUFIaUM7RUE2Q2pDLGFNaEZDLEdBZ0JDO0lBRUUsa0JBQUE7OztBQUdELGFBckJGLEdBZ0JDLEdBS0c7RUFDRCxpQkFBQTs7QU5nQkosUUFIaUM7RUE2Q2pDLGFNaEZDLEdBZ0JDLEdBS0c7SUFJQSxjQUFBOzs7QUF0Q0wsYUFhQyxHQWdCQyxHQWFDO0VId1FELDhCQUFBO0VBQ0EsMkJBQUE7RUFDQSxzQkFBQTtFR3hRRSx5QkFBQTtFQUNBLHFCQUFBO0VBQ0EsYUFBQTs7QU5LSixRQUhpQztFQTZDakMsYU1oRkMsR0FnQkMsR0FhQztJQU9FLFdBQUE7SUhxT0gsd0JBQUE7SUFBaUMsb0NBQUE7SUFDakMscUJBQUE7SUFBOEIsNkJBQUE7SUFDOUIsZ0JBQUE7SUFBeUIsNEJBQUE7OztBSXhSM0I7RVB3QkMsZUFBQTtFQUNBLG9CQUFBO0VBQ0EsbUJBQUE7RUFDQSxvQkFBQTtFT3hCQSxtQkFBQTtFQUNBLHNCQUFBO0VBQ0EsZ0JBQUE7RUFDQSxjQUFBOztBQU5ELFNQNEVDO0VBQ0MsY0FBQTtFQUNBLHFCQUFBO0VBRUEsaUNBQUE7O0FBRUEsU0FORCxFQU1FO0VBQ0EsY0FBQTs7QU9uRkgsU0FRQztFQUNDLFNBQUE7RUFDQSxxQkFBQTtFQUNBLGtCQUFBOztBQ1hGO0VSd0JDLGVBQUE7RUFDQSxtQkFBQTtFQUNBLG1CQUFBO0VBQ0Esb0JBQUE7RVF6QkEsZ0JBQUE7RUFDQSxrQkFBQTtFQUNBLHFCQUFBOztBQUpELFFBU0M7RUFDQyxnQkFBQTs7QUFWRixRQWFDO0FBYkQsUUFhSztBQWJMLFFBYVM7QUFiVCxRQWFjO0FBYmQsUUFhMEIsU0FBUSxJQUFJO0FBYnRDLFFBYXdEO0VBQ3RELGlCQUFBOztBQWRGLFFBaUJDO0FBakJELFFBaUJPO0VMcVFMLDBCQUFBO0VBQWlDLG9DQUFBO0VBQ2pDLHVCQUFBO0VBQThCLDZCQUFBO0VBQzlCLGtCQUFBO0VBQXlCLDRCQUFBO0VLclF6QixnQkFBQTs7QUFuQkYsUUFzQkM7QUF0QkQsUUFzQk07QUF0Qk4sUUFzQlk7QUF0QlosUUFzQmlCO0VBQ2YsbUJBQUE7O0FBdkJGLFFBMEJDO0FBMUJELFFBMEJhO0VBQ1gsZ0JBQUE7RUFDQSw4QkFBQTtFQUNBLHFCQUFBOztBQTdCRixRQW9DQyxFUndDQTtBUTVFRCxRQW9DSSxHUndDSDtBUTVFRCxRQW9DUSxHUndDUDtBUTVFRCxRQW9DWSxXUndDWDtBUTVFRCxRQW9Dd0IsR1J3Q3ZCO0FRNUVELFFBb0M0QixHUndDM0I7QVE1RUQsUUFvQ2dDLEdSd0MvQjtBUTVFRCxRQW9Db0MsR1J3Q25DO0FRNUVELFFBb0N3QyxHUndDdkM7RUFDQyxjQUFBO0VBQ0EscUJBQUE7RUFFQSxpQ0FBQTs7QUFFQSxRUTlDRCxFUndDQSxFQU1FO0FBQUQsUVE5Q0UsR1J3Q0gsRUFNRTtBQUFELFFROUNNLEdSd0NQLEVBTUU7QUFBRCxRUTlDVSxXUndDWCxFQU1FO0FBQUQsUVE5Q3NCLEdSd0N2QixFQU1FO0FBQUQsUVE5QzBCLEdSd0MzQixFQU1FO0FBQUQsUVE5QzhCLEdSd0MvQixFQU1FO0FBQUQsUVE5Q2tDLEdSd0NuQyxFQU1FO0FBQUQsUVE5Q3NDLEdSd0N2QyxFQU1FO0VBQ0EsY0FBQTs7QVFuRkgsUUF3Q0M7QUF4Q0QsUUF3Q0s7QUF4Q0wsUUF3Q1M7QUF4Q1QsUUF3Q2E7QUF4Q2IsUUF3Q2lCO0VBQ2YsV0FBQTtFQUNBLGdCQUFBOztBQTFDRixRQXdDQyxHQUtDO0FBN0NGLFFBd0NLLEdBS0g7QUE3Q0YsUUF3Q1MsR0FLUDtBQTdDRixRQXdDYSxHQUtYO0FBN0NGLFFBd0NpQixHQUtmO0FBN0NGLFFBd0NDLEdBS087QUE3Q1IsUUF3Q0ssR0FLRztBQTdDUixRQXdDUyxHQUtEO0FBN0NSLFFBd0NhLEdBS0w7QUE3Q1IsUUF3Q2lCLEdBS1Q7RUFDTCxrQkFBQTs7QUE5Q0gsUUF3Q0MsR0FVQyxFQUFDO0FBbERILFFBd0NLLEdBVUgsRUFBQztBQWxESCxRQXdDUyxHQVVQLEVBQUM7QUFsREgsUUF3Q2EsR0FVWCxFQUFDO0FBbERILFFBd0NpQixHQVVmLEVBQUM7RUFDQSxnQkFBQTtFQUNBLHNCQUFBO0VBQ0EsVUFBQTtFQUNBLFNBQUE7O0FBR0QsUUFqQkQsR0FpQkUsTUFDQSxFQUFDO0FBREYsUUFqQkcsR0FpQkYsTUFDQSxFQUFDO0FBREYsUUFqQk8sR0FpQk4sTUFDQSxFQUFDO0FBREYsUUFqQlcsR0FpQlYsTUFDQSxFQUFDO0FBREYsUUFqQmUsR0FpQmQsTUFDQSxFQUFDO0VBQ0EsVUFBQTs7QUFJRixRQXZCRCxHQXVCRSxPQUNBO0FBREQsUUF2QkcsR0F1QkYsT0FDQTtBQURELFFBdkJPLEdBdUJOLE9BQ0E7QUFERCxRQXZCVyxHQXVCVixPQUNBO0FBREQsUUF2QmUsR0F1QmQsT0FDQTtFTDhERCwwREFBQTtFQUNBLHVEQUFBO0VBQ0EscURBQUE7RUFDQSxrREFBQTtFSy9ERSxVQUFBOztBQWxFSixRQXVFQztBQXZFRCxRQXVFUTtBQXZFUixRQXVFZ0IsU0FBUSxJQUFJO0VMK00xQiwwQkFBQTtFQUFpQyxvQ0FBQTtFQUNqQyx1QkFBQTtFQUE4Qiw2QkFBQTtFQUM5QixrQkFBQTtFQUF5Qiw0QkFBQTtFQW1CekIsdURBQUE7RUFDQSxvREFBQTtFQUNBLCtDQUFBO0VLbE9BLGFBQUE7RUFDQSxjQUFBO0VBRUEseUJBQUE7RUFDQSxrQkFBQTs7QUFFQSxRQVZELE1BVUU7QUFBRCxRQVZNLE9BVUw7QUFBRCxRQVZjLFNBQVEsSUFBSSxnQkFVekI7RUFDQSxxQkFBQTtFQUNBLFVBQUE7RUx3TkQsd0VBQUE7RUFDQSxxRUFBQTtFQUNBLGdFQUFBOztBSzdTRixRQThGQztFQUNDLDhCQUFBO0VBQ0EsZUFBQTs7QUFoR0YsUUFtR0M7RUFDQyxrQkFBQTtFQUNBLDZCUm5HMkMsd0JRbUczQztFUjdFRCxlQUFBO0VBQ0EsZUFBQTtFQUNBLG1CQUFBO0VBQ0EsbUJBQUE7O0FRM0JELFFBeUdDO0VBQ0Msa0JBQUE7O0FBMUdGLFFBNkdDO0VSckZBLGVBQUE7RUFDQSxrQkFBQTtFQUNBLG1CQUFBO0VBQ0Esb0JBQUE7RVFvRkMsbUJBQUE7O0FBL0dGLFFBa0hDO0VSMUZBLGlCQUFBO0VBQ0EsaUJBQUE7RUFDQSxvQkFBQTtFQUNBLG9CQUFBO0VReUZDLGlCQUFBOztBQXBIRixRQXVIQztFUi9GQSxlQUFBO0VBQ0EsaUJBQUE7RUFDQSxtQkFBQTtFQUNBLG1CQUFBO0VROEZDLGdCQUFBO0VBQ0Esa0JBQUE7O0FBMUhGLFFBNkhDO0VSckdBLGlCQUFBO0VBQ0EsaUJBQUE7RUFDQSxvQkFBQTtFQUNBLG9CQUFBO0VRb0dDLGdCQUFBO0VBQ0Esa0JBQUE7O0FBaElGLFFBbUlDO0VSM0dBLGlCQUFBO0VBQ0EsaUJBQUE7RUFDQSxvQkFBQTtFQUNBLG9CQUFBO0VRMEdDLGdCQUFBO0VBQ0Esa0JBQUE7O0FBdElGLFFBeUlDO0VBQ0MsU0FBQTtFQUNBLDZCQUFBO0VBQ0EsZUFBQTs7QUFJQSxRQURELE1BQ0U7RUFDQSxhQUFBO0VBQ0Esa0JBQUE7O0FBR0QsUUFORCxNQU1FO0VMaURELDBCQUFBO0VBQ0EsdUJBQUE7RUFDQSxrQkFBQTs7QUt4TUYsUUE0SkM7RVJwSUEsZUFBQTtFQUNBLGtCQUFBO0VBQ0EsbUJBQUE7RUFDQSxvQkFBQTtFUW1JQyxvQlI3SjhCLHVDUTZKOUI7RUFDQSxnQkFBQTtFTDRJQSx1REFBQTtFQUNBLG9EQUFBO0VBQ0EsK0NBQUE7O0FLN1NGLFFBdUtDLEVBQ0M7RUFDQyxzQkFBQTs7QUF6S0gsUUF1S0MsRUFLQztFQUNDLGNBQUE7O0FBN0tILFFBaUxDO0VBQ0MsVUFBQTtFQUNBLFNBQUE7RUFFQSxXQUFBO0VBQ0EsY0FBQTtFQUNBLGdCQUFBOztBQXZMRixRQTBMQztBQTFMRCxRQTBMTTtFUmxLTCxrQkFBQTtFQUNBLG1CQUFBO0VBQ0EsbUJBQUE7RUFDQSxvQkFBQTtFUWtLQyxnSkFBQTs7QUE3TEYsUUEwTEMsSUFLQztBQS9MRixRQTBMTSxLQUtKO0VBQ0MsU0FBQTs7QUFoTUgsUUFxTUMsSUFBSTtFQUNILGVBQUE7RUFDQSxjQUFBOztBQXZNRixRQTBNQztFQUNDLFdBQUE7O0FBM01GLFFBOE1DLEdBRUM7QUFoTkYsUUE4TUssR0FFSDtBQWhORixRQThNQyxHQUVLO0FBaE5OLFFBOE1LLEdBRUM7RUFDSCxnQkFBQTs7QUFqTkgsUUE4TUMsR0FNQztBQXBORixRQThNSyxHQU1IO0VSNUxELGVBQUE7RUFDQSxtQkFBQTtFQUNBLG9CQUFBO0VBQ0Esb0JBQUE7O0FRM0JELFFBME5DLFNBQVEsSUFBSTtFQUNYLFdBQUE7O0FBM05GLFFBOE5DLElBQUc7RUFDRix1QkFBQTtFQUNBLGFBQUE7RUFDQSxxQkFBQTs7O0FBR0EsUUFORCxJQUFHLEtBTUQ7RUFDQSxTQUFTLE1BQVQ7RUFDQSxpQkFBQTs7QUNqT0QsSUFERCxFQUNFO0FBQUQsSUFERSxPQUNEO0FBQUQsSUFEVSxNQUNUO0VOaVJELDBCQUFBO0VBQWlDLG9DQUFBO0VBQ2pDLHVCQUFBO0VBQThCLDZCQUFBO0VBQzlCLGtCQUFBO0VBQXlCLDRCQUFBO0VIaFExQixlQUFBO0VBQ0EsbUJBQUE7RUFDQSxtQkFBQTtFQUNBLG9CQUFBO0VTbkJFLFlBQUE7RUFDQSxpQkFBQTtFQUNBLGdCQUFBO0VBQ0EsZ0JBQUE7RUFDQSxjQUFBO0VBQ0EsbUJBQUE7RUFDQSxxQkFBQTtFQUNBLHFCQUFBO0VBQ0EsZUFBQTtFQUNBLFNBQUE7RUFDQSxzQkFBQTtFQUlBLGFBQUE7RUFHQSx1QkFBQTs7QUFFQSxJQXZCRixFQUNFLFNBc0JDO0FBQUQsSUF2QkMsT0FDRCxTQXNCQztBQUFELElBdkJTLE1BQ1QsU0FzQkM7RUFDQSxrQkFBQTs7QUFHRCxJQTNCRixFQUNFLFNBMEJDO0FBQUQsSUEzQkMsT0FDRCxTQTBCQztBQUFELElBM0JTLE1BQ1QsU0EwQkM7RUFDQSxtQkFBQTs7QUFvQkQsSUFoREYsRUFDRSxTQStDQztBQUFELElBaERDLE9BQ0QsU0ErQ0M7QUFBRCxJQWhEUyxNQUNULFNBK0NDO0VOa09GLDRCQUFBO0VBQWlDLG9DQUFBO0VBQ2pDLHlCQUFBO0VBQThCLDZCQUFBO0VBQzlCLG9CQUFBO0VBQXlCLDRCQUFBO0VNblB2QixXQUFBO0VBQ0EsVUFBQTtFQUNBLG1CQUFBO0VBQ0EsZ0JBQUE7RUFDQSxrQkFBQTtFQUNBLGtCQUFBOztBQUVBLElBeENILEVBQ0UsU0ErQ0MsaUJBUkM7QUFBRCxJQXhDQSxPQUNELFNBK0NDLGlCQVJDO0FBQUQsSUF4Q1EsTUFDVCxTQStDQyxpQkFSQztFQUNBLGtCQUFBO0VBQ0EsU0FBQTtFQUNBLFFBQUE7RUFDQSxxQkFBQTs7QVRHTCxRQUhpQztFQTZDakMsSVN6RkMsRUFDRSxTQW1EQztFVHFDSixJU3pGSSxPQUNELFNBbURDO0VUcUNKLElTekZZLE1BQ1QsU0FtREM7SU44TkYsNEJBQUE7SUFBaUMsb0NBQUE7SUFDakMseUJBQUE7SUFBOEIsNkJBQUE7SUFDOUIsb0JBQUE7SUFBeUIsNEJBQUE7SU1uUHZCLFdBQUE7SUFDQSxVQUFBO0lBQ0EsbUJBQUE7SUFDQSxnQkFBQTtJQUNBLGtCQUFBO0lBQ0Esa0JBQUE7O0VBRUEsSUF4Q0gsRUFDRSxTQW1EQywwQkFaQztFQUFELElBeENBLE9BQ0QsU0FtREMsMEJBWkM7RUFBRCxJQXhDUSxNQUNULFNBbURDLDBCQVpDO0lBQ0Esa0JBQUE7SUFDQSxTQUFBO0lBQ0EsUUFBQTtJQUNBLHFCQUFBOztFQUpELElBeENILEVBQ0UsU0FtREMsMEJBWkM7RUFBRCxJQXhDQSxPQUNELFNBbURDLDBCQVpDO0VBQUQsSUF4Q1EsTUFDVCxTQW1EQywwQkFaQztJQUNBLGtCQUFBO0lBQ0EsU0FBQTtJQUNBLFFBQUE7SUFDQSxxQkFBQTs7O0FBY0YsSUExREYsRUFDRSxTQXlEQztBQUFELElBMURDLE9BQ0QsU0F5REM7QUFBRCxJQTFEUyxNQUNULFNBeURDO0FBQ0QsSUEzREYsRUFDRSxTQTBEQztBQUFELElBM0RDLE9BQ0QsU0EwREM7QUFBRCxJQTNEUyxNQUNULFNBMERDO0VBQ0EsV0FBQTtFQUNBLG1CQUFBOztBQUdELElBaEVGLEVBQ0UsU0ErREM7QUFBRCxJQWhFQyxPQUNELFNBK0RDO0FBQUQsSUFoRVMsTUFDVCxTQStEQztFQUNBLHFCQUFBO0VBQ0EsVUFBQTtFTnFPRix5RUFBQTtFQUNBLHNFQUFBO0VBQ0EsaUVBQUE7O0FNNU5BLElBN0VELEVBNkVFO0FBQUQsSUE3RUUsT0E2RUQ7QUFBRCxJQTdFVSxNQTZFVDtFQUNBLG1CQUFBOztBQUVBLElBaEZGLEVBNkVFLGNBR0M7QUFBRCxJQWhGQyxPQTZFRCxjQUdDO0FBQUQsSUFoRlMsTUE2RVQsY0FHQztBQUNELElBakZGLEVBNkVFLGNBSUM7QUFBRCxJQWpGQyxPQTZFRCxjQUlDO0FBQUQsSUFqRlMsTUE2RVQsY0FJQztFQUNBLGNBQUE7RUFDQSxtQkFBQTs7QUFJRixJQXZGRCxFQXVGRTtBQUFELElBdkZFLE9BdUZEO0FBQUQsSUF2RlUsTUF1RlQ7QUFBRCxJQXZGRCxFSGlERyxhQXhDSCxHQWdCQyxHQWFDLEVBV0U7QUdzQ0gsSUF2RkUsT0hpREEsYUF4Q0gsR0FnQkMsR0FhQyxFQVdFO0FHc0NILElBdkZVLE1IaURSLGFBeENILEdBZ0JDLEdBYUMsRUFXRTtFR3VDRixXQUFBO0VBQ0EsbUJBQUE7O0FBRUEsSUEzRkYsRUF1RkUsb0JBSUM7QUFBRCxJQTNGQyxPQXVGRCxvQkFJQztBQUFELElBM0ZTLE1BdUZULG9CQUlDO0FBQ0QsSUE1RkYsRUF1RkUsb0JBS0M7QUFBRCxJQTVGQyxPQXVGRCxvQkFLQztBQUFELElBNUZTLE1BdUZULG9CQUtDO0FBREQsSUEzRkYsRUhpREcsYUF4Q0gsR0FnQkMsR0FhQyxFQVdFLE1HMENEO0FBQUQsSUEzRkMsT0hpREEsYUF4Q0gsR0FnQkMsR0FhQyxFQVdFLE1HMENEO0FBQUQsSUEzRlMsTUhpRFIsYUF4Q0gsR0FnQkMsR0FhQyxFQVdFLE1HMENEO0FBQ0QsSUE1RkYsRUhpREcsYUF4Q0gsR0FnQkMsR0FhQyxFQVdFLE1HMkNEO0FBQUQsSUE1RkMsT0hpREEsYUF4Q0gsR0FnQkMsR0FhQyxFQVdFLE1HMkNEO0FBQUQsSUE1RlMsTUhpRFIsYUF4Q0gsR0FnQkMsR0FhQyxFQVdFLE1HMkNEO0VBQ0EsV0FBQTtFQUNBLG1CQUFBOztBQ2hHSjtFVnNCQyxlQUFBO0VBQ0Esa0JBQUE7RUFDQSxtQkFBQTtFQUNBLG9CQUFBO0VHMlBDLDBCQUFBO0VBQWlDLG9DQUFBO0VBQ2pDLHVCQUFBO0VBQThCLDZCQUFBO0VBQzlCLGtCQUFBO0VBQXlCLDRCQUFBO0VPblIxQixnQ0FBQTtFQUVBLG1CQUFBO0VBQ0EscUJBQUE7RUFDQSxtQkFBQTtFQUNBLHdCQUFBO0VBQ0EsZ0JBQUE7RUFDQSxrQkFBQTtFQUNBLGFBQUE7RUFDQSxvQkFBQTtFQUNBLGNBQUE7O0FBRUEsVUFBQztFQUNBLGNBQUE7O0FBR0QsVUFBQztFQUNBLFNBQVMsRUFBVDtFQUNBLFFBQUE7RUFDQSxTQUFBO0VBQ0EsbUJBQUE7RUFDQSxrQkFBQTs7QUFNRCxhQUFDO0FBQUQsYUFBQztFQUNBLFVBQUE7RUFDQSw4QkFBQTtFQUNBLHlEQUFBOztBQU1ELGFBQUM7QUFBRCxhQUFDO0VBQ0EsYUFBQTtFQUNBLDhCQUFBO0VBQ0EseURBQUE7O0FBTUQsYUFBQztBQUFELGFBQUM7RUFDQSxVQUFBOztBQU1ELGFBQUM7QUFBRCxhQUFDO0VBQ0EsV0FBQTs7QUN2REYsY0FBYztBQUNkLGVBQWU7RUFDZCxTQUFTLEVBQVQ7RUFDQSxxQkFBQTtFQUNBLFdBQUE7RUFDQSxZQUFBO0VBQ0Esc0JBQUE7RUFDQSw0QkFBQTs7QUFHRCxjQUFjO0VBQ2Isa0JBQUE7O0FBR0QsZUFBZTtFQUNkLGlCQUFBOztBQUlBLGNBQUM7QUFBUyxjQUFDO0VBQ1Ysc0JBQWtCLDZjQUFsQjs7QUFLRCxtQkFBQztBQUFTLG1CQUFDO0VBQ1Ysc0JBQWtCLDZpQkFBbEI7O0FBS0QsV0FBQztBQUFTLFdBQUM7RUFDVixzQkFBa0IsNmlCQUFsQjs7QUM1QkYsSUFBSyxRQUVKO0VBQ0Msc0JBQUE7O0FBSEYsSUFBSyxRQU1KLE1BQUs7RUFDSixnQkFBQTtFQUNBLHFCQUFBOztBQVJGLElBQUssUUFXSjtFQUNDLHlCQUFBO0VBQ0EsMEJBQUE7O0FBRUEsSUFmRyxRQVdKLE1BSUU7RUFDQSxXQUFBOztBQUdELElBbkJHLFFBV0osTUFRRTtFQUNBLFlBQUE7O0FBcEJILElBQUssUUF3Qko7RUFDQyxhQUFBOztBQUlGO0VaWkMsZUFBQTtFQUNBLG1CQUFBO0VBQ0EsbUJBQUE7RUFDQSxvQkFBQTtFWVdBLGlCQUFBO0VBQ0EseUJBQUE7RUFDQSxnQkFBQTtFQUNBLHFCQUFBO0VBQ0Esc0JBQUE7RUFDQSxXQUFBO0VUMk9DLDBCQUFBO0VBQWlDLG9DQUFBO0VBQ2pDLHVCQUFBO0VBQThCLDZCQUFBO0VBQzlCLGtCQUFBO0VBQXlCLDRCQUFBO0VTM08xQixrQkFBQTs7QUFURCxPQVdDLE1BQUs7RUFDSixhQUFBOztBQVpGLE9BZUM7RUFDQyxrQkFBQTtFQUNBLFVBQUE7RUFDQSxXQUFBO0VBQ0EsZUFBQTtFQUNBLGlCQUFBOztBQUVBLE9BUEQsTUFPRTtFQUNBLDBCQUFBOztBQXZCSCxPQTJCQztFQUNDLFdBQUE7RUFDQSxzQkFBQTtFQUNBLGFBQUE7RUFDQSxjQUFBO0VBQ0EsWUFBQTtFQUNBLGdCQUFBO0VUaU5BLDRCQUFBO0VBQWlDLG9DQUFBO0VBQ2pDLHlCQUFBO0VBQThCLDZCQUFBO0VBQzlCLG9CQUFBO0VBQXlCLDRCQUFBOztBU3BQM0IsT0EyQkMsY0FTQztFQUNDLGdCQUFBO0VBQ0Esa0JBQUE7RUFDQSxjQUFBO0VBQ0EsYUFBQTtFQUNBLFlBQUE7RUFDQSxtQkFBQTtFVHdNRCw0QkFBQTtFQUFpQyxvQ0FBQTtFQUNqQyx5QkFBQTtFQUE4Qiw2QkFBQTtFQUM5QixvQkFBQTtFQUF5Qiw0QkFBQTs7QVN2TXhCLE9BbEJGLGNBU0MsU0FTRTtFQUNBLFNBQVMsRUFBVDtFQUNBLGNBQUE7RUFDQSxrQkFBQTtFQUNBLE1BQUE7RUFDQSxRQUFBO0VBQ0EsV0FBQTtFQUNBLE9BQUE7RUFFQSx5QkFBQTtFVHNLRix3Q0FBQTtFQUNBLG9DQUFBO0VBQ0EsZ0NBQUE7RUFLQSx5Q0FBQTtFQUE4QyxvQ0FBQTtFQUM5QyxxQ0FBQTtFQUEwQyw2QkFBQTtFQUMxQyxpQ0FBQTtFQUFzQyw0QkFBQTs7QVN2S3ZDLE9BQUMsTUFDQSxjQUFjLFNBQVE7RUFDckIsbUJBQUE7O0FBaEVILE9Bb0VDLE1BQUssY0FBZ0IsUUFFcEIsZ0JBQWdCO0VBQ2YsaUJBQUE7O0FBdkVILE9Bb0VDLE1BQUssY0FBZ0IsUUFTcEIsUUFBTztFQUNOLHNCQUFBO0VBQ0Esc0JBQUE7O0FBL0VILE9BbUZDLE1BQUssY0FBZ0IsUUFBUyxRQUFPO0VBQ3BDLHFCQUFBO0VBQ0EscUJBQUE7O0FDekhGO0VWazNCRSx5QkFBQTtFQUNBLHNCQUFBO0VBQ0EscUJBQUE7RUFDQSxpQkFBQTs7QVVyM0JGLFFBR0M7RUFDQyxlQUFBOztBQUpGLFFBTUM7RUFDQyxnQkFBQTs7QUFQRixRQVVDO0VBQ0MsYUFBQTs7QUFHRCxRQUFDLFVBQ0E7RUFDQyxhQUFBOztBQUZGLFFBQUMsVUFLQTtFQUNDLGdCQUFBOztBQUtIO0VBQ0MsZ0JBQUE7O0FBRUEsa0JBQUM7RUFDQSxTQUFBOztBQU1ELHNCQUFDO0FBQUQsdUJBQUM7QUFBUyxzQkFBQztBQUFELHVCQUFDO0VBQ1Ysc0JBQWtCLHlzQkFBbEI7O0FBSUEsc0JBREEsV0FDQztBQUFELHVCQURBLFdBQ0M7QUFBUyxzQkFEVixXQUNXO0FBQUQsdUJBRFYsV0FDVztFQUNWLHNCQUFrQixxdEJBQWxCOztBQU1GLHNCQUFDO0FBQ0Qsc0JBQUM7RUFDQSw2QkFBQTs7QUFLRCx1QkFBQztBQUNELHVCQUFDO0VBQ0EsZ0NBQUE7O0FDdERGO0VBQ0MsYUFBQTtFQUNBLGtCQUFBO0VBQ0EsdUJBQUE7RUFDQSxnQkFBQTtFWDRTQyw4QkFBQTtFQUNBLDJCQUFBO0VBQ0Esc0JBQUE7RVd6U0QscUJBQUE7RUFDQSxtQkFBQTtFWGd2QkMsd0NBQUE7RUFDQSxxQ0FBQTtFQUNBLG1DQUFBO0VBQ0Esb0NBQUE7RUFDQSxnQ0FBQTs7QVdqdkJELE1BQUM7RVh1UUEsNEJBQUE7RUFBaUMsb0NBQUE7RUFDakMseUJBQUE7RUFBOEIsNkJBQUE7RUFDOUIsb0JBQUE7RUFBeUIsNEJBQUE7RVd2UXpCLGVBQUE7RUFDQSxZQUFBO0VBQ0EsV0FBQTtFQUNBLGtCQUFBO0VBQ0EsU0FBQTtFQUNBLFdBQUE7RUFDQSxlQUFBO0VBQ0Esa0JBQUE7RUFDQSxpQkFBQTtFQUNBLG1CQUFBOztBQ3pCRixJQUFLO0FBQ0wsTUFBTztBQUNQLGFBQWM7QUFDZCxNQUFPO0VBQ04sZ0JBQUE7O0FBSUQ7RUFDQyxnQkFBQTs7QUFHRDtFQUNDLDZCQUFBOztBQ1hBLFNBQUM7RUFDQSx5QkFBQTtFQUNBLFdBQUE7O0FBRkQsU0FBQyxJQUlBLFNBQ0M7QUFMRixTQUFDLElBSUEsU0FDSztBQUxOLFNBQUMsSUFJQSxTQUNTO0FBTFYsU0FBQyxJQUlBLFNBQ2E7QUFMZCxTQUFDLElBSUEsU0FDaUI7RUFDZixXQUFBOztBQU5ILFNBQUMsSUFJQSxTQUtDO0VoQllGLGVBQUE7RUFDQSxtQkFBQTtFQUNBLG1CQUFBO0VBQ0Esb0JBQUE7RWdCYkcsZ0JBQUE7O0FBWEgsU0FBQyxJQUlBLFNBS0MsRUFJQztFQUNDLHFCQUFBO0VBQ0EsOEJBQUE7RUFDQSxjQUFBOztBQUVBLFNBbEJILElBSUEsU0FLQyxFQUlDLEVBS0U7RUFDQSxjQUFBOztBQW5CTCxTQUFDLElBSUEsU0FvQkM7RUFDQyxXQUFBOztBQXpCSCxTQUFDLElBSUEsU0F3QkM7RUFDQyxXQUFBOztBQTdCSCxTQUFDLElBSUEsU0E0QkM7RUFDQyxrQkFBQTtFQUNBLGNBQUE7O0FBS0gsU0FBQztFQUNBLGtCQUFBOztBQUVBLFNBSEEsT0FHQztFQUNBLFdBQUE7RUFDQSxTQUFTLEVBQVQ7RUFDQSxtQkFBQTtFQUNBLGtCQUFBO0VBQ0EsTUFBQTtFQUNBLE9BQUE7RUFDQSxRQUFBO0VBQ0EsV0FBQTs7QUN4REgsSUFBSztBQUNMLE1BQU87QUFDUCxhQUFjO0FBQ2QsTUFBTztFQUNOLGlCQUFBOztBQUdELElBQUssZ0JBQWU7RUFDbkIsZUFBQTs7QUFHRDtFQUNDLG1CQUFBO0VBQ0EsWUFBQTtFQUNBLGlCQUFBOztBQUhELE9BTUM7RUFFQyxrQkFBQTtFQUNBLFVBQUE7RUFDQSxVQUFBO0VBQ0EsbUJBQUE7RUFHQSxtQkFBQTtFQUNBLDRCQUFBOztBQUVBLE9BWEQsV0FXRTtFQUNBLHlEQUFBOztBQUtILFFBQVM7RUFDUixnQkFBQTtFQUNBLFNBQUE7RUFDQSx3QkFBQTs7QUFFQSxRQUxRLG1CQUtQO0VBQ0EsWUFBQTs7QUFLRixLQUFNO0VBQ0wsZ0JBQUE7RUFDQSxjQUFBO0VBQ0EsMEJBQUE7O0FBR0QsS0FBTSxjQUFhO0VBQ2xCLGFBQUE7O0FBSUEsUUFEUSxjQUNQO0VBQ0EsYUFBQTs7QUFGRixRQUFTLGNBS1I7RUFDQyxhQUFBOztBQUlGLFFBQ0M7RUFDQyxpQkFBQTs7QUFJRjtFQUNDLGlCQUFBOztBQURELFNBR0M7RUFDQyxXQUFBO0VBQ0EsaUJBQUE7O0FBTEYsU0FHQyxNQUlDO0VBQ0MsZ0JBQUE7O0FBRUEsU0FQRixNQUlDLFNBR0U7RUFDQSx1QkFBQTtFQUNBLFdBQUE7O0FBRkQsU0FQRixNQUlDLFNBR0UsTUFJQTtFQUNDLDBCQUFBOztBQWZMLFNBR0MsTUFpQkM7RUFDQyxXQUFBO0VBQ0EsYUFBQTs7QUFHRCxTQXRCRCxNQXNCRTtFQUNBLGdCQUFBOztBQUtIO0VBQ0MsZ0JBQUE7RUFDQSxTQUFBO0VBQ0EsVUFBQTtFQUNBLHdCQUFBOztBQUVBLHVCQUFDO0VBQ0EsWUFBQTtFQUNBLFVBQUE7O0FBS0Y7RUFDQyxpQkFBQTs7QUFERCxNQUdDLElBQUc7RUFDRixpQkFBQTs7QUFKRixNQU9DO0VBQ0MsaUJBQUE7O0FBUkYsTUFPQyxjQUdDO0VBQ0MsaUJBQUE7O0FBWEgsTUFPQyxjQU9DO0VBRUMsa0JBQUE7O0FBaEJILE1Bb0JDO0VBQ0Msa0JBQUE7RUFDQSxTQUFBO0VBRUEsU0FBQTtFQUNBLGtCQUFBOztBakJ0RkYsUUFIaUM7RUE2Q2pDLE1pQnVDQztJQVVFLFVBQUE7SUFDQSxpQkFBQTtJQUVBLFVBQUE7SUFDQSxtQkFBQTs7RUFFQSxNQWhCRixXQWdCRztJQUNBLFVBQUE7SUFDQSxXQUFBOzs7QWpCbkdKLFFBSGlDO0VBNkNqQyxNaUJ1Q0M7SUF3QkUsYUFBQTs7O0FDOUlILFFBQVM7RUFDUixvQkFBQTtFZm0yQkMseUJBQUE7RUFDQSxzQkFBQTtFQUNBLHFCQUFBO0VBQ0EsaUJBQUE7RWVwMkJELGVBQUE7O0FBSUQsb0JBQXFCO0VmNmVsQixPQUFBO0VBQVMseUJBQUE7RUFDVixvQkFBQTtFQUNBLGlCQUFBO0VBQ0EsWUFBQTs7QWU1ZUYsWUFBWTtFQUNYLGtCQUFBO0Vmd2VFLE9BQUE7RUFBUywwQkFBQTtFQUNWLGtCQUFBO0VBQ0EsZUFBQTtFQUNBLFVBQUE7O0FldGVELFlBTlcsT0FNVjtFQUNBLFNBQVMsRUFBVDtFQUNBLGNBQUE7RUFDQSxrQkFBQTtFQUNBLE1BQUE7RUFDQSxVQUFBO0VBQ0EsV0FBQTtFQUNBLE9BQUE7RWZnUEEsMEJBQUE7RUFBaUMsb0NBQUE7RUFDakMsdUJBQUE7RUFBOEIsNkJBQUE7RUFDOUIsa0JBQUE7RUFBeUIsNEJBQUE7RUFtQnpCLDRDQUFBO0VBQ0EseUNBQUE7RUFDQSxvQ0FBQTs7QWVwUkYsWUFBWSxPQWtCWDtFZmdRQyx3QkFBQTtFQUNBLHFCQUFBO0VBQ0EsZ0JBQUE7RWVoUUEscUJBQUE7O0FBcEJGLFlBQVksT0F1Qlg7QUF2QkQsWUFBWSxPQXdCWDtFQUNDLGtCQUFBO0VBQ0EsVUFBQTs7QUExQkYsWUFBWSxPQTZCWDtFZnFQQyx3QkFBQTtFQUNBLHFCQUFBO0VBQ0EsZ0JBQUE7O0FlbFBGO0VmdXpCRSx5QkFBQTtFQUNBLHNCQUFBO0VBQ0EscUJBQUE7RUFDQSxpQkFBQTs7QWV2ekJGO0VBQ0MsY0FBQTtFQUNBLG9CQUFBO0VBQ0EsZ0JBQUE7RUFDQSxnQkFBQTs7QUFHQyxRQURELE9BQU0sU0FDSjtFQUNBLGVBQUE7RUFFQSxxQkFBQTtFQUNBLGdCQUFBO0VBQ0EsVUFBQTtFQUNBLHlCQUFBOztBQUdELFFBVkQsT0FBTSxTQVVKO0VBQ0EsYUFBQTs7QUFHRCxRQWRELE9BQU0sU0FjSjtFQUNBLFdBQUE7RUFDQSxpQkFBQTs7QUFHRCxRQW5CRCxPQUFNLFNBbUJKO0VBQ0EsWUFBQTtFQUNBLGdCQUFBOztBQTNCSCxRQU1DLE9BQU0sU0F3Qkw7RUFDQyxjQUFBOztBQU1ILGdCQUFnQjtBQUNoQixnQkFBZ0I7QUFDaEIsc0JBQXNCO0VBQ3JCLGFBQUE7O0FBR0QsZ0JBQWlCO0FBQ2pCLFFBQVMsT0FBTTtBQUNmLGdCQUFpQixTQUFRLFdBQVc7RUFDbkMsYUFBQTs7QUFHRCxHQUFHO0VBQ0YsVUFBQTtFQUNBLGdCQUFBO0VBQ0EsV0FBQTtFQUNBLGtCQUFBO0VBQ0EsY0FBQTtFQUNBLHlCQUFBOztBQU5ELEdBQUcsZ0JBUUY7RUFDQyxTQUFBOztBQVRGLEdBQUcsZ0JBWUY7RUFDQyxhQUFBOztBQUdELEdBaEJFLGdCQWdCRCxjQUFlO0VBQ2Ysa0JBQUE7RWZrWUMsT0FBQTtFQUFTLHlCQUFBO0VBQ1Ysb0JBQUE7RUFDQSxpQkFBQTtFQUNBLFlBQUE7O0FldFpGLEdBQUcsZ0JBdUJGLE9BQU87RUFDTixpQkFBQTs7QUFJRCxHQTVCRSxnQkE0QkE7RUFDRCxVQUFBO0VBQ0EsU0FBQTtFQUNBLDBCQUFBO0VBQ0EsV0FBQTs7QUFFQSxHQWxDQyxnQkE0QkEsS0FNQTtFQUNBLDJCQUFBOztBQUdELEdBdENDLGdCQTRCQSxLQVVBO0VBQ0Esd0JBQUE7O0FBWEYsR0E1QkUsZ0JBNEJBLEtBZUQ7RUFDQyxVQUFBO0VBQ0EsU0FBQTs7QUFJRCxHQWpEQyxnQkE0QkEsS0FxQkM7RUFDRCxrQkFBQTs7QUFFQSxHQXBEQSxnQkE0QkEsS0FxQkMsS0FHQTtFQUNBLGlCQUFBO0VBQ0EsaUJBQUE7RUFDQSxlQUFBOztBQUdELEdBMURBLGdCQTRCQSxLQXFCQyxLQVNBO0FBQ0QsR0EzREEsZ0JBNEJBLEtBcUJDLEtBVUE7RUFDQSw2QkFBQTs7QUFHRCxHQS9EQSxnQkE0QkEsS0FxQkMsS0FjQTtFQUNBLDBCQUFBOztBQUVBLEdBbEVELGdCQTRCQSxLQXFCQyxLQWNBLHNCQUdDO0VBQ0EsZ0JBQUE7O0FBSUYsR0F2RUEsZ0JBNEJBLEtBcUJDLEtBc0JBLG1CQUFtQjtBQUNwQixHQXhFQSxnQkE0QkEsS0FxQkMsS0F1QkEsbUJBQW1CO0FBQ3BCLEdBekVBLGdCQTRCQSxLQXFCQyxLQXdCQSx1QkFBdUI7QUFDeEIsR0ExRUEsZ0JBNEJBLEtBcUJDLEtBeUJBLHVCQUF1QjtFQUN2QixnQkFBQTtFQUNBLFVBQUE7O0FBR0QsR0EvRUEsZ0JBNEJBLEtBcUJDLEtBOEJBLG1CQUFtQjtBQUNwQixHQWhGQSxnQkE0QkEsS0FxQkMsS0ErQkEsdUJBQXVCO0FBQ3hCLEdBakZBLGdCQTRCQSxLQXFCQyxLQWdDQSxtQkFBbUIsT0FBTztBQUMzQixHQWxGQSxnQkE0QkEsS0FxQkMsS0FpQ0EsdUJBQXVCLE9BQU87RUFDOUIsbUJBQUE7O0FBR0QsR0F0RkEsZ0JBNEJBLEtBcUJDLEtBcUNBLG1CQUFtQjtBQUNwQixHQXZGQSxnQkE0QkEsS0FxQkMsS0FzQ0EsdUJBQXVCO0VBQ3ZCLG1CQUFBOztBQUdELEdBM0ZBLGdCQTRCQSxLQXFCQyxLQTBDQTtFQU1BLG1CQUFBOztBQUxBLEdBNUZELGdCQTRCQSxLQXFCQyxLQTBDQSx1QkFDQztFQUNBLFNBQVMsRUFBVDtFQUNBLFdBQUE7O0FBS0QsR0FuR0QsZ0JBNEJBLEtBcUJDLEtBMENBLHVCQVFFO0VBQ0QsZ0JBQUE7O0FBSUYsR0F4R0EsZ0JBNEJBLEtBcUJDLEtBdURDO0FBQUssR0F4R1AsZ0JBNEJBLEtBcUJDLEtBdURRO0VBQ1IsbUJBQUE7RUFDQSxzQkFBQTs7QUF6REYsR0FqREMsZ0JBNEJBLEtBcUJDLEtBNkREO0VBQ0MsaUJBQUE7RUFDQSxnQkFBQTs7QUEvREYsR0FqREMsZ0JBNEJBLEtBcUJDLEtBNkRELEVBSUM7RUFDQyxtQkFBQTtFQUNBLGVBQUE7O0FBbkVILEdBakRDLGdCQTRCQSxLQXFCQyxLQTZERCxFQUlDLEtBSUM7RUFDQyxtQkFBQTtFQUNBLGlCQUFBO0VBQ0EsZUFBQTtFQUNBLGdCQUFBO0VmNkNKLDBCQUFBO0VBQWlDLG9DQUFBO0VBQ2pDLHVCQUFBO0VBQThCLDZCQUFBO0VBQzlCLGtCQUFBO0VBQXlCLDRCQUFBO0VlN0NyQixzQkFBQTtFQUNBLGNBQUE7RUFDQSxpQkFBQTtFQUNBLFlBQUE7O0FBR0MsR0FsSUosZ0JBNEJBLEtBcUJDLEtBNkRELEVBSUMsS0FJQyxPQVdFLElBQUksV0FDSDtBQUNELEdBbklKLGdCQTRCQSxLQXFCQyxLQTZERCxFQUlDLEtBSUMsT0FXRSxJQUFJLFdBRUg7RUFDQSxXQUFBO0VBQ0EseUJBQUE7RUFDQSx5QkFBQTs7QUFJRixHQTFJSCxnQkE0QkEsS0FxQkMsS0E2REQsRUFJQyxLQUlDLE9Bb0JFLEtBQUs7RUFDTCxlQUFBO0Vmd1FKLE9BQUE7RUFBUyx5QkFBQTtFQUNWLG9CQUFBO0VBQ0EsaUJBQUE7RUFDQSxZQUFBOztBZXJXQSxHQWpEQyxnQkE0QkEsS0FxQkMsS0FrR0Q7RUFDQyx5QkFBQTtFQUNBLFVBQUE7RUFDQSxXQUFBOztBQXJHRixHQWpEQyxnQkE0QkEsS0FxQkMsS0FrR0QsR0FNQztFQUNDLGtCQUFBO0VBQ0EscUJBQUE7RUFHQSxjQUFBOztBQUVBLEdBaEtGLGdCQTRCQSxLQXFCQyxLQWtHRCxHQU1DLEdBT0U7RUFDQSwwQkFBQTs7QUFFQSxHQW5LSCxnQkE0QkEsS0FxQkMsS0FrR0QsR0FNQyxHQU9FLHNCQUdDO0VBQ0EsYUFBQTs7QUFKRixHQWhLRixnQkE0QkEsS0FxQkMsS0FrR0QsR0FNQyxHQU9FLHNCQU9BO0VmQUosMEJBQUE7RUFBaUMsb0NBQUE7RUFDakMsdUJBQUE7RUFBOEIsNkJBQUE7RUFDOUIsa0JBQUE7RUFBeUIsNEJBQUE7RWVBcEIsY0FBQTs7QUFFQSxHQTNLSixnQkE0QkEsS0FxQkMsS0FrR0QsR0FNQyxHQU9FLHNCQU9BLHFCQUlFO0VBQ0EsK0JBQUE7O0FBWkgsR0FoS0YsZ0JBNEJBLEtBcUJDLEtBa0dELEdBTUMsR0FPRSxzQkFPQSxxQkFRQztFQUNDLHNCQUFBOztBQUtILEdBckxGLGdCQTRCQSxLQXFCQyxLQWtHRCxHQU1DLEdBNEJHO0FBQUssR0FyTFQsZ0JBNEJBLEtBcUJDLEtBa0dELEdBTUMsR0E0QlU7RUFDUixtQkFBQTtFQUNBLHNCQUFBOztBQXRJSixHQWpEQyxnQkE0QkEsS0FxQkMsS0FrR0QsR0FNQyxHQWtDQztFQUNDLFVBQUE7O0FBM0lKLEdBakRDLGdCQTRCQSxLQXFCQyxLQWtHRCxHQU1DLEdBa0NDLEdBSUM7RUFDQyxVQUFBO0VBQ0EscUJBQUE7RUFDQSxlQUFBO0VBQ0EscUJBQUE7O0FBbEpMLEdBakRDLGdCQTRCQSxLQXFCQyxLQWtHRCxHQU1DLEdBa0NDLEdBSUMsR0FPQztFQUNDLGVBQUE7RUFDQSxtQkFBQTs7QUF2Sk4sR0FqREMsZ0JBNEJBLEtBcUJDLEtBa0dELEdBTUMsR0FrQ0MsR0FJQyxHQVlDO0FBMUpMLEdBakRDLGdCQTRCQSxLQXFCQyxLQWtHRCxHQU1DLEdBa0NDLEdBSUMsR0FhQztFQUNDLGVBQUE7RUFDQSxTQUFBO0VBQ0Esc0JBQUE7RUFDQSxzQkFBQTtFQ2xTUCxrQkFBQTtFQUNBLG1CQUFBO0VBRUEsb0JBQUE7RUFDQSxvQkFBQTs7QUR3U0EsR0ExTkUsZ0JBME5BO0VBQ0QsZ0JBQUE7O0FBSUQsR0EvTkUsZ0JBK05EO0VBQ0EsWUFBQTtFQUNBLFlBQUE7RUFDQSxnQkFBQTtFQUNBLGdCQUFBO0VBQ0Esa0JBQUE7O0FBTEQsR0EvTkUsZ0JBK05ELE1BT0E7RUN4VEQsa0JBQUE7RUFDQSxrQkFBQTtFQUVBLG9CQUFBO0VBQ0Esb0JBQUE7RURzVEUscUJBQUE7RUFDQSxtQkFBQTtFQUNBLDZCQUFBO0VBQ0EsYUFBQTtFQUNBLG9CQUFBOztBQWJGLEdBL05FLGdCQStORCxNQWdCQTtFQUVDLG9CQUFBO0VBQ0EsZ0JBQUE7O0FBbkJGLEdBL05FLGdCQStORCxNQWdCQSxHQUtDO0VBQ0MsaUJBQUE7RUFDQSxTQUFBO0VBQ0EscUJBQUE7O0FBR0QsR0ExUEEsZ0JBK05ELE1BZ0JBLEdBV0c7RUFDRCxrQkFBQTs7QUE1QkgsR0EvTkUsZ0JBK05ELE1BZ0JBLEdBZUM7RUFDQyxXQUFBO0VBQ0EsVUFBQTtFQUNBLFdBQUE7RUFDQSxpQkFBQTtFQUNBLDBCQUFBO0VBQ0EsbUJBQUE7RUFDQSxtQkFBQTtFZmxFRiw4QkFBQTtFQUNBLDJCQUFBO0VBQ0Esc0JBQUE7O0FlMEJELEdBL05FLGdCQStORCxNQWdCQSxHQWVDLEdBVUM7RUFDQyxnQkFBQTtFQUNBLFlBQUE7RUFDQSxzQkFBQTs7QUE1Q0osR0EvTkUsZ0JBK05ELE1BZ0JBLEdBZ0NDO0VBQ0MsaUJBQUE7RUFDQSxZQUFBO0VBQ0Esb0JBQUE7O0FBbkRILEdBL05FLGdCQStORCxNQWdCQSxHQWdDQyxHQUtDO0VBQ0Msa0JBQUE7O0FBR0QsR0F4UkQsZ0JBK05ELE1BZ0JBLEdBZ0NDLEdBU0U7RUFDQSxTQUFTLE9BQVQ7RUFDQSxjQUFBO0VBQ0EsV0FBQTtFQUNBLFlBQUE7RUFDQSxTQUFBO0VBQ0EsUUFBQTs7QUFPTDtBQUNBLGdCQUFpQixTQUFRO0FBQ3pCO0VmaElFLDBCQUFBO0VBQWlDLG9DQUFBO0VBQ2pDLHVCQUFBO0VBQThCLDZCQUFBO0VBQzlCLGtCQUFBO0VBQXlCLDRCQUFBO0VlZ0kxQixzQkFBQTtFQzNYQSxrQkFBQTtFQUNBLG1CQUFBO0VBRUEsb0JBQUE7RUFDQSxvQkFBQTs7QUQyWEQsZ0JBQWlCLFNBQVE7QUFDekIsV0FBWTtBQUNaO0VDallDLGtCQUFBO0VBQ0EsbUJBQUE7RUFFQSxvQkFBQTtFQUNBLG9CQUFBO0VEK1hBLGdKQUFBOztBQUdELFdBQVk7RUFDWCxZQUFBO0VBQ0EsVUFBQTtFQUNBLFNBQUE7O0FBR0QsZ0JBQWlCLFNBQVE7RWZ2SHZCLDhCQUFBO0VBQ0EsMkJBQUE7RUFDQSxzQkFBQTtFZXVIRCxjQUFBO0VBQ0EsYUFBQTtFQUNBLFdBQUE7RUFDQSxpQkFBQTtFQUNBLFNBQUE7RUFDQSxZQUFBO0VBQ0EsYUFBQTtFQUNBLGdCQUFBO0VBQ0EsV0FBQTtFQUNBLGdCQUFBO0VBQ0EsaUJBQUE7RUFDQSxjQUFBOztBQUdELGlCQUFpQjtFQUNoQixVQUFBO0VBQ0EsY0FBQTtFQzlaQSxlQUFBO0VBQ0EsbUJBQUE7RUFFQSxtQkFBQTtFQUNBLG9CQUFBO0VEa2FBLGdKQUFBOztBQVZELGlCQUFpQixpQkFJaEI7RUFDQyxjQUFBO0VBQ0EsbUJBQUE7O0FBTUQsaUJBWmdCLGlCQVlkLEtBQUk7RUFDTCxtQkFBQTs7O0FBS0Y7RUFDQyxvQkFBQTs7QUFERCx1QkFHQyxTQUFTO0VBQ1IsbUJBQUE7RUFDQSxpQkFBQTs7QUFMRix1QkFRQztFQUVDLFlBQUE7RUFHQSxnQkFBQTs7QUFJRjtFQUNDLGVBQUE7RUFDQSxNQUFBO0VBQ0EsV0FBQTtFQUNBLFdBQUE7O0FBSkQsZ0JBTUM7RUFDQyxpQkFBQTs7QUFQRixnQkFNQyxrQkFHQztFQUNDLGdCQUFBOztBQVZILGdCQU1DLGtCQUdDLE9BR0M7RUFDQyxnQkFBQTs7QUFPSjtFQUNDLGtCQUFBO0VBQ0EsVUFBQTtFQUNBLFVBQUE7O0FBRUEsS0FBQztFQUNBLGFBQUEifQ== */\r
diff --git a/js/ckeditor/samples/img/github-top.png b/js/ckeditor/samples/img/github-top.png
new file mode 100644 (file)
index 0000000..7b9cbb1
Binary files /dev/null and b/js/ckeditor/samples/img/github-top.png differ
diff --git a/js/ckeditor/samples/img/header-bg.png b/js/ckeditor/samples/img/header-bg.png
new file mode 100644 (file)
index 0000000..a14166a
Binary files /dev/null and b/js/ckeditor/samples/img/header-bg.png differ
diff --git a/js/ckeditor/samples/img/header-separator.png b/js/ckeditor/samples/img/header-separator.png
new file mode 100644 (file)
index 0000000..8c4fb9b
Binary files /dev/null and b/js/ckeditor/samples/img/header-separator.png differ
diff --git a/js/ckeditor/samples/img/logo.png b/js/ckeditor/samples/img/logo.png
new file mode 100644 (file)
index 0000000..96d86e2
Binary files /dev/null and b/js/ckeditor/samples/img/logo.png differ
diff --git a/js/ckeditor/samples/img/navigation-tip.png b/js/ckeditor/samples/img/navigation-tip.png
new file mode 100644 (file)
index 0000000..2286114
Binary files /dev/null and b/js/ckeditor/samples/img/navigation-tip.png differ
diff --git a/js/ckeditor/samples/index.html b/js/ckeditor/samples/index.html
new file mode 100644 (file)
index 0000000..663fc17
--- /dev/null
@@ -0,0 +1,128 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>CKEditor Sample</title>\r
+       <script src="../ckeditor.js"></script>\r
+       <script src="js/sample.js"></script>\r
+       <link rel="stylesheet" href="css/samples.css">\r
+       <link rel="stylesheet" href="toolbarconfigurator/lib/codemirror/neo.css">\r
+</head>\r
+<body id="main">\r
+\r
+<nav class="navigation-a">\r
+       <div class="grid-container">\r
+               <ul class="navigation-a-left grid-width-70">\r
+                       <li><a href="http://ckeditor.com">Project Homepage</a></li>\r
+                       <li><a href="https://github.com/ckeditor/ckeditor-dev/issues">I found a bug</a></li>\r
+                       <li><a href="http://github.com/ckeditor/ckeditor-dev" class="icon-pos-right icon-navigation-a-github">Fork CKEditor on GitHub</a></li>\r
+               </ul>\r
+               <ul class="navigation-a-right grid-width-30">\r
+                       <li><a href="http://ckeditor.com/blog-list">CKEditor Blog</a></li>\r
+               </ul>\r
+       </div>\r
+</nav>\r
+\r
+<header class="header-a">\r
+       <div class="grid-container">\r
+               <h1 class="header-a-logo grid-width-30">\r
+                       <a href="index.html"><img src="img/logo.png" alt="CKEditor Sample"></a>\r
+               </h1>\r
+\r
+               <nav class="navigation-b grid-width-70">\r
+                       <ul>\r
+                               <li><a href="index.html" class="button-a button-a-background">Start</a></li>\r
+                               <li><a href="toolbarconfigurator/index.html" class="button-a">Toolbar configurator <span class="balloon-a balloon-a-nw">Edit your toolbar now!</span></a></li>\r
+                       </ul>\r
+               </nav>\r
+       </div>\r
+</header>\r
+\r
+<main>\r
+       <div class="adjoined-top">\r
+               <div class="grid-container">\r
+                       <div class="content grid-width-100">\r
+                               <h1>Congratulations!</h1>\r
+                               <p>\r
+                                       If you can see CKEditor below, it means that the installation succeeded.\r
+                                       You can now try out your new editor version, see its features, and when you are ready to move on, check some of the <a href="#sample-customize">most useful resources</a> recommended below.\r
+                               </p>\r
+                       </div>\r
+               </div>\r
+       </div>\r
+       <div class="adjoined-bottom">\r
+               <div class="grid-container">\r
+                       <div class="grid-width-100">\r
+                               <div id="editor">\r
+                                       <h1>Hello world!</h1>\r
+                                       <p>I'm an instance of <a href="http://ckeditor.com">CKEditor</a>.</p>\r
+                               </div>\r
+                       </div>\r
+               </div>\r
+       </div>\r
+\r
+       <div class="grid-container">\r
+               <div class="content grid-width-100">\r
+                       <section id="sample-customize">\r
+                               <h2>Customize Your Editor</h2>\r
+                               <p>Modular build and <a href="http://docs.ckeditor.com/#!/guide/dev_configuration">numerous configuration options</a> give you nearly endless possibilities to customize CKEditor. Replace the content of your <code><a href="../config.js">config.js</a></code> file with the following code and refresh this page (<strong>remember to clear the browser cache</strong>)!</p>\r
+               <pre class="cm-s-neo CodeMirror"><code><span style="padding-right: 0.1px;"><span class="cm-variable">CKEDITOR</span>.<span class="cm-property">editorConfig</span> <span class="cm-operator">=</span> <span class="cm-keyword">function</span>( <span class="cm-def">config</span> ) {</span>\r
+<span style="padding-right: 0.1px;"><span class="cm-tab">      </span><span class="cm-variable-2">config</span>.<span class="cm-property">language</span> <span class="cm-operator">=</span> <span class="cm-string">'es'</span>;</span>\r
+<span style="padding-right: 0.1px;"><span class="cm-tab">      </span><span class="cm-variable-2">config</span>.<span class="cm-property">uiColor</span> <span class="cm-operator">=</span> <span class="cm-string">'#F7B42C'</span>;</span>\r
+<span style="padding-right: 0.1px;"><span class="cm-tab">      </span><span class="cm-variable-2">config</span>.<span class="cm-property">height</span> <span class="cm-operator">=</span> <span class="cm-number">300</span>;</span>\r
+<span style="padding-right: 0.1px;"><span class="cm-tab">      </span><span class="cm-variable-2">config</span>.<span class="cm-property">toolbarCanCollapse</span> <span class="cm-operator">=</span> <span class="cm-atom">true</span>;</span>\r
+<span style="padding-right: 0.1px;">};</span></code></pre>\r
+                       </section>\r
+\r
+                       <section>\r
+                               <h2>Toolbar Configuration</h2>\r
+                               <p>If you want to reorder toolbar buttons or remove some of them, check <a href="toolbarconfigurator/index.html">this handy tool</a>!</p>\r
+                       </section>\r
+\r
+                       <section>\r
+                               <h2>More Samples!</h2>\r
+                               <p>Visit the <a href="http://sdk.ckeditor.com">CKEditor SDK</a> for a huge collection of samples showcasing editor features, with source code readily available to copy and use in your own implementation.</p>\r
+                       </section>\r
+\r
+                       <section>\r
+                               <h2>Developer's Guide</h2>\r
+                               <p>The most important resource for all developers working with CKEditor, integrating it with their websites and applications, and customizing to their needs. You can start from here:</p>\r
+                               <ul>\r
+                                       <li><a href="http://docs.ckeditor.com/#!/guide/dev_installation">Getting Started</a> &ndash; Explains most crucial editor concepts and practices as well as the installation process and integration with your website.</li>\r
+                                       <li><a href="http://docs.ckeditor.com/#!/guide/dev_advanced_installation">Advanced Installation Concepts</a> &ndash; Describes how to upgrade, install additional components (plugins, skins), or create a custom build.</li>\r
+                               </ul>\r
+                                       <p>When you have the basics sorted out, feel free to browse some more advanced sections like:</p>\r
+                               <ul>\r
+                                       <li><a href="http://docs.ckeditor.com/#!/guide/dev_features">Functionality Overview</a> &ndash; Descriptions and samples of various editor features.</li>\r
+                                       <li><a href="http://docs.ckeditor.com/#!/guide/plugin_sdk_intro">Plugin SDK</a>, <a href="http://docs.ckeditor.com/#!/guide/widget_sdk_intro">Widget SDK</a>, and <a href="http://docs.ckeditor.com/#!/guide/skin_sdk_intro">Skin SDK</a> &ndash; Useful when you want to create your own editor components.</li>\r
+                               </ul>\r
+                       </section>\r
+\r
+                       <section>\r
+                               <h2>CKEditor JavaScript API</h2>\r
+                               <p>CKEditor boasts a rich <a href="http://docs.ckeditor.com/#!/api">JavaScript API</a> that you can use to adjust the editor to your needs and integrate it with your website or application.</p>\r
+                       </section>\r
+               </div>\r
+       </div>\r
+</main>\r
+\r
+<footer class="footer-a grid-container">\r
+       <div class="grid-container">\r
+               <p class="grid-width-100">\r
+                       CKEditor &ndash; The text editor for the Internet &ndash; <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p class="grid-width-100" id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> &ndash; Frederico Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</footer>\r
+<script>\r
+       initSample();\r
+</script>\r
+\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/js/sample.js b/js/ckeditor/samples/js/sample.js
new file mode 100644 (file)
index 0000000..f0cd939
--- /dev/null
@@ -0,0 +1,53 @@
+/**\r
+ * Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+ * For licensing, see LICENSE.md or http://ckeditor.com/license\r
+ */\r
+\r
+/* exported initSample */\r
+\r
+if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 )\r
+       CKEDITOR.tools.enableHtml5Elements( document );\r
+\r
+// The trick to keep the editor in the sample quite small\r
+// unless user specified own height.\r
+CKEDITOR.config.height = 150;\r
+CKEDITOR.config.width = 'auto';\r
+\r
+var initSample = ( function() {\r
+       var wysiwygareaAvailable = isWysiwygareaAvailable(),\r
+               isBBCodeBuiltIn = !!CKEDITOR.plugins.get( 'bbcode' );\r
+\r
+       return function() {\r
+               var editorElement = CKEDITOR.document.getById( 'editor' );\r
+\r
+               // :(((\r
+               if ( isBBCodeBuiltIn ) {\r
+                       editorElement.setHtml(\r
+                               'Hello world!\n\n' +\r
+                               'I\'m an instance of [url=http://ckeditor.com]CKEditor[/url].'\r
+                       );\r
+               }\r
+\r
+               // Depending on the wysiwygare plugin availability initialize classic or inline editor.\r
+               if ( wysiwygareaAvailable ) {\r
+                       CKEDITOR.replace( 'editor' );\r
+               } else {\r
+                       editorElement.setAttribute( 'contenteditable', 'true' );\r
+                       CKEDITOR.inline( 'editor' );\r
+\r
+                       // TODO we can consider displaying some info box that\r
+                       // without wysiwygarea the classic editor may not work.\r
+               }\r
+       };\r
+\r
+       function isWysiwygareaAvailable() {\r
+               // If in development mode, then the wysiwygarea must be available.\r
+               // Split REV into two strings so builder does not replace it :D.\r
+               if ( CKEDITOR.revision == ( '%RE' + 'V%' ) ) {\r
+                       return true;\r
+               }\r
+\r
+               return !!CKEDITOR.plugins.get( 'wysiwygarea' );\r
+       }\r
+} )();\r
+\r
diff --git a/js/ckeditor/samples/js/sf.js b/js/ckeditor/samples/js/sf.js
new file mode 100644 (file)
index 0000000..69dd77d
--- /dev/null
@@ -0,0 +1,17 @@
+/*
+ Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ For licensing, see LICENSE.md or http://ckeditor.com/license
+*/
+var SF=function(){function d(a){return(a=a.attributes?a.attributes.getNamedItem("class"):null)?a.value.split(" "):[]}function c(a){var e=document.createAttribute("class");e.value=a.join(" ");return e}var b={attachListener:function(a,e,b){if(a.addEventListener)a.addEventListener(e,b,!1);else if(a.attachEvent)a.attachEvent("on"+e,function(){b.apply(a,arguments)});else throw Error("Could not attach event.");}};b.indexOf=function(){var a=Array.prototype.indexOf;return"function"===a?function(e,b){return a.call(e,
+b)}:function(a,b){for(var c=a.length,d=0;d<c;d++)if(a[d]===b)return d;return-1}}();b.accept=function(a,e){var c;a.children?(c=a.children,e(a)):"number"===typeof a.length&&(c=a);for(var d=c?c.length||0:0;d--;)b.accept(c[d],e)};b.getByClass=function(){var a=document.getElementsByClassName;return"function"===typeof a?function(e,b){"string"===typeof e&&(b=e,e=document);return a.call(e,b)}:function(a,c){"string"===typeof a&&(c=a,a=document.getElementsByTagName("html")[0]);var d=[];b.accept(a,function(a){b.classList.contains(a,
+c)&&d.push(a)});return d}}();b.classList={};b.classList.add=function(a,b){var f=d(a);f.push(b);a.attributes.setNamedItem(c(f))};b.classList.remove=function(a,e){var f=d(a,e),n=b.indexOf(f,e);-1!==n&&(f.splice(n,1),a.attributes.setNamedItem(c(f)))};b.classList.contains=function(a,c){return-1!==b.indexOf(d(a),c)};b.classList.toggle=function(a,b){this.contains(a,b)?this.remove(a,b):this.add(a,b)};return b}();"use strict";
+(function(){function d(c){for(var b in c)delete c[b]}SF.modal=function(c){function b(a){27==a.keyCode&&f.close()}c.modalClass="modal content";c.closeClass="modal-close";c.modalStyles=d;c.closeStyles=d;var a=c.afterCreate,e=c.afterClose;c.afterCreate=function(c){a&&a(c);window.addEventListener("keydown",b)};c.afterClose=function(a){e&&e(a);window.removeEventListener("keydown",b)};var f=(new picoModal(c)).afterCreate(c.afterCreate).afterClose(c.afterClose);return f}})();"use strict";
+(function(){for(var d=SF.getByClass("toggler"),c=d.length;c--;)SF.attachListener(d[c],"click",function(){var b=SF.classList.contains(this,"icon-toggler-expanded")||SF.classList.contains(this,"icon-toggler-collapsed"),a=document.getElementById(this.getAttribute("data-for"));SF.classList.toggle(this,"collapsed");SF.classList.contains(this,"collapsed")?(SF.classList.add(a,"collapsed"),b&&(SF.classList.remove(this,"icon-toggler-expanded"),SF.classList.add(this,"icon-toggler-collapsed"))):(SF.classList.remove(a,
+"collapsed"),b&&(SF.classList.remove(this,"icon-toggler-collapsed"),SF.classList.add(this,"icon-toggler-expanded")))})})();"use strict";(function(){for(var d=SF.getByClass("tree-a"),c=d.length;c--;)SF.attachListener(d[c],"click",function(b){b=b.target||b.srcElement;"H2"!==b.nodeName||SF.classList.contains(b,"tree-a-no-sub")||SF.classList.toggle(b,"tree-a-active")})})();
+(function(d,c){function b(a){return"object"===typeof Node?a instanceof Node:a&&"object"===typeof a&&"number"===typeof a.nodeType}function a(){var a=[];return{watch:a.push.bind(a),trigger:function(b){for(var c=!0,d={preventDefault:function(){c=!1}},e=0;e<a.length;e++)a[e](b,d);return c}}}function e(a){this.elem=a}function f(a,b){return e.div().clazz("pico-overlay").clazz(a("overlayClass","")).stylize({display:"block",position:"fixed",top:"0px",left:"0px",height:"100%",width:"100%",zIndex:1E4}).stylize(a("overlayStyles",
+{opacity:.5,background:"#000"})).onClick(function(){a("overlayClose",!0)&&b()})}function n(a,b){var c=a("width","auto");"number"===typeof c&&(c=""+c+"px");return e.div().clazz("pico-content").clazz(a("modalClass","")).stylize({display:"block",position:"fixed",zIndex:10001,left:"50%",top:"50px",width:c,"-ms-transform":"translateX(-50%)","-moz-transform":"translateX(-50%)","-webkit-transform":"translateX(-50%)","-o-transform":"translateX(-50%)",transform:"translateX(-50%)"}).stylize(a("modalStyles",
+{backgroundColor:"white",padding:"20px",borderRadius:"5px"})).html(a("content")).attr("role","dialog").onClick(function(a){(new e(a.target)).anyAncestor(function(a){return/\bpico-close\b/.test(a.elem.className)})&&b()})}function p(a){return function(){return a().elem}}function k(c){function e(a,b){var d=c[a];"function"===typeof d&&(d=d(b));return void 0===d?b:d}function k(){l().hide();m().hide();v.trigger(h)}function q(){w.trigger(h)&&k()}function g(a){return function(){a.apply(this,arguments);return h}}
+function r(a){if(!t){var c=n(e,q),b=f(e,q),d;d=e("closeButton",!0)?c.child().html(e("closeHtml","\x26#xD7;")).clazz("pico-close").clazz(e("closeClass")).stylize(e("closeStyles",{borderRadius:"2px",cursor:"pointer",height:"15px",width:"15px",position:"absolute",top:"5px",right:"5px",fontSize:"16px",textAlign:"center",lineHeight:"15px",background:"#CCC"})):void 0;t={modal:c,overlay:b,close:d};x.trigger(h)}return t[a]}if("string"===typeof c||b(c))c={content:c};var x=a(),y=a(),z=a(),w=a(),v=a(),t,m=r.bind(d,
+"modal"),l=r.bind(d,"overlay"),u=r.bind(d,"close"),h={modalElem:p(m),closeElem:p(u),overlayElem:p(l),show:function(){y.trigger(h)&&(l().show(),u(),m().show(),z.trigger(h));return this},close:g(q),forceClose:g(k),destroy:function(){m=m().destroy();l=l().destroy();u=void 0},options:function(a){c=a},afterCreate:g(x.watch),beforeShow:g(y.watch),afterShow:g(z.watch),beforeClose:g(w.watch),afterClose:g(v.watch)};return h}e.div=function(a){var b=c.createElement("div");(a||c.body).appendChild(b);return new e(b)};
+e.prototype={child:function(){return e.div(this.elem)},stylize:function(a){a=a||{};"undefined"!==typeof a.opacity&&(a.filter="alpha(opacity\x3d"+100*a.opacity+")");for(var b in a)a.hasOwnProperty(b)&&(this.elem.style[b]=a[b]);return this},clazz:function(a){this.elem.className+=" "+a;return this},html:function(a){b(a)?this.elem.appendChild(a):this.elem.innerHTML=a;return this},onClick:function(a){this.elem.addEventListener("click",a);return this},destroy:function(){c.body.removeChild(this.elem)},hide:function(){this.elem.style.display=
+"none"},show:function(){this.elem.style.display="block"},attr:function(a,b){this.elem.setAttribute(a,b);return this},anyAncestor:function(a){for(var b=this.elem;b;){if(a(new e(b)))return!0;b=b.parentNode}return!1}};"function"===typeof d.define&&d.define.amd?d.define(function(){return k}):d.picoModal=k})(window,document);
\ No newline at end of file
diff --git a/js/ckeditor/samples/old/ajax.html b/js/ckeditor/samples/old/ajax.html
new file mode 100644 (file)
index 0000000..3ca07c2
--- /dev/null
@@ -0,0 +1,85 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>Ajax &mdash; CKEditor Sample</title>\r
+       <script src="../../ckeditor.js"></script>\r
+       <link rel="stylesheet" href="sample.css">\r
+       <script>\r
+\r
+               var editor, html = '';\r
+\r
+               function createEditor() {\r
+                       if ( editor )\r
+                               return;\r
+\r
+                       // Create a new editor inside the <div id="editor">, setting its value to html\r
+                       var config = {};\r
+                       editor = CKEDITOR.appendTo( 'editor', config, html );\r
+               }\r
+\r
+               function removeEditor() {\r
+                       if ( !editor )\r
+                               return;\r
+\r
+                       // Retrieve the editor contents. In an Ajax application, this data would be\r
+                       // sent to the server or used in any other way.\r
+                       document.getElementById( 'editorcontents' ).innerHTML = html = editor.getData();\r
+                       document.getElementById( 'contents' ).style.display = '';\r
+\r
+                       // Destroy the editor.\r
+                       editor.destroy();\r
+                       editor = null;\r
+               }\r
+\r
+       </script>\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="index.html">CKEditor Samples</a> &raquo; Create and Destroy Editor Instances for Ajax Applications\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out its <a href="http://sdk.ckeditor.com/samples/saveajax.html">brand new version in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>\r
+                       This sample shows how to create and destroy CKEditor instances on the fly. After the removal of CKEditor the content created inside the editing\r
+                       area will be displayed in a <code>&lt;div&gt;</code> element.\r
+               </p>\r
+               <p>\r
+                       For details of how to create this setup check the source code of this sample page\r
+                       for JavaScript code responsible for the creation and destruction of a CKEditor instance.\r
+               </p>\r
+       </div>\r
+       <p>Click the buttons to create and remove a CKEditor instance.</p>\r
+       <p>\r
+               <input onclick="createEditor();" type="button" value="Create Editor">\r
+               <input onclick="removeEditor();" type="button" value="Remove Editor">\r
+       </p>\r
+       <!-- This div will hold the editor. -->\r
+       <div id="editor">\r
+       </div>\r
+       <div id="contents" style="display: none">\r
+               <p>\r
+                       Edited Contents:\r
+               </p>\r
+               <!-- This div will be used to display the editor contents. -->\r
+               <div id="editorcontents">\r
+               </div>\r
+       </div>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/api.html b/js/ckeditor/samples/old/api.html
new file mode 100644 (file)
index 0000000..88a4b06
--- /dev/null
@@ -0,0 +1,210 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>API Usage &mdash; CKEditor Sample</title>\r
+       <script src="../../ckeditor.js"></script>\r
+       <link href="sample.css" rel="stylesheet">\r
+       <script>\r
+\r
+// The instanceReady event is fired, when an instance of CKEditor has finished\r
+// its initialization.\r
+CKEDITOR.on( 'instanceReady', function( ev ) {\r
+       // Show the editor name and description in the browser status bar.\r
+       document.getElementById( 'eMessage' ).innerHTML = 'Instance <code>' + ev.editor.name + '<\/code> loaded.';\r
+\r
+       // Show this sample buttons.\r
+       document.getElementById( 'eButtons' ).style.display = 'block';\r
+});\r
+\r
+function InsertHTML() {\r
+       // Get the editor instance that we want to interact with.\r
+       var editor = CKEDITOR.instances.editor1;\r
+       var value = document.getElementById( 'htmlArea' ).value;\r
+\r
+       // Check the active editing mode.\r
+       if ( editor.mode == 'wysiwyg' )\r
+       {\r
+               // Insert HTML code.\r
+               // http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-insertHtml\r
+               editor.insertHtml( value );\r
+       }\r
+       else\r
+               alert( 'You must be in WYSIWYG mode!' );\r
+}\r
+\r
+function InsertText() {\r
+       // Get the editor instance that we want to interact with.\r
+       var editor = CKEDITOR.instances.editor1;\r
+       var value = document.getElementById( 'txtArea' ).value;\r
+\r
+       // Check the active editing mode.\r
+       if ( editor.mode == 'wysiwyg' )\r
+       {\r
+               // Insert as plain text.\r
+               // http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-insertText\r
+               editor.insertText( value );\r
+       }\r
+       else\r
+               alert( 'You must be in WYSIWYG mode!' );\r
+}\r
+\r
+function SetContents() {\r
+       // Get the editor instance that we want to interact with.\r
+       var editor = CKEDITOR.instances.editor1;\r
+       var value = document.getElementById( 'htmlArea' ).value;\r
+\r
+       // Set editor contents (replace current contents).\r
+       // http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-setData\r
+       editor.setData( value );\r
+}\r
+\r
+function GetContents() {\r
+       // Get the editor instance that you want to interact with.\r
+       var editor = CKEDITOR.instances.editor1;\r
+\r
+       // Get editor contents\r
+       // http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-getData\r
+       alert( editor.getData() );\r
+}\r
+\r
+function ExecuteCommand( commandName ) {\r
+       // Get the editor instance that we want to interact with.\r
+       var editor = CKEDITOR.instances.editor1;\r
+\r
+       // Check the active editing mode.\r
+       if ( editor.mode == 'wysiwyg' )\r
+       {\r
+               // Execute the command.\r
+               // http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-execCommand\r
+               editor.execCommand( commandName );\r
+       }\r
+       else\r
+               alert( 'You must be in WYSIWYG mode!' );\r
+}\r
+\r
+function CheckDirty() {\r
+       // Get the editor instance that we want to interact with.\r
+       var editor = CKEDITOR.instances.editor1;\r
+       // Checks whether the current editor contents present changes when compared\r
+       // to the contents loaded into the editor at startup\r
+       // http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-checkDirty\r
+       alert( editor.checkDirty() );\r
+}\r
+\r
+function ResetDirty() {\r
+       // Get the editor instance that we want to interact with.\r
+       var editor = CKEDITOR.instances.editor1;\r
+       // Resets the "dirty state" of the editor (see CheckDirty())\r
+       // http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-resetDirty\r
+       editor.resetDirty();\r
+       alert( 'The "IsDirty" status has been reset' );\r
+}\r
+\r
+function Focus() {\r
+       CKEDITOR.instances.editor1.focus();\r
+}\r
+\r
+function onFocus() {\r
+       document.getElementById( 'eMessage' ).innerHTML = '<b>' + this.name + ' is focused </b>';\r
+}\r
+\r
+function onBlur() {\r
+       document.getElementById( 'eMessage' ).innerHTML = this.name + ' lost focus';\r
+}\r
+\r
+       </script>\r
+\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="index.html">CKEditor Samples</a> &raquo; Using CKEditor JavaScript API\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out its <a href="http://sdk.ckeditor.com/samples/api.html">brand new version in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+       <p>\r
+               This sample shows how to use the\r
+               <a class="samples" href="http://docs.ckeditor.com/#!/api/CKEDITOR.editor">CKEditor JavaScript API</a>\r
+               to interact with the editor at runtime.\r
+       </p>\r
+       <p>\r
+               For details on how to create this setup check the source code of this sample page.\r
+       </p>\r
+       </div>\r
+\r
+       <!-- This <div> holds alert messages to be display in the sample page. -->\r
+       <div id="alerts">\r
+               <noscript>\r
+                       <p>\r
+                               <strong>CKEditor requires JavaScript to run</strong>. In a browser with no JavaScript\r
+                               support, like yours, you should still see the contents (HTML data) and you should\r
+                               be able to edit it normally, without a rich editor interface.\r
+                       </p>\r
+               </noscript>\r
+       </div>\r
+       <form action="../../../samples/sample_posteddata.php" method="post">\r
+               <textarea cols="100" id="editor1" name="editor1" rows="10">&lt;p&gt;This is some &lt;strong&gt;sample text&lt;/strong&gt;. You are using &lt;a href="http://ckeditor.com/"&gt;CKEditor&lt;/a&gt;.&lt;/p&gt;</textarea>\r
+\r
+               <script>\r
+                       // Replace the <textarea id="editor1"> with an CKEditor instance.\r
+                       CKEDITOR.replace( 'editor1', {\r
+                               on: {\r
+                                       focus: onFocus,\r
+                                       blur: onBlur,\r
+\r
+                                       // Check for availability of corresponding plugins.\r
+                                       pluginsLoaded: function( evt ) {\r
+                                               var doc = CKEDITOR.document, ed = evt.editor;\r
+                                               if ( !ed.getCommand( 'bold' ) )\r
+                                                       doc.getById( 'exec-bold' ).hide();\r
+                                               if ( !ed.getCommand( 'link' ) )\r
+                                                       doc.getById( 'exec-link' ).hide();\r
+                                       }\r
+                               }\r
+                       });\r
+               </script>\r
+\r
+               <p id="eMessage">\r
+               </p>\r
+\r
+               <div id="eButtons" style="display: none">\r
+                       <input id="exec-bold" onclick="ExecuteCommand('bold');" type="button" value="Execute &quot;bold&quot; Command">\r
+                       <input id="exec-link" onclick="ExecuteCommand('link');" type="button" value="Execute &quot;link&quot; Command">\r
+                       <input onclick="Focus();" type="button" value="Focus">\r
+                       <br><br>\r
+                       <input onclick="InsertHTML();" type="button" value="Insert HTML">\r
+                       <input onclick="SetContents();" type="button" value="Set Editor Contents">\r
+                       <input onclick="GetContents();" type="button" value="Get Editor Contents (HTML)">\r
+                       <br>\r
+                       <textarea cols="100" id="htmlArea" rows="3">&lt;h2&gt;Test&lt;/h2&gt;&lt;p&gt;This is some &lt;a href="/Test1.html"&gt;sample&lt;/a&gt; HTML code.&lt;/p&gt;</textarea>\r
+                       <br>\r
+                       <br>\r
+                       <input onclick="InsertText();" type="button" value="Insert Text">\r
+                       <br>\r
+                       <textarea cols="100" id="txtArea" rows="3">   First line with some leading whitespaces.\r
+\r
+Second line of text preceded by two line breaks.</textarea>\r
+                       <br>\r
+                       <br>\r
+                       <input onclick="CheckDirty();" type="button" value="checkDirty()">\r
+                       <input onclick="ResetDirty();" type="button" value="resetDirty()">\r
+               </div>\r
+       </form>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/appendto.html b/js/ckeditor/samples/old/appendto.html
new file mode 100644 (file)
index 0000000..dbabf75
--- /dev/null
@@ -0,0 +1,59 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>Append To Page Element Using JavaScript Code &mdash; CKEditor Sample</title>\r
+       <script src="../../ckeditor.js"></script>\r
+       <link rel="stylesheet" href="sample.css">\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="index.html">CKEditor Samples</a> &raquo; Append To Page Element Using JavaScript Code\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out the <a href="http://sdk.ckeditor.com/">brand new samples in CKEditor SDK</a>.\r
+       </div>\r
+       <div id="section1">\r
+               <div class="description">\r
+                       <p>\r
+                               The <code><a class="samples" href="http://docs.ckeditor.com/#!/api/CKEDITOR-method-appendTo">CKEDITOR.appendTo()</a></code> method serves to to place editors inside existing DOM elements. Unlike <code><a class="samples" href="http://docs.ckeditor.com/#!/api/CKEDITOR-method-replace">CKEDITOR.replace()</a></code>,\r
+                               a target container to be replaced is no longer necessary. A new editor\r
+                               instance is inserted directly wherever it is desired.\r
+                       </p>\r
+<pre class="samples">CKEDITOR.appendTo( '<em>container_id</em>',\r
+       { /* Configuration options to be used. */ }\r
+       'Editor content to be used.'\r
+);</pre>\r
+               </div>\r
+               <script>\r
+\r
+                       // This call can be placed at any point after the\r
+                       // DOM element to append CKEditor to or inside the <head><script>\r
+                       // in a window.onload event handler.\r
+\r
+                       // Append a CKEditor instance using the default configuration and the\r
+                       // provided content to the <div> element of ID "section1".\r
+                       CKEDITOR.appendTo( 'section1',\r
+                               null,\r
+                               '<p>This is some <strong>sample text</strong>. You are using <a href="http://ckeditor.com/">CKEditor</a>.</p>'\r
+                       );\r
+\r
+               </script>\r
+       </div>\r
+       <br>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/assets/inlineall/logo.png b/js/ckeditor/samples/old/assets/inlineall/logo.png
new file mode 100644 (file)
index 0000000..b4d5979
Binary files /dev/null and b/js/ckeditor/samples/old/assets/inlineall/logo.png differ
diff --git a/js/ckeditor/samples/old/assets/outputxhtml/outputxhtml.css b/js/ckeditor/samples/old/assets/outputxhtml/outputxhtml.css
new file mode 100644 (file)
index 0000000..fbcc767
--- /dev/null
@@ -0,0 +1,204 @@
+/*\r
+ * Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+ * For licensing, see LICENSE.md or http://ckeditor.com/license\r
+ *\r
+ * Styles used by the XHTML 1.1 sample page (xhtml.html).\r
+ */\r
+\r
+/**\r
+ * Basic definitions for the editing area.\r
+ */\r
+body\r
+{\r
+       font-family: Arial, Verdana, sans-serif;\r
+       font-size: 80%;\r
+       color: #000000;\r
+       background-color: #ffffff;\r
+       padding: 5px;\r
+       margin: 0px;\r
+}\r
+\r
+/**\r
+ * Core styles.\r
+ */\r
+\r
+.Bold\r
+{\r
+       font-weight: bold;\r
+}\r
+\r
+.Italic\r
+{\r
+       font-style: italic;\r
+}\r
+\r
+.Underline\r
+{\r
+       text-decoration: underline;\r
+}\r
+\r
+.StrikeThrough\r
+{\r
+       text-decoration: line-through;\r
+}\r
+\r
+.Subscript\r
+{\r
+       vertical-align: sub;\r
+       font-size: smaller;\r
+}\r
+\r
+.Superscript\r
+{\r
+       vertical-align: super;\r
+       font-size: smaller;\r
+}\r
+\r
+/**\r
+ * Font faces.\r
+ */\r
+\r
+.FontComic\r
+{\r
+       font-family: 'Comic Sans MS';\r
+}\r
+\r
+.FontCourier\r
+{\r
+       font-family: 'Courier New';\r
+}\r
+\r
+.FontTimes\r
+{\r
+       font-family: 'Times New Roman';\r
+}\r
+\r
+/**\r
+ * Font sizes.\r
+ */\r
+\r
+.FontSmaller\r
+{\r
+       font-size: smaller;\r
+}\r
+\r
+.FontLarger\r
+{\r
+       font-size: larger;\r
+}\r
+\r
+.FontSmall\r
+{\r
+       font-size: 8pt;\r
+}\r
+\r
+.FontBig\r
+{\r
+       font-size: 14pt;\r
+}\r
+\r
+.FontDouble\r
+{\r
+       font-size: 200%;\r
+}\r
+\r
+/**\r
+ * Font colors.\r
+ */\r
+.FontColor1\r
+{\r
+       color: #ff9900;\r
+}\r
+\r
+.FontColor2\r
+{\r
+       color: #0066cc;\r
+}\r
+\r
+.FontColor3\r
+{\r
+       color: #ff0000;\r
+}\r
+\r
+.FontColor1BG\r
+{\r
+       background-color: #ff9900;\r
+}\r
+\r
+.FontColor2BG\r
+{\r
+       background-color: #0066cc;\r
+}\r
+\r
+.FontColor3BG\r
+{\r
+       background-color: #ff0000;\r
+}\r
+\r
+/**\r
+ * Indentation.\r
+ */\r
+\r
+.Indent1\r
+{\r
+       margin-left: 40px;\r
+}\r
+\r
+.Indent2\r
+{\r
+       margin-left: 80px;\r
+}\r
+\r
+.Indent3\r
+{\r
+       margin-left: 120px;\r
+}\r
+\r
+/**\r
+ * Alignment.\r
+ */\r
+\r
+.JustifyLeft\r
+{\r
+       text-align: left;\r
+}\r
+\r
+.JustifyRight\r
+{\r
+       text-align: right;\r
+}\r
+\r
+.JustifyCenter\r
+{\r
+       text-align: center;\r
+}\r
+\r
+.JustifyFull\r
+{\r
+       text-align: justify;\r
+}\r
+\r
+/**\r
+ * Other.\r
+ */\r
+\r
+code\r
+{\r
+       font-family: courier, monospace;\r
+       background-color: #eeeeee;\r
+       padding-left: 1px;\r
+       padding-right: 1px;\r
+       border: #c0c0c0 1px solid;\r
+}\r
+\r
+kbd\r
+{\r
+       padding: 0px 1px 0px 1px;\r
+       border-width: 1px 2px 2px 1px;\r
+       border-style: solid;\r
+}\r
+\r
+blockquote\r
+{\r
+       color: #808080;\r
+}\r
diff --git a/js/ckeditor/samples/old/assets/posteddata.php b/js/ckeditor/samples/old/assets/posteddata.php
new file mode 100644 (file)
index 0000000..bb4dd94
--- /dev/null
@@ -0,0 +1,59 @@
+<!DOCTYPE html>\r
+<?php\r
+/*\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+*/\r
+?>\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>Sample &mdash; CKEditor</title>\r
+       <link rel="stylesheet" href="sample.css">\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               CKEditor &mdash; Posted Data\r
+       </h1>\r
+       <table border="1" cellspacing="0" id="outputSample">\r
+               <colgroup><col width="120"></colgroup>\r
+               <thead>\r
+                       <tr>\r
+                               <th>Field&nbsp;Name</th>\r
+                               <th>Value</th>\r
+                       </tr>\r
+               </thead>\r
+<?php\r
+\r
+if (!empty($_POST))\r
+{\r
+       foreach ( $_POST as $key => $value )\r
+       {\r
+               if ( ( !is_string($value) && !is_numeric($value) ) || !is_string($key) )\r
+                       continue;\r
+\r
+               if ( get_magic_quotes_gpc() )\r
+                       $value = htmlspecialchars( stripslashes((string)$value) );\r
+               else\r
+                       $value = htmlspecialchars( (string)$value );\r
+?>\r
+               <tr>\r
+                       <th style="vertical-align: top"><?php echo htmlspecialchars( (string)$key ); ?></th>\r
+                       <td><pre class="samples"><?php echo $value; ?></pre></td>\r
+               </tr>\r
+       <?php\r
+       }\r
+}\r
+?>\r
+       </table>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/assets/sample.jpg b/js/ckeditor/samples/old/assets/sample.jpg
new file mode 100644 (file)
index 0000000..9498271
Binary files /dev/null and b/js/ckeditor/samples/old/assets/sample.jpg differ
diff --git a/js/ckeditor/samples/old/assets/uilanguages/languages.js b/js/ckeditor/samples/old/assets/uilanguages/languages.js
new file mode 100644 (file)
index 0000000..3208ff4
--- /dev/null
@@ -0,0 +1,7 @@
+/*
+ Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ For licensing, see LICENSE.md or http://ckeditor.com/license
+*/
+var CKEDITOR_LANGS=function(){var c={af:"Afrikaans",ar:"Arabic",az:"Azerbaijani",bg:"Bulgarian",bn:"Bengali/Bangla",bs:"Bosnian",ca:"Catalan",cs:"Czech",cy:"Welsh",da:"Danish",de:"German","de-ch":"German (Switzerland)",el:"Greek",en:"English","en-au":"English (Australia)","en-ca":"English (Canadian)","en-gb":"English (United Kingdom)",eo:"Esperanto",es:"Spanish","es-mx":"Spanish (Mexico)",et:"Estonian",eu:"Basque",fa:"Persian",fi:"Finnish",fo:"Faroese",fr:"French","fr-ca":"French (Canada)",gl:"Galician",
+gu:"Gujarati",he:"Hebrew",hi:"Hindi",hr:"Croatian",hu:"Hungarian",id:"Indonesian",is:"Icelandic",it:"Italian",ja:"Japanese",ka:"Georgian",km:"Khmer",ko:"Korean",ku:"Kurdish",lt:"Lithuanian",lv:"Latvian",mk:"Macedonian",mn:"Mongolian",ms:"Malay",nb:"Norwegian Bokmal",nl:"Dutch",no:"Norwegian",oc:"Occitan",pl:"Polish",pt:"Portuguese (Portugal)","pt-br":"Portuguese (Brazil)",ro:"Romanian",ru:"Russian",si:"Sinhala",sk:"Slovak",sq:"Albanian",sl:"Slovenian",sr:"Serbian (Cyrillic)","sr-latn":"Serbian (Latin)",
+sv:"Swedish",th:"Thai",tr:"Turkish",tt:"Tatar",ug:"Uighur",uk:"Ukrainian",vi:"Vietnamese",zh:"Chinese Traditional","zh-cn":"Chinese Simplified"},b=[],a;for(a in CKEDITOR.lang.languages)b.push({code:a,name:c[a]||a});b.sort(function(a,b){return a.name<b.name?-1:1});return b}();
\ No newline at end of file
diff --git a/js/ckeditor/samples/old/datafiltering.html b/js/ckeditor/samples/old/datafiltering.html
new file mode 100644 (file)
index 0000000..637c17b
--- /dev/null
@@ -0,0 +1,508 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>Data Filtering &mdash; CKEditor Sample</title>\r
+       <script src="../../ckeditor.js"></script>\r
+       <link rel="stylesheet" href="sample.css">\r
+       <script>\r
+               // Remove advanced tabs for all editors.\r
+               CKEDITOR.config.removeDialogTabs = 'image:advanced;link:advanced;flash:advanced;creatediv:advanced;editdiv:advanced';\r
+       </script>\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="index.html">CKEditor Samples</a> &raquo; Data Filtering and Features Activation\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out its <a href="http://sdk.ckeditor.com/samples/acf.html">brand new version in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>\r
+                       This sample page demonstrates the idea of Advanced Content Filter\r
+                       (<abbr title="Advanced Content Filter">ACF</abbr>), a sophisticated\r
+                       tool that takes control over what kind of data is accepted by the editor and what\r
+                       kind of output is produced.\r
+               </p>\r
+               <h2>When and what is being filtered?</h2>\r
+               <p>\r
+                       <abbr title="Advanced Content Filter">ACF</abbr> controls\r
+                       <strong>every single source of data</strong> that comes to the editor.\r
+                       It process both HTML that is inserted manually (i.e. pasted by the user)\r
+                       and programmatically like:\r
+               </p>\r
+<pre class="samples">\r
+editor.setData( '&lt;p&gt;Hello world!&lt;/p&gt;' );\r
+</pre>\r
+               <p>\r
+                       <abbr title="Advanced Content Filter">ACF</abbr> discards invalid,\r
+                       useless HTML tags and attributes so the editor remains "clean" during\r
+                       runtime. <abbr title="Advanced Content Filter">ACF</abbr> behaviour\r
+                       can be configured and adjusted for a particular case to prevent the\r
+                       output HTML (i.e. in CMS systems) from being polluted.\r
+\r
+                       This kind of filtering is a first, client-side line of defense\r
+                       against "<a href="http://en.wikipedia.org/wiki/Tag_soup">tag soups</a>",\r
+                       the tool that precisely restricts which tags, attributes and styles\r
+                       are allowed (desired). When properly configured, <abbr title="Advanced Content Filter">ACF</abbr>\r
+                       is an easy and fast way to produce a high-quality, intentionally filtered HTML.\r
+               </p>\r
+\r
+               <h3>How to configure or disable ACF?</h3>\r
+               <p>\r
+                       Advanced Content Filter is enabled by default, working in "automatic mode", yet\r
+                       it provides a set of easy rules that allow adjusting filtering rules\r
+                       and disabling the entire feature when necessary. The config property\r
+                       responsible for this feature is <code><a class="samples"\r
+                       href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-allowedContent">config.allowedContent</a></code>.\r
+               </p>\r
+               <p>\r
+                       By "automatic mode" is meant that loaded plugins decide which kind\r
+                       of content is enabled and which is not. For example, if the link\r
+                       plugin is loaded it implies that <code>&lt;a&gt;</code> tag is\r
+                       automatically allowed. Each plugin is given a set\r
+                       of predefined <abbr title="Advanced Content Filter">ACF</abbr> rules\r
+                       that control the editor until <code><a class="samples"\r
+                       href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-allowedContent">\r
+                       config.allowedContent</a></code>\r
+                       is defined manually.\r
+               </p>\r
+               <p>\r
+                       Let's assume our intention is to restrict the editor to accept (produce) <strong>paragraphs\r
+                       only: no attributes, no styles, no other tags</strong>.\r
+                       With <abbr title="Advanced Content Filter">ACF</abbr>\r
+                       this is very simple. Basically set <code><a class="samples"\r
+                       href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-allowedContent">\r
+                       config.allowedContent</a></code> to <code>'p'</code>:\r
+               </p>\r
+<pre class="samples">\r
+var editor = CKEDITOR.replace( <em>textarea_id</em>, {\r
+       <strong>allowedContent: 'p'</strong>\r
+} );\r
+</pre>\r
+               <p>\r
+                       Now try to play with allowed content:\r
+               </p>\r
+<pre class="samples">\r
+// Trying to insert disallowed tag and attribute.\r
+editor.setData( '&lt;p <strong>style="color: red"</strong>&gt;Hello <strong>&lt;em&gt;world&lt;/em&gt;</strong>!&lt;/p&gt;' );\r
+alert( editor.getData() );\r
+\r
+// Filtered data is returned.\r
+"&lt;p&gt;Hello world!&lt;/p&gt;"\r
+</pre>\r
+               <p>\r
+                       What happened? Since <code>config.allowedContent: 'p'</code> is set the editor assumes\r
+                       that only plain <code>&lt;p&gt;</code> are accepted. Nothing more. This is why\r
+                       <code>style</code> attribute and <code>&lt;em&gt;</code> tag are gone. The same\r
+                       filtering would happen if we pasted disallowed HTML into this editor.\r
+               </p>\r
+               <p>\r
+                       This is just a small sample of what <abbr title="Advanced Content Filter">ACF</abbr>\r
+                       can do. To know more, please refer to the sample section below and\r
+                       <a href="http://docs.ckeditor.com/#!/guide/dev_advanced_content_filter">the official Advanced Content Filter guide</a>.\r
+               </p>\r
+               <p>\r
+                       You may, of course, want CKEditor to avoid filtering of any kind.\r
+                       To get rid of <abbr title="Advanced Content Filter">ACF</abbr>,\r
+                       basically set <code><a class="samples"\r
+                       href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-allowedContent">\r
+                       config.allowedContent</a></code> to <code>true</code> like this:\r
+               </p>\r
+<pre class="samples">\r
+CKEDITOR.replace( <em>textarea_id</em>, {\r
+       <strong>allowedContent: true</strong>\r
+} );\r
+</pre>\r
+\r
+               <h2>Beyond data flow: Features activation</h2>\r
+               <p>\r
+                       <abbr title="Advanced Content Filter">ACF</abbr> is far more than\r
+                       <abbr title="Input/Output">I/O</abbr> control: the entire\r
+                       <abbr title="User Interface">UI</abbr> of the editor is adjusted to what\r
+                       filters restrict. For example: if <code>&lt;a&gt;</code> tag is\r
+                       <strong>disallowed</strong>\r
+                       by <abbr title="Advanced Content Filter">ACF</abbr>,\r
+                       then accordingly <code>link</code> command, toolbar button and link dialog\r
+                       are also disabled. Editor is smart: it knows which features must be\r
+                       removed from the interface to match filtering rules.\r
+               </p>\r
+               <p>\r
+                       CKEditor can be far more specific. If <code>&lt;a&gt;</code> tag is\r
+                       <strong>allowed</strong> by filtering rules to be used but it is restricted\r
+                       to have only one attribute (<code>href</code>)\r
+                       <code>config.allowedContent = 'a[!href]'</code>, then\r
+                       "Target" tab of the link dialog is automatically disabled as <code>target</code>\r
+                       attribute isn't included in <abbr title="Advanced Content Filter">ACF</abbr> rules\r
+                       for <code>&lt;a&gt;</code>. This behaviour applies to dialog fields, context\r
+                       menus and toolbar buttons.\r
+               </p>\r
+\r
+               <h2>Sample configurations</h2>\r
+               <p>\r
+                       There are several editor instances below that present different\r
+                       <abbr title="Advanced Content Filter">ACF</abbr> setups. <strong>All of them,\r
+                       except the inline instance, share the same HTML content</strong> to visualize\r
+                       how different filtering rules affect the same input data.\r
+               </p>\r
+       </div>\r
+\r
+       <div>\r
+               <label for="editor1">\r
+                       Editor 1:\r
+               </label>\r
+               <div class="description">\r
+                       <p>\r
+                               This editor is using default configuration ("automatic mode"). It means that\r
+                               <code><a class="samples"\r
+                               href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-allowedContent">\r
+                               config.allowedContent</a></code> is defined by loaded plugins.\r
+                               Each plugin extends filtering rules to make it's own associated content\r
+                               available for the user.\r
+                       </p>\r
+               </div>\r
+               <textarea cols="80" id="editor1" name="editor1" rows="10">\r
+                       &lt;h1&gt;&lt;img alt=&quot;Saturn V carrying Apollo 11&quot; class=&quot;right&quot; src=&quot;assets/sample.jpg&quot;/&gt; Apollo 11&lt;/h1&gt; &lt;p&gt;&lt;b&gt;Apollo 11&lt;/b&gt; was the spaceflight that landed the first humans, Americans &lt;a href=&quot;http://en.wikipedia.org/wiki/Neil_Armstrong&quot; title=&quot;Neil Armstrong&quot;&gt;Neil Armstrong&lt;/a&gt; and &lt;a href=&quot;http://en.wikipedia.org/wiki/Buzz_Aldrin&quot; title=&quot;Buzz Aldrin&quot;&gt;Buzz Aldrin&lt;/a&gt;, on the Moon on July 20, 1969, at 20:18 UTC. Armstrong became the first to step onto the lunar surface 6 hours later on July 21 at 02:56 UTC.&lt;/p&gt; &lt;p&gt;Armstrong spent about &lt;s&gt;three and a half&lt;/s&gt; two and a half hours outside the spacecraft, Aldrin slightly less; and together they collected 47.5 pounds (21.5&amp;nbsp;kg) of lunar material for return to Earth. A third member of the mission, &lt;a href=&quot;http://en.wikipedia.org/wiki/Michael_Collins_(astronaut)&quot; title=&quot;Michael Collins (astronaut)&quot;&gt;Michael Collins&lt;/a&gt;, piloted the &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_Command/Service_Module&quot; title=&quot;Apollo Command/Service Module&quot;&gt;command&lt;/a&gt; spacecraft alone in lunar orbit until Armstrong and Aldrin returned to it for the trip back to Earth.&lt;/p&gt; &lt;h2&gt;Broadcasting and &lt;em&gt;quotes&lt;/em&gt; &lt;a id=&quot;quotes&quot; name=&quot;quotes&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;p&gt;Broadcast on live TV to a world-wide audience, Armstrong stepped onto the lunar surface and described the event as:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;One small step for [a] man, one giant leap for mankind.&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;Apollo 11 effectively ended the &lt;a href=&quot;http://en.wikipedia.org/wiki/Space_Race&quot; title=&quot;Space Race&quot;&gt;Space Race&lt;/a&gt; and fulfilled a national goal proposed in 1961 by the late U.S. President &lt;a href=&quot;http://en.wikipedia.org/wiki/John_F._Kennedy&quot; title=&quot;John F. Kennedy&quot;&gt;John F. Kennedy&lt;/a&gt; in a speech before the United States Congress:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;[...] before this decade is out, of landing a man on the Moon and returning him safely to the Earth.&lt;/p&gt;&lt;/blockquote&gt; &lt;h2&gt;Technical details &lt;a id=&quot;tech-details&quot; name=&quot;tech-details&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;table align=&quot;right&quot; border=&quot;1&quot; bordercolor=&quot;#ccc&quot; cellpadding=&quot;5&quot; cellspacing=&quot;0&quot; style=&quot;border-collapse:collapse;margin:10px 0 10px 15px;&quot;&gt; &lt;caption&gt;&lt;strong&gt;Mission crew&lt;/strong&gt;&lt;/caption&gt; &lt;thead&gt; &lt;tr&gt; &lt;th scope=&quot;col&quot;&gt;Position&lt;/th&gt; &lt;th scope=&quot;col&quot;&gt;Astronaut&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;Commander&lt;/td&gt; &lt;td&gt;Neil A. Armstrong&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Command Module Pilot&lt;/td&gt; &lt;td&gt;Michael Collins&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Lunar Module Pilot&lt;/td&gt; &lt;td&gt;Edwin &amp;quot;Buzz&amp;quot; E. Aldrin, Jr.&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;Launched by a &lt;strong&gt;Saturn V&lt;/strong&gt; rocket from &lt;a href=&quot;http://en.wikipedia.org/wiki/Kennedy_Space_Center&quot; title=&quot;Kennedy Space Center&quot;&gt;Kennedy Space Center&lt;/a&gt; in Merritt Island, Florida on July 16, Apollo 11 was the fifth manned mission of &lt;a href=&quot;http://en.wikipedia.org/wiki/NASA&quot; title=&quot;NASA&quot;&gt;NASA&lt;/a&gt;&amp;#39;s Apollo program. The Apollo spacecraft had three parts:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;strong&gt;Command Module&lt;/strong&gt; with a cabin for the three astronauts which was the only part which landed back on Earth&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Service Module&lt;/strong&gt; which supported the Command Module with propulsion, electrical power, oxygen and water&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Lunar Module&lt;/strong&gt; for landing on the Moon.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;After being sent to the Moon by the Saturn V&amp;#39;s upper stage, the astronauts separated the spacecraft from it and travelled for three days until they entered into lunar orbit. Armstrong and Aldrin then moved into the Lunar Module and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Mare_Tranquillitatis&quot; title=&quot;Mare Tranquillitatis&quot;&gt;Sea of Tranquility&lt;/a&gt;. They stayed a total of about 21 and a half hours on the lunar surface. After lifting off in the upper part of the Lunar Module and rejoining Collins in the Command Module, they returned to Earth and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Pacific_Ocean&quot; title=&quot;Pacific Ocean&quot;&gt;Pacific Ocean&lt;/a&gt; on July 24.&lt;/p&gt; &lt;hr/&gt; &lt;p style=&quot;text-align: right;&quot;&gt;&lt;small&gt;Source: &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_11&quot;&gt;Wikipedia.org&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;\r
+               </textarea>\r
+\r
+               <script>\r
+\r
+                       CKEDITOR.replace( 'editor1' );\r
+\r
+               </script>\r
+       </div>\r
+\r
+       <br>\r
+\r
+       <div>\r
+               <label for="editor2">\r
+                       Editor 2:\r
+               </label>\r
+               <div class="description">\r
+                       <p>\r
+                               This editor is using a custom configuration for\r
+                               <abbr title="Advanced Content Filter">ACF</abbr>:\r
+                       </p>\r
+<pre class="samples">\r
+CKEDITOR.replace( 'editor2', {\r
+       allowedContent:\r
+               'h1 h2 h3 p blockquote strong em;' +\r
+               'a[!href];' +\r
+               'img(left,right)[!src,alt,width,height];' +\r
+               'table tr th td caption;' +\r
+               'span{!font-family};' +'\r
+               'span{!color};' +\r
+               'span(!marker);' +\r
+               'del ins'\r
+} );\r
+</pre>\r
+                       <p>\r
+                               The following rules may require additional explanation:\r
+                       </p>\r
+                       <ul>\r
+                               <li>\r
+                                       <code>h1 h2 h3 p blockquote strong em</code> - These tags\r
+                                       are accepted by the editor. Any tag attributes will be discarded.\r
+                               </li>\r
+                               <li>\r
+                                       <code>a[!href]</code> - <code>href</code> attribute is obligatory\r
+                                       for <code>&lt;a&gt;</code> tag. Tags without this attribute\r
+                                       are disarded. No other attribute will be accepted.\r
+                               </li>\r
+                               <li>\r
+                                       <code>img(left,right)[!src,alt,width,height]</code> - <code>src</code>\r
+                                       attribute is obligatory for <code>&lt;img&gt;</code> tag.\r
+                                       <code>alt</code>, <code>width</code>, <code>height</code>\r
+                                       and <code>class</code> attributes are accepted but\r
+                                       <code>class</code> must be either <code>class="left"</code>\r
+                                       or <code>class="right"</code>\r
+                               </li>\r
+                               <li>\r
+                                       <code>table tr th td caption</code> - These tags\r
+                                       are accepted by the editor. Any tag attributes will be discarded.\r
+                               </li>\r
+                               <li>\r
+                                       <code>span{!font-family}</code>, <code>span{!color}</code>,\r
+                                       <code>span(!marker)</code> - <code>&lt;span&gt;</code> tags\r
+                                       will be accepted if either <code>font-family</code> or\r
+                                       <code>color</code> style is set or <code>class="marker"</code>\r
+                                       is present.\r
+                               </li>\r
+                               <li>\r
+                                       <code>del ins</code> - These tags\r
+                                       are accepted by the editor. Any tag attributes will be discarded.\r
+                               </li>\r
+                       </ul>\r
+                       <p>\r
+                               Please note that <strong><abbr title="User Interface">UI</abbr> of the\r
+                               editor is different</strong>. It's a response to what happened to the filters.\r
+                               Since <code>text-align</code> isn't allowed, the align toolbar is gone.\r
+                               The same thing happened to subscript/superscript, strike, underline\r
+                               (<code>&lt;u&gt;</code>, <code>&lt;sub&gt;</code>, <code>&lt;sup&gt;</code>\r
+                               are disallowed by <code><a class="samples"\r
+                               href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-allowedContent">\r
+                               config.allowedContent</a></code>) and many other buttons.\r
+                       </p>\r
+               </div>\r
+               <textarea cols="80" id="editor2" name="editor2" rows="10">\r
+                       &lt;h1&gt;&lt;img alt=&quot;Saturn V carrying Apollo 11&quot; class=&quot;right&quot; src=&quot;assets/sample.jpg&quot;/&gt; Apollo 11&lt;/h1&gt; &lt;p&gt;&lt;b&gt;Apollo 11&lt;/b&gt; was the spaceflight that landed the first humans, Americans &lt;a href=&quot;http://en.wikipedia.org/wiki/Neil_Armstrong&quot; title=&quot;Neil Armstrong&quot;&gt;Neil Armstrong&lt;/a&gt; and &lt;a href=&quot;http://en.wikipedia.org/wiki/Buzz_Aldrin&quot; title=&quot;Buzz Aldrin&quot;&gt;Buzz Aldrin&lt;/a&gt;, on the Moon on July 20, 1969, at 20:18 UTC. Armstrong became the first to step onto the lunar surface 6 hours later on July 21 at 02:56 UTC.&lt;/p&gt; &lt;p&gt;Armstrong spent about &lt;s&gt;three and a half&lt;/s&gt; two and a half hours outside the spacecraft, Aldrin slightly less; and together they collected 47.5 pounds (21.5&amp;nbsp;kg) of lunar material for return to Earth. A third member of the mission, &lt;a href=&quot;http://en.wikipedia.org/wiki/Michael_Collins_(astronaut)&quot; title=&quot;Michael Collins (astronaut)&quot;&gt;Michael Collins&lt;/a&gt;, piloted the &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_Command/Service_Module&quot; title=&quot;Apollo Command/Service Module&quot;&gt;command&lt;/a&gt; spacecraft alone in lunar orbit until Armstrong and Aldrin returned to it for the trip back to Earth.&lt;/p&gt; &lt;h2&gt;Broadcasting and &lt;em&gt;quotes&lt;/em&gt; &lt;a id=&quot;quotes&quot; name=&quot;quotes&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;p&gt;Broadcast on live TV to a world-wide audience, Armstrong stepped onto the lunar surface and described the event as:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;One small step for [a] man, one giant leap for mankind.&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;Apollo 11 effectively ended the &lt;a href=&quot;http://en.wikipedia.org/wiki/Space_Race&quot; title=&quot;Space Race&quot;&gt;Space Race&lt;/a&gt; and fulfilled a national goal proposed in 1961 by the late U.S. President &lt;a href=&quot;http://en.wikipedia.org/wiki/John_F._Kennedy&quot; title=&quot;John F. Kennedy&quot;&gt;John F. Kennedy&lt;/a&gt; in a speech before the United States Congress:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;[...] before this decade is out, of landing a man on the Moon and returning him safely to the Earth.&lt;/p&gt;&lt;/blockquote&gt; &lt;h2&gt;Technical details &lt;a id=&quot;tech-details&quot; name=&quot;tech-details&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;table align=&quot;right&quot; border=&quot;1&quot; bordercolor=&quot;#ccc&quot; cellpadding=&quot;5&quot; cellspacing=&quot;0&quot; style=&quot;border-collapse:collapse;margin:10px 0 10px 15px;&quot;&gt; &lt;caption&gt;&lt;strong&gt;Mission crew&lt;/strong&gt;&lt;/caption&gt; &lt;thead&gt; &lt;tr&gt; &lt;th scope=&quot;col&quot;&gt;Position&lt;/th&gt; &lt;th scope=&quot;col&quot;&gt;Astronaut&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;Commander&lt;/td&gt; &lt;td&gt;Neil A. Armstrong&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Command Module Pilot&lt;/td&gt; &lt;td&gt;Michael Collins&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Lunar Module Pilot&lt;/td&gt; &lt;td&gt;Edwin &amp;quot;Buzz&amp;quot; E. Aldrin, Jr.&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;Launched by a &lt;strong&gt;Saturn V&lt;/strong&gt; rocket from &lt;a href=&quot;http://en.wikipedia.org/wiki/Kennedy_Space_Center&quot; title=&quot;Kennedy Space Center&quot;&gt;Kennedy Space Center&lt;/a&gt; in Merritt Island, Florida on July 16, Apollo 11 was the fifth manned mission of &lt;a href=&quot;http://en.wikipedia.org/wiki/NASA&quot; title=&quot;NASA&quot;&gt;NASA&lt;/a&gt;&amp;#39;s Apollo program. The Apollo spacecraft had three parts:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;strong&gt;Command Module&lt;/strong&gt; with a cabin for the three astronauts which was the only part which landed back on Earth&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Service Module&lt;/strong&gt; which supported the Command Module with propulsion, electrical power, oxygen and water&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Lunar Module&lt;/strong&gt; for landing on the Moon.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;After being sent to the Moon by the Saturn V&amp;#39;s upper stage, the astronauts separated the spacecraft from it and travelled for three days until they entered into lunar orbit. Armstrong and Aldrin then moved into the Lunar Module and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Mare_Tranquillitatis&quot; title=&quot;Mare Tranquillitatis&quot;&gt;Sea of Tranquility&lt;/a&gt;. They stayed a total of about 21 and a half hours on the lunar surface. After lifting off in the upper part of the Lunar Module and rejoining Collins in the Command Module, they returned to Earth and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Pacific_Ocean&quot; title=&quot;Pacific Ocean&quot;&gt;Pacific Ocean&lt;/a&gt; on July 24.&lt;/p&gt; &lt;hr/&gt; &lt;p style=&quot;text-align: right;&quot;&gt;&lt;small&gt;Source: &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_11&quot;&gt;Wikipedia.org&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;\r
+               </textarea>\r
+               <script>\r
+\r
+                       CKEDITOR.replace( 'editor2', {\r
+                               allowedContent:\r
+                                       'h1 h2 h3 p blockquote strong em;' +\r
+                                       'a[!href];' +\r
+                                       'img(left,right)[!src,alt,width,height];' +\r
+                                       'table tr th td caption;' +\r
+                                       'span{!font-family};' +\r
+                                       'span{!color};' +\r
+                                       'span(!marker);' +\r
+                                       'del ins'\r
+                       } );\r
+\r
+               </script>\r
+       </div>\r
+\r
+       <br>\r
+\r
+       <div>\r
+               <label for="editor3">\r
+                       Editor 3:\r
+               </label>\r
+               <div class="description">\r
+                       <p>\r
+                               This editor is using a custom configuration for\r
+                               <abbr title="Advanced Content Filter">ACF</abbr>.\r
+                               Note that filters can be configured as an object literal\r
+                               as an alternative to a string-based definition.\r
+                       </p>\r
+<pre class="samples">\r
+CKEDITOR.replace( 'editor3', {\r
+       allowedContent: {\r
+               'b i ul ol big small': true,\r
+               'h1 h2 h3 p blockquote li': {\r
+                       styles: 'text-align'\r
+               },\r
+               a: { attributes: '!href,target' },\r
+               img: {\r
+                       attributes: '!src,alt',\r
+                       styles: 'width,height',\r
+                       classes: 'left,right'\r
+               }\r
+       }\r
+} );\r
+</pre>\r
+               </div>\r
+               <textarea cols="80" id="editor3" name="editor3" rows="10">\r
+                       &lt;h1&gt;&lt;img alt=&quot;Saturn V carrying Apollo 11&quot; class=&quot;right&quot; src=&quot;assets/sample.jpg&quot;/&gt; Apollo 11&lt;/h1&gt; &lt;p&gt;&lt;b&gt;Apollo 11&lt;/b&gt; was the spaceflight that landed the first humans, Americans &lt;a href=&quot;http://en.wikipedia.org/wiki/Neil_Armstrong&quot; title=&quot;Neil Armstrong&quot;&gt;Neil Armstrong&lt;/a&gt; and &lt;a href=&quot;http://en.wikipedia.org/wiki/Buzz_Aldrin&quot; title=&quot;Buzz Aldrin&quot;&gt;Buzz Aldrin&lt;/a&gt;, on the Moon on July 20, 1969, at 20:18 UTC. Armstrong became the first to step onto the lunar surface 6 hours later on July 21 at 02:56 UTC.&lt;/p&gt; &lt;p&gt;Armstrong spent about &lt;s&gt;three and a half&lt;/s&gt; two and a half hours outside the spacecraft, Aldrin slightly less; and together they collected 47.5 pounds (21.5&amp;nbsp;kg) of lunar material for return to Earth. A third member of the mission, &lt;a href=&quot;http://en.wikipedia.org/wiki/Michael_Collins_(astronaut)&quot; title=&quot;Michael Collins (astronaut)&quot;&gt;Michael Collins&lt;/a&gt;, piloted the &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_Command/Service_Module&quot; title=&quot;Apollo Command/Service Module&quot;&gt;command&lt;/a&gt; spacecraft alone in lunar orbit until Armstrong and Aldrin returned to it for the trip back to Earth.&lt;/p&gt; &lt;h2&gt;Broadcasting and &lt;em&gt;quotes&lt;/em&gt; &lt;a id=&quot;quotes&quot; name=&quot;quotes&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;p&gt;Broadcast on live TV to a world-wide audience, Armstrong stepped onto the lunar surface and described the event as:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;One small step for [a] man, one giant leap for mankind.&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;Apollo 11 effectively ended the &lt;a href=&quot;http://en.wikipedia.org/wiki/Space_Race&quot; title=&quot;Space Race&quot;&gt;Space Race&lt;/a&gt; and fulfilled a national goal proposed in 1961 by the late U.S. President &lt;a href=&quot;http://en.wikipedia.org/wiki/John_F._Kennedy&quot; title=&quot;John F. Kennedy&quot;&gt;John F. Kennedy&lt;/a&gt; in a speech before the United States Congress:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;[...] before this decade is out, of landing a man on the Moon and returning him safely to the Earth.&lt;/p&gt;&lt;/blockquote&gt; &lt;h2&gt;Technical details &lt;a id=&quot;tech-details&quot; name=&quot;tech-details&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;table align=&quot;right&quot; border=&quot;1&quot; bordercolor=&quot;#ccc&quot; cellpadding=&quot;5&quot; cellspacing=&quot;0&quot; style=&quot;border-collapse:collapse;margin:10px 0 10px 15px;&quot;&gt; &lt;caption&gt;&lt;strong&gt;Mission crew&lt;/strong&gt;&lt;/caption&gt; &lt;thead&gt; &lt;tr&gt; &lt;th scope=&quot;col&quot;&gt;Position&lt;/th&gt; &lt;th scope=&quot;col&quot;&gt;Astronaut&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;Commander&lt;/td&gt; &lt;td&gt;Neil A. Armstrong&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Command Module Pilot&lt;/td&gt; &lt;td&gt;Michael Collins&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Lunar Module Pilot&lt;/td&gt; &lt;td&gt;Edwin &amp;quot;Buzz&amp;quot; E. Aldrin, Jr.&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;Launched by a &lt;strong&gt;Saturn V&lt;/strong&gt; rocket from &lt;a href=&quot;http://en.wikipedia.org/wiki/Kennedy_Space_Center&quot; title=&quot;Kennedy Space Center&quot;&gt;Kennedy Space Center&lt;/a&gt; in Merritt Island, Florida on July 16, Apollo 11 was the fifth manned mission of &lt;a href=&quot;http://en.wikipedia.org/wiki/NASA&quot; title=&quot;NASA&quot;&gt;NASA&lt;/a&gt;&amp;#39;s Apollo program. The Apollo spacecraft had three parts:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;strong&gt;Command Module&lt;/strong&gt; with a cabin for the three astronauts which was the only part which landed back on Earth&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Service Module&lt;/strong&gt; which supported the Command Module with propulsion, electrical power, oxygen and water&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Lunar Module&lt;/strong&gt; for landing on the Moon.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;After being sent to the Moon by the Saturn V&amp;#39;s upper stage, the astronauts separated the spacecraft from it and travelled for three days until they entered into lunar orbit. Armstrong and Aldrin then moved into the Lunar Module and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Mare_Tranquillitatis&quot; title=&quot;Mare Tranquillitatis&quot;&gt;Sea of Tranquility&lt;/a&gt;. They stayed a total of about 21 and a half hours on the lunar surface. After lifting off in the upper part of the Lunar Module and rejoining Collins in the Command Module, they returned to Earth and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Pacific_Ocean&quot; title=&quot;Pacific Ocean&quot;&gt;Pacific Ocean&lt;/a&gt; on July 24.&lt;/p&gt; &lt;hr/&gt; &lt;p style=&quot;text-align: right;&quot;&gt;&lt;small&gt;Source: &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_11&quot;&gt;Wikipedia.org&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;\r
+               </textarea>\r
+               <script>\r
+\r
+                       CKEDITOR.replace( 'editor3', {\r
+                               allowedContent: {\r
+                                       'b i ul ol big small': true,\r
+                                       'h1 h2 h3 p blockquote li': {\r
+                                               styles: 'text-align'\r
+                                       },\r
+                                       a: { attributes: '!href,target' },\r
+                                       img: {\r
+                                               attributes: '!src,alt',\r
+                                               styles: 'width,height',\r
+                                               classes: 'left,right'\r
+                                       }\r
+                               }\r
+                       } );\r
+\r
+               </script>\r
+       </div>\r
+\r
+       <br>\r
+\r
+       <div>\r
+               <label for="editor4">\r
+                       Editor 4:\r
+               </label>\r
+               <div class="description">\r
+                       <p>\r
+                               This editor is using a custom set of plugins and buttons.\r
+                       </p>\r
+<pre class="samples">\r
+CKEDITOR.replace( 'editor4', {\r
+       removePlugins: 'bidi,font,forms,flash,horizontalrule,iframe,justify,table,tabletools,smiley',\r
+       removeButtons: 'Anchor,Underline,Strike,Subscript,Superscript,Image',\r
+       format_tags: 'p;h1;h2;h3;pre;address'\r
+} );\r
+</pre>\r
+                       <p>\r
+                               As you can see, removing plugins and buttons implies filtering.\r
+                               Several tags are not allowed in the editor because there's no\r
+                               plugin/button that is responsible for creating and editing this\r
+                               kind of content (for example: the image is missing because\r
+                               of <code>removeButtons: 'Image'</code>). The conclusion is that\r
+                               <abbr title="Advanced Content Filter">ACF</abbr> works "backwards"\r
+                               as well: <strong>modifying <abbr title="User Interface">UI</abbr>\r
+                               elements is changing allowed content rules</strong>.\r
+                       </p>\r
+               </div>\r
+               <textarea cols="80" id="editor4" name="editor4" rows="10">\r
+                       &lt;h1&gt;&lt;img alt=&quot;Saturn V carrying Apollo 11&quot; class=&quot;right&quot; src=&quot;assets/sample.jpg&quot;/&gt; Apollo 11&lt;/h1&gt; &lt;p&gt;&lt;b&gt;Apollo 11&lt;/b&gt; was the spaceflight that landed the first humans, Americans &lt;a href=&quot;http://en.wikipedia.org/wiki/Neil_Armstrong&quot; title=&quot;Neil Armstrong&quot;&gt;Neil Armstrong&lt;/a&gt; and &lt;a href=&quot;http://en.wikipedia.org/wiki/Buzz_Aldrin&quot; title=&quot;Buzz Aldrin&quot;&gt;Buzz Aldrin&lt;/a&gt;, on the Moon on July 20, 1969, at 20:18 UTC. Armstrong became the first to step onto the lunar surface 6 hours later on July 21 at 02:56 UTC.&lt;/p&gt; &lt;p&gt;Armstrong spent about &lt;s&gt;three and a half&lt;/s&gt; two and a half hours outside the spacecraft, Aldrin slightly less; and together they collected 47.5 pounds (21.5&amp;nbsp;kg) of lunar material for return to Earth. A third member of the mission, &lt;a href=&quot;http://en.wikipedia.org/wiki/Michael_Collins_(astronaut)&quot; title=&quot;Michael Collins (astronaut)&quot;&gt;Michael Collins&lt;/a&gt;, piloted the &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_Command/Service_Module&quot; title=&quot;Apollo Command/Service Module&quot;&gt;command&lt;/a&gt; spacecraft alone in lunar orbit until Armstrong and Aldrin returned to it for the trip back to Earth.&lt;/p&gt; &lt;h2&gt;Broadcasting and &lt;em&gt;quotes&lt;/em&gt; &lt;a id=&quot;quotes&quot; name=&quot;quotes&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;p&gt;Broadcast on live TV to a world-wide audience, Armstrong stepped onto the lunar surface and described the event as:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;One small step for [a] man, one giant leap for mankind.&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;Apollo 11 effectively ended the &lt;a href=&quot;http://en.wikipedia.org/wiki/Space_Race&quot; title=&quot;Space Race&quot;&gt;Space Race&lt;/a&gt; and fulfilled a national goal proposed in 1961 by the late U.S. President &lt;a href=&quot;http://en.wikipedia.org/wiki/John_F._Kennedy&quot; title=&quot;John F. Kennedy&quot;&gt;John F. Kennedy&lt;/a&gt; in a speech before the United States Congress:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;[...] before this decade is out, of landing a man on the Moon and returning him safely to the Earth.&lt;/p&gt;&lt;/blockquote&gt; &lt;h2&gt;Technical details &lt;a id=&quot;tech-details&quot; name=&quot;tech-details&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;table align=&quot;right&quot; border=&quot;1&quot; bordercolor=&quot;#ccc&quot; cellpadding=&quot;5&quot; cellspacing=&quot;0&quot; style=&quot;border-collapse:collapse;margin:10px 0 10px 15px;&quot;&gt; &lt;caption&gt;&lt;strong&gt;Mission crew&lt;/strong&gt;&lt;/caption&gt; &lt;thead&gt; &lt;tr&gt; &lt;th scope=&quot;col&quot;&gt;Position&lt;/th&gt; &lt;th scope=&quot;col&quot;&gt;Astronaut&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;Commander&lt;/td&gt; &lt;td&gt;Neil A. Armstrong&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Command Module Pilot&lt;/td&gt; &lt;td&gt;Michael Collins&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Lunar Module Pilot&lt;/td&gt; &lt;td&gt;Edwin &amp;quot;Buzz&amp;quot; E. Aldrin, Jr.&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;Launched by a &lt;strong&gt;Saturn V&lt;/strong&gt; rocket from &lt;a href=&quot;http://en.wikipedia.org/wiki/Kennedy_Space_Center&quot; title=&quot;Kennedy Space Center&quot;&gt;Kennedy Space Center&lt;/a&gt; in Merritt Island, Florida on July 16, Apollo 11 was the fifth manned mission of &lt;a href=&quot;http://en.wikipedia.org/wiki/NASA&quot; title=&quot;NASA&quot;&gt;NASA&lt;/a&gt;&amp;#39;s Apollo program. The Apollo spacecraft had three parts:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;strong&gt;Command Module&lt;/strong&gt; with a cabin for the three astronauts which was the only part which landed back on Earth&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Service Module&lt;/strong&gt; which supported the Command Module with propulsion, electrical power, oxygen and water&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Lunar Module&lt;/strong&gt; for landing on the Moon.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;After being sent to the Moon by the Saturn V&amp;#39;s upper stage, the astronauts separated the spacecraft from it and travelled for three days until they entered into lunar orbit. Armstrong and Aldrin then moved into the Lunar Module and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Mare_Tranquillitatis&quot; title=&quot;Mare Tranquillitatis&quot;&gt;Sea of Tranquility&lt;/a&gt;. They stayed a total of about 21 and a half hours on the lunar surface. After lifting off in the upper part of the Lunar Module and rejoining Collins in the Command Module, they returned to Earth and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Pacific_Ocean&quot; title=&quot;Pacific Ocean&quot;&gt;Pacific Ocean&lt;/a&gt; on July 24.&lt;/p&gt; &lt;hr/&gt; &lt;p style=&quot;text-align: right;&quot;&gt;&lt;small&gt;Source: &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_11&quot;&gt;Wikipedia.org&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;\r
+               </textarea>\r
+               <script>\r
+\r
+                       CKEDITOR.replace( 'editor4', {\r
+                               removePlugins: 'bidi,div,font,forms,flash,horizontalrule,iframe,justify,table,tabletools,smiley',\r
+                               removeButtons: 'Anchor,Underline,Strike,Subscript,Superscript,Image',\r
+                               format_tags: 'p;h1;h2;h3;pre;address'\r
+                       } );\r
+\r
+               </script>\r
+       </div>\r
+\r
+       <br>\r
+\r
+       <div>\r
+               <label for="editor5">\r
+                       Editor 5:\r
+               </label>\r
+               <div class="description">\r
+                       <p>\r
+                               This editor is built on editable <code>&lt;h1&gt;</code> element.\r
+                               <abbr title="Advanced Content Filter">ACF</abbr> takes care of\r
+                               what can be included in <code>&lt;h1&gt;</code>. Note that there\r
+                               are no block styles in Styles combo. Also why lists, indentation,\r
+                               blockquote, div, form and other buttons are missing.\r
+                       </p>\r
+                       <p>\r
+                               <abbr title="Advanced Content Filter">ACF</abbr> makes sure that\r
+                               no disallowed tags will come to <code>&lt;h1&gt;</code> so the final\r
+                               markup is valid. If the user tried to paste some invalid HTML\r
+                               into this editor (let's say a list), it would be automatically\r
+                               converted into plain text.\r
+                       </p>\r
+               </div>\r
+               <h1 id="editor5" contenteditable="true">\r
+                       <em>Apollo 11</em> was the spaceflight that landed the first humans, Americans <a href="http://en.wikipedia.org/wiki/Neil_Armstrong" title="Neil Armstrong">Neil Armstrong</a> and <a href="http://en.wikipedia.org/wiki/Buzz_Aldrin" title="Buzz Aldrin">Buzz Aldrin</a>, on the Moon on July 20, 1969, at 20:18 UTC.\r
+               </h1>\r
+       </div>\r
+\r
+       <br>\r
+\r
+       <div>\r
+               <label for="editor3">\r
+                       Editor 6:\r
+               </label>\r
+               <div class="description">\r
+                       <p>\r
+                               This editor is using a custom configuration for <abbr title="Advanced Content Filter">ACF</abbr>.\r
+                               It's using the <a href="http://docs.ckeditor.com/#!/guide/dev_disallowed_content" rel="noopener noreferrer" target="_blank">\r
+                               Disallowed Content</a> property of the filter to eliminate all <code>title</code> attributes.\r
+                       </p>\r
+\r
+<pre class="samples">\r
+CKEDITOR.replace( 'editor6', {\r
+       allowedContent: {\r
+               'b i ul ol big small': true,\r
+               'h1 h2 h3 p blockquote li': {\r
+                       styles: 'text-align'\r
+               },\r
+               a: {attributes: '!href,target'},\r
+               img: {\r
+                       attributes: '!src,alt',\r
+                       styles: 'width,height',\r
+                       classes: 'left,right'\r
+               }\r
+       },\r
+       disallowedContent: '*{title*}'\r
+} );\r
+</pre>\r
+               </div>\r
+               <textarea cols="80" id="editor6" name="editor6" rows="10">\r
+                       &lt;h1&gt;&lt;img alt=&quot;Saturn V carrying Apollo 11&quot; class=&quot;right&quot; src=&quot;assets/sample.jpg&quot;/&gt; Apollo 11&lt;/h1&gt; &lt;p&gt;&lt;b&gt;Apollo 11&lt;/b&gt; was the spaceflight that landed the first humans, Americans &lt;a href=&quot;http://en.wikipedia.org/wiki/Neil_Armstrong&quot; title=&quot;Neil Armstrong&quot;&gt;Neil Armstrong&lt;/a&gt; and &lt;a href=&quot;http://en.wikipedia.org/wiki/Buzz_Aldrin&quot; title=&quot;Buzz Aldrin&quot;&gt;Buzz Aldrin&lt;/a&gt;, on the Moon on July 20, 1969, at 20:18 UTC. Armstrong became the first to step onto the lunar surface 6 hours later on July 21 at 02:56 UTC.&lt;/p&gt; &lt;p&gt;Armstrong spent about &lt;s&gt;three and a half&lt;/s&gt; two and a half hours outside the spacecraft, Aldrin slightly less; and together they collected 47.5 pounds (21.5&amp;nbsp;kg) of lunar material for return to Earth. A third member of the mission, &lt;a href=&quot;http://en.wikipedia.org/wiki/Michael_Collins_(astronaut)&quot; title=&quot;Michael Collins (astronaut)&quot;&gt;Michael Collins&lt;/a&gt;, piloted the &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_Command/Service_Module&quot; title=&quot;Apollo Command/Service Module&quot;&gt;command&lt;/a&gt; spacecraft alone in lunar orbit until Armstrong and Aldrin returned to it for the trip back to Earth.&lt;/p&gt; &lt;h2&gt;Broadcasting and &lt;em&gt;quotes&lt;/em&gt; &lt;a id=&quot;quotes&quot; name=&quot;quotes&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;p&gt;Broadcast on live TV to a world-wide audience, Armstrong stepped onto the lunar surface and described the event as:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;One small step for [a] man, one giant leap for mankind.&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;Apollo 11 effectively ended the &lt;a href=&quot;http://en.wikipedia.org/wiki/Space_Race&quot; title=&quot;Space Race&quot;&gt;Space Race&lt;/a&gt; and fulfilled a national goal proposed in 1961 by the late U.S. President &lt;a href=&quot;http://en.wikipedia.org/wiki/John_F._Kennedy&quot; title=&quot;John F. Kennedy&quot;&gt;John F. Kennedy&lt;/a&gt; in a speech before the United States Congress:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;[...] before this decade is out, of landing a man on the Moon and returning him safely to the Earth.&lt;/p&gt;&lt;/blockquote&gt; &lt;h2&gt;Technical details &lt;a id=&quot;tech-details&quot; name=&quot;tech-details&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;table align=&quot;right&quot; border=&quot;1&quot; bordercolor=&quot;#ccc&quot; cellpadding=&quot;5&quot; cellspacing=&quot;0&quot; style=&quot;border-collapse:collapse;margin:10px 0 10px 15px;&quot;&gt; &lt;caption&gt;&lt;strong&gt;Mission crew&lt;/strong&gt;&lt;/caption&gt; &lt;thead&gt; &lt;tr&gt; &lt;th scope=&quot;col&quot;&gt;Position&lt;/th&gt; &lt;th scope=&quot;col&quot;&gt;Astronaut&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;Commander&lt;/td&gt; &lt;td&gt;Neil A. Armstrong&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Command Module Pilot&lt;/td&gt; &lt;td&gt;Michael Collins&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Lunar Module Pilot&lt;/td&gt; &lt;td&gt;Edwin &amp;quot;Buzz&amp;quot; E. Aldrin, Jr.&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;Launched by a &lt;strong&gt;Saturn V&lt;/strong&gt; rocket from &lt;a href=&quot;http://en.wikipedia.org/wiki/Kennedy_Space_Center&quot; title=&quot;Kennedy Space Center&quot;&gt;Kennedy Space Center&lt;/a&gt; in Merritt Island, Florida on July 16, Apollo 11 was the fifth manned mission of &lt;a href=&quot;http://en.wikipedia.org/wiki/NASA&quot; title=&quot;NASA&quot;&gt;NASA&lt;/a&gt;&amp;#39;s Apollo program. The Apollo spacecraft had three parts:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;strong&gt;Command Module&lt;/strong&gt; with a cabin for the three astronauts which was the only part which landed back on Earth&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Service Module&lt;/strong&gt; which supported the Command Module with propulsion, electrical power, oxygen and water&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Lunar Module&lt;/strong&gt; for landing on the Moon.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;After being sent to the Moon by the Saturn V&amp;#39;s upper stage, the astronauts separated the spacecraft from it and travelled for three days until they entered into lunar orbit. Armstrong and Aldrin then moved into the Lunar Module and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Mare_Tranquillitatis&quot; title=&quot;Mare Tranquillitatis&quot;&gt;Sea of Tranquility&lt;/a&gt;. They stayed a total of about 21 and a half hours on the lunar surface. After lifting off in the upper part of the Lunar Module and rejoining Collins in the Command Module, they returned to Earth and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Pacific_Ocean&quot; title=&quot;Pacific Ocean&quot;&gt;Pacific Ocean&lt;/a&gt; on July 24.&lt;/p&gt; &lt;hr/&gt; &lt;p style=&quot;text-align: right;&quot;&gt;&lt;small&gt;Source: &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_11&quot;&gt;Wikipedia.org&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;\r
+               </textarea>\r
+               <script>\r
+\r
+                       CKEDITOR.replace( 'editor6', {\r
+                               allowedContent: {\r
+                                       'b i ul ol big small': true,\r
+                                       'h1 h2 h3 p blockquote li': {\r
+                                               styles: 'text-align'\r
+                                       },\r
+                                       a: {attributes: '!href,target'},\r
+                                       img: {\r
+                                               attributes: '!src,alt',\r
+                                               styles: 'width,height',\r
+                                               classes: 'left,right'\r
+                                       }\r
+                               },\r
+                               disallowedContent: '*{title*}'\r
+                       } );\r
+\r
+               </script>\r
+       </div>\r
+\r
+       <br>\r
+\r
+       <div>\r
+               <label for="editor7">\r
+                       Editor 7:\r
+               </label>\r
+               <div class="description">\r
+                       <p>\r
+                               This editor is using a custom configuration for <abbr title="Advanced Content Filter">ACF</abbr>.\r
+                               It's using the <a href="http://docs.ckeditor.com/#!/guide/dev_disallowed_content" rel="noopener noreferrer" target="_blank">\r
+                               Disallowed Content</a> property of the filter to eliminate all <code>a</code> and <code>img</code> tags,\r
+                               while allowing all other tags.\r
+                       </p>\r
+<pre class="samples">\r
+CKEDITOR.replace( 'editor7', {\r
+       allowedContent: {\r
+               // Allow all content.\r
+               $1: {\r
+                       elements: CKEDITOR.dtd,\r
+                       attributes: true,\r
+                       styles: true,\r
+                       classes: true\r
+               }\r
+       },\r
+       disallowedContent: 'img a'\r
+} );\r
+</pre>\r
+               </div>\r
+               <textarea cols="80" id="editor7" name="editor7" rows="10">\r
+                       &lt;h1&gt;&lt;img alt=&quot;Saturn V carrying Apollo 11&quot; class=&quot;right&quot; src=&quot;assets/sample.jpg&quot;/&gt; Apollo 11&lt;/h1&gt; &lt;p&gt;&lt;b&gt;Apollo 11&lt;/b&gt; was the spaceflight that landed the first humans, Americans &lt;a href=&quot;http://en.wikipedia.org/wiki/Neil_Armstrong&quot; title=&quot;Neil Armstrong&quot;&gt;Neil Armstrong&lt;/a&gt; and &lt;a href=&quot;http://en.wikipedia.org/wiki/Buzz_Aldrin&quot; title=&quot;Buzz Aldrin&quot;&gt;Buzz Aldrin&lt;/a&gt;, on the Moon on July 20, 1969, at 20:18 UTC. Armstrong became the first to step onto the lunar surface 6 hours later on July 21 at 02:56 UTC.&lt;/p&gt; &lt;p&gt;Armstrong spent about &lt;s&gt;three and a half&lt;/s&gt; two and a half hours outside the spacecraft, Aldrin slightly less; and together they collected 47.5 pounds (21.5&amp;nbsp;kg) of lunar material for return to Earth. A third member of the mission, &lt;a href=&quot;http://en.wikipedia.org/wiki/Michael_Collins_(astronaut)&quot; title=&quot;Michael Collins (astronaut)&quot;&gt;Michael Collins&lt;/a&gt;, piloted the &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_Command/Service_Module&quot; title=&quot;Apollo Command/Service Module&quot;&gt;command&lt;/a&gt; spacecraft alone in lunar orbit until Armstrong and Aldrin returned to it for the trip back to Earth.&lt;/p&gt; &lt;h2&gt;Broadcasting and &lt;em&gt;quotes&lt;/em&gt; &lt;a id=&quot;quotes&quot; name=&quot;quotes&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;p&gt;Broadcast on live TV to a world-wide audience, Armstrong stepped onto the lunar surface and described the event as:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;One small step for [a] man, one giant leap for mankind.&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;Apollo 11 effectively ended the &lt;a href=&quot;http://en.wikipedia.org/wiki/Space_Race&quot; title=&quot;Space Race&quot;&gt;Space Race&lt;/a&gt; and fulfilled a national goal proposed in 1961 by the late U.S. President &lt;a href=&quot;http://en.wikipedia.org/wiki/John_F._Kennedy&quot; title=&quot;John F. Kennedy&quot;&gt;John F. Kennedy&lt;/a&gt; in a speech before the United States Congress:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;[...] before this decade is out, of landing a man on the Moon and returning him safely to the Earth.&lt;/p&gt;&lt;/blockquote&gt; &lt;h2&gt;Technical details &lt;a id=&quot;tech-details&quot; name=&quot;tech-details&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;table align=&quot;right&quot; border=&quot;1&quot; bordercolor=&quot;#ccc&quot; cellpadding=&quot;5&quot; cellspacing=&quot;0&quot; style=&quot;border-collapse:collapse;margin:10px 0 10px 15px;&quot;&gt; &lt;caption&gt;&lt;strong&gt;Mission crew&lt;/strong&gt;&lt;/caption&gt; &lt;thead&gt; &lt;tr&gt; &lt;th scope=&quot;col&quot;&gt;Position&lt;/th&gt; &lt;th scope=&quot;col&quot;&gt;Astronaut&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;Commander&lt;/td&gt; &lt;td&gt;Neil A. Armstrong&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Command Module Pilot&lt;/td&gt; &lt;td&gt;Michael Collins&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Lunar Module Pilot&lt;/td&gt; &lt;td&gt;Edwin &amp;quot;Buzz&amp;quot; E. Aldrin, Jr.&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;Launched by a &lt;strong&gt;Saturn V&lt;/strong&gt; rocket from &lt;a href=&quot;http://en.wikipedia.org/wiki/Kennedy_Space_Center&quot; title=&quot;Kennedy Space Center&quot;&gt;Kennedy Space Center&lt;/a&gt; in Merritt Island, Florida on July 16, Apollo 11 was the fifth manned mission of &lt;a href=&quot;http://en.wikipedia.org/wiki/NASA&quot; title=&quot;NASA&quot;&gt;NASA&lt;/a&gt;&amp;#39;s Apollo program. The Apollo spacecraft had three parts:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;strong&gt;Command Module&lt;/strong&gt; with a cabin for the three astronauts which was the only part which landed back on Earth&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Service Module&lt;/strong&gt; which supported the Command Module with propulsion, electrical power, oxygen and water&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Lunar Module&lt;/strong&gt; for landing on the Moon.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;After being sent to the Moon by the Saturn V&amp;#39;s upper stage, the astronauts separated the spacecraft from it and travelled for three days until they entered into lunar orbit. Armstrong and Aldrin then moved into the Lunar Module and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Mare_Tranquillitatis&quot; title=&quot;Mare Tranquillitatis&quot;&gt;Sea of Tranquility&lt;/a&gt;. They stayed a total of about 21 and a half hours on the lunar surface. After lifting off in the upper part of the Lunar Module and rejoining Collins in the Command Module, they returned to Earth and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Pacific_Ocean&quot; title=&quot;Pacific Ocean&quot;&gt;Pacific Ocean&lt;/a&gt; on July 24.&lt;/p&gt; &lt;hr/&gt; &lt;p style=&quot;text-align: right;&quot;&gt;&lt;small&gt;Source: &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_11&quot;&gt;Wikipedia.org&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;\r
+               </textarea>\r
+               <script>\r
+\r
+                       CKEDITOR.replace( 'editor7', {\r
+                               allowedContent: {\r
+                                       // allow all content\r
+                                       $1: {\r
+                                               elements: CKEDITOR.dtd,\r
+                                               attributes: true,\r
+                                               styles: true,\r
+                                               classes: true\r
+                                       }\r
+                               },\r
+                               disallowedContent: 'img a'\r
+                       } );\r
+\r
+               </script>\r
+       </div>\r
+\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/dialog/assets/my_dialog.js b/js/ckeditor/samples/old/dialog/assets/my_dialog.js
new file mode 100644 (file)
index 0000000..fcd1a43
--- /dev/null
@@ -0,0 +1,48 @@
+/**\r
+ * Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+ * For licensing, see LICENSE.md or http://ckeditor.com/license\r
+ */\r
+\r
+CKEDITOR.dialog.add( 'myDialog', function() {\r
+       return {\r
+               title: 'My Dialog',\r
+               minWidth: 400,\r
+               minHeight: 200,\r
+               contents: [\r
+                       {\r
+                               id: 'tab1',\r
+                               label: 'First Tab',\r
+                               title: 'First Tab',\r
+                               elements: [\r
+                                       {\r
+                                               id: 'input1',\r
+                                               type: 'text',\r
+                                               label: 'Text Field'\r
+                                       },\r
+                                       {\r
+                                               id: 'select1',\r
+                                               type: 'select',\r
+                                               label: 'Select Field',\r
+                                               items: [\r
+                                                       [ 'option1', 'value1' ],\r
+                                                       [ 'option2', 'value2' ]\r
+                                               ]\r
+                                       }\r
+                               ]\r
+                       },\r
+                       {\r
+                               id: 'tab2',\r
+                               label: 'Second Tab',\r
+                               title: 'Second Tab',\r
+                               elements: [\r
+                                       {\r
+                                               id: 'button1',\r
+                                               type: 'button',\r
+                                               label: 'Button Field'\r
+                                       }\r
+                               ]\r
+                       }\r
+               ]\r
+       };\r
+} );\r
+\r
diff --git a/js/ckeditor/samples/old/dialog/dialog.html b/js/ckeditor/samples/old/dialog/dialog.html
new file mode 100644 (file)
index 0000000..0f22a1a
--- /dev/null
@@ -0,0 +1,190 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>Using API to Customize Dialog Windows &mdash; CKEditor Sample</title>\r
+       <script src="../../../ckeditor.js"></script>\r
+       <link rel="stylesheet" href="../../../samples/old/sample.css">\r
+       <meta name="ckeditor-sample-name" content="Using the JavaScript API to customize dialog windows">\r
+       <meta name="ckeditor-sample-group" content="Advanced Samples">\r
+       <meta name="ckeditor-sample-description" content="Using the dialog windows API to customize dialog windows without changing the original editor code.">\r
+       <style>\r
+\r
+               .cke_button__mybutton_icon\r
+               {\r
+                       display: none !important;\r
+               }\r
+\r
+               .cke_button__mybutton_label\r
+               {\r
+                       display: inline !important;\r
+               }\r
+\r
+       </style>\r
+       <script>\r
+\r
+               CKEDITOR.on( 'instanceCreated', function( ev ){\r
+                       var editor = ev.editor;\r
+\r
+                       // Listen for the "pluginsLoaded" event, so we are sure that the\r
+                       // "dialog" plugin has been loaded and we are able to do our\r
+                       // customizations.\r
+                       editor.on( 'pluginsLoaded', function() {\r
+\r
+                               // If our custom dialog has not been registered, do that now.\r
+                               if ( !CKEDITOR.dialog.exists( 'myDialog' ) ) {\r
+                                       // We need to do the following trick to find out the dialog\r
+                                       // definition file URL path. In the real world, you would simply\r
+                                       // point to an absolute path directly, like "/mydir/mydialog.js".\r
+                                       var href = document.location.href.split( '/' );\r
+                                       href.pop();\r
+                                       href.push( 'assets/my_dialog.js' );\r
+                                       href = href.join( '/' );\r
+\r
+                                       // Finally, register the dialog.\r
+                                       CKEDITOR.dialog.add( 'myDialog', href );\r
+                               }\r
+\r
+                               // Register the command used to open the dialog.\r
+                               editor.addCommand( 'myDialogCmd', new CKEDITOR.dialogCommand( 'myDialog' ) );\r
+\r
+                               // Add the a custom toolbar buttons, which fires the above\r
+                               // command..\r
+                               editor.ui.add( 'MyButton', CKEDITOR.UI_BUTTON, {\r
+                                       label: 'My Dialog',\r
+                                       command: 'myDialogCmd'\r
+                               });\r
+                       });\r
+               });\r
+\r
+               // When opening a dialog, its "definition" is created for it, for\r
+               // each editor instance. The "dialogDefinition" event is then\r
+               // fired. We should use this event to make customizations to the\r
+               // definition of existing dialogs.\r
+               CKEDITOR.on( 'dialogDefinition', function( ev ) {\r
+                       // Take the dialog name and its definition from the event data.\r
+                       var dialogName = ev.data.name;\r
+                       var dialogDefinition = ev.data.definition;\r
+\r
+                       // Check if the definition is from the dialog we're\r
+                       // interested on (the "Link" dialog).\r
+                       if ( dialogName == 'myDialog' && ev.editor.name == 'editor2' ) {\r
+                               // Get a reference to the "Link Info" tab.\r
+                               var infoTab = dialogDefinition.getContents( 'tab1' );\r
+\r
+                               // Add a new text field to the "tab1" tab page.\r
+                               infoTab.add( {\r
+                                       type: 'text',\r
+                                       label: 'My Custom Field',\r
+                                       id: 'customField',\r
+                                       'default': 'Sample!',\r
+                                       validate: function() {\r
+                                               if ( ( /\d/ ).test( this.getValue() ) )\r
+                                                       return 'My Custom Field must not contain digits';\r
+                                       }\r
+                               });\r
+\r
+                               // Remove the "select1" field from the "tab1" tab.\r
+                               infoTab.remove( 'select1' );\r
+\r
+                               // Set the default value for "input1" field.\r
+                               var input1 = infoTab.get( 'input1' );\r
+                               input1[ 'default' ] = 'www.example.com';\r
+\r
+                               // Remove the "tab2" tab page.\r
+                               dialogDefinition.removeContents( 'tab2' );\r
+\r
+                               // Add a new tab to the "Link" dialog.\r
+                               dialogDefinition.addContents( {\r
+                                       id: 'customTab',\r
+                                       label: 'My Tab',\r
+                                       accessKey: 'M',\r
+                                       elements: [\r
+                                               {\r
+                                                       id: 'myField1',\r
+                                                       type: 'text',\r
+                                                       label: 'My Text Field'\r
+                                               },\r
+                                               {\r
+                                                       id: 'myField2',\r
+                                                       type: 'text',\r
+                                                       label: 'Another Text Field'\r
+                                               }\r
+                                       ]\r
+                               });\r
+\r
+                               // Provide the focus handler to start initial focus in "customField" field.\r
+                               dialogDefinition.onFocus = function() {\r
+                                       var urlField = this.getContentElement( 'tab1', 'customField' );\r
+                                       urlField.select();\r
+                               };\r
+                       }\r
+               });\r
+\r
+               var config = {\r
+                       extraPlugins: 'dialog',\r
+                       toolbar: [ [ 'MyButton' ] ]\r
+               };\r
+\r
+       </script>\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="../../../samples/old/index.html">CKEditor Samples</a> &raquo; Using CKEditor Dialog API\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out the <a href="http://sdk.ckeditor.com/">brand new samples in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>\r
+                       This sample shows how to use the\r
+                       <a class="samples" href="http://docs.ckeditor.com/#!/api/CKEDITOR.dialog">CKEditor Dialog API</a>\r
+                       to customize CKEditor dialog windows without changing the original editor code.\r
+                       The following customizations are being done in the example below:\r
+               </p>\r
+               <p>\r
+                       For details on how to create this setup check the source code of this sample page.\r
+               </p>\r
+       </div>\r
+       <p>A custom dialog is added to the editors using the <code>pluginsLoaded</code> event, from an external <a target="_blank" href="assets/my_dialog.js">dialog definition file</a>:</p>\r
+       <ol>\r
+               <li><strong>Creating a custom dialog window</strong> &ndash; "My Dialog" dialog window opened with the "My Dialog" toolbar button.</li>\r
+               <li><strong>Creating a custom button</strong> &ndash; Add button to open the dialog with "My Dialog" toolbar button.</li>\r
+       </ol>\r
+       <textarea cols="80" id="editor1" name="editor1" rows="10">&lt;p&gt;This is some &lt;strong&gt;sample text&lt;/strong&gt;. You are using &lt;a href="http://ckeditor.com/"&gt;CKEditor&lt;/a&gt;.&lt;/p&gt;</textarea>\r
+       <script>\r
+               // Replace the <textarea id="editor1"> with an CKEditor instance.\r
+               CKEDITOR.replace( 'editor1', config );\r
+       </script>\r
+       <p>The below editor modify the dialog definition of the above added dialog using the <code>dialogDefinition</code> event:</p>\r
+       <ol>\r
+               <li><strong>Adding dialog tab</strong> &ndash; Add new tab "My Tab" to dialog window.</li>\r
+               <li><strong>Removing a dialog window tab</strong> &ndash; Remove "Second Tab" page from the dialog window.</li>\r
+               <li><strong>Adding dialog window fields</strong> &ndash; Add "My Custom Field" to the dialog window.</li>\r
+               <li><strong>Removing dialog window field</strong> &ndash; Remove "Select Field" selection field from the dialog window.</li>\r
+               <li><strong>Setting default values for dialog window fields</strong> &ndash; Set default value of "Text Field" text field. </li>\r
+               <li><strong>Setup initial focus for dialog window</strong> &ndash; Put initial focus on "My Custom Field" text field. </li>\r
+       </ol>\r
+       <textarea cols="80" id="editor2" name="editor2" rows="10">&lt;p&gt;This is some &lt;strong&gt;sample text&lt;/strong&gt;. You are using &lt;a href="http://ckeditor.com/"&gt;CKEditor&lt;/a&gt;.&lt;/p&gt;</textarea>\r
+       <script>\r
+\r
+               // Replace the <textarea id="editor1"> with an CKEditor instance.\r
+               CKEDITOR.replace( 'editor2', config );\r
+\r
+       </script>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/divreplace.html b/js/ckeditor/samples/old/divreplace.html
new file mode 100644 (file)
index 0000000..c6724f0
--- /dev/null
@@ -0,0 +1,144 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>Replace DIV &mdash; CKEditor Sample</title>\r
+       <script src="../../ckeditor.js"></script>\r
+       <link href="sample.css" rel="stylesheet">\r
+       <style>\r
+\r
+               div.editable\r
+               {\r
+                       border: solid 2px transparent;\r
+                       padding-left: 15px;\r
+                       padding-right: 15px;\r
+               }\r
+\r
+               div.editable:hover\r
+               {\r
+                       border-color: black;\r
+               }\r
+\r
+       </style>\r
+       <script>\r
+\r
+               // Uncomment the following code to test the "Timeout Loading Method".\r
+               // CKEDITOR.loadFullCoreTimeout = 5;\r
+\r
+               window.onload = function() {\r
+                       // Listen to the double click event.\r
+                       if ( window.addEventListener )\r
+                               document.body.addEventListener( 'dblclick', onDoubleClick, false );\r
+                       else if ( window.attachEvent )\r
+                               document.body.attachEvent( 'ondblclick', onDoubleClick );\r
+\r
+               };\r
+\r
+               function onDoubleClick( ev ) {\r
+                       // Get the element which fired the event. This is not necessarily the\r
+                       // element to which the event has been attached.\r
+                       var element = ev.target || ev.srcElement;\r
+\r
+                       // Find out the div that holds this element.\r
+                       var name;\r
+\r
+                       do {\r
+                               element = element.parentNode;\r
+                       }\r
+                       while ( element && ( name = element.nodeName.toLowerCase() ) &&\r
+                               ( name != 'div' || element.className.indexOf( 'editable' ) == -1 ) && name != 'body' );\r
+\r
+                       if ( name == 'div' && element.className.indexOf( 'editable' ) != -1 )\r
+                               replaceDiv( element );\r
+               }\r
+\r
+               var editor;\r
+\r
+               function replaceDiv( div ) {\r
+                       if ( editor )\r
+                               editor.destroy();\r
+\r
+                       editor = CKEDITOR.replace( div );\r
+               }\r
+\r
+       </script>\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="index.html">CKEditor Samples</a> &raquo; Replace DIV with CKEditor on the Fly\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out the <a href="http://sdk.ckeditor.com/">brand new samples in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>\r
+                       This sample shows how to automatically replace <code>&lt;div&gt;</code> elements\r
+                       with a CKEditor instance on the fly, following user's doubleclick. The content\r
+                       that was previously placed inside the <code>&lt;div&gt;</code> element will now\r
+                       be moved into CKEditor editing area.\r
+               </p>\r
+               <p>\r
+                       For details on how to create this setup check the source code of this sample page.\r
+               </p>\r
+       </div>\r
+       <p>\r
+               Double-click any of the following <code>&lt;div&gt;</code> elements to transform them into\r
+               editor instances.\r
+       </p>\r
+       <div class="editable">\r
+               <h3>\r
+                       Part 1\r
+               </h3>\r
+               <p>\r
+                       Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Cras et ipsum quis mi\r
+                       semper accumsan. Integer pretium dui id massa. Suspendisse in nisl sit amet urna\r
+                       rutrum imperdiet. Nulla eu tellus. Donec ante nisi, ullamcorper quis, fringilla\r
+                       nec, sagittis eleifend, pede. Nulla commodo interdum massa. Donec id metus. Fusce\r
+                       eu ipsum. Suspendisse auctor. Phasellus fermentum porttitor risus.\r
+               </p>\r
+       </div>\r
+       <div class="editable">\r
+               <h3>\r
+                       Part 2\r
+               </h3>\r
+               <p>\r
+                       Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Cras et ipsum quis mi\r
+                       semper accumsan. Integer pretium dui id massa. Suspendisse in nisl sit amet urna\r
+                       rutrum imperdiet. Nulla eu tellus. Donec ante nisi, ullamcorper quis, fringilla\r
+                       nec, sagittis eleifend, pede. Nulla commodo interdum massa. Donec id metus. Fusce\r
+                       eu ipsum. Suspendisse auctor. Phasellus fermentum porttitor risus.\r
+               </p>\r
+               <p>\r
+                       Donec velit. Mauris massa. Vestibulum non nulla. Nam suscipit arcu nec elit. Phasellus\r
+                       sollicitudin iaculis ante. Ut non mauris et sapien tincidunt adipiscing. Vestibulum\r
+                       vitae leo. Suspendisse nec mi tristique nulla laoreet vulputate.\r
+               </p>\r
+       </div>\r
+       <div class="editable">\r
+               <h3>\r
+                       Part 3\r
+               </h3>\r
+               <p>\r
+                       Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Cras et ipsum quis mi\r
+                       semper accumsan. Integer pretium dui id massa. Suspendisse in nisl sit amet urna\r
+                       rutrum imperdiet. Nulla eu tellus. Donec ante nisi, ullamcorper quis, fringilla\r
+                       nec, sagittis eleifend, pede. Nulla commodo interdum massa. Donec id metus. Fusce\r
+                       eu ipsum. Suspendisse auctor. Phasellus fermentum porttitor risus.\r
+               </p>\r
+       </div>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/enterkey/enterkey.html b/js/ckeditor/samples/old/enterkey/enterkey.html
new file mode 100644 (file)
index 0000000..79afee3
--- /dev/null
@@ -0,0 +1,106 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>ENTER Key Configuration &mdash; CKEditor Sample</title>\r
+       <script src="../../../ckeditor.js"></script>\r
+       <link href="../../../samples/old/sample.css" rel="stylesheet">\r
+       <meta name="ckeditor-sample-name" content="Using the &quot;Enter&quot; key in CKEditor">\r
+       <meta name="ckeditor-sample-group" content="Advanced Samples">\r
+       <meta name="ckeditor-sample-description" content="Configuring the behavior of &lt;em&gt;Enter&lt;/em&gt; and &lt;em&gt;Shift+Enter&lt;/em&gt; keys.">\r
+       <script>\r
+\r
+               var editor;\r
+\r
+               function changeEnter() {\r
+                       // If we already have an editor, let's destroy it first.\r
+                       if ( editor )\r
+                               editor.destroy( true );\r
+\r
+                       // Create the editor again, with the appropriate settings.\r
+                       editor = CKEDITOR.replace( 'editor1', {\r
+                               extraPlugins: 'enterkey',\r
+                               enterMode: Number( document.getElementById( 'xEnter' ).value ),\r
+                               shiftEnterMode: Number( document.getElementById( 'xShiftEnter' ).value )\r
+                       });\r
+               }\r
+\r
+               window.onload = changeEnter;\r
+\r
+       </script>\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="../../../samples/old/index.html">CKEditor Samples</a> &raquo; ENTER Key Configuration\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out its <a href="http://sdk.ckeditor.com/samples/enterkey.html">brand new version in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>\r
+                       This sample shows how to configure the <em>Enter</em> and <em>Shift+Enter</em> keys\r
+                       to perform actions specified in the\r
+                       <a class="samples" href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-enterMode"><code>enterMode</code></a>\r
+                       and <a class="samples" href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-shiftEnterMode"><code>shiftEnterMode</code></a>\r
+                       parameters, respectively.\r
+                       You can choose from the following options:\r
+               </p>\r
+               <ul class="samples">\r
+                       <li><strong><code>ENTER_P</code></strong> &ndash; new <code>&lt;p&gt;</code> paragraphs are created;</li>\r
+                       <li><strong><code>ENTER_BR</code></strong> &ndash; lines are broken with <code>&lt;br&gt;</code> elements;</li>\r
+                       <li><strong><code>ENTER_DIV</code></strong> &ndash; new <code>&lt;div&gt;</code> blocks are created.</li>\r
+               </ul>\r
+               <p>\r
+                       The sample code below shows how to configure CKEditor to create a <code>&lt;div&gt;</code> block when <em>Enter</em> key is pressed.\r
+               </p>\r
+<pre class="samples">\r
+CKEDITOR.replace( '<em>textarea_id</em>', {\r
+       <strong>enterMode: CKEDITOR.ENTER_DIV</strong>\r
+});</pre>\r
+               <p>\r
+                       Note that <code><em>textarea_id</em></code> in the code above is the <code>id</code> attribute of\r
+                       the <code>&lt;textarea&gt;</code> element to be replaced.\r
+               </p>\r
+       </div>\r
+       <div style="float: left; margin-right: 20px">\r
+               When <em>Enter</em> is pressed:<br>\r
+               <select id="xEnter" onchange="changeEnter();">\r
+                       <option selected="selected" value="1">Create a new &lt;P&gt; (recommended)</option>\r
+                       <option value="3">Create a new &lt;DIV&gt;</option>\r
+                       <option value="2">Break the line with a &lt;BR&gt;</option>\r
+               </select>\r
+       </div>\r
+       <div style="float: left">\r
+               When <em>Shift+Enter</em> is pressed:<br>\r
+               <select id="xShiftEnter" onchange="changeEnter();">\r
+                       <option value="1">Create a new &lt;P&gt;</option>\r
+                       <option value="3">Create a new &lt;DIV&gt;</option>\r
+                       <option selected="selected" value="2">Break the line with a &lt;BR&gt; (recommended)</option>\r
+               </select>\r
+       </div>\r
+       <br style="clear: both">\r
+       <form action="../../../samples/sample_posteddata.php" method="post">\r
+               <p>\r
+                       <br>\r
+                       <textarea cols="80" id="editor1" name="editor1" rows="10">This is some &lt;strong&gt;sample text&lt;/strong&gt;. You are using &lt;a href="http://ckeditor.com/"&gt;CKEditor&lt;/a&gt;.</textarea>\r
+               </p>\r
+               <p>\r
+                       <input type="submit" value="Submit">\r
+               </p>\r
+       </form>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/index.html b/js/ckeditor/samples/old/index.html
new file mode 100644 (file)
index 0000000..aa51b70
--- /dev/null
@@ -0,0 +1,122 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>CKEditor Samples</title>\r
+       <link rel="stylesheet" href="sample.css">\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               CKEditor Samples\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               These samples are not maintained anymore. Check out the <a href="http://sdk.ckeditor.com/">brand new samples in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="twoColumns">\r
+               <div class="twoColumnsLeft">\r
+                       <h2 class="samples">\r
+                               Basic Samples\r
+                       </h2>\r
+                       <dl class="samples">\r
+                               <dt><a class="samples" href="replacebyclass.html">Replace textarea elements by class name</a></dt>\r
+                               <dd>Automatic replacement of all textarea elements of a given class with a CKEditor instance.</dd>\r
+\r
+                               <dt><a class="samples" href="replacebycode.html">Replace textarea elements by code</a></dt>\r
+                               <dd>Replacement of textarea elements with CKEditor instances by using a JavaScript call.</dd>\r
+\r
+                               <dt><a class="samples" href="jquery.html">Create editors with jQuery</a></dt>\r
+                               <dd>Creating standard and inline CKEditor instances with jQuery adapter.</dd>\r
+                       </dl>\r
+\r
+                       <h2 class="samples">\r
+                               Basic Customization\r
+                       </h2>\r
+                       <dl class="samples">\r
+                               <dt><a class="samples" href="uicolor.html">User Interface color</a></dt>\r
+                               <dd>Changing CKEditor User Interface color and adding a toolbar button that lets the user set the UI color.</dd>\r
+\r
+                               <dt><a class="samples" href="uilanguages.html">User Interface languages</a></dt>\r
+                               <dd>Changing CKEditor User Interface language and adding a drop-down list that lets the user choose the UI language.</dd>\r
+                       </dl>\r
+\r
+\r
+                       <h2 class="samples">Plugins</h2>
+<dl class="samples">
+<dt><a class="samples" href="sourcedialog/sourcedialog.html">Editing source code in a dialog</a><span class="new">New!</span></dt>
+<dd>Editing HTML content of both inline and classic editor instances.</dd>
+</dl>\r
+               </div>\r
+               <div class="twoColumnsRight">\r
+                       <h2 class="samples">\r
+                               Inline Editing\r
+                       </h2>\r
+                       <dl class="samples">\r
+                               <dt><a class="samples" href="inlineall.html">Massive inline editor creation</a></dt>\r
+                               <dd>Turn all elements with <code>contentEditable = true</code> attribute into inline editors.</dd>\r
+\r
+                               <dt><a class="samples" href="inlinebycode.html">Convert element into an inline editor by code</a></dt>\r
+                               <dd>Conversion of DOM elements into inline CKEditor instances by using a JavaScript call.</dd>\r
+\r
+                               <dt><a class="samples" href="inlinetextarea.html">Replace textarea with inline editor</a> <span class="new">New!</span></dt>\r
+                               <dd>A form with a textarea that is replaced by an inline editor at runtime.</dd>\r
+\r
+                               \r
+                       </dl>\r
+\r
+                       <h2 class="samples">\r
+                               Advanced Samples\r
+                       </h2>\r
+                       <dl class="samples">\r
+                               <dt><a class="samples" href="datafiltering.html">Data filtering and features activation</a> <span class="new">New!</span></dt>\r
+                               <dd>Data filtering and automatic features activation basing on configuration.</dd>\r
+\r
+                               <dt><a class="samples" href="divreplace.html">Replace DIV elements on the fly</a></dt>\r
+                               <dd>Transforming a <code>div</code> element into an instance of CKEditor with a mouse click.</dd>\r
+\r
+                               <dt><a class="samples" href="appendto.html">Append editor instances</a></dt>\r
+                               <dd>Appending editor instances to existing DOM elements.</dd>\r
+\r
+                               <dt><a class="samples" href="ajax.html">Create and destroy editor instances for Ajax applications</a></dt>\r
+                               <dd>Creating and destroying CKEditor instances on the fly and saving the contents entered into the editor window.</dd>\r
+\r
+                               <dt><a class="samples" href="api.html">Basic usage of the API</a></dt>\r
+                               <dd>Using the CKEditor JavaScript API to interact with the editor at runtime.</dd>\r
+\r
+                               <dt><a class="samples" href="xhtmlstyle.html">XHTML-compliant style</a></dt>\r
+                               <dd>Configuring CKEditor to produce XHTML 1.1 compliant attributes and styles.</dd>\r
+\r
+                               <dt><a class="samples" href="readonly.html">Read-only mode</a></dt>\r
+                               <dd>Using the readOnly API to block introducing changes to the editor contents.</dd>\r
+\r
+                               <dt><a class="samples" href="tabindex.html">"Tab" key-based navigation</a></dt>\r
+                               <dd>Navigating among editor instances with tab key.</dd>\r
+\r
+\r
+                               
+<dt><a class="samples" href="dialog/dialog.html">Using the JavaScript API to customize dialog windows</a></dt>
+<dd>Using the dialog windows API to customize dialog windows without changing the original editor code.</dd>
+
+<dt><a class="samples" href="enterkey/enterkey.html">Using the &quot;Enter&quot; key in CKEditor</a></dt>
+<dd>Configuring the behavior of <em>Enter</em> and <em>Shift+Enter</em> keys.</dd>
+
+<dt><a class="samples" href="toolbar/toolbar.html">Toolbar Configurations</a></dt>
+<dd>Configuring CKEditor to display full or custom toolbar layout.</dd>
+\r
+                       </dl>\r
+               </div>\r
+       </div>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/inlineall.html b/js/ckeditor/samples/old/inlineall.html
new file mode 100644 (file)
index 0000000..e0241e8
--- /dev/null
@@ -0,0 +1,314 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>Massive inline editing &mdash; CKEditor Sample</title>\r
+       <script src="../../ckeditor.js"></script>\r
+       <script>\r
+\r
+               // This code is generally not necessary, but it is here to demonstrate\r
+               // how to customize specific editor instances on the fly. This fits well\r
+               // this demo because we have editable elements (like headers) that\r
+               // require less features.\r
+\r
+               // The "instanceCreated" event is fired for every editor instance created.\r
+               CKEDITOR.on( 'instanceCreated', function( event ) {\r
+                       var editor = event.editor,\r
+                               element = editor.element;\r
+\r
+                       // Customize editors for headers and tag list.\r
+                       // These editors don't need features like smileys, templates, iframes etc.\r
+                       if ( element.is( 'h1', 'h2', 'h3' ) || element.getAttribute( 'id' ) == 'taglist' ) {\r
+                               // Customize the editor configurations on "configLoaded" event,\r
+                               // which is fired after the configuration file loading and\r
+                               // execution. This makes it possible to change the\r
+                               // configurations before the editor initialization takes place.\r
+                               editor.on( 'configLoaded', function() {\r
+\r
+                                       // Remove unnecessary plugins to make the editor simpler.\r
+                                       editor.config.removePlugins = 'colorbutton,find,flash,font,' +\r
+                                               'forms,iframe,image,newpage,removeformat,' +\r
+                                               'smiley,specialchar,stylescombo,templates';\r
+\r
+                                       // Rearrange the layout of the toolbar.\r
+                                       editor.config.toolbarGroups = [\r
+                                               { name: 'editing',              groups: [ 'basicstyles', 'links' ] },\r
+                                               { name: 'undo' },\r
+                                               { name: 'clipboard',    groups: [ 'selection', 'clipboard' ] },\r
+                                               { name: 'about' }\r
+                                       ];\r
+                               });\r
+                       }\r
+               });\r
+\r
+       </script>\r
+       <link href="sample.css" rel="stylesheet">\r
+       <style>\r
+\r
+               /* The following styles are just to make the page look nice. */\r
+\r
+               /* Workaround to show Arial Black in Firefox. */\r
+               @font-face\r
+               {\r
+                       font-family: 'arial-black';\r
+                       src: local('Arial Black');\r
+               }\r
+\r
+               *[contenteditable="true"]\r
+               {\r
+                       padding: 10px;\r
+               }\r
+\r
+               #container\r
+               {\r
+                       width: 960px;\r
+                       margin: 30px auto 0;\r
+               }\r
+\r
+               #header\r
+               {\r
+                       overflow: hidden;\r
+                       padding: 0 0 30px;\r
+                       border-bottom: 5px solid #05B2D2;\r
+                       position: relative;\r
+               }\r
+\r
+               #headerLeft,\r
+               #headerRight\r
+               {\r
+                       width: 49%;\r
+                       overflow: hidden;\r
+               }\r
+\r
+               #headerLeft\r
+               {\r
+                       float: left;\r
+                       padding: 10px 1px 1px;\r
+               }\r
+\r
+               #headerLeft h2,\r
+               #headerLeft h3\r
+               {\r
+                       text-align: right;\r
+                       margin: 0;\r
+                       overflow: hidden;\r
+                       font-weight: normal;\r
+               }\r
+\r
+               #headerLeft h2\r
+               {\r
+                       font-family: "Arial Black",arial-black;\r
+                       font-size: 4.6em;\r
+                       line-height: 1.1;\r
+                       text-transform: uppercase;\r
+               }\r
+\r
+               #headerLeft h3\r
+               {\r
+                       font-size: 2.3em;\r
+                       line-height: 1.1;\r
+                       margin: .2em 0 0;\r
+                       color: #666;\r
+               }\r
+\r
+               #headerRight\r
+               {\r
+                       float: right;\r
+                       padding: 1px;\r
+               }\r
+\r
+               #headerRight p\r
+               {\r
+                       line-height: 1.8;\r
+                       text-align: justify;\r
+                       margin: 0;\r
+               }\r
+\r
+               #headerRight p + p\r
+               {\r
+                       margin-top: 20px;\r
+               }\r
+\r
+               #headerRight > div\r
+               {\r
+                       padding: 20px;\r
+                       margin: 0 0 0 30px;\r
+                       font-size: 1.4em;\r
+                       color: #666;\r
+               }\r
+\r
+               #columns\r
+               {\r
+                       color: #333;\r
+                       overflow: hidden;\r
+                       padding: 20px 0;\r
+               }\r
+\r
+               #columns > div\r
+               {\r
+                       float: left;\r
+                       width: 33.3%;\r
+               }\r
+\r
+               #columns #column1 > div\r
+               {\r
+                       margin-left: 1px;\r
+               }\r
+\r
+               #columns #column3 > div\r
+               {\r
+                       margin-right: 1px;\r
+               }\r
+\r
+               #columns > div > div\r
+               {\r
+                       margin: 0px 10px;\r
+                       padding: 10px 20px;\r
+               }\r
+\r
+               #columns blockquote\r
+               {\r
+                       margin-left: 15px;\r
+               }\r
+\r
+               #tagLine\r
+               {\r
+                       border-top: 5px solid #05B2D2;\r
+                       padding-top: 20px;\r
+               }\r
+\r
+               #taglist {\r
+                       display: inline-block;\r
+                       margin-left: 20px;\r
+                       font-weight: bold;\r
+                       margin: 0 0 0 20px;\r
+               }\r
+\r
+       </style>\r
+</head>\r
+<body>\r
+<div>\r
+       <h1 class="samples"><a href="index.html">CKEditor Samples</a> &raquo; Massive inline editing</h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out its <a href="http://sdk.ckeditor.com/samples/inline.html">brand new version in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>This sample page demonstrates the inline editing feature - CKEditor instances will be created automatically from page elements with <strong>contentEditable</strong> attribute set to value <strong>true</strong>:</p>\r
+               <pre class="samples">&lt;div <strong>contenteditable="true</strong>" &gt; ... &lt;/div&gt;</pre>\r
+               <p>Click inside of any element below to start editing.</p>\r
+       </div>\r
+</div>\r
+<div id="container">\r
+       <div id="header">\r
+               <div id="headerLeft">\r
+                       <h2 id="sampleTitle" contenteditable="true">\r
+                               CKEditor<br> Goes Inline!\r
+                       </h2>\r
+                       <h3 contenteditable="true">\r
+                               Lorem ipsum dolor sit amet dolor duis blandit vestibulum faucibus a, tortor.\r
+                       </h3>\r
+               </div>\r
+               <div id="headerRight">\r
+                       <div contenteditable="true">\r
+                               <p>\r
+                                       Lorem ipsum dolor sit amet enim. Etiam ullamcorper. Suspendisse a pellentesque dui, non felis. Maecenas malesuada elit lectus felis, malesuada ultricies.\r
+                               </p>\r
+                               <p>\r
+                                       Curabitur et ligula. Ut molestie a, ultricies porta urna. Vestibulum commodo volutpat a, convallis ac, laoreet enim. Phasellus fermentum in, dolor. Pellentesque facilisis. Nulla imperdiet sit amet magna. Vestibulum dapibus, mauris nec malesuada fames ac.\r
+                               </p>\r
+                       </div>\r
+               </div>\r
+       </div>\r
+       <div id="columns">\r
+               <div id="column1">\r
+                       <div contenteditable="true">\r
+                               <h3>\r
+                                       Fusce vitae porttitor\r
+                               </h3>\r
+                               <p>\r
+                                       <strong>\r
+                                               Lorem ipsum dolor sit amet dolor. Duis blandit vestibulum faucibus a, tortor.\r
+                                       </strong>\r
+                               </p>\r
+                               <p>\r
+                                       Proin nunc justo felis mollis tincidunt, risus risus pede, posuere cubilia Curae, Nullam euismod, enim. Etiam nibh ultricies dolor ac dignissim erat volutpat. Vivamus fermentum <a href="http://ckeditor.com/">nisl nulla sem in</a> metus. Maecenas wisi. Donec nec erat volutpat.\r
+                               </p>\r
+                               <blockquote>\r
+                                       <p>\r
+                                               Fusce vitae porttitor a, euismod convallis nisl, blandit risus tortor, pretium.\r
+                                               Vehicula vitae, imperdiet vel, ornare enim vel sodales rutrum\r
+                                       </p>\r
+                               </blockquote>\r
+                               <blockquote>\r
+                                       <p>\r
+                                               Libero nunc, rhoncus ante ipsum non ipsum. Nunc eleifend pede turpis id sollicitudin fringilla. Phasellus ultrices, velit ac arcu.\r
+                                       </p>\r
+                               </blockquote>\r
+                               <p>Pellentesque nunc. Donec suscipit erat. Pellentesque habitant morbi tristique ullamcorper.</p>\r
+                               <p><s>Mauris mattis feugiat lectus nec mauris. Nullam vitae ante.</s></p>\r
+                       </div>\r
+               </div>\r
+               <div id="column2">\r
+                       <div contenteditable="true">\r
+                               <h3>\r
+                                       Integer condimentum sit amet\r
+                               </h3>\r
+                               <p>\r
+                                       <strong>Aenean nonummy a, mattis varius. Cras aliquet.</strong>\r
+                                       Praesent <a href="http://ckeditor.com/">magna non mattis ac, rhoncus nunc</a>, rhoncus eget, cursus pulvinar mollis.</p>\r
+                               <p>Proin id nibh. Sed eu libero posuere sed, lectus. Phasellus dui gravida gravida feugiat mattis ac, felis.</p>\r
+                               <p>Integer condimentum sit amet, tempor elit odio, a dolor non ante at sapien. Sed ac lectus. Nulla ligula quis eleifend mi, id leo velit pede cursus arcu id nulla ac lectus. Phasellus vestibulum. Nunc viverra enim quis diam.</p>\r
+                       </div>\r
+                       <div contenteditable="true">\r
+                               <h3>\r
+                                       Praesent wisi accumsan sit amet nibh\r
+                               </h3>\r
+                               <p>Donec ullamcorper, risus tortor, pretium porttitor. Morbi quam quis lectus non leo.</p>\r
+                               <p style="margin-left: 40px; ">Integer faucibus scelerisque. Proin faucibus at, aliquet vulputate, odio at eros. Fusce <a href="http://ckeditor.com/">gravida, erat vitae augue</a>. Fusce urna fringilla gravida.</p>\r
+                               <p>In hac habitasse platea dictumst. Praesent wisi accumsan sit amet nibh. Maecenas orci luctus a, lacinia quam sem, posuere commodo, odio condimentum tempor, pede semper risus. Suspendisse pede. In hac habitasse platea dictumst. Nam sed laoreet sit amet erat. Integer.</p>\r
+                       </div>\r
+               </div>\r
+               <div id="column3">\r
+                       <div contenteditable="true">\r
+                               <p>\r
+                                       <img src="assets/inlineall/logo.png" alt="CKEditor logo" style="float:left">\r
+                               </p>\r
+                               <p>Quisque justo neque, mattis sed, fermentum ultrices <strong>posuere cubilia Curae</strong>, Vestibulum elit metus, quis placerat ut, lectus. Ut sagittis, nunc libero, egestas consequat lobortis velit rutrum ut, faucibus turpis. Fusce porttitor, nulla quis turpis. Nullam laoreet vel, consectetuer tellus suscipit ultricies, hendrerit wisi. Donec odio nec velit ac nunc sit amet, accumsan cursus aliquet. Vestibulum ante sit amet sagittis mi.</p>\r
+                               <h3>\r
+                                       Nullam laoreet vel consectetuer tellus suscipit\r
+                               </h3>\r
+                               <ul>\r
+                                       <li>Ut sagittis, nunc libero, egestas consequat lobortis velit rutrum ut, faucibus turpis.</li>\r
+                                       <li>Fusce porttitor, nulla quis turpis. Nullam laoreet vel, consectetuer tellus suscipit ultricies, hendrerit wisi.</li>\r
+                                       <li>Mauris eget tellus. Donec non felis. Nam eget dolor. Vestibulum enim. Donec.</li>\r
+                               </ul>\r
+                               <p>Quisque justo neque, mattis sed, <a href="http://ckeditor.com/">fermentum ultrices posuere cubilia</a> Curae, Vestibulum elit metus, quis placerat ut, lectus.</p>\r
+                               <p>Nullam laoreet vel, consectetuer tellus suscipit ultricies, hendrerit wisi. Ut sagittis, nunc libero, egestas consequat lobortis velit rutrum ut, faucibus turpis. Fusce porttitor, nulla quis turpis.</p>\r
+                               <p>Donec odio nec velit ac nunc sit amet, accumsan cursus aliquet. Vestibulum ante sit amet sagittis mi. Sed in nonummy faucibus turpis. Mauris eget tellus. Donec non felis. Nam eget dolor. Vestibulum enim. Donec.</p>\r
+                       </div>\r
+               </div>\r
+       </div>\r
+       <div id="tagLine">\r
+               Tags of this article:\r
+               <p id="taglist" contenteditable="true">\r
+                       inline, editing, floating, CKEditor\r
+               </p>\r
+       </div>\r
+</div>\r
+<div id="footer">\r
+       <hr>\r
+       <p>\r
+               CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">\r
+                       http://ckeditor.com</a>\r
+       </p>\r
+       <p id="copy">\r
+               Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a>\r
+               - Frederico Knabben. All rights reserved.\r
+       </p>\r
+</div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/inlinebycode.html b/js/ckeditor/samples/old/inlinebycode.html
new file mode 100644 (file)
index 0000000..339be0c
--- /dev/null
@@ -0,0 +1,124 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>Inline Editing by Code &mdash; CKEditor Sample</title>\r
+       <script src="../../ckeditor.js"></script>\r
+       <link href="sample.css" rel="stylesheet">\r
+       <style>\r
+\r
+               #editable\r
+               {\r
+                       padding: 10px;\r
+                       float: left;\r
+               }\r
+\r
+       </style>\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="index.html">CKEditor Samples</a> &raquo; Inline Editing by Code\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out its <a href="http://sdk.ckeditor.com/samples/inline.html">brand new version in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>\r
+                       This sample shows how to create an inline editor instance of CKEditor. It is created\r
+                       with a JavaScript call using the following code:\r
+               </p>\r
+<pre class="samples">\r
+// This property tells CKEditor to not activate every element with contenteditable=true element.\r
+CKEDITOR.disableAutoInline = true;\r
+\r
+var editor = CKEDITOR.inline( document.getElementById( 'editable' ) );\r
+</pre>\r
+               <p>\r
+                       Note that <code>editable</code> in the code above is the <code>id</code>\r
+                       attribute of the <code>&lt;div&gt;</code> element to be converted into an inline instance.\r
+               </p>\r
+       </div>\r
+       <div id="editable" contenteditable="true">\r
+               <h1><img alt="Saturn V carrying Apollo 11" class="right" src="assets/sample.jpg" /> Apollo 11</h1>\r
+\r
+               <p><b>Apollo 11</b> was the spaceflight that landed the first humans, Americans <a href="http://en.wikipedia.org/wiki/Neil_Armstrong" title="Neil Armstrong">Neil Armstrong</a> and <a href="http://en.wikipedia.org/wiki/Buzz_Aldrin" title="Buzz Aldrin">Buzz Aldrin</a>, on the Moon on July 20, 1969, at 20:18 UTC. Armstrong became the first to step onto the lunar surface 6 hours later on July 21 at 02:56 UTC.</p>\r
+\r
+               <p>Armstrong spent about <s>three and a half</s> two and a half hours outside the spacecraft, Aldrin slightly less; and together they collected 47.5 pounds (21.5&nbsp;kg) of lunar material for return to Earth. A third member of the mission, <a href="http://en.wikipedia.org/wiki/Michael_Collins_(astronaut)" title="Michael Collins (astronaut)">Michael Collins</a>, piloted the <a href="http://en.wikipedia.org/wiki/Apollo_Command/Service_Module" title="Apollo Command/Service Module">command</a> spacecraft alone in lunar orbit until Armstrong and Aldrin returned to it for the trip back to Earth.</p>\r
+\r
+               <h2>Broadcasting and <em>quotes</em> <a id="quotes" name="quotes"></a></h2>\r
+\r
+               <p>Broadcast on live TV to a world-wide audience, Armstrong stepped onto the lunar surface and described the event as:</p>\r
+\r
+               <blockquote>\r
+                       <p>One small step for [a] man, one giant leap for mankind.</p>\r
+               </blockquote>\r
+\r
+               <p>Apollo 11 effectively ended the <a href="http://en.wikipedia.org/wiki/Space_Race" title="Space Race">Space Race</a> and fulfilled a national goal proposed in 1961 by the late U.S. President <a href="http://en.wikipedia.org/wiki/John_F._Kennedy" title="John F. Kennedy">John F. Kennedy</a> in a speech before the United States Congress:</p>\r
+\r
+               <blockquote>\r
+                       <p>[...] before this decade is out, of landing a man on the Moon and returning him safely to the Earth.</p>\r
+               </blockquote>\r
+\r
+               <h2>Technical details <a id="tech-details" name="tech-details"></a></h2>\r
+\r
+               <table align="right" border="1" bordercolor="#ccc" cellpadding="5" cellspacing="0" style="border-collapse:collapse;margin:10px 0 10px 15px;">\r
+                       <caption><strong>Mission crew</strong></caption>\r
+                       <thead>\r
+                       <tr>\r
+                               <th scope="col">Position</th>\r
+                               <th scope="col">Astronaut</th>\r
+                       </tr>\r
+                       </thead>\r
+                       <tbody>\r
+                       <tr>\r
+                               <td>Commander</td>\r
+                               <td>Neil A. Armstrong</td>\r
+                       </tr>\r
+                       <tr>\r
+                               <td>Command Module Pilot</td>\r
+                               <td>Michael Collins</td>\r
+                       </tr>\r
+                       <tr>\r
+                               <td>Lunar Module Pilot</td>\r
+                               <td>Edwin &quot;Buzz&quot; E. Aldrin, Jr.</td>\r
+                       </tr>\r
+                       </tbody>\r
+               </table>\r
+\r
+               <p>Launched by a <strong>Saturn V</strong> rocket from <a href="http://en.wikipedia.org/wiki/Kennedy_Space_Center" title="Kennedy Space Center">Kennedy Space Center</a> in Merritt Island, Florida on July 16, Apollo 11 was the fifth manned mission of <a href="http://en.wikipedia.org/wiki/NASA" title="NASA">NASA</a>&#39;s Apollo program. The Apollo spacecraft had three parts:</p>\r
+\r
+               <ol>\r
+                       <li><strong>Command Module</strong> with a cabin for the three astronauts which was the only part which landed back on Earth</li>\r
+                       <li><strong>Service Module</strong> which supported the Command Module with propulsion, electrical power, oxygen and water</li>\r
+                       <li><strong>Lunar Module</strong> for landing on the Moon.</li>\r
+               </ol>\r
+\r
+               <p>After being sent to the Moon by the Saturn V&#39;s upper stage, the astronauts separated the spacecraft from it and travelled for three days until they entered into lunar orbit. Armstrong and Aldrin then moved into the Lunar Module and landed in the <a href="http://en.wikipedia.org/wiki/Mare_Tranquillitatis" title="Mare Tranquillitatis">Sea of Tranquility</a>. They stayed a total of about 21 and a half hours on the lunar surface. After lifting off in the upper part of the Lunar Module and rejoining Collins in the Command Module, they returned to Earth and landed in the <a href="http://en.wikipedia.org/wiki/Pacific_Ocean" title="Pacific Ocean">Pacific Ocean</a> on July 24.</p>\r
+\r
+               <hr />\r
+               <p style="text-align: right;"><small>Source: <a href="http://en.wikipedia.org/wiki/Apollo_11">Wikipedia.org</a></small></p>\r
+       </div>\r
+\r
+       <script>\r
+               // We need to turn off the automatic editor creation first.\r
+               CKEDITOR.disableAutoInline = true;\r
+\r
+               var editor = CKEDITOR.inline( 'editable' );\r
+       </script>\r
+       <div id="footer">\r
+               <hr>\r
+               <p contenteditable="true">\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">\r
+                               http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a>\r
+                       - Frederico Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/inlinetextarea.html b/js/ckeditor/samples/old/inlinetextarea.html
new file mode 100644 (file)
index 0000000..504d181
--- /dev/null
@@ -0,0 +1,113 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>Replace Textarea with Inline Editor &mdash; CKEditor Sample</title>\r
+       <script src="../../ckeditor.js"></script>\r
+       <link href="sample.css" rel="stylesheet">\r
+       <style>\r
+\r
+               /* Style the CKEditor element to look like a textfield */\r
+               .cke_textarea_inline\r
+               {\r
+                       padding: 10px;\r
+                       height: 200px;\r
+                       overflow: auto;\r
+\r
+                       border: 1px solid gray;\r
+                       -webkit-appearance: textfield;\r
+               }\r
+\r
+       </style>\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="index.html">CKEditor Samples</a> &raquo; Replace Textarea with Inline Editor\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out its <a href="http://sdk.ckeditor.com/samples/inline.html">brand new version in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>\r
+                       You can also create an inline editor from a <code>textarea</code>\r
+                       element. In this case the <code>textarea</code> will be replaced\r
+                       by a <code>div</code> element with inline editing enabled.\r
+               </p>\r
+<pre class="samples">\r
+// "article-body" is the name of a textarea element.\r
+var editor = CKEDITOR.inline( 'article-body' );\r
+</pre>\r
+       </div>\r
+       <form action="sample_posteddata.php" method="post">\r
+               <h2>This is a sample form with some fields</h2>\r
+               <p>\r
+                       Title:<br>\r
+                       <input type="text" name="title" value="Sample Form"></p>\r
+               <p>\r
+                       Article Body (Textarea converted to CKEditor):<br>\r
+                       <textarea name="article-body" style="height: 200px">\r
+                               &lt;h2&gt;Technical details &lt;a id="tech-details" name="tech-details"&gt;&lt;/a&gt;&lt;/h2&gt;\r
+\r
+                               &lt;table align="right" border="1" bordercolor="#ccc" cellpadding="5" cellspacing="0" style="border-collapse:collapse;margin:10px 0 10px 15px;"&gt;\r
+                                       &lt;caption&gt;&lt;strong&gt;Mission crew&lt;/strong&gt;&lt;/caption&gt;\r
+                                       &lt;thead&gt;\r
+                                       &lt;tr&gt;\r
+                                               &lt;th scope="col"&gt;Position&lt;/th&gt;\r
+                                               &lt;th scope="col"&gt;Astronaut&lt;/th&gt;\r
+                                       &lt;/tr&gt;\r
+                                       &lt;/thead&gt;\r
+                                       &lt;tbody&gt;\r
+                                       &lt;tr&gt;\r
+                                               &lt;td&gt;Commander&lt;/td&gt;\r
+                                               &lt;td&gt;Neil A. Armstrong&lt;/td&gt;\r
+                                       &lt;/tr&gt;\r
+                                       &lt;tr&gt;\r
+                                               &lt;td&gt;Command Module Pilot&lt;/td&gt;\r
+                                               &lt;td&gt;Michael Collins&lt;/td&gt;\r
+                                       &lt;/tr&gt;\r
+                                       &lt;tr&gt;\r
+                                               &lt;td&gt;Lunar Module Pilot&lt;/td&gt;\r
+                                               &lt;td&gt;Edwin &quot;Buzz&quot; E. Aldrin, Jr.&lt;/td&gt;\r
+                                       &lt;/tr&gt;\r
+                                       &lt;/tbody&gt;\r
+                               &lt;/table&gt;\r
+\r
+                               &lt;p&gt;Launched by a &lt;strong&gt;Saturn V&lt;/strong&gt; rocket from &lt;a href="http://en.wikipedia.org/wiki/Kennedy_Space_Center" title="Kennedy Space Center"&gt;Kennedy Space Center&lt;/a&gt; in Merritt Island, Florida on July 16, Apollo 11 was the fifth manned mission of &lt;a href="http://en.wikipedia.org/wiki/NASA" title="NASA"&gt;NASA&lt;/a&gt;&#39;s Apollo program. The Apollo spacecraft had three parts:&lt;/p&gt;\r
+\r
+                               &lt;ol&gt;\r
+                                       &lt;li&gt;&lt;strong&gt;Command Module&lt;/strong&gt; with a cabin for the three astronauts which was the only part which landed back on Earth&lt;/li&gt;\r
+                                       &lt;li&gt;&lt;strong&gt;Service Module&lt;/strong&gt; which supported the Command Module with propulsion, electrical power, oxygen and water&lt;/li&gt;\r
+                                       &lt;li&gt;&lt;strong&gt;Lunar Module&lt;/strong&gt; for landing on the Moon.&lt;/li&gt;\r
+                               &lt;/ol&gt;\r
+\r
+                               &lt;p&gt;After being sent to the Moon by the Saturn V&#39;s upper stage, the astronauts separated the spacecraft from it and travelled for three days until they entered into lunar orbit. Armstrong and Aldrin then moved into the Lunar Module and landed in the &lt;a href="http://en.wikipedia.org/wiki/Mare_Tranquillitatis" title="Mare Tranquillitatis"&gt;Sea of Tranquility&lt;/a&gt;. They stayed a total of about 21 and a half hours on the lunar surface. After lifting off in the upper part of the Lunar Module and rejoining Collins in the Command Module, they returned to Earth and landed in the &lt;a href="http://en.wikipedia.org/wiki/Pacific_Ocean" title="Pacific Ocean"&gt;Pacific Ocean&lt;/a&gt; on July 24.&lt;/p&gt;\r
+\r
+                               &lt;hr /&gt;\r
+                               &lt;p style="text-align: right;"&gt;&lt;small&gt;Source: &lt;a href="http://en.wikipedia.org/wiki/Apollo_11"&gt;Wikipedia.org&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;\r
+                       </textarea>\r
+               </p>\r
+               <p>\r
+                       <input type="submit" value="Submit">\r
+               </p>\r
+       </form>\r
+\r
+       <script>\r
+               CKEDITOR.inline( 'article-body' );\r
+       </script>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">\r
+                               http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a>\r
+                       - Frederico Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/jquery.html b/js/ckeditor/samples/old/jquery.html
new file mode 100644 (file)
index 0000000..95e43ea
--- /dev/null
@@ -0,0 +1,103 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>jQuery Adapter &mdash; CKEditor Sample</title>\r
+       <script src="http://code.jquery.com/jquery-1.11.0.min.js"></script>\r
+       <script src="../../ckeditor.js"></script>\r
+       <script src="../../adapters/jquery.js"></script>\r
+       <link href="sample.css" rel="stylesheet">\r
+       <style>\r
+\r
+               #editable\r
+               {\r
+                       padding: 10px;\r
+                       float: left;\r
+               }\r
+\r
+       </style>\r
+       <script>\r
+\r
+               CKEDITOR.disableAutoInline = true;\r
+\r
+               $( document ).ready( function() {\r
+                       $( '#editor1' ).ckeditor(); // Use CKEDITOR.replace() if element is <textarea>.\r
+                       $( '#editable' ).ckeditor(); // Use CKEDITOR.inline().\r
+               } );\r
+\r
+               function setValue() {\r
+                       $( '#editor1' ).val( $( 'input#val' ).val() );\r
+               }\r
+\r
+       </script>\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="index.html" id="a-test">CKEditor Samples</a> &raquo; Create Editors with jQuery\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out the <a href="http://sdk.ckeditor.com/">brand new samples in CKEditor SDK</a>.\r
+       </div>\r
+       <form action="sample_posteddata.php" method="post">\r
+               <div class="description">\r
+                       <p>\r
+                               This sample shows how to use the <a href="http://docs.ckeditor.com/#!/guide/dev_jquery">jQuery adapter</a>.\r
+                               Note that you have to include both CKEditor and jQuery scripts before including the adapter.\r
+                       </p>\r
+\r
+<pre class="samples">\r
+&lt;script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"&gt;&lt;/script&gt;\r
+&lt;script src="/ckedit../../ckeditor.js"&gt;&lt;/script&gt;\r
+&lt;script src="/ckeditor/adapters/jquery.js"&gt;&lt;/script&gt;\r
+</pre>\r
+\r
+                       <p>Then you can replace HTML elements with a CKEditor instance using the <code>ckeditor()</code> method.</p>\r
+\r
+<pre class="samples">\r
+$( document ).ready( function() {\r
+       $( 'textarea#editor1' ).ckeditor();\r
+} );\r
+</pre>\r
+               </div>\r
+\r
+               <h2 class="samples">Inline Example</h2>\r
+\r
+               <div id="editable" contenteditable="true">\r
+                       <p><img alt="Saturn V carrying Apollo 11" class="right" src="assets/sample.jpg"/><b>Apollo 11</b> was the spaceflight that landed the first humans, Americans <a href="http://en.wikipedia.org/wiki/Neil_Armstrong" title="Neil Armstrong">Neil Armstrong</a> and <a href="http://en.wikipedia.org/wiki/Buzz_Aldrin" title="Buzz Aldrin">Buzz Aldrin</a>, on the Moon on July 20, 1969, at 20:18 UTC. Armstrong became the first to step onto the lunar surface 6 hours later on July 21 at 02:56 UTC.</p>\r
+                       <p>Armstrong spent about <s>three and a half</s> two and a half hours outside the spacecraft, Aldrin slightly less; and together they collected 47.5 pounds (21.5&nbsp;kg) of lunar material for return to Earth. A third member of the mission, <a href="http://en.wikipedia.org/wiki/Michael_Collins_(astronaut)" title="Michael Collins (astronaut)">Michael Collins</a>, piloted the <a href="http://en.wikipedia.org/wiki/Apollo_Command/Service_Module" title="Apollo Command/Service Module">command</a> spacecraft alone in lunar orbit until Armstrong and Aldrin returned to it for the trip back to Earth.\r
+                       <p>Broadcast on live TV to a world-wide audience, Armstrong stepped onto the lunar surface and described the event as:</p>\r
+                       <blockquote><p>One small step for [a] man, one giant leap for mankind.</p></blockquote> <p>Apollo 11 effectively ended the <a href="http://en.wikipedia.org/wiki/Space_Race" title="Space Race">Space Race</a> and fulfilled a national goal proposed in 1961 by the late U.S. President <a href="http://en.wikipedia.org/wiki/John_F._Kennedy" title="John F. Kennedy">John F. Kennedy</a> in a speech before the United States Congress:</p> <blockquote><p>[...] before this decade is out, of landing a man on the Moon and returning him safely to the Earth.</p></blockquote>\r
+               </div>\r
+\r
+               <br style="clear: both">\r
+\r
+               <h2 class="samples">Classic (iframe-based) Example</h2>\r
+\r
+               <textarea cols="80" id="editor1" name="editor1" rows="10">\r
+                       &lt;h2&gt;Technical details &lt;a id=&quot;tech-details&quot; name=&quot;tech-details&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;table align=&quot;right&quot; border=&quot;1&quot; bordercolor=&quot;#ccc&quot; cellpadding=&quot;5&quot; cellspacing=&quot;0&quot; style=&quot;border-collapse:collapse;margin:10px 0 10px 15px;&quot;&gt; &lt;caption&gt;&lt;strong&gt;Mission crew&lt;/strong&gt;&lt;/caption&gt; &lt;thead&gt; &lt;tr&gt; &lt;th scope=&quot;col&quot;&gt;Position&lt;/th&gt; &lt;th scope=&quot;col&quot;&gt;Astronaut&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;Commander&lt;/td&gt; &lt;td&gt;Neil A. Armstrong&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Command Module Pilot&lt;/td&gt; &lt;td&gt;Michael Collins&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Lunar Module Pilot&lt;/td&gt; &lt;td&gt;Edwin &amp;quot;Buzz&amp;quot; E. Aldrin, Jr.&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;Launched by a &lt;strong&gt;Saturn V&lt;/strong&gt; rocket from &lt;a href=&quot;http://en.wikipedia.org/wiki/Kennedy_Space_Center&quot; title=&quot;Kennedy Space Center&quot;&gt;Kennedy Space Center&lt;/a&gt; in Merritt Island, Florida on July 16, Apollo 11 was the fifth manned mission of &lt;a href=&quot;http://en.wikipedia.org/wiki/NASA&quot; title=&quot;NASA&quot;&gt;NASA&lt;/a&gt;&amp;#39;s Apollo program. The Apollo spacecraft had three parts:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;strong&gt;Command Module&lt;/strong&gt; with a cabin for the three astronauts which was the only part which landed back on Earth&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Service Module&lt;/strong&gt; which supported the Command Module with propulsion, electrical power, oxygen and water&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Lunar Module&lt;/strong&gt; for landing on the Moon.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;After being sent to the Moon by the Saturn V&amp;#39;s upper stage, the astronauts separated the spacecraft from it and travelled for three days until they entered into lunar orbit. Armstrong and Aldrin then moved into the Lunar Module and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Mare_Tranquillitatis&quot; title=&quot;Mare Tranquillitatis&quot;&gt;Sea of Tranquility&lt;/a&gt;. They stayed a total of about 21 and a half hours on the lunar surface. After lifting off in the upper part of the Lunar Module and rejoining Collins in the Command Module, they returned to Earth and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Pacific_Ocean&quot; title=&quot;Pacific Ocean&quot;&gt;Pacific Ocean&lt;/a&gt; on July 24.&lt;/p&gt; &lt;hr/&gt; &lt;p style=&quot;text-align: right;&quot;&gt;&lt;small&gt;Source: &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_11&quot;&gt;Wikipedia.org&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;\r
+               </textarea>\r
+\r
+               <p style="overflow: hidden">\r
+                       <input style="float: left" type="submit" value="Submit">\r
+                       <span style="float: right">\r
+                               <input type="text" id="val" value="I'm using jQuery val()!" size="30">\r
+                               <input onclick="setValue();" type="button" value="Set value">\r
+                       </span>\r
+               </p>\r
+       </form>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/readonly.html b/js/ckeditor/samples/old/readonly.html
new file mode 100644 (file)
index 0000000..edd1118
--- /dev/null
@@ -0,0 +1,76 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>Using the CKEditor Read-Only API &mdash; CKEditor Sample</title>\r
+       <script src="../../ckeditor.js"></script>\r
+       <link rel="stylesheet" href="sample.css">\r
+       <script>\r
+\r
+               var editor;\r
+\r
+               // The instanceReady event is fired, when an instance of CKEditor has finished\r
+               // its initialization.\r
+               CKEDITOR.on( 'instanceReady', function( ev ) {\r
+                       editor = ev.editor;\r
+\r
+                       // Show this "on" button.\r
+                       document.getElementById( 'readOnlyOn' ).style.display = '';\r
+\r
+                       // Event fired when the readOnly property changes.\r
+                       editor.on( 'readOnly', function() {\r
+                               document.getElementById( 'readOnlyOn' ).style.display = this.readOnly ? 'none' : '';\r
+                               document.getElementById( 'readOnlyOff' ).style.display = this.readOnly ? '' : 'none';\r
+                       });\r
+               });\r
+\r
+               function toggleReadOnly( isReadOnly ) {\r
+                       // Change the read-only state of the editor.\r
+                       // http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-setReadOnly\r
+                       editor.setReadOnly( isReadOnly );\r
+               }\r
+\r
+       </script>\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="index.html">CKEditor Samples</a> &raquo; Using the CKEditor Read-Only API\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out its <a href="http://sdk.ckeditor.com/samples/readonly.html">brand new version in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>\r
+                       This sample shows how to use the\r
+                       <code><a class="samples" href="http://docs.ckeditor.com/#!/api/CKEDITOR.editor-method-setReadOnly">setReadOnly</a></code>\r
+                       API to put editor into the read-only state that makes it impossible for users to change the editor contents.\r
+               </p>\r
+               <p>\r
+                       For details on how to create this setup check the source code of this sample page.\r
+               </p>\r
+       </div>\r
+       <form action="sample_posteddata.php" method="post">\r
+               <p>\r
+                       <textarea class="ckeditor" id="editor1" name="editor1" cols="100" rows="10">&lt;p&gt;This is some &lt;strong&gt;sample text&lt;/strong&gt;. You are using &lt;a href="http://ckeditor.com/"&gt;CKEditor&lt;/a&gt;.&lt;/p&gt;</textarea>\r
+               </p>\r
+               <p>\r
+                       <input id="readOnlyOn" onclick="toggleReadOnly();" type="button" value="Make it read-only" style="display:none">\r
+                       <input id="readOnlyOff" onclick="toggleReadOnly( false );" type="button" value="Make it editable again" style="display:none">\r
+               </p>\r
+       </form>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/replacebyclass.html b/js/ckeditor/samples/old/replacebyclass.html
new file mode 100644 (file)
index 0000000..2edbf49
--- /dev/null
@@ -0,0 +1,60 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>Replace Textareas by Class Name &mdash; CKEditor Sample</title>\r
+       <script src="../../ckeditor.js"></script>\r
+       <link rel="stylesheet" href="sample.css">\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="index.html">CKEditor Samples</a> &raquo; Replace Textarea Elements by Class Name\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out the <a href="http://sdk.ckeditor.com/">brand new samples in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>\r
+                       This sample shows how to automatically replace all <code>&lt;textarea&gt;</code> elements\r
+                       of a given class with a CKEditor instance.\r
+               </p>\r
+               <p>\r
+                       To replace a <code>&lt;textarea&gt;</code> element, simply assign it the <code>ckeditor</code>\r
+                       class, as in the code below:\r
+               </p>\r
+<pre class="samples">\r
+&lt;textarea <strong>class="ckeditor</strong>" name="editor1"&gt;&lt;/textarea&gt;\r
+</pre>\r
+               <p>\r
+                       Note that other <code>&lt;textarea&gt;</code> attributes (like <code>id</code> or <code>name</code>) need to be adjusted to your document.\r
+               </p>\r
+       </div>\r
+       <form action="sample_posteddata.php" method="post">\r
+               <p>\r
+                       <label for="editor1">\r
+                               Editor 1:\r
+                       </label>\r
+                       <textarea class="ckeditor" cols="80" id="editor1" name="editor1" rows="10">\r
+                               &lt;h1&gt;&lt;img alt=&quot;Saturn V carrying Apollo 11&quot; class=&quot;right&quot; src=&quot;assets/sample.jpg&quot;/&gt; Apollo 11&lt;/h1&gt; &lt;p&gt;&lt;b&gt;Apollo 11&lt;/b&gt; was the spaceflight that landed the first humans, Americans &lt;a href=&quot;http://en.wikipedia.org/wiki/Neil_Armstrong&quot; title=&quot;Neil Armstrong&quot;&gt;Neil Armstrong&lt;/a&gt; and &lt;a href=&quot;http://en.wikipedia.org/wiki/Buzz_Aldrin&quot; title=&quot;Buzz Aldrin&quot;&gt;Buzz Aldrin&lt;/a&gt;, on the Moon on July 20, 1969, at 20:18 UTC. Armstrong became the first to step onto the lunar surface 6 hours later on July 21 at 02:56 UTC.&lt;/p&gt; &lt;p&gt;Armstrong spent about &lt;s&gt;three and a half&lt;/s&gt; two and a half hours outside the spacecraft, Aldrin slightly less; and together they collected 47.5 pounds (21.5&amp;nbsp;kg) of lunar material for return to Earth. A third member of the mission, &lt;a href=&quot;http://en.wikipedia.org/wiki/Michael_Collins_(astronaut)&quot; title=&quot;Michael Collins (astronaut)&quot;&gt;Michael Collins&lt;/a&gt;, piloted the &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_Command/Service_Module&quot; title=&quot;Apollo Command/Service Module&quot;&gt;command&lt;/a&gt; spacecraft alone in lunar orbit until Armstrong and Aldrin returned to it for the trip back to Earth.&lt;/p&gt; &lt;h2&gt;Broadcasting and &lt;em&gt;quotes&lt;/em&gt; &lt;a id=&quot;quotes&quot; name=&quot;quotes&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;p&gt;Broadcast on live TV to a world-wide audience, Armstrong stepped onto the lunar surface and described the event as:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;One small step for [a] man, one giant leap for mankind.&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;Apollo 11 effectively ended the &lt;a href=&quot;http://en.wikipedia.org/wiki/Space_Race&quot; title=&quot;Space Race&quot;&gt;Space Race&lt;/a&gt; and fulfilled a national goal proposed in 1961 by the late U.S. President &lt;a href=&quot;http://en.wikipedia.org/wiki/John_F._Kennedy&quot; title=&quot;John F. Kennedy&quot;&gt;John F. Kennedy&lt;/a&gt; in a speech before the United States Congress:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;[...] before this decade is out, of landing a man on the Moon and returning him safely to the Earth.&lt;/p&gt;&lt;/blockquote&gt; &lt;h2&gt;Technical details &lt;a id=&quot;tech-details&quot; name=&quot;tech-details&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;table align=&quot;right&quot; border=&quot;1&quot; bordercolor=&quot;#ccc&quot; cellpadding=&quot;5&quot; cellspacing=&quot;0&quot; style=&quot;border-collapse:collapse;margin:10px 0 10px 15px;&quot;&gt; &lt;caption&gt;&lt;strong&gt;Mission crew&lt;/strong&gt;&lt;/caption&gt; &lt;thead&gt; &lt;tr&gt; &lt;th scope=&quot;col&quot;&gt;Position&lt;/th&gt; &lt;th scope=&quot;col&quot;&gt;Astronaut&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;Commander&lt;/td&gt; &lt;td&gt;Neil A. Armstrong&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Command Module Pilot&lt;/td&gt; &lt;td&gt;Michael Collins&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Lunar Module Pilot&lt;/td&gt; &lt;td&gt;Edwin &amp;quot;Buzz&amp;quot; E. Aldrin, Jr.&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;Launched by a &lt;strong&gt;Saturn V&lt;/strong&gt; rocket from &lt;a href=&quot;http://en.wikipedia.org/wiki/Kennedy_Space_Center&quot; title=&quot;Kennedy Space Center&quot;&gt;Kennedy Space Center&lt;/a&gt; in Merritt Island, Florida on July 16, Apollo 11 was the fifth manned mission of &lt;a href=&quot;http://en.wikipedia.org/wiki/NASA&quot; title=&quot;NASA&quot;&gt;NASA&lt;/a&gt;&amp;#39;s Apollo program. The Apollo spacecraft had three parts:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;strong&gt;Command Module&lt;/strong&gt; with a cabin for the three astronauts which was the only part which landed back on Earth&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Service Module&lt;/strong&gt; which supported the Command Module with propulsion, electrical power, oxygen and water&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Lunar Module&lt;/strong&gt; for landing on the Moon.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;After being sent to the Moon by the Saturn V&amp;#39;s upper stage, the astronauts separated the spacecraft from it and travelled for three days until they entered into lunar orbit. Armstrong and Aldrin then moved into the Lunar Module and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Mare_Tranquillitatis&quot; title=&quot;Mare Tranquillitatis&quot;&gt;Sea of Tranquility&lt;/a&gt;. They stayed a total of about 21 and a half hours on the lunar surface. After lifting off in the upper part of the Lunar Module and rejoining Collins in the Command Module, they returned to Earth and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Pacific_Ocean&quot; title=&quot;Pacific Ocean&quot;&gt;Pacific Ocean&lt;/a&gt; on July 24.&lt;/p&gt; &lt;hr/&gt; &lt;p style=&quot;text-align: right;&quot;&gt;&lt;small&gt;Source: &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_11&quot;&gt;Wikipedia.org&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;\r
+                       </textarea>\r
+               </p>\r
+               <p>\r
+                       <input type="submit" value="Submit">\r
+               </p>\r
+       </form>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/replacebycode.html b/js/ckeditor/samples/old/replacebycode.html
new file mode 100644 (file)
index 0000000..36ace8b
--- /dev/null
@@ -0,0 +1,59 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>Replace Textarea by Code &mdash; CKEditor Sample</title>\r
+       <script src="../../ckeditor.js"></script>\r
+       <link href="sample.css" rel="stylesheet">\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="index.html">CKEditor Samples</a> &raquo; Replace Textarea Elements Using JavaScript Code\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out its <a href="http://sdk.ckeditor.com/samples/classic.html">brand new version in CKEditor SDK</a>.\r
+       </div>\r
+       <form action="sample_posteddata.php" method="post">\r
+               <div class="description">\r
+                       <p>\r
+                               This editor is using an <code>&lt;iframe&gt;</code> element-based editing area, provided by the <strong>Wysiwygarea</strong> plugin.\r
+                       </p>\r
+<pre class="samples">\r
+CKEDITOR.replace( '<em>textarea_id</em>' )\r
+</pre>\r
+               </div>\r
+               <textarea cols="80" id="editor1" name="editor1" rows="10">\r
+                       &lt;h1&gt;&lt;img alt=&quot;Saturn V carrying Apollo 11&quot; class=&quot;right&quot; src=&quot;assets/sample.jpg&quot;/&gt; Apollo 11&lt;/h1&gt; &lt;p&gt;&lt;b&gt;Apollo 11&lt;/b&gt; was the spaceflight that landed the first humans, Americans &lt;a href=&quot;http://en.wikipedia.org/wiki/Neil_Armstrong&quot; title=&quot;Neil Armstrong&quot;&gt;Neil Armstrong&lt;/a&gt; and &lt;a href=&quot;http://en.wikipedia.org/wiki/Buzz_Aldrin&quot; title=&quot;Buzz Aldrin&quot;&gt;Buzz Aldrin&lt;/a&gt;, on the Moon on July 20, 1969, at 20:18 UTC. Armstrong became the first to step onto the lunar surface 6 hours later on July 21 at 02:56 UTC.&lt;/p&gt; &lt;p&gt;Armstrong spent about &lt;s&gt;three and a half&lt;/s&gt; two and a half hours outside the spacecraft, Aldrin slightly less; and together they collected 47.5 pounds (21.5&amp;nbsp;kg) of lunar material for return to Earth. A third member of the mission, &lt;a href=&quot;http://en.wikipedia.org/wiki/Michael_Collins_(astronaut)&quot; title=&quot;Michael Collins (astronaut)&quot;&gt;Michael Collins&lt;/a&gt;, piloted the &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_Command/Service_Module&quot; title=&quot;Apollo Command/Service Module&quot;&gt;command&lt;/a&gt; spacecraft alone in lunar orbit until Armstrong and Aldrin returned to it for the trip back to Earth.&lt;/p&gt; &lt;h2&gt;Broadcasting and &lt;em&gt;quotes&lt;/em&gt; &lt;a id=&quot;quotes&quot; name=&quot;quotes&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;p&gt;Broadcast on live TV to a world-wide audience, Armstrong stepped onto the lunar surface and described the event as:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;One small step for [a] man, one giant leap for mankind.&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;Apollo 11 effectively ended the &lt;a href=&quot;http://en.wikipedia.org/wiki/Space_Race&quot; title=&quot;Space Race&quot;&gt;Space Race&lt;/a&gt; and fulfilled a national goal proposed in 1961 by the late U.S. President &lt;a href=&quot;http://en.wikipedia.org/wiki/John_F._Kennedy&quot; title=&quot;John F. Kennedy&quot;&gt;John F. Kennedy&lt;/a&gt; in a speech before the United States Congress:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;[...] before this decade is out, of landing a man on the Moon and returning him safely to the Earth.&lt;/p&gt;&lt;/blockquote&gt; &lt;h2&gt;Technical details &lt;a id=&quot;tech-details&quot; name=&quot;tech-details&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;table align=&quot;right&quot; border=&quot;1&quot; bordercolor=&quot;#ccc&quot; cellpadding=&quot;5&quot; cellspacing=&quot;0&quot; style=&quot;border-collapse:collapse;margin:10px 0 10px 15px;&quot;&gt; &lt;caption&gt;&lt;strong&gt;Mission crew&lt;/strong&gt;&lt;/caption&gt; &lt;thead&gt; &lt;tr&gt; &lt;th scope=&quot;col&quot;&gt;Position&lt;/th&gt; &lt;th scope=&quot;col&quot;&gt;Astronaut&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;Commander&lt;/td&gt; &lt;td&gt;Neil A. Armstrong&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Command Module Pilot&lt;/td&gt; &lt;td&gt;Michael Collins&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Lunar Module Pilot&lt;/td&gt; &lt;td&gt;Edwin &amp;quot;Buzz&amp;quot; E. Aldrin, Jr.&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;Launched by a &lt;strong&gt;Saturn V&lt;/strong&gt; rocket from &lt;a href=&quot;http://en.wikipedia.org/wiki/Kennedy_Space_Center&quot; title=&quot;Kennedy Space Center&quot;&gt;Kennedy Space Center&lt;/a&gt; in Merritt Island, Florida on July 16, Apollo 11 was the fifth manned mission of &lt;a href=&quot;http://en.wikipedia.org/wiki/NASA&quot; title=&quot;NASA&quot;&gt;NASA&lt;/a&gt;&amp;#39;s Apollo program. The Apollo spacecraft had three parts:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;strong&gt;Command Module&lt;/strong&gt; with a cabin for the three astronauts which was the only part which landed back on Earth&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Service Module&lt;/strong&gt; which supported the Command Module with propulsion, electrical power, oxygen and water&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Lunar Module&lt;/strong&gt; for landing on the Moon.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;After being sent to the Moon by the Saturn V&amp;#39;s upper stage, the astronauts separated the spacecraft from it and travelled for three days until they entered into lunar orbit. Armstrong and Aldrin then moved into the Lunar Module and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Mare_Tranquillitatis&quot; title=&quot;Mare Tranquillitatis&quot;&gt;Sea of Tranquility&lt;/a&gt;. They stayed a total of about 21 and a half hours on the lunar surface. After lifting off in the upper part of the Lunar Module and rejoining Collins in the Command Module, they returned to Earth and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Pacific_Ocean&quot; title=&quot;Pacific Ocean&quot;&gt;Pacific Ocean&lt;/a&gt; on July 24.&lt;/p&gt; &lt;hr/&gt; &lt;p style=&quot;text-align: right;&quot;&gt;&lt;small&gt;Source: &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_11&quot;&gt;Wikipedia.org&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;\r
+               </textarea>\r
+               <script>\r
+\r
+                       // This call can be placed at any point after the\r
+                       // <textarea>, or inside a <head><script> in a\r
+                       // window.onload event handler.\r
+\r
+                       // Replace the <textarea id="editor"> with an CKEditor\r
+                       // instance, using default configurations.\r
+\r
+                       CKEDITOR.replace( 'editor1' );\r
+\r
+               </script>\r
+               <p>\r
+                       <input type="submit" value="Submit">\r
+               </p>\r
+       </form>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/sample.css b/js/ckeditor/samples/old/sample.css
new file mode 100644 (file)
index 0000000..af866fe
--- /dev/null
@@ -0,0 +1,357 @@
+/*\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+*/\r
+\r
+html, body, h1, h2, h3, h4, h5, h6, div, span, blockquote, p, address, form, fieldset, img, ul, ol, dl, dt, dd, li, hr, table, td, th, strong, em, sup, sub, dfn, ins, del, q, cite, var, samp, code, kbd, tt, pre\r
+{\r
+       line-height: 1.5;\r
+}\r
+\r
+body\r
+{\r
+       padding: 10px 30px;\r
+}\r
+\r
+input, textarea, select, option, optgroup, button, td, th\r
+{\r
+       font-size: 100%;\r
+}\r
+\r
+pre\r
+{\r
+       -moz-tab-size: 4;\r
+       tab-size: 4;\r
+}\r
+\r
+pre, code, kbd, samp, tt\r
+{\r
+       font-family: monospace,monospace;\r
+       font-size: 1em;\r
+}\r
+\r
+body {\r
+       width: 960px;\r
+       margin: 0 auto;\r
+}\r
+\r
+code\r
+{\r
+       background: #f3f3f3;\r
+       border: 1px solid #ddd;\r
+       padding: 1px 4px;\r
+       border-radius: 3px;\r
+}\r
+\r
+abbr\r
+{\r
+       border-bottom: 1px dotted #555;\r
+       cursor: pointer;\r
+}\r
+\r
+.new, .beta\r
+{\r
+       text-transform: uppercase;\r
+       font-size: 10px;\r
+       font-weight: bold;\r
+       padding: 1px 4px;\r
+       margin: 0 0 0 5px;\r
+       color: #fff;\r
+       float: right;\r
+       border-radius: 3px;\r
+}\r
+\r
+.new\r
+{\r
+       background: #FF7E00;\r
+       border: 1px solid #DA8028;\r
+       text-shadow: 0 1px 0 #C97626;\r
+\r
+       box-shadow: 0 2px 3px 0 #FFA54E inset;\r
+}\r
+\r
+.beta\r
+{\r
+       background: #18C0DF;\r
+       border: 1px solid #19AAD8;\r
+       text-shadow: 0 1px 0 #048CAD;\r
+       font-style: italic;\r
+\r
+       box-shadow: 0 2px 3px 0 #50D4FD inset;\r
+}\r
+\r
+h1.samples\r
+{\r
+       color: #0782C1;\r
+       font-size: 200%;\r
+       font-weight: normal;\r
+       margin: 0;\r
+       padding: 0;\r
+}\r
+\r
+h1.samples a\r
+{\r
+       color: #0782C1;\r
+       text-decoration: none;\r
+       border-bottom: 1px dotted #0782C1;\r
+}\r
+\r
+.samples a:hover\r
+{\r
+       border-bottom: 1px dotted #0782C1;\r
+}\r
+\r
+h2.samples\r
+{\r
+       color: #000000;\r
+       font-size: 130%;\r
+       margin: 15px 0 0 0;\r
+       padding: 0;\r
+}\r
+\r
+p, blockquote, address, form, pre, dl, h1.samples, h2.samples\r
+{\r
+       margin-bottom: 15px;\r
+}\r
+\r
+ul.samples\r
+{\r
+       margin-bottom: 15px;\r
+}\r
+\r
+.clear\r
+{\r
+       clear: both;\r
+}\r
+\r
+fieldset\r
+{\r
+       margin: 0;\r
+       padding: 10px;\r
+}\r
+\r
+body, input, textarea\r
+{\r
+       color: #333333;\r
+       font-family: Arial, Helvetica, sans-serif;\r
+}\r
+\r
+body\r
+{\r
+       font-size: 75%;\r
+}\r
+\r
+a.samples\r
+{\r
+       color: #189DE1;\r
+       text-decoration: none;\r
+}\r
+\r
+form\r
+{\r
+       margin: 0;\r
+       padding: 0;\r
+}\r
+\r
+pre.samples\r
+{\r
+       background-color: #F7F7F7;\r
+       border: 1px solid #D7D7D7;\r
+       overflow: auto;\r
+       padding: 0.25em;\r
+       white-space: pre-wrap; /* CSS 2.1 */\r
+       word-wrap: break-word; /* IE7 */\r
+}\r
+\r
+#footer\r
+{\r
+       clear: both;\r
+       padding-top: 10px;\r
+}\r
+\r
+#footer hr\r
+{\r
+       margin: 10px 0 15px 0;\r
+       height: 1px;\r
+       border: solid 1px gray;\r
+       border-bottom: none;\r
+}\r
+\r
+#footer p\r
+{\r
+       margin: 0 10px 10px 10px;\r
+       float: left;\r
+}\r
+\r
+#footer #copy\r
+{\r
+       float: right;\r
+}\r
+\r
+#outputSample\r
+{\r
+       width: 100%;\r
+       table-layout: fixed;\r
+}\r
+\r
+#outputSample thead th\r
+{\r
+       color: #dddddd;\r
+       background-color: #999999;\r
+       padding: 4px;\r
+       white-space: nowrap;\r
+}\r
+\r
+#outputSample tbody th\r
+{\r
+       vertical-align: top;\r
+       text-align: left;\r
+}\r
+\r
+#outputSample pre\r
+{\r
+       margin: 0;\r
+       padding: 0;\r
+}\r
+\r
+.description\r
+{\r
+       border: 1px dotted #B7B7B7;\r
+       margin-bottom: 10px;\r
+       padding: 10px 10px 0;\r
+       overflow: hidden;\r
+}\r
+\r
+label\r
+{\r
+       display: block;\r
+       margin-bottom: 6px;\r
+}\r
+\r
+/**\r
+ *     CKEditor editables are automatically set with the "cke_editable" class\r
+ *     plus cke_editable_(inline|themed) depending on the editor type.\r
+ */\r
+\r
+/* Style a bit the inline editables. */\r
+.cke_editable.cke_editable_inline\r
+{\r
+       cursor: pointer;\r
+}\r
+\r
+/* Once an editable element gets focused, the "cke_focus" class is\r
+   added to it, so we can style it differently. */\r
+.cke_editable.cke_editable_inline.cke_focus\r
+{\r
+       box-shadow: inset 0px 0px 20px 3px #ddd, inset 0 0 1px #000;\r
+       outline: none;\r
+       background: #eee;\r
+       cursor: text;\r
+}\r
+\r
+/* Avoid pre-formatted overflows inline editable. */\r
+.cke_editable_inline pre\r
+{\r
+       white-space: pre-wrap;\r
+       word-wrap: break-word;\r
+}\r
+\r
+/**\r
+ *     Samples index styles.\r
+ */\r
+\r
+.twoColumns,\r
+.twoColumnsLeft,\r
+.twoColumnsRight\r
+{\r
+       overflow: hidden;\r
+}\r
+\r
+.twoColumnsLeft,\r
+.twoColumnsRight\r
+{\r
+       width: 45%;\r
+}\r
+\r
+.twoColumnsLeft\r
+{\r
+       float: left;\r
+}\r
+\r
+.twoColumnsRight\r
+{\r
+       float: right;\r
+}\r
+\r
+dl.samples\r
+{\r
+       padding: 0 0 0 40px;\r
+}\r
+dl.samples > dt\r
+{\r
+       display: list-item;\r
+       list-style-type: disc;\r
+       list-style-position: outside;\r
+       margin: 0 0 3px;\r
+}\r
+dl.samples > dd\r
+{\r
+       margin: 0 0 3px;\r
+}\r
+.warning\r
+{\r
+       color: #ff0000;\r
+       background-color: #FFCCBA;\r
+       border: 2px dotted #ff0000;\r
+       padding: 15px 10px;\r
+       margin: 10px 0;\r
+}\r
+\r
+.warning.deprecated {\r
+       font-size: 1.3em;\r
+}\r
+\r
+/* Used on inline samples */\r
+\r
+blockquote\r
+{\r
+       font-style: italic;\r
+       font-family: Georgia, Times, "Times New Roman", serif;\r
+       padding: 2px 0;\r
+       border-style: solid;\r
+       border-color: #ccc;\r
+       border-width: 0;\r
+}\r
+\r
+.cke_contents_ltr blockquote\r
+{\r
+       padding-left: 20px;\r
+       padding-right: 8px;\r
+       border-left-width: 5px;\r
+}\r
+\r
+.cke_contents_rtl blockquote\r
+{\r
+       padding-left: 8px;\r
+       padding-right: 20px;\r
+       border-right-width: 5px;\r
+}\r
+\r
+img.right {\r
+       border: 1px solid #ccc;\r
+       float: right;\r
+       margin-left: 15px;\r
+       padding: 5px;\r
+}\r
+\r
+img.left {\r
+       border: 1px solid #ccc;\r
+       float: left;\r
+       margin-right: 15px;\r
+       padding: 5px;\r
+}\r
+\r
+.marker\r
+{\r
+       background-color: Yellow;\r
+}\r
diff --git a/js/ckeditor/samples/old/sample.js b/js/ckeditor/samples/old/sample.js
new file mode 100644 (file)
index 0000000..59b64ee
--- /dev/null
@@ -0,0 +1,50 @@
+/**\r
+ * Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+ * For licensing, see LICENSE.md or http://ckeditor.com/license\r
+ */\r
+\r
+// Tool scripts for the sample pages.\r
+// This file can be ignored and is not required to make use of CKEditor.\r
+\r
+( function() {\r
+       CKEDITOR.on( 'instanceReady', function( ev ) {\r
+               // Check for sample compliance.\r
+               var editor = ev.editor,\r
+                       meta = CKEDITOR.document.$.getElementsByName( 'ckeditor-sample-required-plugins' ),\r
+                       requires = meta.length ? CKEDITOR.dom.element.get( meta[ 0 ] ).getAttribute( 'content' ).split( ',' ) : [],\r
+                       missing = [],\r
+                       i;\r
+\r
+               if ( requires.length ) {\r
+                       for ( i = 0; i < requires.length; i++ ) {\r
+                               if ( !editor.plugins[ requires[ i ] ] )\r
+                                       missing.push( '<code>' + requires[ i ] + '</code>' );\r
+                       }\r
+\r
+                       if ( missing.length ) {\r
+                               var warn = CKEDITOR.dom.element.createFromHtml(\r
+                                       '<div class="warning">' +\r
+                                               '<span>To fully experience this demo, the ' + missing.join( ', ' ) + ' plugin' + ( missing.length > 1 ? 's are' : ' is' ) + ' required.</span>' +\r
+                                       '</div>'\r
+                               );\r
+                               warn.insertBefore( editor.container );\r
+                       }\r
+               }\r
+\r
+               // Set icons.\r
+               var doc = new CKEDITOR.dom.document( document ),\r
+                       icons = doc.find( '.button_icon' );\r
+\r
+               for ( i = 0; i < icons.count(); i++ ) {\r
+                       var icon = icons.getItem( i ),\r
+                               name = icon.getAttribute( 'data-icon' ),\r
+                               style = CKEDITOR.skin.getIconStyle( name, ( CKEDITOR.lang.dir == 'rtl' ) );\r
+\r
+                       icon.addClass( 'cke_button_icon' );\r
+                       icon.addClass( 'cke_button__' + name + '_icon' );\r
+                       icon.setAttribute( 'style', style );\r
+                       icon.setStyle( 'float', 'none' );\r
+\r
+               }\r
+       } );\r
+} )();\r
diff --git a/js/ckeditor/samples/old/sample_posteddata.php b/js/ckeditor/samples/old/sample_posteddata.php
new file mode 100644 (file)
index 0000000..866867e
--- /dev/null
@@ -0,0 +1,16 @@
+<?php /* <body><pre>\r
+\r
+-------------------------------------------------------------------------------------------\r
+  CKEditor - Posted Data\r
+\r
+  We are sorry, but your Web server does not support the PHP language used in this script.\r
+\r
+  Please note that CKEditor can be used with any other server-side language than just PHP.\r
+  To save the content created with CKEditor you need to read the POST data on the server\r
+  side and write it to a file or the database.\r
+\r
+  Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+  For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-------------------------------------------------------------------------------------------\r
+\r
+</pre><div style="display:none"></body> */ include "assets/posteddata.php"; ?>\r
diff --git a/js/ckeditor/samples/old/sourcedialog/sourcedialog.html b/js/ckeditor/samples/old/sourcedialog/sourcedialog.html
new file mode 100644 (file)
index 0000000..9232b0e
--- /dev/null
@@ -0,0 +1,121 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>Editing source code in a dialog &mdash; CKEditor Sample</title>\r
+       <script src="../../../ckeditor.js"></script>\r
+       <link rel="stylesheet" href="../../../samples/old/sample.css">\r
+       <meta name="ckeditor-sample-name" content="Editing source code in a dialog">\r
+       <meta name="ckeditor-sample-group" content="Plugins">\r
+       <meta name="ckeditor-sample-description" content="Editing HTML content of both inline and classic editor instances.">\r
+       <meta name="ckeditor-sample-isnew" content="1">\r
+       <style>\r
+\r
+               #editable\r
+               {\r
+                       padding: 10px;\r
+                       float: left;\r
+               }\r
+\r
+       </style>\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="../../../samples/old/index.html">CKEditor Samples</a> &raquo; Editing source code in a dialog\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out its <a href="http://sdk.ckeditor.com/samples/sourcearea.html">brand new version in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>\r
+                       <strong>Sourcedialog</strong> plugin provides an easy way to edit raw HTML content\r
+                       of an editor, similarly to what is possible with <strong>Sourcearea</strong>\r
+                       plugin for classic (<code>iframe</code>-based) instances but using dialogs. Thanks to that, it's also possible\r
+                       to manipulate raw content of inline editor instances.\r
+               </p>\r
+               <p>\r
+                       This plugin extends the toolbar with a button,\r
+                       which opens a dialog window with a source code editor. It works with both classic\r
+                       and inline instances. To enable this\r
+                       plugin, basically add <code>extraPlugins: 'sourcedialog'</code> to editor's\r
+                       config:\r
+               </p>\r
+<pre class="samples">\r
+// Inline editor.\r
+CKEDITOR.inline( 'editable', {\r
+       <strong>extraPlugins: 'sourcedialog'</strong>\r
+});\r
+\r
+// Classic (iframe-based) editor.\r
+CKEDITOR.replace( 'textarea_id', {\r
+       <strong>extraPlugins: 'sourcedialog'</strong>,\r
+       removePlugins: 'sourcearea'\r
+});\r
+</pre>\r
+               <p>\r
+                       Note that you may want to include <code>removePlugins: 'sourcearea'</code>\r
+                       in your config when using <strong>Sourcedialog</strong> in classic editor instances.\r
+                       This prevents feature redundancy.\r
+               </p>\r
+               <p>\r
+                       Note that <code>editable</code> in the code above is the <code>id</code>\r
+                       attribute of the <code>&lt;div&gt;</code> element to be converted into an inline instance.\r
+               </p>\r
+               <p>\r
+                       Note that <code><em>textarea_id</em></code> in the code above is the <code>id</code> attribute of\r
+                       the <code>&lt;textarea&gt;</code> element to be replaced with CKEditor.\r
+               </p>\r
+       </div>\r
+       <div>\r
+               <label for="editor1">\r
+                       Inline editor:\r
+               </label>\r
+               <div id="editor1" contenteditable="true" style="padding: 5px 20px;">\r
+                       <p>This is some <strong>sample text</strong>. You are using <a href="http://ckeditor.com/">CKEditor</a>.</p>\r
+               </div>\r
+       </div>\r
+       <br>\r
+       <div>\r
+               <label for="editor2">\r
+                       Classic editor:\r
+               </label>\r
+               <textarea cols="80" id="editor2" name="editor2" rows="10">\r
+                       This is some &lt;strong&gt;sample text&lt;/strong&gt;. You are using &lt;a href="http://ckeditor.com/"&gt;CKEditor&lt;/a&gt;.\r
+               </textarea>\r
+       </div>\r
+       <script>\r
+\r
+               // We need to turn off the automatic editor creation first.\r
+               CKEDITOR.disableAutoInline = true;\r
+\r
+               var config = {\r
+                       toolbarGroups: [\r
+                               { name: 'mode' },\r
+                               { name: 'basicstyles' },\r
+                               { name: 'links' }\r
+                       ],\r
+                       extraPlugins: 'sourcedialog',\r
+                       removePlugins: 'sourcearea'\r
+               }\r
+\r
+               CKEDITOR.inline( 'editor1', config );\r
+               CKEDITOR.replace( 'editor2', config );\r
+\r
+       </script>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">\r
+                               http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a>\r
+                       - Frederico Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/tabindex.html b/js/ckeditor/samples/old/tabindex.html
new file mode 100644 (file)
index 0000000..82ed647
--- /dev/null
@@ -0,0 +1,78 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>TAB Key-Based Navigation &mdash; CKEditor Sample</title>\r
+       <script src="../../ckeditor.js"></script>\r
+       <link href="sample.css" rel="stylesheet">\r
+       <style>\r
+\r
+               .cke_focused,\r
+               .cke_editable.cke_focused\r
+               {\r
+                       outline: 3px dotted blue !important;\r
+                       *border: 3px dotted blue !important;    /* For IE7 */\r
+               }\r
+\r
+       </style>\r
+       <script>\r
+\r
+               CKEDITOR.on( 'instanceReady', function( evt ) {\r
+                       var editor = evt.editor;\r
+                       editor.setData( 'This editor has it\'s tabIndex set to <strong>' + editor.tabIndex + '</strong>' );\r
+\r
+                       // Apply focus class name.\r
+                       editor.on( 'focus', function() {\r
+                               editor.container.addClass( 'cke_focused' );\r
+                       });\r
+                       editor.on( 'blur', function() {\r
+                               editor.container.removeClass( 'cke_focused' );\r
+                       });\r
+\r
+                       // Put startup focus on the first editor in tab order.\r
+                       if ( editor.tabIndex == 1 )\r
+                               editor.focus();\r
+               });\r
+\r
+       </script>\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="index.html">CKEditor Samples</a> &raquo; TAB Key-Based Navigation\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out its <a href="http://sdk.ckeditor.com/samples/tabindex.html">brand new version in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>\r
+                       This sample shows how tab key navigation among editor instances is\r
+                       affected by the <code>tabIndex</code> attribute from\r
+                       the original page element. Use TAB key to move between the editors.\r
+               </p>\r
+       </div>\r
+       <p>\r
+               <textarea class="ckeditor" cols="80" id="editor4" rows="10" tabindex="1"></textarea>\r
+       </p>\r
+       <div class="ckeditor" contenteditable="true" id="editor1" tabindex="4"></div>\r
+       <p>\r
+               <textarea class="ckeditor" cols="80" id="editor2" rows="10" tabindex="2"></textarea>\r
+       </p>\r
+       <p>\r
+               <textarea class="ckeditor" cols="80" id="editor3" rows="10" tabindex="3"></textarea>\r
+       </p>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/toolbar/toolbar.html b/js/ckeditor/samples/old/toolbar/toolbar.html
new file mode 100644 (file)
index 0000000..e40d2a1
--- /dev/null
@@ -0,0 +1,235 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>Toolbar Configuration &mdash; CKEditor Sample</title>\r
+       <meta name="ckeditor-sample-name" content="Toolbar Configurations">\r
+       <meta name="ckeditor-sample-group" content="Advanced Samples">\r
+       <meta name="ckeditor-sample-description" content="Configuring CKEditor to display full or custom toolbar layout.">\r
+       <script src="../../../ckeditor.js"></script>\r
+       <link href="../../../samples/old/sample.css" rel="stylesheet">\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="../../../samples/old/index.html">CKEditor Samples</a> &raquo; Toolbar Configuration\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out the <a href="../../../samples/toolbarconfigurator/index.html#basic">brand new CKEditor Toolbar Configurator</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>\r
+                       This sample page demonstrates editor with loaded <a href="#fullToolbar">full toolbar</a> (all registered buttons) and, if\r
+                       current editor's configuration modifies default settings, also editor with <a href="#currentToolbar">modified toolbar</a>.\r
+               </p>\r
+\r
+               <p>Since CKEditor 4 there are two ways to configure toolbar buttons.</p>\r
+\r
+               <h2 class="samples">By <a href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-toolbar">config.toolbar</a></h2>\r
+\r
+               <p>\r
+                       You can explicitly define which buttons are displayed in which groups and in which order.\r
+                       This is the more precise setting, but less flexible. If newly added plugin adds its\r
+                       own button you'll have to add it manually to your <code>config.toolbar</code> setting as well.\r
+               </p>\r
+\r
+               <p>To add a CKEditor instance with custom toolbar setting, insert the following JavaScript call to your code:</p>\r
+\r
+               <pre class="samples">\r
+CKEDITOR.replace( <em>'textarea_id'</em>, {\r
+       <strong>toolbar:</strong> [\r
+               { name: 'document', items: [ 'Source', '-', 'NewPage', 'Preview', '-', 'Templates' ] }, // Defines toolbar group with name (used to create voice label) and items in 3 subgroups.\r
+               [ 'Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo' ],                  // Defines toolbar group without name.\r
+               '/',                                                                                                                                                                    // Line break - next group will be placed in new line.\r
+               { name: 'basicstyles', items: [ 'Bold', 'Italic' ] }\r
+       ]\r
+});</pre>\r
+\r
+               <h2 class="samples">By <a href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-toolbarGroups">config.toolbarGroups</a></h2>\r
+\r
+               <p>\r
+                       You can define which groups of buttons (like e.g. <code>basicstyles</code>, <code>clipboard</code>\r
+                       and <code>forms</code>) are displayed and in which order. Registered buttons are associated\r
+                       with toolbar groups by <code>toolbar</code> property in their definition.\r
+                       This setting's advantage is that you don't have to modify toolbar configuration\r
+                       when adding/removing plugins which register their own buttons.\r
+               </p>\r
+\r
+               <p>To add a CKEditor instance with custom toolbar groups setting, insert the following JavaScript call to your code:</p>\r
+\r
+               <pre class="samples">\r
+CKEDITOR.replace( <em>'textarea_id'</em>, {\r
+       <strong>toolbarGroups:</strong> [\r
+               { name: 'document',        groups: [ 'mode', 'document' ] },                    // Displays document group with its two subgroups.\r
+               { name: 'clipboard',   groups: [ 'clipboard', 'undo' ] },                       // Group's name will be used to create voice label.\r
+               '/',                                                                                                                            // Line break - next group will be placed in new line.\r
+               { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] },\r
+               { name: 'links' }\r
+       ]\r
+\r
+       // NOTE: Remember to leave 'toolbar' property with the default value (null).\r
+});</pre>\r
+       </div>\r
+\r
+       <div id="currentToolbar" style="display: none">\r
+               <h2 class="samples">Current toolbar configuration</h2>\r
+               <p>Below you can see editor with current toolbar definition.</p>\r
+               <textarea cols="80" id="editorCurrent" name="editorCurrent" rows="10">&lt;p&gt;This is some &lt;strong&gt;sample text&lt;/strong&gt;. You are using &lt;a href="http://ckeditor.com/"&gt;CKEditor&lt;/a&gt;.&lt;/p&gt;</textarea>\r
+               <pre id="editorCurrentCfg" class="samples"></pre>\r
+       </div>\r
+\r
+       <div id="fullToolbar">\r
+               <h2 class="samples">Full toolbar configuration</h2>\r
+               <p>Below you can see editor with full toolbar, generated automatically by the editor.</p>\r
+               <p>\r
+                       <strong>Note</strong>: To create editor instance with full toolbar you don't have to set anything.\r
+                       Just leave <code>toolbar</code> and <code>toolbarGroups</code> with the default, <code>null</code> values.\r
+               </p>\r
+               <textarea cols="80" id="editorFull" name="editorFull" rows="10">&lt;p&gt;This is some &lt;strong&gt;sample text&lt;/strong&gt;. You are using &lt;a href="http://ckeditor.com/"&gt;CKEditor&lt;/a&gt;.&lt;/p&gt;</textarea>\r
+               <pre id="editorFullCfg" class="samples"></pre>\r
+       </div>\r
+\r
+       <script>\r
+\r
+(function() {\r
+       'use strict';\r
+\r
+       var buttonsNames;\r
+\r
+       CKEDITOR.config.extraPlugins = 'toolbar';\r
+\r
+       CKEDITOR.on( 'instanceReady', function( evt ) {\r
+               var editor = evt.editor,\r
+                       editorCurrent = editor.name == 'editorCurrent',\r
+                       defaultToolbar = !( editor.config.toolbar || editor.config.toolbarGroups || editor.config.removeButtons ),\r
+                       pre = CKEDITOR.document.getById( editor.name + 'Cfg' ),\r
+                       output = '';\r
+\r
+               if ( editorCurrent ) {\r
+                       // If default toolbar configuration has been modified, show "current toolbar" section.\r
+                       if ( !defaultToolbar )\r
+                               CKEDITOR.document.getById( 'currentToolbar' ).show();\r
+                       else\r
+                               return;\r
+               }\r
+\r
+               if ( !buttonsNames )\r
+                       buttonsNames = createButtonsNamesHash( editor.ui.items );\r
+\r
+               // Toolbar isn't set explicitly, so it was created automatically from toolbarGroups.\r
+               if ( !editor.config.toolbar ) {\r
+                       output +=\r
+                               '// Toolbar configuration generated automatically by the editor based on config.toolbarGroups.\n' +\r
+                               dumpToolbarConfiguration( editor ) +\r
+                               '\n\n' +\r
+                               '// Toolbar groups configuration.\n' +\r
+                               dumpToolbarConfiguration( editor, true )\r
+               }\r
+               // Toolbar groups doesn't count in this case - print only toolbar.\r
+               else {\r
+                       output += '// Toolbar configuration.\n' +\r
+                               dumpToolbarConfiguration( editor );\r
+               }\r
+\r
+               // Recreate to avoid old IE from loosing whitespaces on filling <pre> content.\r
+               var preOutput = pre.getOuterHtml().replace( /(?=<\/)/, output );\r
+               CKEDITOR.dom.element.createFromHtml( preOutput ).replace( pre );\r
+       } );\r
+\r
+       CKEDITOR.replace( 'editorCurrent', { height: 100 } );\r
+       CKEDITOR.replace( 'editorFull', {\r
+               // Reset toolbar settings, so full toolbar will be generated automatically.\r
+               toolbar: null,\r
+               toolbarGroups: null,\r
+               removeButtons: null,\r
+               height: 100\r
+       } );\r
+\r
+       function dumpToolbarConfiguration( editor, printGroups ) {\r
+               var output = [],\r
+                       toolbar = editor.toolbar;\r
+\r
+               for ( var i = 0; i < toolbar.length; ++i ) {\r
+                       var group = dumpToolbarGroup( toolbar[ i ], printGroups );\r
+                       if ( group )\r
+                               output.push( group );\r
+               }\r
+\r
+               return 'config.toolbar' + ( printGroups ? 'Groups' : '' ) + ' = [\n\t' + output.join( ',\n\t' ) + '\n];';\r
+       }\r
+\r
+       function dumpToolbarGroup( group, printGroups ) {\r
+               var output = [];\r
+\r
+               if ( typeof group == 'string' )\r
+                       return '\'' + group + '\'';\r
+               if ( CKEDITOR.tools.isArray( group ) )\r
+                       return dumpToolbarItems( group );\r
+               // Skip group when printing entire toolbar configuration and there are no items in this group.\r
+               if ( !printGroups && !group.items )\r
+                       return;\r
+\r
+               if ( group.name )\r
+                       output.push( 'name: \'' + group.name + '\'' );\r
+\r
+               if ( group.groups )\r
+                       output.push( 'groups: ' + dumpToolbarItems( group.groups ) );\r
+\r
+               if ( !printGroups )\r
+                       output.push( 'items: ' + dumpToolbarItems( group.items ) );\r
+\r
+               return '{ ' + output.join( ', ' ) + ' }';\r
+       }\r
+\r
+       function dumpToolbarItems( items ) {\r
+               if ( typeof items == 'string' )\r
+                       return '\'' + items + '\'';\r
+\r
+               var names = [],\r
+                       i, item;\r
+\r
+               for ( var i = 0; i < items.length; ++i ) {\r
+                       item = items[ i ];\r
+                       if ( typeof item == 'string' )\r
+                               names.push( item );\r
+                       else {\r
+                               if ( item.type == CKEDITOR.UI_SEPARATOR )\r
+                                       names.push( '-' );\r
+                               else\r
+                                       names.push( buttonsNames[ item.name ] );\r
+                       }\r
+               }\r
+\r
+               return '[ \'' + names.join( '\', \'' ) + '\' ]';\r
+       }\r
+\r
+       // Creates { 'lowercased': 'LowerCased' } buttons names hash.\r
+       function createButtonsNamesHash( items ) {\r
+               var hash = {},\r
+                       name;\r
+\r
+               for ( name in items ) {\r
+                       hash[ items[ name ].name ] = name;\r
+               }\r
+\r
+               return hash;\r
+       }\r
+\r
+})();\r
+       </script>\r
+\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/uicolor.html b/js/ckeditor/samples/old/uicolor.html
new file mode 100644 (file)
index 0000000..bff1b96
--- /dev/null
@@ -0,0 +1,72 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>UI Color Picker &mdash; CKEditor Sample</title>\r
+       <script src="../../ckeditor.js"></script>\r
+       <link rel="stylesheet" href="sample.css">\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="index.html">CKEditor Samples</a> &raquo; UI Color\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out its <a href="http://sdk.ckeditor.com/samples/uicolor.html">brand new version in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>\r
+                       This sample shows how to automatically replace <code>&lt;textarea&gt;</code> elements\r
+                       with a CKEditor instance with an option to change the color of its user interface.<br>\r
+                       <strong>Note:</strong>The UI skin color feature depends on the CKEditor skin\r
+                       compatibility. The Moono and Kama skins are examples of skins that work with it.\r
+               </p>\r
+       </div>\r
+       <form action="sample_posteddata.php" method="post">\r
+       <p>\r
+               This editor instance has a UI color value defined in configuration to change the skin color,\r
+               To specify the color of the user interface, set the <code>uiColor</code> property:\r
+       </p>\r
+       <pre class="samples">\r
+CKEDITOR.replace( '<em>textarea_id</em>', {\r
+       <strong>uiColor: '#14B8C4'</strong>\r
+});</pre>\r
+       <p>\r
+               Note that <code><em>textarea_id</em></code> in the code above is the <code>id</code> attribute of\r
+               the <code>&lt;textarea&gt;</code> element to be replaced.\r
+       </p>\r
+       <p>\r
+               <textarea cols="80" id="editor1" name="editor1" rows="10">&lt;p&gt;This is some &lt;strong&gt;sample text&lt;/strong&gt;. You are using &lt;a href="http://ckeditor.com/"&gt;CKEditor&lt;/a&gt;.&lt;/p&gt;</textarea>\r
+               <script>\r
+\r
+                       // Replace the <textarea id="editor"> with an CKEditor\r
+                       // instance, using default configurations.\r
+                       CKEDITOR.replace( 'editor1', {\r
+                               uiColor: '#14B8C4',\r
+                               toolbar: [\r
+                                       [ 'Bold', 'Italic', '-', 'NumberedList', 'BulletedList', '-', 'Link', 'Unlink' ],\r
+                                       [ 'FontSize', 'TextColor', 'BGColor' ]\r
+                               ]\r
+                       });\r
+\r
+               </script>\r
+       </p>\r
+       <p>\r
+               <input type="submit" value="Submit">\r
+       </p>\r
+       </form>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/uilanguages.html b/js/ckeditor/samples/old/uilanguages.html
new file mode 100644 (file)
index 0000000..9c1d0b8
--- /dev/null
@@ -0,0 +1,122 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>User Interface Globalization &mdash; CKEditor Sample</title>\r
+       <script src="../../ckeditor.js"></script>\r
+       <script src="assets/uilanguages/languages.js"></script>\r
+       <link rel="stylesheet" href="sample.css">\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="index.html">CKEditor Samples</a> &raquo; User Interface Languages\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out its <a href="http://sdk.ckeditor.com/samples/uilanguages.html">brand new version in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>\r
+                       This sample shows how to automatically replace <code>&lt;textarea&gt;</code> elements\r
+                       with a CKEditor instance with an option to change the language of its user interface.\r
+               </p>\r
+               <p>\r
+                       It pulls the language list from CKEditor <code>_languages.js</code> file that contains the list of supported languages and creates\r
+                       a drop-down list that lets the user change the UI language.\r
+               </p>\r
+               <p>\r
+                       By default, CKEditor automatically localizes the editor to the language of the user.\r
+                       The UI language can be controlled with two configuration options:\r
+                       <code><a class="samples" href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-language">language</a></code> and\r
+                       <code><a class="samples" href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-defaultLanguage">\r
+                       defaultLanguage</a></code>. The <code>defaultLanguage</code> setting specifies the\r
+                       default CKEditor language to be used when a localization suitable for user's settings is not available.\r
+               </p>\r
+               <p>\r
+                       To specify the user interface language that will be used no matter what language is\r
+                       specified in user's browser or operating system, set the <code>language</code> property:\r
+               </p>\r
+<pre class="samples">\r
+CKEDITOR.replace( '<em>textarea_id</em>', {\r
+       // Load the German interface.\r
+       <strong>language: 'de'</strong>\r
+});</pre>\r
+               <p>\r
+                       Note that <code><em>textarea_id</em></code> in the code above is the <code>id</code> attribute of\r
+                       the <code>&lt;textarea&gt;</code> element to be replaced.\r
+               </p>\r
+       </div>\r
+       <form action="sample_posteddata.php" method="post">\r
+               <p>\r
+                       Available languages (<span id="count"> </span> languages!):<br>\r
+                       <script>\r
+\r
+                               document.write( '<select disabled="disabled" id="languages" onchange="createEditor( this.value );">' );\r
+\r
+                               // Get the language list from the _languages.js file.\r
+                               for ( var i = 0 ; i < window.CKEDITOR_LANGS.length ; i++ ) {\r
+                                       document.write(\r
+                                               '<option value="' + window.CKEDITOR_LANGS[i].code + '">' +\r
+                                                       window.CKEDITOR_LANGS[i].name +\r
+                                               '</option>' );\r
+                               }\r
+\r
+                               document.write( '</select>' );\r
+\r
+                       </script>\r
+                       <br>\r
+                       <span style="color: #888888">\r
+                               (You may see strange characters if your system does not support the selected language)\r
+                       </span>\r
+               </p>\r
+               <p>\r
+                       <textarea cols="80" id="editor1" name="editor1" rows="10">&lt;p&gt;This is some &lt;strong&gt;sample text&lt;/strong&gt;. You are using &lt;a href="http://ckeditor.com/"&gt;CKEditor&lt;/a&gt;.&lt;/p&gt;</textarea>\r
+                       <script>\r
+\r
+                               // Set the number of languages.\r
+                               document.getElementById( 'count' ).innerHTML = window.CKEDITOR_LANGS.length;\r
+\r
+                               var editor;\r
+\r
+                               function createEditor( languageCode ) {\r
+                                       if ( editor )\r
+                                               editor.destroy();\r
+\r
+                                       // Replace the <textarea id="editor"> with an CKEditor\r
+                                       // instance, using default configurations.\r
+                                       editor = CKEDITOR.replace( 'editor1', {\r
+                                               language: languageCode,\r
+\r
+                                               on: {\r
+                                                       instanceReady: function() {\r
+                                                               // Wait for the editor to be ready to set\r
+                                                               // the language combo.\r
+                                                               var languages = document.getElementById( 'languages' );\r
+                                                               languages.value = this.langCode;\r
+                                                               languages.disabled = false;\r
+                                                       }\r
+                                               }\r
+                                       });\r
+                               }\r
+\r
+                               // At page startup, load the default language:\r
+                               createEditor( '' );\r
+\r
+                       </script>\r
+               </p>\r
+       </form>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/wysiwygarea/fullpage.html b/js/ckeditor/samples/old/wysiwygarea/fullpage.html
new file mode 100644 (file)
index 0000000..bb3193a
--- /dev/null
@@ -0,0 +1,80 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>Full Page Editing &mdash; CKEditor Sample</title>\r
+       <script src="../../../ckeditor.js"></script>\r
+       <script src="../../../samples/old/sample.js"></script>\r
+       <link rel="stylesheet" href="../../../samples/old/sample.css">\r
+       <meta name="ckeditor-sample-required-plugins" content="sourcearea">\r
+       <meta name="ckeditor-sample-name" content="Full page support">\r
+       <meta name="ckeditor-sample-group" content="Plugins">\r
+       <meta name="ckeditor-sample-description" content="CKEditor inserted with a JavaScript call and used to edit the whole page from &lt;html&gt; to &lt;/html&gt;.">\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="../../../samples/old/index.html">CKEditor Samples</a> &raquo; Full Page Editing\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out its <a href="http://sdk.ckeditor.com/samples/fullpage.html">brand new version in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>\r
+                       This sample shows how to configure CKEditor to edit entire HTML pages, from the\r
+                       <code>&lt;html&gt;</code> tag to the <code>&lt;/html&gt;</code> tag.\r
+               </p>\r
+               <p>\r
+                       The CKEditor instance below is inserted with a JavaScript call using the following code:\r
+               </p>\r
+<pre class="samples">\r
+CKEDITOR.replace( '<em>textarea_id</em>', {\r
+       <strong>fullPage: true</strong>,\r
+       <strong>allowedContent: true</strong>\r
+});\r
+</pre>\r
+               <p>\r
+                       Note that <code><em>textarea_id</em></code> in the code above is the <code>id</code> attribute of\r
+                       the <code>&lt;textarea&gt;</code> element to be replaced.\r
+               </p>\r
+               <p>\r
+                       The <code><em>allowedContent</em></code> in the code above is set to <code>true</code> to disable content filtering.\r
+                       Setting this option is not obligatory, but in full page mode there is a strong chance that one may want be able to freely enter any HTML content in source mode without any limitations.\r
+               </p>\r
+       </div>\r
+       <form action="../../../samples/sample_posteddata.php" method="post">\r
+               <label for="editor1">\r
+                       CKEditor output the entire page including content outside of\r
+                       <code>&lt;body&gt;</code> element, so content like meta and title can be changed:\r
+               </label>\r
+               <textarea cols="80" id="editor1" name="editor1" rows="10">\r
+                       &lt;h1&gt;&lt;img align=&quot;right&quot; alt=&quot;Saturn V carrying Apollo 11&quot; src=&quot;../../../samples/old/assets/sample.jpg&quot;/&gt; Apollo 11&lt;/h1&gt; &lt;p&gt;&lt;b&gt;Apollo 11&lt;/b&gt; was the spaceflight that landed the first humans, Americans &lt;a href=&quot;http://en.wikipedia.org/wiki/Neil_Armstrong&quot; title=&quot;Neil Armstrong&quot;&gt;Neil Armstrong&lt;/a&gt; and &lt;a href=&quot;http://en.wikipedia.org/wiki/Buzz_Aldrin&quot; title=&quot;Buzz Aldrin&quot;&gt;Buzz Aldrin&lt;/a&gt;, on the Moon on July 20, 1969, at 20:18 UTC. Armstrong became the first to step onto the lunar surface 6 hours later on July 21 at 02:56 UTC.&lt;/p&gt; &lt;p&gt;Armstrong spent about &lt;s&gt;three and a half&lt;/s&gt; two and a half hours outside the spacecraft, Aldrin slightly less; and together they collected 47.5 pounds (21.5&amp;nbsp;kg) of lunar material for return to Earth. A third member of the mission, &lt;a href=&quot;http://en.wikipedia.org/wiki/Michael_Collins_(astronaut)&quot; title=&quot;Michael Collins (astronaut)&quot;&gt;Michael Collins&lt;/a&gt;, piloted the &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_Command/Service_Module&quot; title=&quot;Apollo Command/Service Module&quot;&gt;command&lt;/a&gt; spacecraft alone in lunar orbit until Armstrong and Aldrin returned to it for the trip back to Earth.&lt;/p&gt; &lt;h2&gt;Broadcasting and &lt;em&gt;quotes&lt;/em&gt; &lt;a id=&quot;quotes&quot; name=&quot;quotes&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;p&gt;Broadcast on live TV to a world-wide audience, Armstrong stepped onto the lunar surface and described the event as:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;One small step for [a] man, one giant leap for mankind.&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;Apollo 11 effectively ended the &lt;a href=&quot;http://en.wikipedia.org/wiki/Space_Race&quot; title=&quot;Space Race&quot;&gt;Space Race&lt;/a&gt; and fulfilled a national goal proposed in 1961 by the late U.S. President &lt;a href=&quot;http://en.wikipedia.org/wiki/John_F._Kennedy&quot; title=&quot;John F. Kennedy&quot;&gt;John F. Kennedy&lt;/a&gt; in a speech before the United States Congress:&lt;/p&gt; &lt;blockquote&gt;&lt;p&gt;[...] before this decade is out, of landing a man on the Moon and returning him safely to the Earth.&lt;/p&gt;&lt;/blockquote&gt; &lt;h2&gt;Technical details &lt;a id=&quot;tech-details&quot; name=&quot;tech-details&quot;&gt;&lt;/a&gt;&lt;/h2&gt; &lt;table align=&quot;right&quot; border=&quot;1&quot; bordercolor=&quot;#ccc&quot; cellpadding=&quot;5&quot; cellspacing=&quot;0&quot; style=&quot;border-collapse:collapse;margin:10px 0 10px 15px;&quot;&gt; &lt;caption&gt;&lt;strong&gt;Mission crew&lt;/strong&gt;&lt;/caption&gt; &lt;thead&gt; &lt;tr&gt; &lt;th scope=&quot;col&quot;&gt;Position&lt;/th&gt; &lt;th scope=&quot;col&quot;&gt;Astronaut&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;Commander&lt;/td&gt; &lt;td&gt;Neil A. Armstrong&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Command Module Pilot&lt;/td&gt; &lt;td&gt;Michael Collins&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Lunar Module Pilot&lt;/td&gt; &lt;td&gt;Edwin &amp;quot;Buzz&amp;quot; E. Aldrin, Jr.&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;Launched by a &lt;strong&gt;Saturn V&lt;/strong&gt; rocket from &lt;a href=&quot;http://en.wikipedia.org/wiki/Kennedy_Space_Center&quot; title=&quot;Kennedy Space Center&quot;&gt;Kennedy Space Center&lt;/a&gt; in Merritt Island, Florida on July 16, Apollo 11 was the fifth manned mission of &lt;a href=&quot;http://en.wikipedia.org/wiki/NASA&quot; title=&quot;NASA&quot;&gt;NASA&lt;/a&gt;&amp;#39;s Apollo program. The Apollo spacecraft had three parts:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;strong&gt;Command Module&lt;/strong&gt; with a cabin for the three astronauts which was the only part which landed back on Earth&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Service Module&lt;/strong&gt; which supported the Command Module with propulsion, electrical power, oxygen and water&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Lunar Module&lt;/strong&gt; for landing on the Moon.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;After being sent to the Moon by the Saturn V&amp;#39;s upper stage, the astronauts separated the spacecraft from it and travelled for three days until they entered into lunar orbit. Armstrong and Aldrin then moved into the Lunar Module and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Mare_Tranquillitatis&quot; title=&quot;Mare Tranquillitatis&quot;&gt;Sea of Tranquility&lt;/a&gt;. They stayed a total of about 21 and a half hours on the lunar surface. After lifting off in the upper part of the Lunar Module and rejoining Collins in the Command Module, they returned to Earth and landed in the &lt;a href=&quot;http://en.wikipedia.org/wiki/Pacific_Ocean&quot; title=&quot;Pacific Ocean&quot;&gt;Pacific Ocean&lt;/a&gt; on July 24.&lt;/p&gt; &lt;hr/&gt; &lt;p style=&quot;text-align: right;&quot;&gt;&lt;small&gt;Source: &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_11&quot;&gt;Wikipedia.org&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;\r
+               </textarea>\r
+               <script>\r
+\r
+                       CKEDITOR.replace( 'editor1', {\r
+                               fullPage: true,\r
+                               allowedContent: true,\r
+                               extraPlugins: 'wysiwygarea'\r
+                       });\r
+\r
+               </script>\r
+               <p>\r
+                       <input type="submit" value="Submit">\r
+               </p>\r
+       </form>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/old/xhtmlstyle.html b/js/ckeditor/samples/old/xhtmlstyle.html
new file mode 100644 (file)
index 0000000..daf5879
--- /dev/null
@@ -0,0 +1,234 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<html>\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>XHTML Compliant Output &mdash; CKEditor Sample</title>\r
+       <meta name="ckeditor-sample-required-plugins" content="sourcearea">\r
+       <script src="../../ckeditor.js"></script>\r
+       <script src="sample.js"></script>\r
+       <link href="sample.css" rel="stylesheet">\r
+</head>\r
+<body>\r
+       <h1 class="samples">\r
+               <a href="index.html">CKEditor Samples</a> &raquo; Producing XHTML Compliant Output\r
+       </h1>\r
+       <div class="warning deprecated">\r
+               This sample is not maintained anymore. Check out its <a href="http://sdk.ckeditor.com/samples/basicstyles.html">brand new version in CKEditor SDK</a>.\r
+       </div>\r
+       <div class="description">\r
+               <p>\r
+                       This sample shows how to configure CKEditor to output valid\r
+                       <a class="samples" href="http://www.w3.org/TR/xhtml11/">XHTML 1.1</a> code.\r
+                       Deprecated elements (<code>&lt;font&gt;</code>, <code>&lt;u&gt;</code>) or attributes\r
+                       (<code>size</code>, <code>face</code>) will be replaced with XHTML compliant code.\r
+               </p>\r
+               <p>\r
+                       To add a CKEditor instance outputting valid XHTML code, load the editor using a standard\r
+                       JavaScript call and define CKEditor features to use the XHTML compliant elements and styles.\r
+               </p>\r
+               <p>\r
+                       A snippet of the configuration code can be seen below; check the source of this page for\r
+                       full definition:\r
+               </p>\r
+<pre class="samples">\r
+CKEDITOR.replace( '<em>textarea_id</em>', {\r
+       contentsCss: 'assets/outputxhtml.css',\r
+\r
+       coreStyles_bold: {\r
+               element: 'span',\r
+               attributes: { 'class': 'Bold' }\r
+       },\r
+       coreStyles_italic: {\r
+               element: 'span',\r
+               attributes: { 'class': 'Italic' }\r
+       },\r
+\r
+       ...\r
+});</pre>\r
+       </div>\r
+       <form action="sample_posteddata.php" method="post">\r
+               <p>\r
+                       <label for="editor1">\r
+                               Editor 1:\r
+                       </label>\r
+                       <textarea cols="80" id="editor1" name="editor1" rows="10">&lt;p&gt;This is some &lt;span class="Bold"&gt;sample text&lt;/span&gt;. You are using &lt;a href="http://ckeditor.com/"&gt;CKEditor&lt;/a&gt;.&lt;/p&gt;</textarea>\r
+                       <script>\r
+\r
+                               CKEDITOR.replace( 'editor1', {\r
+                                       /*\r
+                                        * Style sheet for the contents\r
+                                        */\r
+                                       contentsCss: 'assets/outputxhtml/outputxhtml.css',\r
+\r
+                                       /*\r
+                                        * Special allowed content rules for spans used by\r
+                                        * font face, size, and color buttons.\r
+                                        *\r
+                                        * Note: all rules have been written separately so\r
+                                        * it was possible to specify required classes.\r
+                                        */\r
+                                       extraAllowedContent: 'span(!FontColor1);span(!FontColor2);span(!FontColor3);' +\r
+                                               'span(!FontColor1BG);span(!FontColor2BG);span(!FontColor3BG);' +\r
+                                               'span(!FontComic);span(!FontCourier);span(!FontTimes);' +\r
+                                               'span(!FontSmaller);span(!FontLarger);span(!FontSmall);span(!FontBig);span(!FontDouble)',\r
+\r
+                                       /*\r
+                                        * Core styles.\r
+                                        */\r
+                                       coreStyles_bold: {\r
+                                               element: 'span',\r
+                                               attributes: { 'class': 'Bold' }\r
+                                       },\r
+                                       coreStyles_italic: {\r
+                                               element: 'span',\r
+                                               attributes: { 'class': 'Italic' }\r
+                                       },\r
+                                       coreStyles_underline: {\r
+                                               element: 'span',\r
+                                               attributes: { 'class': 'Underline' }\r
+                                       },\r
+                                       coreStyles_strike: {\r
+                                               element: 'span',\r
+                                               attributes: { 'class': 'StrikeThrough' },\r
+                                               overrides: 'strike'\r
+                                       },\r
+                                       coreStyles_subscript: {\r
+                                               element: 'span',\r
+                                               attributes: { 'class': 'Subscript' },\r
+                                               overrides: 'sub'\r
+                                       },\r
+                                       coreStyles_superscript: {\r
+                                               element: 'span',\r
+                                               attributes: { 'class': 'Superscript' },\r
+                                               overrides: 'sup'\r
+                                       },\r
+\r
+                                       /*\r
+                                        * Font face.\r
+                                        */\r
+\r
+                                       // List of fonts available in the toolbar combo. Each font definition is\r
+                                       // separated by a semi-colon (;). We are using class names here, so each font\r
+                                       // is defined by {Combo Label}/{Class Name}.\r
+                                       font_names: 'Comic Sans MS/FontComic;Courier New/FontCourier;Times New Roman/FontTimes',\r
+\r
+                                       // Define the way font elements will be applied to the document. The "span"\r
+                                       // element will be used. When a font is selected, the font name defined in the\r
+                                       // above list is passed to this definition with the name "Font", being it\r
+                                       // injected in the "class" attribute.\r
+                                       // We must also instruct the editor to replace span elements that are used to\r
+                                       // set the font (Overrides).\r
+                                       font_style: {\r
+                                               element: 'span',\r
+                                               attributes: { 'class': '#(family)' },\r
+                                               overrides: [\r
+                                                       {\r
+                                                               element: 'span',\r
+                                                               attributes: {\r
+                                                                       'class': /^Font(?:Comic|Courier|Times)$/\r
+                                                               }\r
+                                                       }\r
+                                               ]\r
+                                       },\r
+\r
+                                       /*\r
+                                        * Font sizes.\r
+                                        */\r
+                                       fontSize_sizes: 'Smaller/FontSmaller;Larger/FontLarger;8pt/FontSmall;14pt/FontBig;Double Size/FontDouble',\r
+                                       fontSize_style: {\r
+                                               element: 'span',\r
+                                               attributes: { 'class': '#(size)' },\r
+                                               overrides: [\r
+                                                       {\r
+                                                               element: 'span',\r
+                                                               attributes: {\r
+                                                                       'class': /^Font(?:Smaller|Larger|Small|Big|Double)$/\r
+                                                               }\r
+                                                       }\r
+                                               ]\r
+                                       } ,\r
+\r
+                                       /*\r
+                                        * Font colors.\r
+                                        */\r
+                                       colorButton_enableMore: false,\r
+\r
+                                       colorButton_colors: 'FontColor1/FF9900,FontColor2/0066CC,FontColor3/F00',\r
+                                       colorButton_foreStyle: {\r
+                                               element: 'span',\r
+                                               attributes: { 'class': '#(color)' },\r
+                                               overrides: [\r
+                                                       {\r
+                                                               element: 'span',\r
+                                                               attributes: {\r
+                                                                       'class': /^FontColor(?:1|2|3)$/\r
+                                                               }\r
+                                                       }\r
+                                               ]\r
+                                       },\r
+\r
+                                       colorButton_backStyle: {\r
+                                               element: 'span',\r
+                                               attributes: { 'class': '#(color)BG' },\r
+                                               overrides: [\r
+                                                       {\r
+                                                               element: 'span',\r
+                                                               attributes: {\r
+                                                                       'class': /^FontColor(?:1|2|3)BG$/\r
+                                                               }\r
+                                                       }\r
+                                               ]\r
+                                       },\r
+\r
+                                       /*\r
+                                        * Indentation.\r
+                                        */\r
+                                       indentClasses: [ 'Indent1', 'Indent2', 'Indent3' ],\r
+\r
+                                       /*\r
+                                        * Paragraph justification.\r
+                                        */\r
+                                       justifyClasses: [ 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyFull' ],\r
+\r
+                                       /*\r
+                                        * Styles combo.\r
+                                        */\r
+                                       stylesSet: [\r
+                                               { name: 'Strong Emphasis', element: 'strong' },\r
+                                               { name: 'Emphasis', element: 'em' },\r
+\r
+                                               { name: 'Computer Code', element: 'code' },\r
+                                               { name: 'Keyboard Phrase', element: 'kbd' },\r
+                                               { name: 'Sample Text', element: 'samp' },\r
+                                               { name: 'Variable', element: 'var' },\r
+\r
+                                               { name: 'Deleted Text', element: 'del' },\r
+                                               { name: 'Inserted Text', element: 'ins' },\r
+\r
+                                               { name: 'Cited Work', element: 'cite' },\r
+                                               { name: 'Inline Quotation', element: 'q' }\r
+                                       ]\r
+                               });\r
+\r
+                       </script>\r
+               </p>\r
+               <p>\r
+                       <input type="submit" value="Submit">\r
+               </p>\r
+       </form>\r
+       <div id="footer">\r
+               <hr>\r
+               <p>\r
+                       CKEditor - The text editor for the Internet - <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+               </p>\r
+               <p id="copy">\r
+                       Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> - Frederico\r
+                       Knabben. All rights reserved.\r
+               </p>\r
+       </div>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/toolbarconfigurator/css/fontello.css b/js/ckeditor/samples/toolbarconfigurator/css/fontello.css
new file mode 100644 (file)
index 0000000..d983707
--- /dev/null
@@ -0,0 +1,55 @@
+@font-face {\r
+  font-family: 'fontello';\r
+  src: url('../font/fontello.eot?89024372');\r
+  src: url('../font/fontello.eot?89024372#iefix') format('embedded-opentype'),\r
+       url('../font/fontello.woff?89024372') format('woff'),\r
+       url('../font/fontello.ttf?89024372') format('truetype'),\r
+       url('../font/fontello.svg?89024372#fontello') format('svg');\r
+  font-weight: normal;\r
+  font-style: normal;\r
+}\r
+/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */\r
+/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */\r
+/*\r
+@media screen and (-webkit-min-device-pixel-ratio:0) {\r
+  @font-face {\r
+    font-family: 'fontello';\r
+    src: url('../font/fontello.svg?89024372#fontello') format('svg');\r
+  }\r
+}\r
+*/\r
+\r
+ [class^="icon-"]:before, [class*=" icon-"]:before {\r
+  font-family: "fontello";\r
+  font-style: normal;\r
+  font-weight: normal;\r
+  speak: none;\r
+\r
+  display: inline-block;\r
+  text-decoration: inherit;\r
+  width: 1em;\r
+  margin-right: .2em;\r
+  text-align: center;\r
+  /* opacity: .8; */\r
+\r
+  /* For safety - reset parent styles, that can break glyph codes*/\r
+  font-variant: normal;\r
+  text-transform: none;\r
+\r
+  /* fix buttons height, for twitter bootstrap */\r
+  line-height: 1em;\r
+\r
+  /* Animation center compensation - margins should be symmetric */\r
+  /* remove if not needed */\r
+  margin-left: .2em;\r
+\r
+  /* you can be more comfortable with increased icons size */\r
+  /* font-size: 120%; */\r
+\r
+  /* Uncomment for 3D effect */\r
+  /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */\r
+}\r
+\r
+.icon-trash:before { content: '\e802'; } /* '' */\r
+.icon-down-big:before { content: '\e800'; } /* '' */\r
+.icon-up-big:before { content: '\e801'; } /* '' */\r
diff --git a/js/ckeditor/samples/toolbarconfigurator/font/LICENSE.txt b/js/ckeditor/samples/toolbarconfigurator/font/LICENSE.txt
new file mode 100644 (file)
index 0000000..4a73f6c
--- /dev/null
@@ -0,0 +1,10 @@
+Font license info\r
+\r
+\r
+## Font Awesome\r
+\r
+   Copyright (C) 2012 by Dave Gandy\r
+\r
+   Author:    Dave Gandy\r
+   License:   SIL ()\r
+   Homepage:  http://fortawesome.github.com/Font-Awesome/\r
diff --git a/js/ckeditor/samples/toolbarconfigurator/font/config.json b/js/ckeditor/samples/toolbarconfigurator/font/config.json
new file mode 100644 (file)
index 0000000..94809d7
--- /dev/null
@@ -0,0 +1,28 @@
+{
+  "name": "",
+  "css_prefix_text": "icon-",
+  "css_use_suffix": false,
+  "hinting": true,
+  "units_per_em": 1000,
+  "ascent": 850,
+  "glyphs": [
+    {
+      "uid": "f48ae54adfb27d8ada53d0fd9e34ee10",
+      "css": "trash-empty",
+      "code": 59392,
+      "src": "fontawesome"
+    },
+    {
+      "uid": "1c4068ed75209e21af36017df8871802",
+      "css": "down-big",
+      "code": 59393,
+      "src": "fontawesome"
+    },
+    {
+      "uid": "95376bf082bfec6ce06ea1cda7bd7ead",
+      "css": "up-big",
+      "code": 59394,
+      "src": "fontawesome"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/js/ckeditor/samples/toolbarconfigurator/font/fontello.eot b/js/ckeditor/samples/toolbarconfigurator/font/fontello.eot
new file mode 100644 (file)
index 0000000..2732fad
Binary files /dev/null and b/js/ckeditor/samples/toolbarconfigurator/font/fontello.eot differ
diff --git a/js/ckeditor/samples/toolbarconfigurator/font/fontello.svg b/js/ckeditor/samples/toolbarconfigurator/font/fontello.svg
new file mode 100644 (file)
index 0000000..33d14ac
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata>Copyright (C) 2014 by original authors @ fontello.com</metadata>
+<defs>
+<font id="fontello" horiz-adv-x="1000" >
+<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
+<missing-glyph horiz-adv-x="1000" />
+<glyph glyph-name="trash" unicode="&#xe802;" d="m286 439v-321q0-8-5-13t-13-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q8 0 13-5t5-13z m143 0v-321q0-8-5-13t-13-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q8 0 13-5t5-13z m142 0v-321q0-8-5-13t-12-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q7 0 12-5t5-13z m72-404v529h-500v-529q0-12 4-22t8-15t6-5h464q2 0 6 5t8 15t4 22z m-375 601h250l-27 65q-4 5-9 6h-177q-6-1-10-6z m518-18v-36q0-8-5-13t-13-5h-54v-529q0-46-26-80t-63-34h-464q-37 0-63 33t-27 79v531h-53q-8 0-13 5t-5 13v36q0 8 5 13t13 5h172l39 93q9 21 31 35t44 15h178q22 0 44-15t30-35l39-93h173q8 0 13-5t5-13z" horiz-adv-x="785.7" />
+<glyph glyph-name="down-big" unicode="&#xe800;" d="m899 386q0-30-21-50l-363-364q-22-21-51-21q-29 0-50 21l-363 364q-21 20-21 50q0 29 21 51l41 41q22 21 51 21q29 0 50-21l164-164v393q0 29 21 50t51 22h71q29 0 50-22t21-50v-393l164 164q21 21 51 21q29 0 50-21l42-42q21-21 21-50z" horiz-adv-x="928.6" />
+<glyph glyph-name="up-big" unicode="&#xe801;" d="m899 308q0-28-21-50l-42-42q-21-21-50-21q-30 0-51 21l-164 164v-393q0-29-20-47t-51-19h-71q-30 0-51 19t-21 47v393l-164-164q-20-21-50-21t-50 21l-42 42q-21 21-21 50q0 30 21 51l363 363q20 21 50 21q30 0 51-21l363-363q21-22 21-51z" horiz-adv-x="928.6" />
+</font>
+</defs>
+</svg>
\ No newline at end of file
diff --git a/js/ckeditor/samples/toolbarconfigurator/font/fontello.ttf b/js/ckeditor/samples/toolbarconfigurator/font/fontello.ttf
new file mode 100644 (file)
index 0000000..fbcbf06
Binary files /dev/null and b/js/ckeditor/samples/toolbarconfigurator/font/fontello.ttf differ
diff --git a/js/ckeditor/samples/toolbarconfigurator/font/fontello.woff b/js/ckeditor/samples/toolbarconfigurator/font/fontello.woff
new file mode 100644 (file)
index 0000000..e1d5647
Binary files /dev/null and b/js/ckeditor/samples/toolbarconfigurator/font/fontello.woff differ
diff --git a/js/ckeditor/samples/toolbarconfigurator/index.html b/js/ckeditor/samples/toolbarconfigurator/index.html
new file mode 100644 (file)
index 0000000..5c06a0f
--- /dev/null
@@ -0,0 +1,446 @@
+<!DOCTYPE html>\r
+<!--\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+-->\r
+<!--[if IE 8]><html class="ie8"><![endif]-->\r
+<!--[if gt IE 8]><html><![endif]-->\r
+<!--[if !IE]><!--><html><!--<![endif]-->\r
+<head>\r
+       <meta charset="utf-8">\r
+       <title>Toolbar Configurator</title>\r
+       <script src="../../ckeditor.js"></script>\r
+       <script>\r
+               if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 )\r
+                       CKEDITOR.tools.enableHtml5Elements( document );\r
+       </script>\r
+       <link rel="stylesheet" href="lib/codemirror/codemirror.css">\r
+       <link rel="stylesheet" href="lib/codemirror/show-hint.css">\r
+       <link rel="stylesheet" href="lib/codemirror/neo.css">\r
+       <link rel="stylesheet" href="css/fontello.css">\r
+       <link rel="stylesheet" href="../css/samples.css">\r
+</head>\r
+<body id="toolbar">\r
+\r
+<nav class="navigation-a">\r
+       <div class="grid-container">\r
+               <ul class="navigation-a-left grid-width-70">\r
+                       <li><a href="http://ckeditor.com">Project Homepage</a></li>\r
+                       <li><a href="https://github.com/ckeditor/ckeditor-dev/issues">I found a bug</a></li>\r
+                       <li><a href="http://github.com/ckeditor/ckeditor-dev" class="icon-pos-right icon-navigation-a-github">Fork CKEditor on GitHub</a></li>\r
+               </ul>\r
+               <ul class="navigation-a-right grid-width-30">\r
+                       <li><a href="http://ckeditor.com/blog-list">CKEditor Blog</a></li>\r
+               </ul>\r
+       </div>\r
+</nav>\r
+\r
+<header class="header-a">\r
+       <div class="grid-container">\r
+               <h1 class="header-a-logo grid-width-30">\r
+                       <a href="../index.html"><img src="../img/logo.png" alt="CKEditor Logo"></a>\r
+               </h1>\r
+               <nav class="navigation-b grid-width-70">\r
+                       <ul>\r
+                               <li><a href="../index.html"  class="button-a">Start</a></li>\r
+                               <li><a href="index.html"  class="button-a button-a-background">Toolbar configurator</a></li>\r
+                       </ul>\r
+               </nav>\r
+       </div>\r
+</header>\r
+\r
+<main>\r
+       <div class="adjoined-top">\r
+               <div class="grid-container">\r
+                       <div class="content grid-width-100">\r
+                               <div class="grid-container-nested">\r
+                                       <h1 class="grid-width-60">\r
+                                               Toolbar Configurator\r
+                                               <a href="#help-content" type="button" title="Configurator help" id="help" class="button-a button-a-background button-a-no-text icon-pos-left icon-question-mark">Help</a>\r
+                                       </h1>\r
+\r
+                                       <div class="grid-width-40 grid-switch-magic">\r
+                                               <div class="switch">\r
+                                                       <span class="balloon-a balloon-a-se">Select configurator type</span>\r
+                                                       <input type="radio" name="radio" data-num="1" id="radio-basic" />\r
+                                                       <input type="radio" name="radio" data-num="2" id="radio-advanced" />\r
+                                                       <label data-for="1" for="radio-basic">Basic</label>\r
+                                                       <span class="switch-inner">\r
+                                                               <span class="handler"></span>\r
+                                                       </span>\r
+                                                       <label data-for="2" for="radio-advanced">Advanced</label>\r
+                                               </div>\r
+                                       </div>\r
+                               </div>\r
+                       </div>\r
+               </div>\r
+       </div>\r
+       <div class="adjoined-bottom">\r
+               <div class="grid-container">\r
+                       <div class="grid-width-100">\r
+                               <div class="editors-container">\r
+                                       <div id="editor-basic"></div>\r
+                                       <div id="editor-advanced"></div>\r
+                               </div>\r
+                       </div>\r
+               </div>\r
+       </div>\r
+\r
+       <div class="grid-container configurator">\r
+               <div class="content grid-width-100">\r
+                       <div class="configurator">\r
+                               <div>\r
+                                       <div id="toolbarModifierWrapper"></div>\r
+                               </div>\r
+                       </div>\r
+               </div>\r
+       </div>\r
+\r
+       <div id="help-content">\r
+               <div class="grid-container">\r
+                       <div class="grid-width-100">\r
+                               <h2>What Am I Doing Here?</h2>\r
+\r
+                               <div class="grid-container grid-container-nested">\r
+                                       <div class="basic">\r
+                                               <div class="grid-width-50">\r
+                                                       <p>Arrange <a href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-toolbarGroups">toolbar groups</a>, toggle <a href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-removeButtons">button visibility</a> according to your needs and get your toolbar configuration.</p>\r
+                                                       <p>You can replace the content of the <a href="../../config.js"><code>config.js</code></a> file with the generated configuration. If you already set some configuration options you will need to merge both configurations.</p>\r
+                                               </div>\r
+                                               <div class="grid-width-50">\r
+                                                       <p>Read more about different ways of <a href="http://docs.ckeditor.com/#!/guide/dev_configuration">setting configuration</a> and do not forget about <strong>clearing browser cache</strong>.</p>\r
+                                                       <p>Arranging toolbar groups is the recommended way of configuring the toolbar, but if you need more freedom you can use the <a href="#advanced">advanced configurator</a>.</p>\r
+                                               </div>\r
+                                       </div>\r
+                                       <div class="advanced" style="display: none;">\r
+                                               <div class="grid-width-50">\r
+                                                       <p>With this code editor you can edit your <a href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-toolbar">toolbar configuration</a> live.</p>\r
+                                                       <p>You can replace the content of the <a href="../../config.js"><code>config.js</code></a> file with the generated configuration. If you already set some configuration options you will need to merge both configurations.</p>\r
+                                               </div>\r
+                                               <div class="grid-width-50">\r
+                                                       <p>Read more about different ways of <a href="http://docs.ckeditor.com/#!/guide/dev_configuration">setting configuration</a> and do not forget about <strong>clearing browser cache</strong>.</p>\r
+                                               </div>\r
+                                       </div>\r
+                               </div>\r
+\r
+                               <p class="grid-container grid-container-nested">\r
+                                       <button type="button" class="help-content-close grid-width-100 button-a button-a-background">Got it. Let's play!</button>\r
+                               </p>\r
+                       </div>\r
+               </div>\r
+       </div>\r
+</main>\r
+\r
+<footer class="footer-a grid-container">\r
+       <p class="grid-width-100">\r
+               CKEditor &ndash; The text editor for the Internet &ndash; <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>\r
+       </p>\r
+       <p class="grid-width-100" id="copy">\r
+               Copyright &copy; 2003-2017, <a class="samples" href="http://cksource.com/">CKSource</a> &ndash; Frederico Knabben. All rights reserved.\r
+       </p>\r
+</footer>\r
+\r
+<script src="lib/codemirror/codemirror.js"></script>\r
+<script src="lib/codemirror/javascript.js"></script>\r
+<script src="lib/codemirror/show-hint.js"></script>\r
+\r
+<script src="js/fulltoolbareditor.js"></script>\r
+<script src="js/abstracttoolbarmodifier.js"></script>\r
+<script src="js/toolbarmodifier.js"></script>\r
+<script src="js/toolbartextmodifier.js"></script>\r
+<script src="../js/sf.js"></script>\r
+\r
+<script>\r
+       ( function() {\r
+               'use strict';\r
+\r
+               var mode = ( window.location.hash.substr( 1 ) === 'advanced' ) ? 'advanced' : 'basic',\r
+                       configuratorSection = CKEDITOR.document.findOne( 'main > .grid-container.configurator' ),\r
+                       basicInstruction = CKEDITOR.document.findOne( '#help-content .basic' ),\r
+                       advancedInstruction = CKEDITOR.document.findOne( '#help-content .advanced' ),\r
+\r
+                       // Configurator mode switcher.\r
+                       modeSwitchBasic = CKEDITOR.document.getById( 'radio-basic' ),\r
+                       modeSwitchAdvanced = CKEDITOR.document.getById( 'radio-advanced' );\r
+\r
+               // Initial setup\r
+               function updateSwitcher() {\r
+                       if ( mode === 'advanced' ) {\r
+                               modeSwitchAdvanced.$.checked = true;\r
+                       } else {\r
+                               modeSwitchBasic.$.checked = true;\r
+                       }\r
+               }\r
+\r
+               updateSwitcher();\r
+\r
+               CKEDITOR.document.getWindow().on( 'hashchange', function( e ) {\r
+                       var hash = window.location.hash.substr( 1 );\r
+                       if ( !( hash === 'advanced' || hash === 'basic' ) ) {\r
+                               return;\r
+                       }\r
+                       mode = hash;\r
+                       onToolbarsDone( mode );\r
+               } );\r
+\r
+               CKEDITOR.document.getWindow().on( 'resize', function() {\r
+                       updateToolbar( ( mode === 'basic' ? toolbarModifier : toolbarTextModifier )[ 'editorInstance' ] );\r
+               } );\r
+\r
+               function onRefresh( modifier ) {\r
+                       modifier = modifier || this;\r
+\r
+                       if ( mode === 'basic' && modifier instanceof ToolbarConfigurator.ToolbarTextModifier ) {\r
+                               return;\r
+                       }\r
+\r
+                       // CodeMirror container becomes visible, so we need to refresh and to avoid rendering problems.\r
+                       if ( mode === 'advanced' && modifier instanceof ToolbarConfigurator.ToolbarTextModifier ) {\r
+                               modifier.codeContainer.refresh();\r
+                       }\r
+\r
+                       updateToolbar( modifier.editorInstance );\r
+               }\r
+\r
+               function updateToolbar( editor ) {\r
+                       var editorContainer = editor.container;\r
+\r
+                       // Not always editor is loaded.\r
+                       if ( !editorContainer ) {\r
+                               return;\r
+                       }\r
+\r
+                       var displayStyle = editorContainer.getStyle( 'display' );\r
+\r
+                       editorContainer.setStyle( 'display', 'block' );\r
+\r
+                       var newHeight = editorContainer.getSize( 'height' );\r
+\r
+                       var newMarginTop = parseInt( editorContainer.getComputedStyle( 'margin-top' ), 10 );\r
+                       newMarginTop = ( isNaN( newMarginTop ) ? 0 : Number( newMarginTop ) );\r
+\r
+                       var newMarginBottom = parseInt( editorContainer.getComputedStyle( 'margin-bottom' ), 10 );\r
+                       newMarginBottom = ( isNaN( newMarginBottom ) ? 0 : Number( newMarginBottom ) );\r
+\r
+                       var result = newHeight + newMarginTop + newMarginBottom;\r
+\r
+                       editorContainer.setStyle( 'display', displayStyle );\r
+\r
+                       editor.container.getAscendant( 'div' ).setStyle( 'height', result + 'px' );\r
+               }\r
+\r
+               var toolbarModifier = new ToolbarConfigurator.ToolbarModifier( 'editor-basic' );\r
+\r
+               var done = 0;\r
+               toolbarModifier.init( onToolbarInit );\r
+               toolbarModifier.onRefresh = onRefresh;\r
+\r
+               CKEDITOR.document.getById( 'toolbarModifierWrapper' ).append( toolbarModifier.mainContainer );\r
+\r
+               var toolbarTextModifier = new ToolbarConfigurator.ToolbarTextModifier( 'editor-advanced' );\r
+               toolbarTextModifier.init( onToolbarInit );\r
+               toolbarTextModifier.onRefresh = onRefresh;\r
+\r
+               function onToolbarInit() {\r
+                       if ( ++done === 2 ) {\r
+                               onToolbarsDone();\r
+\r
+                               positionSticky.watch( CKEDITOR.document.findOne( '.toolbar' ), function() {\r
+                                       return mode === 'advanced';\r
+                               } );\r
+                       }\r
+               }\r
+\r
+               function onToolbarsDone() {\r
+                       if ( mode === 'basic' ) {\r
+                               toggleModeBasic( false );\r
+                       } else {\r
+                               toggleModeAdvanced( false );\r
+                       }\r
+\r
+                       updateSwitcher();\r
+\r
+                       setTimeout( function() {\r
+                               CKEDITOR.document.findOne( '.editors-container' ).addClass( 'active' );\r
+                               CKEDITOR.document.findOne( '#toolbarModifierWrapper' ).addClass( 'active' );\r
+                       }, 200 );\r
+               }\r
+\r
+               CKEDITOR.document.getById( 'toolbarModifierWrapper' ).append( toolbarTextModifier.mainContainer );\r
+\r
+               function toogleModeSwitch( onElement, offElement, onModifier, offModifier ) {\r
+                       onElement.addClass( 'fancy-button-active' );\r
+                       offElement.removeClass( 'fancy-button-active' );\r
+\r
+                       onModifier.showUI();\r
+                       offModifier.hideUI();\r
+               }\r
+\r
+               function toggleModeBasic( callOnRefresh ) {\r
+                       callOnRefresh = ( callOnRefresh !== false );\r
+                       mode = 'basic';\r
+                       window.location.hash = '#basic';\r
+                       toogleModeSwitch( modeSwitchBasic, modeSwitchAdvanced, toolbarModifier, toolbarTextModifier );\r
+\r
+                       configuratorSection.removeClass( 'freed-width' );\r
+                       basicInstruction.show();\r
+                       advancedInstruction.hide();\r
+\r
+                       callOnRefresh && onRefresh( toolbarModifier );\r
+               }\r
+\r
+               function toggleModeAdvanced( callOnRefresh ) {\r
+                       callOnRefresh = ( callOnRefresh !== false );\r
+                       mode = 'advanced';\r
+                       window.location.hash = '#advanced';\r
+                       toogleModeSwitch( modeSwitchAdvanced, modeSwitchBasic, toolbarTextModifier, toolbarModifier );\r
+\r
+                       configuratorSection.addClass( 'freed-width' );\r
+                       advancedInstruction.show();\r
+                       basicInstruction.hide();\r
+\r
+                       callOnRefresh && onRefresh( toolbarTextModifier );\r
+               }\r
+\r
+               modeSwitchBasic.on( 'click', toggleModeBasic );\r
+               modeSwitchAdvanced.on( 'click', toggleModeAdvanced );\r
+\r
+               //\r
+               // Position:sticky for the toolbar.\r
+               //\r
+\r
+               // Will make elements behave like they were styled with position:sticky.\r
+               var positionSticky = {\r
+                       // Store object: {\r
+                       //              element: CKEDITOR.dom.element, // Element which will float.\r
+                       //              placeholder: CKEDITOR.dom.element, // Placeholder which is place to prevent page bounce.\r
+                       //              isFixed: boolean // Whether element float now.\r
+                       // }\r
+                       watched: [],\r
+\r
+                       active: [],\r
+\r
+                       staticContainer: null,\r
+\r
+                       init: function() {\r
+                               var element = CKEDITOR.dom.element.createFromHtml(\r
+                                       '<div class="staticContainer">' +\r
+                                               '<div class="grid-container" >' +\r
+                                                       '<div class="grid-width-100">' +\r
+                                                               '<div class="inner"></div>' +\r
+                                                       '</div>' +\r
+                                               '</div>' +\r
+                                       '</div>' );\r
+\r
+                               this.staticContainer = element.findOne( '.inner' );\r
+\r
+                               CKEDITOR.document.getBody().append( element );\r
+                       },\r
+\r
+                       watch: function( element, preventFunc ) {\r
+                               this.watched.push( {\r
+                                       element: element,\r
+                                       placeholder: new CKEDITOR.dom.element( 'div' ),\r
+                                       isFixed: false,\r
+                                       preventFunc: preventFunc\r
+                               } );\r
+                       },\r
+\r
+                       checkAll: function() {\r
+                               for ( var i = 0; i < this.watched.length; i++ ) {\r
+                                       this.check( this.watched[ i ] );\r
+                               }\r
+                       },\r
+\r
+                       check: function( element ) {\r
+                               var isFixed = element.isFixed;\r
+                               var shouldBeFixed = this.shouldBeFixed( element );\r
+\r
+                               // Nothing to be done.\r
+                               if ( isFixed === shouldBeFixed ) {\r
+                                       return;\r
+                               }\r
+\r
+                               var placeholder = element.placeholder;\r
+\r
+                               if ( isFixed ) {\r
+                                       // Unfixing.\r
+\r
+                                       element.element.insertBefore( placeholder );\r
+                                       placeholder.remove();\r
+\r
+                                       element.element.removeStyle( 'margin' );\r
+\r
+                                       this.active.splice( CKEDITOR.tools.indexOf( this.active, element ), 1 );\r
+\r
+                               } else {\r
+                                       // Fixing.\r
+                                       placeholder.setStyle( 'width', element.element.getSize( 'width' ) + 'px' );\r
+                                       placeholder.setStyle( 'height', element.element.getSize( 'height' ) + 'px' );\r
+                                       placeholder.setStyle( 'margin-bottom', element.element.getComputedStyle( 'margin-bottom' ) );\r
+                                       placeholder.setStyle( 'display', element.element.getComputedStyle( 'display' ) );\r
+                                       placeholder.insertAfter( element.element );\r
+\r
+                                       this.staticContainer.append( element.element );\r
+\r
+                                       this.active.push( element );\r
+                               }\r
+\r
+                               element.isFixed = !element.isFixed;\r
+                       },\r
+\r
+                       shouldBeFixed: function( element ) {\r
+                               if ( element.preventFunc && element.preventFunc() ) {\r
+                                       return false;\r
+                               }\r
+\r
+                               // If element is already fixed we are checking it's placeholder.\r
+                               var related = ( element.isFixed ? element.placeholder : element.element ),\r
+                                       clientRect = related.$.getBoundingClientRect(),\r
+                                       staticHeight = this.staticContainer.getSize('height' ),\r
+                                       elemHeight = element.element.getSize( 'height' );\r
+\r
+                               if ( element.isFixed ) {\r
+                                       return ( clientRect.top + elemHeight < staticHeight );\r
+                               } else {\r
+                                       return ( clientRect.top < staticHeight );\r
+                               }\r
+                       }\r
+               };\r
+\r
+               positionSticky.init();\r
+\r
+               CKEDITOR.document.getWindow().on( 'scroll',\r
+                       new CKEDITOR.tools.eventsBuffer( 100, positionSticky.checkAll, positionSticky ).input\r
+               );\r
+\r
+               // Make the toolbar sticky.\r
+               positionSticky.watch( CKEDITOR.document.findOne( '.editors-container' ) );\r
+\r
+               // Help button and help-content.\r
+               ( function() {\r
+                       var helpButton = CKEDITOR.document.getById( 'help' ),\r
+                               helpContent = CKEDITOR.document.getById( 'help-content' );\r
+\r
+                       // Don't show help button on IE8 because it's unsupported by Pico Modal.\r
+                       if ( CKEDITOR.env.ie && CKEDITOR.env.version == 8 ) {\r
+                               helpButton.hide();\r
+                       } else {\r
+                               // Display help modal when the button is clicked.\r
+                               helpButton.on( 'click', function( evt ) {\r
+                                       SF.modal( {\r
+                                               // Clone modal content from DOM.\r
+                                               content: helpContent.getHtml(),\r
+\r
+                                               afterCreate: function( modal ) {\r
+                                                       // Enable modal content button to close the modal.\r
+                                                       new CKEDITOR.dom.element( modal.modalElem() ).findOne( '.help-content-close' ).once( 'click', modal.close );\r
+                                               }\r
+                                       } ).show();\r
+                               } );\r
+                       }\r
+               } )();\r
+       } )();\r
+</script>\r
+</body>\r
+</html>\r
diff --git a/js/ckeditor/samples/toolbarconfigurator/js/abstracttoolbarmodifier.js b/js/ckeditor/samples/toolbarconfigurator/js/abstracttoolbarmodifier.js
new file mode 100644 (file)
index 0000000..65f0b87
--- /dev/null
@@ -0,0 +1,13 @@
+"function"!=typeof Object.create&&function(){var a=function(){};Object.create=function(b){if(1<arguments.length)throw Error("Second argument not supported");if(null===b)throw Error("Cannot set a null [[Prototype]]");if("object"!=typeof b)throw TypeError("Argument must be an object");a.prototype=b;return new a}}();
+CKEDITOR.plugins.add("toolbarconfiguratorarea",{afterInit:function(a){a.addMode("wysiwyg",function(b){var c=CKEDITOR.dom.element.createFromHtml('\x3cdiv class\x3d"cke_wysiwyg_div cke_reset" hidefocus\x3d"true"\x3e\x3c/div\x3e');a.ui.space("contents").append(c);c=a.editable(c);c.detach=CKEDITOR.tools.override(c.detach,function(b){return function(){b.apply(this,arguments);this.remove()}});a.setData(a.getData(1),b);a.fire("contentDom")});a.dataProcessor.toHtml=function(b){return b};a.dataProcessor.toDataFormat=
+function(b){return b}}});Object.keys||(Object.keys=function(){var a=Object.prototype.hasOwnProperty,b=!{toString:null}.propertyIsEnumerable("toString"),c="toString toLocaleString valueOf hasOwnProperty isPrototypeOf propertyIsEnumerable constructor".split(" "),e=c.length;return function(d){if("object"!==typeof d&&("function"!==typeof d||null===d))throw new TypeError("Object.keys called on non-object");var g=[],f;for(f in d)a.call(d,f)&&g.push(f);if(b)for(f=0;f<e;f++)a.call(d,c[f])&&g.push(c[f]);return g}}());
+(function(){function a(b,c){this.cfg=c||{};this.hidden=!1;this.editorId=b;this.fullToolbarEditor=new ToolbarConfigurator.FullToolbarEditor;this.actualConfig=this.originalConfig=this.mainContainer=null;this.isEditableVisible=this.waitForReady=!1;this.toolbarContainer=null;this.toolbarButtons=[]}ToolbarConfigurator.AbstractToolbarModifier=a;a.prototype.setConfig=function(b){this._onInit(void 0,b,!0)};a.prototype.init=function(b){var c=this;this.mainContainer=new CKEDITOR.dom.element("div");if(null!==
+this.fullToolbarEditor.editorInstance)throw"Only one instance of ToolbarModifier is allowed";this.editorInstance||this._createEditor(!1);this.editorInstance.once("loaded",function(){c.fullToolbarEditor.init(function(){c._onInit(b);if("function"==typeof c.onRefresh)c.onRefresh()},c.editorInstance.config)});return this.mainContainer};a.prototype._onInit=function(b,c){this.originalConfig=this.editorInstance.config;this.actualConfig=c?JSON.parse(c):JSON.parse(JSON.stringify(this.originalConfig));if(!this.actualConfig.toolbarGroups&&
+!this.actualConfig.toolbar){for(var a=this.actualConfig,d=this.editorInstance.toolbar,g=[],f=d.length,k=0;k<f;k++){var h=d[k];"string"==typeof h?g.push(h):g.push({name:h.name,groups:h.groups?h.groups.slice():[]})}a.toolbarGroups=g}"function"===typeof b&&b(this.mainContainer)};a.prototype._createModifier=function(){this.mainContainer.addClass("unselectable");this.modifyContainer&&this.modifyContainer.remove();this.modifyContainer=new CKEDITOR.dom.element("div");this.modifyContainer.addClass("toolbarModifier");
+this.mainContainer.append(this.modifyContainer);return this.mainContainer};a.prototype.getEditableArea=function(){return this.editorInstance.container.findOne("#"+this.editorInstance.id+"_contents")};a.prototype._hideEditable=function(){var b=this.getEditableArea();this.isEditableVisible=!1;this.lastEditableAreaHeight=b.getStyle("height");b.setStyle("height","0")};a.prototype._showEditable=function(){this.isEditableVisible=!0;this.getEditableArea().setStyle("height",this.lastEditableAreaHeight||"auto")};
+a.prototype._toggleEditable=function(){this.isEditableVisible?this._hideEditable():this._showEditable()};a.prototype._refreshEditor=function(){function b(){c.editorInstance.destroy();c._createEditor(!0,c.getActualConfig());c.waitForReady=!1}var c=this,a=this.editorInstance.status;this.waitForReady||("unloaded"==a||"loaded"==a?(this.waitForReady=!0,this.editorInstance.once("instanceReady",function(){b()},this)):b())};a.prototype._createEditor=function(b,c){function e(){}var d=this;this.editorInstance=
+CKEDITOR.replace(this.editorId);this.editorInstance.on("configLoaded",function(){var b=d.editorInstance.config;c&&CKEDITOR.tools.extend(b,c,!0);a.extendPluginsConfig(b)});this.editorInstance.on("uiSpace",function(b){"top"!=b.data.space&&b.stop()},null,null,-999);this.editorInstance.once("loaded",function(){var c=d.editorInstance.ui.instances,a;for(a in c)c[a]&&(c[a].click=e,c[a].onClick=e);d.isEditableVisible||d._hideEditable();d.currentActive&&d.currentActive.name&&d._highlightGroup(d.currentActive.name);
+d.hidden?d.hideUI():d.showUI();if(b&&"function"===typeof d.onRefresh)d.onRefresh()})};a.prototype.getActualConfig=function(){return JSON.parse(JSON.stringify(this.actualConfig))};a.prototype._createToolbar=function(){if(this.toolbarButtons.length){this.toolbarContainer=new CKEDITOR.dom.element("div");this.toolbarContainer.addClass("toolbar");for(var b=this.toolbarButtons.length,c=0;c<b;c+=1)this._createToolbarBtn(this.toolbarButtons[c])}};a.prototype._createToolbarBtn=function(b){var c=ToolbarConfigurator.FullToolbarEditor.createButton("string"===
+typeof b.text?b.text:b.text.inactive,b.cssClass);this.toolbarContainer.append(c);c.data("group",b.group);c.addClass(b.position);c.on("click",function(){b.clickCallback.call(this,c,b)},this);return c};a.prototype._fixGroups=function(b){b=b.toolbarGroups||[];for(var c=b.length,a=0;a<c;a+=1){var d=b[a];"/"==d?(d=b[a]={},d.type="separator",d.name="separator"+CKEDITOR.tools.getNextNumber()):(d.groups=d.groups||[],-1==CKEDITOR.tools.indexOf(d.groups,d.name)&&(this.editorInstance.ui.addToolbarGroup(d.name,
+d.groups[d.groups.length-1],d.name),d.groups.push(d.name)),this._fixSubgroups(d))}};a.prototype._fixSubgroups=function(b){b=b.groups;for(var c=b.length,a=0;a<c;a+=1){var d=b[a];b[a]={name:d,totalBtns:ToolbarConfigurator.ToolbarModifier.getTotalSubGroupButtonsNumber(d,this.fullToolbarEditor)}}};a.stringifyJSONintoOneLine=function(b,a){a=a||{};var e=JSON.stringify(b,null,""),e=e.replace(/\n/g,"");a.addSpaces&&(e=e.replace(/(\{|:|,|\[|\])/g,function(a){return a+" "}),e=e.replace(/(\])/g,function(a){return" "+
+a}));a.noQuotesOnKey&&(e=e.replace(/"(\w*)":/g,function(a,b){return b+":"}));a.singleQuotes&&(e=e.replace(/\"/g,"'"));return e};a.prototype.hideUI=function(){this.hidden=!0;this.mainContainer.hide();this.editorInstance.container&&this.editorInstance.container.hide()};a.prototype.showUI=function(){this.hidden=!1;this.mainContainer.show();this.editorInstance.container&&this.editorInstance.container.show()};a.extendPluginsConfig=function(a){var c=a.extraPlugins;a.extraPlugins=(c?c+",":"")+"toolbarconfiguratorarea"}})();
\ No newline at end of file
diff --git a/js/ckeditor/samples/toolbarconfigurator/js/fulltoolbareditor.js b/js/ckeditor/samples/toolbarconfigurator/js/fulltoolbareditor.js
new file mode 100644 (file)
index 0000000..dc19bee
--- /dev/null
@@ -0,0 +1,9 @@
+window.ToolbarConfigurator={};
+(function(){function e(){this.instanceid="fte"+CKEDITOR.tools.getNextId();this.textarea=new CKEDITOR.dom.element("textarea");this.textarea.setAttributes({id:this.instanceid,name:this.instanceid,contentEditable:!0});this.editorInstance=this.buttons=null}ToolbarConfigurator.FullToolbarEditor=e;e.prototype.init=function(b){var a=this;document.body.appendChild(this.textarea.$);CKEDITOR.replace(this.instanceid);this.editorInstance=CKEDITOR.instances[this.instanceid];this.editorInstance.once("configLoaded",function(d){var c=
+d.editor.config;delete c.removeButtons;delete c.toolbarGroups;delete c.toolbar;ToolbarConfigurator.AbstractToolbarModifier.extendPluginsConfig(c);d.editor.once("loaded",function(){a.buttons=e.toolbarToButtons(a.editorInstance.toolbar);a.buttonsByGroup=e.groupButtons(a.buttons);a.buttonNamesByGroup=a.groupButtonNamesByGroup(a.buttons);d.editor.container.hide();"function"===typeof b&&b(a.buttons)})})};e.prototype.groupButtonNamesByGroup=function(b){var a=this;b=e.groupButtons(b);for(var d in b)b[d]=
+e.map(b[d],function(b){return a.getCamelCasedButtonName(b.name)});return b};e.prototype.getGroupByName=function(b){for(var a=this.editorInstance.config.toolbarGroups||this.getFullToolbarGroupsConfig(),d=a.length,c=0;c<d;c+=1)if(a[c].name===b)return a[c];return null};e.prototype.getCamelCasedButtonName=function(b){var a=this.editorInstance.ui.items,d;for(d in a)if(a[d].name==b)return d;return null};e.prototype.getFullToolbarGroupsConfig=function(b){b=!0===b?!0:!1;for(var a=[],d=this.editorInstance.toolbar,
+c=d.length,f=0;f<c;f+=1){var e=d[f],g={};"string"!=typeof e.name?b&&a.push("/"):(g.name=e.name,e.groups&&(g.groups=Array.prototype.slice.call(e.groups)),a.push(g))}return a};e.filter=function(b,a){for(var d=b&&b.length?b.length:0,c=[],f=0;f<d;f+=1)a(b[f])&&c.push(b[f]);return c};e.map=function(b,a){var d;if(CKEDITOR.tools.isArray(b)){d=[];for(var c=b.length,f=0;f<c;f+=1)d.push(a(b[f]))}else for(c in d={},b)d[c]=a(b[c]);return d};e.groupButtons=function(b){for(var a={},d=b.length,c=0;c<d;c+=1){var f=
+b[c],e=f.toolbar.split(",")[0];a[e]=a[e]||[];a[e].push(f)}return a};e.toolbarToButtons=function(b){for(var a=[],d=b.length,c=0;c<d;c+=1)"object"==typeof b[c]&&(a=a.concat(e.groupToButtons(b[c])));return a};e.createToolbarButton=function(b){var a=new CKEDITOR.dom.element("a"),d=e.createIcon(b.name,b.icon,b.command);a.setStyle("float","none");a.addClass("cke_"+("rtl"==CKEDITOR.lang.dir?"rtl":"ltr"));if(b instanceof CKEDITOR.ui.button)a.addClass("cke_button"),a.addClass("cke_toolgroup"),a.append(d);
+else if(CKEDITOR.ui.richCombo&&b instanceof CKEDITOR.ui.richCombo){var d=new CKEDITOR.dom.element("span"),c=new CKEDITOR.dom.element("span"),f=new CKEDITOR.dom.element("span");a.addClass("cke_combo_button");d.addClass("cke_combo_text");d.addClass("cke_combo_inlinelabel");d.setText(b.label);c.addClass("cke_combo_open");f.addClass("cke_combo_arrow");c.append(f);a.append(d);a.append(c)}return a};e.createIcon=function(b,a,d){var c=CKEDITOR.skin.getIconStyle(b,"rtl"==CKEDITOR.lang.dir),c=(c=c||CKEDITOR.skin.getIconStyle(a,
+"rtl"==CKEDITOR.lang.dir))||CKEDITOR.skin.getIconStyle(d,"rtl"==CKEDITOR.lang.dir);a=new CKEDITOR.dom.element("span");a.addClass("cke_button_icon");a.addClass("cke_button__"+b+"_icon");a.setAttribute("style",c);a.setStyle("float","none");return a};e.createButton=function(b,a){var d=new CKEDITOR.dom.element("button");d.addClass("button-a");d.setAttribute("type","button");if("string"==typeof a){a=a.split(" ");for(var c=a.length;c--;)d.addClass(a[c])}d.setHtml(b);return d};e.groupToButtons=function(b){for(var a=
+[],d=(b=b.items)?b.length:0,c=0;c<d;c+=1){var f=b[c];if(f instanceof CKEDITOR.ui.button||CKEDITOR.ui.richCombo&&f instanceof CKEDITOR.ui.richCombo)f.$=e.createToolbarButton(f),a.push(f)}return a}})();
\ No newline at end of file
diff --git a/js/ckeditor/samples/toolbarconfigurator/js/toolbarmodifier.js b/js/ckeditor/samples/toolbarconfigurator/js/toolbarmodifier.js
new file mode 100644 (file)
index 0000000..9731818
--- /dev/null
@@ -0,0 +1,33 @@
+(function(){function d(a,b){l.call(this,a,b);this.actualConfig=this.originalConfig=this.removedButtons=null;this.emptyVisible=!1;this.state="edit";this.toolbarButtons=[{text:{active:"Hide empty toolbar groups",inactive:"Show empty toolbar groups"},group:"edit",position:"left",cssClass:"button-a-soft",clickCallback:function(a,b){a[a.hasClass("button-a-background")?"removeClass":"addClass"]("button-a-background");this._toggleVisibilityEmptyElements();this.emptyVisible?a.setText(b.text.active):a.setText(b.text.inactive)}},
+{text:"Add row separator",group:"edit",position:"left",cssClass:"button-a-soft",clickCallback:function(){this._addSeparator()}},{text:"Select config",group:"config",position:"left",cssClass:"button-a-soft",clickCallback:function(){this.configContainer.findOne("textarea").$.select()}},{text:"Back to configurator",group:"config",position:"right",cssClass:"button-a-background",clickCallback:function(){if("paste"===this.state){var a=this.configContainer.findOne("textarea").getValue();(a=d.evaluateToolbarGroupsConfig(a))?
+this.setConfig(a):alert("Your pasted config is wrong.")}this.state="edit";this._showConfigurationTool();this.showToolbarBtnsByGroupName(this.state)}},{text:'Get toolbar \x3cspan class\x3d"highlight"\x3econfig\x3c/span\x3e',group:"edit",position:"right",cssClass:"button-a-background icon-pos-left icon-download",clickCallback:function(){this.state="config";this._showConfig();this.showToolbarBtnsByGroupName(this.state)}}];this.cachedActiveElement=null}var l=ToolbarConfigurator.AbstractToolbarModifier;
+ToolbarConfigurator.ToolbarModifier=d;d.prototype=Object.create(ToolbarConfigurator.AbstractToolbarModifier.prototype);d.prototype.getActualConfig=function(){var a=l.prototype.getActualConfig.call(this);if(a.toolbarGroups)for(var b=a.toolbarGroups.length,c=0;c<b;c+=1)a.toolbarGroups[c]=d.parseGroupToConfigValue(a.toolbarGroups[c]);return a};d.prototype._onInit=function(a,b,c){c=!0===c;l.prototype._onInit.call(this,void 0,b);this.removedButtons=[];c?this.removedButtons=this.actualConfig.removeButtons?
+this.actualConfig.removeButtons.split(","):[]:"removeButtons"in this.originalConfig?this.removedButtons=this.originalConfig.removeButtons?this.originalConfig.removeButtons.split(","):[]:(this.originalConfig.removeButtons="",this.removedButtons=[]);this.actualConfig.toolbarGroups||(this.actualConfig.toolbarGroups=this.fullToolbarEditor.getFullToolbarGroupsConfig());this._fixGroups(this.actualConfig);this._calculateTotalBtns();this._createModifier();this._refreshMoveBtnsAvalibility();this._refreshBtnTabIndexes();
+"function"===typeof a&&a(this.mainContainer)};d.prototype._showConfigurationTool=function(){this.configContainer.addClass("hidden");this.modifyContainer.removeClass("hidden")};d.prototype._showConfig=function(){var a=this.getActualConfig(),b,c;if(a.toolbarGroups){b=a.toolbarGroups;for(var e=this.cfg.trimEmptyGroups,f=[],g=b.length,m=0;m<g;m++){var h=b[m];if("/"===h)f.push("'/'");else{if(e)for(var k=h.groups.length;k--;)0===d.getTotalSubGroupButtonsNumber(h.groups[k],this.fullToolbarEditor)&&h.groups.splice(k,
+1);e&&0===h.groups.length||f.push(l.stringifyJSONintoOneLine(h,{addSpaces:!0,noQuotesOnKey:!0,singleQuotes:!0}))}}b="\n\t\t"+f.join(",\n\t\t")}a.removeButtons&&(c=a.removeButtons);a=['\x3ctextarea class\x3d"configCode" readonly\x3eCKEDITOR.editorConfig \x3d function( config ) {\n',b?"\tconfig.toolbarGroups \x3d ["+b+"\n\t];":"",c?"\n\n":"",c?"\tconfig.removeButtons \x3d '"+c+"';":"","\n};\x3c/textarea\x3e"].join("");this.modifyContainer.addClass("hidden");this.configContainer.removeClass("hidden");
+this.configContainer.setHtml(a)};d.prototype._toggleVisibilityEmptyElements=function(){this.modifyContainer.hasClass("empty-visible")?(this.modifyContainer.removeClass("empty-visible"),this.emptyVisible=!1):(this.modifyContainer.addClass("empty-visible"),this.emptyVisible=!0);this._refreshMoveBtnsAvalibility()};d.prototype._createModifier=function(){function a(){b._highlightGroup(this.data("name"))}var b=this;l.prototype._createModifier.call(this);this.modifyContainer.setHtml(this._toolbarConfigToListString());
+var c=this.modifyContainer.find('li[data-type\x3d"group"]');this.modifyContainer.on("mouseleave",function(){this._dehighlightActiveToolGroup()},this);for(var e=c.count(),f=0;f<e;f+=1)c.getItem(f).on("mouseenter",a);CKEDITOR.document.on("keypress",function(a){a=a.data.$.keyCode;a=32===a||13===a;var c=new CKEDITOR.dom.element(CKEDITOR.document.$.activeElement);c.getAscendant(function(a){return a.$===b.mainContainer.$})&&a&&"button"===c.data("type")&&c.findOne("input").$.click()});this.modifyContainer.on("click",
+function(a){var c=a.data.$,e=new CKEDITOR.dom.element(c.target||c.srcElement);if(a=d.getGroupOrSeparatorLiAncestor(e)){b.cachedActiveElement=document.activeElement;if(e.$ instanceof HTMLInputElement)b._handleCheckboxClicked(e);else if(e.$ instanceof HTMLButtonElement&&(c.preventDefault?c.preventDefault():c.returnValue=!1,(c=b._handleAnchorClicked(e.$))&&"remove"==c.action))return;c=a.data("type");a=a.data("name");b._setActiveElement(c,a);b.cachedActiveElement&&b.cachedActiveElement.focus()}});this.toolbarContainer||
+(this._createToolbar(),this.toolbarContainer.insertBefore(this.mainContainer.getChildren().getItem(0)));this.showToolbarBtnsByGroupName("edit");this.configContainer||(this.configContainer=new CKEDITOR.dom.element("div"),this.configContainer.addClass("configContainer"),this.configContainer.addClass("hidden"),this.mainContainer.append(this.configContainer));return this.mainContainer};d.prototype.showToolbarBtnsByGroupName=function(a){if(this.toolbarContainer)for(var b=this.toolbarContainer.find("button"),
+c=b.count(),e=0;e<c;e+=1){var d=b.getItem(e);d.data("group")==a?d.removeClass("hidden"):d.addClass("hidden")}};d.parseGroupToConfigValue=function(a){if("separator"==a.type)return"/";var b=a.groups,c=b.length;delete a.totalBtns;for(var e=0;e<c;e+=1)b[e]=b[e].name;return a};d.getGroupOrSeparatorLiAncestor=function(a){return a.$ instanceof HTMLLIElement&&"group"==a.data("type")?a:d.getFirstAncestor(a,function(a){a=a.data("type");return"group"==a||"separator"==a})};d.prototype._setActiveElement=function(a,
+b){this.currentActive&&this.currentActive.elem.removeClass("active");if(null===a)this._dehighlightActiveToolGroup(),this.currentActive=null;else{var c=this.mainContainer.findOne('ul[data-type\x3dtable-body] li[data-type\x3d"'+a+'"][data-name\x3d"'+b+'"]');c.addClass("active");this.currentActive={type:a,name:b,elem:c};"group"==a&&this._highlightGroup(b);"separator"==a&&this._dehighlightActiveToolGroup()}};d.prototype.getActiveToolGroup=function(){return this.editorInstance.container?this.editorInstance.container.findOne(".cke_toolgroup.active, .cke_toolbar.active"):
+null};d.prototype._dehighlightActiveToolGroup=function(){var a=this.getActiveToolGroup();a&&a.removeClass("active");this.editorInstance.container&&this.editorInstance.container.removeClass("some-toolbar-active")};d.prototype._highlightGroup=function(a){this.editorInstance.container&&(a=this.getFirstEnabledButtonInGroup(a),a=this.editorInstance.container.findOne(".cke_button__"+a+", .cke_combo__"+a),this._dehighlightActiveToolGroup(),this.editorInstance.container&&this.editorInstance.container.addClass("some-toolbar-active"),
+a&&(a=d.getFirstAncestor(a,function(a){return a.hasClass("cke_toolbar")}))&&a.addClass("active"))};d.prototype.getFirstEnabledButtonInGroup=function(a){var b=this.actualConfig.toolbarGroups;a=this.getGroupIndex(a);b=b[a];if(-1===a)return null;a=b.groups?b.groups.length:0;for(var c=0;c<a;c+=1){var e=this.getFirstEnabledButtonInSubgroup(b.groups[c].name);if(e)return e}return null};d.prototype.getFirstEnabledButtonInSubgroup=function(a){for(var b=(a=this.fullToolbarEditor.buttonsByGroup[a])?a.length:
+0,c=0;c<b;c+=1){var e=a[c].name;if(!this.isButtonRemoved(e))return e}return null};d.prototype._handleCheckboxClicked=function(a){var b=a.getAscendant("li").data("name");a.$.checked?this._removeButtonFromRemoved(b):this._addButtonToRemoved(b)};d.prototype._handleAnchorClicked=function(a){a=new CKEDITOR.dom.element(a);var b=a.getAscendant("li"),c=b.getAscendant("ul"),e=b.data("type"),d=b.data("name"),g=a.data("direction"),m="up"===g?b.getPrevious():b.getNext(),h;if(a.hasClass("disabled"))return null;
+if(a.hasClass("remove"))return b.remove(),this._removeSeparator(b.data("name")),this._setActiveElement(null),{action:"remove"};if(!a.hasClass("move")||!m)return{action:null};if("group"===e||"separator"===e)h=this._moveGroup(g,d);"subgroup"===e&&(h=b.getAscendant("li").data("name"),h=this._moveSubgroup(g,h,d));"up"===g&&b.insertBefore(c.getChild(h));"down"===g&&b.insertAfter(c.getChild(h));for(var k;b="up"===g?b.getPrevious():b.getNext();)if(this.emptyVisible||!b.hasClass("empty")){k=b;break}k||(k=
+'[data-direction\x3d"'+("up"===g?"down":"up")+'"]',this.cachedActiveElement=a.getParent().findOne(k));this._refreshMoveBtnsAvalibility();this._refreshBtnTabIndexes();return{action:"move"}};d.prototype._refreshMoveBtnsAvalibility=function(){function a(a){var c=a.count();for(d=0;d<c;d+=1)b._disableElementsInList(a.getItem(d))}for(var b=this,c=this.mainContainer.find("ul[data-type\x3dtable-body] li \x3e p \x3e span \x3e button.move.disabled"),e=c.count(),d=0;d<e;d+=1)c.getItem(d).removeClass("disabled");
+a(this.mainContainer.find("ul[data-type\x3dtable-body]"));a(this.mainContainer.find("ul[data-type\x3dtable-body] \x3e li \x3e ul"))};d.prototype._refreshBtnTabIndexes=function(){for(var a=this.mainContainer.find('[data-tab\x3d"true"]'),b=a.count(),c=0;c<b;c++){var e=a.getItem(c),d=e.hasClass("disabled");e.setAttribute("tabindex",d?-1:c)}};d.prototype._disableElementsInList=function(a){function b(a){return!a.hasClass("empty")}if(a.getChildren().count()){var c;this.emptyVisible?(c=a.getFirst(),a=a.getLast()):
+(c=a.getFirst(b),a=a.getLast(b));if(c)var e=c.findOne('p button[data-direction\x3d"up"]');if(a)var d=a.findOne('p button[data-direction\x3d"down"]');e&&(e.addClass("disabled"),e.setAttribute("tabindex","-1"));d&&(d.addClass("disabled"),d.setAttribute("tabindex","-1"))}};d.prototype.getGroupIndex=function(a){for(var b=this.actualConfig.toolbarGroups,c=b.length,d=0;d<c;d+=1)if(b[d].name===a)return d;return-1};d.prototype._addSeparator=function(){var a=this._determineSeparatorToAddIndex(),b=d.createSeparatorLiteral(),
+c=CKEDITOR.dom.element.createFromHtml(d.getToolbarSeparatorString(b));this.actualConfig.toolbarGroups.splice(a,0,b);c.insertBefore(this.modifyContainer.findOne("ul[data-type\x3dtable-body]").getChild(a));this._setActiveElement("separator",b.name);this._refreshMoveBtnsAvalibility();this._refreshBtnTabIndexes();this._refreshEditor()};d.prototype._removeSeparator=function(a){var b=CKEDITOR.tools.indexOf(this.actualConfig.toolbarGroups,function(b){return"separator"==b.type&&b.name==a});this.actualConfig.toolbarGroups.splice(b,
+1);this._refreshMoveBtnsAvalibility();this._refreshBtnTabIndexes();this._refreshEditor()};d.prototype._determineSeparatorToAddIndex=function(){return this.currentActive?("group"==this.currentActive.elem.data("type")||"separator"==this.currentActive.elem.data("type")?this.currentActive.elem:this.currentActive.elem.getAscendant("li")).getIndex():0};d.prototype._moveElement=function(a,b,c){function e(a){return a.totalBtns||"separator"==a.type}c=this.emptyVisible?"down"==c?b+1:b-1:d.getFirstElementIndexWith(a,
+b,c,e);return d.moveTo(c-b,a,b)};d.prototype._moveGroup=function(a,b){var c=this.getGroupIndex(b),c=this._moveElement(this.actualConfig.toolbarGroups,c,a);this._refreshMoveBtnsAvalibility();this._refreshBtnTabIndexes();this._refreshEditor();return c};d.prototype._moveSubgroup=function(a,b,c){b=this.getGroupIndex(b);b=this.actualConfig.toolbarGroups[b];var d=CKEDITOR.tools.indexOf(b.groups,function(a){return a.name==c});a=this._moveElement(b.groups,d,a);this._refreshEditor();return a};d.prototype._calculateTotalBtns=
+function(){for(var a=this.actualConfig.toolbarGroups,b=a.length;b--;){var c=a[b],e=d.getTotalGroupButtonsNumber(c,this.fullToolbarEditor);"separator"!=c.type&&(c.totalBtns=e)}};d.prototype._addButtonToRemoved=function(a){if(-1!=CKEDITOR.tools.indexOf(this.removedButtons,a))throw"Button already added to removed";this.removedButtons.push(a);this.actualConfig.removeButtons=this.removedButtons.join(",");this._refreshEditor()};d.prototype._removeButtonFromRemoved=function(a){a=CKEDITOR.tools.indexOf(this.removedButtons,
+a);if(-1===a)throw"Trying to remove button from removed, but not found";this.removedButtons.splice(a,1);this.actualConfig.removeButtons=this.removedButtons.join(",");this._refreshEditor()};d.parseGroupToConfigValue=function(a){if("separator"==a.type)return"/";var b=a.groups,c=b.length;delete a.totalBtns;for(var d=0;d<c;d+=1)b[d]=b[d].name;return a};d.getGroupOrSeparatorLiAncestor=function(a){return a.$ instanceof HTMLLIElement&&"group"==a.data("type")?a:d.getFirstAncestor(a,function(a){a=a.data("type");
+return"group"==a||"separator"==a})};d.createSeparatorLiteral=function(){return{type:"separator",name:"separator"+CKEDITOR.tools.getNextNumber()}};d.prototype._toolbarConfigToListString=function(){for(var a=this.actualConfig.toolbarGroups||[],b='\x3cul data-type\x3d"table-body"\x3e',c=a.length,e=0;e<c;e+=1)var f=a[e],b="separator"===f.type?b+d.getToolbarSeparatorString(f):b+this._getToolbarGroupString(f);b+="\x3c/ul\x3e";return d.getToolbarHeaderString()+b};d.prototype._getToolbarGroupString=function(a){var b=
+a.groups,c;c=""+['\x3cli data-type\x3d"group" data-name\x3d"',a.name,'" ',a.totalBtns?"":'class\x3d"empty"',"\x3e"].join("");c+=d.getToolbarElementPreString(a)+"\x3cul\x3e";a=b.length;for(var e=0;e<a;e+=1){var f=b[e];c+=this._getToolbarSubgroupString(f,this.fullToolbarEditor.buttonsByGroup[f.name])}return c+"\x3c/ul\x3e\x3c/li\x3e"};d.getToolbarSeparatorString=function(a){return['\x3cli data-type\x3d"',a.type,'" data-name\x3d"',a.name,'"\x3e',d.getToolbarElementPreString("row separator"),"\x3c/li\x3e"].join("")};
+d.getToolbarHeaderString=function(){return'\x3cul data-type\x3d"table-header"\x3e\x3cli data-type\x3d"header"\x3e\x3cp\x3eToolbars\x3c/p\x3e\x3cul\x3e\x3cli\x3e\x3cp\x3eToolbar groups\x3c/p\x3e\x3cp\x3eToolbar group items\x3c/p\x3e\x3c/li\x3e\x3c/ul\x3e\x3c/li\x3e\x3c/ul\x3e'};d.getFirstAncestor=function(a,b){for(var c=a.getParents(),d=c.length;d--;)if(b(c[d]))return c[d];return null};d.getFirstElementIndexWith=function(a,b,c,d){for(;"up"===c?b--:++b<a.length;)if(d(a[b]))return b;return-1};d.moveTo=
+function(a,b,c){var d;-1!==c&&(d=b.splice(c,1)[0]);a=c+a;b.splice(a,0,d);return a};d.getTotalSubGroupButtonsNumber=function(a,b){var c=b.buttonsByGroup["string"==typeof a?a:a.name];return c?c.length:0};d.getTotalGroupButtonsNumber=function(a,b){for(var c=0,e=a.groups,f=e?e.length:0,g=0;g<f;g+=1)c+=d.getTotalSubGroupButtonsNumber(e[g],b);return c};d.prototype._getToolbarSubgroupString=function(a,b){var c;c=""+['\x3cli data-type\x3d"subgroup" data-name\x3d"',a.name,'" ',a.totalBtns?"":'class\x3d"empty" ',
+"\x3e"].join("");c+=d.getToolbarElementPreString(a.name);c+="\x3cul\x3e";for(var e=b?b.length:0,f=0;f<e;f+=1)c+=this.getButtonString(b[f]);return c+="\x3c/ul\x3e\x3c/li\x3e"};d.prototype._getConfigButtonName=function(a){var b=this.fullToolbarEditor.editorInstance.ui.items,c;for(c in b)if(b[c].name==a)return c;return null};d.prototype.isButtonRemoved=function(a){return-1!=CKEDITOR.tools.indexOf(this.removedButtons,this._getConfigButtonName(a))};d.prototype.getButtonString=function(a){var b=this.isButtonRemoved(a.name)?
+"":'checked\x3d"checked"';return['\x3cli data-tab\x3d"true" data-type\x3d"button" data-name\x3d"',this._getConfigButtonName(a.name),'"\x3e\x3clabel title\x3d"',a.label,'" \x3e\x3cinput tabindex\x3d"-1"type\x3d"checkbox"',b,"/\x3e",a.$.getOuterHtml(),"\x3c/label\x3e\x3c/li\x3e"].join("")};d.getToolbarElementPreString=function(a){a=a.name?a.name:a;return['\x3cp\x3e\x3cspan\x3e\x3cbutton title\x3d"Move element upward" data-tab\x3d"true" data-direction\x3d"up" class\x3d"move icon-up-big"\x3e\x3c/button\x3e\x3cbutton title\x3d"Move element downward" data-tab\x3d"true" data-direction\x3d"down" class\x3d"move icon-down-big"\x3e\x3c/button\x3e',
+"row separator"==a?'\x3cbutton title\x3d"Remove element" data-tab\x3d"true" class\x3d"remove icon-trash"\x3e\x3c/button\x3e':"",a,"\x3c/span\x3e\x3c/p\x3e"].join("")};d.evaluateToolbarGroupsConfig=function(a){return a=function(a){var c={},d;try{d=eval("("+a+")")}catch(f){try{d=eval(a)}catch(g){return null}}return c.toolbarGroups&&"number"===typeof c.toolbarGroups.length?JSON.stringify(c):d&&"number"===typeof d.length?JSON.stringify({toolbarGroups:d}):d&&d.toolbarGroups?JSON.stringify(d):null}(a)};
+return d})();
\ No newline at end of file
diff --git a/js/ckeditor/samples/toolbarconfigurator/js/toolbartextmodifier.js b/js/ckeditor/samples/toolbarconfigurator/js/toolbartextmodifier.js
new file mode 100644 (file)
index 0000000..b9ef46b
--- /dev/null
@@ -0,0 +1,14 @@
+(function(){function e(a){l.call(this,a);this.hintContainer=this.codeContainer=null}var l=ToolbarConfigurator.AbstractToolbarModifier,g=ToolbarConfigurator.FullToolbarEditor;ToolbarConfigurator.ToolbarTextModifier=e;e.prototype=Object.create(l.prototype);e.prototype._onInit=function(a,d){l.prototype._onInit.call(this,void 0,d);this._createModifier(d?this.actualConfig:void 0);"function"===typeof a&&a(this.mainContainer)};e.prototype._createModifier=function(a){function d(a){var b=c(a);if(null!==b.charsBetween){var d=
+k.getUnusedButtonsArray(k.actualConfig.toolbar,!0,b.charsBetween),e=a.getCursor(),b=CodeMirror.Pos(e.line,e.ch-b.charsBetween.length),h=a.getTokenAt(e);"{"===a.getTokenAt({line:e.line,ch:h.start}).string&&(d=["name"]);if(0!==d.length)return new f(b,e,d)}}function f(a,c,b){this.from=a;this.to=c;this.list=b;this._handlers=[]}function c(a,c){var b={};b.cur=a.getCursor();b.tok=a.getTokenAt(b.cur);b["char"]=c||b.tok.string.charAt(b.tok.string.length-1);var d=a.getRange(CodeMirror.Pos(b.cur.line,0),b.cur).split("").reverse().join(""),
+d=d.replace(/(['|"]\w*['|"])/g,"");b.charsBetween=d.match(/(^\w*)(['|"])/);b.charsBetween&&(b.endChar=b.charsBetween[2],b.charsBetween=b.charsBetween[1].split("").reverse().join(""));return b}function b(a){setTimeout(function(){a.state.completionActive||CodeMirror.showHint(a,d,{hintsClass:"toolbar-modifier",completeSingle:!1})},100);return CodeMirror.Pass}var k=this;this._createToolbar();this.toolbarContainer&&this.mainContainer.append(this.toolbarContainer);l.prototype._createModifier.call(this);
+this._setupActualConfig(a);a=this.actualConfig.toolbar;a=CKEDITOR.tools.isArray(a)?"\tconfig.toolbar \x3d "+("[\n\t\t"+g.map(a,function(a){return l.stringifyJSONintoOneLine(a,{addSpaces:!0,noQuotesOnKey:!0,singleQuotes:!0})}).join(",\n\t\t")+"\n\t]")+";":"config.toolbar \x3d [];";a=["CKEDITOR.editorConfig \x3d function( config ) {\n",a,"\n};"].join("");var e=new CKEDITOR.dom.element("div");e.addClass("codemirror-wrapper");this.modifyContainer.append(e);this.codeContainer=CodeMirror(e.$,{mode:{name:"javascript",
+json:!0},lineNumbers:!1,lineWrapping:!0,viewportMargin:Infinity,value:a,smartIndent:!1,indentWithTabs:!0,indentUnit:4,tabSize:4,theme:"neo",extraKeys:{Left:b,Right:b,"'''":b,"'\"'":b,Backspace:b,Delete:b,"Shift-Tab":"indentLess"}});this.codeContainer.on("endCompletion",function(a,b){var d=c(a);void 0!==b&&a.replaceSelection(d.endChar)});this.codeContainer.on("change",function(){var a=k.codeContainer.getValue(),a=k._evaluateValue(a);null!==a?(k.actualConfig.toolbar=a.toolbar?a.toolbar:k.actualConfig.toolbar,
+k._fillHintByUnusedElements(),k._refreshEditor(),k.mainContainer.removeClass("invalid")):k.mainContainer.addClass("invalid")});this.hintContainer=new CKEDITOR.dom.element("div");this.hintContainer.addClass("toolbarModifier-hints");this._fillHintByUnusedElements();this.hintContainer.insertBefore(e)};e.prototype._fillHintByUnusedElements=function(){var a=this.getUnusedButtonsArray(this.actualConfig.toolbar,!0),a=this.groupButtonNamesByGroup(a),d=g.map(a,function(a){var b=g.map(a.buttons,function(a){return"\x3ccode\x3e"+
+a+"\x3c/code\x3e "}).join("");return["\x3cdt\x3e\x3ccode\x3e",a.name,"\x3c/code\x3e\x3c/dt\x3e\x3cdd\x3e",b,"\x3c/dd\x3e"].join("")}).join(" "),f='\x3cdt class\x3d"list-header"\x3eToolbar group\x3c/dt\x3e\x3cdd class\x3d"list-header"\x3eUnused items\x3c/dd\x3e';a.length||(f="\x3cp\x3eAll items are in use.\x3c/p\x3e");this.codeContainer.refresh();this.hintContainer.setHtml("\x3ch3\x3eUnused toolbar items\x3c/h3\x3e\x3cdl\x3e"+f+d+"\x3c/dl\x3e")};e.prototype.getToolbarGroupByButtonName=function(a){var d=
+this.fullToolbarEditor.buttonNamesByGroup,f;for(f in d)for(var c=d[f],b=c.length;b--;)if(a===c[b])return f;return null};e.prototype.getUnusedButtonsArray=function(a,d,f){d=!0===d?!0:!1;var c=e.mapToolbarCfgToElementsList(a);a=Object.keys(this.fullToolbarEditor.editorInstance.ui.items);a=g.filter(a,function(a){var d="-"===a;a=void 0===f||0===a.toLowerCase().indexOf(f.toLowerCase());return!d&&a});a=g.filter(a,function(a){return-1==CKEDITOR.tools.indexOf(c,a)});d&&a.sort();return a};e.prototype.groupButtonNamesByGroup=
+function(a){var d=[],f=JSON.parse(JSON.stringify(this.fullToolbarEditor.buttonNamesByGroup)),c;for(c in f){var b=f[c],b=g.filter(b,function(b){return-1!==CKEDITOR.tools.indexOf(a,b)});b.length&&d.push({name:c,buttons:b})}return d};e.mapToolbarCfgToElementsList=function(a){function d(a){return"-"!==a}for(var f=[],c=a.length,b=0;b<c;b+=1)a[b]&&"string"!==typeof a[b]&&(f=f.concat(g.filter(a[b].items,d)));return f};e.prototype._setupActualConfig=function(a){a=a||this.editorInstance.config;CKEDITOR.tools.isArray(a.toolbar)||
+(a.toolbarGroups||(a.toolbarGroups=this.fullToolbarEditor.getFullToolbarGroupsConfig(!0)),this._fixGroups(a),a.toolbar=this._mapToolbarGroupsToToolbar(a.toolbarGroups,this.actualConfig.removeButtons),this.actualConfig.toolbar=a.toolbar,this.actualConfig.removeButtons="")};e.prototype._mapToolbarGroupsToToolbar=function(a,d){d=d||this.editorInstance.config.removedBtns;d="string"==typeof d?d.split(","):[];for(var f=a.length;f--;){var c=this._mapToolbarSubgroup(a[f],d);"separator"===a[f].type?a[f]="/":
+CKEDITOR.tools.isArray(c)&&0===c.length?a.splice(f,1):a[f]="string"==typeof c?c:{name:a[f].name,items:c}}return a};e.prototype._mapToolbarSubgroup=function(a,d){if("string"==typeof a)return a;for(var f=a.groups?a.groups.length:0,c=[],b=0;b<f;b+=1){var e=a.groups[b],e=this.fullToolbarEditor.buttonsByGroup["string"===typeof e?e:e.name]||[],e=this._mapButtonsToButtonsNames(e,d),g=e.length,c=c.concat(e);g&&c.push("-")}"-"==c[c.length-1]&&c.pop();return c};e.prototype._mapButtonsToButtonsNames=function(a,
+d){for(var f=a.length;f--;){var c=a[f],c="string"===typeof c?c:this.fullToolbarEditor.getCamelCasedButtonName(c.name);-1!==CKEDITOR.tools.indexOf(d,c)?a.splice(f,1):a[f]=c}return a};e.prototype._evaluateValue=function(a){var d;try{var f={};Function("var CKEDITOR \x3d {}; "+a+"; return CKEDITOR;")().editorConfig(f);d=f;for(var c=d.toolbar.length;c--;)d.toolbar[c]||d.toolbar.splice(c,1)}catch(b){d=null}return d};e.prototype.mapToolbarToToolbarGroups=function(a){function d(a,b){a=a.slice();for(var d=
+b.length;d--;){var c=a.indexOf(b[d]);-1!==c&&a.splice(c,1)}return a}for(var f={},c=[],b=[],c=a.length,e=0;e<c;e++)if("/"===a[e])b.push("/");else{var g=a[e].items,m={};m.name=a[e].name;m.groups=[];for(var l=g.length,p=0;p<l;p++){var n=g[p];if("-"!==n){var h=this.getToolbarGroupByButtonName(n);-1===m.groups.indexOf(h)&&m.groups.push(h);f[h]=f[h]||{};h=f[h].buttons=f[h].buttons||{};h[n]=h[n]||{used:0,origin:m.name};h[n].used++}}b.push(m)}c=function(a,b){var c=[],e;for(e in a)var f=a[e],g=b[e].slice(),
+c=c.concat(d(g,Object.keys(f.buttons)));return c}(f,this.fullToolbarEditor.buttonNamesByGroup);return{toolbarGroups:b,removeButtons:c.join(",")}};return e})();
\ No newline at end of file
diff --git a/js/ckeditor/samples/toolbarconfigurator/lib/codemirror/LICENSE b/js/ckeditor/samples/toolbarconfigurator/lib/codemirror/LICENSE
new file mode 100644 (file)
index 0000000..d21bbea
--- /dev/null
@@ -0,0 +1,19 @@
+Copyright (C) 2014 by Marijn Haverbeke <marijnh@gmail.com> and others
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/js/ckeditor/samples/toolbarconfigurator/lib/codemirror/codemirror.css b/js/ckeditor/samples/toolbarconfigurator/lib/codemirror/codemirror.css
new file mode 100644 (file)
index 0000000..2fe9d0f
--- /dev/null
@@ -0,0 +1,325 @@
+/* BASICS */\r
+\r
+.CodeMirror {\r
+  /* Set height, width, borders, and global font properties here */\r
+  font-family: monospace;\r
+  height: 300px;\r
+  color: black;\r
+}\r
+\r
+/* PADDING */\r
+\r
+.CodeMirror-lines {\r
+  padding: 4px 0; /* Vertical padding around content */\r
+}\r
+.CodeMirror pre {\r
+  padding: 0 4px; /* Horizontal padding of content */\r
+}\r
+\r
+.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {\r
+  background-color: white; /* The little square between H and V scrollbars */\r
+}\r
+\r
+/* GUTTER */\r
+\r
+.CodeMirror-gutters {\r
+  border-right: 1px solid #ddd;\r
+  background-color: #f7f7f7;\r
+  white-space: nowrap;\r
+}\r
+.CodeMirror-linenumbers {}\r
+.CodeMirror-linenumber {\r
+  padding: 0 3px 0 5px;\r
+  min-width: 20px;\r
+  text-align: right;\r
+  color: #999;\r
+  white-space: nowrap;\r
+}\r
+\r
+.CodeMirror-guttermarker { color: black; }\r
+.CodeMirror-guttermarker-subtle { color: #999; }\r
+\r
+/* CURSOR */\r
+\r
+.CodeMirror div.CodeMirror-cursor {\r
+  border-left: 1px solid black;\r
+}\r
+/* Shown when moving in bi-directional text */\r
+.CodeMirror div.CodeMirror-secondarycursor {\r
+  border-left: 1px solid silver;\r
+}\r
+.CodeMirror.cm-fat-cursor div.CodeMirror-cursor {\r
+  width: auto;\r
+  border: 0;\r
+  background: #7e7;\r
+}\r
+.CodeMirror.cm-fat-cursor div.CodeMirror-cursors {\r
+  z-index: 1;\r
+}\r
+\r
+.cm-animate-fat-cursor {\r
+  width: auto;\r
+  border: 0;\r
+  -webkit-animation: blink 1.06s steps(1) infinite;\r
+  -moz-animation: blink 1.06s steps(1) infinite;\r
+  animation: blink 1.06s steps(1) infinite;\r
+}\r
+@-moz-keyframes blink {\r
+  0% { background: #7e7; }\r
+  50% { background: none; }\r
+  100% { background: #7e7; }\r
+}\r
+@-webkit-keyframes blink {\r
+  0% { background: #7e7; }\r
+  50% { background: none; }\r
+  100% { background: #7e7; }\r
+}\r
+@keyframes blink {\r
+  0% { background: #7e7; }\r
+  50% { background: none; }\r
+  100% { background: #7e7; }\r
+}\r
+\r
+/* Can style cursor different in overwrite (non-insert) mode */\r
+div.CodeMirror-overwrite div.CodeMirror-cursor {}\r
+\r
+.cm-tab { display: inline-block; text-decoration: inherit; }\r
+\r
+.CodeMirror-ruler {\r
+  border-left: 1px solid #ccc;\r
+  position: absolute;\r
+}\r
+\r
+/* DEFAULT THEME */\r
+\r
+.cm-s-default .cm-keyword {color: #708;}\r
+.cm-s-default .cm-atom {color: #219;}\r
+.cm-s-default .cm-number {color: #164;}\r
+.cm-s-default .cm-def {color: #00f;}\r
+.cm-s-default .cm-variable,\r
+.cm-s-default .cm-punctuation,\r
+.cm-s-default .cm-property,\r
+.cm-s-default .cm-operator {}\r
+.cm-s-default .cm-variable-2 {color: #05a;}\r
+.cm-s-default .cm-variable-3 {color: #085;}\r
+.cm-s-default .cm-comment {color: #a50;}\r
+.cm-s-default .cm-string {color: #a11;}\r
+.cm-s-default .cm-string-2 {color: #f50;}\r
+.cm-s-default .cm-meta {color: #555;}\r
+.cm-s-default .cm-qualifier {color: #555;}\r
+.cm-s-default .cm-builtin {color: #30a;}\r
+.cm-s-default .cm-bracket {color: #997;}\r
+.cm-s-default .cm-tag {color: #170;}\r
+.cm-s-default .cm-attribute {color: #00c;}\r
+.cm-s-default .cm-header {color: blue;}\r
+.cm-s-default .cm-quote {color: #090;}\r
+.cm-s-default .cm-hr {color: #999;}\r
+.cm-s-default .cm-link {color: #00c;}\r
+\r
+.cm-negative {color: #d44;}\r
+.cm-positive {color: #292;}\r
+.cm-header, .cm-strong {font-weight: bold;}\r
+.cm-em {font-style: italic;}\r
+.cm-link {text-decoration: underline;}\r
+.cm-strikethrough {text-decoration: line-through;}\r
+\r
+.cm-s-default .cm-error {color: #f00;}\r
+.cm-invalidchar {color: #f00;}\r
+\r
+.CodeMirror-composing { border-bottom: 2px solid; }\r
+\r
+/* Default styles for common addons */\r
+\r
+div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}\r
+div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}\r
+.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }\r
+.CodeMirror-activeline-background {background: #e8f2ff;}\r
+\r
+/* STOP */\r
+\r
+/* The rest of this file contains styles related to the mechanics of\r
+   the editor. You probably shouldn't touch them. */\r
+\r
+.CodeMirror {\r
+  position: relative;\r
+  overflow: hidden;\r
+  background: white;\r
+}\r
+\r
+.CodeMirror-scroll {\r
+  overflow: scroll !important; /* Things will break if this is overridden */\r
+  /* 30px is the magic margin used to hide the element's real scrollbars */\r
+  /* See overflow: hidden in .CodeMirror */\r
+  margin-bottom: -30px; margin-right: -30px;\r
+  padding-bottom: 30px;\r
+  height: 100%;\r
+  outline: none; /* Prevent dragging from highlighting the element */\r
+  position: relative;\r
+}\r
+.CodeMirror-sizer {\r
+  position: relative;\r
+  border-right: 30px solid transparent;\r
+}\r
+\r
+/* The fake, visible scrollbars. Used to force redraw during scrolling\r
+   before actuall scrolling happens, thus preventing shaking and\r
+   flickering artifacts. */\r
+.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {\r
+  position: absolute;\r
+  z-index: 6;\r
+  display: none;\r
+}\r
+.CodeMirror-vscrollbar {\r
+  right: 0; top: 0;\r
+  overflow-x: hidden;\r
+  overflow-y: scroll;\r
+}\r
+.CodeMirror-hscrollbar {\r
+  bottom: 0; left: 0;\r
+  overflow-y: hidden;\r
+  overflow-x: scroll;\r
+}\r
+.CodeMirror-scrollbar-filler {\r
+  right: 0; bottom: 0;\r
+}\r
+.CodeMirror-gutter-filler {\r
+  left: 0; bottom: 0;\r
+}\r
+\r
+.CodeMirror-gutters {\r
+  position: absolute; left: 0; top: 0;\r
+  z-index: 3;\r
+}\r
+.CodeMirror-gutter {\r
+  white-space: normal;\r
+  height: 100%;\r
+  display: inline-block;\r
+  margin-bottom: -30px;\r
+  /* Hack to make IE7 behave */\r
+  *zoom:1;\r
+  *display:inline;\r
+}\r
+.CodeMirror-gutter-wrapper {\r
+  position: absolute;\r
+  z-index: 4;\r
+  height: 100%;\r
+}\r
+.CodeMirror-gutter-elt {\r
+  position: absolute;\r
+  cursor: default;\r
+  z-index: 4;\r
+}\r
+.CodeMirror-gutter-wrapper {\r
+  -webkit-user-select: none;\r
+  -moz-user-select: none;\r
+  user-select: none;\r
+}\r
+\r
+.CodeMirror-lines {\r
+  cursor: text;\r
+  min-height: 1px; /* prevents collapsing before first draw */\r
+}\r
+.CodeMirror pre {\r
+  /* Reset some styles that the rest of the page might have set */\r
+  -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;\r
+  border-width: 0;\r
+  background: transparent;\r
+  font-family: inherit;\r
+  font-size: inherit;\r
+  margin: 0;\r
+  white-space: pre;\r
+  word-wrap: normal;\r
+  line-height: inherit;\r
+  color: inherit;\r
+  z-index: 2;\r
+  position: relative;\r
+  overflow: visible;\r
+  -webkit-tap-highlight-color: transparent;\r
+}\r
+.CodeMirror-wrap pre {\r
+  word-wrap: break-word;\r
+  white-space: pre-wrap;\r
+  word-break: normal;\r
+}\r
+\r
+.CodeMirror-linebackground {\r
+  position: absolute;\r
+  left: 0; right: 0; top: 0; bottom: 0;\r
+  z-index: 0;\r
+}\r
+\r
+.CodeMirror-linewidget {\r
+  position: relative;\r
+  z-index: 2;\r
+  overflow: auto;\r
+}\r
+\r
+.CodeMirror-widget {}\r
+\r
+.CodeMirror-code {\r
+  outline: none;\r
+}\r
+\r
+/* Force content-box sizing for the elements where we expect it */\r
+.CodeMirror-scroll,\r
+.CodeMirror-sizer,\r
+.CodeMirror-gutter,\r
+.CodeMirror-gutters,\r
+.CodeMirror-linenumber {\r
+  -moz-box-sizing: content-box;\r
+  box-sizing: content-box;\r
+}\r
+\r
+.CodeMirror-measure {\r
+  position: absolute;\r
+  width: 100%;\r
+  height: 0;\r
+  overflow: hidden;\r
+  visibility: hidden;\r
+}\r
+.CodeMirror-measure pre { position: static; }\r
+\r
+.CodeMirror div.CodeMirror-cursor {\r
+  position: absolute;\r
+  border-right: none;\r
+  width: 0;\r
+}\r
+\r
+div.CodeMirror-cursors {\r
+  visibility: hidden;\r
+  position: relative;\r
+  z-index: 3;\r
+}\r
+.CodeMirror-focused div.CodeMirror-cursors {\r
+  visibility: visible;\r
+}\r
+\r
+.CodeMirror-selected { background: #d9d9d9; }\r
+.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }\r
+.CodeMirror-crosshair { cursor: crosshair; }\r
+.CodeMirror ::selection { background: #d7d4f0; }\r
+.CodeMirror ::-moz-selection { background: #d7d4f0; }\r
+\r
+.cm-searching {\r
+  background: #ffa;\r
+  background: rgba(255, 255, 0, .4);\r
+}\r
+\r
+/* IE7 hack to prevent it from returning funny offsetTops on the spans */\r
+.CodeMirror span { *vertical-align: text-bottom; }\r
+\r
+/* Used to force a border model for a node */\r
+.cm-force-border { padding-right: .1px; }\r
+\r
+@media print {\r
+  /* Hide the cursor when printing */\r
+  .CodeMirror div.CodeMirror-cursors {\r
+    visibility: hidden;\r
+  }\r
+}\r
+\r
+/* See issue #2901 */\r
+.cm-tab-wrap-hack:after { content: ''; }\r
+\r
+/* Help users use markselection to safely style text background */\r
+span.CodeMirror-selectedtext { background: none; }\r
diff --git a/js/ckeditor/samples/toolbarconfigurator/lib/codemirror/codemirror.js b/js/ckeditor/samples/toolbarconfigurator/lib/codemirror/codemirror.js
new file mode 100644 (file)
index 0000000..538493f
--- /dev/null
@@ -0,0 +1,288 @@
+(function(q){if("object"==typeof exports&&"object"==typeof module)module.exports=q();else{if("function"==typeof define&&define.amd)return define([],q);this.CodeMirror=q()}})(function(){function q(a,b){if(!(this instanceof q))return new q(a,b);this.options=b=b?V(b):{};V(qf,b,!1);wc(b);var c=b.value;"string"==typeof c&&(c=new P(c,b.mode));this.doc=c;var d=new q.inputStyles[b.inputStyle](this),d=this.display=new rf(a,c,d);d.wrapper.CodeMirror=this;Ad(this);Bd(this);b.lineWrapping&&(this.display.wrapper.className+=
+" CodeMirror-wrap");b.autofocus&&!ab&&d.input.focus();Cd(this);this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,delayingBlurEvent:!1,focused:!1,suppressEdits:!1,pasteIncoming:!1,cutIncoming:!1,draggingText:!1,highlight:new bb,keySeq:null,specialChars:null};var e=this;B&&11>C&&setTimeout(function(){e.display.input.reset(!0)},20);sf(this);Dd||(tf(),Dd=!0);Ja(this);this.curOp.forceUpdate=!0;Ed(this,c);b.autofocus&&!ab||e.hasFocus()?setTimeout(cb(xc,this),20):db(this);for(var f in Ka)if(Ka.hasOwnProperty(f))Ka[f](this,
+b[f],Fd);Gd(this);b.finishInit&&b.finishInit(this);for(c=0;c<yc.length;++c)yc[c](this);La(this);J&&b.lineWrapping&&"optimizelegibility"==getComputedStyle(d.lineDiv).textRendering&&(d.lineDiv.style.textRendering="auto")}function rf(a,b,c){this.input=c;this.scrollbarFiller=t("div",null,"CodeMirror-scrollbar-filler");this.scrollbarFiller.setAttribute("cm-not-content","true");this.gutterFiller=t("div",null,"CodeMirror-gutter-filler");this.gutterFiller.setAttribute("cm-not-content","true");this.lineDiv=
+t("div",null,"CodeMirror-code");this.selectionDiv=t("div",null,null,"position: relative; z-index: 1");this.cursorDiv=t("div",null,"CodeMirror-cursors");this.measure=t("div",null,"CodeMirror-measure");this.lineMeasure=t("div",null,"CodeMirror-measure");this.lineSpace=t("div",[this.measure,this.lineMeasure,this.selectionDiv,this.cursorDiv,this.lineDiv],null,"position: relative; outline: none");this.mover=t("div",[t("div",[this.lineSpace],"CodeMirror-lines")],null,"position: relative");this.sizer=t("div",
+[this.mover],"CodeMirror-sizer");this.sizerWidth=null;this.heightForcer=t("div",null,null,"position: absolute; height: "+Hd+"px; width: 1px;");this.gutters=t("div",null,"CodeMirror-gutters");this.lineGutter=null;this.scroller=t("div",[this.sizer,this.heightForcer,this.gutters],"CodeMirror-scroll");this.scroller.setAttribute("tabIndex","-1");this.wrapper=t("div",[this.scrollbarFiller,this.gutterFiller,this.scroller],"CodeMirror");B&&8>C&&(this.gutters.style.zIndex=-1,this.scroller.style.paddingRight=
+0);J||wa&&ab||(this.scroller.draggable=!0);a&&(a.appendChild?a.appendChild(this.wrapper):a(this.wrapper));this.reportedViewFrom=this.reportedViewTo=this.viewFrom=this.viewTo=b.first;this.view=[];this.externalMeasured=this.renderedView=null;this.lastWrapHeight=this.lastWrapWidth=this.viewOffset=0;this.updateLineNumbers=null;this.nativeBarWidth=this.barHeight=this.barWidth=0;this.scrollbarsClipped=!1;this.lineNumWidth=this.lineNumInnerWidth=this.lineNumChars=null;this.alignWidgets=!1;this.maxLine=this.cachedCharWidth=
+this.cachedTextHeight=this.cachedPaddingH=null;this.maxLineLength=0;this.maxLineChanged=!1;this.wheelDX=this.wheelDY=this.wheelStartX=this.wheelStartY=null;this.shift=!1;this.activeTouch=this.selForContextMenu=null;c.init(this)}function zc(a){a.doc.mode=q.getMode(a.options,a.doc.modeOption);eb(a)}function eb(a){a.doc.iter(function(a){a.stateAfter&&(a.stateAfter=null);a.styles&&(a.styles=null)});a.doc.frontier=a.doc.first;fb(a,100);a.state.modeGen++;a.curOp&&Q(a)}function Id(a){var b=xa(a.display),
+c=a.options.lineWrapping,d=c&&Math.max(5,a.display.scroller.clientWidth/gb(a.display)-3);return function(e){if(ya(a.doc,e))return 0;var f=0;if(e.widgets)for(var g=0;g<e.widgets.length;g++)e.widgets[g].height&&(f+=e.widgets[g].height);return c?f+(Math.ceil(e.text.length/d)||1)*b:f+b}}function Ac(a){var b=a.doc,c=Id(a);b.iter(function(a){var b=c(a);b!=a.height&&ca(a,b)})}function Bd(a){a.display.wrapper.className=a.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+a.options.theme.replace(/(^|\s)\s*/g,
+" cm-s-");hb(a)}function ib(a){Ad(a);Q(a);setTimeout(function(){Bc(a)},20)}function Ad(a){var b=a.display.gutters,c=a.options.gutters;za(b);for(var d=0;d<c.length;++d){var e=c[d],f=b.appendChild(t("div",null,"CodeMirror-gutter "+e));"CodeMirror-linenumbers"==e&&(a.display.lineGutter=f,f.style.width=(a.display.lineNumWidth||1)+"px")}b.style.display=d?"":"none";Cc(a)}function Cc(a){a.display.sizer.style.marginLeft=a.display.gutters.offsetWidth+"px"}function Kb(a){if(0==a.height)return 0;for(var b=a.text.length,
+c,d=a;c=Aa(d,!0);)c=c.find(0,!0),d=c.from.line,b+=c.from.ch-c.to.ch;for(d=a;c=Aa(d,!1);)c=c.find(0,!0),b-=d.text.length-c.from.ch,d=c.to.line,b+=d.text.length-c.to.ch;return b}function Dc(a){var b=a.display;a=a.doc;b.maxLine=u(a,a.first);b.maxLineLength=Kb(b.maxLine);b.maxLineChanged=!0;a.iter(function(a){var d=Kb(a);d>b.maxLineLength&&(b.maxLineLength=d,b.maxLine=a)})}function wc(a){var b=D(a.gutters,"CodeMirror-linenumbers");-1==b&&a.lineNumbers?a.gutters=a.gutters.concat(["CodeMirror-linenumbers"]):
+-1<b&&!a.lineNumbers&&(a.gutters=a.gutters.slice(0),a.gutters.splice(b,1))}function jb(a){var b=a.display,c=b.gutters.offsetWidth,d=Math.round(a.doc.height+Ec(a.display));return{clientHeight:b.scroller.clientHeight,viewHeight:b.wrapper.clientHeight,scrollWidth:b.scroller.scrollWidth,clientWidth:b.scroller.clientWidth,viewWidth:b.wrapper.clientWidth,barLeft:a.options.fixedGutter?c:0,docHeight:d,scrollHeight:d+da(a)+b.barHeight,nativeBarWidth:b.nativeBarWidth,gutterWidth:c}}function Fc(a,b,c){this.cm=
+c;var d=this.vert=t("div",[t("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),e=this.horiz=t("div",[t("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");a(d);a(e);v(d,"scroll",function(){d.clientHeight&&b(d.scrollTop,"vertical")});v(e,"scroll",function(){e.clientWidth&&b(e.scrollLeft,"horizontal")});this.checkedOverlay=!1;B&&8>C&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")}function Gc(){}function Cd(a){a.display.scrollbars&&(a.display.scrollbars.clear(),
+a.display.scrollbars.addClass&&kb(a.display.wrapper,a.display.scrollbars.addClass));a.display.scrollbars=new q.scrollbarModel[a.options.scrollbarStyle](function(b){a.display.wrapper.insertBefore(b,a.display.scrollbarFiller);v(b,"mousedown",function(){a.state.focused&&setTimeout(function(){a.display.input.focus()},0)});b.setAttribute("cm-not-content","true")},function(b,c){"horizontal"==c?Ma(a,b):lb(a,b)},a);a.display.scrollbars.addClass&&mb(a.display.wrapper,a.display.scrollbars.addClass)}function Na(a,
+b){b||(b=jb(a));var c=a.display.barWidth,d=a.display.barHeight;Jd(a,b);for(var e=0;4>e&&c!=a.display.barWidth||d!=a.display.barHeight;e++)c!=a.display.barWidth&&a.options.lineWrapping&&Lb(a),Jd(a,jb(a)),c=a.display.barWidth,d=a.display.barHeight}function Jd(a,b){var c=a.display,d=c.scrollbars.update(b);c.sizer.style.paddingRight=(c.barWidth=d.right)+"px";c.sizer.style.paddingBottom=(c.barHeight=d.bottom)+"px";d.right&&d.bottom?(c.scrollbarFiller.style.display="block",c.scrollbarFiller.style.height=
+d.bottom+"px",c.scrollbarFiller.style.width=d.right+"px"):c.scrollbarFiller.style.display="";d.bottom&&a.options.coverGutterNextToScrollbar&&a.options.fixedGutter?(c.gutterFiller.style.display="block",c.gutterFiller.style.height=d.bottom+"px",c.gutterFiller.style.width=b.gutterWidth+"px"):c.gutterFiller.style.display=""}function Hc(a,b,c){var d=c&&null!=c.top?Math.max(0,c.top):a.scroller.scrollTop,d=Math.floor(d-a.lineSpace.offsetTop),e=c&&null!=c.bottom?c.bottom:d+a.wrapper.clientHeight,d=Ba(b,d),
+e=Ba(b,e);if(c&&c.ensure){var f=c.ensure.from.line;c=c.ensure.to.line;f<d?(d=f,e=Ba(b,ea(u(b,f))+a.wrapper.clientHeight)):Math.min(c,b.lastLine())>=e&&(d=Ba(b,ea(u(b,c))-a.wrapper.clientHeight),e=c)}return{from:d,to:Math.max(e,d+1)}}function Bc(a){var b=a.display,c=b.view;if(b.alignWidgets||b.gutters.firstChild&&a.options.fixedGutter){for(var d=Ic(b)-b.scroller.scrollLeft+a.doc.scrollLeft,e=b.gutters.offsetWidth,f=d+"px",g=0;g<c.length;g++)if(!c[g].hidden){a.options.fixedGutter&&c[g].gutter&&(c[g].gutter.style.left=
+f);var h=c[g].alignable;if(h)for(var k=0;k<h.length;k++)h[k].style.left=f}a.options.fixedGutter&&(b.gutters.style.left=d+e+"px")}}function Gd(a){if(!a.options.lineNumbers)return!1;var b=a.doc,b=Jc(a.options,b.first+b.size-1),c=a.display;if(b.length!=c.lineNumChars){var d=c.measure.appendChild(t("div",[t("div",b)],"CodeMirror-linenumber CodeMirror-gutter-elt")),e=d.firstChild.offsetWidth,d=d.offsetWidth-e;c.lineGutter.style.width="";c.lineNumInnerWidth=Math.max(e,c.lineGutter.offsetWidth-d)+1;c.lineNumWidth=
+c.lineNumInnerWidth+d;c.lineNumChars=c.lineNumInnerWidth?b.length:-1;c.lineGutter.style.width=c.lineNumWidth+"px";Cc(a);return!0}return!1}function Jc(a,b){return String(a.lineNumberFormatter(b+a.firstLineNumber))}function Ic(a){return a.scroller.getBoundingClientRect().left-a.sizer.getBoundingClientRect().left}function Mb(a,b,c){var d=a.display;this.viewport=b;this.visible=Hc(d,a.doc,b);this.editorIsHidden=!d.wrapper.offsetWidth;this.wrapperHeight=d.wrapper.clientHeight;this.wrapperWidth=d.wrapper.clientWidth;
+this.oldDisplayWidth=pa(a);this.force=c;this.dims=Kc(a);this.events=[]}function Lc(a,b){var c=a.display,d=a.doc;if(b.editorIsHidden)return qa(a),!1;if(!b.force&&b.visible.from>=c.viewFrom&&b.visible.to<=c.viewTo&&(null==c.updateLineNumbers||c.updateLineNumbers>=c.viewTo)&&c.renderedView==c.view&&0==Kd(a))return!1;Gd(a)&&(qa(a),b.dims=Kc(a));var e=d.first+d.size,f=Math.max(b.visible.from-a.options.viewportMargin,d.first),g=Math.min(e,b.visible.to+a.options.viewportMargin);c.viewFrom<f&&20>f-c.viewFrom&&
+(f=Math.max(d.first,c.viewFrom));c.viewTo>g&&20>c.viewTo-g&&(g=Math.min(e,c.viewTo));ra&&(f=Mc(a.doc,f),g=Ld(a.doc,g));d=f!=c.viewFrom||g!=c.viewTo||c.lastWrapHeight!=b.wrapperHeight||c.lastWrapWidth!=b.wrapperWidth;e=a.display;0==e.view.length||f>=e.viewTo||g<=e.viewFrom?(e.view=Nb(a,f,g),e.viewFrom=f):(e.viewFrom>f?e.view=Nb(a,f,e.viewFrom).concat(e.view):e.viewFrom<f&&(e.view=e.view.slice(Ca(a,f))),e.viewFrom=f,e.viewTo<g?e.view=e.view.concat(Nb(a,e.viewTo,g)):e.viewTo>g&&(e.view=e.view.slice(0,
+Ca(a,g))));e.viewTo=g;c.viewOffset=ea(u(a.doc,c.viewFrom));a.display.mover.style.top=c.viewOffset+"px";g=Kd(a);if(!d&&0==g&&!b.force&&c.renderedView==c.view&&(null==c.updateLineNumbers||c.updateLineNumbers>=c.viewTo))return!1;f=fa();4<g&&(c.lineDiv.style.display="none");uf(a,c.updateLineNumbers,b.dims);4<g&&(c.lineDiv.style.display="");c.renderedView=c.view;f&&fa()!=f&&f.offsetHeight&&f.focus();za(c.cursorDiv);za(c.selectionDiv);c.gutters.style.height=0;d&&(c.lastWrapHeight=b.wrapperHeight,c.lastWrapWidth=
+b.wrapperWidth,fb(a,400));c.updateLineNumbers=null;return!0}function Md(a,b){for(var c=b.viewport,d=!0;;d=!1){if(!d||!a.options.lineWrapping||b.oldDisplayWidth==pa(a))if(c&&null!=c.top&&(c={top:Math.min(a.doc.height+Ec(a.display)-Nc(a),c.top)}),b.visible=Hc(a.display,a.doc,c),b.visible.from>=a.display.viewFrom&&b.visible.to<=a.display.viewTo)break;if(!Lc(a,b))break;Lb(a);d=jb(a);nb(a);Oc(a,d);Na(a,d)}b.signal(a,"update",a);if(a.display.viewFrom!=a.display.reportedViewFrom||a.display.viewTo!=a.display.reportedViewTo)b.signal(a,
+"viewportChange",a,a.display.viewFrom,a.display.viewTo),a.display.reportedViewFrom=a.display.viewFrom,a.display.reportedViewTo=a.display.viewTo}function Pc(a,b){var c=new Mb(a,b);if(Lc(a,c)){Lb(a);Md(a,c);var d=jb(a);nb(a);Oc(a,d);Na(a,d);c.finish()}}function Oc(a,b){a.display.sizer.style.minHeight=b.docHeight+"px";var c=b.docHeight+a.display.barHeight;a.display.heightForcer.style.top=c+"px";a.display.gutters.style.height=Math.max(c+da(a),b.clientHeight)+"px"}function Lb(a){a=a.display;for(var b=
+a.lineDiv.offsetTop,c=0;c<a.view.length;c++){var d=a.view[c],e;if(!d.hidden){if(B&&8>C){var f=d.node.offsetTop+d.node.offsetHeight;e=f-b;b=f}else e=d.node.getBoundingClientRect(),e=e.bottom-e.top;f=d.line.height-e;2>e&&(e=xa(a));if(.001<f||-.001>f)if(ca(d.line,e),Nd(d.line),d.rest)for(e=0;e<d.rest.length;e++)Nd(d.rest[e])}}}function Nd(a){if(a.widgets)for(var b=0;b<a.widgets.length;++b)a.widgets[b].height=a.widgets[b].node.offsetHeight}function Kc(a){for(var b=a.display,c={},d={},e=b.gutters.clientLeft,
+f=b.gutters.firstChild,g=0;f;f=f.nextSibling,++g)c[a.options.gutters[g]]=f.offsetLeft+f.clientLeft+e,d[a.options.gutters[g]]=f.clientWidth;return{fixedPos:Ic(b),gutterTotalWidth:b.gutters.offsetWidth,gutterLeft:c,gutterWidth:d,wrapperWidth:b.wrapper.clientWidth}}function uf(a,b,c){function d(b){var c=b.nextSibling;J&&W&&a.display.currentWheelTarget==b?b.style.display="none":b.parentNode.removeChild(b);return c}for(var e=a.display,f=a.options.lineNumbers,g=e.lineDiv,h=g.firstChild,k=e.view,e=e.viewFrom,
+l=0;l<k.length;l++){var m=k[l];if(!m.hidden)if(m.node&&m.node.parentNode==g){for(;h!=m.node;)h=d(h);h=f&&null!=b&&b<=e&&m.lineNumber;m.changes&&(-1<D(m.changes,"gutter")&&(h=!1),Od(a,m,e,c));h&&(za(m.lineNumber),m.lineNumber.appendChild(document.createTextNode(Jc(a.options,e))));h=m.node.nextSibling}else{var p=vf(a,m,e,c);g.insertBefore(p,h)}e+=m.size}for(;h;)h=d(h)}function Od(a,b,c,d){for(var e=0;e<b.changes.length;e++){var f=b.changes[e];if("text"==f){var f=b,g=f.text.className,h=Pd(a,f);f.text==
+f.node&&(f.node=h.pre);f.text.parentNode.replaceChild(h.pre,f.text);f.text=h.pre;h.bgClass!=f.bgClass||h.textClass!=f.textClass?(f.bgClass=h.bgClass,f.textClass=h.textClass,Qc(f)):g&&(f.text.className=g)}else if("gutter"==f)Qd(a,b,c,d);else if("class"==f)Qc(b);else if("widget"==f){f=a;g=b;h=d;g.alignable&&(g.alignable=null);for(var k=g.node.firstChild,l=void 0;k;k=l)l=k.nextSibling,"CodeMirror-linewidget"==k.className&&g.node.removeChild(k);Rd(f,g,h)}}b.changes=null}function Ob(a){a.node==a.text&&
+(a.node=t("div",null,null,"position: relative"),a.text.parentNode&&a.text.parentNode.replaceChild(a.node,a.text),a.node.appendChild(a.text),B&&8>C&&(a.node.style.zIndex=2));return a.node}function Pd(a,b){var c=a.display.externalMeasured;return c&&c.line==b.line?(a.display.externalMeasured=null,b.measure=c.measure,c.built):Sd(a,b)}function Qc(a){var b=a.bgClass?a.bgClass+" "+(a.line.bgClass||""):a.line.bgClass;b&&(b+=" CodeMirror-linebackground");if(a.background)b?a.background.className=b:(a.background.parentNode.removeChild(a.background),
+a.background=null);else if(b){var c=Ob(a);a.background=c.insertBefore(t("div",null,b),c.firstChild)}a.line.wrapClass?Ob(a).className=a.line.wrapClass:a.node!=a.text&&(a.node.className="");a.text.className=(a.textClass?a.textClass+" "+(a.line.textClass||""):a.line.textClass)||""}function Qd(a,b,c,d){b.gutter&&(b.node.removeChild(b.gutter),b.gutter=null);var e=b.line.gutterMarkers;if(a.options.lineNumbers||e){var f=Ob(b),g=b.gutter=t("div",null,"CodeMirror-gutter-wrapper","left: "+(a.options.fixedGutter?
+d.fixedPos:-d.gutterTotalWidth)+"px; width: "+d.gutterTotalWidth+"px");a.display.input.setUneditable(g);f.insertBefore(g,b.text);b.line.gutterClass&&(g.className+=" "+b.line.gutterClass);!a.options.lineNumbers||e&&e["CodeMirror-linenumbers"]||(b.lineNumber=g.appendChild(t("div",Jc(a.options,c),"CodeMirror-linenumber CodeMirror-gutter-elt","left: "+d.gutterLeft["CodeMirror-linenumbers"]+"px; width: "+a.display.lineNumInnerWidth+"px")));if(e)for(b=0;b<a.options.gutters.length;++b)c=a.options.gutters[b],
+(f=e.hasOwnProperty(c)&&e[c])&&g.appendChild(t("div",[f],"CodeMirror-gutter-elt","left: "+d.gutterLeft[c]+"px; width: "+d.gutterWidth[c]+"px"))}}function vf(a,b,c,d){var e=Pd(a,b);b.text=b.node=e.pre;e.bgClass&&(b.bgClass=e.bgClass);e.textClass&&(b.textClass=e.textClass);Qc(b);Qd(a,b,c,d);Rd(a,b,d);return b.node}function Rd(a,b,c){Td(a,b.line,b,c,!0);if(b.rest)for(var d=0;d<b.rest.length;d++)Td(a,b.rest[d],b,c,!1)}function Td(a,b,c,d,e){if(b.widgets){var f=Ob(c),g=0;for(b=b.widgets;g<b.length;++g){var h=
+b[g],k=t("div",[h.node],"CodeMirror-linewidget");h.handleMouseEvents||k.setAttribute("cm-ignore-events","true");var l=h,m=k,p=d;if(l.noHScroll){(c.alignable||(c.alignable=[])).push(m);var n=p.wrapperWidth;m.style.left=p.fixedPos+"px";l.coverGutter||(n-=p.gutterTotalWidth,m.style.paddingLeft=p.gutterTotalWidth+"px");m.style.width=n+"px"}l.coverGutter&&(m.style.zIndex=5,m.style.position="relative",l.noHScroll||(m.style.marginLeft=-p.gutterTotalWidth+"px"));a.display.input.setUneditable(k);e&&h.above?
+f.insertBefore(k,c.gutter||c.text):f.appendChild(k);L(h,"redraw")}}}function Rc(a){return r(a.line,a.ch)}function Pb(a,b){return 0>y(a,b)?b:a}function Qb(a,b){return 0>y(a,b)?a:b}function Ud(a){a.state.focused||(a.display.input.focus(),xc(a))}function Rb(a){return a.options.readOnly||a.doc.cantEdit}function Sc(a,b,c,d,e){var f=a.doc;a.display.shift=!1;d||(d=f.sel);var g=sa(b),h=null;a.state.pasteIncoming&&1<d.ranges.length&&(X&&X.join("\n")==b?h=0==d.ranges.length%X.length&&ob(X,sa):g.length==d.ranges.length&&
+(h=ob(g,function(a){return[a]})));for(var k=d.ranges.length-1;0<=k;k--){var l=d.ranges[k],m=l.from(),p=l.to();l.empty()&&(c&&0<c?m=r(m.line,m.ch-c):a.state.overwrite&&!a.state.pasteIncoming&&(p=r(p.line,Math.min(u(f,p.line).text.length,p.ch+A(g).length))));var n=a.curOp.updateInput,m={from:m,to:p,text:h?h[k%h.length]:g,origin:e||(a.state.pasteIncoming?"paste":a.state.cutIncoming?"cut":"+input")};Oa(a.doc,m);L(a,"inputRead",a,m);if(b&&!a.state.pasteIncoming&&a.options.electricChars&&a.options.smartIndent&&
+100>l.head.ch&&(!k||d.ranges[k-1].head.line!=l.head.line)){l=a.getModeAt(l.head);m=ta(m);p=!1;if(l.electricChars)for(var E=0;E<l.electricChars.length;E++){if(-1<b.indexOf(l.electricChars.charAt(E))){p=pb(a,m.line,"smart");break}}else l.electricInput&&l.electricInput.test(u(f,m.line).text.slice(0,m.ch))&&(p=pb(a,m.line,"smart"));p&&L(a,"electricInput",a,m.line)}}Pa(a);a.curOp.updateInput=n;a.curOp.typing=!0;a.state.pasteIncoming=a.state.cutIncoming=!1}function Vd(a){for(var b=[],c=[],d=0;d<a.doc.sel.ranges.length;d++){var e=
+a.doc.sel.ranges[d].head.line,e={anchor:r(e,0),head:r(e+1,0)};c.push(e);b.push(a.getRange(e.anchor,e.head))}return{text:b,ranges:c}}function Wd(a){a.setAttribute("autocorrect","off");a.setAttribute("autocapitalize","off");a.setAttribute("spellcheck","false")}function Tc(a){this.cm=a;this.prevInput="";this.pollingFast=!1;this.polling=new bb;this.hasSelection=this.inaccurateSelection=!1;this.composing=null}function Xd(){var a=t("textarea",null,null,"position: absolute; padding: 0; width: 1px; height: 1em; outline: none"),
+b=t("div",[a],null,"overflow: hidden; position: relative; width: 3px; height: 0px;");J?a.style.width="1000px":a.setAttribute("wrap","off");Qa&&(a.style.border="1px solid black");Wd(a);return b}function Uc(a){this.cm=a;this.lastAnchorNode=this.lastAnchorOffset=this.lastFocusNode=this.lastFocusOffset=null;this.polling=new bb;this.gracePeriod=!1}function Yd(a,b){var c=Vc(a,b.line);if(!c||c.hidden)return null;var d=u(a.doc,b.line),c=Zd(c,d,b.line);(d=Y(d))&&Sb(d,b.ch);d=$d(c.map,b.ch,"left");d.offset=
+"right"==d.collapse?d.end:d.start;return d}function Ra(a,b){b&&(a.bad=!0);return a}function Tb(a,b,c){var d;if(b==a.display.lineDiv){d=a.display.lineDiv.childNodes[c];if(!d)return Ra(a.clipPos(r(a.display.viewTo-1)),!0);b=null;c=0}else for(d=b;;d=d.parentNode){if(!d||d==a.display.lineDiv)return null;if(d.parentNode&&d.parentNode==a.display.lineDiv)break}for(var e=0;e<a.display.view.length;e++){var f=a.display.view[e];if(f.node==d)return wf(f,b,c)}}function wf(a,b,c){function d(b,c,d){for(var e=-1;e<
+(l?l.length:0);e++)for(var f=0>e?k.map:l[e],g=0;g<f.length;g+=3){var h=f[g+2];if(h==b||h==c){c=F(0>e?a.line:a.rest[e]);e=f[g]+d;if(0>d||h!=b)e=f[g+(d?1:0)];return r(c,e)}}}var e=a.text.firstChild,f=!1;if(!b||!Wc(e,b))return Ra(r(F(a.line),0),!0);if(b==e&&(f=!0,b=e.childNodes[c],c=0,!b))return c=a.rest?A(a.rest):a.line,Ra(r(F(c),c.text.length),f);var g=3==b.nodeType?b:null,h=b;g||1!=b.childNodes.length||3!=b.firstChild.nodeType||(g=b.firstChild,c&&(c=g.nodeValue.length));for(;h.parentNode!=e;)h=h.parentNode;
+var k=a.measure,l=k.maps;if(b=d(g,h,c))return Ra(b,f);e=h.nextSibling;for(g=g?g.nodeValue.length-c:0;e;e=e.nextSibling){if(b=d(e,e.firstChild,0))return Ra(r(b.line,b.ch-g),f);g+=e.textContent.length}h=h.previousSibling;for(g=c;h;h=h.previousSibling){if(b=d(h,h.firstChild,-1))return Ra(r(b.line,b.ch+g),f);g+=e.textContent.length}}function xf(a,b,c,d,e){function f(a){return function(b){return b.id==a}}function g(b){if(1==b.nodeType){var c=b.getAttribute("cm-text");if(null!=c)""==c&&(c=b.textContent.replace(/\u200b/g,
+"")),h+=c;else{var c=b.getAttribute("cm-marker"),p;if(c)b=a.findMarks(r(d,0),r(e+1,0),f(+c)),b.length&&(p=b[0].find())&&(h+=Da(a.doc,p.from,p.to).join("\n"));else if("false"!=b.getAttribute("contenteditable")){for(p=0;p<b.childNodes.length;p++)g(b.childNodes[p]);/^(pre|div|p)$/i.test(b.nodeName)&&(k=!0)}}}else 3==b.nodeType&&(b=b.nodeValue)&&(k&&(h+="\n",k=!1),h+=b)}for(var h="",k=!1;;){g(b);if(b==c)break;b=b.nextSibling}return h}function la(a,b){this.ranges=a;this.primIndex=b}function z(a,b){this.anchor=
+a;this.head=b}function Z(a,b){var c=a[b];a.sort(function(a,b){return y(a.from(),b.from())});b=D(a,c);for(c=1;c<a.length;c++){var d=a[c],e=a[c-1];if(0<=y(e.to(),d.from())){var f=Qb(e.from(),d.from()),g=Pb(e.to(),d.to()),d=e.empty()?d.from()==d.head:e.from()==e.head;c<=b&&--b;a.splice(--c,2,new z(d?g:f,d?f:g))}}return new la(a,b)}function ga(a,b){return new la([new z(a,b||a)],0)}function w(a,b){if(b.line<a.first)return r(a.first,0);var c=a.first+a.size-1;if(b.line>c)return r(c,u(a,c).text.length);var c=
+u(a,b.line).text.length,d=b.ch,c=null==d||d>c?r(b.line,c):0>d?r(b.line,0):b;return c}function qb(a,b){return b>=a.first&&b<a.first+a.size}function rb(a,b,c,d){return a.cm&&a.cm.display.shift||a.extend?(a=b.anchor,d&&(b=0>y(c,a),b!=0>y(d,a)?(a=c,c=d):b!=0>y(c,d)&&(c=d)),new z(a,c)):new z(d||c,c)}function Ub(a,b,c,d){H(a,new la([rb(a,a.sel.primary(),b,c)],0),d)}function ae(a,b,c){for(var d=[],e=0;e<a.sel.ranges.length;e++)d[e]=rb(a,a.sel.ranges[e],b[e],null);b=Z(d,a.sel.primIndex);H(a,b,c)}function Xc(a,
+b,c,d){var e=a.sel.ranges.slice(0);e[b]=c;H(a,Z(e,a.sel.primIndex),d)}function yf(a,b){var c={ranges:b.ranges,update:function(b){this.ranges=[];for(var c=0;c<b.length;c++)this.ranges[c]=new z(w(a,b[c].anchor),w(a,b[c].head))}};K(a,"beforeSelectionChange",a,c);a.cm&&K(a.cm,"beforeSelectionChange",a.cm,c);return c.ranges!=b.ranges?Z(c.ranges,c.ranges.length-1):b}function be(a,b,c){var d=a.history.done,e=A(d);e&&e.ranges?(d[d.length-1]=b,Vb(a,b,c)):H(a,b,c)}function H(a,b,c){Vb(a,b,c);b=a.sel;var d=
+a.cm?a.cm.curOp.id:NaN,e=a.history,f=c&&c.origin,g;if(!(g=d==e.lastSelOp)&&(g=f&&e.lastSelOrigin==f)&&!(g=e.lastModTime==e.lastSelTime&&e.lastOrigin==f)){g=A(e.done);var h=f.charAt(0);g="*"==h||"+"==h&&g.ranges.length==b.ranges.length&&g.somethingSelected()==b.somethingSelected()&&new Date-a.history.lastSelTime<=(a.cm?a.cm.options.historyEventDelay:500)}g?e.done[e.done.length-1]=b:Wb(b,e.done);e.lastSelTime=+new Date;e.lastSelOrigin=f;e.lastSelOp=d;c&&!1!==c.clearRedo&&ce(e.undone)}function Vb(a,
+b,c){if(S(a,"beforeSelectionChange")||a.cm&&S(a.cm,"beforeSelectionChange"))b=yf(a,b);var d=c&&c.bias||(0>y(b.primary().head,a.sel.primary().head)?-1:1);de(a,ee(a,b,d,!0));c&&!1===c.scroll||!a.cm||Pa(a.cm)}function de(a,b){b.equals(a.sel)||(a.sel=b,a.cm&&(a.cm.curOp.updateInput=a.cm.curOp.selectionChanged=!0,fe(a.cm)),L(a,"cursorActivity",a))}function ge(a){de(a,ee(a,a.sel,null,!1),ha)}function ee(a,b,c,d){for(var e,f=0;f<b.ranges.length;f++){var g=b.ranges[f],h=Xb(a,g.anchor,c,d),k=Xb(a,g.head,c,
+d);if(e||h!=g.anchor||k!=g.head)e||(e=b.ranges.slice(0,f)),e[f]=new z(h,k)}return e?Z(e,b.primIndex):b}function Xb(a,b,c,d){var e=!1,f=b,g=c||1;a.cantEdit=!1;a:for(;;){var h=u(a,f.line);if(h.markedSpans)for(var k=0;k<h.markedSpans.length;++k){var l=h.markedSpans[k],m=l.marker;if((null==l.from||(m.inclusiveLeft?l.from<=f.ch:l.from<f.ch))&&(null==l.to||(m.inclusiveRight?l.to>=f.ch:l.to>f.ch))){if(d&&(K(m,"beforeCursorEnter"),m.explicitlyCleared))if(h.markedSpans){--k;continue}else break;if(m.atomic){k=
+m.find(0>g?-1:1);if(0==y(k,f)&&(k.ch+=g,0>k.ch?k=k.line>a.first?w(a,r(k.line-1)):null:k.ch>h.text.length&&(k=k.line<a.first+a.size-1?r(k.line+1,0):null),!k)){if(e){if(!d)return Xb(a,b,c,!0);a.cantEdit=!0;return r(a.first,0)}e=!0;k=b;g=-g}f=k;continue a}}}return f}}function nb(a){a.display.input.showSelection(a.display.input.prepareSelection())}function he(a,b){for(var c=a.doc,d={},e=d.cursors=document.createDocumentFragment(),f=d.selection=document.createDocumentFragment(),g=0;g<c.sel.ranges.length;g++)if(!1!==
+b||g!=c.sel.primIndex){var h=c.sel.ranges[g],k=h.empty();if(k||a.options.showCursorWhenSelecting){var l=a,m=e,p=ma(l,h.head,"div",null,null,!l.options.singleCursorHeightPerLine),n=m.appendChild(t("div"," ","CodeMirror-cursor"));n.style.left=p.left+"px";n.style.top=p.top+"px";n.style.height=Math.max(0,p.bottom-p.top)*l.options.cursorHeight+"px";p.other&&(l=m.appendChild(t("div"," ","CodeMirror-cursor CodeMirror-secondarycursor")),l.style.display="",l.style.left=p.other.left+"px",l.style.top=p.other.top+
+"px",l.style.height=.85*(p.other.bottom-p.other.top)+"px")}k||zf(a,h,f)}return d}function zf(a,b,c){function d(a,b,c,d){0>b&&(b=0);b=Math.round(b);d=Math.round(d);h.appendChild(t("div",null,"CodeMirror-selected","position: absolute; left: "+a+"px; top: "+b+"px; width: "+(null==c?m-a:c)+"px; height: "+(d-b)+"px"))}function e(b,c,e){var f=u(g,b),h=f.text.length,k,p;Af(Y(f),c||0,null==e?h:e,function(g,q,t){var u=Yb(a,r(b,g),"div",f,"left"),v,w;g==q?(v=u,t=w=u.left):(v=Yb(a,r(b,q-1),"div",f,"right"),
+"rtl"==t&&(t=u,u=v,v=t),t=u.left,w=v.right);null==c&&0==g&&(t=l);3<v.top-u.top&&(d(t,u.top,null,u.bottom),t=l,u.bottom<v.top&&d(t,u.bottom,null,v.top));null==e&&q==h&&(w=m);if(!k||u.top<k.top||u.top==k.top&&u.left<k.left)k=u;if(!p||v.bottom>p.bottom||v.bottom==p.bottom&&v.right>p.right)p=v;t<l+1&&(t=l);d(t,v.top,w-t,v.bottom)});return{start:k,end:p}}var f=a.display,g=a.doc,h=document.createDocumentFragment(),k=ie(a.display),l=k.left,m=Math.max(f.sizerWidth,pa(a)-f.sizer.offsetLeft)-k.right,f=b.from();
+b=b.to();if(f.line==b.line)e(f.line,f.ch,b.ch);else{var p=u(g,f.line),k=u(g,b.line),k=ia(p)==ia(k),f=e(f.line,f.ch,k?p.text.length+1:null).end;b=e(b.line,k?0:null,b.ch).start;k&&(f.top<b.top-2?(d(f.right,f.top,null,f.bottom),d(l,b.top,b.left,b.bottom)):d(f.right,f.top,b.left-f.right,f.bottom));f.bottom<b.top&&d(l,f.bottom,null,b.top)}c.appendChild(h)}function Yc(a){if(a.state.focused){var b=a.display;clearInterval(b.blinker);var c=!0;b.cursorDiv.style.visibility="";0<a.options.cursorBlinkRate?b.blinker=
+setInterval(function(){b.cursorDiv.style.visibility=(c=!c)?"":"hidden"},a.options.cursorBlinkRate):0>a.options.cursorBlinkRate&&(b.cursorDiv.style.visibility="hidden")}}function fb(a,b){a.doc.mode.startState&&a.doc.frontier<a.display.viewTo&&a.state.highlight.set(b,cb(Bf,a))}function Bf(a){var b=a.doc;b.frontier<b.first&&(b.frontier=b.first);if(!(b.frontier>=a.display.viewTo)){var c=+new Date+a.options.workTime,d=Sa(b.mode,sb(a,b.frontier)),e=[];b.iter(b.frontier,Math.min(b.first+b.size,a.display.viewTo+
+500),function(f){if(b.frontier>=a.display.viewFrom){var g=f.styles,h=je(a,f,d,!0);f.styles=h.styles;var k=f.styleClasses;(h=h.classes)?f.styleClasses=h:k&&(f.styleClasses=null);k=!g||g.length!=f.styles.length||k!=h&&(!k||!h||k.bgClass!=h.bgClass||k.textClass!=h.textClass);for(h=0;!k&&h<g.length;++h)k=g[h]!=f.styles[h];k&&e.push(b.frontier);f.stateAfter=Sa(b.mode,d)}else Zc(a,f.text,d),f.stateAfter=0==b.frontier%5?Sa(b.mode,d):null;++b.frontier;if(+new Date>c)return fb(a,a.options.workDelay),!0});
+e.length&&T(a,function(){for(var b=0;b<e.length;b++)na(a,e[b],"text")})}}function Cf(a,b,c){for(var d,e,f=a.doc,g=c?-1:b-(a.doc.mode.innerMode?1E3:100);b>g;--b){if(b<=f.first)return f.first;var h=u(f,b-1);if(h.stateAfter&&(!c||b<=f.frontier))return b;h=aa(h.text,null,a.options.tabSize);if(null==e||d>h)e=b-1,d=h}return e}function sb(a,b,c){var d=a.doc,e=a.display;if(!d.mode.startState)return!0;var f=Cf(a,b,c),g=f>d.first&&u(d,f-1).stateAfter,g=g?Sa(d.mode,g):Df(d.mode);d.iter(f,b,function(c){Zc(a,
+c.text,g);c.stateAfter=f==b-1||0==f%5||f>=e.viewFrom&&f<e.viewTo?Sa(d.mode,g):null;++f});c&&(d.frontier=f);return g}function Ec(a){return a.mover.offsetHeight-a.lineSpace.offsetHeight}function ie(a){if(a.cachedPaddingH)return a.cachedPaddingH;var b=U(a.measure,t("pre","x")),b=window.getComputedStyle?window.getComputedStyle(b):b.currentStyle,b={left:parseInt(b.paddingLeft),right:parseInt(b.paddingRight)};isNaN(b.left)||isNaN(b.right)||(a.cachedPaddingH=b);return b}function da(a){return Hd-a.display.nativeBarWidth}
+function pa(a){return a.display.scroller.clientWidth-da(a)-a.display.barWidth}function Nc(a){return a.display.scroller.clientHeight-da(a)-a.display.barHeight}function Zd(a,b,c){if(a.line==b)return{map:a.measure.map,cache:a.measure.cache};for(var d=0;d<a.rest.length;d++)if(a.rest[d]==b)return{map:a.measure.maps[d],cache:a.measure.caches[d]};for(d=0;d<a.rest.length;d++)if(F(a.rest[d])>c)return{map:a.measure.maps[d],cache:a.measure.caches[d],before:!0}}function Vc(a,b){if(b>=a.display.viewFrom&&b<a.display.viewTo)return a.display.view[Ca(a,
+b)];var c=a.display.externalMeasured;if(c&&b>=c.lineN&&b<c.lineN+c.size)return c}function Zb(a,b){var c=F(b),d=Vc(a,c);d&&!d.text?d=null:d&&d.changes&&Od(a,d,c,Kc(a));if(!d){var e;e=ia(b);d=F(e);e=a.display.externalMeasured=new ke(a.doc,e,d);e.lineN=d;d=e.built=Sd(a,e);e.text=d.pre;U(a.display.lineMeasure,d.pre);d=e}c=Zd(d,b,c);return{line:b,view:d,rect:null,map:c.map,cache:c.cache,before:c.before,hasHeights:!1}}function $c(a,b,c,d,e){b.before&&(c=-1);var f=c+(d||"");if(b.cache.hasOwnProperty(f))a=
+b.cache[f];else{b.rect||(b.rect=b.view.text.getBoundingClientRect());if(!b.hasHeights){var g=b.view,h=b.rect,k=a.options.lineWrapping,l=k&&pa(a);if(!g.measure.heights||k&&g.measure.width!=l){var m=g.measure.heights=[];if(k)for(g.measure.width=l,g=g.text.firstChild.getClientRects(),k=0;k<g.length-1;k++){var l=g[k],p=g[k+1];2<Math.abs(l.bottom-p.bottom)&&m.push((l.bottom+p.top)/2-h.top)}m.push(h.bottom-h.top)}b.hasHeights=!0}g=d;k=$d(b.map,c,g);d=k.node;h=k.start;l=k.end;c=k.collapse;var n;if(3==d.nodeType){for(m=
+0;4>m;m++){for(;h&&tb(b.line.text.charAt(k.coverStart+h));)--h;for(;k.coverStart+l<k.coverEnd&&tb(b.line.text.charAt(k.coverStart+l));)++l;if(B&&9>C&&0==h&&l==k.coverEnd-k.coverStart)n=d.parentNode.getBoundingClientRect();else if(B&&a.options.lineWrapping){var E=Ea(d,h,l).getClientRects();n=E.length?E["right"==g?E.length-1:0]:ad}else n=Ea(d,h,l).getBoundingClientRect()||ad;if(n.left||n.right||0==h)break;l=h;--h;c="right"}B&&11>C&&((E=!window.screen||null==screen.logicalXDPI||screen.logicalXDPI==screen.deviceXDPI)||
+(null!=bd?E=bd:(m=U(a.display.measure,t("span","x")),E=m.getBoundingClientRect(),m=Ea(m,0,1).getBoundingClientRect(),E=bd=1<Math.abs(E.left-m.left)),E=!E),E||(E=screen.logicalXDPI/screen.deviceXDPI,m=screen.logicalYDPI/screen.deviceYDPI,n={left:n.left*E,right:n.right*E,top:n.top*m,bottom:n.bottom*m}))}else 0<h&&(c=g="right"),n=a.options.lineWrapping&&1<(E=d.getClientRects()).length?E["right"==g?E.length-1:0]:d.getBoundingClientRect();!(B&&9>C)||h||n&&(n.left||n.right)||(n=(n=d.parentNode.getClientRects()[0])?
+{left:n.left,right:n.left+gb(a.display),top:n.top,bottom:n.bottom}:ad);E=n.top-b.rect.top;d=n.bottom-b.rect.top;h=(E+d)/2;g=b.view.measure.heights;for(m=0;m<g.length-1&&!(h<g[m]);m++);c={left:("right"==c?n.right:n.left)-b.rect.left,right:("left"==c?n.left:n.right)-b.rect.left,top:m?g[m-1]:0,bottom:g[m]};n.left||n.right||(c.bogus=!0);a.options.singleCursorHeightPerLine||(c.rtop=E,c.rbottom=d);a=c;a.bogus||(b.cache[f]=a)}return{left:a.left,right:a.right,top:e?a.rtop:a.top,bottom:e?a.rbottom:a.bottom}}
+function $d(a,b,c){for(var d,e,f,g,h=0;h<a.length;h+=3){var k=a[h],l=a[h+1];if(b<k)e=0,f=1,g="left";else if(b<l)e=b-k,f=e+1;else if(h==a.length-3||b==l&&a[h+3]>b)f=l-k,e=f-1,b>=l&&(g="right");if(null!=e){d=a[h+2];k==l&&c==(d.insertLeft?"left":"right")&&(g=c);if("left"==c&&0==e)for(;h&&a[h-2]==a[h-3]&&a[h-1].insertLeft;)d=a[(h-=3)+2],g="left";if("right"==c&&e==l-k)for(;h<a.length-3&&a[h+3]==a[h+4]&&!a[h+5].insertLeft;)d=a[(h+=3)+2],g="right";break}}return{node:d,start:e,end:f,collapse:g,coverStart:k,
+coverEnd:l}}function le(a){if(a.measure&&(a.measure.cache={},a.measure.heights=null,a.rest))for(var b=0;b<a.rest.length;b++)a.measure.caches[b]={}}function me(a){a.display.externalMeasure=null;za(a.display.lineMeasure);for(var b=0;b<a.display.view.length;b++)le(a.display.view[b])}function hb(a){me(a);a.display.cachedCharWidth=a.display.cachedTextHeight=a.display.cachedPaddingH=null;a.options.lineWrapping||(a.display.maxLineChanged=!0);a.display.lineNumChars=null}function cd(a,b,c,d){if(b.widgets)for(var e=
+0;e<b.widgets.length;++e)if(b.widgets[e].above){var f=ub(b.widgets[e]);c.top+=f;c.bottom+=f}if("line"==d)return c;d||(d="local");b=ea(b);b="local"==d?b+a.display.lineSpace.offsetTop:b-a.display.viewOffset;if("page"==d||"window"==d)a=a.display.lineSpace.getBoundingClientRect(),b+=a.top+("window"==d?0:window.pageYOffset||(document.documentElement||document.body).scrollTop),d=a.left+("window"==d?0:window.pageXOffset||(document.documentElement||document.body).scrollLeft),c.left+=d,c.right+=d;c.top+=b;
+c.bottom+=b;return c}function ne(a,b,c){if("div"==c)return b;var d=b.left;b=b.top;"page"==c?(d-=window.pageXOffset||(document.documentElement||document.body).scrollLeft,b-=window.pageYOffset||(document.documentElement||document.body).scrollTop):"local"!=c&&c||(c=a.display.sizer.getBoundingClientRect(),d+=c.left,b+=c.top);a=a.display.lineSpace.getBoundingClientRect();return{left:d-a.left,top:b-a.top}}function Yb(a,b,c,d,e){d||(d=u(a.doc,b.line));var f=d;b=b.ch;d=$c(a,Zb(a,d),b,e);return cd(a,f,d,c)}
+function ma(a,b,c,d,e,f){function g(b,g){var h=$c(a,e,b,g?"right":"left",f);g?h.left=h.right:h.right=h.left;return cd(a,d,h,c)}function h(a,b){var c=k[b],d=c.level%2;a==dd(c)&&b&&c.level<k[b-1].level?(c=k[--b],a=ed(c)-(c.level%2?0:1),d=!0):a==ed(c)&&b<k.length-1&&c.level<k[b+1].level&&(c=k[++b],a=dd(c)-c.level%2,d=!1);return d&&a==c.to&&a>c.from?g(a-1):g(a,d)}d=d||u(a.doc,b.line);e||(e=Zb(a,d));var k=Y(d);b=b.ch;if(!k)return g(b);var l=Sb(k,b),l=h(b,l);null!=vb&&(l.other=h(b,vb));return l}function oe(a,
+b){var c=0;b=w(a.doc,b);a.options.lineWrapping||(c=gb(a.display)*b.ch);var d=u(a.doc,b.line),e=ea(d)+a.display.lineSpace.offsetTop;return{left:c,right:c,top:e,bottom:e+d.height}}function $b(a,b,c,d){a=r(a,b);a.xRel=d;c&&(a.outside=!0);return a}function fd(a,b,c){var d=a.doc;c+=a.display.viewOffset;if(0>c)return $b(d.first,0,!0,-1);var e=Ba(d,c),f=d.first+d.size-1;if(e>f)return $b(d.first+d.size-1,u(d,f).text.length,!0,1);0>b&&(b=0);for(d=u(d,e);;)if(e=Ef(a,d,e,b,c),f=(d=Aa(d,!1))&&d.find(0,!0),d&&
+(e.ch>f.from.ch||e.ch==f.from.ch&&0<e.xRel))e=F(d=f.to.line);else return e}function Ef(a,b,c,d,e){function f(d){d=ma(a,r(c,d),"line",b,l);h=!0;if(g>d.bottom)return d.left-k;if(g<d.top)return d.left+k;h=!1;return d.left}var g=e-ea(b),h=!1,k=2*a.display.wrapper.clientWidth,l=Zb(a,b),m=Y(b),p=b.text.length;e=ac(b);var n=bc(b),E=f(e),q=h,t=f(n),u=h;if(d>t)return $b(c,n,u,1);for(;;){if(m?n==e||n==gd(b,e,1):1>=n-e){m=d<E||d-E<=t-d?e:n;for(d-=m==e?E:t;tb(b.text.charAt(m));)++m;return $b(c,m,m==e?q:u,-1>
+d?-1:1<d?1:0)}var v=Math.ceil(p/2),w=e+v;if(m)for(var w=e,x=0;x<v;++x)w=gd(b,w,1);x=f(w);if(x>d){n=w;t=x;if(u=h)t+=1E3;p=v}else e=w,E=x,q=h,p-=v}}function xa(a){if(null!=a.cachedTextHeight)return a.cachedTextHeight;if(null==Fa){Fa=t("pre");for(var b=0;49>b;++b)Fa.appendChild(document.createTextNode("x")),Fa.appendChild(t("br"));Fa.appendChild(document.createTextNode("x"))}U(a.measure,Fa);b=Fa.offsetHeight/50;3<b&&(a.cachedTextHeight=b);za(a.measure);return b||1}function gb(a){if(null!=a.cachedCharWidth)return a.cachedCharWidth;
+var b=t("span","xxxxxxxxxx"),c=t("pre",[b]);U(a.measure,c);b=b.getBoundingClientRect();b=(b.right-b.left)/10;2<b&&(a.cachedCharWidth=b);return b||10}function Ja(a){a.curOp={cm:a,viewChanged:!1,startHeight:a.doc.height,forceUpdate:!1,updateInput:null,typing:!1,changeObjs:null,cursorActivityHandlers:null,cursorActivityCalled:0,selectionChanged:!1,updateMaxLine:!1,scrollLeft:null,scrollTop:null,scrollToPos:null,focus:!1,id:++Ff};Ta?Ta.ops.push(a.curOp):a.curOp.ownsGroup=Ta={ops:[a.curOp],delayedCallbacks:[]}}
+function La(a){if(a=a.curOp.ownsGroup)try{var b=a.delayedCallbacks,c=0;do{for(;c<b.length;c++)b[c]();for(var d=0;d<a.ops.length;d++){var e=a.ops[d];if(e.cursorActivityHandlers)for(;e.cursorActivityCalled<e.cursorActivityHandlers.length;)e.cursorActivityHandlers[e.cursorActivityCalled++](e.cm)}}while(c<b.length)}finally{Ta=null;for(b=0;b<a.ops.length;b++)a.ops[b].cm.curOp=null;a=a.ops;for(b=0;b<a.length;b++){var e=a[b],c=e.cm,f=d=c.display;!f.scrollbarsClipped&&f.scroller.offsetWidth&&(f.nativeBarWidth=
+f.scroller.offsetWidth-f.scroller.clientWidth,f.heightForcer.style.height=da(c)+"px",f.sizer.style.marginBottom=-f.nativeBarWidth+"px",f.sizer.style.borderRightWidth=da(c)+"px",f.scrollbarsClipped=!0);e.updateMaxLine&&Dc(c);e.mustUpdate=e.viewChanged||e.forceUpdate||null!=e.scrollTop||e.scrollToPos&&(e.scrollToPos.from.line<d.viewFrom||e.scrollToPos.to.line>=d.viewTo)||d.maxLineChanged&&c.options.lineWrapping;e.update=e.mustUpdate&&new Mb(c,e.mustUpdate&&{top:e.scrollTop,ensure:e.scrollToPos},e.forceUpdate)}for(b=
+0;b<a.length;b++)e=a[b],e.updatedDisplay=e.mustUpdate&&Lc(e.cm,e.update);for(b=0;b<a.length;b++)if(e=a[b],c=e.cm,d=c.display,e.updatedDisplay&&Lb(c),e.barMeasure=jb(c),d.maxLineChanged&&!c.options.lineWrapping&&(f=void 0,f=d.maxLine.text.length,f=$c(c,Zb(c,d.maxLine),f,void 0),e.adjustWidthTo=f.left+3,c.display.sizerWidth=e.adjustWidthTo,e.barMeasure.scrollWidth=Math.max(d.scroller.clientWidth,d.sizer.offsetLeft+e.adjustWidthTo+da(c)+c.display.barWidth),e.maxScrollLeft=Math.max(0,d.sizer.offsetLeft+
+e.adjustWidthTo-pa(c))),e.updatedDisplay||e.selectionChanged)e.preparedSelection=d.input.prepareSelection();for(b=0;b<a.length;b++)e=a[b],c=e.cm,null!=e.adjustWidthTo&&(c.display.sizer.style.minWidth=e.adjustWidthTo+"px",e.maxScrollLeft<c.doc.scrollLeft&&Ma(c,Math.min(c.display.scroller.scrollLeft,e.maxScrollLeft),!0),c.display.maxLineChanged=!1),e.preparedSelection&&c.display.input.showSelection(e.preparedSelection),e.updatedDisplay&&Oc(c,e.barMeasure),(e.updatedDisplay||e.startHeight!=c.doc.height)&&
+Na(c,e.barMeasure),e.selectionChanged&&Yc(c),c.state.focused&&e.updateInput&&c.display.input.reset(e.typing),e.focus&&e.focus==fa()&&Ud(e.cm);for(b=0;b<a.length;b++){e=a[b];c=e.cm;d=c.display;f=c.doc;e.updatedDisplay&&Md(c,e.update);null==d.wheelStartX||null==e.scrollTop&&null==e.scrollLeft&&!e.scrollToPos||(d.wheelStartX=d.wheelStartY=null);null==e.scrollTop||d.scroller.scrollTop==e.scrollTop&&!e.forceScroll||(f.scrollTop=Math.max(0,Math.min(d.scroller.scrollHeight-d.scroller.clientHeight,e.scrollTop)),
+d.scrollbars.setScrollTop(f.scrollTop),d.scroller.scrollTop=f.scrollTop);null==e.scrollLeft||d.scroller.scrollLeft==e.scrollLeft&&!e.forceScroll||(f.scrollLeft=Math.max(0,Math.min(d.scroller.scrollWidth-pa(c),e.scrollLeft)),d.scrollbars.setScrollLeft(f.scrollLeft),d.scroller.scrollLeft=f.scrollLeft,Bc(c));if(e.scrollToPos){var g=void 0,h=w(f,e.scrollToPos.from),g=w(f,e.scrollToPos.to),k=e.scrollToPos.margin;null==k&&(k=0);for(var l=0;5>l;l++){var m=!1,p=ma(c,h),n=g&&g!=h?ma(c,g):p,n=cc(c,Math.min(p.left,
+n.left),Math.min(p.top,n.top)-k,Math.max(p.left,n.left),Math.max(p.bottom,n.bottom)+k),q=c.doc.scrollTop,r=c.doc.scrollLeft;null!=n.scrollTop&&(lb(c,n.scrollTop),1<Math.abs(c.doc.scrollTop-q)&&(m=!0));null!=n.scrollLeft&&(Ma(c,n.scrollLeft),1<Math.abs(c.doc.scrollLeft-r)&&(m=!0));if(!m)break}g=p;e.scrollToPos.isCursor&&c.state.focused&&(ja(c,"scrollCursorIntoView")||(k=c.display,l=k.sizer.getBoundingClientRect(),h=null,0>g.top+l.top?h=!0:g.bottom+l.top>(window.innerHeight||document.documentElement.clientHeight)&&
+(h=!1),null==h||Gf||(g=t("div","​",null,"position: absolute; top: "+(g.top-k.viewOffset-c.display.lineSpace.offsetTop)+"px; height: "+(g.bottom-g.top+da(c)+k.barHeight)+"px; left: "+g.left+"px; width: 2px;"),c.display.lineSpace.appendChild(g),g.scrollIntoView(h),c.display.lineSpace.removeChild(g))))}h=e.maybeHiddenMarkers;g=e.maybeUnhiddenMarkers;if(h)for(k=0;k<h.length;++k)h[k].lines.length||K(h[k],"hide");if(g)for(k=0;k<g.length;++k)g[k].lines.length&&K(g[k],"unhide");d.wrapper.offsetHeight&&(f.scrollTop=
+c.display.scroller.scrollTop);e.changeObjs&&K(c,"changes",c,e.changeObjs);e.update&&e.update.finish()}}}function T(a,b){if(a.curOp)return b();Ja(a);try{return b()}finally{La(a)}}function G(a,b){return function(){if(a.curOp)return b.apply(a,arguments);Ja(a);try{return b.apply(a,arguments)}finally{La(a)}}}function M(a){return function(){if(this.curOp)return a.apply(this,arguments);Ja(this);try{return a.apply(this,arguments)}finally{La(this)}}}function N(a){return function(){var b=this.cm;if(!b||b.curOp)return a.apply(this,
+arguments);Ja(b);try{return a.apply(this,arguments)}finally{La(b)}}}function ke(a,b,c){for(var d=this.line=b,e;d=Aa(d,!1);)d=d.find(1,!0).line,(e||(e=[])).push(d);this.size=(this.rest=e)?F(A(this.rest))-c+1:1;this.node=this.text=null;this.hidden=ya(a,b)}function Nb(a,b,c){var d=[],e;for(e=b;e<c;)b=new ke(a.doc,u(a.doc,e),e),e+=b.size,d.push(b);return d}function Q(a,b,c,d){null==b&&(b=a.doc.first);null==c&&(c=a.doc.first+a.doc.size);d||(d=0);var e=a.display;d&&c<e.viewTo&&(null==e.updateLineNumbers||
+e.updateLineNumbers>b)&&(e.updateLineNumbers=b);a.curOp.viewChanged=!0;if(b>=e.viewTo)ra&&Mc(a.doc,b)<e.viewTo&&qa(a);else if(c<=e.viewFrom)ra&&Ld(a.doc,c+d)>e.viewFrom?qa(a):(e.viewFrom+=d,e.viewTo+=d);else if(b<=e.viewFrom&&c>=e.viewTo)qa(a);else if(b<=e.viewFrom){var f=dc(a,c,c+d,1);f?(e.view=e.view.slice(f.index),e.viewFrom=f.lineN,e.viewTo+=d):qa(a)}else if(c>=e.viewTo)(f=dc(a,b,b,-1))?(e.view=e.view.slice(0,f.index),e.viewTo=f.lineN):qa(a);else{var f=dc(a,b,b,-1),g=dc(a,c,c+d,1);f&&g?(e.view=
+e.view.slice(0,f.index).concat(Nb(a,f.lineN,g.lineN)).concat(e.view.slice(g.index)),e.viewTo+=d):qa(a)}if(a=e.externalMeasured)c<a.lineN?a.lineN+=d:b<a.lineN+a.size&&(e.externalMeasured=null)}function na(a,b,c){a.curOp.viewChanged=!0;var d=a.display,e=a.display.externalMeasured;e&&b>=e.lineN&&b<e.lineN+e.size&&(d.externalMeasured=null);b<d.viewFrom||b>=d.viewTo||(a=d.view[Ca(a,b)],null!=a.node&&(a=a.changes||(a.changes=[]),-1==D(a,c)&&a.push(c)))}function qa(a){a.display.viewFrom=a.display.viewTo=
+a.doc.first;a.display.view=[];a.display.viewOffset=0}function Ca(a,b){if(b>=a.display.viewTo)return null;b-=a.display.viewFrom;if(0>b)return null;for(var c=a.display.view,d=0;d<c.length;d++)if(b-=c[d].size,0>b)return d}function dc(a,b,c,d){var e=Ca(a,b),f=a.display.view;if(!ra||c==a.doc.first+a.doc.size)return{index:e,lineN:c};for(var g=0,h=a.display.viewFrom;g<e;g++)h+=f[g].size;if(h!=b){if(0<d){if(e==f.length-1)return null;b=h+f[e].size-b;e++}else b=h-b;c+=b}for(;Mc(a.doc,c)!=c;){if(e==(0>d?0:f.length-
+1))return null;c+=d*f[e-(0>d?1:0)].size;e+=d}return{index:e,lineN:c}}function Kd(a){a=a.display.view;for(var b=0,c=0;c<a.length;c++){var d=a[c];d.hidden||d.node&&!d.changes||++b}return b}function sf(a){function b(){d.activeTouch&&(e=setTimeout(function(){d.activeTouch=null},1E3),f=d.activeTouch,f.end=+new Date)}function c(a,b){if(null==b.left)return!0;var c=b.left-a.left,d=b.top-a.top;return 400<c*c+d*d}var d=a.display;v(d.scroller,"mousedown",G(a,pe));B&&11>C?v(d.scroller,"dblclick",G(a,function(b){if(!ja(a,
+b)){var c=Ua(a,b);!c||hd(a,b,"gutterClick",!0,L)||oa(a.display,b)||(O(b),b=a.findWordAt(c),Ub(a.doc,b.anchor,b.head))}})):v(d.scroller,"dblclick",function(b){ja(a,b)||O(b)});id||v(d.scroller,"contextmenu",function(b){qe(a,b)});var e,f={end:0};v(d.scroller,"touchstart",function(a){var b;1!=a.touches.length?b=!1:(b=a.touches[0],b=1>=b.radiusX&&1>=b.radiusY);b||(clearTimeout(e),b=+new Date,d.activeTouch={start:b,moved:!1,prev:300>=b-f.end?f:null},1==a.touches.length&&(d.activeTouch.left=a.touches[0].pageX,
+d.activeTouch.top=a.touches[0].pageY))});v(d.scroller,"touchmove",function(){d.activeTouch&&(d.activeTouch.moved=!0)});v(d.scroller,"touchend",function(e){var f=d.activeTouch;if(f&&!oa(d,e)&&null!=f.left&&!f.moved&&300>new Date-f.start){var g=a.coordsChar(d.activeTouch,"page"),f=!f.prev||c(f,f.prev)?new z(g,g):!f.prev.prev||c(f,f.prev.prev)?a.findWordAt(g):new z(r(g.line,0),w(a.doc,r(g.line+1,0)));a.setSelection(f.anchor,f.head);a.focus();O(e)}b()});v(d.scroller,"touchcancel",b);v(d.scroller,"scroll",
+function(){d.scroller.clientHeight&&(lb(a,d.scroller.scrollTop),Ma(a,d.scroller.scrollLeft,!0),K(a,"scroll",a))});v(d.scroller,"mousewheel",function(b){re(a,b)});v(d.scroller,"DOMMouseScroll",function(b){re(a,b)});v(d.wrapper,"scroll",function(){d.wrapper.scrollTop=d.wrapper.scrollLeft=0});d.dragFunctions={simple:function(b){ja(a,b)||jd(b)},start:function(b){if(B&&(!a.state.draggingText||100>+new Date-se))jd(b);else if(!ja(a,b)&&!oa(a.display,b)&&(b.dataTransfer.setData("Text",a.getSelection()),b.dataTransfer.setDragImage&&
+!te)){var c=t("img",null,null,"position: fixed; left: 0; top: 0;");c.src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw\x3d\x3d";ba&&(c.width=c.height=1,a.display.wrapper.appendChild(c),c._top=c.offsetTop);b.dataTransfer.setDragImage(c,0,0);ba&&c.parentNode.removeChild(c)}},drop:G(a,Hf)};var g=d.input.getField();v(g,"keyup",function(b){ue.call(a,b)});v(g,"keydown",G(a,ve));v(g,"keypress",G(a,we));v(g,"focus",cb(xc,a));v(g,"blur",cb(db,a))}function If(a){var b=a.display;
+if(b.lastWrapHeight!=b.wrapper.clientHeight||b.lastWrapWidth!=b.wrapper.clientWidth)b.cachedCharWidth=b.cachedTextHeight=b.cachedPaddingH=null,b.scrollbarsClipped=!1,a.setSize()}function oa(a,b){for(var c=b.target||b.srcElement;c!=a.wrapper;c=c.parentNode)if(!c||1==c.nodeType&&"true"==c.getAttribute("cm-ignore-events")||c.parentNode==a.sizer&&c!=a.mover)return!0}function Ua(a,b,c,d){var e=a.display;if(!c&&"true"==(b.target||b.srcElement).getAttribute("cm-not-content"))return null;var f,g;c=e.lineSpace.getBoundingClientRect();
+try{f=b.clientX-c.left,g=b.clientY-c.top}catch(h){return null}b=fd(a,f,g);var k;d&&1==b.xRel&&(k=u(a.doc,b.line).text).length==b.ch&&(d=aa(k,k.length,a.options.tabSize)-k.length,b=r(b.line,Math.max(0,Math.round((f-ie(a.display).left)/gb(a.display))-d)));return b}function pe(a){var b=this.display;if(!(b.activeTouch&&b.input.supportsTouch()||ja(this,a)))if(b.shift=a.shiftKey,oa(b,a))J||(b.scroller.draggable=!1,setTimeout(function(){b.scroller.draggable=!0},100));else if(!hd(this,a,"gutterClick",!0,
+L)){var c=Ua(this,a);window.focus();switch(xe(a)){case 1:c?Jf(this,a,c):(a.target||a.srcElement)==b.scroller&&O(a);break;case 2:J&&(this.state.lastMiddleDown=+new Date);c&&Ub(this.doc,c);setTimeout(function(){b.input.focus()},20);O(a);break;case 3:id?qe(this,a):Kf(this)}}}function Jf(a,b,c){B?setTimeout(cb(Ud,a),0):a.curOp.focus=fa();var d=+new Date,e;ec&&ec.time>d-400&&0==y(ec.pos,c)?e="triple":fc&&fc.time>d-400&&0==y(fc.pos,c)?(e="double",ec={time:d,pos:c}):(e="single",fc={time:d,pos:c});var d=
+a.doc.sel,f=W?b.metaKey:b.ctrlKey,g;a.options.dragDrop&&Lf&&!Rb(a)&&"single"==e&&-1<(g=d.contains(c))&&!d.ranges[g].empty()?Mf(a,b,c,f):Nf(a,b,c,e,f)}function Mf(a,b,c,d){var e=a.display,f=+new Date,g=G(a,function(h){J&&(e.scroller.draggable=!1);a.state.draggingText=!1;ka(document,"mouseup",g);ka(e.scroller,"drop",g);10>Math.abs(b.clientX-h.clientX)+Math.abs(b.clientY-h.clientY)&&(O(h),!d&&+new Date-200<f&&Ub(a.doc,c),J||B&&9==C?setTimeout(function(){document.body.focus();e.input.focus()},20):e.input.focus())});
+J&&(e.scroller.draggable=!0);a.state.draggingText=g;e.scroller.dragDrop&&e.scroller.dragDrop();v(document,"mouseup",g);v(e.scroller,"drop",g)}function Nf(a,b,c,d,e){function f(b){if(0!=y(x,b))if(x=b,"rect"==d){for(var e=[],f=a.options.tabSize,g=aa(u(l,c.line).text,c.ch,f),h=aa(u(l,b.line).text,b.ch,f),k=Math.min(g,h),g=Math.max(g,h),h=Math.min(c.line,b.line),q=Math.min(a.lastLine(),Math.max(c.line,b.line));h<=q;h++){var E=u(l,h).text,t=ye(E,k,f);k==g?e.push(new z(r(h,t),r(h,t))):E.length>t&&e.push(new z(r(h,
+t),r(h,ye(E,g,f))))}e.length||e.push(new z(c,c));H(l,Z(n.ranges.slice(0,p).concat(e),p),{origin:"*mouse",scroll:!1});a.scrollIntoView(b)}else e=m,f=e.anchor,k=b,"single"!=d&&(b="double"==d?a.findWordAt(b):new z(r(b.line,0),w(l,r(b.line+1,0))),0<y(b.anchor,f)?(k=b.head,f=Qb(e.from(),b.anchor)):(k=b.anchor,f=Pb(e.to(),b.head))),e=n.ranges.slice(0),e[p]=new z(w(l,f),k),H(l,Z(e,p),kd)}function g(b){var c=++A,e=Ua(a,b,!0,"rect"==d);if(e)if(0!=y(e,x)){a.curOp.focus=fa();f(e);var h=Hc(k,l);(e.line>=h.to||
+e.line<h.from)&&setTimeout(G(a,function(){A==c&&g(b)}),150)}else{var m=b.clientY<B.top?-20:b.clientY>B.bottom?20:0;m&&setTimeout(G(a,function(){A==c&&(k.scroller.scrollTop+=m,g(b))}),50)}}function h(a){A=Infinity;O(a);k.input.focus();ka(document,"mousemove",F);ka(document,"mouseup",C);l.history.lastSelOrigin=null}var k=a.display,l=a.doc;O(b);var m,p,n=l.sel,q=n.ranges;e&&!b.shiftKey?(p=l.sel.contains(c),m=-1<p?q[p]:new z(c,c)):(m=l.sel.primary(),p=l.sel.primIndex);if(b.altKey)d="rect",e||(m=new z(c,
+c)),c=Ua(a,b,!0,!0),p=-1;else if("double"==d){var t=a.findWordAt(c);m=a.display.shift||l.extend?rb(l,m,t.anchor,t.head):t}else"triple"==d?(t=new z(r(c.line,0),w(l,r(c.line+1,0))),m=a.display.shift||l.extend?rb(l,m,t.anchor,t.head):t):m=rb(l,m,c);e?-1==p?(p=q.length,H(l,Z(q.concat([m]),p),{scroll:!1,origin:"*mouse"})):1<q.length&&q[p].empty()&&"single"==d&&!b.shiftKey?(H(l,Z(q.slice(0,p).concat(q.slice(p+1)),0)),n=l.sel):Xc(l,p,m,kd):(p=0,H(l,new la([m],0),kd),n=l.sel);var x=c,B=k.wrapper.getBoundingClientRect(),
+A=0,F=G(a,function(a){xe(a)?g(a):h(a)}),C=G(a,h);v(document,"mousemove",F);v(document,"mouseup",C)}function hd(a,b,c,d,e){try{var f=b.clientX,g=b.clientY}catch(h){return!1}if(f>=Math.floor(a.display.gutters.getBoundingClientRect().right))return!1;d&&O(b);d=a.display;var k=d.lineDiv.getBoundingClientRect();if(g>k.bottom||!S(a,c))return ld(b);g-=k.top-d.viewOffset;for(k=0;k<a.options.gutters.length;++k){var l=d.gutters.childNodes[k];if(l&&l.getBoundingClientRect().right>=f)return f=Ba(a.doc,g),e(a,
+c,a,f,a.options.gutters[k],b),ld(b)}}function Hf(a){var b=this;if(!ja(b,a)&&!oa(b.display,a)){O(a);B&&(se=+new Date);var c=Ua(b,a,!0),d=a.dataTransfer.files;if(c&&!Rb(b))if(d&&d.length&&window.FileReader&&window.File){var e=d.length,f=Array(e),g=0;a=function(a,d){var h=new FileReader;h.onload=G(b,function(){f[d]=h.result;if(++g==e){c=w(b.doc,c);var a={from:c,to:c,text:sa(f.join("\n")),origin:"paste"};Oa(b.doc,a);be(b.doc,ga(c,ta(a)))}});h.readAsText(a)};for(var h=0;h<e;++h)a(d[h],h)}else if(b.state.draggingText&&
+-1<b.doc.sel.contains(c))b.state.draggingText(a),setTimeout(function(){b.display.input.focus()},20);else try{if(f=a.dataTransfer.getData("Text")){if(b.state.draggingText&&(W?!a.altKey:!a.ctrlKey))var k=b.listSelections();Vb(b.doc,ga(c,c));if(k)for(h=0;h<k.length;++h)wb(b.doc,"",k[h].anchor,k[h].head,"drag");b.replaceSelection(f,"around","paste");b.display.input.focus()}}catch(l){}}}function lb(a,b){2>Math.abs(a.doc.scrollTop-b)||(a.doc.scrollTop=b,wa||Pc(a,{top:b}),a.display.scroller.scrollTop!=b&&
+(a.display.scroller.scrollTop=b),a.display.scrollbars.setScrollTop(b),wa&&Pc(a),fb(a,100))}function Ma(a,b,c){(c?b==a.doc.scrollLeft:2>Math.abs(a.doc.scrollLeft-b))||(b=Math.min(b,a.display.scroller.scrollWidth-a.display.scroller.clientWidth),a.doc.scrollLeft=b,Bc(a),a.display.scroller.scrollLeft!=b&&(a.display.scroller.scrollLeft=b),a.display.scrollbars.setScrollLeft(b))}function re(a,b){var c=ze(b),d=c.x,c=c.y,e=a.display,f=e.scroller;if(d&&f.scrollWidth>f.clientWidth||c&&f.scrollHeight>f.clientHeight){if(c&&
+W&&J){var g=b.target,h=e.view;a:for(;g!=f;g=g.parentNode)for(var k=0;k<h.length;k++)if(h[k].node==g){a.display.currentWheelTarget=g;break a}}!d||wa||ba||null==R?(c&&null!=R&&(g=c*R,h=a.doc.scrollTop,k=h+e.wrapper.clientHeight,0>g?h=Math.max(0,h+g-50):k=Math.min(a.doc.height,k+g+50),Pc(a,{top:h,bottom:k})),20>gc&&(null==e.wheelStartX?(e.wheelStartX=f.scrollLeft,e.wheelStartY=f.scrollTop,e.wheelDX=d,e.wheelDY=c,setTimeout(function(){if(null!=e.wheelStartX){var a=f.scrollLeft-e.wheelStartX,b=f.scrollTop-
+e.wheelStartY,a=b&&e.wheelDY&&b/e.wheelDY||a&&e.wheelDX&&a/e.wheelDX;e.wheelStartX=e.wheelStartY=null;a&&(R=(R*gc+a)/(gc+1),++gc)}},200)):(e.wheelDX+=d,e.wheelDY+=c))):(c&&lb(a,Math.max(0,Math.min(f.scrollTop+c*R,f.scrollHeight-f.clientHeight))),Ma(a,Math.max(0,Math.min(f.scrollLeft+d*R,f.scrollWidth-f.clientWidth))),O(b),e.wheelStartX=null)}}function hc(a,b,c){if("string"==typeof b&&(b=ic[b],!b))return!1;a.display.input.ensurePolled();var d=a.display.shift,e=!1;try{Rb(a)&&(a.state.suppressEdits=
+!0),c&&(a.display.shift=!1),e=b(a)!=Ae}finally{a.display.shift=d,a.state.suppressEdits=!1}return e}function Of(a,b,c){for(var d=0;d<a.state.keyMaps.length;d++){var e=xb(b,a.state.keyMaps[d],c,a);if(e)return e}return a.options.extraKeys&&xb(b,a.options.extraKeys,c,a)||xb(b,a.options.keyMap,c,a)}function jc(a,b,c,d){var e=a.state.keySeq;if(e){if(Pf(b))return"handled";Qf.set(50,function(){a.state.keySeq==e&&(a.state.keySeq=null,a.display.input.reset())});b=e+" "+b}d=Of(a,b,d);"multi"==d&&(a.state.keySeq=
+b);"handled"==d&&L(a,"keyHandled",a,b,c);if("handled"==d||"multi"==d)O(c),Yc(a);return e&&!d&&/\'$/.test(b)?(O(c),!0):!!d}function Be(a,b){var c=Rf(b,!0);return c?b.shiftKey&&!a.state.keySeq?jc(a,"Shift-"+c,b,function(b){return hc(a,b,!0)})||jc(a,c,b,function(b){if("string"==typeof b?/^go[A-Z]/.test(b):b.motion)return hc(a,b)}):jc(a,c,b,function(b){return hc(a,b)}):!1}function Sf(a,b,c){return jc(a,"'"+c+"'",b,function(b){return hc(a,b,!0)})}function ve(a){this.curOp.focus=fa();if(!ja(this,a)){B&&
+11>C&&27==a.keyCode&&(a.returnValue=!1);var b=a.keyCode;this.display.shift=16==b||a.shiftKey;var c=Be(this,a);ba&&(md=c?b:null,!c&&88==b&&!Ce&&(W?a.metaKey:a.ctrlKey)&&this.replaceSelection("",null,"cut"));18!=b||/\bCodeMirror-crosshair\b/.test(this.display.lineDiv.className)||Tf(this)}}function Tf(a){function b(a){18!=a.keyCode&&a.altKey||(kb(c,"CodeMirror-crosshair"),ka(document,"keyup",b),ka(document,"mouseover",b))}var c=a.display.lineDiv;mb(c,"CodeMirror-crosshair");v(document,"keyup",b);v(document,
+"mouseover",b)}function ue(a){16==a.keyCode&&(this.doc.sel.shift=!1);ja(this,a)}function we(a){if(!(oa(this.display,a)||ja(this,a)||a.ctrlKey&&!a.altKey||W&&a.metaKey)){var b=a.keyCode,c=a.charCode;if(ba&&b==md)md=null,O(a);else if(!ba||a.which&&!(10>a.which)||!Be(this,a))if(b=String.fromCharCode(null==c?b:c),!Sf(this,a,b))this.display.input.onKeyPress(a)}}function Kf(a){a.state.delayingBlurEvent=!0;setTimeout(function(){a.state.delayingBlurEvent&&(a.state.delayingBlurEvent=!1,db(a))},100)}function xc(a){a.state.delayingBlurEvent&&
+(a.state.delayingBlurEvent=!1);"nocursor"!=a.options.readOnly&&(a.state.focused||(K(a,"focus",a),a.state.focused=!0,mb(a.display.wrapper,"CodeMirror-focused"),a.curOp||a.display.selForContextMenu==a.doc.sel||(a.display.input.reset(),J&&setTimeout(function(){a.display.input.reset(!0)},20)),a.display.input.receivedFocus()),Yc(a))}function db(a){a.state.delayingBlurEvent||(a.state.focused&&(K(a,"blur",a),a.state.focused=!1,kb(a.display.wrapper,"CodeMirror-focused")),clearInterval(a.display.blinker),
+setTimeout(function(){a.state.focused||(a.display.shift=!1)},150))}function qe(a,b){var c;(c=oa(a.display,b))||(c=S(a,"gutterContextMenu")?hd(a,b,"gutterContextMenu",!1,K):!1);if(!c)a.display.input.onContextMenu(b)}function De(a,b){if(0>y(a,b.from))return a;if(0>=y(a,b.to))return ta(b);var c=a.line+b.text.length-(b.to.line-b.from.line)-1,d=a.ch;a.line==b.to.line&&(d+=ta(b).ch-b.to.ch);return r(c,d)}function nd(a,b){for(var c=[],d=0;d<a.sel.ranges.length;d++){var e=a.sel.ranges[d];c.push(new z(De(e.anchor,
+b),De(e.head,b)))}return Z(c,a.sel.primIndex)}function Ee(a,b,c){return a.line==b.line?r(c.line,a.ch-b.ch+c.ch):r(c.line+(a.line-b.line),a.ch)}function Fe(a,b,c){b={canceled:!1,from:b.from,to:b.to,text:b.text,origin:b.origin,cancel:function(){this.canceled=!0}};c&&(b.update=function(b,c,f,g){b&&(this.from=w(a,b));c&&(this.to=w(a,c));f&&(this.text=f);void 0!==g&&(this.origin=g)});K(a,"beforeChange",a,b);a.cm&&K(a.cm,"beforeChange",a.cm,b);return b.canceled?null:{from:b.from,to:b.to,text:b.text,origin:b.origin}}
+function Oa(a,b,c){if(a.cm){if(!a.cm.curOp)return G(a.cm,Oa)(a,b,c);if(a.cm.state.suppressEdits)return}if(S(a,"beforeChange")||a.cm&&S(a.cm,"beforeChange"))if(b=Fe(a,b,!0),!b)return;if(c=Ge&&!c&&Uf(a,b.from,b.to))for(var d=c.length-1;0<=d;--d)He(a,{from:c[d].from,to:c[d].to,text:d?[""]:b.text});else He(a,b)}function He(a,b){if(1!=b.text.length||""!=b.text[0]||0!=y(b.from,b.to)){var c=nd(a,b);Ie(a,b,c,a.cm?a.cm.curOp.id:NaN);yb(a,b,c,od(a,b));var d=[];Ga(a,function(a,c){c||-1!=D(d,a.history)||(Je(a.history,
+b),d.push(a.history));yb(a,b,null,od(a,b))})}}function kc(a,b,c){if(!a.cm||!a.cm.state.suppressEdits){for(var d=a.history,e,f=a.sel,g="undo"==b?d.done:d.undone,h="undo"==b?d.undone:d.done,k=0;k<g.length&&(e=g[k],c?!e.ranges||e.equals(a.sel):e.ranges);k++);if(k!=g.length){for(d.lastOrigin=d.lastSelOrigin=null;;)if(e=g.pop(),e.ranges){Wb(e,h);if(c&&!e.equals(a.sel)){H(a,e,{clearRedo:!1});return}f=e}else break;c=[];Wb(f,h);h.push({changes:c,generation:d.generation});d.generation=e.generation||++d.maxGeneration;
+d=S(a,"beforeChange")||a.cm&&S(a.cm,"beforeChange");for(k=e.changes.length-1;0<=k;--k){var l=e.changes[k];l.origin=b;if(d&&!Fe(a,l,!1)){g.length=0;break}c.push(pd(a,l));f=k?nd(a,l):A(g);yb(a,l,f,Ke(a,l));!k&&a.cm&&a.cm.scrollIntoView({from:l.from,to:ta(l)});var m=[];Ga(a,function(a,b){b||-1!=D(m,a.history)||(Je(a.history,l),m.push(a.history));yb(a,l,null,Ke(a,l))})}}}}function Le(a,b){if(0!=b&&(a.first+=b,a.sel=new la(ob(a.sel.ranges,function(a){return new z(r(a.anchor.line+b,a.anchor.ch),r(a.head.line+
+b,a.head.ch))}),a.sel.primIndex),a.cm)){Q(a.cm,a.first,a.first-b,b);for(var c=a.cm.display,d=c.viewFrom;d<c.viewTo;d++)na(a.cm,d,"gutter")}}function yb(a,b,c,d){if(a.cm&&!a.cm.curOp)return G(a.cm,yb)(a,b,c,d);if(b.to.line<a.first)Le(a,b.text.length-1-(b.to.line-b.from.line));else if(!(b.from.line>a.lastLine())){if(b.from.line<a.first){var e=b.text.length-1-(a.first-b.from.line);Le(a,e);b={from:r(a.first,0),to:r(b.to.line+e,b.to.ch),text:[A(b.text)],origin:b.origin}}e=a.lastLine();b.to.line>e&&(b=
+{from:b.from,to:r(e,u(a,e).text.length),text:[b.text[0]],origin:b.origin});b.removed=Da(a,b.from,b.to);c||(c=nd(a,b));a.cm?Vf(a.cm,b,d):qd(a,b,d);Vb(a,c,ha)}}function Vf(a,b,c){var d=a.doc,e=a.display,f=b.from,g=b.to,h=!1,k=f.line;a.options.lineWrapping||(k=F(ia(u(d,f.line))),d.iter(k,g.line+1,function(a){if(a==e.maxLine)return h=!0}));-1<d.sel.contains(b.from,b.to)&&fe(a);qd(d,b,c,Id(a));a.options.lineWrapping||(d.iter(k,f.line+b.text.length,function(a){var b=Kb(a);b>e.maxLineLength&&(e.maxLine=
+a,e.maxLineLength=b,e.maxLineChanged=!0,h=!1)}),h&&(a.curOp.updateMaxLine=!0));d.frontier=Math.min(d.frontier,f.line);fb(a,400);c=b.text.length-(g.line-f.line)-1;b.full?Q(a):f.line!=g.line||1!=b.text.length||Me(a.doc,b)?Q(a,f.line,g.line+1,c):na(a,f.line,"text");c=S(a,"changes");if((d=S(a,"change"))||c)b={from:f,to:g,text:b.text,removed:b.removed,origin:b.origin},d&&L(a,"change",a,b),c&&(a.curOp.changeObjs||(a.curOp.changeObjs=[])).push(b);a.display.selForContextMenu=null}function wb(a,b,c,d,e){d||
+(d=c);if(0>y(d,c)){var f=d;d=c;c=f}"string"==typeof b&&(b=sa(b));Oa(a,{from:c,to:d,text:b,origin:e})}function cc(a,b,c,d,e){var f=a.display,g=xa(a.display);0>c&&(c=0);var h=a.curOp&&null!=a.curOp.scrollTop?a.curOp.scrollTop:f.scroller.scrollTop,k=Nc(a),l={};e-c>k&&(e=c+k);var m=a.doc.height+Ec(f),p=c<g,g=e>m-g;c<h?l.scrollTop=p?0:c:e>h+k&&(c=Math.min(c,(g?m:e)-k),c!=h&&(l.scrollTop=c));h=a.curOp&&null!=a.curOp.scrollLeft?a.curOp.scrollLeft:f.scroller.scrollLeft;a=pa(a)-(a.options.fixedGutter?f.gutters.offsetWidth:
+0);(f=d-b>a)&&(d=b+a);10>b?l.scrollLeft=0:b<h?l.scrollLeft=Math.max(0,b-(f?0:10)):d>a+h-3&&(l.scrollLeft=d+(f?0:10)-a);return l}function lc(a,b,c){null==b&&null==c||mc(a);null!=b&&(a.curOp.scrollLeft=(null==a.curOp.scrollLeft?a.doc.scrollLeft:a.curOp.scrollLeft)+b);null!=c&&(a.curOp.scrollTop=(null==a.curOp.scrollTop?a.doc.scrollTop:a.curOp.scrollTop)+c)}function Pa(a){mc(a);var b=a.getCursor(),c=b,d=b;a.options.lineWrapping||(c=b.ch?r(b.line,b.ch-1):b,d=r(b.line,b.ch+1));a.curOp.scrollToPos={from:c,
+to:d,margin:a.options.cursorScrollMargin,isCursor:!0}}function mc(a){var b=a.curOp.scrollToPos;if(b){a.curOp.scrollToPos=null;var c=oe(a,b.from),d=oe(a,b.to),b=cc(a,Math.min(c.left,d.left),Math.min(c.top,d.top)-b.margin,Math.max(c.right,d.right),Math.max(c.bottom,d.bottom)+b.margin);a.scrollTo(b.scrollLeft,b.scrollTop)}}function pb(a,b,c,d){var e=a.doc,f;null==c&&(c="add");"smart"==c&&(e.mode.indent?f=sb(a,b):c="prev");var g=a.options.tabSize,h=u(e,b),k=aa(h.text,null,g);h.stateAfter&&(h.stateAfter=
+null);var l=h.text.match(/^\s*/)[0],m;if(!d&&!/\S/.test(h.text))m=0,c="not";else if("smart"==c&&(m=e.mode.indent(f,h.text.slice(l.length),h.text),m==Ae||150<m)){if(!d)return;c="prev"}"prev"==c?m=b>e.first?aa(u(e,b-1).text,null,g):0:"add"==c?m=k+a.options.indentUnit:"subtract"==c?m=k-a.options.indentUnit:"number"==typeof c&&(m=k+c);m=Math.max(0,m);c="";d=0;if(a.options.indentWithTabs)for(a=Math.floor(m/g);a;--a)d+=g,c+="\t";d<m&&(c+=Ne(m-d));if(c!=l)return wb(e,c,r(b,0),r(b,l.length),"+input"),h.stateAfter=
+null,!0;for(a=0;a<e.sel.ranges.length;a++)if(g=e.sel.ranges[a],g.head.line==b&&g.head.ch<l.length){d=r(b,l.length);Xc(e,a,new z(d,d));break}}function nc(a,b,c,d){var e=b,f=b;"number"==typeof b?f=u(a,Math.max(a.first,Math.min(b,a.first+a.size-1))):e=F(b);if(null==e)return null;d(f,e)&&a.cm&&na(a.cm,e,c);return f}function Va(a,b){for(var c=a.doc.sel.ranges,d=[],e=0;e<c.length;e++){for(var f=b(c[e]);d.length&&0>=y(f.from,A(d).to);){var g=d.pop();if(0>y(g.from,f.from)){f.from=g.from;break}}d.push(f)}T(a,
+function(){for(var b=d.length-1;0<=b;b--)wb(a.doc,"",d[b].from,d[b].to,"+delete");Pa(a)})}function rd(a,b,c,d,e){function f(b){var d=(e?gd:Oe)(l,h,c,!0);if(null==d){if(b=!b)b=g+c,b<a.first||b>=a.first+a.size?b=m=!1:(g=b,b=l=u(a,b));if(b)h=e?(0>c?bc:ac)(l):0>c?l.text.length:0;else return m=!1}else h=d;return!0}var g=b.line,h=b.ch,k=c,l=u(a,g),m=!0;if("char"==d)f();else if("column"==d)f(!0);else if("word"==d||"group"==d){var p=null;d="group"==d;b=a.cm&&a.cm.getHelper(b,"wordChars");for(var n=!0;!(0>
+c)||f(!n);n=!1){var q=l.text.charAt(h)||"\n",q=oc(q,b)?"w":d&&"\n"==q?"n":!d||/\s/.test(q)?null:"p";!d||n||q||(q="s");if(p&&p!=q){0>c&&(c=1,f());break}q&&(p=q);if(0<c&&!f(!n))break}}k=Xb(a,r(g,h),k,!0);m||(k.hitSide=!0);return k}function Pe(a,b,c,d){var e=a.doc,f=b.left,g;"page"==d?(g=Math.min(a.display.wrapper.clientHeight,window.innerHeight||document.documentElement.clientHeight),g=b.top+c*(g-(0>c?1.5:.5)*xa(a.display))):"line"==d&&(g=0<c?b.bottom+3:b.top-3);for(;;){b=fd(a,f,g);if(!b.outside)break;
+if(0>c?0>=g:g>=e.height){b.hitSide=!0;break}g+=5*c}return b}function x(a,b,c,d){q.defaults[a]=b;c&&(Ka[a]=d?function(a,b,d){d!=Fd&&c(a,b,d)}:c)}function Wf(a){var b=a.split(/-(?!$)/);a=b[b.length-1];for(var c,d,e,f,g=0;g<b.length-1;g++){var h=b[g];if(/^(cmd|meta|m)$/i.test(h))f=!0;else if(/^a(lt)?$/i.test(h))c=!0;else if(/^(c|ctrl|control)$/i.test(h))d=!0;else if(/^s(hift)$/i.test(h))e=!0;else throw Error("Unrecognized modifier name: "+h);}c&&(a="Alt-"+a);d&&(a="Ctrl-"+a);f&&(a="Cmd-"+a);e&&(a="Shift-"+
+a);return a}function pc(a){return"string"==typeof a?ua[a]:a}function Wa(a,b,c,d,e){if(d&&d.shared)return Xf(a,b,c,d,e);if(a.cm&&!a.cm.curOp)return G(a.cm,Wa)(a,b,c,d,e);var f=new Ha(a,e);e=y(b,c);d&&V(d,f,!1);if(0<e||0==e&&!1!==f.clearWhenEmpty)return f;f.replacedWith&&(f.collapsed=!0,f.widgetNode=t("span",[f.replacedWith],"CodeMirror-widget"),d.handleMouseEvents||f.widgetNode.setAttribute("cm-ignore-events","true"),d.insertLeft&&(f.widgetNode.insertLeft=!0));if(f.collapsed){if(Qe(a,b.line,b,c,f)||
+b.line!=c.line&&Qe(a,c.line,b,c,f))throw Error("Inserting collapsed marker partially overlapping an existing one");ra=!0}f.addToHistory&&Ie(a,{from:b,to:c,origin:"markText"},a.sel,NaN);var g=b.line,h=a.cm,k;a.iter(g,c.line+1,function(a){h&&f.collapsed&&!h.options.lineWrapping&&ia(a)==h.display.maxLine&&(k=!0);f.collapsed&&g!=b.line&&ca(a,0);var d=new qc(f,g==b.line?b.ch:null,g==c.line?c.ch:null);a.markedSpans=a.markedSpans?a.markedSpans.concat([d]):[d];d.marker.attachLine(a);++g});f.collapsed&&a.iter(b.line,
+c.line+1,function(b){ya(a,b)&&ca(b,0)});f.clearOnEnter&&v(f,"beforeCursorEnter",function(){f.clear()});f.readOnly&&(Ge=!0,(a.history.done.length||a.history.undone.length)&&a.clearHistory());f.collapsed&&(f.id=++sd,f.atomic=!0);if(h){k&&(h.curOp.updateMaxLine=!0);if(f.collapsed)Q(h,b.line,c.line+1);else if(f.className||f.title||f.startStyle||f.endStyle||f.css)for(d=b.line;d<=c.line;d++)na(h,d,"text");f.atomic&&ge(h.doc);L(h,"markerAdded",h,f)}return f}function Xf(a,b,c,d,e){d=V(d);d.shared=!1;var f=
+[Wa(a,b,c,d,e)],g=f[0],h=d.widgetNode;Ga(a,function(a){h&&(d.widgetNode=h.cloneNode(!0));f.push(Wa(a,w(a,b),w(a,c),d,e));for(var l=0;l<a.linked.length;++l)if(a.linked[l].isParent)return;g=A(f)});return new rc(f,g)}function Re(a){return a.findMarks(r(a.first,0),a.clipPos(r(a.lastLine())),function(a){return a.parent})}function Yf(a){for(var b=0;b<a.length;b++){var c=a[b],d=[c.primary.doc];Ga(c.primary.doc,function(a){d.push(a)});for(var e=0;e<c.markers.length;e++){var f=c.markers[e];-1==D(d,f.doc)&&
+(f.parent=null,c.markers.splice(e--,1))}}}function qc(a,b,c){this.marker=a;this.from=b;this.to=c}function zb(a,b){if(a)for(var c=0;c<a.length;++c){var d=a[c];if(d.marker==b)return d}}function od(a,b){if(b.full)return null;var c=qb(a,b.from.line)&&u(a,b.from.line).markedSpans,d=qb(a,b.to.line)&&u(a,b.to.line).markedSpans;if(!c&&!d)return null;var e=b.from.ch,f=b.to.ch,g=0==y(b.from,b.to);if(c)for(var h=0,k;h<c.length;++h){var l=c[h],m=l.marker;if(null==l.from||(m.inclusiveLeft?l.from<=e:l.from<e)||
+!(l.from!=e||"bookmark"!=m.type||g&&l.marker.insertLeft)){var p=null==l.to||(m.inclusiveRight?l.to>=e:l.to>e);(k||(k=[])).push(new qc(m,l.from,p?null:l.to))}}c=k;if(d)for(var h=0,n;h<d.length;++h)if(k=d[h],l=k.marker,null==k.to||(l.inclusiveRight?k.to>=f:k.to>f)||k.from==f&&"bookmark"==l.type&&(!g||k.marker.insertLeft))m=null==k.from||(l.inclusiveLeft?k.from<=f:k.from<f),(n||(n=[])).push(new qc(l,m?null:k.from-f,null==k.to?null:k.to-f));d=n;g=1==b.text.length;n=A(b.text).length+(g?e:0);if(c)for(f=
+0;f<c.length;++f)if(h=c[f],null==h.to)(k=zb(d,h.marker),k)?g&&(h.to=null==k.to?null:k.to+n):h.to=e;if(d)for(f=0;f<d.length;++f)h=d[f],null!=h.to&&(h.to+=n),null==h.from?(k=zb(c,h.marker),k||(h.from=n,g&&(c||(c=[])).push(h))):(h.from+=n,g&&(c||(c=[])).push(h));c&&(c=Se(c));d&&d!=c&&(d=Se(d));e=[c];if(!g){var g=b.text.length-2,q;if(0<g&&c)for(f=0;f<c.length;++f)null==c[f].to&&(q||(q=[])).push(new qc(c[f].marker,null,null));for(f=0;f<g;++f)e.push(q);e.push(d)}return e}function Se(a){for(var b=0;b<a.length;++b){var c=
+a[b];null!=c.from&&c.from==c.to&&!1!==c.marker.clearWhenEmpty&&a.splice(b--,1)}return a.length?a:null}function Ke(a,b){var c;if(c=b["spans_"+a.id]){for(var d=0,e=[];d<b.text.length;++d)e.push(Zf(c[d]));c=e}else c=null;d=od(a,b);if(!c)return d;if(!d)return c;for(e=0;e<c.length;++e){var f=c[e],g=d[e];if(f&&g){var h=0;a:for(;h<g.length;++h){for(var k=g[h],l=0;l<f.length;++l)if(f[l].marker==k.marker)continue a;f.push(k)}}else g&&(c[e]=g)}return c}function Uf(a,b,c){var d=null;a.iter(b.line,c.line+1,function(a){if(a.markedSpans)for(var b=
+0;b<a.markedSpans.length;++b){var c=a.markedSpans[b].marker;!c.readOnly||d&&-1!=D(d,c)||(d||(d=[])).push(c)}});if(!d)return null;a=[{from:b,to:c}];for(b=0;b<d.length;++b){c=d[b];for(var e=c.find(0),f=0;f<a.length;++f){var g=a[f];if(!(0>y(g.to,e.from)||0<y(g.from,e.to))){var h=[f,1],k=y(g.from,e.from),l=y(g.to,e.to);(0>k||!c.inclusiveLeft&&!k)&&h.push({from:g.from,to:e.from});(0<l||!c.inclusiveRight&&!l)&&h.push({from:e.to,to:g.to});a.splice.apply(a,h);f+=h.length-1}}}return a}function Te(a){var b=
+a.markedSpans;if(b){for(var c=0;c<b.length;++c)b[c].marker.detachLine(a);a.markedSpans=null}}function Ue(a,b){if(b){for(var c=0;c<b.length;++c)b[c].marker.attachLine(a);a.markedSpans=b}}function Ve(a,b){var c=a.lines.length-b.lines.length;if(0!=c)return c;var c=a.find(),d=b.find(),e=y(c.from,d.from)||(a.inclusiveLeft?-1:0)-(b.inclusiveLeft?-1:0);return e?-e:(c=y(c.to,d.to)||(a.inclusiveRight?1:0)-(b.inclusiveRight?1:0))?c:b.id-a.id}function Aa(a,b){var c=ra&&a.markedSpans,d;if(c)for(var e,f=0;f<c.length;++f)e=
+c[f],e.marker.collapsed&&null==(b?e.from:e.to)&&(!d||0>Ve(d,e.marker))&&(d=e.marker);return d}function Qe(a,b,c,d,e){a=u(a,b);if(a=ra&&a.markedSpans)for(b=0;b<a.length;++b){var f=a[b];if(f.marker.collapsed){var g=f.marker.find(0),h=y(g.from,c)||(f.marker.inclusiveLeft?-1:0)-(e.inclusiveLeft?-1:0),k=y(g.to,d)||(f.marker.inclusiveRight?1:0)-(e.inclusiveRight?1:0);if(!(0<=h&&0>=k||0>=h&&0<=k)&&(0>=h&&(0<y(g.to,c)||f.marker.inclusiveRight&&e.inclusiveLeft)||0<=h&&(0>y(g.from,d)||f.marker.inclusiveLeft&&
+e.inclusiveRight)))return!0}}}function ia(a){for(var b;b=Aa(a,!0);)a=b.find(-1,!0).line;return a}function Mc(a,b){var c=u(a,b),d=ia(c);return c==d?b:F(d)}function Ld(a,b){if(b>a.lastLine())return b;var c=u(a,b),d;if(!ya(a,c))return b;for(;d=Aa(c,!1);)c=d.find(1,!0).line;return F(c)+1}function ya(a,b){var c=ra&&b.markedSpans;if(c)for(var d,e=0;e<c.length;++e)if(d=c[e],d.marker.collapsed&&(null==d.from||!d.marker.widgetNode&&0==d.from&&d.marker.inclusiveLeft&&td(a,b,d)))return!0}function td(a,b,c){if(null==
+c.to)return b=c.marker.find(1,!0),td(a,b.line,zb(b.line.markedSpans,c.marker));if(c.marker.inclusiveRight&&c.to==b.text.length)return!0;for(var d,e=0;e<b.markedSpans.length;++e)if(d=b.markedSpans[e],d.marker.collapsed&&!d.marker.widgetNode&&d.from==c.to&&(null==d.to||d.to!=c.from)&&(d.marker.inclusiveLeft||c.marker.inclusiveRight)&&td(a,b,d))return!0}function ub(a){if(null!=a.height)return a.height;var b=a.doc.cm;if(!b)return 0;if(!Wc(document.body,a.node)){var c="position: relative;";a.coverGutter&&
+(c+="margin-left: -"+b.display.gutters.offsetWidth+"px;");a.noHScroll&&(c+="width: "+b.display.wrapper.clientWidth+"px;");U(b.display.measure,t("div",[a.node],null,c))}return a.height=a.node.offsetHeight}function $f(a,b,c,d){var e=new sc(a,c,d),f=a.cm;f&&e.noHScroll&&(f.display.alignWidgets=!0);nc(a,b,"widget",function(b){var c=b.widgets||(b.widgets=[]);null==e.insertAt?c.push(e):c.splice(Math.min(c.length-1,Math.max(0,e.insertAt)),0,e);e.line=b;f&&!ya(a,b)&&(c=ea(b)<a.scrollTop,ca(b,b.height+ub(e)),
+c&&lc(f,null,e.height),f.curOp.forceUpdate=!0);return!0});return e}function We(a,b){if(a)for(;;){var c=a.match(/(?:^|\s+)line-(background-)?(\S+)/);if(!c)break;a=a.slice(0,c.index)+a.slice(c.index+c[0].length);var d=c[1]?"bgClass":"textClass";null==b[d]?b[d]=c[2]:(new RegExp("(?:^|s)"+c[2]+"(?:$|s)")).test(b[d])||(b[d]+=" "+c[2])}return a}function Xe(a,b){if(a.blankLine)return a.blankLine(b);if(a.innerMode){var c=q.innerMode(a,b);if(c.mode.blankLine)return c.mode.blankLine(c.state)}}function ud(a,
+b,c,d){for(var e=0;10>e;e++){d&&(d[0]=q.innerMode(a,c).mode);var f=a.token(b,c);if(b.pos>b.start)return f}throw Error("Mode "+a.name+" failed to advance stream.");}function Ye(a,b,c,d){function e(a){return{start:m.start,end:m.pos,string:m.current(),type:h||null,state:a?Sa(f.mode,l):l}}var f=a.doc,g=f.mode,h;b=w(f,b);var k=u(f,b.line),l=sb(a,b.line,c),m=new tc(k.text,a.options.tabSize),p;for(d&&(p=[]);(d||m.pos<b.ch)&&!m.eol();)m.start=m.pos,h=ud(g,m,l),d&&p.push(e(!0));return d?p:e()}function Ze(a,
+b,c,d,e,f,g){var h=c.flattenSpans;null==h&&(h=a.options.flattenSpans);var k=0,l=null,m=new tc(b,a.options.tabSize),p,n=a.options.addModeClass&&[null];for(""==b&&We(Xe(c,d),f);!m.eol();){m.pos>a.options.maxHighlightLength?(h=!1,g&&Zc(a,b,d,m.pos),m.pos=b.length,p=null):p=We(ud(c,m,d,n),f);if(n){var q=n[0].name;q&&(p="m-"+(p?q+" "+p:q))}if(!h||l!=p){for(;k<m.start;)k=Math.min(m.start,k+5E4),e(k,l);l=p}m.start=m.pos}for(;k<m.pos;)a=Math.min(m.pos,k+5E4),e(a,l),k=a}function je(a,b,c,d){var e=[a.state.modeGen],
+f={};Ze(a,b.text,a.doc.mode,c,function(a,b){e.push(a,b)},f,d);for(c=0;c<a.state.overlays.length;++c){var g=a.state.overlays[c],h=1,k=0;Ze(a,b.text,g.mode,!0,function(a,b){for(var c=h;k<a;){var d=e[h];d>a&&e.splice(h,1,a,e[h+1],d);h+=2;k=Math.min(a,d)}if(b)if(g.opaque)e.splice(c,h-c,a,"cm-overlay "+b),h=c+2;else for(;c<h;c+=2)d=e[c+1],e[c+1]=(d?d+" ":"")+"cm-overlay "+b},f)}return{styles:e,classes:f.bgClass||f.textClass?f:null}}function $e(a,b,c){if(!b.styles||b.styles[0]!=a.state.modeGen){var d=je(a,
+b,b.stateAfter=sb(a,F(b)));b.styles=d.styles;d.classes?b.styleClasses=d.classes:b.styleClasses&&(b.styleClasses=null);c===a.doc.frontier&&a.doc.frontier++}return b.styles}function Zc(a,b,c,d){var e=a.doc.mode,f=new tc(b,a.options.tabSize);f.start=f.pos=d||0;for(""==b&&Xe(e,c);!f.eol()&&f.pos<=a.options.maxHighlightLength;)ud(e,f,c),f.start=f.pos}function af(a,b){if(!a||/^\s*$/.test(a))return null;var c=b.addModeClass?ag:bg;return c[a]||(c[a]=a.replace(/\S+/g,"cm-$\x26"))}function Sd(a,b){var c=t("span",
+null,null,J?"padding-right: .1px":null),c={pre:t("pre",[c]),content:c,col:0,pos:0,cm:a,splitSpaces:(B||J)&&a.getOption("lineWrapping")};b.measure={};for(var d=0;d<=(b.rest?b.rest.length:0);d++){var e=d?b.rest[d-1]:b.line,f;c.pos=0;c.addToken=cg;var g;if(null!=vd)g=vd;else{g=U(a.display.measure,document.createTextNode("AخA"));var h=Ea(g,0,1).getBoundingClientRect();g=h&&h.left!=h.right?vd=3>Ea(g,1,2).getBoundingClientRect().right-h.right:!1}g&&(f=Y(e))&&(c.addToken=dg(c.addToken,f));c.map=[];h=b!=
+a.display.externalMeasured&&F(e);a:{g=c;var h=$e(a,e,h),k=e.markedSpans,l=e.text,m=0;if(k)for(var p=l.length,n=0,q=1,r="",u=void 0,v=void 0,w=0,x=void 0,y=void 0,A=void 0,C=void 0,z=void 0;;){if(w==n){for(var x=y=A=C=v="",z=null,w=Infinity,G=[],H=0;H<k.length;++H){var I=k[H],D=I.marker;"bookmark"==D.type&&I.from==n&&D.widgetNode?G.push(D):I.from<=n&&(null==I.to||I.to>n||D.collapsed&&I.to==n&&I.from==n)?(null!=I.to&&I.to!=n&&w>I.to&&(w=I.to,y=""),D.className&&(x+=" "+D.className),D.css&&(v=D.css),
+D.startStyle&&I.from==n&&(A+=" "+D.startStyle),D.endStyle&&I.to==w&&(y+=" "+D.endStyle),D.title&&!C&&(C=D.title),D.collapsed&&(!z||0>Ve(z.marker,D))&&(z=I)):I.from>n&&w>I.from&&(w=I.from)}if(z&&(z.from||0)==n){bf(g,(null==z.to?p+1:z.to)-n,z.marker,null==z.from);if(null==z.to)break a;z.to==n&&(z=!1)}if(!z&&G.length)for(H=0;H<G.length;++H)bf(g,0,G[H])}if(n>=p)break;for(G=Math.min(p,w);;){if(r){H=n+r.length;z||(I=H>G?r.slice(0,G-n):r,g.addToken(g,I,u?u+x:x,A,n+I.length==w?y:"",C,v));if(H>=G){r=r.slice(G-
+n);n=G;break}n=H;A=""}r=l.slice(m,m=h[q++]);u=af(h[q++],g.cm.options)}}else for(var q=1;q<h.length;q+=2)g.addToken(g,l.slice(m,m=h[q]),af(h[q+1],g.cm.options))}e.styleClasses&&(e.styleClasses.bgClass&&(c.bgClass=wd(e.styleClasses.bgClass,c.bgClass||"")),e.styleClasses.textClass&&(c.textClass=wd(e.styleClasses.textClass,c.textClass||"")));0==c.map.length&&c.map.push(0,0,c.content.appendChild(eg(a.display.measure)));0==d?(b.measure.map=c.map,b.measure.cache={}):((b.measure.maps||(b.measure.maps=[])).push(c.map),
+(b.measure.caches||(b.measure.caches=[])).push({}))}J&&/\bcm-tab\b/.test(c.content.lastChild.className)&&(c.content.className="cm-tab-wrap-hack");K(a,"renderLine",a,b.line,c.pre);c.pre.className&&(c.textClass=wd(c.pre.className,c.textClass||""));return c}function cg(a,b,c,d,e,f,g){if(b){var h=a.splitSpaces?b.replace(/ {3,}/g,fg):b,k=a.cm.state.specialChars,l=!1;if(k.test(b))for(var m=document.createDocumentFragment(),p=0;;){k.lastIndex=p;var n=k.exec(b),q=n?n.index-p:b.length-p;if(q){var r=document.createTextNode(h.slice(p,
+p+q));B&&9>C?m.appendChild(t("span",[r])):m.appendChild(r);a.map.push(a.pos,a.pos+q,r);a.col+=q;a.pos+=q}if(!n)break;p+=q+1;"\t"==n[0]?(r=a.cm.options.tabSize,n=r-a.col%r,r=m.appendChild(t("span",Ne(n),"cm-tab")),r.setAttribute("role","presentation"),r.setAttribute("cm-text","\t"),a.col+=n):(r=a.cm.options.specialCharPlaceholder(n[0]),r.setAttribute("cm-text",n[0]),B&&9>C?m.appendChild(t("span",[r])):m.appendChild(r),a.col+=1);a.map.push(a.pos,a.pos+1,r);a.pos++}else{a.col+=b.length;var m=document.createTextNode(h);
+a.map.push(a.pos,a.pos+b.length,m);B&&9>C&&(l=!0);a.pos+=b.length}if(c||d||e||l||g)return b=c||"",d&&(b+=d),e&&(b+=e),d=t("span",[m],b,g),f&&(d.title=f),a.content.appendChild(d);a.content.appendChild(m)}}function fg(a){for(var b=" ",c=0;c<a.length-2;++c)b+=c%2?" ":" ";return b+" "}function dg(a,b){return function(c,d,e,f,g,h,k){e=e?e+" cm-force-border":"cm-force-border";for(var l=c.pos,m=l+d.length;;){for(var p=0;p<b.length;p++){var n=b[p];if(n.to>l&&n.from<=l)break}if(n.to>=m)return a(c,d,e,f,g,
+h,k);a(c,d.slice(0,n.to-l),e,f,null,h,k);f=null;d=d.slice(n.to-l);l=n.to}}}function bf(a,b,c,d){var e=!d&&c.widgetNode;e&&a.map.push(a.pos,a.pos+b,e);!d&&a.cm.display.input.needsContentAttribute&&(e||(e=a.content.appendChild(document.createElement("span"))),e.setAttribute("cm-marker",c.id));e&&(a.cm.display.input.setUneditable(e),a.content.appendChild(e));a.pos+=b}function Me(a,b){return 0==b.from.ch&&0==b.to.ch&&""==A(b.text)&&(!a.cm||a.cm.options.wholeLineUpdateBefore)}function qd(a,b,c,d){function e(a,
+c,e){a.text=c;a.stateAfter&&(a.stateAfter=null);a.styles&&(a.styles=null);null!=a.order&&(a.order=null);Te(a);Ue(a,e);c=d?d(a):1;c!=a.height&&ca(a,c);L(a,"change",a,b)}function f(a,b){for(var e=a,f=[];e<b;++e)f.push(new Ab(k[e],c?c[e]:null,d));return f}var g=b.from,h=b.to,k=b.text,l=u(a,g.line),m=u(a,h.line),p=A(k),n=c?c[k.length-1]:null,q=h.line-g.line;if(b.full)a.insert(0,f(0,k.length)),a.remove(k.length,a.size-k.length);else if(Me(a,b)){var r=f(0,k.length-1);e(m,m.text,n);q&&a.remove(g.line,q);
+r.length&&a.insert(g.line,r)}else l==m?1==k.length?e(l,l.text.slice(0,g.ch)+p+l.text.slice(h.ch),n):(r=f(1,k.length-1),r.push(new Ab(p+l.text.slice(h.ch),n,d)),e(l,l.text.slice(0,g.ch)+k[0],c?c[0]:null),a.insert(g.line+1,r)):1==k.length?(e(l,l.text.slice(0,g.ch)+k[0]+m.text.slice(h.ch),c?c[0]:null),a.remove(g.line+1,q)):(e(l,l.text.slice(0,g.ch)+k[0],c?c[0]:null),e(m,p+m.text.slice(h.ch),n),r=f(1,k.length-1),1<q&&a.remove(g.line+1,q-1),a.insert(g.line+1,r));L(a,"change",a,b)}function Bb(a){this.lines=
+a;this.parent=null;for(var b=0,c=0;b<a.length;++b)a[b].parent=this,c+=a[b].height;this.height=c}function Cb(a){this.children=a;for(var b=0,c=0,d=0;d<a.length;++d){var e=a[d],b=b+e.chunkSize(),c=c+e.height;e.parent=this}this.size=b;this.height=c;this.parent=null}function Ga(a,b,c){function d(a,f,g){if(a.linked)for(var h=0;h<a.linked.length;++h){var k=a.linked[h];if(k.doc!=f){var l=g&&k.sharedHist;if(!c||l)b(k.doc,l),d(k.doc,a,l)}}}d(a,null,!0)}function Ed(a,b){if(b.cm)throw Error("This document is already in use.");
+a.doc=b;b.cm=a;Ac(a);zc(a);a.options.lineWrapping||Dc(a);a.options.mode=b.modeOption;Q(a)}function u(a,b){b-=a.first;if(0>b||b>=a.size)throw Error("There is no line "+(b+a.first)+" in the document.");for(var c=a;!c.lines;)for(var d=0;;++d){var e=c.children[d],f=e.chunkSize();if(b<f){c=e;break}b-=f}return c.lines[b]}function Da(a,b,c){var d=[],e=b.line;a.iter(b.line,c.line+1,function(a){a=a.text;e==c.line&&(a=a.slice(0,c.ch));e==b.line&&(a=a.slice(b.ch));d.push(a);++e});return d}function xd(a,b,c){var d=
+[];a.iter(b,c,function(a){d.push(a.text)});return d}function ca(a,b){var c=b-a.height;if(c)for(var d=a;d;d=d.parent)d.height+=c}function F(a){if(null==a.parent)return null;var b=a.parent;a=D(b.lines,a);for(var c=b.parent;c;b=c,c=c.parent)for(var d=0;c.children[d]!=b;++d)a+=c.children[d].chunkSize();return a+b.first}function Ba(a,b){var c=a.first;a:do{for(var d=0;d<a.children.length;++d){var e=a.children[d],f=e.height;if(b<f){a=e;continue a}b-=f;c+=e.chunkSize()}return c}while(!a.lines);for(d=0;d<
+a.lines.length;++d){e=a.lines[d].height;if(b<e)break;b-=e}return c+d}function ea(a){a=ia(a);for(var b=0,c=a.parent,d=0;d<c.lines.length;++d){var e=c.lines[d];if(e==a)break;else b+=e.height}for(a=c.parent;a;c=a,a=c.parent)for(d=0;d<a.children.length&&(e=a.children[d],e!=c);++d)b+=e.height;return b}function Y(a){var b=a.order;null==b&&(b=a.order=gg(a.text));return b}function uc(a){this.done=[];this.undone=[];this.undoDepth=Infinity;this.lastModTime=this.lastSelTime=0;this.lastOrigin=this.lastSelOrigin=
+this.lastOp=this.lastSelOp=null;this.generation=this.maxGeneration=a||1}function pd(a,b){var c={from:Rc(b.from),to:ta(b),text:Da(a,b.from,b.to)};cf(a,c,b.from.line,b.to.line+1);Ga(a,function(a){cf(a,c,b.from.line,b.to.line+1)},!0);return c}function ce(a){for(;a.length;)if(A(a).ranges)a.pop();else break}function Ie(a,b,c,d){var e=a.history;e.undone.length=0;var f=+new Date,g,h;if(h=e.lastOp==d||e.lastOrigin==b.origin&&b.origin&&("+"==b.origin.charAt(0)&&a.cm&&e.lastModTime>f-a.cm.options.historyEventDelay||
+"*"==b.origin.charAt(0)))e.lastOp==d?(ce(e.done),g=A(e.done)):e.done.length&&!A(e.done).ranges?g=A(e.done):1<e.done.length&&!e.done[e.done.length-2].ranges?(e.done.pop(),g=A(e.done)):g=void 0,h=g;if(h){var k=A(g.changes);0==y(b.from,b.to)&&0==y(b.from,k.to)?k.to=ta(b):g.changes.push(pd(a,b))}else for((g=A(e.done))&&g.ranges||Wb(a.sel,e.done),g={changes:[pd(a,b)],generation:e.generation},e.done.push(g);e.done.length>e.undoDepth;)e.done.shift(),e.done[0].ranges||e.done.shift();e.done.push(c);e.generation=
+++e.maxGeneration;e.lastModTime=e.lastSelTime=f;e.lastOp=e.lastSelOp=d;e.lastOrigin=e.lastSelOrigin=b.origin;k||K(a,"historyAdded")}function Wb(a,b){var c=A(b);c&&c.ranges&&c.equals(a)||b.push(a)}function cf(a,b,c,d){var e=b["spans_"+a.id],f=0;a.iter(Math.max(a.first,c),Math.min(a.first+a.size,d),function(c){c.markedSpans&&((e||(e=b["spans_"+a.id]={}))[f]=c.markedSpans);++f})}function Zf(a){if(!a)return null;for(var b=0,c;b<a.length;++b)a[b].marker.explicitlyCleared?c||(c=a.slice(0,b)):c&&c.push(a[b]);
+return c?c.length?c:null:a}function Xa(a,b,c){for(var d=0,e=[];d<a.length;++d){var f=a[d];if(f.ranges)e.push(c?la.prototype.deepCopy.call(f):f);else{var f=f.changes,g=[];e.push({changes:g});for(var h=0;h<f.length;++h){var k=f[h],l;g.push({from:k.from,to:k.to,text:k.text});if(b)for(var m in k)(l=m.match(/^spans_(\d+)$/))&&-1<D(b,Number(l[1]))&&(A(g)[m]=k[m],delete k[m])}}}return e}function df(a,b,c,d){c<a.line?a.line+=d:b<a.line&&(a.line=b,a.ch=0)}function ef(a,b,c,d){for(var e=0;e<a.length;++e){var f=
+a[e],g=!0;if(f.ranges){f.copied||(f=a[e]=f.deepCopy(),f.copied=!0);for(var h=0;h<f.ranges.length;h++)df(f.ranges[h].anchor,b,c,d),df(f.ranges[h].head,b,c,d)}else{for(h=0;h<f.changes.length;++h){var k=f.changes[h];if(c<k.from.line)k.from=r(k.from.line+d,k.from.ch),k.to=r(k.to.line+d,k.to.ch);else if(b<=k.to.line){g=!1;break}}g||(a.splice(0,e+1),e=0)}}}function Je(a,b){var c=b.from.line,d=b.to.line,e=b.text.length-(d-c)-1;ef(a.done,c,d,e);ef(a.undone,c,d,e)}function ld(a){return null!=a.defaultPrevented?
+a.defaultPrevented:0==a.returnValue}function xe(a){var b=a.which;null==b&&(a.button&1?b=1:a.button&2?b=3:a.button&4&&(b=2));W&&a.ctrlKey&&1==b&&(b=3);return b}function L(a,b){function c(a){return function(){a.apply(null,e)}}var d=a._handlers&&a._handlers[b];if(d){var e=Array.prototype.slice.call(arguments,2),f;Ta?f=Ta.delayedCallbacks:Db?f=Db:(f=Db=[],setTimeout(hg,0));for(var g=0;g<d.length;++g)f.push(c(d[g]))}}function hg(){var a=Db;Db=null;for(var b=0;b<a.length;++b)a[b]()}function ja(a,b,c){"string"==
+typeof b&&(b={type:b,preventDefault:function(){this.defaultPrevented=!0}});K(a,c||b.type,a,b);return ld(b)||b.codemirrorIgnore}function fe(a){var b=a._handlers&&a._handlers.cursorActivity;if(b){a=a.curOp.cursorActivityHandlers||(a.curOp.cursorActivityHandlers=[]);for(var c=0;c<b.length;++c)-1==D(a,b[c])&&a.push(b[c])}}function S(a,b){var c=a._handlers&&a._handlers[b];return c&&0<c.length}function Ya(a){a.prototype.on=function(a,c){v(this,a,c)};a.prototype.off=function(a,c){ka(this,a,c)}}function bb(){this.id=
+null}function ye(a,b,c){for(var d=0,e=0;;){var f=a.indexOf("\t",d);-1==f&&(f=a.length);var g=f-d;if(f==a.length||e+g>=b)return d+Math.min(g,b-e);e+=f-d;e+=c-e%c;d=f+1;if(e>=b)return d}}function Ne(a){for(;vc.length<=a;)vc.push(A(vc)+" ");return vc[a]}function A(a){return a[a.length-1]}function D(a,b){for(var c=0;c<a.length;++c)if(a[c]==b)return c;return-1}function ob(a,b){for(var c=[],d=0;d<a.length;d++)c[d]=b(a[d],d);return c}function Eb(){}function ff(a,b){var c;Object.create?c=Object.create(a):
+(Eb.prototype=a,c=new Eb);b&&V(b,c);return c}function V(a,b,c){b||(b={});for(var d in a)!a.hasOwnProperty(d)||!1===c&&b.hasOwnProperty(d)||(b[d]=a[d]);return b}function cb(a){var b=Array.prototype.slice.call(arguments,1);return function(){return a.apply(null,b)}}function oc(a,b){return b?-1<b.source.indexOf("\\w")&&gf(a)?!0:b.test(a):gf(a)}function hf(a){for(var b in a)if(a.hasOwnProperty(b)&&a[b])return!1;return!0}function tb(a){return 768<=a.charCodeAt(0)&&ig.test(a)}function t(a,b,c,d){a=document.createElement(a);
+c&&(a.className=c);d&&(a.style.cssText=d);if("string"==typeof b)a.appendChild(document.createTextNode(b));else if(b)for(c=0;c<b.length;++c)a.appendChild(b[c]);return a}function za(a){for(var b=a.childNodes.length;0<b;--b)a.removeChild(a.firstChild);return a}function U(a,b){return za(a).appendChild(b)}function fa(){return document.activeElement}function Fb(a){return new RegExp("(^|\\s)"+a+"(?:$|\\s)\\s*")}function wd(a,b){for(var c=a.split(" "),d=0;d<c.length;d++)c[d]&&!Fb(c[d]).test(b)&&(b+=" "+c[d]);
+return b}function jf(a){if(document.body.getElementsByClassName)for(var b=document.body.getElementsByClassName("CodeMirror"),c=0;c<b.length;c++){var d=b[c].CodeMirror;d&&a(d)}}function tf(){var a;v(window,"resize",function(){null==a&&(a=setTimeout(function(){a=null;jf(If)},100))});v(window,"blur",function(){jf(db)})}function eg(a){if(null==yd){var b=t("span","​");U(a,t("span",[b,document.createTextNode("x")]));0!=a.firstChild.offsetHeight&&(yd=1>=b.offsetWidth&&2<b.offsetHeight&&!(B&&8>C))}a=yd?t("span",
+"​"):t("span"," ",null,"display: inline-block; width: 1px; margin-right: -1px");a.setAttribute("cm-text","");return a}function Af(a,b,c,d){if(!a)return d(b,c,"ltr");for(var e=!1,f=0;f<a.length;++f){var g=a[f];if(g.from<c&&g.to>b||b==c&&g.to==b)d(Math.max(g.from,b),Math.min(g.to,c),1==g.level?"rtl":"ltr"),e=!0}e||d(b,c,"ltr")}function dd(a){return a.level%2?a.to:a.from}function ed(a){return a.level%2?a.from:a.to}function ac(a){return(a=Y(a))?dd(a[0]):0}function bc(a){var b=Y(a);return b?ed(A(b)):a.text.length}
+function kf(a,b){var c=u(a.doc,b),d=ia(c);d!=c&&(b=F(d));d=(c=Y(d))?c[0].level%2?bc(d):ac(d):0;return r(b,d)}function lf(a,b){var c=kf(a,b.line),d=u(a.doc,c.line),e=Y(d);return e&&0!=e[0].level?c:(d=Math.max(0,d.text.search(/\S/)),r(c.line,b.line==c.line&&b.ch<=d&&b.ch?0:d))}function Sb(a,b){vb=null;for(var c=0,d;c<a.length;++c){var e=a[c];if(e.from<b&&e.to>b)return c;if(e.from==b||e.to==b)if(null==d)d=c;else{var f;f=e.level;var g=a[d].level,h=a[0].level;f=f==h?!0:g==h?!1:f<g;if(f)return e.from!=
+e.to&&(vb=d),c;e.from!=e.to&&(vb=c);break}}return d}function zd(a,b,c,d){if(!d)return b+c;do b+=c;while(0<b&&tb(a.text.charAt(b)));return b}function gd(a,b,c,d){var e=Y(a);if(!e)return Oe(a,b,c,d);var f=Sb(e,b),g=e[f];for(b=zd(a,b,g.level%2?-c:c,d);;){if(b>g.from&&b<g.to)return b;if(b==g.from||b==g.to){if(Sb(e,b)==f)return b;g=e[f+c];return 0<c==g.level%2?g.to:g.from}g=e[f+=c];if(!g)return null;b=0<c==g.level%2?zd(a,g.to,-1,d):zd(a,g.from,1,d)}}function Oe(a,b,c,d){b+=c;if(d)for(;0<b&&tb(a.text.charAt(b));)b+=
+c;return 0>b||b>a.text.length?null:b}var wa=/gecko\/\d/i.test(navigator.userAgent),mf=/MSIE \d/.test(navigator.userAgent),nf=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent),B=mf||nf,C=B&&(mf?document.documentMode||6:nf[1]),J=/WebKit\//.test(navigator.userAgent),jg=J&&/Qt\/\d+\.\d+/.test(navigator.userAgent),kg=/Chrome\//.test(navigator.userAgent),ba=/Opera\//.test(navigator.userAgent),te=/Apple Computer/.test(navigator.vendor),lg=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent),
+Gf=/PhantomJS/.test(navigator.userAgent),Qa=/AppleWebKit/.test(navigator.userAgent)&&/Mobile\/\w+/.test(navigator.userAgent),ab=Qa||/Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent),W=Qa||/Mac/.test(navigator.platform),mg=/win/i.test(navigator.platform),Ia=ba&&navigator.userAgent.match(/Version\/(\d*\.\d*)/);Ia&&(Ia=Number(Ia[1]));Ia&&15<=Ia&&(ba=!1,J=!0);var of=W&&(jg||ba&&(null==Ia||12.11>Ia)),id=wa||B&&9<=C,Ge=!1,ra=!1;Fc.prototype=V({update:function(a){var b=
+a.scrollWidth>a.clientWidth+1,c=a.scrollHeight>a.clientHeight+1,d=a.nativeBarWidth;c?(this.vert.style.display="block",this.vert.style.bottom=b?d+"px":"0",this.vert.firstChild.style.height=Math.max(0,a.scrollHeight-a.clientHeight+(a.viewHeight-(b?d:0)))+"px"):(this.vert.style.display="",this.vert.firstChild.style.height="0");b?(this.horiz.style.display="block",this.horiz.style.right=c?d+"px":"0",this.horiz.style.left=a.barLeft+"px",this.horiz.firstChild.style.width=a.scrollWidth-a.clientWidth+(a.viewWidth-
+a.barLeft-(c?d:0))+"px"):(this.horiz.style.display="",this.horiz.firstChild.style.width="0");!this.checkedOverlay&&0<a.clientHeight&&(0==d&&this.overlayHack(),this.checkedOverlay=!0);return{right:c?d:0,bottom:b?d:0}},setScrollLeft:function(a){this.horiz.scrollLeft!=a&&(this.horiz.scrollLeft=a)},setScrollTop:function(a){this.vert.scrollTop!=a&&(this.vert.scrollTop=a)},overlayHack:function(){this.horiz.style.minHeight=this.vert.style.minWidth=W&&!lg?"12px":"18px";var a=this,b=function(b){(b.target||
+b.srcElement)!=a.vert&&(b.target||b.srcElement)!=a.horiz&&G(a.cm,pe)(b)};v(this.vert,"mousedown",b);v(this.horiz,"mousedown",b)},clear:function(){var a=this.horiz.parentNode;a.removeChild(this.horiz);a.removeChild(this.vert)}},Fc.prototype);Gc.prototype=V({update:function(){return{bottom:0,right:0}},setScrollLeft:function(){},setScrollTop:function(){},clear:function(){}},Gc.prototype);q.scrollbarModel={"native":Fc,"null":Gc};Mb.prototype.signal=function(a,b){S(a,b)&&this.events.push(arguments)};Mb.prototype.finish=
+function(){for(var a=0;a<this.events.length;a++)K.apply(null,this.events[a])};var r=q.Pos=function(a,b){if(!(this instanceof r))return new r(a,b);this.line=a;this.ch=b},y=q.cmpPos=function(a,b){return a.line-b.line||a.ch-b.ch},X=null;Tc.prototype=V({init:function(a){function b(a){if(d.somethingSelected())X=d.getSelections(),c.inaccurateSelection&&(c.prevInput="",c.inaccurateSelection=!1,f.value=X.join("\n"),Za(f));else if(d.options.lineWiseCopyCut){var b=Vd(d);X=b.text;"cut"==a.type?d.setSelections(b.ranges,
+null,ha):(c.prevInput="",f.value=b.text.join("\n"),Za(f))}else return;"cut"==a.type&&(d.state.cutIncoming=!0)}var c=this,d=this.cm,e=this.wrapper=Xd(),f=this.textarea=e.firstChild;a.wrapper.insertBefore(e,a.wrapper.firstChild);Qa&&(f.style.width="0px");v(f,"input",function(){B&&9<=C&&c.hasSelection&&(c.hasSelection=null);c.poll()});v(f,"paste",function(){if(J&&!d.state.fakedLastChar&&!(200>new Date-d.state.lastMiddleDown)){var a=f.selectionStart,b=f.selectionEnd;f.value+="$";f.selectionEnd=b;f.selectionStart=
+a;d.state.fakedLastChar=!0}d.state.pasteIncoming=!0;c.fastPoll()});v(f,"cut",b);v(f,"copy",b);v(a.scroller,"paste",function(b){oa(a,b)||(d.state.pasteIncoming=!0,c.focus())});v(a.lineSpace,"selectstart",function(b){oa(a,b)||O(b)});v(f,"compositionstart",function(){var a=d.getCursor("from");c.composing={start:a,range:d.markText(a,d.getCursor("to"),{className:"CodeMirror-composing"})}});v(f,"compositionend",function(){c.composing&&(c.poll(),c.composing.range.clear(),c.composing=null)})},prepareSelection:function(){var a=
+this.cm,b=a.display,c=a.doc,d=he(a);if(a.options.moveInputWithCursor){var a=ma(a,c.sel.primary().head,"div"),c=b.wrapper.getBoundingClientRect(),e=b.lineDiv.getBoundingClientRect();d.teTop=Math.max(0,Math.min(b.wrapper.clientHeight-10,a.top+e.top-c.top));d.teLeft=Math.max(0,Math.min(b.wrapper.clientWidth-10,a.left+e.left-c.left))}return d},showSelection:function(a){var b=this.cm.display;U(b.cursorDiv,a.cursors);U(b.selectionDiv,a.selection);null!=a.teTop&&(this.wrapper.style.top=a.teTop+"px",this.wrapper.style.left=
+a.teLeft+"px")},reset:function(a){if(!this.contextMenuPending){var b,c,d=this.cm,e=d.doc;d.somethingSelected()?(this.prevInput="",b=e.sel.primary(),c=(b=Ce&&(100<b.to().line-b.from().line||1E3<(c=d.getSelection()).length))?"-":c||d.getSelection(),this.textarea.value=c,d.state.focused&&Za(this.textarea),B&&9<=C&&(this.hasSelection=c)):a||(this.prevInput=this.textarea.value="",B&&9<=C&&(this.hasSelection=null));this.inaccurateSelection=b}},getField:function(){return this.textarea},supportsTouch:function(){return!1},
+focus:function(){if("nocursor"!=this.cm.options.readOnly&&(!ab||fa()!=this.textarea))try{this.textarea.focus()}catch(a){}},blur:function(){this.textarea.blur()},resetPosition:function(){this.wrapper.style.top=this.wrapper.style.left=0},receivedFocus:function(){this.slowPoll()},slowPoll:function(){var a=this;a.pollingFast||a.polling.set(this.cm.options.pollInterval,function(){a.poll();a.cm.state.focused&&a.slowPoll()})},fastPoll:function(){function a(){c.poll()||b?(c.pollingFast=!1,c.slowPoll()):(b=
+!0,c.polling.set(60,a))}var b=!1,c=this;c.pollingFast=!0;c.polling.set(20,a)},poll:function(){var a=this.cm,b=this.textarea,c=this.prevInput;if(!a.state.focused||ng(b)&&!c||Rb(a)||a.options.disableInput||a.state.keySeq)return!1;a.state.pasteIncoming&&a.state.fakedLastChar&&(b.value=b.value.substring(0,b.value.length-1),a.state.fakedLastChar=!1);var d=b.value;if(d==c&&!a.somethingSelected())return!1;if(B&&9<=C&&this.hasSelection===d||W&&/[\uf700-\uf7ff]/.test(d))return a.display.input.reset(),!1;if(a.doc.sel==
+a.display.selForContextMenu){var e=d.charCodeAt(0);8203!=e||c||(c="​");if(8666==e)return this.reset(),this.cm.execCommand("undo")}for(var f=0,e=Math.min(c.length,d.length);f<e&&c.charCodeAt(f)==d.charCodeAt(f);)++f;var g=this;T(a,function(){Sc(a,d.slice(f),c.length-f,null,g.composing?"*compose":null);1E3<d.length||-1<d.indexOf("\n")?b.value=g.prevInput="":g.prevInput=d;g.composing&&(g.composing.range.clear(),g.composing.range=a.markText(g.composing.start,a.getCursor("to"),{className:"CodeMirror-composing"}))});
+return!0},ensurePolled:function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},onKeyPress:function(){B&&9<=C&&(this.hasSelection=null);this.fastPoll()},onContextMenu:function(a){function b(){if(null!=g.selectionStart){var a=e.somethingSelected(),b="​"+(a?g.value:"");g.value="⇚";g.value=b;d.prevInput=a?"":"​";g.selectionStart=1;g.selectionEnd=b.length;f.selForContextMenu=e.doc.sel}}function c(){d.contextMenuPending=!1;d.wrapper.style.position="relative";g.style.cssText=l;B&&9>C&&f.scrollbars.setScrollTop(f.scroller.scrollTop=
+k);if(null!=g.selectionStart){(!B||B&&9>C)&&b();var a=0,c=function(){f.selForContextMenu==e.doc.sel&&0==g.selectionStart&&0<g.selectionEnd&&"​"==d.prevInput?G(e,ic.selectAll)(e):10>a++?f.detectingSelectAll=setTimeout(c,500):f.input.reset()};f.detectingSelectAll=setTimeout(c,200)}}var d=this,e=d.cm,f=e.display,g=d.textarea,h=Ua(e,a),k=f.scroller.scrollTop;if(h&&!ba){e.options.resetSelectionOnContextMenu&&-1==e.doc.sel.contains(h)&&G(e,H)(e.doc,ga(h),ha);var l=g.style.cssText;d.wrapper.style.position=
+"absolute";g.style.cssText="position: fixed; width: 30px; height: 30px; top: "+(a.clientY-5)+"px; left: "+(a.clientX-5)+"px; z-index: 1000; background: "+(B?"rgba(255, 255, 255, .05)":"transparent")+"; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity\x3d5);";if(J)var m=window.scrollY;f.input.focus();J&&window.scrollTo(null,m);f.input.reset();e.somethingSelected()||(g.value=d.prevInput=" ");d.contextMenuPending=!0;f.selForContextMenu=e.doc.sel;clearTimeout(f.detectingSelectAll);
+B&&9<=C&&b();if(id){jd(a);var p=function(){ka(window,"mouseup",p);setTimeout(c,20)};v(window,"mouseup",p)}else setTimeout(c,50)}},setUneditable:Eb,needsContentAttribute:!1},Tc.prototype);Uc.prototype=V({init:function(a){function b(a){if(d.somethingSelected())X=d.getSelections(),"cut"==a.type&&d.replaceSelection("",null,"cut");else if(d.options.lineWiseCopyCut){var b=Vd(d);X=b.text;"cut"==a.type&&d.operation(function(){d.setSelections(b.ranges,0,ha);d.replaceSelection("",null,"cut")})}else return;
+if(a.clipboardData&&!Qa)a.preventDefault(),a.clipboardData.clearData(),a.clipboardData.setData("text/plain",X.join("\n"));else{var c=Xd();a=c.firstChild;d.display.lineSpace.insertBefore(c,d.display.lineSpace.firstChild);a.value=X.join("\n");var h=document.activeElement;Za(a);setTimeout(function(){d.display.lineSpace.removeChild(c);h.focus()},50)}}var c=this,d=c.cm;a=c.div=a.lineDiv;a.contentEditable="true";Wd(a);v(a,"paste",function(a){var b=a.clipboardData&&a.clipboardData.getData("text/plain");
+b&&(a.preventDefault(),d.replaceSelection(b,null,"paste"))});v(a,"compositionstart",function(a){a=a.data;c.composing={sel:d.doc.sel,data:a,startData:a};if(a){var b=d.doc.sel.primary(),g=d.getLine(b.head.line).indexOf(a,Math.max(0,b.head.ch-a.length));-1<g&&g<=b.head.ch&&(c.composing.sel=ga(r(b.head.line,g),r(b.head.line,g+a.length)))}});v(a,"compositionupdate",function(a){c.composing.data=a.data});v(a,"compositionend",function(a){var b=c.composing;b&&(a.data==b.startData||/\u200b/.test(a.data)||(b.data=
+a.data),setTimeout(function(){b.handled||c.applyComposition(b);c.composing==b&&(c.composing=null)},50))});v(a,"touchstart",function(){c.forceCompositionEnd()});v(a,"input",function(){c.composing||c.pollContent()||T(c.cm,function(){Q(d)})});v(a,"copy",b);v(a,"cut",b)},prepareSelection:function(){var a=he(this.cm,!1);a.focus=this.cm.state.focused;return a},showSelection:function(a){a&&this.cm.display.view.length&&(a.focus&&this.showPrimarySelection(),this.showMultipleSelections(a))},showPrimarySelection:function(){var a=
+window.getSelection(),b=this.cm.doc.sel.primary(),c=Tb(this.cm,a.anchorNode,a.anchorOffset),d=Tb(this.cm,a.focusNode,a.focusOffset);if(!c||c.bad||!d||d.bad||0!=y(Qb(c,d),b.from())||0!=y(Pb(c,d),b.to()))if(c=Yd(this.cm,b.from()),d=Yd(this.cm,b.to()),c||d){var e=this.cm.display.view,b=a.rangeCount&&a.getRangeAt(0);c?d||(d=e[e.length-1].measure,d=d.maps?d.maps[d.maps.length-1]:d.map,d={node:d[d.length-1],offset:d[d.length-2]-d[d.length-3]}):c={node:e[0].measure.map[2],offset:0};try{var f=Ea(c.node,c.offset,
+d.offset,d.node)}catch(g){}f&&(a.removeAllRanges(),a.addRange(f),b&&null==a.anchorNode?a.addRange(b):wa&&this.startGracePeriod());this.rememberSelection()}},startGracePeriod:function(){var a=this;clearTimeout(this.gracePeriod);this.gracePeriod=setTimeout(function(){a.gracePeriod=!1;a.selectionChanged()&&a.cm.operation(function(){a.cm.curOp.selectionChanged=!0})},20)},showMultipleSelections:function(a){U(this.cm.display.cursorDiv,a.cursors);U(this.cm.display.selectionDiv,a.selection)},rememberSelection:function(){var a=
+window.getSelection();this.lastAnchorNode=a.anchorNode;this.lastAnchorOffset=a.anchorOffset;this.lastFocusNode=a.focusNode;this.lastFocusOffset=a.focusOffset},selectionInEditor:function(){var a=window.getSelection();if(!a.rangeCount)return!1;a=a.getRangeAt(0).commonAncestorContainer;return Wc(this.div,a)},focus:function(){"nocursor"!=this.cm.options.readOnly&&this.div.focus()},blur:function(){this.div.blur()},getField:function(){return this.div},supportsTouch:function(){return!0},receivedFocus:function(){function a(){b.cm.state.focused&&
+(b.pollSelection(),b.polling.set(b.cm.options.pollInterval,a))}var b=this;this.selectionInEditor()?this.pollSelection():T(this.cm,function(){b.cm.curOp.selectionChanged=!0});this.polling.set(this.cm.options.pollInterval,a)},selectionChanged:function(){var a=window.getSelection();return a.anchorNode!=this.lastAnchorNode||a.anchorOffset!=this.lastAnchorOffset||a.focusNode!=this.lastFocusNode||a.focusOffset!=this.lastFocusOffset},pollSelection:function(){if(!this.composing&&!this.gracePeriod&&this.selectionChanged()){var a=
+window.getSelection(),b=this.cm;this.rememberSelection();var c=Tb(b,a.anchorNode,a.anchorOffset),d=Tb(b,a.focusNode,a.focusOffset);c&&d&&T(b,function(){H(b.doc,ga(c,d),ha);if(c.bad||d.bad)b.curOp.selectionChanged=!0})}},pollContent:function(){var a=this.cm,b=a.display,c=a.doc.sel.primary(),d=c.from(),c=c.to();if(d.line<b.viewFrom||c.line>b.viewTo-1)return!1;var e;d.line==b.viewFrom||0==(e=Ca(a,d.line))?(d=F(b.view[0].line),e=b.view[0].node):(d=F(b.view[e].line),e=b.view[e-1].node.nextSibling);var f=
+Ca(a,c.line);f==b.view.length-1?(c=b.viewTo-1,b=b.view[f].node):(c=F(b.view[f+1].line)-1,b=b.view[f+1].node.previousSibling);b=sa(xf(a,e,b,d,c));for(e=Da(a.doc,r(d,0),r(c,u(a.doc,c).text.length));1<b.length&&1<e.length;)if(A(b)==A(e))b.pop(),e.pop(),c--;else if(b[0]==e[0])b.shift(),e.shift(),d++;else break;for(var g=0,f=0,h=b[0],k=e[0],l=Math.min(h.length,k.length);g<l&&h.charCodeAt(g)==k.charCodeAt(g);)++g;h=A(b);k=A(e);for(l=Math.min(h.length-(1==b.length?g:0),k.length-(1==e.length?g:0));f<l&&h.charCodeAt(h.length-
+f-1)==k.charCodeAt(k.length-f-1);)++f;b[b.length-1]=h.slice(0,h.length-f);b[0]=b[0].slice(g);d=r(d,g);c=r(c,e.length?A(e).length-f:0);if(1<b.length||b[0]||y(d,c))return wb(a.doc,b,d,c,"+input"),!0},ensurePolled:function(){this.forceCompositionEnd()},reset:function(){this.forceCompositionEnd()},forceCompositionEnd:function(){this.composing&&!this.composing.handled&&(this.applyComposition(this.composing),this.composing.handled=!0,this.div.blur(),this.div.focus())},applyComposition:function(a){a.data&&
+a.data!=a.startData&&G(this.cm,Sc)(this.cm,a.data,0,a.sel)},setUneditable:function(a){a.setAttribute("contenteditable","false")},onKeyPress:function(a){a.preventDefault();G(this.cm,Sc)(this.cm,String.fromCharCode(null==a.charCode?a.keyCode:a.charCode),0)},onContextMenu:Eb,resetPosition:Eb,needsContentAttribute:!0},Uc.prototype);q.inputStyles={textarea:Tc,contenteditable:Uc};la.prototype={primary:function(){return this.ranges[this.primIndex]},equals:function(a){if(a==this)return!0;if(a.primIndex!=
+this.primIndex||a.ranges.length!=this.ranges.length)return!1;for(var b=0;b<this.ranges.length;b++){var c=this.ranges[b],d=a.ranges[b];if(0!=y(c.anchor,d.anchor)||0!=y(c.head,d.head))return!1}return!0},deepCopy:function(){for(var a=[],b=0;b<this.ranges.length;b++)a[b]=new z(Rc(this.ranges[b].anchor),Rc(this.ranges[b].head));return new la(a,this.primIndex)},somethingSelected:function(){for(var a=0;a<this.ranges.length;a++)if(!this.ranges[a].empty())return!0;return!1},contains:function(a,b){b||(b=a);
+for(var c=0;c<this.ranges.length;c++){var d=this.ranges[c];if(0<=y(b,d.from())&&0>=y(a,d.to()))return c}return-1}};z.prototype={from:function(){return Qb(this.anchor,this.head)},to:function(){return Pb(this.anchor,this.head)},empty:function(){return this.head.line==this.anchor.line&&this.head.ch==this.anchor.ch}};var ad={left:0,right:0,top:0,bottom:0},Fa,Ta=null,Ff=0,fc,ec,se=0,gc=0,R=null;B?R=-.53:wa?R=15:kg?R=-.7:te&&(R=-1/3);var ze=function(a){var b=a.wheelDeltaX,c=a.wheelDeltaY;null==b&&a.detail&&
+a.axis==a.HORIZONTAL_AXIS&&(b=a.detail);null==c&&a.detail&&a.axis==a.VERTICAL_AXIS?c=a.detail:null==c&&(c=a.wheelDelta);return{x:b,y:c}};q.wheelEventPixels=function(a){a=ze(a);a.x*=R;a.y*=R;return a};var Qf=new bb,md=null,ta=q.changeEnd=function(a){return a.text?r(a.from.line+a.text.length-1,A(a.text).length+(1==a.text.length?a.from.ch:0)):a.to};q.prototype={constructor:q,focus:function(){window.focus();this.display.input.focus()},setOption:function(a,b){var c=this.options,d=c[a];if(c[a]!=b||"mode"==
+a)c[a]=b,Ka.hasOwnProperty(a)&&G(this,Ka[a])(this,b,d)},getOption:function(a){return this.options[a]},getDoc:function(){return this.doc},addKeyMap:function(a,b){this.state.keyMaps[b?"push":"unshift"](pc(a))},removeKeyMap:function(a){for(var b=this.state.keyMaps,c=0;c<b.length;++c)if(b[c]==a||b[c].name==a)return b.splice(c,1),!0},addOverlay:M(function(a,b){var c=a.token?a:q.getMode(this.options,a);if(c.startState)throw Error("Overlays may not be stateful.");this.state.overlays.push({mode:c,modeSpec:a,
+opaque:b&&b.opaque});this.state.modeGen++;Q(this)}),removeOverlay:M(function(a){for(var b=this.state.overlays,c=0;c<b.length;++c){var d=b[c].modeSpec;if(d==a||"string"==typeof a&&d.name==a){b.splice(c,1);this.state.modeGen++;Q(this);break}}}),indentLine:M(function(a,b,c){"string"!=typeof b&&"number"!=typeof b&&(b=null==b?this.options.smartIndent?"smart":"prev":b?"add":"subtract");qb(this.doc,a)&&pb(this,a,b,c)}),indentSelection:M(function(a){for(var b=this.doc.sel.ranges,c=-1,d=0;d<b.length;d++){var e=
+b[d];if(e.empty())e.head.line>c&&(pb(this,e.head.line,a,!0),c=e.head.line,d==this.doc.sel.primIndex&&Pa(this));else{for(var f=e.from(),e=e.to(),g=Math.max(c,f.line),c=Math.min(this.lastLine(),e.line-(e.ch?0:1))+1,e=g;e<c;++e)pb(this,e,a);e=this.doc.sel.ranges;0==f.ch&&b.length==e.length&&0<e[d].from().ch&&Xc(this.doc,d,new z(f,e[d].to()),ha)}}}),getTokenAt:function(a,b){return Ye(this,a,b)},getLineTokens:function(a,b){return Ye(this,r(a),b,!0)},getTokenTypeAt:function(a){a=w(this.doc,a);var b=$e(this,
+u(this.doc,a.line)),c=0,d=(b.length-1)/2;a=a.ch;if(0==a)b=b[2];else for(;;){var e=c+d>>1;if((e?b[2*e-1]:0)>=a)d=e;else if(b[2*e+1]<a)c=e+1;else{b=b[2*e+2];break}}c=b?b.indexOf("cm-overlay "):-1;return 0>c?b:0==c?null:b.slice(0,c-1)},getModeAt:function(a){var b=this.doc.mode;return b.innerMode?q.innerMode(b,this.getTokenAt(a).state).mode:b},getHelper:function(a,b){return this.getHelpers(a,b)[0]},getHelpers:function(a,b){var c=[];if(!$a.hasOwnProperty(b))return c;var d=$a[b],e=this.getModeAt(a);if("string"==
+typeof e[b])d[e[b]]&&c.push(d[e[b]]);else if(e[b])for(var f=0;f<e[b].length;f++){var g=d[e[b][f]];g&&c.push(g)}else e.helperType&&d[e.helperType]?c.push(d[e.helperType]):d[e.name]&&c.push(d[e.name]);for(f=0;f<d._global.length;f++)g=d._global[f],g.pred(e,this)&&-1==D(c,g.val)&&c.push(g.val);return c},getStateAfter:function(a,b){var c=this.doc;a=Math.max(c.first,Math.min(null==a?c.first+c.size-1:a,c.first+c.size-1));return sb(this,a+1,b)},cursorCoords:function(a,b){var c;c=this.doc.sel.primary();c=
+null==a?c.head:"object"==typeof a?w(this.doc,a):a?c.from():c.to();return ma(this,c,b||"page")},charCoords:function(a,b){return Yb(this,w(this.doc,a),b||"page")},coordsChar:function(a,b){a=ne(this,a,b||"page");return fd(this,a.left,a.top)},lineAtHeight:function(a,b){a=ne(this,{top:a,left:0},b||"page").top;return Ba(this.doc,a+this.display.viewOffset)},heightAtLine:function(a,b){var c=!1,d;"number"==typeof a?(d=this.doc.first+this.doc.size-1,a<this.doc.first?a=this.doc.first:a>d&&(a=d,c=!0),d=u(this.doc,
+a)):d=a;return cd(this,d,{top:0,left:0},b||"page").top+(c?this.doc.height-ea(d):0)},defaultTextHeight:function(){return xa(this.display)},defaultCharWidth:function(){return gb(this.display)},setGutterMarker:M(function(a,b,c){return nc(this.doc,a,"gutter",function(a){var e=a.gutterMarkers||(a.gutterMarkers={});e[b]=c;!c&&hf(e)&&(a.gutterMarkers=null);return!0})}),clearGutter:M(function(a){var b=this,c=b.doc,d=c.first;c.iter(function(c){c.gutterMarkers&&c.gutterMarkers[a]&&(c.gutterMarkers[a]=null,
+na(b,d,"gutter"),hf(c.gutterMarkers)&&(c.gutterMarkers=null));++d})}),lineInfo:function(a){if("number"==typeof a){if(!qb(this.doc,a))return null;var b=a;a=u(this.doc,a);if(!a)return null}else if(b=F(a),null==b)return null;return{line:b,handle:a,text:a.text,gutterMarkers:a.gutterMarkers,textClass:a.textClass,bgClass:a.bgClass,wrapClass:a.wrapClass,widgets:a.widgets}},getViewport:function(){return{from:this.display.viewFrom,to:this.display.viewTo}},addWidget:function(a,b,c,d,e){var f=this.display;a=
+ma(this,w(this.doc,a));var g=a.bottom,h=a.left;b.style.position="absolute";b.setAttribute("cm-ignore-events","true");this.display.input.setUneditable(b);f.sizer.appendChild(b);if("over"==d)g=a.top;else if("above"==d||"near"==d){var k=Math.max(f.wrapper.clientHeight,this.doc.height),l=Math.max(f.sizer.clientWidth,f.lineSpace.clientWidth);("above"==d||a.bottom+b.offsetHeight>k)&&a.top>b.offsetHeight?g=a.top-b.offsetHeight:a.bottom+b.offsetHeight<=k&&(g=a.bottom);h+b.offsetWidth>l&&(h=l-b.offsetWidth)}b.style.top=
+g+"px";b.style.left=b.style.right="";"right"==e?(h=f.sizer.clientWidth-b.offsetWidth,b.style.right="0px"):("left"==e?h=0:"middle"==e&&(h=(f.sizer.clientWidth-b.offsetWidth)/2),b.style.left=h+"px");c&&(a=cc(this,h,g,h+b.offsetWidth,g+b.offsetHeight),null!=a.scrollTop&&lb(this,a.scrollTop),null!=a.scrollLeft&&Ma(this,a.scrollLeft))},triggerOnKeyDown:M(ve),triggerOnKeyPress:M(we),triggerOnKeyUp:ue,execCommand:function(a){if(ic.hasOwnProperty(a))return ic[a](this)},findPosH:function(a,b,c,d){var e=1;
+0>b&&(e=-1,b=-b);var f=0;for(a=w(this.doc,a);f<b&&(a=rd(this.doc,a,e,c,d),!a.hitSide);++f);return a},moveH:M(function(a,b){var c=this;c.extendSelectionsBy(function(d){return c.display.shift||c.doc.extend||d.empty()?rd(c.doc,d.head,a,b,c.options.rtlMoveVisually):0>a?d.from():d.to()},Gb)}),deleteH:M(function(a,b){var c=this.doc;this.doc.sel.somethingSelected()?c.replaceSelection("",null,"+delete"):Va(this,function(d){var e=rd(c,d.head,a,b,!1);return 0>a?{from:e,to:d.head}:{from:d.head,to:e}})}),findPosV:function(a,
+b,c,d){var e=1;0>b&&(e=-1,b=-b);var f=0;for(a=w(this.doc,a);f<b&&(a=ma(this,a,"div"),null==d?d=a.left:a.left=d,a=Pe(this,a,e,c),!a.hitSide);++f);return a},moveV:M(function(a,b){var c=this,d=this.doc,e=[],f=!c.display.shift&&!d.extend&&d.sel.somethingSelected();d.extendSelectionsBy(function(g){if(f)return 0>a?g.from():g.to();var k=ma(c,g.head,"div");null!=g.goalColumn&&(k.left=g.goalColumn);e.push(k.left);var l=Pe(c,k,a,b);"page"==b&&g==d.sel.primary()&&lc(c,null,Yb(c,l,"div").top-k.top);return l},
+Gb);if(e.length)for(var g=0;g<d.sel.ranges.length;g++)d.sel.ranges[g].goalColumn=e[g]}),findWordAt:function(a){var b=u(this.doc,a.line).text,c=a.ch,d=a.ch;if(b){var e=this.getHelper(a,"wordChars");(0>a.xRel||d==b.length)&&c?--c:++d;for(var f=b.charAt(c),f=oc(f,e)?function(a){return oc(a,e)}:/\s/.test(f)?function(a){return/\s/.test(a)}:function(a){return!/\s/.test(a)&&!oc(a)};0<c&&f(b.charAt(c-1));)--c;for(;d<b.length&&f(b.charAt(d));)++d}return new z(r(a.line,c),r(a.line,d))},toggleOverwrite:function(a){if(null==
+a||a!=this.state.overwrite)(this.state.overwrite=!this.state.overwrite)?mb(this.display.cursorDiv,"CodeMirror-overwrite"):kb(this.display.cursorDiv,"CodeMirror-overwrite"),K(this,"overwriteToggle",this,this.state.overwrite)},hasFocus:function(){return this.display.input.getField()==fa()},scrollTo:M(function(a,b){null==a&&null==b||mc(this);null!=a&&(this.curOp.scrollLeft=a);null!=b&&(this.curOp.scrollTop=b)}),getScrollInfo:function(){var a=this.display.scroller;return{left:a.scrollLeft,top:a.scrollTop,
+height:a.scrollHeight-da(this)-this.display.barHeight,width:a.scrollWidth-da(this)-this.display.barWidth,clientHeight:Nc(this),clientWidth:pa(this)}},scrollIntoView:M(function(a,b){null==a?(a={from:this.doc.sel.primary().head,to:null},null==b&&(b=this.options.cursorScrollMargin)):"number"==typeof a?a={from:r(a,0),to:null}:null==a.from&&(a={from:a,to:null});a.to||(a.to=a.from);a.margin=b||0;if(null!=a.from.line)mc(this),this.curOp.scrollToPos=a;else{var c=cc(this,Math.min(a.from.left,a.to.left),Math.min(a.from.top,
+a.to.top)-a.margin,Math.max(a.from.right,a.to.right),Math.max(a.from.bottom,a.to.bottom)+a.margin);this.scrollTo(c.scrollLeft,c.scrollTop)}}),setSize:M(function(a,b){function c(a){return"number"==typeof a||/^\d+$/.test(String(a))?a+"px":a}var d=this;null!=a&&(d.display.wrapper.style.width=c(a));null!=b&&(d.display.wrapper.style.height=c(b));d.options.lineWrapping&&me(this);var e=d.display.viewFrom;d.doc.iter(e,d.display.viewTo,function(a){if(a.widgets)for(var b=0;b<a.widgets.length;b++)if(a.widgets[b].noHScroll){na(d,
+e,"widget");break}++e});d.curOp.forceUpdate=!0;K(d,"refresh",this)}),operation:function(a){return T(this,a)},refresh:M(function(){var a=this.display.cachedTextHeight;Q(this);this.curOp.forceUpdate=!0;hb(this);this.scrollTo(this.doc.scrollLeft,this.doc.scrollTop);Cc(this);(null==a||.5<Math.abs(a-xa(this.display)))&&Ac(this);K(this,"refresh",this)}),swapDoc:M(function(a){var b=this.doc;b.cm=null;Ed(this,a);hb(this);this.display.input.reset();this.scrollTo(a.scrollLeft,a.scrollTop);this.curOp.forceScroll=
+!0;L(this,"swapDoc",this,b);return b}),getInputField:function(){return this.display.input.getField()},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}};Ya(q);var qf=q.defaults={},Ka=q.optionHandlers={},Fd=q.Init={toString:function(){return"CodeMirror.Init"}};x("value","",function(a,b){a.setValue(b)},!0);x("mode",null,function(a,b){a.doc.modeOption=b;zc(a)},!0);x("indentUnit",
+2,zc,!0);x("indentWithTabs",!1);x("smartIndent",!0);x("tabSize",4,function(a){eb(a);hb(a);Q(a)},!0);x("specialChars",/[\t\u0000-\u0019\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g,function(a,b,c){a.state.specialChars=new RegExp(b.source+(b.test("\t")?"":"|\t"),"g");c!=q.Init&&a.refresh()});x("specialCharPlaceholder",function(a){var b=t("span","•","cm-invalidchar");b.title="\\u"+a.charCodeAt(0).toString(16);b.setAttribute("aria-label",b.title);return b},function(a){a.refresh()},!0);x("electricChars",!0);
+x("inputStyle",ab?"contenteditable":"textarea",function(){throw Error("inputStyle can not (yet) be changed in a running editor");},!0);x("rtlMoveVisually",!mg);x("wholeLineUpdateBefore",!0);x("theme","default",function(a){Bd(a);ib(a)},!0);x("keyMap","default",function(a,b,c){b=pc(b);(c=c!=q.Init&&pc(c))&&c.detach&&c.detach(a,b);b.attach&&b.attach(a,c||null)});x("extraKeys",null);x("lineWrapping",!1,function(a){a.options.lineWrapping?(mb(a.display.wrapper,"CodeMirror-wrap"),a.display.sizer.style.minWidth=
+"",a.display.sizerWidth=null):(kb(a.display.wrapper,"CodeMirror-wrap"),Dc(a));Ac(a);Q(a);hb(a);setTimeout(function(){Na(a)},100)},!0);x("gutters",[],function(a){wc(a.options);ib(a)},!0);x("fixedGutter",!0,function(a,b){a.display.gutters.style.left=b?Ic(a.display)+"px":"0";a.refresh()},!0);x("coverGutterNextToScrollbar",!1,function(a){Na(a)},!0);x("scrollbarStyle","native",function(a){Cd(a);Na(a);a.display.scrollbars.setScrollTop(a.doc.scrollTop);a.display.scrollbars.setScrollLeft(a.doc.scrollLeft)},
+!0);x("lineNumbers",!1,function(a){wc(a.options);ib(a)},!0);x("firstLineNumber",1,ib,!0);x("lineNumberFormatter",function(a){return a},ib,!0);x("showCursorWhenSelecting",!1,nb,!0);x("resetSelectionOnContextMenu",!0);x("lineWiseCopyCut",!0);x("readOnly",!1,function(a,b){"nocursor"==b?(db(a),a.display.input.blur(),a.display.disabled=!0):(a.display.disabled=!1,b||a.display.input.reset())});x("disableInput",!1,function(a,b){b||a.display.input.reset()},!0);x("dragDrop",!0,function(a,b,c){!b!=!(c&&c!=q.Init)&&
+(c=a.display.dragFunctions,b=b?v:ka,b(a.display.scroller,"dragstart",c.start),b(a.display.scroller,"dragenter",c.simple),b(a.display.scroller,"dragover",c.simple),b(a.display.scroller,"drop",c.drop))});x("cursorBlinkRate",530);x("cursorScrollMargin",0);x("cursorHeight",1,nb,!0);x("singleCursorHeightPerLine",!0,nb,!0);x("workTime",100);x("workDelay",100);x("flattenSpans",!0,eb,!0);x("addModeClass",!1,eb,!0);x("pollInterval",100);x("undoDepth",200,function(a,b){a.doc.history.undoDepth=b});x("historyEventDelay",
+1250);x("viewportMargin",10,function(a){a.refresh()},!0);x("maxHighlightLength",1E4,eb,!0);x("moveInputWithCursor",!0,function(a,b){b||a.display.input.resetPosition()});x("tabindex",null,function(a,b){a.display.input.getField().tabIndex=b||""});x("autofocus",null);var pf=q.modes={},Hb=q.mimeModes={};q.defineMode=function(a,b){q.defaults.mode||"null"==a||(q.defaults.mode=a);2<arguments.length&&(b.dependencies=Array.prototype.slice.call(arguments,2));pf[a]=b};q.defineMIME=function(a,b){Hb[a]=b};q.resolveMode=
+function(a){if("string"==typeof a&&Hb.hasOwnProperty(a))a=Hb[a];else if(a&&"string"==typeof a.name&&Hb.hasOwnProperty(a.name)){var b=Hb[a.name];"string"==typeof b&&(b={name:b});a=ff(b,a);a.name=b.name}else if("string"==typeof a&&/^[\w\-]+\/[\w\-]+\+xml$/.test(a))return q.resolveMode("application/xml");return"string"==typeof a?{name:a}:a||{name:"null"}};q.getMode=function(a,b){b=q.resolveMode(b);var c=pf[b.name];if(!c)return q.getMode(a,"text/plain");c=c(a,b);if(Ib.hasOwnProperty(b.name)){var d=Ib[b.name],
+e;for(e in d)d.hasOwnProperty(e)&&(c.hasOwnProperty(e)&&(c["_"+e]=c[e]),c[e]=d[e])}c.name=b.name;b.helperType&&(c.helperType=b.helperType);if(b.modeProps)for(e in b.modeProps)c[e]=b.modeProps[e];return c};q.defineMode("null",function(){return{token:function(a){a.skipToEnd()}}});q.defineMIME("text/plain","null");var Ib=q.modeExtensions={};q.extendMode=function(a,b){var c=Ib.hasOwnProperty(a)?Ib[a]:Ib[a]={};V(b,c)};q.defineExtension=function(a,b){q.prototype[a]=b};q.defineDocExtension=function(a,b){P.prototype[a]=
+b};q.defineOption=x;var yc=[];q.defineInitHook=function(a){yc.push(a)};var $a=q.helpers={};q.registerHelper=function(a,b,c){$a.hasOwnProperty(a)||($a[a]=q[a]={_global:[]});$a[a][b]=c};q.registerGlobalHelper=function(a,b,c,d){q.registerHelper(a,b,d);$a[a]._global.push({pred:c,val:d})};var Sa=q.copyState=function(a,b){if(!0===b)return b;if(a.copyState)return a.copyState(b);var c={},d;for(d in b){var e=b[d];e instanceof Array&&(e=e.concat([]));c[d]=e}return c},Df=q.startState=function(a,b,c){return a.startState?
+a.startState(b,c):!0};q.innerMode=function(a,b){for(;a.innerMode;){var c=a.innerMode(b);if(!c||c.mode==a)break;b=c.state;a=c.mode}return c||{mode:a,state:b}};var ic=q.commands={selectAll:function(a){a.setSelection(r(a.firstLine(),0),r(a.lastLine()),ha)},singleSelection:function(a){a.setSelection(a.getCursor("anchor"),a.getCursor("head"),ha)},killLine:function(a){Va(a,function(b){if(b.empty()){var c=u(a.doc,b.head.line).text.length;return b.head.ch==c&&b.head.line<a.lastLine()?{from:b.head,to:r(b.head.line+
+1,0)}:{from:b.head,to:r(b.head.line,c)}}return{from:b.from(),to:b.to()}})},deleteLine:function(a){Va(a,function(b){return{from:r(b.from().line,0),to:w(a.doc,r(b.to().line+1,0))}})},delLineLeft:function(a){Va(a,function(a){return{from:r(a.from().line,0),to:a.from()}})},delWrappedLineLeft:function(a){Va(a,function(b){var c=a.charCoords(b.head,"div").top+5;return{from:a.coordsChar({left:0,top:c},"div"),to:b.from()}})},delWrappedLineRight:function(a){Va(a,function(b){var c=a.charCoords(b.head,"div").top+
+5,c=a.coordsChar({left:a.display.lineDiv.offsetWidth+100,top:c},"div");return{from:b.from(),to:c}})},undo:function(a){a.undo()},redo:function(a){a.redo()},undoSelection:function(a){a.undoSelection()},redoSelection:function(a){a.redoSelection()},goDocStart:function(a){a.extendSelection(r(a.firstLine(),0))},goDocEnd:function(a){a.extendSelection(r(a.lastLine()))},goLineStart:function(a){a.extendSelectionsBy(function(b){return kf(a,b.head.line)},{origin:"+move",bias:1})},goLineStartSmart:function(a){a.extendSelectionsBy(function(b){return lf(a,
+b.head)},{origin:"+move",bias:1})},goLineEnd:function(a){a.extendSelectionsBy(function(b){b=b.head.line;for(var c,d=u(a.doc,b);c=Aa(d,!1);)d=c.find(1,!0).line,b=null;c=(c=Y(d))?c[0].level%2?ac(d):bc(d):d.text.length;return r(null==b?F(d):b,c)},{origin:"+move",bias:-1})},goLineRight:function(a){a.extendSelectionsBy(function(b){b=a.charCoords(b.head,"div").top+5;return a.coordsChar({left:a.display.lineDiv.offsetWidth+100,top:b},"div")},Gb)},goLineLeft:function(a){a.extendSelectionsBy(function(b){b=
+a.charCoords(b.head,"div").top+5;return a.coordsChar({left:0,top:b},"div")},Gb)},goLineLeftSmart:function(a){a.extendSelectionsBy(function(b){var c=a.charCoords(b.head,"div").top+5,c=a.coordsChar({left:0,top:c},"div");return c.ch<a.getLine(c.line).search(/\S/)?lf(a,b.head):c},Gb)},goLineUp:function(a){a.moveV(-1,"line")},goLineDown:function(a){a.moveV(1,"line")},goPageUp:function(a){a.moveV(-1,"page")},goPageDown:function(a){a.moveV(1,"page")},goCharLeft:function(a){a.moveH(-1,"char")},goCharRight:function(a){a.moveH(1,
+"char")},goColumnLeft:function(a){a.moveH(-1,"column")},goColumnRight:function(a){a.moveH(1,"column")},goWordLeft:function(a){a.moveH(-1,"word")},goGroupRight:function(a){a.moveH(1,"group")},goGroupLeft:function(a){a.moveH(-1,"group")},goWordRight:function(a){a.moveH(1,"word")},delCharBefore:function(a){a.deleteH(-1,"char")},delCharAfter:function(a){a.deleteH(1,"char")},delWordBefore:function(a){a.deleteH(-1,"word")},delWordAfter:function(a){a.deleteH(1,"word")},delGroupBefore:function(a){a.deleteH(-1,
+"group")},delGroupAfter:function(a){a.deleteH(1,"group")},indentAuto:function(a){a.indentSelection("smart")},indentMore:function(a){a.indentSelection("add")},indentLess:function(a){a.indentSelection("subtract")},insertTab:function(a){a.replaceSelection("\t")},insertSoftTab:function(a){for(var b=[],c=a.listSelections(),d=a.options.tabSize,e=0;e<c.length;e++){var f=c[e].from(),f=aa(a.getLine(f.line),f.ch,d);b.push(Array(d-f%d+1).join(" "))}a.replaceSelections(b)},defaultTab:function(a){a.somethingSelected()?
+a.indentSelection("add"):a.execCommand("insertTab")},transposeChars:function(a){T(a,function(){for(var b=a.listSelections(),c=[],d=0;d<b.length;d++){var e=b[d].head,f=u(a.doc,e.line).text;if(f)if(e.ch==f.length&&(e=new r(e.line,e.ch-1)),0<e.ch)e=new r(e.line,e.ch+1),a.replaceRange(f.charAt(e.ch-1)+f.charAt(e.ch-2),r(e.line,e.ch-2),e,"+transpose");else if(e.line>a.doc.first){var g=u(a.doc,e.line-1).text;g&&a.replaceRange(f.charAt(0)+"\n"+g.charAt(g.length-1),r(e.line-1,g.length-1),r(e.line,1),"+transpose")}c.push(new z(e,
+e))}a.setSelections(c)})},newlineAndIndent:function(a){T(a,function(){for(var b=a.listSelections().length,c=0;c<b;c++){var d=a.listSelections()[c];a.replaceRange("\n",d.anchor,d.head,"+input");a.indentLine(d.from().line+1,null,!0);Pa(a)}})},toggleOverwrite:function(a){a.toggleOverwrite()}},ua=q.keyMap={};ua.basic={Left:"goCharLeft",Right:"goCharRight",Up:"goLineUp",Down:"goLineDown",End:"goLineEnd",Home:"goLineStartSmart",PageUp:"goPageUp",PageDown:"goPageDown",Delete:"delCharAfter",Backspace:"delCharBefore",
+"Shift-Backspace":"delCharBefore",Tab:"defaultTab","Shift-Tab":"indentAuto",Enter:"newlineAndIndent",Insert:"toggleOverwrite",Esc:"singleSelection"};ua.pcDefault={"Ctrl-A":"selectAll","Ctrl-D":"deleteLine","Ctrl-Z":"undo","Shift-Ctrl-Z":"redo","Ctrl-Y":"redo","Ctrl-Home":"goDocStart","Ctrl-End":"goDocEnd","Ctrl-Up":"goLineUp","Ctrl-Down":"goLineDown","Ctrl-Left":"goGroupLeft","Ctrl-Right":"goGroupRight","Alt-Left":"goLineStart","Alt-Right":"goLineEnd","Ctrl-Backspace":"delGroupBefore","Ctrl-Delete":"delGroupAfter",
+"Ctrl-S":"save","Ctrl-F":"find","Ctrl-G":"findNext","Shift-Ctrl-G":"findPrev","Shift-Ctrl-F":"replace","Shift-Ctrl-R":"replaceAll","Ctrl-[":"indentLess","Ctrl-]":"indentMore","Ctrl-U":"undoSelection","Shift-Ctrl-U":"redoSelection","Alt-U":"redoSelection",fallthrough:"basic"};ua.emacsy={"Ctrl-F":"goCharRight","Ctrl-B":"goCharLeft","Ctrl-P":"goLineUp","Ctrl-N":"goLineDown","Alt-F":"goWordRight","Alt-B":"goWordLeft","Ctrl-A":"goLineStart","Ctrl-E":"goLineEnd","Ctrl-V":"goPageDown","Shift-Ctrl-V":"goPageUp",
+"Ctrl-D":"delCharAfter","Ctrl-H":"delCharBefore","Alt-D":"delWordAfter","Alt-Backspace":"delWordBefore","Ctrl-K":"killLine","Ctrl-T":"transposeChars"};ua.macDefault={"Cmd-A":"selectAll","Cmd-D":"deleteLine","Cmd-Z":"undo","Shift-Cmd-Z":"redo","Cmd-Y":"redo","Cmd-Home":"goDocStart","Cmd-Up":"goDocStart","Cmd-End":"goDocEnd","Cmd-Down":"goDocEnd","Alt-Left":"goGroupLeft","Alt-Right":"goGroupRight","Cmd-Left":"goLineLeft","Cmd-Right":"goLineRight","Alt-Backspace":"delGroupBefore","Ctrl-Alt-Backspace":"delGroupAfter",
+"Alt-Delete":"delGroupAfter","Cmd-S":"save","Cmd-F":"find","Cmd-G":"findNext","Shift-Cmd-G":"findPrev","Cmd-Alt-F":"replace","Shift-Cmd-Alt-F":"replaceAll","Cmd-[":"indentLess","Cmd-]":"indentMore","Cmd-Backspace":"delWrappedLineLeft","Cmd-Delete":"delWrappedLineRight","Cmd-U":"undoSelection","Shift-Cmd-U":"redoSelection","Ctrl-Up":"goDocStart","Ctrl-Down":"goDocEnd",fallthrough:["basic","emacsy"]};ua["default"]=W?ua.macDefault:ua.pcDefault;q.normalizeKeyMap=function(a){var b={},c;for(c in a)if(a.hasOwnProperty(c)){var d=
+a[c];if(!/^(name|fallthrough|(de|at)tach)$/.test(c)){if("..."!=d)for(var e=ob(c.split(" "),Wf),f=0;f<e.length;f++){var g,h;f==e.length-1?(h=c,g=d):(h=e.slice(0,f+1).join(" "),g="...");var k=b[h];if(!k)b[h]=g;else if(k!=g)throw Error("Inconsistent bindings for "+h);}delete a[c]}}for(var l in b)a[l]=b[l];return a};var xb=q.lookupKey=function(a,b,c,d){b=pc(b);var e=b.call?b.call(a,d):b[a];if(!1===e)return"nothing";if("..."===e)return"multi";if(null!=e&&c(e))return"handled";if(b.fallthrough){if("[object Array]"!=
+Object.prototype.toString.call(b.fallthrough))return xb(a,b.fallthrough,c,d);for(e=0;e<b.fallthrough.length;e++){var f=xb(a,b.fallthrough[e],c,d);if(f)return f}}},Pf=q.isModifierKey=function(a){a="string"==typeof a?a:va[a.keyCode];return"Ctrl"==a||"Alt"==a||"Shift"==a||"Mod"==a},Rf=q.keyName=function(a,b){if(ba&&34==a.keyCode&&a["char"])return!1;var c=va[a.keyCode],d=c;if(null==d||a.altGraphKey)return!1;a.altKey&&"Alt"!=c&&(d="Alt-"+d);(of?a.metaKey:a.ctrlKey)&&"Ctrl"!=c&&(d="Ctrl-"+d);(of?a.ctrlKey:
+a.metaKey)&&"Cmd"!=c&&(d="Cmd-"+d);!b&&a.shiftKey&&"Shift"!=c&&(d="Shift-"+d);return d};q.fromTextArea=function(a,b){function c(){a.value=k.getValue()}b=b?V(b):{};b.value=a.value;!b.tabindex&&a.tabIndex&&(b.tabindex=a.tabIndex);!b.placeholder&&a.placeholder&&(b.placeholder=a.placeholder);if(null==b.autofocus){var d=fa();b.autofocus=d==a||null!=a.getAttribute("autofocus")&&d==document.body}if(a.form&&(v(a.form,"submit",c),!b.leaveSubmitMethodAlone)){var e=a.form,f=e.submit;try{var g=e.submit=function(){c();
+e.submit=f;e.submit();e.submit=g}}catch(h){}}b.finishInit=function(b){b.save=c;b.getTextArea=function(){return a};b.toTextArea=function(){b.toTextArea=isNaN;c();a.parentNode.removeChild(b.getWrapperElement());a.style.display="";a.form&&(ka(a.form,"submit",c),"function"==typeof a.form.submit&&(a.form.submit=f))}};a.style.display="none";var k=q(function(b){a.parentNode.insertBefore(b,a.nextSibling)},b);return k};var tc=q.StringStream=function(a,b){this.pos=this.start=0;this.string=a;this.tabSize=b||
+8;this.lineStart=this.lastColumnPos=this.lastColumnValue=0};tc.prototype={eol:function(){return this.pos>=this.string.length},sol:function(){return this.pos==this.lineStart},peek:function(){return this.string.charAt(this.pos)||void 0},next:function(){if(this.pos<this.string.length)return this.string.charAt(this.pos++)},eat:function(a){var b=this.string.charAt(this.pos);if("string"==typeof a?b==a:b&&(a.test?a.test(b):a(b)))return++this.pos,b},eatWhile:function(a){for(var b=this.pos;this.eat(a););return this.pos>
+b},eatSpace:function(){for(var a=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>a},skipToEnd:function(){this.pos=this.string.length},skipTo:function(a){a=this.string.indexOf(a,this.pos);if(-1<a)return this.pos=a,!0},backUp:function(a){this.pos-=a},column:function(){this.lastColumnPos<this.start&&(this.lastColumnValue=aa(this.string,this.start,this.tabSize,this.lastColumnPos,this.lastColumnValue),this.lastColumnPos=this.start);return this.lastColumnValue-(this.lineStart?
+aa(this.string,this.lineStart,this.tabSize):0)},indentation:function(){return aa(this.string,null,this.tabSize)-(this.lineStart?aa(this.string,this.lineStart,this.tabSize):0)},match:function(a,b,c){if("string"==typeof a){var d=function(a){return c?a.toLowerCase():a},e=this.string.substr(this.pos,a.length);if(d(e)==d(a))return!1!==b&&(this.pos+=a.length),!0}else{if((a=this.string.slice(this.pos).match(a))&&0<a.index)return null;a&&!1!==b&&(this.pos+=a[0].length);return a}},current:function(){return this.string.slice(this.start,
+this.pos)},hideFirstChars:function(a,b){this.lineStart+=a;try{return b()}finally{this.lineStart-=a}}};var sd=0,Ha=q.TextMarker=function(a,b){this.lines=[];this.type=b;this.doc=a;this.id=++sd};Ya(Ha);Ha.prototype.clear=function(){if(!this.explicitlyCleared){var a=this.doc.cm,b=a&&!a.curOp;b&&Ja(a);if(S(this,"clear")){var c=this.find();c&&L(this,"clear",c.from,c.to)}for(var d=c=null,e=0;e<this.lines.length;++e){var f=this.lines[e],g=zb(f.markedSpans,this);a&&!this.collapsed?na(a,F(f),"text"):a&&(null!=
+g.to&&(d=F(f)),null!=g.from&&(c=F(f)));for(var h=f,k=f.markedSpans,l=g,m=void 0,p=0;p<k.length;++p)k[p]!=l&&(m||(m=[])).push(k[p]);h.markedSpans=m;null==g.from&&this.collapsed&&!ya(this.doc,f)&&a&&ca(f,xa(a.display))}if(a&&this.collapsed&&!a.options.lineWrapping)for(e=0;e<this.lines.length;++e)f=ia(this.lines[e]),g=Kb(f),g>a.display.maxLineLength&&(a.display.maxLine=f,a.display.maxLineLength=g,a.display.maxLineChanged=!0);null!=c&&a&&this.collapsed&&Q(a,c,d+1);this.lines.length=0;this.explicitlyCleared=
+!0;this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,a&&ge(a.doc));a&&L(a,"markerCleared",a,this);b&&La(a);this.parent&&this.parent.clear()}};Ha.prototype.find=function(a,b){null==a&&"bookmark"==this.type&&(a=1);for(var c,d,e=0;e<this.lines.length;++e){var f=this.lines[e],g=zb(f.markedSpans,this);if(null!=g.from&&(c=r(b?f:F(f),g.from),-1==a))return c;if(null!=g.to&&(d=r(b?f:F(f),g.to),1==a))return d}return c&&{from:c,to:d}};Ha.prototype.changed=function(){var a=this.find(-1,!0),b=this,c=this.doc.cm;
+a&&c&&T(c,function(){var d=a.line,e=F(a.line);if(e=Vc(c,e))le(e),c.curOp.selectionChanged=c.curOp.forceUpdate=!0;c.curOp.updateMaxLine=!0;ya(b.doc,d)||null==b.height||(e=b.height,b.height=null,(e=ub(b)-e)&&ca(d,d.height+e))})};Ha.prototype.attachLine=function(a){if(!this.lines.length&&this.doc.cm){var b=this.doc.cm.curOp;b.maybeHiddenMarkers&&-1!=D(b.maybeHiddenMarkers,this)||(b.maybeUnhiddenMarkers||(b.maybeUnhiddenMarkers=[])).push(this)}this.lines.push(a)};Ha.prototype.detachLine=function(a){this.lines.splice(D(this.lines,
+a),1);!this.lines.length&&this.doc.cm&&(a=this.doc.cm.curOp,(a.maybeHiddenMarkers||(a.maybeHiddenMarkers=[])).push(this))};var sd=0,rc=q.SharedTextMarker=function(a,b){this.markers=a;this.primary=b;for(var c=0;c<a.length;++c)a[c].parent=this};Ya(rc);rc.prototype.clear=function(){if(!this.explicitlyCleared){this.explicitlyCleared=!0;for(var a=0;a<this.markers.length;++a)this.markers[a].clear();L(this,"clear")}};rc.prototype.find=function(a,b){return this.primary.find(a,b)};var sc=q.LineWidget=function(a,
+b,c){if(c)for(var d in c)c.hasOwnProperty(d)&&(this[d]=c[d]);this.doc=a;this.node=b};Ya(sc);sc.prototype.clear=function(){var a=this.doc.cm,b=this.line.widgets,c=this.line,d=F(c);if(null!=d&&b){for(var e=0;e<b.length;++e)b[e]==this&&b.splice(e--,1);b.length||(c.widgets=null);var f=ub(this);ca(c,Math.max(0,c.height-f));a&&T(a,function(){var b=-f;ea(c)<(a.curOp&&a.curOp.scrollTop||a.doc.scrollTop)&&lc(a,null,b);na(a,d,"widget")})}};sc.prototype.changed=function(){var a=this.height,b=this.doc.cm,c=this.line;
+this.height=null;var d=ub(this)-a;d&&(ca(c,c.height+d),b&&T(b,function(){b.curOp.forceUpdate=!0;ea(c)<(b.curOp&&b.curOp.scrollTop||b.doc.scrollTop)&&lc(b,null,d)}))};var Ab=q.Line=function(a,b,c){this.text=a;Ue(this,b);this.height=c?c(this):1};Ya(Ab);Ab.prototype.lineNo=function(){return F(this)};var bg={},ag={};Bb.prototype={chunkSize:function(){return this.lines.length},removeInner:function(a,b){for(var c=a,d=a+b;c<d;++c){var e=this.lines[c];this.height-=e.height;var f=e;f.parent=null;Te(f);L(e,
+"delete")}this.lines.splice(a,b)},collapse:function(a){a.push.apply(a,this.lines)},insertInner:function(a,b,c){this.height+=c;this.lines=this.lines.slice(0,a).concat(b).concat(this.lines.slice(a));for(a=0;a<b.length;++a)b[a].parent=this},iterN:function(a,b,c){for(b=a+b;a<b;++a)if(c(this.lines[a]))return!0}};Cb.prototype={chunkSize:function(){return this.size},removeInner:function(a,b){this.size-=b;for(var c=0;c<this.children.length;++c){var d=this.children[c],e=d.chunkSize();if(a<e){var f=Math.min(b,
+e-a),g=d.height;d.removeInner(a,f);this.height-=g-d.height;e==f&&(this.children.splice(c--,1),d.parent=null);if(0==(b-=f))break;a=0}else a-=e}25>this.size-b&&(1<this.children.length||!(this.children[0]instanceof Bb))&&(c=[],this.collapse(c),this.children=[new Bb(c)],this.children[0].parent=this)},collapse:function(a){for(var b=0;b<this.children.length;++b)this.children[b].collapse(a)},insertInner:function(a,b,c){this.size+=b.length;this.height+=c;for(var d=0;d<this.children.length;++d){var e=this.children[d],
+f=e.chunkSize();if(a<=f){e.insertInner(a,b,c);if(e.lines&&50<e.lines.length){for(;50<e.lines.length;)a=e.lines.splice(e.lines.length-25,25),a=new Bb(a),e.height-=a.height,this.children.splice(d+1,0,a),a.parent=this;this.maybeSpill()}break}a-=f}},maybeSpill:function(){if(!(10>=this.children.length)){var a=this;do{var b=a.children.splice(a.children.length-5,5),b=new Cb(b);if(a.parent){a.size-=b.size;a.height-=b.height;var c=D(a.parent.children,a);a.parent.children.splice(c+1,0,b)}else c=new Cb(a.children),
+c.parent=a,a.children=[c,b],a=c;b.parent=a.parent}while(10<a.children.length);a.parent.maybeSpill()}},iterN:function(a,b,c){for(var d=0;d<this.children.length;++d){var e=this.children[d],f=e.chunkSize();if(a<f){f=Math.min(b,f-a);if(e.iterN(a,f,c))return!0;if(0==(b-=f))break;a=0}else a-=f}}};var og=0,P=q.Doc=function(a,b,c){if(!(this instanceof P))return new P(a,b,c);null==c&&(c=0);Cb.call(this,[new Bb([new Ab("",null)])]);this.first=c;this.scrollTop=this.scrollLeft=0;this.cantEdit=!1;this.cleanGeneration=
+1;this.frontier=c;c=r(c,0);this.sel=ga(c);this.history=new uc(null);this.id=++og;this.modeOption=b;"string"==typeof a&&(a=sa(a));qd(this,{from:c,to:c,text:a});H(this,ga(c),ha)};P.prototype=ff(Cb.prototype,{constructor:P,iter:function(a,b,c){c?this.iterN(a-this.first,b-a,c):this.iterN(this.first,this.first+this.size,a)},insert:function(a,b){for(var c=0,d=0;d<b.length;++d)c+=b[d].height;this.insertInner(a-this.first,b,c)},remove:function(a,b){this.removeInner(a-this.first,b)},getValue:function(a){var b=
+xd(this,this.first,this.first+this.size);return!1===a?b:b.join(a||"\n")},setValue:N(function(a){var b=r(this.first,0),c=this.first+this.size-1;Oa(this,{from:b,to:r(c,u(this,c).text.length),text:sa(a),origin:"setValue",full:!0},!0);H(this,ga(b))}),replaceRange:function(a,b,c,d){b=w(this,b);c=c?w(this,c):b;wb(this,a,b,c,d)},getRange:function(a,b,c){a=Da(this,w(this,a),w(this,b));return!1===c?a:a.join(c||"\n")},getLine:function(a){return(a=this.getLineHandle(a))&&a.text},getLineHandle:function(a){if(qb(this,
+a))return u(this,a)},getLineNumber:function(a){return F(a)},getLineHandleVisualStart:function(a){"number"==typeof a&&(a=u(this,a));return ia(a)},lineCount:function(){return this.size},firstLine:function(){return this.first},lastLine:function(){return this.first+this.size-1},clipPos:function(a){return w(this,a)},getCursor:function(a){var b=this.sel.primary();return null==a||"head"==a?b.head:"anchor"==a?b.anchor:"end"==a||"to"==a||!1===a?b.to():b.from()},listSelections:function(){return this.sel.ranges},
+somethingSelected:function(){return this.sel.somethingSelected()},setCursor:N(function(a,b,c){a=w(this,"number"==typeof a?r(a,b||0):a);H(this,ga(a,null),c)}),setSelection:N(function(a,b,c){var d=w(this,a);a=w(this,b||a);H(this,ga(d,a),c)}),extendSelection:N(function(a,b,c){Ub(this,w(this,a),b&&w(this,b),c)}),extendSelections:N(function(a,b){for(var c=[],d=0;d<a.length;d++)c[d]=w(this,a[d]);ae(this,c)}),extendSelectionsBy:N(function(a,b){ae(this,ob(this.sel.ranges,a),b)}),setSelections:N(function(a,
+b,c){if(a.length){for(var d=0,e=[];d<a.length;d++)e[d]=new z(w(this,a[d].anchor),w(this,a[d].head));null==b&&(b=Math.min(a.length-1,this.sel.primIndex));H(this,Z(e,b),c)}}),addSelection:N(function(a,b,c){var d=this.sel.ranges.slice(0);d.push(new z(w(this,a),w(this,b||a)));H(this,Z(d,d.length-1),c)}),getSelection:function(a){for(var b=this.sel.ranges,c,d=0;d<b.length;d++){var e=Da(this,b[d].from(),b[d].to());c=c?c.concat(e):e}return!1===a?c:c.join(a||"\n")},getSelections:function(a){for(var b=[],c=
+this.sel.ranges,d=0;d<c.length;d++){var e=Da(this,c[d].from(),c[d].to());!1!==a&&(e=e.join(a||"\n"));b[d]=e}return b},replaceSelection:function(a,b,c){for(var d=[],e=0;e<this.sel.ranges.length;e++)d[e]=a;this.replaceSelections(d,b,c||"+input")},replaceSelections:N(function(a,b,c){for(var d=[],e=this.sel,f=0;f<e.ranges.length;f++){var g=e.ranges[f];d[f]={from:g.from(),to:g.to(),text:sa(a[f]),origin:c}}if(f=b&&"end"!=b){f=[];c=a=r(this.first,0);for(e=0;e<d.length;e++){var h=d[e],g=Ee(h.from,a,c),k=
+Ee(ta(h),a,c);a=h.to;c=k;"around"==b?(h=this.sel.ranges[e],h=0>y(h.head,h.anchor),f[e]=new z(h?k:g,h?g:k)):f[e]=new z(g,g)}f=new la(f,this.sel.primIndex)}b=f;for(f=d.length-1;0<=f;f--)Oa(this,d[f]);b?be(this,b):this.cm&&Pa(this.cm)}),undo:N(function(){kc(this,"undo")}),redo:N(function(){kc(this,"redo")}),undoSelection:N(function(){kc(this,"undo",!0)}),redoSelection:N(function(){kc(this,"redo",!0)}),setExtending:function(a){this.extend=a},getExtending:function(){return this.extend},historySize:function(){for(var a=
+this.history,b=0,c=0,d=0;d<a.done.length;d++)a.done[d].ranges||++b;for(d=0;d<a.undone.length;d++)a.undone[d].ranges||++c;return{undo:b,redo:c}},clearHistory:function(){this.history=new uc(this.history.maxGeneration)},markClean:function(){this.cleanGeneration=this.changeGeneration(!0)},changeGeneration:function(a){a&&(this.history.lastOp=this.history.lastSelOp=this.history.lastOrigin=null);return this.history.generation},isClean:function(a){return this.history.generation==(a||this.cleanGeneration)},
+getHistory:function(){return{done:Xa(this.history.done),undone:Xa(this.history.undone)}},setHistory:function(a){var b=this.history=new uc(this.history.maxGeneration);b.done=Xa(a.done.slice(0),null,!0);b.undone=Xa(a.undone.slice(0),null,!0)},addLineClass:N(function(a,b,c){return nc(this,a,"gutter"==b?"gutter":"class",function(a){var e="text"==b?"textClass":"background"==b?"bgClass":"gutter"==b?"gutterClass":"wrapClass";if(a[e]){if(Fb(c).test(a[e]))return!1;a[e]+=" "+c}else a[e]=c;return!0})}),removeLineClass:N(function(a,
+b,c){return nc(this,a,"gutter"==b?"gutter":"class",function(a){var e="text"==b?"textClass":"background"==b?"bgClass":"gutter"==b?"gutterClass":"wrapClass",f=a[e];if(f)if(null==c)a[e]=null;else{var g=f.match(Fb(c));if(!g)return!1;var h=g.index+g[0].length;a[e]=f.slice(0,g.index)+(g.index&&h!=f.length?" ":"")+f.slice(h)||null}else return!1;return!0})}),addLineWidget:N(function(a,b,c){return $f(this,a,b,c)}),removeLineWidget:function(a){a.clear()},markText:function(a,b,c){return Wa(this,w(this,a),w(this,
+b),c,"range")},setBookmark:function(a,b){var c={replacedWith:b&&(null==b.nodeType?b.widget:b),insertLeft:b&&b.insertLeft,clearWhenEmpty:!1,shared:b&&b.shared,handleMouseEvents:b&&b.handleMouseEvents};a=w(this,a);return Wa(this,a,a,c,"bookmark")},findMarksAt:function(a){a=w(this,a);var b=[],c=u(this,a.line).markedSpans;if(c)for(var d=0;d<c.length;++d){var e=c[d];(null==e.from||e.from<=a.ch)&&(null==e.to||e.to>=a.ch)&&b.push(e.marker.parent||e.marker)}return b},findMarks:function(a,b,c){a=w(this,a);
+b=w(this,b);var d=[],e=a.line;this.iter(a.line,b.line+1,function(f){if(f=f.markedSpans)for(var g=0;g<f.length;g++){var h=f[g];e==a.line&&a.ch>h.to||null==h.from&&e!=a.line||e==b.line&&h.from>b.ch||c&&!c(h.marker)||d.push(h.marker.parent||h.marker)}++e});return d},getAllMarks:function(){var a=[];this.iter(function(b){if(b=b.markedSpans)for(var c=0;c<b.length;++c)null!=b[c].from&&a.push(b[c].marker)});return a},posFromIndex:function(a){var b,c=this.first;this.iter(function(d){d=d.text.length+1;if(d>
+a)return b=a,!0;a-=d;++c});return w(this,r(c,b))},indexFromPos:function(a){a=w(this,a);var b=a.ch;if(a.line<this.first||0>a.ch)return 0;this.iter(this.first,a.line,function(a){b+=a.text.length+1});return b},copy:function(a){var b=new P(xd(this,this.first,this.first+this.size),this.modeOption,this.first);b.scrollTop=this.scrollTop;b.scrollLeft=this.scrollLeft;b.sel=this.sel;b.extend=!1;a&&(b.history.undoDepth=this.history.undoDepth,b.setHistory(this.getHistory()));return b},linkedDoc:function(a){a||
+(a={});var b=this.first,c=this.first+this.size;null!=a.from&&a.from>b&&(b=a.from);null!=a.to&&a.to<c&&(c=a.to);b=new P(xd(this,b,c),a.mode||this.modeOption,b);a.sharedHist&&(b.history=this.history);(this.linked||(this.linked=[])).push({doc:b,sharedHist:a.sharedHist});b.linked=[{doc:this,isParent:!0,sharedHist:a.sharedHist}];a=Re(this);for(c=0;c<a.length;c++){var d=a[c],e=d.find(),f=b.clipPos(e.from),e=b.clipPos(e.to);y(f,e)&&(f=Wa(b,f,e,d.primary,d.primary.type),d.markers.push(f),f.parent=d)}return b},
+unlinkDoc:function(a){a instanceof q&&(a=a.doc);if(this.linked)for(var b=0;b<this.linked.length;++b)if(this.linked[b].doc==a){this.linked.splice(b,1);a.unlinkDoc(this);Yf(Re(this));break}if(a.history==this.history){var c=[a.id];Ga(a,function(a){c.push(a.id)},!0);a.history=new uc(null);a.history.done=Xa(this.history.done,c);a.history.undone=Xa(this.history.undone,c)}},iterLinkedDocs:function(a){Ga(this,a)},getMode:function(){return this.mode},getEditor:function(){return this.cm}});P.prototype.eachLine=
+P.prototype.iter;var pg=["iter","insert","remove","copy","getEditor"],Jb;for(Jb in P.prototype)P.prototype.hasOwnProperty(Jb)&&0>D(pg,Jb)&&(q.prototype[Jb]=function(a){return function(){return a.apply(this.doc,arguments)}}(P.prototype[Jb]));Ya(P);var O=q.e_preventDefault=function(a){a.preventDefault?a.preventDefault():a.returnValue=!1},qg=q.e_stopPropagation=function(a){a.stopPropagation?a.stopPropagation():a.cancelBubble=!0},jd=q.e_stop=function(a){O(a);qg(a)},v=q.on=function(a,b,c){a.addEventListener?
+a.addEventListener(b,c,!1):a.attachEvent?a.attachEvent("on"+b,c):(a=a._handlers||(a._handlers={}),(a[b]||(a[b]=[])).push(c))},ka=q.off=function(a,b,c){if(a.removeEventListener)a.removeEventListener(b,c,!1);else if(a.detachEvent)a.detachEvent("on"+b,c);else if(a=a._handlers&&a._handlers[b])for(b=0;b<a.length;++b)if(a[b]==c){a.splice(b,1);break}},K=q.signal=function(a,b){var c=a._handlers&&a._handlers[b];if(c)for(var d=Array.prototype.slice.call(arguments,2),e=0;e<c.length;++e)c[e].apply(null,d)},Db=
+null,Hd=30,Ae=q.Pass={toString:function(){return"CodeMirror.Pass"}},ha={scroll:!1},kd={origin:"*mouse"},Gb={origin:"+move"};bb.prototype.set=function(a,b){clearTimeout(this.id);this.id=setTimeout(b,a)};var aa=q.countColumn=function(a,b,c,d,e){null==b&&(b=a.search(/[^\s\u00a0]/),-1==b&&(b=a.length));d=d||0;for(e=e||0;;){var f=a.indexOf("\t",d);if(0>f||f>=b)return e+(b-d);e+=f-d;e+=c-e%c;d=f+1}},vc=[""],Za=function(a){a.select()};Qa?Za=function(a){a.selectionStart=0;a.selectionEnd=a.value.length}:B&&
+(Za=function(a){try{a.select()}catch(b){}});var rg=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/,gf=q.isWordChar=function(a){return/\w/.test(a)||"\80"<a&&(a.toUpperCase()!=a.toLowerCase()||rg.test(a))},ig=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/,
+Ea;Ea=document.createRange?function(a,b,c,d){var e=document.createRange();e.setEnd(d||a,c);e.setStart(a,b);return e}:function(a,b,c){var d=document.body.createTextRange();try{d.moveToElementText(a.parentNode)}catch(e){return d}d.collapse(!0);d.moveEnd("character",c);d.moveStart("character",b);return d};var Wc=q.contains=function(a,b){3==b.nodeType&&(b=b.parentNode);if(a.contains)return a.contains(b);do if(11==b.nodeType&&(b=b.host),b==a)return!0;while(b=b.parentNode)};B&&11>C&&(fa=function(){try{return document.activeElement}catch(a){return document.body}});
+var kb=q.rmClass=function(a,b){var c=a.className,d=Fb(b).exec(c);if(d){var e=c.slice(d.index+d[0].length);a.className=c.slice(0,d.index)+(e?d[1]+e:"")}},mb=q.addClass=function(a,b){var c=a.className;Fb(b).test(c)||(a.className+=(c?" ":"")+b)},Dd=!1,Lf=function(){if(B&&9>C)return!1;var a=t("div");return"draggable"in a||"dragDrop"in a}(),yd,vd,sa=q.splitLines=3!="\n\nb".split(/\n/).length?function(a){for(var b=0,c=[],d=a.length;b<=d;){var e=a.indexOf("\n",b);-1==e&&(e=a.length);var f=a.slice(b,"\r"==
+a.charAt(e-1)?e-1:e),g=f.indexOf("\r");-1!=g?(c.push(f.slice(0,g)),b+=g+1):(c.push(f),b=e+1)}return c}:function(a){return a.split(/\r\n?|\n/)},ng=window.getSelection?function(a){try{return a.selectionStart!=a.selectionEnd}catch(b){return!1}}:function(a){try{var b=a.ownerDocument.selection.createRange()}catch(c){}return b&&b.parentElement()==a?0!=b.compareEndPoints("StartToEnd",b):!1},Ce=function(){var a=t("div");if("oncopy"in a)return!0;a.setAttribute("oncopy","return;");return"function"==typeof a.oncopy}(),
+bd=null,va={3:"Enter",8:"Backspace",9:"Tab",13:"Enter",16:"Shift",17:"Ctrl",18:"Alt",19:"Pause",20:"CapsLock",27:"Esc",32:"Space",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"Left",38:"Up",39:"Right",40:"Down",44:"PrintScrn",45:"Insert",46:"Delete",59:";",61:"\x3d",91:"Mod",92:"Mod",93:"Mod",107:"\x3d",109:"-",127:"Delete",173:"-",186:";",187:"\x3d",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'",63232:"Up",63233:"Down",63234:"Left",63235:"Right",63272:"Delete",63273:"Home",
+63275:"End",63276:"PageUp",63277:"PageDown",63302:"Insert"};q.keyNames=va;(function(){for(var a=0;10>a;a++)va[a+48]=va[a+96]=String(a);for(a=65;90>=a;a++)va[a]=String.fromCharCode(a);for(a=1;12>=a;a++)va[a+111]=va[a+63235]="F"+a})();var vb,gg=function(){function a(a){return 247>=a?"bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN".charAt(a):
+1424<=a&&1524>=a?"R":1536<=a&&1773>=a?"rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm".charAt(a-1536):1774<=a&&2220>=a?"r":8192<=a&&8203>=a?"w":8204==a?"b":"L"}function b(a,b,c){this.level=a;this.from=b;this.to=c}var c=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,d=/[stwN]/,e=/[LRr]/,f=/[Lb1n]/,g=/[1n]/;return function(h){if(!c.test(h))return!1;
+for(var k=h.length,l=[],m=0,p;m<k;++m)l.push(a(h.charCodeAt(m)));for(var m=0,n="L";m<k;++m)p=l[m],"m"==p?l[m]=n:n=p;m=0;for(n="L";m<k;++m)p=l[m],"1"==p&&"r"==n?l[m]="n":e.test(p)&&(n=p,"r"==p&&(l[m]="R"));m=1;for(n=l[0];m<k-1;++m)p=l[m],"+"==p&&"1"==n&&"1"==l[m+1]?l[m]="1":","!=p||n!=l[m+1]||"1"!=n&&"n"!=n||(l[m]=n),n=p;for(m=0;m<k;++m)if(p=l[m],","==p)l[m]="N";else if("%"==p){for(n=m+1;n<k&&"%"==l[n];++n);var q=m&&"!"==l[m-1]||n<k&&"1"==l[n]?"1":"N";for(p=m;p<n;++p)l[p]=q;m=n-1}m=0;for(n="L";m<k;++m)p=
+l[m],"L"==n&&"1"==p?l[m]="L":e.test(p)&&(n=p);for(m=0;m<k;++m)if(d.test(l[m])){for(n=m+1;n<k&&d.test(l[n]);++n);p="L"==(n<k?l[n]:"L");q="L"==(m?l[m-1]:"L")||p?"L":"R";for(p=m;p<n;++p)l[p]=q;m=n-1}for(var n=[],r,m=0;m<k;)if(f.test(l[m])){p=m;for(++m;m<k&&f.test(l[m]);++m);n.push(new b(0,p,m))}else{var t=m,q=n.length;for(++m;m<k&&"L"!=l[m];++m);for(p=t;p<m;)if(g.test(l[p])){t<p&&n.splice(q,0,new b(1,t,p));t=p;for(++p;p<m&&g.test(l[p]);++p);n.splice(q,0,new b(2,t,p));t=p}else++p;t<m&&n.splice(q,0,new b(1,
+t,m))}1==n[0].level&&(r=h.match(/^\s+/))&&(n[0].from=r[0].length,n.unshift(new b(0,0,r[0].length)));1==A(n).level&&(r=h.match(/\s+$/))&&(A(n).to-=r[0].length,n.push(new b(0,k-r[0].length,k)));2==n[0].level&&n.unshift(new b(1,n[0].to,n[0].to));n[0].level!=A(n).level&&n.push(new b(n[0].level,k,k));return n}}();q.version="5.2.0";return q});
\ No newline at end of file
diff --git a/js/ckeditor/samples/toolbarconfigurator/lib/codemirror/javascript.js b/js/ckeditor/samples/toolbarconfigurator/lib/codemirror/javascript.js
new file mode 100644 (file)
index 0000000..c76ab46
--- /dev/null
@@ -0,0 +1,25 @@
+(function(p){"object"==typeof exports&&"object"==typeof module?p(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],p):p(CodeMirror)})(function(p){p.defineMode("javascript",function(oa,t){function q(a,c,e){E=a;I=e;return c}function w(a,c){var e=a.next();if('"'==e||"'"==e)return c.tokenize=pa(e),c.tokenize(a,c);if("."==e&&a.match(/^\d+(?:[eE][+\-]?\d+)?/))return q("number","number");if("."==e&&a.match(".."))return q("spread","meta");if(/[\[\]{}\(\),;\:\.]/.test(e))return q(e);
+if("\x3d"==e&&a.eat("\x3e"))return q("\x3d\x3e","operator");if("0"==e&&a.eat(/x/i))return a.eatWhile(/[\da-f]/i),q("number","number");if(/\d/.test(e))return a.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/),q("number","number");if("/"==e){if(a.eat("*"))return c.tokenize=J,J(a,c);if(a.eat("/"))return a.skipToEnd(),q("comment","comment");if("operator"==c.lastType||"keyword c"==c.lastType||"sof"==c.lastType||/^[\[{}\(,;:]$/.test(c.lastType)){a:for(var e=!1,d,b=!1;null!=(d=a.next());){if(!e){if("/"==d&&!b)break a;
+"["==d?b=!0:b&&"]"==d&&(b=!1)}e=!e&&"\\"==d}a.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/);return q("regexp","string-2")}a.eatWhile(K);return q("operator","operator",a.current())}if("`"==e)return c.tokenize=Q,Q(a,c);if("#"==e)return a.skipToEnd(),q("error","error");if(K.test(e))return a.eatWhile(K),q("operator","operator",a.current());if(R.test(e))return a.eatWhile(R),e=a.current(),(d=ba.propertyIsEnumerable(e)&&ba[e])&&"."!=c.lastType?q(d.type,d.style,e):q("variable","variable",e)}function pa(a){return function(c,
+e){var d=!1,b;if(L&&"@"==c.peek()&&c.match(qa))return e.tokenize=w,q("jsonld-keyword","meta");for(;null!=(b=c.next())&&(b!=a||d);)d=!d&&"\\"==b;d||(e.tokenize=w);return q("string","string")}}function J(a,c){for(var e=!1,d;d=a.next();){if("/"==d&&e){c.tokenize=w;break}e="*"==d}return q("comment","comment")}function Q(a,c){for(var e=!1,d;null!=(d=a.next());){if(!e&&("`"==d||"$"==d&&a.eat("{"))){c.tokenize=w;break}e=!e&&"\\"==d}return q("quasi","string-2",a.current())}function S(a,c){c.fatArrowAt&&(c.fatArrowAt=
+null);var e=a.string.indexOf("\x3d\x3e",a.start);if(!(0>e)){for(var d=0,b=!1,e=e-1;0<=e;--e){var f=a.string.charAt(e),g="([{}])".indexOf(f);if(0<=g&&3>g){if(!d){++e;break}if(0==--d)break}else if(3<=g&&6>g)++d;else if(R.test(f))b=!0;else{if(/["'\/]/.test(f))return;if(b&&!d){++e;break}}}b&&!d&&(c.fatArrowAt=e)}}function ca(a,c,b,d,f,h){this.indented=a;this.column=c;this.type=b;this.prev=f;this.info=h;null!=d&&(this.align=d)}function g(){for(var a=arguments.length-1;0<=a;a--)f.cc.push(arguments[a])}
+function b(){g.apply(null,arguments);return!0}function x(a){function c(c){for(;c;c=c.next)if(c.name==a)return!0;return!1}var b=f.state;b.context?(f.marked="def",c(b.localVars)||(b.localVars={name:a,next:b.localVars})):!c(b.globalVars)&&t.globalVars&&(b.globalVars={name:a,next:b.globalVars})}function y(){f.state.context={prev:f.state.context,vars:f.state.localVars};f.state.localVars=ra}function z(){f.state.localVars=f.state.context.vars;f.state.context=f.state.context.prev}function l(a,c){var b=function(){var b=
+f.state,e=b.indented;if("stat"==b.lexical.type)e=b.lexical.indented;else for(var h=b.lexical;h&&")"==h.type&&h.align;h=h.prev)e=h.indented;b.lexical=new ca(e,f.stream.column(),a,null,b.lexical,c)};b.lex=!0;return b}function k(){var a=f.state;a.lexical.prev&&(")"==a.lexical.type&&(a.indented=a.lexical.indented),a.lexical=a.lexical.prev)}function m(a){function c(e){return e==a?b():";"==a?g():b(c)}return c}function r(a,c){return"var"==a?b(l("vardef",c.length),T,m(";"),k):"keyword a"==a?b(l("form"),n,
+r,k):"keyword b"==a?b(l("form"),r,k):"{"==a?b(l("}"),U,k):";"==a?b():"if"==a?("else"==f.state.lexical.info&&f.state.cc[f.state.cc.length-1]==k&&f.state.cc.pop()(),b(l("form"),n,r,k,da)):"function"==a?b(v):"for"==a?b(l("form"),ea,r,k):"variable"==a?b(l("stat"),sa):"switch"==a?b(l("form"),n,l("}","switch"),m("{"),U,k,k):"case"==a?b(n,m(":")):"default"==a?b(m(":")):"catch"==a?b(l("form"),y,m("("),V,m(")"),r,k,z):"module"==a?b(l("form"),y,ta,z,k):"class"==a?b(l("form"),ua,k):"export"==a?b(l("form"),va,
+k):"import"==a?b(l("form"),wa,k):g(l("stat"),n,m(";"),k)}function n(a){return fa(a,!1)}function u(a){return fa(a,!0)}function fa(a,c){if(f.state.fatArrowAt==f.stream.start){var e=c?ga:ha;if("("==a)return b(y,l(")"),F(A,")"),k,m("\x3d\x3e"),e,z);if("variable"==a)return g(y,A,m("\x3d\x3e"),e,z)}e=c?W:M;return xa.hasOwnProperty(a)?b(e):"function"==a?b(v,e):"keyword c"==a?b(c?ia:X):"("==a?b(l(")"),X,N,m(")"),k,e):"operator"==a||"spread"==a?b(c?u:n):"["==a?b(l("]"),ya,k,e):"{"==a?G(za,"}",null,e):"quasi"==
+a?g(O,e):b()}function X(a){return a.match(/[;\}\)\],]/)?g():g(n)}function ia(a){return a.match(/[;\}\)\],]/)?g():g(u)}function M(a,c){return","==a?b(n):W(a,c,!1)}function W(a,c,e){var d=0==e?M:W,f=0==e?n:u;if("\x3d\x3e"==a)return b(y,e?ga:ha,z);if("operator"==a)return/\+\+|--/.test(c)?b(d):"?"==c?b(n,m(":"),f):b(f);if("quasi"==a)return g(O,d);if(";"!=a){if("("==a)return G(u,")","call",d);if("."==a)return b(Aa,d);if("["==a)return b(l("]"),X,m("]"),k,d)}}function O(a,c){return"quasi"!=a?g():"${"!=c.slice(c.length-
+2)?b(O):b(n,Ba)}function Ba(a){if("}"==a)return f.marked="string-2",f.state.tokenize=Q,b(O)}function ha(a){S(f.stream,f.state);return g("{"==a?r:n)}function ga(a){S(f.stream,f.state);return g("{"==a?r:u)}function sa(a){return":"==a?b(k,r):g(M,m(";"),k)}function Aa(a){if("variable"==a)return f.marked="property",b()}function za(a,c){if("variable"==a||"keyword"==f.style)return f.marked="property","get"==c||"set"==c?b(Ca):b(H);if("number"==a||"string"==a)return f.marked=L?"property":f.style+" property",
+b(H);if("jsonld-keyword"==a)return b(H);if("["==a)return b(n,m("]"),H)}function Ca(a){if("variable"!=a)return g(H);f.marked="property";return b(v)}function H(a){if(":"==a)return b(u);if("("==a)return g(v)}function F(a,c){function e(d){return","==d?(d=f.state.lexical,"call"==d.info&&(d.pos=(d.pos||0)+1),b(a,e)):d==c?b():b(m(c))}return function(d){return d==c?b():g(a,e)}}function G(a,c,e){for(var d=3;d<arguments.length;d++)f.cc.push(arguments[d]);return b(l(c,e),F(a,c),k)}function U(a){return"}"==a?
+b():g(r,U)}function ja(a){if(ka&&":"==a)return b(Da)}function Da(a){if("variable"==a)return f.marked="variable-3",b()}function T(){return g(A,ja,Y,Ea)}function A(a,c){if("variable"==a)return x(c),b();if("["==a)return G(A,"]");if("{"==a)return G(Fa,"}")}function Fa(a,c){if("variable"==a&&!f.stream.match(/^\s*:/,!1))return x(c),b(Y);"variable"==a&&(f.marked="property");return b(m(":"),A,Y)}function Y(a,c){if("\x3d"==c)return b(u)}function Ea(a){if(","==a)return b(T)}function da(a,c){if("keyword b"==
+a&&"else"==c)return b(l("form","else"),r,k)}function ea(a){if("("==a)return b(l(")"),Ga,m(")"),k)}function Ga(a){return"var"==a?b(T,m(";"),P):";"==a?b(P):"variable"==a?b(Ha):g(n,m(";"),P)}function Ha(a,c){return"in"==c||"of"==c?(f.marked="keyword",b(n)):b(M,P)}function P(a,c){return";"==a?b(la):"in"==c||"of"==c?(f.marked="keyword",b(n)):g(n,m(";"),la)}function la(a){")"!=a&&b(n)}function v(a,c){if("*"==c)return f.marked="keyword",b(v);if("variable"==a)return x(c),b(v);if("("==a)return b(y,l(")"),
+F(V,")"),k,r,z)}function V(a){return"spread"==a?b(V):g(A,ja)}function ua(a,c){if("variable"==a)return x(c),b(ma)}function ma(a,c){if("extends"==c)return b(n,ma);if("{"==a)return b(l("}"),B,k)}function B(a,c){if("variable"==a||"keyword"==f.style){if("static"==c)return f.marked="keyword",b(B);f.marked="property";return"get"==c||"set"==c?b(Ia,v,B):b(v,B)}if("*"==c)return f.marked="keyword",b(B);if(";"==a)return b(B);if("}"==a)return b()}function Ia(a){if("variable"!=a)return g();f.marked="property";
+return b()}function ta(a,c){if("string"==a)return b(r);if("variable"==a)return x(c),b(Z)}function va(a,c){return"*"==c?(f.marked="keyword",b(Z,m(";"))):"default"==c?(f.marked="keyword",b(n,m(";"))):g(r)}function wa(a){return"string"==a?b():g(aa,Z)}function aa(a,c){if("{"==a)return G(aa,"}");"variable"==a&&x(c);"*"==c&&(f.marked="keyword");return b(Ja)}function Ja(a,c){if("as"==c)return f.marked="keyword",b(aa)}function Z(a,c){if("from"==c)return f.marked="keyword",b(n)}function ya(a){return"]"==a?
+b():g(u,Ka)}function Ka(a){return"for"==a?g(N,m("]")):","==a?b(F(ia,"]")):g(F(u,"]"))}function N(a){if("for"==a)return b(ea,N);if("if"==a)return b(n,N)}var C=oa.indentUnit,na=t.statementIndent,L=t.jsonld,D=t.json||L,ka=t.typescript,R=t.wordCharacters||/[\w$\xa1-\uffff]/,ba=function(){function a(a){return{type:a,style:"keyword"}}var c=a("keyword a"),b=a("keyword b"),d=a("keyword c"),f=a("operator"),h={type:"atom",style:"atom"},c={"if":a("if"),"while":c,"with":c,"else":b,"do":b,"try":b,"finally":b,
+"return":d,"break":d,"continue":d,"new":d,"delete":d,"throw":d,"debugger":d,"var":a("var"),"const":a("var"),let:a("var"),"function":a("function"),"catch":a("catch"),"for":a("for"),"switch":a("switch"),"case":a("case"),"default":a("default"),"in":f,"typeof":f,"instanceof":f,"true":h,"false":h,"null":h,undefined:h,NaN:h,Infinity:h,"this":a("this"),module:a("module"),"class":a("class"),"super":a("atom"),yield:d,"export":a("export"),"import":a("import"),"extends":d};if(ka){var b={type:"variable",style:"variable-3"},
+b={"interface":a("interface"),"extends":a("extends"),constructor:a("constructor"),"public":a("public"),"private":a("private"),"protected":a("protected"),"static":a("static"),string:b,number:b,bool:b,any:b},g;for(g in b)c[g]=b[g]}return c}(),K=/[+\-*&%=<>!?|~^]/,qa=/^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/,E,I,xa={atom:!0,number:!0,variable:!0,string:!0,regexp:!0,"this":!0,"jsonld-keyword":!0},f={state:null,column:null,marked:null,cc:null},ra={name:"this",
+next:{name:"arguments"}};k.lex=!0;return{startState:function(a){a={tokenize:w,lastType:"sof",cc:[],lexical:new ca((a||0)-C,0,"block",!1),localVars:t.localVars,context:t.localVars&&{vars:t.localVars},indented:0};t.globalVars&&"object"==typeof t.globalVars&&(a.globalVars=t.globalVars);return a},token:function(a,b){a.sol()&&(b.lexical.hasOwnProperty("align")||(b.lexical.align=!1),b.indented=a.indentation(),S(a,b));if(b.tokenize!=J&&a.eatSpace())return null;var e=b.tokenize(a,b);if("comment"==E)return e;
+b.lastType="operator"!=E||"++"!=I&&"--"!=I?E:"incdec";a:{var d=E,g=I,h=b.cc;f.state=b;f.stream=a;f.marked=null;f.cc=h;f.style=e;b.lexical.hasOwnProperty("align")||(b.lexical.align=!0);for(;;)if((h.length?h.pop():D?n:r)(d,g)){for(;h.length&&h[h.length-1].lex;)h.pop()();if(f.marked){e=f.marked;break a}if(d="variable"==d)b:{for(d=b.localVars;d;d=d.next)if(d.name==g){d=!0;break b}for(h=b.context;h;h=h.prev)for(d=h.vars;d;d=d.next)if(d.name==g){d=!0;break b}d=void 0}if(d){e="variable-2";break a}break a}}return e},
+indent:function(a,b){if(a.tokenize==J)return p.Pass;if(a.tokenize!=w)return 0;var e=b&&b.charAt(0),d=a.lexical;if(!/^\s*else\b/.test(b))for(var f=a.cc.length-1;0<=f;--f){var g=a.cc[f];if(g==k)d=d.prev;else if(g!=da)break}"stat"==d.type&&"}"==e&&(d=d.prev);na&&")"==d.type&&"stat"==d.prev.type&&(d=d.prev);f=d.type;g=e==f;return"vardef"==f?d.indented+("operator"==a.lastType||","==a.lastType?d.info+1:0):"form"==f&&"{"==e?d.indented:"form"==f?d.indented+C:"stat"==f?(e=d.indented,d="operator"==a.lastType||
+","==a.lastType||K.test(b.charAt(0))||/[,.]/.test(b.charAt(0)),e+(d?na||C:0)):"switch"!=d.info||g||0==t.doubleIndentSwitch?d.align?d.column+(g?0:1):d.indented+(g?0:C):d.indented+(/^(?:case|default)\b/.test(b)?C:2*C)},electricInput:/^\s*(?:case .*?:|default:|\{|\})$/,blockCommentStart:D?null:"/*",blockCommentEnd:D?null:"*/",lineComment:D?null:"//",fold:"brace",closeBrackets:"()[]{}''\"\"``",helperType:D?"json":"javascript",jsonldMode:L,jsonMode:D}});p.registerHelper("wordChars","javascript",/[\w$]/);
+p.defineMIME("text/javascript","javascript");p.defineMIME("text/ecmascript","javascript");p.defineMIME("application/javascript","javascript");p.defineMIME("application/x-javascript","javascript");p.defineMIME("application/ecmascript","javascript");p.defineMIME("application/json",{name:"javascript",json:!0});p.defineMIME("application/x-json",{name:"javascript",json:!0});p.defineMIME("application/ld+json",{name:"javascript",jsonld:!0});p.defineMIME("text/typescript",{name:"javascript",typescript:!0});
+p.defineMIME("application/typescript",{name:"javascript",typescript:!0})});
\ No newline at end of file
diff --git a/js/ckeditor/samples/toolbarconfigurator/lib/codemirror/neo.css b/js/ckeditor/samples/toolbarconfigurator/lib/codemirror/neo.css
new file mode 100644 (file)
index 0000000..f932db0
--- /dev/null
@@ -0,0 +1,36 @@
+/* neo theme for codemirror */\r
+\r
+/* Color scheme */\r
+\r
+.cm-s-neo.CodeMirror {\r
+  background-color:#ffffff;\r
+  color:#2e383c;\r
+  line-height:1.4375;\r
+}\r
+.cm-s-neo .cm-comment {color:#75787b}\r
+.cm-s-neo .cm-keyword, .cm-s-neo .cm-property {color:#1d75b3}\r
+.cm-s-neo .cm-atom,.cm-s-neo .cm-number {color:#75438a}\r
+.cm-s-neo .cm-node,.cm-s-neo .cm-tag {color:#9c3328}\r
+.cm-s-neo .cm-string {color:#b35e14}\r
+.cm-s-neo .cm-variable,.cm-s-neo .cm-qualifier {color:#047d65}\r
+\r
+\r
+/* Editor styling */\r
+\r
+.cm-s-neo pre {\r
+  padding:0;\r
+}\r
+\r
+.cm-s-neo .CodeMirror-gutters {\r
+  border:none;\r
+  border-right:10px solid transparent;\r
+  background-color:transparent;\r
+}\r
+\r
+.cm-s-neo .CodeMirror-linenumber {\r
+  padding:0;\r
+  color:#e0e2e5;\r
+}\r
+\r
+.cm-s-neo .CodeMirror-guttermarker { color: #1d75b3; }\r
+.cm-s-neo .CodeMirror-guttermarker-subtle { color: #e0e2e5; }\r
diff --git a/js/ckeditor/samples/toolbarconfigurator/lib/codemirror/show-hint.css b/js/ckeditor/samples/toolbarconfigurator/lib/codemirror/show-hint.css
new file mode 100644 (file)
index 0000000..e38bfb6
--- /dev/null
@@ -0,0 +1,38 @@
+.CodeMirror-hints {\r
+  position: absolute;\r
+  z-index: 10;\r
+  overflow: hidden;\r
+  list-style: none;\r
+\r
+  margin: 0;\r
+  padding: 2px;\r
+\r
+  -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2);\r
+  -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2);\r
+  box-shadow: 2px 3px 5px rgba(0,0,0,.2);\r
+  border-radius: 3px;\r
+  border: 1px solid silver;\r
+\r
+  background: white;\r
+  font-size: 90%;\r
+  font-family: monospace;\r
+\r
+  max-height: 20em;\r
+  overflow-y: auto;\r
+}\r
+\r
+.CodeMirror-hint {\r
+  margin: 0;\r
+  padding: 0 4px;\r
+  border-radius: 2px;\r
+  max-width: 19em;\r
+  overflow: hidden;\r
+  white-space: pre;\r
+  color: black;\r
+  cursor: pointer;\r
+}\r
+\r
+li.CodeMirror-hint-active {\r
+  background: #08f;\r
+  color: white;\r
+}\r
diff --git a/js/ckeditor/samples/toolbarconfigurator/lib/codemirror/show-hint.js b/js/ckeditor/samples/toolbarconfigurator/lib/codemirror/show-hint.js
new file mode 100644 (file)
index 0000000..072359c
--- /dev/null
@@ -0,0 +1,16 @@
+(function(f){"object"==typeof exports&&"object"==typeof module?f(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],f):f(CodeMirror)})(function(f){function p(a,b){this.cm=a;this.options=this.buildOptions(b);this.widget=null;this.tick=this.debounce=0;this.startPos=this.cm.getCursor();this.startLen=this.cm.getLine(this.startPos.line).length;var c=this;a.on("cursorActivity",this.activityFunc=function(){c.cursorActivity()})}function w(a,b){function c(a,
+c){var d;d="string"!=typeof c?function(a){return c(a,b)}:e.hasOwnProperty(c)?e[c]:c;f[a]=d}var e={Up:function(){b.moveFocus(-1)},Down:function(){b.moveFocus(1)},PageUp:function(){b.moveFocus(-b.menuSize()+1,!0)},PageDown:function(){b.moveFocus(b.menuSize()-1,!0)},Home:function(){b.setFocus(0)},End:function(){b.setFocus(b.length-1)},Enter:b.pick,Tab:b.pick,Esc:b.close},d=a.options.customKeys,f=d?{}:e;if(d)for(var g in d)d.hasOwnProperty(g)&&c(g,d[g]);if(d=a.options.extraKeys)for(g in d)d.hasOwnProperty(g)&&
+c(g,d[g]);return f}function v(a,b){for(;b&&b!=a;){if("LI"===b.nodeName.toUpperCase()&&b.parentNode==a)return b;b=b.parentNode}}function n(a,b){this.completion=a;this.data=b;this.picked=!1;var c=this,e=a.cm,d=this.hints=document.createElement("ul");d.className="CodeMirror-hints";this.selectedHint=b.selectedHint||0;for(var m=b.list,g=0;g<m.length;++g){var l=d.appendChild(document.createElement("li")),h=m[g],k="CodeMirror-hint"+(g!=this.selectedHint?"":" CodeMirror-hint-active");null!=h.className&&(k=
+h.className+" "+k);l.className=k;h.render?h.render(l,b,h):l.appendChild(document.createTextNode(h.displayText||("string"==typeof h?h:h.text)));l.hintId=g}var g=e.cursorCoords(a.options.alignWithWord?b.from:null),r=g.left,t=g.bottom,n=!0;d.style.left=r+"px";d.style.top=t+"px";l=window.innerWidth||Math.max(document.body.offsetWidth,document.documentElement.offsetWidth);k=window.innerHeight||Math.max(document.body.offsetHeight,document.documentElement.offsetHeight);(a.options.container||document.body).appendChild(d);
+h=d.getBoundingClientRect();if(0<h.bottom-k){var u=h.bottom-h.top;0<g.top-(g.bottom-h.top)-u?(d.style.top=(t=g.top-u)+"px",n=!1):u>k&&(d.style.height=k-5+"px",d.style.top=(t=g.bottom-h.top)+"px",k=e.getCursor(),b.from.ch!=k.ch&&(g=e.cursorCoords(k),d.style.left=(r=g.left)+"px",h=d.getBoundingClientRect()))}k=h.right-l;0<k&&(h.right-h.left>l&&(d.style.width=l-5+"px",k-=h.right-h.left-l),d.style.left=(r=g.left-k)+"px");e.addKeyMap(this.keyMap=w(a,{moveFocus:function(a,b){c.changeActive(c.selectedHint+
+a,b)},setFocus:function(a){c.changeActive(a)},menuSize:function(){return c.screenAmount()},length:m.length,close:function(){a.close()},pick:function(){c.pick()},data:b}));if(a.options.closeOnUnfocus){var p;e.on("blur",this.onBlur=function(){p=setTimeout(function(){a.close()},100)});e.on("focus",this.onFocus=function(){clearTimeout(p)})}var q=e.getScrollInfo();e.on("scroll",this.onScroll=function(){var c=e.getScrollInfo(),b=e.getWrapperElement().getBoundingClientRect(),f=t+q.top-c.top,g=f-(window.pageYOffset||
+(document.documentElement||document.body).scrollTop);n||(g+=d.offsetHeight);if(g<=b.top||g>=b.bottom)return a.close();d.style.top=f+"px";d.style.left=r+q.left-c.left+"px"});f.on(d,"dblclick",function(a){(a=v(d,a.target||a.srcElement))&&null!=a.hintId&&(c.changeActive(a.hintId),c.pick())});f.on(d,"click",function(b){(b=v(d,b.target||b.srcElement))&&null!=b.hintId&&(c.changeActive(b.hintId),a.options.completeOnSingleClick&&c.pick())});f.on(d,"mousedown",function(){setTimeout(function(){e.focus()},20)});
+f.signal(b,"select",m[0],d.firstChild);return!0}f.showHint=function(a,b,c){if(!b)return a.showHint(c);c&&c.async&&(b.async=!0);b={hint:b};if(c)for(var e in c)b[e]=c[e];return a.showHint(b)};f.defineExtension("showHint",function(a){1<this.listSelections().length||this.somethingSelected()||(this.state.completionActive&&this.state.completionActive.close(),a=this.state.completionActive=new p(this,a),a.options.hint&&(f.signal(this,"startCompletion",this),a.update()))});var x=window.requestAnimationFrame||
+function(a){return setTimeout(a,1E3/60)},y=window.cancelAnimationFrame||clearTimeout;p.prototype={close:function(){this.active()&&(this.tick=this.cm.state.completionActive=null,this.cm.off("cursorActivity",this.activityFunc),this.widget&&this.widget.close(),f.signal(this.cm,"endCompletion",this.cm))},active:function(){return this.cm.state.completionActive==this},pick:function(a,b){var c=a.list[b];c.hint?c.hint(this.cm,a,c):this.cm.replaceRange("string"==typeof c?c:c.text,c.from||a.from,c.to||a.to,
+"complete");f.signal(a,"pick",c);this.close()},showHints:function(a){if(!a||!a.list.length||!this.active())return this.close();this.options.completeSingle&&1==a.list.length?this.pick(a,0):this.showWidget(a)},cursorActivity:function(){this.debounce&&(y(this.debounce),this.debounce=0);var a=this.cm.getCursor(),b=this.cm.getLine(a.line);if(a.line!=this.startPos.line||b.length-a.ch!=this.startLen-this.startPos.ch||a.ch<this.startPos.ch||this.cm.somethingSelected()||a.ch&&this.options.closeCharacters.test(b.charAt(a.ch-
+1)))this.close();else{var c=this;this.debounce=x(function(){c.update()});this.widget&&this.widget.disable()}},update:function(){if(null!=this.tick)if(this.data&&f.signal(this.data,"update"),this.options.hint.async){var a=++this.tick,b=this;this.options.hint(this.cm,function(c){b.tick==a&&b.finishUpdate(c)},this.options)}else this.finishUpdate(this.options.hint(this.cm,this.options),a)},finishUpdate:function(a){this.data=a;var b=this.widget&&this.widget.picked;this.widget&&this.widget.close();a&&a.list.length&&
+(b&&1==a.list.length?this.pick(a,0):this.widget=new n(this,a))},showWidget:function(a){this.data=a;this.widget=new n(this,a);f.signal(a,"shown")},buildOptions:function(a){var b=this.cm.options.hintOptions,c={},e;for(e in q)c[e]=q[e];if(b)for(e in b)void 0!==b[e]&&(c[e]=b[e]);if(a)for(e in a)void 0!==a[e]&&(c[e]=a[e]);return c}};n.prototype={close:function(){if(this.completion.widget==this){this.completion.widget=null;this.hints.parentNode.removeChild(this.hints);this.completion.cm.removeKeyMap(this.keyMap);
+var a=this.completion.cm;this.completion.options.closeOnUnfocus&&(a.off("blur",this.onBlur),a.off("focus",this.onFocus));a.off("scroll",this.onScroll)}},disable:function(){this.completion.cm.removeKeyMap(this.keyMap);var a=this;this.keyMap={Enter:function(){a.picked=!0}};this.completion.cm.addKeyMap(this.keyMap)},pick:function(){this.completion.pick(this.data,this.selectedHint)},changeActive:function(a,b){a>=this.data.list.length?a=b?this.data.list.length-1:0:0>a&&(a=b?0:this.data.list.length-1);
+if(this.selectedHint!=a){var c=this.hints.childNodes[this.selectedHint];c.className=c.className.replace(" CodeMirror-hint-active","");c=this.hints.childNodes[this.selectedHint=a];c.className+=" CodeMirror-hint-active";c.offsetTop<this.hints.scrollTop?this.hints.scrollTop=c.offsetTop-3:c.offsetTop+c.offsetHeight>this.hints.scrollTop+this.hints.clientHeight&&(this.hints.scrollTop=c.offsetTop+c.offsetHeight-this.hints.clientHeight+3);f.signal(this.data,"select",this.data.list[this.selectedHint],c)}},
+screenAmount:function(){return Math.floor(this.hints.clientHeight/this.hints.firstChild.offsetHeight)||1}};f.registerHelper("hint","auto",function(a,b){var c=a.getHelpers(a.getCursor(),"hint");if(c.length)for(var e=0;e<c.length;e++){var d=c[e](a,b);if(d&&d.list.length)return d}else if(c=a.getHelper(a.getCursor(),"hintWords")){if(c)return f.hint.fromList(a,{words:c})}else if(f.hint.anyword)return f.hint.anyword(a,b)});f.registerHelper("hint","fromList",function(a,b){for(var c=a.getCursor(),e=a.getTokenAt(c),
+d=[],m=0;m<b.words.length;m++){var g=b.words[m];g.slice(0,e.string.length)==e.string&&d.push(g)}if(d.length)return{list:d,from:f.Pos(c.line,e.start),to:f.Pos(c.line,e.end)}});f.commands.autocomplete=f.showHint;var q={hint:f.hint.auto,completeSingle:!0,alignWithWord:!0,closeCharacters:/[\s()\[\]{};:>,]/,closeOnUnfocus:!0,completeOnSingleClick:!1,container:null,customKeys:null,extraKeys:null};f.defineOption("hintOptions",null)});
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono-lisa/dialog.css b/js/ckeditor/skins/moono-lisa/dialog.css
new file mode 100644 (file)
index 0000000..76f8854
--- /dev/null
@@ -0,0 +1,5 @@
+/*\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+*/\r
+.cke_dialog{visibility:visible}.cke_dialog_body{z-index:1;background:#fff}.cke_dialog strong{font-weight:bold}.cke_dialog_title{font-weight:bold;font-size:12px;cursor:move;position:relative;color:#484848;border-bottom:1px solid #d1d1d1;padding:12px 19px 12px 12px;background:#f8f8f8;letter-spacing:.3px}.cke_dialog_spinner{border-radius:50%;width:12px;height:12px;overflow:hidden;text-indent:-9999em;border:2px solid rgba(102,102,102,0.2);border-left-color:rgba(102,102,102,1);-webkit-animation:dialog_spinner 1s infinite linear;animation:dialog_spinner 1s infinite linear}.cke_browser_ie8 .cke_dialog_spinner,.cke_browser_ie9 .cke_dialog_spinner{background:url(images/spinner.gif) center top no-repeat;width:16px;height:16px;border:0}@-webkit-keyframes dialog_spinner{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes dialog_spinner{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.cke_dialog_contents{background-color:#fff;overflow:auto;padding:15px 10px 5px 10px;margin-top:43px;border-top:1px solid #d1d1d1}.cke_dialog_contents_body{overflow:auto;padding:9px 10px 5px 10px;margin-top:22px}.cke_dialog_footer{text-align:right;position:relative;border-top:1px solid #d1d1d1;background:#f8f8f8}.cke_rtl .cke_dialog_footer{text-align:left}.cke_hc .cke_dialog_footer{outline:0;border-top:1px solid #fff}.cke_dialog .cke_resizer{margin-top:22px}.cke_dialog .cke_resizer_rtl{margin-left:5px}.cke_dialog .cke_resizer_ltr{margin-right:5px}.cke_dialog_tabs{height:33px;display:inline-block;margin:9px 0 0;position:absolute;z-index:2;left:11px}.cke_rtl .cke_dialog_tabs{left:auto;right:11px}a.cke_dialog_tab{height:25px;padding:4px 8px;display:inline-block;cursor:pointer;line-height:26px;outline:0;color:#484848;border:1px solid #d1d1d1;border-radius:3px 3px 0 0;background:#f8f8f8;min-width:90px;text-align:center;margin-left:-1px;letter-spacing:.3px}a.cke_dialog_tab:hover{background-color:#fff}a.cke_dialog_tab:focus{border:2px solid #139ff7;border-bottom-color:#d1d1d1;padding:3px 7px;position:relative;z-index:1}a.cke_dialog_tab_selected{background:#fff;border-bottom-color:#fff;cursor:default;filter:none}a.cke_dialog_tab_selected:hover,a.cke_dialog_tab_selected:focus{border-bottom-color:#fff}.cke_hc a.cke_dialog_tab:hover,.cke_hc a.cke_dialog_tab:focus,.cke_hc a.cke_dialog_tab_selected{border:3px solid;padding:2px 6px}a.cke_dialog_tab_disabled{color:#bababa;cursor:default}.cke_single_page .cke_dialog_tabs{display:none}.cke_single_page .cke_dialog_contents{padding-top:5px;margin-top:0;border-top:0}a.cke_dialog_close_button{background-image:url(images/close.png);background-repeat:no-repeat;background-position:50%;position:absolute;cursor:pointer;text-align:center;height:16px;width:16px;top:11px;z-index:5;opacity:.7;filter:alpha(opacity = 70)}.cke_rtl .cke_dialog_close_button{left:12px}.cke_ltr .cke_dialog_close_button{right:12px}.cke_hc a.cke_dialog_close_button{background-image:none}.cke_hidpi a.cke_dialog_close_button{background-image:url(images/hidpi/close.png);background-size:16px}a.cke_dialog_close_button:hover{opacity:1;filter:alpha(opacity = 100)}a.cke_dialog_close_button span{display:none}.cke_hc a.cke_dialog_close_button span{display:inline;cursor:pointer;font-weight:bold;position:relative;top:3px}div.cke_disabled .cke_dialog_ui_labeled_content div *{background-color:#ddd;cursor:default}.cke_dialog_ui_vbox table,.cke_dialog_ui_hbox table{margin:auto}.cke_dialog_ui_vbox_child{padding:5px 0}.cke_dialog_ui_hbox{width:100%;margin-top:12px}.cke_dialog_ui_hbox_first,.cke_dialog_ui_hbox_child,.cke_dialog_ui_hbox_last{vertical-align:top}.cke_ltr .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_ui_hbox_child{padding-right:10px}.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_child{padding-left:10px}.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-right:5px}.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-left:5px;padding-right:0}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:1px solid}textarea.cke_dialog_ui_input_textarea{overflow:auto;resize:none}input.cke_dialog_ui_input_text,input.cke_dialog_ui_input_password,textarea.cke_dialog_ui_input_textarea{background-color:#fff;border:1px solid #bcbcbc;padding:4px 6px;outline:0;width:100%;*width:95%;box-sizing:border-box;border-radius:2px;min-height:28px;margin-left:1px}input.cke_dialog_ui_input_text:hover,input.cke_dialog_ui_input_password:hover,textarea.cke_dialog_ui_input_textarea:hover{border:1px solid #aeb3b9}input.cke_dialog_ui_input_text:focus,input.cke_dialog_ui_input_password:focus,textarea.cke_dialog_ui_input_textarea:focus,select.cke_dialog_ui_input_select:focus{outline:0;border:2px solid #139ff7}input.cke_dialog_ui_input_text:focus{padding-left:5px}textarea.cke_dialog_ui_input_textarea:focus{padding:3px 5px}select.cke_dialog_ui_input_select:focus{margin:0;width:100%!important}input.cke_dialog_ui_checkbox_input,input.cke_dialog_ui_radio_input{margin-left:1px;margin-right:2px}input.cke_dialog_ui_checkbox_input:focus,input.cke_dialog_ui_checkbox_input:active,input.cke_dialog_ui_radio_input:focus,input.cke_dialog_ui_radio_input:active{border:0;outline:2px solid #139ff7}a.cke_dialog_ui_button{display:inline-block;*display:inline;*zoom:1;padding:4px 1px;margin:0;text-align:center;color:#484848;vertical-align:middle;cursor:pointer;border:1px solid #bcbcbc;border-radius:2px;background:#f8f8f8;letter-spacing:.3px;line-height:18px;box-sizing:border-box}.cke_hc a.cke_dialog_ui_button{border-width:3px}span.cke_dialog_ui_button{padding:0 10px;cursor:pointer}a.cke_dialog_ui_button:hover{background:#fff}a.cke_dialog_ui_button:focus,a.cke_dialog_ui_button:active{border:2px solid #139ff7;outline:0;padding:3px 0}.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button:focus,.cke_hc a.cke_dialog_ui_button:active{border:3px solid}.cke_dialog_footer_buttons a.cke_dialog_ui_button span{color:inherit;font-size:12px;font-weight:bold;padding:0 12px}a.cke_dialog_ui_button_ok{color:#fff;background:#09863e;border:1px solid #09863e}.cke_hc a.cke_dialog_ui_button{border:3px solid #bcbcbc}a.cke_dialog_ui_button_ok:hover{background:#53aa78;border-color:#53aa78}a.cke_dialog_ui_button_ok:focus{box-shadow:inset 0 0 0 2px #FFF}a.cke_dialog_ui_button_ok:focus,a.cke_dialog_ui_button_ok:active{border-color:#139ff7}.cke_hc a.cke_dialog_ui_button_ok:hover,.cke_hc a.cke_dialog_ui_button_ok:focus,.cke_hc a.cke_dialog_ui_button_ok:active{border-color:#484848}a.cke_dialog_ui_button_ok.cke_disabled{background:#d1d1d1;border-color:#d1d1d1;cursor:default}a.cke_dialog_ui_button_ok.cke_disabled span{cursor:default}.cke_dialog_footer_buttons{display:inline-table;margin:5px;width:auto;position:relative;vertical-align:middle}div.cke_dialog_ui_input_select{display:table}select.cke_dialog_ui_input_select{height:28px;line-height:28px;background-color:#fff;border:1px solid #bcbcbc;padding:3px 3px 3px 6px;outline:0;border-radius:2px;margin:0 1px;box-sizing:border-box;width:calc(100% - 2px)!important}.cke_dialog_ui_input_file{width:100%;height:25px}.cke_hc .cke_dialog_ui_labeled_content input:focus,.cke_hc .cke_dialog_ui_labeled_content select:focus,.cke_hc .cke_dialog_ui_labeled_content textarea:focus{outline:1px dotted}.cke_dialog_ui_labeled_label{margin-left:1px}.cke_dialog .cke_dark_background{background-color:transparent}.cke_dialog .cke_light_background{background-color:#ebebeb}.cke_dialog .cke_centered{text-align:center}.cke_dialog a.cke_btn_reset{float:right;background:url(images/refresh.png) top left no-repeat;width:16px;height:16px;border:1px none;font-size:1px}.cke_hidpi .cke_dialog a.cke_btn_reset{background-size:16px;background-image:url(images/hidpi/refresh.png)}.cke_rtl .cke_dialog a.cke_btn_reset{float:left}.cke_dialog a.cke_btn_locked,.cke_dialog a.cke_btn_unlocked{float:left;width:16px;height:16px;background-repeat:no-repeat;border:none 1px;font-size:1px}.cke_dialog a.cke_btn_locked,.cke_dialog a.cke_btn_unlocked,.cke_dialog a.cke_btn_reset{margin:2px}.cke_dialog a.cke_btn_locked{background-image:url(images/lock.png)}.cke_dialog a.cke_btn_unlocked{background-image:url(images/lock-open.png)}.cke_rtl .cke_dialog a.cke_btn_locked,.cke_rtl .cke_dialog a.cke_btn_unlocked{float:right}.cke_hidpi .cke_dialog a.cke_btn_unlocked,.cke_hidpi .cke_dialog a.cke_btn_locked{background-size:16px}.cke_hidpi .cke_dialog a.cke_btn_locked{background-image:url(images/hidpi/lock.png)}.cke_hidpi .cke_dialog a.cke_btn_unlocked{background-image:url(images/hidpi/lock-open.png)}.cke_dialog a.cke_btn_locked .cke_icon{display:none}.cke_dialog a.cke_btn_over,.cke_dialog a.cke_btn_locked:hover,.cke_dialog a.cke_btn_locked:focus,.cke_dialog a.cke_btn_locked:active,.cke_dialog a.cke_btn_unlocked:hover,.cke_dialog a.cke_btn_unlocked:focus,.cke_dialog a.cke_btn_unlocked:active,.cke_dialog a.cke_btn_reset:hover,.cke_dialog a.cke_btn_reset:focus,.cke_dialog a.cke_btn_reset:active{cursor:pointer;outline:0;margin:0;border:2px solid #139ff7}.cke_dialog fieldset{border:1px solid #bcbcbc}.cke_dialog fieldset legend{padding:0 6px}.cke_dialog_ui_checkbox,.cke_dialog fieldset .cke_dialog_ui_vbox .cke_dialog_ui_checkbox{display:inline-block}.cke_dialog fieldset .cke_dialog_ui_vbox .cke_dialog_ui_checkbox{padding-top:5px}.cke_dialog_ui_checkbox .cke_dialog_ui_checkbox_input,.cke_dialog_ui_checkbox .cke_dialog_ui_checkbox_input+label,.cke_dialog fieldset .cke_dialog_ui_vbox .cke_dialog_ui_checkbox .cke_dialog_ui_checkbox_input,.cke_dialog fieldset .cke_dialog_ui_vbox .cke_dialog_ui_checkbox .cke_dialog_ui_checkbox_input+label{vertical-align:middle}.cke_dialog .ImagePreviewBox{border:1px ridge #bcbcbc;overflow:scroll;height:200px;width:300px;padding:2px;background-color:white}.cke_dialog .ImagePreviewBox table td{white-space:normal}.cke_dialog .ImagePreviewLoader{position:absolute;white-space:normal;overflow:hidden;height:160px;width:230px;margin:2px;padding:2px;opacity:.9;filter:alpha(opacity = 90);background-color:#e4e4e4}.cke_dialog .FlashPreviewBox{white-space:normal;border:1px solid #bcbcbc;overflow:auto;height:160px;width:390px;padding:2px;background-color:white}.cke_dialog .cke_pastetext{width:346px;height:170px}.cke_dialog .cke_pastetext textarea{width:340px;height:170px;resize:none}.cke_dialog iframe.cke_pasteframe{width:346px;height:130px;background-color:white;border:1px solid #aeb3b9;border-radius:3px}.cke_dialog .cke_hand{cursor:pointer}.cke_disabled{color:#a0a0a0}.cke_dialog_body .cke_label{display:none}.cke_dialog_body label{display:inline;cursor:default;letter-spacing:.3px}.cke_dialog_body label+.cke_dialog_ui_labeled_content{margin-top:2px}.cke_dialog_contents_body .cke_dialog_ui_text,.cke_dialog_contents_body .cke_dialog_ui_select,.cke_dialog_contents_body .cke_dialog_ui_hbox_last>a.cke_dialog_ui_button{margin-top:4px}a.cke_smile{overflow:hidden;display:block;text-align:center;padding:.3em 0}a.cke_smile img{vertical-align:middle}a.cke_specialchar{cursor:inherit;display:block;height:1.25em;padding:.2em .3em;text-align:center}a.cke_smile,a.cke_specialchar{border:2px solid transparent}a.cke_smile:hover,a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:hover,a.cke_specialchar:focus,a.cke_specialchar:active{background:#fff;outline:0}a.cke_smile:hover,a.cke_specialchar:hover{border-color:#888}a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:focus,a.cke_specialchar:active{border-color:#139ff7}.cke_dialog_contents a.colorChooser{display:block;margin-top:6px;margin-left:10px;width:80px}.cke_rtl .cke_dialog_contents a.colorChooser{margin-right:10px}.cke_iframe_shim{display:block;position:absolute;top:0;left:0;z-index:-1;filter:alpha(opacity = 0);width:100%;height:100%}.cke_dialog_contents_body .cke_accessibility_legend{margin:2px 7px 2px 2px}.cke_dialog_contents_body .cke_accessibility_legend:focus,.cke_dialog_contents_body .cke_accessibility_legend:active{outline:0;border:2px solid #139ff7;margin:0 5px 0 0}.cke_dialog_contents_body input[type=file]:focus,.cke_dialog_contents_body input[type=file]:active{border:2px solid #139ff7}.cke_dialog_find_fieldset{margin-top:10px!important}.cke_dialog_image_ratiolock{margin-top:52px!important}.cke_dialog_forms_select_order label.cke_dialog_ui_labeled_label{margin-left:0}.cke_dialog_forms_select_order div.cke_dialog_ui_input_select{width:100%}.cke_dialog_forms_select_order_txtsize .cke_dialog_ui_hbox_last{padding-top:4px}.cke_dialog_image_url .cke_dialog_ui_hbox_last,.cke_dialog_flash_url .cke_dialog_ui_hbox_last{vertical-align:bottom}a.cke_dialog_ui_button.cke_dialog_image_browse{margin-top:10px}.cke_dialog_contents_body .cke_tpl_list{border:#bcbcbc 1px solid;margin:1px}.cke_dialog_contents_body .cke_tpl_list:focus,.cke_dialog_contents_body .cke_tpl_list:active{outline:0;margin:0;border:2px solid #139ff7}.cke_dialog_contents_body .cke_tpl_list a:focus,.cke_dialog_contents_body .cke_tpl_list a:active{outline:0}.cke_dialog_contents_body .cke_tpl_list a:focus .cke_tpl_item,.cke_dialog_contents_body .cke_tpl_list a:active .cke_tpl_item{border:2px solid #139ff7;padding:6px}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono-lisa/dialog_ie.css b/js/ckeditor/skins/moono-lisa/dialog_ie.css
new file mode 100644 (file)
index 0000000..cc53ab1
--- /dev/null
@@ -0,0 +1,5 @@
+/*\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+*/\r
+.cke_dialog{visibility:visible}.cke_dialog_body{z-index:1;background:#fff}.cke_dialog strong{font-weight:bold}.cke_dialog_title{font-weight:bold;font-size:12px;cursor:move;position:relative;color:#484848;border-bottom:1px solid #d1d1d1;padding:12px 19px 12px 12px;background:#f8f8f8;letter-spacing:.3px}.cke_dialog_spinner{border-radius:50%;width:12px;height:12px;overflow:hidden;text-indent:-9999em;border:2px solid rgba(102,102,102,0.2);border-left-color:rgba(102,102,102,1);-webkit-animation:dialog_spinner 1s infinite linear;animation:dialog_spinner 1s infinite linear}.cke_browser_ie8 .cke_dialog_spinner,.cke_browser_ie9 .cke_dialog_spinner{background:url(images/spinner.gif) center top no-repeat;width:16px;height:16px;border:0}@-webkit-keyframes dialog_spinner{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes dialog_spinner{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.cke_dialog_contents{background-color:#fff;overflow:auto;padding:15px 10px 5px 10px;margin-top:43px;border-top:1px solid #d1d1d1}.cke_dialog_contents_body{overflow:auto;padding:9px 10px 5px 10px;margin-top:22px}.cke_dialog_footer{text-align:right;position:relative;border-top:1px solid #d1d1d1;background:#f8f8f8}.cke_rtl .cke_dialog_footer{text-align:left}.cke_hc .cke_dialog_footer{outline:0;border-top:1px solid #fff}.cke_dialog .cke_resizer{margin-top:22px}.cke_dialog .cke_resizer_rtl{margin-left:5px}.cke_dialog .cke_resizer_ltr{margin-right:5px}.cke_dialog_tabs{height:33px;display:inline-block;margin:9px 0 0;position:absolute;z-index:2;left:11px}.cke_rtl .cke_dialog_tabs{left:auto;right:11px}a.cke_dialog_tab{height:25px;padding:4px 8px;display:inline-block;cursor:pointer;line-height:26px;outline:0;color:#484848;border:1px solid #d1d1d1;border-radius:3px 3px 0 0;background:#f8f8f8;min-width:90px;text-align:center;margin-left:-1px;letter-spacing:.3px}a.cke_dialog_tab:hover{background-color:#fff}a.cke_dialog_tab:focus{border:2px solid #139ff7;border-bottom-color:#d1d1d1;padding:3px 7px;position:relative;z-index:1}a.cke_dialog_tab_selected{background:#fff;border-bottom-color:#fff;cursor:default;filter:none}a.cke_dialog_tab_selected:hover,a.cke_dialog_tab_selected:focus{border-bottom-color:#fff}.cke_hc a.cke_dialog_tab:hover,.cke_hc a.cke_dialog_tab:focus,.cke_hc a.cke_dialog_tab_selected{border:3px solid;padding:2px 6px}a.cke_dialog_tab_disabled{color:#bababa;cursor:default}.cke_single_page .cke_dialog_tabs{display:none}.cke_single_page .cke_dialog_contents{padding-top:5px;margin-top:0;border-top:0}a.cke_dialog_close_button{background-image:url(images/close.png);background-repeat:no-repeat;background-position:50%;position:absolute;cursor:pointer;text-align:center;height:16px;width:16px;top:11px;z-index:5;opacity:.7;filter:alpha(opacity = 70)}.cke_rtl .cke_dialog_close_button{left:12px}.cke_ltr .cke_dialog_close_button{right:12px}.cke_hc a.cke_dialog_close_button{background-image:none}.cke_hidpi a.cke_dialog_close_button{background-image:url(images/hidpi/close.png);background-size:16px}a.cke_dialog_close_button:hover{opacity:1;filter:alpha(opacity = 100)}a.cke_dialog_close_button span{display:none}.cke_hc a.cke_dialog_close_button span{display:inline;cursor:pointer;font-weight:bold;position:relative;top:3px}div.cke_disabled .cke_dialog_ui_labeled_content div *{background-color:#ddd;cursor:default}.cke_dialog_ui_vbox table,.cke_dialog_ui_hbox table{margin:auto}.cke_dialog_ui_vbox_child{padding:5px 0}.cke_dialog_ui_hbox{width:100%;margin-top:12px}.cke_dialog_ui_hbox_first,.cke_dialog_ui_hbox_child,.cke_dialog_ui_hbox_last{vertical-align:top}.cke_ltr .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_ui_hbox_child{padding-right:10px}.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_child{padding-left:10px}.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-right:5px}.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-left:5px;padding-right:0}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:1px solid}textarea.cke_dialog_ui_input_textarea{overflow:auto;resize:none}input.cke_dialog_ui_input_text,input.cke_dialog_ui_input_password,textarea.cke_dialog_ui_input_textarea{background-color:#fff;border:1px solid #bcbcbc;padding:4px 6px;outline:0;width:100%;*width:95%;box-sizing:border-box;border-radius:2px;min-height:28px;margin-left:1px}input.cke_dialog_ui_input_text:hover,input.cke_dialog_ui_input_password:hover,textarea.cke_dialog_ui_input_textarea:hover{border:1px solid #aeb3b9}input.cke_dialog_ui_input_text:focus,input.cke_dialog_ui_input_password:focus,textarea.cke_dialog_ui_input_textarea:focus,select.cke_dialog_ui_input_select:focus{outline:0;border:2px solid #139ff7}input.cke_dialog_ui_input_text:focus{padding-left:5px}textarea.cke_dialog_ui_input_textarea:focus{padding:3px 5px}select.cke_dialog_ui_input_select:focus{margin:0;width:100%!important}input.cke_dialog_ui_checkbox_input,input.cke_dialog_ui_radio_input{margin-left:1px;margin-right:2px}input.cke_dialog_ui_checkbox_input:focus,input.cke_dialog_ui_checkbox_input:active,input.cke_dialog_ui_radio_input:focus,input.cke_dialog_ui_radio_input:active{border:0;outline:2px solid #139ff7}a.cke_dialog_ui_button{display:inline-block;*display:inline;*zoom:1;padding:4px 1px;margin:0;text-align:center;color:#484848;vertical-align:middle;cursor:pointer;border:1px solid #bcbcbc;border-radius:2px;background:#f8f8f8;letter-spacing:.3px;line-height:18px;box-sizing:border-box}.cke_hc a.cke_dialog_ui_button{border-width:3px}span.cke_dialog_ui_button{padding:0 10px;cursor:pointer}a.cke_dialog_ui_button:hover{background:#fff}a.cke_dialog_ui_button:focus,a.cke_dialog_ui_button:active{border:2px solid #139ff7;outline:0;padding:3px 0}.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button:focus,.cke_hc a.cke_dialog_ui_button:active{border:3px solid}.cke_dialog_footer_buttons a.cke_dialog_ui_button span{color:inherit;font-size:12px;font-weight:bold;padding:0 12px}a.cke_dialog_ui_button_ok{color:#fff;background:#09863e;border:1px solid #09863e}.cke_hc a.cke_dialog_ui_button{border:3px solid #bcbcbc}a.cke_dialog_ui_button_ok:hover{background:#53aa78;border-color:#53aa78}a.cke_dialog_ui_button_ok:focus{box-shadow:inset 0 0 0 2px #FFF}a.cke_dialog_ui_button_ok:focus,a.cke_dialog_ui_button_ok:active{border-color:#139ff7}.cke_hc a.cke_dialog_ui_button_ok:hover,.cke_hc a.cke_dialog_ui_button_ok:focus,.cke_hc a.cke_dialog_ui_button_ok:active{border-color:#484848}a.cke_dialog_ui_button_ok.cke_disabled{background:#d1d1d1;border-color:#d1d1d1;cursor:default}a.cke_dialog_ui_button_ok.cke_disabled span{cursor:default}.cke_dialog_footer_buttons{display:inline-table;margin:5px;width:auto;position:relative;vertical-align:middle}div.cke_dialog_ui_input_select{display:table}select.cke_dialog_ui_input_select{height:28px;line-height:28px;background-color:#fff;border:1px solid #bcbcbc;padding:3px 3px 3px 6px;outline:0;border-radius:2px;margin:0 1px;box-sizing:border-box;width:calc(100% - 2px)!important}.cke_dialog_ui_input_file{width:100%;height:25px}.cke_hc .cke_dialog_ui_labeled_content input:focus,.cke_hc .cke_dialog_ui_labeled_content select:focus,.cke_hc .cke_dialog_ui_labeled_content textarea:focus{outline:1px dotted}.cke_dialog_ui_labeled_label{margin-left:1px}.cke_dialog .cke_dark_background{background-color:transparent}.cke_dialog .cke_light_background{background-color:#ebebeb}.cke_dialog .cke_centered{text-align:center}.cke_dialog a.cke_btn_reset{float:right;background:url(images/refresh.png) top left no-repeat;width:16px;height:16px;border:1px none;font-size:1px}.cke_hidpi .cke_dialog a.cke_btn_reset{background-size:16px;background-image:url(images/hidpi/refresh.png)}.cke_rtl .cke_dialog a.cke_btn_reset{float:left}.cke_dialog a.cke_btn_locked,.cke_dialog a.cke_btn_unlocked{float:left;width:16px;height:16px;background-repeat:no-repeat;border:none 1px;font-size:1px}.cke_dialog a.cke_btn_locked,.cke_dialog a.cke_btn_unlocked,.cke_dialog a.cke_btn_reset{margin:2px}.cke_dialog a.cke_btn_locked{background-image:url(images/lock.png)}.cke_dialog a.cke_btn_unlocked{background-image:url(images/lock-open.png)}.cke_rtl .cke_dialog a.cke_btn_locked,.cke_rtl .cke_dialog a.cke_btn_unlocked{float:right}.cke_hidpi .cke_dialog a.cke_btn_unlocked,.cke_hidpi .cke_dialog a.cke_btn_locked{background-size:16px}.cke_hidpi .cke_dialog a.cke_btn_locked{background-image:url(images/hidpi/lock.png)}.cke_hidpi .cke_dialog a.cke_btn_unlocked{background-image:url(images/hidpi/lock-open.png)}.cke_dialog a.cke_btn_locked .cke_icon{display:none}.cke_dialog a.cke_btn_over,.cke_dialog a.cke_btn_locked:hover,.cke_dialog a.cke_btn_locked:focus,.cke_dialog a.cke_btn_locked:active,.cke_dialog a.cke_btn_unlocked:hover,.cke_dialog a.cke_btn_unlocked:focus,.cke_dialog a.cke_btn_unlocked:active,.cke_dialog a.cke_btn_reset:hover,.cke_dialog a.cke_btn_reset:focus,.cke_dialog a.cke_btn_reset:active{cursor:pointer;outline:0;margin:0;border:2px solid #139ff7}.cke_dialog fieldset{border:1px solid #bcbcbc}.cke_dialog fieldset legend{padding:0 6px}.cke_dialog_ui_checkbox,.cke_dialog fieldset .cke_dialog_ui_vbox .cke_dialog_ui_checkbox{display:inline-block}.cke_dialog fieldset .cke_dialog_ui_vbox .cke_dialog_ui_checkbox{padding-top:5px}.cke_dialog_ui_checkbox .cke_dialog_ui_checkbox_input,.cke_dialog_ui_checkbox .cke_dialog_ui_checkbox_input+label,.cke_dialog fieldset .cke_dialog_ui_vbox .cke_dialog_ui_checkbox .cke_dialog_ui_checkbox_input,.cke_dialog fieldset .cke_dialog_ui_vbox .cke_dialog_ui_checkbox .cke_dialog_ui_checkbox_input+label{vertical-align:middle}.cke_dialog .ImagePreviewBox{border:1px ridge #bcbcbc;overflow:scroll;height:200px;width:300px;padding:2px;background-color:white}.cke_dialog .ImagePreviewBox table td{white-space:normal}.cke_dialog .ImagePreviewLoader{position:absolute;white-space:normal;overflow:hidden;height:160px;width:230px;margin:2px;padding:2px;opacity:.9;filter:alpha(opacity = 90);background-color:#e4e4e4}.cke_dialog .FlashPreviewBox{white-space:normal;border:1px solid #bcbcbc;overflow:auto;height:160px;width:390px;padding:2px;background-color:white}.cke_dialog .cke_pastetext{width:346px;height:170px}.cke_dialog .cke_pastetext textarea{width:340px;height:170px;resize:none}.cke_dialog iframe.cke_pasteframe{width:346px;height:130px;background-color:white;border:1px solid #aeb3b9;border-radius:3px}.cke_dialog .cke_hand{cursor:pointer}.cke_disabled{color:#a0a0a0}.cke_dialog_body .cke_label{display:none}.cke_dialog_body label{display:inline;cursor:default;letter-spacing:.3px}.cke_dialog_body label+.cke_dialog_ui_labeled_content{margin-top:2px}.cke_dialog_contents_body .cke_dialog_ui_text,.cke_dialog_contents_body .cke_dialog_ui_select,.cke_dialog_contents_body .cke_dialog_ui_hbox_last>a.cke_dialog_ui_button{margin-top:4px}a.cke_smile{overflow:hidden;display:block;text-align:center;padding:.3em 0}a.cke_smile img{vertical-align:middle}a.cke_specialchar{cursor:inherit;display:block;height:1.25em;padding:.2em .3em;text-align:center}a.cke_smile,a.cke_specialchar{border:2px solid transparent}a.cke_smile:hover,a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:hover,a.cke_specialchar:focus,a.cke_specialchar:active{background:#fff;outline:0}a.cke_smile:hover,a.cke_specialchar:hover{border-color:#888}a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:focus,a.cke_specialchar:active{border-color:#139ff7}.cke_dialog_contents a.colorChooser{display:block;margin-top:6px;margin-left:10px;width:80px}.cke_rtl .cke_dialog_contents a.colorChooser{margin-right:10px}.cke_iframe_shim{display:block;position:absolute;top:0;left:0;z-index:-1;filter:alpha(opacity = 0);width:100%;height:100%}.cke_dialog_contents_body .cke_accessibility_legend{margin:2px 7px 2px 2px}.cke_dialog_contents_body .cke_accessibility_legend:focus,.cke_dialog_contents_body .cke_accessibility_legend:active{outline:0;border:2px solid #139ff7;margin:0 5px 0 0}.cke_dialog_contents_body input[type=file]:focus,.cke_dialog_contents_body input[type=file]:active{border:2px solid #139ff7}.cke_dialog_find_fieldset{margin-top:10px!important}.cke_dialog_image_ratiolock{margin-top:52px!important}.cke_dialog_forms_select_order label.cke_dialog_ui_labeled_label{margin-left:0}.cke_dialog_forms_select_order div.cke_dialog_ui_input_select{width:100%}.cke_dialog_forms_select_order_txtsize .cke_dialog_ui_hbox_last{padding-top:4px}.cke_dialog_image_url .cke_dialog_ui_hbox_last,.cke_dialog_flash_url .cke_dialog_ui_hbox_last{vertical-align:bottom}a.cke_dialog_ui_button.cke_dialog_image_browse{margin-top:10px}.cke_dialog_contents_body .cke_tpl_list{border:#bcbcbc 1px solid;margin:1px}.cke_dialog_contents_body .cke_tpl_list:focus,.cke_dialog_contents_body .cke_tpl_list:active{outline:0;margin:0;border:2px solid #139ff7}.cke_dialog_contents_body .cke_tpl_list a:focus,.cke_dialog_contents_body .cke_tpl_list a:active{outline:0}.cke_dialog_contents_body .cke_tpl_list a:focus .cke_tpl_item,.cke_dialog_contents_body .cke_tpl_list a:active .cke_tpl_item{border:2px solid #139ff7;padding:6px}.cke_rtl input.cke_dialog_ui_input_text,.cke_rtl input.cke_dialog_ui_input_password{padding-right:2px}.cke_rtl div.cke_dialog_ui_input_text,.cke_rtl div.cke_dialog_ui_input_password{padding-left:2px}.cke_rtl div.cke_dialog_ui_input_text{padding-right:1px}.cke_rtl .cke_dialog_ui_vbox_child,.cke_rtl .cke_dialog_ui_hbox_child,.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_last{padding-right:2px!important}.cke_hc .cke_dialog_title,.cke_hc .cke_dialog_footer,.cke_hc a.cke_dialog_tab,.cke_hc a.cke_dialog_ui_button,.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button_ok,.cke_hc a.cke_dialog_ui_button_ok:hover{filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:0}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono-lisa/dialog_ie8.css b/js/ckeditor/skins/moono-lisa/dialog_ie8.css
new file mode 100644 (file)
index 0000000..4356336
--- /dev/null
@@ -0,0 +1,5 @@
+/*\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+*/\r
+.cke_dialog{visibility:visible}.cke_dialog_body{z-index:1;background:#fff}.cke_dialog strong{font-weight:bold}.cke_dialog_title{font-weight:bold;font-size:12px;cursor:move;position:relative;color:#484848;border-bottom:1px solid #d1d1d1;padding:12px 19px 12px 12px;background:#f8f8f8;letter-spacing:.3px}.cke_dialog_spinner{border-radius:50%;width:12px;height:12px;overflow:hidden;text-indent:-9999em;border:2px solid rgba(102,102,102,0.2);border-left-color:rgba(102,102,102,1);-webkit-animation:dialog_spinner 1s infinite linear;animation:dialog_spinner 1s infinite linear}.cke_browser_ie8 .cke_dialog_spinner,.cke_browser_ie9 .cke_dialog_spinner{background:url(images/spinner.gif) center top no-repeat;width:16px;height:16px;border:0}@-webkit-keyframes dialog_spinner{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes dialog_spinner{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.cke_dialog_contents{background-color:#fff;overflow:auto;padding:15px 10px 5px 10px;margin-top:43px;border-top:1px solid #d1d1d1}.cke_dialog_contents_body{overflow:auto;padding:9px 10px 5px 10px;margin-top:22px}.cke_dialog_footer{text-align:right;position:relative;border-top:1px solid #d1d1d1;background:#f8f8f8}.cke_rtl .cke_dialog_footer{text-align:left}.cke_hc .cke_dialog_footer{outline:0;border-top:1px solid #fff}.cke_dialog .cke_resizer{margin-top:22px}.cke_dialog .cke_resizer_rtl{margin-left:5px}.cke_dialog .cke_resizer_ltr{margin-right:5px}.cke_dialog_tabs{height:33px;display:inline-block;margin:9px 0 0;position:absolute;z-index:2;left:11px}.cke_rtl .cke_dialog_tabs{left:auto;right:11px}a.cke_dialog_tab{height:25px;padding:4px 8px;display:inline-block;cursor:pointer;line-height:26px;outline:0;color:#484848;border:1px solid #d1d1d1;border-radius:3px 3px 0 0;background:#f8f8f8;min-width:90px;text-align:center;margin-left:-1px;letter-spacing:.3px}a.cke_dialog_tab:hover{background-color:#fff}a.cke_dialog_tab:focus{border:2px solid #139ff7;border-bottom-color:#d1d1d1;padding:3px 7px;position:relative;z-index:1}a.cke_dialog_tab_selected{background:#fff;border-bottom-color:#fff;cursor:default;filter:none}a.cke_dialog_tab_selected:hover,a.cke_dialog_tab_selected:focus{border-bottom-color:#fff}.cke_hc a.cke_dialog_tab:hover,.cke_hc a.cke_dialog_tab:focus,.cke_hc a.cke_dialog_tab_selected{border:3px solid;padding:2px 6px}a.cke_dialog_tab_disabled{color:#bababa;cursor:default}.cke_single_page .cke_dialog_tabs{display:none}.cke_single_page .cke_dialog_contents{padding-top:5px;margin-top:0;border-top:0}a.cke_dialog_close_button{background-image:url(images/close.png);background-repeat:no-repeat;background-position:50%;position:absolute;cursor:pointer;text-align:center;height:16px;width:16px;top:11px;z-index:5;opacity:.7;filter:alpha(opacity = 70)}.cke_rtl .cke_dialog_close_button{left:12px}.cke_ltr .cke_dialog_close_button{right:12px}.cke_hc a.cke_dialog_close_button{background-image:none}.cke_hidpi a.cke_dialog_close_button{background-image:url(images/hidpi/close.png);background-size:16px}a.cke_dialog_close_button:hover{opacity:1;filter:alpha(opacity = 100)}a.cke_dialog_close_button span{display:none}.cke_hc a.cke_dialog_close_button span{display:inline;cursor:pointer;font-weight:bold;position:relative;top:3px}div.cke_disabled .cke_dialog_ui_labeled_content div *{background-color:#ddd;cursor:default}.cke_dialog_ui_vbox table,.cke_dialog_ui_hbox table{margin:auto}.cke_dialog_ui_vbox_child{padding:5px 0}.cke_dialog_ui_hbox{width:100%;margin-top:12px}.cke_dialog_ui_hbox_first,.cke_dialog_ui_hbox_child,.cke_dialog_ui_hbox_last{vertical-align:top}.cke_ltr .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_ui_hbox_child{padding-right:10px}.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_child{padding-left:10px}.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-right:5px}.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-left:5px;padding-right:0}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:1px solid}textarea.cke_dialog_ui_input_textarea{overflow:auto;resize:none}input.cke_dialog_ui_input_text,input.cke_dialog_ui_input_password,textarea.cke_dialog_ui_input_textarea{background-color:#fff;border:1px solid #bcbcbc;padding:4px 6px;outline:0;width:100%;*width:95%;box-sizing:border-box;border-radius:2px;min-height:28px;margin-left:1px}input.cke_dialog_ui_input_text:hover,input.cke_dialog_ui_input_password:hover,textarea.cke_dialog_ui_input_textarea:hover{border:1px solid #aeb3b9}input.cke_dialog_ui_input_text:focus,input.cke_dialog_ui_input_password:focus,textarea.cke_dialog_ui_input_textarea:focus,select.cke_dialog_ui_input_select:focus{outline:0;border:2px solid #139ff7}input.cke_dialog_ui_input_text:focus{padding-left:5px}textarea.cke_dialog_ui_input_textarea:focus{padding:3px 5px}select.cke_dialog_ui_input_select:focus{margin:0;width:100%!important}input.cke_dialog_ui_checkbox_input,input.cke_dialog_ui_radio_input{margin-left:1px;margin-right:2px}input.cke_dialog_ui_checkbox_input:focus,input.cke_dialog_ui_checkbox_input:active,input.cke_dialog_ui_radio_input:focus,input.cke_dialog_ui_radio_input:active{border:0;outline:2px solid #139ff7}a.cke_dialog_ui_button{display:inline-block;*display:inline;*zoom:1;padding:4px 1px;margin:0;text-align:center;color:#484848;vertical-align:middle;cursor:pointer;border:1px solid #bcbcbc;border-radius:2px;background:#f8f8f8;letter-spacing:.3px;line-height:18px;box-sizing:border-box}.cke_hc a.cke_dialog_ui_button{border-width:3px}span.cke_dialog_ui_button{padding:0 10px;cursor:pointer}a.cke_dialog_ui_button:hover{background:#fff}a.cke_dialog_ui_button:focus,a.cke_dialog_ui_button:active{border:2px solid #139ff7;outline:0;padding:3px 0}.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button:focus,.cke_hc a.cke_dialog_ui_button:active{border:3px solid}.cke_dialog_footer_buttons a.cke_dialog_ui_button span{color:inherit;font-size:12px;font-weight:bold;padding:0 12px}a.cke_dialog_ui_button_ok{color:#fff;background:#09863e;border:1px solid #09863e}.cke_hc a.cke_dialog_ui_button{border:3px solid #bcbcbc}a.cke_dialog_ui_button_ok:hover{background:#53aa78;border-color:#53aa78}a.cke_dialog_ui_button_ok:focus{box-shadow:inset 0 0 0 2px #FFF}a.cke_dialog_ui_button_ok:focus,a.cke_dialog_ui_button_ok:active{border-color:#139ff7}.cke_hc a.cke_dialog_ui_button_ok:hover,.cke_hc a.cke_dialog_ui_button_ok:focus,.cke_hc a.cke_dialog_ui_button_ok:active{border-color:#484848}a.cke_dialog_ui_button_ok.cke_disabled{background:#d1d1d1;border-color:#d1d1d1;cursor:default}a.cke_dialog_ui_button_ok.cke_disabled span{cursor:default}.cke_dialog_footer_buttons{display:inline-table;margin:5px;width:auto;position:relative;vertical-align:middle}div.cke_dialog_ui_input_select{display:table}select.cke_dialog_ui_input_select{height:28px;line-height:28px;background-color:#fff;border:1px solid #bcbcbc;padding:3px 3px 3px 6px;outline:0;border-radius:2px;margin:0 1px;box-sizing:border-box;width:calc(100% - 2px)!important}.cke_dialog_ui_input_file{width:100%;height:25px}.cke_hc .cke_dialog_ui_labeled_content input:focus,.cke_hc .cke_dialog_ui_labeled_content select:focus,.cke_hc .cke_dialog_ui_labeled_content textarea:focus{outline:1px dotted}.cke_dialog_ui_labeled_label{margin-left:1px}.cke_dialog .cke_dark_background{background-color:transparent}.cke_dialog .cke_light_background{background-color:#ebebeb}.cke_dialog .cke_centered{text-align:center}.cke_dialog a.cke_btn_reset{float:right;background:url(images/refresh.png) top left no-repeat;width:16px;height:16px;border:1px none;font-size:1px}.cke_hidpi .cke_dialog a.cke_btn_reset{background-size:16px;background-image:url(images/hidpi/refresh.png)}.cke_rtl .cke_dialog a.cke_btn_reset{float:left}.cke_dialog a.cke_btn_locked,.cke_dialog a.cke_btn_unlocked{float:left;width:16px;height:16px;background-repeat:no-repeat;border:none 1px;font-size:1px}.cke_dialog a.cke_btn_locked,.cke_dialog a.cke_btn_unlocked,.cke_dialog a.cke_btn_reset{margin:2px}.cke_dialog a.cke_btn_locked{background-image:url(images/lock.png)}.cke_dialog a.cke_btn_unlocked{background-image:url(images/lock-open.png)}.cke_rtl .cke_dialog a.cke_btn_locked,.cke_rtl .cke_dialog a.cke_btn_unlocked{float:right}.cke_hidpi .cke_dialog a.cke_btn_unlocked,.cke_hidpi .cke_dialog a.cke_btn_locked{background-size:16px}.cke_hidpi .cke_dialog a.cke_btn_locked{background-image:url(images/hidpi/lock.png)}.cke_hidpi .cke_dialog a.cke_btn_unlocked{background-image:url(images/hidpi/lock-open.png)}.cke_dialog a.cke_btn_locked .cke_icon{display:none}.cke_dialog a.cke_btn_over,.cke_dialog a.cke_btn_locked:hover,.cke_dialog a.cke_btn_locked:focus,.cke_dialog a.cke_btn_locked:active,.cke_dialog a.cke_btn_unlocked:hover,.cke_dialog a.cke_btn_unlocked:focus,.cke_dialog a.cke_btn_unlocked:active,.cke_dialog a.cke_btn_reset:hover,.cke_dialog a.cke_btn_reset:focus,.cke_dialog a.cke_btn_reset:active{cursor:pointer;outline:0;margin:0;border:2px solid #139ff7}.cke_dialog fieldset{border:1px solid #bcbcbc}.cke_dialog fieldset legend{padding:0 6px}.cke_dialog_ui_checkbox,.cke_dialog fieldset .cke_dialog_ui_vbox .cke_dialog_ui_checkbox{display:inline-block}.cke_dialog fieldset .cke_dialog_ui_vbox .cke_dialog_ui_checkbox{padding-top:5px}.cke_dialog_ui_checkbox .cke_dialog_ui_checkbox_input,.cke_dialog_ui_checkbox .cke_dialog_ui_checkbox_input+label,.cke_dialog fieldset .cke_dialog_ui_vbox .cke_dialog_ui_checkbox .cke_dialog_ui_checkbox_input,.cke_dialog fieldset .cke_dialog_ui_vbox .cke_dialog_ui_checkbox .cke_dialog_ui_checkbox_input+label{vertical-align:middle}.cke_dialog .ImagePreviewBox{border:1px ridge #bcbcbc;overflow:scroll;height:200px;width:300px;padding:2px;background-color:white}.cke_dialog .ImagePreviewBox table td{white-space:normal}.cke_dialog .ImagePreviewLoader{position:absolute;white-space:normal;overflow:hidden;height:160px;width:230px;margin:2px;padding:2px;opacity:.9;filter:alpha(opacity = 90);background-color:#e4e4e4}.cke_dialog .FlashPreviewBox{white-space:normal;border:1px solid #bcbcbc;overflow:auto;height:160px;width:390px;padding:2px;background-color:white}.cke_dialog .cke_pastetext{width:346px;height:170px}.cke_dialog .cke_pastetext textarea{width:340px;height:170px;resize:none}.cke_dialog iframe.cke_pasteframe{width:346px;height:130px;background-color:white;border:1px solid #aeb3b9;border-radius:3px}.cke_dialog .cke_hand{cursor:pointer}.cke_disabled{color:#a0a0a0}.cke_dialog_body .cke_label{display:none}.cke_dialog_body label{display:inline;cursor:default;letter-spacing:.3px}.cke_dialog_body label+.cke_dialog_ui_labeled_content{margin-top:2px}.cke_dialog_contents_body .cke_dialog_ui_text,.cke_dialog_contents_body .cke_dialog_ui_select,.cke_dialog_contents_body .cke_dialog_ui_hbox_last>a.cke_dialog_ui_button{margin-top:4px}a.cke_smile{overflow:hidden;display:block;text-align:center;padding:.3em 0}a.cke_smile img{vertical-align:middle}a.cke_specialchar{cursor:inherit;display:block;height:1.25em;padding:.2em .3em;text-align:center}a.cke_smile,a.cke_specialchar{border:2px solid transparent}a.cke_smile:hover,a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:hover,a.cke_specialchar:focus,a.cke_specialchar:active{background:#fff;outline:0}a.cke_smile:hover,a.cke_specialchar:hover{border-color:#888}a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:focus,a.cke_specialchar:active{border-color:#139ff7}.cke_dialog_contents a.colorChooser{display:block;margin-top:6px;margin-left:10px;width:80px}.cke_rtl .cke_dialog_contents a.colorChooser{margin-right:10px}.cke_iframe_shim{display:block;position:absolute;top:0;left:0;z-index:-1;filter:alpha(opacity = 0);width:100%;height:100%}.cke_dialog_contents_body .cke_accessibility_legend{margin:2px 7px 2px 2px}.cke_dialog_contents_body .cke_accessibility_legend:focus,.cke_dialog_contents_body .cke_accessibility_legend:active{outline:0;border:2px solid #139ff7;margin:0 5px 0 0}.cke_dialog_contents_body input[type=file]:focus,.cke_dialog_contents_body input[type=file]:active{border:2px solid #139ff7}.cke_dialog_find_fieldset{margin-top:10px!important}.cke_dialog_image_ratiolock{margin-top:52px!important}.cke_dialog_forms_select_order label.cke_dialog_ui_labeled_label{margin-left:0}.cke_dialog_forms_select_order div.cke_dialog_ui_input_select{width:100%}.cke_dialog_forms_select_order_txtsize .cke_dialog_ui_hbox_last{padding-top:4px}.cke_dialog_image_url .cke_dialog_ui_hbox_last,.cke_dialog_flash_url .cke_dialog_ui_hbox_last{vertical-align:bottom}a.cke_dialog_ui_button.cke_dialog_image_browse{margin-top:10px}.cke_dialog_contents_body .cke_tpl_list{border:#bcbcbc 1px solid;margin:1px}.cke_dialog_contents_body .cke_tpl_list:focus,.cke_dialog_contents_body .cke_tpl_list:active{outline:0;margin:0;border:2px solid #139ff7}.cke_dialog_contents_body .cke_tpl_list a:focus,.cke_dialog_contents_body .cke_tpl_list a:active{outline:0}.cke_dialog_contents_body .cke_tpl_list a:focus .cke_tpl_item,.cke_dialog_contents_body .cke_tpl_list a:active .cke_tpl_item{border:2px solid #139ff7;padding:6px}.cke_rtl input.cke_dialog_ui_input_text,.cke_rtl input.cke_dialog_ui_input_password{padding-right:2px}.cke_rtl div.cke_dialog_ui_input_text,.cke_rtl div.cke_dialog_ui_input_password{padding-left:2px}.cke_rtl div.cke_dialog_ui_input_text{padding-right:1px}.cke_rtl .cke_dialog_ui_vbox_child,.cke_rtl .cke_dialog_ui_hbox_child,.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_last{padding-right:2px!important}.cke_hc .cke_dialog_title,.cke_hc .cke_dialog_footer,.cke_hc a.cke_dialog_tab,.cke_hc a.cke_dialog_ui_button,.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button_ok,.cke_hc a.cke_dialog_ui_button_ok:hover{filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:0}a.cke_dialog_ui_button{min-height:18px}input.cke_dialog_ui_input_text,input.cke_dialog_ui_input_password,textarea.cke_dialog_ui_input_textarea{min-height:18px}input.cke_dialog_ui_input_text:focus,input.cke_dialog_ui_input_password:focus,textarea.cke_dialog_ui_input_textarea:focus{padding-top:4px;padding-bottom:2px}select.cke_dialog_ui_input_select{width:100%!important}select.cke_dialog_ui_input_select:focus{margin-left:1px;width:100%!important;padding-top:2px;padding-bottom:2px}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono-lisa/dialog_iequirks.css b/js/ckeditor/skins/moono-lisa/dialog_iequirks.css
new file mode 100644 (file)
index 0000000..fcbeb51
--- /dev/null
@@ -0,0 +1,5 @@
+/*\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+*/\r
+.cke_dialog{visibility:visible}.cke_dialog_body{z-index:1;background:#fff}.cke_dialog strong{font-weight:bold}.cke_dialog_title{font-weight:bold;font-size:12px;cursor:move;position:relative;color:#484848;border-bottom:1px solid #d1d1d1;padding:12px 19px 12px 12px;background:#f8f8f8;letter-spacing:.3px}.cke_dialog_spinner{border-radius:50%;width:12px;height:12px;overflow:hidden;text-indent:-9999em;border:2px solid rgba(102,102,102,0.2);border-left-color:rgba(102,102,102,1);-webkit-animation:dialog_spinner 1s infinite linear;animation:dialog_spinner 1s infinite linear}.cke_browser_ie8 .cke_dialog_spinner,.cke_browser_ie9 .cke_dialog_spinner{background:url(images/spinner.gif) center top no-repeat;width:16px;height:16px;border:0}@-webkit-keyframes dialog_spinner{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes dialog_spinner{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.cke_dialog_contents{background-color:#fff;overflow:auto;padding:15px 10px 5px 10px;margin-top:43px;border-top:1px solid #d1d1d1}.cke_dialog_contents_body{overflow:auto;padding:9px 10px 5px 10px;margin-top:22px}.cke_dialog_footer{text-align:right;position:relative;border-top:1px solid #d1d1d1;background:#f8f8f8}.cke_rtl .cke_dialog_footer{text-align:left}.cke_hc .cke_dialog_footer{outline:0;border-top:1px solid #fff}.cke_dialog .cke_resizer{margin-top:22px}.cke_dialog .cke_resizer_rtl{margin-left:5px}.cke_dialog .cke_resizer_ltr{margin-right:5px}.cke_dialog_tabs{height:33px;display:inline-block;margin:9px 0 0;position:absolute;z-index:2;left:11px}.cke_rtl .cke_dialog_tabs{left:auto;right:11px}a.cke_dialog_tab{height:25px;padding:4px 8px;display:inline-block;cursor:pointer;line-height:26px;outline:0;color:#484848;border:1px solid #d1d1d1;border-radius:3px 3px 0 0;background:#f8f8f8;min-width:90px;text-align:center;margin-left:-1px;letter-spacing:.3px}a.cke_dialog_tab:hover{background-color:#fff}a.cke_dialog_tab:focus{border:2px solid #139ff7;border-bottom-color:#d1d1d1;padding:3px 7px;position:relative;z-index:1}a.cke_dialog_tab_selected{background:#fff;border-bottom-color:#fff;cursor:default;filter:none}a.cke_dialog_tab_selected:hover,a.cke_dialog_tab_selected:focus{border-bottom-color:#fff}.cke_hc a.cke_dialog_tab:hover,.cke_hc a.cke_dialog_tab:focus,.cke_hc a.cke_dialog_tab_selected{border:3px solid;padding:2px 6px}a.cke_dialog_tab_disabled{color:#bababa;cursor:default}.cke_single_page .cke_dialog_tabs{display:none}.cke_single_page .cke_dialog_contents{padding-top:5px;margin-top:0;border-top:0}a.cke_dialog_close_button{background-image:url(images/close.png);background-repeat:no-repeat;background-position:50%;position:absolute;cursor:pointer;text-align:center;height:16px;width:16px;top:11px;z-index:5;opacity:.7;filter:alpha(opacity = 70)}.cke_rtl .cke_dialog_close_button{left:12px}.cke_ltr .cke_dialog_close_button{right:12px}.cke_hc a.cke_dialog_close_button{background-image:none}.cke_hidpi a.cke_dialog_close_button{background-image:url(images/hidpi/close.png);background-size:16px}a.cke_dialog_close_button:hover{opacity:1;filter:alpha(opacity = 100)}a.cke_dialog_close_button span{display:none}.cke_hc a.cke_dialog_close_button span{display:inline;cursor:pointer;font-weight:bold;position:relative;top:3px}div.cke_disabled .cke_dialog_ui_labeled_content div *{background-color:#ddd;cursor:default}.cke_dialog_ui_vbox table,.cke_dialog_ui_hbox table{margin:auto}.cke_dialog_ui_vbox_child{padding:5px 0}.cke_dialog_ui_hbox{width:100%;margin-top:12px}.cke_dialog_ui_hbox_first,.cke_dialog_ui_hbox_child,.cke_dialog_ui_hbox_last{vertical-align:top}.cke_ltr .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_ui_hbox_child{padding-right:10px}.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_child{padding-left:10px}.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-right:5px}.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-left:5px;padding-right:0}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:1px solid}textarea.cke_dialog_ui_input_textarea{overflow:auto;resize:none}input.cke_dialog_ui_input_text,input.cke_dialog_ui_input_password,textarea.cke_dialog_ui_input_textarea{background-color:#fff;border:1px solid #bcbcbc;padding:4px 6px;outline:0;width:100%;*width:95%;box-sizing:border-box;border-radius:2px;min-height:28px;margin-left:1px}input.cke_dialog_ui_input_text:hover,input.cke_dialog_ui_input_password:hover,textarea.cke_dialog_ui_input_textarea:hover{border:1px solid #aeb3b9}input.cke_dialog_ui_input_text:focus,input.cke_dialog_ui_input_password:focus,textarea.cke_dialog_ui_input_textarea:focus,select.cke_dialog_ui_input_select:focus{outline:0;border:2px solid #139ff7}input.cke_dialog_ui_input_text:focus{padding-left:5px}textarea.cke_dialog_ui_input_textarea:focus{padding:3px 5px}select.cke_dialog_ui_input_select:focus{margin:0;width:100%!important}input.cke_dialog_ui_checkbox_input,input.cke_dialog_ui_radio_input{margin-left:1px;margin-right:2px}input.cke_dialog_ui_checkbox_input:focus,input.cke_dialog_ui_checkbox_input:active,input.cke_dialog_ui_radio_input:focus,input.cke_dialog_ui_radio_input:active{border:0;outline:2px solid #139ff7}a.cke_dialog_ui_button{display:inline-block;*display:inline;*zoom:1;padding:4px 1px;margin:0;text-align:center;color:#484848;vertical-align:middle;cursor:pointer;border:1px solid #bcbcbc;border-radius:2px;background:#f8f8f8;letter-spacing:.3px;line-height:18px;box-sizing:border-box}.cke_hc a.cke_dialog_ui_button{border-width:3px}span.cke_dialog_ui_button{padding:0 10px;cursor:pointer}a.cke_dialog_ui_button:hover{background:#fff}a.cke_dialog_ui_button:focus,a.cke_dialog_ui_button:active{border:2px solid #139ff7;outline:0;padding:3px 0}.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button:focus,.cke_hc a.cke_dialog_ui_button:active{border:3px solid}.cke_dialog_footer_buttons a.cke_dialog_ui_button span{color:inherit;font-size:12px;font-weight:bold;padding:0 12px}a.cke_dialog_ui_button_ok{color:#fff;background:#09863e;border:1px solid #09863e}.cke_hc a.cke_dialog_ui_button{border:3px solid #bcbcbc}a.cke_dialog_ui_button_ok:hover{background:#53aa78;border-color:#53aa78}a.cke_dialog_ui_button_ok:focus{box-shadow:inset 0 0 0 2px #FFF}a.cke_dialog_ui_button_ok:focus,a.cke_dialog_ui_button_ok:active{border-color:#139ff7}.cke_hc a.cke_dialog_ui_button_ok:hover,.cke_hc a.cke_dialog_ui_button_ok:focus,.cke_hc a.cke_dialog_ui_button_ok:active{border-color:#484848}a.cke_dialog_ui_button_ok.cke_disabled{background:#d1d1d1;border-color:#d1d1d1;cursor:default}a.cke_dialog_ui_button_ok.cke_disabled span{cursor:default}.cke_dialog_footer_buttons{display:inline-table;margin:5px;width:auto;position:relative;vertical-align:middle}div.cke_dialog_ui_input_select{display:table}select.cke_dialog_ui_input_select{height:28px;line-height:28px;background-color:#fff;border:1px solid #bcbcbc;padding:3px 3px 3px 6px;outline:0;border-radius:2px;margin:0 1px;box-sizing:border-box;width:calc(100% - 2px)!important}.cke_dialog_ui_input_file{width:100%;height:25px}.cke_hc .cke_dialog_ui_labeled_content input:focus,.cke_hc .cke_dialog_ui_labeled_content select:focus,.cke_hc .cke_dialog_ui_labeled_content textarea:focus{outline:1px dotted}.cke_dialog_ui_labeled_label{margin-left:1px}.cke_dialog .cke_dark_background{background-color:transparent}.cke_dialog .cke_light_background{background-color:#ebebeb}.cke_dialog .cke_centered{text-align:center}.cke_dialog a.cke_btn_reset{float:right;background:url(images/refresh.png) top left no-repeat;width:16px;height:16px;border:1px none;font-size:1px}.cke_hidpi .cke_dialog a.cke_btn_reset{background-size:16px;background-image:url(images/hidpi/refresh.png)}.cke_rtl .cke_dialog a.cke_btn_reset{float:left}.cke_dialog a.cke_btn_locked,.cke_dialog a.cke_btn_unlocked{float:left;width:16px;height:16px;background-repeat:no-repeat;border:none 1px;font-size:1px}.cke_dialog a.cke_btn_locked,.cke_dialog a.cke_btn_unlocked,.cke_dialog a.cke_btn_reset{margin:2px}.cke_dialog a.cke_btn_locked{background-image:url(images/lock.png)}.cke_dialog a.cke_btn_unlocked{background-image:url(images/lock-open.png)}.cke_rtl .cke_dialog a.cke_btn_locked,.cke_rtl .cke_dialog a.cke_btn_unlocked{float:right}.cke_hidpi .cke_dialog a.cke_btn_unlocked,.cke_hidpi .cke_dialog a.cke_btn_locked{background-size:16px}.cke_hidpi .cke_dialog a.cke_btn_locked{background-image:url(images/hidpi/lock.png)}.cke_hidpi .cke_dialog a.cke_btn_unlocked{background-image:url(images/hidpi/lock-open.png)}.cke_dialog a.cke_btn_locked .cke_icon{display:none}.cke_dialog a.cke_btn_over,.cke_dialog a.cke_btn_locked:hover,.cke_dialog a.cke_btn_locked:focus,.cke_dialog a.cke_btn_locked:active,.cke_dialog a.cke_btn_unlocked:hover,.cke_dialog a.cke_btn_unlocked:focus,.cke_dialog a.cke_btn_unlocked:active,.cke_dialog a.cke_btn_reset:hover,.cke_dialog a.cke_btn_reset:focus,.cke_dialog a.cke_btn_reset:active{cursor:pointer;outline:0;margin:0;border:2px solid #139ff7}.cke_dialog fieldset{border:1px solid #bcbcbc}.cke_dialog fieldset legend{padding:0 6px}.cke_dialog_ui_checkbox,.cke_dialog fieldset .cke_dialog_ui_vbox .cke_dialog_ui_checkbox{display:inline-block}.cke_dialog fieldset .cke_dialog_ui_vbox .cke_dialog_ui_checkbox{padding-top:5px}.cke_dialog_ui_checkbox .cke_dialog_ui_checkbox_input,.cke_dialog_ui_checkbox .cke_dialog_ui_checkbox_input+label,.cke_dialog fieldset .cke_dialog_ui_vbox .cke_dialog_ui_checkbox .cke_dialog_ui_checkbox_input,.cke_dialog fieldset .cke_dialog_ui_vbox .cke_dialog_ui_checkbox .cke_dialog_ui_checkbox_input+label{vertical-align:middle}.cke_dialog .ImagePreviewBox{border:1px ridge #bcbcbc;overflow:scroll;height:200px;width:300px;padding:2px;background-color:white}.cke_dialog .ImagePreviewBox table td{white-space:normal}.cke_dialog .ImagePreviewLoader{position:absolute;white-space:normal;overflow:hidden;height:160px;width:230px;margin:2px;padding:2px;opacity:.9;filter:alpha(opacity = 90);background-color:#e4e4e4}.cke_dialog .FlashPreviewBox{white-space:normal;border:1px solid #bcbcbc;overflow:auto;height:160px;width:390px;padding:2px;background-color:white}.cke_dialog .cke_pastetext{width:346px;height:170px}.cke_dialog .cke_pastetext textarea{width:340px;height:170px;resize:none}.cke_dialog iframe.cke_pasteframe{width:346px;height:130px;background-color:white;border:1px solid #aeb3b9;border-radius:3px}.cke_dialog .cke_hand{cursor:pointer}.cke_disabled{color:#a0a0a0}.cke_dialog_body .cke_label{display:none}.cke_dialog_body label{display:inline;cursor:default;letter-spacing:.3px}.cke_dialog_body label+.cke_dialog_ui_labeled_content{margin-top:2px}.cke_dialog_contents_body .cke_dialog_ui_text,.cke_dialog_contents_body .cke_dialog_ui_select,.cke_dialog_contents_body .cke_dialog_ui_hbox_last>a.cke_dialog_ui_button{margin-top:4px}a.cke_smile{overflow:hidden;display:block;text-align:center;padding:.3em 0}a.cke_smile img{vertical-align:middle}a.cke_specialchar{cursor:inherit;display:block;height:1.25em;padding:.2em .3em;text-align:center}a.cke_smile,a.cke_specialchar{border:2px solid transparent}a.cke_smile:hover,a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:hover,a.cke_specialchar:focus,a.cke_specialchar:active{background:#fff;outline:0}a.cke_smile:hover,a.cke_specialchar:hover{border-color:#888}a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:focus,a.cke_specialchar:active{border-color:#139ff7}.cke_dialog_contents a.colorChooser{display:block;margin-top:6px;margin-left:10px;width:80px}.cke_rtl .cke_dialog_contents a.colorChooser{margin-right:10px}.cke_iframe_shim{display:block;position:absolute;top:0;left:0;z-index:-1;filter:alpha(opacity = 0);width:100%;height:100%}.cke_dialog_contents_body .cke_accessibility_legend{margin:2px 7px 2px 2px}.cke_dialog_contents_body .cke_accessibility_legend:focus,.cke_dialog_contents_body .cke_accessibility_legend:active{outline:0;border:2px solid #139ff7;margin:0 5px 0 0}.cke_dialog_contents_body input[type=file]:focus,.cke_dialog_contents_body input[type=file]:active{border:2px solid #139ff7}.cke_dialog_find_fieldset{margin-top:10px!important}.cke_dialog_image_ratiolock{margin-top:52px!important}.cke_dialog_forms_select_order label.cke_dialog_ui_labeled_label{margin-left:0}.cke_dialog_forms_select_order div.cke_dialog_ui_input_select{width:100%}.cke_dialog_forms_select_order_txtsize .cke_dialog_ui_hbox_last{padding-top:4px}.cke_dialog_image_url .cke_dialog_ui_hbox_last,.cke_dialog_flash_url .cke_dialog_ui_hbox_last{vertical-align:bottom}a.cke_dialog_ui_button.cke_dialog_image_browse{margin-top:10px}.cke_dialog_contents_body .cke_tpl_list{border:#bcbcbc 1px solid;margin:1px}.cke_dialog_contents_body .cke_tpl_list:focus,.cke_dialog_contents_body .cke_tpl_list:active{outline:0;margin:0;border:2px solid #139ff7}.cke_dialog_contents_body .cke_tpl_list a:focus,.cke_dialog_contents_body .cke_tpl_list a:active{outline:0}.cke_dialog_contents_body .cke_tpl_list a:focus .cke_tpl_item,.cke_dialog_contents_body .cke_tpl_list a:active .cke_tpl_item{border:2px solid #139ff7;padding:6px}.cke_rtl input.cke_dialog_ui_input_text,.cke_rtl input.cke_dialog_ui_input_password{padding-right:2px}.cke_rtl div.cke_dialog_ui_input_text,.cke_rtl div.cke_dialog_ui_input_password{padding-left:2px}.cke_rtl div.cke_dialog_ui_input_text{padding-right:1px}.cke_rtl .cke_dialog_ui_vbox_child,.cke_rtl .cke_dialog_ui_hbox_child,.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_last{padding-right:2px!important}.cke_hc .cke_dialog_title,.cke_hc .cke_dialog_footer,.cke_hc a.cke_dialog_tab,.cke_hc a.cke_dialog_ui_button,.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button_ok,.cke_hc a.cke_dialog_ui_button_ok:hover{filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:0}.cke_dialog_footer{filter:""}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono-lisa/editor.css b/js/ckeditor/skins/moono-lisa/editor.css
new file mode 100644 (file)
index 0000000..7ecdefa
--- /dev/null
@@ -0,0 +1,5 @@
+/*\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+*/\r
+.cke_reset{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;position:static;transition:none}.cke_reset_all,.cke_reset_all *,.cke_reset_all a,.cke_reset_all textarea{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;position:static;transition:none;border-collapse:collapse;font:normal normal normal 12px Arial,Helvetica,Tahoma,Verdana,Sans-Serif;color:#000;text-align:left;white-space:nowrap;cursor:auto;float:none}.cke_reset_all .cke_rtl *{text-align:right}.cke_reset_all iframe{vertical-align:inherit}.cke_reset_all textarea{white-space:pre-wrap}.cke_reset_all textarea,.cke_reset_all input[type="text"],.cke_reset_all input[type="password"]{cursor:text}.cke_reset_all textarea[disabled],.cke_reset_all input[type="text"][disabled],.cke_reset_all input[type="password"][disabled]{cursor:default}.cke_reset_all fieldset{padding:10px;border:2px groove #e0dfe3}.cke_reset_all select{box-sizing:border-box}.cke_reset_all table{table-layout:auto}.cke_chrome{display:block;border:1px solid #d1d1d1;padding:0}.cke_inner{display:block;background:#fff;padding:0;-webkit-touch-callout:none}.cke_float{border:0}.cke_float .cke_inner{padding-bottom:0}.cke_top,.cke_contents,.cke_bottom{display:block;overflow:hidden}.cke_top{border-bottom:1px solid #d1d1d1;background:#f8f8f8;padding:6px 8px 2px;white-space:normal}.cke_float .cke_top{border:1px solid #d1d1d1}.cke_bottom{padding:6px 8px 2px;position:relative;border-top:1px solid #d1d1d1;background:#f8f8f8}.cke_browser_ios .cke_contents{overflow-y:auto;-webkit-overflow-scrolling:touch}.cke_resizer{width:0;height:0;overflow:hidden;border-width:10px 10px 0 0;border-color:transparent #bcbcbc transparent transparent;border-style:dashed solid dashed dashed;font-size:0;vertical-align:bottom;margin-top:6px;margin-bottom:2px}.cke_hc .cke_resizer{font-size:15px;width:auto;height:auto;border-width:0}.cke_resizer_ltr{cursor:se-resize;float:right;margin-right:-4px}.cke_resizer_rtl{border-width:10px 0 0 10px;border-color:transparent transparent transparent #bcbcbc;border-style:dashed dashed dashed solid;cursor:sw-resize;float:left;margin-left:-4px;right:auto}.cke_wysiwyg_div{display:block;height:100%;overflow:auto;padding:0 8px;outline-style:none;box-sizing:border-box}.cke_panel{visibility:visible;width:120px;height:100px;overflow:hidden;background-color:#fff;border:1px solid #d1d1d1}.cke_menu_panel{padding:0;margin:0}.cke_combopanel{width:150px;height:170px}.cke_panel_frame{width:100%;height:100%;font-size:12px;overflow:auto;overflow-x:hidden}.cke_panel_container{overflow-y:auto;overflow-x:hidden}.cke_panel_block:focus{outline:0}.cke_panel_list{margin:0;padding:0;list-style-type:none;white-space:nowrap}.cke_panel_listItem{margin:0;padding:0}.cke_panel_listItem a{padding:6px 7px;display:block;color:inherit!important;text-decoration:none;overflow:hidden;text-overflow:ellipsis}.cke_hc .cke_panel_listItem a{border-style:none}.cke_panel_listItem.cke_selected a,.cke_panel_listItem a:hover,.cke_panel_listItem a:focus,.cke_panel_listItem a:active{background-color:#e9e9e9}.cke_panel_listItem a:focus{outline:1px dotted #000}.cke_hc .cke_panel_listItem a:hover,.cke_hc .cke_panel_listItem a:focus,.cke_hc .cke_panel_listItem a:active{border:2px solid;padding:4px 5px}.cke_panel_listItem p,.cke_panel_listItem h1,.cke_panel_listItem h2,.cke_panel_listItem h3,.cke_panel_listItem h4,.cke_panel_listItem h5,.cke_panel_listItem h6,.cke_panel_listItem pre{margin-top:0;margin-bottom:0}.cke_panel_grouptitle{cursor:default;font-size:11px;font-weight:bold;white-space:nowrap;margin:0;padding:6px 6px 7px 6px;color:#484848;border-bottom:1px solid #d1d1d1;background:#f8f8f8}.cke_colorblock{padding:10px;font-size:11px;font-family:'Microsoft Sans Serif',Tahoma,Arial,Verdana,Sans-Serif}.cke_colorblock,.cke_colorblock a{text-decoration:none;color:#000}a.cke_colorbox{padding:2px;float:left;width:20px;height:20px}.cke_rtl a.cke_colorbox{float:right}a:hover.cke_colorbox,a:focus.cke_colorbox,a:active.cke_colorbox{outline:0;padding:0;border:2px solid #139ff7}a:hover.cke_colorbox{border-color:#bcbcbc}span.cke_colorbox{width:20px;height:20px;float:left}.cke_rtl span.cke_colorbox{float:right}a.cke_colorauto,a.cke_colormore{border:#fff 1px solid;padding:3px;display:block;cursor:pointer}a.cke_colorauto{padding:0;border:1px solid transparent;margin-bottom:6px;height:26px;line-height:26px}a.cke_colormore{margin-top:10px;height:20px;line-height:19px}a:hover.cke_colorauto,a:hover.cke_colormore,a:focus.cke_colorauto,a:focus.cke_colormore,a:active.cke_colorauto,a:active.cke_colormore{outline:0;border:#139ff7 1px solid;background-color:#f8f8f8}a:hover.cke_colorauto,a:hover.cke_colormore{border-color:#bcbcbc}.cke_colorauto span.cke_colorbox{width:18px;height:18px;border:1px solid #808080;margin-left:1px;margin-top:3px}.cke_rtl .cke_colorauto span.cke_colorbox{margin-left:0;margin-right:1px}span.cke_colorbox[style*="#ffffff"],span.cke_colorbox[style*="#FFFFFF"],span.cke_colorbox[style="background-color:#fff"],span.cke_colorbox[style="background-color:#FFF"],span.cke_colorbox[style*="rgb(255,255,255)"],span.cke_colorbox[style*="rgb(255, 255, 255)"]{border:1px solid #808080;width:18px;height:18px}.cke_toolbar{float:left}.cke_rtl .cke_toolbar{float:right}.cke_toolgroup{border:0;float:left;margin:1px 2px 6px 0;padding-right:3px}.cke_rtl .cke_toolgroup{float:right;margin:1px 0 6px 2px;padding-left:3px;padding-right:0}.cke_hc .cke_toolgroup{margin-right:5px;margin-bottom:5px}.cke_hc.cke_rtl .cke_toolgroup{margin-right:0;margin-left:5px}a.cke_button{display:inline-block;height:18px;padding:4px 6px;outline:0;cursor:default;float:left;border:0;position:relative}.cke_rtl a.cke_button{float:right}.cke_hc a.cke_button{border:1px solid black;padding:3px 5px;margin:0 3px 5px 0}.cke_hc.cke_rtl a.cke_button{margin:0 0 5px 3px}a.cke_button_on{background:#fff;border:1px #bcbcbc solid;padding:3px 5px}a.cke_button_off:hover,a.cke_button_off:focus,a.cke_button_off:active{background:#e5e5e5;border:1px #bcbcbc solid;padding:3px 5px}.cke_hc a.cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active{background:#e5e5e5;border:3px solid #000;padding:1px 3px}a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{border:0;padding:4px 6px;background-color:transparent}a.cke_button_disabled:focus{border:1px #bcbcbc solid;padding:3px 5px}.cke_hc a.cke_button_disabled:hover,.cke_hc a.cke_button_disabled:focus,.cke_hc a.cke_button_disabled:active{border:1px solid #acacac;padding:3px 5px;margin:0 3px 5px 0}.cke_hc a.cke_button_disabled:focus{border:3px solid #000;padding:1px 3px}.cke_hc.cke_rtl a.cke_button_disabled:hover,.cke_hc.cke_rtl a.cke_button_disabled:focus,.cke_hc.cke_rtl a.cke_button_disabled:active{margin:0 0 5px 3px}a.cke_button_disabled .cke_button_icon,a.cke_button_disabled .cke_button_arrow{opacity:.3}.cke_hc a.cke_button_disabled{border-color:#acacac}.cke_hc a.cke_button_disabled .cke_button_icon,.cke_hc a.cke_button_disabled .cke_button_label{opacity:.5}.cke_toolgroup a.cke_button:last-child:after,.cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{content:"";position:absolute;height:18px;width:0;border-right:1px solid #bcbcbc;margin-top:4px;top:0;right:-3px}.cke_rtl .cke_toolgroup a.cke_button:last-child:after,.cke_rtl .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{border-right:0;right:auto;border-left:1px solid #bcbcbc;top:0;left:-3px}.cke_hc .cke_toolgroup a.cke_button:last-child:after,.cke_hc .cke_toolgroup a.cke_button.cke_button_disabled:last-child:after,.cke_hc .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{border-color:#000;top:0;right:-7px}.cke_hc.cke_rtl .cke_toolgroup a.cke_button:last-child:after,.cke_hc.cke_rtl .cke_toolgroup a.cke_button.cke_button_disabled:last-child:after,.cke_hc.cke_rtl .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{top:0;right:auto;left:-7px}.cke_toolgroup a.cke_button:hover:last-child:after,.cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-1px;right:-4px}.cke_rtl .cke_toolgroup a.cke_button:hover:last-child:after,.cke_rtl .cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-1px;right:auto;left:-4px}.cke_hc .cke_toolgroup a.cke_button:hover:last-child:after,.cke_hc .cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-2px;right:-9px}.cke_hc.cke_rtl .cke_toolgroup a.cke_button:hover:last-child:after,.cke_hc.cke_rtl .cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-2px;right:auto;left:-9px}.cke_toolbar.cke_toolbar_last .cke_toolgroup a.cke_button:last-child:after{content:none;border:0;width:0;height:0}.cke_button_icon{cursor:inherit;background-repeat:no-repeat;margin-top:1px;width:16px;height:16px;float:left;display:inline-block}.cke_rtl .cke_button_icon{float:right}.cke_hc .cke_button_icon{display:none}.cke_button_label{display:none;padding-left:3px;margin-top:1px;line-height:17px;vertical-align:middle;float:left;cursor:default;color:#484848}.cke_rtl .cke_button_label{padding-right:3px;padding-left:0;float:right}.cke_hc .cke_button_label{padding:0;display:inline-block;font-size:12px}.cke_button_arrow{display:inline-block;margin:8px 0 0 1px;width:0;height:0;cursor:default;vertical-align:top;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #484848}.cke_rtl .cke_button_arrow{margin-right:5px;margin-left:0}.cke_hc .cke_button_arrow{font-size:10px;margin:3px 0 0 3px;width:auto;border:0}.cke_toolbar_separator{float:left;background-color:#bcbcbc;margin:4px 2px 0 2px;height:18px;width:1px}.cke_rtl .cke_toolbar_separator{float:right}.cke_hc .cke_toolbar_separator{background-color:#000;margin-left:2px;margin-right:5px;margin-bottom:9px}.cke_hc.cke_rtl .cke_toolbar_separator{margin-left:5px;margin-right:2px}.cke_toolbar_break{display:block;clear:left}.cke_rtl .cke_toolbar_break{clear:right}a.cke_toolbox_collapser{width:12px;height:11px;float:right;margin:11px 0 0;font-size:0;cursor:default;text-align:center;border:1px solid #bcbcbc}.cke_rtl .cke_toolbox_collapser{float:left}.cke_toolbox_collapser:hover{background:#e5e5e5}.cke_toolbox_collapser.cke_toolbox_collapser_min{margin:0 2px 4px}.cke_toolbox_collapser .cke_arrow{display:inline-block;height:0;width:0;font-size:0;margin-top:1px;border:3px solid transparent;border-bottom-color:#484848}.cke_toolbox_collapser.cke_toolbox_collapser_min .cke_arrow{margin-top:4px;border-bottom-color:transparent;border-top-color:#484848}.cke_hc .cke_toolbox_collapser .cke_arrow{font-size:8px;width:auto;border:0;margin-top:0}.cke_menuitem span{cursor:default}.cke_menubutton{display:block}.cke_hc .cke_menubutton{padding:2px}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#e9e9e9;display:block;outline:1px dotted}.cke_menubutton:hover{outline:0}.cke_hc .cke_menubutton:hover,.cke_hc .cke_menubutton:focus,.cke_hc .cke_menubutton:active{border:2px solid;padding:0}.cke_menubutton_disabled:hover,.cke_menubutton_disabled:focus,.cke_menubutton_disabled:active{background-color:transparent;outline:0}.cke_menubutton_inner{display:table-row}.cke_menubutton_icon,.cke_menubutton_label,.cke_menuarrow{display:table-cell}.cke_menubutton_icon{background-color:#f8f8f8;padding:6px 4px}.cke_hc .cke_menubutton_icon{height:16px;width:0;padding:4px 0}.cke_menubutton:hover .cke_menubutton_icon,.cke_menubutton:focus .cke_menubutton_icon,.cke_menubutton:active .cke_menubutton_icon{background-color:#e9e9e9}.cke_menubutton_disabled:hover .cke_menubutton_icon,.cke_menubutton_disabled:focus .cke_menubutton_icon,.cke_menubutton_disabled:active .cke_menubutton_icon{background-color:#f8f8f8;outline:0}.cke_menuitem .cke_menubutton_on{background-color:#e9e9e9;border:1px solid #dedede;outline:0}.cke_menubutton_on .cke_menubutton_icon{padding-right:3px;background-color:#e9e9e9}.cke_menubutton_label{padding:0 5px;background-color:transparent;width:100%;vertical-align:middle}.cke_menubutton_shortcut{color:#979797}.cke_menubutton_disabled .cke_menubutton_label{opacity:.3;filter:alpha(opacity=30)}.cke_panel_frame .cke_menubutton_label{display:none}.cke_menuseparator{background-color:#d1d1d1;height:1px}.cke_menuarrow{background:transparent url(images/arrow.png) no-repeat 0 10px;padding:0 5px}.cke_rtl .cke_menuarrow{background-position:5px -13px;background-repeat:no-repeat}.cke_hc .cke_menuarrow{background-image:none}.cke_menuarrow span{display:none}.cke_hc .cke_menuarrow span{vertical-align:middle;display:inline}.cke_combo{display:inline-block;float:left;position:relative;margin-bottom:5px}.cke_rtl .cke_combo{float:right}.cke_hc .cke_combo{margin-top:1px;margin-bottom:10px}.cke_combo:after{content:"";position:absolute;height:18px;width:0;border-right:1px solid #bcbcbc;margin-top:5px;top:0;right:0}.cke_rtl .cke_combo:after{border-right:0;border-left:1px solid #bcbcbc;right:auto;left:0}.cke_hc .cke_combo:after{border-color:#000}a.cke_combo_button{cursor:default;display:inline-block;float:left;margin:0;padding:1px}.cke_rtl a.cke_combo_button{float:right}.cke_hc a.cke_combo_button{padding:4px}.cke_combo_on a.cke_combo_button,.cke_combo_off a.cke_combo_button:hover,.cke_combo_off a.cke_combo_button:focus,.cke_combo_off a.cke_combo_button:active{background:#e5e5e5;border:1px solid #bcbcbc;padding:0 0 0 1px;margin-left:-1px}.cke_combo_off a.cke_combo_button:focus{outline:0}.cke_combo_on a.cke_combo_button,.cke_combo_off a.cke_combo_button:active{background:#fff}.cke_rtl .cke_combo_on a.cke_combo_button,.cke_rtl .cke_combo_off a.cke_combo_button:hover,.cke_rtl .cke_combo_off a.cke_combo_button:focus,.cke_rtl .cke_combo_off a.cke_combo_button:active{padding:0 1px 0 0;margin-left:0;margin-right:-1px}.cke_hc .cke_combo_on a.cke_combo_button,.cke_hc .cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_combo_off a.cke_combo_button:active{border:3px solid #000;padding:1px 1px 1px 2px}.cke_hc.cke_rtl .cke_combo_on a.cke_combo_button,.cke_hc.cke_rtl .cke_combo_off a.cke_combo_button:hover,.cke_hc.cke_rtl .cke_combo_off a.cke_combo_button:focus,.cke_hc.cke_rtl .cke_combo_off a.cke_combo_button:active{padding:1px 2px 1px 1px}.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:0 0 0 3px;margin-left:-3px}.cke_rtl .cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_rtl .cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_rtl .cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_rtl .cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:0 3px 0 0;margin-left:0;margin-right:-3px}.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:1px 1px 1px 7px;margin-left:-6px}.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:1px 7px 1px 1px;margin-left:0;margin-right:-6px}.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:0;margin:0}.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:1px;margin:0}.cke_toolbar .cke_combo+.cke_toolbar_end,.cke_toolbar .cke_combo+.cke_toolgroup{margin-right:0;margin-left:2px}.cke_rtl .cke_toolbar .cke_combo+.cke_toolbar_end,.cke_rtl .cke_toolbar .cke_combo+.cke_toolgroup{margin-left:0;margin-right:2px}.cke_hc .cke_toolbar .cke_combo+.cke_toolbar_end,.cke_hc .cke_toolbar .cke_combo+.cke_toolgroup{margin-left:5px}.cke_hc.cke_rtl .cke_toolbar .cke_combo+.cke_toolbar_end,.cke_hc.cke_rtl .cke_toolbar .cke_combo+.cke_toolgroup{margin-left:0;margin-right:5px}.cke_toolbar.cke_toolbar_last .cke_combo:nth-last-child(-n+2):after{content:none;border:0;width:0;height:0}.cke_combo_text{line-height:26px;padding-left:10px;text-overflow:ellipsis;overflow:hidden;float:left;cursor:default;color:#484848;width:60px}.cke_rtl .cke_combo_text{float:right;text-align:right;padding-left:0;padding-right:10px}.cke_hc .cke_combo_text{line-height:18px;font-size:12px}.cke_combo_open{cursor:default;display:inline-block;font-size:0;height:19px;line-height:17px;margin:1px 10px 1px;width:5px}.cke_hc .cke_combo_open{height:12px}.cke_combo_arrow{cursor:default;margin:11px 0 0;float:left;height:0;width:0;font-size:0;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #484848}.cke_hc .cke_combo_arrow{font-size:10px;width:auto;border:0;margin-top:3px}.cke_combo_label{display:none;float:left;line-height:26px;vertical-align:top;margin-right:5px}.cke_rtl .cke_combo_label{float:right;margin-left:5px;margin-right:0}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{opacity:.3}.cke_path{float:left;margin:-2px 0 2px}a.cke_path_item,span.cke_path_empty{display:inline-block;float:left;padding:3px 4px;margin-right:2px;cursor:default;text-decoration:none;outline:0;border:0;color:#484848;font-weight:bold;font-size:11px}.cke_rtl .cke_path,.cke_rtl .cke_path_item,.cke_rtl .cke_path_empty{float:right}a.cke_path_item:hover,a.cke_path_item:focus,a.cke_path_item:active{background-color:#e5e5e5}.cke_hc a.cke_path_item:hover,.cke_hc a.cke_path_item:focus,.cke_hc a.cke_path_item:active{border:2px solid;padding:1px 2px}.cke_button__source_label,.cke_button__sourcedialog_label{display:inline}.cke_combopanel__fontsize{width:135px}textarea.cke_source{font-family:'Courier New',Monospace;font-size:small;background-color:#fff;white-space:pre-wrap;border:0;padding:0;margin:0;display:block}.cke_wysiwyg_frame,.cke_wysiwyg_div{background-color:#fff}.cke_notifications_area{pointer-events:none}.cke_notification{pointer-events:auto;position:relative;margin:10px;width:300px;color:white;text-align:center;opacity:.95;filter:alpha(opacity = 95);-webkit-animation:fadeIn .7s;animation:fadeIn .7s}.cke_notification_message a{color:#12306f}@-webkit-keyframes fadeIn{from{opacity:.4}to{opacity:.95}}@keyframes fadeIn{from{opacity:.4}to{opacity:.95}}.cke_notification_success{background:#72b572;border:1px solid #63a563}.cke_notification_warning{background:#c83939;border:1px solid #902b2b}.cke_notification_info{background:#2e9ad0;border:1px solid #0f74a8}.cke_notification_info span.cke_notification_progress{background-color:#0f74a8;display:block;padding:0;margin:0;height:100%;overflow:hidden;position:absolute;z-index:1}.cke_notification_message{position:relative;margin:4px 23px 3px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:18px;z-index:4;text-overflow:ellipsis;overflow:hidden}.cke_notification_close{background-image:url(images/close.png);background-repeat:no-repeat;background-position:50%;position:absolute;cursor:pointer;text-align:center;height:20px;width:20px;top:1px;right:1px;padding:0;margin:0;z-index:5;opacity:.6;filter:alpha(opacity = 60)}.cke_notification_close:hover{opacity:1;filter:alpha(opacity = 100)}.cke_notification_close span{display:none}.cke_notification_warning a.cke_notification_close{opacity:.8;filter:alpha(opacity = 80)}.cke_notification_warning a.cke_notification_close:hover{opacity:1;filter:alpha(opacity = 100)}.cke_chrome{visibility:inherit}.cke_voice_label{display:none}legend.cke_voice_label{display:none}.cke_button__bold_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -0px !important;}.cke_button__italic_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -24px !important;}.cke_button__strike_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -48px !important;}.cke_button__subscript_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -72px !important;}.cke_button__superscript_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -96px !important;}.cke_button__underline_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -120px !important;}.cke_rtl .cke_button__copy_icon, .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -144px !important;}.cke_ltr .cke_button__copy_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -168px !important;}.cke_rtl .cke_button__cut_icon, .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -192px !important;}.cke_ltr .cke_button__cut_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -216px !important;}.cke_rtl .cke_button__paste_icon, .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -240px !important;}.cke_ltr .cke_button__paste_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -264px !important;}.cke_button__horizontalrule_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -288px !important;}.cke_rtl .cke_button__indent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -312px !important;}.cke_ltr .cke_button__indent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -336px !important;}.cke_rtl .cke_button__outdent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -360px !important;}.cke_ltr .cke_button__outdent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -384px !important;}.cke_rtl .cke_button__anchor_icon, .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -408px !important;}.cke_ltr .cke_button__anchor_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -432px !important;}.cke_button__link_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -456px !important;}.cke_button__unlink_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -480px !important;}.cke_rtl .cke_button__bulletedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -504px !important;}.cke_ltr .cke_button__bulletedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -528px !important;}.cke_rtl .cke_button__numberedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -552px !important;}.cke_ltr .cke_button__numberedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -576px !important;}.cke_button__removeformat_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -600px !important;}.cke_rtl .cke_button__sourcedialog_icon, .cke_mixed_dir_content .cke_rtl .cke_button__sourcedialog_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -624px !important;}.cke_ltr .cke_button__sourcedialog_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -648px !important;}.cke_hidpi .cke_button__bold_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -0px !important;background-size: 16px !important;}.cke_hidpi .cke_button__italic_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -24px !important;background-size: 16px !important;}.cke_hidpi .cke_button__strike_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -48px !important;background-size: 16px !important;}.cke_hidpi .cke_button__subscript_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -72px !important;background-size: 16px !important;}.cke_hidpi .cke_button__superscript_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -96px !important;background-size: 16px !important;}.cke_hidpi .cke_button__underline_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -120px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__copy_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -144px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__copy_icon,.cke_ltr.cke_hidpi .cke_button__copy_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -168px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__cut_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -192px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__cut_icon,.cke_ltr.cke_hidpi .cke_button__cut_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -216px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__paste_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -240px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__paste_icon,.cke_ltr.cke_hidpi .cke_button__paste_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -264px !important;background-size: 16px !important;}.cke_hidpi .cke_button__horizontalrule_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -288px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__indent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -312px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__indent_icon,.cke_ltr.cke_hidpi .cke_button__indent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -336px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__outdent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -360px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__outdent_icon,.cke_ltr.cke_hidpi .cke_button__outdent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -384px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__anchor_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -408px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__anchor_icon,.cke_ltr.cke_hidpi .cke_button__anchor_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -432px !important;background-size: 16px !important;}.cke_hidpi .cke_button__link_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -456px !important;background-size: 16px !important;}.cke_hidpi .cke_button__unlink_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -480px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__bulletedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -504px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__bulletedlist_icon,.cke_ltr.cke_hidpi .cke_button__bulletedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -528px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__numberedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -552px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__numberedlist_icon,.cke_ltr.cke_hidpi .cke_button__numberedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -576px !important;background-size: 16px !important;}.cke_hidpi .cke_button__removeformat_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -600px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__sourcedialog_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__sourcedialog_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -624px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__sourcedialog_icon,.cke_ltr.cke_hidpi .cke_button__sourcedialog_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -648px !important;background-size: 16px !important;}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono-lisa/editor_gecko.css b/js/ckeditor/skins/moono-lisa/editor_gecko.css
new file mode 100644 (file)
index 0000000..ff104f1
--- /dev/null
@@ -0,0 +1,5 @@
+/*\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+*/\r
+.cke_reset{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;position:static;transition:none}.cke_reset_all,.cke_reset_all *,.cke_reset_all a,.cke_reset_all textarea{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;position:static;transition:none;border-collapse:collapse;font:normal normal normal 12px Arial,Helvetica,Tahoma,Verdana,Sans-Serif;color:#000;text-align:left;white-space:nowrap;cursor:auto;float:none}.cke_reset_all .cke_rtl *{text-align:right}.cke_reset_all iframe{vertical-align:inherit}.cke_reset_all textarea{white-space:pre-wrap}.cke_reset_all textarea,.cke_reset_all input[type="text"],.cke_reset_all input[type="password"]{cursor:text}.cke_reset_all textarea[disabled],.cke_reset_all input[type="text"][disabled],.cke_reset_all input[type="password"][disabled]{cursor:default}.cke_reset_all fieldset{padding:10px;border:2px groove #e0dfe3}.cke_reset_all select{box-sizing:border-box}.cke_reset_all table{table-layout:auto}.cke_chrome{display:block;border:1px solid #d1d1d1;padding:0}.cke_inner{display:block;background:#fff;padding:0;-webkit-touch-callout:none}.cke_float{border:0}.cke_float .cke_inner{padding-bottom:0}.cke_top,.cke_contents,.cke_bottom{display:block;overflow:hidden}.cke_top{border-bottom:1px solid #d1d1d1;background:#f8f8f8;padding:6px 8px 2px;white-space:normal}.cke_float .cke_top{border:1px solid #d1d1d1}.cke_bottom{padding:6px 8px 2px;position:relative;border-top:1px solid #d1d1d1;background:#f8f8f8}.cke_browser_ios .cke_contents{overflow-y:auto;-webkit-overflow-scrolling:touch}.cke_resizer{width:0;height:0;overflow:hidden;border-width:10px 10px 0 0;border-color:transparent #bcbcbc transparent transparent;border-style:dashed solid dashed dashed;font-size:0;vertical-align:bottom;margin-top:6px;margin-bottom:2px}.cke_hc .cke_resizer{font-size:15px;width:auto;height:auto;border-width:0}.cke_resizer_ltr{cursor:se-resize;float:right;margin-right:-4px}.cke_resizer_rtl{border-width:10px 0 0 10px;border-color:transparent transparent transparent #bcbcbc;border-style:dashed dashed dashed solid;cursor:sw-resize;float:left;margin-left:-4px;right:auto}.cke_wysiwyg_div{display:block;height:100%;overflow:auto;padding:0 8px;outline-style:none;box-sizing:border-box}.cke_panel{visibility:visible;width:120px;height:100px;overflow:hidden;background-color:#fff;border:1px solid #d1d1d1}.cke_menu_panel{padding:0;margin:0}.cke_combopanel{width:150px;height:170px}.cke_panel_frame{width:100%;height:100%;font-size:12px;overflow:auto;overflow-x:hidden}.cke_panel_container{overflow-y:auto;overflow-x:hidden}.cke_panel_block:focus{outline:0}.cke_panel_list{margin:0;padding:0;list-style-type:none;white-space:nowrap}.cke_panel_listItem{margin:0;padding:0}.cke_panel_listItem a{padding:6px 7px;display:block;color:inherit!important;text-decoration:none;overflow:hidden;text-overflow:ellipsis}.cke_hc .cke_panel_listItem a{border-style:none}.cke_panel_listItem.cke_selected a,.cke_panel_listItem a:hover,.cke_panel_listItem a:focus,.cke_panel_listItem a:active{background-color:#e9e9e9}.cke_panel_listItem a:focus{outline:1px dotted #000}.cke_hc .cke_panel_listItem a:hover,.cke_hc .cke_panel_listItem a:focus,.cke_hc .cke_panel_listItem a:active{border:2px solid;padding:4px 5px}.cke_panel_listItem p,.cke_panel_listItem h1,.cke_panel_listItem h2,.cke_panel_listItem h3,.cke_panel_listItem h4,.cke_panel_listItem h5,.cke_panel_listItem h6,.cke_panel_listItem pre{margin-top:0;margin-bottom:0}.cke_panel_grouptitle{cursor:default;font-size:11px;font-weight:bold;white-space:nowrap;margin:0;padding:6px 6px 7px 6px;color:#484848;border-bottom:1px solid #d1d1d1;background:#f8f8f8}.cke_colorblock{padding:10px;font-size:11px;font-family:'Microsoft Sans Serif',Tahoma,Arial,Verdana,Sans-Serif}.cke_colorblock,.cke_colorblock a{text-decoration:none;color:#000}a.cke_colorbox{padding:2px;float:left;width:20px;height:20px}.cke_rtl a.cke_colorbox{float:right}a:hover.cke_colorbox,a:focus.cke_colorbox,a:active.cke_colorbox{outline:0;padding:0;border:2px solid #139ff7}a:hover.cke_colorbox{border-color:#bcbcbc}span.cke_colorbox{width:20px;height:20px;float:left}.cke_rtl span.cke_colorbox{float:right}a.cke_colorauto,a.cke_colormore{border:#fff 1px solid;padding:3px;display:block;cursor:pointer}a.cke_colorauto{padding:0;border:1px solid transparent;margin-bottom:6px;height:26px;line-height:26px}a.cke_colormore{margin-top:10px;height:20px;line-height:19px}a:hover.cke_colorauto,a:hover.cke_colormore,a:focus.cke_colorauto,a:focus.cke_colormore,a:active.cke_colorauto,a:active.cke_colormore{outline:0;border:#139ff7 1px solid;background-color:#f8f8f8}a:hover.cke_colorauto,a:hover.cke_colormore{border-color:#bcbcbc}.cke_colorauto span.cke_colorbox{width:18px;height:18px;border:1px solid #808080;margin-left:1px;margin-top:3px}.cke_rtl .cke_colorauto span.cke_colorbox{margin-left:0;margin-right:1px}span.cke_colorbox[style*="#ffffff"],span.cke_colorbox[style*="#FFFFFF"],span.cke_colorbox[style="background-color:#fff"],span.cke_colorbox[style="background-color:#FFF"],span.cke_colorbox[style*="rgb(255,255,255)"],span.cke_colorbox[style*="rgb(255, 255, 255)"]{border:1px solid #808080;width:18px;height:18px}.cke_toolbar{float:left}.cke_rtl .cke_toolbar{float:right}.cke_toolgroup{border:0;float:left;margin:1px 2px 6px 0;padding-right:3px}.cke_rtl .cke_toolgroup{float:right;margin:1px 0 6px 2px;padding-left:3px;padding-right:0}.cke_hc .cke_toolgroup{margin-right:5px;margin-bottom:5px}.cke_hc.cke_rtl .cke_toolgroup{margin-right:0;margin-left:5px}a.cke_button{display:inline-block;height:18px;padding:4px 6px;outline:0;cursor:default;float:left;border:0;position:relative}.cke_rtl a.cke_button{float:right}.cke_hc a.cke_button{border:1px solid black;padding:3px 5px;margin:0 3px 5px 0}.cke_hc.cke_rtl a.cke_button{margin:0 0 5px 3px}a.cke_button_on{background:#fff;border:1px #bcbcbc solid;padding:3px 5px}a.cke_button_off:hover,a.cke_button_off:focus,a.cke_button_off:active{background:#e5e5e5;border:1px #bcbcbc solid;padding:3px 5px}.cke_hc a.cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active{background:#e5e5e5;border:3px solid #000;padding:1px 3px}a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{border:0;padding:4px 6px;background-color:transparent}a.cke_button_disabled:focus{border:1px #bcbcbc solid;padding:3px 5px}.cke_hc a.cke_button_disabled:hover,.cke_hc a.cke_button_disabled:focus,.cke_hc a.cke_button_disabled:active{border:1px solid #acacac;padding:3px 5px;margin:0 3px 5px 0}.cke_hc a.cke_button_disabled:focus{border:3px solid #000;padding:1px 3px}.cke_hc.cke_rtl a.cke_button_disabled:hover,.cke_hc.cke_rtl a.cke_button_disabled:focus,.cke_hc.cke_rtl a.cke_button_disabled:active{margin:0 0 5px 3px}a.cke_button_disabled .cke_button_icon,a.cke_button_disabled .cke_button_arrow{opacity:.3}.cke_hc a.cke_button_disabled{border-color:#acacac}.cke_hc a.cke_button_disabled .cke_button_icon,.cke_hc a.cke_button_disabled .cke_button_label{opacity:.5}.cke_toolgroup a.cke_button:last-child:after,.cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{content:"";position:absolute;height:18px;width:0;border-right:1px solid #bcbcbc;margin-top:4px;top:0;right:-3px}.cke_rtl .cke_toolgroup a.cke_button:last-child:after,.cke_rtl .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{border-right:0;right:auto;border-left:1px solid #bcbcbc;top:0;left:-3px}.cke_hc .cke_toolgroup a.cke_button:last-child:after,.cke_hc .cke_toolgroup a.cke_button.cke_button_disabled:last-child:after,.cke_hc .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{border-color:#000;top:0;right:-7px}.cke_hc.cke_rtl .cke_toolgroup a.cke_button:last-child:after,.cke_hc.cke_rtl .cke_toolgroup a.cke_button.cke_button_disabled:last-child:after,.cke_hc.cke_rtl .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{top:0;right:auto;left:-7px}.cke_toolgroup a.cke_button:hover:last-child:after,.cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-1px;right:-4px}.cke_rtl .cke_toolgroup a.cke_button:hover:last-child:after,.cke_rtl .cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-1px;right:auto;left:-4px}.cke_hc .cke_toolgroup a.cke_button:hover:last-child:after,.cke_hc .cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-2px;right:-9px}.cke_hc.cke_rtl .cke_toolgroup a.cke_button:hover:last-child:after,.cke_hc.cke_rtl .cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-2px;right:auto;left:-9px}.cke_toolbar.cke_toolbar_last .cke_toolgroup a.cke_button:last-child:after{content:none;border:0;width:0;height:0}.cke_button_icon{cursor:inherit;background-repeat:no-repeat;margin-top:1px;width:16px;height:16px;float:left;display:inline-block}.cke_rtl .cke_button_icon{float:right}.cke_hc .cke_button_icon{display:none}.cke_button_label{display:none;padding-left:3px;margin-top:1px;line-height:17px;vertical-align:middle;float:left;cursor:default;color:#484848}.cke_rtl .cke_button_label{padding-right:3px;padding-left:0;float:right}.cke_hc .cke_button_label{padding:0;display:inline-block;font-size:12px}.cke_button_arrow{display:inline-block;margin:8px 0 0 1px;width:0;height:0;cursor:default;vertical-align:top;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #484848}.cke_rtl .cke_button_arrow{margin-right:5px;margin-left:0}.cke_hc .cke_button_arrow{font-size:10px;margin:3px 0 0 3px;width:auto;border:0}.cke_toolbar_separator{float:left;background-color:#bcbcbc;margin:4px 2px 0 2px;height:18px;width:1px}.cke_rtl .cke_toolbar_separator{float:right}.cke_hc .cke_toolbar_separator{background-color:#000;margin-left:2px;margin-right:5px;margin-bottom:9px}.cke_hc.cke_rtl .cke_toolbar_separator{margin-left:5px;margin-right:2px}.cke_toolbar_break{display:block;clear:left}.cke_rtl .cke_toolbar_break{clear:right}a.cke_toolbox_collapser{width:12px;height:11px;float:right;margin:11px 0 0;font-size:0;cursor:default;text-align:center;border:1px solid #bcbcbc}.cke_rtl .cke_toolbox_collapser{float:left}.cke_toolbox_collapser:hover{background:#e5e5e5}.cke_toolbox_collapser.cke_toolbox_collapser_min{margin:0 2px 4px}.cke_toolbox_collapser .cke_arrow{display:inline-block;height:0;width:0;font-size:0;margin-top:1px;border:3px solid transparent;border-bottom-color:#484848}.cke_toolbox_collapser.cke_toolbox_collapser_min .cke_arrow{margin-top:4px;border-bottom-color:transparent;border-top-color:#484848}.cke_hc .cke_toolbox_collapser .cke_arrow{font-size:8px;width:auto;border:0;margin-top:0}.cke_menuitem span{cursor:default}.cke_menubutton{display:block}.cke_hc .cke_menubutton{padding:2px}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#e9e9e9;display:block;outline:1px dotted}.cke_menubutton:hover{outline:0}.cke_hc .cke_menubutton:hover,.cke_hc .cke_menubutton:focus,.cke_hc .cke_menubutton:active{border:2px solid;padding:0}.cke_menubutton_disabled:hover,.cke_menubutton_disabled:focus,.cke_menubutton_disabled:active{background-color:transparent;outline:0}.cke_menubutton_inner{display:table-row}.cke_menubutton_icon,.cke_menubutton_label,.cke_menuarrow{display:table-cell}.cke_menubutton_icon{background-color:#f8f8f8;padding:6px 4px}.cke_hc .cke_menubutton_icon{height:16px;width:0;padding:4px 0}.cke_menubutton:hover .cke_menubutton_icon,.cke_menubutton:focus .cke_menubutton_icon,.cke_menubutton:active .cke_menubutton_icon{background-color:#e9e9e9}.cke_menubutton_disabled:hover .cke_menubutton_icon,.cke_menubutton_disabled:focus .cke_menubutton_icon,.cke_menubutton_disabled:active .cke_menubutton_icon{background-color:#f8f8f8;outline:0}.cke_menuitem .cke_menubutton_on{background-color:#e9e9e9;border:1px solid #dedede;outline:0}.cke_menubutton_on .cke_menubutton_icon{padding-right:3px;background-color:#e9e9e9}.cke_menubutton_label{padding:0 5px;background-color:transparent;width:100%;vertical-align:middle}.cke_menubutton_shortcut{color:#979797}.cke_menubutton_disabled .cke_menubutton_label{opacity:.3;filter:alpha(opacity=30)}.cke_panel_frame .cke_menubutton_label{display:none}.cke_menuseparator{background-color:#d1d1d1;height:1px}.cke_menuarrow{background:transparent url(images/arrow.png) no-repeat 0 10px;padding:0 5px}.cke_rtl .cke_menuarrow{background-position:5px -13px;background-repeat:no-repeat}.cke_hc .cke_menuarrow{background-image:none}.cke_menuarrow span{display:none}.cke_hc .cke_menuarrow span{vertical-align:middle;display:inline}.cke_combo{display:inline-block;float:left;position:relative;margin-bottom:5px}.cke_rtl .cke_combo{float:right}.cke_hc .cke_combo{margin-top:1px;margin-bottom:10px}.cke_combo:after{content:"";position:absolute;height:18px;width:0;border-right:1px solid #bcbcbc;margin-top:5px;top:0;right:0}.cke_rtl .cke_combo:after{border-right:0;border-left:1px solid #bcbcbc;right:auto;left:0}.cke_hc .cke_combo:after{border-color:#000}a.cke_combo_button{cursor:default;display:inline-block;float:left;margin:0;padding:1px}.cke_rtl a.cke_combo_button{float:right}.cke_hc a.cke_combo_button{padding:4px}.cke_combo_on a.cke_combo_button,.cke_combo_off a.cke_combo_button:hover,.cke_combo_off a.cke_combo_button:focus,.cke_combo_off a.cke_combo_button:active{background:#e5e5e5;border:1px solid #bcbcbc;padding:0 0 0 1px;margin-left:-1px}.cke_combo_off a.cke_combo_button:focus{outline:0}.cke_combo_on a.cke_combo_button,.cke_combo_off a.cke_combo_button:active{background:#fff}.cke_rtl .cke_combo_on a.cke_combo_button,.cke_rtl .cke_combo_off a.cke_combo_button:hover,.cke_rtl .cke_combo_off a.cke_combo_button:focus,.cke_rtl .cke_combo_off a.cke_combo_button:active{padding:0 1px 0 0;margin-left:0;margin-right:-1px}.cke_hc .cke_combo_on a.cke_combo_button,.cke_hc .cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_combo_off a.cke_combo_button:active{border:3px solid #000;padding:1px 1px 1px 2px}.cke_hc.cke_rtl .cke_combo_on a.cke_combo_button,.cke_hc.cke_rtl .cke_combo_off a.cke_combo_button:hover,.cke_hc.cke_rtl .cke_combo_off a.cke_combo_button:focus,.cke_hc.cke_rtl .cke_combo_off a.cke_combo_button:active{padding:1px 2px 1px 1px}.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:0 0 0 3px;margin-left:-3px}.cke_rtl .cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_rtl .cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_rtl .cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_rtl .cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:0 3px 0 0;margin-left:0;margin-right:-3px}.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:1px 1px 1px 7px;margin-left:-6px}.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:1px 7px 1px 1px;margin-left:0;margin-right:-6px}.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:0;margin:0}.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:1px;margin:0}.cke_toolbar .cke_combo+.cke_toolbar_end,.cke_toolbar .cke_combo+.cke_toolgroup{margin-right:0;margin-left:2px}.cke_rtl .cke_toolbar .cke_combo+.cke_toolbar_end,.cke_rtl .cke_toolbar .cke_combo+.cke_toolgroup{margin-left:0;margin-right:2px}.cke_hc .cke_toolbar .cke_combo+.cke_toolbar_end,.cke_hc .cke_toolbar .cke_combo+.cke_toolgroup{margin-left:5px}.cke_hc.cke_rtl .cke_toolbar .cke_combo+.cke_toolbar_end,.cke_hc.cke_rtl .cke_toolbar .cke_combo+.cke_toolgroup{margin-left:0;margin-right:5px}.cke_toolbar.cke_toolbar_last .cke_combo:nth-last-child(-n+2):after{content:none;border:0;width:0;height:0}.cke_combo_text{line-height:26px;padding-left:10px;text-overflow:ellipsis;overflow:hidden;float:left;cursor:default;color:#484848;width:60px}.cke_rtl .cke_combo_text{float:right;text-align:right;padding-left:0;padding-right:10px}.cke_hc .cke_combo_text{line-height:18px;font-size:12px}.cke_combo_open{cursor:default;display:inline-block;font-size:0;height:19px;line-height:17px;margin:1px 10px 1px;width:5px}.cke_hc .cke_combo_open{height:12px}.cke_combo_arrow{cursor:default;margin:11px 0 0;float:left;height:0;width:0;font-size:0;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #484848}.cke_hc .cke_combo_arrow{font-size:10px;width:auto;border:0;margin-top:3px}.cke_combo_label{display:none;float:left;line-height:26px;vertical-align:top;margin-right:5px}.cke_rtl .cke_combo_label{float:right;margin-left:5px;margin-right:0}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{opacity:.3}.cke_path{float:left;margin:-2px 0 2px}a.cke_path_item,span.cke_path_empty{display:inline-block;float:left;padding:3px 4px;margin-right:2px;cursor:default;text-decoration:none;outline:0;border:0;color:#484848;font-weight:bold;font-size:11px}.cke_rtl .cke_path,.cke_rtl .cke_path_item,.cke_rtl .cke_path_empty{float:right}a.cke_path_item:hover,a.cke_path_item:focus,a.cke_path_item:active{background-color:#e5e5e5}.cke_hc a.cke_path_item:hover,.cke_hc a.cke_path_item:focus,.cke_hc a.cke_path_item:active{border:2px solid;padding:1px 2px}.cke_button__source_label,.cke_button__sourcedialog_label{display:inline}.cke_combopanel__fontsize{width:135px}textarea.cke_source{font-family:'Courier New',Monospace;font-size:small;background-color:#fff;white-space:pre-wrap;border:0;padding:0;margin:0;display:block}.cke_wysiwyg_frame,.cke_wysiwyg_div{background-color:#fff}.cke_notifications_area{pointer-events:none}.cke_notification{pointer-events:auto;position:relative;margin:10px;width:300px;color:white;text-align:center;opacity:.95;filter:alpha(opacity = 95);-webkit-animation:fadeIn .7s;animation:fadeIn .7s}.cke_notification_message a{color:#12306f}@-webkit-keyframes fadeIn{from{opacity:.4}to{opacity:.95}}@keyframes fadeIn{from{opacity:.4}to{opacity:.95}}.cke_notification_success{background:#72b572;border:1px solid #63a563}.cke_notification_warning{background:#c83939;border:1px solid #902b2b}.cke_notification_info{background:#2e9ad0;border:1px solid #0f74a8}.cke_notification_info span.cke_notification_progress{background-color:#0f74a8;display:block;padding:0;margin:0;height:100%;overflow:hidden;position:absolute;z-index:1}.cke_notification_message{position:relative;margin:4px 23px 3px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:18px;z-index:4;text-overflow:ellipsis;overflow:hidden}.cke_notification_close{background-image:url(images/close.png);background-repeat:no-repeat;background-position:50%;position:absolute;cursor:pointer;text-align:center;height:20px;width:20px;top:1px;right:1px;padding:0;margin:0;z-index:5;opacity:.6;filter:alpha(opacity = 60)}.cke_notification_close:hover{opacity:1;filter:alpha(opacity = 100)}.cke_notification_close span{display:none}.cke_notification_warning a.cke_notification_close{opacity:.8;filter:alpha(opacity = 80)}.cke_notification_warning a.cke_notification_close:hover{opacity:1;filter:alpha(opacity = 100)}.cke_chrome{visibility:inherit}.cke_voice_label{display:none}legend.cke_voice_label{display:none}.cke_bottom{padding-bottom:3px}.cke_combo_text{margin-bottom:-1px;margin-top:1px}.cke_button__bold_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -0px !important;}.cke_button__italic_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -24px !important;}.cke_button__strike_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -48px !important;}.cke_button__subscript_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -72px !important;}.cke_button__superscript_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -96px !important;}.cke_button__underline_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -120px !important;}.cke_rtl .cke_button__copy_icon, .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -144px !important;}.cke_ltr .cke_button__copy_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -168px !important;}.cke_rtl .cke_button__cut_icon, .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -192px !important;}.cke_ltr .cke_button__cut_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -216px !important;}.cke_rtl .cke_button__paste_icon, .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -240px !important;}.cke_ltr .cke_button__paste_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -264px !important;}.cke_button__horizontalrule_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -288px !important;}.cke_rtl .cke_button__indent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -312px !important;}.cke_ltr .cke_button__indent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -336px !important;}.cke_rtl .cke_button__outdent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -360px !important;}.cke_ltr .cke_button__outdent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -384px !important;}.cke_rtl .cke_button__anchor_icon, .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -408px !important;}.cke_ltr .cke_button__anchor_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -432px !important;}.cke_button__link_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -456px !important;}.cke_button__unlink_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -480px !important;}.cke_rtl .cke_button__bulletedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -504px !important;}.cke_ltr .cke_button__bulletedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -528px !important;}.cke_rtl .cke_button__numberedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -552px !important;}.cke_ltr .cke_button__numberedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -576px !important;}.cke_button__removeformat_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -600px !important;}.cke_rtl .cke_button__sourcedialog_icon, .cke_mixed_dir_content .cke_rtl .cke_button__sourcedialog_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -624px !important;}.cke_ltr .cke_button__sourcedialog_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -648px !important;}.cke_hidpi .cke_button__bold_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -0px !important;background-size: 16px !important;}.cke_hidpi .cke_button__italic_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -24px !important;background-size: 16px !important;}.cke_hidpi .cke_button__strike_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -48px !important;background-size: 16px !important;}.cke_hidpi .cke_button__subscript_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -72px !important;background-size: 16px !important;}.cke_hidpi .cke_button__superscript_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -96px !important;background-size: 16px !important;}.cke_hidpi .cke_button__underline_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -120px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__copy_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -144px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__copy_icon,.cke_ltr.cke_hidpi .cke_button__copy_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -168px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__cut_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -192px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__cut_icon,.cke_ltr.cke_hidpi .cke_button__cut_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -216px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__paste_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -240px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__paste_icon,.cke_ltr.cke_hidpi .cke_button__paste_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -264px !important;background-size: 16px !important;}.cke_hidpi .cke_button__horizontalrule_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -288px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__indent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -312px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__indent_icon,.cke_ltr.cke_hidpi .cke_button__indent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -336px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__outdent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -360px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__outdent_icon,.cke_ltr.cke_hidpi .cke_button__outdent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -384px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__anchor_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -408px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__anchor_icon,.cke_ltr.cke_hidpi .cke_button__anchor_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -432px !important;background-size: 16px !important;}.cke_hidpi .cke_button__link_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -456px !important;background-size: 16px !important;}.cke_hidpi .cke_button__unlink_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -480px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__bulletedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -504px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__bulletedlist_icon,.cke_ltr.cke_hidpi .cke_button__bulletedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -528px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__numberedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -552px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__numberedlist_icon,.cke_ltr.cke_hidpi .cke_button__numberedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -576px !important;background-size: 16px !important;}.cke_hidpi .cke_button__removeformat_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -600px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__sourcedialog_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__sourcedialog_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -624px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__sourcedialog_icon,.cke_ltr.cke_hidpi .cke_button__sourcedialog_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -648px !important;background-size: 16px !important;}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono-lisa/editor_ie.css b/js/ckeditor/skins/moono-lisa/editor_ie.css
new file mode 100644 (file)
index 0000000..603df07
--- /dev/null
@@ -0,0 +1,5 @@
+/*\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+*/\r
+.cke_reset{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;position:static;transition:none}.cke_reset_all,.cke_reset_all *,.cke_reset_all a,.cke_reset_all textarea{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;position:static;transition:none;border-collapse:collapse;font:normal normal normal 12px Arial,Helvetica,Tahoma,Verdana,Sans-Serif;color:#000;text-align:left;white-space:nowrap;cursor:auto;float:none}.cke_reset_all .cke_rtl *{text-align:right}.cke_reset_all iframe{vertical-align:inherit}.cke_reset_all textarea{white-space:pre-wrap}.cke_reset_all textarea,.cke_reset_all input[type="text"],.cke_reset_all input[type="password"]{cursor:text}.cke_reset_all textarea[disabled],.cke_reset_all input[type="text"][disabled],.cke_reset_all input[type="password"][disabled]{cursor:default}.cke_reset_all fieldset{padding:10px;border:2px groove #e0dfe3}.cke_reset_all select{box-sizing:border-box}.cke_reset_all table{table-layout:auto}.cke_chrome{display:block;border:1px solid #d1d1d1;padding:0}.cke_inner{display:block;background:#fff;padding:0;-webkit-touch-callout:none}.cke_float{border:0}.cke_float .cke_inner{padding-bottom:0}.cke_top,.cke_contents,.cke_bottom{display:block;overflow:hidden}.cke_top{border-bottom:1px solid #d1d1d1;background:#f8f8f8;padding:6px 8px 2px;white-space:normal}.cke_float .cke_top{border:1px solid #d1d1d1}.cke_bottom{padding:6px 8px 2px;position:relative;border-top:1px solid #d1d1d1;background:#f8f8f8}.cke_browser_ios .cke_contents{overflow-y:auto;-webkit-overflow-scrolling:touch}.cke_resizer{width:0;height:0;overflow:hidden;border-width:10px 10px 0 0;border-color:transparent #bcbcbc transparent transparent;border-style:dashed solid dashed dashed;font-size:0;vertical-align:bottom;margin-top:6px;margin-bottom:2px}.cke_hc .cke_resizer{font-size:15px;width:auto;height:auto;border-width:0}.cke_resizer_ltr{cursor:se-resize;float:right;margin-right:-4px}.cke_resizer_rtl{border-width:10px 0 0 10px;border-color:transparent transparent transparent #bcbcbc;border-style:dashed dashed dashed solid;cursor:sw-resize;float:left;margin-left:-4px;right:auto}.cke_wysiwyg_div{display:block;height:100%;overflow:auto;padding:0 8px;outline-style:none;box-sizing:border-box}.cke_panel{visibility:visible;width:120px;height:100px;overflow:hidden;background-color:#fff;border:1px solid #d1d1d1}.cke_menu_panel{padding:0;margin:0}.cke_combopanel{width:150px;height:170px}.cke_panel_frame{width:100%;height:100%;font-size:12px;overflow:auto;overflow-x:hidden}.cke_panel_container{overflow-y:auto;overflow-x:hidden}.cke_panel_block:focus{outline:0}.cke_panel_list{margin:0;padding:0;list-style-type:none;white-space:nowrap}.cke_panel_listItem{margin:0;padding:0}.cke_panel_listItem a{padding:6px 7px;display:block;color:inherit!important;text-decoration:none;overflow:hidden;text-overflow:ellipsis}.cke_hc .cke_panel_listItem a{border-style:none}.cke_panel_listItem.cke_selected a,.cke_panel_listItem a:hover,.cke_panel_listItem a:focus,.cke_panel_listItem a:active{background-color:#e9e9e9}.cke_panel_listItem a:focus{outline:1px dotted #000}.cke_hc .cke_panel_listItem a:hover,.cke_hc .cke_panel_listItem a:focus,.cke_hc .cke_panel_listItem a:active{border:2px solid;padding:4px 5px}.cke_panel_listItem p,.cke_panel_listItem h1,.cke_panel_listItem h2,.cke_panel_listItem h3,.cke_panel_listItem h4,.cke_panel_listItem h5,.cke_panel_listItem h6,.cke_panel_listItem pre{margin-top:0;margin-bottom:0}.cke_panel_grouptitle{cursor:default;font-size:11px;font-weight:bold;white-space:nowrap;margin:0;padding:6px 6px 7px 6px;color:#484848;border-bottom:1px solid #d1d1d1;background:#f8f8f8}.cke_colorblock{padding:10px;font-size:11px;font-family:'Microsoft Sans Serif',Tahoma,Arial,Verdana,Sans-Serif}.cke_colorblock,.cke_colorblock a{text-decoration:none;color:#000}a.cke_colorbox{padding:2px;float:left;width:20px;height:20px}.cke_rtl a.cke_colorbox{float:right}a:hover.cke_colorbox,a:focus.cke_colorbox,a:active.cke_colorbox{outline:0;padding:0;border:2px solid #139ff7}a:hover.cke_colorbox{border-color:#bcbcbc}span.cke_colorbox{width:20px;height:20px;float:left}.cke_rtl span.cke_colorbox{float:right}a.cke_colorauto,a.cke_colormore{border:#fff 1px solid;padding:3px;display:block;cursor:pointer}a.cke_colorauto{padding:0;border:1px solid transparent;margin-bottom:6px;height:26px;line-height:26px}a.cke_colormore{margin-top:10px;height:20px;line-height:19px}a:hover.cke_colorauto,a:hover.cke_colormore,a:focus.cke_colorauto,a:focus.cke_colormore,a:active.cke_colorauto,a:active.cke_colormore{outline:0;border:#139ff7 1px solid;background-color:#f8f8f8}a:hover.cke_colorauto,a:hover.cke_colormore{border-color:#bcbcbc}.cke_colorauto span.cke_colorbox{width:18px;height:18px;border:1px solid #808080;margin-left:1px;margin-top:3px}.cke_rtl .cke_colorauto span.cke_colorbox{margin-left:0;margin-right:1px}span.cke_colorbox[style*="#ffffff"],span.cke_colorbox[style*="#FFFFFF"],span.cke_colorbox[style="background-color:#fff"],span.cke_colorbox[style="background-color:#FFF"],span.cke_colorbox[style*="rgb(255,255,255)"],span.cke_colorbox[style*="rgb(255, 255, 255)"]{border:1px solid #808080;width:18px;height:18px}.cke_toolbar{float:left}.cke_rtl .cke_toolbar{float:right}.cke_toolgroup{border:0;float:left;margin:1px 2px 6px 0;padding-right:3px}.cke_rtl .cke_toolgroup{float:right;margin:1px 0 6px 2px;padding-left:3px;padding-right:0}.cke_hc .cke_toolgroup{margin-right:5px;margin-bottom:5px}.cke_hc.cke_rtl .cke_toolgroup{margin-right:0;margin-left:5px}a.cke_button{display:inline-block;height:18px;padding:4px 6px;outline:0;cursor:default;float:left;border:0;position:relative}.cke_rtl a.cke_button{float:right}.cke_hc a.cke_button{border:1px solid black;padding:3px 5px;margin:0 3px 5px 0}.cke_hc.cke_rtl a.cke_button{margin:0 0 5px 3px}a.cke_button_on{background:#fff;border:1px #bcbcbc solid;padding:3px 5px}a.cke_button_off:hover,a.cke_button_off:focus,a.cke_button_off:active{background:#e5e5e5;border:1px #bcbcbc solid;padding:3px 5px}.cke_hc a.cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active{background:#e5e5e5;border:3px solid #000;padding:1px 3px}a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{border:0;padding:4px 6px;background-color:transparent}a.cke_button_disabled:focus{border:1px #bcbcbc solid;padding:3px 5px}.cke_hc a.cke_button_disabled:hover,.cke_hc a.cke_button_disabled:focus,.cke_hc a.cke_button_disabled:active{border:1px solid #acacac;padding:3px 5px;margin:0 3px 5px 0}.cke_hc a.cke_button_disabled:focus{border:3px solid #000;padding:1px 3px}.cke_hc.cke_rtl a.cke_button_disabled:hover,.cke_hc.cke_rtl a.cke_button_disabled:focus,.cke_hc.cke_rtl a.cke_button_disabled:active{margin:0 0 5px 3px}a.cke_button_disabled .cke_button_icon,a.cke_button_disabled .cke_button_arrow{opacity:.3}.cke_hc a.cke_button_disabled{border-color:#acacac}.cke_hc a.cke_button_disabled .cke_button_icon,.cke_hc a.cke_button_disabled .cke_button_label{opacity:.5}.cke_toolgroup a.cke_button:last-child:after,.cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{content:"";position:absolute;height:18px;width:0;border-right:1px solid #bcbcbc;margin-top:4px;top:0;right:-3px}.cke_rtl .cke_toolgroup a.cke_button:last-child:after,.cke_rtl .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{border-right:0;right:auto;border-left:1px solid #bcbcbc;top:0;left:-3px}.cke_hc .cke_toolgroup a.cke_button:last-child:after,.cke_hc .cke_toolgroup a.cke_button.cke_button_disabled:last-child:after,.cke_hc .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{border-color:#000;top:0;right:-7px}.cke_hc.cke_rtl .cke_toolgroup a.cke_button:last-child:after,.cke_hc.cke_rtl .cke_toolgroup a.cke_button.cke_button_disabled:last-child:after,.cke_hc.cke_rtl .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{top:0;right:auto;left:-7px}.cke_toolgroup a.cke_button:hover:last-child:after,.cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-1px;right:-4px}.cke_rtl .cke_toolgroup a.cke_button:hover:last-child:after,.cke_rtl .cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-1px;right:auto;left:-4px}.cke_hc .cke_toolgroup a.cke_button:hover:last-child:after,.cke_hc .cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-2px;right:-9px}.cke_hc.cke_rtl .cke_toolgroup a.cke_button:hover:last-child:after,.cke_hc.cke_rtl .cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-2px;right:auto;left:-9px}.cke_toolbar.cke_toolbar_last .cke_toolgroup a.cke_button:last-child:after{content:none;border:0;width:0;height:0}.cke_button_icon{cursor:inherit;background-repeat:no-repeat;margin-top:1px;width:16px;height:16px;float:left;display:inline-block}.cke_rtl .cke_button_icon{float:right}.cke_hc .cke_button_icon{display:none}.cke_button_label{display:none;padding-left:3px;margin-top:1px;line-height:17px;vertical-align:middle;float:left;cursor:default;color:#484848}.cke_rtl .cke_button_label{padding-right:3px;padding-left:0;float:right}.cke_hc .cke_button_label{padding:0;display:inline-block;font-size:12px}.cke_button_arrow{display:inline-block;margin:8px 0 0 1px;width:0;height:0;cursor:default;vertical-align:top;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #484848}.cke_rtl .cke_button_arrow{margin-right:5px;margin-left:0}.cke_hc .cke_button_arrow{font-size:10px;margin:3px 0 0 3px;width:auto;border:0}.cke_toolbar_separator{float:left;background-color:#bcbcbc;margin:4px 2px 0 2px;height:18px;width:1px}.cke_rtl .cke_toolbar_separator{float:right}.cke_hc .cke_toolbar_separator{background-color:#000;margin-left:2px;margin-right:5px;margin-bottom:9px}.cke_hc.cke_rtl .cke_toolbar_separator{margin-left:5px;margin-right:2px}.cke_toolbar_break{display:block;clear:left}.cke_rtl .cke_toolbar_break{clear:right}a.cke_toolbox_collapser{width:12px;height:11px;float:right;margin:11px 0 0;font-size:0;cursor:default;text-align:center;border:1px solid #bcbcbc}.cke_rtl .cke_toolbox_collapser{float:left}.cke_toolbox_collapser:hover{background:#e5e5e5}.cke_toolbox_collapser.cke_toolbox_collapser_min{margin:0 2px 4px}.cke_toolbox_collapser .cke_arrow{display:inline-block;height:0;width:0;font-size:0;margin-top:1px;border:3px solid transparent;border-bottom-color:#484848}.cke_toolbox_collapser.cke_toolbox_collapser_min .cke_arrow{margin-top:4px;border-bottom-color:transparent;border-top-color:#484848}.cke_hc .cke_toolbox_collapser .cke_arrow{font-size:8px;width:auto;border:0;margin-top:0}.cke_menuitem span{cursor:default}.cke_menubutton{display:block}.cke_hc .cke_menubutton{padding:2px}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#e9e9e9;display:block;outline:1px dotted}.cke_menubutton:hover{outline:0}.cke_hc .cke_menubutton:hover,.cke_hc .cke_menubutton:focus,.cke_hc .cke_menubutton:active{border:2px solid;padding:0}.cke_menubutton_disabled:hover,.cke_menubutton_disabled:focus,.cke_menubutton_disabled:active{background-color:transparent;outline:0}.cke_menubutton_inner{display:table-row}.cke_menubutton_icon,.cke_menubutton_label,.cke_menuarrow{display:table-cell}.cke_menubutton_icon{background-color:#f8f8f8;padding:6px 4px}.cke_hc .cke_menubutton_icon{height:16px;width:0;padding:4px 0}.cke_menubutton:hover .cke_menubutton_icon,.cke_menubutton:focus .cke_menubutton_icon,.cke_menubutton:active .cke_menubutton_icon{background-color:#e9e9e9}.cke_menubutton_disabled:hover .cke_menubutton_icon,.cke_menubutton_disabled:focus .cke_menubutton_icon,.cke_menubutton_disabled:active .cke_menubutton_icon{background-color:#f8f8f8;outline:0}.cke_menuitem .cke_menubutton_on{background-color:#e9e9e9;border:1px solid #dedede;outline:0}.cke_menubutton_on .cke_menubutton_icon{padding-right:3px;background-color:#e9e9e9}.cke_menubutton_label{padding:0 5px;background-color:transparent;width:100%;vertical-align:middle}.cke_menubutton_shortcut{color:#979797}.cke_menubutton_disabled .cke_menubutton_label{opacity:.3;filter:alpha(opacity=30)}.cke_panel_frame .cke_menubutton_label{display:none}.cke_menuseparator{background-color:#d1d1d1;height:1px}.cke_menuarrow{background:transparent url(images/arrow.png) no-repeat 0 10px;padding:0 5px}.cke_rtl .cke_menuarrow{background-position:5px -13px;background-repeat:no-repeat}.cke_hc .cke_menuarrow{background-image:none}.cke_menuarrow span{display:none}.cke_hc .cke_menuarrow span{vertical-align:middle;display:inline}.cke_combo{display:inline-block;float:left;position:relative;margin-bottom:5px}.cke_rtl .cke_combo{float:right}.cke_hc .cke_combo{margin-top:1px;margin-bottom:10px}.cke_combo:after{content:"";position:absolute;height:18px;width:0;border-right:1px solid #bcbcbc;margin-top:5px;top:0;right:0}.cke_rtl .cke_combo:after{border-right:0;border-left:1px solid #bcbcbc;right:auto;left:0}.cke_hc .cke_combo:after{border-color:#000}a.cke_combo_button{cursor:default;display:inline-block;float:left;margin:0;padding:1px}.cke_rtl a.cke_combo_button{float:right}.cke_hc a.cke_combo_button{padding:4px}.cke_combo_on a.cke_combo_button,.cke_combo_off a.cke_combo_button:hover,.cke_combo_off a.cke_combo_button:focus,.cke_combo_off a.cke_combo_button:active{background:#e5e5e5;border:1px solid #bcbcbc;padding:0 0 0 1px;margin-left:-1px}.cke_combo_off a.cke_combo_button:focus{outline:0}.cke_combo_on a.cke_combo_button,.cke_combo_off a.cke_combo_button:active{background:#fff}.cke_rtl .cke_combo_on a.cke_combo_button,.cke_rtl .cke_combo_off a.cke_combo_button:hover,.cke_rtl .cke_combo_off a.cke_combo_button:focus,.cke_rtl .cke_combo_off a.cke_combo_button:active{padding:0 1px 0 0;margin-left:0;margin-right:-1px}.cke_hc .cke_combo_on a.cke_combo_button,.cke_hc .cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_combo_off a.cke_combo_button:active{border:3px solid #000;padding:1px 1px 1px 2px}.cke_hc.cke_rtl .cke_combo_on a.cke_combo_button,.cke_hc.cke_rtl .cke_combo_off a.cke_combo_button:hover,.cke_hc.cke_rtl .cke_combo_off a.cke_combo_button:focus,.cke_hc.cke_rtl .cke_combo_off a.cke_combo_button:active{padding:1px 2px 1px 1px}.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:0 0 0 3px;margin-left:-3px}.cke_rtl .cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_rtl .cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_rtl .cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_rtl .cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:0 3px 0 0;margin-left:0;margin-right:-3px}.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:1px 1px 1px 7px;margin-left:-6px}.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:1px 7px 1px 1px;margin-left:0;margin-right:-6px}.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:0;margin:0}.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:1px;margin:0}.cke_toolbar .cke_combo+.cke_toolbar_end,.cke_toolbar .cke_combo+.cke_toolgroup{margin-right:0;margin-left:2px}.cke_rtl .cke_toolbar .cke_combo+.cke_toolbar_end,.cke_rtl .cke_toolbar .cke_combo+.cke_toolgroup{margin-left:0;margin-right:2px}.cke_hc .cke_toolbar .cke_combo+.cke_toolbar_end,.cke_hc .cke_toolbar .cke_combo+.cke_toolgroup{margin-left:5px}.cke_hc.cke_rtl .cke_toolbar .cke_combo+.cke_toolbar_end,.cke_hc.cke_rtl .cke_toolbar .cke_combo+.cke_toolgroup{margin-left:0;margin-right:5px}.cke_toolbar.cke_toolbar_last .cke_combo:nth-last-child(-n+2):after{content:none;border:0;width:0;height:0}.cke_combo_text{line-height:26px;padding-left:10px;text-overflow:ellipsis;overflow:hidden;float:left;cursor:default;color:#484848;width:60px}.cke_rtl .cke_combo_text{float:right;text-align:right;padding-left:0;padding-right:10px}.cke_hc .cke_combo_text{line-height:18px;font-size:12px}.cke_combo_open{cursor:default;display:inline-block;font-size:0;height:19px;line-height:17px;margin:1px 10px 1px;width:5px}.cke_hc .cke_combo_open{height:12px}.cke_combo_arrow{cursor:default;margin:11px 0 0;float:left;height:0;width:0;font-size:0;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #484848}.cke_hc .cke_combo_arrow{font-size:10px;width:auto;border:0;margin-top:3px}.cke_combo_label{display:none;float:left;line-height:26px;vertical-align:top;margin-right:5px}.cke_rtl .cke_combo_label{float:right;margin-left:5px;margin-right:0}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{opacity:.3}.cke_path{float:left;margin:-2px 0 2px}a.cke_path_item,span.cke_path_empty{display:inline-block;float:left;padding:3px 4px;margin-right:2px;cursor:default;text-decoration:none;outline:0;border:0;color:#484848;font-weight:bold;font-size:11px}.cke_rtl .cke_path,.cke_rtl .cke_path_item,.cke_rtl .cke_path_empty{float:right}a.cke_path_item:hover,a.cke_path_item:focus,a.cke_path_item:active{background-color:#e5e5e5}.cke_hc a.cke_path_item:hover,.cke_hc a.cke_path_item:focus,.cke_hc a.cke_path_item:active{border:2px solid;padding:1px 2px}.cke_button__source_label,.cke_button__sourcedialog_label{display:inline}.cke_combopanel__fontsize{width:135px}textarea.cke_source{font-family:'Courier New',Monospace;font-size:small;background-color:#fff;white-space:pre-wrap;border:0;padding:0;margin:0;display:block}.cke_wysiwyg_frame,.cke_wysiwyg_div{background-color:#fff}.cke_notifications_area{pointer-events:none}.cke_notification{pointer-events:auto;position:relative;margin:10px;width:300px;color:white;text-align:center;opacity:.95;filter:alpha(opacity = 95);-webkit-animation:fadeIn .7s;animation:fadeIn .7s}.cke_notification_message a{color:#12306f}@-webkit-keyframes fadeIn{from{opacity:.4}to{opacity:.95}}@keyframes fadeIn{from{opacity:.4}to{opacity:.95}}.cke_notification_success{background:#72b572;border:1px solid #63a563}.cke_notification_warning{background:#c83939;border:1px solid #902b2b}.cke_notification_info{background:#2e9ad0;border:1px solid #0f74a8}.cke_notification_info span.cke_notification_progress{background-color:#0f74a8;display:block;padding:0;margin:0;height:100%;overflow:hidden;position:absolute;z-index:1}.cke_notification_message{position:relative;margin:4px 23px 3px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:18px;z-index:4;text-overflow:ellipsis;overflow:hidden}.cke_notification_close{background-image:url(images/close.png);background-repeat:no-repeat;background-position:50%;position:absolute;cursor:pointer;text-align:center;height:20px;width:20px;top:1px;right:1px;padding:0;margin:0;z-index:5;opacity:.6;filter:alpha(opacity = 60)}.cke_notification_close:hover{opacity:1;filter:alpha(opacity = 100)}.cke_notification_close span{display:none}.cke_notification_warning a.cke_notification_close{opacity:.8;filter:alpha(opacity = 80)}.cke_notification_warning a.cke_notification_close:hover{opacity:1;filter:alpha(opacity = 100)}.cke_chrome{visibility:inherit}.cke_voice_label{display:none}legend.cke_voice_label{display:none}a.cke_button_disabled,a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{filter:alpha(opacity = 30)}.cke_button_disabled .cke_button_icon{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#00ffffff,endColorstr=#00ffffff)}.cke_button_off:hover,.cke_button_off:focus,.cke_button_off:active{filter:alpha(opacity = 100)}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{filter:alpha(opacity = 30)}.cke_toolbox_collapser{border:1px solid #a6a6a6}.cke_toolbox_collapser .cke_arrow{margin-top:1px}.cke_hc .cke_top,.cke_hc .cke_bottom,.cke_hc .cke_combo_button,.cke_hc a.cke_combo_button:hover,.cke_hc a.cke_combo_button:focus,.cke_hc .cke_toolgroup,.cke_hc .cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active,.cke_hc .cke_toolbox_collapser,.cke_hc .cke_toolbox_collapser:hover,.cke_hc .cke_panel_grouptitle{filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.cke_button__bold_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -0px !important;}.cke_button__italic_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -24px !important;}.cke_button__strike_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -48px !important;}.cke_button__subscript_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -72px !important;}.cke_button__superscript_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -96px !important;}.cke_button__underline_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -120px !important;}.cke_rtl .cke_button__copy_icon, .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -144px !important;}.cke_ltr .cke_button__copy_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -168px !important;}.cke_rtl .cke_button__cut_icon, .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -192px !important;}.cke_ltr .cke_button__cut_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -216px !important;}.cke_rtl .cke_button__paste_icon, .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -240px !important;}.cke_ltr .cke_button__paste_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -264px !important;}.cke_button__horizontalrule_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -288px !important;}.cke_rtl .cke_button__indent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -312px !important;}.cke_ltr .cke_button__indent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -336px !important;}.cke_rtl .cke_button__outdent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -360px !important;}.cke_ltr .cke_button__outdent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -384px !important;}.cke_rtl .cke_button__anchor_icon, .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -408px !important;}.cke_ltr .cke_button__anchor_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -432px !important;}.cke_button__link_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -456px !important;}.cke_button__unlink_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -480px !important;}.cke_rtl .cke_button__bulletedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -504px !important;}.cke_ltr .cke_button__bulletedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -528px !important;}.cke_rtl .cke_button__numberedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -552px !important;}.cke_ltr .cke_button__numberedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -576px !important;}.cke_button__removeformat_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -600px !important;}.cke_rtl .cke_button__sourcedialog_icon, .cke_mixed_dir_content .cke_rtl .cke_button__sourcedialog_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -624px !important;}.cke_ltr .cke_button__sourcedialog_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -648px !important;}.cke_hidpi .cke_button__bold_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -0px !important;background-size: 16px !important;}.cke_hidpi .cke_button__italic_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -24px !important;background-size: 16px !important;}.cke_hidpi .cke_button__strike_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -48px !important;background-size: 16px !important;}.cke_hidpi .cke_button__subscript_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -72px !important;background-size: 16px !important;}.cke_hidpi .cke_button__superscript_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -96px !important;background-size: 16px !important;}.cke_hidpi .cke_button__underline_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -120px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__copy_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -144px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__copy_icon,.cke_ltr.cke_hidpi .cke_button__copy_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -168px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__cut_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -192px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__cut_icon,.cke_ltr.cke_hidpi .cke_button__cut_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -216px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__paste_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -240px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__paste_icon,.cke_ltr.cke_hidpi .cke_button__paste_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -264px !important;background-size: 16px !important;}.cke_hidpi .cke_button__horizontalrule_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -288px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__indent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -312px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__indent_icon,.cke_ltr.cke_hidpi .cke_button__indent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -336px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__outdent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -360px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__outdent_icon,.cke_ltr.cke_hidpi .cke_button__outdent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -384px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__anchor_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -408px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__anchor_icon,.cke_ltr.cke_hidpi .cke_button__anchor_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -432px !important;background-size: 16px !important;}.cke_hidpi .cke_button__link_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -456px !important;background-size: 16px !important;}.cke_hidpi .cke_button__unlink_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -480px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__bulletedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -504px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__bulletedlist_icon,.cke_ltr.cke_hidpi .cke_button__bulletedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -528px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__numberedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -552px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__numberedlist_icon,.cke_ltr.cke_hidpi .cke_button__numberedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -576px !important;background-size: 16px !important;}.cke_hidpi .cke_button__removeformat_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -600px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__sourcedialog_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__sourcedialog_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -624px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__sourcedialog_icon,.cke_ltr.cke_hidpi .cke_button__sourcedialog_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -648px !important;background-size: 16px !important;}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono-lisa/editor_ie8.css b/js/ckeditor/skins/moono-lisa/editor_ie8.css
new file mode 100644 (file)
index 0000000..e4202f0
--- /dev/null
@@ -0,0 +1,5 @@
+/*\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+*/\r
+.cke_reset{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;position:static;transition:none}.cke_reset_all,.cke_reset_all *,.cke_reset_all a,.cke_reset_all textarea{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;position:static;transition:none;border-collapse:collapse;font:normal normal normal 12px Arial,Helvetica,Tahoma,Verdana,Sans-Serif;color:#000;text-align:left;white-space:nowrap;cursor:auto;float:none}.cke_reset_all .cke_rtl *{text-align:right}.cke_reset_all iframe{vertical-align:inherit}.cke_reset_all textarea{white-space:pre-wrap}.cke_reset_all textarea,.cke_reset_all input[type="text"],.cke_reset_all input[type="password"]{cursor:text}.cke_reset_all textarea[disabled],.cke_reset_all input[type="text"][disabled],.cke_reset_all input[type="password"][disabled]{cursor:default}.cke_reset_all fieldset{padding:10px;border:2px groove #e0dfe3}.cke_reset_all select{box-sizing:border-box}.cke_reset_all table{table-layout:auto}.cke_chrome{display:block;border:1px solid #d1d1d1;padding:0}.cke_inner{display:block;background:#fff;padding:0;-webkit-touch-callout:none}.cke_float{border:0}.cke_float .cke_inner{padding-bottom:0}.cke_top,.cke_contents,.cke_bottom{display:block;overflow:hidden}.cke_top{border-bottom:1px solid #d1d1d1;background:#f8f8f8;padding:6px 8px 2px;white-space:normal}.cke_float .cke_top{border:1px solid #d1d1d1}.cke_bottom{padding:6px 8px 2px;position:relative;border-top:1px solid #d1d1d1;background:#f8f8f8}.cke_browser_ios .cke_contents{overflow-y:auto;-webkit-overflow-scrolling:touch}.cke_resizer{width:0;height:0;overflow:hidden;border-width:10px 10px 0 0;border-color:transparent #bcbcbc transparent transparent;border-style:dashed solid dashed dashed;font-size:0;vertical-align:bottom;margin-top:6px;margin-bottom:2px}.cke_hc .cke_resizer{font-size:15px;width:auto;height:auto;border-width:0}.cke_resizer_ltr{cursor:se-resize;float:right;margin-right:-4px}.cke_resizer_rtl{border-width:10px 0 0 10px;border-color:transparent transparent transparent #bcbcbc;border-style:dashed dashed dashed solid;cursor:sw-resize;float:left;margin-left:-4px;right:auto}.cke_wysiwyg_div{display:block;height:100%;overflow:auto;padding:0 8px;outline-style:none;box-sizing:border-box}.cke_panel{visibility:visible;width:120px;height:100px;overflow:hidden;background-color:#fff;border:1px solid #d1d1d1}.cke_menu_panel{padding:0;margin:0}.cke_combopanel{width:150px;height:170px}.cke_panel_frame{width:100%;height:100%;font-size:12px;overflow:auto;overflow-x:hidden}.cke_panel_container{overflow-y:auto;overflow-x:hidden}.cke_panel_block:focus{outline:0}.cke_panel_list{margin:0;padding:0;list-style-type:none;white-space:nowrap}.cke_panel_listItem{margin:0;padding:0}.cke_panel_listItem a{padding:6px 7px;display:block;color:inherit!important;text-decoration:none;overflow:hidden;text-overflow:ellipsis}.cke_hc .cke_panel_listItem a{border-style:none}.cke_panel_listItem.cke_selected a,.cke_panel_listItem a:hover,.cke_panel_listItem a:focus,.cke_panel_listItem a:active{background-color:#e9e9e9}.cke_panel_listItem a:focus{outline:1px dotted #000}.cke_hc .cke_panel_listItem a:hover,.cke_hc .cke_panel_listItem a:focus,.cke_hc .cke_panel_listItem a:active{border:2px solid;padding:4px 5px}.cke_panel_listItem p,.cke_panel_listItem h1,.cke_panel_listItem h2,.cke_panel_listItem h3,.cke_panel_listItem h4,.cke_panel_listItem h5,.cke_panel_listItem h6,.cke_panel_listItem pre{margin-top:0;margin-bottom:0}.cke_panel_grouptitle{cursor:default;font-size:11px;font-weight:bold;white-space:nowrap;margin:0;padding:6px 6px 7px 6px;color:#484848;border-bottom:1px solid #d1d1d1;background:#f8f8f8}.cke_colorblock{padding:10px;font-size:11px;font-family:'Microsoft Sans Serif',Tahoma,Arial,Verdana,Sans-Serif}.cke_colorblock,.cke_colorblock a{text-decoration:none;color:#000}a.cke_colorbox{padding:2px;float:left;width:20px;height:20px}.cke_rtl a.cke_colorbox{float:right}a:hover.cke_colorbox,a:focus.cke_colorbox,a:active.cke_colorbox{outline:0;padding:0;border:2px solid #139ff7}a:hover.cke_colorbox{border-color:#bcbcbc}span.cke_colorbox{width:20px;height:20px;float:left}.cke_rtl span.cke_colorbox{float:right}a.cke_colorauto,a.cke_colormore{border:#fff 1px solid;padding:3px;display:block;cursor:pointer}a.cke_colorauto{padding:0;border:1px solid transparent;margin-bottom:6px;height:26px;line-height:26px}a.cke_colormore{margin-top:10px;height:20px;line-height:19px}a:hover.cke_colorauto,a:hover.cke_colormore,a:focus.cke_colorauto,a:focus.cke_colormore,a:active.cke_colorauto,a:active.cke_colormore{outline:0;border:#139ff7 1px solid;background-color:#f8f8f8}a:hover.cke_colorauto,a:hover.cke_colormore{border-color:#bcbcbc}.cke_colorauto span.cke_colorbox{width:18px;height:18px;border:1px solid #808080;margin-left:1px;margin-top:3px}.cke_rtl .cke_colorauto span.cke_colorbox{margin-left:0;margin-right:1px}span.cke_colorbox[style*="#ffffff"],span.cke_colorbox[style*="#FFFFFF"],span.cke_colorbox[style="background-color:#fff"],span.cke_colorbox[style="background-color:#FFF"],span.cke_colorbox[style*="rgb(255,255,255)"],span.cke_colorbox[style*="rgb(255, 255, 255)"]{border:1px solid #808080;width:18px;height:18px}.cke_toolbar{float:left}.cke_rtl .cke_toolbar{float:right}.cke_toolgroup{border:0;float:left;margin:1px 2px 6px 0;padding-right:3px}.cke_rtl .cke_toolgroup{float:right;margin:1px 0 6px 2px;padding-left:3px;padding-right:0}.cke_hc .cke_toolgroup{margin-right:5px;margin-bottom:5px}.cke_hc.cke_rtl .cke_toolgroup{margin-right:0;margin-left:5px}a.cke_button{display:inline-block;height:18px;padding:4px 6px;outline:0;cursor:default;float:left;border:0;position:relative}.cke_rtl a.cke_button{float:right}.cke_hc a.cke_button{border:1px solid black;padding:3px 5px;margin:0 3px 5px 0}.cke_hc.cke_rtl a.cke_button{margin:0 0 5px 3px}a.cke_button_on{background:#fff;border:1px #bcbcbc solid;padding:3px 5px}a.cke_button_off:hover,a.cke_button_off:focus,a.cke_button_off:active{background:#e5e5e5;border:1px #bcbcbc solid;padding:3px 5px}.cke_hc a.cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active{background:#e5e5e5;border:3px solid #000;padding:1px 3px}a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{border:0;padding:4px 6px;background-color:transparent}a.cke_button_disabled:focus{border:1px #bcbcbc solid;padding:3px 5px}.cke_hc a.cke_button_disabled:hover,.cke_hc a.cke_button_disabled:focus,.cke_hc a.cke_button_disabled:active{border:1px solid #acacac;padding:3px 5px;margin:0 3px 5px 0}.cke_hc a.cke_button_disabled:focus{border:3px solid #000;padding:1px 3px}.cke_hc.cke_rtl a.cke_button_disabled:hover,.cke_hc.cke_rtl a.cke_button_disabled:focus,.cke_hc.cke_rtl a.cke_button_disabled:active{margin:0 0 5px 3px}a.cke_button_disabled .cke_button_icon,a.cke_button_disabled .cke_button_arrow{opacity:.3}.cke_hc a.cke_button_disabled{border-color:#acacac}.cke_hc a.cke_button_disabled .cke_button_icon,.cke_hc a.cke_button_disabled .cke_button_label{opacity:.5}.cke_toolgroup a.cke_button:last-child:after,.cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{content:"";position:absolute;height:18px;width:0;border-right:1px solid #bcbcbc;margin-top:4px;top:0;right:-3px}.cke_rtl .cke_toolgroup a.cke_button:last-child:after,.cke_rtl .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{border-right:0;right:auto;border-left:1px solid #bcbcbc;top:0;left:-3px}.cke_hc .cke_toolgroup a.cke_button:last-child:after,.cke_hc .cke_toolgroup a.cke_button.cke_button_disabled:last-child:after,.cke_hc .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{border-color:#000;top:0;right:-7px}.cke_hc.cke_rtl .cke_toolgroup a.cke_button:last-child:after,.cke_hc.cke_rtl .cke_toolgroup a.cke_button.cke_button_disabled:last-child:after,.cke_hc.cke_rtl .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{top:0;right:auto;left:-7px}.cke_toolgroup a.cke_button:hover:last-child:after,.cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-1px;right:-4px}.cke_rtl .cke_toolgroup a.cke_button:hover:last-child:after,.cke_rtl .cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-1px;right:auto;left:-4px}.cke_hc .cke_toolgroup a.cke_button:hover:last-child:after,.cke_hc .cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-2px;right:-9px}.cke_hc.cke_rtl .cke_toolgroup a.cke_button:hover:last-child:after,.cke_hc.cke_rtl .cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-2px;right:auto;left:-9px}.cke_toolbar.cke_toolbar_last .cke_toolgroup a.cke_button:last-child:after{content:none;border:0;width:0;height:0}.cke_button_icon{cursor:inherit;background-repeat:no-repeat;margin-top:1px;width:16px;height:16px;float:left;display:inline-block}.cke_rtl .cke_button_icon{float:right}.cke_hc .cke_button_icon{display:none}.cke_button_label{display:none;padding-left:3px;margin-top:1px;line-height:17px;vertical-align:middle;float:left;cursor:default;color:#484848}.cke_rtl .cke_button_label{padding-right:3px;padding-left:0;float:right}.cke_hc .cke_button_label{padding:0;display:inline-block;font-size:12px}.cke_button_arrow{display:inline-block;margin:8px 0 0 1px;width:0;height:0;cursor:default;vertical-align:top;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #484848}.cke_rtl .cke_button_arrow{margin-right:5px;margin-left:0}.cke_hc .cke_button_arrow{font-size:10px;margin:3px 0 0 3px;width:auto;border:0}.cke_toolbar_separator{float:left;background-color:#bcbcbc;margin:4px 2px 0 2px;height:18px;width:1px}.cke_rtl .cke_toolbar_separator{float:right}.cke_hc .cke_toolbar_separator{background-color:#000;margin-left:2px;margin-right:5px;margin-bottom:9px}.cke_hc.cke_rtl .cke_toolbar_separator{margin-left:5px;margin-right:2px}.cke_toolbar_break{display:block;clear:left}.cke_rtl .cke_toolbar_break{clear:right}a.cke_toolbox_collapser{width:12px;height:11px;float:right;margin:11px 0 0;font-size:0;cursor:default;text-align:center;border:1px solid #bcbcbc}.cke_rtl .cke_toolbox_collapser{float:left}.cke_toolbox_collapser:hover{background:#e5e5e5}.cke_toolbox_collapser.cke_toolbox_collapser_min{margin:0 2px 4px}.cke_toolbox_collapser .cke_arrow{display:inline-block;height:0;width:0;font-size:0;margin-top:1px;border:3px solid transparent;border-bottom-color:#484848}.cke_toolbox_collapser.cke_toolbox_collapser_min .cke_arrow{margin-top:4px;border-bottom-color:transparent;border-top-color:#484848}.cke_hc .cke_toolbox_collapser .cke_arrow{font-size:8px;width:auto;border:0;margin-top:0}.cke_menuitem span{cursor:default}.cke_menubutton{display:block}.cke_hc .cke_menubutton{padding:2px}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#e9e9e9;display:block;outline:1px dotted}.cke_menubutton:hover{outline:0}.cke_hc .cke_menubutton:hover,.cke_hc .cke_menubutton:focus,.cke_hc .cke_menubutton:active{border:2px solid;padding:0}.cke_menubutton_disabled:hover,.cke_menubutton_disabled:focus,.cke_menubutton_disabled:active{background-color:transparent;outline:0}.cke_menubutton_inner{display:table-row}.cke_menubutton_icon,.cke_menubutton_label,.cke_menuarrow{display:table-cell}.cke_menubutton_icon{background-color:#f8f8f8;padding:6px 4px}.cke_hc .cke_menubutton_icon{height:16px;width:0;padding:4px 0}.cke_menubutton:hover .cke_menubutton_icon,.cke_menubutton:focus .cke_menubutton_icon,.cke_menubutton:active .cke_menubutton_icon{background-color:#e9e9e9}.cke_menubutton_disabled:hover .cke_menubutton_icon,.cke_menubutton_disabled:focus .cke_menubutton_icon,.cke_menubutton_disabled:active .cke_menubutton_icon{background-color:#f8f8f8;outline:0}.cke_menuitem .cke_menubutton_on{background-color:#e9e9e9;border:1px solid #dedede;outline:0}.cke_menubutton_on .cke_menubutton_icon{padding-right:3px;background-color:#e9e9e9}.cke_menubutton_label{padding:0 5px;background-color:transparent;width:100%;vertical-align:middle}.cke_menubutton_shortcut{color:#979797}.cke_menubutton_disabled .cke_menubutton_label{opacity:.3;filter:alpha(opacity=30)}.cke_panel_frame .cke_menubutton_label{display:none}.cke_menuseparator{background-color:#d1d1d1;height:1px}.cke_menuarrow{background:transparent url(images/arrow.png) no-repeat 0 10px;padding:0 5px}.cke_rtl .cke_menuarrow{background-position:5px -13px;background-repeat:no-repeat}.cke_hc .cke_menuarrow{background-image:none}.cke_menuarrow span{display:none}.cke_hc .cke_menuarrow span{vertical-align:middle;display:inline}.cke_combo{display:inline-block;float:left;position:relative;margin-bottom:5px}.cke_rtl .cke_combo{float:right}.cke_hc .cke_combo{margin-top:1px;margin-bottom:10px}.cke_combo:after{content:"";position:absolute;height:18px;width:0;border-right:1px solid #bcbcbc;margin-top:5px;top:0;right:0}.cke_rtl .cke_combo:after{border-right:0;border-left:1px solid #bcbcbc;right:auto;left:0}.cke_hc .cke_combo:after{border-color:#000}a.cke_combo_button{cursor:default;display:inline-block;float:left;margin:0;padding:1px}.cke_rtl a.cke_combo_button{float:right}.cke_hc a.cke_combo_button{padding:4px}.cke_combo_on a.cke_combo_button,.cke_combo_off a.cke_combo_button:hover,.cke_combo_off a.cke_combo_button:focus,.cke_combo_off a.cke_combo_button:active{background:#e5e5e5;border:1px solid #bcbcbc;padding:0 0 0 1px;margin-left:-1px}.cke_combo_off a.cke_combo_button:focus{outline:0}.cke_combo_on a.cke_combo_button,.cke_combo_off a.cke_combo_button:active{background:#fff}.cke_rtl .cke_combo_on a.cke_combo_button,.cke_rtl .cke_combo_off a.cke_combo_button:hover,.cke_rtl .cke_combo_off a.cke_combo_button:focus,.cke_rtl .cke_combo_off a.cke_combo_button:active{padding:0 1px 0 0;margin-left:0;margin-right:-1px}.cke_hc .cke_combo_on a.cke_combo_button,.cke_hc .cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_combo_off a.cke_combo_button:active{border:3px solid #000;padding:1px 1px 1px 2px}.cke_hc.cke_rtl .cke_combo_on a.cke_combo_button,.cke_hc.cke_rtl .cke_combo_off a.cke_combo_button:hover,.cke_hc.cke_rtl .cke_combo_off a.cke_combo_button:focus,.cke_hc.cke_rtl .cke_combo_off a.cke_combo_button:active{padding:1px 2px 1px 1px}.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:0 0 0 3px;margin-left:-3px}.cke_rtl .cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_rtl .cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_rtl .cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_rtl .cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:0 3px 0 0;margin-left:0;margin-right:-3px}.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:1px 1px 1px 7px;margin-left:-6px}.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:1px 7px 1px 1px;margin-left:0;margin-right:-6px}.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:0;margin:0}.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:1px;margin:0}.cke_toolbar .cke_combo+.cke_toolbar_end,.cke_toolbar .cke_combo+.cke_toolgroup{margin-right:0;margin-left:2px}.cke_rtl .cke_toolbar .cke_combo+.cke_toolbar_end,.cke_rtl .cke_toolbar .cke_combo+.cke_toolgroup{margin-left:0;margin-right:2px}.cke_hc .cke_toolbar .cke_combo+.cke_toolbar_end,.cke_hc .cke_toolbar .cke_combo+.cke_toolgroup{margin-left:5px}.cke_hc.cke_rtl .cke_toolbar .cke_combo+.cke_toolbar_end,.cke_hc.cke_rtl .cke_toolbar .cke_combo+.cke_toolgroup{margin-left:0;margin-right:5px}.cke_toolbar.cke_toolbar_last .cke_combo:nth-last-child(-n+2):after{content:none;border:0;width:0;height:0}.cke_combo_text{line-height:26px;padding-left:10px;text-overflow:ellipsis;overflow:hidden;float:left;cursor:default;color:#484848;width:60px}.cke_rtl .cke_combo_text{float:right;text-align:right;padding-left:0;padding-right:10px}.cke_hc .cke_combo_text{line-height:18px;font-size:12px}.cke_combo_open{cursor:default;display:inline-block;font-size:0;height:19px;line-height:17px;margin:1px 10px 1px;width:5px}.cke_hc .cke_combo_open{height:12px}.cke_combo_arrow{cursor:default;margin:11px 0 0;float:left;height:0;width:0;font-size:0;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #484848}.cke_hc .cke_combo_arrow{font-size:10px;width:auto;border:0;margin-top:3px}.cke_combo_label{display:none;float:left;line-height:26px;vertical-align:top;margin-right:5px}.cke_rtl .cke_combo_label{float:right;margin-left:5px;margin-right:0}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{opacity:.3}.cke_path{float:left;margin:-2px 0 2px}a.cke_path_item,span.cke_path_empty{display:inline-block;float:left;padding:3px 4px;margin-right:2px;cursor:default;text-decoration:none;outline:0;border:0;color:#484848;font-weight:bold;font-size:11px}.cke_rtl .cke_path,.cke_rtl .cke_path_item,.cke_rtl .cke_path_empty{float:right}a.cke_path_item:hover,a.cke_path_item:focus,a.cke_path_item:active{background-color:#e5e5e5}.cke_hc a.cke_path_item:hover,.cke_hc a.cke_path_item:focus,.cke_hc a.cke_path_item:active{border:2px solid;padding:1px 2px}.cke_button__source_label,.cke_button__sourcedialog_label{display:inline}.cke_combopanel__fontsize{width:135px}textarea.cke_source{font-family:'Courier New',Monospace;font-size:small;background-color:#fff;white-space:pre-wrap;border:0;padding:0;margin:0;display:block}.cke_wysiwyg_frame,.cke_wysiwyg_div{background-color:#fff}.cke_notifications_area{pointer-events:none}.cke_notification{pointer-events:auto;position:relative;margin:10px;width:300px;color:white;text-align:center;opacity:.95;filter:alpha(opacity = 95);-webkit-animation:fadeIn .7s;animation:fadeIn .7s}.cke_notification_message a{color:#12306f}@-webkit-keyframes fadeIn{from{opacity:.4}to{opacity:.95}}@keyframes fadeIn{from{opacity:.4}to{opacity:.95}}.cke_notification_success{background:#72b572;border:1px solid #63a563}.cke_notification_warning{background:#c83939;border:1px solid #902b2b}.cke_notification_info{background:#2e9ad0;border:1px solid #0f74a8}.cke_notification_info span.cke_notification_progress{background-color:#0f74a8;display:block;padding:0;margin:0;height:100%;overflow:hidden;position:absolute;z-index:1}.cke_notification_message{position:relative;margin:4px 23px 3px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:18px;z-index:4;text-overflow:ellipsis;overflow:hidden}.cke_notification_close{background-image:url(images/close.png);background-repeat:no-repeat;background-position:50%;position:absolute;cursor:pointer;text-align:center;height:20px;width:20px;top:1px;right:1px;padding:0;margin:0;z-index:5;opacity:.6;filter:alpha(opacity = 60)}.cke_notification_close:hover{opacity:1;filter:alpha(opacity = 100)}.cke_notification_close span{display:none}.cke_notification_warning a.cke_notification_close{opacity:.8;filter:alpha(opacity = 80)}.cke_notification_warning a.cke_notification_close:hover{opacity:1;filter:alpha(opacity = 100)}.cke_chrome{visibility:inherit}.cke_voice_label{display:none}legend.cke_voice_label{display:none}a.cke_button_disabled,a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{filter:alpha(opacity = 30)}.cke_button_disabled .cke_button_icon{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#00ffffff,endColorstr=#00ffffff)}.cke_button_off:hover,.cke_button_off:focus,.cke_button_off:active{filter:alpha(opacity = 100)}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{filter:alpha(opacity = 30)}.cke_toolbox_collapser{border:1px solid #a6a6a6}.cke_toolbox_collapser .cke_arrow{margin-top:1px}.cke_hc .cke_top,.cke_hc .cke_bottom,.cke_hc .cke_combo_button,.cke_hc a.cke_combo_button:hover,.cke_hc a.cke_combo_button:focus,.cke_hc .cke_toolgroup,.cke_hc .cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active,.cke_hc .cke_toolbox_collapser,.cke_hc .cke_toolbox_collapser:hover,.cke_hc .cke_panel_grouptitle{filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.cke_toolbox_collapser .cke_arrow{border-width:4px}.cke_toolbox_collapser.cke_toolbox_collapser_min .cke_arrow{border-width:3px}.cke_toolbox_collapser .cke_arrow{margin-top:0}.cke_toolbar{position:relative}.cke_rtl .cke_toolbar_end{right:auto;left:0}.cke_toolbar_end:after{content:"";position:absolute;height:18px;width:0;border-right:1px solid #bcbcbc;margin-top:4px;top:1px;right:2px}.cke_rtl .cke_toolbar_end:after{right:auto;left:2px}.cke_hc .cke_toolbar_end:after{top:2px;right:5px;border-color:#000}.cke_hc.cke_rtl .cke_toolbar_end:after{right:auto;left:5px}.cke_combo+.cke_toolbar_end:after,.cke_toolbar.cke_toolbar_last .cke_toolbar_end:after{content:none;border:0}.cke_combo+.cke_toolgroup+.cke_toolbar_end:after{right:0}.cke_rtl .cke_combo+.cke_toolgroup+.cke_toolbar_end:after{right:auto;left:0}.cke_button__bold_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -0px !important;}.cke_button__italic_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -24px !important;}.cke_button__strike_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -48px !important;}.cke_button__subscript_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -72px !important;}.cke_button__superscript_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -96px !important;}.cke_button__underline_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -120px !important;}.cke_rtl .cke_button__copy_icon, .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -144px !important;}.cke_ltr .cke_button__copy_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -168px !important;}.cke_rtl .cke_button__cut_icon, .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -192px !important;}.cke_ltr .cke_button__cut_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -216px !important;}.cke_rtl .cke_button__paste_icon, .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -240px !important;}.cke_ltr .cke_button__paste_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -264px !important;}.cke_button__horizontalrule_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -288px !important;}.cke_rtl .cke_button__indent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -312px !important;}.cke_ltr .cke_button__indent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -336px !important;}.cke_rtl .cke_button__outdent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -360px !important;}.cke_ltr .cke_button__outdent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -384px !important;}.cke_rtl .cke_button__anchor_icon, .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -408px !important;}.cke_ltr .cke_button__anchor_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -432px !important;}.cke_button__link_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -456px !important;}.cke_button__unlink_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -480px !important;}.cke_rtl .cke_button__bulletedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -504px !important;}.cke_ltr .cke_button__bulletedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -528px !important;}.cke_rtl .cke_button__numberedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -552px !important;}.cke_ltr .cke_button__numberedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -576px !important;}.cke_button__removeformat_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -600px !important;}.cke_rtl .cke_button__sourcedialog_icon, .cke_mixed_dir_content .cke_rtl .cke_button__sourcedialog_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -624px !important;}.cke_ltr .cke_button__sourcedialog_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -648px !important;}.cke_hidpi .cke_button__bold_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -0px !important;background-size: 16px !important;}.cke_hidpi .cke_button__italic_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -24px !important;background-size: 16px !important;}.cke_hidpi .cke_button__strike_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -48px !important;background-size: 16px !important;}.cke_hidpi .cke_button__subscript_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -72px !important;background-size: 16px !important;}.cke_hidpi .cke_button__superscript_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -96px !important;background-size: 16px !important;}.cke_hidpi .cke_button__underline_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -120px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__copy_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -144px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__copy_icon,.cke_ltr.cke_hidpi .cke_button__copy_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -168px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__cut_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -192px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__cut_icon,.cke_ltr.cke_hidpi .cke_button__cut_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -216px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__paste_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -240px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__paste_icon,.cke_ltr.cke_hidpi .cke_button__paste_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -264px !important;background-size: 16px !important;}.cke_hidpi .cke_button__horizontalrule_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -288px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__indent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -312px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__indent_icon,.cke_ltr.cke_hidpi .cke_button__indent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -336px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__outdent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -360px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__outdent_icon,.cke_ltr.cke_hidpi .cke_button__outdent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -384px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__anchor_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -408px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__anchor_icon,.cke_ltr.cke_hidpi .cke_button__anchor_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -432px !important;background-size: 16px !important;}.cke_hidpi .cke_button__link_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -456px !important;background-size: 16px !important;}.cke_hidpi .cke_button__unlink_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -480px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__bulletedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -504px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__bulletedlist_icon,.cke_ltr.cke_hidpi .cke_button__bulletedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -528px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__numberedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -552px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__numberedlist_icon,.cke_ltr.cke_hidpi .cke_button__numberedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -576px !important;background-size: 16px !important;}.cke_hidpi .cke_button__removeformat_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -600px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__sourcedialog_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__sourcedialog_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -624px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__sourcedialog_icon,.cke_ltr.cke_hidpi .cke_button__sourcedialog_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -648px !important;background-size: 16px !important;}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono-lisa/editor_iequirks.css b/js/ckeditor/skins/moono-lisa/editor_iequirks.css
new file mode 100644 (file)
index 0000000..d3bce52
--- /dev/null
@@ -0,0 +1,5 @@
+/*\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.md or http://ckeditor.com/license\r
+*/\r
+.cke_reset{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;position:static;transition:none}.cke_reset_all,.cke_reset_all *,.cke_reset_all a,.cke_reset_all textarea{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;position:static;transition:none;border-collapse:collapse;font:normal normal normal 12px Arial,Helvetica,Tahoma,Verdana,Sans-Serif;color:#000;text-align:left;white-space:nowrap;cursor:auto;float:none}.cke_reset_all .cke_rtl *{text-align:right}.cke_reset_all iframe{vertical-align:inherit}.cke_reset_all textarea{white-space:pre-wrap}.cke_reset_all textarea,.cke_reset_all input[type="text"],.cke_reset_all input[type="password"]{cursor:text}.cke_reset_all textarea[disabled],.cke_reset_all input[type="text"][disabled],.cke_reset_all input[type="password"][disabled]{cursor:default}.cke_reset_all fieldset{padding:10px;border:2px groove #e0dfe3}.cke_reset_all select{box-sizing:border-box}.cke_reset_all table{table-layout:auto}.cke_chrome{display:block;border:1px solid #d1d1d1;padding:0}.cke_inner{display:block;background:#fff;padding:0;-webkit-touch-callout:none}.cke_float{border:0}.cke_float .cke_inner{padding-bottom:0}.cke_top,.cke_contents,.cke_bottom{display:block;overflow:hidden}.cke_top{border-bottom:1px solid #d1d1d1;background:#f8f8f8;padding:6px 8px 2px;white-space:normal}.cke_float .cke_top{border:1px solid #d1d1d1}.cke_bottom{padding:6px 8px 2px;position:relative;border-top:1px solid #d1d1d1;background:#f8f8f8}.cke_browser_ios .cke_contents{overflow-y:auto;-webkit-overflow-scrolling:touch}.cke_resizer{width:0;height:0;overflow:hidden;border-width:10px 10px 0 0;border-color:transparent #bcbcbc transparent transparent;border-style:dashed solid dashed dashed;font-size:0;vertical-align:bottom;margin-top:6px;margin-bottom:2px}.cke_hc .cke_resizer{font-size:15px;width:auto;height:auto;border-width:0}.cke_resizer_ltr{cursor:se-resize;float:right;margin-right:-4px}.cke_resizer_rtl{border-width:10px 0 0 10px;border-color:transparent transparent transparent #bcbcbc;border-style:dashed dashed dashed solid;cursor:sw-resize;float:left;margin-left:-4px;right:auto}.cke_wysiwyg_div{display:block;height:100%;overflow:auto;padding:0 8px;outline-style:none;box-sizing:border-box}.cke_panel{visibility:visible;width:120px;height:100px;overflow:hidden;background-color:#fff;border:1px solid #d1d1d1}.cke_menu_panel{padding:0;margin:0}.cke_combopanel{width:150px;height:170px}.cke_panel_frame{width:100%;height:100%;font-size:12px;overflow:auto;overflow-x:hidden}.cke_panel_container{overflow-y:auto;overflow-x:hidden}.cke_panel_block:focus{outline:0}.cke_panel_list{margin:0;padding:0;list-style-type:none;white-space:nowrap}.cke_panel_listItem{margin:0;padding:0}.cke_panel_listItem a{padding:6px 7px;display:block;color:inherit!important;text-decoration:none;overflow:hidden;text-overflow:ellipsis}.cke_hc .cke_panel_listItem a{border-style:none}.cke_panel_listItem.cke_selected a,.cke_panel_listItem a:hover,.cke_panel_listItem a:focus,.cke_panel_listItem a:active{background-color:#e9e9e9}.cke_panel_listItem a:focus{outline:1px dotted #000}.cke_hc .cke_panel_listItem a:hover,.cke_hc .cke_panel_listItem a:focus,.cke_hc .cke_panel_listItem a:active{border:2px solid;padding:4px 5px}.cke_panel_listItem p,.cke_panel_listItem h1,.cke_panel_listItem h2,.cke_panel_listItem h3,.cke_panel_listItem h4,.cke_panel_listItem h5,.cke_panel_listItem h6,.cke_panel_listItem pre{margin-top:0;margin-bottom:0}.cke_panel_grouptitle{cursor:default;font-size:11px;font-weight:bold;white-space:nowrap;margin:0;padding:6px 6px 7px 6px;color:#484848;border-bottom:1px solid #d1d1d1;background:#f8f8f8}.cke_colorblock{padding:10px;font-size:11px;font-family:'Microsoft Sans Serif',Tahoma,Arial,Verdana,Sans-Serif}.cke_colorblock,.cke_colorblock a{text-decoration:none;color:#000}a.cke_colorbox{padding:2px;float:left;width:20px;height:20px}.cke_rtl a.cke_colorbox{float:right}a:hover.cke_colorbox,a:focus.cke_colorbox,a:active.cke_colorbox{outline:0;padding:0;border:2px solid #139ff7}a:hover.cke_colorbox{border-color:#bcbcbc}span.cke_colorbox{width:20px;height:20px;float:left}.cke_rtl span.cke_colorbox{float:right}a.cke_colorauto,a.cke_colormore{border:#fff 1px solid;padding:3px;display:block;cursor:pointer}a.cke_colorauto{padding:0;border:1px solid transparent;margin-bottom:6px;height:26px;line-height:26px}a.cke_colormore{margin-top:10px;height:20px;line-height:19px}a:hover.cke_colorauto,a:hover.cke_colormore,a:focus.cke_colorauto,a:focus.cke_colormore,a:active.cke_colorauto,a:active.cke_colormore{outline:0;border:#139ff7 1px solid;background-color:#f8f8f8}a:hover.cke_colorauto,a:hover.cke_colormore{border-color:#bcbcbc}.cke_colorauto span.cke_colorbox{width:18px;height:18px;border:1px solid #808080;margin-left:1px;margin-top:3px}.cke_rtl .cke_colorauto span.cke_colorbox{margin-left:0;margin-right:1px}span.cke_colorbox[style*="#ffffff"],span.cke_colorbox[style*="#FFFFFF"],span.cke_colorbox[style="background-color:#fff"],span.cke_colorbox[style="background-color:#FFF"],span.cke_colorbox[style*="rgb(255,255,255)"],span.cke_colorbox[style*="rgb(255, 255, 255)"]{border:1px solid #808080;width:18px;height:18px}.cke_toolbar{float:left}.cke_rtl .cke_toolbar{float:right}.cke_toolgroup{border:0;float:left;margin:1px 2px 6px 0;padding-right:3px}.cke_rtl .cke_toolgroup{float:right;margin:1px 0 6px 2px;padding-left:3px;padding-right:0}.cke_hc .cke_toolgroup{margin-right:5px;margin-bottom:5px}.cke_hc.cke_rtl .cke_toolgroup{margin-right:0;margin-left:5px}a.cke_button{display:inline-block;height:18px;padding:4px 6px;outline:0;cursor:default;float:left;border:0;position:relative}.cke_rtl a.cke_button{float:right}.cke_hc a.cke_button{border:1px solid black;padding:3px 5px;margin:0 3px 5px 0}.cke_hc.cke_rtl a.cke_button{margin:0 0 5px 3px}a.cke_button_on{background:#fff;border:1px #bcbcbc solid;padding:3px 5px}a.cke_button_off:hover,a.cke_button_off:focus,a.cke_button_off:active{background:#e5e5e5;border:1px #bcbcbc solid;padding:3px 5px}.cke_hc a.cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active{background:#e5e5e5;border:3px solid #000;padding:1px 3px}a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{border:0;padding:4px 6px;background-color:transparent}a.cke_button_disabled:focus{border:1px #bcbcbc solid;padding:3px 5px}.cke_hc a.cke_button_disabled:hover,.cke_hc a.cke_button_disabled:focus,.cke_hc a.cke_button_disabled:active{border:1px solid #acacac;padding:3px 5px;margin:0 3px 5px 0}.cke_hc a.cke_button_disabled:focus{border:3px solid #000;padding:1px 3px}.cke_hc.cke_rtl a.cke_button_disabled:hover,.cke_hc.cke_rtl a.cke_button_disabled:focus,.cke_hc.cke_rtl a.cke_button_disabled:active{margin:0 0 5px 3px}a.cke_button_disabled .cke_button_icon,a.cke_button_disabled .cke_button_arrow{opacity:.3}.cke_hc a.cke_button_disabled{border-color:#acacac}.cke_hc a.cke_button_disabled .cke_button_icon,.cke_hc a.cke_button_disabled .cke_button_label{opacity:.5}.cke_toolgroup a.cke_button:last-child:after,.cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{content:"";position:absolute;height:18px;width:0;border-right:1px solid #bcbcbc;margin-top:4px;top:0;right:-3px}.cke_rtl .cke_toolgroup a.cke_button:last-child:after,.cke_rtl .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{border-right:0;right:auto;border-left:1px solid #bcbcbc;top:0;left:-3px}.cke_hc .cke_toolgroup a.cke_button:last-child:after,.cke_hc .cke_toolgroup a.cke_button.cke_button_disabled:last-child:after,.cke_hc .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{border-color:#000;top:0;right:-7px}.cke_hc.cke_rtl .cke_toolgroup a.cke_button:last-child:after,.cke_hc.cke_rtl .cke_toolgroup a.cke_button.cke_button_disabled:last-child:after,.cke_hc.cke_rtl .cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after{top:0;right:auto;left:-7px}.cke_toolgroup a.cke_button:hover:last-child:after,.cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-1px;right:-4px}.cke_rtl .cke_toolgroup a.cke_button:hover:last-child:after,.cke_rtl .cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-1px;right:auto;left:-4px}.cke_hc .cke_toolgroup a.cke_button:hover:last-child:after,.cke_hc .cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-2px;right:-9px}.cke_hc.cke_rtl .cke_toolgroup a.cke_button:hover:last-child:after,.cke_hc.cke_rtl .cke_toolgroup a.cke_button.cke_button_on:last-child:after{top:-2px;right:auto;left:-9px}.cke_toolbar.cke_toolbar_last .cke_toolgroup a.cke_button:last-child:after{content:none;border:0;width:0;height:0}.cke_button_icon{cursor:inherit;background-repeat:no-repeat;margin-top:1px;width:16px;height:16px;float:left;display:inline-block}.cke_rtl .cke_button_icon{float:right}.cke_hc .cke_button_icon{display:none}.cke_button_label{display:none;padding-left:3px;margin-top:1px;line-height:17px;vertical-align:middle;float:left;cursor:default;color:#484848}.cke_rtl .cke_button_label{padding-right:3px;padding-left:0;float:right}.cke_hc .cke_button_label{padding:0;display:inline-block;font-size:12px}.cke_button_arrow{display:inline-block;margin:8px 0 0 1px;width:0;height:0;cursor:default;vertical-align:top;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #484848}.cke_rtl .cke_button_arrow{margin-right:5px;margin-left:0}.cke_hc .cke_button_arrow{font-size:10px;margin:3px 0 0 3px;width:auto;border:0}.cke_toolbar_separator{float:left;background-color:#bcbcbc;margin:4px 2px 0 2px;height:18px;width:1px}.cke_rtl .cke_toolbar_separator{float:right}.cke_hc .cke_toolbar_separator{background-color:#000;margin-left:2px;margin-right:5px;margin-bottom:9px}.cke_hc.cke_rtl .cke_toolbar_separator{margin-left:5px;margin-right:2px}.cke_toolbar_break{display:block;clear:left}.cke_rtl .cke_toolbar_break{clear:right}a.cke_toolbox_collapser{width:12px;height:11px;float:right;margin:11px 0 0;font-size:0;cursor:default;text-align:center;border:1px solid #bcbcbc}.cke_rtl .cke_toolbox_collapser{float:left}.cke_toolbox_collapser:hover{background:#e5e5e5}.cke_toolbox_collapser.cke_toolbox_collapser_min{margin:0 2px 4px}.cke_toolbox_collapser .cke_arrow{display:inline-block;height:0;width:0;font-size:0;margin-top:1px;border:3px solid transparent;border-bottom-color:#484848}.cke_toolbox_collapser.cke_toolbox_collapser_min .cke_arrow{margin-top:4px;border-bottom-color:transparent;border-top-color:#484848}.cke_hc .cke_toolbox_collapser .cke_arrow{font-size:8px;width:auto;border:0;margin-top:0}.cke_menuitem span{cursor:default}.cke_menubutton{display:block}.cke_hc .cke_menubutton{padding:2px}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#e9e9e9;display:block;outline:1px dotted}.cke_menubutton:hover{outline:0}.cke_hc .cke_menubutton:hover,.cke_hc .cke_menubutton:focus,.cke_hc .cke_menubutton:active{border:2px solid;padding:0}.cke_menubutton_disabled:hover,.cke_menubutton_disabled:focus,.cke_menubutton_disabled:active{background-color:transparent;outline:0}.cke_menubutton_inner{display:table-row}.cke_menubutton_icon,.cke_menubutton_label,.cke_menuarrow{display:table-cell}.cke_menubutton_icon{background-color:#f8f8f8;padding:6px 4px}.cke_hc .cke_menubutton_icon{height:16px;width:0;padding:4px 0}.cke_menubutton:hover .cke_menubutton_icon,.cke_menubutton:focus .cke_menubutton_icon,.cke_menubutton:active .cke_menubutton_icon{background-color:#e9e9e9}.cke_menubutton_disabled:hover .cke_menubutton_icon,.cke_menubutton_disabled:focus .cke_menubutton_icon,.cke_menubutton_disabled:active .cke_menubutton_icon{background-color:#f8f8f8;outline:0}.cke_menuitem .cke_menubutton_on{background-color:#e9e9e9;border:1px solid #dedede;outline:0}.cke_menubutton_on .cke_menubutton_icon{padding-right:3px;background-color:#e9e9e9}.cke_menubutton_label{padding:0 5px;background-color:transparent;width:100%;vertical-align:middle}.cke_menubutton_shortcut{color:#979797}.cke_menubutton_disabled .cke_menubutton_label{opacity:.3;filter:alpha(opacity=30)}.cke_panel_frame .cke_menubutton_label{display:none}.cke_menuseparator{background-color:#d1d1d1;height:1px}.cke_menuarrow{background:transparent url(images/arrow.png) no-repeat 0 10px;padding:0 5px}.cke_rtl .cke_menuarrow{background-position:5px -13px;background-repeat:no-repeat}.cke_hc .cke_menuarrow{background-image:none}.cke_menuarrow span{display:none}.cke_hc .cke_menuarrow span{vertical-align:middle;display:inline}.cke_combo{display:inline-block;float:left;position:relative;margin-bottom:5px}.cke_rtl .cke_combo{float:right}.cke_hc .cke_combo{margin-top:1px;margin-bottom:10px}.cke_combo:after{content:"";position:absolute;height:18px;width:0;border-right:1px solid #bcbcbc;margin-top:5px;top:0;right:0}.cke_rtl .cke_combo:after{border-right:0;border-left:1px solid #bcbcbc;right:auto;left:0}.cke_hc .cke_combo:after{border-color:#000}a.cke_combo_button{cursor:default;display:inline-block;float:left;margin:0;padding:1px}.cke_rtl a.cke_combo_button{float:right}.cke_hc a.cke_combo_button{padding:4px}.cke_combo_on a.cke_combo_button,.cke_combo_off a.cke_combo_button:hover,.cke_combo_off a.cke_combo_button:focus,.cke_combo_off a.cke_combo_button:active{background:#e5e5e5;border:1px solid #bcbcbc;padding:0 0 0 1px;margin-left:-1px}.cke_combo_off a.cke_combo_button:focus{outline:0}.cke_combo_on a.cke_combo_button,.cke_combo_off a.cke_combo_button:active{background:#fff}.cke_rtl .cke_combo_on a.cke_combo_button,.cke_rtl .cke_combo_off a.cke_combo_button:hover,.cke_rtl .cke_combo_off a.cke_combo_button:focus,.cke_rtl .cke_combo_off a.cke_combo_button:active{padding:0 1px 0 0;margin-left:0;margin-right:-1px}.cke_hc .cke_combo_on a.cke_combo_button,.cke_hc .cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_combo_off a.cke_combo_button:active{border:3px solid #000;padding:1px 1px 1px 2px}.cke_hc.cke_rtl .cke_combo_on a.cke_combo_button,.cke_hc.cke_rtl .cke_combo_off a.cke_combo_button:hover,.cke_hc.cke_rtl .cke_combo_off a.cke_combo_button:focus,.cke_hc.cke_rtl .cke_combo_off a.cke_combo_button:active{padding:1px 2px 1px 1px}.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:0 0 0 3px;margin-left:-3px}.cke_rtl .cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_rtl .cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_rtl .cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_rtl .cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:0 3px 0 0;margin-left:0;margin-right:-3px}.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:1px 1px 1px 7px;margin-left:-6px}.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc.cke_rtl .cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:1px 7px 1px 1px;margin-left:0;margin-right:-6px}.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:0;margin:0}.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_toolbox .cke_toolbar:first-child>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_on a.cke_combo_button,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_toolbar_break+.cke_toolbar>.cke_toolbar_start+.cke_combo_off a.cke_combo_button:active{padding:1px;margin:0}.cke_toolbar .cke_combo+.cke_toolbar_end,.cke_toolbar .cke_combo+.cke_toolgroup{margin-right:0;margin-left:2px}.cke_rtl .cke_toolbar .cke_combo+.cke_toolbar_end,.cke_rtl .cke_toolbar .cke_combo+.cke_toolgroup{margin-left:0;margin-right:2px}.cke_hc .cke_toolbar .cke_combo+.cke_toolbar_end,.cke_hc .cke_toolbar .cke_combo+.cke_toolgroup{margin-left:5px}.cke_hc.cke_rtl .cke_toolbar .cke_combo+.cke_toolbar_end,.cke_hc.cke_rtl .cke_toolbar .cke_combo+.cke_toolgroup{margin-left:0;margin-right:5px}.cke_toolbar.cke_toolbar_last .cke_combo:nth-last-child(-n+2):after{content:none;border:0;width:0;height:0}.cke_combo_text{line-height:26px;padding-left:10px;text-overflow:ellipsis;overflow:hidden;float:left;cursor:default;color:#484848;width:60px}.cke_rtl .cke_combo_text{float:right;text-align:right;padding-left:0;padding-right:10px}.cke_hc .cke_combo_text{line-height:18px;font-size:12px}.cke_combo_open{cursor:default;display:inline-block;font-size:0;height:19px;line-height:17px;margin:1px 10px 1px;width:5px}.cke_hc .cke_combo_open{height:12px}.cke_combo_arrow{cursor:default;margin:11px 0 0;float:left;height:0;width:0;font-size:0;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #484848}.cke_hc .cke_combo_arrow{font-size:10px;width:auto;border:0;margin-top:3px}.cke_combo_label{display:none;float:left;line-height:26px;vertical-align:top;margin-right:5px}.cke_rtl .cke_combo_label{float:right;margin-left:5px;margin-right:0}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{opacity:.3}.cke_path{float:left;margin:-2px 0 2px}a.cke_path_item,span.cke_path_empty{display:inline-block;float:left;padding:3px 4px;margin-right:2px;cursor:default;text-decoration:none;outline:0;border:0;color:#484848;font-weight:bold;font-size:11px}.cke_rtl .cke_path,.cke_rtl .cke_path_item,.cke_rtl .cke_path_empty{float:right}a.cke_path_item:hover,a.cke_path_item:focus,a.cke_path_item:active{background-color:#e5e5e5}.cke_hc a.cke_path_item:hover,.cke_hc a.cke_path_item:focus,.cke_hc a.cke_path_item:active{border:2px solid;padding:1px 2px}.cke_button__source_label,.cke_button__sourcedialog_label{display:inline}.cke_combopanel__fontsize{width:135px}textarea.cke_source{font-family:'Courier New',Monospace;font-size:small;background-color:#fff;white-space:pre-wrap;border:0;padding:0;margin:0;display:block}.cke_wysiwyg_frame,.cke_wysiwyg_div{background-color:#fff}.cke_notifications_area{pointer-events:none}.cke_notification{pointer-events:auto;position:relative;margin:10px;width:300px;color:white;text-align:center;opacity:.95;filter:alpha(opacity = 95);-webkit-animation:fadeIn .7s;animation:fadeIn .7s}.cke_notification_message a{color:#12306f}@-webkit-keyframes fadeIn{from{opacity:.4}to{opacity:.95}}@keyframes fadeIn{from{opacity:.4}to{opacity:.95}}.cke_notification_success{background:#72b572;border:1px solid #63a563}.cke_notification_warning{background:#c83939;border:1px solid #902b2b}.cke_notification_info{background:#2e9ad0;border:1px solid #0f74a8}.cke_notification_info span.cke_notification_progress{background-color:#0f74a8;display:block;padding:0;margin:0;height:100%;overflow:hidden;position:absolute;z-index:1}.cke_notification_message{position:relative;margin:4px 23px 3px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:18px;z-index:4;text-overflow:ellipsis;overflow:hidden}.cke_notification_close{background-image:url(images/close.png);background-repeat:no-repeat;background-position:50%;position:absolute;cursor:pointer;text-align:center;height:20px;width:20px;top:1px;right:1px;padding:0;margin:0;z-index:5;opacity:.6;filter:alpha(opacity = 60)}.cke_notification_close:hover{opacity:1;filter:alpha(opacity = 100)}.cke_notification_close span{display:none}.cke_notification_warning a.cke_notification_close{opacity:.8;filter:alpha(opacity = 80)}.cke_notification_warning a.cke_notification_close:hover{opacity:1;filter:alpha(opacity = 100)}.cke_chrome{visibility:inherit}.cke_voice_label{display:none}legend.cke_voice_label{display:none}a.cke_button_disabled,a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{filter:alpha(opacity = 30)}.cke_button_disabled .cke_button_icon{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#00ffffff,endColorstr=#00ffffff)}.cke_button_off:hover,.cke_button_off:focus,.cke_button_off:active{filter:alpha(opacity = 100)}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{filter:alpha(opacity = 30)}.cke_toolbox_collapser{border:1px solid #a6a6a6}.cke_toolbox_collapser .cke_arrow{margin-top:1px}.cke_hc .cke_top,.cke_hc .cke_bottom,.cke_hc .cke_combo_button,.cke_hc a.cke_combo_button:hover,.cke_hc a.cke_combo_button:focus,.cke_hc .cke_toolgroup,.cke_hc .cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active,.cke_hc .cke_toolbox_collapser,.cke_hc .cke_toolbox_collapser:hover,.cke_hc .cke_panel_grouptitle{filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.cke_top,.cke_contents,.cke_bottom{width:100%}.cke_button_arrow{font-size:0}.cke_rtl .cke_toolgroup,.cke_rtl .cke_toolbar_separator,.cke_rtl .cke_button,.cke_rtl .cke_button *,.cke_rtl .cke_combo,.cke_rtl .cke_combo *,.cke_rtl .cke_path_item,.cke_rtl .cke_path_item *,.cke_rtl .cke_path_empty{float:none}.cke_rtl .cke_toolgroup,.cke_rtl .cke_toolbar_separator,.cke_rtl .cke_combo_button,.cke_rtl .cke_combo_button *,.cke_rtl .cke_button,.cke_rtl .cke_button_icon{display:inline-block;vertical-align:top}.cke_rtl .cke_button_icon{float:none}.cke_resizer{width:10px}.cke_source{white-space:normal}.cke_bottom{position:static}.cke_colorbox{font-size:0}.cke_button__bold_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -0px !important;}.cke_button__italic_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -24px !important;}.cke_button__strike_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -48px !important;}.cke_button__subscript_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -72px !important;}.cke_button__superscript_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -96px !important;}.cke_button__underline_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -120px !important;}.cke_rtl .cke_button__copy_icon, .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -144px !important;}.cke_ltr .cke_button__copy_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -168px !important;}.cke_rtl .cke_button__cut_icon, .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -192px !important;}.cke_ltr .cke_button__cut_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -216px !important;}.cke_rtl .cke_button__paste_icon, .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -240px !important;}.cke_ltr .cke_button__paste_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -264px !important;}.cke_button__horizontalrule_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -288px !important;}.cke_rtl .cke_button__indent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -312px !important;}.cke_ltr .cke_button__indent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -336px !important;}.cke_rtl .cke_button__outdent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -360px !important;}.cke_ltr .cke_button__outdent_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -384px !important;}.cke_rtl .cke_button__anchor_icon, .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -408px !important;}.cke_ltr .cke_button__anchor_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -432px !important;}.cke_button__link_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -456px !important;}.cke_button__unlink_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -480px !important;}.cke_rtl .cke_button__bulletedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -504px !important;}.cke_ltr .cke_button__bulletedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -528px !important;}.cke_rtl .cke_button__numberedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -552px !important;}.cke_ltr .cke_button__numberedlist_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -576px !important;}.cke_button__removeformat_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -600px !important;}.cke_rtl .cke_button__sourcedialog_icon, .cke_mixed_dir_content .cke_rtl .cke_button__sourcedialog_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -624px !important;}.cke_ltr .cke_button__sourcedialog_icon {background: url(icons.png?t=c9b79c9) no-repeat 0 -648px !important;}.cke_hidpi .cke_button__bold_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -0px !important;background-size: 16px !important;}.cke_hidpi .cke_button__italic_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -24px !important;background-size: 16px !important;}.cke_hidpi .cke_button__strike_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -48px !important;background-size: 16px !important;}.cke_hidpi .cke_button__subscript_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -72px !important;background-size: 16px !important;}.cke_hidpi .cke_button__superscript_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -96px !important;background-size: 16px !important;}.cke_hidpi .cke_button__underline_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -120px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__copy_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -144px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__copy_icon,.cke_ltr.cke_hidpi .cke_button__copy_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -168px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__cut_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -192px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__cut_icon,.cke_ltr.cke_hidpi .cke_button__cut_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -216px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__paste_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -240px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__paste_icon,.cke_ltr.cke_hidpi .cke_button__paste_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -264px !important;background-size: 16px !important;}.cke_hidpi .cke_button__horizontalrule_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -288px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__indent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -312px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__indent_icon,.cke_ltr.cke_hidpi .cke_button__indent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -336px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__outdent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -360px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__outdent_icon,.cke_ltr.cke_hidpi .cke_button__outdent_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -384px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__anchor_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -408px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__anchor_icon,.cke_ltr.cke_hidpi .cke_button__anchor_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -432px !important;background-size: 16px !important;}.cke_hidpi .cke_button__link_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -456px !important;background-size: 16px !important;}.cke_hidpi .cke_button__unlink_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -480px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__bulletedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -504px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__bulletedlist_icon,.cke_ltr.cke_hidpi .cke_button__bulletedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -528px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__numberedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -552px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__numberedlist_icon,.cke_ltr.cke_hidpi .cke_button__numberedlist_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -576px !important;background-size: 16px !important;}.cke_hidpi .cke_button__removeformat_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -600px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__sourcedialog_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__sourcedialog_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -624px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__sourcedialog_icon,.cke_ltr.cke_hidpi .cke_button__sourcedialog_icon {background: url(icons_hidpi.png?t=c9b79c9) no-repeat 0 -648px !important;background-size: 16px !important;}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono-lisa/icons.png b/js/ckeditor/skins/moono-lisa/icons.png
new file mode 100644 (file)
index 0000000..ad22906
Binary files /dev/null and b/js/ckeditor/skins/moono-lisa/icons.png differ
diff --git a/js/ckeditor/skins/moono-lisa/icons_hidpi.png b/js/ckeditor/skins/moono-lisa/icons_hidpi.png
new file mode 100644 (file)
index 0000000..7c76e52
Binary files /dev/null and b/js/ckeditor/skins/moono-lisa/icons_hidpi.png differ
diff --git a/js/ckeditor/skins/moono-lisa/images/arrow.png b/js/ckeditor/skins/moono-lisa/images/arrow.png
new file mode 100644 (file)
index 0000000..d72b5f3
Binary files /dev/null and b/js/ckeditor/skins/moono-lisa/images/arrow.png differ
diff --git a/js/ckeditor/skins/moono-lisa/images/close.png b/js/ckeditor/skins/moono-lisa/images/close.png
new file mode 100644 (file)
index 0000000..40caa6d
Binary files /dev/null and b/js/ckeditor/skins/moono-lisa/images/close.png differ
diff --git a/js/ckeditor/skins/moono-lisa/images/hidpi/close.png b/js/ckeditor/skins/moono-lisa/images/hidpi/close.png
new file mode 100644 (file)
index 0000000..fa00f4f
Binary files /dev/null and b/js/ckeditor/skins/moono-lisa/images/hidpi/close.png differ
diff --git a/js/ckeditor/skins/moono-lisa/images/hidpi/lock-open.png b/js/ckeditor/skins/moono-lisa/images/hidpi/lock-open.png
new file mode 100644 (file)
index 0000000..c899789
Binary files /dev/null and b/js/ckeditor/skins/moono-lisa/images/hidpi/lock-open.png differ
diff --git a/js/ckeditor/skins/moono-lisa/images/hidpi/lock.png b/js/ckeditor/skins/moono-lisa/images/hidpi/lock.png
new file mode 100644 (file)
index 0000000..25ad0f4
Binary files /dev/null and b/js/ckeditor/skins/moono-lisa/images/hidpi/lock.png differ
diff --git a/js/ckeditor/skins/moono-lisa/images/hidpi/refresh.png b/js/ckeditor/skins/moono-lisa/images/hidpi/refresh.png
new file mode 100644 (file)
index 0000000..117a2d4
Binary files /dev/null and b/js/ckeditor/skins/moono-lisa/images/hidpi/refresh.png differ
diff --git a/js/ckeditor/skins/moono-lisa/images/lock-open.png b/js/ckeditor/skins/moono-lisa/images/lock-open.png
new file mode 100644 (file)
index 0000000..42df5f4
Binary files /dev/null and b/js/ckeditor/skins/moono-lisa/images/lock-open.png differ
diff --git a/js/ckeditor/skins/moono-lisa/images/lock.png b/js/ckeditor/skins/moono-lisa/images/lock.png
new file mode 100644 (file)
index 0000000..bde6772
Binary files /dev/null and b/js/ckeditor/skins/moono-lisa/images/lock.png differ
diff --git a/js/ckeditor/skins/moono-lisa/images/refresh.png b/js/ckeditor/skins/moono-lisa/images/refresh.png
new file mode 100644 (file)
index 0000000..e363764
Binary files /dev/null and b/js/ckeditor/skins/moono-lisa/images/refresh.png differ
diff --git a/js/ckeditor/skins/moono-lisa/images/spinner.gif b/js/ckeditor/skins/moono-lisa/images/spinner.gif
new file mode 100644 (file)
index 0000000..d898d41
Binary files /dev/null and b/js/ckeditor/skins/moono-lisa/images/spinner.gif differ
diff --git a/js/ckeditor/skins/moono-lisa/readme.md b/js/ckeditor/skins/moono-lisa/readme.md
new file mode 100644 (file)
index 0000000..d4c6811
--- /dev/null
@@ -0,0 +1,46 @@
+"Moono-lisa" Skin\r
+=================\r
+\r
+This skin has been made a **default skin** starting from CKEditor 4.6.0 and is maintained by the core developers.\r
+\r
+For more information about skins, please check the [CKEditor Skin SDK](http://docs.cksource.com/CKEditor_4.x/Skin_SDK)\r
+documentation.\r
+\r
+Features\r
+-------------------\r
+"Moono-lisa" is a monochromatic skin, which offers a modern, flat and minimalistic look which blends very well in modern design.\r
+It comes with the following features:\r
+\r
+- Chameleon feature with brightness.\r
+- High-contrast compatibility.\r
+- Graphics source provided in SVG.\r
+\r
+Directory Structure\r
+-------------------\r
+\r
+CSS parts:\r
+- **editor.css**: the main CSS file. It's simply loading several other files, for easier maintenance,\r
+- **mainui.css**: the file contains styles of entire editor outline structures,\r
+- **toolbar.css**: the file contains styles of the editor toolbar space (top),\r
+- **richcombo.css**: the file contains styles of the rich combo ui elements on toolbar,\r
+- **panel.css**: the file contains styles of the rich combo drop-down, it's not loaded\r
+until the first panel open up,\r
+- **elementspath.css**: the file contains styles of the editor elements path bar (bottom),\r
+- **menu.css**: the file contains styles of all editor menus including context menu and button drop-down,\r
+it's not loaded until the first menu open up,\r
+- **dialog.css**: the CSS files for the dialog UI, it's not loaded until the first dialog open,\r
+- **reset.css**: the file defines the basis of style resets among all editor UI spaces,\r
+- **preset.css**: the file defines the default styles of some UI elements reflecting the skin preference,\r
+- **editor_XYZ.css** and **dialog_XYZ.css**: browser specific CSS hacks.\r
+\r
+Other parts:\r
+- **skin.js**: the only JavaScript part of the skin that registers the skin, its browser specific files and its icons and defines the Chameleon feature,\r
+- **images/**: contains a fill general used images,\r
+- **dev/**: contains SVG and PNG source of the skin icons.\r
+\r
+License\r
+-------\r
+\r
+Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+\r
+For licensing, see LICENSE.md or [http://ckeditor.com/license](http://ckeditor.com/license)\r
diff --git a/js/ckeditor/skins/moono/dialog.css b/js/ckeditor/skins/moono/dialog.css
deleted file mode 100644 (file)
index cd7c371..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-/*\r
-Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
-For licensing, see LICENSE.md or http://ckeditor.com/license\r
-*/\r
-.cke_dialog{visibility:visible}.cke_dialog_body{z-index:1;background:#eaeaea;border:1px solid #b2b2b2;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_browser_gecko19 .cke_dialog_body{position:relative}.cke_dialog strong{font-weight:bold}.cke_dialog_title{font-weight:bold;font-size:13px;cursor:move;position:relative;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.75);border-bottom:1px solid #999;padding:6px 10px;-moz-border-radius:2px 2px 0 0;-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_dialog_contents{background-color:#fff;overflow:auto;padding:15px 10px 5px 10px;margin-top:30px;border-top:1px solid #bfbfbf;-moz-border-radius:0 0 3px 3px;-webkit-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px}.cke_dialog_contents_body{overflow:auto;padding:17px 10px 5px 10px;margin-top:22px}.cke_dialog_footer{text-align:right;position:relative;border:0;outline:1px solid #bfbfbf;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;-moz-border-radius:0 0 2px 2px;-webkit-border-radius:0 0 2px 2px;border-radius:0 0 2px 2px;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#cfd1cf));background-image:-moz-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-webkit-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-o-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-ms-linear-gradient(top,#ebebeb,#cfd1cf);background-image:linear-gradient(top,#ebebeb,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ebebeb',endColorstr='#cfd1cf')}.cke_rtl .cke_dialog_footer{text-align:left}.cke_hc .cke_dialog_footer{outline:0;border-top:1px solid #fff}.cke_dialog .cke_resizer{margin-top:22px}.cke_dialog .cke_resizer_rtl{margin-left:5px}.cke_dialog .cke_resizer_ltr{margin-right:5px}.cke_dialog_tabs{height:24px;display:inline-block;margin:5px 0 0;position:absolute;z-index:2;left:10px}.cke_rtl .cke_dialog_tabs{right:10px}a.cke_dialog_tab{height:16px;padding:4px 8px;margin-right:3px;display:inline-block;cursor:pointer;line-height:16px;outline:0;color:#595959;border:1px solid #bfbfbf;-moz-border-radius:3px 3px 0 0;-webkit-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;background:#d4d4d4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fafafa),to(#ededed));background-image:-moz-linear-gradient(top,#fafafa,#ededed);background-image:-webkit-linear-gradient(top,#fafafa,#ededed);background-image:-o-linear-gradient(top,#fafafa,#ededed);background-image:-ms-linear-gradient(top,#fafafa,#ededed);background-image:linear-gradient(top,#fafafa,#ededed);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#fafafa',endColorstr='#ededed')}.cke_rtl a.cke_dialog_tab{margin-right:0;margin-left:3px}a.cke_dialog_tab:hover{background:#ebebeb;background:-moz-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ebebeb),color-stop(100%,#dfdfdf));background:-webkit-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-o-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-ms-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:linear-gradient(to bottom,#ebebeb 0,#dfdfdf 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ebebeb',endColorstr='#dfdfdf',GradientType=0)}a.cke_dialog_tab_selected{background:#fff;color:#383838;border-bottom-color:#fff;cursor:default;filter:none}a.cke_dialog_tab_selected:hover{background:#ededed;background:-moz-linear-gradient(top,#ededed 0,#fff 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ededed),color-stop(100%,#fff));background:-webkit-linear-gradient(top,#ededed 0,#fff 100%);background:-o-linear-gradient(top,#ededed 0,#fff 100%);background:-ms-linear-gradient(top,#ededed 0,#fff 100%);background:linear-gradient(to bottom,#ededed 0,#fff 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ededed',endColorstr='#ffffff',GradientType=0)}.cke_hc a.cke_dialog_tab:hover,.cke_hc a.cke_dialog_tab_selected{border:3px solid;padding:2px 6px}a.cke_dialog_tab_disabled{color:#bababa;cursor:default}.cke_single_page .cke_dialog_tabs{display:none}.cke_single_page .cke_dialog_contents{padding-top:5px;margin-top:0;border-top:0}.cke_dialog_close_button{background-image:url(images/close.png);background-repeat:no-repeat;background-position:0 0;position:absolute;cursor:pointer;text-align:center;height:20px;width:20px;top:5px;z-index:5}.cke_hidpi .cke_dialog_close_button{background-image:url(images/hidpi/close.png);background-size:16px}.cke_dialog_close_button span{display:none}.cke_hc .cke_dialog_close_button span{display:inline;cursor:pointer;font-weight:bold;position:relative;top:3px}.cke_ltr .cke_dialog_close_button{right:5px}.cke_rtl .cke_dialog_close_button{left:6px}.cke_dialog_close_button{top:4px}div.cke_disabled .cke_dialog_ui_labeled_content div *{background-color:#ddd;cursor:default}.cke_dialog_ui_vbox table,.cke_dialog_ui_hbox table{margin:auto}.cke_dialog_ui_vbox_child{padding:5px 0}.cke_dialog_ui_hbox{width:100%}.cke_dialog_ui_hbox_first,.cke_dialog_ui_hbox_child,.cke_dialog_ui_hbox_last{vertical-align:top}.cke_ltr .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_ui_hbox_child{padding-right:10px}.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_child{padding-left:10px}.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-right:5px}.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-left:5px;padding-right:0}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:1px solid}textarea.cke_dialog_ui_input_textarea{overflow:auto;resize:none}input.cke_dialog_ui_input_text,input.cke_dialog_ui_input_password,textarea.cke_dialog_ui_input_textarea{background-color:#fff;border:1px solid #c9cccf;border-top-color:#aeb3b9;padding:4px 6px;outline:0;width:100%;*width:95%;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;box-shadow:0 1px 2px rgba(0,0,0,.15) inset}input.cke_dialog_ui_input_text:hover,input.cke_dialog_ui_input_password:hover,textarea.cke_dialog_ui_input_textarea:hover{border:1px solid #aeb3b9;border-top-color:#a0a6ad}input.cke_dialog_ui_input_text:focus,input.cke_dialog_ui_input_password:focus,textarea.cke_dialog_ui_input_textarea:focus,select.cke_dialog_ui_input_select:focus{outline:0;border:1px solid #139ff7;border-top-color:#1392e9}a.cke_dialog_ui_button{display:inline-block;*display:inline;*zoom:1;padding:3px 0;margin:0;text-align:center;color:#333;vertical-align:middle;cursor:pointer;border:1px solid #b6b6b6;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}span.cke_dialog_ui_button{padding:0 12px}a.cke_dialog_ui_button:hover{border-color:#9e9e9e;background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}a.cke_dialog_ui_button:focus,a.cke_dialog_ui_button:active{border-color:#969696;outline:0;-moz-box-shadow:0 0 6px rgba(0,0,0,.4) inset;-webkit-box-shadow:0 0 6px rgba(0,0,0,.4) inset;box-shadow:0 0 6px rgba(0,0,0,.4) inset}.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button:focus,.cke_hc a.cke_dialog_ui_button:active{border:3px solid;padding-top:1px;padding-bottom:1px}.cke_hc a.cke_dialog_ui_button:hover span,.cke_hc a.cke_dialog_ui_button:focus span,.cke_hc a.cke_dialog_ui_button:active span{padding-left:10px;padding-right:10px}.cke_dialog_footer_buttons a.cke_dialog_ui_button span{color:inherit;font-size:12px;font-weight:bold;line-height:20px}a.cke_dialog_ui_button_ok{color:#fff;text-shadow:0 -1px 0 #55830c;border-color:#62a60a #62a60a #4d9200;background:#69b10b;background-image:-webkit-gradient(linear,0 0,0 100%,from(#9ad717),to(#69b10b));background-image:-webkit-linear-gradient(top,#9ad717,#69b10b);background-image:-o-linear-gradient(top,#9ad717,#69b10b);background-image:linear-gradient(to bottom,#9ad717,#69b10b);background-image:-moz-linear-gradient(top,#9ad717,#69b10b);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#9ad717',endColorstr='#69b10b')}a.cke_dialog_ui_button_ok:hover{border-color:#5b9909 #5b9909 #478500;background:#88be14;background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#88be14),color-stop(100%,#5d9c0a));background:-webkit-linear-gradient(top,#88be14 0,#5d9c0a 100%);background:-o-linear-gradient(top,#88be14 0,#5d9c0a 100%);background:linear-gradient(to bottom,#88be14 0,#5d9c0a 100%);background:-moz-linear-gradient(top,#88be14 0,#5d9c0a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#88be14',endColorstr='#5d9c0a',GradientType=0)}a.cke_dialog_ui_button span{text-shadow:0 1px 0 #fff}a.cke_dialog_ui_button_ok span{text-shadow:0 -1px 0 #55830c}span.cke_dialog_ui_button{cursor:pointer}a.cke_dialog_ui_button_ok:focus,a.cke_dialog_ui_button_ok:active,a.cke_dialog_ui_button_cancel:focus,a.cke_dialog_ui_button_cancel:active{border-width:2px;padding:2px 0}a.cke_dialog_ui_button_ok:focus,a.cke_dialog_ui_button_ok:active{border-color:#568c0a}a.cke_dialog_ui_button_ok:focus span,a.cke_dialog_ui_button_ok:active span,a.cke_dialog_ui_button_cancel:focus span,a.cke_dialog_ui_button_cancel:active span{padding:0 11px}.cke_dialog_footer_buttons{display:inline-table;margin:5px;width:auto;position:relative;vertical-align:middle}div.cke_dialog_ui_input_select{display:table}select.cke_dialog_ui_input_select{height:24px;line-height:24px;background-color:#fff;border:1px solid #c9cccf;border-top-color:#aeb3b9;padding:2px 6px;outline:0;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;box-shadow:0 1px 2px rgba(0,0,0,.15) inset}.cke_dialog_ui_input_file{width:100%;height:25px}.cke_hc .cke_dialog_ui_labeled_content input:focus,.cke_hc .cke_dialog_ui_labeled_content select:focus,.cke_hc .cke_dialog_ui_labeled_content textarea:focus{outline:1px dotted}.cke_dialog .cke_dark_background{background-color:#dedede}.cke_dialog .cke_light_background{background-color:#ebebeb}.cke_dialog .cke_centered{text-align:center}.cke_dialog a.cke_btn_reset{float:right;background:url(images/refresh.png) top left no-repeat;width:16px;height:16px;border:1px none;font-size:1px}.cke_hidpi .cke_dialog a.cke_btn_reset{background-size:16px;background-image:url(images/hidpi/refresh.png)}.cke_rtl .cke_dialog a.cke_btn_reset{float:left}.cke_dialog a.cke_btn_locked,.cke_dialog a.cke_btn_unlocked{float:left;width:16px;height:16px;background-repeat:no-repeat;border:none 1px;font-size:1px}.cke_dialog a.cke_btn_locked .cke_icon{display:none}.cke_rtl .cke_dialog a.cke_btn_locked,.cke_rtl .cke_dialog a.cke_btn_unlocked{float:right}.cke_dialog a.cke_btn_locked{background-image:url(images/lock.png)}.cke_dialog a.cke_btn_unlocked{background-image:url(images/lock-open.png)}.cke_hidpi .cke_dialog a.cke_btn_unlocked,.cke_hidpi .cke_dialog a.cke_btn_locked{background-size:16px}.cke_hidpi .cke_dialog a.cke_btn_locked{background-image:url(images/hidpi/lock.png)}.cke_hidpi .cke_dialog a.cke_btn_unlocked{background-image:url(images/hidpi/lock-open.png)}.cke_dialog .cke_btn_over{border:outset 1px;cursor:pointer}.cke_dialog .ImagePreviewBox{border:2px ridge black;overflow:scroll;height:200px;width:300px;padding:2px;background-color:white}.cke_dialog .ImagePreviewBox table td{white-space:normal}.cke_dialog .ImagePreviewLoader{position:absolute;white-space:normal;overflow:hidden;height:160px;width:230px;margin:2px;padding:2px;opacity:.9;filter:alpha(opacity = 90);background-color:#e4e4e4}.cke_dialog .FlashPreviewBox{white-space:normal;border:2px ridge black;overflow:auto;height:160px;width:390px;padding:2px;background-color:white}.cke_dialog .cke_pastetext{width:346px;height:170px}.cke_dialog .cke_pastetext textarea{width:340px;height:170px;resize:none}.cke_dialog iframe.cke_pasteframe{width:346px;height:130px;background-color:white;border:1px solid #aeb3b9;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px}.cke_dialog .cke_hand{cursor:pointer}.cke_disabled{color:#a0a0a0}.cke_dialog_body .cke_label{display:none}.cke_dialog_body label{display:inline;margin-bottom:auto;cursor:default}.cke_dialog_body label.cke_required{font-weight:bold}a.cke_smile{overflow:hidden;display:block;text-align:center;padding:.3em 0}a.cke_smile img{vertical-align:middle}a.cke_specialchar{cursor:inherit;display:block;height:1.25em;padding:.2em .3em;text-align:center}a.cke_smile,a.cke_specialchar{border:1px solid transparent}a.cke_smile:hover,a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:hover,a.cke_specialchar:focus,a.cke_specialchar:active{background:#fff;outline:0}a.cke_smile:hover,a.cke_specialchar:hover{border-color:#888}a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:focus,a.cke_specialchar:active{border-color:#139ff7}.cke_dialog_contents a.colorChooser{display:block;margin-top:6px;margin-left:10px;width:80px}.cke_rtl .cke_dialog_contents a.colorChooser{margin-right:10px}.cke_dialog_ui_checkbox_input:focus,.cke_dialog_ui_radio_input:focus,.cke_btn_over{outline:1px dotted #696969}.cke_iframe_shim{display:block;position:absolute;top:0;left:0;z-index:-1;filter:alpha(opacity = 0);width:100%;height:100%}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono/dialog_ie.css b/js/ckeditor/skins/moono/dialog_ie.css
deleted file mode 100644 (file)
index bde032b..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-/*\r
-Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
-For licensing, see LICENSE.md or http://ckeditor.com/license\r
-*/\r
-.cke_dialog{visibility:visible}.cke_dialog_body{z-index:1;background:#eaeaea;border:1px solid #b2b2b2;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_browser_gecko19 .cke_dialog_body{position:relative}.cke_dialog strong{font-weight:bold}.cke_dialog_title{font-weight:bold;font-size:13px;cursor:move;position:relative;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.75);border-bottom:1px solid #999;padding:6px 10px;-moz-border-radius:2px 2px 0 0;-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_dialog_contents{background-color:#fff;overflow:auto;padding:15px 10px 5px 10px;margin-top:30px;border-top:1px solid #bfbfbf;-moz-border-radius:0 0 3px 3px;-webkit-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px}.cke_dialog_contents_body{overflow:auto;padding:17px 10px 5px 10px;margin-top:22px}.cke_dialog_footer{text-align:right;position:relative;border:0;outline:1px solid #bfbfbf;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;-moz-border-radius:0 0 2px 2px;-webkit-border-radius:0 0 2px 2px;border-radius:0 0 2px 2px;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#cfd1cf));background-image:-moz-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-webkit-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-o-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-ms-linear-gradient(top,#ebebeb,#cfd1cf);background-image:linear-gradient(top,#ebebeb,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ebebeb',endColorstr='#cfd1cf')}.cke_rtl .cke_dialog_footer{text-align:left}.cke_hc .cke_dialog_footer{outline:0;border-top:1px solid #fff}.cke_dialog .cke_resizer{margin-top:22px}.cke_dialog .cke_resizer_rtl{margin-left:5px}.cke_dialog .cke_resizer_ltr{margin-right:5px}.cke_dialog_tabs{height:24px;display:inline-block;margin:5px 0 0;position:absolute;z-index:2;left:10px}.cke_rtl .cke_dialog_tabs{right:10px}a.cke_dialog_tab{height:16px;padding:4px 8px;margin-right:3px;display:inline-block;cursor:pointer;line-height:16px;outline:0;color:#595959;border:1px solid #bfbfbf;-moz-border-radius:3px 3px 0 0;-webkit-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;background:#d4d4d4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fafafa),to(#ededed));background-image:-moz-linear-gradient(top,#fafafa,#ededed);background-image:-webkit-linear-gradient(top,#fafafa,#ededed);background-image:-o-linear-gradient(top,#fafafa,#ededed);background-image:-ms-linear-gradient(top,#fafafa,#ededed);background-image:linear-gradient(top,#fafafa,#ededed);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#fafafa',endColorstr='#ededed')}.cke_rtl a.cke_dialog_tab{margin-right:0;margin-left:3px}a.cke_dialog_tab:hover{background:#ebebeb;background:-moz-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ebebeb),color-stop(100%,#dfdfdf));background:-webkit-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-o-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-ms-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:linear-gradient(to bottom,#ebebeb 0,#dfdfdf 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ebebeb',endColorstr='#dfdfdf',GradientType=0)}a.cke_dialog_tab_selected{background:#fff;color:#383838;border-bottom-color:#fff;cursor:default;filter:none}a.cke_dialog_tab_selected:hover{background:#ededed;background:-moz-linear-gradient(top,#ededed 0,#fff 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ededed),color-stop(100%,#fff));background:-webkit-linear-gradient(top,#ededed 0,#fff 100%);background:-o-linear-gradient(top,#ededed 0,#fff 100%);background:-ms-linear-gradient(top,#ededed 0,#fff 100%);background:linear-gradient(to bottom,#ededed 0,#fff 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ededed',endColorstr='#ffffff',GradientType=0)}.cke_hc a.cke_dialog_tab:hover,.cke_hc a.cke_dialog_tab_selected{border:3px solid;padding:2px 6px}a.cke_dialog_tab_disabled{color:#bababa;cursor:default}.cke_single_page .cke_dialog_tabs{display:none}.cke_single_page .cke_dialog_contents{padding-top:5px;margin-top:0;border-top:0}.cke_dialog_close_button{background-image:url(images/close.png);background-repeat:no-repeat;background-position:0 0;position:absolute;cursor:pointer;text-align:center;height:20px;width:20px;top:5px;z-index:5}.cke_hidpi .cke_dialog_close_button{background-image:url(images/hidpi/close.png);background-size:16px}.cke_dialog_close_button span{display:none}.cke_hc .cke_dialog_close_button span{display:inline;cursor:pointer;font-weight:bold;position:relative;top:3px}.cke_ltr .cke_dialog_close_button{right:5px}.cke_rtl .cke_dialog_close_button{left:6px}.cke_dialog_close_button{top:4px}div.cke_disabled .cke_dialog_ui_labeled_content div *{background-color:#ddd;cursor:default}.cke_dialog_ui_vbox table,.cke_dialog_ui_hbox table{margin:auto}.cke_dialog_ui_vbox_child{padding:5px 0}.cke_dialog_ui_hbox{width:100%}.cke_dialog_ui_hbox_first,.cke_dialog_ui_hbox_child,.cke_dialog_ui_hbox_last{vertical-align:top}.cke_ltr .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_ui_hbox_child{padding-right:10px}.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_child{padding-left:10px}.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-right:5px}.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-left:5px;padding-right:0}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:1px solid}textarea.cke_dialog_ui_input_textarea{overflow:auto;resize:none}input.cke_dialog_ui_input_text,input.cke_dialog_ui_input_password,textarea.cke_dialog_ui_input_textarea{background-color:#fff;border:1px solid #c9cccf;border-top-color:#aeb3b9;padding:4px 6px;outline:0;width:100%;*width:95%;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;box-shadow:0 1px 2px rgba(0,0,0,.15) inset}input.cke_dialog_ui_input_text:hover,input.cke_dialog_ui_input_password:hover,textarea.cke_dialog_ui_input_textarea:hover{border:1px solid #aeb3b9;border-top-color:#a0a6ad}input.cke_dialog_ui_input_text:focus,input.cke_dialog_ui_input_password:focus,textarea.cke_dialog_ui_input_textarea:focus,select.cke_dialog_ui_input_select:focus{outline:0;border:1px solid #139ff7;border-top-color:#1392e9}a.cke_dialog_ui_button{display:inline-block;*display:inline;*zoom:1;padding:3px 0;margin:0;text-align:center;color:#333;vertical-align:middle;cursor:pointer;border:1px solid #b6b6b6;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}span.cke_dialog_ui_button{padding:0 12px}a.cke_dialog_ui_button:hover{border-color:#9e9e9e;background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}a.cke_dialog_ui_button:focus,a.cke_dialog_ui_button:active{border-color:#969696;outline:0;-moz-box-shadow:0 0 6px rgba(0,0,0,.4) inset;-webkit-box-shadow:0 0 6px rgba(0,0,0,.4) inset;box-shadow:0 0 6px rgba(0,0,0,.4) inset}.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button:focus,.cke_hc a.cke_dialog_ui_button:active{border:3px solid;padding-top:1px;padding-bottom:1px}.cke_hc a.cke_dialog_ui_button:hover span,.cke_hc a.cke_dialog_ui_button:focus span,.cke_hc a.cke_dialog_ui_button:active span{padding-left:10px;padding-right:10px}.cke_dialog_footer_buttons a.cke_dialog_ui_button span{color:inherit;font-size:12px;font-weight:bold;line-height:20px}a.cke_dialog_ui_button_ok{color:#fff;text-shadow:0 -1px 0 #55830c;border-color:#62a60a #62a60a #4d9200;background:#69b10b;background-image:-webkit-gradient(linear,0 0,0 100%,from(#9ad717),to(#69b10b));background-image:-webkit-linear-gradient(top,#9ad717,#69b10b);background-image:-o-linear-gradient(top,#9ad717,#69b10b);background-image:linear-gradient(to bottom,#9ad717,#69b10b);background-image:-moz-linear-gradient(top,#9ad717,#69b10b);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#9ad717',endColorstr='#69b10b')}a.cke_dialog_ui_button_ok:hover{border-color:#5b9909 #5b9909 #478500;background:#88be14;background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#88be14),color-stop(100%,#5d9c0a));background:-webkit-linear-gradient(top,#88be14 0,#5d9c0a 100%);background:-o-linear-gradient(top,#88be14 0,#5d9c0a 100%);background:linear-gradient(to bottom,#88be14 0,#5d9c0a 100%);background:-moz-linear-gradient(top,#88be14 0,#5d9c0a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#88be14',endColorstr='#5d9c0a',GradientType=0)}a.cke_dialog_ui_button span{text-shadow:0 1px 0 #fff}a.cke_dialog_ui_button_ok span{text-shadow:0 -1px 0 #55830c}span.cke_dialog_ui_button{cursor:pointer}a.cke_dialog_ui_button_ok:focus,a.cke_dialog_ui_button_ok:active,a.cke_dialog_ui_button_cancel:focus,a.cke_dialog_ui_button_cancel:active{border-width:2px;padding:2px 0}a.cke_dialog_ui_button_ok:focus,a.cke_dialog_ui_button_ok:active{border-color:#568c0a}a.cke_dialog_ui_button_ok:focus span,a.cke_dialog_ui_button_ok:active span,a.cke_dialog_ui_button_cancel:focus span,a.cke_dialog_ui_button_cancel:active span{padding:0 11px}.cke_dialog_footer_buttons{display:inline-table;margin:5px;width:auto;position:relative;vertical-align:middle}div.cke_dialog_ui_input_select{display:table}select.cke_dialog_ui_input_select{height:24px;line-height:24px;background-color:#fff;border:1px solid #c9cccf;border-top-color:#aeb3b9;padding:2px 6px;outline:0;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;box-shadow:0 1px 2px rgba(0,0,0,.15) inset}.cke_dialog_ui_input_file{width:100%;height:25px}.cke_hc .cke_dialog_ui_labeled_content input:focus,.cke_hc .cke_dialog_ui_labeled_content select:focus,.cke_hc .cke_dialog_ui_labeled_content textarea:focus{outline:1px dotted}.cke_dialog .cke_dark_background{background-color:#dedede}.cke_dialog .cke_light_background{background-color:#ebebeb}.cke_dialog .cke_centered{text-align:center}.cke_dialog a.cke_btn_reset{float:right;background:url(images/refresh.png) top left no-repeat;width:16px;height:16px;border:1px none;font-size:1px}.cke_hidpi .cke_dialog a.cke_btn_reset{background-size:16px;background-image:url(images/hidpi/refresh.png)}.cke_rtl .cke_dialog a.cke_btn_reset{float:left}.cke_dialog a.cke_btn_locked,.cke_dialog a.cke_btn_unlocked{float:left;width:16px;height:16px;background-repeat:no-repeat;border:none 1px;font-size:1px}.cke_dialog a.cke_btn_locked .cke_icon{display:none}.cke_rtl .cke_dialog a.cke_btn_locked,.cke_rtl .cke_dialog a.cke_btn_unlocked{float:right}.cke_dialog a.cke_btn_locked{background-image:url(images/lock.png)}.cke_dialog a.cke_btn_unlocked{background-image:url(images/lock-open.png)}.cke_hidpi .cke_dialog a.cke_btn_unlocked,.cke_hidpi .cke_dialog a.cke_btn_locked{background-size:16px}.cke_hidpi .cke_dialog a.cke_btn_locked{background-image:url(images/hidpi/lock.png)}.cke_hidpi .cke_dialog a.cke_btn_unlocked{background-image:url(images/hidpi/lock-open.png)}.cke_dialog .cke_btn_over{border:outset 1px;cursor:pointer}.cke_dialog .ImagePreviewBox{border:2px ridge black;overflow:scroll;height:200px;width:300px;padding:2px;background-color:white}.cke_dialog .ImagePreviewBox table td{white-space:normal}.cke_dialog .ImagePreviewLoader{position:absolute;white-space:normal;overflow:hidden;height:160px;width:230px;margin:2px;padding:2px;opacity:.9;filter:alpha(opacity = 90);background-color:#e4e4e4}.cke_dialog .FlashPreviewBox{white-space:normal;border:2px ridge black;overflow:auto;height:160px;width:390px;padding:2px;background-color:white}.cke_dialog .cke_pastetext{width:346px;height:170px}.cke_dialog .cke_pastetext textarea{width:340px;height:170px;resize:none}.cke_dialog iframe.cke_pasteframe{width:346px;height:130px;background-color:white;border:1px solid #aeb3b9;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px}.cke_dialog .cke_hand{cursor:pointer}.cke_disabled{color:#a0a0a0}.cke_dialog_body .cke_label{display:none}.cke_dialog_body label{display:inline;margin-bottom:auto;cursor:default}.cke_dialog_body label.cke_required{font-weight:bold}a.cke_smile{overflow:hidden;display:block;text-align:center;padding:.3em 0}a.cke_smile img{vertical-align:middle}a.cke_specialchar{cursor:inherit;display:block;height:1.25em;padding:.2em .3em;text-align:center}a.cke_smile,a.cke_specialchar{border:1px solid transparent}a.cke_smile:hover,a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:hover,a.cke_specialchar:focus,a.cke_specialchar:active{background:#fff;outline:0}a.cke_smile:hover,a.cke_specialchar:hover{border-color:#888}a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:focus,a.cke_specialchar:active{border-color:#139ff7}.cke_dialog_contents a.colorChooser{display:block;margin-top:6px;margin-left:10px;width:80px}.cke_rtl .cke_dialog_contents a.colorChooser{margin-right:10px}.cke_dialog_ui_checkbox_input:focus,.cke_dialog_ui_radio_input:focus,.cke_btn_over{outline:1px dotted #696969}.cke_iframe_shim{display:block;position:absolute;top:0;left:0;z-index:-1;filter:alpha(opacity = 0);width:100%;height:100%}.cke_rtl input.cke_dialog_ui_input_text,.cke_rtl input.cke_dialog_ui_input_password{padding-right:2px}.cke_rtl div.cke_dialog_ui_input_text,.cke_rtl div.cke_dialog_ui_input_password{padding-left:2px}.cke_rtl div.cke_dialog_ui_input_text{padding-right:1px}.cke_rtl .cke_dialog_ui_vbox_child,.cke_rtl .cke_dialog_ui_hbox_child,.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_last{padding-right:2px!important}.cke_hc .cke_dialog_title,.cke_hc .cke_dialog_footer,.cke_hc a.cke_dialog_tab,.cke_hc a.cke_dialog_ui_button,.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button_ok,.cke_hc a.cke_dialog_ui_button_ok:hover{filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:0}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono/dialog_ie7.css b/js/ckeditor/skins/moono/dialog_ie7.css
deleted file mode 100644 (file)
index 8e22de9..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-/*\r
-Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
-For licensing, see LICENSE.md or http://ckeditor.com/license\r
-*/\r
-.cke_dialog{visibility:visible}.cke_dialog_body{z-index:1;background:#eaeaea;border:1px solid #b2b2b2;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_browser_gecko19 .cke_dialog_body{position:relative}.cke_dialog strong{font-weight:bold}.cke_dialog_title{font-weight:bold;font-size:13px;cursor:move;position:relative;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.75);border-bottom:1px solid #999;padding:6px 10px;-moz-border-radius:2px 2px 0 0;-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_dialog_contents{background-color:#fff;overflow:auto;padding:15px 10px 5px 10px;margin-top:30px;border-top:1px solid #bfbfbf;-moz-border-radius:0 0 3px 3px;-webkit-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px}.cke_dialog_contents_body{overflow:auto;padding:17px 10px 5px 10px;margin-top:22px}.cke_dialog_footer{text-align:right;position:relative;border:0;outline:1px solid #bfbfbf;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;-moz-border-radius:0 0 2px 2px;-webkit-border-radius:0 0 2px 2px;border-radius:0 0 2px 2px;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#cfd1cf));background-image:-moz-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-webkit-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-o-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-ms-linear-gradient(top,#ebebeb,#cfd1cf);background-image:linear-gradient(top,#ebebeb,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ebebeb',endColorstr='#cfd1cf')}.cke_rtl .cke_dialog_footer{text-align:left}.cke_hc .cke_dialog_footer{outline:0;border-top:1px solid #fff}.cke_dialog .cke_resizer{margin-top:22px}.cke_dialog .cke_resizer_rtl{margin-left:5px}.cke_dialog .cke_resizer_ltr{margin-right:5px}.cke_dialog_tabs{height:24px;display:inline-block;margin:5px 0 0;position:absolute;z-index:2;left:10px}.cke_rtl .cke_dialog_tabs{right:10px}a.cke_dialog_tab{height:16px;padding:4px 8px;margin-right:3px;display:inline-block;cursor:pointer;line-height:16px;outline:0;color:#595959;border:1px solid #bfbfbf;-moz-border-radius:3px 3px 0 0;-webkit-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;background:#d4d4d4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fafafa),to(#ededed));background-image:-moz-linear-gradient(top,#fafafa,#ededed);background-image:-webkit-linear-gradient(top,#fafafa,#ededed);background-image:-o-linear-gradient(top,#fafafa,#ededed);background-image:-ms-linear-gradient(top,#fafafa,#ededed);background-image:linear-gradient(top,#fafafa,#ededed);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#fafafa',endColorstr='#ededed')}.cke_rtl a.cke_dialog_tab{margin-right:0;margin-left:3px}a.cke_dialog_tab:hover{background:#ebebeb;background:-moz-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ebebeb),color-stop(100%,#dfdfdf));background:-webkit-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-o-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-ms-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:linear-gradient(to bottom,#ebebeb 0,#dfdfdf 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ebebeb',endColorstr='#dfdfdf',GradientType=0)}a.cke_dialog_tab_selected{background:#fff;color:#383838;border-bottom-color:#fff;cursor:default;filter:none}a.cke_dialog_tab_selected:hover{background:#ededed;background:-moz-linear-gradient(top,#ededed 0,#fff 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ededed),color-stop(100%,#fff));background:-webkit-linear-gradient(top,#ededed 0,#fff 100%);background:-o-linear-gradient(top,#ededed 0,#fff 100%);background:-ms-linear-gradient(top,#ededed 0,#fff 100%);background:linear-gradient(to bottom,#ededed 0,#fff 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ededed',endColorstr='#ffffff',GradientType=0)}.cke_hc a.cke_dialog_tab:hover,.cke_hc a.cke_dialog_tab_selected{border:3px solid;padding:2px 6px}a.cke_dialog_tab_disabled{color:#bababa;cursor:default}.cke_single_page .cke_dialog_tabs{display:none}.cke_single_page .cke_dialog_contents{padding-top:5px;margin-top:0;border-top:0}.cke_dialog_close_button{background-image:url(images/close.png);background-repeat:no-repeat;background-position:0 0;position:absolute;cursor:pointer;text-align:center;height:20px;width:20px;top:5px;z-index:5}.cke_hidpi .cke_dialog_close_button{background-image:url(images/hidpi/close.png);background-size:16px}.cke_dialog_close_button span{display:none}.cke_hc .cke_dialog_close_button span{display:inline;cursor:pointer;font-weight:bold;position:relative;top:3px}.cke_ltr .cke_dialog_close_button{right:5px}.cke_rtl .cke_dialog_close_button{left:6px}.cke_dialog_close_button{top:4px}div.cke_disabled .cke_dialog_ui_labeled_content div *{background-color:#ddd;cursor:default}.cke_dialog_ui_vbox table,.cke_dialog_ui_hbox table{margin:auto}.cke_dialog_ui_vbox_child{padding:5px 0}.cke_dialog_ui_hbox{width:100%}.cke_dialog_ui_hbox_first,.cke_dialog_ui_hbox_child,.cke_dialog_ui_hbox_last{vertical-align:top}.cke_ltr .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_ui_hbox_child{padding-right:10px}.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_child{padding-left:10px}.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-right:5px}.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-left:5px;padding-right:0}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:1px solid}textarea.cke_dialog_ui_input_textarea{overflow:auto;resize:none}input.cke_dialog_ui_input_text,input.cke_dialog_ui_input_password,textarea.cke_dialog_ui_input_textarea{background-color:#fff;border:1px solid #c9cccf;border-top-color:#aeb3b9;padding:4px 6px;outline:0;width:100%;*width:95%;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;box-shadow:0 1px 2px rgba(0,0,0,.15) inset}input.cke_dialog_ui_input_text:hover,input.cke_dialog_ui_input_password:hover,textarea.cke_dialog_ui_input_textarea:hover{border:1px solid #aeb3b9;border-top-color:#a0a6ad}input.cke_dialog_ui_input_text:focus,input.cke_dialog_ui_input_password:focus,textarea.cke_dialog_ui_input_textarea:focus,select.cke_dialog_ui_input_select:focus{outline:0;border:1px solid #139ff7;border-top-color:#1392e9}a.cke_dialog_ui_button{display:inline-block;*display:inline;*zoom:1;padding:3px 0;margin:0;text-align:center;color:#333;vertical-align:middle;cursor:pointer;border:1px solid #b6b6b6;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}span.cke_dialog_ui_button{padding:0 12px}a.cke_dialog_ui_button:hover{border-color:#9e9e9e;background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}a.cke_dialog_ui_button:focus,a.cke_dialog_ui_button:active{border-color:#969696;outline:0;-moz-box-shadow:0 0 6px rgba(0,0,0,.4) inset;-webkit-box-shadow:0 0 6px rgba(0,0,0,.4) inset;box-shadow:0 0 6px rgba(0,0,0,.4) inset}.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button:focus,.cke_hc a.cke_dialog_ui_button:active{border:3px solid;padding-top:1px;padding-bottom:1px}.cke_hc a.cke_dialog_ui_button:hover span,.cke_hc a.cke_dialog_ui_button:focus span,.cke_hc a.cke_dialog_ui_button:active span{padding-left:10px;padding-right:10px}.cke_dialog_footer_buttons a.cke_dialog_ui_button span{color:inherit;font-size:12px;font-weight:bold;line-height:20px}a.cke_dialog_ui_button_ok{color:#fff;text-shadow:0 -1px 0 #55830c;border-color:#62a60a #62a60a #4d9200;background:#69b10b;background-image:-webkit-gradient(linear,0 0,0 100%,from(#9ad717),to(#69b10b));background-image:-webkit-linear-gradient(top,#9ad717,#69b10b);background-image:-o-linear-gradient(top,#9ad717,#69b10b);background-image:linear-gradient(to bottom,#9ad717,#69b10b);background-image:-moz-linear-gradient(top,#9ad717,#69b10b);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#9ad717',endColorstr='#69b10b')}a.cke_dialog_ui_button_ok:hover{border-color:#5b9909 #5b9909 #478500;background:#88be14;background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#88be14),color-stop(100%,#5d9c0a));background:-webkit-linear-gradient(top,#88be14 0,#5d9c0a 100%);background:-o-linear-gradient(top,#88be14 0,#5d9c0a 100%);background:linear-gradient(to bottom,#88be14 0,#5d9c0a 100%);background:-moz-linear-gradient(top,#88be14 0,#5d9c0a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#88be14',endColorstr='#5d9c0a',GradientType=0)}a.cke_dialog_ui_button span{text-shadow:0 1px 0 #fff}a.cke_dialog_ui_button_ok span{text-shadow:0 -1px 0 #55830c}span.cke_dialog_ui_button{cursor:pointer}a.cke_dialog_ui_button_ok:focus,a.cke_dialog_ui_button_ok:active,a.cke_dialog_ui_button_cancel:focus,a.cke_dialog_ui_button_cancel:active{border-width:2px;padding:2px 0}a.cke_dialog_ui_button_ok:focus,a.cke_dialog_ui_button_ok:active{border-color:#568c0a}a.cke_dialog_ui_button_ok:focus span,a.cke_dialog_ui_button_ok:active span,a.cke_dialog_ui_button_cancel:focus span,a.cke_dialog_ui_button_cancel:active span{padding:0 11px}.cke_dialog_footer_buttons{display:inline-table;margin:5px;width:auto;position:relative;vertical-align:middle}div.cke_dialog_ui_input_select{display:table}select.cke_dialog_ui_input_select{height:24px;line-height:24px;background-color:#fff;border:1px solid #c9cccf;border-top-color:#aeb3b9;padding:2px 6px;outline:0;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;box-shadow:0 1px 2px rgba(0,0,0,.15) inset}.cke_dialog_ui_input_file{width:100%;height:25px}.cke_hc .cke_dialog_ui_labeled_content input:focus,.cke_hc .cke_dialog_ui_labeled_content select:focus,.cke_hc .cke_dialog_ui_labeled_content textarea:focus{outline:1px dotted}.cke_dialog .cke_dark_background{background-color:#dedede}.cke_dialog .cke_light_background{background-color:#ebebeb}.cke_dialog .cke_centered{text-align:center}.cke_dialog a.cke_btn_reset{float:right;background:url(images/refresh.png) top left no-repeat;width:16px;height:16px;border:1px none;font-size:1px}.cke_hidpi .cke_dialog a.cke_btn_reset{background-size:16px;background-image:url(images/hidpi/refresh.png)}.cke_rtl .cke_dialog a.cke_btn_reset{float:left}.cke_dialog a.cke_btn_locked,.cke_dialog a.cke_btn_unlocked{float:left;width:16px;height:16px;background-repeat:no-repeat;border:none 1px;font-size:1px}.cke_dialog a.cke_btn_locked .cke_icon{display:none}.cke_rtl .cke_dialog a.cke_btn_locked,.cke_rtl .cke_dialog a.cke_btn_unlocked{float:right}.cke_dialog a.cke_btn_locked{background-image:url(images/lock.png)}.cke_dialog a.cke_btn_unlocked{background-image:url(images/lock-open.png)}.cke_hidpi .cke_dialog a.cke_btn_unlocked,.cke_hidpi .cke_dialog a.cke_btn_locked{background-size:16px}.cke_hidpi .cke_dialog a.cke_btn_locked{background-image:url(images/hidpi/lock.png)}.cke_hidpi .cke_dialog a.cke_btn_unlocked{background-image:url(images/hidpi/lock-open.png)}.cke_dialog .cke_btn_over{border:outset 1px;cursor:pointer}.cke_dialog .ImagePreviewBox{border:2px ridge black;overflow:scroll;height:200px;width:300px;padding:2px;background-color:white}.cke_dialog .ImagePreviewBox table td{white-space:normal}.cke_dialog .ImagePreviewLoader{position:absolute;white-space:normal;overflow:hidden;height:160px;width:230px;margin:2px;padding:2px;opacity:.9;filter:alpha(opacity = 90);background-color:#e4e4e4}.cke_dialog .FlashPreviewBox{white-space:normal;border:2px ridge black;overflow:auto;height:160px;width:390px;padding:2px;background-color:white}.cke_dialog .cke_pastetext{width:346px;height:170px}.cke_dialog .cke_pastetext textarea{width:340px;height:170px;resize:none}.cke_dialog iframe.cke_pasteframe{width:346px;height:130px;background-color:white;border:1px solid #aeb3b9;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px}.cke_dialog .cke_hand{cursor:pointer}.cke_disabled{color:#a0a0a0}.cke_dialog_body .cke_label{display:none}.cke_dialog_body label{display:inline;margin-bottom:auto;cursor:default}.cke_dialog_body label.cke_required{font-weight:bold}a.cke_smile{overflow:hidden;display:block;text-align:center;padding:.3em 0}a.cke_smile img{vertical-align:middle}a.cke_specialchar{cursor:inherit;display:block;height:1.25em;padding:.2em .3em;text-align:center}a.cke_smile,a.cke_specialchar{border:1px solid transparent}a.cke_smile:hover,a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:hover,a.cke_specialchar:focus,a.cke_specialchar:active{background:#fff;outline:0}a.cke_smile:hover,a.cke_specialchar:hover{border-color:#888}a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:focus,a.cke_specialchar:active{border-color:#139ff7}.cke_dialog_contents a.colorChooser{display:block;margin-top:6px;margin-left:10px;width:80px}.cke_rtl .cke_dialog_contents a.colorChooser{margin-right:10px}.cke_dialog_ui_checkbox_input:focus,.cke_dialog_ui_radio_input:focus,.cke_btn_over{outline:1px dotted #696969}.cke_iframe_shim{display:block;position:absolute;top:0;left:0;z-index:-1;filter:alpha(opacity = 0);width:100%;height:100%}.cke_rtl input.cke_dialog_ui_input_text,.cke_rtl input.cke_dialog_ui_input_password{padding-right:2px}.cke_rtl div.cke_dialog_ui_input_text,.cke_rtl div.cke_dialog_ui_input_password{padding-left:2px}.cke_rtl div.cke_dialog_ui_input_text{padding-right:1px}.cke_rtl .cke_dialog_ui_vbox_child,.cke_rtl .cke_dialog_ui_hbox_child,.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_last{padding-right:2px!important}.cke_hc .cke_dialog_title,.cke_hc .cke_dialog_footer,.cke_hc a.cke_dialog_tab,.cke_hc a.cke_dialog_ui_button,.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button_ok,.cke_hc a.cke_dialog_ui_button_ok:hover{filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:0}.cke_dialog_title{zoom:1}.cke_dialog_footer{border-top:1px solid #bfbfbf}.cke_dialog_footer_buttons{position:static}.cke_dialog_footer_buttons a.cke_dialog_ui_button{vertical-align:top}.cke_dialog .cke_resizer_ltr{padding-left:4px}.cke_dialog .cke_resizer_rtl{padding-right:4px}.cke_dialog_ui_input_text,.cke_dialog_ui_input_password,.cke_dialog_ui_input_textarea,.cke_dialog_ui_input_select{padding:0!important}.cke_dialog_ui_checkbox_input,.cke_dialog_ui_ratio_input,.cke_btn_reset,.cke_btn_locked,.cke_btn_unlocked{border:1px solid transparent!important}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono/dialog_ie8.css b/js/ckeditor/skins/moono/dialog_ie8.css
deleted file mode 100644 (file)
index 4bd4961..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-/*\r
-Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
-For licensing, see LICENSE.md or http://ckeditor.com/license\r
-*/\r
-.cke_dialog{visibility:visible}.cke_dialog_body{z-index:1;background:#eaeaea;border:1px solid #b2b2b2;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_browser_gecko19 .cke_dialog_body{position:relative}.cke_dialog strong{font-weight:bold}.cke_dialog_title{font-weight:bold;font-size:13px;cursor:move;position:relative;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.75);border-bottom:1px solid #999;padding:6px 10px;-moz-border-radius:2px 2px 0 0;-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_dialog_contents{background-color:#fff;overflow:auto;padding:15px 10px 5px 10px;margin-top:30px;border-top:1px solid #bfbfbf;-moz-border-radius:0 0 3px 3px;-webkit-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px}.cke_dialog_contents_body{overflow:auto;padding:17px 10px 5px 10px;margin-top:22px}.cke_dialog_footer{text-align:right;position:relative;border:0;outline:1px solid #bfbfbf;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;-moz-border-radius:0 0 2px 2px;-webkit-border-radius:0 0 2px 2px;border-radius:0 0 2px 2px;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#cfd1cf));background-image:-moz-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-webkit-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-o-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-ms-linear-gradient(top,#ebebeb,#cfd1cf);background-image:linear-gradient(top,#ebebeb,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ebebeb',endColorstr='#cfd1cf')}.cke_rtl .cke_dialog_footer{text-align:left}.cke_hc .cke_dialog_footer{outline:0;border-top:1px solid #fff}.cke_dialog .cke_resizer{margin-top:22px}.cke_dialog .cke_resizer_rtl{margin-left:5px}.cke_dialog .cke_resizer_ltr{margin-right:5px}.cke_dialog_tabs{height:24px;display:inline-block;margin:5px 0 0;position:absolute;z-index:2;left:10px}.cke_rtl .cke_dialog_tabs{right:10px}a.cke_dialog_tab{height:16px;padding:4px 8px;margin-right:3px;display:inline-block;cursor:pointer;line-height:16px;outline:0;color:#595959;border:1px solid #bfbfbf;-moz-border-radius:3px 3px 0 0;-webkit-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;background:#d4d4d4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fafafa),to(#ededed));background-image:-moz-linear-gradient(top,#fafafa,#ededed);background-image:-webkit-linear-gradient(top,#fafafa,#ededed);background-image:-o-linear-gradient(top,#fafafa,#ededed);background-image:-ms-linear-gradient(top,#fafafa,#ededed);background-image:linear-gradient(top,#fafafa,#ededed);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#fafafa',endColorstr='#ededed')}.cke_rtl a.cke_dialog_tab{margin-right:0;margin-left:3px}a.cke_dialog_tab:hover{background:#ebebeb;background:-moz-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ebebeb),color-stop(100%,#dfdfdf));background:-webkit-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-o-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-ms-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:linear-gradient(to bottom,#ebebeb 0,#dfdfdf 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ebebeb',endColorstr='#dfdfdf',GradientType=0)}a.cke_dialog_tab_selected{background:#fff;color:#383838;border-bottom-color:#fff;cursor:default;filter:none}a.cke_dialog_tab_selected:hover{background:#ededed;background:-moz-linear-gradient(top,#ededed 0,#fff 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ededed),color-stop(100%,#fff));background:-webkit-linear-gradient(top,#ededed 0,#fff 100%);background:-o-linear-gradient(top,#ededed 0,#fff 100%);background:-ms-linear-gradient(top,#ededed 0,#fff 100%);background:linear-gradient(to bottom,#ededed 0,#fff 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ededed',endColorstr='#ffffff',GradientType=0)}.cke_hc a.cke_dialog_tab:hover,.cke_hc a.cke_dialog_tab_selected{border:3px solid;padding:2px 6px}a.cke_dialog_tab_disabled{color:#bababa;cursor:default}.cke_single_page .cke_dialog_tabs{display:none}.cke_single_page .cke_dialog_contents{padding-top:5px;margin-top:0;border-top:0}.cke_dialog_close_button{background-image:url(images/close.png);background-repeat:no-repeat;background-position:0 0;position:absolute;cursor:pointer;text-align:center;height:20px;width:20px;top:5px;z-index:5}.cke_hidpi .cke_dialog_close_button{background-image:url(images/hidpi/close.png);background-size:16px}.cke_dialog_close_button span{display:none}.cke_hc .cke_dialog_close_button span{display:inline;cursor:pointer;font-weight:bold;position:relative;top:3px}.cke_ltr .cke_dialog_close_button{right:5px}.cke_rtl .cke_dialog_close_button{left:6px}.cke_dialog_close_button{top:4px}div.cke_disabled .cke_dialog_ui_labeled_content div *{background-color:#ddd;cursor:default}.cke_dialog_ui_vbox table,.cke_dialog_ui_hbox table{margin:auto}.cke_dialog_ui_vbox_child{padding:5px 0}.cke_dialog_ui_hbox{width:100%}.cke_dialog_ui_hbox_first,.cke_dialog_ui_hbox_child,.cke_dialog_ui_hbox_last{vertical-align:top}.cke_ltr .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_ui_hbox_child{padding-right:10px}.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_child{padding-left:10px}.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-right:5px}.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-left:5px;padding-right:0}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:1px solid}textarea.cke_dialog_ui_input_textarea{overflow:auto;resize:none}input.cke_dialog_ui_input_text,input.cke_dialog_ui_input_password,textarea.cke_dialog_ui_input_textarea{background-color:#fff;border:1px solid #c9cccf;border-top-color:#aeb3b9;padding:4px 6px;outline:0;width:100%;*width:95%;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;box-shadow:0 1px 2px rgba(0,0,0,.15) inset}input.cke_dialog_ui_input_text:hover,input.cke_dialog_ui_input_password:hover,textarea.cke_dialog_ui_input_textarea:hover{border:1px solid #aeb3b9;border-top-color:#a0a6ad}input.cke_dialog_ui_input_text:focus,input.cke_dialog_ui_input_password:focus,textarea.cke_dialog_ui_input_textarea:focus,select.cke_dialog_ui_input_select:focus{outline:0;border:1px solid #139ff7;border-top-color:#1392e9}a.cke_dialog_ui_button{display:inline-block;*display:inline;*zoom:1;padding:3px 0;margin:0;text-align:center;color:#333;vertical-align:middle;cursor:pointer;border:1px solid #b6b6b6;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}span.cke_dialog_ui_button{padding:0 12px}a.cke_dialog_ui_button:hover{border-color:#9e9e9e;background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}a.cke_dialog_ui_button:focus,a.cke_dialog_ui_button:active{border-color:#969696;outline:0;-moz-box-shadow:0 0 6px rgba(0,0,0,.4) inset;-webkit-box-shadow:0 0 6px rgba(0,0,0,.4) inset;box-shadow:0 0 6px rgba(0,0,0,.4) inset}.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button:focus,.cke_hc a.cke_dialog_ui_button:active{border:3px solid;padding-top:1px;padding-bottom:1px}.cke_hc a.cke_dialog_ui_button:hover span,.cke_hc a.cke_dialog_ui_button:focus span,.cke_hc a.cke_dialog_ui_button:active span{padding-left:10px;padding-right:10px}.cke_dialog_footer_buttons a.cke_dialog_ui_button span{color:inherit;font-size:12px;font-weight:bold;line-height:20px}a.cke_dialog_ui_button_ok{color:#fff;text-shadow:0 -1px 0 #55830c;border-color:#62a60a #62a60a #4d9200;background:#69b10b;background-image:-webkit-gradient(linear,0 0,0 100%,from(#9ad717),to(#69b10b));background-image:-webkit-linear-gradient(top,#9ad717,#69b10b);background-image:-o-linear-gradient(top,#9ad717,#69b10b);background-image:linear-gradient(to bottom,#9ad717,#69b10b);background-image:-moz-linear-gradient(top,#9ad717,#69b10b);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#9ad717',endColorstr='#69b10b')}a.cke_dialog_ui_button_ok:hover{border-color:#5b9909 #5b9909 #478500;background:#88be14;background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#88be14),color-stop(100%,#5d9c0a));background:-webkit-linear-gradient(top,#88be14 0,#5d9c0a 100%);background:-o-linear-gradient(top,#88be14 0,#5d9c0a 100%);background:linear-gradient(to bottom,#88be14 0,#5d9c0a 100%);background:-moz-linear-gradient(top,#88be14 0,#5d9c0a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#88be14',endColorstr='#5d9c0a',GradientType=0)}a.cke_dialog_ui_button span{text-shadow:0 1px 0 #fff}a.cke_dialog_ui_button_ok span{text-shadow:0 -1px 0 #55830c}span.cke_dialog_ui_button{cursor:pointer}a.cke_dialog_ui_button_ok:focus,a.cke_dialog_ui_button_ok:active,a.cke_dialog_ui_button_cancel:focus,a.cke_dialog_ui_button_cancel:active{border-width:2px;padding:2px 0}a.cke_dialog_ui_button_ok:focus,a.cke_dialog_ui_button_ok:active{border-color:#568c0a}a.cke_dialog_ui_button_ok:focus span,a.cke_dialog_ui_button_ok:active span,a.cke_dialog_ui_button_cancel:focus span,a.cke_dialog_ui_button_cancel:active span{padding:0 11px}.cke_dialog_footer_buttons{display:inline-table;margin:5px;width:auto;position:relative;vertical-align:middle}div.cke_dialog_ui_input_select{display:table}select.cke_dialog_ui_input_select{height:24px;line-height:24px;background-color:#fff;border:1px solid #c9cccf;border-top-color:#aeb3b9;padding:2px 6px;outline:0;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;box-shadow:0 1px 2px rgba(0,0,0,.15) inset}.cke_dialog_ui_input_file{width:100%;height:25px}.cke_hc .cke_dialog_ui_labeled_content input:focus,.cke_hc .cke_dialog_ui_labeled_content select:focus,.cke_hc .cke_dialog_ui_labeled_content textarea:focus{outline:1px dotted}.cke_dialog .cke_dark_background{background-color:#dedede}.cke_dialog .cke_light_background{background-color:#ebebeb}.cke_dialog .cke_centered{text-align:center}.cke_dialog a.cke_btn_reset{float:right;background:url(images/refresh.png) top left no-repeat;width:16px;height:16px;border:1px none;font-size:1px}.cke_hidpi .cke_dialog a.cke_btn_reset{background-size:16px;background-image:url(images/hidpi/refresh.png)}.cke_rtl .cke_dialog a.cke_btn_reset{float:left}.cke_dialog a.cke_btn_locked,.cke_dialog a.cke_btn_unlocked{float:left;width:16px;height:16px;background-repeat:no-repeat;border:none 1px;font-size:1px}.cke_dialog a.cke_btn_locked .cke_icon{display:none}.cke_rtl .cke_dialog a.cke_btn_locked,.cke_rtl .cke_dialog a.cke_btn_unlocked{float:right}.cke_dialog a.cke_btn_locked{background-image:url(images/lock.png)}.cke_dialog a.cke_btn_unlocked{background-image:url(images/lock-open.png)}.cke_hidpi .cke_dialog a.cke_btn_unlocked,.cke_hidpi .cke_dialog a.cke_btn_locked{background-size:16px}.cke_hidpi .cke_dialog a.cke_btn_locked{background-image:url(images/hidpi/lock.png)}.cke_hidpi .cke_dialog a.cke_btn_unlocked{background-image:url(images/hidpi/lock-open.png)}.cke_dialog .cke_btn_over{border:outset 1px;cursor:pointer}.cke_dialog .ImagePreviewBox{border:2px ridge black;overflow:scroll;height:200px;width:300px;padding:2px;background-color:white}.cke_dialog .ImagePreviewBox table td{white-space:normal}.cke_dialog .ImagePreviewLoader{position:absolute;white-space:normal;overflow:hidden;height:160px;width:230px;margin:2px;padding:2px;opacity:.9;filter:alpha(opacity = 90);background-color:#e4e4e4}.cke_dialog .FlashPreviewBox{white-space:normal;border:2px ridge black;overflow:auto;height:160px;width:390px;padding:2px;background-color:white}.cke_dialog .cke_pastetext{width:346px;height:170px}.cke_dialog .cke_pastetext textarea{width:340px;height:170px;resize:none}.cke_dialog iframe.cke_pasteframe{width:346px;height:130px;background-color:white;border:1px solid #aeb3b9;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px}.cke_dialog .cke_hand{cursor:pointer}.cke_disabled{color:#a0a0a0}.cke_dialog_body .cke_label{display:none}.cke_dialog_body label{display:inline;margin-bottom:auto;cursor:default}.cke_dialog_body label.cke_required{font-weight:bold}a.cke_smile{overflow:hidden;display:block;text-align:center;padding:.3em 0}a.cke_smile img{vertical-align:middle}a.cke_specialchar{cursor:inherit;display:block;height:1.25em;padding:.2em .3em;text-align:center}a.cke_smile,a.cke_specialchar{border:1px solid transparent}a.cke_smile:hover,a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:hover,a.cke_specialchar:focus,a.cke_specialchar:active{background:#fff;outline:0}a.cke_smile:hover,a.cke_specialchar:hover{border-color:#888}a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:focus,a.cke_specialchar:active{border-color:#139ff7}.cke_dialog_contents a.colorChooser{display:block;margin-top:6px;margin-left:10px;width:80px}.cke_rtl .cke_dialog_contents a.colorChooser{margin-right:10px}.cke_dialog_ui_checkbox_input:focus,.cke_dialog_ui_radio_input:focus,.cke_btn_over{outline:1px dotted #696969}.cke_iframe_shim{display:block;position:absolute;top:0;left:0;z-index:-1;filter:alpha(opacity = 0);width:100%;height:100%}.cke_rtl input.cke_dialog_ui_input_text,.cke_rtl input.cke_dialog_ui_input_password{padding-right:2px}.cke_rtl div.cke_dialog_ui_input_text,.cke_rtl div.cke_dialog_ui_input_password{padding-left:2px}.cke_rtl div.cke_dialog_ui_input_text{padding-right:1px}.cke_rtl .cke_dialog_ui_vbox_child,.cke_rtl .cke_dialog_ui_hbox_child,.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_last{padding-right:2px!important}.cke_hc .cke_dialog_title,.cke_hc .cke_dialog_footer,.cke_hc a.cke_dialog_tab,.cke_hc a.cke_dialog_ui_button,.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button_ok,.cke_hc a.cke_dialog_ui_button_ok:hover{filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:0}a.cke_dialog_ui_button_ok:focus span,a.cke_dialog_ui_button_ok:active span,a.cke_dialog_ui_button_cancel:focus span,a.cke_dialog_ui_button_cancel:active span{display:block}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono/dialog_iequirks.css b/js/ckeditor/skins/moono/dialog_iequirks.css
deleted file mode 100644 (file)
index 3c10814..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-/*\r
-Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
-For licensing, see LICENSE.md or http://ckeditor.com/license\r
-*/\r
-.cke_dialog{visibility:visible}.cke_dialog_body{z-index:1;background:#eaeaea;border:1px solid #b2b2b2;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_browser_gecko19 .cke_dialog_body{position:relative}.cke_dialog strong{font-weight:bold}.cke_dialog_title{font-weight:bold;font-size:13px;cursor:move;position:relative;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.75);border-bottom:1px solid #999;padding:6px 10px;-moz-border-radius:2px 2px 0 0;-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_dialog_contents{background-color:#fff;overflow:auto;padding:15px 10px 5px 10px;margin-top:30px;border-top:1px solid #bfbfbf;-moz-border-radius:0 0 3px 3px;-webkit-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px}.cke_dialog_contents_body{overflow:auto;padding:17px 10px 5px 10px;margin-top:22px}.cke_dialog_footer{text-align:right;position:relative;border:0;outline:1px solid #bfbfbf;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;-moz-border-radius:0 0 2px 2px;-webkit-border-radius:0 0 2px 2px;border-radius:0 0 2px 2px;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#cfd1cf));background-image:-moz-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-webkit-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-o-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-ms-linear-gradient(top,#ebebeb,#cfd1cf);background-image:linear-gradient(top,#ebebeb,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ebebeb',endColorstr='#cfd1cf')}.cke_rtl .cke_dialog_footer{text-align:left}.cke_hc .cke_dialog_footer{outline:0;border-top:1px solid #fff}.cke_dialog .cke_resizer{margin-top:22px}.cke_dialog .cke_resizer_rtl{margin-left:5px}.cke_dialog .cke_resizer_ltr{margin-right:5px}.cke_dialog_tabs{height:24px;display:inline-block;margin:5px 0 0;position:absolute;z-index:2;left:10px}.cke_rtl .cke_dialog_tabs{right:10px}a.cke_dialog_tab{height:16px;padding:4px 8px;margin-right:3px;display:inline-block;cursor:pointer;line-height:16px;outline:0;color:#595959;border:1px solid #bfbfbf;-moz-border-radius:3px 3px 0 0;-webkit-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;background:#d4d4d4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fafafa),to(#ededed));background-image:-moz-linear-gradient(top,#fafafa,#ededed);background-image:-webkit-linear-gradient(top,#fafafa,#ededed);background-image:-o-linear-gradient(top,#fafafa,#ededed);background-image:-ms-linear-gradient(top,#fafafa,#ededed);background-image:linear-gradient(top,#fafafa,#ededed);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#fafafa',endColorstr='#ededed')}.cke_rtl a.cke_dialog_tab{margin-right:0;margin-left:3px}a.cke_dialog_tab:hover{background:#ebebeb;background:-moz-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ebebeb),color-stop(100%,#dfdfdf));background:-webkit-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-o-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-ms-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:linear-gradient(to bottom,#ebebeb 0,#dfdfdf 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ebebeb',endColorstr='#dfdfdf',GradientType=0)}a.cke_dialog_tab_selected{background:#fff;color:#383838;border-bottom-color:#fff;cursor:default;filter:none}a.cke_dialog_tab_selected:hover{background:#ededed;background:-moz-linear-gradient(top,#ededed 0,#fff 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ededed),color-stop(100%,#fff));background:-webkit-linear-gradient(top,#ededed 0,#fff 100%);background:-o-linear-gradient(top,#ededed 0,#fff 100%);background:-ms-linear-gradient(top,#ededed 0,#fff 100%);background:linear-gradient(to bottom,#ededed 0,#fff 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ededed',endColorstr='#ffffff',GradientType=0)}.cke_hc a.cke_dialog_tab:hover,.cke_hc a.cke_dialog_tab_selected{border:3px solid;padding:2px 6px}a.cke_dialog_tab_disabled{color:#bababa;cursor:default}.cke_single_page .cke_dialog_tabs{display:none}.cke_single_page .cke_dialog_contents{padding-top:5px;margin-top:0;border-top:0}.cke_dialog_close_button{background-image:url(images/close.png);background-repeat:no-repeat;background-position:0 0;position:absolute;cursor:pointer;text-align:center;height:20px;width:20px;top:5px;z-index:5}.cke_hidpi .cke_dialog_close_button{background-image:url(images/hidpi/close.png);background-size:16px}.cke_dialog_close_button span{display:none}.cke_hc .cke_dialog_close_button span{display:inline;cursor:pointer;font-weight:bold;position:relative;top:3px}.cke_ltr .cke_dialog_close_button{right:5px}.cke_rtl .cke_dialog_close_button{left:6px}.cke_dialog_close_button{top:4px}div.cke_disabled .cke_dialog_ui_labeled_content div *{background-color:#ddd;cursor:default}.cke_dialog_ui_vbox table,.cke_dialog_ui_hbox table{margin:auto}.cke_dialog_ui_vbox_child{padding:5px 0}.cke_dialog_ui_hbox{width:100%}.cke_dialog_ui_hbox_first,.cke_dialog_ui_hbox_child,.cke_dialog_ui_hbox_last{vertical-align:top}.cke_ltr .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_ui_hbox_child{padding-right:10px}.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_child{padding-left:10px}.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-right:5px}.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-left:5px;padding-right:0}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:1px solid}textarea.cke_dialog_ui_input_textarea{overflow:auto;resize:none}input.cke_dialog_ui_input_text,input.cke_dialog_ui_input_password,textarea.cke_dialog_ui_input_textarea{background-color:#fff;border:1px solid #c9cccf;border-top-color:#aeb3b9;padding:4px 6px;outline:0;width:100%;*width:95%;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;box-shadow:0 1px 2px rgba(0,0,0,.15) inset}input.cke_dialog_ui_input_text:hover,input.cke_dialog_ui_input_password:hover,textarea.cke_dialog_ui_input_textarea:hover{border:1px solid #aeb3b9;border-top-color:#a0a6ad}input.cke_dialog_ui_input_text:focus,input.cke_dialog_ui_input_password:focus,textarea.cke_dialog_ui_input_textarea:focus,select.cke_dialog_ui_input_select:focus{outline:0;border:1px solid #139ff7;border-top-color:#1392e9}a.cke_dialog_ui_button{display:inline-block;*display:inline;*zoom:1;padding:3px 0;margin:0;text-align:center;color:#333;vertical-align:middle;cursor:pointer;border:1px solid #b6b6b6;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}span.cke_dialog_ui_button{padding:0 12px}a.cke_dialog_ui_button:hover{border-color:#9e9e9e;background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}a.cke_dialog_ui_button:focus,a.cke_dialog_ui_button:active{border-color:#969696;outline:0;-moz-box-shadow:0 0 6px rgba(0,0,0,.4) inset;-webkit-box-shadow:0 0 6px rgba(0,0,0,.4) inset;box-shadow:0 0 6px rgba(0,0,0,.4) inset}.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button:focus,.cke_hc a.cke_dialog_ui_button:active{border:3px solid;padding-top:1px;padding-bottom:1px}.cke_hc a.cke_dialog_ui_button:hover span,.cke_hc a.cke_dialog_ui_button:focus span,.cke_hc a.cke_dialog_ui_button:active span{padding-left:10px;padding-right:10px}.cke_dialog_footer_buttons a.cke_dialog_ui_button span{color:inherit;font-size:12px;font-weight:bold;line-height:20px}a.cke_dialog_ui_button_ok{color:#fff;text-shadow:0 -1px 0 #55830c;border-color:#62a60a #62a60a #4d9200;background:#69b10b;background-image:-webkit-gradient(linear,0 0,0 100%,from(#9ad717),to(#69b10b));background-image:-webkit-linear-gradient(top,#9ad717,#69b10b);background-image:-o-linear-gradient(top,#9ad717,#69b10b);background-image:linear-gradient(to bottom,#9ad717,#69b10b);background-image:-moz-linear-gradient(top,#9ad717,#69b10b);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#9ad717',endColorstr='#69b10b')}a.cke_dialog_ui_button_ok:hover{border-color:#5b9909 #5b9909 #478500;background:#88be14;background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#88be14),color-stop(100%,#5d9c0a));background:-webkit-linear-gradient(top,#88be14 0,#5d9c0a 100%);background:-o-linear-gradient(top,#88be14 0,#5d9c0a 100%);background:linear-gradient(to bottom,#88be14 0,#5d9c0a 100%);background:-moz-linear-gradient(top,#88be14 0,#5d9c0a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#88be14',endColorstr='#5d9c0a',GradientType=0)}a.cke_dialog_ui_button span{text-shadow:0 1px 0 #fff}a.cke_dialog_ui_button_ok span{text-shadow:0 -1px 0 #55830c}span.cke_dialog_ui_button{cursor:pointer}a.cke_dialog_ui_button_ok:focus,a.cke_dialog_ui_button_ok:active,a.cke_dialog_ui_button_cancel:focus,a.cke_dialog_ui_button_cancel:active{border-width:2px;padding:2px 0}a.cke_dialog_ui_button_ok:focus,a.cke_dialog_ui_button_ok:active{border-color:#568c0a}a.cke_dialog_ui_button_ok:focus span,a.cke_dialog_ui_button_ok:active span,a.cke_dialog_ui_button_cancel:focus span,a.cke_dialog_ui_button_cancel:active span{padding:0 11px}.cke_dialog_footer_buttons{display:inline-table;margin:5px;width:auto;position:relative;vertical-align:middle}div.cke_dialog_ui_input_select{display:table}select.cke_dialog_ui_input_select{height:24px;line-height:24px;background-color:#fff;border:1px solid #c9cccf;border-top-color:#aeb3b9;padding:2px 6px;outline:0;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;box-shadow:0 1px 2px rgba(0,0,0,.15) inset}.cke_dialog_ui_input_file{width:100%;height:25px}.cke_hc .cke_dialog_ui_labeled_content input:focus,.cke_hc .cke_dialog_ui_labeled_content select:focus,.cke_hc .cke_dialog_ui_labeled_content textarea:focus{outline:1px dotted}.cke_dialog .cke_dark_background{background-color:#dedede}.cke_dialog .cke_light_background{background-color:#ebebeb}.cke_dialog .cke_centered{text-align:center}.cke_dialog a.cke_btn_reset{float:right;background:url(images/refresh.png) top left no-repeat;width:16px;height:16px;border:1px none;font-size:1px}.cke_hidpi .cke_dialog a.cke_btn_reset{background-size:16px;background-image:url(images/hidpi/refresh.png)}.cke_rtl .cke_dialog a.cke_btn_reset{float:left}.cke_dialog a.cke_btn_locked,.cke_dialog a.cke_btn_unlocked{float:left;width:16px;height:16px;background-repeat:no-repeat;border:none 1px;font-size:1px}.cke_dialog a.cke_btn_locked .cke_icon{display:none}.cke_rtl .cke_dialog a.cke_btn_locked,.cke_rtl .cke_dialog a.cke_btn_unlocked{float:right}.cke_dialog a.cke_btn_locked{background-image:url(images/lock.png)}.cke_dialog a.cke_btn_unlocked{background-image:url(images/lock-open.png)}.cke_hidpi .cke_dialog a.cke_btn_unlocked,.cke_hidpi .cke_dialog a.cke_btn_locked{background-size:16px}.cke_hidpi .cke_dialog a.cke_btn_locked{background-image:url(images/hidpi/lock.png)}.cke_hidpi .cke_dialog a.cke_btn_unlocked{background-image:url(images/hidpi/lock-open.png)}.cke_dialog .cke_btn_over{border:outset 1px;cursor:pointer}.cke_dialog .ImagePreviewBox{border:2px ridge black;overflow:scroll;height:200px;width:300px;padding:2px;background-color:white}.cke_dialog .ImagePreviewBox table td{white-space:normal}.cke_dialog .ImagePreviewLoader{position:absolute;white-space:normal;overflow:hidden;height:160px;width:230px;margin:2px;padding:2px;opacity:.9;filter:alpha(opacity = 90);background-color:#e4e4e4}.cke_dialog .FlashPreviewBox{white-space:normal;border:2px ridge black;overflow:auto;height:160px;width:390px;padding:2px;background-color:white}.cke_dialog .cke_pastetext{width:346px;height:170px}.cke_dialog .cke_pastetext textarea{width:340px;height:170px;resize:none}.cke_dialog iframe.cke_pasteframe{width:346px;height:130px;background-color:white;border:1px solid #aeb3b9;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px}.cke_dialog .cke_hand{cursor:pointer}.cke_disabled{color:#a0a0a0}.cke_dialog_body .cke_label{display:none}.cke_dialog_body label{display:inline;margin-bottom:auto;cursor:default}.cke_dialog_body label.cke_required{font-weight:bold}a.cke_smile{overflow:hidden;display:block;text-align:center;padding:.3em 0}a.cke_smile img{vertical-align:middle}a.cke_specialchar{cursor:inherit;display:block;height:1.25em;padding:.2em .3em;text-align:center}a.cke_smile,a.cke_specialchar{border:1px solid transparent}a.cke_smile:hover,a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:hover,a.cke_specialchar:focus,a.cke_specialchar:active{background:#fff;outline:0}a.cke_smile:hover,a.cke_specialchar:hover{border-color:#888}a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:focus,a.cke_specialchar:active{border-color:#139ff7}.cke_dialog_contents a.colorChooser{display:block;margin-top:6px;margin-left:10px;width:80px}.cke_rtl .cke_dialog_contents a.colorChooser{margin-right:10px}.cke_dialog_ui_checkbox_input:focus,.cke_dialog_ui_radio_input:focus,.cke_btn_over{outline:1px dotted #696969}.cke_iframe_shim{display:block;position:absolute;top:0;left:0;z-index:-1;filter:alpha(opacity = 0);width:100%;height:100%}.cke_rtl input.cke_dialog_ui_input_text,.cke_rtl input.cke_dialog_ui_input_password{padding-right:2px}.cke_rtl div.cke_dialog_ui_input_text,.cke_rtl div.cke_dialog_ui_input_password{padding-left:2px}.cke_rtl div.cke_dialog_ui_input_text{padding-right:1px}.cke_rtl .cke_dialog_ui_vbox_child,.cke_rtl .cke_dialog_ui_hbox_child,.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_last{padding-right:2px!important}.cke_hc .cke_dialog_title,.cke_hc .cke_dialog_footer,.cke_hc a.cke_dialog_tab,.cke_hc a.cke_dialog_ui_button,.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button_ok,.cke_hc a.cke_dialog_ui_button_ok:hover{filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:0}.cke_dialog_footer{filter:""}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono/dialog_opera.css b/js/ckeditor/skins/moono/dialog_opera.css
deleted file mode 100644 (file)
index 36e9409..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-/*\r
-Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
-For licensing, see LICENSE.md or http://ckeditor.com/license\r
-*/\r
-.cke_dialog{visibility:visible}.cke_dialog_body{z-index:1;background:#eaeaea;border:1px solid #b2b2b2;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_browser_gecko19 .cke_dialog_body{position:relative}.cke_dialog strong{font-weight:bold}.cke_dialog_title{font-weight:bold;font-size:13px;cursor:move;position:relative;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.75);border-bottom:1px solid #999;padding:6px 10px;-moz-border-radius:2px 2px 0 0;-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_dialog_contents{background-color:#fff;overflow:auto;padding:15px 10px 5px 10px;margin-top:30px;border-top:1px solid #bfbfbf;-moz-border-radius:0 0 3px 3px;-webkit-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px}.cke_dialog_contents_body{overflow:auto;padding:17px 10px 5px 10px;margin-top:22px}.cke_dialog_footer{text-align:right;position:relative;border:0;outline:1px solid #bfbfbf;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;-moz-border-radius:0 0 2px 2px;-webkit-border-radius:0 0 2px 2px;border-radius:0 0 2px 2px;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#cfd1cf));background-image:-moz-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-webkit-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-o-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-ms-linear-gradient(top,#ebebeb,#cfd1cf);background-image:linear-gradient(top,#ebebeb,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ebebeb',endColorstr='#cfd1cf')}.cke_rtl .cke_dialog_footer{text-align:left}.cke_hc .cke_dialog_footer{outline:0;border-top:1px solid #fff}.cke_dialog .cke_resizer{margin-top:22px}.cke_dialog .cke_resizer_rtl{margin-left:5px}.cke_dialog .cke_resizer_ltr{margin-right:5px}.cke_dialog_tabs{height:24px;display:inline-block;margin:5px 0 0;position:absolute;z-index:2;left:10px}.cke_rtl .cke_dialog_tabs{right:10px}a.cke_dialog_tab{height:16px;padding:4px 8px;margin-right:3px;display:inline-block;cursor:pointer;line-height:16px;outline:0;color:#595959;border:1px solid #bfbfbf;-moz-border-radius:3px 3px 0 0;-webkit-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;background:#d4d4d4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fafafa),to(#ededed));background-image:-moz-linear-gradient(top,#fafafa,#ededed);background-image:-webkit-linear-gradient(top,#fafafa,#ededed);background-image:-o-linear-gradient(top,#fafafa,#ededed);background-image:-ms-linear-gradient(top,#fafafa,#ededed);background-image:linear-gradient(top,#fafafa,#ededed);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#fafafa',endColorstr='#ededed')}.cke_rtl a.cke_dialog_tab{margin-right:0;margin-left:3px}a.cke_dialog_tab:hover{background:#ebebeb;background:-moz-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ebebeb),color-stop(100%,#dfdfdf));background:-webkit-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-o-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:-ms-linear-gradient(top,#ebebeb 0,#dfdfdf 100%);background:linear-gradient(to bottom,#ebebeb 0,#dfdfdf 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ebebeb',endColorstr='#dfdfdf',GradientType=0)}a.cke_dialog_tab_selected{background:#fff;color:#383838;border-bottom-color:#fff;cursor:default;filter:none}a.cke_dialog_tab_selected:hover{background:#ededed;background:-moz-linear-gradient(top,#ededed 0,#fff 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ededed),color-stop(100%,#fff));background:-webkit-linear-gradient(top,#ededed 0,#fff 100%);background:-o-linear-gradient(top,#ededed 0,#fff 100%);background:-ms-linear-gradient(top,#ededed 0,#fff 100%);background:linear-gradient(to bottom,#ededed 0,#fff 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ededed',endColorstr='#ffffff',GradientType=0)}.cke_hc a.cke_dialog_tab:hover,.cke_hc a.cke_dialog_tab_selected{border:3px solid;padding:2px 6px}a.cke_dialog_tab_disabled{color:#bababa;cursor:default}.cke_single_page .cke_dialog_tabs{display:none}.cke_single_page .cke_dialog_contents{padding-top:5px;margin-top:0;border-top:0}.cke_dialog_close_button{background-image:url(images/close.png);background-repeat:no-repeat;background-position:0 0;position:absolute;cursor:pointer;text-align:center;height:20px;width:20px;top:5px;z-index:5}.cke_hidpi .cke_dialog_close_button{background-image:url(images/hidpi/close.png);background-size:16px}.cke_dialog_close_button span{display:none}.cke_hc .cke_dialog_close_button span{display:inline;cursor:pointer;font-weight:bold;position:relative;top:3px}.cke_ltr .cke_dialog_close_button{right:5px}.cke_rtl .cke_dialog_close_button{left:6px}.cke_dialog_close_button{top:4px}div.cke_disabled .cke_dialog_ui_labeled_content div *{background-color:#ddd;cursor:default}.cke_dialog_ui_vbox table,.cke_dialog_ui_hbox table{margin:auto}.cke_dialog_ui_vbox_child{padding:5px 0}.cke_dialog_ui_hbox{width:100%}.cke_dialog_ui_hbox_first,.cke_dialog_ui_hbox_child,.cke_dialog_ui_hbox_last{vertical-align:top}.cke_ltr .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_ui_hbox_child{padding-right:10px}.cke_rtl .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_ui_hbox_child{padding-left:10px}.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_ltr .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-right:5px}.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_first,.cke_rtl .cke_dialog_footer_buttons .cke_dialog_ui_hbox_child{padding-left:5px;padding-right:0}.cke_hc div.cke_dialog_ui_input_text,.cke_hc div.cke_dialog_ui_input_password,.cke_hc div.cke_dialog_ui_input_textarea,.cke_hc div.cke_dialog_ui_input_select,.cke_hc div.cke_dialog_ui_input_file{border:1px solid}textarea.cke_dialog_ui_input_textarea{overflow:auto;resize:none}input.cke_dialog_ui_input_text,input.cke_dialog_ui_input_password,textarea.cke_dialog_ui_input_textarea{background-color:#fff;border:1px solid #c9cccf;border-top-color:#aeb3b9;padding:4px 6px;outline:0;width:100%;*width:95%;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;box-shadow:0 1px 2px rgba(0,0,0,.15) inset}input.cke_dialog_ui_input_text:hover,input.cke_dialog_ui_input_password:hover,textarea.cke_dialog_ui_input_textarea:hover{border:1px solid #aeb3b9;border-top-color:#a0a6ad}input.cke_dialog_ui_input_text:focus,input.cke_dialog_ui_input_password:focus,textarea.cke_dialog_ui_input_textarea:focus,select.cke_dialog_ui_input_select:focus{outline:0;border:1px solid #139ff7;border-top-color:#1392e9}a.cke_dialog_ui_button{display:inline-block;*display:inline;*zoom:1;padding:3px 0;margin:0;text-align:center;color:#333;vertical-align:middle;cursor:pointer;border:1px solid #b6b6b6;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}span.cke_dialog_ui_button{padding:0 12px}a.cke_dialog_ui_button:hover{border-color:#9e9e9e;background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}a.cke_dialog_ui_button:focus,a.cke_dialog_ui_button:active{border-color:#969696;outline:0;-moz-box-shadow:0 0 6px rgba(0,0,0,.4) inset;-webkit-box-shadow:0 0 6px rgba(0,0,0,.4) inset;box-shadow:0 0 6px rgba(0,0,0,.4) inset}.cke_hc a.cke_dialog_ui_button:hover,.cke_hc a.cke_dialog_ui_button:focus,.cke_hc a.cke_dialog_ui_button:active{border:3px solid;padding-top:1px;padding-bottom:1px}.cke_hc a.cke_dialog_ui_button:hover span,.cke_hc a.cke_dialog_ui_button:focus span,.cke_hc a.cke_dialog_ui_button:active span{padding-left:10px;padding-right:10px}.cke_dialog_footer_buttons a.cke_dialog_ui_button span{color:inherit;font-size:12px;font-weight:bold;line-height:20px}a.cke_dialog_ui_button_ok{color:#fff;text-shadow:0 -1px 0 #55830c;border-color:#62a60a #62a60a #4d9200;background:#69b10b;background-image:-webkit-gradient(linear,0 0,0 100%,from(#9ad717),to(#69b10b));background-image:-webkit-linear-gradient(top,#9ad717,#69b10b);background-image:-o-linear-gradient(top,#9ad717,#69b10b);background-image:linear-gradient(to bottom,#9ad717,#69b10b);background-image:-moz-linear-gradient(top,#9ad717,#69b10b);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#9ad717',endColorstr='#69b10b')}a.cke_dialog_ui_button_ok:hover{border-color:#5b9909 #5b9909 #478500;background:#88be14;background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#88be14),color-stop(100%,#5d9c0a));background:-webkit-linear-gradient(top,#88be14 0,#5d9c0a 100%);background:-o-linear-gradient(top,#88be14 0,#5d9c0a 100%);background:linear-gradient(to bottom,#88be14 0,#5d9c0a 100%);background:-moz-linear-gradient(top,#88be14 0,#5d9c0a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#88be14',endColorstr='#5d9c0a',GradientType=0)}a.cke_dialog_ui_button span{text-shadow:0 1px 0 #fff}a.cke_dialog_ui_button_ok span{text-shadow:0 -1px 0 #55830c}span.cke_dialog_ui_button{cursor:pointer}a.cke_dialog_ui_button_ok:focus,a.cke_dialog_ui_button_ok:active,a.cke_dialog_ui_button_cancel:focus,a.cke_dialog_ui_button_cancel:active{border-width:2px;padding:2px 0}a.cke_dialog_ui_button_ok:focus,a.cke_dialog_ui_button_ok:active{border-color:#568c0a}a.cke_dialog_ui_button_ok:focus span,a.cke_dialog_ui_button_ok:active span,a.cke_dialog_ui_button_cancel:focus span,a.cke_dialog_ui_button_cancel:active span{padding:0 11px}.cke_dialog_footer_buttons{display:inline-table;margin:5px;width:auto;position:relative;vertical-align:middle}div.cke_dialog_ui_input_select{display:table}select.cke_dialog_ui_input_select{height:24px;line-height:24px;background-color:#fff;border:1px solid #c9cccf;border-top-color:#aeb3b9;padding:2px 6px;outline:0;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.15) inset;box-shadow:0 1px 2px rgba(0,0,0,.15) inset}.cke_dialog_ui_input_file{width:100%;height:25px}.cke_hc .cke_dialog_ui_labeled_content input:focus,.cke_hc .cke_dialog_ui_labeled_content select:focus,.cke_hc .cke_dialog_ui_labeled_content textarea:focus{outline:1px dotted}.cke_dialog .cke_dark_background{background-color:#dedede}.cke_dialog .cke_light_background{background-color:#ebebeb}.cke_dialog .cke_centered{text-align:center}.cke_dialog a.cke_btn_reset{float:right;background:url(images/refresh.png) top left no-repeat;width:16px;height:16px;border:1px none;font-size:1px}.cke_hidpi .cke_dialog a.cke_btn_reset{background-size:16px;background-image:url(images/hidpi/refresh.png)}.cke_rtl .cke_dialog a.cke_btn_reset{float:left}.cke_dialog a.cke_btn_locked,.cke_dialog a.cke_btn_unlocked{float:left;width:16px;height:16px;background-repeat:no-repeat;border:none 1px;font-size:1px}.cke_dialog a.cke_btn_locked .cke_icon{display:none}.cke_rtl .cke_dialog a.cke_btn_locked,.cke_rtl .cke_dialog a.cke_btn_unlocked{float:right}.cke_dialog a.cke_btn_locked{background-image:url(images/lock.png)}.cke_dialog a.cke_btn_unlocked{background-image:url(images/lock-open.png)}.cke_hidpi .cke_dialog a.cke_btn_unlocked,.cke_hidpi .cke_dialog a.cke_btn_locked{background-size:16px}.cke_hidpi .cke_dialog a.cke_btn_locked{background-image:url(images/hidpi/lock.png)}.cke_hidpi .cke_dialog a.cke_btn_unlocked{background-image:url(images/hidpi/lock-open.png)}.cke_dialog .cke_btn_over{border:outset 1px;cursor:pointer}.cke_dialog .ImagePreviewBox{border:2px ridge black;overflow:scroll;height:200px;width:300px;padding:2px;background-color:white}.cke_dialog .ImagePreviewBox table td{white-space:normal}.cke_dialog .ImagePreviewLoader{position:absolute;white-space:normal;overflow:hidden;height:160px;width:230px;margin:2px;padding:2px;opacity:.9;filter:alpha(opacity = 90);background-color:#e4e4e4}.cke_dialog .FlashPreviewBox{white-space:normal;border:2px ridge black;overflow:auto;height:160px;width:390px;padding:2px;background-color:white}.cke_dialog .cke_pastetext{width:346px;height:170px}.cke_dialog .cke_pastetext textarea{width:340px;height:170px;resize:none}.cke_dialog iframe.cke_pasteframe{width:346px;height:130px;background-color:white;border:1px solid #aeb3b9;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px}.cke_dialog .cke_hand{cursor:pointer}.cke_disabled{color:#a0a0a0}.cke_dialog_body .cke_label{display:none}.cke_dialog_body label{display:inline;margin-bottom:auto;cursor:default}.cke_dialog_body label.cke_required{font-weight:bold}a.cke_smile{overflow:hidden;display:block;text-align:center;padding:.3em 0}a.cke_smile img{vertical-align:middle}a.cke_specialchar{cursor:inherit;display:block;height:1.25em;padding:.2em .3em;text-align:center}a.cke_smile,a.cke_specialchar{border:1px solid transparent}a.cke_smile:hover,a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:hover,a.cke_specialchar:focus,a.cke_specialchar:active{background:#fff;outline:0}a.cke_smile:hover,a.cke_specialchar:hover{border-color:#888}a.cke_smile:focus,a.cke_smile:active,a.cke_specialchar:focus,a.cke_specialchar:active{border-color:#139ff7}.cke_dialog_contents a.colorChooser{display:block;margin-top:6px;margin-left:10px;width:80px}.cke_rtl .cke_dialog_contents a.colorChooser{margin-right:10px}.cke_dialog_ui_checkbox_input:focus,.cke_dialog_ui_radio_input:focus,.cke_btn_over{outline:1px dotted #696969}.cke_iframe_shim{display:block;position:absolute;top:0;left:0;z-index:-1;filter:alpha(opacity = 0);width:100%;height:100%}.cke_dialog_footer{display:block;height:38px}.cke_ltr .cke_dialog_footer>*{float:right}.cke_rtl .cke_dialog_footer>*{float:left}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono/editor.css b/js/ckeditor/skins/moono/editor.css
deleted file mode 100644 (file)
index c0d3a60..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-/*\r
-Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
-For licensing, see LICENSE.md or http://ckeditor.com/license\r
-*/\r
-.cke_reset{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;position:static;-webkit-transition:none;-moz-transition:none;-ms-transition:none;transition:none}.cke_reset_all,.cke_reset_all *{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;position:static;-webkit-transition:none;-moz-transition:none;-ms-transition:none;transition:none;border-collapse:collapse;font:normal normal normal 12px Arial,Helvetica,Tahoma,Verdana,Sans-Serif;color:#000;text-align:left;white-space:nowrap;cursor:auto}.cke_reset_all .cke_rtl *{text-align:right}.cke_reset_all iframe{vertical-align:inherit}.cke_reset_all textarea{white-space:pre}.cke_reset_all textarea,.cke_reset_all input[type="text"],.cke_reset_all input[type="password"]{cursor:text}.cke_reset_all textarea[disabled],.cke_reset_all input[type="text"][disabled],.cke_reset_all input[type="password"][disabled]{cursor:default}.cke_reset_all fieldset{padding:10px;border:2px groove #e0dfe3}.cke_reset_all select{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.cke_chrome{display:block;border:1px solid #b6b6b6;padding:0;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_inner{display:block;-webkit-touch-callout:none;background:#fff;padding:0}.cke_float{border:0}.cke_float .cke_inner{padding-bottom:0}.cke_top,.cke_contents,.cke_bottom{display:block;overflow:hidden}.cke_top{border-bottom:1px solid #b6b6b6;padding:6px 8px 2px;white-space:normal;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_float .cke_top{border:1px solid #b6b6b6;border-bottom-color:#999}.cke_bottom{padding:6px 8px 2px;position:relative;border-top:1px solid #bfbfbf;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#cfd1cf));background-image:-moz-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-webkit-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-o-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-ms-linear-gradient(top,#ebebeb,#cfd1cf);background-image:linear-gradient(top,#ebebeb,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ebebeb',endColorstr='#cfd1cf')}.cke_browser_ios .cke_contents{overflow-y:auto;-webkit-overflow-scrolling:touch}.cke_resizer{width:0;height:0;overflow:hidden;width:0;height:0;overflow:hidden;border-width:10px 10px 0 0;border-color:transparent #666 transparent transparent;border-style:dashed solid dashed dashed;font-size:0;vertical-align:bottom;margin-top:6px;margin-bottom:2px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.3);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.3);box-shadow:0 1px 0 rgba(255,255,255,.3)}.cke_hc .cke_resizer{font-size:15px;width:auto;height:auto;border-width:0}.cke_resizer_ltr{cursor:se-resize;float:right;margin-right:-4px}.cke_resizer_rtl{border-width:10px 0 0 10px;border-color:transparent transparent transparent #a5a5a5;border-style:dashed dashed dashed solid;cursor:sw-resize;float:left;margin-left:-4px;right:auto}.cke_wysiwyg_div{display:block;height:100%;overflow:auto;padding:0 8px;outline-style:none;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.cke_panel{visibility:visible;width:120px;height:100px;overflow:hidden;background-color:#fff;border:1px solid #b6b6b6;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_menu_panel{padding:0;margin:0}.cke_combopanel{width:150px;height:170px}.cke_panel_frame{width:100%;height:100%;font-size:12px;overflow:auto;overflow-x:hidden}.cke_panel_container{overflow-y:auto;overflow-x:hidden}.cke_panel_list{list-style-type:none;margin:3px;padding:0;white-space:nowrap}.cke_panel_listItem{margin:0;padding-bottom:1px}.cke_panel_listItem a{padding:3px 4px;display:block;border:1px solid #fff;color:inherit!important;text-decoration:none;overflow:hidden;text-overflow:ellipsis;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px}* html .cke_panel_listItem a{width:100%;color:#000}*:first-child+html .cke_panel_listItem a{color:#000}.cke_panel_listItem.cke_selected a{border:1px solid #dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_panel_listItem a:hover,.cke_panel_listItem a:focus,.cke_panel_listItem a:active{border-color:#dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_hc .cke_panel_listItem a{border-style:none}.cke_hc .cke_panel_listItem a:hover,.cke_hc .cke_panel_listItem a:focus,.cke_hc .cke_panel_listItem a:active{border:2px solid;padding:1px 2px}.cke_panel_grouptitle{font-size:11px;font-weight:bold;white-space:nowrap;margin:0;padding:4px 6px;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.75);border-bottom:1px solid #b6b6b6;-moz-border-radius:2px 2px 0 0;-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_panel_listItem p,.cke_panel_listItem h1,.cke_panel_listItem h2,.cke_panel_listItem h3,.cke_panel_listItem h4,.cke_panel_listItem h5,.cke_panel_listItem h6,.cke_panel_listItem pre{margin-top:0;margin-bottom:0}.cke_colorblock{padding:3px;font-size:11px;font-family:'Microsoft Sans Serif',Tahoma,Arial,Verdana,Sans-Serif}.cke_colorblock,.cke_colorblock a{text-decoration:none;color:#000}span.cke_colorbox{width:10px;height:10px;border:#808080 1px solid;float:left}.cke_rtl span.cke_colorbox{float:right}a.cke_colorbox{border:#fff 1px solid;padding:2px;float:left;width:12px;height:12px}.cke_rtl a.cke_colorbox{float:right}a:hover.cke_colorbox,a:focus.cke_colorbox,a:active.cke_colorbox{border:#b6b6b6 1px solid;background-color:#e5e5e5}a.cke_colorauto,a.cke_colormore{border:#fff 1px solid;padding:2px;display:block;cursor:pointer}a:hover.cke_colorauto,a:hover.cke_colormore,a:focus.cke_colorauto,a:focus.cke_colormore,a:active.cke_colorauto,a:active.cke_colormore{border:#b6b6b6 1px solid;background-color:#e5e5e5}.cke_toolbar{float:left}.cke_rtl .cke_toolbar{float:right}.cke_toolgroup{float:left;margin:0 6px 5px 0;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_hc .cke_toolgroup{border:0;margin-right:10px;margin-bottom:10px}.cke_rtl .cke_toolgroup *:first-child{-moz-border-radius:0 2px 2px 0;-webkit-border-radius:0 2px 2px 0;border-radius:0 2px 2px 0}.cke_rtl .cke_toolgroup *:last-child{-moz-border-radius:2px 0 0 2px;-webkit-border-radius:2px 0 0 2px;border-radius:2px 0 0 2px}.cke_rtl .cke_toolgroup{float:right;margin-left:6px;margin-right:0}a.cke_button{display:inline-block;height:18px;padding:4px 6px;outline:0;cursor:default;float:left;border:0}.cke_rtl .cke_button{float:right}.cke_hc .cke_button{border:1px solid black;padding:3px 5px;margin:-2px 4px 0 -2px}.cke_button_on{-moz-box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);background:#b5b5b5;background-image:-webkit-gradient(linear,left top,left bottom,from(#aaa),to(#cacaca));background-image:-moz-linear-gradient(top,#aaa,#cacaca);background-image:-webkit-linear-gradient(top,#aaa,#cacaca);background-image:-o-linear-gradient(top,#aaa,#cacaca);background-image:-ms-linear-gradient(top,#aaa,#cacaca);background-image:linear-gradient(top,#aaa,#cacaca);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#aaaaaa',endColorstr='#cacaca')}.cke_hc .cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active,.cke_hc a.cke_button_disabled:hover,.cke_hc a.cke_button_disabled:focus,.cke_hc a.cke_button_disabled:active{border-width:3px;padding:1px 3px}.cke_button_disabled .cke_button_icon{opacity:.3}.cke_hc .cke_button_disabled{opacity:.5}a.cke_button_on:hover,a.cke_button_on:focus,a.cke_button_on:active{-moz-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2)}a.cke_button_off:hover,a.cke_button_off:focus,a.cke_button_off:active,a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{-moz-box-shadow:0 0 1px rgba(0,0,0,.3) inset;-webkit-box-shadow:0 0 1px rgba(0,0,0,.3) inset;box-shadow:0 0 1px rgba(0,0,0,.3) inset;background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}.cke_button_icon{cursor:inherit;background-repeat:no-repeat;margin-top:1px;width:16px;height:16px;float:left;display:inline-block}.cke_rtl .cke_button_icon{float:right}.cke_hc .cke_button_icon{display:none}.cke_button_label{display:none;padding-left:3px;margin-top:1px;line-height:17px;vertical-align:middle;float:left;cursor:default;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.5)}.cke_rtl .cke_button_label{padding-right:3px;padding-left:0;float:right}.cke_hc .cke_button_label{padding:0;display:inline-block;font-size:12px}.cke_button_arrow{display:inline-block;margin:8px 0 0 1px;width:0;height:0;cursor:default;vertical-align:top;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #474747}.cke_rtl .cke_button_arrow{margin-right:5px;margin-left:0}.cke_hc .cke_button_arrow{font-size:10px;margin:3px -2px 0 3px;width:auto;border:0}.cke_toolbar_separator{float:left;background-color:#c0c0c0;background-color:rgba(0,0,0,.2);margin:5px 2px 0;height:18px;width:1px;-webkit-box-shadow:1px 0 1px rgba(255,255,255,.5);-moz-box-shadow:1px 0 1px rgba(255,255,255,.5);box-shadow:1px 0 1px rgba(255,255,255,.5)}.cke_rtl .cke_toolbar_separator{float:right;-webkit-box-shadow:-1px 0 1px rgba(255,255,255,.1);-moz-box-shadow:-1px 0 1px rgba(255,255,255,.1);box-shadow:-1px 0 1px rgba(255,255,255,.1)}.cke_hc .cke_toolbar_separator{width:0;border-left:1px solid;margin:1px 5px 0 0}.cke_toolbar_break{display:block;clear:left}.cke_rtl .cke_toolbar_break{clear:right}.cke_toolbox_collapser{width:12px;height:11px;float:right;margin:11px 0 0;font-size:0;cursor:default;text-align:center;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_toolbox_collapser:hover{background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}.cke_toolbox_collapser.cke_toolbox_collapser_min{margin:0 2px 4px}.cke_rtl .cke_toolbox_collapser{float:left}.cke_toolbox_collapser .cke_arrow{display:inline-block;height:0;width:0;font-size:0;margin-top:1px;border-left:3px solid transparent;border-right:3px solid transparent;border-bottom:3px solid #474747;border-top:3px solid transparent}.cke_toolbox_collapser.cke_toolbox_collapser_min .cke_arrow{margin-top:4px;border-bottom-color:transparent;border-top-color:#474747}.cke_hc .cke_toolbox_collapser .cke_arrow{font-size:8px;width:auto;border:0;margin-top:0;margin-right:2px}.cke_menubutton{display:block}.cke_menuitem span{cursor:default}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#d3d3d3;display:block}.cke_hc .cke_menubutton{padding:2px}.cke_hc .cke_menubutton:hover,.cke_hc .cke_menubutton:focus,.cke_hc .cke_menubutton:active{border:2px solid;padding:0}.cke_menubutton_inner{display:table-row}.cke_menubutton_icon,.cke_menubutton_label,.cke_menuarrow{display:table-cell}.cke_menubutton_icon{background-color:#d7d8d7;opacity:.70;filter:alpha(opacity=70);padding:4px}.cke_hc .cke_menubutton_icon{height:16px;width:0;padding:4px 0}.cke_menubutton:hover .cke_menubutton_icon,.cke_menubutton:focus .cke_menubutton_icon,.cke_menubutton:active .cke_menubutton_icon{background-color:#d0d2d0}.cke_menubutton_disabled:hover .cke_menubutton_icon,.cke_menubutton_disabled:focus .cke_menubutton_icon,.cke_menubutton_disabled:active .cke_menubutton_icon{opacity:.3;filter:alpha(opacity=30)}.cke_menubutton_label{padding:0 5px;background-color:transparent;width:100%;vertical-align:middle}.cke_menubutton_disabled .cke_menubutton_label{opacity:.3;filter:alpha(opacity=30)}.cke_menubutton_on{border:1px solid #dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_menubutton_on .cke_menubutton_icon{padding-right:3px}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#eff0ef}.cke_panel_frame .cke_menubutton_label{display:none}.cke_menuseparator{background-color:#d3d3d3;height:1px;filter:alpha(opacity=70);opacity:.70}.cke_menuarrow{background-image:url(images/arrow.png);background-position:0 10px;background-repeat:no-repeat;padding:0 5px}.cke_rtl .cke_menuarrow{background-position:5px -13px;background-repeat:no-repeat}.cke_menuarrow span{display:none}.cke_hc .cke_menuarrow span{vertical-align:middle;display:inline}.cke_combo{display:inline-block;float:left}.cke_rtl .cke_combo{float:right}.cke_hc .cke_combo{margin-top:-2px}.cke_combo_label{display:none;float:left;line-height:26px;vertical-align:top;margin-right:5px}.cke_rtl .cke_combo_label{float:right;margin-left:5px;margin-right:0}.cke_combo_button{display:inline-block;float:left;margin:0 6px 5px 0;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_combo_off a.cke_combo_button:hover,.cke_combo_off a.cke_combo_button:focus{background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc');outline:0}.cke_combo_off a.cke_combo_button:active,.cke_combo_on a.cke_combo_button{border:1px solid #777;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;background:#b5b5b5;background-image:-webkit-gradient(linear,left top,left bottom,from(#aaa),to(#cacaca));background-image:-moz-linear-gradient(top,#aaa,#cacaca);background-image:-webkit-linear-gradient(top,#aaa,#cacaca);background-image:-o-linear-gradient(top,#aaa,#cacaca);background-image:-ms-linear-gradient(top,#aaa,#cacaca);background-image:linear-gradient(top,#aaa,#cacaca);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#aaaaaa',endColorstr='#cacaca')}.cke_combo_on a.cke_combo_button:hover,.cke_combo_on a.cke_combo_button:focus,.cke_combo_on a.cke_combo_button:active{-moz-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2)}.cke_rtl .cke_combo_button{float:right;margin-left:5px;margin-right:0}.cke_hc a.cke_combo_button{padding:3px}.cke_hc .cke_combo_on a.cke_combo_button,.cke_hc .cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_combo_off a.cke_combo_button:active{border-width:3px;padding:1px}.cke_combo_text{line-height:26px;padding-left:10px;text-overflow:ellipsis;overflow:hidden;float:left;cursor:default;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.5);width:60px}.cke_rtl .cke_combo_text{float:right;text-align:right;padding-left:0;padding-right:10px}.cke_hc .cke_combo_text{line-height:18px;font-size:12px}.cke_combo_open{cursor:default;display:inline-block;font-size:0;height:19px;line-height:17px;margin:1px 7px 1px;width:5px}.cke_hc .cke_combo_open{height:12px}.cke_combo_arrow{margin:11px 0 0;float:left;height:0;width:0;font-size:0;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #474747}.cke_hc .cke_combo_arrow{font-size:10px;width:auto;border:0;margin-top:3px}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{opacity:.3}.cke_path{float:left;margin:-2px 0 2px}.cke_path_item,.cke_path_empty{display:inline-block;float:left;padding:3px 4px;margin-right:2px;cursor:default;text-decoration:none;outline:0;border:0;color:#4c4c4c;text-shadow:0 1px 0 #fff;font-weight:bold;font-size:11px}.cke_rtl .cke_path,.cke_rtl .cke_path_item,.cke_rtl .cke_path_empty{float:right}a.cke_path_item:hover,a.cke_path_item:focus,a.cke_path_item:active{background-color:#bfbfbf;color:#333;text-shadow:0 1px 0 rgba(255,255,255,.5);-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-moz-box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5);-webkit-box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5);box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5)}.cke_hc a.cke_path_item:hover,.cke_hc a.cke_path_item:focus,.cke_hc a.cke_path_item:active{border:2px solid;padding:1px 2px}.cke_button__source_label,.cke_button__sourcedialog_label{display:inline}.cke_combo__fontsize .cke_combo_text{width:30px}.cke_combopanel__fontsize{width:120px}.cke_source{font-family:'Courier New',Monospace;font-size:small;background-color:#fff;white-space:pre}.cke_wysiwyg_frame,.cke_wysiwyg_div{background-color:#fff}.cke_chrome{visibility:inherit}.cke_voice_label{display:none}legend.cke_voice_label{display:none}.cke_button__about_icon {background: url(icons.png) no-repeat 0 -0px !important;}.cke_button__bold_icon {background: url(icons.png) no-repeat 0 -24px !important;}.cke_button__italic_icon {background: url(icons.png) no-repeat 0 -48px !important;}.cke_button__strike_icon {background: url(icons.png) no-repeat 0 -72px !important;}.cke_button__subscript_icon {background: url(icons.png) no-repeat 0 -96px !important;}.cke_button__superscript_icon {background: url(icons.png) no-repeat 0 -120px !important;}.cke_button__underline_icon {background: url(icons.png) no-repeat 0 -144px !important;}.cke_rtl .cke_button__copy_icon, .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons.png) no-repeat 0 -168px !important;}.cke_ltr .cke_button__copy_icon {background: url(icons.png) no-repeat 0 -192px !important;}.cke_rtl .cke_button__cut_icon, .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons.png) no-repeat 0 -216px !important;}.cke_ltr .cke_button__cut_icon {background: url(icons.png) no-repeat 0 -240px !important;}.cke_rtl .cke_button__paste_icon, .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons.png) no-repeat 0 -264px !important;}.cke_ltr .cke_button__paste_icon {background: url(icons.png) no-repeat 0 -288px !important;}.cke_button__horizontalrule_icon {background: url(icons.png) no-repeat 0 -312px !important;}.cke_rtl .cke_button__indent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons.png) no-repeat 0 -336px !important;}.cke_ltr .cke_button__indent_icon {background: url(icons.png) no-repeat 0 -360px !important;}.cke_rtl .cke_button__outdent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons.png) no-repeat 0 -384px !important;}.cke_ltr .cke_button__outdent_icon {background: url(icons.png) no-repeat 0 -408px !important;}.cke_rtl .cke_button__anchor_icon, .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons.png) no-repeat 0 -432px !important;}.cke_ltr .cke_button__anchor_icon {background: url(icons.png) no-repeat 0 -456px !important;}.cke_button__link_icon {background: url(icons.png) no-repeat 0 -480px !important;}.cke_button__unlink_icon {background: url(icons.png) no-repeat 0 -504px !important;}.cke_rtl .cke_button__bulletedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons.png) no-repeat 0 -528px !important;}.cke_ltr .cke_button__bulletedlist_icon {background: url(icons.png) no-repeat 0 -552px !important;}.cke_rtl .cke_button__numberedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons.png) no-repeat 0 -576px !important;}.cke_ltr .cke_button__numberedlist_icon {background: url(icons.png) no-repeat 0 -600px !important;}.cke_button__removeformat_icon {background: url(icons.png) no-repeat 0 -624px !important;}.cke_rtl .cke_button__redo_icon, .cke_mixed_dir_content .cke_rtl .cke_button__redo_icon {background: url(icons.png) no-repeat 0 -648px !important;}.cke_ltr .cke_button__redo_icon {background: url(icons.png) no-repeat 0 -672px !important;}.cke_rtl .cke_button__undo_icon, .cke_mixed_dir_content .cke_rtl .cke_button__undo_icon {background: url(icons.png) no-repeat 0 -696px !important;}.cke_ltr .cke_button__undo_icon {background: url(icons.png) no-repeat 0 -720px !important;}.cke_hidpi .cke_button__about_icon {background: url(icons_hidpi.png) no-repeat 0 -0px !important;background-size: 16px !important;}.cke_hidpi .cke_button__bold_icon {background: url(icons_hidpi.png) no-repeat 0 -24px !important;background-size: 16px !important;}.cke_hidpi .cke_button__italic_icon {background: url(icons_hidpi.png) no-repeat 0 -48px !important;background-size: 16px !important;}.cke_hidpi .cke_button__strike_icon {background: url(icons_hidpi.png) no-repeat 0 -72px !important;background-size: 16px !important;}.cke_hidpi .cke_button__subscript_icon {background: url(icons_hidpi.png) no-repeat 0 -96px !important;background-size: 16px !important;}.cke_hidpi .cke_button__superscript_icon {background: url(icons_hidpi.png) no-repeat 0 -120px !important;background-size: 16px !important;}.cke_hidpi .cke_button__underline_icon {background: url(icons_hidpi.png) no-repeat 0 -144px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__copy_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons_hidpi.png) no-repeat 0 -168px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__copy_icon,.cke_ltr.cke_hidpi .cke_button__copy_icon {background: url(icons_hidpi.png) no-repeat 0 -192px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__cut_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons_hidpi.png) no-repeat 0 -216px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__cut_icon,.cke_ltr.cke_hidpi .cke_button__cut_icon {background: url(icons_hidpi.png) no-repeat 0 -240px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__paste_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons_hidpi.png) no-repeat 0 -264px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__paste_icon,.cke_ltr.cke_hidpi .cke_button__paste_icon {background: url(icons_hidpi.png) no-repeat 0 -288px !important;background-size: 16px !important;}.cke_hidpi .cke_button__horizontalrule_icon {background: url(icons_hidpi.png) no-repeat 0 -312px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__indent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons_hidpi.png) no-repeat 0 -336px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__indent_icon,.cke_ltr.cke_hidpi .cke_button__indent_icon {background: url(icons_hidpi.png) no-repeat 0 -360px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__outdent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons_hidpi.png) no-repeat 0 -384px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__outdent_icon,.cke_ltr.cke_hidpi .cke_button__outdent_icon {background: url(icons_hidpi.png) no-repeat 0 -408px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__anchor_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons_hidpi.png) no-repeat 0 -432px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__anchor_icon,.cke_ltr.cke_hidpi .cke_button__anchor_icon {background: url(icons_hidpi.png) no-repeat 0 -456px !important;background-size: 16px !important;}.cke_hidpi .cke_button__link_icon {background: url(icons_hidpi.png) no-repeat 0 -480px !important;background-size: 16px !important;}.cke_hidpi .cke_button__unlink_icon {background: url(icons_hidpi.png) no-repeat 0 -504px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__bulletedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -528px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__bulletedlist_icon,.cke_ltr.cke_hidpi .cke_button__bulletedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -552px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__numberedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -576px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__numberedlist_icon,.cke_ltr.cke_hidpi .cke_button__numberedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -600px !important;background-size: 16px !important;}.cke_hidpi .cke_button__removeformat_icon {background: url(icons_hidpi.png) no-repeat 0 -624px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__redo_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__redo_icon {background: url(icons_hidpi.png) no-repeat 0 -648px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__redo_icon,.cke_ltr.cke_hidpi .cke_button__redo_icon {background: url(icons_hidpi.png) no-repeat 0 -672px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__undo_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__undo_icon {background: url(icons_hidpi.png) no-repeat 0 -696px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__undo_icon,.cke_ltr.cke_hidpi .cke_button__undo_icon {background: url(icons_hidpi.png) no-repeat 0 -720px !important;background-size: 16px !important;}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono/editor_gecko.css b/js/ckeditor/skins/moono/editor_gecko.css
deleted file mode 100644 (file)
index 5256d3b..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-/*\r
-Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
-For licensing, see LICENSE.md or http://ckeditor.com/license\r
-*/\r
-.cke_reset{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;position:static;-webkit-transition:none;-moz-transition:none;-ms-transition:none;transition:none}.cke_reset_all,.cke_reset_all *{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;position:static;-webkit-transition:none;-moz-transition:none;-ms-transition:none;transition:none;border-collapse:collapse;font:normal normal normal 12px Arial,Helvetica,Tahoma,Verdana,Sans-Serif;color:#000;text-align:left;white-space:nowrap;cursor:auto}.cke_reset_all .cke_rtl *{text-align:right}.cke_reset_all iframe{vertical-align:inherit}.cke_reset_all textarea{white-space:pre}.cke_reset_all textarea,.cke_reset_all input[type="text"],.cke_reset_all input[type="password"]{cursor:text}.cke_reset_all textarea[disabled],.cke_reset_all input[type="text"][disabled],.cke_reset_all input[type="password"][disabled]{cursor:default}.cke_reset_all fieldset{padding:10px;border:2px groove #e0dfe3}.cke_reset_all select{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.cke_chrome{display:block;border:1px solid #b6b6b6;padding:0;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_inner{display:block;-webkit-touch-callout:none;background:#fff;padding:0}.cke_float{border:0}.cke_float .cke_inner{padding-bottom:0}.cke_top,.cke_contents,.cke_bottom{display:block;overflow:hidden}.cke_top{border-bottom:1px solid #b6b6b6;padding:6px 8px 2px;white-space:normal;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_float .cke_top{border:1px solid #b6b6b6;border-bottom-color:#999}.cke_bottom{padding:6px 8px 2px;position:relative;border-top:1px solid #bfbfbf;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#cfd1cf));background-image:-moz-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-webkit-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-o-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-ms-linear-gradient(top,#ebebeb,#cfd1cf);background-image:linear-gradient(top,#ebebeb,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ebebeb',endColorstr='#cfd1cf')}.cke_browser_ios .cke_contents{overflow-y:auto;-webkit-overflow-scrolling:touch}.cke_resizer{width:0;height:0;overflow:hidden;width:0;height:0;overflow:hidden;border-width:10px 10px 0 0;border-color:transparent #666 transparent transparent;border-style:dashed solid dashed dashed;font-size:0;vertical-align:bottom;margin-top:6px;margin-bottom:2px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.3);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.3);box-shadow:0 1px 0 rgba(255,255,255,.3)}.cke_hc .cke_resizer{font-size:15px;width:auto;height:auto;border-width:0}.cke_resizer_ltr{cursor:se-resize;float:right;margin-right:-4px}.cke_resizer_rtl{border-width:10px 0 0 10px;border-color:transparent transparent transparent #a5a5a5;border-style:dashed dashed dashed solid;cursor:sw-resize;float:left;margin-left:-4px;right:auto}.cke_wysiwyg_div{display:block;height:100%;overflow:auto;padding:0 8px;outline-style:none;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.cke_panel{visibility:visible;width:120px;height:100px;overflow:hidden;background-color:#fff;border:1px solid #b6b6b6;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_menu_panel{padding:0;margin:0}.cke_combopanel{width:150px;height:170px}.cke_panel_frame{width:100%;height:100%;font-size:12px;overflow:auto;overflow-x:hidden}.cke_panel_container{overflow-y:auto;overflow-x:hidden}.cke_panel_list{list-style-type:none;margin:3px;padding:0;white-space:nowrap}.cke_panel_listItem{margin:0;padding-bottom:1px}.cke_panel_listItem a{padding:3px 4px;display:block;border:1px solid #fff;color:inherit!important;text-decoration:none;overflow:hidden;text-overflow:ellipsis;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px}* html .cke_panel_listItem a{width:100%;color:#000}*:first-child+html .cke_panel_listItem a{color:#000}.cke_panel_listItem.cke_selected a{border:1px solid #dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_panel_listItem a:hover,.cke_panel_listItem a:focus,.cke_panel_listItem a:active{border-color:#dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_hc .cke_panel_listItem a{border-style:none}.cke_hc .cke_panel_listItem a:hover,.cke_hc .cke_panel_listItem a:focus,.cke_hc .cke_panel_listItem a:active{border:2px solid;padding:1px 2px}.cke_panel_grouptitle{font-size:11px;font-weight:bold;white-space:nowrap;margin:0;padding:4px 6px;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.75);border-bottom:1px solid #b6b6b6;-moz-border-radius:2px 2px 0 0;-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_panel_listItem p,.cke_panel_listItem h1,.cke_panel_listItem h2,.cke_panel_listItem h3,.cke_panel_listItem h4,.cke_panel_listItem h5,.cke_panel_listItem h6,.cke_panel_listItem pre{margin-top:0;margin-bottom:0}.cke_colorblock{padding:3px;font-size:11px;font-family:'Microsoft Sans Serif',Tahoma,Arial,Verdana,Sans-Serif}.cke_colorblock,.cke_colorblock a{text-decoration:none;color:#000}span.cke_colorbox{width:10px;height:10px;border:#808080 1px solid;float:left}.cke_rtl span.cke_colorbox{float:right}a.cke_colorbox{border:#fff 1px solid;padding:2px;float:left;width:12px;height:12px}.cke_rtl a.cke_colorbox{float:right}a:hover.cke_colorbox,a:focus.cke_colorbox,a:active.cke_colorbox{border:#b6b6b6 1px solid;background-color:#e5e5e5}a.cke_colorauto,a.cke_colormore{border:#fff 1px solid;padding:2px;display:block;cursor:pointer}a:hover.cke_colorauto,a:hover.cke_colormore,a:focus.cke_colorauto,a:focus.cke_colormore,a:active.cke_colorauto,a:active.cke_colormore{border:#b6b6b6 1px solid;background-color:#e5e5e5}.cke_toolbar{float:left}.cke_rtl .cke_toolbar{float:right}.cke_toolgroup{float:left;margin:0 6px 5px 0;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_hc .cke_toolgroup{border:0;margin-right:10px;margin-bottom:10px}.cke_rtl .cke_toolgroup *:first-child{-moz-border-radius:0 2px 2px 0;-webkit-border-radius:0 2px 2px 0;border-radius:0 2px 2px 0}.cke_rtl .cke_toolgroup *:last-child{-moz-border-radius:2px 0 0 2px;-webkit-border-radius:2px 0 0 2px;border-radius:2px 0 0 2px}.cke_rtl .cke_toolgroup{float:right;margin-left:6px;margin-right:0}a.cke_button{display:inline-block;height:18px;padding:4px 6px;outline:0;cursor:default;float:left;border:0}.cke_rtl .cke_button{float:right}.cke_hc .cke_button{border:1px solid black;padding:3px 5px;margin:-2px 4px 0 -2px}.cke_button_on{-moz-box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);background:#b5b5b5;background-image:-webkit-gradient(linear,left top,left bottom,from(#aaa),to(#cacaca));background-image:-moz-linear-gradient(top,#aaa,#cacaca);background-image:-webkit-linear-gradient(top,#aaa,#cacaca);background-image:-o-linear-gradient(top,#aaa,#cacaca);background-image:-ms-linear-gradient(top,#aaa,#cacaca);background-image:linear-gradient(top,#aaa,#cacaca);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#aaaaaa',endColorstr='#cacaca')}.cke_hc .cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active,.cke_hc a.cke_button_disabled:hover,.cke_hc a.cke_button_disabled:focus,.cke_hc a.cke_button_disabled:active{border-width:3px;padding:1px 3px}.cke_button_disabled .cke_button_icon{opacity:.3}.cke_hc .cke_button_disabled{opacity:.5}a.cke_button_on:hover,a.cke_button_on:focus,a.cke_button_on:active{-moz-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2)}a.cke_button_off:hover,a.cke_button_off:focus,a.cke_button_off:active,a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{-moz-box-shadow:0 0 1px rgba(0,0,0,.3) inset;-webkit-box-shadow:0 0 1px rgba(0,0,0,.3) inset;box-shadow:0 0 1px rgba(0,0,0,.3) inset;background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}.cke_button_icon{cursor:inherit;background-repeat:no-repeat;margin-top:1px;width:16px;height:16px;float:left;display:inline-block}.cke_rtl .cke_button_icon{float:right}.cke_hc .cke_button_icon{display:none}.cke_button_label{display:none;padding-left:3px;margin-top:1px;line-height:17px;vertical-align:middle;float:left;cursor:default;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.5)}.cke_rtl .cke_button_label{padding-right:3px;padding-left:0;float:right}.cke_hc .cke_button_label{padding:0;display:inline-block;font-size:12px}.cke_button_arrow{display:inline-block;margin:8px 0 0 1px;width:0;height:0;cursor:default;vertical-align:top;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #474747}.cke_rtl .cke_button_arrow{margin-right:5px;margin-left:0}.cke_hc .cke_button_arrow{font-size:10px;margin:3px -2px 0 3px;width:auto;border:0}.cke_toolbar_separator{float:left;background-color:#c0c0c0;background-color:rgba(0,0,0,.2);margin:5px 2px 0;height:18px;width:1px;-webkit-box-shadow:1px 0 1px rgba(255,255,255,.5);-moz-box-shadow:1px 0 1px rgba(255,255,255,.5);box-shadow:1px 0 1px rgba(255,255,255,.5)}.cke_rtl .cke_toolbar_separator{float:right;-webkit-box-shadow:-1px 0 1px rgba(255,255,255,.1);-moz-box-shadow:-1px 0 1px rgba(255,255,255,.1);box-shadow:-1px 0 1px rgba(255,255,255,.1)}.cke_hc .cke_toolbar_separator{width:0;border-left:1px solid;margin:1px 5px 0 0}.cke_toolbar_break{display:block;clear:left}.cke_rtl .cke_toolbar_break{clear:right}.cke_toolbox_collapser{width:12px;height:11px;float:right;margin:11px 0 0;font-size:0;cursor:default;text-align:center;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_toolbox_collapser:hover{background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}.cke_toolbox_collapser.cke_toolbox_collapser_min{margin:0 2px 4px}.cke_rtl .cke_toolbox_collapser{float:left}.cke_toolbox_collapser .cke_arrow{display:inline-block;height:0;width:0;font-size:0;margin-top:1px;border-left:3px solid transparent;border-right:3px solid transparent;border-bottom:3px solid #474747;border-top:3px solid transparent}.cke_toolbox_collapser.cke_toolbox_collapser_min .cke_arrow{margin-top:4px;border-bottom-color:transparent;border-top-color:#474747}.cke_hc .cke_toolbox_collapser .cke_arrow{font-size:8px;width:auto;border:0;margin-top:0;margin-right:2px}.cke_menubutton{display:block}.cke_menuitem span{cursor:default}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#d3d3d3;display:block}.cke_hc .cke_menubutton{padding:2px}.cke_hc .cke_menubutton:hover,.cke_hc .cke_menubutton:focus,.cke_hc .cke_menubutton:active{border:2px solid;padding:0}.cke_menubutton_inner{display:table-row}.cke_menubutton_icon,.cke_menubutton_label,.cke_menuarrow{display:table-cell}.cke_menubutton_icon{background-color:#d7d8d7;opacity:.70;filter:alpha(opacity=70);padding:4px}.cke_hc .cke_menubutton_icon{height:16px;width:0;padding:4px 0}.cke_menubutton:hover .cke_menubutton_icon,.cke_menubutton:focus .cke_menubutton_icon,.cke_menubutton:active .cke_menubutton_icon{background-color:#d0d2d0}.cke_menubutton_disabled:hover .cke_menubutton_icon,.cke_menubutton_disabled:focus .cke_menubutton_icon,.cke_menubutton_disabled:active .cke_menubutton_icon{opacity:.3;filter:alpha(opacity=30)}.cke_menubutton_label{padding:0 5px;background-color:transparent;width:100%;vertical-align:middle}.cke_menubutton_disabled .cke_menubutton_label{opacity:.3;filter:alpha(opacity=30)}.cke_menubutton_on{border:1px solid #dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_menubutton_on .cke_menubutton_icon{padding-right:3px}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#eff0ef}.cke_panel_frame .cke_menubutton_label{display:none}.cke_menuseparator{background-color:#d3d3d3;height:1px;filter:alpha(opacity=70);opacity:.70}.cke_menuarrow{background-image:url(images/arrow.png);background-position:0 10px;background-repeat:no-repeat;padding:0 5px}.cke_rtl .cke_menuarrow{background-position:5px -13px;background-repeat:no-repeat}.cke_menuarrow span{display:none}.cke_hc .cke_menuarrow span{vertical-align:middle;display:inline}.cke_combo{display:inline-block;float:left}.cke_rtl .cke_combo{float:right}.cke_hc .cke_combo{margin-top:-2px}.cke_combo_label{display:none;float:left;line-height:26px;vertical-align:top;margin-right:5px}.cke_rtl .cke_combo_label{float:right;margin-left:5px;margin-right:0}.cke_combo_button{display:inline-block;float:left;margin:0 6px 5px 0;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_combo_off a.cke_combo_button:hover,.cke_combo_off a.cke_combo_button:focus{background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc');outline:0}.cke_combo_off a.cke_combo_button:active,.cke_combo_on a.cke_combo_button{border:1px solid #777;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;background:#b5b5b5;background-image:-webkit-gradient(linear,left top,left bottom,from(#aaa),to(#cacaca));background-image:-moz-linear-gradient(top,#aaa,#cacaca);background-image:-webkit-linear-gradient(top,#aaa,#cacaca);background-image:-o-linear-gradient(top,#aaa,#cacaca);background-image:-ms-linear-gradient(top,#aaa,#cacaca);background-image:linear-gradient(top,#aaa,#cacaca);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#aaaaaa',endColorstr='#cacaca')}.cke_combo_on a.cke_combo_button:hover,.cke_combo_on a.cke_combo_button:focus,.cke_combo_on a.cke_combo_button:active{-moz-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2)}.cke_rtl .cke_combo_button{float:right;margin-left:5px;margin-right:0}.cke_hc a.cke_combo_button{padding:3px}.cke_hc .cke_combo_on a.cke_combo_button,.cke_hc .cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_combo_off a.cke_combo_button:active{border-width:3px;padding:1px}.cke_combo_text{line-height:26px;padding-left:10px;text-overflow:ellipsis;overflow:hidden;float:left;cursor:default;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.5);width:60px}.cke_rtl .cke_combo_text{float:right;text-align:right;padding-left:0;padding-right:10px}.cke_hc .cke_combo_text{line-height:18px;font-size:12px}.cke_combo_open{cursor:default;display:inline-block;font-size:0;height:19px;line-height:17px;margin:1px 7px 1px;width:5px}.cke_hc .cke_combo_open{height:12px}.cke_combo_arrow{margin:11px 0 0;float:left;height:0;width:0;font-size:0;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #474747}.cke_hc .cke_combo_arrow{font-size:10px;width:auto;border:0;margin-top:3px}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{opacity:.3}.cke_path{float:left;margin:-2px 0 2px}.cke_path_item,.cke_path_empty{display:inline-block;float:left;padding:3px 4px;margin-right:2px;cursor:default;text-decoration:none;outline:0;border:0;color:#4c4c4c;text-shadow:0 1px 0 #fff;font-weight:bold;font-size:11px}.cke_rtl .cke_path,.cke_rtl .cke_path_item,.cke_rtl .cke_path_empty{float:right}a.cke_path_item:hover,a.cke_path_item:focus,a.cke_path_item:active{background-color:#bfbfbf;color:#333;text-shadow:0 1px 0 rgba(255,255,255,.5);-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-moz-box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5);-webkit-box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5);box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5)}.cke_hc a.cke_path_item:hover,.cke_hc a.cke_path_item:focus,.cke_hc a.cke_path_item:active{border:2px solid;padding:1px 2px}.cke_button__source_label,.cke_button__sourcedialog_label{display:inline}.cke_combo__fontsize .cke_combo_text{width:30px}.cke_combopanel__fontsize{width:120px}.cke_source{font-family:'Courier New',Monospace;font-size:small;background-color:#fff;white-space:pre}.cke_wysiwyg_frame,.cke_wysiwyg_div{background-color:#fff}.cke_chrome{visibility:inherit}.cke_voice_label{display:none}legend.cke_voice_label{display:none}.cke_bottom{padding-bottom:3px}.cke_combo_text{margin-bottom:-1px;margin-top:1px}.cke_button__about_icon {background: url(icons.png) no-repeat 0 -0px !important;}.cke_button__bold_icon {background: url(icons.png) no-repeat 0 -24px !important;}.cke_button__italic_icon {background: url(icons.png) no-repeat 0 -48px !important;}.cke_button__strike_icon {background: url(icons.png) no-repeat 0 -72px !important;}.cke_button__subscript_icon {background: url(icons.png) no-repeat 0 -96px !important;}.cke_button__superscript_icon {background: url(icons.png) no-repeat 0 -120px !important;}.cke_button__underline_icon {background: url(icons.png) no-repeat 0 -144px !important;}.cke_rtl .cke_button__copy_icon, .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons.png) no-repeat 0 -168px !important;}.cke_ltr .cke_button__copy_icon {background: url(icons.png) no-repeat 0 -192px !important;}.cke_rtl .cke_button__cut_icon, .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons.png) no-repeat 0 -216px !important;}.cke_ltr .cke_button__cut_icon {background: url(icons.png) no-repeat 0 -240px !important;}.cke_rtl .cke_button__paste_icon, .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons.png) no-repeat 0 -264px !important;}.cke_ltr .cke_button__paste_icon {background: url(icons.png) no-repeat 0 -288px !important;}.cke_button__horizontalrule_icon {background: url(icons.png) no-repeat 0 -312px !important;}.cke_rtl .cke_button__indent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons.png) no-repeat 0 -336px !important;}.cke_ltr .cke_button__indent_icon {background: url(icons.png) no-repeat 0 -360px !important;}.cke_rtl .cke_button__outdent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons.png) no-repeat 0 -384px !important;}.cke_ltr .cke_button__outdent_icon {background: url(icons.png) no-repeat 0 -408px !important;}.cke_rtl .cke_button__anchor_icon, .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons.png) no-repeat 0 -432px !important;}.cke_ltr .cke_button__anchor_icon {background: url(icons.png) no-repeat 0 -456px !important;}.cke_button__link_icon {background: url(icons.png) no-repeat 0 -480px !important;}.cke_button__unlink_icon {background: url(icons.png) no-repeat 0 -504px !important;}.cke_rtl .cke_button__bulletedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons.png) no-repeat 0 -528px !important;}.cke_ltr .cke_button__bulletedlist_icon {background: url(icons.png) no-repeat 0 -552px !important;}.cke_rtl .cke_button__numberedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons.png) no-repeat 0 -576px !important;}.cke_ltr .cke_button__numberedlist_icon {background: url(icons.png) no-repeat 0 -600px !important;}.cke_button__removeformat_icon {background: url(icons.png) no-repeat 0 -624px !important;}.cke_rtl .cke_button__redo_icon, .cke_mixed_dir_content .cke_rtl .cke_button__redo_icon {background: url(icons.png) no-repeat 0 -648px !important;}.cke_ltr .cke_button__redo_icon {background: url(icons.png) no-repeat 0 -672px !important;}.cke_rtl .cke_button__undo_icon, .cke_mixed_dir_content .cke_rtl .cke_button__undo_icon {background: url(icons.png) no-repeat 0 -696px !important;}.cke_ltr .cke_button__undo_icon {background: url(icons.png) no-repeat 0 -720px !important;}.cke_hidpi .cke_button__about_icon {background: url(icons_hidpi.png) no-repeat 0 -0px !important;background-size: 16px !important;}.cke_hidpi .cke_button__bold_icon {background: url(icons_hidpi.png) no-repeat 0 -24px !important;background-size: 16px !important;}.cke_hidpi .cke_button__italic_icon {background: url(icons_hidpi.png) no-repeat 0 -48px !important;background-size: 16px !important;}.cke_hidpi .cke_button__strike_icon {background: url(icons_hidpi.png) no-repeat 0 -72px !important;background-size: 16px !important;}.cke_hidpi .cke_button__subscript_icon {background: url(icons_hidpi.png) no-repeat 0 -96px !important;background-size: 16px !important;}.cke_hidpi .cke_button__superscript_icon {background: url(icons_hidpi.png) no-repeat 0 -120px !important;background-size: 16px !important;}.cke_hidpi .cke_button__underline_icon {background: url(icons_hidpi.png) no-repeat 0 -144px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__copy_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons_hidpi.png) no-repeat 0 -168px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__copy_icon,.cke_ltr.cke_hidpi .cke_button__copy_icon {background: url(icons_hidpi.png) no-repeat 0 -192px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__cut_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons_hidpi.png) no-repeat 0 -216px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__cut_icon,.cke_ltr.cke_hidpi .cke_button__cut_icon {background: url(icons_hidpi.png) no-repeat 0 -240px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__paste_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons_hidpi.png) no-repeat 0 -264px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__paste_icon,.cke_ltr.cke_hidpi .cke_button__paste_icon {background: url(icons_hidpi.png) no-repeat 0 -288px !important;background-size: 16px !important;}.cke_hidpi .cke_button__horizontalrule_icon {background: url(icons_hidpi.png) no-repeat 0 -312px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__indent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons_hidpi.png) no-repeat 0 -336px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__indent_icon,.cke_ltr.cke_hidpi .cke_button__indent_icon {background: url(icons_hidpi.png) no-repeat 0 -360px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__outdent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons_hidpi.png) no-repeat 0 -384px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__outdent_icon,.cke_ltr.cke_hidpi .cke_button__outdent_icon {background: url(icons_hidpi.png) no-repeat 0 -408px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__anchor_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons_hidpi.png) no-repeat 0 -432px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__anchor_icon,.cke_ltr.cke_hidpi .cke_button__anchor_icon {background: url(icons_hidpi.png) no-repeat 0 -456px !important;background-size: 16px !important;}.cke_hidpi .cke_button__link_icon {background: url(icons_hidpi.png) no-repeat 0 -480px !important;background-size: 16px !important;}.cke_hidpi .cke_button__unlink_icon {background: url(icons_hidpi.png) no-repeat 0 -504px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__bulletedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -528px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__bulletedlist_icon,.cke_ltr.cke_hidpi .cke_button__bulletedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -552px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__numberedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -576px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__numberedlist_icon,.cke_ltr.cke_hidpi .cke_button__numberedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -600px !important;background-size: 16px !important;}.cke_hidpi .cke_button__removeformat_icon {background: url(icons_hidpi.png) no-repeat 0 -624px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__redo_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__redo_icon {background: url(icons_hidpi.png) no-repeat 0 -648px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__redo_icon,.cke_ltr.cke_hidpi .cke_button__redo_icon {background: url(icons_hidpi.png) no-repeat 0 -672px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__undo_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__undo_icon {background: url(icons_hidpi.png) no-repeat 0 -696px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__undo_icon,.cke_ltr.cke_hidpi .cke_button__undo_icon {background: url(icons_hidpi.png) no-repeat 0 -720px !important;background-size: 16px !important;}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono/editor_ie.css b/js/ckeditor/skins/moono/editor_ie.css
deleted file mode 100644 (file)
index de43d01..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-/*\r
-Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
-For licensing, see LICENSE.md or http://ckeditor.com/license\r
-*/\r
-.cke_reset{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;position:static;-webkit-transition:none;-moz-transition:none;-ms-transition:none;transition:none}.cke_reset_all,.cke_reset_all *{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;position:static;-webkit-transition:none;-moz-transition:none;-ms-transition:none;transition:none;border-collapse:collapse;font:normal normal normal 12px Arial,Helvetica,Tahoma,Verdana,Sans-Serif;color:#000;text-align:left;white-space:nowrap;cursor:auto}.cke_reset_all .cke_rtl *{text-align:right}.cke_reset_all iframe{vertical-align:inherit}.cke_reset_all textarea{white-space:pre}.cke_reset_all textarea,.cke_reset_all input[type="text"],.cke_reset_all input[type="password"]{cursor:text}.cke_reset_all textarea[disabled],.cke_reset_all input[type="text"][disabled],.cke_reset_all input[type="password"][disabled]{cursor:default}.cke_reset_all fieldset{padding:10px;border:2px groove #e0dfe3}.cke_reset_all select{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.cke_chrome{display:block;border:1px solid #b6b6b6;padding:0;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_inner{display:block;-webkit-touch-callout:none;background:#fff;padding:0}.cke_float{border:0}.cke_float .cke_inner{padding-bottom:0}.cke_top,.cke_contents,.cke_bottom{display:block;overflow:hidden}.cke_top{border-bottom:1px solid #b6b6b6;padding:6px 8px 2px;white-space:normal;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_float .cke_top{border:1px solid #b6b6b6;border-bottom-color:#999}.cke_bottom{padding:6px 8px 2px;position:relative;border-top:1px solid #bfbfbf;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#cfd1cf));background-image:-moz-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-webkit-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-o-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-ms-linear-gradient(top,#ebebeb,#cfd1cf);background-image:linear-gradient(top,#ebebeb,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ebebeb',endColorstr='#cfd1cf')}.cke_browser_ios .cke_contents{overflow-y:auto;-webkit-overflow-scrolling:touch}.cke_resizer{width:0;height:0;overflow:hidden;width:0;height:0;overflow:hidden;border-width:10px 10px 0 0;border-color:transparent #666 transparent transparent;border-style:dashed solid dashed dashed;font-size:0;vertical-align:bottom;margin-top:6px;margin-bottom:2px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.3);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.3);box-shadow:0 1px 0 rgba(255,255,255,.3)}.cke_hc .cke_resizer{font-size:15px;width:auto;height:auto;border-width:0}.cke_resizer_ltr{cursor:se-resize;float:right;margin-right:-4px}.cke_resizer_rtl{border-width:10px 0 0 10px;border-color:transparent transparent transparent #a5a5a5;border-style:dashed dashed dashed solid;cursor:sw-resize;float:left;margin-left:-4px;right:auto}.cke_wysiwyg_div{display:block;height:100%;overflow:auto;padding:0 8px;outline-style:none;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.cke_panel{visibility:visible;width:120px;height:100px;overflow:hidden;background-color:#fff;border:1px solid #b6b6b6;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_menu_panel{padding:0;margin:0}.cke_combopanel{width:150px;height:170px}.cke_panel_frame{width:100%;height:100%;font-size:12px;overflow:auto;overflow-x:hidden}.cke_panel_container{overflow-y:auto;overflow-x:hidden}.cke_panel_list{list-style-type:none;margin:3px;padding:0;white-space:nowrap}.cke_panel_listItem{margin:0;padding-bottom:1px}.cke_panel_listItem a{padding:3px 4px;display:block;border:1px solid #fff;color:inherit!important;text-decoration:none;overflow:hidden;text-overflow:ellipsis;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px}* html .cke_panel_listItem a{width:100%;color:#000}*:first-child+html .cke_panel_listItem a{color:#000}.cke_panel_listItem.cke_selected a{border:1px solid #dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_panel_listItem a:hover,.cke_panel_listItem a:focus,.cke_panel_listItem a:active{border-color:#dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_hc .cke_panel_listItem a{border-style:none}.cke_hc .cke_panel_listItem a:hover,.cke_hc .cke_panel_listItem a:focus,.cke_hc .cke_panel_listItem a:active{border:2px solid;padding:1px 2px}.cke_panel_grouptitle{font-size:11px;font-weight:bold;white-space:nowrap;margin:0;padding:4px 6px;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.75);border-bottom:1px solid #b6b6b6;-moz-border-radius:2px 2px 0 0;-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_panel_listItem p,.cke_panel_listItem h1,.cke_panel_listItem h2,.cke_panel_listItem h3,.cke_panel_listItem h4,.cke_panel_listItem h5,.cke_panel_listItem h6,.cke_panel_listItem pre{margin-top:0;margin-bottom:0}.cke_colorblock{padding:3px;font-size:11px;font-family:'Microsoft Sans Serif',Tahoma,Arial,Verdana,Sans-Serif}.cke_colorblock,.cke_colorblock a{text-decoration:none;color:#000}span.cke_colorbox{width:10px;height:10px;border:#808080 1px solid;float:left}.cke_rtl span.cke_colorbox{float:right}a.cke_colorbox{border:#fff 1px solid;padding:2px;float:left;width:12px;height:12px}.cke_rtl a.cke_colorbox{float:right}a:hover.cke_colorbox,a:focus.cke_colorbox,a:active.cke_colorbox{border:#b6b6b6 1px solid;background-color:#e5e5e5}a.cke_colorauto,a.cke_colormore{border:#fff 1px solid;padding:2px;display:block;cursor:pointer}a:hover.cke_colorauto,a:hover.cke_colormore,a:focus.cke_colorauto,a:focus.cke_colormore,a:active.cke_colorauto,a:active.cke_colormore{border:#b6b6b6 1px solid;background-color:#e5e5e5}.cke_toolbar{float:left}.cke_rtl .cke_toolbar{float:right}.cke_toolgroup{float:left;margin:0 6px 5px 0;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_hc .cke_toolgroup{border:0;margin-right:10px;margin-bottom:10px}.cke_rtl .cke_toolgroup *:first-child{-moz-border-radius:0 2px 2px 0;-webkit-border-radius:0 2px 2px 0;border-radius:0 2px 2px 0}.cke_rtl .cke_toolgroup *:last-child{-moz-border-radius:2px 0 0 2px;-webkit-border-radius:2px 0 0 2px;border-radius:2px 0 0 2px}.cke_rtl .cke_toolgroup{float:right;margin-left:6px;margin-right:0}a.cke_button{display:inline-block;height:18px;padding:4px 6px;outline:0;cursor:default;float:left;border:0}.cke_rtl .cke_button{float:right}.cke_hc .cke_button{border:1px solid black;padding:3px 5px;margin:-2px 4px 0 -2px}.cke_button_on{-moz-box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);background:#b5b5b5;background-image:-webkit-gradient(linear,left top,left bottom,from(#aaa),to(#cacaca));background-image:-moz-linear-gradient(top,#aaa,#cacaca);background-image:-webkit-linear-gradient(top,#aaa,#cacaca);background-image:-o-linear-gradient(top,#aaa,#cacaca);background-image:-ms-linear-gradient(top,#aaa,#cacaca);background-image:linear-gradient(top,#aaa,#cacaca);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#aaaaaa',endColorstr='#cacaca')}.cke_hc .cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active,.cke_hc a.cke_button_disabled:hover,.cke_hc a.cke_button_disabled:focus,.cke_hc a.cke_button_disabled:active{border-width:3px;padding:1px 3px}.cke_button_disabled .cke_button_icon{opacity:.3}.cke_hc .cke_button_disabled{opacity:.5}a.cke_button_on:hover,a.cke_button_on:focus,a.cke_button_on:active{-moz-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2)}a.cke_button_off:hover,a.cke_button_off:focus,a.cke_button_off:active,a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{-moz-box-shadow:0 0 1px rgba(0,0,0,.3) inset;-webkit-box-shadow:0 0 1px rgba(0,0,0,.3) inset;box-shadow:0 0 1px rgba(0,0,0,.3) inset;background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}.cke_button_icon{cursor:inherit;background-repeat:no-repeat;margin-top:1px;width:16px;height:16px;float:left;display:inline-block}.cke_rtl .cke_button_icon{float:right}.cke_hc .cke_button_icon{display:none}.cke_button_label{display:none;padding-left:3px;margin-top:1px;line-height:17px;vertical-align:middle;float:left;cursor:default;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.5)}.cke_rtl .cke_button_label{padding-right:3px;padding-left:0;float:right}.cke_hc .cke_button_label{padding:0;display:inline-block;font-size:12px}.cke_button_arrow{display:inline-block;margin:8px 0 0 1px;width:0;height:0;cursor:default;vertical-align:top;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #474747}.cke_rtl .cke_button_arrow{margin-right:5px;margin-left:0}.cke_hc .cke_button_arrow{font-size:10px;margin:3px -2px 0 3px;width:auto;border:0}.cke_toolbar_separator{float:left;background-color:#c0c0c0;background-color:rgba(0,0,0,.2);margin:5px 2px 0;height:18px;width:1px;-webkit-box-shadow:1px 0 1px rgba(255,255,255,.5);-moz-box-shadow:1px 0 1px rgba(255,255,255,.5);box-shadow:1px 0 1px rgba(255,255,255,.5)}.cke_rtl .cke_toolbar_separator{float:right;-webkit-box-shadow:-1px 0 1px rgba(255,255,255,.1);-moz-box-shadow:-1px 0 1px rgba(255,255,255,.1);box-shadow:-1px 0 1px rgba(255,255,255,.1)}.cke_hc .cke_toolbar_separator{width:0;border-left:1px solid;margin:1px 5px 0 0}.cke_toolbar_break{display:block;clear:left}.cke_rtl .cke_toolbar_break{clear:right}.cke_toolbox_collapser{width:12px;height:11px;float:right;margin:11px 0 0;font-size:0;cursor:default;text-align:center;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_toolbox_collapser:hover{background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}.cke_toolbox_collapser.cke_toolbox_collapser_min{margin:0 2px 4px}.cke_rtl .cke_toolbox_collapser{float:left}.cke_toolbox_collapser .cke_arrow{display:inline-block;height:0;width:0;font-size:0;margin-top:1px;border-left:3px solid transparent;border-right:3px solid transparent;border-bottom:3px solid #474747;border-top:3px solid transparent}.cke_toolbox_collapser.cke_toolbox_collapser_min .cke_arrow{margin-top:4px;border-bottom-color:transparent;border-top-color:#474747}.cke_hc .cke_toolbox_collapser .cke_arrow{font-size:8px;width:auto;border:0;margin-top:0;margin-right:2px}.cke_menubutton{display:block}.cke_menuitem span{cursor:default}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#d3d3d3;display:block}.cke_hc .cke_menubutton{padding:2px}.cke_hc .cke_menubutton:hover,.cke_hc .cke_menubutton:focus,.cke_hc .cke_menubutton:active{border:2px solid;padding:0}.cke_menubutton_inner{display:table-row}.cke_menubutton_icon,.cke_menubutton_label,.cke_menuarrow{display:table-cell}.cke_menubutton_icon{background-color:#d7d8d7;opacity:.70;filter:alpha(opacity=70);padding:4px}.cke_hc .cke_menubutton_icon{height:16px;width:0;padding:4px 0}.cke_menubutton:hover .cke_menubutton_icon,.cke_menubutton:focus .cke_menubutton_icon,.cke_menubutton:active .cke_menubutton_icon{background-color:#d0d2d0}.cke_menubutton_disabled:hover .cke_menubutton_icon,.cke_menubutton_disabled:focus .cke_menubutton_icon,.cke_menubutton_disabled:active .cke_menubutton_icon{opacity:.3;filter:alpha(opacity=30)}.cke_menubutton_label{padding:0 5px;background-color:transparent;width:100%;vertical-align:middle}.cke_menubutton_disabled .cke_menubutton_label{opacity:.3;filter:alpha(opacity=30)}.cke_menubutton_on{border:1px solid #dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_menubutton_on .cke_menubutton_icon{padding-right:3px}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#eff0ef}.cke_panel_frame .cke_menubutton_label{display:none}.cke_menuseparator{background-color:#d3d3d3;height:1px;filter:alpha(opacity=70);opacity:.70}.cke_menuarrow{background-image:url(images/arrow.png);background-position:0 10px;background-repeat:no-repeat;padding:0 5px}.cke_rtl .cke_menuarrow{background-position:5px -13px;background-repeat:no-repeat}.cke_menuarrow span{display:none}.cke_hc .cke_menuarrow span{vertical-align:middle;display:inline}.cke_combo{display:inline-block;float:left}.cke_rtl .cke_combo{float:right}.cke_hc .cke_combo{margin-top:-2px}.cke_combo_label{display:none;float:left;line-height:26px;vertical-align:top;margin-right:5px}.cke_rtl .cke_combo_label{float:right;margin-left:5px;margin-right:0}.cke_combo_button{display:inline-block;float:left;margin:0 6px 5px 0;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_combo_off a.cke_combo_button:hover,.cke_combo_off a.cke_combo_button:focus{background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc');outline:0}.cke_combo_off a.cke_combo_button:active,.cke_combo_on a.cke_combo_button{border:1px solid #777;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;background:#b5b5b5;background-image:-webkit-gradient(linear,left top,left bottom,from(#aaa),to(#cacaca));background-image:-moz-linear-gradient(top,#aaa,#cacaca);background-image:-webkit-linear-gradient(top,#aaa,#cacaca);background-image:-o-linear-gradient(top,#aaa,#cacaca);background-image:-ms-linear-gradient(top,#aaa,#cacaca);background-image:linear-gradient(top,#aaa,#cacaca);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#aaaaaa',endColorstr='#cacaca')}.cke_combo_on a.cke_combo_button:hover,.cke_combo_on a.cke_combo_button:focus,.cke_combo_on a.cke_combo_button:active{-moz-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2)}.cke_rtl .cke_combo_button{float:right;margin-left:5px;margin-right:0}.cke_hc a.cke_combo_button{padding:3px}.cke_hc .cke_combo_on a.cke_combo_button,.cke_hc .cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_combo_off a.cke_combo_button:active{border-width:3px;padding:1px}.cke_combo_text{line-height:26px;padding-left:10px;text-overflow:ellipsis;overflow:hidden;float:left;cursor:default;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.5);width:60px}.cke_rtl .cke_combo_text{float:right;text-align:right;padding-left:0;padding-right:10px}.cke_hc .cke_combo_text{line-height:18px;font-size:12px}.cke_combo_open{cursor:default;display:inline-block;font-size:0;height:19px;line-height:17px;margin:1px 7px 1px;width:5px}.cke_hc .cke_combo_open{height:12px}.cke_combo_arrow{margin:11px 0 0;float:left;height:0;width:0;font-size:0;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #474747}.cke_hc .cke_combo_arrow{font-size:10px;width:auto;border:0;margin-top:3px}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{opacity:.3}.cke_path{float:left;margin:-2px 0 2px}.cke_path_item,.cke_path_empty{display:inline-block;float:left;padding:3px 4px;margin-right:2px;cursor:default;text-decoration:none;outline:0;border:0;color:#4c4c4c;text-shadow:0 1px 0 #fff;font-weight:bold;font-size:11px}.cke_rtl .cke_path,.cke_rtl .cke_path_item,.cke_rtl .cke_path_empty{float:right}a.cke_path_item:hover,a.cke_path_item:focus,a.cke_path_item:active{background-color:#bfbfbf;color:#333;text-shadow:0 1px 0 rgba(255,255,255,.5);-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-moz-box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5);-webkit-box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5);box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5)}.cke_hc a.cke_path_item:hover,.cke_hc a.cke_path_item:focus,.cke_hc a.cke_path_item:active{border:2px solid;padding:1px 2px}.cke_button__source_label,.cke_button__sourcedialog_label{display:inline}.cke_combo__fontsize .cke_combo_text{width:30px}.cke_combopanel__fontsize{width:120px}.cke_source{font-family:'Courier New',Monospace;font-size:small;background-color:#fff;white-space:pre}.cke_wysiwyg_frame,.cke_wysiwyg_div{background-color:#fff}.cke_chrome{visibility:inherit}.cke_voice_label{display:none}legend.cke_voice_label{display:none}a.cke_button_disabled,a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{filter:alpha(opacity = 30)}.cke_button_disabled .cke_button_icon{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#00ffffff,endColorstr=#00ffffff)}.cke_button_off:hover,.cke_button_off:focus,.cke_button_off:active{filter:alpha(opacity = 100)}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{filter:alpha(opacity = 30)}.cke_toolbox_collapser{border:1px solid #a6a6a6}.cke_toolbox_collapser .cke_arrow{margin-top:1px}.cke_hc .cke_top,.cke_hc .cke_bottom,.cke_hc .cke_combo_button,.cke_hc a.cke_combo_button:hover,.cke_hc a.cke_combo_button:focus,.cke_hc .cke_toolgroup,.cke_hc .cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active,.cke_hc .cke_toolbox_collapser,.cke_hc .cke_toolbox_collapser:hover,.cke_hc .cke_panel_grouptitle{filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.cke_button__about_icon {background: url(icons.png) no-repeat 0 -0px !important;}.cke_button__bold_icon {background: url(icons.png) no-repeat 0 -24px !important;}.cke_button__italic_icon {background: url(icons.png) no-repeat 0 -48px !important;}.cke_button__strike_icon {background: url(icons.png) no-repeat 0 -72px !important;}.cke_button__subscript_icon {background: url(icons.png) no-repeat 0 -96px !important;}.cke_button__superscript_icon {background: url(icons.png) no-repeat 0 -120px !important;}.cke_button__underline_icon {background: url(icons.png) no-repeat 0 -144px !important;}.cke_rtl .cke_button__copy_icon, .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons.png) no-repeat 0 -168px !important;}.cke_ltr .cke_button__copy_icon {background: url(icons.png) no-repeat 0 -192px !important;}.cke_rtl .cke_button__cut_icon, .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons.png) no-repeat 0 -216px !important;}.cke_ltr .cke_button__cut_icon {background: url(icons.png) no-repeat 0 -240px !important;}.cke_rtl .cke_button__paste_icon, .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons.png) no-repeat 0 -264px !important;}.cke_ltr .cke_button__paste_icon {background: url(icons.png) no-repeat 0 -288px !important;}.cke_button__horizontalrule_icon {background: url(icons.png) no-repeat 0 -312px !important;}.cke_rtl .cke_button__indent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons.png) no-repeat 0 -336px !important;}.cke_ltr .cke_button__indent_icon {background: url(icons.png) no-repeat 0 -360px !important;}.cke_rtl .cke_button__outdent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons.png) no-repeat 0 -384px !important;}.cke_ltr .cke_button__outdent_icon {background: url(icons.png) no-repeat 0 -408px !important;}.cke_rtl .cke_button__anchor_icon, .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons.png) no-repeat 0 -432px !important;}.cke_ltr .cke_button__anchor_icon {background: url(icons.png) no-repeat 0 -456px !important;}.cke_button__link_icon {background: url(icons.png) no-repeat 0 -480px !important;}.cke_button__unlink_icon {background: url(icons.png) no-repeat 0 -504px !important;}.cke_rtl .cke_button__bulletedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons.png) no-repeat 0 -528px !important;}.cke_ltr .cke_button__bulletedlist_icon {background: url(icons.png) no-repeat 0 -552px !important;}.cke_rtl .cke_button__numberedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons.png) no-repeat 0 -576px !important;}.cke_ltr .cke_button__numberedlist_icon {background: url(icons.png) no-repeat 0 -600px !important;}.cke_button__removeformat_icon {background: url(icons.png) no-repeat 0 -624px !important;}.cke_rtl .cke_button__redo_icon, .cke_mixed_dir_content .cke_rtl .cke_button__redo_icon {background: url(icons.png) no-repeat 0 -648px !important;}.cke_ltr .cke_button__redo_icon {background: url(icons.png) no-repeat 0 -672px !important;}.cke_rtl .cke_button__undo_icon, .cke_mixed_dir_content .cke_rtl .cke_button__undo_icon {background: url(icons.png) no-repeat 0 -696px !important;}.cke_ltr .cke_button__undo_icon {background: url(icons.png) no-repeat 0 -720px !important;}.cke_hidpi .cke_button__about_icon {background: url(icons_hidpi.png) no-repeat 0 -0px !important;background-size: 16px !important;}.cke_hidpi .cke_button__bold_icon {background: url(icons_hidpi.png) no-repeat 0 -24px !important;background-size: 16px !important;}.cke_hidpi .cke_button__italic_icon {background: url(icons_hidpi.png) no-repeat 0 -48px !important;background-size: 16px !important;}.cke_hidpi .cke_button__strike_icon {background: url(icons_hidpi.png) no-repeat 0 -72px !important;background-size: 16px !important;}.cke_hidpi .cke_button__subscript_icon {background: url(icons_hidpi.png) no-repeat 0 -96px !important;background-size: 16px !important;}.cke_hidpi .cke_button__superscript_icon {background: url(icons_hidpi.png) no-repeat 0 -120px !important;background-size: 16px !important;}.cke_hidpi .cke_button__underline_icon {background: url(icons_hidpi.png) no-repeat 0 -144px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__copy_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons_hidpi.png) no-repeat 0 -168px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__copy_icon,.cke_ltr.cke_hidpi .cke_button__copy_icon {background: url(icons_hidpi.png) no-repeat 0 -192px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__cut_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons_hidpi.png) no-repeat 0 -216px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__cut_icon,.cke_ltr.cke_hidpi .cke_button__cut_icon {background: url(icons_hidpi.png) no-repeat 0 -240px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__paste_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons_hidpi.png) no-repeat 0 -264px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__paste_icon,.cke_ltr.cke_hidpi .cke_button__paste_icon {background: url(icons_hidpi.png) no-repeat 0 -288px !important;background-size: 16px !important;}.cke_hidpi .cke_button__horizontalrule_icon {background: url(icons_hidpi.png) no-repeat 0 -312px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__indent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons_hidpi.png) no-repeat 0 -336px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__indent_icon,.cke_ltr.cke_hidpi .cke_button__indent_icon {background: url(icons_hidpi.png) no-repeat 0 -360px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__outdent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons_hidpi.png) no-repeat 0 -384px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__outdent_icon,.cke_ltr.cke_hidpi .cke_button__outdent_icon {background: url(icons_hidpi.png) no-repeat 0 -408px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__anchor_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons_hidpi.png) no-repeat 0 -432px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__anchor_icon,.cke_ltr.cke_hidpi .cke_button__anchor_icon {background: url(icons_hidpi.png) no-repeat 0 -456px !important;background-size: 16px !important;}.cke_hidpi .cke_button__link_icon {background: url(icons_hidpi.png) no-repeat 0 -480px !important;background-size: 16px !important;}.cke_hidpi .cke_button__unlink_icon {background: url(icons_hidpi.png) no-repeat 0 -504px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__bulletedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -528px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__bulletedlist_icon,.cke_ltr.cke_hidpi .cke_button__bulletedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -552px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__numberedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -576px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__numberedlist_icon,.cke_ltr.cke_hidpi .cke_button__numberedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -600px !important;background-size: 16px !important;}.cke_hidpi .cke_button__removeformat_icon {background: url(icons_hidpi.png) no-repeat 0 -624px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__redo_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__redo_icon {background: url(icons_hidpi.png) no-repeat 0 -648px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__redo_icon,.cke_ltr.cke_hidpi .cke_button__redo_icon {background: url(icons_hidpi.png) no-repeat 0 -672px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__undo_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__undo_icon {background: url(icons_hidpi.png) no-repeat 0 -696px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__undo_icon,.cke_ltr.cke_hidpi .cke_button__undo_icon {background: url(icons_hidpi.png) no-repeat 0 -720px !important;background-size: 16px !important;}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono/editor_ie7.css b/js/ckeditor/skins/moono/editor_ie7.css
deleted file mode 100644 (file)
index 570326a..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-/*\r
-Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
-For licensing, see LICENSE.md or http://ckeditor.com/license\r
-*/\r
-.cke_reset{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;position:static;-webkit-transition:none;-moz-transition:none;-ms-transition:none;transition:none}.cke_reset_all,.cke_reset_all *{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;position:static;-webkit-transition:none;-moz-transition:none;-ms-transition:none;transition:none;border-collapse:collapse;font:normal normal normal 12px Arial,Helvetica,Tahoma,Verdana,Sans-Serif;color:#000;text-align:left;white-space:nowrap;cursor:auto}.cke_reset_all .cke_rtl *{text-align:right}.cke_reset_all iframe{vertical-align:inherit}.cke_reset_all textarea{white-space:pre}.cke_reset_all textarea,.cke_reset_all input[type="text"],.cke_reset_all input[type="password"]{cursor:text}.cke_reset_all textarea[disabled],.cke_reset_all input[type="text"][disabled],.cke_reset_all input[type="password"][disabled]{cursor:default}.cke_reset_all fieldset{padding:10px;border:2px groove #e0dfe3}.cke_reset_all select{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.cke_chrome{display:block;border:1px solid #b6b6b6;padding:0;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_inner{display:block;-webkit-touch-callout:none;background:#fff;padding:0}.cke_float{border:0}.cke_float .cke_inner{padding-bottom:0}.cke_top,.cke_contents,.cke_bottom{display:block;overflow:hidden}.cke_top{border-bottom:1px solid #b6b6b6;padding:6px 8px 2px;white-space:normal;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_float .cke_top{border:1px solid #b6b6b6;border-bottom-color:#999}.cke_bottom{padding:6px 8px 2px;position:relative;border-top:1px solid #bfbfbf;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#cfd1cf));background-image:-moz-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-webkit-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-o-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-ms-linear-gradient(top,#ebebeb,#cfd1cf);background-image:linear-gradient(top,#ebebeb,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ebebeb',endColorstr='#cfd1cf')}.cke_browser_ios .cke_contents{overflow-y:auto;-webkit-overflow-scrolling:touch}.cke_resizer{width:0;height:0;overflow:hidden;width:0;height:0;overflow:hidden;border-width:10px 10px 0 0;border-color:transparent #666 transparent transparent;border-style:dashed solid dashed dashed;font-size:0;vertical-align:bottom;margin-top:6px;margin-bottom:2px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.3);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.3);box-shadow:0 1px 0 rgba(255,255,255,.3)}.cke_hc .cke_resizer{font-size:15px;width:auto;height:auto;border-width:0}.cke_resizer_ltr{cursor:se-resize;float:right;margin-right:-4px}.cke_resizer_rtl{border-width:10px 0 0 10px;border-color:transparent transparent transparent #a5a5a5;border-style:dashed dashed dashed solid;cursor:sw-resize;float:left;margin-left:-4px;right:auto}.cke_wysiwyg_div{display:block;height:100%;overflow:auto;padding:0 8px;outline-style:none;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.cke_panel{visibility:visible;width:120px;height:100px;overflow:hidden;background-color:#fff;border:1px solid #b6b6b6;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_menu_panel{padding:0;margin:0}.cke_combopanel{width:150px;height:170px}.cke_panel_frame{width:100%;height:100%;font-size:12px;overflow:auto;overflow-x:hidden}.cke_panel_container{overflow-y:auto;overflow-x:hidden}.cke_panel_list{list-style-type:none;margin:3px;padding:0;white-space:nowrap}.cke_panel_listItem{margin:0;padding-bottom:1px}.cke_panel_listItem a{padding:3px 4px;display:block;border:1px solid #fff;color:inherit!important;text-decoration:none;overflow:hidden;text-overflow:ellipsis;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px}* html .cke_panel_listItem a{width:100%;color:#000}*:first-child+html .cke_panel_listItem a{color:#000}.cke_panel_listItem.cke_selected a{border:1px solid #dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_panel_listItem a:hover,.cke_panel_listItem a:focus,.cke_panel_listItem a:active{border-color:#dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_hc .cke_panel_listItem a{border-style:none}.cke_hc .cke_panel_listItem a:hover,.cke_hc .cke_panel_listItem a:focus,.cke_hc .cke_panel_listItem a:active{border:2px solid;padding:1px 2px}.cke_panel_grouptitle{font-size:11px;font-weight:bold;white-space:nowrap;margin:0;padding:4px 6px;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.75);border-bottom:1px solid #b6b6b6;-moz-border-radius:2px 2px 0 0;-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_panel_listItem p,.cke_panel_listItem h1,.cke_panel_listItem h2,.cke_panel_listItem h3,.cke_panel_listItem h4,.cke_panel_listItem h5,.cke_panel_listItem h6,.cke_panel_listItem pre{margin-top:0;margin-bottom:0}.cke_colorblock{padding:3px;font-size:11px;font-family:'Microsoft Sans Serif',Tahoma,Arial,Verdana,Sans-Serif}.cke_colorblock,.cke_colorblock a{text-decoration:none;color:#000}span.cke_colorbox{width:10px;height:10px;border:#808080 1px solid;float:left}.cke_rtl span.cke_colorbox{float:right}a.cke_colorbox{border:#fff 1px solid;padding:2px;float:left;width:12px;height:12px}.cke_rtl a.cke_colorbox{float:right}a:hover.cke_colorbox,a:focus.cke_colorbox,a:active.cke_colorbox{border:#b6b6b6 1px solid;background-color:#e5e5e5}a.cke_colorauto,a.cke_colormore{border:#fff 1px solid;padding:2px;display:block;cursor:pointer}a:hover.cke_colorauto,a:hover.cke_colormore,a:focus.cke_colorauto,a:focus.cke_colormore,a:active.cke_colorauto,a:active.cke_colormore{border:#b6b6b6 1px solid;background-color:#e5e5e5}.cke_toolbar{float:left}.cke_rtl .cke_toolbar{float:right}.cke_toolgroup{float:left;margin:0 6px 5px 0;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_hc .cke_toolgroup{border:0;margin-right:10px;margin-bottom:10px}.cke_rtl .cke_toolgroup *:first-child{-moz-border-radius:0 2px 2px 0;-webkit-border-radius:0 2px 2px 0;border-radius:0 2px 2px 0}.cke_rtl .cke_toolgroup *:last-child{-moz-border-radius:2px 0 0 2px;-webkit-border-radius:2px 0 0 2px;border-radius:2px 0 0 2px}.cke_rtl .cke_toolgroup{float:right;margin-left:6px;margin-right:0}a.cke_button{display:inline-block;height:18px;padding:4px 6px;outline:0;cursor:default;float:left;border:0}.cke_rtl .cke_button{float:right}.cke_hc .cke_button{border:1px solid black;padding:3px 5px;margin:-2px 4px 0 -2px}.cke_button_on{-moz-box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);background:#b5b5b5;background-image:-webkit-gradient(linear,left top,left bottom,from(#aaa),to(#cacaca));background-image:-moz-linear-gradient(top,#aaa,#cacaca);background-image:-webkit-linear-gradient(top,#aaa,#cacaca);background-image:-o-linear-gradient(top,#aaa,#cacaca);background-image:-ms-linear-gradient(top,#aaa,#cacaca);background-image:linear-gradient(top,#aaa,#cacaca);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#aaaaaa',endColorstr='#cacaca')}.cke_hc .cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active,.cke_hc a.cke_button_disabled:hover,.cke_hc a.cke_button_disabled:focus,.cke_hc a.cke_button_disabled:active{border-width:3px;padding:1px 3px}.cke_button_disabled .cke_button_icon{opacity:.3}.cke_hc .cke_button_disabled{opacity:.5}a.cke_button_on:hover,a.cke_button_on:focus,a.cke_button_on:active{-moz-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2)}a.cke_button_off:hover,a.cke_button_off:focus,a.cke_button_off:active,a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{-moz-box-shadow:0 0 1px rgba(0,0,0,.3) inset;-webkit-box-shadow:0 0 1px rgba(0,0,0,.3) inset;box-shadow:0 0 1px rgba(0,0,0,.3) inset;background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}.cke_button_icon{cursor:inherit;background-repeat:no-repeat;margin-top:1px;width:16px;height:16px;float:left;display:inline-block}.cke_rtl .cke_button_icon{float:right}.cke_hc .cke_button_icon{display:none}.cke_button_label{display:none;padding-left:3px;margin-top:1px;line-height:17px;vertical-align:middle;float:left;cursor:default;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.5)}.cke_rtl .cke_button_label{padding-right:3px;padding-left:0;float:right}.cke_hc .cke_button_label{padding:0;display:inline-block;font-size:12px}.cke_button_arrow{display:inline-block;margin:8px 0 0 1px;width:0;height:0;cursor:default;vertical-align:top;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #474747}.cke_rtl .cke_button_arrow{margin-right:5px;margin-left:0}.cke_hc .cke_button_arrow{font-size:10px;margin:3px -2px 0 3px;width:auto;border:0}.cke_toolbar_separator{float:left;background-color:#c0c0c0;background-color:rgba(0,0,0,.2);margin:5px 2px 0;height:18px;width:1px;-webkit-box-shadow:1px 0 1px rgba(255,255,255,.5);-moz-box-shadow:1px 0 1px rgba(255,255,255,.5);box-shadow:1px 0 1px rgba(255,255,255,.5)}.cke_rtl .cke_toolbar_separator{float:right;-webkit-box-shadow:-1px 0 1px rgba(255,255,255,.1);-moz-box-shadow:-1px 0 1px rgba(255,255,255,.1);box-shadow:-1px 0 1px rgba(255,255,255,.1)}.cke_hc .cke_toolbar_separator{width:0;border-left:1px solid;margin:1px 5px 0 0}.cke_toolbar_break{display:block;clear:left}.cke_rtl .cke_toolbar_break{clear:right}.cke_toolbox_collapser{width:12px;height:11px;float:right;margin:11px 0 0;font-size:0;cursor:default;text-align:center;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_toolbox_collapser:hover{background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}.cke_toolbox_collapser.cke_toolbox_collapser_min{margin:0 2px 4px}.cke_rtl .cke_toolbox_collapser{float:left}.cke_toolbox_collapser .cke_arrow{display:inline-block;height:0;width:0;font-size:0;margin-top:1px;border-left:3px solid transparent;border-right:3px solid transparent;border-bottom:3px solid #474747;border-top:3px solid transparent}.cke_toolbox_collapser.cke_toolbox_collapser_min .cke_arrow{margin-top:4px;border-bottom-color:transparent;border-top-color:#474747}.cke_hc .cke_toolbox_collapser .cke_arrow{font-size:8px;width:auto;border:0;margin-top:0;margin-right:2px}.cke_menubutton{display:block}.cke_menuitem span{cursor:default}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#d3d3d3;display:block}.cke_hc .cke_menubutton{padding:2px}.cke_hc .cke_menubutton:hover,.cke_hc .cke_menubutton:focus,.cke_hc .cke_menubutton:active{border:2px solid;padding:0}.cke_menubutton_inner{display:table-row}.cke_menubutton_icon,.cke_menubutton_label,.cke_menuarrow{display:table-cell}.cke_menubutton_icon{background-color:#d7d8d7;opacity:.70;filter:alpha(opacity=70);padding:4px}.cke_hc .cke_menubutton_icon{height:16px;width:0;padding:4px 0}.cke_menubutton:hover .cke_menubutton_icon,.cke_menubutton:focus .cke_menubutton_icon,.cke_menubutton:active .cke_menubutton_icon{background-color:#d0d2d0}.cke_menubutton_disabled:hover .cke_menubutton_icon,.cke_menubutton_disabled:focus .cke_menubutton_icon,.cke_menubutton_disabled:active .cke_menubutton_icon{opacity:.3;filter:alpha(opacity=30)}.cke_menubutton_label{padding:0 5px;background-color:transparent;width:100%;vertical-align:middle}.cke_menubutton_disabled .cke_menubutton_label{opacity:.3;filter:alpha(opacity=30)}.cke_menubutton_on{border:1px solid #dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_menubutton_on .cke_menubutton_icon{padding-right:3px}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#eff0ef}.cke_panel_frame .cke_menubutton_label{display:none}.cke_menuseparator{background-color:#d3d3d3;height:1px;filter:alpha(opacity=70);opacity:.70}.cke_menuarrow{background-image:url(images/arrow.png);background-position:0 10px;background-repeat:no-repeat;padding:0 5px}.cke_rtl .cke_menuarrow{background-position:5px -13px;background-repeat:no-repeat}.cke_menuarrow span{display:none}.cke_hc .cke_menuarrow span{vertical-align:middle;display:inline}.cke_combo{display:inline-block;float:left}.cke_rtl .cke_combo{float:right}.cke_hc .cke_combo{margin-top:-2px}.cke_combo_label{display:none;float:left;line-height:26px;vertical-align:top;margin-right:5px}.cke_rtl .cke_combo_label{float:right;margin-left:5px;margin-right:0}.cke_combo_button{display:inline-block;float:left;margin:0 6px 5px 0;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_combo_off a.cke_combo_button:hover,.cke_combo_off a.cke_combo_button:focus{background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc');outline:0}.cke_combo_off a.cke_combo_button:active,.cke_combo_on a.cke_combo_button{border:1px solid #777;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;background:#b5b5b5;background-image:-webkit-gradient(linear,left top,left bottom,from(#aaa),to(#cacaca));background-image:-moz-linear-gradient(top,#aaa,#cacaca);background-image:-webkit-linear-gradient(top,#aaa,#cacaca);background-image:-o-linear-gradient(top,#aaa,#cacaca);background-image:-ms-linear-gradient(top,#aaa,#cacaca);background-image:linear-gradient(top,#aaa,#cacaca);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#aaaaaa',endColorstr='#cacaca')}.cke_combo_on a.cke_combo_button:hover,.cke_combo_on a.cke_combo_button:focus,.cke_combo_on a.cke_combo_button:active{-moz-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2)}.cke_rtl .cke_combo_button{float:right;margin-left:5px;margin-right:0}.cke_hc a.cke_combo_button{padding:3px}.cke_hc .cke_combo_on a.cke_combo_button,.cke_hc .cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_combo_off a.cke_combo_button:active{border-width:3px;padding:1px}.cke_combo_text{line-height:26px;padding-left:10px;text-overflow:ellipsis;overflow:hidden;float:left;cursor:default;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.5);width:60px}.cke_rtl .cke_combo_text{float:right;text-align:right;padding-left:0;padding-right:10px}.cke_hc .cke_combo_text{line-height:18px;font-size:12px}.cke_combo_open{cursor:default;display:inline-block;font-size:0;height:19px;line-height:17px;margin:1px 7px 1px;width:5px}.cke_hc .cke_combo_open{height:12px}.cke_combo_arrow{margin:11px 0 0;float:left;height:0;width:0;font-size:0;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #474747}.cke_hc .cke_combo_arrow{font-size:10px;width:auto;border:0;margin-top:3px}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{opacity:.3}.cke_path{float:left;margin:-2px 0 2px}.cke_path_item,.cke_path_empty{display:inline-block;float:left;padding:3px 4px;margin-right:2px;cursor:default;text-decoration:none;outline:0;border:0;color:#4c4c4c;text-shadow:0 1px 0 #fff;font-weight:bold;font-size:11px}.cke_rtl .cke_path,.cke_rtl .cke_path_item,.cke_rtl .cke_path_empty{float:right}a.cke_path_item:hover,a.cke_path_item:focus,a.cke_path_item:active{background-color:#bfbfbf;color:#333;text-shadow:0 1px 0 rgba(255,255,255,.5);-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-moz-box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5);-webkit-box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5);box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5)}.cke_hc a.cke_path_item:hover,.cke_hc a.cke_path_item:focus,.cke_hc a.cke_path_item:active{border:2px solid;padding:1px 2px}.cke_button__source_label,.cke_button__sourcedialog_label{display:inline}.cke_combo__fontsize .cke_combo_text{width:30px}.cke_combopanel__fontsize{width:120px}.cke_source{font-family:'Courier New',Monospace;font-size:small;background-color:#fff;white-space:pre}.cke_wysiwyg_frame,.cke_wysiwyg_div{background-color:#fff}.cke_chrome{visibility:inherit}.cke_voice_label{display:none}legend.cke_voice_label{display:none}a.cke_button_disabled,a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{filter:alpha(opacity = 30)}.cke_button_disabled .cke_button_icon{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#00ffffff,endColorstr=#00ffffff)}.cke_button_off:hover,.cke_button_off:focus,.cke_button_off:active{filter:alpha(opacity = 100)}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{filter:alpha(opacity = 30)}.cke_toolbox_collapser{border:1px solid #a6a6a6}.cke_toolbox_collapser .cke_arrow{margin-top:1px}.cke_hc .cke_top,.cke_hc .cke_bottom,.cke_hc .cke_combo_button,.cke_hc a.cke_combo_button:hover,.cke_hc a.cke_combo_button:focus,.cke_hc .cke_toolgroup,.cke_hc .cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active,.cke_hc .cke_toolbox_collapser,.cke_hc .cke_toolbox_collapser:hover,.cke_hc .cke_panel_grouptitle{filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.cke_rtl .cke_toolgroup,.cke_rtl .cke_toolbar_separator,.cke_rtl .cke_button,.cke_rtl .cke_button *,.cke_rtl .cke_combo,.cke_rtl .cke_combo *,.cke_rtl .cke_path_item,.cke_rtl .cke_path_item *,.cke_rtl .cke_path_empty{float:none}.cke_rtl .cke_toolgroup,.cke_rtl .cke_toolbar_separator,.cke_rtl .cke_combo_button,.cke_rtl .cke_combo_button *,.cke_rtl .cke_button,.cke_rtl .cke_button_icon,{display:inline-block;vertical-align:top}.cke_toolbox{display:inline-block;padding-bottom:5px;height:100%}.cke_rtl .cke_toolbox{padding-bottom:0}.cke_toolbar{margin-bottom:5px}.cke_rtl .cke_toolbar{margin-bottom:0}.cke_toolgroup{height:26px}.cke_toolgroup,.cke_combo{position:relative}a.cke_button{float:none;vertical-align:top}.cke_toolbar_separator{display:inline-block;float:none;vertical-align:top;background-color:#c0c0c0}.cke_toolbox_collapser .cke_arrow{margin-top:0}.cke_toolbox_collapser .cke_arrow{border-width:4px}.cke_toolbox_collapser.cke_toolbox_collapser_min .cke_arrow{border-width:3px}.cke_rtl .cke_button_arrow{padding-top:8px;margin-right:2px}.cke_rtl .cke_combo_inlinelabel{display:table-cell;vertical-align:middle}.cke_menubutton{display:block;height:24px}.cke_menubutton_inner{display:block;position:relative}.cke_menubutton_icon{height:16px;width:16px}.cke_menubutton_icon,.cke_menubutton_label,.cke_menuarrow{display:inline-block}.cke_menubutton_label{width:auto;vertical-align:top;line-height:24px;height:24px;margin:0 10px 0 0}.cke_menuarrow{width:5px;height:6px;padding:0;position:absolute;right:8px;top:10px;background-position:0 0}.cke_rtl .cke_menubutton_icon{position:absolute;right:0;top:0}.cke_rtl .cke_menubutton_label{float:right;clear:both;margin:0 24px 0 10px}.cke_hc .cke_rtl .cke_menubutton_label{margin-right:0}.cke_rtl .cke_menuarrow{left:8px;right:auto;background-position:0 -24px}.cke_hc .cke_menuarrow{top:5px;padding:0 5px}.cke_rtl input.cke_dialog_ui_input_text,.cke_rtl input.cke_dialog_ui_input_password{position:relative}.cke_wysiwyg_div{padding-top:0!important;padding-bottom:0!important}.cke_button__about_icon {background: url(icons.png) no-repeat 0 -0px !important;}.cke_button__bold_icon {background: url(icons.png) no-repeat 0 -24px !important;}.cke_button__italic_icon {background: url(icons.png) no-repeat 0 -48px !important;}.cke_button__strike_icon {background: url(icons.png) no-repeat 0 -72px !important;}.cke_button__subscript_icon {background: url(icons.png) no-repeat 0 -96px !important;}.cke_button__superscript_icon {background: url(icons.png) no-repeat 0 -120px !important;}.cke_button__underline_icon {background: url(icons.png) no-repeat 0 -144px !important;}.cke_rtl .cke_button__copy_icon, .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons.png) no-repeat 0 -168px !important;}.cke_ltr .cke_button__copy_icon {background: url(icons.png) no-repeat 0 -192px !important;}.cke_rtl .cke_button__cut_icon, .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons.png) no-repeat 0 -216px !important;}.cke_ltr .cke_button__cut_icon {background: url(icons.png) no-repeat 0 -240px !important;}.cke_rtl .cke_button__paste_icon, .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons.png) no-repeat 0 -264px !important;}.cke_ltr .cke_button__paste_icon {background: url(icons.png) no-repeat 0 -288px !important;}.cke_button__horizontalrule_icon {background: url(icons.png) no-repeat 0 -312px !important;}.cke_rtl .cke_button__indent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons.png) no-repeat 0 -336px !important;}.cke_ltr .cke_button__indent_icon {background: url(icons.png) no-repeat 0 -360px !important;}.cke_rtl .cke_button__outdent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons.png) no-repeat 0 -384px !important;}.cke_ltr .cke_button__outdent_icon {background: url(icons.png) no-repeat 0 -408px !important;}.cke_rtl .cke_button__anchor_icon, .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons.png) no-repeat 0 -432px !important;}.cke_ltr .cke_button__anchor_icon {background: url(icons.png) no-repeat 0 -456px !important;}.cke_button__link_icon {background: url(icons.png) no-repeat 0 -480px !important;}.cke_button__unlink_icon {background: url(icons.png) no-repeat 0 -504px !important;}.cke_rtl .cke_button__bulletedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons.png) no-repeat 0 -528px !important;}.cke_ltr .cke_button__bulletedlist_icon {background: url(icons.png) no-repeat 0 -552px !important;}.cke_rtl .cke_button__numberedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons.png) no-repeat 0 -576px !important;}.cke_ltr .cke_button__numberedlist_icon {background: url(icons.png) no-repeat 0 -600px !important;}.cke_button__removeformat_icon {background: url(icons.png) no-repeat 0 -624px !important;}.cke_rtl .cke_button__redo_icon, .cke_mixed_dir_content .cke_rtl .cke_button__redo_icon {background: url(icons.png) no-repeat 0 -648px !important;}.cke_ltr .cke_button__redo_icon {background: url(icons.png) no-repeat 0 -672px !important;}.cke_rtl .cke_button__undo_icon, .cke_mixed_dir_content .cke_rtl .cke_button__undo_icon {background: url(icons.png) no-repeat 0 -696px !important;}.cke_ltr .cke_button__undo_icon {background: url(icons.png) no-repeat 0 -720px !important;}.cke_hidpi .cke_button__about_icon {background: url(icons_hidpi.png) no-repeat 0 -0px !important;background-size: 16px !important;}.cke_hidpi .cke_button__bold_icon {background: url(icons_hidpi.png) no-repeat 0 -24px !important;background-size: 16px !important;}.cke_hidpi .cke_button__italic_icon {background: url(icons_hidpi.png) no-repeat 0 -48px !important;background-size: 16px !important;}.cke_hidpi .cke_button__strike_icon {background: url(icons_hidpi.png) no-repeat 0 -72px !important;background-size: 16px !important;}.cke_hidpi .cke_button__subscript_icon {background: url(icons_hidpi.png) no-repeat 0 -96px !important;background-size: 16px !important;}.cke_hidpi .cke_button__superscript_icon {background: url(icons_hidpi.png) no-repeat 0 -120px !important;background-size: 16px !important;}.cke_hidpi .cke_button__underline_icon {background: url(icons_hidpi.png) no-repeat 0 -144px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__copy_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons_hidpi.png) no-repeat 0 -168px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__copy_icon,.cke_ltr.cke_hidpi .cke_button__copy_icon {background: url(icons_hidpi.png) no-repeat 0 -192px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__cut_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons_hidpi.png) no-repeat 0 -216px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__cut_icon,.cke_ltr.cke_hidpi .cke_button__cut_icon {background: url(icons_hidpi.png) no-repeat 0 -240px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__paste_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons_hidpi.png) no-repeat 0 -264px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__paste_icon,.cke_ltr.cke_hidpi .cke_button__paste_icon {background: url(icons_hidpi.png) no-repeat 0 -288px !important;background-size: 16px !important;}.cke_hidpi .cke_button__horizontalrule_icon {background: url(icons_hidpi.png) no-repeat 0 -312px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__indent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons_hidpi.png) no-repeat 0 -336px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__indent_icon,.cke_ltr.cke_hidpi .cke_button__indent_icon {background: url(icons_hidpi.png) no-repeat 0 -360px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__outdent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons_hidpi.png) no-repeat 0 -384px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__outdent_icon,.cke_ltr.cke_hidpi .cke_button__outdent_icon {background: url(icons_hidpi.png) no-repeat 0 -408px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__anchor_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons_hidpi.png) no-repeat 0 -432px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__anchor_icon,.cke_ltr.cke_hidpi .cke_button__anchor_icon {background: url(icons_hidpi.png) no-repeat 0 -456px !important;background-size: 16px !important;}.cke_hidpi .cke_button__link_icon {background: url(icons_hidpi.png) no-repeat 0 -480px !important;background-size: 16px !important;}.cke_hidpi .cke_button__unlink_icon {background: url(icons_hidpi.png) no-repeat 0 -504px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__bulletedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -528px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__bulletedlist_icon,.cke_ltr.cke_hidpi .cke_button__bulletedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -552px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__numberedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -576px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__numberedlist_icon,.cke_ltr.cke_hidpi .cke_button__numberedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -600px !important;background-size: 16px !important;}.cke_hidpi .cke_button__removeformat_icon {background: url(icons_hidpi.png) no-repeat 0 -624px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__redo_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__redo_icon {background: url(icons_hidpi.png) no-repeat 0 -648px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__redo_icon,.cke_ltr.cke_hidpi .cke_button__redo_icon {background: url(icons_hidpi.png) no-repeat 0 -672px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__undo_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__undo_icon {background: url(icons_hidpi.png) no-repeat 0 -696px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__undo_icon,.cke_ltr.cke_hidpi .cke_button__undo_icon {background: url(icons_hidpi.png) no-repeat 0 -720px !important;background-size: 16px !important;}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono/editor_ie8.css b/js/ckeditor/skins/moono/editor_ie8.css
deleted file mode 100644 (file)
index 79228a3..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-/*\r
-Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
-For licensing, see LICENSE.md or http://ckeditor.com/license\r
-*/\r
-.cke_reset{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;position:static;-webkit-transition:none;-moz-transition:none;-ms-transition:none;transition:none}.cke_reset_all,.cke_reset_all *{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;position:static;-webkit-transition:none;-moz-transition:none;-ms-transition:none;transition:none;border-collapse:collapse;font:normal normal normal 12px Arial,Helvetica,Tahoma,Verdana,Sans-Serif;color:#000;text-align:left;white-space:nowrap;cursor:auto}.cke_reset_all .cke_rtl *{text-align:right}.cke_reset_all iframe{vertical-align:inherit}.cke_reset_all textarea{white-space:pre}.cke_reset_all textarea,.cke_reset_all input[type="text"],.cke_reset_all input[type="password"]{cursor:text}.cke_reset_all textarea[disabled],.cke_reset_all input[type="text"][disabled],.cke_reset_all input[type="password"][disabled]{cursor:default}.cke_reset_all fieldset{padding:10px;border:2px groove #e0dfe3}.cke_reset_all select{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.cke_chrome{display:block;border:1px solid #b6b6b6;padding:0;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_inner{display:block;-webkit-touch-callout:none;background:#fff;padding:0}.cke_float{border:0}.cke_float .cke_inner{padding-bottom:0}.cke_top,.cke_contents,.cke_bottom{display:block;overflow:hidden}.cke_top{border-bottom:1px solid #b6b6b6;padding:6px 8px 2px;white-space:normal;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_float .cke_top{border:1px solid #b6b6b6;border-bottom-color:#999}.cke_bottom{padding:6px 8px 2px;position:relative;border-top:1px solid #bfbfbf;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#cfd1cf));background-image:-moz-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-webkit-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-o-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-ms-linear-gradient(top,#ebebeb,#cfd1cf);background-image:linear-gradient(top,#ebebeb,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ebebeb',endColorstr='#cfd1cf')}.cke_browser_ios .cke_contents{overflow-y:auto;-webkit-overflow-scrolling:touch}.cke_resizer{width:0;height:0;overflow:hidden;width:0;height:0;overflow:hidden;border-width:10px 10px 0 0;border-color:transparent #666 transparent transparent;border-style:dashed solid dashed dashed;font-size:0;vertical-align:bottom;margin-top:6px;margin-bottom:2px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.3);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.3);box-shadow:0 1px 0 rgba(255,255,255,.3)}.cke_hc .cke_resizer{font-size:15px;width:auto;height:auto;border-width:0}.cke_resizer_ltr{cursor:se-resize;float:right;margin-right:-4px}.cke_resizer_rtl{border-width:10px 0 0 10px;border-color:transparent transparent transparent #a5a5a5;border-style:dashed dashed dashed solid;cursor:sw-resize;float:left;margin-left:-4px;right:auto}.cke_wysiwyg_div{display:block;height:100%;overflow:auto;padding:0 8px;outline-style:none;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.cke_panel{visibility:visible;width:120px;height:100px;overflow:hidden;background-color:#fff;border:1px solid #b6b6b6;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_menu_panel{padding:0;margin:0}.cke_combopanel{width:150px;height:170px}.cke_panel_frame{width:100%;height:100%;font-size:12px;overflow:auto;overflow-x:hidden}.cke_panel_container{overflow-y:auto;overflow-x:hidden}.cke_panel_list{list-style-type:none;margin:3px;padding:0;white-space:nowrap}.cke_panel_listItem{margin:0;padding-bottom:1px}.cke_panel_listItem a{padding:3px 4px;display:block;border:1px solid #fff;color:inherit!important;text-decoration:none;overflow:hidden;text-overflow:ellipsis;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px}* html .cke_panel_listItem a{width:100%;color:#000}*:first-child+html .cke_panel_listItem a{color:#000}.cke_panel_listItem.cke_selected a{border:1px solid #dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_panel_listItem a:hover,.cke_panel_listItem a:focus,.cke_panel_listItem a:active{border-color:#dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_hc .cke_panel_listItem a{border-style:none}.cke_hc .cke_panel_listItem a:hover,.cke_hc .cke_panel_listItem a:focus,.cke_hc .cke_panel_listItem a:active{border:2px solid;padding:1px 2px}.cke_panel_grouptitle{font-size:11px;font-weight:bold;white-space:nowrap;margin:0;padding:4px 6px;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.75);border-bottom:1px solid #b6b6b6;-moz-border-radius:2px 2px 0 0;-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_panel_listItem p,.cke_panel_listItem h1,.cke_panel_listItem h2,.cke_panel_listItem h3,.cke_panel_listItem h4,.cke_panel_listItem h5,.cke_panel_listItem h6,.cke_panel_listItem pre{margin-top:0;margin-bottom:0}.cke_colorblock{padding:3px;font-size:11px;font-family:'Microsoft Sans Serif',Tahoma,Arial,Verdana,Sans-Serif}.cke_colorblock,.cke_colorblock a{text-decoration:none;color:#000}span.cke_colorbox{width:10px;height:10px;border:#808080 1px solid;float:left}.cke_rtl span.cke_colorbox{float:right}a.cke_colorbox{border:#fff 1px solid;padding:2px;float:left;width:12px;height:12px}.cke_rtl a.cke_colorbox{float:right}a:hover.cke_colorbox,a:focus.cke_colorbox,a:active.cke_colorbox{border:#b6b6b6 1px solid;background-color:#e5e5e5}a.cke_colorauto,a.cke_colormore{border:#fff 1px solid;padding:2px;display:block;cursor:pointer}a:hover.cke_colorauto,a:hover.cke_colormore,a:focus.cke_colorauto,a:focus.cke_colormore,a:active.cke_colorauto,a:active.cke_colormore{border:#b6b6b6 1px solid;background-color:#e5e5e5}.cke_toolbar{float:left}.cke_rtl .cke_toolbar{float:right}.cke_toolgroup{float:left;margin:0 6px 5px 0;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_hc .cke_toolgroup{border:0;margin-right:10px;margin-bottom:10px}.cke_rtl .cke_toolgroup *:first-child{-moz-border-radius:0 2px 2px 0;-webkit-border-radius:0 2px 2px 0;border-radius:0 2px 2px 0}.cke_rtl .cke_toolgroup *:last-child{-moz-border-radius:2px 0 0 2px;-webkit-border-radius:2px 0 0 2px;border-radius:2px 0 0 2px}.cke_rtl .cke_toolgroup{float:right;margin-left:6px;margin-right:0}a.cke_button{display:inline-block;height:18px;padding:4px 6px;outline:0;cursor:default;float:left;border:0}.cke_rtl .cke_button{float:right}.cke_hc .cke_button{border:1px solid black;padding:3px 5px;margin:-2px 4px 0 -2px}.cke_button_on{-moz-box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);background:#b5b5b5;background-image:-webkit-gradient(linear,left top,left bottom,from(#aaa),to(#cacaca));background-image:-moz-linear-gradient(top,#aaa,#cacaca);background-image:-webkit-linear-gradient(top,#aaa,#cacaca);background-image:-o-linear-gradient(top,#aaa,#cacaca);background-image:-ms-linear-gradient(top,#aaa,#cacaca);background-image:linear-gradient(top,#aaa,#cacaca);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#aaaaaa',endColorstr='#cacaca')}.cke_hc .cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active,.cke_hc a.cke_button_disabled:hover,.cke_hc a.cke_button_disabled:focus,.cke_hc a.cke_button_disabled:active{border-width:3px;padding:1px 3px}.cke_button_disabled .cke_button_icon{opacity:.3}.cke_hc .cke_button_disabled{opacity:.5}a.cke_button_on:hover,a.cke_button_on:focus,a.cke_button_on:active{-moz-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2)}a.cke_button_off:hover,a.cke_button_off:focus,a.cke_button_off:active,a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{-moz-box-shadow:0 0 1px rgba(0,0,0,.3) inset;-webkit-box-shadow:0 0 1px rgba(0,0,0,.3) inset;box-shadow:0 0 1px rgba(0,0,0,.3) inset;background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}.cke_button_icon{cursor:inherit;background-repeat:no-repeat;margin-top:1px;width:16px;height:16px;float:left;display:inline-block}.cke_rtl .cke_button_icon{float:right}.cke_hc .cke_button_icon{display:none}.cke_button_label{display:none;padding-left:3px;margin-top:1px;line-height:17px;vertical-align:middle;float:left;cursor:default;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.5)}.cke_rtl .cke_button_label{padding-right:3px;padding-left:0;float:right}.cke_hc .cke_button_label{padding:0;display:inline-block;font-size:12px}.cke_button_arrow{display:inline-block;margin:8px 0 0 1px;width:0;height:0;cursor:default;vertical-align:top;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #474747}.cke_rtl .cke_button_arrow{margin-right:5px;margin-left:0}.cke_hc .cke_button_arrow{font-size:10px;margin:3px -2px 0 3px;width:auto;border:0}.cke_toolbar_separator{float:left;background-color:#c0c0c0;background-color:rgba(0,0,0,.2);margin:5px 2px 0;height:18px;width:1px;-webkit-box-shadow:1px 0 1px rgba(255,255,255,.5);-moz-box-shadow:1px 0 1px rgba(255,255,255,.5);box-shadow:1px 0 1px rgba(255,255,255,.5)}.cke_rtl .cke_toolbar_separator{float:right;-webkit-box-shadow:-1px 0 1px rgba(255,255,255,.1);-moz-box-shadow:-1px 0 1px rgba(255,255,255,.1);box-shadow:-1px 0 1px rgba(255,255,255,.1)}.cke_hc .cke_toolbar_separator{width:0;border-left:1px solid;margin:1px 5px 0 0}.cke_toolbar_break{display:block;clear:left}.cke_rtl .cke_toolbar_break{clear:right}.cke_toolbox_collapser{width:12px;height:11px;float:right;margin:11px 0 0;font-size:0;cursor:default;text-align:center;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_toolbox_collapser:hover{background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}.cke_toolbox_collapser.cke_toolbox_collapser_min{margin:0 2px 4px}.cke_rtl .cke_toolbox_collapser{float:left}.cke_toolbox_collapser .cke_arrow{display:inline-block;height:0;width:0;font-size:0;margin-top:1px;border-left:3px solid transparent;border-right:3px solid transparent;border-bottom:3px solid #474747;border-top:3px solid transparent}.cke_toolbox_collapser.cke_toolbox_collapser_min .cke_arrow{margin-top:4px;border-bottom-color:transparent;border-top-color:#474747}.cke_hc .cke_toolbox_collapser .cke_arrow{font-size:8px;width:auto;border:0;margin-top:0;margin-right:2px}.cke_menubutton{display:block}.cke_menuitem span{cursor:default}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#d3d3d3;display:block}.cke_hc .cke_menubutton{padding:2px}.cke_hc .cke_menubutton:hover,.cke_hc .cke_menubutton:focus,.cke_hc .cke_menubutton:active{border:2px solid;padding:0}.cke_menubutton_inner{display:table-row}.cke_menubutton_icon,.cke_menubutton_label,.cke_menuarrow{display:table-cell}.cke_menubutton_icon{background-color:#d7d8d7;opacity:.70;filter:alpha(opacity=70);padding:4px}.cke_hc .cke_menubutton_icon{height:16px;width:0;padding:4px 0}.cke_menubutton:hover .cke_menubutton_icon,.cke_menubutton:focus .cke_menubutton_icon,.cke_menubutton:active .cke_menubutton_icon{background-color:#d0d2d0}.cke_menubutton_disabled:hover .cke_menubutton_icon,.cke_menubutton_disabled:focus .cke_menubutton_icon,.cke_menubutton_disabled:active .cke_menubutton_icon{opacity:.3;filter:alpha(opacity=30)}.cke_menubutton_label{padding:0 5px;background-color:transparent;width:100%;vertical-align:middle}.cke_menubutton_disabled .cke_menubutton_label{opacity:.3;filter:alpha(opacity=30)}.cke_menubutton_on{border:1px solid #dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_menubutton_on .cke_menubutton_icon{padding-right:3px}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#eff0ef}.cke_panel_frame .cke_menubutton_label{display:none}.cke_menuseparator{background-color:#d3d3d3;height:1px;filter:alpha(opacity=70);opacity:.70}.cke_menuarrow{background-image:url(images/arrow.png);background-position:0 10px;background-repeat:no-repeat;padding:0 5px}.cke_rtl .cke_menuarrow{background-position:5px -13px;background-repeat:no-repeat}.cke_menuarrow span{display:none}.cke_hc .cke_menuarrow span{vertical-align:middle;display:inline}.cke_combo{display:inline-block;float:left}.cke_rtl .cke_combo{float:right}.cke_hc .cke_combo{margin-top:-2px}.cke_combo_label{display:none;float:left;line-height:26px;vertical-align:top;margin-right:5px}.cke_rtl .cke_combo_label{float:right;margin-left:5px;margin-right:0}.cke_combo_button{display:inline-block;float:left;margin:0 6px 5px 0;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_combo_off a.cke_combo_button:hover,.cke_combo_off a.cke_combo_button:focus{background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc');outline:0}.cke_combo_off a.cke_combo_button:active,.cke_combo_on a.cke_combo_button{border:1px solid #777;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;background:#b5b5b5;background-image:-webkit-gradient(linear,left top,left bottom,from(#aaa),to(#cacaca));background-image:-moz-linear-gradient(top,#aaa,#cacaca);background-image:-webkit-linear-gradient(top,#aaa,#cacaca);background-image:-o-linear-gradient(top,#aaa,#cacaca);background-image:-ms-linear-gradient(top,#aaa,#cacaca);background-image:linear-gradient(top,#aaa,#cacaca);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#aaaaaa',endColorstr='#cacaca')}.cke_combo_on a.cke_combo_button:hover,.cke_combo_on a.cke_combo_button:focus,.cke_combo_on a.cke_combo_button:active{-moz-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2)}.cke_rtl .cke_combo_button{float:right;margin-left:5px;margin-right:0}.cke_hc a.cke_combo_button{padding:3px}.cke_hc .cke_combo_on a.cke_combo_button,.cke_hc .cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_combo_off a.cke_combo_button:active{border-width:3px;padding:1px}.cke_combo_text{line-height:26px;padding-left:10px;text-overflow:ellipsis;overflow:hidden;float:left;cursor:default;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.5);width:60px}.cke_rtl .cke_combo_text{float:right;text-align:right;padding-left:0;padding-right:10px}.cke_hc .cke_combo_text{line-height:18px;font-size:12px}.cke_combo_open{cursor:default;display:inline-block;font-size:0;height:19px;line-height:17px;margin:1px 7px 1px;width:5px}.cke_hc .cke_combo_open{height:12px}.cke_combo_arrow{margin:11px 0 0;float:left;height:0;width:0;font-size:0;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #474747}.cke_hc .cke_combo_arrow{font-size:10px;width:auto;border:0;margin-top:3px}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{opacity:.3}.cke_path{float:left;margin:-2px 0 2px}.cke_path_item,.cke_path_empty{display:inline-block;float:left;padding:3px 4px;margin-right:2px;cursor:default;text-decoration:none;outline:0;border:0;color:#4c4c4c;text-shadow:0 1px 0 #fff;font-weight:bold;font-size:11px}.cke_rtl .cke_path,.cke_rtl .cke_path_item,.cke_rtl .cke_path_empty{float:right}a.cke_path_item:hover,a.cke_path_item:focus,a.cke_path_item:active{background-color:#bfbfbf;color:#333;text-shadow:0 1px 0 rgba(255,255,255,.5);-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-moz-box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5);-webkit-box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5);box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5)}.cke_hc a.cke_path_item:hover,.cke_hc a.cke_path_item:focus,.cke_hc a.cke_path_item:active{border:2px solid;padding:1px 2px}.cke_button__source_label,.cke_button__sourcedialog_label{display:inline}.cke_combo__fontsize .cke_combo_text{width:30px}.cke_combopanel__fontsize{width:120px}.cke_source{font-family:'Courier New',Monospace;font-size:small;background-color:#fff;white-space:pre}.cke_wysiwyg_frame,.cke_wysiwyg_div{background-color:#fff}.cke_chrome{visibility:inherit}.cke_voice_label{display:none}legend.cke_voice_label{display:none}a.cke_button_disabled,a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{filter:alpha(opacity = 30)}.cke_button_disabled .cke_button_icon{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#00ffffff,endColorstr=#00ffffff)}.cke_button_off:hover,.cke_button_off:focus,.cke_button_off:active{filter:alpha(opacity = 100)}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{filter:alpha(opacity = 30)}.cke_toolbox_collapser{border:1px solid #a6a6a6}.cke_toolbox_collapser .cke_arrow{margin-top:1px}.cke_hc .cke_top,.cke_hc .cke_bottom,.cke_hc .cke_combo_button,.cke_hc a.cke_combo_button:hover,.cke_hc a.cke_combo_button:focus,.cke_hc .cke_toolgroup,.cke_hc .cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active,.cke_hc .cke_toolbox_collapser,.cke_hc .cke_toolbox_collapser:hover,.cke_hc .cke_panel_grouptitle{filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.cke_toolbox_collapser .cke_arrow{border-width:4px}.cke_toolbox_collapser.cke_toolbox_collapser_min .cke_arrow{border-width:3px}.cke_toolbox_collapser .cke_arrow{margin-top:0}.cke_button__about_icon {background: url(icons.png) no-repeat 0 -0px !important;}.cke_button__bold_icon {background: url(icons.png) no-repeat 0 -24px !important;}.cke_button__italic_icon {background: url(icons.png) no-repeat 0 -48px !important;}.cke_button__strike_icon {background: url(icons.png) no-repeat 0 -72px !important;}.cke_button__subscript_icon {background: url(icons.png) no-repeat 0 -96px !important;}.cke_button__superscript_icon {background: url(icons.png) no-repeat 0 -120px !important;}.cke_button__underline_icon {background: url(icons.png) no-repeat 0 -144px !important;}.cke_rtl .cke_button__copy_icon, .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons.png) no-repeat 0 -168px !important;}.cke_ltr .cke_button__copy_icon {background: url(icons.png) no-repeat 0 -192px !important;}.cke_rtl .cke_button__cut_icon, .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons.png) no-repeat 0 -216px !important;}.cke_ltr .cke_button__cut_icon {background: url(icons.png) no-repeat 0 -240px !important;}.cke_rtl .cke_button__paste_icon, .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons.png) no-repeat 0 -264px !important;}.cke_ltr .cke_button__paste_icon {background: url(icons.png) no-repeat 0 -288px !important;}.cke_button__horizontalrule_icon {background: url(icons.png) no-repeat 0 -312px !important;}.cke_rtl .cke_button__indent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons.png) no-repeat 0 -336px !important;}.cke_ltr .cke_button__indent_icon {background: url(icons.png) no-repeat 0 -360px !important;}.cke_rtl .cke_button__outdent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons.png) no-repeat 0 -384px !important;}.cke_ltr .cke_button__outdent_icon {background: url(icons.png) no-repeat 0 -408px !important;}.cke_rtl .cke_button__anchor_icon, .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons.png) no-repeat 0 -432px !important;}.cke_ltr .cke_button__anchor_icon {background: url(icons.png) no-repeat 0 -456px !important;}.cke_button__link_icon {background: url(icons.png) no-repeat 0 -480px !important;}.cke_button__unlink_icon {background: url(icons.png) no-repeat 0 -504px !important;}.cke_rtl .cke_button__bulletedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons.png) no-repeat 0 -528px !important;}.cke_ltr .cke_button__bulletedlist_icon {background: url(icons.png) no-repeat 0 -552px !important;}.cke_rtl .cke_button__numberedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons.png) no-repeat 0 -576px !important;}.cke_ltr .cke_button__numberedlist_icon {background: url(icons.png) no-repeat 0 -600px !important;}.cke_button__removeformat_icon {background: url(icons.png) no-repeat 0 -624px !important;}.cke_rtl .cke_button__redo_icon, .cke_mixed_dir_content .cke_rtl .cke_button__redo_icon {background: url(icons.png) no-repeat 0 -648px !important;}.cke_ltr .cke_button__redo_icon {background: url(icons.png) no-repeat 0 -672px !important;}.cke_rtl .cke_button__undo_icon, .cke_mixed_dir_content .cke_rtl .cke_button__undo_icon {background: url(icons.png) no-repeat 0 -696px !important;}.cke_ltr .cke_button__undo_icon {background: url(icons.png) no-repeat 0 -720px !important;}.cke_hidpi .cke_button__about_icon {background: url(icons_hidpi.png) no-repeat 0 -0px !important;background-size: 16px !important;}.cke_hidpi .cke_button__bold_icon {background: url(icons_hidpi.png) no-repeat 0 -24px !important;background-size: 16px !important;}.cke_hidpi .cke_button__italic_icon {background: url(icons_hidpi.png) no-repeat 0 -48px !important;background-size: 16px !important;}.cke_hidpi .cke_button__strike_icon {background: url(icons_hidpi.png) no-repeat 0 -72px !important;background-size: 16px !important;}.cke_hidpi .cke_button__subscript_icon {background: url(icons_hidpi.png) no-repeat 0 -96px !important;background-size: 16px !important;}.cke_hidpi .cke_button__superscript_icon {background: url(icons_hidpi.png) no-repeat 0 -120px !important;background-size: 16px !important;}.cke_hidpi .cke_button__underline_icon {background: url(icons_hidpi.png) no-repeat 0 -144px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__copy_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons_hidpi.png) no-repeat 0 -168px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__copy_icon,.cke_ltr.cke_hidpi .cke_button__copy_icon {background: url(icons_hidpi.png) no-repeat 0 -192px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__cut_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons_hidpi.png) no-repeat 0 -216px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__cut_icon,.cke_ltr.cke_hidpi .cke_button__cut_icon {background: url(icons_hidpi.png) no-repeat 0 -240px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__paste_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons_hidpi.png) no-repeat 0 -264px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__paste_icon,.cke_ltr.cke_hidpi .cke_button__paste_icon {background: url(icons_hidpi.png) no-repeat 0 -288px !important;background-size: 16px !important;}.cke_hidpi .cke_button__horizontalrule_icon {background: url(icons_hidpi.png) no-repeat 0 -312px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__indent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons_hidpi.png) no-repeat 0 -336px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__indent_icon,.cke_ltr.cke_hidpi .cke_button__indent_icon {background: url(icons_hidpi.png) no-repeat 0 -360px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__outdent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons_hidpi.png) no-repeat 0 -384px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__outdent_icon,.cke_ltr.cke_hidpi .cke_button__outdent_icon {background: url(icons_hidpi.png) no-repeat 0 -408px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__anchor_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons_hidpi.png) no-repeat 0 -432px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__anchor_icon,.cke_ltr.cke_hidpi .cke_button__anchor_icon {background: url(icons_hidpi.png) no-repeat 0 -456px !important;background-size: 16px !important;}.cke_hidpi .cke_button__link_icon {background: url(icons_hidpi.png) no-repeat 0 -480px !important;background-size: 16px !important;}.cke_hidpi .cke_button__unlink_icon {background: url(icons_hidpi.png) no-repeat 0 -504px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__bulletedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -528px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__bulletedlist_icon,.cke_ltr.cke_hidpi .cke_button__bulletedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -552px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__numberedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -576px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__numberedlist_icon,.cke_ltr.cke_hidpi .cke_button__numberedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -600px !important;background-size: 16px !important;}.cke_hidpi .cke_button__removeformat_icon {background: url(icons_hidpi.png) no-repeat 0 -624px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__redo_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__redo_icon {background: url(icons_hidpi.png) no-repeat 0 -648px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__redo_icon,.cke_ltr.cke_hidpi .cke_button__redo_icon {background: url(icons_hidpi.png) no-repeat 0 -672px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__undo_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__undo_icon {background: url(icons_hidpi.png) no-repeat 0 -696px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__undo_icon,.cke_ltr.cke_hidpi .cke_button__undo_icon {background: url(icons_hidpi.png) no-repeat 0 -720px !important;background-size: 16px !important;}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono/editor_iequirks.css b/js/ckeditor/skins/moono/editor_iequirks.css
deleted file mode 100644 (file)
index f8bb606..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-/*\r
-Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
-For licensing, see LICENSE.md or http://ckeditor.com/license\r
-*/\r
-.cke_reset{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;position:static;-webkit-transition:none;-moz-transition:none;-ms-transition:none;transition:none}.cke_reset_all,.cke_reset_all *{margin:0;padding:0;border:0;background:transparent;text-decoration:none;width:auto;height:auto;vertical-align:baseline;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;position:static;-webkit-transition:none;-moz-transition:none;-ms-transition:none;transition:none;border-collapse:collapse;font:normal normal normal 12px Arial,Helvetica,Tahoma,Verdana,Sans-Serif;color:#000;text-align:left;white-space:nowrap;cursor:auto}.cke_reset_all .cke_rtl *{text-align:right}.cke_reset_all iframe{vertical-align:inherit}.cke_reset_all textarea{white-space:pre}.cke_reset_all textarea,.cke_reset_all input[type="text"],.cke_reset_all input[type="password"]{cursor:text}.cke_reset_all textarea[disabled],.cke_reset_all input[type="text"][disabled],.cke_reset_all input[type="password"][disabled]{cursor:default}.cke_reset_all fieldset{padding:10px;border:2px groove #e0dfe3}.cke_reset_all select{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.cke_chrome{display:block;border:1px solid #b6b6b6;padding:0;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_inner{display:block;-webkit-touch-callout:none;background:#fff;padding:0}.cke_float{border:0}.cke_float .cke_inner{padding-bottom:0}.cke_top,.cke_contents,.cke_bottom{display:block;overflow:hidden}.cke_top{border-bottom:1px solid #b6b6b6;padding:6px 8px 2px;white-space:normal;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_float .cke_top{border:1px solid #b6b6b6;border-bottom-color:#999}.cke_bottom{padding:6px 8px 2px;position:relative;border-top:1px solid #bfbfbf;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#cfd1cf));background-image:-moz-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-webkit-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-o-linear-gradient(top,#ebebeb,#cfd1cf);background-image:-ms-linear-gradient(top,#ebebeb,#cfd1cf);background-image:linear-gradient(top,#ebebeb,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ebebeb',endColorstr='#cfd1cf')}.cke_browser_ios .cke_contents{overflow-y:auto;-webkit-overflow-scrolling:touch}.cke_resizer{width:0;height:0;overflow:hidden;width:0;height:0;overflow:hidden;border-width:10px 10px 0 0;border-color:transparent #666 transparent transparent;border-style:dashed solid dashed dashed;font-size:0;vertical-align:bottom;margin-top:6px;margin-bottom:2px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.3);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.3);box-shadow:0 1px 0 rgba(255,255,255,.3)}.cke_hc .cke_resizer{font-size:15px;width:auto;height:auto;border-width:0}.cke_resizer_ltr{cursor:se-resize;float:right;margin-right:-4px}.cke_resizer_rtl{border-width:10px 0 0 10px;border-color:transparent transparent transparent #a5a5a5;border-style:dashed dashed dashed solid;cursor:sw-resize;float:left;margin-left:-4px;right:auto}.cke_wysiwyg_div{display:block;height:100%;overflow:auto;padding:0 8px;outline-style:none;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.cke_panel{visibility:visible;width:120px;height:100px;overflow:hidden;background-color:#fff;border:1px solid #b6b6b6;border-bottom-color:#999;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 0 3px rgba(0,0,0,.15);-webkit-box-shadow:0 0 3px rgba(0,0,0,.15);box-shadow:0 0 3px rgba(0,0,0,.15)}.cke_menu_panel{padding:0;margin:0}.cke_combopanel{width:150px;height:170px}.cke_panel_frame{width:100%;height:100%;font-size:12px;overflow:auto;overflow-x:hidden}.cke_panel_container{overflow-y:auto;overflow-x:hidden}.cke_panel_list{list-style-type:none;margin:3px;padding:0;white-space:nowrap}.cke_panel_listItem{margin:0;padding-bottom:1px}.cke_panel_listItem a{padding:3px 4px;display:block;border:1px solid #fff;color:inherit!important;text-decoration:none;overflow:hidden;text-overflow:ellipsis;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px}* html .cke_panel_listItem a{width:100%;color:#000}*:first-child+html .cke_panel_listItem a{color:#000}.cke_panel_listItem.cke_selected a{border:1px solid #dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_panel_listItem a:hover,.cke_panel_listItem a:focus,.cke_panel_listItem a:active{border-color:#dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_hc .cke_panel_listItem a{border-style:none}.cke_hc .cke_panel_listItem a:hover,.cke_hc .cke_panel_listItem a:focus,.cke_hc .cke_panel_listItem a:active{border:2px solid;padding:1px 2px}.cke_panel_grouptitle{font-size:11px;font-weight:bold;white-space:nowrap;margin:0;padding:4px 6px;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.75);border-bottom:1px solid #b6b6b6;-moz-border-radius:2px 2px 0 0;-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0;-moz-box-shadow:0 1px 0 #fff inset;-webkit-box-shadow:0 1px 0 #fff inset;box-shadow:0 1px 0 #fff inset;background:#cfd1cf;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#cfd1cf));background-image:-moz-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-webkit-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-o-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:-ms-linear-gradient(top,#f5f5f5,#cfd1cf);background-image:linear-gradient(top,#f5f5f5,#cfd1cf);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f5f5f5',endColorstr='#cfd1cf')}.cke_panel_listItem p,.cke_panel_listItem h1,.cke_panel_listItem h2,.cke_panel_listItem h3,.cke_panel_listItem h4,.cke_panel_listItem h5,.cke_panel_listItem h6,.cke_panel_listItem pre{margin-top:0;margin-bottom:0}.cke_colorblock{padding:3px;font-size:11px;font-family:'Microsoft Sans Serif',Tahoma,Arial,Verdana,Sans-Serif}.cke_colorblock,.cke_colorblock a{text-decoration:none;color:#000}span.cke_colorbox{width:10px;height:10px;border:#808080 1px solid;float:left}.cke_rtl span.cke_colorbox{float:right}a.cke_colorbox{border:#fff 1px solid;padding:2px;float:left;width:12px;height:12px}.cke_rtl a.cke_colorbox{float:right}a:hover.cke_colorbox,a:focus.cke_colorbox,a:active.cke_colorbox{border:#b6b6b6 1px solid;background-color:#e5e5e5}a.cke_colorauto,a.cke_colormore{border:#fff 1px solid;padding:2px;display:block;cursor:pointer}a:hover.cke_colorauto,a:hover.cke_colormore,a:focus.cke_colorauto,a:focus.cke_colormore,a:active.cke_colorauto,a:active.cke_colormore{border:#b6b6b6 1px solid;background-color:#e5e5e5}.cke_toolbar{float:left}.cke_rtl .cke_toolbar{float:right}.cke_toolgroup{float:left;margin:0 6px 5px 0;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_hc .cke_toolgroup{border:0;margin-right:10px;margin-bottom:10px}.cke_rtl .cke_toolgroup *:first-child{-moz-border-radius:0 2px 2px 0;-webkit-border-radius:0 2px 2px 0;border-radius:0 2px 2px 0}.cke_rtl .cke_toolgroup *:last-child{-moz-border-radius:2px 0 0 2px;-webkit-border-radius:2px 0 0 2px;border-radius:2px 0 0 2px}.cke_rtl .cke_toolgroup{float:right;margin-left:6px;margin-right:0}a.cke_button{display:inline-block;height:18px;padding:4px 6px;outline:0;cursor:default;float:left;border:0}.cke_rtl .cke_button{float:right}.cke_hc .cke_button{border:1px solid black;padding:3px 5px;margin:-2px 4px 0 -2px}.cke_button_on{-moz-box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 5px rgba(0,0,0,.6) inset,0 1px 0 rgba(0,0,0,.2);background:#b5b5b5;background-image:-webkit-gradient(linear,left top,left bottom,from(#aaa),to(#cacaca));background-image:-moz-linear-gradient(top,#aaa,#cacaca);background-image:-webkit-linear-gradient(top,#aaa,#cacaca);background-image:-o-linear-gradient(top,#aaa,#cacaca);background-image:-ms-linear-gradient(top,#aaa,#cacaca);background-image:linear-gradient(top,#aaa,#cacaca);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#aaaaaa',endColorstr='#cacaca')}.cke_hc .cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active,.cke_hc a.cke_button_disabled:hover,.cke_hc a.cke_button_disabled:focus,.cke_hc a.cke_button_disabled:active{border-width:3px;padding:1px 3px}.cke_button_disabled .cke_button_icon{opacity:.3}.cke_hc .cke_button_disabled{opacity:.5}a.cke_button_on:hover,a.cke_button_on:focus,a.cke_button_on:active{-moz-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2)}a.cke_button_off:hover,a.cke_button_off:focus,a.cke_button_off:active,a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{-moz-box-shadow:0 0 1px rgba(0,0,0,.3) inset;-webkit-box-shadow:0 0 1px rgba(0,0,0,.3) inset;box-shadow:0 0 1px rgba(0,0,0,.3) inset;background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}.cke_button_icon{cursor:inherit;background-repeat:no-repeat;margin-top:1px;width:16px;height:16px;float:left;display:inline-block}.cke_rtl .cke_button_icon{float:right}.cke_hc .cke_button_icon{display:none}.cke_button_label{display:none;padding-left:3px;margin-top:1px;line-height:17px;vertical-align:middle;float:left;cursor:default;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.5)}.cke_rtl .cke_button_label{padding-right:3px;padding-left:0;float:right}.cke_hc .cke_button_label{padding:0;display:inline-block;font-size:12px}.cke_button_arrow{display:inline-block;margin:8px 0 0 1px;width:0;height:0;cursor:default;vertical-align:top;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #474747}.cke_rtl .cke_button_arrow{margin-right:5px;margin-left:0}.cke_hc .cke_button_arrow{font-size:10px;margin:3px -2px 0 3px;width:auto;border:0}.cke_toolbar_separator{float:left;background-color:#c0c0c0;background-color:rgba(0,0,0,.2);margin:5px 2px 0;height:18px;width:1px;-webkit-box-shadow:1px 0 1px rgba(255,255,255,.5);-moz-box-shadow:1px 0 1px rgba(255,255,255,.5);box-shadow:1px 0 1px rgba(255,255,255,.5)}.cke_rtl .cke_toolbar_separator{float:right;-webkit-box-shadow:-1px 0 1px rgba(255,255,255,.1);-moz-box-shadow:-1px 0 1px rgba(255,255,255,.1);box-shadow:-1px 0 1px rgba(255,255,255,.1)}.cke_hc .cke_toolbar_separator{width:0;border-left:1px solid;margin:1px 5px 0 0}.cke_toolbar_break{display:block;clear:left}.cke_rtl .cke_toolbar_break{clear:right}.cke_toolbox_collapser{width:12px;height:11px;float:right;margin:11px 0 0;font-size:0;cursor:default;text-align:center;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_toolbox_collapser:hover{background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc')}.cke_toolbox_collapser.cke_toolbox_collapser_min{margin:0 2px 4px}.cke_rtl .cke_toolbox_collapser{float:left}.cke_toolbox_collapser .cke_arrow{display:inline-block;height:0;width:0;font-size:0;margin-top:1px;border-left:3px solid transparent;border-right:3px solid transparent;border-bottom:3px solid #474747;border-top:3px solid transparent}.cke_toolbox_collapser.cke_toolbox_collapser_min .cke_arrow{margin-top:4px;border-bottom-color:transparent;border-top-color:#474747}.cke_hc .cke_toolbox_collapser .cke_arrow{font-size:8px;width:auto;border:0;margin-top:0;margin-right:2px}.cke_menubutton{display:block}.cke_menuitem span{cursor:default}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#d3d3d3;display:block}.cke_hc .cke_menubutton{padding:2px}.cke_hc .cke_menubutton:hover,.cke_hc .cke_menubutton:focus,.cke_hc .cke_menubutton:active{border:2px solid;padding:0}.cke_menubutton_inner{display:table-row}.cke_menubutton_icon,.cke_menubutton_label,.cke_menuarrow{display:table-cell}.cke_menubutton_icon{background-color:#d7d8d7;opacity:.70;filter:alpha(opacity=70);padding:4px}.cke_hc .cke_menubutton_icon{height:16px;width:0;padding:4px 0}.cke_menubutton:hover .cke_menubutton_icon,.cke_menubutton:focus .cke_menubutton_icon,.cke_menubutton:active .cke_menubutton_icon{background-color:#d0d2d0}.cke_menubutton_disabled:hover .cke_menubutton_icon,.cke_menubutton_disabled:focus .cke_menubutton_icon,.cke_menubutton_disabled:active .cke_menubutton_icon{opacity:.3;filter:alpha(opacity=30)}.cke_menubutton_label{padding:0 5px;background-color:transparent;width:100%;vertical-align:middle}.cke_menubutton_disabled .cke_menubutton_label{opacity:.3;filter:alpha(opacity=30)}.cke_menubutton_on{border:1px solid #dedede;background-color:#f2f2f2;-moz-box-shadow:0 0 2px rgba(0,0,0,.1) inset;-webkit-box-shadow:0 0 2px rgba(0,0,0,.1) inset;box-shadow:0 0 2px rgba(0,0,0,.1) inset}.cke_menubutton_on .cke_menubutton_icon{padding-right:3px}.cke_menubutton:hover,.cke_menubutton:focus,.cke_menubutton:active{background-color:#eff0ef}.cke_panel_frame .cke_menubutton_label{display:none}.cke_menuseparator{background-color:#d3d3d3;height:1px;filter:alpha(opacity=70);opacity:.70}.cke_menuarrow{background-image:url(images/arrow.png);background-position:0 10px;background-repeat:no-repeat;padding:0 5px}.cke_rtl .cke_menuarrow{background-position:5px -13px;background-repeat:no-repeat}.cke_menuarrow span{display:none}.cke_hc .cke_menuarrow span{vertical-align:middle;display:inline}.cke_combo{display:inline-block;float:left}.cke_rtl .cke_combo{float:right}.cke_hc .cke_combo{margin-top:-2px}.cke_combo_label{display:none;float:left;line-height:26px;vertical-align:top;margin-right:5px}.cke_rtl .cke_combo_label{float:right;margin-left:5px;margin-right:0}.cke_combo_button{display:inline-block;float:left;margin:0 6px 5px 0;border:1px solid #a6a6a6;border-bottom-color:#979797;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 0 2px rgba(255,255,255,.15) inset,0 1px 0 rgba(255,255,255,.15) inset;background:#e4e4e4;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e4e4e4));background-image:-moz-linear-gradient(top,#fff,#e4e4e4);background-image:-webkit-linear-gradient(top,#fff,#e4e4e4);background-image:-o-linear-gradient(top,#fff,#e4e4e4);background-image:-ms-linear-gradient(top,#fff,#e4e4e4);background-image:linear-gradient(top,#fff,#e4e4e4);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#ffffff',endColorstr='#e4e4e4')}.cke_combo_off a.cke_combo_button:hover,.cke_combo_off a.cke_combo_button:focus{background:#ccc;background-image:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#ccc));background-image:-moz-linear-gradient(top,#f2f2f2,#ccc);background-image:-webkit-linear-gradient(top,#f2f2f2,#ccc);background-image:-o-linear-gradient(top,#f2f2f2,#ccc);background-image:-ms-linear-gradient(top,#f2f2f2,#ccc);background-image:linear-gradient(top,#f2f2f2,#ccc);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#f2f2f2',endColorstr='#cccccc');outline:0}.cke_combo_off a.cke_combo_button:active,.cke_combo_on a.cke_combo_button{border:1px solid #777;-moz-box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;box-shadow:0 1px 0 rgba(255,255,255,.5),0 1px 5px rgba(0,0,0,.6) inset;background:#b5b5b5;background-image:-webkit-gradient(linear,left top,left bottom,from(#aaa),to(#cacaca));background-image:-moz-linear-gradient(top,#aaa,#cacaca);background-image:-webkit-linear-gradient(top,#aaa,#cacaca);background-image:-o-linear-gradient(top,#aaa,#cacaca);background-image:-ms-linear-gradient(top,#aaa,#cacaca);background-image:linear-gradient(top,#aaa,#cacaca);filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0,startColorstr='#aaaaaa',endColorstr='#cacaca')}.cke_combo_on a.cke_combo_button:hover,.cke_combo_on a.cke_combo_button:focus,.cke_combo_on a.cke_combo_button:active{-moz-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.7) inset,0 1px 0 rgba(0,0,0,.2)}.cke_rtl .cke_combo_button{float:right;margin-left:5px;margin-right:0}.cke_hc a.cke_combo_button{padding:3px}.cke_hc .cke_combo_on a.cke_combo_button,.cke_hc .cke_combo_off a.cke_combo_button:hover,.cke_hc .cke_combo_off a.cke_combo_button:focus,.cke_hc .cke_combo_off a.cke_combo_button:active{border-width:3px;padding:1px}.cke_combo_text{line-height:26px;padding-left:10px;text-overflow:ellipsis;overflow:hidden;float:left;cursor:default;color:#474747;text-shadow:0 1px 0 rgba(255,255,255,.5);width:60px}.cke_rtl .cke_combo_text{float:right;text-align:right;padding-left:0;padding-right:10px}.cke_hc .cke_combo_text{line-height:18px;font-size:12px}.cke_combo_open{cursor:default;display:inline-block;font-size:0;height:19px;line-height:17px;margin:1px 7px 1px;width:5px}.cke_hc .cke_combo_open{height:12px}.cke_combo_arrow{margin:11px 0 0;float:left;height:0;width:0;font-size:0;border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #474747}.cke_hc .cke_combo_arrow{font-size:10px;width:auto;border:0;margin-top:3px}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{opacity:.3}.cke_path{float:left;margin:-2px 0 2px}.cke_path_item,.cke_path_empty{display:inline-block;float:left;padding:3px 4px;margin-right:2px;cursor:default;text-decoration:none;outline:0;border:0;color:#4c4c4c;text-shadow:0 1px 0 #fff;font-weight:bold;font-size:11px}.cke_rtl .cke_path,.cke_rtl .cke_path_item,.cke_rtl .cke_path_empty{float:right}a.cke_path_item:hover,a.cke_path_item:focus,a.cke_path_item:active{background-color:#bfbfbf;color:#333;text-shadow:0 1px 0 rgba(255,255,255,.5);-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-moz-box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5);-webkit-box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5);box-shadow:0 0 4px rgba(0,0,0,.5) inset,0 1px 0 rgba(255,255,255,.5)}.cke_hc a.cke_path_item:hover,.cke_hc a.cke_path_item:focus,.cke_hc a.cke_path_item:active{border:2px solid;padding:1px 2px}.cke_button__source_label,.cke_button__sourcedialog_label{display:inline}.cke_combo__fontsize .cke_combo_text{width:30px}.cke_combopanel__fontsize{width:120px}.cke_source{font-family:'Courier New',Monospace;font-size:small;background-color:#fff;white-space:pre}.cke_wysiwyg_frame,.cke_wysiwyg_div{background-color:#fff}.cke_chrome{visibility:inherit}.cke_voice_label{display:none}legend.cke_voice_label{display:none}a.cke_button_disabled,a.cke_button_disabled:hover,a.cke_button_disabled:focus,a.cke_button_disabled:active{filter:alpha(opacity = 30)}.cke_button_disabled .cke_button_icon{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#00ffffff,endColorstr=#00ffffff)}.cke_button_off:hover,.cke_button_off:focus,.cke_button_off:active{filter:alpha(opacity = 100)}.cke_combo_disabled .cke_combo_inlinelabel,.cke_combo_disabled .cke_combo_open{filter:alpha(opacity = 30)}.cke_toolbox_collapser{border:1px solid #a6a6a6}.cke_toolbox_collapser .cke_arrow{margin-top:1px}.cke_hc .cke_top,.cke_hc .cke_bottom,.cke_hc .cke_combo_button,.cke_hc a.cke_combo_button:hover,.cke_hc a.cke_combo_button:focus,.cke_hc .cke_toolgroup,.cke_hc .cke_button_on,.cke_hc a.cke_button_off:hover,.cke_hc a.cke_button_off:focus,.cke_hc a.cke_button_off:active,.cke_hc .cke_toolbox_collapser,.cke_hc .cke_toolbox_collapser:hover,.cke_hc .cke_panel_grouptitle{filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.cke_top,.cke_contents,.cke_bottom{width:100%}.cke_button_arrow{font-size:0}.cke_rtl .cke_toolgroup,.cke_rtl .cke_toolbar_separator,.cke_rtl .cke_button,.cke_rtl .cke_button *,.cke_rtl .cke_combo,.cke_rtl .cke_combo *,.cke_rtl .cke_path_item,.cke_rtl .cke_path_item *,.cke_rtl .cke_path_empty{float:none}.cke_rtl .cke_toolgroup,.cke_rtl .cke_toolbar_separator,.cke_rtl .cke_combo_button,.cke_rtl .cke_combo_button *,.cke_rtl .cke_button,.cke_rtl .cke_button_icon,{display:inline-block;vertical-align:top}.cke_rtl .cke_button_icon{float:none}.cke_resizer{width:10px}.cke_source{white-space:normal}.cke_bottom{position:static}.cke_colorbox{font-size:0}.cke_button__about_icon {background: url(icons.png) no-repeat 0 -0px !important;}.cke_button__bold_icon {background: url(icons.png) no-repeat 0 -24px !important;}.cke_button__italic_icon {background: url(icons.png) no-repeat 0 -48px !important;}.cke_button__strike_icon {background: url(icons.png) no-repeat 0 -72px !important;}.cke_button__subscript_icon {background: url(icons.png) no-repeat 0 -96px !important;}.cke_button__superscript_icon {background: url(icons.png) no-repeat 0 -120px !important;}.cke_button__underline_icon {background: url(icons.png) no-repeat 0 -144px !important;}.cke_rtl .cke_button__copy_icon, .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons.png) no-repeat 0 -168px !important;}.cke_ltr .cke_button__copy_icon {background: url(icons.png) no-repeat 0 -192px !important;}.cke_rtl .cke_button__cut_icon, .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons.png) no-repeat 0 -216px !important;}.cke_ltr .cke_button__cut_icon {background: url(icons.png) no-repeat 0 -240px !important;}.cke_rtl .cke_button__paste_icon, .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons.png) no-repeat 0 -264px !important;}.cke_ltr .cke_button__paste_icon {background: url(icons.png) no-repeat 0 -288px !important;}.cke_button__horizontalrule_icon {background: url(icons.png) no-repeat 0 -312px !important;}.cke_rtl .cke_button__indent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons.png) no-repeat 0 -336px !important;}.cke_ltr .cke_button__indent_icon {background: url(icons.png) no-repeat 0 -360px !important;}.cke_rtl .cke_button__outdent_icon, .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons.png) no-repeat 0 -384px !important;}.cke_ltr .cke_button__outdent_icon {background: url(icons.png) no-repeat 0 -408px !important;}.cke_rtl .cke_button__anchor_icon, .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons.png) no-repeat 0 -432px !important;}.cke_ltr .cke_button__anchor_icon {background: url(icons.png) no-repeat 0 -456px !important;}.cke_button__link_icon {background: url(icons.png) no-repeat 0 -480px !important;}.cke_button__unlink_icon {background: url(icons.png) no-repeat 0 -504px !important;}.cke_rtl .cke_button__bulletedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons.png) no-repeat 0 -528px !important;}.cke_ltr .cke_button__bulletedlist_icon {background: url(icons.png) no-repeat 0 -552px !important;}.cke_rtl .cke_button__numberedlist_icon, .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons.png) no-repeat 0 -576px !important;}.cke_ltr .cke_button__numberedlist_icon {background: url(icons.png) no-repeat 0 -600px !important;}.cke_button__removeformat_icon {background: url(icons.png) no-repeat 0 -624px !important;}.cke_rtl .cke_button__redo_icon, .cke_mixed_dir_content .cke_rtl .cke_button__redo_icon {background: url(icons.png) no-repeat 0 -648px !important;}.cke_ltr .cke_button__redo_icon {background: url(icons.png) no-repeat 0 -672px !important;}.cke_rtl .cke_button__undo_icon, .cke_mixed_dir_content .cke_rtl .cke_button__undo_icon {background: url(icons.png) no-repeat 0 -696px !important;}.cke_ltr .cke_button__undo_icon {background: url(icons.png) no-repeat 0 -720px !important;}.cke_hidpi .cke_button__about_icon {background: url(icons_hidpi.png) no-repeat 0 -0px !important;background-size: 16px !important;}.cke_hidpi .cke_button__bold_icon {background: url(icons_hidpi.png) no-repeat 0 -24px !important;background-size: 16px !important;}.cke_hidpi .cke_button__italic_icon {background: url(icons_hidpi.png) no-repeat 0 -48px !important;background-size: 16px !important;}.cke_hidpi .cke_button__strike_icon {background: url(icons_hidpi.png) no-repeat 0 -72px !important;background-size: 16px !important;}.cke_hidpi .cke_button__subscript_icon {background: url(icons_hidpi.png) no-repeat 0 -96px !important;background-size: 16px !important;}.cke_hidpi .cke_button__superscript_icon {background: url(icons_hidpi.png) no-repeat 0 -120px !important;background-size: 16px !important;}.cke_hidpi .cke_button__underline_icon {background: url(icons_hidpi.png) no-repeat 0 -144px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__copy_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__copy_icon {background: url(icons_hidpi.png) no-repeat 0 -168px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__copy_icon,.cke_ltr.cke_hidpi .cke_button__copy_icon {background: url(icons_hidpi.png) no-repeat 0 -192px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__cut_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__cut_icon {background: url(icons_hidpi.png) no-repeat 0 -216px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__cut_icon,.cke_ltr.cke_hidpi .cke_button__cut_icon {background: url(icons_hidpi.png) no-repeat 0 -240px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__paste_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__paste_icon {background: url(icons_hidpi.png) no-repeat 0 -264px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__paste_icon,.cke_ltr.cke_hidpi .cke_button__paste_icon {background: url(icons_hidpi.png) no-repeat 0 -288px !important;background-size: 16px !important;}.cke_hidpi .cke_button__horizontalrule_icon {background: url(icons_hidpi.png) no-repeat 0 -312px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__indent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__indent_icon {background: url(icons_hidpi.png) no-repeat 0 -336px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__indent_icon,.cke_ltr.cke_hidpi .cke_button__indent_icon {background: url(icons_hidpi.png) no-repeat 0 -360px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__outdent_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__outdent_icon {background: url(icons_hidpi.png) no-repeat 0 -384px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__outdent_icon,.cke_ltr.cke_hidpi .cke_button__outdent_icon {background: url(icons_hidpi.png) no-repeat 0 -408px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__anchor_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__anchor_icon {background: url(icons_hidpi.png) no-repeat 0 -432px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__anchor_icon,.cke_ltr.cke_hidpi .cke_button__anchor_icon {background: url(icons_hidpi.png) no-repeat 0 -456px !important;background-size: 16px !important;}.cke_hidpi .cke_button__link_icon {background: url(icons_hidpi.png) no-repeat 0 -480px !important;background-size: 16px !important;}.cke_hidpi .cke_button__unlink_icon {background: url(icons_hidpi.png) no-repeat 0 -504px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__bulletedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__bulletedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -528px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__bulletedlist_icon,.cke_ltr.cke_hidpi .cke_button__bulletedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -552px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__numberedlist_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__numberedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -576px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__numberedlist_icon,.cke_ltr.cke_hidpi .cke_button__numberedlist_icon {background: url(icons_hidpi.png) no-repeat 0 -600px !important;background-size: 16px !important;}.cke_hidpi .cke_button__removeformat_icon {background: url(icons_hidpi.png) no-repeat 0 -624px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__redo_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__redo_icon {background: url(icons_hidpi.png) no-repeat 0 -648px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__redo_icon,.cke_ltr.cke_hidpi .cke_button__redo_icon {background: url(icons_hidpi.png) no-repeat 0 -672px !important;background-size: 16px !important;}.cke_rtl.cke_hidpi .cke_button__undo_icon, .cke_hidpi .cke_mixed_dir_content .cke_rtl .cke_button__undo_icon {background: url(icons_hidpi.png) no-repeat 0 -696px !important;background-size: 16px !important;}.cke_hidpi .cke_ltr .cke_button__undo_icon,.cke_ltr.cke_hidpi .cke_button__undo_icon {background: url(icons_hidpi.png) no-repeat 0 -720px !important;background-size: 16px !important;}
\ No newline at end of file
diff --git a/js/ckeditor/skins/moono/icons.png b/js/ckeditor/skins/moono/icons.png
deleted file mode 100644 (file)
index 59a7ad3..0000000
Binary files a/js/ckeditor/skins/moono/icons.png and /dev/null differ
diff --git a/js/ckeditor/skins/moono/icons_hidpi.png b/js/ckeditor/skins/moono/icons_hidpi.png
deleted file mode 100644 (file)
index 14a9b43..0000000
Binary files a/js/ckeditor/skins/moono/icons_hidpi.png and /dev/null differ
diff --git a/js/ckeditor/skins/moono/images/arrow.png b/js/ckeditor/skins/moono/images/arrow.png
deleted file mode 100644 (file)
index 0d1eb39..0000000
Binary files a/js/ckeditor/skins/moono/images/arrow.png and /dev/null differ
diff --git a/js/ckeditor/skins/moono/images/close.png b/js/ckeditor/skins/moono/images/close.png
deleted file mode 100644 (file)
index 04b9c97..0000000
Binary files a/js/ckeditor/skins/moono/images/close.png and /dev/null differ
diff --git a/js/ckeditor/skins/moono/images/hidpi/close.png b/js/ckeditor/skins/moono/images/hidpi/close.png
deleted file mode 100644 (file)
index 8abca8e..0000000
Binary files a/js/ckeditor/skins/moono/images/hidpi/close.png and /dev/null differ
diff --git a/js/ckeditor/skins/moono/images/hidpi/lock-open.png b/js/ckeditor/skins/moono/images/hidpi/lock-open.png
deleted file mode 100644 (file)
index aa5e740..0000000
Binary files a/js/ckeditor/skins/moono/images/hidpi/lock-open.png and /dev/null differ
diff --git a/js/ckeditor/skins/moono/images/hidpi/lock.png b/js/ckeditor/skins/moono/images/hidpi/lock.png
deleted file mode 100644 (file)
index 5404b06..0000000
Binary files a/js/ckeditor/skins/moono/images/hidpi/lock.png and /dev/null differ
diff --git a/js/ckeditor/skins/moono/images/hidpi/refresh.png b/js/ckeditor/skins/moono/images/hidpi/refresh.png
deleted file mode 100644 (file)
index 1ebef34..0000000
Binary files a/js/ckeditor/skins/moono/images/hidpi/refresh.png and /dev/null differ
diff --git a/js/ckeditor/skins/moono/images/lock-open.png b/js/ckeditor/skins/moono/images/lock-open.png
deleted file mode 100644 (file)
index 3b256c0..0000000
Binary files a/js/ckeditor/skins/moono/images/lock-open.png and /dev/null differ
diff --git a/js/ckeditor/skins/moono/images/lock.png b/js/ckeditor/skins/moono/images/lock.png
deleted file mode 100644 (file)
index c127f9e..0000000
Binary files a/js/ckeditor/skins/moono/images/lock.png and /dev/null differ
diff --git a/js/ckeditor/skins/moono/images/refresh.png b/js/ckeditor/skins/moono/images/refresh.png
deleted file mode 100644 (file)
index a1a061c..0000000
Binary files a/js/ckeditor/skins/moono/images/refresh.png and /dev/null differ
diff --git a/js/ckeditor/skins/moono/readme.md b/js/ckeditor/skins/moono/readme.md
deleted file mode 100644 (file)
index 0fa4c1a..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-"Moono" Skin\r
-====================\r
-\r
-This skin has been chosen for the **default skin** of CKEditor 4.x, elected from the CKEditor\r
-[skin contest](http://ckeditor.com/blog/new_ckeditor_4_skin) and further shaped by\r
-the CKEditor team. "Moono" is maintained by the core developers.\r
-\r
-For more information about skins, please check the [CKEditor Skin SDK](http://docs.cksource.com/CKEditor_4.x/Skin_SDK)\r
-documentation.\r
-\r
-Features\r
--------------------\r
-"Moono" is a monochromatic skin, which offers a modern look coupled with gradients and transparency.\r
-It comes with the following features:\r
-\r
-- Chameleon feature with brightness,\r
-- high-contrast compatibility,\r
-- graphics source provided in SVG.\r
-\r
-Directory Structure\r
--------------------\r
-\r
-CSS parts:\r
-- **editor.css**: the main CSS file. It's simply loading several other files, for easier maintenance,\r
-- **mainui.css**: the file contains styles of entire editor outline structures,\r
-- **toolbar.css**: the file contains styles of the editor toolbar space (top),\r
-- **richcombo.css**: the file contains styles of the rich combo ui elements on toolbar,\r
-- **panel.css**: the file contains styles of the rich combo drop-down, it's not loaded\r
-until the first panel open up,\r
-- **elementspath.css**: the file contains styles of the editor elements path bar (bottom),\r
-- **menu.css**: the file contains styles of all editor menus including context menu and button drop-down,\r
-it's not loaded until the first menu open up,\r
-- **dialog.css**: the CSS files for the dialog UI, it's not loaded until the first dialog open,\r
-- **reset.css**: the file defines the basis of style resets among all editor UI spaces,\r
-- **preset.css**: the file defines the default styles of some UI elements reflecting the skin preference,\r
-- **editor_XYZ.css** and **dialog_XYZ.css**: browser specific CSS hacks.\r
-\r
-Other parts:\r
-- **skin.js**: the only JavaScript part of the skin that registers the skin, its browser specific files and its icons and defines the Chameleon feature,\r
-- **icons/**: contains all skin defined icons,\r
-- **images/**: contains a fill general used images,\r
-- **dev/**: contains SVG source of the skin icons.\r
-\r
-License\r
--------\r
-\r
-Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
-\r
-Licensed under the terms of any of the following licenses at your choice: [GPL](http://www.gnu.org/licenses/gpl.html), [LGPL](http://www.gnu.org/licenses/lgpl.html) and [MPL](http://www.mozilla.org/MPL/MPL-1.1.html).\r
-\r
-See LICENSE.md for more information.\r
index 6ffdb02..025715e 100644 (file)
@@ -1,22 +1,24 @@
 /**\r
- * Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
+ * Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
  * For licensing, see LICENSE.md or http://ckeditor.com/license\r
  */\r
 \r
 // This file contains style definitions that can be used by CKEditor plugins.\r
 //\r
-// The most common use for it is the "stylescombo" plugin, which shows a combo\r
-// in the editor toolbar, containing all styles. Other plugins instead, like\r
-// the div plugin, use a subset of the styles on their feature.\r
+// The most common use for it is the "stylescombo" plugin which shows the Styles drop-down\r
+// list containing all styles in the editor toolbar. Other plugins, like\r
+// the "div" plugin, use a subset of the styles for their features.\r
 //\r
-// If you don't have plugins that depend on this file, you can simply ignore it.\r
-// Otherwise it is strongly recommended to customize this file to match your\r
+// If you do not have plugins that depend on this file in your editor build, you can simply\r
+// ignore it. Otherwise it is strongly recommended to customize this file to match your\r
 // website requirements and design properly.\r
+//\r
+// For more information refer to: http://docs.ckeditor.com/#!/guide/dev_styles-section-style-rules\r
 \r
 CKEDITOR.stylesSet.add( 'default', [\r
-       /* Block Styles */\r
+       /* Block styles */\r
 \r
-       // These styles are already available in the "Format" combo ("format" plugin),\r
+       // These styles are already available in the "Format" drop-down list ("format" plugin),\r
        // so they are not needed here by default. You may enable them to avoid\r
        // placing the "Format" combo in the toolbar, maintaining the same features.\r
        /*\r
@@ -43,11 +45,11 @@ CKEDITOR.stylesSet.add( 'default', [
                }\r
        },\r
 \r
-       /* Inline Styles */\r
+       /* Inline styles */\r
 \r
        // These are core styles available as toolbar buttons. You may opt enabling\r
-       // some of them in the Styles combo, removing them from the toolbar.\r
-       // (This requires the "stylescombo" plugin)\r
+       // some of them in the Styles drop-down list, removing them from the toolbar.\r
+       // (This requires the "stylescombo" plugin.)\r
        /*\r
        { name: 'Strong',                       element: 'strong', overrides: 'b' },\r
        { name: 'Emphasis',                     element: 'em'   , overrides: 'i' },\r
@@ -77,22 +79,22 @@ CKEDITOR.stylesSet.add( 'default', [
        { name: 'Language: RTL',        element: 'span', attributes: { 'dir': 'rtl' } },\r
        { name: 'Language: LTR',        element: 'span', attributes: { 'dir': 'ltr' } },\r
 \r
-       /* Object Styles */\r
+       /* Object styles */\r
 \r
        {\r
-               name: 'Styled image (left)',\r
+               name: 'Styled Image (left)',\r
                element: 'img',\r
                attributes: { 'class': 'left' }\r
        },\r
 \r
        {\r
-               name: 'Styled image (right)',\r
+               name: 'Styled Image (right)',\r
                element: 'img',\r
                attributes: { 'class': 'right' }\r
        },\r
 \r
        {\r
-               name: 'Compact table',\r
+               name: 'Compact Table',\r
                element: 'table',\r
                attributes: {\r
                        cellpadding: '5',\r
@@ -106,6 +108,30 @@ CKEDITOR.stylesSet.add( 'default', [
        },\r
 \r
        { name: 'Borderless Table',             element: 'table',       styles: { 'border-style': 'hidden', 'background-color': '#E6E6FA' } },\r
-       { name: 'Square Bulleted List', element: 'ul',          styles: { 'list-style-type': 'square' } }\r
-]);\r
+       { name: 'Square Bulleted List', element: 'ul',          styles: { 'list-style-type': 'square' } },\r
+\r
+       /* Widget styles */\r
+\r
+       { name: 'Clean Image', type: 'widget', widget: 'image', attributes: { 'class': 'image-clean' } },\r
+       { name: 'Grayscale Image', type: 'widget', widget: 'image', attributes: { 'class': 'image-grayscale' } },\r
+\r
+       { name: 'Featured Snippet', type: 'widget', widget: 'codeSnippet', attributes: { 'class': 'code-featured' } },\r
+\r
+       { name: 'Featured Formula', type: 'widget', widget: 'mathjax', attributes: { 'class': 'math-featured' } },\r
+\r
+       { name: '240p', type: 'widget', widget: 'embedSemantic', attributes: { 'class': 'embed-240p' }, group: 'size' },\r
+       { name: '360p', type: 'widget', widget: 'embedSemantic', attributes: { 'class': 'embed-360p' }, group: 'size' },\r
+       { name: '480p', type: 'widget', widget: 'embedSemantic', attributes: { 'class': 'embed-480p' }, group: 'size' },\r
+       { name: '720p', type: 'widget', widget: 'embedSemantic', attributes: { 'class': 'embed-720p' }, group: 'size' },\r
+       { name: '1080p', type: 'widget', widget: 'embedSemantic', attributes: { 'class': 'embed-1080p' }, group: 'size' },\r
+\r
+       // Adding space after the style name is an intended workaround. For now, there\r
+       // is no option to create two styles with the same name for different widget types. See http://dev.ckeditor.com/ticket/16664.\r
+       { name: '240p ', type: 'widget', widget: 'embed', attributes: { 'class': 'embed-240p' }, group: 'size' },\r
+       { name: '360p ', type: 'widget', widget: 'embed', attributes: { 'class': 'embed-360p' }, group: 'size' },\r
+       { name: '480p ', type: 'widget', widget: 'embed', attributes: { 'class': 'embed-480p' }, group: 'size' },\r
+       { name: '720p ', type: 'widget', widget: 'embed', attributes: { 'class': 'embed-720p' }, group: 'size' },\r
+       { name: '1080p ', type: 'widget', widget: 'embed', attributes: { 'class': 'embed-1080p' }, group: 'size' }\r
+\r
+] );\r
 \r
index 6d89819..c9a1524 100644 (file)
@@ -5,9 +5,27 @@
 // SL/ClientJS.pm for instructions.
 
 namespace("kivi", function(ns) {
-ns.display_flash = function(type, message) {
+ns.display_flash = function(type, message, noscroll) {
   $('#flash_' + type + '_content').text(message);
   $('#flash_' + type).show();
+  if (!noscroll && $('#frame-header')[0]) {
+    $('#frame-header')[0].scrollIntoView();
+  }
+};
+
+ns.display_flash_detail = function(type, message) {
+  $('#flash_' + type + '_detail').html(message);
+  $('#flash_' + type + '_disp').show();
+};
+
+ns.clear_flash = function(category , timeout) {
+  window.setTimeout(function(){
+    $('#flash_' + category).hide();
+    $('#flash_detail_' + category).hide();
+    $('#flash_' + category + '_disp').hide();
+    $('#flash_' + category + '_content').empty();
+    $('#flash_' + category + '_detail').empty();
+  }, timeout);
 };
 
 ns.eval_json_result = function(data) {
@@ -17,13 +35,19 @@ ns.eval_json_result = function(data) {
   if (data.error)
     return ns.display_flash('error', data.error);
 
-  $(['info', 'warning', 'error']).each(function(idx, category) {
-    $('#flash_' + category).hide();
-    $('#flash_' + category + '_content').empty();
-  });
-
-  if ((data.js || '') != '')
+  if (!data.no_flash_clear) {
+    $(['info', 'warning', 'error']).each(function(idx, category) {
+      $('#flash_' + category).hide();
+      $('#flash_detail_' + category).hide();
+      $('#flash_' + category + '_disp').hide();
+      $('#flash_' + category + '_content').empty();
+      $('#flash_' + category + '_detail').empty();
+    });
+  }
+  if ((data.js || '') !== '')
+    // jshint -W061
     eval(data.js);
+    // jshint +W061
 
   if (data.eval_actions)
     $(data.eval_actions).each(function(idx, action) {
@@ -89,12 +113,12 @@ ns.eval_json_result = function(data) {
 
       // ## jQuery UI dialog plugin ##
 
-      // Opening and closing and closing a popup
+      // Opening and closing a popup
       else if (action[0] == 'dialog:open')          kivi.popup_dialog(action[1]);
       else if (action[0] == 'dialog:close')         $(action[1]).dialog('close');
 
       // ## jQuery Form plugin ##
-      else if (action[0] == 'ajaxForm')             pattern: $(action[1]).ajaxForm({ success: eval_json_result });
+      else if (action[0] == 'ajaxForm')             $(action[1]).ajaxForm({ success: eval_json_result });
 
       // ## jstree plugin ##
 
@@ -127,11 +151,15 @@ ns.eval_json_result = function(data) {
 
       // ## other stuff ##
       else if (action[0] == 'redirect_to')          window.location.href = action[1];
+      else if (action[0] == 'save_file')            kivi.save_file(action[1], action[2], action[3], action[4]);
       else if (action[0] == 'flash')                kivi.display_flash(action[1], action[2]);
+      else if (action[0] == 'flash_detail')         kivi.display_flash_detail(action[1], action[2]);
+      else if (action[0] == 'clear_flash')          kivi.clear_flash(action[1], action[2]);
       else if (action[0] == 'reinit_widgets')       kivi.reinit_widgets();
       else if (action[0] == 'run')                  kivi.run(action[1], action.slice(2, action.length));
       else if (action[0] == 'run_once_for')         kivi.run_once_for(action[1], action[2], action[3]);
       else if (action[0] == 'scroll_into_view')     $(action[1])[0].scrollIntoView();
+      else if (action[0] == 'set_cursor_position')  kivi.set_cursor_position(action[1], action[2]);
 
       else                                          console.log('Unknown action: ' + action[0]);
 
index 8ab2a5b..5f2184e 100644 (file)
@@ -1,26 +1,3 @@
-function setupPoints(numberformat, wrongFormat) {
-  decpoint = numberformat.substring((numberformat.substring(1, 2).match(/\.|\,/) ? 5 : 4), (numberformat.substring(1, 2).match(/\.|\,/) ? 6 : 5));
-  if (numberformat.substring(1, 2).match(/\.|\,/)) {
-    thpoint = numberformat.substring(1, 2);
-  }
-  else {
-    thpoint = null;
-  }
-  wrongNumberFormat = wrongFormat + " ( " + numberformat + " ) ";
-}
-
-function setupDateFormat(setDateFormat, setWrongDateFormat) {
-  dateFormat = setDateFormat;
-  wrongDateFormat = setWrongDateFormat + " ( " + setDateFormat + " ) ";
-  formatArray = new Array();
-  if(dateFormat.match(/^\w\w\W/)) {
-    seperator = dateFormat.substring(2,3);
-  }
-  else {
-    seperator = dateFormat.substring(4,5);
-  }
-}
-
 function centerParms(width,height,extra) {
   xPos = (screen.width - width) / 2;
   yPos = (screen.height - height) / 2;
@@ -33,125 +10,6 @@ function centerParms(width,height,extra) {
   return string;
 }
 
-function check_right_number_format(input_name) {
-  if(decpoint && thpoint && thpoint == decpoint) {
-    return show_alert_and_focus(input_name, wrongNumberFormat);
-  }
-  var test_val = input_name.value;
-  if(thpoint && thpoint == ','){
-    test_val = test_val.replace(/,/g, '');
-  }
-  if(thpoint && thpoint == '.'){
-    test_val = test_val.replace(/\./g, '');
-  }
-  if(decpoint && decpoint == ','){
-    test_val = test_val.replace(/,/g, '.');
-  }
-  var forbidden = test_val.match(/[^\s\d\(\)\-\+\*\/\.]/g);
-  if (forbidden && forbidden.length > 0 ){
-    return show_alert_and_focus(input_name, wrongNumberFormat);
-  }
-
-  try{
-    eval(test_val);
-  }catch(err){
-    return show_alert_and_focus(input_name, wrongNumberFormat);
-  }
-
-}
-
-function check_right_date_format(input_name) {
-  if(input_name.value == "") {
-    return true;
-  }
-
-  if ( ( input_name.value.match(/^\d+$/ ) ) && !(dateFormat.lastIndexOf("y") == 3) ) {
-    // date shortcuts for entering date without separator for three date styles, e.g.
-    // 31122014 -> 12.04.2014
-    // 12312014 -> 12/31/2014
-    // 31122014 -> 31/12/2014
-    
-    if (input_name.value.match(/^\d{8}$/)) {
-      input_name.value = input_name.value.replace(/^(\d\d)(\d\d)(\d\d\d\d)$/, "$1" + seperator + "$2" + seperator + "$3")
-    } else if (input_name.value.match(/^\d{6}$/)) {
-      // 120414 -> 12.04.2014
-      input_name.value = input_name.value.replace(/^(\d\d)(\d\d)(\d\d)$/, "$1" + seperator + "$2" + seperator + "$3")
-    } else if (input_name.value.match(/^\d{4}$/)) {
-      // 1204 -> 12.04.2014
-      var today = new Date();
-      var year = today.getYear();
-      if (year < 999) year += 1900;
-      input_name.value = input_name.value.replace(/^(\d\d)(\d\d)$/, "$1" + seperator + "$2");
-      input_name.value = input_name.value + seperator + year;
-    } else  if ( input_name.value.match(/^\d{1,2}$/ ) ) {
-      // assume the entry is the day of the current month and current year
-      var today = new Date();
-      var day = input_name.value;
-      var month = today.getMonth() + 1;
-      var year = today.getYear();
-      if( day.length == 1 && day < 10) {
-        day='0'+day; 
-      };
-      if(month<10) {
-        month='0'+month;
-      };
-      if (year < 999) year += 1900;
-      if ( dateFormat.lastIndexOf("d") == 1) {
-        input_name.value = day + seperator + month + seperator + year;
-      } else {
-        input_name.value = month + seperator + day + seperator + year;
-      } 
-    };
-  }
-
-  var matching = new RegExp(dateFormat.replace(/\w/g, '\\d') + "\$","ig");
-  if(!(dateFormat.lastIndexOf("y") == 3) && !matching.test(input_name.value)) {
-    matching = new RegExp(dateFormat.replace(/\w/g, '\\d') + '\\d\\d\$', "ig");
-    if(!matching.test(input_name.value)) {
-      return show_alert_and_focus(input_name, wrongDateFormat);
-    }
-  }
-  else {
-    if (dateFormat.lastIndexOf("y") == 3 && !matching.test(input_name.value)) {
-      return show_alert_and_focus(input_name, wrongDateFormat);
-    }
-  }
-}
-
-function validate_dates(input_name_1, input_name_2) {
-  var tempArray1 = new Array();
-  var tempArray2 = new Array();
-  tempArray1 = getDateArray(input_name_1);
-  tempArray2 = getDateArray(input_name_2);
-  if(check_right_date_format(input_name_1) && check_right_date_format(input_name_2)) {
-    if(!((new Date(tempArray2[0], tempArray2[1], tempArray2[2])).getTime() >= (new Date(tempArray1[0], tempArray1[1], tempArray1[2])).getTime())) {
-      show_alert_and_focus(input_name_1, wrongDateFormat);
-      return show_alert_and_focus(input_name_2, wrongDateFormat);
-    }
-    if(!((new Date(tempArray2[0], tempArray2[1], tempArray2[2])).getTime() >= (new Date(1900, 1, 1)).getTime())) {
-      show_alert_and_focus(input_name_1, wrongDateFormat);
-      return show_alert_and_focus(input_name_2, wrongDateFormat);
-    }
-  }
-}
-
-function getDateArray(input_name) {
-  formatArray[2] = input_name.value.substring(dateFormat.indexOf("d"), 2);
-  formatArray[1] = input_name.value.substring(dateFormat.indexOf("m"), 2);
-  formatArray[0] = input_name.value.substring(dateFormat.indexOf("y"), (dateFormat.length == 10 ? 4 : 2));
-  if(dateFormat.length == 8) {
-    formatArray[0] += (formatArray[0] < 70 ? 2000 : 1900);
-  }
-  return formatArray;
-}
-
-function show_alert_and_focus(input_name, errorMessage) {
-  input_name.select();
-  alert(errorMessage + "\n\r\n\r--> " + input_name.value);
-  input_name.focus();
-  return false;
-}
-
 function get_input_value(input_name) {
   var the_input = document.getElementsByName(input_name);
   if (the_input && the_input[0])
@@ -189,22 +47,53 @@ function focus_by_name(name){
   return false;
 }
 
-$(document).ready(function () {
+$(function () {
   $('input').focus(function(){
     if (focussable(this)) window.focused_element = this;
   });
 
-  // Lowest priority: first focussable element in form.
-  set_cursor_to_first_element();
+  // setting focus inside a tabbed area fails if this is encountered before the tabbing is complete
+  // in that case the elements count as hidden and jquery aborts .focus()
+  setTimeout(function(){
+    // Lowest priority: first focussable element in form.
+    set_cursor_to_first_element();
+
+    // Medium priority: class set in template
+    var initial_focus = $(".initial_focus").filter(':visible')[0];
+    if (initial_focus)
+      $(initial_focus).focus();
+
+    // special: honour focus_position
+    // if no higher priority applies set focus to the appropriate element
+    if ($("#display_row")[0] && kivi.myconfig.focus_position) {
+      switch(kivi.myconfig.focus_position) {
+        case 'last_partnumber'  : $('#display_row tr.row:gt(-3):lt(-1) input[name*="partnumber"]').focus(); break;
+        case 'last_description' : $('#display_row tr.row:gt(-3):lt(-1) input[name*="description"]').focus(); break;
+        case 'last_qty'         : $('#display_row tr.row:gt(-3):lt(-1) input[name*="qty"]').focus(); break;
+        case 'new_partnumber'   : $('#display_row tr:gt(1) input[name*="partnumber"]').focus(); break;
+        case 'new_description'  : $('#display_row tr:gt(1) input[name*="description"]').focus(); break;
+        case 'new_qty'          : $('#display_row tr:gt(1) input[name*="qty"]').focus(); break;
+      }
+    }
 
-  // Medium priority: class set in template
-  var initial_focus = $(".initial_focus").filter(':visible')[0];
-  if (initial_focus)
-    $(initial_focus).focus();
+    // all of this screws with the native location.hash focus, so reimplement this as well
+    if (location.hash) {
+      var hash_name = location.hash.substr(1);
+      var $hash_by_id = $(location.hash + ':visible');
+      if ($hash_by_id.length > 0) {
+        $hash_by_id.get(0).focus();
+      } else {
+        var $by_name = $('[name=' + hash_name + ']:visible');
+        if ($by_name.length > 0) {
+          $by_name.get(0).focus();
+        }
+      }
+    }
 
-  // legacy. sone forms install these
-  if (typeof fokus == 'function') { fokus(); return; }
-  if (focus_by_name('cursor_fokus')) return;
+    // legacy. some forms install these
+    if (typeof fokus == 'function') { fokus(); return; }
+    if (focus_by_name('cursor_fokus')) return;
+  }, 0);
 });
 
 $('form').submit(function(){
diff --git a/js/customer_or_vendor_selection.js b/js/customer_or_vendor_selection.js
deleted file mode 100644 (file)
index b09183f..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-function customer_or_vendor_selection_window(input_name, input_id, is_vendor, allow_both, action_on_cov_selected) {
-  var parm = centerParms(800,600) + ",width=800,height=600,status=yes,scrollbars=yes";
-  var name = document.getElementsByName(input_name)[0].value;
-  url = "common.pl?" +
-    "INPUT_ENCODING=UTF-8&" +
-    "action=cov_selection_internal&" +
-    "name=" + encodeURIComponent(name) + "&" +
-    "input_name=" + encodeURIComponent(input_name) + "&" +
-    "input_id=" + encodeURIComponent(input_id) + "&" +
-    "is_vendor=" + (is_vendor ? "1" : "0") + "&" +
-    "allow_both=" + (allow_both ? "1" : "0") + "&" +
-    "action_on_cov_selected=" + (action_on_cov_selected ? encodeURIComponent(action_on_cov_selected) : "")
-  //alert(url);
-  window.open(url, "_new_cov_selection", parm);
-}
index b969e1d..263b2fa 100644 (file)
@@ -1,14 +1,18 @@
 function edit_periodic_invoices_config() {
-  var width     = 750;
-  var height    = 550;
+  var width     = 800;
+  var height    = 650;
   var parm      = centerParms(width, height) + ",width=" + width + ",height=" + height + ",status=yes,scrollbars=yes";
 
   var config    = $('#periodic_invoices_config').val();
+  var cus_id    = $('[name=customer_id]').val();
   var transdate = $('#transdate').val();
+  var lang_id   = $('#language_id').val();
 
   var url       = "oe.pl?" +
     "action=edit_periodic_invoices_config&" +
-    "periodic_invoices_config=" + encodeURIComponent(config) + "&" +
+    "customer_id="              + encodeURIComponent(cus_id)  + "&" +
+    "language_id="              + encodeURIComponent(lang_id) + "&" +
+    "periodic_invoices_config=" + encodeURIComponent(config)  + "&" +
     "transdate="                + encodeURIComponent(transdate || '');
 
   // alert(url);
index 4b11dcd..83132ac 100644 (file)
@@ -12,14 +12,16 @@ function follow_up_window() {
 
   if (typeof trans_rowcount != "undefined") {
     for (i = 1; i <= trans_rowcount[0].value; i++) {
-      var trans_id   = document.getElementsByName("follow_up_trans_id_" + i);
-      var trans_type = document.getElementsByName("follow_up_trans_type_" + i);
-      var trans_info = document.getElementsByName("follow_up_trans_info_" + i);
+      var trans_id      = document.getElementsByName("follow_up_trans_id_" + i);
+      var trans_type    = document.getElementsByName("follow_up_trans_type_" + i);
+      var trans_info    = document.getElementsByName("follow_up_trans_info_" + i);
+      var trans_subject = document.getElementsByName("follow_up_trans_subject_" + i);
 
       url += "&" +
-        "trans_id_"   + i + "=" + encodeURIComponent(typeof trans_id   != "undefined" ? trans_id[0].value   : "") + "&" +
-        "trans_type_" + i + "=" + encodeURIComponent(typeof trans_type != "undefined" ? trans_type[0].value : "") + "&" +
-        "trans_info_" + i + "=" + encodeURIComponent(typeof trans_info != "undefined" ? trans_info[0].value : "");
+        "trans_id_"      + i + "=" + encodeURIComponent((typeof trans_id      != "undefined" && trans_id.length      != 0) ? trans_id[0].value      : "") + "&" +
+        "trans_type_"    + i + "=" + encodeURIComponent((typeof trans_type    != "undefined" && trans_type.length    != 0) ? trans_type[0].value    : "") + "&" +
+        "trans_info_"    + i + "=" + encodeURIComponent((typeof trans_info    != "undefined" && trans_info.length    != 0) ? trans_info[0].value    : "") + "&" +
+        "trans_subject_" + i + "=" + encodeURIComponent((typeof trans_subject != "undefined" && trans_subject.length != 0) ? trans_subject[0].value : "");
     }
 
     url += "&trans_rowcount=" + encodeURIComponent(trans_rowcount[0].value);
diff --git a/js/glquicksearch.js b/js/glquicksearch.js
deleted file mode 100644 (file)
index 2b8b3df..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-$(function() {
-  $( "#glquicksearch" ).autocomplete({
-    source: "controller.pl?action=GL/quicksearch",
-    minLength: 3,
-    select: function(event, ui) {
-           var url = ui.item.url;
-           if(url != '#') {
-               location.href = url;
-           }
-       },
-    html: false,
-    autoFocus: true
-  });
-});
index c92c2ac..b7fa78d 100644 (file)
                        })
                        .appendTo("body");
                $(document).bind("mousedown", function (e) { if($.vakata.context.vis && !$.contains($.vakata.context.cnt[0], e.target)) { $.vakata.context.hide(); } });
-               if(typeof $.hotkeys !== "undefined") {
-                       $(document)
-                               .bind("keydown", "up", function (e) { 
-                                       if($.vakata.context.vis) { 
-                                               var o = $.vakata.context.cnt.find("ul:visible").last().children(".vakata-hover").removeClass("vakata-hover").prevAll("li:not(.vakata-separator)").first();
-                                               if(!o.length) { o = $.vakata.context.cnt.find("ul:visible").last().children("li:not(.vakata-separator)").last(); }
-                                               o.addClass("vakata-hover");
-                                               e.stopImmediatePropagation(); 
-                                               e.preventDefault();
-                                       } 
-                               })
-                               .bind("keydown", "down", function (e) { 
-                                       if($.vakata.context.vis) { 
-                                               var o = $.vakata.context.cnt.find("ul:visible").last().children(".vakata-hover").removeClass("vakata-hover").nextAll("li:not(.vakata-separator)").first();
-                                               if(!o.length) { o = $.vakata.context.cnt.find("ul:visible").last().children("li:not(.vakata-separator)").first(); }
-                                               o.addClass("vakata-hover");
-                                               e.stopImmediatePropagation(); 
-                                               e.preventDefault();
-                                       } 
-                               })
-                               .bind("keydown", "right", function (e) { 
-                                       if($.vakata.context.vis) { 
-                                               $.vakata.context.cnt.find(".vakata-hover").children("ul").show().children("li:not(.vakata-separator)").removeClass("vakata-hover").first().addClass("vakata-hover");
-                                               e.stopImmediatePropagation(); 
-                                               e.preventDefault();
-                                       } 
-                               })
-                               .bind("keydown", "left", function (e) { 
-                                       if($.vakata.context.vis) { 
-                                               $.vakata.context.cnt.find(".vakata-hover").children("ul").hide().children(".vakata-separator").removeClass("vakata-hover");
-                                               e.stopImmediatePropagation(); 
-                                               e.preventDefault();
-                                       } 
-                               })
-                               .bind("keydown", "esc", function (e) { 
-                                       $.vakata.context.hide(); 
-                                       e.preventDefault();
-                               })
-                               .bind("keydown", "space", function (e) { 
-                                       $.vakata.context.cnt.find(".vakata-hover").last().children("a").click();
-                                       e.preventDefault();
-                               });
-               }
        });
 
        $.jstree.plugin("contextmenu", {
diff --git a/js/kivi.AP.js b/js/kivi.AP.js
new file mode 100644 (file)
index 0000000..90ef829
--- /dev/null
@@ -0,0 +1,57 @@
+namespace('kivi.AP', function(ns){
+  'use strict';
+
+  ns.check_fields_before_posting = function() {
+    var errors = [];
+
+    // if the element transdate exists, we have a AP form otherwise we have to check the invoice form
+    var invoice_date = ($('#transdate').length === 0) ? $('#transdate').val() : $('#invdate').val();
+    if (invoice_date === '')
+      errors.push(kivi.t8('Invoice Date missing!'));
+
+    if ($('#duedate').val() === '')
+      errors.push(kivi.t8('Due Date missing!'));
+
+    if ($('#invnumber').val() === '')
+      errors.push(kivi.t8('Invoice Number missing!'));
+
+    if ($('#vendor_id').val() ===  '')
+      errors.push(kivi.t8('Vendor missing!'));
+
+    if (errors.length === 0)
+      return true;
+
+    alert(errors.join(' '));
+
+    return false;
+  };
+
+  ns.check_duplicate_invnumber = function() {
+    var exists_invnumber = false;
+
+    $.ajax({
+      url: 'controller.pl',
+      data: { action: 'SalesPurchase/check_duplicate_invnumber',
+              vendor_id    : $('#vendor_id').val(),
+              invnumber    : $('#invnumber').val()
+      },
+      method: "GET",
+      async: false,
+      dataType: 'text',
+      success: function(val) {
+        exists_invnumber = val;
+      }
+    });
+
+    if (exists_invnumber == 1) {
+      return confirm(kivi.t8('This vendor has already a booking with this invoice number, do you really want to add the same invoice number again?'));
+    }
+
+    return true;
+  };
+
+});
+
+$(function() {
+  kivi.File.doc_tab_init('ap_tabs', 'ui-tabs-docs', $('#id').val(), 'purchase_invoice');
+});
diff --git a/js/kivi.AR.js b/js/kivi.AR.js
new file mode 100644 (file)
index 0000000..40b297b
--- /dev/null
@@ -0,0 +1,23 @@
+namespace('kivi.AR', function(ns){
+  'use strict';
+
+  ns.check_fields_before_posting = function() {
+    var errors = [];
+
+    if ($('#transdate').val() === '')
+      errors.push(kivi.t8('Invoice Date missing!'));
+
+    if ($('#duedate').val() === '')
+      errors.push(kivi.t8('Due Date missing!'));
+
+    if ($('#customer').val() === '')
+      errors.push(kivi.t8('Customer missing!'));
+
+    if (errors.length === 0)
+      return true;
+
+    alert(errors.join(' '));
+
+    return false;
+  };
+});
diff --git a/js/kivi.ActionBar.js b/js/kivi.ActionBar.js
new file mode 100644 (file)
index 0000000..dc1d35f
--- /dev/null
@@ -0,0 +1,247 @@
+namespace('kivi.ActionBar', function(k){
+  'use strict';
+
+  var CLASSES = {
+    active:   'active',
+    actionbar: 'layout-actionbar',
+    disabled: 'layout-actionbar-action-disabled',
+    action:   'layout-actionbar-action',
+    combobox: 'layout-actionbar-combobox',
+    default:  'layout-actionbar-default-action',
+  };
+
+  k.Combobox = function(e) {
+    this.combobox  = e;
+    this.head      = e.childNodes[0];
+    this.topAction = this.head.childNodes[0];
+    this.toggle    = this.head.childNodes[1];
+    this.list      = e.childNodes[0];
+    this.init();
+  };
+
+  k.Combobox.prototype = {
+    init: function() {
+      var obj     = this;
+      var toggler = function(event){
+        $('div.' + CLASSES.combobox + '[id!=' + obj.combobox.id + ']').removeClass(CLASSES.active);
+        $(obj.combobox).toggleClass(CLASSES.active);
+        event.stopPropagation();
+      };
+
+      $(obj.toggle).on('click', toggler);
+
+      var data = $(this.topAction).data('action') || {};
+      if (!data.call && !data.submit)
+        $(this.topAction).on('click', toggler);
+    }
+  };
+
+  k.Accesskeys = {
+    known_keys: {
+     'enter': 13,
+     'esc': 27,
+    },
+    actions: {},
+    bound_targets: {},
+
+    add_accesskey: function (target, keystring, action) {
+      if (target === undefined) {
+        target = 'document';
+      }
+
+      var normalized = $.map(String.prototype.split.call(keystring, '+'), function(val, i) {
+        switch (val) {
+          case 'ctrl':
+          case 'alt':  return val;
+          case 'enter': return 13;
+          default:
+            if (val.length == 1) {
+              return val.charCodeAt(0);
+            } else if (val % 1 === 0) {
+              return val;
+            } else {
+              console.log('can not normalize access key token: ' + val);
+            }
+        }
+      }).join('+');
+
+      if (!(target in this.actions))
+        this.actions[target] = {};
+      this.actions[target][normalized] = action;
+    },
+
+    bind_targets: function(){
+      for (var target in this.actions) {
+        if (target in this.bound_targets) continue;
+        $(target).on('keypress', null, { 'target': target }, this.handle_accesskey);
+        this.bound_targets[target] = 1;
+      }
+    },
+
+    handle_accesskey: function(e,t) {
+      var target = e.data.target;
+      var key = e.which;
+      var accesskey = '';
+      if (e.ctrlKey) accesskey += 'crtl+'
+      if (e.altKey)  accesskey += 'alt+'
+      accesskey += e.which;
+
+      // special case. HTML elements that make legitimate use of enter will also trigger the enter accesskey.
+      // so. if accesskey is '13' and the event source is one of these (currently only textareas & combo boxes) ignore it.
+      // higher level widgets will usually prevent their key events from bubbling if used.
+      if (   (accesskey == 13)
+          && (   (e.target.tagName == 'TEXTAREA')
+              || (e.target.tagName == 'SELECT')))
+        return true;
+
+      if ((target in kivi.ActionBar.Accesskeys.actions) && (accesskey in kivi.ActionBar.Accesskeys.actions[target])) {
+        e.stopPropagation();
+        kivi.ActionBar.Accesskeys.actions[target][accesskey].click();
+
+        // and another special case.
+        // if the form contains submit buttons the default action will click them instead.
+        // prevent that
+        if (accesskey == 13) return false;
+      }
+      return true;
+    }
+  };
+
+  k.removeTooltip = function(e) {
+    var $e = $(e);
+    if ($e.hasClass('tooltipstered'))
+      $e.tooltipster('destroy');
+    $e.prop('title', '');
+  };
+
+  k.setTooltip = function(e, tooltip) {
+    var $e = $(e);
+    if ($e.hasClass('tooltipstered'))
+      $e.tooltipster('content', tooltip);
+    else
+      $e.tooltipster({ content: tooltip, theme: 'tooltipster-light' });
+  };
+
+  k.setDisabled = function(e, tooltip) {
+    var $e = $(e);
+
+    $e.addClass(CLASSES.disabled);
+
+    if (tooltip && (tooltip != '1'))
+      kivi.ActionBar.setTooltip($e, tooltip);
+    else
+      kivi.ActionBar.removeTooltip($e);
+  };
+
+  k.setEnabled = function(e) {
+    var $e   = $(e);
+    var data = $e.data('action');
+
+    $e.removeClass(CLASSES.disabled);
+
+    if (data.tooltip)
+      kivi.ActionBar.setTooltip($e, data.tooltip);
+    else
+      kivi.ActionBar.removeTooltip($e);
+  };
+
+  k.Action = function(e) {
+    var $e       = $(e);
+    var instance = $e.data('instance');
+    if (instance)
+      return instance;
+
+    var data = $e.data('action');
+    if (undefined === data) return;
+
+    data.originalTooltip = data.tooltip;
+
+    if (data.disabled && (data.disabled != '0'))
+      kivi.ActionBar.setDisabled($e, data.disabled);
+
+    else if (data.tooltip)
+      kivi.ActionBar.setTooltip($e, data.tooltip);
+
+    if (data.accesskey) {
+      if (data.submit) {
+        kivi.ActionBar.Accesskeys.add_accesskey(data.submit[0], data.accesskey, $e);
+      }
+      if (data.call) {
+        kivi.ActionBar.Accesskeys.add_accesskey('body', data.accesskey, $e);
+      }
+      if (data.accesskey == 'enter') {
+        $e.addClass(CLASSES.default);
+      }
+    }
+
+    if (data.call || data.submit || data.link) {
+      $e.click(function(event) {
+        var $hidden, key, func, check;
+        if ($e.hasClass(CLASSES.disabled)) {
+          event.stopPropagation();
+          return;
+        }
+        if (data.checks) {
+          for (var i=0; i < data.checks.length; i++) {
+            check = data.checks[i];
+            if (check.constructor !== Array)
+              check = [ check ];
+            func = kivi.get_function_by_name(check[0]);
+            if (!func)
+              console.log('Cannot find check function: ' + check);
+            if (!func.apply(document, check.slice(1)))
+              return;
+          }
+        }
+        if (data.confirm && !confirm(data.confirm)) return;
+        if (data.call) {
+          func = kivi.get_function_by_name(data.call[0]);
+          func.apply(document, data.call.slice(1));
+        }
+        if (data.submit) {
+          var form   = data.submit[0];
+          var params = data.submit[1];
+          for (key in params) {
+            $('[name=' + key + ']').remove();
+            $hidden = $('<input type=hidden>');
+            $hidden.attr('name', key);
+            $hidden.attr('value', params[key]);
+            $(form).append($hidden);
+          }
+          $(form).submit();
+        }
+        if (data.link) {
+          window.location.href = data.link;
+        }
+        if ((data.only_once !== undefined) && (data.only_once !== 0)) {
+          $e.addClass(CLASSES.disabled);
+          $e.tooltipster({ content: kivi.t8("The action can only be executed once."), theme: 'tooltipster-light' });
+        }
+      });
+    }
+
+    instance = {
+      removeTooltip: function()        { kivi.ActionBar.removeTooltip($e); },
+      setTooltip:    function(tooltip) { kivi.ActionBar.setTooltip($e, tooltip); },
+      disable:       function(tooltip) { kivi.ActionBar.setDisabled($e, tooltip); },
+      enable:        function()        { kivi.ActionBar.setEnabled($e, $e.data('action').tooltip); },
+    };
+
+    $e.data('instance', instance);
+
+    return instance;
+  };
+});
+
+$(function(){
+  $('div.layout-actionbar .layout-actionbar-action').each(function(_, e) {
+    kivi.ActionBar.Action(e);
+  });
+  $('div.layout-actionbar-combobox').each(function(_, e) {
+    $(e).data('combobox', new kivi.ActionBar.Combobox(e));
+  });
+  $(document).click(function() {
+    $('div.layout-actionbar-combobox').removeClass('active');
+  });
+  kivi.ActionBar.Accesskeys.bind_targets();
+});
diff --git a/js/kivi.BankTransaction.js b/js/kivi.BankTransaction.js
new file mode 100644 (file)
index 0000000..2441aa1
--- /dev/null
@@ -0,0 +1,178 @@
+namespace('kivi.BankTransaction', function(ns) {
+  "use strict";
+
+  ns.assign_invoice = function(bank_transaction_id) {
+    kivi.popup_dialog({
+      url:    'controller.pl?action=BankTransaction/assign_invoice',
+      data:   '&bt_id=' + bank_transaction_id,
+      type:   'POST',
+      id:     'assign_invoice_window',
+      dialog: { title: kivi.t8('Assign invoice') }
+    });
+    return true;
+  };
+
+  ns.add_invoices = function(bank_transaction_id, proposal_id) {
+
+    $.ajax({
+      url: 'controller.pl?action=BankTransaction/ajax_payment_suggestion&bt_id=' + bank_transaction_id  + '&prop_id=' + proposal_id,
+      success: function(data) {
+        $('#assigned_invoices_' + bank_transaction_id + "_" + proposal_id).html(data.html);
+        $('#sources_' + bank_transaction_id + "_" + proposal_id + ',' +
+          '#memos_'   + bank_transaction_id + "_" + proposal_id).show();
+        $('[data-proposal-id=' + proposal_id + ']').hide();
+
+        ns.update_invoice_amount(bank_transaction_id);
+      }
+    });
+  };
+
+  ns.delete_invoice = function(bank_transaction_id, proposal_id) {
+    var $inputs = $('#sources_' + bank_transaction_id + "_" + proposal_id + ',' +
+                    '#memos_'   + bank_transaction_id + "_" + proposal_id);
+
+    $('[data-proposal-id=' + proposal_id + ']').show();
+    $('#assigned_invoices_' + bank_transaction_id + "_" + proposal_id).html('');
+    $('#extra_row_' + bank_transaction_id + '_' + proposal_id).remove();
+
+    $inputs.hide();
+    $inputs.val('');
+
+    ns.update_invoice_amount(bank_transaction_id);
+  };
+
+  ns.create_invoice = function(bank_transaction_id) {
+    $.post('controller.pl?action=BankTransaction/create_invoice',
+           '&bt_id=' + bank_transaction_id + "&filter.bank_account=" + $('#filter_bank_account').val() + '&filter.fromdate=' + $('#filter_fromdate').val() + '&filter.todate=' + $('#filter_todate').val(),
+           kivi.eval_json_result);
+  };
+
+  ns.show_create_invoice_dialog = function(dialog_html) {
+    kivi.popup_dialog({
+      html:    dialog_html,
+      id:     'create_invoice_window',
+      dialog: { title: kivi.t8('Create invoice') }
+    });
+  };
+
+
+  ns.filter_invoices = function() {
+    var url="controller.pl?action=BankTransaction/ajax_add_list&" + $("#assign_invoice_window form").serialize();
+    $.ajax({
+      url: url,
+      success: function(data) {
+        $("#record_list_filtered_list").html(data.html);
+      }
+    });
+  }
+
+  ns.add_selected_invoices = function() {
+    var bank_transaction_id = $("#assign_invoice_window_form").data("bank-transaction-id");
+    var url                 ="controller.pl?action=BankTransaction/ajax_accept_invoices&bt_id=" + bank_transaction_id + '&' + $("#assign_invoice_window form").serialize();
+
+    $.ajax({
+      url: url,
+      success: function(new_html) {
+        $('#bt_rows_' + bank_transaction_id).append(new_html);
+        $('#assign_invoice_window').dialog('close');
+        ns.update_invoice_amount(bank_transaction_id);
+      }
+    });
+  }
+
+  ns.update_invoice_amount = function(bank_transaction_id) {
+    var $container = $('#invoice_amount_' + bank_transaction_id);
+    var amount     = $container.data('invoice-amount') * 1;
+
+    $('[id^="' + bank_transaction_id + '."]').each(function(idx, elt) {
+      if ($("input[name='skonto_pt." + elt.id + "']").val() == 1) {
+        // skonto payment term
+        amount += $(elt).data('invoice-amount-less-skonto');
+      } else {
+        // normal amount
+        amount += $(elt).data('invoice-amount');
+        //subtract free skonto if checked (no check for number!)
+        if ($("input[name='skonto_pt." + elt.id + "']").val() == 'free_skonto') {
+          amount -= $("input[name='free_skonto_amount." + elt.id + "']").val();
+        }
+      }
+    });
+
+    $container.html(kivi.format_amount(amount, 2));
+  };
+
+  ns.init_list = function(ui_tab) {
+    $('#check_all').checkall('INPUT[name^="proposal_ids"]');
+
+    $('.sort_link').each(function() {
+      var _href = $(this).attr("href");
+      $(this).attr("href", _href + "&filter.fromdate=" + $('#filter_fromdate').val() + "&filter.todate=" + $('#filter_todate').val());
+    });
+
+    $.cookie('jquery_ui_tab_bt_tabs', ui_tab);
+  };
+
+  ns.show_set_all_sources_memos_dialog = function(sources_selector, memos_selector) {
+    var dlg_id = 'set_all_sources_memos_dialog';
+    var $dlg   = $('#' + dlg_id);
+
+    $dlg.data('sources-selector', sources_selector);
+    $dlg.data('memos-selector',   memos_selector);
+
+    $('#set_all_sources').val('');
+    $('#set_all_memos').val('');
+
+    kivi.popup_dialog({
+      id: dlg_id,
+      dialog: {
+        title: kivi.t8('Set all source and memo fields')
+      }
+    });
+  };
+
+  ns.set_all_sources_memos = function(sources_selector, memos_selector) {
+    var $dlg = $('#set_all_sources_memos_dialog');
+
+    ['sources', 'memos'].forEach(function(type) {
+      var value = $('#set_all_' + type).val();
+      if (value !== '')
+        $($dlg.data(type + '-selector')).each(function(idx, input) {
+          $(input).val(value);
+        });
+    });
+
+    $dlg.dialog('close');
+  };
+
+  ns.filter_templates = function() {
+    var url="controller.pl?action=BankTransaction/filter_templates&" + $("#create_invoice_window form").serialize();
+    $.ajax({
+      url: url,
+      success: function(new_data) {
+        $("#templates").html(new_data.error || new_data.html);
+      }
+    });
+  };
+  ns.update_skonto = function(caller, bt_id, prop_id, formatted_amount_with_skonto_pt) {
+
+    if (caller.value === 'free_skonto') {
+      $('#free_skonto_amount_' + bt_id + '_' + prop_id).val("");
+      $('#free_skonto_amount_' + bt_id + '_' + prop_id).prop('disabled', false);
+      $("input[name='skonto_pt." + bt_id + '.' + prop_id + "']").val('free_skonto');
+      $('#free_skonto_amount_' + bt_id + '_' + prop_id).focus();
+    }
+    if (caller.value === 'without_skonto') {
+      $('#free_skonto_amount_' + bt_id + '_' + prop_id).val(kivi.format_amount(0,2));
+      $('#free_skonto_amount_' + bt_id + '_' + prop_id).prop('disabled', true);
+      $("input[name='skonto_pt." + bt_id + '.' + prop_id + "']").val(0);
+    }
+    if (caller.value === 'with_skonto_pt') {
+      $('#free_skonto_amount_' + bt_id + '_' + prop_id).val(formatted_amount_with_skonto_pt);
+      $('#free_skonto_amount_' + bt_id + '_' + prop_id).prop('disabled', true);
+      $("input[name='skonto_pt." + bt_id + '.' + prop_id + "']").val(1);
+    }
+    // recalc assigned amount
+    ns.update_invoice_amount(bt_id);
+  };
+
+});
diff --git a/js/kivi.CustomDataExportDesigner.js b/js/kivi.CustomDataExportDesigner.js
new file mode 100644 (file)
index 0000000..53e8953
--- /dev/null
@@ -0,0 +1,13 @@
+namespace('kivi.CustomDataExportDesigner', function(ns){
+  'use strict';
+
+  ns.enable_default_value = function() {
+    var count = $(this).prop('id').replace("default_value_type_", "");
+    var type  = $(this).val();
+    $('#default_value_' + count).prop('disabled', (type === 'none') || (type === 'current_user_login'));
+  };
+});
+
+$(function() {
+  $('[id^="default_value_type_"]').change(kivi.CustomDataExportDesigner.enable_default_value);
+});
index 4b4aa3f..02a2c2e 100644 (file)
@@ -2,28 +2,42 @@ namespace('kivi.CustomerVendor', function(ns) {
 
   this.selectShipto = function(params) {
     var shiptoId = $('#shipto_shipto_id').val();
+    var url      = 'controller.pl?action=CustomerVendor/ajaj_get_shipto&id='+ $('#cv_id').val() +'&db='+ $('#db').val() +'&shipto_id='+ shiptoId;
 
-    if( shiptoId ) {
-      var url = 'controller.pl?action=CustomerVendor/ajaj_get_shipto&id='+ $('#cv_id').val() +'&db='+ $('#db').val() +'&shipto_id='+ shiptoId;
+    $.getJSON(url, function(data) {
+      var shipto = data.shipto;
+      for(var key in shipto)
+        $('#shipto_'+ key).val(shipto[key])
 
-      $.getJSON(url, function(data) {
-        for(var key in data)
-          $(document.getElementById('shipto_'+ key)).val(data[key]);
+      kivi.CustomerVendor.setCustomVariablesFromAJAJ(data.shipto_cvars, 'shipto_cvars_');
 
+      if ( shiptoId )
         $('#action_delete_shipto').show();
+      else
+        $('#action_delete_shipto').hide();
 
-        if( params.onFormSet )
-          params.onFormSet();
-      });
-    }
-    else {
-      $('#shipto :input').not(':button, :submit, :reset, :hidden').val('');
+      if ( params.onFormSet )
+        params.onFormSet();
+    });
+  };
 
-      $('#action_delete_shipto').hide();
+  this.selectAdditionalBillingAddress = function(params) {
+    var additionalBillingAddressId = $('#additional_billing_address_id').val();
+    var url                        = 'controller.pl?action=CustomerVendor/ajaj_get_additional_billing_address&id='+ $('#cv_id').val() +'&db='+ $('#db').val() +'&additional_billing_address_id='+ additionalBillingAddressId;
 
-      if( params.onFormSet )
+    $.getJSON(url, function(data) {
+      var additional_billing_address = data.additional_billing_address;
+      for (var key in additional_billing_address)
+        $('#additional_billing_address_'+ key).val(additional_billing_address[key])
+
+      if ( additionalBillingAddressId )
+        $('#action_delete_additional_billing_address').show();
+      else
+        $('#action_delete_additional_billing_address').hide();
+
+      if ( params.onFormSet )
         params.onFormSet();
-    }
+    });
   };
 
   this.selectDelivery = function(fromDate, toDate) {
@@ -41,21 +55,19 @@ namespace('kivi.CustomerVendor', function(ns) {
     }
   };
 
-  this.setCustomVariablesFromAJAJ = function(cvars) {
+  this.setCustomVariablesFromAJAJ = function(cvars, prefix) {
     for (var key in cvars) {
       var cvar  = cvars[key];
-      var $ctrl = $('#contact_cvars_'+ key);
-
-      console.log($ctrl, cvar);
+      var $ctrl = $('#' + prefix + key);
 
       if (cvar.type == 'bool')
         $ctrl.prop('checked', cvar.value == 1 ? 'checked' : '');
 
       else if ((cvar.type == 'customer') || (cvar.type == 'vendor'))
-        kivi.CustomerVendorPicker($ctrl).set_item({ id: cvar.id, name: cvar.value });
+        kivi.CustomerVendor.Picker($ctrl).set_item({ id: cvar.id, name: cvar.value });
 
       else if (cvar.type == 'part')
-        kivi.PartPicker($ctrl).set_item({ id: cvar.id, name: cvar.value });
+        kivi.Part.Picker($ctrl).set_item({ id: cvar.id, name: cvar.value });
 
       else
         $ctrl.val(cvar.value);
@@ -65,25 +77,31 @@ namespace('kivi.CustomerVendor', function(ns) {
   this.selectContact = function(params) {
     var contactId = $('#contact_cp_id').val();
 
-         var url = 'controller.pl?action=CustomerVendor/ajaj_get_contact&id='+ $('#cv_id').val() +'&db='+ $('#db').val() +'&contact_id='+ contactId;
+    var url = 'controller.pl?action=CustomerVendor/ajaj_get_contact&id='+ $('#cv_id').val() +'&db='+ $('#db').val() +'&contact_id='+ contactId;
 
     $.getJSON(url, function(data) {
       var contact = data.contact;
       for(var key in contact)
-        $(document.getElementById('contact_'+ key)).val(contact[key])
+        $('#contact_'+ key).val(contact[key])
 
-      kivi.CustomerVendor.setCustomVariablesFromAJAJ(data.contact_cvars);
+      kivi.CustomerVendor.setCustomVariablesFromAJAJ(data.contact_cvars, 'contact_cvars_');
 
-      if ( contactId )
+      if ( contactId ) {
         $('#action_delete_contact').show();
-      else
+        $('#contact_cp_title_select').val(contact['cp_title']);
+        $('#contact_cp_abteilung_select').val(contact['cp_abteilung']);
+      } else {
         $('#action_delete_contact').hide();
-
+        $('#contact_cp_title_select, #contact_cp_abteilung_select').val('');
+      }
+      if (data.contact.disable_cp_main === 1)
+        $("#contact_cp_main").prop("disabled", true);
+      else
+        $("#contact_cp_main").prop("disabled", false);
       if ( params.onFormSet )
         params.onFormSet();
     });
 
-    $('#contact_cp_title_select, #contact_cp_abteilung_select').val('');
   };
 
   var mapSearchStmts = [
@@ -117,7 +135,7 @@ namespace('kivi.CustomerVendor', function(ns) {
 
     var isNotEmpty = function() {
       for(var i in $mapSearchElements)
-        if( ($mapSearchElements[i].attr('id') != prefix + 'country') && ($mapSearchElements[i].val() == '') )
+        if( ($mapSearchElements[i].attr('id') != prefix + 'country') && ($mapSearchElements[i].val() === '') )
           return false;
       return true;
     };
@@ -137,7 +155,7 @@ namespace('kivi.CustomerVendor', function(ns) {
       }
 
       source_address = source_address || '';
-      var query      = source_address != '' ? 'saddr=' + encodeURIComponent(source_address) + '&daddr=' : 'q=';
+      var query      = source_address !== '' ? 'saddr=' + encodeURIComponent(source_address) + '&daddr=' : 'q=';
       var url        = 'https://maps.google.com/maps?' + query + encodeURIComponent(searchString);
 
       window.open(url, '_blank');
@@ -155,9 +173,7 @@ namespace('kivi.CustomerVendor', function(ns) {
           showMap();
         });
       for(var i in $mapSearchElements)
-        $mapSearchElements[i].keyup(function() {
-          testInputs();
-        });
+        $mapSearchElements[i].keyup(testInputs);
       this.testInputs();
     };
 
@@ -189,7 +205,7 @@ namespace('kivi.CustomerVendor', function(ns) {
       return true;
 
     var number = $input.val().replace(/\s+/g, '');
-    if (number == '')
+    if (number === '')
       $action.hide();
     else
       $action.prop('href', 'controller.pl?action=CTI/call&number=' + encodeURIComponent(number)).show();
@@ -217,6 +233,7 @@ namespace('kivi.CustomerVendor', function(ns) {
   };
 
   this.inline_report = function(target, source, data){
+//    alert("HALLO S " + source + " --T " + target + " tt D " + data);
     $.ajax({
       url:        source,
       success:    function (rsp) {
@@ -231,10 +248,268 @@ namespace('kivi.CustomerVendor', function(ns) {
     event.preventDefault();
     ns.inline_report(target, event.target + '', {});
   };
-});
 
-function local_reinit_widgets() {
-  $('#cv_phone,#shipto_shiptophone,#contact_cp_phone1,#contact_cp_phone2,#contact_cp_mobile1,#contact_cp_mobile2').each(function(idx, elt) {
-    kivi.CustomerVendor.init_dial_action($(elt));
+  var KEY = {
+    TAB:       9,
+    ENTER:     13,
+    SHIFT:     16,
+    CTRL:      17,
+    ALT:       18,
+    ESCAPE:    27,
+    PAGE_UP:   33,
+    PAGE_DOWN: 34,
+    LEFT:      37,
+    UP:        38,
+    RIGHT:     39,
+    DOWN:      40,
+  };
+
+  ns.Picker = function($real, options) {
+    var self = this;
+    this.o = $.extend(true, {
+      limit: 20,
+      delay: 50,
+      action: {
+        commit_none: function(){ },
+        commit_one:  function(){ $('#update_button').click(); },
+        commit_many: function(){ }
+      }
+    }, $real.data('customer-vendor-picker-data'), options);
+    this.$real              = $real;
+    this.real_id            = $real.attr('id');
+    this.last_real          = $real.val();
+    this.$dummy             = $($real.siblings()[0]);
+    this.autocomplete_open  = false;
+    this.state              = this.STATES.PICKED;
+    this.last_dummy         = this.$dummy.val();
+    this.timer              = undefined;
+
+    this.init();
+  };
+
+  ns.Picker.prototype = {
+    CLASSES: {
+      PICKED:       'customer-vendor-picker-picked',
+      UNDEFINED:    'customer-vendor-picker-undefined',
+    },
+    ajax_data: function(term) {
+      return {
+        'filter.all:substr:multi::ilike': term,
+        'filter.obsolete': 0,
+        current:  this.$real.val(),
+        type:     this.o.cv_type,
+      };
+    },
+    set_item: function(item) {
+      var self = this;
+      if (item.id) {
+        this.$real.val(item.id);
+        // autocomplete ui has name, use the value for ajax items, which contains displayable_name
+        this.$dummy.val(item.name ? item.name : item.value);
+      } else {
+        this.$real.val('');
+        this.$dummy.val('');
+      }
+      this.state      = this.STATES.PICKED;
+      this.last_real  = this.$real.val();
+      this.last_dummy = this.$dummy.val();
+      this.$real.trigger('change');
+
+      if (this.o.fat_set_item && item.id) {
+        $.ajax({
+          url: 'controller.pl?action=CustomerVendor/show.json',
+          data: { 'id': item.id, 'db': item.type },
+          success: function(rsp) {
+            self.$real.trigger('set_item:CustomerVendorPicker', rsp);
+          },
+        });
+      } else {
+        this.$real.trigger('set_item:CustomerVendorPicker', item);
+      }
+      this.annotate_state();
+    },
+    set_multi_items: function(data) {
+      this.run_action(this.o.action.set_multi_items, [ data ]);
+    },
+    make_defined_state: function() {
+      if (this.state == this.STATES.PICKED) {
+        this.annotate_state();
+        return true
+      } else if (this.state == this.STATES.UNDEFINED && this.$dummy.val() === '')
+        this.set_item({})
+      else {
+        this.set_item({ id: this.last_real, name: this.last_dummy })
+      }
+      this.annotate_state();
+    },
+    annotate_state: function() {
+      if (this.state == this.STATES.PICKED)
+        this.$dummy.removeClass(this.STATES.UNDEFINED).addClass(this.STATES.PICKED);
+      else if (this.state == this.STATES.UNDEFINED && this.$dummy.val() === '')
+        this.$dummy.removeClass(this.STATES.UNDEFINED).addClass(this.STATES.PICKED);
+      else {
+        this.$dummy.addClass(this.STATES.UNDEFINED).removeClass(this.STATES.PICKED);
+      }
+    },
+    handle_changed_text: function(callbacks) {
+      var self = this;
+      $.ajax({
+        url: 'controller.pl?action=CustomerVendor/ajaj_autocomplete',
+        dataType: "json",
+        data: $.extend( self.ajax_data(self.$dummy.val()), { prefer_exact: 1 } ),
+        success: function (data) {
+          if (data.length == 1) {
+            self.set_item(data[0]);
+            if (callbacks && callbacks.match_one) self.run_action(callbacks.match_one, [ data[0] ]);
+          } else if (data.length > 1) {
+            self.state = self.STATES.UNDEFINED;
+            if (callbacks && callbacks.match_many) self.run_action(callbacks.match_many, [ data ]);
+          } else {
+            self.state = self.STATES.UNDEFINED;
+            if (callbacks && callbacks.match_none) self.run_action(callbacks.match_none, [ self, self.$dummy.val() ]);
+          }
+          self.annotate_state();
+        }
+      });
+    },
+    handle_keydown: function(event) {
+      var self = this;
+      if (event.which == KEY.ENTER || event.which == KEY.TAB) {
+        // if string is empty assume they want to delete
+        if (self.$dummy.val() === '') {
+          self.set_item({});
+          return true;
+        } else if (self.state == self.STATES.PICKED) {
+          if (self.o.action.commit_one) {
+            self.run_action(self.o.action.commit_one);
+          }
+          return true;
+        }
+        if (event.which == KEY.TAB) {
+          event.preventDefault();
+          self.handle_changed_text();
+        }
+        if (event.which == KEY.ENTER) {
+          event.preventDefault();
+          self.handle_changed_text({
+            match_none: self.o.action.commit_none,
+            match_one:  self.o.action.commit_one,
+            match_many: self.o.action.commit_many
+          });
+          return false;
+        }
+      } else if (event.which == KEY.DOWN && !self.autocomplete_open) {
+        var old_options = self.$dummy.autocomplete('option');
+        self.$dummy.autocomplete('option', 'minLength', 0);
+        self.$dummy.autocomplete('search', self.$dummy.val());
+        self.$dummy.autocomplete('option', 'minLength', old_options.minLength);
+      } else if ((event.which != KEY.SHIFT) && (event.which != KEY.CTRL) && (event.which != KEY.ALT)) {
+        self.state = self.STATES.UNDEFINED;
+      }
+    },
+    init: function() {
+      var self = this;
+      this.$dummy.autocomplete({
+        source: function(req, rsp) {
+          $.ajax($.extend({}, self.o, {
+            url:      'controller.pl?action=CustomerVendor/ajaj_autocomplete',
+            dataType: "json",
+            type:     'get',
+            data:     self.ajax_data(req.term),
+            success:  function (data){ rsp(data) }
+          }));
+        },
+        select: function(event, ui) {
+          self.set_item(ui.item);
+          if (self.o.action.commit_one) {
+            self.run_action(self.o.action.commit_one);
+          }
+        },
+        search: function(event, ui) {
+          if ((event.which == KEY.SHIFT) || (event.which == KEY.CTRL) || (event.which == KEY.ALT))
+            event.preventDefault();
+        },
+        open: function() {
+          self.autocomplete_open = true;
+        },
+        close: function() {
+          self.autocomplete_open = false;
+        }
+      });
+      this.$dummy.keydown(function(event){ self.handle_keydown(event) });
+      this.$dummy.on('paste', function(){
+        setTimeout(function() {
+          self.handle_changed_text();
+        }, 1);
+      });
+      this.$dummy.blur(function(){
+        window.clearTimeout(self.timer);
+        self.timer = window.setTimeout(function() { self.annotate_state() }, 100);
+      });
+    },
+    run_action: function(code, args) {
+      if (typeof code === 'function')
+        code.apply(this, args)
+      else
+        kivi.run(code, args);
+    },
+    clear: function() {
+      this.set_item({});
+    }
+  };
+  ns.Picker.prototype.STATES = {
+    PICKED:    ns.Picker.prototype.CLASSES.PICKED,
+    UNDEFINED: ns.Picker.prototype.CLASSES.UNDEFINED
+  };
+
+  ns.reinit_widgets = function() {
+    kivi.run_once_for('input.customer_vendor_autocomplete', 'customer_vendor_picker', function(elt) {
+      if (!$(elt).data('customer_vendor_picker'))
+        $(elt).data('customer_vendor_picker', new kivi.CustomerVendor.Picker($(elt)));
+    });
+
+    $('#cv_phone,#shipto_shiptophone,#additional_billing_address_phone,#contact_cp_phone1,#contact_cp_phone2,#contact_cp_mobile1,#contact_cp_mobile2').each(function(idx, elt) {
+      kivi.CustomerVendor.init_dial_action($(elt));
+    });
+  }
+
+  ns.init = function() {
+    ns.reinit_widgets();
+  }
+
+  ns.get_price_report = function(target, source, data) {
+    $.ajax({
+      url:        source,
+      success:    function (rsp) {
+        $(target).html(rsp);
+        $(target).find('a.report-generator-header-link').click(function(event){ ns.price_report_redirect_event(event, target) });
+      },
+    });
+  };
+
+  ns.price_report_redirect_event = function (event, target) {
+    event.preventDefault();
+    ns.get_price_report(target, event.target + '');
+  };
+
+  ns.price_list_init = function () {
+    $("#customer_vendor_tabs").on('tabsbeforeactivate', function(event, ui){
+      if (ui.newPanel.attr('id') == 'price_list') {
+        ns.get_price_report('#price_list', "controller.pl?action=CustomerVendor/ajax_list_prices&id=" + $('#cv_id').val() + "&db=" + $('#db').val() + "&callback=" + $('#callback').val());
+      }
+      return 1;
+    });
+
+    $("#customer_vendor_tabs").on('tabscreate', function(event, ui){
+      if (ui.panel.attr('id') == 'price_list') {
+        ns.get_price_report('#price_list', "controller.pl?action=CustomerVendor/ajax_list_prices&id=" + $('#cv_id').val() + "&db=" + $('#db').val() + "&callback=" + $('#callback').val());
+      }
+      return 1;
+    });
+  }
+
+  $(function(){
+    ns.init();
+    ns.price_list_init();
   });
-}
+});
diff --git a/js/kivi.CustomerVendorTurnover.js b/js/kivi.CustomerVendorTurnover.js
new file mode 100644 (file)
index 0000000..62923bb
--- /dev/null
@@ -0,0 +1,48 @@
+namespace('kivi.CustomerVendorTurnover', function(ns) {
+
+  ns.show_dun_stat = function(period) {
+    if (period === 'y') {
+      var url = 'controller.pl?action=CustomerVendorTurnover/count_open_items_by_year&id=' + $('#cv_id').val();
+      $('#duns').load(url);
+    } else {
+      var url = 'controller.pl?action=CustomerVendorTurnover/count_open_items_by_month&id=' + $('#cv_id').val();
+      $('#duns').load(url);
+    }
+  };
+
+  ns.get_invoices = function() {
+    var url = 'controller.pl?action=CustomerVendorTurnover/get_invoices&id=' + $('#cv_id').val() + '&db=' + $('#db').val();
+    $('#invoices').load(url);
+  };
+
+  ns.get_sales_quotations = function() {
+    var url = 'controller.pl?action=CustomerVendorTurnover/get_orders&id=' + $('#cv_id').val() + '&db=' + $('#db').val() + '&type=quotation';
+    $('#quotations').load(url);
+  };
+
+  ns.get_orders = function() {
+    var url = 'controller.pl?action=CustomerVendorTurnover/get_orders&id=' + $('#cv_id').val() + '&db=' + $('#db').val() + '&type=order';
+    $('#orders').load(url);
+  };
+
+  ns.get_letters = function() {
+    var url = 'controller.pl?action=CustomerVendorTurnover/get_letters&id=' + $('#cv_id').val() + '&db=' + $('#db').val();;
+    $('#letters').load(url);
+  };
+
+  ns.get_mails = function() {
+    var url = 'controller.pl?action=CustomerVendorTurnover/get_mails&id=' + $('#cv_id').val() + '&db=' + $('#db').val();;
+    $('#mails').load(url);
+  };
+
+  ns.show_turnover_stat = function(period) {
+    if (period === 'y') {
+      var url = 'controller.pl?action=CustomerVendorTurnover/turnover_by_year&id=' + $('#cv_id').val() + '&db=' + $('#db').val();
+      $('#turnovers').load(url);
+    } else {
+      var url = 'controller.pl?action=CustomerVendorTurnover/turnover_by_month&id=' + $('#cv_id').val() + '&db=' + $('#db').val();
+      $('#turnovers').load(url);
+    }
+  };
+
+});
diff --git a/js/kivi.DeliveryOrder.js b/js/kivi.DeliveryOrder.js
new file mode 100644 (file)
index 0000000..00a9294
--- /dev/null
@@ -0,0 +1,817 @@
+namespace('kivi.DeliveryOrder', function(ns) {
+  ns.check_cv = function() {
+    if ($('#type').val() == 'sales_delivery_order') {
+      if ($('#order_customer_id').val() === '') {
+        alert(kivi.t8('Please select a customer.'));
+        return false;
+      }
+    } else  {
+      if ($('#order_vendor_id').val() === '') {
+        alert(kivi.t8('Please select a vendor.'));
+        return false;
+      }
+    }
+    return true;
+  };
+
+  ns.check_duplicate_parts = function(question) {
+    var id_arr = $('[name="order.orderitems[].parts_id"]').map(function() { return this.value; }).get();
+
+    var i, obj = {}, pos = [];
+
+    for (i = 0; i < id_arr.length; i++) {
+      var id = id_arr[i];
+      if (obj.hasOwnProperty(id)) {
+        pos.push(i + 1);
+      }
+      obj[id] = 0;
+    }
+
+    if (pos.length > 0) {
+      question = question || kivi.t8("Do you really want to continue?");
+      return confirm(kivi.t8("There are duplicate parts at positions") + "\n"
+                     + pos.join(', ') + "\n"
+                     + question);
+    }
+    return true;
+  };
+
+  ns.check_valid_reqdate = function() {
+    if ($('#order_reqdate_as_date').val() === '') {
+      alert(kivi.t8('Please select a delivery date.'));
+      return false;
+    } else {
+      return true;
+    }
+  };
+
+  ns.save = function(action, warn_on_duplicates, warn_on_reqdate) {
+    if (!ns.check_cv()) return;
+    if (warn_on_duplicates && !ns.check_duplicate_parts()) return;
+    if (warn_on_reqdate    && !ns.check_valid_reqdate())   return;
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'DeliveryOrder/' + action });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.delete_order = function() {
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'DeliveryOrder/delete' });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.show_print_options = function(warn_on_duplicates, warn_on_reqdate) {
+    if (!ns.check_cv()) return;
+    if (warn_on_duplicates && !ns.check_duplicate_parts(kivi.t8("Do you really want to print?"))) return;
+    if (warn_on_reqdate    && !ns.check_valid_reqdate())   return;
+
+    kivi.popup_dialog({
+      id: 'print_options',
+      dialog: {
+        title: kivi.t8('Print options'),
+        width:  800,
+        height: 300
+      }
+    });
+  };
+
+  ns.open_stock_in_out_dialog = function(clicked, in_out) {
+    var $row = $(clicked).parents("tbody").first();
+    var id = $row.find('[name="orderitem_ids[+]"]').val();
+    $row.uniqueId();
+
+    kivi.popup_dialog({
+      id: "stock_in_out_dialog",
+      url: "controller.pl?action=DeliveryOrder/stock_in_out_dialog",
+      data: {
+        id:            $("#id").val(),
+        type:          $("#type").val(),
+        parts_id:      $row.find("[name$=parts_id]").val(),
+        unit:          $row.find("[name$=unit]").val(),
+        qty_as_number: $row.find("[name$=qty_as_number]").val(),
+        stock:         $row.find("[name$=stock_info]").val(),
+        item_id:       id,
+        row:           $row.attr("id"),
+      },
+      dialog: { title: kivi.t8('Transfer stock') }
+    });
+  };
+
+  ns.save_updated_stock = function() {
+    // stock information is saved in DOM as a yaml dump.
+    // we don't want to do this in javascript so we do a tiny roundtrip to the backend
+
+    let data = [];
+    $("#stock-in-out-table tr.listrow").each((i,row) => {
+      let qty = kivi.parse_amount($(row).find(".data-qty").val());
+
+      if (qty === 0) return;
+
+      data.push({
+        qty:                           qty,
+        warehouse_id:                  $(row).find(".data-warehouse-id").val(),
+        bin_id:                        $(row).find(".data-bin-id").val(),
+        chargenumber:                  $(row).find(".data-chargenumber").val(),
+        bestbefore:                    $(row).find(".data-bestbefore").val(),
+        unit:                          $(row).find(".data-unit").val(),
+        delivery_order_items_stock_id: $(row).find(".data-stock-id").val(),
+      });
+    });
+
+    let row = $(".data-row").val();
+
+    $.post("controller.pl",
+      kivi.serialize({
+        action:     "DeliveryOrder/update_stock_information",
+        unit:       $("#" + row).find("[name$=unit]").val(),
+        stock_info: data,
+        row:        row
+      }),
+      (data) => {
+        $("#" + row + " .data-stock-info").val(data.stock_info);
+        $("#" + row + " .data-stock-qty").text(data.stock_qty)
+        $("#stock_in_out_dialog").dialog("close");
+      }
+    );
+  };
+
+  ns.print = function() {
+    $('#print_options').dialog('close');
+
+    var data = $('#order_form').serializeArray();
+    data = data.concat($('#print_options_form').serializeArray());
+    data.push({ name: 'action', value: 'DeliveryOrder/print' });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  var email_dialog;
+
+  ns.setup_send_email_dialog = function() {
+    kivi.SalesPurchase.show_all_print_options_elements();
+    kivi.SalesPurchase.show_print_options_elements([ 'sendmode', 'media', 'copies', 'remove_draft' ], false);
+
+    $('#print_options_form table').first().remove().appendTo('#email_form_print_options');
+
+    var to_focus = $('#email_form_to').val() === '' ? 'to' : 'subject';
+    $('#email_form_' + to_focus).focus();
+  };
+
+  ns.finish_send_email_dialog = function() {
+    kivi.SalesPurchase.show_all_print_options_elements();
+
+    $('#email_form_print_options table').first().remove().prependTo('#print_options_form');
+    return true;
+  };
+
+  ns.show_email_dialog = function(html) {
+    var id            = 'send_email_dialog';
+    var dialog_params = {
+      id:     id,
+      width:  800,
+      height: 600,
+      title:  kivi.t8('Send email'),
+      modal:  true,
+      beforeClose: kivi.DeliveryOrder.finish_send_email_dialog,
+      close: function(event, ui) {
+        email_dialog.remove();
+      }
+    };
+
+    $('#' + id).remove();
+
+    email_dialog = $('<div style="display:none" id="' + id + '"></div>').appendTo('body');
+    email_dialog.html(html);
+    email_dialog.dialog(dialog_params);
+
+    kivi.DeliveryOrder.setup_send_email_dialog();
+
+    $('.cancel').click(ns.close_email_dialog);
+
+    return true;
+  };
+
+  ns.send_email = function() {
+    // push button only once -> slow response from mail server
+    ns.email_dialog_disable_send();
+
+    var data = $('#order_form').serializeArray();
+    data = data.concat($('[name^="email_form."]').serializeArray());
+    data = data.concat($('[name^="print_options."]').serializeArray());
+    data.push({ name: 'action', value: 'DeliveryOrder/send_email' });
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.email_dialog_disable_send = function() {
+    // disable mail send event to prevent
+    // impatient users to send multiple times
+    $('#send_email').prop('disabled', true);
+  };
+
+  ns.close_email_dialog = function() {
+    email_dialog.dialog("close");
+  };
+
+  ns.set_number_in_title = function(elt) {
+    $('#nr_in_title').html($(elt).val());
+  };
+
+  ns.reload_cv_dependent_selections = function() {
+    $('#order_shipto_id').val('');
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'DeliveryOrder/customer_vendor_changed' });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.reformat_number = function(event) {
+    $(event.target).val(kivi.format_amount(kivi.parse_amount($(event.target).val()), -2));
+  };
+
+  ns.reformat_number_as_null_number = function(event) {
+    if ($(event.target).val() === '') {
+      return;
+    }
+    ns.reformat_number(event);
+  };
+
+  ns.update_exchangerate = function(event) {
+    if (!ns.check_cv()) {
+      $('#order_currency_id').val($('#old_currency_id').val());
+      return;
+    }
+
+    var rate_input = $('#order_exchangerate_as_null_number');
+    // unset exchangerate if currency changed
+    if ($('#order_currency_id').val() !== $('#old_currency_id').val()) {
+      rate_input.val('');
+    }
+
+    // only set exchangerate if unset
+    if (rate_input.val() !== '') {
+      return;
+    }
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'DeliveryOrder/update_exchangerate' });
+
+    $.ajax({
+      url: 'controller.pl',
+      data: data,
+      method: 'POST',
+      dataType: 'json',
+      success: function(data){
+        if (!data.is_standard) {
+          $('#currency_name').text(data.currency_name);
+          if (data.exchangerate) {
+            rate_input.val(data.exchangerate);
+          } else {
+            rate_input.val('');
+          }
+          $('#exchangerate_settings').show();
+        } else {
+          rate_input.val('');
+          $('#exchangerate_settings').hide();
+        }
+        if ($('#order_currency_id').val() != $('#old_currency_id').val() ||
+            !data.is_standard && data.exchangerate != $('#old_exchangerate').val()) {
+          kivi.display_flash('warning', kivi.t8('You have changed the currency or exchange rate. Please check prices.'));
+        }
+        $('#old_currency_id').val($('#order_currency_id').val());
+        $('#old_exchangerate').val(data.exchangerate);
+      }
+    });
+  };
+
+  ns.exchangerate_changed = function(event) {
+    if (kivi.parse_amount($('#order_exchangerate_as_null_number').val()) != kivi.parse_amount($('#old_exchangerate').val())) {
+      kivi.display_flash('warning', kivi.t8('You have changed the currency or exchange rate. Please check prices.'));
+      $('#old_exchangerate').val($('#order_exchangerate_as_null_number').val());
+    }
+  };
+
+  ns.unit_change = function(event) {
+    var row           = $(event.target).parents("tbody").first();
+    var item_id_dom   = $(row).find('[name="orderitem_ids[+]"]');
+    var sellprice_dom = $(row).find('[name="order.orderitems[].sellprice_as_number"]');
+    var select_elt    = $(row).find('[name="order.orderitems[].unit"]');
+
+    var oldval = $(select_elt).data('oldval');
+    $(select_elt).data('oldval', $(select_elt).val());
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action',           value: 'DeliveryOrder/unit_changed'     },
+              { name: 'item_id',          value: item_id_dom.val()        },
+              { name: 'old_unit',         value: oldval                   },
+              { name: 'sellprice_dom_id', value: sellprice_dom.attr('id') });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.update_sellprice = function(item_id, price_str) {
+    var row       = $('#item_' + item_id).parents("tbody").first();
+    var price_elt = $(row).find('[name="order.orderitems[].sellprice_as_number"]');
+    var html_elt  = $(row).find('[name="sellprice_text"]');
+    price_elt.val(price_str);
+    html_elt.html(price_str);
+  };
+
+  ns.load_second_row = function(row) {
+    var item_id_dom = $(row).find('[name="orderitem_ids[+]"]');
+    var div_elt     = $(row).find('[name="second_row"]');
+
+    if ($(div_elt).data('loaded') == 1) {
+      return;
+    }
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action',     value: 'DeliveryOrder/load_second_rows' },
+              { name: 'item_ids[]', value: item_id_dom.val()        });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.load_all_second_rows = function() {
+    var rows = $('.row_entry').filter(function(idx, elt) {
+      return $(elt).find('[name="second_row"]').data('loaded') != 1;
+    });
+
+    var item_ids = $.map(rows, function(elt) {
+      var item_id = $(elt).find('[name="orderitem_ids[+]"]').val();
+      return { name: 'item_ids[]', value: item_id };
+    });
+
+    if (item_ids.length == 0) {
+      return;
+    }
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'DeliveryOrder/load_second_rows' });
+    data = data.concat(item_ids);
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.hide_second_row = function(row) {
+    $(row).children().not(':first').hide();
+    $(row).data('expanded', 0);
+    var elt = $(row).find('.expand');
+    elt.attr('src', "image/expand.svg");
+    elt.attr('alt', kivi.t8('Show details'));
+    elt.attr('title', kivi.t8('Show details'));
+  };
+
+  ns.show_second_row = function(row) {
+    $(row).children().not(':first').show();
+    $(row).data('expanded', 1);
+    var elt = $(row).find('.expand');
+    elt.attr('src', "image/collapse.svg");
+    elt.attr('alt', kivi.t8('Hide details'));
+    elt.attr('title', kivi.t8('Hide details'));
+  };
+
+  ns.toggle_second_row = function(row) {
+    if ($(row).data('expanded') == 1) {
+      ns.hide_second_row(row);
+    } else {
+      ns.show_second_row(row);
+    }
+  };
+
+  ns.init_row_handlers = function() {
+    kivi.run_once_for('.reformat_number', 'on_change_reformat', function(elt) {
+      $(elt).change(ns.reformat_number);
+    });
+
+    kivi.run_once_for('.unitselect', 'on_change_unit_with_oldval', function(elt) {
+      $(elt).data('oldval', $(elt).val());
+      $(elt).change(ns.unit_change);
+    });
+
+    kivi.run_once_for('.row_entry', 'on_kbd_click_show_hide', function(elt) {
+      $(elt).keydown(function(event) {
+        var row;
+        if (event.keyCode == 40 && event.shiftKey === true) {
+          // shift arrow down
+          event.preventDefault();
+          row = $(event.target).parents(".row_entry").first();
+          ns.load_second_row(row);
+          ns.show_second_row(row);
+          return false;
+        }
+        if (event.keyCode == 38 && event.shiftKey === true) {
+          // shift arrow up
+          event.preventDefault();
+          row = $(event.target).parents(".row_entry").first();
+          ns.hide_second_row(row);
+          return false;
+        }
+      });
+    });
+
+    kivi.run_once_for('.expand', 'expand_second_row', function(elt) {
+      $(elt).click(function(event) {
+        event.preventDefault();
+        var row = $(event.target).parents(".row_entry").first();
+        ns.load_second_row(row);
+        ns.toggle_second_row(row);
+        return false;
+      })
+    });
+
+  };
+
+  ns.redisplay_line_values = function(is_sales, data) {
+    $('.row_entry').each(function(idx, elt) {
+      $(elt).find('[name="linetotal"]').html(data[idx][0]);
+      if (is_sales && $(elt).find('[name="second_row"]').data('loaded') == 1) {
+        var mt = data[idx][1];
+        var mp = data[idx][2];
+        var h  = '<span';
+        if (mt[0] === '-') h += ' class="plus0"';
+        h += '>' + mt + '&nbsp;&nbsp;' + mp + '%';
+        h += '</span>';
+        $(elt).find('[name="linemargin"]').html(h);
+      }
+    });
+  };
+
+  ns.redisplay_cvpartnumbers = function(data) {
+    $('.row_entry').each(function(idx, elt) {
+      $(elt).find('[name="cvpartnumber"]').html(data[idx][0]);
+    });
+  };
+
+  ns.renumber_positions = function() {
+    $('.row_entry [name="position"]').each(function(idx, elt) {
+      $(elt).html(idx+1);
+    });
+    $('.row_entry').each(function(idx, elt) {
+      $(elt).data("position", idx+1);
+    });
+  };
+
+  ns.reorder_items = function(order_by) {
+    var dir = $('#' + order_by + '_header_id a img').attr("data-sort-dir");
+    $('#row_table_id thead a img').remove();
+
+    var src;
+    if (dir == "1") {
+      dir = "0";
+      src = "image/up.png";
+    } else {
+      dir = "1";
+      src = "image/down.png";
+    }
+
+    $('#' + order_by + '_header_id a').append('<img border=0 data-sort-dir=' + dir + ' src=' + src + ' alt="' + kivi.t8('sort items') + '">');
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action',   value: 'DeliveryOrder/reorder_items' },
+              { name: 'order_by', value: order_by              },
+              { name: 'sort_dir', value: dir                   });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.redisplay_items = function(data) {
+    var old_rows = $('.row_entry').detach();
+    var new_rows = [];
+    $(data).each(function(idx, elt) {
+      new_rows.push(old_rows[elt.old_pos - 1]);
+    });
+    $(new_rows).appendTo($('#row_table_id'));
+    ns.renumber_positions();
+  };
+
+  ns.get_insert_before_item_id = function(wanted_pos) {
+    if (wanted_pos === '') return;
+
+    var insert_before_item_id;
+    // selection by data does not seem to work if data is changed at runtime
+    // var elt = $('.row_entry [data-position="' + wanted_pos + '"]');
+    $('.row_entry').each(function(idx, elt) {
+      if ($(elt).data("position") == wanted_pos) {
+        insert_before_item_id = $(elt).find('[name="orderitem_ids[+]"]').val();
+        return false;
+      }
+    });
+
+    return insert_before_item_id;
+  };
+
+  ns.add_item = function() {
+    if ($('#add_item_parts_id').val() === '') return;
+    if (!ns.check_cv()) return;
+
+    $('#row_table_id thead a img').remove();
+
+    var insert_before_item_id = ns.get_insert_before_item_id($('#add_item_position').val());
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'DeliveryOrder/add_item' },
+              { name: 'insert_before_item_id', value: insert_before_item_id });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.open_multi_items_dialog = function() {
+    if (!ns.check_cv()) return;
+
+    var pp = $("#add_item_parts_id").data("part_picker");
+    pp.o.multiple=1;
+    pp.open_dialog();
+  };
+
+  ns.add_multi_items = function(data) {
+    var insert_before_item_id = ns.get_insert_before_item_id($('#multi_items_position').val());
+    data = data.concat($('#order_form').serializeArray());
+    data.push({ name: 'action', value: 'DeliveryOrder/add_multi_items' },
+              { name: 'insert_before_item_id', value: insert_before_item_id });
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.delete_order_item_row = function(clicked) {
+    var row = $(clicked).parents("tbody").first();
+    $(row).remove();
+
+    ns.renumber_positions();
+  };
+
+  ns.row_table_scroll_down = function() {
+    $('#row_table_scroll_id').scrollTop($('#row_table_scroll_id')[0].scrollHeight);
+  };
+
+  ns.show_longdescription_dialog = function(clicked) {
+    var row                 = $(clicked).parents("tbody").first();
+    var position            = $(row).find('[name="position"]').html();
+    var partnumber          = $(row).find('[name="partnumber"]').html();
+    var description_elt     = $(row).find('[name="order.orderitems[].description"]');
+    var longdescription_elt = $(row).find('[name="order.orderitems[].longdescription"]');
+
+    var params = {
+      runningnumber:           position,
+      partnumber:              partnumber,
+      description:             description_elt.val(),
+      default_longdescription: longdescription_elt.val(),
+      set_function:            function(val) {
+        longdescription_elt.val(val);
+      }
+    };
+
+    kivi.SalesPurchase.edit_longdescription_with_params(params);
+  };
+
+  ns.price_chooser_item_row = function(clicked) {
+    if (!ns.check_cv()) return;
+    var row         = $(clicked).parents("tbody").first();
+    var item_id_dom = $(row).find('[name="orderitem_ids[+]"]');
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action',  value: 'DeliveryOrder/price_popup' },
+              { name: 'item_id', value: item_id_dom.val()   });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.show_vc_details_dialog = function() {
+    if (!ns.check_cv()) return;
+    var vc;
+    var vc_id;
+    var title;
+    if ($('#order_customer_id').val()) {
+      vc    = 'customer';
+      vc_id = $('#order_customer_id').val();
+      title = kivi.t8('Customer details');
+    } else {
+      vc    = 'vendor';
+      vc_id = $('#order_vendor_id').val();
+      title = kivi.t8('Vendor details');
+    }
+
+    kivi.popup_dialog({
+      url:    'controller.pl',
+      data:   { action: 'DeliveryOrder/show_customer_vendor_details_dialog',
+                type  : $('#type').val(),
+                vc    : vc,
+                vc_id : vc_id
+              },
+      id:     'jq_customer_vendor_details_dialog',
+      dialog: {
+        title:  title,
+        width:  800,
+        height: 650
+      }
+    });
+    return true;
+  };
+
+  ns.update_row_from_master_data = function(clicked) {
+    var row = $(clicked).parents("tbody").first();
+    var item_id_dom = $(row).find('[name="orderitem_ids[+]"]');
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'DeliveryOrder/update_row_from_master_data' });
+    data.push({ name: 'item_ids[]', value: item_id_dom.val() });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.update_all_rows_from_master_data = function() {
+    var item_ids = $.map($('.row_entry'), function(elt) {
+      var item_id = $(elt).find('[name="orderitem_ids[+]"]').val();
+      return { name: 'item_ids[]', value: item_id };
+    });
+
+    if (item_ids.length == 0) {
+      return;
+    }
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'DeliveryOrder/update_row_from_master_data' });
+    data = data.concat(item_ids);
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.show_calculate_qty_dialog = function(clicked) {
+    var row        = $(clicked).parents("tbody").first();
+    var input_id   = $(row).find('[name="order.orderitems[].qty_as_number"]').attr('id');
+    var formula_id = $(row).find('[name="formula[+]"]').attr('id');
+
+    calculate_qty_selection_dialog("", input_id, "", formula_id);
+    return true;
+  };
+
+  ns.edit_custom_shipto = function() {
+    if (!ns.check_cv()) return;
+
+    kivi.SalesPurchase.edit_custom_shipto();
+  };
+
+  ns.purchase_order_check_for_direct_delivery = function() {
+    if ($('#type').val() != 'sales_order') {
+      kivi.submit_form_with_action($('#order_form'), 'DeliveryOrder/purchase_order');
+    }
+
+    var empty = true;
+    var shipto;
+    if ($('#order_shipto_id').val() !== '') {
+      empty = false;
+      shipto = $('#order_shipto_id option:selected').text();
+    } else {
+      $('#shipto_inputs [id^="shipto"]').each(function(idx, elt) {
+        if (!empty)                                     return true;
+        if (/^shipto_to_copy/.test($(elt).prop('id')))  return true;
+        if (/^shiptocp_gender/.test($(elt).prop('id'))) return true;
+        if (/^shiptocvar_/.test($(elt).prop('id')))     return true;
+        if ($(elt).val() !== '') {
+          empty = false;
+          return false;
+        }
+      });
+      var shipto_elements = [];
+      $([$('#shiptoname').val(), $('#shiptostreet').val(), $('#shiptozipcode').val(), $('#shiptocity').val()]).each(function(idx, elt) {
+        if (elt !== '') shipto_elements.push(elt);
+      });
+      shipto = shipto_elements.join('; ');
+    }
+
+    var use_it = false;
+    if (!empty) {
+      ns.direct_delivery_dialog(shipto);
+    } else {
+      kivi.submit_form_with_action($('#order_form'), 'DeliveryOrder/purchase_order');
+    }
+  };
+
+  ns.direct_delivery_callback = function(accepted) {
+    $('#direct-delivery-dialog').dialog('close');
+
+    if (accepted) {
+      $('<input type="hidden" name="use_shipto">').appendTo('#order_form').val('1');
+    }
+
+    kivi.submit_form_with_action($('#order_form'), 'DeliveryOrder/purchase_order');
+  };
+
+  ns.direct_delivery_dialog = function(shipto) {
+    $('#direct-delivery-dialog').remove();
+
+    var text1 = kivi.t8('You have entered or selected the following shipping address for this customer:');
+    var text2 = kivi.t8('Do you want to carry this shipping address over to the new purchase order so that the vendor can deliver the goods directly to your customer?');
+    var html  = '<div id="direct-delivery-dialog"><p>' + text1 + '</p><p>' + shipto + '</p><p>' + text2 + '</p>';
+    html      = html + '<hr><p>';
+    html      = html + '<input type="button" value="' + kivi.t8('Yes') + '" size="30" onclick="kivi.DeliveryOrder.direct_delivery_callback(true)">';
+    html      = html + '&nbsp;';
+    html      = html + '<input type="button" value="' + kivi.t8('No')  + '" size="30" onclick="kivi.DeliveryOrder.direct_delivery_callback(false)">';
+    html      = html + '</p></div>';
+    $(html).hide().appendTo('#order_form');
+
+    kivi.popup_dialog({id: 'direct-delivery-dialog',
+                       dialog: {title:  kivi.t8('Carry over shipping address'),
+                                height: 300,
+                                width:  500 }});
+  };
+
+  ns.follow_up_window = function() {
+    var id   = $('#id').val();
+    var type = $('#type').val();
+
+    var number_info = '';
+    if ($('#type').val() == 'sales_order' || $('#type').val() == 'purchase_order') {
+      number_info = $('#order_ordnumber').val();
+    } else if ($('#type').val() == 'sales_quotation' || $('#type').val() == 'request_quotation') {
+      number_info = $('#order_quonumber').val();
+    }
+
+    var name_info = '';
+    if ($('#type').val() == 'sales_order' || $('#type').val() == 'sales_quotation') {
+      name_info = $('#order_customer_id_name').val();
+    } else if ($('#type').val() == 'purchase_order' || $('#type').val() == 'request_quotation') {
+      name_info = $('#order_vendor_id_name').val();
+    }
+
+    var info = '';
+    if (number_info !== '') { info += ' (' + number_info + ')' }
+    if (name_info   !== '') { info += ' (' + name_info + ')' }
+
+    if (!$('#follow_up_rowcount').lenght) {
+      $('<input type="hidden" name="follow_up_rowcount"        id="follow_up_rowcount">').appendTo('#order_form');
+      $('<input type="hidden" name="follow_up_trans_id_1"      id="follow_up_trans_id_1">').appendTo('#order_form');
+      $('<input type="hidden" name="follow_up_trans_type_1"    id="follow_up_trans_type_1">').appendTo('#order_form');
+      $('<input type="hidden" name="follow_up_trans_info_1"    id="follow_up_trans_info_1">').appendTo('#order_form');
+      $('<input type="hidden" name="follow_up_trans_subject_1" id="follow_up_trans_subject_1">').appendTo('#order_form');
+    }
+    $('#follow_up_rowcount').val(1);
+    $('#follow_up_trans_id_1').val(id);
+    $('#follow_up_trans_type_1').val(type);
+    $('#follow_up_trans_info_1').val(info);
+    $('#follow_up_trans_subject_1').val($('#order_transaction_description').val());
+
+    follow_up_window();
+  };
+
+  ns.create_part = function() {
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'DeliveryOrder/create_part' });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+});
+
+$(function() {
+  $('#order_customer_id').change(kivi.DeliveryOrder.reload_cv_dependent_selections);
+  $('#order_vendor_id').change(kivi.DeliveryOrder.reload_cv_dependent_selections);
+
+  $('#order_currency_id').change(kivi.DeliveryOrder.update_exchangerate);
+  $('#order_transdate_as_date').change(kivi.DeliveryOrder.update_exchangerate);
+  $('#order_exchangerate_as_null_number').change(kivi.DeliveryOrder.exchangerate_changed);
+
+  $('#add_item_parts_id').on('set_item:PartPicker', function(e,o) { $('#add_item_description').val(o.description) });
+  $('#add_item_parts_id').on('set_item:PartPicker', function(e,o) { $('#add_item_unit').val(o.unit) });
+
+  $('.add_item_input').keydown(function(event) {
+    if (event.keyCode == 13) {
+      event.preventDefault();
+      kivi.DeliveryOrder.add_item();
+      return false;
+    }
+  });
+
+  kivi.DeliveryOrder.init_row_handlers();
+
+  $('#row_table_id').on('sortstop', function(event, ui) {
+    $('#row_table_id thead a img').remove();
+    kivi.DeliveryOrder.renumber_positions();
+  });
+
+  $('#expand_all').on('click', function(event) {
+    event.preventDefault();
+    if ($('#expand_all').data('expanded') == 1) {
+      $('#expand_all').data('expanded', 0);
+      $('#expand_all').attr('src', 'image/expand.svg');
+      $('#expand_all').attr('alt', kivi.t8('Show all details'));
+      $('#expand_all').attr('title', kivi.t8('Show all details'));
+      $('.row_entry').each(function(idx, elt) {
+        kivi.DeliveryOrder.hide_second_row(elt);
+      });
+    } else {
+      $('#expand_all').data('expanded', 1);
+      $('#expand_all').attr('src', "image/collapse.svg");
+      $('#expand_all').attr('alt', kivi.t8('Hide all details'));
+      $('#expand_all').attr('title', kivi.t8('Hide all details'));
+      kivi.DeliveryOrder.load_all_second_rows();
+      $('.row_entry').each(function(idx, elt) {
+        kivi.DeliveryOrder.show_second_row(elt);
+      });
+    }
+    return false;
+  });
+
+  $('.reformat_number_as_null_number').change(kivi.DeliveryOrder.reformat_number_as_null_number);
+
+});
diff --git a/js/kivi.Draft.js b/js/kivi.Draft.js
new file mode 100644 (file)
index 0000000..c87a21f
--- /dev/null
@@ -0,0 +1,34 @@
+namespace('kivi.Draft', function(ns) {
+  'use strict';
+
+  ns.popup = function(module, submodule, id, description) {
+    $.get('controller.pl', {
+      action: 'Draft/draft_dialog.js',
+      module: module,
+      submodule: submodule,
+      id: id,
+      description: description
+    }, kivi.eval_json_result)
+  }
+
+  ns.save = function(module, submodule) {
+    $.post('controller.pl', {
+      action: 'Draft/save.js',
+      module: module,
+      submodule: submodule,
+      form: $('form').serializeArray(),
+      id: $('#new_draft_id').val(),
+      description: $('#new_draft_description').val()
+    }, kivi.eval_json_result)
+  }
+
+  ns.delete = function(id) {
+    if (!confirm(kivi.t8('Do you really want to delete this draft?'))) return;
+
+    $.post('controller.pl', {
+      action: 'Draft/delete.js',
+      id: id
+    }, kivi.eval_json_result)
+
+  }
+});
diff --git a/js/kivi.Dunning.js b/js/kivi.Dunning.js
new file mode 100644 (file)
index 0000000..6c05df9
--- /dev/null
@@ -0,0 +1,12 @@
+namespace('kivi.Dunning', function(ns) {
+  "use strict";
+
+  ns.enable_disable_language_id = function() {
+    $('select[name="language_id"]').prop('disabled', !$('#force_lang').prop('checked'));
+  };
+
+  $(function() {
+    $('#force_lang').click(kivi.Dunning.enable_disable_language_id);
+    kivi.Dunning.enable_disable_language_id();
+  });
+});
diff --git a/js/kivi.File.js b/js/kivi.File.js
new file mode 100644 (file)
index 0000000..55aa4ea
--- /dev/null
@@ -0,0 +1,424 @@
+namespace('kivi.File', function(ns) {
+  ns.list_div_id = undefined;
+
+  ns.rename = function(id,type,file_type,checkbox_class,is_global) {
+    var $dlg       = $('#rename_dialog_'+file_type);
+    var parent_id  = $dlg.parent("div.ui-tabs-panel").attr('id');
+    var checkboxes = $('.'+checkbox_class).filter(function () { return  $(this).prop('checked'); });
+
+    if (checkboxes.size() === 0) {
+      alert(kivi.t8("No file selected, please set one checkbox!"));
+      return false;
+    }
+    if (checkboxes.size() > 1) {
+      alert(kivi.t8("More than one file selected, please set only one checkbox!"));
+      return false;
+    }
+    var file_id = checkboxes[0].value;
+    $('#newfilename_id_'+file_type).val($('#filename_'+file_id).text());
+    $('#next_ids_id_'+file_type).val('');
+    $('#is_global_id_'+file_type).val(is_global);
+    $('#rename_id_id_'+file_type).val(file_id);
+    $('#sessionfile_id_'+file_type).val('');
+    $('#rename_extra_text_'+file_type).html('');
+    kivi.popup_dialog({
+                      id:     'rename_dialog_'+file_type,
+                      dialog: { title: kivi.t8("Rename attachment")
+                               , width:  400
+                               , height: 200
+                               , modal:  true
+                               , close: function() {
+                                 $dlg.remove().appendTo('#' + parent_id);
+                               }
+                              }
+    });
+    return true;
+  }
+
+  ns.renameclose = function(file_type) {
+    $("#rename_dialog_"+file_type).dialog('close');
+    return false;
+  }
+
+  ns.renameaction = function(file_type) {
+    $("#rename_dialog_"+file_type).dialog('close');
+    var data = {
+      action:          'File/ajax_rename',
+      id:              $('#rename_id_id_'+file_type).val(),
+      to:              $('#newfilename_id_'+file_type).val(),
+      next_ids:        $('#next_ids_id_'+file_type).val(),
+      is_global:       $('#is_global_id_'+file_type).val(),
+      sessionfile:     $('#sessionfile_id_'+file_type).val(),
+    };
+    $.post("controller.pl", data, kivi.eval_json_result);
+    return true;
+  }
+
+  ns.askForRename = function(file_id,file_type,file_name,sessionfile,next_ids,is_global) {
+    $('#newfilename_id_'+file_type).val(file_name);
+    $('#rename_id_id_'+file_type).val(file_id);
+    $('#is_global_id_'+file_type).val(is_global);
+    $('#next_ids_id_'+file_type).val(next_ids);
+    $('#sessionfile_id_'+file_type).val(sessionfile);
+    $('#rename_extra_text_'+file_type).html(kivi.t8("The uploaded filename still exists.<br>If you not modify the name this is a new version of the file"));
+    var $dlg       = $('#rename_dialog_'+file_type);
+    var parent_id  = $dlg.parent("div.ui-tabs-panel").attr('id');
+    kivi.popup_dialog(
+      {
+        id:     'rename_dialog_'+file_type,
+        dialog: { title: kivi.t8("Rename attachment")
+                  , width:  400
+                  , height: 200
+                  , modal:  true
+                  , close: function() {
+                    $dlg.remove().appendTo('#' + parent_id);
+                  } }
+      }
+    );
+  }
+
+  ns.upload = function(id,type,filetype,upload_title,gl) {
+    $('#upload_status_dialog').remove();
+
+    kivi.popup_dialog({ url:     'controller.pl',
+                        data:    { action: 'File/ajax_upload',
+                                   file_type:   filetype,
+                                   object_type: type,
+                                   object_id:   id,
+                                   is_global:   gl
+                                 },
+                        id:     'files_upload',
+                        dialog: { title: upload_title, width: 650, height: 240 } });
+    return true;
+  }
+
+  ns.reset_upload_form = function() {
+      $('#attachment_updfile').val('');
+      $("#upload_result").html('');
+      ns.allow_upload_submit();
+  }
+
+  ns.allow_upload_submit = function() {
+      const disable = $('#upload_files').val() === '';
+      $('#upload_selected_button').prop('disabled', disable)
+                                  .toggleClass('disabled', disable);
+  }
+
+  ns.upload_status_dialog = function() {
+    $('#files_upload').remove();
+    $('#upload_status_dialog').remove();
+
+    var html  = '<div id="upload_status_dialog"><p><div id="upload_result"></div></p>';
+    html      = html + '<p><input type="button" value="' + kivi.t8('close') + '" size="30" onclick="$(\'#upload_status_dialog\').dialog(\'close\');">';
+    html      = html + '</p></div>';
+    $(html).hide().appendTo('#' + ns.list_div_id);
+
+    kivi.popup_dialog({id: 'upload_status_dialog',
+                       dialog: {title:  kivi.t8('Upload Status'),
+                                height: 200,
+                                width:  650 }});
+  };
+
+  ns.upload_selected_files = function(id,type,filetype,maxsize,is_global) {
+      var myform = document.getElementById("upload_form");
+      var myfiles = document.getElementById("upload_files").files;
+
+      ns.upload_files(id, type, filetype, maxsize,is_global, myfiles, myform);
+  }
+
+  ns.upload_files = function(id, type, filetype, maxsize, is_global, myfiles, myform) {
+      var filesize  = 0;
+      for ( i=0; i < myfiles.length; i++ ) {
+          var fname ='';
+          try {
+              filesize  += myfiles[i].size;
+              fname = encodeURIComponent(myfiles[i].name);
+          }
+          catch(err) {
+              fname ='';
+              try {
+                  fname = myfiles[i].name;
+              }
+              catch(err2) { fname ='';}
+              $("#upload_result").html(kivi.t8("filename has not uploadable characters ")+fname);
+              return;
+          }
+      }
+      if ( filesize > maxsize ) {
+          $("#upload_result").html(kivi.t8("filesize too big: ")+
+                                   filesize+ kivi.t8(" bytes, max=") + maxsize );
+          return;
+      }
+
+      var fd = new FormData(myform);
+      if (!myform) {
+        $(myfiles).each(function(idx, elt) {
+          fd.append('uploadfiles[+]', elt);
+        });
+      }
+      fd.append('action',      'File/ajax_files_uploaded');
+      fd.append('json',        1);
+      fd.append('object_type', type);
+      fd.append('object_id',   id);
+      fd.append('file_type',   filetype);
+      fd.append('is_global',   is_global);
+
+      var oReq = new XMLHttpRequest();
+      oReq.onload            = ns.attSuccess;
+      oReq.upload.onprogress = ns.attProgress;
+      oReq.upload.onerror    = ns.attFailed;
+      oReq.upload.onabort    = ns.attCanceled;
+      oReq.open("post", 'controller.pl', true);
+      $("#upload_result").html(kivi.t8("start upload"));
+      oReq.send(fd);
+  }
+
+  ns.attProgress = function(oEvent) {
+      if (oEvent.lengthComputable) {
+          var percentComplete = (oEvent.loaded / oEvent.total) * 100;
+          $("#upload_result").html(percentComplete+" % "+ kivi.t8("uploaded"));
+      }
+  }
+
+  ns.attFailed = function(evt) {
+      $('#files_upload').dialog('close');
+      $("#upload_result").html(kivi.t8("An error occurred while transferring the file."));
+  }
+
+  ns.attCanceled = function(evt) {
+      $('#files_upload').dialog('close');
+      $("#upload_result").html(kivi.t8("The transfer has been canceled by the user."));
+  }
+
+  ns.attSuccess = function() {
+      $('#upload_status_dialog').dialog('close');
+      $('#files_upload').dialog('close');
+      kivi.eval_json_result(jQuery.parseJSON(this.response));
+  }
+
+  ns.delete = function(id,type,file_type,checkbox_class,is_global) {
+    var checkboxes = $('.'+checkbox_class).filter(function () { return  $(this).prop('checked'); });
+
+    if ((checkboxes.size() === 0) ||
+        !confirm(kivi.t8('Do you really want to delete the selected documents?')))
+      return false;
+    var data = {
+      action     :  'File/ajax_delete',
+      object_id  :  id,
+      object_type:  type,
+      file_type  :  file_type,
+      ids        :  checkbox_class,
+      is_global  :  is_global,
+    };
+    $.post("controller.pl?" + checkboxes.serialize(), data, kivi.eval_json_result);
+    return false;
+  }
+
+  ns.delete_file = function(id,controller_action) {
+    $.post('controller.pl', { action: controller_action, id: id }, function(data) {
+      kivi.eval_json_result(data);
+    });
+  };
+
+  ns.unimport = function(id,type,file_type,checkbox_class) {
+    var checkboxes = $('.'+checkbox_class).filter(function () { return  $(this).prop('checked'); });
+
+    if ((checkboxes.size() === 0) ||
+        !confirm(kivi.t8('Do you really want to unimport the selected documents?')))
+      return false;
+    var data = {
+      action     :  'File/ajax_unimport',
+      object_id  :  id,
+      object_type:  type,
+      file_type  :  file_type,
+      ids        :  checkbox_class,
+    };
+    $.post("controller.pl?" + checkboxes.serialize(), data, kivi.eval_json_result);
+    return false;
+  }
+
+  ns.update = function(id,type,file_type,is_global) {
+    var data = {
+      action:       'File/list',
+      json:         1,
+      object_type:  type,
+      object_id:    id,
+      file_type:    file_type,
+      is_global:    is_global
+    };
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+    return false;
+  }
+
+  ns.import = function (id,type,file_type,fromwhere,frompath) {
+    kivi.popup_dialog({ url:     'controller.pl',
+                        data:    { action      : 'File/ajax_importdialog',
+                                   object_type : type,
+                                   source      : fromwhere,
+                                   path        : frompath,
+                                   file_type   : file_type,
+                                   object_id   : id
+                                 },
+                        id:     'import_dialog',
+                        dialog: { title: kivi.t8('Import documents from #1',[fromwhere]), width: 420, height: 540 }
+                      });
+    return true;
+  }
+
+  ns.importclose = function() {
+    $("#import_dialog").dialog('close');
+    return false;
+  }
+
+  ns.importaction = function(id,type,file_type,fromwhere,frompath,checkbox_class) {
+    var checkboxes = $('.'+checkbox_class).filter(function () { return  $(this).prop('checked'); });
+
+    $("#import_dialog").dialog('close');
+    if (checkboxes.size() === 0) {
+      return false;
+    }
+    var data = {
+        action     : 'File/ajax_import',
+        object_id  : id,
+        object_type: type,
+        file_type  : file_type,
+        source     : fromwhere,
+        path       : frompath,
+        ids        : checkbox_class
+    };
+    $.post("controller.pl?" + checkboxes.serialize(), data, kivi.eval_json_result);
+    return true;
+  }
+
+  ns.downloadOrderitemsFiles = function(type,id) {
+    var data = {
+      action:       'DownloadZip/download_orderitems_files',
+      object_type:  type,
+      object_id:    id,
+      element_type: 'part',
+      zipname:      'Order_Files_'+id,
+    };
+    $.download("controller.pl", data);
+    return false;
+  }
+
+  ns.add_enlarged_thumbnail = function(e) {
+    var file_id        = $(e.target).data('file-id');
+    var file_version   = $(e.target).data('file-version');
+    var overlay_img_id = 'enlarged_thumb_' + file_id;
+    if (file_version) { overlay_img_id = overlay_img_id + '_' + file_version };
+    var overlay_img    = $('#' + overlay_img_id);
+
+    if (overlay_img.data('is-overlay-shown') == 1) return;
+
+    $('.thumbnail').off('mouseover');
+    overlay_img.data('is-overlay-shown', 1);
+    overlay_img.show();
+
+    if (overlay_img.data('is-overlay-loaded') == 1) return;
+
+    var data = {
+      action:         'File/ajax_get_thumbnail',
+      file_id:        file_id,
+      file_version:   file_version,
+      size:           512
+    };
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.remove_enlarged_thumbnail = function(e) {
+    $(e.target).hide();
+    $(e.target).data('is-overlay-shown', 0);
+    $('.thumbnail').on('mouseover', ns.add_enlarged_thumbnail);
+  };
+
+  ns.download = function(e) {
+    var file_id        = $(e.target).data('file-id');
+    var file_version   = $(e.target).data('file-version');
+
+    var data = {
+      action:  'File/download',
+      id:      file_id,
+      version: file_version,
+    };
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+
+  };
+
+  ns.init = function() {
+    // Preventing page from redirecting
+    $("#" + ns.list_div_id).on("dragover", function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+    });
+
+    $("#" + ns.list_div_id).on("drop", function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+    });
+
+    // Drag enter
+    $('.upload_drop_zone').on('dragenter', function (e) {
+      e.stopPropagation();
+      e.preventDefault();
+    });
+
+    // Drag over
+    $('.upload_drop_zone').on('dragover', function (e) {
+      e.stopPropagation();
+      e.preventDefault();
+    });
+
+    // Drop
+    $('.upload_drop_zone').on('drop', function (e) {
+      e.stopPropagation();
+      e.preventDefault();
+
+      ns.upload_status_dialog();
+
+      var object_type = $(e.target).data('object-type');
+      var object_id   = $(e.target).data('object-id');
+      var file_type   = $(e.target).data('file-type');
+      var is_global   = $(e.target).data('is-global');
+      var maxsize     = $(e.target).data('maxsize');
+      var files       = e.originalEvent.dataTransfer.files;
+      ns.upload_files(object_id, object_type, file_type, maxsize, is_global, files);
+    });
+
+    $('.thumbnail').on('mouseover', ns.add_enlarged_thumbnail);
+    $('.overlay_img').on('mouseout', ns.remove_enlarged_thumbnail);
+    $('.overlay_div img').on('click', ns.download);
+  };
+
+  ns.doc_tab_init = function(tabs_id, doc_tab_id, id, object_type) {
+    var url = 'controller.pl?action=File/list&file_type=document&object_type=' + object_type  + '&object_id=' + $('#id').val();
+
+    $('#' + tabs_id).on('tabsbeforeactivate', function(e, ui) {
+      if (ui.newPanel.attr('id') !== doc_tab_id) return;
+      $('#' + doc_tab_id).html(kivi.t8('Loading...'));
+      $('#' + doc_tab_id).load(url);
+    });
+
+    $('#' + tabs_id).on('tabscreate', function(e, ui) {
+      if (ui.panel.attr('id') !== doc_tab_id) return;
+      $('#' + doc_tab_id).html(kivi.t8('Loading...'));
+      $('#' + doc_tab_id).load(url);
+    });
+  };
+
+  ns.toggle_versions = function(file_id) {
+    if ($('#version_toggle_' + file_id).data('versions_expanded')) {
+      $('.version_row_'    + file_id).hide();
+      $('#version_toggle_' + file_id).data('versions_expanded', 0);
+      $('#version_toggle_' + file_id).html("⏷ ");
+    } else {
+      $('.version_row_'    + file_id).show();
+      $('#version_toggle_' + file_id).data('versions_expanded', 1);
+      $('#version_toggle_' + file_id).html("⏶ ");
+    }
+  };
+
+});
diff --git a/js/kivi.FileDB.js b/js/kivi.FileDB.js
new file mode 100644 (file)
index 0000000..7ee0701
--- /dev/null
@@ -0,0 +1,127 @@
+namespace("kivi.FileDB", function(ns) {
+  "use strict";
+
+  const database = 'kivi';
+  const store    = 'files';
+  const db_version = 1;
+
+  // IndexedDB
+  const indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.OIndexedDB || window.msIndexedDB;
+
+  // Create/open database
+  let db;
+  let request = indexedDB.open(database, db_version);
+  request.onupgradeneeded = (event) => {
+    ns.create_image_store(event.target.result);
+  };
+  request.onerror = ns.onerror;
+  request.aftersuccess = [];
+  request.onsuccess = () => {
+    db = request.result;
+
+    db.onerror = (event) => {
+      console.error("Error creating/accessing IndexedDB database");
+      console.error(event);
+    };
+
+    // Interim solution for Google Chrome to create an objectStore. Will be deprecated
+    if (db.setVersion) {
+      if (db.version != db_version) {
+        let setVersion = db.setVersion(db_version);
+        setVersion.onsuccess = () =>  {
+          ns.create_image_store(db);
+        };
+      }
+    }
+
+    request.aftersuccess.forEach(f => f());
+  };
+
+  ns.create_image_store = function (db) {
+    db.createObjectStore(store, { autoIncrement : true });
+  };
+
+  ns.store_image = function (blob, filename, success) {
+    ns.open_rw_store((store) => {
+      let put_request = store.add(blob, filename);
+
+      put_request.onsuccess = success;
+      put_request.on_error = ns.onerror;
+    });
+  };
+
+  ns.retrieve_image = function(key, success) {
+    ns.open_ro_store((store) => {
+      let get_request = store.get(key);
+
+      get_request.onsuccess = success;
+      get_request.onerror = request.onerror;
+    });
+  };
+
+  ns.retrieve_all = function(success) {
+    ns.open_ro_store((store) => {
+      let request = store.getAll();
+      request.onsuccess = (event) => { success(event.target.result); };
+      request.onerror = ns.error;
+    });
+  };
+
+  ns.retrieve_all_keys = function(success) {
+    ns.open_ro_store((store) => {
+      let request = store.getAllKeys();
+      request.onsuccess = (event) => { success(event.target.result); };
+      request.onerror = ns.error;
+    });
+  };
+
+  ns.delete_all = function(success) {
+    ns.open_rw_store((store) => {
+      let request = store.clear();
+      request.onsuccess = success;
+      request.onerror = ns.error;
+    });
+  };
+
+  ns.delete_key = function(key, success) {
+    ns.open_rw_store((store) => {
+      let request = store.delete(key);
+      request.onsuccess = (event) => { if (success) success(event.target.result); };
+      request.onerror = ns.error;
+    });
+  };
+
+  ns.open_rw_store = function(callback) {
+    if (db && db_version == db.version) {
+      callback(ns.open_store("readwrite"));
+    } else {
+      request.aftersuccess.push(() => callback(ns.open_store("readwrite")));
+    }
+  };
+
+  ns.open_ro_store = function(callback) {
+    if (db && db_version == db.version) {
+      callback(ns.open_store("readonly"));
+    } else {
+      request.aftersuccess.push(() => callback(ns.open_store("readonly")));
+    }
+  };
+
+  ns.open_store = function(mode = "readonly") {
+    return db.transaction([store], mode).objectStore(store);
+  };
+
+  ns.onerror = (event) => {
+    console.error("Error creating/accessing IndexedDB database");
+    console.error(event.errorState);
+  };
+
+  ns.with_db = function(success) {
+    if (db && db_version == db.version) {
+      success();
+    } else {
+      // assume the page load db init isn't done yet and push it onto the success
+      request.aftersuccess.push(success);
+    }
+  };
+});
diff --git a/js/kivi.GL.js b/js/kivi.GL.js
new file mode 100644 (file)
index 0000000..dc17ff3
--- /dev/null
@@ -0,0 +1,35 @@
+namespace('kivi.GL', function(ns) {
+  "use strict";
+
+  this.show_chart_balance = function(obj) {
+    var row = $(obj).attr('name').replace(/.*_/, '');
+
+    $.ajax({
+      url: 'gl.pl?action=get_chart_balance',
+      data: { accno_id:  $(obj).val() },
+      dataType: 'html',
+      success: function (new_html) {
+        $('#chart_balance_' + row).html(new_html);
+      }
+    });
+  };
+
+  this.update_taxes = function(obj) {
+    var row = $(obj).attr('name').replace(/.*_/, '');
+
+    $.ajax({
+      url: 'gl.pl?action=get_tax_dropdown',
+      data: { accno_id:     $(obj).val(),
+              transdate:    $('#transdate').val(),
+              deliverydate: $('#deliverydate').val() },
+      dataType: 'html',
+      success: function (new_html) {
+        $("#taxchart_" + row).html(new_html);
+      }
+    });
+  };
+});
+
+$(function() {
+  kivi.File.doc_tab_init('gl_tabs', 'ui-tabs-docs', $('#id').val(), 'gl_transaction');
+});
diff --git a/js/kivi.GoBD.js b/js/kivi.GoBD.js
new file mode 100644 (file)
index 0000000..9e8c3ec
--- /dev/null
@@ -0,0 +1,23 @@
+namespace('kivi.GoBD', function(ns) {
+  ns.grey_invalid_options = function(el){
+    console.log(el);
+    if ($(el).prop('checked')) {
+      $(el).closest('tr').find('input.datepicker').prop('disabled', false).datepicker('enable');
+      $(el).closest('tr').find('select').prop('disabled', 0);
+    } else {
+      $(el).closest('tr').find('input.datepicker').prop('disabled', true).datepicker('disable');
+      $(el).closest('tr').find('select').prop('disabled', 1);
+    }
+  }
+
+  ns.update_all_radio = function () {
+    $('input[type=radio]').each(function(i,e) {ns.grey_invalid_options (e) });
+  }
+
+  ns.setup = function() {
+    ns.update_all_radio();
+    $('input[type=radio]').change(ns.update_all_radio);
+  }
+});
+
+$(kivi.GoBD.setup);
diff --git a/js/kivi.ImageUpload.js b/js/kivi.ImageUpload.js
new file mode 100644 (file)
index 0000000..24bd262
--- /dev/null
@@ -0,0 +1,178 @@
+namespace("kivi.ImageUpload", function(ns) {
+  "use strict";
+
+  const MAXSIZE = 15*1024*1024; // 5MB size limit
+  const M = kivi.Materialize;
+
+  let num_images = 0;
+  ns.upload_in_progress = undefined;
+
+  ns.add_files = function(target) {
+    let files = [];
+    for (var i = 0; i < target.files.length; i++) {
+      files.push(target.files.item(i));
+    }
+
+    kivi.FileDB.store_image(files[0], files[0].name, () => {
+      ns.reload_images();
+      target.value = null;
+    });
+  };
+
+  ns.reload_images = function() {
+    kivi.FileDB.retrieve_all((data) => {
+      $('#stored-images').empty();
+      num_images = data.length;
+
+      data.forEach(ns.create_thumb_row);
+      ns.set_image_button_enabled();
+    });
+  };
+
+  ns.create_thumb_row = function(file)  {
+    let URL = window.URL || window.webkitURL;
+    let file_url = URL.createObjectURL(file);
+
+    let $row = $("<div>").addClass("row image-upload-row");
+    let $button = $("<a>")
+      .addClass("btn-floating btn-large waves-effect waves-light red")
+      .click((event) => ns.remove_image(event, file.name))
+      .append($("<i>delete</i>").addClass("material-icons"));
+    $row.append($("<div>").addClass("col s3").append($button));
+
+    let $image = $('<img>').attr("src", file_url).addClass("materialboxed responsive-img");
+    $row.append($("<div>").addClass("col s9").append($image));
+
+    $("#stored-images").append($row);
+  };
+
+  ns.remove_image = function(event, key) {
+    let $row = $(event.target).closest(".image-upload-row");
+    kivi.FileDB.delete_key(key, () => {
+      $row.remove();
+      num_images--;
+      ns.set_image_button_enabled();
+    });
+  };
+
+  ns.set_image_button_enabled = function() {
+    $('#upload_images_submit').attr("disabled", num_images == 0 || !$('#object_id').val());
+  };
+
+
+  ns.upload_files = function() {
+    let id = $('#object_id').val();
+    let type = $('#object_type').val();
+
+    ns.upload_selected_files(id, type, MAXSIZE);
+  };
+
+  ns.upload_selected_files = function(id, type, maxsize) {
+    $("#upload_modal").modal({ dismissible: false });
+    $("#upload_modal").modal("open");
+
+    kivi.FileDB.retrieve_all((myfiles) => {
+      let filesize  = 0;
+      myfiles.forEach(file => filesize  += file.size);
+
+      if (filesize > maxsize) {
+        M.flash(kivi.t8("filesize too big: ") + ns.format_si(filesize) + " > " + ns.format_si(maxsize));
+        $("#upload_modal").modal("close");
+        return;
+      }
+
+      let data = new FormData();
+      myfiles.forEach(file => data.append("uploadfiles[]", file));
+      data.append("action", "File/ajax_files_uploaded");
+      data.append("json", "1");
+      data.append("object_type", type);
+      data.append("object_id", id);
+      data.append("file_type", "attachment");
+
+      $("#upload_result").html(kivi.t8("start upload"));
+
+      let xhr = new XMLHttpRequest;
+      xhr.open('POST', 'controller.pl', true);
+      xhr.onload = ns.upload_complete;
+      xhr.upload.onprogress = ns.progress;
+      xhr.upload.onerror = ns.failed;
+      xhr.upload.onabort = ns.abort;
+      xhr.send(data);
+
+      ns.upload_in_progress = xhr;
+    });
+  };
+
+  ns.progress = function(event) {
+    if (event.lengthComputable) {
+      var percent_complete = (event.loaded / event.total) * 100;
+      $("#upload_progress div").removeClass("indeterminate").addClass("determinate").attr("style", "width: " + percent_complete + "%");
+    }
+  };
+
+  ns.failed = function() {
+    $('#upload_modal').modal('close');
+    M.flash(kivi.t8("An error occurred while transferring the file."));
+  };
+
+  ns.abort = function() {
+    $('#upload_modal').modal('close');
+    M.flash(kivi.t8("The transfer has been canceled by the user."));
+
+    ns.upload_in_progress = undefined;
+  };
+
+  ns.upload_complete = function() {
+    $('#upload_modal').modal('close');
+    M.flash(kivi.t8("Files have been uploaded successfully."));
+    kivi.FileDB.delete_all(ns.reload_images);
+  };
+
+  ns.resolve_object = function(event) {
+    let obj_type = $('#object_type').val();
+    let number   = event.target.value;
+
+    $.ajax({
+      url: "controller.pl",
+      data: {
+        action: "ImageUpload/resolve_object_by_number",
+        object_type: obj_type,
+        object_number: number
+      },
+      type: "json",
+      success: (json) => {
+        if (json.error) {
+          $("#object_description").html("");
+          $("#object_id").val("");
+        } else {
+          $("#object_description").html(json.description);
+          $("#object_id").val(json.id);
+        }
+        ns.set_image_button_enabled();
+      },
+      error: () => {
+        $("#object_description").html("");
+        $("#object_id").val("");
+        ns.set_image_button_enabled();
+      }
+    });
+  };
+
+  /* this tries to format the number human readable. 3 significant digits, si suffix, */
+  ns.format_si = function(n) {
+    const prefixes = ["", "K" , "M", "G", "T", "P"];
+    let i = 0;
+    while (n >= 1024) {
+      n /= 1024;
+      i++;
+    }
+
+    return kivi.format_amount(n, 3 - (n|0).toString().length) + prefixes[i] + "B";
+  };
+
+  ns.init = function() {
+    ns.reload_images();
+  };
+});
+
+$(kivi.ImageUpload.init);
diff --git a/js/kivi.Inventory.js b/js/kivi.Inventory.js
new file mode 100644 (file)
index 0000000..622199a
--- /dev/null
@@ -0,0 +1,91 @@
+namespace('kivi.Inventory', function(ns) {
+  ns.reload_bin_selection = function() {
+    $.post("controller.pl", { action: 'Inventory/warehouse_changed',
+                              warehouse_id: function(){ return $('#warehouse_id').val() } },
+           kivi.eval_json_result);
+  };
+
+  ns.save_stocktaking = function(dont_check_already_counted) {
+    var data = $('#stocktaking_form').serializeArray();
+    data.push({ name: 'action', value: 'Inventory/save_stocktaking' });
+    data.push({ name: 'dont_check_already_counted', value: dont_check_already_counted });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.stocktaking_part_changed = function() {
+    var data = $('#stocktaking_form').serializeArray();
+    data.push({ name: 'action', value: 'Inventory/stocktaking_part_changed' });
+    $.post("controller.pl", data, kivi.eval_json_result);
+    $.post("controller.pl", { action: 'Inventory/mini_stock',
+                              part_id: function(){ return $('#part_id').val() } },
+           kivi.eval_json_result);
+  };
+
+  ns.reload_stocktaking_history = function(target, source) {
+    var data = $('#stocktaking_form').serializeArray();
+    $.ajax({
+      url:        source,
+      data:       data,
+      success:    function (rsp) {
+        $(target).html(rsp);
+        $(target).find('a.paginate-link').click(function(event){
+          event.preventDefault();
+          kivi.Inventory.reload_stocktaking_history(target, event.target + '')});
+      }
+    });
+  };
+
+  ns.stocktaking_correct_counted = function() {
+    kivi.Inventory.close_already_counted_dialog();
+    kivi.Inventory.save_stocktaking(1);
+  };
+
+  ns.stocktaking_add_counted = function(qty_to_add_to) {
+    resulting_qty = kivi.parse_amount($('#target_qty').val()) + 1.0*qty_to_add_to;
+    $('#target_qty').val(kivi.format_amount(resulting_qty, -2));
+    kivi.Inventory.close_already_counted_dialog();
+    kivi.Inventory.save_stocktaking(1);
+  };
+
+  ns.close_already_counted_dialog = function() {
+    $('#already_counted_dialog').dialog("close");
+  };
+
+  ns.beep = function() {
+    var snd = new Audio("data:audio/wav;base64,//uQRAAAAWMSLwUIYAAsYkXgoQwAEaYLWfkWgAI0wWs/ItAAAGDgYtAgAyN+QWaAAihwMWm4G8QQRDiMcCBcH3Cc+CDv/7xA4Tvh9Rz/y8QADBwMWgQAZG/ILNAARQ4GLTcDeIIIhxGOBAuD7hOfBB3/94gcJ3w+o5/5eIAIAAAVwWgQAVQ2ORaIQwEMAJiDg95G4nQL7mQVWI6GwRcfsZAcsKkJvxgxEjzFUgfHoSQ9Qq7KNwqHwuB13MA4a1q/DmBrHgPcmjiGoh//EwC5nGPEmS4RcfkVKOhJf+WOgoxJclFz3kgn//dBA+ya1GhurNn8zb//9NNutNuhz31f////9vt///z+IdAEAAAK4LQIAKobHItEIYCGAExBwe8jcToF9zIKrEdDYIuP2MgOWFSE34wYiR5iqQPj0JIeoVdlG4VD4XA67mAcNa1fhzA1jwHuTRxDUQ//iYBczjHiTJcIuPyKlHQkv/LHQUYkuSi57yQT//uggfZNajQ3Vmz+Zt//+mm3Wm3Q576v////+32///5/EOgAAADVghQAAAAA//uQZAUAB1WI0PZugAAAAAoQwAAAEk3nRd2qAAAAACiDgAAAAAAABCqEEQRLCgwpBGMlJkIz8jKhGvj4k6jzRnqasNKIeoh5gI7BJaC1A1AoNBjJgbyApVS4IDlZgDU5WUAxEKDNmmALHzZp0Fkz1FMTmGFl1FMEyodIavcCAUHDWrKAIA4aa2oCgILEBupZgHvAhEBcZ6joQBxS76AgccrFlczBvKLC0QI2cBoCFvfTDAo7eoOQInqDPBtvrDEZBNYN5xwNwxQRfw8ZQ5wQVLvO8OYU+mHvFLlDh05Mdg7BT6YrRPpCBznMB2r//xKJjyyOh+cImr2/4doscwD6neZjuZR4AgAABYAAAABy1xcdQtxYBYYZdifkUDgzzXaXn98Z0oi9ILU5mBjFANmRwlVJ3/6jYDAmxaiDG3/6xjQQCCKkRb/6kg/wW+kSJ5//rLobkLSiKmqP/0ikJuDaSaSf/6JiLYLEYnW/+kXg1WRVJL/9EmQ1YZIsv/6Qzwy5qk7/+tEU0nkls3/zIUMPKNX/6yZLf+kFgAfgGyLFAUwY//uQZAUABcd5UiNPVXAAAApAAAAAE0VZQKw9ISAAACgAAAAAVQIygIElVrFkBS+Jhi+EAuu+lKAkYUEIsmEAEoMeDmCETMvfSHTGkF5RWH7kz/ESHWPAq/kcCRhqBtMdokPdM7vil7RG98A2sc7zO6ZvTdM7pmOUAZTnJW+NXxqmd41dqJ6mLTXxrPpnV8avaIf5SvL7pndPvPpndJR9Kuu8fePvuiuhorgWjp7Mf/PRjxcFCPDkW31srioCExivv9lcwKEaHsf/7ow2Fl1T/9RkXgEhYElAoCLFtMArxwivDJJ+bR1HTKJdlEoTELCIqgEwVGSQ+hIm0NbK8WXcTEI0UPoa2NbG4y2K00JEWbZavJXkYaqo9CRHS55FcZTjKEk3NKoCYUnSQ0rWxrZbFKbKIhOKPZe1cJKzZSaQrIyULHDZmV5K4xySsDRKWOruanGtjLJXFEmwaIbDLX0hIPBUQPVFVkQkDoUNfSoDgQGKPekoxeGzA4DUvnn4bxzcZrtJyipKfPNy5w+9lnXwgqsiyHNeSVpemw4bWb9psYeq//uQZBoABQt4yMVxYAIAAAkQoAAAHvYpL5m6AAgAACXDAAAAD59jblTirQe9upFsmZbpMudy7Lz1X1DYsxOOSWpfPqNX2WqktK0DMvuGwlbNj44TleLPQ+Gsfb+GOWOKJoIrWb3cIMeeON6lz2umTqMXV8Mj30yWPpjoSa9ujK8SyeJP5y5mOW1D6hvLepeveEAEDo0mgCRClOEgANv3B9a6fikgUSu/DmAMATrGx7nng5p5iimPNZsfQLYB2sDLIkzRKZOHGAaUyDcpFBSLG9MCQALgAIgQs2YunOszLSAyQYPVC2YdGGeHD2dTdJk1pAHGAWDjnkcLKFymS3RQZTInzySoBwMG0QueC3gMsCEYxUqlrcxK6k1LQQcsmyYeQPdC2YfuGPASCBkcVMQQqpVJshui1tkXQJQV0OXGAZMXSOEEBRirXbVRQW7ugq7IM7rPWSZyDlM3IuNEkxzCOJ0ny2ThNkyRai1b6ev//3dzNGzNb//4uAvHT5sURcZCFcuKLhOFs8mLAAEAt4UWAAIABAAAAAB4qbHo0tIjVkUU//uQZAwABfSFz3ZqQAAAAAngwAAAE1HjMp2qAAAAACZDgAAAD5UkTE1UgZEUExqYynN1qZvqIOREEFmBcJQkwdxiFtw0qEOkGYfRDifBui9MQg4QAHAqWtAWHoCxu1Yf4VfWLPIM2mHDFsbQEVGwyqQoQcwnfHeIkNt9YnkiaS1oizycqJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=");  
+    snd.play();
+  };
+
+  ns.check_stocktaking_qty_threshold = function() {
+    var data = $('#stocktaking_form').serializeArray();
+    data.push({ name: 'action', value: 'Inventory/stocktaking_get_warn_qty_threshold' });
+
+    var warn = false;
+    $.ajax({
+      url:      'controller.pl',
+      data:     data,
+      method:   "GET",
+      async:    false,
+      dataType: 'text',
+      success:  function(val) {
+        warn = val;
+      }
+    });
+
+    if (warn) {
+      kivi.Inventory.beep();
+      return confirm(warn);
+    } else {
+      return true;
+    }
+  };
+});
+
+$(function(){
+  $('#part_id').change(kivi.Inventory.stocktaking_part_changed);
+  $('#warehouse_id').change(kivi.Inventory.reload_bin_selection);
+  $('#cutoff_date_as_date').change(function() {kivi.Inventory.reload_stocktaking_history('#stocktaking_history', 'controller.pl?action=Inventory/reload_stocktaking_history');});
+
+  kivi.Inventory.reload_stocktaking_history('#stocktaking_history', 'controller.pl?action=Inventory/reload_stocktaking_history');
+});
diff --git a/js/kivi.LeftMenu.js b/js/kivi.LeftMenu.js
new file mode 100644 (file)
index 0000000..537974e
--- /dev/null
@@ -0,0 +1,22 @@
+namespace('kivi.LeftMenu', function(ns) {
+  'use strict';
+  ns.init = function(sections) {
+    sections.forEach(function(b,i){
+      var a=$('<a class="ml">').append($('<span class="mii ms">').append($('<div>').addClass(b[3])),$('<span class="mic">').append(b[0]));
+      if(b[5])a.attr('href', b[5]);
+      if(b[6])a.attr('target', b[6]);
+      $('#html-menu').append($('<div class="mi">').addClass(b[4]).addClass(b[1]).attr('id','mi'+b[2]).append(a))
+    });
+    $('#html-menu div.i, #html-menu div.sm').not('[id^='+$.cookie('html-menu-selection')+'_]').hide();
+    $('#html-menu div.m#'+$.cookie('html-menu-selection')).addClass('menu-open');
+    $('#html-menu div.m').each(function(){
+      $(this).click(function(){
+        $.cookie('html-menu-selection',$(this).attr('id'));
+        $('#html-menu div.mi').not('div.m').not('[id^='+$(this).attr('id')+'_]').hide();
+        $('#html-menu div.mi[id^='+$(this).attr('id')+'_]').toggle();
+        $('#html-menu div.m').not('[id^='+$(this).attr('id')+']').removeClass('menu-open');
+        $(this).toggleClass('menu-open');
+      });
+    });
+  };
+});
diff --git a/js/kivi.Letter.js b/js/kivi.Letter.js
new file mode 100644 (file)
index 0000000..1f2865c
--- /dev/null
@@ -0,0 +1,11 @@
+namespace('kivi.Letter', function(ns) {
+  "use strict";
+
+  $(function() {
+    $('#letter_customer_id,#letter_vendor_id').change(function(){
+      var data = $('form').serializeArray();
+      data.push({ name: 'action', value: 'Letter/update_contacts' });
+      $.post('controller.pl', data, kivi.eval_json_result);
+    });
+  });
+});
diff --git a/js/kivi.MassDeliveryOrderPrint.js b/js/kivi.MassDeliveryOrderPrint.js
new file mode 100644 (file)
index 0000000..e0b5387
--- /dev/null
@@ -0,0 +1,36 @@
+namespace('kivi.MassDeliveryOrderPrint', function(ns) {
+  "use strict";
+
+  ns.massConversionFinishProcess = function() {
+    $('#mass_print_dialog').dialog('close');
+  };
+
+  ns.massConversionStarted = function() {
+   $('#mdo_start_process_button,.ui-dialog-titlebar button.ui-dialog-titlebar-close').prop('disabled', 'disabled');
+   $('#mdo_start_process_abort_link').remove();
+   $('#mass_print_dialog').data('timerId', setInterval(function() {
+      $.get("controller.pl", {
+        action: 'MassDeliveryOrderPrint/mass_mdo_status',
+        job_id: $('#mdo_job_id').val()
+      }, kivi.eval_json_result);
+    }, 5000));
+  };
+
+  ns.massConversionPopup = function() {
+    kivi.popup_dialog({
+      id: 'mass_print_dialog',
+      dialog: {
+        title: kivi.t8('Generate and print sales delivery orders')
+      }
+    });
+  };
+
+  ns.massConversionFinished = function() {
+    clearInterval($('#mass_print_dialog').data('timerId'));
+    $('.ui-dialog-titlebar button.ui-dialog-titlebar-close').prop('disabled', '')
+  };
+
+  ns.submitMultiOrders = function () {
+    return kivi.submit_ajax_form('controller.pl?action=MassDeliveryOrderPrint/mass_mdo_print', $('#form'));
+  };
+});
index 59c7939..4a9ba9f 100644 (file)
@@ -12,6 +12,7 @@ namespace('kivi.MassInvoiceCreatePrint', function(ns) {
     alert(kivi.t8('No delivery orders have been selected.'));
     return false;
   };
+
   this.checkInvoiceSelection = function() {
     if ($("[data-checkall=1]:checked").size() > 0)
       return true;
@@ -19,15 +20,6 @@ namespace('kivi.MassInvoiceCreatePrint', function(ns) {
     return false;
   };
 
-  this.submitMassCreationForm = function() {
-    if (!kivi.MassInvoiceCreatePrint.checkDeliveryOrderSelection())
-      return false;
-
-    $('body').addClass('loading');
-    $('form').submit();
-    return false;
-  };
-
   this.createPrintAllInitialize = function() {
     kivi.popup_dialog({
       id: 'create_print_all_dialog',
@@ -41,13 +33,19 @@ namespace('kivi.MassInvoiceCreatePrint', function(ns) {
     $('#cpa_start_process_button,.ui-dialog-titlebar button.ui-dialog-titlebar-close').prop('disabled', 'disabled');
     $('#cpa_start_process_abort_link').remove();
 
+    var filter = $('[name^=filter\\.]').serializeArray();
     var data = {
+      action:             'MassInvoiceCreatePrint/create_print_all_start',
       number_of_invoices: $('#cpa_number_of_invoices').val(),
+      bothsided:          $('#cpa_bothsided').val(),
       printer_id:         $('#cpa_printer_id').val(),
       copy_printer_id:    $('#cpa_copy_printer_id').val(),
       transdate:          $('#transdate').val()
     };
-    kivi.submit_ajax_form('controller.pl?action=MassInvoiceCreatePrint/create_print_all_start', '[name^=filter\\.]', data);
+
+    $(filter).each(function(index, obj){ data[obj.name] = obj.value; });
+
+    $.post('controller.pl', data, kivi.eval_json_result);
   };
 
   this.createPrintAllFinishProcess = function() {
@@ -69,11 +67,43 @@ namespace('kivi.MassInvoiceCreatePrint', function(ns) {
     $('.ui-dialog-titlebar button.ui-dialog-titlebar-close').prop('disabled', '')
   };
 
-  this.setup = function() {
-    $('#create_button').click(kivi.MassInvoiceCreatePrint.submitMassCreationForm);
-    $('#create_print_all_button').click(kivi.MassInvoiceCreatePrint.createPrintAllInitialize);
-    $('#action_print').click(kivi.MassInvoiceCreatePrint.checkInvoiceSelection);
+  ns.showMassPrintOptions = function() {
+    $('#printer_options_printer_id').val($('#printer_id').val());
+
+    kivi.popup_dialog({
+      id: 'print_options',
+      dialog: {
+        title: kivi.t8('Print options'),
+        width:  600,
+        height: 200
+      }
+    });
+
+    return true;
+  };
+
+  ns.showMassPrintOptionsOrDownloadDirectly = function() {
+    if (!kivi.MassInvoiceCreatePrint.checkInvoiceSelection())
+      return false;
+
+    if ($('#print_options_printer_id').length === 0)
+      return kivi.MassInvoiceCreatePrint.massPrint();
+    return kivi.MassInvoiceCreatePrint.showMassPrintOptions();
   };
-});
 
-$(kivi.MassInvoiceCreatePrint.setup);
+  ns.massPrint = function() {
+    $('#print_options').dialog('close');
+
+    $('#printer_id').val($('#print_options_printer_id').val());
+    $('#bothsided').val($('#print_options_bothsided').prop('checked') ? 1 : 0);
+    $('#action').val('MassInvoiceCreatePrint/print');
+
+    $('#report_form').submit();
+
+    return true;
+  };
+
+  this.resetSearchForm = function() {
+    $("#filter_table input").val("");
+  };
+});
diff --git a/js/kivi.Materialize.js b/js/kivi.Materialize.js
new file mode 100644 (file)
index 0000000..3262ffa
--- /dev/null
@@ -0,0 +1,176 @@
+namespace("kivi.Materialize", function(ns) {
+  "use strict";
+
+  ns.init = function() {
+    ns.reinit_widgets();
+  };
+
+  ns.build_i18n = function() {
+    return {
+      months: [
+        kivi.t8('January'),
+        kivi.t8('February'),
+        kivi.t8('March'),
+        kivi.t8('April'),
+        kivi.t8('May'),
+        kivi.t8('June'),
+        kivi.t8('July'),
+        kivi.t8('August'),
+        kivi.t8('September'),
+        kivi.t8('October'),
+        kivi.t8('November'),
+        kivi.t8('December')
+      ],
+      monthsShort: [
+        kivi.t8('Jan'),
+        kivi.t8('Feb'),
+        kivi.t8('Mar'),
+        kivi.t8('Apr'),
+        kivi.t8('May'),
+        kivi.t8('Jun'),
+        kivi.t8('Jul'),
+        kivi.t8('Aug'),
+        kivi.t8('Sep'),
+        kivi.t8('Oct'),
+        kivi.t8('Nov'),
+        kivi.t8('Dec')
+      ],
+      weekdays: [
+        kivi.t8('Sunday'),
+        kivi.t8('Monday'),
+        kivi.t8('Tuesday'),
+        kivi.t8('Wednesday'),
+        kivi.t8('Thursday'),
+        kivi.t8('Friday'),
+        kivi.t8('Saturday')
+      ],
+      weekdaysShort: [
+        kivi.t8('Sun'),
+        kivi.t8('Mon'),
+        kivi.t8('Tue'),
+        kivi.t8('Wed'),
+        kivi.t8('Thu'),
+        kivi.t8('Fri'),
+        kivi.t8('Sat')
+      ],
+
+      // Buttons
+      today: kivi.t8('Today'),
+      done: kivi.t8('Ok'),
+      clear: kivi.t8('Clear'),
+      cancel: kivi.t8('Cancel'),
+
+      // Accessibility labels
+      labelMonthNext: kivi.t8('Next month'),
+      labelMonthPrev: kivi.t8('Previous month')
+    };
+  };
+
+  ns.flash = function(text) {
+    M.toast({html: text});
+  };
+
+  ns.reinit_widgets = function() {
+    $('.sidenav').sidenav();
+    $('select').formSelect();
+    $('.datepicker').datepicker({
+      firstDay: 1,
+      format: kivi.myconfig.dateformat,
+      showClearBtn: true,
+      i18n: ns.build_i18n()
+    });
+    $('.modal').modal();
+    $('.materialboxed').materialbox();
+    M.updateTextFields();
+  };
+
+  // alternative for kivi.popup_dialog.
+  // opens materialize modal instead.
+  //
+  // differences: M.modal can not load external content, so it needs to be fetched manually and inserted into the DOM.
+  ns.popup_dialog = function(params) {
+    params            = params        || { };
+    let id            = params.id     || 'jqueryui_popup_dialog';
+    let $div;
+    let custom_close  = params.dialog ? params.dialog.close : undefined;
+    let dialog_params = $.extend(
+      { // kivitendo default parameters.
+        // unlike classic layout, there is not fixed size, and M.modal is always... modal
+        onCloseStart: custom_close
+      },
+      // User supplied options:
+      params.dialog || { },
+      { // Options that must not be changed:
+        // close options already work
+      });
+
+    if (params.url) {
+      $.ajax({
+        url: params.url,
+        data: params.data,
+        success: function(data) {
+          params.html = data;
+          params.url = undefined;
+          params.data = undefined;
+          ns.popup_dialog(params);
+        },
+        error: function(x, status, error) { console.error(error); },
+        dataType: 'text',
+      });
+      return 1;
+    }
+
+    if (params.html) {
+      $div = $('<div>');
+      $div.attr('id', id);
+      $div.addClass("modal");
+      let $modal_content = $('<div>');
+      $modal_content.addClass('modal-content');
+      $modal_content.html(params.html);
+      $div.append($modal_content);
+      $('body').append($div);
+      kivi.reinit_widgets();
+      dialog_params.onCloseEnd = function() { $div.remove(); };
+
+      $div.modal(dialog_params);
+    } else if(params.id) {
+      $div = $('#' + params.id);
+    } else {
+      console.error("insufficient parameters to open dialog");
+      return 0;
+    }
+
+    $div.modal('open');
+
+    return true;
+  };
+
+  /**
+   * upload file to local storage for later sync
+   *
+   * should be used with P.M.file_upload(..., local=>1)
+   */
+  ns.LocalFileUpload = function(options) {
+    this.storage_token = options.storage_token; // used in localstorage to retrieve the file
+    this.dom_selector  = options.dom_selector;  // file inputs to listen on
+
+    this.init();
+  };
+
+  ns.LocalFileUpload.prototype = {
+    init: function() {
+      $(this.dom_selector).change(this.handle_file_upload);
+    },
+    handle_file_upload: function() {
+
+    },
+    load_files: function() {
+      return JSON.parse(localStorage.getImte(this.storage_token));
+    },
+    save_files: function() {
+      return JSON.parse(localStorage.getImte(this.storage_token));
+    },
+
+  };
+
+});
diff --git a/js/kivi.Order.js b/js/kivi.Order.js
new file mode 100644 (file)
index 0000000..9ef6ef5
--- /dev/null
@@ -0,0 +1,997 @@
+namespace('kivi.Order', function(ns) {
+  ns.check_cv = function() {
+    if ($('#type').val() == 'sales_order' || $('#type').val() == 'sales_quotation') {
+      if ($('#order_customer_id').val() === '') {
+        alert(kivi.t8('Please select a customer.'));
+        return false;
+      }
+    } else  {
+      if ($('#order_vendor_id').val() === '') {
+        alert(kivi.t8('Please select a vendor.'));
+        return false;
+      }
+    }
+    return true;
+  };
+
+  ns.check_duplicate_parts = function(question) {
+    var id_arr = $('[name="order.orderitems[].parts_id"]').map(function() { return this.value; }).get();
+
+    var i, obj = {}, pos = [];
+
+    for (i = 0; i < id_arr.length; i++) {
+      var id = id_arr[i];
+      if (obj.hasOwnProperty(id)) {
+        pos.push(i + 1);
+      }
+      obj[id] = 0;
+    }
+
+    if (pos.length > 0) {
+      question = question || kivi.t8("Do you really want to continue?");
+      return confirm(kivi.t8("There are duplicate parts at positions") + "\n"
+                     + pos.join(', ') + "\n"
+                     + question);
+    }
+    return true;
+  };
+
+  ns.check_valid_reqdate = function() {
+    if ($('#order_reqdate_as_date').val() === '') {
+      alert(kivi.t8('Please select a delivery date.'));
+      return false;
+    } else {
+      return true;
+    }
+  };
+
+  ns.save = function(action, warn_on_duplicates, warn_on_reqdate, back_to_caller) {
+    if (!ns.check_cv()) return;
+    if (warn_on_duplicates && !ns.check_duplicate_parts()) return;
+    if (warn_on_reqdate    && !ns.check_valid_reqdate())   return;
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'Order/' + action });
+
+    if (back_to_caller) data.push({ name: 'back_to_caller', value: '1' });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.delete_order = function() {
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'Order/delete' });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.show_print_options = function(warn_on_duplicates, warn_on_reqdate) {
+    if (!ns.check_cv()) return;
+    if (warn_on_duplicates && !ns.check_duplicate_parts(kivi.t8("Do you really want to print?"))) return;
+    if (warn_on_reqdate    && !ns.check_valid_reqdate())   return;
+
+    kivi.popup_dialog({
+      id: 'print_options',
+      dialog: {
+        title: kivi.t8('Print options'),
+        width:  800,
+        height: 300
+      }
+    });
+  };
+
+  ns.print = function() {
+    $('#print_options').dialog('close');
+
+    var data = $('#order_form').serializeArray();
+    data = data.concat($('#print_options_form').serializeArray());
+    data.push({ name: 'action', value: 'Order/print' });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  var email_dialog;
+
+  ns.setup_send_email_dialog = function() {
+    kivi.SalesPurchase.show_all_print_options_elements();
+    kivi.SalesPurchase.show_print_options_elements([ 'sendmode', 'media', 'copies', 'remove_draft' ], false);
+
+    $('#print_options_form table').first().remove().appendTo('#email_form_print_options');
+
+    $('select#format').change(kivi.Order.adjust_email_attachment_name_for_template_format);
+    kivi.Order.adjust_email_attachment_name_for_template_format();
+
+    var to_focus = $('#email_form_to').val() === '' ? 'to' : 'subject';
+    $('#email_form_' + to_focus).focus();
+  };
+
+  ns.finish_send_email_dialog = function() {
+    kivi.SalesPurchase.show_all_print_options_elements();
+
+    $('#email_form_print_options table').first().remove().prependTo('#print_options_form');
+    return true;
+  };
+
+  ns.show_email_dialog = function(html) {
+    var id            = 'send_email_dialog';
+    var dialog_params = {
+      id:     id,
+      width:  800,
+      height: 600,
+      title:  kivi.t8('Send email'),
+      modal:  true,
+      beforeClose: kivi.Order.finish_send_email_dialog,
+      close: function(event, ui) {
+        email_dialog.remove();
+      }
+    };
+
+    $('#' + id).remove();
+
+    email_dialog = $('<div style="display:none" id="' + id + '"></div>').appendTo('body');
+    email_dialog.html(html);
+    email_dialog.dialog(dialog_params);
+
+    kivi.Order.setup_send_email_dialog();
+
+    $('.cancel').click(ns.close_email_dialog);
+
+    return true;
+  };
+
+  ns.send_email = function() {
+    // push button only once -> slow response from mail server
+    ns.email_dialog_disable_send();
+
+    var data = $('#order_form').serializeArray();
+    data = data.concat($('[name^="email_form."]').serializeArray());
+    data = data.concat($('[name^="print_options."]').serializeArray());
+    data.push({ name: 'action', value: 'Order/send_email' });
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.email_dialog_disable_send = function() {
+    // disable mail send event to prevent
+    // impatient users to send multiple times
+    $('#send_email').prop('disabled', true);
+  };
+
+  ns.close_email_dialog = function() {
+    email_dialog.dialog("close");
+  };
+
+  ns.adjust_email_attachment_name_for_template_format = function() {
+    var $filename_elt = $('#email_form_attachment_filename');
+    var $format_elt   = $('select#format');
+
+    if (!$filename_elt || !$format_elt)
+      return;
+
+    var format   = $format_elt.val().toLowerCase();
+    var new_ext  = format == 'html' ? 'html' : format == 'opendocument' ? 'odt' : 'pdf';
+    var filename = $filename_elt.val();
+
+    $filename_elt.val(filename.replace(/[^.]+$/, new_ext));
+  };
+
+  ns.set_number_in_title = function(elt) {
+    $('#nr_in_title').html($(elt).val());
+  };
+
+  ns.reload_cv_dependent_selections = function() {
+    $('#order_shipto_id').val('');
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'Order/customer_vendor_changed' });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.reformat_number = function(event) {
+    $(event.target).val(kivi.format_amount(kivi.parse_amount($(event.target).val()), -2));
+  };
+
+  ns.reformat_number_as_null_number = function(event) {
+    if ($(event.target).val() === '') {
+      return;
+    }
+    ns.reformat_number(event);
+  };
+
+  ns.update_exchangerate = function(event) {
+    if (!ns.check_cv()) {
+      $('#order_currency_id').val($('#old_currency_id').val());
+      return;
+    }
+
+    var rate_input = $('#order_exchangerate_as_null_number');
+    // unset exchangerate if currency changed
+    if ($('#order_currency_id').val() !== $('#old_currency_id').val()) {
+      rate_input.val('');
+    }
+
+    // only set exchangerate if unset
+    if (rate_input.val() !== '') {
+      return;
+    }
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'Order/update_exchangerate' });
+
+    $.ajax({
+      url: 'controller.pl',
+      data: data,
+      method: 'POST',
+      dataType: 'json',
+      success: function(data){
+        if (!data.is_standard) {
+          $('#currency_name').text(data.currency_name);
+          if (data.exchangerate) {
+            rate_input.val(data.exchangerate);
+          } else {
+            rate_input.val('');
+          }
+          $('#exchangerate_settings').show();
+        } else {
+          rate_input.val('');
+          $('#exchangerate_settings').hide();
+        }
+        if ($('#order_currency_id').val() != $('#old_currency_id').val() ||
+            !data.is_standard && data.exchangerate != $('#old_exchangerate').val()) {
+          kivi.display_flash('warning', kivi.t8('You have changed the currency or exchange rate. Please check prices.'));
+        }
+        $('#old_currency_id').val($('#order_currency_id').val());
+        $('#old_exchangerate').val(data.exchangerate);
+      }
+    });
+  };
+
+  ns.exchangerate_changed = function(event) {
+    if (kivi.parse_amount($('#order_exchangerate_as_null_number').val()) != kivi.parse_amount($('#old_exchangerate').val())) {
+      kivi.display_flash('warning', kivi.t8('You have changed the currency or exchange rate. Please check prices.'));
+      $('#old_exchangerate').val($('#order_exchangerate_as_null_number').val());
+    }
+  };
+
+  ns.recalc_amounts_and_taxes = function() {
+    if (!kivi.validate_form('#order_form')) return;
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'Order/recalc_amounts_and_taxes' });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.unit_change = function(event) {
+    var row           = $(event.target).parents("tbody").first();
+    var item_id_dom   = $(row).find('[name="orderitem_ids[+]"]');
+    var sellprice_dom = $(row).find('[name="order.orderitems[].sellprice_as_number"]');
+    var select_elt    = $(row).find('[name="order.orderitems[].unit"]');
+
+    var oldval = $(select_elt).data('oldval');
+    $(select_elt).data('oldval', $(select_elt).val());
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action',           value: 'Order/unit_changed'     },
+              { name: 'item_id',          value: item_id_dom.val()        },
+              { name: 'old_unit',         value: oldval                   },
+              { name: 'sellprice_dom_id', value: sellprice_dom.attr('id') });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.update_sellprice = function(item_id, price_str) {
+    var row       = $('#item_' + item_id).parents("tbody").first();
+    var price_elt = $(row).find('[name="order.orderitems[].sellprice_as_number"]');
+    var html_elt  = $(row).find('[name="sellprice_text"]');
+    price_elt.val(price_str);
+    html_elt.html(price_str);
+  };
+
+  ns.load_second_row = function(row) {
+    var item_id_dom = $(row).find('[name="orderitem_ids[+]"]');
+    var div_elt     = $(row).find('[name="second_row"]');
+
+    if ($(div_elt).data('loaded') == 1) {
+      return;
+    }
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action',     value: 'Order/load_second_rows' },
+              { name: 'item_ids[]', value: item_id_dom.val()        });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.load_all_second_rows = function() {
+    var rows = $('.row_entry').filter(function(idx, elt) {
+      return $(elt).find('[name="second_row"]').data('loaded') != 1;
+    });
+
+    var item_ids = $.map(rows, function(elt) {
+      var item_id = $(elt).find('[name="orderitem_ids[+]"]').val();
+      return { name: 'item_ids[]', value: item_id };
+    });
+
+    if (item_ids.length == 0) {
+      return;
+    }
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'Order/load_second_rows' });
+    data = data.concat(item_ids);
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.hide_second_row = function(row) {
+    $(row).children().not(':first').hide();
+    $(row).data('expanded', 0);
+    var elt = $(row).find('.expand');
+    elt.attr('src', "image/expand.svg");
+    elt.attr('alt', kivi.t8('Show details'));
+    elt.attr('title', kivi.t8('Show details'));
+  };
+
+  ns.show_second_row = function(row) {
+    $(row).children().not(':first').show();
+    $(row).data('expanded', 1);
+    var elt = $(row).find('.expand');
+    elt.attr('src', "image/collapse.svg");
+    elt.attr('alt', kivi.t8('Hide details'));
+    elt.attr('title', kivi.t8('Hide details'));
+  };
+
+  ns.toggle_second_row = function(row) {
+    if ($(row).data('expanded') == 1) {
+      ns.hide_second_row(row);
+    } else {
+      ns.show_second_row(row);
+    }
+  };
+
+  ns.init_row_handlers = function() {
+    kivi.run_once_for('.recalc', 'on_change_recalc', function(elt) {
+      $(elt).change(ns.recalc_amounts_and_taxes);
+    });
+
+    kivi.run_once_for('.reformat_number', 'on_change_reformat', function(elt) {
+      $(elt).change(ns.reformat_number);
+    });
+
+    kivi.run_once_for('.unitselect', 'on_change_unit_with_oldval', function(elt) {
+      $(elt).data('oldval', $(elt).val());
+      $(elt).change(ns.unit_change);
+    });
+
+    kivi.run_once_for('.row_entry', 'on_kbd_click_show_hide', function(elt) {
+      $(elt).keydown(function(event) {
+        var row;
+        if (event.keyCode == 40 && event.shiftKey === true) {
+          // shift arrow down
+          event.preventDefault();
+          row = $(event.target).parents(".row_entry").first();
+          ns.load_second_row(row);
+          ns.show_second_row(row);
+          return false;
+        }
+        if (event.keyCode == 38 && event.shiftKey === true) {
+          // shift arrow up
+          event.preventDefault();
+          row = $(event.target).parents(".row_entry").first();
+          ns.hide_second_row(row);
+          return false;
+        }
+      });
+    });
+
+    kivi.run_once_for('.expand', 'expand_second_row', function(elt) {
+      $(elt).click(function(event) {
+        event.preventDefault();
+        var row = $(event.target).parents(".row_entry").first();
+        ns.load_second_row(row);
+        ns.toggle_second_row(row);
+        return false;
+      })
+    });
+
+  };
+
+  ns.redisplay_line_values = function(is_sales, data) {
+    $('.row_entry').each(function(idx, elt) {
+      $(elt).find('[name="linetotal"]').html(data[idx][0]);
+      if (is_sales && $(elt).find('[name="second_row"]').data('loaded') == 1) {
+        var mt = data[idx][1];
+        var mp = data[idx][2];
+        var h  = '<span';
+        if (mt[0] === '-') h += ' class="plus0"';
+        h += '>' + mt + '&nbsp;&nbsp;' + mp + '%';
+        h += '</span>';
+        $(elt).find('[name="linemargin"]').html(h);
+      }
+    });
+  };
+
+  ns.redisplay_cvpartnumbers = function(data) {
+    $('.row_entry').each(function(idx, elt) {
+      $(elt).find('[name="cvpartnumber"]').html(data[idx][0]);
+    });
+  };
+
+  ns.renumber_positions = function() {
+    $('.row_entry [name="position"]').each(function(idx, elt) {
+      $(elt).html(idx+1);
+    });
+    $('.row_entry').each(function(idx, elt) {
+      $(elt).data("position", idx+1);
+    });
+  };
+
+  ns.reorder_items = function(order_by) {
+    var dir = $('#' + order_by + '_header_id a img').attr("data-sort-dir");
+    $('#row_table_id thead a img').remove();
+
+    var src;
+    if (dir == "1") {
+      dir = "0";
+      src = "image/up.png";
+    } else {
+      dir = "1";
+      src = "image/down.png";
+    }
+
+    $('#' + order_by + '_header_id a').append('<img border=0 data-sort-dir=' + dir + ' src=' + src + ' alt="' + kivi.t8('sort items') + '">');
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action',   value: 'Order/reorder_items' },
+              { name: 'order_by', value: order_by              },
+              { name: 'sort_dir', value: dir                   });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.redisplay_items = function(data) {
+    var old_rows = $('.row_entry').detach();
+    var new_rows = [];
+    $(data).each(function(idx, elt) {
+      new_rows.push(old_rows[elt.old_pos - 1]);
+    });
+    $(new_rows).appendTo($('#row_table_id'));
+    ns.renumber_positions();
+  };
+
+  ns.get_insert_before_item_id = function(wanted_pos) {
+    if (wanted_pos === '') return;
+
+    var insert_before_item_id;
+    // selection by data does not seem to work if data is changed at runtime
+    // var elt = $('.row_entry [data-position="' + wanted_pos + '"]');
+    $('.row_entry').each(function(idx, elt) {
+      if ($(elt).data("position") == wanted_pos) {
+        insert_before_item_id = $(elt).find('[name="orderitem_ids[+]"]').val();
+        return false;
+      }
+    });
+
+    return insert_before_item_id;
+  };
+
+  ns.update_item_input_row = function() {
+    if (!ns.check_cv()) return;
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'Order/update_item_input_row' });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.add_item = function() {
+    if ($('#add_item_parts_id').val() === '') return;
+    if (!ns.check_cv()) return;
+
+    $('#row_table_id thead a img').remove();
+
+    var insert_before_item_id = ns.get_insert_before_item_id($('#add_item_position').val());
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'Order/add_item' },
+              { name: 'insert_before_item_id', value: insert_before_item_id });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.open_multi_items_dialog = function() {
+    if (!ns.check_cv()) return;
+
+    var pp = $("#add_item_parts_id").data("part_picker");
+    pp.o.multiple=1;
+    pp.open_dialog();
+  };
+
+  ns.add_multi_items = function(data) {
+    var insert_before_item_id = ns.get_insert_before_item_id($('#multi_items_position').val());
+    data = data.concat($('#order_form').serializeArray());
+    data.push({ name: 'action', value: 'Order/add_multi_items' },
+              { name: 'insert_before_item_id', value: insert_before_item_id });
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.delete_order_item_row = function(clicked) {
+    var row = $(clicked).parents("tbody").first();
+    $(row).remove();
+
+    ns.renumber_positions();
+    ns.recalc_amounts_and_taxes();
+  };
+
+  ns.row_table_scroll_down = function() {
+    $('#row_table_scroll_id').scrollTop($('#row_table_scroll_id')[0].scrollHeight);
+  };
+
+  ns.show_longdescription_dialog = function(clicked) {
+    var row                 = $(clicked).parents("tbody").first();
+    var position            = $(row).find('[name="position"]').html();
+    var partnumber          = $(row).find('[name="partnumber"]').html();
+    var description_elt     = $(row).find('[name="order.orderitems[].description"]');
+    var longdescription_elt = $(row).find('[name="order.orderitems[].longdescription"]');
+
+    var params = {
+      runningnumber:           position,
+      partnumber:              partnumber,
+      description:             description_elt.val(),
+      default_longdescription: longdescription_elt.val(),
+      set_function:            function(val) {
+        longdescription_elt.val(val);
+      }
+    };
+
+    kivi.SalesPurchase.edit_longdescription_with_params(params);
+  };
+
+  ns.price_chooser_item_row = function(clicked) {
+    if (!ns.check_cv()) return;
+    var row         = $(clicked).parents("tbody").first();
+    var item_id_dom = $(row).find('[name="orderitem_ids[+]"]');
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action',  value: 'Order/price_popup' },
+              { name: 'item_id', value: item_id_dom.val()   });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.set_price_and_source_text = function(item_id, source, descr, price_str, price_editable) {
+    var row        = $('#item_' + item_id).parents("tbody").first();
+    var source_elt = $(row).find('[name="order.orderitems[].active_price_source"]');
+    var button_elt = $(row).find('[name="price_chooser_button"]');
+
+    button_elt.val(button_elt.val().replace(/.*\|/, descr + " |"));
+    source_elt.val(source);
+
+    var editable_div_elt     = $(row).find('[name="editable_price"]');
+    var not_editable_div_elt = $(row).find('[name="not_editable_price"]');
+    if (price_editable == 1 && source === '') {
+      // editable
+      $(editable_div_elt).show();
+      $(not_editable_div_elt).hide();
+      $(editable_div_elt).find(':input').prop("disabled", false);
+      $(not_editable_div_elt).find(':input').prop("disabled", true);
+    } else {
+      // not editable
+      $(editable_div_elt).hide();
+      $(not_editable_div_elt).show();
+      $(editable_div_elt).find(':input').prop("disabled", true);
+      $(not_editable_div_elt).find(':input').prop("disabled", false);
+    }
+
+    if (price_str) {
+      var price_elt = $(row).find('[name="order.orderitems[].sellprice_as_number"]');
+      var html_elt  = $(row).find('[name="sellprice_text"]');
+      price_elt.val(price_str);
+      html_elt.html(price_str);
+    }
+  };
+
+  ns.update_price_source = function(item_id, source, descr, price_str, price_editable) {
+    ns.set_price_and_source_text(item_id, source, descr, price_str, price_editable);
+
+    if (price_str) ns.recalc_amounts_and_taxes();
+    kivi.io.close_dialog();
+  };
+
+  ns.set_discount_and_source_text = function(item_id, source, descr, discount_str, price_editable) {
+    var row        = $('#item_' + item_id).parents("tbody").first();
+    var source_elt = $(row).find('[name="order.orderitems[].active_discount_source"]');
+    var button_elt = $(row).find('[name="price_chooser_button"]');
+
+    button_elt.val(button_elt.val().replace(/\|.*/, "| " + descr));
+    source_elt.val(source);
+
+    var editable_div_elt     = $(row).find('[name="editable_discount"]');
+    var not_editable_div_elt = $(row).find('[name="not_editable_discount"]');
+    if (price_editable == 1 && source === '') {
+      // editable
+      $(editable_div_elt).show();
+      $(not_editable_div_elt).hide();
+      $(editable_div_elt).find(':input').prop("disabled", false);
+      $(not_editable_div_elt).find(':input').prop("disabled", true);
+    } else {
+      // not editable
+      $(editable_div_elt).hide();
+      $(not_editable_div_elt).show();
+      $(editable_div_elt).find(':input').prop("disabled", true);
+      $(not_editable_div_elt).find(':input').prop("disabled", false);
+    }
+
+    if (discount_str) {
+      var discount_elt = $(row).find('[name="order.orderitems[].discount_as_percent"]');
+      var html_elt     = $(row).find('[name="discount_text"]');
+      discount_elt.val(discount_str);
+      html_elt.html(discount_str);
+    }
+  };
+
+  ns.update_discount_source = function(item_id, source, descr, discount_str, price_editable) {
+    ns.set_discount_and_source_text(item_id, source, descr, discount_str, price_editable);
+
+    if (discount_str) ns.recalc_amounts_and_taxes();
+    kivi.io.close_dialog();
+  };
+
+  ns.show_periodic_invoices_config_dialog = function() {
+    if ($('#type').val() !== 'sales_order') return;
+
+    kivi.popup_dialog({
+      url:    'controller.pl?action=Order/show_periodic_invoices_config_dialog',
+      data:   { type:              $('#type').val(),
+                id:                $('#id').val(),
+                config:            $('#order_periodic_invoices_config').val(),
+                customer_id:       $('#order_customer_id').val(),
+                transdate_as_date: $('#order_transdate_as_date').val(),
+                language_id:       $('#language_id').val()
+              },
+      id:     'jq_periodic_invoices_config_dialog',
+      load:   kivi.reinit_widgets,
+      dialog: {
+        title:  kivi.t8('Edit the configuration for periodic invoices'),
+        width:  800,
+        height: 650
+      }
+    });
+    return true;
+  };
+
+  ns.close_periodic_invoices_config_dialog = function() {
+    $('#jq_periodic_invoices_config_dialog').dialog('close');
+  };
+
+  ns.assign_periodic_invoices_config = function() {
+    var data = $('[name="Form"]').serializeArray();
+    data.push({ name: 'type',   value: $('#type').val() },
+              { name: 'action', value: 'Order/assign_periodic_invoices_config' });
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.check_save_active_periodic_invoices = function() {
+    var type = $('#type').val();
+    if (type !== 'sales_order') return true;
+
+    var active = false;
+    $.ajax({
+      url:      'controller.pl',
+      data:     { action: 'Order/get_has_active_periodic_invoices',
+                  type  : type,
+                  id    : $('#id').val(),
+                  config: $('#order_periodic_invoices_config').val(),
+                },
+      method:   "GET",
+      async:    false,
+      dataType: 'text',
+      success:  function(val) {
+        active = val;
+      }
+    });
+
+    if (active == 1) {
+      return confirm(kivi.t8('This sales order has an active configuration for periodic invoices. If you save then all subsequently created invoices will contain those changes as well, but not those that have already been created. Do you want to continue?'));
+    }
+
+    return true;
+  };
+
+  ns.show_vc_details_dialog = function() {
+    if (!ns.check_cv()) return;
+    var vc;
+    var vc_id;
+    var title;
+    if ($('#type').val() == 'sales_order' || $('#type').val() == 'sales_quotation' ) {
+      vc    = 'customer';
+      vc_id = $('#order_customer_id').val();
+      title = kivi.t8('Customer details');
+    } else {
+      vc    = 'vendor';
+      vc_id = $('#order_vendor_id').val();
+      title = kivi.t8('Vendor details');
+    }
+
+    kivi.popup_dialog({
+      url:    'controller.pl',
+      data:   { action: 'Order/show_customer_vendor_details_dialog',
+                type  : $('#type').val(),
+                vc    : vc,
+                vc_id : vc_id
+              },
+      id:     'jq_customer_vendor_details_dialog',
+      dialog: {
+        title:  title,
+        width:  800,
+        height: 650
+      }
+    });
+    return true;
+  };
+
+  ns.update_row_from_master_data = function(clicked) {
+    var row = $(clicked).parents("tbody").first();
+    var item_id_dom = $(row).find('[name="orderitem_ids[+]"]');
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'Order/update_row_from_master_data' });
+    data.push({ name: 'item_ids[]', value: item_id_dom.val() });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.update_all_rows_from_master_data = function() {
+    var item_ids = $.map($('.row_entry'), function(elt) {
+      var item_id = $(elt).find('[name="orderitem_ids[+]"]').val();
+      return { name: 'item_ids[]', value: item_id };
+    });
+
+    if (item_ids.length == 0) {
+      return;
+    }
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'Order/update_row_from_master_data' });
+    data = data.concat(item_ids);
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.show_calculate_qty_dialog = function(clicked) {
+    var row        = $(clicked).parents("tbody").first();
+    var input_id   = $(row).find('[name="order.orderitems[].qty_as_number"]').attr('id');
+    var formula_id = $(row).find('[name="formula[+]"]').attr('id');
+
+    calculate_qty_selection_dialog("", input_id, "", formula_id);
+    return true;
+  };
+
+  ns.edit_custom_shipto = function() {
+    if (!ns.check_cv()) return;
+
+    kivi.SalesPurchase.edit_custom_shipto();
+  };
+
+  ns.purchase_order_check_for_direct_delivery = function() {
+    if ($('#type').val() != 'sales_order') {
+      kivi.submit_form_with_action($('#order_form'), 'Order/purchase_order');
+    }
+
+    var empty = true;
+    var shipto;
+    if ($('#order_shipto_id').val() !== '') {
+      empty = false;
+      shipto = $('#order_shipto_id option:selected').text();
+    } else {
+      $('#shipto_inputs [id^="shipto"]').each(function(idx, elt) {
+        if (!empty)                                     return true;
+        if (/^shipto_to_copy/.test($(elt).prop('id')))  return true;
+        if (/^shiptocp_gender/.test($(elt).prop('id'))) return true;
+        if (/^shiptocvar_/.test($(elt).prop('id')))     return true;
+        if ($(elt).val() !== '') {
+          empty = false;
+          return false;
+        }
+      });
+      var shipto_elements = [];
+      $([$('#shiptoname').val(), $('#shiptostreet').val(), $('#shiptozipcode').val(), $('#shiptocity').val()]).each(function(idx, elt) {
+        if (elt !== '') shipto_elements.push(elt);
+      });
+      shipto = shipto_elements.join('; ');
+    }
+
+    var use_it = false;
+    if (!empty) {
+      ns.direct_delivery_dialog(shipto);
+    } else {
+      kivi.submit_form_with_action($('#order_form'), 'Order/purchase_order');
+    }
+  };
+
+  ns.direct_delivery_callback = function(accepted) {
+    $('#direct-delivery-dialog').dialog('close');
+
+    if (accepted) {
+      $('<input type="hidden" name="use_shipto">').appendTo('#order_form').val('1');
+    }
+
+    kivi.submit_form_with_action($('#order_form'), 'Order/purchase_order');
+  };
+
+  ns.direct_delivery_dialog = function(shipto) {
+    $('#direct-delivery-dialog').remove();
+
+    var text1 = kivi.t8('You have entered or selected the following shipping address for this customer:');
+    var text2 = kivi.t8('Do you want to carry this shipping address over to the new purchase order so that the vendor can deliver the goods directly to your customer?');
+    var html  = '<div id="direct-delivery-dialog"><p>' + text1 + '</p><p>' + shipto + '</p><p>' + text2 + '</p>';
+    html      = html + '<hr><p>';
+    html      = html + '<input type="button" value="' + kivi.t8('Yes') + '" size="30" onclick="kivi.Order.direct_delivery_callback(true)">';
+    html      = html + '&nbsp;';
+    html      = html + '<input type="button" value="' + kivi.t8('No')  + '" size="30" onclick="kivi.Order.direct_delivery_callback(false)">';
+    html      = html + '</p></div>';
+    $(html).hide().appendTo('#order_form');
+
+    kivi.popup_dialog({id: 'direct-delivery-dialog',
+                       dialog: {title:  kivi.t8('Carry over shipping address'),
+                                height: 300,
+                                width:  500 }});
+  };
+
+  ns.follow_up_window = function() {
+    var id   = $('#id').val();
+    var type = $('#type').val();
+
+    var number_info = '';
+    if ($('#type').val() == 'sales_order' || $('#type').val() == 'purchase_order') {
+      number_info = $('#order_ordnumber').val();
+    } else if ($('#type').val() == 'sales_quotation' || $('#type').val() == 'request_quotation') {
+      number_info = $('#order_quonumber').val();
+    }
+
+    var name_info = '';
+    if ($('#type').val() == 'sales_order' || $('#type').val() == 'sales_quotation') {
+      name_info = $('#order_customer_id_name').val();
+    } else if ($('#type').val() == 'purchase_order' || $('#type').val() == 'request_quotation') {
+      name_info = $('#order_vendor_id_name').val();
+    }
+
+    var info = '';
+    if (number_info !== '') { info += ' (' + number_info + ')' }
+    if (name_info   !== '') { info += ' (' + name_info + ')' }
+
+    if (!$('#follow_up_rowcount').length) {
+      $('<input type="hidden" name="follow_up_rowcount"        id="follow_up_rowcount">').appendTo('#order_form');
+      $('<input type="hidden" name="follow_up_trans_id_1"      id="follow_up_trans_id_1">').appendTo('#order_form');
+      $('<input type="hidden" name="follow_up_trans_type_1"    id="follow_up_trans_type_1">').appendTo('#order_form');
+      $('<input type="hidden" name="follow_up_trans_info_1"    id="follow_up_trans_info_1">').appendTo('#order_form');
+      $('<input type="hidden" name="follow_up_trans_subject_1" id="follow_up_trans_subject_1">').appendTo('#order_form');
+    }
+    $('#follow_up_rowcount').val(1);
+    $('#follow_up_trans_id_1').val(id);
+    $('#follow_up_trans_type_1').val(type);
+    $('#follow_up_trans_info_1').val(info);
+    $('#follow_up_trans_subject_1').val($('#order_transaction_description').val());
+
+    follow_up_window();
+  };
+
+  ns.create_part = function() {
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'Order/create_part' });
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.check_transport_cost_article_presence = function() {
+    var $form          = $('#order_form');
+    var wanted_part_id = $form.data('transport-cost-reminder-article-id');
+
+    if (!wanted_part_id) return true
+
+    var id_arr = $('[name="order.orderitems[].parts_id"]').map(function() { return this.value; }).get();
+    id_arr = $.grep(id_arr, function(elt) {
+      return ((elt*1) === wanted_part_id);
+    });
+
+    if (id_arr.length) return true;
+
+    var description = $form.data('transport-cost-reminder-article-description');
+    return confirm(kivi.t8("The transport cost article '#1' is missing. Do you want to continue anyway?", [ description ]));
+  };
+
+  ns.check_cusordnumber_presence = function() {
+    if ($('#order_cusordnumber').val() === '') {
+      return confirm(kivi.t8('The customer order number is missing. Do you want to continue anyway?'));
+    }
+    return true;
+  };
+
+  ns.load_phone_note = function(id, subject, body) {
+    $('#phone_note_edit_text').html(kivi.t8('Edit note'));
+    $('#phone_note_id').val(id);
+    $('#phone_note_subject').val(subject);
+    $('#phone_note_body').val(body);
+    $('#phone_note_delete_button').show();
+  };
+
+  ns.cancel_phone_note = function() {
+    $('#phone_note_edit_text').html(kivi.t8('Add note'));
+    $('#phone_note_id').val('');
+    $('#phone_note_subject').val('');
+    $('#phone_note_body').val('');
+    $('#phone_note_delete_button').hide();
+  };
+
+  ns.save_phone_note = function() {
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'Order/save_phone_note' });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.delete_phone_note = function() {
+    if ($('#phone_note_id').val() === '') return;
+
+    var data = $('#order_form').serializeArray();
+    data.push({ name: 'action', value: 'Order/delete_phone_note' });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+});
+
+$(function() {
+  if ($('#type').val() == 'sales_order' || $('#type').val() == 'sales_quotation' ) {
+    $('#order_customer_id').change(kivi.Order.reload_cv_dependent_selections);
+  } else {
+    $('#order_vendor_id').change(kivi.Order.reload_cv_dependent_selections);
+  }
+
+  $('#order_currency_id').change(kivi.Order.update_exchangerate);
+  $('#order_transdate_as_date').change(kivi.Order.update_exchangerate);
+  $('#order_exchangerate_as_null_number').change(kivi.Order.exchangerate_changed);
+
+  $('#add_item_parts_id').on('set_item:PartPicker', function() {
+    kivi.Order.update_item_input_row();
+  });
+
+  $('.add_item_input').keydown(function(event) {
+    if (event.keyCode == 13) {
+      event.preventDefault();
+      kivi.Order.add_item();
+      return false;
+    }
+  });
+
+  kivi.Order.init_row_handlers();
+
+  $('#row_table_id').on('sortstop', function(event, ui) {
+    $('#row_table_id thead a img').remove();
+    kivi.Order.renumber_positions();
+  });
+
+  $('#expand_all').on('click', function(event) {
+    event.preventDefault();
+    if ($('#expand_all').data('expanded') == 1) {
+      $('#expand_all').data('expanded', 0);
+      $('#expand_all').attr('src', 'image/expand.svg');
+      $('#expand_all').attr('alt', kivi.t8('Show all details'));
+      $('#expand_all').attr('title', kivi.t8('Show all details'));
+      $('.row_entry').each(function(idx, elt) {
+        kivi.Order.hide_second_row(elt);
+      });
+    } else {
+      $('#expand_all').data('expanded', 1);
+      $('#expand_all').attr('src', "image/collapse.svg");
+      $('#expand_all').attr('alt', kivi.t8('Hide all details'));
+      $('#expand_all').attr('title', kivi.t8('Hide all details'));
+      kivi.Order.load_all_second_rows();
+      $('.row_entry').each(function(idx, elt) {
+        kivi.Order.show_second_row(elt);
+      });
+    }
+    return false;
+  });
+
+  $('.reformat_number_as_null_number').change(kivi.Order.reformat_number_as_null_number);
+
+});
diff --git a/js/kivi.Part.js b/js/kivi.Part.js
new file mode 100644 (file)
index 0000000..6207569
--- /dev/null
@@ -0,0 +1,810 @@
+namespace('kivi.Part', function(ns) {
+  'use strict';
+
+  ns.open_history_popup = function() {
+    var id = $("#part_id").val();
+    kivi.popup_dialog({
+      url:    'controller.pl?action=Part/history&part.id=' + id,
+      dialog: { title: kivi.t8('History') },
+    });
+  };
+
+  ns.save = function() {
+    var data = $('#ic').serializeArray();
+    data.push({ name: 'action', value: 'Part/save' });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.use_as_new = function() {
+    var oldid = $("#part_id").val();
+    $('#ic').attr('action', 'controller.pl?action=Part/use_as_new&old_id=' + oldid);
+    $('#ic').submit();
+  };
+
+  ns.delete = function() {
+    var data = $('#ic').serializeArray();
+    data.push({ name: 'action', value: 'Part/delete' });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.reformat_number = function(event) {
+    $(event.target).val(kivi.format_amount(kivi.parse_amount($(event.target).val()), -2));
+  };
+
+  ns.set_tab_active_by_index = function (index) {
+    $("#ic_tabs").tabs({active: index});
+  };
+
+  ns.set_tab_active_by_name= function (name) {
+    var index = $('#ic_tabs a[href=#' + name + ']').parent().index();
+    ns.set_tab_active_by_index(index);
+  };
+
+  ns.reorder_items = function(order_by) {
+    var dir = $('#' + order_by + '_header_id a img').attr("data-sort-dir");
+    var part_type = $("#part_part_type").val();
+
+    var data;
+    if (part_type === 'assortment') {
+      $('#assortment thead a img').remove();
+      data = $('#assortment :input').serializeArray();
+    } else if ( part_type === 'assembly') {
+      $('#assembly thead a img').remove();
+      data = $('#assembly :input').serializeArray();
+    }
+
+    var src;
+    if (dir == "1") {
+      dir = "0";
+      src = "image/up.png";
+    } else {
+      dir = "1";
+      src = "image/down.png";
+    }
+
+    $('#' + order_by + '_header_id a').append('<img border=0 data-sort-dir=' + dir + ' src=' + src + ' alt="' + kivi.t8('sort items') + '">');
+
+    data.push(
+      { name: 'action',    value: 'Part/reorder_items' },
+      { name: 'order_by',  value: order_by             },
+      { name: 'part_type', value: part_type            },
+      { name: 'sort_dir',  value: dir                  }
+    );
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.assortment_recalc = function() {
+    var data = $('#assortment :input').serializeArray();
+    data.push(
+      { name: 'action', value: 'Part/update_item_totals' },
+      { name: 'part_type', value: 'assortment'           }
+    );
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.assembly_recalc = function() {
+    var data = $('#assembly :input').serializeArray();
+    data.push(
+      { name: 'action',    value: 'Part/update_item_totals' },
+      { name: 'part_type', value: 'assembly'                }
+    );
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.set_assortment_sellprice = function() {
+    $("#part_sellprice_as_number").val($("#items_sellprice_sum").html());
+    // ns.set_tab_active_by_name('basic_data');
+    // $("#part_sellprice_as_number").focus();
+  };
+
+  ns.set_assortment_lsg_sellprice = function() {
+    $("#items_lsg_sellprice_sum_basic").closest('td').find('input').val($("#items_lsg_sellprice_sum").html());
+  };
+
+  ns.set_assortment_douglas_sellprice = function() {
+    $("#items_douglas_sellprice_sum_basic").closest('td').find('input').val($("#items_douglas_sellprice_sum").html());
+  };
+
+  ns.set_assortment_lastcost = function() {
+    $("#part_lastcost_as_number").val($("#items_lastcost_sum").html());
+    // ns.set_tab_active_by_name('basic_data');
+    // $("#part_lastcost_as_number").focus();
+  };
+
+  ns.set_assembly_sellprice = function() {
+    $("#part_sellprice_as_number").val($("#items_sellprice_sum").html());
+    // ns.set_tab_active_by_name('basic_data');
+    // $("#part_sellprice_as_number").focus();
+  };
+
+  ns.renumber_positions = function() {
+    var part_type = $("#part_part_type").val();
+    var rows;
+    if (part_type === 'assortment') {
+      rows = $('.assortment_item_row [name="position"]');
+    } else if ( part_type === 'assembly') {
+      rows = $('.assembly_item_row [name="position"]');
+    }
+    $(rows).each(function(idx, elt) {
+      $(elt).html(idx+1);
+      var row = $(elt).closest('tr');
+      if ( idx % 2 === 0 ) {
+        if ( row.hasClass('listrow1') ) {
+          row.removeClass('listrow1');
+          row.addClass('listrow0');
+        }
+      } else {
+        if ( row.hasClass('listrow0') ) {
+          row.removeClass('listrow0');
+          row.addClass('listrow1');
+        }
+      }
+    });
+  };
+
+  ns.delete_item_row = function(clicked) {
+    var row = $(clicked).closest('tr');
+    $(row).remove();
+    var part_type = $("#part_part_type").val();
+    ns.renumber_positions();
+    if (part_type === 'assortment') {
+      ns.assortment_recalc();
+    } else if ( part_type === 'assembly') {
+      ns.assembly_recalc();
+    }
+  };
+
+  ns.add_assortment_item = function() {
+    if ($('#assortment_picker').val() === '') return;
+
+    $('#row_table_id thead a img').remove();
+
+    var data = $('#assortment :input').serializeArray();
+    data.push(
+      { name: 'action', value: 'Part/add_assortment_item' },
+      { name: 'part.id', value: $('#part_id').val()       },
+      { name: 'part.part_type', value: 'assortment'       }
+    );
+    $('#assortment_picker').data('part_picker').clear();
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.add_assembly_item = function() {
+    if ($('#assembly_picker').val() === '') return;
+
+    var data = $('#assembly :input').serializeArray();
+    data.push(
+      { name: 'action', value: 'Part/add_assembly_item' },
+      { name: 'part.id', value: $("#part_id").val()     },
+      { name: 'part.part_type', value: 'assembly'       }
+    );
+    $('#assembly_picker').data('part_picker').clear();
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.set_multi_assembly_items = function(data) {
+    data.push({ name: 'part.id',        value: $('#part_id').val() });
+    data.push({ name: 'part.part_type', value: $('#part_part_type').val() });
+    $.post("controller.pl?action=Part/add_multi_assembly_items", data, kivi.eval_json_result);
+  };
+
+  ns.set_multi_assortment_items = function(data) {
+    data.push({ name: 'part.id', value: $('#part_id').val() });
+    data.push({ name: 'part.part_type', value: $('#part_part_type').val() });
+    $.post("controller.pl?action=Part/add_multi_assortment_items", data, kivi.eval_json_result);
+  };
+
+  ns.close_picker_dialogs = function() {
+    $('.part_autocomplete').each(function(_, e) {
+      var picker = $(e).data('part_picker');
+      if (picker && picker.dialog) picker.close_dialog();
+    });
+  };
+
+  ns.redisplay_items = function(data) {
+    var old_rows;
+    var part_type = $("#part_part_type").val();
+    if (part_type === 'assortment') {
+      old_rows = $('.assortment_item_row').detach();
+    } else if ( part_type === 'assembly') {
+      old_rows = $('.assembly_item_row').detach();
+    }
+    var new_rows = [];
+    $(data).each(function(idx, elt) {
+      new_rows.push(old_rows[elt.old_pos - 1]);
+    });
+    if (part_type === 'assortment') {
+      $(new_rows).appendTo($('#assortment_items'));
+    } else if ( part_type === 'assembly') {
+      $(new_rows).appendTo($('#assembly_items'));
+    }
+    ns.renumber_positions();
+  };
+
+  ns.focus_last_assortment_input = function () {
+    $("#assortment_items tr:last").find('input[type=text]').filter(':visible:first').focus();
+  };
+
+  ns.focus_last_assembly_input = function () {
+    $("#assembly_rows tr:last").find('input[type=text]').filter(':visible:first').focus();
+  };
+
+  // makemodel
+  ns.makemodel_renumber_positions = function() {
+    $('.makemodel_row [name="position"]').each(function(idx, elt) {
+      $(elt).html(idx+1);
+    });
+  };
+
+  ns.delete_makemodel_row = function(clicked) {
+    var row = $(clicked).closest('tr');
+    $(row).remove();
+
+    ns.makemodel_renumber_positions();
+  };
+
+  ns.add_makemodel_row = function() {
+    if ($('#add_makemodel').val() === '') return;
+
+    var data = $('#makemodel_table :input').serializeArray();
+    data.push({ name: 'action', value: 'Part/add_makemodel_row' });
+    $('#add_makemodel').data('customer_vendor_picker').clear();
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.focus_last_makemodel_input = function () {
+    $("#makemodel_rows tr:last").find('input[type=text]').filter(':visible:first').focus();
+  };
+
+
+  // customerprice
+  ns.customerprice_renumber_positions = function() {
+    $('.customerprice_row [name="position"]').each(function(idx, elt) {
+      $(elt).html(idx+1);
+    });
+  };
+
+  ns.delete_customerprice_row = function(clicked) {
+    var row = $(clicked).closest('tr');
+    $(row).remove();
+
+    ns.customerprice_renumber_positions();
+  };
+
+  ns.add_customerprice_row = function() {
+    if ($('#add_customerprice').val() === '') return;
+
+    var data = $('#customerprice_table :input').serializeArray();
+    data.push({ name: 'action', value: 'Part/add_customerprice_row' });
+    $('#add_customerprice').data('customer_vendor_picker').clear();
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.focus_last_customerprice_input = function () {
+    $("#customerprice_rows tr:last").find('input[type=text]').filter(':visible:first').focus();
+  };
+
+
+  ns.reload_bin_selection = function() {
+    $.post("controller.pl", { action: 'Part/warehouse_changed', warehouse_id: function(){ return $('#part_warehouse_id').val(); } },   kivi.eval_json_result);
+  };
+
+  var KEY = {
+    TAB:       9,
+    ENTER:     13,
+    SHIFT:     16,
+    CTRL:      17,
+    ALT:       18,
+    ESCAPE:    27,
+    PAGE_UP:   33,
+    PAGE_DOWN: 34,
+    LEFT:      37,
+    UP:        38,
+    RIGHT:     39,
+    DOWN:      40,
+  };
+
+  ns.Picker = function($real, options) {
+    var self = this;
+    this.o = $.extend(true, {
+      limit: 20,
+      delay: 50,
+      action: {
+        commit_none: function(){ },
+        commit_one:  function(){ $('#update_button').click(); },
+        commit_many: function(){ self.open_dialog(); }
+      },
+      multiple_limit: 100
+    }, $real.data('part-picker-data'), options);
+    this.$real              = $real;
+    this.real_id            = $real.attr('id');
+    this.last_real          = $real.val();
+    this.$dummy             = $($real.siblings()[0]);
+    this.autocomplete_open  = false;
+    this.state              = this.STATES.PICKED;
+    this.last_dummy         = this.$dummy.val();
+    this.timer              = undefined;
+    this.dialog             = undefined;
+    this.multiple_default   = this.o.multiple;
+
+    this.init();
+  };
+
+  ns.Picker.prototype = {
+    CLASSES: {
+      PICKED:       'partpicker-picked',
+      UNDEFINED:    'partpicker-undefined',
+    },
+    ajax_data: function(term) {
+      var data = {
+        current:  this.$real.val(),
+      };
+
+      if (this.o.part_type)
+        data['filter.part_type'] = this.o.part_type.split(',');
+
+      if (this.o.status) {
+        if (this.o.status == 'active')   data['filter.obsolete'] = 0;
+        if (this.o.status == 'obsolete') data['filter.obsolete'] = 1;
+      } else
+        data['filter.obsolete'] = 0;
+
+      if (this.o.classification_id)
+        data['filter.classification_id:any'] = this.o.classification_id.replaceAll(',', ' ');
+
+      if (this.o.unit)
+        data['filter.unit'] = this.o.unit.split(',');
+
+      if (this.o.convertible_unit)
+        data['filter.unit_obj.convertible_to'] = this.o.convertible_unit;
+
+      var filter_name = 'all';
+      if (this.o.with_makemodel) {
+        filter_name = 'all_with_makemodel';
+      }
+      if (this.o.with_customer_partnumber) {
+        filter_name = 'all_with_customer_partnumber';
+      }
+      data['filter.' + filter_name + ':substr:multi::ilike'] = term;
+
+      return data;
+    },
+    set_item: function(item) {
+      var self = this;
+      if (item.id) {
+        this.$real.val(item.id);
+        // autocomplete ui has name, use the value for ajax items, which contains displayable_name
+        this.$dummy.val(item.name ? item.name : item.value);
+      } else {
+        this.$real.val('');
+        this.$dummy.val('');
+      }
+      this.state      = this.STATES.PICKED;
+      this.last_real  = this.$real.val();
+      this.last_dummy = this.$dummy.val();
+      this.$real.trigger('change');
+
+      if (this.o.fat_set_item && item.id) {
+        $.ajax({
+          url: 'controller.pl?action=Part/show.json',
+          data: { 'part.id': item.id },
+          success: function(rsp) {
+            self.$real.trigger('set_item:PartPicker', rsp);
+          },
+        });
+      } else {
+        this.$real.trigger('set_item:PartPicker', item);
+      }
+      this.annotate_state();
+    },
+    set_multi_items: function(data) {
+      this.run_action(this.o.action.set_multi_items, [ data ]);
+    },
+    make_defined_state: function() {
+      if (this.state == this.STATES.PICKED) {
+        this.annotate_state();
+        return true;
+      } else if (this.state == this.STATES.UNDEFINED && this.$dummy.val() === '') {
+        this.set_item({});
+      } else {
+        this.set_item({ id: this.last_real, name: this.last_dummy });
+      }
+      this.annotate_state();
+    },
+    annotate_state: function() {
+      if (this.state == this.STATES.PICKED)
+        this.$dummy.removeClass(this.STATES.UNDEFINED).addClass(this.STATES.PICKED);
+      else if (this.state == this.STATES.UNDEFINED && this.$dummy.val() === '')
+        this.$dummy.removeClass(this.STATES.UNDEFINED).addClass(this.STATES.PICKED);
+      else {
+        this.$dummy.addClass(this.STATES.UNDEFINED).removeClass(this.STATES.PICKED);
+      }
+    },
+    handle_changed_text: function(callbacks) {
+      var self = this;
+      $.ajax({
+        url: 'controller.pl?action=Part/ajax_autocomplete',
+        dataType: "json",
+        data: $.extend( self.ajax_data(self.$dummy.val()), { prefer_exact: 1 } ),
+        success: function (data) {
+          if (data.length == 1) {
+            self.set_item(data[0]);
+            if (callbacks && callbacks.match_one) self.run_action(callbacks.match_one, [ data[0] ]);
+          } else if (data.length > 1) {
+            self.state = self.STATES.UNDEFINED;
+            if (callbacks && callbacks.match_many) self.run_action(callbacks.match_many, [ data ]);
+          } else {
+            self.state = self.STATES.UNDEFINED;
+            if (callbacks && callbacks.match_none) self.run_action(callbacks.match_none, [ self, self.$dummy.val() ]);
+          }
+          self.annotate_state();
+        }
+      });
+    },
+    /*  In case users are impatient and want to skip ahead:
+     *  Capture <enter> key events and check if it's a unique hit.
+     *  If it is, go ahead and assume it was selected. If it wasn't don't do
+     *  anything so that autocompletion kicks in.  For <tab> don't prevent
+     *  propagation. It would be nice to catch it, but javascript is too stupid
+     *  to fire a tab event later on, so we'd have to reimplement the "find
+     *  next active element in tabindex order and focus it".
+     */
+    /* note:
+     *  event.which does not contain tab events in keypressed in firefox but will report 0
+     *  chrome does not fire keypressed at all on tab or escape
+     */
+    handle_keydown: function(event) {
+      var self = this;
+      if (event.which == KEY.ENTER || event.which == KEY.TAB) {
+        // if string is empty assume they want to delete
+        if (self.$dummy.val() === '') {
+          self.set_item({});
+          return true;
+        } else if (self.state == self.STATES.PICKED) {
+          if (self.o.action.commit_one) {
+            self.run_action(self.o.action.commit_one);
+          }
+          return true;
+        }
+        if (event.which == KEY.TAB) {
+          event.preventDefault();
+          self.handle_changed_text();
+        }
+        if (event.which == KEY.ENTER) {
+          event.preventDefault();
+          self.handle_changed_text({
+            match_none: self.o.action.commit_none,
+            match_one:  self.o.action.commit_one,
+            match_many: self.o.action.commit_many
+          });
+          return false;
+        }
+      } else if (event.which == KEY.DOWN && !self.autocomplete_open) {
+        var old_options = self.$dummy.autocomplete('option');
+        self.$dummy.autocomplete('option', 'minLength', 0);
+        self.$dummy.autocomplete('search', self.$dummy.val());
+        self.$dummy.autocomplete('option', 'minLength', old_options.minLength);
+      } else if ((event.which != KEY.SHIFT) && (event.which != KEY.CTRL) && (event.which != KEY.ALT)) {
+        self.state = self.STATES.UNDEFINED;
+      }
+    },
+    open_dialog: function() {
+      if (this.o.multiple) {
+        this.o.multiple = this.multiple_default;
+        this.dialog = new ns.PickerMultiPopup(this);
+      } else {
+        this.dialog = new ns.PickerPopup(this);
+      }
+    },
+    close_dialog: function() {
+      this.dialog.close_dialog();
+      this.dialog = undefined;
+    },
+    init: function() {
+      var self = this;
+      this.$dummy.autocomplete({
+        source: function(req, rsp) {
+          $.ajax($.extend(self.o, {
+            url:      'controller.pl?action=Part/ajax_autocomplete',
+            dataType: "json",
+            data:     self.ajax_data(req.term),
+            success:  function (data){ rsp(data); }
+          }));
+        },
+        select: function(event, ui) {
+          self.set_item(ui.item);
+          if (self.o.action.commit_one) {
+            self.run_action(self.o.action.commit_one);
+          }
+        },
+        search: function(event) {
+          if ((event.which == KEY.SHIFT) || (event.which == KEY.CTRL) || (event.which == KEY.ALT))
+            event.preventDefault();
+        },
+        open: function() {
+          self.autocomplete_open = true;
+        },
+        close: function() {
+          self.autocomplete_open = false;
+        }
+      });
+      this.$dummy.keydown(function(event){ self.handle_keydown(event); });
+      this.$dummy.on('paste', function(){
+        setTimeout(function() {
+          self.handle_changed_text();
+        }, 1);
+      });
+      this.$dummy.blur(function(){
+        window.clearTimeout(self.timer);
+        self.timer = window.setTimeout(function() { self.annotate_state(); }, 100);
+      });
+
+      var popup_button = $('<span>').addClass('ppp_popup_button');
+      this.$dummy.after(popup_button);
+      popup_button.click(function() { self.open_dialog(); });
+    },
+    run_action: function(code, args) {
+      if (typeof code === 'function')
+        code.apply(this, args);
+      else
+        kivi.run(code, args);
+    },
+    clear: function() {
+      this.set_item({});
+    }
+  };
+  ns.Picker.prototype.STATES = {
+    PICKED:    ns.Picker.prototype.CLASSES.PICKED,
+    UNDEFINED: ns.Picker.prototype.CLASSES.UNDEFINED
+  };
+
+  ns.PickerPopup = function(pp) {
+    this.timer = undefined;
+    this.pp    = pp;
+    this.open_dialog();
+  };
+
+  ns.PickerPopup.prototype = {
+    open_dialog: function() {
+      var self = this;
+      kivi.popup_dialog({
+        url: 'controller.pl?action=Part/part_picker_search',
+        data: $.extend({
+          real_id: self.pp.real_id,
+        }, self.pp.ajax_data(this.pp.$dummy.val())),
+        id: 'part_selection',
+        dialog: {
+          title: kivi.t8('Part picker'),
+          width: 800,
+          height: 800,
+        },
+        load: function() { self.init_search(); }
+      });
+      window.clearTimeout(this.timer);
+      return true;
+    },
+    init_search: function() {
+      var self = this;
+      $('#part_picker_filter').keypress(function(e) { self.result_timer(e); }).focus();
+      $('#no_paginate').change(function() { self.update_results(); });
+      this.update_results();
+    },
+    update_results: function() {
+      var self = this;
+      $.ajax({
+        url: 'controller.pl?action=Part/part_picker_result',
+        data: $.extend({
+          no_paginate: $('#no_paginate').prop('checked') ? 1 : 0,
+        }, self.pp.ajax_data(function(){
+          var val = $('#part_picker_filter').val();
+          return val === undefined ? '' : val;
+        })),
+        success: function(data){
+          $('#part_picker_result').html(data);
+          self.init_results();
+        }
+      });
+    },
+    init_results: function() {
+      var self = this;
+      $('div.part_picker_part').each(function(){
+        $(this).click(function(){
+          self.pp.set_item({
+            id:   $(this).children('input.part_picker_id').val(),
+            name: $(this).children('input.part_picker_description').val(),
+            classification_id: $(this).children('input.part_picker_classification_id').val(),
+            ean:  $(this).children('input.part_picker_ean').val(),
+            unit: $(this).children('input.part_picker_unit').val(),
+            partnumber:  $(this).children('input.part_picker_partnumber').val(),
+            description: $(this).children('input.part_picker_description').val(),
+          });
+          self.close_dialog();
+          self.pp.$dummy.focus();
+          return true;
+        });
+      });
+      $('#part_selection').keydown(function(e){
+        if (e.which == KEY.ESCAPE) {
+          self.close_dialog();
+          self.pp.$dummy.focus();
+        }
+      });
+    },
+    result_timer: function(event) {
+      var self = this;
+      if (!$('no_paginate').prop('checked')) {
+        if (event.keyCode == KEY.PAGE_UP) {
+          $('#part_picker_result a.paginate-prev').click();
+          return;
+        }
+        if (event.keyCode == KEY.PAGE_DOWN) {
+          $('#part_picker_result a.paginate-next').click();
+          return;
+        }
+      }
+      window.clearTimeout(this.timer);
+      if (event.which == KEY.ENTER) {
+        self.update_results();
+      } else {
+        this.timer = window.setTimeout(function() { self.update_results(); }, 100);
+      }
+    },
+    close_dialog: function() {
+      $('#part_selection').dialog('close');
+    }
+  };
+
+  ns.PickerMultiPopup = function(pp) {
+    this.pp       = pp;
+    this.callback = 'Part/add_multi_' + this.pp.o.part_type + '_items';
+    this.open_dialog();
+  };
+
+  ns.PickerMultiPopup.prototype = {
+    open_dialog: function() {
+      var self = this;
+      $('#row_table_id thead a img').remove();
+
+      kivi.popup_dialog({
+        url: 'controller.pl?action=Part/show_multi_items_dialog',
+        data: $.extend({
+          real_id: self.pp.real_id,
+          show_pos_input: self.pp.o.multiple_pos_input,
+        }, self.pp.ajax_data(this.pp.$dummy.val())),
+        id: 'jq_multi_items_dialog',
+        dialog: {
+          title: kivi.t8('Add multiple items'),
+          width:  800,
+          height: 800
+        },
+        load: function() {
+          self.init_search();
+        }
+      });
+      return true;
+    },
+    init_search: function() {
+      var self = this;
+      $('#multi_items_filter_table input, #multi_items_filter_table select').keydown(function(event) {
+        if(event.which == KEY.ENTER) {
+          event.preventDefault();
+          self.update_results();
+          return false;
+        }
+      });
+
+      $('#multi_items_filter_all_substr_multi_ilike').focus();
+      $('#multi_items_filter_button').click(function(){ self.update_results(); });
+      $('#multi_items_filter_reset').click(function(){ $("#multi_items_form").resetForm(); });
+      $('#continue_button').click(function(){ self.add_multi_items(); });
+    },
+    update_results: function() {
+      var self = this;
+      var data = $('#multi_items_form').serializeArray();
+      data.push({ name: 'type',  value: self.pp.type });
+      data.push({ name: 'limit', value: self.pp.o.multiple_limit });
+      var ppdata = self.pp.ajax_data(function(){
+        var val = $('#multi_items_filter').val();
+        return val === undefined ? '' : val;
+      });
+      $.each(Object.keys(ppdata), function() {data.push({ name: 'multi_items.' + this, value: ppdata[this]});});
+      $.ajax({
+        url: 'controller.pl?action=Part/multi_items_update_result',
+        data: data,
+        method: 'post',
+        success: function(data){
+          $('#multi_items_result').html(data);
+          self.init_results();
+          self.enable_continue();
+        }
+      });
+    },
+    set_qty_to_one: function(clicked) {
+      if ($(clicked).val() === '') {
+        $(clicked).val(kivi.format_amount(1.00, -2));
+      }
+      $(clicked).select();
+    },
+    init_results: function() {
+      var self = this;
+      $('#multi_items_all_qty').change(function(event){
+        $('.multi_items_qty').val($(event.target).val());
+      });
+      $('.multi_items_qty').click(function(){ self.set_qty_to_one(this); });
+    },
+    result_timer: function() {
+    },
+    close_dialog: function() {
+      $('#jq_multi_items_dialog').dialog('close');
+    },
+    disable_continue: function() {
+      $('#multi_items_result input, #multi_items_position').off("keydown");
+      $('#continue_button').prop('disabled', true);
+    },
+    enable_continue: function() {
+      var self = this;
+      $('#multi_items_result input, #multi_items_position').keydown(function(event) {
+        if(event.keyCode == KEY.ENTER) {
+          event.preventDefault();
+          self.add_multi_items();
+          return false;
+        }
+      });
+      $('#continue_button').prop('disabled', false);
+    },
+    add_multi_items: function() {
+      // rows at all
+      var n_rows = $('.multi_items_qty').length;
+      if ( n_rows === 0) { return; }
+
+      // filled rows
+      n_rows = $('.multi_items_qty').filter(function() {
+        return $(this).val().length > 0;
+      }).length;
+      if (n_rows === 0) { return; }
+
+      this.disable_continue();
+
+      var data = $('#multi_items_form').serializeArray();
+      this.pp.set_multi_items(data);
+    }
+  };
+
+  ns.reinit_widgets = function() {
+    kivi.run_once_for('input.part_autocomplete', 'part_picker', function(elt) {
+      if (!$(elt).data('part_picker'))
+        $(elt).data('part_picker', new kivi.Part.Picker($(elt)));
+    });
+
+    kivi.run_once_for('#customerprice_rows', 'customerprice_row_sort_renumber', function(elt) {
+      $(elt).on('sortstop', kivi.Part.customerprice_renumber_positions);
+    });
+
+    kivi.run_once_for('#makemodel_rows', 'makemodel_row_sort_renumber', function(elt) {
+      $(elt).on('sortstop', kivi.Part.makemodel_renumber_positions);
+    });
+  };
+
+  ns.init = function() {
+    ns.reinit_widgets();
+  };
+
+  $(function(){
+    $('#ic').on('focusout', '.reformat_number', function(event) {
+      ns.reformat_number(event);
+    });
+
+    $('#part_warehouse_id').change(kivi.Part.reload_bin_selection);
+
+    ns.init();
+  });
+});
index d3c6a2a..aba5987 100644 (file)
@@ -1,4 +1,5 @@
 namespace('kivi.PriceRule', function(ns) {
+  "use strict";
 
   ns.add_new_row = function (type) {
     var data = {
@@ -6,14 +7,14 @@ namespace('kivi.PriceRule', function(ns) {
       type: type
     };
     $.post('controller.pl', data, kivi.eval_json_result);
-  }
+  };
 
   ns.open_price_type_help_popup = function() {
     kivi.popup_dialog({
       url:    'controller.pl?action=PriceRule/price_type_help',
       dialog: { title: kivi.t8('Price Types') },
     });
-  }
+  };
 
   ns.on_change_filter_type = function() {
     var val = $('#price_rule_filter_type').val();
@@ -27,11 +28,11 @@ namespace('kivi.PriceRule', function(ns) {
       $('#price_rule_filter_vendor_tr').hide();
       $('#price_rule_filter_customer_tr').show();
     }
-    if (val == '') {
+    if (val === '') {
       $('#price_rule_filter_customer_tr').show();
       $('#price_rule_filter_vendor_tr').show();
     }
-  }
+  };
 
   ns.inline_report = function(target, source, data){
     $.ajax({
@@ -54,7 +55,7 @@ namespace('kivi.PriceRule', function(ns) {
       ns.inline_report('#price_rules_customer_report', 'controller.pl', { action: 'PriceRule/list', 'filter.item_type_matches[].part': id, 'filter.type': 'customer', inline: 1 });
       ns.inline_report('#price_rules_vendor_report', 'controller.pl', { action: 'PriceRule/list', 'filter.item_type_matches[].part': id, 'filter.type': 'vendor', inline: 1 });
     }, 200);
-  }
+  };
 
   $(function() {
     $('#price_rule_item_add').click(function() {
diff --git a/js/kivi.QuickSearch.js b/js/kivi.QuickSearch.js
new file mode 100644 (file)
index 0000000..3451b7f
--- /dev/null
@@ -0,0 +1,55 @@
+namespace('kivi', function(k){
+  'use strict';
+  k.QuickSearch = function($real, options) {
+    if ($real.data("quick_search"))
+      return $real.data("quick_search");
+
+    var KEY = {
+      ENTER:     13,
+    };
+    var o = $.extend({
+      limit: 20,
+      delay: 50,
+    }, options);
+
+    function send_query(action, term, id, success) {
+      var data = { module: o.module };
+      if (term !== undefined) data.term = term;
+      if (id   !== undefined) data.id   = id;
+      $.ajax($.extend(o, {
+        url:      'controller.pl?action=TopQuickSearch/' + action,
+        dataType: "json",
+        data:     data,
+        success:  success
+      }));
+    }
+
+    function submit_search(term) {
+      send_query('do_search', term, undefined, kivi.eval_json_result);
+    }
+
+    $real.autocomplete({
+      source: function(req, rsp) {
+        send_query('query_autocomplete', req.term, undefined, function (data){ rsp(data); });
+      },
+      select: function(event, ui) {
+        send_query('select_autocomplete', undefined, ui.item.id, kivi.eval_json_result);
+      },
+    });
+    $real.keypress(function(event){
+      if (event.which == KEY.ENTER) {
+        if ($real.val() !== '') {
+          submit_search($real.val());
+        }
+      }
+    });
+
+    $real.data('quick_search', {});
+  };
+});
+
+$(function(){
+  $('input[id^=top-quick-search]').each(function(_,e){
+    kivi.QuickSearch($(e), { module: $(e).attr('module') });
+  });
+});
diff --git a/js/kivi.RecordTemplate.js b/js/kivi.RecordTemplate.js
new file mode 100644 (file)
index 0000000..af84840
--- /dev/null
@@ -0,0 +1,83 @@
+namespace('kivi.RecordTemplate', function(ns) {
+  'use strict';
+
+  ns.popup = function(template_type) {
+    $.get('controller.pl', {
+      action:        'RecordTemplate/show_dialog.js',
+      template_type: template_type,
+    }, kivi.eval_json_result);
+  };
+
+  ns.create = function() {
+    var new_name = $("#record_template_dialog_new_template_name").val();
+    if (new_name === '') {
+      alert(kivi.t8('Error: Name missing'));
+      return false;
+    }
+
+    kivi.RecordTemplate.save(undefined, new_name);
+  };
+
+  ns.save = function(id, name) {
+    var $type = $("#record_template_dialog_template_type");
+    var $form = $($type.data('form_selector'));
+
+    if (!$form) {
+      console.log("nothing found for form_selector " + $type.data("form_selector"));
+      return false;
+    }
+
+    if ((id !== undefined) && !confirm(kivi.t8('Are you sure you want to update the selected record template with the current values? This cannot be undone.')))
+      return false;
+
+    var data = $form.serializeArray().filter(function(val) { return val.name !== 'action'; });
+    data.push({ name: 'action',                            value: $type.data('save_action') });
+    data.push({ name: 'record_template_id',                value: id });
+    data.push({ name: 'record_template_new_template_name', value: name });
+
+    $.post($type.data('controller'), data, kivi.eval_json_result);
+  };
+
+  ns.load = function(id) {
+    var $type = $("#record_template_dialog_template_type");
+    var url   = encodeURIComponent($type.data('controller'))
+              + '?action=' + encodeURIComponent($type.data('load_action'))
+              + '&id='     + encodeURIComponent(id);
+
+    console.log(url);
+
+    window.location = url;
+  };
+
+  ns.rename = function(id) {
+    var current_name = $("#record_template_dialog_template_name_" + id).val();
+    var new_name     = prompt(kivi.t8("Please enter the new name:"), current_name);
+
+    if ((new_name === current_name) || !new_name || (new_name === ''))
+      return;
+
+    $.post('controller.pl', {
+      action: 'RecordTemplate/rename.js',
+      id: id,
+      template_name: new_name
+    }, kivi.eval_json_result);
+  };
+
+  ns.delete = function(id) {
+    if (!confirm(kivi.t8('Do you really want to delete this record template?')))
+      return;
+
+    $.post('controller.pl', {
+      action: 'RecordTemplate/delete.js',
+      id: id
+    }, kivi.eval_json_result);
+  };
+
+  ns.filter_templates = function() {
+    $.post('controller.pl', {
+      action: 'RecordTemplate/filter_templates',
+      template_filter: $("#template_filter").val(),
+      template_type: $("#record_template_dialog_template_type").val(),
+    }, kivi.eval_json_result);
+  };
+});
index 303ae5f..4dc390b 100644 (file)
@@ -1,4 +1,6 @@
 namespace('kivi.SalesPurchase', function(ns) {
+  this.longdescription_dialog_size_percentage = 0;
+
   this.edit_longdescription = function(row) {
     var $element = $('#longdescription_' + row);
 
@@ -7,21 +9,47 @@ namespace('kivi.SalesPurchase', function(ns) {
       return;
     }
 
+    var params = { element: $element,
+                   runningnumber: row,
+                   partnumber: $('#partnumber_' + row).val() || '',
+                   description: $('#description_' + row).val() || '',
+                   default_longdescription: $('#longdescription_' + row).val() || ''
+                 };
+    this.edit_longdescription_with_params(params);
+  };
+
+  this.edit_longdescription_with_params = function(params) {
+    var dialog_width    = 800;
+    var dialog_height   = 500;
+    var textarea_width  = 750;
+    var textarea_height = 220;
+    if (this.longdescription_dialog_size_percentage != 0) {
+      dialog_width    = Math.ceil(window.innerWidth  * this.longdescription_dialog_size_percentage/100);
+      dialog_height   = Math.ceil(window.innerHeight * this.longdescription_dialog_size_percentage/100);
+      textarea_width  = Math.ceil(dialog_width * 95/100);
+      textarea_height = dialog_height - 220;
+      if (textarea_height <= 0) textarea_height = 220;
+    }
+
     var $container = $('#popup_edit_longdescription_input_container');
-    var $edit      = $('<textarea id="popup_edit_longdescription_input" class="texteditor-in-dialog" wrap="soft" style="width: 750px; height: 220px;"></textarea>');
+    var $edit      = $('<textarea id="popup_edit_longdescription_input" class="texteditor-in-dialog texteditor-space-for-toolbar" wrap="soft" style="width: ' + textarea_width + 'px; height: ' + textarea_height + 'px;"></textarea>');
 
     $container.children().remove();
     $container.append($edit);
-    $container.data('element', $element);
 
-    $edit.val($element.val());
+    if (params.element) {
+      $container.data('element', params.element);
+    }
+    if (params.set_function) {
+      $container.data('setFunction', params.set_function);
+    }
 
-    kivi.init_text_editor($edit);
+    $edit.val(params.default_longdescription);
 
-    $('#popup_edit_longdescription_runningnumber').html(row);
-    $('#popup_edit_longdescription_partnumber').html($('#partnumber_' + row).val() || '');
+    $('#popup_edit_longdescription_runningnumber').html(params.runningnumber);
+    $('#popup_edit_longdescription_partnumber').html(params.partnumber);
 
-    var description = ($('#description_' + row).val() || '').replace(/[\n\r]+/, '');
+    var description = params.description.replace(/[\n\r]+/, '');
     if (description.length >= 50)
       description = description.substring(0, 50) + "…";
     $('#popup_edit_longdescription_description').html(description);
@@ -30,6 +58,8 @@ namespace('kivi.SalesPurchase', function(ns) {
       id:    'edit_longdescription_dialog',
       dialog: {
         title: kivi.t8('Enter longdescription'),
+        width:  dialog_width,
+        height: dialog_height,
         open:  function() { kivi.focus_ckeditor_when_ready('#popup_edit_longdescription_input'); },
         close: function() { $('#popup_edit_longdescription_input_container').children().remove(); }
       }
@@ -37,10 +67,13 @@ namespace('kivi.SalesPurchase', function(ns) {
   };
 
   this.set_longdescription = function() {
-    $('#popup_edit_longdescription_input_container')
-      .data('element')
-      .val( $('#popup_edit_longdescription_input').val() );
-
+    if ($('#popup_edit_longdescription_input_container').data('setFunction')) {
+      $('#popup_edit_longdescription_input_container').data('setFunction')($('#popup_edit_longdescription_input').val());
+    } else {
+      $('#popup_edit_longdescription_input_container')
+        .data('element')
+        .val( $('#popup_edit_longdescription_input').val() );
+    }
     $('#edit_longdescription_dialog').dialog('close');
   };
 
@@ -62,13 +95,30 @@ namespace('kivi.SalesPurchase', function(ns) {
   };
 
   this.check_transaction_description = function() {
-    if ($('#transaction_description').val() != '')
+    if ($('#transaction_description').val() !== '')
       return true;
 
     alert(kivi.t8('A transaction description is required.'));
     return false;
   };
 
+  this.check_transport_cost_article_presence = function() {
+    var $form          = $('#form');
+    var wanted_part_id = $form.data('transport-cost-reminder-article-id');
+
+    if (!wanted_part_id)
+      return true;
+
+    var rowcount = $('#rowcount').val() * 1;
+    for (var row = 1; row <= rowcount; row++)
+      if (   (($('#id_'         + row).val() * 1)   === wanted_part_id)
+          && (($('#partnumber_' + row).val() || '') !== ''))
+        return true;
+
+    var description = $form.data('transport-cost-reminder-article-description');
+    return confirm(kivi.t8("The transport cost article '#1' is missing. Do you want to continue anyway?", [ description ]));
+  };
+
   this.on_submit_checks = function() {
     var $button = $(this);
     if (($button.data('check-transfer-qty') == 1) && !kivi.SalesPurchase.delivery_order_check_transfer_qty())
@@ -98,4 +148,237 @@ namespace('kivi.SalesPurchase', function(ns) {
       $.post('is.pl', data, kivi.eval_json_result);
     });
   };
+
+  // Functions dialog with entering shipping addresses.
+  this.shipto_addresses = [];
+
+  this.copy_shipto_address = function () {
+    var shipto = this.shipto_addresses[ $('#shipto_to_copy').val() ];
+    for (var key in shipto)
+      $('#' + key).val(shipto[key]);
+  };
+
+  this.clear_shipto_fields = function() {
+    var shipto = this.shipto_addresses[0];
+    for (var key in shipto)
+      $('#' + key).val('');
+    $('#shiptocp_gender').val('m');
+  };
+
+  this.clear_shipto_id_before_submit = function() {
+    var shipto = this.shipto_addresses[0];
+    for (var key in shipto)
+      if ((key != 'shiptocp_gender') && ($('#' + key).val() !== '')) {
+        $('#shipto_id').val('');
+        break;
+      }
+  };
+
+  this.setup_shipto_dialog = function() {
+    var $dlg = $('#shipto_dialog');
+
+    $('#shipto_dialog [name^="shipto"]').each(function(idx, elt) {
+      $dlg.data("original-" + $(elt).prop("name"), $(elt).val());
+    });
+
+    $dlg.data('confirmed', false);
+
+    $('#shiptoname').focus();
+  };
+
+  this.submit_custom_shipto = function(id_selector) {
+    id_selector = id_selector || '#shipto_id';
+    $(id_selector).val('');
+    $('#shipto_dialog').data('confirmed', true);
+    $('#shipto_dialog').dialog('close');
+  };
+
+  this.reset_shipto_fields = function() {
+    var $dlg = $('#shipto_dialog');
+
+    $('#shipto_dialog [name^="shipto"]').each(function(idx, elt) {
+      $(elt).val($dlg.data("original-" + $(elt).prop("name")));
+    });
+  };
+
+  this.finish_shipto_dialog = function() {
+    if (!$('#shipto_dialog').data('confirmed'))
+      kivi.SalesPurchase.reset_shipto_fields();
+
+    $('#shipto_dialog').children().remove().appendTo('#shipto_inputs');
+
+    return true;
+  };
+
+  this.edit_custom_shipto = function() {
+    $('#shipto_inputs').children().remove().appendTo('#shipto_dialog');
+
+    kivi.popup_dialog({
+      id:    'shipto_dialog',
+      dialog: {
+        height: 600,
+        title:  kivi.t8('Edit custom shipto'),
+        open:   kivi.SalesPurchase.setup_shipto_dialog,
+        close:  kivi.SalesPurchase.finish_shipto_dialog,
+      }
+    });
+  };
+
+  this.show_print_options_elements = function(elements, show) {
+    $(elements).each(function(idx, elt) {
+      var $elements = $('#print_options_header_' + elt + ',#print_options_input_' + elt);
+      if (show)
+        $elements.show();
+      else
+        $elements.hide();
+    });
+  };
+
+  this.show_all_print_options_elements = function() {
+    kivi.SalesPurchase.show_print_options_elements([ 'formname', 'language_id', 'format', 'sendmode', 'media', 'printer_id', 'copies', 'groupitems', 'remove_draft' ], true);
+  };
+
+  // Sending records via email.
+  this.check_required_email_fields = function() {
+    var unset = $('#email_form_to,#email_form_subject,#email_form_message').filter(function(idx, elt) {
+      return $(elt).val() === '';
+    });
+
+    if (unset.length === 0)
+      return true;
+
+    alert(kivi.t8("The recipient, subject or body is missing."));
+    $(unset[0]).focus();
+
+    return false;
+  };
+
+  this.send_email = function() {
+    if (!kivi.SalesPurchase.check_required_email_fields())
+      return false;
+
+    // ckeditor gets de-initialized when removing the children from
+    // the DOM. Therefore we have to manually preserve its content
+    // over the children's relocation.
+
+    var message = $('#email_form_message').val();
+
+    $('#send_email_dialog').children().remove().appendTo('#email_inputs');
+    $('#send_email_dialog').dialog('close');
+
+    $('#email_form_message').val(message);
+
+    kivi.submit_form_with_action('#form', $('#form').data('send-email-action'));
+
+    return true;
+  };
+
+  this.setup_send_email_dialog = function() {
+    kivi.SalesPurchase.show_all_print_options_elements();
+    kivi.SalesPurchase.show_print_options_elements([ 'sendmode', 'media', 'copies', 'remove_draft' ], false);
+
+    $('#print_options').children().remove().appendTo('#email_form_print_options');
+
+    kivi.reinit_widgets();
+
+    var to_focus = $('#email_form_to').val() === '' ? 'to' : 'subject';
+    $('#email_form_' + to_focus).focus();
+  };
+
+  this.finish_send_email_dialog = function() {
+    $('#email_form_print_options').children().remove().appendTo('#print_options');
+    return true;
+  };
+
+  this.show_email_dialog = function(send_action, vc, vc_id_selector) {
+    $('#form').data('send-email-action', send_action || 'send_sales_purchase_email');
+
+    vc             = vc             || $('#vc').val();
+    vc_id_selector = vc_id_selector || '#' + vc + '_id';
+    var vc_id = $(vc_id_selector).val();
+
+    var data = {
+      action:       'show_sales_purchase_email_dialog',
+      cp_id:        $('#cp_id').val(),
+      direct_debit: $('#direct_debit').prop('checked') ? 1 : 0,
+      donumber:     $('#donumber').val(),
+      format:       $('#format').val(),
+      formname:     $('#formname').val(),
+      id:           $('#id').val(),
+      invnumber:    $('#invnumber').val(),
+      language_id:  $('#language_id').val(),
+      media:        'email',
+      ordnumber:    $('#ordnumber').val(),
+      cusordnumber: $('#cusordnumber').val(),
+      rowcount:     $('#rowcount').val(),
+      quonumber:    $('#quonumber').val(),
+      type:         $('#type').val(),
+      vc:           vc,
+      vc_id:        vc_id,
+      project_id:  $('#globalproject_id').val(),
+    };
+
+    $('[name^=id_],[name^=partnumber_]').each(function(idx, elt) {
+      var val = $(elt).val() || '';
+      if (val !== '')
+        data[ $(elt).attr('name') ] = val;
+    });
+
+    kivi.popup_dialog({
+      id:     'send_email_dialog',
+      url:    'io.pl',
+      load:   kivi.SalesPurchase.setup_send_email_dialog,
+      data:   data,
+      dialog: {
+        height:      600,
+        title:       kivi.t8('Send email'),
+        beforeClose: kivi.SalesPurchase.finish_send_email_dialog
+      }
+    });
+
+    return true;
+  };
+
+  this.activate_send_email_actions_regarding_printout = function() {
+    var selected = $('#email_form_attachment_policy').val();
+    $('#email_form_attachment_filename').parents('tr')[selected !== 'no_file' ? 'show' : 'hide']();
+    $('#email_form_print_options')[selected !== 'no_file' ? 'show' : 'hide']();
+  };
+
+  // Printing records.
+  this.setup_print_dialog = function() {
+    kivi.SalesPurchase.show_all_print_options_elements();
+
+    $('#print_options').children().remove().appendTo('#print_dialog_print_options');
+
+    $('#print_dialog_print_button').focus();
+  };
+
+  this.finish_print_dialog = function() {
+    $('#print_dialog_print_options').children().remove().appendTo('#print_options');
+  };
+
+  this.print_record = function() {
+    $('#print_dialog').dialog('close');
+
+    var action = $('#form').data('print-action');
+    if (action.match("^js:"))
+      return kivi.run(action.substring(3));
+
+    kivi.submit_form_with_action('#form', action);
+  };
+
+  this.show_print_dialog = function(print_action) {
+    $('#form').data('print-action', print_action || 'print');
+
+    kivi.popup_dialog({
+      id:    'print_dialog',
+      dialog: {
+        height: 600,
+        title:  kivi.t8('Print record'),
+        open:   kivi.SalesPurchase.setup_print_dialog,
+        close:  kivi.SalesPurchase.finish_print_dialog,
+      }
+    });
+  };
 });
diff --git a/js/kivi.Shop.js b/js/kivi.Shop.js
new file mode 100644 (file)
index 0000000..b2c0a7d
--- /dev/null
@@ -0,0 +1,17 @@
+namespace('kivi.Shop', function(ns) {
+
+ ns.check_connectivity = function() {
+   var dat = $('form').serializeArray();
+    kivi.popup_dialog({
+      url:    'controller.pl?action=Shop/check_connectivity',
+      data:   dat,
+      type:   'POST',
+      id:     'test_shop_connection_window',
+      dialog: { title: kivi.t8('Shop Connection Test') },
+      width: 60,
+      height: 40,
+    });
+    return true;
+  };
+
+});
diff --git a/js/kivi.ShopOrder.js b/js/kivi.ShopOrder.js
new file mode 100644 (file)
index 0000000..467fcbd
--- /dev/null
@@ -0,0 +1,64 @@
+namespace('kivi.ShopOrder', function(ns) {
+  ns.massTransferInitialize = function() {
+    kivi.popup_dialog({
+      id: 'status_mass_transfer',
+      dialog: {
+        title: kivi.t8('Status Shoptransfer'),
+      }
+    });
+  };
+
+  ns.get_orders_one = function() {
+
+    var data = $('#get_one_order_form').serializeArray();
+    data.push({ name: 'type', value: 'get_one'});
+    data.push({ name: 'action', value: 'ShopOrder/get_orders' });
+
+    $.post("controller.pl", data, kivi.eval_json_result);
+  };
+
+  ns.get_orders_next = function() {
+
+    $.post("controller.pl", { action: 'ShopOrder/get_orders', type: 'get_next'}, kivi.eval_json_result);
+  };
+
+  ns.getOneOrderInitialize = function() {
+    kivi.popup_dialog({
+      id: 'get_one',
+      dialog: {
+        title: kivi.t8('Get one shoporder'),
+      }
+    });
+  };
+
+
+  ns.get_one_order_setup = function() {
+    kivi.ShopOrder.getOneOrderInitialize();
+    kivi.submit_ajax_form('controller.pl?action=ShopOrder/get_orders', $('#shoporder'));
+  };
+
+  ns.massTransferStarted = function() {
+    $('#status_mass_transfer').data('timerId', setInterval(function() {
+      $.get("controller.pl", {
+        action: 'ShopOrder/transfer_status',
+        job_id: $('#smt_job_id').val()
+      }, kivi.eval_json_result);
+    }, 5000));
+  };
+
+  ns.massTransferFinished = function() {
+    clearInterval($('#status_mass_transfer').data('timerId'));
+    $('.ui-dialog-titlebar button.ui-dialog-titlebar-close').prop('disabled', '')
+  };
+
+  ns.processClose = function() {
+    $('#status_mass_transfer').dialog('close');
+    window.location.href = 'controller.pl?filter.obsolete=0&filter.transferred=0&action=ShopOrder%2flist&db=shop_orders&sort_by=shop_ordernumber';
+  };
+
+  ns.setup = function() {
+    kivi.ShopOrder.massTransferInitialize();
+    kivi.submit_ajax_form('controller.pl?action=ShopOrder/mass_transfer','[name=shop_orders_list]');
+  };
+
+});
diff --git a/js/kivi.ShopPart.js b/js/kivi.ShopPart.js
new file mode 100644 (file)
index 0000000..56d524c
--- /dev/null
@@ -0,0 +1,151 @@
+namespace('kivi.ShopPart', function(ns) {
+  var $dialog;
+
+  ns.shop_part_dialog = function(title, html) {
+    var id            = 'jqueryui_popup_dialog';
+    var dialog_params = {
+      id:     id,
+      width:  800,
+      height: 500,
+      modal:  true,
+      close: function(event, ui) { $dialog.remove(); },
+    };
+
+    $('#' + id).remove();
+
+    $dialog = $('<div style="display:none" id="' + id + '"></div>').appendTo('body');
+    $dialog.attr('title', title);
+    $dialog.html(html);
+    $dialog.dialog(dialog_params);
+
+    $('.cancel').click(ns.close_dialog);
+
+    return true;
+  };
+
+  ns.close_dialog = function() {
+    $dialog.dialog("close");
+  }
+
+  ns.save_shop_part = function(shop_part_id) {
+    var form = $('form').serializeArray();
+    form.push( { name: 'action', value: 'ShopPart/update' }
+             , { name: 'shop_part_id',  value: shop_part_id }
+    );
+
+    $.post('controller.pl', form, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.add_shop_part = function() {
+    var form = $('form').serializeArray();
+    form.push( { name: 'action', value: 'ShopPart/update' }
+    );
+    $.post('controller.pl', form, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.edit_shop_part = function(shop_part_id) {
+    $.post('controller.pl', { action: 'ShopPart/create_or_edit_popup', shop_part_id: shop_part_id }, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.create_shop_part = function(part_id, shop_id) {
+    $.post('controller.pl', { action: 'ShopPart/create_or_edit_popup', part_id: part_id, shop_id: shop_id }, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.get_all_categories = function(shop_part_id) {
+    $.post('controller.pl', { action: 'ShopPart/get_categories', shop_part_id: shop_part_id }, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.save_categories = function(shop_part_id, shop_id) {
+    var form = $('form').serializeArray();
+    form.push( { name: 'action', value: 'ShopPart/save_categories' }
+             , { name: 'shop_id', value: shop_id }
+             , { name: 'shop_part_id', value: shop_part_id }
+    );
+
+    $.post('controller.pl', form, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.update_shop_part = function(shop_part_id) {
+    $.post('controller.pl', { action: 'ShopPart/update_shop', shop_part_id: shop_part_id }, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.update_discount_source = function(row, source, discount_str) {
+    $('#active_discount_source_' + row).val(source);
+    if (discount_str) $('#discount_' + row).val(discount_str);
+    $('#update_button').click();
+  }
+
+  ns.show_images = function(id) {
+    var url = 'controller.pl?action=ShopPart/show_files&id='+id;
+    $('#shop_images').load(url);
+  }
+
+  ns.update_price_n_price_source = function(shop_part_id,price_source) {
+    $.post('controller.pl', { action: 'ShopPart/show_price_n_pricesource', shop_part_id: shop_part_id, pricesource: price_source }, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.update_stock = function(shop_part_id) {
+    $.post('controller.pl', { action: 'ShopPart/show_stock', shop_part_id: shop_part_id }, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.massUploadInitialize = function() {
+    kivi.popup_dialog({
+      id: 'status_mass_upload',
+      dialog: {
+        title: kivi.t8('Status Shopupload')
+      }
+    });
+  };
+
+  ns.massUploadStarted = function() {
+    $('#status_mass_upload').data('timerId', setInterval(function() {
+      $.get("controller.pl", {
+        action: 'ShopPart/upload_status',
+        job_id: $('#smu_job_id').val()
+      }, kivi.eval_json_result);
+    }, 5000));
+  };
+
+  ns.massUploadFinished = function() {
+    clearInterval($('#status_mass_upload').data('timerId'));
+    $('.ui-dialog-titlebar button.ui-dialog-titlebar-close').prop('disabled', '')
+  };
+
+  ns.imageUpload = function(id,type,filetype,upload_title,gl) {
+    kivi.popup_dialog({ url:     'controller.pl',
+                        data:    { action: 'File/ajax_upload',
+                                   file_type:   filetype,
+                                   object_type: type,
+                                   object_id:   id,
+                                   is_global:   gl
+                                 },
+                        id:     'files_upload',
+                        dialog: { title: kivi.t8('File upload'), width: 650, height: 240 } });
+    return true;
+  }
+
+
+  ns.setup = function() {
+    kivi.ShopPart.massUploadInitialize();
+    kivi.submit_ajax_form('controller.pl?action=ShopPart/mass_upload','[name=shop_parts]');
+  };
+
+});
diff --git a/js/kivi.TimeRecording.js b/js/kivi.TimeRecording.js
new file mode 100644 (file)
index 0000000..710f272
--- /dev/null
@@ -0,0 +1,88 @@
+namespace('kivi.TimeRecording', function(ns) {
+  'use strict';
+
+  ns.inputs_to_disable = [];
+
+  ns.set_end_date = function() {
+    if ($('#start_date').val() !== '' && $('#end_date').val() === '') {
+      var kivi_start_date  = kivi.format_date(kivi.parse_date($('#start_date').val()));
+      $('#end_date').val(kivi_start_date);
+    }
+  };
+
+  ns.set_current_date_time = function(what) {
+    if (what !== 'start' && what !== 'end') return;
+
+    var $date = $('#' + what + '_date');
+    var $time = $('#' + what + '_time');
+    var date = new Date();
+
+    $date.val(kivi.format_date(date));
+    $time.val(kivi.format_time(date));
+  };
+
+  var order_changed_called;
+  ns.order_changed = function(value) {
+    order_changed_called = true;
+
+    if (!value) {
+      $('#time_recording_customer_id').data('customer_vendor_picker').set_item({});
+      $('#time_recording_customer_id_name').prop('disabled', false);
+      $('#time_recording_project_id').data('project_picker').set_item({});
+      $('#time_recording_project_id_name').prop('disabled', false);
+      $('#time_recording_project_id ~ .ppp_popup_button').show()
+      return;
+    }
+
+    var url = 'controller.pl?action=TimeRecording/ajaj_get_order_info&id='+ value;
+    $.getJSON(url, function(data) {
+      $('#time_recording_customer_id').data('customer_vendor_picker').set_item(data.customer);
+      $('#time_recording_customer_id_name').prop('disabled', true);
+      $('#time_recording_project_id').data('project_picker').set_item(data.project);
+      $('#time_recording_project_id_name').prop('disabled', true);
+      $('#time_recording_project_id ~ .ppp_popup_button').hide()
+    });
+  };
+
+  ns.project_changed = function(event) {
+    if (order_changed_called) {
+      order_changed_called = false;
+      return;
+    }
+
+    var project_id = $('#time_recording_project_id').val();
+
+    if (!project_id) {
+      $('#time_recording_customer_id_name').prop('disabled', false);
+      return;
+    }
+
+    var url = 'controller.pl?action=TimeRecording/ajaj_get_project_info&id='+ project_id;
+    $.getJSON(url, function(data) {
+      if (data) {
+        $('#time_recording_customer_id').data('customer_vendor_picker').set_item(data.customer);
+        $('#time_recording_customer_id_name').prop('disabled', true);
+      } else {
+        $('#time_recording_customer_id_name').prop('disabled', false);
+      }
+    });
+  };
+
+  ns.set_input_constraints = function() {
+    $(ns.inputs_to_disable).each(function(idx, elt) {
+      if ("customer" === elt) {
+        $('#time_recording_customer_id_name').prop('disabled', true);
+      }
+      if ("project" === elt) {
+        $('#time_recording_project_id_name').prop('disabled', true);
+        setTimeout(function() {$('#time_recording_project_id ~ .ppp_popup_button').hide();}, 100);
+      }
+    });
+  };
+
+});
+
+$(function() {
+  kivi.TimeRecording.set_input_constraints();
+  $('#time_recording_project_id').on('set_item:ProjectPicker', function(){ kivi.TimeRecording.project_changed() });
+});
diff --git a/js/kivi.Validator.js b/js/kivi.Validator.js
new file mode 100644 (file)
index 0000000..4d03451
--- /dev/null
@@ -0,0 +1,198 @@
+namespace("kivi.Validator", function(ns) {
+  "use strict";
+
+  // Performs various validation steps on the descendants of
+  // 'selector'. Elements that should be validated must have an
+  // attribute named "data-validate" which is set to a space-separated
+  // list of tests to perform. Additionally, the attribute
+  // "data-title" can be set to a human-readable name of the field
+  // that can be shown in front of an error message.
+  //
+  // Supported validation tests are:
+  // - "required": the field must be set (its .val() must not be empty)
+  // - "number": the field must be in number format (its .val() must in the right format)
+  // - "date": the field must be in date format (its .val() must in the right format)
+  // - "time": the field must be in time format (its .val() must in the right format)
+  //
+  // The validation will abort and return "false" as soon as
+  // validation routine fails.
+  //
+  // The function returns "true" if all validations succeed for all
+  // elements.
+  ns.validate_all = function(selector) {
+    selector = selector || '#form';
+    var to_check = $(selector + ' [data-validate]').toArray();
+
+    for (var to_check_idx in to_check)
+      if (!ns.validate($(to_check[to_check_idx]))) {
+        $(to_check[to_check_idx]).focus();
+        return false;
+      }
+
+    return true;
+  };
+
+  ns.validate = function($e) {
+    var $e_annotate;
+    if ($e.data('ckeditorInstance')) {
+      $e_annotate = $($e.data('ckeditorInstance').editable().$);
+      if ($e.data('title'))
+        $e_annotate.data('title', $e.data('title'));
+    }
+    var tests = $e.data('validate').split(/ +/);
+
+    for (var test_idx in tests) {
+      var test = tests[test_idx];
+      if (!ns.checks[test])
+        continue;
+
+      if (ns.checks[test]) {
+        if (!ns.checks[test]($e, $e_annotate))
+          return false;
+      } else {
+        var error = "kivi.validate_form: unknown test '" + test + "' for element ID '" + $e.prop('id') + "'";
+        console.error(error);
+        alert(error);
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  ns.checks = {
+    required: function($e, $e_annotate) {
+      $e_annotate = $e_annotate || $e;
+
+      if ($e.val() === '') {
+        ns.annotate($e_annotate, kivi.t8("This field must not be empty."));
+        return false;
+      } else {
+        ns.annotate($e_annotate);
+        return true;
+      }
+    },
+    number: function($e, $e_annotate) {
+      $e_annotate = $e_annotate || $e;
+
+      var number_string = $e.val();
+
+      var parsed_number = kivi.parse_amount(number_string);
+
+      if (parsed_number === null) {
+        $e.val('');
+        ns.annotate($e_annotate);
+        return true;
+      } else
+      if (parsed_number === undefined) {
+        ns.annotate($e_annotate, kivi.t8('Wrong number format (#1)', [ kivi.myconfig.numberformat ]));
+        return false;
+      } else
+      {
+        var formatted_number = kivi.format_amount(parsed_number);
+        if (formatted_number != number_string)
+          $e.val(formatted_number);
+        ns.annotate($e_annotate);
+        return true;
+      }
+    },
+    date: function($e, $e_annotate) {
+      $e_annotate = $e_annotate || $e;
+
+      var date_string = $e.val();
+
+      var parsed_date = kivi.parse_date(date_string);
+
+      if (parsed_date === null) {
+        $e.val('');
+        ns.annotate($e_annotate);
+        return true;
+      } else
+      if (parsed_date === undefined) {
+        ns.annotate($e_annotate, kivi.t8('Wrong date format (#1)', [ kivi.myconfig.dateformat ]));
+        return false;
+      } else
+      {
+        var formatted_date = kivi.format_date(parsed_date);
+        if (formatted_date != date_string)
+          $e.val(formatted_date);
+        ns.annotate($e_annotate);
+        return true;
+      }
+    },
+    time: function($e, $e_annotate) {
+      $e_annotate = $e_annotate || $e;
+
+      var time_string = $e.val();
+
+      var parsed_time = kivi.parse_time(time_string);
+      if (parsed_time === null) {
+        $e.val('');
+        ns.annotate($e_annotate);
+        return true;
+      } else
+      if (parsed_time === undefined) {
+        ns.annotate($e_annotate, kivi.t8('Wrong time format (#1)', [ kivi.myconfig.timeformat ]));
+        return false;
+      } else
+      {
+        var formatted_time = kivi.format_time(parsed_time);
+        if (formatted_time != time_string)
+          $e.val(formatted_time);
+        ns.annotate($e_annotate);
+        return true;
+      }
+    },
+    trimmed_whitespaces: function($e, $e_annotate) {
+      $e_annotate = $e_annotate || $e;
+
+      var string = $e.val();
+
+      if ($e.hasClass('tooltipstered'))
+        $e.tooltipster('destroy');
+
+      if (string.match(/^\s|\s$/)) {
+        $e.val(string.trim());
+
+        $e.tooltipster({
+          content: kivi.t8("Leading and trailing whitespaces have been removed."),
+          contentAsHTML: true,
+          theme: 'tooltipster-light',
+        });
+        $e.tooltipster('show');
+      }
+      return true;
+    }
+  };
+
+  ns.annotate = function($e, error) {
+    if (error) {
+      $e.addClass('kivi-validator-invalid');
+      if ($e.hasClass('tooltipstered'))
+        $e.tooltipster('destroy');
+
+      if ($e.data('title'))
+        error = $e.data('title') + ': ' + error;
+
+      $e.tooltipster({
+        content: error,
+        theme: 'tooltipster-light',
+      });
+      $e.tooltipster('show');
+    } else {
+      $e.removeClass('kivi-validator-invalid');
+      if ($e.hasClass('tooltipstered'))
+        $e.tooltipster('destroy');
+    }
+  };
+
+  ns.reinit_widgets = function() {
+    kivi.run_once_for('[data-validate]', 'data-validate', function(elt) {
+      $(elt).change(function(event){ ns.validate($(elt), event) });
+    });
+  }
+
+  ns.init = ns.reinit_widgets;
+
+  $(ns.init);
+});
index 6b47e1b..5f9360f 100644 (file)
@@ -1,4 +1,6 @@
 namespace("kivi", function(ns) {
+  "use strict";
+
   ns._locale = {};
   ns._date_format   = {
     sep: '.',
@@ -6,6 +8,11 @@ namespace("kivi", function(ns) {
     m:   1,
     d:   0
   };
+  ns._time_format = {
+    sep: ':',
+    h: 0,
+    m: 1,
+  };
   ns._number_format = {
     decimalSep:  ',',
     thousandSep: '.'
@@ -20,6 +27,13 @@ namespace("kivi", function(ns) {
       ns._date_format[res[4].substr(0, 1)] = 2;
     }
 
+    res = (params.times || "").match(/^([hm]+)([^a-z])([hm]+)$/);
+    if (res) {
+      ns._time_format                      = { sep: res[2] };
+      ns._time_format[res[1].substr(0, 1)] = 0;
+      ns._time_format[res[3].substr(0, 1)] = 1;
+    }
+
     res = (params.numbers || "").match(/^\d*([^\d]?)\d+([^\d])\d+$/);
     if (res)
       ns._number_format = {
@@ -29,12 +43,61 @@ namespace("kivi", function(ns) {
   };
 
   ns.parse_date = function(date) {
+    if (date === undefined)
+      return undefined;
+
+    if (date === '')
+      return null;
+
+    if (date === '0' || date === '00')
+      return new Date();
+
     var parts = date.replace(/\s+/g, "").split(ns._date_format.sep);
-    date     = new Date(
-      ((parts[ ns._date_format.y ] || 0) * 1) || (new Date).getFullYear(),
-       (parts[ ns._date_format.m ] || 0) * 1 - 1, // Months are 0-based.
-       (parts[ ns._date_format.d ] || 0) * 1
-    );
+    var today = new Date();
+
+    // without separator?
+    // assume fixed pattern, and extract parts again
+    if (parts.length == 1) {
+      date  = parts[0];
+      parts = date.match(/../g);
+      if (date.length == 8) {
+        parts[ns._date_format.y] += parts.splice(ns._date_format.y + 1, 1)
+      }
+      else
+      if (date.length == 6 || date.length == 4) {
+      }
+      else
+      if (date.length == 1 || date.length == 2) {
+        parts = []
+        parts[ ns._date_format.y ] = today.getFullYear();
+        parts[ ns._date_format.m ] = today.getMonth() + 1;
+        parts[ ns._date_format.d ] = date;
+      }
+      else {
+        return undefined;
+      }
+    }
+
+    if (parts.length == 3) {
+      var year = +parts[ ns._date_format.y ] || 0 * 1 || (new Date()).getFullYear();
+      if (year > 9999)
+        return undefined;
+      if (year < 100) {
+        year += year > 70 ? 1900 : 2000;
+      }
+      date = new Date(
+        year,
+        (parts[ ns._date_format.m ] || (today.getMonth() + 1)) * 1 - 1, // Months are 0-based.
+        (parts[ ns._date_format.d ] || today.getDate()) * 1
+      );
+    } else if (parts.length == 2) {
+      date = new Date(
+        (new Date()).getFullYear(),
+        (parts[ (ns._date_format.m > ns._date_format.d) * 1 ] || (today.getMonth() + 1)) * 1 - 1, // Months are 0-based.
+        (parts[ (ns._date_format.d > ns._date_format.m) * 1 ] || today.getDate()) * 1
+      );
+    } else
+      return undefined;
 
     return isNaN(date.getTime()) ? undefined : date;
   };
@@ -50,16 +113,75 @@ namespace("kivi", function(ns) {
     return parts.join(ns._date_format.sep);
   };
 
+  ns.parse_time = function(time) {
+    var now = new Date();
+
+    if (time === undefined)
+      return undefined;
+
+    if (time === '')
+      return null;
+
+    if (time === '0')
+      return now;
+
+    // special case 1: military time in fixed "hhmm" format
+    if (time.length == 4) {
+      var res = time.match(/(\d\d)(\d\d)/);
+      if (res) {
+        now.setHours(res[1], res[2]);
+        return now;
+      } else {
+        return undefined;
+      }
+    }
+
+    var parts = time.replace(/\s+/g, "").split(ns._time_format.sep);
+    if (parts.length == 2) {
+      for (var idx in parts) {
+        if (Number.isNaN(Number.parseInt(parts[idx])))
+          return undefined;
+      }
+      now.setHours(parts[ns._time_format.h], parts[ns._time_format.m]);
+      return now;
+    } else
+      return undefined;
+  }
+
+  ns.format_time = function(date) {
+    if (isNaN(date.getTime()))
+      return undefined;
+
+    var parts = [ "", "" ]
+    parts[ ns._time_format.h ] = date.getHours().toString().padStart(2, '0');
+    parts[ ns._time_format.m ] = date.getMinutes().toString().padStart(2, '0');
+    return parts.join(ns._time_format.sep);
+  };
+
   ns.parse_amount = function(amount) {
-    if ((amount == undefined) || (amount == ''))
-      return 0;
+    if (amount === undefined)
+      return undefined;
+
+    if (amount === '')
+      return null;
 
     if (ns._number_format.decimalSep == ',')
       amount = amount.replace(/\./g, "").replace(/,/g, ".");
 
-    amount = amount.replace(/[\',]/g, "")
+    amount = amount.replace(/[\',]/g, "");
+
+    // Make sure no code wich is not a math expression ends up in eval().
+    if (!amount.match(/^[0-9 ()\-+*/.]*$/))
+      return undefined;
+
+    amount = amount.replace(/^0+(\d+)/, '$1');
 
-    return eval(amount);
+    /* jshint -W061 */
+    try {
+      return eval(amount);
+    } catch (err) {
+      return undefined;
+    }
   };
 
   ns.round_amount = function(amount, places) {
@@ -77,7 +199,7 @@ namespace("kivi", function(ns) {
   ns.format_amount = function(amount, places) {
     amount = amount || 0;
 
-    if ((places != undefined) && (places >= 0))
+    if ((places !== undefined) && (places >= 0))
       amount = ns.round_amount(amount, Math.abs(places));
 
     var parts = ("" + Math.abs(amount)).split(/\./);
@@ -85,7 +207,7 @@ namespace("kivi", function(ns) {
     var dec   = parts.length > 1 ? parts[1] : "";
     var sign  = amount  < 0      ? "-"      : "";
 
-    if (places != undefined) {
+    if (places !== undefined) {
       while (dec.length < Math.abs(places))
         dec += "0";
 
@@ -93,7 +215,7 @@ namespace("kivi", function(ns) {
         dec = d.substr(0, places);
     }
 
-    if ((ns._number_format.thousandSep != "") && (intg.length > 3)) {
+    if ((ns._number_format.thousandSep !== "") && (intg.length > 3)) {
       var len   = ((intg.length + 2) % 3) + 1,
           start = len,
           res   = intg.substr(0, len);
@@ -105,26 +227,27 @@ namespace("kivi", function(ns) {
       intg = res;
     }
 
-    var sep = (places != 0) && (dec != "") ? ns._number_format.decimalSep : "";
+    var sep = (places !== 0) && (dec !== "") ? ns._number_format.decimalSep : "";
 
     return sign + intg + sep + dec;
   };
 
   ns.t8 = function(text, params) {
-    var text = ns._locale[text] || text;
+    text = ns._locale[text] || text;
+    var key, value
 
     if( Object.prototype.toString.call( params ) === '[object Array]' ) {
       var len = params.length;
 
       for(var i=0; i<len; ++i) {
-        var key = i + 1;
-        var value = params[i];
+        key = i + 1;
+        value = params[i];
         text = text.split("#"+ key).join(value);
       }
     }
     else if( typeof params == 'object' ) {
-      for(var key in params) {
-        var value = params[key];
+      for(key in params) {
+        value = params[key];
         text = text.split("#{"+ key +"}").join(value);
       }
     }
@@ -145,41 +268,39 @@ namespace("kivi", function(ns) {
   };
 
   ns.focus_ckeditor_when_ready = function(element) {
-    $(element).ckeditor(function() { ns.focus_ckeditor(element); });
+    $(element).data('ckeditorInstance').on('instanceReady', function() { ns.focus_ckeditor(element); });
   };
 
   ns.focus_ckeditor = function(element) {
-    var editor   = $(element).ckeditorGet();
-               var editable = editor.editable();
-
-               if (editable.is('textarea')) {
-                       var textarea = editable.$;
-
-                       if (CKEDITOR.env.ie)
-                               textarea.createTextRange().execCommand('SelectAll');
-                       else {
-                               textarea.selectionStart = 0;
-                               textarea.selectionEnd   = textarea.value.length;
-                       }
-
-                       textarea.focus();
-
-               } else {
-                       if (editable.is('body'))
-                               editor.document.$.execCommand('SelectAll', false, null);
+    $(element).data('ckeditorInstance').focus();
+  };
 
-                       else {
-                               var range = editor.createRange();
-                               range.selectNodeContents(editable);
-                               range.select();
-                       }
+  ns.selectall_ckeditor = function(element) {
+    var editor   = $(element).ckeditorGet();
+    var editable = editor.editable();
+    if (editable.is('textarea')) {
+      var textarea = editable.$;
+
+      if (CKEDITOR.env.ie)
+        textarea.createTextRange().execCommand('SelectAll');
+      else {
+        textarea.selectionStart = 0;
+        textarea.selectionEnd   = textarea.value.length;
+      }
+    } else {
+      if (editable.is('body'))
+        editor.document.$.execCommand('SelectAll', false, null);
 
-                       editor.forceNextSelectionCheck();
-                       editor.selectionChange();
+      else {
+        var range = editor.createRange();
+        range.selectNodeContents(editable);
+        range.select();
+      }
 
-      editor.focus();
-               }
-  };
+      editor.forceNextSelectionCheck();
+      editor.selectionChange();
+    }
+  }
 
   ns.init_tabwidget = function(element) {
     var $element   = $(element);
@@ -188,7 +309,10 @@ namespace("kivi", function(ns) {
 
     if (elementId) {
       var cookieName      = 'jquery_ui_tab_'+ elementId;
-      tabsParams.active   = $.cookie(cookieName);
+      if (!window.location.hash) {
+        // only activate if there's no hash to overwrite it
+        tabsParams.active   = $.cookie(cookieName);
+      }
       tabsParams.activate = function(event, ui) {
         var i = ui.newTab.parent().children().index(ui.newTab);
         $.cookie(cookieName, i);
@@ -210,20 +334,42 @@ namespace("kivi", function(ns) {
       entities:      false,
       language:      'de',
       removePlugins: 'resize',
-      toolbar:       buttons
-    }
+      extraPlugins:  'inline_resize',
+      toolbar:       buttons,
+      disableAutoInline: true,
+      title:         false,
+      disableNativeSpellChecker: false
+    };
 
-    var style = $e.prop('style');
-    $(['width', 'height']).each(function(idx, prop) {
-      var matches = (style[prop] || '').match(/(\d+)px/);
-      if (matches && (matches.length > 1))
-        config[prop] = matches[1];
-    });
+    config.height = $e.height();
+    config.width  = $e.width();
+
+    var editor = CKEDITOR.inline($e.get(0), config);
+    $e.data('ckeditorInstance', editor);
+
+    if ($e.hasClass('texteditor-space-for-toolbar'))
+      editor.on('instanceReady', function() {
+        var editor   = $e.ckeditorGet();
+        var editable = editor.editable();
+        $(editable.$).css("margin-top", "30px");
+      });
 
-    $e.ckeditor(config);
 
     if ($e.hasClass('texteditor-autofocus'))
-      $e.ckeditor(function() { ns.focus_ckeditor($e); });
+      editor.on('instanceReady', function() { ns.focus_ckeditor($e); });
+  };
+
+  ns.filter_select = function() {
+    var $input  = $(this);
+    var $select = $('#' + $input.data('select-id'));
+    var filter  = $input.val().toLocaleLowerCase();
+
+    $select.find('option').each(function() {
+      if ($(this).text().toLocaleLowerCase().indexOf(filter) != -1)
+        $(this).show();
+      else
+        $(this).hide();
+    });
   };
 
   ns.reinit_widgets = function() {
@@ -231,26 +377,24 @@ namespace("kivi", function(ns) {
       $(elt).datepicker();
     });
 
-    if (ns.PartPicker)
-      ns.run_once_for('input.part_autocomplete', 'part_picker', function(elt) {
-        kivi.PartPicker($(elt));
-      });
+    if (ns.Part) ns.Part.reinit_widgets();
+    if (ns.CustomerVendor) ns.CustomerVendor.reinit_widgets();
+    if (ns.Validator) ns.Validator.reinit_widgets();
+    if (ns.Materialize) ns.Materialize.reinit_widgets();
 
     if (ns.ProjectPicker)
       ns.run_once_for('input.project_autocomplete', 'project_picker', function(elt) {
         kivi.ProjectPicker($(elt));
       });
 
-    if (ns.CustomerVendorPicker)
-      ns.run_once_for('input.customer_vendor_autocomplete', 'customer_vendor_picker', function(elt) {
-        kivi.CustomerVendorPicker($(elt));
-      });
-
     if (ns.ChartPicker)
       ns.run_once_for('input.chart_autocomplete', 'chart_picker', function(elt) {
         kivi.ChartPicker($(elt));
       });
 
+    ns.run_once_for('div.filtered_select input', 'filtered_select', function(elt) {
+      $(elt).bind('change keyup', ns.filter_select);
+    });
 
     var func = kivi.get_function_by_name('local_reinit_widgets');
     if (func)
@@ -284,9 +428,34 @@ namespace("kivi", function(ns) {
     return true;
   };
 
+  // This function submits an existing form given by "form_selector"
+  // and sets the "action" input to "action_to_call" before submitting
+  // it. Any existing input named "action" will be removed prior to
+  // submitting.
+  ns.submit_form_with_action = function(form_selector, action_to_call) {
+    $('[name=action]').remove();
+
+    var $form   = $(form_selector);
+    var $hidden = $('<input type=hidden>');
+
+    $hidden.attr('name',  'action');
+    $hidden.attr('value', action_to_call);
+    $form.append($hidden);
+
+    $form.submit();
+  };
+
+  // This function exists solely so that it can be found with
+  // kivi.get_functions_by_name() and called later on. Using something
+  // like "var func = history["back"]" works, but calling it later
+  // with "func.apply()" doesn't.
+  ns.history_back = function() {
+    history.back();
+  };
+
   // Return a function object by its name (a string). Works both with
-  // global functions (e.g. "check_right_date_format") and those in
-  // namespaces (e.g. "kivi.t8").
+  // global functions (e.g. "focus_by_name") and those in namespaces (e.g.
+  // "kivi.t8").
   // Returns null if the object is not found.
   ns.get_function_by_name = function(name) {
     var parts = name.match("(.+)\\.([^\\.]+)$");
@@ -306,11 +475,16 @@ namespace("kivi", function(ns) {
   // - id: dialog DIV ID (optional; defaults to 'jqueryui_popup_dialog')
   // - url, data, type: passed as the first three arguments to the $.ajax() call if an AJAX call is made, otherwise ignored.
   // - dialog: an optional object of options passed to the $.dialog() call
+  // - load: an optional function that is called after the content has been loaded successfully (only if an AJAX call is made)
   ns.popup_dialog = function(params) {
+    if (kivi.Materialize)
+      return kivi.Materialize.popup_dialog(params);
+
     var dialog;
 
     params            = params        || { };
     var id            = params.id     || 'jqueryui_popup_dialog';
+    var custom_close  = params.dialog ? params.dialog.close : undefined;
     var dialog_params = $.extend(
       { // kivitendo default parameters:
           width:  800
@@ -320,7 +494,15 @@ namespace("kivi", function(ns) {
         // User supplied options:
       params.dialog || { },
       { // Options that must not be changed:
-        close: function(event, ui) { if (params.url || params.html) dialog.remove(); else dialog.dialog('close'); }
+        close: function(event, ui) {
+          dialog.dialog('close');
+
+          if (custom_close)
+            custom_close();
+
+          if (params.url || params.html)
+            dialog.remove();
+        }
       });
 
     if (!params.url && !params.html) {
@@ -350,6 +532,8 @@ namespace("kivi", function(ns) {
         success: function(new_html) {
           dialog.html(new_html);
           dialog.removeClass('loading');
+          if (params.load)
+            params.load();
         }
       });
     }
@@ -378,7 +562,7 @@ namespace("kivi", function(ns) {
       return;
     }
 
-    $(selector).filter(function() { return $(this).data(attr_name) != true; }).each(function(idx, elt) {
+    $(selector).filter(function() { return $(this).data(attr_name) !== true; }).each(function(idx, elt) {
       var $elt = $(elt);
       $elt.data(attr_name, true);
       fn($elt);
@@ -397,11 +581,152 @@ namespace("kivi", function(ns) {
   ns.run = function(function_name, args) {
     var fn = ns.get_function_by_name(function_name);
     if (fn)
-      return fn.apply({}, args);
+      return fn.apply({}, args || []);
 
     console.error('kivi.run("' + function_name + '"): No function by that name found');
     return undefined;
   };
+
+  ns.save_file = function(base64_data, content_type, size, attachment_name) {
+    // atob returns a unicode string with one codepoint per octet. revert this
+    const b64toBlob = (b64Data, contentType='', sliceSize=512) => {
+      const byteCharacters = atob(b64Data);
+      const byteArrays = [];
+
+      for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
+        const slice = byteCharacters.slice(offset, offset + sliceSize);
+
+        const byteNumbers = new Array(slice.length);
+        for (let i = 0; i < slice.length; i++) {
+          byteNumbers[i] = slice.charCodeAt(i);
+        }
+
+        const byteArray = new Uint8Array(byteNumbers);
+        byteArrays.push(byteArray);
+      }
+
+      const blob = new Blob(byteArrays, {type: contentType});
+      return blob;
+    }
+
+    var blob = b64toBlob(base64_data, content_type);
+    var a = $("<a style='display: none;'/>");
+    var url = window.URL.createObjectURL(blob);
+    a.attr("href", url);
+    a.attr("download", attachment_name);
+    $("body").append(a);
+    a[0].click();
+    window.URL.revokeObjectURL(url);
+    a.remove();
+  }
+
+  ns.detect_duplicate_ids_in_dom = function() {
+    var ids   = {},
+        found = false;
+
+    $('[id]').each(function() {
+      if (this.id && ids[this.id]) {
+        found = true;
+        console.warn('Duplicate ID #' + this.id);
+      }
+      ids[this.id] = 1;
+    });
+
+    if (!found)
+      console.log('No duplicate IDs found :)');
+  };
+
+  ns.validate_form = function(selector) {
+    if (!kivi.Validator) {
+      console.log('kivi.Validator is not loaded');
+    } else {
+      return kivi.Validator.validate_all(selector);
+    }
+  };
+
+  // Verifies that at least one checkbox matching the
+  // "checkbox_selector" is actually checked. If not, an error message
+  // is shown, and false is returned. Otherwise (at least one of them
+  // is checked) nothing is shown and true returned.
+  //
+  // Can be used in checks when clicking buttons.
+  ns.check_if_entries_selected = function(checkbox_selector) {
+    if ($(checkbox_selector + ':checked').length > 0)
+      return true;
+
+    alert(kivi.t8('No entries have been selected.'));
+
+    return false;
+  };
+
+  ns.switch_areainput_to_textarea = function(id) {
+    var $input = $('#' + id);
+    if (!$input.length)
+      return;
+
+    var $area = $('<textarea></textarea>');
+
+    $area.prop('rows', 3);
+    $area.prop('cols', $input.prop('size') || 40);
+    $area.prop('name', $input.prop('name'));
+    $area.prop('id',   $input.prop('id'));
+    $area.val($input.val());
+
+    $input.parent().replaceWith($area);
+    $area.focus();
+  };
+
+  ns.set_cursor_position = function(selector, position) {
+    var $input = $(selector);
+    if (position === 'end')
+      position = $input.val().length;
+
+    $input.prop('selectionStart', position);
+    $input.prop('selectionEnd',   position);
+  };
+
+  ns._shell_escape = function(str) {
+    if (str.match(/^[a-zA-Z0-9.,_=+/-]+$/))
+      return str;
+
+    return "'" + str.replace(/'/, "'\\''") + "'";
+  };
+
+  ns.call_as_curl = function(params) {
+    params      = params || {};
+    var uri     = document.documentURI.replace(/\?.*/, '');
+    var command = ['curl', '--user', kivi.myconfig.login + ':SECRET', '--request', params.method || 'POST']
+
+    $(params.data || []).each(function(idx, elt) {
+      command = command.concat([ '--form-string', elt.name + '=' + (elt.value || '') ]);
+    });
+
+    command.push(uri);
+
+    return $.map(command, function(elt, idx) {
+      return kivi._shell_escape(elt);
+    }).join(' ');
+  };
+
+  ns.serialize = function(source, target = [], prefix, in_array = false) {
+    let arr_prefix = first => in_array ? (first ? "[+]" : "[]") : "";
+
+    if (Array.isArray(source) ) {
+      source.forEach(( val, i ) => {
+        ns.serialize(val, target, prefix + arr_prefix(i == 0), true);
+      });
+    } else if (typeof source === "object") {
+      let first = true;
+      for (let key in source) {
+        ns.serialize(source[key], target, (prefix !== undefined ? prefix + arr_prefix(first) + "." : "") + key);
+        first = false;
+      }
+    } else {
+      target.push({ name: prefix + arr_prefix(false), value: source });
+    }
+
+    return target;
+  };
 });
 
 kivi = namespace('kivi');
index 7c95e22..c17b213 100644 (file)
@@ -1,18 +1,29 @@
 namespace("kivi").setupLocale({
+" bytes, max=":" Bytes, Maximum=",
 "A transaction description is required.":"Die Vorgangsbezeichnung muss eingegeben werden.",
 "Add function block":"Funktionsblock hinzufügen",
 "Add linked record":"Verknüpften Beleg hinzufügen",
+"Add multiple items":"Mehrere Artikel hinzufügen",
+"Add note":"Notiz erfassen",
 "Add picture":"Bild hinzufügen",
 "Add picture to text block":"Bild dem Textblock hinzufügen",
 "Add section":"Abschnitt hinzufügen",
 "Add sub function block":"Unterfunktionsblock hinzufügen",
 "Add text block":"Textblock erfassen",
 "Additional articles actions":"Aktionen zu zusätzlichen Artikeln",
+"An error occurred while transferring the file.":"Bei Übertragung der Datei trat ein Fehler auf",
+"Apr":"Apr",
+"April":"April",
+"Are you sure you want to update the selected record template with the current values? This cannot be undone.":"Sind Sie sicher, dass Sie die ausgewählte Belegvorlage mit den aktuellen Daten aktualisieren wollen? Das kann nicht rückgängig gemacht werden.",
 "Are you sure?":"Sind Sie sicher?",
 "Assign invoice":"Rechnung zuweisen",
+"Aug":"Aug",
+"August":"August",
 "Basic settings actions":"Aktionen zu Grundeinstellungen",
 "Cancel":"Abbrechen",
+"Carry over shipping address":"Lieferadresse übernehmen",
 "Chart picker":"Kontenauswahl",
+"Clear":"Löschen",
 "Copy":"Kopieren",
 "Copy requirement spec":"Pflichtenheft kopieren",
 "Copy template":"Vorlage kopieren",
@@ -25,67 +36,173 @@ namespace("kivi").setupLocale({
 "Create new quotation/order":"Neues Angebot/neuen Auftrag anlegen",
 "Create new qutoation/order":"Neues Angebot/neuen Auftrag anlegen",
 "Create new version":"Neue Version anlegen",
+"Customer details":"Kundendetails",
+"Customer missing!":"Kundenname fehlt!",
 "Database Connection Test":"Test der Datenbankverbindung",
+"Dec":"Dez",
+"December":"Dezember",
 "Delete":"Löschen",
 "Delete picture":"Bild löschen",
 "Delete quotation/order":"Angebot/Auftrag löschen",
 "Delete requirement spec":"Pflichtenheft löschen",
 "Delete template":"Vorlage löschen",
 "Delete text block":"Textblock löschen",
-"Do you really want do continue?":"Wollen Sie wirklich fortfahren?",
-"Do you really want to cancel?":"Wollen Sie wirklich abbrechen?",
-"Do you really want to revert to this version?":"Wollen Sie wirklich auf diese Version zurücksetzen?",
+"Do you really want to cancel?":"Möchten Sie wirklich abbrechen?",
+"Do you really want to continue?":"Möchten Sie wirklich fortfahren?",
+"Do you really want to delete the selected documents?":"Möchten Sie wirklich diese Dateien löschen?",
+"Do you really want to delete this draft?":"Möchten Sie diesen Entwurf wirklich löschen?",
+"Do you really want to delete this record template?":"Möchten Sie diese Belegvorlage wirklich löschen?",
+"Do you really want to print?":"Wollen Sie wirklich drucken?",
+"Do you really want to revert to this version?":"Möchten Sie wirklich auf diese Version zurücksetzen?",
+"Do you really want to unimport the selected documents?":"Möchten Sie wirklich diese Dateien an die Quelle zurückgeben?",
+"Do you want to carry this shipping address over to the new purchase order so that the vendor can deliver the goods directly to your customer?":"Möchten Sie diese Lieferadresse in den neuen Lieferantenauftrag übernehmen, damit der Händler die Waren direkt an Ihren Kunden liefern kann?",
 "Do you want to set the account number \"#1\" to \"#2\" and the name \"#3\" to \"#4\"?":"Soll die Kontonummer \"#1\" zu \"#2\" und den Name \"#3\" zu \"#4\" geändert werden?",
 "Download picture":"Bild herunterladen",
+"Due Date missing!":"Fälligkeitsdatum fehlt!",
 "Edit":"Bearbeiten",
 "Edit article/section assignments":"Zuweisung Artikel/Abschnitte bearbeiten",
+"Edit custom shipto":"Individuelle Lieferadresse bearbeiten",
+"Edit note":"Notiz bearbeiten",
 "Edit picture":"Bild bearbeiten",
 "Edit project link":"Projektverknüpfung bearbeiten",
 "Edit text block":"Textblock bearbeiten",
+"Edit the configuration for periodic invoices":"Konfiguration für wiederkehrende Rechnungen bearbeiten",
 "Enter longdescription":"Langtext eingeben",
+"Error: Name missing":"Fehler: Name fehlt",
+"Feb":"Feb",
+"February":"Februar",
+"File upload":"Datei Upload",
+"Files have been uploaded successfully.":"Dateien wurden erfolgreich hochgeladen.",
+"Fri":"Fr",
+"Friday":"Freitag",
 "Function block actions":"Funktionsblockaktionen",
+"Generate and print sales delivery orders":"Erzeuge und drucke Lieferscheine",
+"Get one shoporder":"Hole eine Bestellung",
+"Hide all details":"Alle Details verbergen",
+"Hide details":"Details verbergen",
+"History":"Historie",
 "If you switch to a different tab without saving you will lose the data you've entered in the current tab.":"Wenn Sie auf einen anderen Tab wechseln, ohne vorher zu speichern, so gehen die im aktuellen Tab eingegebenen Daten verloren.",
+"Import documents from #1":"Importiere Dateien von Quelle '#1'",
+"Invoice Date missing!":"Rechnungsdatum fehlt!",
+"Invoice Number missing!":"Rechnungsnummer fehlt!",
+"Jan":"Jan",
+"January":"Januar",
+"Jul":"Jul",
+"July":"Juli",
+"Jun":"Jun",
+"June":"Juni",
+"Leading and trailing whitespaces have been removed.":"Leerzeichen wurden vorne und hinten entfernt",
+"Loading...":"Wird geladen...",
 "Map":"Karte",
+"Mar":"März",
+"March":"März",
+"May":"Mai",
+"Mon":"Mo",
+"Monday":"Montag",
+"More than one file selected, please set only one checkbox!":"Mehr als ein Element selektiert, bitte nur eine Box anklicken",
+"Next month":"nächster Monat",
 "No":"Nein",
+"No article has been selected yet.":"Es wurde noch kein Artikel ausgewählt.",
 "No delivery orders have been selected.":"Es wurden keine Lieferscheine ausgewählt.",
+"No entries have been selected.":"Es wurden keine Einträge ausgewählt.",
+"No file selected, please set one checkbox!":"Kein Element selektiert,bitte eine Box anklicken",
 "No invoices have been selected.":"Es wurden keine Rechnungen ausgewählt.",
+"Nov":"Nov",
+"November":"November",
+"Oct":"Okt",
+"October":"Oktober",
+"Ok":"Ok",
 "Part picker":"Artikelauswahl",
 "Paste":"Einfügen",
 "Paste template":"Vorlage einfügen",
+"Please enter the new name:":"Bitte geben Sie den neuen Namen ein:",
+"Please enter values":"Bitte Werte eingeben",
+"Please select a customer.":"Bitte wählen Sie einen Kunden aus.",
+"Please select a delivery date.":"Bitte einen Liefertermin auswählen",
+"Please select a vendor.":"Bitte wählen Sie einen Lieferanten aus.",
+"Previous month":"vorheriger Monat",
 "Price Types":"Preistypen",
+"Print options":"Druckoptionen",
+"Print record":"Beleg drucken",
 "Project link actions":"Projektverknüpfungs-Aktionen",
+"Project picker":"Projektauswahl",
 "Quotations/Orders actions":"Aktionen für Angebote/Aufträge",
 "Re-numbering all sections and function blocks in the order they are currently shown cannot be undone.":"Das Neu-Nummerieren aller Abschnitte und Funktionsblöcke kann nicht rückgängig gemacht werden.",
 "Remove article":"Artikel entfernen",
+"Rename attachment":"Dateianhang umbenennen",
 "Renumber sections and function blocks":"Abschnitte/Funktionsblöcke neu nummerieren",
 "Requirement spec actions":"Pflichtenheftaktionen",
 "Requirement spec template actions":"Pflichtenheftvorlagen-Aktionen",
 "Revert to version":"Auf Version zurücksetzen",
+"Sat":"Sa",
+"Saturday":"Samstag",
 "Save":"Speichern",
 "Save and keep open":"Speichern und geöffnet lassen",
 "Section/Function block actions":"Abschnitts-/Funktionsblockaktionen",
 "Select template to paste":"Einzufügende Vorlage auswählen",
+"Send email":"E-Mail verschicken",
+"Sep":"Sep",
+"September":"September",
+"Set all source and memo fields":"Alle Beleg-/Memo-Felder setzen",
+"Shop Connection Test":"Shopverbindungstest",
+"Show all details":"Alle Details anzeigen",
+"Show details":"Details anzeigen",
+"Status Shoptransfer":"Status Shoptransfer",
+"Status Shopupload":"Status Shopupload",
+"Subject":"Betreff",
+"Sun":"So",
+"Sunday":"Sonntag",
 "Text block actions":"Textblockaktionen",
 "Text block picture actions":"Aktionen für Textblockbilder",
-"The IBAN is missing.":"Die IBAN fehlt.",
+"The URL is missing.":"URL fehlt",
+"The action can only be executed once.":"Die Aktion kann nur einmal ausgeführt werden.",
+"The customer order number is missing. Do you want to continue anyway?":"Die Kundenbestellnummer fehlt. Möchten Sie trotzdem fortfahren?",
 "The description is missing.":"Die Beschreibung fehlt.",
 "The name is missing.":"Der Name fehlt.",
 "The name must only consist of letters, numbers and underscores and start with a letter.":"Der Name darf nur aus Buchstaben (keine Umlaute), Ziffern und Unterstrichen bestehen und muss mit einem Buchstaben beginnen.",
 "The option field is empty.":"Das Optionsfeld ist leer.",
+"The port is missing.":"Port fehlt",
 "The recipient, subject or body is missing.":"Der Empfäger, der Betreff oder der Text ist leer.",
-"The selected database is still configured for client \"#1\". If you delete the database that client will stop working until you re-configure it. Do you still want to delete the database?":"Die auswählte Datenbank ist noch für Mandant \"#1\" konfiguriert. Wenn Sie die Datenbank löschen, wird der Mandanten nicht mehr funktionieren, bis er anders konfiguriert wurde. Wollen Sie die Datenbank trotzdem löschen?",
-"There are still transfers not matching the qty of the delivery order. Stock operations can not be changed later. Do you really want to proceed?":"Einige der Lagerbewegungen sind nicht vollständig und Lagerbewegungen können nachträglich nicht mehr verändert werden. Wollen Sie wirklich fortfahren?",
-"There is no connected chart.":"Es fehlt ein verknüpftes Buchungskonto.",
+"The selected database is still configured for client \"#1\". If you delete the database that client will stop working until you re-configure it. Do you still want to delete the database?":"Die auswählte Datenbank ist noch für Mandant \"#1\" konfiguriert. Wenn Sie die Datenbank löschen, wird der Mandanten nicht mehr funktionieren, bis er anders konfiguriert wurde. Möchten Sie die Datenbank trotzdem löschen?",
+"The transfer has been canceled by the user.":"Der Vorgang wurde durch den Benutzer abgebrochen.",
+"The transport cost article '#1' is missing. Do you want to continue anyway?":"Der Transportkostenartikel »#1« fehlt. Möchten Sie trotzdem fortfahren?",
+"The uploaded filename still exists.<br>If you not modify the name this is a new version of the file":"Der Dateiname existiert bereits.<br>Wenn Sie den Namen nicht ändern gibt dies eine neue Version der Datei",
+"There are duplicate parts at positions":"Es gibt doppelte Artikel bei den Positionen",
+"There are still transfers not matching the qty of the delivery order. Stock operations can not be changed later. Do you really want to proceed?":"Einige der Lagerbewegungen sind nicht vollständig und Lagerbewegungen können nachträglich nicht mehr verändert werden. Möchten Sie wirklich fortfahren?",
 "There is one or more sections for which no part has been assigned yet; therefore creating the new record is not possible yet.":"Es gibt einen oder mehrere Abschnitte ohne Artikelzuweisung; daher kann der neue Beleg noch nicht erstellt werden.",
-"This sales order has an active configuration for periodic invoices. If you save then all subsequently created invoices will contain those changes as well, but not those that have already been created. Do you want to continue?":"Dieser Auftrag besitzt eine aktive Konfiguration für wiederkehrende Rechnungen. Wenn Sie jetzt speichern, so werden alle zukünftig hieraus erzeugten Rechnungen die Änderungen enthalten, nicht aber die bereits erzeugten Rechnungen. Wollen Sie speichern?",
+"This field must not be empty.":"Dieses Feld darf nicht leer sein.",
+"This sales order has an active configuration for periodic invoices. If you save then all subsequently created invoices will contain those changes as well, but not those that have already been created. Do you want to continue?":"Dieser Auftrag besitzt eine aktive Konfiguration für wiederkehrende Rechnungen. Wenn Sie jetzt speichern, so werden alle zukünftig hieraus erzeugten Rechnungen die Änderungen enthalten, nicht aber die bereits erzeugten Rechnungen. Möchten Sie speichern?",
+"This vendor has already a booking with this invoice number, do you really want to add the same invoice number again?":"Es gibt für diesen Lieferant schon einen Beleg mit dieser Rechnungsnummer. Möchten Sie wirklich eine weitere Buchung mit derselben Rechnungsnummer hinzufügen?",
+"Thu":"Do",
+"Thursday":"Donnerstag",
 "Time/cost estimate actions":"Aktionen für Kosten-/Zeitabschätzung",
 "Title":"Titel",
+"Today":"heute",
 "Toggle marker":"Markierung umschalten",
 "Transaction description":"Vorgangsbezeichnung",
+"Transfer stock":"Lagerbewegungen",
+"Tue":"Di",
+"Tuesday":"Dienstag",
 "Update":"Erneuern",
 "Update quotation/order":"Auftrag/Angebot aktualisieren",
+"Upload Status":"Upload-Status",
+"Vendor details":"Lieferantendetails",
+"Vendor missing!":"Lieferant fehlt!",
 "Version actions":"Aktionen für Versionen",
+"Wed":"Mi",
+"Wednesday":"Mittwoch",
+"Wrong date format (#1)":"Falsches Datumsformat (#1)",
+"Wrong number format (#1)":"Falsches Zahlenformat (#1)",
+"Wrong time format (#1)":"Falsches Zeitformat (#1)",
 "Yes":"Ja",
+"You have changed the currency or exchange rate. Please check prices.":"Die Währung oder der Wechselkurs hat sich geändert. Bitte überprüfen Sie die Preise.",
+"You have entered or selected the following shipping address for this customer:":"Sie haben die folgende Lieferadresse eingegeben oder ausgewählt:",
+"close":"schließen",
+"filename has not uploadable characters ":"Bitte Dateinamen ändern. Er hat für den Upload nicht verwendbare Sonderzeichen ",
+"filesize too big: ":"Datei zu groß: ",
 "flat-rate position":"Pauschalposition",
-"time and effort based position":"Aufwandsposition"
+"sort items":"Positionen sortieren",
+"start upload":"Hochladen beginnt",
+"time and effort based position":"Aufwandsposition",
+"uploaded":"Hochgeladen"
 });
diff --git a/js/locale/en.js b/js/locale/en.js
new file mode 100644 (file)
index 0000000..4306694
--- /dev/null
@@ -0,0 +1,208 @@
+namespace("kivi").setupLocale({
+" bytes, max=":"",
+"A transaction description is required.":"",
+"Add function block":"",
+"Add linked record":"",
+"Add multiple items":"",
+"Add note":"",
+"Add picture":"",
+"Add picture to text block":"",
+"Add section":"",
+"Add sub function block":"",
+"Add text block":"",
+"Additional articles actions":"",
+"An error occurred while transferring the file.":"",
+"Apr":"",
+"April":"",
+"Are you sure you want to update the selected record template with the current values? This cannot be undone.":"",
+"Are you sure?":"",
+"Assign invoice":"",
+"Aug":"",
+"August":"",
+"Basic settings actions":"",
+"Cancel":"",
+"Carry over shipping address":"",
+"Chart picker":"",
+"Clear":"",
+"Copy":"",
+"Copy requirement spec":"",
+"Copy template":"",
+"Create":"",
+"Create HTML":"",
+"Create PDF":"",
+"Create a new version":"",
+"Create and print all invoices":"",
+"Create invoice":"",
+"Create new quotation/order":"",
+"Create new qutoation/order":"",
+"Create new version":"",
+"Customer details":"",
+"Customer missing!":"",
+"Database Connection Test":"",
+"Dec":"",
+"December":"",
+"Delete":"",
+"Delete picture":"",
+"Delete quotation/order":"",
+"Delete requirement spec":"",
+"Delete template":"",
+"Delete text block":"",
+"Do you really want to cancel?":"",
+"Do you really want to continue?":"",
+"Do you really want to delete the selected documents?":"",
+"Do you really want to delete this draft?":"",
+"Do you really want to delete this record template?":"",
+"Do you really want to print?":"",
+"Do you really want to revert to this version?":"",
+"Do you really want to unimport the selected documents?":"",
+"Do you want to carry this shipping address over to the new purchase order so that the vendor can deliver the goods directly to your customer?":"",
+"Do you want to set the account number \"#1\" to \"#2\" and the name \"#3\" to \"#4\"?":"",
+"Download picture":"",
+"Due Date missing!":"",
+"Edit":"",
+"Edit article/section assignments":"",
+"Edit custom shipto":"",
+"Edit note":"",
+"Edit picture":"",
+"Edit project link":"",
+"Edit text block":"",
+"Edit the configuration for periodic invoices":"",
+"Enter longdescription":"",
+"Error: Name missing":"",
+"Feb":"",
+"February":"",
+"File upload":"",
+"Files have been uploaded successfully.":"",
+"Fri":"",
+"Friday":"",
+"Function block actions":"",
+"Generate and print sales delivery orders":"",
+"Get one shoporder":"",
+"Hide all details":"",
+"Hide details":"",
+"History":"",
+"If you switch to a different tab without saving you will lose the data you've entered in the current tab.":"",
+"Import documents from #1":"",
+"Invoice Date missing!":"",
+"Invoice Number missing!":"",
+"Jan":"",
+"January":"",
+"Jul":"",
+"July":"",
+"Jun":"",
+"June":"",
+"Leading and trailing whitespaces have been removed.":"",
+"Loading...":"",
+"Map":"",
+"Mar":"",
+"March":"",
+"May":"",
+"Mon":"",
+"Monday":"",
+"More than one file selected, please set only one checkbox!":"",
+"Next month":"",
+"No":"",
+"No article has been selected yet.":"",
+"No delivery orders have been selected.":"",
+"No entries have been selected.":"",
+"No file selected, please set one checkbox!":"",
+"No invoices have been selected.":"",
+"Nov":"",
+"November":"",
+"Oct":"",
+"October":"",
+"Ok":"",
+"Part picker":"",
+"Paste":"",
+"Paste template":"",
+"Please enter the new name:":"",
+"Please enter values":"",
+"Please select a customer.":"",
+"Please select a delivery date.":"",
+"Please select a vendor.":"",
+"Previous month":"",
+"Price Types":"",
+"Print options":"",
+"Print record":"",
+"Project link actions":"",
+"Project picker":"",
+"Quotations/Orders actions":"",
+"Re-numbering all sections and function blocks in the order they are currently shown cannot be undone.":"",
+"Remove article":"",
+"Rename attachment":"",
+"Renumber sections and function blocks":"",
+"Requirement spec actions":"",
+"Requirement spec template actions":"",
+"Revert to version":"",
+"Sat":"",
+"Saturday":"",
+"Save":"",
+"Save and keep open":"",
+"Section/Function block actions":"",
+"Select template to paste":"",
+"Send email":"",
+"Sep":"",
+"September":"",
+"Set all source and memo fields":"",
+"Shop Connection Test":"",
+"Show all details":"",
+"Show details":"",
+"Status Shoptransfer":"",
+"Status Shopupload":"",
+"Subject":"",
+"Sun":"",
+"Sunday":"",
+"Text block actions":"",
+"Text block picture actions":"",
+"The URL is missing.":"",
+"The action can only be executed once.":"",
+"The customer order number is missing. Do you want to continue anyway?":"",
+"The description is missing.":"",
+"The name is missing.":"",
+"The name must only consist of letters, numbers and underscores and start with a letter.":"",
+"The option field is empty.":"",
+"The port is missing.":"",
+"The recipient, subject or body is missing.":"",
+"The selected database is still configured for client \"#1\". If you delete the database that client will stop working until you re-configure it. Do you still want to delete the database?":"",
+"The transfer has been canceled by the user.":"",
+"The transport cost article '#1' is missing. Do you want to continue anyway?":"",
+"The uploaded filename still exists.<br>If you not modify the name this is a new version of the file":"",
+"There are duplicate parts at positions":"",
+"There are still transfers not matching the qty of the delivery order. Stock operations can not be changed later. Do you really want to proceed?":"",
+"There is one or more sections for which no part has been assigned yet; therefore creating the new record is not possible yet.":"",
+"This field must not be empty.":"",
+"This sales order has an active configuration for periodic invoices. If you save then all subsequently created invoices will contain those changes as well, but not those that have already been created. Do you want to continue?":"",
+"This vendor has already a booking with this invoice number, do you really want to add the same invoice number again?":"",
+"Thu":"",
+"Thursday":"",
+"Time/cost estimate actions":"",
+"Title":"",
+"Today":"",
+"Toggle marker":"",
+"Transaction description":"",
+"Transfer stock":"",
+"Tue":"",
+"Tuesday":"",
+"Update":"",
+"Update quotation/order":"",
+"Upload Status":"",
+"Vendor details":"",
+"Vendor missing!":"",
+"Version actions":"",
+"Wed":"",
+"Wednesday":"",
+"Wrong date format (#1)":"",
+"Wrong number format (#1)":"",
+"Wrong time format (#1)":"",
+"Yes":"",
+"You have changed the currency or exchange rate. Please check prices.":"",
+"You have entered or selected the following shipping address for this customer:":"",
+"close":"",
+"filename has not uploadable characters ":"",
+"filesize too big: ":"",
+"flat-rate position":"",
+"sort items":"",
+"start upload":"",
+"time and effort based position":"",
+"uploaded":""
+});
diff --git a/js/materialize.js b/js/materialize.js
new file mode 120000 (symlink)
index 0000000..91be615
--- /dev/null
@@ -0,0 +1 @@
+materialize/materialize.min.js
\ No newline at end of file
diff --git a/js/materialize/materialize.min.js b/js/materialize/materialize.min.js
new file mode 100644 (file)
index 0000000..7d80c93
--- /dev/null
@@ -0,0 +1,6 @@
+/*!
+ * Materialize v1.0.0 (http://materializecss.com)
+ * Copyright 2014-2017 Materialize
+ * MIT License (https://raw.githubusercontent.com/Dogfalo/materialize/master/LICENSE)
+ */
+var _get=function t(e,i,n){null===e&&(e=Function.prototype);var s=Object.getOwnPropertyDescriptor(e,i);if(void 0===s){var o=Object.getPrototypeOf(e);return null===o?void 0:t(o,i,n)}if("value"in s)return s.value;var a=s.get;return void 0!==a?a.call(n):void 0},_createClass=function(){function n(t,e){for(var i=0;i<e.length;i++){var n=e[i];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(t,e,i){return e&&n(t.prototype,e),i&&n(t,i),t}}();function _possibleConstructorReturn(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function _inherits(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function _classCallCheck(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}window.cash=function(){var i,o=document,a=window,t=Array.prototype,r=t.slice,n=t.filter,s=t.push,e=function(){},h=function(t){return typeof t==typeof e&&t.call},d=function(t){return"string"==typeof t},l=/^#[\w-]*$/,u=/^\.[\w-]*$/,c=/<.+>/,p=/^\w+$/;function v(t,e){e=e||o;var i=u.test(t)?e.getElementsByClassName(t.slice(1)):p.test(t)?e.getElementsByTagName(t):e.querySelectorAll(t);return i}function f(t){if(!i){var e=(i=o.implementation.createHTMLDocument(null)).createElement("base");e.href=o.location.href,i.head.appendChild(e)}return i.body.innerHTML=t,i.body.childNodes}function m(t){"loading"!==o.readyState?t():o.addEventListener("DOMContentLoaded",t)}function g(t,e){if(!t)return this;if(t.cash&&t!==a)return t;var i,n=t,s=0;if(d(t))n=l.test(t)?o.getElementById(t.slice(1)):c.test(t)?f(t):v(t,e);else if(h(t))return m(t),this;if(!n)return this;if(n.nodeType||n===a)this[0]=n,this.length=1;else for(i=this.length=n.length;s<i;s++)this[s]=n[s];return this}function _(t,e){return new g(t,e)}var y=_.fn=_.prototype=g.prototype={cash:!0,length:0,push:s,splice:t.splice,map:t.map,init:g};function k(t,e){for(var i=t.length,n=0;n<i&&!1!==e.call(t[n],t[n],n,t);n++);}function b(t,e){var i=t&&(t.matches||t.webkitMatchesSelector||t.mozMatchesSelector||t.msMatchesSelector||t.oMatchesSelector);return!!i&&i.call(t,e)}function w(e){return d(e)?b:e.cash?function(t){return e.is(t)}:function(t,e){return t===e}}function C(t){return _(r.call(t).filter(function(t,e,i){return i.indexOf(t)===e}))}Object.defineProperty(y,"constructor",{value:_}),_.parseHTML=f,_.noop=e,_.isFunction=h,_.isString=d,_.extend=y.extend=function(t){t=t||{};var e=r.call(arguments),i=e.length,n=1;for(1===e.length&&(t=this,n=0);n<i;n++)if(e[n])for(var s in e[n])e[n].hasOwnProperty(s)&&(t[s]=e[n][s]);return t},_.extend({merge:function(t,e){for(var i=+e.length,n=t.length,s=0;s<i;n++,s++)t[n]=e[s];return t.length=n,t},each:k,matches:b,unique:C,isArray:Array.isArray,isNumeric:function(t){return!isNaN(parseFloat(t))&&isFinite(t)}});var E=_.uid="_cash"+Date.now();function M(t){return t[E]=t[E]||{}}function O(t,e,i){return M(t)[e]=i}function x(t,e){var i=M(t);return void 0===i[e]&&(i[e]=t.dataset?t.dataset[e]:_(t).attr("data-"+e)),i[e]}y.extend({data:function(e,i){if(d(e))return void 0===i?x(this[0],e):this.each(function(t){return O(t,e,i)});for(var t in e)this.data(t,e[t]);return this},removeData:function(s){return this.each(function(t){return i=s,void((n=M(e=t))?delete n[i]:e.dataset?delete e.dataset[i]:_(e).removeAttr("data-"+name));var e,i,n})}});var L=/\S+/g;function T(t){return d(t)&&t.match(L)}function $(t,e){return t.classList?t.classList.contains(e):new RegExp("(^| )"+e+"( |$)","gi").test(t.className)}function B(t,e,i){t.classList?t.classList.add(e):i.indexOf(" "+e+" ")&&(t.className+=" "+e)}function D(t,e){t.classList?t.classList.remove(e):t.className=t.className.replace(e,"")}y.extend({addClass:function(t){var n=T(t);return n?this.each(function(e){var i=" "+e.className+" ";k(n,function(t){B(e,t,i)})}):this},attr:function(e,i){if(e){if(d(e))return void 0===i?this[0]?this[0].getAttribute?this[0].getAttribute(e):this[0][e]:void 0:this.each(function(t){t.setAttribute?t.setAttribute(e,i):t[e]=i});for(var t in e)this.attr(t,e[t]);return this}},hasClass:function(t){var e=!1,i=T(t);return i&&i.length&&this.each(function(t){return!(e=$(t,i[0]))}),e},prop:function(e,i){if(d(e))return void 0===i?this[0][e]:this.each(function(t){t[e]=i});for(var t in e)this.prop(t,e[t]);return this},removeAttr:function(e){return this.each(function(t){t.removeAttribute?t.removeAttribute(e):delete t[e]})},removeClass:function(t){if(!arguments.length)return this.attr("class","");var i=T(t);return i?this.each(function(e){k(i,function(t){D(e,t)})}):this},removeProp:function(e){return this.each(function(t){delete t[e]})},toggleClass:function(t,e){if(void 0!==e)return this[e?"addClass":"removeClass"](t);var n=T(t);return n?this.each(function(e){var i=" "+e.className+" ";k(n,function(t){$(e,t)?D(e,t):B(e,t,i)})}):this}}),y.extend({add:function(t,e){return C(_.merge(this,_(t,e)))},each:function(t){return k(this,t),this},eq:function(t){return _(this.get(t))},filter:function(e){if(!e)return this;var i=h(e)?e:w(e);return _(n.call(this,function(t){return i(t,e)}))},first:function(){return this.eq(0)},get:function(t){return void 0===t?r.call(this):t<0?this[t+this.length]:this[t]},index:function(t){var e=t?_(t)[0]:this[0],i=t?this:_(e).parent().children();return r.call(i).indexOf(e)},last:function(){return this.eq(-1)}});var S,I,A,R,H,P,W=(H=/(?:^\w|[A-Z]|\b\w)/g,P=/[\s-_]+/g,function(t){return t.replace(H,function(t,e){return t[0===e?"toLowerCase":"toUpperCase"]()}).replace(P,"")}),j=(S={},I=document,A=I.createElement("div"),R=A.style,function(e){if(e=W(e),S[e])return S[e];var t=e.charAt(0).toUpperCase()+e.slice(1),i=(e+" "+["webkit","moz","ms","o"].join(t+" ")+t).split(" ");return k(i,function(t){if(t in R)return S[t]=e=S[e]=t,!1}),S[e]});function F(t,e){return parseInt(a.getComputedStyle(t[0],null)[e],10)||0}function q(e,i,t){var n,s=x(e,"_cashEvents"),o=s&&s[i];o&&(t?(e.removeEventListener(i,t),0<=(n=o.indexOf(t))&&o.splice(n,1)):(k(o,function(t){e.removeEventListener(i,t)}),o=[]))}function N(t,e){return"&"+encodeURIComponent(t)+"="+encodeURIComponent(e).replace(/%20/g,"+")}function z(t){var e,i,n,s=t.type;if(!s)return null;switch(s.toLowerCase()){case"select-one":return 0<=(n=(i=t).selectedIndex)?i.options[n].value:null;case"select-multiple":return e=[],k(t.options,function(t){t.selected&&e.push(t.value)}),e.length?e:null;case"radio":case"checkbox":return t.checked?t.value:null;default:return t.value?t.value:null}}function V(e,i,n){var t=d(i);t||!i.length?k(e,t?function(t){return t.insertAdjacentHTML(n?"afterbegin":"beforeend",i)}:function(t,e){return function(t,e,i){if(i){var n=t.childNodes[0];t.insertBefore(e,n)}else t.appendChild(e)}(t,0===e?i:i.cloneNode(!0),n)}):k(i,function(t){return V(e,t,n)})}_.prefixedProp=j,_.camelCase=W,y.extend({css:function(e,i){if(d(e))return e=j(e),1<arguments.length?this.each(function(t){return t.style[e]=i}):a.getComputedStyle(this[0])[e];for(var t in e)this.css(t,e[t]);return this}}),k(["Width","Height"],function(e){var t=e.toLowerCase();y[t]=function(){return this[0].getBoundingClientRect()[t]},y["inner"+e]=function(){return this[0]["client"+e]},y["outer"+e]=function(t){return this[0]["offset"+e]+(t?F(this,"margin"+("Width"===e?"Left":"Top"))+F(this,"margin"+("Width"===e?"Right":"Bottom")):0)}}),y.extend({off:function(e,i){return this.each(function(t){return q(t,e,i)})},on:function(a,i,r,l){var n;if(!d(a)){for(var t in a)this.on(t,i,a[t]);return this}return h(i)&&(r=i,i=null),"ready"===a?(m(r),this):(i&&(n=r,r=function(t){for(var e=t.target;!b(e,i);){if(e===this||null===e)return e=!1;e=e.parentNode}e&&n.call(e,t)}),this.each(function(t){var e,i,n,s,o=r;l&&(o=function(){r.apply(this,arguments),q(t,a,o)}),i=a,n=o,(s=x(e=t,"_cashEvents")||O(e,"_cashEvents",{}))[i]=s[i]||[],s[i].push(n),e.addEventListener(i,n)}))},one:function(t,e,i){return this.on(t,e,i,!0)},ready:m,trigger:function(t,e){if(document.createEvent){var i=document.createEvent("HTMLEvents");return i.initEvent(t,!0,!1),i=this.extend(i,e),this.each(function(t){return t.dispatchEvent(i)})}}}),y.extend({serialize:function(){var s="";return k(this[0].elements||this,function(t){if(!t.disabled&&"FIELDSET"!==t.tagName){var e=t.name;switch(t.type.toLowerCase()){case"file":case"reset":case"submit":case"button":break;case"select-multiple":var i=z(t);null!==i&&k(i,function(t){s+=N(e,t)});break;default:var n=z(t);null!==n&&(s+=N(e,n))}}}),s.substr(1)},val:function(e){return void 0===e?z(this[0]):this.each(function(t){return t.value=e})}}),y.extend({after:function(t){return _(t).insertAfter(this),this},append:function(t){return V(this,t),this},appendTo:function(t){return V(_(t),this),this},before:function(t){return _(t).insertBefore(this),this},clone:function(){return _(this.map(function(t){return t.cloneNode(!0)}))},empty:function(){return this.html(""),this},html:function(t){if(void 0===t)return this[0].innerHTML;var e=t.nodeType?t[0].outerHTML:t;return this.each(function(t){return t.innerHTML=e})},insertAfter:function(t){var s=this;return _(t).each(function(t,e){var i=t.parentNode,n=t.nextSibling;s.each(function(t){i.insertBefore(0===e?t:t.cloneNode(!0),n)})}),this},insertBefore:function(t){var s=this;return _(t).each(function(e,i){var n=e.parentNode;s.each(function(t){n.insertBefore(0===i?t:t.cloneNode(!0),e)})}),this},prepend:function(t){return V(this,t,!0),this},prependTo:function(t){return V(_(t),this,!0),this},remove:function(){return this.each(function(t){if(t.parentNode)return t.parentNode.removeChild(t)})},text:function(e){return void 0===e?this[0].textContent:this.each(function(t){return t.textContent=e})}});var X=o.documentElement;return y.extend({position:function(){var t=this[0];return{left:t.offsetLeft,top:t.offsetTop}},offset:function(){var t=this[0].getBoundingClientRect();return{top:t.top+a.pageYOffset-X.clientTop,left:t.left+a.pageXOffset-X.clientLeft}},offsetParent:function(){return _(this[0].offsetParent)}}),y.extend({children:function(e){var i=[];return this.each(function(t){s.apply(i,t.children)}),i=C(i),e?i.filter(function(t){return b(t,e)}):i},closest:function(t){return!t||this.length<1?_():this.is(t)?this.filter(t):this.parent().closest(t)},is:function(e){if(!e)return!1;var i=!1,n=w(e);return this.each(function(t){return!(i=n(t,e))}),i},find:function(e){if(!e||e.nodeType)return _(e&&this.has(e).length?e:null);var i=[];return this.each(function(t){s.apply(i,v(e,t))}),C(i)},has:function(e){var t=d(e)?function(t){return 0!==v(e,t).length}:function(t){return t.contains(e)};return this.filter(t)},next:function(){return _(this[0].nextElementSibling)},not:function(e){if(!e)return this;var i=w(e);return this.filter(function(t){return!i(t,e)})},parent:function(){var e=[];return this.each(function(t){t&&t.parentNode&&e.push(t.parentNode)}),C(e)},parents:function(e){var i,n=[];return this.each(function(t){for(i=t;i&&i.parentNode&&i!==o.body.parentNode;)i=i.parentNode,(!e||e&&b(i,e))&&n.push(i)}),C(n)},prev:function(){return _(this[0].previousElementSibling)},siblings:function(t){var e=this.parent().children(t),i=this[0];return e.filter(function(t){return t!==i})}}),_}();var Component=function(){function s(t,e,i){_classCallCheck(this,s),e instanceof Element||console.error(Error(e+" is not an HTML Element"));var n=t.getInstance(e);n&&n.destroy(),this.el=e,this.$el=cash(e)}return _createClass(s,null,[{key:"init",value:function(t,e,i){var n=null;if(e instanceof Element)n=new t(e,i);else if(e&&(e.jquery||e.cash||e instanceof NodeList)){for(var s=[],o=0;o<e.length;o++)s.push(new t(e[o],i));n=s}return n}}]),s}();!function(t){t.Package?M={}:t.M={},M.jQueryLoaded=!!t.jQuery}(window),"function"==typeof define&&define.amd?define("M",[],function(){return M}):"undefined"==typeof exports||exports.nodeType||("undefined"!=typeof module&&!module.nodeType&&module.exports&&(exports=module.exports=M),exports.default=M),M.version="1.0.0",M.keys={TAB:9,ENTER:13,ESC:27,ARROW_UP:38,ARROW_DOWN:40},M.tabPressed=!1,M.keyDown=!1;var docHandleKeydown=function(t){M.keyDown=!0,t.which!==M.keys.TAB&&t.which!==M.keys.ARROW_DOWN&&t.which!==M.keys.ARROW_UP||(M.tabPressed=!0)},docHandleKeyup=function(t){M.keyDown=!1,t.which!==M.keys.TAB&&t.which!==M.keys.ARROW_DOWN&&t.which!==M.keys.ARROW_UP||(M.tabPressed=!1)},docHandleFocus=function(t){M.keyDown&&document.body.classList.add("keyboard-focused")},docHandleBlur=function(t){document.body.classList.remove("keyboard-focused")};document.addEventListener("keydown",docHandleKeydown,!0),document.addEventListener("keyup",docHandleKeyup,!0),document.addEventListener("focus",docHandleFocus,!0),document.addEventListener("blur",docHandleBlur,!0),M.initializeJqueryWrapper=function(n,s,o){jQuery.fn[s]=function(e){if(n.prototype[e]){var i=Array.prototype.slice.call(arguments,1);if("get"===e.slice(0,3)){var t=this.first()[0][o];return t[e].apply(t,i)}return this.each(function(){var t=this[o];t[e].apply(t,i)})}if("object"==typeof e||!e)return n.init(this,e),this;jQuery.error("Method "+e+" does not exist on jQuery."+s)}},M.AutoInit=function(t){var e=t||document.body,i={Autocomplete:e.querySelectorAll(".autocomplete:not(.no-autoinit)"),Carousel:e.querySelectorAll(".carousel:not(.no-autoinit)"),Chips:e.querySelectorAll(".chips:not(.no-autoinit)"),Collapsible:e.querySelectorAll(".collapsible:not(.no-autoinit)"),Datepicker:e.querySelectorAll(".datepicker:not(.no-autoinit)"),Dropdown:e.querySelectorAll(".dropdown-trigger:not(.no-autoinit)"),Materialbox:e.querySelectorAll(".materialboxed:not(.no-autoinit)"),Modal:e.querySelectorAll(".modal:not(.no-autoinit)"),Parallax:e.querySelectorAll(".parallax:not(.no-autoinit)"),Pushpin:e.querySelectorAll(".pushpin:not(.no-autoinit)"),ScrollSpy:e.querySelectorAll(".scrollspy:not(.no-autoinit)"),FormSelect:e.querySelectorAll("select:not(.no-autoinit)"),Sidenav:e.querySelectorAll(".sidenav:not(.no-autoinit)"),Tabs:e.querySelectorAll(".tabs:not(.no-autoinit)"),TapTarget:e.querySelectorAll(".tap-target:not(.no-autoinit)"),Timepicker:e.querySelectorAll(".timepicker:not(.no-autoinit)"),Tooltip:e.querySelectorAll(".tooltipped:not(.no-autoinit)"),FloatingActionButton:e.querySelectorAll(".fixed-action-btn:not(.no-autoinit)")};for(var n in i){M[n].init(i[n])}},M.objectSelectorString=function(t){return((t.prop("tagName")||"")+(t.attr("id")||"")+(t.attr("class")||"")).replace(/\s/g,"")},M.guid=function(){function t(){return Math.floor(65536*(1+Math.random())).toString(16).substring(1)}return function(){return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()}}(),M.escapeHash=function(t){return t.replace(/(:|\.|\[|\]|,|=|\/)/g,"\\$1")},M.elementOrParentIsFixed=function(t){var e=$(t),i=e.add(e.parents()),n=!1;return i.each(function(){if("fixed"===$(this).css("position"))return!(n=!0)}),n},M.checkWithinContainer=function(t,e,i){var n={top:!1,right:!1,bottom:!1,left:!1},s=t.getBoundingClientRect(),o=t===document.body?Math.max(s.bottom,window.innerHeight):s.bottom,a=t.scrollLeft,r=t.scrollTop,l=e.left-a,h=e.top-r;return(l<s.left+i||l<i)&&(n.left=!0),(l+e.width>s.right-i||l+e.width>window.innerWidth-i)&&(n.right=!0),(h<s.top+i||h<i)&&(n.top=!0),(h+e.height>o-i||h+e.height>window.innerHeight-i)&&(n.bottom=!0),n},M.checkPossibleAlignments=function(t,e,i,n){var s={top:!0,right:!0,bottom:!0,left:!0,spaceOnTop:null,spaceOnRight:null,spaceOnBottom:null,spaceOnLeft:null},o="visible"===getComputedStyle(e).overflow,a=e.getBoundingClientRect(),r=Math.min(a.height,window.innerHeight),l=Math.min(a.width,window.innerWidth),h=t.getBoundingClientRect(),d=e.scrollLeft,u=e.scrollTop,c=i.left-d,p=i.top-u,v=i.top+h.height-u;return s.spaceOnRight=o?window.innerWidth-(h.left+i.width):l-(c+i.width),s.spaceOnRight<0&&(s.left=!1),s.spaceOnLeft=o?h.right-i.width:c-i.width+h.width,s.spaceOnLeft<0&&(s.right=!1),s.spaceOnBottom=o?window.innerHeight-(h.top+i.height+n):r-(p+i.height+n),s.spaceOnBottom<0&&(s.top=!1),s.spaceOnTop=o?h.bottom-(i.height+n):v-(i.height-n),s.spaceOnTop<0&&(s.bottom=!1),s},M.getOverflowParent=function(t){return null==t?null:t===document.body||"visible"!==getComputedStyle(t).overflow?t:M.getOverflowParent(t.parentElement)},M.getIdFromTrigger=function(t){var e=t.getAttribute("data-target");return e||(e=(e=t.getAttribute("href"))?e.slice(1):""),e},M.getDocumentScrollTop=function(){return window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0},M.getDocumentScrollLeft=function(){return window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft||0};var getTime=Date.now||function(){return(new Date).getTime()};M.throttle=function(i,n,s){var o=void 0,a=void 0,r=void 0,l=null,h=0;s||(s={});var d=function(){h=!1===s.leading?0:getTime(),l=null,r=i.apply(o,a),o=a=null};return function(){var t=getTime();h||!1!==s.leading||(h=t);var e=n-(t-h);return o=this,a=arguments,e<=0?(clearTimeout(l),l=null,h=t,r=i.apply(o,a),o=a=null):l||!1===s.trailing||(l=setTimeout(d,e)),r}};var $jscomp={scope:{}};$jscomp.defineProperty="function"==typeof Object.defineProperties?Object.defineProperty:function(t,e,i){if(i.get||i.set)throw new TypeError("ES3 does not support getters and setters.");t!=Array.prototype&&t!=Object.prototype&&(t[e]=i.value)},$jscomp.getGlobal=function(t){return"undefined"!=typeof window&&window===t?t:"undefined"!=typeof global&&null!=global?global:t},$jscomp.global=$jscomp.getGlobal(this),$jscomp.SYMBOL_PREFIX="jscomp_symbol_",$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){},$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)},$jscomp.symbolCounter_=0,$jscomp.Symbol=function(t){return $jscomp.SYMBOL_PREFIX+(t||"")+$jscomp.symbolCounter_++},$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var t=$jscomp.global.Symbol.iterator;t||(t=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator")),"function"!=typeof Array.prototype[t]&&$jscomp.defineProperty(Array.prototype,t,{configurable:!0,writable:!0,value:function(){return $jscomp.arrayIterator(this)}}),$jscomp.initSymbolIterator=function(){}},$jscomp.arrayIterator=function(t){var e=0;return $jscomp.iteratorPrototype(function(){return e<t.length?{done:!1,value:t[e++]}:{done:!0}})},$jscomp.iteratorPrototype=function(t){return $jscomp.initSymbolIterator(),(t={next:t})[$jscomp.global.Symbol.iterator]=function(){return this},t},$jscomp.array=$jscomp.array||{},$jscomp.iteratorFromArray=function(e,i){$jscomp.initSymbolIterator(),e instanceof String&&(e+="");var n=0,s={next:function(){if(n<e.length){var t=n++;return{value:i(t,e[t]),done:!1}}return s.next=function(){return{done:!0,value:void 0}},s.next()}};return s[Symbol.iterator]=function(){return s},s},$jscomp.polyfill=function(t,e,i,n){if(e){for(i=$jscomp.global,t=t.split("."),n=0;n<t.length-1;n++){var s=t[n];s in i||(i[s]={}),i=i[s]}(e=e(n=i[t=t[t.length-1]]))!=n&&null!=e&&$jscomp.defineProperty(i,t,{configurable:!0,writable:!0,value:e})}},$jscomp.polyfill("Array.prototype.keys",function(t){return t||function(){return $jscomp.iteratorFromArray(this,function(t){return t})}},"es6-impl","es3");var $jscomp$this=this;M.anime=function(){function s(t){if(!B.col(t))try{return document.querySelectorAll(t)}catch(t){}}function b(t,e){for(var i=t.length,n=2<=arguments.length?e:void 0,s=[],o=0;o<i;o++)if(o in t){var a=t[o];e.call(n,a,o,t)&&s.push(a)}return s}function d(t){return t.reduce(function(t,e){return t.concat(B.arr(e)?d(e):e)},[])}function o(t){return B.arr(t)?t:(B.str(t)&&(t=s(t)||t),t instanceof NodeList||t instanceof HTMLCollection?[].slice.call(t):[t])}function a(t,e){return t.some(function(t){return t===e})}function r(t){var e,i={};for(e in t)i[e]=t[e];return i}function u(t,e){var i,n=r(t);for(i in t)n[i]=e.hasOwnProperty(i)?e[i]:t[i];return n}function c(t,e){var i,n=r(t);for(i in e)n[i]=B.und(t[i])?e[i]:t[i];return n}function l(t){if(t=/([\+\-]?[0-9#\.]+)(%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vmin|vmax|deg|rad|turn)?$/.exec(t))return t[2]}function h(t,e){return B.fnc(t)?t(e.target,e.id,e.total):t}function w(t,e){if(e in t.style)return getComputedStyle(t).getPropertyValue(e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase())||"0"}function p(t,e){return B.dom(t)&&a($,e)?"transform":B.dom(t)&&(t.getAttribute(e)||B.svg(t)&&t[e])?"attribute":B.dom(t)&&"transform"!==e&&w(t,e)?"css":null!=t[e]?"object":void 0}function v(t,e){switch(p(t,e)){case"transform":return function(t,i){var e,n=-1<(e=i).indexOf("translate")||"perspective"===e?"px":-1<e.indexOf("rotate")||-1<e.indexOf("skew")?"deg":void 0,n=-1<i.indexOf("scale")?1:0+n;if(!(t=t.style.transform))return n;for(var s=[],o=[],a=[],r=/(\w+)\((.+?)\)/g;s=r.exec(t);)o.push(s[1]),a.push(s[2]);return(t=b(a,function(t,e){return o[e]===i})).length?t[0]:n}(t,e);case"css":return w(t,e);case"attribute":return t.getAttribute(e)}return t[e]||0}function f(t,e){var i=/^(\*=|\+=|-=)/.exec(t);if(!i)return t;var n=l(t)||0;switch(e=parseFloat(e),t=parseFloat(t.replace(i[0],"")),i[0][0]){case"+":return e+t+n;case"-":return e-t+n;case"*":return e*t+n}}function m(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function i(t){t=t.points;for(var e,i=0,n=0;n<t.numberOfItems;n++){var s=t.getItem(n);0<n&&(i+=m(e,s)),e=s}return i}function g(t){if(t.getTotalLength)return t.getTotalLength();switch(t.tagName.toLowerCase()){case"circle":return 2*Math.PI*t.getAttribute("r");case"rect":return 2*t.getAttribute("width")+2*t.getAttribute("height");case"line":return m({x:t.getAttribute("x1"),y:t.getAttribute("y1")},{x:t.getAttribute("x2"),y:t.getAttribute("y2")});case"polyline":return i(t);case"polygon":var e=t.points;return i(t)+m(e.getItem(e.numberOfItems-1),e.getItem(0))}}function C(e,i){function t(t){return t=void 0===t?0:t,e.el.getPointAtLength(1<=i+t?i+t:0)}var n=t(),s=t(-1),o=t(1);switch(e.property){case"x":return n.x;case"y":return n.y;case"angle":return 180*Math.atan2(o.y-s.y,o.x-s.x)/Math.PI}}function _(t,e){var i,n=/-?\d*\.?\d+/g;if(i=B.pth(t)?t.totalLength:t,B.col(i))if(B.rgb(i)){var s=/rgb\((\d+,\s*[\d]+,\s*[\d]+)\)/g.exec(i);i=s?"rgba("+s[1]+",1)":i}else i=B.hex(i)?function(t){t=t.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i,function(t,e,i,n){return e+e+i+i+n+n});var e=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(t);t=parseInt(e[1],16);var i=parseInt(e[2],16),e=parseInt(e[3],16);return"rgba("+t+","+i+","+e+",1)"}(i):B.hsl(i)?function(t){function e(t,e,i){return i<0&&(i+=1),1<i&&--i,i<1/6?t+6*(e-t)*i:i<.5?e:i<2/3?t+(e-t)*(2/3-i)*6:t}var i=/hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.exec(t)||/hsla\((\d+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\)/g.exec(t);t=parseInt(i[1])/360;var n=parseInt(i[2])/100,s=parseInt(i[3])/100,i=i[4]||1;if(0==n)s=n=t=s;else{var o=s<.5?s*(1+n):s+n-s*n,a=2*s-o,s=e(a,o,t+1/3),n=e(a,o,t);t=e(a,o,t-1/3)}return"rgba("+255*s+","+255*n+","+255*t+","+i+")"}(i):void 0;else s=(s=l(i))?i.substr(0,i.length-s.length):i,i=e&&!/\s/g.test(i)?s+e:s;return{original:i+="",numbers:i.match(n)?i.match(n).map(Number):[0],strings:B.str(t)||e?i.split(n):[]}}function y(t){return b(t=t?d(B.arr(t)?t.map(o):o(t)):[],function(t,e,i){return i.indexOf(t)===e})}function k(t,i){var e=r(i);if(B.arr(t)){var n=t.length;2!==n||B.obj(t[0])?B.fnc(i.duration)||(e.duration=i.duration/n):t={value:t}}return o(t).map(function(t,e){return e=e?0:i.delay,t=B.obj(t)&&!B.pth(t)?t:{value:t},B.und(t.delay)&&(t.delay=e),t}).map(function(t){return c(t,e)})}function E(o,a){var r;return o.tweens.map(function(t){var e=(t=function(t,e){var i,n={};for(i in t){var s=h(t[i],e);B.arr(s)&&1===(s=s.map(function(t){return h(t,e)})).length&&(s=s[0]),n[i]=s}return n.duration=parseFloat(n.duration),n.delay=parseFloat(n.delay),n}(t,a)).value,i=v(a.target,o.name),n=r?r.to.original:i,n=B.arr(e)?e[0]:n,s=f(B.arr(e)?e[1]:e,n),i=l(s)||l(n)||l(i);return t.from=_(n,i),t.to=_(s,i),t.start=r?r.end:o.offset,t.end=t.start+t.delay+t.duration,t.easing=function(t){return B.arr(t)?D.apply(this,t):S[t]}(t.easing),t.elasticity=(1e3-Math.min(Math.max(t.elasticity,1),999))/1e3,t.isPath=B.pth(e),t.isColor=B.col(t.from.original),t.isColor&&(t.round=1),r=t})}function M(e,t,i,n){var s="delay"===e;return t.length?(s?Math.min:Math.max).apply(Math,t.map(function(t){return t[e]})):s?n.delay:i.offset+n.delay+n.duration}function n(t){var e,i,n,s,o=u(L,t),a=u(T,t),r=(i=t.targets,(n=y(i)).map(function(t,e){return{target:t,id:e,total:n.length}})),l=[],h=c(o,a);for(e in t)h.hasOwnProperty(e)||"targets"===e||l.push({name:e,offset:h.offset,tweens:k(t[e],a)});return s=l,t=b(d(r.map(function(n){return s.map(function(t){var e=p(n.target,t.name);if(e){var i=E(t,n);t={type:e,property:t.name,animatable:n,tweens:i,duration:i[i.length-1].end,delay:i[0].delay}}else t=void 0;return t})})),function(t){return!B.und(t)}),c(o,{children:[],animatables:r,animations:t,duration:M("duration",t,o,a),delay:M("delay",t,o,a)})}function O(t){function d(){return window.Promise&&new Promise(function(t){return _=t})}function u(t){return k.reversed?k.duration-t:t}function c(e){for(var t=0,i={},n=k.animations,s=n.length;t<s;){var o=n[t],a=o.animatable,r=o.tweens,l=r.length-1,h=r[l];l&&(h=b(r,function(t){return e<t.end})[0]||h);for(var r=Math.min(Math.max(e-h.start-h.delay,0),h.duration)/h.duration,d=isNaN(r)?1:h.easing(r,h.elasticity),r=h.to.strings,u=h.round,l=[],c=void 0,c=h.to.numbers.length,p=0;p<c;p++){var v=void 0,v=h.to.numbers[p],f=h.from.numbers[p],v=h.isPath?C(h.value,d*v):f+d*(v-f);u&&(h.isColor&&2<p||(v=Math.round(v*u)/u)),l.push(v)}if(h=r.length)for(c=r[0],d=0;d<h;d++)u=r[d+1],p=l[d],isNaN(p)||(c=u?c+(p+u):c+(p+" "));else c=l[0];I[o.type](a.target,o.property,c,i,a.id),o.currentValue=c,t++}if(t=Object.keys(i).length)for(n=0;n<t;n++)x||(x=w(document.body,"transform")?"transform":"-webkit-transform"),k.animatables[n].target.style[x]=i[n].join(" ");k.currentTime=e,k.progress=e/k.duration*100}function p(t){k[t]&&k[t](k)}function v(){k.remaining&&!0!==k.remaining&&k.remaining--}function e(t){var e=k.duration,i=k.offset,n=i+k.delay,s=k.currentTime,o=k.reversed,a=u(t);if(k.children.length){var r=k.children,l=r.length;if(a>=k.currentTime)for(var h=0;h<l;h++)r[h].seek(a);else for(;l--;)r[l].seek(a)}(n<=a||!e)&&(k.began||(k.began=!0,p("begin")),p("run")),i<a&&a<e?c(a):(a<=i&&0!==s&&(c(0),o&&v()),(e<=a&&s!==e||!e)&&(c(e),o||v())),p("update"),e<=t&&(k.remaining?(m=f,"alternate"===k.direction&&(k.reversed=!k.reversed)):(k.pause(),k.completed||(k.completed=!0,p("complete"),"Promise"in window&&(_(),y=d()))),g=0)}t=void 0===t?{}:t;var f,m,g=0,_=null,y=d(),k=n(t);return k.reset=function(){var t=k.direction,e=k.loop;for(k.currentTime=0,k.progress=0,k.paused=!0,k.began=!1,k.completed=!1,k.reversed="reverse"===t,k.remaining="alternate"===t&&1===e?2:e,c(0),t=k.children.length;t--;)k.children[t].reset()},k.tick=function(t){f=t,m||(m=f),e((g+f-m)*O.speed)},k.seek=function(t){e(u(t))},k.pause=function(){var t=A.indexOf(k);-1<t&&A.splice(t,1),k.paused=!0},k.play=function(){k.paused&&(k.paused=!1,m=0,g=u(k.currentTime),A.push(k),R||H())},k.reverse=function(){k.reversed=!k.reversed,m=0,g=u(k.currentTime)},k.restart=function(){k.pause(),k.reset(),k.play()},k.finished=y,k.reset(),k.autoplay&&k.play(),k}var x,L={update:void 0,begin:void 0,run:void 0,complete:void 0,loop:1,direction:"normal",autoplay:!0,offset:0},T={duration:1e3,delay:0,easing:"easeOutElastic",elasticity:500,round:0},$="translateX translateY translateZ rotate rotateX rotateY rotateZ scale scaleX scaleY scaleZ skewX skewY perspective".split(" "),B={arr:function(t){return Array.isArray(t)},obj:function(t){return-1<Object.prototype.toString.call(t).indexOf("Object")},pth:function(t){return B.obj(t)&&t.hasOwnProperty("totalLength")},svg:function(t){return t instanceof SVGElement},dom:function(t){return t.nodeType||B.svg(t)},str:function(t){return"string"==typeof t},fnc:function(t){return"function"==typeof t},und:function(t){return void 0===t},hex:function(t){return/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(t)},rgb:function(t){return/^rgb/.test(t)},hsl:function(t){return/^hsl/.test(t)},col:function(t){return B.hex(t)||B.rgb(t)||B.hsl(t)}},D=function(){function u(t,e,i){return(((1-3*i+3*e)*t+(3*i-6*e))*t+3*e)*t}return function(a,r,l,h){if(0<=a&&a<=1&&0<=l&&l<=1){var d=new Float32Array(11);if(a!==r||l!==h)for(var t=0;t<11;++t)d[t]=u(.1*t,a,l);return function(t){if(a===r&&l===h)return t;if(0===t)return 0;if(1===t)return 1;for(var e=0,i=1;10!==i&&d[i]<=t;++i)e+=.1;var i=e+(t-d[--i])/(d[i+1]-d[i])*.1,n=3*(1-3*l+3*a)*i*i+2*(3*l-6*a)*i+3*a;if(.001<=n){for(e=0;e<4&&0!=(n=3*(1-3*l+3*a)*i*i+2*(3*l-6*a)*i+3*a);++e)var s=u(i,a,l)-t,i=i-s/n;t=i}else if(0===n)t=i;else{for(var i=e,e=e+.1,o=0;0<(n=u(s=i+(e-i)/2,a,l)-t)?e=s:i=s,1e-7<Math.abs(n)&&++o<10;);t=s}return u(t,r,h)}}}}(),S=function(){function i(t,e){return 0===t||1===t?t:-Math.pow(2,10*(t-1))*Math.sin(2*(t-1-e/(2*Math.PI)*Math.asin(1))*Math.PI/e)}var t,n="Quad Cubic Quart Quint Sine Expo Circ Back Elastic".split(" "),e={In:[[.55,.085,.68,.53],[.55,.055,.675,.19],[.895,.03,.685,.22],[.755,.05,.855,.06],[.47,0,.745,.715],[.95,.05,.795,.035],[.6,.04,.98,.335],[.6,-.28,.735,.045],i],Out:[[.25,.46,.45,.94],[.215,.61,.355,1],[.165,.84,.44,1],[.23,1,.32,1],[.39,.575,.565,1],[.19,1,.22,1],[.075,.82,.165,1],[.175,.885,.32,1.275],function(t,e){return 1-i(1-t,e)}],InOut:[[.455,.03,.515,.955],[.645,.045,.355,1],[.77,0,.175,1],[.86,0,.07,1],[.445,.05,.55,.95],[1,0,0,1],[.785,.135,.15,.86],[.68,-.55,.265,1.55],function(t,e){return t<.5?i(2*t,e)/2:1-i(-2*t+2,e)/2}]},s={linear:D(.25,.25,.75,.75)},o={};for(t in e)o.type=t,e[o.type].forEach(function(i){return function(t,e){s["ease"+i.type+n[e]]=B.fnc(t)?t:D.apply($jscomp$this,t)}}(o)),o={type:o.type};return s}(),I={css:function(t,e,i){return t.style[e]=i},attribute:function(t,e,i){return t.setAttribute(e,i)},object:function(t,e,i){return t[e]=i},transform:function(t,e,i,n,s){n[s]||(n[s]=[]),n[s].push(e+"("+i+")")}},A=[],R=0,H=function(){function n(){R=requestAnimationFrame(t)}function t(t){var e=A.length;if(e){for(var i=0;i<e;)A[i]&&A[i].tick(t),i++;n()}else cancelAnimationFrame(R),R=0}return n}();return O.version="2.2.0",O.speed=1,O.running=A,O.remove=function(t){t=y(t);for(var e=A.length;e--;)for(var i=A[e],n=i.animations,s=n.length;s--;)a(t,n[s].animatable.target)&&(n.splice(s,1),n.length||i.pause())},O.getValue=v,O.path=function(t,e){var i=B.str(t)?s(t)[0]:t,n=e||100;return function(t){return{el:i,property:t,totalLength:g(i)*(n/100)}}},O.setDashoffset=function(t){var e=g(t);return t.setAttribute("stroke-dasharray",e),e},O.bezier=D,O.easings=S,O.timeline=function(n){var s=O(n);return s.pause(),s.duration=0,s.add=function(t){return s.children.forEach(function(t){t.began=!0,t.completed=!0}),o(t).forEach(function(t){var e=c(t,u(T,n||{}));e.targets=e.targets||n.targets,t=s.duration;var i=e.offset;e.autoplay=!1,e.direction=s.direction,e.offset=B.und(i)?t:f(i,t),s.began=!0,s.completed=!0,s.seek(e.offset),(e=O(e)).began=!0,e.completed=!0,e.duration>t&&(s.duration=e.duration),s.children.push(e)}),s.seek(0),s.reset(),s.autoplay&&s.restart(),s},s},O.random=function(t,e){return Math.floor(Math.random()*(e-t+1))+t},O}(),function(r,l){"use strict";var e={accordion:!0,onOpenStart:void 0,onOpenEnd:void 0,onCloseStart:void 0,onCloseEnd:void 0,inDuration:300,outDuration:300},t=function(t){function s(t,e){_classCallCheck(this,s);var i=_possibleConstructorReturn(this,(s.__proto__||Object.getPrototypeOf(s)).call(this,s,t,e));(i.el.M_Collapsible=i).options=r.extend({},s.defaults,e),i.$headers=i.$el.children("li").children(".collapsible-header"),i.$headers.attr("tabindex",0),i._setupEventHandlers();var n=i.$el.children("li.active").children(".collapsible-body");return i.options.accordion?n.first().css("display","block"):n.css("display","block"),i}return _inherits(s,Component),_createClass(s,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Collapsible=void 0}},{key:"_setupEventHandlers",value:function(){var e=this;this._handleCollapsibleClickBound=this._handleCollapsibleClick.bind(this),this._handleCollapsibleKeydownBound=this._handleCollapsibleKeydown.bind(this),this.el.addEventListener("click",this._handleCollapsibleClickBound),this.$headers.each(function(t){t.addEventListener("keydown",e._handleCollapsibleKeydownBound)})}},{key:"_removeEventHandlers",value:function(){var e=this;this.el.removeEventListener("click",this._handleCollapsibleClickBound),this.$headers.each(function(t){t.removeEventListener("keydown",e._handleCollapsibleKeydownBound)})}},{key:"_handleCollapsibleClick",value:function(t){var e=r(t.target).closest(".collapsible-header");if(t.target&&e.length){var i=e.closest(".collapsible");if(i[0]===this.el){var n=e.closest("li"),s=i.children("li"),o=n[0].classList.contains("active"),a=s.index(n);o?this.close(a):this.open(a)}}}},{key:"_handleCollapsibleKeydown",value:function(t){13===t.keyCode&&this._handleCollapsibleClickBound(t)}},{key:"_animateIn",value:function(t){var e=this,i=this.$el.children("li").eq(t);if(i.length){var n=i.children(".collapsible-body");l.remove(n[0]),n.css({display:"block",overflow:"hidden",height:0,paddingTop:"",paddingBottom:""});var s=n.css("padding-top"),o=n.css("padding-bottom"),a=n[0].scrollHeight;n.css({paddingTop:0,paddingBottom:0}),l({targets:n[0],height:a,paddingTop:s,paddingBottom:o,duration:this.options.inDuration,easing:"easeInOutCubic",complete:function(t){n.css({overflow:"",paddingTop:"",paddingBottom:"",height:""}),"function"==typeof e.options.onOpenEnd&&e.options.onOpenEnd.call(e,i[0])}})}}},{key:"_animateOut",value:function(t){var e=this,i=this.$el.children("li").eq(t);if(i.length){var n=i.children(".collapsible-body");l.remove(n[0]),n.css("overflow","hidden"),l({targets:n[0],height:0,paddingTop:0,paddingBottom:0,duration:this.options.outDuration,easing:"easeInOutCubic",complete:function(){n.css({height:"",overflow:"",padding:"",display:""}),"function"==typeof e.options.onCloseEnd&&e.options.onCloseEnd.call(e,i[0])}})}}},{key:"open",value:function(t){var i=this,e=this.$el.children("li").eq(t);if(e.length&&!e[0].classList.contains("active")){if("function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,e[0]),this.options.accordion){var n=this.$el.children("li");this.$el.children("li.active").each(function(t){var e=n.index(r(t));i.close(e)})}e[0].classList.add("active"),this._animateIn(t)}}},{key:"close",value:function(t){var e=this.$el.children("li").eq(t);e.length&&e[0].classList.contains("active")&&("function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,e[0]),e[0].classList.remove("active"),this._animateOut(t))}}],[{key:"init",value:function(t,e){return _get(s.__proto__||Object.getPrototypeOf(s),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Collapsible}},{key:"defaults",get:function(){return e}}]),s}();M.Collapsible=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"collapsible","M_Collapsible")}(cash,M.anime),function(h,i){"use strict";var e={alignment:"left",autoFocus:!0,constrainWidth:!0,container:null,coverTrigger:!0,closeOnClick:!0,hover:!1,inDuration:150,outDuration:250,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,onItemClick:null},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return i.el.M_Dropdown=i,n._dropdowns.push(i),i.id=M.getIdFromTrigger(t),i.dropdownEl=document.getElementById(i.id),i.$dropdownEl=h(i.dropdownEl),i.options=h.extend({},n.defaults,e),i.isOpen=!1,i.isScrollable=!1,i.isTouchMoving=!1,i.focusedIndex=-1,i.filterQuery=[],i.options.container?h(i.options.container).append(i.dropdownEl):i.$el.after(i.dropdownEl),i._makeDropdownFocusable(),i._resetFilterQueryBound=i._resetFilterQuery.bind(i),i._handleDocumentClickBound=i._handleDocumentClick.bind(i),i._handleDocumentTouchmoveBound=i._handleDocumentTouchmove.bind(i),i._handleDropdownClickBound=i._handleDropdownClick.bind(i),i._handleDropdownKeydownBound=i._handleDropdownKeydown.bind(i),i._handleTriggerKeydownBound=i._handleTriggerKeydown.bind(i),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._resetDropdownStyles(),this._removeEventHandlers(),n._dropdowns.splice(n._dropdowns.indexOf(this),1),this.el.M_Dropdown=void 0}},{key:"_setupEventHandlers",value:function(){this.el.addEventListener("keydown",this._handleTriggerKeydownBound),this.dropdownEl.addEventListener("click",this._handleDropdownClickBound),this.options.hover?(this._handleMouseEnterBound=this._handleMouseEnter.bind(this),this.el.addEventListener("mouseenter",this._handleMouseEnterBound),this._handleMouseLeaveBound=this._handleMouseLeave.bind(this),this.el.addEventListener("mouseleave",this._handleMouseLeaveBound),this.dropdownEl.addEventListener("mouseleave",this._handleMouseLeaveBound)):(this._handleClickBound=this._handleClick.bind(this),this.el.addEventListener("click",this._handleClickBound))}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("keydown",this._handleTriggerKeydownBound),this.dropdownEl.removeEventListener("click",this._handleDropdownClickBound),this.options.hover?(this.el.removeEventListener("mouseenter",this._handleMouseEnterBound),this.el.removeEventListener("mouseleave",this._handleMouseLeaveBound),this.dropdownEl.removeEventListener("mouseleave",this._handleMouseLeaveBound)):this.el.removeEventListener("click",this._handleClickBound)}},{key:"_setupTemporaryEventHandlers",value:function(){document.body.addEventListener("click",this._handleDocumentClickBound,!0),document.body.addEventListener("touchend",this._handleDocumentClickBound),document.body.addEventListener("touchmove",this._handleDocumentTouchmoveBound),this.dropdownEl.addEventListener("keydown",this._handleDropdownKeydownBound)}},{key:"_removeTemporaryEventHandlers",value:function(){document.body.removeEventListener("click",this._handleDocumentClickBound,!0),document.body.removeEventListener("touchend",this._handleDocumentClickBound),document.body.removeEventListener("touchmove",this._handleDocumentTouchmoveBound),this.dropdownEl.removeEventListener("keydown",this._handleDropdownKeydownBound)}},{key:"_handleClick",value:function(t){t.preventDefault(),this.open()}},{key:"_handleMouseEnter",value:function(){this.open()}},{key:"_handleMouseLeave",value:function(t){var e=t.toElement||t.relatedTarget,i=!!h(e).closest(".dropdown-content").length,n=!1,s=h(e).closest(".dropdown-trigger");s.length&&s[0].M_Dropdown&&s[0].M_Dropdown.isOpen&&(n=!0),n||i||this.close()}},{key:"_handleDocumentClick",value:function(t){var e=this,i=h(t.target);this.options.closeOnClick&&i.closest(".dropdown-content").length&&!this.isTouchMoving?setTimeout(function(){e.close()},0):!i.closest(".dropdown-trigger").length&&i.closest(".dropdown-content").length||setTimeout(function(){e.close()},0),this.isTouchMoving=!1}},{key:"_handleTriggerKeydown",value:function(t){t.which!==M.keys.ARROW_DOWN&&t.which!==M.keys.ENTER||this.isOpen||(t.preventDefault(),this.open())}},{key:"_handleDocumentTouchmove",value:function(t){h(t.target).closest(".dropdown-content").length&&(this.isTouchMoving=!0)}},{key:"_handleDropdownClick",value:function(t){if("function"==typeof this.options.onItemClick){var e=h(t.target).closest("li")[0];this.options.onItemClick.call(this,e)}}},{key:"_handleDropdownKeydown",value:function(t){if(t.which===M.keys.TAB)t.preventDefault(),this.close();else if(t.which!==M.keys.ARROW_DOWN&&t.which!==M.keys.ARROW_UP||!this.isOpen)if(t.which===M.keys.ENTER&&this.isOpen){var e=this.dropdownEl.children[this.focusedIndex],i=h(e).find("a, button").first();i.length?i[0].click():e&&e.click()}else t.which===M.keys.ESC&&this.isOpen&&(t.preventDefault(),this.close());else{t.preventDefault();var n=t.which===M.keys.ARROW_DOWN?1:-1,s=this.focusedIndex,o=!1;do{if(s+=n,this.dropdownEl.children[s]&&-1!==this.dropdownEl.children[s].tabIndex){o=!0;break}}while(s<this.dropdownEl.children.length&&0<=s);o&&(this.focusedIndex=s,this._focusFocusedItem())}var a=String.fromCharCode(t.which).toLowerCase();if(a&&-1===[9,13,27,38,40].indexOf(t.which)){this.filterQuery.push(a);var r=this.filterQuery.join(""),l=h(this.dropdownEl).find("li").filter(function(t){return 0===h(t).text().toLowerCase().indexOf(r)})[0];l&&(this.focusedIndex=h(l).index(),this._focusFocusedItem())}this.filterTimeout=setTimeout(this._resetFilterQueryBound,1e3)}},{key:"_resetFilterQuery",value:function(){this.filterQuery=[]}},{key:"_resetDropdownStyles",value:function(){this.$dropdownEl.css({display:"",width:"",height:"",left:"",top:"","transform-origin":"",transform:"",opacity:""})}},{key:"_makeDropdownFocusable",value:function(){this.dropdownEl.tabIndex=0,h(this.dropdownEl).children().each(function(t){t.getAttribute("tabindex")||t.setAttribute("tabindex",0)})}},{key:"_focusFocusedItem",value:function(){0<=this.focusedIndex&&this.focusedIndex<this.dropdownEl.children.length&&this.options.autoFocus&&this.dropdownEl.children[this.focusedIndex].focus()}},{key:"_getDropdownPosition",value:function(){this.el.offsetParent.getBoundingClientRect();var t=this.el.getBoundingClientRect(),e=this.dropdownEl.getBoundingClientRect(),i=e.height,n=e.width,s=t.left-e.left,o=t.top-e.top,a={left:s,top:o,height:i,width:n},r=this.dropdownEl.offsetParent?this.dropdownEl.offsetParent:this.dropdownEl.parentNode,l=M.checkPossibleAlignments(this.el,r,a,this.options.coverTrigger?0:t.height),h="top",d=this.options.alignment;if(o+=this.options.coverTrigger?0:t.height,this.isScrollable=!1,l.top||(l.bottom?h="bottom":(this.isScrollable=!0,l.spaceOnTop>l.spaceOnBottom?(h="bottom",i+=l.spaceOnTop,o-=l.spaceOnTop):i+=l.spaceOnBottom)),!l[d]){var u="left"===d?"right":"left";l[u]?d=u:l.spaceOnLeft>l.spaceOnRight?(d="right",n+=l.spaceOnLeft,s-=l.spaceOnLeft):(d="left",n+=l.spaceOnRight)}return"bottom"===h&&(o=o-e.height+(this.options.coverTrigger?t.height:0)),"right"===d&&(s=s-e.width+t.width),{x:s,y:o,verticalAlignment:h,horizontalAlignment:d,height:i,width:n}}},{key:"_animateIn",value:function(){var e=this;i.remove(this.dropdownEl),i({targets:this.dropdownEl,opacity:{value:[0,1],easing:"easeOutQuad"},scaleX:[.3,1],scaleY:[.3,1],duration:this.options.inDuration,easing:"easeOutQuint",complete:function(t){e.options.autoFocus&&e.dropdownEl.focus(),"function"==typeof e.options.onOpenEnd&&e.options.onOpenEnd.call(e,e.el)}})}},{key:"_animateOut",value:function(){var e=this;i.remove(this.dropdownEl),i({targets:this.dropdownEl,opacity:{value:0,easing:"easeOutQuint"},scaleX:.3,scaleY:.3,duration:this.options.outDuration,easing:"easeOutQuint",complete:function(t){e._resetDropdownStyles(),"function"==typeof e.options.onCloseEnd&&e.options.onCloseEnd.call(e,e.el)}})}},{key:"_placeDropdown",value:function(){var t=this.options.constrainWidth?this.el.getBoundingClientRect().width:this.dropdownEl.getBoundingClientRect().width;this.dropdownEl.style.width=t+"px";var e=this._getDropdownPosition();this.dropdownEl.style.left=e.x+"px",this.dropdownEl.style.top=e.y+"px",this.dropdownEl.style.height=e.height+"px",this.dropdownEl.style.width=e.width+"px",this.dropdownEl.style.transformOrigin=("left"===e.horizontalAlignment?"0":"100%")+" "+("top"===e.verticalAlignment?"0":"100%")}},{key:"open",value:function(){this.isOpen||(this.isOpen=!0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el),this._resetDropdownStyles(),this.dropdownEl.style.display="block",this._placeDropdown(),this._animateIn(),this._setupTemporaryEventHandlers())}},{key:"close",value:function(){this.isOpen&&(this.isOpen=!1,this.focusedIndex=-1,"function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,this.el),this._animateOut(),this._removeTemporaryEventHandlers(),this.options.autoFocus&&this.el.focus())}},{key:"recalculateDimensions",value:function(){this.isOpen&&(this.$dropdownEl.css({width:"",height:"",left:"",top:"","transform-origin":""}),this._placeDropdown())}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Dropdown}},{key:"defaults",get:function(){return e}}]),n}();t._dropdowns=[],M.Dropdown=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"dropdown","M_Dropdown")}(cash,M.anime),function(s,i){"use strict";var e={opacity:.5,inDuration:250,outDuration:250,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,preventScrolling:!0,dismissible:!0,startingTop:"4%",endingTop:"10%"},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Modal=i).options=s.extend({},n.defaults,e),i.isOpen=!1,i.id=i.$el.attr("id"),i._openingTrigger=void 0,i.$overlay=s('<div class="modal-overlay"></div>'),i.el.tabIndex=0,i._nthModalOpened=0,n._count++,i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){n._count--,this._removeEventHandlers(),this.el.removeAttribute("style"),this.$overlay.remove(),this.el.M_Modal=void 0}},{key:"_setupEventHandlers",value:function(){this._handleOverlayClickBound=this._handleOverlayClick.bind(this),this._handleModalCloseClickBound=this._handleModalCloseClick.bind(this),1===n._count&&document.body.addEventListener("click",this._handleTriggerClick),this.$overlay[0].addEventListener("click",this._handleOverlayClickBound),this.el.addEventListener("click",this._handleModalCloseClickBound)}},{key:"_removeEventHandlers",value:function(){0===n._count&&document.body.removeEventListener("click",this._handleTriggerClick),this.$overlay[0].removeEventListener("click",this._handleOverlayClickBound),this.el.removeEventListener("click",this._handleModalCloseClickBound)}},{key:"_handleTriggerClick",value:function(t){var e=s(t.target).closest(".modal-trigger");if(e.length){var i=M.getIdFromTrigger(e[0]),n=document.getElementById(i).M_Modal;n&&n.open(e),t.preventDefault()}}},{key:"_handleOverlayClick",value:function(){this.options.dismissible&&this.close()}},{key:"_handleModalCloseClick",value:function(t){s(t.target).closest(".modal-close").length&&this.close()}},{key:"_handleKeydown",value:function(t){27===t.keyCode&&this.options.dismissible&&this.close()}},{key:"_handleFocus",value:function(t){this.el.contains(t.target)||this._nthModalOpened!==n._modalsOpen||this.el.focus()}},{key:"_animateIn",value:function(){var t=this;s.extend(this.el.style,{display:"block",opacity:0}),s.extend(this.$overlay[0].style,{display:"block",opacity:0}),i({targets:this.$overlay[0],opacity:this.options.opacity,duration:this.options.inDuration,easing:"easeOutQuad"});var e={targets:this.el,duration:this.options.inDuration,easing:"easeOutCubic",complete:function(){"function"==typeof t.options.onOpenEnd&&t.options.onOpenEnd.call(t,t.el,t._openingTrigger)}};this.el.classList.contains("bottom-sheet")?s.extend(e,{bottom:0,opacity:1}):s.extend(e,{top:[this.options.startingTop,this.options.endingTop],opacity:1,scaleX:[.8,1],scaleY:[.8,1]}),i(e)}},{key:"_animateOut",value:function(){var t=this;i({targets:this.$overlay[0],opacity:0,duration:this.options.outDuration,easing:"easeOutQuart"});var e={targets:this.el,duration:this.options.outDuration,easing:"easeOutCubic",complete:function(){t.el.style.display="none",t.$overlay.remove(),"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t,t.el)}};this.el.classList.contains("bottom-sheet")?s.extend(e,{bottom:"-100%",opacity:0}):s.extend(e,{top:[this.options.endingTop,this.options.startingTop],opacity:0,scaleX:.8,scaleY:.8}),i(e)}},{key:"open",value:function(t){if(!this.isOpen)return this.isOpen=!0,n._modalsOpen++,this._nthModalOpened=n._modalsOpen,this.$overlay[0].style.zIndex=1e3+2*n._modalsOpen,this.el.style.zIndex=1e3+2*n._modalsOpen+1,this._openingTrigger=t?t[0]:void 0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el,this._openingTrigger),this.options.preventScrolling&&(document.body.style.overflow="hidden"),this.el.classList.add("open"),this.el.insertAdjacentElement("afterend",this.$overlay[0]),this.options.dismissible&&(this._handleKeydownBound=this._handleKeydown.bind(this),this._handleFocusBound=this._handleFocus.bind(this),document.addEventListener("keydown",this._handleKeydownBound),document.addEventListener("focus",this._handleFocusBound,!0)),i.remove(this.el),i.remove(this.$overlay[0]),this._animateIn(),this.el.focus(),this}},{key:"close",value:function(){if(this.isOpen)return this.isOpen=!1,n._modalsOpen--,this._nthModalOpened=0,"function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,this.el),this.el.classList.remove("open"),0===n._modalsOpen&&(document.body.style.overflow=""),this.options.dismissible&&(document.removeEventListener("keydown",this._handleKeydownBound),document.removeEventListener("focus",this._handleFocusBound,!0)),i.remove(this.el),i.remove(this.$overlay[0]),this._animateOut(),this}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Modal}},{key:"defaults",get:function(){return e}}]),n}();t._modalsOpen=0,t._count=0,M.Modal=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"modal","M_Modal")}(cash,M.anime),function(o,a){"use strict";var e={inDuration:275,outDuration:200,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Materialbox=i).options=o.extend({},n.defaults,e),i.overlayActive=!1,i.doneAnimating=!0,i.placeholder=o("<div></div>").addClass("material-placeholder"),i.originalWidth=0,i.originalHeight=0,i.originInlineStyles=i.$el.attr("style"),i.caption=i.el.getAttribute("data-caption")||"",i.$el.before(i.placeholder),i.placeholder.append(i.$el),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Materialbox=void 0,o(this.placeholder).after(this.el).remove(),this.$el.removeAttr("style")}},{key:"_setupEventHandlers",value:function(){this._handleMaterialboxClickBound=this._handleMaterialboxClick.bind(this),this.el.addEventListener("click",this._handleMaterialboxClickBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleMaterialboxClickBound)}},{key:"_handleMaterialboxClick",value:function(t){!1===this.doneAnimating||this.overlayActive&&this.doneAnimating?this.close():this.open()}},{key:"_handleWindowScroll",value:function(){this.overlayActive&&this.close()}},{key:"_handleWindowResize",value:function(){this.overlayActive&&this.close()}},{key:"_handleWindowEscape",value:function(t){27===t.keyCode&&this.doneAnimating&&this.overlayActive&&this.close()}},{key:"_makeAncestorsOverflowVisible",value:function(){this.ancestorsChanged=o();for(var t=this.placeholder[0].parentNode;null!==t&&!o(t).is(document);){var e=o(t);"visible"!==e.css("overflow")&&(e.css("overflow","visible"),void 0===this.ancestorsChanged?this.ancestorsChanged=e:this.ancestorsChanged=this.ancestorsChanged.add(e)),t=t.parentNode}}},{key:"_animateImageIn",value:function(){var t=this,e={targets:this.el,height:[this.originalHeight,this.newHeight],width:[this.originalWidth,this.newWidth],left:M.getDocumentScrollLeft()+this.windowWidth/2-this.placeholder.offset().left-this.newWidth/2,top:M.getDocumentScrollTop()+this.windowHeight/2-this.placeholder.offset().top-this.newHeight/2,duration:this.options.inDuration,easing:"easeOutQuad",complete:function(){t.doneAnimating=!0,"function"==typeof t.options.onOpenEnd&&t.options.onOpenEnd.call(t,t.el)}};this.maxWidth=this.$el.css("max-width"),this.maxHeight=this.$el.css("max-height"),"none"!==this.maxWidth&&(e.maxWidth=this.newWidth),"none"!==this.maxHeight&&(e.maxHeight=this.newHeight),a(e)}},{key:"_animateImageOut",value:function(){var t=this,e={targets:this.el,width:this.originalWidth,height:this.originalHeight,left:0,top:0,duration:this.options.outDuration,easing:"easeOutQuad",complete:function(){t.placeholder.css({height:"",width:"",position:"",top:"",left:""}),t.attrWidth&&t.$el.attr("width",t.attrWidth),t.attrHeight&&t.$el.attr("height",t.attrHeight),t.$el.removeAttr("style"),t.originInlineStyles&&t.$el.attr("style",t.originInlineStyles),t.$el.removeClass("active"),t.doneAnimating=!0,t.ancestorsChanged.length&&t.ancestorsChanged.css("overflow",""),"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t,t.el)}};a(e)}},{key:"_updateVars",value:function(){this.windowWidth=window.innerWidth,this.windowHeight=window.innerHeight,this.caption=this.el.getAttribute("data-caption")||""}},{key:"open",value:function(){var t=this;this._updateVars(),this.originalWidth=this.el.getBoundingClientRect().width,this.originalHeight=this.el.getBoundingClientRect().height,this.doneAnimating=!1,this.$el.addClass("active"),this.overlayActive=!0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el),this.placeholder.css({width:this.placeholder[0].getBoundingClientRect().width+"px",height:this.placeholder[0].getBoundingClientRect().height+"px",position:"relative",top:0,left:0}),this._makeAncestorsOverflowVisible(),this.$el.css({position:"absolute","z-index":1e3,"will-change":"left, top, width, height"}),this.attrWidth=this.$el.attr("width"),this.attrHeight=this.$el.attr("height"),this.attrWidth&&(this.$el.css("width",this.attrWidth+"px"),this.$el.removeAttr("width")),this.attrHeight&&(this.$el.css("width",this.attrHeight+"px"),this.$el.removeAttr("height")),this.$overlay=o('<div id="materialbox-overlay"></div>').css({opacity:0}).one("click",function(){t.doneAnimating&&t.close()}),this.$el.before(this.$overlay);var e=this.$overlay[0].getBoundingClientRect();this.$overlay.css({width:this.windowWidth+"px",height:this.windowHeight+"px",left:-1*e.left+"px",top:-1*e.top+"px"}),a.remove(this.el),a.remove(this.$overlay[0]),a({targets:this.$overlay[0],opacity:1,duration:this.options.inDuration,easing:"easeOutQuad"}),""!==this.caption&&(this.$photocaption&&a.remove(this.$photoCaption[0]),this.$photoCaption=o('<div class="materialbox-caption"></div>'),this.$photoCaption.text(this.caption),o("body").append(this.$photoCaption),this.$photoCaption.css({display:"inline"}),a({targets:this.$photoCaption[0],opacity:1,duration:this.options.inDuration,easing:"easeOutQuad"}));var i=0,n=this.originalWidth/this.windowWidth,s=this.originalHeight/this.windowHeight;this.newWidth=0,this.newHeight=0,s<n?(i=this.originalHeight/this.originalWidth,this.newWidth=.9*this.windowWidth,this.newHeight=.9*this.windowWidth*i):(i=this.originalWidth/this.originalHeight,this.newWidth=.9*this.windowHeight*i,this.newHeight=.9*this.windowHeight),this._animateImageIn(),this._handleWindowScrollBound=this._handleWindowScroll.bind(this),this._handleWindowResizeBound=this._handleWindowResize.bind(this),this._handleWindowEscapeBound=this._handleWindowEscape.bind(this),window.addEventListener("scroll",this._handleWindowScrollBound),window.addEventListener("resize",this._handleWindowResizeBound),window.addEventListener("keyup",this._handleWindowEscapeBound)}},{key:"close",value:function(){var t=this;this._updateVars(),this.doneAnimating=!1,"function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,this.el),a.remove(this.el),a.remove(this.$overlay[0]),""!==this.caption&&a.remove(this.$photoCaption[0]),window.removeEventListener("scroll",this._handleWindowScrollBound),window.removeEventListener("resize",this._handleWindowResizeBound),window.removeEventListener("keyup",this._handleWindowEscapeBound),a({targets:this.$overlay[0],opacity:0,duration:this.options.outDuration,easing:"easeOutQuad",complete:function(){t.overlayActive=!1,t.$overlay.remove()}}),this._animateImageOut(),""!==this.caption&&a({targets:this.$photoCaption[0],opacity:0,duration:this.options.outDuration,easing:"easeOutQuad",complete:function(){t.$photoCaption.remove()}})}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Materialbox}},{key:"defaults",get:function(){return e}}]),n}();M.Materialbox=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"materialbox","M_Materialbox")}(cash,M.anime),function(s){"use strict";var e={responsiveThreshold:0},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Parallax=i).options=s.extend({},n.defaults,e),i._enabled=window.innerWidth>i.options.responsiveThreshold,i.$img=i.$el.find("img").first(),i.$img.each(function(){this.complete&&s(this).trigger("load")}),i._updateParallax(),i._setupEventHandlers(),i._setupStyles(),n._parallaxes.push(i),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){n._parallaxes.splice(n._parallaxes.indexOf(this),1),this.$img[0].style.transform="",this._removeEventHandlers(),this.$el[0].M_Parallax=void 0}},{key:"_setupEventHandlers",value:function(){this._handleImageLoadBound=this._handleImageLoad.bind(this),this.$img[0].addEventListener("load",this._handleImageLoadBound),0===n._parallaxes.length&&(n._handleScrollThrottled=M.throttle(n._handleScroll,5),window.addEventListener("scroll",n._handleScrollThrottled),n._handleWindowResizeThrottled=M.throttle(n._handleWindowResize,5),window.addEventListener("resize",n._handleWindowResizeThrottled))}},{key:"_removeEventHandlers",value:function(){this.$img[0].removeEventListener("load",this._handleImageLoadBound),0===n._parallaxes.length&&(window.removeEventListener("scroll",n._handleScrollThrottled),window.removeEventListener("resize",n._handleWindowResizeThrottled))}},{key:"_setupStyles",value:function(){this.$img[0].style.opacity=1}},{key:"_handleImageLoad",value:function(){this._updateParallax()}},{key:"_updateParallax",value:function(){var t=0<this.$el.height()?this.el.parentNode.offsetHeight:500,e=this.$img[0].offsetHeight-t,i=this.$el.offset().top+t,n=this.$el.offset().top,s=M.getDocumentScrollTop(),o=window.innerHeight,a=e*((s+o-n)/(t+o));this._enabled?s<i&&n<s+o&&(this.$img[0].style.transform="translate3D(-50%, "+a+"px, 0)"):this.$img[0].style.transform=""}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Parallax}},{key:"_handleScroll",value:function(){for(var t=0;t<n._parallaxes.length;t++){var e=n._parallaxes[t];e._updateParallax.call(e)}}},{key:"_handleWindowResize",value:function(){for(var t=0;t<n._parallaxes.length;t++){var e=n._parallaxes[t];e._enabled=window.innerWidth>e.options.responsiveThreshold}}},{key:"defaults",get:function(){return e}}]),n}();t._parallaxes=[],M.Parallax=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"parallax","M_Parallax")}(cash),function(a,s){"use strict";var e={duration:300,onShow:null,swipeable:!1,responsiveThreshold:1/0},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Tabs=i).options=a.extend({},n.defaults,e),i.$tabLinks=i.$el.children("li.tab").children("a"),i.index=0,i._setupActiveTabLink(),i.options.swipeable?i._setupSwipeableTabs():i._setupNormalTabs(),i._setTabsAndTabWidth(),i._createIndicator(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this._indicator.parentNode.removeChild(this._indicator),this.options.swipeable?this._teardownSwipeableTabs():this._teardownNormalTabs(),this.$el[0].M_Tabs=void 0}},{key:"_setupEventHandlers",value:function(){this._handleWindowResizeBound=this._handleWindowResize.bind(this),window.addEventListener("resize",this._handleWindowResizeBound),this._handleTabClickBound=this._handleTabClick.bind(this),this.el.addEventListener("click",this._handleTabClickBound)}},{key:"_removeEventHandlers",value:function(){window.removeEventListener("resize",this._handleWindowResizeBound),this.el.removeEventListener("click",this._handleTabClickBound)}},{key:"_handleWindowResize",value:function(){this._setTabsAndTabWidth(),0!==this.tabWidth&&0!==this.tabsWidth&&(this._indicator.style.left=this._calcLeftPos(this.$activeTabLink)+"px",this._indicator.style.right=this._calcRightPos(this.$activeTabLink)+"px")}},{key:"_handleTabClick",value:function(t){var e=this,i=a(t.target).closest("li.tab"),n=a(t.target).closest("a");if(n.length&&n.parent().hasClass("tab"))if(i.hasClass("disabled"))t.preventDefault();else if(!n.attr("target")){this.$activeTabLink.removeClass("active");var s=this.$content;this.$activeTabLink=n,this.$content=a(M.escapeHash(n[0].hash)),this.$tabLinks=this.$el.children("li.tab").children("a"),this.$activeTabLink.addClass("active");var o=this.index;this.index=Math.max(this.$tabLinks.index(n),0),this.options.swipeable?this._tabsCarousel&&this._tabsCarousel.set(this.index,function(){"function"==typeof e.options.onShow&&e.options.onShow.call(e,e.$content[0])}):this.$content.length&&(this.$content[0].style.display="block",this.$content.addClass("active"),"function"==typeof this.options.onShow&&this.options.onShow.call(this,this.$content[0]),s.length&&!s.is(this.$content)&&(s[0].style.display="none",s.removeClass("active"))),this._setTabsAndTabWidth(),this._animateIndicator(o),t.preventDefault()}}},{key:"_createIndicator",value:function(){var t=this,e=document.createElement("li");e.classList.add("indicator"),this.el.appendChild(e),this._indicator=e,setTimeout(function(){t._indicator.style.left=t._calcLeftPos(t.$activeTabLink)+"px",t._indicator.style.right=t._calcRightPos(t.$activeTabLink)+"px"},0)}},{key:"_setupActiveTabLink",value:function(){this.$activeTabLink=a(this.$tabLinks.filter('[href="'+location.hash+'"]')),0===this.$activeTabLink.length&&(this.$activeTabLink=this.$el.children("li.tab").children("a.active").first()),0===this.$activeTabLink.length&&(this.$activeTabLink=this.$el.children("li.tab").children("a").first()),this.$tabLinks.removeClass("active"),this.$activeTabLink[0].classList.add("active"),this.index=Math.max(this.$tabLinks.index(this.$activeTabLink),0),this.$activeTabLink.length&&(this.$content=a(M.escapeHash(this.$activeTabLink[0].hash)),this.$content.addClass("active"))}},{key:"_setupSwipeableTabs",value:function(){var i=this;window.innerWidth>this.options.responsiveThreshold&&(this.options.swipeable=!1);var n=a();this.$tabLinks.each(function(t){var e=a(M.escapeHash(t.hash));e.addClass("carousel-item"),n=n.add(e)});var t=a('<div class="tabs-content carousel carousel-slider"></div>');n.first().before(t),t.append(n),n[0].style.display="";var e=this.$activeTabLink.closest(".tab").index();this._tabsCarousel=M.Carousel.init(t[0],{fullWidth:!0,noWrap:!0,onCycleTo:function(t){var e=i.index;i.index=a(t).index(),i.$activeTabLink.removeClass("active"),i.$activeTabLink=i.$tabLinks.eq(i.index),i.$activeTabLink.addClass("active"),i._animateIndicator(e),"function"==typeof i.options.onShow&&i.options.onShow.call(i,i.$content[0])}}),this._tabsCarousel.set(e)}},{key:"_teardownSwipeableTabs",value:function(){var t=this._tabsCarousel.$el;this._tabsCarousel.destroy(),t.after(t.children()),t.remove()}},{key:"_setupNormalTabs",value:function(){this.$tabLinks.not(this.$activeTabLink).each(function(t){if(t.hash){var e=a(M.escapeHash(t.hash));e.length&&(e[0].style.display="none")}})}},{key:"_teardownNormalTabs",value:function(){this.$tabLinks.each(function(t){if(t.hash){var e=a(M.escapeHash(t.hash));e.length&&(e[0].style.display="")}})}},{key:"_setTabsAndTabWidth",value:function(){this.tabsWidth=this.$el.width(),this.tabWidth=Math.max(this.tabsWidth,this.el.scrollWidth)/this.$tabLinks.length}},{key:"_calcRightPos",value:function(t){return Math.ceil(this.tabsWidth-t.position().left-t[0].getBoundingClientRect().width)}},{key:"_calcLeftPos",value:function(t){return Math.floor(t.position().left)}},{key:"updateTabIndicator",value:function(){this._setTabsAndTabWidth(),this._animateIndicator(this.index)}},{key:"_animateIndicator",value:function(t){var e=0,i=0;0<=this.index-t?e=90:i=90;var n={targets:this._indicator,left:{value:this._calcLeftPos(this.$activeTabLink),delay:e},right:{value:this._calcRightPos(this.$activeTabLink),delay:i},duration:this.options.duration,easing:"easeOutQuad"};s.remove(this._indicator),s(n)}},{key:"select",value:function(t){var e=this.$tabLinks.filter('[href="#'+t+'"]');e.length&&e.trigger("click")}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Tabs}},{key:"defaults",get:function(){return e}}]),n}();M.Tabs=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"tabs","M_Tabs")}(cash,M.anime),function(d,e){"use strict";var i={exitDelay:200,enterDelay:0,html:null,margin:5,inDuration:250,outDuration:200,position:"bottom",transitionMovement:10},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Tooltip=i).options=d.extend({},n.defaults,e),i.isOpen=!1,i.isHovered=!1,i.isFocused=!1,i._appendTooltipEl(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){d(this.tooltipEl).remove(),this._removeEventHandlers(),this.el.M_Tooltip=void 0}},{key:"_appendTooltipEl",value:function(){var t=document.createElement("div");t.classList.add("material-tooltip"),this.tooltipEl=t;var e=document.createElement("div");e.classList.add("tooltip-content"),e.innerHTML=this.options.html,t.appendChild(e),document.body.appendChild(t)}},{key:"_updateTooltipContent",value:function(){this.tooltipEl.querySelector(".tooltip-content").innerHTML=this.options.html}},{key:"_setupEventHandlers",value:function(){this._handleMouseEnterBound=this._handleMouseEnter.bind(this),this._handleMouseLeaveBound=this._handleMouseLeave.bind(this),this._handleFocusBound=this._handleFocus.bind(this),this._handleBlurBound=this._handleBlur.bind(this),this.el.addEventListener("mouseenter",this._handleMouseEnterBound),this.el.addEventListener("mouseleave",this._handleMouseLeaveBound),this.el.addEventListener("focus",this._handleFocusBound,!0),this.el.addEventListener("blur",this._handleBlurBound,!0)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("mouseenter",this._handleMouseEnterBound),this.el.removeEventListener("mouseleave",this._handleMouseLeaveBound),this.el.removeEventListener("focus",this._handleFocusBound,!0),this.el.removeEventListener("blur",this._handleBlurBound,!0)}},{key:"open",value:function(t){this.isOpen||(t=void 0===t||void 0,this.isOpen=!0,this.options=d.extend({},this.options,this._getAttributeOptions()),this._updateTooltipContent(),this._setEnterDelayTimeout(t))}},{key:"close",value:function(){this.isOpen&&(this.isHovered=!1,this.isFocused=!1,this.isOpen=!1,this._setExitDelayTimeout())}},{key:"_setExitDelayTimeout",value:function(){var t=this;clearTimeout(this._exitDelayTimeout),this._exitDelayTimeout=setTimeout(function(){t.isHovered||t.isFocused||t._animateOut()},this.options.exitDelay)}},{key:"_setEnterDelayTimeout",value:function(t){var e=this;clearTimeout(this._enterDelayTimeout),this._enterDelayTimeout=setTimeout(function(){(e.isHovered||e.isFocused||t)&&e._animateIn()},this.options.enterDelay)}},{key:"_positionTooltip",value:function(){var t,e=this.el,i=this.tooltipEl,n=e.offsetHeight,s=e.offsetWidth,o=i.offsetHeight,a=i.offsetWidth,r=this.options.margin,l=void 0,h=void 0;this.xMovement=0,this.yMovement=0,l=e.getBoundingClientRect().top+M.getDocumentScrollTop(),h=e.getBoundingClientRect().left+M.getDocumentScrollLeft(),"top"===this.options.position?(l+=-o-r,h+=s/2-a/2,this.yMovement=-this.options.transitionMovement):"right"===this.options.position?(l+=n/2-o/2,h+=s+r,this.xMovement=this.options.transitionMovement):"left"===this.options.position?(l+=n/2-o/2,h+=-a-r,this.xMovement=-this.options.transitionMovement):(l+=n+r,h+=s/2-a/2,this.yMovement=this.options.transitionMovement),t=this._repositionWithinScreen(h,l,a,o),d(i).css({top:t.y+"px",left:t.x+"px"})}},{key:"_repositionWithinScreen",value:function(t,e,i,n){var s=M.getDocumentScrollLeft(),o=M.getDocumentScrollTop(),a=t-s,r=e-o,l={left:a,top:r,width:i,height:n},h=this.options.margin+this.options.transitionMovement,d=M.checkWithinContainer(document.body,l,h);return d.left?a=h:d.right&&(a-=a+i-window.innerWidth),d.top?r=h:d.bottom&&(r-=r+n-window.innerHeight),{x:a+s,y:r+o}}},{key:"_animateIn",value:function(){this._positionTooltip(),this.tooltipEl.style.visibility="visible",e.remove(this.tooltipEl),e({targets:this.tooltipEl,opacity:1,translateX:this.xMovement,translateY:this.yMovement,duration:this.options.inDuration,easing:"easeOutCubic"})}},{key:"_animateOut",value:function(){e.remove(this.tooltipEl),e({targets:this.tooltipEl,opacity:0,translateX:0,translateY:0,duration:this.options.outDuration,easing:"easeOutCubic"})}},{key:"_handleMouseEnter",value:function(){this.isHovered=!0,this.isFocused=!1,this.open(!1)}},{key:"_handleMouseLeave",value:function(){this.isHovered=!1,this.isFocused=!1,this.close()}},{key:"_handleFocus",value:function(){M.tabPressed&&(this.isFocused=!0,this.open(!1))}},{key:"_handleBlur",value:function(){this.isFocused=!1,this.close()}},{key:"_getAttributeOptions",value:function(){var t={},e=this.el.getAttribute("data-tooltip"),i=this.el.getAttribute("data-position");return e&&(t.html=e),i&&(t.position=i),t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Tooltip}},{key:"defaults",get:function(){return i}}]),n}();M.Tooltip=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"tooltip","M_Tooltip")}(cash,M.anime),function(i){"use strict";var t=t||{},e=document.querySelectorAll.bind(document);function m(t){var e="";for(var i in t)t.hasOwnProperty(i)&&(e+=i+":"+t[i]+";");return e}var g={duration:750,show:function(t,e){if(2===t.button)return!1;var i=e||this,n=document.createElement("div");n.className="waves-ripple",i.appendChild(n);var s,o,a,r,l,h,d,u=(h={top:0,left:0},d=(s=i)&&s.ownerDocument,o=d.documentElement,void 0!==s.getBoundingClientRect&&(h=s.getBoundingClientRect()),a=null!==(l=r=d)&&l===l.window?r:9===r.nodeType&&r.defaultView,{top:h.top+a.pageYOffset-o.clientTop,left:h.left+a.pageXOffset-o.clientLeft}),c=t.pageY-u.top,p=t.pageX-u.left,v="scale("+i.clientWidth/100*10+")";"touches"in t&&(c=t.touches[0].pageY-u.top,p=t.touches[0].pageX-u.left),n.setAttribute("data-hold",Date.now()),n.setAttribute("data-scale",v),n.setAttribute("data-x",p),n.setAttribute("data-y",c);var f={top:c+"px",left:p+"px"};n.className=n.className+" waves-notransition",n.setAttribute("style",m(f)),n.className=n.className.replace("waves-notransition",""),f["-webkit-transform"]=v,f["-moz-transform"]=v,f["-ms-transform"]=v,f["-o-transform"]=v,f.transform=v,f.opacity="1",f["-webkit-transition-duration"]=g.duration+"ms",f["-moz-transition-duration"]=g.duration+"ms",f["-o-transition-duration"]=g.duration+"ms",f["transition-duration"]=g.duration+"ms",f["-webkit-transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",f["-moz-transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",f["-o-transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",f["transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",n.setAttribute("style",m(f))},hide:function(t){l.touchup(t);var e=this,i=(e.clientWidth,null),n=e.getElementsByClassName("waves-ripple");if(!(0<n.length))return!1;var s=(i=n[n.length-1]).getAttribute("data-x"),o=i.getAttribute("data-y"),a=i.getAttribute("data-scale"),r=350-(Date.now()-Number(i.getAttribute("data-hold")));r<0&&(r=0),setTimeout(function(){var t={top:o+"px",left:s+"px",opacity:"0","-webkit-transition-duration":g.duration+"ms","-moz-transition-duration":g.duration+"ms","-o-transition-duration":g.duration+"ms","transition-duration":g.duration+"ms","-webkit-transform":a,"-moz-transform":a,"-ms-transform":a,"-o-transform":a,transform:a};i.setAttribute("style",m(t)),setTimeout(function(){try{e.removeChild(i)}catch(t){return!1}},g.duration)},r)},wrapInput:function(t){for(var e=0;e<t.length;e++){var i=t[e];if("input"===i.tagName.toLowerCase()){var n=i.parentNode;if("i"===n.tagName.toLowerCase()&&-1!==n.className.indexOf("waves-effect"))continue;var s=document.createElement("i");s.className=i.className+" waves-input-wrapper";var o=i.getAttribute("style");o||(o=""),s.setAttribute("style",o),i.className="waves-button-input",i.removeAttribute("style"),n.replaceChild(s,i),s.appendChild(i)}}}},l={touches:0,allowEvent:function(t){var e=!0;return"touchstart"===t.type?l.touches+=1:"touchend"===t.type||"touchcancel"===t.type?setTimeout(function(){0<l.touches&&(l.touches-=1)},500):"mousedown"===t.type&&0<l.touches&&(e=!1),e},touchup:function(t){l.allowEvent(t)}};function n(t){var e=function(t){if(!1===l.allowEvent(t))return null;for(var e=null,i=t.target||t.srcElement;null!==i.parentNode;){if(!(i instanceof SVGElement)&&-1!==i.className.indexOf("waves-effect")){e=i;break}i=i.parentNode}return e}(t);null!==e&&(g.show(t,e),"ontouchstart"in i&&(e.addEventListener("touchend",g.hide,!1),e.addEventListener("touchcancel",g.hide,!1)),e.addEventListener("mouseup",g.hide,!1),e.addEventListener("mouseleave",g.hide,!1),e.addEventListener("dragend",g.hide,!1))}t.displayEffect=function(t){"duration"in(t=t||{})&&(g.duration=t.duration),g.wrapInput(e(".waves-effect")),"ontouchstart"in i&&document.body.addEventListener("touchstart",n,!1),document.body.addEventListener("mousedown",n,!1)},t.attach=function(t){"input"===t.tagName.toLowerCase()&&(g.wrapInput([t]),t=t.parentNode),"ontouchstart"in i&&t.addEventListener("touchstart",n,!1),t.addEventListener("mousedown",n,!1)},i.Waves=t,document.addEventListener("DOMContentLoaded",function(){t.displayEffect()},!1)}(window),function(i,n){"use strict";var t={html:"",displayLength:4e3,inDuration:300,outDuration:375,classes:"",completeCallback:null,activationPercent:.8},e=function(){function s(t){_classCallCheck(this,s),this.options=i.extend({},s.defaults,t),this.message=this.options.html,this.panning=!1,this.timeRemaining=this.options.displayLength,0===s._toasts.length&&s._createContainer(),s._toasts.push(this);var e=this._createToast();(e.M_Toast=this).el=e,this.$el=i(e),this._animateIn(),this._setTimer()}return _createClass(s,[{key:"_createToast",value:function(){var t=document.createElement("div");return t.classList.add("toast"),this.options.classes.length&&i(t).addClass(this.options.classes),("object"==typeof HTMLElement?this.message instanceof HTMLElement:this.message&&"object"==typeof this.message&&null!==this.message&&1===this.message.nodeType&&"string"==typeof this.message.nodeName)?t.appendChild(this.message):this.message.jquery?i(t).append(this.message[0]):t.innerHTML=this.message,s._container.appendChild(t),t}},{key:"_animateIn",value:function(){n({targets:this.el,top:0,opacity:1,duration:this.options.inDuration,easing:"easeOutCubic"})}},{key:"_setTimer",value:function(){var t=this;this.timeRemaining!==1/0&&(this.counterInterval=setInterval(function(){t.panning||(t.timeRemaining-=20),t.timeRemaining<=0&&t.dismiss()},20))}},{key:"dismiss",value:function(){var t=this;window.clearInterval(this.counterInterval);var e=this.el.offsetWidth*this.options.activationPercent;this.wasSwiped&&(this.el.style.transition="transform .05s, opacity .05s",this.el.style.transform="translateX("+e+"px)",this.el.style.opacity=0),n({targets:this.el,opacity:0,marginTop:-40,duration:this.options.outDuration,easing:"easeOutExpo",complete:function(){"function"==typeof t.options.completeCallback&&t.options.completeCallback(),t.$el.remove(),s._toasts.splice(s._toasts.indexOf(t),1),0===s._toasts.length&&s._removeContainer()}})}}],[{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Toast}},{key:"_createContainer",value:function(){var t=document.createElement("div");t.setAttribute("id","toast-container"),t.addEventListener("touchstart",s._onDragStart),t.addEventListener("touchmove",s._onDragMove),t.addEventListener("touchend",s._onDragEnd),t.addEventListener("mousedown",s._onDragStart),document.addEventListener("mousemove",s._onDragMove),document.addEventListener("mouseup",s._onDragEnd),document.body.appendChild(t),s._container=t}},{key:"_removeContainer",value:function(){document.removeEventListener("mousemove",s._onDragMove),document.removeEventListener("mouseup",s._onDragEnd),i(s._container).remove(),s._container=null}},{key:"_onDragStart",value:function(t){if(t.target&&i(t.target).closest(".toast").length){var e=i(t.target).closest(".toast")[0].M_Toast;e.panning=!0,(s._draggedToast=e).el.classList.add("panning"),e.el.style.transition="",e.startingXPos=s._xPos(t),e.time=Date.now(),e.xPos=s._xPos(t)}}},{key:"_onDragMove",value:function(t){if(s._draggedToast){t.preventDefault();var e=s._draggedToast;e.deltaX=Math.abs(e.xPos-s._xPos(t)),e.xPos=s._xPos(t),e.velocityX=e.deltaX/(Date.now()-e.time),e.time=Date.now();var i=e.xPos-e.startingXPos,n=e.el.offsetWidth*e.options.activationPercent;e.el.style.transform="translateX("+i+"px)",e.el.style.opacity=1-Math.abs(i/n)}}},{key:"_onDragEnd",value:function(){if(s._draggedToast){var t=s._draggedToast;t.panning=!1,t.el.classList.remove("panning");var e=t.xPos-t.startingXPos,i=t.el.offsetWidth*t.options.activationPercent;Math.abs(e)>i||1<t.velocityX?(t.wasSwiped=!0,t.dismiss()):(t.el.style.transition="transform .2s, opacity .2s",t.el.style.transform="",t.el.style.opacity=""),s._draggedToast=null}}},{key:"_xPos",value:function(t){return t.targetTouches&&1<=t.targetTouches.length?t.targetTouches[0].clientX:t.clientX}},{key:"dismissAll",value:function(){for(var t in s._toasts)s._toasts[t].dismiss()}},{key:"defaults",get:function(){return t}}]),s}();e._toasts=[],e._container=null,e._draggedToast=null,M.Toast=e,M.toast=function(t){return new e(t)}}(cash,M.anime),function(s,o){"use strict";var e={edge:"left",draggable:!0,inDuration:250,outDuration:200,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,preventScrolling:!0},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Sidenav=i).id=i.$el.attr("id"),i.options=s.extend({},n.defaults,e),i.isOpen=!1,i.isFixed=i.el.classList.contains("sidenav-fixed"),i.isDragged=!1,i.lastWindowWidth=window.innerWidth,i.lastWindowHeight=window.innerHeight,i._createOverlay(),i._createDragTarget(),i._setupEventHandlers(),i._setupClasses(),i._setupFixed(),n._sidenavs.push(i),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this._enableBodyScrolling(),this._overlay.parentNode.removeChild(this._overlay),this.dragTarget.parentNode.removeChild(this.dragTarget),this.el.M_Sidenav=void 0,this.el.style.transform="";var t=n._sidenavs.indexOf(this);0<=t&&n._sidenavs.splice(t,1)}},{key:"_createOverlay",value:function(){var t=document.createElement("div");this._closeBound=this.close.bind(this),t.classList.add("sidenav-overlay"),t.addEventListener("click",this._closeBound),document.body.appendChild(t),this._overlay=t}},{key:"_setupEventHandlers",value:function(){0===n._sidenavs.length&&document.body.addEventListener("click",this._handleTriggerClick),this._handleDragTargetDragBound=this._handleDragTargetDrag.bind(this),this._handleDragTargetReleaseBound=this._handleDragTargetRelease.bind(this),this._handleCloseDragBound=this._handleCloseDrag.bind(this),this._handleCloseReleaseBound=this._handleCloseRelease.bind(this),this._handleCloseTriggerClickBound=this._handleCloseTriggerClick.bind(this),this.dragTarget.addEventListener("touchmove",this._handleDragTargetDragBound),this.dragTarget.addEventListener("touchend",this._handleDragTargetReleaseBound),this._overlay.addEventListener("touchmove",this._handleCloseDragBound),this._overlay.addEventListener("touchend",this._handleCloseReleaseBound),this.el.addEventListener("touchmove",this._handleCloseDragBound),this.el.addEventListener("touchend",this._handleCloseReleaseBound),this.el.addEventListener("click",this._handleCloseTriggerClickBound),this.isFixed&&(this._handleWindowResizeBound=this._handleWindowResize.bind(this),window.addEventListener("resize",this._handleWindowResizeBound))}},{key:"_removeEventHandlers",value:function(){1===n._sidenavs.length&&document.body.removeEventListener("click",this._handleTriggerClick),this.dragTarget.removeEventListener("touchmove",this._handleDragTargetDragBound),this.dragTarget.removeEventListener("touchend",this._handleDragTargetReleaseBound),this._overlay.removeEventListener("touchmove",this._handleCloseDragBound),this._overlay.removeEventListener("touchend",this._handleCloseReleaseBound),this.el.removeEventListener("touchmove",this._handleCloseDragBound),this.el.removeEventListener("touchend",this._handleCloseReleaseBound),this.el.removeEventListener("click",this._handleCloseTriggerClickBound),this.isFixed&&window.removeEventListener("resize",this._handleWindowResizeBound)}},{key:"_handleTriggerClick",value:function(t){var e=s(t.target).closest(".sidenav-trigger");if(t.target&&e.length){var i=M.getIdFromTrigger(e[0]),n=document.getElementById(i).M_Sidenav;n&&n.open(e),t.preventDefault()}}},{key:"_startDrag",value:function(t){var e=t.targetTouches[0].clientX;this.isDragged=!0,this._startingXpos=e,this._xPos=this._startingXpos,this._time=Date.now(),this._width=this.el.getBoundingClientRect().width,this._overlay.style.display="block",this._initialScrollTop=this.isOpen?this.el.scrollTop:M.getDocumentScrollTop(),this._verticallyScrolling=!1,o.remove(this.el),o.remove(this._overlay)}},{key:"_dragMoveUpdate",value:function(t){var e=t.targetTouches[0].clientX,i=this.isOpen?this.el.scrollTop:M.getDocumentScrollTop();this.deltaX=Math.abs(this._xPos-e),this._xPos=e,this.velocityX=this.deltaX/(Date.now()-this._time),this._time=Date.now(),this._initialScrollTop!==i&&(this._verticallyScrolling=!0)}},{key:"_handleDragTargetDrag",value:function(t){if(this.options.draggable&&!this._isCurrentlyFixed()&&!this._verticallyScrolling){this.isDragged||this._startDrag(t),this._dragMoveUpdate(t);var e=this._xPos-this._startingXpos,i=0<e?"right":"left";e=Math.min(this._width,Math.abs(e)),this.options.edge===i&&(e=0);var n=e,s="translateX(-100%)";"right"===this.options.edge&&(s="translateX(100%)",n=-n),this.percentOpen=Math.min(1,e/this._width),this.el.style.transform=s+" translateX("+n+"px)",this._overlay.style.opacity=this.percentOpen}}},{key:"_handleDragTargetRelease",value:function(){this.isDragged&&(.2<this.percentOpen?this.open():this._animateOut(),this.isDragged=!1,this._verticallyScrolling=!1)}},{key:"_handleCloseDrag",value:function(t){if(this.isOpen){if(!this.options.draggable||this._isCurrentlyFixed()||this._verticallyScrolling)return;this.isDragged||this._startDrag(t),this._dragMoveUpdate(t);var e=this._xPos-this._startingXpos,i=0<e?"right":"left";e=Math.min(this._width,Math.abs(e)),this.options.edge!==i&&(e=0);var n=-e;"right"===this.options.edge&&(n=-n),this.percentOpen=Math.min(1,1-e/this._width),this.el.style.transform="translateX("+n+"px)",this._overlay.style.opacity=this.percentOpen}}},{key:"_handleCloseRelease",value:function(){this.isOpen&&this.isDragged&&(.8<this.percentOpen?this._animateIn():this.close(),this.isDragged=!1,this._verticallyScrolling=!1)}},{key:"_handleCloseTriggerClick",value:function(t){s(t.target).closest(".sidenav-close").length&&!this._isCurrentlyFixed()&&this.close()}},{key:"_handleWindowResize",value:function(){this.lastWindowWidth!==window.innerWidth&&(992<window.innerWidth?this.open():this.close()),this.lastWindowWidth=window.innerWidth,this.lastWindowHeight=window.innerHeight}},{key:"_setupClasses",value:function(){"right"===this.options.edge&&(this.el.classList.add("right-aligned"),this.dragTarget.classList.add("right-aligned"))}},{key:"_removeClasses",value:function(){this.el.classList.remove("right-aligned"),this.dragTarget.classList.remove("right-aligned")}},{key:"_setupFixed",value:function(){this._isCurrentlyFixed()&&this.open()}},{key:"_isCurrentlyFixed",value:function(){return this.isFixed&&992<window.innerWidth}},{key:"_createDragTarget",value:function(){var t=document.createElement("div");t.classList.add("drag-target"),document.body.appendChild(t),this.dragTarget=t}},{key:"_preventBodyScrolling",value:function(){document.body.style.overflow="hidden"}},{key:"_enableBodyScrolling",value:function(){document.body.style.overflow=""}},{key:"open",value:function(){!0!==this.isOpen&&(this.isOpen=!0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el),this._isCurrentlyFixed()?(o.remove(this.el),o({targets:this.el,translateX:0,duration:0,easing:"easeOutQuad"}),this._enableBodyScrolling(),this._overlay.style.display="none"):(this.options.preventScrolling&&this._preventBodyScrolling(),this.isDragged&&1==this.percentOpen||this._animateIn()))}},{key:"close",value:function(){if(!1!==this.isOpen)if(this.isOpen=!1,"function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,this.el),this._isCurrentlyFixed()){var t="left"===this.options.edge?"-105%":"105%";this.el.style.transform="translateX("+t+")"}else this._enableBodyScrolling(),this.isDragged&&0==this.percentOpen?this._overlay.style.display="none":this._animateOut()}},{key:"_animateIn",value:function(){this._animateSidenavIn(),this._animateOverlayIn()}},{key:"_animateSidenavIn",value:function(){var t=this,e="left"===this.options.edge?-1:1;this.isDragged&&(e="left"===this.options.edge?e+this.percentOpen:e-this.percentOpen),o.remove(this.el),o({targets:this.el,translateX:[100*e+"%",0],duration:this.options.inDuration,easing:"easeOutQuad",complete:function(){"function"==typeof t.options.onOpenEnd&&t.options.onOpenEnd.call(t,t.el)}})}},{key:"_animateOverlayIn",value:function(){var t=0;this.isDragged?t=this.percentOpen:s(this._overlay).css({display:"block"}),o.remove(this._overlay),o({targets:this._overlay,opacity:[t,1],duration:this.options.inDuration,easing:"easeOutQuad"})}},{key:"_animateOut",value:function(){this._animateSidenavOut(),this._animateOverlayOut()}},{key:"_animateSidenavOut",value:function(){var t=this,e="left"===this.options.edge?-1:1,i=0;this.isDragged&&(i="left"===this.options.edge?e+this.percentOpen:e-this.percentOpen),o.remove(this.el),o({targets:this.el,translateX:[100*i+"%",105*e+"%"],duration:this.options.outDuration,easing:"easeOutQuad",complete:function(){"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t,t.el)}})}},{key:"_animateOverlayOut",value:function(){var t=this;o.remove(this._overlay),o({targets:this._overlay,opacity:0,duration:this.options.outDuration,easing:"easeOutQuad",complete:function(){s(t._overlay).css("display","none")}})}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Sidenav}},{key:"defaults",get:function(){return e}}]),n}();t._sidenavs=[],M.Sidenav=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"sidenav","M_Sidenav")}(cash,M.anime),function(o,a){"use strict";var e={throttle:100,scrollOffset:200,activeClass:"active",getActiveElement:function(t){return'a[href="#'+t+'"]'}},t=function(t){function c(t,e){_classCallCheck(this,c);var i=_possibleConstructorReturn(this,(c.__proto__||Object.getPrototypeOf(c)).call(this,c,t,e));return(i.el.M_ScrollSpy=i).options=o.extend({},c.defaults,e),c._elements.push(i),c._count++,c._increment++,i.tickId=-1,i.id=c._increment,i._setupEventHandlers(),i._handleWindowScroll(),i}return _inherits(c,Component),_createClass(c,[{key:"destroy",value:function(){c._elements.splice(c._elements.indexOf(this),1),c._elementsInView.splice(c._elementsInView.indexOf(this),1),c._visibleElements.splice(c._visibleElements.indexOf(this.$el),1),c._count--,this._removeEventHandlers(),o(this.options.getActiveElement(this.$el.attr("id"))).removeClass(this.options.activeClass),this.el.M_ScrollSpy=void 0}},{key:"_setupEventHandlers",value:function(){var t=M.throttle(this._handleWindowScroll,200);this._handleThrottledResizeBound=t.bind(this),this._handleWindowScrollBound=this._handleWindowScroll.bind(this),1===c._count&&(window.addEventListener("scroll",this._handleWindowScrollBound),window.addEventListener("resize",this._handleThrottledResizeBound),document.body.addEventListener("click",this._handleTriggerClick))}},{key:"_removeEventHandlers",value:function(){0===c._count&&(window.removeEventListener("scroll",this._handleWindowScrollBound),window.removeEventListener("resize",this._handleThrottledResizeBound),document.body.removeEventListener("click",this._handleTriggerClick))}},{key:"_handleTriggerClick",value:function(t){for(var e=o(t.target),i=c._elements.length-1;0<=i;i--){var n=c._elements[i];if(e.is('a[href="#'+n.$el.attr("id")+'"]')){t.preventDefault();var s=n.$el.offset().top+1;a({targets:[document.documentElement,document.body],scrollTop:s-n.options.scrollOffset,duration:400,easing:"easeOutCubic"});break}}}},{key:"_handleWindowScroll",value:function(){c._ticks++;for(var t=M.getDocumentScrollTop(),e=M.getDocumentScrollLeft(),i=e+window.innerWidth,n=t+window.innerHeight,s=c._findElements(t,i,n,e),o=0;o<s.length;o++){var a=s[o];a.tickId<0&&a._enter(),a.tickId=c._ticks}for(var r=0;r<c._elementsInView.length;r++){var l=c._elementsInView[r],h=l.tickId;0<=h&&h!==c._ticks&&(l._exit(),l.tickId=-1)}c._elementsInView=s}},{key:"_enter",value:function(){(c._visibleElements=c._visibleElements.filter(function(t){return 0!=t.height()}))[0]?(o(this.options.getActiveElement(c._visibleElements[0].attr("id"))).removeClass(this.options.activeClass),c._visibleElements[0][0].M_ScrollSpy&&this.id<c._visibleElements[0][0].M_ScrollSpy.id?c._visibleElements.unshift(this.$el):c._visibleElements.push(this.$el)):c._visibleElements.push(this.$el),o(this.options.getActiveElement(c._visibleElements[0].attr("id"))).addClass(this.options.activeClass)}},{key:"_exit",value:function(){var e=this;(c._visibleElements=c._visibleElements.filter(function(t){return 0!=t.height()}))[0]&&(o(this.options.getActiveElement(c._visibleElements[0].attr("id"))).removeClass(this.options.activeClass),(c._visibleElements=c._visibleElements.filter(function(t){return t.attr("id")!=e.$el.attr("id")}))[0]&&o(this.options.getActiveElement(c._visibleElements[0].attr("id"))).addClass(this.options.activeClass))}}],[{key:"init",value:function(t,e){return _get(c.__proto__||Object.getPrototypeOf(c),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_ScrollSpy}},{key:"_findElements",value:function(t,e,i,n){for(var s=[],o=0;o<c._elements.length;o++){var a=c._elements[o],r=t+a.options.scrollOffset||200;if(0<a.$el.height()){var l=a.$el.offset().top,h=a.$el.offset().left,d=h+a.$el.width(),u=l+a.$el.height();!(e<h||d<n||i<l||u<r)&&s.push(a)}}return s}},{key:"defaults",get:function(){return e}}]),c}();t._elements=[],t._elementsInView=[],t._visibleElements=[],t._count=0,t._increment=0,t._ticks=0,M.ScrollSpy=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"scrollSpy","M_ScrollSpy")}(cash,M.anime),function(h){"use strict";var e={data:{},limit:1/0,onAutocomplete:null,minLength:1,sortFunction:function(t,e,i){return t.indexOf(i)-e.indexOf(i)}},t=function(t){function s(t,e){_classCallCheck(this,s);var i=_possibleConstructorReturn(this,(s.__proto__||Object.getPrototypeOf(s)).call(this,s,t,e));return(i.el.M_Autocomplete=i).options=h.extend({},s.defaults,e),i.isOpen=!1,i.count=0,i.activeIndex=-1,i.oldVal,i.$inputField=i.$el.closest(".input-field"),i.$active=h(),i._mousedown=!1,i._setupDropdown(),i._setupEventHandlers(),i}return _inherits(s,Component),_createClass(s,[{key:"destroy",value:function(){this._removeEventHandlers(),this._removeDropdown(),this.el.M_Autocomplete=void 0}},{key:"_setupEventHandlers",value:function(){this._handleInputBlurBound=this._handleInputBlur.bind(this),this._handleInputKeyupAndFocusBound=this._handleInputKeyupAndFocus.bind(this),this._handleInputKeydownBound=this._handleInputKeydown.bind(this),this._handleInputClickBound=this._handleInputClick.bind(this),this._handleContainerMousedownAndTouchstartBound=this._handleContainerMousedownAndTouchstart.bind(this),this._handleContainerMouseupAndTouchendBound=this._handleContainerMouseupAndTouchend.bind(this),this.el.addEventListener("blur",this._handleInputBlurBound),this.el.addEventListener("keyup",this._handleInputKeyupAndFocusBound),this.el.addEventListener("focus",this._handleInputKeyupAndFocusBound),this.el.addEventListener("keydown",this._handleInputKeydownBound),this.el.addEventListener("click",this._handleInputClickBound),this.container.addEventListener("mousedown",this._handleContainerMousedownAndTouchstartBound),this.container.addEventListener("mouseup",this._handleContainerMouseupAndTouchendBound),void 0!==window.ontouchstart&&(this.container.addEventListener("touchstart",this._handleContainerMousedownAndTouchstartBound),this.container.addEventListener("touchend",this._handleContainerMouseupAndTouchendBound))}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("blur",this._handleInputBlurBound),this.el.removeEventListener("keyup",this._handleInputKeyupAndFocusBound),this.el.removeEventListener("focus",this._handleInputKeyupAndFocusBound),this.el.removeEventListener("keydown",this._handleInputKeydownBound),this.el.removeEventListener("click",this._handleInputClickBound),this.container.removeEventListener("mousedown",this._handleContainerMousedownAndTouchstartBound),this.container.removeEventListener("mouseup",this._handleContainerMouseupAndTouchendBound),void 0!==window.ontouchstart&&(this.container.removeEventListener("touchstart",this._handleContainerMousedownAndTouchstartBound),this.container.removeEventListener("touchend",this._handleContainerMouseupAndTouchendBound))}},{key:"_setupDropdown",value:function(){var e=this;this.container=document.createElement("ul"),this.container.id="autocomplete-options-"+M.guid(),h(this.container).addClass("autocomplete-content dropdown-content"),this.$inputField.append(this.container),this.el.setAttribute("data-target",this.container.id),this.dropdown=M.Dropdown.init(this.el,{autoFocus:!1,closeOnClick:!1,coverTrigger:!1,onItemClick:function(t){e.selectOption(h(t))}}),this.el.removeEventListener("click",this.dropdown._handleClickBound)}},{key:"_removeDropdown",value:function(){this.container.parentNode.removeChild(this.container)}},{key:"_handleInputBlur",value:function(){this._mousedown||(this.close(),this._resetAutocomplete())}},{key:"_handleInputKeyupAndFocus",value:function(t){"keyup"===t.type&&(s._keydown=!1),this.count=0;var e=this.el.value.toLowerCase();13!==t.keyCode&&38!==t.keyCode&&40!==t.keyCode&&(this.oldVal===e||!M.tabPressed&&"focus"===t.type||this.open(),this.oldVal=e)}},{key:"_handleInputKeydown",value:function(t){s._keydown=!0;var e=t.keyCode,i=void 0,n=h(this.container).children("li").length;e===M.keys.ENTER&&0<=this.activeIndex?(i=h(this.container).children("li").eq(this.activeIndex)).length&&(this.selectOption(i),t.preventDefault()):e!==M.keys.ARROW_UP&&e!==M.keys.ARROW_DOWN||(t.preventDefault(),e===M.keys.ARROW_UP&&0<this.activeIndex&&this.activeIndex--,e===M.keys.ARROW_DOWN&&this.activeIndex<n-1&&this.activeIndex++,this.$active.removeClass("active"),0<=this.activeIndex&&(this.$active=h(this.container).children("li").eq(this.activeIndex),this.$active.addClass("active")))}},{key:"_handleInputClick",value:function(t){this.open()}},{key:"_handleContainerMousedownAndTouchstart",value:function(t){this._mousedown=!0}},{key:"_handleContainerMouseupAndTouchend",value:function(t){this._mousedown=!1}},{key:"_highlight",value:function(t,e){var i=e.find("img"),n=e.text().toLowerCase().indexOf(""+t.toLowerCase()),s=n+t.length-1,o=e.text().slice(0,n),a=e.text().slice(n,s+1),r=e.text().slice(s+1);e.html("<span>"+o+"<span class='highlight'>"+a+"</span>"+r+"</span>"),i.length&&e.prepend(i)}},{key:"_resetCurrentElement",value:function(){this.activeIndex=-1,this.$active.removeClass("active")}},{key:"_resetAutocomplete",value:function(){h(this.container).empty(),this._resetCurrentElement(),this.oldVal=null,this.isOpen=!1,this._mousedown=!1}},{key:"selectOption",value:function(t){var e=t.text().trim();this.el.value=e,this.$el.trigger("change"),this._resetAutocomplete(),this.close(),"function"==typeof this.options.onAutocomplete&&this.options.onAutocomplete.call(this,e)}},{key:"_renderDropdown",value:function(t,i){var n=this;this._resetAutocomplete();var e=[];for(var s in t)if(t.hasOwnProperty(s)&&-1!==s.toLowerCase().indexOf(i)){if(this.count>=this.options.limit)break;var o={data:t[s],key:s};e.push(o),this.count++}if(this.options.sortFunction){e.sort(function(t,e){return n.options.sortFunction(t.key.toLowerCase(),e.key.toLowerCase(),i.toLowerCase())})}for(var a=0;a<e.length;a++){var r=e[a],l=h("<li></li>");r.data?l.append('<img src="'+r.data+'" class="right circle"><span>'+r.key+"</span>"):l.append("<span>"+r.key+"</span>"),h(this.container).append(l),this._highlight(i,l)}}},{key:"open",value:function(){var t=this.el.value.toLowerCase();this._resetAutocomplete(),t.length>=this.options.minLength&&(this.isOpen=!0,this._renderDropdown(this.options.data,t)),this.dropdown.isOpen?this.dropdown.recalculateDimensions():this.dropdown.open()}},{key:"close",value:function(){this.dropdown.close()}},{key:"updateData",value:function(t){var e=this.el.value.toLowerCase();this.options.data=t,this.isOpen&&this._renderDropdown(t,e)}}],[{key:"init",value:function(t,e){return _get(s.__proto__||Object.getPrototypeOf(s),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Autocomplete}},{key:"defaults",get:function(){return e}}]),s}();t._keydown=!1,M.Autocomplete=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"autocomplete","M_Autocomplete")}(cash),function(d){M.updateTextFields=function(){d("input[type=text], input[type=password], input[type=email], input[type=url], input[type=tel], input[type=number], input[type=search], input[type=date], input[type=time], textarea").each(function(t,e){var i=d(this);0<t.value.length||d(t).is(":focus")||t.autofocus||null!==i.attr("placeholder")?i.siblings("label").addClass("active"):t.validity?i.siblings("label").toggleClass("active",!0===t.validity.badInput):i.siblings("label").removeClass("active")})},M.validate_field=function(t){var e=null!==t.attr("data-length"),i=parseInt(t.attr("data-length")),n=t[0].value.length;0!==n||!1!==t[0].validity.badInput||t.is(":required")?t.hasClass("validate")&&(t.is(":valid")&&e&&n<=i||t.is(":valid")&&!e?(t.removeClass("invalid"),t.addClass("valid")):(t.removeClass("valid"),t.addClass("invalid"))):t.hasClass("validate")&&(t.removeClass("valid"),t.removeClass("invalid"))},M.textareaAutoResize=function(t){if(t instanceof Element&&(t=d(t)),t.length){var e=d(".hiddendiv").first();e.length||(e=d('<div class="hiddendiv common"></div>'),d("body").append(e));var i=t.css("font-family"),n=t.css("font-size"),s=t.css("line-height"),o=t.css("padding-top"),a=t.css("padding-right"),r=t.css("padding-bottom"),l=t.css("padding-left");n&&e.css("font-size",n),i&&e.css("font-family",i),s&&e.css("line-height",s),o&&e.css("padding-top",o),a&&e.css("padding-right",a),r&&e.css("padding-bottom",r),l&&e.css("padding-left",l),t.data("original-height")||t.data("original-height",t.height()),"off"===t.attr("wrap")&&e.css("overflow-wrap","normal").css("white-space","pre"),e.text(t[0].value+"\n");var h=e.html().replace(/\n/g,"<br>");e.html(h),0<t[0].offsetWidth&&0<t[0].offsetHeight?e.css("width",t.width()+"px"):e.css("width",window.innerWidth/2+"px"),t.data("original-height")<=e.innerHeight()?t.css("height",e.innerHeight()+"px"):t[0].value.length<t.data("previous-length")&&t.css("height",t.data("original-height")+"px"),t.data("previous-length",t[0].value.length)}else console.error("No textarea element found")},d(document).ready(function(){var n="input[type=text], input[type=password], input[type=email], input[type=url], input[type=tel], input[type=number], input[type=search], input[type=date], input[type=time], textarea";d(document).on("change",n,function(){0===this.value.length&&null===d(this).attr("placeholder")||d(this).siblings("label").addClass("active"),M.validate_field(d(this))}),d(document).ready(function(){M.updateTextFields()}),d(document).on("reset",function(t){var e=d(t.target);e.is("form")&&(e.find(n).removeClass("valid").removeClass("invalid"),e.find(n).each(function(t){this.value.length&&d(this).siblings("label").removeClass("active")}),setTimeout(function(){e.find("select").each(function(){this.M_FormSelect&&d(this).trigger("change")})},0))}),document.addEventListener("focus",function(t){d(t.target).is(n)&&d(t.target).siblings("label, .prefix").addClass("active")},!0),document.addEventListener("blur",function(t){var e=d(t.target);if(e.is(n)){var i=".prefix";0===e[0].value.length&&!0!==e[0].validity.badInput&&null===e.attr("placeholder")&&(i+=", label"),e.siblings(i).removeClass("active"),M.validate_field(e)}},!0);d(document).on("keyup","input[type=radio], input[type=checkbox]",function(t){if(t.which===M.keys.TAB)return d(this).addClass("tabbed"),void d(this).one("blur",function(t){d(this).removeClass("tabbed")})});var t=".materialize-textarea";d(t).each(function(){var t=d(this);t.data("original-height",t.height()),t.data("previous-length",this.value.length),M.textareaAutoResize(t)}),d(document).on("keyup",t,function(){M.textareaAutoResize(d(this))}),d(document).on("keydown",t,function(){M.textareaAutoResize(d(this))}),d(document).on("change",'.file-field input[type="file"]',function(){for(var t=d(this).closest(".file-field").find("input.file-path"),e=d(this)[0].files,i=[],n=0;n<e.length;n++)i.push(e[n].name);t[0].value=i.join(", "),t.trigger("change")})})}(cash),function(s,o){"use strict";var e={indicators:!0,height:400,duration:500,interval:6e3},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Slider=i).options=s.extend({},n.defaults,e),i.$slider=i.$el.find(".slides"),i.$slides=i.$slider.children("li"),i.activeIndex=i.$slides.filter(function(t){return s(t).hasClass("active")}).first().index(),-1!=i.activeIndex&&(i.$active=i.$slides.eq(i.activeIndex)),i._setSliderHeight(),i.$slides.find(".caption").each(function(t){i._animateCaptionIn(t,0)}),i.$slides.find("img").each(function(t){var e="data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";s(t).attr("src")!==e&&(s(t).css("background-image",'url("'+s(t).attr("src")+'")'),s(t).attr("src",e))}),i._setupIndicators(),i.$active?i.$active.css("display","block"):(i.$slides.first().addClass("active"),o({targets:i.$slides.first()[0],opacity:1,duration:i.options.duration,easing:"easeOutQuad"}),i.activeIndex=0,i.$active=i.$slides.eq(i.activeIndex),i.options.indicators&&i.$indicators.eq(i.activeIndex).addClass("active")),i.$active.find("img").each(function(t){o({targets:i.$active.find(".caption")[0],opacity:1,translateX:0,translateY:0,duration:i.options.duration,easing:"easeOutQuad"})}),i._setupEventHandlers(),i.start(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this.pause(),this._removeIndicators(),this._removeEventHandlers(),this.el.M_Slider=void 0}},{key:"_setupEventHandlers",value:function(){var e=this;this._handleIntervalBound=this._handleInterval.bind(this),this._handleIndicatorClickBound=this._handleIndicatorClick.bind(this),this.options.indicators&&this.$indicators.each(function(t){t.addEventListener("click",e._handleIndicatorClickBound)})}},{key:"_removeEventHandlers",value:function(){var e=this;this.options.indicators&&this.$indicators.each(function(t){t.removeEventListener("click",e._handleIndicatorClickBound)})}},{key:"_handleIndicatorClick",value:function(t){var e=s(t.target).index();this.set(e)}},{key:"_handleInterval",value:function(){var t=this.$slider.find(".active").index();this.$slides.length===t+1?t=0:t+=1,this.set(t)}},{key:"_animateCaptionIn",value:function(t,e){var i={targets:t,opacity:0,duration:e,easing:"easeOutQuad"};s(t).hasClass("center-align")?i.translateY=-100:s(t).hasClass("right-align")?i.translateX=100:s(t).hasClass("left-align")&&(i.translateX=-100),o(i)}},{key:"_setSliderHeight",value:function(){this.$el.hasClass("fullscreen")||(this.options.indicators?this.$el.css("height",this.options.height+40+"px"):this.$el.css("height",this.options.height+"px"),this.$slider.css("height",this.options.height+"px"))}},{key:"_setupIndicators",value:function(){var n=this;this.options.indicators&&(this.$indicators=s('<ul class="indicators"></ul>'),this.$slides.each(function(t,e){var i=s('<li class="indicator-item"></li>');n.$indicators.append(i[0])}),this.$el.append(this.$indicators[0]),this.$indicators=this.$indicators.children("li.indicator-item"))}},{key:"_removeIndicators",value:function(){this.$el.find("ul.indicators").remove()}},{key:"set",value:function(t){var e=this;if(t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.activeIndex!=t){this.$active=this.$slides.eq(this.activeIndex);var i=this.$active.find(".caption");this.$active.removeClass("active"),o({targets:this.$active[0],opacity:0,duration:this.options.duration,easing:"easeOutQuad",complete:function(){e.$slides.not(".active").each(function(t){o({targets:t,opacity:0,translateX:0,translateY:0,duration:0,easing:"easeOutQuad"})})}}),this._animateCaptionIn(i[0],this.options.duration),this.options.indicators&&(this.$indicators.eq(this.activeIndex).removeClass("active"),this.$indicators.eq(t).addClass("active")),o({targets:this.$slides.eq(t)[0],opacity:1,duration:this.options.duration,easing:"easeOutQuad"}),o({targets:this.$slides.eq(t).find(".caption")[0],opacity:1,translateX:0,translateY:0,duration:this.options.duration,delay:this.options.duration,easing:"easeOutQuad"}),this.$slides.eq(t).addClass("active"),this.activeIndex=t,this.start()}}},{key:"pause",value:function(){clearInterval(this.interval)}},{key:"start",value:function(){clearInterval(this.interval),this.interval=setInterval(this._handleIntervalBound,this.options.duration+this.options.interval)}},{key:"next",value:function(){var t=this.activeIndex+1;t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.set(t)}},{key:"prev",value:function(){var t=this.activeIndex-1;t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.set(t)}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Slider}},{key:"defaults",get:function(){return e}}]),n}();M.Slider=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"slider","M_Slider")}(cash,M.anime),function(n,s){n(document).on("click",".card",function(t){if(n(this).children(".card-reveal").length){var i=n(t.target).closest(".card");void 0===i.data("initialOverflow")&&i.data("initialOverflow",void 0===i.css("overflow")?"":i.css("overflow"));var e=n(this).find(".card-reveal");n(t.target).is(n(".card-reveal .card-title"))||n(t.target).is(n(".card-reveal .card-title i"))?s({targets:e[0],translateY:0,duration:225,easing:"easeInOutQuad",complete:function(t){var e=t.animatables[0].target;n(e).css({display:"none"}),i.css("overflow",i.data("initialOverflow"))}}):(n(t.target).is(n(".card .activator"))||n(t.target).is(n(".card .activator i")))&&(i.css("overflow","hidden"),e.css({display:"block"}),s({targets:e[0],translateY:"-100%",duration:300,easing:"easeInOutQuad"}))}})}(cash,M.anime),function(h){"use strict";var e={data:[],placeholder:"",secondaryPlaceholder:"",autocompleteOptions:{},limit:1/0,onChipAdd:null,onChipSelect:null,onChipDelete:null},t=function(t){function l(t,e){_classCallCheck(this,l);var i=_possibleConstructorReturn(this,(l.__proto__||Object.getPrototypeOf(l)).call(this,l,t,e));return(i.el.M_Chips=i).options=h.extend({},l.defaults,e),i.$el.addClass("chips input-field"),i.chipsData=[],i.$chips=h(),i._setupInput(),i.hasAutocomplete=0<Object.keys(i.options.autocompleteOptions).length,i.$input.attr("id")||i.$input.attr("id",M.guid()),i.options.data.length&&(i.chipsData=i.options.data,i._renderChips(i.chipsData)),i.hasAutocomplete&&i._setupAutocomplete(),i._setPlaceholder(),i._setupLabel(),i._setupEventHandlers(),i}return _inherits(l,Component),_createClass(l,[{key:"getData",value:function(){return this.chipsData}},{key:"destroy",value:function(){this._removeEventHandlers(),this.$chips.remove(),this.el.M_Chips=void 0}},{key:"_setupEventHandlers",value:function(){this._handleChipClickBound=this._handleChipClick.bind(this),this._handleInputKeydownBound=this._handleInputKeydown.bind(this),this._handleInputFocusBound=this._handleInputFocus.bind(this),this._handleInputBlurBound=this._handleInputBlur.bind(this),this.el.addEventListener("click",this._handleChipClickBound),document.addEventListener("keydown",l._handleChipsKeydown),document.addEventListener("keyup",l._handleChipsKeyup),this.el.addEventListener("blur",l._handleChipsBlur,!0),this.$input[0].addEventListener("focus",this._handleInputFocusBound),this.$input[0].addEventListener("blur",this._handleInputBlurBound),this.$input[0].addEventListener("keydown",this._handleInputKeydownBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleChipClickBound),document.removeEventListener("keydown",l._handleChipsKeydown),document.removeEventListener("keyup",l._handleChipsKeyup),this.el.removeEventListener("blur",l._handleChipsBlur,!0),this.$input[0].removeEventListener("focus",this._handleInputFocusBound),this.$input[0].removeEventListener("blur",this._handleInputBlurBound),this.$input[0].removeEventListener("keydown",this._handleInputKeydownBound)}},{key:"_handleChipClick",value:function(t){var e=h(t.target).closest(".chip"),i=h(t.target).is(".close");if(e.length){var n=e.index();i?(this.deleteChip(n),this.$input[0].focus()):this.selectChip(n)}else this.$input[0].focus()}},{key:"_handleInputFocus",value:function(){this.$el.addClass("focus")}},{key:"_handleInputBlur",value:function(){this.$el.removeClass("focus")}},{key:"_handleInputKeydown",value:function(t){if(l._keydown=!0,13===t.keyCode){if(this.hasAutocomplete&&this.autocomplete&&this.autocomplete.isOpen)return;t.preventDefault(),this.addChip({tag:this.$input[0].value}),this.$input[0].value=""}else 8!==t.keyCode&&37!==t.keyCode||""!==this.$input[0].value||!this.chipsData.length||(t.preventDefault(),this.selectChip(this.chipsData.length-1))}},{key:"_renderChip",value:function(t){if(t.tag){var e=document.createElement("div"),i=document.createElement("i");if(e.classList.add("chip"),e.textContent=t.tag,e.setAttribute("tabindex",0),h(i).addClass("material-icons close"),i.textContent="close",t.image){var n=document.createElement("img");n.setAttribute("src",t.image),e.insertBefore(n,e.firstChild)}return e.appendChild(i),e}}},{key:"_renderChips",value:function(){this.$chips.remove();for(var t=0;t<this.chipsData.length;t++){var e=this._renderChip(this.chipsData[t]);this.$el.append(e),this.$chips.add(e)}this.$el.append(this.$input[0])}},{key:"_setupAutocomplete",value:function(){var e=this;this.options.autocompleteOptions.onAutocomplete=function(t){e.addChip({tag:t}),e.$input[0].value="",e.$input[0].focus()},this.autocomplete=M.Autocomplete.init(this.$input[0],this.options.autocompleteOptions)}},{key:"_setupInput",value:function(){this.$input=this.$el.find("input"),this.$input.length||(this.$input=h("<input></input>"),this.$el.append(this.$input)),this.$input.addClass("input")}},{key:"_setupLabel",value:function(){this.$label=this.$el.find("label"),this.$label.length&&this.$label.setAttribute("for",this.$input.attr("id"))}},{key:"_setPlaceholder",value:function(){void 0!==this.chipsData&&!this.chipsData.length&&this.options.placeholder?h(this.$input).prop("placeholder",this.options.placeholder):(void 0===this.chipsData||this.chipsData.length)&&this.options.secondaryPlaceholder&&h(this.$input).prop("placeholder",this.options.secondaryPlaceholder)}},{key:"_isValid",value:function(t){if(t.hasOwnProperty("tag")&&""!==t.tag){for(var e=!1,i=0;i<this.chipsData.length;i++)if(this.chipsData[i].tag===t.tag){e=!0;break}return!e}return!1}},{key:"addChip",value:function(t){if(this._isValid(t)&&!(this.chipsData.length>=this.options.limit)){var e=this._renderChip(t);this.$chips.add(e),this.chipsData.push(t),h(this.$input).before(e),this._setPlaceholder(),"function"==typeof this.options.onChipAdd&&this.options.onChipAdd.call(this,this.$el,e)}}},{key:"deleteChip",value:function(t){var e=this.$chips.eq(t);this.$chips.eq(t).remove(),this.$chips=this.$chips.filter(function(t){return 0<=h(t).index()}),this.chipsData.splice(t,1),this._setPlaceholder(),"function"==typeof this.options.onChipDelete&&this.options.onChipDelete.call(this,this.$el,e[0])}},{key:"selectChip",value:function(t){var e=this.$chips.eq(t);(this._selectedChip=e)[0].focus(),"function"==typeof this.options.onChipSelect&&this.options.onChipSelect.call(this,this.$el,e[0])}}],[{key:"init",value:function(t,e){return _get(l.__proto__||Object.getPrototypeOf(l),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Chips}},{key:"_handleChipsKeydown",value:function(t){l._keydown=!0;var e=h(t.target).closest(".chips"),i=t.target&&e.length;if(!h(t.target).is("input, textarea")&&i){var n=e[0].M_Chips;if(8===t.keyCode||46===t.keyCode){t.preventDefault();var s=n.chipsData.length;if(n._selectedChip){var o=n._selectedChip.index();n.deleteChip(o),n._selectedChip=null,s=Math.max(o-1,0)}n.chipsData.length&&n.selectChip(s)}else if(37===t.keyCode){if(n._selectedChip){var a=n._selectedChip.index()-1;if(a<0)return;n.selectChip(a)}}else if(39===t.keyCode&&n._selectedChip){var r=n._selectedChip.index()+1;r>=n.chipsData.length?n.$input[0].focus():n.selectChip(r)}}}},{key:"_handleChipsKeyup",value:function(t){l._keydown=!1}},{key:"_handleChipsBlur",value:function(t){l._keydown||(h(t.target).closest(".chips")[0].M_Chips._selectedChip=null)}},{key:"defaults",get:function(){return e}}]),l}();t._keydown=!1,M.Chips=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"chips","M_Chips"),h(document).ready(function(){h(document.body).on("click",".chip .close",function(){var t=h(this).closest(".chips");t.length&&t[0].M_Chips||h(this).closest(".chip").remove()})})}(cash),function(s){"use strict";var e={top:0,bottom:1/0,offset:0,onPositionChange:null},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Pushpin=i).options=s.extend({},n.defaults,e),i.originalOffset=i.el.offsetTop,n._pushpins.push(i),i._setupEventHandlers(),i._updatePosition(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this.el.style.top=null,this._removePinClasses(),this._removeEventHandlers();var t=n._pushpins.indexOf(this);n._pushpins.splice(t,1)}},{key:"_setupEventHandlers",value:function(){document.addEventListener("scroll",n._updateElements)}},{key:"_removeEventHandlers",value:function(){document.removeEventListener("scroll",n._updateElements)}},{key:"_updatePosition",value:function(){var t=M.getDocumentScrollTop()+this.options.offset;this.options.top<=t&&this.options.bottom>=t&&!this.el.classList.contains("pinned")&&(this._removePinClasses(),this.el.style.top=this.options.offset+"px",this.el.classList.add("pinned"),"function"==typeof this.options.onPositionChange&&this.options.onPositionChange.call(this,"pinned")),t<this.options.top&&!this.el.classList.contains("pin-top")&&(this._removePinClasses(),this.el.style.top=0,this.el.classList.add("pin-top"),"function"==typeof this.options.onPositionChange&&this.options.onPositionChange.call(this,"pin-top")),t>this.options.bottom&&!this.el.classList.contains("pin-bottom")&&(this._removePinClasses(),this.el.classList.add("pin-bottom"),this.el.style.top=this.options.bottom-this.originalOffset+"px","function"==typeof this.options.onPositionChange&&this.options.onPositionChange.call(this,"pin-bottom"))}},{key:"_removePinClasses",value:function(){this.el.classList.remove("pin-top"),this.el.classList.remove("pinned"),this.el.classList.remove("pin-bottom")}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Pushpin}},{key:"_updateElements",value:function(){for(var t in n._pushpins){n._pushpins[t]._updatePosition()}}},{key:"defaults",get:function(){return e}}]),n}();t._pushpins=[],M.Pushpin=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"pushpin","M_Pushpin")}(cash),function(r,s){"use strict";var e={direction:"top",hoverEnabled:!0,toolbarEnabled:!1};r.fn.reverse=[].reverse;var t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_FloatingActionButton=i).options=r.extend({},n.defaults,e),i.isOpen=!1,i.$anchor=i.$el.children("a").first(),i.$menu=i.$el.children("ul").first(),i.$floatingBtns=i.$el.find("ul .btn-floating"),i.$floatingBtnsReverse=i.$el.find("ul .btn-floating").reverse(),i.offsetY=0,i.offsetX=0,i.$el.addClass("direction-"+i.options.direction),"top"===i.options.direction?i.offsetY=40:"right"===i.options.direction?i.offsetX=-40:"bottom"===i.options.direction?i.offsetY=-40:i.offsetX=40,i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_FloatingActionButton=void 0}},{key:"_setupEventHandlers",value:function(){this._handleFABClickBound=this._handleFABClick.bind(this),this._handleOpenBound=this.open.bind(this),this._handleCloseBound=this.close.bind(this),this.options.hoverEnabled&&!this.options.toolbarEnabled?(this.el.addEventListener("mouseenter",this._handleOpenBound),this.el.addEventListener("mouseleave",this._handleCloseBound)):this.el.addEventListener("click",this._handleFABClickBound)}},{key:"_removeEventHandlers",value:function(){this.options.hoverEnabled&&!this.options.toolbarEnabled?(this.el.removeEventListener("mouseenter",this._handleOpenBound),this.el.removeEventListener("mouseleave",this._handleCloseBound)):this.el.removeEventListener("click",this._handleFABClickBound)}},{key:"_handleFABClick",value:function(){this.isOpen?this.close():this.open()}},{key:"_handleDocumentClick",value:function(t){r(t.target).closest(this.$menu).length||this.close()}},{key:"open",value:function(){this.isOpen||(this.options.toolbarEnabled?this._animateInToolbar():this._animateInFAB(),this.isOpen=!0)}},{key:"close",value:function(){this.isOpen&&(this.options.toolbarEnabled?(window.removeEventListener("scroll",this._handleCloseBound,!0),document.body.removeEventListener("click",this._handleDocumentClickBound,!0),this._animateOutToolbar()):this._animateOutFAB(),this.isOpen=!1)}},{key:"_animateInFAB",value:function(){var e=this;this.$el.addClass("active");var i=0;this.$floatingBtnsReverse.each(function(t){s({targets:t,opacity:1,scale:[.4,1],translateY:[e.offsetY,0],translateX:[e.offsetX,0],duration:275,delay:i,easing:"easeInOutQuad"}),i+=40})}},{key:"_animateOutFAB",value:function(){var e=this;this.$floatingBtnsReverse.each(function(t){s.remove(t),s({targets:t,opacity:0,scale:.4,translateY:e.offsetY,translateX:e.offsetX,duration:175,easing:"easeOutQuad",complete:function(){e.$el.removeClass("active")}})})}},{key:"_animateInToolbar",value:function(){var t,e=this,i=window.innerWidth,n=window.innerHeight,s=this.el.getBoundingClientRect(),o=r('<div class="fab-backdrop"></div>'),a=this.$anchor.css("background-color");this.$anchor.append(o),this.offsetX=s.left-i/2+s.width/2,this.offsetY=n-s.bottom,t=i/o[0].clientWidth,this.btnBottom=s.bottom,this.btnLeft=s.left,this.btnWidth=s.width,this.$el.addClass("active"),this.$el.css({"text-align":"center",width:"100%",bottom:0,left:0,transform:"translateX("+this.offsetX+"px)",transition:"none"}),this.$anchor.css({transform:"translateY("+-this.offsetY+"px)",transition:"none"}),o.css({"background-color":a}),setTimeout(function(){e.$el.css({transform:"",transition:"transform .2s cubic-bezier(0.550, 0.085, 0.680, 0.530), background-color 0s linear .2s"}),e.$anchor.css({overflow:"visible",transform:"",transition:"transform .2s"}),setTimeout(function(){e.$el.css({overflow:"hidden","background-color":a}),o.css({transform:"scale("+t+")",transition:"transform .2s cubic-bezier(0.550, 0.055, 0.675, 0.190)"}),e.$menu.children("li").children("a").css({opacity:1}),e._handleDocumentClickBound=e._handleDocumentClick.bind(e),window.addEventListener("scroll",e._handleCloseBound,!0),document.body.addEventListener("click",e._handleDocumentClickBound,!0)},100)},0)}},{key:"_animateOutToolbar",value:function(){var t=this,e=window.innerWidth,i=window.innerHeight,n=this.$el.find(".fab-backdrop"),s=this.$anchor.css("background-color");this.offsetX=this.btnLeft-e/2+this.btnWidth/2,this.offsetY=i-this.btnBottom,this.$el.removeClass("active"),this.$el.css({"background-color":"transparent",transition:"none"}),this.$anchor.css({transition:"none"}),n.css({transform:"scale(0)","background-color":s}),this.$menu.children("li").children("a").css({opacity:""}),setTimeout(function(){n.remove(),t.$el.css({"text-align":"",width:"",bottom:"",left:"",overflow:"","background-color":"",transform:"translate3d("+-t.offsetX+"px,0,0)"}),t.$anchor.css({overflow:"",transform:"translate3d(0,"+t.offsetY+"px,0)"}),setTimeout(function(){t.$el.css({transform:"translate3d(0,0,0)",transition:"transform .2s"}),t.$anchor.css({transform:"translate3d(0,0,0)",transition:"transform .2s cubic-bezier(0.550, 0.055, 0.675, 0.190)"})},20)},200)}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_FloatingActionButton}},{key:"defaults",get:function(){return e}}]),n}();M.FloatingActionButton=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"floatingActionButton","M_FloatingActionButton")}(cash,M.anime),function(g){"use strict";var e={autoClose:!1,format:"mmm dd, yyyy",parse:null,defaultDate:null,setDefaultDate:!1,disableWeekends:!1,disableDayFn:null,firstDay:0,minDate:null,maxDate:null,yearRange:10,minYear:0,maxYear:9999,minMonth:void 0,maxMonth:void 0,startRange:null,endRange:null,isRTL:!1,showMonthAfterYear:!1,showDaysInNextAndPreviousMonths:!1,container:null,showClearBtn:!1,i18n:{cancel:"Cancel",clear:"Clear",done:"Ok",previousMonth:"‹",nextMonth:"›",months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],weekdays:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],weekdaysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],weekdaysAbbrev:["S","M","T","W","T","F","S"]},events:[],onSelect:null,onOpen:null,onClose:null,onDraw:null},t=function(t){function B(t,e){_classCallCheck(this,B);var i=_possibleConstructorReturn(this,(B.__proto__||Object.getPrototypeOf(B)).call(this,B,t,e));(i.el.M_Datepicker=i).options=g.extend({},B.defaults,e),e&&e.hasOwnProperty("i18n")&&"object"==typeof e.i18n&&(i.options.i18n=g.extend({},B.defaults.i18n,e.i18n)),i.options.minDate&&i.options.minDate.setHours(0,0,0,0),i.options.maxDate&&i.options.maxDate.setHours(0,0,0,0),i.id=M.guid(),i._setupVariables(),i._insertHTMLIntoDOM(),i._setupModal(),i._setupEventHandlers(),i.options.defaultDate||(i.options.defaultDate=new Date(Date.parse(i.el.value)));var n=i.options.defaultDate;return B._isDate(n)?i.options.setDefaultDate?(i.setDate(n,!0),i.setInputValue()):i.gotoDate(n):i.gotoDate(new Date),i.isOpen=!1,i}return _inherits(B,Component),_createClass(B,[{key:"destroy",value:function(){this._removeEventHandlers(),this.modal.destroy(),g(this.modalEl).remove(),this.destroySelects(),this.el.M_Datepicker=void 0}},{key:"destroySelects",value:function(){var t=this.calendarEl.querySelector(".orig-select-year");t&&M.FormSelect.getInstance(t).destroy();var e=this.calendarEl.querySelector(".orig-select-month");e&&M.FormSelect.getInstance(e).destroy()}},{key:"_insertHTMLIntoDOM",value:function(){this.options.showClearBtn&&(g(this.clearBtn).css({visibility:""}),this.clearBtn.innerHTML=this.options.i18n.clear),this.doneBtn.innerHTML=this.options.i18n.done,this.cancelBtn.innerHTML=this.options.i18n.cancel,this.options.container?this.$modalEl.appendTo(this.options.container):this.$modalEl.insertBefore(this.el)}},{key:"_setupModal",value:function(){var t=this;this.modalEl.id="modal-"+this.id,this.modal=M.Modal.init(this.modalEl,{onCloseEnd:function(){t.isOpen=!1}})}},{key:"toString",value:function(t){var e=this;return t=t||this.options.format,B._isDate(this.date)?t.split(/(d{1,4}|m{1,4}|y{4}|yy|!.)/g).map(function(t){return e.formats[t]?e.formats[t]():t}).join(""):""}},{key:"setDate",value:function(t,e){if(!t)return this.date=null,this._renderDateDisplay(),this.draw();if("string"==typeof t&&(t=new Date(Date.parse(t))),B._isDate(t)){var i=this.options.minDate,n=this.options.maxDate;B._isDate(i)&&t<i?t=i:B._isDate(n)&&n<t&&(t=n),this.date=new Date(t.getTime()),this._renderDateDisplay(),B._setToStartOfDay(this.date),this.gotoDate(this.date),e||"function"!=typeof this.options.onSelect||this.options.onSelect.call(this,this.date)}}},{key:"setInputValue",value:function(){this.el.value=this.toString(),this.$el.trigger("change",{firedBy:this})}},{key:"_renderDateDisplay",value:function(){var t=B._isDate(this.date)?this.date:new Date,e=this.options.i18n,i=e.weekdaysShort[t.getDay()],n=e.monthsShort[t.getMonth()],s=t.getDate();this.yearTextEl.innerHTML=t.getFullYear(),this.dateTextEl.innerHTML=i+", "+n+" "+s}},{key:"gotoDate",value:function(t){var e=!0;if(B._isDate(t)){if(this.calendars){var i=new Date(this.calendars[0].year,this.calendars[0].month,1),n=new Date(this.calendars[this.calendars.length-1].year,this.calendars[this.calendars.length-1].month,1),s=t.getTime();n.setMonth(n.getMonth()+1),n.setDate(n.getDate()-1),e=s<i.getTime()||n.getTime()<s}e&&(this.calendars=[{month:t.getMonth(),year:t.getFullYear()}]),this.adjustCalendars()}}},{key:"adjustCalendars",value:function(){this.calendars[0]=this.adjustCalendar(this.calendars[0]),this.draw()}},{key:"adjustCalendar",value:function(t){return t.month<0&&(t.year-=Math.ceil(Math.abs(t.month)/12),t.month+=12),11<t.month&&(t.year+=Math.floor(Math.abs(t.month)/12),t.month-=12),t}},{key:"nextMonth",value:function(){this.calendars[0].month++,this.adjustCalendars()}},{key:"prevMonth",value:function(){this.calendars[0].month--,this.adjustCalendars()}},{key:"render",value:function(t,e,i){var n=this.options,s=new Date,o=B._getDaysInMonth(t,e),a=new Date(t,e,1).getDay(),r=[],l=[];B._setToStartOfDay(s),0<n.firstDay&&(a-=n.firstDay)<0&&(a+=7);for(var h=0===e?11:e-1,d=11===e?0:e+1,u=0===e?t-1:t,c=11===e?t+1:t,p=B._getDaysInMonth(u,h),v=o+a,f=v;7<f;)f-=7;v+=7-f;for(var m=!1,g=0,_=0;g<v;g++){var y=new Date(t,e,g-a+1),k=!!B._isDate(this.date)&&B._compareDates(y,this.date),b=B._compareDates(y,s),w=-1!==n.events.indexOf(y.toDateString()),C=g<a||o+a<=g,E=g-a+1,M=e,O=t,x=n.startRange&&B._compareDates(n.startRange,y),L=n.endRange&&B._compareDates(n.endRange,y),T=n.startRange&&n.endRange&&n.startRange<y&&y<n.endRange;C&&(g<a?(E=p+E,M=h,O=u):(E-=o,M=d,O=c));var $={day:E,month:M,year:O,hasEvent:w,isSelected:k,isToday:b,isDisabled:n.minDate&&y<n.minDate||n.maxDate&&y>n.maxDate||n.disableWeekends&&B._isWeekend(y)||n.disableDayFn&&n.disableDayFn(y),isEmpty:C,isStartRange:x,isEndRange:L,isInRange:T,showDaysInNextAndPreviousMonths:n.showDaysInNextAndPreviousMonths};l.push(this.renderDay($)),7==++_&&(r.push(this.renderRow(l,n.isRTL,m)),_=0,m=!(l=[]))}return this.renderTable(n,r,i)}},{key:"renderDay",value:function(t){var e=[],i="false";if(t.isEmpty){if(!t.showDaysInNextAndPreviousMonths)return'<td class="is-empty"></td>';e.push("is-outside-current-month"),e.push("is-selection-disabled")}return t.isDisabled&&e.push("is-disabled"),t.isToday&&e.push("is-today"),t.isSelected&&(e.push("is-selected"),i="true"),t.hasEvent&&e.push("has-event"),t.isInRange&&e.push("is-inrange"),t.isStartRange&&e.push("is-startrange"),t.isEndRange&&e.push("is-endrange"),'<td data-day="'+t.day+'" class="'+e.join(" ")+'" aria-selected="'+i+'"><button class="datepicker-day-button" type="button" data-year="'+t.year+'" data-month="'+t.month+'" data-day="'+t.day+'">'+t.day+"</button></td>"}},{key:"renderRow",value:function(t,e,i){return'<tr class="datepicker-row'+(i?" is-selected":"")+'">'+(e?t.reverse():t).join("")+"</tr>"}},{key:"renderTable",value:function(t,e,i){return'<div class="datepicker-table-wrapper"><table cellpadding="0" cellspacing="0" class="datepicker-table" role="grid" aria-labelledby="'+i+'">'+this.renderHead(t)+this.renderBody(e)+"</table></div>"}},{key:"renderHead",value:function(t){var e=void 0,i=[];for(e=0;e<7;e++)i.push('<th scope="col"><abbr title="'+this.renderDayName(t,e)+'">'+this.renderDayName(t,e,!0)+"</abbr></th>");return"<thead><tr>"+(t.isRTL?i.reverse():i).join("")+"</tr></thead>"}},{key:"renderBody",value:function(t){return"<tbody>"+t.join("")+"</tbody>"}},{key:"renderTitle",value:function(t,e,i,n,s,o){var a,r,l=void 0,h=void 0,d=void 0,u=this.options,c=i===u.minYear,p=i===u.maxYear,v='<div id="'+o+'" class="datepicker-controls" role="heading" aria-live="assertive">',f=!0,m=!0;for(d=[],l=0;l<12;l++)d.push('<option value="'+(i===s?l-e:12+l-e)+'"'+(l===n?' selected="selected"':"")+(c&&l<u.minMonth||p&&l>u.maxMonth?'disabled="disabled"':"")+">"+u.i18n.months[l]+"</option>");for(a='<select class="datepicker-select orig-select-month" tabindex="-1">'+d.join("")+"</select>",g.isArray(u.yearRange)?(l=u.yearRange[0],h=u.yearRange[1]+1):(l=i-u.yearRange,h=1+i+u.yearRange),d=[];l<h&&l<=u.maxYear;l++)l>=u.minYear&&d.push('<option value="'+l+'" '+(l===i?'selected="selected"':"")+">"+l+"</option>");r='<select class="datepicker-select orig-select-year" tabindex="-1">'+d.join("")+"</select>";v+='<button class="month-prev'+(f?"":" is-disabled")+'" type="button"><svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z"/><path d="M0-.5h24v24H0z" fill="none"/></svg></button>',v+='<div class="selects-container">',u.showMonthAfterYear?v+=r+a:v+=a+r,v+="</div>",c&&(0===n||u.minMonth>=n)&&(f=!1),p&&(11===n||u.maxMonth<=n)&&(m=!1);return(v+='<button class="month-next'+(m?"":" is-disabled")+'" type="button"><svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"/><path d="M0-.25h24v24H0z" fill="none"/></svg></button>')+"</div>"}},{key:"draw",value:function(t){if(this.isOpen||t){var e,i=this.options,n=i.minYear,s=i.maxYear,o=i.minMonth,a=i.maxMonth,r="";this._y<=n&&(this._y=n,!isNaN(o)&&this._m<o&&(this._m=o)),this._y>=s&&(this._y=s,!isNaN(a)&&this._m>a&&(this._m=a)),e="datepicker-title-"+Math.random().toString(36).replace(/[^a-z]+/g,"").substr(0,2);for(var l=0;l<1;l++)this._renderDateDisplay(),r+=this.renderTitle(this,l,this.calendars[l].year,this.calendars[l].month,this.calendars[0].year,e)+this.render(this.calendars[l].year,this.calendars[l].month,e);this.destroySelects(),this.calendarEl.innerHTML=r;var h=this.calendarEl.querySelector(".orig-select-year"),d=this.calendarEl.querySelector(".orig-select-month");M.FormSelect.init(h,{classes:"select-year",dropdownOptions:{container:document.body,constrainWidth:!1}}),M.FormSelect.init(d,{classes:"select-month",dropdownOptions:{container:document.body,constrainWidth:!1}}),h.addEventListener("change",this._handleYearChange.bind(this)),d.addEventListener("change",this._handleMonthChange.bind(this)),"function"==typeof this.options.onDraw&&this.options.onDraw(this)}}},{key:"_setupEventHandlers",value:function(){this._handleInputKeydownBound=this._handleInputKeydown.bind(this),this._handleInputClickBound=this._handleInputClick.bind(this),this._handleInputChangeBound=this._handleInputChange.bind(this),this._handleCalendarClickBound=this._handleCalendarClick.bind(this),this._finishSelectionBound=this._finishSelection.bind(this),this._handleMonthChange=this._handleMonthChange.bind(this),this._closeBound=this.close.bind(this),this.el.addEventListener("click",this._handleInputClickBound),this.el.addEventListener("keydown",this._handleInputKeydownBound),this.el.addEventListener("change",this._handleInputChangeBound),this.calendarEl.addEventListener("click",this._handleCalendarClickBound),this.doneBtn.addEventListener("click",this._finishSelectionBound),this.cancelBtn.addEventListener("click",this._closeBound),this.options.showClearBtn&&(this._handleClearClickBound=this._handleClearClick.bind(this),this.clearBtn.addEventListener("click",this._handleClearClickBound))}},{key:"_setupVariables",value:function(){var e=this;this.$modalEl=g(B._template),this.modalEl=this.$modalEl[0],this.calendarEl=this.modalEl.querySelector(".datepicker-calendar"),this.yearTextEl=this.modalEl.querySelector(".year-text"),this.dateTextEl=this.modalEl.querySelector(".date-text"),this.options.showClearBtn&&(this.clearBtn=this.modalEl.querySelector(".datepicker-clear")),this.doneBtn=this.modalEl.querySelector(".datepicker-done"),this.cancelBtn=this.modalEl.querySelector(".datepicker-cancel"),this.formats={d:function(){return e.date.getDate()},dd:function(){var t=e.date.getDate();return(t<10?"0":"")+t},ddd:function(){return e.options.i18n.weekdaysShort[e.date.getDay()]},dddd:function(){return e.options.i18n.weekdays[e.date.getDay()]},m:function(){return e.date.getMonth()+1},mm:function(){var t=e.date.getMonth()+1;return(t<10?"0":"")+t},mmm:function(){return e.options.i18n.monthsShort[e.date.getMonth()]},mmmm:function(){return e.options.i18n.months[e.date.getMonth()]},yy:function(){return(""+e.date.getFullYear()).slice(2)},yyyy:function(){return e.date.getFullYear()}}}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleInputClickBound),this.el.removeEventListener("keydown",this._handleInputKeydownBound),this.el.removeEventListener("change",this._handleInputChangeBound),this.calendarEl.removeEventListener("click",this._handleCalendarClickBound)}},{key:"_handleInputClick",value:function(){this.open()}},{key:"_handleInputKeydown",value:function(t){t.which===M.keys.ENTER&&(t.preventDefault(),this.open())}},{key:"_handleCalendarClick",value:function(t){if(this.isOpen){var e=g(t.target);e.hasClass("is-disabled")||(!e.hasClass("datepicker-day-button")||e.hasClass("is-empty")||e.parent().hasClass("is-disabled")?e.closest(".month-prev").length?this.prevMonth():e.closest(".month-next").length&&this.nextMonth():(this.setDate(new Date(t.target.getAttribute("data-year"),t.target.getAttribute("data-month"),t.target.getAttribute("data-day"))),this.options.autoClose&&this._finishSelection()))}}},{key:"_handleClearClick",value:function(){this.date=null,this.setInputValue(),this.close()}},{key:"_handleMonthChange",value:function(t){this.gotoMonth(t.target.value)}},{key:"_handleYearChange",value:function(t){this.gotoYear(t.target.value)}},{key:"gotoMonth",value:function(t){isNaN(t)||(this.calendars[0].month=parseInt(t,10),this.adjustCalendars())}},{key:"gotoYear",value:function(t){isNaN(t)||(this.calendars[0].year=parseInt(t,10),this.adjustCalendars())}},{key:"_handleInputChange",value:function(t){var e=void 0;t.firedBy!==this&&(e=this.options.parse?this.options.parse(this.el.value,this.options.format):new Date(Date.parse(this.el.value)),B._isDate(e)&&this.setDate(e))}},{key:"renderDayName",value:function(t,e,i){for(e+=t.firstDay;7<=e;)e-=7;return i?t.i18n.weekdaysAbbrev[e]:t.i18n.weekdays[e]}},{key:"_finishSelection",value:function(){this.setInputValue(),this.close()}},{key:"open",value:function(){if(!this.isOpen)return this.isOpen=!0,"function"==typeof this.options.onOpen&&this.options.onOpen.call(this),this.draw(),this.modal.open(),this}},{key:"close",value:function(){if(this.isOpen)return this.isOpen=!1,"function"==typeof this.options.onClose&&this.options.onClose.call(this),this.modal.close(),this}}],[{key:"init",value:function(t,e){return _get(B.__proto__||Object.getPrototypeOf(B),"init",this).call(this,this,t,e)}},{key:"_isDate",value:function(t){return/Date/.test(Object.prototype.toString.call(t))&&!isNaN(t.getTime())}},{key:"_isWeekend",value:function(t){var e=t.getDay();return 0===e||6===e}},{key:"_setToStartOfDay",value:function(t){B._isDate(t)&&t.setHours(0,0,0,0)}},{key:"_getDaysInMonth",value:function(t,e){return[31,B._isLeapYear(t)?29:28,31,30,31,30,31,31,30,31,30,31][e]}},{key:"_isLeapYear",value:function(t){return t%4==0&&t%100!=0||t%400==0}},{key:"_compareDates",value:function(t,e){return t.getTime()===e.getTime()}},{key:"_setToStartOfDay",value:function(t){B._isDate(t)&&t.setHours(0,0,0,0)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Datepicker}},{key:"defaults",get:function(){return e}}]),B}();t._template=['<div class= "modal datepicker-modal">','<div class="modal-content datepicker-container">','<div class="datepicker-date-display">','<span class="year-text"></span>','<span class="date-text"></span>',"</div>",'<div class="datepicker-calendar-container">','<div class="datepicker-calendar"></div>','<div class="datepicker-footer">','<button class="btn-flat datepicker-clear waves-effect" style="visibility: hidden;" type="button"></button>','<div class="confirmation-btns">','<button class="btn-flat datepicker-cancel waves-effect" type="button"></button>','<button class="btn-flat datepicker-done waves-effect" type="button"></button>',"</div>","</div>","</div>","</div>","</div>"].join(""),M.Datepicker=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"datepicker","M_Datepicker")}(cash),function(h){"use strict";var e={dialRadius:135,outerRadius:105,innerRadius:70,tickRadius:20,duration:350,container:null,defaultTime:"now",fromNow:0,showClearBtn:!1,i18n:{cancel:"Cancel",clear:"Clear",done:"Ok"},autoClose:!1,twelveHour:!0,vibrate:!0,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,onSelect:null},t=function(t){function f(t,e){_classCallCheck(this,f);var i=_possibleConstructorReturn(this,(f.__proto__||Object.getPrototypeOf(f)).call(this,f,t,e));return(i.el.M_Timepicker=i).options=h.extend({},f.defaults,e),i.id=M.guid(),i._insertHTMLIntoDOM(),i._setupModal(),i._setupVariables(),i._setupEventHandlers(),i._clockSetup(),i._pickerSetup(),i}return _inherits(f,Component),_createClass(f,[{key:"destroy",value:function(){this._removeEventHandlers(),this.modal.destroy(),h(this.modalEl).remove(),this.el.M_Timepicker=void 0}},{key:"_setupEventHandlers",value:function(){this._handleInputKeydownBound=this._handleInputKeydown.bind(this),this._handleInputClickBound=this._handleInputClick.bind(this),this._handleClockClickStartBound=this._handleClockClickStart.bind(this),this._handleDocumentClickMoveBound=this._handleDocumentClickMove.bind(this),this._handleDocumentClickEndBound=this._handleDocumentClickEnd.bind(this),this.el.addEventListener("click",this._handleInputClickBound),this.el.addEventListener("keydown",this._handleInputKeydownBound),this.plate.addEventListener("mousedown",this._handleClockClickStartBound),this.plate.addEventListener("touchstart",this._handleClockClickStartBound),h(this.spanHours).on("click",this.showView.bind(this,"hours")),h(this.spanMinutes).on("click",this.showView.bind(this,"minutes"))}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleInputClickBound),this.el.removeEventListener("keydown",this._handleInputKeydownBound)}},{key:"_handleInputClick",value:function(){this.open()}},{key:"_handleInputKeydown",value:function(t){t.which===M.keys.ENTER&&(t.preventDefault(),this.open())}},{key:"_handleClockClickStart",value:function(t){t.preventDefault();var e=this.plate.getBoundingClientRect(),i=e.left,n=e.top;this.x0=i+this.options.dialRadius,this.y0=n+this.options.dialRadius,this.moved=!1;var s=f._Pos(t);this.dx=s.x-this.x0,this.dy=s.y-this.y0,this.setHand(this.dx,this.dy,!1),document.addEventListener("mousemove",this._handleDocumentClickMoveBound),document.addEventListener("touchmove",this._handleDocumentClickMoveBound),document.addEventListener("mouseup",this._handleDocumentClickEndBound),document.addEventListener("touchend",this._handleDocumentClickEndBound)}},{key:"_handleDocumentClickMove",value:function(t){t.preventDefault();var e=f._Pos(t),i=e.x-this.x0,n=e.y-this.y0;this.moved=!0,this.setHand(i,n,!1,!0)}},{key:"_handleDocumentClickEnd",value:function(t){var e=this;t.preventDefault(),document.removeEventListener("mouseup",this._handleDocumentClickEndBound),document.removeEventListener("touchend",this._handleDocumentClickEndBound);var i=f._Pos(t),n=i.x-this.x0,s=i.y-this.y0;this.moved&&n===this.dx&&s===this.dy&&this.setHand(n,s),"hours"===this.currentView?this.showView("minutes",this.options.duration/2):this.options.autoClose&&(h(this.minutesView).addClass("timepicker-dial-out"),setTimeout(function(){e.done()},this.options.duration/2)),"function"==typeof this.options.onSelect&&this.options.onSelect.call(this,this.hours,this.minutes),document.removeEventListener("mousemove",this._handleDocumentClickMoveBound),document.removeEventListener("touchmove",this._handleDocumentClickMoveBound)}},{key:"_insertHTMLIntoDOM",value:function(){this.$modalEl=h(f._template),this.modalEl=this.$modalEl[0],this.modalEl.id="modal-"+this.id;var t=document.querySelector(this.options.container);this.options.container&&t?this.$modalEl.appendTo(t):this.$modalEl.insertBefore(this.el)}},{key:"_setupModal",value:function(){var t=this;this.modal=M.Modal.init(this.modalEl,{onOpenStart:this.options.onOpenStart,onOpenEnd:this.options.onOpenEnd,onCloseStart:this.options.onCloseStart,onCloseEnd:function(){"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t),t.isOpen=!1}})}},{key:"_setupVariables",value:function(){this.currentView="hours",this.vibrate=navigator.vibrate?"vibrate":navigator.webkitVibrate?"webkitVibrate":null,this._canvas=this.modalEl.querySelector(".timepicker-canvas"),this.plate=this.modalEl.querySelector(".timepicker-plate"),this.hoursView=this.modalEl.querySelector(".timepicker-hours"),this.minutesView=this.modalEl.querySelector(".timepicker-minutes"),this.spanHours=this.modalEl.querySelector(".timepicker-span-hours"),this.spanMinutes=this.modalEl.querySelector(".timepicker-span-minutes"),this.spanAmPm=this.modalEl.querySelector(".timepicker-span-am-pm"),this.footer=this.modalEl.querySelector(".timepicker-footer"),this.amOrPm="PM"}},{key:"_pickerSetup",value:function(){var t=h('<button class="btn-flat timepicker-clear waves-effect" style="visibility: hidden;" type="button" tabindex="'+(this.options.twelveHour?"3":"1")+'">'+this.options.i18n.clear+"</button>").appendTo(this.footer).on("click",this.clear.bind(this));this.options.showClearBtn&&t.css({visibility:""});var e=h('<div class="confirmation-btns"></div>');h('<button class="btn-flat timepicker-close waves-effect" type="button" tabindex="'+(this.options.twelveHour?"3":"1")+'">'+this.options.i18n.cancel+"</button>").appendTo(e).on("click",this.close.bind(this)),h('<button class="btn-flat timepicker-close waves-effect" type="button" tabindex="'+(this.options.twelveHour?"3":"1")+'">'+this.options.i18n.done+"</button>").appendTo(e).on("click",this.done.bind(this)),e.appendTo(this.footer)}},{key:"_clockSetup",value:function(){this.options.twelveHour&&(this.$amBtn=h('<div class="am-btn">AM</div>'),this.$pmBtn=h('<div class="pm-btn">PM</div>'),this.$amBtn.on("click",this._handleAmPmClick.bind(this)).appendTo(this.spanAmPm),this.$pmBtn.on("click",this._handleAmPmClick.bind(this)).appendTo(this.spanAmPm)),this._buildHoursView(),this._buildMinutesView(),this._buildSVGClock()}},{key:"_buildSVGClock",value:function(){var t=this.options.dialRadius,e=this.options.tickRadius,i=2*t,n=f._createSVGEl("svg");n.setAttribute("class","timepicker-svg"),n.setAttribute("width",i),n.setAttribute("height",i);var s=f._createSVGEl("g");s.setAttribute("transform","translate("+t+","+t+")");var o=f._createSVGEl("circle");o.setAttribute("class","timepicker-canvas-bearing"),o.setAttribute("cx",0),o.setAttribute("cy",0),o.setAttribute("r",4);var a=f._createSVGEl("line");a.setAttribute("x1",0),a.setAttribute("y1",0);var r=f._createSVGEl("circle");r.setAttribute("class","timepicker-canvas-bg"),r.setAttribute("r",e),s.appendChild(a),s.appendChild(r),s.appendChild(o),n.appendChild(s),this._canvas.appendChild(n),this.hand=a,this.bg=r,this.bearing=o,this.g=s}},{key:"_buildHoursView",value:function(){var t=h('<div class="timepicker-tick"></div>');if(this.options.twelveHour)for(var e=1;e<13;e+=1){var i=t.clone(),n=e/6*Math.PI,s=this.options.outerRadius;i.css({left:this.options.dialRadius+Math.sin(n)*s-this.options.tickRadius+"px",top:this.options.dialRadius-Math.cos(n)*s-this.options.tickRadius+"px"}),i.html(0===e?"00":e),this.hoursView.appendChild(i[0])}else for(var o=0;o<24;o+=1){var a=t.clone(),r=o/6*Math.PI,l=0<o&&o<13?this.options.innerRadius:this.options.outerRadius;a.css({left:this.options.dialRadius+Math.sin(r)*l-this.options.tickRadius+"px",top:this.options.dialRadius-Math.cos(r)*l-this.options.tickRadius+"px"}),a.html(0===o?"00":o),this.hoursView.appendChild(a[0])}}},{key:"_buildMinutesView",value:function(){for(var t=h('<div class="timepicker-tick"></div>'),e=0;e<60;e+=5){var i=t.clone(),n=e/30*Math.PI;i.css({left:this.options.dialRadius+Math.sin(n)*this.options.outerRadius-this.options.tickRadius+"px",top:this.options.dialRadius-Math.cos(n)*this.options.outerRadius-this.options.tickRadius+"px"}),i.html(f._addLeadingZero(e)),this.minutesView.appendChild(i[0])}}},{key:"_handleAmPmClick",value:function(t){var e=h(t.target);this.amOrPm=e.hasClass("am-btn")?"AM":"PM",this._updateAmPmView()}},{key:"_updateAmPmView",value:function(){this.options.twelveHour&&(this.$amBtn.toggleClass("text-primary","AM"===this.amOrPm),this.$pmBtn.toggleClass("text-primary","PM"===this.amOrPm))}},{key:"_updateTimeFromInput",value:function(){var t=((this.el.value||this.options.defaultTime||"")+"").split(":");if(this.options.twelveHour&&void 0!==t[1]&&(0<t[1].toUpperCase().indexOf("AM")?this.amOrPm="AM":this.amOrPm="PM",t[1]=t[1].replace("AM","").replace("PM","")),"now"===t[0]){var e=new Date(+new Date+this.options.fromNow);t=[e.getHours(),e.getMinutes()],this.options.twelveHour&&(this.amOrPm=12<=t[0]&&t[0]<24?"PM":"AM")}this.hours=+t[0]||0,this.minutes=+t[1]||0,this.spanHours.innerHTML=this.hours,this.spanMinutes.innerHTML=f._addLeadingZero(this.minutes),this._updateAmPmView()}},{key:"showView",value:function(t,e){"minutes"===t&&h(this.hoursView).css("visibility");var i="hours"===t,n=i?this.hoursView:this.minutesView,s=i?this.minutesView:this.hoursView;this.currentView=t,h(this.spanHours).toggleClass("text-primary",i),h(this.spanMinutes).toggleClass("text-primary",!i),s.classList.add("timepicker-dial-out"),h(n).css("visibility","visible").removeClass("timepicker-dial-out"),this.resetClock(e),clearTimeout(this.toggleViewTimer),this.toggleViewTimer=setTimeout(function(){h(s).css("visibility","hidden")},this.options.duration)}},{key:"resetClock",value:function(t){var e=this.currentView,i=this[e],n="hours"===e,s=i*(Math.PI/(n?6:30)),o=n&&0<i&&i<13?this.options.innerRadius:this.options.outerRadius,a=Math.sin(s)*o,r=-Math.cos(s)*o,l=this;t?(h(this.canvas).addClass("timepicker-canvas-out"),setTimeout(function(){h(l.canvas).removeClass("timepicker-canvas-out"),l.setHand(a,r)},t)):this.setHand(a,r)}},{key:"setHand",value:function(t,e,i){var n=this,s=Math.atan2(t,-e),o="hours"===this.currentView,a=Math.PI/(o||i?6:30),r=Math.sqrt(t*t+e*e),l=o&&r<(this.options.outerRadius+this.options.innerRadius)/2,h=l?this.options.innerRadius:this.options.outerRadius;this.options.twelveHour&&(h=this.options.outerRadius),s<0&&(s=2*Math.PI+s);var d=Math.round(s/a);s=d*a,this.options.twelveHour?o?0===d&&(d=12):(i&&(d*=5),60===d&&(d=0)):o?(12===d&&(d=0),d=l?0===d?12:d:0===d?0:d+12):(i&&(d*=5),60===d&&(d=0)),this[this.currentView]!==d&&this.vibrate&&this.options.vibrate&&(this.vibrateTimer||(navigator[this.vibrate](10),this.vibrateTimer=setTimeout(function(){n.vibrateTimer=null},100))),this[this.currentView]=d,o?this.spanHours.innerHTML=d:this.spanMinutes.innerHTML=f._addLeadingZero(d);var u=Math.sin(s)*(h-this.options.tickRadius),c=-Math.cos(s)*(h-this.options.tickRadius),p=Math.sin(s)*h,v=-Math.cos(s)*h;this.hand.setAttribute("x2",u),this.hand.setAttribute("y2",c),this.bg.setAttribute("cx",p),this.bg.setAttribute("cy",v)}},{key:"open",value:function(){this.isOpen||(this.isOpen=!0,this._updateTimeFromInput(),this.showView("hours"),this.modal.open())}},{key:"close",value:function(){this.isOpen&&(this.isOpen=!1,this.modal.close())}},{key:"done",value:function(t,e){var i=this.el.value,n=e?"":f._addLeadingZero(this.hours)+":"+f._addLeadingZero(this.minutes);this.time=n,!e&&this.options.twelveHour&&(n=n+" "+this.amOrPm),(this.el.value=n)!==i&&this.$el.trigger("change"),this.close(),this.el.focus()}},{key:"clear",value:function(){this.done(null,!0)}}],[{key:"init",value:function(t,e){return _get(f.__proto__||Object.getPrototypeOf(f),"init",this).call(this,this,t,e)}},{key:"_addLeadingZero",value:function(t){return(t<10?"0":"")+t}},{key:"_createSVGEl",value:function(t){return document.createElementNS("http://www.w3.org/2000/svg",t)}},{key:"_Pos",value:function(t){return t.targetTouches&&1<=t.targetTouches.length?{x:t.targetTouches[0].clientX,y:t.targetTouches[0].clientY}:{x:t.clientX,y:t.clientY}}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Timepicker}},{key:"defaults",get:function(){return e}}]),f}();t._template=['<div class= "modal timepicker-modal">','<div class="modal-content timepicker-container">','<div class="timepicker-digital-display">','<div class="timepicker-text-container">','<div class="timepicker-display-column">','<span class="timepicker-span-hours text-primary"></span>',":",'<span class="timepicker-span-minutes"></span>',"</div>",'<div class="timepicker-display-column timepicker-display-am-pm">','<div class="timepicker-span-am-pm"></div>',"</div>","</div>","</div>",'<div class="timepicker-analog-display">','<div class="timepicker-plate">','<div class="timepicker-canvas"></div>','<div class="timepicker-dial timepicker-hours"></div>','<div class="timepicker-dial timepicker-minutes timepicker-dial-out"></div>',"</div>",'<div class="timepicker-footer"></div>',"</div>","</div>","</div>"].join(""),M.Timepicker=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"timepicker","M_Timepicker")}(cash),function(s){"use strict";var e={},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_CharacterCounter=i).options=s.extend({},n.defaults,e),i.isInvalid=!1,i.isValidLength=!1,i._setupCounter(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.CharacterCounter=void 0,this._removeCounter()}},{key:"_setupEventHandlers",value:function(){this._handleUpdateCounterBound=this.updateCounter.bind(this),this.el.addEventListener("focus",this._handleUpdateCounterBound,!0),this.el.addEventListener("input",this._handleUpdateCounterBound,!0)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("focus",this._handleUpdateCounterBound,!0),this.el.removeEventListener("input",this._handleUpdateCounterBound,!0)}},{key:"_setupCounter",value:function(){this.counterEl=document.createElement("span"),s(this.counterEl).addClass("character-counter").css({float:"right","font-size":"12px",height:1}),this.$el.parent().append(this.counterEl)}},{key:"_removeCounter",value:function(){s(this.counterEl).remove()}},{key:"updateCounter",value:function(){var t=+this.$el.attr("data-length"),e=this.el.value.length;this.isValidLength=e<=t;var i=e;t&&(i+="/"+t,this._validateInput()),s(this.counterEl).html(i)}},{key:"_validateInput",value:function(){this.isValidLength&&this.isInvalid?(this.isInvalid=!1,this.$el.removeClass("invalid")):this.isValidLength||this.isInvalid||(this.isInvalid=!0,this.$el.removeClass("valid"),this.$el.addClass("invalid"))}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_CharacterCounter}},{key:"defaults",get:function(){return e}}]),n}();M.CharacterCounter=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"characterCounter","M_CharacterCounter")}(cash),function(b){"use strict";var e={duration:200,dist:-100,shift:0,padding:0,numVisible:5,fullWidth:!1,indicators:!1,noWrap:!1,onCycleTo:null},t=function(t){function i(t,e){_classCallCheck(this,i);var n=_possibleConstructorReturn(this,(i.__proto__||Object.getPrototypeOf(i)).call(this,i,t,e));return(n.el.M_Carousel=n).options=b.extend({},i.defaults,e),n.hasMultipleSlides=1<n.$el.find(".carousel-item").length,n.showIndicators=n.options.indicators&&n.hasMultipleSlides,n.noWrap=n.options.noWrap||!n.hasMultipleSlides,n.pressed=!1,n.dragged=!1,n.offset=n.target=0,n.images=[],n.itemWidth=n.$el.find(".carousel-item").first().innerWidth(),n.itemHeight=n.$el.find(".carousel-item").first().innerHeight(),n.dim=2*n.itemWidth+n.options.padding||1,n._autoScrollBound=n._autoScroll.bind(n),n._trackBound=n._track.bind(n),n.options.fullWidth&&(n.options.dist=0,n._setCarouselHeight(),n.showIndicators&&n.$el.find(".carousel-fixed-item").addClass("with-indicators")),n.$indicators=b('<ul class="indicators"></ul>'),n.$el.find(".carousel-item").each(function(t,e){if(n.images.push(t),n.showIndicators){var i=b('<li class="indicator-item"></li>');0===e&&i[0].classList.add("active"),n.$indicators.append(i)}}),n.showIndicators&&n.$el.append(n.$indicators),n.count=n.images.length,n.options.numVisible=Math.min(n.count,n.options.numVisible),n.xform="transform",["webkit","Moz","O","ms"].every(function(t){var e=t+"Transform";return void 0===document.body.style[e]||(n.xform=e,!1)}),n._setupEventHandlers(),n._scroll(n.offset),n}return _inherits(i,Component),_createClass(i,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Carousel=void 0}},{key:"_setupEventHandlers",value:function(){var i=this;this._handleCarouselTapBound=this._handleCarouselTap.bind(this),this._handleCarouselDragBound=this._handleCarouselDrag.bind(this),this._handleCarouselReleaseBound=this._handleCarouselRelease.bind(this),this._handleCarouselClickBound=this._handleCarouselClick.bind(this),void 0!==window.ontouchstart&&(this.el.addEventListener("touchstart",this._handleCarouselTapBound),this.el.addEventListener("touchmove",this._handleCarouselDragBound),this.el.addEventListener("touchend",this._handleCarouselReleaseBound)),this.el.addEventListener("mousedown",this._handleCarouselTapBound),this.el.addEventListener("mousemove",this._handleCarouselDragBound),this.el.addEventListener("mouseup",this._handleCarouselReleaseBound),this.el.addEventListener("mouseleave",this._handleCarouselReleaseBound),this.el.addEventListener("click",this._handleCarouselClickBound),this.showIndicators&&this.$indicators&&(this._handleIndicatorClickBound=this._handleIndicatorClick.bind(this),this.$indicators.find(".indicator-item").each(function(t,e){t.addEventListener("click",i._handleIndicatorClickBound)}));var t=M.throttle(this._handleResize,200);this._handleThrottledResizeBound=t.bind(this),window.addEventListener("resize",this._handleThrottledResizeBound)}},{key:"_removeEventHandlers",value:function(){var i=this;void 0!==window.ontouchstart&&(this.el.removeEventListener("touchstart",this._handleCarouselTapBound),this.el.removeEventListener("touchmove",this._handleCarouselDragBound),this.el.removeEventListener("touchend",this._handleCarouselReleaseBound)),this.el.removeEventListener("mousedown",this._handleCarouselTapBound),this.el.removeEventListener("mousemove",this._handleCarouselDragBound),this.el.removeEventListener("mouseup",this._handleCarouselReleaseBound),this.el.removeEventListener("mouseleave",this._handleCarouselReleaseBound),this.el.removeEventListener("click",this._handleCarouselClickBound),this.showIndicators&&this.$indicators&&this.$indicators.find(".indicator-item").each(function(t,e){t.removeEventListener("click",i._handleIndicatorClickBound)}),window.removeEventListener("resize",this._handleThrottledResizeBound)}},{key:"_handleCarouselTap",value:function(t){"mousedown"===t.type&&b(t.target).is("img")&&t.preventDefault(),this.pressed=!0,this.dragged=!1,this.verticalDragged=!1,this.reference=this._xpos(t),this.referenceY=this._ypos(t),this.velocity=this.amplitude=0,this.frame=this.offset,this.timestamp=Date.now(),clearInterval(this.ticker),this.ticker=setInterval(this._trackBound,100)}},{key:"_handleCarouselDrag",value:function(t){var e=void 0,i=void 0,n=void 0;if(this.pressed)if(e=this._xpos(t),i=this._ypos(t),n=this.reference-e,Math.abs(this.referenceY-i)<30&&!this.verticalDragged)(2<n||n<-2)&&(this.dragged=!0,this.reference=e,this._scroll(this.offset+n));else{if(this.dragged)return t.preventDefault(),t.stopPropagation(),!1;this.verticalDragged=!0}if(this.dragged)return t.preventDefault(),t.stopPropagation(),!1}},{key:"_handleCarouselRelease",value:function(t){if(this.pressed)return this.pressed=!1,clearInterval(this.ticker),this.target=this.offset,(10<this.velocity||this.velocity<-10)&&(this.amplitude=.9*this.velocity,this.target=this.offset+this.amplitude),this.target=Math.round(this.target/this.dim)*this.dim,this.noWrap&&(this.target>=this.dim*(this.count-1)?this.target=this.dim*(this.count-1):this.target<0&&(this.target=0)),this.amplitude=this.target-this.offset,this.timestamp=Date.now(),requestAnimationFrame(this._autoScrollBound),this.dragged&&(t.preventDefault(),t.stopPropagation()),!1}},{key:"_handleCarouselClick",value:function(t){if(this.dragged)return t.preventDefault(),t.stopPropagation(),!1;if(!this.options.fullWidth){var e=b(t.target).closest(".carousel-item").index();0!==this._wrap(this.center)-e&&(t.preventDefault(),t.stopPropagation()),this._cycleTo(e)}}},{key:"_handleIndicatorClick",value:function(t){t.stopPropagation();var e=b(t.target).closest(".indicator-item");e.length&&this._cycleTo(e.index())}},{key:"_handleResize",value:function(t){this.options.fullWidth?(this.itemWidth=this.$el.find(".carousel-item").first().innerWidth(),this.imageHeight=this.$el.find(".carousel-item.active").height(),this.dim=2*this.itemWidth+this.options.padding,this.offset=2*this.center*this.itemWidth,this.target=this.offset,this._setCarouselHeight(!0)):this._scroll()}},{key:"_setCarouselHeight",value:function(t){var i=this,e=this.$el.find(".carousel-item.active").length?this.$el.find(".carousel-item.active").first():this.$el.find(".carousel-item").first(),n=e.find("img").first();if(n.length)if(n[0].complete){var s=n.height();if(0<s)this.$el.css("height",s+"px");else{var o=n[0].naturalWidth,a=n[0].naturalHeight,r=this.$el.width()/o*a;this.$el.css("height",r+"px")}}else n.one("load",function(t,e){i.$el.css("height",t.offsetHeight+"px")});else if(!t){var l=e.height();this.$el.css("height",l+"px")}}},{key:"_xpos",value:function(t){return t.targetTouches&&1<=t.targetTouches.length?t.targetTouches[0].clientX:t.clientX}},{key:"_ypos",value:function(t){return t.targetTouches&&1<=t.targetTouches.length?t.targetTouches[0].clientY:t.clientY}},{key:"_wrap",value:function(t){return t>=this.count?t%this.count:t<0?this._wrap(this.count+t%this.count):t}},{key:"_track",value:function(){var t,e,i,n;e=(t=Date.now())-this.timestamp,this.timestamp=t,i=this.offset-this.frame,this.frame=this.offset,n=1e3*i/(1+e),this.velocity=.8*n+.2*this.velocity}},{key:"_autoScroll",value:function(){var t=void 0,e=void 0;this.amplitude&&(t=Date.now()-this.timestamp,2<(e=this.amplitude*Math.exp(-t/this.options.duration))||e<-2?(this._scroll(this.target-e),requestAnimationFrame(this._autoScrollBound)):this._scroll(this.target))}},{key:"_scroll",value:function(t){var e=this;this.$el.hasClass("scrolling")||this.el.classList.add("scrolling"),null!=this.scrollingTimeout&&window.clearTimeout(this.scrollingTimeout),this.scrollingTimeout=window.setTimeout(function(){e.$el.removeClass("scrolling")},this.options.duration);var i,n,s,o,a=void 0,r=void 0,l=void 0,h=void 0,d=void 0,u=void 0,c=this.center,p=1/this.options.numVisible;if(this.offset="number"==typeof t?t:this.offset,this.center=Math.floor((this.offset+this.dim/2)/this.dim),o=-(s=(n=this.offset-this.center*this.dim)<0?1:-1)*n*2/this.dim,i=this.count>>1,this.options.fullWidth?(l="translateX(0)",u=1):(l="translateX("+(this.el.clientWidth-this.itemWidth)/2+"px) ",l+="translateY("+(this.el.clientHeight-this.itemHeight)/2+"px)",u=1-p*o),this.showIndicators){var v=this.center%this.count,f=this.$indicators.find(".indicator-item.active");f.index()!==v&&(f.removeClass("active"),this.$indicators.find(".indicator-item").eq(v)[0].classList.add("active"))}if(!this.noWrap||0<=this.center&&this.center<this.count){r=this.images[this._wrap(this.center)],b(r).hasClass("active")||(this.$el.find(".carousel-item").removeClass("active"),r.classList.add("active"));var m=l+" translateX("+-n/2+"px) translateX("+s*this.options.shift*o*a+"px) translateZ("+this.options.dist*o+"px)";this._updateItemStyle(r,u,0,m)}for(a=1;a<=i;++a){if(this.options.fullWidth?(h=this.options.dist,d=a===i&&n<0?1-o:1):(h=this.options.dist*(2*a+o*s),d=1-p*(2*a+o*s)),!this.noWrap||this.center+a<this.count){r=this.images[this._wrap(this.center+a)];var g=l+" translateX("+(this.options.shift+(this.dim*a-n)/2)+"px) translateZ("+h+"px)";this._updateItemStyle(r,d,-a,g)}if(this.options.fullWidth?(h=this.options.dist,d=a===i&&0<n?1-o:1):(h=this.options.dist*(2*a-o*s),d=1-p*(2*a-o*s)),!this.noWrap||0<=this.center-a){r=this.images[this._wrap(this.center-a)];var _=l+" translateX("+(-this.options.shift+(-this.dim*a-n)/2)+"px) translateZ("+h+"px)";this._updateItemStyle(r,d,-a,_)}}if(!this.noWrap||0<=this.center&&this.center<this.count){r=this.images[this._wrap(this.center)];var y=l+" translateX("+-n/2+"px) translateX("+s*this.options.shift*o+"px) translateZ("+this.options.dist*o+"px)";this._updateItemStyle(r,u,0,y)}var k=this.$el.find(".carousel-item").eq(this._wrap(this.center));c!==this.center&&"function"==typeof this.options.onCycleTo&&this.options.onCycleTo.call(this,k[0],this.dragged),"function"==typeof this.oneTimeCallback&&(this.oneTimeCallback.call(this,k[0],this.dragged),this.oneTimeCallback=null)}},{key:"_updateItemStyle",value:function(t,e,i,n){t.style[this.xform]=n,t.style.zIndex=i,t.style.opacity=e,t.style.visibility="visible"}},{key:"_cycleTo",value:function(t,e){var i=this.center%this.count-t;this.noWrap||(i<0?Math.abs(i+this.count)<Math.abs(i)&&(i+=this.count):0<i&&Math.abs(i-this.count)<i&&(i-=this.count)),this.target=this.dim*Math.round(this.offset/this.dim),i<0?this.target+=this.dim*Math.abs(i):0<i&&(this.target-=this.dim*i),"function"==typeof e&&(this.oneTimeCallback=e),this.offset!==this.target&&(this.amplitude=this.target-this.offset,this.timestamp=Date.now(),requestAnimationFrame(this._autoScrollBound))}},{key:"next",value:function(t){(void 0===t||isNaN(t))&&(t=1);var e=this.center+t;if(e>=this.count||e<0){if(this.noWrap)return;e=this._wrap(e)}this._cycleTo(e)}},{key:"prev",value:function(t){(void 0===t||isNaN(t))&&(t=1);var e=this.center-t;if(e>=this.count||e<0){if(this.noWrap)return;e=this._wrap(e)}this._cycleTo(e)}},{key:"set",value:function(t,e){if((void 0===t||isNaN(t))&&(t=0),t>this.count||t<0){if(this.noWrap)return;t=this._wrap(t)}this._cycleTo(t,e)}}],[{key:"init",value:function(t,e){return _get(i.__proto__||Object.getPrototypeOf(i),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Carousel}},{key:"defaults",get:function(){return e}}]),i}();M.Carousel=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"carousel","M_Carousel")}(cash),function(S){"use strict";var e={onOpen:void 0,onClose:void 0},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_TapTarget=i).options=S.extend({},n.defaults,e),i.isOpen=!1,i.$origin=S("#"+i.$el.attr("data-target")),i._setup(),i._calculatePositioning(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.TapTarget=void 0}},{key:"_setupEventHandlers",value:function(){this._handleDocumentClickBound=this._handleDocumentClick.bind(this),this._handleTargetClickBound=this._handleTargetClick.bind(this),this._handleOriginClickBound=this._handleOriginClick.bind(this),this.el.addEventListener("click",this._handleTargetClickBound),this.originEl.addEventListener("click",this._handleOriginClickBound);var t=M.throttle(this._handleResize,200);this._handleThrottledResizeBound=t.bind(this),window.addEventListener("resize",this._handleThrottledResizeBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleTargetClickBound),this.originEl.removeEventListener("click",this._handleOriginClickBound),window.removeEventListener("resize",this._handleThrottledResizeBound)}},{key:"_handleTargetClick",value:function(t){this.open()}},{key:"_handleOriginClick",value:function(t){this.close()}},{key:"_handleResize",value:function(t){this._calculatePositioning()}},{key:"_handleDocumentClick",value:function(t){S(t.target).closest(".tap-target-wrapper").length||(this.close(),t.preventDefault(),t.stopPropagation())}},{key:"_setup",value:function(){this.wrapper=this.$el.parent()[0],this.waveEl=S(this.wrapper).find(".tap-target-wave")[0],this.originEl=S(this.wrapper).find(".tap-target-origin")[0],this.contentEl=this.$el.find(".tap-target-content")[0],S(this.wrapper).hasClass(".tap-target-wrapper")||(this.wrapper=document.createElement("div"),this.wrapper.classList.add("tap-target-wrapper"),this.$el.before(S(this.wrapper)),this.wrapper.append(this.el)),this.contentEl||(this.contentEl=document.createElement("div"),this.contentEl.classList.add("tap-target-content"),this.$el.append(this.contentEl)),this.waveEl||(this.waveEl=document.createElement("div"),this.waveEl.classList.add("tap-target-wave"),this.originEl||(this.originEl=this.$origin.clone(!0,!0),this.originEl.addClass("tap-target-origin"),this.originEl.removeAttr("id"),this.originEl.removeAttr("style"),this.originEl=this.originEl[0],this.waveEl.append(this.originEl)),this.wrapper.append(this.waveEl))}},{key:"_calculatePositioning",value:function(){var t="fixed"===this.$origin.css("position");if(!t)for(var e=this.$origin.parents(),i=0;i<e.length&&!(t="fixed"==S(e[i]).css("position"));i++);var n=this.$origin.outerWidth(),s=this.$origin.outerHeight(),o=t?this.$origin.offset().top-M.getDocumentScrollTop():this.$origin.offset().top,a=t?this.$origin.offset().left-M.getDocumentScrollLeft():this.$origin.offset().left,r=window.innerWidth,l=window.innerHeight,h=r/2,d=l/2,u=a<=h,c=h<a,p=o<=d,v=d<o,f=.25*r<=a&&a<=.75*r,m=this.$el.outerWidth(),g=this.$el.outerHeight(),_=o+s/2-g/2,y=a+n/2-m/2,k=t?"fixed":"absolute",b=f?m:m/2+n,w=g/2,C=p?g/2:0,E=u&&!f?m/2-n:0,O=n,x=v?"bottom":"top",L=2*n,T=L,$=g/2-T/2,B=m/2-L/2,D={};D.top=p?_+"px":"",D.right=c?r-y-m+"px":"",D.bottom=v?l-_-g+"px":"",D.left=u?y+"px":"",D.position=k,S(this.wrapper).css(D),S(this.contentEl).css({width:b+"px",height:w+"px",top:C+"px",right:"0px",bottom:"0px",left:E+"px",padding:O+"px",verticalAlign:x}),S(this.waveEl).css({top:$+"px",left:B+"px",width:L+"px",height:T+"px"})}},{key:"open",value:function(){this.isOpen||("function"==typeof this.options.onOpen&&this.options.onOpen.call(this,this.$origin[0]),this.isOpen=!0,this.wrapper.classList.add("open"),document.body.addEventListener("click",this._handleDocumentClickBound,!0),document.body.addEventListener("touchend",this._handleDocumentClickBound))}},{key:"close",value:function(){this.isOpen&&("function"==typeof this.options.onClose&&this.options.onClose.call(this,this.$origin[0]),this.isOpen=!1,this.wrapper.classList.remove("open"),document.body.removeEventListener("click",this._handleDocumentClickBound,!0),document.body.removeEventListener("touchend",this._handleDocumentClickBound))}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_TapTarget}},{key:"defaults",get:function(){return e}}]),n}();M.TapTarget=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"tapTarget","M_TapTarget")}(cash),function(d){"use strict";var e={classes:"",dropdownOptions:{}},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return i.$el.hasClass("browser-default")?_possibleConstructorReturn(i):((i.el.M_FormSelect=i).options=d.extend({},n.defaults,e),i.isMultiple=i.$el.prop("multiple"),i.el.tabIndex=-1,i._keysSelected={},i._valueDict={},i._setupDropdown(),i._setupEventHandlers(),i)}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this._removeDropdown(),this.el.M_FormSelect=void 0}},{key:"_setupEventHandlers",value:function(){var e=this;this._handleSelectChangeBound=this._handleSelectChange.bind(this),this._handleOptionClickBound=this._handleOptionClick.bind(this),this._handleInputClickBound=this._handleInputClick.bind(this),d(this.dropdownOptions).find("li:not(.optgroup)").each(function(t){t.addEventListener("click",e._handleOptionClickBound)}),this.el.addEventListener("change",this._handleSelectChangeBound),this.input.addEventListener("click",this._handleInputClickBound)}},{key:"_removeEventHandlers",value:function(){var e=this;d(this.dropdownOptions).find("li:not(.optgroup)").each(function(t){t.removeEventListener("click",e._handleOptionClickBound)}),this.el.removeEventListener("change",this._handleSelectChangeBound),this.input.removeEventListener("click",this._handleInputClickBound)}},{key:"_handleSelectChange",value:function(t){this._setValueToInput()}},{key:"_handleOptionClick",value:function(t){t.preventDefault();var e=d(t.target).closest("li")[0],i=e.id;if(!d(e).hasClass("disabled")&&!d(e).hasClass("optgroup")&&i.length){var n=!0;if(this.isMultiple){var s=d(this.dropdownOptions).find("li.disabled.selected");s.length&&(s.removeClass("selected"),s.find('input[type="checkbox"]').prop("checked",!1),this._toggleEntryFromArray(s[0].id)),n=this._toggleEntryFromArray(i)}else d(this.dropdownOptions).find("li").removeClass("selected"),d(e).toggleClass("selected",n);d(this._valueDict[i].el).prop("selected")!==n&&(d(this._valueDict[i].el).prop("selected",n),this.$el.trigger("change"))}t.stopPropagation()}},{key:"_handleInputClick",value:function(){this.dropdown&&this.dropdown.isOpen&&(this._setValueToInput(),this._setSelectedStates())}},{key:"_setupDropdown",value:function(){var n=this;this.wrapper=document.createElement("div"),d(this.wrapper).addClass("select-wrapper "+this.options.classes),this.$el.before(d(this.wrapper)),this.wrapper.appendChild(this.el),this.el.disabled&&this.wrapper.classList.add("disabled"),this.$selectOptions=this.$el.children("option, optgroup"),this.dropdownOptions=document.createElement("ul"),this.dropdownOptions.id="select-options-"+M.guid(),d(this.dropdownOptions).addClass("dropdown-content select-dropdown "+(this.isMultiple?"multiple-select-dropdown":"")),this.$selectOptions.length&&this.$selectOptions.each(function(t){if(d(t).is("option")){var e=void 0;e=n.isMultiple?n._appendOptionWithIcon(n.$el,t,"multiple"):n._appendOptionWithIcon(n.$el,t),n._addOptionToValueDict(t,e)}else if(d(t).is("optgroup")){var i=d(t).children("option");d(n.dropdownOptions).append(d('<li class="optgroup"><span>'+t.getAttribute("label")+"</span></li>")[0]),i.each(function(t){var e=n._appendOptionWithIcon(n.$el,t,"optgroup-option");n._addOptionToValueDict(t,e)})}}),this.$el.after(this.dropdownOptions),this.input=document.createElement("input"),d(this.input).addClass("select-dropdown dropdown-trigger"),this.input.setAttribute("type","text"),this.input.setAttribute("readonly","true"),this.input.setAttribute("data-target",this.dropdownOptions.id),this.el.disabled&&d(this.input).prop("disabled","true"),this.$el.before(this.input),this._setValueToInput();var t=d('<svg class="caret" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>');if(this.$el.before(t[0]),!this.el.disabled){var e=d.extend({},this.options.dropdownOptions);e.onOpenEnd=function(t){var e=d(n.dropdownOptions).find(".selected").first();if(e.length&&(M.keyDown=!0,n.dropdown.focusedIndex=e.index(),n.dropdown._focusFocusedItem(),M.keyDown=!1,n.dropdown.isScrollable)){var i=e[0].getBoundingClientRect().top-n.dropdownOptions.getBoundingClientRect().top;i-=n.dropdownOptions.clientHeight/2,n.dropdownOptions.scrollTop=i}},this.isMultiple&&(e.closeOnClick=!1),this.dropdown=M.Dropdown.init(this.input,e)}this._setSelectedStates()}},{key:"_addOptionToValueDict",value:function(t,e){var i=Object.keys(this._valueDict).length,n=this.dropdownOptions.id+i,s={};e.id=n,s.el=t,s.optionEl=e,this._valueDict[n]=s}},{key:"_removeDropdown",value:function(){d(this.wrapper).find(".caret").remove(),d(this.input).remove(),d(this.dropdownOptions).remove(),d(this.wrapper).before(this.$el),d(this.wrapper).remove()}},{key:"_appendOptionWithIcon",value:function(t,e,i){var n=e.disabled?"disabled ":"",s="optgroup-option"===i?"optgroup-option ":"",o=this.isMultiple?'<label><input type="checkbox"'+n+'"/><span>'+e.innerHTML+"</span></label>":e.innerHTML,a=d("<li></li>"),r=d("<span></span>");r.html(o),a.addClass(n+" "+s),a.append(r);var l=e.getAttribute("data-icon");if(l){var h=d('<img alt="" src="'+l+'">');a.prepend(h)}return d(this.dropdownOptions).append(a[0]),a[0]}},{key:"_toggleEntryFromArray",value:function(t){var e=!this._keysSelected.hasOwnProperty(t),i=d(this._valueDict[t].optionEl);return e?this._keysSelected[t]=!0:delete this._keysSelected[t],i.toggleClass("selected",e),i.find('input[type="checkbox"]').prop("checked",e),i.prop("selected",e),e}},{key:"_setValueToInput",value:function(){var i=[];if(this.$el.find("option").each(function(t){if(d(t).prop("selected")){var e=d(t).text();i.push(e)}}),!i.length){var t=this.$el.find("option:disabled").eq(0);t.length&&""===t[0].value&&i.push(t.text())}this.input.value=i.join(", ")}},{key:"_setSelectedStates",value:function(){for(var t in this._keysSelected={},this._valueDict){var e=this._valueDict[t],i=d(e.el).prop("selected");d(e.optionEl).find('input[type="checkbox"]').prop("checked",i),i?(this._activateOption(d(this.dropdownOptions),d(e.optionEl)),this._keysSelected[t]=!0):d(e.optionEl).removeClass("selected")}}},{key:"_activateOption",value:function(t,e){e&&(this.isMultiple||t.find("li.selected").removeClass("selected"),d(e).addClass("selected"))}},{key:"getSelectedValues",value:function(){var t=[];for(var e in this._keysSelected)t.push(this._valueDict[e].el.value);return t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_FormSelect}},{key:"defaults",get:function(){return e}}]),n}();M.FormSelect=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"formSelect","M_FormSelect")}(cash),function(s,e){"use strict";var i={},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Range=i).options=s.extend({},n.defaults,e),i._mousedown=!1,i._setupThumb(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this._removeThumb(),this.el.M_Range=void 0}},{key:"_setupEventHandlers",value:function(){this._handleRangeChangeBound=this._handleRangeChange.bind(this),this._handleRangeMousedownTouchstartBound=this._handleRangeMousedownTouchstart.bind(this),this._handleRangeInputMousemoveTouchmoveBound=this._handleRangeInputMousemoveTouchmove.bind(this),this._handleRangeMouseupTouchendBound=this._handleRangeMouseupTouchend.bind(this),this._handleRangeBlurMouseoutTouchleaveBound=this._handleRangeBlurMouseoutTouchleave.bind(this),this.el.addEventListener("change",this._handleRangeChangeBound),this.el.addEventListener("mousedown",this._handleRangeMousedownTouchstartBound),this.el.addEventListener("touchstart",this._handleRangeMousedownTouchstartBound),this.el.addEventListener("input",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("mousemove",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("touchmove",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("mouseup",this._handleRangeMouseupTouchendBound),this.el.addEventListener("touchend",this._handleRangeMouseupTouchendBound),this.el.addEventListener("blur",this._handleRangeBlurMouseoutTouchleaveBound),this.el.addEventListener("mouseout",this._handleRangeBlurMouseoutTouchleaveBound),this.el.addEventListener("touchleave",this._handleRangeBlurMouseoutTouchleaveBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("change",this._handleRangeChangeBound),this.el.removeEventListener("mousedown",this._handleRangeMousedownTouchstartBound),this.el.removeEventListener("touchstart",this._handleRangeMousedownTouchstartBound),this.el.removeEventListener("input",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("mousemove",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("touchmove",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("mouseup",this._handleRangeMouseupTouchendBound),this.el.removeEventListener("touchend",this._handleRangeMouseupTouchendBound),this.el.removeEventListener("blur",this._handleRangeBlurMouseoutTouchleaveBound),this.el.removeEventListener("mouseout",this._handleRangeBlurMouseoutTouchleaveBound),this.el.removeEventListener("touchleave",this._handleRangeBlurMouseoutTouchleaveBound)}},{key:"_handleRangeChange",value:function(){s(this.value).html(this.$el.val()),s(this.thumb).hasClass("active")||this._showRangeBubble();var t=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",t+"px")}},{key:"_handleRangeMousedownTouchstart",value:function(t){if(s(this.value).html(this.$el.val()),this._mousedown=!0,this.$el.addClass("active"),s(this.thumb).hasClass("active")||this._showRangeBubble(),"input"!==t.type){var e=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",e+"px")}}},{key:"_handleRangeInputMousemoveTouchmove",value:function(){if(this._mousedown){s(this.thumb).hasClass("active")||this._showRangeBubble();var t=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",t+"px"),s(this.value).html(this.$el.val())}}},{key:"_handleRangeMouseupTouchend",value:function(){this._mousedown=!1,this.$el.removeClass("active")}},{key:"_handleRangeBlurMouseoutTouchleave",value:function(){if(!this._mousedown){var t=7+parseInt(this.$el.css("padding-left"))+"px";s(this.thumb).hasClass("active")&&(e.remove(this.thumb),e({targets:this.thumb,height:0,width:0,top:10,easing:"easeOutQuad",marginLeft:t,duration:100})),s(this.thumb).removeClass("active")}}},{key:"_setupThumb",value:function(){this.thumb=document.createElement("span"),this.value=document.createElement("span"),s(this.thumb).addClass("thumb"),s(this.value).addClass("value"),s(this.thumb).append(this.value),this.$el.after(this.thumb)}},{key:"_removeThumb",value:function(){s(this.thumb).remove()}},{key:"_showRangeBubble",value:function(){var t=-7+parseInt(s(this.thumb).parent().css("padding-left"))+"px";e.remove(this.thumb),e({targets:this.thumb,height:30,width:30,top:-30,marginLeft:t,duration:300,easing:"easeOutQuint"})}},{key:"_calcRangeOffset",value:function(){var t=this.$el.width()-15,e=parseFloat(this.$el.attr("max"))||100,i=parseFloat(this.$el.attr("min"))||0;return(parseFloat(this.$el.val())-i)/(e-i)*t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Range}},{key:"defaults",get:function(){return i}}]),n}();M.Range=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"range","M_Range"),t.init(s("input[type=range]"))}(cash,M.anime);
\ No newline at end of file
diff --git a/js/part_selection.js b/js/part_selection.js
deleted file mode 100644 (file)
index 9fd5657..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-function part_selection_window(input_partnumber, input_description, input_partsid, allow_creation, formname, options) {
-  var width                   = allow_creation ? 1000 : 800;
-  var parm                    = centerParms(width,500) + ",width=" + width + ",height=500,status=yes,scrollbars=yes";
-  var partnumber              = document.getElementsByName(input_partnumber)[0].value;
-  var description             = document.getElementsByName(input_description)[0].value;
-  var action_on_part_selected = document.getElementsByName("action_on_part_selected")[0];
-  var form                    = (formname == undefined) ? document.forms[0] : document.getElementsByName(formname)[0];
-  var filter                  = document.getElementsByName(input_partnumber + "_filter")[0];
-  var input_partnotes         = "";
-
-  if (input_partnumber.match(/_\d+$/)) {
-    input_partnotes = input_partnumber;
-    input_partnotes = input_partnotes.replace(/partnumber/, "partnotes");
-    if (input_partnotes == input_partnumber)
-      input_partnotes = "";
-  }
-
-  if (filter)
-    filter = filter.value;
-  else
-    filter = "";
-
-  if (!options)
-    options = "";
-
-  url = "common.pl?" +
-    "INPUT_ENCODING=UTF-8&" +
-    "action=part_selection_internal&" +
-    "partnumber="              + encodeURIComponent(partnumber)        + "&" +
-    "description="             + encodeURIComponent(description)       + "&" +
-    "input_partnumber="        + encodeURIComponent(input_partnumber)  + "&" +
-    "input_description="       + encodeURIComponent(input_description) + "&" +
-    "input_partsid="           + encodeURIComponent(input_partsid)     + "&" +
-    "input_partnotes="         + encodeURIComponent(input_partnotes)   + "&" +
-    "filter="                  + encodeURIComponent(filter)            + "&" +
-    "options="                 + encodeURIComponent(options)           + "&" +
-    "formname="                + encodeURIComponent(formname)          + "&" +
-    "allow_creation="          + (allow_creation ? "1" : "0")   + "&" +
-    "action_on_part_selected=" + (null == action_on_part_selected ? "" : action_on_part_selected.value);
-  //alert(url);
-  window.open(url, "_new_part_selection", parm);
-}
-
diff --git a/js/quicksearch_input.js b/js/quicksearch_input.js
deleted file mode 100644 (file)
index e281f94..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-function on_keydown_quicksearch(event) {
-  var key;
-  var element = $(this);
-
-  if (window.event)
-    key = window.event.keyCode;   // IE
-  else
-    key = event.which;            // Firefox
-
-  if (key != 13)
-    return true;
-
-  var search_term = $(element);
-  var value       = search_term.val();
-  if (!value)
-    return true;
-
-  var url = "ct.pl?action=list_contacts&INPUT_ENCODING=utf-8&filter.status=active&search_term=" + encodeURIComponent(value);
-
-  window.location.href = url;
-
-  return false;
-}
-$(function(){ $('#frame_header_contact_search').keydown(on_keydown_quicksearch) });
index 9e5b856..1d5ace4 100644 (file)
@@ -430,7 +430,7 @@ ns.renumber = function(opt) {
   $('#rs-dialog-confirm').remove();
 
   var text1   = kivi.t8('Re-numbering all sections and function blocks in the order they are currently shown cannot be undone.');
-  var text2   = kivi.t8('Do you really want do continue?');
+  var text2   = kivi.t8('Do you really want to continue?');
   var $dialog = $('<div id="rs-dialog-confirm"><p>' + text1 + '</p><p>' + text2 + '</p></div>').hide().appendTo('body');
   var buttons = {};
 
@@ -873,8 +873,9 @@ ns.tabs_before_activate = function(event, ui) {
 // -------------------------------------------------------------------------
 
 ns.create_context_menus = function(data) {
+  var general_actions;
   if (data.is_template) {
-    var general_actions = {
+    general_actions = {
         sep98:           "---------"
       , general_actions: { name: kivi.t8('Requirement spec template actions'), className: 'context-menu-heading' }
       // , sep99:           "---------"
@@ -893,7 +894,7 @@ ns.create_context_menus = function(data) {
     });
 
   } else {                      // if (is_template)
-    var general_actions = {
+    general_actions = {
         sep98:              "---------"
       , general_actions:    { name: kivi.t8('Requirement spec actions'), className: 'context-menu-heading' }
       , create_pdf:         { name: kivi.t8('Create PDF'),              icon: "pdf",    callback: kivi.requirement_spec.create_reqspec_pdf }
@@ -901,7 +902,7 @@ ns.create_context_menus = function(data) {
       , create_version:     { name: kivi.t8('Create new version'),      icon: "new",    callback: kivi.requirement_spec.create_version, disabled: kivi.requirement_spec.disable_commands }
       , copy_reqspec:       { name: kivi.t8('Copy requirement spec'),   icon: "copy",   callback: kivi.requirement_spec.copy_reqspec   }
       , delete_reqspec:     { name: kivi.t8('Delete requirement spec'), icon: "delete", callback: kivi.requirement_spec.delete_reqspec }
-      , sep_paste_template: "---------"
+      , sep_renumber:       "---------"
       , renumber:           { name: kivi.t8('Renumber sections and function blocks'), icon: "renumber", callback: kivi.requirement_spec.renumber }
       , sep_paste_template: "---------"
       , paste_template:     { name: kivi.t8('Paste template'),     icon: "paste",  callback: kivi.requirement_spec.paste_template }
index 6985c69..a61e21d 100644 (file)
@@ -10,14 +10,18 @@ function centerParms(width,height,extra) {
   return string;
 }
 
-function set_history_window(id,trans_id_type) {
-  var parm = centerParms(800,500) + ",width=800,height=500,status=yes,scrollbars=yes";
-  var name = "History";
-  url = "common.pl?" +
-    "INPUT_ENCODING=UTF-8&" +
-    "action=show_history&" +
-    "longdescription=" + "&" +
-    "trans_id_type=" + encodeURIComponent(trans_id_type) + "&" +
-    "input_name=" + encodeURIComponent(id) + "&"
+function set_history_window(id,trans_id_type, snumbers, what_done) {
+  var parm = centerParms(1100,500) + ",width=1100,height=500,status=yes,scrollbars=yes";
+  var url  = "common.pl?action=show_history&INPUT_ENCODING=UTF-8&";
+
+  if (trans_id_type)
+    url += "&trans_id_type=" + encodeURIComponent(trans_id_type);
+  if (snumbers)
+    url += "&s_numbers=" + encodeURIComponent(snumbers);
+  if (what_done)
+    url += "&what_done=" + encodeURIComponent(what_done);
+  if (id)
+    url += "&input_name=" + encodeURIComponent(id);
+
   window.open(url, "_new_generic", parm);
 }
diff --git a/js/switchmenuframe.js b/js/switchmenuframe.js
deleted file mode 100644 (file)
index ed7c48f..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-var vSwitch_Menu = 1;
-function Switch_Menu() {
-  vSwitch_Menu=!vSwitch_Menu;
-  SetMenuFolded(vSwitch_Menu);
-}
-function SetMenuFolded(on) {
-  if (on) {
-    $('#html-menu').removeClass('folded');
-    $('#content').removeClass('folded');
-  } else {
-    $('#html-menu').addClass('folded');
-    $('#content').addClass('folded');
-  }
-}
-$(function(){
-  SetMenuFolded(vSwitch_Menu);
-})
index bfc093d..256c2ea 100644 (file)
@@ -38,6 +38,16 @@ QUnit.test("kivi.format_amount function English number style without thousand se
   assert.equal(kivi.format_amount(-1000000000.1234, 2), '-1000000000.12', 'format -1000000000.1234');
 });
 
+QUnit.test("kivi.format_amount function Swiss number style with thousand separator", function( assert ) {
+  kivi.setup_formats({ numbers: '1\'000.00' });
+
+  assert.equal(kivi.format_amount('1e1', 2), '10.00', 'format 1e1');
+  assert.equal(kivi.format_amount(1000, 2), '1\'000.00', 'format 1000');
+  assert.equal(kivi.format_amount(1000.1234, 2), '1\'000.12', 'format 1000.1234');
+  assert.equal(kivi.format_amount(1000000000.1234, 2), '1\'000\'000\'000.12', 'format 1000000000.1234');
+  assert.equal(kivi.format_amount(-1000000000.1234, 2), '-1\'000\'000\'000.12', 'format -1000000000.1234');
+});
+
 QUnit.test("kivi.format_amount function negative places", function( assert ) {
   kivi.setup_formats({ numbers: '1000.00' });
 
index d0c6e88..8df7767 100644 (file)
@@ -81,3 +81,43 @@ QUnit.test("kivi.parse_amount function English number style without thousand sep
   assert.equal(kivi.parse_amount('1010.987654321'), 1010.987654321, '1010.987654321');
   assert.equal(kivi.parse_amount('1,010.987654321'), 1010.987654321, '1,010.987654321');
 });
+
+QUnit.test("kivi.parse_amount function Swiss number style with thousand separator", function( assert ) {
+  kivi.setup_formats({ numbers: '1\'000.00' });
+
+  assert.equal(kivi.parse_amount('10.00'), 10, '10.00');
+  assert.equal(kivi.parse_amount('10.'), 10, '10.');
+  assert.equal(kivi.parse_amount('1010.00'), 1010, '1010.00');
+  assert.equal(kivi.parse_amount('1010.'), 1010, '1010.');
+  assert.equal(kivi.parse_amount('1\'010.00'), 1010, '1\'010.00');
+  assert.equal(kivi.parse_amount('1\'010.'), 1010, '1\'010.');
+  assert.equal(kivi.parse_amount('9\'080\'070\'060\'050\'040\'030\'020\'010.00'), 9080070060050040030020010, '9\'080\'070\'060\'050\'040\'030\'020\'010.00');
+  assert.equal(kivi.parse_amount('9\'080\'070\'060\'050\'040\'030\'020\'010.'), 9080070060050040030020010, '9\'080\'070\'060\'050\'040\'030\'020\'010.');
+
+  assert.equal(kivi.parse_amount('10.98'), 10.98, '10.98');
+  assert.equal(kivi.parse_amount('1010.98'), 1010.98, '1010.98');
+  assert.equal(kivi.parse_amount('1\'010.98'), 1010.98, '1\'010.98');
+
+  assert.equal(kivi.parse_amount('10.987654321'), 10.987654321, '10.987654321');
+  assert.equal(kivi.parse_amount('1010.987654321'), 1010.987654321, '1010.987654321');
+  assert.equal(kivi.parse_amount('1\'010.987654321'), 1010.987654321, '1\'010.987654321');
+});
+
+QUnit.test("kivi.parse_amount function numbers with leading 0 should still be parsed as decimal and not octal", function( assert ) {
+  kivi.setup_formats({ numbers: '1000,00' });
+
+  assert.equal(kivi.parse_amount('0123456789'),   123456789, '0123456789');
+  assert.equal(kivi.parse_amount('000123456789'), 123456789, '000123456789');
+});
+
+QUnit.test("kivi.parse_amount function German number style with thousand separator & contains invalid characters", function( assert ) {
+  kivi.setup_formats({ numbers: '1.000,00' });
+
+  assert.equal(kivi.parse_amount('iuh !@#$% 10,00'), undefined, 'iuh !@#$% 10,00');
+});
+
+QUnit.test("kivi.parse_amount function German number style with thousand separator & invalid math expression", function( assert ) {
+  kivi.setup_formats({ numbers: '1.000,00' });
+
+  assert.equal(kivi.parse_amount('54--42'), undefined, '54--42');
+});
index b488b1e..121d43e 100644 (file)
@@ -1,3 +1,12 @@
+function today() {
+  var today = new Date();
+  today.setMilliseconds(0);
+  today.setSeconds(0);
+  today.setMinutes(0);
+  today.setHours(0);
+  return today;
+};
+
 QUnit.test("kivi.parse_date function for German date style with dots", function( assert ) {
   kivi.setup_formats({ dates: "dd.mm.yy" });
 
@@ -12,7 +21,22 @@ QUnit.test("kivi.parse_date function for German date style with dots", function(
   assert.deepEqual(kivi.parse_date("25.12."), new Date((new Date).getFullYear(), 11, 25));
   assert.deepEqual(kivi.parse_date("25.12"), new Date((new Date).getFullYear(), 11, 25));
 
+  assert.deepEqual(kivi.parse_date("2512"), new Date((new Date).getFullYear(), 11, 25));
+  assert.deepEqual(kivi.parse_date("25122015"), new Date(2015, 11, 25));
+  assert.deepEqual(kivi.parse_date("251215"), new Date(2015, 11, 25));
+  assert.deepEqual(kivi.parse_date("25"), new Date((new Date).getFullYear(), (new Date).getMonth(), 25));
+  assert.deepEqual(kivi.parse_date("1"), new Date((new Date).getFullYear(), (new Date).getMonth(), 1));
+  assert.deepEqual(kivi.parse_date("01"), new Date((new Date).getFullYear(), (new Date).getMonth(), 1));
+
   assert.deepEqual(kivi.parse_date("Totally Invalid!"), undefined);
+  assert.deepEqual(kivi.parse_date(":"), undefined);
+  assert.deepEqual(kivi.parse_date("::"), undefined);
+  assert.deepEqual(kivi.parse_date("."), today());
+  assert.deepEqual(kivi.parse_date(".."), today());
+  assert.deepEqual(kivi.parse_date(""), null);
+  assert.deepEqual(kivi.parse_date("0"), new Date());
+  assert.deepEqual(kivi.parse_date("00"), new Date());
+  assert.deepEqual(kivi.parse_date("29.02.20008"), undefined);
 });
 
 QUnit.test("kivi.parse_date function for German date style with slashes", function( assert ) {
@@ -29,6 +53,9 @@ QUnit.test("kivi.parse_date function for German date style with slashes", functi
   assert.deepEqual(kivi.parse_date("25/12/"), new Date((new Date).getFullYear(), 11, 25));
   assert.deepEqual(kivi.parse_date("25/12"), new Date((new Date).getFullYear(), 11, 25));
 
+  assert.deepEqual(kivi.parse_date("/"), today());
+  assert.deepEqual(kivi.parse_date("//"), today());
+
   assert.deepEqual(kivi.parse_date("Totally Invalid!"), undefined);
 });
 
@@ -46,6 +73,9 @@ QUnit.test("kivi.parse_date function for American date style", function( assert
   assert.deepEqual(kivi.parse_date("12/25/"), new Date((new Date).getFullYear(), 11, 25));
   assert.deepEqual(kivi.parse_date("12/25"), new Date((new Date).getFullYear(), 11, 25));
 
+  assert.deepEqual(kivi.parse_date("/"), today());
+  assert.deepEqual(kivi.parse_date("//"), today());
+
   assert.deepEqual(kivi.parse_date("Totally Invalid!"), undefined);
 });
 
@@ -100,5 +130,18 @@ QUnit.test("kivi.format_date function for ISO date style", function( assert ) {
   assert.deepEqual(kivi.format_date(new Date(2008, 1, 29)), "2008-02-29");
   assert.deepEqual(kivi.format_date(new Date(2014, 11, 11)), "2014-12-11");
 
+  assert.deepEqual(kivi.parse_date("1225"), new Date((new Date).getFullYear(), 11, 25));
+  assert.deepEqual(kivi.parse_date("20151225"), new Date(2015, 11, 25));
+  assert.deepEqual(kivi.parse_date("151225"), new Date(2015, 11, 25));
+  assert.deepEqual(kivi.parse_date("25"), new Date((new Date).getFullYear(), (new Date).getMonth(), 25));
+  assert.deepEqual(kivi.parse_date("1"), new Date((new Date).getFullYear(), (new Date).getMonth(), 1));
+  assert.deepEqual(kivi.parse_date("01"), new Date((new Date).getFullYear(), (new Date).getMonth(), 1));
+
+  assert.deepEqual(kivi.parse_date("0"), new Date());
+  assert.deepEqual(kivi.parse_date("00"), new Date());
+
+  assert.deepEqual(kivi.parse_date("-"), today());
+  assert.deepEqual(kivi.parse_date("--"), today());
+
   assert.deepEqual(kivi.format_date(new Date(undefined, undefined, undefined)), undefined);
 });
diff --git a/js/t/kivi/parse_format_time.js b/js/t/kivi/parse_format_time.js
new file mode 100644 (file)
index 0000000..393d790
--- /dev/null
@@ -0,0 +1,47 @@
+function custom_time(h,m) {
+  var time = new Date();
+  time.setHours(h,m);
+  return time;
+}
+
+QUnit.test("kivi.parse_time function for German time style with colon", function( assert ) {
+  assert.equalTimes = function(actual, expected, message) {
+    console.log(this);
+    var result = (expected === undefined && actual === undefined)
+                || (expected === null      && actual === null)
+                || (expected instanceof Date && actual instanceof Date &&
+                    expected.getHours()   == actual.getHours() &&
+                    expected.getMinutes() == actual.getMinutes());
+
+    this.push( {
+        result: result,
+        actual: actual,
+        expected: expected,
+        message: message
+    } );
+  }
+
+  kivi.setup_formats({ times: "hh:mm" });
+
+  assert.equalTimes(kivi.parse_time("12:34"), custom_time(12,34));
+  assert.equalTimes(kivi.parse_time("10:00"), custom_time(10,0));
+  assert.equalTimes(kivi.parse_time("      12 :  23  ") - custom_time(12,23));
+
+  assert.equalTimes(kivi.parse_time("00:20"), custom_time(0,20));
+
+  assert.equalTimes(kivi.parse_time("23:60"), custom_time(23,60));
+
+  assert.equalTimes(kivi.parse_time("1142"), custom_time(11,42));
+
+  assert.equalTimes(kivi.parse_time("Totally Invalid!"), undefined);
+  assert.equalTimes(kivi.parse_time("."), undefined);
+  assert.equalTimes(kivi.parse_time(".."), undefined);
+  assert.equalTimes(kivi.parse_time(":"), undefined);
+  assert.equalTimes(kivi.parse_time("::"), undefined);
+  assert.equalTimes(kivi.parse_time("aa:bb"), undefined);
+  assert.equalTimes(kivi.parse_time("aasd:bbaf"), undefined);
+  assert.equalTimes(kivi.parse_time(""), null);
+  assert.equalTimes(kivi.parse_time("0"), new Date());
+  assert.equalTimes(kivi.parse_time("29:20008"), custom_time(29,20008));
+});
+
index 0a9a047..cde5b44 100644 (file)
@@ -1,2 +1,9 @@
-Order Allow,Deny
-Deny from all
+<IfModule mod_authz_core.c>
+  # Apache 2.4
+  Require all denied
+</IfModule>
+<IfModule !mod_authz_core.c>
+  # Apache 2.2
+  Order deny,allow
+  Deny from all
+</IfModule>
index b12eb1b..22ffdfe 100644 (file)
@@ -18,5 +18,6 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 ######################################################################
index 5b4f8d9..e5024fe 100644 (file)
@@ -19,7 +19,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #======================================================================
 #
 # this is a variation of the Lingua package
index 0121e1c..e148066 100755 (executable)
@@ -10,31 +10,35 @@ use utf8;
 # run locales.pl from this directory to rebuild the translation files
 
 $self->{texts} = {
-  ' (in use so no change allowed)' => ' (Faktor wird verwendet, keine Änderung erlaubt)',
   ' Date missing!'              => ' Datum fehlt!',
-  ' Part Number missing!'       => ' Artikelnummer fehlt!',
+  ' bytes, max='                => ' Bytes, Maximum=',
   ' missing!'                   => ' fehlt!',
+  '"#1" seems to be a faulty list of email addresses. After extracing addresses (#2) too many characters are left.' => '"#1" scheint fehlerhaft zu sein. Es wurden E-Mail Adressen extrahiert (#2), aber es sind noch zu viele Zeichen übrig.',
+  '"#1" seems to be a faulty list of email addresses. No addresses could be extracted.' => '"#1" scheint fehlerhaft zu sein. Es konnte keine E-Mail Adresse extrahiert werden',
   '#1 (custom variable)'        => '#1 (benutzerdefinierte Variable)',
   '#1 MD'                       => '#1 PT',
   '#1 additional part(s)'       => '#1 zusätzliche(r) Artikel',
+  '#1 bank transaction bookings undone.' => '#1 Bankbewegung(en) rückgängig gemacht',
   '#1 dunnings have been deleted' => '#1 Mahnung(en) wurden gelöscht',
   '#1 h'                        => '#1 h',
-  '#1 of #2 importable objects were imported.' => '#1 von #2 importierbaren Objekten wurden importiert.',
+  '#1 invoice(s) saved.'        => '#1 Rechnung(en) abgespeichert.',
   '#1 prices were updated.'     => '#1 Preise wurden aktualisiert.',
   '#1 proposal(s) saved.'       => '#1 Vorschläge gespeichert.',
+  '#1 proposal(s) with #2 invoice(s) saved.' => '#1 Vorschlag(e) mit #2 Rechnung(en) abgespeichert',
   '#1 section(s)'               => '#1 Abschnitt(e)',
   '#1 text block(s) back'       => '#1 Textlock/-blöcke vorne',
   '#1 text block(s) front'      => '#1 Textblock/-blöcke hinten',
   '%'                           => '%',
   '(recommended) Insert the used currencies in the system. You can simply change the name of the currencies by editing the textfields above. Do not use a name of a currency that is already in use.' => '(empfohlen) Fügen Sie die verwaisten Währungen in Ihr System ein. Sie können den Namen der Währung einfach ändern, indem Sie die Felder oben bearbeiten. Benutzen Sie keine Namen von Währungen, die Sie bereits benutzen.',
   '*/'                          => '*/',
+  '+'                           => '+',
   ', if set'                    => ', falls gesetzt',
   '---please select---'         => '---bitte auswählen---',
   '. Automatically generated.'  => '. Automatisch erzeugt.',
   '...after logging in'         => '...nach dem Anmelden',
   '...done'                     => '...fertig',
   '...on the TODO list'         => '...auf der Aufgabenliste',
-  '0% tax with taxkey'          => '0% Steuer mit Steuerschl&uuml;ssel ',
+  '0% tax with taxkey'          => '0% Steuer mit Steuerschlüssel ',
   '1. Quarter'                  => '1. Quartal',
   '2 years'                     => '2 Jahre',
   '2. Quarter'                  => '2. Quartal',
@@ -43,24 +47,34 @@ $self->{texts} = {
   '4 years'                     => '4 Jahre',
   '4. Quarter'                  => '4. Quartal',
   '5 years'                     => '5 Jahre',
-  '<b> I DO CARE!</b> Please check create warehouse and bins and define a name for the warehouse (Bins will be created automatically) and then continue' => '<b>ICH KÜMMER MICH</b> Bitte haken Sie Lager und Lagerplätze erzeugen an (Automatisches Zuweisen der Lagerplätze) und vergeben einen Namen für dieses Lager (Lagerpl&auml;tze werden automatisch übernommen). Danach auf weiter.',
+  '<b> I DO CARE!</b> Please check create warehouse and bins and define a name for the warehouse (Bins will be created automatically) and then continue' => '<b>ICH KÜMMER MICH</b> Bitte haken Sie Lager und Lagerplätze erzeugen an (Automatisches Zuweisen der Lagerplätze) und vergeben einen Namen für dieses Lager (Lagerplätze werden automatisch übernommen). Danach auf weiter.',
   '<b> I DO CARE!</b> Please click back and cancel the update and come back after there has been at least one warehouse defined with bin(s).:' => '<b>ICH KÜMMER MICH</b> Brechen Sie das Update ab und legen selber mindestens ein Lager mit Lagerplätzen unter dem Menü System / Lager an.',
   '<b> I DO NOT CARE</b> Please click continue and the following data (see list) will be deleted:' => '<b>IST MIR EGAL</b> Mit einem Klick auf Weiter (rot) werden keine Daten übernommen, bzw. migriert und die folgende Information in der untenstehenden Liste wird gelöscht.',
   '<b>Automatically create new bins</b> in the following new warehouse ' => '<b>Automatisches Zuweisen der Lagerplätze</b> im folgenden neuem Lager:',
   '<b>Automatically create new bins</b> in the following warehouse if not selected in the list above' => '<b>Automatisches Zuweisen der Lagerplätze</b> im folgenden Lager, falls keine andere Zuweisung oben ausgewählt ist. ',
   '<b>Default Bins Migration !READ CAREFULLY!</b>' => 'Standardlagerplatz Migration !AUFMERKSAM LESEN!',
   '<b>What</b> do you want to look for?' => '<b>Wonach</b> wollen Sie suchen?',
+  'A canceled general ledger transaction cannot be canceled again.' => 'Eine stornierte Dialogbuchung kann nicht erneut storniert werden.',
+  'A canceled general ledger transaction cannot be deleted.' => 'Eine stornierte Dialogbuchung kann nicht gelöscht werden.',
+  'A canceled general ledger transaction cannot be posted.' => 'Eine stornierte Dialogbuchung kann nicht mehr gebucht werden.',
+  'A canceled invoice cannot be posted.' => 'Eine stornierte Rechnung kann nicht mehr gebucht werden.',
+  'A canceled invoice cannot be used. Please undo the cancellation first.' => 'Eine stornierte Rechnung kann nicht verwendet werden. Bitte machen Sie die Stornierung zunächst rückgängig.',
+  'A customer with the same VAT ID already exists.' => 'Ein Kunde mit der gleichen USt-IdNr. existiert bereits.',
+  'A customer with the same taxnumber already exists.' => 'Ein Kunde mit der gleichen Steuernummer existiert bereits.',
   'A digit is required.'        => 'Eine Ziffer ist vorgeschrieben.',
   'A directory with the name for the new print templates exists already.' => 'Ein Verzeichnis mit dem selben Namen wie die neuen Druckvorlagen existiert bereits.',
   'A lot of the usability of kivitendo has been enhanced with javascript. Although it is currently possible to use every aspect of kivitendo without javascript, we strongly recommend it. In a future version this may change and javascript may be necessary to access advanced features.' => 'Die Bedienung von kivitendo wurde an vielen Stellen mit Javascript verbessert. Obwohl es derzeit möglich ist, jeden Aspekt von kivitendo auch ohne Javascript zu benutzen, empfehlen wir es. In einer zukünftigen Version wird Javascript eventuell notwendig sein um weitergehende Features zu benutzen.',
   'A lower-case character is required.' => 'Ein Kleinbuchstabe ist vorgeschrieben.',
-  'A mail error occurred: #1'   => 'Ein ',
+  'A payment can only be posted for multiple invoices if the amount to post is equal to or bigger than the sum of the open amounts of the affected invoices.' => 'Eine Zahlung kann nur dann für mehrere Rechnungen verbucht werden, wenn die Zahlung gleich oder größer als die Summe der offenen Beträge der betroffenen Rechnungen ist.',
   'A special character is required (valid characters: #1).' => 'Ein Sonderzeichen ist vorgeschrieben (gültige Zeichen: #1).',
+  'A target quantitiy has to be given' => 'Es muss eine Zielmenge angegeben werden',
   'A transaction description is required.' => 'Die Vorgangsbezeichnung muss eingegeben werden.',
   'A unit with this name does already exist.' => 'Eine Einheit mit diesem Namen existiert bereits.',
-  'A valid taxkey is missing!'  => 'Einen gültiger Steuerschlüssel fehlt!',
+  'A valid taxkey is missing!'  => 'Ein gültiger Steuerschlüssel fehlt!',
   'A variable marked as \'Deactivate by default\' isn\'t automatically added to all articles, and has to be explicitly added for each desired article in its master data tab. Only then can the variable be used for that article in the records.' => 'Eine als \'Deaktiviert als Voreinstellung\' markierte Variable wird nicht automatisch bei allen Artikeln hinzugefügt, sondern muß explizit für jeden gewünschten Artikel in den Stammdaten aktiviert werden. Erst danach ist die Variable für den Artikel in Belegen bearbeitbar.',
-  'A variable marked as \'editable\' can be changed in each quotation, order, invoice etc.' => 'Eine als \'Bearbeitbar\' markierte Variable kann in jedem Angebot, Auftrag, jeder Rechnung etc für jede Position geändert werden.',
+  'A variable marked as \'editable\' can be changed in each quotation, order, invoice etc.' => 'Eine als \'editierbar\' markierte Variable kann in jedem Angebot, Auftrag, jeder Rechnung etc für jede Position geändert werden.',
+  'A vendor with the same VAT ID already exists.' => 'Ein Lieferant mit der gleichen USt-IdNr. existiert bereits.',
+  'A vendor with the same taxnumber already exists.' => 'Ein Lieferant mit der gleichen Steuernummer existiert bereits.',
   'ADDED'                       => 'Hinzugefügt',
   'AP'                          => 'Einkauf',
   'AP Aging'                    => 'Offene Verbindlichkeiten',
@@ -69,25 +83,31 @@ $self->{texts} = {
   'AP Transaction Storno (one letter abbreviation)' => 'S',
   'AP Transaction with Storno (abbreviation)' => 'K(S)',
   'AP Transactions'             => 'Kreditorenbuchungen',
+  'AP template suggestions'     => 'Vorschlag Kreditorenbuchung',
+  'AP transaction \'#1\' posted (ID: #2)' => 'Kreditorenbuchung \'#1\' verbucht (Buchungsnummer: #2)',
   'AP transactions changeable'  => 'Änderbarkeit von Kreditorenbuchungen',
   'AP transactions with sales taxkeys and/or AR transactions with input taxkeys' => 'Kreditorenbuchungen mit Umsatzsteuer-Steuerschlüsseln und/oder Debitorenbuchungen mit Vorsteuer-Steuerschlüsseln',
+  'AP/AR Aging & Journal'       => 'Offene Forderungen/Verbindlichkeiten & Buchungsjournal',
   'AR'                          => 'Verkauf',
   'AR Aging'                    => 'Offene Forderungen',
   'AR Transaction'              => 'Debitorenbuchung',
   'AR Transaction (abbreviation)' => 'D',
+  'AR Transaction/AccTrans Item row names' => 'Namen der Rechnungs/Buchungszeilen',
   'AR Transactions'             => 'Debitorenbuchungen',
+  'AR transaction \'#1\' posted (ID: #2)' => 'Debitorenbuchung \'#1\' verbucht (Buchungsnummer: #2)',
   'AR transactions changeable'  => 'Änderbarkeit von Debitorenbuchungen',
   'ASSETS'                      => 'AKTIVA',
   'ATTENTION! If you enabled this feature you can not simply turn it off again without taking care that best_before fields are emptied in the database.' => 'ACHTUNG! Wenn Sie diese Einstellung aktivieren, dann können Sie sie später nicht ohne Weiteres deaktivieren, ohne dafür zu sorgen, dass die Felder der Mindeshaltbarkeitsdaten in der Datenbank leer gemacht werden.',
   'ATTENTION! You can not simply change it from periodic to perpetual once you started posting.' => 'ACHTUNG! Es kann nicht ohne Weiteres im laufenden Betrieb von der Aufwandsmethode zur Bestandsmethode gewechselt werden.',
   'AUTOMATICALLY MATCH BINS'    => 'LAGERPLÄTZE AUTOMATISCH ZUWEISEN',
+  'Abbreviation Legend'         => 'Beschreibung der Typ-Abkürzungen (1 Zeichen Typ, 1-2 Zeichen Klassifizierung)',
   'Abort'                       => 'Abbrechen',
   'Abrechnungsnummer'           => 'Abrechnungsnummer',
   'Absolute BB Balance'         => 'Gesamtsaldo laut Bankbuchungen',
   'Absolute BT Balance'         => 'Gesamtsaldo laut Kontoauszug',
-  'Abteilung'                   => 'Abteilung',
   'Acc Transaction'             => 'Hauptbuch',
   'Acc transaction'             => 'Hauptbuch Buchung',
+  'AccTransaction'              => 'AccTransaction',
   'Acceptance Statuses'         => 'Abnahmestatus',
   'Access rights'               => 'Zugriffsrechte',
   'Access to clients'           => 'Zugriff auf Mandanten',
@@ -122,17 +142,22 @@ $self->{texts} = {
   'Account Type missing!'       => 'Kontoart fehlt!',
   'Account categories'          => 'Kontoarten',
   'Account deleted!'            => 'Konto gelöscht!',
-  'Account for fees'            => 'Konto f&uuml;r Geb&uuml;hren',
-  'Account for interest'        => 'Konto f&uuml;r Zinsen',
+  'Account for fees'            => 'Konto für Gebühren',
+  'Account for interest'        => 'Konto für Zinsen',
+  'Account for workflow from purchase order to ap transaction' => 'Konto für den Workflow von Lieferantenauftrag nach Kreditorenbuchung',
   'Account number'              => 'Kontonummer',
   'Account number not unique!'  => 'Kontonummer bereits vorhanden!',
   'Account number of the goal/source' => 'Ziel- oder Quellkonto',
   'Account saved!'              => 'Konto gespeichert!',
+  'Accounting desired'          => 'Verrechnung des Erstattungsbetrags erwünscht',
   'Accounting method'           => 'Versteuerungsart',
   'Accrual'                     => 'Soll-Versteuerung',
   'Accrual accounting'          => 'Soll-Versteuerung',
+  'Action'                      => 'Aktion',
+  'Actions'                     => 'Aktionen',
   'Activate kivitendo module'   => 'Modul aktivieren',
   'Active'                      => 'Aktiv',
+  'Active shops:'               => 'Webshops aktiv',
   'Active?'                     => 'Aktiviert?',
   'Add'                         => 'Erfassen',
   'Add AP Transaction'          => 'Kreditorenbuchung',
@@ -141,29 +166,31 @@ $self->{texts} = {
   'Add Accounts Payables Transaction' => 'Kreditorenbuchung erfassen',
   'Add Accounts Receivables Transaction' => 'Debitorenbuchung erfassen',
   'Add Assembly'                => 'Erzeugnis erfassen',
-  'Add Buchungsgruppe'          => 'Buchungsgruppe erfassen',
+  'Add Assortment'              => 'Sortiment erfassen',
   'Add Client'                  => 'Neuer Mandant',
   'Add Credit Note'             => 'Gutschrift erfassen',
+  'Add Credit Note for this dunning level:' => 'Diese Gutschrift für untenstehende Mahnstufe anzeigen',
   'Add Customer'                => 'Kunde erfassen',
+  'Add Customer/Vendor Number as a reference add-on for SEPA export.' => 'Kunden- Lieferantennummer im Verwendungszweck bei SEPA-Überweisungen anhängen',
   'Add Delivery Note'           => 'Lieferschein erfassen',
   'Add Delivery Order'          => 'Lieferschein erfassen',
+  'Add Document from \'#1\''    => 'Dokument von \'#1\' hinzufügen',
   'Add Dunning'                 => 'Mahnung erzeugen',
+  'Add Final Invoice'           => 'Schlussrechnung erfassen',
   'Add Follow-Up'               => 'Wiedervorlage erstellen',
-  'Add Follow-Up for #1'        => 'Wiedervorlage f&uuml;r #1 erstellen',
+  'Add Follow-Up for #1'        => 'Wiedervorlage für #1 erstellen',
   'Add General Ledger Transaction' => 'Dialogbuchen',
-  'Add Group'                   => 'Warengruppe erfassen',
-  'Add Language'                => 'Sprache hinzufügen',
-  'Add Lead'                    => 'Kundenquelle erfassen',
+  'Add Invoice for Advance Payment' => 'Anzahlungsrechnung erfassen',
   'Add Letter'                  => 'Brief hinzufügen',
   'Add Part'                    => 'Ware erfassen',
   'Add Price Factor'            => 'Preisfaktor erfassen',
-  'Add Pricegroup'              => 'Preisgruppe erfassen',
   'Add Printer'                 => 'Drucker hinzufügen',
   'Add Project'                 => 'Projekt erfassen',
   'Add Purchase Delivery Order' => 'Lieferschein (Einkauf) erfassen',
   'Add Purchase Order'          => 'Lieferantenauftrag erfassen',
   'Add Quotation'               => 'Angebot erfassen',
   'Add RFQ'                     => 'Preisanfrage erfassen',
+  'Add RMA Delivery Order'      => 'Retouren-Lieferschein erfassen',
   'Add Request for Quotation'   => 'Anfrage erfassen',
   'Add Requirement Spec'        => 'Pflichtenheft erfassen',
   'Add Requirement Spec Template' => 'Pflichtenheftvorlage erfassen',
@@ -172,136 +199,200 @@ $self->{texts} = {
   'Add Sales Order'             => 'Auftrag erfassen',
   'Add Service'                 => 'Dienstleistung erfassen',
   'Add Storno Credit Note'      => 'Gutschrift Storno hinzufügen',
+  'Add Supplier Delivery Order' => 'Beistell-Lieferschein erfassen',
   'Add Transaction'             => 'Dialogbuchen',
   'Add User'                    => 'Neuer Benutzer',
   'Add User Group'              => 'Neue Benutzergruppe',
   'Add Vendor'                  => 'Lieferant erfassen',
   'Add Vendor Invoice'          => 'Einkaufsrechnung erfassen',
   'Add Warehouse'               => 'Lager erfassen',
-  'Add and edit units'          => 'Einheiten erfassen und bearbeiten',
+  'Add acceptance status'       => 'Abnahmestatus hinzufügen',
   'Add bank account'            => 'Bankkonto erfassen',
+  'Add booking group'           => 'Buchungsgruppe erfassen',
+  'Add business'                => 'Kunden-/Lieferantentyp hinzufügen',
+  'Add complexity'              => 'Komplexitätsgrad hinzufügen',
+  'Add counted'                 => 'Hinzufügen',
+  'Add custom data export query' => 'Benutzerdefinierte Datenexport-Abfrage erfassen',
   'Add custom variable'         => 'Benutzerdefinierte Variable erfassen',
+  'Add department'              => 'Abteilung hinzufügen',
+  'Add document for AP transactions' => 'Dokumente für Kreditorenbuchung hinzufügen (benötigt DMS)',
+  'Add document for AR transactions' => 'Dokumente für Debitorenbuchung hinzufügen (benötigt DMS)',
+  'Add document for GL transactions' => 'Dokumente für Dialogbuchung hinzufügen (benötigt DMS)',
+  'Add document for Purchase invoices' => 'Dokumente für Einkaufsrechnung hinzufügen (benötigt DMS)',
+  'Add empty line (csv_import)' => 'Leere Zeile einfügen',
   'Add function block'          => 'Funktionsblock hinzufügen',
+  'Add greeting'                => 'Anrede hinzufügen',
+  'Add headers from last uploaded file (csv_import)' => 'Spalten aus der hochgeladenen Datei einfügen',
   'Add invoices'                => 'Rechnungen hinzufügen',
+  'Add language'                => 'Sprache hinzufügen',
   'Add link: select records to link with' => 'Verknüpfungen hinzufügen: zu verknüpfende Belege auswählen',
   'Add linked record'           => 'Verknüpften Beleg hinzufügen',
   'Add links'                   => 'Verknüpfungen hinzufügen',
+  'Add multiple items'          => 'Mehrere Artikel hinzufügen',
   'Add new currency'            => 'Neue Währung hinzufügen',
   'Add new custom variable'     => 'Neue benutzerdefinierte Variable erfassen',
   'Add new price rule item'     => 'Neue Bedingung hinzufügen',
+  'Add new record template'     => 'Neue Belegvorlage hinzufügen',
   'Add note'                    => 'Notiz erfassen',
+  'Add open Credit Notes'       => 'Offene Gutschriften hinzufügen',
   'Add part'                    => 'Artikel hinzufügen',
+  'Add part classification'     => 'Artikel-Klassifizierung hinzufügen',
+  'Add partsgroup'              => 'Warengruppe hinzufügen',
   'Add picture'                 => 'Bild hinzufügen',
   'Add picture to text block'   => 'Bild dem Textblock hinzufügen',
+  'Add pre-defined text'        => 'Vordefinierten Textblock hinzufügen',
+  'Add pricegroup'              => 'Preisgruppe hinzufügen',
+  'Add project status'          => 'Projektstatus hinzufügen',
+  'Add project type'            => 'Projekttypen hinzufügen',
+  'Add requirement spec status' => 'Pflichtenheftstatus hinzufügen',
+  'Add requirement spec type'   => 'Pflichtenhefttypen hinzufügen',
+  'Add risk level'              => 'Risikograd hinzufügen',
   'Add section'                 => 'Abschnitt hinzufügen',
+  'Add shop'                    => 'Webshop hinzufügen',
   'Add sub function block'      => 'Unterfunktionsblock hinzufügen',
   'Add taxzone'                 => 'Steuerzone hinzufügen',
   'Add text block'              => 'Textblock erfassen',
-  'Add unit'                    => 'Einheit hinzuf&uuml;gen',
+  'Add time recording article'  => 'Artikel für Zeiterfassung erfassen',
+  'Add title'                   => 'Titel hinzufügen',
+  'Add unit'                    => 'Einheit hinzufügen',
   'Added sections and function blocks: #1' => 'Hinzugefügte Abschnitte und Funktionsblöcke: #1',
   'Added text blocks: #1'       => 'Hinzugefügte Textblöcke: #1',
+  'Addition'                    => 'Zusatz',
+  'Additional Billing Address'  => 'Zusätzliche Rechnungsadresse',
+  'Additional Billing Addresses' => 'Zusätzliche Rechnungsadressen',
   'Additional articles'         => 'Zusätzliche Artikel',
   'Additional articles actions' => 'Aktionen zu zusätzlichen Artikeln',
   'Additionally the invoice is marked for direct debit and would have been checked automatically had the bank information been entered.' => 'Weiterhin ist die Rechnung für Lastschrifteinzug vorgesehen und wäre standardmäßig ausgewählt, wenn die Bankinformationen eingetragen wären.',
   'Additionally the invoice is not marked for direct debit and would have been checked automatically had the bank information been entered.' => 'Weiterhin ist die Rechnung nicht für Lastschrifteinzug vorgesehen und wäre standardmäßig ausgewählt, wenn die Bankinformationen eingetragen wären.',
   'Address'                     => 'Adresse',
+  'Address deleted.'            => 'Adresse gelöscht',
+  'Address is in use and was flagged invalid.' => 'Adresse wurde benutzt und wird nur als ungültig markiert',
   'Administration'              => 'Administration',
   'Administration area'         => 'Administration',
   'Advance turnover tax return' => 'Umsatzsteuervoranmeldung',
+  'Advance turnover tax return only valid for SKR03 or SKR04' => 'UstVA nur für Standardkontenrahmen SKR03 oder SKR04 möglich.',
   'After closed period'         => 'Ab geschlossenem Zeitraum',
   'Aktion'                      => 'Aktion',
   'All'                         => 'Alle',
   'All Accounts'                => 'Alle Konten',
+  'All Data'                    => 'Alle Daten',
   'All as list'                 => 'Alle als Liste',
-  'All changes in that file have been reverted.' => 'Alle &Auml;nderungen in dieser Datei wurden r&uuml;ckg&auml;ngig gemacht.',
+  'All changes in that file have been reverted.' => 'Alle Änderungen in dieser Datei wurden rückgängig gemacht.',
   'All clients'                 => 'Alle Mandanten',
+  'All employees'               => 'Alle Angestellten',
   'All general ledger entries'  => 'Alle Hauptbucheinträge',
   'All groups'                  => 'Alle Gruppen',
-  'All of the exports you have selected were already closed.' => 'Alle von Ihnen ausgewählten Exporte sind bereits abgeschlossen.',
+  'All modules'                 => 'Alle Module',
   'All partsgroups'             => 'Alle Warengruppen',
+  'All pay postings successfully imported.' => 'Alle Lohnbuchungen erfolgreich importiert.',
+  'All payments have already been posted.' => 'Es wurden bereits alle Zahlungen verbucht.',
+  'All payments must be posted before the payment list can be downloaded.' => 'Alle Zahlungen müssen verbucht werden, bevor die Zahlungsliste heruntergeladen werden kann.',
+  'All phone numbers'           => 'Alle Telefonnummern',
   'All price sources'           => 'Alle Preisquellen',
-  'All reports'                 => 'Alle Berichte (Konten&uuml;bersicht, Summen- u. Saldenliste, GuV, BWA, Bilanz, Projektbuchungen)',
-  'All the other clients will start with an empty set of WebDAV folders.' => 'Alle anderen Mandanten werden mit einem leeren Satz von WebDAV-Ordnern ausgestattet.',
+  'All reports'                 => 'Alle Berichte (Kontenübersicht, Summen- u. Saldenliste, Erfolgsrechnung, GuV, BWA, Bilanz, Projektbuchungen)',
+  'All the other clients will start with an empty set of WebDAV folders.' => 'Alle anderen Mandanten werden mit einem leeren Satz von Dokumenten-Ordnern ausgestattet.',
   'All the selected exports have already been closed, or all of their items have already been executed.' => 'Alle ausgewählten Exporte sind als abgeschlossen markiert, oder für alle Einträge wurden bereits Zahlungen verbucht.',
   'All transactions'            => 'Alle Buchungen',
   'All units have either no or exactly one base unit of which they are multiples.' => 'Einheiten haben entweder keine oder genau eine Basiseinheit, von der sie ein Vielfaches sind.',
   'All users'                   => 'Alle BenutzerInnen',
+  'Allocations didn\'t pass constraints' => 'Keine Verfügbarkeit wegen Lagereinschränkung',
   'Allow access'                => 'Zugriff erlauben',
   'Allow conversion from sales orders to sales invoices' => 'Umwandlung von Verkaufsaufträgen in Verkaufsrechnungen zulassen',
   'Allow conversion from sales quotations to sales invoices' => 'Umwandlung von Verkaufsangeboten in Verkaufsrechnungen zulassen',
   'Allow direct creation of new purchase delivery orders' => 'Direktes Anlegen neuer Einkaufslieferscheine zulassen',
   'Allow direct creation of new purchase invoices' => 'Direktes Anlegen neuer Einkaufsrechnungen zulassen',
   'Allow the following users access to my follow-ups:' => 'Erlaube den folgenden Benutzern Zugriff auf meine Wiedervorlagen:',
-  'Already as letter saved.'    => 'Wurde schon als Brief gespeichert.',
-  'Alternatively you can create a new part which will then be selected.' => 'Sie k&ouml;nnen auch einen neuen Artikel anlegen, der dann automatisch ausgew&auml;hlt wird.',
+  'Allow to delete generated printfiles' => 'Löschen von erzeugten Dokumenten erlaubt',
+  'Already counted'             => 'Bereits erfasst',
+  'Already imported entries (duplicates)' => 'Bereits importierte Einträge (Duplikate)',
+  'Already imported: '          => 'Bereits importiert:',
+  'Always edit assembly items (user can change/delete items even if assemblies are already produced)' => 'Erzeugnisbestandteile verändern (Löschen/Umsortieren) auch nachdem dieses Erzeugnis schon produziert wurde.',
+  'Always edit assortment items (user can change/delete items even if assortments are already used)' => 'Sortimentsbestandteile verändern (Löschen/Umsortieren), auch nachdem dieses Sortiment schon verwendet wurde.',
+  'Always save orders with a projectnumber (create new projects)' => 'Aufträge immer mit Projektnummer speichern (neue Projekte erstellen)',
   'Amended Advance Turnover Tax Return' => 'Berichtigte Anmeldung',
-  'Amended Advance Turnover Tax Return (Nr. 10)' => 'Ist dies eine berichtigte Anmeldung? (Nr. 10/Zeile 15 Steuererklärung)',
   'Amount'                      => 'Betrag',
   'Amount (for verification)'   => 'Betrag (zur Überprüfung)',
   'Amount BB'                   => 'Betrag Buchungen',
   'Amount BT'                   => 'Betrag Bank',
   'Amount Due'                  => 'Betrag fällig',
   'Amount and net amount are calculated by kivitendo. "verify_amount" and "verify_netamount" can be used for sanity checks.' => 'Betrag und Nettobetrag werden von kivitendo berechnet. "verify_amount" und "verify_netamount" können für Plausibilitätsprüfungen angegeben werden.',
+  'Amount has wrong format.'    => 'Betrag hat falsches Format.',
   'Amount less skonto'          => 'Betrag abzgl. Skonto',
   'Amount payable'              => 'Noch zu bezahlender Betrag',
   'Amount payable less discount' => 'Noch zu bezahlender Betrag abzüglich Skonto',
+  'Amounts differ too much'     => 'Beträge weichen zu sehr voneinander ab.',
+  'An error occurred while transferring the file.' => 'Bei Übertragung der Datei trat ein Fehler auf',
+  'An error occurred. Letter could not be deleted.' => 'Es ist ein Fehler aufgetreten. Der Brief konnte nicht gelöscht werden.',
   'An exception occurred during execution.' => 'Während der Ausführung trat eine Ausnahme auf.',
   'An invalid character was used (invalid characters: #1).' => 'Ein ungültiges Zeichen wurde benutzt (ungültige Zeichen: #1).',
   'An invalid character was used (valid characters: #1).' => 'Ein ungültiges Zeichen wurde benutzt (gültige Zeichen: #1).',
   'An upper-case character is required.' => 'Ein Großbuchstabe ist vorgeschrieben.',
+  'Analyze'                     => 'Analysieren',
   'Annotations'                 => 'Anmerkungen',
   'Any stock contents containing a best before date will be impossible to stock out otherwise.' => 'Sonst können Artikel, bei denen ein Mindesthaltbarkeitsdatum gesetzt ist, nicht mehr ausgelagert werden.',
-  'Ap aging on %s'              => 'Offene Verbindlichkeiten zum %s',
-  'Application Error. No Format given' => 'Fehler in der Anwendung. Das Ausgabeformat fehlt.',
+  'Ap aging on %s'              => 'Offene Verbindlichkeiten an %s',
   'Application Error. Wrong Format' => 'Fehler in der Anwendung. Falsches Format: ',
+  'Apply'                       => 'Anwenden',
+  'Apply customer'              => 'Kunde hinzufügen',
   'Apply to all parts'          => 'Bei allen Artikeln setzen',
   'Apply to all transfers'      => 'Bei allen Lagerbewegungen setzen',
-  'Apply to parts without buchungsgruppe' => 'Bei allen Artikeln ohne gültige Buchungsgruppe setzen',
+  'Apply to parts without booking group' => 'Bei allen Artikeln ohne gültige Buchungsgruppe setzen',
   'Apply to transfers without bin' => 'Bei allen Lagerbewegungen ohne Lagerplatz setzen',
   'Apply to transfers without comment' => 'Bei allen Lagerbewegungen ohne Kommentar setzen',
   'Apply to transfers without warehouse' => 'Bei allen Lagerbewegungen ohne Lager setzen',
+  'Apply year-end bookings'     => 'Jahresabschlußbuchungen durchführen',
   'Applying #1:'                => 'Führe #1 aus:',
   'Approximately #1 prices will be updated.' => 'Ungefähr #1 Preise werden aktualisiert.',
   'Apr'                         => 'Apr',
   'April'                       => 'April',
   'Ar aging on %s'              => 'Offene Forderungen zum %s',
+  'Are you sure to update all positions from master data?' => 'Alle Positionen aus den Stammdaten aktualisieren?',
+  'Are you sure to update this position from master data?' => 'Diese Position aus den Stammdaten aktualisieren?',
   'Are you sure you want to delete Invoice Number' => 'Soll die Rechnung mit folgender Nummer wirklich gelöscht werden:',
-  'Are you sure you want to delete Transaction' => 'Buchung wirklich löschen?',
-  'Are you sure you want to delete this background job?' => 'Sind Sie sicher, dass Sie diesen Hintergrund-Job löschen möchten?',
-  'Are you sure you want to delete this business?' => 'Sind Sie sicher, dass Sie diesen Kunden-/Lieferantentyp löschen wollen?',
-  'Are you sure you want to delete this delivery term?' => 'Wollen Sie diese Lieferbedingungen wirklich löschen?',
-  'Are you sure you want to delete this department?' => 'Sind Sie sicher, dass Sie diese Abteilung löschen wollen?',
-  'Are you sure you want to delete this payment term?' => 'Wollen Sie diese Zahlungsbedingungen wirklich löschen?',
+  'Are you sure you want to delete this letter?' => 'Sind Sie sicher, dass Sie diesen Brief löschen wollen?',
   'Are you sure you want to remove the marked entries from the queue?' => 'Sind Sie sicher, dass die markierten Einträge von der Warteschlange gelöscht werden sollen?',
   'Are you sure you want to update the prices' => 'Sind Sie sicher, dass Sie die Preise aktualisieren wollen?',
+  'Are you sure you want to update the selected record template with the current values? This cannot be undone.' => 'Sind Sie sicher, dass Sie die ausgewählte Belegvorlage mit den aktuellen Daten aktualisieren wollen? Das kann nicht rückgängig gemacht werden.',
   'Are you sure?'               => 'Sind Sie sicher?',
   'Article'                     => 'Artikel',
   'Article Code'                => 'Artikelkürzel',
-  'Article Code missing!'       => 'Artikelkürzel fehlt',
+  'Article classification'      => 'Artikel-Klassifizierung',
   'Article type'                => 'Artikeltyp',
-  'As a result, the saved onhand values of the present goods can be stored into a warehouse designated by you, or will be reset for a proper warehouse tracking' => 'Als Konsequenz k&ouml;nnen die gespeicherten Mengen entweder in ein Lager &uuml;berf&uuml;hrt werden, oder f&uuml;r eine frische Lagerverwaltung resettet werden.',
+  'Articles'                    => 'Artikel',
+  'As a result, the saved onhand values of the present goods can be stored into a warehouse designated by you, or will be reset for a proper warehouse tracking' => 'Als Konsequenz können die gespeicherten Mengen entweder in ein Lager überführt werden, oder für eine frische Lagerverwaltung resettet werden.',
   'Assemblies'                  => 'Erzeugnisse',
-  'Assemblies can not be imported (yet). But the type column is used for sanity checks on price updates in order to prevent that articles with the wrong type will be updated.' => 'Erzeugnisse können (noch) nicht importiert werden. Aber die Typ-Spalte wird für Plausibilitätsprüfungen bei Preisaktualisierungen verwendet, um zu verhindern, dass Artikel vom falschen Typ aktualisiert werden.',
   'Assembly'                    => 'Erzeugnis',
-  'Assembly Description'        => 'Erzeugnis-Beschreibung',
-  'Assembly Number'             => 'Erzeugnis-Nummer',
+  'Assembly (typeabbreviation)' => 'E',
+  'Assembly Item Qty'           => 'Menge für Erzeugnis',
+  'Assembly Last Cost'          => 'Erzeugnis-Einkaufspreis',
   'Assembly Number missing!'    => 'Erzeugnisnummer fehlt!',
+  'Assembly creation transfers services' => 'Erzeugnis fertigen berücksichtigt Dienstleistungen',
+  'Assembly creation warehouse dependent' => 'Erzeugnis fertigen ist lagerabhängig',
+  'Assembly items'              => 'Erzeugnisbestandteile',
   'Asset'                       => 'Aktiva/Mittelverwendung',
   'Assets'                      => 'Aktiva',
+  'Assign'                      => 'Übernehmen',
   'Assign article'              => 'Artikel zuweisen',
   'Assign invoice'              => 'Rechnung zuweisen',
   'Assign the following article to all sections' => 'Den folgenden Artikel allen Abschnitten zuweisen',
   'Assigned'                    => 'Zugewiesen',
-  'Assigned invoices'           => 'Zugewiesene Rechnungen',
+  'Assigned invoices with amount' => 'Zugewiesene Rechnungen mit Betrag',
+  'Assigned order must be a sales order.' => 'Zugeordneter Auftrag muss ein Verkaufsauftrag sein.',
   'Assignment of articles to sections' => 'Zuweisung von Artikeln zu Abschnitten',
   'Assistant for general ledger corrections' => 'Assistent für die Korrektur von Hauptbucheinträgen',
+  'Assortment'                  => 'Sortiment',
+  'Assortment (typeabbreviation)' => 'K',
+  'Assortment items'            => 'Sortimentsartikel',
   'Assume Tax Consultant Data in Tax Computation?' => 'Beraterdaten in UStVA übernehmen?',
   'At least'                    => 'Mindestens',
-  'At least one Perl module that kivitendo ERP requires for running is not installed on your system.' => 'Mindestes ein Perl-Modul, das kivitendo ERP zur Ausf&uuml;hrung ben&ouml;tigt, ist auf Ihrem System nicht installiert.',
-  'At least one of the columns #1, customer, customernumber, vendor, vendornumber (depending on the target table) is required for matching the entry to an existing customer or vendor.' => 'Mindestens eine der Spalten #1, customer, customernumber, vendor, vendornumber (von Zieltabelle abhängig) wird benötigt, um einen Eintrag einem bestehenden Kunden bzw. Lieferanten zuzuordnen.',
-  'At most'                     => 'H&ouml;chstens',
+  'At least #1 invoice(s) not saved' => 'Mindestens #1 Rechnung(en) nicht verarbeitet',
+  'At least one Perl module that kivitendo ERP requires for running is not installed on your system.' => 'Mindestes ein Perl-Modul, das kivitendo ERP zur Ausführung benötigt, ist auf Ihrem System nicht installiert.',
+  'At least one of the columns #1, customer, customernumber, customer_gln, vendor, vendornumber, vendor_gln (depending on the target table) is required for matching the entry to an existing customer or vendor.' => 'Mindestens eine der Spalten #1, customer, customernumber, customer_gln, vendor, vendornumber, vendor_gln (von Zieltabelle abhängig) wird benötigt, um einen Eintrag einem bestehenden Kunden bzw. Lieferanten zuzuordnen.',
+  'At most'                     => 'Höchstens',
+  'At position'                 => 'An Position',
   'At the moment the transaction looks like this:' => 'Aktuell sieht die Buchung wie folgt aus:',
   'Attach PDF:'                 => 'PDF anhängen',
+  'Attached Filename'           => 'Name des Dateianhangs',
   'Attachment'                  => 'als Anhang',
   'Attachment name'             => 'Name des Anhangs',
   'Attachments'                 => 'Dateianhänge',
@@ -309,8 +400,10 @@ $self->{texts} = {
   'Audit Control'               => 'Bücherkontrolle',
   'Aug'                         => 'Aug',
   'August'                      => 'August',
+  'Austria'                     => 'Österreich',
   'Authentification database creation' => 'Anlegen der Datenbank zur Benutzerauthentifizierung',
   'Authentification tables creation' => 'Anlegen der Tabellen zur Benutzerauthentifizierung',
+  'Author'                      => 'Verfasser/in',
   'Auto Send?'                  => 'Auto. Versand?',
   'Automatic date calculation'  => 'Automatische Datumsberechnung',
   'Automatic deletion of leading, trailing and excessive (repetitive) spaces in customer or vendor names' => 'Automatisches Löschen von voran-/nachgestellten und aufeinanderfolgenden Leerzeichen im Kunden- oder Lieferantennamen',
@@ -321,6 +414,7 @@ $self->{texts} = {
   'Available'                   => 'Verfügbar',
   'Available Prices'            => 'Mögliche Preise',
   'Available qty'               => 'Lagerbestand',
+  'Available to all users'      => 'Für alle BenutzerInnen verfügbar',
   'BALANCE SHEET'               => 'BILANZ',
   'BB Balance'                  => 'Saldo Bank',
   'BIC'                         => 'BIC',
@@ -334,11 +428,14 @@ $self->{texts} = {
   'Background jobs and task server' => 'Hintergrund-Jobs und Task-Server',
   'Balance'                     => 'Bilanz',
   'Balance Sheet'               => 'Bilanz',
+  'Balance accounts'            => 'Bestandskonten',
   'Balance sheet date'          => 'Bilanzstichtag',
   'Balance startdate method'    => 'Methode zur Ermittlung des Startdatums für Bilanz',
+  'Balance with CB'             => 'Saldo mit SB',
   'Balances'                    => 'Salden',
   'Balancing'                   => 'Bilanzierung',
   'Bank'                        => 'Bank',
+  'Bank Account Id Number (Swiss)' => 'Bankkonto Identifikationsnummer (Schweiz)',
   'Bank Code'                   => 'BLZ',
   'Bank Code (long)'            => 'Bankleitzahl (BLZ)',
   'Bank Code Number'            => 'Bankleitzahl',
@@ -346,7 +443,9 @@ $self->{texts} = {
   'Bank Connections'            => 'Bankverbindungen',
   'Bank Import'                 => 'Kontoauszug importieren',
   'Bank Transaction'            => 'Bankkonto',
+  'Bank Transaction is in a closed period.' => 'Die Bankbewegung befindet sich innerhalb eines geschlossenen Zeitraums.',
   'Bank account'                => 'Bankkonto',
+  'Bank account id number invalid. Must be 6 digits.' => 'Bank Identifikationsnummer ungültig. (6-stellig)',
   'Bank accounts'               => 'Bankkonten',
   'Bank code'                   => 'Bankleitzahl',
   'Bank code of the goal/source' => 'Bankleitzahl von Ziel- oder Quellkonto',
@@ -355,13 +454,16 @@ $self->{texts} = {
   'Bank collection via SEPA'    => 'Bankeinzug via SEPA',
   'Bank collections via SEPA'   => 'Bankeinzüge via SEPA',
   'Bank transaction'            => 'Bankbuchung',
-  'Bank transaction with id #1 has already been linked to #2.' => 'Bankbuchung mit id #1 wurde schon mit #2 verlinkt.',
   'Bank transactions'           => 'Bankbewegungen',
   'Bank transactions MT940'     => 'Kontoauszug verbuchen',
+  'Bank transactions that either only have warnings or no message at all have been posted.' => 'Banktransaktionen, die entweder nur Warnungen oder gar keine Nachricht haben, wurden hingegen verbucht.',
+  'Bank transactions with errors have not been posted.' => 'Banktransaktionen mit Fehlern wurden nicht verbucht.',
   'Bank transfer amount'        => 'Überweisungssumme',
   'Bank transfer payment list for export #1' => 'Überweisungszahlungsliste für SEPA-Export #1',
   'Bank transfer via SEPA'      => 'Überweisung via SEPA',
   'Bank transfers via SEPA'     => 'Überweisungen via SEPA',
+  'Base Transaction Value'      => 'Basisumsatz',
+  'Base Transaction Value Currency Code' => 'WKZ Basisumsatz',
   'Base unit'                   => 'Basiseinheit',
   'Basic Data'                  => 'Basisdaten',
   'Basic Settings for the Requirement Spec' => 'Grundeinstellungen des Pflichtenheftes',
@@ -374,7 +476,7 @@ $self->{texts} = {
   'Bcc E-mail'                  => 'BCC (E-Mail)',
   'Because the useability gets worse if one partnumber is used for several parts (for example if you are searching a position for an invoice), partnumbers should be unique.' => 'Da die Benutzerfreundlichkeit durch doppelte Artikelnummern erheblich verschlechtert wird (zum Beispiel, wenn man einen Artikel für eine Rechnung sucht), sollten Artikelnummern eindeutig vergeben sein.',
   'Before saving a sales order, this article will be checked and a warning is generated.' => 'Vor dem Speichern eines Angebots oder Auftrags wird überprüft, ob die hier definierte Artikelnummer vorhanden ist (Versandkosten01, etc.) und eine entsprechende Hinweiswarnung angezeigt',
-  'Belegnummer'                 => 'Buchungsnummer',
+  'Belgium'                     => 'Belgien',
   'Beratername'                 => 'Beratername',
   'Beraternummer'               => 'Beraternummer',
   'Best Before'                 => 'Mindesthaltbarkeit',
@@ -386,45 +488,46 @@ $self->{texts} = {
   'Billed extra expenses'       => 'Abgerechnete Nebenkosten',
   'Billing Address'             => 'Rechnungsadresse',
   'Billing Periodicity'         => 'Abrechnungsperiodizität',
-  'Billing/shipping address (city)' => 'Rechnungsadresse (Stadt)',
-  'Billing/shipping address (country)' => 'Rechnungsadresse (Land)',
-  'Billing/shipping address (street)' => 'Rechnungsadresse (Straße)',
-  'Billing/shipping address (zipcode)' => 'Rechnungsadresse (PLZ)',
+  'Billing/shipping address (GLN)' => 'Rechnungs-/Lieferadresse (GLN)',
+  'Billing/shipping address (city)' => 'Rechnungs-/Lieferadresse (Stadt)',
+  'Billing/shipping address (country)' => 'Rechnungs-/Lieferadresse (Land)',
+  'Billing/shipping address (street)' => 'Rechnungs-/Lieferadresse (Straße)',
+  'Billing/shipping address (zipcode)' => 'Rechnungs-/Lieferadresse (PLZ)',
   'Bin'                         => 'Lagerplatz',
   'Bin (database ID)'           => 'Lagerplatz (Datenbank-ID)',
+  'Bin (name)'                  => 'Lagerplatz (Name)',
   'Bin From'                    => 'Quelllagerplatz',
   'Bin List'                    => 'Lagerliste',
   'Bin To'                      => 'Ziellagerplatz',
-  'Binding to the LDAP server as "#1" failed. Please check config/kivitendo.conf.' => 'Die Anmeldung am LDAP-Server als "#1" schlug fehl. Bitte &uuml;berpr&uuml;fen Sie die Angaben in config/kivitendo.conf.',
-  'Bins saved.'                 => 'Lagerpl&auml;tze gespeichert.',
-  'Bins that have been used in the past cannot be deleted anymore. For these bins there\'s no checkbox in the &quot;Delete&quot; column.' => 'Lagerpl&auml;tze, die bereits benutzt wurden, k&ouml;nnen nicht mehr gel&ouml;scht werden. Deswegen fehlt bei ihnen die Checkbox in der Spalte &quot;L&ouml;schen&quot;.',
+  'Binding to the LDAP server as "#1" failed. Please check config/kivitendo.conf.' => 'Die Anmeldung am LDAP-Server als "#1" schlug fehl. Bitte überprüfen Sie die Angaben in config/kivitendo.conf.',
+  'Bins'                        => 'Lagerplätze',
+  'Bins saved.'                 => 'Lagerplätze gespeichert.',
+  'Bins that have been used in the past cannot be deleted anymore. For these bins there\'s no checkbox in the &quot;Delete&quot; column.' => 'Lagerplätze, die bereits benutzt wurden, können nicht mehr gelöscht werden. Deswegen fehlt bei ihnen die Checkbox in der Spalte &quot;Löschen&quot;.',
   'Birthday'                    => 'Geburtstag',
   'Birthday (after conversion)' => 'Geburtstag (nach Umstellung)',
   'Birthday (before conversion)' => 'Geburtstag (vor Umstellung)',
   'Bis'                         => 'bis',
-  'Bis Konto: '                 => 'bis Konto: ',
-  'Block'                       => 'Block',
   'Body'                        => 'Text',
   'Body:'                       => 'Text:',
-  'Booking Date'                => 'Buchungsdatum',
+  'Booked'                      => 'gebucht',
+  'Booking group'               => 'Buchungsgruppe',
+  'Booking group #1 needs a valid expense account' => 'Buchungsgruppe #1 braucht ein gültiges Aufwandskonto',
+  'Booking group #1 needs a valid income account' => 'Buchungsgruppe #1 braucht ein gültiges Erfolgskonto',
+  'Booking group #1 needs a valid inventory account' => 'Buchungsgruppe #1 braucht ein gültiges Warenbestandskonto',
+  'Booking group (database ID)' => 'Buchungsgruppe (database ID)',
+  'Booking group (name)'        => 'Buchungsgruppe (name)',
+  'Booking groups'              => 'Buchungsgruppen',
+  'Booking needs at least one debit and one credit booking!' => 'Die Buchung benötigt mindestens eine Buchung im Soll eine im Haben!',
+  'Bookinggroup/Tax'            => 'Buchungsgruppe/Steuer',
   'Books are open'              => 'Die Bücher sind geöffnet.',
   'Books closed up to'          => 'Bücher abgeschlossen bis zum',
-  'Boolean variables: If the default value is non-empty then the checkbox will be checked by default and unchecked otherwise.' => 'Ja/Nein-Variablen: Wenn der Standardwert nicht leer ist, so wird die Checkbox standardm&auml;&szlig;ig angehakt.',
+  'Boolean variables: If the default value is non-empty then the checkbox will be checked by default and unchecked otherwise.' => 'Ja/Nein-Variablen: Wenn der Standardwert nicht leer ist, so wird die Checkbox standardmäßig angehakt.',
   'Both'                        => 'Beide',
+  'Both-sided'                  => 'Beidseitig',
   'Bottom'                      => 'Unten',
   'Bought'                      => 'Gekauft',
   'Break down by'               => 'Aufschlüsseln nach',
   'Break up the update and contact a service provider.' => 'Diese Option bricht das Update ab. Bitte kontaktieren Sie Ihren Administrator oder beauftragen einen Dienstleister.',
-  'Buchungsdatum'               => 'Buchungsdatum',
-  'Buchungsgruppe'              => 'Buchungsgruppe',
-  'Buchungsgruppe #1 needs a valid expense account' => 'Buchungsgruppe #1 braucht ein gültiges Aufwandskonto',
-  'Buchungsgruppe #1 needs a valid income account' => 'Buchungsgruppe #1 braucht ein gültiges Erfolgskonto',
-  'Buchungsgruppe #1 needs a valid inventory account' => 'Buchungsgruppe #1 braucht ein gültiges Warenbestandskonto',
-  'Buchungsgruppe (database ID)' => 'Buchungsgruppe (Datenbank-ID)',
-  'Buchungsgruppe (name)'       => 'Buchungsgruppe (Name)',
-  'Buchungsgruppen'             => 'Buchungsgruppen',
-  'Buchungskonto'               => 'Buchungskonto',
-  'Buchungsnummer'              => 'Buchungsnummer',
   'Business'                    => 'Kunden-/Lieferantentyp',
   'Business Discount'           => 'Kunden-/Lieferantentyp-Rabatt',
   'Business Number'             => 'Firmennummer',
@@ -436,13 +539,19 @@ $self->{texts} = {
   'CANCELED'                    => 'Storniert',
   'CB Transaction'              => 'SB-Buchung',
   'CB Transactions'             => 'SB-Buchungen',
+  'CC to Employee'              => 'CC an Mitarbeiter',
+  'CN'                          => 'Kd-Nr.',
   'CR'                          => 'H',
   'CSS style for pictures'      => 'CSS Style für Bilder',
-  'CSV'                         => 'CSV',
+  'CSV Export successful!'      => 'CSV-Export erfolgreich!',
+  'CSV export'                  => 'CSV-Export',
   'CSV export -- options'       => 'CSV-Export -- Optionen',
+  'CSV import: additional billing addresses' => 'CSV-Import: zusätzliche Rechnungsadressen',
+  'CSV import: ar transactions' => 'CSV Import: Debitorenbuchungen',
   'CSV import: bank transactions' => 'CSV Import: Bankbewegungen',
   'CSV import: contacts'        => 'CSV-Import: Ansprechpersonen',
   'CSV import: customers and vendors' => 'CSV-Import: Kunden und Lieferanten',
+  'CSV import: delivery orders' => 'CSV-Import: Lieferscheine',
   'CSV import: inventories'     => 'CSV-Import: Lagerbewegungen/-bestände',
   'CSV import: orders'          => 'CSV-Import: Aufträge',
   'CSV import: parts and services' => 'CSV-Import: Waren und Dienstleistungen',
@@ -452,28 +561,39 @@ $self->{texts} = {
   'Calculate'                   => 'Berechnen',
   'Calculate due date automatically' => 'Fälligkeitsdatum automatisch berechnen',
   'Calling #1 now'              => 'Wähle jetzt #1',
-  'Can not create that quantity with current stock' => 'Diese Anzahl kann mit dem gegenwärtigen Lagerbestand nicht hergestellt werden.',
+  'Can only delete the "Storno zu" part of the cancellation pair.' => 'Löschen von R(S) Rechnung nicht erlaubt. Löschen der entsprechenden "Storno zu" Gutschrift reaktiviert diese Rechnung wieder.',
+  'Can only save template if amounts,i.e. 1 for debit and credit are set.' => 'Kann die Vorlage nicht speichern. Es wird mindestens ein Betrag im Soll und im Haben benötigt (bspw. 1), damit bspw. Beträge aus Kontoauszügen korrekt gesetzt werden können.',
+  'Can\'t connect to shop. #1'  => 'Kann keine Verbindung zu Shop #1 herstellen.',
+  'Can\'t load item without a valid part.id' => 'Kann Artikel ohne gültige part.id nicht laden',
   'Cancel'                      => 'Abbrechen',
   'Cancel Accounts Payables Transaction' => 'Kreditorenbuchung stornieren',
   'Cancel Accounts Receivables Transaction' => 'Debitorenbuchung stornieren',
+  'Cancelling is disallowed. Either undo or balance the current payments until the open amount matches the invoice amount' => 'Storno verboten, da Zahlungen zum Beleg vorhanden sind. Entweder die Zahlungen löschen oder mit umgekehrten Vorzeichen ausbuchen, sodass der offene Betrag dem Rechnungsbetrag entspricht.',
+  'Cannot Post AP transaction with tax included!' => 'Kann diesen kreditorischen Beleg nicht mit "Steuer im Preis inbegriffen" verbuchen!',
+  'Cannot add Booking, reason: #1 DB: #2 ' => 'Kann die Buchung nicht hinzufügen, Grund: #1 DB: #2',
+  'Cannot allocate parts.'      => 'Es sind nicht genügend Artikel vorhanden',
+  'Cannot change transaction in a closed period!' => 'In einem bereits abgeschlossenen Zeitraum kann keine Buchung verändert werden!',
   'Cannot check correct WebDAV folder' => 'Kann nicht den richtigen WebDAV Pfad überprüfen',
+  'Cannot convert date.'        => 'Kann das Datum nicht verarbeiten',
   'Cannot delete account!'      => 'Konto kann nicht gelöscht werden!',
   'Cannot delete customer!'     => 'Kunde kann nicht gelöscht werden!',
   'Cannot delete default account!' => 'Das Standard-Konto kann nicht gelöscht werden!',
   'Cannot delete delivery order!' => 'Lieferschein kann nicht gelöscht werden!',
   'Cannot delete invoice!'      => 'Rechnung kann nicht gelöscht werden!',
-  'Cannot delete item!'         => 'Artikel kann nicht gelöscht werden!',
   'Cannot delete order!'        => 'Auftrag kann nicht gelöscht werden!',
   'Cannot delete quotation!'    => 'Angebot kann nicht gelöscht werden!',
   'Cannot delete transaction!'  => 'Buchung kann nicht gelöscht werden!',
   'Cannot delete vendor!'       => 'Lieferant kann nicht gelöscht werden!',
   'Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.' => 'Konnte keine passende Vorlage für diesen Druckauftrag finden. Bitte benachrichtigen Sie Ihren Vorlagenadministrator. Die folgenden Pfade wurden durchsucht: #1 ',
+  'Cannot get shippingOrderAddressId for #1' => 'Finde das Feld shippingOrderAddressId für #1 nicht.',
   'Cannot have a value in both Debit and Credit!' => 'Es kann nicht gleichzeitig Soll und Haben gebucht werden!',
   'Cannot post Payment!'        => 'Zahlung kann nicht gebucht werden!',
   'Cannot post Receipt!'        => 'Beleg kann nicht gebucht werden!',
   'Cannot post a transaction without a value!' => 'Eine Buchung ohne Betrag kann nicht vorgenommen werden!',
   'Cannot post invoice and/or transfer out! Error message:' => 'Rechnung kann nicht gebucht oder es kann nicht ausgelagert werden. Fehlermeldung:',
   'Cannot post invoice for a closed period!' => 'Das Rechnungsdatum fällt in einen abgeschlossen Zeitraum!',
+  'Cannot post invoice for advance payment with more than one tax' => 'Anzahlungsrechnung mit mehr als einem Steuersatz kann nicht gebucht werden',
+  'Cannot post invoice for advance payment with taxincluded' => 'Eine Anzahlungsrechnung mit Steuer im Preis inbegriffen kann nicht gebucht werden',
   'Cannot post invoice!'        => 'Rechnung kann nicht gebucht werden!',
   'Cannot post payment for a closed period!' => 'Es können keine Zahlungen für abgeschlossene Bücher gebucht werden!',
   'Cannot post payment!'        => 'Zahlung kann nicht gebucht werden!',
@@ -485,21 +605,30 @@ $self->{texts} = {
   'Cannot process payment for a closed period!' => 'Es kann keine Zahlung in einem abgeschlossenen Zeitraum verbucht werden!',
   'Cannot remove files!'        => 'Dateien können nicht gelöscht werden!',
   'Cannot revert a versioned copy.' => 'Eine versionierte Kopie selber kann nicht zurückgesetzt werden.',
+  'Cannot safely book imported bank transactions due to lax posting settings for payments' => 'Kann in diesem Mandanten die importierten Bankbewegungen nicht ordnungsgemäß verbuchen, da die Veränderbarkeit von Zahlungen nicht restriktiv genug gesetzt ist. Bitte wie gewohnt manuell ausbuchen oder die Veränderbarkeit von Zahlungen auf unveränderbar setzen.',
+  'Cannot safely unlink bank transactions, please set the posting configuration for payments to unchangeable.' => 'Bankbewegungen können nicht rückgängig gemacht werden, da die Veränderbarkeit von Zahlungen möglich ist (s.a. Mandantenkonfiguration, Reiter Buchungskonfiguration).',
   'Cannot save account!'        => 'Konto kann nicht gespeichert werden!',
   'Cannot save order!'          => 'Auftrag kann nicht gespeichert werden!',
   'Cannot save preferences!'    => 'Einstellungen können nicht gespeichert werden!',
   'Cannot save quotation!'      => 'Angebot kann nicht gespeichert werden!',
+  'Cannot send E-mail without customer given' => 'E-Mail kann nicht ohne Angabe eines Kunden versendet werden.',
+  'Cannot send E-mail without vendor given' => 'E-Mail kann nicht ohne Angabe eines Lieferanten versendet werden.',
   'Cannot stock negative amounts' => 'Negative Mengen können nicht eingelagert werden!',
   'Cannot stock without amount' => 'Kann nicht ohne Menge einlagern!',
   'Cannot storno invoice for a closed period!' => 'Das Rechnungsdatum der zu stornierenden Rechnung fällt in einen abgeschlossenen Zeitraum!',
   'Cannot storno storno invoice!' => 'Kann eine Stornorechnung nicht stornieren',
+  'Cannot transfer #1 qty with #2 serial number(s)' => 'Kann nicht die Menge von #1 mit #2 Seriennummer auslagern.',
   'Cannot transfer negative entries.' => 'Kann keine negativen Mengen auslagern.',
   'Cannot transfer negative quantities.' => 'Negative Mengen können nicht ausgelagert werden.',
   'Cannot transfer. <br> Reason:<br>#1' => 'Kann nicht ein-/auslagern. <br>Grund:<br>#1',
-  'Carry over shipping address' => 'Lieferadresse &uuml;bernehmen',
+  'Cannot undo delivery order transfer!' => 'Kann Lagerbewegung des Lieferscheins nicht zurücklagern!',
+  'Cannot unlink payment for a closed period!' => 'Ein oder alle Bankbewegungen befinden sich innerhalb einer geschloßenen Periode. ',
+  'Carry over account for year-end closing' => 'Saldenvortragskonto',
+  'Carry over shipping address' => 'Lieferadresse übernehmen',
   'Cash'                        => 'Zahlungsverkehr',
   'Cash accounting'             => 'Ist-Versteuerung',
   'Cash basis accounting'       => 'Einnahmen-Überschuss-Rechnung',
+  'Category'                    => 'Artikelkategorie',
   'Cc'                          => 'Cc',
   'Cc E-mail'                   => 'CC (E-Mail)',
   'Change default bin for this parts' => 'Standardlagerplatz für diese Waren ändern',
@@ -508,35 +637,46 @@ $self->{texts} = {
   'Changed text blocks: #1'     => 'Geänderte Textblöcke: #1',
   'Changes in this block are only sensible if the account is NOT a summary account AND there exists one valid taxkey. To select both Receivables and Payables only make sense for Payment / Receipt (i.e. account cash).' => 'Es ist nur sinnvoll Änderungen vorzunehmen, wenn das Konto KEIN Sammelkonto ist und wenn ein gültiger Steuerschlüssel für das Konto existiert. Gleichzeitig Haken bei Forderungen und Verbindlichkeiten zu setzen, macht auch NUR für den Zahlungsein- und Ausgang (bspw. Bank oder Kasse) Sinn.',
   'Changes to Receivables and Payables are only possible if no transactions to this account are posted yet.' => 'Änderungen bei Forderungen oder Verbindlichkeiten sind nur möglich, wenn dieses Konto noch nicht bebucht wurde.',
+  'Changing general ledger transaction has been disabled in the configuration.' => 'Das Verändern von Dialogbuchungen ist in der Konfiguration deaktiviert.',
+  'Changing invoices has been disabled in the configuration.' => 'Das Verändern von Rechnungen ist in der Konfiguration deaktiviert.',
+  'Charge'                      => 'Berechnen',
   'Charge Number'               => 'Chargennummer',
   'Charge number'               => 'Chargennummer',
+  'Chargenumbers'               => 'Chargennummern',
   'Charset'                     => 'Zeichensatz',
   'Chart'                       => 'Buchungskonto',
   'Chart Type'                  => 'Kontentyp',
   'Chart balance'               => 'Kontensaldo',
+  'Chart configuration overview regarding reports' => 'Kontenkonfigurationsübersicht bezüglich Berichte',
+  'Chart list'                  => 'Kontenliste',
   'Chart of Accounts'           => 'Kontenübersicht',
   'Chart picker'                => 'Kontenauswahl',
   'Chartaccounts connected to this Tax:' => 'Konten, die mit dieser Steuer verknüpft sind:',
+  'Charts'                      => 'Konten',
   'Check'                       => 'Scheck',
+  'Check Api'                   => 'Check Api',
   'Check Details'               => 'Bitte Angaben überprüfen',
+  'Check connectivity'          => 'Verbindungstest',
   'Check for duplicates'        => 'Dublettencheck',
-  'Check full signature'        => 'Volle Signatur prüfen',
   'Check on ap transaction'     => 'Prüfen bei Kreditorenbuchung',
   'Check on ar transaction'     => 'Prüfen bei Debitorenbuchung',
   'Check on gl transaction'     => 'Prüfen bei Dialogbuchung',
   'Check on purchase invoice'   => 'Prüfen bei Einkaufsrechnung',
   'Check on sales invoice'      => 'Prüfen bei Verkaufsrechnung',
   'Checks'                      => 'Schecks',
+  'Choose "continue" if you want to use this value. Choose "cancel" otherwise.' => 'Wählen Sie "Ok" um diesen Wert zu übernehmen. Andernfalls wählen Sie "Abbrechen".',
   'Choose Customer'             => 'Endkunde wählen:',
   'Choose Outputformat'         => 'Ausgabeformat auswählen...',
   'Choose Vendor'               => 'Händler wählen',
   'Choose a Tax Number'         => 'Bitte eine Steuernummer angeben',
   'Choose bank account for reconciliation' => 'Wählen Sie das Bankkonto für den Kontenabgleich',
   'City'                        => 'Stadt',
+  'Clear'                       => 'Löschen',
   'Clear fields'                => 'Felder leeren',
   'Cleared Balance'             => 'abgeschlossen',
   'Cleared/uncleared only'      => 'Status abgeglichen',
   'Clearing Tax Received (No 71)' => 'Verrechnung des Erstattungsbetrages erwünscht (Zeile 71)',
+  'Clearing account for advance payments' => 'Verrechnungskonto für Anzahlungen',
   'Client'                      => 'Mandant',
   'Client #1'                   => 'Mandant #1',
   'Client Configuration'        => 'Mandantenkonfiguration',
@@ -549,59 +689,74 @@ $self->{texts} = {
   'Client to configure the printers for' => 'Mandant, für den Drucker konfiguriert werden',
   'Clients this Group is valid for' => 'Mandanten, für die diese Gruppe gültig ist',
   'Clients this user has access to' => 'Mandanten, auf die Benutzer Zugriff hat',
-  'Close'                       => 'Übernehmen',
   'Close Books up to'           => 'Die Bücher abschließen bis zum',
+  'Close Details'               => 'Details schließen',
   'Close Flash'                 => 'Schließen',
   'Close SEPA exports'          => 'SEPA-Export abschließen',
   'Close Window'                => 'Fenster Schließen',
   'Close window'                => 'Fenster schließen',
   'Closed'                      => 'Geschlossen',
+  'Closing Balance'             => 'Abschlußsaldo',
   'Collective Orders only work for orders from one customer!' => 'Sammelaufträge funktionieren nur für Aufträge von einem Kunden!',
   'Column name'                 => 'Spaltenname',
   'Comma'                       => 'Komma',
   'Comment'                     => 'Kommentar',
+  'Commercial court'            => 'Amtsgericht',
   'Company'                     => 'Firma',
   'Company Name'                => 'Firmenname',
   'Company name'                => 'Firmenname',
+  'Company name and address'    => 'Firmenname und -adresse',
   'Company settings'            => 'Firmeneinstellungen',
+  'Company\'s email signature'  => 'Firmen-E-Mail-Signatur',
   'Compare to'                  => 'Gegenüberstellen zu',
   'Complexities'                => 'Komplexitätsgrade',
   'Complexity'                  => 'Komplexität',
+  'Component Test'              => 'Komponenten-Test',
   'Configuration'               => 'Konfiguration',
-  'Configuration of individual TODO items' => 'Konfiguration f&uuml;r die einzelnen Aufgabenlistenpunkte',
+  'Configuration of individual TODO items' => 'Konfiguration für die einzelnen Aufgabenlistenpunkte',
   'Configure'                   => 'Konfigurieren',
   'Confirm!'                    => 'Bestätigen Sie!',
   'Confirmation'                => 'Auftragsbestätigung',
   'Contact'                     => 'Kontakt',
+  'Contact Departments'         => 'Abteilungen von Ansprechpersonen',
   'Contact Person'              => 'Ansprechperson',
   'Contact Person (database ID)' => 'Ansprechperson (Datenbank-ID)',
   'Contact Person (name)'       => 'Ansprechperson (Name)',
+  'Contact Titles'              => 'Titel von Ansprechpersonen',
   'Contact deleted.'            => 'Ansprechperson gelöscht.',
   'Contact is in use and was flagged invalid.' => 'Die Ansprechperson ist noch in Verwendung und wurde deshalb nur als ungültig markiert.',
   'Contact person (surname)'    => 'Ansprechperson (Nachname)',
   'Contact persons'             => 'Ansprechpersonen',
+  'Contact to send to'          => 'An Ansprechperson schicken',
   'Contacts'                    => 'Ansprechpersonen',
   'Content'                     => 'Inhalt',
   'Continue'                    => 'Weiter',
   'Contra'                      => 'gegen',
+  'Contra Account'              => 'Gegenkonto',
   'Contrary to Reduced Master Data this will be shown as discount in records.' => 'Im Gegensatz zu Abschlag wird der Rabatt in Belegen ausgewiesen',
   'Conversion of "birthday" contact person attribute' => 'Umstellung des Kontaktpersonenfeldes "Geburtstag"',
   'Conversion to PDF failed: #1' => 'Konvertierung zu PDF schlug fehl: #1',
+  'Conversion:'                 => 'Konversion',
+  'Converting to deliveryorder' => 'Konvertiere zu Lieferschein',
   'Copies'                      => 'Kopien',
   'Copy'                        => 'Kopieren',
   'Copy address from master data' => 'Adresse aus Stammdaten kopieren',
   'Copy file from #1 to #2 failed: #3' => 'Kopieren der Datei von #1 nach #2 schlug fehl: #3',
   'Copy requirement spec'       => 'Pflichtenheft kopieren',
   'Copy template'               => 'Vorlage kopieren',
+  'Correct counted'             => 'Korrigieren',
   'Correct taxkey'              => 'Richtiger Steuerschlüssel',
-  'Cost'                        => 'Kosten',
+  'Cost Center'                 => 'Kostenstelle',
   'Costs'                       => 'Kosten',
-  'Could not execute printer command: #1' => 'Der Druckerbefehl konnte nicht ausgeführt werden: #1',
+  'Could not create new project #1' => 'Neues Projekt #1 kann nicht angelegt werden',
+  'Could not extract Factur-X/ZUGFeRD data, data and error message:' => 'Konnte keine Factur-X-/ZUGFeRD-Daten extrahieren, folgende Fehlermeldung und das PDF:',
+  'Could not find an entry for this part in the pricegroup.' => 'Konnte keinen Eintrag für diesen Artikel in der Preisgruppe finden.',
   'Could not load class #1 (#2): "#3"' => 'Konnte Klasse #1 (#2) nicht laden: "#3"',
   'Could not load class #1, #2' => 'Konnte Klasse #1 nicht laden: "#2"',
   'Could not load employee'     => 'Konnte Benutzer nicht laden',
   'Could not load this business' => 'Konnte diesen Kunden-/Lieferantentyp nicht laden',
   'Could not load this customer' => 'Konnte diesen Kunden nicht laden',
+  'Could not load this draft'   => 'Dieser Entwurf konnte nicht geladen werden',
   'Could not load this vendor'  => 'Konnte diesen Lieferanten nicht laden',
   'Could not print dunning.'    => 'Die Mahnungen konnten nicht gedruckt werden.',
   'Could not reconcile chosen elements!' => 'Die gewählten Elemente konnten nicht ausgeglichen werden!',
@@ -610,34 +765,24 @@ $self->{texts} = {
   'Could not update prices!'    => 'Preise konnten nicht aktualisiert werden!',
   'Country'                     => 'Land',
   'Create'                      => 'Anlegen',
-  'Create Assembly'             => 'Erzeugnis fertigen',
   'Create Chart of Accounts'    => 'Zu verwendender Kontenplan',
   'Create Dataset'              => 'Neue Datenbank anlegen',
   'Create Date'                 => 'Erstelldatum',
   'Create HTML'                 => 'HTML erzeugen',
   'Create PDF'                  => 'PDF erzeugen',
-  'Create a new acceptance status' => 'Einen neuen Abnahmestatus anlegen',
   'Create a new background job' => 'Einen neuen Hintergrund-Job anlegen',
-  'Create a new business'       => 'Einen neuen Kunden-/Lieferantentyp erfassen',
   'Create a new client'         => 'Einen neuen Mandanten anlegen',
-  'Create a new complexity'     => 'Einen Komplexitätsgrad anlegen',
   'Create a new delivery term'  => 'Neue Lieferbedingungen anlegen',
-  'Create a new department'     => 'Eine neue Abteilung erfassen',
   'Create a new group'          => 'Neue Benutzergruppe erfassen',
+  'Create a new part'           => 'Einen neuen Artikel anlegen',
   'Create a new payment term'   => 'Neue Zahlungsbedingungen anlegen',
-  'Create a new predefined text' => 'Einen neuen vordefinierten Textblock anlegen',
   'Create a new price rule'     => 'Neue Preisregel anlegen',
   'Create a new printer'        => 'Einen neuen Drucker anlegen',
   'Create a new project'        => 'Neues Projekt anlegen',
   'Create a new project and link to it.' => 'Neues Projekt anlegen und damit verknüpfen.',
-  'Create a new project status' => 'Einen neuen Projektstatus anlegen',
-  'Create a new project type'   => 'Einen neuen Projekttypen anlegen',
   'Create a new purchase price rule' => 'Neue Einkaufspreisregel anlegen',
   'Create a new requirement spec' => 'Ein neues Pflichtenheft anlegen',
-  'Create a new requirement spec status' => 'Einen neuen Pflichtenheftstatus anlegen',
   'Create a new requirement spec template' => 'Eine neue Pflichtenheftvorlage erfassen',
-  'Create a new requirement spec type' => 'Einen neuen Pflichtenhefttypen anlegen',
-  'Create a new risk level'     => 'Einen neuen Risikograd anlegen',
   'Create a new sales price rule' => 'Neue Verkaufspreisregel anlegen',
   'Create a new user'           => 'Einen neuen Benutzer anlegen',
   'Create a new user group'     => 'Eine neue Benutzergruppe erfassen',
@@ -648,13 +793,19 @@ $self->{texts} = {
   'Create and edit parts, services, assemblies' => 'Artikel, Dienstleistungen, Erzeugnisse erfassen und bearbeiten',
   'Create and edit projects'    => 'Projekte erfassen und bearbeiten',
   'Create and edit purchase delivery orders' => 'Lieferscheine von Lieferanten erfassen und bearbeiten',
-  'Create and edit purchase orders' => 'Lieferantenauftr&auml;ge erfassen und bearbeiten',
+  'Create and edit purchase orders' => 'Lieferantenaufträge erfassen und bearbeiten',
   'Create and edit requirement specs' => 'Pflichtenhefte erstellen und bearbeiten',
-  'Create and edit sales delivery orders' => 'Lieferscheine f&uuml;r Kunden erfassen und bearbeiten',
-  'Create and edit sales orders' => 'Auftragsbest&auml;tigungen erfassen und bearbeiten',
+  'Create and edit sales delivery orders' => 'Lieferscheine für Kunden erfassen und bearbeiten',
+  'Create and edit sales orders' => 'Auftragsbestätigungen erfassen und bearbeiten',
   'Create and edit sales quotations' => 'Angebote erfassen und bearbeiten',
+  'Create and edit shopparts'   => 'Webshopartikel anlegen und bearbeiten',
   'Create and edit vendor invoices' => 'Eingangsrechnungen erfassen und bearbeiten',
+  'Create and edit webshops'    => 'Webshopeinstellungen bearbeiten',
   'Create and print all invoices' => 'Alle Rechnungen erzeugen und ausdrucken',
+  'Create and print invoices'   => 'Rechnungen erzeugen und ausdrucken',
+  'Create and print invoices for all delivery orders matching the filter' => 'Rechnungen für alle den Suchkriterien entsprechenden Lieferscheine erzeugen und ausdrucken',
+  'Create and print invoices for all selected delivery orders' => 'Rechnungen für alle markierten Lieferscheine erzeugen und ausdrucken',
+  'Create and send a new printout for this record' => 'Neuen Belegausdruck erstellen und verschicken',
   'Create bank collection'      => 'Bankeinzug erstellen',
   'Create bank collection via SEPA XML' => 'Bankeinzug via SEPA XML erstellen',
   'Create bank transfer'        => 'Überweisung erstellen',
@@ -664,31 +815,38 @@ $self->{texts} = {
   'Create first invoice on'     => 'Erste Rechnung erzeugen am',
   'Create invoice'              => 'Buchung erstellen',
   'Create invoice?'             => 'Rechnung erstellen?',
-  'Create invoices'             => 'Rechnungen erzeugen',
   'Create new'                  => 'Neu erfassen',
-  'Create new background job'   => 'Neuen Hintergrund-Job anlegen',
-  'Create new business'         => 'Kunden-/Lieferantentyp erfassen',
   'Create new client #1'        => 'Neuen Mandanten #1 anlegen',
-  'Create new delivery term'    => 'Neue Lieferbedingungen anlegen',
-  'Create new department'       => 'Neue Abteilung erfassen',
-  'Create new payment term'     => 'Neue Zahlungsbedingung anlegen',
-  'Create new project type'     => 'Neuen Projekttypen anlegen',
   'Create new quotation or order' => 'Neues Angebot oder neuen Auftrag anlegen',
   'Create new quotation/order'  => 'Neues Angebot/neuen Auftrag anlegen',
   'Create new qutoation/order'  => 'Neues Angebot/neuen Auftrag anlegen',
   'Create new templates from master templates' => 'Neue Druckvorlagen aus Vorlagensatz erstellen',
   'Create new version'          => 'Neue Version anlegen',
   'Create one from the context menu by right-clicking on this text.' => 'Erstellen Sie einen aus dem Kontextmenü, indem Sie auf diesen Text rechtsklicken.',
+  'Create order'                => 'Auftrag erstellen',
+  'Create sales invoices with Factur-X/ZUGFeRD data' => 'Verkaufsrechnungen mit Factur-X-/ZUGFeRD-Daten erzeugen',
+  'Create sales invoices with Swiss QR-bill' => 'Verkaufsrechnungen mit Schweizer QR-Rechnung erzeugen',
   'Create tables'               => 'Tabellen anlegen',
+  'Create variant IBAN without reference' => 'Variante IBAN ohne Referenz erzeugen',
+  'Create variant QR-IBAN with QR reference' => 'Variante QR-IBAN mit QR-Referenz erzeugen',
+  'Create with profile \'Factur-X 1.0.05/ZUGFeRD 2.1.1 extended\'' => 'Mit Profil »Factur-X 1.0.05/ZUGFeRD 2.1.1 extended«',
+  'Create with profile \'Factur-X 1.0.05/ZUGFeRD 2.1.1 extended\' (test mode)' => 'Mit Profil »Factur-X 1.0.05/ZUGFeRD 2.1.1 extended« (Test-Modus)',
+  'Create with profile \'XRechnung 2.0.0\'' => 'Mit Profil »XRechnung 2.0.0«',
+  'Create with profile \'XRechnung 2.0.0\' (test mode)' => 'Mit Profil »XRechnung 2.0.0« (Test-Modus)',
+  'Create, edit and list time recordings' => 'Zeiterfassungen erfassen, bearbeiten und ansehen',
   'Created by'                  => 'Erstellt von',
-  'Created for'                 => 'Erstellt f&uuml;r',
+  'Created for'                 => 'Erstellt für',
   'Created on'                  => 'Erstellt am',
+  'Creating Documents'          => 'Erzeuge Dokumente',
+  'Creating Factur-X/ZUGFeRD invoices is not enabled for this customer.' => 'Das Erzeugen von Factur-X/ZUGFeRD-Rechnungen ist für diesen Kunden nicht aktiviert.',
   'Creating invoices'           => 'Erzeuge Rechnungen',
   'Creating the PDF failed:'    => 'PDF-Erzeugung fehlgeschlagen:',
   'Creation Date'               => 'Erstelldatum',
   'Credit'                      => 'Haben',
   'Credit (one letter abbreviation)' => 'H',
   'Credit Account'              => 'Habenkonto',
+  'Credit Account Name'         => 'Haben-Kontoname',
+  'Credit Amount'               => 'Habenbetrag',
   'Credit Limit'                => 'Kreditlimit',
   'Credit Limit exceeded!!!'    => 'Kreditlimit überschritten!',
   'Credit Note'                 => 'Gutschrift',
@@ -696,11 +854,13 @@ $self->{texts} = {
   'Credit Note Number'          => 'Gutschriftnummer',
   'Credit Starting Balance'     => 'EB Aktiva',
   'Credit Tax'                  => 'Umsatzsteuer',
+  'Credit Tax (lit)'            => 'Habensteuer',
   'Credit Tax Account'          => 'Umsatzsteuerkonto',
   'Credit note (one letter abbreviation)' => 'G',
+  'Credit notes cannot be converted into other credit notes.' => 'Gutschriften können nicht in andere Gutschriften umgewandelt werden.',
   'Cumulated or averaged values' => 'Kumulierte oder gemittelte Werte',
   'Curr'                        => 'Währung',
-  'Currencies'                  => 'W&auml;hrungen',
+  'Currencies'                  => 'Währungen',
   'Currency'                    => 'Währung',
   'Currency (database ID)'      => 'Währung (Datenbank-ID)',
   'Currency name'               => 'Währungsname',
@@ -708,33 +868,47 @@ $self->{texts} = {
   'Currency names must not be empty.' => 'Währungsnamen dürfen nicht leer sein.',
   'Current / Next Level'        => 'Aktuelles / Nächstes Mahnlevel',
   'Current Earnings'            => 'Gewinn',
+  'Current Employee'            => 'Aktuelle Mitarbeiter',
   'Current assets account'      => 'Konto für Umlaufvermögen',
   'Current filter'              => 'Aktueller Filter',
   'Current picture'             => 'Aktuelles Bild',
   'Current profile'             => 'Aktuelles Profil',
   'Current status'              => 'Aktueller Status',
   'Current status:'             => 'Aktueller Status:',
+  'Current user\'s login'       => 'Login der aktuellen BenutzerIn',
   'Current value:'              => 'Aktueller Wert:',
   'Current version'             => 'Aktuelle Version',
   'Current year'                => 'Aktuelles Jahr',
   'Currently #1 delivery orders can be converted into invoices and printed.' => 'Momentan können #1 Lieferscheine in Rechnungen umgewandelt werden.',
+  'Custom Billing Address'      => 'Abweichende Rechnungsadresse',
+  'Custom CSV format'           => 'Eigenes CSV-Format',
   'Custom Variables'            => 'Benutzerdefinierte Variablen',
+  'Custom data export'          => 'Benutzerdefinierter Datenexport',
+  'Custom shipto'               => 'Individuelle Lieferadresse',
   'Custom variables for module' => 'Benutzerdefinierte Variablen für Modul',
   'Customer'                    => 'Kunde',
   'Customer (database ID)'      => 'Kunde (Datenbank-ID)',
   'Customer (name)'             => 'Kunde (Name)',
   'Customer Discount'           => 'Kundenrabatt',
+  'Customer GLN'                => 'GLN des Kunden',
   'Customer Master Data'        => 'Kundenstammdaten',
   'Customer Name'               => 'Kundenname',
   'Customer Number'             => 'Kundennummer',
   'Customer Order Number'       => 'Bestellnummer des Kunden',
+  'Customer Part Number'        => 'Kunden-Art-Nr.',
+  'Customer Price'              => 'Kundenpreis',
+  'Customer Proposals'          => 'Kundenvorschläge',
   'Customer deleted!'           => 'Kunde gelöscht!',
   'Customer details'            => 'Kundendetails',
   'Customer missing!'           => 'Kundenname fehlt!',
-  'Customer not on file or locked!' => 'Dieser Kunde existiert nicht oder ist gesperrt.',
-  'Customer not on file!'       => 'Kunde ist nicht in der Datenbank!',
+  'Customer must not be empty.' => 'Kunden darf nicht leer sein.',
+  'Customer not found'          => 'Kunde nicht gefunden',
+  'Customer number invalid. Must be less then or equal to 6 digits after prefix.' => 'Kundennummer ungültig. (kleiner/gleich 6 Stellen nach Prefix)',
+  'Customer of assigned order must match customer.' => 'Kunde des zugeordneten Auftrags muss mit dem gewählten Kunden übereinstimmen.',
+  'Customer of assigned project must match customer.' => 'Kunde des zugeordneten Projekts muss mit dem gewählten Kunden übereinstimmen.',
   'Customer saved'              => 'Kunde gespeichert',
   'Customer saved!'             => 'Kunde gespeichert!',
+  'Customer specific Price'     => 'Kundenpreis',
   'Customer type'               => 'Kundentyp',
   'Customer variables'          => 'Kundenvariablen',
   'Customer\'s Mandate Date of Signature' => 'Mandatsunterschriftsdatum des Kunden',
@@ -749,19 +923,25 @@ $self->{texts} = {
   'Customer/Vendor (database ID)' => 'Kunde/Lieferant (Datenbank-ID)',
   'Customer/Vendor Name'        => 'Kunde/Lieferant',
   'Customer/Vendor Number'      => 'Kunden-/Lieferantennummer',
-  'Customer/Vendor name'        => 'Kunden-/Lieferantenname',
-  'Customer/Vendor number'      => 'Kunden-/Lieferantennummer',
   'Customer/Vendor/Remote name' => 'Kunden/Lieferantenname laut Bank',
   'Customername'                => 'Kundenname',
+  'Customernumber'              => 'Kundennummer',
   'Customernumberinit'          => 'Kunden-/Lieferantennummernkreis',
+  'Customerorderlock'           => 'Shopauftragssperre',
   'Customers'                   => 'Kunden',
   'Customers and vendors'       => 'Kunden und Lieferanten',
+  'Customers: VAT ID / taxnumber unique' => 'Kunden: UStID / Steuernummer eindeutig',
   'Customized Report'           => 'Vorgewählte Zeiträume',
+  'Cutoff Date'                 => 'Stichtag',
+  'Czech Republic'              => 'Tschechien',
+  'DATEV'                       => 'DATEV',
   'DATEV - Export Assistent'    => 'DATEV-Exportassistent',
+  'DATEV - Pay Postings Import' => 'DATEV - Lohnbuchungsimport',
   'DATEV Angaben'               => 'DATEV-Angaben',
   'DATEV Export'                => 'DATEV-Export',
-  'DATEV check configuration'   => 'Einstellungen für DATEV-Prüfung',
   'DATEV check returned errors:' => 'Die DATEV Prüfung dieser Buchung ergab Fehler:',
+  'DATEV configuration'         => 'Einstellungen für DATEV',
+  'DATEV expects the encoding to be Western Europe conform (LATIN-1, cp1252). By setting this to "Strict and halt" the DATEV export halts with a error if there is a single character in "Posting Text" which is not LATIN-1 encodeable. By setting this to "Strict but replace" kivitendo will replace the character with a similar one and the export will simply warn about those fields. By setting this to relaxed (UTF-8) the DATEV export encoding will be in kivitendo (UTF-8) encoded and the external import program has to handle this (this may work for DATEV deriviates or future versions of DATEV). Background details: For example turkish characters (Ç) are not valid cp1252 charactes and armenian characters like "Գեղարդ" are probably not replaceable in cp1252' => 'DATEV erwartet eine westeuropäische Zeichenkodierung (LATIN-1, cp1252). Die Einstellung "Strikt und Abbruch" erlaubt keine nicht kodierbaren Zeichen im DATEV-Export und bricht diesen mit einer Fehlermeldung ab. Die Einstellung "Strikt mit Ersetzungen" versucht ähnliche Zeichen (bspw. c statt ć) zu verwenden und gibt zusätzlich eine Warnung beim DATEV-Export aus. Die Einstellung "Lax (UTF-8)" ignoriert diese Anforderung und übergibt die Daten im kivitendo-konformen UTF-8 Format. Letzteres kann für zukünftige DATEV-Version oder DATEV-kompatible Alternativen interessant sein. Hintergrund-Info: Beispielsweise sind schon türkische Zeichen (Ç) nicht mehr im westeuropäischen Zeichensätz enthalten und armenische Zeiche wie "Գեղարդ" könnnen sicherlich überhaupt nicht mit Zeichen in cp1252 ersetzt werden.',
   'DATEX - Export Assistent'    => 'DATEV-Exportassistent',
   'DELETED'                     => 'Gelöscht',
   'DFV-Kennzeichen'             => 'DFV-Kennzeichen',
@@ -770,16 +950,23 @@ $self->{texts} = {
   'DUNS number'                 => 'DUNS-Nummer',
   'DUNS-Nr'                     => 'DUNS-Nr.',
   'Data'                        => 'Daten',
+  'Data type'                   => 'Datentyp',
+  'DataSet #1'                  => 'Datensatz #1',
+  'DataSet for GoBD version #1. Created with kivitendo #2 by #3 (#4)' => 'Datenüberlassung nach GoBD vom #1. Erstellt mit kivitendo #2. Ansprechpartner ist #3 (#4)',
   'Database Administration'     => 'Datenbankadministration',
   'Database Connection Test'    => 'Test der Datenbankverbindung',
   'Database Host'               => 'Datenbankcomputer',
   'Database ID'                 => 'Datenbank-ID',
   'Database Management'         => 'Datenbankadministration',
+  'Database Superuser'          => 'Datenbank-Super-Benutzer',
   'Database User'               => 'Datenbankbenutzer',
+  'Database errors: #1'         => 'Datenbankfehler: #1',
   'Database host and port'      => 'Datenbankhost und -port',
   'Database login (#1)'         => 'Datenbankanmeldung (#1)',
   'Database name'               => 'Datenbankname',
   'Database settings'           => 'Datenbankeinstellungen',
+  'Database superuser privileges are required for parts of the database modifications.' => 'Für einige Teile der Datenbankänderungen werden Datenbank-Super-Benutzer-Rechte benötigt.',
+  'Database superuser privileges are required for the update.' => 'Datenbank-Super-Benutzer-Rechte werden für das Update benötigt.',
   'Database template'           => 'Datenbankvorlage',
   'Database update error:'      => 'Fehler beim Datenbankupgrade:',
   'Database user and password'  => 'Datenbankbenutzer und -passwort',
@@ -788,11 +975,12 @@ $self->{texts} = {
   'Date'                        => 'Datum',
   'Date Format'                 => 'Datumsformat',
   'Date Paid'                   => 'Zahlungsdatum',
-  'Date and timestamp variables: If the default value equals \'NOW\' then the current date/current timestamp will be used. Otherwise the default value is copied as-is.' => 'Datums- und Uhrzeitvariablen: Wenn der Standardwert \'NOW\' ist, so wird das aktuelle Datum/die aktuelle Uhrzeit eingef&uuml;gt. Andernfalls wird der Standardwert so wie er ist benutzt.',
+  'Date and timestamp variables: If the default value equals \'NOW\' then the current date/current timestamp will be used. Otherwise the default value is copied as-is.' => 'Datums- und Uhrzeitvariablen: Wenn der Standardwert \'NOW\' ist, so wird das aktuelle Datum/die aktuelle Uhrzeit eingefügt. Andernfalls wird der Standardwert so wie er ist benutzt.',
   'Date missing!'               => 'Datum fehlt!',
-  'Date of transaction'         => 'Buchungsdatum',
+  'Date of Last Payment'        => 'Letzter Zahlungseingang',
   'Date the payment is due in full' => 'Das Datum, bis die Rechnung in voller Höhe bezahlt werden muss',
   'Date the payment is due with discount' => 'Das Datum, bis die Rechnung unter Abzug von Skonto bezahlt werden kann',
+  'Datev export encoding'       => 'DATEV-Export Kodierung',
   'Datevautomatik'              => 'Datev-Automatik',
   'Datum von'                   => 'Datum von',
   'Deactivate by default'       => 'Deaktiviert als Voreinstellung',
@@ -800,16 +988,22 @@ $self->{texts} = {
   'Debit'                       => 'Soll',
   'Debit (one letter abbreviation)' => 'S',
   'Debit Account'               => 'Sollkonto',
+  'Debit Account Name'          => 'Soll-Kontoname',
+  'Debit Amount'                => 'Sollbetrag',
   'Debit Starting Balance'      => 'EB Passiva',
   'Debit Tax'                   => 'Vorsteuer',
+  'Debit Tax (lit)'             => 'Sollsteuer',
   'Debit Tax Account'           => 'Vorsteuerkonto',
   'Debit and credit out of balance!' => 'Soll und Haben müssen gleich sein.',
+  'Debit/Credit Label'          => 'Soll-/Haben-Kennzeichen',
   'Dec'                         => 'Dez',
   'December'                    => 'Dezember',
+  'December last year period'   => 'Dezember letzten Jahres',
   'Decimalplaces'               => 'Dezimalstellen',
   'Decrease'                    => 'Verringern',
   'Default (no language selected)' => 'Standard (keine Sprache ausgewählt)',
   'Default Accounts'            => 'Standardkonten',
+  'Default Billing Address'     => 'Standard-Rechnungsadresse',
   'Default Bin'                 => 'Standard-Lagerplatz',
   'Default Bin with ignoring onhand' => 'Standard-Lagerplatz für Auslagern ohne Prüfung auf Bestand',
   'Default Client (unconfigured)' => 'Standardmandant (unkonfiguriert)',
@@ -821,36 +1015,48 @@ $self->{texts} = {
   'Default Transfer with services' => 'Ein- /Auslagern von Dienstleistungen über Standard-Lagerplatz',
   'Default Warehouse'           => 'Standard-Lager',
   'Default Warehouse with ignoring onhand' => 'Standard-Lager für Auslagern ohne Prüfung auf Bestand',
+  'Default address flag'        => 'Standard-Adresse-Schalter',
   'Default article for converting into quotations and orders' => 'Standardartikel für Konvertierung von Pflichtenheften in Angebote und Aufträge',
-  'Default buchungsgruppe'      => 'Standardbuchungsgruppe',
+  'Default booking group'       => 'Standardbuchungsgruppe',
   'Default client'              => 'Standardmandant',
   'Default currency'            => 'Standardwährung',
   'Default currency missing!'   => 'Standardwährung fehlt!',
   'Default hourly rate for new customers' => 'Standard-Stundensatz für neue Kunden',
   'Default output medium'       => 'Standardausgabekanal',
+  'Default part for shipping costs' => 'Standardartikel für Lieferkosten',
   'Default printer'             => 'Standarddrucker',
   'Default taxzone'             => 'Standardsteuerzone',
   'Default template format'     => 'Standardvorlagenformat',
   'Default transfer delivery order' => 'Standard-Auslagern über Lieferschein',
   'Default transfer invoice'    => 'Standard-Auslagern über Rechnung',
+  'Default transfer invoice with charge number' => 'Standard-Auslagern über Rechnung mit Chargennummer',
   'Default transport article number' => 'Standard Versand / Transport-Erinnerungs-Artikel',
   'Default unit'                => 'Standardeinheit',
   'Default value'               => 'Standardwert',
+  'Defines the interval where undoing transfers from a delivery order are allowed.' => 'Zeitintervall in Tagen, an denen ein Zurücklagern der Lagerbewegung innerhalb eines Lieferscheins möglich ist.',
   'Delete'                      => 'Löschen',
   'Delete Account'              => 'Konto löschen',
+  'Delete Attachments'          => 'Anhänge löschen',
   'Delete Contact'              => 'Ansprechperson löschen',
   'Delete Dataset'              => 'Datenbank löschen',
+  'Delete Documents'            => 'Dokumente löschen',
+  'Delete Images'               => 'Bilder löschen',
   'Delete Shipto'               => 'Lieferadresse löschen',
-  'Delete drafts'               => 'Entwürfe löschen',
+  'Delete address'              => 'Adresse löschen',
+  'Delete all'                  => 'Alle Löschen',
+  'Delete for Customers'        => 'Bei Kunden löschen',
   'Delete links'                => 'Verknüpfungen löschen',
   'Delete picture'              => 'Bild löschen',
+  'Delete printfiles'           => 'Dokumente löschen',
   'Delete profile'              => 'Profil löschen',
   'Delete quotation/order'      => 'Angebot/Auftrag löschen',
   'Delete requirement spec'     => 'Pflichtenheft löschen',
+  'Delete shoporder'            => 'Shopbestellung löschen',
   'Delete template'             => 'Vorlage löschen',
   'Delete text block'           => 'Textblock löschen',
   'Delete transaction'          => 'Buchung löschen',
   'Deleted'                     => 'Gelöscht',
+  'Deleting this type of record has been disabled in the configuration.' => 'Das Löschen von dieser Belegart ist in der Konfiguration deaktiviert.',
   'Delivered'                   => 'Geliefert',
   'Delivered amount'            => 'Gelieferter Betrag',
   'Delivery Date'               => 'Lieferdatum',
@@ -858,12 +1064,14 @@ $self->{texts} = {
   'Delivery Order Date'         => 'Lieferscheindatum',
   'Delivery Order Date missing!' => 'Lieferscheindatum fehlt!',
   'Delivery Order Number'       => 'Lieferscheinnummer',
+  'Delivery Order Type'         => 'Lieferschein Typ',
   'Delivery Order created'      => 'Lieferschein erstellt',
   'Delivery Order deleted!'     => 'Lieferschein gelöscht!',
+  'Delivery Order has been deleted' => 'Lieferschein wurde gelöscht',
+  'Delivery Order has been saved' => 'Lieferschein wurde gespeichert',
   'Delivery Order(s) for full qty created' => 'Lieferschein(e) mit kompletter Menge erstellt',
   'Delivery Orders'             => 'Lieferscheine',
   'Delivery Plan'               => 'Lieferplan',
-  'Delivery Plan check for transferred delivery orders' => 'Lieferplan berücksichtig den Status des Lieferscheins (ausgelagert / nicht ausgelagert)',
   'Delivery Plan for currently outstanding purchase orders' => 'Lieferplan für nicht vollständig gelieferte Einkaufs-Aufträge',
   'Delivery Plan for currently outstanding sales orders' => 'Lieferplan für nicht vollständig gelieferte Verkaufsaufträge',
   'Delivery Terms'              => 'Lieferbedingungen',
@@ -873,22 +1081,24 @@ $self->{texts} = {
   'Delivery terms'              => 'Lieferbedingungen',
   'Delivery terms (database ID)' => 'Lieferbedingungen (Datenbank-ID)',
   'Delivery terms (name)'       => 'Lieferbedingungen (Name)',
+  'DeliveryOrder'               => 'Lieferschein',
+  'Denmark'                     => 'Dänemark',
   'Department'                  => 'Abteilung',
   'Department (database ID)'    => 'Abeilung (Datenbank-ID)',
   'Department (description)'    => 'Abteilung (Beschreibung)',
   'Department 1'                => 'Abteilung (1)',
   'Department 2'                => 'Abteilung (2)',
-  'Department Id'               => 'Reservierung',
   'Departments'                 => 'Abteilungen',
   'Dependencies'                => 'Abhängigkeiten',
-  'Dependency loop detected:'   => 'Schleife in den Abh&auml;ngigkeiten entdeckt:',
+  'Dependency loop detected:'   => 'Schleife in den Abhängigkeiten entdeckt:',
   'Deposit'                     => 'Gutschrift',
   'Description'                 => 'Beschreibung',
   'Description (Click on Description for details)' => 'Beschreibung (Klick öffnet einzelne Kontendetails)',
   'Description (translation for #1)' => 'Beschreibung (Übersetzung für #1)',
   'Description missing!'        => 'Beschreibung fehlt.',
-  'Description must not be empty!' => 'Beschreibung darf nicht leer sein',
+  'Description must not be empty.' => 'Beschreibung darf nicht leer sein.',
   'Description of #1'           => 'Beschreibung von #1',
+  'Design custom data export queries' => 'Benutzerdefinierte Datenexport-Abfragen designen',
   'Destination BIC'             => 'Ziel-BIC',
   'Destination IBAN'            => 'Ziel-IBAN',
   'Destination bin'             => 'Ziellagerplatz',
@@ -897,11 +1107,16 @@ $self->{texts} = {
   'Detail view'                 => 'Detailanzeige',
   'Details'                     => 'Details',
   'Details (one letter abbreviation)' => 'D',
+  'Details: #1'                 => 'Details: #1',
+  'Developer Tools'             => 'Developer Tools',
   'Dial command missing in kivitendo configuration\'s [cti] section' => 'Wählbefehl fehlt im Abschnitt [cti] der kivitendo-Konfiguration',
   'Difference'                  => 'Differenz',
   'Dimensions'                  => 'Abmessungen',
+  'Direct debit revoked'        => 'Die Einzugsermächtigung wird widerrufen',
   'Directory'                   => 'Verzeichnis',
   'Disabled Price Sources'      => 'Deaktivierte Preisquellen',
+  'Disassemble Assembly'        => 'Erzeugnis zerlegen',
+  'Disassembly successful for trans_id #1' => 'Erzeugnis für Transaktions-Id #1 erfolgreich zerlegt',
   'Discard duplicate entries in CSV file' => 'Doppelte Einträge in CSV-Datei verwerfen',
   'Discard entries with duplicates in database or CSV file' => 'Einträge aus CSV-Datei verwerfen, die es bereits in der Datenbank oder der CSV-Datei gibt',
   'Discount'                    => 'Rabatt',
@@ -909,66 +1124,92 @@ $self->{texts} = {
   'Discounts'                   => 'Rabatte',
   'Display'                     => 'Anzeigen',
   'Display file'                => 'Datei anzeigen',
+  'Display in basic data tab'   => 'Im Reiter Basisdaten anzeigen',
   'Display options'             => 'Anzeigeoptionen',
+  'Displayable Name Preferences' => 'Einstellungen für Anzeigenamen',
   'Do not change the tax rate of taxkey 0.' => 'Ändern Sie nicht den Steuersatz vom Steuerschlüssel 0.',
   'Do not check for duplicates' => 'Nicht nach Dubletten suchen',
+  'Do not create Factur-X/ZUGFeRD invoices' => 'Keine Factur-X-/ZUGFeRD-Rechnungen erzeugen',
+  'Do not create QR-bill invoices' => 'Keine QR-Rechnungen erzeugen',
+  'Do not leave booking form?'  => 'Buchungsmaske nicht verlassen?',
   'Do not link to a project.'   => 'Nicht mit einem Projekt verknüpfen.',
   'Do not modify this position' => 'Diese Position nicht verändern',
-  'Do not set default buchungsgruppe' => 'Nie Standardbuchungsgruppe setzen',
+  'Do not run the task server for this client' => 'Task-Server nicht für diesen Mandanten ausführen',
+  'Do not set default booking group' => 'Nie Standardbuchungsgruppe setzen',
   'Do not set this bin'         => 'Diesen Lagerplatz nicht setzen',
   'Do not set this comment'     => 'Diesen Kommentar nicht setzen',
   'Do not set this warehouse'   => 'Dieses Lager nicht setzen',
-  'Do you really want do continue?' => 'Wollen Sie wirklich fortfahren?',
-  'Do you really want to cancel?' => 'Wollen Sie wirklich abbrechen?',
-  'Do you really want to close the following SEPA exports? No payment will be recorded for bank collections that haven\'t been marked as executed yet.' => 'Wollen Sie wirklich die folgenden SEPA-Exporte abschließen? Für Überweisungen, die noch nicht gebucht wurden, werden dann keine Zahlungen verbucht.',
-  'Do you really want to close the following SEPA exports? No payment will be recorded for bank transfers that haven\'t been marked as executed yet.' => 'Wollen Sie wirklich die folgenden SEPA-Exporte abschließen? Für Überweisungen, die noch nicht gebucht wurden, werden dann keine Zahlungen verbucht.',
-  'Do you really want to delete AP transaction #1?' => 'Wollen Sie wirklich die Kreditorenbuchung #1 löschen?',
-  'Do you really want to delete AR transaction #1?' => 'Wollen Sie wirklich die Debitorenbuchung #1 löschen?',
-  'Do you really want to delete GL transaction #1?' => 'Wollen Sie wirklich die Dialogbuchung #1 löschen?',
-  'Do you really want to delete the selected links?' => 'Wollen Sie wirklich die ausgewählten Verknüpfungen löschen?',
-  'Do you really want to delete this object?' => 'Wollen Sie dieses Objekt wirklich löschen?',
-  'Do you really want to delete this warehouse?' => 'Wollen Sie dieses Lager wirklich l&ouml;schen?',
-  'Do you really want to revert to this version?' => 'Wollen Sie wirklich auf diese Version zurücksetzen?',
-  'Do you want to <b>limit</b> your search?' => 'Wollen Sie Ihre Suche <b>spezialisieren</b>?',
-  'Do you want to carry this shipping address over to the new purchase order so that the vendor can deliver the goods directly to your customer?' => 'Wollen Sie diese Lieferadresse in den neuen Lieferantenauftrag &uuml;bernehmen, damit der H&auml;ndler die Waren direkt an Ihren Kunden liefern kann?',
-  'Do you want to overwrite your current title?' => 'Wollen Sie den aktuellen Titel überschreiben?',
+  'Do you really want to cancel this general ledger transaction?' => 'Möchten Sie diese Dialogbuchung wirklich stornieren?',
+  'Do you really want to cancel this invoice?' => 'Möchten Sie diese Rechnung wirklich stornieren?',
+  'Do you really want to cancel?' => 'Möchten Sie wirklich abbrechen?',
+  'Do you really want to close the selected SEPA exports? No payment will be recorded for bank collections that haven\'t been marked as executed yet.' => 'Möchten Sie wirklich die ausgewählten SEPA-Exporte abschließen? Für Überweisungen, die noch nicht gebucht wurden, werden dann keine Zahlungen verbucht.',
+  'Do you really want to close the selected SEPA exports? No payment will be recorded for bank transfers that haven\'t been marked as executed yet.' => 'Möchten Sie wirklich die ausgewählten SEPA-Exporte abschließen? Für Überweisungen, die noch nicht gebucht wurden, werden dann keine Zahlungen verbucht.',
+  'Do you really want to continue?' => 'Möchten Sie wirklich fortfahren?',
+  'Do you really want to delete AP transaction #1?' => 'Möchten Sie wirklich die Kreditorenbuchung #1 löschen?',
+  'Do you really want to delete AR transaction #1?' => 'Möchten Sie wirklich die Debitorenbuchung #1 löschen?',
+  'Do you really want to delete GL transaction #1?' => 'Möchten Sie wirklich die Dialogbuchung #1 löschen?',
+  'Do you really want to delete the selected documents?' => 'Möchten Sie wirklich diese Dateien löschen?',
+  'Do you really want to delete the selected links?' => 'Möchten Sie wirklich die ausgewählten Verknüpfungen löschen?',
+  'Do you really want to delete the selected objects?' => 'Möchten Sie die ausgewählten Objekte wirklich löschen?',
+  'Do you really want to delete this draft?' => 'Möchten Sie diesen Entwurf wirklich löschen?',
+  'Do you really want to delete this object?' => 'Möchten Sie dieses Objekt wirklich löschen?',
+  'Do you really want to delete this record template?' => 'Möchten Sie diese Belegvorlage wirklich löschen?',
+  'Do you really want to mark the selected entries as booked?' => 'Möchten Sie die ausgewählten Einträge wirklich als gebucht markieren?',
+  'Do you really want to print?' => 'Wollen Sie wirklich drucken?',
+  'Do you really want to revert to this version?' => 'Möchten Sie wirklich auf diese Version zurücksetzen?',
+  'Do you really want to transfer the stock and set this order to delivered?' => 'Wollen Sie wirklich alle Lagerbewegungen durchführen?',
+  'Do you really want to undo the selected SEPA exports? You have to reassign the export again.' => 'Möchten Sie wirklich die ausgewählten SEPA-Exports rückgängig machen? Der Export muss anschließend neu erzeugt werden.',
+  'Do you really want to unimport the selected documents?' => 'Möchten Sie wirklich diese Dateien an die Quelle zurückgeben?',
+  'Do you want to <b>limit</b> your search?' => 'Möchten Sie Ihre Suche <b>spezialisieren</b>?',
+  'Do you want to carry this shipping address over to the new purchase order so that the vendor can deliver the goods directly to your customer?' => 'Möchten Sie diese Lieferadresse in den neuen Lieferantenauftrag übernehmen, damit der Händler die Waren direkt an Ihren Kunden liefern kann?',
+  'Do you want to overwrite your current title?' => 'Möchten Sie den aktuellen Titel überschreiben?',
   'Do you want to set the account number "#1" to "#2" and the name "#3" to "#4"?' => 'Soll die Kontonummer "#1" zu "#2" und den Name "#3" zu "#4" geändert werden?',
-  'Do you want to store the existing onhand values into a new warehouse?' => 'M&ouml;chten Sie die vorhandenen Mengendaten in ein Lager &uuml;bertragen?',
+  'Do you want to store the existing onhand values into a new warehouse?' => 'Möchten Sie die vorhandenen Mengendaten in ein Lager übertragen?',
   'Document'                    => 'Dokument',
+  'Document Count'              => 'Anz. PDF Belege',
   'Document Project (database ID)' => 'Projektnummer des Belegs (Datenbank-ID)',
   'Document Project (description)' => 'Projektnummer des Belegs (Beschreibung)',
   'Document Project (number)'   => 'Projektnummer des Belegs',
   'Document Project Number'     => 'Projektnummer des Belegs',
+  'Document generating failed. Please check Templates an LateX !' => 'Das Dokument konnte nicht erzeugt werden. Bitte Vorlagen und LateX prüfen!',
   'Documentation'               => 'Dokumentation',
   'Documentation (in German)'   => 'Dokumentation',
+  'Documents'                   => 'Dokumente',
   'Documents in the WebDAV repository' => 'Dokumente im WebDAV-Repository',
+  'Don\'t include a printout of the record with the email' => 'Keinen Belegausdruck mit E-Mail schicken',
+  'Don\'t include a printout of the record with the email, only selected files' => 'Keinen Belegausdruck mit E-Mail schicken, sondern nur ausgewählte Dateien',
   'Done'                        => 'Fertig',
   'Done.'                       => 'Fertig.',
   'Double partnumbers'          => 'Doppelte Artikelnummern',
+  'Download'                    => 'Download',
   'Download PDF'                => 'PDF herunterladen',
-  'Download PDF, do not print'  => 'PDF herunterladen, nicht drucken',
+  'Download PDF, do not print'  => 'Nicht drucken, sondern PDF herunterladen',
   'Download SEPA XML export file' => 'SEPA-XML-Exportdatei herunterladen',
+  'Download attachments of all parts' => 'Anhänge aller Artikel herunterladen',
+  'Download list of payments as PDF' => 'Zahlungsliste als PDF herunterladen',
   'Download picture'            => 'Bild herunterladen',
   'Download sample file'        => 'Beispieldatei herunterladen',
+  'Draft deleted'               => 'Entwurf gelöscht',
   'Draft for this Letter saved!' => 'Briefentwurf gespeichert!',
-  'Draft from:'                 => 'Entwurf vom:',
   'Draft saved.'                => 'Entwurf gespeichert.',
-  'Draft suggestions'           => 'Entwurfsvorschläge',
+  'Drafts'                      => 'Entwürfe',
+  'Drag and drop files here'    => 'Dateien hierher ziehen und fallen lassen',
   'Drawing'                     => 'Zeichnung',
-  'Dropdown Limit'              => 'Auswahllistenbegrenzung',
   'Due'                         => 'Fällig',
   'Due Date'                    => 'Fälligkeitsdatum',
   'Due Date missing!'           => 'Fälligkeitsdatum fehlt!',
   'Due to security concerns these files have to be deleted or moved after the migration before you can continue using kivitendo.' => 'Aus Sicherheitsgründen müssen diese Dateien nach erfolgter Migration gelöscht oder verschoben werden, bevor kivitendo weiter genutzt werden kann.',
-  'Duedate +Days'               => 'Fällikeitsdatum +Tage',
+  'Duedate +Days'               => 'Fälligkeitsdatum +Tage',
   'Dunned open amount: #1'      => 'Angemahnter, offener Betrag: #1',
   'Dunning'                     => 'Mahnung',
   'Dunning Amount'              => 'gemahnter Betrag',
+  'Dunning Creator'             => 'Mahnungsersteller',
   'Dunning Date'                => 'Mahndatum',
   'Dunning Date from'           => 'Mahnungen von',
   'Dunning Description'         => 'Mahnstufenbeschreibung',
   'Dunning Description missing in row ' => 'Mahnstufenbeschreibung fehlt in Zeile ',
   'Dunning Duedate'             => 'Zahlbar bis',
+  'Dunning Invoice'             => 'Mahnrechnung',
   'Dunning Level'               => 'Mahnlevel',
   'Dunning Level missing in row ' => 'Mahnlevel fehlt in ',
   'Dunning Process Config saved!' => 'Mahnwesenkonfiguration gespeichert!',
@@ -978,19 +1219,27 @@ $self->{texts} = {
   'Dunning overview'            => 'Mahnungsübersicht',
   'Dunning status'              => 'Mahnstatus',
   'Dunnings'                    => 'Mahnungen',
+  'Dunnings (Id -- Dunning Date --Dunning Level -- Dunning Fee)' => 'Mahnungen (Nummer -- Mahndatum -- Mahnstufe -- Mahngebühr/Zinsen)',
+  'Dunningstatistic'            => 'Mahnstatistik',
+  'Duplicate'                   => 'Duplikat',
   'Duplicate in CSV file'       => 'Duplikat in CSV-Datei',
   'Duplicate in database'       => 'Duplikat in Datenbank',
+  'Duration'                    => 'Dauer',
   'During the next update a taxkey 0 with tax rate of 0 will automatically created.' => 'Beim nächsten Ausführen des Updates wird ein Steuerschlüssel 0 mit einem Steuersatz von 0% automatisch erzeugt.',
+  'E Mail'                      => 'E-Mail',
+  'E-Mail'                      => 'E-Mail',
+  'E-Mail-Journal'              => 'E-Mail-Journal',
   'E-mail'                      => 'E-Mail',
-  'E-mail Statement to'         => 'Fälligkeitsabrechnung als E-Mail an',
   'E-mail address missing!'     => 'E-Mail-Adresse fehlt!',
+  'E.g. "<%customernumber%> <%name%>"' => 'Beispiel: "<%customernumber%> <%name%>"',
   'EAN'                         => 'EAN',
   'EAN-Code'                    => 'EAN-Code',
   'EB-Wert'                     => 'EB-Wert',
   'EK'                          => 'EK',
   'ELSE'                        => 'Zusatz',
-  'ELSTER Tax Number'           => 'ELSTER-Steuernummer',
+  'ELSTER Export (via Geierlein)' => 'ELSTER Export (via Geierlein)',
   'EQUITY'                      => 'EIGENTUM',
+  'EU Member State and VAT ID Number' => 'EU-Mitgliedstaat u. USt-IdNr.',
   'EUER'                        => 'Einnahmen-/Überschussrechnung',
   'Earlier versions of kivitendo contained bugs which might have led to wrong entries in the general ledger.' => 'Frühere Versionen von kivitendo enthielten Bugs, die zu falschen Einträgen im Hauptbuch geführt haben können.',
   'Edit'                        => 'Bearbeiten',
@@ -1000,29 +1249,29 @@ $self->{texts} = {
   'Edit Accounts Payables Transaction' => 'Kreditorenbuchung bearbeiten',
   'Edit Accounts Receivables Transaction' => 'Debitorenbuchung bearbeiten',
   'Edit Assembly'               => 'Erzeugnis bearbeiten',
-  'Edit Bins'                   => 'Lagerpl&auml;tze bearbeiten',
-  'Edit Buchungsgruppe'         => 'Buchungsgruppe bearbeiten',
+  'Edit Assortment'             => 'Sortiment bearbeiten',
+  'Edit Bins for Warehouse \'#1\'' => 'Lagerplätze von Lager »#1« bearbeiten',
   'Edit Client'                 => 'Mandanten bearbeiten',
   'Edit Credit Note'            => 'Gutschrift bearbeiten',
   'Edit Customer'               => 'Kunde editieren',
   'Edit Dunning'                => 'Mahnungen konfigurieren',
   'Edit Dunning Process Config' => 'Mahnwesenkonfiguration bearbeiten',
   'Edit Employee #1'            => 'Benutzer #1 bearbeiten',
+  'Edit Factur-X/ZUGFeRD notes' => 'Factur-X-/ZUGFeRD-Notizen bearbeiten',
+  'Edit Final Invoice'          => 'Schlussrechnung bearbeiten',
   'Edit Follow-Up'              => 'Wiedervorlage bearbeiten',
-  'Edit Follow-Up for #1'       => 'Wiedervorlage f&uuml;r #1 bearbeiten',
+  'Edit Follow-Up for #1'       => 'Wiedervorlage für #1 bearbeiten',
   'Edit General Ledger Transaction' => 'Buchung im Hauptbuch bearbeiten',
-  'Edit Group'                  => 'Warengruppe editieren',
-  'Edit Language'               => 'Sprache bearbeiten',
-  'Edit Lead'                   => 'Kundenquelle bearbeiten',
+  'Edit Invoice for Advance Payment' => 'Anzahlungsrechnung bearbeiten',
   'Edit Letter'                 => 'Brief bearbeiten',
   'Edit Part'                   => 'Ware bearbeiten',
   'Edit Preferences for #1'     => 'Einstellungen von #1 bearbeiten',
   'Edit Price Factor'           => 'Preisfaktor bearbeiten',
-  'Edit Pricegroup'             => 'Preisgruppe bearbeiten',
   'Edit Printer'                => 'Drucker bearbeiten',
   'Edit Purchase Delivery Order' => 'Lieferschein (Einkauf) bearbeiten',
   'Edit Purchase Order'         => 'Lieferantenauftrag bearbeiten',
   'Edit Quotation'              => 'Angebot bearbeiten',
+  'Edit RMA Delivery Order'     => 'Retouren-Lieferschein bearbeiten',
   'Edit Request for Quotation'  => 'Anfrage bearbeiten',
   'Edit SEPA strings'           => 'Begriffe bei SEPA-Überweisungen bearbeiten',
   'Edit Sales Delivery Order'   => 'Lieferschein (Verkauf) bearbeiten',
@@ -1031,6 +1280,8 @@ $self->{texts} = {
   'Edit Service'                => 'Dienstleistung bearbeiten',
   'Edit Storno Credit Note'     => 'Storno Gutschrift bearbeiten',
   'Edit Storno Invoice'         => 'Stornorechnung bearbeiten',
+  'Edit Storno Invoice for Advance Payment' => 'Storno-Anzahlungsrechnung bearbeiten',
+  'Edit Supplier Delivery Order' => 'Beistell-Lieferschein bearbeiten',
   'Edit User'                   => 'Benutzerdaten bearbeiten',
   'Edit User Group'             => 'Benutzergruppe bearbeiten',
   'Edit Vendor'                 => 'Lieferant editieren',
@@ -1038,29 +1289,40 @@ $self->{texts} = {
   'Edit Warehouse'              => 'Lager bearbeiten',
   'Edit acceptance status'      => 'Abnahmestatus bearbeiten',
   'Edit additional articles'    => 'Zusätzliche Artikel bearbeiten',
+  'Edit all drafts'             => 'Entwürfe von allen Benutzern bearbeiten',
   'Edit article/section assignments' => 'Zuweisung Artikel/Abschnitte bearbeiten',
   'Edit assignment of articles to sections' => 'Zuweisung Artikel zu Abschnitten bearbeiten',
   'Edit background job'         => 'Hintergrund-Job bearbeiten',
   'Edit bank account'           => 'Bankkonto bearbeiten',
+  'Edit booking group'          => 'Buchungsgruppe bearbeiten',
   'Edit business'               => 'Kunden-/Lieferantentyp bearbeiten',
   'Edit complexity'             => 'Komplexitätsgrad bearbeiten',
+  'Edit custom data export query' => 'Benutzerdefinierte Datenexport-Abfrage bearbeiten',
+  'Edit custom shipto'          => 'Individuelle Lieferadresse bearbeiten',
   'Edit custom variable'        => 'Benutzerdefinierte Variable bearbeiten',
   'Edit delivery term'          => 'Lieferbedingungen bearbeiten',
   'Edit department'             => 'Abteilung bearbeiten',
   'Edit file'                   => 'Datei bearbeiten',
   'Edit general settings'       => 'Grundeinstellungen bearbeiten',
+  'Edit greeting'               => 'Anrede bearbeiten',
   'Edit greetings'              => 'Anreden bearbeiten',
+  'Edit language'               => 'Sprache bearbeiten',
   'Edit note'                   => 'Notiz bearbeiten',
+  'Edit part classification'    => 'Artikel-Klassifizierung bearbeiten',
+  'Edit partsgroup'             => 'Warengruppe bearbeiten',
   'Edit payment term'           => 'Zahlungsbedingungen bearbeiten',
   'Edit picture'                => 'Bild bearbeiten',
-  'Edit predefined text'        => 'Vordefinierten Textblock bearbeiten',
+  'Edit pre-defined text'       => 'Vordefinierten Textblock bearbeiten',
+  'Edit preset email strings'   => 'Vorbelegte Texte für E-Mails editieren',
   'Edit price rule'             => 'Preisregel bearbeiten',
+  'Edit pricegroup'             => 'Preisgruppe bearbeiten',
   'Edit prices and discount (if not used, textfield is ONLY set readonly)' => 'Preise und Rabatt in Formularen frei anpassen (falls deaktiviert, wird allerdings NUR das textfield auf READONLY gesetzt / kann je nach Browserversion und technischen Fähigkeiten des Anwenders noch umgangen werden)',
   'Edit project'                => 'Projekt bearbeiten',
   'Edit project #1'             => 'Projekt #1 bearbeiten',
   'Edit project link'           => 'Projektverknüpfung bearbeiten',
   'Edit project status'         => 'Projektstatus bearbeiten',
   'Edit project type'           => 'Projekttypen bearbeiten',
+  'Edit purchase letters'       => 'Einkaufsbrief erstellen',
   'Edit purchase price rule'    => 'Einkaufspreisregel bearbeiten',
   'Edit requirement spec'       => 'Pflichtenheft bearbeiten',
   'Edit requirement spec status' => 'Pflichtenheftstatus bearbeiten',
@@ -1070,91 +1332,152 @@ $self->{texts} = {
   'Edit sales letters'          => 'Verkaufsbrief erstellen',
   'Edit sales price rule'       => 'Verkaufspreisregel bearbeiten',
   'Edit section #1'             => 'Abschnitt #1 bearbeiten',
+  'Edit shop'                   => 'Shopeigenschaften bearbeiten',
   'Edit taxzone'                => 'Steuerzone bearbeiten',
   'Edit templates'              => 'Vorlagen bearbeiten',
   'Edit text block'             => 'Textblock bearbeiten',
   'Edit text block \'#1\''      => 'Textblock \'#1\' bearbeiten',
   'Edit text block picture #1'  => 'Textblockbild #1 bearbeiten',
-  'Edit the Delivery Order'     => 'Lieferschein bearbeiten',
   'Edit the configuration for periodic invoices' => 'Konfiguration für wiederkehrende Rechnungen bearbeiten',
   'Edit the currency names in order to rename them.' => 'Bearbeiten Sie den Namen, um eine Währung umzubennen.',
   'Edit the purchase_order'     => 'Bearbeiten des Lieferantenauftrags',
   'Edit the request_quotation'  => 'Bearbeiten der Preisanfrage',
   'Edit the sales_order'        => 'Bearbeiten des Auftrags',
   'Edit the sales_quotation'    => 'Bearbeiten des Angebots',
-  'Edit the stylesheet'         => 'Stilvorlage bearbeiten',
+  'Edit time recording article' => 'Artikel für Zeiterfassung bearbeiten',
+  'Edit time recordings of all staff members' => 'Zeiterfassungseinträge aller Mitarbeiter bearbeiten',
+  'Edit title'                  => 'Titiel bearbeiten',
   'Edit units'                  => 'Einheiten bearbeiten',
-  'Edit user signature'         => 'Benutzersignatur bearbeiten',
   'Editable'                    => 'Bearbeitbar',
   'Either there are no open invoices, or you have already initiated bank transfers with the open amounts for those that are still open.' => 'Entweder gibt es keine offenen Rechnungen, oder es wurden bereits Überweisungen über die offenen Beträge aller offenen Rechnungen erstellt.',
   'Element disabled'            => 'Element deaktiviert',
+  'Email'                       => 'E-Mail',
+  'Email address'               => 'E-Mail-Adresse',
   'Email journal'               => 'E-Mail-Journal',
+  'Email of the delivery order recipient' => 'E-Mail des Lieferscheinempfängers',
+  'Email of the invoice recipient' => 'E-Mail des Rechnungsempfängers',
+  'Email signature'             => 'E-Mail-Signatur',
   'Employee'                    => 'Bearbeiter',
   'Employee #1 saved!'          => 'Benutzer #1 gespeichert!',
   'Employee (database ID)'      => 'Bearbeiter (Datenbank-ID)',
+  'Employee from the original invoice' => 'Mitarbeiter der Ursprungs-Rechnung',
+  'Employee must not be empty.' => 'Bearbeiter darf nicht leer sein.',
   'Employees'                   => 'Benutzer',
+  'Employees with read access to the project\'s invoices' => 'Angestellte mit Leserechten auf die Projektrechnungen',
   'Empty selection for warehouse will not be added, even if the old bin is still visible (use back and forth to edit again).' => 'Leere Lager-Auswahl wird ignoriert, selbst wenn noch ein Lagerplatz ausgewählt ist. Alle Daten können durch zurück und vorwärts korrigiert werden.',
   'Empty transaction!'          => 'Buchung ist leer!',
+  'Enabled Quick Searched'      => 'Aktivierte Schnellsuchen',
+  'Enabled modules'             => 'Aktivierte Module',
+  'End'                         => 'Ende',
   'End date'                    => 'Enddatum',
-  'Enter a description for this new draft.' => 'Geben Sie eine Beschreibung f&uuml;r diesen Entwurf ein.',
   'Enter longdescription'       => 'Langtext eingeben',
   'Enter the requested execution date or leave empty for the quickest possible execution:' => 'Geben Sie das jeweils gewünschte Ausführungsdatum an, oder lassen Sie das Feld leer für die schnellstmögliche Ausführung:',
   'Entries for which automatic conversion failed:' => 'Einträge, für die die automatische Umstellung fehlschlug:',
   'Entries for which automatic conversion succeeded:' => 'Einträge, für die die automatische Umstellung erfolgreich war:',
+  'Entries ready to import'     => 'Zu importierende Einträge',
+  'Entries with errors'         => 'Einträge mit Fehlern',
+  'Entry overlaps with "#1".'   => 'Einträg überlappt sich mit "#1"',
   'Equity'                      => 'Passiva',
+  'Erfolgsrechnung'             => 'Erfolgsrechnung',
   'Error'                       => 'Fehler',
+  'Error getting QR-Bill type.' => 'Fehler in QR-Rechnung Varianten Auswahl.',
+  'Error handling'              => 'Fehlerbehandlung',
   'Error in database control file \'%s\': %s' => 'Fehler in Datenbankupgradekontrolldatei \'%s\': %s',
-  'Error in position #1: You must either assign no stock at all or the full quantity of #2 #3.' => 'Fehler in Position #1: Sie m&uuml;ssen einer Position entweder gar keinen Lagereingang oder die vollst&auml;ndige im Lieferschein vermerkte Menge von #2 #3 zuweisen.',
-  'Error in position #1: You must either assign no transfer at all or the full quantity of #2 #3.' => 'Fehler in Position #1: Sie m&uuml;ssen einer Position entweder gar keinen Lagerausgang oder die vollst&auml;ndige im Lieferschein vermerkte Menge von #2 #3 zuweisen.',
-  'Error in row #1: The quantity you entered is bigger than the stocked quantity.' => 'Fehler in Zeile #1: Die angegebene Menge ist gr&ouml;&szlig;er als die vorhandene Menge.',
+  'Error in position #1: You must either assign no stock at all or the full quantity of #2 #3.' => 'Fehler in Position #1: Sie müssen einer Position entweder gar keinen Lagereingang oder die vollständige im Lieferschein vermerkte Menge von #2 #3 zuweisen.',
+  'Error in position #1: You must either assign no transfer at all or the full quantity of #2 #3.' => 'Fehler in Position #1: Sie müssen einer Position entweder gar keinen Lagerausgang oder die vollständige im Lieferschein vermerkte Menge von #2 #3 zuweisen.',
+  'Error in row #1: The quantity you entered is bigger than the stocked quantity.' => 'Fehler in Zeile #1: Die angegebene Menge ist größer als die vorhandene Menge.',
+  'Error mapping biller countrycode.' => 'Fehler beim Erzeugen des Ländercodes für Rechnungssteller.',
+  'Error mapping customer countrycode.' => 'Fehler beim Erzeugen des Ländercodes für Kunden.',
   'Error message from the database driver:' => 'Fehlermeldung des Datenbanktreibers:',
   'Error message from the database: #1' => 'Fehlermeldung der Datenbank: #1',
+  'Error message from the webshop api:' => 'Fehlermeldung der Webshop Api',
   'Error when saving: #1'       => 'Fehler beim Speichern: #1',
+  'Error while applying year-end bookings!' => 'Fehler beim Durchführen der Abschlußbuchungen!',
+  'Error while creating project with project number of new order number, project number #1 already exists!' => 'Fehler beim Erstellen eines Projekts mit der Projektnummer der neuen Auftragsnummer, Projektnummer #1 existiert bereits!',
+  'Error while saving shop order #1. DB Error #2. Generic exception #3.' => 'Fehler beim Speichern der Shop-Bestellung #1. DB Fehler #2. Genereller Fehler #3.',
   'Error with default taxzone'  => 'Ungültige Standardsteuerzone',
   'Error!'                      => 'Fehler!',
+  'Error: #1'                   => 'Fehler: #1',
   'Error: A negative target quantity is not allowed.' => 'Fehler: Eine negative Zielmenge ist nicht erlaubt.',
   'Error: A quantity and a target quantity could not be given both.' => 'Fehler: Menge und Zielmenge können nicht beide angegeben werden.',
   'Error: A quantity or a target quantity must be given.' => 'Fehler: Menge oder Zielmenge muss angegeben werden.',
+  'Error: Bin #1 is not from warehouse #2' => 'Lager \'#2\' hat keinen Lagerplatz \'#1\'',
   'Error: Bin not found'        => 'Fehler: Lagerplatz nicht gefunden',
-  'Error: Buchungsgruppe missing or invalid' => 'Fehler: Buchungsgruppe fehlt oder ungültig',
+  'Error: Customer/vendor is ambiguous' => 'Kunde/Lieferant ist mehrdeutig',
   'Error: Customer/vendor missing' => 'Fehler: Kunde/Lieferant fehlt',
   'Error: Customer/vendor not found' => 'Fehler: Kunde/Lieferant nicht gefunden',
+  'Error: Faulty position in this delivery order' => 'Fehler: Fehlerhafte Artikel-Position in diesem Lieferschein',
   'Error: Found local bank account number but local bank code doesn\'t match' => 'Fehler: Kontonummer wurde gefunden aber gespeicherte Bankleitzahl stimmt nicht überein',
   'Error: Gender (cp_gender) missing or invalid' => 'Fehler: Geschlecht (cp_gender) fehlt oder ungültig',
   'Error: Invalid bin'          => 'Fehler: Ungültiger Lagerplatz',
+  'Error: Invalid bin id'       => 'Ungültige Lagerplatz-ID',
+  'Error: Invalid bin name #1'  => 'Ungültiger Lagerplatz \'#1\'',
   'Error: Invalid business'     => 'Fehler: Kunden-/Lieferantentyp ungültig',
   'Error: Invalid contact'      => 'Fehler: Ansprechperson ungültig',
   'Error: Invalid currency'     => 'Fehler: ungültige Währung',
   'Error: Invalid delivery terms' => 'Fehler: Lieferbedingungen ungültig',
   'Error: Invalid department'   => 'Fehler: Abteilung ungültig',
   'Error: Invalid language'     => 'Fehler: Sprache ungültig',
-  'Error: Invalid order for this order item' => 'Fehler: Auftrag für diese Position ungültig',
   'Error: Invalid part'         => 'Fehler: Artikel ungültig',
-  'Error: Invalid part type'    => 'Fehler: Artikeltyp ungültig',
-  'Error: Invalid parts group'  => 'Fehler: Warengruppe ungültig',
+  'Error: Invalid part type'    => 'Fehler: ungültiger Artikeltyp',
+  'Error: Invalid parts group id #1' => 'Fehler: Ungültige Warengruppen-ID #1',
+  'Error: Invalid parts group name #1' => 'Fehler: Ungültiger Warengruppenname: #1',
   'Error: Invalid payment terms' => 'Fehler: Zahlungsbedingungen ungültig',
   'Error: Invalid price factor' => 'Fehler: Preisfaktor ungültig',
   'Error: Invalid price group'  => 'Fehler: Preisgruppe ungültig',
   'Error: Invalid project'      => 'Fehler: Projekt ungültig',
-  'Error: Invalid salesman'     => 'Fehler: Ungültiger Verkäufer',
+  'Error: Invalid salesman'     => 'Fehler: ungültige Verkäufer/in',
   'Error: Invalid shipto'       => 'Fehler: Lieferadresse ungültig',
   'Error: Invalid tax zone'     => 'Fehler: Steuerzone ungültig',
+  'Error: Invalid unit'         => 'Fehler: Einheit ungültig',
   'Error: Invalid vendor in column make_#1' => 'Fehler: Lieferant ungültig in Spalte make_#1',
   'Error: Invalid warehouse'    => 'Fehler: Ungültiges Lager',
+  'Error: Invalid warehouse id' => 'Ungültige Lager-ID',
+  'Error: Invalid warehouse name #1' => 'Ungültiger Lagername \'#1\'',
+  'Error: More than one source order found' => 'Fehler: mehr als ein Quell-Auftrag gefunden',
   'Error: Name missing'         => 'Fehler: Name fehlt',
+  'Error: Not enough parts in stock' => 'Fehler: Nicht genügend Artikel eingelagert',
+  'Error: Part is ambiguous'    => 'Artikel ist mehrdeutig',
+  'Error: Part is obsolete'     => 'Fehler: Artikel ist ungültig',
   'Error: Part not found'       => 'Fehler: Artikel nicht gefunden',
   'Error: Quantity to transfer is zero.' => 'Fehler: Zu bewegende Menge ist Null.',
+  'Error: Source order not found' => 'Fehler: Quell-Auftrag nicht gefunden',
+  'Error: Stock problem'        => 'Fehler: Problem bei der Lagerbewegung',
+  'Error: Stocking out would result in stock underrun' => 'Auslagern würde zu einem negativen Lagerbestand führen',
+  'Error: Stocking out would result in stock underrun: #1' => 'Auslagern würde zu einem negativen Lagerbestand führen: #1',
   'Error: Transfer would result in a negative target quantity.' => 'Fehler: Lagerbewegung würde zu einer negativen Zielmenge führen.',
   'Error: Unit missing or invalid' => 'Fehler: Einheit fehlt oder ungültig',
   'Error: Warehouse not found'  => 'Fehler: Lager nicht gefunden',
+  'Error: amount and netamount need to be numeric' => 'Fehler: amount und netamount müssen numerisch sein',
+  'Error: ar transaction doesn\'t validate' => 'Fehler: die Debitorenbuchung ist nicht korrekt',
+  'Error: archart isn\'t an AR chart' => 'Fehler: das Forderungskonto ist nicht als Forderungskonto definiert (link = AR)',
+  'Error: booking group missing or invalid' => 'Fehler: Buchungsgruppe fehlt oder ungültig',
+  'Error: can\'t find ar chart with accno #1' => 'Fehler: kein Forderungskonto mit Kontonummer #1',
+  'Error: chart isn\'t an ar_amount chart' => 'Fehler: Konto ist kein Erlöskonto',
+  'Error: chart missing'        => 'Fehler: Konto fehlt',
+  'Error: chart not found'      => 'Fehler: Konto nicht gefunden',
+  'Error: invalid acc transactions for this ar row' => 'Fehler: ungültige Buchungszeilen für diese Rechnungzeile',
+  'Error: invalid ar row for this transaction' => 'Ungültige Rechnungszeile für diese Buchungszeile',
+  'Error: invalid chart'        => 'Fehler: ungültiges Konto',
+  'Error: invalid chart (accno)' => 'Fehler: ungültiges Konto (accno)',
+  'Error: invalid chart_id'     => 'Fehler: ungültige Konto ID (chart_id)',
+  'Error: invalid taxkey'       => 'Fehler: ungültiger Steuerschlüssel',
+  'Error: invnumber already exists' => 'Fehler: Rechnungsnummer existiert schon',
   'Error: local bank account id doesn\'t match local bank account number' => 'Fehler: Bankkonto-ID stimmt nicht mit Kontonummer überein',
   'Error: local bank account id doesn\'t match local bank code' => 'Fehler: Bankkonto-ID stimmt nicht mit BLZ überein',
+  'Error: need amount and netamount' => 'Fehler: amount und netamount werden benötigt',
+  'Error: neither archart passed, no default receivables chart configured' => 'Fehler: Forderungskonto (archart) fehlt, kein Standardforderungskonto definiert',
+  'Error: taxincluded has to be t or f' => 'Fehler: Steuer im Preis inbegriffen muß t oder f sein',
+  'Error: taxincluded wasn\'t set' => 'Fehler: Steuer im Preis inbegriffen nicht gesetzt (taxincluded)',
+  'Error: taxkey missing'       => 'Fehler: Steuerschlüssel fehlt',
   'Error: this feature requires that articles with a time-based unit (e.g. \'h\' or \'min\') exist.' => 'Fehler: dieses Feature setzt voraus, dass Artikel mit einer Zeit-basierenden Einheit (z.B. "Std") existieren.',
   'Error: unknown local bank account' => 'Fehler: unbekannte Kontnummer',
   'Error: unknown local bank account id' => 'Fehler: unbekannte Bankkonto-ID',
   'Errors'                      => 'Fehler',
   'Errors during conversion:'   => 'Umwandlungsfehler:',
   'Errors during printing:'     => 'Druckfehler:',
+  'Errors in GL transaction:'   => 'Fehler in Dialogbuchung:',
+  'Errors: #1'                  => 'Fehler: #1',
   'Ertrag'                      => 'Ertrag',
   'Ertrag prozentual'           => 'Ertrag prozentual',
   'Escape character'            => 'Escape-Zeichen',
@@ -1164,11 +1487,14 @@ $self->{texts} = {
   'Example: http://kivitendo.de' => 'Beispiel:  http://kivitendo.de',
   'Excel'                       => 'Excel',
   'Exch'                        => 'Wechselkurs.',
+  'Exchange Rate'               => 'Kurs',
   'Exchangerate'                => 'Wechselkurs',
   'Exchangerate Difference'     => 'Wechselkursunterschied',
   'Exchangerate for payment missing!' => 'Es fehlt der Wechselkurs für die Bezahlung!',
   'Exchangerate missing!'       => 'Es fehlt der Wechselkurs!',
-  'Execute now'                 => 'Jetzt ausführen',
+  'Execute'                     => 'Ausführen',
+  'Execute a custom data export query' => 'Benutzerdefinierte Datenexport-Abfrage ausführen',
+  'Execute custom data export \'#1\'' => 'Benutzerdefinierter Datenexport »#1« ausführen',
   'Executed'                    => 'Ausgeführt',
   'Execution date'              => 'Ausführungsdatum',
   'Execution date from'         => 'Ausführungsdatum von',
@@ -1181,8 +1507,10 @@ $self->{texts} = {
   'Existing contacts (with column \'cp_id\')' => 'Existierende Ansprechpersonen (mit Spalte \'cp_id\')',
   'Existing customers/vendors with same customer/vendor number' => 'Existierende Kunden/Lieferanten mit derselben Kunden-/Lieferantennummer',
   'Existing file on server'     => 'Auf dem Server existierende Datei',
-  'Existing pending follow-ups for this item' => 'Noch nicht erledigte Wiedervorlagen f&uuml;r dieses Dokument',
+  'Existing finished follow-ups for this item' => 'Erledigte Wiedervorlagen für dieses Dokument',
+  'Existing pending follow-ups for this item' => 'Noch nicht erledigte Wiedervorlagen für dieses Dokument',
   'Existing profiles'           => 'Existierende Profile',
+  'Existing templates'          => 'Vorhandene Belegvorlagen',
   'Exp. bill. date'             => 'Vorauss. Abr.datum',
   'Exp. netamount'              => 'Vorauss. Summe',
   'Expected Tax'                => 'Erwartete Steuern',
@@ -1190,121 +1518,174 @@ $self->{texts} = {
   'Expense'                     => 'Aufwandskonto',
   'Expense Account'             => 'Aufwandskonto',
   'Expense/Asset'               => 'Aufwand/Anlagen',
-  'Export Buchungsdaten'        => 'Export Buchungsdaten',
+  'Experimental Features'       => 'Experimentelle Features',
+  'Export'                      => 'Export',
   'Export Number'               => 'Exportnummer',
-  'Export Stammdaten'           => 'Export Stammdaten',
   'Export as CSV'               => 'Als CSV exportieren',
   'Export as PDF'               => 'Als PDF exportieren',
   'Export date'                 => 'Exportdatum',
   'Export date from'            => 'Exportdatum von',
   'Export date to'              => 'Exportdatum bis',
+  'Export error in transaction #1: Rounding error too large #2' => 'Exportfehler in Transaktion #1: Zu großer Rundungsfehler (#2)',
+  'Export error in transaction #1: Unbalanced ledger before next transaction (#2)' => 'Exportfehler in Transaktion #1: Unausgeglichene Buchung',
+  'Export imported bookings'    => 'Importierte Buchungen exportieren',
+  'Export with CV Charts'       => 'Mit Personenkonten exportieren',
   'Extend automatically by n months' => 'Automatische Verlängerung um x Monate',
   'Extended'                    => 'Gesamt',
   'Extended status'             => 'Erweiterter Status',
   'Extension Of Time'           => 'Dauerfristverlängerung',
   'Factor'                      => 'Faktor',
-  'Factor missing!'             => 'Der Faktor fehlt.',
-  'Falsches Datumsformat!'      => 'Falsches Datumsformat!',
+  'Factur-X/ZUGFeRD'            => 'Factur-X/ZUGFeRD',
+  'Factur-X/ZUGFeRD import'     => 'Factur-X-/ZUGFeRD-Import',
+  'Factur-X/ZUGFeRD invoice'    => 'Factur-X-/ZUGFeRD-Rechnung',
+  'Factur-X/ZUGFeRD notes for each invoice' => 'Factur-X-/ZUGFeRD-Notizen für jede Rechnung',
+  'Factur-X/ZUGFeRD settings'   => 'Factur-X-/ZUGFeRD-Einstellungen',
   'Fax'                         => 'Fax',
   'Features'                    => 'Features',
   'Feb'                         => 'Feb',
   'February'                    => 'Februar',
   'Fee'                         => 'Gebühr',
+  'Fetch from last order number is not implemented' => 'Das Abholen ab der letzten Auftragsnummer ist nicht implementiert',
+  'Fetch order'                 => 'Hole Bestellung',
   'Field'                       => 'Feld',
   'File'                        => 'Datei',
+  'File \'#1\' is used as new Version !' => 'Datei \'#1\' wird als neue Version verwendet!',
+  'File Management'             => 'Dateimanagement',
   'File name'                   => 'Dateiname',
+  'File not exists !'           => 'Datei nicht vorhanden !',
+  'File still exists !'         => 'Datei existiert bereits !',
+  'File upload'                 => 'Datei Upload',
+  'Filemanagement'              => 'Dateimanagement',
+  'Filename'                    => 'Dateiname',
+  'Files'                       => 'Dateien',
+  'Files from customer'         => 'Kundendateien',
+  'Files from parts'            => 'Artikeldateien',
+  'Files from projects'         => 'Projektdateien',
+  'Files from vendor'           => 'Lieferantendateien',
+  'Files have been uploaded successfully.' => 'Dateien wurden erfolgreich hochgeladen.',
   'Filter'                      => 'Filter',
   'Filter by Partsgroups'       => 'Nach Warengruppen filtern',
   'Filter date by'              => 'Datum filtern nach',
   'Filter for customer variables' => 'Filter für benutzerdefinierte Kundenvariablen',
   'Filter for item variables'   => 'Filter für benutzerdefinierte Artikelvariablen',
   'Filter parts'                => 'Artikel filtern',
+  'Filter record template'      => 'Filter für Buchungsvorlagen',
+  'Final Invoice'               => 'Schlussrechnung',
+  'Final Invoice (one letter abbreviation)' => 'F',
+  'Final Invoice, please use mark as paid manually' => 'Rechnungstyp Schlussrechnung, bitte den Beleg manuell als bezahlt markieren',
   'Financial Controlling'       => 'Finanzcontrolling',
   'Financial Controlling Report' => 'Finanzcontrollingbericht',
   'Financial Overview'          => 'Finanzübersicht',
   'Financial controlling report for open sales orders' => 'Finanzcontrollingbericht für offene Verkaufsaufträge',
   'Financial overview for #1'   => 'Finanzübersicht für #1',
-  'Finish'                      => 'Abschlie&szlig;en',
+  'Finish'                      => 'Abschließen',
   'First 20 Lines'              => 'Nur erste 20 Datensätze',
+  'Firstname'                   => 'Vorname',
   'Fix transaction'             => 'Buchung korrigieren',
   'Fix transactions'            => 'Buchungen korrigieren',
-  'Focus position after update' => 'Kursor-Position nach Erneuern',
+  'Fixed value'                 => 'Fester Wert',
+  'Focus position after update' => 'Eingabe-Fokus-Position nach Erneuern',
   'Folgekonto'                  => 'Folgekonto',
   'Follow-Up'                   => 'Wiedervorlage',
   'Follow-Up Date'              => 'Wiedervorlagedatum',
   'Follow-Up On'                => 'Wiedervorlage am',
   'Follow-Up done'              => 'Wiedervorlage erledigt',
-  'Follow-Up for'               => 'Wiedervorlage f&uuml;r',
-  'Follow-Up for user'          => 'Wiedervorlage f&uuml;r Benutzer',
+  'Follow-Up for'               => 'Wiedervorlage für',
+  'Follow-Up for user'          => 'Wiedervorlage für Benutzer',
   'Follow-Up saved.'            => 'Wiedervorlage gespeichert.',
   'Follow-Ups'                  => 'Wiedervorlagen',
   'Follow-up for'               => 'Wiedervorlage für',
+  'Following files are deleted:' => 'Folgende Dateien wurden gelöscht:',
+  'Following files are unimported:' => 'Folgende Dateien sind zur Quelle exportiert:',
   'Following year'              => 'Folgendes Jahr',
   'Font'                        => 'Schriftart',
-  'Font size'                   => 'Schriftgr&ouml;&szlig;e',
+  'Font size'                   => 'Schriftgröße',
   'For AP transactions it will replace the sales taxkeys with input taxkeys with the same tax rate.' => 'Bei Kreditorenbuchungen werden die Umsatzsteuer-Steuerschlüssel durch Vorsteuer-Steuerschlüssel mit demselben Steuersatz ersetzt.',
   'For AR transactions it will replace the input taxkeys with sales taxkeys with the same tax rate.' => 'Bei Debitorenbuchungen werden die Vorsteuer-Steuerschlüssel durch Umsatzsteuer-Steuerschlüssel mit demselben Steuersatz ersetzt.',
-  'For all delivery orders create and print invoices' => 'Erstelle und drucke Rechnungen für alle Lieferscheine',
+  'For changeing goto USTVA Config' => 'Zum Verändern bitte zu den UStVa Einstellungen gehen',
   'For further information read this: ' => 'Für weitere Informationen zu diesem Thema lesen Sie bitte: ',
   'For part "#1" there are missing #2 #3 in the default warehouse/bin "#4/#5".' => 'Es fehlen #2 #3 des Artikels "#1" im Standardlager "#4/#5".',
   'For part "#1" there is no default warehouse and bin defined.' => 'Für Artikel "#1" ist kein Standardlager/-lagerplatz angegeben.',
   'For part "#1" there is no default warehouse and bin for ignoring onhand defined.' => 'Für Artikel "#1" ist kein Standardlager/-lagerplatz für das Auslagern ohne Bestandsprüfung angegeben.',
+  'For purchase delivery orders, warn on workflow to invoice if not stocked in' => 'Warnung in Einkaufslieferscheinen beim Workflow zur Rechnung ausgeben, wenn nicht eingelagert',
+  'For sales delivery orders, warn on workflow to invoice if not stocked out' => 'Warnung in Verkaufslieferscheinen beim Workflow zur Rechnung ausgeben, wenn nicht ausgelagert',
+  'For sales invoices, warn if invoice has no delivery order as a predecessor' => 'Bei Verkaufsrechnungen warnen, dass die Rechnung nicht aus einem Lieferschein generiert wurde.',
   'For type "customer" the perl module JSON is required. Please check this on system level: $ ./scripts/installation_check.pl' => 'Für den Typ "Kunde" wird das Perl Module JSON benötigt. Überprüfbar im Installationspfad mit: $ ./scripts/installation_check.pl',
   'Foreign Exchange Gain'       => 'Wechselkurserträge',
   'Foreign Exchange Loss'       => 'Wechselkursaufwendungen',
   'Form details (second row)'   => 'Formulardetails (zweite Positionszeile)',
+  'Format \'#1\' is not supported yet/anymore.' => 'Das Format \'#1\' wird noch nicht oder nicht mehr unterstützt.',
   'Formula'                     => 'Formel',
-  'Found #1 errors.'            => '#1 Fehler gefunden.',
-  'Found #1 objects of which #2 can be imported.' => 'Es wurden #1 Objekte gefunden, von denen #2 importiert werden können.',
+  'France'                      => 'Frankreich',
   'Free report period'          => 'Freier Zeitraum',
+  'Free skonto amount has to be a positive number.' => 'Der freie Skontobetrag muss positiv (absolut) sein.',
   'Free-form text'              => 'Textzeile',
+  'Fri'                         => 'Fr',
+  'Friday'                      => 'Freitag',
   'Fristsetzung'                => 'Fristsetzung',
   'From'                        => 'Von',
   'From Date'                   => 'Von',
+  'From bin'                    => 'Ausgelagert',
+  'From shop "#1" :  #2 '       => 'Shop #1 : #2',
+  'From shop #1 :  #2 shoporders have been fetched.' => 'Es wurden #2 Bestellungen von #1 geholt.',
   'From this version on a new feature is available.' => 'Ab dieser Version ist ein neues Feature verfügbar.',
   'From this version on it is necessary to name a default value.' => 'Ab dieser Version benötigt kivitendo eine Standardwährung.',
   'From this version on the partnumber of services, articles and assemblies have to be unique.' => 'Ab dieser Version müssen Artikelnummern eindeutig vergeben werden.',
   'From this version on the taxkey 0 must have a tax rate of 0 (for DATEV compatibility).' => 'Ab dieser Version muss der Steuerschlüssel 0 einen Steuersatz von 0% haben (auf Grund der DATEV-Kompatibilität).',
+  'Front page'                  => 'Hauptseite',
   'Full Access'                 => 'Vollzugriff',
   'Full Preview'                => 'Alles',
+  'Full Text'                   => 'Volltext',
   'Full access to all functions' => 'Vollzugriff auf alle Funktionen',
   'Function block'              => 'Funktionsblock',
   'Function block actions'      => 'Funktionsblockaktionen',
   'Function block number format' => 'Format der Funktionsblocknummerierung',
   'Function/position'           => 'Funktion/Position',
+  'Further Invoice for Advance Payment' => 'Weitere Anzahlungsrechnung',
   'GL Transaction'              => 'Dialogbuchung',
   'GL Transaction (abbreviation)' => 'DB',
+  'GL Transactions'             => 'Dialogbuchungen',
   'GL search'                   => 'FiBu Suche',
+  'GL template suggestions'     => 'Vorschlag Dialogbuchung',
   'GL transactions changeable'  => 'Änderbarkeit von Dialogbuchungen',
+  'GLN'                         => 'GLN',
   'Gegenkonto'                  => 'Gegenkonto',
+  'Geierlein'                   => 'Geierlein',
   'Gender'                      => 'Geschlecht',
   'General Ledger'              => 'Finanzbuchhaltung',
   'General Ledger Corrections'  => 'Korrekturen im Hauptbuch',
   'General Ledger Transaction'  => 'Dialogbuchung',
   'General ledger and cash'     => 'Finanzbuchhaltung und Zahlungsverkehr',
   'General ledger corrections'  => 'Korrekturen im Hauptbuch',
+  'General ledger transaction \'#1\' posted (ID: #2)' => 'Dialogbuchung \'#1\' verbucht (Buchungsnummer: #2)',
+  'General ledger transactions can only be changed on the day they are posted.' => 'Dialogbuchungen können nur am Buchungstag geändert werden.',
   'General settings'            => 'Allgemeine Einstellungen',
-  'Generic Tax Report'          => 'USTVA Bericht',
+  'Generate and print sales delivery orders' => 'Erzeuge und drucke Lieferscheine',
+  'Generating the document failed: #1' => 'Das Dokument konnte nicht erzeugt werden: #1',
+  'Germany'                     => 'Deutschland',
+  'Get one order'               => 'Hole eine Bestellung',
+  'Get one order by shopordernumber' => 'Hole eine Bestellung über Shopbestellnummer',
+  'Get one shoporder'           => 'Hole eine Bestellung',
+  'Get shoporders'              => 'Shopbestellungen holen und bearbeiten',
   'Git revision: #1, #2 #3'     => 'Git-Revision: #1, #2 #3',
   'Given Name'                  => 'Vorname',
+  'Gldate'                      => 'Erfassungsdatum',
+  'Global Attachments'          => 'Allgemeine Dokumentenanhänge',
   'Global Record BCC'           => 'Globale BCC-Adresse',
+  'GoBD Export'                 => 'GoBD Export',
   'Greeting'                    => 'Anrede',
   'Greetings'                   => 'Anreden',
-  'Group'                       => 'Warengruppe',
   'Group Invoices'              => 'Rechnungen zusammenfassen',
   'Group Items'                 => 'Waren gruppieren',
   'Group assignment'            => 'Gruppenzuordnung',
-  'Group deleted!'              => 'Warengruppe gelöscht!',
   'Group list'                  => 'Gruppenliste',
   'Group membership'            => 'Gruppenzugehörigkeit',
-  'Group missing!'              => 'Warengruppe fehlt!',
-  'Group saved!'                => 'Warengruppe gespeichert!',
-  'Groups'                      => 'Warengruppen',
   'Groups that are valid for this client for access rights' => 'Gruppen, die für diesen Mandanten gültig sind',
   'Groups this user is a member in' => 'Gruppen, in denen Benutzer Mitglied ist',
   'Groups valid for this client' => 'Für Mandanten gültige Gruppen',
   'HTML'                        => 'HTML',
   'HTML Templates'              => 'HTML-Vorlagen',
+  'HTML field'                  => 'HTML-Feld',
   'Handling of WebDAV'          => 'Behandlung von WebDAV',
   'Hardcopy'                    => 'Seite drucken',
   'Has item type'               => 'Hat Regeltypen',
@@ -1317,11 +1698,18 @@ $self->{texts} = {
   'Here you only provide the credentials for logging into the database.' => 'Hier geben Sie nur die Logindaten für die Anmeldung an der Datenbank ein.',
   'Here\'s an example command line:' => 'Hier ist eine Kommandozeile, die als Beispiel dient:',
   'Hide Filter'                 => 'Filter verbergen',
-  'Hide by default'             => 'Standardm&auml;&szlig;ig verstecken',
+  'Hide all details'            => 'Alle Details verbergen',
+  'Hide buttons'                => 'Knöpfe verstecken',
+  'Hide by default'             => 'Standardmäßig verstecken',
   'Hide chart details'          => 'Konteninformation verstecken',
+  'Hide chart list'             => 'Kontenliste verstecken',
+  'Hide charts'                 => 'Konten verstecken',
   'Hide details'                => 'Details verbergen',
   'Hide help text'              => 'Hilfetext verbergen',
+  'Hide mappings (csv_import)'  => 'Spaltenzuordnungen verbergen',
   'Hide settings'               => 'Einstellungen verbergen',
+  'Highest Dunninglevel'        => 'Höchste Mahnstufe',
+  'Hint: Not all VC Numbers are personal accounts compliant' => 'Hinweis: Nicht alle Kunden-/Lieferantennummern sind DATEV-Personenkonten kompatibel.',
   'Hints'                       => 'Hinweise',
   'History'                     => 'Historie',
   'History Search'              => 'Historien Suche',
@@ -1331,55 +1719,99 @@ $self->{texts} = {
   'Hourly Rate'                 => 'Stundensatz',
   'Hourly rate'                 => 'Stundensatz',
   'How many do you want to create and print?' => 'Wie viele wollen Sie erstellen und drucken?',
-  'However, you can create a new part which will then be selected.' => 'Sie k&ouml;nnen jedoch einen neuen Artikel anlegen, der dann automatisch ausgew&auml;hlt wird.',
   'I'                           => 'I',
   'IBAN'                        => 'IBAN',
   'ID'                          => 'Buchungsnummer',
+  'ID (lit)'                    => 'ID',
+  'ID number'                   => 'ID-Nummer',
   'ID of own bank account'      => 'Datenbank-ID des Bankkontos',
-  'ID-Nummer'                   => 'ID-Nummer (intern)',
   'ID/Acc_ID'                   => 'ID/Acc_ID',
   'II'                          => 'II',
   'III'                         => 'III',
+  'IMPORT'                      => 'Importiert',
   'IV'                          => 'IV',
   'If all of the following match' => 'Wenn alle der folgenden Bedingungen zutreffen',
   'If amounts differ more than "Maximal amount difference" (see settings), this item is marked as invalid.' => 'Weichen die Beträge mehr als die "maximale Betragsabweichung" (siehe Einstellungen) ab, so wird diese Position als ungültig markiert.',
   'If checked the taxkey will not be exported in the DATEV Export, but only IF chart taxkeys differ from general ledger taxkeys' => 'Falls angehakt wird der DATEV-Steuerschlüssel bei Buchungen auf dieses Konto nicht beim DATEV-Export mitexportiert, allerdings nur wenn zusätzlich der Konto-Steuerschlüssel vom Buchungs (Hauptbuch) Steuerschlüssel abweicht',
+  'If column \'pclass\' is present the article type is then irrelevant or used as default ' => 'Falls Spalte \'pclass\' existiert, wird dies nur als default genommen',
   'If configured this bin will be preselected for all new parts. Also this bin will be used as the master default bin, if default transfer out with master bin is activated.' => 'Falls konfiguriert, wird dieses Lager mit Lagerplatz für neu angelegte Waren vorausgewählt.',
+  'If configured this bin will be preselected for stocktaking.' => 'Wenn konfiguriert, wird dieser Lagerplatz bei der Inventur vorausgewählt.',
+  'If configured this date will used as preselected cutoff date for stocktaking.' => 'Wenn konfiguriert, wird dieses Datum bei der Inventur als Stichtag voreingestellt.',
+  'If configured this warehouse will be preselected for stocktaking.' => 'Wenn konfiguriert, wird dieses Lager bei der Inventur vorausgewählt.',
   'If disabled purchase delivery orders can only be created by conversion from existing requests for quotations and purchase orders.' => 'Falls deaktiviert, so können Einkaufslieferscheine nur durch Umwandlung aus bestehenden Preisanfragen und Lieferantenaufträgen angelegt werden.',
   'If disabled purchase invoices can only be created by conversion from existing requests for quotations, purchase orders and purchase delivery orders.' => 'Falls deaktiviert, so können Einkaufsrechnungen nur durch Umwandlung aus bestehenden Preisanfragen, Lieferantenaufträgen und Einkaufslieferscheinen angelegt werden.',
   'If disabled sales orders cannot be converted into sales invoices directly.' => 'Falls deaktiviert, so können Verkaufsaufträge nicht direkt in Verkaufsrechnungen umgewandelt werden.',
   'If disabled sales quotations cannot be converted into sales invoices directly.' => 'Falls deaktiviert, so können Verkaufsangebote nicht direkt in Verkaufsrechnungen umgewandelt werden.',
+  'If disabled, record numbers for sales records & purchase records produced by our side will always be auto-generated and cannot be changed later.' => 'Falls deaktiviert, werden Belegnummern in Verkaufs- und Einkaufsbelegen, die auf unserer Seite erzeugt wurden, immer automatisch vergeben und können anschließend nicht mehr geändert werden.',
+  'If enabled Factur-X/ZUGFeRD conformant sales invoice PDFs will be created.' => 'Falls aktiviert, werden Factur-X-/ZUGFeRD-konforme PDFs für Verkaufsrechnungen erzeugt.',
   'If enabled a column will be shown in sales and purchase orders that lists both the amount and the value not shipped yet for each item.' => 'Falls eingeschaltet, wird für jede Position in Auftragsbestätigungen und Lieferantenaufträgen eine Spalte mit noch nicht gelieferter Menge und Wert angezeigt.',
+  'If enabled a warning will be shown if a sales invoices is created without having a sales delivery order as a predecessor.' => 'Falls aktiv, wird eine Warnung beim Buchen einer Verkaufsrechnung angezeigt, falls es keinen Lieferschein als Vorgänger gibt.',
+  'If enabled a warning will be shown in purchase delivery orders on workflow to invoices if positions are not stocked in.' => 'Falls aktiviert, wird eine Warnung beim Workflow von Einkaufslieferscheinen zu Rechnungen ausgegeben, wenn die Positionen noch nicht eingelagert sind.',
+  'If enabled a warning will be shown in sales and purchase orders if there are two or more positions of the same part (new controller only).' => 'Falls eingeschaltet, wird eine Warnung angezeigt, wenn der Auftrag mehrere gleiche Artikel enthält (nur neuer Controller).',
+  'If enabled a warning will be shown in sales and purchase orders if there the delivery date is empty.' => 'Falls aktiviert, Warnungen ausgeben sobald Aufträge (Einkauf- und Verkauf) keinen Liefertermin haben.',
+  'If enabled a warning will be shown in sales delivery orders if the customer order number is missing.' => 'Falls aktiviert, wird eine Warnung beim Speichern von Verkaufsaufträgen ausgegeben, wenn die Kundenbestellnummer fehlt.',
+  'If enabled a warning will be shown in sales delivery orders on workflow to invoices if positions are not stocked out.' => 'Falls aktiviert, wird eine Warnung beim Workflow von Verkaufslieferscheinen zu Rechnungen ausgegeben, wenn die Positionen noch nicht ausgelagert sind.',
   'If enabled only those projects that are assigned to the currently selected customer are offered for selection in sales records.' => 'Wenn eingeschaltet, so werden in Verkaufsbelegen nur diejenigen Projekte zur Auswahl angeboten, die dem aktuell ausgewählten Kunden zugewiesen wurden.',
   'If enabled purchase and sales records cannot be saved if no transaction description has been entered.' => 'Wenn angeschaltet, so können Einkaufs- und Verkaufsbelege nicht gespeichert werden, solange keine Vorgangsbezeichnung eingegeben wurde.',
+  'If enabled sales invoices created using OpenDocument/OASIS format will include data for Swiss QR-Bill creation.' => 'Falls aktiviert, enthalten Rechnungen im OpenDocument/OASIS Format, Daten zur Schweizer QR-Rechnung.',
+  'If enabled the record links view starts always from the sales order including all sublevels' => 'Falls aktiv, werden die verknüpften Belege immer vom Verkaufsauftrag inkl. aller darunterliegenden Belege angezeigt',
+  'If enabled try to overrule the brower\'s back button to prevent double booking of sales invoices.' => 'Falls aktiviert, wird versucht, den Zurück-Knopf des Browsers auszuhebel, um doppeltes Buchen von Verkaufsrechnungen zu verhindern.',
+  'If enabled, when saving parts the partsgroup must be not be empty.' => 'Falls aktiviert muß beim Speichern von Artikeln eine Warengruppe ausgewählt sein.',
+  'If item not found, allow creation of new item' => 'Falls Artikel nicht gefunden, erlaube Erfassen eines Neuen',
+  'If left empty the default sender from the kivitendo configuration will be used (key \'email_from\' in section \'periodic_invoices\'; current value: #1).' => 'Falls leer, so wird der Standardabsender aus der kivitendo-Konfiguration genutzt (Schlüssel »email_from« in Abschnitt »periodic_invoices«; aktueller Wert: #1).',
   'If missing then the start date will be used.' => 'Falls es fehlt, so wird die erste Rechnung für das Startdatum erzeugt.',
-  'If the article type is set to \'mixed\' then a column called \'type\' must be present.' => 'Falls der Artikeltyp auf \'gemischt\' gestellt wird, muss eine Spalte namens \'type\' vorhanden sein.',
-  'If the automatic creation of invoices for fees and interest is switched on for a dunning level then the following accounts will be used for the invoice.' => 'Wenn das automatische Erstellen einer Rechnung &uuml;ber Mahngeb&uuml;hren und Zinsen f&uuml;r ein Mahnlevel aktiviert ist, so werden die folgenden Konten f&uuml;r die Rechnung benutzt.',
-  'If the database user listed above does not have the right to create a database then enter the name and password of the superuser below:' => 'Falls der oben genannte Datenbankbenutzer nicht die Berechtigung zum Anlegen neuer Datenbanken hat, so k&ouml;nnen Sie hier den Namen und das Passwort des Datenbankadministratoraccounts angeben:',
+  'If one or more space separated serial numbers are assigned in a sales invoice, match the charge number of the inventory item. Assumes that Serial Number and Charge Number have 1:1 relation. Otherwise throw a error message for the default sales invoice transfer.' => 'Falls eine oder mehrere Leerzeichen separierte Seriennummern in Verkaufsrechnungen definiert sind, nutz diese als Chargennummern fürs Standard-Auslagern über Rechnung. Seriennummern und eingelagerte Chargen kommen jeweils exakt nur einmal vor. Falls die Chargennummer oder das Mengenverhältnis (1:1) in keinem Lagerort existiert wird eine Fehlermeldung beim Auslagern generiert.',
+  'If searching a part from a document and no part is found then offer to create a new part.' => 'Wenn bei der Artikelsuche aus einem Dokument heraus kein Artikel gefunden wird, dann wird ermöglicht, von dort aus einen neuen Artikel anzulegen.',
+  'If set to no the \'delivery date\' field for sales orders won\'t be set at all.' => 'Falls der Wert auf Nein gesetzt wird, wird überhaupt kein Lieferdatum in Verkaufsaufträgen gesetzt',
+  'If set to no the \'valid until\' field for sales quotation won\'t be set at all.' => 'Falls der Wert auf Nein gesetzt wird, wird überhaupt kein Gültigkeitsdatum bei Verkaufs-Angeboten gesetzt',
+  'If the article type is set to \'mixed\' then a column called \'part_type\' or called \'pclass\' must be present.' => 'Falls der Artikeltyp auf \'mixed\' gesetzt ist muss entweder eine Spalte \'part_type\' oder \'pclass\' im Import vorhanden sein',
+  'If the automatic creation of invoices for fees and interest is switched on for a dunning level then the following accounts will be used for the invoice.' => 'Wenn das automatische Erstellen einer Rechnung über Mahngebühren und Zinsen für ein Mahnlevel aktiviert ist, so werden die folgenden Konten für die Rechnung benutzt.',
+  'If the counted quantity differs more than this threshold from the quantity in the database, a warning will be shown. Set to 0 to switch of this feature.' => 'Wenn die gezählte Menge mehr als diesen Schwellenwert von der Menge in der Datenbank abweicht, wird eine Warnmeldung angezeigt. Setzen Sie den Schwellenwert auf 0, um dieses Feature abzuschalten.',
+  'If the database user listed above does not have the right to create a database then enter the name and password of the superuser below:' => 'Falls der oben genannte Datenbankbenutzer nicht die Berechtigung zum Anlegen neuer Datenbanken hat, so können Sie hier den Namen und das Passwort des Datenbankadministratoraccounts angeben:',
   'If the default transfer out always succeed use this bin for negative stock quantity.' => 'Standardlagerplatz für Auslagern ohne Prüfung auf Bestand',
-  'If you enter values for the part number and / or part description then only those bins containing parts whose part number or part description match your input will be shown.' => 'Wenn Sie f&uuml;r die Artikelnummer und / oder die Beschreibung etwas eingeben, so werden nur die Lagerpl&auml;tze angezeigt, in denen Waren eingelagert sind, die Ihre Suchbegriffe enthalten.',
+  'If the test mode is enabled, the Factur-X/ZUGFeRD invoices will be flagged so that they\'re only fit to be used for testing purposes.' => 'Wenn der Testmodus aktiviert ist, werden Factur-X-/ZUGFeRD-Rechnungen so markiert, dass sie nur für Testzwecke dienen dürfen.',
+  'If yes, delivery order positions are considered "delivered" only if they have been stocked out of the inventory. Otherwise saving the delivery order is considered delivered.' => 'Wenn diese Option aktiviert ist, gelten Lieferscheinpositionen nur dann als geliefert wenn sie im Lieferschein ausgelagert wurden, und die Ware aus dem Lager ausgebucht wurde. Andernfalls gilt das Speichern des Lieferscheins als Lieferung.',
+  'If you enter values for the part number and / or part description then only those bins containing parts whose part number or part description match your input will be shown.' => 'Wenn Sie für die Artikelnummer und / oder die Beschreibung etwas eingeben, so werden nur die Lagerplätze angezeigt, in denen Waren eingelagert sind, die Ihre Suchbegriffe enthalten.',
   'If you have not chosen for example the category revenue for a tax and you choose an revenue account to create a transfer in the general ledger, this tax will not be displayed in the tax dropdown.' => 'Wenn Sie z.B. die Kategory Erlös für eine Steuer nicht gewählt haben und ein Erlöskonto beim Erstellen einer Dialogbuchung wählen, wird diese Steuer auch nicht im Dropdown-Menü für die Steuern angezeigt.',
   'If you lock the system normal users won\'t be able to log in.' => 'Wenn Sie das System sperren, so werden sich normale Benutzer nicht mehr anmelden können.',
-  'If you see this message, you most likely just setup your LX-Office and haven\'t added any entry types. If this is the case, the option is accessible for administrators in the System menu.' => 'Wenn Sie diese Meldung sehen haben Sie wahrscheinlich ein frisches LX-Office Setup und noch keine Buchungsgruppen eingerichtet. Ein Administrator kann dies im Systemmen&uuml; erledigen.',
   'If you select a base unit then you also have to enter a factor.' => 'Wenn Sie eine Basiseinheit auswählen, dann müssen Sie auch einen Faktor eingeben.',
   'If you switch to a different tab without saving you will lose the data you\'ve entered in the current tab.' => 'Wenn Sie auf einen anderen Tab wechseln, ohne vorher zu speichern, so gehen die im aktuellen Tab eingegebenen Daten verloren.',
   'If you want to change any of these parameters then press the "Back" button, edit the file "config/kivitendo.conf" and login into the admin module again.' => 'Wenn Sie einen der Parameter ändern wollen, so drücken Sie auf den "Zurück"-Button, bearbeiten Sie die Datei "config/kivitendo.conf", und melden Sie sich erneut im Administrationsbereich an.',
   'If you want to delete such a dataset you have to edit the client(s) that are using the dataset in question and have them use another dataset.' => 'Wenn Sie eine solche Datenbank löschen möchten, dann müssen Sie zuerst den/die Mandanten auf eine andere Datenbank umstellen, die die zu löschende Datenbank benutzen.',
   'If you want to set up the authentication database yourself then log in to the administration panel. kivitendo will then create the database and tables for you.' => 'Wenn Sie die Authentifizierungs-Datenbank selber einrichten wollen, so melden Sie sich im Administrationsbereich an. kivitendo wird dann die Datenbank und die erforderlichen Tabellen für Sie anlegen.',
   'If your old bins match exactly Bins in the Warehouse CLICK on <b>AUTOMATICALLY MATCH BINS</b>.' => 'Falls die alte Lagerplatz-Beschreibung in Stammdaten genau mit einem Lagerplatz in einem vorhandenem Lager übereinstimmt, KLICK auf <b>LAGERPLÄTZE AUTOMATISCH ZUWEISEN</b>',
+  'Ignore faulty positions'     => 'Fehlerhafte Artikel-Positionen ignorieren',
+  'Ignore services for the purchase orders state of delivery' => 'Dienstleistungen werden bei der Statusänderung geliefert für Einkaufsaufträge ignoriert',
+  'Ignore services for the sales orders state of delivery' => ' Dienstleistungen werden bei der Statusänderung geliefert für Verkaufsaufträge ignoriert',
   'Illegal characters have been removed from the following fields: #1' => 'Ungültige Zeichen wurden aus den folgenden Feldern entfernt: #1',
   'Illegal date'                => 'Ungültiges Datum',
   'Image'                       => 'Grafik',
+  'Image Upload'                => 'Bilder Upload',
+  'ImagePreview'                => 'Bildvorschau',
+  'Images'                      => 'Bilder',
   'Import'                      => 'Import',
+  'Import AP from Scanner or Email' => 'Einkaufsbelege importieren vom Scanner oder von Email',
+  'Import AR from Scanner or Email' => 'Verkaufsbelege importieren vom Scanner oder von Email',
   'Import CSV'                  => 'CSV-Import',
+  'Import Pay Postings'         => 'Lohnbuchungen importieren',
   'Import Status'               => 'Import Status',
-  'Import a MT940 file:'        => 'Laden Sie eine MT940 Datei hoch:',
+  'Import a Factur-X/ZUGFeRD file:' => 'Eine Factur-X-/ZUGFeRD-Datei importieren',
+  'Import a File:'              => 'Datei importieren:',
+  'Import all'                  => 'Importiere Alle',
+  'Import documents from #1'    => 'Importiere Dateien von Quelle \'#1\'',
   'Import file'                 => 'Import-Datei',
+  'Import finished with errors.' => 'Der Import wurde mit Fehlern beendet.',
+  'Import finished without errors.' => 'Der Import wurde ohne Fehler beendet.',
   'Import not started yet, please wait...' => 'Der Taskserver ist gerade ausgelastet. Ihr Import wird gleich gestartet, bitte warten...',
   'Import preview'              => 'Import-Vorschau',
   'Import profiles'             => 'Import-Profil',
   'Import result'               => 'Import-Ergebnis',
-  'Import summary'              => 'Import-Zusammenfassung',
+  'Import scanned documents'    => 'Importiere gescannte Dateien',
+  'Importdate'                  => 'Importdatum',
+  'Imported'                    => 'Importiert',
+  'Imported Pay Postings'       => 'Importierte Lohnbuchungen',
+  'Imported entries'            => 'Importierte Einträge',
+  'In addition to the above date functions, subtract the following amount of days from the calculated date as a buffer.' => 'Der folgende Puffer in Tagen wird von den beiden obigen vorausberechneten Daten abgezogen.',
   'In order to do that hit the button "Delete transaction".' => 'Drücken Sie dafür auf den Button "Buchung löschen".',
   'In order to migrate the old folder structure into the new structure you have to chose which client the old structure will be assigned to.' => 'Um die alte Ordnerstruktur in die neue Struktur zu migrieren, müssen Sie festlegen, welchem Mandanten die bisherige Struktur zugewiesen wird.',
   'In order to use kivitendo you have to create at least a client, a user and a group.' => 'Um kivitendo zu nutzen, müssen Sie mindestens einen Mandanten, einen Benutzer und eine Gruppe anlegen.',
@@ -1387,12 +1819,13 @@ $self->{texts} = {
   'In-line'                     => 'im Text',
   'Inactive'                    => 'Inaktiv',
   'Include Exchangerate Difference' => 'Wechselkursunterschied einbeziehen',
-  'Include column headings'     => 'Spalten&uuml;berschriften erzeugen',
-  'Include empty bins'          => 'Leere Lagerpl&auml;tze anzeigen',
+  'Include column headings'     => 'Spaltenüberschriften erzeugen',
+  'Include empty bins'          => 'Leere Lagerplätze anzeigen',
   'Include in Report'           => 'In Bericht aufnehmen',
   'Include in drop-down menus'  => 'In Aufklappmenü aufnehmen',
   'Include invalid warehouses ' => 'Ungültige Lager berücksichtigen',
   'Include invoices with direct debit' => 'Inklusive Rechnungen mit Lastschrifteinzug',
+  'Include original Invoices?'  => 'Original-Rechnung hinzufügen?',
   'Includeable in reports'      => 'In Berichten anzeigbar',
   'Included in reports by default' => 'In Berichten standardmäßig enthalten',
   'Including'                   => 'Enthaltene',
@@ -1404,8 +1837,11 @@ $self->{texts} = {
   'Incorrect username or password or no access to selected client!' => 'Ungültiger Benutzername oder Passwort oder kein Zugriff auf den ausgewählten Mandanten!',
   'Increase'                    => 'Erhöhen',
   'Individual Items'            => 'Einzelteile',
+  'Info'                        => 'Info',
   'Information'                 => 'Information',
   'Initial version.'            => 'Initiale Version.',
+  'Input from string: #1'       => 'Eingabe Von-Zeichenkette: #1',
+  'Input to string: #1'         => 'Eingabe Bis-Zeichenkete: #1',
   'Insert'                      => 'Einfügen',
   'Insert Date'                 => 'Erfassungsdatum',
   'Insert new'                  => 'Hinzufügen',
@@ -1418,13 +1854,21 @@ $self->{texts} = {
   'Internal Phone List'         => 'Interne Telefonliste',
   'Internal comment'            => 'Interne Bemerkungen',
   'Internet'                    => 'Internet',
+  'Interpolate variables in texts of positions' => 'Variablen in Positionstexten interpolieren',
+  'Into bin'                    => 'Eingelagert',
+  'Intra-Community supply'      => 'Gelangensbestätigung',
   'Introduction of clients'     => 'Einführung von Mandanten',
   'Inv. Duedate'                => 'Rg. Fälligkeit',
   'Invalid'                     => 'Ungültig',
+  'Invalid assembly'            => 'Ungültiges Erzeugnis',
+  'Invalid bin'                 => 'Ungültiger Lagerplatz',
+  'Invalid charge number: #1'   => 'Ungültige Chargennummer: #1',
+  'Invalid combination of ledger account number length. Mismatch length of #1 with length of #2. Please check your account settings. ' => 'Ungültige Kombination der Nummernkreislänge der Sachkonten. Kann nicht eine Länge von #1 und eine Länge von #2 verarbeiten. Bitte entsprechend die Konteneinstellungen überprüfen.',
   'Invalid duration format'     => 'Falsches Format für Zeitdauer',
-  'Invalid follow-up ID.'       => 'Ung&uuml;ltige Wiedervorlage-ID.',
-  'Invalid quantity.'           => 'Die Mengenangabe ist ung&uuml;ltig.',
+  'Invalid follow-up ID.'       => 'Ungültige Wiedervorlage-ID.',
+  'Invalid quantity.'           => 'Die Mengenangabe ist ungültig.',
   'Invalid request type \'#1\'' => 'Ungültiger Request-Typ \'#1\'',
+  'Invalid todo for updating Part' => 'Ungültiger Wert für das Feld todo bei Artikel aktualisieren',
   'Invalid transactions'        => 'Ungültige Buchungen',
   'Invalid variable #1'         => 'Ungültige Variable #1',
   'Invdate'                     => 'Rechnungsdatum',
@@ -1440,38 +1884,52 @@ $self->{texts} = {
   'Invnumber missing!'          => 'Rechnungsnummer fehlt!',
   'Invoice'                     => 'Rechnung',
   'Invoice (one letter abbreviation)' => 'R',
+  'Invoice Copy'                => 'Rechnungskopie',
   'Invoice Date'                => 'Rechnungsdatum',
   'Invoice Date missing!'       => 'Rechnungsdatum fehlt!',
   'Invoice Duedate'             => 'Fälligkeitsdatum',
+  'Invoice Field 1'             => 'Belegfeld 1',
+  'Invoice Field 2'             => 'Belegfeld 2',
   'Invoice Number'              => 'Rechnungsnummer',
   'Invoice Number missing!'     => 'Rechnungsnummer fehlt!',
   'Invoice deleted!'            => 'Rechnung gelöscht!',
+  'Invoice email'               => 'E-Mail des Rechnungsempfängers (Kundenstammdaten)',
+  'Invoice email and Contact Person' => 'E-Mail des Rechnungsempfängers und CC an Ansprechpartner',
+  'Invoice email settings'      => 'E-Mail Rechnungsversand',
   'Invoice filter'              => 'Rechnungsfilter',
+  'Invoice for Advance Payment' => 'Anzahlungsrechnung',
+  'Invoice for Advance Payment (one letter abbreviation)' => 'A',
+  'Invoice for Advance Payment with Storno (abbreviation)' => 'A(S)',
   'Invoice for fees'            => 'Rechnung über Gebühren',
   'Invoice has already been storno\'d!' => 'Diese Rechnung wurde bereits storniert.',
   'Invoice number'              => 'Rechnungsnummer',
+  'Invoice number invalid. Must be less then or equal to 7 digits after prefix.' => 'Rechnungsnummer ungültig. (kleiner/gleich 7 Stellen nach Prefix)',
+  'Invoice to:'                 => 'Rechnung an:',
   'Invoice total'               => 'Die Rechnungssumme',
   'Invoice total less discount' => 'Rechnungssumme abzüglich Skonto',
   'Invoice with Storno (abbreviation)' => 'R(S)',
   'Invoices'                    => 'Rechnungen',
+  'Invoices can only be changed on the day they are posted.' => 'Rechnungen können nur am Buchungstag geändert werden.',
+  'Invoices with payments cannot be canceled.' => 'Rechnungen mit Zahlungen können nicht storniert werden.',
   'Invoices, Credit Notes & AR Transactions' => 'Rechnungen, Gutschriften & Debitorenbuchungen',
   'Is Searchable'               => 'Durchsuchbar',
   'Is this a summary account to record' => 'Sammelkonto für',
   'It can be changed later but must be unique within the installation.' => 'Er ist nachträglich änderbar, muss aber im System eindeutig sein.',
   'It is not allowed that a summary account occurs in a drop-down menu!' => 'Ein Sammelkonto darf nicht in Aufklappmenüs aufgenommen werden!',
   'It is possible that even after such a correction there is something wrong with this transaction (e.g. taxes that don\'t match the selected taxkey). Therefore you should re-run the general ledger analysis.' => 'Auch nach einer Korrektur kann es mit dieser Buchung noch weitere Probleme geben (z.B. nicht zum Steuerschlüssel passende Steuern), weshalb ein erneutes Ausführen der Hauptbuchanalyse empfohlen wird.',
-  'It is possible to make a quick DATEV export everytime you post a record to ensure things work nicely with their data requirements. This will result in a slight overhead though you can enable this for each type of record independantly.' => 'Es ist möglich, bei jeder Buchung einen schnellen DATEV-Export durchzuführen, um sicherzustellen, dass die Datensätze den DATEV-Anforderungen genügen. Da dies einen kleinen Overhead bedeutet, lässt sich die Einstellung für jeden Buchungstyp getrennt einstellen.',
+  'It is possible to make a quick DATEV export everytime you post a record to ensure things work nicely with their data requirements. This will result in a slight overhead though you can enable this for each type of record independently.' => 'Es ist möglich, bei jeder Buchung einen schnellen DATEV-Export durchzuführen, um sicherzustellen, dass die Datensätze den DATEV-Anforderungen genügen. Da dies einen kleinen Overhead bedeutet, lässt sich die Einstellung für jeden Buchungstyp getrennt einstellen.',
   'It will not be further modified by any other source, and will be offered in records like this.' => 'Er wird nicht weiter verändert werden und genau so im Beleg vorgeschlagen werden.',
   'It will simply set the taxkey to 0 (meaning "no taxes") which is the correct value for such inventory transactions.' => 'Es wird einfach die Steuerschlüssel auf  0 setzen, was "keine Steuer" bedeutet und für solche Warenbestandsbuchungen der richtige Wert ist.',
-  'Item deleted!'               => 'Artikel gelöscht!',
+  'Italy'                       => 'Italien',
+  'Item does not exists in the database' => 'Den Artikel gibt es nicht',
   'Item mode'                   => 'Artikelmodus',
   'Item multi selection with qty' => 'Artikel-Mehrfachauswahl mit Menge',
-  'Item not on file!'           => 'Dieser Artikel ist nicht in der Datenbank!',
   'Item values'                 => 'Artikelwerte',
   'Item variables'              => 'Artikelvariablen',
   'Jahresverkehrszahlen neu'    => 'Jahresverkehrszahlen neu',
   'Jan'                         => 'Jan',
   'January'                     => 'Januar',
+  'Job history'                 => 'Jobverlauf',
   'Journal'                     => 'Buchungsjournal',
   'Journal of Last 10 Transfers' => 'Letzte 10 Lagertransaktionen',
   'Jul'                         => 'Jul',
@@ -1479,12 +1937,11 @@ $self->{texts} = {
   'Jump to'                     => 'Springe zu',
   'Jun'                         => 'Jun',
   'June'                        => 'Juni',
-  'KNE-Export erfolgreich!'     => 'KNE-Export erfolgreich!',
   'KNr. beim Kunden'            => 'KNr. beim Kunden',
+  'KOST Quantity'               => 'KOST-Menge',
   'Keep the project link the way it is.' => 'Die aktuelle Verknüpfung beibehalten.',
-  'Keine Suchergebnisse gefunden!' => 'Keine Suchergebnisse gefunden!',
-  'Konten'                      => 'Konten',
-  'L'                           => 'L',
+  'Known Column'                => 'Bekannte Spalte',
+  'L'                           => 'T',
   'LIABILITIES'                 => 'PASSIVA',
   'LP'                          => 'LP',
   'LaTeX Templates'             => 'LaTeX-Vorlagen',
@@ -1492,20 +1949,19 @@ $self->{texts} = {
   'Language'                    => 'Sprache',
   'Language (database ID)'      => 'Sprache (Datenbank-ID)',
   'Language (name)'             => 'Sprache (Name)',
-  'Language deleted!'           => 'Sprache gelöscht!',
-  'Language missing!'           => 'Sprache fehlt!',
-  'Language saved!'             => 'Sprache gespeichert!',
   'Language settings'           => 'Spracheinstellungen',
   'Languages'                   => 'Sprachen',
   'Languages and translations'  => 'Sprachen und Übersetzungen',
   'Last Article Number'         => 'Letzte Artikelnummer',
   'Last Assembly Number'        => 'Letzte Erzeugnisnummer',
+  'Last Assortment Number'      => 'Letzte Sortimentsnummer',
   'Last Cost'                   => 'Einkaufspreis',
   'Last Credit Note Number'     => 'Letzte Gutschriftnummer',
   'Last Customer Number'        => 'Letzte Kundennummer',
+  'Last Dunning'                => 'Letzte Mahnung',
   'Last Invoice Number'         => 'Letzte Rechnungsnummer',
   'Last Purchase Delivery Order Number' => 'Letzte Lieferscheinnummer (Einkauf)',
-  'Last Purchase Order Number'  => 'Letzte Lieferantenautragsnummer',
+  'Last Purchase Order Number'  => 'Letzte Lieferantenauftragsnummer',
   'Last RFQ Number'             => 'Letzte Anfragenummer',
   'Last Sales Delivery Order Number' => 'Letzte Lieferscheinnummer (Verkauf)',
   'Last Sales Order Number'     => 'Letzte Auftragsnummer',
@@ -1517,56 +1973,67 @@ $self->{texts} = {
   'Last modification'           => 'Letzte Änderung',
   'Last opening balance or all transactions' => 'Letzte Eröffnungsbuchung oder alle Buchungen',
   'Last opening balance or start of year' => 'Letzte Eröffnungsbuchung oder Jahresanfang',
+  'Last ordernumber'            => 'letzte Bestellnummer',
   'Last row, description'       => 'Letzte Zeile, Artikelbeschreibung',
   'Last row, partnumber'        => 'Letzte Zeile, Nummer',
+  'Last row, qty'               => 'Letzte Zeile, Menge',
   'Last run at'                 => 'Letzte Ausführung um',
   'Last transaction'            => 'Letzte Buchung',
+  'Last update'                 => 'letzter Upload',
   'Lastcost'                    => 'Einkaufspreis',
   'Lastcost (with X being a number)' => 'Einkaufspreis (X ist eine fortlaufende Zahl)',
-  'Lead'                        => 'Kundenquelle',
-  'Leads'                       => 'Kundenquellen',
+  'Lastname'                    => 'Nachname',
+  'Leading and trailing whitespaces have been removed.' => 'Leerzeichen wurden vorne und hinten entfernt',
   'Left'                        => 'Links',
   'Letter'                      => 'Brief',
   'Letter Draft'                => 'Briefentwurf',
+  'Letter deleted'              => 'Brief gelöscht',
   'Letter saved!'               => 'Brief gespeichert!',
   'Letternumber'                => 'Briefnummer',
   'Letters'                     => 'Briefe',
   'Liability'                   => 'Passiva/Mittelherkunft',
   'Limit part selection'        => 'Artikelauswahl eingrenzen',
   'Line Total'                  => 'Zeilensumme',
-  'Line and column'             => 'Zeile und Spalte',
-  'Line endings'                => 'Zeilenumbr&uuml;che',
+  'Line endings'                => 'Zeilenumbrüche',
   'Link direction'              => 'Verknüpfungsrichtung',
   'Link to'                     => 'Verknüpfen mit',
+  'Link to invoice'             => 'Beleglink',
   'Link to the following project:' => 'Mit dem folgenden Projekt verknüpfen:',
   'Linked Records'              => 'Verknüpfte Belege',
   'Linked invoices'             => 'Verknüpfte Rechnungen',
   'Liquidity projection'        => 'Liquiditätsübersicht',
   'List Accounts'               => 'Konten anzeigen',
-  'List Languages'              => 'Sprachen anzeigen',
   'List Price'                  => 'Listenpreis',
   'List Printers'               => 'Drucker anzeigen',
   'List Transactions'           => 'Buchungsliste',
   'List Users, Clients and User Groups' => 'Benutzer, Mandanten und Benutzergruppen anzeigen',
+  'List all rows'               => 'Alle Reihen anzeigen',
   'List current background jobs' => 'Aktuelle Hintergrund-Jobs anzeigen',
   'List export'                 => 'Export anzeigen',
   'List of bank collections'    => 'Bankeinzugsliste',
   'List of bank transfers'      => 'Überweisungsliste',
   'List of custom variables'    => 'Liste der benutzerdefinierten Variablen',
   'List of database upgrades to be applied:' => 'Liste der noch einzuspielenden Datenbankupgrades:',
+  'List of jobs'                => 'Jobliste',
   'List of tax zones'           => 'Liste der Steuerzonen',
   'List open SEPA exports'      => 'Noch nicht ausgeführte SEPA-Exporte anzeigen',
-  'Load draft'                  => 'Entwurf laden',
+  'List time recordings of all staff members' => 'Zeiterfassungseinträge aller Mitarbeiter anzeigen',
+  'Listprice'                   => 'Listenpreis',
+  'Load'                        => 'Laden',
+  'Load an existing draft'      => 'Einen bestehenden Entwurf laden',
   'Load letter draft'           => 'Briefentwurf laden',
   'Load profile'                => 'Profil laden',
   'Loading...'                  => 'Wird geladen...',
   'Local Bank Code'             => 'Lokale Bankleitzahl',
   'Local Tax Office Preferences' => 'Angaben zum Finanzamt',
+  'Local account'               => 'Eigenes Konto',
   'Local account number'        => 'Lokale Kontonummer',
   'Local bank account'          => 'Lokales Bankkonto',
   'Local bank code'             => 'Lokale Bankleitzahl',
+  'Lock'                        => 'Festschreibung',
   'Lock System'                 => 'System sperren',
   'Lock and unlock installation' => 'Installation sperren/entsperren',
+  'Lock bookings'               => 'Buchungen festschreiben',
   'Lock file handling failed. Please verify that the directory "#1" is writeable by the webserver.' => 'Die Lockdateibehandlung schlug fehl. Bitte stellen Sie sicher, dass der Webserver das Verzeichnis "#1" beschreiben darf.',
   'Lockfile created!'           => 'System gesperrt!',
   'Lockfile removed!'           => 'System entsperrt!',
@@ -1577,11 +2044,23 @@ $self->{texts} = {
   'Logout now'                  => 'kivitendo jetzt verlassen',
   'Long Dates'                  => 'Lange Monatsnamen',
   'Long Description'            => 'Langtext',
+  'Long Description (invoices)' => 'Langtext (Rechnungen)',
+  'Long Description (quotations & orders)' => 'Langtext (Angebote & Aufträge)',
+  'Long Description for invoices' => 'Langtext für Rechnungen',
+  'Long Description for quotations & orders' => 'Langtext für Angebote & Aufträge',
+  'Longdescription dialog size percentage from main window (0 means fix values)' => 'Prozentuale Größe des Langtext-Dialogs im Verhältnis zum Hauptfenster (0 bedeutet feste Größe)',
+  'Loss'                        => 'Verlust',
+  'Loss carried forward account' => 'Verlustvortragskonto',
+  'Luxembourg'                  => 'Luxemburg',
   'MAILED'                      => 'Gesendet',
   'MD'                          => 'PT',
   'MIME type'                   => 'MIME-Typ',
-  'MT940'                       => 'MT940',
+  'MT940 file'                  => 'MT940-Datei',
   'MT940 import'                => 'MT940 Import',
+  'MT940 import preview'        => 'MT940-Import-Vorschau',
+  'MT940 import result'         => 'MT940-Import-Ergebnis',
+  'Mails'                       => 'E-Mails',
+  'Main Contact Person'         => 'Hauptansprechperson',
   'Main Preferences'            => 'Grundeinstellungen',
   'Main sorting'                => 'Hauptsortierung',
   'Make'                        => 'Lieferant',
@@ -1596,20 +2075,22 @@ $self->{texts} = {
   'Mandatory Departments'       => 'Benutzer muss Abteilungen vergeben',
   'Manually sent E-Mails will have their BCC field appended with this address. Will not trigger for employees without the right to send bcc, and will not apply to mails sent by automated jobs.' => 'Diese Mailadresse wird automatisch in das BCC Feld bei Mailversand kopiert. Hat keine Auswirkungen für Mitarbeiter ohne das Recht BCC zu versenden, und ignoriert wenn Mails automatisch versendet werden.',
   'Map'                         => 'Karte',
+  'Mappings (csv_import)'       => 'Spaltenzuordnungen',
   'Mar'                         => 'März',
   'March'                       => 'März',
   'Margepercent'                => 'Ertrag prozentual',
   'Margetotal'                  => 'Ertrag',
-  'Margins'                     => 'Seitenr&auml;nder',
-  'Mark as closed'              => 'Abschließen',
-  'Mark as paid?'               => 'Als bezahlt markieren?',
+  'Margins'                     => 'Seitenränder',
+  'Mark as booked'              => 'Als gebucht markieren',
+  'Mark as closed'              => 'Als geschlossen markieren',
+  'Mark as paid'                => 'Als bezahlt markieren',
   'Mark as shop article if column missing' => 'Als Shopartikel setzen, falls Spalte nicht vorhanden',
-  'Mark closed'                 => 'Als geschlossen markieren',
   'Marked as paid'              => 'Als bezahlt markiert',
   'Marked entries printed!'     => 'Markierte Einträge wurden gedruckt!',
   'Mass Create Print Sales Invoice from Delivery Order' => 'Massenerstellen und Ausdruck von Rechnungen aus Lieferscheinen',
   'Master Data'                 => 'Stammdaten',
   'Master Data Bin Text Deleted' => 'Gelöschte Stammdaten Freitext-Lagerplätze',
+  'Match Sales Invoice Serial numbers with inventory charge numbers?' => 'Gleiche die Seriennummern aus der VK-Rechnung mit den eingelagerten Chargennummern ab?',
   'Matching Price Rules can apply in one of three types:' => 'Preisregeln können Preise in drei Varianten vorschlagen:',
   'Max. Dunning Level'          => 'höchste Mahnstufe',
   'Maximal amount difference'   => 'maximale Betragsabweichung',
@@ -1618,9 +2099,15 @@ $self->{texts} = {
   'May '                        => 'Mai',
   'May set the BCC field when sending emails' => 'Beim Verschicken von E-Mails das Feld \'BCC\' setzen',
   'Meaning'                     => 'Bedeutung',
-  'Medium Number'               => 'Datentr&auml;gernummer',
+  'Media \'#1\' is not supported yet/anymore.' => 'Das Medium \'#1\' wird noch nicht oder nicht mehr unterstützt.',
+  'Medium Number'               => 'Datenträgernummer',
   'Memo'                        => 'Memo',
+  'Merchandise'                 => 'Handelsware',
+  'Merchandise (typeabbreviation)' => 'H',
   'Message'                     => 'Nachricht',
+  'Meta tag description'        => 'Metatag Beschreibung',
+  'Meta tag keywords'           => 'Metatag Keywords',
+  'Meta tag title'              => 'Metatag Titel',
   'Method'                      => 'Verfahren',
   'Microfiche'                  => 'Mikrofilm',
   'Minimum Amount'              => 'Mindestbetrag',
@@ -1630,124 +2117,170 @@ $self->{texts} = {
   'Missing Method!'             => 'Fehlender Voranmeldungszeitraum',
   'Missing Tax Authoritys Preferences' => 'Fehlende Angaben zum Finanzamt!',
   'Missing amount'              => 'Fehlbetrag',
+  'Missing configuration section "authentication/#1" in "config/kivitendo.conf".' => 'Fehlender Konfigurationsabschnitt "authentication/#1" in "config/kivitendo.conf".',
   'Missing parameter #1 in call to sub #2.' => 'Fehlender Parameter \'#1\' in Funktionsaufruf \'#2\'.',
   'Missing parameter (at least one of #1) in call to sub #2.' => 'Fehlernder Parameter (mindestens einer aus \'#1\') in Funktionsaufruf \'#2\'.',
   'Missing parameter for WebDAV file copy' => 'Fehlender Parameter für WebDAV Datei kopieren',
-  'Missing taxkeys in invoices with taxes.' => 'Fehlende Steuerschl&uuml;ssel in Rechnungen mit Steuern',
-  'Missing transport cost: #1  Are you sure?' => 'Fehlender Transportkosten-Artikel #1 Trotzdem speichern?',
+  'Missing taxkeys in invoices with taxes.' => 'Fehlende Steuerschlüssel in Rechnungen mit Steuern',
   'Mitarbeiter'                 => 'Mitarbeiter',
-  'Mixed (requires column "type")' => 'Gemischt (erfordert Spalte "type")',
+  'Mixed (requires column "type" or "pclass")' => 'Gemischt (Spalte "type" oder "pclass" notwendig',
   'Mobile'                      => 'Mobiltelefon',
   'Mobile1'                     => 'Mobil 1',
   'Mobile2'                     => 'Mobil 2',
+  'Modal Test'                  => 'Modals-Test',
   'Model'                       => 'Lieferanten-Art-Nr.',
   'Model (with X being a number)' => 'Lieferanten-Art-Nr. (X ist eine fortlaufende Zahl)',
+  'Modification date'           => 'Änderungsdatum',
   'Module'                      => 'Modul',
   'Module home page'            => 'Modul-Webseite',
   'Module name'                 => 'Modulname',
+  'Mon'                         => 'Mo',
   'Monat'                       => 'Monat',
+  'Monday'                      => 'Montag',
   'Month'                       => 'Monat',
+  'Month/Year'                  => 'Monat/Jahr',
   'Monthly'                     => 'monatlich',
-  'More than one #1 found matching, please be more specific.' => 'Mehr als ein #1 wurde gefunden, bitte geben Sie den Namen genauer an.',
   'More than one control file with the tag \'%s\' exist.' => 'Es gibt mehr als eine Kontrolldatei mit dem Tag \'%s\'.',
+  'More than one file selected, please set only one checkbox!' => 'Mehr als ein Element selektiert, bitte nur eine Box anklicken',
   'Multi mode not supported.'   => 'Multimodus wird nicht unterstützt.',
+  'Multiple addresses can be entered separated by commas.' => 'Mehrere Adressen können durch Kommata getrennt angegeben werden.',
   'MwSt. inkl.'                 => 'MwSt. inkl.',
   'Name'                        => 'Name',
+  'Name 2'                      => 'Name 2',
+  'Name 3'                      => 'Name 3',
   'Name and Street'             => 'Name und Straße',
   'Name does not make sense without any bsooqr options' => 'Option "Name in gewählten Belegen" wird ignoriert.',
   'Name in Selected Records'    => 'Name in gewählten Belegen',
   'Name of the goal/source (if field names remote_name and remote_name_1 exist they will be combined into field "remote_name")' => 'Name des Ziel- oder Quellkontos (wenn die Spalten remote_name und remote_name_1 existieren werden diese zu Feld "remote_name" zusammengefügt)',
+  'Need a image title'          => 'Benötige einen Titel für das Bild',
+  'Need a valid Shop Part for updating Part' => 'Benötige eine gültiges Shop Part Objekt, um den Artikel zu aktualisieren.',
+  'Need a workflow for Supplier Delivery Order' => 'Benötige zwingend einen Workflow-Vorgänger für den Beistell-Lieferschein',
+  'Need at least one original position for the workflow Order to Delivery Order!' => 'Benötige mindestens eine Position die aus dem Auftrag übernommen wurde, ansonsten ist der Workflow inkosistent.',
+  'Need charge number!'         => 'Benötige Chargennummer!',
   'Negative reductions are possible to model price increases.' => 'Negative Abschläge sind möglich um Aufschläge zu modellieren.',
   'Neither sections nor function blocks have been created yet.' => 'Es wurden bisher weder Abschnitte noch Funktionsblöcke angelegt.',
   'Net Income Statement'        => 'Einnahmenüberschußrechnung',
   'Net Value in delivery orders' => 'Netto mit Lieferschein',
   'Net amount'                  => 'Nettobetrag',
   'Net amount (for verification)' => 'Nettobetrag (zur Überprüfung)',
+  'Net amounts differ too much' => 'Nettobeträge weichen zu sehr ab.',
   'Net value in Order'          => 'Netto Auftrag',
+  'Net value in closed delivery orders' => 'Netto in geschlossenen Lieferscheinen',
   'Net value transferred in / out' => 'Netto ein- /ausgelagert',
   'Net value without delivery orders' => 'Netto ohne Lieferschein',
+  'Net.Turnover'                => 'Netto Umsatz',
+  'Netherlands'                 => 'Niederlande',
   'Netto Terms'                 => 'Zahlungsziel netto',
   'New Password'                => 'Neues Passwort',
   'New Purchase Price Rule'     => 'Neue Einkaufspreisregel',
   'New Sales Price Rule'        => 'Neue Verkaufspreisregel',
-  'New assembly'                => 'Neues Erzeugnis',
+  'New address'                 => 'Neue Adresse',
   'New client #1: The database configuration fields "host", "port", "name" and "user" must not be empty.' => 'Neuer Mandant #1: Die Datenbankkonfigurationsfelder "Host", "Port" und "Name" dürfen nicht leer sein.',
   'New client #1: The name must be unique and not empty.' => 'Neuer Mandant #1: Der Name darf nicht leer und muss eindeutig sein.',
   'New contact'                 => 'Neue Ansprechperson',
-  'New customer'                => 'Neuer Kunde',
   'New filter for tax accounts' => 'Neue Filter für Steuerkonten',
   'New invoice'                 => 'Neue Rechnung',
   'New name'                    => 'Neuer Name',
-  'New part'                    => 'Neue Ware',
   'New row, description'        => 'Neue Zeile, Artikelbeschreibung',
   'New row, partnumber'         => 'Neue Zeile, Nummer',
+  'New row, qty'                => 'Neue Zeile, Menge',
   'New sales order'             => 'Neuer Auftrag',
-  'New service'                 => 'Neue Dienstleistung',
   'New shipto'                  => 'Neue Lieferadresse',
-  'New vendor'                  => 'Neuer Lieferant',
+  'New shop orders'             => 'Neue Shopbestellungen',
   'New window/tab'              => 'Neues Fenster/Tab',
   'Next Dunning Level'          => 'Nächste Mahnstufe',
+  'Next month'                  => 'nächster Monat',
   'Next run at'                 => 'Nächste Ausführung um',
   'No'                          => 'Nein',
-  'No %s was found matching the search parameters.' => 'Es wurde kein %s gefunden, auf den die Suchparameter zutreffen.',
   'No 1:n or n:1 relation'      => 'Keine 1:n oder n:1 Beziehung',
+  'No AP Record Template for this vendor found, please add one' => 'Konnte keine Kreditorenbuchungsvorlage für diesen Lieferanten finden, bitte legen Sie eine an.',
+  'No AP template was found.'   => 'Keine Kreditorenbuchungsvorlage gefunden.',
+  'No Billing and ship to address, for Order Number #1 with ID Billing #2 and ID Shipping #3' => 'Keine Rechnungs- und Lieferadresse zur Bestellnummer #1 mit Rechnungs-ID #2 und Liefer-ID #3 gefunden',
   'No Company Address given'    => 'Keine Firmenadresse hinterlegt!',
   'No Company Name given'       => 'Kein Firmenname hinterlegt!',
   'No Customer was found matching the search parameters.' => 'Zu dem Suchbegriff wurde kein Endkunde gefunden',
+  'No GL template was found.'   => 'Keine Dialogbuchungsvorlage gefunden.',
   'No Journal'                  => 'Kein Journal',
+  'No Order Number'             => 'Keine Auftragsnummer',
+  'No Order items fetched'      => 'Keine Auftragspositionen gefunden',
+  'No Shopdescription'          => 'Keine Shop-Artikelbeschreibung',
+  'No Shopimages'               => 'Keine Shop-Bilder',
+  'No VAT Info for this Factur-X/ZUGFeRD invoice, please ask your vendor to add this for his Factur-X/ZUGFeRD data.' => 'Konnte keine UST-ID für diese Factur-X-/ZUGFeRD-Rechnungen finden, bitte fragen Sie bei Ihren Lieferanten nach, ob dieses Feld im Factur-X-/ZUGFeRD-Datensatz gesetzt wird.',
   'No Vendor was found matching the search parameters.' => 'Zu dem Suchbegriff wurde kein Händler gefunden',
-  'No acceptance statuses has been created yet.' => 'Es wurde noch kein Abnahmestatus angelegt.',
   'No action defined.'          => 'Keine Aktion definiert.',
+  'No address selected to delete' => 'Keine Adresse zum Löschen ausgewählt',
+  'No article has been selected yet.' => 'Es wurde noch kein Artikel ausgewählt.',
   'No articles have been added yet.' => 'Es wurden noch keine Artikel hinzugefügt.',
+  'No assembly has been selected yet.' => 'Es wurde noch kein Erzeugnis ausgewahlt.',
   'No background job has been created yet.' => 'Es wurden noch keine Hintergrund-Jobs angelegt.',
   'No bank account chosen!'     => 'Kein Bankkonto ausgewählt!',
+  'No bank account configured for bank code/BIC #1, account number/IBAN #2.' => 'Kein Bankkonto für BLZ/BIC #1, Kontonummer/IBAN #2 konfiguriert.',
+  'No bank account flagged for Factur-X/ZUGFeRD usage was found.' => 'Es wurde kein Bankkonto gefunden, das für Nutzung mit Factur-X/ZUGFeRD markiert ist.',
+  'No bank account flagged for QRBill usage was found.' => 'Kein Bankkonto markiert für QR-Rechnung gefunden.',
   'No bank information has been entered in this customer\'s master data entry. You cannot create bank collections unless you enter bank information.' => 'Für diesen Kunden wurden in seinen Stammdaten keine Kontodaten hinterlegt. Solange dies nicht geschehen ist, können Sie keine Überweisungen für den Lieferanten anlegen.',
   'No bank information has been entered in this vendor\'s master data entry. You cannot create bank transfers unless you enter bank information.' => 'Für diesen Lieferanten wurden in seinen Stammdaten keine Kontodaten hinterlegt. Solange dies nicht geschehen ist, können Sie keine Überweisungen für den Lieferanten anlegen.',
-  'No bins have been added to this warehouse yet.' => 'Es wurden zu diesem Lager noch keine Lagerpl&auml;tze angelegt.',
-  'No business has been created yet.' => 'Es wurden noch kein Kunden-/Lieferantentyp erfasst.',
+  'No billing city'             => 'Die Stadt für die Rechnungsadresse fehlt',
+  'No bins have been added to this warehouse yet.' => 'Es wurden zu diesem Lager noch keine Lagerplätze angelegt.',
+  'No carry-over chart configured!' => 'Kein Saldenvortragskonto konfiguriert!',
   'No changes since previous version.' => 'Keine Änderungen seit der letzten Version.',
   'No clients have been created yet.' => 'Es wurden noch keine Mandanten angelegt.',
-  'No complexities has been created yet.' => 'Es wurden noch keine Komplexitätsgrade angelegt.',
   'No contact selected to delete' => 'Keine Ansprechperson zum Löschen ausgewählt',
+  'No contra account selected!' => 'Kein Gegenkonto ausgewählt!',
+  'No custom data exports have been created yet.' => 'Es wurden noch keine benutzerdefinierten Datenexporte angelegt.',
+  'No customer email'           => 'Die E-Mail-Adresse des Kunden fehlt',
   'No customer has been selected yet.' => 'Es wurde noch kein Kunde ausgewählt.',
+  'No customer selected or found!' => 'Kein Kunde selektiert oder keinen gefunden!',
   'No data was found.'          => 'Es wurden keine Daten gefunden.',
   'No default currency'         => 'Keine Standardwährung',
+  'No default value'            => 'Kein Standardwert',
   'No delivery orders have been selected.' => 'Es wurden keine Lieferscheine ausgewählt.',
   'No delivery term has been created yet.' => 'Es wurden noch keine Lieferbedingungen angelegt',
-  'No department has been created yet.' => 'Es wurde noch keine Abteilung erfasst.',
-  'No draft was found.'         => 'Kein Entwurf gefunden.',
-  'No dunnings have been selected for printing.' => 'Es wurden keine Mahnungen zum Drucken ausgew&auml;hlt.',
+  'No dunnings have been selected for printing.' => 'Es wurden keine Mahnungen zum Drucken ausgewählt.',
+  'No email for current user #1 defined.' => 'Keine E-Mail-Adresse für den angemeldeten Benutzer #1 definiert.',
+  'No email for user with login #1 defined.' => 'Keine E-Mail-Adresse für den Benutzer mit dem Login #1 definiert.',
+  'No email recipient for customer #1 defined.' => 'Keine E-Mail-Adresse (Rechnungs- oder global) für den Kunden #1 definiert.',
+  'No end date given, setting to today' => 'Kein Enddatum gegeben, setze Enddatum auf heute',
+  'No entries can be imported.' => 'Es können keine Einträge importiert werden.',
+  'No entries have been imported yet.' => 'Es wurden noch keine Einträge importiert.',
+  'No entries have been selected.' => 'Es wurden keine Einträge ausgewählt.',
   'No errors have occurred.'    => 'Es sind keine Fehler aufgetreten.',
   'No file has been uploaded yet.' => 'Es wurde noch keine Datei hochgeladen.',
+  'No file selected, please set one checkbox!' => 'Kein Element selektiert,bitte eine Box anklicken',
+  'No file uploaded yet'        => 'Keine Datei hochgeladen',
+  'No filename exists!'         => 'Kein Dateiname angegeben',
   'No function blocks have been created yet.' => 'Es wurden noch keine Funktionsblöcke angelegt.',
   'No groups have been created yet.' => 'Es wurden noch keine Gruppen angelegt.',
   'No internal phone extensions have been configured yet.' => 'Es wurden noch keine internen Durchwahlen konfiguriert.',
   'No invoices have been selected.' => 'Es wurden keine Rechnungen ausgewählt.',
-  'No or an unknown authenticantion module specified in "config/kivitendo.conf".' => 'Es wurde kein oder ein unbekanntes Authentifizierungsmodul in "config/kivitendo.conf" angegeben.',
-  'No part was found matching the search parameters.' => 'Es wurde kein Artikel gefunden, auf den die Suchparameter zutreffen.',
+  'No part was selected.'       => 'Es wurde kein Artikel ausgewählt',
   'No payment term has been created yet.' => 'Es wurden noch keine Zahlungsbedingungen angelegt.',
   'No picture has been uploaded' => 'Es wurde kein Bild hochgeladen',
   'No picture uploaded yet'     => 'Noch kein Bild hochgeladen',
-  'No predefined texts has been created yet.' => 'Es wurden noch keine vordefinierten Textblöcken angelegt.',
   'No prices will be updated because no prices have been entered.' => 'Es werden keine Preise aktualisiert, weil keine gültigen Preisänderungen eingegeben wurden.',
   'No print templates have been created for this client yet. Please do so in the client configuration.' => 'Für diesen Mandanten wurden noch keine Druckvorlagen angelegt. Bitte holen Sie dies in der Mandantenkonfiguration nach.',
   'No printers have been created yet.' => 'Es wurden noch keine Drucker angelegt.',
   'No problems were recognized.' => 'Es wurden keine Probleme gefunden.',
-  'No project status has been created yet.' => 'Es wurde noch kein Projektstatus angelegt.',
-  'No project type has been created yet.' => 'Es wurden noch keine Projekttypen angelegt.',
+  'No profit and loss carried forward chart configured!' => 'Kein Verlustvortragskonto konfiguriert!',
+  'No profit carried forward chart configured!' => 'Kein Gewinnvortragskonto konfiguriert!',
   'No quotations or orders have been created yet.' => 'Es wurden noch keine Angebote oder Aufträge angelegt.',
   'No report with id #1'        => 'Es gibt keinen Report mit der Id #1',
-  'No requirement spec statuses has been created yet.' => 'Es wurden noch keine Pflichtenheftstatus angelegt.',
   'No requirement spec templates have been created yet.' => 'Es wurden noch keine Pflichtenheftvorlagen angelegt.',
-  'No requirement spec type has been created yet.' => 'Es wurden noch keine Pflichtenhefttypen angelegt.',
-  'No risks level has been created yet.' => 'Es wurden noch keine Risikograde angelegt.',
+  'No results.'                 => 'Keine Artikel',
+  'No search results found!'    => 'Keine Suchergebnisse gefunden!',
   'No sections created yet'     => 'Keine Abschnitte erstellt',
   'No sections have been created so far.' => 'Bisher wurden noch keine Abschnitte angelegt.',
   'No sections have been created yet.' => 'Es wurden noch keine Abschnitte angelegt.',
+  'No shipto city'              => 'Die Stadt für die Lieferadresse fehlt',
   'No shipto selected to delete' => 'Keine Lieferadresse zum Löschen ausgewählt',
+  'No start date given, setting to #1' => 'Kein Startdatum gegeben, setze Startdatum auf #1',
+  'No stock to transfer'        => 'Keine Lagerbewegungen vorhanden',
+  'No such job #1 in the database.' => 'Hintergrund-Job #1 existiert nicht mehr.',
   'No summary account'          => 'Kein Sammelkonto',
+  'No superuser credentials were entered.' => 'Es wurden keine Super-Benutzer-Anmeldedaten eingegeben.',
+  'No template has been selected yet.' => 'Es wurde noch keine Vorlage ausgewählt.',
   'No text blocks have been created for this position.' => 'Für diese Position wurden noch keine Textblöcke angelegt.',
   'No text has been entered yet.' => 'Es wurde noch kein Text eingegeben.',
+  'No time recordings to convert' => 'Es sind keine Zeiterfassungseinträge zu konvertieren',
   'No title yet'                => 'Bisher ohne Titel',
   'No transaction on chart bank chosen!' => 'Keine Buchung auf Bankkonto gewählt.',
   'No transaction selected!'    => 'Keine Transaktion ausgewählt',
@@ -1756,12 +2289,15 @@ $self->{texts} = {
   'No users have been created yet.' => 'Es wurden noch keine Benutzer angelegt.',
   'No valid number entered for pricegroup "#1".' => 'Für Preisgruppe "#1" wurde keine gültige Nummer eingegeben.',
   'No vendor has been selected yet.' => 'Es wurde noch kein Lieferant ausgewählt.',
+  'No vendor selected or found!' => 'Kein Lieferant ausgewählt oder gefunden!',
   'No warehouse has been created yet or the quantity of the bins is not configured yet.' => 'Es wurde noch kein Lager angelegt, bzw. die dazugehörigen Lagerplätze sind noch nicht konfiguriert.',
+  'No year given for method year' => 'Für diese Exportmethode wird ein Jahr benötigt',
   'No.'                         => 'Position',
   'No/individual shipping address' => 'Keine/individuelle Lieferadresse',
   'None'                        => 'Kein',
   'None (PriceSource Discount)' => 'Freier Rabatt',
   'None (PriceSource)'          => 'Freier Preis',
+  'None (typeabbreviation)'     => '-',
   'Normal'                      => 'Normal',
   'Normal users cannot log in.' => 'Normale Benutzer können sich nicht anmelden.',
   'Normalize Customer / Vendor names' => 'Normalisierung Kunden- / Lieferantennamen',
@@ -1769,17 +2305,19 @@ $self->{texts} = {
   'Not Discountable'            => 'Nicht rabattierfähig',
   'Not delivered'               => 'Nicht geliefert',
   'Not done yet'                => 'Noch nicht fertig',
+  'Not enough in stock for the serial number #1' => 'Nicht genug auf Lager von der Seriennummer #1',
   'Not obsolete'                => 'Gültig',
-  'Not yet implemented!'        => 'Noch nicht implementiert!',
+  'Not yet implemented'         => 'Noch nicht implementiert',
   'Note'                        => 'Hinweis',
+  'Note that parameter names must not be quoted.' => 'Beachten Sie, dass Parameternamen nicht in Anführungszeichen stehen dürfen.',
   'Note: Taxkeys must have a "valid from" date, and will not behave correctly without.' => 'Hinweis: Steuerschlüssel sind fehlerhaft ohne "Gültig ab" Datum',
+  'Note: the object is already in use. Therefore some values cannot be changed.' => 'Anmerkung: das Objekt ist bereits in Benutzung. Einige Werte können daher nicht geändert werden.',
   'Notes'                       => 'Bemerkungen',
   'Notes (translation for #1)'  => 'Bemerkungen (Übersetzung für #1)',
   'Notes (will appear on hard copy)' => 'Bemerkungen',
   'Notes for customer'          => 'Bemerkungen beim Kunden',
-  'Notes for vendor'            => 'Bemerkungen beim Lieferanten',
-  'Nothing has been selected for removal.' => 'Es wurde nichts f&uuml;r eine Entnahme ausgew&auml;hlt.',
-  'Nothing has been selected for transfer.' => 'Es wurde nichts zum Umlagern ausgew&auml;hlt.',
+  'Nothing has been selected for removal.' => 'Es wurde nichts für eine Entnahme ausgewählt.',
+  'Nothing has been selected for transfer.' => 'Es wurde nichts zum Umlagern ausgewählt.',
   'Nothing selected!'           => 'Es wurde nichts ausgewählt!',
   'Nothing stocked yet.'        => 'Noch nichts eingelagert.',
   'Nothing will be created or deleted at this stage!' => 'In diesem Schritt wird nichts angelegt oder gelöscht!',
@@ -1788,16 +2326,22 @@ $self->{texts} = {
   'Number'                      => 'Nummer',
   'Number Format'               => 'Zahlenformat',
   'Number missing in Row'       => 'Nummer fehlt in Zeile',
-  'Number of bins'              => 'Anzahl Lagerpl&auml;tze',
+  'Number of Data: '            => 'Anzahl Datensätze',
+  'Number of bins'              => 'Anzahl Lagerplätze',
   'Number of columns of custom variables in form details (second row)' => 'Anzahl der Spalten für benutzerdef. Variablen in den Formulardetails (zweite Positionszeile)',
   'Number of copies'            => 'Anzahl Kopien',
+  'Number of data sets'         => 'Anzahl Datensätze',
+  'Number of data uploaded:'    => 'Uploaded Datensätze',
+  'Number of delivery orders created:' => 'Anzahl erzeugter Lieferscheine:',
+  'Number of delivery orders printed:' => 'Anzahl gedruckter Lieferscheine:',
   'Number of entries changed: #1' => 'Anzahl geänderter Einträge: #1',
   'Number of invoices'          => 'Anzahl Rechnungen',
   'Number of invoices created:' => 'Anzahl erstellter Rechnungen:',
   'Number of invoices printed:' => 'Anzahl gedruckter Rechnungen:',
   'Number of invoices to create' => 'Anzahl zu erstellender Rechnungen',
   'Number of months'            => 'Anzahl Monate',
-  'Number of new bins'          => 'Anzahl neuer Lagerpl&auml;tze',
+  'Number of new bins'          => 'Anzahl neuer Lagerplätze',
+  'Number of orders created:'   => 'Anzahl Aufträge erstellt',
   'Number pages'                => 'Seiten nummerieren',
   'Number variables: \'PRECISION=n\' forces numbers to be shown with exactly n decimal places.' => 'Zahlenvariablen: Mit \'PRECISION=n\' erzwingt man, dass Zahlen mit n Nachkommastellen formatiert werden.',
   'OB Transaction'              => 'EB-Buchung',
@@ -1812,12 +2356,16 @@ $self->{texts} = {
   'On'                          => 'An',
   'On Hand'                     => 'Auf Lager',
   'On Order'                    => 'Ist bestellt',
+  'On the next page the type of all variables can be set.' => 'Auf der folgenden Seite können die Typen aller Variablen gesetzt werden.',
   'One of the columns "qty" or "target_qty" must be given. If "target_qty" is given, the quantity to transfer for each transfer will be calculate, so that the quantity for this part, warehouse and bin will result in the given "target_qty" after each transfer.' => 'Eine der Spalten "qty" oder "target_qty" muss angegeben werden. Wird "target_qty" angegeben, so wird die zu bewegende Menge für jede Lagerbewegung so berechnet, dass die Lagermenge für diesen Artikel, Lager und Lagerplatz nach jeder Lagerbewegung der angegebenen Zielmenge entspricht.',
+  'One of the units used (#1) cannot be mapped to a known unit code from the UN/ECE Recommendation 20 list.' => 'Eine der verwendeten Einheiten (#1) kann keinem der bekannten Einheiten-Codes aus der Liste UN/ECE Recommendation 20 zugeordnet werden.',
   'One or more Perl modules missing' => 'Ein oder mehr Perl-Module fehlen',
   'Onhand only sets the quantity in master data, not in inventory. This is only a legacy info field and will be overwritten as soon as a inventory transfer happens.' => 'Das Import-Feld Auf Lager setzt nur die Menge in den Stammdaten, nicht im Lagerbereich. Dies ist historisch gewachsen nur ein Informationsfeld was mit dem tatsächlichen Wert überschrieben wird, sobald eine wirkliche Lagerbewegung stattfindet (DB-Trigger).',
-  'Only Warnings and Errors'    => 'Nur Warnungen und Fehler',
+  'Only Lines with Notes or Errors' => 'Nur Zeilen mit Bemerkungen oder Fehlern',
+  'Only Price'                  => 'Nur Preis',
+  'Only Stock'                  => 'Nur Bestand',
   'Only booked accounts'        => 'Nur bebuchte Konten',
-  'Only due follow-ups'         => 'Nur f&auml;llige Wiedervorlagen',
+  'Only due follow-ups'         => 'Nur fällige Wiedervorlagen',
   'Only groups that have been configured for the client the user logs in to will be considered.' => 'Allerdings werden nur diejenigen Gruppen herangezogen, die für den Mandanten konfiguriert sind.',
   'Only list customer\'s projects in sales records' => 'Nur Projekte des Kunden in Verkaufsbelegen anzeigen',
   'Only run tests from this file:' => 'Nur Tests aus dieser Datei ausführen:',
@@ -1825,15 +2373,20 @@ $self->{texts} = {
   'Oops. No valid action found to dispatch. Please report this case to the kivitendo team.' => 'Ups. Es wurde keine gültige Funktion zum Aufrufen gefunden. Bitte berichten Sie diesen Fall den kivitendo-Entwicklern.',
   'Open'                        => 'Offen',
   'Open Amount'                 => 'Offener Betrag',
+  'Open Amount at Last Payment Date' => 'Offener Betrag zum letzten Zahlungseingang',
+  'Open Items'                  => 'Offene Posten',
+  'Open Orders'                 => 'Offene Aufträge',
   'Open a further kivitendo window or tab' => 'Weiteres kivitendo-Fenster/-Tab öffnen',
   'Open amount'                 => 'offener Betrag',
-  'Open in new window'          => 'In neuem Fenster &ouml;ffnen.',
+  'Open in new window'          => 'In neuem Fenster öffnen.',
   'Open invoice'                => 'Offene Rechnungen',
   'Open new tab'                => 'Neuen Tab öffnen',
   'Open sales delivery orders'  => 'Offene Verkaufslieferscheine',
-  'Open this Website'           => 'Homepage in neuem Fenster &ouml;ffnen',
+  'Open this Website'           => 'Homepage in neuem Fenster öffnen',
   'OpenDocument/OASIS'          => 'OpenDocument/OASIS',
   'Openings'                    => 'Öffnungszeiten',
+  'Option'                      => 'Option',
+  'Optional'                    => 'Optional',
   'Optional comment'            => 'Optionaler Kommentar',
   'Options'                     => 'Optionen',
   'Or download the whole Installation Documentation as PDF (350kB) for off-line study (currently in German Language): ' => 'Oder laden Sie die komplette Installationsbeschreibung als PDF (350kB) herunter: ',
@@ -1844,27 +2397,38 @@ $self->{texts} = {
   'Order Number missing!'       => 'Auftragsnummer fehlt!',
   'Order amount'                => 'Auftragswert',
   'Order deleted!'              => 'Auftrag gelöscht!',
+  'Order item search'           => 'Auftragsartikelsuche',
+  'Order number invalid. Must be less then or equal to 7 digits after prefix.' => 'Auftragsnummer ungültig. (kleiner/gleich 7 Stellen nach Prefix)',
   'Order probability'           => 'Auftragswahrscheinlichkeit',
   'Order probability & expected billing date' => 'Auftragswahrscheinlichkeit & vorrauss. Abrechnungsdatum',
   'Order value periodicity'     => 'Auftragswert basiert auf Periodizität',
   'Order/Item row name'         => 'Name der Auftrag-/Positions-Zeilen',
+  'Order/Item/Stock row name'   => 'Name der Auftrag-/Positions-/Lager-Zeilen',
+  'Order/RFQ Number'            => 'Belegnummer',
   'OrderItem'                   => 'Position',
   'Ordered'                     => 'Von Kunden bestellt',
   'Orders'                      => 'Aufträge',
   'Orders / Delivery Orders deleteable' => 'Aufträge / Lieferscheine löschbar',
+  'Orders to fetch'             => 'Anzahl Bestellungen holen',
+  'Orders to fetch neeeds a positive Integer' => 'Die Anzahl der zu holenden Aufträge muss eine positive Ganzzahl sein',
   'Orientation'                 => 'Seitenformat',
+  'Orig. Size w/h'              => 'Orig. Größe b/h',
+  'Origin of personal data'     => 'Herkunft der personenbezogenen Daten',
   'Orphaned'                    => 'Nie benutzt',
   'Orphaned currencies'         => 'Verwaiste Währungen',
   'Other Matches'               => 'Andere Treffer',
+  'Other party'                 => 'Andere Partei',
+  'Other recipients'            => 'Weitere EmpfängerInnen',
   'Other users\' follow-ups'    => 'Wiedervorlagen anderer Benutzer',
   'Other values are ignored.'   => 'Andere Eingaben werden ignoriert.',
   'Others'                      => 'Andere',
   'Otherwise the variable is only available for printing.' => 'Andernfalls steht die Variable nur beim Ausdruck zur Verfügung.',
   'Otherwise you can simply check create warehouse and bins and define a name for the warehouse (Bins will be created automatically) and then continue' => 'Andernfalls einfach <b>"Automatisches Zuweisen der Lagerplätze"</b>  anhaken und einen Namen für das Lager vergeben, bzw. per Auswahl auswählen (Lagerplätze werden dann automatisch hinzugefügt) danach auf weiter',
+  'Our routing id at customer'  => 'Unsere Leitweg-ID beim Kunden',
   'Out of balance transaction!' => 'Buchung ist nicht ausgeglichen!',
   'Out of balance!'             => 'Summen stimmen nicht überein!',
   'Output Number Format'        => 'Zahlenformat (Ausgabe)',
-  'Outputformat'                => 'Ausgabeformat',
+  'Overdue invoices'            => 'Überfällige Rechnungen',
   'Overdue sales quotations and requests for quotations' => 'Überfällige Angebote und Preisanfragen',
   'Override'                    => 'Override',
   'Override invoice language'   => 'Diese Sprache verwenden',
@@ -1873,12 +2437,16 @@ $self->{texts} = {
   'Own bank account number or IBAN' => 'Eigene Kontonummer oder IBAN',
   'Own bank code'               => 'Eigene Bankleitzahl',
   'Owner of account'            => 'Kontoinhaber',
-  'PAYMENT POSTED'              => 'Zahlung gebucht',
+  'PAYMENT POSTED'              => 'Rechnung gebucht',
   'PDF'                         => 'PDF',
   'PDF (OpenDocument/OASIS)'    => 'PDF (OpenDocument/OASIS)',
+  'PDF export'                  => 'PDF-Export',
   'PDF export -- options'       => 'PDF-Export -- Optionen',
+  'PDF export with attachments' => 'Als PDF mit Anhängen exportieren',
+  'PLZ Grosskunden'             => 'PLZ Grosskunden',
   'POSTED'                      => 'Gebucht',
   'POSTED AS NEW'               => 'Als neu gebucht',
+  'PREVIEWED'                   => 'Druckvorschau',
   'PRINTED'                     => 'Gedruckt',
   'Package name'                => 'Paketname',
   'Packing Lists'               => 'Lieferschein',
@@ -1886,41 +2454,60 @@ $self->{texts} = {
   'Page #1/#2'                  => 'Seite #1/#2',
   'Paid'                        => 'bezahlt',
   'Paid amount'                 => 'Bezahlter Betrag',
+  'Parsing the XMP metadata failed.' => 'Parsen der XMP-Metadaten schlug fehl.',
   'Part'                        => 'Ware',
-  'Part "#1" has chargenumber or best before date set. So it cannot be transfered automaticaly.' => 'Bei Artikel "#1" ist eine Chargenummer oder ein Mindesthaltbarkeitsdatum vergeben. Deshalb kann dieser Artikel nicht automatisch ausgelagert werden.',
+  'Part "#1" has chargenumber or best before date set. So it cannot be transfered automatically.' => 'Bei Artikel "#1" ist eine Chargenummer oder ein Mindesthaltbarkeitsdatum vergeben. Deshalb kann dieser Artikel nicht automatisch ausgelagert werden.',
+  'Part #1 exists in warehouse #2, but not in warehouse #3 ' => 'Artikel #1 existiert im Lager #2, aber nicht im Lager #3',
   'Part (database ID)'          => 'Artikel (Datenbank-ID)',
+  'Part (typeabbreviation)'     => 'W',
+  'Part Classification'         => 'Artikel-Klassifizierung',
   'Part Description'            => 'Artikelbeschreibung',
+  'Part Description is too long for this Shopware version. It should have lower than 255 characters.' => 'Artikelbeschreibung enthält mehr als 255 Zeichen. Shopware in dieser Version kann nur Artikelbeschreibungen mit weniger als 255 Zeichen verarbeiten.',
   'Part Description missing!'   => 'Artikelbezeichnung fehlt!',
   'Part Notes'                  => 'Bemerkungen',
   'Part Number'                 => 'Artikelnummer',
   'Part Number missing!'        => 'Artikelnummer fehlt!',
+  'Part Test'                   => 'Artikel-Test',
+  'Part Type'                   => 'Artikel-Typ',
+  'Part Unit'                   => 'Einheit',
+  'Part classifications'        => 'Artikel-Klassifizierungen',
+  'Part marked as "Shop part"'  => 'Markiert als Shopartikel',
   'Part picker'                 => 'Artikelauswahl',
+  'Part successful counted'     => 'Der Artikel wurde gezählt',
+  'Part with partnumber: #1 not found' => 'Artikel mit Artikelnummer #1 wurde nicht gefunden',
+  'PartClassAbbreviation'       => 'Abkürzung der Artikel-Klassifizierung',
+  'Part_br_Description'         => 'Beschreibung',
+  'Partdescriptipion'           => 'Beschreibung',
   'Partial invoices'            => 'Teilrechnungen',
   'Partnumber'                  => 'Artikelnummer',
-  'Partnumber must not be set to empty!' => 'Die Artikelnummer darf nicht auf leer ge&auml;ndert werden.',
-  'Partnumber not unique!'      => 'Artikelnummer bereits vorhanden!',
   'Parts'                       => 'Waren',
+  'Parts Classification'        => 'Artikel-Klassifizierung',
   'Parts Inventory'             => 'Warenliste',
   'Parts Master Data'           => 'Artikelstammdaten',
-  'Parts must have an entry type.' => 'Waren m&uuml;ssen eine Buchungsgruppe haben.',
   'Parts with existing part numbers' => 'Artikel mit existierender Artikelnummer',
   'Parts, services and assemblies' => 'Waren, Dienstleistungen und Erzeugnisse',
+  'Partsgroup'                  => 'Warengruppe',
   'Partsgroup (database ID)'    => 'Warengruppe (Datenbank-ID)',
   'Partsgroup (name)'           => 'Warengruppe (Name)',
+  'Partsgroup is required for parts' => 'Warengruppe ist Pflichtfeld für Artikel',
+  'Partsgroups'                 => 'Warengruppen',
   'Partsgroups where variables are shown' => 'Warengruppen, bei denen Variablen angezeigt werden',
   'Password'                    => 'Passwort',
   'Paste'                       => 'Einfügen',
   'Paste template'              => 'Vorlage einfügen',
+  'Path'                        => 'Pfad',
+  'Payable account'             => 'Verbindlichkeitskonto',
   'Payables'                    => 'Verbindlichkeiten',
   'Payment'                     => 'Zahlungsausgang',
   'Payment / Delivery Options'  => 'Zahlungs- und Lieferoptionen',
+  'Payment Date'                => 'Leistungsdatum',
   'Payment Reminder'            => 'Zahlungserinnerung',
   'Payment Terms'               => 'Zahlungsbedingungen',
   'Payment Terms missing in row ' => 'Zahlungsfrist fehlt in Zeile ',
+  'Payment bookings disallowed. After the booking this record may be suggested with the amount of \'#1\' or otherwise has to be choosen manually. No automatic payment booking will be done to chart \'#2\'.' => 'Zahlungsbuchungen nicht erlaubt. Nach der Verbuchung kann dieser Beleg in der Vorschlagsliste mit dem Zahlungs-Betrag von \'#1\' erscheinen, ansonsten muss dieser manuell verknüpft werden. Es wird nicht automatisch eine Zahlungsbuchung auf das Konto \'#2\' durchgeführt.',
   'Payment date missing!'       => 'Tag der Zahlung fehlt!',
-  'Payment description'         => 'Beschreibung der Zahlungsbedingung',
-  'Payment description detail'  => 'Langtext (Detail) der Zahlungsbedingung',
-  'Payment list as PDF'         => 'Zahlungsliste als PDF',
+  'Payment description'         => 'Zahlungsbedingung',
+  'Payment list'                => 'Zahlungsliste',
   'Payment posted!'             => 'Zahlung gebucht!',
   'Payment terms'               => 'Zahlungsbedingungen',
   'Payment terms (database ID)' => 'Zahlungsbedingungen (Datenbank-ID)',
@@ -1933,18 +2520,25 @@ $self->{texts} = {
   'Perform check when a purchase invoice or a payment for a purchase invoice is posted?' => 'Prüfung durchführen, wenn eine Einkaufsrechnung oder ein Zahlungsausgang hierfür gebucht wird?',
   'Perform check when a sales invoice or a payment for a sales invoice is posted?' => 'Prüfung durchführen, wenn eine Verkaufsrechnung oder ein Zahlungseingang hierfür gebucht wird?',
   'Perform check when an ap transaction is posted?' => 'Prüfung durchführen, wenn eine Kreditorenbuchung gebucht wird?',
-  'Perform check when an ar transaction is posted?' => 'Prüfung durchführen, wenn eine Debiotorenbuchung gebucht wird?',
+  'Perform check when an ar transaction is posted?' => 'Prüfung durchführen, wenn eine Debitorenbuchung gebucht wird?',
   'Period'                      => 'Zeitraum',
   'Period:'                     => 'Zeitraum:',
   'Periodic Invoices'           => 'Wiederkehrende Rechnungen',
   'Periodic inventory'          => 'Aufwandsmethode',
   'Periodic invoices active'    => 'Wiederkehrende Rechnungen aktiv',
   'Periodic invoices inactive'  => 'Wiederkehrende Rechnungen inaktiv',
+  'Permissions for invoices'    => 'Ansehrechte für Rechnungen',
   'Perpetual inventory'         => 'Bestandsmethode',
-  'Personal settings'           => 'Pers&ouml;nliche Einstellungen',
+  'Personal settings'           => 'Persönliche Einstellungen',
   'Phone'                       => 'Telefon',
+  'Phone Notes'                 => 'Telefonnotizen',
   'Phone extension'             => 'Durchwahl',
   'Phone extension missing in user configuration' => 'Durchwahl fehlt in der Benutzerkonfiguration',
+  'Phone note has been created.' => 'Die Telefonnotiz wurde angelegt.',
+  'Phone note has been deleted.' => 'Die Telefonnotiz wurde gelöscht.',
+  'Phone note has been updated.' => 'Die Telefonnotiz wurde aktualisiert.',
+  'Phone note needs a subject and a body.' => 'Eine Telefonnotiz muss einen Betreff und einen Text haben.',
+  'Phone note not found for this order.' => 'Diese Telefonnotiz wurde für dieses Dokument nicht gefunden.',
   'Phone password'              => 'Telefonpasswort',
   'Phone password missing in user configuration' => 'Telefonpasswort fehlt in der Benutzerkonfiguration',
   'Phone1'                      => 'Telefon 1 ',
@@ -1955,115 +2549,170 @@ $self->{texts} = {
   'Pictures for search parts'   => 'Bilder für Warensuche',
   'Please Check the bank information for each customer:' => 'Bitte überprüfen Sie die Bankinformationen der Kunden:',
   'Please Check the bank information for each vendor:' => 'Bitte überprüfen Sie die Kontoinformationen der Lieferanten:',
-  'Please ask your administrator to create warehouses and bins.' => 'Bitten Sie Ihren Administrator, dass er Lager und Lagerpl&auml;tze anlegt.',
+  'Please add a valid VAT-ID for this vendor: #1' => 'Bitte fügen Sie für den folgenden Lieferanten eine gültige UStID-Nummer hinzu: #1',
+  'Please ask your administrator to create warehouses and bins.' => 'Bitten Sie Ihren Administrator, dass er Lager und Lagerplätze anlegt.',
   'Please change the partnumber of the following parts and run the update again:' => 'Bitte ändern Sie daher die Artikelnummer folgender Artikel:',
+  'Please choose a part.'       => 'Bitte wählen Sie einen Artikel aus.',
   'Please choose for which categories the taxes should be displayed (otherwise remove the ticks):' => 'Bitte wählen Sie für welche Kontoart die Steuer angezeigt werden soll (ansonsten einfach die Häkchen entfernen)',
+  'Please choose the action to be processed for your target quantity:' => 'Bitte wählen Sie eine Aktion, die mit Ihrer gezählten Zielmenge durchgeführt werden soll:',
+  'Please configure the carry over and profit and loss accounts for year-end closing in the client configuration!' => 'Bitte konfigurieren Sie in der Mandantenkonfiguration das Saldenvortragskonto, das Gewinnvortragskonto und das Verlustvortragskonto!',
   'Please contact your administrator or a service provider.' => 'Bitte kontaktieren Sie Ihren Administrator oder einen Dienstleister.',
   'Please contact your administrator.' => 'Bitte wenden Sie sich an Ihren Administrator.',
   'Please correct the settings and try again or deactivate that client.' => 'Bitte korrigieren Sie die Einstellungen und versuchen Sie es erneut, oder deaktivieren Sie diesen Mandanten.',
-  'Please create a CSV import profile called "MT940" for the import type bank transactions:' => 'Bitte erstellen Sie ein CSV Import Profil mit dem Namen "MT940" für den Importtyp Bankbewegungen',
-  'Please create/copy a template named letter.tex in your client template dir' => 'Bitte erstellen / kopieren Sie eine Druckvorlage namens letter.tex in Ihren Mandantenvorlagen-Ordner',
   'Please define a taxkey for the following taxes and run the update again:' => 'Bitte definieren Sie einen Steuerschlüssel für die folgenden Steuern und starten Sie dann das Update erneut:',
   'Please do so in the administration area.' => 'Bitte erledigen Sie dies im Administrationsbereich.',
   'Please enter a profile name.' => 'Bitte geben Sie einen Profilnamen an.',
   'Please enter the currency you are working with.' => 'Bitte geben Sie die Währung an, mit der Sie arbeiten.',
   'Please enter the name for the new client.' => 'Bitte geben Sie einen Namen für den neuen Mandanten ein.',
   'Please enter the name for the new group.' => 'Bitte geben Sie den Namen für die neue Gruppe ein.',
-  'Please enter the name of the database that will be used as the template for the new database:' => 'Bitte geben Sie den Namen der Datenbank an, die als Vorlage f&uuml;r die neue Datenbank benutzt wird:',
+  'Please enter the name of the database that will be used as the template for the new database:' => 'Bitte geben Sie den Namen der Datenbank an, die als Vorlage für die neue Datenbank benutzt wird:',
+  'Please enter the new name:'  => 'Bitte geben Sie den neuen Namen ein:',
   'Please enter the sales tax identification number.' => 'Bitte geben Sie die Umsatzsteueridentifikationsnummer an.',
   'Please enter the taxnumber in the client configuration.' => 'Bitte geben Sie in der Mandantenkonfiguration die Steuernummer an.',
   'Please enter values'         => 'Bitte Werte eingeben',
   'Please insert object dimensions below.' => 'Bitte geben Sie die Abmessungen unten ein',
-  'Please install the below listed modules or ask your system administrator to.' => 'Bitte installieren Sie die unten aufgef&uuml;hrten Module, oder bitten Sie Ihren Administrator darum.',
+  'Please install the below listed modules or ask your system administrator to.' => 'Bitte installieren Sie die unten aufgeführten Module, oder bitten Sie Ihren Administrator darum.',
   'Please log in to the administration panel.' => 'Bitte melden Sie sich im Administrationsbereich an.',
+  'Please modify filename'      => 'Bitte Dateinamen editieren',
+  'Please provide corresponding credentials.' => 'Bitte geben Sie entsprechende Logindaten an.',
   'Please re-run the analysis for broken general ledger entries by clicking this button:' => 'Bitte wiederholen Sie die Analyse der Hauptbucheinträge, indem Sie auf diesen Button klicken:',
   'Please read the file'        => 'Bitte lesen Sie die Datei',
   'Please select a customer from the list below.' => 'Bitte einen Endkunden aus der Liste auswählen',
-  'Please select a part from the list below.' => 'Bitte w&auml;hlen Sie einen Artikel aus der Liste aus.',
+  'Please select a customer.'   => 'Bitte wählen Sie einen Kunden aus.',
+  'Please select a delivery date.' => 'Bitte einen Liefertermin auswählen',
   'Please select a vendor from the list below.' => 'Bitte einen Händler aus der Liste auswählen',
+  'Please select a vendor.'     => 'Bitte wählen Sie einen Lieferanten aus.',
   'Please select the dataset you want to delete:' => 'Bitte wählen Sie die zu löschende Datenbank aus:',
   'Please select the destination bank account for the collections:' => 'Bitte wählen Sie das Bankkonto als Ziel für die Einzüge aus:',
   'Please select the source bank account for the transfers:' => 'Bitte wählen Sie das Bankkonto als Quelle für die Überweisungen aus:',
   'Please select which client configurations you want to create.' => 'Bitte wählen Sie aus, welche Mandanten mit welchen Einstellungen angelegt werden sollen.',
   'Please set another taxnumber for the following taxes and run the update again:' => 'Bitte wählen Sie ein anderes Steuerautomatik-Konto für die folgenden Steuern aus uns starten Sie dann das Update erneut.',
-  'Please specify a description for the warehouse designated for these goods.' => 'Bitte geben Sie den Namen des Ziellagers f&uuml;r die &uuml;bernommenen Daten ein.',
+  'Please specify a description for the warehouse designated for these goods.' => 'Bitte geben Sie den Namen des Ziellagers für die übernommenen Daten ein.',
   'Plural'                      => 'Plural',
+  'Poland'                      => 'Polen',
   'Port'                        => 'Port',
   'Portrait'                    => 'Hochformat',
+  'Position'                    => 'Position',
+  'Position #1'                 => 'Position #1',
+  'Position #1: #2'             => 'Position #1: #2',
   'Position type in quotation/order' => 'Positionstyp in Angebot/Auftrag',
+  'Positions'                   => 'Positionen',
   'Post'                        => 'Buchen',
   'Post Payment'                => 'Zahlung buchen',
-  'Post and E-mail'             => 'Buchen und E-Mail',
+  'Post and new booking'        => 'Buchen und neue Buchung',
+  'Post and upload document'    => 'Buchen und Dokument hochladen',
   'Post payments'               => 'Zahlungen buchen',
+  'Post payments for selected invoices' => 'Zahlungen für ausgewählten Rechnungen buchen',
+  'Postal Invoice'              => 'Rechnung per Post',
   'Posting Configuration'       => 'Buchungskonfiguration',
+  'Posting Key'                 => 'BU-Schlüssel',
+  'Posting Text'                => 'Buchungstext',
   'Postscript'                  => 'Postscript',
   'Posustva_coa'                => 'USTVA Kennz.',
   'Pre-defined Texts'           => 'Vordefinierte Textblöcke',
   'Preamble'                    => 'Einleitung',
+  'Precision'                   => 'Genauigkeit',
+  'Precision Note'              => '<b>Achtung:</b> Bei Genauigkeit 0.05 dürfen Verkaufsbelege aktuell nur in Standardwährung erstellt werden.',
   'Preferences'                 => 'Einstellungen',
   'Preferences saved!'          => 'Einstellungen gespeichert!',
-  'Prefix for the new bins\' names' => 'Namenspr&auml;fix f&uuml;r die neuen Lagerpl&auml;tze',
+  'Prefix for the new bins\' names' => 'Namenspräfix für die neuen Lagerplätze',
   'Preis'                       => 'Preis',
-  'Preisklasse'                 => 'Preisgruppe',
   'Prepare bank collection via SEPA XML' => 'Einzug via SEPA XML vorbereiten',
   'Prepare bank transfer via SEPA XML' => 'Überweisung via SEPA XML vorbereiten',
   'Prepayment'                  => 'Vorauszahlung',
-  'Presskit'                    => 'Pressemappe',
-  'Preview'                     => 'Druckvorschau',
+  'Preselect Customer/Vendor documents as email attachments' => 'Vorausgewählte Kunden-/Lieferantendokumente im E-Mail-Anhang',
+  'Preselect all documents for the current selected parts in a record as a mail attachment.' => 'Wählt alle Artikel-Dokumente der aktuellen Artikel des Belegs beim E-Mail-Versand aus.',
+  'Preselect all documents saved for the current customer/vendor as a mail attachment.' => 'Wählt alle Kunden/Lieferanten-Dokumente beim E-Mail-Versand eines Belegs aus.',
+  'Preselect all documents saved for the current record as a mail attachment.' => 'Wählt alle Dateianhänge des aktuellen Belegs beim E-Mail-Versand aus.',
+  'Preselect part documents as email attachments' => 'Vorausgewählte Artikeldokumente im E-Mail-Anhang',
+  'Preselect record documents as email attachments' => 'Vorausgewählte Dateianhänge im E-Mail-Anhang',
+  'Preselected bin'             => 'Vorausgewählter Lagerplatz',
+  'Preselected cutoff date'     => 'Vorausgewählter Stichtag',
+  'Preselected warehouse'       => 'Vorausgewähltes Lager',
+  'Preset email body for periodic invoices' => 'Vorbelegter E-Mail-Text für wiederkehrende Rechnungen',
+  'Preset email strings'        => 'Vorbelegte E-Mail-Texte',
+  'Preset email subject for periodic invoices' => 'Vorbelegter E-Mail-Betreff für wiederkehrende Rechnungen',
+  'Preset email text for purchase orders' => 'Vorbelegter E-Mail-Text für Einkaufsaufträge',
+  'Preset email text for requests (rfq)' => 'Vorbelegter E-Mail-Text für Anfragen',
+  'Preset email text for sales delivery orders' => 'Vorbelegter E-Mail-Text für Verkaufs-Lieferscheine',
+  'Preset email text for sales invoices' => 'Vorbelegter E-Mail-Text für Rechnungen',
+  'Preset email text for sales invoices with direct debit' => 'Vorbelegter E-Mail-Text für Rechnungen mit Bankeinzug',
+  'Preset email text for sales orders' => 'Vorbelegter E-Mail-Text für Aufträge',
+  'Preset email text for sales quotations' => 'Vorbelegter E-Mail-Text für Angebote',
+  'Prevent browser\'s back button in sales invoices' => 'Browser-Zurück-Knopf bei Verkaufsrechnungen verhindern',
+  'Preview'                     => 'Vorschau',
   'Preview Mode'                => 'Vorschaumodus',
+  'Previous month'              => 'vorheriger Monat',
   'Previous transdate text'     => 'wurde gespeichert am',
   'Previous transnumber text'   => 'Letzte Buchung mit der Buchungsnummer',
   'Price'                       => 'Preis',
   'Price #1'                    => 'Preis #1',
   'Price Factor'                => 'Preisfaktor',
   'Price Factors'               => 'Preisfaktoren',
+  'Price List'                  => 'Preisliste',
   'Price Rule'                  => 'Preisregel',
   'Price Rules'                 => 'Preisregeln',
   'Price Source'                => 'Preisquelle',
   'Price Sources to be disabled in this client' => 'Preisquellen die in diesem Mandanten deaktiviert werden sollen',
   'Price Types'                 => 'Preistypen',
+  'Price and Stock'             => 'Preis und Bestand',
   'Price factor (database ID)'  => 'Preisfaktor (Datenbank-ID)',
   'Price factor (name)'         => 'Preisfaktor (Name)',
-  'Price factor deleted!'       => 'Preisfaktor gel&ouml;scht.',
-  'Price factor saved!'         => 'Preisfaktor gespeichert.',
+  'Price group'                 => 'Preisgruppe',
   'Price group (database ID)'   => 'Preisgruppe (Datenbank-ID)',
   'Price group (name)'          => 'Preisgruppe (Name) ',
+  'Price history for master data' => 'Preisentwicklung für Stammdaten',
   'Price information'           => 'Preisinformation',
   'Price or discount must not be zero.' => 'Preis/Rabatt darf nicht 0,00 sein',
   'Price rules must have at least one rule.' => 'Preisregeln brauchen mindestens eine Bedingung.',
+  'Price source'                => 'Preisquelle',
   'Price sources deactivated in this client' => 'Preisquellen die in diesem Mandanten deaktiviert sind',
+  'Price type'                  => 'Preistyp',
   'Price type explanation'      => 'Preistyp Erklärung',
+  'Price updated'               => 'Preisänderung am',
   'Pricegroup'                  => 'Preisgruppe',
-  'Pricegroup deleted!'         => 'Preisgruppe gelöscht!',
-  'Pricegroup missing!'         => 'Preisgruppe fehlt!',
-  'Pricegroup saved!'           => 'Preisgruppe gespeichert!',
   'Pricegroups'                 => 'Preisgruppen',
   'Prices'                      => 'Preise',
   'Print'                       => 'Drucken',
   'Print and Post'              => 'Drucken und Buchen',
   'Print automatically'         => 'Automatisch ausdrucken',
-  'Print destination'           => 'Druckausgabe',
+  'Print both sided'            => 'Beidseitig ausdrucken',
+  'Print delivery orders'       => 'Drucke Lieferscheine',
+  'Print destination'           => 'Druckort',
   'Print destination (copy)'    => 'Druckausgabe (Kopie)',
   'Print dunnings'              => 'Mahnungen drucken',
   'Print list'                  => 'Liste ausdrucken',
   'Print options'               => 'Druckoptionen',
+  'Print record'                => 'Beleg drucken',
   'Print template base file name' => 'Druckvorlagen-Basisdateiname',
   'Print templates'             => 'Druckvorlagen',
   'Print templates to use'      => 'Zu verwendende Druckvorlagen',
+  'Printdate'                   => 'Druckdatum',
   'Printer'                     => 'Drucker',
   'Printer Command'             => 'Druckbefehl',
   'Printer Description'         => 'Druckerbeschreibung',
   'Printer Management'          => 'Druckeradministration',
   'Printer management'          => 'Druckerverwaltung',
   'Printing ... '               => 'Es wird gedruckt.',
+  'Printing Documents'          => 'Drucke Dokumente',
   'Printing invoices (this can take a while)' => 'Drucke Rechnungen (kann eine Weile dauern)',
   'Prior year'                  => 'Vorheriges Jahr',
   'Priority'                    => 'Priorität',
   'Private E-mail'              => 'Private E-Mail',
   'Private Phone'               => 'Privates Tel.',
   'Problem'                     => 'Problem',
+  'Produce'                     => 'Fertigen',
   'Produce Assembly'            => 'Erzeugnis fertigen',
+  'Produce Assembly Configuration' => 'Konfiguration für Erzeugnis fertigen',
+  'Produce assembly consumes services if assigned as a assembly item' => 'Erzeugnis fertigen verbraucht auch Dienstleistungen falls diese Erzeugnisbestandteile sind ',
+  'Produce assembly only if all parts are in the same warehouse' => 'Erzeugnisse können nur gefertigt werden, wenn alle Einzelteile sich in demselben Lager befinden',
+  'Production'                  => 'Produktion',
+  'Production (typeabbreviation)' => 'P',
   'Productivity'                => 'Produktivität',
+  'Productivity (TODO list, Follow-Ups)' => 'Produktivität (Aufgabenliste, Wiedervorlagen)',
+  'Profit'                      => 'Gewinn',
+  'Profit and loss accounts'    => 'Erfolgskonten',
+  'Profit carried forward account' => 'Gewinnvortragskonto',
   'Profit determination'        => 'Gewinnermittlung',
   'Proforma Invoice'            => 'Proformarechnung',
   'Program'                     => 'Programm',
@@ -2080,12 +2729,21 @@ $self->{texts} = {
   'Project Type'                => 'Projekttyp',
   'Project Types'               => 'Projekttypen',
   'Project link actions'        => 'Projektverknüpfungs-Aktionen',
+  'Project of assigned order must match assigned project.' => 'Projekt des zugeordneten Auftrags muss mit dem gewählten Projekt übereinstimmen.',
+  'Project picker'              => 'Projektauswahl',
+  'Project statuses'            => 'Projektstatus',
   'Project type'                => 'Projekttyp',
+  'Project types'               => 'Projekttypen',
   'Projects'                    => 'Projekte',
+  'Projects: edit the list of employees allowed to view invoices' => 'Projekte: Liste der Angestellten bearbeiten, die Projektrechnungen ansehen dürfen',
   'Projecttransactions'         => 'Projektbuchungen',
   'Proposal'                    => 'Vorschlag',
   'Proposals'                   => 'Vorschläge',
+  'Protocol'                    => 'Protokoll',
+  'Proxy'                       => 'Proxy',
   'Prozentual/Absolut'          => 'Prozentual/Absolut',
+  'Purchase'                    => 'Einkauf',
+  'Purchase (typeabbreviation)' => 'E',
   'Purchase Delivery Order'     => 'Einkaufslieferschein',
   'Purchase Delivery Orders'    => 'Einkaufslieferscheine',
   'Purchase Delivery Orders deleteable' => 'Einkaufslieferscheine löschbar',
@@ -2093,11 +2751,10 @@ $self->{texts} = {
   'Purchase Invoices'           => 'Einkaufsrechnungen',
   'Purchase Order'              => 'Lieferantenauftrag',
   'Purchase Orders'             => 'Lieferantenaufträge',
+  'Purchase Orders Services are deliverable' => 'Dienstleistungen im Einkaufsauftrag sind lieferbar',
   'Purchase Orders deleteable'  => 'Lieferantenaufträge löschbar',
-  'Purchase Price'              => 'Einkaufspreis',
   'Purchase Price Rules'        => 'Preisregeln Einkauf',
   'Purchase Price Rules '       => 'Preisregeln (Einkauf)',
-  'Purchase Prices'             => 'Einkaufspreise',
   'Purchase delivery order'     => 'Lieferschein (Einkauf)',
   'Purchase invoices'           => 'Einkaufsrechnungen',
   'Purchase invoices changeable' => 'Änderbarkeit von Einkaufsrechnunen',
@@ -2108,6 +2765,10 @@ $self->{texts} = {
   'Purpose'                     => 'Verwendungszweck',
   'Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")' => 'Verwendungszweck (wenn die Spalten purpose, purpose1, purpose2 ... existieren werden diese zum Feld "purpose" zusammengefügt)',
   'Purpose/Reference'           => 'Verwendungszweck und Referenz',
+  'QR bill without amount'      => 'QR-Rechnung ohne Betrag',
+  'QR-Code placeholder image: QRCodePlaceholder not found in template.' => 'QR-Code Platzhalter Bild: QRCodePlaceholder nicht gefunden.',
+  'QR-Image generation failed: ' => 'QR-Code Erzeugung fehlgeschlagen: ',
+  'QUEUED'                      => 'In Warteschlange',
   'Qty'                         => 'Menge',
   'Qty according to delivery order' => 'Menge laut Lieferschein',
   'Qty equal or less than #1'   => 'Menge gleich oder kleiner als #1',
@@ -2125,7 +2786,11 @@ $self->{texts} = {
   'Quartal'                     => 'Quartal',
   'Quarter'                     => 'Quartal',
   'Quarterly'                   => 'quartalsweise',
+  'Query parameters'            => 'Abfrageparameter',
   'Queue'                       => 'Warteschlange',
+  'Quick Search'                => 'Schnellsuche',
+  'Quick Searches that will be shown in the header for this user' => 'Schnellsuchen, die in der Kopfzeile gezeigt werden sollen',
+  'Quick Searches that will be shown in the header in this client' => 'Schnellsuchen, die in der Kopfzeile in diesem Mandanten gezeigt werden sollen',
   'Quotation'                   => 'Angebot',
   'Quotation Date'              => 'Angebotsdatum',
   'Quotation Date missing!'     => 'Angebotsdatum fehlt!',
@@ -2137,45 +2802,63 @@ $self->{texts} = {
   'Quotations and orders'       => 'Angebote und Aufträge',
   'Quotations/Orders actions'   => 'Aktionen für Angebote/Aufträge',
   'Quote character'             => 'Anführungszeichen-Symbol',
-  'Quote chararacter'           => 'Anf&uuml;hrungszeichen',
+  'Quote chararacter'           => 'Anführungszeichen',
   'Quoted'                      => 'Angeboten',
   'Quotes'                      => 'Doppelte Anführungszeichen',
   'RFQ'                         => 'Anfrage',
   'RFQ Date'                    => 'Anfragedatum',
   'RFQ Number'                  => 'Anfragenummer',
   'RFQs'                        => 'Preisanfragen',
+  'RMA Delivery Order'          => 'Retouren-Lieferschein',
+  'RMA Delivery Orders'         => 'Retouren-Lieferscheine',
+  'RMA delivery order'          => 'Retouren-Lieferschein',
   'ROP'                         => 'Mindestlagerbestand',
   'Ranges of numbers'           => 'Nummernkreise',
   'Re-numbering all sections and function blocks in the order they are currently shown cannot be undone.' => 'Das Neu-Nummerieren aller Abschnitte und Funktionsblöcke kann nicht rückgängig gemacht werden.',
   'Re-run analysis'             => 'Analyse wiederholen',
+  'Read all employee e-mails'   => 'Ansehen der E-Mails aller Mitarbeiter',
   'Really cancel link?'         => 'Verknüpfung wirklich aufheben?',
+  'Realm'                       => 'Realm',
   'Receipt'                     => 'Zahlungseingang',
   'Receipt posted!'             => 'Beleg gebucht!',
   'Receipt, payment, reconciliation' => 'Zahlungseingang, Zahlungsausgang, Kontenabgleich',
   'Receipts'                    => 'Zahlungseingänge',
+  'Receipts attached/extra'     => 'Belege werden gesondert eingereicht',
+  'Receivable account'          => 'Forderungskonto',
   'Receivables'                 => 'Forderungen',
-  'Recipients'                  => 'EmpfängerInnen',
+  'Receivables account'         => 'Forderungskonto',
+  'Receivables account (account number)' => 'Forderungskonto (Kontonummer)',
+  'Received payments can only be posted for sales invoices and purchase credit notes.' => 'Erhaltene Zahlungen können nur mit Verkaufsrechnungen und Einkaufsgutschriften verbucht werden.',
+  'Recipients'                  => 'Empfänger',
   'Reconcile'                   => 'Abgleichen',
   'Reconciliation'              => 'Kontenabgleich',
   'Reconciliation with bank'    => 'Kontenabgleich mit Bank',
+  'Record Type'                 => 'Belegtyp',
   'Record Vendor Invoice'       => 'Einkaufsrechnung erfassen',
   'Record in'                   => 'Buchen auf',
   'Record number'               => 'Belegnummer',
+  'Record numbers changeable'   => 'Änderbarkeit von Belegnummern',
+  'Record templates'            => 'Belegvorlagen',
   'Record type to create'       => 'Anzulegender Belegtyp',
+  'Record\'s files'             => 'Belegdateien',
   'Recorded Tax'                => 'Gespeicherte Steuern',
   'Recorded taxkey'             => 'Gespeicherter Steuerschlüssel',
+  'Records'                     => 'Belege',
   'Reduced Master Data'         => 'Abschlag',
   'Reference'                   => 'Referenz',
   'Reference / Invoice Number'  => 'Referenz / Rechnungsnummer',
-  'Reference day'               => 'Stichtag',
+  'Reference day'               => 'Referenztag',
   'Reference missing!'          => 'Referenz fehlt!',
+  'Relaxed (UTF-8)'             => 'Lax (UTF-8)',
   'Release From Stock'          => 'Lagerausgang',
   'Remaining'                   => 'Rest',
   'Remaining Amount'            => 'abzurechnender Betrag',
   'Remaining Net Amount'        => 'abzurechnender Nettobetrag',
+  'Remittance information optional Vendor/Customer No postfix' => 'optionale Verwendungszweckvorbelegung Kd./Lief.-Nummer',
   'Remittance information prefix' => 'Verwendungszweckvorbelegung (Präfix)',
   'Remote Bank Code'            => 'Fremde Bankleitzahl',
   'Remote Name/Customer/Description' => 'Kunden/Lieferantenname und Beschreibung',
+  'Remote account'              => 'Gegenkonto',
   'Remote account number'       => 'Fremde Kontonummer',
   'Remote bank code'            => 'Fremde Bankleitzahl',
   'Remote name'                 => 'Fremder Kontoinhaber',
@@ -2184,24 +2867,30 @@ $self->{texts} = {
   'Removal from warehouse'      => 'Entnahme aus Lager',
   'Removal qty'                 => 'Entnahmemenge',
   'Remove'                      => 'Entfernen',
-  'Remove Draft'                => 'Entwurf l&ouml;schen',
+  'Remove Draft'                => 'Entwurf löschen',
   'Remove article'              => 'Artikel entfernen',
-  'Remove draft when posting'   => 'Entwurf beim Buchen l&ouml;schen',
   'Removed sections and function blocks: #1' => 'Entfernte Abschnitte und Funktionsblöcke: #1',
   'Removed spoolfiles!'         => 'Druckdateien entfernt!',
   'Removed text blocks: #1'     => 'Entfernte Textblöcke: #1',
   'Removing marked entries from queue ...' => 'Markierte Einträge werden von der Warteschlange entfernt ...',
+  'Rename'                      => 'Umbenennen',
+  'Rename Attachments'          => 'Anhang umbenennen',
+  'Rename Documents'            => 'Dokument umbenennen',
+  'Rename Images'               => 'Bilder umbenennen',
+  'Rename attachment'           => 'Dateianhang umbenennen',
   'Renumber sections and function blocks' => 'Abschnitte/Funktionsblöcke neu nummerieren',
   'Replace the orphaned currencies by other not orphaned currencies. To do so, please delete the currency in the textfields above and replace it by another currency. You could loose or change unintentionally exchangerates. Go on very carefully since you could destroy transactions.' => 'Ersetze die Währungen durch andere gültige Währungen. Wenn Sie sich hierfür entscheiden, ersetzen Sie bitte alle Währungen, die oben angegeben sind, durch Währungen, die in Ihrem System ordnungsgemäß eingetragen sind. Alle eingetragenen Wechselkurse für die verwaiste Währung werden dabei gelöscht. Bitte gehen Sie sehr vorsichtig vor, denn die betroffenen Buchungen können unter Umständen kaputt gehen.',
   'Report Positions'            => 'Berichte',
   'Report about warehouse contents' => 'Lagerbestand anzeigen',
   'Report about warehouse transactions' => 'Lagerbuchungen anzeigen',
   'Report and misc. Preferences' => 'Sonstige Einstellungen',
+  'Report configuration overview' => 'Berichtskonfigurationsübersicht',
   'Report date'                 => 'Berichtsdatum',
   'Report for'                  => 'Bericht für',
+  'Report separately'           => 'Preis separat ausweisen',
   'Reports'                     => 'Berichte',
   'Representative'              => 'Vertreter',
-  'Representative for Customer' => 'Vertreter für Kunden',
+  'Representative for Customer' => 'Vertreter für Kunden (aktuell (3.6) deaktiviert)',
   'Reqdate'                     => 'Liefertermin',
   'Reqdate is #1'               => 'Liefertermin ist #1',
   'Reqdate is after #1'         => 'Liefertermin nach #1',
@@ -2217,6 +2906,8 @@ $self->{texts} = {
   'Requested execution date to' => 'Gewünschtes Ausführungsdatum bis',
   'Requests for Quotation'      => 'Preisanfragen',
   'Require a transaction description in purchase and sales records' => 'Vorgangsbezeichnung in Einkaufs- und Verkaufsbelegen erzwingen',
+  'Require stock out to consider a delivery order position delivered?' => 'Muss eine Lieferscheinposition ausgelagert sein um als geliefert zu gelten?',
+  'Required access right'       => 'Benötigtes Zugriffsrecht',
   'Required by'                 => 'Lieferdatum',
   'Requirement Spec Status'     => 'Pflichtenheftstatus',
   'Requirement Spec Statuses'   => 'Pflichtenheftstatus',
@@ -2238,14 +2929,20 @@ $self->{texts} = {
   'Requirement specs'           => 'Pflichtenhefte',
   'Reset'                       => 'Zurücksetzen',
   'Result'                      => 'Ergebnis',
+  'Result of SQL query'         => 'Ergebnis einer SQL-Abfrage',
+  'Results per page'            => 'Treffer pro Seite',
   'Revenue'                     => 'Erlöskonto',
   'Revenue Account'             => 'Erlöskonto',
+  'Reversal invoices cannot be canceled.' => 'Stornorechnungen können nicht storniert werden.',
   'Revert to version'           => 'Auf Version zurücksetzen',
   'Review of Aging list'        => 'Altersstrukturliste',
   'Right'                       => 'Rechts',
   'Risk'                        => 'Risiko',
   'Risk levels'                 => 'Risikograde',
   'Risks'                       => 'Risikograde',
+  'Rounding'                    => 'Rundung',
+  'Rounding Gain'               => 'Rundungserträge',
+  'Rounding Loss'               => 'Rundungsaufwendungen',
   'Row'                         => 'Zeile',
   'Row #1: amount has to be different from zero.' => 'Zeile #1: Der Wert darf nicht 0 sein.',
   'Row number'                  => 'Zeilennummer',
@@ -2258,30 +2955,36 @@ $self->{texts} = {
   'Rule for vendor must not be empty' => 'Eine Lieferantenbedingung darf nicht leer sein',
   'Run JavaScript unit tests'   => 'JavaScript-Unit-Tests ausführen',
   'Run at'                      => 'Ausgeführt um',
+  'Run task server for this client with the following user' => 'Task-Server für diesen Mandanten mit der folgenden BenutzerIn ausführen',
   'Run tests'                   => 'Tests ausführen',
   'SAVED'                       => 'Gespeichert',
-  'SAVED FOR DUNNING'           => 'Gespeichert',
+  'SAVED FOR DUNNING'           => 'Gespeichert zum Mahnen',
   'SCREENED'                    => 'Angezeigt',
+  'SEPA'                        => 'SEPA',
   'SEPA XML download'           => 'SEPA-XML-Download',
   'SEPA creditor ID'            => 'SEPA-Kreditoren-Identifikation',
   'SEPA exports'                => 'SEPA-Exporte',
-  'SEPA exports:'               => 'SEPA-Exporte:',
   'SEPA message ID'             => 'SEPA-Nachrichten-ID',
   'SEPA message IDs'            => 'SEPA-Nachrichten-IDs',
   'SEPA strings'                => 'SEPA-Überweisungen',
+  'SQL query'                   => 'SQL-Abfrage',
+  'SWIFT MT940 format'          => 'SWIFT-MT940-Format',
   'Saldo Credit'                => 'Saldo Haben',
   'Saldo Debit'                 => 'Saldo Soll',
   'Saldo neu'                   => 'Saldo neu',
   'Saldo per'                   => 'Saldo per',
-  'Sale Prices'                 => 'Verkaufspreise',
+  'Sales'                       => 'Verkauf',
+  'Sales (typeabbreviation)'    => 'V',
   'Sales Delivery Order'        => 'Verkaufslieferschein',
   'Sales Delivery Orders'       => 'Verkaufslieferscheine',
   'Sales Delivery Orders deleteable' => 'Verkaufslieferscheine löschbar',
   'Sales Invoice'               => 'Rechnung',
   'Sales Invoices'              => 'Kundenrechnungen',
   'Sales Order'                 => 'Kundenauftrag',
+  'Sales Order delivery date interval' => 'Lieferdatumintervall',
   'Sales Orders'                => 'Aufträge',
   'Sales Orders Advance'        => 'Auftragsvorlauf',
+  'Sales Orders Services are deliverable' => 'Dienstleistungen im Verkaufsauftrag sind lieferbar',
   'Sales Orders deleteable'     => 'Kundenaufträge löschbar',
   'Sales Price Rules'           => 'Preisregeln Verkauf',
   'Sales Price Rules '          => 'Preisregeln (Verkauf)',
@@ -2307,25 +3010,44 @@ $self->{texts} = {
   'Sales quotation #1 has been deleted.' => 'Angebot #1 wurde gelöscht.',
   'Sales quotation #1 has been updated.' => 'Angebot #1 wurde aktualisiert.',
   'Salesman'                    => 'Verkäufer/in',
-  'Salesman (database ID)'      => 'Verkäufer (Datenbank-ID)',
+  'Salesman (database ID)'      => 'Verkäufer/in (Datenbank-ID)',
+  'Salesman (login)'            => 'Verkäufer/in (Login)',
   'Salesperson'                 => 'Verkäufer',
-  'Same as the quote character' => 'Wie Anf&uuml;hrungszeichen',
+  'Salutation female'           => 'Anrede weiblich',
+  'Salutation general'          => 'Anrede anonym (personenlos)',
+  'Salutation male'             => 'Anrede männlich',
+  'Salutation punctuation mark' => 'Zeichensetzungs-Trenner nach der Anrede-Formel (Punkt, Ausrufezeichen, etc)',
+  'Same Filename !'             => 'unveränderter Dateiname !',
+  'Same as the quote character' => 'Wie Anführungszeichen',
+  'Sat'                         => 'Sa',
   'Sat. Fax'                    => 'Sat. Fax',
   'Sat. Phone'                  => 'Sat. Tel.',
+  'Saturday'                    => 'Samstag',
   'Satz %'                      => 'Satz %',
   'Save'                        => 'Speichern',
   'Save Draft'                  => 'Entwurf speichern',
   'Save and AP Transaction'     => 'Speichern und Kreditorenbuchung erfassen',
   'Save and AR Transaction'     => 'Speichern und Debitorenbuchung erfassen',
   'Save and Close'              => 'Speichern und schließen',
+  'Save and Delivery Order'     => 'Speichern und Lieferschein',
+  'Save and E-mail'             => 'Speichern und E-Mail',
+  'Save and Final Invoice'      => 'Speichern und Schlussrechnung',
+  'Save and Further Invoice for Advance Payment' => 'Speichern und weitere Anzahlungsrechnung',
   'Save and Invoice'            => 'Speichern und Rechnung erfassen',
+  'Save and Invoice for Advance Payment' => 'Speichern und Anzahlungsrechnung',
   'Save and Order'              => 'Speichern und Auftrag erfassen',
+  'Save and Purchase Order'     => 'Speichern und Lieferantenauftrag',
   'Save and Quotation'          => 'Speichern und Angebot',
   'Save and RFQ'                => 'Speichern und Lieferantenanfrage',
+  'Save and Sales Order'        => 'Speichern und Kundenauftrag',
+  'Save and Supplier Delivery Order' => 'Speichern und Beistelllieferschein',
   'Save and close'              => 'Speichern und schließen',
   'Save and execute'            => 'Speichern und ausführen',
   'Save and keep open'          => 'Speichern und geöffnet lassen',
-  'Save as new'                 => 'als neu speichern',
+  'Save and preview PDF'        => 'PDF-Druckvorschau',
+  'Save and print'              => 'Speichern und drucken',
+  'Save as a new draft.'        => 'Als neuen Entwurf speichern',
+  'Save as new'                 => 'Als neu speichern',
   'Save document in WebDAV repository' => 'Dokument in WebDAV-Ablage speichern',
   'Save draft'                  => 'Entwurf speichern',
   'Save invoices'               => 'Rechnungen speichern',
@@ -2333,15 +3055,21 @@ $self->{texts} = {
   'Save proposals'              => 'Vorschläge speichern',
   'Save settings as'            => 'Einstellungen speichern unter',
   'Saving failed. Error message from the database: #1' => 'Speichern schlug fehl. Fehlermeldung der Datenbank: #1',
+  'Saving failed. Error message from the server: #1' => 'Speichern schlug fehl. Fehlermeldung der Servers #1',
   'Saving the file \'%s\' failed. OS error message: %s' => 'Das Speichern der Datei \'%s\' schlug fehl. Fehlermeldung des Betriebssystems: %s',
+  'Saving the record template \'#1\' failed.' => 'Das Speichern der Belegvorlage »#1« schlug fehl.',
+  'Saving the time recording entry failed: #1' => 'Speichern des Zeiterfassung-Eintrags schlug fehl: #1',
   'Score'                       => 'Punkte',
   'Screen'                      => 'Bildschirm',
+  'Scrollbar height percentage for form postion area (0 means no scrollbar)' => 'Prozentuale Höhe des Scrollbereichs der Positionen in Belegen (0 bedeutet kein Scrollbar)',
   'Search'                      => 'Suchen',
   'Search AP Aging'             => 'Offene Verbindlichkeiten',
   'Search AR Aging'             => 'Offene Forderungen',
   'Search bank transactions'    => 'Filter für Bankbuchungen',
-  'Search contacts'             => 'Ansprechpersonensuche',
-  'Search projects'             => 'Projektsuche',
+  'Search contacts'             => 'Personensuche',
+  'Search for Items used in Assemblies' => 'Suche nach in Erzeugnissen verbauten Artikeln',
+  'Search parts by customer partnumber in sales order forms' => 'Artikel nach Kunden-Art.-Nr. in Verkaufsbelegen suchen',
+  'Search parts by vendor partnumber (model) in purchase order forms' => 'Artikel nach Lieferanten-Art.-Nr. in Einkaufsbelegen suchen',
   'Search term'                 => 'Suchbegriff',
   'Searchable'                  => 'Durchsuchbar',
   'Secondary sorting'           => 'Untersortierung',
@@ -2350,63 +3078,121 @@ $self->{texts} = {
   'Section/Function block actions' => 'Abschnitts-/Funktionsblockaktionen',
   'Sections'                    => 'Abschnitte',
   'Sections that are not assigned to any of the items above will be added as new positions.' => 'Abschnitte, die keiner der oben aufgeführten Positionen zugeordnet sind, werden als neue Positionen ergänzt.',
+  'See various menu entries intended for developers' => 'Entwickler-Tools anzeigen',
   'Select'                      => 'auswählen',
+  'Select Mulit-Item Options'   => 'Multi-Treffer Auswahlliste',
   'Select a Customer'           => 'Endkunde auswählen',
-  'Select a customer'           => 'Einen Kunden ausw&auml;hlen',
-  'Select a part'               => 'Artikel ausw&auml;hlen',
-  'Select a part or assembly'   => 'Artikel oder Erzeugnis ausw&auml;hlen',
   'Select a period'             => 'Bitte Zeitraum auswählen',
-  'Select a vendor'             => 'Einen Lieferanten ausw&auml;hlen',
-  'Select all'                  => 'Alle auswählen',
   'Select federal state...'     => 'Bundesland auswählen...',
   'Select file to upload'       => 'Datei zum Hochladen auswählen',
   'Select from one of the items below' => 'Wählen Sie einen der untenstehenden Einträge',
-  'Select from one of the names below' => 'Wählen Sie einen der untenstehenden Namen',
-  'Select from one of the projects below' => 'Wählen Sie eines der untenstehenden Projekte',
   'Select postscript or PDF!'   => 'Postscript oder PDF auswählen!',
   'Select tax office...'        => 'Finanzamt auswählen...',
   'Select template to paste'    => 'Einzufügende Vorlage auswählen',
-  'Select type of removal'      => 'Grund der Entnahme ausw&auml;hlen',
-  'Select type of transfer'     => 'Grund der Umlagerung ausw&auml;hlen',
+  'Select type of removal'      => 'Grund der Entnahme auswählen',
+  'Select type of transfer'     => 'Grund der Umlagerung auswählen',
+  'Select type of transfer in'  => 'Grund der Einlagerung auswählen:',
   'Selected'                    => 'Ausgewählt',
   'Selection'                   => 'Auswahlbox',
-  'Selection fields: The option field must contain the available options for the selection. Options are separated by \'##\', for example \'Early##Normal##Late\'.' => 'Auswahlboxen: Das Optionenfeld muss die f&uuml;r die Auswahl verf&uuml;gbaren Eintr&auml;ge enthalten. Die Eintr&auml;ge werden mit \'##\' voneinander getrennt. Beispiel: \'Fr&uuml;h##Normal##Sp&auml;t\'.',
+  'Selection fields: The option field must contain the available options for the selection. Options are separated by \'##\', for example \'Early##Normal##Late\'.' => 'Auswahlboxen: Das Optionenfeld muss die für die Auswahl verfügbaren Einträge enthalten. Die Einträge werden mit \'##\' voneinander getrennt. Beispiel: \'Früh##Normal##Spät\'.',
   'Sell Price'                  => 'Verkaufspreis',
   'Sellprice'                   => 'Verkaufspreis',
   'Sellprice adjustment'        => 'Verkaufspreis: Preisanpassung',
   'Sellprice for price group \'#1\'' => 'Verkaufspreis für Preisgruppe \'#1\'',
   'Sellprice significant places' => 'Verkaufspreis: Nachkommastellen',
   'Semicolon'                   => 'Semikolon',
+  'Send a BCC to logged in user?' => 'BCC an eingeloggten Benutzer?',
+  'Send a blind copy of all outgoing emails to current user\'s email address?' => 'Eine blinde Kopie aller ausgehenden E-Mails wird an den angemeldeten Nutzer geschickt',
+  'Send email'                  => 'E-Mail verschicken',
+  'Send invoice via email'      => 'Rechnung via E-Mail verschicken',
+  'Send printout of record'     => 'Belegausdruck mitschicken',
+  'Send the last or create the first version for this record' => 'Den zuletzt erstellten oder neuen Belegausdruck verschicken',
   'Sender'                      => 'AbsenderIn',
   'Sent emails can be optionally stored in the database with or without their attachments.' => 'Gesendete E-Mails können optional mit oder ohne ihre Anhänge in der Datenbank gespeichert werden.',
   'Sent on'                     => 'Verschickt am',
+  'Sent payments can only be posted for purchase invoices and sales credit notes.' => 'Gesendete Zahlungen können nur mit Einkaufsrechnungen und Verkaufsgutschriften verbucht werden.',
   'Sep'                         => 'Sep',
   'Separator'                   => 'Trennzeichen',
   'Separator chararacter'       => 'Feldtrennzeichen',
   'September'                   => 'September',
   'Serial No.'                  => 'Seriennummer',
   'Serial Number'               => 'Seriennummer',
+  'Serial Number missing in Row' => 'Seriennummer fehlt in Position',
+  'Server'                      => 'Server',
+  'Server control'              => 'Serversteuerung',
   'Service'                     => 'Dienstleistung',
+  'Service (typeabbreviation)'  => 'D',
   'Service Items'               => 'Dienstleistungen',
   'Service Number missing!'     => 'Dienstleistungsnummer fehlt!',
   'Service, assembly or part'   => 'Dienstleistung, Erzeugnis oder Ware',
   'Services'                    => 'Dienstleistungen',
+  'Services in Delivery Orders' => 'Dienstleistungen in Lieferscheinen',
   'Set (set to)'                => 'Setze',
+  'Set all source and memo fields' => 'Alle Beleg-/Memo-Felder setzen',
+  'Set count for one or more of the items to select them' => 'Zum Selektieren bitte Menge für einen oder mehrere Artikel setzen',
+  'Set delivery date for Sales Orders' => 'Lieferdatum im Verkaufsauftrag setzen',
   'Set eMail text'              => 'E-Mail Text eingeben',
+  'Set fields'                  => 'Felder setzen',
+  'Set lastcost'                => 'EK-Preis übernehmen',
+  'Set sellprice'               => 'VK-Preis übernehmen',
+  'Set the invoice duedate as the default execution date for SEPA export.' => 'Das Fälligkeitsdatum des Belegs als Ausführungsdatum im SEPA-Export setzen.',
+  'Set the invoice skonto date (if exists) as the default execution date for SEPA export.' => 'Das Skonto-Datum des Belegs als Ausführungsdatum im SEPA-Export setzen. Hat Priorität vor dem Fälligkeitsdatum.',
   'Set to paid missing'         => 'Fehlbetrag setzen',
+  'Set valid until date for Sales Quotation' => 'Gültigkeitsdatum bei Verkaufs-Angeboten setzen',
   'Settings'                    => 'Einstellungen',
   'Setup Menu'                  => 'Menü-Variante',
-  'Ship to'                     => 'Lieferadresse',
   'Ship to (database ID)'       => 'Lieferadresse (Datenbank-ID)',
   'Ship via'                    => 'Transportmittel',
+  'Shipped Quantity Algorithm'  => 'Liefermengen Berechnung',
   'Shipping Address'            => 'Lieferadresse',
   'Shipping Point'              => 'Versandort',
+  'Shipping address (name)'     => 'Name der Lieferadresse',
+  'Shipping cost article is not implemented' => 'Versandkosten-Artikel ist nicht implementiert',
+  'Shipping cost article not implemented' => 'Lieferkosten-Artikel nicht implementiert',
+  'Shipping costs'              => 'Versandkosten',
   'Shipping date'               => 'Lieferdatum',
+  'Shippingcosts'               => 'Versandkosten',
   'Shipto'                      => 'Lieferanschriften',
   'Shipto deleted.'             => 'Lieferadresse gelöscht',
   'Shipto is in use and was flagged invalid.' => 'Lieferadresse ist noch in Verwendung, und wurde als ungültig markiert.',
-  'Shopartikel'                 => 'Shopartikel',
+  'Shop'                        => 'Webshop',
+  'Shop Billing Address'        => 'Shop - Rechnungsadresse',
+  'Shop Connection Test'        => 'Shopverbindungstest',
+  'Shop Customer Address'       => 'Shop - Kundenadresse',
+  'Shop Delivery Address'       => 'Shop - Lieferadresse',
+  'Shop Headdata'               => 'Shop - Stammdaten',
+  'Shop Host'                   => 'Shop Host',
+  'Shop Host/Connector'         => 'Shop Host/Connector',
+  'Shop Order'                  => 'Shopauftrag',
+  'Shop Order Date'             => 'Shopauftragsdatum',
+  'Shop Order Number'           => 'Shopauftragsnummer',
+  'Shop OrderIP'                => 'Shop Bestell IP',
+  'Shop Orderamount'            => 'Shop Auftragssumme',
+  'Shop Orderdate'              => 'Shopauftragsdatum',
+  'Shop Ordernotes'             => 'Shop Bemerkungen',
+  'Shop Ordernumber'            => 'Shopauftragsnummer',
+  'Shop Orders'                 => 'Shopaufträge',
+  'Shop article'                => 'Shopartikel',
+  'Shop customernumber'         => 'Shop - Kundennumer',
+  'Shop or ordernumber not selected.' => 'Shop oder Bestellnummer nicht ausgewählt',
+  'Shop orderdate'              => 'Shopauftragsdatum',
+  'Shop ordernumber'            => 'Shopauftragsnummer',
+  'Shop part'                   => 'Shopartikel',
+  'Shop type'                   => 'Shop Typ',
+  'Shop variables'              => 'Shopvariablen',
+  'ShopOrders'                  => 'Shopbestellungen',
+  'Shopcategories'              => 'Shopartikelgruppen',
+  'Shopimages - valid for all shops' => 'Shopbilder Gültig für alle Shops',
+  'Shoporder'                   => 'Shopbestellung',
+  'Shoporder "#2" From Shop "#1" is already fetched' => 'Shopbestellung #1 von Shop #2 wurde schon geholt',
+  'Shoporder deleted -- '       => 'ungültig',
+  'Shoporder not found'         => 'Shopbestellung nicht gefunden',
+  'Shoporderlock'               => 'Shopauftragssperre',
+  'Shoporders'                  => 'Shopbestellungen',
+  'Shops'                       => 'Webshops',
   'Short'                       => 'Knapp',
+  'Should VAT ID or taxnumber be unique for all vendors? This is checked when saving a vendor\'s master data. One of the fields is sufficient and required.' => 'Soll die UStID oder Steuernummer eindeutig über alle Lieferanten sein? Dies wird beim Speichern von Lieferantenstammdaten geprüft. Eines dieser Felder reicht aus und muss angegeben sein.',
+  'Should VAT ID or taxnumber be unique for customers? This is checked when saving a customer\'s master data. One of the fields is sufficient and required.' => 'Soll die UStID oder Steuernummer eindeutig über alle Kunden sein? Dies wird beim Speichern von Kundenstammdaten geprüft. Eines dieser Felder reicht aus und muss angegeben sein.',
   'Should ap transactions be and when should they be changeable or deleteable after posting?' => 'Sollen Kreditorenbuchungen nach der Buchung zu ändern oder zu löschen sein?',
   'Should ar transactions be and when should they be changeable or deleteable after posting?' => 'Sollen Debitorenbuchungen nach der Buchung zu ändern oder zu löschen sein?',
   'Should gl transactions be and when should they be changeable or deleteable after posting?' => 'Sollen Dialogbuchungen nach der Buchung zu ändern oder zu löschen sein?',
@@ -2425,14 +3211,18 @@ $self->{texts} = {
   'Show AP transactions as part of AP invoice report' => 'Kreditorenbuchungen zusammen mit Verkaufsrechnungen anzeigen',
   'Show AR transactions as part of AR invoice report' => 'Debitorenbuchungen zusammen mit Verkaufsrechnungen anzeigen',
   'Show Bestbefore'             => 'Mindesthaltbarkeit anzeigen',
+  'Show E-Mails'                => 'E-Mails anzeigen',
   'Show Filter'                 => 'Filter zeigen',
   'Show Salesman'               => 'Verkäufer anzeigen',
   'Show Stornos'                => 'Stornos anzeigen',
   'Show TODO list'              => 'Aufgabenliste anzeigen',
   'Show Transfer via default'   => 'Ein- / Auslagern über Standardlagerplatz anzeigen (zusätzlicher Knopf in Beleg Lieferschein)',
   'Show administration link'    => 'Link zur Administration anzeigen',
+  'Show all details'            => 'Alle Details anzeigen',
   'Show all parts'              => 'Alle Artikel anzeigen',
-  'Show by default'             => 'Standardm&auml;&szlig;ig anzeigen',
+  'Show by default'             => 'Standardmäßig anzeigen',
+  'Show chart list'             => 'Kontenliste zeigen',
+  'Show charts'                 => 'Konten zeigen',
   'Show custom variable search inputs' => 'Suchoptionen für Benutzerdefinierte Variablen verstecken',
   'Show delete button in purchase delivery orders?' => 'Soll der "Löschen"-Knopf bei Einkaufslieferscheinen angezeigt werden?',
   'Show delete button in purchase orders?' => 'Soll der "Löschen"-Knopf bei Lieferantenaufträgen angezeigt werden?',
@@ -2442,14 +3232,25 @@ $self->{texts} = {
   'Show delivery value report'  => 'Lieferwertbericht anzeigen',
   'Show details'                => 'Details anzeigen',
   'Show details and reports of parts, services, assemblies' => 'Details und Berichte von Waren, Dienstleistungen und Erzeugnissen anzeigen',
+  'Show document tab after posting?' => 'Buchungsmaske nicht verlassen und Dokumenten-Reiter anzeigen?',
+  'Show documents in WebDAV'    => 'Dokumente im WebDAV anzeigen',
+  'Show documents in file storage' => 'Dokumente im Dateimanagement anzeigen',
   'Show fields used for the best before date?' => 'Felder zur Eingabe des Mindesthaltbarkeitsdatums anzeigen?',
   'Show follow ups...'          => 'Zeige Wiedervorlagen...',
   'Show help text'              => 'Hilfetext anzeigen',
-  'Show history'                => 'Verlauf anzeigen',
+  'Show images'                 => 'Bilder zeigen',
   'Show items from invoices individually' => 'Artikel aus Rechnungen anzeigen',
+  'Show mappings (csv_import)'  => 'Spaltenzuordnungen anzeigen',
   'Show old dunnings'           => 'Alte Mahnungen anzeigen',
+  'Show only marked as paid invoices' => 'Nur "als geschlossen markiert" zeigen',
+  'Show only not mailed invoices' => 'Nur "nicht per E-Mail versendete" anzeigen',
+  'Show order'                  => 'Bestellung anzeigen',
   'Show overdue sales quotations and requests for quotations...' => 'Überfällige Angebote und Preisanfragen anzeigen...',
   'Show parts'                  => 'Artikel anzeigen',
+  'Show parts longdescription (notes) in select list' => 'Langtext in Auswahlliste bei mehreren Treffern im Stammdaten-Bestand anzeigen',
+  'Show purchase letters report' => 'Einkaufsbriefe zeigen',
+  'Show record tab in customer' => 'Zeige Tab Belege in den Kundenstammdaten',
+  'Show record tab in vendor'   => 'Zeige Tab Belege in den Lieferantenstammdaten',
   'Show requirement spec'       => 'Pflichtenheft anzeigen',
   'Show requirement spec template' => 'Pflichtenheftvorlage anzeigen',
   'Show sales letters report'   => 'Verkaufsbrief anzeigen',
@@ -2457,31 +3258,37 @@ $self->{texts} = {
   'Show the picture in the part form' => 'Bild in Warenmaske anzeigen',
   'Show the pictures in the result for search parts' => 'Bilder in Suchergebnis für Stammdaten -> Berichte -> Waren anzeigen',
   'Show the weights of articles and the total weight in orders, invoices and delivery notes?' => 'Sollen Warengewichte und Gesamtgewicht in Aufträgen, Rechnungen und Lieferscheinen angezeigt werden?',
+  'Show update button for positions in order forms' => 'Aktualisieren-Knopf bei Positionen in Belegen anzeigen (neuer Auftrags-Controller)',
   'Show weights'                => 'Gewichte anzeigen',
   'Show your TODO list after logging in' => 'Aufgabenliste nach dem Anmelden anzeigen',
   'Show »not delivered qty/value« column in sales and purchase orders' => 'Spalte »Nicht gelieferte Menge/Wert« in Aufträgen anzeigen',
   'Signature'                   => 'Unterschrift',
-  'Since bin is not enforced in the parts data, please specify a bin where goods without a specified bin will be put.' => 'Da Lagerpl&auml;tze kein Pflichtfeld sind, geben Sie bitte einen Lagerplatz an, in dem Waren ohne spezifizierten Lagerplatz eingelagert werden sollen.',
+  'Since bin is not enforced in the parts data, please specify a bin where goods without a specified bin will be put.' => 'Da Lagerplätze kein Pflichtfeld sind, geben Sie bitte einen Lagerplatz an, in dem Waren ohne spezifizierten Lagerplatz eingelagert werden sollen.',
   'Single quotes'               => 'Einfache Anführungszeichen',
   'Single values in item mode, cumulated values in invoice mode' => 'Einzelwerte im Artikelmodus, kumulierte Werte im Rechnungsmodus',
   'Size'                        => 'Größe',
-  'Sketch'                      => 'Skizze',
   'Skip'                        => 'Überspringen',
   'Skip entry'                  => 'Eintrag überspringen',
   'Skipping because transfer amount is empty.' => 'Übersprungen wegen leeren Betrags.',
   'Skipping due to existing bank transaction in database' => 'Wegen schon existierender Bankbewegung in Datenbank übersprungen',
   'Skipping due to existing entry in database' => 'Wegen existierendem Eintrag mit selber Nummer übersprungen',
   'Skipping due to existing entry in database with different type' => 'Wegen existierendem Eintrag von unterschiedlichem Artikeltyp übersprungen',
-  'Skipping, for assemblies are not importable (yet)' => 'Übersprungen, da Erzeugnisse (noch) nicht importiert werden können',
+  'Skipping due to existing entry with different unit' => 'Wird aufgrund eines existierenden Eintrags mit anderer Einheit übersprungen.',
+  'Skipping due to same partnumber in csv file' => 'Eintrag in Datei mit doppelter Artikelnummer wird übersprungen',
+  'Skipping non-existent article' => 'Überspringe nicht vorhandenen Artikel',
   'Skonto'                      => 'Skonto',
+  'Skonto Tax Correction for'   => 'Skonto-Steuerkorrektur für',
   'Skonto Terms'                => 'Zahlungsziel Skonto',
   'Skonto amount'               => 'Skontobetrag',
   'Skonto information'          => 'Skonto Information',
   'So far you could use one partnumber for severel parts, for example a service and an article.' => 'Bisher war es möglich eine Artikelnummer für mehrere Artikel zu verwenden, zum Beispiel eine Artikelnummer für eine Dienstleistung, eine Ware und ein Erzeugnis.',
   'Sold'                        => 'Verkauft',
+  'Sold order items'            => 'Verkaufte Auftragsartikel',
   'Soldtotal does not make sense without any bsooqr options' => 'Option "Menge in gewählten Belegen" ohne gewählte Belege wird ignoriert.',
   'Solution'                    => 'Lösung',
+  'Sorry, I am too stupid to figure out the default warehouse/bin and the sold qty. I drop the default warehouse/bin option.' => 'Entschuldigung, ich bin nicht in der Lage Standard-Lager und die Menge in gewählten Belegen gleichzeitig anzuzeigen. Ich lass die Standard-Lager weg.',
   'Sort By'                     => 'Sortiert nach',
+  'Sort order'                  => 'Sortierfolge',
   'Source'                      => 'Beleg',
   'Source BIC'                  => 'Quell-BIC',
   'Source IBAN'                 => 'Quell-IBAN',
@@ -2490,40 +3297,67 @@ $self->{texts} = {
   'Space'                       => 'Leerzeichen',
   'Split entry detected. The values you have entered will result in an entry with more than one position on both debit and credit. Due to known problems involving accounting software kivitendo does not allow these.' => 'Splitbuchung! Die eingebenen Werte würden eine Buchung auslösen, die jeweils mehr als eine Position auf Soll und Haben hätte. Um Kompatibilität mit DATEV zu gewährleisten erlaubt kivitendo keine Splitbuchungen.',
   'Spoolfile'                   => 'Druckdatei',
+  'Staff member must not be empty.' => 'Mitarbeiter darf nicht leer sein.',
+  'Start'                       => 'Start',
+  'Start (verb)'                => 'Starten',
   'Start Dunning Process'       => 'Mahnprozess starten',
-  'Start analysis'              => 'Analyse beginnen',
   'Start date'                  => 'Startdatum',
   'Start of year'               => 'Jahresanfang',
   'Start process'               => 'Prozess starten',
-  'Start task server'           => 'Task-Server starten',
   'Start the correction assistant' => 'Korrekturassistenten starten',
+  'Start time'                  => 'Startzeit',
+  'Start time must be earlier than end time.' => 'Startzeit muss vor der Endzeit liegen.',
+  'Startdate method'            => 'Methode zur Ermittlung des Startdatums',
   'Startdate_coa'               => 'Gültig ab',
   'Starting Balance'            => 'Eröffnungsbilanzwerte',
   'Starting balance'            => 'Anfangssaldo',
   'Starting date'               => 'Anfangsdatum',
   'Starting the task server failed.' => 'Das Starten des Task-Servers schlug fehl.',
   'Starting with version 2.6.3 the configuration files in "config" have been consolidated.' => 'Ab Version 2.6.3 wurden die Konfiguration vereinfacht und es gibt nur noch eine Konfigurationsdatei im Verzeichnis config',
-  'Statement'                   => 'Sammelrechnung',
+  'Statement'                   => 'Statement',
   'Statement Balance'           => 'Sammelrechnungsbilanz',
-  'Statement sent to'           => 'Sammelrechnung verschickt an',
+  'Statement sent to'           => 'Gesendet an ',
   'Statements sent to printer!' => 'Sammelrechnungen an Drucker geschickt!',
   'Status'                      => 'Status',
+  'Status Shoptransfer'         => 'Status Shoptransfer',
+  'Status Shopupload'           => 'Status Shopupload',
+  'Step #1/#2'                  => 'Schritt #1/#2',
   'Step 1 -- limit number of delivery orders to process' => 'Schritt 1 -- Anzahl zu verarbeitender Lieferscheine begrenzen',
   'Step 2'                      => 'Schritt 2',
   'Step 2 -- Watch status'      => 'Schritt 2 -- Status beobachten',
   'Steuersatz'                  => 'Steuersatz',
   'Stock'                       => 'Einlagern',
+  'Stock Local/Shop'            => 'Bestand Lokal/Online',
   'Stock Qty for Date'          => 'Lagerbestand am',
   'Stock for part #1'           => 'Bestand für Artikel #1',
+  'Stock levels'                => 'Lagerbestände',
+  'Stock transfered'            => 'Lagerbewegungen ausgeführt',
   'Stock value'                 => 'Bestandswert',
+  'StockInfo'                   => 'Lagerinfo',
   'Stocked Qty'                 => 'Lagermenge',
-  'Stop task server'            => 'Task-Server beenden',
+  'Stocktaking'                 => 'Inventur',
+  'Stocktaking History'         => 'Inventur Historie',
+  'Stocktaking Journal'         => 'Inventurbuchungen',
+  'Stop (verb)'                 => 'Stoppen',
   'Stopping the task server failed. Output:' => 'Das Beenden des Task-Servers schlug fehl.',
+  'Storage Backends'            => 'Datei-Speicher',
+  'Storage Type for Attachments' => 'Speichertyp für Anhänge',
+  'Storage Type for generated/imported PDF Documents' => 'Speichertyp für erzeugte oder importierte Dokumente',
+  'Storage Type for images'     => 'Speichertyp für Bilder',
+  'Storage Type for shopimages' => 'Speichertyp für Shopbilder',
+  'Storing PDF in storage backend failed: #1' => 'Speichern der PDF-Datei im Datei-Speicher fehlgeschlagen: #1',
+  'Storing PDF to webdav folder failed: #1' => 'Speichern der PDF im WebDAV Ordner fehlgeschlagen: #1',
+  'Storing the document in the storage backend failed: #1' => 'Das Ablegen des Dokuments im Dokumentenspeicher schlug fehl: #1',
+  'Storing the document to the WebDAV folder failed: #1' => 'Das Ablegen des Dokuments im WebDAV-Ordner schlug fehl: #1',
   'Storing the emails in the journal is currently disabled in the client configuration.' => 'Das Speichern von versendeten E-Mails ist derzeit in der Mandantenkonfigurierung abgeschaltet.',
   'Storno'                      => 'Storno',
   'Storno (one letter abbreviation)' => 'S',
   'Storno Invoice'              => 'Stornorechnung',
   'Street'                      => 'Straße',
+  'Street 1'                    => 'Straße 1',
+  'Street 2'                    => 'Straße 2',
+  'Strict and halt'             => 'Strikt und Abbruch',
+  'Strict but replace'          => 'Strikt mit Ersetzungen',
   'Style the picture with the following CSS code' => 'Bildeigenschaft mit folgendem CSS-Style versehen',
   'Stylesheet'                  => 'Stilvorlage',
   'Sub function blocks'         => 'Unterfunktionsblöcke',
@@ -2540,11 +3374,21 @@ $self->{texts} = {
   'Sum for #1'                  => 'Summe für #1',
   'Sum for section'             => 'Summe für Abschnitt',
   'Sum of all amounts'          => 'Summe aller Beträge',
+  'Sum of bank #1 and sum of bookings #2' => 'Summe der Bank #1 und Summe der Buchungen #2',
   'Sum open amount'             => 'Summierter offener Betrag',
   'Sum per'                     => 'Summe per',
   'Summen- und Saldenliste'     => 'Summen- und Saldenliste',
+  'Sun'                         => 'So',
+  'Sunday'                      => 'Sonntag',
   'Superuser name'              => 'Datenbankadministrator',
+  'Supplier Delivery Order'     => 'Beistell-Lieferschein',
+  'Supplier Delivery Order has been deleted' => 'Beistell-Lieferschein wurde gelöscht',
+  'Supplier Delivery Order has been saved' => 'Beistell-Lieferschein wurde gespeichert',
+  'Supplier Delivery Orders'    => 'Beistell-Lieferscheine',
+  'Supplier delivery order'     => 'Beistell-Lieferschein',
   'Supplies'                    => 'Lieferungen',
+  'Surname'                     => 'Nachname',
+  'Switzerland'                 => 'Schweiz',
   'System'                      => 'System',
   'System currently down for maintenance!' => 'kivitendo ist momentan zwecks Wartungsarbeiten nicht zugänglich.',
   'TODO list'                   => 'Aufgabenliste',
@@ -2556,11 +3400,14 @@ $self->{texts} = {
   'Target bank account'         => 'Zielkonto',
   'Target table'                => 'Zieltabelle',
   'Task Server is not running, starting it now. If this does not change, please check your task server config' => 'Der Taskserver läuft nicht, starte ihn jetzt. Das kann ein paar Sekunden dauern. Sollte das nicht funktionieren, prüfen Sie bitte die Taskserver-Konfiguration.',
+  'Task server'                 => 'Task-Server',
   'Task server control'         => 'Task-Server-Steuerung',
   'Task server status'          => 'Task-Server-Status',
   'Tax'                         => 'Steuer',
+  'Tax Account'                 => 'Steuerkonto',
+  'Tax Account Name'            => 'Steuerkontoname',
   'Tax Consultant'              => 'Steuerberater/-in',
-  'Tax ID number'               => 'UStID-Nummer',
+  'Tax ID number'               => 'USt-IdNr.',
   'Tax Included'                => 'Steuer im Preis inbegriffen',
   'Tax Number'                  => 'Steuernummer',
   'Tax Number / SSN'            => 'Steuernummer',
@@ -2569,13 +3416,14 @@ $self->{texts} = {
   'Tax Percent is a number between 0 and 100' => 'Prozentsatz muss zwischen
   1% und 100% liegen',
   'Tax Period'                  => 'Voranmeldungszeitraum',
-  'Tax Position'                => 'Position',
   'Tax collected'               => 'vereinnahmte Steuer',
   'Tax deleted!'                => 'Steuer gelöscht!',
   'Tax number'                  => 'Steuernummer',
   'Tax paid'                    => 'Vorsteuer',
+  'Tax point'                   => 'Leistungsdatum',
   'Tax rate'                    => 'Steuersatz',
   'Tax saved!'                  => 'Steuer gespeichert!',
+  'Tax zone'                    => 'Steuerzone',
   'Tax zone #1 needs a valid expense account' => 'Steuerzone #1 braucht ein gültiges Aufwandskonto',
   'Tax zone #1 needs a valid income account' => 'Steuerzone #1 braucht ein gültiges Ertragskonto',
   'Tax zone (database ID)'      => 'Steuerzone ((Datenbank-ID)',
@@ -2593,7 +3441,6 @@ $self->{texts} = {
   'Taxkey_coa'                  => 'Steuerschlüssel',
   'Taxkeys and Taxreport Preferences' => 'Steuerautomatik und UStVA',
   'Taxlink_coa'                 => 'Steuerautomatik',
-  'Taxnumber'                   => 'Steuernummer',
   'Taxrate missing!'            => 'Prozentsatz fehlt!',
   'Taxzones'                    => 'Steuerzonen',
   'Tel'                         => 'Tel',
@@ -2601,79 +3448,102 @@ $self->{texts} = {
   'Telephone'                   => 'Telefon',
   'Template'                    => 'Druckvorlage',
   'Template Code'               => 'Vorlagenkürzel',
-  'Template Code missing!'      => 'Vorlagenkürzel fehlt!',
+  'Template Description'        => 'Name der Vorlage',
   'Template database'           => 'Datenbankvorlage',
+  'Template date'               => 'Vorlagendatum',
   'Templates'                   => 'Vorlagen',
   'Terms missing in row '       => '+Tage fehlen in Zeile ',
-  'Test and preview'            => 'Test und Vorschau',
   'Test database connectivity'  => 'Datenbankverbindung testen',
+  'Text'                        => 'Text',
   'Text block actions'          => 'Textblockaktionen',
   'Text block picture actions'  => 'Aktionen für Textblockbilder',
   'Text blocks'                 => 'Textblöcke',
   'Text blocks back'            => 'Textblöcke hinten',
   'Text blocks front'           => 'Textblöcke vorne',
   'Text field'                  => 'Textfeld',
-  'Text field variables: \'WIDTH=w HEIGHT=h\' sets the width and height of the text field. They default to 30 and 5 respectively.' => 'Textfelder: \'WIDTH=w HEIGHT=h\' setzen die Breite und die H&ouml;he des Textfeldes. Wenn nicht anders angegeben, so werden sie 30 Zeichen breit und f&uuml;nf Zeichen hoch dargestellt.',
-  'Text variables: \'MAXLENGTH=n\' sets the maximum entry length to \'n\'.' => 'Textzeilen: \'MAXLENGTH=n\' setzt eine Maximall&auml;nge von n Zeichen.',
-  'Text, text field and number variables: The default value will be used as-is.' => 'Textzeilen, Textfelder und Zahlenvariablen: Der Standardwert wird so wie er ist &uuml;bernommen.',
+  'Text field and HTML field variables: \'WIDTH=w HEIGHT=h\' sets the width and height of the field in pixels. They default to 225 and 90 respectively.' => 'Textfelder und HTML-Felder: \'WIDTH=w HEIGHT=h\' setzen die Breite und die Höhe des Feldes in Pixeln. Wenn nicht anders angegeben, so werden sie 225 Pixel breit und 90 Pixel hoch dargestellt.',
+  'Text in CSV File'            => 'Spalte in der CSV Datei',
+  'Text variables: \'MAXLENGTH=n\' sets the maximum entry length to \'n\'.' => 'Textzeilen: \'MAXLENGTH=n\' setzt eine Maximallänge von n Zeichen.',
+  'Text, text field, HTML field and number variables: The default value will be used as-is.' => 'Textzeilen, Textfelder, HTML-Felder und Zahlenvariablen: Der Standardwert wird so wie er ist übernommen.',
+  'Texts for invoices'          => 'Texte für Rechnungen',
+  'Texts for quotations & orders' => 'Texte für Angebote & Aufträge',
   'That export does not exist.' => 'Dieser Export existiert nicht.',
   'That is why kivitendo could not find a default currency.' => 'Daher konnte kivitendo keine Standardwährung finden.',
   'The \'name\' is the field shown to the user during login.' => 'Der \'Name\' ist derjenige, der dem Benutzer beim Login angezeigt wird.',
+  'The \'pclass\' column has the same abbreviation like a part export. The first letter is for the type Part,Assembly or Service, the second(and third) for Part Classification' => 'Die Spalte \'pclass\' besteht aus zwei Buchstaben entsprechend dem Export. Der erste Buchstabe ist für \'W\'=Ware \'E\'=Erzeugnis oder \'D\' für Dienstleistung, der zweite steht für den Artikeltyp, z.B. E,V,H,P oder - ',
   'The \'tag\' field must only consist of alphanumeric characters or the carachters - _ ( )' => 'Das Feld \'tag\' darf nur aus alphanumerischen Zeichen und den Zeichen - _ ( ) bestehen.',
   'The AP transaction #1 has been deleted.' => 'Die Kreditorenbuchung #1 wurde gelöscht.',
   'The AR transaction #1 has been deleted.' => 'Die Debitorenbuchung #1 wurde gelöscht.',
   'The Bins in Inventory were only a information text field.' => 'Die Lagerplätze unter Stammdaten/Waren sind nur ein informatives Textfeld.',
   'The Bins in master data were only a information text field.' => 'Die Lagerplätze unter Stammdaten/Waren sind nur ein informatives Textfeld.',
-  'The Buchungsgruppe has been created.' => 'Die Buchungsgruppe wurde erstellt.',
-  'The Buchungsgruppe has been saved.' => 'Die Buchungsgruppe wurde gespeichert.',
-  'The Buchungsgruppe needs an inventory account.' => 'Die Buchungsgruppe braucht ein Warenbestandskonto.',
+  'The Factur-X/ZUGFeRD XML invoice was not found.' => 'Die Factur-X-/ZUGFeRD-XML-Rechnungsdaten wurden nicht gefunden.',
+  'The Factur-X/ZUGFeRD notes have been saved.' => 'Die Factur-X-/ZUGFeRD-Notizen wurden gespeichert.',
+  'The Factur-X/ZUGFeRD version used is not supported.' => 'Die verwendete Factur-X-/ZUGFeRD-Version wird nicht unterstützt.',
   'The GL transaction #1 has been deleted.' => 'Die Dialogbuchung #1 wurde gelöscht.',
+  'The Geierlein path has not been set in the configuration.' => 'Der Geierlein-Pfad wurde in der Konfigurationsdatei nicht gesetzt.',
+  'The Host Name is missing'    => 'Der Name des Servers fehlt',
+  'The Host Name seems invalid' => 'Der Name des Servers sieht ungültig aus, bspw.: www.server.com',
+  'The IBAN \'#1\' is not valid as IBANs in #2 must be exactly #3 characters long.' => 'Die IBAN \'#1\' ist ungültig, da IBANs in #2 genau #3 Zeichen lang sein müssen.',
   'The IBAN is missing.'        => 'Die IBAN fehlt.',
-  'The LDAP server "#1:#2" is unreachable. Please check config/kivitendo.conf.' => 'Der LDAP-Server "#1:#2" ist nicht erreichbar. Bitte &uuml;berpr&uuml;fen Sie die Angaben in config/kivitendo.conf.',
-  'The MT940 import needs an import profile called MT940' => 'Der MT940 Import benötigt ein Importprofil mit dem Namen "MT940"',
+  'The ID #1 is not a valid database ID.' => 'Die ID #1 ist keine gültige Datenbank-ID.',
+  'The LDAP server "#1:#2" is unreachable. Please check config/kivitendo.conf.' => 'Der LDAP-Server "#1:#2" ist nicht erreichbar. Bitte überprüfen Sie die Angaben in config/kivitendo.conf.',
+  'The Mail strings have been saved.' => 'Die vorbelegten E-Mail-Texte wurden gespeichert.',
+  'The PDF has been created'    => 'Die PDF-Datei wurde erstellt.',
+  'The PDF has been previewed'  => 'PDF-Druckvorschau ausgeführt',
+  'The PDF has been printed'    => 'Das PDF-Dokument wurde gedruckt.',
+  'The Protocol for Host Name seems invalid (expected: http:// or https://)!' => 'Das Protokoll für den Server sieht falsch aus. Erwartet wird "http://" oder "https://".',
+  'The Proxy Name seems invalid' => 'Der Hostname des Proxys sieht falsch aus',
   'The SEPA export has been created.' => 'Der SEPA-Export wurde erstellt',
   'The SEPA strings have been saved.' => 'Die bei SEPA-Überweisungen verwendeten Begriffe wurden gespeichert.',
+  'The SQL query can be parameterized with variables named as follows: <%name%>.' => 'Die SQL-Abfrage kann mittels Variablen wie folgt parametrisiert werden: <%Variablenname%>.',
+  'The SQL query does not contain any parameter that need to be configured.' => 'Die SQL-Abfrage enthält keine Parameter, die angegeben werden müssten.',
+  'The URL is missing.'         => 'URL fehlt',
+  'The VAT ID number \'#1\' is invalid.' => 'Die UStID-Nummer »#1« ist ungültig.',
+  'The VAT ID number in the client configuration is invalid.' => 'Die UStID-Nummer in der Mandantenkonfiguraiton ist ungültig.',
+  'The VAT registration number is missing in the client configuration.' => 'Die Umsatzsteuer-ID-Nummer fehlt in der Mandantenkonfiguration.',
   'The WebDAV feature has been used.' => 'Das WebDAV-Feature wurde benutzt.',
-  'The acceptance status has been created.' => 'Der Abnahmestatus wurde angelegt.',
-  'The acceptance status has been deleted.' => 'Der Abnahmestatus wurde gelöscht.',
-  'The acceptance status has been saved.' => 'Der Abnahmestatus wurde gespeichert.',
-  'The acceptance status is in use and cannot be deleted.' => 'Der Abnahmestatus wird verwendet und kann nicht gelöscht werden.',
+  'The XMP metadata does not declare the Factur-X/ZUGFeRD data.' => 'Die XMP-Metadaten enthalten keine Factur-X-/ZUGFeRD-Deklaration.',
+  'The ZUGFeRD invoice data cannot be generated because the data validation failed.' => 'Die ZUGFeRD-Rechnungsdaten können nicht erzeugt werden, da die Validierung fehlschlug.',
+  'The abbreviation is missing.' => 'Abkürzung fehlt',
   'The access rights a user has within a client instance is still governed by his group membership.' => 'Welche Zugriffsrechte ein Benutzer innerhalb eines Mandanten hat, wird weiterhin über Gruppenmitgliedschaften geregelt.',
   'The access rights have been saved.' => 'Die Zugriffsrechte wurden gespeichert.',
   'The account #1 is already being used by bank account #2.' => 'Das Konto #1 wird schon von Bankkonto #2 benutzt.',
   'The account 3804 already exists, the update will be skipped.' => 'Das Konto 3804 existiert schon, das Update wird übersprungen.',
   'The account 3804 will not be added automatically.' => 'Das Konto 3804 wird nicht automatisch hinzugefügt.',
+  'The action can only be executed once.' => 'Die Aktion kann nur einmal ausgeführt werden.',
   'The action is missing or invalid.' => 'Die action fehlt, oder sie ist ungültig.',
   'The action you\'ve chosen has not been executed because the document does not contain any item yet.' => 'Die von Ihnen ausgewählte Aktion wurde nicht ausgeführt, weil der Beleg noch keine Positionen enthält.',
   'The administration area is always accessible.' => 'Der Administrationsbereich ist immer zugänglich.',
   'The application "#1" was not found on the system.' => 'Die Anwendung "#1" wurde auf dem System nicht gefunden.',
+  'The assembly \'#1\' cannot be a part from itself.' => 'Das Erzeugnis \'#1\' kann kein Teil von sich selbst sein.',
+  'The assembly \'#1\' would make a loop in assembly tree.' => 'Das Erzeugnis \'#1\' würde eine Schleife im Erzeugnisbaum machen.',
+  'The assembly doesn\'t have any items.' => 'Das Erzeugnis enthält keine Artikel.',
   'The assembly has been created.' => 'Das Erzeugnis wurde hergestellt.',
   'The assistant could not find anything wrong with #1. Maybe the problem has been solved in the meantime.' => 'Der Korrekturassistent konnte kein Problem bei #1 feststellen. Eventuell wurde das Problem in der Zwischenzeit bereits behoben.',
+  'The assortment doesn\'t have any items.' => 'Das Sortiment enthält keine Artikel.',
   'The authentication database is not reachable at the moment. Either it hasn\'t been set up yet or the database server might be down. Please contact your administrator.' => 'Die Authentifizierungs-Datenbank kann momentan nicht erreicht werden. Entweder wurde sie noch nicht eingerichtet, oder der Datenbankserver ist momentan nicht verfügbar. Bitte wenden Sie sich an Ihren Administrator.',
-  'The available options depend on the varibale type:' => 'Die verf&uuml;gbaren Optionen h&auml;ngen vom Variablentypen ab:',
+  'The available options depend on the varibale type:' => 'Die verfügbaren Optionen hängen vom Variablentypen ab:',
   'The background job could not be destroyed.' => 'Der Hintergrund-Job konnte nicht gelöscht werden.',
   'The background job has been created.' => 'Der Hintergrund-Job wurden angelegt.',
   'The background job has been deleted.' => 'Der Hintergrund-Job wurde gelöscht.',
   'The background job has been saved.' => 'Der Hintergrund-Job wurde gespeichert.',
   'The background job was executed successfully.' => 'Der Hintergrund-Job wurde erfolgreich ausgeführt.',
-  'The bank account has been created.' => 'Das Bankkonto wurde erstellt.',
-  'The bank account has been deleted.' => 'Das Bankkonto wurde gelöscht.',
-  'The bank account has been saved.' => 'Das Bankkonto wurde gespeichert',
-  'The bank account has been used and cannot be deleted.' => 'Das Bankkonto wurde benutzt und kann nicht gelöscht werden.',
   'The bank information must not be empty.' => 'Die Bankinformationen müssen vollständig ausgefüllt werden.',
   'The base file name without a path or an extension to be used for printing for this type of requirement spec.' => 'Der Basisdateiname ohne Pfadanteil oder Erweiterung, der bei Drucken dieses Pflichtenhefttyps verwendet wird.',
-  'The base unit does not exist or it is about to be deleted in row %d.' => 'Die Basiseinheit in Zeile %d existiert nicht oder soll gel&ouml;scht werden.',
+  'The base unit does not exist or it is about to be deleted in row %d.' => 'Die Basiseinheit in Zeile %d existiert nicht oder soll gelöscht werden.',
   'The base unit does not exist.' => 'Die Basiseinheit existiert nicht.',
-  'The base unit relations must not contain loops (e.g. by saying that unit A\'s base unit is B, B\'s base unit is C and C\'s base unit is A) in row %d.' => 'Die Beziehungen der Einheiten d&uuml;rfen keine Schleifen beinhalten (z.B. wenn gesagt wird, dass Einheit As Basiseinheit B, Bs Basiseinheit C und Cs Basiseinheit A ist) in Zeile %d.',
+  'The base unit relations must not contain loops (e.g. by saying that unit A\'s base unit is B, B\'s base unit is C and C\'s base unit is A) in row %d.' => 'Die Beziehungen der Einheiten dürfen keine Schleifen beinhalten (z.B. wenn gesagt wird, dass Einheit As Basiseinheit B, Bs Basiseinheit C und Cs Basiseinheit A ist) in Zeile %d.',
   'The basic client tables have not been created for this client\'s database yet.' => 'Die grundlegenden Mandantentabellen wurden in der für diesen Mandanten konfigurierten Datenbank noch nicht angelegt.',
+  'The billing period has already been locked.' => 'Die Buchungsperiode wurde bereits abgeschlossen.',
   'The body is missing.'        => 'Der Text fehlt',
-  'The buchungsgruppe has been deleted.' => 'Die Buchungsgruppe wurde gelöscht.',
-  'The buchungsgruppe is in use and cannot be deleted.' => 'Die Buchungsgruppe wird benutzt und kann daher nicht gelöscht werden.',
-  'The business has been created.' => 'Der Kunden-/Lieferantentyp wurde erfasst.',
-  'The business has been deleted.' => 'Der Kunden-/Lieferantentyp wurde gelöscht.',
-  'The business has been saved.' => 'Der Kunden-/Lieferantentyp wurde gespeichert.',
-  'The business is in use and cannot be deleted.' => 'Der Kunden-/Lieferantentyp wird benutzt und kann nicht gelöscht werden.',
-  'The changing of tax-o-matic account is NOT recommended, but if you do so please also (re)configure buchungsgruppen and reconfigure ALL charts which point to this tax-o-matic account. ' => 'Es wird nicht empfohlen Steuerkonten (Umsatzsteuer oder Vorsteuer) "umzuhängen", aber falls es gemacht wird, bitte auch entsprechend konsequent die Buchungsgruppen und die Konten die mit dieser Steuer verknüpft sind umkonfigurieren.',
+  'The booking group has been created.' => 'Die Buchungsgruppe wurde erstellt.',
+  'The booking group has been deleted.' => 'Die Buchungsgruppe wurde gelöscht.',
+  'The booking group has been saved.' => 'Die Buchungsgruppe wurde gespeichert.',
+  'The booking group is in use and cannot be deleted.' => 'Die Buchungsgruppe wird benutzt und kann daher nicht gelöscht werden.',
+  'The booking group needs an inventory account.' => 'Die Buchungsgruppe braucht ein Warenbestandskonto.',
+  'The buchungsgruppe is missing.' => 'Die Buchungsgruppe fehlt.',
+  'The categories has been saved.' => 'Artikelgruppe gespeichert',
+  'The changing of tax-o-matic account is NOT recommended, but if you do so please also (re)configure booking groups and reconfigure ALL charts which point to this tax-o-matic account. ' => 'Es wird nicht empfohlen Steuerkonten (Umsatzsteuer oder Vorsteuer) "umzuhängen", aber falls es gemacht wird, bitte auch entsprechend konsequent die Buchungsgruppen und die Konten die mit dieser Steuer verknüpft sind umkonfigurieren.',
   'The chart is not valid.'     => 'Das Konto ist nicht gültig.',
   'The client could not be deleted.' => 'Der Mandant konnte nicht gelöscht werden.',
   'The client has been created.' => 'Der Mandant wurde angelegt.',
@@ -2681,77 +3551,89 @@ $self->{texts} = {
   'The client has been saved.'  => 'Der Mandant wurde gespeichert.',
   'The clipboard does not contain anything that can be pasted here.' => 'Die Zwischenablage enthält momentan keine Objekte, die hier eingefügt werden können.',
   'The column "datatype" must be present and must be at the same position / column in each data set. The values must be the row names (see settings) for order and item data respectively.' => 'Die Spalte "datatype" muss vorhanden sein und sie muss in jedem Datensatz an der gleichen Stelle / Spalte sein. Die Werte in dieser Spalte müssen die Namen der Auftrag-/Positions-Zeilen (siehe Einstellungen) sein.',
-  'The column "make_X" can contain either a vendor\'s database ID, a vendor number or a vendor\'s name.' => 'Die Spalte "make_X" can entweder die Datenbank-ID des Lieferanten, eine Lieferantennummer oder einen Lieferantennamen enthalten.',
+  'The column "make_X" can contain either a vendor\'s database ID, a vendor number or a vendor\'s name.' => 'Die Spalte "make_X" kann entweder die Datenbank-ID des Lieferanten, eine Lieferantennummer oder einen Lieferantennamen enthalten.',
   'The column triplets can occur multiple times with different numbers "X" each time (e.g. "make_1", "model_1", "lastcost_1", "make_2", "model_2", "lastcost_2", "make_3", "model_3", "lastcost_3" etc).' => 'Die Spalten-Dreiergruppen können mehrfach auftreten, sofern sie unterschiedliche Nummern "X" verwenden (z.B. "make_1", "model_1", "lastcost_1", "make_2", "model_2", "lastcost_2", "make_3", "model_3", "lastcost_3" etc).',
-  'The columns &quot;Dunning Duedate&quot;, &quot;Total Fees&quot; and &quot;Interest&quot; show data for the previous dunning created for this invoice.' => 'Die Spalten &quot;Zahlbar bis&quot;, &quot;Kumulierte Geb&uuml;hren&quot; und &quot;Zinsen&quot; zeigen Daten der letzten f&uuml;r diese Rechnung erzeugten Mahnung.',
+  'The columns &quot;Dunning Duedate&quot;, &quot;Total Fees&quot; and &quot;Interest&quot; show data for the previous dunning created for this invoice.' => 'Die Spalten &quot;Zahlbar bis&quot;, &quot;Kumulierte Gebühren&quot; und &quot;Zinsen&quot; zeigen Daten der letzten für diese Rechnung erzeugten Mahnung.',
   'The combination of database host, port and name is not unique.' => 'Die Kombination aus Datenbankhost, -port und -name ist nicht eindeutig.',
   'The command is missing.'     => 'Der Befehl fehlt.',
-  'The complexity has been created.' => 'Der Komplexitätsgrad wurde angelegt.',
-  'The complexity has been deleted.' => 'Der Komplexitätsgrad wurde gelöscht.',
-  'The complexity has been saved.' => 'Der Komplexitätsgrad wurde gespeichert.',
-  'The complexity is in use and cannot be deleted.' => 'Der Komplexitätsgrad wird verwendet und kann nicht gelöscht werden.',
-  'The connection to the LDAP server cannot be encrypted (SSL/TLS startup failure). Please check config/kivitendo.conf.' => 'Die Verbindung zum LDAP-Server kann nicht verschl&uuml;sselt werden (Fehler bei SSL/TLS-Initialisierung). Bitte &uuml;berpr&uuml;fen Sie die Angaben in config/kivitendo.conf.',
+  'The company\'s address information is incomplete in the client configuration.' => 'Die Firmenadresse in der Mandantenkonfiguration ist unvollständig.',
+  'The connection to the LDAP server cannot be encrypted (SSL/TLS startup failure). Please check config/kivitendo.conf.' => 'Die Verbindung zum LDAP-Server kann nicht verschlüsselt werden (Fehler bei SSL/TLS-Initialisierung). Bitte überprüfen Sie die Angaben in config/kivitendo.conf.',
   'The connection to the authentication database failed:' => 'Die Verbindung zur Authentifizierungsdatenbank schlug fehl:',
   'The connection to the configured client database "#1" on host "#2:#3" failed.' => 'Die Verbindung zur konfigurierten Datenbank "#1" auf Host "#2:#3" schlug fehl.',
   'The connection to the database could not be established.' => 'Die Verbindung zur Datenbank konnte nicht hergestellt werden.',
+  'The connection to the shop could not be established.' => 'Es konnte keine Verbindung zum Shop hergestellt werden',
+  'The connection to the shop was established successfully.' => 'Die Verbindung konnte erfolgreich hergestellt werden',
   'The connection to the template database failed:' => 'Die Verbindung zur Vorlagendatenbank schlug fehl:',
   'The connection was established successfully.' => 'Die Verbindung zur Datenbank wurde erfolgreich hergestellt.',
   'The contact person attribute "birthday" is converted from a free-form text field into a date field.' => 'Das Kontaktpersonenfeld "Geburtstag" wird von einem freien Textfeld auf ein Datumsfeld umgestellt.',
+  'The country from the company\'s address in the client configuration cannot be mapped to an ISO 3166-1 alpha 2 code.' => 'Das Land der Firmenadresse in der Mandantenkonfiguration kann keinem der bekannten ISO 3166-1 Alpha 2-Codes zugeordnet werden.',
+  'The country from the customer\'s address cannot be mapped to an ISO 3166-1 alpha 2 code.' => 'Das Land aus der Kunden-Rechnungsadresse kann keinem der bekannten ISO 3166-1 Alpha 2-Codes zugeordnet werden.',
   'The creation of the authentication database failed:' => 'Das Anlegen der Authentifizierungsdatenbank schlug fehl:',
+  'The credentials (username & password) for connecting database are wrong.' => 'Die Daten (Benutzername & Passwort) für das Login zur Datenbank sind falsch.',
+  'The currency "#1" cannot be mapped to an ISO 4217 currency code.' => 'Die Währung "#1" kann keinem der bekannten ISO 4217-Codes zugeordnet werden.',
+  'The custom data export has been deleted.' => 'Der benutzerdefinierte Datenexport wurde gelöscht.',
+  'The custom data export has been saved.' => 'Der benutzerdefinierte Datenexport wurde gespeichert.',
   'The custom variable has been created.' => 'Die benutzerdefinierte Variable wurde erfasst.',
   'The custom variable has been deleted.' => 'Die benutzerdefinierte Variable wurde gelöscht.',
   'The custom variable has been saved.' => 'Die benutzerdefinierte Variable wurde gespeichert.',
   'The custom variable is in use and cannot be deleted.' => 'Die benutzerdefinierte Variable ist in Benutzung und kann nicht gelöscht werden.',
   'The customer name is missing.' => 'Der Kundenname fehlt.',
+  'The customer order number is missing. Do you want to continue anyway?' => 'Die Kundenbestellnummer fehlt. Möchten Sie trotzdem fortfahren?',
+  'The customer\'s bank account number (IBAN) is missing.' => 'Die Kontonummer (IBAN) des Kunden fehlt.',
   'The database for user management and authentication does not exist. You can create let kivitendo create it with the following parameters:' => 'Die Datenbank für die Benutzeranmeldung existiert nicht. Sie können Sie von kivitendo automatisch mit den folgenden Parametern anlegen lassen:',
   'The database host is missing.' => 'Der Datenbankhost fehlt.',
   'The database name is missing.' => 'Der Datenbankname fehlt.',
   'The database port is missing.' => 'Der Datenbankport fehlt.',
   'The database update/creation did not succeed. The file #1 contained the following error:' => 'Die Datenbankaktualisierung/erstellung schlug fehl. Die Datei #1 enthielt den folgenden Fehler:',
+  'The database user \'#1\' does not have superuser privileges.' => 'Der Datenbankbenutzer »#1« hat keine Super-Benutzer-Rechte.',
   'The database user is missing.' => 'Der Datenbankbenutzer fehlt.',
   'The dataset #1 has been created.' => 'Die Datenbank #1 wurde angelegt.',
   'The dataset #1 has been deleted.' => 'Die Datenbank #1 wurde gelöscht.',
   'The deductible amount'       => 'Der abziehbare Skontobetrag',
-  'The default delivery value report only checks if all delivery orders have been created not if the goods are transferred. This feature will check if all the goods are transferred. Caveat: Only the state of the delivery orders are checked not partial transferred delivery orders (in technical terms: the table inventory is not checked' => 'Standardmässig wird beim Lieferwertbericht überprüft, ob es eine vollständige Liefermenge über alle Lieferscheine gibt. Dies ist dann die Statusbedingung für geliefert oder nicht geliefert. Mit dieser Erweiterung wird geprüft ob die Lieferbelege auch wirklich ausgelagert sind oder nicht. Teilausgelagerte Lieferscheine werden allerdings nicht berücksichtigt (Technischer Hintergrund: Keine Überprüfung der Lagertabelle inventory).  ',
-  'The default value depends on the variable type:' => 'Die Bedeutung des Standardwertes h&auml;ngt vom Variablentypen ab:',
+  'The default value depends on the variable type:' => 'Die Bedeutung des Standardwertes hängt vom Variablentypen ab:',
   'The delivery order has not been marked as delivered. The warehouse contents have not changed.' => 'Der Lieferschein wurde nicht als geliefert markiert. Der Lagerinhalt wurde nicht verändert.',
   'The delivery term has been created.' => 'Die Lieferbedingungen wurden angelegt.',
   'The delivery term has been deleted.' => 'Die Lieferbedingungen wurden gelöscht.',
   'The delivery term has been saved.' => 'Die Lieferbedingungen wurden gespeichert.',
   'The delivery term is in use and cannot be deleted.' => 'Die Lieferbedingungen werden bereits verwendet und können nicht gelöscht werden.',
-  'The department has been created.' => 'Die Abteilung wurde angelegt.',
-  'The department has been deleted.' => 'Die Abteilung wurde gelöscht.',
-  'The department has been saved.' => 'Die Abteilung wurde gespeichert.',
-  'The department is in use and cannot be deleted.' => 'Die Abteilung wird benutzt und kann nicht gelöscht werden.',
   'The description is missing.' => 'Die Beschreibung fehlt.',
   'The description is not unique.' => 'Die Beschreibung ist nicht eindeutig.',
-  'The description is shown on the form. Chose something short and descriptive.' => 'Die Beschreibung wird in der jeweiligen Maske angezeigt. Sie sollte kurz und pr&auml;gnant sein.',
+  'The description is shown on the form. Chose something short and descriptive.' => 'Die Beschreibung wird in der jeweiligen Maske angezeigt. Sie sollte kurz und prägnant sein.',
   'The directory %s does not exist.' => 'Das Verzeichnis %s existiert nicht.',
   'The discount in percent'     => 'Der prozentuale Rabatt',
   'The discount must be less than 100%.' => 'Der Rabatt muss kleiner als 100% sein.',
   'The discount must not be negative.' => 'Der Rabatt darf nicht negativ sein.',
   'The discounted amount will be shown in documents.' => 'Der Rabattbetrag wird in Belegen ausgewiesen.',
+  'The display of (mainly) picker results can be configured. To insert the value of one option use <%Name%>.' => 'Die Anzeigenamen von (hauptsächlich) Auswahl-Ergebnissen (Picker) können konfiguriert werden. Um einen Wert einer Option in die Anzeige aufzunehmen, verwenden Sie <%Name%>.',
   'The document has been changed by another user. No mail was sent. Please reopen it in another window and copy the changes to the new window' => 'Die Daten wurden bereits von einem anderen Benutzer verändert. Deshalb ist das Dokument ungültig und es wurde keine E-Mail verschickt. Bitte öffnen Sie das Dokument erneut in einem extra Fenster und übertragen Sie die Daten',
   'The document has been changed by another user. Please reopen it in another window and copy the changes to the new window' => 'Die Daten wurden bereits von einem anderen Benutzer verändert. Deshalb ist das Dokument ungültig. Bitte öffnen Sie das Dokument erneut in einem extra Fenster und übertragen Sie die Daten',
-  'The document have been sent to \'#1\'.' => 'Das Dokument wurde an \'#1\' geschickt.',
-  'The documents have been sent to the printer \'#1\'.' => 'Die Dokumente wurden an den Drucker \'#1\' geschickt.',
-  'The dunning process started' => 'Der Mahnprozess ist gestartet.',
+  'The document has been created.' => 'Das Dokument wurde erzeugt.',
+  'The document has been printed.' => 'Das Dokument wurde gedruckt.',
+  'The documents have been sent to the printer \'#1\'.' => 'Die Dokumente sind zum Drucker \'#1\' geschickt',
   'The dunnings have been printed.' => 'Die Mahnung(en) wurden gedruckt.',
+  'The email has been sent.'    => 'Die E-Mail wurde verschickt.',
+  'The email was not sent due to the following error: #1.' => 'Die E-Mail wurde aufgrund des folgenden Fehlers nicht verschickt: #1',
   'The employee is missing.'    => 'Der Bearbeiter fehlt.',
   'The end date is the last day for which invoices will possibly be created.' => 'Das Enddatum ist das letztmögliche Datum, an dem eine Rechnung erzeugt wird.',
   'The execution schedule is invalid.' => 'Der Ausführungszeitplan ist ungültig.',
   'The execution type is invalid.' => 'Der Ausführungstyp ist ungültig.',
   'The existing record has been created from the link target to add.' => 'Der bestehende Beleg wurde aus dem auszuwählenden Verknüpfungsziel erstellt.',
+  'The export failed because of malformed transactions. Please fix those before exporting.' => 'Es sind fehlerhafte Buchungen im Exportzeitraum vorhanden. Bitte korrigieren Sie diese vor dem Export.',
   'The factor is missing in row %d.' => 'Der Faktor fehlt in Zeile %d.',
   'The factor is missing.'      => 'Der Faktor fehlt.',
+  'The file \'#1\' could not be opened for reading.' => 'Die Datei \'#1\' konnte nicht zum Lesen geöffnet werden.',
+  'The file \'#1\' does not contain the required XMP meta data.' => 'Die Datei \'#1\' enthält die erforderlichen XMP-Metadaten nicht.',
   'The file has been sent to the printer.' => 'Die Datei wurde an den Drucker geschickt.',
   'The file is available for download.' => 'Die Datei ist zum Herunterladen verfügbar.',
   'The file name is missing'    => 'Der Dateiname fehlt',
   'The first reason is that kivitendo contained a bug which resulted in the wrong taxkeys being recorded for transactions in which two entries are posted for the same chart with different taxkeys.' => 'Der erste Grund war ein Fehler in kivitendo, der dazu führte, dass bei einer Transaktion, bei der zwei Buchungen mit unterschiedlichen Steuerschlüsseln auf dasselbe Konto durchgeführt wurden, die falschen Steuerschlüssel gespeichert wurden.',
   'The follow-up date is missing.' => 'Das Wiedervorlagedatum fehlt.',
   'The following currencies have been used, but they are not defined:' => 'Die folgenden Währungen wurden benutzt, sind aber nicht ordnungsgemäß in der Datenbank eingetragen:',
-  'The following drafts have been saved and can be loaded.' => 'Die folgenden Entw&uuml;rfe wurden gespeichert und k&ouml;nnen geladen werden.',
+  'The following delivery orders could not be processed because they are already closed: #1' => 'Die folgenden Lieferscheine konnten nicht verarbeitet werden, da sie bereits geschlossen sind: #1',
+  'The following drafts have been saved and can be loaded.' => 'Die folgenden Entwürfe wurden gespeichert und können geladen werden.',
+  'The following errors occurred:' => 'Folgende Fehler sind aufgetreten:',
   'The following groups are valid for this client' => 'Die folgenden Gruppen sind für diesen Mandanten gültig',
+  'The following is only a preview.' => 'Das Folgende ist nur eine Vorschau.',
   'The following list has been generated automatically from existing users collapsing users with identical settings into a single entry.' => 'Die folgende Liste wurde automatisch aus den im System vorhandenen Benutzern zusammengestellt, wobei identische Einstellungen zu einem Eintrag zusammengefasst wurden.',
   'The following old files whose settings have to be merged manually into the new configuration file "config/kivitendo.conf" still exist:' => 'Es existieren noch die folgenden alten Dateien, deren Einstellungen manuell in die neue Konfiguratsdatei "config/kivitendo.conf" migriert werden müssen:',
   'The following transaction contains wrong taxes:' => 'Die folgende Buchung enthält falsche Steuern:',
@@ -2759,11 +3641,18 @@ $self->{texts} = {
   'The following transactions are concerned:' => 'Die folgenden Buchungen sind betroffen:',
   'The following users are a member of this group' => 'Die folgenden Benutzer sind Mitglieder dieser Gruppe',
   'The following users will have access to this client' => 'Die folgenden Benutzer werden auf diesen Mandanten Zugriff haben',
-  'The formula needs the following syntax:<br>For regular article:<br>Variablename= Variable Unit;<br>Variablename2= Variable2 Unit2;<br>...<br>###<br>Variable + ( Variable2 / Variable )<br><b>Please be beware of the spaces in the formula</b><br>' => 'Die Formeln m&uuml;ssen in der folgenden Syntax eingegeben werden:<br>Bei normalen Artikeln:<br>Variablenname = Variable Einheit;<br>Variablenname2 = Variable2 Einheit2;<br>...<br>###<br>Variable + Variable2 * ( Variable - Variable2 )<br>Variablennamen und Einheiten dürfen nur aus alphanumerischen Zeichen bestehen.<br>Es muss jeweils die Gesamte Zeile eingegeben werden',
+  'The formula needs the following syntax:<br>For regular article:<br>Variablename= Variable Unit;<br>Variablename2= Variable2 Unit2;<br>...<br>###<br>Variable + ( Variable2 / Variable )<br><b>Please be beware of the spaces in the formula</b><br>' => 'Die Formeln müssen in der folgenden Syntax eingegeben werden:<br>Bei normalen Artikeln:<br>Variablenname = Variable Einheit;<br>Variablenname2 = Variable2 Einheit2;<br>...<br>###<br>Variable + Variable2 * ( Variable - Variable2 )<br>Variablennamen und Einheiten dürfen nur aus alphanumerischen Zeichen bestehen.<br>Es muss jeweils die Gesamte Zeile eingegeben werden',
   'The greetings have been saved.' => 'Die Anreden wurden gespeichert',
   'The installation is currently locked.' => 'Die Installation ist momentan gesperrt.',
   'The installation is currently unlocked.' => 'Die Installation ist momentan entsperrt.',
+  'The invoice is not linked with a sales delivery order. Post anyway?' => 'Diese Rechnung ist mit keinem Lieferschein verknüpft. Dennoch Buchen?',
+  'The invoice recipient can either be a selected contact person (default) or the email adress set in the master data of the customer. Additionally a contact persons mail and the company\'s invoicing mail can be combined.' => 'Der E-Mail-Rechnungsempfänger ist entweder mit dem Ansprechpartner des Belegs vorbelegt (Standard) oder mit der E-Mail-Rechnungsadresse aus den Stammdaten. Alternativ können beide (Ansprechpartner in CC) vorbelegt werden.',
   'The invoices have been created. They\'re pre-selected below.' => 'Die Rechnungen wurden erzeugt. Sie sind unten vorausgewählt.',
+  'The item couldn\'t be deleted!' => 'Der Artikel konnte nicht gelöscht werden!',
+  'The item couldn\'t be saved!' => 'Der Artikel konnte nicht gespeichert werden!',
+  'The item has been created.'  => 'Der Artikel wurde angelegt.',
+  'The item has been deleted.'  => 'Der Artikel wurde gelöscht.',
+  'The item has been saved.'    => 'Der Artikel wurde gespeichert.',
   'The items are imported accoring do their number "X" regardless of the column order inside the file.' => 'Die Einträge werden in der Reihenfolge ihrer Indizes "X" unabhängig von der Spaltenreihenfolge in der Datei importiert.',
   'The link target to add has been created from the existing record.' => 'Das auszuwählende Verknüpfungsziel wurde aus dem bestehenden Beleg erstellt.',
   'The list has been printed.'  => 'Die Liste wurde ausgedruckt.',
@@ -2771,6 +3660,7 @@ $self->{texts} = {
   'The login is not unique.'    => 'Der Loginname ist nicht eindeutig.',
   'The long description is missing.' => 'Der Langtext fehlt.',
   'The master templates where not found.' => 'Der Vorlagensatz wurde nicht gefunden.',
+  'The maximum of uploadable filesize in Megabyte' => 'Die maximale Dateigröße in Megabytes, die hochladbar ist',
   'The name and description are not unique.' => 'Name und Beschreibung sind nicht einmalig.',
   'The name in row %d has already been used before.' => 'Der Name in Zeile %d wurde vorher bereits benutzt.',
   'The name is invalid.'        => 'Der Name ist ungültigt.',
@@ -2783,26 +3673,37 @@ $self->{texts} = {
   'The new requirement spec will be a copy of \'#1\' for customer \'#2\'.' => 'Das neue Pflichtenheft wird eine Kopie von \'#1\' für Kunde \'#2\' sein.',
   'The number of days for full payment' => 'Die Anzahl Tage, bis die Rechnung in voller Höhe bezahlt werden muss',
   'The numbering will start at 1 with each requirement spec.' => 'Die Nummerierung beginnt bei jedem Pflichtenheft bei 1.',
+  'The object has been created.' => 'Das Objekt wurde angelegt.',
+  'The object has been deleted.' => 'Das Objekt wurde gelöscht..',
+  'The object has been saved.'  => 'Das Objekt wurde gespeichert.',
+  'The object has not been saved yet.' => 'Das Objekt wurde noch nicht gespeichert.',
+  'The object is in use and cannot be deleted.' => 'Das Objekt ist in Benutzung und kann nicht gelöscht werden.',
   'The option field is empty.'  => 'Das Optionsfeld ist leer.',
+  'The order has been deleted'  => 'Der Auftrag wurde gelöscht.',
+  'The order has been saved'    => 'Der Auftrag wurde gespeichert.',
   'The package name is invalid.' => 'Der Paketname ist ungültig.',
+  'The partnumber already exists!' => 'Die Artikelnummer wird bereits verwendet.',
+  'The partnumber already exists.' => 'Die Artikelnummer wird bereits verwendet.',
+  'The partnumber is already being used' => 'Der Artikel ist bereits in Verwendung',
+  'The partnumber is missing.'  => 'Die Artikelnummer fehlt.',
   'The parts for this delivery order have already been transferred in.' => 'Die Artikel dieses Lieferscheins wurden bereits eingelagert.',
   'The parts for this delivery order have already been transferred out.' => 'Die Artikel dieses Lieferscheins wurden bereits ausgelagert.',
+  'The parts for this order have already been transferred' => 'Die Artikel in diesem Lieferschein wurden schon umgelagert',
   'The parts have been removed.' => 'Die Waren wurden aus dem Lager entnommen.',
-  'The parts have been stocked.' => 'Die Artikel wurden eingelagert.',
   'The parts have been transferred.' => 'Die Waren wurden umgelagert.',
+  'The partsgroup is missing.'  => 'Die Warengruppe fehlt.',
   'The password is too long (maximum length: #1).' => 'Das Passwort ist zu lang (maximale Länge: #1).',
   'The password is too short (minimum length: #1).' => 'Das Password ist zu kurz (minimale Länge: #1).',
   'The password is weak (e.g. it can be found in a dictionary).' => 'Das Passwort ist schwach (z.B. wenn es in einem Wörterbuch steht).',
+  'The path is missing.'        => 'Pfad fehlt',
   'The payment term has been created.' => 'Die Zahlungsbedingungen wurden angelegt.',
   'The payment term has been deleted.' => 'Die Zahlungsbedingungen wurden gelöscht.',
   'The payment term has been saved.' => 'Die Zahlungsbedingungen wurden gespeichert.',
   'The payment term is in use and cannot be deleted.' => 'Die Zahlungsbedingungen werden bereits benutzt und können nicht gelöscht werden.',
   'The payments have been posted.' => 'Die Zahlungen wurden gebucht.',
-  'The predefined text has been created.' => 'Der vordefinierte Textblock wurde angelegt.',
-  'The predefined text has been deleted.' => 'Der vordefinierte Textblock wurde gelöscht.',
-  'The predefined text has been saved.' => 'Der vordefinierte Textblock wurde gespeichert.',
-  'The predefined text is in use and cannot be deleted.' => 'Der vordefinierte Textblock wird verwendet und kann nicht gelöscht werden.',
-  'The preferred one is to install packages provided by your operating system distribution (e.g. Debian or RPM packages).' => 'Die bevorzugte Art, ein Perl-Modul zu installieren, ist durch Installation eines von Ihrem Betriebssystem zur Verf&uuml;gung gestellten Paketes (z.B. Debian-Pakete oder RPM).',
+  'The periodic invoices config has been assigned.' => 'Die Konfiguration für wiederkehrende Rechnungen wurde übernommen.',
+  'The port is missing.'        => 'Port fehlt',
+  'The preferred one is to install packages provided by your operating system distribution (e.g. Debian or RPM packages).' => 'Die bevorzugte Art, ein Perl-Modul zu installieren, ist durch Installation eines von Ihrem Betriebssystem zur Verfügung gestellten Paketes (z.B. Debian-Pakete oder RPM).',
   'The price rule for this discount does not exist anymore' => 'Die Preisregel für diesen Rabatt existiert nicht mehr',
   'The price rule for this price does not exist anymore' => 'Die Preisregel für diesen Preis existiert nicht mehr',
   'The price rule has been created.' => 'Die Preisregel wurde angelegt.',
@@ -2810,6 +3711,7 @@ $self->{texts} = {
   'The price rule has been saved.' => 'Die Preisregel wurde gespeichert.',
   'The price rule is not a rule for discounts' => 'Die Preisregel ist keine Regel für Rabatte',
   'The price rule is not a rule for prices' => 'Die Preisregel ist keine Regel für Preise',
+  'The pricegroup is being used by customers.' => 'Die Preisgruppe wird von Kunden verwendet.',
   'The printer could not be deleted.' => 'Der Drucker konnte nicht gelöscht werden.',
   'The printer has been created.' => 'Der Drucker wurde angelegt.',
   'The printer has been deleted.' => 'Der Drucker wurde entfernt.',
@@ -2823,71 +3725,79 @@ $self->{texts} = {
   'The project link has been updated.' => 'Die Projektverknüpfung wurde aktualisiert.',
   'The project number is already in use.' => 'Die Projektnummer wird bereits verwendet.',
   'The project number is missing.' => 'Die Projektnummer fehlt.',
-  'The project status has been created.' => 'Der Projektstatus wurde angelegt.',
-  'The project status has been deleted.' => 'Der Projektstatus wurde gelöscht.',
-  'The project status has been saved.' => 'Der Projektstatus wurde gespeichert.',
-  'The project status is in use and cannot be deleted.' => 'Der Projektstatus wird verwendet und kann nicht gelöscht werden.',
-  'The project type has been created.' => 'Der Projekttyp wurde angelegt.',
-  'The project type has been deleted.' => 'Der Projekttyp wurde gelöscht.',
-  'The project type has been saved.' => 'Der Projekttyp wurde gespeichert.',
-  'The project type is in use and cannot be deleted.' => 'Der Projekttyp wird verwendet und kann nicht gelöscht werden.',
+  'The query did not return any data.' => 'Die Abfrage lieferte keine Daten',
+  'The quotation has been deleted' => 'Das Angebot wurde gelöscht',
+  'The quotation has been saved' => 'Das Angebot wurde gespeichert',
+  'The receivables chart isn\'t a valid chart.' => 'Das Forderungskonto ist kein gültiges Konto',
   'The recipient, subject or body is missing.' => 'Der Empfäger, der Betreff oder der Text ist leer.',
-  'The required information consists of the IBAN and the BIC.' => 'Die benötigten Informationen bestehen aus der IBAN und der BIC.',
-  'The required information consists of the IBAN, the BIC, the mandator ID and the mandate\'s date of signature.' => 'Die benötigten Informationen bestehen aus IBAN, BIC, Mandanten-ID und dem Unterschriftsdatum des Mandates.',
+  'The record template \'#1\' has been loaded.' => 'Die Belegvorlage »#1« wurde geladen.',
+  'The record template \'#1\' has been saved.' => 'Die Belegvorlage »#1« wurde gespeichert.',
+  'The report doesn\'t contain entries.' => 'Der Bericht enthält keine Einträge.',
+  'The required information consists of the IBAN and the BIC.' => 'Die benötigten Informationen bestehen aus der IBAN und der BIC. Zusätzlich wird die SEPA-Kreditoren-Identifikation aus der Mandantenkonfiguration benötigt.',
+  'The required information consists of the IBAN, the BIC, the mandator ID and the mandate\'s date of signature.' => 'Die benötigten Informationen bestehen aus IBAN, BIC, Mandanten-ID und dem Unterschriftsdatum des Mandates. Zusätzlich wird die SEPA-Kreditoren-Identifikation aus der Mandantenkonfiguration benötigt.',
   'The requirement spec has been deleted.' => 'Das Pflichtenheft wurde gelöscht.',
   'The requirement spec has been reverted to version #1.' => 'Das Pflichtenheft wurde auf Version #1 zurückgesetzt.',
   'The requirement spec has been saved.' => 'Das Pflichtenheft wurde gespeichert.',
   'The requirement spec is in use and cannot be deleted.' => 'Das Pflichtenheft wird verwendet und kann nicht gelöscht werden.',
-  'The requirement spec status has been created.' => 'Der Pflichtenheftstatus wurde angelegt.',
-  'The requirement spec status has been deleted.' => 'Der Pflichtenheftstatus wurde gelöscht.',
-  'The requirement spec status has been saved.' => 'Der Pflichtenheftstatus wurde gespeichert.',
-  'The requirement spec status is in use and cannot be deleted.' => 'Der Pflichtenheftstatus wird verwendet und kann nicht gelöscht werden.',
   'The requirement spec template has been saved.' => 'Die Pflichtenheftvorlage wurde gespeichert.',
-  'The requirement spec type has been created.' => 'Der Pflichtenhefttyp wurde angelegt.',
-  'The requirement spec type has been deleted.' => 'Der Pflichtenhefttyp wurde gelöscht.',
-  'The requirement spec type has been saved.' => 'Der Pflichtenhefttyp wurde gespeichert.',
-  'The requirement spec type is in use and cannot be deleted.' => 'Der Pflichtenhefttyp wird verwendet und kann nicht gelöscht werden.',
-  'The risk level has been created.' => 'Der Risikograd wurde angelegt.',
-  'The risk level has been deleted.' => 'Der Risikograd wurde gelöscht.',
-  'The risk level has been saved.' => 'Der Risikograd wurde gespeichert.',
-  'The risk level is in use and cannot be deleted.' => 'Der Risikograd wird verwendet und kann nicht gelöscht werden.',
+  'The rfq has been deleted'    => 'Die Anfrage wurde gelöscht',
+  'The rfq has been saved'      => 'Die Anfrage wurde gespeichert',
   'The second reason is that kivitendo allowed the user to enter the tax amount manually regardless of the taxkey used.' => 'Der zweite Grund war, dass kivitendo zuließ, dass die Benutzer beliebige, von den tatsächlichen Steuerschlüsseln unabhängige Steuerbeträge eintrugen.',
-  'The second way is to use Perl\'s CPAN module and let it download and install the module for you.' => 'Die zweite Variante besteht darin, Perls CPAN-Modul zu benutzen und es das Modul f&uuml;r Sie installieren zu lassen.',
+  'The second way is to use Perl\'s CPAN module and let it download and install the module for you.' => 'Die zweite Variante besteht darin, Perls CPAN-Modul zu benutzen und es das Modul für Sie installieren zu lassen.',
   'The selected bank account does not exist anymore.' => 'Das ausgewählte Bankkonto existiert nicht mehr.',
-  'The selected bin does not exist.' => 'Der ausgew&auml;hlte Lagerplatz existiert nicht.',
+  'The selected bin does not exist.' => 'Der ausgewählte Lagerplatz existiert nicht.',
   'The selected currency'       => 'Die ausgewählte Währung',
-  'The selected database is still configured for client "#1". If you delete the database that client will stop working until you re-configure it. Do you still want to delete the database?' => 'Die auswählte Datenbank ist noch für Mandant "#1" konfiguriert. Wenn Sie die Datenbank löschen, wird der Mandanten nicht mehr funktionieren, bis er anders konfiguriert wurde. Wollen Sie die Datenbank trotzdem löschen?',
+  'The selected database is still configured for client "#1". If you delete the database that client will stop working until you re-configure it. Do you still want to delete the database?' => 'Die auswählte Datenbank ist noch für Mandant "#1" konfiguriert. Wenn Sie die Datenbank löschen, wird der Mandanten nicht mehr funktionieren, bis er anders konfiguriert wurde. Möchten Sie die Datenbank trotzdem löschen?',
   'The selected exports have been closed.' => 'Die ausgewählten Exporte wurden abgeschlossen.',
-  'The selected warehouse does not exist.' => 'Das ausgew&auml;hlte Lager existiert nicht.',
+  'The selected exports have been undone.' => 'Die ausgwählten Exporte wurden rückgängig gemacht.',
+  'The selected warehouse does not exist.' => 'Das ausgewählte Lager existiert nicht.',
   'The selected warehouse is empty, or no stocked items where found that match the filter settings.' => 'Das ausgewählte Lager ist leer, oder in ihm wurden keine zu den Sucheinstellungen passenden eingelagerten Artikel gefunden.',
   'The session has expired. Please log in again.' => 'Die Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.',
   'The session is invalid or has expired.' => 'Sie sind von kivitendo abgemeldet.',
   'The settings were saved, but the password was not changed.' => 'Die Einstellungen wurden gespeichert, aber das Passwort wurde nicht geändert.',
-  'The source warehouse does not contain any bins.' => 'Das Quelllager enth&auml;lt keine Lagerpl&auml;tze.',
+  'The shop has been created.'  => 'Shop hinzugefügt',
+  'The shop has been deleted.'  => 'Shop gelöscht',
+  'The shop has been saved.'    => 'Shop gespeichert',
+  'The shop is in use and cannot be deleted.' => 'Der Webshop wird benutzt und kann nicht gelöscht werden',
+  'The shop part has been created.' => 'Shopartikel angelegt',
+  'The shop part has been saved.' => 'Shopartikel gespeichert',
+  'The shop part wasn\'t updated.' => 'Shopartikel wurde nicht aktualisiert!',
+  'The shop part wasn\'t updated. #1' => 'Der Artikel ist nicht aktualisiert: #1',
+  'The source warehouse does not contain any bins.' => 'Das Quelllager enthält keine Lagerplätze.',
   'The start date is missing.'  => 'Das Startdatum fehlt.',
+  'The stock will be changed to your target quantity.' => 'Der Lagerbestand wird auf Ihre gezählte Zielmenge geändert.',
   'The subject is missing.'     => 'Der Betreff fehlt.',
   'The tables for user management and authentication do not exist. They will be created in the next step in the following database:' => 'Die Tabellen zum Speichern der Benutzerdaten und zur Benutzerauthentifizierung wurden nicht gefunden. Sie werden in der folgenden Datenbank angelegt:',
   'The tabulator character'     => 'Das Tabulator-Symbol',
+  'The target quantity of #1 differs more than the threshold quantity of #2.' => 'Die Abweichung der Zielmenge #1 ist größer als der Mengenschwellwert #2.',
   'The task server does not appear to be running.' => 'Der Task-Server scheint nicht zu laufen.',
   'The task server is already running.' => 'Der Task-Server läuft bereits.',
   'The task server is not running at the moment but needed for this module' => 'Der Taskserver wird für dieses Modul benötigt, läuft aber im Moment nicht.',
   'The task server is not running.' => 'Der Task-Server läuft nicht.',
+  'The task server is required for this module but not enabled for the current client. Please enable it for the client "#1" in the administration section.' => 'Der Task-Server wird für dieses Modul benötigt, ist aber für den aktuellen Mandanten deaktiviert. Bitte aktivieren Sie ihn für den Mandanten »#1« im Administrationsbereich.',
   'The task server was started successfully.' => 'Der Task-Server wurde erfolgreich gestartet.',
   'The task server was stopped successfully.' => 'Der Task-Server wurde erfolgreich beendet.',
   'The tax zone has been deleted.' => 'Die Steuerzone wurde gelöscht.',
   'The tax zone is in use and cannot be deleted.' => 'Die Steuerzone wird benutzt und kann nicht gelöscht werden',
   'The taxzone has been created.' => 'Die Steuerzone wurde erstellt.',
   'The taxzone has been saved.' => 'Die Steuerzone wurde gespeichert.',
+  'The test import has not been executed yet.' => 'Der Testimport wurde noch nicht durchgeführt.',
+  'The third reason is that wrong (taxkey) settings for the credit / debit CSV-import were used.' => 'Der dritte Grund ist, dass fehlerhafte Einstellungen (Steuerschlüssel) beim Kreditoren / Debitoren CSV-Import benutzt worden sind.',
   'The third way is to download the module from the above mentioned URL and to install the module manually following the installations instructions contained in the source archive.' => 'Die dritte Variante besteht darin, das Paket von der oben genannten URL herunterzuladen und es manuell zu installieren. Beachten Sie dabei die im Paket enthaltenen Installationsanweisungen.',
   'The three columns "make_X", "model_X" and "lastcost_X" with the same number "X" are used to import vendor part numbers and vendor prices.' => 'Die drei Spalten "make_X", "model_X" und "lastcost_X" mit derselben Nummer "X" werden zum Import von Lieferantenartikelnummern und -preisen genutzt.',
   'The title is missing.'       => 'Der Titel fehlt.',
   'The transaction is shown below in its current state.' => 'Nachfolgend wird angezeigt, wie die Buchung momentan aussieht.',
+  'The transfer has been canceled by the user.' => 'Der Vorgang wurde durch den Benutzer abgebrochen.',
+  'The transport cost article \'#1\' is missing. Do you want to continue anyway?' => 'Der Transportkostenartikel »#1« fehlt. Möchten Sie trotzdem fortfahren?',
   'The type is missing.'        => 'Der Typ fehlt.',
-  'The unit has been saved.'    => 'Die Einheit wurde gespeichert.',
-  'The unit in row %d has been deleted in the meantime.' => 'Die Einheit in Zeile %d ist in der Zwischentzeit gel&ouml;scht worden.',
-  'The unit in row %d has been used in the meantime and cannot be changed anymore.' => 'Die Einheit in Zeile %d wurde in der Zwischenzeit benutzt und kann nicht mehr ge&auml;ndert werden.',
+  'The unit has been added.'    => 'Die Einheit wurde erfasst.',
+  'The unit in row %d has been deleted in the meantime.' => 'Die Einheit in Zeile %d ist in der Zwischentzeit gelöscht worden.',
+  'The unit in row %d has been used in the meantime and cannot be changed anymore.' => 'Die Einheit in Zeile %d wurde in der Zwischenzeit benutzt und kann nicht mehr geändert werden.',
+  'The unit is missing.'        => 'Die Einheit fehlt.',
   'The units have been saved.'  => 'Die Einheiten wurden gespeichert.',
+  'The uploaded filename still exists.<br>If you not modify the name this is a new version of the file' => 'Der Dateiname existiert bereits.<br>Wenn Sie den Namen nicht ändern gibt dies eine neue Version der Datei',
   'The user can chose which client to connect to during login.' => 'Bei der Anmeldung kann der Benutzer auswählen, welchen Mandanten er benutzen möchte.',
+  'The user cannot be deleted as it is used in the following clients: #1' => 'Die BenutzerIn kann nicht gelöscht werden, da sie für die folgenden Mandanten benötigt wird: #1',
   'The user could not be deleted.' => 'Der Benutzer konnte nicht gelöscht werden.',
   'The user group could not be deleted.' => 'Die Benutzergurppe konnte nicht gelöscht werden.',
   'The user group has been created.' => 'Die Benutzergruppe wurde erstellt.',
@@ -2896,11 +3806,13 @@ $self->{texts} = {
   'The user has been created.'  => 'Der Benutzer wurde angelegt.',
   'The user has been deleted.'  => 'Der Benutzer wurde gelöscht.',
   'The user has been saved.'    => 'Der Benutzer wurde gespeichert.',
+  'The value \'#1\' is not a valid IBAN.' => 'Der Wert \'#1\' ist keine gültige IBAN.',
+  'The value \'our routing id at customer\' must be set in the customer\'s master data for profile #1.' => 'Der Wert »unsere Leitweg-ID beim Kunden« muss in den Kundenstammdaten gesetzt sein für Profil #1.',
   'The variable name must only consist of letters, numbers and underscores. It must begin with a letter. Example: send_christmas_present' => 'Der Variablenname darf nur aus Zeichen (keine Umlaute), Ziffern und Unterstrichen bestehen. Er muss mit einem Buchstaben beginnen. Beispiel: weihnachtsgruss_verschicken',
   'The vendor name is missing.' => 'Der Liefeantenname fehlt.',
   'The version number is missing.' => 'Die Versionsnummer fehlt.',
-  'The warehouse could not be deleted because it has already been used.' => 'Das Lager konnte nicht gel&ouml;scht werden, da es bereits in Benutzung war.',
-  'The warehouse does not contain any bins.' => 'Das Lager enth&auml;lt keine Lagerpl&auml;tze.',
+  'The warehouse could not be deleted because it has already been used.' => 'Das Lager konnte nicht gelöscht werden, da es bereits in Benutzung war.',
+  'The warehouse does not contain any bins.' => 'Das Lager enthält keine Lagerplätze.',
   'The warehouse or the bin is missing.' => 'Das Lager oder der Lagerplatz fehlen.',
   'The wrong taxkeys for AP and AR transactions have been fixed.' => 'Die Probleme mit falschen Steuerschlüssel bei Kreditoren- und Debitorenbuchungen wurden behoben.',
   'The wrong taxkeys for inventory transactions for sales and purchase invoices have been fixed.' => 'Die falschen Steuerschlüssel für Warenbestandsbuchungen bei Einkaufs- und Verkaufsrechnungen wurden behoben.',
@@ -2913,19 +3825,23 @@ $self->{texts} = {
   'There are Bins defined in your Inventory.' => 'Unter Stammdaten/Waren sind Lagerplätze definiert.',
   'There are Bins defined in your master data.' => 'Unter Stammdaten/Waren sind Lagerplätze defininert',
   'There are bookings to the account 3803 after 01.01.2007. If you didn\'t change this account manually to 19% the bookings are probably incorrect.' => 'Das Konto 3803 wurde nach dem 01.01.2007 bebucht. Falls Sie dieses Konto nicht manuell auf 19% gestellt haben sind die Buchungen wahrscheinlich mit falscher Umsatzsteuer gebucht worden.',
+  'There are currently no delivery orders, or none matches your filter conditions.' => 'kein Lieferschein vorhanden',
   'There are currently no open invoices, or none matches your filter conditions.' => 'Es gibt momentan keine offenen Rechnungen, oder keine erfüllt die Filterkriterien.',
   'There are currently no open sales delivery orders.' => 'Es gibt zur Zeit keine offenen Verkaufslieferscheine.',
   'There are double partnumbers in your database.' => 'In ihrer Datenbank befinden sich mehrfach vergebene Artikelnummern.',
+  'There are duplicate assortment items' => 'Es kommen doppelte Artikel im Sortiment vor',
+  'There are duplicate parts at positions' => 'Es gibt doppelte Artikel bei den Positionen',
   'There are entries in tax where taxkey is NULL.' => 'In der Datenbank sind Steuern ohne Steuerschlüssel vorhanden (in der Tabelle tax Spalte taxkey).',
   'There are invalid taxnumbers in use.' => 'Es werden ungültige Steuerautomatik-Konten benutzt.',
   'There are invalid transactions in your database.' => 'Sie haben ungültige Buchungen in Ihrer Datenbank.',
-  'There are invoices which could not be paid by bank transaction #1 (Account number: #2, bank code: #3)!' => 'Einige Rechnungen konnten nicht durch die Bankbewegung #1 (Kontonummer: #2, Bankleitzahl: #3) bezahlt werden!',
+  'There are no documents in the WebDAV directory at the moment.' => 'Es befinden sich im WebDAV-Verzeichnis momentan keine Dokumente.',
   'There are no entries in the background job history.' => 'Es gibt keine Einträge im Hintergrund-Job-Verlauf.',
   'There are no entries that match the filter.' => 'Es gibt keine Einträge, auf die der Filter zutrifft.',
   'There are no items in stock.' => 'Dieser Artikel ist nicht eingelagert.',
-  'There are no items on your TODO list at the moment.' => 'Ihre Aufgabenliste enth&auml;lt momentan keine Eintr&auml;ge.',
+  'There are no items on your TODO list at the moment.' => 'Ihre Aufgabenliste enthält momentan keine Einträge.',
+  'There are no record templates yet.' => 'Es gibt noch keine Belegvorlagen.',
   'There are several options you can handle this problem, please select one:' => 'Bitte wählen Sie eine der folgenden Optionen, um mit dem Problem umzugehen:',
-  'There are still transfers not matching the qty of the delivery order. Stock operations can not be changed later. Do you really want to proceed?' => 'Einige der Lagerbewegungen sind nicht vollständig und Lagerbewegungen können nachträglich nicht mehr verändert werden. Wollen Sie wirklich fortfahren?',
+  'There are still transfers not matching the qty of the delivery order. Stock operations can not be changed later. Do you really want to proceed?' => 'Einige der Lagerbewegungen sind nicht vollständig und Lagerbewegungen können nachträglich nicht mehr verändert werden. Möchten Sie wirklich fortfahren?',
   'There are undefined currencies in your system.' => 'In Ihrer Datenbank wurden Währungen benutzt, die nicht ordnungsgemäß in den Währungen eingetragen wurden.',
   'There are usually three ways to install Perl modules.' => 'Es gibt normalerweise drei Arten, ein Perlmodul zu installieren.',
   'There is a better discount available' => 'Es is ein besserer Rabatt verfügbar',
@@ -2935,21 +3851,31 @@ $self->{texts} = {
   'There is at least one sales or purchase invoice for which kivitendo recorded an inventory transaction with taxkeys even though no tax was recorded.' => 'Es gibt mindestens eine Verkaufs- oder Einkaufsrechnung, für die kivitendo eine Warenbestandsbuchung ohne dazugehörige Steuerbuchung durchgeführt hat.',
   'There is at least one transaction for which the user has chosen a logically wrong taxkey.' => 'Es gibt mindestens eine Buchung, bei der ein logisch nicht passender Steuerschlüssel ausgewählt wurde.',
   'There is no connected chart.' => 'Es fehlt ein verknüpftes Buchungskonto.',
-  'There is not enough available of \'#1\' at warehouse \'#2\', bin \'#3\', #4, #5, for the transfer of #6.' => 'Von \'#1\' ist in Lager \'#2\', Lagerplatz \'#3\', #4, #5, nicht gen&uuml;gend eingelagert, um insgesamt #6 auszulagern.',
-  'There is not enough available of \'#1\' at warehouse \'#2\', bin \'#3\', #4, for the transfer of #5.' => 'Von \'#1\' ist in Lager \'#2\', Lagerplatz \'#3\', #4 nicht gen&uuml;gend eingelagert, um insgesamt #5 auszulagern.',
+  'There is not enough available of \'#1\' at warehouse \'#2\', bin \'#3\', #4, #5, for the transfer of #6.' => 'Von \'#1\' ist in Lager \'#2\', Lagerplatz \'#3\', #4, #5, nicht genügend eingelagert, um insgesamt #6 auszulagern.',
+  'There is not enough available of \'#1\' at warehouse \'#2\', bin \'#3\', #4, for the transfer of #5.' => 'Von \'#1\' ist in Lager \'#2\', Lagerplatz \'#3\', #4 nicht genügend eingelagert, um insgesamt #5 auszulagern.',
   'There is not enough left of \'#1\' in bin \'#2\' for the removal of #3.' => 'In Lagerplatz \'#2\' ist nicht genug von \'#1\' vorhanden, um #3 zu entnehmen.',
+  'There is nothing here yet (csv_import)' => 'Noch keine Zuordnungen',
   'There is one or more sections for which no part has been assigned yet; therefore creating the new record is not possible yet.' => 'Es gibt einen oder mehrere Abschnitte ohne Artikelzuweisung; daher kann der neue Beleg noch nicht erstellt werden.',
+  'There was an error deleting the draft' => 'Beim Löschen des Entwurfs ist ein Fehler aufgetretetn',
   'There was an error executing the background job.' => 'Bei der Ausführung des Hintergrund-Jobs trat ein Fehler auf.',
   'There was an error parsing the csv file: #1 in line #2.' => 'Es gab einen Fehler beim Parsen der CSV Datei: "#1" in der Zeile "#2"',
+  'There was an error saving the draft' => 'Beim Speichern des Entwurfs ist ein Fehler aufgetretetn',
+  'There was an error saving the letter' => 'Ein Fehler ist aufgetreten. Der Brief konnte nicht gespeichert werden.',
+  'There was an error saving the letter draft' => 'Ein Fehler ist aufgetreten. Der Briefentwurf konnte nicht gespeichert werden.',
   'There you can let kivitendo create the basic tables for you, even in an already existing database.' => 'Dort können Sie kivitendo diese grundlegenden Tabellen erstellen lassen, selbst in einer bereits existierenden Datenbank.',
   'Therefore several settings that had to be made for each user in the past have been consolidated into the client configuration.' => 'Dazu wurden gewisse Einstellungen, die vorher bei jedem Benutzer vorgenommen werden mussten, in die Konfiguration eines Mandanten verschoben.',
   'Therefore the definition of "kg" with the base unit "g" and a factor of 1000 is valid while defining "g" with a base unit of "kg" and a factor of "0.001" is not.' => 'So ist die Definition von "kg" mit der Basiseinheit "g" und dem Faktor 1000 zulässig, die Definition von "g" mit der Basiseinheit "kg" und dem Faktor "0,001" hingegen nicht.',
+  'These mappings can be used to map heading from non standard csv files to known columns. These will also be saved in profiles, so you can save profiles for every source of formats.' => 'Mit diesen Spaltenzuordnungen können die Kopfzeilen aus beliebigen CSV-Dateien verarbeitet werden. Die Zuordnungen werden im Profil mitgespeichert, so dass regelmäßige Quellen behandelt werden können.',
   'These wrong entries cannot be fixed automatically.' => 'Diese Einträge können nicht automatisch bereinigt werden.',
   'They will be updated, new ones for additional parts without a line item added automatically.' => 'Diese Positionen werden automatisch aktualisiert bzw. ergänzt, wenn es noch keine Position zu einem zusätzlichen Artikel gibt.',
   'This Price Rule is no longer valid' => 'Diese Preisregel ist nicht mehr gültig',
+  'This also enables displaying a column with the customer partnumber (new order controller).' => 'Hiermit wird auch die Anzeige der Kunden-Art.-Nr. eingeschaltet (neuer Auftrags-Controller).',
+  'This also enables displaying a column with the vendor partnumber (model) (new order controller).' => 'Hiermit wird auch die Anzeige der Lieferanten-Art.-Nr. eingeschaltet (neuer Auftrags-Controller).',
   'This can be done with the following query:' => 'Dies kann mit der folgenden Datenbankabfrage erreicht werden:',
   'This could have happened for two reasons:' => 'Dies kann aus zwei Gründen geschehen sein:',
+  'This customer has already been added.' => 'Für diesen Kunden ist bereits ein Preis hinzugefügt.',
   'This customer number is already in use.' => 'Diese Kundennummer wird bereits verwendet.',
+  'This customer wants a postal invoices.' => 'Dieser Kunde möchte Rechnungen nur per Post.',
   'This discount has since gone down' => 'Dieser Rabatt ist mittlerweile niedriger',
   'This discount has since gone up' => 'Dieser Rabatt ist mittlerweile höher',
   'This discount is only valid for business #1' => 'Dieser Rabatt ist nur für Kunden-/Lieferantentyp #1 gültig',
@@ -2958,11 +3884,24 @@ $self->{texts} = {
   'This discount is only valid in purchase documents' => 'Dieser Rabatt ist nur in Einkaufsdokumenten gültig',
   'This discount is only valid in records with customer or vendor' => 'Dieser Rabatt ist nur in Dokumenten mit Kunde oder Lieferant gültig',
   'This discount is only valid in sales documents' => 'Dieser Rabatt ist nur in Verkaufsdokumenten gültig',
+  'This entry is using date and duration. This information will be overwritten on saving.' => 'Dieser Eintrag verwendet Datum und Dauer. Diese Information wird beim Speichern überschrieben.',
+  'This entry is using start and end time. This information will be overwritten on saving.' => 'Dieser Eintrag verwendet Start- und End-Zeit. Diese Information wird beim Speichern überschrieben.',
+  'This export will include all records in the given time range and all supplicant information from checked entities. You will receive a single zip file. Please extract this file onto the data medium requested by your auditor.' => 'Dieser Export umfasst alle Belege im gewählten Zeitrahmen und die dazugehörgen Informationen aus den gewählten Blöcken. Sie erhalten eine einzelne Zip-Datei. Bitte entpacken Sie diese auf das Medium das Ihr Steuerprüfer wünscht.',
   'This feature especially prevents mistakes by mixing up prior tax and sales tax.' => 'Dieses Feature vermeidet insbesondere Verwechslungen von Umsatz- und Vorsteuer.',
+  'This field must not be empty.' => 'Dieses Feld darf nicht leer sein.',
   'This function requires the presence of articles with a time-based unit such as "h" or "min".' => 'Für diese Funktion mussen Artikel mit einer Zeit-basierten Einheit wie "Std" oder "min" existieren.',
+  'This general ledger transaction has not been posted yet.' => 'Die Dialogbuchung wurde noch nicht gebucht.',
   'This group is valid for the following clients' => 'Diese Gruppe ist für die folgenden Mandanten gültig',
   'This has been changed in this version, therefore please change the "old" bins to some real warehouse bins.' => 'Das wurde in dieser Version umgestellt, bitte ändern Sie die Freitext-Lagerplätze auf vorhandene Lagerplätze.',
   'This has been changed in this version.' => 'Ab dieser Version ist dies nicht mehr so.',
+  'This invoice has a further invoice for advanced payment.' => 'Diese Rechnung hat eine weitere Anzahlungsrechnung.',
+  'This invoice has already a final invoice.' => 'Diese Rechnung hat schon eine Schlussrechnung.',
+  'This invoice has already a further invoice for advanced payment.' => 'Diese Rechnung hat schon eine weitere Anzahlungsrechnung.',
+  'This invoice has already been posted.' => 'Die Rechnung wurde bereits gebucht.',
+  'This invoice has been canceled already.' => 'Die Rechnung wurde bereits storniert.',
+  'This invoice has been linked with a sepa export, undo this first.' => 'Diese Rechnung ist mit einem SEPA-Export verknüpft. Bitte diese Verknüpfung zuerst aufheben.',
+  'This invoice has not been posted yet.' => 'Die Rechnung wurde noch nicht gebucht.',
+  'This invoice was added from an order. See there.' => 'Diese Rechnung wurde aus einem Auftrag erstellt. Siehe dort.',
   'This invoice\'s dunning level: #1' => 'Mahnstufe dieser Rechnung: #1',
   'This is a very critical problem.' => 'Dieses Problem ist sehr schwerwiegend.',
   'This is the client to be selected by default on the login screen.' => 'Dies ist derjenige Mandant, der im Loginbildschirm standardmäßig ausgewählt sein wird.',
@@ -2972,53 +3911,85 @@ $self->{texts} = {
   'This makemodel price does not exist anymore' => 'Dieser Lieferantenpreis existiert nicht mehr',
   'This means that the user has created an AP transaction and chosen a taxkey for sales taxes, or that he has created an AR transaction and chosen a taxkey for input taxes.' => 'Das bedeutet, dass ein Benutzer eine Kreditorenbuchung angelegt und in ihr einen Umsatzsteuer-Steuerschlüssel verwendet oder eine Debitorenbuchung mit Vorsteuer-Steuerschlüssel angelegt hat.',
   'This module can help you identify and correct such entries by analyzing the general ledger and presenting you likely solutions but also allowing you to fix problems yourself.' => 'Dieses Modul kann Ihnen helfen, problematische Einträge im Hauptbuch zu identifizieren und teilweise zu beheben. Dabei werden je nach Problem mögliche Lösungen aufgezeigt, wobei Sie die entscheiden können, welche Probleme automatisch gelöst werden sollen.',
+  'This object has already been used.' => 'Dieses Objekt wird bereits verwendet.',
+  'This object has not been saved yet.' => 'Das Objekt wurde noch nicht gespeichert.',
+  'This object is used in price rules.' => 'Dieses Objekt wird in Preisregeln verwendet.',
   'This option controls the inventory system.' => 'Dieser Parameter legt die Warenbuchungsmethode fest.',
   'This option controls the method used for determining the startdate for the balance report.' => 'Diese Option bestimmt, wie das Startdatum für den Bilanzbericht ermittelt wird',
   'This option controls the method used for profit determination.' => 'Dieser Parameter legt die Berechnungsmethode für die Gewinnermittlung fest.',
   'This option controls the posting and calculation behavior for the accounting method.' => 'Dieser Parameter steuert die Buchungs- und Berechnungsmethoden für die Versteuerungsart.',
-  'This partnumber is not unique. You should change it.' => 'Diese Artikelnummer ist nicht eindeutig. Bitte wählen Sie eine andere.',
+  'This order has already a final invoice.' => 'Dieser Auftrag hat schon eine Schlussrechnung.',
+  'This part has already been added.' => 'Dieser Artikel wurde schon hinzugefügt',
+  'This part was already counted for this bin:' => 'Dieser Artikel wurde für diesen Lagerplatz bereits erfasst:',
   'This price has since gone down' => 'Dieser Preis ist mittlerweile niedriger',
   'This price has since gone up' => 'Dieser Preis ist mittlerweile höher',
+  'This record containts obsolete items at position #1' => 'Dieser Beleg enthält ungültige Artikel an Position #1',
+  'This record has already been closed.' => 'Dieser Beleg wurde bereits geschlossen.',
+  'This record has already been delivered.' => 'Dieser Beleg wurde bereits geliefert.',
+  'This record has not been saved yet.' => 'Der Beleg wurde noch nicht gespeichert.',
+  'This record has not been stocked in. Proceed?' => 'Dieser Beleg wurde noch nicht eingelagert. Fortfahren?',
+  'This record has not been stocked out. Proceed?' => 'Dieser Beleg wurde noch nicht ausgelagert. Fortfahren?',
   'This requirement spec is currently linked to the following project:' => 'Dieses Pflichtenheft ist mit dem folgenden Projekt verknüpft:',
   'This requirement spec is currently not linked to a project.' => 'Dieses Pflichtenheft ist noch nicht mit einem Projekt verknüpft.',
   'This requires you to manually correct entries for which an automatic conversion failed and to check those for which it succeeded.' => 'Dies erfordert, dass Sie diejenigen Einträge manuell korrigieren, für die die automatische Umstellung fehlschlug, sowie dass Sie diejenigen überprüfen, für die die Umstellung erfolgreich war.',
   'This resets the dunning process for the selected invoices. Posted dunning invoices will not be changed!' => 'Hiermit wird der Mahnprozess für die ausgewählten Rechnungen zurückgesetzt. Bereits gebuchte Rechnungen über Mahngebühren werden nicht geändert!',
-  'This sales order has an active configuration for periodic invoices. If you save then all subsequently created invoices will contain those changes as well, but not those that have already been created. Do you want to continue?' => 'Dieser Auftrag besitzt eine aktive Konfiguration für wiederkehrende Rechnungen. Wenn Sie jetzt speichern, so werden alle zukünftig hieraus erzeugten Rechnungen die Änderungen enthalten, nicht aber die bereits erzeugten Rechnungen. Wollen Sie speichern?',
+  'This sales order has an active configuration for periodic invoices. If you save then all subsequently created invoices will contain those changes as well, but not those that have already been created. Do you want to continue?' => 'Dieser Auftrag besitzt eine aktive Konfiguration für wiederkehrende Rechnungen. Wenn Sie jetzt speichern, so werden alle zukünftig hieraus erzeugten Rechnungen die Änderungen enthalten, nicht aber die bereits erzeugten Rechnungen. Möchten Sie speichern?',
   'This status output will be refreshed every five seconds.' => 'Diese Statusausgabe wird alle fünf Sekunden aktualisiert.',
   'This transaction has to be split into several transactions manually.' => 'Diese Buchung muss manuell in mehrere Buchungen aufgeteilt werden.',
-  'This update will change the nature the onhand of goods is tracked.' => 'Dieses update &auml;ndert die Art und Weise wie Lagermengen gez&auml;lt werden.',
+  'This transaction is linked with a AP transaction. Please undo and redo the AP transaction booking if needed.' => 'Diese Buchung ist mit einer Kreditorenbuchung verknüpft. Bitte Löschen oder Ändern Sie die Kreditorenbuchung nötigenfalls.',
+  'This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.' => 'Ein oder mehrere Zahlungen des Belegs sind über das Verbuchen von Kontoauszüge erstellt worden, falls notwendig kann eine Neuverbuchung über Zahlungsverkehr -> Bericht Bankbewegung möglich gemacht werden.',
+  'This transaction is linked with a gl transaction. Please delete the ap transaction booking if needed.' => 'Diese Buchung ist mit einer Dialogbuchung verknüpft. Bitte Löschen oder Ändern Sie diese Kreditorenbuchung nötigenfalls.',
+  'This transaction is reconciled with a bank transaction. Please undo the reconciliation if needed.' => 'Diese Buchung ist mit einer Bankbewegung abgeglichen. Falls die Buchung geändert werden soll, muss der Abgleich mit der Bankbewegung zuerst aufgelöst werden.',
+  'This update will change the nature the onhand of goods is tracked.' => 'Dieses update ändert die Art und Weise wie Lagermengen gezält werden.',
   'This user is a member in the following groups' => 'Dieser Benutzer ist Mitglied in den folgenden Gruppen',
   'This user will have access to the following clients' => 'Dieser Benutzer wird Zugriff auf die folgenden Mandanten haben',
+  'This vendor has already a booking with this invoice number, do you really want to add the same invoice number again?' => 'Es gibt für diesen Lieferant schon einen Beleg mit dieser Rechnungsnummer. Möchten Sie wirklich eine weitere Buchung mit derselben Rechnungsnummer hinzufügen?',
+  'This vendor has already been added.' => 'Der Lieferant wurde bereits hinzugefügt.',
   'This vendor number is already in use.' => 'Diese Lieferantennummer wird bereits verwendet.',
+  'This will also remove this pricegroup for all customers.' => 'Damit werden auch alle verknüpften Preisgruppen im Kundenstamm gelöscht!',
   'This will apply a 3% reduction to the master data price before entering it into the record item.' => 'Diese Zeile zieht vom Stammdatenpreis 3% ab, und schlägt den resultierenden Preis vor.',
   'This will be treated as a discount in percent points.' => 'Diese Option schlägt den Wert in Prozentpunkten als Rabatt vor.',
   'This will happen before the price is offered, and the reduction will not be printed in documents.' => 'Das passiert, bevor der Preis vorgeschlagen wird, und der Abschlag wird nicht in Belegen ausgewiesen.',
   'This will reduce the appropriate Master Data price by this in percent points.' => 'Diese Option reduziert den zugehörigen Stammdatenpreis um den angegebenen Wert in Prozentpunkten.',
+  'This will remove the delivery order from showing as open even if contents are not delivered. Proceed?' => 'Dies wird den Lieferschein nicht mehr als offen anzeigen, auch wenn der Inhalt noch nicht geliefert wurde. Fortfahren?',
+  'This will remove the invoice from showing as unpaid even if the unpaid amount does not match the amount. Proceed?' => 'Dies wird die Rechnung nicht mehr als offen anzeigen, auch wenn der unbezahlte Betrag nicht dem Rechnungsbetrag entspricht. Fortfahren?',
   'This will set an exact price.' => 'Diese Option setzt einen festen Preis.',
   'Three Options:'              => 'Drei Optionen:',
+  'Threshold for warning on quantity difference' => 'Schwellenwert für Warnung bei Mengenabweichung',
+  'Thu'                         => 'Do',
+  'Thursday'                    => 'Donnerstag',
+  'Time'                        => 'Zeit',
   'Time Format'                 => 'Uhrzeitformat',
-  'Time and cost estimate'      => 'Zeit- und Kostenschätzung',
+  'Time Recording'              => 'Zeiterfassung',
+  'Time Recording Articles'     => 'Artikel für Zeiterfassung',
+  'Time Recordings'             => 'Zeiterfassung',
+  'Time and price estimate'     => 'Zeit- und Preisschätzung',
   'Time estimate'               => 'Zeitschätzung',
   'Time period for the analysis:' => 'Analysezeitraum:',
   'Time/cost estimate actions'  => 'Aktionen für Kosten-/Zeitabschätzung',
   'Timestamp'                   => 'Uhrzeit',
+  'Tired of copying always nice phrases for this message? Click here to use the new preset message option!' => 'Müde vom vielen Copy & Paste aus vorherigen Anschreiben? Hier klicken, um E-Mail-Texte vorzudefinieren!',
   'Title'                       => 'Titel',
   'To'                          => 'An',
   'To (email)'                  => 'An',
   'To (time)'                   => 'Bis',
   'To Date'                     => 'Bis',
   'To continue please change the taxkey 0 to another value.' => 'Um fortzufahren, ändern Sie bitte den Steuerschlüssel 0 auf einen anderen Wert.',
+  'To import'                   => 'Zu importieren',
+  'To upload images: Please create shoppart first' => 'Um Bilder hochzuladen bitte Shopartikel zuerst anlegen',
   'To user login'               => 'Zum Benutzerlogin',
+  'Today'                       => 'heute',
   'Toggle marker'               => 'Markierung umschalten',
+  'Too many results (#1 from #2).' => 'Zu viele Artikel (#1 von #2)',
+  'Too much recursions in assembly tree (>100)' => 'Zu tiefe Verschachtelung (>100) des Erzeugnisbaum',
   'Top'                         => 'Oben',
   'Top (CSS)'                   => 'Oben (mit CSS)',
   'Top (Javascript)'            => 'Oben (mit Javascript)',
-  'Top 100'                     => 'Top 100',
-  'Top 100 hinzufuegen'         => 'Top 100 hinzufügen',
   'Top Level Designation only'  => 'Nur Hauptartikelbezeichnung',
   'Total'                       => 'Summe',
   'Total Fees'                  => 'Kumulierte Gebühren',
   'Total Sales Orders Value'    => 'Auftragseingang',
+  'Total number of entries'     => 'Gesamtzahl Einträge',
   'Total stock value'           => 'Gesamter Bestandswert',
   'Total sum'                   => 'Gesamtsumme',
   'Total weight'                => 'Gesamtgewicht',
@@ -3029,25 +4000,33 @@ $self->{texts} = {
   'Transaction'                 => 'Buchung',
   'Transaction %d cancelled.'   => 'Buchung %d erfolgreich storniert.',
   'Transaction Date missing!'   => 'Buchungsdatum fehlt!',
+  'Transaction Description is not yet implemented' => 'Vorgangsbezeichnung ist noch nicht implementiert',
   'Transaction ID missing.'     => 'Die Buchungs-ID fehlt.',
+  'Transaction Value'           => 'Umsatz',
+  'Transaction Value Currency Code' => 'WKZ Umsatz',
+  'Transaction date'            => 'Buchungsdatum',
   'Transaction deleted!'        => 'Buchung gelöscht!',
   'Transaction description'     => 'Vorgangsbezeichnung',
   'Transaction has already been cancelled!' => 'Diese Buchung wurde bereits storniert.',
   'Transaction has been split on both the credit and the debit side' => 'Sowohl auf der Soll- als auch auf der Haben-Seite gesplittete Buchung',
-  'Transaction posted!'         => 'Buchung verbucht!',
   'Transactions'                => 'Buchungen',
   'Transactions without account:' => 'Buchungen ohne Konto:',
   'Transactions without reference:' => 'Buchungen ohne Referenz:',
   'Transactions, AR transactions, AP transactions' => 'Dialogbuchen, Debitorenrechnungen, Kreditorenrechnungen',
-  'Transdate'                   => 'Belegdatum',
+  'Transdate'                   => 'Buchungsdatum',
+  'Transdate Record'            => 'Buchungsdatum Beleg',
   'Transdate from'              => 'Kontoauszugsdatum von',
   'Transdate is #1'             => 'Belegdatum ist #1',
   'Transdate is after #1'       => 'Belegdatum ist nach #1',
   'Transdate is before #1'      => 'Belegdatum ist vor #1',
-  'Transdate to'                => 'Buchungsdatum bis',
+  'Transdate to'                => 'Kontoauszugsdatum bis',
   'Transfer'                    => 'Umlagern',
+  'Transfer Date'               => 'übernommen am',
   'Transfer Quantity'           => 'Umlagermenge',
   'Transfer To Stock'           => 'Lagereingang',
+  'Transfer all marked'         => 'Markierte übernehmen',
+  'Transfer data to Geierlein ELSTER application' => 'Daten in Geierlein ELSTER-Anwendung übernehmen',
+  'Transfer date exceeds the maximum allowed interval.' => 'Das Belegdatum ist älter als das maximale Zurücklagerungs-Intervall es zulässt.',
   'Transfer from warehouse'     => 'Quelllager',
   'Transfer in'                 => 'Einlagern',
   'Transfer in via default'     => 'Einlagern über Standard-Lagerplatz',
@@ -3057,24 +4036,37 @@ $self->{texts} = {
   'Transfer out via default'    => 'Auslagern über Standard-Lagerplatz',
   'Transfer qty'                => 'Umlagermenge',
   'Transfer services via default' => 'Falls Ein- /Auslagern über Standardlagerplatz aktiviert ist, auch die Dienstleistungen standardmässig Ein- und Auslagern',
+  'Transfer stock'              => 'Lagerbewegungen',
   'Transfer successful'         => 'Lagervorgang erfolgreich',
+  'Transfer undone.'            => 'Zurücklagerung erfolgreich',
+  'Transferred'                 => 'Übernommen',
   'Translation'                 => 'Übersetzung',
   'Translations'                => 'Übersetzungen',
   'Transport and service costs reminder' => 'Transport- und Versandkosten-Erinnerung',
   'Trial Balance'               => 'Summen- und Saldenliste',
   'Trial balance between %s and %s' => 'Summen- und Saldenlisten vom %s bis zum %s',
   'Trying to call a sub without a name' => 'Es wurde versucht, eine Unterfunktion ohne Namen aufzurufen.',
+  'Tue'                         => 'Di',
+  'Tuesday'                     => 'Dienstag',
+  'Turnover'                    => 'Umsätze',
+  'Turnoverstatistic'           => 'Umsatzstatistik',
+  'TypAbbreviation'             => 'Abkürzung des Artikel-Typs',
   'Type'                        => 'Typ',
+  'Type abbreviation'           => 'Typen-Abkürzung',
   'Type can be either \'part\', \'service\' or \'assembly\'.' => 'Der Typ kann entweder \'part\' (für Waren), \'service\' (für Dienstleistungen) oder \'assembly\' (für Erzeugnisse) enthalten.',
   'Type of Business'            => 'Kunden-/Lieferantentyp',
   'Type of Customer'            => 'Kundentyp',
   'Type of Vendor'              => 'Lieferantentyp',
+  'TypeAbbreviation'            => 'Typ-Abkürzung',
   'Types of Business'           => 'Kunden-/Lieferantentypen',
+  'UNDO TRANSFER'               => 'Zurücklagern',
+  'UNIMPORT'                    => 'Import rückgängig',
   'USTVA'                       => 'USTVA',
   'USTVA 2004'                  => 'USTVA 2004',
   'USTVA 2005'                  => 'USTVA 2005',
   'USTVA 2006'                  => 'USTVA 2006',
   'USTVA 2007'                  => 'USTVA 2007',
+  'USTVA Data sent to geierlein' => 'Daten sind an Geierlein ELSTER Anwendung übergeben',
   'USTVA-Hint: Method'          => 'Wenn Sie Ist-Versteuert sind, wählen Sie die Einnahmen-/Überschuß-Rechnung aus. Sind Sie Soll-Versteuert und bilanzverpflichtet, dann wählen Sie Bilanz aus.',
   'USTVA-Hint: Tax Authoritys'  => 'Bitte das Bundesland UND die Stadt bzw. den Einzugsbereich Ihres zuständigen Finanzamts auswählen.',
   'USt-IdNr.'                   => 'USt-IdNr.',
@@ -3082,21 +4074,33 @@ $self->{texts} = {
   'UStVA'                       => 'UStVA',
   'UStVa'                       => 'UStVa',
   'UStVa Einstellungen'         => 'UStVa Einstellungen',
+  'Unable to book transactions for bank purpose #1' => 'Konnte die Transaktion für den Bank-Verwendungszweck #1 nicht erfolgreich durchführen.',
+  'Unable to reconcile, database transaction failure' => 'Abgleich konnte nicht durchgeführt werden: Datenbank-Transaktionsfehler',
   'Unbalanced Ledger'           => 'Bilanzfehler',
   'Unchecked custom variables will not appear in orders and invoices.' => 'Unmarkierte Variablen werden für diesen Artikel nicht in Aufträgen und Rechnungen angezeigt.',
+  'Undo SEPA exports'           => 'SEPA-Exporte rückgängig machen',
+  'Undo Transfer'               => 'Zurücklagern',
+  'Undo Transfer Interval'      => 'Zurücklagerungs-Intervall',
   'Unfinished follow-ups'       => 'Nicht erledigte Wiedervorlagen',
   'Unfortunately you have no warehouse defined.' => 'Leider, gibt es kein Lager in diesem Mandanten.',
+  'Unimport all'                => 'Alle zurück zur Quelle',
+  'Unimport documents'          => 'Importierte Dokumente zurück zurQuelle',
   'Unit'                        => 'Einheit',
   'Unit (if missing or empty default unit will be used)' => 'Einheit (falls nicht vorhanden oder leer wird die Standardeinheit benutzt)',
   'Unit missing.'               => 'Die Einheit fehlt.',
   'Unit of measure'             => 'Maßeinheit',
-  'Units marked for deletion will be deleted upon saving.' => 'Einheiten, die zum L&ouml;schen markiert sind, werden beim Speichern gel&ouml;scht.',
-  'Units that have already been used (e.g. for parts and services or in invoices or warehouse transactions) cannot be changed.' => 'Einheiten, die bereits in Benutzung sind (z.B. bei einer Warendefinition, einer Rechnung oder bei einer Lagerbuchung) k&ouml;nnen nachtr&auml;glich nicht mehr ver&auml;ndert werden.',
+  'Units marked for deletion will be deleted upon saving.' => 'Einheiten, die zum Löschen markiert sind, werden beim Speichern gelöscht.',
+  'Units that have already been used (e.g. for parts and services or in invoices or warehouse transactions) cannot be changed.' => 'Einheiten, die bereits in Benutzung sind (z.B. bei einer Warendefinition, einer Rechnung oder bei einer Lagerbuchung) können nachträglich nicht mehr verändert werden.',
   'Unknown Category'            => 'Unbekannte Kategorie',
   'Unknown Link'                => 'Unbekannte Verknüpfung',
-  'Unknown dependency \'%s\'.'  => 'Unbekannte Abh&auml;ngigkeit \'%s\'.',
+  'Unknown authenticantion module #1 specified in "config/kivitendo.conf".' => 'Unbekanntes Authentifizierungsmodul #1 angegeben in "config/kivitendo.conf".',
+  'Unknown control fields: #1'  => 'Unbekannte Kontrollfelder: #1',
+  'Unknown dependency \'%s\'.'  => 'Unbekannte Abhängigkeit \'%s\'.',
+  'Unknown module: #1'          => 'Unbekanntes Modul #1',
   'Unknown problem type.'       => 'Unbekannter Problem-Typ',
+  'Unlink bank transactions'    => 'Bankverbuchung(en) rückgängig machen',
   'Unlock System'               => 'System entsperren',
+  'Unsuccessfully executed:\n'  => 'Erfolglos ausgeführt:',
   'Unsupported image type (supported types: #1)' => 'Nicht unterstützter Bildtyp (unterstützte Typen: #1)',
   'Until'                       => 'Bis',
   'Update'                      => 'Erneuern',
@@ -3104,28 +4108,77 @@ $self->{texts} = {
   'Update Price'                => 'Preis übernehmen',
   'Update Prices'               => 'Preise aktualisieren',
   'Update SKR04: new tax account 3804 (19%)' => 'Update SKR04: neues Steuerkonto 3804 (19%) für innergemeinschaftlichen Erwerb',
+  'Update customer using billing address' => 'Kunde mit Shop-Rechnungsadresse überschreiben',
+  'Update from master data'     => 'Aktualisieren aus Stammdaten',
   'Update prices'               => 'Preise aktualisieren',
   'Update prices of existing entries' => 'Preise von vorhandenen Artikeln aktualisieren',
+  'Update prices of existing entries / skip non-existent' => 'Preise von vorhandenen Artikel aktualisieren / Nicht vorhandene überspringen',
   'Update properties of existing entries' => 'Eigenschaften von existierenden Einträgen aktualisieren',
+  'Update properties of existing entries / skip non-existent' => 'Eigenschaften von existierenden Artikeln aktualisieren / Nicht vorhandene überspringen',
   'Update quotation/order'      => 'Auftrag/Angebot aktualisieren',
   'Update sales order #1'       => 'Kundenauftrag #1 aktualisieren',
   'Update sales quotation #1'   => 'Angebot #1 aktualisieren',
+  'Update this draft.'          => 'Aktuellen Entwurf speichern',
   'Update with section'         => 'Mit Abschnitt aktualisieren',
   'Updated'                     => 'Erneuert am',
+  'Updated categories'          => 'Artikelgruppe aktualisiert',
+  'Updated part [#1] in shop [#2] at #3' => 'Artikel [#1] in Shop [#2] am [#3] aktualisiert',
+  'Updated shop part'           => 'Artikel aktualisiert',
+  'Updating data of existing entry in database' => 'Aktualisierung von vorhandenen Datenbankdaten',
   'Updating existing entry in database' => 'Existierenden Eintrag in Datenbank aktualisieren',
   'Updating items with additional parts' => 'Positionen für zusätzliche Artikel aktualisieren',
   'Updating items with sections' => 'Positionen für Abschnitte aktualisieren',
   'Updating prices of existing entry in database' => 'Preis des Eintrags in der Datenbank wird aktualisiert',
   'Updating the client fields in the database "#1" on host "#2:#3" failed.' => 'Die Aktualisierung der Mandantenfelder in der Datenbank "#1" auf Host "#2:#3" schlug fehl.',
+  'Upload'                      => 'Aktualisieren',
+  'Upload Attachments'          => 'Anhänge hochladen',
+  'Upload Documents'            => 'Dokumente hochladen',
+  'Upload Images'               => 'Bilder hochladen',
+  'Upload Status'               => 'Upload-Status',
+  'Upload all marked'           => 'Markierte aktualisieren',
+  'Upload file'                 => 'Datei hochladen',
   'Uploaded at'                 => 'Hochgeladen um',
   'Uploaded on #1, size #2 kB'  => 'Am #1 hochgeladen, Größe #2 kB',
+  'Uploading Data'              => 'Uploading',
+  'UsageE'                      => 'Lagerentnahme',
+  'UsageWithout'                => 'Entnommen (ohne Korr.)',
   'Use As New'                  => 'Als neu verwenden',
-  'Use WebDAV Repository'       => 'WebDAV-Ablage verwenden',
+  'Use Balance Sheet'           => 'Bilanz verwenden',
+  'Use Datevautomatik'          => 'Datev-Automatik verwenden',
+  'Use Erfolgsrechnung'         => 'Erfolgsrechnung verwenden',
+  'Use File Storage backend'    => 'Verwende Dateisystem-Backend',
+  'Use Filemanagement'          => 'Verwende Dateimanagement',
+  'Use Income'                  => 'GUV und BWA verwenden',
+  'Use Long Description from Parts for Shop Long Description' => 'Verwende den Artikel Langtext aus den Stammdaten für den Langtext im Shop',
+  'Use Long Description from Parts is only for Shopware6 implemented' => 'Der Langtext aus den Stammdaten kann nur in Shopware6 verwendet werden',
+  'Use UStVA'                   => 'UStVA verwenden',
+  'Use WebDAV Repository'       => 'Verwende WebDAV',
+  'Use WebDAV Storage backend'  => 'Verwende WebDAV-Backend',
+  'Use a text field to enter (new) contact departments if enabled. Otherwise, only a drop down box is offered.' => 'Textfeld zusätzlich zur Eingabe (neuer) Abteilungen von Ansprechpersonen verwenden. Sonst wird nur eine Auswahlliste angezeigt.',
+  'Use a text field to enter (new) contact titles if enabled. Otherwise, only a drop down box is offered.' => 'Textfeld zusätzlich zur Eingabe (neuer) Titel von Ansprechpersonen verwenden. Sonst wird nur eine Auswahlliste angezeigt.',
+  'Use a text field to enter (new) greetings if enabled. Otherwise, only a drop down box is offered.' => 'Textfeld zusätzlich zur Eingabe (neuer) Anreden verwenden. Sonst wird nur eine Auswahlliste angezeigt.',
+  'Use as new'                  => 'Als neu verwenden',
+  'Use date and duration for time recordings' => 'Datum und Dauer für Zeiterfassung verwenden',
+  'Use default booking group because setting is \'all\'' => 'Standardbuchungsgruppe wird verwendet',
+  'Use default booking group because wanted is missing' => 'Fehlende Buchungsgruppe, deshalb Standardbuchungsgruppe',
   'Use existing templates'      => 'Vorhandene Druckvorlagen verwenden',
+  'Use for Factur-X/ZUGFeRD'    => 'Nutzung mit Factur-X/ZUGFeRD',
+  'Use for Swiss QR-Bill'       => 'Nutzung mit Schweizer QR-Rechnung',
   'Use master default bin for Default Transfer, if no default bin for the part is configured' => 'Standardlagerplatz für Ein- / Auslagern über Standard-Lagerplatz, falls für die Ware kein expliziter Lagerplatz konfiguriert ist',
+  'Use settings from client configuration' => 'Einstellungen aus Mandantenkonfiguration folgen',
+  'Use text field for department of contacts' => 'Textfeld für Abteilungen von Ansprechpersonen verwenden',
+  'Use text field for greetings' => 'Textfeld für Anreden verwenden',
+  'Use text field for title of contacts' => 'Textfeld für Titel von Ansprechpersonen verwenden',
+  'Use this storage backend for all generated PDF-Files' => 'Verwende dieses Backend für generierte PDF-Dateien',
+  'Use this storage backend for all uploaded attachments' => 'Verwende dieses Backend für hochgeladene Dateien',
+  'Use this storage backend for uploaded images' => 'Verwende dieses Backend für hochgeladene Bilder',
+  'Useable for sections'        => 'Für Abschnitte nutzbar',
+  'Useable for text blocks'     => 'Für Textblöcke nutzbar',
   'Useable for…'                => 'Benutzbar für…',
+  'Used for Purchase'           => 'im Einkauf verwenden',
+  'Used for Sale'               => 'im Verkauf verwenden',
+  'Used for assembly #1 #2'     => 'Verwendet für Erzeugnis #1 #2',
   'User'                        => 'Benutzer',
-  'User Config'                 => 'Einstellungen',
   'User Preferences'            => 'Benutzereinstellungen',
   'User access'                 => 'Benutzerzugriff',
   'User list'                   => 'Benutzerliste',
@@ -3137,14 +4190,17 @@ $self->{texts} = {
   'Users with access'           => 'Benutzer mit Zugriff',
   'Users with access to this client' => 'Benutzer mit Zugriff auf diesen Mandanten',
   'Users, Clients and User Groups' => 'Benutzer, Mandanten und Benutzergruppen',
+  'Usually the delivery date of an order is the next working day. If a value is set here this value will be added to the delivery date of the sales order. The resulting date will be adjusted to the next working day if it ends up on a weekend.' => 'Standardmäßig ist das vorausgewählte Lieferdatum der nächste Arbeitstag. Falls hier ein Wert gesetzt ist, wird dieser zum eigentlichen Lieferdatum hinzuaddiert. Fällt das daraus resultierende Datum auf ein Wochenende, so wird der nächste Werktag genommen.',
   'Usually the sales quotation is valid until the next working day. If a value is set here then the quotation will be valid for at least that many days. The resulting date will be adjusted to the next working day if it ends up on a weekend.' => 'Standardmäßig ist ein Verkaufsangebot bis zum nächsten Werktag gültig. Ist hier ein Wert angegeben, so ist ein Angebot mindestens so viele Tage gültig. Sollte das dabei herauskommende Datum auf ein Wochenende fallen, so wird statt dessen der nachfolgende Arbeitstag genommen.',
-  'VAT ID'                      => 'UStdID-Nr',
+  'VAT ID'                      => 'USt-IdNr.',
+  'VAT ID and/or taxnumber must be given.' => 'UStId und/oder Steuernummer muss angegeben werden.',
+  'VN'                          => 'Kred.-Nr.',
   'Valid'                       => 'Gültig',
+  'Valid are integer values and floating point numbers, e.g. 4.75h = 4 hours and 45 minutes.' => 'Erlaubt sind ganzzahlige Werte und Kommawerte: Beispiel: 4,75h = 4 Stunden und 45 Minuten.',
   'Valid from'                  => 'Gültig ab',
   'Valid until'                 => 'gültig bis',
   'Valid/Obsolete'              => 'Gültig/ungültig',
   'Value'                       => 'Wert',
-  'Valuta date'                 => 'Valutadatum',
   'Valutadate'                  => 'Valutadatum',
   'Valutadate from'             => 'Valutadatum von',
   'Valutadate to'               => 'Valutadatum bis',
@@ -3152,65 +4208,93 @@ $self->{texts} = {
   'Variable Description'        => 'Datenfeldbezeichnung',
   'Variable Name'               => 'Datenfeldname (intern)',
   'Vendor'                      => 'Lieferant',
-  'Vendor (database ID)'        => '(Datenbank-ID)',
+  'Vendor (database ID)'        => 'Lieferant (Datenbank-ID)',
   'Vendor (name)'               => 'Lieferant (Name)',
   'Vendor Discount'             => 'Lieferantenrabatt',
+  'Vendor GLN'                  => 'GLN des Lieferanten',
   'Vendor Invoice'              => 'Einkaufsrechnung',
   'Vendor Invoices & AP Transactions' => 'Einkaufsrechnungen & Kreditorenbuchungen',
+  'Vendor Master Data'          => 'Lieferantenstammdaten',
   'Vendor Name'                 => 'Lieferantenname',
   'Vendor Number'               => 'Lieferantennummer',
   'Vendor Order Number'         => 'Bestellnummer beim Lieferanten',
   'Vendor deleted!'             => 'Lieferant gelöscht!',
   'Vendor details'              => 'Lieferantendetails',
-  'Vendor filter for AP transaction drafts' => 'Filter für Entwürfe',
   'Vendor missing!'             => 'Lieferant fehlt!',
-  'Vendor not on file or locked!' => 'Dieser Lieferant existiert nicht oder ist gesperrt.',
-  'Vendor not on file!'         => 'Lieferant ist nicht in der Datenbank!',
   'Vendor saved'                => 'Lieferant gespeichert',
   'Vendor saved!'               => 'Lieferant gespeichert!',
   'Vendor type'                 => 'Lieferantentyp',
   'Vendors'                     => 'Lieferanten',
+  'Vendors: VAT ID / taxnumber unique' => 'Lieferanten: UStID / Steuernummer eindeutig',
   'Verrechnungseinheit'         => 'Verrechnungseinheit',
   'Version'                     => 'Version',
   'Version actions'             => 'Aktionen für Versionen',
   'Version number'              => 'Versionsnummer',
   'Versions'                    => 'Versionen',
+  'View RFQs'                   => 'Lieferantenanfragen ansehen',
   'View SEPA export'            => 'SEPA-Export-Details ansehen',
   'View background job execution result' => 'Verlauf der Hintergrund-Job-Ausführungen anzeigen',
-  'View background job history' => 'Hintergrund-Job-Verlauf anzeigen',
-  'View background jobs'        => 'Hintergrund-Jobs anzeigen',
+  'View purchase delivery orders' => 'Einkaufslieferscheine ansehen',
+  'View purchase invoices'      => 'Einkaufsrechungen ansehen',
+  'View purchase orders'        => 'Lieferantenaufträge ansehen',
+  'View record links from Sales Order' => 'Verknüpfte Belege immer vom Verkaufsauftrag ansehen',
+  'View sales delivery orders'  => 'Verkaufslieferscheine ansehen',
+  'View sales invoices and credit notes' => 'Rechnungen und Gutschriften ansehen',
+  'View sales orders'           => 'Auftragsbestätigungen ansehen',
+  'View sales quotations'       => 'Angebote ansehen',
   'View sent email'             => 'Verschickte E-Mail anzeigen',
   'View warehouse content'      => 'Lagerbestand ansehen',
+  'View/edit all employees purchase documents' => 'Bearbeiten/ansehen der Einkaufsdokumente aller Mitarbeiter',
   'View/edit all employees sales documents' => 'Bearbeiten/ansehen der Verkaufsdokumente aller Mitarbeiter',
-  'Von Konto: '                 => 'von Konto: ',
   'WHJournal'                   => 'Lagerbuchungen',
+  'WHUsage'                     => 'Lagerentnahme',
   'Warehouse'                   => 'Lager',
-  'Warehouse (database ID)'     => 'Lager (Datenbank-ID)',
+  'Warehouse (database ID)'     => 'Lager (database ID)',
+  'Warehouse (name)'            => 'Lager (Name)',
   'Warehouse From'              => 'Quelllager',
   'Warehouse Migration'         => 'Lagermigration',
   'Warehouse To'                => 'Ziellager',
   'Warehouse content'           => 'Lagerbestand',
-  'Warehouse deleted.'          => 'Lager gel&ouml;scht.',
+  'Warehouse deleted.'          => 'Lager gelöscht.',
   'Warehouse management'        => 'Lagerverwaltung/Bestandsveränderung',
   'Warehouse saved.'            => 'Lager gespeichert.',
   'Warehouses'                  => 'Lager',
+  'Warn before saving orders with duplicate parts (new controller only)' => 'Beim Speichern warnen, wenn doppelte Artikel in einem Auftrag sind',
+  'Warn before saving orders without a delivery date' => 'Warnung ausgeben, falls Aufträge kein Lieferdatum haben.',
+  'Warn before saving sales orders with missing customer order number (new controller only)' => 'Warnung ausgeben, falls Verkaufsaufträge keine Kundenbestellnummer haben',
   'Warning'                     => 'Warnung',
+  'Warning! Loading a draft will discard unsaved data!' => 'Achtung! Beim Laden eines Entwurfs werden ungespeicherte Daten verworfen!',
+  'Warning: Faulty position ignored' => 'Warnung: Fehlerhafte Artikel-Position ignoriert',
+  'Warning: One or more field value are not in valid DATEV format at:' => 'Warnung: Ein oder mehere Felder haben ungültige Feldwerte laut DATEV-Spezifikation bei:',
+  'Warnings and errors'         => 'Warnungen und Fehler',
+  'Watch status'                => 'Status',
+  'We need a array of datev_lines' => 'Es wird eine Array vom Typ datev_lines erwartet',
+  'We need a valid from date'   => 'Es wird ein gültiges von Datum erwartet',
+  'We need a valid to date'     => 'Es wird ein gültiges bis Datum erwartet',
+  'Web shops'                   => 'Webshops',
   'WebDAV'                      => 'WebDAV',
   'WebDAV link'                 => 'WebDAV-Link',
   'WebDAV save documents'       => 'Belege in WebDAV-Ablage speichern',
   'Webserver interface'         => 'Webserverschnittstelle',
+  'Webshop'                     => 'Webshop',
+  'Webshop Import'              => 'Webshop Import',
+  'Webshop articles'            => 'Webshop Artikel',
+  'Webshops articles'           => 'Webshops Artikel',
+  'Wed'                         => 'Mi',
+  'Wednesday'                   => 'Mittwoch',
   'Weight'                      => 'Gewicht',
   'Weight unit'                 => 'Gewichtseinheit',
   'What <b>term</b> you are looking for?' => 'Nach welchem <b>Begriff</b> wollen Sie suchen?',
   'What this template contains' => 'Was diese Vorlage enthält',
   'What type of item is this?'  => 'Was ist dieser Artikel?',
   'When converting a requirement spec into a quotation or an oder each section gets converted into a line position in the new record. This is the article used by default for this conversion.' => 'Wenn ein Pflichtenheft in ein Angebot oder Auftrag umgewandelt wird, wird für jeden Abschnitt eine Position im neuen Beleg angelegt. Dies ist der Artikel, der standardmäßig bei dieser Umwandlung genutzt wird.',
+  'Whether or not to replace variable placeholders such as "<%invdate%>" in texts in positions such as the part description by the record\'s actual value' => 'Ob Variablenplatzhalter wie z.B. <%invdate%> in Positionstexten wie der Artikelbeschreibung durch den tatsächlichen Wert aus dem Beleg ersetzt werden sollen',
   'Which is located at doc/kivitendo-Dokumentation.pdf. Click here: ' => 'Diese befindet sich unter doc/kivitendo-Dokumentation.pdf. Klicken Sie hier:',
   'With Attachments'            => 'Journal mit Anhängen',
   'With Extension Of Time'      => 'mit Dauerfristverlängerung',
   'With the introduction of clients each client gets its own WebDAV folder.' => 'Mit der Einführung von Mandanten erhält jeder Mandant sein eigenes WebDAV-Verzeichnis.',
   'Without Attachments'         => 'Journal ohne Anhänge',
-  'Workflow Delivery Order'     => 'Workflow Lieferschein',
+  'Workflow'                    => 'Workflow',
   'Workflow purchase_order'     => 'Workflow Lieferantenauftrag',
   'Workflow request_quotation'  => 'Workflow Preisanfrage',
   'Workflow sales_order'        => 'Workflow Auftrag',
@@ -3218,42 +4302,58 @@ $self->{texts} = {
   'Working copy identical to version number #1' => 'Mit Versionsnummer #1 identische Arbeitskopie',
   'Working copy without version' => 'Arbeitskopie ohne Version',
   'Working copy; no description yet' => 'Arbeitskopie; noch keine Beschreibung',
+  'Working on export'           => 'Generiere Export',
   'Write bin to default bin in part?' => 'Diesen Lagerplatz als Standardlagerplatz im Artikel setzen?',
+  'Wrong date format (#1)'      => 'Falsches Datumsformat (#1)',
+  'Wrong field value \'#1\' for field \'#2\' for the transaction with amount \'#3\'' => 'Falscher Feldwert \'#1\' für Feld \'#2\' bei der Transaktion mit dem Umsatz von \'#3\'',
+  'Wrong file name, expects name like: DTVF_*_LOHNBUCHUNG*.csv' => 'Falscher Dateiname, erwartet wird DTVF_*_LOHNBUCHUNG*.csv',
+  'Wrong number format (#1)'    => 'Falsches Zahlenformat (#1)',
   'Wrong tax keys recorded'     => 'Gespeicherte Steuerschlüssel sind falsch',
   'Wrong taxes recorded'        => 'Gespeicherte Steuern passen nicht zum Steuerschlüssel',
+  'Wrong time format (#1)'      => 'Falsches Zeitformat (#1)',
   'X'                           => 'X',
   'YYYY'                        => 'JJJJ',
   'Year'                        => 'Jahr',
+  'Year-end bookings were successfully completed!' => 'Die Jahresabschlußbuchungen wurden erfolgreich durchgeführt!',
+  'Year-end closing'            => 'Jahresabschluß',
+  'Year-end date'               => 'Jahresabschlußdatum',
+  'Year-end date missing'       => 'Jahresabschlußdatum fehlt',
   'Yearly'                      => 'jährlich',
   'Yearly taxreport not yet implemented' => 'Jährlicher Steuerreport für dieses Ausgabeformat noch nicht implementiert',
   'Yes'                         => 'Ja',
-  'Yes, included by default'    => 'Ja, standardm&auml;&szlig;ig an',
+  'Yes, included by default'    => 'Ja, standardmäßig an',
   'Yes/No (Checkbox)'           => 'Ja/Nein (Checkbox)',
+  'You are adding a new part while you are editing another document. You will be redirected to your document when saving the new part or aborting this form.' => 'Sie legen einen neuen Artikel an, während Sie ein anderes Dokument bearbeiten. Sie werden zu Ihrem Dokument zurückgeleitet, wenn Sie den Artikel speichern oder die Bearbeitung dieser Maske abbrechen.',
   'You are logged out!'         => 'Auf Wiedersehen!',
   'You can also delete this transaction and re-enter it manually.' => 'Alternativ können Sie die Buchung auch mit löschen lassen und sie anschließend neu eingeben.',
   'You can choose account categories for taxes. Depending on these categories taxes will be displayed for transfers in the general ledger or not.' => 'Sie können Kontoarten für Steuern auswählen. Abhängig von diesen Kontoarten werden dann Steuern bei Dialogbuchungen angezeigt oder nicht.',
   'You can correct this transaction by chosing the correct taxkeys from the drop down boxes and hitting the button "Fix transaction" afterwards.' => 'Sie haben die Möglichkeit, die Buchung zu korrigieren, indem Sie in den Drop-Down-Boxen die richtigen Steuerschlüssel auswählen und anschließend auf den Button "Buchung korrigieren" drücken.',
-  'You can create warehouses and bins via the menu "System -> Warehouses".' => 'Sie k&ouml;nnen Lager und Lagerpl&auml;tze &uuml;ber das Men&uuml; "System -> Lager" anlegen.',
-  'You can declare different translations for singular and plural for each unit (e.g. &quot;day&quot; and &quot;days).' => 'Bei den &Uuml;bersetzungen k&ouml;nnen Sie unterschiedliche Varianten f&uuml;r singular und plural angeben (z.B. &quot;day&quot; und &quot;days&quot;).',
+  'You can create warehouses and bins via the menu "System -> Warehouses".' => 'Sie können Lager und Lagerplätze über das Menü "System -> Lager" anlegen.',
+  'You can declare different translations for singular and plural for each unit (e.g. &quot;day&quot; and &quot;days).' => 'Bei den Übersetzungen können Sie unterschiedliche Varianten für singular und plural angeben (z.B. &quot;day&quot; und &quot;days&quot;).',
   'You can either create a new database or chose an existing database.' => 'Sie können entweder eine neue Datenbank erstellen oder eine existierende auswählen.',
   'You can find information on the migration in the upgrade chapter of the documentation.' => 'Informationen über die Migration sind in der Upgrade Kapitel in der Dokumentation zu finden.',
-  'You can only delete datasets that are not in use.' => 'Sie k&ouml;nnen nur Datenbanken l&ouml;schen, die momentan nicht in Benutzung sind.',
+  'You can only delete datasets that are not in use.' => 'Sie können nur Datenbanken löschen, die momentan nicht in Benutzung sind.',
   'You can update existing contacts by providing the \'cp_id\' column with their database IDs. Otherwise: ' => 'Sie können existierende Einträge aktualisieren, indem Sie eine Spalte \'cp_id\' mit der Datenbank-ID des Eintrags mitgeben. Andernfalls: ',
   'You can use the following strings in the long description and all translations. They will be replaced by their actual values by kivitendo before they\'re output.' => 'Sie können die folgenden Begriffe in den Langtexten und allen Übersetzungen benutzen. Sie werden von kivitendo vor der Ausgabe durch ihren tatsächlichen Wert ersetzt.',
   'You cannot adjust the price for pricegroup "#1" by a negative percentage.' => 'Sie können den Preis für Preisgruppe "#1" nicht um einen negativen Prozentwert anpassen.',
-  'You cannot continue before all required modules are installed.' => 'Sie k&ouml;nnen nicht fortfahren, bevor alle ben&ouml;tigten Pakete installiert sind.',
+  'You cannot continue before all required modules are installed.' => 'Sie können nicht fortfahren, bevor alle benötigten Pakete installiert sind.',
   'You cannot create an invoice for delivery orders for different customers.' => 'Sie können keine Rechnung zu Lieferscheinen für verschiedene Kunden erstellen.',
   'You cannot create an invoice for delivery orders from different vendors.' => 'Sie können keine Rechnung aus Lieferscheinen von verschiedenen Lieferanten erstellen.',
   'You cannot modify individual assigments from additional articles to line items.' => 'Eine individuelle Zuordnung der zusätzlichen Artikel zu Positionen kann nicht vorgenommen werden.',
   'You cannot paste function blocks or sub function blocks if there is no section.' => 'Sie können keine Funktionsblöcke oder Unterfunktionsblöcke einfügen, wenn es noch keinen Abschnitt gibt.',
+  'You cannot use a negative amount with debit/credit!' => 'Sie dürfen für Soll/Haben keine negativen Werte benutzen!',
+  'You do not have access to any custom data export.' => 'Sie haben auf keine benutzerdefinierten Datenexporte Zugriff.',
   'You do not have permission to access this entry.' => 'Sie verfügen nicht über die Berechtigung, auf diesen Eintrag zuzugreifen.',
   'You do not have the permissions to access this function.' => 'Sie verfügen nicht über die notwendigen Rechte, um auf diese Funktion zuzugreifen.',
-  'You have entered or selected the following shipping address for this customer:' => 'Sie haben die folgende Lieferadresse eingegeben oder ausgew&auml;hlt:',
+  'You don\'t have the rights to edit this customer.' => 'Sie verfügen nicht über die erforderlichen Rechte, um diesen Kunden zu bearbeiten.',
+  'You don\'t have the rights to edit this vendor.' => 'Sie verfügen nicht über die erforderlichen Rechte, um diesen Lieferanten zu bearbeiten.',
+  'You have changed the currency or exchange rate. Please check prices.' => 'Die Währung oder der Wechselkurs hat sich geändert. Bitte überprüfen Sie die Preise.',
+  'You have entered or selected the following shipping address for this customer:' => 'Sie haben die folgende Lieferadresse eingegeben oder ausgewählt:',
   'You have never worked with currencies.' => 'Sie haben noch nie  mit Währungen gearbeitet.',
   'You have not added bank accounts yet.' => 'Sie haben noch keine Bankkonten angelegt.',
   'You have not selected any delivery order.' => 'Sie haben keinen Lieferschein ausgewählt.',
   'You have not selected any export.' => 'Sie haben keinen Export ausgewählt.',
-  'You have not selected any item.' => 'Sie haben keine noch nicht gebuchten Einträge ausgewählt.',
+  'You have not selected any item.' => 'Sie haben keine Einträge ausgewählt.',
   'You have selected none of the invoices.' => 'Sie haben keine der Rechnungen ausgewählt.',
   'You have to define a unit as a multiple of a smaller unit.' => 'Sie müssen Einheiten als ein Vielfaches einer kleineren Einheit eingeben.',
   'You have to enter a company name in the client configuration.' => 'Sie müssen in der Mandantenkonfiguration einen Firmennamen angeben.',
@@ -3261,24 +4361,31 @@ $self->{texts} = {
   'You have to grant users access to one or more clients.' => 'Benutzern muss dann Zugriff auf einzelne Mandanten gewährt werden.',
   'You have to specify a department.' => 'Sie müssen eine Abteilung wählen.',
   'You have to specify an execution date for each antry.' => 'Sie müssen für jeden zu buchenden Eintrag ein Ausführungsdatum angeben.',
-  'You must chose a user.'      => 'Sie m&uuml;ssen einen Benutzer ausw&auml;hlen.',
+  'You have to upload an MT940 file to import.' => 'Sie müssen die zu importierende MT940-Datei hochladen.',
+  'You must chose a user.'      => 'Sie müssen einen Benutzer auswählen.',
   'You must enter a name for your new print templates.' => 'Sie müssen einen Namen für die neuen Druckvorlagen angeben.',
+  'You must not change this AP transaction.' => 'Sie dürfen diese Kreditorenbuchung nicht verändern.',
+  'You must not change this AR transaction.' => 'Sie dürfen diese Debitorenbuchung nicht verändern.',
+  'You must not change this invoice.' => 'Sie dürfen diese Rechnung nicht verändern.',
+  'You must not print this invoice.' => 'Sie dürfen diese Rechnung nicht drucken.',
   'You must select existing print templates or create a new set.' => 'Sie müssen vorhandene Druckvorlagen auswählen oder einen neuen Satz anlegen.',
   'You should create a backup of the database before proceeding because the backup might not be reversible.' => 'Sie sollten eine Sicherungskopie der Datenbank erstellen, bevor Sie fortfahren, da die Aktualisierung unter Umständen nicht umkehrbar ist.',
   'You\'re not editing a file.' => 'Sie bearbeiten momentan keine Datei.',
-  'You\'ve already chosen the following limitations:' => 'Sie haben bereits die folgenden Einschr&auml;nkungen vorgenommen:',
+  'You\'ve already chosen the following limitations:' => 'Sie haben bereits die folgenden Einschränkungen vorgenommen:',
+  'Your Order'                  => 'Ihre Bestellung',
   'Your PostgreSQL installationen does not use Unicode as its encoding. This is not supported anymore.' => 'Ihre PostgreSQL-Installation benutzt ein anderes Encoding als Unicode. Dies wird nicht mehr unterstützt.',
   'Your Reference'              => 'Ihr Zeichen',
   'Your TODO list'              => 'Ihre Aufgabenliste',
   'Your browser does not currently support Javascript.' => 'Ihr Browser unterstützt im Moment kein Javascript!',
   'Your download does not exist anymore. Please re-run the DATEV export assistant.' => 'Ihr Download existiert nicht mehr. Bitte starten Sie den DATEV-Exportassistenten erneut.',
   'Your import is being processed.' => 'Ihr Import wird verarbeitet',
-  'Zeitpunkt'                   => 'Zeitpunkt',
+  'Your target quantity will be added to the stocked quantity.' => 'Ihre gezählte Zielmenge wird zum Lagerbestand hinzugezählt.',
   'Zeitraum'                    => 'Zeitraum',
   'Zero amount posting!'        => 'Buchung ohne Wert',
+  'Zip'                         => 'PLZ',
   'Zip, City'                   => 'PLZ, Ort',
   'Zipcode'                     => 'PLZ',
-  'Zusatz'                      => 'Zusatz',
+  'Zipcode and city'            => 'PLZ und Stadt',
   '[email]'                     => '[email]',
   'absolute'                    => 'absolut',
   'account_description'         => 'Beschreibung',
@@ -3290,48 +4397,61 @@ $self->{texts} = {
   'and'                         => 'und',
   'ap_aging_list'               => 'liste_offene_verbindlichkeiten',
   'ar_aging_list'               => 'liste_offene_forderungen',
+  'ar_chart isn\'t a valid chart' => 'Das Forderungskonto ist kein gültiges Konto.',
+  'article_list'                => 'article_list',
   'as at'                       => 'zum Stand',
+  'assembled'                   => 'Gefertigt',
   'assembly'                    => 'Erzeugnis',
   'assembly_list'               => 'erzeugnisliste',
   'averaged values, in invoice mode only useful when filtered by a part' => 'gemittelte Werte, im Rechnungsmodus nur sinnvoll wenn nach Artikel gefiltert wird',
+  'averconsumed_br'             => 'Ø mtl.',
   'back'                        => 'zurück',
+  'back_br'                     => 'Zurk.',
+  'backend "#1" not enabled'    => 'Dateimanagement-Subsystem "#1" nicht aktiviert',
+  'backend "#1" not found'      => 'Dateimanagement-Subsystem "#1" unbekannt',
   'balance'                     => 'Betriebsvermögensvergleich/Bilanzierung',
   'bank_collection_payment_list_#1' => 'bankeinzugszahlungsliste_#1',
   'bank_transfer_payment_list_#1' => 'ueberweisungszahlungsliste_#1',
   'banktransfers'               => 'ueberweisungen',
+  'basis for stock value'       => 'Grundlage für Bestandswert',
   'bestbefore #1'               => 'Mindesthaltbarkeit #1',
   'bin_list'                    => 'Lagerliste',
   'bis'                         => 'bis',
+  'brutto'                      => 'brutto',
   'building data'               => 'Verarbeite Daten',
   'building report'             => 'Erstelle Bericht',
+  'can not allocate #1 units of #2, missing #3 units' => 'Kann keine #1 Einheiten von #2 belegen, es fehlen #3 Einheiten',
+  'can not allocate enough resources for production' => 'Kann nicht genug Mengen für die Produktion belegen',
+  'can only parse a pdf file'   => 'Kann nur eine gültige PDF-Datei verwenden.',
   'cash'                        => 'Ist-Versteuerung',
   'chargenumber #1'             => 'Chargennummer #1',
   'chart_of_accounts'           => 'kontenuebersicht',
-  'choice'                      => 'auswählen',
-  'choice part'                 => 'Artikel auswählen',
   'cleared'                     => 'Abgeglichen',
   'click here to edit cvars'    => 'Klicken Sie hier, um nach benutzerdefinierten Variablen zu suchen',
   'close'                       => 'schließen',
   'closed'                      => 'geschlossen',
-  'companylogo_subtitle'        => 'Lizenziert f&uuml;r',
-  'config/kivitendo.conf: Key "DB_config" is missing.' => 'config/kivitendo.conf: Das Schl&uuml;sselwort "DB_config" fehlt.',
+  'companylogo_subtitle'        => 'Lizenziert für',
+  'config/kivitendo.conf: Key "DB_config" is missing.' => 'config/kivitendo.conf: Das Schlüsselwort "DB_config" fehlt.',
   'config/kivitendo.conf: Key "authentication/ldap" is missing.' => 'config/kivitendo.conf: Der Schlüssel "authentication/ldap" fehlt.',
-  'config/kivitendo.conf: Missing parameters in "authentication/database". Required parameters are "host", "db" and "user".' => 'config/kivitendo.conf: Fehlende Parameter in "authentication/database". Ben&ouml;tigte Parameter sind "host", "db" und "user".',
+  'config/kivitendo.conf: Missing parameters in "authentication/database". Required parameters are "host", "db" and "user".' => 'config/kivitendo.conf: Fehlende Parameter in "authentication/database". Benötigte Parameter sind "host", "db" und "user".',
   'config/kivitendo.conf: Missing parameters in "authentication/ldap". Required parameters are "host", "attribute" and "base_dn".' => 'config/kivitendo.conf: Fehlende Parameter in "authentication/ldap". Benötigt werden "host", "attribute" und "base_dn".',
+  'consumed'                    => 'Im Zeitraum',
   'contact_list'                => 'ansprechperson_liste',
-  'continue'                    => 'weiter',
   'correction'                  => 'Korrektur',
+  'correction_br'               => 'Korr.',
   'cp_greeting to cp_gender migration' => 'Datenumwandlung von Titel nach Geschlecht (cp_greeting to cp_gender)',
-  'customer'                    => 'Kunde',
   'customer_list'               => 'kundenliste',
   'dated'                       => 'datiert',
-  'debug'                       => 'Debug',
   'delete'                      => 'Löschen',
+  'delete item'                 => 'Position löschen',
+  'delete order'                => 'Bestellung löschen',
+  'deleted'                     => 'gelöscht',
   'delivered'                   => 'geliefert',
   'deliverydate'                => 'Lieferdatum',
   'difference as skonto'        => 'Differenz als Skonto',
   'direct debit'                => 'Lastschrifteinzug',
   'disposed'                    => 'Entsorgung',
+  'disposed_br'                 => 'Entsgt.',
   'do not include'              => 'Nicht aufnehmen',
   'done'                        => 'erledigt',
   'dunning_list'                => 'mahnungsliste',
@@ -3340,27 +4460,51 @@ $self->{texts} = {
   'ea'                          => 'St.',
   'emailed to'                  => 'gemailt an',
   'empty'                       => 'leer',
+  'entries imported'            => 'Einträge importiert',
+  'error while disassembling for trans_ids #1 : #2' => 'Fehler beim Zerlegen von Erzeugnis für Transaktions-Id #1: #2',
   'error while paying invoice #1 : ' => 'Fehler beim Bezahlen von Rechnung #1 : ',
+  'error while unlinking payment #1 : ' => 'Fehler beim Zurücksetzen von Zahlung #1:',
   'every third month'           => 'vierteljährlich',
   'every time'                  => 'immer',
+  'exchange rate already exists, no update allowed' => 'Wechselkurs existiert bereits und kann nicht geändert werden',
+  'exchange rate has to be positive' => 'Wechselkurs muss positiv sein',
   'executed'                    => 'ausgeführt',
+  'execution as user \'#1\''    => 'Ausführung als User »#1«',
   'failed'                      => 'fehlgeschlagen',
+  'false'                       => 'falsch',
   'female'                      => 'weiblich',
+  'file \'#1\' has unsupported image type \'#2\' (supported types: #3)' => 'Datei \'#1\' hat nicht unterstütztes Format \'#2\' (unterstützt wird:\'#3\')',
+  'filename'                    => 'Dateiname',
+  'filename has not uploadable characters ' => 'Bitte Dateinamen ändern. Er hat für den Upload nicht verwendbare Sonderzeichen ',
+  'filesize too big: '          => 'Datei zu groß: ',
+  'final_invoice'               => 'Schlussrechnung',
   'flat-rate position'          => 'Pauschalposition',
   'follow_up_list'              => 'wiedervorlageliste',
-  'for'                         => 'f&uuml;r',
+  'for'                         => 'für',
+  'for Document types'          => 'für unterschiedliche ERP Dokumententypen',
   'for Period'                  => 'für den Zeitraum',
+  'for all'                     => 'für alle',
   'for date'                    => 'zum Stichtag',
   'found'                       => 'Gefunden',
+  'found_br'                    => 'Gef.',
+  'free skonto'                 => 'Freier Skontobetrag',
+  'from'                        => 'von',
+  'from \'#1\' imported Files'  => 'Von \'#1\' importierte Dateien',
   'from (time)'                 => 'von',
-  'general_ledger_list'         => 'buchungsjournal',
+  'general_ledger_list'         => 'Buchungsjournal',
+  'generated Files'             => 'Erzeugte Dokumente',
+  'gobd-#1-#2.zip'              => 'gobd-#1-#2.zip',
   'h'                           => 'h',
-  'history'                     => 'Historie',
   'history search engine'       => 'Historien Suchmaschine',
+  'http'                        => 'http',
+  'https'                       => 'https',
+  'imported'                    => 'Importiert',
   'inactive'                    => 'inaktiv',
   'income'                      => 'Einnahmen-Überschuß-Rechnung',
+  'internal error (see details)' => 'Interner Fehler (siehe Details)!',
   'invoice'                     => 'Rechnung',
   'invoice mode or item mode'   => 'Rechnungsmodus oder Artikelmodus',
+  'invoice_for_advance_payment' => 'Anzahlungsrechnung',
   'invoice_list'                => 'debitorenbuchungsliste',
   'is'                          => 'ist',
   'is after'                    => 'ist nach dem',
@@ -3369,51 +4513,65 @@ $self->{texts} = {
   'is greater than or equal'    => 'ist größer oder gleich',
   'is lower than or equal'      => 'ist kleiner oder gleich',
   'kivitendo'                   => 'kivitendo',
-  'kivitendo Homepage'          => 'Infos zu kivitendo',
+  'kivitendo Homepage'          => 'kivitendo Homepage',
   'kivitendo can fix these problems automatically.' => 'kivitendo kann solche Probleme automatisch beheben.',
   'kivitendo has been extended to handle multiple clients within a single installation.' => 'kivitendo wurde um Mandantenfähigkeit erweitert.',
   'kivitendo has found one or more problems in the general ledger.' => 'kivitendo hat ein oder mehrere Probleme im Hauptbuch gefunden.',
   'kivitendo is about to update the database [ #1 ].' => 'kivitendo wird gleich die Datenbank [ #1 ] aktualisieren.',
-  'kivitendo is now able to manage warehouses instead of just tracking the amount of goods in your system.' => 'kivitendo enth&auml;lt jetzt auch echte Lagerverwaultung anstatt reiner Mengenz&auml;hlung.',
+  'kivitendo is now able to manage warehouses instead of just tracking the amount of goods in your system.' => 'kivitendo enthält jetzt auch echte Lagerverwaultung anstatt reiner Mengenzählung.',
   'kivitendo modules'           => 'Module',
   'kivitendo needs to update the authentication database before you can proceed.' => 'kivitendo muss die Authentifizierungsdatenbank aktualisieren, bevor Sie fortfahren können.',
   'kivitendo v#1'               => 'kivitendo v#1',
   'kivitendo v#1 administration' => 'kivitendo v#1 Administration',
-  'kivitendo website (external)' => 'kivitendo-Webseite (extern)',
+  'kivitendo website (external)' => 'kivitendo Webseite (extern)',
   'kivitendo will then update the database automatically.' => 'kivitendo wird die Datenbank daraufhin automatisch aktualisieren.',
-  'lead deleted!'               => 'Kundenquelle gelöscht',
-  'lead saved!'                 => 'Kundenquelle geichert',
   'letters_list'                => 'briefliste',
-  'list'                        => 'auflisten',
   'list_of_payments'            => 'zahlungsausgaenge',
   'list_of_receipts'            => 'zahlungseingaenge',
   'list_of_transactions'        => 'buchungsliste',
   'male'                        => 'männlich',
   'mark as paid'                => 'als bezahlt markieren',
   'mebil - Mapping: values for #1' => 'mebil - Mapping: Werte für #1',
+  'max filesize'                => 'maximale Dateigröße',
+  'min'                         => 'min',
   'missing'                     => 'Fehlbestand',
+  'missing file for action import' => 'Es wurde keine Datei zum Hochladen ausgewählt',
+  'missing_br'                  => 'Fehl.',
   'month'                       => 'Monatliche Abgabe',
   'monthly'                     => 'monatlich',
+  'more'                        => 'mehr',
+  'natural person'              => 'natürliche Person',
+  'netto'                       => 'netto',
   'never'                       => 'niemals',
+  'new order controller'        => 'Neuer Auftrags-Controller',
   'next'                        => 'vor',
   'no'                          => 'nein',
   'no article assigned yet'     => 'noch kein Artikel zugewiesen',
+  'no backend enabled'          => 'Kein Dateimanagement-Subsystem aktiviert',
   'no bestbefore'               => 'keine Mindesthaltbarkeit',
   'no chargenumber'             => 'keine Chargennummer',
+  'no execution for this client' => 'keine Ausführung für diesen Mandanten',
+  'no shipping address'         => 'keine Lieferadresse',
   'no skonto_chart configured for taxkey #1 : #2 : #3' => 'Kein Skontokonto für Steuerschlüssel #1 : #2 : #3',
+  'no tax_id in acc_trans'      => 'Keine tax_id in acc_trans',
+  'not a valid DTVF file, expected field header start with \'Umsatz; (..) ;Konto;Gegenkonto\'' => 'Keine gültige DTVF-Datei, die erwartete Kopfzeile startet mit \'Umsatz; (..) ;Konto;Gegenkonto\'',
+  'not a valid DTVF file, expected first field in A1 \'DTVF\'' => 'Keine gültige DTVF-Datei, der erwarte Feldwert in A1 ist \'DTVF\'',
   'not configured'              => 'nicht konfiguriert',
   'not delivered'               => 'nicht geliefert',
   'not executed'                => 'nicht ausgeführt',
   'not running'                 => 'läuft nicht',
   'not set'                     => 'nicht gesetzt',
   'not shipped'                 => 'nicht geliefert',
+  'not transferred'             => 'nicht übernommen',
   'not transferred in yet'      => 'noch nicht eingelagert',
   'not transferred out yet'     => 'noch nicht ausgelagert',
   'not yet executed'            => 'Noch nicht ausgeführt',
+  'now'                         => 'jetzt',
   'number'                      => 'Nummer',
   'oe.pl::search called with unknown type' => 'oe.pl::search mit unbekanntem Typ aufgerufen',
   'old'                         => 'alt',
   'on the same day'             => 'am selben Tag',
+  'one time'                    => 'einmalig',
   'one-time execution'          => 'einmalige Ausführung',
   'only OB Transactions'        => 'nur EB-Buchungen',
   'open'                        => 'Offen',
@@ -3421,6 +4579,7 @@ $self->{texts} = {
   'our vendor number at customer' => 'Unsere Lieferanten-Nr. beim Kunden',
   'parsing csv'                 => 'Parse CSV Daten',
   'part'                        => 'Ware',
+  'part \'#\'1 in bin \'#2\' only with qty #3 (need additional #4) and chargenumber \'#5\'.' => 'Artikel \'#1\' im \'#2\' nur mit der Menge #3 (noch #4 benötig) und Chargennummer \'#5\'.',
   'part_list'                   => 'Warenliste',
   'percental'                   => 'prozentual',
   'periodic'                    => 'Aufwandsmethode',
@@ -3443,17 +4602,20 @@ $self->{texts} = {
   'quarter'                     => 'Vierteljährliche (quartalsweise) Abgabe',
   'quotation_list'              => 'angebotsliste',
   'release_material'            => 'Materialausgabebe',
+  'renew'                       => 'erneuern',
   'reorder item'                => 'Eintrag umsortieren',
   'repeated execution'          => 'wiederholte Ausführung',
   'report_generator_dispatch_to is not defined.' => 'report_generator_dispatch_to ist nicht definiert.',
   'report_generator_nextsub is not defined.' => 'report_generator_nextsub ist nicht definiert.',
   'request_quotation'           => 'Angebotsanforderung',
-  'reset'                       => 'zurücksetzen',
-  'return_material'             => 'Materialr&uuml;ckgabe',
+  'return_material'             => 'Materialrückgabe',
+  'revert deleted'              => 'löschen rückgängig',
   'rfq_list'                    => 'anfragenliste',
+  'rma_delivery_order_list'     => 'lieferscheinliste_rma',
   'running'                     => 'läuft',
   'sales tax identification number' => 'USt-IdNr.',
   'sales_delivery_order_list'   => 'lieferscheinliste_verkauf',
+  'sales_delivery_order_printer' => 'sales_delivery_order_printer',
   'sales_invoice_printer'       => 'Rechnungsdrucker',
   'sales_order'                 => 'Kundenauftrag',
   'sales_order_list'            => 'auftragsliste',
@@ -3462,17 +4624,24 @@ $self->{texts} = {
   'saved'                       => 'gespeichert',
   'saved!'                      => 'gespeichert',
   'saving data'                 => 'Speichere Daten',
+  'searched part not for purchase' => 'Gesuchter Artikel ist nicht für den Einkauf',
+  'searched part not for sale'  => 'Gesuchter Artikel ist nicht für den Verkauf',
   'semiannually'                => 'halbjährlich',
   'sent'                        => 'gesendet',
   'sent to printer'             => 'an Drucker geschickt',
   'service'                     => 'Dienstleistung',
   'service_list'                => 'dienstleistungsliste',
   'shipped'                     => 'verschickt',
+  'shipped_br'                  => 'Verschk.',
   'singular first char'         => 'S',
-  'soldtotal'                   => 'Verkaufte Anzahl',
+  'sort items'                  => 'Positionen sortieren',
+  'start upload'                => 'Hochladen beginnt',
   'stock'                       => 'Einlagerung',
-  'submit'                      => 'abschicken',
+  'stock_br'                    => 'Eingel.',
+  'stocktaking'                 => 'Inventur',
   'succeeded'                   => 'erfolgreich',
+  'sum'                         => 'Summe',
+  'supplier_delivery_order_list' => 'lieferscheinliste_beistell',
   'tax_chartaccno'              => 'Automatikkonto',
   'tax_percent'                 => 'Prozentsatz',
   'tax_rate'                    => 'Prozent',
@@ -3484,33 +4653,44 @@ $self->{texts} = {
   'taxnumber'                   => 'Automatikkonto',
   'terminated'                  => 'gekündigt',
   'time and effort based position' => 'Aufwandsposition',
+  'time_recordings'             => 'zeiterfassung',
+  'to'                          => 'bis',
   'to (date)'                   => 'bis',
   'to (set to)'                 => 'auf',
   'to (time)'                   => 'bis',
-  'to be used as template for'  => 'als neue Vorlage verwenden für',
   'transfer'                    => 'Umlagerung',
+  'transferred'                 => 'übernommen',
   'transferred in'              => 'eingelagert',
   'transferred in / out'        => 'ein- / ausgelagert',
   'transferred out'             => 'ausgelagert',
   'trial_balance'               => 'susa',
+  'true'                        => 'wahr',
   'uncleared'                   => 'Nicht abgeglichen',
   'unconfigured'                => 'unkonfiguriert',
-  'uncorrect partnumber '       => 'Unbekannte Teilenummer ',
+  'unimport'                    => 'Import rückgängig machen',
+  'unimported'                  => 'Import rückgängig gemacht',
+  'unnamed record template'     => 'unbenannte Belegvorlage',
   'until'                       => 'bis',
+  'uploaded'                    => 'Hochgeladen',
+  'uploaded Documents'          => 'Hochgeladene Dokumente',
   'use program settings'        => 'benutze Programmeinstellungen',
   'use user config'             => 'Verwende Benutzereinstellung',
   'used'                        => 'Verbraucht',
+  'used_br'                     => 'Verbr.',
   'valid from'                  => 'Gültig ab',
-  'vendor'                      => 'Lieferant',
   'vendor_invoice_list'         => 'kreditorenbuchungsliste',
   'vendor_list'                 => 'lieferantenliste',
   'waiting for job to be started' => 'warte darauf, dass der Job gestartet wird',
   'warehouse_journal_list'      => 'lagerbuchungsliste',
   'warehouse_report_list'       => 'lagerbestandsliste',
+  'warehouse_usage_list'        => 'Lagerentnahmeliste',
+  'will be set upon posting'    => 'wird beim Buchen vergeben',
+  'will be set upon saving'     => 'wird beim Speichern vergeben',
   'with skonto acc. to pt'      => 'mit Skonto nach ZB',
+  'with_skonto_pt'              => 'mit Skonto nach ZB',
   'without skonto'              => 'ohne Skonto',
+  'without_skonto'              => 'ohne Skonto',
   'working copy'                => 'Arbeitskopie',
-  'wrongformat'                 => 'Falsches Format',
   'yearly'                      => 'jährlich',
   'yes'                         => 'ja',
   'you can find professional help.' => 'finden Sie professionelle Hilfe.',
diff --git a/locale/de/more/all b/locale/de/more/all
new file mode 100644 (file)
index 0000000..237e19c
--- /dev/null
@@ -0,0 +1,18 @@
+#!/usr/bin/perl
+# -*- coding: utf-8; -*-
+# vim: fenc=utf-8
+
+use utf8;
+
+# These are additional texts for custom translations.
+# The format is the same as for the normal file all, only
+# with another key (more_texts instead of texts).
+# You can overload current translation with custom ones (see example
+# in comments).
+# The file has the form of 'english text'  => 'foreign text',
+
+$self->{more_texts} = {
+
+  #'Ship via'                    => 'Terms of delivery',
+  #'Shipping Point'              => 'Delivery times and new roman',
+}
index ccf947a..314fabc 100644 (file)
@@ -1,5 +1,17 @@
 [HTML]
-order=& " < >
+order=& \xc3\xa4 \xc3\xb6 \xc3\xbc \xc3\x84 \xc3\x96 \xc3\x9c \xc3\x9f \xe1\xba\x9e " < > \xc2\xa7 \xc2\xb2 \xc2\xb3 \xe2\x82\xac
+\xc3\xa4=&auml;
+\xc3\xb6=&ouml;
+\xc3\xbc=&uuml;
+\xc3\x84=&Auml;
+\xc3\x96=&Ouml;
+\xc3\x9c=&Uuml;
+\xc3\x9f=&szlig;
+\xe1\xba\x9e=&#7838;
+\xc2\xa7=&sect;
+\xc2\xb2=&sup2;
+\xc2\xb3=&sup3;
+\xe2\x82\xac=&euro;
 "=&quot;
 &=&amp;
 <=&lt;
@@ -15,18 +27,14 @@ order=< > \n
 >=&gt;
 \n=<br>
 
-[Template/XML]
-order=< > \n
-<=&lt;
->=&gt;
-\n=<br>
-
 [Template/LaTeX]
-order=\\ <pagebreak> & \n \r " $ <bullet> % _ # ^ { } < > £ ± ² ³ ° § ® © \xad \xa0 ➔ → ← | −
+order=\\ <pagebreak> & \n \r " $ <bullet> % _ # ^ { } < > £ ± ² ³ ° § ® © ~ \xad \xa0 ➔ → ← ↔ ↕ | − ≤ ≥ ‐ ​ Ω μ Δ λ Ø ø ‑
 \\=\\textbackslash\s
 <pagebreak>=
-"=''
 &=\\&
+\n=\\newline\s
+\r=
+"=''
 $=\\$
 <bullet>=$\\bullet$
 %=\\%
@@ -34,28 +42,40 @@ _=\\_
 # A hash mark starts a comment; therefore the line is ignored. So use
 # its hex code instead.
 \x23=\\#
+^=\\^\\\s
 {=\\{
 }=\\}
 <=$<$
 >=$>$
 £=\\pounds\s
-\n=\\newline\s
-\r=
 ±=$\\pm$
-^=\\^\\\s
 ²=$^2$
 ³=$^3$
 °=$^\\circ$
 §=\\S\s
-®=\\textregistered\s
-©=\\textcopyright\s
+®={\\textregistered}
+©={\\textcopyright}
+~={\\raisebox{0.5ex}{\\texttildelow}}
 \xad=\\-
+\xa0=~
 ➔=$\\rightarrow$
 →=$\\rightarrow$
 ←=$\\leftarrow$
-\xa0=~
-|=\\textbar\s
+↔=$\\leftrightarrow$
+↕=$\\updownarrow$
+|={\\textbar}
 −={\\textemdash}
+≤=$\\leq$
+≥=$\\geq$
+‐={}-{}
+​={\\hspace{0pt}}
+Ω=$\\Omega$
+μ={\\textmu}
+Δ=$\\Delta$
+λ=$\\lambda$
+Ø={\\O}
+ø={\\o}
+‑={}-{}
 
 [Template/OpenDocument]
 order=& < > " ' \x80 \n \r
@@ -70,7 +90,7 @@ order=& < > " ' \x80 \n \r
 \r=
 
 [filenames]
-order=ä ö ü Ä Ö Ü ß
+order=ä ö ü Ä Ö Ü ß ẞ
 ä=ae
 ö=oe
 ü=ue
@@ -78,3 +98,4 @@ order=ä ö ü Ä Ö Ü ß
 Ö=Oe
 Ü=Ue
 ß=ss
+ẞ=Ss
index ceaa769..a07f6d2 100644 (file)
@@ -17,7 +17,8 @@
 # GNU General Public License for more details.
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1335, USA.
 #
 #######################################################################
 
index 62a2696..7db3e8a 100644 (file)
@@ -11,26 +11,42 @@ use utf8;
 
 $self->{texts} = {
   ' Date missing!'              => '',
-  ' Part Number missing!'       => '',
+  ' bytes, max='                => '',
   ' missing!'                   => '',
+  '"#1" seems to be a faulty list of email addresses. After extracing addresses (#2) too many characters are left.' => '',
+  '"#1" seems to be a faulty list of email addresses. No addresses could be extracted.' => '',
   '#1 (custom variable)'        => '',
   '#1 MD'                       => '',
+  '#1 additional part(s)'       => '',
+  '#1 bank transaction bookings undone.' => '',
+  '#1 dunnings have been deleted' => '',
   '#1 h'                        => '',
-  '#1 of #2 importable objects were imported.' => '',
+  '#1 invoice(s) saved.'        => '',
   '#1 prices were updated.'     => '',
+  '#1 proposal(s) saved.'       => '',
+  '#1 proposal(s) with #2 invoice(s) saved.' => '',
+  '#1 section(s)'               => '',
+  '#1 text block(s) back'       => '',
+  '#1 text block(s) front'      => '',
+  '%'                           => '',
   '(recommended) Insert the used currencies in the system. You can simply change the name of the currencies by editing the textfields above. Do not use a name of a currency that is already in use.' => '',
   '*/'                          => '',
+  '+'                           => '',
   ', if set'                    => '',
   '---please select---'         => '',
   '. Automatically generated.'  => '',
-  '...after loggin in'          => '',
+  '...after logging in'         => '',
   '...done'                     => '',
   '...on the TODO list'         => '',
   '0% tax with taxkey'          => '',
   '1. Quarter'                  => '',
+  '2 years'                     => '',
   '2. Quarter'                  => '',
+  '3 years'                     => '',
   '3. Quarter'                  => '',
+  '4 years'                     => '',
   '4. Quarter'                  => '',
+  '5 years'                     => '',
   '<b> I DO CARE!</b> Please check create warehouse and bins and define a name for the warehouse (Bins will be created automatically) and then continue' => '',
   '<b> I DO CARE!</b> Please click back and cancel the update and come back after there has been at least one warehouse defined with bin(s).:' => '',
   '<b> I DO NOT CARE</b> Please click continue and the following data (see list) will be deleted:' => '',
@@ -38,15 +54,27 @@ $self->{texts} = {
   '<b>Automatically create new bins</b> in the following warehouse if not selected in the list above' => '',
   '<b>Default Bins Migration !READ CAREFULLY!</b>' => '',
   '<b>What</b> do you want to look for?' => '',
+  'A canceled general ledger transaction cannot be canceled again.' => '',
+  'A canceled general ledger transaction cannot be deleted.' => '',
+  'A canceled general ledger transaction cannot be posted.' => '',
+  'A canceled invoice cannot be posted.' => '',
+  'A canceled invoice cannot be used. Please undo the cancellation first.' => '',
+  'A customer with the same VAT ID already exists.' => '',
+  'A customer with the same taxnumber already exists.' => '',
   'A digit is required.'        => '',
   'A directory with the name for the new print templates exists already.' => '',
   'A lot of the usability of kivitendo has been enhanced with javascript. Although it is currently possible to use every aspect of kivitendo without javascript, we strongly recommend it. In a future version this may change and javascript may be necessary to access advanced features.' => '',
   'A lower-case character is required.' => '',
+  'A payment can only be posted for multiple invoices if the amount to post is equal to or bigger than the sum of the open amounts of the affected invoices.' => '',
   'A special character is required (valid characters: #1).' => '',
+  'A target quantitiy has to be given' => '',
+  'A transaction description is required.' => '',
   'A unit with this name does already exist.' => '',
   'A valid taxkey is missing!'  => '',
   'A variable marked as \'Deactivate by default\' isn\'t automatically added to all articles, and has to be explicitly added for each desired article in its master data tab. Only then can the variable be used for that article in the records.' => '',
   'A variable marked as \'editable\' can be changed in each quotation, order, invoice etc.' => '',
+  'A vendor with the same VAT ID already exists.' => '',
+  'A vendor with the same taxnumber already exists.' => '',
   'ADDED'                       => '',
   'AP'                          => 'Purchases',
   'AP Aging'                    => 'Creditor Aging',
@@ -55,21 +83,32 @@ $self->{texts} = {
   'AP Transaction Storno (one letter abbreviation)' => '',
   'AP Transaction with Storno (abbreviation)' => '',
   'AP Transactions'             => 'Purchase Transactions',
+  'AP template suggestions'     => '',
+  'AP transaction \'#1\' posted (ID: #2)' => '',
   'AP transactions changeable'  => '',
   'AP transactions with sales taxkeys and/or AR transactions with input taxkeys' => '',
+  'AP/AR Aging & Journal'       => '',
   'AR'                          => 'Sales',
   'AR Aging'                    => 'Debtor Aging',
   'AR Transaction'              => 'Sales Transaction',
   'AR Transaction (abbreviation)' => '',
+  'AR Transaction/AccTrans Item row names' => '',
   'AR Transactions'             => 'Sales Transactions',
+  'AR transaction \'#1\' posted (ID: #2)' => '',
   'AR transactions changeable'  => '',
   'ASSETS'                      => '',
   'ATTENTION! If you enabled this feature you can not simply turn it off again without taking care that best_before fields are emptied in the database.' => '',
   'ATTENTION! You can not simply change it from periodic to perpetual once you started posting.' => '',
   'AUTOMATICALLY MATCH BINS'    => '',
+  'Abbreviation Legend'         => '',
   'Abort'                       => '',
   'Abrechnungsnummer'           => '',
-  'Abteilung'                   => '',
+  'Absolute BB Balance'         => '',
+  'Absolute BT Balance'         => '',
+  'Acc Transaction'             => '',
+  'Acc transaction'             => '',
+  'AccTransaction'              => '',
+  'Acceptance Statuses'         => '',
   'Access rights'               => '',
   'Access to clients'           => '',
   'Account'                     => '',
@@ -105,169 +144,282 @@ $self->{texts} = {
   'Account deleted!'            => '',
   'Account for fees'            => '',
   'Account for interest'        => '',
+  'Account for workflow from purchase order to ap transaction' => '',
   'Account number'              => '',
-  'Account number #1, bank code #2, #3' => '',
   'Account number not unique!'  => '',
+  'Account number of the goal/source' => '',
   'Account saved!'              => '',
-  'Accounting Group deleted!'   => '',
-  'Accounting Group saved!'     => '',
+  'Accounting desired'          => '',
   'Accounting method'           => '',
   'Accrual'                     => '',
   'Accrual accounting'          => '',
+  'Action'                      => '',
+  'Actions'                     => '',
+  'Activate kivitendo module'   => '',
   'Active'                      => '',
+  'Active shops:'               => '',
   'Active?'                     => '',
   'Add'                         => '',
   'Add AP Transaction'          => '',
   'Add AR Transaction'          => '',
   'Add Account'                 => '',
-  'Add Accounting Group'        => '',
   'Add Accounts Payables Transaction' => 'Add Purchase Transaction',
   'Add Accounts Receivables Transaction' => 'Add Sales Transaction',
   'Add Assembly'                => '',
-  'Add Buchungsgruppe'          => '',
+  'Add Assortment'              => '',
   'Add Client'                  => '',
   'Add Credit Note'             => '',
+  'Add Credit Note for this dunning level:' => '',
   'Add Customer'                => '',
+  'Add Customer/Vendor Number as a reference add-on for SEPA export.' => '',
   'Add Delivery Note'           => '',
   'Add Delivery Order'          => '',
+  'Add Document from \'#1\''    => '',
   'Add Dunning'                 => '',
-  'Add Exchangerate'            => '',
+  'Add Final Invoice'           => '',
   'Add Follow-Up'               => '',
   'Add Follow-Up for #1'        => '',
   'Add General Ledger Transaction' => '',
-  'Add Group'                   => '',
-  'Add Language'                => '',
-  'Add Lead'                    => '',
-  'Add Machine'                 => '',
+  'Add Invoice for Advance Payment' => '',
+  'Add Letter'                  => '',
   'Add Part'                    => '',
   'Add Price Factor'            => '',
-  'Add Pricegroup'              => '',
   'Add Printer'                 => '',
   'Add Project'                 => '',
   'Add Purchase Delivery Order' => '',
   'Add Purchase Order'          => '',
   'Add Quotation'               => '',
   'Add RFQ'                     => '',
+  'Add RMA Delivery Order'      => '',
   'Add Request for Quotation'   => '',
+  'Add Requirement Spec'        => '',
+  'Add Requirement Spec Template' => '',
   'Add Sales Delivery Order'    => '',
   'Add Sales Invoice'           => '',
   'Add Sales Order'             => '',
   'Add Service'                 => '',
-  'Add Service Contract'        => '',
   'Add Storno Credit Note'      => '',
+  'Add Supplier Delivery Order' => '',
   'Add Transaction'             => '',
   'Add User'                    => '',
   'Add User Group'              => '',
   'Add Vendor'                  => '',
   'Add Vendor Invoice'          => '',
   'Add Warehouse'               => '',
-  'Add and edit units'          => '',
+  'Add acceptance status'       => '',
   'Add bank account'            => '',
+  'Add booking group'           => '',
+  'Add business'                => '',
+  'Add complexity'              => '',
+  'Add counted'                 => '',
+  'Add custom data export query' => '',
   'Add custom variable'         => '',
+  'Add department'              => '',
+  'Add document for AP transactions' => '',
+  'Add document for AR transactions' => '',
+  'Add document for GL transactions' => '',
+  'Add document for Purchase invoices' => '',
+  'Add empty line (csv_import)' => '',
+  'Add function block'          => '',
+  'Add greeting'                => '',
+  'Add headers from last uploaded file (csv_import)' => '',
+  'Add invoices'                => '',
+  'Add language'                => '',
   'Add link: select records to link with' => '',
   'Add linked record'           => '',
   'Add links'                   => '',
+  'Add multiple items'          => '',
   'Add new currency'            => '',
   'Add new custom variable'     => '',
+  'Add new price rule item'     => '',
+  'Add new record template'     => '',
   'Add note'                    => '',
+  'Add open Credit Notes'       => '',
+  'Add part'                    => '',
+  'Add part classification'     => '',
+  'Add partsgroup'              => '',
+  'Add picture'                 => '',
+  'Add picture to text block'   => '',
+  'Add pre-defined text'        => '',
+  'Add pricegroup'              => '',
+  'Add project status'          => '',
+  'Add project type'            => '',
+  'Add requirement spec status' => '',
+  'Add requirement spec type'   => '',
+  'Add risk level'              => '',
+  'Add section'                 => '',
+  'Add shop'                    => '',
+  'Add sub function block'      => '',
+  'Add taxzone'                 => '',
+  'Add text block'              => '',
+  'Add time recording article'  => '',
+  'Add title'                   => '',
   'Add unit'                    => '',
+  'Added sections and function blocks: #1' => '',
+  'Added text blocks: #1'       => '',
+  'Addition'                    => '',
+  'Additional Billing Address'  => '',
+  'Additional Billing Addresses' => '',
+  'Additional articles'         => '',
+  'Additional articles actions' => '',
+  'Additionally the invoice is marked for direct debit and would have been checked automatically had the bank information been entered.' => '',
+  'Additionally the invoice is not marked for direct debit and would have been checked automatically had the bank information been entered.' => '',
   'Address'                     => '',
-  'Admin'                       => '',
+  'Address deleted.'            => '',
+  'Address is in use and was flagged invalid.' => '',
   'Administration'              => '',
   'Administration area'         => '',
   'Advance turnover tax return' => '',
+  'Advance turnover tax return only valid for SKR03 or SKR04' => '',
+  'After closed period'         => '',
   'Aktion'                      => '',
   'All'                         => '',
   'All Accounts'                => '',
+  'All Data'                    => '',
+  'All as list'                 => '',
   'All changes in that file have been reverted.' => '',
   'All clients'                 => '',
+  'All employees'               => '',
   'All general ledger entries'  => '',
   'All groups'                  => '',
-  'All of the exports you have selected were already closed.' => '',
+  'All modules'                 => '',
   'All partsgroups'             => '',
+  'All pay postings successfully imported.' => '',
+  'All payments have already been posted.' => '',
+  'All payments must be posted before the payment list can be downloaded.' => '',
+  'All phone numbers'           => '',
+  'All price sources'           => '',
   'All reports'                 => '',
   'All the other clients will start with an empty set of WebDAV folders.' => '',
   'All the selected exports have already been closed, or all of their items have already been executed.' => '',
+  'All transactions'            => '',
   'All units have either no or exactly one base unit of which they are multiples.' => '',
   'All users'                   => '',
+  'Allocations didn\'t pass constraints' => '',
   'Allow access'                => '',
+  'Allow conversion from sales orders to sales invoices' => '',
+  'Allow conversion from sales quotations to sales invoices' => '',
+  'Allow direct creation of new purchase delivery orders' => '',
+  'Allow direct creation of new purchase invoices' => '',
   'Allow the following users access to my follow-ups:' => '',
-  'Alternatively you can create a new part which will then be selected.' => '',
+  'Allow to delete generated printfiles' => '',
+  'Already counted'             => '',
+  'Already imported entries (duplicates)' => '',
+  'Already imported: '          => '',
+  'Always edit assembly items (user can change/delete items even if assemblies are already produced)' => '',
+  'Always edit assortment items (user can change/delete items even if assortments are already used)' => '',
+  'Always save orders with a projectnumber (create new projects)' => '',
   'Amended Advance Turnover Tax Return' => '',
-  'Amended Advance Turnover Tax Return (Nr. 10)' => '',
   'Amount'                      => '',
   'Amount (for verification)'   => '',
+  'Amount BB'                   => '',
+  'Amount BT'                   => '',
   'Amount Due'                  => '',
   'Amount and net amount are calculated by kivitendo. "verify_amount" and "verify_netamount" can be used for sanity checks.' => '',
+  'Amount has wrong format.'    => '',
+  'Amount less skonto'          => '',
   'Amount payable'              => '',
   'Amount payable less discount' => '',
+  'Amounts differ too much'     => '',
+  'An error occurred while transferring the file.' => '',
+  'An error occurred. Letter could not be deleted.' => '',
   'An exception occurred during execution.' => '',
   'An invalid character was used (invalid characters: #1).' => '',
   'An invalid character was used (valid characters: #1).' => '',
   'An upper-case character is required.' => '',
+  'Analyze'                     => '',
   'Annotations'                 => '',
   'Any stock contents containing a best before date will be impossible to stock out otherwise.' => '',
   'Ap aging on %s'              => '',
-  'Application Error. No Format given' => '',
   'Application Error. Wrong Format' => '',
+  'Apply'                       => '',
+  'Apply customer'              => '',
   'Apply to all parts'          => '',
   'Apply to all transfers'      => '',
-  'Apply to parts without buchungsgruppe' => '',
+  'Apply to parts without booking group' => '',
   'Apply to transfers without bin' => '',
   'Apply to transfers without comment' => '',
   'Apply to transfers without warehouse' => '',
+  'Apply year-end bookings'     => '',
   'Applying #1:'                => '',
-  'Appointment Category'        => '',
-  'Appointments'                => '',
   'Approximately #1 prices will be updated.' => '',
   'Apr'                         => '',
   'April'                       => '',
   'Ar aging on %s'              => '',
+  'Are you sure to update all positions from master data?' => '',
+  'Are you sure to update this position from master data?' => '',
   'Are you sure you want to delete Invoice Number' => '',
-  'Are you sure you want to delete Transaction' => '',
-  'Are you sure you want to delete this background job?' => '',
-  'Are you sure you want to delete this business?' => '',
-  'Are you sure you want to delete this delivery term?' => '',
-  'Are you sure you want to delete this department?' => '',
-  'Are you sure you want to delete this payment term?' => '',
+  'Are you sure you want to delete this letter?' => '',
   'Are you sure you want to remove the marked entries from the queue?' => '',
   'Are you sure you want to update the prices' => '',
+  'Are you sure you want to update the selected record template with the current values? This cannot be undone.' => '',
   'Are you sure?'               => '',
+  'Article'                     => '',
   'Article Code'                => '',
-  'Article Code missing!'       => '',
+  'Article classification'      => '',
   'Article type'                => '',
+  'Articles'                    => '',
   'As a result, the saved onhand values of the present goods can be stored into a warehouse designated by you, or will be reset for a proper warehouse tracking' => '',
   'Assemblies'                  => '',
-  'Assemblies can not be imported (yet). But the type column is used for sanity checks on price updates in order to prevent that articles with the wrong type will be updated.' => '',
   'Assembly'                    => '',
-  'Assembly Description'        => '',
-  'Assembly Number'             => '',
+  'Assembly (typeabbreviation)' => 'A',
+  'Assembly Item Qty'           => '',
+  'Assembly Last Cost'          => '',
   'Assembly Number missing!'    => '',
+  'Assembly creation transfers services' => '',
+  'Assembly creation warehouse dependent' => '',
+  'Assembly items'              => '',
   'Asset'                       => '',
   'Assets'                      => '',
+  'Assign'                      => '',
+  'Assign article'              => '',
+  'Assign invoice'              => '',
+  'Assign the following article to all sections' => '',
+  'Assigned'                    => '',
+  'Assigned invoices with amount' => '',
+  'Assigned order must be a sales order.' => '',
+  'Assignment of articles to sections' => '',
   'Assistant for general ledger corrections' => '',
+  'Assortment'                  => '',
+  'Assortment (typeabbreviation)' => 'As',
+  'Assortment items'            => '',
   'Assume Tax Consultant Data in Tax Computation?' => '',
   'At least'                    => '',
+  'At least #1 invoice(s) not saved' => '',
   'At least one Perl module that kivitendo ERP requires for running is not installed on your system.' => '',
-  'At least one of the columns #1, customer, customernumber, vendor, vendornumber (depending on the target table) is required for matching the entry to an existing customer or vendor.' => '',
+  'At least one of the columns #1, customer, customernumber, customer_gln, vendor, vendornumber, vendor_gln (depending on the target table) is required for matching the entry to an existing customer or vendor.' => '',
   'At most'                     => '',
+  'At position'                 => '',
   'At the moment the transaction looks like this:' => '',
   'Attach PDF:'                 => '',
+  'Attached Filename'           => '',
   'Attachment'                  => '',
   'Attachment name'             => '',
+  'Attachments'                 => '',
   'Attempt to call an undefined sub named \'%s\'' => '',
   'Audit Control'               => '',
   'Aug'                         => '',
   'August'                      => '',
+  'Austria'                     => '',
   'Authentification database creation' => '',
   'Authentification tables creation' => '',
+  'Author'                      => '',
   'Auto Send?'                  => '',
+  'Automatic date calculation'  => '',
+  'Automatic deletion of leading, trailing and excessive (repetitive) spaces in customer or vendor names' => '',
+  'Automatic deletion of leading, trailing and excessive (repetitive) spaces in part description and part notes. Affects the CSV import as well.' => '',
+  'Automatic skonto chart purchase' => '',
+  'Automatic skonto chart sales' => '',
   'Automatically created invoice for fee and interest for dunning %s' => '',
   'Available'                   => '',
+  'Available Prices'            => '',
   'Available qty'               => '',
+  'Available to all users'      => '',
   'BALANCE SHEET'               => '',
+  'BB Balance'                  => '',
   'BIC'                         => '',
   'BOM'                         => '',
+  'BT Balance'                  => '',
   'BWA'                         => '',
   'Back'                        => '',
   'Back to login'               => '',
@@ -276,71 +428,108 @@ $self->{texts} = {
   'Background jobs and task server' => '',
   'Balance'                     => '',
   'Balance Sheet'               => '',
+  'Balance accounts'            => '',
+  'Balance sheet date'          => '',
+  'Balance startdate method'    => '',
+  'Balance with CB'             => '',
+  'Balances'                    => '',
   'Balancing'                   => '',
   'Bank'                        => '',
+  'Bank Account Id Number (Swiss)' => '',
   'Bank Code'                   => '',
   'Bank Code (long)'            => '',
   'Bank Code Number'            => '',
   'Bank Connection Tax Office'  => '',
   'Bank Connections'            => '',
+  'Bank Import'                 => '',
+  'Bank Transaction'            => '',
+  'Bank Transaction is in a closed period.' => '',
+  'Bank account'                => '',
+  'Bank account id number invalid. Must be 6 digits.' => '',
   'Bank accounts'               => '',
   'Bank code'                   => '',
+  'Bank code of the goal/source' => '',
   'Bank collection amount'      => '',
   'Bank collection payment list for export #1' => '',
   'Bank collection via SEPA'    => '',
   'Bank collections via SEPA'   => '',
+  'Bank transaction'            => '',
+  'Bank transactions'           => '',
+  'Bank transactions MT940'     => '',
+  'Bank transactions that either only have warnings or no message at all have been posted.' => '',
+  'Bank transactions with errors have not been posted.' => '',
   'Bank transfer amount'        => '',
   'Bank transfer payment list for export #1' => '',
   'Bank transfer via SEPA'      => '',
   'Bank transfers via SEPA'     => '',
+  'Base Transaction Value'      => '',
+  'Base Transaction Value Currency Code' => '',
   'Base unit'                   => '',
   'Basic Data'                  => '',
+  'Basic Settings for the Requirement Spec' => '',
+  'Basic Settings for the Requirement Spec Template' => '',
+  'Basic settings'              => '',
+  'Basic settings actions'      => '',
+  'Basis of calculation'        => '',
   'Batch Printing'              => '',
   'Bcc'                         => '',
   'Bcc E-mail'                  => '',
   'Because the useability gets worse if one partnumber is used for several parts (for example if you are searching a position for an invoice), partnumbers should be unique.' => '',
-  'Belegnummer'                 => '',
+  'Before saving a sales order, this article will be checked and a warning is generated.' => '',
+  'Belgium'                     => '',
   'Beratername'                 => '',
   'Beraternummer'               => '',
   'Best Before'                 => '',
-  'Bestandskonto'               => '',
+  'Best Discount'               => '',
+  'Best Price'                  => '',
   'Bilanz'                      => '',
+  'Billable amount'             => '',
+  'Billed amount'               => '',
+  'Billed extra expenses'       => '',
   'Billing Address'             => '',
+  'Billing Periodicity'         => '',
+  'Billing/shipping address (GLN)' => '',
   'Billing/shipping address (city)' => '',
   'Billing/shipping address (country)' => '',
   'Billing/shipping address (street)' => '',
   'Billing/shipping address (zipcode)' => '',
   'Bin'                         => '',
   'Bin (database ID)'           => '',
+  'Bin (name)'                  => '',
   'Bin From'                    => '',
   'Bin List'                    => '',
   'Bin To'                      => '',
   'Binding to the LDAP server as "#1" failed. Please check config/kivitendo.conf.' => '',
+  'Bins'                        => '',
   'Bins saved.'                 => '',
   'Bins that have been used in the past cannot be deleted anymore. For these bins there\'s no checkbox in the &quot;Delete&quot; column.' => '',
   'Birthday'                    => '',
   'Birthday (after conversion)' => '',
   'Birthday (before conversion)' => '',
   'Bis'                         => '',
-  'Bis Konto: '                 => '',
-  'Block'                       => '',
   'Body'                        => '',
   'Body:'                       => '',
-  'Booking Date'                => '',
+  'Booked'                      => '',
+  'Booking group'               => '',
+  'Booking group #1 needs a valid expense account' => '',
+  'Booking group #1 needs a valid income account' => '',
+  'Booking group #1 needs a valid inventory account' => '',
+  'Booking group (database ID)' => '',
+  'Booking group (name)'        => '',
+  'Booking groups'              => '',
+  'Booking needs at least one debit and one credit booking!' => '',
+  'Bookinggroup/Tax'            => '',
   'Books are open'              => '',
   'Books closed up to'          => '',
   'Boolean variables: If the default value is non-empty then the checkbox will be checked by default and unchecked otherwise.' => '',
   'Both'                        => '',
+  'Both-sided'                  => '',
   'Bottom'                      => '',
   'Bought'                      => '',
+  'Break down by'               => '',
   'Break up the update and contact a service provider.' => '',
-  'Buchungsdatum'               => '',
-  'Buchungsgruppe'              => '',
-  'Buchungsgruppe (database ID)' => '',
-  'Buchungsgruppe (name)'       => '',
-  'Buchungsgruppen'             => '',
-  'Buchungskonto'               => '',
-  'Buchungsnummer'              => '',
+  'Business'                    => '',
+  'Business Discount'           => '',
   'Business Number'             => '',
   'Business Volume'             => '',
   'Business evaluation'         => '',
@@ -350,54 +539,61 @@ $self->{texts} = {
   'CANCELED'                    => '',
   'CB Transaction'              => '',
   'CB Transactions'             => '',
+  'CC to Employee'              => '',
+  'CN'                          => '',
   'CR'                          => '',
-  'CRM'                         => '',
-  'CRM admin'                   => '',
-  'CRM create customers, vendors and contacts' => '',
-  'CRM follow up'               => '',
-  'CRM know how'                => '',
-  'CRM notices'                 => '',
-  'CRM opportunity'             => '',
-  'CRM optional software'       => '',
-  'CRM other'                   => '',
-  'CRM search'                  => '',
-  'CRM send email'              => '',
-  'CRM services'                => '',
-  'CRM status'                  => '',
-  'CRM termin'                  => '',
-  'CRM user'                    => '',
   'CSS style for pictures'      => '',
+  'CSV Export successful!'      => '',
+  'CSV export'                  => '',
   'CSV export -- options'       => '',
+  'CSV import: additional billing addresses' => '',
+  'CSV import: ar transactions' => '',
+  'CSV import: bank transactions' => '',
   'CSV import: contacts'        => '',
   'CSV import: customers and vendors' => '',
+  'CSV import: delivery orders' => '',
   'CSV import: inventories'     => '',
   'CSV import: orders'          => '',
   'CSV import: parts and services' => '',
   'CSV import: projects'        => '',
   'CSV import: shipping addresses' => '',
+  'CTI settings'                => '',
   'Calculate'                   => '',
-  'Can not create that quantity with current stock' => '',
+  'Calculate due date automatically' => '',
+  'Calling #1 now'              => '',
+  'Can only delete the "Storno zu" part of the cancellation pair.' => '',
+  'Can only save template if amounts,i.e. 1 for debit and credit are set.' => '',
+  'Can\'t connect to shop. #1'  => '',
+  'Can\'t load item without a valid part.id' => '',
   'Cancel'                      => '',
   'Cancel Accounts Payables Transaction' => '',
   'Cancel Accounts Receivables Transaction' => '',
+  'Cancelling is disallowed. Either undo or balance the current payments until the open amount matches the invoice amount' => '',
+  'Cannot Post AP transaction with tax included!' => '',
+  'Cannot add Booking, reason: #1 DB: #2 ' => '',
+  'Cannot allocate parts.'      => '',
+  'Cannot change transaction in a closed period!' => '',
   'Cannot check correct WebDAV folder' => '',
+  'Cannot convert date.'        => '',
   'Cannot delete account!'      => '',
   'Cannot delete customer!'     => '',
   'Cannot delete default account!' => '',
   'Cannot delete delivery order!' => '',
   'Cannot delete invoice!'      => '',
-  'Cannot delete item!'         => '',
   'Cannot delete order!'        => '',
   'Cannot delete quotation!'    => '',
   'Cannot delete transaction!'  => '',
   'Cannot delete vendor!'       => '',
   'Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.' => '',
+  'Cannot get shippingOrderAddressId for #1' => '',
   'Cannot have a value in both Debit and Credit!' => '',
   'Cannot post Payment!'        => '',
   'Cannot post Receipt!'        => '',
   'Cannot post a transaction without a value!' => '',
   'Cannot post invoice and/or transfer out! Error message:' => '',
   'Cannot post invoice for a closed period!' => '',
+  'Cannot post invoice for advance payment with more than one tax' => '',
+  'Cannot post invoice for advance payment with taxincluded' => '',
   'Cannot post invoice!'        => '',
   'Cannot post payment for a closed period!' => '',
   'Cannot post payment!'        => '',
@@ -408,35 +604,59 @@ $self->{texts} = {
   'Cannot post transaction!'    => '',
   'Cannot process payment for a closed period!' => '',
   'Cannot remove files!'        => '',
+  'Cannot revert a versioned copy.' => '',
+  'Cannot safely book imported bank transactions due to lax posting settings for payments' => '',
+  'Cannot safely unlink bank transactions, please set the posting configuration for payments to unchangeable.' => '',
   'Cannot save account!'        => '',
   'Cannot save order!'          => '',
   'Cannot save preferences!'    => '',
   'Cannot save quotation!'      => '',
+  'Cannot send E-mail without customer given' => '',
+  'Cannot send E-mail without vendor given' => '',
+  'Cannot stock negative amounts' => '',
+  'Cannot stock without amount' => '',
+  'Cannot storno invoice for a closed period!' => '',
   'Cannot storno storno invoice!' => '',
+  'Cannot transfer #1 qty with #2 serial number(s)' => '',
+  'Cannot transfer negative entries.' => '',
   'Cannot transfer negative quantities.' => '',
   'Cannot transfer. <br> Reason:<br>#1' => '',
+  'Cannot undo delivery order transfer!' => '',
+  'Cannot unlink payment for a closed period!' => '',
+  'Carry over account for year-end closing' => '',
   'Carry over shipping address' => '',
   'Cash'                        => '',
   'Cash accounting'             => '',
   'Cash basis accounting'       => '',
-  'Catalog'                     => '',
+  'Category'                    => '',
   'Cc'                          => '',
   'Cc E-mail'                   => '',
   'Change default bin for this parts' => '',
   'Change kivitendo installation settings (most entries in the \'System\' menu)' => '',
-  'Change representative to'    => '',
+  'Changed sections and function blocks: #1' => '',
+  'Changed text blocks: #1'     => '',
   'Changes in this block are only sensible if the account is NOT a summary account AND there exists one valid taxkey. To select both Receivables and Payables only make sense for Payment / Receipt (i.e. account cash).' => '',
   'Changes to Receivables and Payables are only possible if no transactions to this account are posted yet.' => '',
+  'Changing general ledger transaction has been disabled in the configuration.' => '',
+  'Changing invoices has been disabled in the configuration.' => '',
+  'Charge'                      => '',
   'Charge Number'               => '',
   'Charge number'               => '',
+  'Chargenumbers'               => '',
   'Charset'                     => '',
   'Chart'                       => '',
   'Chart Type'                  => '',
   'Chart balance'               => '',
+  'Chart configuration overview regarding reports' => '',
+  'Chart list'                  => '',
   'Chart of Accounts'           => '',
+  'Chart picker'                => '',
   'Chartaccounts connected to this Tax:' => '',
+  'Charts'                      => '',
   'Check'                       => 'Cheque',
+  'Check Api'                   => '',
   'Check Details'               => '',
+  'Check connectivity'          => '',
   'Check for duplicates'        => '',
   'Check on ap transaction'     => '',
   'Check on ar transaction'     => '',
@@ -444,84 +664,129 @@ $self->{texts} = {
   'Check on purchase invoice'   => '',
   'Check on sales invoice'      => '',
   'Checks'                      => '',
+  'Choose "continue" if you want to use this value. Choose "cancel" otherwise.' => '',
   'Choose Customer'             => '',
   'Choose Outputformat'         => '',
   'Choose Vendor'               => '',
   'Choose a Tax Number'         => '',
+  'Choose bank account for reconciliation' => '',
   'City'                        => '',
+  'Clear'                       => '',
+  'Clear fields'                => '',
   'Cleared Balance'             => '',
+  'Cleared/uncleared only'      => '',
   'Clearing Tax Received (No 71)' => '',
+  'Clearing account for advance payments' => '',
   'Client'                      => '',
   'Client #1'                   => '',
   'Client Configuration'        => '',
   'Client Configuration saved!' => '',
   'Client administration: configuration, editing templates, task server control, background jobs (remaining entries in the \'System\' menu)' => '',
+  'Client assignment'           => '',
   'Client list'                 => '',
   'Client name'                 => '',
   'Client to assign the existing WebDAV folders to' => '',
   'Client to configure the printers for' => '',
   'Clients this Group is valid for' => '',
   'Clients this user has access to' => '',
-  'Close'                       => '',
   'Close Books up to'           => '',
+  'Close Details'               => '',
   'Close Flash'                 => '',
   'Close SEPA exports'          => '',
   'Close Window'                => '',
   'Close window'                => '',
   'Closed'                      => '',
+  'Closing Balance'             => '',
   'Collective Orders only work for orders from one customer!' => '',
   'Column name'                 => '',
   'Comma'                       => '',
   'Comment'                     => '',
+  'Commercial court'            => '',
   'Company'                     => '',
   'Company Name'                => '',
   'Company name'                => '',
+  'Company name and address'    => '',
   'Company settings'            => '',
+  'Company\'s email signature'  => '',
   'Compare to'                  => '',
+  'Complexities'                => '',
+  'Complexity'                  => '',
+  'Component Test'              => '',
   'Configuration'               => '',
   'Configuration of individual TODO items' => '',
   'Configure'                   => '',
   'Confirm!'                    => '',
   'Confirmation'                => '',
   'Contact'                     => '',
+  'Contact Departments'         => '',
   'Contact Person'              => '',
   'Contact Person (database ID)' => '',
   'Contact Person (name)'       => '',
+  'Contact Titles'              => '',
   'Contact deleted.'            => '',
   'Contact is in use and was flagged invalid.' => '',
   'Contact person (surname)'    => '',
   'Contact persons'             => '',
+  'Contact to send to'          => '',
   'Contacts'                    => '',
+  'Content'                     => '',
   'Continue'                    => '',
   'Contra'                      => '',
+  'Contra Account'              => '',
+  'Contrary to Reduced Master Data this will be shown as discount in records.' => '',
   'Conversion of "birthday" contact person attribute' => '',
+  'Conversion to PDF failed: #1' => '',
+  'Conversion:'                 => '',
+  'Converting to deliveryorder' => '',
   'Copies'                      => '',
+  'Copy'                        => '',
+  'Copy address from master data' => '',
   'Copy file from #1 to #2 failed: #3' => '',
+  'Copy requirement spec'       => '',
+  'Copy template'               => '',
+  'Correct counted'             => '',
   'Correct taxkey'              => '',
+  'Cost Center'                 => '',
   'Costs'                       => '',
+  'Could not create new project #1' => '',
+  'Could not extract Factur-X/ZUGFeRD data, data and error message:' => '',
+  'Could not find an entry for this part in the pricegroup.' => '',
   'Could not load class #1 (#2): "#3"' => '',
   'Could not load class #1, #2' => '',
   'Could not load employee'     => '',
+  'Could not load this business' => '',
+  'Could not load this customer' => '',
+  'Could not load this draft'   => '',
+  'Could not load this vendor'  => '',
   'Could not print dunning.'    => '',
+  'Could not reconcile chosen elements!' => '',
   'Could not spawn ghostscript.' => '',
   'Could not spawn the printer command.' => '',
   'Could not update prices!'    => '',
   'Country'                     => '',
-  'Create Assembly'             => '',
+  'Create'                      => '',
   'Create Chart of Accounts'    => '',
   'Create Dataset'              => '',
   'Create Date'                 => '',
+  'Create HTML'                 => '',
+  'Create PDF'                  => '',
   'Create a new background job' => '',
-  'Create a new business'       => '',
   'Create a new client'         => '',
   'Create a new delivery term'  => '',
-  'Create a new department'     => '',
   'Create a new group'          => '',
+  'Create a new part'           => '',
   'Create a new payment term'   => '',
+  'Create a new price rule'     => '',
   'Create a new printer'        => '',
   'Create a new project'        => '',
+  'Create a new project and link to it.' => '',
+  'Create a new purchase price rule' => '',
+  'Create a new requirement spec' => '',
+  'Create a new requirement spec template' => '',
+  'Create a new sales price rule' => '',
   'Create a new user'           => '',
   'Create a new user group'     => '',
+  'Create a new version'        => '',
   'Create and edit RFQs'        => '',
   'Create and edit dunnings'    => '',
   'Create and edit invoices and credit notes' => '',
@@ -529,31 +794,59 @@ $self->{texts} = {
   'Create and edit projects'    => '',
   'Create and edit purchase delivery orders' => '',
   'Create and edit purchase orders' => '',
+  'Create and edit requirement specs' => '',
   'Create and edit sales delivery orders' => '',
   'Create and edit sales orders' => '',
   'Create and edit sales quotations' => '',
+  'Create and edit shopparts'   => 'Create and edit the parts assigned to a web shop',
   'Create and edit vendor invoices' => '',
+  'Create and edit webshops'    => '',
+  'Create and print all invoices' => '',
+  'Create and print invoices'   => '',
+  'Create and print invoices for all delivery orders matching the filter' => '',
+  'Create and print invoices for all selected delivery orders' => '',
+  'Create and send a new printout for this record' => '',
   'Create bank collection'      => '',
   'Create bank collection via SEPA XML' => '',
   'Create bank transfer'        => '',
   'Create bank transfer via SEPA XML' => '',
   'Create customers and vendors. Edit all vendors. Edit all customers' => '',
   'Create customers and vendors. Edit all vendors. Edit only customers where salesman equals employee (login)' => '',
+  'Create first invoice on'     => '',
+  'Create invoice'              => '',
   'Create invoice?'             => '',
   'Create new'                  => '',
-  'Create new background job'   => '',
-  'Create new business'         => '',
   'Create new client #1'        => '',
-  'Create new department'       => '',
-  'Create new payment term'     => '',
+  'Create new quotation or order' => '',
+  'Create new quotation/order'  => '',
+  'Create new qutoation/order'  => '',
   'Create new templates from master templates' => '',
+  'Create new version'          => '',
+  'Create one from the context menu by right-clicking on this text.' => '',
+  'Create order'                => '',
+  'Create sales invoices with Factur-X/ZUGFeRD data' => '',
+  'Create sales invoices with Swiss QR-bill' => '',
   'Create tables'               => '',
+  'Create variant IBAN without reference' => '',
+  'Create variant QR-IBAN with QR reference' => '',
+  'Create with profile \'Factur-X 1.0.05/ZUGFeRD 2.1.1 extended\'' => '',
+  'Create with profile \'Factur-X 1.0.05/ZUGFeRD 2.1.1 extended\' (test mode)' => '',
+  'Create with profile \'XRechnung 2.0.0\'' => '',
+  'Create with profile \'XRechnung 2.0.0\' (test mode)' => '',
+  'Create, edit and list time recordings' => '',
   'Created by'                  => '',
   'Created for'                 => '',
   'Created on'                  => '',
+  'Creating Documents'          => '',
+  'Creating Factur-X/ZUGFeRD invoices is not enabled for this customer.' => '',
+  'Creating invoices'           => '',
+  'Creating the PDF failed:'    => '',
+  'Creation Date'               => '',
   'Credit'                      => '',
   'Credit (one letter abbreviation)' => '',
   'Credit Account'              => '',
+  'Credit Account Name'         => '',
+  'Credit Amount'               => '',
   'Credit Limit'                => '',
   'Credit Limit exceeded!!!'    => '',
   'Credit Note'                 => '',
@@ -561,8 +854,10 @@ $self->{texts} = {
   'Credit Note Number'          => '',
   'Credit Starting Balance'     => '',
   'Credit Tax'                  => '',
+  'Credit Tax (lit)'            => '',
   'Credit Tax Account'          => '',
   'Credit note (one letter abbreviation)' => '',
+  'Credit notes cannot be converted into other credit notes.' => '',
   'Cumulated or averaged values' => '',
   'Curr'                        => '',
   'Currencies'                  => '',
@@ -573,62 +868,105 @@ $self->{texts} = {
   'Currency names must not be empty.' => '',
   'Current / Next Level'        => '',
   'Current Earnings'            => '',
+  'Current Employee'            => '',
   'Current assets account'      => '',
   'Current filter'              => '',
+  'Current picture'             => '',
   'Current profile'             => '',
   'Current status'              => '',
+  'Current status:'             => '',
+  'Current user\'s login'       => '',
   'Current value:'              => '',
+  'Current version'             => '',
+  'Current year'                => '',
+  'Currently #1 delivery orders can be converted into invoices and printed.' => '',
+  'Custom Billing Address'      => '',
+  'Custom CSV format'           => '',
   'Custom Variables'            => '',
+  'Custom data export'          => '',
+  'Custom shipto'               => '',
   'Custom variables for module' => '',
   'Customer'                    => '',
   'Customer (database ID)'      => '',
   'Customer (name)'             => '',
+  'Customer Discount'           => '',
+  'Customer GLN'                => '',
   'Customer Master Data'        => '',
   'Customer Name'               => '',
   'Customer Number'             => '',
   'Customer Order Number'       => '',
+  'Customer Part Number'        => '',
+  'Customer Price'              => '',
+  'Customer Proposals'          => '',
   'Customer deleted!'           => '',
   'Customer details'            => '',
   'Customer missing!'           => '',
-  'Customer not on file or locked!' => '',
-  'Customer not on file!'       => '',
+  'Customer must not be empty.' => '',
+  'Customer not found'          => '',
+  'Customer number invalid. Must be less then or equal to 6 digits after prefix.' => '',
+  'Customer of assigned order must match customer.' => '',
+  'Customer of assigned project must match customer.' => '',
   'Customer saved'              => '',
   'Customer saved!'             => '',
+  'Customer specific Price'     => '',
   'Customer type'               => '',
   'Customer variables'          => '',
+  'Customer\'s Mandate Date of Signature' => '',
+  'Customer\'s SEPA mandator ID' => '',
+  'Customer\'s current maximum dunning level: #1' => '',
+  'Customer\'s/vendor\'s BIC'   => '',
+  'Customer\'s/vendor\'s IBAN'  => '',
+  'Customer\'s/vendor\'s account number' => '',
+  'Customer\'s/vendor\'s bank'  => '',
+  'Customer\'s/vendor\'s bank code' => '',
   'Customer/Vendor'             => '',
   'Customer/Vendor (database ID)' => '',
   'Customer/Vendor Name'        => '',
   'Customer/Vendor Number'      => '',
+  'Customer/Vendor/Remote name' => '',
   'Customername'                => '',
+  'Customernumber'              => '',
   'Customernumberinit'          => '',
+  'Customerorderlock'           => '',
   'Customers'                   => '',
   'Customers and vendors'       => '',
+  'Customers: VAT ID / taxnumber unique' => '',
   'Customized Report'           => '',
+  'Cutoff Date'                 => '',
+  'Czech Republic'              => '',
+  'DATEV'                       => '',
   'DATEV - Export Assistent'    => '',
+  'DATEV - Pay Postings Import' => '',
   'DATEV Angaben'               => '',
   'DATEV Export'                => '',
-  'DATEV check configuration'   => '',
   'DATEV check returned errors:' => '',
+  'DATEV configuration'         => '',
+  'DATEV expects the encoding to be Western Europe conform (LATIN-1, cp1252). By setting this to "Strict and halt" the DATEV export halts with a error if there is a single character in "Posting Text" which is not LATIN-1 encodeable. By setting this to "Strict but replace" kivitendo will replace the character with a similar one and the export will simply warn about those fields. By setting this to relaxed (UTF-8) the DATEV export encoding will be in kivitendo (UTF-8) encoded and the external import program has to handle this (this may work for DATEV deriviates or future versions of DATEV). Background details: For example turkish characters (Ç) are not valid cp1252 charactes and armenian characters like "Գեղարդ" are probably not replaceable in cp1252' => '',
   'DATEX - Export Assistent'    => '',
   'DELETED'                     => '',
   'DFV-Kennzeichen'             => '',
-  'DHL'                         => '',
   'DR'                          => '',
   'DUNNING STARTED'             => '',
   'DUNS number'                 => '',
   'DUNS-Nr'                     => '',
   'Data'                        => '',
+  'Data type'                   => '',
+  'DataSet #1'                  => '',
+  'DataSet for GoBD version #1. Created with kivitendo #2 by #3 (#4)' => '',
   'Database Administration'     => '',
   'Database Connection Test'    => '',
   'Database Host'               => '',
   'Database ID'                 => '',
   'Database Management'         => '',
+  'Database Superuser'          => '',
   'Database User'               => '',
+  'Database errors: #1'         => '',
   'Database host and port'      => '',
   'Database login (#1)'         => '',
   'Database name'               => '',
   'Database settings'           => '',
+  'Database superuser privileges are required for parts of the database modifications.' => '',
+  'Database superuser privileges are required for the update.' => '',
   'Database template'           => '',
   'Database update error:'      => '',
   'Database user and password'  => '',
@@ -639,24 +977,33 @@ $self->{texts} = {
   'Date Paid'                   => '',
   'Date and timestamp variables: If the default value equals \'NOW\' then the current date/current timestamp will be used. Otherwise the default value is copied as-is.' => '',
   'Date missing!'               => '',
+  'Date of Last Payment'        => '',
   'Date the payment is due in full' => '',
   'Date the payment is due with discount' => '',
+  'Datev export encoding'       => '',
   'Datevautomatik'              => '',
   'Datum von'                   => '',
   'Deactivate by default'       => '',
+  'Dear Sir or Madam,'          => '',
   'Debit'                       => '',
   'Debit (one letter abbreviation)' => '',
   'Debit Account'               => '',
+  'Debit Account Name'          => '',
+  'Debit Amount'                => '',
   'Debit Starting Balance'      => '',
   'Debit Tax'                   => '',
+  'Debit Tax (lit)'             => '',
   'Debit Tax Account'           => '',
   'Debit and credit out of balance!' => '',
+  'Debit/Credit Label'          => '',
   'Dec'                         => '',
   'December'                    => '',
+  'December last year period'   => '',
   'Decimalplaces'               => '',
   'Decrease'                    => '',
   'Default (no language selected)' => '',
   'Default Accounts'            => '',
+  'Default Billing Address'     => '',
   'Default Bin'                 => '',
   'Default Bin with ignoring onhand' => '',
   'Default Client (unconfigured)' => '',
@@ -665,172 +1012,266 @@ $self->{texts} = {
   'Default Transfer Out always succeed. The current part onhand is ignored and the inventory can have negative stocks (not recommended).' => '',
   'Default Transfer Out with negative inventory' => '',
   'Default Transfer with Master Bin' => '',
+  'Default Transfer with services' => '',
   'Default Warehouse'           => '',
-  'Default Warehouse with ignoring on hand' => '',
-  'Default buchungsgruppe'      => '',
+  'Default Warehouse with ignoring onhand' => '',
+  'Default address flag'        => '',
+  'Default article for converting into quotations and orders' => '',
+  'Default booking group'       => '',
   'Default client'              => '',
   'Default currency'            => '',
   'Default currency missing!'   => '',
+  'Default hourly rate for new customers' => '',
   'Default output medium'       => '',
+  'Default part for shipping costs' => '',
   'Default printer'             => '',
+  'Default taxzone'             => '',
   'Default template format'     => '',
+  'Default transfer delivery order' => '',
+  'Default transfer invoice'    => '',
+  'Default transfer invoice with charge number' => '',
+  'Default transport article number' => '',
   'Default unit'                => '',
   'Default value'               => '',
+  'Defines the interval where undoing transfers from a delivery order are allowed.' => '',
   'Delete'                      => '',
   'Delete Account'              => '',
+  'Delete Attachments'          => '',
   'Delete Contact'              => '',
   'Delete Dataset'              => '',
+  'Delete Documents'            => '',
+  'Delete Images'               => '',
   'Delete Shipto'               => '',
-  'Delete drafts'               => '',
+  'Delete address'              => '',
+  'Delete all'                  => '',
+  'Delete for Customers'        => '',
   'Delete links'                => '',
+  'Delete picture'              => '',
+  'Delete printfiles'           => '',
   'Delete profile'              => '',
+  'Delete quotation/order'      => '',
+  'Delete requirement spec'     => '',
+  'Delete shoporder'            => '',
+  'Delete template'             => '',
+  'Delete text block'           => '',
   'Delete transaction'          => '',
   'Deleted'                     => '',
+  'Deleting this type of record has been disabled in the configuration.' => '',
   'Delivered'                   => '',
+  'Delivered amount'            => '',
   'Delivery Date'               => '',
   'Delivery Order'              => '',
   'Delivery Order Date'         => '',
   'Delivery Order Date missing!' => '',
   'Delivery Order Number'       => '',
+  'Delivery Order Type'         => '',
   'Delivery Order created'      => '',
   'Delivery Order deleted!'     => '',
+  'Delivery Order has been deleted' => '',
+  'Delivery Order has been saved' => '',
+  'Delivery Order(s) for full qty created' => '',
   'Delivery Orders'             => '',
   'Delivery Plan'               => '',
+  'Delivery Plan for currently outstanding purchase orders' => '',
   'Delivery Plan for currently outstanding sales orders' => '',
   'Delivery Terms'              => '',
+  'Delivery Value Report'       => '',
+  'Delivery Value Report for currently open sales orders' => '',
+  'Delivery Value Report for currently outstanding purchase orders' => '',
   'Delivery terms'              => '',
   'Delivery terms (database ID)' => '',
   'Delivery terms (name)'       => '',
+  'DeliveryOrder'               => '',
+  'Denmark'                     => '',
   'Department'                  => '',
   'Department (database ID)'    => '',
   'Department (description)'    => '',
   'Department 1'                => '',
   'Department 2'                => '',
-  'Department Id'               => '',
   'Departments'                 => '',
+  'Dependencies'                => '',
   'Dependency loop detected:'   => '',
   'Deposit'                     => '',
   'Description'                 => '',
   'Description (Click on Description for details)' => '',
   'Description (translation for #1)' => '',
   'Description missing!'        => '',
-  'Description must not be empty!' => '',
+  'Description must not be empty.' => '',
+  'Description of #1'           => '',
+  'Design custom data export queries' => '',
   'Destination BIC'             => '',
   'Destination IBAN'            => '',
   'Destination bin'             => '',
   'Destination warehouse'       => '',
   'Destination warehouse and bin' => '',
+  'Detail view'                 => '',
+  'Details'                     => '',
   'Details (one letter abbreviation)' => '',
+  'Details: #1'                 => '',
+  'Developer Tools'             => '',
+  'Dial command missing in kivitendo configuration\'s [cti] section' => '',
   'Difference'                  => '',
+  'Dimensions'                  => '',
+  'Direct debit revoked'        => '',
   'Directory'                   => '',
+  'Disabled Price Sources'      => '',
+  'Disassemble Assembly'        => '',
+  'Disassembly successful for trans_id #1' => '',
   'Discard duplicate entries in CSV file' => '',
   'Discard entries with duplicates in database or CSV file' => '',
   'Discount'                    => '',
+  'Discount #1%'                => '',
+  'Discounts'                   => '',
   'Display'                     => '',
   'Display file'                => '',
+  'Display in basic data tab'   => '',
   'Display options'             => '',
+  'Displayable Name Preferences' => '',
   'Do not change the tax rate of taxkey 0.' => '',
   'Do not check for duplicates' => '',
-  'Do not set default buchungsgruppe' => '',
+  'Do not create Factur-X/ZUGFeRD invoices' => '',
+  'Do not create QR-bill invoices' => '',
+  'Do not leave booking form?'  => '',
+  'Do not link to a project.'   => '',
+  'Do not modify this position' => '',
+  'Do not run the task server for this client' => '',
+  'Do not set default booking group' => '',
   'Do not set this bin'         => '',
   'Do not set this comment'     => '',
   'Do not set this warehouse'   => '',
-  'Do you really want to close the following SEPA exports? No payment will be recorded for bank collections that haven\'t been marked as executed yet.' => '',
-  'Do you really want to close the following SEPA exports? No payment will be recorded for bank transfers that haven\'t been marked as executed yet.' => '',
+  'Do you really want to cancel this general ledger transaction?' => '',
+  'Do you really want to cancel this invoice?' => '',
+  'Do you really want to cancel?' => '',
+  'Do you really want to close the selected SEPA exports? No payment will be recorded for bank collections that haven\'t been marked as executed yet.' => '',
+  'Do you really want to close the selected SEPA exports? No payment will be recorded for bank transfers that haven\'t been marked as executed yet.' => '',
+  'Do you really want to continue?' => '',
   'Do you really want to delete AP transaction #1?' => '',
   'Do you really want to delete AR transaction #1?' => '',
   'Do you really want to delete GL transaction #1?' => '',
+  'Do you really want to delete the selected documents?' => '',
   'Do you really want to delete the selected links?' => '',
+  'Do you really want to delete the selected objects?' => '',
+  'Do you really want to delete this draft?' => '',
   'Do you really want to delete this object?' => '',
-  'Do you really want to delete this warehouse?' => '',
+  'Do you really want to delete this record template?' => '',
+  'Do you really want to mark the selected entries as booked?' => '',
+  'Do you really want to print?' => '',
+  'Do you really want to revert to this version?' => '',
+  'Do you really want to transfer the stock and set this order to delivered?' => '',
+  'Do you really want to undo the selected SEPA exports? You have to reassign the export again.' => '',
+  'Do you really want to unimport the selected documents?' => '',
   'Do you want to <b>limit</b> your search?' => '',
   'Do you want to carry this shipping address over to the new purchase order so that the vendor can deliver the goods directly to your customer?' => '',
+  'Do you want to overwrite your current title?' => '',
   'Do you want to set the account number "#1" to "#2" and the name "#3" to "#4"?' => '',
   'Do you want to store the existing onhand values into a new warehouse?' => '',
   'Document'                    => '',
+  'Document Count'              => '',
   'Document Project (database ID)' => '',
   'Document Project (description)' => '',
   'Document Project (number)'   => '',
   'Document Project Number'     => '',
-  'Document Template'           => '',
+  'Document generating failed. Please check Templates an LateX !' => '',
   'Documentation'               => '',
   'Documentation (in German)'   => '',
   'Documents'                   => '',
   'Documents in the WebDAV repository' => '',
+  'Don\'t include a printout of the record with the email' => '',
+  'Don\'t include a printout of the record with the email, only selected files' => '',
   'Done'                        => '',
+  'Done.'                       => '',
   'Double partnumbers'          => '',
+  'Download'                    => '',
+  'Download PDF'                => '',
+  'Download PDF, do not print'  => '',
   'Download SEPA XML export file' => '',
+  'Download attachments of all parts' => '',
+  'Download list of payments as PDF' => '',
+  'Download picture'            => '',
   'Download sample file'        => '',
+  'Draft deleted'               => '',
+  'Draft for this Letter saved!' => '',
   'Draft saved.'                => '',
+  'Drafts'                      => '',
+  'Drag and drop files here'    => '',
   'Drawing'                     => '',
-  'Dropdown Limit'              => '',
   'Due'                         => '',
   'Due Date'                    => '',
   'Due Date missing!'           => '',
   'Due to security concerns these files have to be deleted or moved after the migration before you can continue using kivitendo.' => '',
   'Duedate +Days'               => '',
+  'Dunned open amount: #1'      => '',
   'Dunning'                     => '',
   'Dunning Amount'              => '',
+  'Dunning Creator'             => '',
   'Dunning Date'                => '',
   'Dunning Date from'           => '',
   'Dunning Description'         => '',
   'Dunning Description missing in row ' => '',
   'Dunning Duedate'             => '',
+  'Dunning Invoice'             => '',
   'Dunning Level'               => '',
   'Dunning Level missing in row ' => '',
   'Dunning Process Config saved!' => '',
   'Dunning Process started for selected invoices!' => '',
+  'Dunning level'               => '',
   'Dunning number'              => '',
   'Dunning overview'            => '',
+  'Dunning status'              => '',
   'Dunnings'                    => '',
+  'Dunnings (Id -- Dunning Date --Dunning Level -- Dunning Fee)' => '',
+  'Dunningstatistic'            => '',
+  'Duplicate'                   => '',
   'Duplicate in CSV file'       => '',
   'Duplicate in database'       => '',
+  'Duration'                    => '',
   'During the next update a taxkey 0 with tax rate of 0 will automatically created.' => '',
+  'E Mail'                      => '',
+  'E-Mail'                      => '',
+  'E-Mail-Journal'              => '',
   'E-mail'                      => '',
-  'E-mail Statement to'         => '',
   'E-mail address missing!'     => '',
+  'E.g. "<%customernumber%> <%name%>"' => '',
   'EAN'                         => '',
   'EAN-Code'                    => '',
   'EB-Wert'                     => '',
   'EK'                          => '',
   'ELSE'                        => '',
-  'ELSTER Export (Taxbird)'     => '',
-  'ELSTER Export (Winston)'     => '',
-  'ELSTER Export nach Winston'  => '',
-  'ELSTER Tax Number'           => '',
+  'ELSTER Export (via Geierlein)' => '',
   'EQUITY'                      => '',
+  'EU Member State and VAT ID Number' => '',
   'EUER'                        => '',
   'Earlier versions of kivitendo contained bugs which might have led to wrong entries in the general ledger.' => '',
   'Edit'                        => '',
   'Edit Access Rights'          => '',
   'Edit Access Rights for Follow-Ups' => '',
   'Edit Account'                => '',
-  'Edit Accounting Group'       => '',
   'Edit Accounts Payables Transaction' => 'Edit Creditor Transaction',
   'Edit Accounts Receivables Transaction' => 'Edit Debtor Transaction',
   'Edit Assembly'               => '',
-  'Edit Bins'                   => '',
-  'Edit Buchungsgruppe'         => '',
+  'Edit Assortment'             => '',
+  'Edit Bins for Warehouse \'#1\'' => '',
   'Edit Client'                 => '',
   'Edit Credit Note'            => '',
   'Edit Customer'               => '',
   'Edit Dunning'                => '',
   'Edit Dunning Process Config' => '',
   'Edit Employee #1'            => '',
+  'Edit Factur-X/ZUGFeRD notes' => '',
+  'Edit Final Invoice'          => '',
   'Edit Follow-Up'              => '',
   'Edit Follow-Up for #1'       => '',
   'Edit General Ledger Transaction' => '',
-  'Edit Group'                  => '',
-  'Edit Language'               => '',
-  'Edit Lead'                   => '',
+  'Edit Invoice for Advance Payment' => '',
+  'Edit Letter'                 => '',
   'Edit Part'                   => '',
   'Edit Preferences for #1'     => '',
   'Edit Price Factor'           => '',
-  'Edit Pricegroup'             => '',
   'Edit Printer'                => '',
   'Edit Purchase Delivery Order' => '',
   'Edit Purchase Order'         => '',
   'Edit Quotation'              => '',
+  'Edit RMA Delivery Order'     => '',
   'Edit Request for Quotation'  => '',
   'Edit SEPA strings'           => '',
   'Edit Sales Delivery Order'   => '',
@@ -839,78 +1280,148 @@ $self->{texts} = {
   'Edit Service'                => '',
   'Edit Storno Credit Note'     => '',
   'Edit Storno Invoice'         => '',
+  'Edit Storno Invoice for Advance Payment' => '',
+  'Edit Supplier Delivery Order' => '',
   'Edit User'                   => '',
   'Edit User Group'             => '',
   'Edit Vendor'                 => '',
   'Edit Vendor Invoice'         => '',
   'Edit Warehouse'              => '',
+  'Edit acceptance status'      => '',
+  'Edit additional articles'    => '',
+  'Edit all drafts'             => '',
+  'Edit article/section assignments' => '',
+  'Edit assignment of articles to sections' => '',
   'Edit background job'         => '',
   'Edit bank account'           => '',
+  'Edit booking group'          => '',
   'Edit business'               => '',
+  'Edit complexity'             => '',
+  'Edit custom data export query' => '',
+  'Edit custom shipto'          => '',
   'Edit custom variable'        => '',
   'Edit delivery term'          => '',
   'Edit department'             => '',
   'Edit file'                   => '',
+  'Edit general settings'       => '',
+  'Edit greeting'               => '',
   'Edit greetings'              => '',
+  'Edit language'               => '',
   'Edit note'                   => '',
+  'Edit part classification'    => '',
+  'Edit partsgroup'             => '',
   'Edit payment term'           => '',
+  'Edit picture'                => '',
+  'Edit pre-defined text'       => '',
+  'Edit preset email strings'   => '',
+  'Edit price rule'             => '',
+  'Edit pricegroup'             => '',
   'Edit prices and discount (if not used, textfield is ONLY set readonly)' => '',
   'Edit project'                => '',
   'Edit project #1'             => '',
+  'Edit project link'           => '',
+  'Edit project status'         => '',
+  'Edit project type'           => '',
+  'Edit purchase letters'       => '',
+  'Edit purchase price rule'    => '',
+  'Edit requirement spec'       => '',
+  'Edit requirement spec status' => '',
+  'Edit requirement spec template' => '',
+  'Edit requirement spec type'  => '',
+  'Edit risk level'             => '',
+  'Edit sales letters'          => '',
+  'Edit sales price rule'       => '',
+  'Edit section #1'             => '',
+  'Edit shop'                   => '',
+  'Edit taxzone'                => '',
   'Edit templates'              => 'Templates, edit',
-  'Edit the Delivery Order'     => '',
+  'Edit text block'             => '',
+  'Edit text block \'#1\''      => '',
+  'Edit text block picture #1'  => '',
   'Edit the configuration for periodic invoices' => '',
   'Edit the currency names in order to rename them.' => '',
   'Edit the purchase_order'     => '',
   'Edit the request_quotation'  => '',
   'Edit the sales_order'        => '',
   'Edit the sales_quotation'    => '',
-  'Edit the stylesheet'         => '',
+  'Edit time recording article' => '',
+  'Edit time recordings of all staff members' => '',
+  'Edit title'                  => '',
   'Edit units'                  => '',
   'Editable'                    => '',
   'Either there are no open invoices, or you have already initiated bank transfers with the open amounts for those that are still open.' => '',
   'Element disabled'            => '',
+  'Email'                       => '',
+  'Email address'               => '',
+  'Email journal'               => '',
+  'Email of the delivery order recipient' => '',
+  'Email of the invoice recipient' => '',
+  'Email signature'             => '',
   'Employee'                    => '',
   'Employee #1 saved!'          => '',
   'Employee (database ID)'      => '',
+  'Employee from the original invoice' => '',
+  'Employee must not be empty.' => '',
   'Employees'                   => '',
+  'Employees with read access to the project\'s invoices' => '',
   'Empty selection for warehouse will not be added, even if the old bin is still visible (use back and forth to edit again).' => '',
   'Empty transaction!'          => '',
+  'Enabled Quick Searched'      => '',
+  'Enabled modules'             => '',
+  'End'                         => '',
   'End date'                    => '',
-  'Enter a description for this new draft.' => '',
   'Enter longdescription'       => '',
   'Enter the requested execution date or leave empty for the quickest possible execution:' => '',
   'Entries for which automatic conversion failed:' => '',
   'Entries for which automatic conversion succeeded:' => '',
+  'Entries ready to import'     => '',
+  'Entries with errors'         => '',
+  'Entry overlaps with "#1".'   => '',
   'Equity'                      => '',
+  'Erfolgsrechnung'             => '',
   'Error'                       => '',
+  'Error getting QR-Bill type.' => '',
+  'Error handling'              => '',
   'Error in database control file \'%s\': %s' => '',
   'Error in position #1: You must either assign no stock at all or the full quantity of #2 #3.' => '',
   'Error in position #1: You must either assign no transfer at all or the full quantity of #2 #3.' => '',
   'Error in row #1: The quantity you entered is bigger than the stocked quantity.' => '',
+  'Error mapping biller countrycode.' => '',
+  'Error mapping customer countrycode.' => '',
   'Error message from the database driver:' => '',
   'Error message from the database: #1' => '',
+  'Error message from the webshop api:' => '',
   'Error when saving: #1'       => '',
+  'Error while applying year-end bookings!' => '',
+  'Error while creating project with project number of new order number, project number #1 already exists!' => '',
+  'Error while saving shop order #1. DB Error #2. Generic exception #3.' => '',
+  'Error with default taxzone'  => '',
   'Error!'                      => '',
+  'Error: #1'                   => '',
   'Error: A negative target quantity is not allowed.' => '',
   'Error: A quantity and a target quantity could not be given both.' => '',
   'Error: A quantity or a target quantity must be given.' => '',
+  'Error: Bin #1 is not from warehouse #2' => '',
   'Error: Bin not found'        => '',
-  'Error: Buchungsgruppe missing or invalid' => '',
+  'Error: Customer/vendor is ambiguous' => '',
   'Error: Customer/vendor missing' => '',
   'Error: Customer/vendor not found' => '',
+  'Error: Faulty position in this delivery order' => '',
+  'Error: Found local bank account number but local bank code doesn\'t match' => '',
   'Error: Gender (cp_gender) missing or invalid' => '',
   'Error: Invalid bin'          => '',
+  'Error: Invalid bin id'       => '',
+  'Error: Invalid bin name #1'  => '',
   'Error: Invalid business'     => '',
   'Error: Invalid contact'      => '',
   'Error: Invalid currency'     => '',
   'Error: Invalid delivery terms' => '',
   'Error: Invalid department'   => '',
   'Error: Invalid language'     => '',
-  'Error: Invalid order for this order item' => '',
   'Error: Invalid part'         => '',
   'Error: Invalid part type'    => '',
-  'Error: Invalid parts group'  => '',
+  'Error: Invalid parts group id #1' => '',
+  'Error: Invalid parts group name #1' => '',
   'Error: Invalid payment terms' => '',
   'Error: Invalid price factor' => '',
   'Error: Invalid price group'  => '',
@@ -918,29 +1429,72 @@ $self->{texts} = {
   'Error: Invalid salesman'     => '',
   'Error: Invalid shipto'       => '',
   'Error: Invalid tax zone'     => '',
+  'Error: Invalid unit'         => '',
   'Error: Invalid vendor in column make_#1' => '',
   'Error: Invalid warehouse'    => '',
+  'Error: Invalid warehouse id' => '',
+  'Error: Invalid warehouse name #1' => '',
+  'Error: More than one source order found' => '',
   'Error: Name missing'         => '',
+  'Error: Not enough parts in stock' => '',
+  'Error: Part is ambiguous'    => '',
+  'Error: Part is obsolete'     => '',
   'Error: Part not found'       => '',
   'Error: Quantity to transfer is zero.' => '',
+  'Error: Source order not found' => '',
+  'Error: Stock problem'        => '',
+  'Error: Stocking out would result in stock underrun' => '',
+  'Error: Stocking out would result in stock underrun: #1' => '',
   'Error: Transfer would result in a negative target quantity.' => '',
   'Error: Unit missing or invalid' => '',
   'Error: Warehouse not found'  => '',
+  'Error: amount and netamount need to be numeric' => '',
+  'Error: ar transaction doesn\'t validate' => '',
+  'Error: archart isn\'t an AR chart' => '',
+  'Error: booking group missing or invalid' => '',
+  'Error: can\'t find ar chart with accno #1' => '',
+  'Error: chart isn\'t an ar_amount chart' => '',
+  'Error: chart missing'        => '',
+  'Error: chart not found'      => '',
+  'Error: invalid acc transactions for this ar row' => '',
+  'Error: invalid ar row for this transaction' => '',
+  'Error: invalid chart'        => '',
+  'Error: invalid chart (accno)' => '',
+  'Error: invalid chart_id'     => '',
+  'Error: invalid taxkey'       => '',
+  'Error: invnumber already exists' => '',
+  'Error: local bank account id doesn\'t match local bank account number' => '',
+  'Error: local bank account id doesn\'t match local bank code' => '',
+  'Error: need amount and netamount' => '',
+  'Error: neither archart passed, no default receivables chart configured' => '',
+  'Error: taxincluded has to be t or f' => '',
+  'Error: taxincluded wasn\'t set' => '',
+  'Error: taxkey missing'       => '',
+  'Error: this feature requires that articles with a time-based unit (e.g. \'h\' or \'min\') exist.' => '',
+  'Error: unknown local bank account' => '',
+  'Error: unknown local bank account id' => '',
   'Errors'                      => '',
+  'Errors during conversion:'   => '',
+  'Errors during printing:'     => '',
+  'Errors in GL transaction:'   => '',
+  'Errors: #1'                  => '',
   'Ertrag'                      => '',
   'Ertrag prozentual'           => '',
   'Escape character'            => '',
-  'EuR'                         => '',
   'Everyone can log in.'        => '',
   'Exact'                       => '',
+  'Example'                     => '',
   'Example: http://kivitendo.de' => '',
   'Excel'                       => '',
   'Exch'                        => '',
+  'Exchange Rate'               => '',
   'Exchangerate'                => '',
   'Exchangerate Difference'     => '',
   'Exchangerate for payment missing!' => '',
   'Exchangerate missing!'       => '',
-  'Execute now'                 => '',
+  'Execute'                     => '',
+  'Execute a custom data export query' => '',
+  'Execute custom data export \'#1\'' => '',
   'Executed'                    => '',
   'Execution date'              => '',
   'Execution date from'         => '',
@@ -949,49 +1503,88 @@ $self->{texts} = {
   'Execution status'            => '',
   'Execution type'              => '',
   'Existing Datasets'           => '',
+  'Existing bank transactions'  => '',
   'Existing contacts (with column \'cp_id\')' => '',
   'Existing customers/vendors with same customer/vendor number' => '',
   'Existing file on server'     => '',
+  'Existing finished follow-ups for this item' => '',
   'Existing pending follow-ups for this item' => '',
   'Existing profiles'           => '',
+  'Existing templates'          => '',
+  'Exp. bill. date'             => '',
+  'Exp. netamount'              => '',
   'Expected Tax'                => '',
+  'Expected billing date'       => '',
   'Expense'                     => '',
   'Expense Account'             => '',
   'Expense/Asset'               => '',
-  'Expenses EU with UStId'      => '',
-  'Expenses EU without UStId'   => '',
-  'Export Buchungsdaten'        => '',
+  'Experimental Features'       => '',
+  'Export'                      => '',
   'Export Number'               => '',
-  'Export Stammdaten'           => '',
   'Export as CSV'               => '',
   'Export as PDF'               => '',
   'Export date'                 => '',
   'Export date from'            => '',
   'Export date to'              => '',
+  'Export error in transaction #1: Rounding error too large #2' => '',
+  'Export error in transaction #1: Unbalanced ledger before next transaction (#2)' => '',
+  'Export imported bookings'    => '',
+  'Export with CV Charts'       => '',
   'Extend automatically by n months' => '',
   'Extended'                    => '',
+  'Extended status'             => '',
   'Extension Of Time'           => '',
   'Factor'                      => '',
-  'Factor missing!'             => '',
-  'Falsches Datumsformat!'      => '',
+  'Factur-X/ZUGFeRD'            => '',
+  'Factur-X/ZUGFeRD import'     => '',
+  'Factur-X/ZUGFeRD invoice'    => '',
+  'Factur-X/ZUGFeRD notes for each invoice' => '',
+  'Factur-X/ZUGFeRD settings'   => '',
   'Fax'                         => '',
   'Features'                    => '',
   'Feb'                         => '',
   'February'                    => '',
   'Fee'                         => '',
+  'Fetch from last order number is not implemented' => '',
+  'Fetch order'                 => '',
   'Field'                       => '',
   'File'                        => '',
+  'File \'#1\' is used as new Version !' => '',
+  'File Management'             => '',
   'File name'                   => '',
+  'File not exists !'           => '',
+  'File still exists !'         => '',
+  'File upload'                 => '',
+  'Filemanagement'              => '',
+  'Filename'                    => '',
+  'Files'                       => '',
+  'Files from customer'         => '',
+  'Files from parts'            => '',
+  'Files from projects'         => '',
+  'Files from vendor'           => '',
+  'Files have been uploaded successfully.' => '',
   'Filter'                      => '',
   'Filter by Partsgroups'       => '',
   'Filter date by'              => '',
   'Filter for customer variables' => '',
   'Filter for item variables'   => '',
   'Filter parts'                => '',
+  'Filter record template'      => '',
+  'Final Invoice'               => '',
+  'Final Invoice (one letter abbreviation)' => '',
+  'Final Invoice, please use mark as paid manually' => '',
+  'Financial Controlling'       => '',
+  'Financial Controlling Report' => '',
+  'Financial Overview'          => '',
+  'Financial controlling report for open sales orders' => '',
+  'Financial overview for #1'   => '',
   'Finish'                      => '',
   'First 20 Lines'              => '',
+  'Firstname'                   => '',
   'Fix transaction'             => '',
   'Fix transactions'            => '',
+  'Fixed value'                 => '',
+  'Focus position after update' => '',
   'Folgekonto'                  => '',
   'Follow-Up'                   => '',
   'Follow-Up Date'              => '',
@@ -1002,72 +1595,102 @@ $self->{texts} = {
   'Follow-Up saved.'            => '',
   'Follow-Ups'                  => '',
   'Follow-up for'               => '',
+  'Following files are deleted:' => '',
+  'Following files are unimported:' => '',
+  'Following year'              => '',
   'Font'                        => '',
   'Font size'                   => '',
   'For AP transactions it will replace the sales taxkeys with input taxkeys with the same tax rate.' => '',
   'For AR transactions it will replace the input taxkeys with sales taxkeys with the same tax rate.' => '',
+  'For changeing goto USTVA Config' => '',
   'For further information read this: ' => '',
-  'For part \"#1\" there are missing #2 #3 in the default warehouse/bin \"#4/#5\"' => '',
-  'For part \"#1\" there is no default warehouse and bin defined.' => '',
-  'For part \"#1\" there is no default warehouse and bin for ignoring onhand defined.' => '',
+  'For part "#1" there are missing #2 #3 in the default warehouse/bin "#4/#5".' => '',
+  'For part "#1" there is no default warehouse and bin defined.' => '',
+  'For part "#1" there is no default warehouse and bin for ignoring onhand defined.' => '',
+  'For purchase delivery orders, warn on workflow to invoice if not stocked in' => '',
+  'For sales delivery orders, warn on workflow to invoice if not stocked out' => '',
+  'For sales invoices, warn if invoice has no delivery order as a predecessor' => '',
   'For type "customer" the perl module JSON is required. Please check this on system level: $ ./scripts/installation_check.pl' => '',
   'Foreign Exchange Gain'       => '',
   'Foreign Exchange Loss'       => '',
-  'Foreign Expenses'            => '',
-  'Foreign Revenues'            => '',
   'Form details (second row)'   => '',
+  'Format \'#1\' is not supported yet/anymore.' => '',
   'Formula'                     => '',
-  'Found #1 errors.'            => '',
-  'Found #1 objects of which #2 can be imported.' => '',
+  'France'                      => '',
   'Free report period'          => '',
+  'Free skonto amount has to be a positive number.' => '',
   'Free-form text'              => '',
+  'Fri'                         => '',
+  'Friday'                      => '',
   'Fristsetzung'                => '',
   'From'                        => '',
   'From Date'                   => '',
+  'From bin'                    => '',
+  'From shop "#1" :  #2 '       => '',
+  'From shop #1 :  #2 shoporders have been fetched.' => '',
   'From this version on a new feature is available.' => '',
   'From this version on it is necessary to name a default value.' => '',
   'From this version on the partnumber of services, articles and assemblies have to be unique.' => '',
   'From this version on the taxkey 0 must have a tax rate of 0 (for DATEV compatibility).' => '',
+  'Front page'                  => '',
   'Full Access'                 => '',
   'Full Preview'                => '',
+  'Full Text'                   => '',
   'Full access to all functions' => '',
+  'Function block'              => '',
+  'Function block actions'      => '',
+  'Function block number format' => '',
   'Function/position'           => '',
-  'Fwd'                         => 'Forward',
+  'Further Invoice for Advance Payment' => '',
   'GL Transaction'              => '',
+  'GL Transaction (abbreviation)' => '',
+  'GL Transactions'             => '',
+  'GL search'                   => '',
+  'GL template suggestions'     => '',
   'GL transactions changeable'  => '',
+  'GLN'                         => '',
   'Gegenkonto'                  => '',
+  'Geierlein'                   => '',
   'Gender'                      => '',
   'General Ledger'              => '',
   'General Ledger Corrections'  => '',
   'General Ledger Transaction'  => '',
   'General ledger and cash'     => '',
   'General ledger corrections'  => '',
+  'General ledger transaction \'#1\' posted (ID: #2)' => '',
+  'General ledger transactions can only be changed on the day they are posted.' => '',
   'General settings'            => '',
-  'Generic Tax Report'          => '',
+  'Generate and print sales delivery orders' => '',
+  'Generating the document failed: #1' => '',
+  'Germany'                     => '',
+  'Get one order'               => '',
+  'Get one order by shopordernumber' => '',
+  'Get one shoporder'           => '',
+  'Get shoporders'              => 'Get and process orders from a web shop',
   'Git revision: #1, #2 #3'     => '',
   'Given Name'                  => '',
-  'Go one step back'            => '',
-  'Go one step forward'         => '',
+  'Gldate'                      => 'Entry Date',
+  'Global Attachments'          => '',
+  'Global Record BCC'           => '',
+  'GoBD Export'                 => '',
   'Greeting'                    => '',
   'Greetings'                   => '',
-  'Group'                       => '',
   'Group Invoices'              => '',
   'Group Items'                 => '',
   'Group assignment'            => '',
-  'Group deleted!'              => '',
   'Group list'                  => '',
   'Group membership'            => '',
-  'Group missing!'              => '',
-  'Group saved!'                => '',
-  'Groups'                      => '',
   'Groups that are valid for this client for access rights' => '',
   'Groups this user is a member in' => '',
   'Groups valid for this client' => '',
   'HTML'                        => '',
   'HTML Templates'              => '',
+  'HTML field'                  => '',
   'Handling of WebDAV'          => '',
   'Hardcopy'                    => '',
+  'Has item type'               => '',
   'Has serial number'           => '',
+  'Headers'                     => '',
   'Heading'                     => '',
   'Help Template Variables'     => '',
   'Help on column names'        => '',
@@ -1075,50 +1698,120 @@ $self->{texts} = {
   'Here you only provide the credentials for logging into the database.' => '',
   'Here\'s an example command line:' => '',
   'Hide Filter'                 => '',
+  'Hide all details'            => '',
+  'Hide buttons'                => '',
   'Hide by default'             => '',
+  'Hide chart details'          => '',
+  'Hide chart list'             => '',
+  'Hide charts'                 => '',
+  'Hide details'                => '',
   'Hide help text'              => '',
+  'Hide mappings (csv_import)'  => '',
   'Hide settings'               => '',
+  'Highest Dunninglevel'        => '',
+  'Hint: Not all VC Numbers are personal accounts compliant' => '',
   'Hints'                       => '',
   'History'                     => '',
   'History Search'              => '',
   'History Search Engine'       => '',
   'Homepage'                    => '',
   'Host'                        => '',
-  'However, you can create a new part which will then be selected.' => '',
+  'Hourly Rate'                 => '',
+  'Hourly rate'                 => '',
+  'How many do you want to create and print?' => '',
   'I'                           => '',
   'IBAN'                        => '',
   'ID'                          => '',
-  'ID-Nummer'                   => '',
+  'ID (lit)'                    => '',
+  'ID number'                   => '',
+  'ID of own bank account'      => '',
+  'ID/Acc_ID'                   => '',
   'II'                          => '',
   'III'                         => '',
+  'IMPORT'                      => '',
   'IV'                          => '',
+  'If all of the following match' => '',
   'If amounts differ more than "Maximal amount difference" (see settings), this item is marked as invalid.' => '',
   'If checked the taxkey will not be exported in the DATEV Export, but only IF chart taxkeys differ from general ledger taxkeys' => '',
+  'If column \'pclass\' is present the article type is then irrelevant or used as default ' => '',
   'If configured this bin will be preselected for all new parts. Also this bin will be used as the master default bin, if default transfer out with master bin is activated.' => '',
-  'If the article type is set to \'mixed\' then a column called \'type\' must be present.' => '',
+  'If configured this bin will be preselected for stocktaking.' => '',
+  'If configured this date will used as preselected cutoff date for stocktaking.' => '',
+  'If configured this warehouse will be preselected for stocktaking.' => '',
+  'If disabled purchase delivery orders can only be created by conversion from existing requests for quotations and purchase orders.' => '',
+  'If disabled purchase invoices can only be created by conversion from existing requests for quotations, purchase orders and purchase delivery orders.' => '',
+  'If disabled sales orders cannot be converted into sales invoices directly.' => '',
+  'If disabled sales quotations cannot be converted into sales invoices directly.' => '',
+  'If disabled, record numbers for sales records & purchase records produced by our side will always be auto-generated and cannot be changed later.' => '',
+  'If enabled Factur-X/ZUGFeRD conformant sales invoice PDFs will be created.' => '',
+  'If enabled a column will be shown in sales and purchase orders that lists both the amount and the value not shipped yet for each item.' => '',
+  'If enabled a warning will be shown if a sales invoices is created without having a sales delivery order as a predecessor.' => '',
+  'If enabled a warning will be shown in purchase delivery orders on workflow to invoices if positions are not stocked in.' => '',
+  'If enabled a warning will be shown in sales and purchase orders if there are two or more positions of the same part (new controller only).' => '',
+  'If enabled a warning will be shown in sales and purchase orders if there the delivery date is empty.' => '',
+  'If enabled a warning will be shown in sales delivery orders if the customer order number is missing.' => '',
+  'If enabled a warning will be shown in sales delivery orders on workflow to invoices if positions are not stocked out.' => '',
+  'If enabled only those projects that are assigned to the currently selected customer are offered for selection in sales records.' => '',
+  'If enabled purchase and sales records cannot be saved if no transaction description has been entered.' => '',
+  'If enabled sales invoices created using OpenDocument/OASIS format will include data for Swiss QR-Bill creation.' => '',
+  'If enabled the record links view starts always from the sales order including all sublevels' => '',
+  'If enabled try to overrule the brower\'s back button to prevent double booking of sales invoices.' => '',
+  'If enabled, when saving parts the partsgroup must be not be empty.' => '',
+  'If item not found, allow creation of new item' => '',
+  'If left empty the default sender from the kivitendo configuration will be used (key \'email_from\' in section \'periodic_invoices\'; current value: #1).' => '',
+  'If missing then the start date will be used.' => '',
+  'If one or more space separated serial numbers are assigned in a sales invoice, match the charge number of the inventory item. Assumes that Serial Number and Charge Number have 1:1 relation. Otherwise throw a error message for the default sales invoice transfer.' => '',
+  'If searching a part from a document and no part is found then offer to create a new part.' => '',
+  'If set to no the \'delivery date\' field for sales orders won\'t be set at all.' => '',
+  'If set to no the \'valid until\' field for sales quotation won\'t be set at all.' => '',
+  'If the article type is set to \'mixed\' then a column called \'part_type\' or called \'pclass\' must be present.' => '',
   'If the automatic creation of invoices for fees and interest is switched on for a dunning level then the following accounts will be used for the invoice.' => '',
+  'If the counted quantity differs more than this threshold from the quantity in the database, a warning will be shown. Set to 0 to switch of this feature.' => '',
   'If the database user listed above does not have the right to create a database then enter the name and password of the superuser below:' => '',
   'If the default transfer out always succeed use this bin for negative stock quantity.' => '',
+  'If the test mode is enabled, the Factur-X/ZUGFeRD invoices will be flagged so that they\'re only fit to be used for testing purposes.' => '',
+  'If yes, delivery order positions are considered "delivered" only if they have been stocked out of the inventory. Otherwise saving the delivery order is considered delivered.' => '',
   'If you enter values for the part number and / or part description then only those bins containing parts whose part number or part description match your input will be shown.' => '',
   'If you have not chosen for example the category revenue for a tax and you choose an revenue account to create a transfer in the general ledger, this tax will not be displayed in the tax dropdown.' => '',
   'If you lock the system normal users won\'t be able to log in.' => '',
-  'If you see this message, you most likely just setup your LX-Office and haven\'t added any entry types. If this is the case, the option is accessible for administrators in the System menu.' => '',
   'If you select a base unit then you also have to enter a factor.' => '',
+  'If you switch to a different tab without saving you will lose the data you\'ve entered in the current tab.' => '',
   'If you want to change any of these parameters then press the "Back" button, edit the file "config/kivitendo.conf" and login into the admin module again.' => '',
   'If you want to delete such a dataset you have to edit the client(s) that are using the dataset in question and have them use another dataset.' => '',
   'If you want to set up the authentication database yourself then log in to the administration panel. kivitendo will then create the database and tables for you.' => '',
   'If your old bins match exactly Bins in the Warehouse CLICK on <b>AUTOMATICALLY MATCH BINS</b>.' => '',
+  'Ignore faulty positions'     => '',
+  'Ignore services for the purchase orders state of delivery' => '',
+  'Ignore services for the sales orders state of delivery' => '',
   'Illegal characters have been removed from the following fields: #1' => '',
+  'Illegal date'                => '',
   'Image'                       => '',
+  'Image Upload'                => '',
+  'ImagePreview'                => '',
+  'Images'                      => '',
   'Import'                      => '',
+  'Import AP from Scanner or Email' => '',
+  'Import AR from Scanner or Email' => '',
   'Import CSV'                  => '',
+  'Import Pay Postings'         => '',
   'Import Status'               => '',
+  'Import a Factur-X/ZUGFeRD file:' => '',
+  'Import a File:'              => '',
+  'Import all'                  => '',
+  'Import documents from #1'    => '',
   'Import file'                 => '',
+  'Import finished with errors.' => '',
+  'Import finished without errors.' => '',
   'Import not started yet, please wait...' => '',
   'Import preview'              => '',
   'Import profiles'             => '',
   'Import result'               => '',
-  'Import summary'              => '',
+  'Import scanned documents'    => '',
+  'Importdate'                  => '',
+  'Imported'                    => '',
+  'Imported Pay Postings'       => '',
+  'Imported entries'            => '',
+  'In addition to the above date functions, subtract the following amount of days from the calculated date as a buffer.' => '',
   'In order to do that hit the button "Delete transaction".' => '',
   'In order to migrate the old folder structure into the new structure you have to chose which client the old structure will be assigned to.' => '',
   'In order to use kivitendo you have to create at least a client, a user and a group.' => '',
@@ -1131,6 +1824,8 @@ $self->{texts} = {
   'Include in Report'           => '',
   'Include in drop-down menus'  => '',
   'Include invalid warehouses ' => '',
+  'Include invoices with direct debit' => '',
+  'Include original Invoices?'  => '',
   'Includeable in reports'      => '',
   'Included in reports by default' => '',
   'Including'                   => '',
@@ -1142,22 +1837,40 @@ $self->{texts} = {
   'Incorrect username or password or no access to selected client!' => '',
   'Increase'                    => '',
   'Individual Items'            => '',
+  'Info'                        => '',
   'Information'                 => '',
+  'Initial version.'            => '',
+  'Input from string: #1'       => '',
+  'Input to string: #1'         => '',
+  'Insert'                      => '',
   'Insert Date'                 => '',
+  'Insert new'                  => '',
   'Insert with new customer/vendor number' => '',
   'Insert with new database ID' => '',
   'Insert with new part number' => '',
   'Interest'                    => '',
   'Interest Rate'               => '',
   'Internal Notes'              => '',
+  'Internal Phone List'         => '',
+  'Internal comment'            => '',
   'Internet'                    => '',
+  'Interpolate variables in texts of positions' => '',
+  'Into bin'                    => '',
+  'Intra-Community supply'      => '',
   'Introduction of clients'     => '',
   'Inv. Duedate'                => '',
   'Invalid'                     => '',
+  'Invalid assembly'            => '',
+  'Invalid bin'                 => '',
+  'Invalid charge number: #1'   => '',
+  'Invalid combination of ledger account number length. Mismatch length of #1 with length of #2. Please check your account settings. ' => '',
+  'Invalid duration format'     => '',
   'Invalid follow-up ID.'       => '',
   'Invalid quantity.'           => '',
   'Invalid request type \'#1\'' => '',
+  'Invalid todo for updating Part' => '',
   'Invalid transactions'        => '',
+  'Invalid variable #1'         => '',
   'Invdate'                     => '',
   'Invdate from'                => '',
   'Inventories'                 => '',
@@ -1171,36 +1884,52 @@ $self->{texts} = {
   'Invnumber missing!'          => '',
   'Invoice'                     => '',
   'Invoice (one letter abbreviation)' => '',
+  'Invoice Copy'                => '',
   'Invoice Date'                => '',
   'Invoice Date missing!'       => '',
   'Invoice Duedate'             => '',
+  'Invoice Field 1'             => '',
+  'Invoice Field 2'             => '',
   'Invoice Number'              => '',
   'Invoice Number missing!'     => '',
   'Invoice deleted!'            => '',
+  'Invoice email'               => '',
+  'Invoice email and Contact Person' => '',
+  'Invoice email settings'      => '',
+  'Invoice filter'              => '',
+  'Invoice for Advance Payment' => '',
+  'Invoice for Advance Payment (one letter abbreviation)' => '',
+  'Invoice for Advance Payment with Storno (abbreviation)' => '',
   'Invoice for fees'            => '',
   'Invoice has already been storno\'d!' => '',
   'Invoice number'              => '',
+  'Invoice number invalid. Must be less then or equal to 7 digits after prefix.' => '',
+  'Invoice to:'                 => '',
   'Invoice total'               => '',
   'Invoice total less discount' => '',
   'Invoice with Storno (abbreviation)' => '',
   'Invoices'                    => '',
+  'Invoices can only be changed on the day they are posted.' => '',
+  'Invoices with payments cannot be canceled.' => '',
   'Invoices, Credit Notes & AR Transactions' => '',
   'Is Searchable'               => '',
   'Is this a summary account to record' => '',
   'It can be changed later but must be unique within the installation.' => '',
   'It is not allowed that a summary account occurs in a drop-down menu!' => '',
   'It is possible that even after such a correction there is something wrong with this transaction (e.g. taxes that don\'t match the selected taxkey). Therefore you should re-run the general ledger analysis.' => '',
-  'It is possible to make a quick DATEV export everytime you post a record to ensure things work nicely with their data requirements. This will result in a slight overhead though you can enable this for each type of record independantly.' => '',
+  'It is possible to make a quick DATEV export everytime you post a record to ensure things work nicely with their data requirements. This will result in a slight overhead though you can enable this for each type of record independently.' => '',
+  'It will not be further modified by any other source, and will be offered in records like this.' => '',
   'It will simply set the taxkey to 0 (meaning "no taxes") which is the correct value for such inventory transactions.' => '',
-  'Item deleted!'               => '',
+  'Italy'                       => '',
+  'Item does not exists in the database' => '',
   'Item mode'                   => '',
   'Item multi selection with qty' => '',
-  'Item not on file!'           => '',
   'Item values'                 => '',
   'Item variables'              => '',
   'Jahresverkehrszahlen neu'    => '',
   'Jan'                         => '',
   'January'                     => '',
+  'Job history'                 => '',
   'Journal'                     => '',
   'Journal of Last 10 Transfers' => '',
   'Jul'                         => '',
@@ -1208,32 +1937,28 @@ $self->{texts} = {
   'Jump to'                     => '',
   'Jun'                         => '',
   'June'                        => '',
-  'KNE-Export erfolgreich!'     => '',
   'KNr. beim Kunden'            => '',
-  'Keine Suchergebnisse gefunden!' => '',
-  'Knowledge'                   => '',
-  'Konten'                      => '',
+  'KOST Quantity'               => '',
+  'Keep the project link the way it is.' => '',
+  'Known Column'                => '',
   'L'                           => '',
   'LIABILITIES'                 => '',
   'LP'                          => '',
   'LaTeX Templates'             => '',
-  'Label'                       => '',
   'Landscape'                   => '',
   'Language'                    => '',
   'Language (database ID)'      => '',
   'Language (name)'             => '',
-  'Language Values'             => '',
-  'Language deleted!'           => '',
-  'Language missing!'           => '',
-  'Language saved!'             => '',
   'Language settings'           => '',
   'Languages'                   => '',
   'Languages and translations'  => '',
   'Last Article Number'         => '',
   'Last Assembly Number'        => '',
+  'Last Assortment Number'      => '',
   'Last Cost'                   => '',
   'Last Credit Note Number'     => '',
   'Last Customer Number'        => '',
+  'Last Dunning'                => '',
   'Last Invoice Number'         => '',
   'Last Purchase Delivery Order Number' => '',
   'Last Purchase Order Number'  => '',
@@ -1245,39 +1970,70 @@ $self->{texts} = {
   'Last Transaction'            => '',
   'Last Vendor Number'          => '',
   'Last command output'         => '',
+  'Last modification'           => '',
+  'Last opening balance or all transactions' => '',
+  'Last opening balance or start of year' => '',
+  'Last ordernumber'            => '',
+  'Last row, description'       => '',
+  'Last row, partnumber'        => '',
+  'Last row, qty'               => '',
   'Last run at'                 => '',
+  'Last transaction'            => '',
+  'Last update'                 => '',
   'Lastcost'                    => '',
   'Lastcost (with X being a number)' => '',
-  'Lead'                        => '',
-  'Leads'                       => '',
+  'Lastname'                    => '',
+  'Leading and trailing whitespaces have been removed.' => '',
   'Left'                        => '',
+  'Letter'                      => '',
+  'Letter Draft'                => '',
+  'Letter deleted'              => '',
+  'Letter saved!'               => '',
+  'Letternumber'                => '',
+  'Letters'                     => '',
   'Liability'                   => '',
   'Limit part selection'        => '',
   'Line Total'                  => '',
-  'Line and column'             => '',
   'Line endings'                => '',
   'Link direction'              => '',
   'Link to'                     => '',
+  'Link to invoice'             => '',
+  'Link to the following project:' => '',
   'Linked Records'              => '',
+  'Linked invoices'             => '',
+  'Liquidity projection'        => '',
   'List Accounts'               => '',
-  'List Languages'              => '',
   'List Price'                  => '',
   'List Printers'               => '',
   'List Transactions'           => '',
   'List Users, Clients and User Groups' => '',
+  'List all rows'               => '',
   'List current background jobs' => '',
   'List export'                 => '',
-  'List of bank accounts'       => '',
   'List of bank collections'    => '',
   'List of bank transfers'      => '',
   'List of custom variables'    => '',
+  'List of database upgrades to be applied:' => '',
+  'List of jobs'                => '',
+  'List of tax zones'           => '',
   'List open SEPA exports'      => '',
-  'Load draft'                  => '',
+  'List time recordings of all staff members' => '',
+  'Listprice'                   => '',
+  'Load'                        => '',
+  'Load an existing draft'      => '',
+  'Load letter draft'           => '',
   'Load profile'                => '',
   'Loading...'                  => '',
+  'Local Bank Code'             => '',
   'Local Tax Office Preferences' => '',
+  'Local account'               => '',
+  'Local account number'        => '',
+  'Local bank account'          => '',
+  'Local bank code'             => '',
+  'Lock'                        => '',
   'Lock System'                 => '',
   'Lock and unlock installation' => '',
+  'Lock bookings'               => '',
   'Lock file handling failed. Please verify that the directory "#1" is writeable by the webserver.' => '',
   'Lockfile created!'           => '',
   'Lockfile removed!'           => '',
@@ -1288,32 +2044,54 @@ $self->{texts} = {
   'Logout now'                  => '',
   'Long Dates'                  => '',
   'Long Description'            => '',
+  'Long Description (invoices)' => '',
+  'Long Description (quotations & orders)' => '',
+  'Long Description for invoices' => '',
+  'Long Description for quotations & orders' => '',
+  'Longdescription dialog size percentage from main window (0 means fix values)' => '',
+  'Loss'                        => '',
+  'Loss carried forward account' => '',
+  'Luxembourg'                  => '',
   'MAILED'                      => '',
   'MD'                          => '',
-  'Machine'                     => '',
+  'MIME type'                   => '',
+  'MT940 file'                  => '',
+  'MT940 import'                => '',
+  'MT940 import preview'        => '',
+  'MT940 import result'         => '',
+  'Mails'                       => '',
+  'Main Contact Person'         => '',
   'Main Preferences'            => '',
   'Main sorting'                => '',
   'Make'                        => '',
   'Make (vendor\'s database ID, number or name; with X being a number)' => '',
   'Make compatible for import'  => '',
   'Make default profile'        => '',
+  'Makemodel Price'             => '',
   'Manage Custom Variables'     => '',
   'Mandantennummer'             => '',
+  'Mandate Date of Signature'   => '',
+  'Mandator ID'                 => '',
   'Mandatory Departments'       => '',
+  'Manually sent E-Mails will have their BCC field appended with this address. Will not trigger for employees without the right to send bcc, and will not apply to mails sent by automated jobs.' => '',
   'Map'                         => '',
+  'Mappings (csv_import)'       => '',
   'Mar'                         => '',
   'March'                       => '',
   'Margepercent'                => '',
   'Margetotal'                  => '',
   'Margins'                     => '',
+  'Mark as booked'              => '',
   'Mark as closed'              => '',
-  'Mark as paid?'               => '',
+  'Mark as paid               => '',
   'Mark as shop article if column missing' => '',
-  'Mark closed'                 => '',
   'Marked as paid'              => '',
   'Marked entries printed!'     => '',
+  'Mass Create Print Sales Invoice from Delivery Order' => '',
   'Master Data'                 => '',
   'Master Data Bin Text Deleted' => '',
+  'Match Sales Invoice Serial numbers with inventory charge numbers?' => '',
+  'Matching Price Rules can apply in one of three types:' => '',
   'Max. Dunning Level'          => '',
   'Maximal amount difference'   => '',
   'Maximum future booking interval' => '',
@@ -1321,10 +2099,15 @@ $self->{texts} = {
   'May '                        => '',
   'May set the BCC field when sending emails' => '',
   'Meaning'                     => '',
+  'Media \'#1\' is not supported yet/anymore.' => '',
   'Medium Number'               => '',
   'Memo'                        => '',
-  'Menu'                        => '',
+  'Merchandise'                 => 'Merchandise',
+  'Merchandise (typeabbreviation)' => 'M',
   'Message'                     => '',
+  'Meta tag description'        => '',
+  'Meta tag keywords'           => '',
+  'Meta tag title'              => '',
   'Method'                      => '',
   'Microfiche'                  => '',
   'Minimum Amount'              => '',
@@ -1334,106 +2117,205 @@ $self->{texts} = {
   'Missing Method!'             => '',
   'Missing Tax Authoritys Preferences' => '',
   'Missing amount'              => '',
+  'Missing configuration section "authentication/#1" in "config/kivitendo.conf".' => '',
   'Missing parameter #1 in call to sub #2.' => '',
   'Missing parameter (at least one of #1) in call to sub #2.' => '',
   'Missing parameter for WebDAV file copy' => '',
   'Missing taxkeys in invoices with taxes.' => '',
   'Mitarbeiter'                 => '',
-  'Mixed (requires column "type")' => '',
+  'Mixed (requires column "type" or "pclass")' => '',
   'Mobile'                      => '',
   'Mobile1'                     => '',
   'Mobile2'                     => '',
+  'Modal Test'                  => '',
   'Model'                       => '',
   'Model (with X being a number)' => '',
+  'Modification date'           => '',
   'Module'                      => '',
   'Module home page'            => '',
   'Module name'                 => '',
+  'Mon'                         => '',
   'Monat'                       => '',
+  'Monday'                      => '',
   'Month'                       => '',
+  'Month/Year'                  => '',
   'Monthly'                     => '',
-  'More than one #1 found matching, please be more specific.' => '',
   'More than one control file with the tag \'%s\' exist.' => '',
+  'More than one file selected, please set only one checkbox!' => '',
   'Multi mode not supported.'   => '',
+  'Multiple addresses can be entered separated by commas.' => '',
   'MwSt. inkl.'                 => '',
   'Name'                        => '',
+  'Name 2'                      => '',
+  'Name 3'                      => '',
   'Name and Street'             => '',
-  'Name missing!'               => '',
-  'National Expenses'           => '',
-  'National Revenues'           => '',
+  'Name does not make sense without any bsooqr options' => '',
+  'Name in Selected Records'    => '',
+  'Name of the goal/source (if field names remote_name and remote_name_1 exist they will be combined into field "remote_name")' => '',
+  'Need a image title'          => '',
+  'Need a valid Shop Part for updating Part' => '',
+  'Need a workflow for Supplier Delivery Order' => '',
+  'Need at least one original position for the workflow Order to Delivery Order!' => '',
+  'Need charge number!'         => '',
+  'Negative reductions are possible to model price increases.' => '',
+  'Neither sections nor function blocks have been created yet.' => '',
+  'Net Income Statement'        => '',
+  'Net Value in delivery orders' => '',
   'Net amount'                  => '',
   'Net amount (for verification)' => '',
+  'Net amounts differ too much' => '',
+  'Net value in Order'          => '',
+  'Net value in closed delivery orders' => '',
+  'Net value transferred in / out' => '',
+  'Net value without delivery orders' => '',
+  'Net.Turnover'                => '',
+  'Netherlands'                 => '',
   'Netto Terms'                 => '',
   'New Password'                => '',
-  'New assembly'                => '',
-  'New bank account'            => '',
+  'New Purchase Price Rule'     => '',
+  'New Sales Price Rule'        => '',
+  'New address'                 => '',
   'New client #1: The database configuration fields "host", "port", "name" and "user" must not be empty.' => '',
   'New client #1: The name must be unique and not empty.' => '',
   'New contact'                 => '',
-  'New customer'                => '',
   'New filter for tax accounts' => '',
   'New invoice'                 => '',
   'New name'                    => '',
-  'New part'                    => '',
+  'New row, description'        => '',
+  'New row, partnumber'         => '',
+  'New row, qty'                => '',
   'New sales order'             => '',
-  'New service'                 => '',
   'New shipto'                  => '',
-  'New vendor'                  => '',
+  'New shop orders'             => '',
   'New window/tab'              => '',
   'Next Dunning Level'          => '',
+  'Next month'                  => '',
   'Next run at'                 => '',
   'No'                          => '',
-  'No %s was found matching the search parameters.' => '',
+  'No 1:n or n:1 relation'      => '',
+  'No AP Record Template for this vendor found, please add one' => '',
+  'No AP template was found.'   => '',
+  'No Billing and ship to address, for Order Number #1 with ID Billing #2 and ID Shipping #3' => '',
   'No Company Address given'    => '',
   'No Company Name given'       => '',
   'No Customer was found matching the search parameters.' => '',
+  'No GL template was found.'   => '',
+  'No Journal'                  => '',
+  'No Order Number'             => '',
+  'No Order items fetched'      => '',
+  'No Shopdescription'          => '',
+  'No Shopimages'               => '',
+  'No VAT Info for this Factur-X/ZUGFeRD invoice, please ask your vendor to add this for his Factur-X/ZUGFeRD data.' => '',
   'No Vendor was found matching the search parameters.' => '',
   'No action defined.'          => '',
+  'No address selected to delete' => '',
+  'No article has been selected yet.' => '',
+  'No articles have been added yet.' => '',
+  'No assembly has been selected yet.' => '',
   'No background job has been created yet.' => '',
+  'No bank account chosen!'     => '',
+  'No bank account configured for bank code/BIC #1, account number/IBAN #2.' => '',
+  'No bank account flagged for Factur-X/ZUGFeRD usage was found.' => '',
+  'No bank account flagged for QRBill usage was found.' => '',
   'No bank information has been entered in this customer\'s master data entry. You cannot create bank collections unless you enter bank information.' => '',
   'No bank information has been entered in this vendor\'s master data entry. You cannot create bank transfers unless you enter bank information.' => '',
+  'No billing city'             => '',
   'No bins have been added to this warehouse yet.' => '',
-  'No business has been created yet.' => '',
+  'No carry-over chart configured!' => '',
+  'No changes since previous version.' => '',
   'No clients have been created yet.' => '',
   'No contact selected to delete' => '',
+  'No contra account selected!' => '',
+  'No custom data exports have been created yet.' => '',
+  'No customer email'           => '',
   'No customer has been selected yet.' => '',
+  'No customer selected or found!' => '',
   'No data was found.'          => '',
   'No default currency'         => '',
+  'No default value'            => '',
+  'No delivery orders have been selected.' => '',
   'No delivery term has been created yet.' => '',
-  'No department has been created yet.' => '',
   'No dunnings have been selected for printing.' => '',
+  'No email for current user #1 defined.' => '',
+  'No email for user with login #1 defined.' => '',
+  'No email recipient for customer #1 defined.' => '',
+  'No end date given, setting to today' => '',
+  'No entries can be imported.' => '',
+  'No entries have been imported yet.' => '',
+  'No entries have been selected.' => '',
+  'No errors have occurred.'    => '',
   'No file has been uploaded yet.' => '',
+  'No file selected, please set one checkbox!' => '',
+  'No file uploaded yet'        => '',
+  'No filename exists!'         => '',
+  'No function blocks have been created yet.' => '',
   'No groups have been created yet.' => '',
-  'No or an unknown authenticantion module specified in "config/kivitendo.conf".' => '',
-  'No part was found matching the search parameters.' => '',
+  'No internal phone extensions have been configured yet.' => '',
+  'No invoices have been selected.' => '',
+  'No part was selected.'       => '',
   'No payment term has been created yet.' => '',
+  'No picture has been uploaded' => '',
+  'No picture uploaded yet'     => '',
   'No prices will be updated because no prices have been entered.' => '',
   'No print templates have been created for this client yet. Please do so in the client configuration.' => '',
   'No printers have been created yet.' => '',
   'No problems were recognized.' => '',
+  'No profit and loss carried forward chart configured!' => '',
+  'No profit carried forward chart configured!' => '',
+  'No quotations or orders have been created yet.' => '',
   'No report with id #1'        => '',
+  'No requirement spec templates have been created yet.' => '',
+  'No results.'                 => '',
+  'No search results found!'    => '',
+  'No sections created yet'     => '',
+  'No sections have been created so far.' => '',
+  'No sections have been created yet.' => '',
+  'No shipto city'              => '',
   'No shipto selected to delete' => '',
+  'No start date given, setting to #1' => '',
+  'No stock to transfer'        => '',
+  'No such job #1 in the database.' => '',
   'No summary account'          => '',
+  'No superuser credentials were entered.' => '',
+  'No template has been selected yet.' => '',
+  'No text blocks have been created for this position.' => '',
+  'No text has been entered yet.' => '',
+  'No time recordings to convert' => '',
+  'No title yet'                => '',
+  'No transaction on chart bank chosen!' => '',
   'No transaction selected!'    => '',
   'No transactions yet.'        => '',
   'No transfers were executed in this export.' => '',
   'No users have been created yet.' => '',
   'No valid number entered for pricegroup "#1".' => '',
   'No vendor has been selected yet.' => '',
+  'No vendor selected or found!' => '',
   'No warehouse has been created yet or the quantity of the bins is not configured yet.' => '',
+  'No year given for method year' => '',
   'No.'                         => '',
+  'No/individual shipping address' => '',
   'None'                        => '',
+  'None (PriceSource Discount)' => '',
+  'None (PriceSource)'          => '',
+  'None (typeabbreviation)'     => '-',
+  'Normal'                      => '',
   'Normal users cannot log in.' => '',
+  'Normalize Customer / Vendor names' => '',
+  'Normalize part description and part notes' => '',
   'Not Discountable'            => '',
   'Not delivered'               => '',
   'Not done yet'                => '',
+  'Not enough in stock for the serial number #1' => '',
   'Not obsolete'                => '',
+  'Not yet implemented'         => '',
   'Note'                        => '',
+  'Note that parameter names must not be quoted.' => '',
   'Note: Taxkeys must have a "valid from" date, and will not behave correctly without.' => '',
+  'Note: the object is already in use. Therefore some values cannot be changed.' => '',
   'Notes'                       => '',
   'Notes (translation for #1)'  => '',
   'Notes (will appear on hard copy)' => '',
   'Notes for customer'          => '',
-  'Notes for vendor'            => '',
   'Nothing has been selected for removal.' => '',
   'Nothing has been selected for transfer.' => '',
   'Nothing selected!'           => '',
@@ -1444,11 +2326,22 @@ $self->{texts} = {
   'Number'                      => '',
   'Number Format'               => '',
   'Number missing in Row'       => '',
+  'Number of Data: '            => '',
   'Number of bins'              => '',
   'Number of columns of custom variables in form details (second row)' => '',
   'Number of copies'            => '',
+  'Number of data sets'         => '',
+  'Number of data uploaded:'    => '',
+  'Number of delivery orders created:' => '',
+  'Number of delivery orders printed:' => '',
   'Number of entries changed: #1' => '',
+  'Number of invoices'          => '',
+  'Number of invoices created:' => '',
+  'Number of invoices printed:' => '',
+  'Number of invoices to create' => '',
+  'Number of months'            => '',
   'Number of new bins'          => '',
+  'Number of orders created:'   => '',
   'Number pages'                => '',
   'Number variables: \'PRECISION=n\' forces numbers to be shown with exactly n decimal places.' => '',
   'OB Transaction'              => '',
@@ -1457,27 +2350,43 @@ $self->{texts} = {
   'Oct'                         => '',
   'October'                     => '',
   'Off'                         => '',
+  'Ok'                          => '',
   'Old (on the side)'           => '',
   'Old configuration files'     => '',
   'On'                          => '',
   'On Hand'                     => '',
   'On Order'                    => '',
+  'On the next page the type of all variables can be set.' => '',
   'One of the columns "qty" or "target_qty" must be given. If "target_qty" is given, the quantity to transfer for each transfer will be calculate, so that the quantity for this part, warehouse and bin will result in the given "target_qty" after each transfer.' => '',
+  'One of the units used (#1) cannot be mapped to a known unit code from the UN/ECE Recommendation 20 list.' => '',
   'One or more Perl modules missing' => '',
-  'Only Warnings and Errors'    => '',
+  'Onhand only sets the quantity in master data, not in inventory. This is only a legacy info field and will be overwritten as soon as a inventory transfer happens.' => '',
+  'Only Lines with Notes or Errors' => '',
+  'Only Price'                  => '',
+  'Only Stock'                  => '',
+  'Only booked accounts'        => '',
   'Only due follow-ups'         => '',
   'Only groups that have been configured for the client the user logs in to will be considered.' => '',
+  'Only list customer\'s projects in sales records' => '',
+  'Only run tests from this file:' => '',
   'Only shown in item mode'     => '',
   'Oops. No valid action found to dispatch. Please report this case to the kivitendo team.' => '',
   'Open'                        => '',
   'Open Amount'                 => '',
+  'Open Amount at Last Payment Date' => '',
+  'Open Items'                  => '',
+  'Open Orders'                 => '',
   'Open a further kivitendo window or tab' => '',
   'Open amount'                 => '',
   'Open in new window'          => '',
+  'Open invoice'                => '',
+  'Open new tab'                => '',
+  'Open sales delivery orders'  => '',
   'Open this Website'           => '',
   'OpenDocument/OASIS'          => '',
   'Openings'                    => '',
-  'Opportunity'                 => '',
+  'Option'                      => '',
+  'Optional'                    => '',
   'Optional comment'            => '',
   'Options'                     => '',
   'Or download the whole Installation Documentation as PDF (350kB) for off-line study (currently in German Language): ' => '',
@@ -1486,75 +2395,124 @@ $self->{texts} = {
   'Order Date missing!'         => '',
   'Order Number'                => '',
   'Order Number missing!'       => '',
+  'Order amount'                => '',
   'Order deleted!'              => '',
+  'Order item search'           => '',
+  'Order number invalid. Must be less then or equal to 7 digits after prefix.' => '',
+  'Order probability'           => '',
+  'Order probability & expected billing date' => '',
+  'Order value periodicity'     => '',
   'Order/Item row name'         => '',
+  'Order/Item/Stock row name'   => '',
+  'Order/RFQ Number'            => '',
   'OrderItem'                   => '',
   'Ordered'                     => '',
   'Orders'                      => '',
   'Orders / Delivery Orders deleteable' => '',
+  'Orders to fetch'             => '',
+  'Orders to fetch neeeds a positive Integer' => '',
   'Orientation'                 => '',
+  'Orig. Size w/h'              => '',
+  'Origin of personal data'     => '',
   'Orphaned'                    => '',
   'Orphaned currencies'         => '',
-  'Other'                       => '',
+  'Other Matches'               => '',
+  'Other party'                 => '',
+  'Other recipients'            => '',
   'Other users\' follow-ups'    => '',
   'Other values are ignored.'   => '',
   'Others'                      => '',
   'Otherwise the variable is only available for printing.' => '',
   'Otherwise you can simply check create warehouse and bins and define a name for the warehouse (Bins will be created automatically) and then continue' => '',
+  'Our routing id at customer'  => '',
   'Out of balance transaction!' => '',
   'Out of balance!'             => '',
   'Output Number Format'        => '',
-  'Outputformat'                => '',
+  'Overdue invoices'            => '',
   'Overdue sales quotations and requests for quotations' => '',
+  'Override'                    => '',
   'Override invoice language'   => '',
+  'Overview'                    => '',
+  'Overview kivitendo modules'  => '',
+  'Own bank account number or IBAN' => '',
+  'Own bank code'               => '',
+  'Owner of account'            => '',
   'PAYMENT POSTED'              => '',
   'PDF'                         => '',
   'PDF (OpenDocument/OASIS)'    => '',
+  'PDF export'                  => '',
   'PDF export -- options'       => '',
+  'PDF export with attachments' => '',
+  'PLZ Grosskunden'             => '',
   'POSTED'                      => '',
   'POSTED AS NEW'               => '',
+  'PREVIEWED'                   => '',
   'PRINTED'                     => '',
   'Package name'                => '',
   'Packing Lists'               => '',
   'Page'                        => '',
   'Page #1/#2'                  => '',
   'Paid'                        => '',
+  'Paid amount'                 => '',
+  'Parsing the XMP metadata failed.' => '',
   'Part'                        => '',
+  'Part "#1" has chargenumber or best before date set. So it cannot be transfered automatically.' => '',
+  'Part #1 exists in warehouse #2, but not in warehouse #3 ' => '',
   'Part (database ID)'          => '',
+  'Part (typeabbreviation)'     => 'P',
+  'Part Classification'         => '',
   'Part Description'            => '',
+  'Part Description is too long for this Shopware version. It should have lower than 255 characters.' => '',
   'Part Description missing!'   => '',
   'Part Notes'                  => '',
   'Part Number'                 => '',
   'Part Number missing!'        => '',
-  'Part \"#1\" has chargenumber or best before date set. So it cannot be transfered automaticaly.' => '',
+  'Part Test'                   => '',
+  'Part Type'                   => '',
+  'Part Unit'                   => '',
+  'Part classifications'        => '',
+  'Part marked as "Shop part"'  => '',
   'Part picker'                 => '',
+  'Part successful counted'     => '',
+  'Part with partnumber: #1 not found' => '',
+  'PartClassAbbreviation'       => '',
+  'Part_br_Description'         => 'Description',
+  'Partdescriptipion'           => '',
+  'Partial invoices'            => '',
   'Partnumber'                  => '',
-  'Partnumber must not be set to empty!' => '',
-  'Partnumber not unique!'      => '',
   'Parts'                       => '',
+  'Parts Classification'        => '',
   'Parts Inventory'             => '',
   'Parts Master Data'           => '',
-  'Parts must have an entry type.' => '',
   'Parts with existing part numbers' => '',
   'Parts, services and assemblies' => '',
+  'Partsgroup'                  => '',
   'Partsgroup (database ID)'    => '',
   'Partsgroup (name)'           => '',
+  'Partsgroup is required for parts' => '',
+  'Partsgroups'                 => '',
   'Partsgroups where variables are shown' => '',
   'Password'                    => '',
+  'Paste'                       => '',
+  'Paste template'              => '',
+  'Path'                        => '',
+  'Payable account'             => '',
   'Payables'                    => '',
   'Payment'                     => '',
   'Payment / Delivery Options'  => '',
+  'Payment Date'                => '',
   'Payment Reminder'            => '',
   'Payment Terms'               => '',
   'Payment Terms missing in row ' => '',
+  'Payment bookings disallowed. After the booking this record may be suggested with the amount of \'#1\' or otherwise has to be choosen manually. No automatic payment booking will be done to chart \'#2\'.' => '',
   'Payment date missing!'       => '',
   'Payment description'         => '',
-  'Payment description detail'  => '',
-  'Payment list as PDF'         => '',
+  'Payment list'                => '',
   'Payment posted!'             => '',
   'Payment terms'               => '',
   'Payment terms (database ID)' => '',
   'Payment terms (name)'        => '',
+  'Payment type'                => '',
   'Payments'                    => '',
   'Payments Changeable'         => '',
   'Per. Inv.'                   => '',
@@ -1569,21 +2527,35 @@ $self->{texts} = {
   'Periodic inventory'          => '',
   'Periodic invoices active'    => '',
   'Periodic invoices inactive'  => '',
-  'Periodicity'                 => '',
+  'Permissions for invoices'    => '',
   'Perpetual inventory'         => '',
-  'Person'                      => '',
   'Personal settings'           => '',
   'Phone'                       => '',
+  'Phone Notes'                 => '',
+  'Phone extension'             => '',
+  'Phone extension missing in user configuration' => '',
+  'Phone note has been created.' => '',
+  'Phone note has been deleted.' => '',
+  'Phone note has been updated.' => '',
+  'Phone note needs a subject and a body.' => '',
+  'Phone note not found for this order.' => '',
+  'Phone password'              => '',
+  'Phone password missing in user configuration' => '',
   'Phone1'                      => '',
   'Phone2'                      => '',
   'Pick List'                   => '',
+  'Picture #1: #2'              => '',
   'Pictures for parts'          => '',
   'Pictures for search parts'   => '',
   'Please Check the bank information for each customer:' => '',
   'Please Check the bank information for each vendor:' => '',
+  'Please add a valid VAT-ID for this vendor: #1' => '',
   'Please ask your administrator to create warehouses and bins.' => '',
   'Please change the partnumber of the following parts and run the update again:' => '',
+  'Please choose a part.'       => '',
   'Please choose for which categories the taxes should be displayed (otherwise remove the ticks):' => '',
+  'Please choose the action to be processed for your target quantity:' => '',
+  'Please configure the carry over and profit and loss accounts for year-end closing in the client configuration!' => '',
   'Please contact your administrator or a service provider.' => '',
   'Please contact your administrator.' => '',
   'Please correct the settings and try again or deactivate that client.' => '',
@@ -1594,19 +2566,22 @@ $self->{texts} = {
   'Please enter the name for the new client.' => '',
   'Please enter the name for the new group.' => '',
   'Please enter the name of the database that will be used as the template for the new database:' => '',
+  'Please enter the new name:'  => '',
   'Please enter the sales tax identification number.' => '',
   'Please enter the taxnumber in the client configuration.' => '',
   'Please enter values'         => '',
   'Please insert object dimensions below.' => '',
-  'Please insert your language values below' => '',
-  'Please insert your longdescription below' => '',
   'Please install the below listed modules or ask your system administrator to.' => '',
   'Please log in to the administration panel.' => '',
+  'Please modify filename'      => '',
+  'Please provide corresponding credentials.' => '',
   'Please re-run the analysis for broken general ledger entries by clicking this button:' => '',
   'Please read the file'        => '',
   'Please select a customer from the list below.' => '',
-  'Please select a part from the list below.' => '',
+  'Please select a customer.'   => '',
+  'Please select a delivery date.' => '',
   'Please select a vendor from the list below.' => '',
+  'Please select a vendor.'     => '',
   'Please select the dataset you want to delete:' => '',
   'Please select the destination bank account for the collections:' => '',
   'Please select the source bank account for the transfers:' => '',
@@ -1614,60 +2589,130 @@ $self->{texts} = {
   'Please set another taxnumber for the following taxes and run the update again:' => '',
   'Please specify a description for the warehouse designated for these goods.' => '',
   'Plural'                      => '',
+  'Poland'                      => '',
   'Port'                        => '',
   'Portrait'                    => '',
+  'Position'                    => '',
+  'Position #1'                 => '',
+  'Position #1: #2'             => '',
+  'Position type in quotation/order' => '',
+  'Positions'                   => '',
   'Post'                        => '',
   'Post Payment'                => '',
+  'Post and new booking'        => '',
+  'Post and upload document'    => '',
   'Post payments'               => '',
+  'Post payments for selected invoices' => '',
+  'Postal Invoice'              => '',
   'Posting Configuration'       => '',
+  'Posting Key'                 => '',
+  'Posting Text'                => '',
   'Postscript'                  => '',
   'Posustva_coa'                => '',
+  'Pre-defined Texts'           => '',
+  'Preamble'                    => '',
+  'Precision'                   => '',
+  'Precision Note'              => '<b>Attention:</b> currently with a precision of 0.05 sales sheets have to use the default currency.',
   'Preferences'                 => '',
   'Preferences saved!'          => '',
   'Prefix for the new bins\' names' => '',
   'Preis'                       => '',
-  'Preisklasse'                 => '',
   'Prepare bank collection via SEPA XML' => '',
   'Prepare bank transfer via SEPA XML' => '',
   'Prepayment'                  => '',
+  'Preselect Customer/Vendor documents as email attachments' => '',
+  'Preselect all documents for the current selected parts in a record as a mail attachment.' => '',
+  'Preselect all documents saved for the current customer/vendor as a mail attachment.' => '',
+  'Preselect all documents saved for the current record as a mail attachment.' => '',
+  'Preselect part documents as email attachments' => '',
+  'Preselect record documents as email attachments' => '',
+  'Preselected bin'             => '',
+  'Preselected cutoff date'     => '',
+  'Preselected warehouse'       => '',
+  'Preset email body for periodic invoices' => '',
+  'Preset email strings'        => '',
+  'Preset email subject for periodic invoices' => '',
+  'Preset email text for purchase orders' => '',
+  'Preset email text for requests (rfq)' => '',
+  'Preset email text for sales delivery orders' => '',
+  'Preset email text for sales invoices' => '',
+  'Preset email text for sales invoices with direct debit' => '',
+  'Preset email text for sales orders' => '',
+  'Preset email text for sales quotations' => '',
+  'Prevent browser\'s back button in sales invoices' => '',
   'Preview'                     => '',
   'Preview Mode'                => '',
+  'Previous month'              => '',
   'Previous transdate text'     => '',
   'Previous transnumber text'   => '',
   'Price'                       => '',
+  'Price #1'                    => '',
   'Price Factor'                => '',
   'Price Factors'               => '',
+  'Price List'                  => '',
+  'Price Rule'                  => '',
+  'Price Rules'                 => '',
+  'Price Source'                => '',
+  'Price Sources to be disabled in this client' => '',
+  'Price Types'                 => '',
+  'Price and Stock'             => '',
   'Price factor (database ID)'  => '',
   'Price factor (name)'         => '',
-  'Price factor deleted!'       => '',
-  'Price factor saved!'         => '',
+  'Price group'                 => '',
   'Price group (database ID)'   => '',
   'Price group (name)'          => '',
+  'Price history for master data' => '',
   'Price information'           => '',
+  'Price or discount must not be zero.' => '',
+  'Price rules must have at least one rule.' => '',
+  'Price source'                => '',
+  'Price sources deactivated in this client' => '',
+  'Price type'                  => '',
+  'Price type explanation'      => '',
+  'Price updated'               => '',
   'Pricegroup'                  => '',
-  'Pricegroup deleted!'         => '',
-  'Pricegroup missing!'         => '',
-  'Pricegroup saved!'           => '',
   'Pricegroups'                 => '',
+  'Prices'                      => '',
   'Print'                       => '',
   'Print and Post'              => '',
   'Print automatically'         => '',
+  'Print both sided'            => '',
+  'Print delivery orders'       => '',
+  'Print destination'           => '',
+  'Print destination (copy)'    => '',
   'Print dunnings'              => '',
   'Print list'                  => '',
   'Print options'               => '',
+  'Print record'                => '',
+  'Print template base file name' => '',
   'Print templates'             => '',
   'Print templates to use'      => '',
+  'Printdate'                   => '',
   'Printer'                     => '',
   'Printer Command'             => '',
   'Printer Description'         => '',
   'Printer Management'          => '',
   'Printer management'          => '',
   'Printing ... '               => '',
+  'Printing Documents'          => '',
+  'Printing invoices (this can take a while)' => '',
+  'Prior year'                  => '',
+  'Priority'                    => '',
   'Private E-mail'              => '',
   'Private Phone'               => '',
   'Problem'                     => '',
+  'Produce'                     => '',
   'Produce Assembly'            => '',
+  'Produce Assembly Configuration' => '',
+  'Produce assembly consumes services if assigned as a assembly item' => '',
+  'Produce assembly only if all parts are in the same warehouse' => '',
+  'Production'                  => 'Production',
+  'Production (typeabbreviation)' => 'W',
   'Productivity'                => '',
+  'Productivity (TODO list, Follow-Ups)' => '',
+  'Profit'                      => '',
+  'Profit and loss accounts'    => '',
+  'Profit carried forward account' => '',
   'Profit determination'        => '',
   'Proforma Invoice'            => '',
   'Program'                     => '',
@@ -1676,45 +2721,86 @@ $self->{texts} = {
   'Project (description)'       => '',
   'Project (number)'            => '',
   'Project Description'         => '',
+  'Project Link'                => '',
   'Project Number'              => '',
   'Project Numbers'             => '',
+  'Project Status'              => '',
   'Project Transactions'        => '',
+  'Project Type'                => '',
+  'Project Types'               => '',
+  'Project link actions'        => '',
+  'Project of assigned order must match assigned project.' => '',
+  'Project picker'              => '',
+  'Project statuses'            => '',
+  'Project type'                => '',
+  'Project types'               => '',
   'Projects'                    => '',
+  'Projects: edit the list of employees allowed to view invoices' => '',
   'Projecttransactions'         => '',
+  'Proposal'                    => '',
+  'Proposals'                   => '',
+  'Protocol'                    => '',
+  'Proxy'                       => '',
   'Prozentual/Absolut'          => '',
+  'Purchase'                    => 'Purchase',
+  'Purchase (typeabbreviation)' => 'P',
+  'Purchase Delivery Order'     => '',
   'Purchase Delivery Orders'    => '',
   'Purchase Delivery Orders deleteable' => '',
   'Purchase Invoice'            => '',
   'Purchase Invoices'           => '',
   'Purchase Order'              => '',
   'Purchase Orders'             => '',
+  'Purchase Orders Services are deliverable' => '',
   'Purchase Orders deleteable'  => '',
-  'Purchase Price'              => '',
-  'Purchase Prices'             => '',
+  'Purchase Price Rules'        => '',
+  'Purchase Price Rules '       => '',
   'Purchase delivery order'     => '',
   'Purchase invoices'           => '',
   'Purchase invoices changeable' => '',
   'Purchase net amount'         => '',
   'Purchase price'              => '',
   'Purchase price total'        => '',
+  'Purchasing & Sales'          => '',
   'Purpose'                     => '',
+  'Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")' => '',
+  'Purpose/Reference'           => '',
+  'QR bill without amount'      => '',
+  'QR-Code placeholder image: QRCodePlaceholder not found in template.' => '',
+  'QR-Image generation failed: ' => '',
+  'QUEUED'                      => '',
   'Qty'                         => '',
   'Qty according to delivery order' => '',
+  'Qty equal or less than #1'   => '',
+  'Qty equal or more than #1'   => '',
+  'Qty equals #1'               => '',
+  'Qty in Order'                => '',
   'Qty in Selected Records'     => '',
+  'Qty in closed delivery orders' => '',
+  'Qty in delivery orders'      => '',
   'Qty in stock'                => '',
+  'Qty less than #1'            => '',
+  'Qty more than #1'            => '',
   'Quantity'                    => '',
   'Quantity missing.'           => '',
   'Quartal'                     => '',
   'Quarter'                     => '',
   'Quarterly'                   => '',
+  'Query parameters'            => '',
   'Queue'                       => '',
+  'Quick Search'                => '',
+  'Quick Searches that will be shown in the header for this user' => '',
+  'Quick Searches that will be shown in the header in this client' => '',
   'Quotation'                   => '',
   'Quotation Date'              => '',
   'Quotation Date missing!'     => '',
   'Quotation Number'            => '',
   'Quotation Number missing!'   => '',
   'Quotation deleted!'          => '',
+  'Quotation/Order Date'        => '',
   'Quotations'                  => '',
+  'Quotations and orders'       => '',
+  'Quotations/Orders actions'   => '',
   'Quote character'             => '',
   'Quote chararacter'           => '',
   'Quoted'                      => '',
@@ -1723,46 +2809,93 @@ $self->{texts} = {
   'RFQ Date'                    => '',
   'RFQ Number'                  => '',
   'RFQs'                        => '',
+  'RMA Delivery Order'          => '',
+  'RMA Delivery Orders'         => '',
+  'RMA delivery order'          => '',
   'ROP'                         => '',
   'Ranges of numbers'           => '',
+  'Re-numbering all sections and function blocks in the order they are currently shown cannot be undone.' => '',
   'Re-run analysis'             => '',
+  'Read all employee e-mails'   => '',
+  'Really cancel link?'         => '',
+  'Realm'                       => '',
   'Receipt'                     => '',
   'Receipt posted!'             => '',
   'Receipt, payment, reconciliation' => '',
   'Receipts'                    => '',
+  'Receipts attached/extra'     => '',
+  'Receivable account'          => '',
   'Receivables'                 => '',
-  'Rechnungsnummer'             => '',
+  'Receivables account'         => '',
+  'Receivables account (account number)' => '',
+  'Received payments can only be posted for sales invoices and purchase credit notes.' => '',
+  'Recipients'                  => '',
+  'Reconcile'                   => '',
   'Reconciliation'              => '',
+  'Reconciliation with bank'    => '',
+  'Record Type'                 => '',
   'Record Vendor Invoice'       => '',
   'Record in'                   => '',
+  'Record number'               => '',
+  'Record numbers changeable'   => '',
+  'Record templates'            => '',
+  'Record type to create'       => '',
+  'Record\'s files'             => '',
   'Recorded Tax'                => '',
   'Recorded taxkey'             => '',
+  'Records'                     => '',
+  'Reduced Master Data'         => '',
   'Reference'                   => '',
   'Reference / Invoice Number'  => '',
   'Reference day'               => '',
   'Reference missing!'          => '',
+  'Relaxed (UTF-8)'             => '',
   'Release From Stock'          => '',
   'Remaining'                   => '',
+  'Remaining Amount'            => '',
+  'Remaining Net Amount'        => '',
+  'Remittance information optional Vendor/Customer No postfix' => '',
   'Remittance information prefix' => '',
+  'Remote Bank Code'            => '',
+  'Remote Name/Customer/Description' => '',
+  'Remote account'              => '',
+  'Remote account number'       => '',
+  'Remote bank code'            => '',
+  'Remote name'                 => '',
   'Removal'                     => '',
   'Removal from Warehouse'      => '',
   'Removal from warehouse'      => '',
   'Removal qty'                 => '',
   'Remove'                      => '',
   'Remove Draft'                => '',
-  'Remove draft when posting'   => '',
+  'Remove article'              => '',
+  'Removed sections and function blocks: #1' => '',
   'Removed spoolfiles!'         => '',
+  'Removed text blocks: #1'     => '',
   'Removing marked entries from queue ...' => '',
+  'Rename'                      => '',
+  'Rename Attachments'          => '',
+  'Rename Documents'            => '',
+  'Rename Images'               => '',
+  'Rename attachment'           => '',
+  'Renumber sections and function blocks' => '',
   'Replace the orphaned currencies by other not orphaned currencies. To do so, please delete the currency in the textfields above and replace it by another currency. You could loose or change unintentionally exchangerates. Go on very carefully since you could destroy transactions.' => '',
   'Report Positions'            => '',
   'Report about warehouse contents' => '',
   'Report about warehouse transactions' => '',
   'Report and misc. Preferences' => '',
+  'Report configuration overview' => '',
+  'Report date'                 => '',
   'Report for'                  => '',
+  'Report separately'           => '',
   'Reports'                     => '',
   'Representative'              => '',
   'Representative for Customer' => '',
   'Reqdate'                     => '',
+  'Reqdate is #1'               => '',
+  'Reqdate is after #1'         => '',
+  'Reqdate is before #1'        => '',
+  'Reqdate not set or before current month' => '',
   'Request Quotations'          => '',
   'Request for Quotation'       => '',
   'Request for Quotation Number' => '',
@@ -1771,40 +2904,92 @@ $self->{texts} = {
   'Requested execution date'    => '',
   'Requested execution date from' => '',
   'Requested execution date to' => '',
+  'Requests for Quotation'      => '',
+  'Require a transaction description in purchase and sales records' => '',
+  'Require stock out to consider a delivery order position delivered?' => '',
+  'Required access right'       => '',
   'Required by'                 => '',
+  'Requirement Spec Status'     => '',
+  'Requirement Spec Statuses'   => '',
+  'Requirement Spec Templates'  => '',
+  'Requirement Spec Type'       => '',
+  'Requirement Spec Types'      => '',
+  'Requirement Spec Version'    => '',
+  'Requirement Specs'           => '',
+  'Requirement spec'            => '',
+  'Requirement spec actions'    => '',
+  'Requirement spec function block #1 with #2 sub function blocks; description: "#3"' => '',
+  'Requirement spec number'     => '',
+  'Requirement spec picture "#1"' => '',
+  'Requirement spec section #1 "#2" with #3 function blocks and a total of #4 sub function blocks; preamble: "#5"' => '',
+  'Requirement spec sub function block #1; description: "#2"' => '',
+  'Requirement spec template \'#1\'' => '',
+  'Requirement spec template actions' => '',
+  'Requirement spec text block "#1"; content: "#2"' => '',
+  'Requirement specs'           => '',
   'Reset'                       => '',
   'Result'                      => '',
+  'Result of SQL query'         => '',
+  'Results per page'            => '',
   'Revenue'                     => '',
   'Revenue Account'             => '',
-  'Revenues EU with UStId'      => '',
-  'Revenues EU without UStId'   => '',
+  'Reversal invoices cannot be canceled.' => '',
+  'Revert to version'           => '',
   'Review of Aging list'        => '',
   'Right'                       => '',
+  'Risk'                        => '',
+  'Risk levels'                 => '',
+  'Risks'                       => '',
+  'Rounding'                    => '',
+  'Rounding Gain'               => '',
+  'Rounding Loss'               => '',
+  'Row'                         => '',
   'Row #1: amount has to be different from zero.' => '',
   'Row number'                  => '',
   'Row was created from current record' => '',
+  'Row was linked to another record' => '',
   'Row was source for current record' => '',
+  'Rule Details'                => '',
+  'Rule for customer must not be empty' => '',
+  'Rule for part must not be empty' => '',
+  'Rule for vendor must not be empty' => '',
+  'Run JavaScript unit tests'   => '',
   'Run at'                      => '',
+  'Run task server for this client with the following user' => '',
+  'Run tests'                   => '',
   'SAVED'                       => '',
   'SAVED FOR DUNNING'           => '',
   'SCREENED'                    => '',
+  'SEPA'                        => '',
   'SEPA XML download'           => '',
   'SEPA creditor ID'            => '',
-  'SEPA exports:'               => '',
+  'SEPA exports'                => '',
+  'SEPA message ID'             => '',
+  'SEPA message IDs'            => '',
   'SEPA strings'                => '',
+  'SQL query'                   => '',
+  'SWIFT MT940 format'          => '',
   'Saldo Credit'                => '',
   'Saldo Debit'                 => '',
   'Saldo neu'                   => '',
   'Saldo per'                   => '',
-  'Sale Prices'                 => '',
+  'Sales'                       => 'Sales',
+  'Sales (typeabbreviation)'    => 'S',
+  'Sales Delivery Order'        => '',
   'Sales Delivery Orders'       => '',
   'Sales Delivery Orders deleteable' => '',
   'Sales Invoice'               => '',
   'Sales Invoices'              => '',
   'Sales Order'                 => '',
+  'Sales Order delivery date interval' => '',
   'Sales Orders'                => '',
+  'Sales Orders Advance'        => '',
+  'Sales Orders Services are deliverable' => '',
   'Sales Orders deleteable'     => '',
+  'Sales Price Rules'           => '',
+  'Sales Price Rules '          => '',
   'Sales Price information'     => '',
+  'Sales Quotation valid interval' => '',
   'Sales Quotations'            => '',
   'Sales Report'                => '',
   'Sales and purchase invoices with inventory transactions with taxkeys' => '',
@@ -1815,59 +3000,98 @@ $self->{texts} = {
   'Sales margin'                => '',
   'Sales margin %'              => '',
   'Sales net amount'            => '',
+  'Sales order #1 has been created.' => '',
+  'Sales order #1 has been deleted.' => '',
+  'Sales order #1 has been updated.' => '',
   'Sales price'                 => '',
   'Sales price total'           => '',
   'Sales quotation'             => '',
+  'Sales quotation #1 has been created.' => '',
+  'Sales quotation #1 has been deleted.' => '',
+  'Sales quotation #1 has been updated.' => '',
   'Salesman'                    => '',
   'Salesman (database ID)'      => '',
+  'Salesman (login)'            => '',
   'Salesperson'                 => '',
+  'Salutation female'           => '',
+  'Salutation general'          => '',
+  'Salutation male'             => '',
+  'Salutation punctuation mark' => '',
+  'Same Filename !'             => '',
   'Same as the quote character' => '',
+  'Sat'                         => '',
   'Sat. Fax'                    => '',
   'Sat. Phone'                  => '',
+  'Saturday'                    => '',
   'Satz %'                      => '',
   'Save'                        => '',
   'Save Draft'                  => '',
   'Save and AP Transaction'     => '',
   'Save and AR Transaction'     => '',
   'Save and Close'              => '',
+  'Save and Delivery Order'     => '',
+  'Save and E-mail'             => '',
+  'Save and Final Invoice'      => '',
+  'Save and Further Invoice for Advance Payment' => '',
   'Save and Invoice'            => '',
+  'Save and Invoice for Advance Payment' => '',
   'Save and Order'              => '',
+  'Save and Purchase Order'     => '',
   'Save and Quotation'          => '',
   'Save and RFQ'                => '',
+  'Save and Sales Order'        => '',
+  'Save and Supplier Delivery Order' => '',
   'Save and close'              => '',
   'Save and execute'            => '',
+  'Save and keep open'          => '',
+  'Save and preview PDF'        => '',
+  'Save and print'              => '',
+  'Save as a new draft.'        => '',
   'Save as new'                 => '',
   'Save document in WebDAV repository' => '',
   'Save draft'                  => '',
+  'Save invoices'               => '',
   'Save profile'                => '',
+  'Save proposals'              => '',
   'Save settings as'            => '',
+  'Saving failed. Error message from the database: #1' => '',
+  'Saving failed. Error message from the server: #1' => '',
   'Saving the file \'%s\' failed. OS error message: %s' => '',
+  'Saving the record template \'#1\' failed.' => '',
+  'Saving the time recording entry failed: #1' => '',
+  'Score'                       => '',
   'Screen'                      => '',
+  'Scrollbar height percentage for form postion area (0 means no scrollbar)' => '',
   'Search'                      => '',
   'Search AP Aging'             => '',
   'Search AR Aging'             => '',
+  'Search bank transactions'    => '',
   'Search contacts'             => '',
-  'Search projects'             => '',
+  'Search for Items used in Assemblies' => '',
+  'Search parts by customer partnumber in sales order forms' => '',
+  'Search parts by vendor partnumber (model) in purchase order forms' => '',
   'Search term'                 => '',
   'Searchable'                  => '',
   'Secondary sorting'           => '',
   'Section "#1"'                => '',
+  'Section number format'       => '',
+  'Section/Function block actions' => '',
+  'Sections'                    => '',
+  'Sections that are not assigned to any of the items above will be added as new positions.' => '',
+  'See various menu entries intended for developers' => '',
   'Select'                      => '',
+  'Select Mulit-Item Options'   => '',
   'Select a Customer'           => '',
-  'Select a customer'           => '',
-  'Select a part'               => '',
-  'Select a part or assembly'   => '',
   'Select a period'             => '',
-  'Select a vendor'             => '',
-  'Select all'                  => '',
   'Select federal state...'     => '',
+  'Select file to upload'       => '',
   'Select from one of the items below' => '',
-  'Select from one of the names below' => '',
-  'Select from one of the projects below' => '',
   'Select postscript or PDF!'   => '',
   'Select tax office...'        => '',
+  'Select template to paste'    => '',
   'Select type of removal'      => '',
   'Select type of transfer'     => '',
+  'Select type of transfer in'  => '',
   'Selected'                    => '',
   'Selection'                   => '',
   'Selection fields: The option field must contain the available options for the selection. Options are separated by \'##\', for example \'Early##Normal##Late\'.' => '',
@@ -1877,33 +3101,98 @@ $self->{texts} = {
   'Sellprice for price group \'#1\'' => '',
   'Sellprice significant places' => '',
   'Semicolon'                   => '',
+  'Send a BCC to logged in user?' => '',
+  'Send a blind copy of all outgoing emails to current user\'s email address?' => '',
+  'Send email'                  => '',
+  'Send invoice via email'      => '',
+  'Send printout of record'     => '',
+  'Send the last or create the first version for this record' => '',
+  'Sender'                      => '',
+  'Sent emails can be optionally stored in the database with or without their attachments.' => '',
+  'Sent on'                     => '',
+  'Sent payments can only be posted for purchase invoices and sales credit notes.' => '',
   'Sep'                         => '',
   'Separator'                   => '',
   'Separator chararacter'       => '',
   'September'                   => '',
   'Serial No.'                  => '',
   'Serial Number'               => '',
+  'Serial Number missing in Row' => '',
+  'Server'                      => '',
+  'Server control'              => '',
   'Service'                     => '',
-  'Service Contract'            => '',
+  'Service (typeabbreviation)'  => 'Sv',
   'Service Items'               => '',
   'Service Number missing!'     => '',
   'Service, assembly or part'   => '',
   'Services'                    => '',
-  'Set Language Values'         => '',
+  'Services in Delivery Orders' => '',
+  'Set (set to)'                => '',
+  'Set all source and memo fields' => '',
+  'Set count for one or more of the items to select them' => '',
+  'Set delivery date for Sales Orders' => '',
   'Set eMail text'              => '',
+  'Set fields'                  => '',
+  'Set lastcost'                => '',
+  'Set sellprice'               => '',
+  'Set the invoice duedate as the default execution date for SEPA export.' => '',
+  'Set the invoice skonto date (if exists) as the default execution date for SEPA export.' => '',
+  'Set to paid missing'         => '',
+  'Set valid until date for Sales Quotation' => '',
   'Settings'                    => '',
   'Setup Menu'                  => '',
-  'Ship to'                     => '',
   'Ship to (database ID)'       => '',
   'Ship via'                    => '',
+  'Shipped Quantity Algorithm'  => '',
   'Shipping Address'            => '',
   'Shipping Point'              => '',
+  'Shipping address (name)'     => '',
+  'Shipping cost article is not implemented' => '',
+  'Shipping cost article not implemented' => '',
+  'Shipping costs'              => '',
   'Shipping date'               => '',
+  'Shippingcosts'               => '',
   'Shipto'                      => '',
   'Shipto deleted.'             => '',
   'Shipto is in use and was flagged invalid.' => '',
-  'Shopartikel'                 => '',
+  'Shop'                        => '',
+  'Shop Billing Address'        => '',
+  'Shop Connection Test'        => '',
+  'Shop Customer Address'       => '',
+  'Shop Delivery Address'       => '',
+  'Shop Headdata'               => '',
+  'Shop Host'                   => '',
+  'Shop Host/Connector'         => '',
+  'Shop Order'                  => '',
+  'Shop Order Date'             => '',
+  'Shop Order Number'           => '',
+  'Shop OrderIP'                => '',
+  'Shop Orderamount'            => '',
+  'Shop Orderdate'              => '',
+  'Shop Ordernotes'             => '',
+  'Shop Ordernumber'            => '',
+  'Shop Orders'                 => '',
+  'Shop article'                => '',
+  'Shop customernumber'         => '',
+  'Shop or ordernumber not selected.' => '',
+  'Shop orderdate'              => '',
+  'Shop ordernumber'            => '',
+  'Shop part'                   => '',
+  'Shop type'                   => '',
+  'Shop variables'              => '',
+  'ShopOrders'                  => '',
+  'Shopcategories'              => '',
+  'Shopimages - valid for all shops' => '',
+  'Shoporder'                   => '',
+  'Shoporder "#2" From Shop "#1" is already fetched' => '',
+  'Shoporder deleted -- '       => '',
+  'Shoporder not found'         => '',
+  'Shoporderlock'               => '',
+  'Shoporders'                  => '',
+  'Shops'                       => '',
   'Short'                       => '',
+  'Should VAT ID or taxnumber be unique for all vendors? This is checked when saving a vendor\'s master data. One of the fields is sufficient and required.' => '',
+  'Should VAT ID or taxnumber be unique for customers? This is checked when saving a customer\'s master data. One of the fields is sufficient and required.' => '',
   'Should ap transactions be and when should they be changeable or deleteable after posting?' => '',
   'Should ar transactions be and when should they be changeable or deleteable after posting?' => '',
   'Should gl transactions be and when should they be changeable or deleteable after posting?' => '',
@@ -1922,46 +3211,84 @@ $self->{texts} = {
   'Show AP transactions as part of AP invoice report' => '',
   'Show AR transactions as part of AR invoice report' => '',
   'Show Bestbefore'             => '',
+  'Show E-Mails'                => '',
   'Show Filter'                 => '',
   'Show Salesman'               => '',
+  'Show Stornos'                => '',
   'Show TODO list'              => '',
   'Show Transfer via default'   => '',
   'Show administration link'    => '',
+  'Show all details'            => '',
   'Show all parts'              => '',
   'Show by default'             => '',
+  'Show chart list'             => '',
+  'Show charts'                 => '',
   'Show custom variable search inputs' => '',
   'Show delete button in purchase delivery orders?' => '',
   'Show delete button in purchase orders?' => '',
   'Show delete button in sales delivery orders?' => '',
   'Show delete button in sales orders?' => '',
+  'Show delivery plan'          => '',
+  'Show delivery value report'  => '',
   'Show details'                => '',
   'Show details and reports of parts, services, assemblies' => '',
+  'Show document tab after posting?' => '',
+  'Show documents in WebDAV'    => '',
+  'Show documents in file storage' => '',
   'Show fields used for the best before date?' => '',
   'Show follow ups...'          => '',
   'Show help text'              => '',
+  'Show images'                 => '',
   'Show items from invoices individually' => '',
+  'Show mappings (csv_import)'  => '',
   'Show old dunnings'           => '',
+  'Show only marked as paid invoices' => '',
+  'Show only not mailed invoices' => '',
+  'Show order'                  => '',
   'Show overdue sales quotations and requests for quotations...' => '',
   'Show parts'                  => '',
+  'Show parts longdescription (notes) in select list' => '',
+  'Show purchase letters report' => '',
+  'Show record tab in customer' => '',
+  'Show record tab in vendor'   => '',
+  'Show requirement spec'       => '',
+  'Show requirement spec template' => '',
+  'Show sales letters report'   => '',
   'Show settings'               => '',
   'Show the picture in the part form' => '',
   'Show the pictures in the result for search parts' => '',
   'Show the weights of articles and the total weight in orders, invoices and delivery notes?' => '',
+  'Show update button for positions in order forms' => '',
   'Show weights'                => '',
-  'Show your TODO list after loggin in' => '',
+  'Show your TODO list after logging in' => '',
+  'Show »not delivered qty/value« column in sales and purchase orders' => '',
   'Signature'                   => '',
   'Since bin is not enforced in the parts data, please specify a bin where goods without a specified bin will be put.' => '',
   'Single quotes'               => '',
   'Single values in item mode, cumulated values in invoice mode' => '',
+  'Size'                        => '',
   'Skip'                        => '',
   'Skip entry'                  => '',
+  'Skipping because transfer amount is empty.' => '',
+  'Skipping due to existing bank transaction in database' => '',
   'Skipping due to existing entry in database' => '',
+  'Skipping due to existing entry in database with different type' => '',
+  'Skipping due to existing entry with different unit' => '',
+  'Skipping due to same partnumber in csv file' => '',
+  'Skipping non-existent article' => '',
   'Skonto'                      => '',
+  'Skonto Tax Correction for'   => '',
   'Skonto Terms'                => '',
+  'Skonto amount'               => '',
+  'Skonto information'          => '',
   'So far you could use one partnumber for severel parts, for example a service and an article.' => '',
   'Sold'                        => '',
+  'Sold order items'            => '',
+  'Soldtotal does not make sense without any bsooqr options' => '',
   'Solution'                    => '',
+  'Sorry, I am too stupid to figure out the default warehouse/bin and the sold qty. I drop the default warehouse/bin option.' => '',
   'Sort By'                     => '',
+  'Sort order'                  => '',
   'Source'                      => '',
   'Source BIC'                  => '',
   'Source IBAN'                 => '',
@@ -1970,13 +3297,21 @@ $self->{texts} = {
   'Space'                       => '',
   'Split entry detected. The values you have entered will result in an entry with more than one position on both debit and credit. Due to known problems involving accounting software kivitendo does not allow these.' => '',
   'Spoolfile'                   => '',
+  'Staff member must not be empty.' => '',
+  'Start'                       => '',
+  'Start (verb)'                => '',
   'Start Dunning Process'       => '',
-  'Start analysis'              => '',
   'Start date'                  => '',
-  'Start task server'           => '',
+  'Start of year'               => '',
+  'Start process'               => '',
   'Start the correction assistant' => '',
+  'Start time'                  => '',
+  'Start time must be earlier than end time.' => '',
+  'Startdate method'            => '',
   'Startdate_coa'               => '',
   'Starting Balance'            => '',
+  'Starting balance'            => '',
+  'Starting date'               => '',
   'Starting the task server failed.' => '',
   'Starting with version 2.6.3 the configuration files in "config" have been consolidated.' => '',
   'Statement'                   => '',
@@ -1984,34 +3319,76 @@ $self->{texts} = {
   'Statement sent to'           => '',
   'Statements sent to printer!' => '',
   'Status'                      => '',
+  'Status Shoptransfer'         => '',
+  'Status Shopupload'           => '',
+  'Step #1/#2'                  => '',
+  'Step 1 -- limit number of delivery orders to process' => '',
   'Step 2'                      => '',
+  'Step 2 -- Watch status'      => '',
   'Steuersatz'                  => '',
   'Stock'                       => '',
+  'Stock Local/Shop'            => '',
   'Stock Qty for Date'          => '',
   'Stock for part #1'           => '',
+  'Stock levels'                => '',
+  'Stock transfered'            => '',
   'Stock value'                 => '',
+  'StockInfo'                   => '',
   'Stocked Qty'                 => '',
-  'Stop task server'            => '',
+  'Stocktaking'                 => '',
+  'Stocktaking History'         => '',
+  'Stocktaking Journal'         => '',
+  'Stop (verb)'                 => '',
   'Stopping the task server failed. Output:' => '',
+  'Storage Backends'            => '',
+  'Storage Type for Attachments' => '',
+  'Storage Type for generated/imported PDF Documents' => '',
+  'Storage Type for images'     => '',
+  'Storage Type for shopimages' => '',
+  'Storing PDF in storage backend failed: #1' => '',
+  'Storing PDF to webdav folder failed: #1' => '',
+  'Storing the document in the storage backend failed: #1' => '',
+  'Storing the document to the WebDAV folder failed: #1' => '',
+  'Storing the emails in the journal is currently disabled in the client configuration.' => '',
   'Storno'                      => '',
   'Storno (one letter abbreviation)' => '',
   'Storno Invoice'              => '',
   'Street'                      => '',
+  'Street 1'                    => '',
+  'Street 2'                    => '',
+  'Strict and halt'             => '',
+  'Strict but replace'          => '',
   'Style the picture with the following CSS code' => '',
   'Stylesheet'                  => '',
+  'Sub function blocks'         => '',
   'Subject'                     => '',
   'Subject:'                    => '',
   'Subtotal'                    => '',
   'Subtotal cannot distinguish betweens record types. Only one of the selected record types will be displayed: #1' => '',
+  'Subtotals per quarter'       => '',
   'Such entries cannot be exported into the DATEV format and have to be fixed as well.' => '',
+  'Suggested invoice'           => '',
   'Sum Credit'                  => '',
   'Sum Debit'                   => '',
   'Sum for'                     => '',
+  'Sum for #1'                  => '',
+  'Sum for section'             => '',
+  'Sum of all amounts'          => '',
+  'Sum of bank #1 and sum of bookings #2' => '',
+  'Sum open amount'             => '',
   'Sum per'                     => '',
   'Summen- und Saldenliste'     => '',
+  'Sun'                         => '',
+  'Sunday'                      => '',
   'Superuser name'              => '',
+  'Supplier Delivery Order'     => '',
+  'Supplier Delivery Order has been deleted' => '',
+  'Supplier Delivery Order has been saved' => '',
+  'Supplier Delivery Orders'    => '',
+  'Supplier delivery order'     => '',
   'Supplies'                    => '',
-  'Switch Menu on / off'        => '',
+  'Surname'                     => '',
+  'Switzerland'                 => '',
   'System'                      => '',
   'System currently down for maintenance!' => '',
   'TODO list'                   => '',
@@ -2023,9 +3400,12 @@ $self->{texts} = {
   'Target bank account'         => '',
   'Target table'                => '',
   'Task Server is not running, starting it now. If this does not change, please check your task server config' => '',
+  'Task server'                 => '',
   'Task server control'         => '',
   'Task server status'          => '',
   'Tax'                         => '',
+  'Tax Account'                 => '',
+  'Tax Account Name'            => '',
   'Tax Consultant'              => '',
   'Tax ID number'               => '',
   'Tax Included'                => '',
@@ -2035,13 +3415,16 @@ $self->{texts} = {
   'Tax Office Preferences'      => '',
   'Tax Percent is a number between 0 and 100' => '',
   'Tax Period'                  => '',
-  'Tax Position'                => '',
   'Tax collected'               => '',
   'Tax deleted!'                => '',
   'Tax number'                  => '',
   'Tax paid'                    => '',
+  'Tax point'                   => '',
   'Tax rate'                    => '',
   'Tax saved!'                  => '',
+  'Tax zone'                    => '',
+  'Tax zone #1 needs a valid expense account' => '',
+  'Tax zone #1 needs a valid income account' => '',
   'Tax zone (database ID)'      => '',
   'Tax zone (description)'      => '',
   'Tax-O-Matic'                 => '',
@@ -2057,45 +3440,86 @@ $self->{texts} = {
   'Taxkey_coa'                  => '',
   'Taxkeys and Taxreport Preferences' => '',
   'Taxlink_coa'                 => '',
-  'Taxnumber'                   => '',
   'Taxrate missing!'            => '',
+  'Taxzones'                    => '',
   'Tel'                         => '',
   'Tel.'                        => '',
   'Telephone'                   => '',
   'Template'                    => '',
   'Template Code'               => '',
-  'Template Code missing!'      => '',
+  'Template Description'        => '',
   'Template database'           => '',
+  'Template date'               => '',
   'Templates'                   => '',
   'Terms missing in row '       => '',
-  'Test and preview'            => '',
   'Test database connectivity'  => '',
+  'Text'                        => '',
+  'Text block actions'          => '',
+  'Text block picture actions'  => '',
+  'Text blocks'                 => '',
+  'Text blocks back'            => '',
+  'Text blocks front'           => '',
   'Text field'                  => '',
-  'Text field variables: \'WIDTH=w HEIGHT=h\' sets the width and height of the text field. They default to 30 and 5 respectively.' => '',
+  'Text field and HTML field variables: \'WIDTH=w HEIGHT=h\' sets the width and height of the field in pixels. They default to 225 and 90 respectively.' => '',
+  'Text in CSV File'            => '',
   'Text variables: \'MAXLENGTH=n\' sets the maximum entry length to \'n\'.' => '',
-  'Text, text field and number variables: The default value will be used as-is.' => '',
+  'Text, text field, HTML field and number variables: The default value will be used as-is.' => '',
+  'Texts for invoices'          => '',
+  'Texts for quotations & orders' => '',
   'That export does not exist.' => '',
   'That is why kivitendo could not find a default currency.' => '',
   'The \'name\' is the field shown to the user during login.' => '',
+  'The \'pclass\' column has the same abbreviation like a part export. The first letter is for the type Part,Assembly or Service, the second(and third) for Part Classification' => '',
   'The \'tag\' field must only consist of alphanumeric characters or the carachters - _ ( )' => '',
   'The AP transaction #1 has been deleted.' => '',
   'The AR transaction #1 has been deleted.' => '',
   'The Bins in Inventory were only a information text field.' => '',
   'The Bins in master data were only a information text field.' => '',
+  'The Factur-X/ZUGFeRD XML invoice was not found.' => '',
+  'The Factur-X/ZUGFeRD notes have been saved.' => '',
+  'The Factur-X/ZUGFeRD version used is not supported.' => '',
   'The GL transaction #1 has been deleted.' => '',
+  'The Geierlein path has not been set in the configuration.' => '',
+  'The Host Name is missing'    => '',
+  'The Host Name seems invalid' => '',
+  'The IBAN \'#1\' is not valid as IBANs in #2 must be exactly #3 characters long.' => '',
+  'The IBAN is missing.'        => '',
+  'The ID #1 is not a valid database ID.' => '',
   'The LDAP server "#1:#2" is unreachable. Please check config/kivitendo.conf.' => '',
+  'The Mail strings have been saved.' => '',
+  'The PDF has been created'    => '',
+  'The PDF has been previewed'  => '',
+  'The PDF has been printed'    => '',
+  'The Protocol for Host Name seems invalid (expected: http:// or https://)!' => '',
+  'The Proxy Name seems invalid' => '',
   'The SEPA export has been created.' => '',
   'The SEPA strings have been saved.' => '',
+  'The SQL query can be parameterized with variables named as follows: <%name%>.' => '',
+  'The SQL query does not contain any parameter that need to be configured.' => '',
+  'The URL is missing.'         => '',
+  'The VAT ID number \'#1\' is invalid.' => '',
+  'The VAT ID number in the client configuration is invalid.' => '',
+  'The VAT registration number is missing in the client configuration.' => '',
   'The WebDAV feature has been used.' => '',
+  'The XMP metadata does not declare the Factur-X/ZUGFeRD data.' => '',
+  'The ZUGFeRD invoice data cannot be generated because the data validation failed.' => '',
+  'The abbreviation is missing.' => '',
   'The access rights a user has within a client instance is still governed by his group membership.' => '',
   'The access rights have been saved.' => '',
+  'The account #1 is already being used by bank account #2.' => '',
   'The account 3804 already exists, the update will be skipped.' => '',
   'The account 3804 will not be added automatically.' => '',
+  'The action can only be executed once.' => '',
+  'The action is missing or invalid.' => '',
   'The action you\'ve chosen has not been executed because the document does not contain any item yet.' => '',
   'The administration area is always accessible.' => '',
   'The application "#1" was not found on the system.' => '',
+  'The assembly \'#1\' cannot be a part from itself.' => '',
+  'The assembly \'#1\' would make a loop in assembly tree.' => '',
+  'The assembly doesn\'t have any items.' => '',
   'The assembly has been created.' => '',
   'The assistant could not find anything wrong with #1. Maybe the problem has been solved in the meantime.' => '',
+  'The assortment doesn\'t have any items.' => '',
   'The authentication database is not reachable at the moment. Either it hasn\'t been set up yet or the database server might be down. Please contact your administrator.' => '',
   'The available options depend on the varibale type:' => '',
   'The background job could not be destroyed.' => '',
@@ -2104,43 +3528,63 @@ $self->{texts} = {
   'The background job has been saved.' => '',
   'The background job was executed successfully.' => '',
   'The bank information must not be empty.' => '',
+  'The base file name without a path or an extension to be used for printing for this type of requirement spec.' => '',
   'The base unit does not exist or it is about to be deleted in row %d.' => '',
   'The base unit does not exist.' => '',
   'The base unit relations must not contain loops (e.g. by saying that unit A\'s base unit is B, B\'s base unit is C and C\'s base unit is A) in row %d.' => '',
   'The basic client tables have not been created for this client\'s database yet.' => '',
-  'The business has been created.' => '',
-  'The business has been deleted.' => '',
-  'The business has been saved.' => '',
-  'The business is in use and cannot be deleted.' => '',
-  'The changing of tax-o-matic account is NOT recommended, but if you do so please also (re)configure buchungsgruppen and reconfigure ALL charts which point to this tax-o-matic account. ' => '',
+  'The billing period has already been locked.' => '',
+  'The body is missing.'        => '',
+  'The booking group has been created.' => '',
+  'The booking group has been deleted.' => '',
+  'The booking group has been saved.' => '',
+  'The booking group is in use and cannot be deleted.' => '',
+  'The booking group needs an inventory account.' => '',
+  'The buchungsgruppe is missing.' => '',
+  'The categories has been saved.' => '',
+  'The changing of tax-o-matic account is NOT recommended, but if you do so please also (re)configure booking groups and reconfigure ALL charts which point to this tax-o-matic account. ' => '',
+  'The chart is not valid.'     => '',
   'The client could not be deleted.' => '',
   'The client has been created.' => '',
   'The client has been deleted.' => '',
   'The client has been saved.'  => '',
+  'The clipboard does not contain anything that can be pasted here.' => '',
   'The column "datatype" must be present and must be at the same position / column in each data set. The values must be the row names (see settings) for order and item data respectively.' => '',
   'The column "make_X" can contain either a vendor\'s database ID, a vendor number or a vendor\'s name.' => '',
   'The column triplets can occur multiple times with different numbers "X" each time (e.g. "make_1", "model_1", "lastcost_1", "make_2", "model_2", "lastcost_2", "make_3", "model_3", "lastcost_3" etc).' => '',
   'The columns &quot;Dunning Duedate&quot;, &quot;Total Fees&quot; and &quot;Interest&quot; show data for the previous dunning created for this invoice.' => '',
   'The combination of database host, port and name is not unique.' => '',
   'The command is missing.'     => '',
+  'The company\'s address information is incomplete in the client configuration.' => '',
   'The connection to the LDAP server cannot be encrypted (SSL/TLS startup failure). Please check config/kivitendo.conf.' => '',
   'The connection to the authentication database failed:' => '',
   'The connection to the configured client database "#1" on host "#2:#3" failed.' => '',
   'The connection to the database could not be established.' => '',
+  'The connection to the shop could not be established.' => '',
+  'The connection to the shop was established successfully.' => '',
   'The connection to the template database failed:' => '',
   'The connection was established successfully.' => '',
   'The contact person attribute "birthday" is converted from a free-form text field into a date field.' => '',
+  'The country from the company\'s address in the client configuration cannot be mapped to an ISO 3166-1 alpha 2 code.' => '',
+  'The country from the customer\'s address cannot be mapped to an ISO 3166-1 alpha 2 code.' => '',
   'The creation of the authentication database failed:' => '',
+  'The credentials (username & password) for connecting database are wrong.' => '',
+  'The currency "#1" cannot be mapped to an ISO 4217 currency code.' => '',
+  'The custom data export has been deleted.' => '',
+  'The custom data export has been saved.' => '',
   'The custom variable has been created.' => '',
   'The custom variable has been deleted.' => '',
   'The custom variable has been saved.' => '',
   'The custom variable is in use and cannot be deleted.' => '',
   'The customer name is missing.' => '',
+  'The customer order number is missing. Do you want to continue anyway?' => '',
+  'The customer\'s bank account number (IBAN) is missing.' => '',
   'The database for user management and authentication does not exist. You can create let kivitendo create it with the following parameters:' => '',
   'The database host is missing.' => '',
   'The database name is missing.' => '',
   'The database port is missing.' => '',
   'The database update/creation did not succeed. The file #1 contained the following error:' => '',
+  'The database user \'#1\' does not have superuser privileges.' => '',
   'The database user is missing.' => '',
   'The dataset #1 has been created.' => '',
   'The dataset #1 has been deleted.' => '',
@@ -2151,10 +3595,6 @@ $self->{texts} = {
   'The delivery term has been deleted.' => '',
   'The delivery term has been saved.' => '',
   'The delivery term is in use and cannot be deleted.' => '',
-  'The department has been created.' => '',
-  'The department has been deleted.' => '',
-  'The department has been saved.' => '',
-  'The department is in use and cannot be deleted.' => '',
   'The description is missing.' => '',
   'The description is not unique.' => '',
   'The description is shown on the form. Chose something short and descriptive.' => '',
@@ -2162,19 +3602,37 @@ $self->{texts} = {
   'The discount in percent'     => '',
   'The discount must be less than 100%.' => '',
   'The discount must not be negative.' => '',
-  'The dunning process started' => '',
+  'The discounted amount will be shown in documents.' => '',
+  'The display of (mainly) picker results can be configured. To insert the value of one option use <%Name%>.' => '',
+  'The document has been changed by another user. No mail was sent. Please reopen it in another window and copy the changes to the new window' => '',
+  'The document has been changed by another user. Please reopen it in another window and copy the changes to the new window' => '',
+  'The document has been created.' => '',
+  'The document has been printed.' => '',
+  'The documents have been sent to the printer \'#1\'.' => '',
   'The dunnings have been printed.' => '',
+  'The email has been sent.'    => '',
+  'The email was not sent due to the following error: #1.' => '',
+  'The employee is missing.'    => '',
   'The end date is the last day for which invoices will possibly be created.' => '',
   'The execution schedule is invalid.' => '',
   'The execution type is invalid.' => '',
   'The existing record has been created from the link target to add.' => '',
+  'The export failed because of malformed transactions. Please fix those before exporting.' => '',
   'The factor is missing in row %d.' => '',
   'The factor is missing.'      => '',
+  'The file \'#1\' could not be opened for reading.' => '',
+  'The file \'#1\' does not contain the required XMP meta data.' => '',
+  'The file has been sent to the printer.' => '',
+  'The file is available for download.' => '',
+  'The file name is missing'    => '',
   'The first reason is that kivitendo contained a bug which resulted in the wrong taxkeys being recorded for transactions in which two entries are posted for the same chart with different taxkeys.' => '',
   'The follow-up date is missing.' => '',
   'The following currencies have been used, but they are not defined:' => '',
+  'The following delivery orders could not be processed because they are already closed: #1' => '',
   'The following drafts have been saved and can be loaded.' => '',
+  'The following errors occurred:' => '',
   'The following groups are valid for this client' => '',
+  'The following is only a preview.' => '',
   'The following list has been generated automatically from existing users collapsing users with identical settings into a single entry.' => '',
   'The following old files whose settings have to be merged manually into the new configuration file "config/kivitendo.conf" still exist:' => '',
   'The following transaction contains wrong taxes:' => '',
@@ -2186,6 +3644,14 @@ $self->{texts} = {
   'The greetings have been saved.' => '',
   'The installation is currently locked.' => '',
   'The installation is currently unlocked.' => '',
+  'The invoice is not linked with a sales delivery order. Post anyway?' => '',
+  'The invoice recipient can either be a selected contact person (default) or the email adress set in the master data of the customer. Additionally a contact persons mail and the company\'s invoicing mail can be combined.' => '',
+  'The invoices have been created. They\'re pre-selected below.' => '',
+  'The item couldn\'t be deleted!' => '',
+  'The item couldn\'t be saved!' => '',
+  'The item has been created.'  => '',
+  'The item has been deleted.'  => '',
+  'The item has been saved.'    => '',
   'The items are imported accoring do their number "X" regardless of the column order inside the file.' => '',
   'The link target to add has been created from the existing record.' => '',
   'The list has been printed.'  => '',
@@ -2193,32 +3659,58 @@ $self->{texts} = {
   'The login is not unique.'    => '',
   'The long description is missing.' => '',
   'The master templates where not found.' => '',
+  'The maximum of uploadable filesize in Megabyte' => '',
+  'The name and description are not unique.' => '',
   'The name in row %d has already been used before.' => '',
+  'The name is invalid.'        => '',
   'The name is missing in row %d.' => '',
   'The name is missing.'        => '',
   'The name is not unique.'     => '',
+  'The name must not be empty.' => '',
   'The name must only consist of letters, numbers and underscores and start with a letter.' => '',
+  'The new requirement spec template will be a copy of \'#1\'.' => '',
+  'The new requirement spec will be a copy of \'#1\' for customer \'#2\'.' => '',
   'The number of days for full payment' => '',
+  'The numbering will start at 1 with each requirement spec.' => '',
+  'The object has been created.' => '',
+  'The object has been deleted.' => '',
+  'The object has been saved.'  => '',
+  'The object has not been saved yet.' => '',
+  'The object is in use and cannot be deleted.' => '',
   'The option field is empty.'  => '',
+  'The order has been deleted'  => '',
+  'The order has been saved'    => '',
   'The package name is invalid.' => '',
+  'The partnumber already exists!' => '',
+  'The partnumber already exists.' => '',
+  'The partnumber is already being used' => '',
+  'The partnumber is missing.'  => '',
   'The parts for this delivery order have already been transferred in.' => '',
   'The parts for this delivery order have already been transferred out.' => '',
+  'The parts for this order have already been transferred' => '',
   'The parts have been removed.' => '',
-  'The parts have been stocked.' => '',
   'The parts have been transferred.' => '',
+  'The partsgroup is missing.'  => '',
   'The password is too long (maximum length: #1).' => '',
   'The password is too short (minimum length: #1).' => '',
   'The password is weak (e.g. it can be found in a dictionary).' => '',
+  'The path is missing.'        => '',
   'The payment term has been created.' => '',
   'The payment term has been deleted.' => '',
   'The payment term has been saved.' => '',
   'The payment term is in use and cannot be deleted.' => '',
   'The payments have been posted.' => '',
+  'The periodic invoices config has been assigned.' => '',
+  'The port is missing.'        => '',
   'The preferred one is to install packages provided by your operating system distribution (e.g. Debian or RPM packages).' => '',
   'The price rule for this discount does not exist anymore' => '',
   'The price rule for this price does not exist anymore' => '',
+  'The price rule has been created.' => '',
+  'The price rule has been deleted.' => '',
+  'The price rule has been saved.' => '',
   'The price rule is not a rule for discounts' => '',
   'The price rule is not a rule for prices' => '',
+  'The pricegroup is being used by customers.' => '',
   'The printer could not be deleted.' => '',
   'The printer has been created.' => '',
   'The printer has been deleted.' => '',
@@ -2229,8 +3721,26 @@ $self->{texts} = {
   'The project has been deleted.' => '',
   'The project has been saved.' => '',
   'The project is in use and cannot be deleted.' => '',
+  'The project link has been updated.' => '',
   'The project number is already in use.' => '',
   'The project number is missing.' => '',
+  'The query did not return any data.' => '',
+  'The quotation has been deleted' => '',
+  'The quotation has been saved' => '',
+  'The receivables chart isn\'t a valid chart.' => '',
+  'The recipient, subject or body is missing.' => '',
+  'The record template \'#1\' has been loaded.' => '',
+  'The record template \'#1\' has been saved.' => '',
+  'The report doesn\'t contain entries.' => '',
+  'The required information consists of the IBAN and the BIC.' => '',
+  'The required information consists of the IBAN, the BIC, the mandator ID and the mandate\'s date of signature.' => '',
+  'The requirement spec has been deleted.' => '',
+  'The requirement spec has been reverted to version #1.' => '',
+  'The requirement spec has been saved.' => '',
+  'The requirement spec is in use and cannot be deleted.' => '',
+  'The requirement spec template has been saved.' => '',
+  'The rfq has been deleted'    => '',
+  'The rfq has been saved'      => '',
   'The second reason is that kivitendo allowed the user to enter the tax amount manually regardless of the taxkey used.' => '',
   'The second way is to use Perl\'s CPAN module and let it download and install the module for you.' => '',
   'The selected bank account does not exist anymore.' => '',
@@ -2238,31 +3748,55 @@ $self->{texts} = {
   'The selected currency'       => '',
   'The selected database is still configured for client "#1". If you delete the database that client will stop working until you re-configure it. Do you still want to delete the database?' => '',
   'The selected exports have been closed.' => '',
+  'The selected exports have been undone.' => '',
   'The selected warehouse does not exist.' => '',
   'The selected warehouse is empty, or no stocked items where found that match the filter settings.' => '',
   'The session has expired. Please log in again.' => '',
   'The session is invalid or has expired.' => '',
   'The settings were saved, but the password was not changed.' => '',
+  'The shop has been created.'  => '',
+  'The shop has been deleted.'  => '',
+  'The shop has been saved.'    => '',
+  'The shop is in use and cannot be deleted.' => '',
+  'The shop part has been created.' => '',
+  'The shop part has been saved.' => '',
+  'The shop part wasn\'t updated.' => '',
+  'The shop part wasn\'t updated. #1' => '',
   'The source warehouse does not contain any bins.' => '',
   'The start date is missing.'  => '',
+  'The stock will be changed to your target quantity.' => '',
   'The subject is missing.'     => '',
   'The tables for user management and authentication do not exist. They will be created in the next step in the following database:' => '',
   'The tabulator character'     => '',
+  'The target quantity of #1 differs more than the threshold quantity of #2.' => '',
   'The task server does not appear to be running.' => '',
   'The task server is already running.' => '',
   'The task server is not running at the moment but needed for this module' => '',
   'The task server is not running.' => '',
+  'The task server is required for this module but not enabled for the current client. Please enable it for the client "#1" in the administration section.' => '',
   'The task server was started successfully.' => '',
   'The task server was stopped successfully.' => '',
+  'The tax zone has been deleted.' => '',
+  'The tax zone is in use and cannot be deleted.' => '',
+  'The taxzone has been created.' => '',
+  'The taxzone has been saved.' => '',
+  'The test import has not been executed yet.' => '',
+  'The third reason is that wrong (taxkey) settings for the credit / debit CSV-import were used.' => '',
   'The third way is to download the module from the above mentioned URL and to install the module manually following the installations instructions contained in the source archive.' => '',
   'The three columns "make_X", "model_X" and "lastcost_X" with the same number "X" are used to import vendor part numbers and vendor prices.' => '',
+  'The title is missing.'       => '',
   'The transaction is shown below in its current state.' => '',
+  'The transfer has been canceled by the user.' => '',
+  'The transport cost article \'#1\' is missing. Do you want to continue anyway?' => '',
   'The type is missing.'        => '',
-  'The unit has been saved.'    => '',
+  'The unit has been added.'    => '',
   'The unit in row %d has been deleted in the meantime.' => '',
   'The unit in row %d has been used in the meantime and cannot be changed anymore.' => '',
+  'The unit is missing.'        => '',
   'The units have been saved.'  => '',
+  'The uploaded filename still exists.<br>If you not modify the name this is a new version of the file' => '',
   'The user can chose which client to connect to during login.' => '',
+  'The user cannot be deleted as it is used in the following clients: #1' => '',
   'The user could not be deleted.' => '',
   'The user group could not be deleted.' => '',
   'The user group has been created.' => '',
@@ -2271,14 +3805,18 @@ $self->{texts} = {
   'The user has been created.'  => '',
   'The user has been deleted.'  => '',
   'The user has been saved.'    => '',
+  'The value \'#1\' is not a valid IBAN.' => '',
+  'The value \'our routing id at customer\' must be set in the customer\'s master data for profile #1.' => '',
   'The variable name must only consist of letters, numbers and underscores. It must begin with a letter. Example: send_christmas_present' => '',
   'The vendor name is missing.' => '',
+  'The version number is missing.' => '',
   'The warehouse could not be deleted because it has already been used.' => '',
   'The warehouse does not contain any bins.' => '',
   'The warehouse or the bin is missing.' => '',
   'The wrong taxkeys for AP and AR transactions have been fixed.' => '',
   'The wrong taxkeys for inventory transactions for sales and purchase invoices have been fixed.' => '',
   'The wrong taxkeys have been fixed.' => '',
+  'Then'                        => '',
   'Then go to the database administration and chose "create database".' => '',
   'There are #1 more open invoices for this customer with other currencies.' => '',
   'There are #1 more open invoices from this vendor with other currencies.' => '',
@@ -2286,74 +3824,171 @@ $self->{texts} = {
   'There are Bins defined in your Inventory.' => '',
   'There are Bins defined in your master data.' => '',
   'There are bookings to the account 3803 after 01.01.2007. If you didn\'t change this account manually to 19% the bookings are probably incorrect.' => '',
+  'There are currently no delivery orders, or none matches your filter conditions.' => '',
+  'There are currently no open invoices, or none matches your filter conditions.' => '',
+  'There are currently no open sales delivery orders.' => '',
   'There are double partnumbers in your database.' => '',
+  'There are duplicate assortment items' => '',
+  'There are duplicate parts at positions' => '',
   'There are entries in tax where taxkey is NULL.' => '',
   'There are invalid taxnumbers in use.' => '',
   'There are invalid transactions in your database.' => '',
+  'There are no documents in the WebDAV directory at the moment.' => '',
   'There are no entries in the background job history.' => '',
+  'There are no entries that match the filter.' => '',
   'There are no items in stock.' => '',
   'There are no items on your TODO list at the moment.' => '',
+  'There are no record templates yet.' => '',
   'There are several options you can handle this problem, please select one:' => '',
   'There are still transfers not matching the qty of the delivery order. Stock operations can not be changed later. Do you really want to proceed?' => '',
   'There are undefined currencies in your system.' => '',
   'There are usually three ways to install Perl modules.' => '',
+  'There is a better discount available' => '',
+  'There is a better price available' => '',
   'There is already a taxkey 0 with tax rate not 0.' => '',
   'There is an inconsistancy in your database.' => '',
   'There is at least one sales or purchase invoice for which kivitendo recorded an inventory transaction with taxkeys even though no tax was recorded.' => '',
   'There is at least one transaction for which the user has chosen a logically wrong taxkey.' => '',
+  'There is no connected chart.' => '',
   'There is not enough available of \'#1\' at warehouse \'#2\', bin \'#3\', #4, #5, for the transfer of #6.' => '',
   'There is not enough available of \'#1\' at warehouse \'#2\', bin \'#3\', #4, for the transfer of #5.' => '',
   'There is not enough left of \'#1\' in bin \'#2\' for the removal of #3.' => '',
+  'There is nothing here yet (csv_import)' => '',
+  'There is one or more sections for which no part has been assigned yet; therefore creating the new record is not possible yet.' => '',
+  'There was an error deleting the draft' => '',
   'There was an error executing the background job.' => '',
   'There was an error parsing the csv file: #1 in line #2.' => '',
+  'There was an error saving the draft' => '',
+  'There was an error saving the letter' => '',
+  'There was an error saving the letter draft' => '',
   'There you can let kivitendo create the basic tables for you, even in an already existing database.' => '',
   'Therefore several settings that had to be made for each user in the past have been consolidated into the client configuration.' => '',
   'Therefore the definition of "kg" with the base unit "g" and a factor of 1000 is valid while defining "g" with a base unit of "kg" and a factor of "0.001" is not.' => '',
+  'These mappings can be used to map heading from non standard csv files to known columns. These will also be saved in profiles, so you can save profiles for every source of formats.' => '',
   'These wrong entries cannot be fixed automatically.' => '',
+  'They will be updated, new ones for additional parts without a line item added automatically.' => '',
+  'This Price Rule is no longer valid' => '',
+  'This also enables displaying a column with the customer partnumber (new order controller).' => '',
+  'This also enables displaying a column with the vendor partnumber (model) (new order controller).' => '',
   'This can be done with the following query:' => '',
   'This could have happened for two reasons:' => '',
+  'This customer has already been added.' => '',
   'This customer number is already in use.' => '',
+  'This customer wants a postal invoices.' => '',
+  'This discount has since gone down' => '',
+  'This discount has since gone up' => '',
+  'This discount is only valid for business #1' => '',
+  'This discount is only valid for customer #1' => '',
+  'This discount is only valid for vendor #1' => '',
+  'This discount is only valid in purchase documents' => '',
+  'This discount is only valid in records with customer or vendor' => '',
+  'This discount is only valid in sales documents' => '',
+  'This entry is using date and duration. This information will be overwritten on saving.' => '',
+  'This entry is using start and end time. This information will be overwritten on saving.' => '',
+  'This export will include all records in the given time range and all supplicant information from checked entities. You will receive a single zip file. Please extract this file onto the data medium requested by your auditor.' => '',
   'This feature especially prevents mistakes by mixing up prior tax and sales tax.' => '',
+  'This field must not be empty.' => '',
+  'This function requires the presence of articles with a time-based unit such as "h" or "min".' => '',
+  'This general ledger transaction has not been posted yet.' => '',
   'This group is valid for the following clients' => '',
   'This has been changed in this version, therefore please change the "old" bins to some real warehouse bins.' => '',
   'This has been changed in this version.' => '',
+  'This invoice has a further invoice for advanced payment.' => '',
+  'This invoice has already a final invoice.' => '',
+  'This invoice has already a further invoice for advanced payment.' => '',
+  'This invoice has already been posted.' => '',
+  'This invoice has been canceled already.' => '',
+  'This invoice has been linked with a sepa export, undo this first.' => '',
+  'This invoice has not been posted yet.' => '',
+  'This invoice was added from an order. See there.' => '',
+  'This invoice\'s dunning level: #1' => '',
   'This is a very critical problem.' => '',
   'This is the client to be selected by default on the login screen.' => '',
-  'This is the default bin for ignoring onhand' => '',
   'This is the default bin for parts' => '',
+  'This is the default warehouse for ignoring onhand' => '',
   'This list is capped at 15 items to keep it fast. If you need a full list, please use reports.' => '',
+  'This makemodel price does not exist anymore' => '',
   'This means that the user has created an AP transaction and chosen a taxkey for sales taxes, or that he has created an AR transaction and chosen a taxkey for input taxes.' => '',
   'This module can help you identify and correct such entries by analyzing the general ledger and presenting you likely solutions but also allowing you to fix problems yourself.' => '',
+  'This object has already been used.' => '',
+  'This object has not been saved yet.' => '',
+  'This object is used in price rules.' => '',
   'This option controls the inventory system.' => '',
+  'This option controls the method used for determining the startdate for the balance report.' => '',
   'This option controls the method used for profit determination.' => '',
   'This option controls the posting and calculation behavior for the accounting method.' => '',
-  'This partnumber is not unique. You should change it.' => '',
+  'This order has already a final invoice.' => '',
+  'This part has already been added.' => '',
+  'This part was already counted for this bin:' => '',
+  'This price has since gone down' => '',
+  'This price has since gone up' => '',
+  'This record containts obsolete items at position #1' => '',
+  'This record has already been closed.' => '',
+  'This record has already been delivered.' => '',
+  'This record has not been saved yet.' => '',
+  'This record has not been stocked in. Proceed?' => '',
+  'This record has not been stocked out. Proceed?' => '',
+  'This requirement spec is currently linked to the following project:' => '',
+  'This requirement spec is currently not linked to a project.' => '',
   'This requires you to manually correct entries for which an automatic conversion failed and to check those for which it succeeded.' => '',
+  'This resets the dunning process for the selected invoices. Posted dunning invoices will not be changed!' => '',
+  'This sales order has an active configuration for periodic invoices. If you save then all subsequently created invoices will contain those changes as well, but not those that have already been created. Do you want to continue?' => '',
+  'This status output will be refreshed every five seconds.' => '',
   'This transaction has to be split into several transactions manually.' => '',
+  'This transaction is linked with a AP transaction. Please undo and redo the AP transaction booking if needed.' => '',
+  'This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.' => '',
+  'This transaction is linked with a gl transaction. Please delete the ap transaction booking if needed.' => '',
+  'This transaction is reconciled with a bank transaction. Please undo the reconciliation if needed.' => '',
   'This update will change the nature the onhand of goods is tracked.' => '',
   'This user is a member in the following groups' => '',
   'This user will have access to the following clients' => '',
+  'This vendor has already a booking with this invoice number, do you really want to add the same invoice number again?' => '',
+  'This vendor has already been added.' => '',
   'This vendor number is already in use.' => '',
+  'This will also remove this pricegroup for all customers.' => '',
+  'This will apply a 3% reduction to the master data price before entering it into the record item.' => '',
+  'This will be treated as a discount in percent points.' => '',
+  'This will happen before the price is offered, and the reduction will not be printed in documents.' => '',
+  'This will reduce the appropriate Master Data price by this in percent points.' => '',
+  'This will remove the delivery order from showing as open even if contents are not delivered. Proceed?' => '',
+  'This will remove the invoice from showing as unpaid even if the unpaid amount does not match the amount. Proceed?' => '',
+  'This will set an exact price.' => '',
   'Three Options:'              => '',
+  'Threshold for warning on quantity difference' => '',
+  'Thu'                         => '',
+  'Thursday'                    => '',
+  'Time'                        => '',
   'Time Format'                 => '',
-  'Time Tracking'               => '',
+  'Time Recording'              => '',
+  'Time Recording Articles'     => '',
+  'Time Recordings'             => '',
+  'Time and price estimate'     => '',
+  'Time estimate'               => '',
   'Time period for the analysis:' => '',
+  'Time/cost estimate actions'  => '',
   'Timestamp'                   => '',
+  'Tired of copying always nice phrases for this message? Click here to use the new preset message option!' => '',
   'Title'                       => '',
   'To'                          => '',
   'To (email)'                  => '',
   'To (time)'                   => '',
   'To Date'                     => '',
   'To continue please change the taxkey 0 to another value.' => '',
+  'To import'                   => '',
+  'To upload images: Please create shoppart first' => '',
   'To user login'               => '',
+  'Today'                       => '',
+  'Toggle marker'               => '',
+  'Too many results (#1 from #2).' => '',
+  'Too much recursions in assembly tree (>100)' => '',
   'Top'                         => '',
   'Top (CSS)'                   => '',
   'Top (Javascript)'            => '',
-  'Top 100'                     => '',
-  'Top 100 hinzufuegen'         => '',
   'Top Level Designation only'  => '',
   'Total'                       => '',
   'Total Fees'                  => '',
+  'Total Sales Orders Value'    => '',
+  'Total number of entries'     => '',
   'Total stock value'           => '',
   'Total sum'                   => '',
   'Total weight'                => '',
@@ -2364,19 +3999,33 @@ $self->{texts} = {
   'Transaction'                 => '',
   'Transaction %d cancelled.'   => '',
   'Transaction Date missing!'   => '',
+  'Transaction Description is not yet implemented' => '',
   'Transaction ID missing.'     => '',
+  'Transaction Value'           => '',
+  'Transaction Value Currency Code' => '',
+  'Transaction date'            => '',
   'Transaction deleted!'        => '',
   'Transaction description'     => '',
   'Transaction has already been cancelled!' => '',
   'Transaction has been split on both the credit and the debit side' => '',
-  'Transaction posted!'         => '',
+  'Transactions'                => '',
   'Transactions without account:' => '',
   'Transactions without reference:' => '',
   'Transactions, AR transactions, AP transactions' => '',
-  'Transdate'                   => '',
+  'Transdate'                   => 'Booking Date',
+  'Transdate Record'            => 'Booking Date Record',
+  'Transdate from'              => '',
+  'Transdate is #1'             => 'Record date is #1',
+  'Transdate is after #1'       => 'Record date is after #1',
+  'Transdate is before #1'      => 'Record date is before #1',
+  'Transdate to'                => '',
   'Transfer'                    => '',
+  'Transfer Date'               => '',
   'Transfer Quantity'           => '',
   'Transfer To Stock'           => '',
+  'Transfer all marked'         => '',
+  'Transfer data to Geierlein ELSTER application' => '',
+  'Transfer date exceeds the maximum allowed interval.' => '',
   'Transfer from warehouse'     => '',
   'Transfer in'                 => '',
   'Transfer in via default'     => '',
@@ -2385,34 +4034,56 @@ $self->{texts} = {
   'Transfer out on posting sales invoices?' => '',
   'Transfer out via default'    => '',
   'Transfer qty'                => '',
+  'Transfer services via default' => '',
+  'Transfer stock'              => '',
   'Transfer successful'         => '',
+  'Transfer undone.'            => '',
+  'Transferred'                 => '',
   'Translation'                 => '',
+  'Translations'                => '',
+  'Transport and service costs reminder' => '',
   'Trial Balance'               => '',
   'Trial balance between %s and %s' => '',
   'Trying to call a sub without a name' => '',
+  'Tue'                         => '',
+  'Tuesday'                     => '',
+  'Turnover'                    => '',
+  'Turnoverstatistic'           => '',
+  'TypAbbreviation'             => '',
   'Type'                        => '',
+  'Type abbreviation'           => '',
   'Type can be either \'part\', \'service\' or \'assembly\'.' => '',
   'Type of Business'            => '',
   'Type of Customer'            => '',
   'Type of Vendor'              => '',
+  'TypeAbbreviation'            => '',
   'Types of Business'           => '',
+  'UNDO TRANSFER'               => '',
+  'UNIMPORT'                    => '',
   'USTVA'                       => '',
   'USTVA 2004'                  => '',
   'USTVA 2005'                  => '',
   'USTVA 2006'                  => '',
   'USTVA 2007'                  => '',
+  'USTVA Data sent to geierlein' => '',
   'USTVA-Hint: Method'          => '',
   'USTVA-Hint: Tax Authoritys'  => '',
   'USt-IdNr.'                   => '',
   'USt-Konto'                   => '',
   'UStVA'                       => '',
-  'UStVA (PDF-Dokument)'        => '',
   'UStVa'                       => '',
   'UStVa Einstellungen'         => '',
+  'Unable to book transactions for bank purpose #1' => '',
+  'Unable to reconcile, database transaction failure' => '',
   'Unbalanced Ledger'           => '',
   'Unchecked custom variables will not appear in orders and invoices.' => '',
+  'Undo SEPA exports'           => '',
+  'Undo Transfer'               => '',
+  'Undo Transfer Interval'      => '',
   'Unfinished follow-ups'       => '',
   'Unfortunately you have no warehouse defined.' => '',
+  'Unimport all'                => '',
+  'Unimport documents'          => '',
   'Unit'                        => '',
   'Unit (if missing or empty default unit will be used)' => '',
   'Unit missing.'               => '',
@@ -2421,28 +4092,92 @@ $self->{texts} = {
   'Units that have already been used (e.g. for parts and services or in invoices or warehouse transactions) cannot be changed.' => '',
   'Unknown Category'            => '',
   'Unknown Link'                => '',
+  'Unknown authenticantion module #1 specified in "config/kivitendo.conf".' => '',
+  'Unknown control fields: #1'  => '',
   'Unknown dependency \'%s\'.'  => '',
+  'Unknown module: #1'          => '',
   'Unknown problem type.'       => '',
+  'Unlink bank transactions'    => '',
   'Unlock System'               => '',
+  'Unsuccessfully executed:\n'  => '',
+  'Unsupported image type (supported types: #1)' => '',
   'Until'                       => '',
   'Update'                      => '',
+  'Update Discount'             => '',
+  'Update Price'                => '',
   'Update Prices'               => '',
   'Update SKR04: new tax account 3804 (19%)' => '',
+  'Update customer using billing address' => '',
+  'Update from master data'     => '',
   'Update prices'               => '',
   'Update prices of existing entries' => '',
+  'Update prices of existing entries / skip non-existent' => '',
   'Update properties of existing entries' => '',
+  'Update properties of existing entries / skip non-existent' => '',
+  'Update quotation/order'      => '',
+  'Update sales order #1'       => '',
+  'Update sales quotation #1'   => '',
+  'Update this draft.'          => '',
+  'Update with section'         => '',
   'Updated'                     => '',
+  'Updated categories'          => '',
+  'Updated part [#1] in shop [#2] at #3' => '',
+  'Updated shop part'           => '',
+  'Updating data of existing entry in database' => '',
   'Updating existing entry in database' => '',
+  'Updating items with additional parts' => '',
+  'Updating items with sections' => '',
   'Updating prices of existing entry in database' => '',
   'Updating the client fields in the database "#1" on host "#2:#3" failed.' => '',
+  'Upload'                      => '',
+  'Upload Attachments'          => '',
+  'Upload Documents'            => '',
+  'Upload Images'               => '',
+  'Upload Status'               => '',
+  'Upload all marked'           => '',
+  'Upload file'                 => '',
+  'Uploaded at'                 => '',
   'Uploaded on #1, size #2 kB'  => '',
+  'Uploading Data'              => '',
+  'UsageE'                      => 'Report about stock withdrawal',
+  'UsageWithout'                => 'Usage (without correction)',
   'Use As New'                  => '',
+  'Use Balance Sheet'           => '',
+  'Use Datevautomatik'          => '',
+  'Use Erfolgsrechnung'         => '',
+  'Use File Storage backend'    => '',
+  'Use Filemanagement'          => '',
+  'Use Income'                  => 'Use GUV and BWA',
+  'Use Long Description from Parts for Shop Long Description' => '',
+  'Use Long Description from Parts is only for Shopware6 implemented' => '',
+  'Use UStVA'                   => '',
   'Use WebDAV Repository'       => '',
+  'Use WebDAV Storage backend'  => '',
+  'Use a text field to enter (new) contact departments if enabled. Otherwise, only a drop down box is offered.' => '',
+  'Use a text field to enter (new) contact titles if enabled. Otherwise, only a drop down box is offered.' => '',
+  'Use a text field to enter (new) greetings if enabled. Otherwise, only a drop down box is offered.' => '',
+  'Use as new'                  => '',
+  'Use date and duration for time recordings' => '',
+  'Use default booking group because setting is \'all\'' => '',
+  'Use default booking group because wanted is missing' => '',
   'Use existing templates'      => '',
+  'Use for Factur-X/ZUGFeRD'    => '',
+  'Use for Swiss QR-Bill'       => '',
   'Use master default bin for Default Transfer, if no default bin for the part is configured' => '',
+  'Use settings from client configuration' => '',
+  'Use text field for department of contacts' => '',
+  'Use text field for greetings' => '',
+  'Use text field for title of contacts' => '',
+  'Use this storage backend for all generated PDF-Files' => '',
+  'Use this storage backend for all uploaded attachments' => '',
+  'Use this storage backend for uploaded images' => '',
+  'Useable for sections'        => '',
+  'Useable for text blocks'     => '',
+  'Useable for…'                => '',
+  'Used for Purchase'           => '',
+  'Used for Sale'               => '',
+  'Used for assembly #1 #2'     => '',
   'User'                        => '',
-  'User Config'                 => '',
-  'User Groups'                 => '',
   'User Preferences'            => '',
   'User access'                 => '',
   'User list'                   => '',
@@ -2454,82 +4189,140 @@ $self->{texts} = {
   'Users with access'           => '',
   'Users with access to this client' => '',
   'Users, Clients and User Groups' => '',
+  'Usually the delivery date of an order is the next working day. If a value is set here this value will be added to the delivery date of the sales order. The resulting date will be adjusted to the next working day if it ends up on a weekend.' => '',
+  'Usually the sales quotation is valid until the next working day. If a value is set here then the quotation will be valid for at least that many days. The resulting date will be adjusted to the next working day if it ends up on a weekend.' => '',
   'VAT ID'                      => '',
+  'VAT ID and/or taxnumber must be given.' => '',
+  'VN'                          => '',
   'Valid'                       => '',
+  'Valid are integer values and floating point numbers, e.g. 4.75h = 4 hours and 45 minutes.' => '',
   'Valid from'                  => '',
   'Valid until'                 => '',
   'Valid/Obsolete'              => '',
   'Value'                       => '',
+  'Valutadate'                  => '',
+  'Valutadate from'             => '',
+  'Valutadate to'               => '',
   'Variable'                    => '',
   'Variable Description'        => '',
   'Variable Name'               => '',
   'Vendor'                      => '',
   'Vendor (database ID)'        => '',
   'Vendor (name)'               => '',
+  'Vendor Discount'             => '',
+  'Vendor GLN'                  => '',
   'Vendor Invoice'              => '',
   'Vendor Invoices & AP Transactions' => '',
+  'Vendor Master Data'          => '',
   'Vendor Name'                 => '',
   'Vendor Number'               => '',
   'Vendor Order Number'         => '',
   'Vendor deleted!'             => '',
   'Vendor details'              => '',
   'Vendor missing!'             => '',
-  'Vendor not on file or locked!' => '',
-  'Vendor not on file!'         => '',
   'Vendor saved'                => '',
   'Vendor saved!'               => '',
   'Vendor type'                 => '',
   'Vendors'                     => '',
+  'Vendors: VAT ID / taxnumber unique' => '',
   'Verrechnungseinheit'         => '',
   'Version'                     => '',
+  'Version actions'             => '',
+  'Version number'              => '',
+  'Versions'                    => '',
+  'View RFQs'                   => '',
   'View SEPA export'            => '',
   'View background job execution result' => '',
-  'View background job history' => '',
-  'View background jobs'        => '',
+  'View purchase delivery orders' => '',
+  'View purchase invoices'      => '',
+  'View purchase orders'        => '',
+  'View record links from Sales Order' => '',
+  'View sales delivery orders'  => '',
+  'View sales invoices and credit notes' => '',
+  'View sales orders'           => '',
+  'View sales quotations'       => '',
+  'View sent email'             => '',
   'View warehouse content'      => '',
+  'View/edit all employees purchase documents' => '',
   'View/edit all employees sales documents' => '',
-  'Von Konto: '                 => '',
-  'WHJournal'                   => '',
+  'WHJournal'                   => 'Warehouse journal',
+  'WHUsage'                     => 'Warehouse withdrawal',
   'Warehouse'                   => '',
   'Warehouse (database ID)'     => '',
+  'Warehouse (name)'            => '',
   'Warehouse From'              => '',
   'Warehouse Migration'         => '',
   'Warehouse To'                => '',
   'Warehouse content'           => '',
-  'Warehouse correction'        => '',
   'Warehouse deleted.'          => '',
-  'Warehouse list'              => '',
   'Warehouse management'        => '',
   'Warehouse saved.'            => '',
   'Warehouses'                  => '',
+  'Warn before saving orders with duplicate parts (new controller only)' => '',
+  'Warn before saving orders without a delivery date' => '',
+  'Warn before saving sales orders with missing customer order number (new controller only)' => '',
   'Warning'                     => '',
+  'Warning! Loading a draft will discard unsaved data!' => '',
+  'Warning: Faulty position ignored' => '',
+  'Warning: One or more field value are not in valid DATEV format at:' => '',
+  'Warnings and errors'         => '',
+  'Watch status'                => '',
+  'We need a array of datev_lines' => '',
+  'We need a valid from date'   => '',
+  'We need a valid to date'     => '',
+  'Web shops'                   => '',
   'WebDAV'                      => '',
   'WebDAV link'                 => '',
   'WebDAV save documents'       => '',
   'Webserver interface'         => '',
+  'Webshop'                     => '',
+  'Webshop Import'              => '',
+  'Webshop articles'            => '',
+  'Webshops articles'           => '',
+  'Wed'                         => '',
+  'Wednesday'                   => '',
   'Weight'                      => '',
   'Weight unit'                 => '',
   'What <b>term</b> you are looking for?' => '',
+  'What this template contains' => '',
   'What type of item is this?'  => '',
+  'When converting a requirement spec into a quotation or an oder each section gets converted into a line position in the new record. This is the article used by default for this conversion.' => '',
+  'Whether or not to replace variable placeholders such as "<%invdate%>" in texts in positions such as the part description by the record\'s actual value' => '',
   'Which is located at doc/kivitendo-Dokumentation.pdf. Click here: ' => '',
+  'With Attachments'            => '',
   'With Extension Of Time'      => '',
   'With the introduction of clients each client gets its own WebDAV folder.' => '',
-  'Workflow Delivery Order'     => '',
+  'Without Attachments'         => '',
+  'Workflow'                    => '',
   'Workflow purchase_order'     => '',
   'Workflow request_quotation'  => '',
   'Workflow sales_order'        => '',
   'Workflow sales_quotation'    => '',
+  'Working copy identical to version number #1' => '',
+  'Working copy without version' => '',
+  'Working copy; no description yet' => '',
+  'Working on export'           => '',
   'Write bin to default bin in part?' => '',
-  'Wrong Period'                => '',
+  'Wrong date format (#1)'      => '',
+  'Wrong field value \'#1\' for field \'#2\' for the transaction with amount \'#3\'' => '',
+  'Wrong file name, expects name like: DTVF_*_LOHNBUCHUNG*.csv' => '',
+  'Wrong number format (#1)'    => '',
   'Wrong tax keys recorded'     => '',
   'Wrong taxes recorded'        => '',
+  'Wrong time format (#1)'      => '',
+  'X'                           => '',
   'YYYY'                        => '',
   'Year'                        => '',
+  'Year-end bookings were successfully completed!' => '',
+  'Year-end closing'            => '',
+  'Year-end date'               => '',
+  'Year-end date missing'       => '',
   'Yearly'                      => '',
   'Yearly taxreport not yet implemented' => '',
   'Yes'                         => '',
   'Yes, included by default'    => '',
   'Yes/No (Checkbox)'           => '',
+  'You are adding a new part while you are editing another document. You will be redirected to your document when saving the new part or aborting this form.' => '',
   'You are logged out!'         => '',
   'You can also delete this transaction and re-enter it manually.' => '',
   'You can choose account categories for taxes. Depending on these categories taxes will be displayed for transfers in the general ledger or not.' => '',
@@ -2545,7 +4338,15 @@ $self->{texts} = {
   'You cannot continue before all required modules are installed.' => '',
   'You cannot create an invoice for delivery orders for different customers.' => '',
   'You cannot create an invoice for delivery orders from different vendors.' => '',
+  'You cannot modify individual assigments from additional articles to line items.' => '',
+  'You cannot paste function blocks or sub function blocks if there is no section.' => '',
+  'You cannot use a negative amount with debit/credit!' => '',
+  'You do not have access to any custom data export.' => '',
+  'You do not have permission to access this entry.' => '',
   'You do not have the permissions to access this function.' => '',
+  'You don\'t have the rights to edit this customer.' => '',
+  'You don\'t have the rights to edit this vendor.' => '',
+  'You have changed the currency or exchange rate. Please check prices.' => '',
   'You have entered or selected the following shipping address for this customer:' => '',
   'You have never worked with currencies.' => '',
   'You have not added bank accounts yet.' => '',
@@ -2556,61 +4357,75 @@ $self->{texts} = {
   'You have to define a unit as a multiple of a smaller unit.' => '',
   'You have to enter a company name in the client configuration.' => '',
   'You have to enter the SEPA creditor ID in the client configuration.' => '',
-  'You have to fill in at least an account number, the bank code, the IBAN and the BIC.' => '',
   'You have to grant users access to one or more clients.' => '',
   'You have to specify a department.' => '',
   'You have to specify an execution date for each antry.' => '',
+  'You have to upload an MT940 file to import.' => '',
   'You must chose a user.'      => '',
   'You must enter a name for your new print templates.' => '',
+  'You must not change this AP transaction.' => '',
+  'You must not change this AR transaction.' => '',
+  'You must not change this invoice.' => '',
+  'You must not print this invoice.' => '',
   'You must select existing print templates or create a new set.' => '',
   'You should create a backup of the database before proceeding because the backup might not be reversible.' => '',
   'You\'re not editing a file.' => '',
   'You\'ve already chosen the following limitations:' => '',
+  'Your Order'                  => '',
   'Your PostgreSQL installationen does not use Unicode as its encoding. This is not supported anymore.' => '',
+  'Your Reference'              => '',
   'Your TODO list'              => '',
-  'Your account number'         => '',
-  'Your bank'                   => '',
-  'Your bank code'              => '',
   'Your browser does not currently support Javascript.' => '',
   'Your download does not exist anymore. Please re-run the DATEV export assistant.' => '',
   'Your import is being processed.' => '',
-  'ZM'                          => '',
-  'Zeitpunkt'                   => '',
+  'Your target quantity will be added to the stocked quantity.' => '',
   'Zeitraum'                    => '',
   'Zero amount posting!'        => '',
+  'Zip'                         => '',
   'Zip, City'                   => '',
   'Zipcode'                     => '',
-  'Zusatz'                      => '',
+  'Zipcode and city'            => '',
   '[email]'                     => '',
   'absolute'                    => '',
   'account_description'         => '',
   'accrual'                     => '',
   'action= not defined!'        => '',
   'active'                      => '',
+  'all'                         => '',
   'all entries'                 => '',
   'and'                         => '',
   'ap_aging_list'               => '',
   'ar_aging_list'               => '',
+  'ar_chart isn\'t a valid chart' => '',
+  'article_list'                => '',
   'as at'                       => '',
+  'assembled'                   => '',
   'assembly'                    => '',
   'assembly_list'               => '',
   'averaged values, in invoice mode only useful when filtered by a part' => '',
+  'averconsumed_br'             => 'Ø monthly',
   'back'                        => '',
+  'back_br'                     => 'back',
+  'backend "#1" not enabled'    => '',
+  'backend "#1" not found'      => '',
   'balance'                     => '',
   'bank_collection_payment_list_#1' => '',
   'bank_transfer_payment_list_#1' => '',
-  'bankaccounts'                => '',
   'banktransfers'               => '',
+  'basis for stock value'       => '',
   'bestbefore #1'               => '',
   'bin_list'                    => '',
   'bis'                         => '',
+  'brutto'                      => '',
   'building data'               => '',
   'building report'             => '',
+  'can not allocate #1 units of #2, missing #3 units' => '',
+  'can not allocate enough resources for production' => '',
+  'can only parse a pdf file'   => '',
   'cash'                        => '',
   'chargenumber #1'             => '',
   'chart_of_accounts'           => '',
-  'choice'                      => '',
-  'choice part'                 => '',
+  'cleared'                     => '',
   'click here to edit cvars'    => '',
   'close'                       => '',
   'closed'                      => '',
@@ -2619,50 +4434,83 @@ $self->{texts} = {
   'config/kivitendo.conf: Key "authentication/ldap" is missing.' => '',
   'config/kivitendo.conf: Missing parameters in "authentication/database". Required parameters are "host", "db" and "user".' => '',
   'config/kivitendo.conf: Missing parameters in "authentication/ldap". Required parameters are "host", "attribute" and "base_dn".' => '',
+  'consumed'                    => '',
   'contact_list'                => '',
-  'continue'                    => '',
   'correction'                  => '',
+  'correction_br'               => 'correction',
   'cp_greeting to cp_gender migration' => '',
-  'customer'                    => '',
   'customer_list'               => '',
-  'debug'                       => '',
+  'dated'                       => '',
   'delete'                      => '',
+  'delete item'                 => '',
+  'delete order'                => '',
+  'deleted'                     => '',
   'delivered'                   => '',
   'deliverydate'                => '',
   'difference as skonto'        => '',
-  'difference_as_skonto'        => 'remainder as skonto',
   'direct debit'                => '',
   'disposed'                    => '',
+  'disposed_br'                 => 'disposed',
   'do not include'              => '',
   'done'                        => '',
-  'down'                        => '',
   'dunning_list'                => '',
-  'eBayImporter'                => '',
   'eMail Send?'                 => '',
   'eMail?'                      => '',
   'ea'                          => '',
   'emailed to'                  => '',
   'empty'                       => '',
+  'entries imported'            => '',
+  'error while disassembling for trans_ids #1 : #2' => '',
+  'error while paying invoice #1 : ' => '',
+  'error while unlinking payment #1 : ' => '',
   'every third month'           => '',
   'every time'                  => '',
+  'exchange rate already exists, no update allowed' => '',
+  'exchange rate has to be positive' => '',
   'executed'                    => '',
+  'execution as user \'#1\''    => '',
   'failed'                      => '',
+  'false'                       => '',
   'female'                      => '',
+  'file \'#1\' has unsupported image type \'#2\' (supported types: #3)' => '',
+  'filename'                    => '',
+  'filename has not uploadable characters ' => '',
+  'filesize too big: '          => '',
+  'final_invoice'               => '',
+  'flat-rate position'          => '',
   'follow_up_list'              => '',
   'for'                         => '',
+  'for Document types'          => '',
   'for Period'                  => '',
+  'for all'                     => '',
   'for date'                    => '',
   'found'                       => '',
+  'found_br'                    => 'found',
+  'free skonto'                 => '',
+  'from'                        => '',
+  'from \'#1\' imported Files'  => '',
   'from (time)'                 => '',
   'general_ledger_list'         => '',
+  'generated Files'             => '',
+  'gobd-#1-#2.zip'              => '',
   'h'                           => '',
-  'history'                     => '',
   'history search engine'       => '',
+  'http'                        => '',
+  'https'                       => '',
+  'imported'                    => '',
   'inactive'                    => '',
-  'income'                      => '',
+  'income'                      => 'GUV and BWA',
+  'internal error (see details)' => '',
   'invoice'                     => '',
   'invoice mode or item mode'   => '',
+  'invoice_for_advance_payment' => '',
   'invoice_list'                => '',
+  'is'                          => '',
+  'is after'                    => '',
+  'is before'                   => '',
+  'is equal to'                 => '',
+  'is greater than or equal'    => '',
+  'is lower than or equal'      => '',
   'kivitendo'                   => '',
   'kivitendo Homepage'          => '',
   'kivitendo can fix these problems automatically.' => '',
@@ -2670,42 +4518,57 @@ $self->{texts} = {
   'kivitendo has found one or more problems in the general ledger.' => '',
   'kivitendo is about to update the database [ #1 ].' => '',
   'kivitendo is now able to manage warehouses instead of just tracking the amount of goods in your system.' => '',
+  'kivitendo modules'           => '',
   'kivitendo needs to update the authentication database before you can proceed.' => '',
   'kivitendo v#1'               => '',
   'kivitendo v#1 administration' => '',
   'kivitendo website (external)' => '',
   'kivitendo will then update the database automatically.' => '',
-  'lead deleted!'               => '',
-  'lead saved!'                 => '',
-  'list'                        => '',
+  'letters_list'                => '',
   'list_of_payments'            => '',
   'list_of_receipts'            => '',
   'list_of_transactions'        => '',
-  'logout'                      => '',
   'male'                        => '',
-  'mark as paid'                => '',
+  'max filesize'                => '',
+  'min'                         => '',
   'missing'                     => '',
+  'missing file for action import' => '',
+  'missing_br'                  => 'missing',
   'month'                       => '',
   'monthly'                     => '',
+  'more'                        => '',
+  'natural person'              => '',
+  'netto'                       => '',
   'never'                       => '',
-  'new Window'                  => '',
+  'new order controller'        => '',
   'next'                        => '',
   'no'                          => '',
+  'no article assigned yet'     => '',
+  'no backend enabled'          => '',
   'no bestbefore'               => '',
   'no chargenumber'             => '',
-  'none (pricegroup)'           => '',
+  'no execution for this client' => '',
+  'no shipping address'         => '',
+  'no skonto_chart configured for taxkey #1 : #2 : #3' => '',
+  'no tax_id in acc_trans'      => '',
+  'not a valid DTVF file, expected field header start with \'Umsatz; (..) ;Konto;Gegenkonto\'' => '',
+  'not a valid DTVF file, expected first field in A1 \'DTVF\'' => '',
   'not configured'              => '',
   'not delivered'               => '',
   'not executed'                => '',
   'not running'                 => '',
   'not set'                     => '',
   'not shipped'                 => '',
+  'not transferred'             => '',
   'not transferred in yet'      => '',
   'not transferred out yet'     => '',
   'not yet executed'            => '',
+  'now'                         => '',
   'number'                      => '',
   'oe.pl::search called with unknown type' => '',
+  'old'                         => '',
   'on the same day'             => '',
+  'one time'                    => '',
   'one-time execution'          => '',
   'only OB Transactions'        => '',
   'open'                        => '',
@@ -2713,6 +4576,7 @@ $self->{texts} = {
   'our vendor number at customer' => '',
   'parsing csv'                 => '',
   'part'                        => '',
+  'part \'#\'1 in bin \'#2\' only with qty #3 (need additional #4) and chargenumber \'#5\'.' => '',
   'part_list'                   => '',
   'percental'                   => '',
   'periodic'                    => '',
@@ -2726,8 +4590,8 @@ $self->{texts} = {
   'position'                    => '',
   'posted!'                     => '',
   'prev'                        => '',
-  'print'                       => '',
   'proforma'                    => '',
+  'prospective'                 => '',
   'purchase_delivery_order_list' => '',
   'purchase_order'              => '',
   'purchase_order_list'         => '',
@@ -2735,33 +4599,46 @@ $self->{texts} = {
   'quarter'                     => '',
   'quotation_list'              => '',
   'release_material'            => '',
+  'renew'                       => '',
   'reorder item'                => '',
   'repeated execution'          => '',
   'report_generator_dispatch_to is not defined.' => '',
   'report_generator_nextsub is not defined.' => '',
   'request_quotation'           => '',
-  'reset'                       => '',
   'return_material'             => '',
+  'revert deleted'              => '',
   'rfq_list'                    => '',
+  'rma_delivery_order_list'     => '',
   'running'                     => '',
   'sales tax identification number' => '',
   'sales_delivery_order_list'   => '',
+  'sales_delivery_order_printer' => '',
+  'sales_invoice_printer'       => '',
   'sales_order'                 => '',
   'sales_order_list'            => '',
   'sales_quotation'             => '',
+  'same as periodicity'         => '',
   'saved'                       => '',
   'saved!'                      => '',
   'saving data'                 => '',
+  'searched part not for purchase' => '',
+  'searched part not for sale'  => '',
+  'semiannually'                => '',
   'sent'                        => '',
   'sent to printer'             => '',
   'service'                     => '',
   'service_list'                => '',
   'shipped'                     => '',
+  'shipped_br'                  => 'shipped',
   'singular first char'         => '',
-  'soldtotal'                   => '',
+  'sort items'                  => '',
+  'start upload'                => '',
   'stock'                       => '',
-  'submit'                      => '',
+  'stock_br'                    => 'stock',
+  'stocktaking'                 => '',
   'succeeded'                   => '',
+  'sum'                         => '',
+  'supplier_delivery_order_list' => '',
   'tax_chartaccno'              => '',
   'tax_percent'                 => '',
   'tax_rate'                    => '',
@@ -2772,31 +4649,49 @@ $self->{texts} = {
   'taxkey 0 with taxrate 0 was created.' => '',
   'taxnumber'                   => '',
   'terminated'                  => '',
+  'time and effort based position' => '',
+  'time_recordings'             => '',
+  'to'                          => '',
   'to (date)'                   => '',
+  'to (set to)'                 => '',
   'to (time)'                   => '',
   'transfer'                    => '',
+  'transferred'                 => '',
   'transferred in'              => '',
+  'transferred in / out'        => '',
   'transferred out'             => '',
   'trial_balance'               => '',
+  'true'                        => '',
+  'uncleared'                   => '',
   'unconfigured'                => '',
-  'up'                          => '',
+  'unimport'                    => '',
+  'unimported'                  => '',
+  'unnamed record template'     => '',
+  'until'                       => '',
+  'uploaded'                    => '',
+  'uploaded Documents'          => '',
   'use program settings'        => '',
   'use user config'             => '',
   'used'                        => '',
+  'used_br'                     => 'used',
   'valid from'                  => '',
-  'vendor'                      => '',
   'vendor_invoice_list'         => '',
   'vendor_list'                 => '',
+  'waiting for job to be started' => '',
   'warehouse_journal_list'      => '',
   'warehouse_report_list'       => '',
+  'warehouse_usage_list'        => '',
+  'will be set upon posting'    => '',
+  'will be set upon saving'     => '',
   'with skonto acc. to pt'      => '',
-  'with_skonto_pt'              => 'with skonto payment terms',
+  'with_skonto_pt'              => '',
   'without skonto'              => '',
-  'without_skonto'              => 'without skonto',
-  'wrongformat'                 => '',
+  'without_skonto'              => '',
+  'working copy'                => '',
   'yearly'                      => '',
   'yes'                         => '',
   'you can find professional help.' => '',
+  '– all available test files –' => '',
 };
 
 1;
index 945b239..104dd4d 100644 (file)
@@ -29,31 +29,54 @@ order=< > \n
 \n=<br>
 
 [Template/LaTeX]
-order=\\ <pagebreak> & \n \r " $ <bullet> % _ # ^ { } < > £ ± ² ³ ° § ® ©
+order=\\ <pagebreak> & \n \r " $ <bullet> % _ # ^ { } < > £ ± ² ³ ° § ® © ~ \xad \xa0 ➔ → ← ↔ ↕ | − ≤ ≥ ‐ <200b> Ω μ Δ λ Ø ø ‑
 \\=\\textbackslash\s
 <pagebreak>=
-"=''
 &=\\&
+\n=\\newline\s
+\r=
+"=''
 $=\\$
 <bullet>=$\\bullet$
 %=\\%
 _=\\_
-#=\\#
+# A hash mark starts a comment; therefore the line is ignored. So use
+# its hex code instead.
+\x23=\\#
+^=\\^\\\s
 {=\\{
 }=\\}
 <=$<$
 >=$>$
 £=\\pounds\s
-\n=\\newline\s
-\r=
 ±=$\\pm$
-^=\\^\\\s
 ²=$^2$
 ³=$^3$
 °=$^\\circ$
 §=\\S
 ®=\\textregistered
 ©=\\textcopyright
+~={\\raisebox{0.5ex}{\\texttildelow}}
+\xad=\\-
+\xa0=~
+➔=$\\rightarrow$
+→=$\\rightarrow$
+←=$\\leftarrow$
+↔=$\\leftrightarrow$
+↕=$\\updownarrow$
+|={\\textbar}
+−={\\textemdash}
+≤=$\\leq$
+≥=$\\geq$
+‐={}-{}
+​={\\hspace{0pt}}
+Ω=$\\Omega$
+μ={\\textmu}
+Δ=$\\Delta$
+λ=$\\lambda$
+Ø={\\O}
+ø={\\o}
+‑={}-{}
 
 [Template/OpenDocument]
 order=& < > " ' \x80 \n \r
diff --git a/menus/.dummy b/menus/.dummy
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/menus/admin/.dummy b/menus/admin/.dummy
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/menus/mobile/.dummy b/menus/mobile/.dummy
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/menus/mobile/00-erp.yaml b/menus/mobile/00-erp.yaml
new file mode 100644 (file)
index 0000000..448a554
--- /dev/null
@@ -0,0 +1,24 @@
+# This is the main menu config file for mobile user space menu entries.
+#
+# Th structure is the same as in user/, but currently infinite nesting is not supported.
+#
+---
+- id: image_upload
+  name: Image Upload
+  order: 100
+  access: sales_delivery_order_edit
+  params:
+    action: ImageUpload/upload_image
+    object_type: sales_delivery_order
+- id: component_test
+  name: Component Test
+  order: 200
+  access: developer
+  params:
+    action: MaterializeTest/components
+- id: modal_test
+  name: Modal Test
+  order: 300
+  access: developer
+  params:
+    action: MaterializeTest/modal
diff --git a/menus/user/.dummy b/menus/user/.dummy
new file mode 100644 (file)
index 0000000..e69de29
index ec7672e..2a8777d 100644 (file)
@@ -11,7 +11,9 @@
 #          ( ) & | are supported.  if binary operator is missing the last
 #          operator in same scope is repeated, or "|" if none used in scope
 #          yet. client config entries can be used as rights by prefixing them
-#          with "client/". If missing, access will be granted.
+#          with "client/".
+#          ! is supported to negate the subsequent expression.
+#          If missing, access will be granted.
 #
 #          Example:
 #            client/feature_default_enabled | ( feature & system )
@@ -32,7 +34,7 @@
   name: Add Customer
   icon: customer_add
   order: 100
-  access: customer_vendor_edit
+  access: customer_vendor_edit|customer_vendor_all_edit
   params:
     action: CustomerVendor/add
     db: customer
@@ -41,7 +43,7 @@
   name: Add Vendor
   icon: vendor_add
   order: 200
-  access: customer_vendor_edit
+  access: customer_vendor_edit|customer_vendor_all_edit
   params:
     action: CustomerVendor/add
     db: vendor
   icon: part_add
   order: 300
   access: part_service_assembly_edit
-  module: ic.pl
   params:
-    action: add
-    item: part
+    action: Part/add_part
 - parent: master_data
   id: master_data_add_service
   name: Add Service
   icon: service_add
   order: 400
   access: part_service_assembly_edit
-  module: ic.pl
   params:
-    action: add
-    item: service
+    action: Part/add_service
 - parent: master_data
   id: master_data_add_assembly
   name: Add Assembly
   icon: assembly_add
   order: 500
   access: part_service_assembly_edit
-  module: ic.pl
   params:
-    action: add
-    item: assembly
+    action: Part/add_assembly
+- parent: master_data
+  id: master_data_add_assortment
+  name: Add Assortment
+  icon: assortment_add
+  order: 550
+  access: part_service_assembly_edit & client/feature_experimental_assortment
+  params:
+    action: Part/add_assortment
 - parent: master_data
   id: master_data_add_project
   name: Add Project
   icon: prices_update
   order: 800
   access: part_service_assembly_edit
-  module: ic.pl
   params:
-    action: search_update_prices
+    action: PartsPriceUpdate/search_update_prices
 - parent: master_data
   id: master_data_price_rules
   name: Price Rules
   name: Customers
   icon: customer_report
   order: 100
-  access: customer_vendor_edit
   params:
     action: CustomerVendor/search
     db: customer
   name: Vendors
   icon: vendor_report
   order: 200
-  access: customer_vendor_edit
   params:
     action: CustomerVendor/search
     db: vendor
   id: master_data_reports_contacts
   name: Contacts
   order: 300
-  access: customer_vendor_edit
   params:
     action: CustomerVendor/search_contact
     db: customer
 - parent: master_data_reports
-  id: master_data_reports_parts
-  name: Parts
+  id: master_data_reports_articles
+  name: Articles
   icon: part_report
-  order: 400
-  access: part_service_assembly_details
-  module: ic.pl
-  params:
-    action: search
-    searchitems: part
-- parent: master_data_reports
-  id: master_data_reports_services
-  name: Services
-  icon: service_report
   order: 500
   access: part_service_assembly_details
   module: ic.pl
   params:
     action: search
-    searchitems: service
-- parent: master_data_reports
-  id: master_data_reports_assemblies
-  name: Assemblies
-  icon: assembly_report
-  order: 600
-  access: part_service_assembly_details
-  module: ic.pl
-  params:
-    action: search
-    searchitems: assembly
+    searchitems: article
 - parent: master_data_reports
   id: master_data_reports_projects
   name: Projects
   name: Add Letter
   order: 800
   access: sales_letter_edit
-  module: letter.pl
   params:
-    action: add
+    action: Letter/add
+    is_sales: 1
 - parent: ar
   id: ar_invoices
   name: Invoices
   name: Quotations
   icon: report_quotations
   order: 200
-  access: sales_quotation_edit
+  access: sales_quotation_edit | sales_quotation_view
   module: oe.pl
   params:
     action: search
   name: Sales Orders
   icon: report_sales_orders
   order: 300
-  access: sales_order_edit
+  access: sales_order_edit | sales_order_view
   module: oe.pl
   params:
     action: search
   name: Delivery Orders
   icon: delivery_order_report
   order: 400
-  access: sales_delivery_order_edit
+  access: sales_delivery_order_edit | sales_delivery_order_view
   module: do.pl
   params:
     action: search
   name: Invoices, Credit Notes & AR Transactions
   icon: invoices_report
   order: 500
-  access: invoice_edit
   module: ar.pl
   params:
     action: search
   module: dn.pl
   params:
     action: search
+- parent: ar_reports
+  id: ar_order_item_search
+  name: Order item search
+  order: 750
+  access: sales_order_edit
+  params:
+    action: OrderItem/search
 - parent: ar_reports
   id: ar_reports_delivery_plan
   name: Delivery Plan
   name: Letters
   order: 1100
   access: sales_letter_report
-  module: letter.pl
   params:
-    action: search
+    action: Letter/list
+    is_sales: 1
+- id: webshop
+  name: Webshop
+  order: 250
+- parent: webshop
+  id: webshop_import
+  name: Webshop Import
+  access: shop_order
+  params:
+    action: ShopOrder/list
+    filter.transferred:eq_ignore_empty: 0
+    filter.obsolete: 0
+    db: shop_orders
+    sort_by: shop_ordernumber
+- parent: webshop
+  id: webshop_articles
+  name: Webshop articles
+  access: shop_part_edit
+  params:
+    action: ShopPart/list_articles
+    db: shop_parts
+    sort_by: part.onhand
 - id: ap
   name: AP
   icon: ap
   params:
     action: add
     type: invoice
+- parent: ap
+  id: ap_add_letter
+  name: Add Letter
+  order: 450
+  access: purchase_letter_edit
+  params:
+    action: Letter/add
+    is_sales: 0
 - parent: ap
   id: ap_reports
   name: Reports
   name: RFQs
   icon: rfq_report
   order: 100
-  access: request_quotation_edit
+  access: request_quotation_edit | request_quotation_view
   module: oe.pl
   params:
     action: search
   name: Purchase Orders
   icon: purchase_order_report
   order: 200
-  access: purchase_order_edit
+  access: purchase_order_edit | purchase_order_view
   module: oe.pl
   params:
     action: search
   id: ap_reports_delivery_orders
   name: Delivery Orders
   order: 300
-  access: purchase_delivery_order_edit
+  access: purchase_delivery_order_edit | purchase_delivery_order_view
   module: do.pl
   params:
     action: search
     type: purchase_delivery_order
+- parent: ap_reports
+  id: ap_reports_supplier_delivery_orders
+  name: Supplier Delivery Orders
+  order: 350
+  access: purchase_delivery_order_edit | purchase_delivery_order_view
+  module: do.pl
+  params:
+    action: search
+    type: supplier_delivery_order
 - parent: ap_reports
   id: ap_reports_vendor_invoices_ap_transactions
   name: Vendor Invoices & AP Transactions
   order: 400
-  access: vendor_invoice_edit
   module: ap.pl
   params:
     action: search
-    nextsub: ap_transactions
 - parent: ap_reports
   id: ap_reports_delivery_plan
   name: Delivery Plan
   params:
     action: DeliveryValueReport/list
     vc: vendor
+- parent: ap_reports
+  id: ap_reports_letters
+  name: Letters
+  order: 1100
+  access: purchase_letter_report
+  params:
+    action: Letter/list
+    is_sales: 0
 - id: warehouse
   name: Warehouse
   icon: warehouse
   params:
     action: transfer_warehouse_selection
     trans_type: removal
+- parent: warehouse
+  id: warehouse_stocktaking
+  name: Stocktaking
+  order: 450
+  access: warehouse_management
+  params:
+    action: Inventory/stocktaking
 - parent: warehouse
   id: warehouse_reports
   name: Reports
   module: wh.pl
   params:
     action: journal
+- parent: warehouse_reports
+  id: warehouse_reports_whusage
+  name: WHUsage
+  icon: warehouse_usage
+  order: 300
+  access: warehouse_contents | warehouse_management
+  params:
+    action: Inventory/stock_usage
+- parent: warehouse_reports
+  id: warehouse_stocktaking_journal
+  name: Stocktaking Journal
+  order: 400
+  access: warehouse_contents | warehouse_management
+  params:
+    action: Inventory/stocktaking_journal
 - id: general_ledger
   name: General Ledger
   icon: gl
   name: Add Transaction
   icon: transaction_add
   order: 100
-  access: general_ledger
+  access: gl_transactions
   module: gl.pl
   params:
     action: add
   name: Add AR Transaction
   icon: ar_transaction_add
   order: 200
-  access: general_ledger
+  access: ar_transactions
   module: ar.pl
   params:
     action: add
   name: Add AP Transaction
   icon: ap_transaction_add
   order: 300
-  access: general_ledger
+  access: ap_transactions
   module: ap.pl
   params:
     action: add
   name: DATEV - Export Assistent
   icon: datev
   order: 400
-  access: datev_export
+  access: datev_export & client/feature_datev
   module: datev.pl
   params:
     action: export
+- parent: general_ledger
+  id: general_ledger_gobd_export
+  name: GoBD Export
+  icon: gobd
+  order: 450
+  access: general_ledger
+  params:
+    action: GoBD/filter
+- parent: general_ledger
+  id: year_end_closing
+  name: Year-end closing
+  icon: cbob
+  order: 470
+  access: general_ledger
+  params:
+    action: YearEndTransactions/form
+- parent: general_ledger
+  id: zugferd_import
+  name: Factur-X/ZUGFeRD import
+  icon: cbob
+  order: 485
+  access: ap_transactions
+  params:
+    action: ZUGFeRD/upload_zugferd
+- parent: general_ledger
+  id: pay_posting_import
+  name: DATEV - Pay Postings Import
+  icon: datev
+  order: 495
+  access: datev_export & client/feature_datev
+  params:
+    action: PayPostingImport/upload_pay_postings
 - parent: general_ledger
   id: general_ledger_reports
   name: Reports
   name: Journal
   icon: journal
   order: 300
-  access: general_ledger
+  access: general_ledger | gl_transactions
   module: gl.pl
   params:
     action: search
     action: bank_transfer_add
     vc: vendor
 - parent: cash
-  id: cash_bank_import
+  id: cash_bank_import_menu
   name: Bank Import
   order: 500
-- parent: cash_bank_import
+  access: bank_transaction
+- parent: cash_bank_import_menu
   id: cash_bank_import_csv
-  name: CSV
+  name: Custom CSV format
   order: 100
-  access: bank_transaction
   params:
     action: CsvImport/new
     profile.type: bank_transactions
-- parent: cash_bank_import
+- parent: cash_bank_import_menu
   id: cash_bank_import_mt940
-  name: MT940
+  name: SWIFT MT940 format
   order: 200
-  access: bank_transaction
   params:
     action: BankImport/upload_mt940
 - parent: cash
   access: bank_transaction
   params:
     action: Reconciliation/search
-    next_sub: Reconciliation/reconciliation
 - parent: cash
   id: cash_reconciliation
   name: Reconciliation
   params:
     action: report
     report: trial_balance
+- parent: reports
+  id: reports_erfolgsrechnung
+  name: Erfolgsrechnung
+  icon: income_statement
+  order: 300
+  access: report & client/feature_erfolgsrechnung
+  module: rp.pl
+  params:
+    action: report
+    report: erfolgsrechnung
 - parent: reports
   id: reports_income_statement
   name: Income Statement
   icon: income_statement
   order: 300
-  access: report
+  access: report & client/feature_eurechnung
   module: rp.pl
   params:
     action: report
   id: reports_bwa
   name: BWA
   order: 400
-  access: report
+  access: report & client/feature_eurechnung
   module: rp.pl
   params:
     action: report
   name: Balance Sheet
   icon: balance_sheet
   order: 500
-  access: report
+  access: report & client/feature_balance
   module: rp.pl
   params:
     action: report
   name: UStVa
   icon: ustva
   order: 600
-  access: advance_turnover_tax_return
+  access: advance_turnover_tax_return & client/feature_ustva
   module: ustva.pl
   params:
     action: report
     action: LiquidityProjection/show
 - id: mebil
   name: Mebil
-  icon: report
+  icon: wtg
   order: 750
 - parent: mebil
   id: mebil_showmap
   name: Productivity
   icon: productivity
   order: 900
-  access: productivity
 - parent: productivity
   id: productivity_show_todo_list
   name: Show TODO list
   order: 100
+  access: productivity
   module: todo.pl
   params:
     action: show_todo_list
   id: productivity_add_follow_up
   name: Add Follow-Up
   order: 200
+  access: productivity
   module: fu.pl
   params:
     action: add
   id: productivity_edit_access_rights
   name: Edit Access Rights
   order: 300
+  access: productivity
   module: fu.pl
   params:
     action: edit_access_rights
   id: productivity_reports_follow_ups
   name: Follow-Ups
   order: 100
+  access: productivity
   module: fu.pl
   params:
     action: search
   id: productivity_reports_email_journal
   name: Email journal
   order: 200
+  access: email_journal
   module: controller.pl
+  icon: mail_journal
   params:
     action: EmailJournal/list
 - id: system
   id: system_ustva_einstellungen
   name: UStVa Einstellungen
   order: 200
+  access: client/feature_ustva
   module: ustva.pl
   params:
     action: config_step1
   module: am.pl
   params:
     action: list_account
+- parent: system_chart_of_accounts
+  id: system_chart_of_accounts_report_configuration_overview
+  name: Report configuration overview
+  order: 300
+  params:
+    action: Chart/show_report_configuration_overview
 - parent: system
   id: system_buchungsgruppen
-  name: Buchungsgruppen
+  name: Booking groups
   order: 500
   params:
     action: Buchungsgruppen/list
   name: Bank accounts
   order: 800
   params:
-    action: BankAccount/list
+    action: SimpleSystemSetting/list
+    type: bank_account
 - parent: system
-  id: system_groups
-  name: Groups
+  id: system_partsgroups
+  name: Partsgroups
   order: 900
-  module: pe.pl
   params:
-    action: search
-    type: partsgroup
+    action: SimpleSystemSetting/list
+    type: parts_group
+- parent: system
+  id: system_part_classification
+  name: Parts Classification
+  icon: partsclassific
+  order: 1100
+  params:
+    action: SimpleSystemSetting/list
+    type: part_classification
 - parent: system
   id: system_pricegroups
   name: Pricegroups
-  order: 1000
-  module: pe.pl
+  order: 1120
   params:
-    action: search
+    action: SimpleSystemSetting/list
     type: pricegroup
 - parent: system
   id: system_edit_units
   name: Edit units
-  order: 1100
+  order: 1140
   module: am.pl
   params:
     action: edit_units
   id: system_price_factors
   name: Price Factors
   order: 1200
-  module: am.pl
   params:
-    action: list_price_factors
+    action: SimpleSystemSetting/list
+    type: price_factor
+- parent: system
+  id: system_greetings
+  name: Greetings
+  order: 1250
+  params:
+    action: SimpleSystemSetting/list
+    type: greeting
 - parent: system
   id: system_departments
   name: Departments
   order: 1300
   params:
-    action: Department/list
+    action: SimpleSystemSetting/list
+    type: department
 - parent: system
   id: system_types_of_business
   name: Types of Business
   order: 1400
   params:
-    action: Business/list
+    action: SimpleSystemSetting/list
+    type: business
 - parent: system
-  id: system_leads
-  name: Leads
-  order: 1500
-  module: am.pl
+  id: system_contact_titles
+  name: Contact Titles
+  order: 1420
   params:
-    action: list_lead
+    action: SimpleSystemSetting/list
+    type: contact_title
+- parent: system
+  id: system_contact_departments
+  name: Contact Departments
+  order: 1430
+  params:
+    action: SimpleSystemSetting/list
+    type: contact_department
 - parent: system
   id: system_project_types
   name: Project Types
   order: 1600
   params:
-    action: ProjectType/list
+    action: SimpleSystemSetting/list
+    type: project_type
 - parent: system
   id: system_project_status
   name: Project Status
   order: 1700
   params:
-    action: ProjectStatus/list
+    action: SimpleSystemSetting/list
+    type: project_status
 - parent: system
   id: system_requirement_specs
   name: Requirement specs
   name: Pre-defined Texts
   order: 100
   params:
-    action: RequirementSpecPredefinedText/list
+    action: SimpleSystemSetting/list
+    type: requirement_spec_predefined_text
 - parent: system_requirement_specs
   id: system_requirement_specs_requirement_spec_types
   name: Requirement Spec Types
   order: 200
   params:
-    action: RequirementSpecType/list
+    action: SimpleSystemSetting/list
+    type: requirement_spec_type
 - parent: system_requirement_specs
   id: system_requirement_specs_requirement_spec_statuses
   name: Requirement Spec Statuses
   order: 300
   params:
-    action: RequirementSpecStatus/list
+    action: SimpleSystemSetting/list
+    type: requirement_spec_status
 - parent: system_requirement_specs
   id: system_requirement_specs_complexities
   name: Complexities
   order: 400
   params:
-    action: RequirementSpecComplexity/list
+    action: SimpleSystemSetting/list
+    type: requirement_spec_complexity
 - parent: system_requirement_specs
   id: system_requirement_specs_risks
   name: Risks
   order: 500
   params:
-    action: RequirementSpecRisk/list
+    action: SimpleSystemSetting/list
+    type: requirement_spec_risk
 - parent: system_requirement_specs
   id: system_requirement_specs_acceptance_statuses
   name: Acceptance Statuses
   order: 600
   params:
-    action: RequirementSpecAcceptanceStatus/list
+    action: SimpleSystemSetting/list
+    type: requirement_spec_acceptance_status
 - parent: system
   id: system_languages_and_translations
   name: Languages and translations
   order: 1900
 - parent: system_languages_and_translations
   id: system_languages_and_translations_add_language
-  name: Add Language
+  name: Languages
   order: 100
-  module: am.pl
   params:
-    action: add_language
-- parent: system_languages_and_translations
-  id: system_languages_and_translations_list_languages
-  name: List Languages
-  order: 200
-  module: am.pl
-  params:
-    action: list_language
+    action: SimpleSystemSetting/list
+    type: language
 - parent: system_languages_and_translations
   id: system_languages_and_translations_greetings
   name: Greetings
   module: generictranslations.pl
   params:
     action: edit_sepa_strings
+- parent: system_languages_and_translations
+  id: system_languages_and_translations_zugferd_notes
+  name: Factur-X/ZUGFeRD notes for each invoice
+  order: 450
+  module: generictranslations.pl
+  params:
+    action: edit_zugferd_notes
+- parent: system_languages_and_translations
+  id: system_languages_and_translations_email_strings
+  name: Preset email strings
+  order: 500
+  module: generictranslations.pl
+  params:
+    action: edit_email_strings
 - parent: system
   id: system_payment_terms
   name: Payment Terms
   module: am.pl
   params:
     action: list_warehouses
+- parent: system
+  id: system_shops
+  name: Web shops
+  order: 2350
+  access: edit_shop_config
+  params:
+    action: Shop/list
 - parent: system
   id: system_import_csv
   name: Import CSV
   params:
     action: CsvImport/new
     profile.type: contacts
+- parent: system_import_csv
+  id: system_import_csv_additional_billing_address
+  name: Additional Billing Addresses
+  order: 250
+  params:
+    action: CsvImport/new
+    profile.type: billing_addresses
 - parent: system_import_csv
   id: system_import_csv_shipto
   name: Shipto
   params:
     action: CsvImport/new
     profile.type: orders
+- parent: system_import_csv
+  id: system_import_csv_delivery_orders
+  name: Delivery Orders
+  order: 720
+  params:
+    action: CsvImport/new
+    profile.type: delivery_orders
+- parent: system_import_csv
+  id: system_import_csv_ar_transactions
+  name: AR Transactions
+  order: 800
+  params:
+    action: CsvImport/new
+    profile.type: ar_transactions
 - parent: system
   id: system_templates
   name: Templates
     action: display_template_form
     format: tex
     type: templates
-- parent: system_templates
-  id: system_templates_stylesheet
-  name: Stylesheet
-  order: 300
-  module: amtemplates.pl
-  params:
-    action: display_template_form
-    type: stylesheet
 - parent: system
   id: system_general_ledger_corrections
   name: General Ledger Corrections
   order: 700
   params:
     action: LoginScreen/logout
+
+- id: develop
+  name: Developer Tools
+  icon: developer
+  order: 1200
+  access: developer
+- parent: develop
+  id: part_test
+  name: Part Test
+  access: developer
+  icon: part
+  order: 100
+  params:
+    action: Part/test_page
diff --git a/menus/user/10-custom-data-export.yaml b/menus/user/10-custom-data-export.yaml
new file mode 100644 (file)
index 0000000..bbe7e8b
--- /dev/null
@@ -0,0 +1,14 @@
+---
+- parent: reports
+  id: custom_data_export
+  name: Custom data export
+  order: 9000
+  params:
+    action: CustomDataExport/list
+- parent: system
+  id: custom_data_export_designer
+  name: Custom data export
+  order: 2250
+  access: custom_data_export_designer
+  params:
+    action: CustomDataExportDesigner/list
diff --git a/menus/user/10-order-controller.yaml b/menus/user/10-order-controller.yaml
new file mode 100644 (file)
index 0000000..f4ec4d1
--- /dev/null
@@ -0,0 +1,50 @@
+- parent: ar
+  id: ar_add_quotation
+  access: sales_quotation_edit & (!client/feature_experimental_order)
+- parent: ar
+  id: ar_add_sales_order
+  access: sales_order_edit & (!client/feature_experimental_order)
+- parent: ap
+  id: ap_add_rfq
+  access: request_quotation_edit & (!client/feature_experimental_order)
+- parent: ap
+  id: ap_add_purchase_order
+  access: purchase_order_edit & (!client/feature_experimental_order)
+
+- parent: ar
+  id: ar_add_quotation_experimental
+  name: Add Quotation
+  icon: quotation_add
+  order: 250
+  access: sales_quotation_edit & client/feature_experimental_order
+  params:
+    action: Order/add
+    type: sales_quotation
+- parent: ar
+  id: ar_add_sales_order_experimental
+  name: Add Sales Order
+  icon: sales_order_add
+  order: 350
+  access: sales_order_edit & client/feature_experimental_order
+  params:
+    action: Order/add
+    type: sales_order
+
+- parent: ap
+  id: ap_add_rfq_experimental
+  name: Add RFQ
+  icon: rfq_add
+  order: 150
+  access: request_quotation_edit & client/feature_experimental_order
+  params:
+    action: Order/add
+    type: request_quotation
+- parent: ap
+  id: ap_add_purchase_order_experimental
+  name: Add Purchase Order
+  icon: purchase_order_add
+  order: 250
+  access: purchase_order_edit & client/feature_experimental_order
+  params:
+    action: Order/add
+    type: purchase_order
diff --git a/menus/user/10-time-recording.yaml b/menus/user/10-time-recording.yaml
new file mode 100644 (file)
index 0000000..eab685f
--- /dev/null
@@ -0,0 +1,22 @@
+---
+- parent: system
+  id: system_time_recording_articles
+  name: Time Recording Articles
+  order: 2370
+  params:
+    action: SimpleSystemSetting/list
+    type: time_recording_article
+- parent: productivity
+  id: productivity_time_recording
+  name: Time Recording
+  order: 350
+  access: time_recording
+  params:
+    action: TimeRecording/edit
+- parent: productivity_reports
+  id: productivity_reports_time_recording
+  name: Time Recording
+  order: 300
+  access: time_recording
+  params:
+    action: TimeRecording/list
diff --git a/menus/user/20-invoice-for-advance-payment.yaml b/menus/user/20-invoice-for-advance-payment.yaml
new file mode 100644 (file)
index 0000000..b76dcad
--- /dev/null
@@ -0,0 +1,10 @@
+- parent: ar
+  id: ar_add_sales_invoice_for_advance_payment
+  name: Add Invoice for Advance Payment
+  icon: sales_invoice_add
+  order: 550
+  access: invoice_edit
+  module: is.pl
+  params:
+    action: add
+    type: invoice_for_advance_payment
diff --git a/modules/fallback/Daemon/Generic.pm b/modules/fallback/Daemon/Generic.pm
deleted file mode 100644 (file)
index c185e8a..0000000
+++ /dev/null
@@ -1,553 +0,0 @@
-
-# Copyright (C) 2006, David Muir Sharnoff <perl@dave.sharnoff.org>
-
-package Daemon::Generic;
-
-use strict;
-use warnings;
-require Exporter;
-require POSIX;
-use Getopt::Long;
-use File::Slurp;
-use File::Flock;
-our @ISA = qw(Exporter);
-our @EXPORT = qw(newdaemon);
-
-our $VERSION = 0.71;
-
-our $force_quit_delay = 15;
-our $package = __PACKAGE__;
-our $caller;
-
-sub newdaemon
-{
-       my (%args) = @_;
-       my $pkg = $caller || caller() || 'main';
-
-       my $foo = bless {}, $pkg;
-
-       unless ($foo->isa($package)) {
-               no strict qw(refs);
-               my $isa = \@{"${pkg}::ISA"};
-               unshift(@$isa, $package);
-       }
-
-       bless $foo, 'This::Package::Does::Not::Exist';
-       undef $foo;
-
-       new($pkg, %args);
-}
-
-sub new
-{
-       my ($pkg, %args) = @_;
-
-       if ($pkg eq __PACKAGE__) {
-               $pkg = caller() || 'main';
-       }
-
-       srand(time ^ ($$ << 5))
-               unless $args{no_srand};
-
-       my $av0 = $0;
-       $av0 =~ s!/!/.!g;
-
-       my $self = {
-               gd_args         => \%args,
-               gd_pidfile      => $args{pidfile},
-               gd_logpriority  => $args{logpriority},
-               gd_progname     => $args{progname}
-                                       ? $args{progname}
-                                       : $0,
-               gd_pidbase      => $args{pidbase}
-                                       ? $args{pidbase}
-                                       : ($args{progname} 
-                                               ? "/var/run/$args{progname}"
-                                               : "/var/run/$av0"),
-               gd_foreground   => $args{foreground} || 0,
-               configfile      => $args{configfile}
-                                       ? $args{configfile}
-                                       : ($args{progname}
-                                               ? "/etc/$args{progname}.conf"
-                                               : "/etc/$av0"),
-               debug           => $args{debug} || 0,
-       };
-       bless $self, $pkg;
-
-       $self->gd_getopt;
-       $self->gd_parse_argv;
-
-       my $do = $self->{do} = $ARGV[0];
-
-       $self->gd_help          if $do eq 'help';
-       $self->gd_version       if $do eq 'version';
-       $self->gd_install       if $do eq 'install';
-       $self->gd_uninstall     if $do eq 'uninstall';
-
-       $self->gd_pidfile unless $self->{gd_pidfile};
-
-       my %newconfig = $self->gd_preconfig;
-
-       $self->{gd_pidfile} = $newconfig{pidfile} if $newconfig{pidfile};
-
-       print "Configuration looks okay\n" if $do eq 'check';
-
-       my $pidfile = $self->{gd_pidfile};
-       my $killed = 0;
-       my $locked = 0;
-       if (-e $pidfile) {
-               if ($locked = lock($pidfile, undef, 'nonblocking')) {
-                       # old process is dead
-                       if ($do eq 'status') {
-                           print "$0 dead\n";
-                           exit 1;
-                       }
-               } else {
-                       sleep(2) if -M $pidfile < 2/86400;
-                       my $oldpid = read_file($pidfile);
-                       chomp($oldpid);
-                       if ($oldpid) {
-                               if ($do eq 'stop' or $do eq 'restart') {
-                                       $killed = $self->gd_kill($oldpid);
-                                       $locked = lock($pidfile);
-                                       if ($do eq 'stop') {
-                                               unlink($pidfile);
-                                               exit;
-                                       }
-                               } elsif ($do eq 'reload') {
-                                       if (kill(1,$oldpid)) {
-                                               print "Requested reconfiguration\n";
-                                               exit;
-                                       } else {
-                                               print "Kill failed: $!\n";
-                                       }
-                               } elsif ($do eq 'status') {
-                                       if (kill(0,$oldpid)) {
-                                               print "$0 running - pid $oldpid\n";
-                                               $self->gd_check($pidfile, $oldpid);
-                                               exit 0;
-                                       } else {
-                                               print "$0 dead\n";
-                                               exit 1;
-                                       }
-                               } elsif ($do eq 'check') {
-                                       if (kill(0,$oldpid)) {
-                                               print "$0 running - pid $oldpid\n";
-                                               $self->gd_check($pidfile, $oldpid);
-                                               exit;
-                                       } 
-                               } elsif ($do eq 'start') {
-                                       print "\u$self->{gd_progname} is already running (pid $oldpid)\n";
-                                       exit; # according to LSB, this is no error
-                               }
-                       } else {
-                               $self->gd_error("Pid file $pidfile is invalid but locked, exiting\n");
-                       }
-               }
-       } else {
-               $locked = lock($pidfile, undef, 'nonblocking') 
-                       or die "Could not lock pid file $pidfile: $!";
-       }
-
-       if ($do eq 'reload' || $do eq 'stop' || $do eq 'check' || ($do eq 'restart' && ! $killed)) {
-               print "No $0 running\n";
-       }
-
-       if ($do eq 'stop') {
-               unlink($pidfile);
-               exit;
-       }
-
-       if ($do eq 'status') {
-               print "Unused\n";
-               exit 3;
-       }
-
-       if ($do eq 'check') {
-               $self->gd_check($pidfile);
-               exit 
-       }
-
-       unless ($do eq 'reload' || $do eq 'restart' || $do eq 'start') {
-               $self->gd_other_cmd($do, $locked);
-       }
-
-       unless ($self->{gd_foreground}) {
-               $self->gd_daemonize;
-       }
-
-       $locked or lock($pidfile, undef, 'nonblocking') 
-               or die "Could not lock PID file $pidfile: $!";
-
-       write_file($pidfile, "$$\n");
-
-       print STDERR "Starting up...\n";
-
-       $self->gd_postconfig(%newconfig);
-
-       $self->gd_setup_signals;
-
-       $self->gd_run;
-
-       unlink($pidfile);
-       exit(0);
-}
-
-sub gd_check {}
-
-sub gd_more_opt { return() }
-
-sub gd_getopt
-{
-       my $self = shift;
-       Getopt::Long::Configure("auto_version");
-       GetOptions(
-               'configfile=s'  => \$self->{configfile},
-               'foreground!'   => \$self->{gd_foreground},
-               'debug!'        => \$self->{debug},
-               $self->{gd_args}{options}
-                       ? %{$self->{gd_args}{options}}
-                       : (),
-               $self->gd_more_opt(),
-       ) or exit($self->gd_usage());
-
-       if (@ARGV < ($self->{gd_args}{minimum_args} || 1)) {
-               exit($self->gd_usage());
-       }
-       if (@ARGV > ($self->{gd_args}{maximum_args} || 1)) {
-               exit($self->gd_usage());
-       }
-}
-
-sub gd_parse_argv { }
-
-sub gd_help
-{
-       my $self = shift;
-       exit($self->gd_usage($self->{gd_args}));
-}
-
-sub gd_version
-{
-       my $self = shift;
-       no strict qw(refs);
-       my $v = $self->{gd_args}{version} 
-               || ${ref($self)."::VERSION"} 
-               || $::VERSION 
-               || $main::VERSION 
-               || "?";
-       print "$self->{gd_progname} - version $v\n";;
-       exit;
-} 
-
-sub gd_pidfile
-{
-       my $self = shift;
-       my $x = $self->{configfile};
-       $x =~ s!/!.!g;
-       $self->{gd_pidfile} = "$self->{gd_pidbase}$x.pid";
-}
-
-sub gd_other_cmd
-{
-       my $self = shift;
-       $self->gd_usage;
-       exit(1);
-}
-
-sub gd_redirect_output
-{
-       my $self = shift;
-       return if $self->{gd_foreground};
-       my $logname = $self->gd_logname;
-       my $p = $self->{gd_logpriority} ? "-p $self->{gd_logpriority}" : "";
-       open(STDERR, "|logger $p -t '$logname'") or (print "could not open stderr: $!" && exit(1));
-       close(STDOUT);
-       open(STDOUT, ">&STDERR") or die "redirect STDOUT -> STDERR: $!";
-       close(STDIN);
-}
-
-sub gd_daemonize
-{
-       my $self = shift;
-       print "Starting $self->{gd_progname} server\n";
-       $self->gd_redirect_output();
-       my $pid;
-       POSIX::_exit(0) if $pid = fork;
-       die "Could not fork: $!" unless defined $pid;
-       POSIX::_exit(0) if $pid = fork;
-       die "Could not fork: $!" unless defined $pid;
-
-       POSIX::setsid();
-       select(STDERR);
-       $| = 1;
-       print "Sucessfully daemonized\n";
-}
-
-sub gd_logname
-{
-       my $self = shift;
-       return $self->{gd_progname}."[$$]";
-}
-
-sub gd_reconfig_event
-{
-       my $self = shift;
-       print STDERR "Reconfiguration requested\n";
-       $self->gd_postconfig($self->gd_preconfig());
-}
-
-sub gd_quit_event
-{
-       my $self = shift;
-       print STDERR "Quitting...\n";
-       exit(0);
-}
-
-sub gd_setup_signals
-{
-       my $self = shift;
-       $SIG{INT} = sub { $self->gd_quit_event() };
-       $SIG{HUP} = sub { $self->gd_reconfig_event() };
-}
-
-sub gd_run { die "must defined gd_run()" }
-
-sub gd_error
-{
-       my $self = shift;
-       my $e = shift;
-       my $do = $self->{do};
-       if ($do && $do eq 'stop') {
-               warn $e;
-       } else {
-               die $e;
-       }
-}
-
-sub gd_flags_more { return () }
-
-sub gd_flags
-{
-       my $self = shift;
-       return (
-               '-c file'       => "Specify configuration file (instead of $self->{configfile})",
-               '-f'            => "Run in the foreground (don't detach)",
-               $self->gd_flags_more
-       );
-}
-
-sub gd_commands_more { return () }
-
-sub gd_commands
-{
-       my $self = shift;
-       return (
-               start           => "Starts a new $self->{gd_progname} if there isn't one running already",
-               stop            => "Stops a running $self->{gd_progname}",
-               reload          => "Causes a running $self->{gd_progname} to reload it's config file.  Starts a new one if none is running.",
-               restart         => "Stops a running $self->{gd_progname} if one is running.  Starts a new one.",
-               $self->gd_commands_more(),
-               ($self->gd_can_install()
-                       ? ('install' => "Setup $self->{gd_progname} to run automatically after reboot")
-                       : ()),
-               ($self->gd_can_uninstall()
-                       ? ('uninstall' => "Do not run $self->{gd_progname} after reboots")
-                       : ()),
-               check           => "Check the configuration file and report the daemon state",
-               help            => "Display this usage info",
-               version         => "Display the version of $self->{gd_progname}",
-       )
-}
-
-sub gd_positional_more { return() }
-
-sub gd_alts
-{
-       my $offset = shift;
-       my @results;
-       for (my $i = $offset; $i <= $#_; $i += 2) {
-               push(@results, $_[$i]);
-       }
-       return @results;
-}
-
-sub gd_usage
-{
-       my $self = shift;
-
-       require Text::Wrap;
-       import Text::Wrap;
-
-       my $col = 15;
-
-       my @flags = $self->gd_flags;
-       my @commands = $self->gd_commands;
-       my @positional = $self->gd_positional_more;
-
-       my $summary = "Usage: $self->{gd_progname} ";
-       my $details = '';
-       for my $i (gd_alts(0, @flags)) {
-               $summary .= "[ $i ] ";
-       }
-       $summary .= "{ ";
-       $summary .= join(" | ", gd_alts(0, @commands));
-       $summary .= " } ";
-       $summary .= join(" ", gd_alts(0, @positional));
-
-       my (@all) = (@flags, @commands, @positional);
-       while (@all) {
-               my ($key, $desc) = splice(@all, 0, 2);
-               local($Text::Wrap::columns) = 79;
-               $details .= wrap(
-                       sprintf(" %-${col}s ", $key),
-                       " " x ($col + 2),
-                       $desc);
-               $details .= "\n";
-       }
-
-       print "$summary\n$details";
-       return 0;
-}
-
-sub gd_install_pre {}
-sub gd_install_post {}
-
-sub gd_can_install
-{
-       my $self = shift;
-       require File::Basename;
-       my $basename = File::Basename::basename($0);
-       if (
-               -x "/usr/sbin/update-rc.d"
-               && 
-               -x $0
-               && 
-               $0 !~ m{^(?:/usr|/var)?/tmp/}
-               &&
-               eval { symlink("",""); 1 }
-               && 
-               -d "/etc/init.d"
-               &&
-               ! -e "/etc/init.d/$basename"
-       ) {
-               return sub {
-                       $self->gd_install_pre("update-rc.d");
-                       require Cwd;
-                       my $abs_path = Cwd::abs_path($0);
-                       symlink($abs_path, "/etc/init.d/$basename")
-                               or die "Install failed: symlink /etc/init.d/$basename -> $abs_path: $!\n";
-                       print "+ /usr/sbin/update-rc.d $basename defaults\n";
-                       system("/usr/sbin/update-rc.d", $basename, "defaults");
-                       my $exit = $? >> 8;
-                       $self->gd_install_post("update-rc.d");
-                       exit($exit) if $exit;
-               };
-       }
-
-       return 0;
-}
-
-sub gd_install
-{
-       my $self = shift;
-       my $ifunc = $self->gd_can_install();
-       die "Install command not supported\n" unless $ifunc;
-       &$ifunc($self);
-       exit(0);
-}
-
-sub gd_uninstall_pre {}
-sub gd_uninstall_post {}
-
-sub gd_can_uninstall
-{
-       my $self = shift;
-       require File::Basename;
-       my $basename = File::Basename::basename($0);
-       require Cwd;
-       my $abs_path = Cwd::abs_path($0) || 'no abs path';
-       my $link = readlink("/etc/init.d/$basename") || 'no link';
-       if (
-               $link eq $abs_path
-               && 
-               -x "/usr/sbin/update-rc.d"
-       ) {
-               return sub {
-                       $self->gd_uninstall_pre("update-rc.d");
-                       unlink("/etc/init.d/$basename");
-                       print "+ /usr/sbin/update-rc.d $basename remove\n";
-                       system("/usr/sbin/update-rc.d", $basename, "remove");
-                       my $exit = $? >> 8;
-                       $self->gd_uninstall_post("update-rc.d");
-                       exit($exit) if $exit;
-               }
-       }
-       return 0;
-}
-
-sub gd_uninstall
-{
-       my $self = shift;
-       my $ufunc = $self->gd_can_uninstall();
-       die "Cannot uninstall\n" unless $ufunc;
-       &$ufunc($self);
-       exit(0);
-}
-
-sub gd_kill
-{
-       my ($self, $pid) = @_;
-
-       my $talkmore = 0;
-       my $killed = 0;
-       if (kill(0, $pid)) {
-               $killed = 1;
-               kill(2,$pid);
-               print "Killing $pid\n";
-               my $t = time;
-               sleep(1) if kill(0, $pid);
-               if ($force_quit_delay && kill(0, $pid)) {
-                       print "Waiting for $pid to die...\n";
-                       $talkmore = 1;
-                       while(kill(0, $pid) && time - $t < $force_quit_delay) {
-                               sleep(1);
-                       }
-               }
-               if (kill(15, $pid)) {
-                       print "Killing $pid with -TERM...\n";
-                       if ($force_quit_delay) {
-                               while(kill(0, $pid) && time - $t < $force_quit_delay * 2) {
-                                       sleep(1);
-                               }
-                       } else {
-                               sleep(1) if kill(0, $pid);
-                       }
-               }
-               if (kill(9, $pid)) {
-                       print "Killing $pid with -KILL...\n";
-                       my $k9 = time;
-                       my $max = $force_quit_delay * 4;
-                       $max = 60 if $max < 60;
-                       while(kill(0, $pid)) {
-                               if (time - $k9 > $max) {
-                                       print "Giving up on $pid ever dying.\n";
-                                       exit(1);
-                               }
-                               print "Waiting for $pid to die...\n";
-                               sleep(1);
-                       }
-               }
-               print "Process $pid is gone\n" if $talkmore;
-       } else {
-               print "Process $pid no longer running\n";
-       }
-       return $killed;
-}
-
-sub gd_preconfig { die "gd_preconfig() must be redefined"; }
-
-sub gd_postconfig { }
-
-
-1;
diff --git a/modules/fallback/Daemon/Generic/Event.pm b/modules/fallback/Daemon/Generic/Event.pm
deleted file mode 100644 (file)
index 2279a1e..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-
-# Copyright (C) 2006, David Muir Sharnoff <muir@idiom.com>
-
-package Daemon::Generic::Event;
-
-use strict;
-use warnings;
-require Daemon::Generic;
-require Event;
-require Exporter;
-
-our @ISA = qw(Daemon::Generic Exporter);
-our @EXPORT = @Daemon::Generic::EXPORT;
-our $VERSION = 0.3;
-
-sub newdaemon
-{
-       local($Daemon::Generic::caller) = caller() || 'main';
-       local($Daemon::Generic::package) = __PACKAGE__;
-       Daemon::Generic::newdaemon(@_);
-}
-
-sub gd_setup_signals
-{
-       my $self = shift;
-       my $reload_event = Event->signal(
-               signal  => 'HUP',
-               desc    => 'reload on SIGHUP',
-               prio    => 6,
-               cb      => sub { 
-                       $self->gd_reconfig_event; 
-                       $self->{gd_timer}->cancel()
-                               if $self->{gd_timer};
-                       $self->gd_setup_timer();
-               },
-       );
-       my $quit_event = Event->signal(
-               signal  => 'INT',
-               cb      => sub { $self->gd_quit_event; },
-       );
-}
-
-sub gd_setup_timer
-{
-       my $self = shift;
-       if ($self->can('gd_run_body')) {
-               my $interval = ($self->can('gd_interval') && $self->gd_interval()) || 1;
-               $self->{gd_timer} = Event->timer(
-                       cb              => [ $self, 'gd_run_body' ],
-                       interval        => $interval,
-                       hard            => 0,
-               );
-       }
-}
-
-sub gd_run
-{
-       my $self = shift;
-       $self->gd_setup_timer();
-       Event::loop();
-}
-
-sub gd_quit_event
-{
-       my $self = shift;
-       print STDERR "Quitting...\n";
-       Event::unloop_all();
-}
-
-1;
-
-=head1 NAME
-
- Daemon::Generic::Event - Generic daemon framework with Event.pm
-
-=head1 SYNOPSIS
-
- use Daemon::Generic::Event;
-
- @ISA = qw(Daemon::Generic::Event);
-
- sub gd_preconfig {
-       # stuff
- }
-
-=head1 DESCRIPTION
-
-Daemon::Generic::Event is a subclass of L<Daemon::Generic> that
-predefines some methods:
-
-=over 15
-
-=item gd_run()
-
-Setup a periodic callback to C<gd_run_body()> if there is a C<gd_run_body()>.
-Call C<Event::loop()>.  
-
-=item gd_setup_signals()
-
-Bind SIGHUP to call C<gd_reconfig_event()>. 
-Bind SIGINT to call C<gd_quit_event()>.
-
-=back
-
-To use Daemon::Generic::Event, you have to provide a C<gd_preconfig()>
-method.   It can be empty if you have a C<gd_run_body()>.
-
-Set up your own events in C<gd_preconfig()> and C<gd_postconfig()>.
-
-If you have a C<gd_run_body()> method, it will be called once per
-second or every C<gd_interval()> seconds if you have a C<gd_interval()>
-method.  Unlike in L<Daemon::Generic::While1>, C<gd_run_body()> should
-not include a call to C<sleep()>.
-
-=head1 THANK THE AUTHOR
-
-If you need high-speed internet services (T1, T3, OC3 etc), please 
-send me your request-for-quote.  I have access to very good pricing:
-you'll save money and get a great service.
-
-=head1 LICENSE
-
-Copyright(C) 2006 David Muir Sharnoff <muir@idiom.com>. 
-This module may be used and distributed on the same terms
-as Perl itself.
-
diff --git a/modules/fallback/Daemon/Generic/While1.pm b/modules/fallback/Daemon/Generic/While1.pm
deleted file mode 100644 (file)
index 9c26914..0000000
+++ /dev/null
@@ -1,189 +0,0 @@
-# Copyright (C) 2006, David Muir Sharnoff <muir@idiom.com>
-
-package Daemon::Generic::While1;
-
-use strict;
-use warnings;
-use Carp;
-require Daemon::Generic;
-require POSIX;
-require Exporter;
-
-our @ISA = qw(Daemon::Generic Exporter);
-our @EXPORT = @Daemon::Generic::EXPORT;
-our $VERSION = 0.3;
-
-sub newdaemon
-{
-       local($Daemon::Generic::caller) = caller() || 'main';
-       local($Daemon::Generic::package) = __PACKAGE__;
-       Daemon::Generic::newdaemon(@_);
-}
-
-sub gd_setup_signals
-{
-       my ($self) = @_;
-       $SIG{HUP} = sub {
-               $self->{gd_sighup} = time;
-       };
-       my $child;
-       $SIG{INT} = sub {
-               $self->{gd_sigint} = time;
-               #
-               # We'll be getting a SIGTERM in a bit if we're not dead, so let's use it.
-               #
-               $SIG{TERM} = sub {
-                       $self->gd_quit_event(); 
-                       kill(15, $child) if $child;  # if we're still alive, let's stay that way
-               };
-       };
-}
-
-sub gd_sleep
-{
-       my ($self, $period) = @_;
-       croak "Sleep period must be defined" unless defined $period;
-       my $hires;
-       if ($period*1000 != int($period*1000)) {
-               $hires = 1;
-               require Time::HiRes;
-               import Time::HiRes qw(time sleep);
-       }
-       my $t = time;
-       while (time - $t < $period) {
-               return if $self->{gd_sigint};
-               return if $self->{gd_sighup};
-               if ($hires) {
-                       my $p = (time - $t < 1)
-                               ? time - $t
-                               : 1;
-                       sleep($p);
-               } else {
-                       sleep(1);
-               }
-       }
-}
-
-sub gd_run
-{
-       my ($self) = @_;
-       while(1) {
-               if ($self->{gd_sigint}) {
-                       $self->{gd_sigint} = 0;
-                       $self->gd_quit_event();
-               }
-
-               if ($self->{gd_sighup}) {
-                       $self->{gd_sighup} = 0;
-                       $self->gd_reconfig_event();
-               }
-
-               $self->gd_run_body();
-       }
-}
-
-sub gd_reconfig_event
-{
-       my $self = shift;
-       print STDERR "Reconfiguration requested\n";
-       $self->gd_postconfig($self->gd_preconfig());
-}
-
-sub gd_quit_event
-{
-       print STDERR "Quitting...\n";
-       exit(0);
-}
-
-
-sub gd_run_body { die "must override gd_run_body()" }
-
-1;
-
-=head1 NAME
-
- Daemon::Generic::While1 - Daemon framework with default while(1) loop
-
-=head1 SYNOPSIS
-
- @ISA = qw(Daemon::Generic::While1);
-
- sub gd_run_body {
-       # stuff
- }
-
-=head1 DESCRIPTION
-
-This is a slight variation on L<Daemon::Generic>: a default
-C<gd_run()> provided.  It has a while(1) loop that calls 
-C<gd_run_body()> over and over.  It checks for reconifg and
-and terminate events and only actions them between calls
-to C<gd_run_body()>. 
-
-Terminate events will be forced through after 
-C<$Daemon::Generic::force_quit_delay> seconds if
-C<gd_run_body()> doesn't return quickly enough.
-
-=head1 SUBCLASS METHODS REQUIRD
-
-The following method is required to be overridden to subclass
-Daemon::Generic::While1:
-
-=over 15
-
-=item gd_run_body()
-
-This method will be called over and over.  This method should
-include a call to C<sleep(1)> (or a bit more).  Reconfig events
-will not interrupt it.  Quit events will only interrupt it 
-after 15 seconds.  
-
-=back
-
-=head1 ADDITIONAL METHODS
-
-The following additional methods are available for your use
-(as compared to L<Daemon::Generic>):
-
-=over 15
-
-=item gd_sleep($period)
-
-This will sleep for C<$period> seconds but in one-second
-intervals so that if a SIGINT or SIGHUP arrives the sleep
-period can end more quickly.
-
-Using this makes it safe for C<gd_run_body()> to sleep for
-longer than C<$Daemon::Generic::force_quit_delay> seconds 
-at a time.
-
-=back
-
-=head1 ADDITIONAL MEMBER DATA
-
-The following additional bits of member data are defined:
-
-=over 15
-
-=item gd_sigint
-
-The time at which an (unprocessed) SIGINT was recevied.
-
-=item gd_sighup
-
-The time at which an (unprocessed) SIGHUP was recevied.
-
-=back
-
-=head1 THANK THE AUTHOR
-
-If you need high-speed internet services (T1, T3, OC3 etc), please 
-send me your request-for-quote.  I have access to very good pricing:
-you'll save money and get a great service.
-
-=head1 LICENSE
-
-Copyright(C) 2006 David Muir Sharnoff <muir@idiom.com>. 
-This module may be used and distributed on the same terms
-as Perl itself.
-
diff --git a/modules/fallback/DateTime/Event/Cron.pm b/modules/fallback/DateTime/Event/Cron.pm
deleted file mode 100644 (file)
index a835aa7..0000000
+++ /dev/null
@@ -1,885 +0,0 @@
-package DateTime::Event::Cron;
-
-use 5.006;
-use strict;
-use warnings;
-use Carp;
-
-use vars qw($VERSION);
-
-$VERSION = '0.08';
-
-use constant DEBUG => 0;
-
-use DateTime;
-use DateTime::Set;
-use Set::Crontab;
-
-my %Object_Attributes;
-
-###
-
-sub from_cron {
-  # Return cron as DateTime::Set
-  my $class = shift;
-  my %sparms = @_ == 1 ? (cron => shift) : @_;
-  my %parms;
-  $parms{cron}      = delete $sparms{cron};
-  $parms{user_mode} = delete $sparms{user_mode};
-  $parms{cron} or croak "Cron string parameter required.\n";
-  my $dtc = $class->new(%parms);
-  $dtc->as_set(%sparms);
-}
-
-sub from_crontab {
-  # Return list of DateTime::Sets based on entries from
-  # a crontab file.
-  my $class = shift;
-  my %sparms = @_ == 1 ? (file => shift) : @_;
-  my $file = delete $sparms{file};
-  delete $sparms{cron};
-  my $fh = $class->_prepare_fh($file);
-  my @cronsets;
-  while (<$fh>) {
-    chomp;
-    my $set;
-    eval { $set = $class->from_cron(%sparms, cron => $_) };
-    push(@cronsets, $set) if ref $set && !$@;
-  }
-  @cronsets;
-}
-
-sub as_set {
-  # Return self as DateTime::Set
-  my $self = shift;
-  my %sparms = @_;
-  Carp::cluck "Recurrence callbacks overriden by ". ref $self . "\n"
-    if $sparms{next} || $sparms{recurrence} || $sparms{previous};
-  delete $sparms{next};
-  delete $sparms{previous};
-  delete $sparms{recurrence};
-  $sparms{next}     = sub { $self->next(@_) };
-  $sparms{previous} = sub { $self->previous(@_) };
-  DateTime::Set->from_recurrence(%sparms);
-}
-
-###
-
-sub new {
-  my $class = shift;
-  my $self = {};
-  bless $self, $class;
-  my %parms = @_ == 1 ? (cron => shift) : @_;
-  my $crontab = $self->_make_cronset(%parms);
-  $self->_cronset($crontab);
-  $self;
-}
-
-sub new_from_cron { new(@_) }
-
-sub new_from_crontab {
-  my $class = shift;
-  my %parms = @_ == 1 ? (file => shift()) : @_;
-  my $fh = $class->_prepare_fh($parms{file});
-  delete $parms{file};
-  my @dtcrons;
-  while (<$fh>) {
-    my $dtc;
-    eval { $dtc = $class->new(%parms, cron => $_) };
-    if (ref $dtc && !$@) {
-      push(@dtcrons, $dtc);
-      $parms{user_mode} = 1 if defined $dtc->user;
-    }
-  }
-  @dtcrons;
-}
-
-###
-
-sub _prepare_fh {
-  my $class = shift;
-  my $fh = shift;
-  if (! ref $fh) {
-    my $file = $fh;
-    local(*FH);
-    $fh = do { local *FH; *FH }; # doubled *FH avoids warning
-    open($fh, "<$file")
-      or croak "Error opening $file for reading\n";
-  }
-  $fh;
-}
-
-###
-
-sub valid {
-  # Is the given date valid according the current cron settings?
-  my($self, $date) = @_;
-  return if !$date || $date->second;
-  $self->minute->contains($date->minute)      &&
-  $self->hour->contains($date->hour)          &&
-  $self->days_contain($date->day, $date->dow) &&
-  $self->month->contains($date->month);
-}
-
-sub match {
-  # Does the given date match the cron spec?
-  my($self, $date) = @_;
-  $date = DateTime->now unless $date;
-  $self->minute->contains($date->minute)      &&
-  $self->hour->contains($date->hour)          &&
-  $self->days_contain($date->day, $date->dow) &&
-  $self->month->contains($date->month);
-}
-
-### Return adjacent dates without altering original date
-
-sub next {
-  my($self, $date) = @_;
-  $date = DateTime->now unless $date;
-  $self->increment($date->clone);
-}
-
-sub previous {
-  my($self, $date) = @_;
-  $date = DateTime->now unless $date;
-  $self->decrement($date->clone);
-}
-
-### Change given date to adjacent dates
-
-sub increment {
-  my($self, $date) = @_;
-  $date = DateTime->now unless $date;
-  return $date if $date->is_infinite;
-  do {
-    $self->_attempt_increment($date);
-  } until $self->valid($date);
-  $date;
-}
-
-sub decrement {
-  my($self, $date) = @_;
-  $date = DateTime->now unless $date;
-  return $date if $date->is_infinite;
-  do {
-    $self->_attempt_decrement($date);
-  } until $self->valid($date);
-  $date;
-}
-
-###
-
-sub _attempt_increment {
-  my($self, $date) = @_;
-  ref $date or croak "Reference to datetime object reqired\n";
-  $self->valid($date) ?
-    $self->_valid_incr($date) :
-    $self->_invalid_incr($date);
-}
-
-sub _attempt_decrement {
-  my($self, $date) = @_;
-  ref $date or croak "Reference to datetime object reqired\n";
-  $self->valid($date) ?
-    $self->_valid_decr($date) :
-    $self->_invalid_decr($date);
-}
-
-sub _valid_incr { shift->_minute_incr(@_) }
-
-sub _valid_decr { shift->_minute_decr(@_) }
-
-sub _invalid_incr {
-  # If provided date is valid, return it. Otherwise return
-  # nearest valid date after provided date.
-  my($self, $date) = @_;
-  ref $date or croak "Reference to datetime object reqired\n";
-
-  print STDERR "\nI GOT: ", $date->datetime, "\n" if DEBUG;
-
-  $date->truncate(to => 'minute')->add(minutes => 1)
-    if $date->second;
-
-  print STDERR "RND: ", $date->datetime, "\n" if DEBUG;
-
-  # Find our greatest invalid unit and clip
-  if (!$self->month->contains($date->month)) {
-    $date->truncate(to => 'month');
-  }
-  elsif (!$self->days_contain($date->day, $date->dow)) {
-    $date->truncate(to => 'day');
-  }
-  elsif (!$self->hour->contains($date->hour)) {
-    $date->truncate(to => 'hour');
-  }
-  else {
-    $date->truncate(to => 'minute');
-  }
-
-  print STDERR "BBT: ", $date->datetime, "\n" if DEBUG;
-
-  return $date if $self->valid($date);
-
-  print STDERR "ZZT: ", $date->datetime, "\n" if DEBUG;
-
-  # Extraneous durations clipped. Start searching.
-  while (!$self->valid($date)) {
-    $date->add(months => 1) until $self->month->contains($date->month);
-    print STDERR "MON: ", $date->datetime, "\n" if DEBUG;
-
-    my $day_orig = $date->day;
-    $date->add(days => 1) until $self->days_contain($date->day, $date->dow);
-    $date->truncate(to => 'month') && next if $date->day < $day_orig;
-    print STDERR "DAY: ", $date->datetime, "\n" if DEBUG;
-
-    my $hour_orig = $date->hour;
-    $date->add(hours => 1) until $self->hour->contains($date->hour);
-    $date->truncate(to => 'day') && next if $date->hour < $hour_orig;
-    print STDERR "HOR: ", $date->datetime, "\n" if DEBUG;
-
-    my $min_orig = $date->minute;
-    $date->add(minutes => 1) until $self->minute->contains($date->minute);
-    $date->truncate(to => 'hour') && next if $date->minute < $min_orig;
-    print STDERR "MIN: ", $date->datetime, "\n" if DEBUG;
-  }
-  print STDERR "SET: ", $date->datetime, "\n" if DEBUG;
-  $date;
-}
-
-sub _invalid_decr {
-  # If provided date is valid, return it. Otherwise
-  # return the nearest previous valid date.
-  my($self, $date) = @_;
-  ref $date or croak "Reference to datetime object reqired\n";
-
-  print STDERR "\nD GOT: ", $date->datetime, "\n" if DEBUG;
-
-  if (!$self->month->contains($date->month)) {
-    $date->truncate(to => 'month');
-  }
-  elsif (!$self->days_contain($date->day, $date->dow)) {
-    $date->truncate(to => 'day');
-  }
-  elsif (!$self->hour->contains($date->hour)) {
-    $date->truncate(to => 'hour');
-  }
-  else {
-    $date->truncate(to => 'minute');
-  }
-
-  print STDERR "BBT: ", $date->datetime, "\n" if DEBUG;
-
-  return $date if $self->valid($date);
-
-  print STDERR "ZZT: ", $date->datetime, "\n" if DEBUG;
-
-  # Extraneous durations clipped. Start searching.
-  while (!$self->valid($date)) {
-    if (!$self->month->contains($date->month)) {
-      $date->subtract(months => 1) until $self->month->contains($date->month);
-      $self->_unit_peak($date, 'month');
-      print STDERR "MON: ", $date->datetime, "\n" if DEBUG;
-    }
-    if (!$self->days_contain($date->day, $date->dow)) {
-      my $day_orig = $date->day;
-      $date->subtract(days => 1)
-        until $self->days_contain($date->day, $date->dow);
-      $self->_unit_peak($date, 'month') && next if ($date->day > $day_orig);
-      $self->_unit_peak($date, 'day');
-      print STDERR "DAY: ", $date->datetime, "\n" if DEBUG;
-    }
-    if (!$self->hour->contains($date->hour)) {
-      my $hour_orig = $date->hour;
-      $date->subtract(hours => 1) until $self->hour->contains($date->hour);
-      $self->_unit_peak($date, 'day') && next if ($date->hour > $hour_orig);
-      $self->_unit_peak($date, 'hour');
-      print STDERR "HOR: ", $date->datetime, "\n" if DEBUG;
-    }
-    if (!$self->minute->contains($date->minute)) {
-      my $min_orig = $date->minute;
-      $date->subtract(minutes => 1)
-        until $self->minute->contains($date->minute);
-      $self->_unit_peak($date, 'hour') && next if ($date->minute > $min_orig);
-      print STDERR "MIN: ", $date->datetime, "\n" if DEBUG;
-    }
-  }
-  print STDERR "SET: ", $date->datetime, "\n" if DEBUG;
-  $date;
-}
-
-###
-
-sub _unit_peak {
-  my($self, $date, $unit) = @_;
-  $date && $unit or croak "DateTime ref and unit required.\n";
-  $date->truncate(to => $unit)
-       ->add($unit . 's' => 1)
-       ->subtract(minutes => 1);
-}
-
-### Unit cascades
-
-sub _minute_incr {
-  my($self, $date) = @_;
-  croak "datetime object required\n" unless $date;
-  my $cur = $date->minute;
-  my $next = $self->minute->next($cur);
-  $date->set(minute => $next);
-  $next <= $cur ? $self->_hour_incr($date) : $date;
-}
-
-sub _hour_incr {
-  my($self, $date) = @_;
-  croak "datetime object required\n" unless $date;
-  my $cur = $date->hour;
-  my $next = $self->hour->next($cur);
-  $date->set(hour => $next);
-  $next <= $cur ? $self->_day_incr($date) : $date;
-}
-
-sub _day_incr {
-  my($self, $date) = @_;
-  croak "datetime object required\n" unless $date;
-  $date->add(days => 1);
-  $self->_invalid_incr($date);
-}
-
-sub _minute_decr {
-  my($self, $date) = @_;
-  croak "datetime object required\n" unless $date;
-  my $cur = $date->minute;
-  my $next = $self->minute->previous($cur);
-  $date->set(minute => $next);
-  $next >= $cur ? $self->_hour_decr($date) : $date;
-}
-
-sub _hour_decr {
-  my($self, $date) = @_;
-  croak "datetime object required\n" unless $date;
-  my $cur = $date->hour;
-  my $next = $self->hour->previous($cur);
-  $date->set(hour => $next);
-  $next >= $cur ? $self->_day_decr($date) : $date;
-}
-
-sub _day_decr {
-  my($self, $date) = @_;
-  croak "datetime object required\n" unless $date;
-  $date->subtract(days => 1);
-  $self->_invalid_decr($date);
-}
-
-### Factories
-
-sub _make_cronset { shift; DateTime::Event::Cron::IntegratedSet->new(@_) }
-
-### Shortcuts
-
-sub days_contain { shift->_cronset->days_contain(@_) }
-
-sub minute   { shift->_cronset->minute  }
-sub hour     { shift->_cronset->hour    }
-sub day      { shift->_cronset->day     }
-sub month    { shift->_cronset->month   }
-sub dow      { shift->_cronset->dow     }
-sub user     { shift->_cronset->user    }
-sub command  { shift->_cronset->command }
-sub original { shift->_cronset->original }
-
-### Static acessors/mutators
-
-sub _cronset { shift->_attr('cronset', @_) }
-
-sub _attr {
-  my $self = shift;
-  my $name = shift;
-  if (@_) {
-    $Object_Attributes{$self}{$name} = shift;
-  }
-  $Object_Attributes{$self}{$name};
-}
-
-### debugging
-
-sub _dump_sets {
-  my($self, $date) = @_;
-  foreach (qw(minute hour day month dow)) {
-    print STDERR "$_: ", join(',',$self->$_->list), "\n";
-  }
-  if (ref $date) {
-    $date = $date->clone;
-    my @mod;
-    my $mon = $date->month;
-    $date->truncate(to => 'month');
-    while ($date->month == $mon) {
-      push(@mod, $date->day) if $self->days_contain($date->day, $date->dow);
-      $date->add(days => 1);
-    }
-    print STDERR "mod for month($mon): ", join(',', @mod), "\n";
-  }
-  print STDERR "day_squelch: ", $self->_cronset->day_squelch, " ",
-               "dow_squelch: ", $self->_cronset->dow_squelch, "\n";
-  $self;
-}
-
-###
-
-sub DESTROY { delete $Object_Attributes{shift()} }
-
-##########
-
-{
-
-package DateTime::Event::Cron::IntegratedSet;
-
-# IntegratedSet manages the collection of field sets for
-# each cron entry, including sanity checks. Individual
-# field sets are accessed through their respective names,
-# i.e., minute hour day month dow.
-#
-# Also implements some merged field logic for day/dow
-# interactions.
-
-use strict;
-use Carp;
-
-my %Range = (
-  minute => [0..59],
-  hour   => [0..23],
-  day    => [1..31],
-  month  => [1..12],
-  dow    => [1..7],
-);
-
-my @Month_Max = qw( 31 29 31 30 31 30 31 31 30 31 30 31 );
-
-my %Object_Attributes;
-
-sub new {
-  my $self = [];
-  bless $self, shift;
-  $self->_range(\%Range);
-  $self->set_cron(@_);
-  $self;
-}
-
-sub set_cron {
-  # Initialize
-  my $self = shift;
-  my %parms = @_;
-  my $cron = $parms{cron};
-  my $user_mode = $parms{user_mode};
-  defined $cron or croak "Cron entry fields required\n";
-  $self->_attr('original', $cron);
-  my @line;
-  if (ref $cron) {
-    @line = grep(!/^\s*$/, @$cron);
-  }
-  else {
-    $cron =~ s/^\s+//;
-    $cron =~ s/\s+$//;
-    @line = split(/\s+/, $cron);
-  }
-  @line >= 5 or croak "At least five cron entry fields required.\n";
-  my @entry = splice(@line, 0, 5);
-  my($user, $command);
-  unless (defined $user_mode) {
-    # auto-detect
-    if (@line > 1 && $line[0] =~ /^\w+$/) {
-      $user_mode = 1;
-    }
-  }
-  $user = shift @line if $user_mode;
-  $command = join(' ', @line);
-  $self->_attr('command', $command);
-  $self->_attr('user', $user);
-  my $i = 0;
-  foreach my $name (qw( minute hour day month dow )) {
-    $self->_attr($name, $self->make_valid_set($name, $entry[$i]));
-    ++$i;
-  }
-  my @day_list  = $self->day->list;
-  my @dow_list  = $self->dow->list;
-  my $day_range = $self->range('day');
-  my $dow_range = $self->range('dow');
-  $self->day_squelch(scalar @day_list == scalar @$day_range &&
-                     scalar @dow_list != scalar @$dow_range ? 1 : 0);
-  $self->dow_squelch(scalar @dow_list == scalar @$dow_range &&
-                     scalar @day_list != scalar @$day_range ? 1 : 0);
-  unless ($self->day_squelch) {
-    my @days = $self->day->list;
-    my $pass = 0;
-    MONTH: foreach my $month ($self->month->list) {
-      foreach (@days) {
-        ++$pass && last MONTH if $_ <= $Month_Max[$month - 1];
-      }
-    }
-    croak "Impossible last day for provided months.\n" unless $pass;
-  }
-  $self;
-}
-
-# Field range queries
-sub range {
-  my($self, $name) = @_;
-  my $val = $self->_range->{$name} or croak "Unknown field '$name'\n";
-  $val;
-}
-
-# Perform sanity checks when setting up each field set.
-sub make_valid_set {
-  my($self, $name, $str) = @_;
-  my $range = $self->range($name);
-  my $set = $self->make_set($str, $range);
-  my @list = $set->list;
-  croak "Malformed cron field '$str'\n" unless @list;
-  croak "Field value ($list[-1]) out of range ($range->[0]-$range->[-1])\n"
-    if $list[-1] > $range->[-1];
-  if ($name eq 'dow' && $set->contains(0)) {
-    shift(@list);
-    push(@list, 7) unless $set->contains(7);
-    $set = $self->make_set(join(',',@list), $range);
-  }
-  croak "Field value ($list[0]) out of range ($range->[0]-$range->[-1])\n"
-    if $list[0] < $range->[0];
-  $set;
-}
-
-# No sanity checks
-sub make_set { shift; DateTime::Event::Cron::OrderedSet->new(@_) }
-
-# Flags for when day/dow are applied.
-sub day_squelch { shift->_attr('day_squelch', @_ ) }
-sub dow_squelch { shift->_attr('dow_squelch', @_ ) }
-
-# Merged logic for day/dow
-sub days_contain {
-  my($self, $day, $dow) = @_;
-  defined $day && defined $dow
-    or croak "Day of month and day of week required.\n";
-  my $day_c = $self->day->contains($day);
-  my $dow_c = $self->dow->contains($dow);
-  return $dow_c if $self->day_squelch;
-  return $day_c if $self->dow_squelch;
-  $day_c || $dow_c;
-}
-
-# Set Accessors
-sub minute   { shift->_attr('minute' ) }
-sub hour     { shift->_attr('hour'   ) }
-sub day      { shift->_attr('day'    ) }
-sub month    { shift->_attr('month'  ) }
-sub dow      { shift->_attr('dow'    ) }
-sub user     { shift->_attr('user'   ) }
-sub command  { shift->_attr('command') }
-sub original { shift->_attr('original') }
-
-# Accessors/mutators
-sub _range       { shift->_attr('range', @_) }
-
-sub _attr {
-  my $self = shift;
-  my $name = shift;
-  if (@_) {
-    $Object_Attributes{$self}{$name} = shift;
-  }
-  $Object_Attributes{$self}{$name};
-}
-
-sub DESTROY { delete $Object_Attributes{shift()} }
-
-}
-
-##########
-
-{
-
-package DateTime::Event::Cron::OrderedSet;
-
-# Extends Set::Crontab with some progression logic (next/prev)
-
-use strict;
-use Carp;
-use base 'Set::Crontab';
-
-my %Object_Attributes;
-
-sub new {
-  my $class = shift;
-  my($string, $range) = @_;
-  defined $string && ref $range
-    or croak "Cron field and range ref required.\n";
-  my $self = Set::Crontab->new($string, $range);
-  bless $self, $class;
-  my @list = $self->list;
-  my(%next, %prev);
-  foreach (0 .. $#list) {
-    $next{$list[$_]} = $list[($_+1)%@list];
-    $prev{$list[$_]} = $list[($_-1)%@list];
-  }
-  $self->_attr('next', \%next);
-  $self->_attr('previous', \%prev);
-  $self;
-}
-
-sub next {
-  my($self, $entry) = @_;
-  my $hash = $self->_attr('next');
-  croak "Missing entry($entry) in set\n" unless exists $hash->{$entry};
-  my $next = $hash->{$entry};
-  wantarray ? ($next, $next <= $entry) : $next;
-}
-
-sub previous {
-  my($self, $entry) = @_;
-  my $hash = $self->_attr('previous');
-  croak "Missing entry($entry) in set\n" unless exists $hash->{$entry};
-  my $prev = $hash->{$entry};
-  wantarray ? ($prev, $prev >= $entry) : $prev;
-}
-
-sub _attr {
-  my $self = shift;
-  my $name = shift;
-  if (@_) {
-    $Object_Attributes{$self}{$name} = shift;
-  }
-  $Object_Attributes{$self}{$name};
-}
-
-sub DESTROY { delete $Object_Attributes{shift()} }
-
-}
-
-###
-
-1;
-
-__END__
-
-=head1 NAME
-
-DateTime::Event::Cron - DateTime extension for generating recurrence
-sets from crontab lines and files.
-
-=head1 SYNOPSIS
-
-  use DateTime::Event::Cron;
-
-  # check if a date matches (defaults to current time)
-  my $c = DateTime::Event::Cron->new('* 2 * * *');
-  if ($c->match) {
-    # do stuff
-  }
-  if ($c->match($date)) {
-    # do something else for datetime $date
-  }
-
-  # DateTime::Set construction from crontab line
-  $crontab = '*/3 15 1-10 3,4,5 */2';
-  $set = DateTime::Event::Cron->from_cron($crontab);
-  $iter = $set->iterator(after => DateTime->now);
-  while (1) {
-    my $next = $iter->next;
-    my $now  = DateTime->now;
-    sleep(($next->subtract_datetime_absolute($now))->seconds);
-    # do stuff...
-  }
-
-  # List of DateTime::Set objects from crontab file
-  @sets = DateTime::Event::Cron->from_crontab(file => '/etc/crontab');
-  $now = DateTime->now;
-  print "Now: ", $now->datetime, "\n";
-  foreach (@sets) {
-    my $next = $_->next($now);
-    print $next->datetime, "\n";
-  }
-
-  # DateTime::Set parameters
-  $crontab = '* * * * *';
-
-  $now = DateTime->now;
-  %set_parms = ( after => $now );
-  $set = DateTime::Event::Cron->from_cron(cron => $crontab, %set_parms);
-  $dt = $set->next;
-  print "Now: ", $now->datetime, " and next: ", $dt->datetime, "\n";
-
-  # Spans for DateTime::Set
-  $crontab = '* * * * *';
-  $now = DateTime->now;
-  $now2 = $now->clone;
-  $span = DateTime::Span->from_datetimes(
-            start => $now->add(minutes => 1),
-           end   => $now2->add(hours => 1),
-         );
-  %parms = (cron => $crontab, span => $span);
-  $set = DateTime::Event::Cron->from_cron(%parms);
-  # ...do things with the DateTime::Set
-
-  # Every RTFCT relative to 12am Jan 1st this year
-  $crontab = '7-10 6,12-15 10-28/2 */3 3,4,5';
-  $date = DateTime->now->truncate(to => 'year');
-  $set = DateTime::Event::Cron->from_cron(cron => $crontab, after => $date);
-
-  # Rather than generating DateTime::Set objects, next/prev
-  # calculations can be made directly:
-
-  # Every day at 10am, 2pm, and 6pm. Reference date
-  # defaults to DateTime->now.
-  $crontab = '10,14,18 * * * *';
-  $dtc = DateTime::Event::Cron->new_from_cron(cron => $crontab);
-  $next_datetime = $dtc->next;
-  $last_datetime = $dtc->previous;
-  ...
-
-  # List of DateTime::Event::Cron objects from
-  # crontab file
-  @dtc = DateTime::Event::Cron->new_from_crontab(file => '/etc/crontab');
-
-  # Full cron lines with user, such as from /etc/crontab
-  # or files in /etc/cron.d, are supported and auto-detected:
-  $crontab = '* * * * * gump /bin/date';
-  $dtc = DateTime::Event::Cron->new(cron => $crontab);
-
-  # Auto-detection of users is disabled if you explicitly
-  # enable/disable via the user_mode parameter:
-  $dtc = DateTime::Event::Cron->new(cron => $crontab, user_mode => 1);
-  my $user = $dtc->user;
-  my $command = $dtc->command;
-
-  # Unparsed original cron entry
-  my $original = $dtc->original;
-
-=head1 DESCRIPTION
-
-DateTime::Event::Cron generated DateTime events or DateTime::Set objects
-based on crontab-style entries.
-
-=head1 METHODS
-
-The cron fields are typical crontab-style entries. For more information,
-see L<crontab(5)> and extensions described in L<Set::Crontab>. The
-fields can be passed as a single string or as a reference to an array
-containing each field. Only the first five fields are retained.
-
-=head2 DateTime::Set Factories
-
-See L<DateTime::Set> for methods provided by Set objects, such as
-C<next()> and C<previous()>.
-
-=over 4
-
-=item from_cron($cronline)
-
-=item from_cron(cron => $cronline, %parms, %set_parms)
-
-Generates a DateTime::Set recurrence for the cron line provided. See
-new() for details on %parms. Optionally takes parameters for
-DateTime::Set.
-
-=item from_crontab(file => $crontab_fh, %parms, %set_parms)
-
-Returns a list of DateTime::Set recurrences based on lines from a
-crontab file. C<$crontab_fh> can be either a filename or filehandle
-reference. See new() for details on %parm. Optionally takes parameters
-for DateTime::Set which will be passed along to each set for each line.
-
-=item as_set(%set_parms)
-
-Generates a DateTime::Set recurrence from an existing
-DateTime::Event::Cron object.
-
-=back
-
-=head2 Constructors
-
-=over 4
-
-=item new_from_cron(cron => $cronstring, %parms)
-
-Returns a DateTime::Event::Cron object based on the cron specification.
-Optional parameters include the boolean 'user_mode' which indicates that
-the crontab entry includes a username column before the command.
-
-=item new_from_crontab(file => $fh, %parms)
-
-Returns a list of DateTime::Event::Cron objects based on the lines of a
-crontab file. C<$fh> can be either a filename or a filehandle reference.
-Optional parameters include the boolean 'user_mode' as mentioned above.
-
-=back
-
-=head2 Other methods
-
-=over 4
-
-=item next()
-
-=item next($date)
-
-Returns the next valid datetime according to the cron specification.
-C<$date> defaults to DateTime->now unless provided.
-
-=item previous()
-
-=item previous($date)
-
-Returns the previous valid datetime according to the cron specification.
-C<$date> defaults to DateTime->now unless provided.
-
-=item increment($date)
-
-=item decrement($date)
-
-Same as C<next()> and C<previous()> except that the provided datetime is
-modified to the new datetime.
-
-=item match($date)
-
-Returns whether or not the given datetime (defaults to current time)
-matches the current cron specification. Dates are truncated to minute
-resolution.
-
-=item valid($date)
-
-A more strict version of match(). Returns whether the given datetime is
-valid under the current cron specification. Cron dates are only accurate
-to the minute -- datetimes with seconds greater than 0 are invalid by
-default. (note: never fear, all methods accepting dates will accept
-invalid dates -- they will simply be rounded to the next nearest valid
-date in all cases except this particular method)
-
-=item command()
-
-Returns the command string, if any, from the original crontab entry.
-Currently no expansion is performed such as resolving environment
-variables, etc.
-
-=item user()
-
-Returns the username under which this cron command was to be executed,
-assuming such a field was present in the original cron entry.
-
-=item original()
-
-Returns the original, unparsed cron string including any user or
-command fields.
-
-=back
-
-=head1 AUTHOR
-
-Matthew P. Sisk E<lt>sisk@mojotoad.comE<gt>
-
-=head1 COPYRIGHT
-
-Copyright (c) 2003 Matthew P. Sisk. All rights reserved. All wrongs
-revenged. This program is free software; you can distribute it and/or
-modify it under the same terms as Perl itself.
-
-=head1 SEE ALSO
-
-DateTime(3), DateTime::Set(3), DateTime::Event::Recurrence(3),
-DateTime::Event::ICal(3), DateTime::Span(3), Set::Crontab(3), crontab(5)
-
-=cut
diff --git a/modules/fallback/DateTime/Set.pm b/modules/fallback/DateTime/Set.pm
deleted file mode 100644 (file)
index 05fac96..0000000
+++ /dev/null
@@ -1,1149 +0,0 @@
-
-package DateTime::Set;
-
-use strict;
-use Carp;
-use Params::Validate qw( validate SCALAR BOOLEAN OBJECT CODEREF ARRAYREF );
-use DateTime 0.12;  # this is for version checking only
-use DateTime::Duration;
-use DateTime::Span;
-use Set::Infinite 0.59;
-use Set::Infinite::_recurrence;
-
-use vars qw( $VERSION );
-
-use constant INFINITY     =>       100 ** 100 ** 100 ;
-use constant NEG_INFINITY => -1 * (100 ** 100 ** 100);
-
-BEGIN {
-    $VERSION = '0.28';
-}
-
-
-sub _fix_datetime {
-    # internal function -
-    # (not a class method)
-    #
-    # checks that the parameter is an object, and
-    # also protects the object against mutation
-    
-    return $_[0]
-        unless defined $_[0];      # error
-    return $_[0]->clone
-        if ref( $_[0] );           # "immutable" datetime
-    return DateTime::Infinite::Future->new 
-        if $_[0] == INFINITY;      # Inf
-    return DateTime::Infinite::Past->new
-        if $_[0] == NEG_INFINITY;  # -Inf
-    return $_[0];                  # error
-}
-
-sub _fix_return_datetime {
-    my ( $dt, $dt_arg ) = @_;
-
-    # internal function -
-    # (not a class method)
-    #
-    # checks that the returned datetime has the same
-    # time zone as the parameter
-
-    # TODO: set locale
-
-    return unless $dt;
-    return unless $dt_arg;
-    if ( $dt_arg->can('time_zone_long_name') &&
-         !( $dt_arg->time_zone_long_name eq 'floating' ) )
-    {
-        $dt->set_time_zone( $dt_arg->time_zone );
-    }
-    return $dt;
-}
-
-sub iterate {
-    # deprecated method - use map() or grep() instead
-    my ( $self, $callback ) = @_;
-    my $class = ref( $self );
-    my $return = $class->empty_set;
-    $return->{set} = $self->{set}->iterate( 
-        sub {
-            my $min = $_[0]->min;
-            $callback->( $min->clone ) if ref($min);
-        }
-    );
-    $return;
-}
-
-sub map {
-    my ( $self, $callback ) = @_;
-    my $class = ref( $self );
-    die "The callback parameter to map() must be a subroutine reference"
-        unless ref( $callback ) eq 'CODE';
-    my $return = $class->empty_set;
-    $return->{set} = $self->{set}->iterate( 
-        sub {
-            local $_ = $_[0]->min;
-            next unless ref( $_ );
-            $_ = $_->clone;
-            my @list = $callback->();
-            my $set = Set::Infinite::_recurrence->new();
-            $set = $set->union( $_ ) for @list;
-            return $set;
-        }
-    );
-    $return;
-}
-
-sub grep {
-    my ( $self, $callback ) = @_;
-    my $class = ref( $self );
-    die "The callback parameter to grep() must be a subroutine reference"
-        unless ref( $callback ) eq 'CODE';
-    my $return = $class->empty_set;
-    $return->{set} = $self->{set}->iterate( 
-        sub {
-            local $_ = $_[0]->min;
-            next unless ref( $_ );
-            $_ = $_->clone;
-            my $result = $callback->();
-            return $_ if $result;
-            return;
-        }
-    );
-    $return;
-}
-
-sub add { return shift->add_duration( DateTime::Duration->new(@_) ) }
-
-sub subtract { return shift->subtract_duration( DateTime::Duration->new(@_) ) }
-
-sub subtract_duration { return $_[0]->add_duration( $_[1]->inverse ) }
-
-sub add_duration {
-    my ( $self, $dur ) = @_;
-    $dur = $dur->clone;  # $dur must be "immutable"
-
-    $self->{set} = $self->{set}->iterate(
-        sub {
-            my $min = $_[0]->min;
-            $min->clone->add_duration( $dur ) if ref($min);
-        },
-        backtrack_callback => sub { 
-            my ( $min, $max ) = ( $_[0]->min, $_[0]->max );
-            if ( ref($min) )
-            {
-                $min = $min->clone;
-                $min->subtract_duration( $dur );
-            }
-            if ( ref($max) )
-            {
-                $max = $max->clone;
-                $max->subtract_duration( $dur );
-            }
-            return Set::Infinite::_recurrence->new( $min, $max );
-        },
-    );
-    $self;
-}
-
-sub set_time_zone {
-    my ( $self, $tz ) = @_;
-
-    $self->{set} = $self->{set}->iterate(
-        sub {
-            my $min = $_[0]->min;
-            $min->clone->set_time_zone( $tz ) if ref($min);
-        },
-        backtrack_callback => sub {
-            my ( $min, $max ) = ( $_[0]->min, $_[0]->max );
-            if ( ref($min) )
-            {
-                $min = $min->clone;
-                $min->set_time_zone( $tz );
-            }
-            if ( ref($max) )
-            {
-                $max = $max->clone;
-                $max->set_time_zone( $tz );
-            }
-            return Set::Infinite::_recurrence->new( $min, $max );
-        },
-    );
-    $self;
-}
-
-sub set {
-    my $self = shift;
-    my %args = validate( @_,
-                         { locale => { type => SCALAR | OBJECT,
-                                       default => undef },
-                         }
-                       );
-    $self->{set} = $self->{set}->iterate( 
-        sub {
-            my $min = $_[0]->min;
-            $min->clone->set( %args ) if ref($min);
-        },
-    );
-    $self;
-}
-
-sub from_recurrence {
-    my $class = shift;
-
-    my %args = @_;
-    my %param;
-    
-    # Parameter renaming, such that we can use either
-    #   recurrence => xxx   or   next => xxx, previous => xxx
-    $param{next} = delete $args{recurrence} || delete $args{next};
-    $param{previous} = delete $args{previous};
-
-    $param{span} = delete $args{span};
-    # they might be specifying a span using begin / end
-    $param{span} = DateTime::Span->new( %args ) if keys %args;
-
-    my $self = {};
-    
-    die "Not enough arguments in from_recurrence()"
-        unless $param{next} || $param{previous}; 
-
-    if ( ! $param{previous} ) 
-    {
-        my $data = {};
-        $param{previous} =
-                sub {
-                    _callback_previous ( _fix_datetime( $_[0] ), $param{next}, $data );
-                }
-    }
-    else
-    {
-        my $previous = $param{previous};
-        $param{previous} =
-                sub {
-                    $previous->( _fix_datetime( $_[0] ) );
-                }
-    }
-
-    if ( ! $param{next} ) 
-    {
-        my $data = {};
-        $param{next} =
-                sub {
-                    _callback_next ( _fix_datetime( $_[0] ), $param{previous}, $data );
-                }
-    }
-    else
-    {
-        my $next = $param{next};
-        $param{next} =
-                sub {
-                    $next->( _fix_datetime( $_[0] ) );
-                }
-    }
-
-    my ( $min, $max );
-    $max = $param{previous}->( DateTime::Infinite::Future->new );
-    $min = $param{next}->( DateTime::Infinite::Past->new );
-    $max = INFINITY if $max->is_infinite;
-    $min = NEG_INFINITY if $min->is_infinite;
-        
-    my $base_set = Set::Infinite::_recurrence->new( $min, $max );
-    $base_set = $base_set->intersection( $param{span}->{set} )
-         if $param{span};
-         
-    # warn "base set is $base_set\n";
-
-    my $data = {};
-    $self->{set} = 
-            $base_set->_recurrence(
-                $param{next}, 
-                $param{previous},
-                $data,
-        );
-    bless $self, $class;
-    
-    return $self;
-}
-
-sub from_datetimes {
-    my $class = shift;
-    my %args = validate( @_,
-                         { dates => 
-                           { type => ARRAYREF,
-                           },
-                         }
-                       );
-    my $self = {};
-    $self->{set} = Set::Infinite::_recurrence->new;
-    # possible optimization: sort datetimes and use "push"
-    for( @{ $args{dates} } ) 
-    {
-        # DateTime::Infinite objects are not welcome here,
-        # but this is not enforced (it does't hurt)
-
-        carp "The 'dates' argument to from_datetimes() must only contain ".
-             "datetime objects"
-            unless UNIVERSAL::can( $_, 'utc_rd_values' );
-
-        $self->{set} = $self->{set}->union( $_->clone );
-    }
-
-    bless $self, $class;
-    return $self;
-}
-
-sub empty_set {
-    my $class = shift;
-
-    return bless { set => Set::Infinite::_recurrence->new }, $class;
-}
-
-sub clone { 
-    my $self = bless { %{ $_[0] } }, ref $_[0];
-    $self->{set} = $_[0]->{set}->copy;
-    return $self;
-}
-
-# default callback that returns the 
-# "previous" value in a callback recurrence.
-#
-# This is used to simulate a 'previous' callback,
-# when then 'previous' argument in 'from_recurrence' is missing.
-#
-sub _callback_previous {
-    my ($value, $callback_next, $callback_info) = @_; 
-    my $previous = $value->clone;
-
-    return $value if $value->is_infinite;
-
-    my $freq = $callback_info->{freq};
-    unless (defined $freq) 
-    { 
-        # This is called just once, to setup the recurrence frequency
-        my $previous = $callback_next->( $value );
-        my $next =     $callback_next->( $previous );
-        $freq = 2 * ( $previous - $next );
-        # save it for future use with this same recurrence
-        $callback_info->{freq} = $freq;
-    }
-
-    $previous->add_duration( $freq );  
-    $previous = $callback_next->( $previous );
-    if ($previous >= $value) 
-    {
-        # This error happens if the event frequency oscilates widely
-        # (more than 100% of difference from one interval to next)
-        my @freq = $freq->deltas;
-        print STDERR "_callback_previous: Delta components are: @freq\n";
-        warn "_callback_previous: iterator can't find a previous value, got ".
-            $previous->ymd." after ".$value->ymd;
-    }
-    my $previous1;
-    while (1) 
-    {
-        $previous1 = $previous->clone;
-        $previous = $callback_next->( $previous );
-        return $previous1 if $previous >= $value;
-    }
-}
-
-# default callback that returns the 
-# "next" value in a callback recurrence.
-#
-# This is used to simulate a 'next' callback,
-# when then 'next' argument in 'from_recurrence' is missing.
-#
-sub _callback_next {
-    my ($value, $callback_previous, $callback_info) = @_; 
-    my $next = $value->clone;
-
-    return $value if $value->is_infinite;
-
-    my $freq = $callback_info->{freq};
-    unless (defined $freq) 
-    { 
-        # This is called just once, to setup the recurrence frequency
-        my $next =     $callback_previous->( $value );
-        my $previous = $callback_previous->( $next );
-        $freq = 2 * ( $next - $previous );
-        # save it for future use with this same recurrence
-        $callback_info->{freq} = $freq;
-    }
-
-    $next->add_duration( $freq );  
-    $next = $callback_previous->( $next );
-    if ($next <= $value) 
-    {
-        # This error happens if the event frequency oscilates widely
-        # (more than 100% of difference from one interval to next)
-        my @freq = $freq->deltas;
-        print STDERR "_callback_next: Delta components are: @freq\n";
-        warn "_callback_next: iterator can't find a previous value, got ".
-            $next->ymd." before ".$value->ymd;
-    }
-    my $next1;
-    while (1) 
-    {
-        $next1 = $next->clone;
-        $next =  $callback_previous->( $next );
-        return $next1 if $next >= $value;
-    }
-}
-
-sub iterator {
-    my $self = shift;
-
-    my %args = @_;
-    my $span;
-    $span = delete $args{span};
-    $span = DateTime::Span->new( %args ) if %args;
-
-    return $self->intersection( $span ) if $span;
-    return $self->clone;
-}
-
-
-# next() gets the next element from an iterator()
-# next( $dt ) returns the next element after a datetime.
-sub next {
-    my $self = shift;
-    return undef unless ref( $self->{set} );
-
-    if ( @_ ) 
-    {
-        if ( $self->{set}->_is_recurrence )
-        {
-            return _fix_return_datetime(
-                       $self->{set}->{param}[0]->( $_[0] ), $_[0] );
-        }
-        else 
-        {
-            my $span = DateTime::Span->from_datetimes( after => $_[0] );
-            return _fix_return_datetime(
-                        $self->intersection( $span )->next, $_[0] );
-        }
-    }
-
-    my ($head, $tail) = $self->{set}->first;
-    $self->{set} = $tail;
-    return $head->min if defined $head;
-    return $head;
-}
-
-# previous() gets the last element from an iterator()
-# previous( $dt ) returns the previous element before a datetime.
-sub previous {
-    my $self = shift;
-    return undef unless ref( $self->{set} );
-
-    if ( @_ ) 
-    {
-        if ( $self->{set}->_is_recurrence ) 
-        {
-            return _fix_return_datetime(
-                      $self->{set}->{param}[1]->( $_[0] ), $_[0] );
-        }
-        else 
-        {
-            my $span = DateTime::Span->from_datetimes( before => $_[0] );
-            return _fix_return_datetime(
-                      $self->intersection( $span )->previous, $_[0] );
-        }
-    }
-
-    my ($head, $tail) = $self->{set}->last;
-    $self->{set} = $tail;
-    return $head->max if defined $head;
-    return $head;
-}
-
-# "current" means less-or-equal to a datetime
-sub current {
-    my $self = shift;
-
-    return undef unless ref( $self->{set} );
-
-    if ( $self->{set}->_is_recurrence )
-    {
-        my $tmp = $self->next( $_[0] );
-        return $self->previous( $tmp );
-    }
-
-    return $_[0] if $self->contains( $_[0] );
-    $self->previous( $_[0] );
-}
-
-sub closest {
-    my $self = shift;
-    # return $_[0] if $self->contains( $_[0] );
-    my $dt1 = $self->current( $_[0] );
-    my $dt2 = $self->next( $_[0] );
-
-    return $dt2 unless defined $dt1;
-    return $dt1 unless defined $dt2;
-
-    my $delta = $_[0] - $dt1;
-    return $dt1 if ( $dt2 - $delta ) >= $_[0];
-
-    return $dt2;
-}
-
-sub as_list {
-    my $self = shift;
-    return undef unless ref( $self->{set} );
-
-    my %args = @_;
-    my $span;
-    $span = delete $args{span};
-    $span = DateTime::Span->new( %args ) if %args;
-
-    my $set = $self->clone;
-    $set = $set->intersection( $span ) if $span;
-
-    return if $set->{set}->is_null;  # nothing = empty
-
-    # Note: removing this line means we may end up in an infinite loop!
-    ## return undef if $set->{set}->is_too_complex;  # undef = no begin/end
-    return undef
-        if $set->max->is_infinite ||
-           $set->min->is_infinite;
-
-    my @result;
-    my $next = $self->min;
-    if ( $span ) {
-        my $next1 = $span->min;
-        $next = $next1 if $next1 && $next1 > $next;
-        $next = $self->current( $next );
-    }
-    my $last = $self->max;
-    if ( $span ) {
-        my $last1 = $span->max;
-        $last = $last1 if $last1 && $last1 < $last;
-    }
-    do {
-        push @result, $next if !$span || $span->contains($next);
-        $next = $self->next( $next );
-    }
-    while $next && $next <= $last;
-    return @result;
-}
-
-sub intersection {
-    my ($set1, $set2) = ( shift, shift );
-    my $class = ref($set1);
-    my $tmp = $class->empty_set();
-    $set2 = $set2->as_set
-        if $set2->can( 'as_set' );
-    $set2 = $class->from_datetimes( dates => [ $set2, @_ ] ) 
-        unless $set2->can( 'union' );
-    $tmp->{set} = $set1->{set}->intersection( $set2->{set} );
-    return $tmp;
-}
-
-sub intersects {
-    my ($set1, $set2) = ( shift, shift );
-    my $class = ref($set1);
-    $set2 = $set2->as_set
-        if $set2->can( 'as_set' );
-    unless ( $set2->can( 'union' ) )
-    {
-        if ( $set1->{set}->_is_recurrence )
-        {
-            for ( $set2, @_ )
-            {
-                return 1 if $set1->current( $_ ) == $_;
-            }
-            return 0;
-        }
-        $set2 = $class->from_datetimes( dates => [ $set2, @_ ] )
-    }
-    return $set1->{set}->intersects( $set2->{set} );
-}
-
-sub contains {
-    my ($set1, $set2) = ( shift, shift );
-    my $class = ref($set1);
-    $set2 = $set2->as_set
-        if $set2->can( 'as_set' );
-    unless ( $set2->can( 'union' ) )
-    {
-        if ( $set1->{set}->_is_recurrence )
-        {
-            for ( $set2, @_ ) 
-            {
-                return 0 unless $set1->current( $_ ) == $_;
-            }
-            return 1;
-        }
-        $set2 = $class->from_datetimes( dates => [ $set2, @_ ] ) 
-    }
-    return $set1->{set}->contains( $set2->{set} );
-}
-
-sub union {
-    my ($set1, $set2) = ( shift, shift );
-    my $class = ref($set1);
-    my $tmp = $class->empty_set();
-    $set2 = $set2->as_set
-        if $set2->can( 'as_set' );
-    $set2 = $class->from_datetimes( dates => [ $set2, @_ ] ) 
-        unless $set2->can( 'union' );
-    $tmp->{set} = $set1->{set}->union( $set2->{set} );
-    bless $tmp, 'DateTime::SpanSet' 
-        if $set2->isa('DateTime::Span') or $set2->isa('DateTime::SpanSet');
-    return $tmp;
-}
-
-sub complement {
-    my ($set1, $set2) = ( shift, shift );
-    my $class = ref($set1);
-    my $tmp = $class->empty_set();
-    if (defined $set2) 
-    {
-        $set2 = $set2->as_set
-            if $set2->can( 'as_set' );
-        $set2 = $class->from_datetimes( dates => [ $set2, @_ ] ) 
-            unless $set2->can( 'union' );
-        # TODO: "compose complement";
-        $tmp->{set} = $set1->{set}->complement( $set2->{set} );
-    }
-    else 
-    {
-        $tmp->{set} = $set1->{set}->complement;
-        bless $tmp, 'DateTime::SpanSet';
-    }
-    return $tmp;
-}
-
-sub min { 
-    return _fix_datetime( $_[0]->{set}->min );
-}
-
-sub max { 
-    return _fix_datetime( $_[0]->{set}->max );
-}
-
-# returns a DateTime::Span
-sub span {
-  my $set = $_[0]->{set}->span;
-  my $self = bless { set => $set }, 'DateTime::Span';
-  return $self;
-}
-
-sub count {
-    my ($self) = shift;
-    return undef unless ref( $self->{set} );
-
-    my %args = @_;
-    my $span;
-    $span = delete $args{span};
-    $span = DateTime::Span->new( %args ) if %args;
-
-    my $set = $self->clone;
-    $set = $set->intersection( $span ) if $span;
-
-    return $set->{set}->count
-        unless $set->{set}->is_too_complex;
-
-    return undef
-        if $set->max->is_infinite ||
-           $set->min->is_infinite;
-
-    my $count = 0;
-    my $iter = $set->iterator;
-    $count++ while $iter->next;
-    return $count;
-}
-
-1;
-
-__END__
-
-=head1 NAME
-
-DateTime::Set - Datetime sets and set math
-
-=head1 SYNOPSIS
-
-    use DateTime;
-    use DateTime::Set;
-
-    $date1 = DateTime->new( year => 2002, month => 3, day => 11 );
-    $set1 = DateTime::Set->from_datetimes( dates => [ $date1 ] );
-    #  set1 = 2002-03-11
-
-    $date2 = DateTime->new( year => 2003, month => 4, day => 12 );
-    $set2 = DateTime::Set->from_datetimes( dates => [ $date1, $date2 ] );
-    #  set2 = 2002-03-11, and 2003-04-12
-
-    $date3 = DateTime->new( year => 2003, month => 4, day => 1 );
-    print $set2->next( $date3 )->ymd;      # 2003-04-12
-    print $set2->previous( $date3 )->ymd;  # 2002-03-11
-    print $set2->current( $date3 )->ymd;   # 2002-03-11
-    print $set2->closest( $date3 )->ymd;   # 2003-04-12
-
-    # a 'monthly' recurrence:
-    $set = DateTime::Set->from_recurrence( 
-        recurrence => sub {
-            return $_[0] if $_[0]->is_infinite;
-            return $_[0]->truncate( to => 'month' )->add( months => 1 )
-        },
-        span => $date_span1,    # optional span
-    );
-
-    $set = $set1->union( $set2 );         # like "OR", "insert", "both"
-    $set = $set1->complement( $set2 );    # like "delete", "remove"
-    $set = $set1->intersection( $set2 );  # like "AND", "while"
-    $set = $set1->complement;             # like "NOT", "negate", "invert"
-
-    if ( $set1->intersects( $set2 ) ) { ...  # like "touches", "interferes"
-    if ( $set1->contains( $set2 ) ) { ...    # like "is-fully-inside"
-
-    # data extraction 
-    $date = $set1->min;           # first date of the set
-    $date = $set1->max;           # last date of the set
-
-    $iter = $set1->iterator;
-    while ( $dt = $iter->next ) {
-        print $dt->ymd;
-    };
-
-=head1 DESCRIPTION
-
-DateTime::Set is a module for datetime sets.  It can be used to handle
-two different types of sets.
-
-The first is a fixed set of predefined datetime objects.  For example,
-if we wanted to create a set of datetimes containing the birthdays of
-people in our family for the current year.
-
-The second type of set that it can handle is one based on a
-recurrence, such as "every Wednesday", or "noon on the 15th day of
-every month".  This type of set can have fixed starting and ending
-datetimes, but neither is required.  So our "every Wednesday set"
-could be "every Wednesday from the beginning of time until the end of
-time", or "every Wednesday after 2003-03-05 until the end of time", or
-"every Wednesday between 2003-03-05 and 2004-01-07".
-
-This module also supports set math operations, so you do things like
-create a new set from the union or difference of two sets, check
-whether a datetime is a member of a given set, etc.
-
-This is different from a C<DateTime::Span>, which handles a continuous
-range as opposed to individual datetime points. There is also a module
-C<DateTime::SpanSet> to handle sets of spans.
-
-=head1 METHODS
-
-=over 4
-
-=item * from_datetimes
-
-Creates a new set from a list of datetimes.
-
-   $dates = DateTime::Set->from_datetimes( dates => [ $dt1, $dt2, $dt3 ] );
-
-The datetimes can be objects from class C<DateTime>, or from a
-C<DateTime::Calendar::*> class.
-
-C<DateTime::Infinite::*> objects are not valid set members.
-
-=item * from_recurrence
-
-Creates a new set specified via a "recurrence" callback.
-
-    $months = DateTime::Set->from_recurrence( 
-        span => $dt_span_this_year,    # optional span
-        recurrence => sub { 
-            return $_[0]->truncate( to => 'month' )->add( months => 1 ) 
-        }, 
-    );
-
-The C<span> parameter is optional. It must be a C<DateTime::Span> object.
-
-The span can also be specified using C<begin> / C<after> and C<before>
-/ C<end> parameters, as in the C<DateTime::Span> constructor.  In this
-case, if there is a C<span> parameter it will be ignored.
-
-    $months = DateTime::Set->from_recurrence(
-        after => $dt_now,
-        recurrence => sub {
-            return $_[0]->truncate( to => 'month' )->add( months => 1 );
-        },
-    );
-
-The recurrence function will be passed a single parameter, a datetime
-object. The parameter can be an object from class C<DateTime>, or from
-one of the C<DateTime::Calendar::*> classes.  The parameter can also
-be a C<DateTime::Infinite::Future> or a C<DateTime::Infinite::Past>
-object.
-
-The recurrence must return the I<next> event after that object.  There
-is no guarantee as to what the returned object will be set to, only
-that it will be greater than the object passed to the recurrence.
-
-If there are no more datetimes after the given parameter, then the
-recurrence function should return C<DateTime::Infinite::Future>.
-
-It is ok to modify the parameter C<$_[0]> inside the recurrence
-function.  There are no side-effects.
-
-For example, if you wanted a recurrence that generated datetimes in
-increments of 30 seconds, it would look like this:
-
-  sub every_30_seconds {
-      my $dt = shift;
-      if ( $dt->second < 30 ) {
-          return $dt->truncate( to => 'minute' )->add( seconds => 30 );
-      } else {
-          return $dt->truncate( to => 'minute' )->add( minutes => 1 );
-      }
-  }
-
-Note that this recurrence takes leap seconds into account.  Consider
-using C<truncate()> in this manner to avoid complicated arithmetic
-problems!
-
-It is also possible to create a recurrence by specifying either or both
-of 'next' and 'previous' callbacks.
-
-The callbacks can return C<DateTime::Infinite::Future> and
-C<DateTime::Infinite::Past> objects, in order to define I<bounded
-recurrences>.  In this case, both 'next' and 'previous' callbacks must
-be defined:
-
-    # "monthly from $dt until forever"
-
-    my $months = DateTime::Set->from_recurrence(
-        next => sub {
-            return $dt if $_[0] < $dt;
-            $_[0]->truncate( to => 'month' );
-            $_[0]->add( months => 1 );
-            return $_[0];
-        },
-        previous => sub {
-            my $param = $_[0]->clone;
-            $_[0]->truncate( to => 'month' );
-            $_[0]->subtract( months => 1 ) if $_[0] == $param;
-            return $_[0] if $_[0] >= $dt;
-            return DateTime::Infinite::Past->new;
-        },
-    );
-
-Bounded recurrences are easier to write using C<span> parameters. See above.
-
-See also C<DateTime::Event::Recurrence> and the other
-C<DateTime::Event::*> factory modules for generating specialized
-recurrences, such as sunrise and sunset times, and holidays.
-
-=item * empty_set
-
-Creates a new empty set.
-
-    $set = DateTime::Set->empty_set;
-    print "empty set" unless defined $set->max;
-
-=item * clone
-
-This object method returns a replica of the given object.
-
-C<clone> is useful if you want to apply a transformation to a set,
-but you want to keep the previous value:
-
-    $set2 = $set1->clone;
-    $set2->add_duration( year => 1 );  # $set1 is unaltered
-
-=item * add_duration( $duration )
-
-This method adds the specified duration to every element of the set.
-
-    $dt_dur = new DateTime::Duration( year => 1 );
-    $set->add_duration( $dt_dur );
-
-The original set is modified. If you want to keep the old values use:
-
-    $new_set = $set->clone->add_duration( $dt_dur );
-
-=item * add
-
-This method is syntactic sugar around the C<add_duration()> method.
-
-    $meetings_2004 = $meetings_2003->clone->add( years => 1 );
-
-=item * subtract_duration( $duration_object )
-
-When given a C<DateTime::Duration> object, this method simply calls
-C<invert()> on that object and passes that new duration to the
-C<add_duration> method.
-
-=item * subtract( DateTime::Duration->new parameters )
-
-Like C<add()>, this is syntactic sugar for the C<subtract_duration()>
-method.
-
-=item * set_time_zone( $tz )
-
-This method will attempt to apply the C<set_time_zone> method to every 
-datetime in the set.
-
-=item * set( locale => .. )
-
-This method can be used to change the C<locale> of a datetime set.
-
-=item * min
-
-=item * max
-
-The first and last C<DateTime> in the set.  These methods may return
-C<undef> if the set is empty.  It is also possible that these methods
-may return a C<DateTime::Infinite::Past> or
-C<DateTime::Infinite::Future> object.
-
-These methods return just a I<copy> of the actual boundary value.
-If you modify the result, the set will not be modified.
-
-=item * span
-
-Returns the total span of the set, as a C<DateTime::Span> object.
-
-=item * iterator / next / previous
-
-These methods can be used to iterate over the datetimes in a set.
-
-    $iter = $set1->iterator;
-    while ( $dt = $iter->next ) {
-        print $dt->ymd;
-    }
-
-    # iterate backwards
-    $iter = $set1->iterator;
-    while ( $dt = $iter->previous ) {
-        print $dt->ymd;
-    }
-
-The boundaries of the iterator can be limited by passing it a C<span>
-parameter.  This should be a C<DateTime::Span> object which delimits
-the iterator's boundaries.  Optionally, instead of passing an object,
-you can pass any parameters that would work for one of the
-C<DateTime::Span> class's constructors, and an object will be created
-for you.
-
-Obviously, if the span you specify is not restricted both at the start
-and end, then your iterator may iterate forever, depending on the
-nature of your set.  User beware!
-
-The C<next()> or C<previous()> method will return C<undef> when there
-are no more datetimes in the iterator.
-
-=item * as_list
-
-Returns the set elements as a list of C<DateTime> objects.  Just as
-with the C<iterator()> method, the C<as_list()> method can be limited
-by a span.
-
-  my @dt = $set->as_list( span => $span );
-
-Applying C<as_list()> to a large recurrence set is a very expensive
-operation, both in CPU time and in the memory used.  If you I<really>
-need to extract elements from a large set, you can limit the set with
-a shorter span:
-
-    my @short_list = $large_set->as_list( span => $short_span );
-
-For I<infinite> sets, C<as_list()> will return C<undef>.  Please note
-that this is explicitly not an empty list, since an empty list is a
-valid return value for empty sets!
-
-=item * count
-
-Returns a count of C<DateTime> objects in the set.  Just as with the
-C<iterator()> method, the C<count()> method can be limited by a span.
-
-  defined( my $n = $set->count) or die "can't count";
-
-  my $n = $set->count( span => $span );
-  die "can't count" unless defined $n;
-
-Applying C<count()> to a large recurrence set is a very expensive
-operation, both in CPU time and in the memory used.  If you I<really>
-need to count elements from a large set, you can limit the set with a
-shorter span:
-
-    my $count = $large_set->count( span => $short_span );
-
-For I<infinite> sets, C<count()> will return C<undef>.  Please note
-that this is explicitly not a scalar zero, since a zero count is a
-valid return value for empty sets!
-
-=item * union
-
-=item * intersection
-
-=item * complement
-
-These set operation methods can accept a C<DateTime> list, a
-C<DateTime::Set>, a C<DateTime::Span>, or a C<DateTime::SpanSet>
-object as an argument.
-
-    $set = $set1->union( $set2 );         # like "OR", "insert", "both"
-    $set = $set1->complement( $set2 );    # like "delete", "remove"
-    $set = $set1->intersection( $set2 );  # like "AND", "while"
-    $set = $set1->complement;             # like "NOT", "negate", "invert"
-
-The C<union> of a C<DateTime::Set> with a C<DateTime::Span> or a
-C<DateTime::SpanSet> object returns a C<DateTime::SpanSet> object.
-
-If C<complement> is called without any arguments, then the result is a
-C<DateTime::SpanSet> object representing the spans between each of the
-set's elements.  If complement is given an argument, then the return
-value is a C<DateTime::Set> object representing the I<set difference>
-between the sets.
-
-All other operations will always return a C<DateTime::Set>.
-
-=item * intersects
-
-=item * contains
-
-These set operations result in a boolean value.
-
-    if ( $set1->intersects( $set2 ) ) { ...  # like "touches", "interferes"
-    if ( $set1->contains( $dt ) ) { ...    # like "is-fully-inside"
-
-These methods can accept a C<DateTime> list, a C<DateTime::Set>, a
-C<DateTime::Span>, or a C<DateTime::SpanSet> object as an argument.
-
-=item * previous
-
-=item * next
-
-=item * current
-
-=item * closest
-
-  my $dt = $set->next( $dt );
-  my $dt = $set->previous( $dt );
-  my $dt = $set->current( $dt );
-  my $dt = $set->closest( $dt );
-
-These methods are used to find a set member relative to a given
-datetime.
-
-The C<current()> method returns C<$dt> if $dt is an event, otherwise
-it returns the previous event.
-
-The C<closest()> method returns C<$dt> if $dt is an event, otherwise
-it returns the closest event (previous or next).
-
-All of these methods may return C<undef> if there is no matching
-datetime in the set.
-
-These methods will try to set the returned value to the same time zone
-as the argument, unless the argument has a 'floating' time zone.
-
-=item * map ( sub { ... } )
-
-    # example: remove the hour:minute:second information
-    $set = $set2->map( 
-        sub {
-            return $_->truncate( to => day );
-        }
-    );
-
-    # example: postpone or antecipate events which 
-    #          match datetimes within another set
-    $set = $set2->map(
-        sub {
-            return $_->add( days => 1 ) while $holidays->contains( $_ );
-        }
-    );
-
-This method is the "set" version of Perl "map".
-
-It evaluates a subroutine for each element of the set (locally setting
-"$_" to each datetime) and returns the set composed of the results of
-each such evaluation.
-
-Like Perl "map", each element of the set may produce zero, one, or
-more elements in the returned value.
-
-Unlike Perl "map", changing "$_" does not change the original
-set. This means that calling map in void context has no effect.
-
-The callback subroutine may be called later in the program, due to
-lazy evaluation.  So don't count on subroutine side-effects. For
-example, a C<print> inside the subroutine may happen later than you
-expect.
-
-The callback return value is expected to be within the span of the
-C<previous> and the C<next> element in the original set.  This is a
-limitation of the backtracking algorithm used in the C<Set::Infinite>
-library.
-
-For example: given the set C<[ 2001, 2010, 2015 ]>, the callback
-result for the value C<2010> is expected to be within the span C<[
-2001 .. 2015 ]>.
-
-=item * grep ( sub { ... } )
-
-    # example: filter out any sundays
-    $set = $set2->grep( 
-        sub {
-            return ( $_->day_of_week != 7 );
-        }
-    );
-
-This method is the "set" version of Perl "grep".
-
-It evaluates a subroutine for each element of the set (locally setting
-"$_" to each datetime) and returns the set consisting of those
-elements for which the expression evaluated to true.
-
-Unlike Perl "grep", changing "$_" does not change the original
-set. This means that calling grep in void context has no effect.
-
-Changing "$_" does change the resulting set.
-
-The callback subroutine may be called later in the program, due to
-lazy evaluation.  So don't count on subroutine side-effects. For
-example, a C<print> inside the subroutine may happen later than you
-expect.
-
-=item * iterate ( sub { ... } )
-
-I<deprecated method - please use "map" or "grep" instead.>
-
-=back
-
-=head1 SUPPORT
-
-Support is offered through the C<datetime@perl.org> mailing list.
-
-Please report bugs using rt.cpan.org
-
-=head1 AUTHOR
-
-Flavio Soibelmann Glock <fglock@pucrs.br>
-
-The API was developed together with Dave Rolsky and the DateTime
-Community.
-
-=head1 COPYRIGHT
-
-Copyright (c) 2003-2006 Flavio Soibelmann Glock. All rights reserved.
-This program is free software; you can distribute it and/or modify it
-under the same terms as Perl itself.
-
-The full text of the license can be found in the LICENSE file included
-with this module.
-
-=head1 SEE ALSO
-
-Set::Infinite
-
-For details on the Perl DateTime Suite project please see
-L<http://datetime.perl.org>.
-
-=cut
-
diff --git a/modules/fallback/DateTime/Span.pm b/modules/fallback/DateTime/Span.pm
deleted file mode 100644 (file)
index 5917a8a..0000000
+++ /dev/null
@@ -1,501 +0,0 @@
-# Copyright (c) 2003 Flavio Soibelmann Glock. All rights reserved.
-# This program is free software; you can redistribute it and/or
-# modify it under the same terms as Perl itself.
-
-package DateTime::Span;
-
-use strict;
-
-use DateTime::Set;
-use DateTime::SpanSet;
-
-use Params::Validate qw( validate SCALAR BOOLEAN OBJECT CODEREF ARRAYREF );
-use vars qw( $VERSION );
-
-use constant INFINITY     => DateTime::INFINITY;
-use constant NEG_INFINITY => DateTime::NEG_INFINITY;
-$VERSION = $DateTime::Set::VERSION;
-
-sub set_time_zone {
-    my ( $self, $tz ) = @_;
-
-    $self->{set} = $self->{set}->iterate( 
-        sub {
-            my %tmp = %{ $_[0]->{list}[0] };
-            $tmp{a} = $tmp{a}->clone->set_time_zone( $tz ) if ref $tmp{a};
-            $tmp{b} = $tmp{b}->clone->set_time_zone( $tz ) if ref $tmp{b};
-            \%tmp;
-        }
-    );
-    return $self;
-}
-
-# note: the constructor must clone its DateTime parameters, such that
-# the set elements become immutable
-sub from_datetimes {
-    my $class = shift;
-    my %args = validate( @_,
-                         { start =>
-                           { type => OBJECT,
-                             optional => 1,
-                           },
-                           end =>
-                           { type => OBJECT,
-                             optional => 1,
-                           },
-                           after =>
-                           { type => OBJECT,
-                             optional => 1,
-                           },
-                           before =>
-                           { type => OBJECT,
-                             optional => 1,
-                           },
-                         }
-                       );
-    my $self = {};
-    my $set;
-
-    die "No arguments given to DateTime::Span->from_datetimes\n"
-        unless keys %args;
-
-    if ( exists $args{start} && exists $args{after} ) {
-        die "Cannot give both start and after arguments to DateTime::Span->from_datetimes\n";
-    }
-    if ( exists $args{end} && exists $args{before} ) {
-        die "Cannot give both end and before arguments to DateTime::Span->from_datetimes\n";
-    }
-
-    my ( $start, $open_start, $end, $open_end );
-    ( $start, $open_start ) = ( NEG_INFINITY,  0 );
-    ( $start, $open_start ) = ( $args{start},  0 ) if exists $args{start};
-    ( $start, $open_start ) = ( $args{after},  1 ) if exists $args{after};
-    ( $end,   $open_end   ) = ( INFINITY,      0 );
-    ( $end,   $open_end   ) = ( $args{end},    0 ) if exists $args{end};
-    ( $end,   $open_end   ) = ( $args{before}, 1 ) if exists $args{before};
-
-    if ( $start > $end ) {
-        die "Span cannot start after the end in DateTime::Span->from_datetimes\n";
-    }
-    $set = Set::Infinite::_recurrence->new( $start, $end );
-    if ( $start != $end ) {
-        # remove start, such that we have ">" instead of ">="
-        $set = $set->complement( $start ) if $open_start;  
-        # remove end, such that we have "<" instead of "<="
-        $set = $set->complement( $end )   if $open_end;    
-    }
-
-    $self->{set} = $set;
-    bless $self, $class;
-    return $self;
-}
-
-sub from_datetime_and_duration {
-    my $class = shift;
-    my %args = @_;
-
-    my $key;
-    my $dt;
-    # extract datetime parameters
-    for ( qw( start end before after ) ) {
-        if ( exists $args{$_} ) {
-           $key = $_;
-           $dt = delete $args{$_};
-       }
-    }
-
-    # extract duration parameters
-    my $dt_duration;
-    if ( exists $args{duration} ) {
-        $dt_duration = $args{duration};
-    }
-    else {
-        $dt_duration = DateTime::Duration->new( %args );
-    }
-    # warn "Creating span from $key => ".$dt->datetime." and $dt_duration";
-    my $other_date = $dt->clone->add_duration( $dt_duration );
-    # warn "Creating span from $key => ".$dt->datetime." and ".$other_date->datetime;
-    my $other_key;
-    if ( $dt_duration->is_positive ) {
-        # check if have to invert keys
-        $key = 'after' if $key eq 'end';
-        $key = 'start' if $key eq 'before';
-        $other_key = 'before';
-    }
-    else {
-        # check if have to invert keys
-        $other_key = 'end' if $key eq 'after';
-        $other_key = 'before' if $key eq 'start';
-        $key = 'start';
-    }
-    return $class->new( $key => $dt, $other_key => $other_date ); 
-}
-
-# This method is intentionally not documented.  It's really only for
-# use by ::Set and ::SpanSet's as_list() and iterator() methods.
-sub new {
-    my $class = shift;
-    my %args = @_;
-
-    # If we find anything _not_ appropriate for from_datetimes, we
-    # assume it must be for durations, and call this constructor.
-    # This way, we don't need to hardcode the DateTime::Duration
-    # parameters.
-    foreach ( keys %args )
-    {
-        return $class->from_datetime_and_duration(%args)
-            unless /^(?:before|after|start|end)$/;
-    }
-
-    return $class->from_datetimes(%args);
-}
-
-sub clone { 
-    bless { 
-        set => $_[0]->{set}->copy,
-        }, ref $_[0];
-}
-
-# Set::Infinite methods
-
-sub intersection {
-    my ($set1, $set2) = @_;
-    my $class = ref($set1);
-    my $tmp = {};  # $class->new();
-    $set2 = $set2->as_spanset
-        if $set2->can( 'as_spanset' );
-    $set2 = $set2->as_set
-        if $set2->can( 'as_set' );
-    $set2 = DateTime::Set->from_datetimes( dates => [ $set2 ] ) 
-        unless $set2->can( 'union' );
-    $tmp->{set} = $set1->{set}->intersection( $set2->{set} );
-
-    # intersection() can generate something more complex than a span.
-    bless $tmp, 'DateTime::SpanSet';
-
-    return $tmp;
-}
-
-sub intersects {
-    my ($set1, $set2) = @_;
-    my $class = ref($set1);
-    $set2 = $set2->as_spanset
-        if $set2->can( 'as_spanset' );
-    $set2 = $set2->as_set
-        if $set2->can( 'as_set' );
-    $set2 = DateTime::Set->from_datetimes( dates => [ $set2 ] ) 
-        unless $set2->can( 'union' );
-    return $set1->{set}->intersects( $set2->{set} );
-}
-
-sub contains {
-    my ($set1, $set2) = @_;
-    my $class = ref($set1);
-    $set2 = $set2->as_spanset
-        if $set2->can( 'as_spanset' );
-    $set2 = $set2->as_set
-        if $set2->can( 'as_set' );
-    $set2 = DateTime::Set->from_datetimes( dates => [ $set2 ] ) 
-        unless $set2->can( 'union' );
-    return $set1->{set}->contains( $set2->{set} );
-}
-
-sub union {
-    my ($set1, $set2) = @_;
-    my $class = ref($set1);
-    my $tmp = {};   # $class->new();
-    $set2 = $set2->as_spanset
-        if $set2->can( 'as_spanset' );
-    $set2 = $set2->as_set
-        if $set2->can( 'as_set' );
-    $set2 = DateTime::Set->from_datetimes( dates => [ $set2 ] ) 
-        unless $set2->can( 'union' );
-    $tmp->{set} = $set1->{set}->union( $set2->{set} );
-    # union() can generate something more complex than a span.
-    bless $tmp, 'DateTime::SpanSet';
-
-    # # We have to check it's internal structure to find out.
-    # if ( $#{ $tmp->{set}->{list} } != 0 ) {
-    #    bless $tmp, 'Date::SpanSet';
-    # }
-
-    return $tmp;
-}
-
-sub complement {
-    my ($set1, $set2) = @_;
-    my $class = ref($set1);
-    my $tmp = {};   # $class->new;
-    if (defined $set2) {
-        $set2 = $set2->as_spanset
-            if $set2->can( 'as_spanset' );
-        $set2 = $set2->as_set
-            if $set2->can( 'as_set' );
-        $set2 = DateTime::Set->from_datetimes( dates => [ $set2 ] ) 
-            unless $set2->can( 'union' );
-        $tmp->{set} = $set1->{set}->complement( $set2->{set} );
-    }
-    else {
-        $tmp->{set} = $set1->{set}->complement;
-    }
-
-    # complement() can generate something more complex than a span.
-    bless $tmp, 'DateTime::SpanSet';
-
-    # # We have to check it's internal structure to find out.
-    # if ( $#{ $tmp->{set}->{list} } != 0 ) {
-    #    bless $tmp, 'Date::SpanSet';
-    # }
-
-    return $tmp;
-}
-
-sub start { 
-    return DateTime::Set::_fix_datetime( $_[0]->{set}->min );
-}
-
-*min = \&start;
-
-sub end { 
-    return DateTime::Set::_fix_datetime( $_[0]->{set}->max );
-}
-
-*max = \&end;
-
-sub start_is_open {
-    # min_a returns info about the set boundary 
-    my ($min, $open) = $_[0]->{set}->min_a;
-    return $open;
-}
-
-sub start_is_closed { $_[0]->start_is_open ? 0 : 1 }
-
-sub end_is_open {
-    # max_a returns info about the set boundary 
-    my ($max, $open) = $_[0]->{set}->max_a;
-    return $open;
-}
-
-sub end_is_closed { $_[0]->end_is_open ? 0 : 1 }
-
-
-# span == $self
-sub span { @_ }
-
-sub duration { 
-    my $dur;
-
-    local $@;
-    eval {
-        local $SIG{__DIE__};   # don't want to trap this (rt ticket 5434)
-        $dur = $_[0]->end->subtract_datetime_absolute( $_[0]->start )
-    };
-    
-    return $dur if defined $dur;
-
-    return DateTime::Infinite::Future->new -
-           DateTime::Infinite::Past->new;
-}
-*size = \&duration;
-
-1;
-
-__END__
-
-=head1 NAME
-
-DateTime::Span - Datetime spans
-
-=head1 SYNOPSIS
-
-    use DateTime;
-    use DateTime::Span;
-
-    $date1 = DateTime->new( year => 2002, month => 3, day => 11 );
-    $date2 = DateTime->new( year => 2003, month => 4, day => 12 );
-    $set2 = DateTime::Span->from_datetimes( start => $date1, end => $date2 );
-    #  set2 = 2002-03-11 until 2003-04-12
-
-    $set = $set1->union( $set2 );         # like "OR", "insert", "both"
-    $set = $set1->complement( $set2 );    # like "delete", "remove"
-    $set = $set1->intersection( $set2 );  # like "AND", "while"
-    $set = $set1->complement;             # like "NOT", "negate", "invert"
-
-    if ( $set1->intersects( $set2 ) ) { ...  # like "touches", "interferes"
-    if ( $set1->contains( $set2 ) ) { ...    # like "is-fully-inside"
-
-    # data extraction 
-    $date = $set1->start;           # first date of the span
-    $date = $set1->end;             # last date of the span
-
-=head1 DESCRIPTION
-
-C<DateTime::Span> is a module for handling datetime spans, otherwise
-known as ranges or periods ("from X to Y, inclusive of all datetimes
-in between").
-
-This is different from a C<DateTime::Set>, which is made of individual
-datetime points as opposed to a range. There is also a module
-C<DateTime::SpanSet> to handle sets of spans.
-
-=head1 METHODS
-
-=over 4
-
-=item * from_datetimes
-
-Creates a new span based on a starting and ending datetime.
-
-A 'closed' span includes its end-dates:
-
-   $span = DateTime::Span->from_datetimes( start => $dt1, end => $dt2 );
-
-An 'open' span does not include its end-dates:
-
-   $span = DateTime::Span->from_datetimes( after => $dt1, before => $dt2 );
-
-A 'semi-open' span includes one of its end-dates:
-
-   $span = DateTime::Span->from_datetimes( start => $dt1, before => $dt2 );
-   $span = DateTime::Span->from_datetimes( after => $dt1, end => $dt2 );
-
-A span might have just a beginning date, or just an ending date.
-These spans end, or start, in an imaginary 'forever' date:
-
-   $span = DateTime::Span->from_datetimes( start => $dt1 );
-   $span = DateTime::Span->from_datetimes( end => $dt2 );
-   $span = DateTime::Span->from_datetimes( after => $dt1 );
-   $span = DateTime::Span->from_datetimes( before => $dt2 );
-
-You cannot give both a "start" and "after" argument, nor can you give
-both an "end" and "before" argument.  Either of these conditions will
-cause the C<from_datetimes()> method to die.
-
-To summarize, a datetime passed as either "start" or "end" is included
-in the span.  A datetime passed as either "after" or "before" is
-excluded from the span.
-
-=item * from_datetime_and_duration
-
-Creates a new span.
-
-   $span = DateTime::Span->from_datetime_and_duration( 
-       start => $dt1, duration => $dt_dur1 );
-   $span = DateTime::Span->from_datetime_and_duration( 
-       after => $dt1, hours => 12 );
-
-The new "end of the set" is I<open> by default.
-
-=item * clone
-
-This object method returns a replica of the given object.
-
-=item * set_time_zone( $tz )
-
-This method accepts either a time zone object or a string that can be
-passed as the "name" parameter to C<< DateTime::TimeZone->new() >>.
-If the new time zone's offset is different from the old time zone,
-then the I<local> time is adjusted accordingly.
-
-If the old time zone was a floating time zone, then no adjustments to
-the local time are made, except to account for leap seconds.  If the
-new time zone is floating, then the I<UTC> time is adjusted in order
-to leave the local time untouched.
-
-=item * duration
-
-The total size of the set, as a C<DateTime::Duration> object, or as a
-scalar containing infinity.
-
-Also available as C<size()>.
-
-=item * start
-
-=item * end
-
-First or last dates in the span.
-
-It is possible that the return value from these methods may be a
-C<DateTime::Infinite::Future> or a C<DateTime::Infinite::Past>xs object.
-
-If the set ends C<before> a date C<$dt>, it returns C<$dt>. Note that
-in this case C<$dt> is not a set element - but it is a set boundary.
-
-=cut
-
-# scalar containing either negative infinity
-# or positive infinity.
-
-=item * start_is_closed
-
-=item * end_is_closed
-
-Returns true if the first or last dates belong to the span ( begin <= x <= end ).
-
-=item * start_is_open
-
-=item * end_is_open
-
-Returns true if the first or last dates are excluded from the span ( begin < x < end ).
-
-=item * union
-
-=item * intersection
-
-=item * complement
-
-Set operations may be performed not only with C<DateTime::Span>
-objects, but also with C<DateTime::Set> and C<DateTime::SpanSet>
-objects.  These set operations always return a C<DateTime::SpanSet>
-object.
-
-    $set = $span->union( $set2 );         # like "OR", "insert", "both"
-    $set = $span->complement( $set2 );    # like "delete", "remove"
-    $set = $span->intersection( $set2 );  # like "AND", "while"
-    $set = $span->complement;             # like "NOT", "negate", "invert"
-
-=item * intersects
-
-=item * contains
-
-These set functions return a boolean value.
-
-    if ( $span->intersects( $set2 ) ) { ...  # like "touches", "interferes"
-    if ( $span->contains( $dt ) ) { ...    # like "is-fully-inside"
-
-These methods can accept a C<DateTime>, C<DateTime::Set>,
-C<DateTime::Span>, or C<DateTime::SpanSet> object as an argument.
-
-=back
-
-=head1 SUPPORT
-
-Support is offered through the C<datetime@perl.org> mailing list.
-
-Please report bugs using rt.cpan.org
-
-=head1 AUTHOR
-
-Flavio Soibelmann Glock <fglock@pucrs.br>
-
-The API was developed together with Dave Rolsky and the DateTime Community.
-
-=head1 COPYRIGHT
-
-Copyright (c) 2003-2006 Flavio Soibelmann Glock. All rights reserved.
-This program is free software; you can distribute it and/or modify it
-under the same terms as Perl itself.
-
-The full text of the license can be found in the LICENSE file
-included with this module.
-
-=head1 SEE ALSO
-
-Set::Infinite
-
-For details on the Perl DateTime Suite project please see
-L<http://datetime.perl.org>.
-
-=cut
-
diff --git a/modules/fallback/DateTime/SpanSet.pm b/modules/fallback/DateTime/SpanSet.pm
deleted file mode 100644 (file)
index 8a258f1..0000000
+++ /dev/null
@@ -1,945 +0,0 @@
-# Copyright (c) 2003 Flavio Soibelmann Glock. All rights reserved.
-# This program is free software; you can redistribute it and/or
-# modify it under the same terms as Perl itself.
-
-package DateTime::SpanSet;
-
-use strict;
-
-use DateTime::Set;
-use DateTime::Infinite;
-
-use Carp;
-use Params::Validate qw( validate SCALAR BOOLEAN OBJECT CODEREF ARRAYREF );
-use vars qw( $VERSION );
-
-use constant INFINITY     =>       100 ** 100 ** 100 ;
-use constant NEG_INFINITY => -1 * (100 ** 100 ** 100);
-$VERSION = $DateTime::Set::VERSION;
-
-sub iterate {
-    my ( $self, $callback ) = @_;
-    my $class = ref( $self );
-    my $return = $class->empty_set;
-    $return->{set} = $self->{set}->iterate(
-        sub {
-            my $span = bless { set => $_[0] }, 'DateTime::Span';
-            $callback->( $span->clone );
-            $span = $span->{set} 
-                if UNIVERSAL::can( $span, 'union' );
-            return $span;
-        }
-    );
-    $return;
-}
-
-sub map {
-    my ( $self, $callback ) = @_;
-    my $class = ref( $self );
-    die "The callback parameter to map() must be a subroutine reference"
-        unless ref( $callback ) eq 'CODE';
-    my $return = $class->empty_set;
-    $return->{set} = $self->{set}->iterate( 
-        sub {
-            local $_ = bless { set => $_[0]->clone }, 'DateTime::Span';
-            my @list = $callback->();
-            my $set = $class->empty_set;
-            $set = $set->union( $_ ) for @list;
-            return $set->{set};
-        }
-    );
-    $return;
-}
-
-sub grep {
-    my ( $self, $callback ) = @_;
-    my $class = ref( $self );
-    die "The callback parameter to grep() must be a subroutine reference"
-        unless ref( $callback ) eq 'CODE';
-    my $return = $class->empty_set;
-    $return->{set} = $self->{set}->iterate( 
-        sub {
-            local $_ = bless { set => $_[0]->clone }, 'DateTime::Span';
-            my $result = $callback->();
-            return $_ if $result;
-            return;
-        }
-    );
-    $return;
-}
-
-sub set_time_zone {
-    my ( $self, $tz ) = @_;
-
-    # TODO - use iterate() instead 
-
-    my $result = $self->{set}->iterate( 
-        sub {
-            my %tmp = %{ $_[0]->{list}[0] };
-            $tmp{a} = $tmp{a}->clone->set_time_zone( $tz ) if ref $tmp{a};
-            $tmp{b} = $tmp{b}->clone->set_time_zone( $tz ) if ref $tmp{b};
-            \%tmp;
-        },
-        backtrack_callback => sub {
-            my ( $min, $max ) = ( $_[0]->min, $_[0]->max );
-            if ( ref($min) )
-            {
-                $min = $min->clone;
-                $min->set_time_zone( 'floating' );
-            }
-            if ( ref($max) )
-            {
-                $max = $max->clone;
-                $max->set_time_zone( 'floating' ); 
-            }
-            return Set::Infinite::_recurrence->new( $min, $max );
-        },
-    );
-
-    ### this code enables 'subroutine method' behaviour
-    $self->{set} = $result;
-    return $self;
-}
-
-sub from_spans {
-    my $class = shift;
-    my %args = validate( @_,
-                         { spans =>
-                           { type => ARRAYREF,
-                             optional => 1,
-                           },
-                         }
-                       );
-    my $self = {};
-    my $set = Set::Infinite::_recurrence->new();
-    $set = $set->union( $_->{set} ) for @{ $args{spans} };
-    $self->{set} = $set;
-    bless $self, $class;
-    return $self;
-}
-
-sub from_set_and_duration {
-    # set => $dt_set, days => 1
-    my $class = shift;
-    my %args = @_;
-    my $set = delete $args{set} || 
-        carp "from_set_and_duration needs a 'set' parameter";
-
-    $set = $set->as_set
-        if UNIVERSAL::can( $set, 'as_set' );
-    unless ( UNIVERSAL::can( $set, 'union' ) ) {
-        carp "'set' must be a set" };
-
-    my $duration = delete $args{duration} ||
-                   new DateTime::Duration( %args );
-    my $end_set = $set->clone->add_duration( $duration );
-    return $class->from_sets( start_set => $set, 
-                              end_set =>   $end_set );
-}
-
-sub from_sets {
-    my $class = shift;
-    my %args = validate( @_,
-                         { start_set =>
-                           { # can => 'union',
-                             optional => 0,
-                           },
-                           end_set =>
-                           { # can => 'union',
-                             optional => 0,
-                           },
-                         }
-                       );
-    my $start_set = delete $args{start_set};
-    my $end_set   = delete $args{end_set};
-
-    $start_set = $start_set->as_set
-        if UNIVERSAL::can( $start_set, 'as_set' );
-    $end_set = $end_set->as_set
-        if UNIVERSAL::can( $end_set, 'as_set' );
-
-    unless ( UNIVERSAL::can( $start_set, 'union' ) ) {
-        carp "'start_set' must be a set" };
-    unless ( UNIVERSAL::can( $end_set, 'union' ) ) {
-        carp "'end_set' must be a set" };
-
-    my $self;
-    $self->{set} = $start_set->{set}->until( 
-                   $end_set->{set} );
-    bless $self, $class;
-    return $self;
-}
-
-sub start_set {
-    if ( exists $_[0]->{set}{method} &&
-         $_[0]->{set}{method} eq 'until' )
-    {
-        return bless { set => $_[0]->{set}{parent}[0] }, 'DateTime::Set';
-    }
-    my $return = DateTime::Set->empty_set;
-    $return->{set} = $_[0]->{set}->start_set;
-    $return;
-}
-
-sub end_set {
-    if ( exists $_[0]->{set}{method} &&
-         $_[0]->{set}{method} eq 'until' )
-    {
-        return bless { set => $_[0]->{set}{parent}[1] }, 'DateTime::Set';
-    }
-    my $return = DateTime::Set->empty_set;
-    $return->{set} = $_[0]->{set}->end_set;
-    $return;
-}
-
-sub empty_set {
-    my $class = shift;
-
-    return bless { set => Set::Infinite::_recurrence->new }, $class;
-}
-
-sub clone { 
-    bless { 
-        set => $_[0]->{set}->copy,
-        }, ref $_[0];
-}
-
-
-sub iterator {
-    my $self = shift;
-
-    my %args = @_;
-    my $span;
-    $span = delete $args{span};
-    $span = DateTime::Span->new( %args ) if %args;
-
-    return $self->intersection( $span ) if $span;
-    return $self->clone;
-}
-
-
-# next() gets the next element from an iterator()
-sub next {
-    my ($self) = shift;
-
-    # TODO: this is fixing an error from elsewhere
-    # - find out what's going on! (with "sunset.pl")
-    return undef unless ref $self->{set};
-
-    if ( @_ )
-    {
-        my $max;
-        $max = $_[0]->max if UNIVERSAL::can( $_[0], 'union' );
-        $max = $_[0] if ! defined $max;
-
-        return undef if ! ref( $max ) && $max == INFINITY;
-
-        my $span = DateTime::Span->from_datetimes( start => $max );
-        my $iterator = $self->intersection( $span );
-        my $return = $iterator->next;
-
-        return $return if ! defined $return;
-        return $return if ! $return->intersects( $max );
-
-        return $iterator->next;
-    }
-
-    my ($head, $tail) = $self->{set}->first;
-    $self->{set} = $tail;
-    return $head unless ref $head;
-    my $return = {
-        set => $head,
-    };
-    bless $return, 'DateTime::Span';
-    return $return;
-}
-
-# previous() gets the last element from an iterator()
-sub previous {
-    my ($self) = shift;
-
-    return undef unless ref $self->{set};
-
-    if ( @_ )
-    {
-        my $min;
-        $min = $_[0]->min if UNIVERSAL::can( $_[0], 'union' );
-        $min = $_[0] if ! defined $min;
-
-        return undef if ! ref( $min ) && $min == INFINITY;
-
-        my $span = DateTime::Span->from_datetimes( end => $min );
-        my $iterator = $self->intersection( $span );
-        my $return = $iterator->previous;
-
-        return $return if ! defined $return;
-        return $return if ! $return->intersects( $min );
-
-        return $iterator->previous;
-    }
-
-    my ($head, $tail) = $self->{set}->last;
-    $self->{set} = $tail;
-    return $head unless ref $head;
-    my $return = {
-        set => $head,
-    };
-    bless $return, 'DateTime::Span';
-    return $return;
-}
-
-# "current" means less-or-equal to a DateTime
-sub current {
-    my $self = shift;
-
-    my $previous;
-    my $next;
-    {
-        my $min;
-        $min = $_[0]->min if UNIVERSAL::can( $_[0], 'union' );
-        $min = $_[0] if ! defined $min;
-        return undef if ! ref( $min ) && $min == INFINITY;
-        my $span = DateTime::Span->from_datetimes( end => $min );
-        my $iterator = $self->intersection( $span );
-        $previous = $iterator->previous;
-        $span = DateTime::Span->from_datetimes( start => $min );
-        $iterator = $self->intersection( $span );
-        $next = $iterator->next;
-    }
-    return $previous unless defined $next;
-
-    my $dt1 = defined $previous
-        ? $next->union( $previous )
-        : $next;
-
-    my $return = $dt1->intersected_spans( $_[0] );
-
-    $return = $previous
-        if !defined $return->max;
-
-    bless $return, 'DateTime::SpanSet'
-        if defined $return;
-    return $return;
-}
-
-sub closest {
-    my $self = shift;
-    my $dt = shift;
-
-    my $dt1 = $self->current( $dt );
-    my $dt2 = $self->next( $dt );
-    bless $dt2, 'DateTime::SpanSet' 
-        if defined $dt2;
-
-    return $dt2 unless defined $dt1;
-    return $dt1 unless defined $dt2;
-
-    $dt = DateTime::Set->from_datetimes( dates => [ $dt ] )
-        unless UNIVERSAL::can( $dt, 'union' );
-
-    return $dt1 if $dt1->contains( $dt );
-
-    my $delta = $dt->min - $dt1->max;
-    return $dt1 if ( $dt2->min - $delta ) >= $dt->max;
-
-    return $dt2;
-}
-
-sub as_list {
-    my $self = shift;
-    return undef unless ref( $self->{set} );
-
-    my %args = @_;
-    my $span;
-    $span = delete $args{span};
-    $span = DateTime::Span->new( %args ) if %args;
-
-    my $set = $self->clone;
-    $set = $set->intersection( $span ) if $span;
-
-    # Note: removing this line means we may end up in an infinite loop!
-    return undef if $set->{set}->is_too_complex;  # undef = no begin/end
-
-    # return if $set->{set}->is_null;  # nothing = empty
-    my @result;
-    # we should extract _copies_ of the set elements,
-    # such that the user can't modify the set indirectly
-
-    my $iter = $set->iterator;
-    while ( my $dt = $iter->next )
-    {
-        push @result, $dt
-            if ref( $dt );   # we don't want to return INFINITY value
-    };
-
-    return @result;
-}
-
-# Set::Infinite methods
-
-sub intersection {
-    my ($set1, $set2) = ( shift, shift );
-    my $class = ref($set1);
-    my $tmp = $class->empty_set();
-    $set2 = $set2->as_spanset
-        if $set2->can( 'as_spanset' );
-    $set2 = $set2->as_set
-        if $set2->can( 'as_set' );
-    $set2 = DateTime::Set->from_datetimes( dates => [ $set2, @_ ] ) 
-        unless $set2->can( 'union' );
-    $tmp->{set} = $set1->{set}->intersection( $set2->{set} );
-    return $tmp;
-}
-
-sub intersected_spans {
-    my ($set1, $set2) = ( shift, shift );
-    my $class = ref($set1);
-    my $tmp = $class->empty_set();
-    $set2 = $set2->as_spanset
-        if $set2->can( 'as_spanset' );
-    $set2 = $set2->as_set
-        if $set2->can( 'as_set' );
-    $set2 = DateTime::Set->from_datetimes( dates => [ $set2, @_ ] )
-        unless $set2->can( 'union' );
-    $tmp->{set} = $set1->{set}->intersected_spans( $set2->{set} );
-    return $tmp;
-}
-
-sub intersects {
-    my ($set1, $set2) = ( shift, shift );
-    
-    unless ( $set2->can( 'union' ) )
-    {
-        for ( $set2, @_ )
-        {
-            return 1 if $set1->contains( $_ );
-        }
-        return 0;
-    }
-    
-    my $class = ref($set1);
-    $set2 = $set2->as_spanset
-        if $set2->can( 'as_spanset' );
-    $set2 = $set2->as_set
-        if $set2->can( 'as_set' );
-    $set2 = DateTime::Set->from_datetimes( dates => [ $set2, @_ ] ) 
-        unless $set2->can( 'union' );
-    return $set1->{set}->intersects( $set2->{set} );
-}
-
-sub contains {
-    my ($set1, $set2) = ( shift, shift );
-    
-    unless ( $set2->can( 'union' ) )
-    {
-        if ( exists $set1->{set}{method} &&
-             $set1->{set}{method} eq 'until' )
-        {
-            my $start_set = $set1->start_set;
-            my $end_set =   $set1->end_set;
-
-            for ( $set2, @_ )
-            {
-                my $start = $start_set->next( $set2 );
-                my $end =   $end_set->next( $set2 );
-
-                goto ABORT unless defined $start && defined $end;
-            
-                return 0 if $start < $end;
-            }
-            return 1;
-
-            ABORT: ;
-            # don't know 
-        }
-    }
-    
-    my $class = ref($set1);
-    $set2 = $set2->as_spanset
-        if $set2->can( 'as_spanset' );
-    $set2 = $set2->as_set
-        if $set2->can( 'as_set' );
-    $set2 = DateTime::Set->from_datetimes( dates => [ $set2, @_ ] ) 
-        unless $set2->can( 'union' );
-    return $set1->{set}->contains( $set2->{set} );
-}
-
-sub union {
-    my ($set1, $set2) = ( shift, shift );
-    my $class = ref($set1);
-    my $tmp = $class->empty_set();
-    $set2 = $set2->as_spanset
-        if $set2->can( 'as_spanset' );
-    $set2 = $set2->as_set
-        if $set2->can( 'as_set' );
-    $set2 = DateTime::Set->from_datetimes( dates => [ $set2, @_ ] ) 
-        unless $set2->can( 'union' );
-    $tmp->{set} = $set1->{set}->union( $set2->{set} );
-    return $tmp;
-}
-
-sub complement {
-    my ($set1, $set2) = ( shift, shift );
-    my $class = ref($set1);
-    my $tmp = $class->empty_set();
-    if (defined $set2) {
-        $set2 = $set2->as_spanset
-            if $set2->can( 'as_spanset' );
-        $set2 = $set2->as_set
-            if $set2->can( 'as_set' );
-        $set2 = DateTime::Set->from_datetimes( dates => [ $set2, @_ ] ) 
-            unless $set2->can( 'union' );
-        $tmp->{set} = $set1->{set}->complement( $set2->{set} );
-    }
-    else {
-        $tmp->{set} = $set1->{set}->complement;
-    }
-    return $tmp;
-}
-
-sub min {
-    return DateTime::Set::_fix_datetime( $_[0]->{set}->min );
-}
-
-sub max { 
-    return DateTime::Set::_fix_datetime( $_[0]->{set}->max );
-}
-
-# returns a DateTime::Span
-sub span { 
-    my $set = $_[0]->{set}->span;
-    my $self = bless { set => $set }, 'DateTime::Span';
-    return $self;
-}
-
-# returns a DateTime::Duration
-sub duration { 
-    my $dur; 
-
-    return DateTime::Duration->new( seconds => 0 ) 
-        if $_[0]->{set}->is_empty;
-
-    local $@;
-    eval { 
-        local $SIG{__DIE__};   # don't want to trap this (rt ticket 5434)
-        $dur = $_[0]->{set}->size 
-    };
-
-    return $dur if defined $dur && ref( $dur );
-    return DateTime::Infinite::Future->new -
-           DateTime::Infinite::Past->new;
-    # return INFINITY;
-}
-*size = \&duration;
-
-1;
-
-__END__
-
-=head1 NAME
-
-DateTime::SpanSet - set of DateTime spans
-
-=head1 SYNOPSIS
-
-    $spanset = DateTime::SpanSet->from_spans( spans => [ $dt_span, $dt_span ] );
-
-    $set = $spanset->union( $set2 );         # like "OR", "insert", "both"
-    $set = $spanset->complement( $set2 );    # like "delete", "remove"
-    $set = $spanset->intersection( $set2 );  # like "AND", "while"
-    $set = $spanset->complement;             # like "NOT", "negate", "invert"
-
-    if ( $spanset->intersects( $set2 ) ) { ...  # like "touches", "interferes"
-    if ( $spanset->contains( $set2 ) ) { ...    # like "is-fully-inside"
-
-    # data extraction 
-    $date = $spanset->min;           # first date of the set
-    $date = $spanset->max;           # last date of the set
-
-    $iter = $spanset->iterator;
-    while ( $dt = $iter->next ) {
-        # $dt is a DateTime::Span
-        print $dt->start->ymd;   # first date of span
-        print $dt->end->ymd;     # last date of span
-    };
-
-=head1 DESCRIPTION
-
-C<DateTime::SpanSet> is a class that represents sets of datetime
-spans.  An example would be a recurring meeting that occurs from
-13:00-15:00 every Friday.
-
-This is different from a C<DateTime::Set>, which is made of individual
-datetime points as opposed to ranges.
-
-=head1 METHODS
-
-=over 4
-
-=item * from_spans
-
-Creates a new span set from one or more C<DateTime::Span> objects.
-
-   $spanset = DateTime::SpanSet->from_spans( spans => [ $dt_span ] );
-
-=item * from_set_and_duration
-
-Creates a new span set from one or more C<DateTime::Set> objects and a
-duration.
-
-The duration can be a C<DateTime::Duration> object, or the parameters
-to create a new C<DateTime::Duration> object, such as "days",
-"months", etc.
-
-   $spanset =
-       DateTime::SpanSet->from_set_and_duration
-           ( set => $dt_set, days => 1 );
-
-=item * from_sets
-
-Creates a new span set from two C<DateTime::Set> objects.
-
-One set defines the I<starting dates>, and the other defines the I<end
-dates>.
-
-   $spanset =
-       DateTime::SpanSet->from_sets
-           ( start_set => $dt_set1, end_set => $dt_set2 );
-
-The spans have the starting date C<closed>, and the end date C<open>,
-like in C<[$dt1, $dt2)>.
-
-If an end date comes without a starting date before it, then it
-defines a span like C<(-inf, $dt)>.
-
-If a starting date comes without an end date after it, then it defines
-a span like C<[$dt, inf)>.
-
-=item * empty_set
-
-Creates a new empty set.
-
-=item * clone
-
-This object method returns a replica of the given object.
-
-=item * set_time_zone( $tz )
-
-This method accepts either a time zone object or a string that can be
-passed as the "name" parameter to C<< DateTime::TimeZone->new() >>.
-If the new time zone's offset is different from the old time zone,
-then the I<local> time is adjusted accordingly.
-
-If the old time zone was a floating time zone, then no adjustments to
-the local time are made, except to account for leap seconds.  If the
-new time zone is floating, then the I<UTC> time is adjusted in order
-to leave the local time untouched.
-
-=item * min
-
-=item * max
-
-First or last dates in the set.  These methods may return C<undef> if
-the set is empty.  It is also possible that these methods may return a
-scalar containing infinity or negative infinity.
-
-=item * duration
-
-The total size of the set, as a C<DateTime::Duration> object.
-
-The duration may be infinite.
-
-Also available as C<size()>.
-
-=item * span
-
-The total span of the set, as a C<DateTime::Span> object.
-
-=item * next 
-
-  my $span = $set->next( $dt );
-
-This method is used to find the next span in the set,
-after a given datetime or span.
-
-The return value is a C<DateTime::Span>, or C<undef> if there is no matching
-span in the set.
-
-=item * previous 
-
-  my $span = $set->previous( $dt );
-
-This method is used to find the previous span in the set,
-before a given datetime or span.
-
-The return value is a C<DateTime::Span>, or C<undef> if there is no matching
-span in the set.
-
-
-=item * current 
-
-  my $span = $set->current( $dt );
-
-This method is used to find the "current" span in the set,
-that intersects a given datetime or span. If no current span
-is found, then the "previous" span is returned.
-
-The return value is a C<DateTime::SpanSet>, or C<undef> if there is no
-matching span in the set.
-
-If a span parameter is given, it may happen that "current" returns
-more than one span.
-
-See also: C<intersected_spans()> method.
-
-=item * closest 
-
-  my $span = $set->closest( $dt );
-
-This method is used to find the "closest" span in the set, given a
-datetime or span.
-
-The return value is a C<DateTime::SpanSet>, or C<undef> if the set is
-empty.
-
-If a span parameter is given, it may happen that "closest" returns
-more than one span.
-
-=item * as_list
-
-Returns a list of C<DateTime::Span> objects.
-
-  my @dt_span = $set->as_list( span => $span );
-
-Just as with the C<iterator()> method, the C<as_list()> method can be
-limited by a span.
-
-Applying C<as_list()> to a large recurring spanset is a very expensive
-operation, both in CPU time and in the memory used.
-
-For this reason, when C<as_list()> operates on large recurrence sets,
-it will return at most approximately 200 spans. For larger sets, and
-for I<infinite> sets, C<as_list()> will return C<undef>.
-
-Please note that this is explicitly not an empty list, since an empty
-list is a valid return value for empty sets!
-
-If you I<really> need to extract spans from a large set, you can:
-
-- limit the set with a shorter span:
-
-    my @short_list = $large_set->as_list( span => $short_span );
-
-- use an iterator:
-
-    my @large_list;
-    my $iter = $large_set->iterator;
-    push @large_list, $dt while $dt = $iter->next;
-
-=item * union
-
-=item * intersection
-
-=item * complement
-
-Set operations may be performed not only with C<DateTime::SpanSet>
-objects, but also with C<DateTime>, C<DateTime::Set> and
-C<DateTime::Span> objects.  These set operations always return a
-C<DateTime::SpanSet> object.
-
-    $set = $spanset->union( $set2 );         # like "OR", "insert", "both"
-    $set = $spanset->complement( $set2 );    # like "delete", "remove"
-    $set = $spanset->intersection( $set2 );  # like "AND", "while"
-    $set = $spanset->complement;             # like "NOT", "negate", "invert"
-
-=item * intersected_spans
-
-This method can accept a C<DateTime> list, a C<DateTime::Set>, a
-C<DateTime::Span>, or a C<DateTime::SpanSet> object as an argument.
-
-    $set = $set1->intersected_spans( $set2 );
-
-The method always returns a C<DateTime::SpanSet> object, containing
-all spans that are intersected by the given set.
-
-Unlike the C<intersection> method, the spans are not modified.  See
-diagram below:
-
-               set1   [....]   [....]   [....]   [....]
-               set2      [................]
-
-       intersection      [.]   [....]   [.]
-
-  intersected_spans   [....]   [....]   [....]
-
-=item * intersects
-
-=item * contains
-
-These set functions return a boolean value.
-
-    if ( $spanset->intersects( $set2 ) ) { ...  # like "touches", "interferes"
-    if ( $spanset->contains( $dt ) ) { ...    # like "is-fully-inside"
-
-These methods can accept a C<DateTime>, C<DateTime::Set>,
-C<DateTime::Span>, or C<DateTime::SpanSet> object as an argument.
-
-=item * iterator / next / previous
-
-This method can be used to iterate over the spans in a set.
-
-    $iter = $spanset->iterator;
-    while ( $dt = $iter->next ) {
-        # $dt is a DateTime::Span
-        print $dt->min->ymd;   # first date of span
-        print $dt->max->ymd;   # last date of span
-    }
-
-The boundaries of the iterator can be limited by passing it a C<span>
-parameter.  This should be a C<DateTime::Span> object which delimits
-the iterator's boundaries.  Optionally, instead of passing an object,
-you can pass any parameters that would work for one of the
-C<DateTime::Span> class's constructors, and an object will be created
-for you.
-
-Obviously, if the span you specify does is not restricted both at the
-start and end, then your iterator may iterate forever, depending on
-the nature of your set.  User beware!
-
-The C<next()> or C<previous()> methods will return C<undef> when there
-are no more spans in the iterator.
-
-=item * start_set
-
-=item * end_set
-
-These methods do the inverse of the C<from_sets> method:
-
-C<start_set> retrieves a DateTime::Set with the start datetime of each
-span.
-
-C<end_set> retrieves a DateTime::Set with the end datetime of each
-span.
-
-=item * map ( sub { ... } )
-
-    # example: enlarge the spans
-    $set = $set2->map( 
-        sub {
-            my $start = $_->start;
-            my $end = $_->end;
-            return DateTime::Span->from_datetimes(
-                start => $start,
-                before => $end,
-            );
-        }
-    );
-
-This method is the "set" version of Perl "map".
-
-It evaluates a subroutine for each element of the set (locally setting
-"$_" to each DateTime::Span) and returns the set composed of the
-results of each such evaluation.
-
-Like Perl "map", each element of the set may produce zero, one, or
-more elements in the returned value.
-
-Unlike Perl "map", changing "$_" does not change the original
-set. This means that calling map in void context has no effect.
-
-The callback subroutine may not be called immediately.  Don't count on
-subroutine side-effects. For example, a C<print> inside the subroutine
-may happen later than you expect.
-
-The callback return value is expected to be within the span of the
-C<previous> and the C<next> element in the original set.
-
-For example: given the set C<[ 2001, 2010, 2015 ]>, the callback
-result for the value C<2010> is expected to be within the span C<[
-2001 .. 2015 ]>.
-
-=item * grep ( sub { ... } )
-
-    # example: filter out all spans happening today
-    my $today = DateTime->today;
-    $set = $set2->grep( 
-        sub {
-            return ( ! $_->contains( $today ) );
-        }
-    );
-
-This method is the "set" version of Perl "grep".
-
-It evaluates a subroutine for each element of the set (locally setting
-"$_" to each DateTime::Span) and returns the set consisting of those
-elements for which the expression evaluated to true.
-
-Unlike Perl "grep", changing "$_" does not change the original
-set. This means that calling grep in void context has no effect.
-
-Changing "$_" does change the resulting set.
-
-The callback subroutine may not be called immediately.  Don't count on
-subroutine side-effects. For example, a C<print> inside the subroutine
-may happen later than you expect.
-
-=item * iterate
-
-I<Internal method - use "map" or "grep" instead.>
-
-This function apply a callback subroutine to all elements of a set and
-returns the resulting set.
-
-The parameter C<$_[0]> to the callback subroutine is a
-C<DateTime::Span> object.
-
-If the callback returns C<undef>, the datetime is removed from the
-set:
-
-    sub remove_sundays {
-        $_[0] unless $_[0]->start->day_of_week == 7;
-    }
-
-The callback return value is expected to be within the span of the
-C<previous> and the C<next> element in the original set.
-
-For example: given the set C<[ 2001, 2010, 2015 ]>, the callback
-result for the value C<2010> is expected to be within the span C<[
-2001 .. 2015 ]>.
-
-The callback subroutine may not be called immediately.  Don't count on
-subroutine side-effects. For example, a C<print> inside the subroutine
-may happen later than you expect.
-
-=back
-
-=head1 SUPPORT
-
-Support is offered through the C<datetime@perl.org> mailing list.
-
-Please report bugs using rt.cpan.org
-
-=head1 AUTHOR
-
-Flavio Soibelmann Glock <fglock@pucrs.br>
-
-The API was developed together with Dave Rolsky and the DateTime Community.
-
-=head1 COPYRIGHT
-
-Copyright (c) 2003 Flavio Soibelmann Glock. All rights reserved.
-This program is free software; you can distribute it and/or
-modify it under the same terms as Perl itself.
-
-The full text of the license can be found in the LICENSE file
-included with this module.
-
-=head1 SEE ALSO
-
-Set::Infinite
-
-For details on the Perl DateTime Suite project please see
-L<http://datetime.perl.org>.
-
-=cut
-
diff --git a/modules/fallback/Email/Address.pm b/modules/fallback/Email/Address.pm
deleted file mode 100644 (file)
index 5fb84e8..0000000
+++ /dev/null
@@ -1,564 +0,0 @@
-package Email::Address;
-use strict;
-## no critic RequireUseWarnings
-# support pre-5.6
-
-use vars qw[$VERSION $COMMENT_NEST_LEVEL $STRINGIFY
-            $COLLAPSE_SPACES
-            %PARSE_CACHE %FORMAT_CACHE %NAME_CACHE
-            $addr_spec $angle_addr $name_addr $mailbox];
-
-my $NOCACHE;
-
-$VERSION              = '1.888';
-$COMMENT_NEST_LEVEL ||= 2;
-$STRINGIFY          ||= 'format';
-$COLLAPSE_SPACES      = 1 unless defined $COLLAPSE_SPACES; # who wants //=? me!
-
-=head1 NAME
-
-Email::Address - RFC 2822 Address Parsing and Creation
-
-=head1 SYNOPSIS
-
-  use Email::Address;
-
-  my @addresses = Email::Address->parse($line);
-  my $address   = Email::Address->new(Casey => 'casey@localhost');
-
-  print $address->format;
-
-=head1 VERSION
-
-version 1.886
-
- $Id: /my/pep/Email-Address/trunk/lib/Email/Address.pm 31900 2007-06-23T01:25:34.344997Z rjbs  $
-
-=head1 DESCRIPTION
-
-This class implements a regex-based RFC 2822 parser that locates email
-addresses in strings and returns a list of C<Email::Address> objects found.
-Alternatley you may construct objects manually. The goal of this software is to
-be correct, and very very fast.
-
-=cut
-
-my $CTL            = q{\x00-\x1F\x7F};
-my $special        = q{()<>\\[\\]:;@\\\\,."};
-
-my $text           = qr/[^\x0A\x0D]/;
-
-my $quoted_pair    = qr/\\$text/;
-
-my $ctext          = qr/(?>[^()\\]+)/;
-my ($ccontent, $comment) = (q{})x2;
-for (1 .. $COMMENT_NEST_LEVEL) {
-  $ccontent = qr/$ctext|$quoted_pair|$comment/;
-  $comment  = qr/\s*\((?:\s*$ccontent)*\s*\)\s*/;
-}
-my $cfws           = qr/$comment|\s+/;
-
-my $atext          = qq/[^$CTL$special\\s]/;
-my $atom           = qr/$cfws*$atext+$cfws*/;
-my $dot_atom_text  = qr/$atext+(?:\.$atext+)*/;
-my $dot_atom       = qr/$cfws*$dot_atom_text$cfws*/;
-
-my $qtext          = qr/[^\\"]/;
-my $qcontent       = qr/$qtext|$quoted_pair/;
-my $quoted_string  = qr/$cfws*"$qcontent+"$cfws*/;
-
-my $word           = qr/$atom|$quoted_string/;
-
-# XXX: This ($phrase) used to just be: my $phrase = qr/$word+/; It was changed
-# to resolve bug 22991, creating a significant slowdown.  Given current speed
-# problems.  Once 16320 is resolved, this section should be dealt with.
-# -- rjbs, 2006-11-11
-#my $obs_phrase     = qr/$word(?:$word|\.|$cfws)*/;
-
-# XXX: ...and the above solution caused endless problems (never returned) when
-# examining this address, now in a test:
-#   admin+=E6=96=B0=E5=8A=A0=E5=9D=A1_Weblog-- ATAT --test.socialtext.com
-# So we disallow the hateful CFWS in this context for now.  Of modern mail
-# agents, only Apple Web Mail 2.0 is known to produce obs-phrase.
-# -- rjbs, 2006-11-19
-my $simple_word    = qr/$atom|\.|\s*"$qcontent+"\s*/;
-my $obs_phrase     = qr/$simple_word+/;
-
-my $phrase         = qr/$obs_phrase|(?:$word+)/;
-
-my $local_part     = qr/$dot_atom|$quoted_string/;
-my $dtext          = qr/[^\[\]\\]/;
-my $dcontent       = qr/$dtext|$quoted_pair/;
-my $domain_literal = qr/$cfws*\[(?:\s*$dcontent)*\s*\]$cfws*/;
-my $domain         = qr/$dot_atom|$domain_literal/;
-
-my $display_name   = $phrase;
-
-=head2 Package Variables
-
-Several regular expressions used in this package are useful to others.
-For convenience, these variables are declared as package variables that
-you may access from your program.
-
-These regular expressions conform to the rules specified in RFC 2822.
-
-You can access these variables using the full namespace. If you want
-short names, define them yourself.
-
-  my $addr_spec = $Email::Address::addr_spec;
-
-=over 4
-
-=item $Email::Address::addr_spec
-
-This regular expression defined what an email address is allowed to
-look like.
-
-=item $Email::Address::angle_addr
-
-This regular expression defines an C<$addr_spec> wrapped in angle
-brackets.
-
-=item $Email::Address::name_addr
-
-This regular expression defines what an email address can look like
-with an optional preceeding display name, also known as the C<phrase>.
-
-=item $Email::Address::mailbox
-
-This is the complete regular expression defining an RFC 2822 emial
-address with an optional preceeding display name and optional
-following comment.
-
-=back
-
-=cut
-
-$addr_spec  = qr/$local_part\@$domain/;
-$angle_addr = qr/$cfws*<$addr_spec>$cfws*/;
-$name_addr  = qr/$display_name?$angle_addr/;
-$mailbox    = qr/(?:$name_addr|$addr_spec)$comment*/;
-
-sub _PHRASE   () { 0 }
-sub _ADDRESS  () { 1 }
-sub _COMMENT  () { 2 }
-sub _ORIGINAL () { 3 }
-sub _IN_CACHE () { 4 }
-
-=head2 Class Methods
-
-=over 4
-
-=item parse
-
-  my @addrs = Email::Address->parse(
-    q[me@local, Casey <me@local>, "Casey" <me@local> (West)]
-  );
-
-This method returns a list of C<Email::Address> objects it finds
-in the input string.
-
-The specification for an email address allows for infinitley
-nestable comments. That's nice in theory, but a little over done.
-By default this module allows for two (C<2>) levels of nested
-comments. If you think you need more, modify the
-C<$Email::Address::COMMENT_NEST_LEVEL> package variable to allow
-more.
-
-  $Email::Address::COMMENT_NEST_LEVEL = 10; # I'm deep
-
-The reason for this hardly limiting limitation is simple: efficiency.
-
-Long strings of whitespace can be problematic for this module to parse, a bug
-which has not yet been adequately addressed.  The default behavior is now to
-collapse multiple spaces into a single space, which avoids this problem.  To
-prevent this behavior, set C<$Email::Address::COLLAPSE_SPACES> to zero.  This
-variable will go away when the bug is resolved properly.
-
-=cut
-
-sub __get_cached_parse {
-    return if $NOCACHE;
-
-    my ($class, $line) = @_;
-
-    return @{$PARSE_CACHE{$line}} if exists $PARSE_CACHE{$line};
-    return; 
-}
-
-sub __cache_parse {
-    return if $NOCACHE;
-    
-    my ($class, $line, $addrs) = @_;
-
-    $PARSE_CACHE{$line} = $addrs;
-}
-
-sub parse {
-    my ($class, $line) = @_;
-    return unless $line;
-
-    $line =~ s/[ \t]+/ /g if $COLLAPSE_SPACES;
-
-    if (my @cached = $class->__get_cached_parse($line)) {
-        return @cached;
-    }
-
-    my (@mailboxes) = ($line =~ /$mailbox/go);
-    my @addrs;
-    foreach (@mailboxes) {
-      my $original = $_;
-
-      my @comments = /($comment)/go;
-      s/$comment//go if @comments;
-
-      my ($user, $host, $com);
-      ($user, $host) = ($1, $2) if s/<($local_part)\@($domain)>//o;
-      if (! defined($user) || ! defined($host)) {
-          s/($local_part)\@($domain)//o;
-          ($user, $host) = ($1, $2);
-      }
-
-      my ($phrase)       = /($display_name)/o;
-
-      for ( $phrase, $host, $user, @comments ) {
-        next unless defined $_;
-        s/^\s+//;
-        s/\s+$//;
-        $_ = undef unless length $_;
-      }
-
-      my $new_comment = join q{ }, @comments;
-      push @addrs,
-        $class->new($phrase, "$user\@$host", $new_comment, $original);
-      $addrs[-1]->[_IN_CACHE] = [ \$line, $#addrs ]
-    }
-
-    $class->__cache_parse($line, \@addrs);
-    return @addrs;
-}
-
-=pod
-
-=item new
-
-  my $address = Email::Address->new(undef, 'casey@local');
-  my $address = Email::Address->new('Casey West', 'casey@local');
-  my $address = Email::Address->new(undef, 'casey@local', '(Casey)');
-
-Constructs and returns a new C<Email::Address> object. Takes four
-positional arguments: phrase, email, and comment, and original string.
-
-The original string should only really be set using C<parse>.
-
-=cut
-
-sub new { bless [@_[1..4]], $_[0] }
-
-=pod
-
-=item purge_cache
-
-  Email::Address->purge_cache;
-
-One way this module stays fast is with internal caches. Caches live
-in memory and there is the remote possibility that you will have a
-memory problem. In the off chance that you think you're one of those
-people, this class method will empty those caches.
-
-I've loaded over 12000 objects and not encountered a memory problem.
-
-=cut
-
-sub purge_cache {
-    %NAME_CACHE   = ();
-    %FORMAT_CACHE = ();
-    %PARSE_CACHE  = ();
-}
-
-=item disable_cache
-
-=item enable_cache
-
-  Email::Address->disable_cache if memory_low();
-
-If you'd rather not cache address parses at all, you can disable (and reenable) the Email::Address cache with these methods.  The cache is enabled by default.
-
-=cut
-
-sub disable_cache {
-  my ($class) = @_;
-  $class->purge_cache;
-  $NOCACHE = 1;
-}
-
-sub enable_cache {
-  $NOCACHE = undef;
-}
-
-=pod
-
-=back
-
-=head2 Instance Methods
-
-=over 4
-
-=item phrase
-
-  my $phrase = $address->phrase;
-  $address->phrase( "Me oh my" );
-
-Accessor and mutator for the phrase portion of an address.
-
-=item address
-
-  my $addr = $address->address;
-  $addr->address( "me@PROTECTED.com" );
-
-Accessor and mutator for the address portion of an address.
-
-=item comment
-
-  my $comment = $address->comment;
-  $address->comment( "(Work address)" );
-
-Accessor and mutator for the comment portion of an address.
-
-=item original
-
-  my $orig = $address->original;
-
-Accessor for the original address found when parsing, or passed
-to C<new>.
-
-=item host
-
-  my $host = $address->host;
-
-Accessor for the host portion of an address's address.
-
-=item user
-
-  my $user = $address->user;
-
-Accessor for the user portion of an address's address.
-
-=cut
-
-BEGIN {
-  my %_INDEX = (
-    phrase   => _PHRASE,
-    address  => _ADDRESS,
-    comment  => _COMMENT,
-    original => _ORIGINAL,
-  );
-
-  for my $method (keys %_INDEX) {
-    no strict 'refs';
-    my $index = $_INDEX{ $method };
-    *$method = sub {
-      if ($_[1]) {
-        if ($_[0][_IN_CACHE]) {
-          my $replicant = bless [ @{$_[0]} ] => ref $_[0];
-          $PARSE_CACHE{ ${ $_[0][_IN_CACHE][0] } }[ $_[0][_IN_CACHE][1] ] 
-            = $replicant;
-          $_[0][_IN_CACHE] = undef;
-        }
-        $_[0]->[ $index ] = $_[1];
-      } else {
-        $_[0]->[ $index ];
-      }
-    };
-  }
-}
-
-sub host { ($_[0]->[_ADDRESS] =~ /\@($domain)/o)[0]     }
-sub user { ($_[0]->[_ADDRESS] =~ /($local_part)\@/o)[0] }
-
-=pod
-
-=item format
-
-  my $printable = $address->format;
-
-Returns a properly formatted RFC 2822 address representing the
-object.
-
-=cut
-
-sub format {
-    local $^W = 0; ## no critic
-    return $FORMAT_CACHE{"@{$_[0]}"} if exists $FORMAT_CACHE{"@{$_[0]}"};
-    $FORMAT_CACHE{"@{$_[0]}"} = $_[0]->_format;
-}
-
-sub _format {
-    my ($self) = @_;
-
-    unless (
-      defined $self->[_PHRASE] && length $self->[_PHRASE]
-      ||
-      defined $self->[_COMMENT] && length $self->[_COMMENT]
-    ) {
-        return $self->[_ADDRESS];
-    }
-
-    my $format = sprintf q{%s <%s> %s},
-                 $self->_enquoted_phrase, $self->[_ADDRESS], $self->[_COMMENT];
-
-    $format =~ s/^\s+//;
-    $format =~ s/\s+$//;
-
-    return $format;
-}
-
-sub _enquoted_phrase {
-  my ($self) = @_;
-
-  my $phrase = $self->[_PHRASE];
-
-  # if it's encoded -- rjbs, 2007-02-28
-  return $phrase if $phrase =~ /\A=\?.+\?=\z/;
-
-  $phrase =~ s/\A"(.+)"\z/$1/;
-  $phrase =~ s/\"/\\"/g;
-
-  return qq{"$phrase"};
-}
-
-=pod
-
-=item name
-
-  my $name = $address->name;
-
-This method tries very hard to determine the name belonging to the address.
-First the C<phrase> is checked. If that doesn't work out the C<comment>
-is looked into. If that still doesn't work out, the C<user> portion of
-the C<address> is returned.
-
-This method does B<not> try to massage any name it identifies and instead
-leaves that up to someone else. Who is it to decide if someone wants their
-name capitalized, or if they're Irish?
-
-=cut
-
-sub name {
-    local $^W = 0;
-    return $NAME_CACHE{"@{$_[0]}"} if exists $NAME_CACHE{"@{$_[0]}"};
-    my ($self) = @_;
-    my $name = q{};
-    if ( $name = $self->[_PHRASE] ) {
-        $name =~ s/^"//;
-        $name =~ s/"$//;
-        $name =~ s/($quoted_pair)/substr $1, -1/goe;
-    } elsif ( $name = $self->[_COMMENT] ) {
-        $name =~ s/^\(//;
-        $name =~ s/\)$//;
-        $name =~ s/($quoted_pair)/substr $1, -1/goe;
-        $name =~ s/$comment/ /go;
-    } else {
-        ($name) = $self->[_ADDRESS] =~ /($local_part)\@/o;
-    }
-    $NAME_CACHE{"@{$_[0]}"} = $name;
-}
-
-=pod
-
-=back
-
-=head2 Overloaded Operators
-
-=over 4
-
-=item stringify
-
-  print "I have your email address, $address.";
-
-Objects stringify to C<format> by default. It's possible that you don't
-like that idea. Okay, then, you can change it by modifying
-C<$Email:Address::STRINGIFY>. Please consider modifying this package
-variable using C<local>. You might step on someone else's toes if you
-don't.
-
-  {
-    local $Email::Address::STRINGIFY = 'address';
-    print "I have your address, $address.";
-    #   geeknest.com
-  }
-  print "I have your address, $address.";
-  #   "Casey West" <casey@geeknest.com>
-
-=cut
-
-sub as_string {
-  warn 'altering $Email::Address::STRINGIFY is deprecated; subclass instead'
-    if $STRINGIFY ne 'format';
-
-  $_[0]->can($STRINGIFY)->($_[0]);
-}
-
-use overload '""' => 'as_string';
-
-=pod
-
-=back
-
-=cut
-
-1;
-
-__END__
-
-=head2 Did I Mention Fast?
-
-On his 1.8GHz Apple MacBook, rjbs gets these results:
-
-  $ perl -Ilib bench/ea-vs-ma.pl bench/corpus.txt 5 
-                   Rate  Mail::Address Email::Address
-  Mail::Address  2.59/s             --           -44%
-  Email::Address 4.59/s            77%             --
-
-  $ perl -Ilib bench/ea-vs-ma.pl bench/corpus.txt 25
-                   Rate  Mail::Address Email::Address
-  Mail::Address  2.58/s             --           -67%
-  Email::Address 7.84/s           204%             --
-
-  $ perl -Ilib bench/ea-vs-ma.pl bench/corpus.txt 50
-                   Rate  Mail::Address Email::Address
-  Mail::Address  2.57/s             --           -70%
-  Email::Address 8.53/s           232%             --
-
-...unfortunately, a known bug causes a loss of speed the string to parse has
-certain known characteristics, and disabling cache will also degrade
-performance.
-
-=head1 PERL EMAIL PROJECT
-
-This module is maintained by the Perl Email Project
-
-L<http://emailproject.perl.org/wiki/Email::Address>
-
-=head1 SEE ALSO
-
-L<Email::Simple>, L<perl>.
-
-=head1 AUTHOR
-
-Originally by Casey West, <F<casey@geeknest.com>>.
-
-Maintained, 2006-2007, Ricardo SIGNES <F<rjbs@cpan.org>>.
-
-=head1 ACKNOWLEDGEMENTS
-
-Thanks to Kevin Riggle and Tatsuhiko Miyagawa for tests for annoying phrase-quoting bugs!
-
-=head1 COPYRIGHT
-
-Copyright (c) 2004 Casey West.  All rights reserved.  This module is free
-software; you can redistribute it and/or modify it under the same terms as Perl
-itself.
-
-=cut
-
diff --git a/modules/fallback/Exception/Lite.pm b/modules/fallback/Exception/Lite.pm
deleted file mode 100644 (file)
index 5f467e6..0000000
+++ /dev/null
@@ -1,527 +0,0 @@
-# Copyright (c) 2010 Elizabeth Grace Frank-Backman.
-# All rights reserved.
-# Liscenced under the "Artistic Liscence"
-# (see http://dev.perl.org/licenses/artistic.html)
-
-use 5.8.8;
-use strict;
-use warnings;
-use overload;
-
-package Exception::Lite;
-our @ISA = qw(Exporter);
-our @EXPORT_OK=qw(declareExceptionClass isException isChainable
-                  onDie onWarn);
-our %EXPORT_TAGS
-  =( common => [qw(declareExceptionClass isException isChainable)]
-     , all => [@EXPORT_OK]
-   );
-my $CLASS='Exception::Lite';
-
-#------------------------------------------------------------------
-
-our $STRINGIFY=3;
-our $FILTER=1;
-our $UNDEF='<undef>';
-our $TAB=3;
-our $LINE_LENGTH=120;
-
-# provide command line control over amount and layout of debugging
-# information, e.g. perl -mException::Lite=STRINGIFY=4
-
-sub import {
-  Exception::Lite->export_to_level(1, grep {
-    if (/^(\w+)=(.*)$/) {
-      my $k = $1;
-      my $v = $2;
-      if ($k eq 'STRINGIFY')        { $STRINGIFY=$v;
-      } elsif ($k eq 'FILTER')      { $FILTER=$v;
-      } elsif ($k eq 'LINE_LENGTH') { $LINE_LENGTH=$v;
-      } elsif ($k eq 'TAB')         { $TAB=$v;
-      }
-      0;
-    } else {
-      1;
-    }
-  } @_);
-}
-
-#------------------------------------------------------------------
-# Note to source code divers: DO NOT USE THIS. This is intended for
-# internal use but must be declared with "our" because we need to
-# localize it.  This is an implementation detail and cannot be relied
-# on for future releases.
-
-our $STACK_OFFSET=0;
-
-#------------------------------------------------------------------
-
-use Scalar::Util ();
-use constant EVAL => '(eval)';
-
-#==================================================================
-# EXPORTABLE FUNCTIONS
-#==================================================================
-
-sub declareExceptionClass {
-  my ($sClass, $sSuperClass, $xFormatRule, $bCustomizeSubclass) = @_;
-  my $sPath = $sClass; $sPath =~ s/::/\//g; $sPath .= '.pm';
-  if ($INC{$sPath}) {
-    # we want to start with the caller's frame, not ours
-    local $STACK_OFFSET = $STACK_OFFSET + 1;
-    die 'Exception::Lite::Any'->new("declareExceptionClass failed: "
-                                    . "$sClass is already defined!");
-    return undef;
-  }
-
-  my $sRef=ref($sSuperClass);
-  if ($sRef) {
-    $bCustomizeSubclass = $xFormatRule;
-    $xFormatRule = $sSuperClass;
-    $sSuperClass=undef;
-  } else {
-    $sRef = ref($xFormatRule);
-    if (!$sRef && defined($xFormatRule)) {
-      $bCustomizeSubclass = $xFormatRule;
-      $xFormatRule = undef;
-    }
-  }
-
-  # set up things dependent on whether or not the class has a
-  # format string or expects a message for each instance
-
-  my ($sLeadingParams, $sAddOrOmit, $sRethrowMsg, $sMakeMsg);
-  my $sReplaceMsg='';
-
-  if ($sRef) {
-    $sLeadingParams='my $e; $e=shift if ref($_[0]);';
-    $sAddOrOmit='added an unnecessary message or format';
-    $sRethrowMsg='';
-
-    #generate format rule
-    $xFormatRule=$xFormatRule->($sClass) if ($sRef eq 'CODE');
-
-    my $sFormat= 'q{' . $xFormatRule->[0] . '}';
-    if (scalar($xFormatRule) == 1) {
-      $sMakeMsg='my $msg='.$sFormat;
-    } else {
-      my $sSprintf = 'Exception::Lite::_sprintf(' . $sFormat
-        . ', map {defined($_)?$_:\''. $UNDEF .'\'} @$h{qw('
-        . join(' ', @$xFormatRule[1..$#$xFormatRule]) . ')});';
-      $sMakeMsg='my $msg='.$sSprintf;
-      $sReplaceMsg='$_[0]->[0]='.$sSprintf;
-    }
-
-  } else {
-    $sLeadingParams = 'my $e=shift; my $msg;'.
-      'if(ref($e)) { $msg=shift; $msg=$e->[0] if !defined($msg);}'.
-      'else { $msg=$e;$e=undef; }';
-    $sAddOrOmit='omitted a required message';
-    $sRethrowMsg='my $msg=shift; $_[0]->[0]=$msg if defined($msg);';
-    $sMakeMsg='';
-  }
-
-  # put this in an eval so that it doesn't cause parse errors at
-  # compile time in no-threads versions of Perl
-
-  my $sTid = eval q{defined(&threads::tid)?'threads->tid':'undef'};
-
-  my $sDeclare = "package $sClass;".
-    'sub new { my $cl=shift;'.  $sLeadingParams .
-      'my $st=Exception::Lite::_cacheStackTrace($e);'.
-      'my $h= Exception::Lite::_shiftProperties($cl' .
-         ',$st,"'.$sAddOrOmit.'",@_);' . $sMakeMsg .
-      'my $self=bless([$msg,$h,$st,$$,'.$sTid.',$e,[]],$cl);';
-
-  # the remainder depends on the type of subclassing
-
-  if ($bCustomizeSubclass) {
-    $sDeclare .= '$self->[7]={}; $self->_new(); return $self; }'
-      . 'sub _p_getSubclassData { $_[0]->[7]; }';
-  } else {
-    $sDeclare .= 'return $self;}'.
-    'sub replaceProperties {'.
-       'my $h={%{$_[0]->[1]},%{$_[1]}}; $_[0]->[1]=$h;'.$sReplaceMsg.
-    '}'.
-    'sub rethrow {' .
-      'my $self=shift;' . $sRethrowMsg .
-      'Exception::Lite::_rethrow($self,"'.$sAddOrOmit.'",@_)' .
-    '}';
-
-    unless (isExceptionClass($sSuperClass)) {
-      $sDeclare .=
-        'sub _getInterface { \'Exception::Lite\' }' .
-        'sub getMessage { $_[0]->[0] };' .
-        'sub getProperty { $_[0]->[1]->{$_[1]} }' .
-        'sub isProperty { exists($_[0]->[1]->{$_[1]})?1:0 }' .
-        'sub getStackTrace { $_[0]->[2] }' .
-        'sub getFrameCount { scalar(@{$_[0]->[2]}); }' .
-        'sub getFile { $_[0]->[2]->[ $_[1]?$_[1]:0 ]->[0] };' .
-        'sub getLine { $_[0]->[2]->[ $_[1]?$_[1]:0 ]->[1] };' .
-        'sub getSubroutine { $_[0]->[2]->[ $_[1]?$_[1]:0 ]->[2] };' .
-        'sub getArgs { $_[0]->[2]->[ $_[1]?$_[1]:0 ]->[3] };' .
-        'sub getPackage {$_[0]->[2]->[-1]->[2] =~ /(\w+)>$/;$1}'.
-        'sub getPid { $_[0]->[3] }' .
-        'sub getTid { $_[0]->[4] }' .
-        'sub getChained { $_[0]->[5] }' .
-        'sub getPropagation { $_[0]->[6]; }' .
-        'use overload '.
-           'q{""} => \&Exception::Lite::_dumpMessage ' .
-           ', q{0+} => \&Exception::Lite::_refaddr, fallback=>1;' .
-        'sub PROPAGATE { push @{$_[0]->[6]},[$_[1],$_[2]]; $_[0]}';
-    }
-  }
-  $sDeclare .= 'return 1;';
-
-  local $SIG{__WARN__} = sub {
-    my ($p,$f,$l) = caller(2);
-    my $s=$_[0]; $s =~ s/at \(eval \d+\)\s+line\s+\d+\.//m;
-    print STDERR "$s in declareExceptionClass($sClass,...) "
-      ."in file $f, line $l\n";
-  };
-
-  eval $sDeclare or do {
-    my ($p,$f,$l) = caller(1);
-    print STDERR "Can't create class $sClass at file $f, line $l\n";
-    if ($sClass =~ /\w:\w/) {
-      print STDERR "Bad class name: "
-        ."At least one ':' is not doubled\n";
-    } elsif ($sClass !~ /^\w+(?:::\w+)*$/) {
-      print STDERR "Bad class name: $sClass\n";
-    } else {
-      $sDeclare=~s/(sub |use )/\n$1/g; print STDERR "$sDeclare\n";
-    }
-  };
-
-  # this needs to be separate from the eval, otherwise it never
-  # ends up in @INC or @ISA, at least in Perl 5.8.8
-  $INC{$sPath} = __FILE__;
-  eval "\@${sClass}::ISA=qw($sSuperClass);" if $sSuperClass;
-
-  return $sClass;
-}
-
-#------------------------------------------------------------------
-
-sub isChainable { return ref($_[0])?1:0; }
-
-#------------------------------------------------------------------
-
-sub isException {
-  my ($e, $sClass) = @_;
-  my $sRef=ref($e);
-  return !defined($sClass)
-    ? ($sRef ? isExceptionClass($sRef) : 0)
-    : $sClass eq ''
-       ? ($sRef eq '' ? 1 : 0)
-       : ($sRef eq '')
-            ? 0
-            : $sRef->isa($sClass)
-               ?1:0;
-}
-
-#------------------------------------------------------------------
-
-sub isExceptionClass {
-  return defined($_[0]) && $_[0]->can('_getInterface')
-    && ($_[0]->_getInterface() eq __PACKAGE__) ? 1 : 0;
-}
-
-#------------------------------------------------------------------
-
-sub onDie {
-  my $iStringify = $_[0];
-  $SIG{__DIE__} = sub {
-    $Exception::Lite::STRINGIFY=$iStringify;
-    warn 'Exception::Lite::Any'->new('Unexpected death:'.$_[0])
-      unless $^S || isException($_[0]);
-  };
-}
-
-#------------------------------------------------------------------
-
-sub onWarn {
-  my $iStringify = $_[0];
-  $SIG{__WARN__} = sub {
-    $Exception::Lite::STRINGIFY=$iStringify;
-    print STDERR 'Exception::Lite::Any'->new("Warning: $_[0]");
-  };
-}
-
-#==================================================================
-# PRIVATE SUBROUTINES
-#==================================================================
-
-#------------------------------------------------------------------
-
-sub _cacheCall {
-  my $iFrame = $_[0];
-
-  my @aCaller;
-  my $aArgs;
-
-  # caller populates @DB::args if called within DB package
-  eval {
-    # this 2 line wierdness is needed to prevent Module::Build from finding
-    # this and adding it to the provides list.
-    package
-      DB;
-
-    #get rid of eval and call to _cacheCall
-    @aCaller = caller($iFrame+2);
-
-    # mark leading undefined elements as maybe shifted away
-    my $iDefined;
-    if ($#aCaller < 0) {
-      @DB::args=@ARGV;
-    }
-    $aArgs = [  map {
-      defined($_)
-        ? do {$iDefined=1;
-              "'$_'" . (overload::Method($_,'""')
-                        ? ' ('.overload::StrVal($_).')':'')}
-          : 'undef' . (defined($iDefined)
-                       ? '':'  (maybe shifted away?)')
-        } @DB::args];
-  };
-
-  return $#aCaller < 0 ? \$aArgs : [ @aCaller[0..3], $aArgs ];
-}
-
-#------------------------------------------------------------------
-
-sub _cacheStackTrace {
-  my $e=$_[0]; my $st=[];
-
-  # set up initial frame
-  my $iFrame= $STACK_OFFSET + 1; # call to new
-  my $aCall = _cacheCall($iFrame++);
-  my ($sPackage, $iFile, $iLine, $sSub, $sArgs) = @$aCall;
-  my $iLineFrame=$iFrame;
-
-  $aCall =  _cacheCall($iFrame++);  #context of call to new
-  while (ref($aCall) ne 'REF') {
-    $sSub  = $aCall->[3];  # subroutine containing file,line
-    $sArgs = $aCall->[4];  # args used to call $sSub
-
-    #print STDERR "debug-2: package=$sPackage file=$iFile line=$iLine"
-    #  ." sub=$sSub, args=@$sArgs\n";
-
-    # in evals we want the line number within the eval, but the
-    # name of the sub in which the eval was located. To get this
-    # we wait to push on the stack until we get an actual sub name
-    # and we avoid overwriting the location information, hence 'ne'
-
-    if (!$FILTER || ($sSub ne EVAL)) {
-      my $aFrame=[ $iFile, $iLine, $sSub, $sArgs ];
-      ($sPackage, $iFile, $iLine) = @$aCall;
-      $iLineFrame=$iFrame;
-
-      my $sRef=ref($FILTER);
-      if ($sRef eq 'CODE') {
-        my $x = $FILTER->(@$aFrame, $iFrame, $iLineFrame);
-        if (ref($x) eq 'ARRAY') {
-          $aFrame=$x;
-        } elsif (!$x) {
-          $aFrame=undef;
-        }
-      } elsif (($sRef eq 'ARRAY') && ! _isIgnored($sSub, $FILTER)) {
-        $aFrame=undef;
-      } elsif (($sRef eq 'Regexp') && !_isIgnored($sSub, [$FILTER])) {
-        $aFrame=undef;
-      }
-      push(@$st, $aFrame) if $aFrame;
-    }
-
-    $aCall = _cacheCall($iFrame++);
-  }
-
-  push @$st, [ $iFile, $iLine, "<package: $sPackage>", $$aCall ];
-  if ($e) { my $n=$#{$e->[2]}-$#$st;$e->[2]=[@{$e->[2]}[0..$n]]};
-  return $st;
-}
-
-#-----------------------------
-
-sub _isIgnored {
-  my ($sSub, $aIgnore) = @_;
-  foreach my $re (@$aIgnore) { return 1 if $sSub =~ $re; }
-  return 0;
-}
-
-#------------------------------------------------------------------
-
-sub _dumpMessage {
-  my ($e, $iDepth) = @_;
-
-  my $sMsg = $e->getMessage();
-  return $sMsg unless $STRINGIFY;
-  if (ref($STRINGIFY) eq 'CODE') {
-    return $STRINGIFY->($sMsg);
-  }
-
-  $iDepth = 0 unless defined($iDepth);
-  my $sIndent = ' ' x ($TAB*$iDepth);
-  $sMsg = "\n${sIndent}Exception! $sMsg";
-  return $sMsg if $STRINGIFY == 0;
-
-  my ($sThrow, $sReach);
-  my $sTab = ' ' x $TAB;
-
-  $sIndent.= $sTab;
-  if ($STRINGIFY > 2) {
-    my $aPropagation = $e->getPropagation();
-    for (my $i=$#$aPropagation; $i >= 0; $i--) {
-      my ($f,$l) = @{$aPropagation->[$i]};
-      $sMsg .= "\n${sIndent}rethrown at file $f, line $l";
-    }
-    $sMsg .= "\n";
-    $sThrow='thrown  ';
-    $sReach='reached ';
-  } else {
-    $sThrow='';
-    $sReach='';
-  }
-
-  my $st=$e->getStackTrace();
-  my $iTop = scalar @$st;
-
-  for (my $iFrame=0; $iFrame<$iTop; $iFrame++) {
-    my ($f,$l,$s,$aArgs) = @{$st->[$iFrame]};
-
-    if ($iFrame) {
-      #2nd and following stack frame
-      my $sVia="${sIndent}${sReach}via file $f, line $l";
-      my $sLine="$sVia in $s";
-      $sMsg .= (length($sLine)>$LINE_LENGTH
-                ? "\n$sVia\n$sIndent${sTab}in $s" : "\n$sLine");
-    } else {
-      # first stack frame
-      my $tid=$e->getTid();
-      my $sAt="${sIndent}${sThrow}at  file $f, line $l";
-      my $sLine="$sAt in $s";
-      $sMsg .= (length($sLine)>$LINE_LENGTH
-                ? "\n$sAt\n$sIndent${sTab}in $s" : "\n$sLine")
-        . ", pid=" . $e->getPid() . (defined($tid)?", tid=$tid":'');
-
-      return "$sMsg\n" if $STRINGIFY == 1;
-    }
-
-    if ($STRINGIFY > 3) {
-      my $bTop = ($iFrame+1) == $iTop;
-      my $sVar= ($bTop && !$iDepth) ? '@ARGV' : '@_';
-      my $bMaybeEatenByGetOpt = $bTop && !scalar(@$aArgs)
-        && exists($INC{'Getopt/Long.pm'});
-
-      my $sVarIndent = "\n${sIndent}" . (' ' x $TAB);
-      my $sArgPrefix = "${sVarIndent}".(' ' x length($sVar)).' ';
-      if ($bMaybeEatenByGetOpt) {
-        $sMsg .= $sArgPrefix . $sVar
-          . '()    # maybe eaten by Getopt::Long?';
-      } else {
-        my $sArgs = join($sArgPrefix.',', @$aArgs);
-        $sMsg .= "${sVarIndent}$sVar=($sArgs";
-        $sMsg .= $sArgs ? "$sArgPrefix)" : ')';
-      }
-    }
-  }
-  $sMsg.="\n";
-  return $sMsg if $STRINGIFY == 2;
-
-  my $eChained = $e->getChained();
-  if (defined($eChained)) {
-    my $sTrigger = isException($eChained)
-      ? _dumpMessage($eChained, $iDepth+1)
-      : "\n${sIndent}$eChained\n";
-    $sMsg .= "\n${sIndent}Triggered by...$sTrigger";
-  }
-  return $sMsg;
-}
-
-#------------------------------------------------------------------
-
-# refaddr has a prototype($) so we can't use it directly as an
-# overload operator: it complains about being passed 3 parameters
-# instead of 1.
-sub _refaddr { Scalar::Util::refaddr($_[0]) };
-
-#------------------------------------------------------------------
-
-sub _rethrow {
-  my $self = shift; my $sAddOrOmit = shift;
-  my ($p,$f,$l)=caller(1);
-  $self->PROPAGATE($f,$l);
-
-  if (@_%2) {
-    warn sprintf('bad parameter list to %s->rethrow(...)'
-      .'at file %d, line %d: odd number of elements in property-value '
-      .'list, property value has no property name and will be '
-      ."discarded (common causes: you have %s string)\n"
-      ,$f, $l, $sAddOrOmit);
-    shift @_;
-  }
-  $self->replaceProperties({@_}) if (@_);
-  return $self;
-}
-
-#------------------------------------------------------------------
-# Traps warnings and reworks them so that they tell the user how
-# to fix the problem rather than obscurely complain about an
-# invisible sprintf with uninitialized values that seem to come from
-# no where (and make Exception::Lite look like it is broken)
-
-sub _sprintf {
-  my $sMsg;
-  my $sWarn;
-
-  {
-    local $SIG{__WARN__} = sub { $sWarn=$_[0] if !defined($sWarn) };
-
-    # sprintf has prototype ($@)
-    my $sFormat = shift;
-    $sMsg = sprintf($sFormat, @_);
-  }
-
-  if (defined($sWarn)) {
-    my $sReason='';
-    my ($f, $l, $s) = (caller(1))[1,2,3];
-    $s =~ s/::(\w+)\z/->$1/;
-    $sWarn =~ s/sprintf/$s/;
-    $sWarn =~ s/\s+at\s+[\w\/\.]+\s+line\s+\d+\.\s+\z//;
-    if ($sWarn
-        =~ m{^Use of uninitialized value in|^Missing argument}) {
-      my $p=$s; $p =~ s/->\w+\z//;
-      $sReason ="\n     Most likely cause: "
-        . "Either you are missing property-value pairs needed to"
-        . "build the message or your exception class's format"
-        . "definition mistakenly has too many placeholders "
-        . "(e.g. %s,%d,etc)\n";
-    }
-    warn "$sWarn called at file $f, line $l$sReason\n";
-  }
-  return $sMsg;
-}
-
-#------------------------------------------------------------------
-
-sub _shiftProperties {
-  my $cl= shift;  my $st=shift;  my $sAddOrOmit = shift;
-  if (@_%2) {
-    $"='|';
-    warn sprintf('bad parameter list to %s->new(...) at '
-      .'file %s, line %d: odd number of elements in property-value '
-      .'list, property value has no property name and will be '
-      .'discarded (common causes: you have %s string -or- you are '
-      ."using a string as a chained exception)\n"
-      ,$cl,$st->[0]->[0],$st->[0]->[1], $sAddOrOmit);
-    shift @_;
-  }
-  return {@_};
-}
-
-#==================================================================
-# MODULE INITIALIZATION
-#==================================================================
-
-declareExceptionClass(__PACKAGE__ .'::Any');
-1;
diff --git a/modules/fallback/Exception/Lite.pod b/modules/fallback/Exception/Lite.pod
deleted file mode 100644 (file)
index cea165f..0000000
+++ /dev/null
@@ -1,2314 +0,0 @@
-=head1 NAME
-
-Exception::Lite - light weight exception handling class with smart
-stack tracing, chaining, and localization support.
-
-=head1 SYNOPSIS
-
-   # --------------------------------------------------------
-   # making this module available to your code
-   # --------------------------------------------------------
-
-   #Note: there are NO automatic exports
-
-   use Exception::Lite qw(declareExceptionClass
-                          isException
-                          isChainable
-                          onDie
-                          onWarn);
-
-   # imports only: declareExceptionClass isException isChainable
-   use Exception::Lite qw(:common);
-
-   # imports all exportable methods listed above
-   use Exception::Lite qw(:all);
-
-
-   # --------------------------------------------------------
-   # declare an exception class
-   # --------------------------------------------------------
-
-   # no format rule
-   declareExceptionClass($sClass);
-   declareExceptionClass($sClass, $sSuperClass);
-
-   # with format rule
-   declareExceptionClass($sClass, $aFormatRule);
-   declareExceptionClass($sClass, $sSuperClass, $aFormatRule);
-
-   # with customized subclass
-   declareExceptionClass($sClass, $sSuperClass, 1);
-   declareExceptionClass($sClass, $aFormatRule, 1);
-   declareExceptionClass($sClass, $sSuperClass, $aFormatRule, 1);
-
-   # --------------------------------------------------------
-   # throw an exception
-   # --------------------------------------------------------
-
-   die $sClass->new($sMsg, $prop1 => $val1, ...);  #no format rule
-   die $sClass->new($prop1 => $val1, ...);         #has format rule
-
-   #-or-
-
-   $e = $sClass->new($sMsg, $prop1 => $val1, ...); #no format rule
-   $e = $sClass->new($prop1 => $val1, ...);        #has format rule
-
-   die $e;
-
-   # --------------------------------------------------------
-   # catch and test an exception
-   # --------------------------------------------------------
-
-   # Note: for an explanation of why we don't use if ($@)... here,
-   # see Catching and Rethrowing exceptions below
-
-   eval {
-     .... some code that may die here ...
-     return 1;
-   } or do {
-     my $e=$@;
-
-     if (isException($e, 'Class1')) {
-       ... do something ...
-     } elsif (isExcption($e, 'Class2')) {
-        ... do something else ...
-     }
-   };
-
-   isException($e);        # does $e have the above exception methods?
-   isException($e,$sClass) # does $e belong to $sClass or a subclass?
-
-   # --------------------------------------------------------
-   # getting information about an exception object
-   # --------------------------------------------------------
-
-   $e->getMessage();
-   $e->getProperty($sName);
-   $e->isProperty($sName);
-   $e->replaceProperties($hOverride);
-
-   $e->getPid();
-   $e->getPackage();
-   $e->getTid();
-
-   $e->getStackTrace();
-   $e->getFrameCount();
-   $e->getFile($i);
-   $e->getLine($i);
-   $e->getSubroutine($i);
-   $e->getArgs($i);
-
-   $e->getPropagation();
-   $e->getChained();
-
-
-   # --------------------------------------------------------
-   # rethrowing exceptions
-   # --------------------------------------------------------
-
-   # using original properties and message
-
-   $@=$e; die;         # pure Perl way (reset $@ in case wiped out)
-
-   die $e->rethrow();  # same thing, but a little less cryptic
-
-
-   # overriding original message/properties
-
-   die $e->rethrow(path=>$altpath, user=>$nameReplacingId);
-
-
-   # --------------------------------------------------------
-   # creation of chained exceptions (one triggered by another)
-   # (new exception with "memory" of what caused it and stack
-   # trace from point of cause to point of capture)
-   # --------------------------------------------------------
-
-   isChainable($e);        # can $e be used as a chained exception?
-
-   die $sClass->new($e, $sMsg, $prop1 => $val1, ...);#no format rule
-   die $sClass->new($e, $prop1 => $val1, ...);       #has format rule
-
-   # --------------------------------------------------------
-   # print out full message from an exception
-   # --------------------------------------------------------
-
-   print $e                         # print works
-   warn $e                          # warn works
-   print "$e\n";                    # double quotes work
-   my $sMsg=$e."\n"; print $sMsg;   # . operator works
-
-
-   # --------------------------------------------------------
-   # global control variables (maybe set on the command line)
-   # --------------------------------------------------------
-
-   $Exception::Lite::STRINGIFY   #set rule for stringifying messages
-
-      = 1;        # message and file/line where it occured
-      = 2;        # 1 + what called what (simplified stack trace)
-      = 3;        # 2 + plus any chained exceptions and where message
-                  # was caught, if propagated and rethrown
-      = 4;        # 3 + arguments given to each call in stack trace
-      = coderef   # custom formatting routine
-
-   $Exception::Lite::TAB   # set indentation for stringified
-                           # messages, particularly indentation for
-                           # call parameters and chained exceptions
-
-   $Exception::Lite::FILTER
-     = 0         # see stack exactly as Perl does
-     = 1         # remove frames added by eval blocks
-     = coderef   # custom filter - see getStackTrace for details
-
-   # --------------------------------------------------------
-   # controlling the stack trace from the command line
-   # --------------------------------------------------------
-
-   perl -mException::Lite=STRINGIFY=1,FILTER=0,TAB=4
-   perl -m'Exception::Lite qw(STRINGIFY=1 FILTER=0 TAB=4)'
-
-   # --------------------------------------------------------
-   # built in exception classes
-   # --------------------------------------------------------
-
-   # generic wrapper for converting exception strings and other
-   # non-Exception::Lite exceptions into exception objects
-
-   Exception::Class::Any->new($sMessageText);
-
-To assist in debugging and testing, this package also includes
-two methods that set handlers for die and warn. These methods
-should I<only> be used temporarily during active debugging. They
-should not be used in production software, least they interfere
-with the way other programmers using your module wish to do their
-debugging and testing.
-
-   # --------------------------------------------------------
-   # force all exceptions/warnings to use Exception::Lite to
-   # print out messages and stack traces
-   # --------------------------------------------------------
-
-   # $stringify is the value for EXCEPTION::Lite::STRINGIFY
-   # that you want to use locally to print out messages. It
-   # will have no effect outside of the die handler
-
-   Exception::Lite::onDie($stringify);
-   Exception::Lite::onWarn($stringify);
-
-=head1 DESCRIPTION
-
-The C<Exception::Lite> class provides an easy and very light weight
-way to generate context aware exceptions.  It was developed because
-the exception modules on CPAN as of December,2010 were heavy on
-features I didn't care for and did not have the features I most
-needed to test and debug code efficiently.
-
-=head2 Features
-
-This module provides a light weight but powerful exception class
-that
-
-=over 
-
-=item *
-
-provides an uncluttered stack trace that clearly shows what
-called what and what exception triggered what other exception.
-It significantly improves on the readability of the stack trace
-dumps provided by C<carp> and other exception modules on
-CPAN (as of 12/2010).  For further discussion and a sample, see
-L</More intelligent stack trace>.
-
-=item *
-
-gives the user full control over the amount of debugging
-information displayed when exceptions are thrown.
-
-=item *
-
-permits global changes to the amount of debugging information
-displayed via the command line.
-
-=item *
-
-closely integrates exception classes, messages, and properties
-so that they never get out of sync with one another.  This in
-turn eliminates redundant coding and helps reduce the cost of
-writing,validating and maintaining a set of exceptions.
-
-=item *
-
-is easy to retrofit with native language support, even if this
-need appears late in the development process.This makes it
-suitable for use with agile development strategies.
-
-=item *
-
-act like strings in string context but are in fact objects with
-a class hierarchy and properties.They can be thrown and rethrown
-with standard Perl syntax. Like any object, they can be uniquely
-identified in numeric context where they equal their reference
-address (the value returned by C<Scalar::Util::refaddr()>.
-
-=item *
-
-does not interfere with signal handlers or the normal Perl syntax
-and the assumptions of Perl operators.
-
-=item *
-
-can be easily extended and subclassed
-
-=back
-
-=head2 Lightweight how?
-
-Despite these features C<Exception::Lite> maintains its "lite"
-status by
-
-=over
-
-=item *
-
-using only core modules
-
-=item *
-
-generating tiny exception classes (30-45LOC per class).
-
-=item *
-
-eliminating excess baggage by customizing generated classes to
-  reflect the actual needs of exception message generation.  For
-  instance an exception wrapped around a fixed string message would
-  omit code for message/property integration and would be little
-  more than a string tied to a stack trace and property hash.
-
-=item *
-
-storing only the minimum amount of stack trace data needed to
-  generate exception messages and avoiding holding onto references
-  from dead stack frames.  (Note: some CPAN modules hold onto
-  actual variables from each frame, possibly interfering with
-  garbage collection).
-
-=item *
-
-doing all its work, including class generation and utilities in
-  a single file that is less than half the size of the next smallest
-  similarly featured all-core exception class on CPAN (support for
-  both properties and a class heirarchy).  C<Exception::Lite>
-  contains about 400 lines when developer comments are excluded). The
-  next smallest all core module is L<Exception::Base|Exception::Base>
-  which clocks in at just over 1000 lines after pod and developer
-  comments are excluded).
-
-=item *
-
-avoiding a heavy-weight base class.  Code shared by
-  C<Exception::Lite> classes are stored in function calls that total
-  230 or so lines of code relying on nothing but core modules. This
-  is significantly less code than is needed by the two CPAN packages
-  with comparable features.  The all core
-  L<Exception::Base|Exception::Base> class contains 700+ lines of
-  code.  The base class of L<Exception::Class|Exception::Class> has
-  200 lines of its own but drags in two rather large non-core
-  modules as dependencies:  L<Devel::StackTrace|Devel::StackTrace>
-  L<Class::Data::Inheritable|Class::Data::Inheritable>.
-
-=back
-
-C<Exception::Lite> has more features (chaining, message/property
-integration) but less code due to the following factors:
-
-=over
-
-=item *
-
-working with Perl syntax rather than trying to replace it.
-
-=item *
-
-using a light approach to OOP - exception classes have just enough
-and no more OO features than are needed to be categorized by a
-class, participate in a class heirarchy and to have properties.
-
-=item *
-
-respecting separation of concerns. C<Exception::Lite> focuses
-on the core responsibility of an exception and leaves the bulk of
-syntax creation (e.g. Try/Catch) to specialist modules like
-L<Try::Tiny|Try::Tiny>.  Other modules try to double as
-comprehensive providers of exception related syntactic sugar.
-
-=item *
-
-not trying to be the only kind of exception that an application
-uses.
-
-=back
-
-=head1 USAGE
-
-=head2 Defining Exception Classes
-
-C<Exception::Lite> provides two different ways to define messages.
-The first way, without a format rule, lets you compose a freeform
-message for each exception.  The second way, with a format rule,
-lets you closely integrate messages and properties and facilitates
-localization of messages for any packages using your software.
-
-=head3 Defining freeform messages
-
-If you want to compose a free form message for each and every
-exception, the class declaration is very simple:
-
-   declareExceptionClass($sClass);
-   declareExceptionClass($sClass, $sSuperClass);
-
-   # with customized subclass
-   declareExceptionClass($sClass, $sSuperClass, 1);
-
-C<$sClass> is the name of the exception class.
-
-C<$sSuperClass> is the name of the superclass, if there is one.
-The superclass can be any class created by C<Exception::Lite>. It
-can also be any role class, i.e. a class that has methods but no
-object data of its own.
-
-The downside of this simple exception class is that there is
-absolutely no integration of your messages and any properties that
-you assign to the exception.  If you would like to see your property
-values included in the message string,consider using a formatted
-message instead.
-
-=head3 Defining formatted messages
-
-If you wish to include property values in your messages, you need
-to declare a formatted message class. To do this, you define a
-format rule and pass it to the constructor:
-
-   $aFormatRule = ['Cannot copy %s to %s', qw(from to) ];
-
-   declareExceptionClass($sClass, $aFormatRule);
-   declareExceptionClass($sClass, $sSuperClass, $aFormatRule);
-
-   # with customized subclass
-   declareExceptionClass($sClass, $aFormatRule, 1);
-   declareExceptionClass($sClass, $sSuperClass, $aFormatRule, 1);
-
-Format rules are nothing more than a sprintf message string
-followed by a list of properties in the same order as the
-placeholders in the message string.  Later on when an exception
-is generated, the values of the properties will replace the
-property names.  Some more examples of format rules:
-
-
-   $aFormatRule = ['Illegal argument <%s>: %s', qw(arg reason)];
-   declareExceptionClass('BadArg', $aFormatRule);
-
-   $aFormatRule = ['Cannot open file <%s>> %s', qw(file reason)];
-   declareExceptionClass('OpenFailed', $aFormatRule);
-
-   $sFormatRule = ['Too few %s,  must be at least %s', qw(item min)];
-   declareExceptionClass('TooFewWidgets', $aFormatRule);
-
-
-Later on when you throw an exception you can forget about the message
-and set the properties, the class will do the rest of the work:
-
-    die BadArg->new(arg=>$sPassword, reason=>'Too few characters');
-
-
-    open(my $fh, '>', $sFile)
-      or die OpenFailed->new(file=>$sFile, reason=>$!);
-
-And still later when you catch the exception, you have two kinds
-of information for the price of one:
-
-    # if you catch BadArg
-
-    $e->getProperty('arg')      # mine
-    $e->getProperty('reason')   # too few characters
-    $e->getMessage()   # Illegal argument <mine>: too few characters
-
-
-    # if you catch OpenFailed
-
-    $e->getProperty('file')     # foo.txt
-    $e->getProperty('reason')   # path not found
-    $e->getMessage()   # Cannot open <foo.txt>: path not found
-
-
-=head2 Creating and throwing exceptions
-
-When it comes times to create an exception, you create and
-throw it like this (C<$sClass> is a placeholder for the name of
-your exception class);
-
-
-   die $sClass->new($sMsg, prop1 => $val1, ...);  #no format rule
-   die $sClass->new(prop1 => $val1, ...);         #has format rule
-
-   #-or-
-
-   $e = $sClass->new($sMsg, prop1 => $val1, ...); #no format rule
-   $e = $sClass->new(prop1 => $val1, ...);        #has format rule
-
-   die $e;
-
-
-For example:
-
-   # Freeform exceptions (caller composes message, has message
-   # parameter ($sMsg) before the list of properties)
-
-   close $fh or die UnexpectedException
-     ->new("Couldn't close file handle (huh?): $!");
-
-   die PropertySettingError("Couldn't set property"
-     , prop=>foo, value=>bar);
-
-   # Formatted exceptions (no $sMsg parameter)
-
-   if (length($sPassword) < 8) {
-      die BadArg->new(arg=>$sPassword, reason=>'Too few characters');
-   }
-
-   open(my $fh, '>', $sFile)
-      or die OpenFailed->new(file=>$sFile, reason=>$!);
-
-In the above examples the order of the properties does not matter.
-C<Exception::Lite> is using the property names, not the order of
-the properties to find the right value to plug into the message
-format string.
-
-=head2 Catching and testing exceptions
-
-In Perl there are two basic ways to work with exceptions:
-
-* native Perl syntax
-
-* Java like syntax (requires non-core modules)
-
-=head3 Catching exceptions the Java way
-
-Java uses the following idiom to catch exceptions:
-
-   try {
-     .... some code here ...
-  } catch (SomeExceptionClass e) {
-    ... error handling code here ...
-  } catch (SomeOtherExceptionClass e) {
-    ... error handling code here ...
-  } finally {
-    ... cleanup code here ...
-  }
-
-There are several CPAN modules that provide some sort of syntactic
-sugar so that you can emulate java syntax. The one recommended
-for C<Exception::Lite> users is L<Try::Tiny|Try::Tiny>.
-L<Try::Tiny|Try::Tiny> is an elegant class that concerns itself
-only with making it possible to use java-like syntax.  It can be
-used with any sort of exception.
-
-Some of the other CPAN modules that provide java syntax also
-require that you use their exception classes because the java like
-syntax is part of the class definition rather than a pure
-manipulation of Perl syntax.
-
-
-=head3 Catching exceptions the Perl way
-
-The most reliable and fastest way to catch an exception is to use
-C< eval/do >:
-
-   eval {
-     ...
-     return 1;
-   } or do {
-     # save $@ before using it - it can easily be clobbered
-     my $e=$@;
-
-     ... do something with the exception ...
-
-     warn $e;                 #use $e as a string
-     warn $e->getMessage();   # use $e as an object
-   }
-
-
-The C<eval> block ends with C<return 1;> to insure that successful
-completion of the eval block never results in an undefined value.
-In certain cases C<undef> is a valid return value for a statement,
-We don't want to enter the C<do> block for any reason other than
-a thrown exception.
-
-C< eval/do > is both faster and more reliable than the C< eval/if>
-which is commonly promoted in Perl programming tutorials:
-
-  # eval ... if
-
-  eval {...};
-  if ($@) {....}
-
-It is faster because the C<do> block is executed if and only
-if the eval fails. By contrast the C<if> must be evaluated both
-in cases of succes and failure.
-
-C< eval/do > is more reliable because the C<do> block is guaranteed
-to be triggered by any die, even one that accidentally throws undef
-or '' as the "exception". If an exception is thrown within the C<eval>
-block, it will always evaluate to C<undef> therefore triggering the
-C<do> block.
-
-On the other hand we can't guarentee that C<$@> will be defined
-even if an exception is thrown. If C<$@> is C<0>, C<undef>, or an
-empty string, the C<if> block will never be entered.  This happens
-more often then many programmers realize.  When eval exits the
-C< eval > block, it calls destructors of any C<my> variables. If
-any of those has an C< eval > statement, then the value of C<$@> is
-wiped clean or reset to the exception generated by the destructor.
-
-Within the C<do> block, it is a good idea to save C<$@> immediately
-into a variable before doing any additional work.  Any subroutine
-you call might also clobber it.  Even built-in commands that don't
-normally set C<$@> can because Perl lets a programmer override
-built-ins with user defined routines and those user define routines
-might set C<$@> even if the built-in does not.
-
-=head3 Testing exceptions
-
-Often when we catch an exception we want to ignore some, rethrow
-others, and in still other cases, fix the problem. Thus we need a
-way to tell what kind of exception we've caught.  C<Exception::Lite>
-provides the C<isException> method for this purpose.  It can be
-passed any exception, including scalar exceptions:
-
-   # true if this exception was generated by Exception::Line
-   isException($e);
-
-
-   # true if this exception belongs to $sClass. It may be a member
-   # of the class or a subclass.  C<$sClass> may be any class, not
-   # just an Exception::Lite generated class. You can even use this
-   # method to test for string (scalar) exceptions:
-
-   isException($e,$sClass);
-
-   isException($e,'Excption::Class');
-   isException($e, 'BadArg');
-   isException($e, '');
-
-And here is an example in action. It converts an exception to a
-warning and determines how to do it by checing the class.
-
-
-   eval {
-     ...
-     return 1;
-   } or do {
-     my $e=$@;
-     if (Exception::Lite::isException($e)) {
-
-        # get message w/o stack trace, "$e" would produce trace
-        warn $e->getMessage();
-
-     } elsif (Exception::Lite::isException('Exception::Class') {
-
-        # get message w/o stack trace, "$e" would produce trace
-        warn $e->message();
-
-     } elsif (Exception::Lite::isException($e,'')) {
-
-        warn $e;
-     }
-   }
-
-=head2 Rethrowing exceptions
-
-Perl doesn't have a C<rethrow> statement.  To reliably rethrow an
-exception, you must set C<$@> to the original exception (in case it
-has been clobbered during the error handling process) and then call
-C<die> without any arguments.
-
-   eval {
-     ...
-     return 1;
-   } or do {
-     my $e=$@;
-
-     # do some stuff
-
-     # rethrow $e
-     $@=$e; die;
-   }
-
-The above code will cause the exception's C<PROPAGATE> method to
-record the file and line number where the exception is rethrown.
-See C<getLine>, C<getFile>, and C<getPropagation> in the class
-reference below for more information.
-
-As this Perl syntax is not exactly screaming "I'm a rethrow", 
-C<Exception::Lite> provides an alternative and hopefully more
-intuitive way of propagating an exception. There is no magic here,
-it just does what perl would do had you used the normal syntax,
-i.e. call the exception's C<PROPAGATE> method.
-
-   eval {
-     ...
-     return 1;
-   } or do {
-     my $e=$@;
-
-     # rethrow $e
-     die $e->rethrow();
-   }
-
-=head2 Chaining Messages
-
-As an exception moves up the stack, its meaning may change. For
-example, suppose a subroutine throws the message "File not open".
-The immediate caller might be able to use that to try and open
-a different file.  On the other hand, if the message gets thrown
-up the stack, the fact that a file failed to open might not
-have any meaning at all.  That higher level code only cares that
-the data it needed wasn't available. When it notifies the user,
-it isn't going to say "File not found", but "Can't run market
-report: missing data feed.".
-
-When the meaning of the exception changes, it is normal to throw
-a new exception with a class and message that captures the new
-meaning. However, if this is all we do, we lose the original
-source of the problem.
-
-Enter chaining.  Chaining is the process of making one exception
-"know" what other exception caused it.  You can create a new
-exception without losing track of the original source of the
-problem.
-
-To chain exceptions is simple: just create a new exception and
-pass the caught exception as the first parameter to C<new>. So
-long as the exception is a non-scalar, it will be interpreted
-as a chained exception and not a property name or message text
-(the normal first parameter of C<new>).
-
-Chaining is efficient, especially if the chained exception is
-another C<Exception::Lite> exception. It does not replicate
-the stack trace.  Rather the original stack trace is shorted to
-include only the those fromes frome the time it was created to
-the time it was chained.
-
-Any non-scalar exception can be chained.  To test whether or not
-a caught exception is chainable, you can use the method
-C<isChainable>.  This method is really nothing more than
-a check to see if the exception is a non-scalar, but it helps
-to make your code more self documenting if you use that method
-rather than C<if (ref($e))>.
-
-If an exception isn't chainable, and you still want to chain
-it, you can wrap the exception in an exception class. You
-can use the built-in C<Exception::Class::Any> or any class of
-your own choosing.
-
-   #-----------------------------------------------------
-   # define some classes
-   #-----------------------------------------------------
-
-   # no format rule
-   declareExceptionClass('HouseholdDisaster');
-
-   # format rule
-   declareExceptionClass('ProjectDelay'
-     , ['The project was delayed % days', qw(days)]);
-
-   #-----------------------------------------------------
-   # chain some exceptins
-   #-----------------------------------------------------
-
-   eval {
-     .... some code here ...
-     return 1;
-  } or do {
-    my $e=$@;
-    if (Exception::Lite::isChainable($e)) {
-      if (Exception::Lite::isException($e, 'FooErr') {
-        die 'SomeNoFormatException'->new($e, "Caught a foo");
-      } else {
-        die 'SomeFormattedException'->new($e, when => 'today');
-      }
-    } elsif ($e =~ /fire/) {
-       die 'Exception::Lite::Any'->new($e);
-       die 'SomeFormattedException'->new($e, when => 'today');
-    } else {
-      # rethrow it since we can't chain it
-      $@=$e; die;
-    }
-  }
-
-=head2 Reading Stack Traces
-
-At its fullest level of detail, a stack trace looks something
-like this:
-
- Exception! Mayhem! and then ...
-
-    thrown  at  file Exception/Lite.t, line 307
-    in main::weKnowBetterThanYou, pid=24986, tid=1
-       @_=('ARRAY(0x83a8a90)'
-          ,'rot, rot, rot'
-          ,'Wikerson brothers'
-          ,'triculous tripe'
-          ,'There will be no more talking to hoos who are not!'
-          ,'black bottom birdie'
-          ,'from the three billionth flower'
-          ,'Mrs Tucanella returns with uncles and cousins'
-          ,'sound off! sound off! come make yourself known!'
-          ,'Apartment 12J'
-          ,'Jo Jo the young lad'
-          ,'the whole world was saved by the smallest of all'
-          )
-    reached via file Exception/Lite.t, line 281
-    in main::notAWhatButAWho
-       @_=()
-    reached via file Exception/Lite.t, line 334 in main::__ANON__
-       @_=()
-    reached via file Exception/Lite.t, line 335 in <package: main>
-       @ARGV=()
-
-    Triggered by...
-    Exception! Horton hears a hoo!
-       rethrown at file Exception/Lite.t, line 315
-
-       thrown  at  file Exception/Lite.t, line 316
-       in main::horton, pid=24986, tid=1
-          @_=('15th of May'
-             ,'Jungle of Nool'
-             ,'a small speck of dust on a small clover'
-             ,'a person's a person no matter how small'
-             )
-       reached via file Exception/Lite.t, line 310 in main::hoo
-          @_=('Dr Hoovey'
-             ,'hoo-hoo scope'
-             ,'Mrs Tucanella'
-             ,'Uncle Nate'
-             )
-       reached via file Exception/Lite.t, line 303
-       in main::weKnowBetterThanYou
-          @_=('ARRAY(0x83a8a90)'
-             ,'rot, rot, rot'
-             ,'Wikerson brothers'
-             ,'triculous tripe'
-             ,'There will be no more talking to hoos who are not!'
-             ,'black bottom birdie'
-             ,'from the three billionth flower'
-             ,'Mrs Tucanella returns with uncles and cousins'
-             ,'sound off! sound off! come make yourself known!'
-             ,'Apartment 12J'
-             ,'Jo Jo the young lad'
-             ,'the whole world was saved by the smallest of all'
-             )
-
-
-=over
-
-=item *
-
-lines begining with "thrown" indicate a line where a new exception
-was thrown. If an exception was chained, there might be multiple
-such lines.
-
-=item *
-
-lines beginning with "reached via" indicate the path travelled
-I<down> to the point where the exception was thrown. This is the
-code that was excuted before the exception was triggered.
-
-=item *
-
-lines beginning with "rethrown at" indicate the path travelled
-I<up> the stack by the exception I<after> it was geenerated. Each
-line indicates a place where the exception was caught and rethrown.
-
-=item *
-
-lines introduced with "Triggered by" are exceptions that were
-chained together. The original exception is the last of the
-triggered exceptions.  The original line is the "thrown" line
-for the original exception.
-
-=item *
-
-C<@_> and <C@ARGV> below a line indicates what is left of the
-parameters passed to a method, function or entry point routine.
-In ideal circumstances they are the parameters passed to the
-subroutine mentioned in the line immediately above C<@_>. In
-reality, they can be overwritten or shifted away between the
-point when the subroutine started and the line was reached.
-
-Note: if you use L<Getopt::Long> to process C<@ARGV>, C<@ARGV>
-will be empty reduced to an empty array. If this bothers you, you
-can localize <@ARGV> before calling C<GetOptions>, like this:
-
-  my %hARGV;
-  {
-    local @ARGV = @ARGV;
-    GetOptions(\%hARGV,...);
-  }
-
-=item *
-
-pid is the process id where the code was running
-
-=item *
-
-tid is the thread id where the code was running
-
-=back
-
-=head1 SPECIAL TOPICS
-
-=head2 Localization of error messages
-
-Rather than treat the error message and properties as entirely
-separate entities, it gives you the option to define a format string
-that will take your property values and insert them automatically
-into your message.  Thus when you generate an exception, you can
-specify only the properties and have your message automatically
-generated without any need to repeat the property values in messy
-C<sprintf>'s that clutter up your program.
-
-One can localize from the very beginning when one declares the
-class or later on after the fact if you are dealing with legacy
-software or developing on an agile module and only implementing
-what you need now.
-
-To localize from the get-go:
-
-   # myLookupSub returns the arguments to declareException
-   # e.g.  ('CopyError', [ 'On ne peut pas copier de %s a %s'
-                           , qw(from to)])
-
-   declareExceptionClass( myLookupSub('CopyError', $ENV{LANG}) );
-
-
-   # .... later on, exception generation code doesn't need to
-   # know or care about the language. it just sets the properties
-
-
-    # error message depends on locale:
-    #   en_US:  'Cannot copy A.txt to B.txt'
-    #   fr_FR:  'On ne peut pas copier de A.txt a B.txt'
-    #   de_DE:  'Kann nicht kopieren von A.txt nach B.txt'
-
-    die 'CopyError'->new(from => 'A.txt', to => 'B.txt');
-
-
-Another alternative if you wish to localize from the get-go is
-to pass a code reference instead of a format rule array. In this
-case, C<Exception::Lite> will automatically pass the class name
-to the subroutine and retrieve the value returned.
-
-
-   # anothherLookupSub has parameters ($sClass) and returns
-   # a format array, for example:
-   #
-   # %LOCALE_FORMAT_HASH = (
-   #    CopyError => {
-   #        en_US => ['Cannot copy %s to %s', qw(from to)]
-   #       ,fr_FR => ['On ne peut pas copier de %s a %s', qw(from to)]
-   #       ,de_DE => ['Kann nicht kopieren von %s nach %s''
-   #                   , qw(from to)]
-   #
-   #    AddError => ...
-   # );
-   #
-   # sub anotherLookupSub {
-   #    my ($sClass) = @_;
-   #    my $sLocale = $ENV{LANG}
-   #    return $LOCALE_FORMAT_HASH{$sClass}{$sLocale};
-   # }
-   #
-
-   declareExceptionClass('CopyError', &anotherLookupSub);
-   declareExceptionClass('AddError', &anotherLookupSub);
-
-
-    # error message depends on locale:
-    #   en_US:  'Cannot copy A.txt to B.txt'
-    #   fr_FR:  'On ne peut pas copier de A.txt a B.txt'
-    #   de_DE:  'Kann nicht kopieren von A.txt nach B.txt'
-
-    die CopyError->new(from => 'A.txt', to => 'B.txt');
-    die AddError->new(path => 'C.txt');
-
-
-If you need to put in localization after the fact, perhaps for a
-new user interface you are developing, the design pattern might
-look like this:
-
-   # in the code module you are retrofitting would be an exception
-   # that lived in a single language world. 
-
-   declareExceptionClass('CopyError'
-     ['Cannot copy %s to %s', [qw(from to)]);
-
-
-   # in your user interface application.
-
-   if (isException($e, 'CopyError') && isLocale('fr_FR')) {
-     my $sFrom = $e->getProperty('from');
-     my $sTo = $e->getProperty('to');
-     warn sprintf('On ne peut pas copier de %s a %s', $sFrom,$sTo);
-   }
-
-=head2 Controlling verbosity and stack tracing
-
-You don't need to print out the fully verbose stack trace and in
-fact, by default you won't.  The default setting, prints out
-only what called what. To make it easier to see what called what,
-it leaves out all of the dumps of C<@_> and C<@ARGV>.
-
-If you want more or less verbosity or even an entirely different
-trace, C<Exception::Lite> is at your sevice.  It provides a variety
-of options for controlling the output of the exception:
-
-* Adjusting the level of debugging information when an exception is
-  thrown by setting C<$Exception::Lite::STRINGIFY>
-  in the program or C<-mException::Lite=STRINGIFY=level> on the
-  command line. This can be set to either a verbosity level or to
-  an exception stringification routine of your own choosing.
-
-* Control which stack frames are displayed by setting
-  C<$Exception::Lite::FILTER>. By default, only calls within named
-  and anonymous subroutines are displayed in the stack trace. Perl
-  sometimes creates frames for blocks of code within a subroutine.
-  These are omitted by default. If you want to see them, you can
-  turn filterin off. Alternatively you can set up an entirely
-  custon stack filtering rule by assigning a code reference to
-  C<$Exception::Lite::FILTER>.
-
-* By default, exceptions store and print a subset of the data
-  available for each stack frame. If you would like to display
-  richer per-frame information, you can do that too. See below
-  for details.
-
-=head3 Verbosity level
-
-The built-in rules for displaying exceptions as strings offer five
-levels of detail.
-
-* 0: Just the error message
-
-* 1: the error message and the file/line number where it occured
-     along with pid and tid.
-
-* 2: the error message and the calling sequence from the point where
-  the exception was generated to the package or script entry point
-  The calling sequence shows only file, line number and the name
-  of the subroutine where the exception was generated. It is not
-  cluttered with parameters, making it easy to scan.
-
-* 3: similar to 2, except that propagation and chained exceptions
-  are also displayed.
-
-* 4: same as 3, except that the state of C<@_> or C<@ARGV> at the
-  time the exception was thrown is also displayed.  usually this
-  is the parameters that were passed in, but it may include several
-  leading C<undef> if C<shift> was used to process the parameter
-  list.
-
-Here are some samples illustrating different level of debugging
-information and what happens when the filter is turned off
-
- #---------------------------------------------------
- #Sample exception STRINGIFY=0 running on thread 5
- #---------------------------------------------------
-
- Exception! Mayhem! and then ...
-
- #---------------------------------------------------
- #Sample exception STRINGIFY=1 running on thread 5
- #---------------------------------------------------
-
- Exception! Mayhem! and then ...
-    at  file Exception/Lite.t, line 307 in main::weKnowBetterThanYou, pid=24986, tid=5
-
- #---------------------------------------------------
- #Sample exception STRINGIFY=2 running on thread 4
- #---------------------------------------------------
-
- Exception! Mayhem! and then ...
-    at  file Exception/Lite.t, line 307 in main::weKnowBetterThanYou, pid=24986, tid=4
-    via file Exception/Lite.t, line 281 in main::notAWhatButAWho
-    via file Exception/Lite.t, line 373 in main::__ANON__
-    via file Exception/Lite.t, line 374 in <package: main>
-
- #---------------------------------------------------
- #Sample exception STRINGIFY=3 running on thread 3
- #---------------------------------------------------
-
- Exception! Mayhem! and then ...
-
-    thrown  at  file Exception/Lite.t, line 307 in main::weKnowBetterThanYou, pid=24986, tid=3
-    reached via file Exception/Lite.t, line 281 in main::notAWhatButAWho
-    reached via file Exception/Lite.t, line 362 in main::__ANON__
-    reached via file Exception/Lite.t, line 363 in <package: main>
-
-    Triggered by...
-    Exception! Horton hears a hoo!
-       rethrown at file Exception/Lite.t, line 315
-
-       thrown  at  file Exception/Lite.t, line 316 in main::horton, pid=24986, tid=3
-       reached via file Exception/Lite.t, line 310 in main::hoo
-       reached via file Exception/Lite.t, line 303 in main::weKnowBetterThanYou
-
- #---------------------------------------------------
- #Sample exception STRINGIFY=3 running on thread 2
- #FILTER=OFF (see hidden eval frames)
- #---------------------------------------------------
-
- Exception! Mayhem! and then ...
-
-    thrown  at  file Exception/Lite.t, line 307 in main::weKnowBetterThanYou, pid=24986, tid=2
-    reached via file Exception/Lite.t, line 281 in main::notAWhatButAWho
-    reached via file Exception/Lite.t, line 348 in (eval)
-    reached via file Exception/Lite.t, line 348 in main::__ANON__
-    reached via file Exception/Lite.t, line 350 in (eval)
-    reached via file Exception/Lite.t, line 350 in <package: main>
-
-    Triggered by...
-    Exception! Horton hears a hoo!
-       rethrown at file Exception/Lite.t, line 315
-
-       thrown  at  file Exception/Lite.t, line 316 in main::horton, pid=24986, tid=2
-       reached via file Exception/Lite.t, line 310 in (eval)
-       reached via file Exception/Lite.t, line 315 in main::hoo
-       reached via file Exception/Lite.t, line 303 in (eval)
-       reached via file Exception/Lite.t, line 305 in main::weKnowBetterThanYou
-
- #---------------------------------------------------
- #Sample exception STRINGIFY=4 running on thread 1
- #FILTER=ON
- #---------------------------------------------------
-
- Exception! Mayhem! and then ...
-
-    thrown  at  file Exception/Lite.t, line 307 in main::weKnowBetterThanYou, pid=24986, tid=1
-       @_=('ARRAY(0x83a8a90)'
-          ,'rot, rot, rot'
-          ,'Wikerson brothers'
-          ,'triculous tripe'
-          ,'There will be no more talking to hoos who are not!'
-          ,'black bottom birdie'
-          ,'from the three billionth flower'
-          ,'Mrs Tucanella returns with Wikerson uncles and cousins'
-          ,'sound off! sound off! come make yourself known!'
-          ,'Apartment 12J'
-          ,'Jo Jo the young lad'
-          ,'the whole world was saved by the tiny Yopp! of the smallest of all'
-          )
-    reached via file Exception/Lite.t, line 281 in main::notAWhatButAWho
-       @_=()
-    reached via file Exception/Lite.t, line 334 in main::__ANON__
-       @_=()
-    reached via file Exception/Lite.t, line 335 in <package: main>
-       @ARGV=()
-
-    Triggered by...
-    Exception! Horton hears a hoo!
-       rethrown at file Exception/Lite.t, line 315
-
-       thrown  at  file Exception/Lite.t, line 316 in main::horton, pid=24986, tid=1
-          @_=('15th of May'
-             ,'Jungle of Nool'
-             ,'a small speck of dust on a small clover'
-             ,'a person's a person no matter how small'
-             )
-       reached via file Exception/Lite.t, line 310 in main::hoo
-          @_=('Dr Hoovey'
-             ,'hoo-hoo scope'
-             ,'Mrs Tucanella'
-             ,'Uncle Nate'
-             )
-       reached via file Exception/Lite.t, line 303 in main::weKnowBetterThanYou
-          @_=('ARRAY(0x83a8a90)'
-              ,'rot, rot, rot'
-              ,'Wikerson brothers'
-              ,'triculous tripe'
-              ,'There will be no more talking to hoos who are not!'
-              ,'black bottom birdie'
-              ,'from the three billionth flower'
-              ,'Mrs Tucanella returns with Wikerson uncles and cousins'
-              ,'sound off! sound off! come make yourself known!'
-              ,'Apartment 12J'
-              ,'Jo Jo the young lad'
-              ,'the whole world was saved by the tiny Yopp! of the smallest of all'
-                )
-
-
-=head3 Custom stringification subroutines
-
-The custom stringification subroutine expects one parameter, the
-exception to be stringified. It returns the stringified form of
-the exception. Here is an example of a fairly silly custom
-stringification routine that just prints out the chained messages
-without any stack trace:
-
-   $Exception::Lite::STRINGIFY = sub {
-      my $e=$_[0];    # exception is sole input parameter
-      my $sMsg='';
-      while ($e) {
-        $sMsg .= $e->getMessage() . "\n";
-        $e= $e->getChained();
-      }
-      return $sMsg;   # return string repreentation of message
-  };
-
-=head3 Adding information to the stack trace
-
-By default, each frame of the stack trace contains only the file,
-line, containing subroutine, and the state of C<@_> at the time
-C<$sFile>,C<$iLine> was reached.
-
-If your custom subroutine needs more information about the stack
-than C<Exception::Lite> normally provides, you can change the
-contents of the stack trace by assigning a custom filter routine
-to C<$Exception::Lite::FILTER>.
-
-The arguments to this subroutine are:
-
-
-   ($iFrame, $sFile, $iLine $sSub, $aArgs, $iSubFrame, $iLineFrame)
-
-where
-
-* C<$sFile> is the file of the current line in that frame
-
-* C<$iLine> is the line number of current line in that frame
-
-* C<$sSub> is the name of the subroutine that contains C<$sFile> and
-  C<$iLine>
-
-* C<$aArgs> is an array that holds the stringified value of each
-  member of @_ at the time the line at C<$sFile>, C<$sLine> was
-  called.  Usually, this is the parameters passed into C<$sSub>,
-  but may not be.
-
-* C<$iSubFrame> is the stack frame that provided the name of the sub
-  and the contents of $aArgs.
-
-* C<$iLineFrame> is the stack frame that provided the file and line
-  number for the frame.
-
-Please be aware that each line of the stack trace passed into the
-filter subroutine is a composite drawn from two different frames of
-the Perl stack trace, C<$iSubFrame> and C<$iLineFrame>.   This
-composition is necessary because the Perl stack trace contains the
-subroutine that was called at C<$sFile>, C<$iLine> rather than the
-subroutine that I<contains> C<$sFile>,C<$iLine>.
-
-The subroutine returns 0 or any other false value if the stack frame
-should be omitted. It returns to 1 accept the default stack frame as
-is.  If it accepts the stack frame but wants to insert extra data
-in the frame, it returns
-C<[$sFile,$iLine,$sSub,$aArgs, $extra1, $extra2, ...]>
-
-The extra data is always placed at the end after the C<$aArgs>
-member.
-
-=head3 Stack trace filtering
-
-To avoid noise, by default, intermediate frames that are associated
-with a block of code within a subroutine other than an anonymous
-sub (e.g. the frame created by C<eval {...} or do {...} >) are
-omitted from the stack trace.
-
-These omissions add to readability for most debugging purposes.
-In most cases one just wants to see which subroutine called which
-other subroutine.  Frames created by eval blocks don't provide
-useful information for that purpose and simply clutter up the
-debugging output.
-
-However, there are situations where one either wants more or less
-stack trace filtering.  Stack filtering can turned on or off or
-customized by setting C<$Exception::Lite::FILTER> to any of the
-following values:
-
-Normally the filtering rule is set at the start of the program or
-via the command line.   It can also be set anywhere in code, with one
-caveat: an error handling block.
-
-=over
-
-=item 0
-
-Turns all filtering off so that you see each and every frame
-in the stack trace.
-
-=item 1
-
-Turns on filtering of eval frames only (default)
-
-=item C<[ regex1, regex2, ... ]>
-
-A list of regexes. If the fully qualified subroutine name matches
-any one of these regular expressions it will be omitted from the
-stack trace.
-
-=item C<$regex>
-
-A single regular expression.  If the fully qualified subroutine name
-matches this regular expression, it will be omitted from the stack
-trace.
-
-=item C<$codeReference>
-
-The address of a named or anonymous routine that returns a boolean
-value: true if the frame should be includeed, false if it should be
-omitted. For parameters and return value of this subroutine see
-L</Adding information to the stack trace>.
-
-
-=back
-
-If filtering strategies change and an exception is chained, some of
-its stack frames might be lost during the chaining process if the
-filtering strategy that was in effect when the exception was
-generated changes before it is chained to another exception.
-
-
-=head2 Subclassing
-
-To declare a subclass with custom data and methods, use a three step
-process:
-
-=over
-
-=item *
-
-choose an exception superclass.  The choice of superclass follows
-the rule, "like gives birth to like".  Exception superclasses that
-have formats must have a superclass that also takes a format.
-Exception subclasses that have no format, must use an exception.
-
-=item *
-
-call C<declareExceptionClass> with its C<$bCustom> parameter set
-to 1
-
-=item *
-
-define a C<_new(...)> method (note the leading underscore _) and
-subclass specific methods in a  block that sets the package to
-the subclass package.
-
-=back
-
-
-When the C<$bCustom> flag is set to true, it might be best to think
-of C<declareExceptionClass> as something like C<use base> or
-C<use parent> except that there is no implicit BEGIN block. Like
-both these methods it handles all of the setup details for the
-class so that you can focus on defining methods and functionality.
-
-Wnen C<Exception::Lite> sees the C<$bCustom> flag set to true, it
-assumes you plan on customizing the class. It will set up inhertance,
-and generate all the usual method definition for an C<Exception::Lite>
-class. However, on account of C<$bCustom> being true, it will add a
-few extra things so that and your custom code can play nicely
-together:
-
-=over
-
-=item *
-
-a special hash reserved for your subclsses data. You can get
-access to this hash by calling C<_p_getSubclassData()>. You are
-free to add, change, or remove entries in the hash as needed.
-
-=item *
-
-at the end of its C<new()> method, it calls
-C<< $sClass->_new($self) >>. This is why you must define a C<_new()>
-method in your subclass package block. The C<_new> method is
-responsible for doing additional setup of exception data. Since
-this method is called last it can count on all of the normally
-expected methods and data having been set up, including the
-stack trace and the message generated by the classes format rule
-(if there is one).
-
-=back
-
-For example, suppose we want to define a subclass that accepts
-formats:
-
-  #define a superclass that accepts formats
-
-  declareExceptionClass('AnyError'
-    , ['Unexpected exception: %s','exception']);
-
-
-  # declare Exception subclass
-
-  declareExceptionClass('TimedException', 'AnyError', $aFormatData,1);
-  {
-     package TimedException;
-
-     sub _new {
-       my $self = $_[0];  #exception object created by Exception::Lite
-
-       # do additional setup of properties here
-       my $timestamp=time();
-       my $hMyData = $self->_p_getSubclassData();
-       $hMyData->{when} = time();
-    }
-
-    sub getWhen {
-       my $self=$_[0];
-       return $self->_p_getSubclassData()->{when};
-    }
-  }
-
-
-Now suppose we wish to extend our custom class further.  There is
-no difference in the way we do things just because it is a subclass
-of a customized C<Exception::Lite> class:
-
-  # extend TimedException further so that it
-  #
-  # - adds two additional bits of data - the effective gid and uid
-  #   at the time the exception was thrown
-  # - overrides getMessage() to include the time, egid, and euid
-
-  declareExceptionClass('SecureException', 'TimedException'
-                       , $aFormatData,1);
-  {
-     package TimedException;
-
-     sub _new {
-       my $self = $_[0];  #exception object created by Exception::Lite
-
-       # do additional setup of properties here
-       my $timestamp=time();
-       my $hMyData = $self->_p_getSubclassData();
-       $hMyData->{euid} = $>;
-       $hMyData->{egid} = $);
-    }
-
-    sub getEuid {
-       my $self=$_[0];
-       return $self->_p_getSubclassData()->{euid};
-    }
-    sub getEgid {
-       my $self=$_[0];
-       return $self->_p_getSubclassData()->{egid};
-    }
-    sub getMessage {
-       my $self=$_[0];
-       my $sMsg = $self->SUPER::getMessage();
-       return sprintf("%s at %s, euid=%s, guid=%s", $sMsg
-           , $self->getWhen(), $self->getEuid(), $self->getGuid());
-    }
-  }
-
-=head2 Converting other exceptions into Exception::Lite exceptions
-
-If you decide that you prefer the stack traces of this package, you
-can temporarily force all exceptions to use the C<Exception::Lite>
-stack trace, even those not generated by your own code.
-
-There are two ways to do this:
-
-* production code:  chaining/wrapping
-
-* active debugging: die/warn handlers
-
-
-=head3 Wrapping and chaining
-
-The preferred solution for production code is wrapping and/or
-chaining the exception.  Any non-string exception, even one
-of a class not created by C<Exception::Lite> can be chained
-to an C<Exception::Lite> exception.
-
-To chain a string exception, you first need to wrap it in
-an exception class.  For this purpose you can create a special
-purpose class or use the generic exception class provided by
-the C<Exception::Lite> module: C<Exception::Lite::Any>.
-
-If you don't want to chain the exception, you can also just
-rethrow the wrapped exception, as is.  Some examples:
-
-   #-----------------------------------------------------
-   # define some classes
-   #-----------------------------------------------------
-
-   # no format rule
-   declareExceptionClass('HouseholdRepairNeeded');
-
-   # format rule
-   declareExceptionClass('ProjectDelay'
-     , ['The project was delayed % days', qw(days)]);
-
-   #-----------------------------------------------------
-   # chain and/or wrap some exceptins
-   #-----------------------------------------------------
-
-   eval {
-     .... some code here ...
-     return 1;
-  } or do {
-
-    my $e=$@;
-    if (Exception::Lite::isChainable($e)) {
-      if ("$e" =~ /project/) {
-
-         # chain formatted message
-         die 'ProjectDelay'->new($e, days => 3);
-
-      } elsif ("$e" =~ /water pipe exploded/) {
-
-         # chain unformatted message
-         die 'HouseholdRepairNeeded'->new($e, 'Call the plumber');
-
-      }
-    } elsif ($e =~ 'repairman') {   #exception is a string
-
-       # wrapping a scalar exception so it has the stack trace
-       # up to this point, but _no_ chaining
-       #
-       # since the exception is a scalar, the constructor
-       # of a no-format exception class will treat the first
-       # parameter as a message rather than a chained exception
-
-       die 'HouseholdRepairNeeded'->new($e);
-
-    } else {
-
-       # if we do want to chain a string exception, we need to
-       # wrap it first in an exception class:
-
-       my $eWrapped = Exception::Lite::Any->new($e);
-       die 'HouseholdRepairNeeded'
-         ->new($eWrapped, "Call the repair guy");
-    }
-  }
-
-=head3 Die/Warn Handlers
-
-Die/Warn handlers provide a quick and dirty way to at Exception::Lite
-style stack traces to all warnings and exceptions.  However,
-it should ONLY BE USED DURING ACTIVE DEBUGGING.  They should never
-be used in production code. Setting these handlers
-can interfere with the debugging style and techiniques of other
-programmers and that is not nice.
-
-However, so long as you are actiely debugging, setting a die or
-warn handler can be quite useful, especially if a third party module
-is generating an exception or warning and you have no idea where it
-is coming from.
-
-To set a die handler, you pass your desired stringify level or
-code reference to C<onDie>:
-
-    Exception::Lite::onDie(4);
-
-This is roughly equivalent to:
-
-   $SIG{__DIE__} = sub {
-     $Exception::Lite::STRINGIFY=4;
-     warn 'Exception::Lite::Any'->new('Unexpected death:'.$_[0])
-       unless ($^S || Exception::Lite::isException($_[0]));
-   };
-
-To set a warning handler, you pass your desired stringify level or
-code reference to C<onWarn>:
-
-    Exception::Lite::onWarn(4);
-
-This is roughly equivalent to:
-
-  $SIG{__WARN__} = sub {
-    $Exception::Lite::STRINGIFY=4;
-    print STDERR 'Exception::Lite::Any'->new("Warning: $_[0]");
-  };
-
-Typically these handlers are placed at the top of a test script
-like this:
-
-  use strict;
-  use warnings;
-  use Test::More tests => 25;
-
-  use Exception::Lite;
-  Exception::Lite::onDie(4);
-  Exception::Lite::onWarn(3);
-
-  ... actual testing code here ...
-
-
-
-=head1 WHY A NEW EXCEPTION CLASS
-
-Aren't there enough already? Well, no.  This class differs from
-existing classes in several significant ways beyond "lite"-ness.
-
-=head2 Simplified integration of properties and messages
-
-C<Exception::Lite> simplifies the creation of exceptions by
-minimizing the amount of metadata that needs to be declared for
-each exception and by closely integrating exception properties
-and error messages.  Though there are many exception modules
-that let you define message and properties for exceptions, in
-those other modules you have to manually maintain any connection
-between the two either in your code or in a custom subclass.
-
-In L<Exception::Class|Exception::Class>, for example, you have to
-do something like this:
-
-     #... at the start of your code ...
-     # notice how exception definition and message format
-     # string constant are in two different places and need
-     # to be manually coordinated by the programmer.
-
-     use Exception::Class {
-       'Exception::Copy::Mine' {
-           fields => [qw(from to)];
-        }
-        # ... lots of other exceptions here ...
-     }
-     my $MSG_COPY='Could not copy A.txt to B.txt";
-
-     ... later on when you throw the exception ...
-
-     # notice the repetition in the use of exception
-     # properties; the repetition is error prone and adds
-     # unnecessary extra typing     
-
-     my $sMsg = sprintf($MSG_COPY, 'A.txt', 'B.txt');
-     Exception::Copy::Mine->throw(error => $sMsg
-                                  , from => 'A.txt'
-                                  , to => 'B.txt');
-
-
-C<Exception::Lite> provides a succinct and easy to maintain
-method of declaring those same exceptions
-
-    # the declaration puts the message format string and the
-    # class declaration together for the programmer, thus
-    # resulting in less maintenence work
-
-    declareExceptionClass("Exception::Mine::Copy"
-       , ["Could not copy %s to %s", qw(from, to) ]);
-
-
-    .... some where else in your code ...
-
-
-    # there is no need to explicitly call sprintf or
-    # repetitively type variable names, nor even remember
-    # the order of parameters in the format string or check
-    # for undefined values. Both of these will produce
-    # the same error message:
-    #   "Could not copy A.txt to B.txt"
-
-    die "Exception::Mine:Copy"->new(from =>'A.txt', to=>'B.txt');
-    die "Exception::Mine:Copy"->new(to =>'B.txt', from=>'A.txt');
-
-     # and this will politely fill in 'undef' for the
-     # property you leave out:
-     #    "Could not copy A.txt to <undef>"
-
-     die "Exception::Mine::Copy"->new(from=>'A.txt');
-
-
-=head2 More intelligent stack trace
-
-The vast majority, if not all, of the exception modules on CPAN
-essentially reproduce Carp's presentation of the stack trace. They
-sometimes provide parameters to control the level of detail, but
-make only minimal efforts, if any, to improve on the quality of
-debugging information.
-
-C<Exception::Lite> improves on the traditional Perl stack trace
-provided by Carp in a number of ways.
-
-=over
-
-=item *
-
-Error messages are shown in full and never truncated (a problem with
-  C<Carp::croak>.
-
-=item *
-
-The ability to see a list of what called what without the clutter
-  of subroutine parameters.
-
-=item *
-
-The ability to see the context of a line that fails rather than
-a pinhole snapshot of the line itself. Thus one sees
-"at file Foo.pm, line 13 in sub doTheFunkyFunk" rather
-  than the  contextless stack trace line displayed by nearly every,
-  if not all Perl stacktraces, including C<Carp::croak>:
-  "called foobar(...) at line 13 in Foo.pm".
-  When context rather than line snapshots
-  are provided, it is often enough simply to scan the list of what
-  called what to see where the error occurred.
-
-=item *
-
-Automatic filtering of stack frames that do not show the actual
-Flow from call to call.  Perl internally creates stack frames for
-each eval block.  Seeing these in the stack trace make it harder
-to scan the stack trace and see what called what.
-
-=item *
-
-The automatic filtering can be turned off or, alternatively
-customized to include/exclude arbitrary stack frames.
-
-=item *
-
-One can chain together exceptions and then print out what exception
-triggered what other exception.  Sometimes what a low level module
-considers important about an exception is not what a higher level
-module considers important. When that happens, the programmer can
-create a new exception with a more relevant error message that
-"remembers" the exception that inspired it. If need be, one can
-see the entire history from origin to destination.
-
-=back
-
-The "traditional" stack trace smushes together all parameters into
-a single long line that is very hard to read.  C<Exception::Lite>
-provides a much more readable parametr listing:
-
-=over
-
-=item *
-
-They are displayed one per line so that they can be easily read
-  and distinguished one from another
-
-=item *
-
-The string value <i>and</i> the normal object representation is
-  shown when an object's string conversion is overloaded. That way
-  there can be no confusion about whether the actual object or a
-  string was passed in as a parameter.
-
-=item *
-
-It doesn't pretend that these are the parameters passed to the
-  subroutine.  It is impossible to recreate the actual values in
-  the parameter list because the parameter list for any sub is
-  just C<@_> and that can be modified when a programmer uses shift
-  to process command line arguments. The most Perl can give (through
-  its DB module) is the way C<@_> looked at the time the next frame
-  in the stack was set up.  Instead of positioning the parameters
-  as if they were being passed to the subroutine, they are listed
-  below the stacktrace line saying "thrown at in line X in
-  subroutine Y".  In reality, the "parameters" are the value of
-  @_ passed to subroutine Y (or @ARGV if this is the entry point),
-  or what was left of it when we got to line X.
-
-=item
-
-A visual hint that leading C<undef>s in C<@_> or C<@ARGV> may be
-  the result of shifts rather than a heap of C<undef>s passed into
-  the subroutine.  This lets the programmer focus on the code, not
-  on remembering the quirks of Perl stack tracing.
-
-=back
-
-=head1 CLASS REFERENCE
-
-=head2 Class factory methods
-
-=head3 C<declareExceptionClass>
-
-   declareExceptionClass($sClass);
-   declareExceptionClass($sClass, $sSuperclass);
-   declareExceptionClass($sClass, $sSuperclass, $bCustom);
-
-   declareExceptionClass($sClass, $aFormatRule);
-   declareExceptionClass($sClass, $sSuperclass, $aFormatRule);
-   declareExceptionClass($sClass, $sSuperclass, $aFormatRule
-      , $bCustom);
-
-Generates a lightweight class definition for an exception class. It
-returns the name of the created class, i.e. $sClass.
-
-=over
-
-=item C<$sClass>
-
-The name of the class (package) to be created.  Required.
-
-Any legal Perl package name may be used, so long as it hasn't
-already been used to define an exception or any other class.
-
-=item C<$sSuperclass>
-
-The name of the superclass of C<$sClass>.  Optional.
-
-If missing or undefed, C<$sClass> will be be a base class
-whose only superclass is C<UNIVERSAL>, the root class of all Perl
-classes.  There is no special "Exception::Base" class that all
-exceptions have to descend from, unless you want it that way
-and choose to define your set of exception classes that way.
-
-=item C<$aFormatRule>
-
-An array reference describing how to use properties to construct
-a message. Optional.
-
-If provided, the format rule is essential the same parameters as
-used by sprintf with one major exception: instead of using actual
-values as arguments, you use property names, like this:
-
-    # insert value of 'from' property in place of first %s
-    # insert value of 'to' property in place of first %s
-
-    [ 'Cannot copy from %s to %s, 'from', 'to' ]
-
-When a format rule is provided, C<Exception::Lite> will auto-generate
-the message from the properties whenever the properties are set or
-changed. Regeneration is a lightweight process that selects property
-values from the hash and sends them to C<sprintf> for formatting.
-
-Later on, when you are creating exceptions, you simply pass in the
-property values. They can be listed in any order and extra properties
-that do not appear in the message string can also be provided. If
-for some reason the value of a property is unknown, you can assign it
-C<undef> and C<Exception::Lite> will politely insert a placeholder
-for the missing value.  All of the following are valid:
-
-
-    # These all generate "Cannot copy A.txt to B.txt"
-
-    $sClass->new(from => 'A.txt', to => 'B.txt');
-    $sClass->new(to => 'B.txt', from => 'A.txt');
-    $sClass->new(to => 'B.txt', from => 'A.txt'
-                 , reason => 'source doesn't exist'
-                 , seriousness => 4
-                );
-    $sClass->new(reason => 'source doesn't exist'
-                 , seriousness => 4
-                 , to => 'B.txt', from => 'A.txt'
-                );
-
-    # These generate "Cannot copy A.txt to <undef>"
-
-    $sClass->new(from => 'A.txt');
-    $sClass->new(from => 'A.txt', to => 'B.txt');
-
-=item C<$bCustom>
-
-True if the caller intends to add custom methods and/or a custom
-constructor to the newly declared class.  This will force the
-L<Excepton::Lite> to generate some extra methods and data so
-that the subclass can have its own private data area in the class.
-See L</Subclassing> for more information.
-
-
-=back
-
-=head2 Object construction methods
-
-=head3 C<new>
-
-    # class configured for no generation from properties
-
-    $sClass->new($sMsg);
-    $sClass->new($sMsg,$prop1 => $val1, ....);
-    $sClass->new($e);
-    $sClass->new($e, $sMsg);
-    $sClass->new($e, $sMsg,$prop1 => $val1, ....);
-
-    # class configured to generate messages from properties
-    # using a per-class format string
-
-    $sClass->new($prop1 => $val1, ....);
-    $sClass->new($e, $prop1 => $val1, ....);
-
-
-Creates a new instance of exception class C<$sClass>. The exception
-may be independent or chained to the exception that triggered it.
-
-=over
-
-=item $e
-
-The exception that logically triggered this new exception.
-May be omitted or left undefined.  If defined, the new exception is
-considered chained to C<$e>.
-
-=item $sMsg
-
-The message text, for classes with no autogeneration from properties,
-that is, classes declared like
-
-   declareExceptionClass($sClass);
-   declareExceptionClass($sClass, $sSuperclass);
-
-In the constructor, C< $sClass->new($e) >>, the message defaults to
-the message of C<$e>. Otherwise the message is required for any
-class that id declared in the above two ways.
-
-=item $prop1 => $val1
-
-The first property name and its associated value. There can be
-as many repetitions of this as there are properties.  All types
-of exception classes may have property lists.
-
-=back
-
-If you have chosen to have the message be completely independent
-of properties:
-
-   declareExceptionClass('A');
-
-   # unchained exception - print output "Hello"
-
-   my $e1 = A->new("Hello", importance => 'small', risk => 'large');
-   print "$e1\n";
-
-   # chained exception - print output "Hello"
-
-   my $e2 = A->new($e1,'Goodbye');
-
-   $e2->getChained();                      # returns $e1
-   print $e1->getMessage();                # outputs "Goodbye"
-   print $e1;                              # outputs "Goodbye"
-   print $e2->getChained()->getMessage();  # outputs "Hello"
-
-
-If you have chosen to have the message autogenerated from properties
-your call to C<new> will look like this:
-
-   $sFormat ='the importance is %s, but the risk is %s';
-   declareExceptionClass('B', [ $sFormat, qw(importance risk)]);
-
-
-   # unchained exception
-
-   my $e1 = B->new(importance=>'small', risk=>'large');
-
-   $e1->getChained();   # returns undef
-   print "$e1\n";       # outputs "The importance is small, but the
-                        #   risk is large"
-
-   # chained exception
-
-   $e2 = B->new($e1, importance=>'yink', risk=>'hooboy');
-   $e2->getChained();   # returns $e1
-   "$e2"                # evaluates to "The importance is yink, but
-                        # the risk is hooboy"
-   $e2->getMessage()                # same as "$e2"
-   $e2->getChained()->getMessage(); # same as "$e1"
-
-
-
-=head2 Object methods
-
-=head3 C<getMessage>
-
-   $e->getMessage();
-
-Returns the messsage, i.e. the value displayed when this exception
-is treated as a string.  This is the value without line numbers
-stack trace or other information.  It includes only the format
-string with the property values inserted.
-
-=head3 C<getProperty>
-
-   $e->getProperty($sName);
-
-Returns the property value for the C<$sName> property.
-
-=head3 C<isProperty>
-
-   $e->isProperty($sName)
-
-Returns true if the exception has the C<$sName> property, even if
-the value is undefined. (checks existance, not definition).
-
-=head3 C<getPid>
-
-   $e->getPid();
-
-Returns the process id of the process where the exception was
-thrown.
-
-=head3 C<getPackage>
-
-   $e->getPackage();
-
-Returns the package contining the entry point of the process, i.e.
-the package identified at the top of the stack.
-
-
-=head3 C<getTid>
-
-Returns the thread where the exception was thrown.
-
-   $e->getTid();
-
-=head3 C<getStackTrace>
-
-   $e->getStackTrace();
-
-Returns the stack trace from the point where the exception was
-thrown (frame 0) to the entry point (frame -1).  The stack trace
-is structured as an array of arrays (AoA) where each member array
-represents a single lightweight frame with four data per frame:
-
-   [0]  the file
-   [1]  the line number within the file
-   [2]  the subroutine where the exception was called. File and
-        line number will be within this subroutine.
-   [3]  a comma delimited string containing string representations
-        of the values that were stored in @_ at the time the
-        exception was thrown. If shift was used to process the
-        incoming subroutine arguments, @_ will usually contain
-        several leading undefs.
-
-For more information about each component of a stack frame, please
-see the documentation below for the following methods:
-
-* C<getFile>       - explains what to expect in [0] of stack frame
-
-* C<getLine>       - explains what to expect in [1] of stack frame
-
-* C<getSubroutine> - explains what to expect in [2] of stack frame
-
-* C<getArgs>       - explains what to expect in [3] of stack frame
-
-The frame closest to the thrown exception is numbered 0. In fact
-frame 0, stores information about the actual point where the exception
-was thrown.
-
-
-=head3 C<getFrameCount>
-
-   $e->getFrameCount();
-
-Returns the number of frames in the stack trace.
-
-=head3 C<getFile>
-
-   $e->getFile(0);    # gets frame where exception was thrown
-   $e->getFile(-1);   # gets entry point frame
-
-   $e->getFile();     # short hand for $e->getFile(0)
-   $e->getFile($i);
-
-Without an argument, this method returns the name of the file where
-the exception was thrown.  With an argument it returns the name of
-the file in the C<$i>th frame of the stack trace.
-
-Negative values of C<$i> will be counted from the entry point with
-C<-1> representing the entry point frame, C<-2> representing the
-first call made within the script and so on.
-
-=head3 C<getLine>
-
-   $e->getLine(0);    # gets frame where exception was thrown
-   $e->getLine(-1);   # gets entry point frame
-
-   $e->getLine();     # short hand for $e->getLine(0)
-   $e->getLine($i);
-
-Without an argument, this method returns the line number where the
-exception was thrown.  With an argument it returns the line number
-in the C<$i>th frame of the stack trace.
-
-Negative values of C<$i> will be counted from the entry point with
-C<-1> representing the entry point frame, C<-2> representing the
-first call made within the script and so on.
-
-=head3 C<getSubroutine>
-
-   $e->getSubroutine(0);    # gets frame where exception was thrown
-   $e->getSubroutine(-1);   # gets entry point frame
-
-   $e->getSubroutine();     # short hand for $e->getSubroutine(0)
-   $e->getSubroutine($i);
-
-Without an argument, this method returns the name of the subroutine
-where this exception was created via C<new(...)>.  With an argument
-it returns the value of the subroutine (or package entry point) in
-the C<$i>th frame of the stack trace.
-
-Negative values of C<$i> will be counted from the entry point with
-C<-1> representing the entry point frame, C<-2> representing the
-first call made within the script and so on.
-
-Note: This is not the same value as returned by C<caller($i)>. C<caller> returns the name of the subroutine that was being called
-at the time of death rather than the containing subroutine.
-
-The subroutine name in array element [2] includes the package name
-so it will be 'MyPackage::Utils::doit' and not just 'doit'. In the
-entry point frame there is, of course, no containing subroutine so
-the value in this string is instead the package name embedded in
-the string "<package: packageName>".
-
-
-=head3 C<getArgs>
-
-   $e->getArgs(0);    # gets frame where exception was thrown
-   $e->getArgs(-1);   # gets entry point frame
-
-   $e->getArgs();     # short hand for $e->getArgs(0)
-   $e->getArgs($i);
-
-Without an argument, this method returns the value of C<@_> (or
-C<@ARGV> for an entry point frame) at the time the exception was
-thrown.  With an argument it returns the name of
-the file in the C<$i>th frame of the stack trace.
-
-Negative values of C<$i> will be counted from the entry point with
-C<-1> representing the entry point frame, C<-2> representing the
-first call made within the script and so on.
-
- @_, is the best approximation Perl provides for the arguments
-used to call the subroutine.  At the start of the subroutine it does
-in fact reflect the parameters passed in, but frequently programmers
-will process this array with the C<shift> operator which will set
-leading arguments to C<undef>. The debugger does not cache the
-oiginal value of @_, so all you can get from its stack trace is the
-value at the time the exception was thrown, not the value when the
-subroutine was entered.
-
-=head3 C<getPropagation>
-
-   $e->getPropagation();
-
-Returns an array reference with one element for each time this
-exception was caught and rethrown using either Perl's own rethrow
-syntax C<$@=$e; die;> or this packages: C<< die->rethrow() >>.
-
-Each element of the array contains a file and line number where
-the exception was rethrown:
-
- [0]  file where exception was caught and rethrown
- [1]  line number where the exception was caught and rethrown
-
-Note: do not confuse the stack trace with propagation. The stack
-trace is the sequence of calls that were made I<before> the
-exception was thrown.  The propagation file and line numbers 
-refer to where the exception was caught in an exception handling
-block I<after> the exception was thrown.
-
-Generally, bad data is the reason behind an exception.  To see
-where the bad data came from, it is generally more useful to
-look at the stack and see what data was passed down to the point
-where the exception was generated than it is to look at where
-the exception was caught after the fact.
-
-=head3 C<getChained>
-
-   my $eChained = $e->getChained();
-
-Returns the chained exception, or undef if the exception is not
-chained.  Chained exceptions are created by inserting the triggering
-exception as the first parameter to C<new(...)>.
-
-  # class level format
-  MyException1->new(reason=>'blahblahblah');       #unchained
-  MyException1->new($e, reason=>'blahblahblah');   #chained
-
-  # no format string
-  MyException1->new('blahblahblah');               #unchained
-  MyException1->new($e, reason=>'blahblahblah');   #chained
-
-
-The chained exception can be a reference to any sort of data. It
-does not need to belong to the same class as the new exception,
-nor does it even have to belong to a class generated by
-C<Exception::Lite>. Its only restriction is that it may not be
-a scalar(string, number, ec).  To see if an exception
-may be chained you can call C<Exception::Lite::isChainable()>:
-
-   if (Exception::Lite::isChainable($e)) {
-      die MyException1->new($e, reason=>'blahblahblah');
-   } else {
-
-      # another alternative for string exceptions
-      my $eWrapper=MyWrapperForStringExceptions->new($e);
-      die MyException1->new($eWrapper, reason=>'blahblahblah');
-
-      # another alternative for string exceptions
-      die MyException1->new($eWrapper, reason=>"blahblahblah: $e");
-   }
-
-
-=head3 C<rethrow>
-
-   $e->rethrow();
-   $e->rethrow($prop => $newValue);         # format rule
-
-   $e->rethrow($newMsg, $p1 => $newValue);  # no format rule
-   $e->rethrow(undef, $pl => $newValue);    # no format rule
-   $e->rethrow($sNewMsg);                   # no format rule
-
-
-Propagates the exception using the method (C<PROPAGATE>) as would
-be called were one to use Perl's native 'rethrow' syntax,
-C<$@=$e; die>.
-
-The first form with no arguments simply rethrows the exception.
-The remain formats let one override property values and/or update
-the message. The argument list is the same as for C<new> except
-that exceptions with no or object level format strings may have
-an undefined message.
-
-For class format exceptions, the message will automatically be
-updated if any of the properties used to construct it have changed.
-
-For exception classes with no formatting, property and message
-changes are independent of each other. If C<$sMsg> is set to C<undef>
-the properties will be changed and the message will be left alone.
-If C<$sMsg> is provided, but no override properties are provided,
-the message will change but the properties will be left untouched.
-
-=head3 C<_p_getSubclassData>
-
-Method for internal use by custom subclasses. This method retrieves
-the data hash reserved for use by custom methods.
-
-
-=head1 SEE ALSO
-
-=head2 Canned test modules
-
-Test modules for making sure your code generates the right
-exceptions.  They work with any OOP solution, even C<Exception::Lite>
-
-* L<Test::Exception|Test::Exception>  - works with any OOP solution
-
-* L<Test::Exception::LessClever|Test::Exception::LessClever> - works
-  with any OOP solution
-
-=head2 Alternate OOP solutions
-
-=head3 L<Exception::Class|Exception::Class>
-
-This module has a fair number of non-core modules. There are several
-extension modules. Most are adapter classes that convert exceptions
-produced by popular CPAN modules into Exception::Class modules:
-
-* L<Exception::Class::Nested|Exception::Class::Nested> - changes
-  the syntax for declaring exceptions.
-
-* L<MooseX::Error::Exception::Class|MooseX::Error::Exception::Class>
-  - converts Moose exceptions to
-  C<Exception::Class> instances.
-
-* L<HTTP::Exception|HTTP::Exception>  - wrapper around HTTP exceptions
-
-* L<Mail::Log::Exceptions|Mail::Log::Exceptions> - wrapper around
-  Mail::Log exceptions
-
-* L<Exception::Class::DBI|Exception::Class::DBI> - wrapper around
-  DBI exceptions
-
-* L<Error::Exception|Error::Exception> - prints out exception
-  properties as part of exception stringification.
-
-It takes a heavy approach to OOP, requiring all properties to be
-predeclared.  It also stores a lot of information about an exception,
-not all of which is likely to be important to the average user, e.g.
-pid, uid, guid and even the entire stack trace.
-
-There is no support for auto-generating messages based on
-properties.
-
-For an extended discussion of C<Exception::Class>, see
-L<http://www.drdobbs.com/web-development/184416129>.
-
-=head3 L<Exception::Base|Exception::Base>
-
-A light weight version of L<Exception::Class|Exception::Class>.
-Uses only core modules but is fairly new and has no significant
-eco-system of extensions (yet).
-Like C<Exception::Class> properties must be explicitly declared and
-there is no support for autogenerating messages based on properties.
-
-
-=head3 L<Class::Throwable|Class::Throwable>
-
-Another light weight version of L<Exception::Class|Exception::Class>.
-Unlike C<Exception::Class> you can control the amount of system
-state and stack trace information stored at the time an exception
-is generated.
-
-=head2 Syntactic sugar solutions
-
-Syntactical sugar solutions allow java like try/catch blocks to
-replace the more awkward C<die>, C<eval/do>, and C<$@=$e; die>
-pattern. Take care in chosing these methods as they sometimes
-use coding strategies known to cause problems:
-
-=over
-
-=item *
-
-overriding signal handlers - possible interference with your own
-code or third party module use of those handlers.
-
-=item *
-
-source code filtering - can shift line numbers so that the reported
-line number and the actual line number may not be the same.
-
-=item *
-
-closures - there is a apparently a problem with nested closures
-causing memory leaks in some versions of Perl (pre 5.8.4). This
-has been since fixed since 5.8.4.
-
-=back
-
-Modules providing syntactic sugar include:
-
-* L<Try::Catch|Try::Catch>
-
-* L<Try::Tiny|Try::Tiny>
-
-* C<Error|Error>
-
-* L<Exception::Caught|Exception::Caught>
-
-* L<Exception::SEH|Exception::SEH>
-
-* C<Exception|Exception>
-
-* L<Exception::Class::TryCatch|Exception::Class::TryCatch> - extension of L<Exception::Class|Exception::Class>
-
-* L<Exception::Class::TCF|Exception::Class::TCF> - extension of L<Exception::Class|Exception::Class>
-
-
-=head1 EXPORTS
-
-No subroutines are exported by default. See the start of the synopsis
-for optional exports.
-
-
-=head1 AUTHOR
-
-Elizabeth Grace Frank-Backman
-
-=head1 COPYRIGHT
-
-Copyright (c) 2011 Elizabeth Grace Frank-Backman.
-All rights reserved.
-
-=head1 LICENSE
-
-This program is free software; you can redistribute it and/or
-modify it under the same terms as Perl itself.
diff --git a/modules/fallback/File/Flock.pm b/modules/fallback/File/Flock.pm
deleted file mode 100644 (file)
index f9b62c1..0000000
+++ /dev/null
@@ -1,327 +0,0 @@
-# Copyright (C) 1996, 1998 David Muir Sharnoff
-
-package File::Flock;
-
-require Exporter;
-@ISA = qw(Exporter);
-@EXPORT = qw(lock unlock lock_rename);
-
-use Carp;
-use POSIX qw(EAGAIN EACCES EWOULDBLOCK ENOENT EEXIST O_EXCL O_CREAT O_RDWR); 
-use Fcntl qw(LOCK_SH LOCK_EX LOCK_NB LOCK_UN);
-use IO::File;
-
-use vars qw($VERSION $debug $av0debug);
-
-BEGIN  {
-       $VERSION = 2008.01;
-       $debug = 0;
-       $av0debug = 0;
-}
-
-use strict;
-no strict qw(refs);
-
-my %locks;             # did we create the file?
-my %lockHandle;
-my %shared;
-my %pid;
-my %rm;
-
-sub new
-{
-       my ($pkg, $file, $shared, $nonblocking) = @_;
-       &lock($file, $shared, $nonblocking) or return undef;
-       return bless \$file, $pkg;
-}
-
-sub DESTROY
-{
-       my ($this) = @_;
-       unlock($$this);
-}
-
-sub lock
-{
-       my ($file, $shared, $nonblocking) = @_;
-
-       my $f = new IO::File;
-
-       my $created = 0;
-       my $previous = exists $locks{$file};
-
-       # the file may be springing in and out of existance...
-       OPEN:
-       for(;;) {
-               if (-e $file) {
-                       unless (sysopen($f, $file, O_RDWR)) {
-                               redo OPEN if $! == ENOENT;
-                               croak "open $file: $!";
-                       }
-               } else {
-                       unless (sysopen($f, $file, O_CREAT|O_EXCL|O_RDWR)) {
-                               redo OPEN if $! == EEXIST;
-                               croak "open >$file: $!";
-                       }
-                       print STDERR " {$$ " if $debug; # }
-                       $created = 1;
-               }
-               last;
-       }
-       $locks{$file} = $created || $locks{$file} || 0;
-       $shared{$file} = $shared;
-       $pid{$file} = $$;
-       
-       $lockHandle{$file} = $f;
-
-       my $flags;
-
-       $flags = $shared ? LOCK_SH : LOCK_EX;
-       $flags |= LOCK_NB
-               if $nonblocking;
-       
-       local($0) = "$0 - locking $file" if $av0debug && ! $nonblocking;
-       my $r = flock($f, $flags);
-
-       print STDERR " ($$ " if $debug and $r;
-
-       if ($r) {
-               # let's check to make sure the file wasn't
-               # removed on us!
-
-               my $ifile = (stat($file))[1];
-               my $ihandle;
-               eval { $ihandle = (stat($f))[1] };
-               croak $@ if $@;
-
-               return 1 if defined $ifile 
-                       and defined $ihandle 
-                       and $ifile == $ihandle;
-
-               # oh well, try again
-               flock($f, LOCK_UN);
-               close($f);
-               return File::Flock::lock($file);
-       }
-
-       return 1 if $r;
-       if ($nonblocking and 
-               (($! == EAGAIN) 
-               or ($! == EACCES)
-               or ($! == EWOULDBLOCK))) 
-       {
-               if (! $previous) {
-                       delete $locks{$file};
-                       delete $lockHandle{$file};
-                       delete $shared{$file};
-                       delete $pid{$file};
-               }
-               if ($created) {
-                       # oops, a bad thing just happened.  
-                       # We don't want to block, but we made the file.
-                       &background_remove($f, $file);
-               }
-               close($f);
-               return 0;
-       }
-       croak "flock $f $flags: $!";
-}
-
-#
-# get a lock on a file and remove it if it's empty.  This is to
-# remove files that were created just so that they could be locked.
-#
-# To do this without blocking, defer any files that are locked to the
-# the END block.
-#
-sub background_remove
-{
-       my ($f, $file) = @_;
-
-       if (flock($f, LOCK_EX|LOCK_NB)) {
-               unlink($file)
-                       if -s $file == 0;
-               flock($f, LOCK_UN);
-               return 1;
-       } else {
-               $rm{$file} = 1
-                       unless exists $rm{$file};
-               return 0;
-       }
-}
-
-sub unlock
-{
-       my ($file) = @_;
-
-       if (ref $file eq 'File::Flock') {
-               bless $file, 'UNIVERSAL'; # avoid destructor later
-               $file = $$file;
-       }
-
-       croak "no lock on $file" unless exists $locks{$file};
-       my $created = $locks{$file};
-       my $unlocked = 0;
-
-
-       my $size = -s $file;
-       if ($created && defined($size) && $size == 0) {
-               if ($shared{$file}) {
-                       $unlocked = 
-                               &background_remove($lockHandle{$file}, $file);
-               } else { 
-                       # {
-                       print STDERR " $$} " if $debug;
-                       unlink($file) 
-                               or croak "unlink $file: $!";
-               }
-       }
-       delete $locks{$file};
-       delete $pid{$file};
-
-       my $f = $lockHandle{$file};
-
-       delete $lockHandle{$file};
-
-       return 0 unless defined $f;
-
-       print STDERR " $$) " if $debug;
-       $unlocked or flock($f, LOCK_UN)
-               or croak "flock $file UN: $!";
-
-       close($f);
-       return 1;
-}
-
-sub lock_rename
-{
-       my ($oldfile, $newfile) = @_;
-
-       if (exists $locks{$newfile}) {
-               unlock $newfile;
-       }
-       delete $locks{$newfile};
-       delete $shared{$newfile};
-       delete $pid{$newfile};
-       delete $lockHandle{$newfile};
-       delete $rm{$newfile};
-
-       $locks{$newfile}        = $locks{$oldfile}      if exists $locks{$oldfile};
-       $shared{$newfile}       = $shared{$oldfile}     if exists $shared{$oldfile};
-       $pid{$newfile}          = $pid{$oldfile}        if exists $pid{$oldfile};
-       $lockHandle{$newfile}   = $lockHandle{$oldfile} if exists $lockHandle{$oldfile};
-       $rm{$newfile}           = $rm{$oldfile}         if exists $rm{$oldfile};
-
-       delete $locks{$oldfile};
-       delete $shared{$oldfile};
-       delete $pid{$oldfile};
-       delete $lockHandle{$oldfile};
-       delete $rm{$oldfile};
-}
-
-#
-# Unlock any files that are still locked and remove any files
-# that were created just so that they could be locked.
-#
-END {
-       my $f;
-       for $f (keys %locks) {
-               &unlock($f)
-                       if $pid{$f} == $$;
-       }
-
-       my %bgrm;
-       for my $file (keys %rm) {
-               my $f = new IO::File;
-               if (sysopen($f, $file, O_RDWR)) {
-                       if (flock($f, LOCK_EX|LOCK_NB)) {
-                               unlink($file)
-                                       if -s $file == 0;
-                               flock($f, LOCK_UN);
-                       } else {
-                               $bgrm{$file} = 1;
-                       }
-                       close($f);
-               }
-       }
-       if (%bgrm) {
-               my $ppid = fork;
-               croak "cannot fork" unless defined $ppid;
-               my $pppid = $$;
-               my $b0 = $0;
-               $0 = "$b0: waiting for child ($ppid) to fork()";
-               unless ($ppid) {
-                       my $pid = fork;
-                       croak "cannot fork" unless defined $pid;
-                       unless ($pid) {
-                               for my $file (keys %bgrm) {
-                                       my $f = new IO::File;
-                                       if (sysopen($f, $file, O_RDWR)) {
-                                               if (flock($f, LOCK_EX)) {
-                                                       unlink($file)
-                                                               if -s $file == 0;
-                                                       flock($f, LOCK_UN);
-                                               }
-                                               close($f);
-                                       }
-                               }
-                               print STDERR " $pppid] $pppid)" if $debug;
-                       }
-                       kill(9, $$); # exit w/o END or anything else
-               }
-               waitpid($ppid, 0);
-               kill(9, $$); # exit w/o END or anything else
-       }
-}
-
-1;
-
-__DATA__
-
-=head1 NAME
-
- File::Flock - file locking with flock
-
-=head1 SYNOPSIS
-
- use File::Flock;
-
- lock($filename);
-
- lock($filename, 'shared');
-
- lock($filename, undef, 'nonblocking');
-
- lock($filename, 'shared', 'nonblocking');
-
- unlock($filename);
-
- my $lock = new File::Flock '/somefile';
-
- lock_rename($oldfilename, $newfilename)
-
-=head1 DESCRIPTION
-
-Lock files using the flock() call.  If the file to be locked does not
-exist, then the file is created.  If the file was created then it will
-be removed when it is unlocked assuming it's still an empty file.
-
-Locks can be created by new'ing a B<File::Flock> object.  Such locks
-are automatically removed when the object goes out of scope.  The
-B<unlock()> method may also be used.
-
-B<lock_rename()> is used to tell File::Flock when a file has been
-renamed (and thus the internal locking data that is stored based
-on the filename should be moved to a new name).  B<unlock()> the
-new name rather than the original name.
-
-=head1 LICENSE
-
-File::Flock may be used/modified/distibuted on the same terms
-as perl itself.  
-
-=head1 AUTHOR
-
-David Muir Sharnoff <muir@idiom.org>
-
-
diff --git a/modules/fallback/File/Slurp.pm b/modules/fallback/File/Slurp.pm
deleted file mode 100644 (file)
index 0aad7ed..0000000
+++ /dev/null
@@ -1,742 +0,0 @@
-package File::Slurp;
-
-use strict;
-
-use Carp ;
-use POSIX qw( :fcntl_h ) ;
-use Fcntl qw( :DEFAULT ) ;
-use Symbol ;
-
-my $is_win32 = $^O =~ /win32/i ;
-
-# Install subs for various constants that aren't set in older perls
-# (< 5.005).  Fcntl on old perls uses Exporter to define subs without a
-# () prototype These can't be overridden with the constant pragma or
-# we get a prototype mismatch.  Hence this less than aesthetically
-# appealing BEGIN block:
-
-BEGIN {
-       unless( eval { defined SEEK_SET() } ) {
-               *SEEK_SET = sub { 0 };
-               *SEEK_CUR = sub { 1 };
-               *SEEK_END = sub { 2 };
-       }
-
-       unless( eval { defined O_BINARY() } ) {
-               *O_BINARY = sub { 0 };
-               *O_RDONLY = sub { 0 };
-               *O_WRONLY = sub { 1 };
-       }
-
-       unless ( eval { defined O_APPEND() } ) {
-
-               if ( $^O =~ /olaris/ ) {
-                       *O_APPEND = sub { 8 };
-                       *O_CREAT = sub { 256 };
-                       *O_EXCL = sub { 1024 };
-               }
-               elsif ( $^O =~ /inux/ ) {
-                       *O_APPEND = sub { 1024 };
-                       *O_CREAT = sub { 64 };
-                       *O_EXCL = sub { 128 };
-               }
-               elsif ( $^O =~ /BSD/i ) {
-                       *O_APPEND = sub { 8 };
-                       *O_CREAT = sub { 512 };
-                       *O_EXCL = sub { 2048 };
-               }
-       }
-}
-
-# print "OS [$^O]\n" ;
-
-# print "O_BINARY = ", O_BINARY(), "\n" ;
-# print "O_RDONLY = ", O_RDONLY(), "\n" ;
-# print "O_WRONLY = ", O_WRONLY(), "\n" ;
-# print "O_APPEND = ", O_APPEND(), "\n" ;
-# print "O_CREAT   ", O_CREAT(), "\n" ;
-# print "O_EXCL   ", O_EXCL(), "\n" ;
-
-use base 'Exporter' ;
-use vars qw( %EXPORT_TAGS @EXPORT_OK $VERSION @EXPORT ) ;
-
-%EXPORT_TAGS = ( 'all' => [
-       qw( read_file write_file overwrite_file append_file read_dir ) ] ) ;
-
-@EXPORT = ( @{ $EXPORT_TAGS{'all'} } );
-@EXPORT_OK = qw( slurp ) ;
-
-$VERSION = '9999.13';
-
-*slurp = \&read_file ;
-
-sub read_file {
-
-       my( $file_name, %args ) = @_ ;
-
-# set the buffer to either the passed in one or ours and init it to the null
-# string
-
-       my $buf ;
-       my $buf_ref = $args{'buf_ref'} || \$buf ;
-       ${$buf_ref} = '' ;
-
-       my( $read_fh, $size_left, $blk_size ) ;
-
-# check if we are reading from a handle (glob ref or IO:: object)
-
-       if ( ref $file_name ) {
-
-# slurping a handle so use it and don't open anything.
-# set the block size so we know it is a handle and read that amount
-
-               $read_fh = $file_name ;
-               $blk_size = $args{'blk_size'} || 1024 * 1024 ;
-               $size_left = $blk_size ;
-
-# DEEP DARK MAGIC. this checks the UNTAINT IO flag of a
-# glob/handle. only the DATA handle is untainted (since it is from
-# trusted data in the source file). this allows us to test if this is
-# the DATA handle and then to do a sysseek to make sure it gets
-# slurped correctly. on some systems, the buffered i/o pointer is not
-# left at the same place as the fd pointer. this sysseek makes them
-# the same so slurping with sysread will work.
-
-               eval{ require B } ;
-
-               if ( $@ ) {
-
-                       @_ = ( \%args, <<ERR ) ;
-Can't find B.pm with this Perl: $!.
-That module is needed to slurp the DATA handle.
-ERR
-                       goto &_error ;
-               }
-
-               if ( B::svref_2object( $read_fh )->IO->IoFLAGS & 16 ) {
-
-# set the seek position to the current tell.
-
-                       sysseek( $read_fh, tell( $read_fh ), SEEK_SET ) ||
-                               croak "sysseek $!" ;
-               }
-       }
-       else {
-
-# a regular file. set the sysopen mode
-
-               my $mode = O_RDONLY ;
-
-#printf "RD: BINARY %x MODE %x\n", O_BINARY, $mode ;
-
-# open the file and handle any error
-
-               $read_fh = gensym ;
-               unless ( sysopen( $read_fh, $file_name, $mode ) ) {
-                       @_ = ( \%args, "read_file '$file_name' - sysopen: $!");
-                       goto &_error ;
-               }
-
-               binmode($read_fh, $args{'binmode'}) if $args{'binmode'};
-
-# get the size of the file for use in the read loop
-
-               $size_left = -s $read_fh ;
-
-               unless( $size_left ) {
-
-                       $blk_size = $args{'blk_size'} || 1024 * 1024 ;
-                       $size_left = $blk_size ;
-               }
-       }
-
-# infinite read loop. we exit when we are done slurping
-
-       while( 1 ) {
-
-# do the read and see how much we got
-
-               my $read_cnt = sysread( $read_fh, ${$buf_ref},
-                               $size_left, length ${$buf_ref} ) ;
-
-               if ( defined $read_cnt ) {
-
-# good read. see if we hit EOF (nothing left to read)
-
-                       last if $read_cnt == 0 ;
-
-# loop if we are slurping a handle. we don't track $size_left then.
-
-                       next if $blk_size ;
-
-# count down how much we read and loop if we have more to read.
-                       $size_left -= $read_cnt ;
-                       last if $size_left <= 0 ;
-                       next ;
-               }
-
-# handle the read error
-
-               @_ = ( \%args, "read_file '$file_name' - sysread: $!");
-               goto &_error ;
-       }
-
-# fix up cr/lf to be a newline if this is a windows text file
-
-       ${$buf_ref} =~ s/\015\012/\n/g if $is_win32 && !$args{'binmode'} ;
-
-# this is the 5 returns in a row. each handles one possible
-# combination of caller context and requested return type
-
-       my $sep = $/ ;
-       $sep = '\n\n+' if defined $sep && $sep eq '' ;
-
-# caller wants to get an array ref of lines
-
-# this split doesn't work since it tries to use variable length lookbehind
-# the m// line works.
-#      return [ split( m|(?<=$sep)|, ${$buf_ref} ) ] if $args{'array_ref'}  ;
-       return [ length(${$buf_ref}) ? ${$buf_ref} =~ /(.*?$sep|.+)/sg : () ]
-               if $args{'array_ref'}  ;
-
-# caller wants a list of lines (normal list context)
-
-# same problem with this split as before.
-#      return split( m|(?<=$sep)|, ${$buf_ref} ) if wantarray ;
-       return length(${$buf_ref}) ? ${$buf_ref} =~ /(.*?$sep|.+)/sg : ()
-               if wantarray ;
-
-# caller wants a scalar ref to the slurped text
-
-       return $buf_ref if $args{'scalar_ref'} ;
-
-# caller wants a scalar with the slurped text (normal scalar context)
-
-       return ${$buf_ref} if defined wantarray ;
-
-# caller passed in an i/o buffer by reference (normal void context)
-
-       return ;
-}
-
-sub write_file {
-
-       my $file_name = shift ;
-
-# get the optional argument hash ref from @_ or an empty hash ref.
-
-       my $args = ( ref $_[0] eq 'HASH' ) ? shift : {} ;
-
-       my( $buf_ref, $write_fh, $no_truncate, $orig_file_name, $data_is_ref ) ;
-
-# get the buffer ref - it depends on how the data is passed into write_file
-# after this if/else $buf_ref will have a scalar ref to the data.
-
-       if ( ref $args->{'buf_ref'} eq 'SCALAR' ) {
-
-# a scalar ref passed in %args has the data
-# note that the data was passed by ref
-
-               $buf_ref = $args->{'buf_ref'} ;
-               $data_is_ref = 1 ;
-       }
-       elsif ( ref $_[0] eq 'SCALAR' ) {
-
-# the first value in @_ is the scalar ref to the data
-# note that the data was passed by ref
-
-               $buf_ref = shift ;
-               $data_is_ref = 1 ;
-       }
-       elsif ( ref $_[0] eq 'ARRAY' ) {
-
-# the first value in @_ is the array ref to the data so join it.
-
-               ${$buf_ref} = join '', @{$_[0]} ;
-       }
-       else {
-
-# good old @_ has all the data so join it.
-
-               ${$buf_ref} = join '', @_ ;
-       }
-
-# see if we were passed a open handle to spew to.
-
-       if ( ref $file_name ) {
-
-# we have a handle. make sure we don't call truncate on it.
-
-               $write_fh = $file_name ;
-               $no_truncate = 1 ;
-       }
-       else {
-
-# spew to regular file.
-
-               if ( $args->{'atomic'} ) {
-
-# in atomic mode, we spew to a temp file so make one and save the original
-# file name.
-                       $orig_file_name = $file_name ;
-                       $file_name .= ".$$" ;
-               }
-
-# set the mode for the sysopen
-
-               my $mode = O_WRONLY | O_CREAT ;
-               $mode |= O_APPEND if $args->{'append'} ;
-               $mode |= O_EXCL if $args->{'no_clobber'} ;
-
-#printf "WR: BINARY %x MODE %x\n", O_BINARY, $mode ;
-
-# open the file and handle any error.
-
-               $write_fh = gensym ;
-               unless ( sysopen( $write_fh, $file_name, $mode ) ) {
-                       @_ = ( $args, "write_file '$file_name' - sysopen: $!");
-                       goto &_error ;
-               }
-
-               binmode($write_fh, $args->{'binmode'}) if $args->{'binmode'};
-       }
-
-       sysseek( $write_fh, 0, SEEK_END ) if $args->{'append'} ;
-
-
-#print 'WR before data ', unpack( 'H*', ${$buf_ref}), "\n" ;
-
-# fix up newline to write cr/lf if this is a windows text file
-
-       if ( $is_win32 && !$args->{'binmode'} ) {
-
-# copy the write data if it was passed by ref so we don't clobber the
-# caller's data
-               $buf_ref = \do{ my $copy = ${$buf_ref}; } if $data_is_ref ;
-               ${$buf_ref} =~ s/\n/\015\012/g ;
-       }
-
-#print 'after data ', unpack( 'H*', ${$buf_ref}), "\n" ;
-
-# get the size of how much we are writing and init the offset into that buffer
-
-       my $size_left = length( ${$buf_ref} ) ;
-       my $offset = 0 ;
-
-# loop until we have no more data left to write
-
-       do {
-
-# do the write and track how much we just wrote
-
-               my $write_cnt = syswrite( $write_fh, ${$buf_ref},
-                               $size_left, $offset ) ;
-
-               unless ( defined $write_cnt ) {
-
-# the write failed
-                       @_ = ( $args, "write_file '$file_name' - syswrite: $!");
-                       goto &_error ;
-               }
-
-# track much left to write and where to write from in the buffer
-
-               $size_left -= $write_cnt ;
-               $offset += $write_cnt ;
-
-       } while( $size_left > 0 ) ;
-
-# we truncate regular files in case we overwrite a long file with a shorter file
-# so seek to the current position to get it (same as tell()).
-
-       truncate( $write_fh,
-                 sysseek( $write_fh, 0, SEEK_CUR ) ) unless $no_truncate ;
-
-       close( $write_fh ) ;
-
-# handle the atomic mode - move the temp file to the original filename.
-
-       rename( $file_name, $orig_file_name ) if $args->{'atomic'} ;
-
-       return 1 ;
-}
-
-# this is for backwards compatibility with the previous File::Slurp module. 
-# write_file always overwrites an existing file
-
-*overwrite_file = \&write_file ;
-
-# the current write_file has an append mode so we use that. this
-# supports the same API with an optional second argument which is a
-# hash ref of options.
-
-sub append_file {
-
-# get the optional args hash ref
-       my $args = $_[1] ;
-       if ( ref $args eq 'HASH' ) {
-
-# we were passed an args ref so just mark the append mode
-
-               $args->{append} = 1 ;
-       }
-       else {
-
-# no args hash so insert one with the append mode
-
-               splice( @_, 1, 0, { append => 1 } ) ;
-       }
-
-# magic goto the main write_file sub. this overlays the sub without touching
-# the stack or @_
-
-       goto &write_file
-}
-
-# basic wrapper around opendir/readdir
-
-sub read_dir {
-
-       my ($dir, %args ) = @_;
-
-# this handle will be destroyed upon return
-
-       local(*DIRH);
-
-# open the dir and handle any errors
-
-       unless ( opendir( DIRH, $dir ) ) {
-
-               @_ = ( \%args, "read_dir '$dir' - opendir: $!" ) ;
-               goto &_error ;
-       }
-
-       my @dir_entries = readdir(DIRH) ;
-
-       @dir_entries = grep( $_ ne "." && $_ ne "..", @dir_entries )
-               unless $args{'keep_dot_dot'} ;
-
-       return @dir_entries if wantarray ;
-       return \@dir_entries ;
-}
-
-# error handling section
-#
-# all the error handling uses magic goto so the caller will get the
-# error message as if from their code and not this module. if we just
-# did a call on the error code, the carp/croak would report it from
-# this module since the error sub is one level down on the call stack
-# from read_file/write_file/read_dir.
-
-
-my %err_func = (
-       'carp'  => \&carp,
-       'croak' => \&croak,
-) ;
-
-sub _error {
-
-       my( $args, $err_msg ) = @_ ;
-
-# get the error function to use
-
-       my $func = $err_func{ $args->{'err_mode'} || 'croak' } ;
-
-# if we didn't find it in our error function hash, they must have set
-# it to quiet and we don't do anything.
-
-       return unless $func ;
-
-# call the carp/croak function
-
-       $func->($err_msg) ;
-
-# return a hard undef (in list context this will be a single value of
-# undef which is not a legal in-band value)
-
-       return undef ;
-}
-
-1;
-__END__
-
-=head1 NAME
-
-File::Slurp - Efficient Reading/Writing of Complete Files
-
-=head1 SYNOPSIS
-
-  use File::Slurp;
-
-  my $text = read_file( 'filename' ) ;
-  my @lines = read_file( 'filename' ) ;
-
-  write_file( 'filename', @lines ) ;
-
-  use File::Slurp qw( slurp ) ;
-
-  my $text = slurp( 'filename' ) ;
-
-
-=head1 DESCRIPTION
-
-This module provides subs that allow you to read or write entire files
-with one simple call. They are designed to be simple to use, have
-flexible ways to pass in or get the file contents and to be very
-efficient.  There is also a sub to read in all the files in a
-directory other than C<.> and C<..>
-
-These slurp/spew subs work for files, pipes and
-sockets, and stdio, pseudo-files, and DATA.
-
-=head2 B<read_file>
-
-This sub reads in an entire file and returns its contents to the
-caller. In list context it will return a list of lines (using the
-current value of $/ as the separator including support for paragraph
-mode when it is set to ''). In scalar context it returns the entire
-file as a single scalar.
-
-  my $text = read_file( 'filename' ) ;
-  my @lines = read_file( 'filename' ) ;
-
-The first argument to C<read_file> is the filename and the rest of the
-arguments are key/value pairs which are optional and which modify the
-behavior of the call. Other than binmode the options all control how
-the slurped file is returned to the caller.
-
-If the first argument is a file handle reference or I/O object (if ref
-is true), then that handle is slurped in. This mode is supported so
-you slurp handles such as C<DATA>, C<STDIN>. See the test handle.t
-for an example that does C<open( '-|' )> and child process spews data
-to the parant which slurps it in.  All of the options that control how
-the data is returned to the caller still work in this case.
-
-NOTE: as of version 9999.06, read_file works correctly on the C<DATA>
-handle. It used to need a sysseek workaround but that is now handled
-when needed by the module itself.
-
-You can optionally request that C<slurp()> is exported to your code. This
-is an alias for read_file and is meant to be forward compatible with
-Perl 6 (which will have slurp() built-in).
-
-The options are:
-
-=head3 binmode
-
-If you set the binmode option, then the file will be slurped in binary
-mode.
-
-       my $bin_data = read_file( $bin_file, binmode => ':raw' ) ;
-       # Or
-       my $bin_data = read_file( $bin_file, binmode => ':utf8' ) ;
-
-=head3 array_ref
-
-If this boolean option is set, the return value (only in scalar
-context) will be an array reference which contains the lines of the
-slurped file. The following two calls are equivalent:
-
-       my $lines_ref = read_file( $bin_file, array_ref => 1 ) ;
-       my $lines_ref = [ read_file( $bin_file ) ] ;
-
-=head3 scalar_ref
-
-If this boolean option is set, the return value (only in scalar
-context) will be an scalar reference to a string which is the contents
-of the slurped file. This will usually be faster than returning the
-plain scalar.
-
-       my $text_ref = read_file( $bin_file, scalar_ref => 1 ) ;
-
-=head3 buf_ref
-
-You can use this option to pass in a scalar reference and the slurped
-file contents will be stored in the scalar. This can be used in
-conjunction with any of the other options.
-
-       my $text_ref = read_file( $bin_file, buf_ref => \$buffer,
-                                            array_ref => 1 ) ;
-       my @lines = read_file( $bin_file, buf_ref => \$buffer ) ;
-
-=head3 blk_size
-
-You can use this option to set the block size used when slurping from an already open handle (like \*STDIN). It defaults to 1MB.
-
-       my $text_ref = read_file( $bin_file, blk_size => 10_000_000,
-                                            array_ref => 1 ) ;
-
-=head3 err_mode
-
-You can use this option to control how read_file behaves when an error
-occurs. This option defaults to 'croak'. You can set it to 'carp' or
-to 'quiet to have no error handling. This code wants to carp and then
-read abother file if it fails.
-
-       my $text_ref = read_file( $file, err_mode => 'carp' ) ;
-       unless ( $text_ref ) {
-
-               # read a different file but croak if not found
-               $text_ref = read_file( $another_file ) ;
-       }
-       
-       # process ${$text_ref}
-
-=head2 B<write_file>
-
-This sub writes out an entire file in one call.
-
-  write_file( 'filename', @data ) ;
-
-The first argument to C<write_file> is the filename. The next argument
-is an optional hash reference and it contains key/values that can
-modify the behavior of C<write_file>. The rest of the argument list is
-the data to be written to the file.
-
-  write_file( 'filename', {append => 1 }, @data ) ;
-  write_file( 'filename', {binmode => ':raw' }, $buffer ) ;
-
-As a shortcut if the first data argument is a scalar or array
-reference, it is used as the only data to be written to the file. Any
-following arguments in @_ are ignored. This is a faster way to pass in
-the output to be written to the file and is equivilent to the
-C<buf_ref> option. These following pairs are equivilent but the pass
-by reference call will be faster in most cases (especially with larger
-files).
-
-  write_file( 'filename', \$buffer ) ;
-  write_file( 'filename', $buffer ) ;
-
-  write_file( 'filename', \@lines ) ;
-  write_file( 'filename', @lines ) ;
-
-If the first argument is a file handle reference or I/O object (if ref
-is true), then that handle is slurped in. This mode is supported so
-you spew to handles such as \*STDOUT. See the test handle.t for an
-example that does C<open( '-|' )> and child process spews data to the
-parant which slurps it in.  All of the options that control how the
-data is passes into C<write_file> still work in this case.
-
-C<write_file> returns 1 upon successfully writing the file or undef if
-it encountered an error.
-
-The options are:
-
-=head3 binmode
-
-If you set the binmode option, then the file will be written in binary
-mode.
-
-       write_file( $bin_file, {binmode => ':raw'}, @data ) ;
-       # Or
-       write_file( $bin_file, {binmode => ':utf8'}, @data ) ;
-
-=head3 buf_ref
-
-You can use this option to pass in a scalar reference which has the
-data to be written. If this is set then any data arguments (including
-the scalar reference shortcut) in @_ will be ignored. These are
-equivilent:
-
-       write_file( $bin_file, { buf_ref => \$buffer } ) ;
-       write_file( $bin_file, \$buffer ) ;
-       write_file( $bin_file, $buffer ) ;
-
-=head3 atomic
-
-If you set this boolean option, the file will be written to in an
-atomic fashion. A temporary file name is created by appending the pid
-($$) to the file name argument and that file is spewed to. After the
-file is closed it is renamed to the original file name (and rename is
-an atomic operation on most OS's). If the program using this were to
-crash in the middle of this, then the file with the pid suffix could
-be left behind.
-
-=head3 append
-
-If you set this boolean option, the data will be written at the end of
-the current file.
-
-       write_file( $file, {append => 1}, @data ) ;
-
-C<write_file> croaks if it cannot open the file. It returns true if it
-succeeded in writing out the file and undef if there was an
-error. (Yes, I know if it croaks it can't return anything but that is
-for when I add the options to select the error handling mode).
-
-=head3 no_clobber
-
-If you set this boolean option, an existing file will not be overwritten.
-
-       write_file( $file, {no_clobber => 1}, @data ) ;
-
-=head3 err_mode
-
-You can use this option to control how C<write_file> behaves when an
-error occurs. This option defaults to 'croak'. You can set it to
-'carp' or to 'quiet' to have no error handling other than the return
-value. If the first call to C<write_file> fails it will carp and then
-write to another file. If the second call to C<write_file> fails, it
-will croak.
-
-       unless ( write_file( $file, { err_mode => 'carp', \$data ) ;
-
-               # write a different file but croak if not found
-               write_file( $other_file, \$data ) ;
-       }
-
-=head2 overwrite_file
-
-This sub is just a typeglob alias to write_file since write_file
-always overwrites an existing file. This sub is supported for
-backwards compatibility with the original version of this module. See
-write_file for its API and behavior.
-
-=head2 append_file
-
-This sub will write its data to the end of the file. It is a wrapper
-around write_file and it has the same API so see that for the full
-documentation. These calls are equivilent:
-
-       append_file( $file, @data ) ;
-       write_file( $file, {append => 1}, @data ) ;
-
-=head2 read_dir
-
-This sub reads all the file names from directory and returns them to
-the caller but C<.> and C<..> are removed by default.
-
-       my @files = read_dir( '/path/to/dir' ) ;
-
-It croaks if it cannot open the directory.
-
-In a list context C<read_dir> returns a list of the entries in the
-directory. In a scalar context it returns an array reference which has
-the entries.
-
-=head3 keep_dot_dot
-
-If this boolean option is set, C<.> and C<..> are not removed from the
-list of files.
-
-       my @all_files = read_dir( '/path/to/dir', keep_dot_dot => 1 ) ;
-
-=head2 EXPORT
-
-  read_file write_file overwrite_file append_file read_dir
-
-=head2 SEE ALSO
-
-An article on file slurping in extras/slurp_article.pod. There is
-also a benchmarking script in extras/slurp_bench.pl.
-
-=head2 BUGS
-
-If run under Perl 5.004, slurping from the DATA handle will fail as
-that requires B.pm which didn't get into core until 5.005.
-
-=head1 AUTHOR
-
-Uri Guttman, E<lt>uri@stemsystems.comE<gt>
-
-=cut
diff --git a/modules/fallback/List/MoreUtils.pm b/modules/fallback/List/MoreUtils.pm
deleted file mode 100644 (file)
index 1251b53..0000000
+++ /dev/null
@@ -1,847 +0,0 @@
-package List::MoreUtils;
-
-use 5.00503;
-use strict;
-use Exporter   ();
-use DynaLoader ();
-
-use vars qw{ $VERSION @ISA @EXPORT_OK %EXPORT_TAGS };
-BEGIN {
-    $VERSION   = '0.30';
-    @ISA       = qw{ Exporter DynaLoader };
-    @EXPORT_OK = qw{
-        any all none notall true false
-        firstidx first_index lastidx last_index
-        insert_after insert_after_string
-        apply indexes
-        after after_incl before before_incl
-        firstval first_value lastval last_value
-        each_array each_arrayref
-        pairwise natatime
-        mesh zip uniq distinct
-        minmax part
-    };
-    %EXPORT_TAGS = (
-        all => \@EXPORT_OK,
-    );
-
-    # Load the XS at compile-time so that redefinition warnings will be
-    # thrown correctly if the XS versions of part or indexes loaded
-    eval {
-        # PERL_DL_NONLAZY must be false, or any errors in loading will just
-        # cause the perl code to be tested
-        local $ENV{PERL_DL_NONLAZY} = 0 if $ENV{PERL_DL_NONLAZY};
-
-        bootstrap List::MoreUtils $VERSION;
-        1;
-
-    } unless $ENV{LIST_MOREUTILS_PP};
-}
-
-# Always use Perl apply() until memory leaks are resolved.
-sub apply (&@) {
-    my $action = shift;
-    &$action foreach my @values = @_;
-    wantarray ? @values : $values[-1];
-}
-
-# Always use Perl part() until memory leaks are resolved.
-sub part (&@) {
-    my ($code, @list) = @_;
-    my @parts;
-    push @{ $parts[ $code->($_) ] }, $_  foreach @list;
-    return @parts;
-}
-
-# Always use Perl indexes() until memory leaks are resolved.
-sub indexes (&@) {
-    my $test = shift;
-    grep {
-        local *_ = \$_[$_];
-        $test->()
-    } 0 .. $#_;
-}
-
-# Load the pure-Perl versions of the other functions if needed
-eval <<'END_PERL' unless defined &any;
-
-# Use pure scalar boolean return values for compatibility with XS
-use constant YES => ! 0;
-use constant NO  => ! 1;
-
-sub any (&@) {
-    my $f = shift;
-    foreach ( @_ ) {
-        return YES if $f->();
-    }
-    return NO;
-}
-
-sub all (&@) {
-    my $f = shift;
-    foreach ( @_ ) {
-        return NO unless $f->();
-    }
-    return YES;
-}
-
-sub none (&@) {
-    my $f = shift;
-    foreach ( @_ ) {
-        return NO if $f->();
-    }
-    return YES;
-}
-
-sub notall (&@) {
-    my $f = shift;
-    foreach ( @_ ) {
-        return YES unless $f->();
-    }
-    return NO;
-}
-
-sub true (&@) {
-    my $f     = shift;
-    my $count = 0;
-    foreach ( @_ ) {
-        $count++ if $f->();
-    }
-    return $count;
-}
-
-sub false (&@) {
-    my $f     = shift;
-    my $count = 0;
-    foreach ( @_ ) {
-        $count++ unless $f->();
-    }
-    return $count;
-}
-
-sub firstidx (&@) {
-    my $f = shift;
-    foreach my $i ( 0 .. $#_ ) {
-        local *_ = \$_[$i];
-        return $i if $f->();
-    }
-    return -1;
-}
-
-sub lastidx (&@) {
-    my $f = shift;
-    foreach my $i ( reverse 0 .. $#_ ) {
-        local *_ = \$_[$i];
-        return $i if $f->();
-    }
-    return -1;
-}
-
-sub insert_after (&$\@) {
-    my ($f, $val, $list) = @_;
-    my $c = -1;
-    local *_;
-    foreach my $i ( 0 .. $#$list ) {
-        $_ = $list->[$i];
-        $c = $i, last if $f->();
-    }
-    @$list = (
-        @{$list}[ 0 .. $c ],
-        $val,
-        @{$list}[ $c + 1 .. $#$list ],
-    ) and return 1 if $c != -1;
-    return 0;
-}
-
-sub insert_after_string ($$\@) {
-    my ($string, $val, $list) = @_;
-    my $c = -1;
-    foreach my $i ( 0 .. $#$list ) {
-        local $^W = 0;
-        $c = $i, last if $string eq $list->[$i];
-    }
-    @$list = (
-        @{$list}[ 0 .. $c ],
-        $val,
-        @{$list}[ $c + 1 .. $#$list ],
-    ) and return 1 if $c != -1;
-    return 0;
-}
-
-sub after (&@) {
-    my $test = shift;
-    my $started;
-    my $lag;
-    grep $started ||= do {
-        my $x = $lag;
-        $lag = $test->();
-        $x
-    }, @_;
-}
-
-sub after_incl (&@) {
-    my $test = shift;
-    my $started;
-    grep $started ||= $test->(), @_;
-}
-
-sub before (&@) {
-    my $test = shift;
-    my $more = 1;
-    grep $more &&= ! $test->(), @_;
-}
-
-sub before_incl (&@) {
-    my $test = shift;
-    my $more = 1;
-    my $lag  = 1;
-    grep $more &&= do {
-        my $x = $lag;
-        $lag = ! $test->();
-        $x
-    }, @_;
-}
-
-sub lastval (&@) {
-    my $test = shift;
-    my $ix;
-    for ( $ix = $#_; $ix >= 0; $ix-- ) {
-        local *_ = \$_[$ix];
-        my $testval = $test->();
-
-        # Simulate $_ as alias
-        $_[$ix] = $_;
-        return $_ if $testval;
-    }
-    return undef;
-}
-
-sub firstval (&@) {
-    my $test = shift;
-    foreach ( @_ ) {
-        return $_ if $test->();
-    }
-    return undef;
-}
-
-sub pairwise (&\@\@) {
-    my $op = shift;
-
-    # Symbols for caller's input arrays
-    use vars qw{ @A @B };
-    local ( *A, *B ) = @_;
-
-    # Localise $a, $b
-    my ( $caller_a, $caller_b ) = do {
-        my $pkg = caller();
-        no strict 'refs';
-        \*{$pkg.'::a'}, \*{$pkg.'::b'};
-    };
-
-    # Loop iteration limit
-    my $limit = $#A > $#B? $#A : $#B;
-
-    # This map expression is also the return value
-    local( *$caller_a, *$caller_b );
-    map {
-        # Assign to $a, $b as refs to caller's array elements
-        ( *$caller_a, *$caller_b ) = \( $A[$_], $B[$_] );
-
-        # Perform the transformation
-        $op->();
-    }  0 .. $limit;
-}
-
-sub each_array (\@;\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@) {
-    return each_arrayref(@_);
-}
-
-sub each_arrayref {
-    my @list  = @_; # The list of references to the arrays
-    my $index = 0;  # Which one the caller will get next
-    my $max   = 0;  # Number of elements in longest array
-
-    # Get the length of the longest input array
-    foreach ( @list ) {
-        unless ( ref $_ eq 'ARRAY' ) {
-            require Carp;
-            Carp::croak("each_arrayref: argument is not an array reference\n");
-        }
-        $max = @$_ if @$_ > $max;
-    }
-
-    # Return the iterator as a closure wrt the above variables.
-    return sub {
-        if ( @_ ) {
-            my $method = shift;
-            unless ( $method eq 'index' ) {
-                require Carp;
-                Carp::croak("each_array: unknown argument '$method' passed to iterator.");
-            }
-
-            # Return current (last fetched) index
-            return undef if $index == 0  ||  $index > $max;
-            return $index - 1;
-        }
-
-        # No more elements to return
-        return if $index >= $max;
-        my $i = $index++;
-
-        # Return ith elements
-        return map $_->[$i], @list; 
-    }
-}
-
-sub natatime ($@) {
-    my $n    = shift;
-    my @list = @_;
-    return sub {
-        return splice @list, 0, $n;
-    }
-}
-
-sub mesh (\@\@;\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@) {
-    my $max = -1;
-    $max < $#$_ && ( $max = $#$_ ) foreach @_;
-    map {
-        my $ix = $_;
-        map $_->[$ix], @_;
-    } 0 .. $max; 
-}
-
-sub uniq (@) {
-    my %seen = ();
-    grep { not $seen{$_}++ } @_;
-}
-
-sub minmax (@) {
-    return unless @_;
-    my $min = my $max = $_[0];
-
-    for ( my $i = 1; $i < @_; $i += 2 ) {
-        if ( $_[$i-1] <= $_[$i] ) {
-            $min = $_[$i-1] if $min > $_[$i-1];
-            $max = $_[$i]   if $max < $_[$i];
-        } else {
-            $min = $_[$i]   if $min > $_[$i];
-            $max = $_[$i-1] if $max < $_[$i-1];
-        }
-    }
-
-    if ( @_ & 1 ) {
-        my $i = $#_;
-        if ($_[$i-1] <= $_[$i]) {
-            $min = $_[$i-1] if $min > $_[$i-1];
-            $max = $_[$i]   if $max < $_[$i];
-        } else {
-            $min = $_[$i]   if $min > $_[$i];
-            $max = $_[$i-1] if $max < $_[$i-1];
-        }
-    }
-
-    return ($min, $max);
-}
-
-sub _XScompiled {
-    return 0;
-}
-
-END_PERL
-die $@ if $@;
-
-# Function aliases
-*first_index = \&firstidx;
-*last_index  = \&lastidx;
-*first_value = \&firstval;
-*last_value  = \&lastval;
-*zip         = \&mesh;
-*distinct    = \&uniq;
-
-1;
-
-__END__
-
-=pod
-
-=head1 NAME
-
-List::MoreUtils - Provide the stuff missing in List::Util
-
-=head1 SYNOPSIS
-
-    use List::MoreUtils qw{
-        any all none notall true false
-        firstidx first_index lastidx last_index
-        insert_after insert_after_string
-        apply indexes
-        after after_incl before before_incl
-        firstval first_value lastval last_value
-        each_array each_arrayref
-        pairwise natatime
-        mesh zip uniq distinct minmax part
-    };
-
-=head1 DESCRIPTION
-
-B<List::MoreUtils> provides some trivial but commonly needed functionality on
-lists which is not going to go into L<List::Util>.
-
-All of the below functions are implementable in only a couple of lines of Perl
-code. Using the functions from this module however should give slightly better
-performance as everything is implemented in C. The pure-Perl implementation of
-these functions only serves as a fallback in case the C portions of this module
-couldn't be compiled on this machine.
-
-=over 4
-
-=item any BLOCK LIST
-
-Returns a true value if any item in LIST meets the criterion given through
-BLOCK. Sets C<$_> for each item in LIST in turn:
-
-    print "At least one value undefined"
-        if any { ! defined($_) } @list;
-
-Returns false otherwise, or if LIST is empty.
-
-=item all BLOCK LIST
-
-Returns a true value if all items in LIST meet the criterion given through
-BLOCK. Sets C<$_> for each item in LIST in turn:
-
-    print "All items defined"
-        if all { defined($_) } @list;
-
-Returns false otherwise, or if LIST is empty.
-
-=item none BLOCK LIST
-
-Logically the negation of C<any>. Returns a true value if no item in LIST meets
-the criterion given through BLOCK. Sets C<$_> for each item in LIST in turn:
-
-    print "No value defined"
-        if none { defined($_) } @list;
-
-Returns false otherwise, or if LIST is empty.
-
-=item notall BLOCK LIST
-
-Logically the negation of C<all>. Returns a true value if not all items in LIST
-meet the criterion given through BLOCK. Sets C<$_> for each item in LIST in
-turn:
-
-    print "Not all values defined"
-        if notall { defined($_) } @list;
-
-Returns false otherwise, or if LIST is empty.
-
-=item true BLOCK LIST
-
-Counts the number of elements in LIST for which the criterion in BLOCK is true.
-Sets C<$_> for  each item in LIST in turn:
-
-    printf "%i item(s) are defined", true { defined($_) } @list;
-
-=item false BLOCK LIST
-
-Counts the number of elements in LIST for which the criterion in BLOCK is false.
-Sets C<$_> for each item in LIST in turn:
-
-    printf "%i item(s) are not defined", false { defined($_) } @list;
-
-=item firstidx BLOCK LIST
-
-=item first_index BLOCK LIST
-
-Returns the index of the first element in LIST for which the criterion in BLOCK
-is true. Sets C<$_> for each item in LIST in turn:
-
-    my @list = (1, 4, 3, 2, 4, 6);
-    printf "item with index %i in list is 4", firstidx { $_ == 4 } @list;
-    __END__
-    item with index 1 in list is 4
-    
-Returns C<-1> if no such item could be found.
-
-C<first_index> is an alias for C<firstidx>.
-
-=item lastidx BLOCK LIST
-
-=item last_index BLOCK LIST
-
-Returns the index of the last element in LIST for which the criterion in BLOCK
-is true. Sets C<$_> for each item in LIST in turn:
-
-    my @list = (1, 4, 3, 2, 4, 6);
-    printf "item with index %i in list is 4", lastidx { $_ == 4 } @list;
-    __END__
-    item with index 4 in list is 4
-
-Returns C<-1> if no such item could be found.
-
-C<last_index> is an alias for C<lastidx>.
-
-=item insert_after BLOCK VALUE LIST
-
-Inserts VALUE after the first item in LIST for which the criterion in BLOCK is
-true. Sets C<$_> for each item in LIST in turn.
-
-    my @list = qw/This is a list/;
-    insert_after { $_ eq "a" } "longer" => @list;
-    print "@list";
-    __END__
-    This is a longer list
-
-=item insert_after_string STRING VALUE LIST
-
-Inserts VALUE after the first item in LIST which is equal to STRING. 
-
-    my @list = qw/This is a list/;
-    insert_after_string "a", "longer" => @list;
-    print "@list";
-    __END__
-    This is a longer list
-
-=item apply BLOCK LIST
-
-Applies BLOCK to each item in LIST and returns a list of the values after BLOCK
-has been applied. In scalar context, the last element is returned.  This
-function is similar to C<map> but will not modify the elements of the input
-list:
-
-    my @list = (1 .. 4);
-    my @mult = apply { $_ *= 2 } @list;
-    print "\@list = @list\n";
-    print "\@mult = @mult\n";
-    __END__
-    @list = 1 2 3 4
-    @mult = 2 4 6 8
-
-Think of it as syntactic sugar for
-
-    for (my @mult = @list) { $_ *= 2 }
-
-=item before BLOCK LIST
-
-Returns a list of values of LIST upto (and not including) the point where BLOCK
-returns a true value. Sets C<$_> for each element in LIST in turn.
-
-=item before_incl BLOCK LIST
-
-Same as C<before> but also includes the element for which BLOCK is true.
-
-=item after BLOCK LIST
-
-Returns a list of the values of LIST after (and not including) the point
-where BLOCK returns a true value. Sets C<$_> for each element in LIST in turn.
-
-    @x = after { $_ % 5 == 0 } (1..9);    # returns 6, 7, 8, 9
-
-=item after_incl BLOCK LIST
-
-Same as C<after> but also inclues the element for which BLOCK is true.
-
-=item indexes BLOCK LIST
-
-Evaluates BLOCK for each element in LIST (assigned to C<$_>) and returns a list
-of the indices of those elements for which BLOCK returned a true value. This is
-just like C<grep> only that it returns indices instead of values:
-
-    @x = indexes { $_ % 2 == 0 } (1..10);   # returns 1, 3, 5, 7, 9
-
-=item firstval BLOCK LIST
-
-=item first_value BLOCK LIST
-
-Returns the first element in LIST for which BLOCK evaluates to true. Each
-element of LIST is set to C<$_> in turn. Returns C<undef> if no such element
-has been found.
-
-C<first_val> is an alias for C<firstval>.
-
-=item lastval BLOCK LIST
-
-=item last_value BLOCK LIST
-
-Returns the last value in LIST for which BLOCK evaluates to true. Each element
-of LIST is set to C<$_> in turn. Returns C<undef> if no such element has been
-found.
-
-C<last_val> is an alias for C<lastval>.
-
-=item pairwise BLOCK ARRAY1 ARRAY2
-
-Evaluates BLOCK for each pair of elements in ARRAY1 and ARRAY2 and returns a
-new list consisting of BLOCK's return values. The two elements are set to C<$a>
-and C<$b>.  Note that those two are aliases to the original value so changing
-them will modify the input arrays.
-
-    @a = (1 .. 5);
-    @b = (11 .. 15);
-    @x = pairwise { $a + $b } @a, @b;  # returns 12, 14, 16, 18, 20
-
-    # mesh with pairwise
-    @a = qw/a b c/;
-    @b = qw/1 2 3/;
-    @x = pairwise { ($a, $b) } @a, @b; # returns a, 1, b, 2, c, 3
-
-=item each_array ARRAY1 ARRAY2 ...
-
-Creates an array iterator to return the elements of the list of arrays ARRAY1,
-ARRAY2 throughout ARRAYn in turn.  That is, the first time it is called, it
-returns the first element of each array.  The next time, it returns the second
-elements.  And so on, until all elements are exhausted.
-
-This is useful for looping over more than one array at once:
-
-    my $ea = each_array(@a, @b, @c);
-    while ( my ($a, $b, $c) = $ea->() )   { .... }
-
-The iterator returns the empty list when it reached the end of all arrays.
-
-If the iterator is passed an argument of 'C<index>', then it retuns
-the index of the last fetched set of values, as a scalar.
-
-=item each_arrayref LIST
-
-Like each_array, but the arguments are references to arrays, not the
-plain arrays.
-
-=item natatime BLOCK LIST
-
-Creates an array iterator, for looping over an array in chunks of
-C<$n> items at a time.  (n at a time, get it?).  An example is
-probably a better explanation than I could give in words.
-
-Example:
-
-    my @x = ('a' .. 'g');
-    my $it = natatime 3, @x;
-    while (my @vals = $it->())
-    {
-        print "@vals\n";
-    }
-
-This prints
-
-    a b c
-    d e f
-    g
-
-=item mesh ARRAY1 ARRAY2 [ ARRAY3 ... ]
-
-=item zip ARRAY1 ARRAY2 [ ARRAY3 ... ]
-
-Returns a list consisting of the first elements of each array, then
-the second, then the third, etc, until all arrays are exhausted.
-
-Examples:
-
-    @x = qw/a b c d/;
-    @y = qw/1 2 3 4/;
-    @z = mesh @x, @y;      # returns a, 1, b, 2, c, 3, d, 4
-
-    @a = ('x');
-    @b = ('1', '2');
-    @c = qw/zip zap zot/;
-    @d = mesh @a, @b, @c;   # x, 1, zip, undef, 2, zap, undef, undef, zot
-
-C<zip> is an alias for C<mesh>.
-
-=item uniq LIST
-
-=item distinct LIST
-
-Returns a new list by stripping duplicate values in LIST. The order of
-elements in the returned list is the same as in LIST. In scalar context,
-returns the number of unique elements in LIST.
-
-    my @x = uniq 1, 1, 2, 2, 3, 5, 3, 4; # returns 1 2 3 5 4
-    my $x = uniq 1, 1, 2, 2, 3, 5, 3, 4; # returns 5
-
-=item minmax LIST
-
-Calculates the minimum and maximum of LIST and returns a two element list with
-the first element being the minimum and the second the maximum. Returns the
-empty list if LIST was empty.
-
-The C<minmax> algorithm differs from a naive iteration over the list where each
-element is compared to two values being the so far calculated min and max value
-in that it only requires 3n/2 - 2 comparisons. Thus it is the most efficient
-possible algorithm.
-
-However, the Perl implementation of it has some overhead simply due to the fact
-that there are more lines of Perl code involved. Therefore, LIST needs to be
-fairly big in order for C<minmax> to win over a naive implementation. This
-limitation does not apply to the XS version.
-
-=item part BLOCK LIST
-
-Partitions LIST based on the return value of BLOCK which denotes into which
-partition the current value is put.
-
-Returns a list of the partitions thusly created. Each partition created is a
-reference to an array.
-
-    my $i = 0;
-    my @part = part { $i++ % 2 } 1 .. 8;   # returns [1, 3, 5, 7], [2, 4, 6, 8]
-
-You can have a sparse list of partitions as well where non-set partitions will
-be undef:
-
-    my @part = part { 2 } 1 .. 10;         # returns undef, undef, [ 1 .. 10 ]
-
-Be careful with negative values, though:
-
-    my @part = part { -1 } 1 .. 10;
-    __END__
-    Modification of non-creatable array value attempted, subscript -1 ...
-
-Negative values are only ok when they refer to a partition previously created:
-
-    my @idx  = ( 0, 1, -1 );
-    my $i    = 0;
-    my @part = part { $idx[$++ % 3] } 1 .. 8; # [1, 4, 7], [2, 3, 5, 6, 8]
-
-=back
-
-=head1 EXPORTS
-
-Nothing by default. To import all of this module's symbols, do the conventional
-
-    use List::MoreUtils ':all';
-
-It may make more sense though to only import the stuff your program actually
-needs:
-
-    use List::MoreUtils qw{ any firstidx };
-
-=head1 ENVIRONMENT
-
-When C<LIST_MOREUTILS_PP> is set, the module will always use the pure-Perl
-implementation and not the XS one. This environment variable is really just
-there for the test-suite to force testing the Perl implementation, and possibly
-for reporting of bugs. I don't see any reason to use it in a production
-environment.
-
-=head1 BUGS
-
-There is a problem with a bug in 5.6.x perls. It is a syntax error to write
-things like:
-
-    my @x = apply { s/foo/bar/ } qw{ foo bar baz };
-
-It has to be written as either
-
-    my @x = apply { s/foo/bar/ } 'foo', 'bar', 'baz';
-
-or
-
-    my @x = apply { s/foo/bar/ } my @dummy = qw/foo bar baz/;
-
-Perl 5.5.x and Perl 5.8.x don't suffer from this limitation.
-
-If you have a functionality that you could imagine being in this module, please
-drop me a line. This module's policy will be less strict than L<List::Util>'s
-when it comes to additions as it isn't a core module.
-
-When you report bugs, it would be nice if you could additionally give me the
-output of your program with the environment variable C<LIST_MOREUTILS_PP> set
-to a true value. That way I know where to look for the problem (in XS,
-pure-Perl or possibly both).
-
-=head1 SUPPORT
-
-Bugs should always be submitted via the CPAN bug tracker.
-
-L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=List-MoreUtils>
-
-=head1 THANKS
-
-Credits go to a number of people: Steve Purkis for giving me namespace advice
-and James Keenan and Terrence Branno for their effort of keeping the CPAN
-tidier by making L<List::Utils> obsolete. 
-
-Brian McCauley suggested the inclusion of apply() and provided the pure-Perl
-implementation for it.
-
-Eric J. Roode asked me to add all functions from his module C<List::MoreUtil>
-into this one. With minor modifications, the pure-Perl implementations of those
-are by him.
-
-The bunch of people who almost immediately pointed out the many problems with
-the glitchy 0.07 release (Slaven Rezic, Ron Savage, CPAN testers).
-
-A particularly nasty memory leak was spotted by Thomas A. Lowery.
-
-Lars Thegler made me aware of problems with older Perl versions.
-
-Anno Siegel de-orphaned each_arrayref().
-
-David Filmer made me aware of a problem in each_arrayref that could ultimately
-lead to a segfault.
-
-Ricardo Signes suggested the inclusion of part() and provided the
-Perl-implementation.
-
-Robin Huston kindly fixed a bug in perl's MULTICALL API to make the
-XS-implementation of part() work.
-
-=head1 TODO
-
-A pile of requests from other people is still pending further processing in
-my mailbox. This includes:
-
-=over 4
-
-=item * List::Util export pass-through
-
-Allow B<List::MoreUtils> to pass-through the regular L<List::Util>
-functions to end users only need to C<use> the one module.
-
-=item * uniq_by(&@)
-
-Use code-reference to extract a key based on which the uniqueness is
-determined. Suggested by Aaron Crane.
-
-=item * delete_index
-
-=item * random_item
-
-=item * random_item_delete_index
-
-=item * list_diff_hash
-
-=item * list_diff_inboth
-
-=item * list_diff_infirst
-
-=item * list_diff_insecond
-
-These were all suggested by Dan Muey.
-
-=item * listify
-
-Always return a flat list when either a simple scalar value was passed or an
-array-reference. Suggested by Mark Summersault.
-
-=back
-
-=head1 SEE ALSO
-
-L<List::Util>
-
-=head1 AUTHOR
-
-Tassilo von Parseval E<lt>tassilo.von.parseval@rwth-aachen.deE<gt>
-
-=head1 COPYRIGHT AND LICENSE
-
-Copyright 2004 - 2010 by Tassilo von Parseval
-
-This library is free software; you can redistribute it and/or modify
-it under the same terms as Perl itself, either Perl version 5.8.4 or,
-at your option, any later version of Perl 5 you may have available.
-
-=cut
diff --git a/modules/fallback/List/UtilsBy.pm b/modules/fallback/List/UtilsBy.pm
deleted file mode 100644 (file)
index d4244f9..0000000
+++ /dev/null
@@ -1,529 +0,0 @@
-#  You may distribute under the terms of either the GNU General Public License
-#  or the Artistic License (the same terms as Perl itself)
-#
-#  (C) Paul Evans, 2009-2012 -- leonerd@leonerd.org.uk
-
-package List::UtilsBy;
-
-use strict;
-use warnings;
-
-our $VERSION = '0.09';
-
-use Exporter 'import';
-
-our @EXPORT_OK = qw(
-   sort_by
-   nsort_by
-   rev_sort_by
-   rev_nsort_by
-
-   max_by nmax_by
-   min_by nmin_by
-
-   uniq_by
-
-   partition_by
-   count_by
-
-   zip_by
-   unzip_by
-
-   extract_by
-
-   weighted_shuffle_by
-
-   bundle_by
-);
-
-=head1 NAME
-
-C<List::UtilsBy> - higher-order list utility functions
-
-=head1 SYNOPSIS
-
- use List::UtilsBy qw( nsort_by min_by );
-
- use File::stat qw( stat );
- my @files_by_age = nsort_by { stat($_)->mtime } @files;
-
- my $shortest_name = min_by { length } @names;
-
-=head1 DESCRIPTION
-
-This module provides a number of list utility functions, all of which take an
-initial code block to control their behaviour. They are variations on similar
-core perl or C<List::Util> functions of similar names, but which use the block
-to control their behaviour. For example, the core Perl function C<sort> takes
-a list of values and returns them, sorted into order by their string value.
-The C<sort_by> function sorts them according to the string value returned by
-the extra function, when given each value.
-
- my @names_sorted = sort @names;
-
- my @people_sorted = sort_by { $_->name } @people;
-
-=cut
-
-=head1 FUNCTIONS
-
-=cut
-
-=head2 @vals = sort_by { KEYFUNC } @vals
-
-Returns the list of values sorted according to the string values returned by
-the C<KEYFUNC> block or function. A typical use of this may be to sort objects
-according to the string value of some accessor, such as
-
- sort_by { $_->name } @people
-
-The key function is called in scalar context, being passed each value in turn
-as both C<$_> and the only argument in the parameters, C<@_>. The values are
-then sorted according to string comparisons on the values returned.
-
-This is equivalent to
-
- sort { $a->name cmp $b->name } @people
-
-except that it guarantees the C<name> accessor will be executed only once per
-value.
-
-One interesting use-case is to sort strings which may have numbers embedded in
-them "naturally", rather than lexically.
-
- sort_by { s/(\d+)/sprintf "%09d", $1/eg; $_ } @strings
-
-This sorts strings by generating sort keys which zero-pad the embedded numbers
-to some level (9 digits in this case), helping to ensure the lexical sort puts
-them in the correct order.
-
-=cut
-
-sub sort_by(&@)
-{
-   my $keygen = shift;
-
-   my @keys = map { local $_ = $_; scalar $keygen->( $_ ) } @_;
-   return @_[ sort { $keys[$a] cmp $keys[$b] } 0 .. $#_ ];
-}
-
-=head2 @vals = nsort_by { KEYFUNC } @vals
-
-Similar to C<sort_by> but compares its key values numerically.
-
-=cut
-
-sub nsort_by(&@)
-{
-   my $keygen = shift;
-
-   my @keys = map { local $_ = $_; scalar $keygen->( $_ ) } @_;
-   return @_[ sort { $keys[$a] <=> $keys[$b] } 0 .. $#_ ];
-}
-
-=head2 @vals = rev_sort_by { KEYFUNC } @vals
-
-=head2 @vals = rev_nsort_by { KEYFUNC } @vals
-
-Similar to C<sort_by> and C<nsort_by> but returns the list in the reverse
-order. Equivalent to
-
- @vals = reverse sort_by { KEYFUNC } @vals
-
-except that these functions are slightly more efficient because they avoid
-the final C<reverse> operation.
-
-=cut
-
-sub rev_sort_by(&@)
-{
-   my $keygen = shift;
-
-   my @keys = map { local $_ = $_; scalar $keygen->( $_ ) } @_;
-   return @_[ sort { $keys[$b] cmp $keys[$a] } 0 .. $#_ ];
-}
-
-sub rev_nsort_by(&@)
-{
-   my $keygen = shift;
-
-   my @keys = map { local $_ = $_; scalar $keygen->( $_ ) } @_;
-   return @_[ sort { $keys[$b] <=> $keys[$a] } 0 .. $#_ ];
-}
-
-=head2 $optimal = max_by { KEYFUNC } @vals
-
-=head2 @optimal = max_by { KEYFUNC } @vals
-
-Returns the (first) value from C<@vals> that gives the numerically largest
-result from the key function.
-
- my $tallest = max_by { $_->height } @people
-
- use File::stat qw( stat );
- my $newest = max_by { stat($_)->mtime } @files;
-
-In scalar context, the first maximal value is returned. In list context, a
-list of all the maximal values is returned. This may be used to obtain
-positions other than the first, if order is significant.
-
-If called on an empty list, an empty list is returned.
-
-For symmetry with the C<nsort_by> function, this is also provided under the
-name C<nmax_by> since it behaves numerically.
-
-=cut
-
-sub max_by(&@)
-{
-   my $code = shift;
-
-   return unless @_;
-
-   local $_;
-
-   my @maximal = $_ = shift @_;
-   my $max     = $code->( $_ );
-
-   foreach ( @_ ) {
-      my $this = $code->( $_ );
-      if( $this > $max ) {
-         @maximal = $_;
-         $max     = $this;
-      }
-      elsif( wantarray and $this == $max ) {
-         push @maximal, $_;
-      }
-   }
-
-   return wantarray ? @maximal : $maximal[0];
-}
-
-*nmax_by = \&max_by;
-
-=head2 $optimal = min_by { KEYFUNC } @vals
-
-=head2 @optimal = min_by { KEYFUNC } @vals
-
-Similar to C<max_by> but returns values which give the numerically smallest
-result from the key function. Also provided as C<nmin_by>
-
-=cut
-
-sub min_by(&@)
-{
-   my $code = shift;
-
-   return unless @_;
-
-   local $_;
-
-   my @minimal = $_ = shift @_;
-   my $min     = $code->( $_ );
-
-   foreach ( @_ ) {
-      my $this = $code->( $_ );
-      if( $this < $min ) {
-         @minimal = $_;
-         $min     = $this;
-      }
-      elsif( wantarray and $this == $min ) {
-         push @minimal, $_;
-      }
-   }
-
-   return wantarray ? @minimal : $minimal[0];
-}
-
-*nmin_by = \&min_by;
-
-=head2 @vals = uniq_by { KEYFUNC } @vals
-
-Returns a list of the subset of values for which the key function block
-returns unique values. The first value yielding a particular key is chosen,
-subsequent values are rejected.
-
- my @some_fruit = uniq_by { $_->colour } @fruit;
-
-To select instead the last value per key, reverse the input list. If the order
-of the results is significant, don't forget to reverse the result as well:
-
- my @some_fruit = reverse uniq_by { $_->colour } reverse @fruit;
-
-=cut
-
-sub uniq_by(&@)
-{
-   my $code = shift;
-
-   my %present;
-   return grep {
-      my $key = $code->( local $_ = $_ );
-      !$present{$key}++
-   } @_;
-}
-
-=head2 %parts = partition_by { KEYFUNC } @vals
-
-Returns a key/value list of ARRAY refs containing all the original values
-distributed according to the result of the key function block. Each value will
-be an ARRAY ref containing all the values which returned the string from the
-key function, in their original order.
-
- my %balls_by_colour = partition_by { $_->colour } @balls;
-
-Because the values returned by the key function are used as hash keys, they
-ought to either be strings, or at least well-behaved as strings (such as
-numbers, or object references which overload stringification in a suitable
-manner).
-
-=cut
-
-sub partition_by(&@)
-{
-   my $code = shift;
-
-   my %parts;
-   push @{ $parts{ $code->( local $_ = $_ ) } }, $_ for @_;
-
-   return %parts;
-}
-
-=head2 %counts = count_by { KEYFUNC } @vals
-
-Returns a key/value list of integers, giving the number of times the key
-function block returned the key, for each value in the list.
-
- my %count_of_balls = count_by { $_->colour } @balls;
-
-Because the values returned by the key function are used as hash keys, they
-ought to either be strings, or at least well-behaved as strings (such as
-numbers, or object references which overload stringification in a suitable
-manner).
-
-=cut
-
-sub count_by(&@)
-{
-   my $code = shift;
-
-   my %counts;
-   $counts{ $code->( local $_ = $_ ) }++ for @_;
-
-   return %counts;
-}
-
-=head2 @vals = zip_by { ITEMFUNC } \@arr0, \@arr1, \@arr2,...
-
-Returns a list of each of the values returned by the function block, when
-invoked with values from across each each of the given ARRAY references. Each
-value in the returned list will be the result of the function having been
-invoked with arguments at that position, from across each of the arrays given.
-
- my @transposition = zip_by { [ @_ ] } @matrix;
-
- my @names = zip_by { "$_[1], $_[0]" } \@firstnames, \@surnames;
-
- print zip_by { "$_[0] => $_[1]\n" } [ keys %hash ], [ values %hash ];
-
-If some of the arrays are shorter than others, the function will behave as if
-they had C<undef> in the trailing positions. The following two lines are
-equivalent:
-
- zip_by { f(@_) } [ 1, 2, 3 ], [ "a", "b" ]
- f( 1, "a" ), f( 2, "b" ), f( 3, undef )
-
-The item function is called by C<map>, so if it returns a list, the entire
-list is included in the result. This can be useful for example, for generating
-a hash from two separate lists of keys and values
-
- my %nums = zip_by { @_ } [qw( one two three )], [ 1, 2, 3 ];
- # %nums = ( one => 1, two => 2, three => 3 )
-
-(A function having this behaviour is sometimes called C<zipWith>, e.g. in
-Haskell, but that name would not fit the naming scheme used by this module).
-
-=cut
-
-sub zip_by(&@)
-{
-   my $code = shift;
-
-   @_ or return;
-
-   my $len = 0;
-   scalar @$_ > $len and $len = scalar @$_ for @_;
-
-   return map {
-      my $idx = $_;
-      $code->( map { $_[$_][$idx] } 0 .. $#_ )
-   } 0 .. $len-1;
-}
-
-=head2 $arr0, $arr1, $arr2, ... = unzip_by { ITEMFUNC } @vals
-
-Returns a list of ARRAY references containing the values returned by the
-function block, when invoked for each of the values given in the input list.
-Each of the returned ARRAY references will contain the values returned at that
-corresponding position by the function block. That is, the first returned
-ARRAY reference will contain all the values returned in the first position by
-the function block, the second will contain all the values from the second
-position, and so on.
-
- my ( $firstnames, $lastnames ) = unzip_by { m/^(.*?) (.*)$/ } @names;
-
-If the function returns lists of differing lengths, the result will be padded
-with C<undef> in the missing elements.
-
-This function is an inverse of C<zip_by>, if given a corresponding inverse
-function.
-
-=cut
-
-sub unzip_by(&@)
-{
-   my $code = shift;
-
-   my @ret;
-   foreach my $idx ( 0 .. $#_ ) {
-      my @slice = $code->( local $_ = $_[$idx] );
-      $#slice = $#ret if @slice < @ret;
-      $ret[$_][$idx] = $slice[$_] for 0 .. $#slice;
-   }
-
-   return @ret;
-}
-
-=head2 @vals = extract_by { SELECTFUNC } @arr
-
-Removes elements from the referenced array on which the selection function
-returns true, and returns a list containing those elements. This function is
-similar to C<grep>, except that it modifies the referenced array to remove the
-selected values from it, leaving only the unselected ones.
-
- my @red_balls = extract_by { $_->color eq "red" } @balls;
-
- # Now there are no red balls in the @balls array
-
-This function modifies a real array, unlike most of the other functions in this
-module. Because of this, it requires a real array, not just a list.
-
-This function is implemented by invoking C<splice()> on the array, not by
-constructing a new list and assigning it. One result of this is that weak
-references will not be disturbed.
-
- extract_by { !defined $_ } @refs;
-
-will leave weak references weakened in the C<@refs> array, whereas
-
- @refs = grep { defined $_ } @refs;
-
-will strengthen them all again.
-
-=cut
-
-sub extract_by(&\@)
-{
-   my $code = shift;
-   my ( $arrref ) = @_;
-
-   my @ret;
-   for( my $idx = 0; $idx < scalar @$arrref; ) {
-      if( $code->( local $_ = $arrref->[$idx] ) ) {
-         push @ret, splice @$arrref, $idx, 1, ();
-      }
-      else {
-         $idx++;
-      }
-   }
-
-   return @ret;
-}
-
-=head2 @vals = weighted_shuffle_by { WEIGHTFUNC } @vals
-
-Returns the list of values shuffled into a random order. The randomisation is
-not uniform, but weighted by the value returned by the C<WEIGHTFUNC>. The
-probabilty of each item being returned first will be distributed with the
-distribution of the weights, and so on recursively for the remaining items.
-
-=cut
-
-sub weighted_shuffle_by(&@)
-{
-   my $code = shift;
-   my @vals = @_;
-
-   my @weights = map { $code->( local $_ = $_ ) } @vals;
-
-   my @ret;
-   while( @vals > 1 ) {
-      my $total = 0; $total += $_ for @weights;
-      my $select = int rand $total;
-      my $idx = 0;
-      while( $select >= $weights[$idx] ) {
-         $select -= $weights[$idx++];
-      }
-
-      push @ret, splice @vals, $idx, 1, ();
-      splice @weights, $idx, 1, ();
-   }
-
-   push @ret, @vals if @vals;
-
-   return @ret;
-}
-
-=head2 @vals = bundle_by { BLOCKFUNC } $number, @vals
-
-Similar to a regular C<map> functional, returns a list of the values returned
-by C<BLOCKFUNC>. Values from the input list are given to the block function in
-bundles of C<$number>.
-
-If given a list of values whose length does not evenly divide by C<$number>,
-the final call will be passed fewer elements than the others.
-
-=cut
-
-sub bundle_by(&@)
-{
-   my $code = shift;
-   my $n = shift;
-
-   my @ret;
-   for( my ( $pos, $next ) = ( 0, $n ); $pos < @_; $pos = $next, $next += $n ) {
-      $next = @_ if $next > @_;
-      push @ret, $code->( @_[$pos .. $next-1] );
-   }
-   return @ret;
-}
-
-=head1 TODO
-
-=over 4
-
-=item * XS implementations
-
-These functions are currently all written in pure perl. Some at least, may
-benefit from having XS implementations to speed up their logic.
-
-=item * Merge into L<List::Util> or L<List::MoreUtils>
-
-This module shouldn't really exist. The functions should instead be part of
-one of the existing modules that already contain many list utility functions.
-Having Yet Another List Utilty Module just worsens the problem.
-
-I have attempted to contact the authors of both of the above modules, to no
-avail; therefore I decided it best to write and release this code here anyway
-so that it is at least on CPAN. Once there, we can then see how best to merge
-it into an existing module.
-
-=back
-
-=head1 AUTHOR
-
-Paul Evans <leonerd@leonerd.org.uk>
-
-=cut
-
-0x55AA;
diff --git a/modules/fallback/Regexp/IPv6.pm b/modules/fallback/Regexp/IPv6.pm
deleted file mode 100644 (file)
index 24ecf5d..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-package Regexp::IPv6;
-
-our $VERSION = '0.03';
-
-use strict;
-use warnings;
-
-require Exporter;
-our @ISA = qw(Exporter);
-our @EXPORT_OK = qw($IPv6_re);
-
-my $IPv4 = "((25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))";
-my $G = "[0-9a-fA-F]{1,4}";
-
-my @tail = ( ":",
-       "(:($G)?|$IPv4)",
-             ":($IPv4|$G(:$G)?|)",
-             "(:$IPv4|:$G(:$IPv4|(:$G){0,2})|:)",
-       "((:$G){0,2}(:$IPv4|(:$G){1,2})|:)",
-       "((:$G){0,3}(:$IPv4|(:$G){1,2})|:)",
-       "((:$G){0,4}(:$IPv4|(:$G){1,2})|:)" );
-
-our $IPv6_re = $G;
-$IPv6_re = "$G:($IPv6_re|$_)" for @tail;
-$IPv6_re = qq/:(:$G){0,5}((:$G){1,2}|:$IPv4)|$IPv6_re/;
-$IPv6_re =~ s/\(/(?:/g;
-$IPv6_re = qr/$IPv6_re/;
-
-1;
-__END__
-
-=head1 NAME
-
-Regexp::IPv6 - Regular expression for IPv6 addresses
-
-=head1 SYNOPSIS
-
-  use Regexp::IPv6 qw($IPv6_re);
-
-  $address =~ /^$IPv6_re$/ and print "IPv6 address\n";
-
-=head1 DESCRIPTION
-
-This module exports the $IPv6_re regular expression that matches any
-valid IPv6 address as described in "RFC 2373 - 2.2 Text Representation
-of Addresses" but C<::>. Any string not compliant with such RFC will
-be rejected.
-
-To match full strings use C</^$IPv6_re$/>.
-
-=head1 COPYRIGHT AND LICENSE
-
-Copyright (C) 2009, 2010 by Salvador FandiE<ntilde>o
-(sfandino@yahoo.com)
-
-This library is free software; you can redistribute it and/or modify
-it under the same terms as Perl itself, either Perl version 5.10.0 or,
-at your option, any later version of Perl 5 you may have available.
-
-Additionally, you are allowed to use the regexp generated by the
-module in any way you want, without any restriction. For instance, you
-are allowed to copy it verbating in your program.
-
-=cut
-
diff --git a/modules/fallback/Set/Crontab.pm b/modules/fallback/Set/Crontab.pm
deleted file mode 100644 (file)
index 033d20d..0000000
+++ /dev/null
@@ -1,160 +0,0 @@
-# Copyright 2001 Abhijit Menon-Sen <ams@toroid.org>
-
-package Set::Crontab;
-
-use strict;
-use Carp;
-use vars qw( $VERSION );
-
-$VERSION = '1.03';
-
-sub _expand
-{
-    my (@list, @and, @not);
-    my ($self, $spec, $range) = @_;
-
-    # 1,2-4,*/3,!13,>9,<15
-    foreach (split /,/, $spec) {
-        my @pick;
-        my $step = $1 if s#/(\d+)$##;
-
-        # 0+"01" == 1
-        if    (/^(\d+)$/)       { push @pick, 0+$1;          }
-        elsif (/^\*$/)          { push @pick, @$range;       }
-        elsif (/^(\d+)-(\d+)$/) { push @pick, 0+$1..0+$2;    } 
-        elsif (/^!(\d+)$/)      { push @not,  "\$_ != 0+$1"; }
-        elsif (/^([<>])(\d+)$/) { push @and,  "\$_ $1 0+$2"; }
-
-        if ($step) {
-            my $i;
-            @pick = grep { defined $_ if $i++ % $step == 0 } @pick;
-        }
-
-        push @list, @pick;
-    }
-
-    if (@and) {
-        my $and = join q{ && }, @and;
-        push @list, grep { defined $_ if eval $and } @$range;
-    }
-
-    if (@not) {
-        my $not = join q{ && }, @not;
-        @list = grep { defined $_ if eval $not } (@list ? @list : @$range);
-    }
-
-    @list = sort { $a <=> $b } @list;
-    return \@list;
-}
-
-sub _initialise
-{
-    my ($self, $spec, $range) = @_;
-    return undef unless ref($self);
-
-    croak "Usage: ".__PACKAGE__."->new(\$spec, [\@range])"
-        unless defined $spec && ref($range) eq "ARRAY";
-
-    $self->{LIST} = $self->_expand($spec, $range);
-    $self->{HASH} = {map {$_ => 1} @{$self->{LIST}}};
-
-    return $self;
-};
-
-sub new
-{
-    my $class = shift;
-    my $self  = bless {}, ref($class) || $class;
-    return $self->_initialise(@_);
-}
-
-sub contains
-{
-    my ($self, $num) = @_;
-
-    croak "Usage: \$set->contains(\$num)" unless ref($self) && defined $num;
-    return exists $self->{HASH}{$num};
-}
-
-sub list
-{
-    my $self = shift;
-
-    croak "Usage: \$set->list()" unless ref($self);
-    return @{$self->{LIST}};
-}
-
-1;
-__END__
-
-=head1 NAME
-
-Set::Crontab - Expand crontab(5)-style integer lists
-
-=head1 SYNOPSIS
-
-$s = Set::Crontab->new("1-9/3,>15,>30,!23", [0..30]);
-
-if ($s->contains(3)) { ... }
-
-=head1 DESCRIPTION
-
-Set::Crontab parses crontab-style lists of integers and defines some
-utility functions to make it easier to deal with them.
-
-=head2 Syntax
-
-Numbers, ranges, *, and step values all work exactly as described in
-L<crontab(5)>. A few extensions to the standard syntax are described
-below.
-
-=over 4
-
-=item < and >
-
-<N selects the elements smaller than N from the entire range, and adds
-them to the set. >N does likewise for elements larger than N.
-
-=item !
-
-!N excludes N from the set. It applies to the other specified 
-range; otherwise it applies to the specified ranges (i.e. "!3" with a
-range of "1-10" corresponds to "1-2,4-10", but ">3,!7" in the same range
-means "4-6,8-10").
-
-=back
-
-=head2 Functions
-
-=over 4
-
-=item new($spec, [@range])
-
-Creates a new Set::Crontab object and returns a reference to it.
-
-=item contains($num)
-
-Returns true if C<$num> exists in the set.
-
-=item list()
-
-Returns the expanded list corresponding to the set. Elements are in
-ascending order.
-
-=back
-
-The functions described above croak if they are called with incorrect
-arguments.
-
-=head1 SEE ALSO
-
-L<crontab(5)>
-
-=head1 AUTHOR
-
-Abhijit Menon-Sen <ams@toroid.org>
-
-Copyright 2001 Abhijit Menon-Sen <ams@toroid.org>
-
-This module is free software; you can redistribute it and/or modify it
-under the same terms as Perl itself.
diff --git a/modules/fallback/Set/Infinite.pm b/modules/fallback/Set/Infinite.pm
deleted file mode 100644 (file)
index 72bda52..0000000
+++ /dev/null
@@ -1,1921 +0,0 @@
-package Set::Infinite;
-
-# Copyright (c) 2001, 2002, 2003, 2004 Flavio Soibelmann Glock. 
-# All rights reserved.
-# This program is free software; you can redistribute it and/or
-# modify it under the same terms as Perl itself.
-
-use 5.005_03;
-
-# These methods are inherited from Set::Infinite::Basic "as-is":
-#   type list fixtype numeric min max integer real new span copy
-#   start_set end_set universal_set empty_set minus difference
-#   symmetric_difference is_empty
-
-use strict;
-use base qw(Set::Infinite::Basic Exporter);
-use Carp;
-use Set::Infinite::Arithmetic;
-
-use overload
-    '<=>' => \&spaceship,
-    '""'  => \&as_string;
-
-use vars qw(@EXPORT_OK $VERSION 
-    $TRACE $DEBUG_BT $PRETTY_PRINT $inf $minus_inf $neg_inf 
-    %_first %_last %_backtrack
-    $too_complex $backtrack_depth 
-    $max_backtrack_depth $max_intersection_depth
-    $trace_level %level_title );
-
-@EXPORT_OK = qw(inf $inf trace_open trace_close);
-
-$inf     = 100**100**100;
-$neg_inf = $minus_inf  = -$inf;
-
-
-# obsolete methods - included for backward compatibility
-sub inf ()            { $inf }
-sub minus_inf ()      { $minus_inf }
-sub no_cleanup { $_[0] }
-*type       = \&Set::Infinite::Basic::type;
-sub compact { @_ }
-
-
-BEGIN {
-    $VERSION = "0.65";
-    $TRACE = 0;         # enable basic trace method execution
-    $DEBUG_BT = 0;      # enable backtrack tracer
-    $PRETTY_PRINT = 0;  # 0 = print 'Too Complex'; 1 = describe functions
-    $trace_level = 0;   # indentation level when debugging
-
-    $too_complex =    "Too complex";
-    $backtrack_depth = 0;
-    $max_backtrack_depth = 10;    # _backtrack()
-    $max_intersection_depth = 5;  # first()
-}
-
-sub trace { # title=>'aaa'
-    return $_[0] unless $TRACE;
-    my ($self, %parm) = @_;
-    my @caller = caller(1);
-    # print "self $self ". ref($self). "\n";
-    print "" . ( ' | ' x $trace_level ) .
-            "$parm{title} ". $self->copy .
-            ( exists $parm{arg} ? " -- " . $parm{arg}->copy : "" ).
-            " $caller[1]:$caller[2] ]\n" if $TRACE == 1;
-    return $self;
-}
-
-sub trace_open { 
-    return $_[0] unless $TRACE;
-    my ($self, %parm) = @_;
-    my @caller = caller(1);
-    print "" . ( ' | ' x $trace_level ) .
-            "\\ $parm{title} ". $self->copy .
-            ( exists $parm{arg} ? " -- ". $parm{arg}->copy : "" ).
-            " $caller[1]:$caller[2] ]\n";
-    $trace_level++; 
-    $level_title{$trace_level} = $parm{title};
-    return $self;
-}
-
-sub trace_close { 
-    return $_[0] unless $TRACE;
-    my ($self, %parm) = @_;  
-    my @caller = caller(0);
-    print "" . ( ' | ' x ($trace_level-1) ) .
-            "\/ $level_title{$trace_level} ".
-            ( exists $parm{arg} ? 
-               (
-                  defined $parm{arg} ? 
-                      "ret ". ( UNIVERSAL::isa($parm{arg}, __PACKAGE__ ) ? 
-                           $parm{arg}->copy : 
-                           "<$parm{arg}>" ) :
-                      "undef"
-               ) : 
-               ""     # no arg 
-            ).
-            " $caller[1]:$caller[2] ]\n";
-    $trace_level--;
-    return $self;
-}
-
-
-# creates a 'function' object that can be solved by _backtrack()
-sub _function {
-    my ($self, $method) = (shift, shift);
-    my $b = $self->empty_set();
-    $b->{too_complex} = 1;
-    $b->{parent} = $self;   
-    $b->{method} = $method;
-    $b->{param}  = [ @_ ];
-    return $b;
-}
-
-
-# same as _function, but with 2 arguments
-sub _function2 {
-    my ($self, $method, $arg) = (shift, shift, shift);
-    unless ( $self->{too_complex} || $arg->{too_complex} ) {
-        return $self->$method($arg, @_);
-    }
-    my $b = $self->empty_set();
-    $b->{too_complex} = 1;
-    $b->{parent} = [ $self, $arg ];
-    $b->{method} = $method;
-    $b->{param}  = [ @_ ];
-    return $b;
-}
-
-
-sub quantize {
-    my $self = shift;
-    $self->trace_open(title=>"quantize") if $TRACE; 
-    my @min = $self->min_a;
-    my @max = $self->max_a;
-    if (($self->{too_complex}) or 
-        (defined $min[0] && $min[0] == $neg_inf) or 
-        (defined $max[0] && $max[0] == $inf)) {
-
-        return $self->_function( 'quantize', @_ );
-    }
-
-    my @a;
-    my %rule = @_;
-    my $b = $self->empty_set();    
-    my $parent = $self;
-
-    $rule{unit} =   'one' unless $rule{unit};
-    $rule{quant} =  1     unless $rule{quant};
-    $rule{parent} = $parent; 
-    $rule{strict} = $parent unless exists $rule{strict};
-    $rule{type} =   $parent->{type};
-
-    my ($min, $open_begin) = $parent->min_a;
-
-    unless (defined $min) {
-        $self->trace_close( arg => $b ) if $TRACE;
-        return $b;    
-    }
-
-    $rule{fixtype} = 1 unless exists $rule{fixtype};
-    $Set::Infinite::Arithmetic::Init_quantizer{$rule{unit}}->(\%rule);
-
-    $rule{sub_unit} = $Set::Infinite::Arithmetic::Offset_to_value{$rule{unit}};
-    carp "Quantize unit '".$rule{unit}."' not implemented" unless ref( $rule{sub_unit} ) eq 'CODE';
-
-    my ($max, $open_end) = $parent->max_a;
-    $rule{offset} = $Set::Infinite::Arithmetic::Value_to_offset{$rule{unit}}->(\%rule, $min);
-    my $last_offset = $Set::Infinite::Arithmetic::Value_to_offset{$rule{unit}}->(\%rule, $max);
-    $rule{size} = $last_offset - $rule{offset} + 1; 
-    my ($index, $tmp, $this, $next);
-    for $index (0 .. $rule{size} ) {
-        # ($this, $next) = $rule{sub_unit} (\%rule, $index);
-        ($this, $next) = $rule{sub_unit}->(\%rule, $index);
-        unless ( $rule{fixtype} ) {
-                $tmp = { a => $this , b => $next ,
-                        open_begin => 0, open_end => 1 };
-        }
-        else {
-                $tmp = Set::Infinite::Basic::_simple_new($this,$next, $rule{type} );
-                $tmp->{open_end} = 1;
-        }
-        next if ( $rule{strict} and not $rule{strict}->intersects($tmp));
-        push @a, $tmp;
-    }
-
-    $b->{list} = \@a;        # change data
-    $self->trace_close( arg => $b ) if $TRACE;
-    return $b;
-}
-
-
-sub _first_n {
-    my $self = shift;
-    my $n = shift;
-    my $tail = $self->copy;
-    my @result;
-    my $first;
-    for ( 1 .. $n )
-    {
-        ( $first, $tail ) = $tail->first if $tail;
-        push @result, $first;
-    }
-    return $tail, @result;
-}
-
-sub _last_n {
-    my $self = shift;
-    my $n = shift;
-    my $tail = $self->copy;
-    my @result;
-    my $last;
-    for ( 1 .. $n )
-    {
-        ( $last, $tail ) = $tail->last if $tail;
-        unshift @result, $last;
-    }
-    return $tail, @result;
-}
-
-
-sub select {
-    my $self = shift;
-    $self->trace_open(title=>"select") if $TRACE;
-
-    my %param = @_;
-    die "select() - parameter 'freq' is deprecated" if exists $param{freq};
-
-    my $res;
-    my $count;
-    my @by;
-    @by = @{ $param{by} } if exists $param{by}; 
-    $count = delete $param{count} || $inf;
-    # warn "select: count=$count by=[@by]";
-
-    if ($count <= 0) {
-        $self->trace_close( arg => $res ) if $TRACE;
-        return $self->empty_set();
-    }
-
-    my @set;
-    my $tail;
-    my $first;
-    my $last;
-    if ( @by ) 
-    {
-        my @res;
-        if ( ! $self->is_too_complex ) 
-        {
-            $res = $self->new;
-            @res = @{ $self->{list} }[ @by ] ;
-        }
-        else
-        {
-            my ( @pos_by, @neg_by );
-            for ( @by ) {
-                ( $_ < 0 ) ? push @neg_by, $_ :
-                             push @pos_by, $_;
-            }
-            my @first;
-            if ( @pos_by ) {
-                @pos_by = sort { $a <=> $b } @pos_by;
-                ( $tail, @set ) = $self->_first_n( 1 + $pos_by[-1] );
-                @first = @set[ @pos_by ];
-            }
-            my @last;
-            if ( @neg_by ) {
-                @neg_by = sort { $a <=> $b } @neg_by;
-                ( $tail, @set ) = $self->_last_n( - $neg_by[0] );
-                @last = @set[ @neg_by ];
-            }
-            @res = map { $_->{list}[0] } ( @first , @last );
-        }
-
-        $res = $self->new;
-        @res = sort { $a->{a} <=> $b->{a} } grep { defined } @res;
-        my $last;
-        my @a;
-        for ( @res ) {
-            push @a, $_ if ! $last || $last->{a} != $_->{a};
-            $last = $_;
-        }
-        $res->{list} = \@a;
-    }
-    else
-    {
-        $res = $self;
-    }
-
-    return $res if $count == $inf;
-    my $count_set = $self->empty_set();
-    if ( ! $self->is_too_complex )
-    {
-        my @a;
-        @a = grep { defined } @{ $res->{list} }[ 0 .. $count - 1 ] ;
-        $count_set->{list} = \@a;
-    }
-    else
-    {
-        my $last;
-        while ( $res ) {
-            ( $first, $res ) = $res->first;
-            last unless $first;
-            last if $last && $last->{a} == $first->{list}[0]{a};
-            $last = $first->{list}[0];
-            push @{$count_set->{list}}, $first->{list}[0];
-            $count--;
-            last if $count <= 0;
-        }
-    }
-    return $count_set;
-}
-
-BEGIN {
-
-  # %_first and %_last hashes are used to backtrack the value
-  # of first() and last() of an infinite set
-
-  %_first = (
-    'complement' =>
-        sub {
-            my $self = $_[0];
-            my @parent_min = $self->{parent}->first;
-            unless ( defined $parent_min[0] ) {
-                return (undef, 0);
-            }
-            my $parent_complement;
-            my $first;
-            my @next;
-            my $parent;
-            if ( $parent_min[0]->min == $neg_inf ) {
-                my @parent_second = $parent_min[1]->first;
-                #    (-inf..min)        (second..?)
-                #            (min..second)   = complement
-                $first = $self->new( $parent_min[0]->complement );
-                $first->{list}[0]{b} = $parent_second[0]->{list}[0]{a};
-                $first->{list}[0]{open_end} = ! $parent_second[0]->{list}[0]{open_begin};
-                @{ $first->{list} } = () if 
-                    ( $first->{list}[0]{a} == $first->{list}[0]{b}) && 
-                        ( $first->{list}[0]{open_begin} ||
-                          $first->{list}[0]{open_end} );
-                @next = $parent_second[0]->max_a;
-                $parent = $parent_second[1];
-            }
-            else {
-                #            (min..?)
-                #    (-inf..min)        = complement
-                $parent_complement = $parent_min[0]->complement;
-                $first = $self->new( $parent_complement->{list}[0] );
-                @next = $parent_min[0]->max_a;
-                $parent = $parent_min[1];
-            }
-            my @no_tail = $self->new($neg_inf,$next[0]);
-            $no_tail[0]->{list}[0]{open_end} = $next[1];
-            my $tail = $parent->union($no_tail[0])->complement;  
-            return ($first, $tail);
-        },  # end: first-complement
-    'intersection' =>
-        sub {
-            my $self = $_[0];
-            my @parent = @{ $self->{parent} };
-            # warn "$method parents @parent";
-            my $retry_count = 0;
-            my (@first, @min, $which, $first1, $intersection);
-            SEARCH: while ($retry_count++ < $max_intersection_depth) {
-                return undef unless defined $parent[0];
-                return undef unless defined $parent[1];
-                @{$first[0]} = $parent[0]->first;
-                @{$first[1]} = $parent[1]->first;
-                unless ( defined $first[0][0] ) {
-                    # warn "don't know first of $method";
-                    $self->trace_close( arg => 'undef' ) if $TRACE;
-                    return undef;
-                }
-                unless ( defined $first[1][0] ) {
-                    # warn "don't know first of $method";
-                    $self->trace_close( arg => 'undef' ) if $TRACE;
-                    return undef;
-                }
-                @{$min[0]} = $first[0][0]->min_a;
-                @{$min[1]} = $first[1][0]->min_a;
-                unless ( defined $min[0][0] && defined $min[1][0] ) {
-                    return undef;
-                } 
-                # $which is the index to the bigger "first".
-                $which = ($min[0][0] < $min[1][0]) ? 1 : 0;  
-                for my $which1 ( $which, 1 - $which ) {
-                  my $tmp_parent = $parent[$which1];
-                  ($first1, $parent[$which1]) = @{ $first[$which1] };
-                  if ( $first1->is_empty ) {
-                    # warn "first1 empty! count $retry_count";
-                    # trace_close;
-                    # return $first1, undef;
-                    $intersection = $first1;
-                    $which = $which1;
-                    last SEARCH;
-                  }
-                  $intersection = $first1->intersection( $parent[1-$which1] );
-                  # warn "intersection with $first1 is $intersection";
-                  unless ( $intersection->is_null ) { 
-                    # $self->trace( title=>"got an intersection" );
-                    if ( $intersection->is_too_complex ) {
-                        $parent[$which1] = $tmp_parent;
-                    }
-                    else {
-                        $which = $which1;
-                        last SEARCH;
-                    }
-                  };
-                }
-            }
-            if ( $#{ $intersection->{list} } > 0 ) {
-                my $tail;
-                ($intersection, $tail) = $intersection->first;
-                $parent[$which] = $parent[$which]->union( $tail );
-            }
-            my $tmp;
-            if ( defined $parent[$which] and defined $parent[1-$which] ) {
-                $tmp = $parent[$which]->intersection ( $parent[1-$which] );
-            }
-            return ($intersection, $tmp);
-        }, # end: first-intersection
-    'union' =>
-        sub {
-            my $self = $_[0];
-            my (@first, @min);
-            my @parent = @{ $self->{parent} };
-            @{$first[0]} = $parent[0]->first;
-            @{$first[1]} = $parent[1]->first;
-            unless ( defined $first[0][0] ) {
-                # looks like one set was empty
-                return @{$first[1]};
-            }
-            @{$min[0]} = $first[0][0]->min_a;
-            @{$min[1]} = $first[1][0]->min_a;
-
-            # check min1/min2 for undef
-            unless ( defined $min[0][0] ) {
-                $self->trace_close( arg => "@{$first[1]}" ) if $TRACE;
-                return @{$first[1]}
-            }
-            unless ( defined $min[1][0] ) {
-                $self->trace_close( arg => "@{$first[0]}" ) if $TRACE;
-                return @{$first[0]}
-            }
-
-            my $which = ($min[0][0] < $min[1][0]) ? 0 : 1;
-            my $first = $first[$which][0];
-
-            # find out the tail
-            my $parent1 = $first[$which][1];
-            # warn $self->{parent}[$which]." - $first = $parent1";
-            my $parent2 = ($min[0][0] == $min[1][0]) ? 
-                $self->{parent}[1-$which]->complement($first) : 
-                $self->{parent}[1-$which];
-            my $tail;
-            if (( ! defined $parent1 ) || $parent1->is_null) {
-                # warn "union parent1 tail is null"; 
-                $tail = $parent2;
-            }
-            else {
-                my $method = $self->{method};
-                $tail = $parent1->$method( $parent2 );
-            }
-
-            if ( $first->intersects( $tail ) ) {
-                my $first2;
-                ( $first2, $tail ) = $tail->first;
-                $first = $first->union( $first2 );
-            }
-
-            $self->trace_close( arg => "$first $tail" ) if $TRACE;
-            return ($first, $tail);
-        }, # end: first-union
-    'iterate' =>
-        sub {
-            my $self = $_[0];
-            my $parent = $self->{parent};
-            my ($first, $tail) = $parent->first;
-            $first = $first->iterate( @{$self->{param}} ) if ref($first);
-            $tail  = $tail->_function( 'iterate', @{$self->{param}} ) if ref($tail);
-            my $more;
-            ($first, $more) = $first->first if ref($first);
-            $tail = $tail->_function2( 'union', $more ) if defined $more;
-            return ($first, $tail);
-        },
-    'until' =>
-        sub {
-            my $self = $_[0];
-            my ($a1, $b1) = @{ $self->{parent} };
-            $a1->trace( title=>"computing first()" );
-            my @first1 = $a1->first;
-            my @first2 = $b1->first;
-            my ($first, $tail);
-            if ( $first2[0] <= $first1[0] ) {
-                # added ->first because it returns 2 spans if $a1 == $a2
-                $first = $a1->empty_set()->until( $first2[0] )->first;
-                $tail = $a1->_function2( "until", $first2[1] );
-            }
-            else {
-                $first = $a1->new( $first1[0] )->until( $first2[0] );
-                if ( defined $first1[1] ) {
-                    $tail = $first1[1]->_function2( "until", $first2[1] );
-                }
-                else {
-                    $tail = undef;
-                }
-            }
-            return ($first, $tail);
-        },
-    'offset' =>
-        sub {
-            my $self = $_[0];
-            my ($first, $tail) = $self->{parent}->first;
-            $first = $first->offset( @{$self->{param}} );
-            $tail  = $tail->_function( 'offset', @{$self->{param}} );
-            my $more;
-            ($first, $more) = $first->first;
-            $tail = $tail->_function2( 'union', $more ) if defined $more;
-            return ($first, $tail);
-        },
-    'quantize' =>
-        sub {
-            my $self = $_[0];
-            my @min = $self->{parent}->min_a;
-            if ( $min[0] == $neg_inf || $min[0] == $inf ) {
-                return ( $self->new( $min[0] ) , $self->copy );
-            }
-            my $first = $self->new( $min[0] )->quantize( @{$self->{param}} );
-            return ( $first,
-                     $self->{parent}->
-                        _function2( 'intersection', $first->complement )->
-                        _function( 'quantize', @{$self->{param}} ) );
-        },
-    'tolerance' =>
-        sub {
-            my $self = $_[0];
-            my ($first, $tail) = $self->{parent}->first;
-            $first = $first->tolerance( @{$self->{param}} );
-            $tail  = $tail->tolerance( @{$self->{param}} );
-            return ($first, $tail);
-        },
-  );  # %_first
-
-  %_last = (
-    'complement' =>
-        sub {
-            my $self = $_[0];
-            my @parent_max = $self->{parent}->last;
-            unless ( defined $parent_max[0] ) {
-                return (undef, 0);
-            }
-            my $parent_complement;
-            my $last;
-            my @next;
-            my $parent;
-            if ( $parent_max[0]->max == $inf ) {
-                #    (inf..min)        (second..?) = parent
-                #            (min..second)         = complement
-                my @parent_second = $parent_max[1]->last;
-                $last = $self->new( $parent_max[0]->complement );
-                $last->{list}[0]{a} = $parent_second[0]->{list}[0]{b};
-                $last->{list}[0]{open_begin} = ! $parent_second[0]->{list}[0]{open_end};
-                @{ $last->{list} } = () if
-                    ( $last->{list}[0]{a} == $last->{list}[0]{b}) &&
-                        ( $last->{list}[0]{open_end} ||
-                          $last->{list}[0]{open_begin} );
-                @next = $parent_second[0]->min_a;
-                $parent = $parent_second[1];
-            }
-            else {
-                #            (min..?)
-                #    (-inf..min)        = complement
-                $parent_complement = $parent_max[0]->complement;
-                $last = $self->new( $parent_complement->{list}[-1] );
-                @next = $parent_max[0]->min_a;
-                $parent = $parent_max[1];
-            }
-            my @no_tail = $self->new($next[0], $inf);
-            $no_tail[0]->{list}[-1]{open_begin} = $next[1];
-            my $tail = $parent->union($no_tail[-1])->complement;
-            return ($last, $tail);
-        },
-    'intersection' =>
-        sub {
-            my $self = $_[0];
-            my @parent = @{ $self->{parent} };
-            # TODO: check max1/max2 for undef
-
-            my $retry_count = 0;
-            my (@last, @max, $which, $last1, $intersection);
-
-            SEARCH: while ($retry_count++ < $max_intersection_depth) {
-                return undef unless defined $parent[0];
-                return undef unless defined $parent[1];
-
-                @{$last[0]} = $parent[0]->last;
-                @{$last[1]} = $parent[1]->last;
-                unless ( defined $last[0][0] ) {
-                    $self->trace_close( arg => 'undef' ) if $TRACE;
-                    return undef;
-                }
-                unless ( defined $last[1][0] ) {
-                    $self->trace_close( arg => 'undef' ) if $TRACE;
-                    return undef;
-                }
-                @{$max[0]} = $last[0][0]->max_a;
-                @{$max[1]} = $last[1][0]->max_a;
-                unless ( defined $max[0][0] && defined $max[1][0] ) {
-                    $self->trace( title=>"can't find max()" ) if $TRACE;
-                    $self->trace_close( arg => 'undef' ) if $TRACE;
-                    return undef;
-                }
-
-                # $which is the index to the smaller "last".
-                $which = ($max[0][0] > $max[1][0]) ? 1 : 0;
-
-                for my $which1 ( $which, 1 - $which ) {
-                  my $tmp_parent = $parent[$which1];
-                  ($last1, $parent[$which1]) = @{ $last[$which1] };
-                  if ( $last1->is_null ) {
-                    $which = $which1;
-                    $intersection = $last1;
-                    last SEARCH;
-                  }
-                  $intersection = $last1->intersection( $parent[1-$which1] );
-
-                  unless ( $intersection->is_null ) {
-                    # $self->trace( title=>"got an intersection" );
-                    if ( $intersection->is_too_complex ) {
-                        $self->trace( title=>"got a too_complex intersection" ) if $TRACE; 
-                        # warn "too complex intersection";
-                        $parent[$which1] = $tmp_parent;
-                    }
-                    else {
-                        $self->trace( title=>"got an intersection" ) if $TRACE;
-                        $which = $which1;
-                        last SEARCH;
-                    }
-                  };
-                }
-            }
-            $self->trace( title=>"exit loop" ) if $TRACE;
-            if ( $#{ $intersection->{list} } > 0 ) {
-                my $tail;
-                ($intersection, $tail) = $intersection->last;
-                $parent[$which] = $parent[$which]->union( $tail );
-            }
-            my $tmp;
-            if ( defined $parent[$which] and defined $parent[1-$which] ) {
-                $tmp = $parent[$which]->intersection ( $parent[1-$which] );
-            }
-            return ($intersection, $tmp);
-        },
-    'union' =>
-        sub {
-            my $self = $_[0];
-            my (@last, @max);
-            my @parent = @{ $self->{parent} };
-            @{$last[0]} = $parent[0]->last;
-            @{$last[1]} = $parent[1]->last;
-            @{$max[0]} = $last[0][0]->max_a;
-            @{$max[1]} = $last[1][0]->max_a;
-            unless ( defined $max[0][0] ) {
-                return @{$last[1]}
-            }
-            unless ( defined $max[1][0] ) {
-                return @{$last[0]}
-            }
-
-            my $which = ($max[0][0] > $max[1][0]) ? 0 : 1;
-            my $last = $last[$which][0];
-            # find out the tail
-            my $parent1 = $last[$which][1];
-            # warn $self->{parent}[$which]." - $last = $parent1";
-            my $parent2 = ($max[0][0] == $max[1][0]) ?
-                $self->{parent}[1-$which]->complement($last) :
-                $self->{parent}[1-$which];
-            my $tail;
-            if (( ! defined $parent1 ) || $parent1->is_null) {
-                $tail = $parent2;
-            }
-            else {
-                my $method = $self->{method};
-                $tail = $parent1->$method( $parent2 );
-            }
-
-            if ( $last->intersects( $tail ) ) {
-                my $last2;
-                ( $last2, $tail ) = $tail->last;
-                $last = $last->union( $last2 );
-            }
-
-            return ($last, $tail);
-        },
-    'until' =>
-        sub {
-            my $self = $_[0];
-            my ($a1, $b1) = @{ $self->{parent} };
-            $a1->trace( title=>"computing last()" );
-            my @last1 = $a1->last;
-            my @last2 = $b1->last;
-            my ($last, $tail);
-            if ( $last2[0] <= $last1[0] ) {
-                # added ->last because it returns 2 spans if $a1 == $a2
-                $last = $last2[0]->until( $a1 )->last;
-                $tail = $a1->_function2( "until", $last2[1] );
-            }
-            else {
-                $last = $a1->new( $last1[0] )->until( $last2[0] );
-                if ( defined $last1[1] ) {
-                    $tail = $last1[1]->_function2( "until", $last2[1] );
-                }
-                else {
-                    $tail = undef;
-                }
-            }
-            return ($last, $tail);
-        },
-    'iterate' =>
-        sub {
-            my $self = $_[0];
-            my $parent = $self->{parent};
-            my ($last, $tail) = $parent->last;
-            $last = $last->iterate( @{$self->{param}} ) if ref($last);
-            $tail = $tail->_function( 'iterate', @{$self->{param}} ) if ref($tail);
-            my $more;
-            ($last, $more) = $last->last if ref($last);
-            $tail = $tail->_function2( 'union', $more ) if defined $more;
-            return ($last, $tail);
-        },
-    'offset' =>
-        sub {
-            my $self = $_[0];
-            my ($last, $tail) = $self->{parent}->last;
-            $last = $last->offset( @{$self->{param}} );
-            $tail  = $tail->_function( 'offset', @{$self->{param}} );
-            my $more;
-            ($last, $more) = $last->last;
-            $tail = $tail->_function2( 'union', $more ) if defined $more;
-            return ($last, $tail);
-        },
-    'quantize' =>
-        sub {
-            my $self = $_[0];
-            my @max = $self->{parent}->max_a;
-            if (( $max[0] == $neg_inf ) || ( $max[0] == $inf )) {
-                return ( $self->new( $max[0] ) , $self->copy );
-            }
-            my $last = $self->new( $max[0] )->quantize( @{$self->{param}} );
-            if ($max[1]) {  # open_end
-                    if ( $last->min <= $max[0] ) {
-                        $last = $self->new( $last->min - 1e-9 )->quantize( @{$self->{param}} );
-                    }
-            }
-            return ( $last, $self->{parent}->
-                        _function2( 'intersection', $last->complement )->
-                        _function( 'quantize', @{$self->{param}} ) );
-        },
-    'tolerance' =>
-        sub {
-            my $self = $_[0];
-            my ($last, $tail) = $self->{parent}->last;
-            $last = $last->tolerance( @{$self->{param}} );
-            $tail  = $tail->tolerance( @{$self->{param}} );
-            return ($last, $tail);
-        },
-  );  # %_last
-} # BEGIN
-
-sub first {
-    my $self = $_[0];
-    unless ( exists $self->{first} ) {
-        $self->trace_open(title=>"first") if $TRACE;
-        if ( $self->{too_complex} ) {
-            my $method = $self->{method};
-            # warn "method $method ". ( exists $_first{$method} ? "exists" : "does not exist" );
-            if ( exists $_first{$method} ) {
-                @{$self->{first}} = $_first{$method}->($self);
-            }
-            else {
-                my $redo = $self->{parent}->$method ( @{ $self->{param} } );
-                @{$self->{first}} = $redo->first;
-            }
-        }
-        else {
-            return $self->SUPER::first;
-        }
-    }
-    return wantarray ? @{$self->{first}} : $self->{first}[0];
-}
-
-
-sub last {
-    my $self = $_[0];
-    unless ( exists $self->{last} ) {
-        $self->trace(title=>"last") if $TRACE;
-        if ( $self->{too_complex} ) {
-            my $method = $self->{method};
-            if ( exists $_last{$method} ) {
-                @{$self->{last}} = $_last{$method}->($self);
-            }
-            else {
-                my $redo = $self->{parent}->$method ( @{ $self->{param} } );
-                @{$self->{last}} = $redo->last;
-            }
-        }
-        else {
-            return $self->SUPER::last;
-        }
-    }
-    return wantarray ? @{$self->{last}} : $self->{last}[0];
-}
-
-
-# offset: offsets subsets
-sub offset {
-    my $self = shift;
-    if ($self->{too_complex}) {
-        return $self->_function( 'offset', @_ );
-    }
-    $self->trace_open(title=>"offset") if $TRACE;
-
-    my @a;
-    my %param = @_;
-    my $b1 = $self->empty_set();    
-    my ($interval, $ia, $i);
-    $param{mode} = 'offset' unless $param{mode};
-
-    unless (ref($param{value}) eq 'ARRAY') {
-        $param{value} = [0 + $param{value}, 0 + $param{value}];
-    }
-    $param{unit} =    'one'  unless $param{unit};
-    my $parts    =    ($#{$param{value}}) / 2;
-    my $sub_unit =    $Set::Infinite::Arithmetic::subs_offset2{$param{unit}};
-    my $sub_mode =    $Set::Infinite::Arithmetic::_MODE{$param{mode}};
-
-    carp "unknown unit $param{unit} for offset()" unless defined $sub_unit;
-    carp "unknown mode $param{mode} for offset()" unless defined $sub_mode;
-
-    my ($j);
-    my ($cmp, $this, $next, $ib, $part, $open_begin, $open_end, $tmp);
-
-    my @value;
-    foreach $j (0 .. $parts) {
-        push @value, [ $param{value}[$j+$j], $param{value}[$j+$j + 1] ];
-    }
-
-    foreach $interval ( @{ $self->{list} } ) {
-        $ia =         $interval->{a};
-        $ib =         $interval->{b};
-        $open_begin = $interval->{open_begin};
-        $open_end =   $interval->{open_end};
-        foreach $j (0 .. $parts) {
-            # print " [ofs($ia,$ib)] ";
-            ($this, $next) = $sub_mode->( $sub_unit, $ia, $ib, @{$value[$j]} );
-            next if ($this > $next);    # skip if a > b
-            if ($this == $next) {
-                # TODO: fix this
-                $open_end = $open_begin;
-            }
-            push @a, { a => $this , b => $next ,
-                       open_begin => $open_begin , open_end => $open_end };
-        }  # parts
-    }  # self
-    @a = sort { $a->{a} <=> $b->{a} } @a;
-    $b1->{list} = \@a;        # change data
-    $self->trace_close( arg => $b1 ) if $TRACE;
-    $b1 = $b1->fixtype if $self->{fixtype};
-    return $b1;
-}
-
-
-sub is_null {
-    $_[0]->{too_complex} ? 0 : $_[0]->SUPER::is_null;
-}
-
-
-sub is_too_complex {
-    $_[0]->{too_complex} ? 1 : 0;
-}
-
-
-# shows how a 'compacted' set looks like after quantize
-sub _quantize_span {
-    my $self = shift;
-    my %param = @_;
-    $self->trace_open(title=>"_quantize_span") if $TRACE;
-    my $res;
-    if ($self->{too_complex}) {
-        $res = $self->{parent};
-        if ($self->{method} ne 'quantize') {
-            $self->trace( title => "parent is a ". $self->{method} );
-            if ( $self->{method} eq 'union' ) {
-                my $arg0 = $self->{parent}[0]->_quantize_span(%param);
-                my $arg1 = $self->{parent}[1]->_quantize_span(%param);
-                $res = $arg0->union( $arg1 );
-            }
-            elsif ( $self->{method} eq 'intersection' ) {
-                my $arg0 = $self->{parent}[0]->_quantize_span(%param);
-                my $arg1 = $self->{parent}[1]->_quantize_span(%param);
-                $res = $arg0->intersection( $arg1 );
-            }
-
-            # TODO: other methods
-            else {
-                $res = $self; # ->_function( "_quantize_span", %param );
-            }
-            $self->trace_close( arg => $res ) if $TRACE;
-            return $res;
-        }
-
-        # $res = $self->{parent};
-        if ($res->{too_complex}) {
-            $res->trace( title => "parent is complex" );
-            $res = $res->_quantize_span( %param );
-            $res = $res->quantize( @{$self->{param}} )->_quantize_span( %param );
-        }
-        else {
-            $res = $res->iterate (
-                sub {
-                    $_[0]->quantize( @{$self->{param}} )->span;
-                }
-            );
-        }
-    }
-    else {
-        $res = $self->iterate (   sub { $_[0] }   );
-    }
-    $self->trace_close( arg => $res ) if $TRACE;
-    return $res;
-}
-
-
-
-BEGIN {
-
-    %_backtrack = (
-
-        until => sub {
-            my ($self, $arg) = @_;
-            my $before = $self->{parent}[0]->intersection( $neg_inf, $arg->min )->max;
-            $before = $arg->min unless $before;
-            my $after = $self->{parent}[1]->intersection( $arg->max, $inf )->min;
-            $after = $arg->max unless $after;
-            return $arg->new( $before, $after );
-        },
-
-        iterate => sub {
-            my ($self, $arg) = @_;
-
-            if ( defined $self->{backtrack_callback} )
-            {
-                return $arg = $self->new( $self->{backtrack_callback}->( $arg ) );
-            }
-
-            my $before = $self->{parent}->intersection( $neg_inf, $arg->min )->max;
-            $before = $arg->min unless $before;
-            my $after = $self->{parent}->intersection( $arg->max, $inf )->min;
-            $after = $arg->max unless $after;
-
-            return $arg->new( $before, $after );
-        },
-
-        quantize => sub {
-            my ($self, $arg) = @_;
-            if ($arg->{too_complex}) {
-                return $arg;
-            }
-            else {
-                return $arg->quantize( @{$self->{param}} )->_quantize_span;
-            }
-        },
-
-        offset => sub {
-            my ($self, $arg) = @_;
-            # offset - apply offset with negative values
-            my %tmp = @{$self->{param}};
-            my @values = sort @{$tmp{value}};
-
-            my $backtrack_arg2 = $arg->offset( 
-                   unit => $tmp{unit}, 
-                   mode => $tmp{mode}, 
-                   value => [ - $values[-1], - $values[0] ] );
-            return $arg->union( $backtrack_arg2 );   # fixes some problems with 'begin' mode
-        },
-
-    );
-}
-
-
-sub _backtrack {
-    my ($self, $method, $arg) = @_;
-    return $self->$method ($arg) unless $self->{too_complex};
-
-    $self->trace_open( title => 'backtrack '.$self->{method} ) if $TRACE;
-
-    $backtrack_depth++;
-    if ( $backtrack_depth > $max_backtrack_depth ) {
-        carp ( __PACKAGE__ . ": Backtrack too deep " .
-               "(more than $max_backtrack_depth levels)" );
-    }
-
-    if (exists $_backtrack{ $self->{method} } ) {
-        $arg = $_backtrack{ $self->{method} }->( $self, $arg );
-    }
-
-    my $result;
-    if ( ref($self->{parent}) eq 'ARRAY' ) {
-        # has 2 parents (intersection, union, until)
-
-        my ( $result1, $result2 ) = @{$self->{parent}};
-        $result1 = $result1->_backtrack( $method, $arg )
-            if $result1->{too_complex};
-        $result2 = $result2->_backtrack( $method, $arg )
-            if $result2->{too_complex};
-
-        $method = $self->{method};
-        if ( $result1->{too_complex} || $result2->{too_complex} ) {
-            $result = $result1->_function2( $method, $result2 );
-        }
-        else {
-            $result = $result1->$method ($result2);
-        }
-    }
-    else {
-        # has 1 parent and parameters (offset, select, quantize, iterate)
-
-        $result = $self->{parent}->_backtrack( $method, $arg ); 
-        $method = $self->{method};
-        $result = $result->$method ( @{$self->{param}} );
-    }
-
-    $backtrack_depth--;
-    $self->trace_close( arg => $result ) if $TRACE;
-    return $result;
-}
-
-
-sub intersects {
-    my $a1 = shift;
-    my $b1 = (ref ($_[0]) eq ref($a1) ) ? shift : $a1->new(@_);
-
-    $a1->trace(title=>"intersects");
-    if ($a1->{too_complex}) {
-        $a1 = $a1->_backtrack('intersection', $b1 ); 
-    }  # don't put 'else' here
-    if ($b1->{too_complex}) {
-        $b1 = $b1->_backtrack('intersection', $a1);
-    }
-    if (($a1->{too_complex}) or ($b1->{too_complex})) {
-        return undef;   # we don't know the answer!
-    }
-    return $a1->SUPER::intersects( $b1 );
-}
-
-
-sub iterate {
-    my $self = shift;
-    my $callback = shift;
-    die "First argument to iterate() must be a subroutine reference"
-        unless ref( $callback ) eq 'CODE';
-    my $backtrack_callback;
-    if ( @_ && $_[0] eq 'backtrack_callback' )
-    {
-        ( undef, $backtrack_callback ) = ( shift, shift );
-    }
-    my $set;
-    if ($self->{too_complex}) {
-        $self->trace(title=>"iterate:backtrack") if $TRACE;
-        $set = $self->_function( 'iterate', $callback, @_ );
-    }
-    else
-    {
-        $self->trace(title=>"iterate") if $TRACE;
-        $set = $self->SUPER::iterate( $callback, @_ );
-    }
-    $set->{backtrack_callback} = $backtrack_callback;
-    # warn "set backtrack_callback" if defined $backtrack_callback;
-    return $set;
-}
-
-
-sub intersection {
-    my $a1 = shift;
-    my $b1 = (ref ($_[0]) eq ref($a1) ) ? shift : $a1->new(@_);
-
-    $a1->trace_open(title=>"intersection", arg => $b1) if $TRACE;
-    if (($a1->{too_complex}) or ($b1->{too_complex})) {
-        my $arg0 = $a1->_quantize_span;
-        my $arg1 = $b1->_quantize_span;
-        unless (($arg0->{too_complex}) or ($arg1->{too_complex})) {
-            my $res = $arg0->intersection( $arg1 );
-            $a1->trace_close( arg => $res ) if $TRACE;
-            return $res;
-        }
-    }
-    if ($a1->{too_complex}) {
-        $a1 = $a1->_backtrack('intersection', $b1) unless $b1->{too_complex};
-    }  # don't put 'else' here
-    if ($b1->{too_complex}) {
-        $b1 = $b1->_backtrack('intersection', $a1) unless $a1->{too_complex};
-    }
-    if ( $a1->{too_complex} || $b1->{too_complex} ) {
-        $a1->trace_close( ) if $TRACE;
-        return $a1->_function2( 'intersection', $b1 );
-    }
-    return $a1->SUPER::intersection( $b1 );
-}
-
-
-sub intersected_spans {
-    my $a1 = shift;
-    my $b1 = ref ($_[0]) eq ref($a1) ? $_[0] : $a1->new(@_);
-
-    if ($a1->{too_complex}) {
-        $a1 = $a1->_backtrack('intersection', $b1 ) unless $b1->{too_complex};  
-    }  # don't put 'else' here
-    if ($b1->{too_complex}) {
-        $b1 = $b1->_backtrack('intersection', $a1) unless $a1->{too_complex};
-    }
-
-    if ( ! $b1->{too_complex} && ! $a1->{too_complex} )
-    {
-        return $a1->SUPER::intersected_spans ( $b1 );
-    }
-
-    return $b1->iterate(
-        sub {
-            my $tmp = $a1->intersection( $_[0] );
-            return $tmp unless defined $tmp->max;
-
-            my $before = $a1->intersection( $neg_inf, $tmp->min )->last;
-            my $after =  $a1->intersection( $tmp->max, $inf )->first;
-
-            $before = $tmp->union( $before )->first;
-            $after  = $tmp->union( $after )->last;
-
-            $tmp = $tmp->union( $before )
-                if defined $before && $tmp->intersects( $before );
-            $tmp = $tmp->union( $after )
-                if defined $after && $tmp->intersects( $after );
-            return $tmp;
-        }
-    );
-
-}
-
-
-sub complement {
-    my $a1 = shift;
-    # do we have a parameter?
-    if (@_) {
-        my $b1 = (ref ($_[0]) eq ref($a1) ) ? shift : $a1->new(@_);
-
-        $a1->trace_open(title=>"complement", arg => $b1) if $TRACE;
-        $b1 = $b1->complement;
-        my $tmp =$a1->intersection($b1);
-        $a1->trace_close( arg => $tmp ) if $TRACE;
-        return $tmp;
-    }
-    $a1->trace_open(title=>"complement") if $TRACE;
-    if ($a1->{too_complex}) {
-        $a1->trace_close( ) if $TRACE;
-        return $a1->_function( 'complement', @_ );
-    }
-    return $a1->SUPER::complement;
-}
-
-
-sub until {
-    my $a1 = shift;
-    my $b1 = (ref ($_[0]) eq ref($a1) ) ? shift : $a1->new(@_);
-
-    if (($a1->{too_complex}) or ($b1->{too_complex})) {
-        return $a1->_function2( 'until', $b1 );
-    }
-    return $a1->SUPER::until( $b1 );
-}
-
-
-sub union {
-    my $a1 = shift;
-    my $b1 = (ref ($_[0]) eq ref($a1) ) ? shift : $a1->new(@_);  
-    
-    $a1->trace_open(title=>"union", arg => $b1) if $TRACE;
-    if (($a1->{too_complex}) or ($b1->{too_complex})) {
-        $a1->trace_close( ) if $TRACE;
-        return $a1 if $b1->is_null;
-        return $b1 if $a1->is_null;
-        return $a1->_function2( 'union', $b1);
-    }
-    return $a1->SUPER::union( $b1 );
-}
-
-
-# there are some ways to process 'contains':
-# A CONTAINS B IF A == ( A UNION B )
-#    - faster
-# A CONTAINS B IF B == ( A INTERSECTION B )
-#    - can backtrack = works for unbounded sets
-sub contains {
-    my $a1 = shift;
-    $a1->trace_open(title=>"contains") if $TRACE;
-    if ( $a1->{too_complex} ) { 
-        # we use intersection because it is better for backtracking
-        my $b0 = (ref $_[0] eq ref $a1) ? shift : $a1->new(@_);
-        my $b1 = $a1->intersection($b0);
-        if ( $b1->{too_complex} ) {
-            $b1->trace_close( arg => 'undef' ) if $TRACE;
-            return undef;
-        }
-        $a1->trace_close( arg => ($b1 == $b0 ? 1 : 0) ) if $TRACE;
-        return ($b1 == $b0) ? 1 : 0;
-    }
-    my $b1 = $a1->union(@_);
-    if ( $b1->{too_complex} ) {
-        $b1->trace_close( arg => 'undef' ) if $TRACE;
-        return undef;
-    }
-    $a1->trace_close( arg => ($b1 == $a1 ? 1 : 0) ) if $TRACE;
-    return ($b1 == $a1) ? 1 : 0;
-}
-
-
-sub min_a { 
-    my $self = $_[0];
-    return @{$self->{min}} if exists $self->{min};
-    if ($self->{too_complex}) {
-        my @first = $self->first;
-        return @{$self->{min}} = $first[0]->min_a if defined $first[0];
-        return @{$self->{min}} = (undef, 0);
-    }
-    return $self->SUPER::min_a;
-};
-
-
-sub max_a { 
-    my $self = $_[0];
-    return @{$self->{max}} if exists $self->{max};
-    if ($self->{too_complex}) {
-        my @last = $self->last;
-        return @{$self->{max}} = $last[0]->max_a if defined $last[0];
-        return @{$self->{max}} = (undef, 0);
-    }
-    return $self->SUPER::max_a;
-};
-
-
-sub count {
-    my $self = $_[0];
-    # NOTE: subclasses may return "undef" if necessary
-    return $inf if $self->{too_complex};
-    return $self->SUPER::count;
-}
-
-
-sub size { 
-    my $self = $_[0];
-    if ($self->{too_complex}) {
-        my @min = $self->min_a;
-        my @max = $self->max_a;
-        return undef unless defined $max[0] && defined $min[0];
-        return $max[0] - $min[0];
-    }
-    return $self->SUPER::size;
-};
-
-
-sub spaceship {
-    my ($tmp1, $tmp2, $inverted) = @_;
-    carp "Can't compare unbounded sets" 
-        if $tmp1->{too_complex} or $tmp2->{too_complex};
-    return $tmp1->SUPER::spaceship( $tmp2, $inverted );
-}
-
-
-sub _cleanup { @_ }    # this subroutine is obsolete
-
-
-sub tolerance {
-    my $self = shift;
-    my $tmp = pop;
-    if (ref($self)) {  
-        # local
-        return $self->{tolerance} unless defined $tmp;
-        if ($self->{too_complex}) {
-            my $b1 = $self->_function( 'tolerance', $tmp );
-            $b1->{tolerance} = $tmp;   # for max/min processing
-            return $b1;
-        }
-        return $self->SUPER::tolerance( $tmp );
-    }
-    # class method
-    __PACKAGE__->SUPER::tolerance( $tmp ) if defined($tmp);
-    return __PACKAGE__->SUPER::tolerance;   
-}
-
-
-sub _pretty_print {
-    my $self = shift;
-    return "$self" unless $self->{too_complex};
-    return $self->{method} . "( " .
-               ( ref($self->{parent}) eq 'ARRAY' ? 
-                   $self->{parent}[0] . ' ; ' . $self->{parent}[1] : 
-                   $self->{parent} ) .
-           " )";
-}
-
-
-sub as_string {
-    my $self = shift;
-    return ( $PRETTY_PRINT ? $self->_pretty_print : $too_complex ) 
-        if $self->{too_complex};
-    return $self->SUPER::as_string;
-}
-
-
-sub DESTROY {}
-
-1;
-
-__END__
-
-
-=head1 NAME
-
-Set::Infinite - Sets of intervals
-
-
-=head1 SYNOPSIS
-
-  use Set::Infinite;
-
-  $set = Set::Infinite->new(1,2);    # [1..2]
-  print $set->union(5,6);            # [1..2],[5..6]
-
-
-=head1 DESCRIPTION
-
-Set::Infinite is a Set Theory module for infinite sets.
-
-A set is a collection of objects. 
-The objects that belong to a set are called its members, or "elements". 
-
-As objects we allow (almost) anything:  reals, integers, and objects (such as dates).
-
-We allow sets to be infinite.
-
-There is no account for the order of elements. For example, {1,2} = {2,1}.
-
-There is no account for repetition of elements. For example, {1,2,2} = {1,1,1,2} = {1,2}.
-
-=head1 CONSTRUCTOR
-
-=head2 new
-
-Creates a new set object:
-
-    $set = Set::Infinite->new;             # empty set
-    $set = Set::Infinite->new( 10 );       # single element
-    $set = Set::Infinite->new( 10, 20 );   # single range
-    $set = Set::Infinite->new( 
-              [ 10, 20 ], [ 50, 70 ] );    # two ranges
-
-=over 4
-
-=item empty set
-
-    $set = Set::Infinite->new;
-
-=item set with a single element
-
-    $set = Set::Infinite->new( 10 );
-
-    $set = Set::Infinite->new( [ 10 ] );
-
-=item set with a single span
-
-    $set = Set::Infinite->new( 10, 20 );
-
-    $set = Set::Infinite->new( [ 10, 20 ] );
-    # 10 <= x <= 20
-
-=item set with a single, open span
-
-    $set = Set::Infinite->new(
-        {
-            a => 10, open_begin => 0,
-            b => 20, open_end => 1,
-        }
-    );
-    # 10 <= x < 20
-
-=item set with multiple spans
-
-    $set = Set::Infinite->new( 10, 20,  100, 200 );
-
-    $set = Set::Infinite->new( [ 10, 20 ], [ 100, 200 ] );
-
-    $set = Set::Infinite->new(
-        {
-            a => 10, open_begin => 0,
-            b => 20, open_end => 0,
-        },
-        {
-            a => 100, open_begin => 0,
-            b => 200, open_end => 0,
-        }
-    );
-
-=back
-
-The C<new()> method expects I<ordered> parameters.
-
-If you have unordered ranges, you can build the set using C<union>:
-
-    @ranges = ( [ 10, 20 ], [ -10, 1 ] );
-    $set = Set::Infinite->new;
-    $set = $set->union( @$_ ) for @ranges;
-
-The data structures passed to C<new> must be I<immutable>.
-So this is not good practice:
-
-    $set = Set::Infinite->new( $object_a, $object_b );
-    $object_a->set_value( 10 );
-
-This is the recommended way to do it:
-
-    $set = Set::Infinite->new( $object_a->clone, $object_b->clone );
-    $object_a->set_value( 10 );
-
-
-=head2 clone / copy
-
-Creates a new object, and copy the object data.
-
-=head2 empty_set
-
-Creates an empty set.
-
-If called from an existing set, the empty set inherits
-the "type" and "density" characteristics.
-
-=head2 universal_set
-
-Creates a set containing "all" possible elements.
-
-If called from an existing set, the universal set inherits
-the "type" and "density" characteristics.
-
-=head1 SET FUNCTIONS
-
-=head2 union
-
-    $set = $set->union($b);
-
-Returns the set of all elements from both sets.
-
-This function behaves like an "OR" operation.
-
-    $set1 = new Set::Infinite( [ 1, 4 ], [ 8, 12 ] );
-    $set2 = new Set::Infinite( [ 7, 20 ] );
-    print $set1->union( $set2 );
-    # output: [1..4],[7..20]
-
-=head2 intersection
-
-    $set = $set->intersection($b);
-
-Returns the set of elements common to both sets.
-
-This function behaves like an "AND" operation.
-
-    $set1 = new Set::Infinite( [ 1, 4 ], [ 8, 12 ] );
-    $set2 = new Set::Infinite( [ 7, 20 ] );
-    print $set1->intersection( $set2 );
-    # output: [8..12]
-
-=head2 complement
-
-=head2 minus
-
-=head2 difference
-
-    $set = $set->complement;
-
-Returns the set of all elements that don't belong to the set.
-
-    $set1 = new Set::Infinite( [ 1, 4 ], [ 8, 12 ] );
-    print $set1->complement;
-    # output: (-inf..1),(4..8),(12..inf)
-
-The complement function might take a parameter:
-
-    $set = $set->minus($b);
-
-Returns the set-difference, that is, the elements that don't
-belong to the given set.
-
-    $set1 = new Set::Infinite( [ 1, 4 ], [ 8, 12 ] );
-    $set2 = new Set::Infinite( [ 7, 20 ] );
-    print $set1->minus( $set2 );
-    # output: [1..4]
-
-=head2 symmetric_difference
-
-Returns a set containing elements that are in either set,
-but not in both. This is the "set" version of "XOR".
-
-=head1 DENSITY METHODS    
-
-=head2 real
-
-    $set1 = $set->real;
-
-Returns a set with density "0".
-
-=head2 integer
-
-    $set1 = $set->integer;
-
-Returns a set with density "1".
-
-=head1 LOGIC FUNCTIONS
-
-=head2 intersects
-
-    $logic = $set->intersects($b);
-
-=head2 contains
-
-    $logic = $set->contains($b);
-
-=head2 is_empty
-
-=head2 is_null
-
-    $logic = $set->is_null;
-
-=head2 is_nonempty 
-
-This set that has at least 1 element.
-
-=head2 is_span 
-
-This set that has a single span or interval.
-
-=head2 is_singleton
-
-This set that has a single element.
-
-=head2 is_subset( $set )
-
-Every element of this set is a member of the given set.
-
-=head2 is_proper_subset( $set )
-
-Every element of this set is a member of the given set.
-Some members of the given set are not elements of this set.
-
-=head2 is_disjoint( $set )
-
-The given set has no elements in common with this set.
-
-=head2 is_too_complex
-
-Sometimes a set might be too complex to enumerate or print.
-
-This happens with sets that represent infinite recurrences, such as
-when you ask for a quantization on a
-set bounded by -inf or inf.
-
-See also: C<count> method.
-
-=head1 SCALAR FUNCTIONS
-
-=head2 min
-
-    $i = $set->min;
-
-=head2 max
-
-    $i = $set->max;
-
-=head2 size
-
-    $i = $set->size;  
-
-=head2 count
-
-    $i = $set->count;
-
-=head1 OVERLOADED OPERATORS
-
-=head2 stringification
-
-    print $set;
-
-    $str = "$set";
-
-See also: C<as_string>.
-
-=head2 comparison
-
-    sort
-
-    > < == >= <= <=> 
-
-See also: C<spaceship> method.
-
-=head1 CLASS METHODS
-
-    Set::Infinite->separators(@i)
-
-        chooses the interval separators for stringification. 
-
-        default are [ ] ( ) '..' ','.
-
-    inf
-
-        returns an 'Infinity' number.
-
-    minus_inf
-
-        returns '-Infinity' number.
-
-=head2 type
-
-    type( "My::Class::Name" )
-
-Chooses a default object data type.
-
-Default is none (a normal Perl SCALAR).
-
-
-=head1 SPECIAL SET FUNCTIONS
-
-=head2 span
-
-    $set1 = $set->span;
-
-Returns the set span.
-
-=head2 until
-
-Extends a set until another:
-
-    0,5,7 -> until 2,6,10
-
-gives
-
-    [0..2), [5..6), [7..10)
-
-=head2 start_set
-
-=head2 end_set
-
-These methods do the inverse of the "until" method.
-
-Given:
-
-    [0..2), [5..6), [7..10)
-
-start_set is:
-
-    0,5,7
-
-end_set is:
-
-    2,6,10
-
-=head2 intersected_spans
-
-    $set = $set1->intersected_spans( $set2 );
-
-The method returns a new set,
-containing all spans that are intersected by the given set.
-
-Unlike the C<intersection> method, the spans are not modified.
-See diagram below:
-
-               set1   [....]   [....]   [....]   [....]
-               set2      [................]
-
-       intersection      [.]   [....]   [.]
-
-  intersected_spans   [....]   [....]   [....]
-
-
-=head2 quantize
-
-    quantize( parameters )
-
-        Makes equal-sized subsets.
-
-        Returns an ordered set of equal-sized subsets.
-
-        Example: 
-
-            $set = Set::Infinite->new([1,3]);
-            print join (" ", $set->quantize( quant => 1 ) );
-
-        Gives: 
-
-            [1..2) [2..3) [3..4)
-
-=head2 select
-
-    select( parameters )
-
-Selects set spans based on their ordered positions
-
-C<select> has a behaviour similar to an array C<slice>.
-
-            by       - default=All
-            count    - default=Infinity
-
- 0  1  2  3  4  5  6  7  8      # original set
- 0  1  2                        # count => 3 
-    1              6            # by => [ -2, 1 ]
-
-=head2 offset
-
-    offset ( parameters )
-
-Offsets the subsets. Parameters: 
-
-    value   - default=[0,0]
-    mode    - default='offset'. Possible values are: 'offset', 'begin', 'end'.
-    unit    - type of value. Can be 'days', 'weeks', 'hours', 'minutes', 'seconds'.
-
-=head2 iterate
-
-    iterate ( sub { } , @args )
-
-Iterates on the set spans, over a callback subroutine. 
-Returns the union of all partial results.
-
-The callback argument C<$_[0]> is a span. If there are additional arguments they are passed to the callback.
-
-The callback can return a span, a hashref (see C<Set::Infinite::Basic>), a scalar, an object, or C<undef>.
-
-[EXPERIMENTAL]
-C<iterate> accepts an optional C<backtrack_callback> argument. 
-The purpose of the C<backtrack_callback> is to I<reverse> the
-iterate() function, overcoming the limitations of the internal
-backtracking algorithm.
-The syntax is:
-
-    iterate ( sub { } , backtrack_callback => sub { }, @args )
-
-The C<backtrack_callback> can return a span, a hashref, a scalar, 
-an object, or C<undef>. 
-
-For example, the following snippet adds a constant to each
-element of an unbounded set:
-
-    $set1 = $set->iterate( 
-                 sub { $_[0]->min + 54, $_[0]->max + 54 }, 
-              backtrack_callback =>  
-                 sub { $_[0]->min - 54, $_[0]->max - 54 }, 
-              );
-
-=head2 first / last
-
-    first / last
-
-In scalar context returns the first or last interval of a set.
-
-In list context returns the first or last interval of a set, 
-and the remaining set (the 'tail').
-
-See also: C<min>, C<max>, C<min_a>, C<max_a> methods.
-
-=head2 type
-
-    type( "My::Class::Name" )
-
-Chooses a default object data type. 
-
-default is none (a normal perl SCALAR).
-
-
-=head1 INTERNAL FUNCTIONS
-
-=head2 _backtrack
-
-    $set->_backtrack( 'intersection', $b );
-
-Internal function to evaluate recurrences.
-
-=head2 numeric
-
-    $set->numeric;
-
-Internal function to ignore the set "type".
-It is used in some internal optimizations, when it is
-possible to use scalar values instead of objects.
-
-=head2 fixtype
-
-    $set->fixtype;
-
-Internal function to fix the result of operations
-that use the numeric() function.
-
-=head2 tolerance
-
-    $set = $set->tolerance(0)    # defaults to real sets (default)
-    $set = $set->tolerance(1)    # defaults to integer sets
-
-Internal function for changing the set "density".
-
-=head2 min_a
-
-    ($min, $min_is_open) = $set->min_a;
-
-=head2 max_a
-
-    ($max, $max_is_open) = $set->max_a;
-
-
-=head2 as_string
-
-Implements the "stringification" operator.
-
-Stringification of unbounded recurrences is not implemented.
-
-Unbounded recurrences are stringified as "function descriptions",
-if the class variable $PRETTY_PRINT is set.
-
-=head2 spaceship
-
-Implements the "comparison" operator.
-
-Comparison of unbounded recurrences is not implemented.
-
-
-=head1 CAVEATS
-
-=over 4
-
-=item * constructor "span" notation
-
-    $set = Set::Infinite->new(10,1);
-
-Will be interpreted as [1..10]
-
-=item * constructor "multiple-span" notation
-
-    $set = Set::Infinite->new(1,2,3,4);
-
-Will be interpreted as [1..2],[3..4] instead of [1,2,3,4].
-You probably want ->new([1],[2],[3],[4]) instead,
-or maybe ->new(1,4) 
-
-=item * "range operator"
-
-    $set = Set::Infinite->new(1..3);
-
-Will be interpreted as [1..2],3 instead of [1,2,3].
-You probably want ->new(1,3) instead.
-
-=back
-
-=head1 INTERNALS
-
-The base I<set> object, without recurrences, is a C<Set::Infinite::Basic>.
-
-A I<recurrence-set> is represented by a I<method name>, 
-one or two I<parent objects>, and extra arguments.
-The C<list> key is set to an empty array, and the
-C<too_complex> key is set to C<1>.
-
-This is a structure that holds the union of two "complex sets":
-
-  {
-    too_complex => 1,             # "this is a recurrence"
-    list   => [ ],                # not used
-    method => 'union',            # function name
-    parent => [ $set1, $set2 ],   # "leaves" in the syntax-tree
-    param  => [ ]                 # optional arguments for the function
-  }
-
-This is a structure that holds the complement of a "complex set":
-
-  {
-    too_complex => 1,             # "this is a recurrence"
-    list   => [ ],                # not used
-    method => 'complement',       # function name
-    parent => $set,               # "leaf" in the syntax-tree
-    param  => [ ]                 # optional arguments for the function
-  }
-
-
-=head1 SEE ALSO
-
-See modules DateTime::Set, DateTime::Event::Recurrence, 
-DateTime::Event::ICal, DateTime::Event::Cron
-for up-to-date information on date-sets.
-
-The perl-date-time project <http://datetime.perl.org> 
-
-
-=head1 AUTHOR
-
-Flavio S. Glock <fglock@gmail.com>
-
-=head1 COPYRIGHT
-
-Copyright (c) 2003 Flavio Soibelmann Glock.  All rights reserved.  
-This program is free software; you can redistribute it and/or modify 
-it under the same terms as Perl itself.
-
-The full text of the license can be found in the LICENSE file included
-with this module.
-
-=cut
-
diff --git a/modules/fallback/Set/Infinite/Arithmetic.pm b/modules/fallback/Set/Infinite/Arithmetic.pm
deleted file mode 100644 (file)
index e1a05c5..0000000
+++ /dev/null
@@ -1,367 +0,0 @@
-package Set::Infinite::Arithmetic;
-# Copyright (c) 2001 Flavio Soibelmann Glock. All rights reserved.
-# This program is free software; you can redistribute it and/or
-# modify it under the same terms as Perl itself.
-
-use strict;
-# use warnings;
-require Exporter;
-use Carp;
-use Time::Local;
-use POSIX qw(floor);
-
-use vars qw( @EXPORT @EXPORT_OK $inf );
-
-@EXPORT = qw();
-@EXPORT_OK = qw();
-# @EXPORT_OK = qw( %subs_offset2 %Offset_to_value %Value_to_offset %Init_quantizer );
-
-$inf = 100**100**100;    # $Set::Infinite::inf;  doesn't work! (why?)
-
-=head2 NAME
-
-Set::Infinite::Arithmetic - Scalar operations used by quantize() and offset()
-
-=head2 AUTHOR
-
-Flavio Soibelmann Glock - fglock@pucrs.br
-
-=cut
-
-use vars qw( $day_size $hour_size $minute_size $second_size ); 
-$day_size =    timegm(0,0,0,2,3,2001) - timegm(0,0,0,1,3,2001);
-$hour_size =   $day_size / 24;
-$minute_size = $hour_size / 60;
-$second_size = $minute_size / 60;
-
-use vars qw( %_MODE %subs_offset2 %Offset_to_value @week_start %Init_quantizer %Value_to_offset %Offset_to_value );
-
-=head2 %_MODE hash of subs
-
-    $a->offset ( value => [1,2], mode => 'offset', unit => 'days' );
-
-    $a->offset ( value => [1,2, -5,-4], mode => 'offset', unit => 'days' );
-
-note: if mode = circle, then "-5" counts from end (like a Perl negative array index).
-
-    $a->offset ( value => [1,2], mode => 'offset', unit => 'days', strict => $a );
-
-option 'strict' will return intersection($a,offset). Default: none.
-
-=cut
-
-# return value = ($this, $next, $cmp)
-%_MODE = (
-    circle => sub {
-            if ($_[3] >= 0) {
-                &{ $_[0] } ($_[1], $_[3], $_[4] ) 
-            }
-            else {
-                &{ $_[0] } ($_[2], $_[3], $_[4] ) 
-            }
-    },
-    begin =>  sub { &{ $_[0] } ($_[1], $_[3], $_[4] ) },
-    end =>    sub { &{ $_[0] } ($_[2], $_[3], $_[4] ) },
-    offset => sub {
-            my ($this, undef) = &{ $_[0] } ($_[1], $_[3], $_[4] );
-            my (undef, $next) = &{ $_[0] } ($_[2], $_[3], $_[4] );
-            ($this, $next); 
-    }
-);
-
-
-=head2 %subs_offset2($object, $offset1, $offset2)
-
-    &{ $subs_offset2{$unit} } ($object, $offset1, $offset2);
-
-A hash of functions that return:
-
-    ($object+$offset1, $object+$offset2)
-
-in $unit context.
-
-Returned $object+$offset1, $object+$offset2 may be scalars or objects.
-
-=cut
-
-%subs_offset2 = (
-    weekdays =>    sub {
-        # offsets to week-day specified
-        # 0 = first sunday from today (or today if today is sunday)
-        # 1 = first monday from today (or today if today is monday)
-        # 6 = first friday from today (or today if today is friday)
-        # 13 = second friday from today 
-        # -1 = last saturday from today (not today, even if today were saturday)
-        # -2 = last friday
-        my ($self, $index1, $index2) = @_;
-        return ($self, $self) if $self == $inf;
-        # my $class = ref($self);
-        my @date = gmtime( $self ); 
-        my $wday = $date[6];
-        my ($tmp1, $tmp2);
-
-        $tmp1 = $index1 - $wday;
-        if ($index1 >= 0) { 
-            $tmp1 += 7 if $tmp1 < 0; # it will only happen next week 
-        }
-        else {
-            $tmp1 += 7 if $tmp1 < -7; # if will happen this week
-        } 
-
-        $tmp2 = $index2 - $wday;
-        if ($index2 >= 0) { 
-            $tmp2 += 7 if $tmp2 < 0; # it will only happen next week 
-        }
-        else {
-            $tmp2 += 7 if $tmp2 < -7; # if will happen this week
-        } 
-
-        # print " [ OFS:weekday $self $tmp1 $tmp2 ] \n";
-        # $date[3] += $tmp1;
-        $tmp1 = $self + $tmp1 * $day_size;
-        # $date[3] += $tmp2 - $tmp1;
-        $tmp2 = $self + $tmp2 * $day_size;
-
-        ($tmp1, $tmp2);
-    },
-    years =>     sub {
-        my ($self, $index, $index2) = @_;
-        return ($self, $self) if $self == $inf;
-        # my $class = ref($self);
-        # print " [ofs:year:$self -- $index]\n";
-        my @date = gmtime( $self ); 
-        $date[5] +=    1900 + $index;
-        my $tmp = timegm(@date);
-
-        $date[5] +=    $index2 - $index;
-        my $tmp2 = timegm(@date);
-
-        ($tmp, $tmp2);
-    },
-    months =>     sub {
-        my ($self, $index, $index2) = @_;
-        # carp " [ofs:month:$self -- $index -- $inf]";
-        return ($self, $self) if $self == $inf;
-        # my $class = ref($self);
-        my @date = gmtime( $self );
-
-        my $mon =     $date[4] + $index; 
-        my $year =    $date[5] + 1900;
-        # print " [OFS: month: from $year$mon ]\n";
-        if (($mon > 11) or ($mon < 0)) {
-            my $addyear = floor($mon / 12);
-            $mon = $mon - 12 * $addyear;
-            $year += $addyear;
-        }
-
-        my $mon2 =     $date[4] + $index2; 
-        my $year2 =    $date[5] + 1900;
-        if (($mon2 > 11) or ($mon2 < 0)) {
-            my $addyear2 = floor($mon2 / 12);
-            $mon2 = $mon2 - 12 * $addyear2;
-            $year2 += $addyear2;
-        }
-
-        # print " [OFS: month: to $year $mon ]\n";
-
-        $date[4] = $mon;
-        $date[5] = $year;
-        my $tmp = timegm(@date);
-
-        $date[4] = $mon2;
-        $date[5] = $year2;
-        my $tmp2 = timegm(@date);
-
-        ($tmp, $tmp2);
-    },
-    days =>     sub { 
-        ( $_[0] + $_[1] * $day_size,
-          $_[0] + $_[2] * $day_size,
-        )
-    },
-    weeks =>    sub { 
-        ( $_[0] + $_[1] * (7 * $day_size),
-          $_[0] + $_[2] * (7 * $day_size),
-        )
-    },
-    hours =>    sub { 
-        # carp " [ $_[0]+$_[1] hour = ".( $_[0] + $_[1] * $hour_size )." mode=".($_[0]->{mode})." ]";
-        ( $_[0] + $_[1] * $hour_size,
-          $_[0] + $_[2] * $hour_size,
-        )
-    },
-    minutes =>    sub { 
-        ( $_[0] + $_[1] * $minute_size,
-          $_[0] + $_[2] * $minute_size,
-        )
-    },
-    seconds =>    sub { 
-        ( $_[0] + $_[1] * $second_size, 
-          $_[0] + $_[2] * $second_size, 
-        )
-    },
-    one =>      sub { 
-        ( $_[0] + $_[1], 
-          $_[0] + $_[2], 
-        )
-    },
-);
-
-
-@week_start = ( 0, -1, -2, -3, 3, 2, 1, 0, -1, -2, -3, 3, 2, 1, 0 );
-
-=head2 %Offset_to_value($object, $offset)
-
-=head2 %Init_quantizer($object)
-
-    $Offset_to_value{$unit} ($object, $offset);
-
-    $Init_quantizer{$unit} ($object);
-
-Maps an 'offset value' to a 'value'
-
-A hash of functions that return ( int($object) + $offset ) in $unit context.
-
-Init_quantizer subroutines must be called before using subs_offset1 functions.
-
-int(object)+offset is a scalar.
-
-Offset_to_value is optimized for calling it multiple times on the same object,
-with different offsets. That's why there is a separate initialization
-subroutine.
-
-$self->{offset} is created on initialization. It is an index used 
-by the memoization cache.
-
-=cut
-
-%Offset_to_value = (
-    weekyears =>    sub {
-        my ($self, $index) = @_;
-        my $epoch = timegm( 0,0,0, 
-            1,0,$self->{offset} + $self->{quant} * $index);
-        my @time = gmtime($epoch);
-        # print " [QT_D:weekyears:$self->{offset} + $self->{quant} * $index]\n";
-        # year modulo week
-        # print " [QT:weekyears: time = ",join(";", @time )," ]\n";
-        $epoch += ( $week_start[$time[6] + 7 - $self->{wkst}] ) * $day_size;
-        # print " [QT:weekyears: week=",join(";", gmtime($epoch) )," wkst=$self->{wkst} tbl[",$time[6] + 7 - $self->{wkst},"]=",$week_start[$time[6] + 7 - $self->{wkst}]," ]\n\n";
-
-        my $epoch2 = timegm( 0,0,0,
-            1,0,$self->{offset} + $self->{quant} * (1 + $index) );
-        @time = gmtime($epoch2);
-        $epoch2 += ( $week_start[$time[6] + 7 - $self->{wkst}] ) * $day_size;
-        ( $epoch, $epoch2 );
-    },
-    years =>     sub {
-        my $index = $_[0]->{offset} + $_[0]->{quant} * $_[1];
-        ( timegm( 0,0,0, 1, 0, $index),
-          timegm( 0,0,0, 1, 0, $index + $_[0]->{quant}) )
-      },
-    months =>     sub {
-        my $mon = $_[0]->{offset} + $_[0]->{quant} * $_[1]; 
-        my $year = int($mon / 12);
-        $mon -= $year * 12;
-        my $tmp = timegm( 0,0,0, 1, $mon, $year);
-
-        $mon += $year * 12 + $_[0]->{quant};
-        $year = int($mon / 12);
-        $mon -= $year * 12;
-        ( $tmp, timegm( 0,0,0, 1, $mon, $year) );
-      },
-    weeks =>    sub {
-        my $tmp = 3 * $day_size + $_[0]->{quant} * ($_[0]->{offset} + $_[1]);
-        ($tmp, $tmp + $_[0]->{quant})
-      },
-    days =>     sub {
-        my $tmp = $_[0]->{quant} * ($_[0]->{offset} + $_[1]);
-        ($tmp, $tmp + $_[0]->{quant})
-      },
-    hours =>    sub {
-        my $tmp = $_[0]->{quant} * ($_[0]->{offset} + $_[1]);
-        ($tmp, $tmp + $_[0]->{quant})
-      },
-    minutes =>    sub {
-        my $tmp = $_[0]->{quant} * ($_[0]->{offset} + $_[1]);
-        ($tmp, $tmp + $_[0]->{quant})
-      },
-    seconds =>    sub {
-        my $tmp = $_[0]->{quant} * ($_[0]->{offset} + $_[1]);
-        ($tmp, $tmp + $_[0]->{quant})
-      },
-    one =>       sub { 
-        my $tmp = $_[0]->{quant} * ($_[0]->{offset} + $_[1]);
-        ($tmp, $tmp + $_[0]->{quant})
-      },
-);
-
-
-# Maps an 'offset value' to a 'value'
-
-%Value_to_offset = (
-    one =>      sub { floor( $_[1] / $_[0]{quant} ) },
-    seconds =>  sub { floor( $_[1] / $_[0]{quant} ) },
-    minutes =>  sub { floor( $_[1] / $_[0]{quant} ) },
-    hours =>    sub { floor( $_[1] / $_[0]{quant} ) },
-    days =>     sub { floor( $_[1] / $_[0]{quant} ) },
-    weeks =>    sub { floor( ($_[1] - 3 * $day_size) / $_[0]{quant} ) },
-    months =>   sub {
-        my @date = gmtime( 0 + $_[1] );
-        my $tmp = $date[4] + 12 * (1900 + $date[5]);
-        floor( $tmp / $_[0]{quant} );
-      },
-    years =>    sub {
-        my @date = gmtime( 0 + $_[1] );
-        my $tmp = $date[5] + 1900;
-        floor( $tmp / $_[0]{quant} );
-      },
-    weekyears =>    sub {
-
-        my ($self, $value) = @_;
-        my @date;
-
-        # find out YEAR number
-        @date = gmtime( 0 + $value );
-        my $year = floor( $date[5] + 1900 / $self->{quant} );
-
-        # what is the EPOCH for this week-year's begin ?
-        my $begin_epoch = timegm( 0,0,0,  1,0,$year);
-        @date = gmtime($begin_epoch);
-        $begin_epoch += ( $week_start[$date[6] + 7 - $self->{wkst}] ) * $day_size;
-
-        # what is the EPOCH for this week-year's end ?
-        my $end_epoch = timegm( 0,0,0,  1,0,$year+1);
-        @date = gmtime($end_epoch);
-        $end_epoch += ( $week_start[$date[6] + 7 - $self->{wkst}] ) * $day_size;
-
-        $year-- if $value <  $begin_epoch;
-        $year++ if $value >= $end_epoch;
-
-        # carp " value=$value offset=$year this_epoch=".$begin_epoch;
-        # carp " next_epoch=".$end_epoch;
-
-        $year;
-      },
-);
-
-# Initialize quantizer
-
-%Init_quantizer = (
-    one =>       sub {},
-    seconds =>   sub { $_[0]->{quant} *= $second_size },
-    minutes =>   sub { $_[0]->{quant} *= $minute_size },
-    hours =>     sub { $_[0]->{quant} *= $hour_size },
-    days =>      sub { $_[0]->{quant} *= $day_size },
-    weeks =>     sub { $_[0]->{quant} *= 7 * $day_size },
-    months =>    sub {},
-    years =>     sub {},
-    weekyears => sub { 
-        $_[0]->{wkst} = 1 unless defined $_[0]->{wkst};
-        # select which 'cache' to use
-        # $_[0]->{memo} .= $_[0]->{wkst};
-    },
-);
-
-
-1;
-
diff --git a/modules/fallback/Set/Infinite/Basic.pm b/modules/fallback/Set/Infinite/Basic.pm
deleted file mode 100644 (file)
index b917bfb..0000000
+++ /dev/null
@@ -1,1157 +0,0 @@
-package Set::Infinite::Basic;
-
-# Copyright (c) 2001, 2002, 2003 Flavio Soibelmann Glock. All rights reserved.
-# This program is free software; you can redistribute it and/or
-# modify it under the same terms as Perl itself.
-
-require 5.005_03;
-use strict;
-
-require Exporter;
-use Carp;
-use Data::Dumper; 
-use vars qw( @ISA @EXPORT_OK @EXPORT );
-use vars qw( $Type $tolerance $fixtype $inf $minus_inf @Separators $neg_inf );
-
-@ISA = qw(Exporter);
-@EXPORT_OK = qw( INFINITY NEG_INFINITY );
-@EXPORT = qw();
-
-use constant INFINITY => 100**100**100;
-use constant NEG_INFINITY => - INFINITY;
-
-$inf       = INFINITY;
-$minus_inf = $neg_inf = NEG_INFINITY;
-
-use overload
-    '<=>' => \&spaceship,
-    qw("" as_string),
-;
-
-
-# TODO: make this an object _and_ class method
-# TODO: POD
-sub separators {
-    shift;
-    return $Separators[ $_[0] ] if $#_ == 0;
-    @Separators = @_ if @_;
-    return @Separators;
-}
-
-BEGIN {
-    __PACKAGE__->separators (
-        '[', ']',    # a closed interval
-        '(', ')',    # an open interval
-        '..',        # number separator
-        ',',         # list separator
-        '', '',      # set delimiter  '{' '}'
-    );
-    # global defaults for object private vars
-    $Type = undef;
-    $tolerance = 0;
-    $fixtype = 1;
-}
-
-# _simple_* set of internal methods: basic processing of "spans"
-
-sub _simple_intersects {
-    my $tmp1 = $_[0];
-    my $tmp2 = $_[1];
-    my ($i_beg, $i_end, $open_beg, $open_end);
-    my $cmp = $tmp1->{a} <=> $tmp2->{a};
-    if ($cmp < 0) {
-        $i_beg       = $tmp2->{a};
-        $open_beg    = $tmp2->{open_begin};
-    }
-    elsif ($cmp > 0) {
-        $i_beg       = $tmp1->{a};
-        $open_beg    = $tmp1->{open_begin};
-    }
-    else {
-        $i_beg       = $tmp1->{a};
-        $open_beg    = $tmp1->{open_begin} || $tmp2->{open_begin};
-    }
-    $cmp = $tmp1->{b} <=> $tmp2->{b};
-    if ($cmp > 0) {
-        $i_end       = $tmp2->{b};
-        $open_end    = $tmp2->{open_end};
-    }
-    elsif ($cmp < 0) {
-        $i_end       = $tmp1->{b};
-        $open_end    = $tmp1->{open_end};
-    }
-    else { 
-        $i_end       = $tmp1->{b};
-        $open_end    = ($tmp1->{open_end} || $tmp2->{open_end});
-    }
-    $cmp = $i_beg <=> $i_end;
-    return 0 if 
-        ( $cmp > 0 ) || 
-        ( ($cmp == 0) && ($open_beg || $open_end) ) ;
-    return 1;
-}
-
-
-sub _simple_complement {
-    my $self = $_[0];
-    if ($self->{b} == $inf) {
-        return if $self->{a} == $neg_inf;
-        return { a => $neg_inf, 
-                 b => $self->{a}, 
-                 open_begin => 1, 
-                 open_end => ! $self->{open_begin} };
-    }
-    if ($self->{a} == $neg_inf) {
-        return { a => $self->{b}, 
-                 b => $inf,  
-                 open_begin => ! $self->{open_end}, 
-                 open_end => 1 };
-    }
-    ( { a => $neg_inf, 
-        b => $self->{a}, 
-        open_begin => 1, 
-        open_end => ! $self->{open_begin} 
-      },
-      { a => $self->{b}, 
-        b => $inf,  
-        open_begin => ! $self->{open_end}, 
-        open_end => 1 
-      }
-    );
-}
-
-sub _simple_union {
-    my ($tmp2, $tmp1, $tolerance) = @_; 
-    my $cmp; 
-    if ($tolerance) {
-        # "integer"
-        my $a1_open =  $tmp1->{open_begin} ? -$tolerance : $tolerance ;
-        my $b1_open =  $tmp1->{open_end}   ? -$tolerance : $tolerance ;
-        my $a2_open =  $tmp2->{open_begin} ? -$tolerance : $tolerance ;
-        my $b2_open =  $tmp2->{open_end}   ? -$tolerance : $tolerance ;
-        # open_end touching?
-        if ((($tmp1->{b}+$tmp1->{b}) + $b1_open ) < 
-            (($tmp2->{a}+$tmp2->{a}) - $a2_open)) {
-            # self disjuncts b
-            return ( $tmp1, $tmp2 );
-        }
-        if ((($tmp1->{a}+$tmp1->{a}) - $a1_open ) > 
-            (($tmp2->{b}+$tmp2->{b}) + $b2_open)) {
-            # self disjuncts b
-            return ( $tmp2, $tmp1 );
-        }
-    }
-    else {
-        # "real"
-        $cmp = $tmp1->{b} <=> $tmp2->{a};
-        if ( $cmp < 0 ||
-             ( $cmp == 0 && $tmp1->{open_end} && $tmp2->{open_begin} ) ) {
-            return ( $tmp1, $tmp2 );
-        }
-        $cmp = $tmp1->{a} <=> $tmp2->{b};
-        if ( $cmp > 0 || 
-             ( $cmp == 0 && $tmp2->{open_end} && $tmp1->{open_begin} ) ) {
-            return ( $tmp2, $tmp1 );
-        }
-    }
-
-    my $tmp;
-    $cmp = $tmp1->{a} <=> $tmp2->{a};
-    if ($cmp > 0) {
-        $tmp->{a} = $tmp2->{a};
-        $tmp->{open_begin} = $tmp2->{open_begin};
-    }
-    elsif ($cmp == 0) {
-        $tmp->{a} = $tmp1->{a};
-        $tmp->{open_begin} = $tmp1->{open_begin} ? $tmp2->{open_begin} : 0;
-    }
-    else {
-        $tmp->{a} = $tmp1->{a};
-        $tmp->{open_begin} = $tmp1->{open_begin};
-    }
-
-    $cmp = $tmp1->{b} <=> $tmp2->{b};
-    if ($cmp < 0) {
-        $tmp->{b} = $tmp2->{b};
-        $tmp->{open_end} = $tmp2->{open_end};
-    }
-    elsif ($cmp == 0) {
-        $tmp->{b} = $tmp1->{b};
-        $tmp->{open_end} = $tmp1->{open_end} ? $tmp2->{open_end} : 0;
-    }
-    else {
-        $tmp->{b} = $tmp1->{b};
-        $tmp->{open_end} = $tmp1->{open_end};
-    }
-    return $tmp;
-}
-
-
-sub _simple_spaceship {
-    my ($tmp1, $tmp2, $inverted) = @_;
-    my $cmp;
-    if ($inverted) {
-        $cmp = $tmp2->{a} <=> $tmp1->{a};
-        return $cmp if $cmp;
-        $cmp = $tmp1->{open_begin} <=> $tmp2->{open_begin};
-        return $cmp if $cmp;
-        $cmp = $tmp2->{b} <=> $tmp1->{b};
-        return $cmp if $cmp;
-        return $tmp1->{open_end} <=> $tmp2->{open_end};
-    }
-    $cmp = $tmp1->{a} <=> $tmp2->{a};
-    return $cmp if $cmp;
-    $cmp = $tmp2->{open_begin} <=> $tmp1->{open_begin};
-    return $cmp if $cmp;
-    $cmp = $tmp1->{b} <=> $tmp2->{b};
-    return $cmp if $cmp;
-    return $tmp2->{open_end} <=> $tmp1->{open_end};
-}
-
-
-sub _simple_new {
-    my ($tmp, $tmp2, $type) = @_;
-    if ($type) {
-        if ( ref($tmp) ne $type ) { 
-            $tmp = new $type $tmp;
-        }
-        if ( ref($tmp2) ne $type ) {
-            $tmp2 = new $type $tmp2;
-        }
-    }
-    if ($tmp > $tmp2) {
-        carp "Invalid interval specification: start value is after end";
-        # ($tmp, $tmp2) = ($tmp2, $tmp);
-    }
-    return { a => $tmp , b => $tmp2 , open_begin => 0 , open_end => 0 };
-}
-
-
-sub _simple_as_string {
-    my $set = shift;
-    my $self = $_[0];
-    my $s;
-    return "" unless defined $self;
-    $self->{open_begin} = 1 if ($self->{a} == -$inf );
-    $self->{open_end}   = 1 if ($self->{b} == $inf );
-    my $tmp1 = $self->{a};
-    $tmp1 = $tmp1->datetime if UNIVERSAL::can( $tmp1, 'datetime' );
-    $tmp1 = "$tmp1";
-    my $tmp2 = $self->{b};
-    $tmp2 = $tmp2->datetime if UNIVERSAL::can( $tmp2, 'datetime' );
-    $tmp2 = "$tmp2";
-    return $tmp1 if $tmp1 eq $tmp2;
-    $s = $self->{open_begin} ? $set->separators(2) : $set->separators(0);
-    $s .= $tmp1 . $set->separators(4) . $tmp2;
-    $s .= $self->{open_end} ? $set->separators(3) : $set->separators(1);
-    return $s;
-}
-
-# end of "_simple_" methods
-
-
-sub type {
-    my $self = shift;
-    unless (@_) {
-        return ref($self) ? $self->{type} : $Type;
-    }
-    my $tmp_type = shift;
-    eval "use " . $tmp_type;
-    carp "Warning: can't start $tmp_type : $@" if $@;
-    if (ref($self))  {
-        $self->{type} = $tmp_type;
-        return $self;
-    }
-    else {
-        $Type = $tmp_type;
-        return $Type;
-    }
-}
-
-sub list {
-    my $self = shift;
-    my @b = ();
-    foreach (@{$self->{list}}) {
-        push @b, $self->new($_);
-    }
-    return @b;
-}
-
-sub fixtype {
-    my $self = shift;
-    $self = $self->copy;
-    $self->{fixtype} = 1;
-    my $type = $self->type;
-    return $self unless $type;
-    foreach (@{$self->{list}}) {
-        $_->{a} = $type->new($_->{a}) unless ref($_->{a}) eq $type;
-        $_->{b} = $type->new($_->{b}) unless ref($_->{b}) eq $type;
-    }
-    return $self;
-}
-
-sub numeric {
-    my $self = shift;
-    return $self unless $self->{fixtype};
-    $self = $self->copy;
-    $self->{fixtype} = 0;
-    foreach (@{$self->{list}}) {
-        $_->{a} = 0 + $_->{a};
-        $_->{b} = 0 + $_->{b};
-    }
-    return $self;
-}
-
-sub _no_cleanup { $_[0] }   # obsolete
-
-sub first {
-    my $self = $_[0];
-    if (exists $self->{first} ) {
-        return wantarray ? @{$self->{first}} : $self->{first}[0];
-    }
-    unless ( @{$self->{list}} ) {
-        return wantarray ? (undef, 0) : undef; 
-    }
-    my $first = $self->new( $self->{list}[0] );
-    return $first unless wantarray;
-    my $res = $self->new;   
-    push @{$res->{list}}, @{$self->{list}}[1 .. $#{$self->{list}}];
-    return @{$self->{first}} = ($first) if $res->is_null;
-    return @{$self->{first}} = ($first, $res);
-}
-
-sub last {
-    my $self = $_[0];
-    if (exists $self->{last} ) {
-        return wantarray ? @{$self->{last}} : $self->{last}[0];
-    }
-    unless ( @{$self->{list}} ) {
-        return wantarray ? (undef, 0) : undef;
-    }
-    my $last = $self->new( $self->{list}[-1] );
-    return $last unless wantarray;  
-    my $res = $self->new; 
-    push @{$res->{list}}, @{$self->{list}}[0 .. $#{$self->{list}}-1];
-    return @{$self->{last}} = ($last) if $res->is_null;
-    return @{$self->{last}} = ($last, $res);
-}
-
-sub is_null {
-    @{$_[0]->{list}} ? 0 : 1;
-}
-
-sub is_empty {
-    $_[0]->is_null;
-}
-
-sub is_nonempty {
-    ! $_[0]->is_null;
-}
-
-sub is_span {
-    ( $#{$_[0]->{list}} == 0 ) ? 1 : 0;
-}
-
-sub is_singleton {
-    ( $#{$_[0]->{list}} == 0 &&
-      $_[0]->{list}[0]{a} == $_[0]->{list}[0]{b} ) ? 1 : 0;
-}
-
-sub is_subset {
-    my $a1 = shift;
-    my $b1;
-    if (ref ($_[0]) eq ref($a1) ) { 
-        $b1 = shift;
-    } 
-    else {
-        $b1 = $a1->new(@_);  
-    }
-    return $b1->contains( $a1 );
-}
-
-sub is_proper_subset {
-    my $a1 = shift;
-    my $b1;
-    if (ref ($_[0]) eq ref($a1) ) { 
-        $b1 = shift;
-    } 
-    else {
-        $b1 = $a1->new(@_);  
-    }
-
-    my $contains = $b1->contains( $a1 );
-    return $contains unless $contains;
-     
-    my $equal = ( $a1 == $b1 );
-    return $equal if !defined $equal || $equal;
-
-    return 1;
-}
-
-sub is_disjoint {
-    my $intersects = shift->intersects( @_ );
-    return ! $intersects if defined $intersects;
-    return $intersects;
-}
-
-sub iterate {
-    # TODO: options 'no-sort', 'no-merge', 'keep-null' ...
-    my $a1 = shift;
-    my $iterate = $a1->empty_set();
-    my (@tmp, $ia);
-    my $subroutine = shift;
-    foreach $ia (0 .. $#{$a1->{list}}) {
-        @tmp = $subroutine->( $a1->new($a1->{list}[$ia]), @_ );
-        $iterate = $iterate->union(@tmp) if @tmp; 
-    }
-    return $iterate;    
-}
-
-
-sub intersection {
-    my $a1 = shift;
-    my $b1 = ref ($_[0]) eq ref($a1) ? $_[0] : $a1->new(@_);
-    return _intersection ( 'intersection', $a1, $b1 );
-}
-
-sub intersects {
-    my $a1 = shift;
-    my $b1 = ref ($_[0]) eq ref($a1) ? $_[0] : $a1->new(@_);
-    return _intersection ( 'intersects', $a1, $b1 );
-}
-
-sub intersected_spans {
-    my $a1 = shift;
-    my $b1 = ref ($_[0]) eq ref($a1) ? $_[0] : $a1->new(@_);
-    return _intersection ( 'intersected_spans', $a1, $b1 );
-}
-
-
-sub _intersection {
-    my ( $op, $a1, $b1 ) = @_;
-
-    my $ia;   
-    my ( $a0, $na ) = ( 0, $#{$a1->{list}} );
-    my ( $tmp1, $tmp1a, $tmp2a, $tmp1b, $tmp2b, $i_beg, $i_end, $open_beg, $open_end );
-    my ( $cmp1, $cmp2 );
-    my @a;
-
-    # for-loop optimization (makes little difference)
-    # This was kept for backward compatibility with Date::Set tests
-    my $self = $a1;
-    if ($na < $#{ $b1->{list} })
-    {
-        $na = $#{ $b1->{list} };
-        ($a1, $b1) = ($b1, $a1);
-    }
-    # ---
-
-    B: foreach my $tmp2 ( @{ $b1->{list} } ) {
-        $tmp2a = $tmp2->{a};
-        $tmp2b = $tmp2->{b};
-        A: foreach $ia ($a0 .. $na) {
-            $tmp1 = $a1->{list}[$ia];
-            $tmp1b = $tmp1->{b};
-
-            if ($tmp1b < $tmp2a) {
-                $a0++;
-                next A;
-            }
-            $tmp1a = $tmp1->{a};
-            if ($tmp1a > $tmp2b) {
-                next B;
-            }
-
-            $cmp1 = $tmp1a <=> $tmp2a;
-            if ( $cmp1 < 0 ) {
-                $tmp1a        = $tmp2a;
-                $open_beg     = $tmp2->{open_begin};
-            }
-            elsif ( $cmp1 ) {
-                $open_beg     = $tmp1->{open_begin};
-            }
-            else {
-                $open_beg     = $tmp1->{open_begin} || $tmp2->{open_begin};
-            }
-
-            $cmp2 = $tmp1b <=> $tmp2b;
-            if ( $cmp2 > 0 ) {
-                $tmp1b        = $tmp2b;
-                $open_end     = $tmp2->{open_end};
-            }
-            elsif ( $cmp2 ) {
-                $open_end     = $tmp1->{open_end};
-            }
-            else {
-                $open_end     = $tmp1->{open_end} || $tmp2->{open_end};
-            }
-
-            if ( ( $tmp1a <= $tmp1b ) &&
-                 ( ($tmp1a != $tmp1b) || 
-                   (!$open_beg and !$open_end) ||
-                   ($tmp1a == $inf)   ||               # XXX
-                   ($tmp1a == $neg_inf)
-                 )
-               ) 
-            {
-                if ( $op eq 'intersection' )
-                {
-                    push @a, {
-                        a => $tmp1a, b => $tmp1b, 
-                        open_begin => $open_beg, open_end => $open_end } ;
-                }
-                if ( $op eq 'intersects' )
-                {
-                    return 1;
-                }
-                if ( $op eq 'intersected_spans' )
-                {
-                    push @a, $tmp1;
-                    $a0++;
-                    next A;
-                }
-            }
-        }
-    }
-
-    return 0 if $op eq 'intersects';
-   
-    my $intersection = $self->new();
-    $intersection->{list} = \@a;
-    return $intersection;    
-}
-
-
-sub complement {
-    my $self = shift;
-    if (@_) {
-        my $a1;
-        if (ref ($_[0]) eq ref($self) ) {
-            $a1 = shift;
-        } 
-        else {
-            $a1 = $self->new(@_);  
-        }
-        return $self->intersection( $a1->complement );
-    }
-
-    unless ( @{$self->{list}} ) {
-        return $self->universal_set;
-    }
-    my $complement = $self->empty_set();
-    @{$complement->{list}} = _simple_complement($self->{list}[0]); 
-
-    my $tmp = $self->empty_set();    
-    foreach my $ia (1 .. $#{$self->{list}}) {
-        @{$tmp->{list}} = _simple_complement($self->{list}[$ia]);
-        $complement = $complement->intersection($tmp); 
-    }
-    return $complement;    
-}
-
-
-sub until {
-    my $a1 = shift;
-    my $b1;
-    if (ref ($_[0]) eq ref($a1) ) {
-        $b1 = shift;
-    } 
-    else {
-        $b1 = $a1->new(@_);  
-    }
-    my @b1_min = $b1->min_a;
-    my @a1_max = $a1->max_a;
-
-    unless (defined $b1_min[0]) {
-        return $a1->until($inf);
-    } 
-    unless (defined $a1_max[0]) {
-        return $a1->new(-$inf)->until($b1);
-    }
-
-    my ($ia, $ib, $begin, $end);
-    $ia = 0;
-    $ib = 0;
-
-    my $u = $a1->new;   
-    my $last = -$inf;
-    while ( ($ia <= $#{$a1->{list}}) && ($ib <= $#{$b1->{list}})) {
-        $begin = $a1->{list}[$ia]{a};
-        $end   = $b1->{list}[$ib]{b};
-        if ( $end <= $begin ) {
-            push @{$u->{list}}, {
-                a => $last ,
-                b => $end ,
-                open_begin => 0 ,
-                open_end => 1 };
-            $ib++;
-            $last = $end;
-            next;
-        }
-        push @{$u->{list}}, { 
-            a => $begin , 
-            b => $end ,
-            open_begin => 0 , 
-            open_end => 1 };
-        $ib++;
-        $ia++;
-        $last = $end;
-    }
-    if ($ia <= $#{$a1->{list}}  &&
-        $a1->{list}[$ia]{a} >= $last ) 
-    {
-        push @{$u->{list}}, {
-            a => $a1->{list}[$ia]{a} ,
-            b => $inf ,
-            open_begin => 0 ,
-            open_end => 1 };
-    }
-    return $u;    
-}
-
-sub start_set {
-    return $_[0]->iterate(
-        sub { $_[0]->min }
-    );
-}
-
-
-sub end_set {
-    return $_[0]->iterate(
-        sub { $_[0]->max }
-    );
-}
-
-sub union {
-    my $a1 = shift;
-    my $b1;
-    if (ref ($_[0]) eq ref($a1) ) {
-        $b1 = shift;
-    } 
-    else {
-        $b1 = $a1->new(@_);  
-    }
-    # test for union with empty set
-    if ( $#{ $a1->{list} } < 0 ) {
-        return $b1;
-    }
-    if ( $#{ $b1->{list} } < 0 ) {
-        return $a1;
-    }
-    my @b1_min = $b1->min_a;
-    my @a1_max = $a1->max_a;
-    unless (defined $b1_min[0]) {
-        return $a1;
-    }
-    unless (defined $a1_max[0]) {
-        return $b1;
-    }
-    my ($ia, $ib);
-    $ia = 0;
-    $ib = 0;
-
-    #  size+order matters on speed 
-    $a1 = $a1->new($a1);    # don't modify ourselves 
-    my $b_list = $b1->{list};
-    # -- frequent case - $b1 is after $a1
-    if ($b1_min[0] > $a1_max[0]) {
-        push @{$a1->{list}}, @$b_list;
-        return $a1;
-    }
-
-    my @tmp;
-    my $is_real = !$a1->tolerance && !$b1->tolerance;
-    B: foreach $ib ($ib .. $#{$b_list}) {
-        foreach $ia ($ia .. $#{$a1->{list}}) {
-            @tmp = _simple_union($a1->{list}[$ia], $b_list->[$ib], $a1->{tolerance});
-            if ($#tmp == 0) {
-                    $a1->{list}[$ia] = $tmp[0];
-
-                    while (1) {
-                        last if $ia >= $#{$a1->{list}};    
-                        last unless _simple_intersects ( $a1->{list}[$ia], $a1->{list}[$ia + 1] )
-                            ||    $is_real 
-                               && $a1->{list}[$ia]{b} == $a1->{list}[$ia + 1]{a};
-                        @tmp = _simple_union($a1->{list}[$ia], $a1->{list}[$ia + 1], $a1->{tolerance});
-                        last unless @tmp == 1;
-                        $a1->{list}[$ia] = $tmp[0];
-                        splice( @{$a1->{list}}, $ia + 1, 1 );
-                    }
-                    
-                    next B;
-            }
-            if ($a1->{list}[$ia]{a} >= $b_list->[$ib]{a}) {
-                splice (@{$a1->{list}}, $ia, 0, $b_list->[$ib]);
-                next B;
-            }
-        }
-        push @{$a1->{list}}, $b_list->[$ib];
-    }
-    return $a1;    
-}
-
-
-# there are some ways to process 'contains':
-# A CONTAINS B IF A == ( A UNION B )
-#    - faster
-# A CONTAINS B IF B == ( A INTERSECTION B )
-#    - can backtrack = works for unbounded sets
-sub contains {
-    my $a1 = shift;
-    my $b1 = $a1->union(@_);
-    return ($b1 == $a1) ? 1 : 0;
-}
-
-
-sub copy {
-    my $self = shift;
-    my $copy = $self->empty_set();
-    ## return $copy unless ref($self);   # constructor!
-    foreach my $key (keys %{$self}) {
-        if ( ref( $self->{$key} ) eq 'ARRAY' ) {
-            @{ $copy->{$key} } = @{ $self->{$key} };
-        }
-        else {
-            $copy->{$key} = $self->{$key};
-        }
-    }
-    return $copy;
-}
-
-*clone = \&copy;
-
-
-sub new {
-    my $class = shift;
-    my $self;
-    if ( ref $class ) {
-        $self = bless {
-                    list      => [],
-                    tolerance => $class->{tolerance},
-                    type      => $class->{type},
-                    fixtype   => $class->{fixtype},
-                }, ref($class);
-    }
-    else {
-        $self = bless { 
-                    list      => [],
-                    tolerance => $tolerance ? $tolerance : 0,
-                    type      => $class->type,
-                    fixtype   => $fixtype   ? $fixtype : 0,
-                }, $class;
-    }
-    my ($tmp, $tmp2, $ref);
-    while (@_) {
-        $tmp = shift;
-        $ref = ref($tmp);
-        if ($ref) {
-            if ($ref eq 'ARRAY') {
-                # allows arrays of arrays
-                $tmp = $class->new(@$tmp);  # call new() recursively
-                push @{ $self->{list} }, @{$tmp->{list}};
-                next;
-            }
-            if ($ref eq 'HASH') {
-                push @{ $self->{list} }, $tmp; 
-                next;
-            }
-            if ($tmp->isa(__PACKAGE__)) {
-                push @{ $self->{list} }, @{$tmp->{list}};
-                next;
-            }
-        }
-        if ( @_ ) { 
-            $tmp2 = shift
-        }
-        else {
-            $tmp2 = $tmp
-        }
-        push @{ $self->{list} }, _simple_new($tmp,$tmp2, $self->{type} )
-    }
-    $self;
-}
-
-sub empty_set {
-    $_[0]->new;
-}
-
-sub universal_set {
-    $_[0]->new( NEG_INFINITY, INFINITY );
-}
-
-*minus = \&complement;
-
-*difference = \&complement;
-
-sub symmetric_difference {
-    my $a1 = shift;
-    my $b1;
-    if (ref ($_[0]) eq ref($a1) ) {
-        $b1 = shift;
-    }
-    else {
-        $b1 = $a1->new(@_);
-    }
-
-    return $a1->complement( $b1 )->union(
-           $b1->complement( $a1 ) );
-}
-
-*simmetric_difference = \&symmetric_difference; # bugfix
-
-sub min { 
-    ($_[0]->min_a)[0];
-}
-
-sub min_a { 
-    my $self = $_[0];
-    return @{$self->{min}} if exists $self->{min};
-    return @{$self->{min}} = (undef, 0) unless @{$self->{list}};
-    my $tmp = $self->{list}[0]{a};
-    my $tmp2 = $self->{list}[0]{open_begin} || 0;
-    if ($tmp2 && $self->{tolerance}) {
-        $tmp2 = 0;
-        $tmp += $self->{tolerance};
-    }
-    return @{$self->{min}} = ($tmp, $tmp2);  
-};
-
-sub max { 
-    ($_[0]->max_a)[0];
-}
-
-sub max_a { 
-    my $self = $_[0];
-    return @{$self->{max}} if exists $self->{max};
-    return @{$self->{max}} = (undef, 0) unless @{$self->{list}};
-    my $tmp = $self->{list}[-1]{b};
-    my $tmp2 = $self->{list}[-1]{open_end} || 0;
-    if ($tmp2 && $self->{tolerance}) {
-        $tmp2 = 0;
-        $tmp -= $self->{tolerance};
-    }
-    return @{$self->{max}} = ($tmp, $tmp2);  
-};
-
-sub count {
-    1 + $#{$_[0]->{list}};
-}
-
-sub size { 
-    my $self = $_[0];
-    my $size;  
-    foreach( @{$self->{list}} ) {
-        if ( $size ) {
-            $size += $_->{b} - $_->{a};
-        }
-        else {
-            $size = $_->{b} - $_->{a};
-        }
-        if ( $self->{tolerance} ) {
-            $size += $self->{tolerance} unless $_->{open_end};
-            $size -= $self->{tolerance} if $_->{open_begin};
-            $size -= $self->{tolerance} if $_->{open_end};
-        }
-    }
-    return $size; 
-};
-
-sub span { 
-    my $self = $_[0];
-    my @max = $self->max_a;
-    my @min = $self->min_a;
-    return undef unless defined $min[0] && defined $max[0];
-    my $a1 = $self->new($min[0], $max[0]);
-    $a1->{list}[0]{open_end} = $max[1];
-    $a1->{list}[0]{open_begin} = $min[1];
-    return $a1;
-};
-
-sub spaceship {
-    my ($tmp1, $tmp2, $inverted) = @_;
-    if ($inverted) {
-        ($tmp2, $tmp1) = ($tmp1, $tmp2);
-    }
-    foreach(0 .. $#{$tmp1->{list}}) {
-        my $this  = $tmp1->{list}[$_];
-        if ($_ > $#{ $tmp2->{list} } ) { 
-            return 1; 
-        }
-        my $other = $tmp2->{list}[$_];
-        my $cmp = _simple_spaceship($this, $other);
-        return $cmp if $cmp;   # this != $other;
-    }
-    return $#{ $tmp1->{list} } == $#{ $tmp2->{list} } ? 0 : -1;
-}
-
-sub tolerance {
-    my $self = shift;
-    my $tmp = pop;
-    if (ref($self)) {  
-        # local
-        return $self->{tolerance} unless defined $tmp;
-        $self = $self->copy;
-        $self->{tolerance} = $tmp;
-        delete $self->{max};  # tolerance may change "max"
-
-        $_ = 1;
-        my @tmp;
-        while ( $_ <= $#{$self->{list}} ) {
-            @tmp = Set::Infinite::Basic::_simple_union($self->{list}->[$_],
-                $self->{list}->[$_ - 1],
-                $self->{tolerance});
-            if ($#tmp == 0) {
-                $self->{list}->[$_ - 1] = $tmp[0];
-                splice (@{$self->{list}}, $_, 1);
-            }
-            else {
-                $_ ++;
-            }
-        }
-
-        return $self;
-    }
-    # global
-    $tolerance = $tmp if defined($tmp);
-    return $tolerance;
-}
-
-sub integer { 
-    $_[0]->tolerance (1);
-}
-
-sub real {
-    $_[0]->tolerance (0);
-}
-
-sub as_string {
-    my $self = shift;
-    return $self->separators(6) . 
-           join( $self->separators(5), 
-                 map { $self->_simple_as_string($_) } @{$self->{list}} ) .
-           $self->separators(7),;
-}
-
-
-sub DESTROY {}
-
-1;
-
-__END__
-
-=head1 NAME
-
-Set::Infinite::Basic - Sets of intervals
-6
-=head1 SYNOPSIS
-
-  use Set::Infinite::Basic;
-
-  $set = Set::Infinite::Basic->new(1,2);    # [1..2]
-  print $set->union(5,6);            # [1..2],[5..6]
-
-=head1 DESCRIPTION
-
-Set::Infinite::Basic is a Set Theory module for infinite sets.
-
-It works on reals, integers, and objects.
-
-This module does not support recurrences. Recurrences are implemented in Set::Infinite.
-
-=head1 METHODS
-
-=head2 empty_set
-
-Creates an empty_set.
-
-If called from an existing set, the empty set inherits
-the "type" and "density" characteristics.
-
-=head2 universal_set
-
-Creates a set containing "all" possible elements.
-
-If called from an existing set, the universal set inherits
-the "type" and "density" characteristics.
-
-=head2 until
-
-Extends a set until another:
-
-    0,5,7 -> until 2,6,10
-
-gives
-
-    [0..2), [5..6), [7..10)
-
-Note: this function is still experimental.
-
-=head2 copy
-
-=head2 clone
-
-Makes a new object from the object's data.
-
-=head2 Mode functions:    
-
-    $set = $set->real;
-
-    $set = $set->integer;
-
-=head2 Logic functions:
-
-    $logic = $set->intersects($b);
-
-    $logic = $set->contains($b);
-
-    $logic = $set->is_null;  # also called "is_empty"
-
-=head2 Set functions:
-
-    $set = $set->union($b);    
-
-    $set = $set->intersection($b);
-
-    $set = $set->complement;
-    $set = $set->complement($b);   # can also be called "minus" or "difference"
-
-    $set = $set->symmetric_difference( $b );
-
-    $set = $set->span;   
-
-        result is (min .. max)
-
-=head2 Scalar functions:
-
-    $i = $set->min;
-
-    $i = $set->max;
-
-    $i = $set->size;  
-
-    $i = $set->count;  # number of spans
-
-=head2 Overloaded Perl functions:
-
-    print    
-
-    sort, <=> 
-
-=head2 Global functions:
-
-    separators(@i)
-
-        chooses the interval separators. 
-
-        default are [ ] ( ) '..' ','.
-
-    INFINITY
-
-        returns an 'Infinity' number.
-
-    NEG_INFINITY
-
-        returns a '-Infinity' number.
-
-    iterate ( sub { } )
-
-        Iterates over a subroutine. 
-        Returns the union of partial results.
-
-    first
-
-        In scalar context returns the first interval of a set.
-
-        In list context returns the first interval of a set, and the
-        'tail'.
-
-        Works in unbounded sets
-
-    type($i)
-
-        chooses an object data type. 
-
-        default is none (a normal perl SCALAR).
-
-        examples: 
-
-        type('Math::BigFloat');
-        type('Math::BigInt');
-        type('Set::Infinite::Date');
-            See notes on Set::Infinite::Date below.
-
-    tolerance(0)    defaults to real sets (default)
-    tolerance(1)    defaults to integer sets
-
-    real            defaults to real sets (default)
-
-    integer         defaults to integer sets
-
-=head2 Internal functions:
-
-    $set->fixtype; 
-
-    $set->numeric;
-
-=head1 CAVEATS
-
-    $set = Set::Infinite->new(10,1);
-        Will be interpreted as [1..10]
-
-    $set = Set::Infinite->new(1,2,3,4);
-        Will be interpreted as [1..2],[3..4] instead of [1,2,3,4].
-        You probably want ->new([1],[2],[3],[4]) instead,
-        or maybe ->new(1,4) 
-
-    $set = Set::Infinite->new(1..3);
-        Will be interpreted as [1..2],3 instead of [1,2,3].
-        You probably want ->new(1,3) instead.
-
-=head1 INTERNALS
-
-The internal representation of a I<span> is a hash:
-
-    { a =>   start of span,
-      b =>   end of span,
-      open_begin =>   '0' the span starts in 'a'
-                      '1' the span starts after 'a'
-      open_end =>     '0' the span ends in 'b'
-                      '1' the span ends before 'b'
-    }
-
-For example, this set:
-
-    [100..200),300,(400..infinity)
-
-is represented by the array of hashes:
-
-    list => [
-        { a => 100, b => 200, open_begin => 0, open_end => 1 },
-        { a => 300, b => 300, open_begin => 0, open_end => 0 },
-        { a => 400, b => infinity, open_begin => 0, open_end => 1 },
-    ]
-
-The I<density> of a set is stored in the C<tolerance> variable:
-
-    tolerance => 0;  # the set is made of real numbers.
-
-    tolerance => 1;  # the set is made of integers.
-
-The C<type> variable stores the I<class> of objects that will be stored in the set.
-
-    type => 'DateTime';   # this is a set of DateTime objects
-
-The I<infinity> value is generated by Perl, when it finds a numerical overflow:
-
-    $inf = 100**100**100;
-
-=head1 SEE ALSO
-
-    Set::Infinite
-
-=head1 AUTHOR
-
-    Flavio S. Glock <fglock@gmail.com>
-
-=cut
-
diff --git a/modules/fallback/Set/Infinite/_recurrence.pm b/modules/fallback/Set/Infinite/_recurrence.pm
deleted file mode 100644 (file)
index 376e168..0000000
+++ /dev/null
@@ -1,404 +0,0 @@
-# Copyright (c) 2003 Flavio Soibelmann Glock. All rights reserved.
-# This program is free software; you can redistribute it and/or
-# modify it under the same terms as Perl itself.
-
-package Set::Infinite::_recurrence;
-
-use strict;
-
-use constant INFINITY     =>       100 ** 100 ** 100 ;
-use constant NEG_INFINITY => -1 * (100 ** 100 ** 100);
-
-use vars qw( @ISA $PRETTY_PRINT $max_iterate );
-
-@ISA = qw( Set::Infinite );
-use Set::Infinite 0.5502;
-
-BEGIN {
-    $PRETTY_PRINT = 1;   # enable Set::Infinite debug
-    $max_iterate = 20;
-
-    # TODO: inherit %Set::Infinite::_first / _last 
-    #       in a more "object oriented" way
-
-    $Set::Infinite::_first{_recurrence} = 
-        sub {
-            my $self = $_[0];
-            my ($callback_next, $callback_previous) = @{ $self->{param} };
-            my ($min, $min_open) = $self->{parent}->min_a;
-
-            my ( $min1, $min2 );
-            $min1 = $callback_next->( $min );
-            if ( ! $min_open )
-            {
-                $min2 = $callback_previous->( $min1 );
-                $min1 = $min2 if defined $min2 && $min == $min2;
-            }
-
-            my $start = $callback_next->( $min1 );
-            my $end   = $self->{parent}->max;
-            
-            #print STDERR "set ";
-            #print STDERR $start->datetime
-            #   unless $start == INFINITY;
-            #print STDERR " - " ;
-            #print STDERR $end->datetime 
-            #    unless $end == INFINITY;
-            #print STDERR "\n";
-            
-            return ( $self->new( $min1 ), undef )
-                if $start > $end;
-
-            return ( $self->new( $min1 ),
-                     $self->new( $start, $end )->
-                          _function( '_recurrence', @{ $self->{param} } ) );
-        };
-    $Set::Infinite::_last{_recurrence} =
-        sub {
-            my $self = $_[0];
-            my ($callback_next, $callback_previous) = @{ $self->{param} };
-            my ($max, $max_open) = $self->{parent}->max_a;
-
-            my ( $max1, $max2 );
-            $max1 = $callback_previous->( $max );
-            if ( ! $max_open )
-            {
-                $max2 = $callback_next->( $max1 );
-                $max1 = $max2 if $max == $max2;
-            }
-
-            return ( $self->new( $max1 ),
-                     $self->new( $self->{parent}->min, 
-                                 $callback_previous->( $max1 ) )->
-                          _function( '_recurrence', @{ $self->{param} } ) );
-        };
-}
-
-# $si->_recurrence(
-#     \&callback_next, \&callback_previous )
-#
-# Generates "recurrences" from a callback.
-# These recurrences are simple lists of dates.
-#
-# The recurrence generation is based on an idea from Dave Rolsky.
-#
-
-# use Data::Dumper;
-# use Carp qw(cluck);
-
-sub _recurrence { 
-    my $set = shift;
-    my ( $callback_next, $callback_previous, $delta ) = @_;
-
-    $delta->{count} = 0 unless defined $delta->{delta};
-
-    # warn "reusing delta: ". $delta->{count} if defined $delta->{delta};
-    # warn Dumper( $delta );
-
-    if ( $#{ $set->{list} } != 0 || $set->is_too_complex )
-    {
-        return $set->iterate( 
-            sub { 
-                $_[0]->_recurrence( 
-                    $callback_next, $callback_previous, $delta ) 
-            } );
-    }
-    # $set is a span
-    my $result;
-    if ($set->min != NEG_INFINITY && $set->max != INFINITY)
-    {
-        # print STDERR " finite set\n";
-        my ($min, $min_open) = $set->min_a;
-        my ($max, $max_open) = $set->max_a;
-
-        my ( $min1, $min2 );
-        $min1 = $callback_next->( $min );
-        if ( ! $min_open )
-        {
-                $min2 = $callback_previous->( $min1 );
-                $min1 = $min2 if defined $min2 && $min == $min2;
-        }
-        
-        $result = $set->new();
-
-        # get "delta" - abort if this will take too much time.
-
-        unless ( defined $delta->{max_delta} )
-        {
-          for ( $delta->{count} .. 10 ) 
-          {
-            if ( $max_open )
-            {
-                return $result if $min1 >= $max;
-            }
-            else
-            {
-                return $result if $min1 > $max;
-            }
-            push @{ $result->{list} }, 
-                 { a => $min1, b => $min1, open_begin => 0, open_end => 0 };
-            $min2 = $callback_next->( $min1 );
-            
-            if ( $delta->{delta} ) 
-            {
-                $delta->{delta} += $min2 - $min1;
-            }
-            else
-            {
-                $delta->{delta} = $min2 - $min1;
-            }
-            $delta->{count}++;
-            $min1 = $min2;
-          }
-
-          $delta->{max_delta} = $delta->{delta} * 40;
-        }
-
-        if ( $max < $min + $delta->{max_delta} ) 
-        {
-          for ( 1 .. 200 ) 
-          {
-            if ( $max_open )
-            {
-                return $result if $min1 >= $max;
-            }
-            else
-            {
-                return $result if $min1 > $max;
-            }
-            push @{ $result->{list} }, 
-                 { a => $min1, b => $min1, open_begin => 0, open_end => 0 };
-            $min1 = $callback_next->( $min1 );
-          } 
-        }
-
-        # cluck "give up";
-    }
-
-    # return a "_function", such that we can backtrack later.
-    my $func = $set->_function( '_recurrence', $callback_next, $callback_previous, $delta );
-    
-    # removed - returning $result doesn't help on speed
-    ## return $func->_function2( 'union', $result ) if $result;
-
-    return $func;
-}
-
-sub is_forever
-{
-    $#{ $_[0]->{list} } == 0 &&
-    $_[0]->max == INFINITY &&
-    $_[0]->min == NEG_INFINITY
-}
-
-sub _is_recurrence 
-{
-    exists $_[0]->{method}           && 
-    $_[0]->{method} eq '_recurrence' &&
-    $_[0]->{parent}->is_forever
-}
-
-sub intersection
-{
-    my ($s1, $s2) = (shift,shift);
-
-    if ( exists $s1->{method} && $s1->{method} eq '_recurrence' )
-    {
-        # optimize: recurrence && span
-        return $s1->{parent}->
-            intersection( $s2, @_ )->
-            _recurrence( @{ $s1->{param} } )
-                unless ref($s2) && exists $s2->{method};
-
-        # optimize: recurrence && recurrence
-        if ( $s1->{parent}->is_forever && 
-            ref($s2) && _is_recurrence( $s2 ) )
-        {
-            my ( $next1, $previous1 ) = @{ $s1->{param} };
-            my ( $next2, $previous2 ) = @{ $s2->{param} };
-            return $s1->{parent}->_function( '_recurrence', 
-                  sub {
-                               # intersection of parent 'next' callbacks
-                               my ($n1, $n2);
-                               my $iterate = 0;
-                               $n2 = $next2->( $_[0] );
-                               while(1) { 
-                                   $n1 = $next1->( $previous1->( $n2 ) );
-                                   return $n1 if $n1 == $n2;
-                                   $n2 = $next2->( $previous2->( $n1 ) );
-                                   return if $iterate++ == $max_iterate;
-                               }
-                  },
-                  sub {
-                               # intersection of parent 'previous' callbacks
-                               my ($p1, $p2);
-                               my $iterate = 0;
-                               $p2 = $previous2->( $_[0] );
-                               while(1) { 
-                                   $p1 = $previous1->( $next1->( $p2 ) );
-                                   return $p1 if $p1 == $p2;
-                                   $p2 = $previous2->( $next2->( $p1 ) ); 
-                                   return if $iterate++ == $max_iterate;
-                               }
-                  },
-               );
-        }
-    }
-    return $s1->SUPER::intersection( $s2, @_ );
-}
-
-sub union
-{
-    my ($s1, $s2) = (shift,shift);
-    if ( $s1->_is_recurrence &&
-         ref($s2) && _is_recurrence( $s2 ) )
-    {
-        # optimize: recurrence || recurrence
-        my ( $next1, $previous1 ) = @{ $s1->{param} };
-        my ( $next2, $previous2 ) = @{ $s2->{param} };
-        return $s1->{parent}->_function( '_recurrence',
-                  sub {  # next
-                               my $n1 = $next1->( $_[0] );
-                               my $n2 = $next2->( $_[0] );
-                               return $n1 < $n2 ? $n1 : $n2;
-                  },
-                  sub {  # previous
-                               my $p1 = $previous1->( $_[0] );
-                               my $p2 = $previous2->( $_[0] );
-                               return $p1 > $p2 ? $p1 : $p2;
-                  },
-               );
-    }
-    return $s1->SUPER::union( $s2, @_ );
-}
-
-=head1 NAME
-
-Set::Infinite::_recurrence - Extends Set::Infinite with recurrence functions
-
-=head1 SYNOPSIS
-
-    $recurrence = $base_set->_recurrence ( \&next, \&previous );
-
-=head1 DESCRIPTION
-
-This is an internal class used by the DateTime::Set module.
-The API is subject to change.
-
-It provides all functionality provided by Set::Infinite, plus the ability
-to define recurrences with arbitrary objects, such as dates.
-
-=head1 METHODS
-
-=over 4
-
-=item * _recurrence ( \&next, \&previous )
-
-Creates a recurrence set. The set is defined inside a 'base set'.
-
-   $recurrence = $base_set->_recurrence ( \&next, \&previous );
-
-The recurrence functions take one argument, and return the 'next' or 
-the 'previous' occurence. 
-
-Example: defines the set of all 'integer numbers':
-
-    use strict;
-
-    use Set::Infinite::_recurrence;
-    use POSIX qw(floor);
-
-    # define the recurrence span
-    my $forever = Set::Infinite::_recurrence->new( 
-        Set::Infinite::_recurrence::NEG_INFINITY, 
-        Set::Infinite::_recurrence::INFINITY
-    );
-
-    my $recurrence = $forever->_recurrence(
-        sub {   # next
-                floor( $_[0] + 1 ) 
-            },   
-        sub {   # previous
-                my $tmp = floor( $_[0] ); 
-                $tmp < $_[0] ? $tmp : $_[0] - 1
-            },   
-    );
-
-    print "sample recurrence ",
-          $recurrence->intersection( -5, 5 ), "\n";
-    # sample recurrence -5,-4,-3,-2,-1,0,1,2,3,4,5
-
-    {
-        my $x = 234.567;
-        print "next occurence after $x = ", 
-              $recurrence->{param}[0]->( $x ), "\n";  # 235
-        print "previous occurence before $x = ",
-              $recurrence->{param}[2]->( $x ), "\n";  # 234
-    }
-
-    {
-        my $x = 234;
-        print "next occurence after $x = ",
-              $recurrence->{param}[0]->( $x ), "\n";  # 235
-        print "previous occurence before $x = ",
-              $recurrence->{param}[2]->( $x ), "\n";  # 233
-    }
-
-=item * is_forever
-
-Returns true if the set is a single span, 
-ranging from -Infinity to Infinity.
-
-=item * _is_recurrence
-
-Returns true if the set is an unbounded recurrence, 
-ranging from -Infinity to Infinity.
-
-=back
-
-=head1 CONSTANTS
-
-=over 4
-
-=item * INFINITY
-
-The C<Infinity> value.
-
-=item * NEG_INFINITY
-
-The C<-Infinity> value.
-
-=back
-
-=head1 SUPPORT
-
-Support is offered through the C<datetime@perl.org> mailing list.
-
-Please report bugs using rt.cpan.org
-
-=head1 AUTHOR
-
-Flavio Soibelmann Glock <fglock@pucrs.br>
-
-The recurrence generation algorithm is based on an idea from Dave Rolsky.
-
-=head1 COPYRIGHT
-
-Copyright (c) 2003 Flavio Soibelmann Glock. All rights reserved.
-This program is free software; you can distribute it and/or
-modify it under the same terms as Perl itself.
-
-The full text of the license can be found in the LICENSE file
-included with this module.
-
-=head1 SEE ALSO
-
-Set::Infinite
-
-DateTime::Set
-
-For details on the Perl DateTime Suite project please see
-L<http://datetime.perl.org>.
-
-=cut
-
diff --git a/modules/fallback/Sort/Naturally.pm b/modules/fallback/Sort/Naturally.pm
deleted file mode 100644 (file)
index a62af08..0000000
+++ /dev/null
@@ -1,812 +0,0 @@
-
-require 5;
-package Sort::Naturally;  # Time-stamp: "2004-12-29 18:30:03 AST"
-$VERSION = '1.02';
-@EXPORT = ('nsort', 'ncmp');
-require Exporter;
-@ISA = ('Exporter');
-
-use strict;
-use locale;
-use integer;
-
-#-----------------------------------------------------------------------------
-# constants:
-BEGIN { *DEBUG = sub () {0} unless defined &DEBUG }
-
-use Config ();
-BEGIN {
-  # Make a constant such that if a whole-number string is that long
-  #  or shorter, we KNOW it's treatable as an integer
-  no integer;
-  my $x = length(256 ** $Config::Config{'intsize'} / 2) - 1;
-  die "Crazy intsize: <$Config::Config{'intsize'}>" if $x < 4;
-  eval 'sub MAX_INT_SIZE () {' . $x . '}';
-  die $@ if $@;
-  print "intsize $Config::Config{'intsize'} => MAX_INT_SIZE $x\n" if DEBUG;
-}
-
-sub X_FIRST () {-1}
-sub Y_FIRST () { 1}
-
-my @ORD = ('same', 'swap', 'asis');
-
-#-----------------------------------------------------------------------------
-# For lack of a preprocessor:
-
-my($code, $guts);
-$guts = <<'EOGUTS';  # This is the guts of both ncmp and nsort:
-
-    if($x eq $y) {
-      # trap this expensive case first, and then fall thru to tiebreaker
-      $rv = 0;
-
-    # Convoluted hack to get numerics to sort first, at string start:
-    } elsif($x =~ m/^\d/s) {
-      if($y =~ m/^\d/s) {
-        $rv = 0;    # fall thru to normal comparison for the two numbers
-      } else {
-        $rv = X_FIRST;
-        DEBUG > 1 and print "Numeric-initial $x trumps letter-initial $y\n";
-      }
-    } elsif($y =~ m/^\d/s) {
-      $rv = Y_FIRST;
-      DEBUG > 1 and print "Numeric-initial $y trumps letter-initial $x\n";
-    } else {
-      $rv = 0;
-    }
-    
-    unless($rv) {
-      # Normal case:
-      $rv = 0;
-      DEBUG and print "<$x> and <$y> compared...\n";
-      
-     Consideration:
-      while(length $x and length $y) {
-      
-        DEBUG > 2 and print " <$x> and <$y>...\n";
-        
-        # First, non-numeric comparison:
-        $x2 = ($x =~ m/^(\D+)/s) ? length($1) : 0;
-        $y2 = ($y =~ m/^(\D+)/s) ? length($1) : 0;
-        # Now make x2 the min length of the two:
-        $x2 = $y2 if $x2 > $y2;
-        if($x2) {
-          DEBUG > 1 and printf " <%s> and <%s> lexically for length $x2...\n", 
-            substr($x,0,$x2), substr($y,0,$x2);
-          do {
-           my $i = substr($x,0,$x2);
-           my $j = substr($y,0,$x2);
-           my $sv = $i cmp $j;
-           print "SCREAM! on <$i><$j> -- $sv != $rv \n" unless $rv == $sv;
-           last;
-          }
-          
-          
-           if $rv =
-           # The ''. things here force a copy that seems to work around a 
-           #  mysterious intermittent bug that 'use locale' provokes in
-           #  many versions of Perl.
-                   $cmp
-                   ? $cmp->(substr($x,0,$x2) . '',
-                            substr($y,0,$x2) . '',
-                           )
-                   :
-                   scalar(( substr($x,0,$x2) . '' ) cmp
-                          ( substr($y,0,$x2) . '' )
-                          )
-          ;
-          # otherwise trim and keep going:
-          substr($x,0,$x2) = '';
-          substr($y,0,$x2) = '';
-        }
-        
-        # Now numeric:
-        #  (actually just using $x2 and $y2 as scratch)
-
-        if( $x =~ s/^(\d+)//s ) {
-          $x2 = $1;
-          if( $y =~ s/^(\d+)//s ) {
-            # We have two numbers here.
-            DEBUG > 1 and print " <$x2> and <$1> numerically\n";
-            if(length($x2) < MAX_INT_SIZE and length($1) < MAX_INT_SIZE) {
-              # small numbers: we can compare happily
-              last if $rv = $x2 <=> $1;
-            } else {
-              # ARBITRARILY large integers!
-              
-              # This saves on loss of precision that could happen
-              #  with actual stringification.
-              # Also, I sense that very large numbers aren't too
-              #  terribly common in sort data.
-              
-              # trim leading 0's:
-              ($y2 = $1) =~ s/^0+//s;
-              $x2 =~ s/^0+//s;
-              print "   Treating $x2 and $y2 as bigint\n" if DEBUG;
-
-              no locale; # we want the dumb cmp back.
-              last if $rv = (
-                 # works only for non-negative whole numbers:
-                 length($x2) <=> length($y2)
-                   # the longer the numeral, the larger the value
-                 or $x2 cmp $y2
-                   # between equals, compare lexically!!  amazing but true.
-              );
-            }
-          } else {
-            # X is numeric but Y isn't
-            $rv = Y_FIRST;
-            last;
-          }        
-        } elsif( $y =~ s/^\d+//s ) {  # we don't need to capture the substring
-          $rv = X_FIRST;
-          last;
-        }
-         # else one of them is 0-length.
-
-       # end-while
-      }
-    }
-EOGUTS
-
-sub maker {
-  my $code = $_[0];
-  $code =~ s/~COMPARATOR~/$guts/g || die "Can't find ~COMPARATOR~";
-  eval $code;
-  die $@ if $@;
-}
-
-##############################################################################
-
-maker(<<'EONSORT');
-sub nsort {
-  # get options:
-  my($cmp, $lc);
-  ($cmp,$lc) = @{shift @_} if @_ and ref($_[0]) eq 'ARRAY';
-
-  return @_ unless @_ > 1 or wantarray; # be clever
-  
-  my($x, $x2, $y, $y2, $rv);  # scratch vars
-
-  # We use a Schwartzian xform to memoize the lc'ing and \W-removal
-
-  map $_->[0],
-  sort {
-    if($a->[0] eq $b->[0]) { 0 }   # trap this expensive case
-    else {
-    
-    $x = $a->[1];
-    $y = $b->[1];
-
-~COMPARATOR~
-
-    # Tiebreakers...
-    DEBUG > 1 and print " -<${$a}[0]> cmp <${$b}[0]> is $rv ($ORD[$rv])\n";
-    $rv ||= (length($x) <=> length($y))  # shorter is always first
-        ||  ($cmp and $cmp->($x,$y) || $cmp->($a->[0], $b->[0]))
-        ||  ($x      cmp $y     )
-        ||  ($a->[0] cmp $b->[0])
-    ;
-    
-    DEBUG > 1 and print "  <${$a}[0]> cmp <${$b}[0]> is $rv ($ORD[$rv])\n";
-    $rv;
-  }}
-
-  map {;
-    $x = $lc ? $lc->($_) : lc($_); # x as scratch
-    $x =~ s/\W+//s;
-    [$_, $x];
-  }
-  @_
-}
-EONSORT
-
-#-----------------------------------------------------------------------------
-maker(<<'EONCMP');
-sub ncmp {
-  # The guts are basically the same as above...
-
-  # get options:
-  my($cmp, $lc);
-  ($cmp,$lc) = @{shift @_} if @_ and ref($_[0]) eq 'ARRAY';
-
-  if(@_ == 0) {
-    @_ = ($a, $b); # bit of a hack!
-    DEBUG > 1 and print "Hacking in <$a><$b>\n";
-  } elsif(@_ != 2) {
-    require Carp;
-    Carp::croak("Not enough options to ncmp!");
-  }
-  my($a,$b) = @_;
-  my($x, $x2, $y, $y2, $rv);  # scratch vars
-  
-  DEBUG > 1 and print "ncmp args <$a><$b>\n";
-  if($a eq $b) { # trap this expensive case
-    0;
-  } else {
-    $x = ($lc ? $lc->($a) : lc($a));
-    $x =~ s/\W+//s;
-    $y = ($lc ? $lc->($b) : lc($b));
-    $y =~ s/\W+//s;
-    
-~COMPARATOR~
-
-
-    # Tiebreakers...
-    DEBUG > 1 and print " -<$a> cmp <$b> is $rv ($ORD[$rv])\n";
-    $rv ||= (length($x) <=> length($y))  # shorter is always first
-        ||  ($cmp and $cmp->($x,$y) || $cmp->($a,$b))
-        ||  ($x cmp $y)
-        ||  ($a cmp $b)
-    ;
-    
-    DEBUG > 1 and print "  <$a> cmp <$b> is $rv\n";
-    $rv;
-  }
-}
-EONCMP
-
-# clean up:
-undef $guts;
-undef &maker;
-
-#-----------------------------------------------------------------------------
-1;
-
-############### END OF MAIN SOURCE ###########################################
-__END__
-
-=head1 NAME
-
-Sort::Naturally -- sort lexically, but sort numeral parts numerically
-
-=head1 SYNOPSIS
-
-  @them = nsort(qw(
-   foo12a foo12z foo13a foo 14 9x foo12 fooa foolio Foolio Foo12a
-  ));
-  print join(' ', @them), "\n";
-
-Prints:
-
-  9x 14 foo fooa foolio Foolio foo12 foo12a Foo12a foo12z foo13a
-
-(Or "foo12a" + "Foo12a" and "foolio" + "Foolio" and might be
-switched, depending on your locale.)
-
-=head1 DESCRIPTION
-
-This module exports two functions, C<nsort> and C<ncmp>; they are used
-in implementing my idea of a "natural sorting" algorithm.  Under natural
-sorting, numeric substrings are compared numerically, and other
-word-characters are compared lexically.
-
-This is the way I define natural sorting:
-
-=over
-
-=item *
-
-Non-numeric word-character substrings are sorted lexically,
-case-insensitively: "Foo" comes between "fish" and "fowl".
-
-=item *
-
-Numeric substrings are sorted numerically:
-"100" comes after "20", not before.
-
-=item *
-
-\W substrings (neither words-characters nor digits) are I<ignored>.
-
-=item *
-
-Our use of \w, \d, \D, and \W is locale-sensitive:  Sort::Naturally
-uses a C<use locale> statement.
-
-=item *
-
-When comparing two strings, where a numeric substring in one
-place is I<not> up against a numeric substring in another,
-the non-numeric always comes first.  This is fudged by
-reading pretending that the lack of a number substring has
-the value -1, like so:
-
-  foo       =>  "foo",  -1
-  foobar    =>  "foo",  -1,  "bar"
-  foo13     =>  "foo",  13,
-  foo13xyz  =>  "foo",  13,  "xyz"
-
-That's so that "foo" will come before "foo13", which will come
-before "foobar".
-
-=item *
-
-The start of a string is exceptional: leading non-\W (non-word,
-non-digit)
-components are are ignored, and numbers come I<before> letters.
-
-=item *
-
-I define "numeric substring" just as sequences matching m/\d+/ --
-scientific notation, commas, decimals, etc., are not seen.  If
-your data has thousands separators in numbers
-("20,000 Leagues Under The Sea" or "20.000 lieues sous les mers"),
-consider stripping them before feeding them to C<nsort> or
-C<ncmp>.
-
-=back
-
-=head2 The nsort function
-
-This function takes a list of strings, and returns a copy of the list,
-sorted.
-
-This is what most people will want to use:
-
-  @stuff = nsort(...list...);
-
-When nsort needs to compare non-numeric substrings, it
-uses Perl's C<lc> function in scope of a <use locale>.
-And when nsort needs to lowercase things, it uses Perl's
-C<lc> function in scope of a <use locale>.  If you want nsort
-to use other functions instead, you can specify them in
-an arrayref as the first argument to nsort:
-
-  @stuff = nsort( [
-                    \&string_comparator,   # optional
-                    \&lowercaser_function  # optional
-                  ],
-                  ...list...
-                );
-
-If you want to specify a string comparator but no lowercaser,
-then the options list is C<[\&comparator, '']> or
-C<[\&comparator]>.  If you want to specify no string comparator
-but a lowercaser, then the options list is
-C<['', \&lowercaser]>.
-
-Any comparator you specify is called as
-C<$comparator-E<gt>($left, $right)>,
-and, like a normal Perl C<cmp> replacement, must return
--1, 0, or 1 depending on whether the left argument is stringwise
-less than, equal to, or greater than the right argument.
-
-Any lowercaser function you specify is called as
-C<$lowercased = $lowercaser-E<gt>($original)>.  The routine
-must not modify its C<$_[0]>.
-
-=head2 The ncmp function
-
-Often, when sorting non-string values like this:
-
-   @objects_sorted = sort { $a->tag cmp $b->tag } @objects;
-
-...or even in a Schwartzian transform, like this:
-
-   @strings =
-     map $_->[0]
-     sort { $a->[1] cmp $b->[1] }
-     map { [$_, make_a_sort_key_from($_) ]
-     @_
-   ;
-   
-...you wight want something that replaces not C<sort>, but C<cmp>.
-That's what Sort::Naturally's C<ncmp> function is for.  Call it with
-the syntax C<ncmp($left,$right)> instead of C<$left cmp $right>,
-but otherwise it's a fine replacement:
-
-   @objects_sorted = sort { ncmp($a->tag,$b->tag) } @objects;
-
-   @strings =
-     map $_->[0]
-     sort { ncmp($a->[1], $b->[1]) }
-     map { [$_, make_a_sort_key_from($_) ]
-     @_
-   ;
-
-Just as with C<nsort> can take different a string-comparator
-and/or lowercaser, you can do the same with C<ncmp>, by passing
-an arrayref as the first argument:
-
-  ncmp( [
-          \&string_comparator,   # optional
-          \&lowercaser_function  # optional
-        ],
-        $left, $right
-      )
-
-You might get string comparators from L<Sort::ArbBiLex|Sort::ArbBiLex>.
-
-=head1 NOTES
-
-=over
-
-=item *
-
-This module is not a substitute for
-L<Sort::Versions|Sort::Versions>!  If
-you just need proper version sorting, use I<that!>
-
-=item *
-
-If you need something that works I<sort of> like this module's
-functions, but not quite the same, consider scouting thru this
-module's source code, and adapting what you see.  Besides
-the functions that actually compile in this module, after the POD,
-there's several alternate attempts of mine at natural sorting
-routines, which are not compiled as part of the module, but which you
-might find useful.  They should all be I<working> implementations of
-slightly different algorithms
-(all of them based on Martin Pool's C<nsort>) which I eventually
-discarded in favor of my algorithm.  If you are having to
-naturally-sort I<very large> data sets, and sorting is getting
-ridiculously slow, you might consider trying one of those
-discarded functions -- I have a feeling they might be faster on
-large data sets.  Benchmark them on your data and see.  (Unless
-you I<need> the speed, don't bother.  Hint: substitute C<sort>
-for C<nsort> in your code, and unless your program speeds up
-drastically, it's not the sorting that's slowing things down.
-But if it I<is> C<nsort> that's slowing things down, consider
-just:
-
-      if(@set >= SOME_VERY_BIG_NUMBER) {
-        no locale; # vroom vroom
-        @sorted = sort(@set);  # feh, good enough
-      } elsif(@set >= SOME_BIG_NUMBER) {
-        use locale;
-        @sorted = sort(@set);  # feh, good enough
-      } else {
-        # but keep it pretty for normal cases
-        @sorted = nsort(@set);
-      }
-
-=item *
-
-If you do adapt the routines in this module, email me; I'd
-just be interested in hearing about it.
-
-=item *
-
-Thanks to the EFNet #perl people for encouraging this module,
-especially magister and a-mused.
-
-=back
-
-=head1 COPYRIGHT AND DISCLAIMER
-
-Copyright 2001, Sean M. Burke C<sburke@cpan.org>, all rights
-reserved.  This program is free software; you can redistribute it
-and/or modify it under the same terms as Perl itself.
-
-This program is distributed in the hope that it will be useful, but
-without any warranty; without even the implied warranty of
-merchantability or fitness for a particular purpose.
-
-=head1 AUTHOR
-
-Sean M. Burke C<sburke@cpan.org>
-
-=cut
-
-############   END OF DOCS   ############
-
-############################################################################
-############################################################################
-
-############ BEGIN OLD STUFF ############
-
-# We can't have "use integer;", or else (5 <=> 5.1) comes out "0" !
-
-#-----------------------------------------------------------------------------
-sub nsort {
-  my($cmp, $lc);
-  return @_ if @_ < 2;   # Just to be CLEVER.
-  
-  my($x, $i);  # scratch vars
-  
-  # And now, the GREAT BIG Schwartzian transform:
-  
-  map
-    $_->[0],
-
-  sort {
-    # Uses $i as the index variable, $x as the result.
-    $x = 0;
-    $i = 1;
-    DEBUG and print "\nComparing ", map("{$_}", @$a),
-                 ' : ', map("{$_}", @$b), , "...\n";
-
-    while($i < @$a and $i < @$b) {
-      DEBUG and print "  comparing $i: {$a->[$i]} cmp {$b->[$i]} => ",
-        $a->[$i] cmp $b->[$i], "\n";
-      last if ($x = ($a->[$i] cmp $b->[$i])); # lexicographic
-      ++$i;
-
-      DEBUG and print "  comparing $i: {$a->[$i]} <=> {$b->[$i]} => ",
-        $a->[$i] <=> $b->[$i], "\n";
-      last if ($x = ($a->[$i] <=> $b->[$i])); # numeric
-      ++$i;
-    }
-
-    DEBUG and print "{$a->[0]} : {$b->[0]} is ",
-      $x || (@$a <=> @$b) || 0
-      ,"\n"
-    ;
-    $x || (@$a <=> @$b) || ($a->[0] cmp $b->[0]);
-      # unless we found a result for $x in the while loop,
-      #  use length as a tiebreaker, otherwise use cmp
-      #  on the original string as a fallback tiebreaker.
-  }
-
-  map {
-    my @bit = ($x = defined($_) ? $_ : '');
-    
-    if($x =~ m/^[+-]?(?=\d|\.\d)\d*(?:\.\d*)?(?:[Ee](?:[+-]?\d+))?\z/s) {
-      # It's entirely purely numeric, so treat it specially:
-      push @bit, '', $x;
-    } else {
-      # Consume the string.
-      while(length $x) {
-        push @bit, ($x =~ s/^(\D+)//s) ? lc($1) : '';
-        push @bit, ($x =~ s/^(\d+)//s) ?    $1  :  0;
-      }
-    }
-    DEBUG and print "$bit[0] => ", map("{$_} ", @bit), "\n";
-
-    # End result: [original bit         , (text, number), (text, number), ...]
-    # Minimally:  [0-length original bit,]
-    # Examples:
-    #    ['10'         => ''   ,  10,              ]
-    #    ['fo900'      => 'fo' , 900,              ]
-    #    ['foo10'      => 'foo',  10,              ]
-    #    ['foo9.pl'    => 'foo',   9,   , '.pl', 0 ]
-    #    ['foo32.pl'   => 'foo',  32,   , '.pl', 0 ]
-    #    ['foo325.pl'  => 'foo', 325,   , '.pl', 0 ]
-    #  Yes, always an ODD number of elements.
-    
-    \@bit;
-  }
-  @_;
-}
-
-#-----------------------------------------------------------------------------
-# Same as before, except without the pure-number trap.
-
-sub nsorts {
-  return @_ if @_ < 2;   # Just to be CLEVER.
-  
-  my($x, $i);  # scratch vars
-  
-  # And now, the GREAT BIG Schwartzian transform:
-  
-  map
-    $_->[0],
-
-  sort {
-    # Uses $i as the index variable, $x as the result.
-    $x = 0;
-    $i = 1;
-    DEBUG and print "\nComparing ", map("{$_}", @$a),
-                 ' : ', map("{$_}", @$b), , "...\n";
-
-    while($i < @$a and $i < @$b) {
-      DEBUG and print "  comparing $i: {$a->[$i]} cmp {$b->[$i]} => ",
-        $a->[$i] cmp $b->[$i], "\n";
-      last if ($x = ($a->[$i] cmp $b->[$i])); # lexicographic
-      ++$i;
-
-      DEBUG and print "  comparing $i: {$a->[$i]} <=> {$b->[$i]} => ",
-        $a->[$i] <=> $b->[$i], "\n";
-      last if ($x = ($a->[$i] <=> $b->[$i])); # numeric
-      ++$i;
-    }
-
-    DEBUG and print "{$a->[0]} : {$b->[0]} is ",
-      $x || (@$a <=> @$b) || 0
-      ,"\n"
-    ;
-    $x || (@$a <=> @$b) || ($a->[0] cmp $b->[0]);
-      # unless we found a result for $x in the while loop,
-      #  use length as a tiebreaker, otherwise use cmp
-      #  on the original string as a fallback tiebreaker.
-  }
-
-  map {
-    my @bit = ($x = defined($_) ? $_ : '');
-    
-    while(length $x) {
-      push @bit, ($x =~ s/^(\D+)//s) ? lc($1) : '';
-      push @bit, ($x =~ s/^(\d+)//s) ?    $1  :  0;
-    }
-    DEBUG and print "$bit[0] => ", map("{$_} ", @bit), "\n";
-
-    # End result: [original bit         , (text, number), (text, number), ...]
-    # Minimally:  [0-length original bit,]
-    # Examples:
-    #    ['10'         => ''   ,  10,              ]
-    #    ['fo900'      => 'fo' , 900,              ]
-    #    ['foo10'      => 'foo',  10,              ]
-    #    ['foo9.pl'    => 'foo',   9,   , '.pl', 0 ]
-    #    ['foo32.pl'   => 'foo',  32,   , '.pl', 0 ]
-    #    ['foo325.pl'  => 'foo', 325,   , '.pl', 0 ]
-    #  Yes, always an ODD number of elements.
-    
-    \@bit;
-  }
-  @_;
-}
-
-#-----------------------------------------------------------------------------
-# Same as before, except for the sort-key-making
-
-sub nsort0 {
-  return @_ if @_ < 2;   # Just to be CLEVER.
-  
-  my($x, $i);  # scratch vars
-  
-  # And now, the GREAT BIG Schwartzian transform:
-  
-  map
-    $_->[0],
-
-  sort {
-    # Uses $i as the index variable, $x as the result.
-    $x = 0;
-    $i = 1;
-    DEBUG and print "\nComparing ", map("{$_}", @$a),
-                 ' : ', map("{$_}", @$b), , "...\n";
-
-    while($i < @$a and $i < @$b) {
-      DEBUG and print "  comparing $i: {$a->[$i]} cmp {$b->[$i]} => ",
-        $a->[$i] cmp $b->[$i], "\n";
-      last if ($x = ($a->[$i] cmp $b->[$i])); # lexicographic
-      ++$i;
-
-      DEBUG and print "  comparing $i: {$a->[$i]} <=> {$b->[$i]} => ",
-        $a->[$i] <=> $b->[$i], "\n";
-      last if ($x = ($a->[$i] <=> $b->[$i])); # numeric
-      ++$i;
-    }
-
-    DEBUG and print "{$a->[0]} : {$b->[0]} is ",
-      $x || (@$a <=> @$b) || 0
-      ,"\n"
-    ;
-    $x || (@$a <=> @$b) || ($a->[0] cmp $b->[0]);
-      # unless we found a result for $x in the while loop,
-      #  use length as a tiebreaker, otherwise use cmp
-      #  on the original string as a fallback tiebreaker.
-  }
-
-  map {
-    my @bit = ($x = defined($_) ? $_ : '');
-    
-    if($x =~ m/^[+-]?(?=\d|\.\d)\d*(?:\.\d*)?(?:[Ee](?:[+-]?\d+))?\z/s) {
-      # It's entirely purely numeric, so treat it specially:
-      push @bit, '', $x;
-    } else {
-      # Consume the string.
-      while(length $x) {
-        push @bit, ($x =~ s/^(\D+)//s) ? lc($1) : '';
-        # Secret sauce:
-        if($x =~ s/^(\d+)//s) {
-          if(substr($1,0,1) eq '0' and $1 != 0) {
-            push @bit, $1 / (10 ** length($1));
-          } else {
-            push @bit, $1;
-          }
-        } else {
-          push @bit, 0;
-        }
-      }
-    }
-    DEBUG and print "$bit[0] => ", map("{$_} ", @bit), "\n";
-    
-    \@bit;
-  }
-  @_;
-}
-
-#-----------------------------------------------------------------------------
-# Like nsort0, but WITHOUT pure number handling, and WITH special treatment
-# of pulling off extensions and version numbers.
-
-sub nsortf {
-  return @_ if @_ < 2;   # Just to be CLEVER.
-  
-  my($x, $i);  # scratch vars
-  
-  # And now, the GREAT BIG Schwartzian transform:
-  
-  map
-    $_->[0],
-
-  sort {
-    # Uses $i as the index variable, $x as the result.
-    $x = 0;
-    $i = 3;
-    DEBUG and print "\nComparing ", map("{$_}", @$a),
-                 ' : ', map("{$_}", @$b), , "...\n";
-
-    while($i < @$a and $i < @$b) {
-      DEBUG and print "  comparing $i: {$a->[$i]} cmp {$b->[$i]} => ",
-        $a->[$i] cmp $b->[$i], "\n";
-      last if ($x = ($a->[$i] cmp $b->[$i])); # lexicographic
-      ++$i;
-
-      DEBUG and print "  comparing $i: {$a->[$i]} <=> {$b->[$i]} => ",
-        $a->[$i] <=> $b->[$i], "\n";
-      last if ($x = ($a->[$i] <=> $b->[$i])); # numeric
-      ++$i;
-    }
-
-    DEBUG and print "{$a->[0]} : {$b->[0]} is ",
-      $x || (@$a <=> @$b) || 0
-      ,"\n"
-    ;
-    $x || (@$a     <=> @$b    ) || ($a->[1] cmp $b->[1])
-       || ($a->[2] <=> $b->[2]) || ($a->[0] cmp $b->[0]);
-      # unless we found a result for $x in the while loop,
-      #  use length as a tiebreaker, otherwise use the 
-      #  lc'd extension, otherwise the verison, otherwise use
-      #  the original string as a fallback tiebreaker.
-  }
-
-  map {
-    my @bit = ( ($x = defined($_) ? $_ : ''), '',0 );
-    
-    {
-      # Consume the string.
-      
-      # First, pull off any VAX-style version
-      $bit[2] = $1 if $x =~ s/;(\d+)$//;
-      
-      # Then pull off any apparent extension
-      if( $x !~ m/^\.+$/s and     # don't mangle ".", "..", or "..."
-          $x =~ s/(\.[^\.\;]*)$//sg
-          # We could try to avoid catching all-digit extensions,
-          #  but I think that's getting /too/ clever.
-      ) {
-        $i = $1;
-        if($x =~ m<[^\\\://]$>s) {
-          # We didn't take the whole basename.
-          $bit[1] = lc $i;
-          DEBUG and print "Consuming extension \"$1\"\n";
-        } else {
-          # We DID take the whole basename.  Fix it.
-          $x = $1;  # Repair it.
-        }
-      }
-
-      push @bit, '', -1   if $x =~ m/^\./s;
-       # A hack to make .-initial filenames sort first, regardless of locale.
-       # And -1 is always a sort-firster, since in the code below, there's
-       # no allowance for filenames containing negative numbers: -1.dat
-       # will be read as string '-' followed by number 1.
-
-      while(length $x) {
-        push @bit, ($x =~ s/^(\D+)//s) ? lc($1) : '';
-        # Secret sauce:
-        if($x =~ s/^(\d+)//s) {
-          if(substr($1,0,1) eq '0' and $1 != 0) {
-            push @bit, $1 / (10 ** length($1));
-          } else {
-            push @bit, $1;
-          }
-        } else {
-          push @bit, 0;
-        }
-      }
-    }
-    
-    DEBUG and print "$bit[0] => ", map("{$_} ", @bit), "\n";
-    
-    \@bit;
-  }
-  @_;
-}
-
-# yowza yowza yowza.
-
diff --git a/modules/fallback/String/ShellQuote.pm b/modules/fallback/String/ShellQuote.pm
deleted file mode 100644 (file)
index 0bd0a35..0000000
+++ /dev/null
@@ -1,197 +0,0 @@
-# $Id: ShellQuote.pm,v 1.11 2010-06-11 20:08:57 roderick Exp $
-#
-# Copyright (c) 1997 Roderick Schertler.  All rights reserved.  This
-# program is free software; you can redistribute it and/or modify it
-# under the same terms as Perl itself.
-
-=head1 NAME
-
-String::ShellQuote - quote strings for passing through the shell
-
-=head1 SYNOPSIS
-
-    $string = shell_quote @list;
-    $string = shell_quote_best_effort @list;
-    $string = shell_comment_quote $string;
-
-=head1 DESCRIPTION
-
-This module contains some functions which are useful for quoting strings
-which are going to pass through the shell or a shell-like object.
-
-=over
-
-=cut
-
-package String::ShellQuote;
-
-use strict;
-use vars qw($VERSION @ISA @EXPORT);
-
-require Exporter;
-
-$VERSION    = '1.04';
-@ISA        = qw(Exporter);
-@EXPORT     = qw(shell_quote shell_quote_best_effort shell_comment_quote);
-
-sub croak {
-    require Carp;
-    goto &Carp::croak;
-}
-
-sub _shell_quote_backend {
-    my @in = @_;
-    my @err = ();
-
-    if (0) {
-  require RS::Handy;
-  print RS::Handy::data_dump(\@in);
-    }
-
-    return \@err, '' unless @in;
-
-    my $ret = '';
-    my $saw_non_equal = 0;
-    foreach (@in) {
-  if (!defined $_ or $_ eq '') {
-      $_ = "''";
-      next;
-  }
-
-  if (s/\x00//g) {
-      push @err, "No way to quote string containing null (\\000) bytes";
-  }
-
-      my $escape = 0;
-
-  # = needs quoting when it's the first element (or part of a
-  # series of such elements), as in command position it's a
-  # program-local environment setting
-
-  if (/=/) {
-      if (!$saw_non_equal) {
-        $escape = 1;
-      }
-  }
-  else {
-      $saw_non_equal = 1;
-  }
-
-  if (m|[^\w!%+,\-./:=@^]|) {
-      $escape = 1;
-  }
-
-  if ($escape
-    || (!$saw_non_equal && /=/)) {
-
-      # ' -> '\''
-          s/'/'\\''/g;
-
-      # make multiple ' in a row look simpler
-      # '\'''\'''\'' -> '"'''"'
-          s|((?:'\\''){2,})|q{'"} . (q{'} x (length($1) / 4)) . q{"'}|ge;
-
-      $_ = "'$_'";
-      s/^''//;
-      s/''$//;
-  }
-    }
-    continue {
-  $ret .= "$_ ";
-    }
-
-    chop $ret;
-    return \@err, $ret;
-}
-
-=item B<shell_quote> [I<string>]...
-
-B<shell_quote> quotes strings so they can be passed through the shell.
-Each I<string> is quoted so that the shell will pass it along as a
-single argument and without further interpretation.  If no I<string>s
-are given an empty string is returned.
-
-If any I<string> can't be safely quoted B<shell_quote> will B<croak>.
-
-=cut
-
-sub shell_quote {
-    my ($rerr, $s) = _shell_quote_backend @_;
-
-    if (@$rerr) {
-      my %seen;
-      @$rerr = grep { !$seen{$_}++ } @$rerr;
-  my $s = join '', map { "shell_quote(): $_\n" } @$rerr;
-  chomp $s;
-  croak $s;
-    }
-    return $s;
-}
-
-=item B<shell_quote_best_effort> [I<string>]...
-
-This is like B<shell_quote>, excpet if the string can't be safely quoted
-it does the best it can and returns the result, instead of dying.
-
-=cut
-
-sub shell_quote_best_effort {
-    my ($rerr, $s) = _shell_quote_backend @_;
-
-    return $s;
-}
-
-=item B<shell_comment_quote> [I<string>]
-
-B<shell_comment_quote> quotes the I<string> so that it can safely be
-included in a shell-style comment (the current algorithm is that a sharp
-character is placed after any newlines in the string).
-
-This routine might be changed to accept multiple I<string> arguments
-in the future.  I haven't done this yet because I'm not sure if the
-I<string>s should be joined with blanks ($") or nothing ($,).  Cast
-your vote today!  Be sure to justify your answer.
-
-=cut
-
-sub shell_comment_quote {
-    return '' unless @_;
-    unless (@_ == 1) {
-  croak "Too many arguments to shell_comment_quote "
-            . "(got " . @_ . " expected 1)";
-    }
-    local $_ = shift;
-    s/\n/\n#/g;
-    return $_;
-}
-
-1;
-
-__END__
-
-=back
-
-=head1 EXAMPLES
-
-    $cmd = 'fuser 2>/dev/null ' . shell_quote @files;
-    @pids = split ' ', `$cmd`;
-
-    print CFG "# Configured by: ",
-    shell_comment_quote($ENV{LOGNAME}), "\n";
-
-=head1 BUGS
-
-Only Bourne shell quoting is supported.  I'd like to add other shells
-(particularly cmd.exe), but I'm not familiar with them.  It would be a
-big help if somebody supplied the details.
-
-=head1 AUTHOR
-
-Roderick Schertler <F<roderick@argon.org>>
-
-=head1 SEE ALSO
-
-perl(1).
-
-=cut
-
diff --git a/modules/fallback/parent.pm b/modules/fallback/parent.pm
deleted file mode 100644 (file)
index 435ff25..0000000
+++ /dev/null
@@ -1,136 +0,0 @@
-package parent;
-use strict;
-use vars qw($VERSION);
-$VERSION = '0.221';
-
-sub import {
-    my $class = shift;
-
-    my $inheritor = caller(0);
-
-    if ( @_ and $_[0] eq '-norequire' ) {
-        shift @_;
-    } else {
-        for ( my @filename = @_ ) {
-            if ( $_ eq $inheritor ) {
-                warn "Class '$inheritor' tried to inherit from itself\n";
-            };
-
-            s{::|'}{/}g;
-            require "$_.pm"; # dies if the file is not found
-        }
-    }
-
-    {
-        no strict 'refs';
-        # This is more efficient than push for the new MRO
-        # at least until the new MRO is fixed
-        @{"$inheritor\::ISA"} = (@{"$inheritor\::ISA"} , @_);
-    };
-};
-
-"All your base are belong to us"
-
-__END__
-
-=head1 NAME
-
-parent - Establish an ISA relationship with base classes at compile time
-
-=head1 SYNOPSIS
-
-    package Baz;
-    use parent qw(Foo Bar);
-
-=head1 DESCRIPTION
-
-Allows you to both load one or more modules, while setting up inheritance from
-those modules at the same time.  Mostly similar in effect to
-
-    package Baz;
-    BEGIN {
-        require Foo;
-        require Bar;
-        push @ISA, qw(Foo Bar);
-    }
-
-By default, every base class needs to live in a file of its own.
-If you want to have a subclass and its parent class in the same file, you
-can tell C<parent> not to load any modules by using the C<-norequire> switch:
-
-  package Foo;
-  sub exclaim { "I CAN HAS PERL" }
-
-  package DoesNotLoadFooBar;
-  use parent -norequire, 'Foo', 'Bar';
-  # will not go looking for Foo.pm or Bar.pm
-
-This is equivalent to the following code:
-
-  package Foo;
-  sub exclaim { "I CAN HAS PERL" }
-
-  package DoesNotLoadFooBar;
-  push @DoesNotLoadFooBar::ISA, 'Foo';
-
-This is also helpful for the case where a package lives within
-a differently named file:
-
-  package MyHash;
-  use Tie::Hash;
-  use parent -norequire, 'Tie::StdHash';
-
-This is equivalent to the following code:
-
-  package MyHash;
-  require Tie::Hash;
-  push @ISA, 'Tie::StdHash';
-
-If you want to load a subclass from a file that C<require> would
-not consider an eligible filename (that is, it does not end in
-either C<.pm> or C<.pmc>), use the following code:
-
-  package MySecondPlugin;
-  require './plugins/custom.plugin'; # contains Plugin::Custom
-  use parent -norequire, 'Plugin::Custom';
-
-=head1 DIAGNOSTICS
-
-=over 4
-
-=item Class 'Foo' tried to inherit from itself
-
-Attempting to inherit from yourself generates a warning.
-
-    use Foo;
-    use parent 'Foo';
-
-=back
-
-=head1 HISTORY
-
-This module was forked from L<base> to remove the cruft
-that had accumulated in it.
-
-=head1 CAVEATS
-
-=head1 SEE ALSO
-
-L<base>
-
-=head1 AUTHORS AND CONTRIBUTORS
-
-Rafaël Garcia-Suarez, Bart Lateur, Max Maischein, Anno Siegel, Michael Schwern
-
-=head1 MAINTAINER
-
-Max Maischein C< corion@cpan.org >
-
-Copyright (c) 2007 Max Maischein C<< <corion@cpan.org> >>
-Based on the idea of C<base.pm>, which was introduced with Perl 5.004_04.
-
-=head1 LICENSE
-
-This module is released under the same terms as Perl itself.
-
-=cut
diff --git a/modules/override/Algorithm/CheckDigits/M97_001.pm b/modules/override/Algorithm/CheckDigits/M97_001.pm
new file mode 100644 (file)
index 0000000..39c2d53
--- /dev/null
@@ -0,0 +1,156 @@
+package Algorithm::CheckDigits::M97_001;
+
+use 5.006;
+use strict;
+use warnings;
+use integer;
+
+use version; our $VERSION = 'v1.3.2';
+
+our @ISA = qw(Algorithm::CheckDigits);
+
+sub new {
+       my $proto = shift;
+       my $type  = shift;
+       my $class = ref($proto) || $proto;
+       my $self  = bless({}, $class);
+       $self->{type} = lc($type);
+       return $self;
+} # new()
+
+sub is_valid {
+       my ($self,$number) = @_;
+       if ($number =~ /^(\d{7,8})?(\d\d)$/i) {
+               return $2 eq $self->_compute_checkdigit($1);
+       }
+       return ''
+} # is_valid()
+
+sub complete {
+       my ($self,$number) = @_;
+       if ($number =~ /^(\d{7,8})$/i) {
+               return sprintf('%08d', $number) . $self->_compute_checkdigit($1);
+       }
+       return '';
+} # complete()
+
+sub basenumber {
+       my ($self,$number) = @_;
+       if ($number =~ /^(\d{7,8})(\d\d)$/i) {
+               return sprintf('%08d', $1) if ($2 eq $self->_compute_checkdigit($1));
+       }
+       return '';
+} # basenumber()
+
+sub checkdigit {
+       my ($self,$number) = @_;
+       if ($number =~ /^(\d{7,8})(\d\d)$/i) {
+               return $2 if (uc($2) eq $self->_compute_checkdigit($1));
+       }
+       return '';
+} # checkdigit()
+
+sub _compute_checkdigit {
+       my $self   = shift;
+       my $number = shift;
+
+       if ($number =~ /^\d{7,8}$/i) {
+               return sprintf("%2.2d",97 - ($number % 97));
+       }
+       return -1;
+} # _compute_checkdigit()
+
+# Preloaded methods go here.
+
+1;
+__END__
+
+=head1 NAME
+
+CheckDigits::M97_001 - compute check digits for VAT Registration Number (BE)
+
+=head1 SYNOPSIS
+
+  use Algorithm::CheckDigits;
+
+  $ustid = CheckDigits('ustid_be');
+
+  if ($ustid->is_valid('136695962')) {
+       # do something
+  }
+
+  $cn = $ustid->complete('1366959');
+  # $cn = '136695962'
+
+  $cd = $ustid->checkdigit('136695962');
+  # $cd = '62'
+
+  $bn = $ustid->basenumber('136695962');
+  # $bn = '1366959'
+
+=head1 DESCRIPTION
+
+=head2 ALGORITHM
+
+=over 4
+
+=item 1
+
+The whole number (without checksum) is taken modulo 97.
+
+=item 2
+
+The checksum is difference of the remainder from step 1 to 97.
+
+=back
+
+=head2 METHODS
+
+=over 4
+
+=item is_valid($number)
+
+Returns true only if C<$number> consists solely of numbers and the last digit
+is a valid check digit according to the algorithm given above.
+
+Returns false otherwise,
+
+=item complete($number)
+
+The check digit for C<$number> is computed and concatenated to the end
+of C<$number>.
+
+Returns the complete number with check digit or '' if C<$number>
+does not consist solely of digits and spaces.
+
+=item basenumber($number)
+
+Returns the basenumber of C<$number> if C<$number> has a valid check
+digit.
+
+Return '' otherwise.
+
+=item checkdigit($number)
+
+Returns the checkdigits of C<$number> if C<$number> has a valid check
+digit.
+
+Return '' otherwise.
+
+=back
+
+=head2 EXPORT
+
+None by default.
+
+=head1 AUTHOR
+
+Mathias Weidner, C<< <mamawe@cpan.org> >>
+
+=head1 SEE ALSO
+
+L<perl>,
+L<CheckDigits>,
+F<www.pruefziffernberechnung.de>.
+
+=cut
diff --git a/modules/override/Devel/REPL/Plugin/AutoloadModules.pm b/modules/override/Devel/REPL/Plugin/AutoloadModules.pm
deleted file mode 100644 (file)
index e36ee96..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-package Devel::REPL::Plugin::AutoloadModules;
-
-use Moose::Role;
-use namespace::clean -except => [ 'meta' ];
-use Data::Dumper;
-
-has 'autoloaded' => ( is => 'rw', isa => 'HashRef', default => sub { {} } );
-
-my $re = qr/Runtime error: Can.t locate object method "\w+" via package "\w+" \(perhaps you forgot to load "(\w+)"\?\)/;
-around 'execute' => sub {
-  my $orig = shift;
-  my $self = shift;
-
-  my @re = $self->$orig(@_);                           # original call
-
-  return @re unless defined $re[0] && $re[0] =~ /$re/; # if there is no "perhaps you forgot" error, just return
-  my $module = $1;                                     # save the missing package name
-
-  return @re if $self->autoloaded->{$module};          # if we tried to load it before, give up and return the error
-
-  $self->autoloaded->{$module} = 1;                    # make sure we don't try this again
-  $self->eval("use SL::$module");                      # try to load the missing module
-
-  @re = $self->$orig(@_);                              # try again
-
-  return @re;
-};
-
-1;
diff --git a/modules/override/Devel/REPL/Plugin/PermanentHistory.pm b/modules/override/Devel/REPL/Plugin/PermanentHistory.pm
deleted file mode 100644 (file)
index 3a46b56..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-package Devel::REPL::Plugin::PermanentHistory;
-
-use Moose::Role;
-use namespace::clean -except => [ 'meta' ];
-use File::Slurp;
-use Data::Dumper;
-
-has 'history_file' => ( is => 'rw' );
-
-sub load_history {
-  my $self = shift;
-  my $file = shift;
-
-  $self->history_file( $file );
-
-  return unless $self->history_file && -f $self->history_file;
-
-  my @history =
-    map { chomp; $_ }
-    read_file($self->history_file);
-#  print  Dumper(\@history);
-  $self->history( \@history );
-  $self->term->addhistory($_) for @history;
-}
-
-before 'DESTROY' => sub {
-  my $self = shift;
-
-  return unless $self->history_file;
-
-  write_file $self->history_file,
-    map { $_, $/ }
-    grep $_,
-    grep { !/^quit\b/ }
-    @{ $self->history };
-};
-
-1;
-
old mode 100644 (file)
new mode 100755 (executable)
index 81bf5d3..602c887
@@ -1,12 +1,19 @@
-package PDF::Table;
+#!/usr/bin/env perl
+# vim: softtabstop=4 tabstop=4 shiftwidth=4 ft=perl expandtab smarttab
 
 use 5.006;
 use strict;
 use warnings;
-our $VERSION = '0.9.3';
 
+package PDF::Table;
+
+use Carp;
 use List::Util qw(sum);
 
+our $VERSION = '0.10.1';
+
+print __PACKAGE__.' is version: '.$VERSION.$/ if($ENV{'PDF_TABLE_DEBUG'});
+
 ############################################################
 #
 # new - Constructor
@@ -17,13 +24,70 @@ use List::Util qw(sum);
 #
 ############################################################
 
-sub new {
-  my ($type) = @_;
+sub new
+{
+    my $type = shift(@_);
+    my $class = ref($type) || $type;
+    my $self  = {};
+    bless ($self, $class);
+
+    # Pass all the rest to init for validation and initialisation
+    $self->_init(@_);
+
+    return $self;
+}
+
+sub _init
+{
+    my ($self, $pdf, $page, $data, %options ) = @_;
+
+    # Check and set default values
+    $self->set_defaults();
+
+    # Check and set mandatory params
+    $self->set_pdf($pdf);
+    $self->set_page($page);
+    $self->set_data($data);
+    $self->set_options(\%options);
+
+    return;
+}
+
+sub set_defaults{
+    my $self = shift;
+
+    $self->{'font_size'} = 12;
+}
+
+sub set_pdf{
+    my ($self, $pdf) = @_;
+    $self->{'pdf'} = $pdf;
+}
+
+sub set_page{
+    my ($self, $page) = @_;
+    if ( defined($page) && ref($page) ne 'PDF::API2::Page' ){
 
-  my $class = ref($type) || $type;
-  my $self  = {};
-  bless ($self, $class);
-  return $self;
+        if( ref($self->{'pdf'}) eq 'PDF::API2' ){
+            $self->{'page'} = $self->{'pdf'}->page();
+        } else {
+            carp 'Warning: Page must be a PDF::API2::Page object but it seems to be: '.ref($page).$/;
+            carp 'Error: Cannot set page from passed PDF object either as it is invalid!'.$/;
+        }
+        return;
+    }
+    $self->{'page'} = $page;
+
+}
+
+sub set_data{
+    my ($self, $data) = @_;
+    #TODO: implement
+}
+
+sub set_options{
+    my ($self, $options) = @_;
+    #TODO: implement
 }
 
 ############################################################
@@ -32,261 +96,350 @@ sub new {
 #
 ############################################################
 
-sub text_block {
-  my $self        = shift;
-  my $text_object = shift;
-  my $text        = shift;          # The text to be displayed
-  my %arg         = @_;             # Additional Arguments
-
-  my  ($align, $xpos, $ypos, $xbase, $ybase, $line_width, $wordspace, $endw , $width, $height)
-    = (undef , undef, undef, undef , undef , undef      , undef     , undef , undef , undef  );
-  my @line  = ();          # Temp data array with words on one line
-  my %width = ();          # The width of every unique word in the givven text
-
-  # Try to provide backward compatibility
-  foreach my $key (keys %arg) {
-    my $newkey = $key;
-    if ($newkey =~ s#^-##) {
-      $arg{$newkey} = $arg{$key};
-      delete $arg{$key};
+sub text_block
+{
+    my $self        = shift;
+    my $text_object = shift;
+    my $text        = shift;    # The text to be displayed
+    my %arg         = @_;       # Additional Arguments
+
+    my  ( $align, $xpos, $ypos, $xbase, $ybase, $line_width, $wordspace, $endw , $width, $height) =
+        ( undef , undef, undef, undef , undef , undef      , undef     , undef , undef , undef  );
+    my @line        = ();       # Temp data array with words on one line
+    my %width       = ();       # The width of every unique word in the givven text
+
+    # Try to provide backward compatibility
+    foreach my $key (keys %arg)
+    {
+        my $newkey = $key;
+        if($newkey =~ s#^-##)
+        {
+            $arg{$newkey} = $arg{$key};
+            delete $arg{$key};
+        }
     }
-  }
-  #####
-
-  #---
-  # Lets check mandatory parameters with no default values
-  #---
-  $xbase  = $arg{'x'} || -1;
-  $ybase  = $arg{'y'} || -1;
-  $width  = $arg{'w'} || -1;
-  $height = $arg{'h'} || -1;
-  unless ( $xbase  > 0 ) { print "Error: Left Edge of Block is NOT defined!\n"; return; }
-  unless ( $ybase  > 0 ) { print "Error: Base Line of Block is NOT defined!\n"; return; }
-  unless ( $width  > 0 ) { print "Error: Width of Block is NOT defined!\n";     return; }
-  unless ( $height > 0 ) { print "Error: Height of Block is NOT defined!\n";    return; }
-  # Check if any text to display
-  unless ( defined( $text) and length($text) > 0 ) {
-    print "Warning: No input text found. Trying to add dummy '-' and not to break everything.\n";
-    $text = '-';
-  }
-
-  # Strip any <CR> and Split the text into paragraphs
-  $text          =~ s/\r//g;
-  my @paragraphs =  split(/\n/, $text);
-
-  # Width between lines in pixels
-  my $line_space = defined $arg{'lead'} && $arg{'lead'} > 0 ? $arg{'lead'} : 12;
-
-  # Calculate width of all words
-  my $space_width = $text_object->advancewidth("\x20");
-  my @words       = split(/\s+/, $text);
-  foreach (@words) {
-    next if exists $width{$_};
-    $width{$_} = $text_object->advancewidth($_);
-  }
-
-  my @paragraph       = split(' ', shift(@paragraphs));
-  my $first_line      = 1;
-  my $first_paragraph = 1;
-
-  # Little Init
-  $xpos             = $xbase;
-  $ypos             = $ybase;
-  $ypos             = $ybase + $line_space;
-  my $bottom_border = $ybase - $height;
-  # While we can add another line
-  while ( $ypos >= $bottom_border + $line_space ) {
-    # Is there any text to render ?
-    unless (@paragraph) {
-      # Finish if nothing left
-      last unless scalar @paragraphs;
-      # Else take one line from the text
-      @paragraph  = split(' ', shift( @paragraphs ) );
-
-      $ypos      -= $arg{'parspace'} if $arg{'parspace'};
-      last unless $ypos >= $bottom_border;
+    #####
+
+    #---
+    # Lets check mandatory parameters with no default values
+    #---
+    $xbase  = $arg{'x'} || -1;
+    $ybase  = $arg{'y'} || -1;
+    $width  = $arg{'w'} || -1;
+    $height = $arg{'h'} || -1;
+    unless( $xbase  > 0 ){ carp "Error: Left Edge of Block is NOT defined!\n";  return; }
+    unless( $ybase  > 0 ){ carp "Error: Base Line of Block is NOT defined!\n"; return; }
+    unless( $width  > 0 ){ carp "Error: Width of Block is NOT defined!\n";  return; }
+    unless( $height > 0 ){ carp "Error: Height of Block is NOT defined!\n"; return; }
+    # Check if any text to display
+    unless( defined( $text) and length($text) > 0 )
+    {
+#         carp "Warning: No input text found. Trying to add dummy '-' and not to break everything.\n";
+        $text = ' ';
     }
-    $ypos -= $line_space;
-    $xpos  = $xbase;
 
-    # While there's room on the line, add another word
-    @line       = ();
-    $line_width = 0;
-    if ( $first_line && exists $arg{'hang'} ) {
-      my $hang_width = $text_object->advancewidth($arg{'hang'});
+    # Strip any <CR> and Split the text into paragraphs
+    $text =~ s/\r//g;
+    my @paragraphs  = split(/\n/, $text);
 
-      $text_object->translate( $xpos, $ypos );
-      $text_object->text( $arg{'hang'} );
+    # Width between lines in pixels
+    my $line_space = defined $arg{'lead'} && $arg{'lead'} > 0 ? $arg{'lead'} : 12;
 
-      $xpos          += $hang_width;
-      $line_width    += $hang_width;
-      $arg{'indent'} += $hang_width if $first_paragraph;
-
-    } elsif ( $first_line && exists $arg{'flindent'} && $arg{'flindent'} > 0 ) {
-      $xpos       += $arg{'flindent'};
-      $line_width += $arg{'flindent'};
+    # Calculate width of all words
+    my $space_width = $text_object->advancewidth("\x20");
+    my @words = split(/\s+/, $text);
+    foreach (@words)
+    {
+        next if exists $width{$_};
+        $width{$_} = $text_object->advancewidth($_);
+    }
 
-    } elsif ( $first_paragraph && exists $arg{'fpindent'} && $arg{'fpindent'} > 0 ) {
-      $xpos       += $arg{'fpindent'};
-      $line_width += $arg{'fpindent'};
+    my @paragraph = split(' ', shift(@paragraphs));
+    my $first_line = 1;
+    my $first_paragraph = 1;
+
+    # Little Init
+    $xpos = $xbase;
+    $ypos = $ybase;
+    $ypos = $ybase + $line_space;
+    my $bottom_border = $ypos - $height;
+    # While we can add another line
+    while ( $ypos >= $bottom_border + $line_space )
+    {
+        # Is there any text to render ?
+        unless (@paragraph)
+        {
+            # Finish if nothing left
+            last unless scalar @paragraphs;
+            # Else take one line from the text
+            @paragraph = split(' ', shift( @paragraphs ) );
+
+            $ypos -= $arg{'parspace'} if $arg{'parspace'};
+            last unless $ypos >= $bottom_border;
+        }
+        $ypos -= $line_space;
+        $xpos = $xbase;
+
+        # While there's room on the line, add another word
+        @line = ();
+        $line_width = 0;
+        if( $first_line && exists $arg{'hang'} )
+        {
+            my $hang_width = $text_object->advancewidth($arg{'hang'});
+
+            $text_object->translate( $xpos, $ypos );
+            $text_object->text( $arg{'hang'} );
+
+            $xpos         += $hang_width;
+            $line_width   += $hang_width;
+            $arg{'indent'} += $hang_width if $first_paragraph;
+        }
+        elsif( $first_line && exists $arg{'flindent'} && $arg{'flindent'} > 0 )
+        {
+            $xpos += $arg{'flindent'};
+            $line_width += $arg{'flindent'};
+        }
+        elsif( $first_paragraph && exists $arg{'fpindent'} && $arg{'fpindent'} > 0 )
+        {
+            $xpos += $arg{'fpindent'};
+            $line_width += $arg{'fpindent'};
+        }
+        elsif (exists $arg{'indent'} && $arg{'indent'} > 0 )
+        {
+            $xpos += $arg{'indent'};
+            $line_width += $arg{'indent'};
+        }
 
-    } elsif (exists $arg{'indent'} && $arg{'indent'} > 0 ) {
-      $xpos       += $arg{'indent'};
-      $line_width += $arg{'indent'};
-    }
+        # Lets take from paragraph as many words as we can put into $width - $indent;
+        # Always take at least one word; otherwise we'd end up in an infinite loop.
+        while ( !scalar(@line) || (
+          @paragraph && (
+            $text_object->advancewidth( join("\x20", @line)."\x20" . $paragraph[0]) + $line_width < $width
+          )
+        ))
+        {
+            push(@line, shift(@paragraph));
+        }
+        $line_width += $text_object->advancewidth(join('', @line));
+
+        # calculate the space width
+        if( $arg{'align'} eq 'fulljustify' or ($arg{'align'} eq 'justify' and @paragraph))
+        {
+            @line = split(//,$line[0]) if (scalar(@line) == 1) ;
+            $wordspace = ($width - $line_width) / (scalar(@line) - 1);
+            $align='justify';
+        }
+        else
+        {
+            $align=($arg{'align'} eq 'justify') ? 'left' : $arg{'align'};
+            $wordspace = $space_width;
+        }
+        $line_width += $wordspace * (scalar(@line) - 1);
+
+        if( $align eq 'justify')
+        {
+            foreach my $word (@line)
+            {
+                $text_object->translate( $xpos, $ypos );
+                $text_object->text( $word );
+                $xpos += ($width{$word} + $wordspace) if (@line);
+            }
+            $endw = $width;
+        }
+        else
+        {
+            # calculate the left hand position of the line
+            if( $align eq 'right' )
+            {
+                $xpos += $width - $line_width;
+            }
+            elsif( $align eq 'center' )
+            {
+                $xpos += ( $width / 2 ) - ( $line_width / 2 );
+            }
 
-    # Lets take from paragraph as many words as we can put into $width - $indent;. Always take at least one word; otherwise we'd end up in an infinite loop.
-    while (!scalar(@line) || (@paragraph && ($text_object->advancewidth( join("\x20", @line)."\x20" . $paragraph[0]) + $line_width < $width))) {
-      push(@line, shift(@paragraph));
-    }
-    $line_width += $text_object->advancewidth(join('', @line));
+            # render the line
+            $text_object->translate( $xpos, $ypos );
+            $endw = $text_object->text( join("\x20", @line));
+        }
+        $first_line = 0;
+    }#End of while(
+    unshift(@paragraphs, join(' ',@paragraph)) if scalar(@paragraph);
+    return ($endw, $ypos, join("\n", @paragraphs))
+}
 
-    # calculate the space width
-    if ( $arg{'align'} eq 'fulljustify' or ($arg{'align'} eq 'justify' and @paragraph)) {
-      @line      = split(//,$line[0]) if (scalar(@line) == 1) ;
-      $wordspace = ($width - $line_width) / (scalar(@line) - 1);
-      $align     ='justify';
 
-    } else {
-      $align     = ($arg{'align'} eq 'justify') ? 'left' : $arg{'align'};
-      $wordspace = $space_width;
-    }
-    $line_width += $wordspace * (scalar(@line) - 1);
-
-    if ( $align eq 'justify') {
-      foreach my $word (@line) {
-        $text_object->translate( $xpos, $ypos );
-        $text_object->text( $word );
-        $xpos += ($width{$word} + $wordspace) if (@line);
-      }
-      $endw = $width;
-
-    } else {
-      # calculate the left hand position of the line
-      if ( $align eq 'right' ) {
-        $xpos += $width - $line_width;
-
-      } elsif ( $align eq 'center' ) {
-        $xpos += ( $width / 2 ) - ( $line_width / 2 );
-      }
-
-      # render the line
-      $text_object->translate( $xpos, $ypos );
-      $endw = $text_object->text( join("\x20", @line));
+################################################################
+# table - utility method to build multi-row, multicolumn tables
+################################################################
+sub table
+{
+    my $self    = shift;
+    my $pdf     = shift;
+    my $page    = shift;
+    my $data    = shift;
+    my %arg     = @_;
+
+    #=====================================
+    # Mandatory Arguments Section
+    #=====================================
+    unless($pdf and $page and $data)
+    {
+        carp "Error: Mandatory parameter is missing pdf/page/data object!\n";
+        return;
     }
-    $first_line = 0;
-  }#End of while(
 
-  unshift(@paragraphs, join(' ',@paragraph)) if scalar(@paragraph);
+    # Validate mandatory argument data type
+    croak "Error: Invalid pdf object received."  unless (ref($pdf) eq 'PDF::API2');
+    croak "Error: Invalid page object received." unless (ref($page) eq 'PDF::API2::Page');
+    croak "Error: Invalid data received."        unless ((ref($data) eq 'ARRAY') && scalar(@$data));
+    croak "Error: Missing required settings."    unless (scalar(keys %arg));
+
+    # Validate settings key
+    my %valid_settings_key = (
+        x                     => 1,
+        w                     => 1,
+        start_y               => 1,
+        start_h               => 1,
+        next_y                => 1,
+        next_h                => 1,
+        lead                  => 1,
+        padding               => 1,
+        padding_right         => 1,
+        padding_left          => 1,
+        padding_top           => 1,
+        padding_bottom        => 1,
+        background_color      => 1,
+        background_color_odd  => 1,
+        background_color_even => 1,
+        border                => 1,
+        border_color          => 1,
+        horizontal_borders    => 1,
+        vertical_borders      => 1,
+        font                  => 1,
+        font_size             => 1,
+        font_underline        => 1,
+        font_color            => 1,
+        font_color_even       => 1,
+        font_color_odd        => 1,
+        background_color_odd  => 1,
+        background_color_even => 1,
+        row_height            => 1,
+        new_page_func         => 1,
+        header_props          => 1,
+        column_props          => 1,
+        cell_props            => 1,
+        max_word_length       => 1,
+        cell_render_hook      => 1,
+        default_text          => 1,
+        num_header_rows       => 1,
+    );
+    foreach my $key (keys %arg)
+    {
+        # Provide backward compatibility
+        $arg{$key} = delete $arg{"-$key"} if $key =~ s/^-//;
 
-  return ($endw, $ypos, join("\n", @paragraphs))
-}
+        croak "Error: Invalid setting key '$key' received."
+            unless exists $valid_settings_key{$key};
+    }
 
 
-############################################################
-# table - utility method to build multi-row, multicolumn tables
-############################################################
-sub table {
-  my $self  = shift;
-  my $pdf   = shift;
-  my $page  = shift;
-  my $data  = shift;
-  my %arg   = @_;
-
-  #=====================================
-  # Mandatory Arguments Section
-  #=====================================
-  unless ($pdf and $page and $data) {
-    print "Error: Mandatory parameter is missing pdf/page/data object!\n";
-    return;
-  }
-  # Try to provide backward compatibility
-  foreach my $key (keys %arg) {
-    my $newkey = $key;
-    if ($newkey =~ s#^-##) {
-      $arg{$newkey} = $arg{$key};
-      delete $arg{$key};
+    ######
+    #TODO: Add code for header props compatibility and col_props comp....
+    ######
+    my ( $xbase, $ybase, $width, $height ) = ( undef, undef, undef, undef );
+    # Could be 'int' or 'real' values
+    $xbase  = $arg{'x'      } || -1;
+    $ybase  = $arg{'start_y'} || -1;
+    $width  = $arg{'w'      } || -1;
+    $height = $arg{'start_h'} || -1;
+
+    # Global geometry parameters are also mandatory.
+    unless( $xbase  > 0 ){ carp "Error: Left Edge of Table is NOT defined!\n";  return; }
+    unless( $ybase  > 0 ){ carp "Error: Base Line of Table is NOT defined!\n"; return; }
+    unless( $width  > 0 ){ carp "Error: Width of Table is NOT defined!\n";  return; }
+    unless( $height > 0 ){ carp "Error: Height of Table is NOT defined!\n"; return; }
+
+    # Ensure default values for -next_y and -next_h
+    my $next_y  = $arg{'next_y'} || $arg{'start_y'} || 0;
+    my $next_h  = $arg{'next_h'} || $arg{'start_h'} || 0;
+
+    # Create Text Object
+    my $txt     = $page->text;
+
+    # Set Default Properties
+    my $fnt_name       = $arg{'font'            } || $pdf->corefont('Times',-encode => 'utf8');
+    my $fnt_size       = $arg{'font_size'       } || 12;
+    my $fnt_underline  = $arg{'font_underline'  } || undef; # merely stating undef is the intended default
+    my $max_word_len   = $arg{'max_word_length' } || 20;
+
+    #=====================================
+    # Table Header Section
+    #=====================================
+    # Disable header row into the table
+    my $header_props = undef;
+    my (@header_rows, @header_row_cell_props);
+    # Check if the user enabled it ?
+    if(defined $arg{'header_props'} and ref( $arg{'header_props'}) eq 'HASH')
+    {
+        # Transfer the reference to local variable
+        $header_props = $arg{'header_props'};
+
+        # Check other params and put defaults if needed
+        $header_props->{'repeat'        } = $header_props->{'repeat'        } || 0;
+        $header_props->{'font'          } = $header_props->{'font'          } || $fnt_name;
+        $header_props->{'font_color'    } = $header_props->{'font_color'    } || '#000066';
+        $header_props->{'font_size'     } = $header_props->{'font_size'     } || $fnt_size + 2;
+        $header_props->{'font_underline'} = $header_props->{'font_underline'} || $fnt_underline;
+        $header_props->{'bg_color'      } = $header_props->{'bg_color'      } || '#FFFFAA';
+        $header_props->{'justify'       } = $header_props->{'justify'       };
+        $header_props->{num_header_rows } = $arg{num_header_rows } || 1;
+    }
+    #=====================================
+    # Other Parameters check
+    #=====================================
+    my $lead          = $arg{'lead'          } || $fnt_size;
+    my $pad_left      = $arg{'padding_left'  } || $arg{'padding'} || 0;
+    my $pad_right     = $arg{'padding_right' } || $arg{'padding'} || 0;
+    my $pad_top       = $arg{'padding_top'   } || $arg{'padding'} || 0;
+    my $pad_bot       = $arg{'padding_bottom'} || $arg{'padding'} || 0;
+    my $default_text  = $arg{'default_text'  } // '-';
+    my $line_w        = defined $arg{'border'} ? $arg{'border'} : 1 ;
+    my $horiz_borders = defined $arg{'horizontal_borders'}
+        ? $arg{'horizontal_borders'}
+        : $line_w;
+    my $vert_borders  = defined $arg{'vertical_borders'}
+        ? $arg{'vertical_borders'}
+        : $line_w;
+
+    my $background_color_even   = $arg{'background_color_even'  } || $arg{'background_color'} || undef;
+    my $background_color_odd    = $arg{'background_color_odd'   } || $arg{'background_color'} || undef;
+    my $font_color_even         = $arg{'font_color_even'        } || $arg{'font_color'      } || 'black';
+    my $font_color_odd          = $arg{'font_color_odd'         } || $arg{'font_color'      } || 'black';
+    my $border_color            = $arg{'border_color'           } || 'black';
+
+    my $min_row_h   = $fnt_size + $pad_top + $pad_bot;
+    my $row_h       = defined ($arg{'row_height'})
+                                &&
+                    ($arg{'row_height'} > $min_row_h)
+                                ?
+                     $arg{'row_height'} : $min_row_h;
+
+    my $pg_cnt      = 1;
+    my $cur_y       = $ybase;
+    my $cell_props  = $arg{cell_props} || [];   # per cell properties
+
+    #If there is no valid data array reference warn and return!
+    if(ref $data ne 'ARRAY')
+    {
+        carp "Passed table data is not an ARRAY reference. It's actually a ref to ".ref($data);
+        return ($page,0,$cur_y);
     }
-  }
-  #TODO: Add code for header props compatibility and col_props comp....
-  #####
-  my ( $xbase, $ybase, $width, $height ) = ( undef, undef, undef, undef );
-  # Could be 'int' or 'real' values
-  $xbase  = $arg{'x'    } || -1;
-  $ybase  = $arg{'start_y'} || -1;
-  $width  = $arg{'w'    } || -1;
-  $height = $arg{'start_h'} || -1;
-
-  # Global geometry parameters are also mandatory.
-  unless ( $xbase  > 0 ) { print "Error: Left Edge of Table is NOT defined!\n"; return; }
-  unless ( $ybase  > 0 ) { print "Error: Base Line of Table is NOT defined!\n"; return; }
-  unless ( $width  > 0 ) { print "Error: Width of Table is NOT defined!\n";     return; }
-  unless ( $height > 0 ) { print "Error: Height of Table is NOT defined!\n";    return; }
-
-  # Ensure default values for -next_y and -next_h
-  my $next_y       = $arg{'next_y'} || $arg{'start_y'} || 0;
-  my $next_h       = $arg{'next_h'} || $arg{'start_h'} || 0;
-
-  # Create Text Object
-  my $txt          = $page->text;
-  # Set Default Properties
-  my $fnt_name     = $arg{'font'}            || $pdf->corefont('Times', -encode => 'utf8');
-  my $fnt_size     = $arg{'font_size'}       || 12;
-  my $max_word_len = $arg{'max_word_length'} || 20;
-
-  #=====================================
-  # Table Header Section
-  #=====================================
-  # Disable header row into the table
-  my $header_props;
-  my $num_header_rows = 0;
-  my (@header_rows, @header_row_cell_props);
-  # Check if the user enabled it ?
-  if (defined $arg{'header_props'} and ref( $arg{'header_props'}) eq 'HASH') {
-    # Transfer the reference to local variable
-    $header_props = $arg{'header_props'};
-    # Check other params and put defaults if needed
-    $header_props->{'repeat'}     = $header_props->{'repeat'}     || 0;
-    $header_props->{'font'}       = $header_props->{'font'}       || $fnt_name;
-    $header_props->{'font_color'} = $header_props->{'font_color'} || '#000066';
-    $header_props->{'font_size'}  = $header_props->{'font_size'}  || $fnt_size + 2;
-    $header_props->{'bg_color'}   = $header_props->{'bg_color'}   || '#FFFFAA';
-
-    $num_header_rows              = $arg{'num_header_rows'}       || 1;
-  }
-  #=====================================
-  # Other Parameters check
-  #=====================================
-
-  my $lead      = $arg{'lead'}           || $fnt_size;
-  my $pad_left  = $arg{'padding_left'}   || $arg{'padding'} || 0;
-  my $pad_right = $arg{'padding_right'}  || $arg{'padding'} || 0;
-  my $pad_top   = $arg{'padding_top'}    || $arg{'padding'} || 0;
-  my $pad_bot   = $arg{'padding_bottom'} || $arg{'padding'} || 0;
-  my $pad_w     = $pad_left + $pad_right;
-  my $pad_h     = $pad_top  + $pad_bot  ;
-  my $line_w    = defined $arg{'border'} ? $arg{'border'} : 1 ;
-
-  my $background_color_even = $arg{'background_color_even'} || $arg{'background_color'} || undef;
-  my $background_color_odd  = $arg{'background_color_odd'}  || $arg{'background_color'} || undef;
-  my $font_color_even       = $arg{'font_color_even'}       || $arg{'font_color'}       || 'black';
-  my $font_color_odd        = $arg{'font_color_odd'}        || $arg{'font_color'}       || 'black';
-  my $border_color          = $arg{'border_color'}          || 'black';
-
-  my $min_row_h  = $fnt_size + $pad_top + $pad_bot;
-  my $row_h      = defined ($arg{'row_height'}) && ($arg{'row_height'} > $min_row_h) ? $arg{'row_height'} : $min_row_h;
-
-  my $pg_cnt     = 1;
-  my $cur_y      = $ybase;
-  my $cell_props = $arg{cell_props} || [];   # per cell properties
-  my $row_cnt    = $num_header_rows;
-
-  #If there is valid data array reference use it!
-  if (ref $data eq 'ARRAY') {
+
     # Copy the header row if header is enabled
     if (defined $header_props) {
-      map { push @header_rows,           $$data[$_] }       (0..$num_header_rows - 1);
-      map { push @header_row_cell_props, $$cell_props[$_] } (0..$num_header_rows - 1);
+      map { push @header_rows,           $$data[$_]       } (0..$header_props->{num_header_rows} - 1);
+      map { push @header_row_cell_props, $$cell_props[$_] } (0..$header_props->{num_header_rows} - 1);
     }
     # Determine column widths based on content
 
@@ -296,363 +449,511 @@ sub table {
 
     # An array ref of arrayrefs whose values are
     #  the actual widths of the column/row intersection
-    my $row_props = [];
+    my $row_col_widths = [];
     # An array ref with the widths of the header row
     my @header_row_widths;
 
     # Scalars that hold sum of the maximum and minimum widths of all columns
-    my ( $max_col_w, $min_col_w ) = ( 0,0 );
-    my ( $row, $col_name, $col_fnt_size, $space_w );
+    my ( $max_col_w  , $min_col_w   ) = ( 0,0 );
+    my ( $row, $col_name, $col_fnt_size, $col_fnt_underline, $space_w );
 
-    # Hash that will hold the width of every word from input text
-    my $word_w       = {};
-    my $rows_counter = 0;
+    my $word_widths  = {};
+    my $rows_height  = [];
+    my $first_row    = 1;
 
-    foreach $row ( @{$data} ) {
-      push(@header_row_widths, []) if ($rows_counter < $num_header_rows);
+    for( my $row_idx = 0; $row_idx < scalar(@$data) ; $row_idx++ )
+    {
+        #push @header_row_widths, [] if $row_idx < $header_props->{num_header_rows};
+
+        my $column_widths = []; #holds the width of each column
+        # Init the height for this row
+        $rows_height->[$row_idx] = 0;
+
+        for( my $column_idx = 0; $column_idx < scalar(@{$data->[$row_idx]}) ; $column_idx++ )
+        {
+            # look for font information for this column
+            my ($cell_font, $cell_font_size, $cell_font_underline);
+
+            if( !$row_idx and ref $header_props )
+            {
+                $cell_font           = $header_props->{'font'};
+                $cell_font_size      = $header_props->{'font_size'};
+                $cell_font_underline = $header_props->{'font_underline'};
+            }
 
-      my $column_widths = []; #holds the width of each column
-      for( my $j = 0; $j < scalar(@$row) ; $j++ ) {
-        # look for font information for this column
-        $col_fnt_size   =  $col_props->[$j]->{'font_size'} || $fnt_size;
-        if ( !$rows_counter and ref $header_props) {
-          $txt->font(  $header_props->{'font'}, $header_props->{'font_size'} );
+            # Get the most specific value if none was already set from header_props
+            $cell_font      ||= $cell_props->[$row_idx][$column_idx]->{'font'}
+                            ||  $col_props->[$column_idx]->{'font'}
+                            ||  $fnt_name;
 
-        } elsif ( $col_props->[$j]->{'font'} ) {
-          $txt->font( $col_props->[$j]->{'font'}, $col_fnt_size );
+            $cell_font_size ||= $cell_props->[$row_idx][$column_idx]->{'font_size'}
+                            ||  $col_props->[$column_idx]->{'font_size'}
+                            ||  $fnt_size;
 
-        } else {
-          $txt->font( $fnt_name, $col_fnt_size );
-        }
+            $cell_font_underline ||= $cell_props->[$row_idx][$column_idx]->{'font_underline'}
+                                 ||  $col_props->[$column_idx]->{'font_underline'}
+                                 ||  $fnt_underline;
 
-        # This should fix a bug with very long word like serial numbers etc.
-        # $myone is used because $1 gets out of scope in while condition
-        my $myone;
-        do {
-          $myone = 0;
-          # This RegEx will split any word that is longer than {25} symbols
-          $row->[$j] =~ s#(\b\S{$max_word_len}?)(\S.*?\b)# $1 $2#;
-          $myone = 1 if ( defined $2 );
-        } while( $myone );
-        $row->[$j] =~ s/^\s+//;
-
-        $space_w             = $txt->advancewidth( "\x20" );
-        $column_widths->[$j] = 0;
-        $max_col_w           = 0;
-        $min_col_w           = 0;
-
-        my @words = split( /\s+/, $row->[$j] );
-
-        foreach( @words ) {
-          unless ( exists $word_w->{$_} ) { # Calculate the width of every word and add the space width to it
-            $word_w->{$_} = $txt->advancewidth( $_ ) + $space_w;
-          }
-          $column_widths->[$j] += $word_w->{$_};
-          $min_col_w            = $word_w->{$_} if $word_w->{$_} > $min_col_w;
-          $max_col_w           += $word_w->{$_};
-        }
-        $min_col_w             += $pad_w;
-        $max_col_w             += $pad_w;
-        $column_widths->[$j]   += $pad_w;
+            # Set Font
 
-        # Keep a running total of the overall min and max widths
-        $col_props->[$j]->{min_w} = $col_props->[$j]->{min_w} || 0;
-        $col_props->[$j]->{max_w} = $col_props->[$j]->{max_w} || 0;
+            # Set Font
+            $txt->font( $cell_font, $cell_font_size );
 
-        if ( $min_col_w > $col_props->[$j]->{min_w} ) { # Calculated Minimum Column Width is more than user-defined
-          $col_props->[$j]->{min_w}    = $min_col_w ;
-        }
-        if ( $max_col_w > $col_props->[$j]->{max_w} ) { # Calculated Maximum Column Width is more than user-defined
-          $col_props->[$j]->{max_w}    = $max_col_w ;
-        }
-      }#End of for(my $j....
-      $row_props->[$rows_counter] = $column_widths;
-      # Copy the calculated row properties of header row.
-      if (($rows_counter < $num_header_rows) && $header_props) {
-        push(@header_row_widths, [ @{ $column_widths } ]);
-      }
-      $rows_counter++;
-    }
-    # Calc real column widths and expand table width if needed.
-    my $calc_column_widths;
-    ($calc_column_widths, $width) = $self->CalcColumnWidths( $col_props, $width );
-    my $num_cols  = scalar @{ $calc_column_widths };
-    my $comp_cnt  = 1;
-    $rows_counter = 0;
+            # Set row height to biggest font size from row's cells
+            if( $cell_font_size  > $rows_height->[$row_idx] )
+            {
+                $rows_height->[$row_idx] = $cell_font_size;
+            }
+
+            if (!defined $data->[$row_idx][$column_idx]) {
+              $data->[$row_idx][$column_idx] = ' ';
+            }
 
-    my ( $gfx   , $gfx_bg   , $background_color , $font_color,        );
-    my ( $bot_marg, $table_top_y, $text_start   , $record,  $record_widths  );
+            # This should fix a bug with very long words like serial numbers etc.
+            if( $max_word_len > 0 && $data->[$row_idx][$column_idx])
+            {
+                $data->[$row_idx][$column_idx] =~ s#(\S{$max_word_len})(?=\S)#$1 #g;
+            }
 
-    my $remaining_header_rows = $header_props ? $num_header_rows : 0;
+            # Init cell size limits
+            $space_w                      = $txt->advancewidth( "\x20" );
+            $column_widths->[$column_idx] = 0;
+            $max_col_w                    = 0;
+            $min_col_w                    = 0;
 
-    # Each iteration adds a new page as neccessary
-    while(scalar(@{$data})) {
-      my $page_header;
-      if ($pg_cnt == 1) {
-        $table_top_y = $ybase;
-        $bot_marg = $table_top_y - $height;
+            my @words = split( /\s+/, $data->[$row_idx][$column_idx] );
 
-      } else {
-        if (ref $arg{'new_page_func'}) {
-          $page = &{$arg{'new_page_func'}};
+            foreach( @words )
+            {
+                unless( exists $word_widths->{$_} )
+                {   # Calculate the width of every word and add the space width to it
+                    $word_widths->{$_} = $txt->advancewidth( $_ ) + $space_w;
+                }
 
-        } else {
-          $page = $pdf->page;
-        }
+                $column_widths->[$column_idx] += $word_widths->{$_};
+                $min_col_w                     = $word_widths->{$_} if( $word_widths->{$_} > $min_col_w );
+                $max_col_w                    += $word_widths->{$_};
+            }
 
-        $table_top_y = $next_y;
-        $bot_marg = $table_top_y - $next_h;
+            $min_col_w                    += $pad_left + $pad_right;
+            $max_col_w                    += $pad_left + $pad_right;
+            $column_widths->[$column_idx] += $pad_left + $pad_right;
 
-        if ( ref $header_props and $header_props->{'repeat'}) {
-          foreach my $idx (0 .. $num_header_rows - 1) {
-            unshift @$data,      [ @{ $header_rows[$idx]      } ];
-            unshift @$row_props, [ @{ $header_row_widths[$idx] } ];
-          }
-          $remaining_header_rows = $num_header_rows;
-        }
-      }
-
-      # Check for safety reasons
-      if ( $bot_marg < 0 ) { # This warning should remain i think
-#         print "!!! Warning: !!! Incorrect Table Geometry! Setting bottom margin to end of sheet!\n";
-        $bot_marg = 0;
-      }
-
-      $gfx_bg = $page->gfx;
-      $txt = $page->text;
-      $txt->font($fnt_name, $fnt_size);
-      $gfx = $page->gfx;
-      $gfx->strokecolor($border_color);
-      $gfx->linewidth($line_w);
-
-      # Draw the top line
-      $cur_y = $table_top_y;
-      $gfx->move( $xbase , $cur_y );
-      $gfx->hline($xbase + $width );
-
-      # Each iteration adds a row to the current page until the page is full
-      #  or there are no more rows to add
-      while(scalar(@{$data}) and $cur_y-$row_h > $bot_marg) {
-        # Remove the next item from $data
-        $record = shift @{$data};
-        # Added to resolve infite loop bug with returned undef values
-        for(my $d = 0; $d < scalar(@{$record}) ; $d++) {
-          $record->[$d] = '-' unless ( defined $record->[$d]);
-        }
+            # Keep a running total of the overall min and max widths
+            $col_props->[$column_idx]->{'min_w'} ||= 0;
+            $col_props->[$column_idx]->{'max_w'} ||= 0;
 
-        $record_widths = shift @$row_props;
-        next unless $record;
+            if( $min_col_w > $col_props->[$column_idx]->{'min_w'} )
+            {   # Calculated Minimum Column Width is more than user-defined
+                $col_props->[$column_idx]->{'min_w'} = $min_col_w ;
+            }
+
+            if( $max_col_w > $col_props->[$column_idx]->{'max_w'} )
+            {   # Calculated Maximum Column Width is more than user-defined
+                $col_props->[$column_idx]->{'max_w'} = $max_col_w ;
+            }
+        }#End of for(my $column_idx....
 
-        # Choose colors for this row
-        $background_color = $rows_counter % 2 ? $background_color_even  : $background_color_odd;
-        $font_color     = $rows_counter % 2 ? $font_color_even    : $font_color_odd;
+        $row_col_widths->[$row_idx] = $column_widths;
 
-        if ($remaining_header_rows and ref $header_props) {
-          $background_color = $header_props->{'bg_color'}
+        # Copy the calculated row properties of header row.
+        if (ref $header_props && $row_idx < $header_props->{num_header_rows}) {
+          push @header_row_widths, [ @{ $column_widths } ];
         }
-        $text_start    = $cur_y - $fnt_size - $pad_top;
-        my $cur_x    = $xbase;
-        my $leftovers    = undef; # Reference to text that is returned from textblock()
-        my $do_leftovers = 0;
+    }
+
+    # Calc real column widths and expand table width if needed.
+    my $calc_column_widths;
+    ($calc_column_widths, $width) = CalcColumnWidths( $col_props, $width );
+    my $num_cols = scalar @{ $calc_column_widths };
+
+    # Lets draw what we have!
+    my $row_index    = 0;
+    # Store header row height for later use if headers have to be repeated
+    my @header_row_heights = @$rows_height[0 .. $header_props->{num_header_rows}-1];
 
-        my ($colspan, @vertical_lines);
+    my ( $gfx, $gfx_bg, $background_color, $font_color, $bot_marg, $table_top_y, $text_start);
 
-        # Process every column from current row
-        for( my $j = 0; $j < scalar( @$record); $j++ ) {
-          next unless $col_props->[$j]->{max_w};
-          next unless $col_props->[$j]->{min_w};
-          $leftovers->[$j] = undef;
+    my $remaining_header_rows = $header_props ? $header_props->{num_header_rows} : 0;
 
-          # Choose font color
-          if ( $remaining_header_rows and ref $header_props ) {
-            $txt->fillcolor( $header_props->{'font_color'} );
+    # Each iteration adds a new page as neccessary
+    while(scalar(@{$data}))
+    {
+        my ($page_header);
+        my $columns_number = 0;
+
+        if($pg_cnt == 1)
+        {
+            $table_top_y = $ybase;
+            $bot_marg = $table_top_y - $height;
+
+            # Check for safety reasons
+            if( $bot_marg < 0 )
+            {   # This warning should remain i think
+                #carp "!!! Warning: !!! Incorrect Table Geometry! start_h (${height}) is above start_y (${table_top_y}). Setting bottom margin to end of sheet!\n";
+                $bot_marg = 0;
+            }
 
-          } elsif ( $cell_props->[$row_cnt][$j]{font_color} ) {
-            $txt->fillcolor( $cell_props->[$row_cnt][$j]{font_color} );
+        }
+        else
+        {
+            if(ref $arg{'new_page_func'})
+            {
+                $page = &{$arg{'new_page_func'}};
+            }
+            else
+            {
+                $page = $pdf->page;
+            }
 
-          } elsif ( $col_props->[$j]->{'font_color'} ) {
-            $txt->fillcolor( $col_props->[$j]->{'font_color'} );
+            $table_top_y = $next_y;
+            $bot_marg = $table_top_y - $next_h;
 
-          } else {
-            $txt->fillcolor($font_color);
-          }
+            # Check for safety reasons
+            if( $bot_marg < 0 )
+            {   # This warning should remain i think
+                #carp "!!! Warning: !!! Incorrect Table Geometry! next_y or start_y (${next_y}) is above next_h or start_h (${next_h}). Setting bottom margin to end of sheet!\n";
+                $bot_marg = 0;
+            }
 
-          # Choose font size
-          if ( $remaining_header_rows and ref $header_props ) {
-            $col_fnt_size = $header_props->{'font_size'};
+            if( ref $header_props and $header_props->{'repeat'})
+            {
+                unshift @$data,           @header_rows;
+                unshift @$row_col_widths, @header_row_widths;
+                unshift @$rows_height,    @header_row_heights;
+                $remaining_header_rows = $header_props->{num_header_rows};
+                $first_row = 1;
+            }
+        }
 
-          } elsif ( $col_props->[$j]->{'font_size'} ) {
-            $col_fnt_size = $col_props->[$j]->{'font_size'};
+        $gfx_bg = $page->gfx;
+        $txt = $page->text;
+        $txt->font($fnt_name, $fnt_size);
 
-          } else {
-            $col_fnt_size = $fnt_size;
-          }
+        $cur_y = $table_top_y;
 
-          # Choose font family
-          if ( $remaining_header_rows and ref $header_props ) {
-            $txt->font( $header_props->{'font'}, $header_props->{'font_size'});
+        if ($line_w)
+        {
+            $gfx = $page->gfx;
+            $gfx->strokecolor($border_color);
+            $gfx->linewidth($line_w);
 
-          } elsif ( $col_props->[$j]->{'font'} ) {
-            $txt->font( $col_props->[$j]->{'font'}, $col_fnt_size);
+            # Draw the top line
+            if ($horiz_borders)
+            {
+                $gfx->move( $xbase , $cur_y );
+                $gfx->hline($xbase + $width );
+            }
+        }
+        else
+        {
+            $gfx = undef;
+        }
 
-          } else {
-            $txt->font( $fnt_name, $col_fnt_size);
-          }
-          #TODO: Implement Center text align
-          $col_props->[$j]->{justify} = $col_props->[$j]->{justify} || 'left';
+        # Each iteration adds a row to the current page until the page is full
+        #  or there are no more rows to add
+        # Row_Loop
+        while(scalar(@{$data}) and $cur_y-$row_h > $bot_marg)
+        {
+            # Remove the next item from $data
+            my $record = shift @{$data};
 
-          my $this_width;
-          if (!$remaining_header_rows && $cell_props->[$row_cnt]->[$j]->{colspan}) {
-            $colspan = $cell_props->[$row_cnt]->[$j]->{colspan};
+            # Get max columns number to know later how many vertical lines to draw
+            $columns_number = scalar(@$record)
+                if scalar(@$record) > $columns_number;
 
-          } elsif ($remaining_header_rows && $header_row_cell_props[$num_header_rows - $remaining_header_rows]->[$j]->{colspan}) {
-            $colspan = $header_row_cell_props[$num_header_rows - $remaining_header_rows]->[$j]->{colspan};
+            # Get the next set of row related settings
+            # Row Height
+            my $pre_calculated_row_height = shift @$rows_height;
 
-          }
+            # Row cell widths
+            my $record_widths = shift @$row_col_widths;
 
-          if ($colspan) {
-            $colspan     = $num_cols - $j if (-1 == $colspan);
-            my $last_idx = $j + $colspan - 1;
-            $this_width  = sum @{ $calc_column_widths }[$j..$last_idx];
+            # Row coloumn props - TODO in another commit
 
-          } else {
-            $this_width = $calc_column_widths->[$j];
-          }
+            # Row cell props - TODO in another commit
 
-          # If the content is wider than the specified width, we need to add the text as a text block
-          if ($record->[$j] !~ m#(.\n.)# and  $record_widths->[$j] and ($record_widths->[$j] < $this_width)) {
-            my $space = $pad_left;
-            if ($col_props->[$j]->{justify} eq 'right') {
-              $space = $this_width -($txt->advancewidth($record->[$j]) + $pad_right);
-            }
-            $txt->translate( $cur_x + $space, $text_start );
-            $txt->text( $record->[$j] );
-          } else { # Otherwise just use the $page->text() method
-            my($width_of_last_line, $ypos_of_last_line, $left_over_text) =
-              $self->text_block($txt,
-                                $record->[$j],
-                                'x'     => $cur_x + $pad_left,
-                                'y'     => $text_start,
-                                'w'     => $this_width - $pad_w,
-                                'h'     => $cur_y - $bot_marg - $pad_top - $pad_bot,
-                                'align' => $col_props->[$j]->{justify},
-                                'lead'  => $lead
-              );
-            # Desi - Removed $lead because of fixed incorrect ypos bug in text_block
-            my $this_row_h = $cur_y - ( $ypos_of_last_line - $pad_bot );
-            $row_h = $this_row_h if $this_row_h > $row_h;
-            if ( $left_over_text ) {
-              $leftovers->[$j] = $left_over_text;
-              $do_leftovers    = 1;
+            # Added to resolve infite loop bug with returned undef values
+            for(my $d = 0; $d < scalar(@{$record}) ; $d++)
+            {
+                $record->[$d] = ' ' unless( defined $record->[$d]);
             }
-          }
-          $cur_x += $calc_column_widths->[$j];
 
-          push @vertical_lines, (!$colspan || (1 >= $colspan)) ? 1 : 0;
-          $colspan-- if ($colspan);
-        }
+            # Choose colors for this row
+            $background_color = ($row_index - $header_props->{num_header_rows}) % 2 ? $background_color_even  : $background_color_odd;
+            $font_color       = ($row_index - $header_props->{num_header_rows}) % 2 ? $font_color_even        : $font_color_odd;
 
-        if ( $do_leftovers ) {
-          unshift @$data, $leftovers;
-          unshift @$row_props, $record_widths;
-          $rows_counter--;
-        }
+            #Determine current row height
+            my $current_row_height = $pad_top + $pre_calculated_row_height + $pad_bot;
 
-        # Draw cell bgcolor
-        # This has to be separately from the text loop
-        #  because we do not know the final height of the cell until all text has been drawn
-        $cur_x = $xbase;
-        for(my $j =0;$j < scalar(@$record);$j++) {
-          if (  $cell_props->[$row_cnt][$j]->{'background_color'} ||
-                $col_props->[$j]->{'background_color'} ||
-                $background_color ) {
-            $gfx_bg->rect( $cur_x, $cur_y-$row_h, $calc_column_widths->[$j], $row_h);
-            if ( $cell_props->[$row_cnt][$j]->{'background_color'} && !$remaining_header_rows ) {
-              $gfx_bg->fillcolor($cell_props->[$row_cnt][$j]->{'background_color'});
-
-            } elsif ( $col_props->[$j]->{'background_color'} && !$remaining_header_rows  ) {
-              $gfx_bg->fillcolor($col_props->[$j]->{'background_color'});
+            # $row_h is the calculated global user requested row height.
+            # It will be honored, only if it has bigger value than the calculated one.
+            # TODO: It's questionable if padding should be inclided in this calculation or not
+            if($current_row_height < $row_h){
+                $current_row_height = $row_h;
+            }
+
+            # Define the font y base position for this line.
+            $text_start      = $cur_y - ($current_row_height - $pad_bot);
+
+            my $cur_x        = $xbase;
+            my $leftovers    = undef;   # Reference to text that is returned from textblock()
+            my $do_leftovers = 0;
+            my ($colspan, @vertical_lines);
+
+            # Process every cell(column) from current row
+            for( my $column_idx = 0; $column_idx < scalar( @$record); $column_idx++ )
+            {
+                next unless $col_props->[$column_idx]->{'max_w'};
+                next unless $col_props->[$column_idx]->{'min_w'};
+                $leftovers->[$column_idx] = undef;
+
+                # look for font information for this cell
+                my ($cell_font, $cell_font_size, $cell_font_color, $cell_font_underline, $justify);
+
+                if( $remaining_header_rows and ref $header_props)
+                {
+                    $cell_font           = $header_props->{'font'};
+                    $cell_font_size      = $header_props->{'font_size'};
+                    $cell_font_color     = $header_props->{'font_color'};
+                    $cell_font_underline = $header_props->{'font_underline'};
+                    $justify             = $header_props->{'justify'};
+                }
+
+                # Get the most specific value if none was already set from header_props
+                $cell_font       ||= $cell_props->[$row_index][$column_idx]->{'font'}
+                                 ||  $col_props->[$column_idx]->{'font'}
+                                 ||  $fnt_name;
+
+                $cell_font_size  ||= $cell_props->[$row_index][$column_idx]->{'font_size'}
+                                 ||  $col_props->[$column_idx]->{'font_size'}
+                                 ||  $fnt_size;
+
+                $cell_font_color ||= $cell_props->[$row_index][$column_idx]->{'font_color'}
+                                 ||  $col_props->[$column_idx]->{'font_color'}
+                                 ||  $font_color;
+
+                $cell_font_underline ||= $cell_props->[$row_index][$column_idx]->{'font_underline'}
+                                     ||  $col_props->[$column_idx]->{'font_underline'}
+                                     ||  $fnt_underline;
+
+
+                $justify         ||= $cell_props->[$row_index][$column_idx]->{'justify'}
+                                 ||  $col_props->[$column_idx]->{'justify'}
+                                 ||  $arg{'justify'}
+                                 ||  'left';
+
+                # Init cell font object
+                $txt->font( $cell_font, $cell_font_size );
+                $txt->fillcolor($cell_font_color);
+
+                # Added to resolve infite loop bug with returned undef values
+                $record->[$column_idx] //= $cell_props->[$row_index][$column_idx]->{'default_text'}
+                                       //  $col_props->[$column_idx]->{'default_text'}
+                                       //  $default_text;
+
+                my $this_width;
+                if (!$remaining_header_rows && $cell_props->[$row_index + $header_props->{num_header_rows}][$column_idx]->{colspan}) {
+                    $colspan = $cell_props->[$row_index + $header_props->{num_header_rows}][$column_idx]->{colspan};
+                } elsif ($remaining_header_rows && ($header_row_cell_props[$header_props->{num_header_rows} - $remaining_header_rows][$column_idx]->{colspan})) {
+                    $colspan = $header_row_cell_props[$header_props->{num_header_rows} - $remaining_header_rows][$column_idx]->{colspan};
+                }
+
+                if ($colspan) {
+                    $colspan     = $num_cols - $column_idx if (-1 == $colspan);
+                    my $last_idx = $column_idx + $colspan - 1;
+                    $this_width  = sum @{ $calc_column_widths }[$column_idx..$last_idx];
+                } else {
+                    $this_width = $calc_column_widths->[$column_idx];
+                }
+
+                # If the content is wider than the specified width, we need to add the text as a text block
+                if( $record->[$column_idx] !~ m/(.\n.)/ and
+                    $record_widths->[$column_idx] and
+                    $record_widths->[$column_idx] <= $this_width
+                ){
+                    my $space = $pad_left;
+                    if ($justify eq 'right')
+                    {
+                        $space = $this_width -($txt->advancewidth($record->[$column_idx]) + $pad_right);
+                    }
+                    elsif ($justify eq 'center')
+                    {
+                        $space = ($this_width - $txt->advancewidth($record->[$column_idx])) / 2;
+                    }
+                    $txt->translate( $cur_x + $space, $text_start );
+                    my %text_options;
+                    $text_options{'-underline'} = $cell_font_underline if $cell_font_underline;
+                    $txt->text( $record->[$column_idx], %text_options );
+                }
+                # Otherwise just use the $page->text() method
+                else
+                {
+                    my ($width_of_last_line, $ypos_of_last_line, $left_over_text) = $self->text_block(
+                        $txt,
+                        $record->[$column_idx],
+                        x        => $cur_x + $pad_left,
+                        y        => $text_start,
+                        w        => $this_width - $pad_left - $pad_right,
+                        h        => $cur_y - $bot_marg - $pad_top - $pad_bot,
+                        align    => $justify,
+                        lead     => $lead
+                    );
+                    # Desi - Removed $lead because of fixed incorrect ypos bug in text_block
+                    my  $current_cell_height = $cur_y - $ypos_of_last_line + $pad_bot;
+                    if( $current_cell_height > $current_row_height )
+                    {
+                        $current_row_height = $current_cell_height;
+                    }
+
+                    if( $left_over_text )
+                    {
+                        $leftovers->[$column_idx] = $left_over_text;
+                        $do_leftovers = 1;
+                    }
+                }
+
+                # Hook to pass coordinates back - http://www.perlmonks.org/?node_id=754777
+                if (ref $arg{cell_render_hook} eq 'CODE') {
+                   $arg{cell_render_hook}->(
+                                            $page,
+                                            $first_row,
+                                            $row_index,
+                                            $column_idx,
+                                            $cur_x,
+                                            $cur_y-$row_h,
+                                            $calc_column_widths->[$column_idx],
+                                            $row_h
+                                           );
+                }
+
+                $cur_x += $calc_column_widths->[$column_idx];
+
+                push @vertical_lines, (!$colspan || (1 >= $colspan)) ? 1 : 0;
+                $colspan-- if $colspan;
+            }
+            if( $do_leftovers )
+            {
+                unshift @$data, $leftovers;
+                unshift @$row_col_widths, $record_widths;
+                unshift @$rows_height, $pre_calculated_row_height;
+            }
+
+            # Draw cell bgcolor
+            # This has to be separately from the text loop
+            #  because we do not know the final height of the cell until all text has been drawn
+            $cur_x = $xbase;
+            for(my $column_idx = 0 ; $column_idx < scalar(@$record) ; $column_idx++)
+            {
+                my $cell_bg_color;
+
+                if( $remaining_header_rows and ref $header_props)
+                {                                  #Compatibility                 Consistency with other props
+                    $cell_bg_color = $header_props->{'bg_color'} || $header_props->{'background_color'};
+                }
+
+                # Get the most specific value if none was already set from header_props
+                $cell_bg_color ||= $cell_props->[$row_index + $header_props->{num_header_rows}][$column_idx]->{'background_color'}
+                               ||  $col_props->[$column_idx]->{'background_color'}
+                               ||  $background_color;
+
+                if ($cell_bg_color)
+                {
+                    $gfx_bg->rect( $cur_x, $cur_y-$current_row_height, $calc_column_widths->[$column_idx], $current_row_height);
+                    $gfx_bg->fillcolor($cell_bg_color);
+                    $gfx_bg->fill();
+                }
+                $cur_x += $calc_column_widths->[$column_idx];
+
+                if ($line_w && $vertical_lines[$column_idx] && ($column_idx != (scalar(@{ $record }) - 1))) {
+                    $gfx->move($cur_x, $cur_y);
+                    $gfx->vline($cur_y - $current_row_height);
+                    $gfx->fillcolor($border_color);
+                }
+            }#End of for(my $column_idx....
+
+            $cur_y -= $current_row_height;
+            if ($gfx && $horiz_borders)
+            {
+                $gfx->move(  $xbase , $cur_y );
+                $gfx->hline( $xbase + $width );
+            }
 
+            $first_row = 0;
+            if ($remaining_header_rows) {
+              $remaining_header_rows--;
             } else {
-              $gfx_bg->fillcolor($background_color);
+              $row_index++ unless $do_leftovers;
             }
-            $gfx_bg->fill();
-          }
-
-          $cur_x += $calc_column_widths->[$j];
-
-          if ($line_w && $vertical_lines[$j] && ($j != (scalar(@{ $record }) - 1))) {
-            $gfx->move($cur_x, $cur_y);
-            $gfx->vline($cur_y - $row_h);
-            $gfx->fillcolor($border_color);
-          }
-        }#End of for(my $j....
-
-        $cur_y -= $row_h;
-        $row_h  = $min_row_h;
-        $gfx->move(  $xbase , $cur_y );
-        $gfx->hline( $xbase + $width );
-        $rows_counter++;
-        if ($remaining_header_rows) {
-          $remaining_header_rows--;
-        } else {
-          $row_cnt++ unless $do_leftovers;
+        }# End of Row_Loop
+
+        if ($gfx)
+        {
+            # Draw vertical lines
+            if ($vert_borders)
+            {
+                $gfx->move(  $xbase, $table_top_y);
+                $gfx->vline( $cur_y );
+                $gfx->move($xbase + sum(@{ $calc_column_widths }[0..$num_cols - 1]), $table_top_y);
+                $gfx->vline( $cur_y );
+            }
+
+            # ACTUALLY draw all the lines
+            $gfx->fillcolor( $border_color);
+            $gfx->stroke;
         }
-      }# End of while(scalar(@{$data}) and $cur_y-$row_h > $bot_marg)
-
-      # Draw vertical lines
-      if ($line_w) {
-        $gfx->move($xbase, $table_top_y);
-        $gfx->vline($cur_y);
-        $gfx->move($xbase + sum(@{ $calc_column_widths }[0..$num_cols - 1]), $table_top_y);
-        $gfx->vline($cur_y);
-        $gfx->fillcolor($border_color);
-        $gfx->stroke();
-      }
-      $pg_cnt++;
+        $pg_cnt++;
     }# End of while(scalar(@{$data}))
-  }# End of if (ref $data eq 'ARRAY')
 
-  return ($page,--$pg_cnt,$cur_y);
+    return ($page,--$pg_cnt,$cur_y);
 }
 
 
 # calculate the column widths
-sub CalcColumnWidths {
-  my $self    = shift;
-  my $col_props   = shift;
-  my $avail_width = shift;
-  my $min_width   = 0;
-
-  my $calc_widths ;
-  for(my $j = 0; $j < scalar( @$col_props); $j++) {
-    $min_width += $col_props->[$j]->{min_w};
-  }
-
-  # I think this is the optimal variant when good view can be guaranateed
-  if ($avail_width < $min_width) {
-#     print "!!! Warning !!!\n Calculated Mininal width($min_width) > Table width($avail_width).\n",
-#       ' Expanding table width to:',int($min_width)+1,' but this could lead to unexpected results.',"\n",
-#       ' Possible solutions:',"\n",
-#       '  0)Increase table width.',"\n",
-#       '  1)Decrease font size.',"\n",
-#       '  2)Choose a more narrow font.',"\n",
-#       '  3)Decrease "max_word_length" parameter.',"\n",
-#       '  4)Rotate page to landscape(if it is portrait).',"\n",
-#       '  5)Use larger paper size.',"\n",
-#       '!!! --------- !!!',"\n";
-    $avail_width = int( $min_width) + 1;
-
-  }
-
-  my $span = 0;
-  # Calculate how much can be added to every column to fit the available width
-  $span = ($avail_width - $min_width) / scalar( @$col_props);
-  for (my $j = 0; $j < scalar(@$col_props); $j++ ) {
-    $calc_widths->[$j] = $col_props->[$j]->{min_w} + $span;
-  }
-
-  return ($calc_widths,$avail_width);
+sub CalcColumnWidths
+{
+    my $col_props   = shift;
+    my $avail_width = shift;
+    my $min_width   = 0;
+
+    my $calc_widths ;
+
+    for(my $j = 0; $j < scalar( @$col_props); $j++)
+    {
+        $min_width += $col_props->[$j]->{min_w} || 0;
+    }
+
+    # I think this is the optimal variant when good view can be guaranateed
+    if($avail_width < $min_width)
+    {
+        carp "!!! Warning !!!\n Calculated Mininal width($min_width) > Table width($avail_width).\n",
+            ' Expanding table width to:',int($min_width)+1,' but this could lead to unexpected results.',"\n",
+            ' Possible solutions:',"\n",
+            '  0)Increase table width.',"\n",
+            '  1)Decrease font size.',"\n",
+            '  2)Choose a more narrow font.',"\n",
+            '  3)Decrease "max_word_length" parameter.',"\n",
+            '  4)Rotate page to landscape(if it is portrait).',"\n",
+            '  5)Use larger paper size.',"\n",
+            '!!! --------- !!!',"\n";
+        $avail_width = int( $min_width) + 1;
+
+    }
+
+    # Calculate how much can be added to every column to fit the available width.
+    for(my $j = 0; $j < scalar(@$col_props); $j++ )
+    {
+        $calc_widths->[$j] = $col_props->[$j]->{min_w} || 0;;
+    }
+
+    my $span = 0;
+    # Calculate how much can be added to every column to fit the available width
+    $span = ($avail_width - $min_width) / scalar( @$col_props);
+    for (my $j = 0; $j < scalar(@$col_props); $j++ ) {
+      $calc_widths->[$j] = $col_props->[$j]->{min_w} + $span;
+    }
+
+    return ($calc_widths,$avail_width);
 }
 1;
 
@@ -693,11 +994,11 @@ PDF::Table - A utility class for building table layouts in a PDF::API2 object.
      $some_data,
      x => $left_edge_of_table,
      w => 495,
-     start_y => 750,
-     next_y  => 700,
+     start_y => 500,
      start_h => 300,
-     next_h  => 500,
      # some optional params
+     next_y  => 750,
+     next_h  => 500,
      padding => 5,
      padding_right => 10,
      background_color_odd  => "gray",
@@ -716,105 +1017,329 @@ For a complete working example or initial script look into distribution`s 'examp
 =head1 DESCRIPTION
 
 This class is a utility for use with the PDF::API2 module from CPAN.
-It can be used to display text data in a table layout within the PDF.
-The text data must be in a 2d array (such as returned by a DBI statement handle fetchall_arrayref() call).
+It can be used to display text data in a table layout within a PDF.
+The text data must be in a 2D array (such as returned by a DBI statement handle fetchall_arrayref() call).
 The PDF::Table will automatically add as many new pages as necessary to display all of the data.
 Various layout properties, such as font, font size, and cell padding and background color can be specified for each column and/or for even/odd rows.
 Also a (non)repeated header row with different layout properties can be specified.
 
-See the METHODS section for complete documentation of every parameter.
+See the L</METHODS> section for complete documentation of every parameter.
 
-=head1  METHODS
+=head1 METHODS
 
-=head2 new
+=head2 new()
+
+    my $pdf_table = new PDF::Table;
 
 =over
 
-Returns an instance of the class. There are no parameters.
+=item Description
+
+Creates a new instance of the class. (to be improved)
+
+=item Parameters
+
+There are no parameters.
+
+=item Returns
+
+Reference to the new instance
 
 =back
 
-=head2 table($pdf, $page_obj, $data, %opts)
+=head2 table()
+
+    my ($final_page, $number_of_pages, $final_y) = table($pdf, $page, $data, %settings)
 
 =over
 
-The main method of this class.
-Takes a PDF::API2 instance, a page instance, some data to build the table and formatting options.
-The formatting options should be passed as named parameters.
+=item Description
+
+Generates a multi-row, multi-column table into an existing PDF document based on provided data set and settings.
+
+=item Parameters
+
+    $pdf      - a PDF::API2 instance representing the document being created
+    $page     - a PDF::API2::Page instance representing the current page of the document
+    $data     - an ARRAY reference to a 2D data structure that will be used to build the table
+    %settings - HASH with geometry and formatting parameters.
+
+For full %settings description see section L</Table settings> below.
+
 This method will add more pages to the pdf instance as required based on the formatting options and the amount of data.
 
+=item Returns
+
+The return value is a 3 items list where
+
+    $final_page - The first item is a PDF::API2::Page instance that the table ends on
+    $number_of_pages - The second item is the count of pages that the table spans on
+    $final_y - The third item is the Y coordinate of the table bottom so that additional content can be added in the same document.
+
+=item Example
+
+    my $pdf  = new PDF::API2;
+    my $page = $pdf->page();
+    my $data = [
+        ['foo1','bar1','baz1'],
+        ['foo2','bar2','baz2']
+    ];
+    my %settings = (
+        x       => 10,
+        w       => 570,
+        start_y => 220,
+        start_h => 180,
+    );
+
+    my ($final_page, $number_of_pages, $final_y) = $pdftable->table( $pdf, $page, $data, %options );
+
 =back
 
+=head3 Table settings
+
+=head4 Mandatory
+
+There are some mandatory parameteres for setting table geometry and position across page(s)
+
 =over
 
-The return value is a 3 item list where
-The first item is the PDF::API2::Page instance that the table ends on,
-The second item is the count of pages that the table spans, and
-The third item is the y position of the table bottom.
+=item B<x> - X coordinate of upper left corner of the table. Left edge of the sheet is 0.
+
+B<Value:> can be any whole number satisfying 0 =< X < PageWidth
+B<Default:> No default value
+
+    x => 10
+
+=item B<start_y> - Y coordinate of upper left corner of the table at the initial page.
+
+B<Value:> can be any whole number satisfying 0 < start_y < PageHeight (depending on space availability when embedding a table)
+B<Default:> No default value
+
+    start_y => 327
+
+=item B<w> - width of the table starting from X.
+
+B<Value:> can be any whole number satisfying 0 < w < PageWidth - x
+B<Default:> No default value
+
+    w  => 570
+
+=item B<start_h> - Height of the table on the initial page
+
+B<Value:> can be any whole number satisfying 0 < start_h < PageHeight - Current Y position
+B<Default:> No default value
+
+    start_h => 250
 
 =back
 
+=head4 Optional
+
 =over
 
-=item Example:
-
- ($end_page, $pages_spanned, $table_bot_y) = $pdftable->table(
-     $pdf,               # A PDF::API2 instance
-     $page_to_start_on,  # A PDF::API2::Page instance created with $page_to_start_on = $pdf->page();
-     $data,              # 2D arrayref of text strings
-     x  => $left_edge_of_table,    #X - coordinate of upper left corner
-     w  => 570, # width of table.
-     start_y => $initial_y_position_on_first_page,
-     next_y  => $initial_y_position_on_every_new_page,
-     start_h => $table_height_on_first_page,
-     next_h  => $table_height_on_every_new_page,
-     #OPTIONAL PARAMS BELOW
-     max_word_length=> 20,   # add a space after every 20th symbol in long words like serial numbers
-     padding        => 5,    # cell padding
-     padding_top    => 10,   # top cell padding, overides padding
-     padding_right  => 10,   # right cell padding, overides padding
-     padding_left   => 10,   # left cell padding, overides padding
-     padding_bottom => 10,   # bottom padding, overides -padding
-     border         => 1,    # border width, default 1, use 0 for no border
-     border_color   => 'red',# default black
-     font           => $pdf->corefont("Helvetica", -encoding => "utf8"), # default font
-     font_size      => 12,
-     font_color_odd => 'purple',
-     font_color_even=> 'black',
-     background_color_odd  => 'gray',         #cell background color for odd rows
-     background_color_even => 'lightblue',     #cell background color for even rows
-     new_page_func  => $code_ref,  # see section TABLE SPANNING
-     header_props   => $hdr_props, # see section HEADER ROW PROPERTIES
-     column_props   => $col_props, # see section COLUMN PROPERTIES
-     cell_props     => $row_props, # see section CELL PROPERTIES
- )
+=item B<next_h> - Height of the table on any additional page
+
+B<Value:> can be any whole number satisfying 0 < next_h < PageHeight
+B<Default:> Value of param B<'start_h'>
+
+    next_h  => 700
+
+=item B<next_y> - Y coordinate of upper left corner of the table at any additional page.
+
+B<Value:> can be any whole number satisfying 0 < next_y < PageHeight
+B<Default:> Value of param B<'start_y'>
+
+    next_y  => 750
+
+=item B<max_word_length> - Breaks long words (like serial numbers hashes etc.) by adding a space after every Nth symbol
+
+B<Value:> can be any whole positive number
+B<Default:> 20
+
+    max_word_length => 20    # Will add a space after every 20 symbols
+
+=item B<padding> - Padding applied to every cell
+
+=item B<padding_top>    - top cell padding, overrides 'padding'
+
+=item B<padding_right>  - right cell padding, overrides 'padding'
+
+=item B<padding_left>   - left cell padding, overrides 'padding'
+
+=item B<padding_bottom> - bottom padding, overrides 'padding'
+
+B<Value:> can be any whole positive number
+
+B<Default padding:> 0
+
+B<Default padding_*> $padding
+
+    padding        => 5      # all sides cell padding
+    padding_top    => 8,     # top cell padding, overrides 'padding'
+    padding_right  => 6,     # right cell padding, overrides 'padding'
+    padding_left   => 2,     # left cell padding, overrides 'padding'
+    padding_bottom => undef  # bottom padding will be 5 as it will fallback to 'padding'
+
+=item B<border> - Width of table border lines.
+
+=item B<horizontal_borders> - Width of horizontal border lines. Overrides 'border' value.
+
+=item B<vertical_borders> -  Width of vertical border lines. Overrides 'border' value.
+
+B<Value:> can be any whole positive number. When set to 0 will disable border lines.
+B<Default:> 1
+
+    border             => 3     # border width is 3
+    horizontal_borders => 1     # horizontal borders will be 1 overriding 3
+    vertical_borders   => undef # vertical borders will be 3 as it will fallback to 'border'
+
+=item B<vertical_borders> -  Width of vertical border lines. Overrides 'border' value.
+
+B<Value:> Color specifier as 'name' or 'HEX'
+B<Default:> 'black'
+
+    border_color => 'red'
+
+=item B<font> - instance of PDF::API2::Resource::Font defining the fontf to be used in the table
+
+B<Value:> can be any PDF::API2::Resource::* type of font
+B<Default:> 'Times' with UTF8 encoding
+
+    font => $pdf->corefont("Helvetica", -encoding => "utf8")
+
+=item B<font_size> - Default size of the font that will be used across the table
+
+B<Value:> can be any positive number
+B<Default:> 12
+
+    font_size => 16
+
+=item B<font_color> - Font color for all rows
+
+=item B<font_color_odd> - Font color for odd rows
+
+=item B<font_color_even> - Font color for even rows
+
+=item B<font_underline> - Font underline of the header row
+
+B<Value:> 'auto', integer of distance, or arrayref of distance & thickness (more than one pair will provide mlultiple underlines. Negative distance gives strike-through.
+B<Default:> none
+
+=item B<background_color_odd> - Background color for odd rows
+
+=item B<background_color_even> - Background color for even rows
+
+B<Value:> Color specifier as 'name' or 'HEX'
+B<Default:> 'black' font on 'white' background
+
+    font_color            => '#333333'
+    font_color_odd        => 'purple'
+    font_color_even       => '#00FF00'
+    background_color_odd  => 'gray'
+    background_color_even => 'lightblue'
+
+=item B<row_height> - Desired row height but it will be honored only if row_height > font_size + padding_top + padding_bottom
+
+B<Value:> can be any whole positive number
+B<Default:> font_size + padding_top + padding_bottom
+
+    row_height => 24
+
+=item B<new_page_func> - CODE reference to a function that returns a PDF::API2::Page instance.
+
+If used the parameter 'new_page_func' must be a function reference which when executed will create a new page and will return the object back to the module.
+For example you can use it to put Page Title, Page Frame, Page Numbers and other staff that you need.
+Also if you need some different type of paper size and orientation than the default A4-Portrait for example B2-Landscape you can use this function ref to set it up for you. For more info about creating pages refer to PDF::API2 PAGE METHODS Section.
+Don't forget that your function must return a page object created with PDF::API2 page() method.
+
+    new_page_func  => $code_ref
+
+=item B<header_props> - HASH reference to specific settings for the Header row of the table. See section L</Header Row Properties> below
+
+    header_props => $hdr_props
+
+=item B<column_props> - HASH reference to specific settings for each column of the table. See section L</Column Properties> below
+
+    column_props => $col_props
+
+=item B<cell_props> - HASH reference to specific settings for each column of the table. See section L</Cell Properties> below
+
+    cell_props => $cel_props
+
+=item B<cell_render_hook> - CODE reference to a function called with the current cell coordinates.  If used the parameter 'cell_render_hook' must be a function reference. It is most useful for creating a url link inside of a cell. The following example adds a link in the first column of each non-header row:
+
+    cell_render_hook  => sub {
+        my ($page, $first_row, $row, $col, $x, $y, $w, $h) = @_;
+
+        # Do nothing except for first column (and not a header row)
+        return unless ($col == 0);
+        return if ($first_row);
+
+        # Create link
+        my $value = $list_of_vals[$row-1];
+        my $url = "https://${hostname}/app/${value}";
+
+        my $annot = $page->annotation();
+        $annot->url( $url, -rect => [$x, $y, $x+$w, $y+$h] );
+    },
 
 =back
 
+=head4 Header Row Properties
+
+If the 'header_props' parameter is used, it should be a hashref. Passing an empty HASH will trigger a header row initialised with Default values.
+There is no 'data' variable for the content, because the module asumes that first table row will become the header row. It will copy this row and put it on every new page if 'repeat' param is set.
+
 =over
 
-=item HEADER ROW PROPERTIES
+=item B<font> - instance of PDF::API2::Resource::Font defining the fontf to be used in the header row
 
-If the 'header_props' parameter is used, it should be a hashref.
-It is your choice if it will be anonymous inline hash or predefined one.
-Also as you can see there is no data variable for the content because the module asumes that the first table row will become the header row. It will copy this row and put it on every new page if 'repeat' param is set.
+B<Value:> can be any PDF::API2::Resource::* type of font
+B<Default:> 'font' of the table. See table parameter 'font' for more details.
 
-=back
+=item B<font_size> - Font size of the header row
+
+B<Value:> can be any positive number
+B<Default:> 'font_size' of the table + 2
+
+=item B<font_color> - Font color of the header row
+
+B<Value:> Color specifier as 'name' or 'HEX'
+B<Default:> '#000066'
+
+=item B<font_underline> - Font underline of the header row
+
+B<Value:> 'auto', integer of distance, or arrayref of distance & thickness (more than one pair will provide mlultiple underlines. Negative distance gives strike-through.
+B<Default:> none
+
+=item B<bg_color> - Background color of the header row
+
+B<Value:> Color specifier as 'name' or 'HEX'
+B<Default:> #FFFFAA
+
+=item B<repeat> - Flag showing if header row should be repeated on every new page
 
-    $hdr_props =
+B<Value:> 0,1   1-Yes/True, 0-No/False
+B<Default:> 0
+
+=item B<justify> - Alignment of text in the header row.
+
+B<Value:> One of 'left', 'right', 'center'
+B<Default:> Same as column alignment (or 'left' if undefined)
+
+    my $hdr_props =
     {
-        # This param could be a pdf core font or user specified TTF.
-        #  See PDF::API2 FONT METHODS for more information
-        font       => $pdf->corefont("Times", -encoding => "utf8"),
-        font_size  => 10,
-        font_color => '#006666',
+        font       => $pdf->corefont("Helvetica", -encoding => "utf8"),
+        font_size  => 18,
+        font_color => '#004444',
         bg_color   => 'yellow',
-        repeat     => 1,    # 1/0 eq On/Off  if the header row should be repeated to every new page
+        repeat     => 1,
+        justify    => 'center'
     };
 
-=over
+=back
 
-=item COLUMN PROPERTIES
+=head4 Column Properties
 
 If the 'column_props' parameter is used, it should be an arrayref of hashrefs,
 with one hashref for each column of the table. The columns are counted from left to right so the hash reference at $col_props[0] will hold properties for the first column from left to right.
@@ -822,145 +1347,216 @@ If you DO NOT want to give properties for a column but to give for another just
 
 Each hashref can contain any of the keys shown below:
 
-=back
+=over
 
-  $col_props = [
-    {},# This is an empty hash so the next one will hold the properties for the second row from left to right.
-    {
-        min_w => 100,       # Minimum column width.
-        justify => 'right', # One of left|right ,
-        font => $pdf->corefont("Times", -encoding => "latin1"),
-        font_size => 10,
-        font_color=> 'blue',
-        background_color => '#FFFF00',
-    },
-    # etc.
-  ];
+=item B<min_w> - Minimum width of this column. Auto calculation will try its best to honour this param but aplying it is NOT guaranteed.
 
-=over
+B<Value:> can be any whole number satisfying 0 < min_w < w
+B<Default:> Auto calculated
+
+=item B<max_w> - Maximum width of this column. Auto calculation will try its best to honour this param but aplying it is NOT guaranteed.
+
+B<Value:> can be any whole number satisfying 0 < max_w < w
+B<Default:> Auto calculated
+
+=item B<font> - instance of PDF::API2::Resource::Font defining the fontf to be used in this column
+
+B<Value:> can be any PDF::API2::Resource::* type of font
+B<Default:> 'font' of the table. See table parameter 'font' for more details.
+
+=item B<font_size> - Font size of this column
+
+B<Value:> can be any positive number
+B<Default:> 'font_size' of the table.
+
+=item B<font_color> - Font color of this column
+
+B<Value:> Color specifier as 'name' or 'HEX'
+B<Default:> 'font_color' of the table.
+
+=item B<font_underline> - Font underline of this cell
+
+B<Value:> 'auto', integer of distance, or arrayref of distance & thickness (more than one pair will provide mlultiple underlines. Negative distance gives strike-through.
+B<Default:> none
 
-If the 'min_w' parameter is used for 'col_props', have in mind that it can be overwritten
-by the calculated minimum cell witdh if the userdefined value is less that calculated.
-This is done for safety reasons.
-In cases of a conflict between column formatting and odd/even row formatting,
-the former will override the latter.
+=item B<background_color> - Background color of this column
+
+B<Value:> Color specifier as 'name' or 'HEX'
+B<Default:> undef
+
+=item B<justify> - Alignment of text in this column
+
+B<Value:> One of 'left', 'right', 'center'
+B<Default:> 'left'
+
+Example:
+
+    my $col_props = [
+        {},# This is an empty hash so the next one will hold the properties for the second column from left to right.
+        {
+            min_w => 100,       # Minimum column width of 100.
+            max_w => 150,       # Maximum column width of 150 .
+            justify => 'right', # Right text alignment
+            font => $pdf->corefont("Helvetica", -encoding => "latin1"),
+            font_size => 10,
+            font_color=> 'blue',
+            background_color => '#FFFF00',
+        },
+        # etc.
+    ];
 
 =back
 
-=over
+NOTE: If 'min_w' and/or 'max_w' parameter is used in 'col_props', have in mind that it may be overridden by the calculated minimum/maximum cell witdh so that table can be created.
+When this happens a warning will be issued with some advises what can be done.
+In cases of a conflict between column formatting and odd/even row formatting, 'col_props' will override odd/even.
 
-=item CELL PROPERTIES
+=head4 Cell Properties
 
 If the 'cell_props' parameter is used, it should be an arrayref with arrays of hashrefs
 (of the same dimension as the data array) with one hashref for each cell of the table.
-Each hashref can contain any of keys shown here:
-
-=back
 
-  $cell_props = [
-    [ #This array is for the first row. If header_props is defined it will overwrite this settings.
-      {#Row 1 cell 1
-        background_color => '#AAAA00',
-        font_color       => 'blue',
-      },
-      # etc.
-    ],
-    [ #Row 2
-      {#Row 2 cell 1
-        background_color => '#CCCC00',
-        font_color       => 'blue',
-      },
-      {#Row 2 cell 2
-        background_color => '#CCCC00',
-        font_color       => 'blue',
-      },
-      # etc.
-    ],
-  # etc.
-  ];
+Each hashref can contain any of the keys shown below:
 
 =over
 
-In case of a conflict between column, odd/even and cell formating, cell formating will overwrite the other two.
-In case of a conflict between header row cell formating, header formating will win.
+=item B<font> - instance of PDF::API2::Resource::Font defining the fontf to be used in this cell
 
-=back
+B<Value:> can be any PDF::API2::Resource::* type of font
+B<Default:> 'font' of the table. See table parameter 'font' for more details.
 
-=over
+=item B<font_size> - Font size of this cell
 
+B<Value:> can be any positive number
+B<Default:> 'font_size' of the table.
 
+=item B<font_color> - Font color of this cell
 
-=item TABLE SPANNING
+B<Value:> Color specifier as 'name' or 'HEX'
+B<Default:> 'font_color' of the table.
 
-If used the parameter 'new_page_func' must be a function reference which when executed will create a new page and will return the object back to the module.
-For example you can use it to put Page Title, Page Frame, Page Numbers and other staff that you need.
-Also if you need some different type of paper size and orientation than the default A4-Portrait for example B2-Landscape you can use this function ref to set it up for you. For more info about creating pages refer to PDF::API2 PAGE METHODS Section.
-Dont forget that your function must return a page object created with PDF::API2 page() method.
+=item B<font_underline> - Font underline of this cell
+
+B<Value:> 'auto', integer of distance, or arrayref of distance & thickness (more than one pair will provide mlultiple underlines. Negative distance gives strike-through.
+B<Default:> none
+
+=item B<background_color> - Background color of this cell
+
+B<Value:> Color specifier as 'name' or 'HEX'
+B<Default:> undef
+
+=item B<justify> - Alignment of text in this cell
+
+B<Value:> One of 'left', 'right', 'center'
+B<Default:> 'left'
+
+Example:
+
+    my $cell_props = [
+        [ #This array is for the first row. If header_props is defined it will overwrite these settings.
+            {    #Row 1 cell 1
+                background_color => '#AAAA00',
+                font_color       => 'yellow',
+                font_underline   => [ 2, 2 ],
+            },
+
+            # etc.
+        ],
+        [#Row 2
+            {    #Row 2 cell 1
+                background_color => '#CCCC00',
+                font_color       => 'blue',
+            },
+            {    #Row 2 cell 2
+                background_color => '#BBBB00',
+                font_color       => 'red',
+            },
+            # etc.
+        ],
+        # etc.
+    ];
+
+    OR
+
+    my $cell_props = [];
+    $cell_props->[1][0] = {
+        #Row 2 cell 1
+        background_color => '#CCCC00',
+        font_color       => 'blue',
+    };
 
 =back
 
-=head2 text_block( $txtobj, $string, x => $x, y => $y, w => $width, h => $height)
+NOTE: In case of a conflict between column, odd/even and cell formatting, cell formatting will overwrite the other two.
+In case of a conflict between header row and cell formatting, header formatting will override cell.
+
+=head2 text_block()
+
+    my ($width_of_last_line, $ypos_of_last_line, $left_over_text) = text_block( $txt, $data, %settings)
 
 =over
 
+=item Description
+
 Utility method to create a block of text. The block may contain multiple paragraphs.
-It is mainly used internaly but you can use it from outside for placing formated text anywhere on the sheet.
+It is mainly used internaly but you can use it from outside for placing formatted text anywhere on the sheet.
 
-=back
+NOTE: This method will NOT add more pages to the pdf instance if the space is not enough to place the string inside the block.
+Leftover text will be returned and has to be handled by the caller - i.e. add a new page and a new block with the leftover.
 
-=over
+=item Parameters
 
-=item Example:
+    $txt  - a PDF::API2::Page::Text instance representing the text tool
+    $data - a string that will be placed inside the block
+    %settings - HASH with geometry and formatting parameters.
 
-=back
+=item Reuturns
 
-=over
+The return value is a 3 items list where
 
- # PDF::API2 objects
- my $page = $pdf->page;
- my $txt = $page->text;
+    $width_of_last_line - Width of last line in the block
+    $final_y - The Y coordinate of the block bottom so that additional content can be added after it
+    $left_over_text - Text that was did not fit in the provided box geometry.
 
-=back
+=item Example
 
-=over
+    # PDF::API2 objects
+    my $page = $pdf->page;
+    my $txt  = $page->text;
+
+    my %settings = (
+        x => 10,
+        y => 570,
+        w => 220,
+        h => 180
 
- ($width_of_last_line, $ypos_of_last_line, $left_over_text) = $pdftable->text_block(
-    $txt,
-    $text_to_place,
-    #X,Y - coordinates of upper left corner
-    x        => $left_edge_of_block,
-    y        => $y_position_of_first_line,
-    w        => $width_of_block,
-    h        => $height_of_block,
-    #OPTIONAL PARAMS
-    lead     => $font_size | $distance_between_lines,
-    align    => "left|right|center|justify|fulljustify",
-    hang     => $optional_hanging_indent,
-    Only one of the subsequent 3params can be given.
-    They override each other.-parspace is the weightest
-    parspace => $optional_vertical_space_before_first_paragraph,
-    flindent => $optional_indent_of_first_line,
-    fpindent => $optional_indent_of_first_paragraph,
-
-    indent   => $optional_indent_of_text_to_every_non_first_line,
- );
+        #OPTIONAL PARAMS
+        lead     => $font_size | $distance_between_lines,
+        align    => "left|right|center|justify|fulljustify",
+        hang     => $optional_hanging_indent,
+        Only one of the subsequent 3params can be given.
+        They override each other.-parspace is the weightest
+        parspace => $optional_vertical_space_before_first_paragraph,
+        flindent => $optional_indent_of_first_line,
+        fpindent => $optional_indent_of_first_paragraph,
+        indent   => $optional_indent_of_text_to_every_non_first_line,
+    );
 
+    my ( $width_of_last_line, $final_y, $left_over_text ) = $pdftable->text_block( $txt, $data, %settings );
 
 =back
 
+=head1 VERSION
+
+0.9.7
+
 =head1 AUTHOR
 
 Daemmon Hughes
 
 =head1 DEVELOPMENT
 
-ALL IMPROVEMENTS and BUGS Since Ver: 0.02
-
-Desislav Kamenov
-
-=head1 VERSION
-
-0.9.3
+Further development since Ver: 0.02 - Desislav Kamenov
 
 =head1 COPYRIGHT AND LICENSE
 
@@ -973,7 +1569,9 @@ at your option, any later version of Perl 5 you may have available.
 
 =head1 PLUGS
 
-by Daemmon Hughes
+=over
+
+=item by Daemmon Hughes
 
 Much of the work on this module was sponsered by
 Stone Environmental Inc. (www.stone-env.com).
@@ -981,16 +1579,26 @@ Stone Environmental Inc. (www.stone-env.com).
 The text_block() method is a slightly modified copy of the one from
 Rick Measham's PDF::API2 tutorial at
 http://pdfapi2.sourceforge.net/cgi-bin/view/Main/YourFirstDocument
-update: The tutorial is no longer available. Please visit http://pdfapi2.sourceforge.net .
 
-by Desislav Kamenov
+=item by Desislav Kamenov (@deskata on Twitter)
+
+The development of this module was supported by SEEBURGER AG (www.seeburger.com) till year 2007
 
-The development of this module is sponsored by SEEBURGER AG (www.seeburger.com)
+Thanks to my friends Krasimir Berov and Alex Kantchev for helpful tips and QA during development of versions 0.9.0 to 0.9.5
 
-Thanks to my friends Krasimir Berov and Alex Kantchev for helpful tips and QA during development.
+Thanks to all GitHub contributors!
+
+=back
+
+=head1 CONTRIBUTION
+
+Hey PDF::Table is on GitHub. You are more than welcome to contribute!
+
+https://github.com/kamenov/PDF-Table
 
 =head1 SEE ALSO
 
 L<PDF::API2>
 
 =cut
+
diff --git a/modules/override/Rose/DBx/Cache/Anywhere.pm b/modules/override/Rose/DBx/Cache/Anywhere.pm
deleted file mode 100644 (file)
index abc87c7..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-package Rose::DBx::Cache::Anywhere;
-use strict;
-use warnings;
-use Carp;
-use base qw( Rose::DB::Cache );
-
-=head1 NAME
-
-Rose::DBx::Cache::Anywhere - get Apache::DBI behaviour without Apache
-
-=head1 DESCRIPTION
-
-This class is used by Rose::DBx::AutoReconnect.
-The author uses
-Rose::DB with Catalyst under both the Catalyst dev server and
-FastCGI and found that the standard Rose::DB::Cache behaviour
-did not work well with those environments.
-
-=head1 METHODS
-
-=head2 prepare_db( I<rose_db>, I<entry> )
-
-Overrides default method to always ping() dbh if not running
-under mod_perl.
-
-=cut
-
-sub prepare_db {
-    my ( $self, $db, $entry ) = @_;
-
-    if ( Rose::DB::Cache::MOD_PERL_1 || Rose::DB::Cache::MOD_PERL_2 ) {
-        return $self->SUPER::prepare_db( $db, $entry );
-    }
-
-    if ( !$entry->is_prepared ) {
-        if ( $entry->created_during_apache_startup ) {
-            if ( $db->has_dbh ) {
-                eval { $db->dbh->disconnect };    # will probably fail!
-                $db->dbh(undef);
-            }
-
-            $entry->created_during_apache_startup(0);
-            return;
-        }
-
-        # if this a dummy kivitendo dbh, don't try to actually prepare this.
-        if ($db->type =~ /KIVITENDO_EMPTY/) {
-          return;
-        }
-
-        $entry->prepared(1);
-    }
-
-    if ( !$db->dbh->ping ) {
-        $db->dbh(undef);
-    }
-}
-
-1;
-
-__END__
-
-=head1 AUTHOR
-
-Peter Karman, C<< <karman at cpan.org> >>
-
-=head1 BUGS
-
-Please report any bugs or feature requests to
-C<bug-rose-dbx-autoreconnect at rt.cpan.org>, or through the web interface at
-L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Rose-DBx-AutoReconnect>.
-I will be notified, and then you'll automatically be notified of progress on
-your bug as I make changes.
-
-=head1 SUPPORT
-
-You can find documentation for this module with the perldoc command.
-
-    perldoc Rose::DBx::AutoReconnect
-
-You can also look for information at:
-
-=over 4
-
-=item * AnnoCPAN: Annotated CPAN documentation
-
-L<http://annocpan.org/dist/Rose-DBx-AutoReconnect>
-
-=item * CPAN Ratings
-
-L<http://cpanratings.perl.org/d/Rose-DBx-AutoReconnect>
-
-=item * RT: CPAN's request tracker
-
-L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Rose-DBx-AutoReconnect>
-
-=item * Search CPAN
-
-L<http://search.cpan.org/dist/Rose-DBx-AutoReconnect>
-
-=back
-
-=head1 ACKNOWLEDGEMENTS
-
-The Minnesota Supercomputing Institute C<< http://www.msi.umn.edu/ >>
-sponsored the development of this software.
-
-=head1 COPYRIGHT
-
-Copyright 2008 by the Regents of the University of Minnesota.
-All rights reserved.
-
-This program is free software; you can redistribute it and/or modify it
-under the same terms as Perl itself.
-
-=cut
diff --git a/modules/override/YAML.pm b/modules/override/YAML.pm
deleted file mode 100644 (file)
index 56c3c95..0000000
+++ /dev/null
@@ -1,100 +0,0 @@
-package YAML;
-our $VERSION = '1.14';
-
-use YAML::Mo;
-
-use Exporter;
-push @YAML::ISA, 'Exporter';
-our @EXPORT = qw{ Dump Load };
-our @EXPORT_OK = qw{ freeze thaw DumpFile LoadFile Bless Blessed };
-
-use YAML::Node; # XXX This is a temp fix for Module::Build
-
-# XXX This VALUE nonsense needs to go.
-use constant VALUE => "\x07YAML\x07VALUE\x07";
-
-# YAML Object Properties
-has dumper_class => default => sub {'YAML::Dumper'};
-has loader_class => default => sub {'YAML::Loader'};
-has dumper_object => default => sub {$_[0]->init_action_object("dumper")};
-has loader_object => default => sub {$_[0]->init_action_object("loader")};
-
-sub Dump {
-    my $yaml = YAML->new;
-    $yaml->dumper_class($YAML::DumperClass)
-        if $YAML::DumperClass;
-    return $yaml->dumper_object->dump(@_);
-}
-
-sub Load {
-    my $yaml = YAML->new;
-    $yaml->loader_class($YAML::LoaderClass)
-        if $YAML::LoaderClass;
-    return $yaml->loader_object->load(@_);
-}
-
-{
-    no warnings 'once';
-    # freeze/thaw is the API for Storable string serialization. Some
-    # modules make use of serializing packages on if they use freeze/thaw.
-    *freeze = \ &Dump;
-    *thaw   = \ &Load;
-}
-
-sub DumpFile {
-    my $OUT;
-    my $filename = shift;
-    if (ref $filename eq 'GLOB') {
-        $OUT = $filename;
-    }
-    else {
-        my $mode = '>';
-        if ($filename =~ /^\s*(>{1,2})\s*(.*)$/) {
-            ($mode, $filename) = ($1, $2);
-        }
-        open $OUT, $mode, $filename
-          or YAML::Mo::Object->die('YAML_DUMP_ERR_FILE_OUTPUT', $filename, $!);
-    }
-    binmode $OUT, ':utf8';  # if $Config{useperlio} eq 'define';
-    local $/ = "\n"; # reset special to "sane"
-    print $OUT Dump(@_);
-}
-
-sub LoadFile {
-    my $IN;
-    my $filename = shift;
-    if (ref $filename eq 'GLOB') {
-        $IN = $filename;
-    }
-    else {
-        open $IN, '<', $filename
-          or YAML::Mo::Object->die('YAML_LOAD_ERR_FILE_INPUT', $filename, $!);
-    }
-    binmode $IN, ':utf8';  # if $Config{useperlio} eq 'define';
-    return Load(do { local $/; <$IN> });
-}
-
-sub init_action_object {
-    my $self = shift;
-    my $object_class = (shift) . '_class';
-    my $module_name = $self->$object_class;
-    eval "require $module_name";
-    $self->die("Error in require $module_name - $@")
-        if $@ and "$@" !~ /Can't locate/;
-    my $object = $self->$object_class->new;
-    $object->set_global_options;
-    return $object;
-}
-
-my $global = {};
-sub Bless {
-    require YAML::Dumper::Base;
-    YAML::Dumper::Base::bless($global, @_)
-}
-sub Blessed {
-    require YAML::Dumper::Base;
-    YAML::Dumper::Base::blessed($global, @_)
-}
-sub global_object { $global }
-
-1;
diff --git a/modules/override/YAML/Any.pm b/modules/override/YAML/Any.pm
deleted file mode 100644 (file)
index c2d35ee..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-use strict; use warnings;
-package YAML::Any;
-our $VERSION = '1.14';
-
-use Exporter ();
-
-@YAML::Any::ISA       = 'Exporter';
-@YAML::Any::EXPORT    = qw(Dump Load);
-@YAML::Any::EXPORT_OK = qw(DumpFile LoadFile);
-
-my @dump_options = qw(
-    UseCode
-    DumpCode
-    SpecVersion
-    Indent
-    UseHeader
-    UseVersion
-    SortKeys
-    AnchorPrefix
-    UseBlock
-    UseFold
-    CompressSeries
-    InlineSeries
-    UseAliases
-    Purity
-    Stringify
-);
-
-my @load_options = qw(
-    UseCode
-    LoadCode
-);
-
-my @implementations = qw(
-    YAML::XS
-    YAML::Syck
-    YAML::Old
-    YAML
-    YAML::Tiny
-);
-
-sub import {
-    __PACKAGE__->implementation;
-    goto &Exporter::import;
-}
-
-sub Dump {
-    no strict 'refs';
-    no warnings 'once';
-    my $implementation = __PACKAGE__->implementation;
-    for my $option (@dump_options) {
-        my $var = "$implementation\::$option";
-        my $value = $$var;
-        local $$var;
-        $$var = defined $value ? $value : ${"YAML::$option"};
-    }
-    return &{"$implementation\::Dump"}(@_);
-}
-
-sub DumpFile {
-    no strict 'refs';
-    no warnings 'once';
-    my $implementation = __PACKAGE__->implementation;
-    for my $option (@dump_options) {
-        my $var = "$implementation\::$option";
-        my $value = $$var;
-        local $$var;
-        $$var = defined $value ? $value : ${"YAML::$option"};
-    }
-    return &{"$implementation\::DumpFile"}(@_);
-}
-
-sub Load {
-    no strict 'refs';
-    no warnings 'once';
-    my $implementation = __PACKAGE__->implementation;
-    for my $option (@load_options) {
-        my $var = "$implementation\::$option";
-        my $value = $$var;
-        local $$var;
-        $$var = defined $value ? $value : ${"YAML::$option"};
-    }
-    return &{"$implementation\::Load"}(@_);
-}
-
-sub LoadFile {
-    no strict 'refs';
-    no warnings 'once';
-    my $implementation = __PACKAGE__->implementation;
-    for my $option (@load_options) {
-        my $var = "$implementation\::$option";
-        my $value = $$var;
-        local $$var;
-        $$var = defined $value ? $value : ${"YAML::$option"};
-    }
-    return &{"$implementation\::LoadFile"}(@_);
-}
-
-sub order {
-    return @YAML::Any::_TEST_ORDER
-        if @YAML::Any::_TEST_ORDER;
-    return @implementations;
-}
-
-sub implementation {
-    my @order = __PACKAGE__->order;
-    for my $module (@order) {
-        my $path = $module;
-        $path =~ s/::/\//g;
-        $path .= '.pm';
-        return $module if exists $INC{$path};
-        eval "require $module; 1" and return $module;
-    }
-    croak("YAML::Any couldn't find any of these YAML implementations: @order");
-}
-
-sub croak {
-    require Carp;
-    Carp::croak(@_);
-}
-
-1;
diff --git a/modules/override/YAML/Dumper.pm b/modules/override/YAML/Dumper.pm
deleted file mode 100644 (file)
index 5f75ab2..0000000
+++ /dev/null
@@ -1,575 +0,0 @@
-package YAML::Dumper;
-
-use YAML::Mo;
-extends 'YAML::Dumper::Base';
-
-use YAML::Dumper::Base;
-use YAML::Node;
-use YAML::Types;
-use Scalar::Util qw();
-
-# Context constants
-use constant KEY       => 3;
-use constant BLESSED   => 4;
-use constant FROMARRAY => 5;
-use constant VALUE     => "\x07YAML\x07VALUE\x07";
-
-# Common YAML character sets
-my $ESCAPE_CHAR = '[\\x00-\\x08\\x0b-\\x0d\\x0e-\\x1f]';
-my $LIT_CHAR    = '|';
-
-#==============================================================================
-# OO version of Dump. YAML->new->dump($foo);
-sub dump {
-    my $self = shift;
-    $self->stream('');
-    $self->document(0);
-    for my $document (@_) {
-        $self->{document}++;
-        $self->transferred({});
-        $self->id_refcnt({});
-        $self->id_anchor({});
-        $self->anchor(1);
-        $self->level(0);
-        $self->offset->[0] = 0 - $self->indent_width;
-        $self->_prewalk($document);
-        $self->_emit_header($document);
-        $self->_emit_node($document);
-    }
-    return $self->stream;
-}
-
-# Every YAML document in the stream must begin with a YAML header, unless
-# there is only a single document and the user requests "no header".
-sub _emit_header {
-    my $self = shift;
-    my ($node) = @_;
-    if (not $self->use_header and
-        $self->document == 1
-       ) {
-        $self->die('YAML_DUMP_ERR_NO_HEADER')
-          unless ref($node) =~ /^(HASH|ARRAY)$/;
-        $self->die('YAML_DUMP_ERR_NO_HEADER')
-          if ref($node) eq 'HASH' and keys(%$node) == 0;
-        $self->die('YAML_DUMP_ERR_NO_HEADER')
-          if ref($node) eq 'ARRAY' and @$node == 0;
-        # XXX Also croak if aliased, blessed, or ynode
-        $self->headless(1);
-        return;
-    }
-    $self->{stream} .= '---';
-# XXX Consider switching to 1.1 style
-    if ($self->use_version) {
-#         $self->{stream} .= " #YAML:1.0";
-    }
-}
-
-# Walk the tree to be dumped and keep track of its reference counts.
-# This function is where the Dumper does all its work. All type
-# transfers happen here.
-sub _prewalk {
-    my $self = shift;
-    my $stringify = $self->stringify;
-    my ($class, $type, $node_id) = $self->node_info(\$_[0], $stringify);
-
-    # Handle typeglobs
-    if ($type eq 'GLOB') {
-        $self->transferred->{$node_id} =
-          YAML::Type::glob->yaml_dump($_[0]);
-        $self->_prewalk($self->transferred->{$node_id});
-        return;
-    }
-
-    # Handle regexps
-    if (ref($_[0]) eq 'Regexp') {
-        return;
-    }
-
-    # Handle Purity for scalars.
-    # XXX can't find a use case yet. Might be YAGNI.
-    if (not ref $_[0]) {
-        $self->{id_refcnt}{$node_id}++ if $self->purity;
-        return;
-    }
-
-    # Make a copy of original
-    my $value = $_[0];
-    ($class, $type, $node_id) = $self->node_info($value, $stringify);
-
-    # Must be a stringified object.
-    return if (ref($value) and not $type);
-
-    # Look for things already transferred.
-    if ($self->transferred->{$node_id}) {
-        (undef, undef, $node_id) = (ref $self->transferred->{$node_id})
-          ? $self->node_info($self->transferred->{$node_id}, $stringify)
-          : $self->node_info(\ $self->transferred->{$node_id}, $stringify);
-        $self->{id_refcnt}{$node_id}++;
-        return;
-    }
-
-    # Handle code refs
-    if ($type eq 'CODE') {
-        $self->transferred->{$node_id} = 'placeholder';
-        YAML::Type::code->yaml_dump(
-            $self->dump_code,
-            $_[0],
-            $self->transferred->{$node_id}
-        );
-        ($class, $type, $node_id) =
-          $self->node_info(\ $self->transferred->{$node_id}, $stringify);
-        $self->{id_refcnt}{$node_id}++;
-        return;
-    }
-
-    # Handle blessed things
-    if (defined $class) {
-        if ($value->can('yaml_dump')) {
-            $value = $value->yaml_dump;
-        }
-        elsif ($type eq 'SCALAR') {
-            $self->transferred->{$node_id} = 'placeholder';
-            YAML::Type::blessed->yaml_dump
-              ($_[0], $self->transferred->{$node_id});
-            ($class, $type, $node_id) =
-              $self->node_info(\ $self->transferred->{$node_id}, $stringify);
-            $self->{id_refcnt}{$node_id}++;
-            return;
-        }
-        else {
-            $value = YAML::Type::blessed->yaml_dump($value);
-        }
-        $self->transferred->{$node_id} = $value;
-        (undef, $type, $node_id) = $self->node_info($value, $stringify);
-    }
-
-    # Handle YAML Blessed things
-    require YAML;
-    if (defined YAML->global_object()->{blessed_map}{$node_id}) {
-        $value = YAML->global_object()->{blessed_map}{$node_id};
-        $self->transferred->{$node_id} = $value;
-        ($class, $type, $node_id) = $self->node_info($value, $stringify);
-        $self->_prewalk($value);
-        return;
-    }
-
-    # Handle hard refs
-    if ($type eq 'REF' or $type eq 'SCALAR') {
-        $value = YAML::Type::ref->yaml_dump($value);
-        $self->transferred->{$node_id} = $value;
-        (undef, $type, $node_id) = $self->node_info($value, $stringify);
-    }
-
-    # Handle ref-to-glob's
-    elsif ($type eq 'GLOB') {
-        my $ref_ynode = $self->transferred->{$node_id} =
-          YAML::Type::ref->yaml_dump($value);
-
-        my $glob_ynode = $ref_ynode->{&VALUE} =
-          YAML::Type::glob->yaml_dump($$value);
-
-        (undef, undef, $node_id) = $self->node_info($glob_ynode, $stringify);
-        $self->transferred->{$node_id} = $glob_ynode;
-        $self->_prewalk($glob_ynode);
-        return;
-    }
-
-    # Increment ref count for node
-    return if ++($self->{id_refcnt}{$node_id}) > 1;
-
-    # Keep on walking
-    if ($type eq 'HASH') {
-        $self->_prewalk($value->{$_})
-            for keys %{$value};
-        return;
-    }
-    elsif ($type eq 'ARRAY') {
-        $self->_prewalk($_)
-            for @{$value};
-        return;
-    }
-
-    # Unknown type. Need to know about it.
-    $self->warn(<<"...");
-YAML::Dumper can't handle dumping this type of data.
-Please report this to the author.
-
-id:    $node_id
-type:  $type
-class: $class
-value: $value
-
-...
-
-    return;
-}
-
-# Every data element and sub data element is a node.
-# Everything emitted goes through this function.
-sub _emit_node {
-    my $self = shift;
-    my ($type, $node_id);
-    my $ref = ref($_[0]);
-    if ($ref) {
-        if ($ref eq 'Regexp') {
-            $self->_emit(' !!perl/regexp');
-            $self->_emit_str("$_[0]");
-            return;
-        }
-        (undef, $type, $node_id) = $self->node_info($_[0], $self->stringify);
-    }
-    else {
-        $type = $ref || 'SCALAR';
-        (undef, undef, $node_id) = $self->node_info(\$_[0], $self->stringify);
-    }
-
-    my ($ynode, $tag) = ('') x 2;
-    my ($value, $context) = (@_, 0);
-
-    if (defined $self->transferred->{$node_id}) {
-        $value = $self->transferred->{$node_id};
-        $ynode = ynode($value);
-        if (ref $value) {
-            $tag = defined $ynode ? $ynode->tag->short : '';
-            (undef, $type, $node_id) =
-              $self->node_info($value, $self->stringify);
-        }
-        else {
-            $ynode = ynode($self->transferred->{$node_id});
-            $tag = defined $ynode ? $ynode->tag->short : '';
-            $type = 'SCALAR';
-            (undef, undef, $node_id) =
-              $self->node_info(
-                  \ $self->transferred->{$node_id},
-                  $self->stringify
-              );
-        }
-    }
-    elsif ($ynode = ynode($value)) {
-        $tag = $ynode->tag->short;
-    }
-
-    if ($self->use_aliases) {
-        $self->{id_refcnt}{$node_id} ||= 0;
-        if ($self->{id_refcnt}{$node_id} > 1) {
-            if (defined $self->{id_anchor}{$node_id}) {
-                $self->{stream} .= ' *' . $self->{id_anchor}{$node_id} . "\n";
-                return;
-            }
-            my $anchor = $self->anchor_prefix . $self->{anchor}++;
-            $self->{stream} .= ' &' . $anchor;
-            $self->{id_anchor}{$node_id} = $anchor;
-        }
-    }
-
-    return $self->_emit_str("$value")   # Stringified object
-      if ref($value) and not $type;
-    return $self->_emit_scalar($value, $tag)
-      if $type eq 'SCALAR' and $tag;
-    return $self->_emit_str($value)
-      if $type eq 'SCALAR';
-    return $self->_emit_mapping($value, $tag, $node_id, $context)
-      if $type eq 'HASH';
-    return $self->_emit_sequence($value, $tag)
-      if $type eq 'ARRAY';
-    $self->warn('YAML_DUMP_WARN_BAD_NODE_TYPE', $type);
-    return $self->_emit_str("$value");
-}
-
-# A YAML mapping is akin to a Perl hash.
-sub _emit_mapping {
-    my $self = shift;
-    my ($value, $tag, $node_id, $context) = @_;
-    $self->{stream} .= " !$tag" if $tag;
-
-    # Sometimes 'keys' fails. Like on a bad tie implementation.
-    my $empty_hash = not(eval {keys %$value});
-    $self->warn('YAML_EMIT_WARN_KEYS', $@) if $@;
-    return ($self->{stream} .= " {}\n") if $empty_hash;
-
-    # If CompressSeries is on (default) and legal is this context, then
-    # use it and make the indent level be 2 for this node.
-    if ($context == FROMARRAY and
-        $self->compress_series and
-        not (defined $self->{id_anchor}{$node_id} or $tag or $empty_hash)
-       ) {
-        $self->{stream} .= ' ';
-        $self->offset->[$self->level+1] = $self->offset->[$self->level] + 2;
-    }
-    else {
-        $context = 0;
-        $self->{stream} .= "\n"
-          unless $self->headless && not($self->headless(0));
-        $self->offset->[$self->level+1] =
-          $self->offset->[$self->level] + $self->indent_width;
-    }
-
-    $self->{level}++;
-    my @keys;
-    if ($self->sort_keys == 1) {
-        if (ynode($value)) {
-            @keys = keys %$value;
-        }
-        else {
-            @keys = sort keys %$value;
-        }
-    }
-    elsif ($self->sort_keys == 2) {
-        @keys = sort keys %$value;
-    }
-    # XXX This is hackish but sometimes handy. Not sure whether to leave it in.
-    elsif (ref($self->sort_keys) eq 'ARRAY') {
-        my $i = 1;
-        my %order = map { ($_, $i++) } @{$self->sort_keys};
-        @keys = sort {
-            (defined $order{$a} and defined $order{$b})
-              ? ($order{$a} <=> $order{$b})
-              : ($a cmp $b);
-        } keys %$value;
-    }
-    else {
-        @keys = keys %$value;
-    }
-    # Force the YAML::VALUE ('=') key to sort last.
-    if (exists $value->{&VALUE}) {
-        for (my $i = 0; $i < @keys; $i++) {
-            if ($keys[$i] eq &VALUE) {
-                splice(@keys, $i, 1);
-                push @keys, &VALUE;
-                last;
-            }
-        }
-    }
-
-    for my $key (@keys) {
-        $self->_emit_key($key, $context);
-        $context = 0;
-        $self->{stream} .= ':';
-        $self->_emit_node($value->{$key});
-    }
-    $self->{level}--;
-}
-
-# A YAML series is akin to a Perl array.
-sub _emit_sequence {
-    my $self = shift;
-    my ($value, $tag) = @_;
-    $self->{stream} .= " !$tag" if $tag;
-
-    return ($self->{stream} .= " []\n") if @$value == 0;
-
-    $self->{stream} .= "\n"
-      unless $self->headless && not($self->headless(0));
-
-    # XXX Really crufty feature. Better implemented by ynodes.
-    if ($self->inline_series and
-        @$value <= $self->inline_series and
-        not (scalar grep {ref or /\n/} @$value)
-       ) {
-        $self->{stream} =~ s/\n\Z/ /;
-        $self->{stream} .= '[';
-        for (my $i = 0; $i < @$value; $i++) {
-            $self->_emit_str($value->[$i], KEY);
-            last if $i == $#{$value};
-            $self->{stream} .= ', ';
-        }
-        $self->{stream} .= "]\n";
-        return;
-    }
-
-    $self->offset->[$self->level + 1] =
-      $self->offset->[$self->level] + $self->indent_width;
-    $self->{level}++;
-    for my $val (@$value) {
-        $self->{stream} .= ' ' x $self->offset->[$self->level];
-        $self->{stream} .= '-';
-        $self->_emit_node($val, FROMARRAY);
-    }
-    $self->{level}--;
-}
-
-# Emit a mapping key
-sub _emit_key {
-    my $self = shift;
-    my ($value, $context) = @_;
-    $self->{stream} .= ' ' x $self->offset->[$self->level]
-      unless $context == FROMARRAY;
-    $self->_emit_str($value, KEY);
-}
-
-# Emit a blessed SCALAR
-sub _emit_scalar {
-    my $self = shift;
-    my ($value, $tag) = @_;
-    $self->{stream} .= " !$tag";
-    $self->_emit_str($value, BLESSED);
-}
-
-sub _emit {
-    my $self = shift;
-    $self->{stream} .= join '', @_;
-}
-
-# Emit a string value. YAML has many scalar styles. This routine attempts to
-# guess the best style for the text.
-sub _emit_str {
-    my $self = shift;
-    my $type = $_[1] || 0;
-
-    # Use heuristics to find the best scalar emission style.
-    $self->offset->[$self->level + 1] =
-      $self->offset->[$self->level] + $self->indent_width;
-    $self->{level}++;
-
-    my $sf = $type == KEY ? '' : ' ';
-    my $sb = $type == KEY ? '? ' : ' ';
-    my $ef = $type == KEY ? '' : "\n";
-    my $eb = "\n";
-
-    while (1) {
-        $self->_emit($sf),
-        $self->_emit_plain($_[0]),
-        $self->_emit($ef), last
-          if not defined $_[0];
-        $self->_emit($sf, '=', $ef), last
-          if $_[0] eq VALUE;
-        $self->_emit($sf),
-        $self->_emit_double($_[0]),
-        $self->_emit($ef), last
-          if $_[0] =~ /$ESCAPE_CHAR/;
-        if ($_[0] =~ /\n/) {
-            $self->_emit($sb),
-            $self->_emit_block($LIT_CHAR, $_[0]),
-            $self->_emit($eb), last
-              if $self->use_block;
-              Carp::cluck "[YAML] \$UseFold is no longer supported"
-              if $self->use_fold;
-            $self->_emit($sf),
-            $self->_emit_double($_[0]),
-            $self->_emit($ef), last
-              if length $_[0] <= 30;
-            $self->_emit($sf),
-            $self->_emit_double($_[0]),
-            $self->_emit($ef), last
-              if $_[0] !~ /\n\s*\S/;
-            $self->_emit($sb),
-            $self->_emit_block($LIT_CHAR, $_[0]),
-            $self->_emit($eb), last;
-        }
-        $self->_emit($sf),
-        $self->_emit_number($_[0]),
-        $self->_emit($ef), last
-          if $self->is_literal_number($_[0]);
-        $self->_emit($sf),
-        $self->_emit_plain($_[0]),
-        $self->_emit($ef), last
-          if $self->is_valid_plain($_[0]);
-        $self->_emit($sf),
-        $self->_emit_double($_[0]),
-        $self->_emit($ef), last
-          if $_[0] =~ /'/;
-        $self->_emit($sf),
-        $self->_emit_single($_[0]),
-        $self->_emit($ef);
-        last;
-    }
-
-    $self->{level}--;
-
-    return;
-}
-
-sub is_literal_number {
-    my $self = shift;
-    # Stolen from JSON::Tiny
-    return B::svref_2object(\$_[0])->FLAGS & (B::SVp_IOK | B::SVp_NOK)
-            && 0 + $_[0] eq $_[0];
-}
-
-sub _emit_number {
-    my $self = shift;
-    return $self->_emit_plain($_[0]);
-}
-
-# Check whether or not a scalar should be emitted as an plain scalar.
-sub is_valid_plain {
-    my $self = shift;
-    return 0 unless length $_[0];
-    return 0 if $self->quote_numeric_strings and Scalar::Util::looks_like_number($_[0]);
-    # refer to YAML::Loader::parse_inline_simple()
-    return 0 if $_[0] =~ /^[\s\{\[\~\`\'\"\!\@\#\>\|\%\&\?\*\^]/;
-    return 0 if $_[0] =~ /[\{\[\]\},]/;
-    return 0 if $_[0] =~ /[:\-\?]\s/;
-    return 0 if $_[0] =~ /\s#/;
-    return 0 if $_[0] =~ /\:(\s|$)/;
-    return 0 if $_[0] =~ /[\s\|\>]$/;
-    return 0 if $_[0] eq '-';
-    return 1;
-}
-
-sub _emit_block {
-    my $self = shift;
-    my ($indicator, $value) = @_;
-    $self->{stream} .= $indicator;
-    $value =~ /(\n*)\Z/;
-    my $chomp = length $1 ? (length $1 > 1) ? '+' : '' : '-';
-    $value = '~' if not defined $value;
-    $self->{stream} .= $chomp;
-    $self->{stream} .= $self->indent_width if $value =~ /^\s/;
-    $self->{stream} .= $self->indent($value);
-}
-
-# Plain means that the scalar is unquoted.
-sub _emit_plain {
-    my $self = shift;
-    $self->{stream} .= defined $_[0] ? $_[0] : '~';
-}
-
-# Double quoting is for single lined escaped strings.
-sub _emit_double {
-    my $self = shift;
-    (my $escaped = $self->escape($_[0])) =~ s/"/\\"/g;
-    $self->{stream} .= qq{"$escaped"};
-}
-
-# Single quoting is for single lined unescaped strings.
-sub _emit_single {
-    my $self = shift;
-    my $item = shift;
-    $item =~ s{'}{''}g;
-    $self->{stream} .= "'$item'";
-}
-
-#==============================================================================
-# Utility subroutines.
-#==============================================================================
-
-# Indent a scalar to the current indentation level.
-sub indent {
-    my $self = shift;
-    my ($text) = @_;
-    return $text unless length $text;
-    $text =~ s/\n\Z//;
-    my $indent = ' ' x $self->offset->[$self->level];
-    $text =~ s/^/$indent/gm;
-    $text = "\n$text";
-    return $text;
-}
-
-# Escapes for unprintable characters
-my @escapes = qw(\0   \x01 \x02 \x03 \x04 \x05 \x06 \a
-                 \x08 \t   \n   \v   \f   \r   \x0e \x0f
-                 \x10 \x11 \x12 \x13 \x14 \x15 \x16 \x17
-                 \x18 \x19 \x1a \e   \x1c \x1d \x1e \x1f
-                );
-
-# Escape the unprintable characters
-sub escape {
-    my $self = shift;
-    my ($text) = @_;
-    $text =~ s/\\/\\\\/g;
-    $text =~ s/([\x00-\x1f])/$escapes[ord($1)]/ge;
-    return $text;
-}
-
-1;
diff --git a/modules/override/YAML/Dumper/Base.pm b/modules/override/YAML/Dumper/Base.pm
deleted file mode 100644 (file)
index 23db7b1..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-package YAML::Dumper::Base;
-
-use YAML::Mo;
-
-use YAML::Node;
-
-# YAML Dumping options
-has spec_version    => default => sub {'1.0'};
-has indent_width    => default => sub {2};
-has use_header      => default => sub {1};
-has use_version     => default => sub {0};
-has sort_keys       => default => sub {1};
-has anchor_prefix   => default => sub {''};
-has dump_code       => default => sub {0};
-has use_block       => default => sub {0};
-has use_fold        => default => sub {0};
-has compress_series => default => sub {1};
-has inline_series   => default => sub {0};
-has use_aliases     => default => sub {1};
-has purity          => default => sub {0};
-has stringify       => default => sub {0};
-has quote_numeric_strings => default => sub {0};
-
-# Properties
-has stream      => default => sub {''};
-has document    => default => sub {0};
-has transferred => default => sub {{}};
-has id_refcnt   => default => sub {{}};
-has id_anchor   => default => sub {{}};
-has anchor      => default => sub {1};
-has level       => default => sub {0};
-has offset      => default => sub {[]};
-has headless    => default => sub {0};
-has blessed_map => default => sub {{}};
-
-# Global Options are an idea taken from Data::Dumper. Really they are just
-# sugar on top of real OO properties. They make the simple Dump/Load API
-# easy to configure.
-sub set_global_options {
-    my $self = shift;
-    $self->spec_version($YAML::SpecVersion)
-      if defined $YAML::SpecVersion;
-    $self->indent_width($YAML::Indent)
-      if defined $YAML::Indent;
-    $self->use_header($YAML::UseHeader)
-      if defined $YAML::UseHeader;
-    $self->use_version($YAML::UseVersion)
-      if defined $YAML::UseVersion;
-    $self->sort_keys($YAML::SortKeys)
-      if defined $YAML::SortKeys;
-    $self->anchor_prefix($YAML::AnchorPrefix)
-      if defined $YAML::AnchorPrefix;
-    $self->dump_code($YAML::DumpCode || $YAML::UseCode)
-      if defined $YAML::DumpCode or defined $YAML::UseCode;
-    $self->use_block($YAML::UseBlock)
-      if defined $YAML::UseBlock;
-    $self->use_fold($YAML::UseFold)
-      if defined $YAML::UseFold;
-    $self->compress_series($YAML::CompressSeries)
-      if defined $YAML::CompressSeries;
-    $self->inline_series($YAML::InlineSeries)
-      if defined $YAML::InlineSeries;
-    $self->use_aliases($YAML::UseAliases)
-      if defined $YAML::UseAliases;
-    $self->purity($YAML::Purity)
-      if defined $YAML::Purity;
-    $self->stringify($YAML::Stringify)
-      if defined $YAML::Stringify;
-    $self->quote_numeric_strings($YAML::QuoteNumericStrings)
-      if defined $YAML::QuoteNumericStrings;
-}
-
-sub dump {
-    my $self = shift;
-    $self->die('dump() not implemented in this class.');
-}
-
-sub blessed {
-    my $self = shift;
-    my ($ref) = @_;
-    $ref = \$_[0] unless ref $ref;
-    my (undef, undef, $node_id) = YAML::Mo::Object->node_info($ref);
-    $self->{blessed_map}->{$node_id};
-}
-
-sub bless {
-    my $self = shift;
-    my ($ref, $blessing) = @_;
-    my $ynode;
-    $ref = \$_[0] unless ref $ref;
-    my (undef, undef, $node_id) = YAML::Mo::Object->node_info($ref);
-    if (not defined $blessing) {
-        $ynode = YAML::Node->new($ref);
-    }
-    elsif (ref $blessing) {
-        $self->die() unless ynode($blessing);
-        $ynode = $blessing;
-    }
-    else {
-        no strict 'refs';
-        my $transfer = $blessing . "::yaml_dump";
-        $self->die() unless defined &{$transfer};
-        $ynode = &{$transfer}($ref);
-        $self->die() unless ynode($ynode);
-    }
-    $self->{blessed_map}->{$node_id} = $ynode;
-    my $object = ynode($ynode) or $self->die();
-    return $object;
-}
-
-1;
diff --git a/modules/override/YAML/Error.pm b/modules/override/YAML/Error.pm
deleted file mode 100644 (file)
index e855092..0000000
+++ /dev/null
@@ -1,191 +0,0 @@
-package YAML::Error;
-
-use YAML::Mo;
-
-has 'code';
-has 'type' => default => sub {'Error'};
-has 'line';
-has 'document';
-has 'arguments' => default => sub {[]};
-
-my ($error_messages, %line_adjust);
-
-sub format_message {
-    my $self = shift;
-    my $output = 'YAML ' . $self->type . ': ';
-    my $code = $self->code;
-    if ($error_messages->{$code}) {
-        $code = sprintf($error_messages->{$code}, @{$self->arguments});
-    }
-    $output .= $code . "\n";
-
-    $output .= '   Code: ' . $self->code . "\n"
-        if defined $self->code;
-    $output .= '   Line: ' . $self->line . "\n"
-        if defined $self->line;
-    $output .= '   Document: ' . $self->document . "\n"
-        if defined $self->document;
-    return $output;
-}
-
-sub error_messages {
-    $error_messages;
-}
-
-%$error_messages = map {s/^\s+//;$_} split "\n", <<'...';
-YAML_PARSE_ERR_BAD_CHARS
-  Invalid characters in stream. This parser only supports printable ASCII
-YAML_PARSE_ERR_NO_FINAL_NEWLINE
-  Stream does not end with newline character
-YAML_PARSE_ERR_BAD_MAJOR_VERSION
-  Can't parse a %s document with a 1.0 parser
-YAML_PARSE_WARN_BAD_MINOR_VERSION
-  Parsing a %s document with a 1.0 parser
-YAML_PARSE_WARN_MULTIPLE_DIRECTIVES
-  '%s directive used more than once'
-YAML_PARSE_ERR_TEXT_AFTER_INDICATOR
-  No text allowed after indicator
-YAML_PARSE_ERR_NO_ANCHOR
-  No anchor for alias '*%s'
-YAML_PARSE_ERR_NO_SEPARATOR
-  Expected separator '---'
-YAML_PARSE_ERR_SINGLE_LINE
-  Couldn't parse single line value
-YAML_PARSE_ERR_BAD_ANCHOR
-  Invalid anchor
-YAML_DUMP_ERR_INVALID_INDENT
-  Invalid Indent width specified: '%s'
-YAML_LOAD_USAGE
-  usage: YAML::Load($yaml_stream_scalar)
-YAML_PARSE_ERR_BAD_NODE
-  Can't parse node
-YAML_PARSE_ERR_BAD_EXPLICIT
-  Unsupported explicit transfer: '%s'
-YAML_DUMP_USAGE_DUMPCODE
-  Invalid value for DumpCode: '%s'
-YAML_LOAD_ERR_FILE_INPUT
-  Couldn't open %s for input:\n%s
-YAML_DUMP_ERR_FILE_CONCATENATE
-  Can't concatenate to YAML file %s
-YAML_DUMP_ERR_FILE_OUTPUT
-  Couldn't open %s for output:\n%s
-YAML_DUMP_ERR_NO_HEADER
-  With UseHeader=0, the node must be a plain hash or array
-YAML_DUMP_WARN_BAD_NODE_TYPE
-  Can't perform serialization for node type: '%s'
-YAML_EMIT_WARN_KEYS
-  Encountered a problem with 'keys':\n%s
-YAML_DUMP_WARN_DEPARSE_FAILED
-  Deparse failed for CODE reference
-YAML_DUMP_WARN_CODE_DUMMY
-  Emitting dummy subroutine for CODE reference
-YAML_PARSE_ERR_MANY_EXPLICIT
-  More than one explicit transfer
-YAML_PARSE_ERR_MANY_IMPLICIT
-  More than one implicit request
-YAML_PARSE_ERR_MANY_ANCHOR
-  More than one anchor
-YAML_PARSE_ERR_ANCHOR_ALIAS
-  Can't define both an anchor and an alias
-YAML_PARSE_ERR_BAD_ALIAS
-  Invalid alias
-YAML_PARSE_ERR_MANY_ALIAS
-  More than one alias
-YAML_LOAD_ERR_NO_CONVERT
-  Can't convert implicit '%s' node to explicit '%s' node
-YAML_LOAD_ERR_NO_DEFAULT_VALUE
-  No default value for '%s' explicit transfer
-YAML_LOAD_ERR_NON_EMPTY_STRING
-  Only the empty string can be converted to a '%s'
-YAML_LOAD_ERR_BAD_MAP_TO_SEQ
-  Can't transfer map as sequence. Non numeric key '%s' encountered.
-YAML_DUMP_ERR_BAD_GLOB
-  '%s' is an invalid value for Perl glob
-YAML_DUMP_ERR_BAD_REGEXP
-  '%s' is an invalid value for Perl Regexp
-YAML_LOAD_ERR_BAD_MAP_ELEMENT
-  Invalid element in map
-YAML_LOAD_WARN_DUPLICATE_KEY
-  Duplicate map key found. Ignoring.
-YAML_LOAD_ERR_BAD_SEQ_ELEMENT
-  Invalid element in sequence
-YAML_PARSE_ERR_INLINE_MAP
-  Can't parse inline map
-YAML_PARSE_ERR_INLINE_SEQUENCE
-  Can't parse inline sequence
-YAML_PARSE_ERR_BAD_DOUBLE
-  Can't parse double quoted string
-YAML_PARSE_ERR_BAD_SINGLE
-  Can't parse single quoted string
-YAML_PARSE_ERR_BAD_INLINE_IMPLICIT
-  Can't parse inline implicit value '%s'
-YAML_PARSE_ERR_BAD_IMPLICIT
-  Unrecognized implicit value '%s'
-YAML_PARSE_ERR_INDENTATION
-  Error. Invalid indentation level
-YAML_PARSE_ERR_INCONSISTENT_INDENTATION
-  Inconsistent indentation level
-YAML_LOAD_WARN_UNRESOLVED_ALIAS
-  Can't resolve alias *%s
-YAML_LOAD_WARN_NO_REGEXP_IN_REGEXP
-  No 'REGEXP' element for Perl regexp
-YAML_LOAD_WARN_BAD_REGEXP_ELEM
-  Unknown element '%s' in Perl regexp
-YAML_LOAD_WARN_GLOB_NAME
-  No 'NAME' element for Perl glob
-YAML_LOAD_WARN_PARSE_CODE
-  Couldn't parse Perl code scalar: %s
-YAML_LOAD_WARN_CODE_DEPARSE
-  Won't parse Perl code unless $YAML::LoadCode is set
-YAML_EMIT_ERR_BAD_LEVEL
-  Internal Error: Bad level detected
-YAML_PARSE_WARN_AMBIGUOUS_TAB
-  Amibiguous tab converted to spaces
-YAML_LOAD_WARN_BAD_GLOB_ELEM
-  Unknown element '%s' in Perl glob
-YAML_PARSE_ERR_ZERO_INDENT
-  Can't use zero as an indentation width
-YAML_LOAD_WARN_GLOB_IO
-  Can't load an IO filehandle. Yet!!!
-...
-
-%line_adjust = map {($_, 1)}
-  qw(YAML_PARSE_ERR_BAD_MAJOR_VERSION
-     YAML_PARSE_WARN_BAD_MINOR_VERSION
-     YAML_PARSE_ERR_TEXT_AFTER_INDICATOR
-     YAML_PARSE_ERR_NO_ANCHOR
-     YAML_PARSE_ERR_MANY_EXPLICIT
-     YAML_PARSE_ERR_MANY_IMPLICIT
-     YAML_PARSE_ERR_MANY_ANCHOR
-     YAML_PARSE_ERR_ANCHOR_ALIAS
-     YAML_PARSE_ERR_BAD_ALIAS
-     YAML_PARSE_ERR_MANY_ALIAS
-     YAML_LOAD_ERR_NO_CONVERT
-     YAML_LOAD_ERR_NO_DEFAULT_VALUE
-     YAML_LOAD_ERR_NON_EMPTY_STRING
-     YAML_LOAD_ERR_BAD_MAP_TO_SEQ
-     YAML_LOAD_ERR_BAD_STR_TO_INT
-     YAML_LOAD_ERR_BAD_STR_TO_DATE
-     YAML_LOAD_ERR_BAD_STR_TO_TIME
-     YAML_LOAD_WARN_DUPLICATE_KEY
-     YAML_PARSE_ERR_INLINE_MAP
-     YAML_PARSE_ERR_INLINE_SEQUENCE
-     YAML_PARSE_ERR_BAD_DOUBLE
-     YAML_PARSE_ERR_BAD_SINGLE
-     YAML_PARSE_ERR_BAD_INLINE_IMPLICIT
-     YAML_PARSE_ERR_BAD_IMPLICIT
-     YAML_LOAD_WARN_NO_REGEXP_IN_REGEXP
-     YAML_LOAD_WARN_BAD_REGEXP_ELEM
-     YAML_LOAD_WARN_REGEXP_CREATE
-     YAML_LOAD_WARN_GLOB_NAME
-     YAML_LOAD_WARN_PARSE_CODE
-     YAML_LOAD_WARN_CODE_DEPARSE
-     YAML_LOAD_WARN_BAD_GLOB_ELEM
-     YAML_PARSE_ERR_ZERO_INDENT
-    );
-
-package YAML::Warning;
-
-our @ISA = 'YAML::Error';
-
-1;
diff --git a/modules/override/YAML/Loader.pm b/modules/override/YAML/Loader.pm
deleted file mode 100644 (file)
index 2cef54e..0000000
+++ /dev/null
@@ -1,756 +0,0 @@
-package YAML::Loader;
-
-use YAML::Mo;
-extends 'YAML::Loader::Base';
-
-use YAML::Loader::Base;
-use YAML::Types;
-
-# Context constants
-use constant LEAF       => 1;
-use constant COLLECTION => 2;
-use constant VALUE      => "\x07YAML\x07VALUE\x07";
-use constant COMMENT    => "\x07YAML\x07COMMENT\x07";
-
-# Common YAML character sets
-my $ESCAPE_CHAR = '[\\x00-\\x08\\x0b-\\x0d\\x0e-\\x1f]';
-my $FOLD_CHAR   = '>';
-my $LIT_CHAR    = '|';
-my $LIT_CHAR_RX = "\\$LIT_CHAR";
-
-sub load {
-    my $self = shift;
-    $self->stream($_[0] || '');
-    return $self->_parse();
-}
-
-# Top level function for parsing. Parse each document in order and
-# handle processing for YAML headers.
-sub _parse {
-    my $self = shift;
-    my (%directives, $preface);
-    $self->{stream} =~ s|\015\012|\012|g;
-    $self->{stream} =~ s|\015|\012|g;
-    $self->line(0);
-    $self->die('YAML_PARSE_ERR_BAD_CHARS')
-      if $self->stream =~ /$ESCAPE_CHAR/;
-#     $self->die('YAML_PARSE_ERR_NO_FINAL_NEWLINE')
-    $self->{stream} .= "\n"
-      if length($self->stream) and
-         $self->{stream} !~ s/(.)\n\Z/$1/s;
-    $self->lines([split /\x0a/, $self->stream, -1]);
-    $self->line(1);
-    # Throw away any comments or blanks before the header (or start of
-    # content for headerless streams)
-    $self->_parse_throwaway_comments();
-    $self->document(0);
-    $self->documents([]);
-    # Add an "assumed" header if there is no header and the stream is
-    # not empty (after initial throwaways).
-    if (not $self->eos) {
-        if ($self->lines->[0] !~ /^---(\s|$)/) {
-            unshift @{$self->lines}, '---';
-            $self->{line}--;
-        }
-    }
-
-    # Main Loop. Parse out all the top level nodes and return them.
-    while (not $self->eos) {
-        $self->anchor2node({});
-        $self->{document}++;
-        $self->done(0);
-        $self->level(0);
-        $self->offset->[0] = -1;
-
-        if ($self->lines->[0] =~ /^---\s*(.*)$/) {
-            my @words = split /\s+/, $1;
-            %directives = ();
-            while (@words && $words[0] =~ /^#(\w+):(\S.*)$/) {
-                my ($key, $value) = ($1, $2);
-                shift(@words);
-                if (defined $directives{$key}) {
-                    $self->warn('YAML_PARSE_WARN_MULTIPLE_DIRECTIVES',
-                      $key, $self->document);
-                    next;
-                }
-                $directives{$key} = $value;
-            }
-            $self->preface(join ' ', @words);
-        }
-        else {
-            $self->die('YAML_PARSE_ERR_NO_SEPARATOR');
-        }
-
-        if (not $self->done) {
-            $self->_parse_next_line(COLLECTION);
-        }
-        if ($self->done) {
-            $self->{indent} = -1;
-            $self->content('');
-        }
-
-        $directives{YAML} ||= '1.0';
-        $directives{TAB} ||= 'NONE';
-        ($self->{major_version}, $self->{minor_version}) =
-          split /\./, $directives{YAML}, 2;
-        $self->die('YAML_PARSE_ERR_BAD_MAJOR_VERSION', $directives{YAML})
-          if $self->major_version ne '1';
-        $self->warn('YAML_PARSE_WARN_BAD_MINOR_VERSION', $directives{YAML})
-          if $self->minor_version ne '0';
-        $self->die('Unrecognized TAB policy')
-          unless $directives{TAB} =~ /^(NONE|\d+)(:HARD)?$/;
-
-        push @{$self->documents}, $self->_parse_node();
-    }
-    return wantarray ? @{$self->documents} : $self->documents->[-1];
-}
-
-# This function is the dispatcher for parsing each node. Every node
-# recurses back through here. (Inlines are an exception as they have
-# their own sub-parser.)
-sub _parse_node {
-    my $self = shift;
-    my $preface = $self->preface;
-    $self->preface('');
-    my ($node, $type, $indicator, $escape, $chomp) = ('') x 5;
-    my ($anchor, $alias, $explicit, $implicit, $class) = ('') x 5;
-    ($anchor, $alias, $explicit, $implicit, $preface) =
-      $self->_parse_qualifiers($preface);
-    if ($anchor) {
-        $self->anchor2node->{$anchor} = CORE::bless [], 'YAML-anchor2node';
-    }
-    $self->inline('');
-    while (length $preface) {
-        my $line = $self->line - 1;
-        if ($preface =~ s/^($FOLD_CHAR|$LIT_CHAR_RX)(-|\+)?\d*\s*//) {
-            $indicator = $1;
-            $chomp = $2 if defined($2);
-        }
-        else {
-            $self->die('YAML_PARSE_ERR_TEXT_AFTER_INDICATOR') if $indicator;
-            $self->inline($preface);
-            $preface = '';
-        }
-    }
-    if ($alias) {
-        $self->die('YAML_PARSE_ERR_NO_ANCHOR', $alias)
-          unless defined $self->anchor2node->{$alias};
-        if (ref($self->anchor2node->{$alias}) ne 'YAML-anchor2node') {
-            $node = $self->anchor2node->{$alias};
-        }
-        else {
-            $node = do {my $sv = "*$alias"};
-            push @{$self->anchor2node->{$alias}}, [\$node, $self->line];
-        }
-    }
-    elsif (length $self->inline) {
-        $node = $self->_parse_inline(1, $implicit, $explicit);
-        if (length $self->inline) {
-            $self->die('YAML_PARSE_ERR_SINGLE_LINE');
-        }
-    }
-    elsif ($indicator eq $LIT_CHAR) {
-        $self->{level}++;
-        $node = $self->_parse_block($chomp);
-        $node = $self->_parse_implicit($node) if $implicit;
-        $self->{level}--;
-    }
-    elsif ($indicator eq $FOLD_CHAR) {
-        $self->{level}++;
-        $node = $self->_parse_unfold($chomp);
-        $node = $self->_parse_implicit($node) if $implicit;
-        $self->{level}--;
-    }
-    else {
-        $self->{level}++;
-        $self->offset->[$self->level] ||= 0;
-        if ($self->indent == $self->offset->[$self->level]) {
-            if ($self->content =~ /^-( |$)/) {
-                $node = $self->_parse_seq($anchor);
-            }
-            elsif ($self->content =~ /(^\?|\:( |$))/) {
-                $node = $self->_parse_mapping($anchor);
-            }
-            elsif ($preface =~ /^\s*$/) {
-                $node = $self->_parse_implicit('');
-            }
-            else {
-                $self->die('YAML_PARSE_ERR_BAD_NODE');
-            }
-        }
-        else {
-            $node = undef;
-        }
-        $self->{level}--;
-    }
-    $#{$self->offset} = $self->level;
-
-    if ($explicit) {
-        if ($class) {
-            if (not ref $node) {
-                my $copy = $node;
-                undef $node;
-                $node = \$copy;
-            }
-            CORE::bless $node, $class;
-        }
-        else {
-            $node = $self->_parse_explicit($node, $explicit);
-        }
-    }
-    if ($anchor) {
-        if (ref($self->anchor2node->{$anchor}) eq 'YAML-anchor2node') {
-            # XXX Can't remember what this code actually does
-            for my $ref (@{$self->anchor2node->{$anchor}}) {
-                ${$ref->[0]} = $node;
-                $self->warn('YAML_LOAD_WARN_UNRESOLVED_ALIAS',
-                    $anchor, $ref->[1]);
-            }
-        }
-        $self->anchor2node->{$anchor} = $node;
-    }
-    return $node;
-}
-
-# Preprocess the qualifiers that may be attached to any node.
-sub _parse_qualifiers {
-    my $self = shift;
-    my ($preface) = @_;
-    my ($anchor, $alias, $explicit, $implicit, $token) = ('') x 5;
-    $self->inline('');
-    while ($preface =~ /^[&*!]/) {
-        my $line = $self->line - 1;
-        if ($preface =~ s/^\!(\S+)\s*//) {
-            $self->die('YAML_PARSE_ERR_MANY_EXPLICIT') if $explicit;
-            $explicit = $1;
-        }
-        elsif ($preface =~ s/^\!\s*//) {
-            $self->die('YAML_PARSE_ERR_MANY_IMPLICIT') if $implicit;
-            $implicit = 1;
-        }
-        elsif ($preface =~ s/^\&([^ ,:]+)\s*//) {
-            $token = $1;
-            $self->die('YAML_PARSE_ERR_BAD_ANCHOR')
-              unless $token =~ /^[a-zA-Z0-9]+$/;
-            $self->die('YAML_PARSE_ERR_MANY_ANCHOR') if $anchor;
-            $self->die('YAML_PARSE_ERR_ANCHOR_ALIAS') if $alias;
-            $anchor = $token;
-        }
-        elsif ($preface =~ s/^\*([^ ,:]+)\s*//) {
-            $token = $1;
-            $self->die('YAML_PARSE_ERR_BAD_ALIAS')
-              unless $token =~ /^[a-zA-Z0-9]+$/;
-            $self->die('YAML_PARSE_ERR_MANY_ALIAS') if $alias;
-            $self->die('YAML_PARSE_ERR_ANCHOR_ALIAS') if $anchor;
-            $alias = $token;
-        }
-    }
-    return ($anchor, $alias, $explicit, $implicit, $preface);
-}
-
-# Morph a node to it's explicit type
-sub _parse_explicit {
-    my $self = shift;
-    my ($node, $explicit) = @_;
-    my ($type, $class);
-    if ($explicit =~ /^\!?perl\/(hash|array|ref|scalar)(?:\:(\w(\w|\:\:)*)?)?$/) {
-        ($type, $class) = (($1 || ''), ($2 || ''));
-
-        # FIXME # die unless uc($type) eq ref($node) ?
-
-        if ( $type eq "ref" ) {
-            $self->die('YAML_LOAD_ERR_NO_DEFAULT_VALUE', 'XXX', $explicit)
-            unless exists $node->{VALUE()} and scalar(keys %$node) == 1;
-
-            my $value = $node->{VALUE()};
-            $node = \$value;
-        }
-
-        if ( $type eq "scalar" and length($class) and !ref($node) ) {
-            my $value = $node;
-            $node = \$value;
-        }
-
-        if ( length($class) ) {
-            CORE::bless($node, $class);
-        }
-
-        return $node;
-    }
-    if ($explicit =~ m{^!?perl/(glob|regexp|code)(?:\:(\w(\w|\:\:)*)?)?$}) {
-        ($type, $class) = (($1 || ''), ($2 || ''));
-        my $type_class = "YAML::Type::$type";
-        no strict 'refs';
-        if ($type_class->can('yaml_load')) {
-            return $type_class->yaml_load($node, $class, $self);
-        }
-        else {
-            $self->die('YAML_LOAD_ERR_NO_CONVERT', 'XXX', $explicit);
-        }
-    }
-    # This !perl/@Foo and !perl/$Foo are deprecated but still parsed
-    elsif ($YAML::TagClass->{$explicit} ||
-           $explicit =~ m{^perl/(\@|\$)?([a-zA-Z](\w|::)+)$}
-          ) {
-        $class = $YAML::TagClass->{$explicit} || $2;
-        if ($class->can('yaml_load')) {
-            require YAML::Node;
-            return $class->yaml_load(YAML::Node->new($node, $explicit));
-        }
-        else {
-            if (ref $node) {
-                return CORE::bless $node, $class;
-            }
-            else {
-                return CORE::bless \$node, $class;
-            }
-        }
-    }
-    elsif (ref $node) {
-        require YAML::Node;
-        return YAML::Node->new($node, $explicit);
-    }
-    else {
-        # XXX This is likely wrong. Failing test:
-        # --- !unknown 'scalar value'
-        return $node;
-    }
-}
-
-# Parse a YAML mapping into a Perl hash
-sub _parse_mapping {
-    my $self = shift;
-    my ($anchor) = @_;
-    my $mapping = {};
-    $self->anchor2node->{$anchor} = $mapping;
-    my $key;
-    while (not $self->done and $self->indent == $self->offset->[$self->level]) {
-        # If structured key:
-        if ($self->{content} =~ s/^\?\s*//) {
-            $self->preface($self->content);
-            $self->_parse_next_line(COLLECTION);
-            $key = $self->_parse_node();
-            $key = "$key";
-        }
-        # If "default" key (equals sign)
-        elsif ($self->{content} =~ s/^\=\s*//) {
-            $key = VALUE;
-        }
-        # If "comment" key (slash slash)
-        elsif ($self->{content} =~ s/^\=\s*//) {
-            $key = COMMENT;
-        }
-        # Regular scalar key:
-        else {
-            $self->inline($self->content);
-            $key = $self->_parse_inline();
-            $key = "$key";
-            $self->content($self->inline);
-            $self->inline('');
-        }
-
-        unless ($self->{content} =~ s/^:\s*//) {
-            $self->die('YAML_LOAD_ERR_BAD_MAP_ELEMENT');
-        }
-        $self->preface($self->content);
-        my $line = $self->line;
-        $self->_parse_next_line(COLLECTION);
-        my $value = $self->_parse_node();
-        if (exists $mapping->{$key}) {
-            $self->warn('YAML_LOAD_WARN_DUPLICATE_KEY');
-        }
-        else {
-            $mapping->{$key} = $value;
-        }
-    }
-    return $mapping;
-}
-
-# Parse a YAML sequence into a Perl array
-sub _parse_seq {
-    my $self = shift;
-    my ($anchor) = @_;
-    my $seq = [];
-    $self->anchor2node->{$anchor} = $seq;
-    while (not $self->done and $self->indent == $self->offset->[$self->level]) {
-        if ($self->content =~ /^-(?: (.*))?$/) {
-            $self->preface(defined($1) ? $1 : '');
-        }
-        else {
-            $self->die('YAML_LOAD_ERR_BAD_SEQ_ELEMENT');
-        }
-        if ($self->preface =~ /^(\s*)(\w.*\:(?: |$).*)$/) {
-            $self->indent($self->offset->[$self->level] + 2 + length($1));
-            $self->content($2);
-            $self->level($self->level + 1);
-            $self->offset->[$self->level] = $self->indent;
-            $self->preface('');
-            push @$seq, $self->_parse_mapping('');
-            $self->{level}--;
-            $#{$self->offset} = $self->level;
-        }
-        else {
-            $self->_parse_next_line(COLLECTION);
-            push @$seq, $self->_parse_node();
-        }
-    }
-    return $seq;
-}
-
-# Parse an inline value. Since YAML supports inline collections, this is
-# the top level of a sub parsing.
-sub _parse_inline {
-    my $self = shift;
-    my ($top, $top_implicit, $top_explicit) = (@_, '', '', '');
-    $self->{inline} =~ s/^\s*(.*)\s*$/$1/; # OUCH - mugwump
-    my ($node, $anchor, $alias, $explicit, $implicit) = ('') x 5;
-    ($anchor, $alias, $explicit, $implicit, $self->{inline}) =
-      $self->_parse_qualifiers($self->inline);
-    if ($anchor) {
-        $self->anchor2node->{$anchor} = CORE::bless [], 'YAML-anchor2node';
-    }
-    $implicit ||= $top_implicit;
-    $explicit ||= $top_explicit;
-    ($top_implicit, $top_explicit) = ('', '');
-    if ($alias) {
-        $self->die('YAML_PARSE_ERR_NO_ANCHOR', $alias)
-          unless defined $self->anchor2node->{$alias};
-        if (ref($self->anchor2node->{$alias}) ne 'YAML-anchor2node') {
-            $node = $self->anchor2node->{$alias};
-        }
-        else {
-            $node = do {my $sv = "*$alias"};
-            push @{$self->anchor2node->{$alias}}, [\$node, $self->line];
-        }
-    }
-    elsif ($self->inline =~ /^\{/) {
-        $node = $self->_parse_inline_mapping($anchor);
-    }
-    elsif ($self->inline =~ /^\[/) {
-        $node = $self->_parse_inline_seq($anchor);
-    }
-    elsif ($self->inline =~ /^"/) {
-        $node = $self->_parse_inline_double_quoted();
-        $node = $self->_unescape($node);
-        $node = $self->_parse_implicit($node) if $implicit;
-    }
-    elsif ($self->inline =~ /^'/) {
-        $node = $self->_parse_inline_single_quoted();
-        $node = $self->_parse_implicit($node) if $implicit;
-    }
-    else {
-        if ($top) {
-            $node = $self->inline;
-            $self->inline('');
-        }
-        else {
-            $node = $self->_parse_inline_simple();
-        }
-        $node = $self->_parse_implicit($node) unless $explicit;
-    }
-    if ($explicit) {
-        $node = $self->_parse_explicit($node, $explicit);
-    }
-    if ($anchor) {
-        if (ref($self->anchor2node->{$anchor}) eq 'YAML-anchor2node') {
-            for my $ref (@{$self->anchor2node->{$anchor}}) {
-                ${$ref->[0]} = $node;
-                $self->warn('YAML_LOAD_WARN_UNRESOLVED_ALIAS',
-                    $anchor, $ref->[1]);
-            }
-        }
-        $self->anchor2node->{$anchor} = $node;
-    }
-    return $node;
-}
-
-# Parse the inline YAML mapping into a Perl hash
-sub _parse_inline_mapping {
-    my $self = shift;
-    my ($anchor) = @_;
-    my $node = {};
-    $self->anchor2node->{$anchor} = $node;
-
-    $self->die('YAML_PARSE_ERR_INLINE_MAP')
-      unless $self->{inline} =~ s/^\{\s*//;
-    while (not $self->{inline} =~ s/^\s*\}//) {
-        my $key = $self->_parse_inline();
-        $self->die('YAML_PARSE_ERR_INLINE_MAP')
-          unless $self->{inline} =~ s/^\: \s*//;
-        my $value = $self->_parse_inline();
-        if (exists $node->{$key}) {
-            $self->warn('YAML_LOAD_WARN_DUPLICATE_KEY');
-        }
-        else {
-            $node->{$key} = $value;
-        }
-        next if $self->inline =~ /^\s*\}/;
-        $self->die('YAML_PARSE_ERR_INLINE_MAP')
-          unless $self->{inline} =~ s/^\,\s*//;
-    }
-    return $node;
-}
-
-# Parse the inline YAML sequence into a Perl array
-sub _parse_inline_seq {
-    my $self = shift;
-    my ($anchor) = @_;
-    my $node = [];
-    $self->anchor2node->{$anchor} = $node;
-
-    $self->die('YAML_PARSE_ERR_INLINE_SEQUENCE')
-      unless $self->{inline} =~ s/^\[\s*//;
-    while (not $self->{inline} =~ s/^\s*\]//) {
-        my $value = $self->_parse_inline();
-        push @$node, $value;
-        next if $self->inline =~ /^\s*\]/;
-        $self->die('YAML_PARSE_ERR_INLINE_SEQUENCE')
-          unless $self->{inline} =~ s/^\,\s*//;
-    }
-    return $node;
-}
-
-# Parse the inline double quoted string.
-sub _parse_inline_double_quoted {
-    my $self = shift;
-    my $node;
-    # https://rt.cpan.org/Public/Bug/Display.html?id=90593
-    if ($self->inline =~ /^"((?:(?:\\"|[^"]){0,32766}){0,32766})"\s*(.*)$/) {
-        $node = $1;
-        $self->inline($2);
-        $node =~ s/\\"/"/g;
-    }
-    else {
-        $self->die('YAML_PARSE_ERR_BAD_DOUBLE');
-    }
-    return $node;
-}
-
-
-# Parse the inline single quoted string.
-sub _parse_inline_single_quoted {
-    my $self = shift;
-    my $node;
-    if ($self->inline =~ /^'((?:(?:''|[^']){0,32766}){0,32766})'\s*(.*)$/) {
-        $node = $1;
-        $self->inline($2);
-        $node =~ s/''/'/g;
-    }
-    else {
-        $self->die('YAML_PARSE_ERR_BAD_SINGLE');
-    }
-    return $node;
-}
-
-# Parse the inline unquoted string and do implicit typing.
-sub _parse_inline_simple {
-    my $self = shift;
-    my $value;
-    if ($self->inline =~ /^(|[^!@#%^&*].*?)(?=[\[\]\{\},]|, |: |- |:\s*$|$)/) {
-        $value = $1;
-        substr($self->{inline}, 0, length($1)) = '';
-    }
-    else {
-        $self->die('YAML_PARSE_ERR_BAD_INLINE_IMPLICIT', $value);
-    }
-    return $value;
-}
-
-sub _parse_implicit {
-    my $self = shift;
-    my ($value) = @_;
-    $value =~ s/\s*$//;
-    return $value if $value eq '';
-    return undef if $value =~ /^~$/;
-    return $value
-      unless $value =~ /^[\@\`]/ or
-             $value =~ /^[\-\?]\s/;
-    $self->die('YAML_PARSE_ERR_BAD_IMPLICIT', $value);
-}
-
-# Unfold a YAML multiline scalar into a single string.
-sub _parse_unfold {
-    my $self = shift;
-    my ($chomp) = @_;
-    my $node = '';
-    my $space = 0;
-    while (not $self->done and $self->indent == $self->offset->[$self->level]) {
-        $node .= $self->content. "\n";
-        $self->_parse_next_line(LEAF);
-    }
-    $node =~ s/^(\S.*)\n(?=\S)/$1 /gm;
-    $node =~ s/^(\S.*)\n(\n+\S)/$1$2/gm;
-    $node =~ s/\n*\Z// unless $chomp eq '+';
-    $node .= "\n" unless $chomp;
-    return $node;
-}
-
-# Parse a YAML block style scalar. This is like a Perl here-document.
-sub _parse_block {
-    my $self = shift;
-    my ($chomp) = @_;
-    my $node = '';
-    while (not $self->done and $self->indent == $self->offset->[$self->level]) {
-        $node .= $self->content . "\n";
-        $self->_parse_next_line(LEAF);
-    }
-    return $node if '+' eq $chomp;
-    $node =~ s/\n*\Z/\n/;
-    $node =~ s/\n\Z// if $chomp eq '-';
-    return $node;
-}
-
-# Handle Perl style '#' comments. Comments must be at the same indentation
-# level as the collection line following them.
-sub _parse_throwaway_comments {
-    my $self = shift;
-    while (@{$self->lines} and
-           $self->lines->[0] =~ m{^\s*(\#|$)}
-          ) {
-        shift @{$self->lines};
-        $self->{line}++;
-    }
-    $self->eos($self->{done} = not @{$self->lines});
-}
-
-# This is the routine that controls what line is being parsed. It gets called
-# once for each line in the YAML stream.
-#
-# This routine must:
-# 1) Skip past the current line
-# 2) Determine the indentation offset for a new level
-# 3) Find the next _content_ line
-#   A) Skip over any throwaways (Comments/blanks)
-#   B) Set $self->indent, $self->content, $self->line
-# 4) Expand tabs appropriately
-sub _parse_next_line {
-    my $self = shift;
-    my ($type) = @_;
-    my $level = $self->level;
-    my $offset = $self->offset->[$level];
-    $self->die('YAML_EMIT_ERR_BAD_LEVEL') unless defined $offset;
-    shift @{$self->lines};
-    $self->eos($self->{done} = not @{$self->lines});
-    return if $self->eos;
-    $self->{line}++;
-
-    # Determine the offset for a new leaf node
-    if ($self->preface =~
-        qr/(?:^|\s)(?:$FOLD_CHAR|$LIT_CHAR_RX)(?:-|\+)?(\d*)\s*$/
-       ) {
-        $self->die('YAML_PARSE_ERR_ZERO_INDENT')
-          if length($1) and $1 == 0;
-        $type = LEAF;
-        if (length($1)) {
-            $self->offset->[$level + 1] = $offset + $1;
-        }
-        else {
-            # First get rid of any comments.
-            while (@{$self->lines} && ($self->lines->[0] =~ /^\s*#/)) {
-                $self->lines->[0] =~ /^( *)/;
-                last unless length($1) <= $offset;
-                shift @{$self->lines};
-                $self->{line}++;
-            }
-            $self->eos($self->{done} = not @{$self->lines});
-            return if $self->eos;
-            if ($self->lines->[0] =~ /^( *)\S/ and length($1) > $offset) {
-                $self->offset->[$level+1] = length($1);
-            }
-            else {
-                $self->offset->[$level+1] = $offset + 1;
-            }
-        }
-        $offset = $self->offset->[++$level];
-    }
-    # Determine the offset for a new collection level
-    elsif ($type == COLLECTION and
-           $self->preface =~ /^(\s*(\!\S*|\&\S+))*\s*$/) {
-        $self->_parse_throwaway_comments();
-        if ($self->eos) {
-            $self->offset->[$level+1] = $offset + 1;
-            return;
-        }
-        else {
-            $self->lines->[0] =~ /^( *)\S/ or
-                $self->die('YAML_PARSE_ERR_NONSPACE_INDENTATION');
-            if (length($1) > $offset) {
-                $self->offset->[$level+1] = length($1);
-            }
-            else {
-                $self->offset->[$level+1] = $offset + 1;
-            }
-        }
-        $offset = $self->offset->[++$level];
-    }
-
-    if ($type == LEAF) {
-        while (@{$self->lines} and
-               $self->lines->[0] =~ m{^( *)(\#)} and
-               length($1) < $offset
-              ) {
-            shift @{$self->lines};
-            $self->{line}++;
-        }
-        $self->eos($self->{done} = not @{$self->lines});
-    }
-    else {
-        $self->_parse_throwaway_comments();
-    }
-    return if $self->eos;
-
-    if ($self->lines->[0] =~ /^---(\s|$)/) {
-        $self->done(1);
-        return;
-    }
-    if ($type == LEAF and
-        $self->lines->[0] =~ /^ {$offset}(.*)$/
-       ) {
-        $self->indent($offset);
-        $self->content($1);
-    }
-    elsif ($self->lines->[0] =~ /^\s*$/) {
-        $self->indent($offset);
-        $self->content('');
-    }
-    else {
-        $self->lines->[0] =~ /^( *)(\S.*)$/;
-        while ($self->offset->[$level] > length($1)) {
-            $level--;
-        }
-        $self->die('YAML_PARSE_ERR_INCONSISTENT_INDENTATION')
-          if $self->offset->[$level] != length($1);
-        $self->indent(length($1));
-        $self->content($2);
-    }
-    $self->die('YAML_PARSE_ERR_INDENTATION')
-      if $self->indent - $offset > 1;
-}
-
-#==============================================================================
-# Utility subroutines.
-#==============================================================================
-
-# Printable characters for escapes
-my %unescapes = (
-   0 => "\x00",
-   a => "\x07",
-   t => "\x09",
-   n => "\x0a",
-   'v' => "\x0b", # Potential v-string error on 5.6.2 if not quoted
-   f => "\x0c",
-   r => "\x0d",
-   e => "\x1b",
-   '\\' => '\\',
-  );
-
-# Transform all the backslash style escape characters to their literal meaning
-sub _unescape {
-    my $self = shift;
-    my ($node) = @_;
-    $node =~ s/\\([never\\fart0]|x([0-9a-fA-F]{2}))/
-              (length($1)>1)?pack("H2",$2):$unescapes{$1}/gex;
-    return $node;
-}
-
-1;
diff --git a/modules/override/YAML/Loader/Base.pm b/modules/override/YAML/Loader/Base.pm
deleted file mode 100644 (file)
index 6a3504c..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-package YAML::Loader::Base;
-
-use YAML::Mo;
-
-has load_code     => default => sub {0};
-has stream        => default => sub {''};
-has document      => default => sub {0};
-has line          => default => sub {0};
-has documents     => default => sub {[]};
-has lines         => default => sub {[]};
-has eos           => default => sub {0};
-has done          => default => sub {0};
-has anchor2node   => default => sub {{}};
-has level         => default => sub {0};
-has offset        => default => sub {[]};
-has preface       => default => sub {''};
-has content       => default => sub {''};
-has indent        => default => sub {0};
-has major_version => default => sub {0};
-has minor_version => default => sub {0};
-has inline        => default => sub {''};
-
-sub set_global_options {
-    my $self = shift;
-    $self->load_code($YAML::LoadCode || $YAML::UseCode)
-      if defined $YAML::LoadCode or defined $YAML::UseCode;
-}
-
-sub load {
-    die 'load() not implemented in this class.';
-}
-
-1;
diff --git a/modules/override/YAML/Marshall.pm b/modules/override/YAML/Marshall.pm
deleted file mode 100644 (file)
index 14d378b..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-use strict; use warnings;
-package YAML::Marshall;
-
-use YAML::Node ();
-
-sub import {
-    my $class = shift;
-    no strict 'refs';
-    my $package = caller;
-    unless (grep { $_ eq $class} @{$package . '::ISA'}) {
-        push @{$package . '::ISA'}, $class;
-    }
-
-    my $tag = shift;
-    if ( $tag ) {
-        no warnings 'once';
-        $YAML::TagClass->{$tag} = $package;
-        ${$package . "::YamlTag"} = $tag;
-    }
-}
-
-sub yaml_dump {
-    my $self = shift;
-    no strict 'refs';
-    my $tag = ${ref($self) . "::YamlTag"} || 'perl/' . ref($self);
-    $self->yaml_node($self, $tag);
-}
-
-sub yaml_load {
-    my ($class, $node) = @_;
-    if (my $ynode = $class->yaml_ynode($node)) {
-        $node = $ynode->{NODE};
-    }
-    bless $node, $class;
-}
-
-sub yaml_node {
-    shift;
-    YAML::Node->new(@_);
-}
-
-sub yaml_ynode {
-    shift;
-    YAML::Node::ynode(@_);
-}
-
-1;
diff --git a/modules/override/YAML/Mo.pm b/modules/override/YAML/Mo.pm
deleted file mode 100644 (file)
index c669ff0..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-package YAML::Mo; $VERSION = '0.88';
-# use Mo qw[builder default import];
-#   The following line of code was produced from the previous line by
-#   Mo::Inline version 0.31
-no warnings;my$M=__PACKAGE__.'::';*{$M.Object::new}=sub{bless{@_[1..$#_]},$_[0]};*{$M.import}=sub{import warnings;$^H|=1538;my($P,%e,%o)=caller.'::';shift;eval"no Mo::$_",&{$M.$_.::e}($P,\%e,\%o,\@_)for@_;return if$e{M};%e=(extends,sub{eval"no $_[0]()";@{$P.ISA}=$_[0]},has,sub{my$n=shift;my$m=sub{$#_?$_[0]{$n}=$_[1]:$_[0]{$n}};$m=$o{$_}->($m,$n,@_)for sort keys%o;*{$P.$n}=$m},%e,);*{$P.$_}=$e{$_}for keys%e;@{$P.ISA}=$M.Object};*{$M.'builder::e'}=sub{my($P,$e,$o)=@_;$o->{builder}=sub{my($m,$n,%a)=@_;my$b=$a{builder}or return$m;sub{$#_?$m->(@_):!exists$_[0]{$n}?$_[0]{$n}=$_[0]->$b:$m->(@_)}}};*{$M.'default::e'}=sub{my($P,$e,$o)=@_;$o->{default}=sub{my($m,$n,%a)=@_;$a{default}or return$m;sub{$#_?$m->(@_):!exists$_[0]{$n}?$_[0]{$n}=$a{default}->(@_):$m->(@_)}}};my$i=\&import;*{$M.import}=sub{(@_==2 and not $_[1])?pop@_:@_==1?push@_,grep!/import/,@f:();goto&$i};@f=qw[builder default import];use strict;use warnings;
-
-our $DumperModule = 'Data::Dumper';
-
-my ($_new_error, $_info, $_scalar_info);
-
-no strict 'refs';
-*{$M.'Object::die'} = sub {
-    my $self = shift;
-    my $error = $self->$_new_error(@_);
-    $error->type('Error');
-    Carp::croak($error->format_message);
-};
-
-*{$M.'Object::warn'} = sub {
-    my $self = shift;
-    return unless $^W;
-    my $error = $self->$_new_error(@_);
-    $error->type('Warning');
-    Carp::cluck($error->format_message);
-};
-
-# This code needs to be refactored to be simpler and more precise, and no,
-# Scalar::Util doesn't DWIM.
-#
-# Can't handle:
-# * blessed regexp
-*{$M.'Object::node_info'} = sub {
-    my $self = shift;
-    my $stringify = $_[1] || 0;
-    my ($class, $type, $id) =
-        ref($_[0])
-        ? $stringify
-          ? &$_info("$_[0]")
-          : do {
-              require overload;
-              my @info = &$_info(overload::StrVal($_[0]));
-              if (ref($_[0]) eq 'Regexp') {
-                  @info[0, 1] = (undef, 'REGEXP');
-              }
-              @info;
-          }
-        : &$_scalar_info($_[0]);
-    ($class, $type, $id) = &$_scalar_info("$_[0]")
-        unless $id;
-    return wantarray ? ($class, $type, $id) : $id;
-};
-
-#-------------------------------------------------------------------------------
-$_info = sub {
-    return (($_[0]) =~ qr{^(?:(.*)\=)?([^=]*)\(([^\(]*)\)$}o);
-};
-
-$_scalar_info = sub {
-    my $id = 'undef';
-    if (defined $_[0]) {
-        \$_[0] =~ /\((\w+)\)$/o or CORE::die();
-        $id = "$1-S";
-    }
-    return (undef, undef, $id);
-};
-
-$_new_error = sub {
-    require Carp;
-    my $self = shift;
-    require YAML::Error;
-
-    my $code = shift || 'unknown error';
-    my $error = YAML::Error->new(code => $code);
-    $error->line($self->line) if $self->can('line');
-    $error->document($self->document) if $self->can('document');
-    $error->arguments([@_]);
-    return $error;
-};
-
-1;
diff --git a/modules/override/YAML/Node.pm b/modules/override/YAML/Node.pm
deleted file mode 100644 (file)
index 81c2727..0000000
+++ /dev/null
@@ -1,218 +0,0 @@
-use strict; use warnings;
-package YAML::Node;
-
-use YAML::Tag;
-require YAML::Mo;
-
-use Exporter;
-our @ISA     = qw(Exporter YAML::Mo::Object);
-our @EXPORT  = qw(ynode);
-
-sub ynode {
-    my $self;
-    if (ref($_[0]) eq 'HASH') {
-        $self = tied(%{$_[0]});
-    }
-    elsif (ref($_[0]) eq 'ARRAY') {
-        $self = tied(@{$_[0]});
-    }
-    elsif (ref(\$_[0]) eq 'GLOB') {
-        $self = tied(*{$_[0]});
-    }
-    else {
-        $self = tied($_[0]);
-    }
-    return (ref($self) =~ /^yaml_/) ? $self : undef;
-}
-
-sub new {
-    my ($class, $node, $tag) = @_;
-    my $self;
-    $self->{NODE} = $node;
-    my (undef, $type) = YAML::Mo::Object->node_info($node);
-    $self->{KIND} = (not defined $type) ? 'scalar' :
-                    ($type eq 'ARRAY') ? 'sequence' :
-                    ($type eq 'HASH') ? 'mapping' :
-                    $class->die("Can't create YAML::Node from '$type'");
-    tag($self, ($tag || ''));
-    if ($self->{KIND} eq 'scalar') {
-        yaml_scalar->new($self, $_[1]);
-        return \ $_[1];
-    }
-    my $package = "yaml_" . $self->{KIND};
-    $package->new($self)
-}
-
-sub node { $_->{NODE} }
-sub kind { $_->{KIND} }
-sub tag {
-    my ($self, $value) = @_;
-    if (defined $value) {
-               $self->{TAG} = YAML::Tag->new($value);
-        return $self;
-    }
-    else {
-       return $self->{TAG};
-    }
-}
-sub keys {
-    my ($self, $value) = @_;
-    if (defined $value) {
-               $self->{KEYS} = $value;
-        return $self;
-    }
-    else {
-       return $self->{KEYS};
-    }
-}
-
-#==============================================================================
-package yaml_scalar;
-
-@yaml_scalar::ISA = qw(YAML::Node);
-
-sub new {
-    my ($class, $self) = @_;
-    tie $_[2], $class, $self;
-}
-
-sub TIESCALAR {
-    my ($class, $self) = @_;
-    bless $self, $class;
-    $self
-}
-
-sub FETCH {
-    my ($self) = @_;
-    $self->{NODE}
-}
-
-sub STORE {
-    my ($self, $value) = @_;
-    $self->{NODE} = $value
-}
-
-#==============================================================================
-package yaml_sequence;
-
-@yaml_sequence::ISA = qw(YAML::Node);
-
-sub new {
-    my ($class, $self) = @_;
-    my $new;
-    tie @$new, $class, $self;
-    $new
-}
-
-sub TIEARRAY {
-    my ($class, $self) = @_;
-    bless $self, $class
-}
-
-sub FETCHSIZE {
-    my ($self) = @_;
-    scalar @{$self->{NODE}};
-}
-
-sub FETCH {
-    my ($self, $index) = @_;
-    $self->{NODE}[$index]
-}
-
-sub STORE {
-    my ($self, $index, $value) = @_;
-    $self->{NODE}[$index] = $value
-}
-
-sub undone {
-    die "Not implemented yet"; # XXX
-}
-
-*STORESIZE = *POP = *PUSH = *SHIFT = *UNSHIFT = *SPLICE = *DELETE = *EXISTS =
-*STORESIZE = *POP = *PUSH = *SHIFT = *UNSHIFT = *SPLICE = *DELETE = *EXISTS =
-*undone; # XXX Must implement before release
-
-#==============================================================================
-package yaml_mapping;
-
-@yaml_mapping::ISA = qw(YAML::Node);
-
-sub new {
-    my ($class, $self) = @_;
-    @{$self->{KEYS}} = sort keys %{$self->{NODE}};
-    my $new;
-    tie %$new, $class, $self;
-    $new
-}
-
-sub TIEHASH {
-    my ($class, $self) = @_;
-    bless $self, $class
-}
-
-sub FETCH {
-    my ($self, $key) = @_;
-    if (exists $self->{NODE}{$key}) {
-        return (grep {$_ eq $key} @{$self->{KEYS}})
-               ? $self->{NODE}{$key} : undef;
-    }
-    return $self->{HASH}{$key};
-}
-
-sub STORE {
-    my ($self, $key, $value) = @_;
-    if (exists $self->{NODE}{$key}) {
-        $self->{NODE}{$key} = $value;
-    }
-    elsif (exists $self->{HASH}{$key}) {
-        $self->{HASH}{$key} = $value;
-    }
-    else {
-        if (not grep {$_ eq $key} @{$self->{KEYS}}) {
-            push(@{$self->{KEYS}}, $key);
-        }
-        $self->{HASH}{$key} = $value;
-    }
-    $value
-}
-
-sub DELETE {
-    my ($self, $key) = @_;
-    my $return;
-    if (exists $self->{NODE}{$key}) {
-        $return = $self->{NODE}{$key};
-    }
-    elsif (exists $self->{HASH}{$key}) {
-        $return = delete $self->{NODE}{$key};
-    }
-    for (my $i = 0; $i < @{$self->{KEYS}}; $i++) {
-        if ($self->{KEYS}[$i] eq $key) {
-            splice(@{$self->{KEYS}}, $i, 1);
-        }
-    }
-    return $return;
-}
-
-sub CLEAR {
-    my ($self) = @_;
-    @{$self->{KEYS}} = ();
-    %{$self->{HASH}} = ();
-}
-
-sub FIRSTKEY {
-    my ($self) = @_;
-    $self->{ITER} = 0;
-    $self->{KEYS}[0]
-}
-
-sub NEXTKEY {
-    my ($self) = @_;
-    $self->{KEYS}[++$self->{ITER}]
-}
-
-sub EXISTS {
-    my ($self, $key) = @_;
-    exists $self->{NODE}{$key}
-}
-
-1;
diff --git a/modules/override/YAML/Tag.pm b/modules/override/YAML/Tag.pm
deleted file mode 100644 (file)
index 57aef46..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-use strict; use warnings;
-package YAML::Tag;
-
-use overload '""' => sub { ${$_[0]} };
-
-sub new {
-    my ($class, $self) = @_;
-    bless \$self, $class
-}
-
-sub short {
-    ${$_[0]}
-}
-
-sub canonical {
-    ${$_[0]}
-}
-
-1;
diff --git a/modules/override/YAML/Types.pm b/modules/override/YAML/Types.pm
deleted file mode 100644 (file)
index 8cbbde2..0000000
+++ /dev/null
@@ -1,235 +0,0 @@
-package YAML::Types;
-
-use YAML::Mo;
-use YAML::Node;
-
-# XXX These classes and their APIs could still use some refactoring,
-# but at least they work for now.
-#-------------------------------------------------------------------------------
-package YAML::Type::blessed;
-
-use YAML::Mo; # XXX
-
-sub yaml_dump {
-    my $self = shift;
-    my ($value) = @_;
-    my ($class, $type) = YAML::Mo::Object->node_info($value);
-    no strict 'refs';
-    my $kind = lc($type) . ':';
-    my $tag = ${$class . '::ClassTag'} ||
-              "!perl/$kind$class";
-    if ($type eq 'REF') {
-        YAML::Node->new(
-            {(&YAML::VALUE, ${$_[0]})}, $tag
-        );
-    }
-    elsif ($type eq 'SCALAR') {
-        $_[1] = $$value;
-        YAML::Node->new($_[1], $tag);
-    }
-    elsif ($type eq 'GLOB') {
-        # blessed glob support is minimal, and will not round-trip
-        # initial aim: to not cause an error
-        return YAML::Type::glob->yaml_dump($value, $tag);
-    } else {
-        YAML::Node->new($value, $tag);
-    }
-}
-
-#-------------------------------------------------------------------------------
-package YAML::Type::undef;
-
-sub yaml_dump {
-    my $self = shift;
-}
-
-sub yaml_load {
-    my $self = shift;
-}
-
-#-------------------------------------------------------------------------------
-package YAML::Type::glob;
-
-sub yaml_dump {
-    my $self = shift;
-    # $_[0] remains as the glob
-    my $tag = pop @_ if 2==@_;
-
-    $tag = '!perl/glob:' unless defined $tag;
-    my $ynode = YAML::Node->new({}, $tag);
-    for my $type (qw(PACKAGE NAME SCALAR ARRAY HASH CODE IO)) {
-        my $value = *{$_[0]}{$type};
-        $value = $$value if $type eq 'SCALAR';
-        if (defined $value) {
-            if ($type eq 'IO') {
-                my @stats = qw(device inode mode links uid gid rdev size
-                               atime mtime ctime blksize blocks);
-                undef $value;
-                $value->{stat} = YAML::Node->new({});
-                if ($value->{fileno} = fileno(*{$_[0]})) {
-                    local $^W;
-                    map {$value->{stat}{shift @stats} = $_} stat(*{$_[0]});
-                    $value->{tell} = tell(*{$_[0]});
-                }
-            }
-            $ynode->{$type} = $value;
-        }
-    }
-    return $ynode;
-}
-
-sub yaml_load {
-    my $self = shift;
-    my ($node, $class, $loader) = @_;
-    my ($name, $package);
-    if (defined $node->{NAME}) {
-        $name = $node->{NAME};
-        delete $node->{NAME};
-    }
-    else {
-        $loader->warn('YAML_LOAD_WARN_GLOB_NAME');
-        return undef;
-    }
-    if (defined $node->{PACKAGE}) {
-        $package = $node->{PACKAGE};
-        delete $node->{PACKAGE};
-    }
-    else {
-        $package = 'main';
-    }
-    no strict 'refs';
-    if (exists $node->{SCALAR}) {
-        *{"${package}::$name"} = \$node->{SCALAR};
-        delete $node->{SCALAR};
-    }
-    for my $elem (qw(ARRAY HASH CODE IO)) {
-        if (exists $node->{$elem}) {
-            if ($elem eq 'IO') {
-                $loader->warn('YAML_LOAD_WARN_GLOB_IO');
-                delete $node->{IO};
-                next;
-            }
-            *{"${package}::$name"} = $node->{$elem};
-            delete $node->{$elem};
-        }
-    }
-    for my $elem (sort keys %$node) {
-        $loader->warn('YAML_LOAD_WARN_BAD_GLOB_ELEM', $elem);
-    }
-    return *{"${package}::$name"};
-}
-
-#-------------------------------------------------------------------------------
-package YAML::Type::code;
-
-my $dummy_warned = 0;
-my $default = '{ "DUMMY" }';
-
-sub yaml_dump {
-    my $self = shift;
-    my $code;
-    my ($dumpflag, $value) = @_;
-    my ($class, $type) = YAML::Mo::Object->node_info($value);
-    my $tag = "!perl/code";
-    $tag .= ":$class" if defined $class;
-    if (not $dumpflag) {
-        $code = $default;
-    }
-    else {
-        bless $value, "CODE" if $class;
-        eval { use B::Deparse };
-        return if $@;
-        my $deparse = B::Deparse->new();
-        eval {
-            local $^W = 0;
-            $code = $deparse->coderef2text($value);
-        };
-        if ($@) {
-            warn YAML::YAML_DUMP_WARN_DEPARSE_FAILED() if $^W;
-            $code = $default;
-        }
-        bless $value, $class if $class;
-        chomp $code;
-        $code .= "\n";
-    }
-    $_[2] = $code;
-    YAML::Node->new($_[2], $tag);
-}
-
-sub yaml_load {
-    my $self = shift;
-    my ($node, $class, $loader) = @_;
-    if ($loader->load_code) {
-        my $code = eval "package main; sub $node";
-        if ($@) {
-            $loader->warn('YAML_LOAD_WARN_PARSE_CODE', $@);
-            return sub {};
-        }
-        else {
-            CORE::bless $code, $class if $class;
-            return $code;
-        }
-    }
-    else {
-        return CORE::bless sub {}, $class if $class;
-        return sub {};
-    }
-}
-
-#-------------------------------------------------------------------------------
-package YAML::Type::ref;
-
-sub yaml_dump {
-    my $self = shift;
-    YAML::Node->new({(&YAML::VALUE, ${$_[0]})}, '!perl/ref')
-}
-
-sub yaml_load {
-    my $self = shift;
-    my ($node, $class, $loader) = @_;
-    $loader->die('YAML_LOAD_ERR_NO_DEFAULT_VALUE', 'ptr')
-      unless exists $node->{&YAML::VALUE};
-    return \$node->{&YAML::VALUE};
-}
-
-#-------------------------------------------------------------------------------
-package YAML::Type::regexp;
-
-# XXX Be sure to handle blessed regexps (if possible)
-sub yaml_dump {
-    die "YAML::Type::regexp::yaml_dump not currently implemented";
-}
-
-use constant _QR_TYPES => {
-    '' => sub { qr{$_[0]} },
-    x => sub { qr{$_[0]}x },
-    i => sub { qr{$_[0]}i },
-    s => sub { qr{$_[0]}s },
-    m => sub { qr{$_[0]}m },
-    ix => sub { qr{$_[0]}ix },
-    sx => sub { qr{$_[0]}sx },
-    mx => sub { qr{$_[0]}mx },
-    si => sub { qr{$_[0]}si },
-    mi => sub { qr{$_[0]}mi },
-    ms => sub { qr{$_[0]}sm },
-    six => sub { qr{$_[0]}six },
-    mix => sub { qr{$_[0]}mix },
-    msx => sub { qr{$_[0]}msx },
-    msi => sub { qr{$_[0]}msi },
-    msix => sub { qr{$_[0]}msix },
-};
-
-sub yaml_load {
-    my $self = shift;
-    my ($node, $class) = @_;
-    return qr{$node} unless $node =~ /^\(\?([\^\-xism]*):(.*)\)\z/s;
-    my ($flags, $re) = ($1, $2);
-    $flags =~ s/-.*//;
-    $flags =~ s/^\^//;
-    my $sub = _QR_TYPES->{$flags} || sub { qr{$_[0]} };
-    my $qr = &$sub($re);
-    bless $qr, $class if length $class;
-    return $qr;
-}
-
-1;
index 0a9a047..cde5b44 100644 (file)
@@ -1,2 +1,9 @@
-Order Allow,Deny
-Deny from all
+<IfModule mod_authz_core.c>
+  # Apache 2.4
+  Require all denied
+</IfModule>
+<IfModule !mod_authz_core.c>
+  # Apache 2.2
+  Order deny,allow
+  Deny from all
+</IfModule>
index a9e6778..75efd8c 100644 (file)
@@ -1,14 +1,19 @@
 [Unit]
 Description=kivitendo background jobs server
+Requires=postgresql.service
+After=postgresql.service
 
 [Service]
 Type=forking
+# Change the user to the one your web server runs as.
+User=www-data
 # Change these two to point to the kivitendo "task_server.pl" location.
 ExecStart=/var/www/kivitendo-erp/scripts/task_server.pl start
 ExecStop=/var/www/kivitendo-erp/scripts/task_server.pl stop
 Restart=always
-Requires=postgresql.service
-After=postgresql.service
+ProtectSystem=full
+ProtectHome=yes
+PrivateTmp=yes
 
 [Install]
 WantedBy=multi-user.target
index 6362075..2efa9b9 100755 (executable)
@@ -39,7 +39,7 @@ fi
 dobudish=$(ls -d doc/build/dobudish* 2> /dev/null)
 
 if [[ -z $dobudish ]] || [[ ! -d ${dobudish} ]]; then
-  echo "There's no dobudish directory inside doc/build."
+  echo "There's no dobudish directory inside doc/build OR more than one file / dir starting with dobudish (hint: zip file downloaded there?)."
   exit 1
 fi
 
index 1d97581..bf4ffaa 100755 (executable)
@@ -2,11 +2,16 @@
 
 use warnings;
 use strict;
+use utf8;
+use open qw(:std :utf8);
 use 5.008;                          # too much magic in here to include perl 5.6
 
 BEGIN {
-  unshift @INC, "modules/override"; # Use our own versions of various modules (e.g. YAML).
-  push    @INC, "modules/fallback"; # Only use our own versions of modules if there's no system version.
+  use FindBin;
+
+  unshift(@INC, $FindBin::Bin . '/../modules/override'); # Use our own versions of various modules (e.g. YAML).
+  push   (@INC, $FindBin::Bin . '/..');
+  push   (@INC, $FindBin::Bin . '/../modules/fallback'); # Only use our own versions of modules if there's no system version.
 }
 
 use Data::Dumper;
@@ -61,6 +66,10 @@ sub execute_code {
 my $repl = Devel::REPL->new;
 $repl->load_plugin($_) for @plugins;
 $repl->load_history($history_file);
+
+binmode($repl->out_fh, 'utf8');
+
+$repl->eval('use utf8;');
 $repl->eval('help');
 $repl->print("trying to auto login into client '$client' with login '$login'...\n");
 execute_code($repl, "lxinit '$client', '$login'");
@@ -118,6 +127,7 @@ sub lxinit {
   die "cannot find user $login"            unless %::myconfig = $::auth->read_user(login => $login);
 
   die "cannot find locale for user $login" unless $::locale   = Locale->new($::myconfig{countrycode});
+  $::myconfig{login} = $login; # so SL::DB::Manager::Employee->current works in test database
 
   $::instance_conf->init;
 
@@ -287,6 +297,12 @@ Print the manual page and exit.
 Log in as C<username>. The default is to use the value from the
 configuration file and C<demo> if none is set there.
 
+=item B<-c>, B<--client>=C<client>
+
+Use the database for client C<client>. C<client> can be a client's
+database ID or its name. The default is to use the value from the
+configuration file.
+
 =item B<-o>, B<--log-file>=C<filename>
 
 Use C<filename> as the log file. The default is to use the value from
diff --git a/scripts/create_tags_file.pl b/scripts/create_tags_file.pl
deleted file mode 100644 (file)
index d7cef8e..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/usr/bin/perl
-#
-########################################################
-#
-# This script creates a 'tags' file in the style of ctags
-# out of the SL/ modules.
-# Tags file is usable in some editors (vim, joe, emacs, ...). 
-# See your editors documentation for more information.
-#
-# (c) Udo Spallek, Aachen
-# Licenced under GNU/GPL.
-#
-########################################################
-
-use Perl::Tags;
-use IO::Dir;
-use Data::Dumper;
-
-use strict;
-use warnings FATAL =>'all';
-use diagnostics;
-
-use Getopt::Long;
-
-my $parse_SL         = 1;
-my $parse_binmozilla = 0;
-GetOptions("sl!"         => \$parse_SL,
-           "pm!"         => \$parse_SL,
-           "binmozilla!" => \$parse_binmozilla,
-           "pl!"         => \$parse_binmozilla,
-           );
-
-my @files = ();
-push @files, grep { /\.pm$/ && s{^}{SL/}gxms         } IO::Dir->new("SL/")->read()          if $parse_SL;
-push @files, grep { /\.pl$/ && s{^}{bin/mozilla/}gxms} IO::Dir->new("bin/mozilla/")->read() if $parse_binmozilla;
-
-#map { s{^}{SL\/}gxms } @files;
-
-#print Dumper(@files);
-
-#__END__
-my $naive_tagger = Perl::Tags::Naive->new( max_level=>1 );
-$naive_tagger->process(
-         files => [@files],
-         refresh=>1 
-);
-
-my $tagsfile="tags";
-
-# of course, it may not even output, for example, if there's nothing new to process
-$naive_tagger->output( outfile => $tagsfile );
-
-
-1;
index e8a1991..94e4435 100755 (executable)
@@ -66,7 +66,7 @@ function do_curl {
   #   "settings.apply_buchungsgruppe": Buchungsgruppe wo anwenden:
   #   "never", "all", "missing"
 
-  #   "settings.parts_type": Artikeltyp: "part", "service", "mixed"
+  #   "settings.part_type": Artikeltyp: "part", "service", "mixed"
 
   #   "settings.article_number_policy": Artikel mit existierender
   #   Artikelnummer: "update_prices", "insert_new"
@@ -113,7 +113,19 @@ function do_curl {
   #   Projektnummer (nur, wenn "settings.duplicates" auch gesetzt
   #   ist).
 
+  # Spaltenzuordnungen für Benutzerdefinierte Variablen:
+  #   Beispiel (Achtung, die Reihenfolge ist wichtig):
+
+  #   "mappings[+].from=vm_product_length"
+  #   "mappings[].to=cvar_vm_product_length"
+  #   "mappings[+].from=vm_product_width"
+  #   "mappings[].to=cvar_vm_product_width"
+  #   "mappings[+].from=vm_product_height"
+  #   "mappings[].to=cvar_vm_product_height"
+
   curl \
+    -X 'POST' \
+    -H 'Content-Type:multipart/form-data' \
     --silent --insecure \
     -F 'action=CsvImport/dispatch' \
     -F "${action}=1" \
@@ -127,7 +139,7 @@ function do_curl {
     -F 'settings.default_buchungsgruppe=395' \
     -F 'settings.duplicates=no_check' \
     -F 'settings.numberformat=1.000,00' \
-    -F 'settings.parts_type=part' \
+    -F 'settings.part_type=part' \
     -F 'settings.sellprice_adjustment=0' \
     -F 'settings.sellprice_adjustment_type=percent' \
     -F 'settings.sellprice_places=2' \
@@ -142,10 +154,10 @@ function do_curl {
 tmpf=$(mktemp)
 do_curl 'action_test'  > $tmpf
 
-if grep -q -i 'es wurden.*objekte gefunden, von denen.*' $tmpf; then
+if grep -q -i 'Ihr Import wird verarbeitet' $tmpf; then
   rm $tmpf
   do_curl 'action_import' > $tmpf
-  if grep -i 'von.*objekten wurden importiert' $tmpf ; then
+  if grep -i 'Ihr Import wird verarbeitet' $tmpf ; then
     rm $tmpf
   else
     echo "Import schlug fehl. Ausgabe befindet sich in ${tmpf}"
index 6eed2a0..734b030 100755 (executable)
@@ -1,12 +1,10 @@
 #!/usr/bin/perl
 
 BEGIN {
-  use SL::System::Process;
-  my $exe_dir = SL::System::Process::exe_dir;
+  use FindBin;
 
-  unshift @INC, "${exe_dir}/modules/override"; # Use our own versions of various modules (e.g. YAML).
-  push    @INC, "${exe_dir}/modules/fallback"; # Only use our own versions of modules if there's no system version.
-  unshift @INC, $exe_dir;
+  unshift(@INC, $FindBin::Bin . '/../modules/override'); # Use our own versions of various modules (e.g. YAML).
+  push   (@INC, $FindBin::Bin . '/..');                  # '.' will be removed from @INC soon.
 }
 
 use strict;
index 7b614a8..c3ef1e7 100755 (executable)
@@ -1,16 +1,12 @@
 #!/usr/bin/perl
 
 BEGIN {
-  if (! -d "bin" || ! -d "SL") {
-    print("This tool must be run from the kivitendo ERP base directory.\n");
-    exit(1);
-  }
+  use FindBin;
 
-  unshift @INC, "modules/override"; # Use our own versions of various modules (e.g. YAML).
-  push    @INC, "modules/fallback"; # Only use our own versions of modules if there's no system version.
+  unshift(@INC, $FindBin::Bin . '/../modules/override'); # Use our own versions of various modules (e.g. YAML).
+  push   (@INC, $FindBin::Bin . '/..');
 }
 
-
 use strict;
 use warnings;
 
@@ -44,7 +40,7 @@ use SL::Dispatcher;
 my ($opt_list, $opt_tree, $opt_rtree, $opt_nodeps, $opt_graphviz, $opt_help);
 my ($opt_user, $opt_client, $opt_apply, $opt_applied, $opt_unapplied, $opt_format, $opt_test_utf8);
 my ($opt_dbhost, $opt_dbport, $opt_dbname, $opt_dbuser, $opt_dbpassword, $opt_create, $opt_type);
-my ($opt_description, $opt_encoding, @opt_depends, $opt_auth_db);
+my ($opt_description, @opt_depends, $opt_auth_db);
 
 our (%myconfig, $form, $user, $auth, $locale, $controls, $dbupgrader);
 
@@ -90,7 +86,8 @@ dbupgrade2_tool.pl [options]
                          upgrade file your \$EDITOR will be called with it.
     --apply=tag          Applies the database upgrades \'tag\' and all
                          upgrades it depends on. If \'--apply\' is used
-                         then the option \'--user\' must be used as well.
+                         then the option \'--user\' and \'--client\' must be
+                         used as well. Use \'--apply=ALL\' to apply all.
     --applied            List the applied database upgrades for the
                          database that the user given with \'--user\' uses.
     --unapplied          List the database upgrades that haven\'t been applied
@@ -120,9 +117,6 @@ dbupgrade2_tool.pl [options]
   Options for --create:
     --type               \'sql\' or \'pl\'. Defaults to sql.
     --description        The description field of the generated upgrade.
-    --encoding           Encoding used for the file. Defaults to \'utf8\'.
-                         Note: Your editor will not be told to open the file in
-                         this encoding.
     --depends            Tags of upgrades which this upgrade depends upon.
                          Defaults to the latest stable release upgrade.
                          Multiple values possible.
@@ -270,9 +264,10 @@ sub create_upgrade {
   my $dbupgrader  = $params{dbupgrader};
   my $type        = $params{type}        || 'sql';
   my $description = $params{description} || '';
-  my $encoding    = $params{encoding}    || 'utf-8';
   my @depends     = @{ $params{depends} };
 
+  my $encoding    = 'utf-8';
+
   if (!@depends) {
     my @releases = grep { /^release_/ } keys %$controls;
     @depends = ((sort @releases)[-1]);
@@ -298,7 +293,6 @@ sub create_upgrade {
   print $fh "$comment \@tag: $filename\n";
   print $fh "$comment \@description: $description\n";
   print $fh "$comment \@depends: @depends\n";
-  print $fh "$comment \@encoding: $encoding\n";
 
   if ($type eq 'pl') {
     print $fh "package SL::DBUpgrade2::$filename;\n";
@@ -344,8 +338,7 @@ sub apply_upgrade {
 
   my @upgradescripts = map { $controls->{$_}->{applied} = 0; $controls->{$_} } @order;
 
-  my $dbh            = $opt_auth_db ? connect_auth()->dbconnect : $form->dbconnect_noauto(\%myconfig);
-  $dbh->{AutoCommit} = 0;
+  my $dbh            = $opt_auth_db ? connect_auth()->dbconnect : SL::DB->client->dbh;
 
   $dbh->{PrintWarn}  = 0;
   $dbh->{PrintError} = 0;
@@ -372,12 +365,7 @@ sub apply_upgrade {
 
     # apply upgrade
     print "Applying upgrade $control->{file}\n";
-
-    if ($file_type eq "sql") {
-      $dbupgrader->process_query($dbh, "sql/Pg-upgrade2/$control->{file}", $control);
-    } else {
-      $dbupgrader->process_perl_script($dbh, "sql/Pg-upgrade2/$control->{file}", $control);
-    }
+    $dbupgrader->process_file($dbh, "sql/Pg-upgrade2/$control->{file}", $control);
   }
 
   $dbh->disconnect unless $opt_auth_db;
@@ -413,7 +401,7 @@ sub dump_sql_result {
 sub dump_applied {
   my @results;
 
-  my $dbh            = $opt_auth_db ? connect_auth()->dbconnect : $form->dbconnect_noauto(\%myconfig);
+  my $dbh            = $opt_auth_db ? connect_auth()->dbconnect : SL::DB->client->dbh;
   $dbh->{AutoCommit} = 0;
 
   $dbh->{PrintWarn}  = 0;
@@ -441,7 +429,7 @@ sub dump_applied {
 sub dump_unapplied {
   my @results;
 
-  my $dbh = $opt_auth_db ? connect_auth()->dbconnect : $form->dbconnect_noauto(\%myconfig);
+  my $dbh = $opt_auth_db ? connect_auth()->dbconnect : SL::DB->client->dbh;
 
   $dbh->{PrintWarn}  = 0;
   $dbh->{PrintError} = 0;
@@ -501,7 +489,6 @@ GetOptions("list"         => \$opt_list,
            "applied"      => \$opt_applied,
            "create=s"     => \$opt_create,
            "type=s"       => \$opt_type,
-           "encoding=s"   => \$opt_encoding,
            "description=s" => \$opt_description,
            "depends=s"    => \@opt_depends,
            "unapplied"    => \$opt_unapplied,
@@ -530,7 +517,6 @@ create_upgrade(filename   => $opt_create,
                dbupgrader  => $dbupgrader,
                type        => $opt_type,
                description => $opt_description,
-               encoding    => $opt_encoding,
                depends     => \@opt_depends) if ($opt_create);
 
 if ($opt_client && !connect_auth()->set_client($opt_client)) {
index 3df14f4..b2a3c0d 100755 (executable)
@@ -1,4 +1,12 @@
 #!/usr/bin/perl -l
+
+BEGIN {
+  use FindBin;
+
+  unshift(@INC, $FindBin::Bin . '/../modules/override'); # Use our own versions of various modules (e.g. YAML).
+  push   (@INC, $FindBin::Bin . '/..');                  # '.' will be removed from @INC soon.
+}
+
 use strict;
 #use warnings; # corelist and find throw tons of warnings
 use File::Find;
@@ -23,6 +31,7 @@ my (%uselines, %modules, %supplied, %requires);
    'Rose::DB::Object::Metadata'          => 1,
    'Rose::DB::Object::Helpers'           => 1,
    'Rose::DB::Object::Util'              => 1,
+   'Rose::DB::Object::Constants'         => 1,
   },
   'Rose::Object' => {
     'Rose::Object::MakeMethods::Generic' => 1,
@@ -48,13 +57,24 @@ my (%uselines, %modules, %supplied, %requires);
   },
   'Archive::Zip' => {
     'Archive::Zip::Member'               => 1,
-  }
+  },
+  'HTML::Parser' => {
+    'HTML::Entities'                     => 1,
+  },
+  'URI' => {
+    'URI::Escape'                        => 1,
+  },
+  'File::MimeInfo' => {
+    'File::MimeInfo::Magic'              => 1,
+  },
 );
 
 GetOptions(
   'files-with-match|l' => \ my $l,
 );
 
+chmod($FindBin::Bin . '/..');
+
 find(sub {
   return unless /(\.p[lm]|console)$/;
 
@@ -69,6 +89,7 @@ find(sub {
     next if /SL::/;
     next if /Support::Files/; # our own test support module
     next if /use (warnings|strict|vars|lib|constant|utf8)/;
+    next if /^use (with|the)/;
 
     my ($useline) = m/^use\s+(.*?)$/;
 
@@ -124,7 +145,7 @@ $modules{$_->{name}} ||= { status => 'required' } for @SL::InstallationCheck::re
 $modules{$_->{name}} ||= { status => 'optional' } for @SL::InstallationCheck::optional_modules;
 $modules{$_->{name}} ||= { status => 'developer' } for @SL::InstallationCheck::developer_modules;
 
-# build transitive closure for documented dependancies
+# build transitive closure for documented dependencies
 my $changed = 1;
 while ($changed) {
   $changed = 0;
@@ -202,7 +223,6 @@ find-use
 =head1 EXAMPLE
 
  # perl scipts/find-use.pl
- !missing : Perl::Tags
  !missing : Template::Constants
  !missing : DBI
 
@@ -229,7 +249,7 @@ This module is included in C<modules/*>. Don't worry about it.
 =item required
 
 This module is documented in C<SL:InstallationCheck> to be necessary, or is a
-dependancy of one of these. Everything alright.
+dependency of one of these. Everything alright.
 
 =item !missing
 
index a51c669..f7f9a16 100755 (executable)
@@ -4,10 +4,11 @@ use strict;
 use warnings;
 
 use File::Slurp;
+use FindBin;
 use List::Util qw(first max);
 use Template;
 
-my $rel_dir = (first { -f "${_}/SL/ClientJS.pm" } qw(. ..)) || die "ClientJS.pm not found";
+my $rel_dir = $FindBin::Bin . '/..';
 my @actions;
 
 foreach (read_file("${rel_dir}/SL/ClientJS.pm")) {
@@ -58,6 +59,6 @@ foreach my $action (@actions) {
 
 $output .= sprintf "\n      else\%sconsole.log('Unknown action: ' + action[0]);\n", ' ' x (4 + 2 + 6 + 3 + 4 + 2 + $longest + 1);
 
-my $template = Template->new({ RELATIVE => 1 });
+my $template = Template->new({ ABSOLUTE => 1 });
 $template->process($rel_dir . '/scripts/generate_client_js_actions.tpl', { actions => $output }, $rel_dir . '/js/client_js.js') || die $template->error(), "\n";
 print "js/client_js.js generated automatically.\n";
index 9e21802..467344f 100644 (file)
@@ -5,9 +5,27 @@
 // SL/ClientJS.pm for instructions.
 
 namespace("kivi", function(ns) {
-ns.display_flash = function(type, message) {
+ns.display_flash = function(type, message, noscroll) {
   $('#flash_' + type + '_content').text(message);
   $('#flash_' + type).show();
+  if (!noscroll) {
+    $('#frame-header')[0].scrollIntoView();
+  }
+};
+
+ns.display_flash_detail = function(type, message) {
+  $('#flash_' + type + '_detail').html(message);
+  $('#flash_' + type + '_disp').show();
+};
+
+ns.clear_flash = function(category , timeout) {
+  window.setTimeout(function(){
+    $('#flash_' + category).hide();
+    $('#flash_detail_' + category).hide();
+    $('#flash_' + category + '_disp').hide();
+    $('#flash_' + category + '_content').empty();
+    $('#flash_' + category + '_detail').empty();
+  }, timeout);
 };
 
 ns.eval_json_result = function(data) {
@@ -17,13 +35,19 @@ ns.eval_json_result = function(data) {
   if (data.error)
     return ns.display_flash('error', data.error);
 
-  $(['info', 'warning', 'error']).each(function(idx, category) {
-    $('#flash_' + category).hide();
-    $('#flash_' + category + '_content').empty();
-  });
-
-  if ((data.js || '') != '')
+  if (!data.no_flash_clear) {
+    $(['info', 'warning', 'error']).each(function(idx, category) {
+      $('#flash_' + category).hide();
+      $('#flash_detail_' + category).hide();
+      $('#flash_' + category + '_disp').hide();
+      $('#flash_' + category + '_content').empty();
+      $('#flash_' + category + '_detail').empty();
+    });
+  }
+  if ((data.js || '') !== '')
+    // jshint -W061
     eval(data.js);
+    // jshint +W061
 
   if (data.eval_actions)
     $(data.eval_actions).each(function(idx, action) {
index a38e417..f0471d6 100755 (executable)
@@ -25,7 +25,7 @@ my @images;
 for my $filename (sort @files) {
    my $image = `$identify_bin $filename`;
    if (!defined $image) {
-     warn "warning: could not identify image '$filename'. skpping...";
+     warn "warning: could not identify image '$filename'. skipping...";
      next;
    }
   $image =~ /^(?<filename>\S+) \s (?<type>\S+) \s (?<width>\d+) x (?<height>\d+)/x;
index 43b290a..33fd7ca 100755 (executable)
@@ -1,19 +1,23 @@
 #!/usr/bin/perl -w
 
-use strict;
-use Getopt::Long;
-use Pod::Usage;
-use Term::ANSIColor;
-use Text::Wrap;
 our $master_templates;
 BEGIN {
-  unshift @INC, "modules/override"; # Use our own versions of various modules (e.g. YAML).
-  push    @INC, "modules/fallback"; # Only use our own versions of modules if there's no system version.
+  use FindBin;
+
+  unshift(@INC, $FindBin::Bin . '/../modules/override'); # Use our own versions of various modules (e.g. YAML).
+  push   (@INC, $FindBin::Bin . '/..');                  # '.' will be removed from @INC soon.
 
   # this is a default dir. may be wrong in your installation, change it then
-  $master_templates = './templates/print/';
+  $master_templates = $FindBin::Bin . '/../templates/print/';
 }
 
+use strict;
+use Getopt::Long;
+use List::MoreUtils qw(uniq);
+use Pod::Usage;
+use Term::ANSIColor;
+use Text::Wrap;
+
 unless (eval { require Config::Std; 1 }){
   print STDERR <<EOL ;
 +------------------------------------------------------------------------------+
@@ -54,7 +58,7 @@ GetOptions(
 );
 
 my %install_methods = (
-  apt    => { key => 'debian', install => 'sudo apt-get install', system => "Debian, Ubuntu" },
+  apt    => { key => 'debian', install => 'sudo apt install', system => "Debian, Ubuntu" },
   yum    => { key => 'fedora', install => 'sudo yum install',     system => "RHEL, Fedora, CentOS" },
   zypper => { key => 'suse',   install => 'sudo zypper install',  system => "SLES, openSUSE" },
   cpan   => { key => 'name',   install => "sudo cpan",            system => "CPAN" },
@@ -78,7 +82,7 @@ if ($check{a}) {
 $| = 1;
 
 if (!SL::LxOfficeConf->read(undef, 'may fail')) {
-  print_header('Could not load the config file. If you have dependancies from any features enabled in the configuration these will still show up as optional because of this. Please rerun this script after installing the dependancies needed to load the cofiguration.')
+  print_header('Could not load the config file. If you have dependencies from any features enabled in the configuration these will still show up as optional because of this. Please rerun this script after installing the dependencies needed to load the configuration.')
 } else {
   SL::InstallationCheck::check_for_conditional_dependencies();
 }
@@ -86,6 +90,7 @@ if (!SL::LxOfficeConf->read(undef, 'may fail')) {
 if ($check{r}) {
   print_header('Checking Required Modules');
   check_module($_, required => 1) for @SL::InstallationCheck::required_modules;
+  check_pdfinfo();
 }
 if ($check{o}) {
   print_header('Checking Optional Modules');
@@ -116,7 +121,7 @@ EOL
 
 Standard check done, everything is OK and up to date. Have a look at the --help
 section of this script to see some more advanced checks for developer and
-optional dependancies, as well as LaTeX packages you might need.
+optional dependencies, as well as LaTeX packages you might need.
 EOL
   }
 }
@@ -140,9 +145,28 @@ exit !!@missing_modules;
 sub check_latex {
   my ($res) = check_kpsewhich();
   print_result("Looking for LaTeX kpsewhich", $res);
+
+  # no pdfx -> no zugferd possible
+  my $ret = kpsewhich('template/print/', 'sty', 'pdfx');
+  die "Cannot use pdfx. Please install this package first (debian: apt install texlive-latex-extra)"  if $ret;
+  # check version 2018
+  my $latex = $::lx_office_conf{applications}->{latex} || 'pdflatex';
+  my $pdfx = (system ${latex} . ' --interaction=batchmode "\documentclass{minimal} \RequirePackage{pdfx} \csname @ifpackagelater\endcsname{pdfx}{2018/12/22}{}{\show\relax} \begin{document} \end{document}"');
+
+  print_result ("Looking for pdfx version 2018 or higher", !$pdfx);
+  push @missing_modules, \(name => 'pdfx') if $pdfx;
+
   if ($res) {
     check_template_dir($_) for SL::InstallationCheck::template_dirs($master_templates);
   }
+  print STDERR <<EOL if $pdfx;
++------------------------------------------------------------------------------+
+  Your pdfx version is too old. You cannot use ZuGFeRD or modern (2018+)
+  templates. Please consider using a more recent LaTeX environment.
+  Verify with:
+  pdflatex --interaction=batchmode "\RequirePackage{pdfx}[2018/12/22]"
++------------------------------------------------------------------------------+
+EOL
 }
 
 sub check_template_dir {
@@ -151,7 +175,12 @@ sub check_template_dir {
 
   print_header("Checking LaTeX Dependencies for Master Templates '$dir'");
   kpsewhich($path, 'cls', $_) for SL::InstallationCheck::classes_from_latex($path, '\documentclass');
-  kpsewhich($path, 'sty', $_) for SL::InstallationCheck::classes_from_latex($path, '\usepackage');
+
+  my @sty = sort { $a cmp $b } uniq (
+    SL::InstallationCheck::classes_from_latex($path, '\usepackage'),
+    qw(textcomp ulem embedfile)
+  );
+  kpsewhich($path, 'sty', $_) for @sty;
 }
 
 our $mastertemplate_path = './templates/print/';
@@ -173,10 +202,10 @@ sub kpsewhich {
   $package =~ s/[^-_0-9A-Za-z]//g;
   my $type_desc = $type eq 'cls' ? 'document class' : 'package';
 
-  eval { use String::ShellQuote; 1 } or warn "can't load String::ShellQuote" && return;
-     $dw         = shell_quote $dw;
-  my $e_package  = shell_quote $package;
-  my $e_type     = shell_quote $type;
+  eval { require String::ShellQuote; 1 } or warn "can't load String::ShellQuote" && return;
+     $dw         = String::ShellQuote::shell_quote $dw;
+  my $e_package  = String::ShellQuote::shell_quote $package;
+  my $e_type     = String::ShellQuote::shell_quote $type;
 
   my $exit = system(qq|TEXINPUTS=".:$dw:" kpsewhich $e_package.$e_type > /dev/null|);
   my $res  = $exit > 0 ? 0 : 1;
@@ -197,6 +226,21 @@ EOL
   }
 }
 
+sub check_pdfinfo {
+  my $line = "Looking for pdfinfo executable";
+  my $shell_out = `pdfinfo -v 2>&1 | grep version 2> /dev/null`;
+  my ($label,$vers,$ver_string)  = split / /,$shell_out;
+  if ( $label && $label eq 'pdfinfo' ) {
+    chop $ver_string;
+    print_line($line, $ver_string, 'green');
+  } else {
+    print_line($line, 'not installed','red');
+    my %modinfo = ( debian => 'poppler-utils' );
+    push @missing_modules, \%modinfo;
+
+  }
+}
+
 sub check_module {
   my ($module, %role) = @_;
 
@@ -219,7 +263,7 @@ sub check_module {
       $role{optional} ? 'It is OPTIONAL for kivitendo but RECOMMENDED for improved functionality.'
     : $role{required} ? 'It is NEEDED by kivitendo and must be installed.'
     : $role{devel}    ? 'It is OPTIONAL for kivitendo and only useful for developers.'
-    :                   'It is not listed as a dependancy yet. Please tell this the developers.';
+    :                   'It is not listed as a dependency yet. Please tell this the developers.';
 
   my @source_texts = module_source_texts($module);
   local $" = $/;
@@ -289,7 +333,7 @@ __END__
 
 =head1 NAME
 
-scripts/installation_check.pl - check kivitendo dependancies
+scripts/installation_check.pl - check kivitendo dependencies
 
 =head1 SYNOPSIS
 
@@ -321,11 +365,11 @@ No color output. Helpful to avoid terminal escape problems.
 
 =item C<-d, --devel>
 
-Probe for perl developer dependancies. (Used for console  and tags file)
+Probe for perl developer dependencies. (Used for console  and tags file)
 
 =item C<--no-devel>
 
-Don't probe for perl developer dependancies. (Useful in combination with --all)
+Don't probe for perl developer dependencies. (Useful in combination with --all)
 
 =item C<-h, --help>
 
@@ -357,7 +401,7 @@ Don't probe for LaTeX document classes and packages in master templates. (Useful
 
 =item C<-v. --verbose>
 
-Print additional info for missing dependancies
+Print additional info for missing dependencies
 
 =item C<-i, --install-command>
 
index 2c31f87..1bf35df 100755 (executable)
@@ -1,17 +1,16 @@
 #!/usr/bin/perl
 
-# -n do not include custom_ scripts
-# -v verbose mode, shows progress stuff
-
-# this version of locles processes not only all required .pl files
+# this version of locales processes not only all required .pl files
 # but also all parse_html_templated files.
 
 use utf8;
 use strict;
 
 BEGIN {
-  unshift(@INC, 'modules/override'); # Use our own versions of various modules (e.g. YAML).
-  push   (@INC, 'modules/fallback'); # Only use our own versions of modules if there's no system version.
+  use FindBin;
+
+  unshift(@INC, $FindBin::Bin . '/../modules/override'); # Use our own versions of various modules (e.g. YAML).
+  push   (@INC, $FindBin::Bin . '/..');
 }
 
 use Carp;
@@ -25,14 +24,13 @@ use IO::Dir;
 use List::MoreUtils qw(apply);
 use List::Util qw(first);
 use Pod::Usage;
-use YAML ();
-use YAML::Loader (); # YAML tries to load Y:L at runtime, but can't find it after we chdir'ed
 use SL::DBUpgrade2;
+use SL::System::Process;
+use SL::YAML;
 
 $OUTPUT_AUTOFLUSH = 1;
 
 my $opt_v  = 0;
-my $opt_n  = 0;
 my $opt_c  = 0;
 my $opt_f  = 0;
 my $debug  = 0;
@@ -45,7 +43,7 @@ my $locales_dir  = ".";
 my $bindir       = "$basedir/bin/mozilla";
 my @progdirs     = ( "$basedir/SL" );
 my @menufiles    = glob("${basedir}/menus/*/*");
-my @javascript_dirs = ($basedir .'/js', $basedir .'/templates/webpages');
+my @javascript_dirs = ($basedir .'/js', $basedir .'/templates/webpages', $basedir .'/templates/mobile_webpages');
 my $javascript_output_dir = $basedir .'/js';
 my $submitsearch = qr/type\s*=\s*[\"\']?submit/i;
 our $self        = {};
@@ -53,7 +51,7 @@ our $missing     = {};
 our @lost        = ();
 
 my %ignore_unused_templates = (
-  map { $_ => 1 } qw(ct/testpage.html generic/autocomplete.html oe/periodic_invoices_email.txt part/testpage.html t/render.html t/render.js task_server/failure_notification_email.txt
+  map { $_ => 1 } qw(ct/testpage.html oe/periodic_invoices_email.txt part/testpage.html t/render.html t/render.js task_server/failure_notification_email.txt
                      failed_background_jobs_report/email.txt)
 );
 
@@ -98,7 +96,6 @@ my @customfiles  = grep /_custom/, @bindir_files;
 
 push @progfiles, map { m:^(.+)/([^/]+)$:; [ $2, $1 ] } grep { /\.pm$/ } map { find_files($_) } @progdirs;
 
-# put customized files into @customfiles
 my %dir_h;
 
 my @dbplfiles;
@@ -112,6 +109,18 @@ foreach my $sub_dir ("Pg-upgrade2", "Pg-upgrade2-auth") {
 if (-f "$locales_dir/all") {
   require "$locales_dir/all";
 }
+# load custom translation (more_texts)
+for my $file (glob("${locales_dir}/more/*")) {
+  if (open my $in, "<", "$file") {
+    local $/ = undef;
+    my $code = <$in>;
+    eval($code);
+    close($in);
+    $self->{more_texts_temp}{$_} = $self->{more_texts}{$_} for keys %{ $self->{more_texts} };
+  }
+}
+$self->{more_texts} = delete $self->{more_texts_temp};
+
 if (-f "$locales_dir/missing") {
   require "$locales_dir/missing" ;
   unlink "$locales_dir/missing";
@@ -135,19 +144,26 @@ for my $file_name (grep { /\.(?:js|html)$/i } map({find_files($_)} @javascript_d
 # merge entries to translate with entries from files 'missing' and 'lost'
 merge_texts();
 
-# generate all
+# Generate "all" without translations in more_texts.
+# But keep the ones which are in both old_texts (texts) and more_texts,
+# because this are ones which are overwritten in more_texts for custom usage.
+my %to_keep;
+$to_keep{$_} = 1 for grep { !!$self->{more_texts}{$_} } keys %old_texts;
+my @new_all  = grep { $to_keep{$_} || !$self->{more_texts}{$_} } sort keys %alllocales;
+
 generate_file(
   file      => "$locales_dir/all",
   header    => $ALL_HEADER,
   data_name => '$self->{texts}',
-  data_sub  => sub { _print_line($_, $self->{texts}{$_}, @_) for sort keys %alllocales },
+  data_sub  => sub { _print_line($_, $self->{texts}{$_}, @_) for @new_all },
 );
 
 open(my $js_file, '>:encoding(utf8)', $javascript_output_dir .'/locale/'. $locale .'.js') || die;
 print $js_file 'namespace("kivi").setupLocale({';
 my $first_entry = 1;
 for my $key (sort(keys(%jslocale))) {
-  print $js_file ((!$first_entry ? ',' : '') ."\n". _double_quote($key) .':'. _double_quote($self->{texts}{$key}));
+  my $trans = $self->{more_texts}{$key} // $self->{texts}{$key};
+  print $js_file ((!$first_entry ? ',' : '') ."\n". _double_quote($key) .':'. _double_quote($trans));
   $first_entry = 0;
 }
 print $js_file ("\n");
@@ -164,7 +180,8 @@ close($js_file);
 
 
 # calc and generate missing
-my @new_missing = grep { !$self->{texts}{$_} } sort keys %alllocales;
+# don't add missing ones if we have a translation in more_texts
+my @new_missing = grep { !$self->{more_texts}{$_} && !$self->{texts}{$_} } sort keys %alllocales;
 
 if (@new_missing) {
   if ($opt_c) {
@@ -251,7 +268,6 @@ sub parse_args {
   my ($opt_no_c, $ignore_for_compatiblity);
 
   GetOptions(
-    'no-custom-files' => \$opt_n,
     'check-files'     => \$ignore_for_compatiblity,
     'no-check-files'  => \$opt_no_c,
     'verbose'         => \$opt_v,
@@ -418,23 +434,25 @@ sub scanfile {
 
       # is this a template call?
       if (/(?:parse_html_template2?|render)\s*\(\s*[\"\']([\w\/]+)\s*[\"\']/) {
-        my $new_file_base = "$basedir/templates/webpages/$1.";
+        my $new_file_name = $1;
         if (/parse_html_template2/) {
-          print "E: " . strip_base($file) . " is still using 'parse_html_template2' for " . strip_base("${new_file_base}html") . ".\n";
+          print "E: " . strip_base($file) . " is still using 'parse_html_template2' for $new_file_name.html.\n";
         }
 
         my $found_one = 0;
-        foreach my $ext (qw(html js json)) {
-          my $new_file = "${new_file_base}${ext}";
-          if (-f $new_file) {
-            $cached{$file}{scanh}{$new_file} = 1;
-            print "." if $opt_v;
-            $found_one = 1;
+        for my $space (qw(webpages mobile_webpages)) {
+          for my $ext (qw(html js json)) {
+            my $new_file = "$basedir/templates/$space/$new_file_name.$ext";
+            if (-f $new_file) {
+              $cached{$file}{scanh}{$new_file} = 1;
+              print "." if $opt_v;
+              $found_one = 1;
+            }
           }
         }
 
         if ($opt_c && !$found_one) {
-          print "W: missing HTML template: " . strip_base($new_file_base) . "{html,json,js} (referenced from " . strip_base($file) . ")\n";
+          print "W: missing HTML template: $new_file_name.{html,json,js} (referenced from " . strip_base($file) . ")\n";
         }
       }
 
@@ -517,7 +535,7 @@ sub scanfile {
 sub scanmenu {
   my $file = shift;
 
-  my $menu = YAML::LoadFile($file);
+  my $menu = SL::YAML::LoadFile($file);
 
   for my $node (@$menu) {
     # possible for override files
@@ -533,7 +551,7 @@ sub scandbupgrades {
   # we only need to do this for auth atm, because only auth scripts can include new rights, which are translateable
   my $auth = 1;
 
-  my $dbu = SL::DBUpgrade2->new(auth => $auth, path => '../../sql/Pg-upgrade2-auth');
+  my $dbu = SL::DBUpgrade2->new(auth => $auth, path => SL::System::Process->exe_dir . '/sql/Pg-upgrade2-auth');
 
   for my $upgrade ($dbu->sort_dbupdate_controls) {
     for my $string (@{ $upgrade->{locales} || [] }) {
@@ -551,15 +569,16 @@ sub unescape_template_string {
 }
 
 sub scanhtmlfile {
-  local *IN;
-
-  my $file = shift;
+  my ($file) = @_;
 
   return if defined $cached{$file};
 
+  my $template_space = $file =~ m{templates/(\w+)/} ? $1 : 'webpages';
+
   my %plugins = ( 'loaded' => { }, 'needed' => { } );
 
-  if (!open(IN, '<:encoding(utf8)', $file)) {
+  my $fh;
+  if (!open($fh, '<:encoding(utf8)', $file)) {
     print "E: template file '$file' not found\n";
     return;
   }
@@ -567,7 +586,7 @@ sub scanhtmlfile {
   my $copying  = 0;
   my $issubmit = 0;
   my $text     = "";
-  while (my $line = <IN>) {
+  while (my $line = <$fh>) {
     chomp($line);
 
     while ($line =~ m/\[\%[^\w]*use[^\w]+(\w+)[^\w]*?\%\]/gi) {
@@ -627,13 +646,13 @@ sub scanhtmlfile {
                       ([^\s]+)      # Beliebig viele Nicht-Whitespaces -- Dateiname
                       \.(html|js)   # Endung ".html" oder ".js", ansonsten kann es der Name eines Blocks sein
                      /ix) {
-      my $new_file_name = "$basedir/templates/webpages/$1.$2";
+      my $new_file_name = "$basedir/templates/$template_space/$1.$2";
       $cached{$file}{scanh}{$new_file_name} = 1;
       substr $line, $LAST_MATCH_START[1], $LAST_MATCH_END[0] - $LAST_MATCH_START[0], '';
     }
   }
 
-  close(IN);
+  close($fh);
 
   foreach my $plugin (keys %{ $plugins{needed} }) {
     next if ($plugins{loaded}->{$plugin});
@@ -676,7 +695,7 @@ sub scan_javascript_file {
   close($fh);
 }
 sub search_unused_htmlfiles {
-  my @unscanned_dirs = ('../../templates/webpages');
+  my @unscanned_dirs = ('../../templates/webpages', '../../templates/mobile_webpages');
 
   while (scalar @unscanned_dirs) {
     my $dir = shift @unscanned_dirs;
@@ -697,7 +716,7 @@ sub strip_base {
   my $s =  "$_[0]";             # Create a copy of the string.
 
   $s    =~ s|^../../||;
-  $s    =~ s|templates/webpages/||;
+  $s    =~ s|templates/\w+/||;
 
   return $s;
 }
@@ -756,8 +775,8 @@ locales.pl - Collect strings for translation in kivitendo
 locales.pl [options] lang_code
 
  Options:
-  -n, --no-custom-files  Do not process files whose name contains "_"
-  -c, --check-files      Run extended checks on HTML files
+  -c, --check-files      Run extended checks on HTML files (default)
+  -n, --no-check-files   Do not run extended checks on HTML files
   -f, --filenames        Show the filenames where new strings where found
   -v, --verbose          Be more verbose
   -h, --help             Show this help
@@ -766,15 +785,16 @@ locales.pl [options] lang_code
 
 =over 8
 
-=item B<-n>, B<--no-custom-files>
-
-Do not process files whose name contains "_", e.g. "custom_io.pl".
-
 =item B<-c>, B<--check-files>
 
 Run extended checks on the usage of templates. This can be used to
 discover HTML templates that are never used as well as the usage of
-non-existing HTML templates.
+non-existing HTML templates. This is enabled by default.
+
+=item B<-n>, B<--no-check-files>
+
+Do not run extended checks on the usage of templates. See
+C<--no-check-files>.
 
 =item B<-v>, B<--verbose>
 
old mode 100644 (file)
new mode 100755 (executable)
index f5ec66e..8b4f463
@@ -4,6 +4,9 @@ use strict;
 
 use Pod::Html;
 use File::Find;
+use FindBin;
+
+chdir($FindBin::Bin . '/..');
 
 my $doc_path     = "doc/online";
 #my $pod2html_bin = `which pod2html` or die 'cannot find pod2html on your system';
diff --git a/scripts/pl2tmpl.pl b/scripts/pl2tmpl.pl
deleted file mode 100755 (executable)
index b83e279..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/usr/local/bin/perl -pli.orig
-
-#
-# perlcode -> template converter
-#
-# there's ugly perl generated html in your xy.pl?
-# no problem. copy&paste it into a separate html file, remove 'qq|' and '|;' and use this script to fix most of the rest.
-#
-# use: perl pl2tmpl.pl <file>
-#
-# will save the original file as file.orig
-#
-
-s/\$form->\{(?:"([^}]+)"|([^}]+))\}/[% $+ %]/g;
-s/\| \s* \. \s* \$locale->text \( ' ([^']+) ' \) \s* \. \s* qq\|/<translate>$1<\/translate>/xg;
-s/\| \s* \. \s* \$locale->text \( " ([^"]+) " \) \s* \. \s* qq\|/<translate>$1<\/translate>/xg;
index 54693cb..2ab34cb 100755 (executable)
@@ -3,8 +3,10 @@
 use strict;
 
 BEGIN {
-  unshift @INC, "modules/override"; # Use our own versions of various modules (e.g. YAML).
-  push    @INC, "modules/fallback"; # Only use our own versions of modules if there's no system version.
+  use FindBin;
+
+  unshift(@INC, $FindBin::Bin . '/../modules/override'); # Use our own versions of various modules (e.g. YAML).
+  push   (@INC, $FindBin::Bin . '/..');                  # '.' will be removed from @INC soon.
 }
 
 use CGI qw( -no_xhtml);
@@ -13,7 +15,7 @@ use Data::Dumper;
 use Digest::MD5 qw(md5_hex);
 use English qw( -no_match_vars );
 use Getopt::Long;
-use List::MoreUtils qw(none);
+use List::MoreUtils qw(apply none uniq);
 use List::UtilsBy qw(partition_by);
 use Pod::Usage;
 use Rose::DB::Object 0.809;
@@ -30,6 +32,8 @@ use SL::LxOfficeConf;
 use SL::DB::Helper::ALL;
 use SL::DB::Helper::Mappings;
 
+chdir($FindBin::Bin . '/..');
+
 my %blacklist     = SL::DB::Helper::Mappings->get_blacklist;
 my %package_names = SL::DB::Helper::Mappings->get_package_names;
 
@@ -70,11 +74,16 @@ our %foreign_key_name_map     = (
     orderitems                => { parts_id => 'part', trans_id => 'order', },
     delivery_order_items      => { parts_id => 'part' },
     invoice                   => { parts_id => 'part' },
-    follow_ups                => { created_for_user => 'created_for', created_by => 'created_by', },
+    follow_ups                => { created_for_user => 'created_for_employee', created_by => 'created_by_employee', },
     follow_up_access          => { who => 'with_access', what => 'to_follow_ups_by', },
 
-    periodic_invoices_configs => { oe_id => 'order' },
+    periodic_invoices_configs => { oe_id => 'order', email_recipient_contact_id => 'email_recipient_contact' },
     reconciliation_links      => { acc_trans_id => 'acc_trans' },
+
+    assembly                  => { parts_id => 'part', id => 'assembly_part' },
+    assortment_items          => { parts_id => 'part' },
+
+    dunning                   => { trans_id => 'invoice', fee_interest_ar_id => 'fee_interest_invoice' },
   },
 );
 
@@ -340,6 +349,22 @@ sub usage {
   pod2usage(verbose => 99, sections => 'SYNOPSIS');
 }
 
+sub list_all_tables {
+  my ($db) = @_;
+
+  my @schemas = (undef, uniq apply { s{\..*}{} } grep { m{\.} } keys %{ $package_names{KIVITENDO} });
+  my @tables;
+
+  foreach my $schema (@schemas) {
+    $db->schema($schema);
+    push @tables, map { $schema ? "${schema}.${_}" : $_ } $db->list_tables;
+  }
+
+  $db->schema(undef);
+
+  return @tables;
+}
+
 sub make_tables {
   my %tables_by_domain;
   if ($config{all}) {
@@ -347,7 +372,7 @@ sub make_tables {
 
     foreach my $domain (@domains) {
       my $db  = SL::DB::create(undef, $domain);
-      $tables_by_domain{$domain} = [ grep { my $table = $_; none { $_ eq $table } @{ $blacklist{$domain} } } $db->list_tables ];
+      $tables_by_domain{$domain} = [ grep { my $table = $_; none { $_ eq $table } @{ $blacklist{$domain} } } list_all_tables($db) ];
       $db->disconnect;
     }
 
@@ -406,8 +431,6 @@ sub drop_and_create_test_database {
     $auth_dbh->disconnect;
 
     dbh_do($dbh_template, "DROP DATABASE \"" . $db_cfg->{db} . "\"", message => "Database could not be dropped");
-
-    $::auth->reset;
   }
 
   notice("Creating database");
@@ -442,6 +465,8 @@ sub drop_and_create_test_database {
 
   apply_upgrades(auth => 1, dbh => $dbh);
 
+  $::auth->reset;
+
   notice("Creating client, user, group and employee");
 
   dbh_do($dbh, qq|DELETE FROM auth.clients|);
@@ -465,7 +490,6 @@ sub drop_and_create_test_database {
     signature                => '',
     hide_cvar_search_options => '',
     numberformat             => '1.000,00',
-    vclimit                  => 0,
     favorites                => '',
     copies                   => '',
     menustyle                => 'v3',
diff --git a/scripts/spawn_oo.pl b/scripts/spawn_oo.pl
deleted file mode 100755 (executable)
index d0e8f78..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/usr/bin/perl
-
-BEGIN {
-  unshift @INC, "modules/override"; # Use our own versions of various modules (e.g. YAML).
-  push    @INC, "modules/fallback"; # Only use our own versions of modules if there's no system version.
-}
-
-use DBI;
-use Data::Dumper;
-
-use SL::LXDebug;
-
-use SL::Form;
-use SL::Template;
-
-$sendmail   = "| /usr/sbin/sendmail -t";
-
-$| = 1;
-
-$lxdebug = LXDebug->new();
-
-$form = new Form;
-$form->{"script"} = "oe.pl";
-
-$ENV{'HOME'} = getcwd() . "/$userspath";
-
-my $template = SL::Template::create(type => 'OpenDocument', file_name => '', form => $form, myconfig => \%myconfig, userspath => $userspath);
-
-if (@ARGV && ($ARGV[0] eq "-r")) {
-  system("ps auxww | " .
-         "grep -v awk | " .
-         "awk '/^www-data.*(soffice|Xvfb)/ { print \$2 }' | " .
-         "xargs -r kill");
-  sleep(10);
-}
-
-exit(1) unless ($template->spawn_xvfb());
-exit(2) unless ($template->spawn_openoffice());
-exit(0);
diff --git a/scripts/sync_files_from_backend.pl b/scripts/sync_files_from_backend.pl
new file mode 100755 (executable)
index 0000000..b3b0c2c
--- /dev/null
@@ -0,0 +1,90 @@
+#!/usr/bin/perl
+
+BEGIN {
+  use FindBin;
+
+  if (! -d "bin" || ! -d "SL") {
+    print("This tool must be run from the kivitendo ERP base directory.\n");
+    exit(1);
+  }
+
+  unshift @INC, $FindBin::Bin . '/../modules/override'; # Use our own versions of various modules (e.g. YAML).
+  push    @INC, $FindBin::Bin . '/..';
+}
+
+
+use strict;
+use warnings;
+use utf8;
+use English '-no_match_vars';
+use POSIX qw(setuid setgid);
+use Text::CSV_XS;
+
+use Config::Std;
+use DBI;
+use SL::LXDebug;
+use SL::LxOfficeConf;
+
+use SL::DBUtils;
+use SL::Auth;
+use SL::Form;
+use SL::User;
+use SL::Locale;
+use SL::File;
+use SL::InstanceConfiguration;
+use Getopt::Long;
+use Pod::Usage;
+use Term::ANSIColor;
+
+my %config;
+
+sub parse_args {
+  my ($options) = @_;
+  GetOptions(
+    'client=s'          => \ my $client,
+  );
+
+  $options->{client}   = $client;
+}
+
+sub setup {
+
+  SL::LxOfficeConf->read;
+
+  my $client = $config{client} || $::lx_office_conf{devel}{client};
+
+  if (!$client) {
+    error("No client found in config. Please provide a client:");
+    usage();
+  }
+
+  $::lxdebug      = LXDebug->new();
+  $::locale       = Locale->new("de");
+  $::form         = new Form;
+  $::auth         = SL::Auth->new();
+
+  if (!$::auth->set_client($client)) {
+    error("No client with ID or name '$client' found in config. Please provide a client:");
+    usage();
+  }
+  $::instance_conf = SL::InstanceConfiguration->new;
+  $::instance_conf->init;
+}
+
+sub error {
+  print STDERR colored(shift, 'red'), $/;
+}
+
+sub usage {
+  print STDERR "scripts/sync_files_from_backend.pl --client name-or-id\n" ;
+  exit 1;
+}
+
+parse_args(\%config);
+setup();
+
+SL::File->sync_from_backend( file_type => 'document');
+SL::File->sync_from_backend( file_type => 'attachment');
+SL::File->sync_from_backend( file_type => 'image');
+
+1;
index 661c19a..f6c8f59 100755 (executable)
@@ -1,24 +1,14 @@
 #!/usr/bin/perl
 
-
-use List::MoreUtils qw(any);
-
 use strict;
 
 my $exe_dir;
 
 BEGIN {
   use FindBin;
-  use lib "$FindBin::Bin/..";
-
-  use SL::System::Process;
-  $exe_dir = SL::System::Process::exe_dir;
-
-  unshift @INC, "${exe_dir}/modules/override"; # Use our own versions of various modules (e.g. YAML).
-  push    @INC, "${exe_dir}/modules/fallback"; # Only use our own versions of modules if there's no system version.
-  unshift @INC, $exe_dir;
 
-  chdir($exe_dir) || die "Cannot change directory to ${exe_dir}\n";
+  unshift(@INC, $FindBin::Bin . '/../modules/override'); # Use our own versions of various modules (e.g. YAML).
+  push   (@INC, $FindBin::Bin . '/..');                  # '.' will be removed from @INC soon.
 }
 
 use CGI qw( -no_xhtml);
@@ -26,14 +16,16 @@ use Cwd;
 use Daemon::Generic;
 use Data::Dumper;
 use DateTime;
-use Encode qw();
 use English qw(-no_match_vars);
 use File::Spec;
+use List::MoreUtils qw(any);
 use List::Util qw(first);
-use POSIX qw(setuid setgid);
+use POSIX qw(setlocale setuid setgid);
 use SL::Auth;
+use SL::DBUpgrade2;
+use SL::DB::AuthClient;
 use SL::DB::BackgroundJob;
-use SL::BackgroundJob::ALL;
+use SL::System::Process;
 use SL::Form;
 use SL::Helper::DateTime;
 use SL::InstanceConfiguration;
@@ -41,30 +33,40 @@ use SL::LXDebug;
 use SL::LxOfficeConf;
 use SL::Locale;
 use SL::Mailer;
+use SL::System::Process;
 use SL::System::TaskServer;
 use Template;
 
 our %lx_office_conf;
+our $run_single_job;
 
 sub debug {
   return if !$lx_office_conf{task_server}->{debug};
-  $::lxdebug->message(0, @_);
+  $::lxdebug->message(LXDebug::DEBUG1(), join(' ', "task server:", @_));
+}
+
+sub enabled_clients {
+  return SL::DB::Manager::AuthClient->get_all(where => [ '!task_server_user_id' => undef ]);
 }
 
 sub initialize_kivitendo {
-  chdir $exe_dir;
+  my ($client) = @_;
 
-  my $login  = $lx_office_conf{task_server}->{login};
-  my $client = $lx_office_conf{task_server}->{client};
+  chdir $exe_dir;
 
   package main;
 
-  Form::disconnect_standard_dbh;
   $::lxdebug       = LXDebug->new;
   $::locale        = Locale->new($::lx_office_conf{system}->{language});
   $::form          = Form->new;
   $::auth          = SL::Auth->new;
-  die "No client configured or no client found with the name/ID '$client'" unless $::auth->set_client($client);
+
+  return if !$client;
+
+  $::auth->set_client($client->id);
+
+  $::form->{__ERROR_HANDLER} = sub { die @_ };
+
   $::instance_conf = SL::InstanceConfiguration->new;
   $::request       = SL::Request->new(
     cgi            => CGI->new({}),
@@ -76,15 +78,14 @@ sub initialize_kivitendo {
   $::auth->restore_session;
   $::auth->create_or_refresh_session;
 
+  my $login = $client->task_server_user->login;
+
   die "cannot find user $login"            unless %::myconfig = $::auth->read_user(login => $login);
   die "cannot find locale for user $login" unless $::locale   = Locale->new($::myconfig{countrycode} || $::lx_office_conf{system}->{language});
-
-  $::form->{__ERROR_HANDLER} = sub { die @_ };
 }
 
 sub cleanup_kivitendo {
-  eval { SL::DB::Auth->new->db->dbh->rollback; };
-  eval { SL::DB::BackgroundJob->new->db->dbh->rollback; };
+  eval { SL::DB->client->dbh->rollback; };
 
   $::auth->save_session;
   $::auth->expire_sessions;
@@ -93,7 +94,14 @@ sub cleanup_kivitendo {
   $::form     = undef;
   $::myconfig = ();
   $::request  = undef;
-  Form::disconnect_standard_dbh;
+  $::auth     = undef;
+}
+
+sub clean_before_sleeping {
+  SL::DBConnect::Cache->disconnect_all_and_clear;
+  SL::DB->db_cache->clear;
+
+  File::Temp::cleanup();
 }
 
 sub drop_privileges {
@@ -149,58 +157,149 @@ sub notify_on_failure {
     EVAL_PERL   => 0,
     ABSOLUTE    => 1,
     CACHE_SIZE  => 0,
+    ENCODING    => 'utf8',
   });
 
   return debug("Could not create Template instance") unless $template;
 
   $params{client} = $::auth->client;
 
-  my $body;
-  $template->process($cfg->{email_template}, \%params, \$body);
-
-  Mailer->new(
-    from         => $cfg->{email_from},
-    to           => $email_to,
-    subject      => $cfg->{email_subject},
-    content_type => 'text/plain',
-    charset      => 'utf-8',
-    message      => Encode::decode('utf-8', $body),
-  )->send;
+  eval {
+    my $body;
+    $template->process($cfg->{email_template}, \%params, \$body);
+
+    Mailer->new(
+      from         => $cfg->{email_from},
+      to           => $email_to,
+      subject      => $cfg->{email_subject},
+      content_type => 'text/plain',
+      charset      => 'utf-8',
+      message      => $body,
+    )->send;
+
+    1;
+  } or do {
+    debug("Sending a failure notification failed with an exception: $@");
+  };
 }
 
 sub gd_preconfig {
   my $self = shift;
 
+  # Initialize character type locale to be UTF-8 instead of C:
+  foreach my $locale (qw(de_DE.UTF-8 en_US.UTF-8)) {
+    last if setlocale('LC_CTYPE', $locale);
+  }
+
   SL::LxOfficeConf->read($self->{configfile});
 
-  die "Missing section [task_server] in config file"                 unless $lx_office_conf{task_server};
-  die "Missing key 'login' in section [task_server] in config file"  unless $lx_office_conf{task_server}->{login};
-  die "Missing key 'client' in section [task_server] in config file" unless $lx_office_conf{task_server}->{client};
+  die "Missing section [task_server] in config file" unless $lx_office_conf{task_server};
+
+  if ($lx_office_conf{task_server}->{login} || $lx_office_conf{task_server}->{client}) {
+    print STDERR <<EOT;
+ERROR: The keys 'login' and/or 'client' are still present in the
+section [task_server] in the configuration file. These keys are
+deprecated. You have to configure the clients for which to run the
+task server in the web admin interface.
+
+The task server will refuse to start until the keys have been removed from
+the configuration file.
+EOT
+    exit 2;
+  }
 
-  drop_privileges();
   initialize_kivitendo();
 
+  my $dbupdater_auth = SL::DBUpgrade2->new(form => $::form, auth => 1)->parse_dbupdate_controls;
+  if ($dbupdater_auth->unapplied_upgrade_scripts($::auth->dbconnect)) {
+    print STDERR <<EOT;
+The authentication database requires an upgrade. Please login to
+kivitendo's administration interface in order to apply it. The task
+server cannot start until the upgrade has been applied.
+EOT
+    exit 2;
+  }
+
+  drop_privileges();
+
   return ();
 }
 
-sub gd_run {
-  while (1) {
+sub run_single_job_for_all_clients {
+  initialize_kivitendo();
+
+  my $clients = enabled_clients();
+
+  foreach my $client (@{ $clients }) {
+    debug("Running single job ID $run_single_job for client ID " . $client->id . " (" . $client->name . ")");
+
     my $ok = eval {
-      initialize_kivitendo();
+      initialize_kivitendo($client);
 
-      debug("Retrieving jobs");
+      my $job = SL::DB::Manager::BackgroundJob->find_by(id => $run_single_job);
+
+      if ($job) {
+        debug(" Executing the following job: " . $job->package_name);
+      } else {
+        debug(" No jobs to execute found");
+        next;
+      }
+
+      # Provide fresh global variables in case legacy code modifies
+      # them somehow.
+      initialize_kivitendo($client);
+
+      my $history = $job->run;
+
+      debug("   Executed job " . $job->package_name .
+            "; result: " . (!$history ? "no return value" : $history->has_failed ? "failed" : "succeeded") .
+            ($history && $history->has_failed ? "; error: " . $history->error_col : ""));
+
+      notify_on_failure(history => $history) if $history && $history->has_failed;
+
+      1;
+    };
+
+    if (!$ok) {
+      my $error = $EVAL_ERROR;
+      $::lxdebug->message(LXDebug::WARN(), "Exception during execution: ${error}");
+      notify_on_failure(exception => $error);
+    }
+
+    cleanup_kivitendo();
+  }
+}
+
+sub run_once_for_all_clients {
+  initialize_kivitendo();
+
+  my $clients = enabled_clients();
+
+  foreach my $client (@{ $clients }) {
+    debug("Running for client ID " . $client->id . " (" . $client->name . ")");
+
+    my $ok = eval {
+      initialize_kivitendo($client);
 
       my $jobs = SL::DB::Manager::BackgroundJob->get_all_need_to_run;
 
-      debug("  Found: " . join(' ', map { $_->package_name } @{ $jobs })) if @{ $jobs };
+      if (@{ $jobs }) {
+        debug(" Executing the following jobs: " . join(' ', map { $_->package_name } @{ $jobs }));
+      } else {
+        debug(" No jobs to execute found");
+      }
 
       foreach my $job (@{ $jobs }) {
         # Provide fresh global variables in case legacy code modifies
         # them somehow.
-        initialize_kivitendo();
+        initialize_kivitendo($client);
 
         my $history = $job->run;
 
+        debug("   Executed job " . $job->package_name .
+              "; result: " . (!$history ? "no return value" : $history->has_failed ? "failed" : "succeeded") .
+              ($history && $history->has_failed ? "; error: " . $history->error_col : ""));
+
         notify_on_failure(history => $history) if $history && $history->has_failed;
       }
 
@@ -209,17 +308,39 @@ sub gd_run {
 
     if (!$ok) {
       my $error = $EVAL_ERROR;
-      debug("Exception during execution: ${error}");
+      $::lxdebug->message(LXDebug::WARN(), "Exception during execution: ${error}");
       notify_on_failure(exception => $error);
     }
 
     cleanup_kivitendo();
+  }
+}
+
+sub gd_run {
+  if ($run_single_job) {
+    run_single_job_for_all_clients();
+    return;
+  }
+  $::lxdebug->message(LXDebug::INFO(), "The task server for node " . SL::System::TaskServer::node_id() . " is up and running.");
+
+  while (1) {
+    $SIG{'ALRM'} = 'IGNORE';
+
+    run_once_for_all_clients();
 
     debug("Sleeping");
 
+    clean_before_sleeping();
+
+    if (SL::System::Process::memory_usage_is_too_high()) {
+      debug("Memory usage too high - exiting.");
+      return;
+    }
+
     my $seconds = 60 - (localtime)[0];
     if (!eval {
-      local $SIG{'ALRM'} = sub {
+      $SIG{'ALRM'} = sub {
+        $SIG{'ALRM'} = 'IGNORE';
         debug("Got woken up by SIGALRM");
         die "Alarm!\n"
       };
@@ -231,18 +352,14 @@ sub gd_run {
   }
 }
 
-sub end_of_request {
-  $main::lxdebug->show_backtrace();
-  die <<EOF;
-Job called ::end_of_request()!
-
-This usually indicates success but should not be used by background jobs. A
-backtrace has been logged. Please tell the job author to have a look at it.
-EOF
-
+sub gd_flags_more {
+  return (
+    '--run-job=<id>' => 'Run the single job with the database ID <id> no matter if it is active or when its next execution is supposed to be; the daemon will exit afterwards',
+  );
 }
 
-chdir $exe_dir;
+$exe_dir = SL::System::Process->exe_dir;
+chdir($exe_dir) || die "Cannot change directory to ${exe_dir}\n";
 
 mkdir SL::System::TaskServer::PID_BASE() if !-d SL::System::TaskServer::PID_BASE();
 
@@ -255,6 +372,9 @@ $file = File::Spec->abs2rel(Cwd::abs_path($file), Cwd::abs_path($exe_dir));
 newdaemon(configfile => $file,
           progname   => 'kivitendo-background-jobs',
           pidbase    => SL::System::TaskServer::PID_BASE() . '/',
+          options    => {
+            'run-job=i' => \$run_single_job,
+          },
           );
 
 1;
diff --git a/scripts/templ2t8.pl b/scripts/templ2t8.pl
deleted file mode 100755 (executable)
index f1e5bf6..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/perl -pli.orig
-
-#
-# template converter -> T8 converter
-#
-# wanna get rid of those <translate> tags?
-# no problem. use this script to fix most it.
-#
-# use: perl tmpl2t8.pl <file>
-#
-# will save the original file as file.orig
-#
-s/$/[% USE T8 %]/ if $. == 1;
-s/<translate>([^<]+)<\/translate>/[%- '$1' | \$T8 %]/xg;
index 82375f6..09ef1f8 100644 (file)
@@ -1,5 +1,4 @@
 -- @tag: add_api_token
 -- @description: Feld 'api_token' in 'session' ergänzen
 -- @depends:
--- @charset: utf-8
 ALTER TABLE auth.session ADD COLUMN api_token text;
index 66ecb26..5aebfca 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: add_batch_printing_to_full_access
 -- @description: Gruppe "Vollzugriff" Recht auf Stapeldruck-Menü gewähren
 -- @depends:
--- @charset: utf-8
 DELETE FROM auth.group_rights
 WHERE ("right" = 'batch_printing')
   AND group_id = (
index 6a3c58c..5e61ae4 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: add_master_rights
 -- @description: Rechte in die Datenbank migrieren
 -- @depends: release_3_2_0
--- @charset: utf-8
 -- @locales: Master Data
 -- @locales: Create customers and vendors. Edit all vendors. Edit only customers where salesman equals employee (login)
 -- @locales: Create customers and vendors. Edit all vendors. Edit all customers
diff --git a/sql/Pg-upgrade2-auth/all_drafts_edit.pl b/sql/Pg-upgrade2-auth/all_drafts_edit.pl
new file mode 100644 (file)
index 0000000..f9db7d0
--- /dev/null
@@ -0,0 +1,31 @@
+# @tag: all_drafts_edit
+# @description: Zugriffsrecht auf alle Entwürfe
+# @depends: release_3_4_0 add_master_rights master_rights_position_gaps
+# @locales: Edit all drafts
+# @ignore: 0
+package SL::DBUpgrade2::Auth::all_drafts_edit;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::DBUtils;
+
+sub run {
+  my ($self) = @_;
+
+  $self->db_query("INSERT INTO auth.master_rights (position, name, description) VALUES (?, ?, ?)", bind => $_) for
+    [ 5000, 'all_drafts_edit',   'Edit all drafts'        ];
+
+  my $groups = $main::auth->read_groups();
+
+  foreach my $group (values %{$groups}) {
+    $group->{rights}->{all_drafts_edit} = $group->{rights}->{email_employee_readall};
+    $main::auth->save_group($group);
+  }
+
+  return 1;
+} # end run
+
+1;
diff --git a/sql/Pg-upgrade2-auth/assembly_edit_right.pl b/sql/Pg-upgrade2-auth/assembly_edit_right.pl
new file mode 100644 (file)
index 0000000..49ee305
--- /dev/null
@@ -0,0 +1,29 @@
+# @tag: assembly_edit_right
+# @description: Setzt das Recht Erzeugnisbestandteile editieren, auch nachdem es schon erstmalig erzeugt wurde.
+# @depends: release_3_5_0 master_rights_position_gaps
+# @locales: Always edit assembly items (user can change/delete items even if assemblies are already produced)
+package SL::DBUpgrade2::Auth::assembly_edit_right;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::DBUtils;
+
+sub run {
+  my ($self) = @_;
+
+  $self->db_query("INSERT INTO auth.master_rights (position, name, description) VALUES ( 550, 'assembly_edit', 'Always edit assembly items (user can change/delete items even if assemblies are already produced)')");
+
+  my $groups = $main::auth->read_groups();
+
+  foreach my $group (values %{$groups}) {
+    $group->{rights}->{assembly_edit} = 0;
+    $main::auth->save_group($group);
+  }
+
+  return 1;
+} # end run
+
+1;
index 2b0e28b..c39b97d 100644 (file)
@@ -1,7 +1,7 @@
 # @tag: auth_schema_normalization_1
 # @description: Auth-Datenbankschema Normalisierungen Teil 1
 # @depends:
-package SL::DBUpgrade2::auth_schema_normalization_1;
+package SL::DBUpgrade2::Auth::auth_schema_normalization_1;
 
 use strict;
 use utf8;
index e3e048a..2f175ac 100644 (file)
@@ -1,7 +1,7 @@
 # @tag: bank_transaction_rights
 # @description: Setzt das neue Recht die Bankerweiterung zu nutzen (für Gruppen die auch Recht Kontenabgleich haben)
 # @depends: release_3_2_0
-package SL::DBUpgrade2::bank_transaction_rights;
+package SL::DBUpgrade2::Auth::bank_transaction_rights;
 
 use strict;
 use utf8;
diff --git a/sql/Pg-upgrade2-auth/client_task_server.sql b/sql/Pg-upgrade2-auth/client_task_server.sql
new file mode 100644 (file)
index 0000000..e636dbf
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: client_task_server
+-- @description: Einstellungen, um eine Task-Server-Instanz für mehrere Mandanten laufen zu lassen
+-- @depends: release_3_3_0
+ALTER TABLE auth.clients ADD COLUMN task_server_user_id INTEGER;
+ALTER TABLE auth.clients ADD FOREIGN KEY (task_server_user_id) REFERENCES auth.user (id);
index 4288546..4791823 100644 (file)
@@ -2,7 +2,7 @@
 # @description: Einführung von Mandanten
 # @depends: release_3_0_0
 # @ignore: 0
-package SL::DBUpgrade2::clients;
+package SL::DBUpgrade2::Auth::clients;
 
 use strict;
 use utf8;
index 6f73f21..68650e7 100644 (file)
@@ -2,7 +2,7 @@
 # @description: WebDAV-Migration für Mandanten
 # @depends: clients
 # @ignore: 0
-package SL::DBUpgrade2::clients_webdav;
+package SL::DBUpgrade2::Auth::clients_webdav;
 
 use strict;
 use utf8;
diff --git a/sql/Pg-upgrade2-auth/convert_columns_to_html_for_sending_html_emails.pl b/sql/Pg-upgrade2-auth/convert_columns_to_html_for_sending_html_emails.pl
new file mode 100644 (file)
index 0000000..f8ffbe6
--- /dev/null
@@ -0,0 +1,46 @@
+# @tag: convert_columns_to_html_for_sending_html_emails
+# @description: Versand von E-Mails in HTML: mehrere Text-Spalten nach HTML umwandeln
+# @depends: release_3_5_8
+package SL::DBUpgrade2::Auth::convert_columns_to_html_for_sending_html_emails;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::HTML::Util;
+
+sub run {
+  my ($self) = @_;
+
+  my $q_fetch = <<SQL;
+    SELECT user_id, cfg_key, cfg_value
+    FROM auth.user_config
+    WHERE (cfg_key = 'signature')
+SQL
+
+  my $q_update = <<SQL;
+    UPDATE auth.user_config
+    SET cfg_value = ?
+    WHERE (user_id = ?)
+      AND (cfg_key = 'signature')
+SQL
+
+  my $h_fetch = $self->dbh->prepare($q_fetch);
+  $h_fetch->execute || $::form->dberror($q_fetch);
+
+  my $h_update = $self->dbh->prepare($q_update);
+
+  while (my $entry = $h_fetch->fetchrow_hashref) {
+    $entry->{cfg_value} //= '';
+    my $new_value = SL::HTML::Util->plain_text_to_html($entry->{cfg_value});
+
+    next if $entry->{cfg_value} eq $new_value;
+
+    $h_update->execute($new_value, $entry->{user_id}) || $::form->dberror($q_update);
+  }
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2-auth/custom_data_export_rights.pl b/sql/Pg-upgrade2-auth/custom_data_export_rights.pl
new file mode 100644 (file)
index 0000000..cfffcb2
--- /dev/null
@@ -0,0 +1,27 @@
+# @tag: custom_data_export_rights
+# @description: Rechte für benutzerdefinierten Datenexport
+# @depends: release_3_5_0
+package SL::DBUpgrade2::Auth::custom_data_export_rights;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+sub run {
+  my ($self) = @_;
+  my $right  = 'custom_data_export_designer';
+
+  $self->db_query("INSERT INTO auth.master_rights (position, name, description) VALUES (4275, '${right}', 'Custom data export')");
+
+  my $groups = $::auth->read_groups;
+
+  foreach my $group (grep { $_->{rights}->{admin} } values %{$groups}) {
+    $group->{rights}->{$right} = 1;
+    $::auth->save_group($group);
+  }
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2-auth/customer_vendor_record_extra_tab_rights.pl b/sql/Pg-upgrade2-auth/customer_vendor_record_extra_tab_rights.pl
new file mode 100644 (file)
index 0000000..4431a34
--- /dev/null
@@ -0,0 +1,32 @@
+# @tag: customer_vendor_record_extra_tab_rights
+# @description: Setzt Rechte um bei Kunden/Lieferanten einen Extratab anzeigen zu lassen, der Belege anzeigt per Default erlaubt
+# @depends: release_3_5_2
+# @locales: Show record tab in customer
+# @locales: Show record tab in vendor
+package SL::DBUpgrade2::Auth::customer_vendor_record_extra_tab_rights;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::DBUtils;
+
+sub run {
+  my ($self) = @_;
+
+  $self->db_query("INSERT INTO auth.master_rights (position, name, description) VALUES ( 610,  'show_extra_record_tab_customer',   'Show record tab in customer')");
+  $self->db_query("INSERT INTO auth.master_rights (position, name, description) VALUES ( 611,  'show_extra_record_tab_vendor',   'Show record tab in vendor')");
+
+  my $groups = $main::auth->read_groups();
+
+  foreach my $group (values %{$groups}) {
+    $group->{rights}->{show_extra_record_tab_customer}   = 1;
+    $group->{rights}->{show_extra_record_tab_vendor}     = 1;
+    $main::auth->save_group($group);
+  }
+
+  return 1;
+} # end run
+
+1;
index f28b6f5..75de329 100644 (file)
@@ -1,7 +1,7 @@
 # @tag: delivery_plan_rights
 # @description: Setzt das neue Recht den Lieferplan anzuzeigen
 # @depends: release_3_2_0 add_master_rights
-package SL::DBUpgrade2::delivery_plan_rights;
+package SL::DBUpgrade2::Auth::delivery_plan_rights;
 
 use strict;
 use utf8;
index e0ff4b6..51379e2 100644 (file)
@@ -1,7 +1,7 @@
 # @tag: delivery_process_value
 # @description: Setzt das neue Recht den Lieferstatus mit Warenwert zu sehen
 # @depends: release_3_2_0 add_master_rights
-package SL::DBUpgrade2::delivery_process_value;
+package SL::DBUpgrade2::Auth::delivery_process_value;
 
 use strict;
 use utf8;
index 90fc16a..f116b84 100644 (file)
@@ -1,7 +1,7 @@
 # @tag: details_and_report_of_parts
 # @description: Setzt das Recht zur Anzeige von Details und Berichten von Waren, Dienstleistungen und Erzeugnissen
 # @depends: release_3_2_0 add_master_rights
-package SL::DBUpgrade2::details_and_report_of_parts;
+package SL::DBUpgrade2::Auth::details_and_report_of_parts;
 
 use strict;
 use utf8;
index 5423e91..f60148d 100644 (file)
@@ -2,7 +2,7 @@
 # @description: Ändert "FOREIGN KEY" constraints auf "ON DELETE CASCADE"
 # @depends: clients
 # @ignore: 0
-package SL::DBUpgrade2::foreign_key_constraints_on_delete;
+package SL::DBUpgrade2::Auth::foreign_key_constraints_on_delete;
 
 use Data::Dumper;
 
diff --git a/sql/Pg-upgrade2-auth/mail_journal_rights.pl b/sql/Pg-upgrade2-auth/mail_journal_rights.pl
new file mode 100644 (file)
index 0000000..1476b66
--- /dev/null
@@ -0,0 +1,34 @@
+# @tag: mail_journal_rights
+# @description:  Extra right for email journal
+# @depends: master_rights_position_gaps
+# @locales: E-Mail-Journal
+# @locales: Read all employee e-mails
+
+package SL::DBUpgrade2::Auth::mail_journal_rights;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::DBUtils;
+
+sub run {
+  my ($self) = @_;
+
+  $self->db_query("INSERT INTO auth.master_rights (position, name, description) VALUES (?, ?, ?)", bind => $_) for
+    [ 4450, 'email_journal'         , 'E-Mail-Journal'            ],
+    [ 4480, 'email_employee_readall', 'Read all employee e-mails' ];
+
+  my $groups = $main::auth->read_groups();
+
+  foreach my $group (values %{$groups}) {
+    $group->{rights}->{email_journal}          = $group->{rights}->{productivity};
+    $group->{rights}->{email_employee_readall} = $group->{rights}->{admin};
+    $main::auth->save_group($group);
+  }
+
+  return 1;
+} # end run
+
+1;
diff --git a/sql/Pg-upgrade2-auth/master_rights_position_gaps.sql b/sql/Pg-upgrade2-auth/master_rights_position_gaps.sql
new file mode 100644 (file)
index 0000000..e1dcec2
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: master_rights_position_gaps
+-- @description: Position in Rechtetabelle mit 100 multipliziert damit Lücken für neue Einträge entstehen
+-- @depends: release_3_4_0 add_master_rights
+
+UPDATE auth.master_rights SET position=position*100;
diff --git a/sql/Pg-upgrade2-auth/master_rights_positions_fix.sql b/sql/Pg-upgrade2-auth/master_rights_positions_fix.sql
new file mode 100644 (file)
index 0000000..5ec5ffa
--- /dev/null
@@ -0,0 +1,15 @@
+-- @tag: master_rights_positions_fix
+-- @description: Position in Rechtetabelle korrigieren (falls zutreffend)
+-- @depends: release_3_5_4 purchase_letter_rights all_drafts_edit right_purchase_all_edit rights_sales_purchase_edit_prices
+
+UPDATE auth.master_rights SET position = position/100
+       WHERE position > 10000
+       AND   name IN ('purchase_letter_edit', 'purchase_letter_report', 'all_drafts_edit');
+
+UPDATE auth.master_rights SET position = (SELECT position + 10 FROM auth.master_rights WHERE name = 'purchase_letter_edit')
+       WHERE position > 10000
+       AND   name LIKE 'purchase_all_edit';
+
+UPDATE auth.master_rights SET position =(SELECT position + 10 FROM auth.master_rights WHERE name = 'purchase_all_edit')
+       WHERE position > 10000
+       AND   name LIKE 'purchase_edit_prices';
diff --git a/sql/Pg-upgrade2-auth/move_shop_part_edit_right.sql b/sql/Pg-upgrade2-auth/move_shop_part_edit_right.sql
new file mode 100644 (file)
index 0000000..00e4d41
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: move_shop_part_edit_right
+-- @description: Recht zum Editieren von Shop-Artikeln verschieben
+-- @depends: release_3_5_7
+
+UPDATE auth.master_rights SET position = 580 WHERE position = 550 AND name = 'shop_part_edit';
diff --git a/sql/Pg-upgrade2-auth/other_file_sources.sql b/sql/Pg-upgrade2-auth/other_file_sources.sql
new file mode 100644 (file)
index 0000000..1f26477
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: other_file_sources
+-- @description: Neue Gruppenrechte für das Importieren von Scannern oder email
+-- @depends: release_3_4_0 master_rights_position_gaps
+-- @locales: Import AP from Scanner or Email
+-- @locales: Import AR from Scanner or Email
+INSERT INTO auth.master_rights (position, name, description) VALUES (2050, 'import_ar', 'Import AR from Scanner or Email');
+INSERT INTO auth.master_rights (position, name, description) VALUES (2650, 'import_ap', 'Import AP from Scanner or Email');
diff --git a/sql/Pg-upgrade2-auth/other_file_sources2.sql b/sql/Pg-upgrade2-auth/other_file_sources2.sql
new file mode 100644 (file)
index 0000000..dedf0be
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: other_file_sources2
+-- @description: Neue Gruppenrechte für das Importieren von Scannern oder email auf freie Position
+-- @depends: release_3_4_0 other_file_sources
+update auth.master_rights set position='2680' where name='import_ap';
index c1db613..40956d0 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: password_hashing
 -- @description: Explicitely set a password hashing algorithm
 -- @depends:
--- @charset: utf-8
 UPDATE auth."user"
   SET password = '{CRYPT}' || password
   WHERE NOT (password IS NULL)
index 7f28711..067a9d9 100644 (file)
@@ -1,7 +1,7 @@
 # @tag: productivity_rights
 # @description: Setzt das Recht die Produktivität einzusehen und das Recht den Link zum Admin-Menü anzuzeigen wieder wie vorher
 # @depends: release_3_2_0 add_master_rights
-package SL::DBUpgrade2::productivity_rights;
+package SL::DBUpgrade2::Auth::productivity_rights;
 
 use strict;
 use utf8;
diff --git a/sql/Pg-upgrade2-auth/purchase_letter_rights.pl b/sql/Pg-upgrade2-auth/purchase_letter_rights.pl
new file mode 100644 (file)
index 0000000..d803cbe
--- /dev/null
@@ -0,0 +1,33 @@
+# @tag: purchase_letter_rights
+# @description: Neue Rechte für Lieferantenbriefe
+# @depends: release_3_4_0 add_master_rights master_rights_position_gaps
+# @locales: Edit purchase letters
+# @locales: Show purchase letters report
+package SL::DBUpgrade2::Auth::purchase_letter_rights;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::DBUtils;
+
+sub run {
+  my ($self) = @_;
+
+  $self->db_query("INSERT INTO auth.master_rights (position, name, description) VALUES (?, ?, ?)", bind => $_) for
+    [ 2550, 'purchase_letter_edit',   'Edit purchase letters'        ],
+    [ 2650, 'purchase_letter_report', 'Show purchase letters report' ];
+
+  my $groups = $main::auth->read_groups();
+
+  foreach my $group (values %{$groups}) {
+    $group->{rights}->{purchase_letter_edit} = $group->{rights}->{purchase_order_edit};
+    $group->{rights}->{purchase_letter_report} = $group->{rights}->{purchase_order_edit};
+    $main::auth->save_group($group);
+  }
+
+  return 1;
+} # end run
+
+1;
diff --git a/sql/Pg-upgrade2-auth/record_links_rights.pl b/sql/Pg-upgrade2-auth/record_links_rights.pl
new file mode 100644 (file)
index 0000000..5f85588
--- /dev/null
@@ -0,0 +1,28 @@
+# @tag: record_links_rights
+# @description: Setzt das Recht um den Tab verknüpfte Belege zu sehen, per Default erlaubt (wie vorher auch)
+# @depends: release_3_4_0 master_rights_position_gaps
+package SL::DBUpgrade2::Auth::record_links_rights;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::DBUtils;
+
+sub run {
+  my ($self) = @_;
+
+  $self->db_query("INSERT INTO auth.master_rights (position, name, description) VALUES ( 4750, 'record_links', 'Linked Records')");
+
+  my $groups = $main::auth->read_groups();
+
+  foreach my $group (values %{$groups}) {
+    $group->{rights}->{record_links} = 1;
+    $main::auth->save_group($group);
+  }
+
+  return 1;
+} # end run
+
+1;
index e906009..42397cc 100644 (file)
@@ -1,4 +1,3 @@
 -- @tag: release_3_0_0
 -- @description: Abhängigkeitsscript für Release 3.0.0
 -- @depends: remove_menustyle_v4 remove_menustyle_xml add_batch_printing_to_full_access auth_schema_normalization_1 session_content_auto_restore add_api_token password_hashing
--- @charset: utf-8
index 852ec01..77dc457 100644 (file)
@@ -1,5 +1,4 @@
 -- @tag: release_3_2_0
 -- @description: Abhängigkeitsscript für Release 3.2.0, bzw. vergessene 3.1.0
 -- @depends: release_3_0_0 clients_webdav foreign_key_constraints_on_delete  clients
--- @charset: utf-8
 
index efcfa17..c28efb6 100644 (file)
@@ -1,4 +1,3 @@
 -- @tag: release_3_3_0
 -- @description: Abhängigkeitsscript für Release 3.3.0
 -- @depends: release_3_2_0 sales_letter_rights delivery_plan_rights requirement_spec_rights delivery_process_value bank_transaction_rights details_and_report_of_parts productivity_rights rights_for_showing_ar_and_ap_transactions
--- @charset: utf-8
diff --git a/sql/Pg-upgrade2-auth/release_3_4_0.sql b/sql/Pg-upgrade2-auth/release_3_4_0.sql
new file mode 100644 (file)
index 0000000..c169c4c
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: release_3_4_0
+-- @description: Abhängigkeitsscript für Release 3.4.0
+-- @depends: release_3_3_0 client_task_server remove_insecurely_hashed_passwords session_content_primary_key
+
diff --git a/sql/Pg-upgrade2-auth/release_3_5_0.sql b/sql/Pg-upgrade2-auth/release_3_5_0.sql
new file mode 100644 (file)
index 0000000..e8e7978
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: release_3_5_0
+-- @description: Abhängigkeitsscript für Release 3.5.0
+-- @depends: release_3_4_0 record_links_rights other_file_sources2 mail_journal_rights purchase_letter_rights rename_general_ledger_rights all_drafts_edit
+
+
diff --git a/sql/Pg-upgrade2-auth/release_3_5_1.sql b/sql/Pg-upgrade2-auth/release_3_5_1.sql
new file mode 100644 (file)
index 0000000..5fb5bad
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: release_3_5_1
+-- @description: Abhängigkeitsscript für Release 3.5.1
+-- @depends: release_3_5_0 assembly_edit_right webshop_api_rights_2
+
+
diff --git a/sql/Pg-upgrade2-auth/release_3_5_2.sql b/sql/Pg-upgrade2-auth/release_3_5_2.sql
new file mode 100644 (file)
index 0000000..698a7c4
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_5_2
+-- @description: Abhängigkeitsscript für Release 3.5.2
+-- @depends: release_3_5_1 custom_data_export_rights
diff --git a/sql/Pg-upgrade2-auth/release_3_5_3.sql b/sql/Pg-upgrade2-auth/release_3_5_3.sql
new file mode 100644 (file)
index 0000000..7cabb93
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_5_3
+-- @description: Abhängigkeitsscript für Release 3.5.3
+-- @depends: release_3_5_2 customer_vendor_record_extra_tab_rights
diff --git a/sql/Pg-upgrade2-auth/release_3_5_4.sql b/sql/Pg-upgrade2-auth/release_3_5_4.sql
new file mode 100644 (file)
index 0000000..cc0985f
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_5_4
+-- @description: Abhängigkeitsscript für Release 3.5.4
+-- @depends: release_3_5_3 rights_for_viewing_project_specific_invoices
diff --git a/sql/Pg-upgrade2-auth/release_3_5_5.sql b/sql/Pg-upgrade2-auth/release_3_5_5.sql
new file mode 100644 (file)
index 0000000..73d0d04
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_5_5
+-- @description: Abhängigkeitsscript für Release 3.5.5
+-- @depends: release_3_5_4 master_rights_positions_fix
diff --git a/sql/Pg-upgrade2-auth/release_3_5_6.sql b/sql/Pg-upgrade2-auth/release_3_5_6.sql
new file mode 100644 (file)
index 0000000..dd05ba4
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_5_6
+-- @description: Abhängigkeitsscript für Release 3.5.6
+-- @depends: release_3_5_5
diff --git a/sql/Pg-upgrade2-auth/release_3_5_6_1.sql b/sql/Pg-upgrade2-auth/release_3_5_6_1.sql
new file mode 100644 (file)
index 0000000..721ad60
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_5_6_1
+-- @description: Abhängigkeitsscript für Release 3.5.6.1
+-- @depends: release_3_5_6
diff --git a/sql/Pg-upgrade2-auth/release_3_5_7.sql b/sql/Pg-upgrade2-auth/release_3_5_7.sql
new file mode 100644 (file)
index 0000000..734501f
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_5_7
+-- @description: Abhängigkeitsscript für Release 3.5.7
+-- @depends: release_3_5_6_1 rights_time_recording_show_edit_all right_productivity_as_category
diff --git a/sql/Pg-upgrade2-auth/release_3_5_8.sql b/sql/Pg-upgrade2-auth/release_3_5_8.sql
new file mode 100644 (file)
index 0000000..24d6fc1
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_5_8
+-- @description: Abhängigkeitsscript für Release 3.5.8
+-- @depends: release_3_5_7 right_assortment_edit right_develop
diff --git a/sql/Pg-upgrade2-auth/release_3_6_0.sql b/sql/Pg-upgrade2-auth/release_3_6_0.sql
new file mode 100644 (file)
index 0000000..a7142ef
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_6_0
+-- @description: Abhängigkeitsscript für Release 3.6.0
+-- @depends: release_3_5_8 convert_columns_to_html_for_sending_html_emails
diff --git a/sql/Pg-upgrade2-auth/release_3_6_1.sql b/sql/Pg-upgrade2-auth/release_3_6_1.sql
new file mode 100644 (file)
index 0000000..6a9f3f3
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_6_1
+-- @description: Abhängigkeitsscript für Release 3.6.1
+-- @depends: release_3_6_0 rights_view_docs
diff --git a/sql/Pg-upgrade2-auth/remove_insecurely_hashed_passwords.sql b/sql/Pg-upgrade2-auth/remove_insecurely_hashed_passwords.sql
new file mode 100644 (file)
index 0000000..1ea6b44
--- /dev/null
@@ -0,0 +1,8 @@
+-- @tag: remove_insecurely_hashed_passwords
+-- @description: Passwörter löschen, die mit unsicheren Hash-Verfahren gehasht wurden
+-- @depends: release_3_3_0
+UPDATE auth.user
+SET password = '*'
+WHERE (password IS NOT NULL)
+  AND (password NOT LIKE '{PBKDF2%')
+  AND (password NOT LIKE '{SHA256%');
index 3744bd3..b4d598a 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: remove_menustyle_v4
 -- @description: Menütyp "CSS (oben, neu)" wurde entfernt; also durch v3 ersetzen
 -- @depends:
--- @charset: utf-8
 UPDATE auth.user_config
 SET cfg_value = 'v3'
 WHERE ((cfg_key   = 'menustyle')
index f1e3327..a8ec107 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: remove_menustyle_xml
 -- @description: Menütyp "XUL/XML" wurde entfernt; also durch v3 ersetzen
 -- @depends:
--- @charset: utf-8
 UPDATE auth.user_config
 SET cfg_value = 'v3'
 WHERE ((cfg_key   = 'menustyle')
diff --git a/sql/Pg-upgrade2-auth/rename_general_ledger_rights.sql b/sql/Pg-upgrade2-auth/rename_general_ledger_rights.sql
new file mode 100644 (file)
index 0000000..018eae8
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: rename_general_ledger_rights
+-- @description: Umbennenung des general ledger Rechts
+-- @depends: split_transaction_rights
+-- @locales: AP/AR Aging & Journal
+UPDATE auth.master_rights SET description='AP/AR Aging & Journal' WHERE name='general_ledger';
index e93d4ba..5e12edb 100644 (file)
@@ -1,7 +1,7 @@
 # @tag: requirement_spec_rights
 # @description: Neues Gruppenrecht für Pflichtenhefte
 # @depends: release_3_2_0 add_master_rights
-package SL::DBUpgrade2::requirement_spec_rights;
+package SL::DBUpgrade2::Auth::requirement_spec_rights;
 
 use strict;
 use utf8;
diff --git a/sql/Pg-upgrade2-auth/right_assortment_edit.sql b/sql/Pg-upgrade2-auth/right_assortment_edit.sql
new file mode 100644 (file)
index 0000000..d0a6e07
--- /dev/null
@@ -0,0 +1,15 @@
+-- @tag: right_assortment_edit
+-- @description: Recht zum Ändern von Sortimentsbestandteilen auch nach Verwendeung
+-- @depends: release_3_5_7 move_shop_part_edit_right
+-- @locales: Always edit assortment items (user can change/delete items even if assortments are already used)
+
+INSERT INTO auth.master_rights (position, name, description, category)
+  VALUES ((SELECT position + 10 FROM auth.master_rights WHERE name = 'assembly_edit'),
+          'assortment_edit',
+          'Always edit assortment items (user can change/delete items even if assortments are already used)',
+          FALSE);
+
+INSERT INTO auth.group_rights (group_id, "right", granted)
+  SELECT id, 'assortment_edit', true
+  FROM auth.group
+  WHERE name = 'Vollzugriff';
diff --git a/sql/Pg-upgrade2-auth/right_develop.sql b/sql/Pg-upgrade2-auth/right_develop.sql
new file mode 100644 (file)
index 0000000..2eecf74
--- /dev/null
@@ -0,0 +1,10 @@
+-- @tag: right_develop
+-- @description: Recht für Entwickler
+-- @depends: release_3_5_7
+-- @locales: See various menu entries intended for developers
+
+INSERT INTO auth.master_rights (position, name, description, category)
+  VALUES ((SELECT position + 20 FROM auth.master_rights WHERE name = 'admin'),
+          'developer',
+          'See various menu entries intended for developers',
+          FALSE);
diff --git a/sql/Pg-upgrade2-auth/right_productivity_as_category.sql b/sql/Pg-upgrade2-auth/right_productivity_as_category.sql
new file mode 100644 (file)
index 0000000..3f161fc
--- /dev/null
@@ -0,0 +1,26 @@
+-- @tag: right_productivity_as_category
+-- @description: Rechte: Produktivität als eigene Kategorie
+-- @depends: master_rights_positions_fix
+-- @locales: Productivity (TODO list, Follow-Ups)
+
+-- make space before 'configuration'
+UPDATE auth.master_rights SET position = position+1000
+  WHERE position >= (SELECT position FROM auth.master_rights WHERE name LIKE 'configuration');
+
+-- insert category for productivity before 'configuration'
+INSERT INTO auth.master_rights (position, name, description, category)
+  VALUES ((SELECT position FROM auth.master_rights WHERE name LIKE 'configuration') - 1000,
+          'productivity_category',
+          'Productivity',
+          TRUE);
+
+-- move productivity rights below 'productivity_category'
+UPDATE auth.master_rights SET position    = (SELECT position FROM auth.master_rights WHERE name LIKE 'productivity_category') + 100,
+                              description = 'Productivity (TODO list, Follow-Ups)'
+  WHERE name LIKE 'productivity';
+
+UPDATE auth.master_rights SET position = (SELECT position FROM auth.master_rights WHERE name LIKE 'productivity_category') + 200
+  WHERE name LIKE 'email_journal';
+
+UPDATE auth.master_rights SET position = (SELECT position FROM auth.master_rights WHERE name LIKE 'productivity_category') + 250
+  WHERE name LIKE 'email_employee_readall';
diff --git a/sql/Pg-upgrade2-auth/right_purchase_all_edit.sql b/sql/Pg-upgrade2-auth/right_purchase_all_edit.sql
new file mode 100644 (file)
index 0000000..ca35442
--- /dev/null
@@ -0,0 +1,14 @@
+-- @tag: right_purchase_all_edit
+-- @description: Recht zum Bearbeiten von Einkaufsdokumenten aller Mitarbeiter (Trennung nach VK u. EK)
+-- @depends: release_3_5_4
+-- @locales: View/edit all employees purchase documents
+
+INSERT INTO auth.master_rights (position, name, description, category)
+  VALUES ((SELECT position + 10 FROM auth.master_rights WHERE name = 'purchase_letter_edit'),
+          'purchase_all_edit',
+          'View/edit all employees purchase documents',
+          FALSE);
+
+-- same rights as sales_all_edit because sales and purchase were not distingushed before
+INSERT INTO auth.group_rights (group_id, "right", granted)
+  SELECT group_id, 'purchase_all_edit', granted FROM auth.group_rights WHERE "right" = 'sales_all_edit';
diff --git a/sql/Pg-upgrade2-auth/right_time_recording.sql b/sql/Pg-upgrade2-auth/right_time_recording.sql
new file mode 100644 (file)
index 0000000..b8e0b93
--- /dev/null
@@ -0,0 +1,15 @@
+-- @tag: right_time_recording
+-- @description: Recht zur Zeiterfassung
+-- @depends: release_3_5_6_1
+-- @locales: Create, edit and list time recordings
+
+INSERT INTO auth.master_rights (position, name, description, category)
+  VALUES ((SELECT position + 50 FROM auth.master_rights WHERE name = 'email_employee_readall'),
+          'time_recording',
+          'Create, edit and list time recordings',
+          FALSE);
+
+INSERT INTO auth.group_rights (group_id, "right", granted)
+  SELECT id, 'time_recording', true
+  FROM auth.group
+  WHERE name = 'Vollzugriff';
index 523975e..1dc7950 100644 (file)
@@ -1,7 +1,7 @@
 # @tag: rights_for_showing_ar_and_ap_transactions
 # @description: Setzt das Recht zur Anzeige von Debitoren- und Kreditorenbuchungen im Rechnungsbericht
 # @depends: release_3_2_0 add_master_rights
-package SL::DBUpgrade2::rights_for_showing_ar_and_ap_transactions;
+package SL::DBUpgrade2::Auth::rights_for_showing_ar_and_ap_transactions;
 
 use strict;
 use utf8;
diff --git a/sql/Pg-upgrade2-auth/rights_for_viewing_project_specific_invoices.sql b/sql/Pg-upgrade2-auth/rights_for_viewing_project_specific_invoices.sql
new file mode 100644 (file)
index 0000000..c08eab1
--- /dev/null
@@ -0,0 +1,18 @@
+-- @tag: rights_for_viewing_project_specific_invoices
+-- @description: Rechte zum Anzeigen von Rechnungen, die zu Projekten gehören
+-- @depends: release_3_5_3
+-- @locales: Projects: edit the list of employees allowed to view invoices
+INSERT INTO auth.master_rights (position, name, description, category)
+VALUES (
+  (SELECT position + 2
+   FROM auth.master_rights
+   WHERE name = 'project_edit'),
+  'project_edit_view_invoices_permission',
+  'Projects: edit the list of employees allowed to view invoices',
+  false
+);
+
+INSERT INTO auth.group_rights (group_id, "right", granted)
+SELECT id, 'project_edit_view_invoices_permission', true
+FROM auth.group
+WHERE name = 'Vollzugriff';
diff --git a/sql/Pg-upgrade2-auth/rights_sales_purchase_edit_prices.sql b/sql/Pg-upgrade2-auth/rights_sales_purchase_edit_prices.sql
new file mode 100644 (file)
index 0000000..aa529f8
--- /dev/null
@@ -0,0 +1,17 @@
+-- @tag: rights_sales_purchase_edit_prices
+-- @description: Recht zum Bearbeiten von Preisen nach Ver- und Einkauf trennen
+-- @depends: release_3_5_4 right_purchase_all_edit
+
+INSERT INTO auth.master_rights (position, name, description, category)
+  VALUES ((SELECT position + 10 FROM auth.master_rights WHERE name = 'purchase_all_edit'),
+          'purchase_edit_prices',
+          'Edit prices and discount (if not used, textfield is ONLY set readonly)',
+          FALSE);
+
+-- same rights as edit_prices because sales and purchase were not distingushed before
+INSERT INTO auth.group_rights (group_id, "right", granted)
+  SELECT group_id, 'purchase_edit_prices', granted FROM auth.group_rights WHERE "right" = 'edit_prices';
+
+-- rename "edit_prices" to "sales_edit_prices"
+UPDATE auth.master_rights SET name    = 'sales_edit_prices' WHERE name    = 'edit_prices';
+UPDATE auth.group_rights  SET "right" = 'sales_edit_prices' WHERE "right" = 'edit_prices';
diff --git a/sql/Pg-upgrade2-auth/rights_time_recording_show_edit_all.sql b/sql/Pg-upgrade2-auth/rights_time_recording_show_edit_all.sql
new file mode 100644 (file)
index 0000000..24453a9
--- /dev/null
@@ -0,0 +1,27 @@
+-- @tag: rights_time_recording_show_edit_all
+-- @description: Rechte, Zeiterfassungseinträge aller Mitarbeiter anzuzeigen bzw. zu bearbeiten
+-- @depends: right_time_recording
+-- @locales: List time recordings of all staff members
+-- @locales: Edit time recordings of all staff members
+
+INSERT INTO auth.master_rights (position, name, description, category)
+  VALUES ((SELECT position + 20 FROM auth.master_rights WHERE name = 'time_recording'),
+          'time_recording_show_all',
+          'List time recordings of all staff members',
+          FALSE);
+
+INSERT INTO auth.group_rights (group_id, "right", granted)
+  SELECT id, 'time_recording_show_all', true
+  FROM auth.group
+  WHERE name = 'Vollzugriff';
+
+INSERT INTO auth.master_rights (position, name, description, category)
+  VALUES ((SELECT position + 20 FROM auth.master_rights WHERE name = 'time_recording_show_all'),
+          'time_recording_edit_all',
+          'Edit time recordings of all staff members',
+          FALSE);
+
+INSERT INTO auth.group_rights (group_id, "right", granted)
+  SELECT id, 'time_recording_edit_all', true
+  FROM auth.group
+  WHERE name = 'Vollzugriff';
diff --git a/sql/Pg-upgrade2-auth/rights_view_docs.sql b/sql/Pg-upgrade2-auth/rights_view_docs.sql
new file mode 100644 (file)
index 0000000..2591ada
--- /dev/null
@@ -0,0 +1,80 @@
+-- @tag: rights_view_docs
+-- @description: Rechte zum Lesen von Belegen
+-- @depends: release_3_6_0
+-- @locales: View sales quotations
+-- @locales: View sales orders
+-- @locales: View sales delivery orders
+-- @locales: View sales invoices and credit notes
+-- @locales: View RFQs
+-- @locales: View purchase orders
+-- @locales: View purchase delivery orders
+-- @locales: View purchase invoices
+
+INSERT INTO auth.master_rights (position, name, description, category)
+  VALUES ((SELECT position + 10 FROM auth.master_rights WHERE name = 'sales_quotation_edit'),
+          'sales_quotation_view',
+           'View sales quotations',
+          FALSE);
+
+INSERT INTO auth.master_rights (position, name, description, category)
+  VALUES ((SELECT position + 10 FROM auth.master_rights WHERE name = 'sales_order_edit'),
+          'sales_order_view',
+           'View sales orders',
+          FALSE);
+
+INSERT INTO auth.master_rights (position, name, description, category)
+  VALUES ((SELECT position + 10 FROM auth.master_rights WHERE name = 'sales_delivery_order_edit'),
+          'sales_delivery_order_view',
+           'View sales delivery orders',
+          FALSE);
+
+INSERT INTO auth.master_rights (position, name, description, category)
+  VALUES ((SELECT position + 10 FROM auth.master_rights WHERE name = 'invoice_edit'),
+          'sales_invoice_view',
+          'View sales invoices and credit notes',
+          FALSE);
+
+INSERT INTO auth.master_rights (position, name, description, category)
+  VALUES ((SELECT position + 10 FROM auth.master_rights WHERE name = 'request_quotation_edit'),
+          'request_quotation_view',
+           'View RFQs',
+          FALSE);
+
+INSERT INTO auth.master_rights (position, name, description, category)
+  VALUES ((SELECT position + 10 FROM auth.master_rights WHERE name = 'purchase_order_edit'),
+          'purchase_order_view',
+           'View purchase orders',
+          FALSE);
+
+INSERT INTO auth.master_rights (position, name, description, category)
+  VALUES ((SELECT position + 10 FROM auth.master_rights WHERE name = 'purchase_delivery_order_edit'),
+          'purchase_delivery_order_view',
+           'View purchase delivery orders',
+          FALSE);
+
+INSERT INTO auth.master_rights (position, name, description, category)
+  VALUES ((SELECT position + 10 FROM auth.master_rights WHERE name = 'vendor_invoice_edit'),
+          'purchase_invoice_view',
+          'View purchase invoices',
+          FALSE);
+
+
+-- INSERT INTO auth.group_rights (group_id, "right", granted)
+--    SELECT (SELECT id FROM auth.group WHERE name = 'Vollzugriff'), 'sales_quotation_view',         true UNION
+--    SELECT (SELECT id FROM auth.group WHERE name = 'Vollzugriff'), 'sales_order_view',             true UNION
+--    SELECT (SELECT id FROM auth.group WHERE name = 'Vollzugriff'), 'sales_delivery_order_view',    true UNION
+--    SELECT (SELECT id FROM auth.group WHERE name = 'Vollzugriff'), 'sales_invoice_view',           true UNION
+--    SELECT (SELECT id FROM auth.group WHERE name = 'Vollzugriff'), 'request_quotation_view',       true UNION
+--    SELECT (SELECT id FROM auth.group WHERE name = 'Vollzugriff'), 'purchase_order_view',          true UNION
+--    SELECT (SELECT id FROM auth.group WHERE name = 'Vollzugriff'), 'purchase_delivery_order_view', true UNION
+--    SELECT (SELECT id FROM auth.group WHERE name = 'Vollzugriff'), 'purchase_invoice_view',        true;
+
+INSERT INTO auth.group_rights (group_id, "right", granted)
+   SELECT id, 'sales_quotation_view',         true FROM auth.group WHERE name = 'Vollzugriff' UNION
+   SELECT id, 'sales_order_view',             true FROM auth.group WHERE name = 'Vollzugriff' UNION
+   SELECT id, 'sales_delivery_order_view',    true FROM auth.group WHERE name = 'Vollzugriff' UNION
+   SELECT id, 'sales_invoice_view',           true FROM auth.group WHERE name = 'Vollzugriff' UNION
+   SELECT id, 'request_quotation_view',       true FROM auth.group WHERE name = 'Vollzugriff' UNION
+   SELECT id, 'purchase_order_view',          true FROM auth.group WHERE name = 'Vollzugriff' UNION
+   SELECT id, 'purchase_delivery_order_view', true FROM auth.group WHERE name = 'Vollzugriff' UNION
+   SELECT id, 'purchase_invoice_view',        true FROM auth.group WHERE name = 'Vollzugriff';
index f018317..6a960f4 100644 (file)
@@ -1,7 +1,7 @@
 # @tag: sales_letter_rights
 # @description: Setzt das neue Recht die Brieffunktion anzuzeigen
 # @depends: release_3_2_0 add_master_rights
-package SL::DBUpgrade2::sales_letter_rights;
+package SL::DBUpgrade2::Auth::sales_letter_rights;
 
 use strict;
 use utf8;
index d0d84a5..61cdf57 100644 (file)
@@ -1,6 +1,5 @@
 -- @tag: session_content_auto_restore
 -- @description: Spalte "auto_restore" in auth.session_content
 -- @depends:
--- @charset: utf-8
 ALTER TABLE auth.session_content ADD COLUMN auto_restore boolean;
 UPDATE auth.session_content SET auto_restore = FALSE;
diff --git a/sql/Pg-upgrade2-auth/session_content_primary_key.sql b/sql/Pg-upgrade2-auth/session_content_primary_key.sql
new file mode 100644 (file)
index 0000000..0cf8017
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: session_content_primary_key
+-- @description: Primärschlüssel für Tabelle auth.session_content
+-- @depends: release_3_3_0
+ALTER TABLE auth.session_content ADD PRIMARY KEY (session_id, sess_key);
diff --git a/sql/Pg-upgrade2-auth/split_transaction_rights.pl b/sql/Pg-upgrade2-auth/split_transaction_rights.pl
new file mode 100644 (file)
index 0000000..5b2e666
--- /dev/null
@@ -0,0 +1,38 @@
+# @tag: split_transaction_rights
+# @description: Finanzbuchhaltungsrechte für Buchungen aufspalten
+# @depends: release_3_4_0 master_rights_position_gaps
+# @locales: General Ledger Transaction
+# @locales: AR Transactions
+# @locales: AP Transactions
+
+
+package SL::DBUpgrade2::Auth::split_transaction_rights;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::DBUtils;
+
+sub run {
+  my ($self) = @_;
+
+  $self->db_query("INSERT INTO auth.master_rights (position, name, description) VALUES (3130,'gl_transactions','General Ledger Transaction')");
+  $self->db_query("INSERT INTO auth.master_rights (position, name, description) VALUES (3150,'ar_transactions','AR Transactions')");
+  $self->db_query("INSERT INTO auth.master_rights (position, name, description) VALUES (3170,'ap_transactions','AP Transactions')");
+  $self->db_query("UPDATE auth.master_rights SET description='General Ledger' WHERE name='general_ledger'");
+
+  my $groups = $main::auth->read_groups();
+
+  foreach my $group (values %{$groups}) {
+    $group->{rights}->{gl_transactions} = $group->{rights}->{general_ledger};
+    $group->{rights}->{ar_transactions} = $group->{rights}->{general_ledger};
+    $group->{rights}->{ap_transactions} = $group->{rights}->{general_ledger};
+    $main::auth->save_group($group);
+  }
+
+  return 1;
+} # end run
+
+1;
diff --git a/sql/Pg-upgrade2-auth/webshop_api_rights.pl b/sql/Pg-upgrade2-auth/webshop_api_rights.pl
new file mode 100644 (file)
index 0000000..b62b3fe
--- /dev/null
@@ -0,0 +1,35 @@
+# @tag: webshop_api_rights
+# @description: Setzt die Rechte Shopconfig, Shopbestellungen, Shopartikel, per Default erlaubt
+# @depends: release_3_5_0
+# @locales: Create and edit shopparts
+# @locales: Get shoporders
+# @locales: Create and edit webshops
+package SL::DBUpgrade2::Auth::webshop_api_rights;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::DBUtils;
+
+sub run {
+  my ($self) = @_;
+
+  $self->db_query("INSERT INTO auth.master_rights (position, name, description) VALUES ( 550,  'shop_part_edit',   'Create and edit shopparts')");
+  $self->db_query("INSERT INTO auth.master_rights (position, name, description) VALUES ( 950,  'shop_order',       'Get shoporders')");
+  $self->db_query("INSERT INTO auth.master_rights (position, name, description) VALUES ( 4300, 'edit_shop_config', 'Create and edit webshops')");
+
+  my $groups = $main::auth->read_groups();
+
+  foreach my $group (values %{$groups}) {
+    $group->{rights}->{shop_part_edit}   = 1;
+    $group->{rights}->{shop_order}       = 1;
+    $group->{rights}->{edit_shop_config} = 1;
+    $main::auth->save_group($group);
+  }
+
+  return 1;
+} # end run
+
+1;
diff --git a/sql/Pg-upgrade2-auth/webshop_api_rights_2.pl b/sql/Pg-upgrade2-auth/webshop_api_rights_2.pl
new file mode 100644 (file)
index 0000000..3f476a5
--- /dev/null
@@ -0,0 +1,30 @@
+# @tag: webshop_api_rights_2
+# @description: Setzt die Rechte Shopconfig, Shopbestellungen, Shopartikel, per Default nicht erlaubt
+# @depends: webshop_api_rights
+package SL::DBUpgrade2::Auth::webshop_api_rights_2;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::DBUtils;
+
+sub run {
+  my ($self) = @_;
+
+  $self->db_query("UPDATE auth.master_rights SET position = 4250 WHERE name = 'edit_shop_config'");
+
+  my $groups = $main::auth->read_groups();
+
+  foreach my $group (values %{$groups}) {
+    $group->{rights}->{shop_part_edit}   = 0;
+    $group->{rights}->{shop_order}       = 0;
+    $group->{rights}->{edit_shop_config} = 0;
+    $main::auth->save_group($group);
+  }
+
+  return 1;
+} # end run
+
+1;
index d6c1ffe..9b4434e 100644 (file)
@@ -43,15 +43,7 @@ sub run {
     # 3804 existiert, wir gehen davon aus, daß der Benutzer das Konto schon selber angelegt hat und
     # ordnungsgemäß benutzt
 
-    if ($::form->{account_exists} ) {
-      # Benutzer hat Meldung bestätigt
-      print "Konto existiert, Upgrade &uuml;berspringen\n";
-      return 1;
-    }
-
-    # Meldung anzeigen und auf Rückgabe warten
-    print_3804_already_exists();
-    return 2;
+    return 1;
   }
 
     # noch keine Buchungen mit taxkey 13 und Konto 3804 existiert noch nicht,
index ef2e931..b086b01 100644 (file)
@@ -2,6 +2,16 @@
 -- @description: Einführen einer ID-Spalte in acc_trans
 -- @depends: release_2_4_3 cb_ob_transaction
 
+-- INFO: Dieses Script hat früher die Spalte acc_trans_id aus der
+-- impliziten OID gesetzt. PostgreSQL 12 unterstützt aber keine OIDs
+-- mehr, daher wurde die OID hier entfernt. Das ist insofern auch kein
+-- Problem, weil dieses Upgrade-Script in Version 2.6.0 benutzt wurde,
+-- und direkte Updates auf die aktuelle kivitendo-Version von vor 3.0
+-- eh nicht mehr unterstützt werden.
+--
+-- Das Script muss aber trotzdem beim Anlegen neuer Datenbanken
+-- abgearbeitet werden und daher funktionieren.
+
 CREATE SEQUENCE acc_trans_id_seq;
 
 CREATE TABLE new_acc_trans (
@@ -23,14 +33,12 @@ CREATE TABLE new_acc_trans (
     mtime timestamp without time zone
 );
 
-INSERT INTO new_acc_trans (acc_trans_id, trans_id, chart_id, amount, transdate, gldate, source, cleared,
+INSERT INTO new_acc_trans (trans_id, chart_id, amount, transdate, gldate, source, cleared,
                            fx_transaction, ob_transaction, cb_transaction, project_id, memo, taxkey, itime, mtime)
-  SELECT oid, trans_id, chart_id, amount, transdate, gldate, source, cleared,
+  SELECT trans_id, chart_id, amount, transdate, gldate, source, cleared,
     fx_transaction, ob_transaction, cb_transaction, project_id, memo, taxkey, itime, mtime
   FROM acc_trans;
 
-SELECT setval('acc_trans_id_seq', (SELECT COALESCE((SELECT MAX(oid::integer) FROM acc_trans), 0) + 1));
-
 DROP TABLE acc_trans;
 ALTER TABLE new_acc_trans RENAME TO acc_trans;
 
diff --git a/sql/Pg-upgrade2/accounts_tax_office_leonberg.sql b/sql/Pg-upgrade2/accounts_tax_office_leonberg.sql
new file mode 100644 (file)
index 0000000..11e67be
--- /dev/null
@@ -0,0 +1,12 @@
+-- @tag: accounts_tax_office_leonberg
+-- @description: Geänderte Kontoverbindung, Öffnungszeiten und Kontaktdaten für Finanzamt Leonberg
+-- @depends: release_3_5_2
+UPDATE finanzamt
+SET fa_bankbezeichnung_1 = 'DT BBK Filiale Stuttgart', fa_blz_1 = '60000000', fa_kontonummer_1 = '60301501',
+    fa_bankbezeichnung_2 = '',                         fa_blz_2 = '', fa_kontonummer_2 = '',
+    fa_oeffnungszeiten = 'MO-MI 7.30-12.00,DO 7.30-17.30,FR 7.30-12.30',
+    fa_email = 'poststelle-70@finanzamt.bwl.de',
+    fa_internet = 'http://www.fa-leonberg.de/'
+WHERE (fa_land_nr = '8')
+  AND (fa_bufa_nr = '2870')
+  AND (fa_name LIKE 'Leonberg%');
diff --git a/sql/Pg-upgrade2/add_emloyee_project_assignment_for_viewing_invoices.sql b/sql/Pg-upgrade2/add_emloyee_project_assignment_for_viewing_invoices.sql
new file mode 100644 (file)
index 0000000..2f26968
--- /dev/null
@@ -0,0 +1,11 @@
+-- @tag: add_emloyee_project_assignment_for_viewing_invoices
+-- @description: Mitarbeiter*innen Projekten zuweisen können, damit diese Projektrechnungen anschauen dürfen
+-- @depends: release_3_5_3
+CREATE TABLE employee_project_invoices (
+  employee_id INTEGER NOT NULL,
+  project_id  INTEGER NOT NULL,
+
+  CONSTRAINT employee_project_invoices_pkey             PRIMARY KEY (employee_id, project_id),
+  CONSTRAINT employee_project_invoices_employee_id_fkey FOREIGN KEY (employee_id) REFERENCES employee (id) ON DELETE CASCADE,
+  CONSTRAINT employee_project_invoices_project_id_fkey  FOREIGN KEY (project_id)  REFERENCES project  (id) ON DELETE CASCADE
+);
diff --git a/sql/Pg-upgrade2/add_gl_imported.sql b/sql/Pg-upgrade2/add_gl_imported.sql
new file mode 100644 (file)
index 0000000..a0bf715
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: add_gl_imported
+-- @description: Dialogbuchungsimport entsprechend kennzeichnen
+-- @depends: release_3_5_6
+
+ALTER TABLE gl ADD imported BOOLEAN DEFAULT 'f';
+
diff --git a/sql/Pg-upgrade2/add_gl_transaction_description.sql b/sql/Pg-upgrade2/add_gl_transaction_description.sql
new file mode 100644 (file)
index 0000000..a8c7b13
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: add_gl_transaction_description
+-- @description: Vorgangsbezeichnung für Dialogbuchungen
+-- @depends: release_3_5_8
+
+ALTER TABLE gl ADD transaction_description TEXT;
diff --git a/sql/Pg-upgrade2/add_node_id_to_background_jobs.sql b/sql/Pg-upgrade2/add_node_id_to_background_jobs.sql
new file mode 100644 (file)
index 0000000..6fac50e
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: add_node_id_to_background_jobs
+-- @description: Spalte 'node_id' in 'background_jobs'
+-- @depends: release_3_5_4
+ALTER TABLE background_jobs
+ADD COLUMN node_id TEXT;
diff --git a/sql/Pg-upgrade2/add_parts_price_history.sql b/sql/Pg-upgrade2/add_parts_price_history.sql
new file mode 100644 (file)
index 0000000..78fd9b5
--- /dev/null
@@ -0,0 +1,39 @@
+-- @tag: add_parts_price_history
+-- @description: Tabelle für Entwicklung der Stammdatenpreise
+-- @depends: release_3_4_0
+DROP TRIGGER  IF EXISTS add_parts_price_history_entry_after_changes_on_parts ON parts;
+DROP FUNCTION IF EXISTS add_parts_price_history_entry();
+DROP TABLE    IF EXISTS parts_price_history;
+
+CREATE TABLE parts_price_history (
+  id         SERIAL,
+  part_id    INTEGER   NOT NULL,
+  valid_from TIMESTAMP NOT NULL,
+  lastcost   NUMERIC(15, 5),
+  listprice  NUMERIC(15, 5),
+  sellprice  NUMERIC(15, 5),
+
+  PRIMARY KEY (id),
+  FOREIGN KEY (part_id) REFERENCES parts (id) ON DELETE CASCADE
+);
+
+INSERT INTO parts_price_history (part_id, valid_from, lastcost, listprice, sellprice)
+SELECT id, COALESCE(COALESCE(mtime, itime), now()), lastcost, listprice, sellprice
+FROM parts;
+
+CREATE FUNCTION add_parts_price_history_entry() RETURNS "trigger" AS $$
+  BEGIN
+    IF (TG_OP = 'UPDATE') AND (OLD.lastcost = NEW.lastcost) AND (OLD.listprice = NEW.listprice) AND (OLD.sellprice = NEW.sellprice) THEN
+      RETURN NEW;
+    END IF;
+
+    INSERT INTO parts_price_history (part_id, lastcost, listprice, sellprice, valid_from)
+    VALUES (NEW.id, NEW.lastcost, NEW.listprice, NEW.sellprice, now());
+
+    RETURN NEW;
+  END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER add_parts_price_history_entry_after_changes_on_parts
+AFTER INSERT OR UPDATE on parts
+FOR EACH ROW EXECUTE PROCEDURE add_parts_price_history_entry();
diff --git a/sql/Pg-upgrade2/add_parts_price_history2.sql b/sql/Pg-upgrade2/add_parts_price_history2.sql
new file mode 100644 (file)
index 0000000..ebf0036
--- /dev/null
@@ -0,0 +1,18 @@
+-- @tag: add_parts_price_history2
+-- @description: Korrigierte Triggerfunktion für Entwicklung der Stammdatenpreise
+-- @depends: add_parts_price_history
+CREATE OR REPLACE FUNCTION add_parts_price_history_entry() RETURNS "trigger" AS $$
+  BEGIN
+    IF      (TG_OP = 'UPDATE')
+        AND ((OLD.lastcost  IS NULL AND NEW.lastcost  IS NULL) OR (OLD.lastcost  = NEW.lastcost))
+        AND ((OLD.listprice IS NULL AND NEW.listprice IS NULL) OR (OLD.listprice = NEW.listprice))
+        AND ((OLD.sellprice IS NULL AND NEW.sellprice IS NULL) OR (OLD.sellprice = NEW.sellprice)) THEN
+      RETURN NEW;
+    END IF;
+
+    INSERT INTO parts_price_history (part_id, lastcost, listprice, sellprice, valid_from)
+    VALUES (NEW.id, NEW.lastcost, NEW.listprice, NEW.sellprice, now());
+
+    RETURN NEW;
+  END;
+$$ LANGUAGE plpgsql;
diff --git a/sql/Pg-upgrade2/add_record_templates_transaction_description.sql b/sql/Pg-upgrade2/add_record_templates_transaction_description.sql
new file mode 100644 (file)
index 0000000..e54fc98
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: add_record_templates_transaction_description
+-- @description: Vorgangsbezeichnung in Dialog-Vorlage ergänzen
+-- @depends: release_3_5_8 create_record_template_tables
+
+ALTER TABLE record_templates ADD COLUMN transaction_description TEXT;
diff --git a/sql/Pg-upgrade2/add_stocktaking_preselects_client_config_default.sql b/sql/Pg-upgrade2/add_stocktaking_preselects_client_config_default.sql
new file mode 100644 (file)
index 0000000..1ec1e56
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: add_stocktaking_preselects_client_config_default
+-- @description: Konfigurations-Optionen für Vorbelegungen zur Inventur
+-- @depends: warehouse
+ALTER TABLE defaults ADD COLUMN stocktaking_warehouse_id INTEGER REFERENCES warehouse(id);
+ALTER TABLE defaults ADD COLUMN stocktaking_bin_id       INTEGER REFERENCES bin(id);
+ALTER TABLE defaults ADD COLUMN stocktaking_cutoff_date  DATE;
diff --git a/sql/Pg-upgrade2/add_stocktaking_qty_threshold_client_config_default.sql b/sql/Pg-upgrade2/add_stocktaking_qty_threshold_client_config_default.sql
new file mode 100644 (file)
index 0000000..a039365
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: add_stocktaking_qty_threshold_client_config_default
+-- @description: Konfigurations-Option für Mengen-Schwellwert zur Inventur
+-- @depends: add_stocktaking_preselects_client_config_default
+
+ALTER TABLE defaults ADD COLUMN stocktaking_qty_threshold NUMERIC(25,5) DEFAULT 0;
diff --git a/sql/Pg-upgrade2/add_test_mode_to_csv_import_report.sql b/sql/Pg-upgrade2/add_test_mode_to_csv_import_report.sql
new file mode 100644 (file)
index 0000000..82e9707
--- /dev/null
@@ -0,0 +1,8 @@
+-- @tag: add_test_mode_to_csv_import_report
+-- @description: In CSV-Import-Berichtstabelle speichern, ob es ein Test war
+-- @depends: release_3_4_1
+ALTER TABLE csv_import_reports ADD COLUMN test_mode BOOLEAN;
+
+UPDATE csv_import_reports SET test_mode = TRUE;
+
+ALTER TABLE csv_import_reports ALTER COLUMN test_mode SET NOT NULL;
diff --git a/sql/Pg-upgrade2/add_transfer_doc_interval.sql b/sql/Pg-upgrade2/add_transfer_doc_interval.sql
new file mode 100644 (file)
index 0000000..79e2aba
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: add_transfer_doc_interval
+-- @description: Konfigurierbarer Zeitraum innerhalb dessen Lieferscheine wieder rückgelagert werden können
+-- @depends: release_3_5_6_1
+ALTER TABLE defaults ADD COLUMN undo_transfer_interval integer DEFAULT 7;
diff --git a/sql/Pg-upgrade2/add_warehouse_for_assembly.sql b/sql/Pg-upgrade2/add_warehouse_for_assembly.sql
new file mode 100644 (file)
index 0000000..c7f701b
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: add_warehouse_for_assembly
+-- @description: Konfigurations-Option für das Fertigen von Erzeugnissen aus dem Standardlager
+-- @depends: release_3_4_1 add_warehouse_defaults add_warehouse_client_config_default
+ALTER TABLE defaults add column transfer_default_warehouse_for_assembly boolean default false;
diff --git a/sql/Pg-upgrade2/alter_default_shipped_qty.sql b/sql/Pg-upgrade2/alter_default_shipped_qty.sql
new file mode 100644 (file)
index 0000000..f1af955
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: alter_default_shipped_qty_config
+-- @description: Mandantenweite Konfiguration für das Verhalten von Liefermengenabgleich
+-- @depends: release_3_5_6
+UPDATE defaults SET shipped_qty_fill_up = 'f';
+
+
diff --git a/sql/Pg-upgrade2/alter_record_template_tables.sql b/sql/Pg-upgrade2/alter_record_template_tables.sql
new file mode 100644 (file)
index 0000000..99185a6
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: alter_record_template_tables
+-- @description: Haken Details anzeigen in Dialog-Vorlage ergänzen
+-- @depends: release_3_5_0 create_record_template_tables
+
+ALTER TABLE record_templates ADD column show_details BOOLEAN NOT NULL DEFAULT FALSE;
+
diff --git a/sql/Pg-upgrade2/ap_gl.sql b/sql/Pg-upgrade2/ap_gl.sql
new file mode 100644 (file)
index 0000000..cb9e01b
--- /dev/null
@@ -0,0 +1,16 @@
+-- @tag: ap_gl
+-- @description: Hilfstabelle für automatische GL-Buchung nach Kreditorenbuchung
+-- @depends: release_3_5_0
+-- @ignore: 0
+      CREATE TABLE ap_gl (
+        ap_id                   integer,
+        gl_id                   integer,
+        itime                   TIMESTAMP      DEFAULT now(),
+        mtime                   TIMESTAMP,
+        PRIMARY KEY (ap_id, gl_id),
+        FOREIGN KEY (ap_id)                    REFERENCES ap (id),
+        FOREIGN KEY (gl_id)                    REFERENCES gl (id) ON DELETE CASCADE);
+
+
+
+
diff --git a/sql/Pg-upgrade2/ap_set_payment_term_from_vendor.sql b/sql/Pg-upgrade2/ap_set_payment_term_from_vendor.sql
new file mode 100644 (file)
index 0000000..1567b51
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: ap_set_payment_term_from_vendor
+-- @description: Zahlungsbedingungen in EK-Rechnungen aus Lieferant setzen
+-- @depends: release_3_5_6
+
+UPDATE ap SET payment_id = (SELECT payment_id FROM vendor WHERE vendor.id = ap.vendor_id)
+  WHERE (SELECT payment_id FROM vendor WHERE vendor.id = ap.vendor_id) IS NOT NULL
+    AND ap.payment_id IS NULL;
diff --git a/sql/Pg-upgrade2/ar_add_qrbill_without_amount.sql b/sql/Pg-upgrade2/ar_add_qrbill_without_amount.sql
new file mode 100644 (file)
index 0000000..15a1602
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: ar_add_qrbill_without_amount
+-- @description: Spalte für QR-Rechnung ohne Betrag
+-- @depends: release_3_5_8
+ALTER TABLE ar ADD COLUMN qrbill_without_amount boolean;
+ALTER TABLE ar ALTER COLUMN qrbill_without_amount SET DEFAULT FALSE;
+UPDATE ar SET qrbill_without_amount = FALSE;
diff --git a/sql/Pg-upgrade2/assembly_parts_foreign_key.sql b/sql/Pg-upgrade2/assembly_parts_foreign_key.sql
new file mode 100644 (file)
index 0000000..8b3d4b2
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: assembly_parts_foreign_key
+-- @description: Erzeugniselement (assembly) erhält Fremdschlüssel auf parts + NOT NULL
+-- @depends: release_3_4_1
+-- @ignore: 0
+
+ALTER TABLE assembly ADD FOREIGN KEY (parts_id) REFERENCES parts(id);
+ALTER TABLE assembly ALTER COLUMN parts_id SET NOT NULL;
diff --git a/sql/Pg-upgrade2/assembly_parts_foreign_key2.sql b/sql/Pg-upgrade2/assembly_parts_foreign_key2.sql
new file mode 100644 (file)
index 0000000..4b4e214
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: assembly_parts_foreign_key2
+-- @description: Erzeugnis erhält Fremdschlüssel auf parts + NOT NULL in Tabelle assembly
+-- @depends: assembly_parts_foreign_key
+-- @ignore: 0
+
+ALTER TABLE assembly ADD FOREIGN KEY (id) REFERENCES parts(id);
+ALTER TABLE assembly ALTER COLUMN id SET NOT NULL;
diff --git a/sql/Pg-upgrade2/assembly_position.sql b/sql/Pg-upgrade2/assembly_position.sql
new file mode 100644 (file)
index 0000000..b96a0d6
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: assembly_position
+-- @description: Erzeugniselemente (assembly) erhalten eine Position
+-- @depends: release_3_4_1
+-- @ignore: 0
+
+ALTER TABLE assembly ADD COLUMN position INTEGER;
diff --git a/sql/Pg-upgrade2/assortment.sql b/sql/Pg-upgrade2/assortment.sql
new file mode 100644 (file)
index 0000000..5d9c685
--- /dev/null
@@ -0,0 +1,20 @@
+-- @tag: assortment_items
+-- @description: Sortimentsartikel eingeführt
+-- @depends: release_3_4_1 part_type_enum
+
+-- adding a new value isn't allowed inside a transaction, which is what DBUpgrade automatically does
+-- run this afterwards manually for now
+-- ALTER TYPE part_type_enum ADD VALUE 'assortment';
+
+CREATE TABLE assortment_items (
+  assortment_id INTEGER REFERENCES parts(id) ON DELETE CASCADE, -- the part id of the assortment
+  parts_id      INTEGER REFERENCES parts(id),
+  itime         timestamp without time zone default now(),
+  mtime         timestamp without time zone,
+  qty           REAL NOT NULL,
+  position      INTEGER NOT NULL,
+  unit          character varying(20) NOT NULL REFERENCES units(name),
+  CONSTRAINT assortment_part_pkey PRIMARY KEY (assortment_id, parts_id)
+);
+
+ALTER TABLE defaults ADD assortmentnumber TEXT;
diff --git a/sql/Pg-upgrade2/assortment_charge.sql b/sql/Pg-upgrade2/assortment_charge.sql
new file mode 100644 (file)
index 0000000..c04397f
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: assortment_charge
+-- @description: Sortimentsartikel erweitert, bestimmen ob Artikel berechnet werden soll
+-- @depends: release_3_3_0 assortment_items
+
+ALTER TABLE assortment_items ADD COLUMN charge BOOLEAN DEFAULT TRUE;
diff --git a/sql/Pg-upgrade2/auto_delete_reconciliation_links_on_acc_trans_deletion.pl b/sql/Pg-upgrade2/auto_delete_reconciliation_links_on_acc_trans_deletion.pl
new file mode 100644 (file)
index 0000000..70e76f7
--- /dev/null
@@ -0,0 +1,26 @@
+# @tag: auto_delete_reconciliation_links_on_acc_trans_deletion
+# @description: Automatisch Einträge aus reconciliation_links entfernen, wenn referenzierte Einträge gelöscht werden
+# @depends: automatic_reconciliation
+package SL::DBUpgrade2::auto_delete_reconciliation_links_on_acc_trans_deletion;
+
+use utf8;
+use strict;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+sub run {
+  my ($self) = @_;
+
+  $self->drop_constraints(table => $_) for qw(reconciliation_links);
+
+  my @queries = (
+    q|ALTER TABLE reconciliation_links ADD CONSTRAINT reconciliation_links_acc_trans_id_fkey   FOREIGN KEY (acc_trans_id)        REFERENCES acc_trans         (acc_trans_id) ON DELETE CASCADE|,
+    q|ALTER TABLE reconciliation_links ADD CONSTRAINT reconciliation_links_bank_transaction_id FOREIGN KEY (bank_transaction_id) REFERENCES bank_transactions (id)           ON DELETE CASCADE|,
+  );
+
+  $self->db_query($_) for @queries;
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/auto_delete_sepa_export_items_on_ap_ar_deletion.pl b/sql/Pg-upgrade2/auto_delete_sepa_export_items_on_ap_ar_deletion.pl
new file mode 100644 (file)
index 0000000..f7bad04
--- /dev/null
@@ -0,0 +1,26 @@
+# @tag: auto_delete_sepa_export_items_on_ap_ar_deletion
+# @description: Automatisch Einträge aus reconciliation_links entfernen, wenn referenzierte Einträge gelöscht werden
+# @depends: sepa_in
+package SL::DBUpgrade2::auto_delete_sepa_export_items_on_ap_ar_deletion;
+
+use utf8;
+use strict;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+sub run {
+  my ($self) = @_;
+
+  $self->drop_constraints(table => $_) for qw(sepa_export_items);
+
+  my @queries = (
+    q|ALTER TABLE sepa_export_items ADD CONSTRAINT sepa_export_items_ar_id_fkey FOREIGN KEY (ar_id) REFERENCES ar (id) ON DELETE CASCADE|,
+    q|ALTER TABLE sepa_export_items ADD CONSTRAINT sepa_export_items_ap_id_fkey FOREIGN KEY (ap_id) REFERENCES ap (id) ON DELETE CASCADE|,
+  );
+
+  $self->db_query($_) for @queries;
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/background_job_change_create_periodic_invoices_to_daily.pl b/sql/Pg-upgrade2/background_job_change_create_periodic_invoices_to_daily.pl
deleted file mode 100644 (file)
index ace35be..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-# @tag: background_job_change_create_periodic_invoices_to_daily
-# @description: Hintergrundjob zum Erzeugen periodischer Rechnungen täglich ausführen
-# @depends: release_3_0_0
-package SL::DBUpgrade2::background_job_change_create_periodic_invoices_to_daily;
-
-use strict;
-use utf8;
-
-use parent qw(SL::DBUpgrade2::Base);
-
-use SL::DB::BackgroundJob;
-
-sub run {
-  my ($self) = @_;
-
-  foreach my $job (@{ SL::DB::Manager::BackgroundJob->get_all(where => [ package_name => 'CreatePeriodicInvoices' ]) }) {
-    $job->update_attributes(cron_spec => '0 3 * * *', next_run_at => undef);
-  }
-
-  return 1;
-}
-
-1;
diff --git a/sql/Pg-upgrade2/background_job_change_create_periodic_invoices_to_daily.sql b/sql/Pg-upgrade2/background_job_change_create_periodic_invoices_to_daily.sql
new file mode 100644 (file)
index 0000000..ac90bb9
--- /dev/null
@@ -0,0 +1,12 @@
+-- @tag: background_job_change_create_periodic_invoices_to_daily
+-- @description: Hintergrundjob zum Erzeugen periodischer Rechnungen täglich ausführen
+-- @depends: release_3_0_0
+UPDATE background_jobs
+SET cron_spec   = '0 3 * * *',
+    next_run_at = CAST(current_date AS timestamp) + CAST(
+                    (CASE
+                     WHEN extract('hour' FROM current_timestamp) < 3 THEN '3 hours'
+                     ELSE                                                 '1 day 3 hours'
+                     END) AS interval
+                  )
+WHERE package_name = 'CreatePeriodicInvoices';
diff --git a/sql/Pg-upgrade2/background_jobs_3.pl b/sql/Pg-upgrade2/background_jobs_3.pl
deleted file mode 100755 (executable)
index 65d07c9..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-# @tag: background_jobs_3
-# @description: Backgroundjob Cleanup einrichten
-# @depends: emmvee_background_jobs_2
-package SL::DBUpgrade2::background_jobs_3;
-
-use strict;
-use utf8;
-
-use parent qw(SL::DBUpgrade2::Base);
-
-use SL::BackgroundJob::BackgroundJobCleanup;
-
-sub run {
-  SL::BackgroundJob::BackgroundJobCleanup->create_job;
-  return 1;
-}
-
-1;
diff --git a/sql/Pg-upgrade2/background_jobs_3.sql b/sql/Pg-upgrade2/background_jobs_3.sql
new file mode 100644 (file)
index 0000000..f3bd765
--- /dev/null
@@ -0,0 +1,12 @@
+-- @tag: background_jobs_3
+-- @description: Backgroundjob Cleanup einrichten
+-- @depends: emmvee_background_jobs_2
+INSERT INTO background_jobs (type, package_name, active, cron_spec, next_run_at)
+VALUES ('interval', 'BackgroundJobCleanup', true, '0 3 * * *',
+  CAST(current_date AS timestamp) + CAST(
+    (CASE
+     WHEN extract('hour' FROM current_timestamp) < 3 THEN '3 hours'
+     ELSE                                                 '1 day 3 hours'
+     END) AS interval
+  )
+);
diff --git a/sql/Pg-upgrade2/background_jobs_clean_auth_sessions.pl b/sql/Pg-upgrade2/background_jobs_clean_auth_sessions.pl
deleted file mode 100644 (file)
index cab9196..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-# @tag: background_jobs_clean_auth_sessions
-# @description: Hintergrundjob zum Löschen abgelaufener Sessions
-# @depends: release_3_1_0
-package SL::DBUpgrade2::background_jobs_clean_auth_sessions;
-
-use strict;
-use utf8;
-
-use parent qw(SL::DBUpgrade2::Base);
-
-use SL::BackgroundJob::CleanAuthSessions;
-
-sub run {
-  my ($self) = @_;
-
-  SL::BackgroundJob::CleanAuthSessions->create_job;
-
-  return 1;
-}
-
-1;
diff --git a/sql/Pg-upgrade2/background_jobs_clean_auth_sessions.sql b/sql/Pg-upgrade2/background_jobs_clean_auth_sessions.sql
new file mode 100644 (file)
index 0000000..35430a3
--- /dev/null
@@ -0,0 +1,12 @@
+-- @tag: background_jobs_clean_auth_sessions
+-- @description: Hintergrundjob zum Löschen abgelaufener Sessions
+-- @depends: release_3_1_0
+INSERT INTO background_jobs (type, package_name, active, cron_spec, next_run_at)
+VALUES ('interval', 'CleanAuthSessions', true, '30 6 * * *',
+  CAST(current_date AS timestamp) + CAST(
+    (CASE
+     WHEN extract('hour' FROM current_timestamp) < 6 THEN '6 hours 30 minutes'
+     ELSE                                                 '1 day 6 hours 30 minutes'
+     END) AS interval
+  )
+);
diff --git a/sql/Pg-upgrade2/bank_account_flag_for_zugferd_usage.sql b/sql/Pg-upgrade2/bank_account_flag_for_zugferd_usage.sql
new file mode 100644 (file)
index 0000000..7935754
--- /dev/null
@@ -0,0 +1,15 @@
+-- @tag: bank_account_flag_for_zugferd_usage
+-- @description: Bankkonto für die Nutzung mit ZUGFeRD markieren
+-- @depends: release_3_5_5
+ALTER TABLE bank_accounts
+ADD COLUMN use_for_zugferd BOOLEAN;
+
+UPDATE bank_accounts
+SET use_for_zugferd = (
+  SELECT COUNT(*)
+  FROM bank_accounts
+) = 1;
+
+ALTER TABLE bank_accounts
+ALTER COLUMN use_for_zugferd SET DEFAULT FALSE,
+ALTER COLUMN use_for_zugferd SET NOT NULL;
diff --git a/sql/Pg-upgrade2/bank_account_informations_for_swiss_qrbill.sql b/sql/Pg-upgrade2/bank_account_informations_for_swiss_qrbill.sql
new file mode 100644 (file)
index 0000000..0c8c64c
--- /dev/null
@@ -0,0 +1,14 @@
+-- @tag: bank_account_information_for_swiss_qrbill
+-- @description: Bankkonto Informationen für Swiss QR-Bill hinzufügen
+-- @depends: release_3_5_6_1
+ALTER TABLE bank_accounts ADD COLUMN use_for_qrbill BOOLEAN;
+ALTER TABLE bank_accounts ADD COLUMN bank_account_id VARCHAR;
+
+UPDATE bank_accounts SET use_for_qrbill = (
+    SELECT COUNT(*)
+    FROM bank_accounts
+  ) = 1;
+
+ALTER TABLE bank_accounts
+  ALTER COLUMN use_for_qrbill SET DEFAULT FALSE,
+  ALTER COLUMN use_for_qrbill SET NOT NULL;
index c5eb53f..b997e00 100644 (file)
@@ -1,6 +1,5 @@
 -- @tag: bank_accounts_unique_chart_constraint
 -- @description: Bankkonto - Constraint für eindeutiges Konto
 -- @depends: release_3_2_0 bank_accounts
--- @encoding: utf-8
 
 ALTER TABLE bank_accounts ADD CONSTRAINT chart_id_unique UNIQUE (chart_id);
diff --git a/sql/Pg-upgrade2/bank_transaction_acc_trans_remove_wrong_primary_key.sql b/sql/Pg-upgrade2/bank_transaction_acc_trans_remove_wrong_primary_key.sql
new file mode 100644 (file)
index 0000000..8b27647
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: bank_transaction_acc_trans_remove_wrong_primary_key
+-- @description: bank_transaction_acc_trans_remove_wrong_primary_key
+-- @depends: release_3_5_4
+ALTER TABLE bank_transaction_acc_trans
+DROP COLUMN id;
diff --git a/sql/Pg-upgrade2/bank_transactions_check_constraint_invoice_amount.sql b/sql/Pg-upgrade2/bank_transactions_check_constraint_invoice_amount.sql
new file mode 100644 (file)
index 0000000..cb81293
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: bank_transactions_check_constraint_invoice_amount
+-- @description: Bank-Transaktionen dürfen mehrfach verbucht werden - Sicherheitscheck auf DB-Ebene, Überbuchen der Bankbewegung verbieten
+-- @depends: bank_transactions_type2 release_3_5_3
+
+ALTER TABLE bank_transactions ADD CHECK (abs(invoice_amount) <= abs(amount));
diff --git a/sql/Pg-upgrade2/bank_transactions_nuke_trailing_spaces_in_purpose.sql b/sql/Pg-upgrade2/bank_transactions_nuke_trailing_spaces_in_purpose.sql
new file mode 100644 (file)
index 0000000..8e9bb29
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: bank_transactions_nuke_trailing_spaces_in_purpose
+-- @description: Banktransaktionen: überflüssige Leerzeichen am Ende des Verwendungszwecks entfernen
+-- @depends: release_3_5_4
+UPDATE bank_transactions
+SET purpose = regexp_replace(purpose, ' +$', '')
+WHERE purpose ~ ' +$';
diff --git a/sql/Pg-upgrade2/bank_transactions_type.sql b/sql/Pg-upgrade2/bank_transactions_type.sql
new file mode 100644 (file)
index 0000000..2467656
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: bank_transactions_type
+-- @description: Erweitern der Tabelle bank_transactions mit Typ der Transaktion.
+-- @depends: bank_transactions
+
+ALTER TABLE bank_transactions ADD COLUMN transactioncode TEXT;
+ALTER TABLE bank_transactions ADD COLUMN transactiontext TEXT;
diff --git a/sql/Pg-upgrade2/bank_transactions_type2.sql b/sql/Pg-upgrade2/bank_transactions_type2.sql
new file mode 100644 (file)
index 0000000..05aa106
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: bank_transactions_type2
+-- @description: Spalten für Transaktions-Code und -Typen etwas besser benennen
+-- @depends: bank_transactions_type
+
+ALTER TABLE bank_transactions RENAME transactioncode TO transaction_code;
+ALTER TABLE bank_transactions RENAME transactiontext TO transaction_text;
index cb9e53b..654369a 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: bankaccounts_reconciliation
 -- @description: Kontenabgleichsststartdatum und -saldo
 -- @depends: release_3_2_0
--- @encoding: utf-8
 
 ALTER TABLE bank_accounts ADD COLUMN reconciliation_starting_date DATE;
 ALTER TABLE bank_accounts ADD COLUMN reconciliation_starting_balance numeric(15,5);
index cdda464..b8bbd3d 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: bankaccounts_sortkey_and_obsolete
 -- @description: Bankkonto - Sortierreihenfolge und Ungültig
 -- @depends: release_3_2_0
--- @encoding: utf-8
 
 -- default false needed so that get_all_sorted( query => [ obsolete => 0 ] ) works
 ALTER TABLE bank_accounts ADD COLUMN obsolete BOOLEAN NOT NULL DEFAULT false;
diff --git a/sql/Pg-upgrade2/change_warehouse_client_config_default.sql b/sql/Pg-upgrade2/change_warehouse_client_config_default.sql
new file mode 100644 (file)
index 0000000..4fbc6fe
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: change_warehouse_client_config_default
+-- @description: Konfigurations-Optionen für das Standard-Auslager-Verfahren zurückschrauben für negative Lagermengen
+-- @depends: release_3_5_7
+UPDATE defaults set transfer_default_ignore_onhand = 'f';
diff --git a/sql/Pg-upgrade2/chart_pos_er.sql b/sql/Pg-upgrade2/chart_pos_er.sql
new file mode 100644 (file)
index 0000000..38ef0b1
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: chart_pos_er
+-- @description: pos_er Feld in Konten für die Position in der Erfolgsrechnung
+-- @depends: release_3_3_0
+-- @may_fail: 1
+
+ALTER TABLE chart ADD COLUMN pos_er INTEGER;
+UPDATE chart SET pos_er = pos_eur;
index 7ca9cb8..b0a703c 100644 (file)
@@ -28,7 +28,7 @@ INSERT INTO tax
   (0, 0, 'Keine Steuer');
 SQL
     $self->db_query($insert_taxkey0);
-    print $::locale->text("taxkey 0 with taxrate 0 was created.");
+    print $::locale->text("taxkey 0 with taxrate 0 was created.");
   };
 
   my $insert_taxkeys = <<SQL;
index 5b5e3d9..8a7163f 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: check_bin_belongs_to_wh_trigger
 -- @description: Trigger, um sicher zu stellen, dass ein angegebener Lagerplatz auch zum Lager gehört.
 -- @depends: delivery_orders warehouse
--- @encoding: utf-8
 
 CREATE FUNCTION check_bin_belongs_to_wh() RETURNS "trigger"
   AS 'BEGIN
diff --git a/sql/Pg-upgrade2/clean_tax_18_19.pl b/sql/Pg-upgrade2/clean_tax_18_19.pl
new file mode 100644 (file)
index 0000000..423fa7a
--- /dev/null
@@ -0,0 +1,74 @@
+# @tag: clean_tax_18_19
+# @description: Vorbereitung für neue Steuerschlüssel 18,19
+# @depends: release_3_6_0 tax_reverse_charge
+# @ignore: 0
+package SL::DBUpgrade2::clean_tax_18_19;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+sub delete_alter_tax {
+  my $self = shift;
+
+  my $query = <<SQL;
+    SELECT id from tax
+    where taxkey = ?
+    and reverse_charge_chart_id is null
+SQL
+  my $q_fetch = <<SQL;
+    SELECT trans_id
+    FROM acc_trans where tax_id = ?
+    LIMIT 1
+SQL
+
+  my $delete_taxkey = <<SQL;
+    DELETE from taxkeys where tax_id = ?
+SQL
+
+  my $delete_tax = <<SQL;
+    DELETE from tax where         id = ?
+SQL
+
+
+  my $edit_tax = <<SQL;
+    UPDATE tax set chart_id = NULL
+    WHERE id = ?
+SQL
+
+
+  my $h_fetch   = $self->dbh->prepare($query);
+  my $acc_fetch = $self->dbh->prepare($q_fetch);
+  my $delete_tk = $self->dbh->prepare($delete_taxkey);
+  my $delete_t  = $self->dbh->prepare($delete_tax);
+  my $edit_q    = $self->dbh->prepare($edit_tax);
+
+
+  my $tax_id;
+  foreach ( qw(18 19) ) {
+    $h_fetch->execute($_) || $::form->dberror($query);
+    while (my $entry = $h_fetch->fetchrow_hashref) {
+      $tax_id = $entry->{id};
+      next unless $tax_id;
+      $edit_q->execute($tax_id)    || $::form->dberror($edit_tax);
+      $acc_fetch->execute($tax_id) || $::form->dberror($q_fetch);
+      if (!$acc_fetch->fetchrow_hashref) {
+        $delete_tk->execute($tax_id) || $::form->dberror($delete_tk);
+        $delete_t ->execute($tax_id) || $::form->dberror($delete_t);
+      }
+    }
+  }
+}
+
+sub run {
+  my ($self) = @_;
+
+  return 1 unless ($self->check_coa('Germany-DATEV-SKR03EU') ||$self->check_coa('Germany-DATEV-SKR04EU'));
+
+  $self->delete_alter_tax;
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/contact_departments_own_table.sql b/sql/Pg-upgrade2/contact_departments_own_table.sql
new file mode 100644 (file)
index 0000000..8535f1e
--- /dev/null
@@ -0,0 +1,15 @@
+-- @tag: contact_departments_own_table
+-- @description: Eigene Tabelle für Abteilungen bei Ansprechpersonen
+-- @depends: release_3_5_5
+
+CREATE TABLE contact_departments (
+  id          SERIAL,
+  description TEXT    NOT NULL,
+  PRIMARY KEY (id),
+  UNIQUE (description)
+);
+
+UPDATE contacts SET cp_abteilung = trim(cp_abteilung) WHERE cp_abteilung NOT LIKE trim(cp_abteilung);
+
+INSERT INTO contact_departments (description)
+  SELECT DISTINCT cp_abteilung FROM contacts WHERE cp_abteilung IS NOT NULL AND cp_abteilung NOT LIKE '' ORDER BY cp_abteilung;
diff --git a/sql/Pg-upgrade2/contact_titles_own_table.sql b/sql/Pg-upgrade2/contact_titles_own_table.sql
new file mode 100644 (file)
index 0000000..3fdaedb
--- /dev/null
@@ -0,0 +1,15 @@
+-- @tag: contact_titles_own_table
+-- @description: Eigene Tabelle für Titel bei Ansprechpersonen
+-- @depends: release_3_5_5
+
+CREATE TABLE contact_titles (
+  id          SERIAL,
+  description TEXT      NOT NULL,
+  PRIMARY KEY (id),
+  UNIQUE (description)
+);
+
+UPDATE contacts SET cp_title = trim(cp_title) WHERE cp_title NOT LIKE trim(cp_title);
+
+INSERT INTO contact_titles (description)
+  SELECT DISTINCT cp_title FROM contacts WHERE cp_title IS NOT NULL AND cp_title NOT LIKE '' ORDER BY cp_title;
diff --git a/sql/Pg-upgrade2/contacts_add_main_contact.pl b/sql/Pg-upgrade2/contacts_add_main_contact.pl
new file mode 100644 (file)
index 0000000..73af23c
--- /dev/null
@@ -0,0 +1,20 @@
+# @tag: contacts_add_main_contact
+# @description: Feld 'Hauptansprechpartner' für Kontakte
+# @depends: release_3_5_3
+package SL::DBUpgrade2::contacts_add_main_contact;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+sub run {
+  my ($self) = @_;
+
+  $self->db_query('ALTER TABLE contacts ADD COLUMN cp_main boolean DEFAULT false', may_fail => 1);
+  $self->db_query("UPDATE contacts set cp_main='false'");
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/convert_columns_to_html_for_sending_html_emails.pl b/sql/Pg-upgrade2/convert_columns_to_html_for_sending_html_emails.pl
new file mode 100644 (file)
index 0000000..168ea45
--- /dev/null
@@ -0,0 +1,62 @@
+# @tag: convert_columns_to_html_for_sending_html_emails
+# @description: Versand von E-Mails in HTML: mehrere Text-Spalten nach HTML umwandeln
+# @depends: release_3_5_8
+package SL::DBUpgrade2::convert_columns_to_html_for_sending_html_emails;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::HTML::Util;
+
+sub convert_column {
+  my ($self, $table, $id_column, $column_to_convert, $condition) = @_;
+
+  $condition = $condition ? "WHERE $condition" : "";
+
+  my $q_fetch = <<SQL;
+    SELECT ${id_column}, ${column_to_convert}
+    FROM ${table}
+    ${condition}
+SQL
+
+  my $q_update = <<SQL;
+    UPDATE ${table}
+    SET ${column_to_convert} = ?
+    WHERE ${id_column} = ?
+SQL
+
+  my $h_fetch = $self->dbh->prepare($q_fetch);
+  $h_fetch->execute || $::form->dberror($q_fetch);
+
+  my $h_update = $self->dbh->prepare($q_update);
+
+  while (my $entry = $h_fetch->fetchrow_hashref) {
+    $entry->{$column_to_convert} //= '';
+    my $new_value = SL::HTML::Util->plain_text_to_html($entry->{$column_to_convert});
+
+    next if $entry->{$column_to_convert} eq $new_value;
+
+    $h_update->execute($new_value, $entry->{id}) || $::form->dberror($q_update);
+  }
+}
+
+sub run {
+  my ($self) = @_;
+
+  $self->convert_column('defaults',                  'id', 'signature');
+  $self->convert_column('employee',                  'id', 'deleted_signature');
+  $self->convert_column('periodic_invoices_configs', 'id', 'email_body');
+  $self->convert_column('generic_translations',      'id', 'translation', <<SQL);
+    translation_type IN (
+      'preset_text_sales_quotation', 'preset_text_sales_order', 'preset_text_sales_delivery_order',
+      'preset_text_invoice', 'preset_text_invoice_direct_debit', 'preset_text_request_quotation',
+      'preset_text_purchase_order', 'preset_text_periodic_invoices_email_body'
+    )
+SQL
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/convert_columns_to_html_for_sending_html_emails2.pl b/sql/Pg-upgrade2/convert_columns_to_html_for_sending_html_emails2.pl
new file mode 100644 (file)
index 0000000..9c79104
--- /dev/null
@@ -0,0 +1,53 @@
+# @tag: convert_columns_to_html_for_sending_html_emails2
+# @description: Versand von E-Mails in HTML: weitere Text-Spalten nach HTML umwandeln
+# @depends: convert_columns_to_html_for_sending_html_emails
+package SL::DBUpgrade2::convert_columns_to_html_for_sending_html_emails2;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::HTML::Util;
+
+sub convert_column {
+  my ($self, $table, $id_column, $column_to_convert, $condition) = @_;
+
+  $condition = $condition ? "WHERE $condition" : "";
+
+  my $q_fetch = <<SQL;
+    SELECT ${id_column}, ${column_to_convert}
+    FROM ${table}
+    ${condition}
+SQL
+
+  my $q_update = <<SQL;
+    UPDATE ${table}
+    SET ${column_to_convert} = ?
+    WHERE ${id_column} = ?
+SQL
+
+  my $h_fetch = $self->dbh->prepare($q_fetch);
+  $h_fetch->execute || $::form->dberror($q_fetch);
+
+  my $h_update = $self->dbh->prepare($q_update);
+
+  while (my $entry = $h_fetch->fetchrow_hashref) {
+    $entry->{$column_to_convert} //= '';
+    my $new_value = SL::HTML::Util->plain_text_to_html($entry->{$column_to_convert});
+
+    next if $entry->{$column_to_convert} eq $new_value;
+
+    $h_update->execute($new_value, $entry->{id}) || $::form->dberror($q_update);
+  }
+}
+
+sub run {
+  my ($self) = @_;
+
+  $self->convert_column('dunning_config', 'id', 'email_body');
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/convert_drafts_to_record_templates.pl b/sql/Pg-upgrade2/convert_drafts_to_record_templates.pl
new file mode 100644 (file)
index 0000000..68c6a48
--- /dev/null
@@ -0,0 +1,327 @@
+# @tag: convert_drafts_to_record_templates
+# @description: Umwandlung von existierenden Entwürfen in Buchungsvorlagen für die Finanzbuchhaltung
+# @depends: create_record_template_tables
+package SL::DBUpgrade2::convert_drafts_to_record_templates;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::DBUtils;
+use SL::YAML;
+
+sub prepare_statements {
+  my ($self) = @_;
+
+  $self->{q_draft} = qq|
+    SELECT description, form, employee_id
+    FROM drafts
+    WHERE module = ?
+|;
+
+  $self->{q_template} = qq|
+    INSERT INTO record_templates (
+      template_name, template_type,  customer_id,    vendor_id,
+      currency_id,   department_id,  project_id,     employee_id,
+      taxincluded,   direct_debit,   ob_transaction, cb_transaction,
+      reference,     description,    ordnumber,      notes,
+      ar_ap_chart_id
+    ) VALUES (
+      ?, ? ,?, ?,
+      ?, ? ,?, ?,
+      ?, ? ,?, ?,
+      ?, ? ,?, ?,
+      ?
+    )
+    RETURNING id
+|;
+
+  $self->{q_item} = qq|
+    INSERT INTO record_template_items (
+      record_template_id,
+      chart_id, tax_id,  project_id,
+      amount1,  amount2, source, memo
+    ) VALUES (
+      ?,
+      ?, ?, ?,
+      ?, ?, ?, ?
+    )
+|;
+
+  $self->{h_draft}    = $self->dbh->prepare($self->{q_draft})    || die;
+  $self->{h_template} = $self->dbh->prepare($self->{q_template}) || die;
+  $self->{h_item}     = $self->dbh->prepare($self->{q_item})     || die;
+}
+
+sub fetch_auxilliary_data {
+  my ($self) = @_;
+
+  $self->{default_currency_id}  = selectfirst_hashref_query($::form, $self->dbh, qq|SELECT currency_id FROM defaults|)->{currency_id};
+  $self->{chart_ids_by_accno}   = { selectall_as_map($::form, $self->dbh, qq|SELECT id, accno FROM chart|,      'accno', 'id') };
+  $self->{currency_ids_by_name} = { selectall_as_map($::form, $self->dbh, qq|SELECT id, name  FROM currencies|, 'name',  'id') };
+}
+
+sub finish_statements {
+  my ($self) = @_;
+
+  $self->{h_item}->finish;
+  $self->{h_template}->finish;
+  $self->{h_draft}->finish;
+}
+
+sub migrate_ar_drafts {
+  my ($self) = @_;
+
+  $self->{h_draft}->execute('ar') || die $self->{h_draft}->errstr;
+
+  while (my $draft_record = $self->{h_draft}->fetchrow_hashref) {
+    my $draft       = SL::YAML::Load($draft_record->{form});
+    my $currency_id = $self->{currency_ids_by_name}->{$draft->{currency}};
+    my $employee_id = $draft_record->{employee_id} || $draft->{employee_id} || (split m{--}, $draft->{employee})[1] || undef;
+
+    next unless $currency_id;
+
+    my @values = (
+      # template_name, template_type, customer_id, vendor_id,
+      $draft_record->{description} // $::locale->text('unnamed record template'),
+      'ar_transaction',
+      $draft->{customer_id} || undef,
+      undef,
+
+      # currency_id, department_id, project_id, employee_id,
+      $currency_id,
+      $draft->{department_id}    || undef,
+      $draft->{globalproject_id} || undef,
+      $employee_id,
+
+      # taxincluded,   direct_debit, ob_transaction, cb_transaction,
+      $draft->{taxincluded}  ? 1 : 0,
+      $draft->{direct_debit} ? 1 : 0,
+      0,
+      0,
+
+      # reference, description, ordnumber, notes,
+      undef,
+      undef,
+      $draft->{ordnumber},
+      $draft->{notes},
+
+      # ar_ap_chart_id
+      $self->{chart_ids_by_accno}->{$draft->{ARselected}},
+    );
+
+    $self->{h_template}->execute(@values) || die $self->{h_template}->errstr;
+    my ($template_id) = $self->{h_template}->fetchrow_array;
+
+    foreach my $row (1..$draft->{rowcount}) {
+      my ($chart_accno) = split m{--}, $draft->{"AR_amount_${row}"};
+      my ($tax_id)      = split m{--}, $draft->{"taxchart_${row}"};
+      my $chart_id      = $self->{chart_ids_by_accno}->{$chart_accno // ''};
+      my $amount        = $::form->parse_amount($self->{format}, $draft->{"amount_${row}"});
+
+      # $tax_id may be 0 as there's an entry in tax with id = 0.
+      # $chart_id must not be 0 as there's no entry in chart with id = 0.
+      next unless $chart_id && (($tax_id // '') ne '');
+
+      @values = (
+        # record_template_id,
+        $template_id,
+
+        # chart_id, tax_id, project_id,
+        $chart_id,
+        $tax_id,
+        $draft->{"project_id_${row}"} || undef,
+
+        # amount1, amount2, source, memo
+        $amount,
+        undef,
+        undef,
+        undef,
+      );
+
+      $self->{h_item}->execute(@values) || die $self->{h_item}->errstr;
+    }
+  }
+}
+
+sub migrate_ap_drafts {
+  my ($self) = @_;
+
+  $self->{h_draft}->execute('ap') || die $self->{h_draft}->errstr;
+
+  while (my $draft_record = $self->{h_draft}->fetchrow_hashref) {
+    my $draft       = SL::YAML::Load($draft_record->{form});
+    my $currency_id = $self->{currency_ids_by_name}->{$draft->{currency}};
+    my $employee_id = $draft_record->{employee_id} || $draft->{employee_id} || (split m{--}, $draft->{employee})[1] || undef;
+
+    next unless $currency_id;
+
+    my @values = (
+      # template_name, template_type, customer_id, vendor_id,
+      $draft_record->{description} // $::locale->text('unnamed record template'),
+      'ap_transaction',
+      undef,
+      $draft->{vendor_id} || undef,
+
+      # currency_id, department_id, project_id, employee_id,
+      $currency_id,
+      $draft->{department_id}    || undef,
+      $draft->{globalproject_id} || undef,
+      $employee_id,
+
+      # taxincluded,   direct_debit, ob_transaction, cb_transaction,
+      $draft->{taxincluded}   ? 1 : 0,
+      $draft->{direct_credit} ? 1 : 0,
+      0,
+      0,
+
+      # reference, description, ordnumber, notes,
+      undef,
+      undef,
+      $draft->{ordnumber},
+      $draft->{notes},
+
+      # ar_ap_chart_id
+      $self->{chart_ids_by_accno}->{$draft->{APselected}},
+    );
+
+    $self->{h_template}->execute(@values) || die $self->{h_template}->errstr;
+    my ($template_id) = $self->{h_template}->fetchrow_array;
+
+    foreach my $row (1..$draft->{rowcount}) {
+      my ($chart_accno) = split m{--}, $draft->{"AP_amount_${row}"};
+      my ($tax_id)      = split m{--}, $draft->{"taxchart_${row}"};
+      my $chart_id      = $self->{chart_ids_by_accno}->{$chart_accno // ''};
+      my $amount        = $::form->parse_amount($self->{format}, $draft->{"amount_${row}"});
+
+      # $tax_id may be 0 as there's an entry in tax with id = 0.
+      # $chart_id must not be 0 as there's no entry in chart with id = 0.
+      next unless $chart_id && (($tax_id // '') ne '');
+
+      @values = (
+        # record_template_id,
+        $template_id,
+
+        # chart_id, tax_id, project_id,
+        $chart_id,
+        $tax_id,
+        $draft->{"project_id_${row}"} || undef,
+
+        # amount1, amount2, source, memo
+        $amount,
+        undef,
+        undef,
+        undef,
+      );
+
+      $self->{h_item}->execute(@values) || die $self->{h_item}->errstr;
+    }
+  }
+}
+
+sub migrate_gl_drafts {
+  my ($self) = @_;
+
+  $self->{h_draft}->execute('gl') || die $self->{h_draft}->errstr;
+
+  while (my $draft_record = $self->{h_draft}->fetchrow_hashref) {
+    my $draft       = SL::YAML::Load($draft_record->{form});
+    my $employee_id = $draft_record->{employee_id} || $draft->{employee_id} || (split m{--}, $draft->{employee})[1] || undef;
+
+    my @values = (
+      # template_name, template_type, customer_id, vendor_id,
+      $draft_record->{description} // $::locale->text('unnamed record template'),
+      'gl_transaction',
+      undef,
+      undef,
+
+      # currency_id, department_id, project_id, employee_id,
+      $self->{default_currency_id},
+      $draft->{department_id} || undef,
+      undef,
+      $employee_id,
+
+      # taxincluded,   direct_debit, ob_transaction, cb_transaction,
+      $draft->{taxincluded}    ? 1 : 0,
+      0,
+      $draft->{ob_transaction} ? 1 : 0,
+      $draft->{cb_transaction} ? 1 : 0,
+
+      # reference, description, ordnumber, notes,
+      $draft->{reference},
+      $draft->{description},
+      undef,
+      undef,
+
+      # ar_ap_chart_id
+      undef,
+    );
+
+    $self->{h_template}->execute(@values) || die $self->{h_template}->errstr;
+    my ($template_id) = $self->{h_template}->fetchrow_array;
+
+    foreach my $row (1..$draft->{rowcount}) {
+      my ($chart_accno) = split m{--}, $draft->{"accno_${row}"};
+      my ($tax_id)      = split m{--}, $draft->{"taxchart_${row}"};
+      my $chart_id      = $self->{chart_ids_by_accno}->{$chart_accno // ''};
+      my $debit         = $::form->parse_amount($self->{format}, $draft->{"debit_${row}"});
+      my $credit        = $::form->parse_amount($self->{format}, $draft->{"credit_${row}"});
+
+      # $tax_id may be 0 as there's an entry in tax with id = 0.
+      # $chart_id must not be 0 as there's no entry in chart with id = 0.
+      next unless $chart_id && (($tax_id // '') ne '');
+
+      @values = (
+        # record_template_id,
+        $template_id,
+
+        # chart_id, tax_id, project_id,
+        $chart_id,
+        $tax_id,
+        $draft->{"project_id_${row}"} || undef,
+
+        # amount1, amount2, source, memo
+        $debit,
+        $credit,
+        $draft->{"source_${row}"},
+        $draft->{"memo_${row}"},
+      );
+
+      $self->{h_item}->execute(@values) || die $self->{h_item}->errstr;
+    }
+  }
+}
+
+sub clean_drafts {
+  my ($self) = @_;
+
+  $self->db_query(qq|DELETE FROM drafts WHERE module IN ('ar', 'ap', 'gl')|);
+}
+
+sub run {
+  my ($self) = @_;
+
+  # A dummy for %::myconfig used for parsing numbers. The existing
+  # drafts have a fundamental flaw: they store numbers & dates in the
+  # database still formatted to the user's preferences. Determining
+  # the correct format is not possible. Therefore this script simply
+  # assumes that the installation is used by people with German
+  # preferences regarding both settings.
+  $self->{format} = {
+    numberformat => '1000,00',
+    dateformat   => 'dd.mm.yy',
+  };
+
+  $self->prepare_statements;
+  $self->fetch_auxilliary_data;
+  $self->migrate_ar_drafts;
+  $self->migrate_ap_drafts;
+  $self->migrate_gl_drafts;
+  $self->clean_drafts;
+  $self->finish_statements;
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/convert_real_qty.sql b/sql/Pg-upgrade2/convert_real_qty.sql
new file mode 100644 (file)
index 0000000..5e0ea65
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: convert_real_qty
+-- @description: Spaltentyp auf Numeric anstelle von Real für qty
+-- @depends: release_3_6_0
+ALTER TABLE orderitems ALTER column qty type numeric(25,5);
+ALTER TABLE invoice    ALTER column qty type numeric(25,5);
+
+
diff --git a/sql/Pg-upgrade2/create_part_customerprices.sql b/sql/Pg-upgrade2/create_part_customerprices.sql
new file mode 100644 (file)
index 0000000..98d2303
--- /dev/null
@@ -0,0 +1,18 @@
+-- @tag: create_part_customerprices
+-- @description: VK-Preis für jeden Kunden speichern und das Datum der Eingabe
+-- @depends: release_3_5_1
+
+CREATE TABLE part_customer_prices (
+  id          SERIAL PRIMARY KEY,
+  parts_id    integer NOT NULL,
+  customer_id integer NOT NULL,
+  customer_partnumber text DEFAULT '',
+  price      numeric(15,5) DEFAULT 0,
+  sortorder  integer DEFAULT 0,
+  lastupdate date DEFAULT now(),
+
+  FOREIGN KEY (parts_id)    REFERENCES parts (id),
+  FOREIGN KEY (customer_id) REFERENCES customer (id)
+);
+CREATE INDEX part_customer_prices_parts_id_key    ON part_customer_prices USING btree (parts_id);
+CREATE INDEX part_customer_prices_customer_id_key ON part_customer_prices USING btree (customer_id);
diff --git a/sql/Pg-upgrade2/create_part_if_not_found.sql b/sql/Pg-upgrade2/create_part_if_not_found.sql
new file mode 100644 (file)
index 0000000..0f62ac3
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: create_part_if_not_found
+-- @description: Falls Artikel nicht gefunden wird gleich in die Erfassung gehen
+-- @depends: release_3_2_0
+ALTER TABLE defaults ADD COLUMN create_part_if_not_found BOOLEAN DEFAULT FALSE;
+UPDATE defaults SET create_part_if_not_found = TRUE;
diff --git a/sql/Pg-upgrade2/create_record_template_tables.sql b/sql/Pg-upgrade2/create_record_template_tables.sql
new file mode 100644 (file)
index 0000000..01adfd3
--- /dev/null
@@ -0,0 +1,64 @@
+-- @tag: create_record_template_tables
+-- @description: Einführung echter Vorlagen in der Finanzbuchhaltung anstelle der Entwurfsfunktion
+-- @depends: release_3_4_1
+
+DROP TABLE IF EXISTS record_template_items;
+DROP TABLE IF EXISTS record_templates;
+DROP TYPE IF EXISTS record_template_type;
+
+CREATE TYPE record_template_type AS ENUM ('ar_transaction', 'ap_transaction', 'gl_transaction');
+CREATE TABLE record_templates (
+  id             SERIAL,
+  template_name  TEXT                 NOT NULL,
+  template_type  record_template_type NOT NULL,
+
+  customer_id    INTEGER,
+  vendor_id      INTEGER,
+  currency_id    INTEGER              NOT NULL,
+  department_id  INTEGER,
+  project_id     INTEGER,
+  employee_id    INTEGER,
+  taxincluded    BOOLEAN              NOT NULL DEFAULT FALSE,
+  direct_debit   BOOLEAN              NOT NULL DEFAULT FALSE,
+  ob_transaction BOOLEAN              NOT NULL DEFAULT FALSE,
+  cb_transaction BOOLEAN              NOT NULL DEFAULT FALSE,
+
+  reference      TEXT,
+  description    TEXT,
+  ordnumber      TEXT,
+  notes          TEXT,
+  ar_ap_chart_id INTEGER,
+
+  itime          TIMESTAMP            NOT NULL DEFAULT now(),
+  mtime          TIMESTAMP            NOT NULL DEFAULT now(),
+
+  PRIMARY KEY (id),
+  CONSTRAINT record_templates_customer_id_fkey    FOREIGN KEY (customer_id)    REFERENCES customer   (id) ON DELETE SET NULL,
+  CONSTRAINT record_templates_vendor_id_fkey      FOREIGN KEY (vendor_id)      REFERENCES vendor     (id) ON DELETE SET NULL,
+  CONSTRAINT record_templates_currency_id_fkey    FOREIGN KEY (currency_id)    REFERENCES currencies (id) ON DELETE CASCADE,
+  CONSTRAINT record_templates_department_id_fkey  FOREIGN KEY (department_id)  REFERENCES department (id) ON DELETE SET NULL,
+  CONSTRAINT record_templates_project_id_fkey     FOREIGN KEY (project_id)     REFERENCES project    (id) ON DELETE SET NULL,
+  CONSTRAINT record_templates_employee_id_fkey    FOREIGN KEY (employee_id)    REFERENCES employee   (id) ON DELETE SET NULL,
+  CONSTRAINT record_templates_ar_ap_chart_id_fkey FOREIGN KEY (ar_ap_chart_id) REFERENCES chart      (id) ON DELETE SET NULL
+);
+
+CREATE TRIGGER mtime_record_templates BEFORE UPDATE ON record_templates FOR EACH ROW EXECUTE PROCEDURE set_mtime();
+
+CREATE TABLE record_template_items (
+  id                 SERIAL,
+  record_template_id INTEGER         NOT NULL,
+
+  chart_id           INTEGER         NOT NULL,
+  tax_id             INTEGER         NOT NULL,
+  project_id         INTEGER,
+  amount1            NUMERIC (15, 5) NOT NULL,
+  amount2            NUMERIC (15, 5),
+  source             TEXT,
+  memo               TEXT,
+
+  PRIMARY KEY (id),
+  CONSTRAINT record_template_items_record_template_id FOREIGN KEY (record_template_id) REFERENCES record_templates (id) ON DELETE CASCADE,
+  CONSTRAINT record_template_items_chart_id           FOREIGN KEY (chart_id)           REFERENCES chart            (id) ON DELETE CASCADE,
+  CONSTRAINT record_template_items_tax_id             FOREIGN KEY (tax_id)             REFERENCES tax              (id) ON DELETE CASCADE,
+  CONSTRAINT record_template_items_project_id         FOREIGN KEY (project_id)         REFERENCES project          (id) ON DELETE SET NULL
+);
index b68b668..e3d8214 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: csv_import_reports_add_numheaders
 -- @description: Anzahl der Header-Zeilen in Csv Import Report speichern
 -- @depends: csv_import_report_cache
--- @encoding: utf-8
 
 ALTER TABLE csv_import_reports ADD COLUMN numheaders INTEGER;
 UPDATE csv_import_reports SET numheaders = 1;
diff --git a/sql/Pg-upgrade2/csv_mt940_add_profile.sql b/sql/Pg-upgrade2/csv_mt940_add_profile.sql
new file mode 100644 (file)
index 0000000..b7ef6bc
--- /dev/null
@@ -0,0 +1,16 @@
+-- @tag: csv_mt940_add_profile
+-- @description: Default Profile zum Importieren von mt940
+-- @depends: csv_import_profiles_2
+
+INSERT INTO csv_import_profiles (name,type,is_default,login) VALUES ('MT940','bank_transactions','t','default');
+INSERT INTO csv_import_profile_settings (csv_import_profile_id,key,value) VALUES ((SELECT id FROM csv_import_profiles WHERE name='MT940' AND login='default'),'charset','UTF-8');
+INSERT INTO csv_import_profile_settings (csv_import_profile_id,key,value) VALUES ((SELECT id FROM csv_import_profiles WHERE name='MT940' AND login='default'),'full_preview','0');
+INSERT INTO csv_import_profile_settings (csv_import_profile_id,key,value) VALUES ((SELECT id FROM csv_import_profiles WHERE name='MT940' AND login='default'),'update_policy','skip');
+INSERT INTO csv_import_profile_settings (csv_import_profile_id,key,value) VALUES ((SELECT id FROM csv_import_profiles WHERE name='MT940' AND login='default'),'numberformat','1000.00');
+INSERT INTO csv_import_profile_settings (csv_import_profile_id,key,value) VALUES ((SELECT id FROM csv_import_profiles WHERE name='MT940' AND login='default'),'sep_char',';');
+INSERT INTO csv_import_profile_settings (csv_import_profile_id,key,value) VALUES ((SELECT id FROM csv_import_profiles WHERE name='MT940' AND login='default'),'quote_char','"');
+INSERT INTO csv_import_profile_settings (csv_import_profile_id,key,value) VALUES ((SELECT id FROM csv_import_profiles WHERE name='MT940' AND login='default'),'escape_char','"');
+INSERT INTO csv_import_profile_settings (csv_import_profile_id,key,value) VALUES ((SELECT id FROM csv_import_profiles WHERE name='MT940' AND login='default'),'json_mappings','[]');
+INSERT INTO csv_import_profile_settings (csv_import_profile_id,key,value) VALUES ((SELECT id FROM csv_import_profiles WHERE name='MT940' AND login='default'),'duplicates','no_check');
+INSERT INTO csv_import_profile_settings (csv_import_profile_id,key,value) VALUES ((SELECT id FROM csv_import_profiles WHERE name='MT940' AND login='default'),'dont_edit_profile','1');
+
diff --git a/sql/Pg-upgrade2/custom_data_export.sql b/sql/Pg-upgrade2/custom_data_export.sql
new file mode 100644 (file)
index 0000000..b595932
--- /dev/null
@@ -0,0 +1,37 @@
+-- @tag: custom_data_export
+-- @description: Benutzerdefinierter Datenexport
+-- @depends: release_3_5_0
+CREATE TYPE custom_data_export_query_parameter_type_enum AS ENUM ('text', 'number', 'date', 'timestamp');
+
+CREATE TABLE custom_data_export_queries (
+  id           SERIAL,
+  name         TEXT      NOT NULL,
+  description  TEXT      NOT NULL,
+  sql_query    TEXT      NOT NULL,
+  access_right TEXT,
+  itime        TIMESTAMP NOT NULL DEFAULT now(),
+  mtime        TIMESTAMP NOT NULL DEFAULT now(),
+
+  PRIMARY KEY (id)
+);
+
+CREATE TABLE custom_data_export_query_parameters (
+  id             SERIAL,
+  query_id       INTEGER NOT NULL,
+  name           TEXT NOT NULL,
+  description    TEXT,
+  parameter_type custom_data_export_query_parameter_type_enum NOT NULL,
+  itime          TIMESTAMP NOT NULL DEFAULT now(),
+  mtime          TIMESTAMP NOT NULL DEFAULT now(),
+
+  PRIMARY KEY (id),
+  FOREIGN KEY (query_id) REFERENCES custom_data_export_queries (id) ON DELETE CASCADE
+);
+
+CREATE TRIGGER mtime_custom_data_export_queries
+BEFORE UPDATE ON custom_data_export_queries
+FOR EACH ROW EXECUTE PROCEDURE set_mtime();
+
+CREATE TRIGGER mtime_custom_data_export_query_parameters
+BEFORE UPDATE ON custom_data_export_query_parameters
+FOR EACH ROW EXECUTE PROCEDURE set_mtime();
diff --git a/sql/Pg-upgrade2/custom_data_export_default_values_for_parameters.sql b/sql/Pg-upgrade2/custom_data_export_default_values_for_parameters.sql
new file mode 100644 (file)
index 0000000..7ce5056
--- /dev/null
@@ -0,0 +1,15 @@
+-- @tag: custom_data_export_default_values_for_parameters
+-- @description: Bentuzerdefinierter Datenexport: Vorgabewerte für Parameter
+-- @depends: custom_data_export
+CREATE TYPE custom_data_export_query_parameter_default_value_type_enum AS ENUM ('none', 'current_user_login', 'sql_query', 'fixed_value');
+
+ALTER TABLE custom_data_export_query_parameters
+ADD COLUMN default_value_type custom_data_export_query_parameter_default_value_type_enum,
+ADD COLUMN default_value      TEXT;
+
+UPDATE custom_data_export_query_parameters
+SET default_value_type = 'none';
+
+ALTER TABLE custom_data_export_query_parameters
+ALTER COLUMN default_value_type
+SET NOT NULL;
index 6ba7052..44bc2a1 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: custom_variable_partsgroups
 -- @description: Beziehung zwischen cvar configs und partsgroups für Filter nach Warengruppen
 -- @depends: release_3_1_0
--- @charset: utf-8
 
 CREATE TABLE custom_variable_config_partsgroups (
   custom_variable_config_id integer NOT NULL,
diff --git a/sql/Pg-upgrade2/custom_variables_add_edit_position.sql b/sql/Pg-upgrade2/custom_variables_add_edit_position.sql
new file mode 100644 (file)
index 0000000..efb420d
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: custom_variables_add_edit_position
+-- @description: Erweiterung custom_variables
+-- @depends: release_3_5_6_1 custom_variables
+
+ALTER TABLE custom_variable_configs ADD COLUMN first_tab BOOLEAN NOT NULL DEFAULT FALSE;
+
diff --git a/sql/Pg-upgrade2/custom_variables_convert_width_height_to_pixels.pl b/sql/Pg-upgrade2/custom_variables_convert_width_height_to_pixels.pl
new file mode 100644 (file)
index 0000000..45b22f8
--- /dev/null
@@ -0,0 +1,59 @@
+# @tag: custom_variables_convert_width_height_to_pixels
+# @description: Benutzerdefinierte Variablen: Optionen »WIDTH« & »HEIGHT« nach Pixel konvertieren
+# @depends: release_3_5_8
+package SL::DBUpgrade2::custom_variables_convert_width_height_to_pixels;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::DBUtils;
+
+sub find_configs {
+  my ($self) = @_;
+
+  my $sql = <<SQL;
+    SELECT id, options
+    FROM custom_variable_configs
+    WHERE (COALESCE(options, '') ~ 'WIDTH=|HEIGHT=')
+      AND (type = 'textfield')
+SQL
+
+  return selectall_hashref_query($::form, $self->dbh, $sql);
+}
+
+sub fix_configs {
+  my ($self, $configs) = @_;
+
+  my $sql = <<SQL;
+    UPDATE custom_variable_configs
+    SET options = ?
+    WHERE id = ?
+SQL
+
+  my $update_h = prepare_query($::form, $self->dbh, $sql);
+
+  # Old defaults: 30 columns, 5 rows
+  # New defaults: 225px width, 90px height
+
+  foreach my $config (@{ $configs }) {
+    $config->{options} =~ s{WIDTH=(\d+)}{  int($1 * (225 / 30.0)) }eg;
+    $config->{options} =~ s{HEIGHT=(\d+)}{ int($1 * ( 90 /  5.0)) }eg;
+
+    $update_h->execute(@{$config}{qw(options id)}) || $self->db_error($sql);
+  }
+
+  $update_h->finish;
+}
+
+sub run {
+  my ($self) = @_;
+
+  my $configs = $self->find_configs;
+  $self->fix_configs($configs) if @{ $configs };
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/customer_add_commercial_court.sql b/sql/Pg-upgrade2/customer_add_commercial_court.sql
new file mode 100644 (file)
index 0000000..a896ffb
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: customer_add_commercial_court
+-- @description: Amtsgericht/Handelsgericht für Körperschaften bei den Stammdaten hinterlegen
+-- @depends: release_3_5_3
+ALTER TABLE customer ADD COLUMN commercial_court text;
diff --git a/sql/Pg-upgrade2/customer_add_fields.sql b/sql/Pg-upgrade2/customer_add_fields.sql
new file mode 100644 (file)
index 0000000..5bfd622
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: customer_add_fields
+-- @description: Rechnungsadresse (E-Mail-Empfänger) Herkunft der personenbezogenen Daten
+-- @depends: release_3_5_3
+ALTER TABLE customer ADD COLUMN invoice_mail text;
+ALTER TABLE customer ADD COLUMN contact_origin text;
diff --git a/sql/Pg-upgrade2/customer_add_generic_mail_delivery.sql b/sql/Pg-upgrade2/customer_add_generic_mail_delivery.sql
new file mode 100644 (file)
index 0000000..b091953
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: customer_add_generic_mail_delivery
+-- @description: Lieferschein (generischer E-Mail-Empfänger)
+-- @depends: release_3_5_3
+ALTER TABLE customer ADD COLUMN delivery_order_mail text;
+
diff --git a/sql/Pg-upgrade2/customer_add_postal_invoice.sql b/sql/Pg-upgrade2/customer_add_postal_invoice.sql
new file mode 100644 (file)
index 0000000..43a6c0c
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: customer_vendor_add_postal_invoice
+-- @description: neue Spalte für "Rechnungsempfang nur per Post" bei Kunden
+-- @depends: release_3_5_6_1
+
+ALTER TABLE customer ADD COLUMN postal_invoice BOOLEAN DEFAULT FALSE;
diff --git a/sql/Pg-upgrade2/customer_additional_billing_addresses.sql b/sql/Pg-upgrade2/customer_additional_billing_addresses.sql
new file mode 100644 (file)
index 0000000..11fcfb0
--- /dev/null
@@ -0,0 +1,45 @@
+-- @tag: customer_additional_billing_addresses
+-- @description: Kundenstammdaten: zusätzliche Rechnungsadressen
+-- @depends: release_3_5_8
+CREATE TABLE additional_billing_addresses (
+  id              SERIAL,
+  customer_id     INTEGER,
+  name            TEXT,
+  department_1    TEXT,
+  department_2    TEXT,
+  contact         TEXT,
+  street          TEXT,
+  zipcode         TEXT,
+  city            TEXT,
+  country         TEXT,
+  gln             TEXT,
+  email           TEXT,
+  phone           TEXT,
+  fax             TEXT,
+  default_address BOOLEAN NOT NULL DEFAULT FALSE,
+
+  itime           TIMESTAMP NOT NULL DEFAULT now(),
+  mtime           TIMESTAMP NOT NULL DEFAULT now(),
+
+  PRIMARY KEY (id),
+  FOREIGN KEY (customer_id) REFERENCES customer (id)
+);
+
+CREATE TRIGGER mtime_additional_billing_addresses
+BEFORE UPDATE ON additional_billing_addresses
+FOR EACH ROW EXECUTE PROCEDURE set_mtime();
+
+ALTER TABLE oe
+  ADD COLUMN billing_address_id INTEGER,
+  ADD FOREIGN KEY (billing_address_id)
+    REFERENCES additional_billing_addresses (id);
+
+ALTER TABLE delivery_orders
+  ADD COLUMN billing_address_id INTEGER,
+  ADD FOREIGN KEY (billing_address_id)
+    REFERENCES additional_billing_addresses (id);
+
+ALTER TABLE ar
+  ADD COLUMN billing_address_id INTEGER,
+  ADD FOREIGN KEY (billing_address_id)
+    REFERENCES additional_billing_addresses (id);
diff --git a/sql/Pg-upgrade2/customer_create_zugferd_invoices.sql b/sql/Pg-upgrade2/customer_create_zugferd_invoices.sql
new file mode 100644 (file)
index 0000000..388c8f3
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: customer_create_zugferd_invoices
+-- @description: Kundenstammdaten: Einstellungen für ZUGFeRD-Rechnungen
+-- @depends: release_3_5_5
+ALTER TABLE customer
+ADD COLUMN create_zugferd_invoices INTEGER
+DEFAULT -1 NOT NULL;
diff --git a/sql/Pg-upgrade2/customer_klass_rename_to_pricegroup_id_and_foreign_key.sql b/sql/Pg-upgrade2/customer_klass_rename_to_pricegroup_id_and_foreign_key.sql
new file mode 100644 (file)
index 0000000..b6b789d
--- /dev/null
@@ -0,0 +1,8 @@
+-- @tag: customer_klass_rename_to_pricegroup_id_and_foreign_key
+-- @description: klass nach pricegroup_id umbenannt
+-- @depends: release_3_4_1
+-- @ignore: 0
+
+ALTER TABLE customer ADD COLUMN pricegroup_id INTEGER REFERENCES pricegroup (id);
+UPDATE customer SET pricegroup_id = klass WHERE klass != 0;
+ALTER TABLE customer DROP COLUMN klass;
diff --git a/sql/Pg-upgrade2/customer_orderlock.sql b/sql/Pg-upgrade2/customer_orderlock.sql
new file mode 100644 (file)
index 0000000..7ce2c21
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: customer_orderlock
+-- @description: Boolean Auftragssperre benötigt bei shoporders
+-- @depends: release_3_4_1 shops
+-- @ignore: 0
+ALTER TABLE customer ADD COLUMN order_lock boolean default 'f';
diff --git a/sql/Pg-upgrade2/customer_remove_empty_additional_billing_addresses.sql b/sql/Pg-upgrade2/customer_remove_empty_additional_billing_addresses.sql
new file mode 100644 (file)
index 0000000..bb60358
--- /dev/null
@@ -0,0 +1,16 @@
+-- @tag: customer_remove_empty_additional_billing_addresses
+-- @description: Leere »zusätzliche Rechnungsadressen« entfernen
+-- @depends: customer_additional_billing_addresses
+DELETE
+FROM additional_billing_addresses
+WHERE (coalesce(name,         '') = '')
+  AND (coalesce(department_1, '') = '')
+  AND (coalesce(department_2, '') = '')
+  AND (coalesce(contact,      '') = '')
+  AND (coalesce(street,       '') = '')
+  AND (coalesce(zipcode,      '') = '')
+  AND (coalesce(city,         '') = '')
+  AND (coalesce(country,      '') = '')
+  AND (coalesce(email,        '') = '')
+  AND (coalesce(phone,        '') = '')
+  AND (coalesce(fax,          '') = '');
diff --git a/sql/Pg-upgrade2/customer_vendor_add_natural_person.sql b/sql/Pg-upgrade2/customer_vendor_add_natural_person.sql
new file mode 100644 (file)
index 0000000..875b74c
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: customer_vendor_add_natural_person
+-- @description: neue Spalte für "natürliche Person" bei Kunden/Lieferanten
+-- @depends: release_3_5_5
+
+ALTER TABLE customer ADD COLUMN natural_person BOOLEAN DEFAULT FALSE;
+ALTER TABLE vendor   ADD COLUMN natural_person BOOLEAN DEFAULT FALSE;
diff --git a/sql/Pg-upgrade2/customer_vendor_routing_id.sql b/sql/Pg-upgrade2/customer_vendor_routing_id.sql
new file mode 100644 (file)
index 0000000..9c868c4
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: customer_vendor_routing_id
+-- @description: Kundenstammdaten: Feld »Unsere Leitweg-ID beim Kunden«
+-- @depends: release_3_5_6
+ALTER TABLE customer
+ADD COLUMN c_vendor_routing_id TEXT;
diff --git a/sql/Pg-upgrade2/customer_vendor_shipto_add_gln.sql b/sql/Pg-upgrade2/customer_vendor_shipto_add_gln.sql
new file mode 100644 (file)
index 0000000..b0d0d64
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: customer_vendor_shipto_add_gln
+-- @description: Spalte für GLN bei Kunde/Lieferant und Lieferadresse
+-- @depends: release_3_3_0
+
+ALTER TABLE customer ADD COLUMN       gln TEXT;
+ALTER TABLE vendor   ADD COLUMN       gln TEXT;
+ALTER TABLE shipto   ADD COLUMN shiptogln TEXT;
diff --git a/sql/Pg-upgrade2/cvars_remove_dublicate_entries.pl b/sql/Pg-upgrade2/cvars_remove_dublicate_entries.pl
new file mode 100644 (file)
index 0000000..31e8232
--- /dev/null
@@ -0,0 +1,52 @@
+# @tag: cvars_remove_duplicate_entries
+# @description: Doppelte Einträge für gleiche benutzerdefinierte Variablen entfernen (behalte den Neusten).
+# @depends: release_3_4_1
+
+package SL::DBUpgrade2::cvars_remove_duplicate_entries;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::DBUtils;
+
+sub run {
+  my ($self) = @_;
+
+  # get all duplicates
+  my $query_all_dups = qq|
+    SELECT trans_id, config_id, sub_module FROM custom_variables
+      GROUP BY trans_id, config_id, sub_module
+      HAVING COUNT(*) > 1
+  |;
+
+  my $refs = selectall_hashref_query($::form, $self->dbh, $query_all_dups);
+
+  # remove all but the newest one (order by itime descending)
+  my $query_delete = qq|
+    DELETE FROM custom_variables WHERE id = ?;
+  |;
+  my $sth_delete = $self->dbh->prepare($query_delete);
+
+  my $query_all_but_newest = qq|
+      SELECT id FROM custom_variables WHERE trans_id = ? AND config_id = ? AND sub_module = ? ORDER BY itime DESC OFFSET 1
+  |;
+  my $sth_all_but_newest = $self->dbh->prepare($query_all_but_newest);
+
+  foreach my $ref (@$refs) {
+    my @to_delete_ids;
+    $sth_all_but_newest->execute($ref->{trans_id}, $ref->{config_id}, $ref->{sub_module}) || $::form->dberror($query_all_but_newest);
+    while (my ($row) = $sth_all_but_newest->fetchrow_array()) {
+      push(@to_delete_ids, $row);
+    }
+    ($sth_delete->execute($_) || $::form->dberror($query_delete)) for @to_delete_ids;
+  }
+
+  $sth_all_but_newest->finish;
+  $sth_delete->finish;
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/datev_export_format.sql b/sql/Pg-upgrade2/datev_export_format.sql
new file mode 100644 (file)
index 0000000..c4d79f7
--- /dev/null
@@ -0,0 +1,8 @@
+-- @tag: datev_export_format
+-- @description: Setzt die ausgehende Formatierung des DATEV-Exports
+-- @depends: release_3_5_1
+
+CREATE TYPE datev_export_format_enum AS ENUM ('cp1252', 'cp1252-translit', 'utf-8');
+
+ALTER TABLE defaults ADD COLUMN datev_export_format datev_export_format_enum default 'cp1252-translit';
+
diff --git a/sql/Pg-upgrade2/defaults_add_feature_experimental.sql b/sql/Pg-upgrade2/defaults_add_feature_experimental.sql
new file mode 100644 (file)
index 0000000..a1a429e
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: defaults_add_feature_experimental
+-- @description: Konfigurations-Option, ob experimentelle Features verwendet werden sollen.
+-- @depends: release_3_4_1
+
+ALTER TABLE defaults ADD COLUMN feature_experimental BOOLEAN NOT NULL DEFAULT TRUE;
diff --git a/sql/Pg-upgrade2/defaults_add_feature_experimental2.sql b/sql/Pg-upgrade2/defaults_add_feature_experimental2.sql
new file mode 100644 (file)
index 0000000..66136c9
--- /dev/null
@@ -0,0 +1,8 @@
+-- @tag: defaults_add_feature_experimental2
+-- @description: experimentelle Features mit einzelnen Optionen
+-- @depends: defaults_add_feature_experimental
+
+ALTER TABLE defaults RENAME COLUMN feature_experimental TO feature_experimental_order;
+ALTER TABLE defaults ADD    COLUMN feature_experimental_assortment BOOLEAN NOT NULL DEFAULT TRUE;
+
+UPDATE defaults SET feature_experimental_assortment = feature_experimental_order;
diff --git a/sql/Pg-upgrade2/defaults_add_features.sql b/sql/Pg-upgrade2/defaults_add_features.sql
new file mode 100644 (file)
index 0000000..897082c
--- /dev/null
@@ -0,0 +1,8 @@
+-- @tag: defaults_add_features
+-- @description: flags to switch on/off the features for the clients
+-- @depends: release_3_3_0
+ALTER TABLE defaults ADD COLUMN feature_balance BOOLEAN NOT NULL DEFAULT TRUE;
+ALTER TABLE defaults ADD COLUMN feature_datev BOOLEAN NOT NULL DEFAULT TRUE;
+ALTER TABLE defaults ADD COLUMN feature_erfolgsrechnung BOOLEAN NOT NULL DEFAULT FALSE;
+ALTER TABLE defaults ADD COLUMN feature_eurechnung BOOLEAN NOT NULL DEFAULT TRUE;
+ALTER TABLE defaults ADD COLUMN feature_ustva BOOLEAN NOT NULL DEFAULT TRUE;
diff --git a/sql/Pg-upgrade2/defaults_add_finanzamt_data.sql b/sql/Pg-upgrade2/defaults_add_finanzamt_data.sql
new file mode 100644 (file)
index 0000000..a65c860
--- /dev/null
@@ -0,0 +1,10 @@
+-- @tag: defaults_add_finanzamt_data
+-- @description: Fuer Umsatzsteuer Daten aus finanzamt.ini raus
+-- @depends: release_3_4_1
+ALTER TABLE defaults ADD COLUMN FA_BUFA_Nr text;
+ALTER TABLE defaults ADD COLUMN FA_dauerfrist text;
+ALTER TABLE defaults ADD COLUMN FA_steuerberater_city text;
+ALTER TABLE defaults ADD COLUMN FA_steuerberater_name text;
+ALTER TABLE defaults ADD COLUMN FA_steuerberater_street text;
+ALTER TABLE defaults ADD COLUMN FA_steuerberater_tel text;
+ALTER TABLE defaults ADD COLUMN FA_voranmeld text;
diff --git a/sql/Pg-upgrade2/defaults_add_precision.sql b/sql/Pg-upgrade2/defaults_add_precision.sql
new file mode 100644 (file)
index 0000000..88f854c
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: defaults_add_precision
+-- @description: adds new column 'precision' in table defaults, used to round amounts
+-- @depends: release_3_0_0
+ALTER TABLE defaults ADD COLUMN precision NUMERIC(15,5) NOT NULL DEFAULT(0.01);
+
diff --git a/sql/Pg-upgrade2/defaults_add_quick_search_modules.sql b/sql/Pg-upgrade2/defaults_add_quick_search_modules.sql
new file mode 100644 (file)
index 0000000..94f97a5
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: defaults_add_quick_search_modules
+-- @description: Mandantenkonfiguration für Schnellsuche
+-- @depends: release_3_4_0
+
+ALTER TABLE defaults ADD COLUMN quick_search_modules TEXT[];
+
+UPDATE defaults SET quick_search_modules = '{"contact","gl_transaction"}';
diff --git a/sql/Pg-upgrade2/defaults_add_rnd_accno_ids.sql b/sql/Pg-upgrade2/defaults_add_rnd_accno_ids.sql
new file mode 100644 (file)
index 0000000..9798dcd
--- /dev/null
@@ -0,0 +1,10 @@
+-- @tag: defaults_add_rnd_accno_ids
+-- @description: adds new columns 'rndgain_accno_id' and 'rndloss_accno_id' in table defaults, used to book roundings
+-- @depends: release_3_1_0
+ALTER TABLE defaults ADD COLUMN rndgain_accno_id Integer;
+ALTER TABLE defaults ADD COLUMN rndloss_accno_id Integer;
+UPDATE defaults SET ( rndgain_accno_id , rndloss_accno_id ) = (
+  (SELECT id FROM chart WHERE accno = '6953' LIMIT 1),
+  (SELECT id FROM chart WHERE accno = '6943' LIMIT 1)
+) WHERE coa LIKE 'Switzerland%';
+
diff --git a/sql/Pg-upgrade2/defaults_advance_payment_clearing_chart_id.sql b/sql/Pg-upgrade2/defaults_advance_payment_clearing_chart_id.sql
new file mode 100644 (file)
index 0000000..53da908
--- /dev/null
@@ -0,0 +1,32 @@
+-- @tag: defaults_advance_payment_clearing_chart_id
+-- @description: Voreingestelltes Konto für Verrechnung von Anzahlungen
+-- @depends: new_chart_1593_1495
+
+ALTER TABLE defaults ADD COLUMN advance_payment_clearing_chart_id INTEGER;
+
+DO $$
+BEGIN
+
+  IF ( SELECT coa FROM defaults ) = 'Germany-DATEV-SKR03EU' THEN
+    DECLARE
+      clearing_accno text := '1593';
+
+    BEGIN
+      IF ( SELECT COUNT(accno) FROM chart WHERE accno LIKE clearing_accno ) = 1 THEN
+        UPDATE defaults SET advance_payment_clearing_chart_id = (SELECT id FROM chart WHERE accno LIKE clearing_accno);
+      END IF;
+    END;
+  END IF;
+
+  IF ( SELECT coa FROM defaults ) = 'Germany-DATEV-SKR04EU' THEN
+    DECLARE
+      clearing_accno text := '1495';
+
+    BEGIN
+      IF ( SELECT COUNT(accno) FROM chart WHERE accno LIKE clearing_accno ) = 1 THEN
+        UPDATE defaults SET advance_payment_clearing_chart_id = (SELECT id FROM chart WHERE accno LIKE clearing_accno);
+      END IF;
+    END;
+  END IF;
+
+END $$;
diff --git a/sql/Pg-upgrade2/defaults_advance_payment_transfer_charts.sql b/sql/Pg-upgrade2/defaults_advance_payment_transfer_charts.sql
new file mode 100644 (file)
index 0000000..f77b257
--- /dev/null
@@ -0,0 +1,59 @@
+-- @tag: defaults_advance_payment_transfer_charts
+-- @description: Standardkonten für erhaltene versteuerte Anzahlungen 7% und 19% setzen
+-- @depends:new_chart_3260_1711 new_chart_3272_1718 defaults_advance_payment_clearing_chart_id
+
+
+ALTER TABLE defaults ADD COLUMN advance_payment_taxable_19_id INTEGER;
+ALTER TABLE defaults ADD COLUMN advance_payment_taxable_7_id  INTEGER;
+UPDATE chart set link ='AR' where id = (select advance_payment_clearing_chart_id from defaults);
+
+
+DO $$
+BEGIN
+
+  IF ( SELECT coa FROM defaults ) = 'Germany-DATEV-SKR03EU' THEN
+    DECLARE
+      clearing_accno text := '1718';
+
+    BEGIN
+      IF ( SELECT COUNT(accno) FROM chart WHERE accno LIKE clearing_accno ) = 1 THEN
+        UPDATE defaults SET advance_payment_taxable_19_id = (SELECT id FROM chart WHERE accno LIKE clearing_accno);
+      END IF;
+    END;
+  END IF;
+
+  IF ( SELECT coa FROM defaults ) = 'Germany-DATEV-SKR04EU' THEN
+    DECLARE
+      clearing_accno text := '3272';
+
+    BEGIN
+      IF ( SELECT COUNT(accno) FROM chart WHERE accno LIKE clearing_accno ) = 1 THEN
+        UPDATE defaults SET advance_payment_taxable_19_id = (SELECT id FROM chart WHERE accno LIKE clearing_accno);
+      END IF;
+    END;
+  END IF;
+
+  IF ( SELECT coa FROM defaults ) = 'Germany-DATEV-SKR03EU' THEN
+    DECLARE
+      clearing_accno text := '1711';
+
+    BEGIN
+      IF ( SELECT COUNT(accno) FROM chart WHERE accno LIKE clearing_accno ) = 1 THEN
+        UPDATE defaults SET advance_payment_taxable_7_id = (SELECT id FROM chart WHERE accno LIKE clearing_accno);
+      END IF;
+    END;
+  END IF;
+
+  IF ( SELECT coa FROM defaults ) = 'Germany-DATEV-SKR04EU' THEN
+    DECLARE
+      clearing_accno text := '3260';
+
+    BEGIN
+      IF ( SELECT COUNT(accno) FROM chart WHERE accno LIKE clearing_accno ) = 1 THEN
+        UPDATE defaults SET advance_payment_taxable_7_id = (SELECT id FROM chart WHERE accno LIKE clearing_accno);
+      END IF;
+    END;
+  END IF;
+
+
+END $$;
diff --git a/sql/Pg-upgrade2/defaults_bcc_to_login.sql b/sql/Pg-upgrade2/defaults_bcc_to_login.sql
new file mode 100644 (file)
index 0000000..6321fc0
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: defaults_bcc_to_login
+-- @description: BCC Email zu aktuellem Benutzer
+-- @depends: defaults_global_bcc
+
+ALTER TABLE defaults ADD bcc_to_login boolean NOT NULL DEFAULT FALSE;
+
diff --git a/sql/Pg-upgrade2/defaults_contact_departments_use_textfield.sql b/sql/Pg-upgrade2/defaults_contact_departments_use_textfield.sql
new file mode 100644 (file)
index 0000000..a44823e
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: defaults_contact_departments_use_textfield
+-- @description: Auswahl, ob Freitext-Feld für Abteilungen bei Ansprechpersonen im Kunden-/Lieferantenstamm angeboten wird
+-- @depends: release_3_5_5
+
+ALTER TABLE defaults ADD COLUMN contact_departments_use_textfield BOOLEAN;
+UPDATE defaults SET contact_departments_use_textfield = TRUE;
diff --git a/sql/Pg-upgrade2/defaults_contact_titles_use_textfield.sql b/sql/Pg-upgrade2/defaults_contact_titles_use_textfield.sql
new file mode 100644 (file)
index 0000000..dd71616
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: defaults_contact_titles_use_textfield
+-- @description: Auswahl, ob Freitext-Feld für Titel von Ansprechpersonen im Kunden-/Lieferantenstamm angeboten wird
+-- @depends: release_3_5_5
+
+ALTER TABLE defaults ADD COLUMN contact_titles_use_textfield BOOLEAN;
+UPDATE defaults SET contact_titles_use_textfield = TRUE;
diff --git a/sql/Pg-upgrade2/defaults_create_qrbill_data.sql b/sql/Pg-upgrade2/defaults_create_qrbill_data.sql
new file mode 100644 (file)
index 0000000..cffde9b
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: defaults_create_qrbill_data
+-- @description: Swiss QR-Bill Informationserzeugung Einstellungsoption
+-- @depends: release_3_5_6_1
+ALTER TABLE defaults ADD COLUMN create_qrbill_invoices BOOLEAN;
+UPDATE defaults SET create_qrbill_invoices = FALSE;
diff --git a/sql/Pg-upgrade2/defaults_create_zugferd_data.sql b/sql/Pg-upgrade2/defaults_create_zugferd_data.sql
new file mode 100644 (file)
index 0000000..aa938fe
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: defaults_create_zugferd_data
+-- @description: ZUGFeRD-Informationserzeugung option abstellen
+-- @depends: release_3_5_5
+ALTER TABLE defaults ADD COLUMN create_zugferd_invoices BOOLEAN;
+UPDATE defaults SET create_zugferd_invoices = TRUE;
diff --git a/sql/Pg-upgrade2/defaults_customer_vendor_ustid_taxnummer_unique.sql b/sql/Pg-upgrade2/defaults_customer_vendor_ustid_taxnummer_unique.sql
new file mode 100644 (file)
index 0000000..8b4c0d4
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: defaults_customer_vendor_ustid_taxnummer_unique
+-- @description: Mandanteneinstellung, ob UStId bzw. Steuernummer eindeutig sein sollen
+-- @depends: release_3_5_6_1
+
+ALTER TABLE defaults ADD customer_ustid_taxnummer_unique BOOLEAN DEFAULT FALSE;
+ALTER TABLE defaults ADD vendor_ustid_taxnummer_unique   BOOLEAN DEFAULT FALSE;
diff --git a/sql/Pg-upgrade2/defaults_delivery_date_interval.pl b/sql/Pg-upgrade2/defaults_delivery_date_interval.pl
new file mode 100644 (file)
index 0000000..db8e0ae
--- /dev/null
@@ -0,0 +1,19 @@
+# @tag: defaults_delivery_date_interval
+# @description: Einstellen des Liefertermins für Aufträge per Intervall (z.B.: +28 Tage)
+# @depends: release_3_5_3
+package SL::DBUpgrade2::defaults_delivery_date_interval;
+
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+use strict;
+
+sub run {
+  my ($self) = @_;
+
+  # this query will fail if column already exist (new database)
+  $self->db_query(qq|ALTER TABLE defaults ADD COLUMN delivery_date_interval integer DEFAULT 0|);
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/defaults_delivery_orders_check_stocked.sql b/sql/Pg-upgrade2/defaults_delivery_orders_check_stocked.sql
new file mode 100644 (file)
index 0000000..b719521
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: defaults_delivery_orders_check_stocked
+-- @description: Mandantenkonfiguration: Prüfung, ob Lieferscheine ausgelagert sein müssen für den Workflow zur Rechnung
+-- @depends: release_3_5_6_1
+
+ALTER TABLE defaults ADD COLUMN sales_delivery_order_check_stocked    BOOLEAN DEFAULT FALSE;
+ALTER TABLE defaults ADD COLUMN purchase_delivery_order_check_stocked BOOLEAN DEFAULT FALSE;
diff --git a/sql/Pg-upgrade2/defaults_doc_email_attachment.pl b/sql/Pg-upgrade2/defaults_doc_email_attachment.pl
new file mode 100644 (file)
index 0000000..87c4401
--- /dev/null
@@ -0,0 +1,20 @@
+# @tag: defaults_doc_email_attachment
+# @description: Einstellen der Haken für Anhänge beim Belegversand für E-Mails (Standard alle angehakt)
+# @depends: release_3_5_3
+package SL::DBUpgrade2::defaults_doc_email_attachment;
+
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+use strict;
+
+sub run {
+  my ($self) = @_;
+
+  $self->db_query(qq|ALTER TABLE defaults ADD COLUMN email_attachment_vc_files_checked boolean DEFAULT true|);
+  $self->db_query(qq|ALTER TABLE defaults ADD COLUMN email_attachment_part_files_checked boolean DEFAULT true|);
+  $self->db_query(qq|ALTER TABLE defaults ADD COLUMN email_attachment_record_files_checked boolean DEFAULT true|);
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/defaults_drop_delivery_plan_calculate_transferred_do.sql b/sql/Pg-upgrade2/defaults_drop_delivery_plan_calculate_transferred_do.sql
new file mode 100644 (file)
index 0000000..5ea2124
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: defaults_drop_delivery_plan_calculate_transferred_do
+-- @description: Entferne Einstellung für Lieferplan, nur ausgelagerte Lieferscheine zu berücksichtigen
+-- @depends: defaults_add_delivery_plan_config
+
+ALTER TABLE defaults DROP COLUMN delivery_plan_calculate_transferred_do;
diff --git a/sql/Pg-upgrade2/defaults_filemanagement_remove_doc_database.sql b/sql/Pg-upgrade2/defaults_filemanagement_remove_doc_database.sql
new file mode 100644 (file)
index 0000000..4d54119
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: defaults_filemanagement_remove_doc_database
+-- @description: "Unbenutze Spalte für Dateimanagement-Speichertyp Datenbank entfernen"
+-- @depends: filemanagement_feature
+
+ALTER TABLE defaults DROP COLUMN doc_database;
diff --git a/sql/Pg-upgrade2/defaults_invoice_mail_priority.pl b/sql/Pg-upgrade2/defaults_invoice_mail_priority.pl
new file mode 100644 (file)
index 0000000..602a7ef
--- /dev/null
@@ -0,0 +1,20 @@
+# @tag: defaults_invoice_mail_priority
+# @description: Einstellen der Priorität der generischen E-Mail für Rechnungen (Verkauf)
+# @depends: release_3_5_3
+package SL::DBUpgrade2::defaults_invoice_mail_priority;
+
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+use strict;
+
+sub run {
+  my ($self) = @_;
+
+  # this query will fail if column already exist (new database)
+  $self->db_query(qq|CREATE TYPE invoice_mail_settings AS ENUM ('cp', 'invoice_mail', 'invoice_mail_cc_cp');
+                     ALTER TABLE defaults ADD COLUMN invoice_mail_settings invoice_mail_settings default 'cp'|);
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/defaults_invoice_prevent_browser_back.sql b/sql/Pg-upgrade2/defaults_invoice_prevent_browser_back.sql
new file mode 100644 (file)
index 0000000..c12e82b
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: defaults_invoice_prevent_browser_back
+-- @description: Verhinderung Browser-Zurück-Knopf einstellbar in Mandantenkonfiguration
+-- @depends: release_3_6_0
+
+ALTER TABLE defaults ADD COLUMN invoice_prevent_browser_back boolean NOT NULL DEFAULT FALSE;
diff --git a/sql/Pg-upgrade2/defaults_invoice_warn_no_delivery_order.sql b/sql/Pg-upgrade2/defaults_invoice_warn_no_delivery_order.sql
new file mode 100644 (file)
index 0000000..0038cab
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: defaults_invoice_warn_no_delivery_order
+-- @description: Mandantenkonfiguration: Warnung bei fehlendem Lieferschein als Vorgänger zur Rechnung
+-- @depends: release_3_5_8
+
+ALTER TABLE defaults ADD COLUMN warn_no_delivery_order_for_invoice BOOLEAN DEFAULT FALSE;
diff --git a/sql/Pg-upgrade2/defaults_order_controller.sql b/sql/Pg-upgrade2/defaults_order_controller.sql
new file mode 100644 (file)
index 0000000..0051710
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: defaults_order_controller
+-- @description: Mandantenkonfiguration: Order-Controller auf aktiv setzen
+-- @depends: release_3_5_8
+UPDATE defaults SET feature_experimental_order = TRUE;
diff --git a/sql/Pg-upgrade2/defaults_order_warn_duplicate_parts.sql b/sql/Pg-upgrade2/defaults_order_warn_duplicate_parts.sql
new file mode 100644 (file)
index 0000000..292661e
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: defaults_order_warn_duplicate_parts
+-- @description: Mandantenkonfiguration: Warnung bei doppelten Artikeln in Aufträgen
+-- @depends: release_3_3_0
+
+ALTER TABLE defaults ADD COLUMN order_warn_duplicate_parts BOOLEAN DEFAULT TRUE;
diff --git a/sql/Pg-upgrade2/defaults_order_warn_no_cusordnumber.sql b/sql/Pg-upgrade2/defaults_order_warn_no_cusordnumber.sql
new file mode 100644 (file)
index 0000000..e101bb0
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: defaults_order_warn_no_cusordnumber
+-- @description: Mandantenkonfiguration: Warnung bei fehlender Kundenbestellnummer in Verkaufsaufträgen
+-- @depends: release_3_5_8
+
+ALTER TABLE defaults ADD COLUMN order_warn_no_cusordnumber BOOLEAN DEFAULT FALSE;
diff --git a/sql/Pg-upgrade2/defaults_order_warn_no_deliverydate.sql b/sql/Pg-upgrade2/defaults_order_warn_no_deliverydate.sql
new file mode 100644 (file)
index 0000000..6ec78a3
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: defaults_order_warn_no_deliverydate
+-- @description: Mandantenkonfiguration: Warnung falls kein Liefertermin eingetragen wurden
+-- @depends: release_3_5_2
+
+ALTER TABLE defaults ADD COLUMN order_warn_no_deliverydate BOOLEAN DEFAULT TRUE;
diff --git a/sql/Pg-upgrade2/defaults_partsgroup_required.sql b/sql/Pg-upgrade2/defaults_partsgroup_required.sql
new file mode 100644 (file)
index 0000000..5250002
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: defaults_partsgroup_required
+-- @description: New setting to check that partsgroup is set when saving parts
+-- @depends: release_3_5_8
+
+ALTER TABLE defaults ADD COLUMN partsgroup_required boolean NOT NULL DEFAULT false;
diff --git a/sql/Pg-upgrade2/defaults_posting_records_add.sql b/sql/Pg-upgrade2/defaults_posting_records_add.sql
new file mode 100644 (file)
index 0000000..0fcb155
--- /dev/null
@@ -0,0 +1,8 @@
+-- @tag: defaults_posting_records_add
+-- @description: Einstellung, ob Belege (PDF) zu einer Buchung hinzugefügt werden sollen
+-- @depends: release_3_5_6_1
+
+ALTER TABLE defaults ADD COLUMN ir_add_doc BOOLEAN NOT NULL DEFAULT true;
+ALTER TABLE defaults ADD COLUMN ar_add_doc BOOLEAN NOT NULL DEFAULT true;
+ALTER TABLE defaults ADD COLUMN ap_add_doc BOOLEAN NOT NULL DEFAULT true;
+ALTER TABLE defaults ADD COLUMN gl_add_doc BOOLEAN NOT NULL DEFAULT true;
diff --git a/sql/Pg-upgrade2/defaults_posting_records_default_false.sql b/sql/Pg-upgrade2/defaults_posting_records_default_false.sql
new file mode 100644 (file)
index 0000000..30a4172
--- /dev/null
@@ -0,0 +1,13 @@
+-- @tag: defaults_posting_records_default_false
+-- @description: Einstellung, ob Belege (PDF) zu einer Buchung hinzugefügt werden sollen
+-- @depends: release_3_5_6_1 defaults_posting_records_add
+
+ALTER TABLE defaults ALTER COLUMN ir_add_doc SET DEFAULT false;
+ALTER TABLE defaults ALTER COLUMN ar_add_doc SET DEFAULT false;
+ALTER TABLE defaults ALTER COLUMN ap_add_doc SET DEFAULT false;
+ALTER TABLE defaults ALTER COLUMN gl_add_doc SET DEFAULT false;
+
+UPDATE defaults set ir_add_doc='false';
+UPDATE defaults set ar_add_doc='false';
+UPDATE defaults set ap_add_doc='false';
+UPDATE defaults set gl_add_doc='false';
diff --git a/sql/Pg-upgrade2/defaults_print_interpolate_variables_in_positions.sql b/sql/Pg-upgrade2/defaults_print_interpolate_variables_in_positions.sql
new file mode 100644 (file)
index 0000000..04aa917
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: defaults_print_interpolate_variables_in_positions
+-- @description: Drucken: Variablen in Belegpositionen interpolieren (abschaltbar via Mandantenkonfiguration)
+-- @depends: release_3_5_8
+ALTER TABLE defaults
+ADD COLUMN print_interpolate_variables_in_positions BOOLEAN
+DEFAULT TRUE NOT NULL;
diff --git a/sql/Pg-upgrade2/defaults_produce_assembly_transfer_service.sql b/sql/Pg-upgrade2/defaults_produce_assembly_transfer_service.sql
new file mode 100644 (file)
index 0000000..775013c
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: defaults_produce_assembly_transfer_service
+-- @description: Mandantenkonfiguration: Erzeugnis mit Dienstleistungen, Dienstleistung kann verbraucht werden
+-- @depends: release_3_5_7
+
+ALTER TABLE defaults ADD COLUMN produce_assembly_transfer_service  BOOLEAN DEFAULT FALSE;
diff --git a/sql/Pg-upgrade2/defaults_qrbill_variants.sql b/sql/Pg-upgrade2/defaults_qrbill_variants.sql
new file mode 100644 (file)
index 0000000..622e708
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: defaults_qrbill_variants
+-- @description: Varianten für QR-Rechnung Auswahl
+-- @depends: defaults_create_qrbill_data
+ALTER TABLE defaults
+ALTER COLUMN create_qrbill_invoices TYPE INTEGER
+USING create_qrbill_invoices::INTEGER;
diff --git a/sql/Pg-upgrade2/defaults_req_delivery_date.pl b/sql/Pg-upgrade2/defaults_req_delivery_date.pl
new file mode 100644 (file)
index 0000000..0ee0c96
--- /dev/null
@@ -0,0 +1,20 @@
+# @tag: defaults_req_delivery_date
+# @description: Einstellung ob Liefertermin oder Gültigkeitstermin überhaupt gesetzt werden soll
+# @depends: release_3_5_6_1
+package SL::DBUpgrade2::defaults_req_delivery_date;
+
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+use strict;
+
+sub run {
+  my ($self) = @_;
+
+  # this query will fail if column already exist (new database)
+  $self->db_query(qq|ALTER TABLE defaults ADD COLUMN reqdate_on boolean DEFAULT true|);
+  $self->db_query(qq|ALTER TABLE defaults ADD COLUMN deliverydate_on boolean DEFAULT true|);
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/defaults_sales_purchase_record_numbers_changeable.sql b/sql/Pg-upgrade2/defaults_sales_purchase_record_numbers_changeable.sql
new file mode 100644 (file)
index 0000000..d7dce55
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: defaults_sales_purchase_record_numbers_changeable
+-- @description: Verkauf: Belegnummern nicht mehr ändern können
+-- @depends: release_3_5_8
+ALTER TABLE defaults
+ADD COLUMN sales_purchase_record_numbers_changeable BOOLEAN
+DEFAULT FALSE NOT NULL;
diff --git a/sql/Pg-upgrade2/defaults_set_dunning_creator.sql b/sql/Pg-upgrade2/defaults_set_dunning_creator.sql
new file mode 100644 (file)
index 0000000..7a89b43
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: defaults_set_dunning_creator
+-- @description: Ersteller der Mahnungen konfigurierbar machen
+-- @depends: release_3_5_3
+
+CREATE TYPE dunning_creator AS ENUM ('current_employee', 'invoice_employee');
+ALTER TABLE defaults ADD COLUMN dunning_creator dunning_creator default 'current_employee';
+
diff --git a/sql/Pg-upgrade2/defaults_show_longdescription_select_item.sql b/sql/Pg-upgrade2/defaults_show_longdescription_select_item.sql
new file mode 100644 (file)
index 0000000..3bb03e3
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: defaults_show_longdescription_select_item
+-- @description: Mandantenkonfiguration: Optional Langtext in Auswahlliste bei Artikelauswahl anzeigen
+-- @depends: release_3_3_0
+ALTER TABLE defaults ADD COLUMN show_longdescription_select_item    boolean DEFAULT FALSE;
diff --git a/sql/Pg-upgrade2/defaults_split_address.pl b/sql/Pg-upgrade2/defaults_split_address.pl
new file mode 100644 (file)
index 0000000..a1a4653
--- /dev/null
@@ -0,0 +1,53 @@
+# @tag: defaults_split_address
+# @description: Adress-Feld in Mandantenkonfiguration in einzelne Bestandteile aufteilen
+# @depends: release_3_5_4
+package SL::DBUpgrade2::defaults_split_address;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+sub run {
+  my ($self) = @_;
+
+  my ($address) = $self->dbh->selectrow_array("SELECT address FROM defaults");
+
+  my (@street, $zipcode, $city, $country);
+  my @lines = grep { $_ } split m{\r*\n+}, $address // '';
+
+  foreach my $line (@lines) {
+    if ($line =~ m{^(?:[a-z]+[ -])?(\d+) +(.+)}i) {
+      ($zipcode, $city) = ($1, $2);
+
+    } elsif ($zipcode) {
+      $country = $line;
+
+    } else {
+      push @street, $line;
+    }
+  }
+
+  $self->db_query(<<SQL);
+    ALTER TABLE defaults
+    ADD COLUMN  address_street1 TEXT,
+    ADD COLUMN  address_street2 TEXT,
+    ADD COLUMN  address_zipcode TEXT,
+    ADD COLUMN  address_city    TEXT,
+    ADD COLUMN  address_country TEXT,
+    DROP COLUMN address
+SQL
+
+  $self->db_query(<<SQL, bind => [ map { $_ // '' } ($street[0], $street[1], $zipcode, $city, $country) ]);
+    UPDATE defaults
+    SET address_street1 = ?,
+        address_street2 = ?,
+        address_zipcode = ?,
+        address_city    = ?,
+        address_country = ?
+SQL
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/defaults_transfer_settings.sql b/sql/Pg-upgrade2/defaults_transfer_settings.sql
new file mode 100644 (file)
index 0000000..5974fce
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: defaults_transfer_settings
+-- @description: Mandantenkonfiguration: Erzeugnis nur im gleichen Lager fertigen und Dienstleistungen für Auslagerstatus im Lieferschein ignorieren
+-- @depends: release_3_5_6_1
+
+ALTER TABLE defaults ADD COLUMN sales_delivery_order_check_service    BOOLEAN DEFAULT TRUE;
+ALTER TABLE defaults ADD COLUMN purchase_delivery_order_check_service    BOOLEAN DEFAULT TRUE;
+ALTER TABLE defaults ADD COLUMN produce_assembly_same_warehouse BOOLEAN DEFAULT TRUE;
diff --git a/sql/Pg-upgrade2/defaults_vc_greetings_use_textfield.sql b/sql/Pg-upgrade2/defaults_vc_greetings_use_textfield.sql
new file mode 100644 (file)
index 0000000..b5729cf
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: defaults_vc_greetings_use_textfield
+-- @description: Auswahl, ob Freitext-Feld für Anrede im Kunden-/Lieferantenstamm angeboten wird
+-- @depends: release_3_5_5
+
+ALTER TABLE defaults ADD COLUMN vc_greetings_use_textfield BOOLEAN;
+UPDATE defaults SET vc_greetings_use_textfield = TRUE;
diff --git a/sql/Pg-upgrade2/defaults_view_record_links.sql b/sql/Pg-upgrade2/defaults_view_record_links.sql
new file mode 100644 (file)
index 0000000..fc142bb
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: defaults_view_record_links
+-- @description: Mandantenkonfiguration: Sichtweise für record links immer vom Auftrag
+-- @depends: release_3_5_8
+
+ALTER TABLE defaults ADD COLUMN always_record_links_from_order BOOLEAN DEFAULT FALSE;
diff --git a/sql/Pg-upgrade2/defaults_workflow_po_ap_chart_id.sql b/sql/Pg-upgrade2/defaults_workflow_po_ap_chart_id.sql
new file mode 100644 (file)
index 0000000..93ae0d0
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: defaults_workflow_po_ap_chart_id
+-- @description: Voreingestelltes Konto für Workflow Lieferantenauftrag -> Kreditorenbuchung
+-- @depends: release_3_5_4
+
+ALTER TABLE defaults ADD COLUMN workflow_po_ap_chart_id INTEGER;
diff --git a/sql/Pg-upgrade2/defaults_year_end_charts.sql b/sql/Pg-upgrade2/defaults_year_end_charts.sql
new file mode 100644 (file)
index 0000000..128c3ff
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: defaults_year_end_charts
+-- @description: Standardkonten für Jahresabschluß
+-- @depends: release_3_5_4
+
+ALTER TABLE defaults ADD COLUMN carry_over_account_chart_id     INTEGER REFERENCES chart(id);
+ALTER TABLE defaults ADD COLUMN profit_carried_forward_chart_id INTEGER REFERENCES chart(id);
+ALTER TABLE defaults ADD COLUMN loss_carried_forward_chart_id   INTEGER REFERENCES chart(id);
diff --git a/sql/Pg-upgrade2/defaults_zugferd_test_mode.sql b/sql/Pg-upgrade2/defaults_zugferd_test_mode.sql
new file mode 100644 (file)
index 0000000..924b3df
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: defaults_zugferd_test_mode
+-- @description: ZUGFeRD optional nur im Test-Modus
+-- @depends: defaults_create_zugferd_data
+ALTER TABLE defaults
+ALTER COLUMN create_zugferd_invoices TYPE INTEGER
+USING create_zugferd_invoices::INTEGER;
diff --git a/sql/Pg-upgrade2/delete_cvars_on_trans_deletion_add_shipto.sql b/sql/Pg-upgrade2/delete_cvars_on_trans_deletion_add_shipto.sql
new file mode 100644 (file)
index 0000000..d2f887b
--- /dev/null
@@ -0,0 +1,53 @@
+-- @tag: delete_cvars_on_trans_deletion_add_shipto
+-- @description: Löschen von benutzerdefinierten Variablen via Triggerfunktionen auch für shipto
+-- @depends: delete_cvars_on_trans_deletion delete_cvars_on_trans_deletion_fix1
+
+-- 1.6 Alle benutzerdefinierten Variablen löschen, für die es keine
+-- Einträge in shipto mehr gibt.
+DELETE FROM custom_variables WHERE id IN
+  (SELECT cv.id FROM custom_variables cv LEFT JOIN custom_variable_configs cvc ON (cv.config_id = cvc.id)
+   WHERE module LIKE 'ShipTo'
+     AND NOT EXISTS (SELECT shipto_id FROM shipto WHERE shipto_id = cv.trans_id));
+
+
+-- 2.2. Nun die Funktionen, die als Trigger aufgerufen wird und die
+-- entscheidet, wie genau zu löschen ist:
+CREATE OR REPLACE FUNCTION delete_custom_variables_trigger()
+RETURNS TRIGGER AS $$
+  BEGIN
+    IF (TG_TABLE_NAME IN ('orderitems', 'delivery_order_items', 'invoice')) THEN
+      PERFORM delete_custom_variables_with_sub_module('IC', TG_TABLE_NAME, old.id);
+    END IF;
+
+    IF (TG_TABLE_NAME = 'parts') THEN
+      PERFORM delete_custom_variables_with_sub_module('IC', '', old.id);
+    END IF;
+
+    IF (TG_TABLE_NAME IN ('customer', 'vendor')) THEN
+      PERFORM delete_custom_variables_with_sub_module('CT', '', old.id);
+    END IF;
+
+    IF (TG_TABLE_NAME = 'contacts') THEN
+      PERFORM delete_custom_variables_with_sub_module('Contacts', '', old.cp_id);
+    END IF;
+
+    IF (TG_TABLE_NAME = 'project') THEN
+      PERFORM delete_custom_variables_with_sub_module('Projects', '', old.id);
+    END IF;
+
+    IF (TG_TABLE_NAME = 'shipto') THEN
+      PERFORM delete_custom_variables_with_sub_module('ShipTo', '', old.shipto_id);
+    END IF;
+
+    RETURN old;
+  END;
+$$ LANGUAGE plpgsql;
+
+-- 3. Die eigentlichen Trigger erstellen:
+
+-- 3.9. shipto
+DROP TRIGGER IF EXISTS shipto_delete_custom_variables_after_deletion ON shipto;
+
+CREATE TRIGGER shipto_delete_custom_variables_after_deletion
+AFTER DELETE ON shipto
+FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger();
diff --git a/sql/Pg-upgrade2/delete_from_generic_translations_on_language_deletion.pl b/sql/Pg-upgrade2/delete_from_generic_translations_on_language_deletion.pl
new file mode 100644 (file)
index 0000000..24ee4eb
--- /dev/null
@@ -0,0 +1,35 @@
+# @tag: delete_from_generic_translations_on_language_deletion
+# @description: Übersetzungen automatisch löschen, wenn die dazugehörige Sprache gelöscht wird
+# @depends: release_3_4_0
+package SL::DBUpgrade2::delete_from_generic_translations_on_language_deletion;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+sub run {
+  my ($self) = @_;
+
+  $self->drop_constraints(table => 'generic_translations');
+
+  $self->db_query(<<SQL);
+    ALTER TABLE generic_translations
+    ADD CONSTRAINT generic_translations_language_id_fkey
+      FOREIGN KEY (language_id)
+      REFERENCES language (id)
+      ON DELETE CASCADE
+SQL
+
+  $self->db_query(<<SQL);
+    DELETE FROM generic_translations
+    WHERE language_id NOT IN (
+      SELECT id
+      FROM language
+    )
+SQL
+
+  return 1;
+}
+
+1;
index c4a40a9..ff596de 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: delete_translations_on_delivery_term_delete
 -- @description: Übersetzungen löschen, wenn Lieferbedingung gelöscht wird
 -- @depends: delivery_terms
--- @encoding: utf-8
 
 CREATE OR REPLACE FUNCTION generic_translations_delete_on_delivery_terms_delete_trigger()
 RETURNS TRIGGER AS $$
index 9502452..8bab4e0 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: delete_translations_on_payment_term_delete
 -- @description: Übersetzungen löschen, wenn Lieferbedingung gelöscht wird
 -- @depends: payment_terms_translation2
--- @encoding: utf-8
 
 CREATE OR REPLACE FUNCTION generic_translations_delete_on_payment_terms_delete_trigger()
 RETURNS TRIGGER AS $$
index 5243ace..d3c9ced 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: delete_translations_on_tax_delete
 -- @description: Übersetzungen löschen, wenn Steuer gelöscht wird
 -- @depends: release_3_0_0
--- @encoding: utf-8
 
 CREATE OR REPLACE FUNCTION generic_translations_delete_on_tax_delete_trigger()
 RETURNS TRIGGER AS $$
diff --git a/sql/Pg-upgrade2/delete_warehouse_for_assembly.sql b/sql/Pg-upgrade2/delete_warehouse_for_assembly.sql
new file mode 100644 (file)
index 0000000..f0be3a9
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: delete_warehouse_for_assembly
+-- @description: Entfernen von: Konfigurations-Option für das Fertigen von Erzeugnissen aus dem Standardlager
+-- @depends: release_3_5_7
+ALTER TABLE defaults DROP column transfer_default_warehouse_for_assembly;
diff --git a/sql/Pg-upgrade2/delete_wrong_charts_for_taxkeys.pl b/sql/Pg-upgrade2/delete_wrong_charts_for_taxkeys.pl
new file mode 100644 (file)
index 0000000..159d4b0
--- /dev/null
@@ -0,0 +1,62 @@
+# @tag: delete_wrong_charts_for_taxkeys
+# @description: Uralte falsch angelegte Automatikkonten raus -> Chance auf tax.chart_id unique setzen
+# @depends: release_3_6_0
+# @ignore: 0
+package SL::DBUpgrade2::delete_wrong_charts_for_taxkeys;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+sub delete_chart_id_tax {
+  my $self = shift;
+
+  my $q_fetch = <<SQL;
+    SELECT chart_id
+    FROM tax where chart_id is not null
+    GROUP BY chart_id HAVING COUNT(*) > 1
+SQL
+
+  # skr03
+  my $q_update = <<SQL;
+    UPDATE tax
+    SET chart_id = NULL
+    WHERE chart_id = ?
+    AND rate = 0.16
+    AND (taxkey = 19 OR taxkey = 13)
+    AND EXISTS (SELECT * FROM defaults WHERE coa = 'Germany-DATEV-SKR03EU')
+SQL
+
+  my $h_fetch = $self->dbh->prepare($q_fetch);
+  $h_fetch->execute || $::form->dberror($q_fetch);
+
+  my $h_update_03 = $self->dbh->prepare($q_update);
+
+  while (my $entry = $h_fetch->fetchrow_hashref) {
+    $h_update_03->execute($entry->{chart_id}) || $::form->dberror($q_update);
+  }
+  # might be unique now
+  $h_fetch->execute || $::form->dberror($q_fetch);
+
+  if (!$h_fetch->fetchrow_hashref) {
+    my $q_unique = <<SQL;
+      alter table tax
+      ADD CONSTRAINT chart_id_unique_tax UNIQUE (chart_id)
+SQL
+    my $q_unique_p = $self->dbh->prepare($q_unique);
+    $q_unique_p->execute || $::form->dberror($q_unique_p);
+  }
+}
+
+sub run {
+  my ($self) = @_;
+
+  return 1 unless $self->check_coa('Germany-DATEV-SKR03EU');
+
+  $self->delete_chart_id_tax;
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/delete_wrong_charts_for_taxkeys_04.pl b/sql/Pg-upgrade2/delete_wrong_charts_for_taxkeys_04.pl
new file mode 100644 (file)
index 0000000..95e408f
--- /dev/null
@@ -0,0 +1,63 @@
+# @tag: delete_wrong_charts_for_taxkeys_04
+# @description: SKR04: Uralte falsch angelegte Automatikkonten raus -> Chance auf tax.chart_id unique setzen
+# @depends: release_3_6_0
+# @ignore: 0
+package SL::DBUpgrade2::delete_wrong_charts_for_taxkeys_04;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+sub delete_chart_id_tax {
+  my $self = shift;
+
+  my $q_fetch = <<SQL;
+    SELECT chart_id
+    FROM tax where chart_id is not null
+    GROUP BY chart_id HAVING COUNT(*) > 1
+SQL
+
+  # SKR04
+  my $q_update_04 = <<SQL;
+    UPDATE tax
+    SET chart_id = NULL
+    WHERE chart_id = ?
+    AND rate = 0.16
+    AND (taxkey = 3 OR taxkey = 9)
+    AND EXISTS (SELECT * FROM defaults WHERE coa = 'Germany-DATEV-SKR04EU')
+SQL
+
+
+  my $h_fetch = $self->dbh->prepare($q_fetch);
+  $h_fetch->execute || $::form->dberror($q_fetch);
+
+  my $h_update_04 = $self->dbh->prepare($q_update_04);
+
+  while (my $entry = $h_fetch->fetchrow_hashref) {
+    $h_update_04->execute($entry->{chart_id}) || $::form->dberror($q_update_04);
+  }
+  # might be unique now
+  $h_fetch->execute || $::form->dberror($q_fetch);
+
+  if (!$h_fetch->fetchrow_hashref) {
+    my $q_unique = <<SQL;
+      alter table tax
+      ADD CONSTRAINT chart_id_unique_tax UNIQUE (chart_id)
+SQL
+    my $q_unique_p = $self->dbh->prepare($q_unique);
+    $q_unique_p->execute || $::form->dberror($q_unique_p);
+  }
+}
+
+sub run {
+  my ($self) = @_;
+
+  return 1 unless $self->check_coa('Germany-DATEV-SKR04EU');
+
+  $self->delete_chart_id_tax;
+
+  return 1;
+}
+
+1;
index ba68b2f..83a47e1 100644 (file)
@@ -75,7 +75,7 @@ CREATE TABLE delivery_order_items (
   FOREIGN KEY (parts_id)          REFERENCES parts (id),
   FOREIGN KEY (project_id)        REFERENCES project (id),
   FOREIGN KEY (price_factor_id)   REFERENCES price_factors (id)
-) WITH OIDS;
+);
 
 CREATE TRIGGER mtime_delivery_order_items_id BEFORE UPDATE ON delivery_order_items
     FOR EACH ROW EXECUTE PROCEDURE set_mtime();
index fa7400c..9f9e42d 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: delivery_terms
 -- @description: Neue Tabelle und Spalten für Lieferbedingungen
 -- @depends: release_3_0_0
--- @encoding: utf-8
 
 CREATE TABLE delivery_terms (
        id                        integer        NOT NULL DEFAULT nextval('id'),
diff --git a/sql/Pg-upgrade2/deliveryorder_transnumbers.sql b/sql/Pg-upgrade2/deliveryorder_transnumbers.sql
new file mode 100644 (file)
index 0000000..3e03150
--- /dev/null
@@ -0,0 +1,11 @@
+-- @tag: deliveryorder_transnumbers
+-- @description: Nummernkreise für neue lieferscheintypen
+-- @depends: release_3_5_8
+
+ALTER TABLE defaults ADD COLUMN sudonumber TEXT;
+ALTER TABLE defaults ADD COLUMN rdonumber TEXT;
+
+UPDATE defaults SET
+  sudonumber = '0',
+  rdonumber = '0';
+
diff --git a/sql/Pg-upgrade2/deliveryorder_type.sql b/sql/Pg-upgrade2/deliveryorder_type.sql
new file mode 100644 (file)
index 0000000..8e4a0f4
--- /dev/null
@@ -0,0 +1,12 @@
+-- @tag: deliveryorder_type
+-- @description: Persistente Typen in Lieferscheinen
+-- @depends: release_3_5_8
+
+ALTER TABLE delivery_orders ADD COLUMN order_type TEXT;
+
+UPDATE delivery_orders SET order_type = 'sales_delivery_order' WHERE customer_id IS NOT NULL;
+UPDATE delivery_orders SET order_type = 'purchase_delivery_order' WHERE vendor_id IS NOT NULL;
+
+ALTER TABLE delivery_orders ALTER COLUMN order_type SET NOT NULL;
+
+
diff --git a/sql/Pg-upgrade2/displayable_name_prefs_defaults.sql b/sql/Pg-upgrade2/displayable_name_prefs_defaults.sql
new file mode 100644 (file)
index 0000000..542b36b
--- /dev/null
@@ -0,0 +1,13 @@
+-- @tag: displayable_name_prefs_defaults
+-- @description: Setzen der Default-Einstellungen für einstellbare Picker-Anzeigen
+-- @depends: user_preferences
+
+INSERT INTO user_preferences (login, namespace, version, key, value)
+  SELECT '#default#','DisplayableName','0.00000','SL::DB::Customer','<%customernumber%> <%name%>'
+    WHERE NOT EXISTS (SELECT id FROM user_preferences WHERE login LIKE '#default#' AND namespace LIKE 'DisplayableName' AND version = 0.00000 AND key LIKE 'SL::DB::Customer');
+INSERT INTO user_preferences (login, namespace, version, key, value)
+  SELECT '#default#','DisplayableName','0.00000','SL::DB::Vendor','<%vendornumber%> <%name%>'
+    WHERE NOT EXISTS (SELECT id FROM user_preferences WHERE login LIKE '#default#' AND namespace LIKE 'DisplayableName' AND version = 0.00000 AND key LIKE 'SL::DB::Vendor');
+INSERT INTO user_preferences (login, namespace, version, key, value)
+  SELECT '#default#','DisplayableName','0.00000','SL::DB::Part','<%partnumber%> <%description%>'
+    WHERE NOT EXISTS (SELECT id FROM user_preferences WHERE login LIKE '#default#' AND namespace LIKE 'DisplayableName' AND version = 0.00000 AND key LIKE 'SL::DB::Part');
index 53c2ec7..de4f27e 100644 (file)
@@ -1,6 +1,5 @@
 -- @tag: drop_gifi_2
 -- @description: Spalte gifi_accno vollständig entfernen
 -- @depends: release_3_0_0 drop_gifi
--- @encoding: utf-8
 
 ALTER TABLE "vendor" DROP COLUMN "gifi_accno";
diff --git a/sql/Pg-upgrade2/drop_payment_terms_ranking.sql b/sql/Pg-upgrade2/drop_payment_terms_ranking.sql
new file mode 100644 (file)
index 0000000..736c939
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: drop_payment_terms_ranking
+-- @description: Entfernt Spalte ranking in payment_terms
+-- @depends: release_3_5_3
+
+ALTER TABLE payment_terms DROP COLUMN ranking;
diff --git a/sql/Pg-upgrade2/drop_shipped_qty_config.sql b/sql/Pg-upgrade2/drop_shipped_qty_config.sql
new file mode 100644 (file)
index 0000000..a71a029
--- /dev/null
@@ -0,0 +1,8 @@
+-- @tag: drop_shipped_qty_config
+-- @description: Verwaiste Optionen löschen
+-- @depends: release_3_5_7
+
+ALTER TABLE defaults DROP COLUMN shipped_qty_fill_up;
+ALTER TABLE defaults DROP COLUMN shipped_qty_item_identity_fields;
+
+
diff --git a/sql/Pg-upgrade2/dunning_config_print_original_invoice.sql b/sql/Pg-upgrade2/dunning_config_print_original_invoice.sql
new file mode 100644 (file)
index 0000000..2031b7c
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: dunning_config_print_original_invoice
+-- @description: Optional die Originalrechnung bei Zahlungserinnerungen ausdrucken
+-- @depends: release_3_5_5
+ALTER TABLE dunning_config ADD COLUMN print_original_invoice boolean;
+
diff --git a/sql/Pg-upgrade2/dunning_foreign_key_for_trans_id.sql b/sql/Pg-upgrade2/dunning_foreign_key_for_trans_id.sql
new file mode 100644 (file)
index 0000000..37fb3e7
--- /dev/null
@@ -0,0 +1,15 @@
+-- @tag: dunning_foreign_key_for_trans_id
+-- @description: Mahnungstabelle: Fremdschlüssel für Verknüpfung zur Rechnungstabelle
+-- @depends: release_3_5_3
+DELETE FROM dunning
+WHERE NOT EXISTS (
+  SELECT ar.id
+  FROM ar
+  WHERE ar.id = dunning.trans_id
+  LIMIT 1
+);
+
+ALTER TABLE dunning
+ADD CONSTRAINT dunning_trans_id_fkey
+FOREIGN KEY (trans_id) REFERENCES ar (id)
+ON DELETE CASCADE;
diff --git a/sql/Pg-upgrade2/dunning_original_invoice_printed.sql b/sql/Pg-upgrade2/dunning_original_invoice_printed.sql
new file mode 100644 (file)
index 0000000..bd140e6
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: dunning_original_invoice_printed
+-- @description: In der Tabelle dunning merken, ob beim Mahnlauf die originale Rechnung gedruckt wurde
+-- @depends: release_3_5_6_1
+
+ALTER TABLE dunning ADD COLUMN original_invoice_printed BOOLEAN DEFAULT false;
diff --git a/sql/Pg-upgrade2/email_journal_attachments_add_fileid.sql b/sql/Pg-upgrade2/email_journal_attachments_add_fileid.sql
new file mode 100644 (file)
index 0000000..9465df5
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: email_journal_attachments_add_fileid
+-- @description: attachments mit file_id
+-- @depends: email_journal filemanagement_feature files
+ALTER TABLE email_journal_attachments ADD COLUMN file_id integer default 0 NOT NULL;
diff --git a/sql/Pg-upgrade2/emmvee_background_jobs_2.pl b/sql/Pg-upgrade2/emmvee_background_jobs_2.pl
deleted file mode 100644 (file)
index a7b9e6d..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-# @tag: emmvee_background_jobs_2
-# @description: Hintergrundjobs einrichten
-# @depends: emmvee_background_jobs
-package SL::DBUpgrade2::emmvee_background_jobs_2;
-
-use strict;
-use utf8;
-
-use parent qw(SL::DBUpgrade2::Base);
-
-use SL::BackgroundJob::CleanBackgroundJobHistory;
-
-sub run {
-  SL::BackgroundJob::CleanBackgroundJobHistory->create_job;
-  return 1;
-}
-
-1;
diff --git a/sql/Pg-upgrade2/emmvee_background_jobs_2.sql b/sql/Pg-upgrade2/emmvee_background_jobs_2.sql
new file mode 100644 (file)
index 0000000..a0d6178
--- /dev/null
@@ -0,0 +1,12 @@
+-- @tag: emmvee_background_jobs_2
+-- @description: Hintergrundjobs einrichten
+-- @depends: emmvee_background_jobs
+INSERT INTO background_jobs (type, package_name, active, cron_spec, next_run_at)
+VALUES ('interval', 'CleanBackgroundJobHistory', true, '0 3 * * *',
+  CAST(current_date AS timestamp) + CAST(
+    (CASE
+     WHEN extract('hour' FROM current_timestamp) < 3 THEN '3 hours'
+     ELSE                                                 '1 day 3 hours'
+     END) AS interval
+  )
+);
index a87cf05..98585f7 100644 (file)
@@ -2,7 +2,6 @@
 -- @description: Obsolete Felder in employee entfernt und Datenfelder zum Speichern für die Historie der Mitarbeiter (nach Löschen eines Benutzer) hinzugefügt. Aktuell alle Felder die der Benutzer unter persönliche Einstellungen ändern kann
 -- @depends: release_3_0_0
 -- @ignore: 0
--- @charset: utf-8
 ALTER TABLE employee DROP COLUMN addr1;
 ALTER TABLE employee DROP COLUMN addr2;
 ALTER TABLE employee DROP COLUMN addr3;
index 326122e..8468e69 100644 (file)
@@ -60,35 +60,34 @@ sub filter_parts {
 
   if ( $::form->{filter_partnumber} ) {
     $where .= ' AND partnumber ILIKE ?';
-    push(@values, $::form->like( $::form->{filter_partnumber} ));
+    push(@values, like( $::form->{filter_partnumber} ));
   }
 
   if ($::form->{filter_description}) {
     $where .= ' AND description ILIKE ?';
-    push(@values, $::form->like($::form->{filter_description}));
+    push(@values, like($::form->{filter_description}));
   }
 
   if ($::form->{filter_notes}) {
     $where .= ' AND notes ILIKE ?';
-    push(@values, $::form->like($::form->{filter_notes}));
+    push(@values, like($::form->{filter_notes}));
   }
 
   if ($::form->{filter_ean}) {
     $where .= ' AND ean ILIKE ?';
-    push(@values, $::form->like($::form->{filter_ean}));
+    push(@values, like($::form->{filter_ean}));
   }
 
   if ($::form->{filter_type} eq 'assembly') {
-    $where .= ' AND assembly';
+    $where .= " AND part_type = 'assembly'";
   }
 
   if ($::form->{filter_type} eq 'service') {
-    $where .= ' AND inventory_accno_id IS NULL AND NOT assembly';
+    $where .= " AND part_type = 'service'";
   }
 
   if ($::form->{filter_type} eq 'part') {
-    $where .= ' AND inventory_accno_id IS NOT NULL';
-    $where .= ' AND NOT assembly';
+    $where .= " AND part_type = 'part'";
   }
 
   if ($::form->{filter_obsolete} eq 'obsolete') {
diff --git a/sql/Pg-upgrade2/eur_bwa_category_views.sql b/sql/Pg-upgrade2/eur_bwa_category_views.sql
new file mode 100644 (file)
index 0000000..70f729a
--- /dev/null
@@ -0,0 +1,61 @@
+-- @tag: eur_bwa_category_views
+-- @description: Kategorien für EÜR/GuV und BWA in Datenbank speichern (als View, nicht als Tabelle)
+-- @depends: release_3_4_1
+
+
+CREATE OR REPLACE VIEW "eur_categories" (id, description) AS VALUES
+( 1, 'Umsatzerlöse'),
+( 2, 'sonstige Erlöse'),
+( 3, 'Privatanteile'),
+( 4, 'Zinserträge'),
+( 5, 'Ausserordentliche Erträge'),
+( 6, 'Vereinnahmte Umsatzst.'),
+( 7, 'Umsatzsteuererstattungen'),
+( 8, 'Wareneingänge'),
+( 9, 'Löhne und Gehälter'),
+(10, 'Gesetzl. sozialer Aufw.'),
+(11, 'Mieten'),
+(12, 'Gas, Strom, Wasser'),
+(13, 'Instandhaltung'),
+(14, 'Steuern, Versich., Beiträge'),
+(15, 'Kfz-Steuern'),
+(16, 'Kfz-Versicherungen'),
+(17, 'Sonst. Fahrzeugkosten'),
+(18, 'Werbe- und Reisekosten'),
+(19, 'Instandhaltung u. Werkzeuge'),
+(20, 'Fachzeitschriften, Bücher'),
+(21, 'Miete für Einrichtungen'),
+(22, 'Rechts- und Beratungskosten'),
+(23, 'Bürobedarf, Porto, Telefon'),
+(24, 'Sonstige Aufwendungen'),
+(25, 'Abschreibungen auf Anlagever.'),
+(26, 'Abschreibungen auf GWG'),
+(27, 'Vorsteuer'),
+(28, 'Umsatzsteuerzahlungen'),
+(29, 'Zinsaufwand'),
+(30, 'Ausserordentlicher Aufwand'),
+(31, 'Betriebliche Steuern');
+
+CREATE OR REPLACE VIEW "bwa_categories" (id, description) AS VALUES
+(  1, 'Umsatzerlöse'),
+(  2, 'Best.Verdg.FE/UE'),
+(  3, 'Aktiv.Eigenleistung'),
+(  4, 'Mat./Wareneinkauf'),
+(  5, 'So.betr.Erlöse'),
+( 10, 'Personalkosten'),
+( 11, 'Raumkosten'),
+( 12, 'Betriebl.Steuern'),
+( 13, 'Vers./Beiträge'),
+( 14, 'Kfz.Kosten o.St.'),
+( 15, 'Werbe-Reisek.'),
+( 16, 'Kosten Warenabgabe'),
+( 17, 'Abschreibungen'),
+( 18, 'Rep./instandhlt.'),
+( 19, 'Übrige Steuern'),
+( 20, 'Sonst.Kosten'),
+( 30, 'Zinsaufwand'),
+( 31, 'Sonst.neutr.Aufw.'),
+( 32, 'Zinserträge'),
+( 33, 'Sonst.neutr.Ertrag'),
+( 34, 'Verr.kalk.Kosten'),
+( 35, 'Steuern Eink.u.Ertr.');
diff --git a/sql/Pg-upgrade2/exchangerate_in_oe.sql b/sql/Pg-upgrade2/exchangerate_in_oe.sql
new file mode 100644 (file)
index 0000000..ca3defd
--- /dev/null
@@ -0,0 +1,12 @@
+-- @tag: exchangerate_in_oe
+-- @description: Wechselkurs pro Angebot/Auftrag in Belegtabelle speichern
+-- @depends: release_3_5_5
+
+ALTER TABLE oe ADD COLUMN exchangerate NUMERIC(15,5);
+
+WITH table_ex AS
+  (SELECT oe.id, COALESCE(CASE WHEN customer_id IS NOT NULL THEN buy ELSE sell END, 1.0) AS exchangerate FROM oe
+    LEFT JOIN exchangerate ON (oe.transdate = exchangerate.transdate AND oe.currency_id = exchangerate.currency_id)
+    WHERE oe.currency_id != (SELECT currency_id FROM defaults))
+  UPDATE oe SET exchangerate = (SELECT exchangerate FROM table_ex WHERE table_ex.id = oe.id)
+    WHERE EXISTS (SELECT table_ex.exchangerate FROM table_ex WHERE table_ex.id = oe.id);
diff --git a/sql/Pg-upgrade2/file_full_texts.sql b/sql/Pg-upgrade2/file_full_texts.sql
new file mode 100644 (file)
index 0000000..f479da1
--- /dev/null
@@ -0,0 +1,15 @@
+-- @tag: file_full_texts
+-- @description: Tabelle f. Volltext-Suche anlegen
+-- @depends: release_3_6_0
+
+CREATE TABLE IF NOT EXISTS file_full_texts (
+   id           SERIAL,
+   file_id      INTEGER            NOT NULL REFERENCES files(id) ON DELETE CASCADE,
+   full_text    TEXT               NOT NULL,
+   itime        TIMESTAMP          NOT NULL DEFAULT now(),
+   mtime        TIMESTAMP,
+
+   PRIMARY KEY (id)
+);
+
+CREATE TRIGGER mtime_file_full_texts BEFORE UPDATE ON file_full_texts FOR EACH ROW EXECUTE PROCEDURE set_mtime();
diff --git a/sql/Pg-upgrade2/file_storage_dunning_documents.sql b/sql/Pg-upgrade2/file_storage_dunning_documents.sql
new file mode 100644 (file)
index 0000000..865ef05
--- /dev/null
@@ -0,0 +1,17 @@
+-- @tag: file_storage_dunning_documents
+-- @description: Dateien f. Mahnungen von gemahnter Rechnung zum Mahnlauf verschieben
+-- @depends: file_storage_dunning_invoice
+
+-- for the original invoice, assume that the dunning_id is the one from a dunning row where the trans_id is
+-- the old files object_id (the orig. invoice) and the itime of both tables are (almost) equal
+WITH table_files AS
+  (SELECT dunning.dunning_id, files.id FROM files LEFT JOIN dunning ON (dunning.trans_id = files.object_id)
+     WHERE object_type ILIKE 'dunning_orig_invoice' AND file_type LIKE 'document' AND source LIKE 'created'
+       AND ABS(EXTRACT(EPOCH FROM (dunning.itime - files.itime))) < 0.1)
+  UPDATE files SET object_type = 'dunning', object_id = (SELECT dunning_id FROM table_files WHERE table_files.id = files.id)
+     WHERE EXISTS (SELECT id FROM table_files WHERE table_files.id = files.id);
+
+-- the dunning_id for the following types can be found in the filename
+UPDATE files SET object_type = 'dunning', object_id = substring(file_name FROM '(\d+).pdf')::INT
+  WHERE (object_type LIKE 'dunning1' OR object_type LIKE 'dunning2' OR object_type LIKE 'dunning3' OR object_type LIKE 'dunning_invoice')
+    AND file_type LIKE 'document' AND source LIKE 'created';
diff --git a/sql/Pg-upgrade2/file_storage_dunning_invoice.sql b/sql/Pg-upgrade2/file_storage_dunning_invoice.sql
new file mode 100644 (file)
index 0000000..583f194
--- /dev/null
@@ -0,0 +1,20 @@
+-- @tag: file_storage_dunning_invoice
+-- @description: Datei pro Mahnlauf als dunning. Altes dunning (Mahnrechnung) wird dunning_invoice
+-- @depends: file_storage_type_dunning_orig_invoice
+
+ALTER TABLE files
+  DROP CONSTRAINT valid_type;
+ALTER TABLE files
+  ADD  CONSTRAINT valid_type CHECK (
+             (object_type = 'credit_note'     ) OR (object_type = 'invoice'                 ) OR (object_type = 'sales_order'          )
+          OR (object_type = 'sales_quotation' ) OR (object_type = 'sales_delivery_order'    ) OR (object_type = 'request_quotation'    )
+          OR (object_type = 'purchase_order'  ) OR (object_type = 'purchase_delivery_order' ) OR (object_type = 'purchase_invoice'     )
+          OR (object_type = 'vendor'          ) OR (object_type = 'customer'                ) OR (object_type = 'part'                 )
+          OR (object_type = 'gl_transaction'  ) OR (object_type = 'dunning'                 ) OR (object_type = 'dunning1'             )
+          OR (object_type = 'dunning2'        ) OR (object_type = 'dunning3'                ) OR (object_type = 'dunning_orig_invoice' )
+          OR (object_type = 'dunning_invoice' ) OR (object_type = 'draft'                   ) OR (object_type = 'statement'            )
+          OR (object_type = 'shop_image'      )
+          OR (object_type = 'letter'          )
+  );
+
+UPDATE files SET object_type = 'dunning_invoice' WHERE object_type LIKE 'dunning';
diff --git a/sql/Pg-upgrade2/file_storage_partial_invoices.sql b/sql/Pg-upgrade2/file_storage_partial_invoices.sql
new file mode 100644 (file)
index 0000000..7fed883
--- /dev/null
@@ -0,0 +1,18 @@
+-- @tag: file_storage_partial_invoices
+-- @description: Dateispeicher auch für Anzahlungs- und Schlussrechnung
+-- @depends: file_storage_project
+
+ALTER TABLE files
+  DROP CONSTRAINT valid_type;
+ALTER TABLE files
+  ADD  CONSTRAINT valid_type CHECK (
+             (object_type = 'credit_note'     ) OR (object_type = 'invoice'                 ) OR (object_type = 'sales_order'          )
+          OR (object_type = 'sales_quotation' ) OR (object_type = 'sales_delivery_order'    ) OR (object_type = 'request_quotation'    )
+          OR (object_type = 'purchase_order'  ) OR (object_type = 'purchase_delivery_order' ) OR (object_type = 'purchase_invoice'     )
+          OR (object_type = 'vendor'          ) OR (object_type = 'customer'                ) OR (object_type = 'part'                 )
+          OR (object_type = 'gl_transaction'  ) OR (object_type = 'dunning'                 ) OR (object_type = 'dunning1'             )
+          OR (object_type = 'dunning2'        ) OR (object_type = 'dunning3'                ) OR (object_type = 'dunning_orig_invoice' )
+          OR (object_type = 'dunning_invoice' ) OR (object_type = 'draft'                   ) OR (object_type = 'statement'            )
+          OR (object_type = 'shop_image'      ) OR (object_type = 'letter'                  ) OR (object_type = 'project'              )
+          OR (object_type = 'invoice_for_advance_payment') OR (object_type = 'final_invoice')
+  );
diff --git a/sql/Pg-upgrade2/file_storage_project.sql b/sql/Pg-upgrade2/file_storage_project.sql
new file mode 100644 (file)
index 0000000..4ad74e4
--- /dev/null
@@ -0,0 +1,19 @@
+-- @tag: file_storage_project
+-- @description: Dateispeicher auch für Projekte anbieten
+-- @depends: file_storage_dunning_invoice
+
+ALTER TABLE files
+  DROP CONSTRAINT valid_type;
+ALTER TABLE files
+  ADD  CONSTRAINT valid_type CHECK (
+             (object_type = 'credit_note'     ) OR (object_type = 'invoice'                 ) OR (object_type = 'sales_order'          )
+          OR (object_type = 'sales_quotation' ) OR (object_type = 'sales_delivery_order'    ) OR (object_type = 'request_quotation'    )
+          OR (object_type = 'purchase_order'  ) OR (object_type = 'purchase_delivery_order' ) OR (object_type = 'purchase_invoice'     )
+          OR (object_type = 'vendor'          ) OR (object_type = 'customer'                ) OR (object_type = 'part'                 )
+          OR (object_type = 'gl_transaction'  ) OR (object_type = 'dunning'                 ) OR (object_type = 'dunning1'             )
+          OR (object_type = 'dunning2'        ) OR (object_type = 'dunning3'                ) OR (object_type = 'dunning_orig_invoice' )
+          OR (object_type = 'dunning_invoice' ) OR (object_type = 'draft'                   ) OR (object_type = 'statement'            )
+          OR (object_type = 'shop_image'      )
+          OR (object_type = 'letter'          )
+          OR (object_type = 'project'         )
+  );
diff --git a/sql/Pg-upgrade2/file_storage_type_dunning_orig_invoice.sql b/sql/Pg-upgrade2/file_storage_type_dunning_orig_invoice.sql
new file mode 100644 (file)
index 0000000..92ee580
--- /dev/null
@@ -0,0 +1,17 @@
+-- @tag: file_storage_type_dunning_orig_invoice
+-- @description: original gemahnte Rechnung als valid_type für Filemanagement
+-- @depends: file_storage_type_letter
+
+ALTER TABLE files
+  DROP CONSTRAINT valid_type;
+ALTER TABLE files
+  ADD  CONSTRAINT valid_type CHECK (
+             (object_type = 'credit_note'     ) OR (object_type = 'invoice'                 ) OR (object_type = 'sales_order'          )
+          OR (object_type = 'sales_quotation' ) OR (object_type = 'sales_delivery_order'    ) OR (object_type = 'request_quotation'    )
+          OR (object_type = 'purchase_order'  ) OR (object_type = 'purchase_delivery_order' ) OR (object_type = 'purchase_invoice'     )
+          OR (object_type = 'vendor'          ) OR (object_type = 'customer'                ) OR (object_type = 'part'                 )
+          OR (object_type = 'gl_transaction'  ) OR (object_type = 'dunning'                 ) OR (object_type = 'dunning1'             )
+          OR (object_type = 'dunning2'        ) OR (object_type = 'dunning3'                ) OR (object_type = 'dunning_orig_invoice' )
+          OR (object_type = 'draft'           ) OR (object_type = 'statement'               ) OR (object_type = 'shop_image'           )
+          OR (object_type = 'letter'          )
+  );
diff --git a/sql/Pg-upgrade2/file_storage_type_letter.sql b/sql/Pg-upgrade2/file_storage_type_letter.sql
new file mode 100644 (file)
index 0000000..cf4c832
--- /dev/null
@@ -0,0 +1,16 @@
+-- @tag: file_storage_type_letter
+-- @description: Letter als valid_type für Filemanagement
+-- @depends: shopimages
+
+ALTER TABLE files
+  DROP CONSTRAINT valid_type;
+ALTER TABLE files
+  ADD  CONSTRAINT valid_type CHECK (
+             (object_type = 'credit_note'     ) OR (object_type = 'invoice'                 ) OR (object_type = 'sales_order'       )
+          OR (object_type = 'sales_quotation' ) OR (object_type = 'sales_delivery_order'    ) OR (object_type = 'request_quotation' )
+          OR (object_type = 'purchase_order'  ) OR (object_type = 'purchase_delivery_order' ) OR (object_type = 'purchase_invoice'  )
+          OR (object_type = 'vendor'          ) OR (object_type = 'customer'                ) OR (object_type = 'part'              )
+          OR (object_type = 'gl_transaction'  ) OR (object_type = 'dunning'                 ) OR (object_type = 'dunning1'          )
+          OR (object_type = 'dunning2'        ) OR (object_type = 'dunning3'                ) OR (object_type = 'draft'             )
+          OR (object_type = 'statement'       ) OR (object_type = 'shop_image'              ) OR (object_type = 'letter'            )
+  );
diff --git a/sql/Pg-upgrade2/filemanagement_feature.sql b/sql/Pg-upgrade2/filemanagement_feature.sql
new file mode 100644 (file)
index 0000000..04d3082
--- /dev/null
@@ -0,0 +1,13 @@
+-- @tag: filemanagement_feature
+-- @description: "Zusätzliche Config flags für Filemanagement"
+-- @depends: release_3_4_1
+ALTER TABLE defaults ADD COLUMN doc_delete_printfiles       boolean DEFAULT false;
+ALTER TABLE defaults ADD COLUMN doc_max_filesize            integer DEFAULT 10000000;
+ALTER TABLE defaults ADD COLUMN doc_storage                 boolean DEFAULT false;
+ALTER TABLE defaults ADD COLUMN doc_storage_for_documents   text default 'Filesystem';
+ALTER TABLE defaults ADD COLUMN doc_storage_for_attachments text default 'Filesystem';
+ALTER TABLE defaults ADD COLUMN doc_storage_for_images      text default 'Filesystem';
+ALTER TABLE defaults ADD COLUMN doc_files                   boolean DEFAULT false;
+ALTER TABLE defaults ADD COLUMN doc_files_rootpath          text default './documents';
+ALTER TABLE defaults ADD COLUMN doc_webdav                  boolean DEFAULT false;
+ALTER TABLE defaults ADD COLUMN doc_database                boolean DEFAULT false;
diff --git a/sql/Pg-upgrade2/files.sql b/sql/Pg-upgrade2/files.sql
new file mode 100644 (file)
index 0000000..c5a9cf0
--- /dev/null
@@ -0,0 +1,25 @@
+-- @tag: files
+-- @description: Tabelle für Files
+-- @depends: release_3_4_1
+CREATE TABLE files(
+  id                          SERIAL PRIMARY KEY,
+  object_type                 TEXT NOT NULL,    -- Tabellenname des Moduls z.B. customer, parts ... Fremdschlüssel Zusammen mit object_id
+  object_id                   INTEGER NOT NULL, -- Fremdschlüssel auf die id der Tabelle aus Spalte object_type
+  file_name                   TEXT NOT NULL,
+  file_type                   TEXT NOT NULL,
+  mime_type                   TEXT NOT NULL,
+  source                      TEXT NOT NULL,
+  backend                     TEXT,
+  backend_data                TEXT,
+  title                       varchar(45),
+  description                 TEXT,
+  itime                       TIMESTAMP DEFAULT now(),
+  mtime                       TIMESTAMP,
+  CONSTRAINT valid_type CHECK (
+             (object_type = 'credit_note') OR (object_type = 'invoice') OR (object_type = 'sales_order') OR (object_type = 'sales_quotation')
+          OR (object_type = 'sales_delivery_order') OR (object_type = 'request_quotation') OR (object_type = 'purchase_order')
+          OR (object_type = 'purchase_delivery_order') OR (object_type = 'purchase_invoice')
+          OR (object_type = 'vendor') OR (object_type = 'customer') OR (object_type = 'part') OR (object_type = 'gl_transaction')
+          OR (object_type = 'dunning') OR (object_type = 'dunning1') OR (object_type = 'dunning2') OR (object_type = 'dunning3')
+          OR (object_type = 'draft') OR (object_type = 'statement'))
+);
diff --git a/sql/Pg-upgrade2/files_add_variant.sql b/sql/Pg-upgrade2/files_add_variant.sql
new file mode 100644 (file)
index 0000000..a697e8a
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: files_add_variant
+-- @description: Varianten für DMS System
+-- @depends: release_3_5_8
+
+ALTER TABLE files ADD COLUMN print_variant text;
index 9803988..a0465b5 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: first_aggregator
 -- @description: SQL Aggregat Funktion FIRST
 -- @depends: release_3_0_0
--- @encoding: utf-8
 
 CREATE OR REPLACE FUNCTION public.first_agg ( anyelement, anyelement )
 RETURNS anyelement LANGUAGE SQL IMMUTABLE STRICT AS $$
diff --git a/sql/Pg-upgrade2/full_texts_background_job.sql b/sql/Pg-upgrade2/full_texts_background_job.sql
new file mode 100644 (file)
index 0000000..15a1ec9
--- /dev/null
@@ -0,0 +1,13 @@
+-- @tag: full_texts_background_job
+-- @description: Hintergrundjob für tägliche Extraktion von Texten aus Dokumenten
+-- @depends: release_3_6_0
+
+INSERT INTO background_jobs (type, package_name, active, cron_spec, next_run_at)
+VALUES ('interval', 'CreateOrUpdateFileFullTexts', true, '20 3 * * *',
+  CAST(current_date AS timestamp) + CAST(
+    (CASE
+     WHEN extract('hour' FROM current_timestamp) < 2 THEN '3 hours 20 minutes'
+     ELSE                                                 '1 day 3 hours 20 minutes'
+     END) AS interval
+  )
+);
diff --git a/sql/Pg-upgrade2/get_shipped_qty_config.sql b/sql/Pg-upgrade2/get_shipped_qty_config.sql
new file mode 100644 (file)
index 0000000..fb2494d
--- /dev/null
@@ -0,0 +1,9 @@
+-- @tag: get_shipped_qty_config
+-- @description: Mandantenweite Konfiguration für das Verhalten von Liefermengenabgleich
+-- @depends: release_3_4_1
+
+ALTER TABLE defaults ADD COLUMN shipped_qty_require_stock_out    BOOLEAN NOT NULL DEFAULT FALSE;
+ALTER TABLE defaults ADD COLUMN shipped_qty_fill_up              BOOLEAN NOT NULL DEFAULT TRUE;
+ALTER TABLE defaults ADD COLUMN shipped_qty_item_identity_fields TEXT[] NOT NULL DEFAULT '{parts_id}';
+
+
diff --git a/sql/Pg-upgrade2/gl_add_deliverydate.sql b/sql/Pg-upgrade2/gl_add_deliverydate.sql
new file mode 100644 (file)
index 0000000..2e5500e
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: gl_add_deliverydate
+-- @description: Liefer-/Leistungsdatum in Dialogbuchungen
+-- @depends: release_3_5_5
+
+ALTER TABLE gl ADD COLUMN deliverydate DATE;
diff --git a/sql/Pg-upgrade2/greetings_own_table.sql b/sql/Pg-upgrade2/greetings_own_table.sql
new file mode 100644 (file)
index 0000000..3921d57
--- /dev/null
@@ -0,0 +1,16 @@
+-- @tag: greetings_own_table
+-- @description: Eigene Tabelle für Anreden
+-- @depends: release_3_5_5
+
+CREATE TABLE greetings (
+  id          SERIAL,
+  description TEXT      NOT NULL,
+  PRIMARY KEY (id),
+  UNIQUE (description)
+);
+
+UPDATE customer SET greeting = trim(greeting) WHERE greeting NOT LIKE trim(greeting);
+UPDATE vendor   SET greeting = trim(greeting) WHERE greeting NOT LIKE trim(greeting);
+
+INSERT INTO greetings (description)
+  SELECT DISTINCT greeting FROM (SELECT greeting FROM customer UNION SELECT greeting FROM vendor) AS gr WHERE greeting IS NOT NULL AND greeting NOT LIKE '' ORDER BY greeting;
diff --git a/sql/Pg-upgrade2/inventory_fix_shippingdate_assemblies.sql b/sql/Pg-upgrade2/inventory_fix_shippingdate_assemblies.sql
new file mode 100644 (file)
index 0000000..246292a
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: inventory_fix_shippingdate_assemblies
+-- @description: Shippingdate für assemblies und assembly_items nachträglich wie itime setzen.
+-- @depends: release_3_4_0 warehouse transfer_type_assembled
+update inventory set shippingdate = itime where comment ilike 'Verbraucht %' and shippingdate is null;
+update inventory set shippingdate = itime where shippingdate is null and parts_id in (select id from parts where assembly);
diff --git a/sql/Pg-upgrade2/inventory_itime_parts_id_index.sql b/sql/Pg-upgrade2/inventory_itime_parts_id_index.sql
new file mode 100644 (file)
index 0000000..36cd99c
--- /dev/null
@@ -0,0 +1,13 @@
+-- @tag: inventory_itime_parts_id_index
+-- @description: Index auf inventory itime und parts_id, um schnell die letzten Transaktion raussuchen zu können
+-- @depends: release_3_5_4
+
+-- increase speed of queries such as
+
+-- last 10 entries in inventory:
+-- SELECT * FROM inventory ORDER BY itime desc LIMIT 10
+
+-- last 10 inventory entries for a certain part:
+-- SELECT * FROM inventory WHERE parts_id = 1234 ORDER BY itime desc LIMIT 10
+
+CREATE INDEX inventory_itime_parts_id_idx ON inventory (itime, parts_id);
diff --git a/sql/Pg-upgrade2/inventory_parts_id_index.sql b/sql/Pg-upgrade2/inventory_parts_id_index.sql
new file mode 100644 (file)
index 0000000..84ffec9
--- /dev/null
@@ -0,0 +1,12 @@
+-- @tag: inventory_parts_id_index
+-- @description: Index auf inventory parts_id, um schneller die Bestände eines Artikels in diversen Lagern zu berechnen
+-- @depends: release_3_5_4
+
+-- increase speed of queries for inventory information on one part, e.g.
+
+--   SELECT parts_id, warehouse_id, bin_id, sum(qty)
+--     FROM inventory
+--    WHERE parts_id = 1234
+-- GROUP BY parts_id, bin_id, warehouse_id;
+
+CREATE INDEX inventory_parts_id_idx ON inventory (parts_id);
diff --git a/sql/Pg-upgrade2/inventory_shippingdate_not_null.sql b/sql/Pg-upgrade2/inventory_shippingdate_not_null.sql
new file mode 100644 (file)
index 0000000..a3fa3f9
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: inventory_shippingdate_not_null
+-- @description: shippingdate not null, leeres shippingdate für nachträglich wie itime setzen
+-- @depends: release_3_4_0 inventory_fix_shippingdate_assemblies
+
+UPDATE inventory SET shippingdate = itime WHERE shippingdate IS NULL;
+ALTER TABLE inventory ALTER COLUMN shippingdate SET NOT NULL;
index f8bc911..fea63da 100644 (file)
@@ -1,7 +1,6 @@
 # @tag: invoice_positions
 # @description: Spalte für Positionen der Einträge in Rechnungen
 # @depends: release_3_1_0
-# @encoding: utf-8
 package SL::DBUpgrade2::invoice_positions;
 
 use strict;
@@ -17,8 +16,10 @@ sub run {
 
 
   $query = qq|SELECT * FROM invoice ORDER BY trans_id, id|;
+  my $query2 = qq|UPDATE invoice SET position = ? WHERE id = ?|;
 
   my $sth = $self->dbh->prepare($query);
+  my $sth2 = $self->dbh->prepare($query2);
   $sth->execute || $::form->dberror($query);
 
   # set new position field in order of ids, starting by one for each invoice
@@ -32,10 +33,10 @@ sub run {
     }
     $last_invoice_id = $ref->{trans_id};
 
-    $query = qq|UPDATE invoice SET position = ? WHERE id = ?|;
-    $self->db_query($query, bind => [ $position, $ref->{id} ]);
+    $sth2->execute($position, $ref->{id});
   }
   $sth->finish;
+  $sth2->finish;
 
   $query = qq|ALTER TABLE invoice ALTER COLUMN position SET NOT NULL|;
   $self->db_query($query);
index 9add141..b5e5567 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: invoices_amount_paid_not_null
 -- @description: Bei Rechnungen die drei Spalten "amount", "netamount" und "paid" auf NOT NULL setzen
 -- @depends: release_3_2_0
--- @encoding: utf-8
 
 UPDATE ar SET amount    = 0 WHERE amount    IS NULL;
 ALTER TABLE ar ALTER COLUMN amount    SET NOT NULL;
diff --git a/sql/Pg-upgrade2/konjunkturpaket_2020.sql b/sql/Pg-upgrade2/konjunkturpaket_2020.sql
new file mode 100644 (file)
index 0000000..50a014c
--- /dev/null
@@ -0,0 +1,210 @@
+-- @tag: konjunkturpaket_2020
+-- @description: Anpassung der Steuersätze für 16%/5% für Deutsche DATEV-Kontenrahmen SKR03 und SKR04
+-- @depends: release_3_5_5 konjunkturpaket_2020_SKR03 konjunkturpaket_2020_SKR04
+-- @ignore: 0
+
+-- begin;
+
+DO $$
+
+DECLARE
+  -- variables for main taxkey creation loop, not all are needed
+  _chart_id int;
+  _accno text;
+  _description text;
+  _startdates date[];
+  _tax_ids int[];
+  _taxkeyentry_id int[];
+  _taxkey_ids int[];
+  _rates numeric[];
+  _taxcharts text[];
+
+  current_taxkey record;
+  new_taxkey     record;
+  _rate          numeric;
+  _tax           record; -- store the new tax we need to assign to a chart, e.g. 5%, 16%
+
+  _taxkey    int;
+  _old_rate  numeric;
+  _old_chart text;
+  _new_chart numeric;
+  _new_rate  text;
+
+  _tax_conversion record;
+
+
+BEGIN
+
+IF ( select coa from defaults ) ~ 'DATEV' THEN
+
+--begin;
+--delete from taxkeys where startdate >= '2020-01-01';
+
+--  create temp table temp_taxkey_conversions (taxkey int, old_rate numeric, new_rate numeric, tax_chart_skr03 text, tax_chart_skr04 text);
+--  insert into temp_taxkey_conversions (taxkey, old_rate, new_rate, tax_chart_skr03, tax_chart_skr04) values
+----    (2, 0.07, 0.05, '1773', '3803'),  -- 5% case is handled by skr03 case -> needs different automatic chart: 1773 Umsatzsteuer 5% (SKR03, instead of 1771 Umsatzsteuer 7%) or 3803 Umsatzsteuer 5%
+--    -- (8, 0.07, 0.05, null, null),
+--    -- (3, 0.19, 0.16, null, null),
+--    -- (9, 0.19, 0.16, null, null),
+--   (13, 0.19, 0.16, null, null);
+
+
+  create temp table temp_taxkey_conversions (taxkey int, old_rate numeric, old_chart text, new_rate numeric, new_chart text);
+
+  IF ( select coa from defaults ) = 'Germany-DATEV-SKR03EU' THEN
+    insert into temp_taxkey_conversions (taxkey, old_rate, old_chart, new_rate, new_chart)
+    values (9, 0.19, '1576', 0.16, '1575'),
+           (8, 0.07, '1571', 0.05, '1568'),
+           (3, 0.19, '1776', 0.16, '1575'),
+           (2, 0.07, '1771', 0.05, '1775');
+         --1776 => 19%
+         --1775 => 16%
+         --1775 =>  5%
+         --1771 =>  7%
+         --
+         --VSt:
+         --1576 => 19%
+         --1575 => 16%
+         --1568 =>  5%
+         --1571 =>  7%
+
+  ELSE  -- Germany-DATEV-SKR04EU
+    insert into temp_taxkey_conversions (taxkey, old_rate, old_chart, new_rate, new_chart)
+    values (9, 0.19, '1406', 0.16, '1405'),
+           (8, 0.07, '1401', 0.05, '1403'),
+           (3, 0.19, '3806', 0.16, '3805'),
+           (2, 0.07, '3801', 0.05, '3803');
+  END IF;
+
+  FOR _chart_id, _accno, _description, _startdates, _tax_ids, _taxkeyentry_id, _taxkey_ids, _rates, _taxcharts IN
+
+      select c.id as chart_id,
+             c.accno,
+             c.description,
+             array_agg(t.startdate order by t.startdate desc) as startdates,
+             array_agg(t.tax_id    order by t.startdate desc) as tax_ids,
+             array_agg(t.id        order by t.startdate desc) as taxkeyentry_id,
+             array_agg(t.taxkey_id order by t.startdate desc) as taxkey_ids,
+             array_agg(tax.rate    order by t.startdate desc) as rates,
+             array_agg(tc.accno    order by t.startdate desc) as taxcharts
+        from taxkeys t
+             left join chart c  on (c.id         = t.chart_id)
+             left join tax      on (tax.id       = t.tax_id)
+             left join chart tc on (tax.chart_id = tc.id)
+       where t.taxkey_id in (select taxkey from temp_taxkey_conversions)  -- 2, 3, 8, 9
+             -- and (c.accno = '8400') -- debug
+             -- you can't filter for valid taxrates 19% or 7% here, as that would still leave the 16% rates as the current one
+    group by c.id,
+             c.accno,
+             c.description
+    order by c.accno
+
+    -- example output for human debugging:
+    --  chart_id | accno |     description     |       startdates        |  tax_ids  | taxkeyentry_id | taxkey_ids |       rates       |  taxcharts
+    -- ----------+-------+---------------------+-------------------------+-----------+----------------+------------+-------------------+-------------
+    --       184 | 8400  | Erlöse 16%/19% USt. | {2007-01-01,1970-01-01} | {777,379} | {793,676}      | {3,3}      | {0.19000,0.16000} | {1776,1775}
+
+  -- each chart with one of the applicable taxkeys should receive two new entries, one starting on 01.07.2020, the other on 01.01.2021
+  LOOP
+    -- 1. create new taxkey entry on 2020-07-01, using the active taxkey on 2020-06-30 as a template, but linking to a tax with a different tax rate
+    -- 2. create new taxkey entry on 2021-01-01, using the active taxkey on 2020-06-30 as a template, but with the new date
+
+
+    -- fetch tax information for 2020-06-30, one day before the change, this should also be the first entry in the ordered array aggregates
+    -- this can be used as the template for the reset on 2021-01-01
+
+    -- raise notice 'looking up current taxkey for chart % and taxkey %', (select accno from chart where id = _chart_id), _taxkey_ids[1];
+    select into current_taxkey tk.*, t.rate, t.taxkey
+           from taxkeys tk
+                left join tax t on (t.id = tk.tax_id)
+          where     tk.taxkey_id = _taxkey_ids[1] -- assume taxkey never changed, use the first one
+                and tk.chart_id = _chart_id
+                and tk.startdate <= '2020-06-30'
+       order by tk.startdate desc
+          limit 1;
+    -- RAISE NOTICE 'found current_taxkey = %', current_taxkey;
+    IF current_taxkey is null then continue; end if;
+    -- RAISE NOTICE 'found chart % with current startdate % and taxkey % (current: %), rate = %', _accno, current_taxkey.startdate, _taxkey_ids[1], current_taxkey.taxkey, current_taxkey.rate;
+
+    -- RAISE NOTICE 'current_taxkey = %', current_taxkey;
+    -- RAISE NOTICE 'looking up tkc for chart_id % and taxkey  %', _chart_id, current_taxkey.taxkey;
+
+    select into _taxkey, _old_rate, _old_chart, _new_chart, _new_rate
+                 taxkey,  old_rate,  old_chart,  new_chart,  new_rate
+    from temp_taxkey_conversions tkc
+    where     tkc.taxkey    = current_taxkey.taxkey
+          and tkc.old_rate = current_taxkey.rate;
+          -- and tkc.new_chart = current_taxkey.new_chart;
+
+    -- raise notice '_old_rate = %, _new_rate = %', _old_rate, _new_rate;
+
+    -- don't do anything if current taxrate is 0, which might be the case for taxkey 13, if they were configured in that way
+    IF current_taxkey.rate != 0 THEN  -- debug
+
+      -- _rate := null;
+
+      -- IF current_taxkey.rate = 0.19 THEN _rate := 0.16; END IF;
+      -- IF current_taxkey.rate = 0.07 THEN _rate := 0.05; END IF;
+      IF _old_rate is NULL THEN
+
+        -- option A: ignore rates which don't make sense, useful for upgrade mode
+        -- option B: throw exception, useful for manually testing script
+
+        -- A:
+        -- if the rate on 2020-06-30 is neither 19 or 7, simply ignore it, it is obviously not configured correctly
+        -- This is the case for SKR03 and chart 8315 (taxkey 13)
+        -- It might be better to throw an exception, however then the test cases don't run. Or just fix the chart via an upgrade script!
+        CONTINUE;
+
+        -- B:
+        -- RAISE EXCEPTION 'illegal current taxrate % on 2020-06-30 (startdate = %) for chart % with taxkey %, should be either 0.19 or 0.07',
+        --                 current_taxkey.rate, current_taxkey.startdate,
+        --                 (select accno from chart where id = current_taxkey.chart_id),
+        --                 current_taxkey.taxkey_id;
+      END IF;
+      -- RAISE NOTICE 'current_taxkey.rate = %, desired rate = %, looking for taxkey_id %', current_taxkey.rate, _rate, _taxkey_ids[1];
+
+      -- if a chart was created way after 2007 and only ever configured for
+      -- 19%, never 16%, which is the case for SKR04 and taxkey 13, there will only be 3
+      -- taxkeys per chart after adding the two new ones
+
+      -- RAISE NOTICE 'searching for tax with taxkey % and rate %', _taxkey_ids[1], _rate;
+      select into _tax
+                  *
+             from tax
+            where tax.rate = _old_rate
+                  and tax.taxkey = _taxkey_ids[1]
+         order by itime desc
+            limit 1; -- look up tax with same taxkey but corresponding rate. As there will now be two entries for e.g. taxkey 9 with rate of 0.16, the old pre-2007 entry and the new 2020-entry. They can only be differentiated by their (automatic tax) chart_id, or during this upgrade script, via itime, use the later one
+                     -- this also assumes taxkeys never change
+      -- RAISE NOTICE 'tax = %', _tax;
+
+      -- insert into taxkeys (chart_id,                 tax_id,   taxkey_id,                pos_ustva,    startdate)
+      --              values ( (select id from chart where accno = 'kkkkgtkttttkk current_taxkey.chart_id, _tax.id, _tax.taxkey, current_taxkey.pos_ustva, '2020-07-01');
+    END IF;
+
+    -- raise notice 'inserting taxkey';
+    insert into taxkeys (chart_id,                                tax_id,                taxkey_id,                pos_ustva, startdate   )
+                 values (_chart_id,
+                         (select id from tax where taxkey = current_taxkey.taxkey and rate = _new_rate::numeric),
+                         current_taxkey.taxkey, -- 2, 3, 8, 9
+                         current_taxkey.pos_ustva, '2020-07-01');
+
+    -- finally insert a copy of the taxkey on 2020-06-30 with the new startdate 2021-01-01, thereby resetting the tax rates again
+    insert into taxkeys (chart_id, tax_id, taxkey_id, pos_ustva, startdate)
+                 values (_chart_id,
+                         current_taxkey.tax_id,
+                         current_taxkey.taxkey,
+                         current_taxkey.pos_ustva, '2021-01-01');
+
+    -- RAISE NOTICE 'inserted 2 taxkeys for chart % with taxkey %', (select accno from chart where id = current_taxkey.chart_id), current_taxkey.taxkey_id;
+  END LOOP;  --
+
+  drop table temp_taxkey_conversions;
+
+END IF;
+
+END $$;
+
+-- select * from taxkeys where startdate >= '2020-01-01';
+-- rollback;
diff --git a/sql/Pg-upgrade2/konjunkturpaket_2020_SKR03-korrekturen.sql b/sql/Pg-upgrade2/konjunkturpaket_2020_SKR03-korrekturen.sql
new file mode 100644 (file)
index 0000000..0ae2280
--- /dev/null
@@ -0,0 +1,41 @@
+-- @tag: konjunkturpaket_2020_SKR03-korrekturen
+-- @description: Steuerkonten haben selber keine Steuerautomatik. USTVA-Felder korrigieren
+-- @depends: konjunkturpaket_2020_SKR03 konjunkturpaket_2020
+-- @ignore: 0
+
+DO $$
+BEGIN
+
+IF ( select coa from defaults ) = 'Germany-DATEV-SKR03EU' THEN
+
+  -- DEBUG
+  -- Konto 1771 ist in DATEV vom Typ S und hat keine Steuerautomatik S 1771 Umsatzsteuer 7 %
+  -- Weitere Liste Konten von diesem (s.u.) -> Steuerkonten haben selber keine Automatik
+  -- Der Eintrag wird leider für die pos_ustva benötigt (die könnte besser in tabelle tax sein)
+  -- S 1771 Umsatzsteuer 7 %
+  -- S 1772 Umsatzsteuer aus innergemeinschaftlichem Erwerb
+  -- S 1774 Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %
+  -- S 1775 Umsatzsteuer 16 %
+  -- S 1776 Umsatzsteuer 19 %
+  -- S 1777 Umsatzsteuer aus im Inland steuerpflichtigen EU-Lieferungen
+  -- S 1778 Umsatzsteuer aus im Inland steuerpflichtigen EU-Lieferungen 19 %
+  -- S 1779 Umsatzsteuer aus innergemeinschaftlichem Erwerb ohne Vorsteuerabzug
+  UPDATE taxkeys SET tax_id=0,taxkey_id=0 WHERE chart_id IN
+    (SELECT id FROM chart WHERE accno in ('1771','1772','1774','1775','1776','1777','1778','1779'));
+  -- Alle temporären Steuer auf Pos. 36
+  UPDATE taxkeys SET pos_ustva=36 WHERE chart_id IN
+    (SELECT id FROM chart WHERE accno in ('1773'));
+
+  -- Alle temporären 5% und 16% Erlöskonten auf Pos. 35
+  -- select accno from chart where id in (select chart_id from taxkeys where tax_id in (select id from tax where taxkey=2 and rate=0.05) and pos_ustva=86) order by accno;
+  -- accno
+  -- 2401  8300  8506  8591  8710  8731  8750  8780  8915  8930  8945
+  UPDATE taxkeys SET pos_ustva=35 WHERE tax_id in (SELECT id FROM tax WHERE taxkey=2 AND rate=0.05) AND pos_ustva=86;
+  --  select accno from chart where id in (select chart_id from taxkeys where tax_id in (select id from tax where taxkey=3 and rate=0.16) and pos_ustva=81) order by accno;
+  -- accno
+ -- 2405  2700  2750  8400 8500 8508 8540 8595 8600 8720 8735 8736 8760 8790 8800 8801 8820 8910 8920 8925 8935 8940
+ UPDATE taxkeys SET pos_ustva=35 WHERE tax_id in (SELECT id FROM tax WHERE taxkey=3 AND rate=0.16) and pos_ustva=81;
+
+END IF;
+
+END $$;
diff --git a/sql/Pg-upgrade2/konjunkturpaket_2020_SKR03.sql b/sql/Pg-upgrade2/konjunkturpaket_2020_SKR03.sql
new file mode 100644 (file)
index 0000000..90ae22f
--- /dev/null
@@ -0,0 +1,100 @@
+-- @tag: konjunkturpaket_2020_SKR03
+-- @description: Anpassung des Deutschen DATEV-Kontenrahmen für SKR03 Konjunkturpaket
+-- @depends: release_3_5_5
+-- @ignore: 0
+
+DO $$
+BEGIN
+
+IF ( select coa from defaults ) = 'Germany-DATEV-SKR03EU' THEN
+
+  -- DEBUG
+  -- UPDATE tax SET taxdescription = 'OLD ' || taxdescription WHERE (taxkey = 3 or taxkey = 9) and rate = 0.16;
+
+  -- rename some of the charts, 1773 already exists in kivitendo as Umsatzsteuer 16% innergem.Erwerb
+  -- this is being used by taxkey 13, which is called "Steuerpflichtige EG-Lieferung zum vollen Steuersatz" in kivitendo
+  -- in DATEV taxkey 13 is: innergem. Lieferung ohne USt-IdNr. and should use a different chart
+  UPDATE chart SET description = 'Umsatzsteuer 5 %' where accno = '1773';
+
+  -- rename charts if they weren't already changed
+  UPDATE chart SET description = 'Erlöse 19 % / 16 % USt' where accno = '8400' and description = 'Erlöse 16%/19% USt.';
+  UPDATE chart SET description = 'Erlöse 7 % / 5 % USt'   where accno = '8300' and description = 'Erlöse 7%USt';
+
+  -- there are two strategies for updating the taxkeys.
+
+  -- 1) in any case we need to add the 2 new cases for 5%: 2/0.05/1773 and 8/0.05/1568
+
+  -- 2) default kivi SKR03 already has the correct configuration for 16%, with two entries 3/0.16/1775 and 9/0.16/1575
+  --   a) we could move those to 5 and 7, and then create new 3/0.16/1775 and 9/0.16/1575 entries
+  --   b) simply keep those entries and don't use 5 and 7 (in which case ar/ap/gl must use deliverydate), or create 5 and 7 manually if needed
+
+  -- strategy a:
+  -- datev reactivated the previously reserved chart 1775 in 2020, but it still exists in kivitendo (at least for SKR03)
+  -- with a taxkey starting from 2007 and pointing to the existing automatic tax chart 1775
+
+  -- strategy b:
+  -- UPDATE tax SET taxkey = 5 WHERE taxkey = 3 and rate = 0.16;
+  -- UPDATE tax SET taxkey = 7 WHERE taxkey = 9 and rate = 0.16;
+
+  -- rename old 8735 to 8736
+  UPDATE chart SET accno = '8736', description = 'Gewährte Skonti 19 % USt' where accno = '8735' and description = 'Gewährte Skonti 16%/19% USt.';
+
+  -- new charts, each of these will need a manual taxkey entry for 2020-07-01 after their tax entries are added
+  -- 8732, 3732, 8735, 3737
+  INSERT INTO chart (accno, description, charttype, category, link, taxkey_id, pos_bwa, pos_bilanz, pos_eur, datevautomatik)
+         VALUES ('8732','Gewährte Skonti 5% USt','A', 'I', 'AR_paid', 2, 1, null,1, 't');
+
+  INSERT INTO chart (accno, description, charttype, category, link, taxkey_id, pos_bwa, pos_bilanz, pos_eur, datevautomatik)
+         VALUES ('3732','Erhaltene Skonti 5 % Vorsteuer','A', 'E', 'AP_paid', 8, 4, null, null, 't');
+
+  -- create new 16% charts Skonto
+  INSERT INTO chart(accno,                description, charttype, category,      link, taxkey_id, pos_bwa, pos_bilanz, pos_eur, datevautomatik, pos_er)
+            VALUES ('8735','Gewährte Skonti 16 % USt',       'A',      'I', 'AR_paid',         3,       1,       null,       1,            't',      1);
+
+  INSERT INTO chart(accno,                description, charttype, category,       link, taxkey_id, pos_bwa, pos_bilanz, pos_eur, datevautomatik, pos_er)
+            VALUES ('3737','Erhaltene Skonti 16 % USt',       'A',      'E', 'AP_paid',         9,       4,       null,    null,            't',   null);
+
+  -- create new chart for Abziehbare Vorsteuer 5 % with taxkey 8 for 3732
+  INSERT INTO chart (accno, description, charttype, category, link, taxkey_id, pos_bwa, pos_bilanz, pos_eur, datevautomatik, pos_er)
+         VALUES ('1568','Abziehbare Vorsteuer 5 %','A', 'E', 'AP_tax:IC_taxpart:IC_taxservice', 8, null, null, 27, 't', 27);
+  INSERT INTO taxkeys (chart_id, tax_id, taxkey_id, pos_ustva, startdate)
+               VALUES ( (select id from chart where accno = '1568'), 0, 0, 66, '1970-01-01');
+
+  -- taxkeys can't be inserted until the new taxes exist
+
+  -- new taxes:
+  -- 5% cases for 2 Umsatzsteuer and 8 Vorsteuer
+  INSERT INTO tax (chart_id, rate, taxkey, taxdescription, chart_categories, skonto_sales_chart_id, skonto_purchase_chart_id)
+  VALUES ( (select id from chart where accno = '1773'), 0.05, 2, 'Umsatzsteuer', 'I', (select id from chart where accno = '8732'), null),
+         -- don't add these two entries if we keep the original two 16% accounts, instead better to add new tax entries with taxkey 5 and 7
+         -- ( (select id from chart where accno = '1775'), 0.16, 3, 'Umsatzsteuer', 'I', (select id from chart where accno = '8735'), null),
+         -- ( (select id from chart where accno = '1575'), 0.16, 9, 'Vorsteuer',    'E', null, (select id from chart where accno = '3735')),
+         ( (select id from chart where accno = '1568'), 0.05, 8, 'Vorsteuer',    'E', null, (select id from chart where accno = '3732'));
+
+  UPDATE tax SET skonto_sales_chart_id    = (select id from chart where accno = '8735') where taxkey = 3 and rate = 0.16 and skonto_sales_chart_id    is null;
+  UPDATE tax SET skonto_purchase_chart_id = (select id from chart where accno = '3737') where taxkey = 9 and rate = 0.16 and skonto_purchase_chart_id is null;
+
+  -- new taxkeys for 5% charts only need one startdate, not valid before and won't change back to anything later
+  -- these taxkeys won't be valid on 2020-06-30, so won't be affected later by big taxkeys update
+  -- However, this will also cause opening the charts before 2020-07-01 via the
+  -- interface to break, as AM.pm always calls get_active_taxkey and there won't
+  -- be an active taxkey before 2020-07-01.
+  -- Alternatively you could set those active from 2020-06-01 and in the taxkey upgrade script check for taxkey entries before that date
+  INSERT INTO taxkeys (chart_id, tax_id, taxkey_id, pos_ustva, startdate)
+               VALUES ( (select id from chart where accno = '8732'), (select id from tax where rate = 0.05 and taxkey = 2 and chart_id = (select id from chart where accno = '1773')), 2, 861, '2020-07-01');
+
+  INSERT INTO taxkeys (chart_id, tax_id, taxkey_id, pos_ustva, startdate)
+               VALUES ( (select id from chart where accno = '3732'), (select id from tax where rate = 0.05 and taxkey = 8 and chart_id = (select id from chart where accno = '1568')), 8, 861, '2020-07-01');
+
+  -- 8735 / 3737 - these were never created in the original SKR03, so also start using them from 2020-07-01
+  -- taxkey for Gewährte Skonti 16 % USt pointing to tax 1775 Umsatzsteuer 16%
+  INSERT INTO taxkeys (chart_id, tax_id, taxkey_id, pos_ustva, startdate)
+               VALUES ( (select id from chart where accno = '8735'), (select id from tax where rate = 0.16 and taxkey = 3 and chart_id = (select id from chart where accno = '1775')), 3, 81, '2020-07-01');
+
+  -- taxkey for Erhaltene Skonti 16 % USt pointing to tax 1575 Vorsteuer 16%
+  INSERT INTO taxkeys (chart_id, tax_id, taxkey_id, pos_ustva, startdate)
+               VALUES ( (select id from chart where accno = '3737'), (select id from tax where rate = 0.16 and taxkey = 9 and chart_id = (select id from chart where accno = '1575')), 9, 66, '2020-07-01');
+
+END IF;
+
+END $$;
diff --git a/sql/Pg-upgrade2/konjunkturpaket_2020_SKR04-korrekturen.sql b/sql/Pg-upgrade2/konjunkturpaket_2020_SKR04-korrekturen.sql
new file mode 100644 (file)
index 0000000..ca10583
--- /dev/null
@@ -0,0 +1,28 @@
+-- @tag: konjunkturpaket_2020_SKR04-korrekturen
+-- @description: USTVA-Felder korrigieren
+-- @depends: konjunkturpaket_2020_SKR04
+-- @ignore: 0
+
+DO $$
+BEGIN
+
+IF ( select coa from defaults ) = 'Germany-DATEV-SKR04EU' THEN
+
+  -- Alle temporären Steuer auf Pos. 36
+  UPDATE taxkeys SET pos_ustva=36 WHERE chart_id IN
+    (SELECT id FROM chart WHERE accno in ('3803','3805'));
+
+  -- Alle temporären 5% und 16% Erlöskonten auf Pos. 35
+  -- select accno from chart where id in (select chart_id from taxkeys where tax_id in (select id from tax where taxkey=2 and rate=0.05) and pos_ustva=86) order by accno;
+  -- accno
+  -- 4300 4566 4610 4630 4670 4710 4731 4750 4780 4941 6281
+  UPDATE taxkeys SET pos_ustva=35 WHERE tax_id in (SELECT id FROM tax WHERE taxkey=2 AND rate=0.05) AND pos_ustva=86;
+  --  select accno from chart where id in (select chart_id from taxkeys where tax_id in (select id from tax where taxkey=3 and rate=0.16)) order by accno;
+  -- accno
+  -- 4400 4500 4510 4520 4569 4620 4640 4660 4680 4686 4720 4736 4760 4790 4830 4835 4849 4860 4945 6286 6287
+
+ UPDATE taxkeys SET pos_ustva=35 WHERE tax_id in (SELECT id FROM tax WHERE taxkey=3 AND rate=0.16);
+
+END IF;
+
+END $$;
diff --git a/sql/Pg-upgrade2/konjunkturpaket_2020_SKR04.sql b/sql/Pg-upgrade2/konjunkturpaket_2020_SKR04.sql
new file mode 100644 (file)
index 0000000..552e285
--- /dev/null
@@ -0,0 +1,54 @@
+-- @tag: konjunkturpaket_2020_SKR04
+-- @description: Anpassung des Deutschen DATEV-Kontenrahmen für SKR04 Konjunkturpaket
+-- @depends: release_3_5_5 remove_double_tax_entries_skr04
+-- @ignore: 0
+
+DO $$
+BEGIN
+
+IF ( select coa from defaults ) = 'Germany-DATEV-SKR04EU' THEN
+
+  -- charts 1403 und 3803 for 5% taxes already existed, reconfigure them
+  UPDATE chart set description = 'Abziehbare Vorsteuer 5 %', taxkey_id = 8 where accno = '1403' and description = 'Abziehbare Vorsteuer aus innergemeinschftl. Erwerb 16%';
+  UPDATE chart set description = 'Umsatzsteuer 5 %', taxkey_id = 2 where accno = '3803' and description = 'Umsatzsteuer aus innergemeinschftl. Erwerb 16%';
+
+  -- DEBUG
+  -- UPDATE tax SET taxdescription = 'OLD ' || taxdescription WHERE (taxkey = 5 or taxkey = 7); -- and rate = 0.16;
+
+  UPDATE taxkeys SET tax_id = (SELECT id FROM tax WHERE taxkey = 5 and rate = 0.16)
+   WHERE chart_id = (SELECT id FROM chart where accno = '4400')
+     AND startdate = '1970-01-01';
+
+  -- new charts for 5%
+  -- 4732 and 5732
+  INSERT INTO chart (accno, description, charttype, category, link, taxkey_id, pos_bwa, pos_bilanz, pos_eur, datevautomatik)
+         VALUES ('4732','Gewährte Skonti 5 % USt','A', 'I', 'AR_paid', 2, 1, null, 1, 't');
+  INSERT INTO chart (accno, description, charttype, category, link, taxkey_id, pos_bwa, pos_bilanz, pos_eur, datevautomatik)
+         VALUES ('5732','Erhaltene Skonti 5 % Vorsteuer','A', 'E', 'AP_paid', 8, 4, null, null, 't');
+
+  -- Gewährte and Erhaltene Skonti 16% already exist, but rename them
+  UPDATE chart SET description = 'Gewährte Skonti 16%'  where accno = '4735' and description = 'Gewährte Skonti 16%/19% USt';
+  UPDATE chart SET description = 'Erhaltene Skonti 16%' where accno = '4735' and description = 'Erhaltene Skonti 16%/19% USt';
+
+  -- taxkeys can't be inserted until the new taxes exist
+  INSERT INTO tax (chart_id, rate, taxkey, taxdescription, chart_categories, skonto_sales_chart_id, skonto_purchase_chart_id)
+  VALUES ( (select id from chart where accno = '3803'), 0.05, 2, 'Umsatzsteuer', 'I', (select id from chart where accno = '4732'), null), -- ok
+         ( (select id from chart where accno = '3805'), 0.16, 3, 'Umsatzsteuer', 'I', (select id from chart where accno = '4735'), null),
+         ( (select id from chart where accno = '1405'), 0.16, 9, 'Vorsteuer',    'E', null, (select id from chart where accno = '5735')),
+         ( (select id from chart where accno = '1403'), 0.05, 8, 'Vorsteuer',    'E', null, (select id from chart where accno = '5732'));
+
+  -- new taxkeys for 5% and 16% only need one startdate, not valid before and won't change back to anything later
+  -- these taxkeys won't be valid on 2020-06-30, so won't be affected later by big taxkeys update
+  -- 4732 and 5732
+  INSERT INTO taxkeys (chart_id, tax_id, taxkey_id, pos_ustva, startdate)
+               VALUES ( (select id from chart where accno = '4732'),
+                      ( select id from tax where rate = 0.05 and taxkey = 2 and chart_id = (select id from chart where accno = '3803')), 2, 861, '2020-07-01'); -- ustva_id like 3801, is this correct?
+
+  INSERT INTO taxkeys (chart_id, tax_id, taxkey_id, pos_ustva, startdate)
+               VALUES ( (select id from chart where accno = '5732'),
+                      (select id from tax where rate = 0.05 and taxkey = 8 and chart_id = (select id from chart where accno = '1403')), 8, 66, '2020-07-01'); -- ustva_id like 1401, is this correct?
+
+  -- the taxkeys for the existing charts will be updated in a later update
+END IF;
+
+END $$;
diff --git a/sql/Pg-upgrade2/language_obsolete.sql b/sql/Pg-upgrade2/language_obsolete.sql
new file mode 100644 (file)
index 0000000..53d28b5
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: language_obsolete
+-- @description: Sprachen ungültig setzen
+-- @depends: release_3_6_0
+-- @ignore: 0
+
+ALTER TABLE language ADD COLUMN obsolete BOOLEAN DEFAULT FALSE;
diff --git a/sql/Pg-upgrade2/letter_cleanup.sql b/sql/Pg-upgrade2/letter_cleanup.sql
new file mode 100644 (file)
index 0000000..e87f6e0
--- /dev/null
@@ -0,0 +1,40 @@
+-- @tag: letter_cleanup
+-- @description: Tabelle »letter«: Unbenutzte Spalten entfernen und andere Spalten umbenennen
+-- @depends: release_3_4_0
+
+ALTER TABLE letter       RENAME COLUMN vc_id TO customer_id;
+ALTER TABLE letter_draft RENAME COLUMN vc_id TO customer_id;
+
+ALTER TABLE letter
+  DROP COLUMN close,
+  DROP COLUMN company_name,
+  DROP COLUMN employee_position,
+  DROP COLUMN jobnumber,
+  DROP COLUMN page_created_for,
+  DROP COLUMN rcv_address,
+  DROP COLUMN rcv_city,
+  DROP COLUMN rcv_contact,
+  DROP COLUMN rcv_country,
+  DROP COLUMN rcv_countrycode,
+  DROP COLUMN rcv_name,
+  DROP COLUMN rcv_zipcode,
+  DROP COLUMN salesman_position,
+  DROP COLUMN text_created_for,
+  ADD FOREIGN KEY (customer_id) REFERENCES customer (id);
+
+ALTER TABLE letter_draft
+  DROP COLUMN close,
+  DROP COLUMN company_name,
+  DROP COLUMN employee_position,
+  DROP COLUMN jobnumber,
+  DROP COLUMN page_created_for,
+  DROP COLUMN rcv_address,
+  DROP COLUMN rcv_city,
+  DROP COLUMN rcv_contact,
+  DROP COLUMN rcv_country,
+  DROP COLUMN rcv_countrycode,
+  DROP COLUMN rcv_name,
+  DROP COLUMN rcv_zipcode,
+  DROP COLUMN salesman_position,
+  DROP COLUMN text_created_for,
+  ADD FOREIGN KEY (customer_id) REFERENCES customer (id);
index 8c6293a..931e8f0 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: letter_date_type
 -- @description: Briefe: Datumsfeld als Datum speichern
 -- @depends: release_3_2_0 letter
--- @encoding: utf-8
 ALTER TABLE letter ADD column date_date DATE;
 UPDATE letter SET date_date = date::DATE;
 ALTER TABLE letter DROP COLUMN date;
diff --git a/sql/Pg-upgrade2/letter_vendorletter.sql b/sql/Pg-upgrade2/letter_vendorletter.sql
new file mode 100644 (file)
index 0000000..8c33edf
--- /dev/null
@@ -0,0 +1,9 @@
+-- @tag: letter_vendorletter
+-- @description: Briefe jetzt auch für Lieferanten
+-- @depends: release_3_4_1
+
+ALTER TABLE letter ALTER COLUMN customer_id DROP NOT NULL;
+ALTER TABLE letter ADD COLUMN vendor_id INTEGER REFERENCES vendor(id);
+
+ALTER TABLE letter_draft ALTER COLUMN customer_id DROP NOT NULL;
+ALTER TABLE letter_draft ADD COLUMN vendor_id INTEGER REFERENCES vendor(id);
diff --git a/sql/Pg-upgrade2/link_requirement_spec_to_orders_created_from_quotations_created_from_requirement_spec.sql b/sql/Pg-upgrade2/link_requirement_spec_to_orders_created_from_quotations_created_from_requirement_spec.sql
new file mode 100644 (file)
index 0000000..cf549ac
--- /dev/null
@@ -0,0 +1,29 @@
+-- @tag: link_requirement_spec_to_orders_created_from_quotations_created_from_requirement_spec
+-- @description: Pflichtenhefte mit Aufträgen verknüpfen, die aus Angeboten erstellt wurden, die wiederum aus einem Pflichtenheft erstellt wurden
+-- @depends: release_3_2_0
+CREATE TEMPORARY TABLE temp_link_requirement_spec_to_orders AS
+SELECT rs_orders.requirement_spec_id, orders.id AS order_id, rs_orders.version_id
+FROM record_links rl,
+  requirement_spec_orders rs_orders,
+  oe quotations,
+  oe orders
+WHERE (rl.from_table      = 'oe')
+  AND (rl.from_id         = quotations.id)
+  AND (rl.to_table        = 'oe')
+  AND (rl.to_id           = orders.id)
+  AND (rs_orders.order_id = quotations.id)
+  AND     COALESCE(quotations.quotation, FALSE)
+  AND NOT COALESCE(orders.quotation,     FALSE)
+  AND (quotations.customer_id IS NOT NULL)
+  AND (orders.customer_id     IS NOT NULL);
+
+INSERT INTO requirement_spec_orders (requirement_spec_id, order_id, version_id)
+SELECT requirement_spec_id, order_id, version_id
+FROM temp_link_requirement_spec_to_orders new_orders
+WHERE NOT EXISTS (
+  SELECT existing_orders.id
+  FROM requirement_spec_orders existing_orders
+  WHERE (existing_orders.requirement_spec_id = new_orders.requirement_spec_id)
+    AND (existing_orders.order_id            = new_orders.order_id)
+  LIMIT 1
+);
diff --git a/sql/Pg-upgrade2/makemodel_add_vendor_foreign_key.sql b/sql/Pg-upgrade2/makemodel_add_vendor_foreign_key.sql
new file mode 100644 (file)
index 0000000..85d60ce
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: makemodel_add_vendor_foreign_key
+-- @description: Makemodel make mit Lieferant verknüpft
+-- @depends: release_3_4_1
+-- @ignore: 0
+
+ALTER TABLE makemodel ADD FOREIGN KEY (make) REFERENCES vendor(id);
diff --git a/sql/Pg-upgrade2/mebil_v1.sql b/sql/Pg-upgrade2/mebil_v1.sql
new file mode 100644 (file)
index 0000000..751b264
--- /dev/null
@@ -0,0 +1,102 @@
+-- @tag: mebil_v1
+-- @description: mebil-DB
+-- @depends:
+
+-- Table of mebil extensions
+-- for module 
+-- Lx office
+DROP TABLE IF EXISTS mebil_mapping;
+CREATE TABLE mebil_mapping (
+       id                      SERIAL PRIMARY KEY,
+       ordering        INTEGER NOT NULL,
+       typ                     CHAR NOT NULL,
+       fromacc         VARCHAR(200) NOT NULL,
+       toacc           VARCHAR(200) NOT NULL
+);
+INSERT INTO mebil_mapping (ordering,typ,fromacc,toacc)
+VALUES
+       (10,'S','0483','bs.ass.fixAss.tan.machinery.gwgsammelposten'),
+       (10,'S','0484','bs.ass.fixAss.tan.machinery.gwgsammelposten'),
+       (20,'X','bs.ass.fixAss.tan.machinery.gwgsammelposten','bs.ass.fixAss.tan.machinery'),
+       (21,'X','bs.ass.fixAss.tan.machinery','bs.ass.fixAss.tan'),
+       (22,'X','bs.ass.fixAss.tan','bs.ass.fixAss'),
+       (50,'X','bs.ass.fixAss','bs.ass'),
+
+   (10,'S','1200','bs.ass.currAss.cashEquiv.bank'),
+   (20,'X','bs.ass.currAss.cashEquiv.bank','bs.ass.currAss.cashEquiv'),
+   (21,'X','bs.ass.currAss.cashEquiv.bank','bs.ass.currAss'),
+
+       (10,'S','3980','bs.ass.currAss.inventory.finishedAndMerch.merchandise.new'),
+       (20,'X','bs.ass.currAss.inventory.finishedAndMerch.merchandise.new','bs.ass.currAss.inventory.finishedAndMerch.merchandise'),
+       (21,'X','bs.ass.currAss.inventory.finishedAndMerch.merchandise','bs.ass.currAss.inventory.finishedAndMerch'),
+       (22,'X','bs.ass.currAss.inventory.finishedAndMerch','bs.ass.currAss.inventory'),
+       (23,'X','bs.ass.currAss.inventory','bs.ass.currAss'),
+
+       (10,'S','1400','bs.ass.currAss.receiv.trade'),
+       (20,'X','bs.ass.currAss.receiv.trade','bs.ass.currAss.receiv'),
+
+       (10,'S','1575','bs.ass.currAss.receiv.other.vat'),
+       (10,'S','1576','bs.ass.currAss.receiv.other.vat'),
+       (10,'S','1579','bs.ass.currAss.receiv.other.vat'),
+       (10,'S','1780','bs.ass.currAss.receiv.other.vat'),
+       (20,'X','bs.ass.currAss.receiv.other.vat','bs.ass.currAss.receiv.other'),
+       (21,'X','bs.ass.currAss.receiv.other','bs.ass.currAss.receiv'),
+       (30,'X','bs.ass.currAss.receiv','bs.ass.currAss'),
+
+       (50,'X','bs.ass.currAss','bs.ass'),
+       
+       (10,'H','0800','bs.eqLiab.equity.subscribed.corp'),
+       (20,'X',       'bs.eqLiab.equity.subscribed.corp','bs.eqLiab.equity.subscribed'),
+       (40,'X',       'bs.eqLiab.equity.subscribed',     'bs.eqLiab.equity'),
+       (10,'H','0810','bs.eqLiab.equity.capRes'),
+       (20,'X',       'bs.eqLiab.equity.capRes',         'bs.eqLiab.equity'),
+       (50,'X',       'bs.eqLiab.equity',                'bs.eqLiab'),
+       (10,'H','0970','bs.eqLiab.accruals.other.upTo1year'),
+       (20,'X',       'bs.eqLiab.accruals.other.upTo1year','bs.eqLiab.accruals.other'),
+       (21,'X',       'bs.eqLiab.accruals.other','bs.eqLiab.accruals'),
+       (50,'X',       'bs.eqLiab.accruals','bs.eqLiab'),
+       (10,'H','1775','bs.eqLiab.liab.other.theroffTax.vat'),
+       (10,'H','1776','bs.eqLiab.liab.other.theroffTax.vat'),
+       (20,'X',       'bs.eqLiab.liab.other.theroffTax.vat','bs.eqLiab.liab.other.theroffTax'),
+       (10,'H','1651','bs.eqLiab.liab.other.theroffTax.operatingTaxes'),
+       (10,'H','1652','bs.eqLiab.liab.other.theroffTax.operatingTaxes'),
+       (10,'H','1653','bs.eqLiab.liab.other.theroffTax.operatingTaxes'),
+       (20,'X',       'bs.eqLiab.liab.other.theroffTax.operatingTaxes','bs.eqLiab.liab.other.theroffTax'),
+       (21,'X',       'bs.eqLiab.liab.other.theroffTax','bs.eqLiab.liab.other'),
+       (10,'H','1606','bs.eqLiab.liab.other.profitPartLoans'),
+       (20,'X',       'bs.eqLiab.liab.other.profitPartLoans','bs.eqLiab.liab.other'),
+       (22,'X',       'bs.eqLiab.liab.other','bs.eqLiab.liab'),
+       (50,'X',       'bs.eqLiab.liab','bs.eqLiab'),
+       
+       (10,'S','4121','is.netIncome.regular.operatingTC.staff.salaries.managerPartner'),
+       (20,'X'       ,'is.netIncome.regular.operatingTC.staff.salaries.managerPartner','is.netIncome.regular.operatingTC.staff.salaries'),
+       (21,'X'       ,'is.netIncome.regular.operatingTC.staff.salaries','ismi.netIncome.staff'),
+       (50,'Y'       ,'ismi.netIncome.staff','ismi.netIncome'),
+       (10,'S','3960','is.netIncome.regular.operatingTC.grossTradingProfit.materialServices.material.purchased.generalRateVAT'),
+       (20,'X',       'is.netIncome.regular.operatingTC.grossTradingProfit.materialServices.material.purchased.generalRateVAT','is.netIncome.regular.operatingTC.grossTradingProfit.materialServices.material.purchased'),
+       (21,'X',       'is.netIncome.regular.operatingTC.grossTradingProfit.materialServices.material.purchased','is.netIncome.regular.operatingTC.grossTradingProfit.materialServices.material'),
+       (22,'X',       'is.netIncome.regular.operatingTC.grossTradingProfit.materialServices.material','ismi.netIncome.materialServices'),
+       (50,'Y',       'ismi.netIncome.materialServices','ismi.netIncome'),
+       
+       (50,'X','bs.ass.fixAss.tan.machinery.gwgsammelposten','all'),
+       (10,'R','ACC=0482:START=2016:VALUES=negative','grossCost.beginning'),
+       (10,'R','ACC=0484:START=2017:VALUES=negative','grossCost.beginning'),
+       (50,'X','grossCost.beginning','gross'),
+       (50,'X','gross.addition','gross'),
+       (10,'R','ACC=0483:START=YEAR:VALUES=negative','gross.addition'),
+       (52,'X','grossCost.beginning','accDepr'),
+       (52,'X','gross.addition','accDepr'),
+       (52,'Y','all','accDepr'),
+       (50,'X','grossCost.beginning','accDepr.beginning'),
+       (50,'Y','all_Prev_period','accDepr.beginning'),
+       (50,'X','all_Prev_period','accDepr.DeprPeriod'),
+       (50,'X','gross.addition','accDepr.DeprPeriod'),
+       (50,'Y','all','accDepr.DeprPeriod'),
+       (60,'X','accDepr.DeprPeriod','accDepr.DeprPeriod.regular'),
+       (10,'R','ACC=0482:END=PY:INVERT=true','all_Prev_period'),
+       (10,'R','ACC=0483:END=PY:INVERT=true','all_Prev_period'),
+       (10,'R','ACC=0484:END=PY:INVERT=true','all_Prev_period'),
+       
+       (0,'C','de-gaap-ci.bsAss','hbst.transfer.bsAss.name')
+       ;
+
diff --git a/sql/Pg-upgrade2/new_chart_1593_1495.sql b/sql/Pg-upgrade2/new_chart_1593_1495.sql
new file mode 100644 (file)
index 0000000..d6f83d7
--- /dev/null
@@ -0,0 +1,37 @@
+-- @tag: new_chart_1593_1495
+-- @description: Neue Konten "Verrechnungskonto erhalt. Anzahl. bei Buchung über Debitorenkonto"
+-- @depends: release_3_5_8
+
+
+DO $$
+BEGIN
+
+  IF ( SELECT coa FROM defaults ) = 'Germany-DATEV-SKR03EU' THEN
+    DECLARE
+      new_accno text := '1593';
+
+    BEGIN
+      IF ( SELECT COUNT(accno) FROM chart WHERE accno LIKE new_accno ) = 0 THEN
+        INSERT INTO chart (accno, description, charttype, category, link, taxkey_id)
+          VALUES (new_accno, 'Verrechnungskonto erhalt. Anzahl. bei Buchung über Debitorenkonto','A', 'L', 'AR_amount', 0);
+        INSERT INTO taxkeys (chart_id, tax_id, taxkey_id, startdate)
+          VALUES ((SELECT id FROM chart WHERE accno LIKE new_accno), 0, 0, '1970-01-01');
+      END IF;
+    END;
+  END IF;
+
+  IF ( SELECT coa FROM defaults ) = 'Germany-DATEV-SKR04EU' THEN
+    DECLARE
+      new_accno text := '1495';
+
+    BEGIN
+      IF ( SELECT COUNT(accno) FROM chart WHERE accno LIKE new_accno ) = 0 THEN
+        INSERT INTO chart (accno, description, charttype, category, link, taxkey_id)
+          VALUES (new_accno, 'Verrechnungskonto erhalt. Anzahl. bei Buchung über Debitorenkonto','A', 'L', 'AR_amount', 0);
+        INSERT INTO taxkeys (chart_id, tax_id, taxkey_id, startdate)
+          VALUES ((SELECT id FROM chart WHERE accno LIKE new_accno), 0, 0, '1970-01-01');
+      END IF;
+    END;
+  END IF;
+
+END $$;
diff --git a/sql/Pg-upgrade2/new_chart_3260_1711.sql b/sql/Pg-upgrade2/new_chart_3260_1711.sql
new file mode 100644 (file)
index 0000000..a9fb7d4
--- /dev/null
@@ -0,0 +1,37 @@
+-- @tag: new_chart_3260_1711
+-- @description: Neues Konto "Erhaltene, versteuerte Anzahlungen 7 % USt (Verbindlichkeiten)"
+-- @depends: release_3_5_8
+
+
+DO $$
+BEGIN
+
+  IF ( SELECT coa FROM defaults ) = 'Germany-DATEV-SKR03EU' THEN
+    DECLARE
+      new_accno text := '1711';
+
+    BEGIN
+      IF ( SELECT COUNT(accno) FROM chart WHERE accno LIKE new_accno ) = 0 THEN
+        INSERT INTO chart (accno, description, charttype, category, link, taxkey_id)
+          VALUES (new_accno, 'Erhaltene, versteuerte Anzahlungen 7 % USt (Verbindlichkeiten)','A', 'L', 'AR_amount', 0);
+        INSERT INTO taxkeys (chart_id, tax_id, taxkey_id, startdate)
+          VALUES ((SELECT id FROM chart WHERE accno LIKE new_accno), 0, 0, '1970-01-01');
+      END IF;
+    END;
+  END IF;
+
+  IF ( SELECT coa FROM defaults ) = 'Germany-DATEV-SKR04EU' THEN
+    DECLARE
+      new_accno text := '3260';
+
+    BEGIN
+      IF ( SELECT COUNT(accno) FROM chart WHERE accno LIKE new_accno ) = 0 THEN
+        INSERT INTO chart (accno, description, charttype, category, link, taxkey_id)
+          VALUES (new_accno, 'Erhaltene, versteuerte Anzahlungen 7 % USt (Verbindlichkeiten)','A', 'L', 'AR_amount', 0);
+        INSERT INTO taxkeys (chart_id, tax_id, taxkey_id, startdate)
+          VALUES ((SELECT id FROM chart WHERE accno LIKE new_accno), 0, 0, '1970-01-01');
+      END IF;
+    END;
+  END IF;
+
+END $$;
diff --git a/sql/Pg-upgrade2/new_chart_3272_1718.sql b/sql/Pg-upgrade2/new_chart_3272_1718.sql
new file mode 100644 (file)
index 0000000..24c973a
--- /dev/null
@@ -0,0 +1,37 @@
+-- @tag: new_chart_3272_1718
+-- @description: Neues Konto "Erhaltene, versteuerte Anzahlungen 19 % USt (Verbindlichkeiten)"
+-- @depends: release_3_5_8
+
+
+DO $$
+BEGIN
+
+  IF ( SELECT coa FROM defaults ) = 'Germany-DATEV-SKR03EU' THEN
+    DECLARE
+      new_accno text := '1718';
+
+    BEGIN
+      IF ( SELECT COUNT(accno) FROM chart WHERE accno LIKE new_accno ) = 0 THEN
+        INSERT INTO chart (accno, description, charttype, category, link, taxkey_id)
+          VALUES (new_accno, 'Erhaltene, versteuerte Anzahlungen 19 % USt (Verbindlichkeiten)','A', 'L', 'AR_amount', 0);
+        INSERT INTO taxkeys (chart_id, tax_id, taxkey_id, startdate)
+          VALUES ((SELECT id FROM chart WHERE accno LIKE new_accno), 0, 0, '1970-01-01');
+      END IF;
+    END;
+  END IF;
+
+  IF ( SELECT coa FROM defaults ) = 'Germany-DATEV-SKR04EU' THEN
+    DECLARE
+      new_accno text := '3272';
+
+    BEGIN
+      IF ( SELECT COUNT(accno) FROM chart WHERE accno LIKE new_accno ) = 0 THEN
+        INSERT INTO chart (accno, description, charttype, category, link, taxkey_id)
+          VALUES (new_accno, 'Erhaltene, versteuerte Anzahlungen 19 % USt (Verbindlichkeiten)','A', 'L', 'AR_amount', 0);
+        INSERT INTO taxkeys (chart_id, tax_id, taxkey_id, startdate)
+          VALUES ((SELECT id FROM chart WHERE accno LIKE new_accno), 0, 0, '1970-01-01');
+      END IF;
+    END;
+  END IF;
+
+END $$;
index 2be82f1..a5575f6 100644 (file)
@@ -7,6 +7,7 @@ use strict;
 use utf8;
 
 use SL::DBUtils;
+use SL::Presenter::EscapedText qw(escape);
 
 use parent qw(SL::DBUpgrade2::Base);
 
@@ -18,7 +19,7 @@ sub convert_column {
   foreach my $row (selectall_hashref_query($::form, $self->dbh, qq|SELECT id, $column FROM $table WHERE $column IS NOT NULL|)) {
     next if !$row->{$column} || (($row->{$column} =~ m{^<[a-z]+>}) && ($row->{$column} =~ m{</[a-z]+>$}));
 
-    my $new_content = "" . $::request->presenter->escape($row->{$column});
+    my $new_content = "" . escape($row->{$column});
     $new_content    =~ s{\r}{}g;
     $new_content    =~ s{\n\n+}{</p><p>}g;
     $new_content    =~ s{\n}{<br />}g;
index 47bdea9..dcb3107 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: oe_customer_vendor_fkeys
 -- @description: Foreign Keys für customer und vendor in oe
 -- @depends: release_2_6_3
--- @timestamp: 1317380460
 UPDATE oe SET customer_id = NULL WHERE customer_id = 0;
 UPDATE oe SET   vendor_id = NULL WHERE   vendor_id = 0;
 
index cda9200..97d1a31 100644 (file)
@@ -1,7 +1,6 @@
 # @tag: orderitems_delivery_order_items_positions
 # @description: Spalte für Positionen der Einträge in Angeboten/Auftträgen und Lieferscheinen.
 # @depends: release_3_1_0
-# @encoding: utf-8
 package SL::DBUpgrade2::orderitems_delivery_order_items_positions;
 
 use strict;
@@ -25,8 +24,10 @@ sub run {
 
     my $order_id_col = $order_id_cols{ $table };
     $query = qq|SELECT * FROM $table ORDER BY $order_id_col, id|;
+    my $query2 = qq|UPDATE $table SET position = ? WHERE id = ?|;
 
     my $sth = $self->dbh->prepare($query);
+    my $sth2 = $self->dbh->prepare($query2);
     $sth->execute || $::form->dberror($query);
 
     # set new position field in order of ids, starting by one for each order
@@ -40,10 +41,10 @@ sub run {
       }
       $last_order_id = $ref->{ $order_id_col };
 
-      $query = qq|UPDATE $table SET position = ? WHERE id = ?|;
-      $self->db_query($query, bind => [ $position, $ref->{id} ]);
+      $sth2->execute($position, $ref->{id});
     }
     $sth->finish;
+    $sth2->finish;
 
 
     $query = qq|ALTER TABLE $table ALTER COLUMN position SET NOT NULL|;
diff --git a/sql/Pg-upgrade2/orderitems_optional.sql b/sql/Pg-upgrade2/orderitems_optional.sql
new file mode 100644 (file)
index 0000000..3b4ddda
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: orderitems_optional
+-- @description: Optionale Artikel im Angebot und Auftrag
+-- @depends: release_3_5_6_1
+ALTER TABLE orderitems ADD COLUMN optional BOOLEAN default FALSE;
+
diff --git a/sql/Pg-upgrade2/part_classification_report_separate.sql b/sql/Pg-upgrade2/part_classification_report_separate.sql
new file mode 100644 (file)
index 0000000..9e0260e
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: part_classification_report_separate
+-- @description: "Artikelklassifikation mit weiterer boolschen Variable für separat ausweisen"
+-- @depends: part_classifications
+ALTER TABLE part_classifications ADD COLUMN report_separate BOOLEAN DEFAULT 'f' NOT NULL;
diff --git a/sql/Pg-upgrade2/part_classifications.sql b/sql/Pg-upgrade2/part_classifications.sql
new file mode 100644 (file)
index 0000000..5dc7af8
--- /dev/null
@@ -0,0 +1,19 @@
+-- @tag: part_classifications
+-- @description: "zusätzliche Tabelle mit Flags zur Klassifizierung von Artikeln"
+-- @depends: release_3_4_1
+CREATE TABLE part_classifications (
+    id SERIAL PRIMARY KEY,
+    description text,
+    abbreviation text,
+    used_for_purchase BOOLEAN DEFAULT 't' NOT NULL,
+    used_for_sale     BOOLEAN DEFAULT 't' NOT NULL
+);
+
+INSERT INTO part_classifications values(0,'-------'    ,'None (typeabbreviation)','t','t');
+INSERT INTO part_classifications values(1,'Purchase'   ,'Purchase (typeabbreviation)'   ,'t','f');
+INSERT INTO part_classifications values(2,'Sales'      ,'Sales (typeabbreviation)'      ,'f','t');
+INSERT INTO part_classifications values(3,'Merchandise','Merchandise (typeabbreviation)','t','t');
+INSERT INTO part_classifications values(4,'Production' ,'Production (typeabbreviation)' ,'f','t');
+SELECT setval('part_classifications_id_seq',4);
+ALTER TABLE parts ADD COLUMN classification_id integer DEFAULT 0;
+ALTER TABLE parts ADD CONSTRAINT part_classification_id_fkey FOREIGN KEY (classification_id) REFERENCES part_classifications(id);
diff --git a/sql/Pg-upgrade2/part_type_enum.sql b/sql/Pg-upgrade2/part_type_enum.sql
new file mode 100644 (file)
index 0000000..e92300f
--- /dev/null
@@ -0,0 +1,32 @@
+-- @tag: part_type_enum
+-- @description: enums
+-- @depends: release_3_4_1
+
+CREATE TYPE part_type_enum AS ENUM ('part', 'service', 'assembly', 'assortment');
+ALTER TABLE parts ADD COLUMN part_type part_type_enum;
+
+UPDATE parts SET part_type = 'assembly' WHERE assembly IS TRUE;
+UPDATE parts SET part_type = 'service'  WHERE inventory_accno_id IS NULL and part_type IS NULL;
+UPDATE parts SET part_type = 'part'     WHERE assembly IS FALSE AND inventory_accno_id IS NOT NULL AND part_type IS NULL;
+
+-- don't set a default for now to help with finding bugs where no part_type is passed
+ALTER TABLE parts ALTER COLUMN part_type SET NOT NULL;
+
+CREATE OR REPLACE FUNCTION update_purchase_price() RETURNS trigger AS '
+BEGIN
+  if tg_op = ''DELETE'' THEN
+    UPDATE parts SET lastcost = COALESCE((select sum ((a.qty * (p.lastcost / COALESCE(pf.factor,
+    1)))) as summe from assembly a left join parts p on (p.id = a.parts_id)
+    LEFT JOIN price_factors pf on (p.price_factor_id = pf.id) where a.id = parts.id),0)
+    WHERE part_type = ''assembly'' and id = old.id;
+    return old; -- old ist eine referenz auf die geloeschte reihe
+  ELSE
+    UPDATE parts SET lastcost = COALESCE((select sum ((a.qty * (p.lastcost / COALESCE(pf.factor,
+    1)))) as summe from assembly a left join parts p on (p.id = a.parts_id)
+    LEFT JOIN price_factors pf on (p.price_factor_id = pf.id)
+    WHERE a.id = parts.id),0) where part_type = ''assembly'' and id = new.id;
+    return new; -- entsprechend new, wird wahrscheinlich benoetigt, um den korrekten Eintrag
+                -- zu filtern bzw. dann zu aktualisieren
+  END IF;
+END;
+' LANGUAGE plpgsql;
diff --git a/sql/Pg-upgrade2/parts_remove_unneeded_fields.sql b/sql/Pg-upgrade2/parts_remove_unneeded_fields.sql
new file mode 100644 (file)
index 0000000..fa3a672
--- /dev/null
@@ -0,0 +1,10 @@
+-- @tag: part_remove_unneeded_fields
+-- @description: Removing colums assembly, inventory_accno_id, expense_accno_id, income_accno_id
+-- @depends: part_type_enum
+
+ALTER TABLE parts DROP COLUMN assembly;
+ALTER TABLE parts DROP COLUMN inventory_accno_id;
+ALTER TABLE parts DROP COLUMN expense_accno_id;
+ALTER TABLE parts DROP COLUMN income_accno_id;
+-- keep for now:
+-- ALTER TABLE parts DROP COLUMN makemodel;
diff --git a/sql/Pg-upgrade2/partsgroup_sortkey_obsolete.sql b/sql/Pg-upgrade2/partsgroup_sortkey_obsolete.sql
new file mode 100644 (file)
index 0000000..b148d61
--- /dev/null
@@ -0,0 +1,12 @@
+-- @tag: partsgroup_sortkey_obsolete
+-- @description: Sortierreihenfolge und ungültig für Warengruppen
+-- @depends: release_3_4_1
+-- @ignore: 0
+
+ALTER TABLE partsgroup ADD COLUMN obsolete BOOLEAN DEFAULT FALSE;
+ALTER TABLE partsgroup ADD COLUMN sortkey INTEGER;
+
+CREATE SEQUENCE tmp_counter;
+UPDATE partsgroup SET sortkey = nextval('tmp_counter');
+DROP SEQUENCE tmp_counter;
+ALTER TABLE partsgroup ALTER COLUMN sortkey SET NOT NULL;
diff --git a/sql/Pg-upgrade2/payment_terms_for_invoices.sql b/sql/Pg-upgrade2/payment_terms_for_invoices.sql
new file mode 100644 (file)
index 0000000..6b6653d
--- /dev/null
@@ -0,0 +1,20 @@
+-- @tag: payment_terms_for_invoices
+-- @description: Unterscheidung in Zahlungsbedingungen für Angebote/Aufträge und Rechnungen
+-- @depends: release_3_4_0
+ALTER TABLE payment_terms ADD COLUMN description_long_invoice TEXT;
+UPDATE payment_terms SET description_long_invoice = description_long;
+
+INSERT INTO generic_translations (translation_type, language_id, translation_id, translation)
+SELECT translation_type || '_invoice', language_id, translation_id, translation
+FROM generic_translations
+WHERE translation_type = 'SL::DB::PaymentTerm/description_long';
+
+CREATE OR REPLACE FUNCTION generic_translations_delete_on_payment_terms_delete_trigger()
+RETURNS TRIGGER AS $$
+  BEGIN
+    DELETE FROM generic_translations
+    WHERE (translation_id = OLD.id)
+      AND (translation_type IN ('SL::DB::PaymentTerm/description_long', 'SL::DB::PaymentTerm/description_long_invoice'));
+    RETURN OLD;
+  END;
+$$ LANGUAGE plpgsql;
diff --git a/sql/Pg-upgrade2/payment_terms_obsolete.sql b/sql/Pg-upgrade2/payment_terms_obsolete.sql
new file mode 100644 (file)
index 0000000..90a7740
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: payment_terms_obsolete
+-- @description: Zahlungsbedingungen ungültig setzen
+-- @depends: release_3_4_1
+-- @ignore: 0
+
+ALTER TABLE payment_terms ADD COLUMN obsolete BOOLEAN DEFAULT FALSE;
diff --git a/sql/Pg-upgrade2/periodic_invoices_background_job.pl b/sql/Pg-upgrade2/periodic_invoices_background_job.pl
deleted file mode 100644 (file)
index 91b3a61..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-# @tag: periodic_invoices_background_job
-# @description: Hintergrundjob zum Erzeugen wiederkehrender Rechnungen
-# @depends: periodic_invoices
-package SL::DBUpgrade2::periodic_invoices_background_job;
-
-use strict;
-use utf8;
-
-use parent qw(SL::DBUpgrade2::Base);
-
-use SL::BackgroundJob::CreatePeriodicInvoices;
-
-sub run {
-  SL::BackgroundJob::CreatePeriodicInvoices->create_job;
-  return 1;
-}
-
-1;
diff --git a/sql/Pg-upgrade2/periodic_invoices_background_job.sql b/sql/Pg-upgrade2/periodic_invoices_background_job.sql
new file mode 100644 (file)
index 0000000..74f21af
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: periodic_invoices_background_job
+-- @description: Hintergrundjob zum Erzeugen wiederkehrender Rechnungen
+-- @depends: periodic_invoices
+INSERT INTO background_jobs (type, package_name, active, cron_spec, next_run_at)
+VALUES ('interval', 'CreatePeriodicInvoices', true, '0 3 1 * *',
+        date_trunc('month', current_date) + CAST('1 month 3 hours' AS interval));
index 8487982..ffabb9d 100644 (file)
@@ -1,5 +1,4 @@
 -- @tag: periodic_invoices_first_billing_date
 -- @description: Wiederkehrende Rechnungen: Feld für erstes Rechnungsdatum
 -- @depends: periodic_invoices
--- @charset: utf-8
 ALTER TABLE periodic_invoices_configs ADD COLUMN first_billing_date DATE;
diff --git a/sql/Pg-upgrade2/periodic_invoices_order_value_periodicity2.sql b/sql/Pg-upgrade2/periodic_invoices_order_value_periodicity2.sql
new file mode 100644 (file)
index 0000000..6fd38c0
--- /dev/null
@@ -0,0 +1,11 @@
+-- @tag: periodic_invoices_order_value_periodicity2
+-- @description:periodic_invoices_configs_valid_periodicity Wiederkehrende Rechnungen: Einstellung für Periode, auf die sich der Auftragswert bezieht
+-- @depends: release_3_4_1 periodic_invoices_order_value_periodicity
+
+-- Spalte »periodicity«: Erweiterung um Periode o (one time). Einmalige Ausführung
+ALTER TABLE periodic_invoices_configs
+DROP CONSTRAINT periodic_invoices_configs_valid_periodicity;
+
+ALTER TABLE periodic_invoices_configs
+ADD CONSTRAINT periodic_invoices_configs_valid_periodicity
+CHECK (periodicity IN ('o', 'm', 'q', 'b', 'y'));
diff --git a/sql/Pg-upgrade2/periodic_invoices_send_email.sql b/sql/Pg-upgrade2/periodic_invoices_send_email.sql
new file mode 100644 (file)
index 0000000..91eab25
--- /dev/null
@@ -0,0 +1,18 @@
+-- @tag: periodic_invoices_send_email
+-- @description: Wiederkehrende Rechnungen automatisch per E-Mail verschicken
+-- @depends: release_3_4_0
+ALTER TABLE periodic_invoices_configs ADD COLUMN send_email                 BOOLEAN;
+ALTER TABLE periodic_invoices_configs ADD COLUMN email_recipient_contact_id INTEGER;
+ALTER TABLE periodic_invoices_configs ADD COLUMN email_recipient_address    TEXT;
+ALTER TABLE periodic_invoices_configs ADD COLUMN email_sender               TEXT;
+ALTER TABLE periodic_invoices_configs ADD COLUMN email_subject              TEXT;
+ALTER TABLE periodic_invoices_configs ADD COLUMN email_body                 TEXT;
+
+UPDATE periodic_invoices_configs SET send_email = FALSE;
+
+ALTER TABLE periodic_invoices_configs ALTER COLUMN send_email SET DEFAULT FALSE;
+ALTER TABLE periodic_invoices_configs ALTER COLUMN send_email SET NOT NULL;
+
+ALTER TABLE periodic_invoices_configs
+ADD FOREIGN KEY (email_recipient_contact_id) REFERENCES contacts (cp_id)
+ON DELETE SET NULL;
index 3f62fd2..63d3834 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: price_rules
 -- @description:  Preismatrix Tabellen
 -- @depends: release_3_1_0
--- @encoding: utf-8
 
 CREATE TABLE price_rules (
   id       SERIAL PRIMARY KEY,
index 7bcac1a..affba47 100644 (file)
@@ -1,6 +1,5 @@
 -- @tag: price_source_client_config
 -- @description: Preisquellen: Preisquellen ausschaltbar per Mandant
 -- @depends: release_3_1_0
--- @encoding: utf-8
 
 ALTER TABLE defaults ADD disabled_price_sources TEXT[];
diff --git a/sql/Pg-upgrade2/pricegroup_sortkey_obsolete.sql b/sql/Pg-upgrade2/pricegroup_sortkey_obsolete.sql
new file mode 100644 (file)
index 0000000..684bf37
--- /dev/null
@@ -0,0 +1,12 @@
+-- @tag: pricegroup_sortkey_obsolete
+-- @description: Sortierreihenfolge und ungültig für Preisgruppen
+-- @depends: release_3_4_1
+-- @ignore: 0
+
+ALTER TABLE pricegroup ADD COLUMN obsolete BOOLEAN DEFAULT FALSE;
+ALTER TABLE pricegroup ADD COLUMN sortkey INTEGER;
+
+CREATE SEQUENCE tmp_counter;
+UPDATE pricegroup SET sortkey = nextval('tmp_counter');
+DROP SEQUENCE tmp_counter;
+ALTER TABLE pricegroup ALTER COLUMN sortkey SET NOT NULL;
diff --git a/sql/Pg-upgrade2/prices_delete_cascade.pl b/sql/Pg-upgrade2/prices_delete_cascade.pl
new file mode 100644 (file)
index 0000000..4180348
--- /dev/null
@@ -0,0 +1,30 @@
+# @tag: prices_delete_cascade
+# @description: Preisgruppenpreise Löschen wenn Artikel gelöscht wird
+# @depends: release_3_4_1
+
+# delete price entries if part is deleted
+
+package SL::DBUpgrade2::prices_delete_cascade;
+
+use utf8;
+use strict;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+sub run {
+  my ($self) = @_;
+
+  $self->drop_constraints(table => "prices");
+
+  my $query = <<SQL;
+    ALTER TABLE prices
+    ADD CONSTRAINT prices_pricegroup_id_fkey FOREIGN KEY (pricegroup_id) REFERENCES pricegroup(id) ON DELETE CASCADE,
+    ADD CONSTRAINT prices_parts_id_fkey      FOREIGN KEY (parts_id)      REFERENCES parts(id)      ON DELETE CASCADE
+SQL
+
+  $self->db_query($query);
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/prices_unique.sql b/sql/Pg-upgrade2/prices_unique.sql
new file mode 100644 (file)
index 0000000..f086283
--- /dev/null
@@ -0,0 +1,9 @@
+-- @tag: prices_unique
+-- @description: DB-Constraint - nur ein Preis pro Artikel pro Preisgruppe
+-- @depends: release_3_4_1
+
+-- it would be easier to just have a composite primary key on parts_id and
+-- pricegroup_id, but that would need some code refactoring
+ALTER TABLE prices ADD CONSTRAINT parts_id_pricegroup_id_unique UNIQUE (parts_id, pricegroup_id);
+ALTER TABLE prices ALTER COLUMN parts_id SET NOT NULL;
+ALTER TABLE prices ALTER COLUMN pricegroup_id SET NOT NULL;
diff --git a/sql/Pg-upgrade2/project_defaults.sql b/sql/Pg-upgrade2/project_defaults.sql
new file mode 100644 (file)
index 0000000..f03ca93
--- /dev/null
@@ -0,0 +1,9 @@
+-- @tag: add_project_defaults
+-- @description: Standardprojekttyp und Standardprojectstatus
+-- @depends: release_3_3_0
+ALTER TABLE defaults ADD COLUMN order_always_project boolean DEFAULT false;
+ALTER TABLE defaults ADD COLUMN project_status_id integer;
+ALTER TABLE defaults ADD COLUMN project_type_id integer;
+ALTER TABLE defaults ADD FOREIGN KEY (project_status_id) REFERENCES project_statuses (id);
+ALTER TABLE defaults ADD FOREIGN KEY (project_type_id) REFERENCES project_types (id);
+
diff --git a/sql/Pg-upgrade2/project_mtime_trigger.sql b/sql/Pg-upgrade2/project_mtime_trigger.sql
new file mode 100644 (file)
index 0000000..a2110f7
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: project_mtime_trigger
+-- @description: mtime-Trigger für Tabelle project hinzufügen.
+-- @depends: release_3_3_0
+
+CREATE TRIGGER mtime_project BEFORE UPDATE ON project FOR EACH ROW EXECUTE PROCEDURE set_mtime();
diff --git a/sql/Pg-upgrade2/re_add_sepa_export_items_foreign_keys.sql b/sql/Pg-upgrade2/re_add_sepa_export_items_foreign_keys.sql
new file mode 100644 (file)
index 0000000..83f605d
--- /dev/null
@@ -0,0 +1,13 @@
+-- @tag: re_add_sepa_export_items_foreign_keys
+-- @description: Versehentlich gelöschte Fremdschlüssel in sepa_export_items wieder hinzufügen
+-- @depends: auto_delete_sepa_export_items_on_ap_ar_deletion
+ALTER TABLE sepa_export_items
+  DROP CONSTRAINT IF EXISTS sepa_export_items_chart_id_fkey,
+  ADD CONSTRAINT sepa_export_items_chart_id_fkey
+    FOREIGN KEY (chart_id) REFERENCES chart (id);
+
+ALTER TABLE sepa_export_items
+  DROP CONSTRAINT IF EXISTS sepa_export_items_sepa_export_id_fkey,
+  ADD CONSTRAINT sepa_export_items_sepa_export_id_fkey
+    FOREIGN KEY (sepa_export_id) REFERENCES sepa_export (id)
+    ON DELETE CASCADE;
diff --git a/sql/Pg-upgrade2/receivable_payable_default_accounts.sql b/sql/Pg-upgrade2/receivable_payable_default_accounts.sql
new file mode 100644 (file)
index 0000000..b1346ac
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: ar_ap_default
+-- @description: Standardkonten für Forderungen und Verbindlichkeiten
+-- @depends: release_3_2_0
+ALTER TABLE defaults ADD COLUMN ap_chart_id integer;
+ALTER TABLE defaults ADD FOREIGN KEY (ap_chart_id) REFERENCES chart (id);
+ALTER TABLE defaults ADD COLUMN ar_chart_id integer;
+ALTER TABLE defaults ADD FOREIGN KEY (ar_chart_id) REFERENCES chart (id);
diff --git a/sql/Pg-upgrade2/record_links_bt_acc_trans.pl b/sql/Pg-upgrade2/record_links_bt_acc_trans.pl
new file mode 100644 (file)
index 0000000..e84155b
--- /dev/null
@@ -0,0 +1,84 @@
+# @tag: record_links_bt_acc_trans
+# @description: RecordLinks von bt nach acc_trans
+# @depends: release_3_5_3
+package SL::DBUpgrade2::record_links_bt_acc_trans;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::DBUtils;
+use SL::RecordLinks;
+
+
+sub run {
+  my ($self) = @_;
+
+  my $query_table =
+    qq|
+
+      CREATE SEQUENCE bank_transaction_acc_trans_id_seq;
+      CREATE TABLE bank_transaction_acc_trans (
+        id                      integer NOT NULL DEFAULT nextval('bank_transaction_acc_trans_id_seq'),
+        bank_transaction_id     integer NOT NULL,
+        acc_trans_id            bigint  NOT NULL,
+        ar_id                   integer,
+        ap_id                   integer,
+        gl_id                   integer,
+        itime                   TIMESTAMP      DEFAULT now(),
+        mtime                   TIMESTAMP,
+        PRIMARY KEY (bank_transaction_id, acc_trans_id),
+        FOREIGN KEY (bank_transaction_id)      REFERENCES bank_transactions (id),
+        FOREIGN KEY (acc_trans_id)             REFERENCES acc_trans (acc_trans_id),
+        FOREIGN KEY (ar_id)                    REFERENCES ar (id),
+        FOREIGN KEY (ap_id)                    REFERENCES ap (id),
+        FOREIGN KEY (gl_id)                    REFERENCES gl (id));|;
+
+  $self->db_query($query_table);
+
+
+  my $query = qq|SELECT to_id, itime, from_id, to_table
+                 FROM record_links
+                 WHERE from_table='bank_transactions'|;
+
+  my $sth = $self->dbh->prepare($query);
+
+  my $sql       = <<SQL;
+    SELECT
+     acc_trans_id
+    FROM acc_trans
+    WHERE trans_id = ?
+    AND itime = ?
+    AND (chart_link='AR' OR chart_link='AP' OR chart_link ilike '%paid%');
+SQL
+
+  my $sth_acc_trans_ids = $self->dbh->prepare($sql) or die $self->dbh->errstr;
+
+  my $sql_insert       = <<SQL;
+  INSERT INTO bank_transaction_acc_trans (bank_transaction_id, acc_trans_id, ar_id, ap_id, gl_id)
+  VALUES ( ?, ?, ?, ?, ?);
+SQL
+
+  my $sth_insert = $self->dbh->prepare($sql_insert) or die $self->dbh->errstr;
+
+
+  # get all current record links from bank to arap
+  $sth->execute() or die $self->dbh->errstr;
+
+  while (my $rl_ref = $sth->fetchrow_hashref("NAME_lc")) {
+
+    # get all concurrent acc_trans entries (payment) for this transaction
+    $sth_acc_trans_ids->execute($rl_ref->{to_id}, $rl_ref->{itime}) or die $self->dbh->errstr;
+    while (my $ac_ref = $sth_acc_trans_ids->fetchrow_hashref("NAME_lc")) {
+      my $ar_id = $rl_ref->{to_table} eq 'ar' ? $rl_ref->{to_id} : undef;
+      my $ap_id = $rl_ref->{to_table} eq 'ap' ? $rl_ref->{to_id} : undef;
+      my $gl_id = $rl_ref->{to_table} eq 'gl' ? $rl_ref->{to_id} : undef;
+      $sth_insert->execute($rl_ref->{from_id},$ac_ref->{acc_trans_id},
+                           $ar_id, $ap_id, $gl_id) or die $self->dbh->errstr;
+    }
+  }
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/record_links_dunning_post_delete_trigger.sql b/sql/Pg-upgrade2/record_links_dunning_post_delete_trigger.sql
new file mode 100644 (file)
index 0000000..f7a097e
--- /dev/null
@@ -0,0 +1,21 @@
+-- @tag: record_links_dunning_post_delete_trigger
+-- @description: Verknüpfte Belege für Mahnungen beim Löschen entfernen
+-- @depends: release_3_5_6_1
+
+-- clean up old dangling links
+DELETE FROM record_links WHERE from_table = 'dunning' AND NOT EXISTS (SELECT id FROM dunning WHERE id = from_id);
+DELETE FROM record_links WHERE to_table   = 'dunning' AND NOT EXISTS (SELECT id FROM dunning WHERE id = to_id);
+
+-- install a trigger to delete links on delete
+CREATE OR REPLACE FUNCTION clean_up_record_links_before_dunning_delete() RETURNS trigger AS $$
+  BEGIN
+    DELETE FROM record_links
+      WHERE (from_table = 'dunning' AND from_id = OLD.id)
+         OR (to_table   = 'dunning' AND to_id   = OLD.id);
+    RETURN OLD;
+  END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER before_delete_dunning_trigger
+BEFORE DELETE ON dunning FOR EACH ROW EXECUTE
+PROCEDURE clean_up_record_links_before_dunning_delete();
diff --git a/sql/Pg-upgrade2/record_links_post_delete_triggers_gl.sql b/sql/Pg-upgrade2/record_links_post_delete_triggers_gl.sql
new file mode 100644 (file)
index 0000000..595c8ca
--- /dev/null
@@ -0,0 +1,44 @@
+-- @tag: record_links_post_delete_triggers_gl2
+-- @description: Datenbankkonsistenz record_links nach Löschen von Dialogbuchungen und Briefen
+-- @depends: release_3_5_3
+
+-- When deleting records record_links weren't cleaned up until now
+-- This wasn't really a problem apart from the fact that record_links slowly grew
+-- but deleting records was seldom enough to not matter
+-- Unfortunately delivery_plan decides if an order need to be displayed by the
+-- number of record_links, which generates false negatives.
+-- so, first clean up the database, and after that create triggers to
+-- clean up automatically
+
+DELETE FROM record_links WHERE from_table = 'letter' AND from_id NOT IN (SELECT id FROM letter);
+DELETE FROM record_links WHERE to_table   = 'letter' AND to_id   NOT IN (SELECT id FROM letter);
+
+DELETE FROM record_links WHERE from_table = 'gl' AND from_id NOT IN (SELECT id FROM gl);
+DELETE FROM record_links WHERE to_table   = 'gl' AND to_id   NOT IN (SELECT id FROM gl);
+
+CREATE OR REPLACE FUNCTION clean_up_record_links_before_letter_delete() RETURNS trigger AS $$
+  BEGIN
+    DELETE FROM record_links
+      WHERE (from_table = 'letter' AND from_id = OLD.id)
+         OR (to_table   = 'letter' AND to_id   = OLD.id);
+    RETURN OLD;
+  END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION clean_up_record_links_before_gl_delete() RETURNS trigger AS $$
+  BEGIN
+    DELETE FROM record_links
+      WHERE (from_table = 'gl' AND from_id = OLD.id)
+         OR (to_table   = 'gl' AND to_id   = OLD.id);
+    RETURN OLD;
+  END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE TRIGGER before_delete_gl_trigger
+BEFORE DELETE ON gl FOR EACH ROW EXECUTE
+PROCEDURE clean_up_record_links_before_gl_delete();
+
+CREATE TRIGGER before_delete_letter_trigger
+BEFORE DELETE ON letter FOR EACH ROW EXECUTE
+PROCEDURE clean_up_record_links_before_letter_delete();
diff --git a/sql/Pg-upgrade2/record_links_remove_to_quotation.pl b/sql/Pg-upgrade2/record_links_remove_to_quotation.pl
new file mode 100644 (file)
index 0000000..e926006
--- /dev/null
@@ -0,0 +1,51 @@
+# @tag: record_links_remove_to_quotation
+# @description: Verknüpfte Positionen mit Ziel Angebot und dazugehörige Belegverknüpfung entfernen, wenn Quelle Angebot oder Auftrag.
+# @depends: release_3_6_0
+package SL::DBUpgrade2::record_links_remove_to_quotation;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::DBUtils;
+
+sub run {
+  my ($self) = @_;
+
+  my $query = qq|SELECT record_links.id AS rl_id, from_oe.id AS from_oe_id, to_oe.id AS to_oe_id FROM record_links
+                   LEFT JOIN orderitems from_oi ON (from_oi.id = from_id)
+                   LEFT JOIN orderitems to_oi   ON (to_oi.id   = to_id)
+                   LEFT JOIN oe         from_oe ON (from_oe.id = from_oi.trans_id)
+                   LEFT JOIN oe         to_oe   ON (to_oe.id   = to_oi.trans_id)
+                 WHERE from_table = 'orderitems'
+                   AND to_table   = 'orderitems'
+                   AND to_oe.quotation IS TRUE|;
+
+  my $refs = selectall_hashref_query($::form, $self->dbh, $query);
+
+  my $query_delete_oi_links = qq|
+    DELETE FROM record_links WHERE id = ?;
+  |;
+  my $sth_delete_oi_links = $self->dbh->prepare($query_delete_oi_links);
+
+  my $query_delete_oe_links = qq|
+    DELETE FROM record_links WHERE from_table = 'oe' AND to_table = 'oe' AND from_id = ? AND to_id = ?;
+  |;
+  my $sth_delete_oe_links = $self->dbh->prepare($query_delete_oe_links);
+
+  my %oe_links;
+  foreach my $ref (@$refs) {
+    $sth_delete_oi_links->execute($ref->{rl_id}) || $::form->dberror($query_delete_oi_links);
+    $oe_links{$ref->{from_oe_id} . ':' . $ref->{to_oe_id}} = 1;
+  }
+
+  for my $from_to (keys %oe_links) {
+    my ($from_oe_id, $to_oe_id) = split ':', $from_to;
+    $sth_delete_oe_links->execute($from_oe_id, $to_oe_id) || $::form->dberror($query_delete_oe_links);
+  }
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/record_template_payment_id.sql b/sql/Pg-upgrade2/record_template_payment_id.sql
new file mode 100644 (file)
index 0000000..e251e3d
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: record_template_payment_id
+-- @description: Zahlungsbedingungen in Vorlagen in der Finanzbuchhaltung
+-- @depends: release_3_5_6_1
+
+ALTER TABLE record_templates ADD COLUMN payment_id INTEGER REFERENCES payment_terms(id);
index aea966a..acb713c 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: recorditem_active_record_source
 -- @description: Preisquellen: Rabatte
 -- @depends: release_3_1_0 recorditem_active_price_source
--- @encoding: utf-8
 
 ALTER TABLE orderitems           ADD COLUMN active_discount_source TEXT NOT NULL DEFAULT '';
 ALTER TABLE delivery_order_items ADD COLUMN active_discount_source TEXT NOT NULL DEFAULT '';
index e8f502b..4b2c5db 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: recorditem_active_price_source
 -- @description: Preisquelle in Belegpositionen
 -- @depends: release_3_1_0
--- @encoding: utf-8
 
 ALTER TABLE orderitems           ADD COLUMN active_price_source TEXT NOT NULL DEFAULT '';
 ALTER TABLE delivery_order_items ADD COLUMN active_price_source TEXT NOT NULL DEFAULT '';
diff --git a/sql/Pg-upgrade2/release_3_4_0.sql b/sql/Pg-upgrade2/release_3_4_0.sql
new file mode 100644 (file)
index 0000000..bcaeede
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_4_0
+-- @description: Leeres Script, das alle Upgradescripte bis zum Release 3.4.0 voraussetzt, um ein fest definiertes Schema zu definieren.
+-- @depends: release_3_3_0 remove_index defaults_drop_delivery_plan_calculate_transferred_do use_html_in_letter first_aggregator requirement_spec_parts_foreign_key_cascade defaults_show_longdescription_select_item defaults_enable_email_journal defaults_add_rnd_accno_ids sepa_contained_in_message_ids buchungsgruppen_forein_keys customer_vendor_shipto_add_gln ar_ap_default periodic_invoices_direct_debit_flag defaults_order_warn_duplicate_parts chart_pos_er add_project_defaults project_mtime_trigger defaults_add_precision
diff --git a/sql/Pg-upgrade2/release_3_4_1.sql b/sql/Pg-upgrade2/release_3_4_1.sql
new file mode 100644 (file)
index 0000000..903febb
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: release_3_4_1
+-- @description: Leeres Script, das alle Upgradescripte bis zum Release 3.4.1 voraussetzt, um ein fest definiertes Schema zu definieren.
+-- @depends: release_3_4_0 add_parts_price_history2 defaults_add_features periodic_invoices_send_email payment_terms_for_invoices delete_from_generic_translations_on_language_deletion inventory_fix_shippingdate_assemblies letter_cleanup defaults_add_quick_search_modules
+
diff --git a/sql/Pg-upgrade2/release_3_5_0.sql b/sql/Pg-upgrade2/release_3_5_0.sql
new file mode 100644 (file)
index 0000000..402fac5
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_5_0
+-- @description: Leeres Script, das alle Upgradescripte bis zum Release 3.5.0 voraussetzt, um ein fest definiertes Schema zu definieren.
+-- @depends: release_3_4_1 part_remove_unneeded_fields makemodel_add_vendor_foreign_key bank_transactions_type2 defaults_add_feature_experimental create_part_if_not_found payment_terms_obsolete defaults_bcc_to_login assortment_charge requirement_spec_items_price_factor defaults_add_finanzamt_data periodic_invoices_order_value_periodicity2 partsgroup_sortkey_obsolete part_classification_report_separate assembly_parts_foreign_key2 add_warehouse_for_assembly convert_drafts_to_record_templates eur_bwa_category_views inventory_shippingdate_not_null remove_alternate_from_parts csv_mt940_add_profile email_journal_attachments_add_fileid auto_delete_reconciliation_links_on_acc_trans_deletion assembly_position user_preferences sepa_reference_add_vc_vc_id customer_klass_rename_to_pricegroup_id_and_foreign_key pricegroup_sortkey_obsolete letter_vendorletter re_add_sepa_export_items_foreign_keys add_test_mode_to_csv_import_report sepa_export_items prices_unique prices_delete_cascade
diff --git a/sql/Pg-upgrade2/release_3_5_1.sql b/sql/Pg-upgrade2/release_3_5_1.sql
new file mode 100644 (file)
index 0000000..3240d9b
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: release_3_5_1
+-- @description: Leeres Script, das alle Upgradescripte bis zum Release 3.5.0 voraussetzt, um ein fest definiertes Schema zu definieren.
+-- @depends: release_3_5_0 shop_3 shop_2 trigram_indices shop_1 trigram_indices_webshop get_shipped_qty_config shop_orders_update_3 shopimages_2 customer_orderlock alter_record_template_tables shopimages_3
+
diff --git a/sql/Pg-upgrade2/release_3_5_2.sql b/sql/Pg-upgrade2/release_3_5_2.sql
new file mode 100644 (file)
index 0000000..d81c4a0
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_5_2
+-- @description: Leeres Script, das alle Upgradescripte bis zum Release 3.5.2 voraussetzt, um ein fest definiertes Schema zu definieren.
+-- @depends: release_3_5_1 custom_data_export_default_values_for_parameters transfer_type_stocktaking add_stocktaking_preselects_client_config_default stocktakings create_part_customerprices
diff --git a/sql/Pg-upgrade2/release_3_5_3.sql b/sql/Pg-upgrade2/release_3_5_3.sql
new file mode 100644 (file)
index 0000000..2a04f68
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: release_3_5_3
+-- @description: Leeres Script, das alle Upgradescripte bis zum Release 3.5.3 voraussetzt, um ein fest definiertes Schema zu definieren.
+-- @depends: release_3_5_2 datev_export_format accounts_tax_office_leonberg defaults_filemanagement_remove_doc_database sepa_recommended_execution_date add_stocktaking_qty_threshold_client_config_default defaults_order_warn_no_deliverydate defaults_add_feature_experimental2
+
diff --git a/sql/Pg-upgrade2/release_3_5_4.sql b/sql/Pg-upgrade2/release_3_5_4.sql
new file mode 100644 (file)
index 0000000..43ca9ee
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_5_4
+-- @description: Leeres Script, das alle Upgradescripte bis zum Release 3.5.4 voraussetzt, um ein fest definiertes Schema zu definieren.
+-- @depends: release_3_5_3 defaults_delivery_date_interval add_emloyee_project_assignment_for_viewing_invoices displayable_name_prefs_defaults customer_add_commercial_court record_links_bt_acc_trans record_links_post_delete_triggers_gl2 contacts_add_main_contact defaults_set_dunning_creator customer_add_generic_mail_delivery drop_payment_terms_ranking bank_transactions_check_constraint_invoice_amount defaults_invoice_mail_priority dunning_foreign_key_for_trans_id defaults_doc_email_attachment customer_add_fields
diff --git a/sql/Pg-upgrade2/release_3_5_5.sql b/sql/Pg-upgrade2/release_3_5_5.sql
new file mode 100644 (file)
index 0000000..aed2568
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_5_5
+-- @description: Leeres Script, das alle Upgradescripte bis zum Release 3.5.5 voraussetzt, um ein fest definiertes Schema zu definieren.
+-- @depends: release_3_5_4 bank_transaction_acc_trans_remove_wrong_primary_key inventory_itime_parts_id_index add_node_id_to_background_jobs tax_removed_taxnumber bank_transactions_nuke_trailing_spaces_in_purpose remove_comma_aggregate_functions inventory_parts_id_index defaults_workflow_po_ap_chart_id defaults_year_end_charts
diff --git a/sql/Pg-upgrade2/release_3_5_6.sql b/sql/Pg-upgrade2/release_3_5_6.sql
new file mode 100644 (file)
index 0000000..2324069
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_5_6
+-- @description: Leeres Script, das alle Upgradescripte bis zum Release 3.5.6 voraussetzt, um ein fest definiertes Schema zu definieren.
+-- @depends: release_3_5_5 dunning_config_print_original_invoice customer_vendor_add_natural_person bank_account_flag_for_zugferd_usage defaults_contact_departments_use_textfield konjunkturpaket_2020 contact_departments_own_table defaults_vc_greetings_use_textfield greetings_own_table defaults_contact_titles_use_textfield contact_titles_own_table remove_taxkey_15_17_skr04 defaults_zugferd_test_mode defaults_split_address gl_add_deliverydate customer_create_zugferd_invoices
diff --git a/sql/Pg-upgrade2/release_3_5_6_1.sql b/sql/Pg-upgrade2/release_3_5_6_1.sql
new file mode 100644 (file)
index 0000000..191c211
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_5_6_1
+-- @description: Leeres Script, das alle Upgradescripte bis zum Release 3.5.6.1 voraussetzt, um ein fest definiertes Schema zu definieren.
+-- @depends: release_3_5_6 konjunkturpaket_2020_SKR04-korrekturen exchangerate_in_oe ap_set_payment_term_from_vendor konjunkturpaket_2020_SKR03-korrekturen alter_default_shipped_qty_config delete_cvars_on_trans_deletion_add_shipto transfer_out_serial_charge_number
diff --git a/sql/Pg-upgrade2/release_3_5_7.sql b/sql/Pg-upgrade2/release_3_5_7.sql
new file mode 100644 (file)
index 0000000..aa0baaf
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_5_7
+-- @description: Leeres Script, das alle Upgradescripte bis zum Release 3.5.7 voraussetzt, um ein fest definiertes Schema zu definieren.
+-- @depends: release_3_5_6_1 record_links_dunning_post_delete_trigger customer_vendor_routing_id dunning_original_invoice_printed tax_point2 custom_variables_add_edit_position defaults_req_delivery_date add_transfer_doc_interval defaults_delivery_orders_check_stocked cvars_remove_duplicate_entries time_recordings_articles record_template_payment_id defaults_customer_vendor_ustid_taxnummer_unique shop_4 defaults_posting_records_default_false add_gl_imported file_storage_project customer_vendor_add_postal_invoice file_storage_dunning_documents time_recordings_remove_type orderitems_optional time_recordings_add_order defaults_transfer_settings
diff --git a/sql/Pg-upgrade2/release_3_5_8.sql b/sql/Pg-upgrade2/release_3_5_8.sql
new file mode 100644 (file)
index 0000000..0432c28
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_5_8
+-- @description: Leeres Script, das alle Upgradescripte bis zum Release 3.5.8 voraussetzt, um ein fest definiertes Schema zu definieren.
+-- @depends: release_3_5_7 defaults_produce_assembly_transfer_service change_warehouse_client_config_default drop_shipped_qty_config delete_warehouse_for_assembly delete_cvars_on_trans_deletion_add_shipto mebil_v1
diff --git a/sql/Pg-upgrade2/release_3_6_0.sql b/sql/Pg-upgrade2/release_3_6_0.sql
new file mode 100644 (file)
index 0000000..3ca833a
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_6_0
+-- @description: Leeres Script, das alle Upgradescripte bis zum Release 3.6.0 voraussetzt, um ein fest definiertes Schema zu definieren.
+-- @depends: release_3_5_8 defaults_advance_payment_transfer_charts link_requirement_spec_to_orders_created_from_quotations_created_from_requirement_spec deliveryorder_type defaults_print_interpolate_variables_in_positions file_storage_partial_invoices defaults_qrbill_variants defaults_sales_purchase_record_numbers_changeable ar_add_qrbill_without_amount defaults_view_record_links convert_columns_to_html_for_sending_html_emails2 defaults_partsgroup_required defaults_order_warn_no_cusordnumber files_add_variant custom_variables_convert_width_height_to_pixels deliveryorder_transnumbers shop_add_proxy bank_account_information_for_swiss_qrbill defaults_order_controller customer_remove_empty_additional_billing_addresses shop_orders_update_4 defaults_invoice_warn_no_delivery_order
diff --git a/sql/Pg-upgrade2/release_3_6_1.sql b/sql/Pg-upgrade2/release_3_6_1.sql
new file mode 100644 (file)
index 0000000..e4b43cb
--- /dev/null
@@ -0,0 +1,3 @@
+-- @tag: release_3_6_1
+-- @description: Leeres Script, das alle Upgradescripte bis zum Release 3.6.1 voraussetzt, um ein fest definiertes Schema zu definieren.
+-- @depends: release_3_6_0 tax_reverse_charge_key_19 delete_wrong_charts_for_taxkeys delete_wrong_charts_for_taxkeys_04 defaults_invoice_prevent_browser_back file_full_texts remove_oids language_obsolete add_record_templates_transaction_description full_texts_background_job tax_reverse_charge_key_18 record_links_remove_to_quotation convert_real_qty ap_gl add_gl_transaction_description
diff --git a/sql/Pg-upgrade2/remove_alternate_from_parts.sql b/sql/Pg-upgrade2/remove_alternate_from_parts.sql
new file mode 100644 (file)
index 0000000..8feadb2
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: remove_alternate_from_parts
+-- @description: Veraltete Spalte »alternate« aus Tabelle »parts« entfernen
+-- @depends: release_3_4_1
+ALTER TABLE parts DROP COLUMN alternate;
diff --git a/sql/Pg-upgrade2/remove_comma_aggregate_functions.sql b/sql/Pg-upgrade2/remove_comma_aggregate_functions.sql
new file mode 100644 (file)
index 0000000..00c748d
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: remove_comma_aggregate_functions
+-- @description: Entfernt Aggregate Funktion comma
+-- @depends: release_3_5_3
+
+DROP AGGREGATE IF EXISTS comma(text);
+DROP FUNCTION IF EXISTS comma_aggregate ( text, text) ;
diff --git a/sql/Pg-upgrade2/remove_double_tax_entries_skr04.pl b/sql/Pg-upgrade2/remove_double_tax_entries_skr04.pl
new file mode 100644 (file)
index 0000000..37d35d6
--- /dev/null
@@ -0,0 +1,59 @@
+# @tag: remove_double_tax_entries_skr04
+# @description: doppelte Steuer-Einträge und alte 16% Konten für SKR04 entfernen, wenn unbebucht
+# @depends: release_3_5_5
+package SL::DBUpgrade2::remove_double_tax_entries_skr04;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::DBUtils;
+
+sub run {
+  my ($self) = @_;
+
+  if (!$self->check_coa('Germany-DATEV-SKR04EU')) {
+    return 1;
+  }
+
+  my $query = <<SQL;
+    SELECT id FROM tax WHERE chart_id = (SELECT id FROM chart WHERE accno LIKE ?) AND taxkey = ? AND rate = ? ORDER BY id;
+SQL
+
+  my $query2 = <<SQL;
+    DELETE FROM taxkeys WHERE tax_id = ?;
+SQL
+
+  my $query3 = <<SQL;
+    DELETE FROM tax WHERE id = ?;
+SQL
+
+  my @taxes_to_test = (
+    {accno => '3806', taxkey => 3, rate => 0.19},
+    {accno => '1406', taxkey => 9, rate => 0.19},
+    {accno => '3805', taxkey => 5, rate => 0.16},
+    {accno => '1405', taxkey => 7, rate => 0.16},
+
+  );
+
+  foreach my $tax_to_test (@taxes_to_test) {
+    my @entries = selectall_hashref_query($::form, $self->dbh, $query, ($tax_to_test->{accno}, $tax_to_test->{taxkey}, $tax_to_test->{rate}));
+
+    if (scalar @entries > 1) {
+      foreach my $tax (@entries) {
+        my ($num_acc_trans_entries) = $self->dbh->selectrow_array("SELECT COUNT(*) FROM acc_trans WHERE tax_id = ?", undef, $tax->{id});
+        next if $num_acc_trans_entries > 0;
+
+        $self->db_query($query2, bind => [ $tax->{id} ]);
+        $self->db_query($query3, bind => [ $tax->{id} ]);
+
+        last; # delete only one tax
+      }
+    }
+  }
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/remove_oids.sql b/sql/Pg-upgrade2/remove_oids.sql
new file mode 100644 (file)
index 0000000..9e9f439
--- /dev/null
@@ -0,0 +1,9 @@
+-- @tag: remove_oids
+-- @description: OIDs von Tabellen entfernen
+-- @depends: release_3_6_0
+ALTER TABLE assembly             SET WITHOUT OIDS;
+ALTER TABLE delivery_order_items SET WITHOUT OIDS;
+ALTER TABLE invoice              SET WITHOUT OIDS;
+ALTER TABLE orderitems           SET WITHOUT OIDS;
+ALTER TABLE parts                SET WITHOUT OIDS;
+ALTER TABLE partsgroup           SET WITHOUT OIDS;
index 39c87dc..261aa35 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: remove_redundant_customer_vendor_delete_triggers
 -- @description: Entfernt doppelte/falsche Trigger zum Aufräumen nach dem Löschen von Kunden/Lieferanten
 -- @depends: release_3_1_0
--- @encoding: utf-8
 
 -- drop triggers
 DROP TRIGGER IF EXISTS del_customer ON customer;
index dfefa1c..c7a0f53 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: remove_redundant_cvar_delete_triggers
 -- @description: Entfernt doppelte Trigger zum Löschen von benutzerdefinierten Variablen
 -- @depends: custom_variables_delete_via_trigger custom_variables_delete_via_trigger_2 delete_cvars_on_trans_deletion
--- @encoding: utf-8
 
 -- drop triggers
 DROP TRIGGER IF EXISTS delete_orderitems_dependencies           ON orderitems;
diff --git a/sql/Pg-upgrade2/remove_taxkey_15_17_skr04.sql b/sql/Pg-upgrade2/remove_taxkey_15_17_skr04.sql
new file mode 100644 (file)
index 0000000..e431c51
--- /dev/null
@@ -0,0 +1,19 @@
+-- @tag: remove_taxkey_15_17_skr04
+-- @description: Steuer mit Schlüssel 15 und 17 (16%) für SKR04 entfernen, wenn nicht verknüpft
+-- @depends: release_3_5_5
+
+DELETE FROM tax
+  WHERE (SELECT coa FROM defaults) LIKE 'Germany-DATEV-SKR04EU'
+    AND taxkey = 17
+    AND chart_id = (SELECT chart_id FROM chart WHERE accno LIKE '1403')
+    AND rate = .16
+    AND NOT EXISTS (SELECT id FROM taxkeys WHERE tax_id = tax.id)
+    AND NOT EXISTS (SELECT id FROM acc_trans WHERE tax_id = tax.id);
+
+DELETE FROM tax
+  WHERE (SELECT coa FROM defaults) LIKE 'Germany-DATEV-SKR04EU'
+    AND taxkey = 15
+    AND chart_id = (SELECT chart_id FROM chart WHERE accno LIKE '3803')
+    AND rate = .16
+    AND NOT EXISTS (SELECT id FROM taxkeys WHERE tax_id = tax.id)
+    AND NOT EXISTS (SELECT id FROM acc_trans WHERE tax_id = tax.id);
index c6c7b9f..5075653 100644 (file)
@@ -10,26 +10,6 @@ use SL::DBUtils;
 
 use parent qw(SL::DBUpgrade2::Base);
 
-sub convert_column {
-  my ($self, $table, $column) = @_;
-
-  my $sth = $self->dbh->prepare(qq|UPDATE $table SET $column = ? WHERE id = ?|) || $self->dberror;
-
-  foreach my $row (selectall_hashref_query($::form, $self->dbh, qq|SELECT id, $column FROM $table WHERE $column IS NOT NULL|)) {
-    next if !$row->{$column} || (($row->{$column} =~ m{^<[a-z]+>}) && ($row->{$column} =~ m{</[a-z]+>$}));
-
-    my $new_content = "" . $::request->presenter->escape($row->{$column});
-    $new_content    =~ s{\r}{}g;
-    $new_content    =~ s{\n\n+}{</p><p>}g;
-    $new_content    =~ s{\n}{<br />}g;
-    $new_content    =  "<p>${new_content}</p>" if $new_content;
-
-    $sth->execute($new_content, $row->{id}) if $new_content ne $row->{$column};
-  }
-
-  $sth->finish;
-}
-
 sub run {
   my ($self) = @_;
 
@@ -41,7 +21,7 @@ sub run {
     map({ ($_ => 'longdescription') } qw(translation orderitems invoice delivery_order_items)),
   );
 
-  $self->convert_column($_, $tables{$_}) for keys %tables;
+  $self->convert_column_to_html($_, $tables{$_}) for keys %tables;
 
   return 1;
 }
diff --git a/sql/Pg-upgrade2/requirement_spec_items_price_factor.sql b/sql/Pg-upgrade2/requirement_spec_items_price_factor.sql
new file mode 100644 (file)
index 0000000..b519b38
--- /dev/null
@@ -0,0 +1,9 @@
+-- @tag: requirement_spec_items_price_factor
+-- @description: Pflichtenheftabschnitte: Faktor für Verkaufspreis
+-- @depends: requirement_specs
+ALTER TABLE requirement_spec_items
+  ADD COLUMN   sellprice_factor NUMERIC(10, 5),
+  ALTER COLUMN sellprice_factor SET DEFAULT 1;
+
+UPDATE requirement_spec_items
+SET sellprice_factor = 1;
index 5c37168..25db854 100644 (file)
@@ -1,5 +1,4 @@
 -- @tag: sales_quotation_order_probability_expected_billing_date
--- @charset: utf-8
 -- @description: Weitere Felder im Angebot: Angebotswahrscheinlichkeit, voraussichtliches Abrechnungsdatum
 ALTER TABLE oe
   ADD COLUMN order_probability     INTEGER,
diff --git a/sql/Pg-upgrade2/self_test_background_job.pl b/sql/Pg-upgrade2/self_test_background_job.pl
deleted file mode 100644 (file)
index 468b79c..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-# @tag: self_test_background_job
-# @description: Hintergrundjob für tägliche Selbsttests
-# @depends: release_2_7_0
-package SL::DBUpgrade2::self_test_background_job;
-
-use strict;
-use utf8;
-
-use parent qw(SL::DBUpgrade2::Base);
-
-use SL::BackgroundJob::SelfTest;
-
-sub run {
-  SL::BackgroundJob::SelfTest->create_job;
-  return 1;
-}
-
-1;
diff --git a/sql/Pg-upgrade2/self_test_background_job.sql b/sql/Pg-upgrade2/self_test_background_job.sql
new file mode 100644 (file)
index 0000000..2d9a5e4
--- /dev/null
@@ -0,0 +1,12 @@
+-- @tag: self_test_background_job
+-- @description: Hintergrundjob für tägliche Selbsttests
+-- @depends: release_2_7_0
+INSERT INTO background_jobs (type, package_name, active, cron_spec, next_run_at)
+VALUES ('interval', 'SelfTest', true, '20 2 * * *',
+  CAST(current_date AS timestamp) + CAST(
+    (CASE
+     WHEN extract('hour' FROM current_timestamp) < 2 THEN '2 hours 20 minutes'
+     ELSE                                                 '1 day 2 hours 20 minutes'
+     END) AS interval
+  )
+);
diff --git a/sql/Pg-upgrade2/sepa_export_items.sql b/sql/Pg-upgrade2/sepa_export_items.sql
new file mode 100644 (file)
index 0000000..abed711
--- /dev/null
@@ -0,0 +1,9 @@
+-- @tag: sepa_export_items
+-- @description: sepa reference in tabelle auf den im Standard spezifizierten zulässigen Wert (140) erhöhen
+-- @depends: release_3_4_1 sepa
+ALTER TABLE sepa_export_items
+ALTER COLUMN reference TYPE varchar(140);
+
+
+
+
diff --git a/sql/Pg-upgrade2/sepa_recommended_execution_date.sql b/sql/Pg-upgrade2/sepa_recommended_execution_date.sql
new file mode 100644 (file)
index 0000000..d72b435
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: sepa_recommended_execution_date
+-- @description: Einstellung, ob das Ausführungsdatum für SEPA-Überweisung vorbelegt werden soll (Fälligkeit/Skontotermin)
+-- @depends: release_3_5_2
+
+ALTER TABLE defaults ADD COLUMN sepa_set_duedate_as_default_exec_date boolean DEFAULT FALSE;
+ALTER TABLE defaults ADD COLUMN sepa_set_skonto_date_as_default_exec_date boolean DEFAULT FALSE;
+ALTER TABLE defaults ADD COLUMN sepa_set_skonto_date_buffer_in_days integer DEFAULT 0;
diff --git a/sql/Pg-upgrade2/sepa_reference_add_vc_vc_id.sql b/sql/Pg-upgrade2/sepa_reference_add_vc_vc_id.sql
new file mode 100644 (file)
index 0000000..f454c06
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: sepa_reference_add_vc_vc_id
+-- @description: Einstellung, ob bei SEPA Überweisungen zusätzlich die Lieferanten-/Kundennummer im Verwendungszweck angezeigt wird
+-- @depends: release_3_4_1
+
+ALTER TABLE defaults ADD COLUMN sepa_reference_add_vc_vc_id boolean DEFAULT FALSE;
diff --git a/sql/Pg-upgrade2/shop_orders.sql b/sql/Pg-upgrade2/shop_orders.sql
new file mode 100644 (file)
index 0000000..70164fa
--- /dev/null
@@ -0,0 +1,103 @@
+-- @tag: shop_orders
+-- @description: Erstellen der Tabellen shop_orders und shop_order_items
+-- @depends: release_3_5_0 shops
+
+CREATE TABLE shop_orders (
+  id SERIAL PRIMARY KEY,
+  shop_trans_id integer NOT NULL, --id vom shop
+  shop_ordernumber TEXT, --Bestellnummer vom Shop
+  shop_data text,        -- store whole order as json
+  shop_customer_comment text, --Bestellkommentar des Kunden
+  amount numeric(15,5),  --Bruttogesamtbetrag
+  netamount numeric(15,5),--Nettogesamtbetrag
+  order_date timestamp, --Bestelldatum und Zeit
+  shipping_costs numeric(15,5),
+  shipping_costs_net numeric(15,5),
+  shipping_costs_id integer,
+  tax_included boolean,
+  payment_id integer, --Bezahlart
+  payment_description TEXT,  --Bezahlart
+  shop_id integer,               --welcher shop bei mehreren
+  host TEXT,             --Hostname vom Shop
+  remote_ip text,        --IP Besteller
+  transferred boolean DEFAULT FALSE,    -- übernommen
+  transfer_date date, -- Zeit wann übernommen
+  kivi_customer_id integer,  -- Kundenid von Tbl customer wenn übernommen
+  oe_transid integer,  -- id to
+-- Bestell-, Rechnungs- und Lieferadresse. !!Manche Shops bieten sowas!!
+-- In der Regel ist aber die Rechnungsadresse die Kundenadresse
+  -- Bestelldaten des Kunden
+  shop_customer_id integer,
+  shop_customer_number TEXT,
+  customer_lastname TEXT,
+  customer_firstname TEXT,
+  customer_company TEXT,
+  customer_street TEXT,
+  customer_zipcode TEXT,
+  customer_city TEXT,
+  customer_country TEXT,
+  customer_greeting TEXT,
+  customer_department TEXT,
+  customer_vat TEXT,
+  customer_phone TEXT,
+  customer_fax TEXT,
+  customer_email TEXT,
+  customer_newsletter boolean,
+  -- Rechnungsadresse
+  shop_c_billing_id integer,
+  shop_c_billing_number TEXT,
+  billing_lastname TEXT,
+  billing_firstname TEXT,
+  billing_company TEXT,
+  billing_street TEXT,
+  billing_zipcode TEXT,
+  billing_city TEXT,
+  billing_country TEXT,
+  billing_greeting TEXT,
+  billing_department TEXT,
+  billing_vat TEXT,
+  billing_phone TEXT,
+  billing_fax TEXT,
+  billing_email TEXT,
+
+  -- SEPA
+  sepa_account_holder TEXT,
+  sepa_iban TEXT,
+  sepa_bic TEXT,
+
+  -- Lieferadresse
+  shop_c_delivery_id integer,
+  shop_c_delivery_number TEXT,
+  delivery_lastname TEXT,
+  delivery_firstname TEXT,
+  delivery_company TEXT,
+  delivery_street TEXT,
+  delivery_zipcode TEXT,
+  delivery_city TEXT,
+  delivery_country TEXT,
+  delivery_greeting TEXT,
+  delivery_department TEXT,
+  delivery_vat TEXT,
+  delivery_phone TEXT,
+  delivery_fax TEXT,
+  delivery_email TEXT,
+
+  obsolete boolean DEFAULT FALSE NOT NULL,
+  positions integer,
+
+  itime timestamp DEFAULT now(),
+  mtime timestamp
+);
+
+CREATE TABLE shop_order_items (
+  id            SERIAL PRIMARY KEY,
+  shop_trans_id INTEGER NOT NULL, --id vom shop in shop-db? -> could use $order_item->shop_order->shop_trans_id instead
+  shop_order_id INTEGER REFERENCES shop_orders (id) ON DELETE CASCADE,
+  description   TEXT,  -- Artikelbezeichnung
+  partnumber    TEXT,
+  shop_id       INTEGER,
+  position      INTEGER,
+  tax_rate      NUMERIC(15,2),
+  quantity      NUMERIC(25,5),   -- qty in invoice and orderitems is real, doi is numeric(25,5)
+  price         NUMERIC(15,5)
+);
diff --git a/sql/Pg-upgrade2/shop_orders_add_active_pricesource.sql b/sql/Pg-upgrade2/shop_orders_add_active_pricesource.sql
new file mode 100644 (file)
index 0000000..8b258a7
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: shop_orders_add_active_price_source
+-- @description: Erstellen der Tabellen shop_orders und shop_order_items
+-- @depends: release_3_5_0 shop_orders
+
+ALTER TABLE shop_order_items ADD COLUMN active_price_source TEXT;
diff --git a/sql/Pg-upgrade2/shop_orders_update_1.sql b/sql/Pg-upgrade2/shop_orders_update_1.sql
new file mode 100644 (file)
index 0000000..e87b40e
--- /dev/null
@@ -0,0 +1,21 @@
+-- @tag: shop_orders_update_1
+-- @description: Ändern der Tabellen shop_orders und shop_order_items. Trigger für oe
+-- @depends: release_3_5_0 shop_orders shop_orders_add_active_price_source
+-- @ignore: 0
+
+ALTER TABLE shop_orders ADD FOREIGN KEY (shop_id) REFERENCES shops(id);
+ALTER TABLE shop_orders ADD FOREIGN KEY (kivi_customer_id) REFERENCES customer(id);
+ALTER TABLE shop_orders DROP COLUMN shop_data;
+ALTER TABLE shop_order_items DROP COLUMN shop_id;
+
+CREATE OR REPLACE FUNCTION update_shop_orders_on_delete_oe() RETURNS TRIGGER AS $$
+  BEGIN
+    UPDATE shop_orders SET oe_trans_id = NULL WHERE oe_trans_id = OLD.id;
+
+    RETURN OLD;
+  END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER after_delete_oe_trigger
+AFTER DELETE ON oe FOR EACH ROW EXECUTE
+PROCEDURE update_shop_orders_on_delete_oe();
diff --git a/sql/Pg-upgrade2/shop_orders_update_2.sql b/sql/Pg-upgrade2/shop_orders_update_2.sql
new file mode 100644 (file)
index 0000000..0d60b4c
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: shop_orders_update_2
+-- @description: Ändern der Tabellen shop_orders für Trigger spalte war falsch benannt
+-- @depends: shop_orders_update_1
+-- @ignore: 0
+
+ALTER TABLE shop_orders RENAME COLUMN oe_transid TO oe_trans_id;
diff --git a/sql/Pg-upgrade2/shop_orders_update_3.sql b/sql/Pg-upgrade2/shop_orders_update_3.sql
new file mode 100644 (file)
index 0000000..3f28406
--- /dev/null
@@ -0,0 +1,8 @@
+-- @tag: shop_orders_update_3
+-- @description: Ändern der Tabellen shop_orders und shop_order_items. Trigger für oe
+-- @depends: shop_orders_update_1 shop_orders_update_2
+-- @ignore: 0
+
+ALTER TABLE shop_orders DROP COLUMN oe_trans_id;
+
+DROP FUNCTION update_shop_orders_on_delete_oe() CASCADE;
diff --git a/sql/Pg-upgrade2/shop_orders_update_4.sql b/sql/Pg-upgrade2/shop_orders_update_4.sql
new file mode 100644 (file)
index 0000000..93324fb
--- /dev/null
@@ -0,0 +1,8 @@
+-- @tag: shop_orders_update_4
+-- @description: Ändern der Tabellen shop_orders, shop_trans_id darf auch Text enthalten
+-- @depends: shop_orders_update_1 shop_orders_update_2 shop_orders_update_3
+
+-- @ignore: 0
+
+ALTER TABLE shop_orders ALTER COLUMN shop_trans_id TYPE text;
+ALTER TABLE shop_order_items ALTER COLUMN shop_trans_id TYPE text;
diff --git a/sql/Pg-upgrade2/shop_parts.sql b/sql/Pg-upgrade2/shop_parts.sql
new file mode 100644 (file)
index 0000000..7d509ad
--- /dev/null
@@ -0,0 +1,27 @@
+-- @tag: shop_parts
+-- @description: Add tables for part information for shop
+-- @depends: release_3_5_0 shops
+-- @ignore: 0
+
+CREATE TABLE shop_parts (
+  id               SERIAL PRIMARY KEY,
+  shop_id          INTEGER NOT NULL REFERENCES shops(id),
+  part_id          INTEGER NOT NULL REFERENCES parts(id),
+  shop_description TEXT,
+  itime            TIMESTAMP DEFAULT now(),
+  mtime            TIMESTAMP,
+  last_update      TIMESTAMP,
+  show_date        DATE,   -- the starting date for displaying part in shop
+  sortorder        INTEGER,
+  front_page       BOOLEAN NOT NULL DEFAULT false,
+  active           BOOLEAN NOT NULL DEFAULT false,  -- rather than obsolete
+  shop_category TEXT[][],
+  active_price_source TEXT,
+  metatag_keywords TEXT,
+  metatag_description TEXT,
+  metatag_title TEXT,
+  UNIQUE (part_id, shop_id)  -- make sure a shop_part appears only once per shop and part
+);
+
+CREATE TRIGGER mtime_shop_parts BEFORE UPDATE ON shop_parts
+    FOR EACH ROW EXECUTE PROCEDURE set_mtime();
diff --git a/sql/Pg-upgrade2/shopimages.sql b/sql/Pg-upgrade2/shopimages.sql
new file mode 100644 (file)
index 0000000..767429e
--- /dev/null
@@ -0,0 +1,33 @@
+-- @tag:shopimages
+-- @description: Tabelle für Shopbilder und zusätzliche Konfiguration und valid_type für Filemanagement
+-- @depends: release_3_5_0 files shop_parts
+-- @ignore: 0
+
+CREATE TABLE shop_images(
+  id                      SERIAL PRIMARY KEY,
+  file_id                 INTEGER REFERENCES files(id) ON DELETE CASCADE,
+  position                INTEGER,
+  thumbnail_content       BYTEA,
+  thumbnail_width         INTEGER,
+  thumbnail_height        INTEGER,
+  thumbnail_content_type  TEXT,
+  itime                   TIMESTAMP DEFAULT now(),
+  mtime                   TIMESTAMP
+);
+
+CREATE TRIGGER mtime_shop_images BEFORE UPDATE ON shop_images FOR EACH ROW EXECUTE PROCEDURE set_mtime();
+
+ALTER TABLE defaults ADD COLUMN doc_storage_for_shopimages      text default 'Filesystem';
+
+ALTER TABLE files
+  DROP CONSTRAINT valid_type;
+ALTER TABLE files
+  ADD  CONSTRAINT valid_type CHECK (
+             (object_type = 'credit_note'     ) OR (object_type = 'invoice'                 ) OR (object_type = 'sales_order'       )
+          OR (object_type = 'sales_quotation' ) OR (object_type = 'sales_delivery_order'    ) OR (object_type = 'request_quotation' )
+          OR (object_type = 'purchase_order'  ) OR (object_type = 'purchase_delivery_order' ) OR (object_type = 'purchase_invoice'  )
+          OR (object_type = 'vendor'          ) OR (object_type = 'customer'                ) OR (object_type = 'part'              )
+          OR (object_type = 'gl_transaction'  ) OR (object_type = 'dunning'                 ) OR (object_type = 'dunning1'          )
+          OR (object_type = 'dunning2'        ) OR (object_type = 'dunning3'                ) OR (object_type = 'draft'             )
+          OR (object_type = 'statement'       ) OR (object_type = 'shop_image'              )
+  );
diff --git a/sql/Pg-upgrade2/shopimages_2.sql b/sql/Pg-upgrade2/shopimages_2.sql
new file mode 100644 (file)
index 0000000..0ff4d1c
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag:shopimages_2
+-- @description: Umbennung der Spalten für Weite und Breite in die Weite und Breite des orginal Bildes
+-- @depends: release_3_5_0 files shop_parts shopimages
+-- @ignore: 0
+
+ALTER TABLE shop_images RENAME thumbnail_width TO org_file_width;
+ALTER TABLE shop_images RENAME thumbnail_height TO org_file_height;
diff --git a/sql/Pg-upgrade2/shopimages_3.sql b/sql/Pg-upgrade2/shopimages_3.sql
new file mode 100644 (file)
index 0000000..84a3481
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag:shopimages_3
+-- @description: Neue Spalte object_id um eine group_by Klausel zu haben für act_as_list
+-- @depends: release_3_5_0 files shop_parts shopimages
+-- @ignore: 0
+
+ALTER TABLE shop_images ADD COLUMN object_id text NOT NULL;
diff --git a/sql/Pg-upgrade2/shops.sql b/sql/Pg-upgrade2/shops.sql
new file mode 100644 (file)
index 0000000..840b095
--- /dev/null
@@ -0,0 +1,21 @@
+-- @tag: shops
+-- @description: Tabelle für Shops
+-- @depends: release_3_5_0 customer_klass_rename_to_pricegroup_id_and_foreign_key
+-- @ignore: 0
+
+CREATE TABLE shops (
+  id SERIAL PRIMARY KEY,
+  description text,
+  obsolete BOOLEAN NOT NULL DEFAULT false,
+  sortkey INTEGER,
+  connector text,     -- hardcoded options, e.g. xtcommerce, shopware
+  pricetype text,     -- netto/brutto
+  price_source text,  -- sellprice/listprice/lastcost or pricegroup id
+  taxzone_id INTEGER,
+  last_order_number INTEGER,
+  orders_to_fetch INTEGER,
+  url text,
+  port INTEGER,
+  login text,  -- "user" is reserved
+  password text
+);
diff --git a/sql/Pg-upgrade2/shops_1.sql b/sql/Pg-upgrade2/shops_1.sql
new file mode 100644 (file)
index 0000000..89e69a5
--- /dev/null
@@ -0,0 +1,8 @@
+-- @tag: shop_1
+-- @description: Add tables for part information for shop
+-- @depends: shops
+-- @ignore: 0
+
+ALTER TABLE shops ADD COLUMN protocol TEXT NOT NULL DEFAULT 'http';
+ALTER TABLE shops ADD COLUMN path TEXT NOT NULL DEFAULT '/';
+ALTER TABLE shops RENAME COLUMN url TO server;
diff --git a/sql/Pg-upgrade2/shops_2.sql b/sql/Pg-upgrade2/shops_2.sql
new file mode 100644 (file)
index 0000000..28cf602
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: shop_2
+-- @description: Add tables for part information for shop
+-- @depends: shops
+-- @ignore: 0
+
+ALTER TABLE shops ADD COLUMN realm TEXT;
diff --git a/sql/Pg-upgrade2/shops_3.sql b/sql/Pg-upgrade2/shops_3.sql
new file mode 100644 (file)
index 0000000..9bfcc73
--- /dev/null
@@ -0,0 +1,13 @@
+-- @tag: shop_3
+-- @description: Add columns itime and mtime and transaction_description for table shops
+-- @depends: shops
+-- @ignore: 0
+
+ALTER TABLE shops ADD COLUMN transaction_description TEXT;
+ALTER TABLE shops ADD COLUMN itime timestamp DEFAULT now();
+ALTER TABLE shops ADD COLUMN mtime timestamp DEFAULT now();
+
+CREATE TRIGGER mtime_shops
+    BEFORE UPDATE ON shops
+    FOR EACH ROW
+    EXECUTE PROCEDURE set_mtime();
diff --git a/sql/Pg-upgrade2/shops_4.sql b/sql/Pg-upgrade2/shops_4.sql
new file mode 100644 (file)
index 0000000..78fe6c7
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: shop_4
+-- @description: Add column default_shipping_costs_parts_id
+-- @depends: shops
+-- @ignore: 0
+
+ALTER TABLE shops ADD COLUMN shipping_costs_parts_id integer;
diff --git a/sql/Pg-upgrade2/shops_5.sql b/sql/Pg-upgrade2/shops_5.sql
new file mode 100644 (file)
index 0000000..72b4355
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: shops_5
+-- @description: Shop-Config um Option zur direkten Beschreibungsübernahme erweitern
+-- @depends: shop_4
+-- @ignore: 0
+
+ALTER TABLE shops ADD COLUMN use_part_longdescription BOOLEAN default false;
diff --git a/sql/Pg-upgrade2/shops_6.sql b/sql/Pg-upgrade2/shops_6.sql
new file mode 100644 (file)
index 0000000..788bdb9
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: shop_add_proxy
+-- @description: Shop-Config um Option Proxy erweitert
+-- @depends: shops_5
+-- @ignore: 0
+
+ALTER TABLE shops ADD COLUMN proxy TEXT default '';
index 655d28f..b7be341 100644 (file)
@@ -36,6 +36,7 @@ sub run {
   }
 
   my @well_known_taxes = (
+      # German SKR03
       { taxkey => 0,  rate => 0,    taxdescription => qr{keine.*steuer}i,                       categories => 'ALQCIE' },
       { taxkey => 1,  rate => 0,    taxdescription => qr{frei}i,                                categories => 'ALQCIE' },
       { taxkey => 2,  rate => 0.07, taxdescription => qr{umsatzsteuer}i,                        categories => 'I' },
@@ -56,7 +57,15 @@ sub run {
       { taxkey => 18, rate => 0.07, taxdescription => qr{innergem.*erwerb.*erm}i,               categories => 'E' },
       { taxkey => 19, rate => 0.16, taxdescription => qr{innergem.*erwerb.*voll}i,              categories => 'E' },
       { taxkey => 19, rate => 0.19, taxdescription => qr{innergem.*erwerb.*voll}i,              categories => 'E' },
-      );
+
+      # Swiss
+      { taxkey => 2,  rate => 0.08,  taxdescription => qr{mwst}i,                                categories => 'I' },
+      { taxkey => 3,  rate => 0.025, taxdescription => qr{mwst}i,                                categories => 'I' },
+      { taxkey => 4,  rate => 0.08,  taxdescription => qr{mwst}i,                                categories => 'E' },
+      { taxkey => 5,  rate => 0.025, taxdescription => qr{mwst}i,                                categories => 'E' },
+      { taxkey => 6,  rate => 0.08,  taxdescription => qr{mwst}i,                                categories => 'E' },
+      { taxkey => 7,  rate => 0.025, taxdescription => qr{mwst}i,                                categories => 'E' },
+  );
 
   $query = qq|SELECT taxkey, taxdescription, rate, id AS tax_id FROM tax order by taxkey, rate;|;
 
diff --git a/sql/Pg-upgrade2/stocktakings.sql b/sql/Pg-upgrade2/stocktakings.sql
new file mode 100644 (file)
index 0000000..2c51a31
--- /dev/null
@@ -0,0 +1,24 @@
+-- @tag: stocktakings
+-- @description: Tabelle für in einer Inventur gezählte Artikel
+-- @depends: warehouse release_3_5_1
+
+
+CREATE TABLE stocktakings (
+       id                       INTEGER        NOT NULL DEFAULT nextval('id'),
+       inventory_id             INTEGER        REFERENCES inventory(id),
+       warehouse_id             INTEGER        NOT NULL REFERENCES warehouse(id),
+       bin_id                   INTEGER        NOT NULL REFERENCES bin(id),
+       parts_id                 INTEGER        NOT NULL REFERENCES parts(id),
+       employee_id              INTEGER        NOT NULL REFERENCES employee(id),
+       qty                      NUMERIC(25,5)  NOT NULL ,
+       comment                  TEXT,
+       chargenumber             TEXT           NOT NULL DEFAULT '',
+       bestbefore               DATE,
+       cutoff_date              DATE           NOT NULL,
+       itime                    TIMESTAMP      DEFAULT now(),
+       mtime                    TIMESTAMP,
+
+       PRIMARY KEY (id)
+);
+
+CREATE TRIGGER mtime_stocktakings BEFORE UPDATE ON stocktakings FOR EACH ROW EXECUTE PROCEDURE set_mtime();
diff --git a/sql/Pg-upgrade2/tax_point.sql b/sql/Pg-upgrade2/tax_point.sql
new file mode 100644 (file)
index 0000000..7fcb65d
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: tax_point
+-- @description: Feld Leistungsdatum in Einkaufs- & Verkaufsbelegen
+-- @depends: release_3_5_6_1
+ALTER TABLE ap ADD COLUMN tax_point DATE;
+ALTER TABLE ar ADD COLUMN tax_point DATE;
+ALTER TABLE gl ADD COLUMN tax_point DATE;
+ALTER TABLE oe ADD COLUMN tax_point DATE;
diff --git a/sql/Pg-upgrade2/tax_point2.sql b/sql/Pg-upgrade2/tax_point2.sql
new file mode 100644 (file)
index 0000000..6caf13d
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: tax_point2
+-- @description: Feld Leistungsdatum in Lieferscheinen
+-- @depends: tax_point
+ALTER TABLE delivery_orders ADD COLUMN tax_point DATE;
diff --git a/sql/Pg-upgrade2/tax_removed_taxnumber.sql b/sql/Pg-upgrade2/tax_removed_taxnumber.sql
new file mode 100644 (file)
index 0000000..9315b16
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: tax_removed_taxnumber
+-- @description: Spalte taxnumber aus tax entfernt
+-- @depends: release_3_5_4
+
+alter table tax drop column taxnumber;
diff --git a/sql/Pg-upgrade2/tax_reverse_charge.sql b/sql/Pg-upgrade2/tax_reverse_charge.sql
new file mode 100644 (file)
index 0000000..91ba9c1
--- /dev/null
@@ -0,0 +1,109 @@
+-- @tag: tax_reverse_charge
+-- @description: Reverse Charge für Kreditorenbelege
+-- @depends: release_3_6_0
+-- @ignore: 0
+
+ALTER TABLE tax add column reverse_charge_chart_id integer;
+
+INSERT INTO chart (
+  accno, description,
+  charttype,   category,  link,
+  taxkey_id
+  )
+SELECT
+  '1577','Abziehbare Vorst. nach §13b UstG 19%',
+  'A',         'E',       'AP_tax:IC_taxpart:IC_taxservice',
+  0
+WHERE EXISTS ( -- update only for SKR03
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR03EU' AND NOT EXISTS (SELECT id from chart where accno='1577')
+);
+
+INSERT INTO chart (
+  accno, description,
+  charttype,   category,  link,
+  taxkey_id
+  )
+SELECT
+  '1787','Umsatzsteuer nach §13b UStG 19%',
+  'A',         'I',       'AR_tax:IC_taxpart:IC_taxservice',
+  0
+WHERE EXISTS ( -- update only for SKR03
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR03EU' AND NOT EXISTS (SELECT id from chart where accno='1787')
+);
+
+
+INSERT INTO chart (
+  accno, description,
+  charttype,   category,  link,
+  taxkey_id
+  )
+SELECT
+  '1407','Abziehbare Vorst. nach §13b UstG 19%',
+  'A',         'E',       'AP_tax:IC_taxpart:IC_taxservice',
+  0
+WHERE EXISTS ( -- update only for SKR04
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR04EU' AND NOT EXISTS (SELECT id from chart where accno='1407')
+);
+
+INSERT INTO chart (
+  accno, description,
+  charttype,   category,  link,
+  taxkey_id
+  )
+SELECT
+  '3837','Umsatzsteuer nach §13b UStG 19%',
+  'A',         'I',       'AR_tax:IC_taxpart:IC_taxservice',
+  0
+WHERE EXISTS ( -- update only for SKR04
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR04EU' AND NOT EXISTS (SELECT id from chart where accno='3837')
+);
+
+
+
+INSERT INTO tax (
+  chart_id,
+  reverse_charge_chart_id,
+  rate,
+  taxkey,
+  taxdescription,
+  chart_categories
+  )
+  SELECT
+  (SELECT id FROM chart WHERE accno = '1577'),
+  (SELECT id FROM chart WHERE accno = '1787'), 0,
+  '94', '19% Vorsteuer und 19% Umsatzsteuer', 'EI'
+WHERE EXISTS ( -- update only for SKR03
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR03EU'
+);
+
+
+INSERT INTO tax (
+  chart_id,
+  reverse_charge_chart_id,
+  rate,
+  taxkey,
+  taxdescription,
+  chart_categories
+  )
+  SELECT
+  (SELECT id FROM chart WHERE accno = '1407'),
+  (SELECT id FROM chart WHERE accno = '3837'), 0,
+  '94', '19% Vorsteuer und 19% Umsatzsteuer', 'EI'
+WHERE EXISTS ( -- update only for SKR03
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR04EU'
+);
+
+-- if not defined
+insert into taxkeys(chart_id,tax_id,taxkey_id,startdate) SELECT (SELECT chart_id FROM tax WHERE taxkey = '94'),0,0,'1970-01-01' WHERE NOT EXISTS
+  (SELECT chart_id from taxkeys where chart_id = ( SELECT chart_id FROM tax WHERE taxkey = '94'))
+  AND (EXISTS (SELECT coa FROM defaults WHERE defaults.coa='Germany-DATEV-SKR04EU') OR EXISTS (SELECT coa FROM defaults WHERE defaults.coa='Germany-DATEV-SKR03EU'));
+
+insert into taxkeys(chart_id,tax_id,taxkey_id,startdate) SELECT (SELECT reverse_charge_chart_id FROM tax WHERE taxkey = '94'),0,0,'1970-01-01' WHERE NOT EXISTS
+  (SELECT chart_id from taxkeys where chart_id = ( SELECT reverse_charge_chart_id FROM tax WHERE taxkey = '94'))
+    AND (EXISTS (SELECT coa FROM defaults WHERE defaults.coa='Germany-DATEV-SKR04EU') OR EXISTS (SELECT coa FROM defaults WHERE defaults.coa='Germany-DATEV-SKR03EU'));
diff --git a/sql/Pg-upgrade2/tax_reverse_charge_key_18.sql b/sql/Pg-upgrade2/tax_reverse_charge_key_18.sql
new file mode 100644 (file)
index 0000000..f67eff3
--- /dev/null
@@ -0,0 +1,51 @@
+-- @tag: tax_reverse_charge_key_18
+-- @description: Reverse Charge für Kreditorenbelege Steuerschlüssel 18
+-- @depends: release_3_6_0 clean_tax_18_19
+-- @ignore: 0
+
+INSERT INTO tax (
+  chart_id,
+  reverse_charge_chart_id,
+  rate,
+  taxkey,
+  taxdescription,
+  chart_categories
+  )
+  SELECT
+  (SELECT id FROM chart WHERE accno = '1572'),
+  (SELECT id FROM chart WHERE accno = '1772'), 0.07,
+  '18', 'Stpf. innergemeinschaftlicher Erwerb zum verminderten Vor- und Ust.-satz', 'EI'
+WHERE EXISTS ( -- update only for SKR03
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR03EU'
+);
+
+
+INSERT INTO tax (
+  chart_id,
+  reverse_charge_chart_id,
+  rate,
+  taxkey,
+  taxdescription,
+  chart_categories
+  )
+  SELECT
+  (SELECT id FROM chart WHERE accno = '1402'),
+  (SELECT id FROM chart WHERE accno = '3802'), 0.07,
+  '18', 'Stpf. innergemeinschaftlicher Erwerb zum verminderten Vor- und Ust.-satz', 'EI'
+WHERE EXISTS ( -- update only for SKR04
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR04EU'
+);
+
+
+-- if not defined
+insert into taxkeys(chart_id,tax_id,taxkey_id,startdate) SELECT (SELECT reverse_charge_chart_id FROM tax WHERE taxkey = '18' and rate = 0.07 and reverse_charge_chart_id is not null),0,0,'1970-01-01' WHERE NOT EXISTS
+  (SELECT chart_id from taxkeys where chart_id = ( SELECT reverse_charge_chart_id FROM tax WHERE taxkey = '18' and rate = 0.07 and reverse_charge_chart_id is not null))
+  AND (EXISTS (SELECT coa FROM defaults WHERE defaults.coa='Germany-DATEV-SKR04EU') OR EXISTS (SELECT coa FROM defaults WHERE defaults.coa='Germany-DATEV-SKR03EU'));
+
+-- if not defined
+insert into taxkeys(chart_id,tax_id,taxkey_id,startdate) SELECT (SELECT chart_id FROM tax WHERE taxkey = '18' and rate = 0.07 and reverse_charge_chart_id is not null),0,0,'1970-01-01' WHERE NOT EXISTS
+  (SELECT chart_id from taxkeys where chart_id = ( SELECT chart_id FROM tax WHERE taxkey = '18' and rate = 0.07 and reverse_charge_chart_id is not null))
+  AND (EXISTS (SELECT coa FROM defaults WHERE defaults.coa='Germany-DATEV-SKR04EU') OR EXISTS (SELECT coa FROM defaults WHERE defaults.coa='Germany-DATEV-SKR03EU'));
+
diff --git a/sql/Pg-upgrade2/tax_reverse_charge_key_19.sql b/sql/Pg-upgrade2/tax_reverse_charge_key_19.sql
new file mode 100644 (file)
index 0000000..c4b4a68
--- /dev/null
@@ -0,0 +1,65 @@
+-- @tag: tax_reverse_charge_key_19
+-- @description: Reverse Charge für Kreditorenbelege Steuerschlüssel 19
+-- @depends: release_3_6_0 clean_tax_18_19
+-- @ignore: 0
+
+UPDATE tax set rate=0.19 where taxkey=94 AND reverse_charge_chart_id is not NULL;
+
+INSERT INTO chart (
+  accno, description,
+  charttype,   category,  link,
+  taxkey_id
+  )
+SELECT
+  '1774','Umsatzsteuer aus innergemeinschftl. Erwerb 19%',
+  'A',         'I',       'AR_tax:IC_taxpart:IC_taxservice',
+  0
+WHERE EXISTS ( -- update only for SKR03
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR03EU' AND NOT EXISTS (SELECT id from chart where accno='1774')
+);
+
+INSERT INTO tax (
+  chart_id,
+  reverse_charge_chart_id,
+  rate,
+  taxkey,
+  taxdescription,
+  chart_categories
+  )
+  SELECT
+  (SELECT id FROM chart WHERE accno = '1574'),
+  (SELECT id FROM chart WHERE accno = '1774'), 0.19,
+  '19', 'Stpf. innergemeinschaftlicher Erwerb zum vollem Vor- und Ust.-satz', 'EI'
+WHERE EXISTS ( -- update only for SKR03
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR03EU'
+);
+
+
+INSERT INTO tax (
+  chart_id,
+  reverse_charge_chart_id,
+  rate,
+  taxkey,
+  taxdescription,
+  chart_categories
+  )
+  SELECT
+  (SELECT id FROM chart WHERE accno = '1404'),
+  (SELECT id FROM chart WHERE accno = '3804'), 0.19,
+  '19', 'Stpf. innergemeinschaftlicher Erwerb zum vollem Vor- und Ust.-satz', 'EI'
+WHERE EXISTS ( -- update only for SKR04
+    SELECT coa FROM defaults
+    WHERE defaults.coa='Germany-DATEV-SKR04EU'
+);
+
+-- if not defined
+insert into taxkeys(chart_id,tax_id,taxkey_id,startdate) SELECT (SELECT reverse_charge_chart_id FROM tax WHERE taxkey = '19' and rate = 0.19 and reverse_charge_chart_id is not null),0,0,'1970-01-01' WHERE NOT EXISTS
+  (SELECT chart_id from taxkeys where chart_id = ( SELECT reverse_charge_chart_id FROM tax WHERE taxkey = '19' and rate = 0.19 and reverse_charge_chart_id is not null))
+  AND (EXISTS (SELECT coa FROM defaults WHERE defaults.coa='Germany-DATEV-SKR04EU') OR EXISTS (SELECT coa FROM defaults WHERE defaults.coa='Germany-DATEV-SKR03EU'));
+-- if not defined
+insert into taxkeys(chart_id,tax_id,taxkey_id,startdate) SELECT (SELECT chart_id FROM tax WHERE taxkey = '19' and rate = 0.19 and reverse_charge_chart_id is not null),0,0,'1970-01-01' WHERE NOT EXISTS
+  (SELECT chart_id from taxkeys where chart_id = ( SELECT chart_id FROM tax WHERE taxkey = '19' and rate = 0.19 and reverse_charge_chart_id is not null))
+  AND (EXISTS (SELECT coa FROM defaults WHERE defaults.coa='Germany-DATEV-SKR04EU') OR EXISTS (SELECT coa FROM defaults WHERE defaults.coa='Germany-DATEV-SKR03EU'));
+
diff --git a/sql/Pg-upgrade2/time_recordings.sql b/sql/Pg-upgrade2/time_recordings.sql
new file mode 100644 (file)
index 0000000..ccbf329
--- /dev/null
@@ -0,0 +1,35 @@
+-- @tag: time_recordings
+-- @description: Tabellen zur Zeiterfassung
+-- @depends: release_3_5_6_1
+
+CREATE TABLE time_recording_types (
+  id                 SERIAL,
+  abbreviation       TEXT     NOT NULL,
+  description        TEXT,
+  position           INTEGER  NOT NULL,
+  obsolete           BOOLEAN  NOT NULL DEFAULT false,
+  PRIMARY KEY (id)
+);
+
+CREATE TABLE time_recordings (
+  id                SERIAL,
+  customer_id       INTEGER   NOT NULL,
+  project_id        INTEGER,
+  start_time        TIMESTAMP NOT NULL,
+  end_time          TIMESTAMP,
+  type_id           INTEGER,
+  description       TEXT      NOT NULL,
+  staff_member_id   INTEGER   NOT NULL,
+  employee_id       INTEGER   NOT NULL,
+  itime             TIMESTAMP NOT NULL DEFAULT now(),
+  mtime             TIMESTAMP NOT NULL DEFAULT now(),
+
+  PRIMARY KEY (id),
+  FOREIGN KEY (customer_id)     REFERENCES customer (id),
+  FOREIGN KEY (staff_member_id) REFERENCES employee (id),
+  FOREIGN KEY (employee_id)     REFERENCES employee (id),
+  FOREIGN KEY (project_id)      REFERENCES project (id),
+  FOREIGN KEY (type_id)         REFERENCES time_recording_types (id)
+);
+
+CREATE TRIGGER mtime_time_recordings BEFORE UPDATE ON time_recordings FOR EACH ROW EXECUTE PROCEDURE set_mtime();
diff --git a/sql/Pg-upgrade2/time_recordings2.sql b/sql/Pg-upgrade2/time_recordings2.sql
new file mode 100644 (file)
index 0000000..afebb21
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: time_recordings2
+-- @description: Ergänzung zur Zeiterfassung
+-- @depends: time_recordings
+ALTER TABLE time_recordings ADD column booked boolean DEFAULT false;
+ALTER TABLE time_recordings ADD column payroll boolean DEFAULT false;
+
diff --git a/sql/Pg-upgrade2/time_recordings_add_order.sql b/sql/Pg-upgrade2/time_recordings_add_order.sql
new file mode 100644 (file)
index 0000000..a9b1a0d
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: time_recordings_add_order
+-- @description: Erweiterung Zeiterfassung um Fremdschlüssel zu Auftrag
+-- @depends: time_recordings_date_duration
+
+ALTER TABLE time_recordings ADD COLUMN order_id INTEGER REFERENCES oe (id);
diff --git a/sql/Pg-upgrade2/time_recordings_articles.sql b/sql/Pg-upgrade2/time_recordings_articles.sql
new file mode 100644 (file)
index 0000000..c693c36
--- /dev/null
@@ -0,0 +1,13 @@
+-- @tag: time_recordings_articles
+-- @description: Zeiterfassungs-Artikel
+-- @depends: time_recordings
+
+CREATE TABLE time_recording_articles (
+  id                 SERIAL,
+  part_id            INTEGER  REFERENCES parts(id) UNIQUE NOT NULL,
+  position           INTEGER  NOT NULL,
+
+  PRIMARY KEY (id)
+);
+
+ALTER TABLE time_recordings ADD COLUMN part_id INTEGER REFERENCES parts(id);
diff --git a/sql/Pg-upgrade2/time_recordings_date_duration.sql b/sql/Pg-upgrade2/time_recordings_date_duration.sql
new file mode 100644 (file)
index 0000000..98404e1
--- /dev/null
@@ -0,0 +1,38 @@
+-- @tag: time_recordings_date_duration
+-- @description: Erweiterung Zeiterfassung um Datum und Dauer
+-- @depends: time_recordings2
+
+ALTER TABLE time_recordings ADD   COLUMN date     DATE;
+ALTER TABLE time_recordings ADD   COLUMN duration INTEGER;
+
+UPDATE time_recordings SET date = start_time::DATE;
+ALTER TABLE time_recordings ALTER COLUMN start_time DROP NOT NULL;
+ALTER TABLE time_recordings ALTER COLUMN date SET NOT NULL;
+
+UPDATE time_recordings SET duration = EXTRACT(EPOCH FROM (end_time - start_time))/60;
+
+-- trigger to set date from start_time
+CREATE OR REPLACE FUNCTION time_recordings_set_date_trigger()
+RETURNS TRIGGER AS $$
+  BEGIN
+    IF NEW.start_time IS NOT NULL THEN
+      NEW.date = NEW.start_time::DATE;
+    END IF;
+    RETURN NEW;
+  END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER time_recordings_set_date BEFORE INSERT OR UPDATE ON time_recordings FOR EACH ROW EXECUTE PROCEDURE time_recordings_set_date_trigger();
+
+-- trigger to set duration from start_time and end_time
+CREATE OR REPLACE FUNCTION time_recordings_set_duration_trigger()
+RETURNS TRIGGER AS $$
+  BEGIN
+    IF NEW.start_time IS NOT NULL AND NEW.end_time IS NOT NULL THEN
+      NEW.duration = EXTRACT(EPOCH FROM (NEW.end_time - NEW.start_time))/60;
+    END IF;
+    RETURN NEW;
+  END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER time_recordings_set_duration BEFORE INSERT OR UPDATE ON time_recordings FOR EACH ROW EXECUTE PROCEDURE time_recordings_set_duration_trigger();
diff --git a/sql/Pg-upgrade2/time_recordings_remove_type.sql b/sql/Pg-upgrade2/time_recordings_remove_type.sql
new file mode 100644 (file)
index 0000000..db79bba
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: time_recordings_remove_type
+-- @description: Zeiterfassungs-Typen entfernen
+-- @depends: time_recordings time_recordings2
+
+ALTER TABLE time_recordings DROP column type_id;
+DROP TABLE time_recording_types;
index e3bfe41..07eceb8 100644 (file)
@@ -1,7 +1,6 @@
 -- @tag: transfer_out_sales_invoice
 -- @description: Felder für das Feature "Auslagern beim Buchen von Verkaufsrechnungen".
 -- @depends: warehouse_add_delivery_order_items_stock_id
--- @encoding: utf-8
 
 ALTER TABLE inventory ADD COLUMN invoice_id      INTEGER REFERENCES invoice(id);
 ALTER TABLE defaults  ADD COLUMN is_transfer_out BOOLEAN NOT NULL DEFAULT FALSE;
diff --git a/sql/Pg-upgrade2/transfer_out_serial_charge_number.sql b/sql/Pg-upgrade2/transfer_out_serial_charge_number.sql
new file mode 100644 (file)
index 0000000..07a7667
--- /dev/null
@@ -0,0 +1,4 @@
+-- @tag: transfer_out_serial_charge_number
+-- @description: Feld für das Feature "VK-Seriennummer ist Lager-Chargennummer".
+-- @depends: release_3_5_6
+ALTER TABLE defaults  ADD COLUMN sales_serial_eq_charge BOOLEAN NOT NULL DEFAULT FALSE;
diff --git a/sql/Pg-upgrade2/transfer_type_assembled.sql b/sql/Pg-upgrade2/transfer_type_assembled.sql
new file mode 100644 (file)
index 0000000..5a402fe
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: transfer_type_assembled
+-- @description: Transfertyp &quot;gefertigt&quot; wird ben&ouml;tigt.
+-- @depends: release_3_4_0 warehouse
+
+INSERT INTO transfer_type (direction, description, sortkey) VALUES ('in', 'assembled', (SELECT COALESCE(MAX(sortkey), 0) + 1 FROM transfer_type));
diff --git a/sql/Pg-upgrade2/transfer_type_stocktaking.sql b/sql/Pg-upgrade2/transfer_type_stocktaking.sql
new file mode 100644 (file)
index 0000000..f47c543
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: transfer_type_stocktaking
+-- @description: neuer Transfertyp stocktaking für Inventur
+-- @depends: warehouse
+
+INSERT INTO transfer_type (direction, description, sortkey) VALUES ('in',  'stocktaking', (SELECT COALESCE(MAX(sortkey), 0) + 1 FROM transfer_type));
+INSERT INTO transfer_type (direction, description, sortkey) VALUES ('out', 'stocktaking', (SELECT COALESCE(MAX(sortkey), 0) + 1 FROM transfer_type));
diff --git a/sql/Pg-upgrade2/trigram_extension.sql b/sql/Pg-upgrade2/trigram_extension.sql
new file mode 100644 (file)
index 0000000..3097740
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: trigram_extension
+-- @description: Trigram-Index-Erweiterung installieren
+-- @depends: release_3_5_0
+-- @ignore: 0
+-- @superuser_privileges: 1
+
+CREATE EXTENSION IF NOT EXISTS pg_trgm;
diff --git a/sql/Pg-upgrade2/trigram_indices.sql b/sql/Pg-upgrade2/trigram_indices.sql
new file mode 100644 (file)
index 0000000..d240d80
--- /dev/null
@@ -0,0 +1,42 @@
+-- @tag: trigram_indices
+-- @description: Trigram Indizes für häufig durchsuchte Spalten
+-- @depends: release_3_5_0 trigram_extension
+
+CREATE INDEX customer_customernumber_gin_trgm_idx    ON customer        USING gin (customernumber          gin_trgm_ops);
+CREATE INDEX customer_name_gin_trgm_idx              ON customer        USING gin (name                    gin_trgm_ops);
+
+CREATE INDEX vendor_vendornumber_gin_trgm_idx        ON vendor          USING gin (vendornumber            gin_trgm_ops);
+CREATE INDEX vendor_name_gin_trgm_idx                ON vendor          USING gin (name                    gin_trgm_ops);
+
+CREATE INDEX parts_partnumber_gin_trgm_idx           ON parts           USING gin (partnumber              gin_trgm_ops);
+CREATE INDEX parts_description_gin_trgm_idx          ON parts           USING gin (description             gin_trgm_ops);
+
+CREATE INDEX oe_ordnumber_gin_trgm_idx               ON oe              USING gin (ordnumber               gin_trgm_ops);
+CREATE INDEX oe_quonumber_gin_trgm_idx               ON oe              USING gin (quonumber               gin_trgm_ops);
+CREATE INDEX oe_cusordnumber_gin_trgm_idx            ON oe              USING gin (cusordnumber            gin_trgm_ops);
+CREATE INDEX oe_transaction_description_gin_trgm_idx ON oe              USING gin (transaction_description gin_trgm_ops);
+
+CREATE INDEX do_donumber_gin_trgm_idx                ON delivery_orders USING gin (donumber                gin_trgm_ops);
+CREATE INDEX do_ordnumber_gin_trgm_idx               ON delivery_orders USING gin (ordnumber               gin_trgm_ops);
+CREATE INDEX do_cusordnumber_gin_trgm_idx            ON delivery_orders USING gin (cusordnumber            gin_trgm_ops);
+CREATE INDEX do_transaction_description_gin_trgm_idx ON delivery_orders USING gin (transaction_description gin_trgm_ops);
+
+CREATE INDEX ar_invnumber_gin_trgm_idx               ON ar              USING gin (invnumber               gin_trgm_ops);
+CREATE INDEX ar_ordnumber_gin_trgm_idx               ON ar              USING gin (ordnumber               gin_trgm_ops);
+CREATE INDEX ar_quonumber_gin_trgm_idx               ON ar              USING gin (quonumber               gin_trgm_ops);
+CREATE INDEX ar_cusordnumber_gin_trgm_idx            ON ar              USING gin (cusordnumber            gin_trgm_ops);
+CREATE INDEX ar_transaction_description_gin_trgm_idx ON ar              USING gin (transaction_description gin_trgm_ops);
+
+CREATE INDEX ap_invnumber_gin_trgm_idx               ON ap              USING gin (invnumber               gin_trgm_ops);
+CREATE INDEX ap_ordnumber_gin_trgm_idx               ON ap              USING gin (ordnumber               gin_trgm_ops);
+CREATE INDEX ap_quonumber_gin_trgm_idx               ON ap              USING gin (quonumber               gin_trgm_ops);
+CREATE INDEX ap_transaction_description_gin_trgm_idx ON ap              USING gin (transaction_description gin_trgm_ops);
+
+CREATE INDEX gl_description_gin_trgm_idx             ON gl              USING gin (description             gin_trgm_ops);
+CREATE INDEX gl_reference_gin_trgm_idx               ON gl              USING gin (reference               gin_trgm_ops);
+
+CREATE INDEX orderitems_description_gin_trgm_idx     ON orderitems      USING gin (description             gin_trgm_ops);
+
+CREATE INDEX doi_description_gin_trgm_idx       ON delivery_order_items USING gin (description             gin_trgm_ops);
+
+CREATE INDEX invoice_description_gin_trgm_idx        ON invoice         USING gin (description             gin_trgm_ops);
diff --git a/sql/Pg-upgrade2/trigram_indices_webshop.sql b/sql/Pg-upgrade2/trigram_indices_webshop.sql
new file mode 100644 (file)
index 0000000..399e8df
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: trigram_indices_webshop
+-- @description: Trigram Indizes für Fuzzysearch bei der Kundensuche im Shopmodul
+-- @depends: release_3_5_0 trigram_extension
+
+CREATE INDEX customer_street_gin_trgm_idx            ON customer        USING gin (street                  gin_trgm_ops);
diff --git a/sql/Pg-upgrade2/use_html_in_letter.pl b/sql/Pg-upgrade2/use_html_in_letter.pl
new file mode 100644 (file)
index 0000000..7f42cc0
--- /dev/null
@@ -0,0 +1,21 @@
+# @tag: use_html_in_letter
+# @description: Briefe: HTML für Body nutzen können
+# @depends: letter letter_draft
+package SL::DBUpgrade2::use_html_in_letter;
+
+use strict;
+use utf8;
+
+use SL::DBUtils;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+sub run {
+  my ($self) = @_;
+
+  $self->convert_column_to_html($_, 'body') for qw(letter letter_draft);
+
+  return 1;
+}
+
+1;
diff --git a/sql/Pg-upgrade2/user_preferences.sql b/sql/Pg-upgrade2/user_preferences.sql
new file mode 100644 (file)
index 0000000..d6a1640
--- /dev/null
@@ -0,0 +1,13 @@
+-- @tag: user_preferences
+-- @description: Benutzereinstellungen
+-- @depends: release_3_4_1
+
+CREATE TABLE user_preferences (
+  id         SERIAL PRIMARY KEY,
+  login      TEXT NOT NULL,
+  namespace  TEXT NOT NULL,
+  version    NUMERIC(15,5),
+  key        TEXT NOT NULL,
+  value      TEXT,
+  UNIQUE (login, namespace, version, key)
+);
diff --git a/sql/Swiss-German-chart.sql b/sql/Swiss-German-chart.sql
deleted file mode 100644 (file)
index 7e13898..0000000
+++ /dev/null
@@ -1,160 +0,0 @@
--- Swiss chart of accounts
--- adapted to numeric representation of chart no.
--- contributed by Martin Krung
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('10000','AKTIVEN','H','1','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('11000','UMLAUFSVERMÖGEN','H','10000','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('11100','Flüssige Mittel','H','11000','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('11102','Bank CS Kt. 177929-11','A','11100','A','AR_paid:AP_paid');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('11110','Forderungen','H','11000','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('11120','Vorräte und angefangene Arbeiten','H','11000','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('11128','Angefangene Arbeiten','A','11120','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('11130','Aktive Rechnungsabgrenzung','A','11000','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('14000','ANLAGEVERMÖGEN','H','10000','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('18000','AKTIVIERTER AUFWAND UND AKTIVE BERICHTIGUNGSPOSTEN','H','10000','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('18182','Entwicklungsaufwand','A','18000','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('20000','PASSIVEN','H','2','L','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21000','FREMDKAPITAL KURZFRISTIG','H','20000','L','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21200','Kurzfristige Verbindlichkeiten aus Lieferungen und Leistungen','H','21000','L','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21201','Lieferanten','A','21200','L','AP');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21202','Personalaufwand','A','21200','L','AP');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21203','Sozialversicherungen','A','21200','L','AP');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21205','Leasing','A','21200','L','AP');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21210','Kurzfristige Finanzverbindlichkeiten','H','21000','L','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21220','Andere kurzfristige Verbindlichkeiten','H','21000','L','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21222','MWST (3,6)','A','21220','L','AR_tax:AP_tax:IC_taxpart:IC_taxservice');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21229','Gewinnausschüttung','A','21220','L','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21230','Passive Rechnungsabgrenzung, kurzfristige Rückstellungen','H','21000','L','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21235','Rückstellungen','A','21230','L','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('24000','FREMDKAPITAL LANGFRISTIG','H','20000','L','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('24256','Gesellschafter','A','24000','L','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('28000','EIGENKAPITAL','H','20000','Q','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('28280','Stammkapital','A','28000','Q','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('28290','Reserven, Bilanzgewinn','H','28000','Q','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('28291','Reserven','A','28290','Q','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('28295','Gewinnvortrag','A','28290','Q','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('28296','Jahresgewinn','A','28290','Q','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('30000','BETRIEBSERTRAG AUS LIEFERUNGEN UND LEISTUNGEN','H','30000','I','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('31000','PRODUKTIONSERTRAG','H','30000','I','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('31001','Computer','A','31000','I','AR_amount:IC_sale');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('31005','Übrige Produkte','A','31000','I','AR_amount:IC_sale');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('32000','HANDELSERTRAG','H','30000','I','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('32001','Hardware','A','32000','I','AR_amount:IC_sale');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('32002','Software OSS','A','32000','I','AR_amount:IC_sale');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('32003','Software kommerz.','A','32000','I','AR_amount:IC_sale');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('32005','Übrige','A','32000','I','AR_amount:IC_sale');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('34000','DIENSTLEISTUNGSERTRAG','H','30000','I','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('34001','Beratung','A','34000','I','AR_amount:IC_income');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('34002','Installation','A','34000','I','AR_amount:IC_income');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('36000','ÜBRIGER ERTRAG','H','30000','I','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('37000','EIGENLEISTUNGEN UND EIGENVERBRAUCH','H','30000','I','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('37001','Eigenleistungen','A','37000','I','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('37002','Eigenverbrauch','A','37000','I','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('38000','BESTANDESÄNDERUNGEN ANGEFANGENE UND FERTIGGESTELLTE ARBEITUNG AUS PRODUKTION UND DIENSTLEISTUNG','H','30000','I','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('38001','Bestandesänderungen','A','38000','I','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('39000','ERTRAGSMINDERUNGEN AUS PRODUKTIONS-, HANDELS- UND DIENSTLEISTUNGSERTRÄGEN','H','30000','I','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('40000','AUFWAND FÜR MATERIAL, WAREN UND DIENSTLEISTUNGEN','H','40000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('41000','MATERIALAUFWAND','H','40000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('41001','Computer','A','41000','E','AP_amount:IC_cogs');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('41005','Übrige Produkte','A','41000','E','AP_amount:IC_cogs');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('42000','HANDELSWARENAUFWAND','H','40000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('42001','Hardware','A','42000','E','AP_amount:IC_cogs');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('42002','Software OSS','A','32000','I','AP_amount:IC_cogs');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('42003','Software kommerz.','A','42000','I','AP_amount:IC_cogs');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('42005','Übrige','A','42000','E','AP_amount:IC_cogs');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('44000','AUFWAND FÜR DRITTLEISTUNGEN','H','40000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('46000','ÜBRIGER AUFWAND','H','40000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('47000','DIREKTE EINKAUFSSPESEN','H','40000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('47001','Einkaufsspesen','A','47000','E','AP_amount:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('48000','BESTANDESVERÄNDERUNGEN, MATERIAL- UND WARENVERLUSTE','H','40000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('48001','Bestandesänderungen','A','48000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('49000','AUFWANDMINDERUNGEN','H','40000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('49005','Aufwandminderungen','A','49000','E','AP_amount:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('50000','PERSONALAUFWAND','H','50000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('57000','SOZIALVERSICHERUNGSAUFWAND','H','50000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('58000','ÜBRIGER PERSONALAUFWAND','H','58000','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('58005','Sonstiger Personalaufwand','A','58000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('59000','ARBEITSLEISTUNGEN DRITTER','H','50000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('60000','SONSTIGER BETRIEBSAUFWAND','H','60000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('61000','RAUMAUFWAND','H','60000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('61900','UNTERHALT, REPARATUREN, ERSATZ, LEASINGAUFWAND MOBILE SACHANLAGEN','H','60000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('61901','Unterhalt','A','61900','E','AP_amount');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('62000','FAHRZEUG- UND TRANSPORTAUFWAND','H','60000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('62002','Transportaufwand','A','62000','E','AP_amount');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('63000','SACHVERSICHERUNGEN, ABGABEN, GEBÜHREN, BEWILLIGUNGEN','H','60000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('65000','VERWALTUNGS- UND INFORMATIKAUFWAND','H','60000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('66000','WERBEAUFWAND','H','60000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('67000','ÜBRIGER BETRIEBSAUFWAND','H','60000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('67001','Übriger Betriebsaufwand','A','67000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('68000','FINANZERFOLG','H','60000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('68001','Finanzaufwand','A','68000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('68002','Bankspesen','A','68000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('68005','Finanzertrag','A','68000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('69000','ABSCHREIBUNGEN','H','60000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('69001','Abschreibungen','A','69000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('80000','AUSSERORDENTLICHER UND BETRIEBSFREMDER ERFOLG, STEUERN','H','80000','I','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('80001','Ausserordentlicher Ertrag','A','80000','I','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('80002','Ausserordentlicher Aufwand','A','80000','I','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('89000','STEUERAUFWAND','H','80000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('89001','Steuern','A','80000','E','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('90000','ABSCHLUSS','H','90000','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('91000','ERFOLGSRECHNUNG','H','91000','I','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('92000','BILANZ','H','92000','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('93000','GEWINNVERWENDUNG','H','93000','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('99000','SAMMEL- UND FEHLBUCHUNGEN','H','99000','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('11121','Warenvorräte','A','11120','A','IC');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('44001','Aufwand für Drittleistungen','A','44000','E','AP_amount:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('63001','Betriebsversicherungen','A','63000','E','AP_amount:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('57004','Unfallversicherung','A','57000','E','AP_amount:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('57005','Krankentaggeldversicherung','A','57000','E','AP_amount:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('57003','Berufliche Vorsorge','A','57000','E','AP_amount:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('57002','FAK','A','57000','E','AP_amount:IC_income:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('65009','Übriger Verwaltungsaufwand','A','65000','E','AP_amount:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('65003','Porti','A','65000','E','AP_amount:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('65002','Telekomm','A','65000','E','AP_amount:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('65001','Büromaterial','A','65000','E','AP_amount:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('18181','Gründungsaufwand','A','18000','A','IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('50001','Löhne und Gehälter','A','50000','E','IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('50002','Erfolgsbeteiligungen','A','50000','E','IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21216','Gesellschafter','A','21210','L','AP');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('62001','Fahrzeugaufwand','A','62000','E','AP_amount:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('58003','Spesen','A','58000','E','IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('65004','Fachliteratur','A','65000','E','AP_amount:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('39001','Skonti','A','39000','I','IC_sale:IC_cogs:IC_income:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('39002','Rabatte, Preisnachlässe','A','39000','I','IC_sale:IC_cogs:IC_income:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('36005','Kursgewinn','A','39000','I','IC_sale:IC_cogs:IC_income');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('39006','Kursverlust','A','39000','E','IC_sale:IC_cogs:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('39005','Verluste aus Forderungen','A','39000','E','IC_sale:IC_cogs:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('14151','Mobiliar und Einrichtungen','A','14150','A','IC');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('14152','Büromaschinen, EDV','A','14150','A','IC');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('11119','Verrechnungssteuer','A','11110','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('11118','MWST Vorsteuer auf Investitionen','A','11110','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('36004','Versand','A','36000','I','IC_income');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('36001','Reisezeit','A','36000','I','IC_income');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('36002','Reise (Fahrt)','A','36000','I','IC_income');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('11117','MWST Vorsteuer auf Aufwand','A','11110','A','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21228','Geschuldete Steuern','A','21220','L','AP');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21223','MWST (7,6)','A','21220','L','AR_tax:AP_tax:IC_taxpart:IC_expense:IC_taxservice');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('57001','AHV, IV, EO, ALV','A','57000','E','AP_amount:IC_income:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21221','MWST (2,4)','A','21220','L','AR_tax:AP_tax:IC_taxpart:IC_taxservice');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21224','MWST (7.6) 1/2','A','21220','L','AR_tax:AP_tax:IC_taxpart:IC_expense:IC_taxservice');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('66001','Werbeaufwand','A','66000','E','AP_amount:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21217','Privat','A','21210','L','AP');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('11101','Kasse','A','11100','A','AR_paid:AP_paid');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('50005','Leistungen von Sozialversicherung','A','50000','E','AP_amount:IC_income:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('65005','Informatikaufwand','A','65000','E','AP_amount:IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('39004','Rundungsdifferenzen','A','39000','I','AR_paid:AP_paid');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('11111','Debitoren','A','11110','A','AR');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('61001','Miete','A','61000','E','IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('61002','Reinigung','A','61000','E','IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('61005','Übriger Raumaufwand','A','61000','E','IC_expense');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('36003','Essen','A','36000','I','IC_income');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('21231','Passive Rechnungsabgrenzung','A','21230','L','');
-INSERT INTO chart (accno,description,charttype,gifi_accno,category,link) VALUES ('67002','Produkteentwicklung','A','67000','E','');
---
-insert into tax (taxdescription,taxkey,chart_id,rate) values ('MWST 3.6%',1,(select id from chart where accno = '21222'),0.036);
-insert into tax (taxdescription,taxkey,chart_id,rate) values ('MWST 7.6%',2,(select id from chart where accno = '21223'),0.076);
-insert into tax (taxdescription,taxkey,chart_id,rate) values ('MWST 2.4%',3,(select id from chart where accno = '21221'),0.024);
-insert into tax (taxdescription,taxkey,chart_id,rate) values ('MWST 7.6% 1/2',4,(select id from chart where accno = '21224'),0.076);
---
-update defaults set inventory_accno_id = (select id from chart where accno = '11121'), income_accno_id = (select id from chart where accno = '34002'), expense_accno_id = (select id from chart where accno = '42005'), fxgain_accno_id = (select id from chart where accno = '36005'), fxloss_accno_id = (select id from chart where accno = '39006'), invnumber = '2002000', sonumber = '2002000', ponumber = '2002000', curr = 'EUR:USD', weightunit = 'kg';
---
diff --git a/sql/Switzerland-deutsch-MWST-2014-chart.sql b/sql/Switzerland-deutsch-MWST-2014-chart.sql
new file mode 100644 (file)
index 0000000..1724b5c
--- /dev/null
@@ -0,0 +1,347 @@
+-- deutschsprachiger Kontenplan nach Schweizer Kontenrahmen KMU für Firmen in der Schweiz, die mehrwertsteuerpflichtig sind
+-- Erstellt am 4.6.2014
+-- Korrigiert: November 2015 und Juli 2017
+-- Grundlage: Revision OR Stand 1.1.2013, insbesondere Art. 957a Abs. 2
+-- Redaktion: revamp-it, http://www.revamp-it.ch
+-- Copyright 2014,2015,2017
+
+-- This file is part of kivitendo.
+-- kivitendo is free software; you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation; either version 2 of the License, or
+-- (at your option) any later version.
+-- 
+-- kivitendo is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with kivitendo. If not, see <http://www.gnu.org/licenses/>.
+
+-- Diese Datei ist Teil von kivitendo.
+--
+-- kivitendo ist Freie Software: Sie können es unter den Bedingungen
+-- der GNU General Public License, wie von der Free Software Foundation,
+-- Version 2 der Lizenz oder (nach Ihrer Wahl) jeder späteren
+-- veröffentlichten Version, weiterverbreiten und/oder modifizieren.
+--
+-- kivitendo wird in der Hoffnung, dass es nützlich sein wird, aber
+-- OHNE JEDE GEWÄHRLEISTUNG, bereitgestellt; sogar ohne die implizite
+-- Gewährleistung der MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK.
+-- Siehe die GNU General Public License für weitere Details.
+--
+-- Sie sollten eine Kopie der GNU General Public License zusammen mit diesem
+-- Programm erhalten haben. Wenn nicht, siehe <http://www.gnu.org/licenses/>.
+
+DELETE FROM chart;
+
+INSERT INTO chart (accno, description, charttype, category, link, gifi_accno, taxkey_id, pos_ustva, pos_bwa, pos_bilanz, pos_eur, datevautomatik, valid_from) VALUES
+('1',    'AKTIVEN','H','','','1',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('10',   'UMLAUFSVERMÖGEN','H','','','10',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('100',  'Flüssige Mittel','H','','','100',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1000', 'Kasse','A','A','AR_paid:AP_paid','1000',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1020', 'Postfinance oder Bank1','A','A','AR_paid:AP_paid','1020',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1021', 'Bank2','A','A','AR_paid:AP_paid','1021',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('106',  'Kurzfristig gehaltene Aktiven mit Börsenkurs','H','','','106',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('110',  'Forderungen aus Lieferungen und Leistungen','H','','','110',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1100', 'Forderungen aus Lieferungen und Leistungen','A','A','AR','1100',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('114',  'Übrige kurzfristige Forderungen','H','','','114',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1140', 'Vorschüsse, kurzfristige Darlehen','A','A','AR','1140',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1170', 'Vorsteuer auf Aufwand','A','A','AP_tax:IC_taxpart:IC_taxservice','1170',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1171', 'Vorsteuer auf Investitionen','A','A','AP_tax:IC_taxpart:IC_taxservice','1171',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1176', 'Verrechnungssteuer','A','A','','1176',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('120',  'Vorräte und nicht fakturierte Dienstleistungen','H','','','120',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1200', 'Handelswaren','A','A','IC','1200',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1210', 'Rohstoffe','A','A','IC','1210',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1280', 'Nicht fakturierte Dienstleistungen','A','A','IC','1280',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1290', 'Angefangene Arbeiten','A','A','IC','1290',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('130',  'Aktive Rechnungsabgrenzungen','H','','','130',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1300', 'Aktive Rechnungsabgrenzungen','A','A','','1300',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('14',   'ANLAGEVERMÖGEN','H','','','14',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('140',  'Finanzanlagen','H','','','140',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('148',  'Beteiligungen','H','','','148',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('150',  'Mobile Sachanlagen','H','','','150',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1500', 'Maschinen und Apparate','A','A','IC','1500',6,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1510', 'Mobiliar und Einrichtungen','A','A','IC','1510',6,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1520', 'Büromaschinen, Informatik','A','A','IC','1520',6,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1530', 'Fahrzeuge','A','A','IC','1530',6,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1540', 'Werkzeuge und Geräte','A','A','IC','1540',6,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('160',  'Immobile Sachanlagen','H','','','160',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('170',  'Immaterielle Werte','H','','','170',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('180',  'Nicht einbezahltes Grund- Gesellschafter- oder Stiftungskapital','H','','','180',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2',    'PASSIVEN','H','','','2',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('20',   'KURZFRISTIGES FREMDKAPITAL','H','','','20',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('200',  'Verbindlichkeiten aus Lieferungen und Leistungen','H','','','200',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2000', 'Verbindlichkeiten aus Lieferungen und Leistungen','A','L','AP','2000',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2001', 'Übrige Kreditoren','A','L','AP','2001',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2030', 'Anzahlungen von Kundinnen und Kunden','A','L','AR_paid:AP_paid','2030',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('210',  'Kurzfristige verzinsliche Verbindlichkeiten','H','','','210',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2100', 'Bankverbindlichkeiten','A','L','AR_paid:AP_paid','2100',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2140', 'Übrige verzinsliche Verbindlichkeiten','A','L','AP','2140',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('220',  'Übrige kurzfristige Verbindlichkeiten','H','','','220',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2200', 'Geschuldete MWST(8%)','A','L','AR_tax:IC_taxpart:IC_taxservice','2200',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2201', 'Geschuldete MWST(2.5%)','A','L','AR_tax:IC_taxpart:IC_taxservice','2201',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2206', 'Verrechnungssteuer','A','L','AP','2206',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2210', 'Geschuldete Steuern','A','L','AP','2210',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2250', 'Personalaufwand','A','L','AP','2250',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2270', 'Verbindlichkeiten Sozialversicherungen und Vorsorgeeinrichtungen','A','L','AP','2270',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('230',  'Passive Rechnungsabgrenzungen und kurzfristige Rückstellungen','H','','','230',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2300', 'Passive Rechnungsabgrenzungen','A','L','','2300',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2330', 'Kurzfristige Rückstellungen','A','L','','2330',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('24',   'LANGFRISTIGES FREMDKAPITAL','H','','','24',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('240',  'Langfristige verzinsliche Verbindlichkeiten','H','','','240',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2400', 'Bankverbindlichkeiten','A','L','','2400',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2450', 'Langfristige verzinsliche Darlehen','A','L','','2450',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('250',  'Übrige langfristige Verbindlichkeiten','H','','','250',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2500', 'Zinslose Darlehen','A','L','','2500',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('260',  'Rückstellungen sowie vom Gesetz vorgesehene ähnliche Positionen','H','','','260',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('28',   'EIGENKAPITAL','H','','','28',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('280',  'Grund-, Gesellschafter- oder Stiftungskapital','H','','','280',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2800', 'Stammkapital','A','Q','','2800',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('290',  'Reserven, Jahresgewinn oder Jahresverlust','H','','','290',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2900', 'Gesetzliche Kapitalreserve','A','Q','','2900',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2950', 'Gesetzliche Gewinnreserve','A','Q','','2950',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2960', 'Freiwillige Gewinnreserve','A','Q','','2960',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2970', 'Gewinn- oder Verlustvortrag','A','Q','','2970',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2979', 'Jahresgewinn oder -verlust','A','Q','','2979',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2980', 'Eigene Kapitalanteile','A','Q','','2980',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('3',    'BETRIEBLICHER ERTRAG AUS LIEFERUNGEN UND LEISTUNGEN','H','','','3',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('30',   'PRODUKTIONSERLÖSE','H','','','30',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('3000', 'Produktionserlöse','A','I','AR_amount:IC_sale','3000',2,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('32',   'HANDELSERLÖSE','H','','','32',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('3200', 'Handelserlöse 8%','A','I','AR_amount:IC_sale','3200',2,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('3201', 'Handelserlöse 2.5%','A','I','AR_amount:IC_sale','3201',3,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('3202', 'Handelserlöse 0%','A','I','AR_amount:IC_sale','3202',1,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('34',   'DIENSTLEISTUNGSERLÖSE','H','','','34',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('3400', 'Dienstleistungserlöse','A','I','AR_amount:IC_income','3400',2,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('36',   'ÜBRIGE ERLÖSE AUS LIEFERUNGEN UND LEISTUNGEN','H','','','36',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('3600', 'Übrige Erlöse aus Lieferungen und Leistungen','A','I','IC_sale:IC_income','3600',2,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('37',   'EIGENLEISTUNGEN UND EIGENVERBRAUCH','H','','','37',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('3700', 'Eigenleistungen','A','I','','3700',2,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('38',   'ERLÖSMINDERUNGEN','H','','','38',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('3800', 'Skonti','A','E','AR_paid','3800',2,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('3801', 'Rabatte, Preisnachlässe','A','E','AR_paid','3801',2,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('3805', 'Verluste aus Forderungen','A','E','AR_paid','3805',2,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('3809', 'MWST - nur Saldosteuersatz','A','E','AR_paid','3809',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('39',   'BESTANDESÄNDERUNGEN AN UNFERTIGEN UND FERTIGEN ERZEUGNISSEN SOWIE AN NICHT FAKTURIERTEN DIENSTLEISTUNGEN','H','','','39',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('3900', 'Bestandesänderungen','A','I','','3900',2,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('4',    'AUFWAND FÜR MATERIAL, HANDELSWAREN, DIENSTLEISTUNGEN UND ENERGIE','H','','','4',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('40',   'MATERIALAUFWAND','H','','','40',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('4000', 'Materialeinkauf','A','E','AP_amount:IC_cogs','4000',4,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('42',   'HANDELSWARENAUFWAND','H','','','42',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('4200', 'Einkauf Handelswaren 8%','A','E','AP_amount:IC_cogs','4200',4,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('4201', 'Einkauf Handelswaren 2.5%','A','E','AP_amount:IC_cogs','4201',5,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('4202', 'Einkauf Handelswaren 0%','A','E','AP_amount:IC_cogs','4202',1,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('4208', 'Bestandsänderungen Handelswaren','A','E','AP_amount:IC_cogs','4208',4,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('44',   'AUFWAND FÜR BEZOGENE DRITTLEISTUNGEN','H','','','44',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('4400', 'Aufwand für Drittleistungen','A','E','AP_amount:IC_expense','4400',4,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('45',   'ENERGIEAUFWAND ZUR LEISTUNGSERSTELLUNG','H','','','45',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('4500', 'Energieaufwand zur Leistungserstellung','A','E','AP_amount:IC_expense','4500',4,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('46',   'ÜBRIGER AUFWAND FÜR MATERIAL, HANDELSWAREN UND DIENSTLEISTUNGEN','H','','','46',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('47',   'DIREKTE EINKAUFSSPESEN','H','','','47',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('4700', 'Einkaufsspesen','A','E','AP_amount:IC_expense','4700',4,NULL,NULL,NULL,6, FALSE,'2011-01-01 00:00:00.000000'),
+('48',   'BESTANDESÄNDERUNGEN UND MATERIAL-/WARENVERLUSTE','H','','','48',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('4800', 'Bestandesänderungen','A','E','','4800',4,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('49',   'EINKAUFSPREISMINDERUNGEN','H','','','49',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('4900', 'Skonti, Rabatte, Preisnachlässe','A','I','AP_paid','4900',4,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5',    'PERSONALAUFWAND','H','','','5',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('500',  'Löhne und Gehälter','H','','','500',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('5000', 'Löhne und Gehälter','A','E','IC_expense','5000',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5001', 'Erfolgsbeteiligungen','A','E','IC_expense','5001',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5005', 'Leistungen von Sozialversicherungen','A','I','IC_income','5005',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('57',   'SOZIALVERSICHERUNGSAUFWAND','H','','','57',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('5700', 'AHV, IV, EO, ALV','A','E','AP_amount:IC_expense','5700',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5710', 'FAK','A','E','AP_amount:IC_expense','5710',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5720', 'Berufliche Vorsorge','A','E','AP_amount:IC_expense','5720',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5730', 'Unfallversicherung','A','E','AP_amount:IC_expense','5730',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5740', 'Krankentaggeldversicherung','A','E','AP_amount:IC_expense','5740',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5790', 'Quellensteuer','A','E','AP_amount:IC_expense','5790',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('58',   'ÜBRIGER PERSONALAUFWAND','H','','','58',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('5800', 'Aufwand für Personaleinstellung','A','E','IC_expense','5800',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5810', 'Weiterbildungskosten','A','E','IC_expense','5810',1,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5830', 'Spesen','A','E','IC_expense','5830',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5880', 'Sonstiger Personalaufwand','A','E','IC_expense','5880',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('59',   'LEISTUNGEN DRITTER','H','','','59',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6',    'ÜBRIGER BETRIEBLICHER AUFWAND, ABSCHREIBUNGEN UND WERTBERICHTIGUNGEN SOWIE FINANZERGEBNIS','H','','','6',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('60',   'RAUMAUFWAND','H','','','60',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6000', 'Miete','A','E','AP_amount:IC_expense','6000',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6040', 'Reinigung','A','E','AP_amount:IC_expense','6040',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6050', 'Übriger Raumaufwand','A','E','AP_amount:IC_expense','6050',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('61',   'UNTERHALT, REPARATUREN, ERSATZ, LEASING, MOBILE SACHANLAGEN','H','','','61',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6100', 'Unterhalt','A','E','AP_amount:IC_expense','6100',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('62',   'FAHRZEUG- UND TRANSPORTAUFWAND','H','','','62',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6200', 'Fahrzeugaufwand','A','E','AP_amount:IC_expense','6200',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6201', 'Transportaufwand','A','E','AP_amount:IC_expense','6201',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('63',   'SACHVERSICHERUNGEN, ABGABEN, GEBÜHREN, BEWILLIGUNGEN','H','','','63',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6300', 'Betriebsversicherungen','A','E','AP_amount:IC_expense','6300',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6360', 'Abgaben, Gebühren und Bewilligungen','A','E','AP_amount:IC_expense','6360',1,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('64',   'ENERGIE- UND ENTSORGUNGSAUFWAND','H','','','64',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6400', 'Strom, Gas, Wasser','A','E','AP_amount:IC_expense','6400',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6460', 'Entsorgungsaufwand','A','E','AP_amount:IC_expense','6460',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('65',   'VERWALTUNGS- UND INFORMATIKAUFWAND','H','','','65',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6500', 'Büromaterial, Drucksachen','A','E','AP_amount:IC_expense','6500',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6503', 'Fachliteratur','A','E','AP_amount:IC_expense','6503',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6510', 'Telefon, Fax, Porti Internet','A','E','AP_amount:IC_expense','6510',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6520', 'Beiträge, Spenden','A','E','AP_amount:IC_expense','6520',1,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6530', 'Buchführungs- und Beratungsaufwand','A','E','AP_amount:IC_expense','6530',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6540', 'Verwaltungsrat, GV, Revision','A','E','AP_amount:IC_expense','6540',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6570', 'Informatikaufwand','A','E','AP_amount:IC_expense','6570',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6590', 'Übriger Verwaltungsaufwand','A','E','AP_amount:IC_expense','6590',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('66',   'WERBEAUFWAND','H','','','66',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6600', 'Werbeaufwand','A','E','AP_amount:IC_expense','6600',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('67',   'SONSTIGER BETRIEBLICHER AUFWAND','H','','','67',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6720', 'Forschung und Entwicklung','A','E','AP_amount:IC_expense','6720',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6790', 'Übriger Betriebsaufwand','A','E','AP_amount:IC_expense','6790',6,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('68',   'ABSCHREIBUNGEN UND WERTBERICHTIGUNGEN AUF POSITIONEN DES ANLAGEVERMÖGENS','H','','','68',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6800', 'Abschreibungen Finanzanlagen','A','E','','6800',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6810', 'Abschreibungen Beteiligungen','A','E','','6810',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6820', 'Abschreibungen mobile Sachanlagen','A','E','','6820',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6840', 'Abschreibungen immaterielle Anlagen','A','E','','6840',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('69',   'FINANZAUFWAND UND FINANZERTRAG','H','','','69',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('690',  'Finanzaufwand','H','','','690',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6900', 'Finanzaufwand','A','E','','6900',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6940', 'Bankspesen','A','E','','6940',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6942', 'Kursverluste','A','E','','6942',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6943', 'Rundungsaufwand','A','E','AP_amount:IC_cogs:IC_expense','6943',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('695',  'Finanzertrag','H','','','695',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6950', 'Finanzertrag','A','I','','6950',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('6952', 'Kursgewinne','A','I','','6952',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('6953', 'Rundungsertrag','A','I','AR_amount:IC_sale:IC_income','6953',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('6970', 'Mitgliederbeiträge','A','I','AR_amount:IC_income','6970',1,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('6980', 'Spenden','A','I','AR_amount:IC_income','6980',1,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('7',    'BETRIEBLICHER NEBENERFOLG','H','','','7',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('70',   'ERFOLG AUS NEBENBETRIEBEN','H','','','70',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('75',   'ERFOLG AUS BETRIEBLICHEN LIEGENSCHAFTEN','H','','','75',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('8',    'BETRIEBSFREMDER, AUSSERORDENTLICHER, EINMALIGER UND PERIODENFREMDER AUFWAND UND ERTRAG','H','','','8',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('80',   'BETRIEBSFREMDER AUFWAND UND BETRIEBSFREMDER ERTRAG','H','','','80',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('8000', 'Betriebsfremder Aufwand','A','E','AP_amount:IC_cogs:IC_expense','8000',4,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('8100', 'Betriebsfremder Ertrag','A','I','AR_amount:IC_sale:IC_income','8100',2,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('85',   'AUSSERORDENTLICHER, EINMALIGER AUFWAND UND ERTRAG','H','','','85',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('8500', 'Ausserordentlicher Aufwand','A','E','AP_amount:IC_cogs:IC_expense','8500',4,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('8510', 'Ausserordentlicher Ertrag','A','I','AR_amount:IC_sale:IC_income','8510',2,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('87',   'PERIODENFREMDER AUFWAND UND ERTRAG','H','','','87',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('8700', 'Periodenfremder Aufwand','A','E','AP_amount:IC_cogs:IC_expense','8700',4,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('8710', 'Periodenfremder Ertrag','A','I','AR_amount:IC_sale:IC_income','8710',2,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('89',   'DIREKTE STEUERN','H','','','89',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('8900', 'Direkte Steuern','A','E','','8900',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('9',    'ABSCHLUSS','H','','','9',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('90',   'ERFOLGSRECHNUNG','H','','','90',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('91',   'BILANZ','H','','','91',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('9100', 'Eröffnungsbilanz','A','E','','9100',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('92',   'GEWINNVERWENDUNG','H','','','92',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('95',   'JAHRESERGEBNISSE','H','','','95',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('99',   'HILFSKONTEN NEBENBÜCHER','H','','','99',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000');
+
+
+DELETE FROM buchungsgruppen;
+
+INSERT INTO buchungsgruppen (
+  description, inventory_accno_id,
+  income_accno_id_0, expense_accno_id_0,
+  income_accno_id_1, expense_accno_id_1,
+  income_accno_id_2, expense_accno_id_2,
+  income_accno_id_3, expense_accno_id_3
+) VALUES (
+  'Standard 8%',(SELECT id FROM chart WHERE accno = '1200'),
+  (SELECT id FROM chart WHERE accno = '3200'), (SELECT id FROM chart WHERE accno = '4200'),
+  (SELECT id FROM chart WHERE accno = '3202'), (SELECT id FROM chart WHERE accno = '4202'),
+  (SELECT id FROM chart WHERE accno = '3202'), (SELECT id FROM chart WHERE accno = '4202'),
+  (SELECT id FROM chart WHERE accno = '3202'), (SELECT id FROM chart WHERE accno = '4202')
+),(
+  'Standard 2.5%',(SELECT id FROM chart WHERE accno = '1200'),
+  (SELECT id FROM chart WHERE accno = '3201'), (SELECT id FROM chart WHERE accno = '4201'),
+  (SELECT id FROM chart WHERE accno = '3202'), (SELECT id FROM chart WHERE accno = '4202'),
+  (SELECT id FROM chart WHERE accno = '3202'), (SELECT id FROM chart WHERE accno = '4202'),
+  (SELECT id FROM chart WHERE accno = '3202'), (SELECT id FROM chart WHERE accno = '4202')
+);
+
+
+DELETE FROM tax_zones;
+
+INSERT INTO tax_zones (id, description) VALUES
+(0, 'Schweiz'), -- siehe auch Pg-upgrade2/taxzone_id_in_oe_delivery_orders.sql )=:
+(1, 'EU mit USt-ID Nummer'),
+(2, 'EU ohne USt-ID Nummer'),
+(3, 'Ausserhalb EU');
+
+
+DELETE FROM tax;
+
+INSERT INTO tax (taxkey, taxdescription, rate) VALUES
+(0, 'Keine Steuer', 0),
+(1, 'Mehrwertsteuerfrei', 0);
+
+INSERT INTO tax (taxkey, taxdescription, rate, taxnumber, chart_id) VALUES
+(2, 'MWST', 0.08000, '2200', (SELECT id FROM chart WHERE accno='2200')),
+(3, 'MWST', 0.02500, '2201', (SELECT id FROM chart WHERE accno='2201')),
+(4, 'MWST Aufwand', 0.08000, '1170', (SELECT id FROM chart WHERE accno='1170')),
+(5, 'MWST Aufwand', 0.02500, '1170', (SELECT id FROM chart WHERE accno='1170')),
+(6, 'MWST Investitionen', 0.08000, '1171', (SELECT id FROM chart WHERE accno='1171')),
+(7, 'MWST Investitionen', 0.02500, '1171', (SELECT id FROM chart WHERE accno='1171'));
+
+
+DELETE FROM taxkeys;
+
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 0, (SELECT tax.id FROM tax WHERE taxkey=0), chart.id, '2011-01-01' FROM chart WHERE taxkey_id=0;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 1, (SELECT tax.id FROM tax WHERE taxkey=1), chart.id, '2011-01-01' FROM chart WHERE taxkey_id=1;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 2, (SELECT tax.id FROM tax WHERE taxkey=2), chart.id, '2011-01-01' FROM chart WHERE taxkey_id=2;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 3, (SELECT tax.id FROM tax WHERE taxkey=3), chart.id, '2011-01-01' FROM chart WHERE taxkey_id=3;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 4, (SELECT tax.id FROM tax WHERE taxkey=4), chart.id, '2011-01-01' FROM chart WHERE taxkey_id=4;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 5, (SELECT tax.id FROM tax WHERE taxkey=5), chart.id, '2011-01-01' FROM chart WHERE taxkey_id=5;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 6, (SELECT tax.id FROM tax WHERE taxkey=6), chart.id, '2011-01-01' FROM chart WHERE taxkey_id=6;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 7, (SELECT tax.id FROM tax WHERE taxkey=7), chart.id, '2011-01-01' FROM chart WHERE taxkey_id=7;
+
+
+DELETE FROM units;
+
+INSERT INTO units (name, base_unit, factor, type) VALUES
+('Stck', NULL, 0.00000, 'dimension'),
+('mg', NULL, 0.00000, 'dimension'),
+('g', 'mg', 1000.00000, 'dimension'),
+('kg', 'g', 1000.00000, 'dimension'),
+('t', 'kg', 1000.00000, 'dimension'),
+('ml', NULL, 0.00000, 'dimension'),
+('L', 'ml', 1000.00000, 'dimension'),
+('pauschal', NULL, 0.00000, 'service'),
+('Min', NULL, 0.00000, 'service'),
+('Std', 'Min', 60.00000, 'service'),
+('Tag', 'Std', 8.00000, 'service'),
+('Wo', NULL, 0.00000, 'service'),
+('Mt', 'Wo', 4.00000, 'service'),
+('Jahr', 'Mt', 12.00000, 'service');
+
+
+DELETE FROM defaults;
+
+INSERT INTO defaults (
+  inventory_accno_id,
+  income_accno_id, expense_accno_id,
+  fxgain_accno_id, fxloss_accno_id,
+  invnumber, sonumber,
+  weightunit,
+  businessnumber,
+  version,
+  closedto,
+  revtrans,
+  ponumber, sqnumber, rfqnumber,
+  customernumber, vendornumber,
+  audittrail,
+  articlenumber, servicenumber,
+  rmanumber, cnnumber
+) VALUES (
+  (SELECT id FROM CHART WHERE accno='1200'),
+  (SELECT id FROM CHART WHERE accno='3200'), (SELECT id FROM CHART WHERE accno='4200'),
+  (SELECT id FROM CHART WHERE accno='6952'), (SELECT id FROM CHART WHERE accno='6942'),
+  0, 0,
+  'kg',
+  '',
+  '3.1.0 CH',
+  NULL,
+  FALSE,
+  0, 0, 0,
+  0, 0,
+  FALSE,
+  0, 0,
+  0, 0
+);
diff --git a/sql/Switzerland-deutsch-Verein-2017-chart.sql b/sql/Switzerland-deutsch-Verein-2017-chart.sql
new file mode 100644 (file)
index 0000000..936e580
--- /dev/null
@@ -0,0 +1,372 @@
+-- deutschsprachiger Kontenplan nach Schweizer Kontenrahmen KMU spezifisch angepasst für mehrwertsteuerpflichtige Vereine in der Schweiz
+-- für kivitendo aufbereitet am 11.4.2017, kleinere Korrekturen am 15.7.2017
+-- Grundlage: Revision OR Stand 1.1.2013, insbesondere Art. 957a Abs. 2
+-- Autor: Kurt Pfister, http://www.gotransparent.ch
+-- Redaktion: Andreas Rudin, http://www.revamp-it.ch
+-- Copyright 2014,2017 Kurt Pfister, Andreas Rudin
+
+-- This file is part of kivitendo.
+-- kivitendo is free software; you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation; either version 2 of the License, or
+-- (at your option) any later version.
+-- 
+-- kivitendo is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with kivitendo. If not, see <http://www.gnu.org/licenses/>.
+
+-- Diese Datei ist Teil von kivitendo.
+--
+-- kivitendo ist Freie Software: Sie können es unter den Bedingungen
+-- der GNU General Public License, wie von der Free Software Foundation,
+-- Version 2 der Lizenz oder (nach Ihrer Wahl) jeder späteren
+-- veröffentlichten Version, weiterverbreiten und/oder modifizieren.
+--
+-- kivitendo wird in der Hoffnung, dass es nützlich sein wird, aber
+-- OHNE JEDE GEWÄHRLEISTUNG, bereitgestellt; sogar ohne die implizite
+-- Gewährleistung der MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK.
+-- Siehe die GNU General Public License für weitere Details.
+--
+-- Sie sollten eine Kopie der GNU General Public License zusammen mit diesem
+-- Programm erhalten haben. Wenn nicht, siehe <http://www.gnu.org/licenses/>. 
+
+DELETE FROM chart;
+
+INSERT INTO chart (accno, description, charttype, category, link, gifi_accno, taxkey_id, pos_ustva, pos_bwa, pos_bilanz, pos_eur, datevautomatik, valid_from) VALUES
+('1',    'AKTIVEN','H','','','1',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('10',   'UMLAUFVERMÖGEN','H','','','10',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('100',  'Flüssige Mittel','H','','','100',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1000', 'Kasse','A','A','AR_paid:AP_paid','1000',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1010', 'Postfinance','A','A','AR_paid:AP_paid','1010',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1020', 'Bank','A','A','AR_paid:AP_paid','1020',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('106',  'Kurzfristig gehaltene Aktiven mit Börsenkurs','H','','','106',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1060', 'Wertschriften','A','A','','1060',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('110',  'Forderungen aus Lieferungen und Leistungen','H','','','110',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1100', 'Debitoren CHF','A','A','AR','1100',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1101', 'Debitoren EUR','A','A','AR','1101',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1107', 'Forderungen an Kreditkarten','A','A','','1107',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1109', 'Delkredere','A','A','','1109',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('114',  'Übrige kurzfristige Forderungen','H','','','114',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1140', 'Vorschüsse, kurzfristige Darlehen','A','A','','1140',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1160', 'Kontokorrent Mitglied/Vorstand/GL','A','A','','1160',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1170', 'MWST Vorsteuer Material/Dienstleistungen','A','A','AP_tax:IC_taxpart:IC_taxservice','1170',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1171', 'MWST Vorsteuer Investitionen/übr. Betriebsaufwand','A','A','AP_tax:IC_taxpart:IC_taxservice','1171',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1176', 'Verrechnungssteuer','A','A','','1176',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1181', 'Familienzulagen','A','A','','1181',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1191', 'Kautionen','A','A','','1191',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1192', 'Geleistete Anzahlungen','A','A','','1192',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('120',  'Vorräte und nicht fakturierte Dienstleistungen','H','','','120',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1200', 'Handelswaren','A','A','IC','1200',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1209', 'Wertberichtigung Handelswaren','A','A','','1209',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1210', 'Rohstoffe','A','A','IC','1210',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1219', 'Wertberichtigung Rohstoffe','A','A','','1219',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1260', 'Fertige Erzeugnisse','A','A','IC','1260',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1269', 'Wertberichtigung fertige Erzeugnisse','A','A','','1269',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1280', 'Nicht fakturierte Dienstleistungen','A','A','IC','1280',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('130',  'Aktive Rechnungsabgrenzung','H','','','130',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1300', 'Bezahlter Aufwand des Folgejahrs','A','A','','1300',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1301', 'Noch nicht erhaltener Ertrag','A','A','','1301',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('14',   'ANLAGEVERMÖGEN','H','','','14',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('140',  'Finanzanlagen','H','','','140',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1400', 'Wertschriften','A','A','IC','1400',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1411', 'Mietzinsdepot','A','A','IC','1411',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1440', 'Darlehen','A','A','IC','1440',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1460', 'Darlehen an Mitglied/Vorstand/GL','A','A','IC','1460',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('148',  'Beteiligungen','H','','','148',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1480', 'Beteiligung','A','A','IC','1480',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1489', 'Wertberichtigung Beteiligung','A','A','','1489',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('150',  'Mobile Sachanlagen','H','','','150',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1500', 'Maschinen und Apparate','A','A','IC','1500',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1509', 'Wertberichtigung Maschinen und Apparate','A','A','','1509',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1510', 'Mobiliar und Einrichtungen','A','A','IC','1510',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1519', 'Wertberichtigung Mobiliar und Einrichtungen','A','A','','1519',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1520', 'Informatik und Kommunikation','A','A','IC','1520',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1529', 'Wertberichtigung Informatik und Kommunikation','A','A','','1529',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1530', 'Fahrzeuge','A','A','IC','1530',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1539', 'Wertberichtigung Fahrzeuge','A','A','','1539',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1540', 'Werkzeug und Geräte','A','A','IC','1540',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1549', 'Wertberichtigung Werkzeug und Geräte','A','A','','1549',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('160',  'Immobile Sachanlagen','H','','','160',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1600', 'Geschäftsliegenschaft','A','A','IC','1600',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1609', 'Wertberichtigung Geschäftsliegenschaft','A','A','','1609',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('170',  'Immaterielle Werte','H','','','170',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1700', 'Patent, Konzession, Rechte','A','A','IC','1700',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('1709', 'Wertberichtigung Patent, Konzession, Rechte','A','A','','1709',6,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2',    'PASSIVEN','H','','','2',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('20',   'KURZFRISTIGES FREMDKAPITAL','H','','','20',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('200',  'Verbindlichkeiten aus Lieferungen und Leistungen','H','','','200',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2000', 'Kreditoren CHF','A','L','AP','2000',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2001', 'Kreditoren EUR','A','L','AP','2001',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2030', 'Erhaltene Anzahlungen von Kundinnen und Kunden','A','L','','2030',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('210',  'Kurzfristige verzinsliche Verbindlichkeiten','H','','','210',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2100', 'Bank','A','L','AR_paid:AP_paid','2100',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2160', 'Verzinsliche Verbindlichkeiten gegenüber Mitglied/Vorstand/GL','A','L','','2160',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('220',  'Übrige kurzfristige Verbindlichkeiten','H','','','220',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2200', 'MWST Umsatzsteuer','A','L','AR_tax:IC_taxpart:IC_taxservice','2200',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2201', 'MWST Abrechnungskonto','A','L','','2201',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2203', 'MWST Bezugssteuer','A','L','','2203',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2206', 'Verrechnungssteuer','A','L','','2206',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2208', 'Direkte Steuern','A','L','','2208',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2210', 'Übrige kurzfristige Verbindlichkeiten','A','L','','2210',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2260', 'Verbindlichkeiten gegenüber Mitglied/Vorstand/GL','A','L','','2260',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2270', 'Kontokorrent Berufliche Vorsorge','A','L','','2270',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2271', 'Kontokorrent AHV,IV,EO,ALV','A','L','','2271',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2272', 'Kontokorrent FAK','A','L','','2272',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2273', 'Kontokorrent Unfallversicherung','A','L','','2273',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2274', 'Kontokorrent Krankentaggeldversicherung','A','L','','2274',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2279', 'Kontokorrent Quellensteuer','A','L','','2279',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('230',  'Passive Rechnungsabgrenzung','H','','','230',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2300', 'Noch nicht bezahlter Aufwand','A','L','','2300',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2301', 'Erhaltener Ertrag des Folgejahrs','A','L','','2301',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('24',   'LANGFRISTIGES FREMDKAPITAL','H','','','24',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('240',  'Langfristige verzinsliche Verbindlichkeiten','H','','','240',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2401', 'Hypothek','A','L','','2401',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2450', 'Verzinsliche Darlehen','A','L','','2450',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('250',  'Übrige langfristige Verbindlichkeiten','H','','','250',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2500', 'Zinslose Darlehen','A','L','','2500',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2560', 'Darlehen von Mitglied/Vorstand/GL','A','L','','2560',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('260',  'Rückstellungen langfristig','H','','','260',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2630', 'Rückstellung für Garantieverpflichtungen','A','L','','2630',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('28',   'EIGENKAPITAL','H','','','28',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('280',  'Organisationskapital','H','','','280',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2800', 'Vereinsvermögen','A','Q','','2800',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('290',  'Jahresgewinn oder Jahresverlust','H','','','290',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('2979', 'Jahresgewinn oder Jahresverlust','A','Q','','2979',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('3',    'BETRIEBSERTRAG','H','','','3',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('30',   'PRODUKTIONSERLÖS','H','','','30',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('3000', 'Produktionserlös','A','I','AR_amount:IC_sale','3000',2,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('32',   'HANDELSERLÖSE','H','','','32',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('3200', 'Handelserlöse','A','I','AR_amount:IC_sale','3200',2,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('34',   'DIENSTLEISTUNGSERLÖSE','H','','','34',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('3400', 'Dienstleistungserlöse','A','I','AR_amount:IC_income','3400',2,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('36',   'ÜBRIGE ERLÖSE','H','','','36',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('3600', 'Spenden','A','I','AR_amount:IC_income','3600',1,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('3660', 'Mitgliederbeiträge','A','I','AR_amount:IC_income','3660',1,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('3680', 'Sonstige Erlöse','A','I','AR_amount:IC_sale:IC_income','3680',2,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('37',   'EIGENLEISTUNGEN UND EIGENVERBRAUCH','H','','','37',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('3700', 'Eigenleistungen','A','I','','3700',2,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('38',   'ERLÖSMINDERUNGEN','H','','','38',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('3800', 'Skonti','A','E','AR_paid','3800',2,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('3801', 'Rabatte, Preisnachlässe','A','E','AR_paid','3801',2,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('3805', 'Verluste aus Forderungen, Veränderungen Delkredere','A','E','AR_paid','3805',2,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('3809', 'MWST Saldosteuersatz','A','E','AR_paid','3809',0,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('39',   'BESTANDESÄNDERUNGEN','H','','','39',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('3901', 'Bestandesänderung fertige Erzeugnisse','A','I','','3901',2,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('3940', 'Bestandesänderung nicht fakturierte Dienstleistungen','A','I','','3940',2,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('4',    'AUFWAND FÜR LEISTUNGSERSTELLUNG','H','','','4',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('40',   'MATERIALAUFWAND','H','','','40',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('4000', 'Materialeinkauf','A','E','AP_amount:IC_cogs','4000',4,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('42',   'HANDELSWARENAUFWAND','H','','','42',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('4200', 'Einkauf Handelswaren','A','E','AP_amount:IC_cogs','4200',4,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('44',   'AUFWAND FÜR BEZOGENE DRITTLEISTUNGEN','H','','','44',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('4400', 'Aufwand für Drittleistungen','A','E','AP_amount:IC_expense','4400',4,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('45',   'ENERGIEAUFWAND ZUR LEISTUNGSERSTELLUNG','H','','','45',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('4500', 'Energieaufwand zur Leistungserstellung','A','E','AP_amount:IC_expense','4500',4,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('48',   'BESTANDESÄNDERUNGEN','H','','','48',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('4800', 'Bestandesänderungen','A','E','','4800',4,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('49',   'EINKAUFSPREISMINDERUNGEN','H','','','49',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('4900', 'Skonti, Rabatte, Preisnachlässe','A','I','AP_paid','4900',4,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('5',    'PERSONALAUFWAND','H','','','5',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('50',   'LÖHNE UND GEHÄLTER','H','','','500',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('5000', 'Bruttolohn','A','E','IC_expense','5000',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('5002', 'Erfolgsbeteiligung','A','E','IC_expense','5002',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('5005', 'Leistungen von Sozialversicherungen','A','I','IC_income','5005',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('57',   'SOZIALVERSICHERUNGSAUFWAND','H','','','57',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('5700', 'AHV, IV, EO, ALV','A','E','AP_amount:IC_expense','5700',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('5710', 'FAK','A','E','AP_amount:IC_expense','5710',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('5720', 'Berufliche Vorsorge','A','E','AP_amount:IC_expense','5720',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('5730', 'Unfallversicherung','A','E','AP_amount:IC_expense','5730',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('5740', 'Krankentaggeldversicherung','A','E','AP_amount:IC_expense','5740',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('5790', 'Quellensteuer','A','E','AP_amount:IC_expense','5790',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('58',   'ÜBRIGER PERSONALAUFWAND','H','','','58',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('5800', 'Aufwand für Personaleinstellung','A','E','IC_expense','5800',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('5810', 'Aus- und Weiterbildung','A','E','IC_expense','5810',1,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('5830', 'Spesenentschädigung','A','E','IC_expense','5830',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('5880', 'Sonstiger Personalaufwand','A','E','IC_expense','5880',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('59',   'LEISTUNGEN DRITTER','H','','','59',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('5900', 'Fremdarbeit, Temporärpersonal','A','E','IC_expense','5900',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6',    'ÜBRIGER BETRIEBSAUFWAND','H','','','6',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('60',   'RAUMAUFWAND','H','','','60',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('6000', 'Miete','A','E','AP_amount:IC_expense','6000',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6040', 'Reinigung','A','E','AP_amount:IC_expense','6040',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6050', 'Übriger Raumaufwand','A','E','AP_amount:IC_expense','6050',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('61',   'UNTERHALT, REPARATUREN, ERSATZ, LEASING','H','','','61',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('6100', 'Unterhalt Maschinen und Apparate','A','E','AP_amount:IC_expense','6100',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6110', 'Unterhalt Mobiliar und Einrichtungen','A','E','AP_amount:IC_expense','6110',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6120', 'Unterhalt Informatik und Kommunikation','A','E','AP_amount:IC_expense','6120',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6140', 'Unterhalt Werkzeug und Geräte','A','E','AP_amount:IC_expense','6140',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6150', 'Leasing','A','E','AP_amount:IC_expense','6150',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('62',   'FAHRZEUG- UND TRANSPORTAUFWAND','H','','','62',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('6200', 'Fahrzeugaufwand','A','E','AP_amount:IC_expense','6200',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6201', 'Transportaufwand','A','E','AP_amount:IC_expense','6201',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('63',   'SACHVERSICHERUNGEN, ABGABEN, GEBÜHREN','H','','','63',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('6300', 'Betriebsversicherung','A','E','AP_amount:IC_expense','6300',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6360', 'Abgaben, Gebühren und Bewilligungen','A','E','AP_amount:IC_expense','6360',1,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('64',   'ENERGIE- UND ENTSORGUNGSAUFWAND','H','','','64',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('6400', 'Strom, Gas, Wasser','A','E','AP_amount:IC_expense','6400',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6460', 'Entsorgungsaufwand','A','E','AP_amount:IC_expense','6460',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('65',   'VERWALTUNGS- UND INFORMATIKAUFWAND','H','','','65',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('6500', 'Büromaterial, Drucksachen, Fachliteratur','A','E','AP_amount:IC_expense','6500',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6510', 'Telefon, Porto, Internet','A','E','AP_amount:IC_expense','6510',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6520', 'Beiträge, Spenden','A','E','AP_amount:IC_expense','6520',1,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6530', 'Buchführungs- und Beratungsaufwand','A','E','AP_amount:IC_expense','6530',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6540', 'Vorstandsentschädigung, GV, Revision','A','E','AP_amount:IC_expense','6540',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6570', 'Informatikaufwand','A','E','AP_amount:IC_expense','6570',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6590', 'Übriger Verwaltungsaufwand','A','E','AP_amount:IC_expense','6590',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('66',   'WERBEAUFWAND','H','','','66',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('6600', 'Inserate','A','E','AP_amount:IC_expense','6600',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6604', 'Website','A','E','AP_amount:IC_expense','6604',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6620', 'Schaufenster, Messen','A','E','AP_amount:IC_expense','6620',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6640', 'Reisespesen, Kundenbetreuung','A','E','AP_amount:IC_expense','6640',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6660', 'Sponsoring','A','E','AP_amount:IC_expense','6660',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('67',   'SONSTIGER BETRIEBLICHER AUFWAND','H','','','67',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('6700', 'Wirtschaftsauskunft, Betreibung','A','E','AP_amount:IC_expense','6700',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6720', 'Forschung und Entwicklung','A','E','AP_amount:IC_expense','6720',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6791', 'Privatanteil Fahrzeug','A','E','AP_amount:IC_expense','6791',6,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('68',   'ABSCHREIBUNG UND WERTBERICHTIGUNG','H','','','68',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('6800', 'Wertberichtigung Finanzanlagen','A','E','','6800',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6820', 'Abschreibung mobile Sachanlagen','A','E','','6820',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6830', 'Abschreibung immobile Sachanlagen','A','E','','6830',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6840', 'Abschreibung immaterielle Werte','A','E','','6840',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('69',   'FINANZAUFWAND UND FINANZERTRAG','H','','','69',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('690',  'Finanzaufwand','H','','','690',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('6900', 'Zinsaufwand','A','E','','6900',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6940', 'Bankspesen','A','E','','6940',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6943', 'Rundungsaufwand','A','E','','6943',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('6949', 'Währungsverluste','A','E','','6949',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('695',  'Finanzertrag','H','','','695',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('6950', 'Zinsertrag','A','I','','6950',0,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('6953', 'Rundungsertrag','A','I','','6953',0,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('6999', 'Währungsgewinne','A','I','','6999',0,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('7',    'BETRIEBLICHER NEBENERFOLG','H','','','7',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('70',   'ERFOLG AUS NEBENBETRIEBEN','H','','','70',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('75',   'ERFOLG AUS BETRIEBLICHEN LIEGENSCHAFTEN','H','','','75',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('8',    'BETRIEBSFREMDER, AUSSERORDENTLICHER, EINMALIGER UND PERIODENFREMDER ERFOLG','H','','','8',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('80',   'BETRIEBSFREMDER ERFOLG','H','','','80',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('8000', 'Betriebsfremder Aufwand','A','E','AP_amount:IC_cogs:IC_expense','8000',4,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('8100', 'Betriebsfremder Ertrag','A','I','AR_amount:IC_sale:IC_income','8100',2,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('85',   'AUSSERORDENTLICHER, EINMALIGER ERFOLG','H','','','85',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('8500', 'Ausserordentlicher Aufwand','A','E','AP_amount:IC_cogs:IC_expense','8500',4,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('8510', 'Ausserordentlicher Ertrag','A','I','AR_amount:IC_sale:IC_income','8510',2,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('87',   'PERIODENFREMDER ERFOLG','H','','','87',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('8700', 'Periodenfremder Aufwand','A','E','AP_amount:IC_cogs:IC_expense','8700',4,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('8710', 'Periodenfremder Ertrag','A','I','AR_amount:IC_sale:IC_income','8710',2,NULL,NULL,NULL,1,FALSE,'2017-01-01 00:00:00.000000'),
+('89',   'DIREKTE STEUERN','H','','','89',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('8900', 'Direkte Steuern','A','E','','8900',0,NULL,NULL,NULL,6,FALSE,'2017-01-01 00:00:00.000000'),
+('9',    'ABSCHLUSS','H','','','9',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('90',   'ERFOLGSRECHNUNG','H','','','90',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('91',   'BILANZ','H','','','91',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('9100', 'Eröffnungsbilanz','A','E','','9100',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('92',   'GEWINNVERWENDUNG','H','','','92',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000'),
+('95',   'JAHRESERGEBNISSE','H','','','95',0,NULL,NULL,NULL,NULL,FALSE,'2017-01-01 00:00:00.000000');
+
+
+DELETE FROM buchungsgruppen;
+
+INSERT INTO buchungsgruppen (
+  description, inventory_accno_id,
+  income_accno_id_0, expense_accno_id_0,
+  income_accno_id_1, expense_accno_id_1,
+  income_accno_id_2, expense_accno_id_2,
+  income_accno_id_3, expense_accno_id_3
+) VALUES (
+  'Standard',(SELECT id FROM chart WHERE accno = '1200'),
+  (SELECT id FROM chart WHERE accno = '3200'), (SELECT id FROM chart WHERE accno = '4200'),
+  (SELECT id FROM chart WHERE accno = '3200'), (SELECT id FROM chart WHERE accno = '4200'),
+  (SELECT id FROM chart WHERE accno = '3200'), (SELECT id FROM chart WHERE accno = '4200'),
+  (SELECT id FROM chart WHERE accno = '3200'), (SELECT id FROM chart WHERE accno = '4200')
+);
+
+
+DELETE FROM tax_zones;
+
+INSERT INTO tax_zones (id, description) VALUES
+(0, 'Schweiz'), -- siehe auch Pg-upgrade2/taxzone_id_in_oe_delivery_orders.sql )=:
+(1, 'EU mit USt-ID Nummer'),
+(2, 'EU ohne USt-ID Nummer'),
+(3, 'Ausserhalb EU');
+
+
+DELETE FROM tax;
+
+INSERT INTO tax (taxkey, taxdescription, rate) VALUES
+(0, 'Keine Steuer', 0),
+(1, 'Mehrwertsteuerfrei', 0);
+
+INSERT INTO tax (taxkey, taxdescription, rate, taxnumber, chart_id) VALUES
+(2, 'MWST', 0.08000, '2200', (SELECT id FROM chart WHERE accno='2200')),
+(3, 'MWST', 0.02500, '2200', (SELECT id FROM chart WHERE accno='2200')),
+(4, 'MWST Aufwand', 0.08000, '1170', (SELECT id FROM chart WHERE accno='1170')),
+(5, 'MWST Aufwand', 0.02500, '1170', (SELECT id FROM chart WHERE accno='1170')),
+(6, 'MWST Investitionen', 0.08000, '1171', (SELECT id FROM chart WHERE accno='1171')),
+(7, 'MWST Investitionen', 0.02500, '1171', (SELECT id FROM chart WHERE accno='1171'));
+
+
+DELETE FROM taxkeys;
+
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 0, (SELECT tax.id FROM tax WHERE taxkey=0), chart.id, '2017-01-01' FROM chart WHERE taxkey_id=0;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 1, (SELECT tax.id FROM tax WHERE taxkey=1), chart.id, '2017-01-01' FROM chart WHERE taxkey_id=1;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 2, (SELECT tax.id FROM tax WHERE taxkey=2), chart.id, '2017-01-01' FROM chart WHERE taxkey_id=2;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 3, (SELECT tax.id FROM tax WHERE taxkey=3), chart.id, '2017-01-01' FROM chart WHERE taxkey_id=3;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 4, (SELECT tax.id FROM tax WHERE taxkey=4), chart.id, '2017-01-01' FROM chart WHERE taxkey_id=4;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 5, (SELECT tax.id FROM tax WHERE taxkey=5), chart.id, '2017-01-01' FROM chart WHERE taxkey_id=5;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 6, (SELECT tax.id FROM tax WHERE taxkey=6), chart.id, '2017-01-01' FROM chart WHERE taxkey_id=6;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 7, (SELECT tax.id FROM tax WHERE taxkey=7), chart.id, '2017-01-01' FROM chart WHERE taxkey_id=7;
+
+
+DELETE FROM units;
+
+INSERT INTO units (name, base_unit, factor, type) VALUES
+('Stck', NULL, 0.00000, 'dimension'),
+('mg', NULL, 0.00000, 'dimension'),
+('g', 'mg', 1000.00000, 'dimension'),
+('kg', 'g', 1000.00000, 'dimension'),
+('t', 'kg', 1000.00000, 'dimension'),
+('ml', NULL, 0.00000, 'dimension'),
+('L', 'ml', 1000.00000, 'dimension'),
+('pauschal', NULL, 0.00000, 'service'),
+('Min', NULL, 0.00000, 'service'),
+('Std', 'Min', 60.00000, 'service'),
+('Tag', 'Std', 8.00000, 'service'),
+('Wo', NULL, 0.00000, 'service'),
+('Mt', 'Wo', 4.00000, 'service'),
+('Jahr', 'Mt', 12.00000, 'service');
+
+
+DELETE FROM defaults;
+
+INSERT INTO defaults (
+  inventory_accno_id,
+  income_accno_id, expense_accno_id,
+  fxgain_accno_id, fxloss_accno_id,
+  invnumber, sonumber,
+  weightunit,
+  businessnumber,
+  version,
+  closedto,
+  revtrans,
+  ponumber, sqnumber, rfqnumber,
+  customernumber, vendornumber,
+  audittrail,
+  articlenumber, servicenumber,
+  rmanumber, cnnumber
+) VALUES (
+  (SELECT id FROM CHART WHERE accno='1200'),
+  (SELECT id FROM CHART WHERE accno='3200'), (SELECT id FROM CHART WHERE accno='4200'),
+  (SELECT id FROM CHART WHERE accno='6999'), (SELECT id FROM CHART WHERE accno='6949'),
+  0, 0,
+  'kg',
+  '',
+  '3.5.0 CH',
+  NULL,
+  FALSE,
+  0, 0, 0,
+  0, 0,
+  FALSE,
+  0, 0,
+  0, 0
+);
diff --git a/sql/Switzerland-deutsch-ohneMWST-2014-chart.sql b/sql/Switzerland-deutsch-ohneMWST-2014-chart.sql
new file mode 100644 (file)
index 0000000..33afb9a
--- /dev/null
@@ -0,0 +1,337 @@
+-- deutschsprachiger Kontenplan nach Schweizer Kontenrahmen KMU für Firmen in der Schweiz, die nicht mehrwertsteuerpflichtig sind
+-- Erstellt am 4.6.2014
+-- Korrigiert: November 2015 und Juli 2017
+-- Grundlage: Revision OR Stand 1.1.2013, insbesondere Art. 957a Abs. 2
+-- Redaktion: revamp-it, http://www.revamp-it.ch
+-- Copyright 2014,2015,2017
+
+-- This file is part of kivitendo.
+-- kivitendo is free software; you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation; either version 2 of the License, or
+-- (at your option) any later version.
+--
+-- kivitendo is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with kivitendo. If not, see <http://www.gnu.org/licenses/>.
+
+-- Diese Datei ist Teil von kivitendo.
+--
+-- kivitendo ist Freie Software: Sie können es unter den Bedingungen
+-- der GNU General Public License, wie von der Free Software Foundation,
+-- Version 2 der Lizenz oder (nach Ihrer Wahl) jeder späteren
+-- veröffentlichten Version, weiterverbreiten und/oder modifizieren.
+--
+-- kivitendo wird in der Hoffnung, dass es nützlich sein wird, aber
+-- OHNE JEDE GEWÄHRLEISTUNG, bereitgestellt; sogar ohne die implizite
+-- Gewährleistung der MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK.
+-- Siehe die GNU General Public License für weitere Details.
+--
+-- Sie sollten eine Kopie der GNU General Public License zusammen mit diesem
+-- Programm erhalten haben. Wenn nicht, siehe <http://www.gnu.org/licenses/>.
+
+DELETE FROM chart;
+
+INSERT INTO chart (accno, description, charttype, category, link, gifi_accno, taxkey_id, pos_ustva, pos_bwa, pos_bilanz, pos_eur, datevautomatik, valid_from) VALUES
+('1',    'AKTIVEN','H','','','1',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('10',   'UMLAUFSVERMÖGEN','H','','','10',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('100',  'Flüssige Mittel','H','','','100',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1000', 'Kasse','A','A','AR_paid:AP_paid','1000',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1020', 'Postfinance oder Bank1','A','A','AR_paid:AP_paid','1020',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1021', 'Bank2','A','A','AR_paid:AP_paid','1021',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000' ),
+('106',  'Kurzfristig gehaltene Aktiven mit Börsenkurs','H','','','106',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('110',  'Forderungen aus Lieferungen und Leistungen','H','','','110',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1100', 'Forderungen aus Lieferungen und Leistungen','A','A','AR','1100',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('114',  'Übrige kurzfristige Forderungen','H','','','114',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1140', 'Vorschüsse, kurzfristige Darlehen','A','A','AR','1140',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1170', 'Vorsteuer auf Aufwand','A','A','AP_tax:IC_taxpart:IC_taxservice','1170',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1171', 'Vorsteuer auf Investitionen','A','A','AP_tax:IC_taxpart:IC_taxservice','1171',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1176', 'Verrechnungssteuer','A','A','','1176',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('120',  'Vorräte und nicht fakturierte Dienstleistungen','H','','','120',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1200', 'Handelswaren','A','A','IC','1200',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1210', 'Rohstoffe','A','A','IC','1210',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1280', 'Nicht fakturierte Dienstleistungen','A','A','IC','1280',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1290', 'Angefangene Arbeiten','A','A','IC','1290',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('130',  'Aktive Rechnungsabgrenzungen','H','','','130',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1300', 'Aktive Rechnungsabgrenzungen','A','A','','1300',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('14',   'ANLAGEVERMÖGEN','H','','','14',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('140',  'Finanzanlagen','H','','','140',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('148',  'Beteiligungen','H','','','148',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('150',  'Mobile Sachanlagen','H','','','150',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1500', 'Maschinen und Apparate','A','A','IC','1500',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1510', 'Mobiliar und Einrichtungen','A','A','IC','1510',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1520', 'Büromaschinen, Informatik','A','A','IC','1520',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1530', 'Fahrzeuge','A','A','IC','1530',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('1540', 'Werkzeuge und Geräte','A','A','IC','1540',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('160',  'Immobile Sachanlagen','H','','','160',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('170',  'Immaterielle Werte','H','','','170',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('180',  'Nicht einbezahltes Grund- Gesellschafter- oder Stiftungskapital','H','','','180',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2',    'PASSIVEN','H','','','2',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('20',   'KURZFRISTIGES FREMDKAPITAL','H','','','20',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('200',  'Verbindlichkeiten aus Lieferungen und Leistungen','H','','','200',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2000', 'Verbindlichkeiten aus Lieferungen und Leistungen','A','L','AP','2000',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2001', 'Übrige Kreditoren','A','L','AP','2001',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2030', 'Anzahlungen von Kundinnen und Kunden','A','L','AR_paid:AP_paid','2030',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('210',  'Kurzfristige verzinsliche Verbindlichkeiten','H','','','210',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2100', 'Bankverbindlichkeiten','A','L','AR_paid:AP_paid','2100',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2140', 'Übrige verzinsliche Verbindlichkeiten','A','L','AP','2140',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('220',  'Übrige kurzfristige Verbindlichkeiten','H','','','220',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2200', 'Geschuldete MWST(2,5)','A','L','AR_tax:IC_taxpart:IC_taxservice','2200',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2201', 'Geschuldete MWST(8,0)','A','L','AR_tax:IC_taxpart:IC_taxservice','2201',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2206', 'Verrechnungssteuer','A','L','AP','2206',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2210', 'Geschuldete Steuern','A','L','AP','2210',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2250', 'Personalaufwand','A','L','AP','2250',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2270', 'Verbindlichkeiten Sozialversicherungen und Vorsorgeeinrichtungen','A','L','AP','2270',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('230',  'Passive Rechnungsabgrenzungen und kurzfristige Rückstellungen','H','','','230',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2300', 'Passive Rechnungsabgrenzungen','A','L','','2300',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2330', 'Kurzfristige Rückstellungen','A','L','','2330',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('24',   'LANGFRISTIGES FREMDKAPITAL','H','','','24',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('240',  'Langfristige verzinsliche Verbindlichkeiten','H','','','240',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2400','Bankverbindlichkeiten','A','L','','2400',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2450', 'Langfristige verzinsliche Darlehen','A','L','','2450',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('250',  'Übrige langfristige Verbindlichkeiten','H','','','250',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2500', 'Zinslose Darlehen','A','L','','2500',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('260',  'Rückstellungen sowie vom Gesetz vorgesehene ähnliche Positionen','H','','','260',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('28',   'EIGENKAPITAL','H','','','28',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('280',  'Grund-, Gesellschafter- oder Stiftungskapital','H','','','280',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2800', 'Stammkapital','A','Q','','2800',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('290',  'Reserven, Jahresgewinn oder Jahresverlust','H','','','290',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2900', 'Gesetzliche Kapitalreserve','A','Q','','2900',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2950', 'Gesetzliche Gewinnreserve','A','Q','','2950',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2960', 'Freiwillige Gewinnreserve','A','Q','','2960',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2970', 'Gewinn- oder Verlustvortrag','A','Q','','2970',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2979', 'Jahresgewinn oder -verlust','A','Q','','2979',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('2980', 'Eigene Kapitalanteile','A','Q','','2980',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('3',    'BETRIEBLICHER ERTRAG AUS LIEFERUNGEN UND LEISTUNGEN','H','','','3',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('30',   'PRODUKTIONSERLÖSE','H','','','30',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('3000', 'Produktionserlöse','A','I','AR_amount:IC_sale','3000',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('32',   'HANDELSERLÖSE','H','','','32',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('3200', 'Handelserlöse','A','I','AR_amount:IC_sale','3200',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('34',   'DIENSTLEISTUNGSERLÖSE','H','','','34',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('3400', 'Dienstleistungserlöse','A','I','AR_amount:IC_income','3400',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('36',   'ÜBRIGE ERLÖSE AUS LIEFERUNGEN UND LEISTUNGEN','H','','','36',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('3600', 'Übrige Erlöse aus Lieferungen und Leistungen','A','I','IC_income','3600',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('37',   'EIGENLEISTUNGEN UND EIGENVERBRAUCH','H','','','37',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('3700', 'Eigenleistungen','A','I','','3700',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('38',   'ERLÖSMINDERUNGEN','H','','','38',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('3800', 'Skonti','A','E','AR_paid','3800',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('3801', 'Rabatte, Preisnachlässe','A','E','AR_paid','3801',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('3805', 'Verluste aus Forderungen','A','E','AR_paid','3805',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('3809', 'MWST - nur Saldosteuersatz','A','E','AR_paid','3809',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('39',   'BESTANDESÄNDERUNGEN AN UNFERTIGEN UND FERTIGEN ERZEUGNISSEN SOWIE AN NICHT FAKTURIERTEN DIENSTLEISTUNGEN','H','','','39',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('3900', 'Bestandesänderungen','A','I','','3900',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('4',    'AUFWAND FÜR MATERIAL, HANDELSWAREN, DIENSTLEISTUNGEN UND ENERGIE','H','','','4',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('40',   'MATERIALAUFWAND','H','','','40',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('4000', 'Materialeinkauf','A','E','AP_amount:IC_cogs','4000',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('42',   'HANDELSWARENAUFWAND','H','','','42',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('4200', 'Einkauf Handelswaren','A','E','AP_amount:IC_cogs','4200',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('4208', 'Bestandsänderungen Handelswaren','A','E','AP_amount:IC_cogs','4208',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('44',   'AUFWAND FÜR BEZOGENE DRITTLEISTUNGEN','H','','','44',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('4400', 'Aufwand für Drittleistungen','A','E','AP_amount:IC_expense','4400',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('45',   'ENERGIEAUFWAND ZUR LEISTUNGSERSTELLUNG','H','','','45',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('4500', 'Energieaufwand zur Leistungserstellung','A','E','AP_amount:IC_expense','4500',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('46',   'ÜBRIGER AUFWAND FÜR MATERIAL, HANDELSWAREN UND DIENSTLEISTUNGEN','H','','','46',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('47',   'DIREKTE EINKAUFSSPESEN','H','','','47',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('4700', 'Einkaufsspesen','A','E','AP_amount:IC_expense','4700',0,NULL,NULL,NULL,6, FALSE,'2011-01-01 00:00:00.000000'),
+('48',   'BESTANDESÄNDERUNGEN UND MATERIAL-/WARENVERLUSTE','H','','','48',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('4800', 'Bestandesänderungen','A','E','','4800',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('49',   'EINKAUFSPREISMINDERUNGEN','H','','','49',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('4900', 'Skonti, Rabatte, Preisnachlässe','A','I','AP_paid','4900',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5',    'PERSONALAUFWAND','H','','','5',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('500',  'Löhne und Gehälter','H','','','500',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('5000', 'Löhne und Gehälter','A','E','IC_expense','5000',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5001', 'Erfolgsbeteiligungen','A','E','IC_expense','5001',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5005', 'Leistungen von Sozialversicherungen','A','I','IC_income','5005',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('57',   'SOZIALVERSICHERUNGSAUFWAND','H','','','57',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('5700', 'AHV, IV, EO, ALV','A','E','AP_amount:IC_expense','5700',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5710', 'FAK','A','E','AP_amount:IC_expense','5710',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5720', 'Berufliche Vorsorge','A','E','AP_amount:IC_expense','5720',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5730', 'Unfallversicherung','A','E','AP_amount:IC_expense','5730',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5740', 'Krankentaggeldversicherung','A','E','AP_amount:IC_expense','5740',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5790', 'Quellensteuer','A','E','AP_amount:IC_expense','5790',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('58',   'ÜBRIGER PERSONALAUFWAND','H','','','58',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('5800', 'Aufwand für Personaleinstellung','A','E','IC_expense','5800',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5810', 'Weiterbildungskosten','A','E','IC_expense','5810',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5830', 'Spesen','A','E','IC_expense','5830',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('5880', 'Sonstiger Personalaufwand','A','E','IC_expense','5880',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('59',   'LEISTUNGEN DRITTER','H','','','59',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6',    'ÜBRIGER BETRIEBLICHER AUFWAND, ABSCHREIBUNGEN UND WERTBERICHTIGUNGEN SOWIE FINANZERGEBNIS','H','','','6',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('60',   'RAUMAUFWAND','H','','','60',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6000', 'Miete','A','E','IC_expense','6000',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6040', 'Reinigung','A','E','IC_expense','6040',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6050', 'Übriger Raumaufwand','A','E','IC_expense','6050',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('61',   'UNTERHALT, REPARATUREN, ERSATZ, LEASING, MOBILE SACHANLAGEN','H','','','61',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6100', 'Unterhalt','A','E','AP_amount:IC_expense','6100',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('62',   'FAHRZEUG- UND TRANSPORTAUFWAND','H','','','62',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6200', 'Fahrzeugaufwand','A','E','AP_amount:IC_expense','6200',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6201', 'Transportaufwand','A','E','AP_amount:IC_expense','6201',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('63',   'SACHVERSICHERUNGEN, ABGABEN, GEBÜHREN, BEWILLIGUNGEN','H','','','63',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6300', 'Betriebsversicherungen','A','E','AP_amount:IC_expense','6300',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6360', 'Abgaben, Gebühren und Bewilligungen','A','E','AP_amount:IC_expense','6360',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('64',   'ENERGIE- UND ENTSORGUNGSAUFWAND','H','','','64',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6400', 'Strom, Gas, Wasser','A','E','AP_amount:IC_expense','6400',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6460', 'Entsorgungsaufwand','A','E','AP_amount:IC_expense','6460',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('65',   'VERWALTUNGS- UND INFORMATIKAUFWAND','H','','','65',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6500', 'Büromaterial, Drucksachen','A','E','AP_amount:IC_expense','6500',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6503', 'Fachliteratur','A','E','AP_amount:IC_expense','6503',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6510', 'Telefon, Fax, Porti Internet','A','E','AP_amount:IC_expense','6510',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6520', 'Beiträge, Spenden','A','E','AP_amount:IC_expense','6520',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6530', 'Buchführungs- und Beratungsaufwand','A','E','AP_amount:IC_expense','6530',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6540', 'Verwaltungsrat, GV, Revision','A','E','AP_amount:IC_expense','6540',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6570', 'Informatikaufwand','A','E','AP_amount:IC_expense','6570',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6590', 'Übriger Verwaltungsaufwand','A','E','AP_amount:IC_expense','6590',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('66',   'WERBEAUFWAND','H','','','66',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6600', 'Werbeaufwand','A','E','AP_amount:IC_expense','6600',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('67',   'SONSTIGER BETRIEBLICHER AUFWAND','H','','','67',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6720', 'Forschung und Entwicklung','A','E','AP_amount:IC_expense','6720',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6790', 'Übriger Betriebsaufwand','A','E','AP_amount:IC_expense','6790',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('68',   'ABSCHREIBUNGEN UND WERTBERICHTIGUNGEN AUF POSITIONEN DES ANLAGEVERMÖGENS','H','','','68',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6800', 'Abschreibungen Finanzanlagen','A','E','','6800',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6810', 'Abschreibungen Beteiligungen','A','E','','6810',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6820', 'Abschreibungen mobile Sachanlagen','A','E','','6820',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6840', 'Abschreibungen immaterielle Anlagen','A','E','','6840',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('69',   'FINANZAUFWAND UND FINANZERTRAG','H','','','69',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('690',  'Finanzaufwand','H','','','690',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6900', 'Finanzaufwand','A','E','','6900',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6940', 'Bankspesen','A','E','','6940',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6942', 'Kursverluste','A','E','','6942',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('6943', 'Rundungsaufwand','A','E','AP_amount:IC_cogs:IC_expense','6943',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('695',  'Finanzertrag','H','','','695',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('6950', 'Finanzertrag','A','I','','6950',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('6952', 'Kursgewinne','A','I','','6952',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('6953', 'Rundungsertrag','A','I','AR_amount:IC_sale:IC_income','6953',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('6970', 'Mitgliederbeiträge','A','I','AR_amount:IC_income','6970',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('6980', 'Spenden','A','I','AR_amount:IC_income','6980',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('7',    'BETRIEBLICHER NEBENERFOLG','H','','','7',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('70',   'ERFOLG AUS NEBENBETRIEBEN','H','','','70',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('75',   'ERFOLG AUS BETRIEBLICHEN LIEGENSCHAFTEN','H','','','75',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('8',    'BETRIEBSFREMDER, AUSSERORDENTLICHER, EINMALIGER UND PERIODENFREMDER AUFWAND UND ERTRAG','H','','','8',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('80',   'BETRIEBSFREMDER AUFWAND UND BETRIEBSFREMDER ERTRAG','H','','','80',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('8000', 'Betriebsfremder Aufwand','A','E','AP_amount:IC_cogs:IC_expense','8000',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('8100', 'Betriebsfremder Ertrag','A','I','AR_amount:IC_sale:IC_income','8100',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('85',   'AUSSERORDENTLICHER, EINMALIGER AUFWAND UND ERTRAG','H','','','85',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('8500', 'Ausserordentlicher Aufwand','A','E','AP_amount:IC_cogs:IC_expense','8500',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('8510', 'Ausserordentlicher Ertrag','A','I','AR_amount:IC_sale:IC_income','8510',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('87',   'PERIODENFREMDER AUFWAND UND ERTRAG','H','','','87',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('8700', 'Periodenfremder Aufwand','A','E','AP_amount:IC_cogs:IC_expense','8700',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('8710', 'Periodenfremder Ertrag','A','I','AR_amount:IC_sale:IC_income','8710',0,NULL,NULL,NULL,1,FALSE,'2011-01-01 00:00:00.000000'),
+('89',   'DIREKTE STEUERN','H','','','89',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('8900', 'Direkte Steuern','A','E','','8900',0,NULL,NULL,NULL,6,FALSE,'2011-01-01 00:00:00.000000'),
+('9',    'ABSCHLUSS','H','','','9',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('90',   'ERFOLGSRECHNUNG','H','','','90',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('91',   'BILANZ','H','','','91',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('9100', 'Eröffnungsbilanz','A','E','','9100',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('92',   'GEWINNVERWENDUNG','H','','','92',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('95',   'JAHRESERGEBNISSE','H','','','95',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000'),
+('99',   'HILFSKONTEN NEBENBÜCHER','H','','','99',0,NULL,NULL,NULL,NULL,FALSE,'2011-01-01 00:00:00.000000');
+
+
+DELETE FROM buchungsgruppen;
+
+INSERT INTO buchungsgruppen (
+  description, inventory_accno_id,
+  income_accno_id_0, expense_accno_id_0,
+  income_accno_id_1, expense_accno_id_1,
+  income_accno_id_2, expense_accno_id_2,
+  income_accno_id_3, expense_accno_id_3
+) VALUES (
+  'Standard',(SELECT id FROM chart WHERE accno = '1200'),
+  (SELECT id FROM chart WHERE accno = '3200'), (SELECT id FROM chart WHERE accno = '4200'),
+  (SELECT id FROM chart WHERE accno = '3200'), (SELECT id FROM chart WHERE accno = '4200'),
+  (SELECT id FROM chart WHERE accno = '3200'), (SELECT id FROM chart WHERE accno = '4200'),
+  (SELECT id FROM chart WHERE accno = '3200'), (SELECT id FROM chart WHERE accno = '4200')
+);
+
+
+DELETE FROM tax_zones;
+
+INSERT INTO tax_zones (id, description) VALUES
+(0, 'Schweiz'), -- siehe auch Pg-upgrade2/taxzone_id_in_oe_delivery_orders.sql )=:
+(1, 'EU mit USt-ID Nummer'),
+(2, 'EU ohne USt-ID Nummer'),
+(3, 'Ausserhalb EU');
+
+
+DELETE FROM tax;
+
+INSERT INTO tax (taxkey, taxdescription, rate) VALUES
+(0, 'Keine Steuer', 0),
+(1, 'Mehrwertsteuerfrei', 0);
+
+INSERT INTO tax (taxkey, taxdescription, rate, taxnumber, chart_id) VALUES
+(2, 'MWST', 0.08000, '2200', (SELECT id FROM chart WHERE accno='2200')),
+(3, 'MWST', 0.02500, '2201', (SELECT id FROM chart WHERE accno='2201')),
+(4, 'MWST Aufwand', 0.08000, '1170', (SELECT id FROM chart WHERE accno='1170')),
+(5, 'MWST Aufwand', 0.02500, '1170', (SELECT id FROM chart WHERE accno='1170')),
+(6, 'MWST Investitionen', 0.08000, '1171', (SELECT id FROM chart WHERE accno='1171')),
+(7, 'MWST Investitionen', 0.02500, '1171', (SELECT id FROM chart WHERE accno='1171'));
+
+
+DELETE FROM taxkeys;
+
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 0, (SELECT tax.id FROM tax WHERE taxkey=0), chart.id, '2011-01-01' FROM chart WHERE taxkey_id=0;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 1, (SELECT tax.id FROM tax WHERE taxkey=1), chart.id, '2011-01-01' FROM chart WHERE taxkey_id=1;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 2, (SELECT tax.id FROM tax WHERE taxkey=2), chart.id, '2011-01-01' FROM chart WHERE taxkey_id=2;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 3, (SELECT tax.id FROM tax WHERE taxkey=3), chart.id, '2011-01-01' FROM chart WHERE taxkey_id=3;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 4, (SELECT tax.id FROM tax WHERE taxkey=4), chart.id, '2011-01-01' FROM chart WHERE taxkey_id=4;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 5, (SELECT tax.id FROM tax WHERE taxkey=5), chart.id, '2011-01-01' FROM chart WHERE taxkey_id=5;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 6, (SELECT tax.id FROM tax WHERE taxkey=6), chart.id, '2011-01-01' FROM chart WHERE taxkey_id=6;
+INSERT INTO taxkeys (taxkey_id, tax_id, chart_id, startdate) SELECT 7, (SELECT tax.id FROM tax WHERE taxkey=7), chart.id, '2011-01-01' FROM chart WHERE taxkey_id=7;
+
+
+DELETE FROM units;
+
+INSERT INTO units (name, base_unit, factor, type) VALUES
+('Stck',NULL,0.00000,'dimension'),
+('mg',NULL,0.00000,'dimension'),
+('g','mg',1000.00000,'dimension'),
+('kg','g',1000.00000,'dimension'),
+('t','kg',1000.00000,'dimension'),
+('ml',NULL,0.00000,'dimension'),
+('L','ml',1000.00000,'dimension'),
+('pauschal',NULL,0.00000,'service'),
+('Min',NULL,0.00000,'service'),
+('Std','Min',60.00000,'service'),
+('Tag','Std',8.00000,'service'),
+('Wo',NULL,0.00000,'service'),
+('Mt','Wo',4.00000,'service'),
+('Jahr','Mt',12.00000,'service');
+
+
+DELETE FROM defaults;
+
+INSERT INTO defaults (
+  inventory_accno_id,
+  income_accno_id, expense_accno_id,
+  fxgain_accno_id, fxloss_accno_id,
+  invnumber, sonumber,
+  weightunit,
+  businessnumber,
+  version,
+  closedto,
+  revtrans,
+  ponumber, sqnumber, rfqnumber,
+  customernumber, vendornumber,
+  audittrail,
+  articlenumber, servicenumber,
+  rmanumber, cnnumber
+) VALUES (
+  (SELECT id FROM CHART WHERE accno='1200'),
+  (SELECT id FROM CHART WHERE accno='3200'), (SELECT id FROM CHART WHERE accno='4200'),
+  (SELECT id FROM CHART WHERE accno='6952'), (SELECT id FROM CHART WHERE accno='6942'),
+  0, 0,
+  'kg',
+  '',
+  '3.1.0 CH',
+  NULL,
+  FALSE,
+  0, 0, 0,
+  0, 0,
+  FALSE,
+  0, 0,
+  0, 0
+);
index c78d558..ba13475 100644 (file)
@@ -3,7 +3,6 @@
 --
 
 --
--- TOC entry 5 (OID 1981863)
 -- Name: id; Type: SEQUENCE; Schema: public; Owner: postgres
 --
 
@@ -15,7 +14,6 @@ CREATE SEQUENCE id
 
 
 --
--- TOC entry 7 (OID 1981865)
 -- Name: glid; Type: SEQUENCE; Schema: public; Owner: postgres
 --
 
@@ -28,7 +26,6 @@ CREATE SEQUENCE glid
 
 
 --
--- TOC entry 13 (OID 1981867)
 -- Name: gl; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -49,7 +46,6 @@ CREATE TABLE gl (
 
 
 --
--- TOC entry 14 (OID 1981879)
 -- Name: chart; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -75,7 +71,6 @@ CREATE TABLE chart (
 
 
 --
--- TOC entry 15 (OID 1981890)
 -- Name: datev; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -92,7 +87,6 @@ CREATE TABLE datev (
 
 
 --
--- TOC entry 16 (OID 1981893)
 -- Name: gifi; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -103,7 +97,6 @@ CREATE TABLE gifi (
 
 
 --
--- TOC entry 17 (OID 1981898)
 -- Name: parts; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -142,11 +135,10 @@ CREATE TABLE parts (
     not_discountable boolean DEFAULT false,
     buchungsgruppen_id integer,
     payment_id integer
-) WITH OIDS;
+);
 
 
 --
--- TOC entry 18 (OID 1981915)
 -- Name: defaults; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -185,7 +177,6 @@ CREATE TABLE "defaults" (
 
 
 --
--- TOC entry 19 (OID 1981924)
 -- Name: audittrail; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -201,7 +192,6 @@ CREATE TABLE audittrail (
 
 
 --
--- TOC entry 20 (OID 1981930)
 -- Name: acc_trans; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -219,11 +209,10 @@ CREATE TABLE acc_trans (
     taxkey integer,
     itime timestamp without time zone DEFAULT now(),
     mtime timestamp without time zone
-) WITH OIDS;
+);
 
 
 --
--- TOC entry 21 (OID 1981944)
 -- Name: invoice; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -251,11 +240,10 @@ CREATE TABLE invoice (
     base_qty real,
     subtotal boolean DEFAULT false,
     longdescription text
-) WITH OIDS;
+);
 
 
 --
--- TOC entry 22 (OID 1981958)
 -- Name: vendor; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -306,7 +294,6 @@ CREATE TABLE vendor (
 
 
 --
--- TOC entry 23 (OID 1981969)
 -- Name: customer; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -357,7 +344,6 @@ CREATE TABLE customer (
 
 
 --
--- TOC entry 24 (OID 1981982)
 -- Name: contacts; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -387,7 +373,6 @@ CREATE TABLE contacts (
 
 
 --
--- TOC entry 25 (OID 1981991)
 -- Name: assembly; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -398,11 +383,10 @@ CREATE TABLE assembly (
     bom boolean,
     itime timestamp without time zone DEFAULT now(),
     mtime timestamp without time zone
-) WITH OIDS;
+);
 
 
 --
--- TOC entry 26 (OID 1981994)
 -- Name: ar; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -447,7 +431,6 @@ CREATE TABLE ar (
 
 
 --
--- TOC entry 27 (OID 1982012)
 -- Name: ap; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -484,7 +467,6 @@ CREATE TABLE ap (
 
 
 --
--- TOC entry 28 (OID 1982030)
 -- Name: partstax; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -497,7 +479,6 @@ CREATE TABLE partstax (
 
 
 --
--- TOC entry 29 (OID 1982033)
 -- Name: tax; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -514,7 +495,6 @@ CREATE TABLE tax (
 
 
 --
--- TOC entry 30 (OID 1982039)
 -- Name: customertax; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -527,7 +507,6 @@ CREATE TABLE customertax (
 
 
 --
--- TOC entry 31 (OID 1982042)
 -- Name: vendortax; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -540,7 +519,6 @@ CREATE TABLE vendortax (
 
 
 --
--- TOC entry 32 (OID 1982045)
 -- Name: oe; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -579,7 +557,6 @@ CREATE TABLE oe (
 
 
 --
--- TOC entry 33 (OID 1982058)
 -- Name: orderitems; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -605,11 +582,10 @@ CREATE TABLE orderitems (
     base_qty real,
     subtotal boolean DEFAULT false,
     longdescription text
-) WITH OIDS;
+);
 
 
 --
--- TOC entry 34 (OID 1982069)
 -- Name: exchangerate; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -624,7 +600,6 @@ CREATE TABLE exchangerate (
 
 
 --
--- TOC entry 35 (OID 1982072)
 -- Name: employee; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -649,7 +624,6 @@ CREATE TABLE employee (
 
 
 --
--- TOC entry 36 (OID 1982083)
 -- Name: shipto; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -674,7 +648,6 @@ CREATE TABLE shipto (
 
 
 --
--- TOC entry 37 (OID 1982089)
 -- Name: project; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -688,7 +661,6 @@ CREATE TABLE project (
 
 
 --
--- TOC entry 38 (OID 1982100)
 -- Name: partsgroup; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -697,11 +669,10 @@ CREATE TABLE partsgroup (
     partsgroup text,
     itime timestamp without time zone DEFAULT now(),
     mtime timestamp without time zone
-) WITH OIDS;
+);
 
 
 --
--- TOC entry 39 (OID 1982107)
 -- Name: makemodel; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -715,7 +686,6 @@ CREATE TABLE makemodel (
 
 
 --
--- TOC entry 40 (OID 1982113)
 -- Name: status; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -732,7 +702,6 @@ CREATE TABLE status (
 
 
 --
--- TOC entry 9 (OID 1982121)
 -- Name: invoiceid; Type: SEQUENCE; Schema: public; Owner: postgres
 --
 
@@ -745,7 +714,6 @@ CREATE SEQUENCE invoiceid
 
 
 --
--- TOC entry 11 (OID 1982123)
 -- Name: orderitemsid; Type: SEQUENCE; Schema: public; Owner: postgres
 --
 
@@ -759,7 +727,6 @@ CREATE SEQUENCE orderitemsid
 
 
 --
--- TOC entry 41 (OID 1982125)
 -- Name: warehouse; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -772,7 +739,6 @@ CREATE TABLE warehouse (
 
 
 --
--- TOC entry 42 (OID 1982134)
 -- Name: inventory; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -790,7 +756,6 @@ CREATE TABLE inventory (
 
 
 --
--- TOC entry 43 (OID 1982137)
 -- Name: department; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -804,7 +769,6 @@ CREATE TABLE department (
 
 
 --
--- TOC entry 44 (OID 1982145)
 -- Name: dpt_trans; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -817,7 +781,6 @@ CREATE TABLE dpt_trans (
 
 
 --
--- TOC entry 45 (OID 1982148)
 -- Name: business; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -833,7 +796,6 @@ CREATE TABLE business (
 
 
 --
--- TOC entry 46 (OID 1982158)
 -- Name: sic; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -847,7 +809,6 @@ CREATE TABLE sic (
 
 
 --
--- TOC entry 47 (OID 1982164)
 -- Name: license; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -864,7 +825,6 @@ CREATE TABLE license (
 
 
 --
--- TOC entry 48 (OID 1982174)
 -- Name: licenseinvoice; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -875,7 +835,6 @@ CREATE TABLE licenseinvoice (
 
 
 --
--- TOC entry 49 (OID 1982176)
 -- Name: pricegroup; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -886,7 +845,6 @@ CREATE TABLE pricegroup (
 
 
 --
--- TOC entry 50 (OID 1982184)
 -- Name: prices; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -898,7 +856,6 @@ CREATE TABLE prices (
 
 
 --
--- TOC entry 51 (OID 1982194)
 -- Name: finanzamt; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -927,7 +884,6 @@ CREATE TABLE finanzamt (
 
 
 --
--- TOC entry 157 (OID 1982885)
 -- Name: check_department(); Type: FUNCTION; Schema: public; Owner: postgres
 --
 
@@ -937,7 +893,6 @@ CREATE FUNCTION check_department() RETURNS "trigger"
 
 
 --
--- TOC entry 158 (OID 1982886)
 -- Name: del_department(); Type: FUNCTION; Schema: public; Owner: postgres
 --
 
@@ -947,7 +902,6 @@ CREATE FUNCTION del_department() RETURNS "trigger"
 
 
 --
--- TOC entry 159 (OID 1982887)
 -- Name: del_customer(); Type: FUNCTION; Schema: public; Owner: postgres
 --
 
@@ -957,7 +911,6 @@ CREATE FUNCTION del_customer() RETURNS "trigger"
 
 
 --
--- TOC entry 160 (OID 1982888)
 -- Name: del_vendor(); Type: FUNCTION; Schema: public; Owner: postgres
 --
 
@@ -967,7 +920,6 @@ CREATE FUNCTION del_vendor() RETURNS "trigger"
 
 
 --
--- TOC entry 161 (OID 1982889)
 -- Name: del_exchangerate(); Type: FUNCTION; Schema: public; Owner: postgres
 --
 
@@ -977,7 +929,6 @@ CREATE FUNCTION del_exchangerate() RETURNS "trigger"
 
 
 --
--- TOC entry 162 (OID 1982890)
 -- Name: check_inventory(); Type: FUNCTION; Schema: public; Owner: postgres
 --
 
@@ -987,7 +938,6 @@ CREATE FUNCTION check_inventory() RETURNS "trigger"
 
 
 --
--- TOC entry 163 (OID 1982968)
 -- Name: set_datevexport(); Type: FUNCTION; Schema: public; Owner: postgres
 --
 
@@ -997,7 +947,6 @@ CREATE FUNCTION set_datevexport() RETURNS "trigger"
 
 
 --
--- TOC entry 164 (OID 1982971)
 -- Name: set_mtime(); Type: FUNCTION; Schema: public; Owner: postgres
 --
 
@@ -1007,7 +956,6 @@ CREATE FUNCTION set_mtime() RETURNS "trigger"
 
 
 --
--- TOC entry 52 (OID 1983721)
 -- Name: language; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -1022,7 +970,6 @@ CREATE TABLE "language" (
 
 
 --
--- TOC entry 53 (OID 1983730)
 -- Name: payment_terms; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -1040,7 +987,6 @@ CREATE TABLE payment_terms (
 
 
 --
--- TOC entry 54 (OID 1983739)
 -- Name: translation; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -1055,7 +1001,6 @@ CREATE TABLE translation (
 
 
 --
--- TOC entry 55 (OID 1983745)
 -- Name: units; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -1068,7 +1013,6 @@ CREATE TABLE units (
 
 
 --
--- TOC entry 56 (OID 1983761)
 -- Name: rma; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -1104,7 +1048,6 @@ CREATE TABLE rma (
 
 
 --
--- TOC entry 57 (OID 1983774)
 -- Name: rmaitems; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -1132,7 +1075,6 @@ CREATE TABLE rmaitems (
 
 
 --
--- TOC entry 58 (OID 1983785)
 -- Name: printers; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -1145,7 +1087,6 @@ CREATE TABLE printers (
 
 
 --
--- TOC entry 59 (OID 1983798)
 -- Name: tax_zones; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -1156,7 +1097,6 @@ CREATE TABLE tax_zones (
 
 
 --
--- TOC entry 60 (OID 1983807)
 -- Name: buchungsgruppen; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -1176,7 +1116,6 @@ CREATE TABLE buchungsgruppen (
 
 
 --
--- TOC entry 61 (OID 1983825)
 -- Name: dunning_config; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -1199,7 +1138,6 @@ CREATE TABLE dunning_config (
 
 
 --
--- TOC entry 62 (OID 1983833)
 -- Name: dunning; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -1216,7 +1154,6 @@ CREATE TABLE dunning (
 
 
 --
--- TOC entry 165 (OID 1983838)
 -- Name: set_priceupdate_parts(); Type: FUNCTION; Schema: public; Owner: postgres
 --
 
@@ -1226,7 +1163,6 @@ CREATE FUNCTION set_priceupdate_parts() RETURNS "trigger"
 
 
 --
--- TOC entry 63 (OID 1983846)
 -- Name: leads; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -1237,7 +1173,6 @@ CREATE TABLE leads (
 
 
 --
--- TOC entry 64 (OID 1983849)
 -- Name: taxkeys; Type: TABLE; Schema: public; Owner: postgres
 --
 
@@ -1252,14 +1187,12 @@ CREATE TABLE taxkeys (
 
 
 --
--- Data for TOC entry 171 (OID 1981915)
 -- Name: defaults; Type: TABLE DATA; Schema: public; Owner: postgres
 --
 
 INSERT INTO "defaults" ("version", "curr") VALUES ('2.4.0.0', 'EUR:USD');
 
 --
--- Data for TOC entry 204 (OID 1982194)
 -- Name: finanzamt; Type: TABLE DATA; Schema: public; Owner: postgres
 --
 
@@ -1952,7 +1885,6 @@ INSERT INTO finanzamt (fa_land_nr, fa_bufa_nr, fa_name, fa_strasse, fa_plz, fa_o
 
 
 --
--- Data for TOC entry 208 (OID 1983745)
 -- Name: units; Type: TABLE DATA; Schema: public; Owner: postgres
 --
 
@@ -1970,7 +1902,6 @@ INSERT INTO units (name, base_unit, factor, "type") VALUES ('ml', NULL, NULL, 'd
 
 
 --
--- Data for TOC entry 212 (OID 1983798)
 -- Name: tax_zones; Type: TABLE DATA; Schema: public; Owner: postgres
 --
 
@@ -1980,7 +1911,6 @@ INSERT INTO tax_zones (id, description) VALUES (2, 'EU ohne USt-ID Nummer');
 INSERT INTO tax_zones (id, description) VALUES (3, 'Außerhalb EU');
 
 --
--- TOC entry 143 (OID 1982173)
 -- Name: license_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -1988,7 +1918,6 @@ CREATE INDEX license_id_key ON license USING btree (id);
 
 
 --
--- TOC entry 84 (OID 1982891)
 -- Name: acc_trans_trans_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -1996,7 +1925,6 @@ CREATE INDEX acc_trans_trans_id_key ON acc_trans USING btree (trans_id);
 
 
 --
--- TOC entry 82 (OID 1982892)
 -- Name: acc_trans_chart_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2004,7 +1932,6 @@ CREATE INDEX acc_trans_chart_id_key ON acc_trans USING btree (chart_id);
 
 
 --
--- TOC entry 85 (OID 1982893)
 -- Name: acc_trans_transdate_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2012,7 +1939,6 @@ CREATE INDEX acc_trans_transdate_key ON acc_trans USING btree (transdate);
 
 
 --
--- TOC entry 83 (OID 1982894)
 -- Name: acc_trans_source_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2020,7 +1946,6 @@ CREATE INDEX acc_trans_source_key ON acc_trans USING btree (lower(source));
 
 
 --
--- TOC entry 110 (OID 1982895)
 -- Name: ap_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2028,7 +1953,6 @@ CREATE INDEX ap_id_key ON ap USING btree (id);
 
 
 --
--- TOC entry 115 (OID 1982896)
 -- Name: ap_transdate_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2036,7 +1960,6 @@ CREATE INDEX ap_transdate_key ON ap USING btree (transdate);
 
 
 --
--- TOC entry 111 (OID 1982897)
 -- Name: ap_invnumber_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2044,7 +1967,6 @@ CREATE INDEX ap_invnumber_key ON ap USING btree (lower(invnumber));
 
 
 --
--- TOC entry 112 (OID 1982898)
 -- Name: ap_ordnumber_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2052,7 +1974,6 @@ CREATE INDEX ap_ordnumber_key ON ap USING btree (lower(ordnumber));
 
 
 --
--- TOC entry 116 (OID 1982899)
 -- Name: ap_vendor_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2060,7 +1981,6 @@ CREATE INDEX ap_vendor_id_key ON ap USING btree (vendor_id);
 
 
 --
--- TOC entry 109 (OID 1982900)
 -- Name: ap_employee_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2068,7 +1988,6 @@ CREATE INDEX ap_employee_id_key ON ap USING btree (employee_id);
 
 
 --
--- TOC entry 103 (OID 1982901)
 -- Name: ar_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2076,7 +1995,6 @@ CREATE INDEX ar_id_key ON ar USING btree (id);
 
 
 --
--- TOC entry 108 (OID 1982902)
 -- Name: ar_transdate_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2084,7 +2002,6 @@ CREATE INDEX ar_transdate_key ON ar USING btree (transdate);
 
 
 --
--- TOC entry 104 (OID 1982903)
 -- Name: ar_invnumber_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2092,7 +2009,6 @@ CREATE INDEX ar_invnumber_key ON ar USING btree (lower(invnumber));
 
 
 --
--- TOC entry 105 (OID 1982904)
 -- Name: ar_ordnumber_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2100,7 +2016,6 @@ CREATE INDEX ar_ordnumber_key ON ar USING btree (lower(ordnumber));
 
 
 --
--- TOC entry 101 (OID 1982905)
 -- Name: ar_customer_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2108,7 +2023,6 @@ CREATE INDEX ar_customer_id_key ON ar USING btree (customer_id);
 
 
 --
--- TOC entry 102 (OID 1982906)
 -- Name: ar_employee_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2116,7 +2030,6 @@ CREATE INDEX ar_employee_id_key ON ar USING btree (employee_id);
 
 
 --
--- TOC entry 100 (OID 1982907)
 -- Name: assembly_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2124,7 +2037,6 @@ CREATE INDEX assembly_id_key ON assembly USING btree (id);
 
 
 --
--- TOC entry 74 (OID 1982908)
 -- Name: chart_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2132,7 +2044,6 @@ CREATE INDEX chart_id_key ON chart USING btree (id);
 
 
 --
--- TOC entry 71 (OID 1982909)
 -- Name: chart_accno_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2140,7 +2051,6 @@ CREATE UNIQUE INDEX chart_accno_key ON chart USING btree (accno);
 
 
 --
--- TOC entry 72 (OID 1982910)
 -- Name: chart_category_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2148,7 +2058,6 @@ CREATE INDEX chart_category_key ON chart USING btree (category);
 
 
 --
--- TOC entry 75 (OID 1982911)
 -- Name: chart_link_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2156,7 +2065,6 @@ CREATE INDEX chart_link_key ON chart USING btree (link);
 
 
 --
--- TOC entry 73 (OID 1982912)
 -- Name: chart_gifi_accno_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2164,7 +2072,6 @@ CREATE INDEX chart_gifi_accno_key ON chart USING btree (gifi_accno);
 
 
 --
--- TOC entry 96 (OID 1982913)
 -- Name: customer_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2172,7 +2079,6 @@ CREATE INDEX customer_id_key ON customer USING btree (id);
 
 
 --
--- TOC entry 118 (OID 1982914)
 -- Name: customer_customer_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2180,7 +2086,6 @@ CREATE INDEX customer_customer_id_key ON customertax USING btree (customer_id);
 
 
 --
--- TOC entry 95 (OID 1982915)
 -- Name: customer_customernumber_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2188,7 +2093,6 @@ CREATE INDEX customer_customernumber_key ON customer USING btree (customernumber
 
 
 --
--- TOC entry 97 (OID 1982916)
 -- Name: customer_name_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2196,7 +2100,6 @@ CREATE INDEX customer_name_key ON customer USING btree (name);
 
 
 --
--- TOC entry 94 (OID 1982917)
 -- Name: customer_contact_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2204,7 +2107,6 @@ CREATE INDEX customer_contact_key ON customer USING btree (contact);
 
 
 --
--- TOC entry 128 (OID 1982918)
 -- Name: employee_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2212,7 +2114,6 @@ CREATE INDEX employee_id_key ON employee USING btree (id);
 
 
 --
--- TOC entry 129 (OID 1982919)
 -- Name: employee_login_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2220,7 +2121,6 @@ CREATE UNIQUE INDEX employee_login_key ON employee USING btree (login);
 
 
 --
--- TOC entry 130 (OID 1982920)
 -- Name: employee_name_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2228,7 +2128,6 @@ CREATE INDEX employee_name_key ON employee USING btree (name);
 
 
 --
--- TOC entry 127 (OID 1982921)
 -- Name: exchangerate_ct_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2236,7 +2135,6 @@ CREATE INDEX exchangerate_ct_key ON exchangerate USING btree (curr, transdate);
 
 
 --
--- TOC entry 77 (OID 1982922)
 -- Name: gifi_accno_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2244,7 +2142,6 @@ CREATE UNIQUE INDEX gifi_accno_key ON gifi USING btree (accno);
 
 
 --
--- TOC entry 67 (OID 1982923)
 -- Name: gl_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2252,7 +2149,6 @@ CREATE INDEX gl_id_key ON gl USING btree (id);
 
 
 --
--- TOC entry 70 (OID 1982924)
 -- Name: gl_transdate_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2260,7 +2156,6 @@ CREATE INDEX gl_transdate_key ON gl USING btree (transdate);
 
 
 --
--- TOC entry 69 (OID 1982925)
 -- Name: gl_reference_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2268,7 +2163,6 @@ CREATE INDEX gl_reference_key ON gl USING btree (lower(reference));
 
 
 --
--- TOC entry 65 (OID 1982926)
 -- Name: gl_description_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2276,7 +2170,6 @@ CREATE INDEX gl_description_key ON gl USING btree (lower(description));
 
 
 --
--- TOC entry 66 (OID 1982927)
 -- Name: gl_employee_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2284,7 +2177,6 @@ CREATE INDEX gl_employee_id_key ON gl USING btree (employee_id);
 
 
 --
--- TOC entry 86 (OID 1982928)
 -- Name: invoice_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2292,7 +2184,6 @@ CREATE INDEX invoice_id_key ON invoice USING btree (id);
 
 
 --
--- TOC entry 88 (OID 1982929)
 -- Name: invoice_trans_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2300,7 +2191,6 @@ CREATE INDEX invoice_trans_id_key ON invoice USING btree (trans_id);
 
 
 --
--- TOC entry 121 (OID 1982930)
 -- Name: oe_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2308,7 +2198,6 @@ CREATE INDEX oe_id_key ON oe USING btree (id);
 
 
 --
--- TOC entry 124 (OID 1982931)
 -- Name: oe_transdate_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2316,7 +2205,6 @@ CREATE INDEX oe_transdate_key ON oe USING btree (transdate);
 
 
 --
--- TOC entry 122 (OID 1982932)
 -- Name: oe_ordnumber_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2324,7 +2212,6 @@ CREATE INDEX oe_ordnumber_key ON oe USING btree (lower(ordnumber));
 
 
 --
--- TOC entry 120 (OID 1982933)
 -- Name: oe_employee_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2332,7 +2219,6 @@ CREATE INDEX oe_employee_id_key ON oe USING btree (employee_id);
 
 
 --
--- TOC entry 126 (OID 1982934)
 -- Name: orderitems_trans_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2340,7 +2226,6 @@ CREATE INDEX orderitems_trans_id_key ON orderitems USING btree (trans_id);
 
 
 --
--- TOC entry 79 (OID 1982935)
 -- Name: parts_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2348,7 +2233,6 @@ CREATE INDEX parts_id_key ON parts USING btree (id);
 
 
 --
--- TOC entry 80 (OID 1982936)
 -- Name: parts_partnumber_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2356,7 +2240,6 @@ CREATE INDEX parts_partnumber_key ON parts USING btree (lower(partnumber));
 
 
 --
--- TOC entry 78 (OID 1982937)
 -- Name: parts_description_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2364,7 +2247,6 @@ CREATE INDEX parts_description_key ON parts USING btree (lower(description));
 
 
 --
--- TOC entry 117 (OID 1982938)
 -- Name: partstax_parts_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2372,7 +2254,6 @@ CREATE INDEX partstax_parts_id_key ON partstax USING btree (parts_id);
 
 
 --
--- TOC entry 90 (OID 1982939)
 -- Name: vendor_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2380,7 +2261,6 @@ CREATE INDEX vendor_id_key ON vendor USING btree (id);
 
 
 --
--- TOC entry 91 (OID 1982940)
 -- Name: vendor_name_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2388,7 +2268,6 @@ CREATE INDEX vendor_name_key ON vendor USING btree (name);
 
 
 --
--- TOC entry 93 (OID 1982941)
 -- Name: vendor_vendornumber_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2396,7 +2275,6 @@ CREATE INDEX vendor_vendornumber_key ON vendor USING btree (vendornumber);
 
 
 --
--- TOC entry 89 (OID 1982942)
 -- Name: vendor_contact_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2404,7 +2282,6 @@ CREATE INDEX vendor_contact_key ON vendor USING btree (contact);
 
 
 --
--- TOC entry 119 (OID 1982943)
 -- Name: vendortax_vendor_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2412,7 +2289,6 @@ CREATE INDEX vendortax_vendor_id_key ON vendortax USING btree (vendor_id);
 
 
 --
--- TOC entry 132 (OID 1982944)
 -- Name: shipto_trans_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2420,7 +2296,6 @@ CREATE INDEX shipto_trans_id_key ON shipto USING btree (trans_id);
 
 
 --
--- TOC entry 133 (OID 1982945)
 -- Name: project_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2428,7 +2303,6 @@ CREATE INDEX project_id_key ON project USING btree (id);
 
 
 --
--- TOC entry 107 (OID 1982946)
 -- Name: ar_quonumber_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2436,7 +2310,6 @@ CREATE INDEX ar_quonumber_key ON ar USING btree (lower(quonumber));
 
 
 --
--- TOC entry 114 (OID 1982947)
 -- Name: ap_quonumber_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2444,7 +2317,6 @@ CREATE INDEX ap_quonumber_key ON ap USING btree (lower(quonumber));
 
 
 --
--- TOC entry 138 (OID 1982948)
 -- Name: makemodel_parts_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2452,7 +2324,6 @@ CREATE INDEX makemodel_parts_id_key ON makemodel USING btree (parts_id);
 
 
 --
--- TOC entry 136 (OID 1982949)
 -- Name: makemodel_make_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2460,7 +2331,6 @@ CREATE INDEX makemodel_make_key ON makemodel USING btree (lower(make));
 
 
 --
--- TOC entry 137 (OID 1982950)
 -- Name: makemodel_model_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2468,7 +2338,6 @@ CREATE INDEX makemodel_model_key ON makemodel USING btree (lower(model));
 
 
 --
--- TOC entry 139 (OID 1982951)
 -- Name: status_trans_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2476,7 +2345,6 @@ CREATE INDEX status_trans_id_key ON status USING btree (trans_id);
 
 
 --
--- TOC entry 141 (OID 1982952)
 -- Name: department_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2484,7 +2352,6 @@ CREATE INDEX department_id_key ON department USING btree (id);
 
 
 --
--- TOC entry 125 (OID 1982953)
 -- Name: orderitems_id_key; Type: INDEX; Schema: public; Owner: postgres
 --
 
@@ -2492,7 +2359,6 @@ CREATE INDEX orderitems_id_key ON orderitems USING btree (id);
 
 
 --
--- TOC entry 68 (OID 1981877)
 -- Name: gl_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2501,7 +2367,6 @@ ALTER TABLE ONLY gl
 
 
 --
--- TOC entry 76 (OID 1981888)
 -- Name: chart_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2510,7 +2375,6 @@ ALTER TABLE ONLY chart
 
 
 --
--- TOC entry 81 (OID 1981913)
 -- Name: parts_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2519,7 +2383,6 @@ ALTER TABLE ONLY parts
 
 
 --
--- TOC entry 87 (OID 1981952)
 -- Name: invoice_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2528,7 +2391,6 @@ ALTER TABLE ONLY invoice
 
 
 --
--- TOC entry 92 (OID 1981967)
 -- Name: vendor_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2537,7 +2399,6 @@ ALTER TABLE ONLY vendor
 
 
 --
--- TOC entry 98 (OID 1981980)
 -- Name: customer_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2546,7 +2407,6 @@ ALTER TABLE ONLY customer
 
 
 --
--- TOC entry 99 (OID 1981989)
 -- Name: contacts_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2555,7 +2415,6 @@ ALTER TABLE ONLY contacts
 
 
 --
--- TOC entry 106 (OID 1982006)
 -- Name: ar_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2564,7 +2423,6 @@ ALTER TABLE ONLY ar
 
 
 --
--- TOC entry 113 (OID 1982024)
 -- Name: ap_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2573,7 +2431,6 @@ ALTER TABLE ONLY ap
 
 
 --
--- TOC entry 123 (OID 1982056)
 -- Name: oe_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2582,7 +2439,6 @@ ALTER TABLE ONLY oe
 
 
 --
--- TOC entry 131 (OID 1982081)
 -- Name: employee_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2591,7 +2447,6 @@ ALTER TABLE ONLY employee
 
 
 --
--- TOC entry 134 (OID 1982096)
 -- Name: project_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2600,7 +2455,6 @@ ALTER TABLE ONLY project
 
 
 --
--- TOC entry 135 (OID 1982098)
 -- Name: project_projectnumber_key; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2609,7 +2463,6 @@ ALTER TABLE ONLY project
 
 
 --
--- TOC entry 140 (OID 1982132)
 -- Name: warehouse_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2618,7 +2471,6 @@ ALTER TABLE ONLY warehouse
 
 
 --
--- TOC entry 142 (OID 1982156)
 -- Name: business_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2627,7 +2479,6 @@ ALTER TABLE ONLY business
 
 
 --
--- TOC entry 144 (OID 1982171)
 -- Name: license_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2636,7 +2487,6 @@ ALTER TABLE ONLY license
 
 
 --
--- TOC entry 145 (OID 1982182)
 -- Name: pricegroup_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2645,7 +2495,6 @@ ALTER TABLE ONLY pricegroup
 
 
 --
--- TOC entry 146 (OID 1983728)
 -- Name: language_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2654,7 +2503,6 @@ ALTER TABLE ONLY "language"
 
 
 --
--- TOC entry 147 (OID 1983737)
 -- Name: payment_terms_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2663,7 +2511,6 @@ ALTER TABLE ONLY payment_terms
 
 
 --
--- TOC entry 148 (OID 1983747)
 -- Name: units_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2672,7 +2519,6 @@ ALTER TABLE ONLY units
 
 
 --
--- TOC entry 149 (OID 1983772)
 -- Name: rma_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2681,7 +2527,6 @@ ALTER TABLE ONLY rma
 
 
 --
--- TOC entry 150 (OID 1983791)
 -- Name: printers_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2690,7 +2535,6 @@ ALTER TABLE ONLY printers
 
 
 --
--- TOC entry 151 (OID 1983813)
 -- Name: buchungsgruppen_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2699,7 +2543,6 @@ ALTER TABLE ONLY buchungsgruppen
 
 
 --
--- TOC entry 152 (OID 1983831)
 -- Name: dunning_config_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2708,7 +2551,6 @@ ALTER TABLE ONLY dunning_config
 
 
 --
--- TOC entry 153 (OID 1983836)
 -- Name: dunning_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2717,7 +2559,6 @@ ALTER TABLE ONLY dunning
 
 
 --
--- TOC entry 154 (OID 1983852)
 -- Name: taxkeys_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2726,7 +2567,6 @@ ALTER TABLE ONLY taxkeys
 
 
 --
--- TOC entry 219 (OID 1981940)
 -- Name: $1; Type: FK CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2735,7 +2575,6 @@ ALTER TABLE ONLY acc_trans
 
 
 --
--- TOC entry 220 (OID 1981954)
 -- Name: $1; Type: FK CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2744,7 +2583,6 @@ ALTER TABLE ONLY invoice
 
 
 --
--- TOC entry 221 (OID 1982008)
 -- Name: $1; Type: FK CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2753,7 +2591,6 @@ ALTER TABLE ONLY ar
 
 
 --
--- TOC entry 222 (OID 1982026)
 -- Name: $1; Type: FK CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2762,7 +2599,6 @@ ALTER TABLE ONLY ap
 
 
 --
--- TOC entry 223 (OID 1982065)
 -- Name: $1; Type: FK CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2771,7 +2607,6 @@ ALTER TABLE ONLY orderitems
 
 
 --
--- TOC entry 224 (OID 1982186)
 -- Name: $1; Type: FK CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2780,7 +2615,6 @@ ALTER TABLE ONLY prices
 
 
 --
--- TOC entry 225 (OID 1982190)
 -- Name: $2; Type: FK CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2789,7 +2623,6 @@ ALTER TABLE ONLY prices
 
 
 --
--- TOC entry 226 (OID 1983749)
 -- Name: $1; Type: FK CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2798,7 +2631,6 @@ ALTER TABLE ONLY units
 
 
 --
--- TOC entry 227 (OID 1983781)
 -- Name: $1; Type: FK CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2807,7 +2639,6 @@ ALTER TABLE ONLY rmaitems
 
 
 --
--- TOC entry 218 (OID 1984290)
 -- Name: $1; Type: FK CONSTRAINT; Schema: public; Owner: postgres
 --
 
@@ -2816,7 +2647,6 @@ ALTER TABLE ONLY parts
 
 
 --
--- TOC entry 243 (OID 1982954)
 -- Name: check_department; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -2827,7 +2657,6 @@ CREATE TRIGGER check_department
 
 
 --
--- TOC entry 247 (OID 1982955)
 -- Name: check_department; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -2838,7 +2667,6 @@ CREATE TRIGGER check_department
 
 
 --
--- TOC entry 228 (OID 1982956)
 -- Name: check_department; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -2849,7 +2677,6 @@ CREATE TRIGGER check_department
 
 
 --
--- TOC entry 252 (OID 1982957)
 -- Name: check_department; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -2860,7 +2687,6 @@ CREATE TRIGGER check_department
 
 
 --
--- TOC entry 244 (OID 1982958)
 -- Name: del_department; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -2871,7 +2697,6 @@ CREATE TRIGGER del_department
 
 
 --
--- TOC entry 248 (OID 1982959)
 -- Name: del_department; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -2882,7 +2707,6 @@ CREATE TRIGGER del_department
 
 
 --
--- TOC entry 229 (OID 1982960)
 -- Name: del_department; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -2893,7 +2717,6 @@ CREATE TRIGGER del_department
 
 
 --
--- TOC entry 254 (OID 1982961)
 -- Name: del_department; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -2904,7 +2727,6 @@ CREATE TRIGGER del_department
 
 
 --
--- TOC entry 240 (OID 1982962)
 -- Name: del_customer; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -2915,7 +2737,6 @@ CREATE TRIGGER del_customer
 
 
 --
--- TOC entry 236 (OID 1982963)
 -- Name: del_vendor; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -2926,7 +2747,6 @@ CREATE TRIGGER del_vendor
 
 
 --
--- TOC entry 245 (OID 1982964)
 -- Name: del_exchangerate; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -2937,7 +2757,6 @@ CREATE TRIGGER del_exchangerate
 
 
 --
--- TOC entry 249 (OID 1982965)
 -- Name: del_exchangerate; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -2948,7 +2767,6 @@ CREATE TRIGGER del_exchangerate
 
 
 --
--- TOC entry 255 (OID 1982966)
 -- Name: del_exchangerate; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -2959,7 +2777,6 @@ CREATE TRIGGER del_exchangerate
 
 
 --
--- TOC entry 253 (OID 1982967)
 -- Name: check_inventory; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -2970,7 +2787,6 @@ CREATE TRIGGER check_inventory
 
 
 --
--- TOC entry 239 (OID 1982969)
 -- Name: customer_datevexport; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -2981,7 +2797,6 @@ CREATE TRIGGER customer_datevexport
 
 
 --
--- TOC entry 238 (OID 1982970)
 -- Name: vendor_datevexport; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -2992,7 +2807,6 @@ CREATE TRIGGER vendor_datevexport
 
 
 --
--- TOC entry 241 (OID 1982972)
 -- Name: mtime_customer; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3003,7 +2817,6 @@ CREATE TRIGGER mtime_customer
 
 
 --
--- TOC entry 237 (OID 1982973)
 -- Name: mtime_vendor; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3014,7 +2827,6 @@ CREATE TRIGGER mtime_vendor
 
 
 --
--- TOC entry 246 (OID 1982974)
 -- Name: mtime_ar; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3025,7 +2837,6 @@ CREATE TRIGGER mtime_ar
 
 
 --
--- TOC entry 250 (OID 1982975)
 -- Name: mtime_ap; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3036,7 +2847,6 @@ CREATE TRIGGER mtime_ap
 
 
 --
--- TOC entry 230 (OID 1982976)
 -- Name: mtime_gl; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3047,7 +2857,6 @@ CREATE TRIGGER mtime_gl
 
 
 --
--- TOC entry 234 (OID 1982977)
 -- Name: mtime_acc_trans; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3058,7 +2867,6 @@ CREATE TRIGGER mtime_acc_trans
 
 
 --
--- TOC entry 256 (OID 1982978)
 -- Name: mtime_oe; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3069,7 +2877,6 @@ CREATE TRIGGER mtime_oe
 
 
 --
--- TOC entry 235 (OID 1982979)
 -- Name: mtime_invoice; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3080,7 +2887,6 @@ CREATE TRIGGER mtime_invoice
 
 
 --
--- TOC entry 257 (OID 1982980)
 -- Name: mtime_orderitems; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3091,7 +2897,6 @@ CREATE TRIGGER mtime_orderitems
 
 
 --
--- TOC entry 231 (OID 1982981)
 -- Name: mtime_chart; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3102,7 +2907,6 @@ CREATE TRIGGER mtime_chart
 
 
 --
--- TOC entry 251 (OID 1982982)
 -- Name: mtime_tax; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3113,7 +2917,6 @@ CREATE TRIGGER mtime_tax
 
 
 --
--- TOC entry 232 (OID 1982983)
 -- Name: mtime_parts; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3124,7 +2927,6 @@ CREATE TRIGGER mtime_parts
 
 
 --
--- TOC entry 259 (OID 1982984)
 -- Name: mtime_status; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3135,7 +2937,6 @@ CREATE TRIGGER mtime_status
 
 
 --
--- TOC entry 258 (OID 1982985)
 -- Name: mtime_partsgroup; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3146,7 +2947,6 @@ CREATE TRIGGER mtime_partsgroup
 
 
 --
--- TOC entry 260 (OID 1982986)
 -- Name: mtime_inventory; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3157,7 +2957,6 @@ CREATE TRIGGER mtime_inventory
 
 
 --
--- TOC entry 261 (OID 1982987)
 -- Name: mtime_department; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3168,7 +2967,6 @@ CREATE TRIGGER mtime_department
 
 
 --
--- TOC entry 242 (OID 1982988)
 -- Name: mtime_contacts; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3179,7 +2977,6 @@ CREATE TRIGGER mtime_contacts
 
 
 --
--- TOC entry 233 (OID 1983839)
 -- Name: priceupdate_parts; Type: TRIGGER; Schema: public; Owner: postgres
 --
 
@@ -3187,5 +2984,3 @@ CREATE TRIGGER priceupdate_parts
     AFTER UPDATE ON parts
     FOR EACH ROW
     EXECUTE PROCEDURE set_priceupdate_parts();
-
-
index 0a9a047..cde5b44 100644 (file)
@@ -1,2 +1,9 @@
-Order Allow,Deny
-Deny from all
+<IfModule mod_authz_core.c>
+  # Apache 2.4
+  Require all denied
+</IfModule>
+<IfModule !mod_authz_core.c>
+  # Apache 2.2
+  Order deny,allow
+  Deny from all
+</IfModule>
index 5cf6a28..4e739b7 100755 (executable)
@@ -14,8 +14,9 @@ use SL::InstanceConfiguration;
 use SL::LXDebug;
 use SL::Layout::None;
 use SL::LxOfficeConf;
+use Support::TestSetup;
 
-our ($db_cfg, $dbh);
+our ($db_cfg, $dbh, $superuser_dbh);
 
 sub dbg {
   # diag(@_);
@@ -51,7 +52,7 @@ sub setup {
   $::lxdebug       = LXDebug->new(target => LXDebug::STDERR_TARGET);
   $::lxdebug->disable_sub_tracing;
   $::locale        = Locale->new($::lx_office_conf{system}->{language});
-  $::form          = Form->new;
+  $::form          = Support::TestSetup->create_new_form;
   $::auth          = SL::Auth->new(unit_tests_database => 1);
   $::locale        = Locale->new('de');
   $::instance_conf = SL::InstanceConfiguration->new;
@@ -66,7 +67,6 @@ sub drop_and_create_database {
     SL::DBConnect->get_options,
   );
 
-  $::auth->reset;
   my $dbh_template = SL::DBConnect->connect(@dbi_options) || BAIL_OUT("No database connection to the template database: " . $DBI::errstr);
   my $auth_dbh     = $::auth->dbconnect(1);
 
@@ -75,8 +75,6 @@ sub drop_and_create_database {
     $auth_dbh->disconnect;
 
     dbh_do($dbh_template, "DROP DATABASE \"" . $db_cfg->{db} . "\"", message => "Database could not be dropped");
-
-    $::auth->reset;
   }
 
   dbg("Creating database");
@@ -87,7 +85,8 @@ sub drop_and_create_database {
 
 sub report_success {
   $dbh->disconnect;
-  ok(1, "Database has been setup sucessfully.");
+  $superuser_dbh->disconnect if $superuser_dbh;
+  ok(1, "Database has been set up successfully.");
   done_testing();
 }
 
@@ -99,7 +98,8 @@ sub apply_dbupgrade {
 
   dbg("Applying $file");
 
-  my $error = $dbupdater->process_file($dbh, $file, $control);
+  my $script_dbh = $control && $control->{superuser_privileges} ? ($superuser_dbh // $dbh) : $dbh;
+  my $error      = $dbupdater->process_file($script_dbh, $file, $control);
 
   BAIL_OUT("Error applying $file: $error") if $error;
 }
@@ -119,6 +119,17 @@ sub create_initial_schema {
   my $dbupdater  = SL::DBUpgrade2->new(form => $::form, return_on_error => 1, silent => 1);
   my $coa        = 'Germany-DATEV-SKR03EU';
 
+  if ($db_cfg->{superuser_user} && ($db_cfg->{superuser_user} ne $db_cfg->{user})) {
+    @dbi_options = (
+      'dbi:Pg:dbname=' . $db_cfg->{db} . ';host=' . $db_cfg->{host} . ';port=' . $db_cfg->{port},
+      $db_cfg->{superuser_user},
+      $db_cfg->{superuser_password},
+      SL::DBConnect->get_options(PrintError => 0, PrintWarn => 0),
+    );
+
+    $superuser_dbh = SL::DBConnect->connect(@dbi_options) || BAIL_OUT("Database superuser connection failed: " . $DBI::errstr);
+  }
+
   apply_dbupgrade($dbupdater, "sql/lx-office.sql");
   apply_dbupgrade($dbupdater, "sql/${coa}-chart.sql");
 
@@ -165,7 +176,6 @@ sub create_client_user_and_employee {
     signature                => '',
     hide_cvar_search_options => '',
     numberformat             => '1.000,00',
-    vclimit                  => 0,
     favorites                => '',
     copies                   => '',
     menustyle                => 'v3',
index 5289a2d..044d604 100644 (file)
 ###Compilation###
 
 use strict;
+use threads;
 
 use lib 't';
 
 use Support::Files;
+use Sys::CPU;
+use Thread::Pool::Simple;
 
 use Test::More tests => scalar(@Support::Files::testitems);
 
@@ -61,34 +64,54 @@ my $perlapp = "\"$^X\"";
 
 # Test the scripts by compiling them
 
-foreach my $file (@testitems) {
-    $file =~ s/\s.*$//; # nuke everything after the first space (#comment)
-    next if !$file;    # skip null entries
+my @to_compile;
+
+sub test_compile_file {
+  my ($file, $T) = @{ $_[0] };
+
 
-    open (FILE,$file);
-    my $bang = <FILE>;
-    close (FILE);
-    my $T = "";
-    $T = "T" if $bang =~ m/#!\S*perl\s+-.*T/;
+  my $command = "$perlapp -w -c$T -Imodules/override -It -MSupport::CanonialGlobals $file 2>&1";
+  my $loginfo=`$command`;
 
-    if (-l $file) {
-        ok(1, "$file is a symlink");
+  if ($loginfo =~ /syntax ok$/im) {
+    if ($loginfo ne "$file syntax OK\n") {
+      ok(0,$file." --WARNING");
+      print $fh $loginfo;
     } else {
-        my $command = "$perlapp -w -c$T -Imodules/fallback -Imodules/override -It -MSupport::CanonialGlobals $file 2>&1";
-        my $loginfo=`$command`;
-
-        if ($loginfo =~ /syntax ok$/im) {
-            if ($loginfo ne "$file syntax OK\n") {
-                ok(0,$file." --WARNING");
-                print $fh $loginfo;
-            } else {
-                ok(1,$file);
-            }
-        } else {
-            ok(0,$file." --ERROR");
-            print $fh $loginfo;
-        }
+      ok(1,$file);
     }
+  } else {
+    ok(0,$file." --ERROR");
+    print $fh $loginfo;
+  }
 }
 
+foreach my $file (@testitems) {
+  $file =~ s/\s.*$//;           # nuke everything after the first space (#comment)
+  next if !$file;               # skip null entries
+
+  open (FILE,$file);
+  my $bang = <FILE>;
+  close (FILE);
+  my $T = "";
+  $T = "T" if $bang =~ m/#!\S*perl\s+-.*T/;
+
+  if (-l $file) {
+    ok(1, "$file is a symlink");
+  } else {
+    push @to_compile, [ $file, $T ];
+  }
+}
+
+my $pool = Thread::Pool::Simple->new(
+  min    => 2,
+  max    => Sys::CPU::cpu_count() + 1,
+  do     => [ \&test_compile_file ],
+  passid => 0,
+);
+
+$pool->add($_) for @to_compile;
+
+$pool->join;
+
 exit 0;
index 1f3a611..9517901 100644 (file)
@@ -109,7 +109,6 @@ foreach my $file (@testitems) {
 # the estimate whether a file is dirty or not is still pretty helpful, as it will catch most of the closing tags.
 # if you are in doubt about a specific file, you still have to check it manually.
 my $tags = qr/b|i|u|h[1-6]|a href.*|input|form|br|textarea|table|tr|td|th|body|head|html|p|button|select|option|script/;
-my $todo_files_re = qr{^bin/mozilla/ic\.pl$};
 foreach my $file (@testitems) {
     my $found_html_count = 0;
     my $found_html       = '';
@@ -135,13 +134,7 @@ foreach my $file (@testitems) {
         ok(0,"$file contains at least $found_html_count html tags.");
       }
     } else {
-      if ($file =~ $todo_files_re) {
-        TODO: { local $TODO = q(This file is known to have lots of old cruft.);
-          ok(0,"$file contains at least $found_html_count html tags.");
-        }
-      } else {
-        ok(0,"$file contains at least $found_html_count html tags.");
-      }
+      ok(0,"$file contains at least $found_html_count html tags.");
     }
 }
 
index b4f41f6..436fc7b 100644 (file)
@@ -1,22 +1,22 @@
 # -*- Mode: perl; indent-tabs-mode: nil -*-
-# 
+#
 # The contents of this file are subject to the Mozilla Public
 # License Version 1.1 (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.mozilla.org/MPL/
-# 
+#
 # 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.
-# 
+#
 # The Original Code are the Bugzilla Tests.
-# 
+#
 # The Initial Developer of the Original Code is Zach Lipton
-# Portions created by Zach Lipton are 
+# Portions created by Zach Lipton are
 # Copyright (C) 2001 Zach Lipton.  All
 # Rights Reserved.
-# 
+#
 # Contributor(s): Zach Lipton <zach@zachlipton.com>
 
 
 ###Safesystem####
 
 use strict;
+use threads;
 
 use lib 't';
 
 use Support::Files;
+use Sys::CPU;
+use Thread::Pool::Simple;
 
 use Test::More tests => scalar(@Support::Files::testitems);
 
@@ -46,20 +49,34 @@ my $fh;
     }
 }
 
-my @testitems = @Support::Files::testitems; 
+my @testitems = @Support::Files::testitems;
 my $perlapp = "\"$^X\"";
 
+sub test_file {
+  my ($file)  = @_;
+  my $command = "$perlapp -c -It -MSupport::Systemexec $file 2>&1";
+  my $loginfo =`$command`;
+
+  if ($loginfo =~ /arguments for Support::Systemexec::(system|exec)/im) {
+    ok(0,"$file DOES NOT use proper system or exec calls");
+    print $fh $loginfo;
+  } else {
+    ok(1,"$file uses proper system and exec calls");
+  }
+}
+
+my $pool = Thread::Pool::Simple->new(
+  min    => 2,
+  max    => Sys::CPU::cpu_count() + 1,
+  do     => [ \&test_file ],
+  passid => 0,
+);
+
 foreach my $file (@testitems) {
-    $file =~ s/\s.*$//; # nuke everything after the first space (#comment)
-    next if (!$file); # skip null entries
-    my $command = "$perlapp -c -It -MSupport::Systemexec $file 2>&1";
-    my $loginfo=`$command`;
-    if ($loginfo =~ /arguments for Support::Systemexec::(system|exec)/im) {
-        ok(0,"$file DOES NOT use proper system or exec calls");
-        print $fh $loginfo;
-    } else {
-        ok(1,"$file uses proper system and exec calls");
-    }
+  $file =~ s/\s.*$//;           # nuke everything after the first space (#comment)
+  $pool->add($file) if $file;   # skip null entries
 }
 
+$pool->join;
+
 exit 0;
index e3f2ac6..d8d2360 100644 (file)
@@ -33,25 +33,33 @@ BEGIN { # yes the indenting is off, deal with it
 CONTANTS
 anyways
 arbitary
+cofigur
 custemer
 databasa
 dependan
+execept
 existance
 existant
 fomr
 invoce
+lenght
+occured
 paramater
+pirce
 postition
+primt
 puchase
 puhs
 sekf
+seperat
+substract
 sucess
+unkown
 varsion
-pirce
 wether
 );
 
-$testcount = scalar(@Support::Files::testitems);
+$testcount = scalar(@Support::Files::files);
 }
 
 use Test::More tests => $testcount;
@@ -70,7 +78,7 @@ my $fh;
     }
 }
 
-my @testitems = @Support::Files::testitems;
+my @testitems = @Support::Files::files;
 
 # at last, here we actually run the test...
 my $evilwordsregexp = join('|', @evilwords);
index d05f365..53103a2 100644 (file)
 # Contributor(s): Zach Lipton <zach@zachlipton.com>
 #                 Joel Peshkin <bugreport@peshkin.net>
 
-
 package Support::Files;
 
+use strict;
+
 use File::Find;
 
+our @testitems;
+
 # exclude_deps is a hash of arrays listing the files to be excluded
 # if a module is not available
 #
-@additional_files = ();
-%exclude_deps = (
+our @additional_files = ();
+our %exclude_deps = (
     'XML::Twig' => ['importxml.pl'],
     'Net::LDAP' => ['Bugzilla/Auth/Verify/LDAP.pm'],
     'Email::Reply' => ['email_in.pl'],
@@ -37,9 +40,10 @@ use File::Find;
 );
 
 
-@files = glob('*');
+our @files = glob('*');
 find(sub { push(@files, $File::Find::name) if $_ =~ /\.pm$/;}, 'SL');
 find(sub { push(@files, $File::Find::name) if $_ =~ /\.pl$/;}, qw(bin/mozilla sql/Pg-upgrade2));
+find(sub { push(@files, $File::Find::name) if $_ =~ /\.html$/;}, qw(templates/webpages));
 
 sub have_pkg {
     my ($pkg) = @_;
@@ -49,8 +53,8 @@ sub have_pkg {
     return !($@);
 }
 
-@exclude_files    = ();
-foreach $dep (keys(%exclude_deps)) {
+our @exclude_files    = ();
+foreach my $dep (keys(%exclude_deps)) {
     if (!have_pkg($dep)) {
         push @exclude_files, @{$exclude_deps{$dep}};
     }
@@ -73,7 +77,7 @@ sub isTestingFile {
     return undef;
 }
 
-foreach $currentfile (@files) {
+foreach my $currentfile (@files) {
     if (isTestingFile($currentfile)) {
         push(@testitems,$currentfile);
     }
index 037cfb5..6937b2e 100644 (file)
@@ -29,12 +29,12 @@ sub login {
   $::lxdebug       = LXDebug->new(target => LXDebug::STDERR_TARGET);
   $::lxdebug->disable_sub_tracing;
   $::locale        = Locale->new($::lx_office_conf{system}->{language});
-  $::form          = Form->new;
+  $::form          = Support::TestSetup->create_new_form;
   $::auth          = SL::Auth->new(unit_tests_database => 1);
   die "Cannot find client with ID or name '$client'" if !$::auth->set_client($client);
 
   $::instance_conf = SL::InstanceConfiguration->new;
-  $::request       = SL::Request->new( cgi => CGI->new({}), layout => SL::Layout::None->new );
+  $::request       = Support::TestSetup->create_new_request;
 
   die 'cannot reach auth db'               unless $::auth->session_tables_present;
 
@@ -48,22 +48,38 @@ sub login {
 
   $SIG{__DIE__} = sub { Carp::confess( @_ ) } if $::lx_office_conf{debug}->{backtrace_on_die};
 
+
   return 1;
 }
 
-sub templates_cache_writable {
-  my $dir = $::lx_office_conf{paths}->{userspath} . '/templates-cache';
-  return 1 if -w $dir;
+sub create_new_form { Form->new('') }
 
-  # Try actually creating a file. Due to ACLs this might be possible
-  # even if the basic Unix permissions and Perl's -w test say
-  # otherwise.
-  my $file = "${dir}/.writetest";
-  my $out  = IO::File->new($file, "w") || return 0;
-  $out->close;
-  unlink $file;
+sub create_new_request {
+  my $self = shift;
 
-  return 1;
+  my $request = SL::Request->new(
+    cgi    => CGI->new({}),
+    layout => SL::Layout::None->new,
+    @_,
+  );
+
+  $request->presenter->{template} = Template->new(template_config()) || die;
+
+  return $request;
+}
+
+sub template_config {
+  return {
+    INTERPOLATE  => 0,
+    EVAL_PERL    => 0,
+    ABSOLUTE     => 1,
+    CACHE_SIZE   => 0,
+    PLUGIN_BASE  => 'SL::Template::Plugin',
+    INCLUDE_PATH => '.:templates/webpages/',
+    COMPILE_DIR  => 'users/templates-cache-for-tests',
+    COMPILE_EXT  => '.tcc',
+    ENCODING     => 'utf8',
+  };
 }
 
 1;
diff --git a/t/ar/ar.t b/t/ar/ar.t
new file mode 100644 (file)
index 0000000..c5583f1
--- /dev/null
+++ b/t/ar/ar.t
@@ -0,0 +1,227 @@
+use strict;
+use Test::More tests => 7;
+
+use lib 't';
+use Support::TestSetup;
+use Carp;
+use Test::Exception;
+use SL::DB::Customer;
+use SL::DB::Employee;
+use SL::DB::Invoice;
+use SL::DATEV qw(:CONSTANTS);
+use SL::Dev::CustomerVendor qw(new_customer);
+use Data::Dumper;
+
+my ($customer, $employee, $ar_tax_19, $ar_tax_7, $ar_tax_0);
+my ($ar_chart, $bank, $ar_amount_chart);
+my $config = {};
+$config->{numberformat} = '1.000,00';
+
+sub reset_state {
+  my %params = @_;
+
+  $params{$_} ||= {} for qw(buchungsgruppe customer);
+
+  clear_up();
+
+  $employee        = SL::DB::Manager::Employee->current                       || croak "No employee";
+  $ar_tax_19       = SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19) || croak "No 19% tax";
+  $ar_tax_7        = SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.07) || croak "No 7% tax";
+  $ar_tax_0        = SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.00) || croak "No 0% tax";
+
+  $customer = new_customer()->save; # new customer with default name "Testkunde"
+
+  $ar_chart        = SL::DB::Manager::Chart->find_by(accno => '1400') || croak "Can't find Forderungen";
+  $bank            = SL::DB::Manager::Chart->find_by(accno => '1200') || croak "Can't find Bank";
+  $ar_amount_chart = SL::DB::Manager::Chart->find_by(accno => '8590') || croak "Can't find verrechn., eigentlich Anzahlungen";
+
+};
+
+Support::TestSetup::login();
+
+reset_state();
+
+# check ar without tax
+my $invoice = _ar(customer     => $customer,
+                  amount       => 100,
+                  with_payment => 1,
+                  notes        => 'ar without tax',
+                 );
+
+# for testing load a fresh instance of the invoice from the database
+my $inv = SL::DB::Invoice->new(id => $invoice->id)->load;
+if ( $inv ) {
+  my $number_of_acc_trans = scalar @{ $inv->transactions };
+  is($::form->round_amount($inv->amount) , 100                           , "invoice_amount = 100");
+  is($number_of_acc_trans                , 5                             , "number of transactions");
+  is($inv->datepaid->to_kivitendo        , DateTime->today->to_kivitendo , "datepaid");
+  is($inv->amount - $inv->paid           , 0                             , "paid = amount ");
+  is($inv->gldate->to_kivitendo, $inv->transactions->[0]->gldate->to_kivitendo, "gldate matches in ar and acc_trans");
+} else {
+  ok 0, "couldn't find first invoice";
+}
+
+# check ar with tax
+my $invoice2 = _ar_with_tax(customer     => $customer,
+                            amount       => 200,
+                            with_payment => 1,
+                            notes        => 'ar with taxincluded',
+                           );
+my $inv_with_tax = SL::DB::Invoice->new(id => $invoice2->id)->load;
+if ( $inv_with_tax ) {
+  is(scalar @{ $inv_with_tax->transactions }, 7,  "number of transactions for inv_with_tax");
+} else {
+  ok 0, "couldn't find second invoice";
+}
+
+# general checks
+is(SL::DB::Manager::Invoice->get_all_count(), 2,  "total number of invoices created is 2");
+
+done_testing;
+clear_up();
+
+1;
+
+sub clear_up {
+  SL::DB::Manager::AccTransaction->delete_all(all => 1);
+  SL::DB::Manager::Invoice->delete_all(       all => 1);
+  SL::DB::Manager::Customer->delete_all(      all => 1);
+};
+
+sub _ar {
+  my %params = @_;
+
+  my $amount       = $params{amount}       || croak "ar needs param amount";
+  my $customer     = $params{customer}     || croak "ar needs param customer";
+  my $transdate    = $params{transdate}    || DateTime->today;
+  my $gldate       = $params{gldate}       || DateTime->today->add(days => 1);
+  my $with_payment = $params{with_payment} || 0;
+
+  # SL::DB::Invoice has a _before_save_set_invnumber hook, so we don't need to pass invnumber
+  my $invoice = SL::DB::Invoice->new(
+      invoice          => 0,
+      amount           => $amount,
+      netamount        => $amount,
+      transdate        => $transdate,
+      gldate           => $gldate,
+      taxincluded      => 'f',
+      customer_id      => $customer->id,
+      taxzone_id       => $customer->taxzone_id,
+      currency_id      => $customer->currency_id,
+      globalproject_id => $params{project},
+      notes            => $params{notes},
+      transactions     => [],
+  );
+
+  my $db = $invoice->db;
+
+  $db->with_transaction( sub {
+
+    $invoice->add_ar_amount_row(
+      amount     => $amount / 2,
+      chart      => $ar_amount_chart,
+      tax_id     => $ar_tax_0->id,
+    );
+    $invoice->add_ar_amount_row(
+      amount     => $amount / 2,
+      chart      => $ar_amount_chart,
+      tax_id     => $ar_tax_0->id,
+    );
+
+    $invoice->create_ar_row( chart => $ar_chart );
+
+    _save_and_pay_and_check(invoice     => $invoice,
+                            bank        => $bank,
+                            pay         => $with_payment,
+                            datev_check => 1,
+                           );
+
+    1;
+
+  }) || die "something went wrong: " . $db->error;
+  return $invoice;
+};
+
+sub _ar_with_tax {
+  my %params = @_;
+
+  my $amount       = $params{amount}       || croak "ar needs param amount";
+  my $customer     = $params{customer}     || croak "ar needs param customer";
+  my $transdate    = $params{transdate}    || DateTime->today;
+  my $gldate       = $params{gldate}       || DateTime->today->add(days => 1);
+  my $with_payment = $params{with_payment} || 0;
+
+  my $invoice = SL::DB::Invoice->new(
+    invoice          => 0,
+    amount           => $amount,
+    netamount        => $amount,
+    transdate        => $transdate,
+    gldate           => $gldate,
+    taxincluded      => 'f',
+    customer_id      => $customer->id,
+    taxzone_id       => $customer->taxzone_id,
+    currency_id      => $customer->currency_id,
+    globalproject_id => $params{project},
+    notes            => $params{notes},
+    transactions     => [],
+  );
+
+  my $db = $invoice->db;
+
+  $db->with_transaction( sub {
+
+    # TODO: check for currency and exchange rate
+
+    $invoice->add_ar_amount_row(
+      amount     => $amount / 2,
+      chart      => $ar_amount_chart,
+      tax_id     => $ar_tax_19->id,
+    );
+    $invoice->add_ar_amount_row(
+      amount     => $amount / 2,
+      chart      => $ar_amount_chart,
+      tax_id     => $ar_tax_19->id,
+    );
+
+    $invoice->create_ar_row( chart => $ar_chart );
+    _save_and_pay_and_check(invoice     => $invoice,
+                            bank        => $bank,
+                            pay         => $with_payment,
+                            datev_check => 1,
+                           );
+
+    1;
+  }) || die "something went wrong: " . $db->error;
+  return $invoice;
+};
+
+sub _save_and_pay_and_check {
+  my %params = @_;
+  my $invoice     = $params{invoice} // croak "invoice missing";
+  my $datev_check = $params{datev_check} // 1; # do datev check by default
+  croak "no bank" unless ref $params{bank} eq 'SL::DB::Chart';
+
+  # make sure invoice is saved before making payments
+  my $return = $invoice->save;
+
+  $invoice->pay_invoice(chart_id     => $params{bank}->id,
+                        amount       => $invoice->amount,
+                        transdate    => $invoice->transdate->to_kivitendo,
+                        payment_type => 'without_skonto',  # default if not specified
+                       ) if $params{pay};
+
+  if ($datev_check) {
+    my $datev = SL::DATEV->new(
+      dbh        => $invoice->db->dbh,
+      trans_id   => $invoice->id,
+    );
+
+    $datev->generate_datev_data;
+
+    # _save_and_pay_and_check should always be called inside a with_transaction block
+    if ($datev->errors) {
+      $invoice->db->dbh->rollback;
+      die join "\n", $::locale->text('DATEV check returned errors:'), $datev->errors;
+    }
+  };
+};
diff --git a/t/auth/evaluate_rights_ary.t b/t/auth/evaluate_rights_ary.t
new file mode 100644 (file)
index 0000000..7203d0a
--- /dev/null
@@ -0,0 +1,29 @@
+use strict;
+
+use Test::More;
+
+use lib 't';
+use Support::TestSetup;
+
+Support::TestSetup::login();
+
+use_ok 'SL::Auth';
+
+ok( SL::Auth::evaluate_rights_ary(['1']), 'simple: right');
+ok(!SL::Auth::evaluate_rights_ary(['0']), 'simple: no right');
+ok( SL::Auth::evaluate_rights_ary(['1', '|', 0]), 'simple: or');
+ok( SL::Auth::evaluate_rights_ary(['0', '|', '1']), 'simple: or 2');
+ok(!SL::Auth::evaluate_rights_ary(['1', '&', '0']), 'simple: and');
+ok(!SL::Auth::evaluate_rights_ary(['0', '&', '1']), 'simple: and 2');
+ok( SL::Auth::evaluate_rights_ary(['1', '&', '1']), 'simple: and 3');
+ok(!SL::Auth::evaluate_rights_ary(['!', '1']), 'simple: not');
+ok( SL::Auth::evaluate_rights_ary(['!', '0']), 'simple: not 2');
+ok(!SL::Auth::evaluate_rights_ary(['!', '!', '0']), 'simple: double not');
+ok( SL::Auth::evaluate_rights_ary(['!', ['0']]), 'not 1');
+ok(!SL::Auth::evaluate_rights_ary(['!', ['1']]), 'not 2');
+ok( SL::Auth::evaluate_rights_ary(['!', '!', ['1']]), 'double not');
+ok( SL::Auth::evaluate_rights_ary([ '!', ['!', ['1', '&', '1'], '&', '!', '!', ['1', '|', '!', '1']] ]), 'something more coplex');
+
+done_testing;
+
+1;
diff --git a/t/background_job/convert_time_recordings.t b/t/background_job/convert_time_recordings.t
new file mode 100644 (file)
index 0000000..1624cc1
--- /dev/null
@@ -0,0 +1,652 @@
+use Test::More tests => 52;
+
+use strict;
+
+use lib 't';
+use utf8;
+
+use Support::TestSetup;
+use Test::Exception;
+use DateTime;
+use Rose::DB::Object::Helpers qw(forget_related);
+
+use SL::DB::BackgroundJob;
+use SL::DB::DeliveryOrder;
+
+use_ok 'SL::BackgroundJob::ConvertTimeRecordings';
+
+use SL::Dev::ALL qw(:ALL);
+
+Support::TestSetup::login();
+
+sub clear_up {
+  foreach (qw(TimeRecording OrderItem Order DeliveryOrder Project Part Customer RecordLink)) {
+    "SL::DB::Manager::${_}"->delete_all(all => 1);
+  }
+  SL::DB::Manager::Employee->delete_all(where => [ '!login' => 'unittests' ]);
+};
+
+########################################
+
+$::myconfig{numberformat} = '1000.00';
+my $old_locale = $::locale;
+# set locale to en so we can match errors
+$::locale = Locale->new('en');
+
+
+clear_up();
+
+########################################
+# two time recordings, one order linked with project_id in time recording entry
+########################################
+my $part     = new_service(partnumber => 'Serv1', unit => 'Std')->save;
+my $project  = create_project(projectnumber => 'p1', description => 'Project 1');
+my $customer = new_customer()->save;
+$::form->{type} = 'sales_order';
+
+# sales order with globalproject_id
+my $sales_order = create_sales_order(
+  save             => 1,
+  customer         => $customer,
+  globalproject    => $project,
+  taxincluded      => 0,
+  orderitems       => [ create_order_item(part => $part, qty => 3, sellprice => 70), ]
+);
+
+my @time_recordings;
+push @time_recordings, new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 10, minute =>  5),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 11, minute =>  5),
+  customer   => $customer,
+  project    => $project,
+  part       => $part,
+)->save;
+push @time_recordings, new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 12, minute =>  5),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 14, minute =>  5),
+  customer   => $customer,
+  project    => $project,
+  part       => $part,
+)->save;
+
+my %data   = (
+  link_order => 1,
+  from_date  => '01.01.2021',
+  to_date    => '30.04.2021',
+);
+my $db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+my $job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+my $ret    = $job->run($db_obj);
+
+is_deeply($job->{job_errors}, [], 'no errros');
+like($ret, qr{^Number of delivery orders created: 1}, 'one delivery order created');
+
+my $linked_dos = $sales_order->linked_records(to => 'DeliveryOrder');
+is(scalar @$linked_dos, 1, 'one delivery order linked to order');
+is($linked_dos->[0]->globalproject_id, $sales_order->globalproject_id, 'project ids match');
+
+my $linked_items = $sales_order->items->[0]->linked_records(to => 'DeliveryOrderItem');
+is(scalar @$linked_items, 1, 'one delivery order item linked to order item');
+is($linked_items->[0]->qty*1, 3, 'qty in delivery order');
+is($linked_items->[0]->base_qty*1, 3, 'base_qty in delivery order');
+
+# reload order and orderitems to get changes to deliverd and ship
+Rose::DB::Object::Helpers::forget_related($sales_order, 'orderitems');
+$sales_order->load;
+
+ok($sales_order->delivered, 'related order is delivered');
+is($sales_order->items->[0]->ship*1, 3, 'ship in related order');
+
+clear_up();
+
+
+########################################
+# two time recordings, one order linked with project_id in time recording entry
+# unit in order is 'min', but part is 'Std'
+########################################
+$part     = new_service(partnumber => 'Serv1', unit => 'Std')->save;
+$project  = create_project(projectnumber => 'p1', description => 'Project 1');
+$customer = new_customer()->save;
+
+$sales_order = create_sales_order(
+  save             => 1,
+  customer         => $customer,
+  globalproject    => $project,
+  taxincluded      => 0,
+  orderitems       => [ create_order_item(part => $part, qty => 180, unit => 'min', sellprice => 70), ]
+);
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 10, minute => 10),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 11, minute => 10),
+  customer   => $customer,
+  project    => $project,
+  part       => $part,
+)->save;
+push @time_recordings, new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 12, minute => 10),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 14, minute => 10),
+  customer   => $customer,
+  project    => $project,
+  part       => $part,
+)->save;
+
+%data = (
+  link_order => 1,
+  from_date  => '01.04.2021',
+  to_date    => '30.04.2021',
+);
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+$ret    = $job->run($db_obj);
+
+$linked_dos = $sales_order->linked_records(to => 'DeliveryOrder');
+$linked_items = $sales_order->items->[0]->linked_records(to => 'DeliveryOrderItem');
+is($linked_items->[0]->qty*1, 3, 'different units: qty in delivery order');
+is($linked_items->[0]->base_qty*1, 3, 'different units: base_qty in delivery order');
+
+# reload order and orderitems to get changes to deliverd and ship
+Rose::DB::Object::Helpers::forget_related($sales_order, 'orderitems');
+$sales_order->load;
+
+ok($sales_order->delivered, 'different units: related order is delivered');
+is($sales_order->items->[0]->ship*1, 180, 'different units: ship in related order');
+
+clear_up();
+
+
+########################################
+# two time recordings, one order linked with project_id in time recording entry
+# unit in order is 'Std', but part is 'min'
+########################################
+$part     = new_service(partnumber => 'Serv1', unit => 'min')->save;
+$project  = create_project(projectnumber => 'p1', description => 'Project 1');
+$customer = new_customer()->save;
+
+$sales_order = create_sales_order(
+  save             => 1,
+  customer         => $customer,
+  globalproject    => $project,
+  taxincluded      => 0,
+  orderitems       => [ create_order_item(part => $part, qty => 2, unit => 'Std', sellprice => 70), ]
+);
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 10, minute => 10),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 11, minute => 10),
+  customer   => $customer,
+  project    => $project,
+  part       => $part,
+)->save;
+push @time_recordings, new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 12, minute => 10),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 13, minute => 10),
+  customer   => $customer,
+  project    => $project,
+  part       => $part,
+)->save;
+
+%data = (
+  link_order => 1,
+  from_date  => '01.04.2021',
+  to_date    => '30.04.2021',
+);
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+$ret    = $job->run($db_obj);
+
+$linked_dos = $sales_order->linked_records(to => 'DeliveryOrder');
+$linked_items = $sales_order->items->[0]->linked_records(to => 'DeliveryOrderItem');
+is($linked_items->[0]->qty*1, 2, 'different units 2: qty in delivery order');
+is($linked_items->[0]->base_qty*1, 120, 'different units 2: base_qty in delivery order');
+
+# reload order and orderitems to get changes to deliverd and ship
+Rose::DB::Object::Helpers::forget_related($sales_order, 'orderitems');
+$sales_order->load;
+
+ok($sales_order->delivered, 'different units 2: related order is delivered');
+is($sales_order->items->[0]->ship*1, 2, 'different units 2: ship in related order');
+
+clear_up();
+
+
+########################################
+# two time recordings, one with start/end one with date/duration
+########################################
+$part     = new_service(partnumber => 'Serv1', unit => 'min')->save;
+$customer = new_customer()->save;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 10, minute => 10),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 11, minute => 10),
+  customer   => $customer,
+  part       => $part,
+)->save;
+
+push @time_recordings, new_time_recording(
+  date       => DateTime->new(year => 2021, month =>  4, day => 19),
+  duration   => 120,
+  start_time => undef,
+  end_time   => undef,
+  customer   => $customer,
+  part       => $part,
+)->save;
+
+%data = (
+  link_order => 0,
+  from_date  => '01.04.2021',
+  to_date    => '30.04.2021',
+);
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+$ret    = $job->run($db_obj);
+
+my $dos = SL::DB::Manager::DeliveryOrder->get_all(where => [customer_id => $customer->id]);
+is($dos->[0]->items->[0]->qty*1, 180/60, 'date/duration and start/end: qty in delivery order');
+is($dos->[0]->items->[0]->base_qty*1, 180, 'date/duration and start/end2: base_qty in delivery order');
+
+clear_up();
+
+
+########################################
+# time recording, linked with order_id
+########################################
+$part     = new_service(partnumber => 'Serv1', unit => 'Std')->save;
+$customer = new_customer()->save;
+
+# sales order with globalproject_id
+$sales_order = create_sales_order(
+  save             => 1,
+  customer         => $customer,
+  taxincluded      => 0,
+  orderitems       => [ create_order_item(part => $part, qty => 3, sellprice => 70), ]
+);
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 10, minute =>  5),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 11, minute =>  5),
+  customer   => $customer,
+  order      => $sales_order,
+  part       => $part,
+)->save;
+
+%data = (
+  link_order      => 1,
+  from_date       => '01.04.2021',
+  to_date         => '30.04.2021',
+  customernumbers => [$customer->number],
+);
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+$ret    = $job->run($db_obj);
+
+is_deeply($job->{job_errors}, [], 'no errros');
+like($ret, qr{^Number of delivery orders created: 1}, 'linked by order_id: one delivery order created');
+
+$linked_dos = $sales_order->linked_records(to => 'DeliveryOrder');
+is(scalar @$linked_dos, 1, 'linked by order_id: one delivery order linked to order');
+
+$linked_items = $sales_order->items->[0]->linked_records(to => 'DeliveryOrderItem');
+is(scalar @$linked_items, 1, 'linked by order_id: one delivery order item linked to order item');
+is($linked_items->[0]->qty*1, 1, 'linked by order_id: qty in delivery order');
+is($linked_items->[0]->base_qty*1, 1, 'linked by order_id: base_qty in delivery order');
+
+# reload order and orderitems to get changes to deliverd and ship
+Rose::DB::Object::Helpers::forget_related($sales_order, 'orderitems');
+$sales_order->load;
+
+is($sales_order->items->[0]->ship*1, 1, 'linked by order_id: ship in related order');
+
+clear_up();
+
+
+########################################
+# override project and part
+########################################
+$part     = new_service(partnumber => 'Serv1', unit => 'Std')->save;
+my $part2    = new_service(partnumber => 'Serv2', unit => 'min')->save;
+$project  = create_project(projectnumber => 'p1', description => 'Project 1');
+my $project2 = create_project(projectnumber => 'p2', description => 'Project 2');
+$customer = new_customer()->save;
+
+$sales_order = create_sales_order(
+  save             => 1,
+  customer         => $customer,
+  globalproject    => $project,
+  taxincluded      => 0,
+  orderitems       => [ create_order_item(part => $part, qty => 180, unit => 'min', sellprice => 70), ]
+);
+my $sales_order2 = create_sales_order(
+  save             => 1,
+  customer         => $customer,
+  globalproject    => $project,
+  taxincluded      => 0,
+  orderitems       => [ create_order_item(part => $part2, qty => 180, unit => 'min', sellprice => 70), ]
+);
+
+new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 10, minute => 10),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 11, minute => 10),
+  customer   => $customer,
+  project    => $project,
+  part       => $part,
+)->save;
+new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 11, minute => 10),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 12, minute => 10),
+  customer   => $customer,
+  project    => $project2,
+  part       => $part2,
+)->save;
+new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 12, minute => 10),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 13, minute => 10),
+  customer   => $customer,
+)->save;
+
+%data = (
+  link_order          => 1,
+  from_date           => '01.04.2021',
+  to_date             => '30.04.2021',
+  override_part_id    => $part->id,
+  override_project_id => $project->id,
+);
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+$ret    = $job->run($db_obj);
+
+$linked_dos = $sales_order->linked_records(to => 'DeliveryOrder');
+is($linked_dos->[0]->globalproject_id, $project->id, 'overriden part and project: project in delivery order');
+
+$linked_items = $sales_order->items->[0]->linked_records(to => 'DeliveryOrderItem');
+is($linked_items->[0]->qty*1, 3, 'overriden part and project: qty in delivery order');
+is($linked_items->[0]->base_qty*1, 3, 'overriden part and project: base_qty in delivery order');
+is($linked_items->[0]->parts_id, $part->id, 'overriden part and project: part id');
+
+my $linked_dos2 = $sales_order2->linked_records(to => 'DeliveryOrder');
+is(scalar @$linked_dos2, 0, 'overriden part and project: no delivery order for unused order');
+
+# reload order and orderitems to get changes to deliverd and ship
+Rose::DB::Object::Helpers::forget_related($sales_order, 'orderitems');
+$sales_order->load;
+Rose::DB::Object::Helpers::forget_related($sales_order2, 'orderitems');
+$sales_order2->load;
+
+is($sales_order ->items->[0]->ship||0, 180, 'overriden part and project: ship in related order');
+is($sales_order2->items->[0]->ship||0,   0, 'overriden part and project: ship in not related order');
+
+clear_up();
+
+
+########################################
+# default project and part
+########################################
+$part     = new_service(partnumber => 'Serv1', unit => 'Std')->save;
+$project  = create_project(projectnumber => 'p1', description => 'Project 1');
+$customer = new_customer()->save;
+
+$sales_order = create_sales_order(
+  save             => 1,
+  customer         => $customer,
+  globalproject    => $project,
+  taxincluded      => 0,
+  orderitems       => [ create_order_item(part => $part, qty => 180, unit => 'min', sellprice => 70), ]
+);
+
+new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 10, minute => 10),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 11, minute => 40),
+  customer   => $customer,
+)->save;
+
+%data = (
+  link_order         => 1,
+  from_date          => '01.04.2021',
+  to_date            => '30.04.2021',
+  default_part_id    => $part->id,
+  default_project_id => $project->id,
+);
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+$ret    = $job->run($db_obj);
+
+$linked_dos = $sales_order->linked_records(to => 'DeliveryOrder');
+is($linked_dos->[0]->globalproject_id, $project->id, 'default and project: project in delivery order');
+
+$linked_items = $sales_order->items->[0]->linked_records(to => 'DeliveryOrderItem');
+is($linked_items->[0]->qty*1, 1.5, 'default part and project: qty in delivery order');
+is($linked_items->[0]->base_qty*1, 1.5, 'default part and project: base_qty in delivery order');
+is($linked_items->[0]->parts_id, $part->id, 'default part and project: part id');
+
+# reload order and orderitems to get changes to deliverd and ship
+Rose::DB::Object::Helpers::forget_related($sales_order, 'orderitems');
+$sales_order->load;
+
+is($sales_order->items->[0]->ship*1, 90, 'default part and project: ship in related order');
+
+clear_up();
+
+
+########################################
+# check rounding
+########################################
+$part     = new_service(partnumber => 'Serv1', unit => 'Std')->save;
+$customer = new_customer()->save;
+
+$sales_order = create_sales_order(
+  save             => 1,
+  customer         => $customer,
+  taxincluded      => 0,
+  orderitems       => [ create_order_item(part => $part, qty => 3, sellprice => 70), ]
+);
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 10, minute =>  0),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 10, minute =>  6),
+  customer   => $customer,
+  order      => $sales_order,
+  part       => $part,
+)->save;
+
+%data   = (
+  from_date  => '01.01.2021',
+  to_date    => '30.04.2021',
+  link_order => 1,
+  rounding   => 1,
+);
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+$ret    = $job->run($db_obj);
+
+$linked_dos   = $sales_order->linked_records(to => 'DeliveryOrder');
+$linked_items = $sales_order->items->[0]->linked_records(to => 'DeliveryOrderItem');
+is($linked_items->[0]->qty*1, 0.25, 'rounding to quarter hour: qty in delivery order');
+is($linked_items->[0]->base_qty*1, 0.25, 'rounding to quarter hour: base_qty in delivery order');
+
+# reload order and orderitems to get changes to deliverd and ship
+Rose::DB::Object::Helpers::forget_related($sales_order, 'orderitems');
+$sales_order->load;
+
+is($sales_order->items->[0]->ship*1, 0.25, 'rounding to quarter hour: ship in related order');
+
+clear_up();
+
+
+########################################
+# check rounding
+########################################
+$part     = new_service(partnumber => 'Serv1', unit => 'Std')->save;
+$customer = new_customer()->save;
+
+$sales_order = create_sales_order(
+  save             => 1,
+  customer         => $customer,
+  taxincluded      => 0,
+  orderitems       => [ create_order_item(part => $part, qty => 3, sellprice => 70), ]
+);
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(
+  start_time => DateTime->new(year => 2021, month =>  4, day => 19, hour => 10, minute =>  0),
+  end_time   => DateTime->new(year => 2021, month =>  4, day => 19, hour => 10, minute =>  6),
+  customer   => $customer,
+  order      => $sales_order,
+  part       => $part,
+)->save;
+
+%data   = (
+  from_date  => '01.01.2021',
+  to_date    => '30.04.2021',
+  link_order => 1,
+  rounding   => 0,
+);
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+$ret    = $job->run($db_obj);
+
+$linked_dos   = $sales_order->linked_records(to => 'DeliveryOrder');
+$linked_items = $sales_order->items->[0]->linked_records(to => 'DeliveryOrderItem');
+is($linked_items->[0]->qty*1, 0.1, 'no rounding: qty in delivery order');
+is($linked_items->[0]->base_qty*1, 0.1, 'no rounding: base_qty in delivery order');
+
+# reload order and orderitems to get changes to deliverd and ship
+Rose::DB::Object::Helpers::forget_related($sales_order, 'orderitems');
+$sales_order->load;
+
+is($sales_order->items->[0]->ship*1, 0.1, 'no rounding: ship in related order');
+
+clear_up();
+
+
+########################################
+# are wrong params detected?
+########################################
+%data = (
+  from_date       => 'x01.04.2021',
+);
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+
+my $err_msg = '';
+eval { $ret = $job->run($db_obj);  1; } or do {$err_msg = $@};
+ok($err_msg =~ '^Cannot convert date.', 'wrong date string detected');
+
+#####
+
+$customer = new_customer()->save;
+%data = (
+  customernumbers => ['a fantasy', $customer->number],
+);
+
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+
+$err_msg = '';
+eval { $ret = $job->run($db_obj);  1; } or do {$err_msg = $@};
+ok($err_msg =~ '^Not all customer numbers are valid', 'wrong customer number detected');
+
+#####
+
+%data = (
+  customernumbers => '123',
+);
+
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+
+$err_msg = '';
+eval { $ret = $job->run($db_obj);  1; } or do {$err_msg = $@};
+ok($err_msg =~ '^Customer numbers must be given in an array', 'wrong customer number data type detected');
+
+#####
+
+%data = (
+  override_part_id => '123',
+);
+
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+
+$err_msg = '';
+eval { $ret = $job->run($db_obj);  1; } or do {$err_msg = $@};
+ok($err_msg =~ '^No valid part found by given override part id', 'invalid part id detected');
+
+#####
+
+$part = new_service(partnumber => 'Serv1', unit => 'Std', obsolete => 1)->save;
+%data = (
+  override_part_id => $part->id,
+);
+
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+
+$err_msg = '';
+eval { $ret = $job->run($db_obj);  1; } or do {$err_msg = $@};
+ok($err_msg =~ '^No valid part found by given override part id', 'obsolete part detected');
+
+#####
+
+%data = (
+  override_project_id => 123,
+);
+
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+
+$err_msg = '';
+eval { $ret = $job->run($db_obj);  1; } or do {$err_msg = $@};
+ok($err_msg =~ '^No valid project found by given override project id', 'invalid project id detected');
+
+#####
+
+$project = create_project(projectnumber => 'p1', description => 'Project 1', valid => 0)->save;
+%data = (
+  override_project_id => $project->id,
+);
+
+$db_obj = SL::DB::BackgroundJob->new();
+$db_obj->set_data(%data);
+$job    = SL::BackgroundJob::ConvertTimeRecordings->new;
+
+$err_msg = '';
+eval { $ret = $job->run($db_obj);  1; } or do {$err_msg = $@};
+ok($err_msg =~ '^No valid project found by given override project id', 'invalid project detected');
+
+#####
+
+clear_up();
+
+
+########################################
+
+$::locale = $old_locale;
+
+1;
+
+#####
+# vim: ft=perl
+# set emacs to perl mode
+# Local Variables:
+# mode: perl
+# End:
index 9f55a0c..1ce5fe6 100644 (file)
@@ -14,7 +14,7 @@ sub today_local {
 
 package main;
 
-use Test::More tests => 80;
+use Test::More tests => 56;
 
 use lib 't';
 use strict;
@@ -22,6 +22,7 @@ use utf8;
 
 use Carp;
 use Support::TestSetup;
+use SL::Dev::ALL qw(:ALL);
 
 use_ok 'SL::BackgroundJob::CreatePeriodicInvoices';
 use_ok 'SL::DB::Chart';
@@ -34,15 +35,11 @@ use_ok 'SL::DB::TaxZone';
 
 Support::TestSetup::login();
 
-our ($ar_chart, $buchungsgruppe, $currency_id, $customer, $employee, $order, $part, $tax_zone, $unit, @invoices);
+our ($ar_chart, $customer, $order, $part, $unit, @invoices);
 
 sub init_common_state {
-  $ar_chart       = SL::DB::Manager::Chart->find_by(accno => '1400')                        || croak "No AR chart";
-  $buchungsgruppe = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%') || croak "No accounting group";
-  $currency_id    = SL::DB::Default->get->currency_id;
-  $employee       = SL::DB::Manager::Employee->current                                      || croak "No employee";
-  $tax_zone       = SL::DB::Manager::TaxZone->find_by( description => 'Inland')             || croak "No taxzone";
-  $unit           = SL::DB::Manager::Unit->find_by(name => 'psch')                          || croak "No unit";
+  $ar_chart = SL::DB::Manager::Chart->find_by(accno => '1400') || croak "No AR chart";
+  $unit     = SL::DB::Manager::Unit->find_by(name => 'psch')   || croak "No unit";
 }
 
 sub clear_up {
@@ -57,38 +54,31 @@ sub create_invoices {
   # Clean up: remove invoices, orders, parts and customers
   clear_up();
 
-  $customer     = SL::DB::Customer->new(
-    name        => 'Test Customer',
-    currency_id => $currency_id,
-    taxzone_id  => $tax_zone->id,
+  $customer = new_customer(
+    name => 'Test Customer',
     %{ $params{customer} }
   )->save;
 
-  $part = SL::DB::Part->new(
-    partnumber         => 'T4254',
-    description        => 'Fourty-two fifty-four',
-    lastcost           => 222.22,
-    sellprice          => 333.33,
-    buchungsgruppen_id => $buchungsgruppe->id,
-    unit               => $unit->name,
+  $part = new_part(
+    partnumber  => 'T4254',
+    description => 'Fourty-two fifty-four',
+    lastcost    => 222.22,
+    sellprice   => 333.33,
+    unit        => $unit->name,
     %{ $params{part} }
   )->save;
-  $part->load;
 
-  $order                     = SL::DB::Order->new(
-    customer_id              => $customer->id,
-    currency_id              => $currency_id,
-    taxzone_id               => $tax_zone->id,
+  $order = create_sales_order(
+    save                     => 1,
+    customer                 => $customer,
     transaction_description  => '<%period_start_date%>',
     orderitems               => [
-      { parts_id             => $part->id,
-        description          => $part->description,
-        lastcost             => $part->lastcost,
-        sellprice            => $part->sellprice,
-        qty                  => 1,
-        unit                 => $unit->name,
+      SL::Dev::Record::create_order_item(
+        part => $part,
+        qty  => 1,
+        unit => $unit->name,
         %{ $params{orderitem} },
-      },
+      ),
     ],
     periodic_invoices_config => {
       active                 => 1,
@@ -98,10 +88,6 @@ sub create_invoices {
     %{ $params{order} },
   );
 
-  $order->calculate_prices_and_taxes;
-
-  ok($order->save(cascade => 1));
-
   SL::BackgroundJob::CreatePeriodicInvoices->new->run(SL::DB::BackgroundJob->new);
 
   @invoices = @{ SL::DB::Manager::Invoice->get_all(sort_by => [ qw(id) ]) };
diff --git a/t/background_job/known_jobs.t b/t/background_job/known_jobs.t
deleted file mode 100644 (file)
index 77150ee..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-use Test::More tests => 4;
-
-use lib 't';
-
-use Support::TestSetup;
-
-use_ok 'SL::BackgroundJob::Base';
-
-my @expected_known_job_classes = qw(BackgroundJobCleanup CleanAuthSessions CleanBackgroundJobHistory CreatePeriodicInvoices CsvImport FailedBackgroundJobsReport
-                                      MassRecordCreationAndPrinting SelfTest Test);
-is_deeply [ SL::BackgroundJob::Base->get_known_job_classes ], \@expected_known_job_classes, 'get_known_job_classes called as class method';
-
-my $job = new_ok 'SL::BackgroundJob::Base';
-is_deeply [ $job->get_known_job_classes ], \@expected_known_job_classes, 'get_known_job_classes called as instance method';
diff --git a/t/bank/bank_transactions.t b/t/bank/bank_transactions.t
new file mode 100644 (file)
index 0000000..92f9362
--- /dev/null
@@ -0,0 +1,1274 @@
+use Test::More tests => 293;
+
+use strict;
+
+use lib 't';
+use utf8;
+
+use Carp;
+use Support::TestSetup;
+use Test::Exception;
+use List::Util qw(sum);
+
+use SL::DB::AccTransaction;
+use SL::DB::BankTransactionAccTrans;
+use SL::DB::Buchungsgruppe;
+use SL::DB::Currency;
+use SL::DB::Customer;
+use SL::DB::Default;
+use SL::DB::Vendor;
+use SL::DB::Invoice;
+use SL::DB::Unit;
+use SL::DB::Part;
+use SL::DB::TaxZone;
+use SL::DB::BankAccount;
+use SL::DB::PaymentTerm;
+use SL::DB::PurchaseInvoice;
+use SL::DB::BankTransaction;
+use SL::Controller::BankTransaction;
+use SL::Controller::Reconciliation;
+use SL::Dev::ALL qw(:ALL);
+use Data::Dumper;
+
+my ($customer, $vendor, $currency_id, $unit, $tax, $tax0, $tax7, $tax_9, $payment_terms, $bank_account);
+my ($currency);
+my ($ar_chart,$bank,$ar_amount_chart, $ap_chart, $ap_amount_chart);
+my ($ar_transaction, $ap_transaction);
+my ($dt, $dt_5, $dt_10, $year);
+
+sub clear_up {
+
+  SL::DB::Manager::BankTransactionAccTrans->delete_all(all => 1);
+  SL::DB::Manager::BankTransaction->delete_all(all => 1);
+  SL::DB::Manager::InvoiceItem->delete_all(all => 1);
+  SL::DB::Manager::InvoiceItem->delete_all(all => 1);
+  SL::DB::Manager::Invoice->delete_all(all => 1);
+  SL::DB::Manager::PurchaseInvoice->delete_all(all => 1);
+  SL::DB::Manager::Part->delete_all(all => 1);
+  SL::DB::Manager::Customer->delete_all(all => 1);
+  SL::DB::Manager::Vendor->delete_all(all => 1);
+  SL::DB::Manager::SepaExportItem->delete_all(all => 1);
+  SL::DB::Manager::SepaExport->delete_all(all => 1);
+  SL::DB::Manager::BankAccount->delete_all(all => 1);
+  SL::DB::Manager::PaymentTerm->delete_all(all => 1);
+  SL::DB::Manager::Currency->delete_all(where => [ name => 'CUR' ]);
+  # SL::DB::Manager::Default->delete_all(all => 1);
+};
+
+my $bt_controller;
+
+sub save_btcontroller_to_string {
+  my $output;
+  open(my $outputFH, '>', \$output) or die;
+  my $oldFH = select $outputFH;
+
+  $bt_controller = SL::Controller::BankTransaction->new;
+  $bt_controller->action_save_invoices;
+
+  select $oldFH;
+  close $outputFH;
+  return $output;
+}
+
+# starting test:
+Support::TestSetup::login();
+
+clear_up();
+reset_state(); # initialise customers/vendors/bank/currency/...
+
+test1();
+
+test_overpayment_with_partialpayment();
+test_overpayment();
+reset_state();
+test_skonto_exact();
+test_two_invoices();
+test_partial_payment();
+test_credit_note();
+test_ap_transaction();
+test_neg_ap_transaction(invoice => 0);
+test_neg_ap_transaction(invoice => 1);
+test_ap_payment_transaction();
+test_ap_payment_part_transaction();
+test_neg_sales_invoice();
+test_two_neg_ap_transaction();
+test_one_inv_and_two_invoices_with_skonto_exact();
+test_bt_error();
+test_full_workflow_ar_multiple_inv_skonto_reconciliate_and_undo();
+reset_state();
+test_sepa_export();
+
+reset_state();
+test_bt_rule1();
+reset_state();
+test_two_banktransactions();
+# remove all created data at end of test
+test_closedto();
+clear_up();
+
+done_testing();
+
+###### functions for setting up data
+
+sub reset_state {
+  my %params = @_;
+
+  $params{$_} ||= {} for qw(unit customer tax vendor);
+
+  clear_up();
+
+  $year  = DateTime->today_local->year;
+  $year  = 2019 if $year == 2020; # use year 2019 in 2020, because of tax rate change in Germany
+  $dt    = DateTime->new(year => $year, month => 1, day => 12);
+  $dt_5  = $dt->clone->add(days => 5);
+  $dt_10 = $dt->clone->add(days => 10);
+
+  $tax             = SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19, %{ $params{tax} }) || croak "No tax";
+  $tax7            = SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.07)                    || croak "No tax for 7\%";
+  $tax_9           = SL::DB::Manager::Tax->find_by(taxkey => 9, rate => 0.19, %{ $params{tax} }) || croak "No tax for 19\%";
+  $tax0            = SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.0)                     || croak "No tax for 0\%";
+
+  $currency_id     = $::instance_conf->get_currency_id;
+
+  $bank_account     =  SL::DB::BankAccount->new(
+    account_number  => '123',
+    bank_code       => '123',
+    iban            => '123',
+    bic             => '123',
+    bank            => '123',
+    chart_id        => SL::DB::Manager::Chart->find_by(description => 'Bank')->id,
+    name            => SL::DB::Manager::Chart->find_by(description => 'Bank')->description,
+  )->save;
+
+  $customer = new_customer(
+    name                      => 'Test Customer OLÉ S.L. Årdbärg AB',
+    iban                      => 'DE12500105170648489890',
+    bic                       => 'TESTBIC',
+    account_number            => '648489890',
+    mandate_date_of_signature => $dt,
+    mandator_id               => 'foobar',
+    bank                      => 'Geizkasse',
+    bank_code                 => 'G1235',
+    depositor                 => 'Test Customer',
+    customernumber            => 'CUST1704',
+  )->save;
+
+  $payment_terms = create_payment_terms();
+
+  $vendor = new_vendor(
+    name           => 'Test Vendor',
+    payment_id     => $payment_terms->id,
+    iban           => 'DE12500105170648489890',
+    bic            => 'TESTBIC',
+    account_number => '648489890',
+    bank           => 'Geizkasse',
+    bank_code      => 'G1235',
+    depositor      => 'Test Vendor',
+    vendornumber   => 'VEND1704',
+  )->save;
+
+  $ar_chart        = SL::DB::Manager::Chart->find_by( accno => '1400' ); # Forderungen
+  $ap_chart        = SL::DB::Manager::Chart->find_by( accno => '1600' ); # Verbindlichkeiten
+  $bank            = SL::DB::Manager::Chart->find_by( accno => '1200' ); # Bank
+  $ar_amount_chart = SL::DB::Manager::Chart->find_by( accno => '8400' ); # Erlöse
+  $ap_amount_chart = SL::DB::Manager::Chart->find_by( accno => '3400' ); # Wareneingang 19%
+
+}
+
+sub test_ar_transaction {
+  my (%params) = @_;
+  my $netamount = $::form->round_amount($params{amount}, 2) || 100;
+  my $amount    = $::form->round_amount($netamount * 1.19,2);
+  my $invoice   = SL::DB::Invoice->new(
+      invoice      => 0,
+      invnumber    => $params{invnumber} || undef, # let it use its own invnumber
+      amount       => $amount,
+      netamount    => $netamount,
+      transdate    => $dt,
+      taxincluded  => $params{taxincluded } || 0,
+      customer_id  => $customer->id,
+      taxzone_id   => $customer->taxzone_id,
+      currency_id  => $currency_id,
+      transactions => [],
+      payment_id   => $params{payment_id} || undef,
+      notes        => 'test_ar_transaction',
+  );
+  $invoice->add_ar_amount_row(
+    amount => $invoice->netamount,
+    chart  => $ar_amount_chart,
+    tax_id => $params{tax_id} || $tax->id,
+  );
+
+  $invoice->create_ar_row(chart => $ar_chart);
+  $invoice->save;
+
+  is($invoice->currency_id , $currency_id , 'currency_id has been saved');
+  is($invoice->netamount   , $netamount   , 'ar amount has been converted');
+  is($invoice->amount      , $amount      , 'ar amount has been converted');
+  is($invoice->taxincluded , 0            , 'ar transaction doesn\'t have taxincluded');
+
+  if ( $netamount == 100 ) {
+    is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ar_amount_chart->id , trans_id => $invoice->id)->amount , '100.00000'  , $ar_amount_chart->accno . ': has been converted for currency');
+    is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ar_chart->id        , trans_id => $invoice->id)->amount , '-119.00000' , $ar_chart->accno . ': has been converted for currency');
+  }
+  return $invoice;
+};
+
+sub test_ap_transaction {
+  my (%params) = @_;
+  my $testname = 'test_ap_transaction';
+
+  my $netamount = 100;
+  my $amount    = $::form->round_amount($netamount * 1.19,2);
+  my $invoice   = SL::DB::PurchaseInvoice->new(
+    invoice      => 0,
+    invnumber    => $params{invnumber} || $testname,
+    amount       => $amount,
+    netamount    => $netamount,
+    transdate    => $dt,
+    taxincluded  => 0,
+    vendor_id    => $vendor->id,
+    taxzone_id   => $vendor->taxzone_id,
+    currency_id  => $currency_id,
+    transactions => [],
+    notes        => 'test_ap_transaction',
+  );
+  $invoice->add_ap_amount_row(
+    amount     => $invoice->netamount,
+    chart      => $ap_amount_chart,
+    tax_id     => $params{tax_id} || $tax_9->id,
+  );
+
+  $invoice->create_ap_row(chart => $ap_chart);
+  $invoice->save;
+
+  is($invoice->currency_id , $currency_id , "$testname: currency_id has been saved");
+  is($invoice->netamount   , 100          , "$testname: ap amount has been converted");
+  is($invoice->amount      , 119          , "$testname: ap amount has been converted");
+  is($invoice->taxincluded , 0            , "$testname: ap transaction doesn\'t have taxincluded");
+
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ap_amount_chart->id , trans_id => $invoice->id)->amount , '-100.00000' , $ap_amount_chart->accno . ': has been converted for currency');
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ap_chart->id        , trans_id => $invoice->id)->amount , '119.00000'  , $ap_chart->accno . ': has been converted for currency');
+
+  return $invoice;
+};
+
+###### test cases
+
+sub test1 {
+
+  my $testname = 'test1';
+
+  $ar_transaction = test_ar_transaction(invnumber => 'salesinv1');
+
+  my $bt = create_bank_transaction(record      => $ar_transaction,
+                                   transdate   => $dt,
+                                   valutadate  => $dt) or die "Couldn't create bank_transaction";
+
+  $::form->{invoice_ids} = {
+    $bt->id => [ $ar_transaction->id ]
+  };
+
+  save_btcontroller_to_string();
+
+  $ar_transaction->load;
+  $bt->load;
+  is($ar_transaction->paid   , '119.00000' , "$testname: salesinv1 was paid");
+  is($ar_transaction->closed , 1           , "$testname: salesinv1 is closed");
+  is($bt->invoice_amount     , '119.00000' , "$testname: bt invoice amount was assigned");
+
+};
+
+sub test_skonto_exact {
+
+  my $testname = 'test_skonto_exact';
+
+  $ar_transaction = test_ar_transaction(invnumber => 'salesinv skonto',
+                                        payment_id => $payment_terms->id,
+                                       );
+
+  my $bt = create_bank_transaction(record        => $ar_transaction,
+                                   bank_chart_id => $bank->id,
+                                   transdate     => $dt,
+                                   valutadate    => $dt,
+                                   amount        => $ar_transaction->amount_less_skonto
+                                  ) or die "Couldn't create bank_transaction";
+
+  $::form->{invoice_ids} = {
+    $bt->id => [ $ar_transaction->id ]
+  };
+  $::form->{invoice_skontos} = {
+    $bt->id => [ 'with_skonto_pt' ]
+  };
+
+  save_btcontroller_to_string();
+
+  $ar_transaction->load;
+  $bt->load;
+  is($ar_transaction->paid   , '119.00000' , "$testname: salesinv skonto was paid");
+  is($ar_transaction->closed , 1           , "$testname: salesinv skonto is closed");
+  is($bt->invoice_amount     , '113.05000' , "$testname: bt invoice amount was assigned");
+
+};
+
+sub test_bt_error {
+
+  my $testname = 'test_rollback_error';
+  # without type with_free_skonto the helper function (Payment.pm) looks ugly but not
+  # breakable
+
+  $ar_transaction = test_ar_transaction(invnumber   => 'salesinv skonto',
+                                        payment_id  => $payment_terms->id,
+                                        taxincluded => 0,
+                                        amount      => 168.58 / 1.19,
+                                       );
+
+  my $bt = create_bank_transaction(record        => $ar_transaction,
+                                   bank_chart_id => $bank->id,
+                                   transdate   => $dt,
+                                   valutadate  => $dt,
+                                   amount        => 160.15,
+                                  ) or die "Couldn't create bank_transaction";
+  $::form->{invoice_ids} = {
+    $bt->id => [ $ar_transaction->id ]
+  };
+  $::form->{invoice_skontos} = {
+    $bt->id => [ 'with_skonto_pt' ]
+  };
+  is($ar_transaction->netamount, $::form->round_amount(168.58/1.19, 2), "$testname: Net Amount assigned");
+  is($ar_transaction->amount, 168.58, "$testname: Amount assigned");
+  is($ar_transaction->paid   , '0' , "$testname: salesinv is not paid");
+
+  # generate an error for testing rollback mechanism
+  my $saved_skonto_sales_chart_id = $tax->skonto_sales_chart_id;
+  $tax->skonto_sales_chart_id(undef);
+  $tax->save;
+
+  save_btcontroller_to_string();
+  my @bt_errors = @{ $bt_controller->problems };
+  is(substr($bt_errors[0]->{message},0,38), 'Kein Skontokonto für Steuerschlüssel 3', "$testname: Fehlermeldung ok");
+  # set original value
+  $tax->skonto_sales_chart_id($saved_skonto_sales_chart_id);
+  $tax->save;
+
+  $ar_transaction->load;
+  $bt->load;
+  is($ar_transaction->paid   , '0.00000' , "$testname: salesinv was not paid");
+  is($bt->invoice_amount     , '0.00000' , "$testname: bt invoice amount was not assigned");
+
+};
+
+sub test_two_invoices {
+
+  my $testname = 'test_two_invoices';
+
+  my $ar_transaction_1 = test_ar_transaction(invnumber => 'salesinv_1');
+  my $ar_transaction_2 = test_ar_transaction(invnumber => 'salesinv_2');
+
+  my $bt = create_bank_transaction(record        => $ar_transaction_1,
+                                   amount        => ($ar_transaction_1->amount + $ar_transaction_2->amount),
+                                   purpose       => "Rechnungen " . $ar_transaction_1->invnumber . " und " . $ar_transaction_2->invnumber,
+                                   transdate     => $dt,
+                                   valutadate    => $dt,
+                                   bank_chart_id => $bank->id,
+                                  ) or die "Couldn't create bank_transaction";
+
+  my ($agreement, $rule_matches) = $bt->get_agreement_with_invoice($ar_transaction_1);
+  is($agreement, 16, "points for ar_transaction_1 in test_two_invoices ok");
+
+  $::form->{invoice_ids} = {
+    $bt->id => [ $ar_transaction_1->id, $ar_transaction_2->id ]
+  };
+
+  save_btcontroller_to_string();
+
+  $ar_transaction_1->load;
+  $ar_transaction_2->load;
+  $bt->load;
+
+  is($ar_transaction_1->paid   , '119.00000' , "$testname: salesinv_1 wcsv_import_reportsas paid");
+  is($ar_transaction_1->closed , 1           , "$testname: salesinv_1 is closed");
+  is($ar_transaction_2->paid   , '119.00000' , "$testname: salesinv_2 was paid");
+  is($ar_transaction_2->closed , 1           , "$testname: salesinv_2 is closed");
+  is($bt->invoice_amount       , '238.00000' , "$testname: bt invoice amount was assigned");
+
+}
+
+sub test_one_inv_and_two_invoices_with_skonto_exact {
+
+  my $testname = 'test_two_invoices_with_skonto_exact';
+
+  my $ar_transaction_1 = test_ar_transaction(invnumber => 'salesinv 1 skonto',
+                                             payment_id => $payment_terms->id,
+                                            );
+  my $ar_transaction_2 = test_ar_transaction(invnumber => 'salesinv 2 skonto',
+                                             payment_id => $payment_terms->id,
+                                            );
+  my $ar_transaction_3 = test_ar_transaction(invnumber => 'salesinv 3 no skonto');
+
+
+
+  my $bt = create_bank_transaction(record        => $ar_transaction_1,
+                                   bank_chart_id => $bank->id,
+                                   transdate     => $dt,
+                                   valutadate    => $dt,
+                                   amount        => $ar_transaction_1->amount_less_skonto * 2 + $ar_transaction_3->amount
+                                  ) or die "Couldn't create bank_transaction";
+
+  $::form->{invoice_ids} = {
+    $bt->id => [ $ar_transaction_1->id, $ar_transaction_3->id, $ar_transaction_2->id]
+  };
+  $::form->{invoice_skontos} = {
+    $bt->id => [ 'with_skonto_pt', 'without_skonto', 'with_skonto_pt' ]
+  };
+
+  save_btcontroller_to_string();
+
+  $ar_transaction_1->load;
+  $ar_transaction_2->load;
+  $ar_transaction_3->load;
+  my $skonto_1 = SL::DB::Manager::AccTransaction->find_by(trans_id => $ar_transaction_1->id, chart_id => 162);
+  my $skonto_2 = SL::DB::Manager::AccTransaction->find_by(trans_id => $ar_transaction_2->id, chart_id => 162);
+  $bt->load;
+  is($skonto_1->amount   , '-5.95000' , "$testname: salesinv 1 skonto was booked");
+  is($skonto_2->amount   , '-5.95000' , "$testname: salesinv 2 skonto was booked");
+  is($ar_transaction_1->paid   , '119.00000' , "$testname: salesinv 1 was paid");
+  is($ar_transaction_2->paid   , '119.00000' , "$testname: salesinv 2 was paid");
+  is($ar_transaction_3->paid   , '119.00000' , "$testname: salesinv 3 was paid");
+  is($ar_transaction_1->closed , 1           , "$testname: salesinv 1 skonto is closed");
+  is($ar_transaction_2->closed , 1           , "$testname: salesinv 2 skonto is closed");
+  is($ar_transaction_3->closed , 1           , "$testname: salesinv 2 skonto is closed");
+  is($bt->invoice_amount     , '345.10000' , "$testname: bt invoice amount was assigned");
+
+}
+
+sub test_overpayment {
+
+  my $testname = 'test_overpayment';
+
+  $ar_transaction = test_ar_transaction(invnumber => 'salesinv overpaid');
+
+  # amount 135 > 119
+  my $bt = create_bank_transaction(record        => $ar_transaction,
+                                   bank_chart_id => $bank->id,
+                                   transdate   => $dt,
+                                   valutadate  => $dt,
+                                   amount        => 135
+                                  ) or die "Couldn't create bank_transaction";
+
+  $::form->{invoice_ids} = {
+    $bt->id => [ $ar_transaction->id ]
+  };
+
+  save_btcontroller_to_string();
+
+  $ar_transaction->load;
+  $bt->load;
+
+  is($ar_transaction->paid                     , '119.00000' , "$testname: 'salesinv overpaid' was not overpaid");
+  is($bt->invoice_amount                       , '119.00000' , "$testname: bt invoice amount was not fully assigned with the overpaid amount");
+{ local $TODO = 'this currently fails because closed ignores over-payments, see commit d90966c7';
+  is($ar_transaction->closed                   , 0           , "$testname: 'salesinv overpaid' is open (via 'closed' method')");
+}
+  is($ar_transaction->open_amount == 0 ? 1 : 0 , 1           , "$testname: 'salesinv overpaid is closed (via amount-paid)");
+
+};
+
+sub test_overpayment_with_partialpayment {
+
+  # two payments on different days, 10 and 119. If there is only one invoice we
+  # don't want it to be overpaid.
+  my $testname = 'test_overpayment_with_partialpayment';
+
+  $ar_transaction = test_ar_transaction(invnumber => 'salesinv overpaid partial');
+
+  my $bt_1 = create_bank_transaction(record        => $ar_transaction,
+                                     bank_chart_id => $bank->id,
+                                     transdate   => $dt,
+                                     valutadate  => $dt,
+                                     amount        =>  10
+                                    ) or die "Couldn't create bank_transaction";
+  my $bt_2 = create_bank_transaction(record        => $ar_transaction,
+                                     amount        => 119,
+                                     transdate     => $dt_5,
+                                     valutadate    => $dt_5,
+                                     bank_chart_id => $bank->id,
+                                    ) or die "Couldn't create bank_transaction";
+
+  $::form->{invoice_ids} = {
+    $bt_1->id => [ $ar_transaction->id ]
+  };
+  save_btcontroller_to_string();
+
+  $bt_1->load;
+  is($bt_1->invoice_amount ,  '10.00000' , "$testname: bt_1 invoice amount was fully assigned");
+  $::form->{invoice_ids} = {
+    $bt_2->id => [ $ar_transaction->id ]
+  };
+  save_btcontroller_to_string();
+
+  $ar_transaction->load;
+  $bt_2->load;
+
+  is($bt_1->invoice_amount ,  '10.00000' , "$testname: bt_1 invoice amount was fully assigned");
+  is($ar_transaction->paid , '119.00000' , "$testname: 'salesinv overpaid partial' was not overpaid");
+  is($bt_2->invoice_amount , '109.00000' , "$testname: bt_2 invoice amount was partly assigned");
+
+};
+
+sub test_partial_payment {
+
+  my $testname = 'test_partial_payment';
+
+  $ar_transaction = test_ar_transaction(invnumber => 'salesinv partial payment');
+
+  # amount 100 < 119
+  my $bt = create_bank_transaction(record        => $ar_transaction,
+                                   bank_chart_id => $bank->id,
+                                   transdate     => $dt,
+                                   valutadate    => $dt,
+                                   amount        => 100
+                                  ) or die "Couldn't create bank_transaction";
+
+  $::form->{invoice_ids} = {
+    $bt->id => [ $ar_transaction->id ]
+  };
+
+  save_btcontroller_to_string();
+
+  $ar_transaction->load;
+  $bt->load;
+
+  is($ar_transaction->paid , '100.00000' , "$testname: 'salesinv partial payment' was partially paid");
+  is($bt->invoice_amount   , '100.00000' , "$testname: bt invoice amount was assigned partially paid amount");
+
+  # addon test partial payment full point match
+  my $bt2 = create_bank_transaction(record        => $ar_transaction,
+                                    bank_chart_id => $bank->id,
+                                    transdate     => $dt,
+                                    valutadate    => $dt,
+                                    amount        => 19
+                                   ) or die "Couldn't create bank_transaction";
+
+  my ($agreement, $rule_matches) = $bt2->get_agreement_with_invoice($ar_transaction);
+  is($agreement, 15, "points for exact partial payment ok");
+  is($rule_matches, 'remote_account_number(3) exact_open_amount(4) depositor_matches(2) remote_name(2) payment_within_30_days(1) datebonus0(3) ', "rules_matches for exact partial payment ok");
+};
+
+sub test_full_workflow_ar_multiple_inv_skonto_reconciliate_and_undo {
+
+  my $testname = 'test_partial_payment';
+
+  $ar_transaction = test_ar_transaction(invnumber => 'salesinv partial payment two');
+  my $ar_transaction_2 = test_ar_transaction(invnumber => 'salesinv 2 22d2', amount => 22);
+
+  # amount 299.29 > 119
+  my $bt = create_bank_transaction(record        => $ar_transaction,
+                                   bank_chart_id => $bank->id,
+                                   amount        => 299.29
+                                  ) or die "Couldn't create bank_transaction";
+
+  $::form->{invoice_ids} = {
+    $bt->id => [ $ar_transaction->id ]
+  };
+
+  save_btcontroller_to_string();
+
+  $ar_transaction->load;
+  $bt->load;
+
+  is($ar_transaction->paid , '119.00000' , "$testname: 'salesinv partial payment' was fully paid");
+  is($bt->invoice_amount   , '119.00000' , "$testname: bt invoice amount was assigned partially paid amount");
+  is($bt->amount           , '299.29000' , "$testname: bt amount is stil there");
+  # next invoice, same bank transaction
+  $::form->{invoice_ids} = {
+    $bt->id => [ $ar_transaction_2->id ]
+  };
+
+  save_btcontroller_to_string();
+
+  $ar_transaction_2->load;
+  $bt->load;
+  is($ar_transaction_2->paid , '26.18000' , "$testname: 'salesinv partial payment' was fully paid");
+  is($bt->invoice_amount   , '145.18000' , "$testname: bt invoice amount was assigned partially paid amount");
+  is($bt->amount           , '299.29000' , "$testname: bt amount is stil there");
+  is(scalar @{ SL::DB::Manager::BankTransactionAccTrans->get_all(where => [bank_transaction_id => $bt->id ] )}, 4, "$testname 4 acc_trans entries created");
+
+  #  now check all 4 entries done so far and save paid acc_trans_ids for later use with reconcile
+  foreach my $acc_trans_id_entry (@{ SL::DB::Manager::BankTransactionAccTrans->get_all(where => [bank_transaction_id => $bt->id ] )}) {
+    isnt($acc_trans_id_entry->ar_id, undef, "$testname: bt linked with acc_trans and trans_id set");
+    my $rl = SL::DB::Manager::RecordLink->get_all(where => [ from_id => $bt->id, from_table => 'bank_transactions', to_id => $acc_trans_id_entry->ar_id ]);
+    is (ref $rl->[0], 'SL::DB::RecordLink', "$testname record link created");
+    my $acc_trans = SL::DB::Manager::AccTransaction->get_all(where => [acc_trans_id => $acc_trans_id_entry->acc_trans_id]);
+    foreach my $entry (@{ $acc_trans }) {
+      like(abs($entry->amount), qr/(119|26.18)/, "$testname: abs amount correct");
+      like($entry->chart_link, qr/(paid|AR)/, "$testname chart_link correct");
+      push @{ $::form->{bb_ids} }, $entry->acc_trans_id if $entry->chart_link =~ m/paid/;
+    }
+  }
+  # great we need one last booking to clear the whole bank transaction - we include skonto
+  my $ar_transaction_skonto = test_ar_transaction(invnumber  => 'salesinv skonto last case',
+                                                  payment_id => $payment_terms->id,
+                                                  amount     => 136.32,
+                                                 );
+
+  $::form->{invoice_ids} = {
+    $bt->id => [ $ar_transaction_skonto->id ]
+  };
+  $::form->{invoice_skontos} = {
+    $bt->id => [ 'with_skonto_pt' ]
+  };
+
+  save_btcontroller_to_string();
+
+  $ar_transaction_skonto->load;
+  $bt->load;
+  is($ar_transaction_skonto->paid , '162.22000' , "$testname: 'salesinv skonto fully paid");
+  is($bt->invoice_amount   , '299.29000' , "$testname: bt invoice amount was assigned partially paid amount");
+  is($bt->amount           , '299.29000' , "$testname: bt amount is stil there");
+  is(scalar @{ SL::DB::Manager::BankTransactionAccTrans->get_all(where => [bank_transaction_id => $bt->id ] )},
+       9, "$testname 9 acc_trans entries created");
+
+  # same loop as above, but only for the 3rd ar_id
+  foreach my $acc_trans_id_entry (@{ SL::DB::Manager::BankTransactionAccTrans->get_all(where => [ar_id => $ar_transaction_skonto->id ] )}) {
+    isnt($acc_trans_id_entry->ar_id, '', "$testname: bt linked with acc_trans and trans_id set");
+    my $rl = SL::DB::Manager::RecordLink->get_all(where => [ from_id => $bt->id, from_table => 'bank_transactions', to_id => $acc_trans_id_entry->ar_id ]);
+    is (ref $rl->[0], 'SL::DB::RecordLink', "$testname record link created");
+    my $acc_trans = SL::DB::Manager::AccTransaction->get_all(where => [acc_trans_id => $acc_trans_id_entry->acc_trans_id]);
+    foreach my $entry (@{ $acc_trans }) {
+      like($entry->chart_link, qr/(paid|AR)/, "$testname chart_link correct");
+      is ($entry->amount, '162.22000', "$testname full amont") if $entry->chart_link eq 'AR'; # full amount
+      like(abs($entry->amount), qr/(154.11|8.11)/, "$testname: abs amount correct") if $entry->chart_link =~ m/paid/;
+      push @{ $::form->{bb_ids} }, $entry->acc_trans_id if ($entry->chart_link =~ m/paid/ && $entry->amount == -154.11);
+    }
+  }
+  # done, now reconciliate all bookings
+  $::form->{bt_ids} = [ $bt->id ];
+  my $rec_controller = SL::Controller::Reconciliation->new;
+  my @errors = $rec_controller->_get_elements_and_validate;
+
+  is (scalar @errors, 0, "$testname unsuccesfull reconciliation with error: " . Dumper(@errors));
+  $rec_controller->_reconcile;
+  $bt->load;
+
+  # and check the cleared state of bt and the acc_transactions
+  is($bt->cleared, '1' , "$testname: bt cleared");
+  foreach (@{ $::form->{bb_ids} }) {
+    my $acc_trans = SL::DB::Manager::AccTransaction->find_by(acc_trans_id => $_);
+    is($acc_trans->cleared, '1' , "$testname: acc_trans entry cleared");
+  }
+  # now, this was a really bad idea and in general a major mistake. better undo and redo the whole bank transactions
+
+  $::form->{ids} = [ $bt->id ];
+  $bt_controller = SL::Controller::BankTransaction->new;
+  $bt_controller->action_unlink_bank_transaction('testcase' => 1);
+
+  $bt->load;
+
+  # and check the cleared state of bt and the acc_transactions
+  is($bt->cleared, '0' , "$testname: bt undo cleared");
+  is($bt->invoice_amount, '0.00000' , "$testname: bt undo invoice amount");
+  foreach (@{ $::form->{bb_ids} }) {
+    my $acc_trans = SL::DB::Manager::AccTransaction->find_by(acc_trans_id => $_);
+    is($acc_trans, undef , "$testname: cleared acc_trans entry completely removed");
+  }
+  # this was for data integrity for reconcile, now all the other options
+  is(scalar @{ SL::DB::Manager::BankTransactionAccTrans->get_all(where => [bank_transaction_id => $bt->id ] )},
+       0, "$testname 7 acc_trans entries deleted");
+  my $rl = SL::DB::Manager::RecordLink->get_all(where => [ from_id => $bt->id, from_table => 'bank_transactions' ]);
+  is (ref $rl->[0], '', "$testname record link removed");
+  # double safety and check ar.paid
+  # load all three invoices and check for paid-link via acc_trans and paid in general
+
+  $ar_transaction->load;
+  $ar_transaction_2->load;
+  $ar_transaction_skonto->load;
+
+  is(scalar @{ SL::DB::Manager::AccTransaction->get_all(
+     where => [ trans_id => $ar_transaction->id, chart_link => { like => '%paid%' } ])},
+     0, "$testname no more paid entries in acc_trans for ar_transaction");
+  is(scalar @{ SL::DB::Manager::AccTransaction->get_all(
+     where => [ trans_id => $ar_transaction_2->id, chart_link => { like => '%paid%' } ])},
+     0, "$testname no more paid entries in acc_trans for ar_transaction_2");
+  is(scalar @{ SL::DB::Manager::AccTransaction->get_all(
+     where => [ trans_id => $ar_transaction_skonto->id, chart_link => { like => '%paid%' } ])},
+     0, "$testname no more paid entries in acc_trans for ar_transaction_skonto");
+
+  is($ar_transaction->paid , '0.00000' , "$testname: 'salesinv fully unpaid");
+  is($ar_transaction_2->paid , '0.00000' , "$testname: 'salesinv 2 fully unpaid");
+  is($ar_transaction_skonto->paid , '0.00000' , "$testname: 'salesinv skonto fully unpaid");
+
+  # whew. w(h)a(n)t a whole lotta test
+}
+
+
+sub test_credit_note {
+
+  my $testname = 'test_credit_note';
+
+  my $part1 = new_part(   partnumber => 'T4254')->save;
+  my $part2 = new_service(partnumber => 'Serv1')->save;
+  my $credit_note = create_credit_note(
+    invnumber    => 'cn 1',
+    customer     => $customer,
+    transdate    => $dt,
+    taxincluded  => 0,
+    invoiceitems => [ create_invoice_item(part => $part1, qty =>  3, sellprice => 70),
+                      create_invoice_item(part => $part2, qty => 10, sellprice => 50),
+                    ]
+  );
+  my $bt            = create_bank_transaction(record        => $credit_note,
+                                                                amount        => $credit_note->amount,
+                                                                bank_chart_id => $bank->id,
+                                                                transdate     => $dt_10,
+                                                               );
+  my ($agreement, $rule_matches) = $bt->get_agreement_with_invoice($credit_note);
+  is($agreement, 14, "points for credit note ok");
+  is($rule_matches, 'remote_account_number(3) exact_amount(4) depositor_matches(2) remote_name(2) payment_within_30_days(1) datebonus14(2) ', "rules_matches for credit note ok");
+
+  $::form->{invoice_ids} = {
+    $bt->id => [ $credit_note->id ]
+  };
+
+  save_btcontroller_to_string();
+
+  $credit_note->load;
+  $bt->load;
+  is($credit_note->amount   , '-844.90000', "$testname: amount ok");
+  is($credit_note->netamount, '-710.00000', "$testname: netamount ok");
+  is($credit_note->paid     , '-844.90000', "$testname: paid ok");
+}
+
+sub test_neg_ap_transaction {
+  my (%params) = @_;
+  my $testname = 'test_neg_ap_transaction' . $params{invoice} ? ' invoice booking' : ' credit booking';
+  my $netamount = -20;
+  my $amount    = $::form->round_amount($netamount * 1.19,2);
+  my $invoice   = SL::DB::PurchaseInvoice->new(
+    invoice      => $params{invoice} // 0,
+    invnumber    => $params{invnumber} || 'test_neg_ap_transaction',
+    amount       => $amount,
+    netamount    => $netamount,
+    transdate    => $dt,
+    taxincluded  => 0,
+    vendor_id    => $vendor->id,
+    taxzone_id   => $vendor->taxzone_id,
+    currency_id  => $currency_id,
+    transactions => [],
+    notes        => 'test_neg_ap_transaction',
+  );
+  $invoice->add_ap_amount_row(
+    amount     => $invoice->netamount,
+    chart      => $ap_amount_chart,
+    tax_id     => $tax_9->id,
+  );
+
+  $invoice->create_ap_row(chart => $ap_chart);
+  $invoice->save;
+
+  is($invoice->netamount, -20  , "$testname: netamount ok");
+  is($invoice->amount   , -23.8, "$testname: amount ok");
+
+  my $bt            = create_bank_transaction(record        => $invoice,
+                                              amount        => $invoice->amount,
+                                              bank_chart_id => $bank->id,
+                                              transdate     => $dt_10,
+                                                               );
+
+  my ($agreement, $rule_matches) = $bt->get_agreement_with_invoice($invoice);
+  is($agreement, 16, "points for negative ap transaction ok");
+
+  $::form->{invoice_ids} = {
+    $bt->id => [ $invoice->id ]
+  };
+
+  save_btcontroller_to_string();
+
+  $invoice->load;
+  $bt->load;
+
+  is($invoice->amount   , '-23.80000', "$testname: amount ok");
+  is($invoice->netamount, '-20.00000', "$testname: netamount ok");
+  is($invoice->paid     , '-23.80000', "$testname: paid ok");
+  is($bt->invoice_amount, '23.80000', "$testname: bt invoice amount for ap was assigned");
+  is($bt->amount,         '23.80000', "$testname: bt  amount for ap was assigned");
+
+  return $invoice;
+};
+sub test_two_neg_ap_transaction {
+  my $testname='test_two_neg_ap_transaction';
+  my $netamount = -20;
+  my $amount    = $::form->round_amount($netamount * 1.19,2);
+  my $invoice   = SL::DB::PurchaseInvoice->new(
+    invoice      =>  0,
+    invnumber    => 'test_neg_ap_transaction',
+    amount       => $amount,
+    netamount    => $netamount,
+    transdate    => $dt,
+    taxincluded  => 0,
+    vendor_id    => $vendor->id,
+    taxzone_id   => $vendor->taxzone_id,
+    currency_id  => $currency_id,
+    transactions => [],
+    notes        => 'test_neg_ap_transaction',
+  );
+  $invoice->add_ap_amount_row(
+    amount     => $invoice->netamount,
+    chart      => $ap_amount_chart,
+    tax_id     => $tax_9->id,
+  );
+
+  $invoice->create_ap_row(chart => $ap_chart);
+  $invoice->save;
+
+  is($invoice->netamount, -20  , "$testname: netamount ok");
+  is($invoice->amount   , -23.8, "$testname: amount ok");
+
+  my $netamount_two = -1.14;
+  my $amount_two    = $::form->round_amount($netamount_two * 1.19,2);
+  my $invoice_two   = SL::DB::PurchaseInvoice->new(
+    invoice      => 0,
+    invnumber    => 'test_neg_ap_transaction_two',
+    amount       => $amount_two,
+    netamount    => $netamount_two,
+    transdate    => $dt,
+    taxincluded  => 0,
+    vendor_id    => $vendor->id,
+    taxzone_id   => $vendor->taxzone_id,
+    currency_id  => $currency_id,
+    transactions => [],
+    notes        => 'test_neg_ap_transaction_two',
+  );
+  $invoice_two->add_ap_amount_row(
+    amount     => $invoice_two->netamount,
+    chart      => $ap_amount_chart,
+    tax_id     => $tax_9->id,
+  );
+
+  $invoice_two->create_ap_row(chart => $ap_chart);
+  $invoice_two->save;
+
+  is($invoice_two->netamount, -1.14  , "$testname: netamount ok");
+  is($invoice_two->amount   , -1.36, "$testname: amount ok");
+
+
+  my $bt            = create_bank_transaction(record        => $invoice_two,
+                                              amount        => $invoice_two->amount + $invoice->amount,
+                                              bank_chart_id => $bank->id,
+                                              transdate     => $dt_10,
+                                                               );
+  # my ($agreement, $rule_matches) = $bt->get_agreement_with_invoice($invoice_two);
+  # is($agreement, 15, "points for negative ap transaction ok");
+
+  $::form->{invoice_ids} = {
+    $bt->id => [ $invoice->id, $invoice_two->id ]
+  };
+
+  save_btcontroller_to_string();
+
+  $invoice->load;
+  $invoice_two->load;
+  $bt->load;
+
+  is($invoice->amount   , '-23.80000', "$testname: first inv amount ok");
+  is($invoice->netamount, '-20.00000', "$testname: first inv netamount ok");
+  is($invoice->paid     , '-23.80000', "$testname: first inv paid ok");
+  is($invoice_two->amount   , '-1.36000', "$testname: second inv amount ok");
+  is($invoice_two->netamount, '-1.14000', "$testname: second inv netamount ok");
+  is($invoice_two->paid     , '-1.36000', "$testname: second inv paid ok");
+  is($bt->invoice_amount, '25.16000', "$testname: bt invoice amount for both invoices were assigned");
+
+
+  return ($invoice, $invoice_two);
+};
+
+sub test_ap_payment_transaction {
+  my (%params) = @_;
+  my $testname = 'test_ap_payment_transaction';
+  my $netamount = 115;
+  my $amount    = $::form->round_amount($netamount * 1.19,2);
+  my $invoice   = SL::DB::PurchaseInvoice->new(
+    invoice      => 0,
+    invnumber    => $params{invnumber} || $testname,
+    amount       => $amount,
+    netamount    => $netamount,
+    transdate    => $dt,
+    taxincluded  => 0,
+    vendor_id    => $vendor->id,
+    taxzone_id   => $vendor->taxzone_id,
+    currency_id  => $currency_id,
+    transactions => [],
+    notes        => $testname,
+  );
+  $invoice->add_ap_amount_row(
+    amount     => $invoice->netamount,
+    chart      => $ap_amount_chart,
+    tax_id     => $tax_9->id,
+  );
+
+  $invoice->create_ap_row(chart => $ap_chart);
+  $invoice->save;
+
+  is($invoice->netamount, 115  , "$testname: netamount ok");
+  is($invoice->amount   , 136.85, "$testname: amount ok");
+
+  my $bt            = create_bank_transaction(record        => $invoice,
+                                              amount        => $invoice->amount,
+                                              bank_chart_id => $bank->id,
+                                              transdate     => $dt_10,
+                                             );
+  $::form->{invoice_ids} = {
+    $bt->id => [ $invoice->id ]
+  };
+
+  save_btcontroller_to_string();
+
+  $invoice->load;
+  $bt->load;
+
+  is($invoice->amount   , '136.85000', "$testname: amount ok");
+  is($invoice->netamount, '115.00000', "$testname: netamount ok");
+  is($bt->amount,         '-136.85000', "$testname: bt amount ok");
+  is($invoice->paid     , '136.85000', "$testname: paid ok");
+  is($bt->invoice_amount, '-136.85000', "$testname: bt invoice amount for ap was assigned");
+
+  return $invoice;
+};
+
+sub test_ap_payment_part_transaction {
+  my (%params) = @_;
+  my $testname = 'test_ap_payment_p_transaction';
+  my $netamount = 115;
+  my $amount    = $::form->round_amount($netamount * 1.19,2);
+  my $invoice   = SL::DB::PurchaseInvoice->new(
+    invoice      => 0,
+    invnumber    => $params{invnumber} || $testname,
+    amount       => $amount,
+    netamount    => $netamount,
+    transdate    => $dt,
+    taxincluded  => 0,
+    vendor_id    => $vendor->id,
+    taxzone_id   => $vendor->taxzone_id,
+    currency_id  => $currency_id,
+    transactions => [],
+    notes        => $testname,
+  );
+  $invoice->add_ap_amount_row(
+    amount     => $invoice->netamount,
+    chart      => $ap_amount_chart,
+    tax_id     => $tax_9->id,
+  );
+
+  $invoice->create_ap_row(chart => $ap_chart);
+  $invoice->save;
+
+  is($invoice->netamount, 115  , "$testname: netamount ok");
+  is($invoice->amount   , 136.85, "$testname: amount ok");
+
+  my $bt            = create_bank_transaction(record        => $invoice,
+                                              amount        => $invoice->amount-100,
+                                              bank_chart_id => $bank->id,
+                                              transdate     => $dt_10,
+                                             );
+  $::form->{invoice_ids} = {
+    $bt->id => [ $invoice->id ]
+  };
+
+  save_btcontroller_to_string();
+
+  $invoice->load;
+  $bt->load;
+
+  is($invoice->amount   , '136.85000', "$testname: amount ok");
+  is($invoice->netamount, '115.00000', "$testname: netamount ok");
+  is($bt->amount,         '-36.85000', "$testname: bt amount ok");
+  is($invoice->paid     ,  '36.85000', "$testname: paid ok");
+  is($bt->invoice_amount, '-36.85000', "$testname: bt invoice amount for ap was assigned");
+
+  my $bt2           = create_bank_transaction(record        => $invoice,
+                                              amount        => 100,
+                                              bank_chart_id => $bank->id,
+                                              transdate     => $dt_10,
+                                             );
+  $::form->{invoice_ids} = {
+    $bt2->id => [ $invoice->id ]
+  };
+
+  save_btcontroller_to_string();
+  $invoice->load;
+  $bt2->load;
+
+  is($invoice->amount   , '136.85000', "$testname: amount ok");
+  is($invoice->netamount, '115.00000', "$testname: netamount ok");
+  is($bt2->amount,        '-100.00000',"$testname: bt amount ok");
+  is($invoice->paid     , '136.85000', "$testname: paid ok");
+  is($bt2->invoice_amount,'-100.00000', "$testname: bt invoice amount for ap was assigned");
+
+  return $invoice;
+};
+
+sub test_neg_sales_invoice {
+
+  my $testname = 'test_neg_sales_invoice';
+
+  my $part1 = new_part(   partnumber => 'Funkenhaube öhm')->save;
+  my $part2 = new_service(partnumber => 'Service-Pauschale Pasch!')->save;
+
+  my $neg_sales_inv = create_sales_invoice(
+    invnumber    => '20172201',
+    customer     => $customer,
+    taxincluded  => 0,
+    transdate     => $dt,
+    invoiceitems => [ create_invoice_item(part => $part1, qty =>  3, sellprice => 70),
+                      create_invoice_item(part => $part2, qty => 10, sellprice => -50),
+                    ]
+  );
+  my $bt            = create_bank_transaction(record        => $neg_sales_inv,
+                                                                amount        => $neg_sales_inv->amount,
+                                                                bank_chart_id => $bank->id,
+                                                                transdate     => $dt,
+                                                                valutadate    => $dt,
+                                                               );
+  $::form->{invoice_ids} = {
+    $bt->id => [ $neg_sales_inv->id ]
+  };
+
+  save_btcontroller_to_string();
+
+  $neg_sales_inv->load;
+  $bt->load;
+  is($neg_sales_inv->amount   , '-345.10000', "$testname: amount ok");
+  is($neg_sales_inv->netamount, '-290.00000', "$testname: netamount ok");
+  is($neg_sales_inv->paid     , '-345.10000', "$testname: paid ok");
+  is($bt->amount              , '-345.10000', "$testname: bt amount ok");
+  is($bt->invoice_amount      , '-345.10000', "$testname: bt invoice_amount ok");
+}
+
+sub test_bt_rule1 {
+
+  my $testname = 'test_bt_rule1';
+
+  $ar_transaction = test_ar_transaction(invnumber => 'bt_rule1', transdate => $dt);
+
+  my $bt = create_bank_transaction(record => $ar_transaction, transdate => $dt) or die "Couldn't create bank_transaction";
+
+  $ar_transaction->load;
+  $bt->load;
+  is($ar_transaction->paid   , '0.00000' , "$testname: not paid");
+  is($bt->invoice_amount     , '0.00000' , "$testname: bt invoice amount was not assigned");
+
+  my $bt_controller = SL::Controller::BankTransaction->new;
+  my ( $bt_transactions, $proposals ) = $bt_controller->gather_bank_transactions_and_proposals(bank_account => $bank_account);
+
+  is(scalar(@$bt_transactions)         , 1  , "$testname: one bank_transaction");
+  is($bt_transactions->[0]->{agreement}, 20 , "$testname: agreement == 20");
+  my $match = join ( ' ',@{$bt_transactions->[0]->{rule_matches}});
+  #print "rule_matches='".$match."'\n";
+  is($match,
+     "remote_account_number(3) exact_amount(4) own_invoice_in_purpose(5) depositor_matches(2) remote_name(2) payment_within_30_days(1) datebonus0(3) ",
+     "$testname: rule_matches ok");
+  $bt->invoice_amount($bt->amount);
+  $bt->save;
+  is($bt->invoice_amount     , '119.00000' , "$testname: bt invoice amount now set");
+};
+
+sub test_sepa_export {
+
+  my $testname = 'test_sepa_export';
+
+  $ar_transaction = test_ar_transaction(invnumber => 'sepa1', transdate => $dt);
+
+  my $bt  = create_bank_transaction(record => $ar_transaction, transdate => $dt) or die "Couldn't create bank_transaction";
+  my $se  = create_sepa_export();
+  my $sei = create_sepa_export_item(
+    chart_id       => $bank->id,
+    ar_id          => $ar_transaction->id,
+    sepa_export_id => $se->id,
+    vc_iban        => $customer->iban,
+    vc_bic         => $customer->bic,
+    vc_mandator_id => $customer->mandator_id,
+    vc_depositor   => $customer->depositor,
+    amount         => $ar_transaction->amount,
+  );
+  require SL::SEPA::XML;
+  my $sepa_xml   = SL::SEPA::XML->new('company'     => $customer->name,
+                                      'creditor_id' => "id",
+                                      'src_charset' => 'UTF-8',
+                                      'message_id'  => "test",
+                                      'grouped'     => 1,
+                                      'collection'  => 1,
+                                     );
+  is($sepa_xml->{company}    , 'Test Customer OLE S.L. Ardbaerg AB');
+
+  $ar_transaction->load;
+  $bt->load;
+  $sei->load;
+  is($ar_transaction->paid   , '0.00000' , "$testname: sepa1 not paid");
+  is($bt->invoice_amount     , '0.00000' , "$testname: bt invoice amount was not assigned");
+  is($bt->amount             , '119.00000' , "$testname: bt amount ok");
+  is($sei->amount            , '119.00000' , "$testname: sepa export amount ok");
+
+  my $bt_controller = SL::Controller::BankTransaction->new;
+  my ( $bt_transactions, $proposals ) = $bt_controller->gather_bank_transactions_and_proposals(bank_account => $bank_account);
+
+  is(scalar(@$bt_transactions)         , 1  , "$testname: one bank_transaction");
+  is($bt_transactions->[0]->{agreement}, 25 , "$testname: agreement == 25");
+  my $match = join ( ' ',@{$bt_transactions->[0]->{rule_matches}});
+  is($match,
+     "remote_account_number(3) exact_amount(4) own_invoice_in_purpose(5) depositor_matches(2) remote_name(2) payment_within_30_days(1) datebonus0(3) sepa_export_item(5) ",
+     "$testname: rule_matches ok");
+};
+
+sub test_two_banktransactions {
+
+  my $testname = 'two_banktransactions';
+
+  my $ar_transaction_1 = test_ar_transaction(invnumber => 'salesinv10000' , amount => 2912.00 );
+  my $bt1 = create_bank_transaction(record        => $ar_transaction_1,
+                                    amount        => $ar_transaction_1->amount,
+                                    purpose       => "Rechnung10000 beinahe",
+                                    transdate     => $dt,
+                                    bank_chart_id => $bank->id,
+                                  ) or die "Couldn't create bank_transaction";
+
+  my $bt2 = create_bank_transaction(record        => $ar_transaction_1,
+                                    amount        => $ar_transaction_1->amount + 0.01,
+                                    purpose       => "sicher salesinv20000 vielleicht",
+                                    transdate     => $dt,
+                                    bank_chart_id => $bank->id,
+                                  ) or die "Couldn't create bank_transaction";
+
+  my ($agreement1, $rule_matches1) = $bt1->get_agreement_with_invoice($ar_transaction_1);
+  is($agreement1, 19, "bt1 19 points for ar_transaction_1 in $testname ok");
+  #print "rule_matches1=".$rule_matches1."\n";
+  is($rule_matches1,
+     "remote_account_number(3) exact_amount(4) own_invnumber_in_purpose(4) depositor_matches(2) remote_name(2) payment_within_30_days(1) datebonus0(3) ",
+     "$testname: rule_matches ok");
+  my ($agreement2, $rule_matches2) = $bt2->get_agreement_with_invoice($ar_transaction_1);
+  is($agreement2, 11, "bt2 11 points for ar_transaction_1 in $testname ok");
+  is($rule_matches2,
+     "remote_account_number(3) depositor_matches(2) remote_name(2) payment_within_30_days(1) datebonus0(3) ",
+     "$testname: rule_matches ok");
+
+  my $ar_transaction_2 = test_ar_transaction(invnumber => 'salesinv20000' , amount => 2912.01 );
+  my $ar_transaction_3 = test_ar_transaction(invnumber => 'zweitemit10000', amount => 2912.00 );
+     ($agreement1, $rule_matches1) = $bt1->get_agreement_with_invoice($ar_transaction_2);
+
+  is($agreement1, 11, "bt1 11 points for ar_transaction_2 in $testname ok");
+
+     ($agreement2, $rule_matches2) = $bt2->get_agreement_with_invoice($ar_transaction_2);
+  is($agreement2, 20, "bt2 20 points for ar_transaction_2 in $testname ok");
+
+     ($agreement2, $rule_matches2) = $bt2->get_agreement_with_invoice($ar_transaction_1);
+  is($agreement2, 11, "bt2 11 points for ar_transaction_1 in $testname ok");
+
+  my $bt3 = create_bank_transaction(record        => $ar_transaction_3,
+                                    amount        => $ar_transaction_3->amount,
+                                    purpose       => "sicher Rechnung10000 vielleicht",
+                                    transdate     => $dt,
+                                    bank_chart_id => $bank->id,
+                                  ) or die "Couldn't create bank_transaction";
+
+  my ($agreement3, $rule_matches3) = $bt3->get_agreement_with_invoice($ar_transaction_3);
+  is($agreement3, 19, "bt3 19 points for ar_transaction_3 in $testname ok");
+
+  $bt2->delete;
+  $ar_transaction_2->delete;
+
+  #nun sollten zwei gleichwertige Rechnungen $ar_transaction_1 und $ar_transaction_3 für $bt1 gefunden werden
+  #aber es darf keine Proposals geben mit mehreren Rechnungen
+  my $bt_controller = SL::Controller::BankTransaction->new;
+  my ( $bt_transactions, $proposals ) = $bt_controller->gather_bank_transactions_and_proposals(bank_account => $bank_account);
+
+  is(scalar(@$bt_transactions)   , 2  , "$testname: two bank_transaction");
+  is(scalar(@$proposals)         , 0  , "$testname: no proposals");
+
+  $ar_transaction_3->delete;
+
+  # Jetzt gibt es zwei Kontobewegungen mit gleichen Punkten für eine Rechnung.
+  # hier darf es auch keine Proposals geben
+
+  ( $bt_transactions, $proposals ) = $bt_controller->gather_bank_transactions_and_proposals(bank_account => $bank_account);
+
+  is(scalar(@$bt_transactions)   , 2  , "$testname: two bank_transaction");
+  # odyn testfall - anforderungen so (noch) nicht in kivi
+  # is(scalar(@$proposals)         , 0  , "$testname: no proposals");
+
+  # Jetzt gibt es zwei Kontobewegungen für eine Rechnung.
+  # eine Bewegung bekommt mehr Punkte
+  # hier darf es auch keine Proposals geben
+  $bt3->update_attributes( purpose => "fuer Rechnung salesinv10000");
+
+  ( $bt_transactions, $proposals ) = $bt_controller->gather_bank_transactions_and_proposals(bank_account => $bank_account);
+
+  is(scalar(@$bt_transactions)   , 2  , "$testname: two bank_transaction");
+  # odyn testfall - anforderungen so (noch) nicht in kivi
+  # is(scalar(@$proposals)         , 1  , "$testname: one proposal");
+
+};
+sub test_closedto {
+
+  my $testname = 'closedto';
+
+  my $ar_transaction_1 = test_ar_transaction(invnumber => 'salesinv10000' , amount => 2912.00 );
+  my $bt1 = create_bank_transaction(record        => $ar_transaction_1,
+                                    amount        => $ar_transaction_1->amount,
+                                    purpose       => "Rechnung10000 beinahe",
+                                    bank_chart_id => $bank->id,
+                                  ) or die "Couldn't create bank_transaction";
+
+  $bt1->valutadate(DateTime->new(year => 2019, month => 12, day => 30));
+  $bt1->save();
+
+  is($bt1->closed_period, 0, "$testname undefined closedto");
+
+  my $defaults = SL::DB::Manager::Default->get_all(limit => 1)->[0];
+  $defaults->closedto(DateTime->new(year => 2019, month => 12, day => 31));
+  $defaults->save();
+  $::instance_conf->reload->data;
+  $bt1->load();
+  is($bt1->closed_period, 1, "$testname defined and next date closedto");
+
+  $bt1->valutadate(DateTime->new(year => 2020, month => 1, day => 1));
+  $bt1->save();
+  $bt1->load();
+
+  is($bt1->closed_period, 0, "$testname defined closedto and next date valuta");
+  $defaults->closedto(undef);
+  $defaults->save();
+
+}
+
+sub test_skonto_exact_ap_transaction {
+
+  my $testname = 'test_skonto_exact_ap_transaction';
+
+  $ap_transaction = test_ap_transaction(invnumber => 'ap transaction skonto',
+                                        payment_id => $payment_terms->id,
+                                       );
+
+  my $bt = create_bank_transaction(record        => $ap_transaction,
+                                   bank_chart_id => $bank->id,
+                                   transdate     => $dt,
+                                   valutadate    => $dt,
+                                   amount        => $ap_transaction->amount_less_skonto
+                                  ) or die "Couldn't create bank_transaction";
+
+  $::form->{invoice_ids} = {
+    $bt->id => [ $ap_transaction->id ]
+  };
+  $::form->{invoice_skontos} = {
+    $bt->id => [ 'with_skonto_pt' ]
+  };
+
+  save_btcontroller_to_string();
+
+  $ap_transaction->load;
+  $bt->load;
+  is($ap_transaction->paid   , '119.00000' , "$testname: ap transaction skonto was paid");
+  is($ap_transaction->closed , 1           , "$testname: ap transaction skonto is closed");
+  is($bt->invoice_amount     , '113.05000' , "$testname: bt invoice amount was assigned");
+
+};
+
+1;
index 3d5bad0..b9ce240 100644 (file)
@@ -1,6 +1,6 @@
 use strict;
 use Test::Exception;
-use Test::More;
+use Test::More tests => 19;
 use Test::Output;
 
 use lib 't';
@@ -14,17 +14,10 @@ no warnings 'uninitialized';
 
 Support::TestSetup::login();
 
-if (!Support::TestSetup::templates_cache_writable()) {
-  plan skip_all => 'Cache dir not writable for this test';
-} else {
-  plan tests => 19;
-}
-
 sub reset_test_env {
   $ENV{HTTP_USER_AGENT} = 'Perl Tests';
 
-  $::request       = SL::Request->new(
-    cgi => CGI->new({}),
+  $::request = Support::TestSetup->create_new_request(
     layout => SL::Layout::Javascript->new,
   );
 
diff --git a/t/controllers/csvimport/artransactions.t b/t/controllers/csvimport/artransactions.t
new file mode 100644 (file)
index 0000000..6c902c3
--- /dev/null
@@ -0,0 +1,577 @@
+use Test::More tests => 70;
+
+use strict;
+
+use lib 't';
+
+use Carp;
+use Data::Dumper;
+use Support::TestSetup;
+use Test::Exception;
+
+use List::MoreUtils qw(pairwise);
+use SL::Controller::CsvImport;
+
+my $DEBUG = 0;
+
+use_ok 'SL::Controller::CsvImport::ARTransaction';
+
+use SL::DB::Buchungsgruppe;
+use SL::DB::Currency;
+use SL::DB::Customer;
+use SL::DB::Employee;
+use SL::DB::Invoice;
+use SL::DB::TaxZone;
+use SL::DB::Chart;
+use SL::DB::AccTransaction;
+
+my ($customer, $currency_id, $employee, $taxzone, $project, $department);
+my ($transdate, $transdate_string);
+
+sub reset_state {
+  # Create test data
+  my %params = @_;
+
+  $transdate = DateTime->today_local;
+  $transdate->set_year(2019) if $transdate->year == 2020; # hardcode for 2019 in 2020, because of tax rate change in Germany
+  $transdate_string = $transdate->to_kivitendo;
+
+  $params{$_} ||= {} for qw(buchungsgruppe customer tax);
+
+  clear_up();
+  $employee        = SL::DB::Manager::Employee->current                          || croak "No employee";
+  $taxzone         = SL::DB::Manager::TaxZone->find_by( description => 'Inland') || croak "No taxzone";
+  $currency_id     = $::instance_conf->get_currency_id;
+
+  $customer     = SL::DB::Customer->new(
+    name        => 'Test Customer',
+    currency_id => $currency_id,
+    taxzone_id  => $taxzone->id,
+    %{ $params{customer} }
+  )->save;
+
+  $project     = SL::DB::Project->new(
+    projectnumber  => 'P1',
+    description    => 'Project X',
+    project_type   => SL::DB::Manager::ProjectType->find_by(description => 'Standard'),
+    project_status => SL::DB::Manager::ProjectStatus->find_by(name => 'running'),
+  )->save;
+
+  $department     = SL::DB::Department->new(
+    description    => 'Department 1',
+  )->save;
+}
+
+Support::TestSetup::login();
+
+reset_state(customer => {id => 960, customernumber => 2});
+
+#####
+sub test_import {
+  my $file = shift;
+
+  my $controller = SL::Controller::CsvImport->new(
+    type => 'ar_transactions'
+  );
+  $controller->load_default_profile;
+  $controller->profile->set(
+    charset      => 'utf-8',
+    sep_char     => ',',
+    quote_char   => '"',
+    numberformat => $::myconfig{numberformat},
+  );
+
+  my $csv_artransactions_import = SL::Controller::CsvImport::ARTransaction->new(
+    settings    => {'ar_column'          => 'Rechnung',
+                    'transaction_column' => 'AccTransaction',
+                    'max_amount_diff'    => 0.02
+                  },
+    controller => $controller,
+    file       => $file,
+  );
+
+  # $csv_artransactions_import->init_vc_by;
+  $csv_artransactions_import->run(test => 0);
+
+  # don't try and save objects that have errors
+  $csv_artransactions_import->save_objects unless scalar @{$csv_artransactions_import->controller->data->[0]->{errors}};
+
+ return $csv_artransactions_import->controller->data;
+}
+
+##### manually create an ar transaction from scratch, testing the methods
+$::myconfig{numberformat} = '1000.00';
+$::myconfig{dateformat}   = 'dd.mm.yyyy';
+my $old_locale = $::locale;
+# set locale to en so we can match errors
+$::locale = Locale->new('en');
+
+my $amount = 10;
+
+my $ar = SL::DB::Invoice->new(
+  invoice      => 0,
+  invnumber    => 'manual invoice',
+  taxzone_id   => $taxzone->id,
+  currency_id  => $currency_id,
+  taxincluded  => 'f',
+  customer_id  => $customer->id,
+  transdate    => $transdate,
+  employee_id  => SL::DB::Manager::Employee->current->id,
+  transactions => [],
+);
+
+my $tax3 = SL::DB::Manager::Tax->find_by(rate => 0.19, taxkey => 3) || die "can't find tax with taxkey 3";
+my $income_chart = SL::DB::Manager::Chart->find_by(accno => '8400') || die "can't find income chart";
+
+$ar->add_ar_amount_row(
+  amount => $amount,
+  chart  => $income_chart,
+  tax_id => $tax3->id,
+);
+
+$ar->recalculate_amounts; # set amount and netamount from transactions
+is $ar->amount, '10', 'amount of manual invoice is 10';
+is $ar->netamount, '8.4', 'netamount of manual invoice is 10';
+
+$ar->create_ar_row( chart => SL::DB::Manager::Chart->find_by(accno => '1400', link => 'AR') );
+my $result = $ar->validate_acc_trans(debug => 0);
+is $result, 1, 'manual $ar validates';
+
+$ar->save;
+is ${ $ar->transactions }[0]->chart->accno, '8400', 'assigned income chart after save ok';
+is ${ $ar->transactions }[2]->chart->accno, '1400', 'assigned receivable chart after save ok';
+is scalar @{$ar->transactions}, 3, 'manual invoice has 3 acc_trans entries';
+
+$ar->pay_invoice(  chart_id      => SL::DB::Manager::Chart->find_by(accno => '1200')->id, # bank
+                   amount        => $ar->open_amount,
+                   transdate     => $transdate,
+                   payment_type  => 'without_skonto',  # default if not specified
+                  );
+$result = $ar->validate_acc_trans(debug => 0);
+is $result, 1, 'manual invoice validates after payment';
+
+reset_state(customer => {id => 960, customernumber => 2});
+
+my ($entries, $entry, $file);
+
+# starting test of csv imports
+# to debug errors in certain tests, run after test_import:
+#   die Dumper($entry->{errors});
+##### basic test
+$file = \<<"EOL";
+datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart,transdate
+datatype,accno,amount,taxkey
+"Rechnung",960,4,1,"invoice 1",f,1400,"$transdate_string"
+"AccTransaction",8400,159.48,3
+EOL
+$entries = test_import($file);
+$entry = $entries->[0];
+$entry->{object}->validate_acc_trans;
+
+is $entry->{object}->invnumber, 'invoice 1', 'simple invnumber ok (customer)';
+is $entry->{object}->customer_id, '960', 'simple customer_id ok (customer)';
+is scalar @{$entry->{object}->transactions}, 3, 'invoice 1 has 3 acc_trans entries';
+is $::form->round_amount($entry->{object}->transactions->[0]->amount, 2), 159.48, 'invoice 1 ar amount is 159.48';
+is $entry->{object}->direct_debit, '0', 'no direct debit';
+is $entry->{object}->taxincluded, '0', 'taxincluded is false';
+is $entry->{object}->amount, '189.78', 'ar amount tax not included is 189.78';
+is $entry->{object}->netamount, '159.48', 'ar netamount tax not included is 159.48';
+
+##### test for duplicate invnumber
+$file = \<<"EOL";
+datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart,transdate
+datatype,accno,amount,taxkey
+"Rechnung",960,4,1,"invoice 1",f,1400,"$transdate_string"
+"AccTransaction",8400,159.48,3
+EOL
+$entries = test_import($file);
+$entry = $entries->[0];
+$entry->{object}->validate_acc_trans;
+is $entry->{errors}->[0], 'Error: invnumber already exists', 'detects verify_amount differences';
+
+##### test for no invnumber given
+$file = \<<"EOL";
+datatype,customer_id,taxzone_id,currency_id,taxincluded,archart,transdate
+datatype,accno,amount,taxkey
+"Rechnung",960,4,1,f,1400,"$transdate_string"
+"AccTransaction",8400,159.48,3
+EOL
+$entries = test_import($file);
+$entry = $entries->[0];
+$entry->{object}->validate_acc_trans;
+is $entry->{object}->invnumber =~ /^\d+$/, 1, 'invnumber assigned automatically';
+
+##### basic test without amounts in Rechnung, only specified in AccTransaction
+$file = \<<"EOL";
+datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart,transdate
+datatype,accno,amount,taxkey
+"Rechnung",960,4,1,"invoice 1 no amounts",f,1400,"$transdate_string"
+"AccTransaction",8400,159.48,3
+EOL
+$entries = test_import($file);
+$entry = $entries->[0];
+$entry->{object}->validate_acc_trans;
+
+is $entry->{object}->invnumber, 'invoice 1 no amounts', 'simple invnumber ok (customer)';
+is $entry->{object}->customer_id, '960', 'simple customer_id ok (customer)';
+is scalar @{$entry->{object}->transactions}, 3, 'invoice 1 has 3 acc_trans entries';
+is $::form->round_amount($entry->{object}->amount, 2), '189.78', 'not taxincluded ar amount';
+is $::form->round_amount($entry->{object}->transactions->[0]->amount, 2), '159.48', 'not taxincluded acc_trans netamount';
+is $::form->round_amount($entry->{object}->transactions->[0]->amount, 2), 159.48, 'invoice 1 ar amount is 159.48';
+
+##### basic test: credit_note
+$file = \<<"EOL";
+datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart,transdate
+datatype,accno,amount,taxkey
+"Rechnung",960,4,1,"credit note",f,1400,"$transdate_string"
+"AccTransaction",8400,-159.48,3
+EOL
+$entries = test_import($file);
+$entry = $entries->[0];
+$entry->{object}->validate_acc_trans;
+
+is $entry->{object}->invnumber, 'credit note', 'simple credit note ok';
+is scalar @{$entry->{object}->transactions}, 3, 'credit note has 3 acc_trans entries';
+is $::form->round_amount($entry->{object}->amount, 2), '-189.78', 'taxincluded ar amount';
+is $::form->round_amount($entry->{object}->netamount, 2), '-159.48', 'taxincluded ar net amount';
+is $::form->round_amount($entry->{object}->transactions->[0]->amount, 2), -159.48, 'credit note ar amount is -159.48';
+is $entry->{object}->amount, '-189.78', 'credit note amount tax not included is 189.78';
+is $entry->{object}->netamount, '-159.48', 'credit note netamount tax not included is 159.48';
+
+#### verify_amount differs: max_amount_diff = 0.02, 189.80 is ok, 189.81 is not
+$file = \<<"EOL";
+datatype,customer_id,verify_amount,verify_netamount,taxzone_id,currency_id,invnumber,taxincluded,archart,transdate
+datatype,accno,amount,taxkey
+"Rechnung",960,189.81,159.48,4,1,"invoice amounts differing",f,1400,"$transdate_string"
+"AccTransaction",8400,159.48,3
+EOL
+$entries = test_import($file);
+$entry = $entries->[0];
+is $entry->{errors}->[0], 'Amounts differ too much', 'detects verify_amount differences';
+
+#####  direct debit
+$file = \<<"EOL";
+datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,direct_debit,archart,transdate
+datatype,accno,amount,taxkey
+"Rechnung",960,4,1,"invoice with direct debit",f,t,1400,"$transdate_string"
+"AccTransaction",8400,159.48,3
+EOL
+
+$entries = test_import($file);
+$entry = $entries->[0];
+$entry->{object}->validate_acc_trans;
+is $entry->{object}->direct_debit, '1', 'direct debit';
+
+#### tax included
+$file = \<<"EOL";
+datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart,transdate
+datatype,accno,amount,taxkey
+"Rechnung",960,4,1,"invoice 1 tax included no amounts",t,1400,"$transdate_string"
+"AccTransaction",8400,189.78,3
+EOL
+
+$entries = test_import($file);
+$entry = $entries->[0];
+$entry->{object}->validate_acc_trans(debug => 0);
+is $entry->{object}->taxincluded, '1', 'taxincluded is true';
+is $::form->round_amount($entry->{object}->amount, 2), '189.78', 'taxincluded ar amount';
+is $::form->round_amount($entry->{object}->netamount, 2), '159.48', 'taxincluded ar net amount';
+is $::form->round_amount($entry->{object}->transactions->[0]->amount, 2), '159.48', 'taxincluded acc_trans netamount';
+
+#### multiple tax included
+$file = \<<"EOL";
+datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart,transdate
+datatype,accno,amount,taxkey
+"Rechnung",960,4,1,"invoice multiple tax included",t,1400,"$transdate_string"
+"AccTransaction",8400,94.89,3
+"AccTransaction",8400,94.89,3
+EOL
+
+$entries = test_import($file);
+$entry = $entries->[0];
+$entry->{object}->validate_acc_trans;
+is $::form->round_amount($entry->{object}->amount, 2),    '189.78', 'taxincluded ar amount';
+is $::form->round_amount($entry->{object}->netamount, 2), '159.48', 'taxincluded ar netamount';
+is $::form->round_amount($entry->{object}->transactions->[0]->amount, 2), '79.74', 'taxincluded amount';
+is $::form->round_amount($entry->{object}->transactions->[1]->amount, 2), '15.15', 'taxincluded tax';
+
+# different receivables chart
+$file = \<<EOL;
+datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart
+datatype,accno,amount,taxkey
+"Rechnung",960,4,1,"invoice mit archart 1448",f,1448
+"AccTransaction",8400,159.48,3
+EOL
+$entries = test_import($file);
+$entry = $entries->[0];
+$entry->{object}->validate_acc_trans;
+is $entry->{object}->transactions->[2]->chart->accno, '1448', 'archart set to 1448';
+
+# missing customer
+$file = \<<EOL;
+datatype,taxzone_id,currency_id,invnumber,taxincluded,archart
+datatype,accno,amount,taxkey
+"Rechnung",4,1,"invoice missing customer",f,1400
+"AccTransaction",8400,159.48,3
+EOL
+$entries = test_import($file);
+$entry = $entries->[0];
+is $entry->{errors}->[0], 'Error: Customer/vendor missing', 'detects missing customer or vendor';
+
+
+##### customer by name
+$file = \<<EOL;
+datatype,customer,taxzone_id,currency_id,invnumber,taxincluded,archart
+datatype,accno,amount,taxkey
+"Rechnung","Test Customer",4,1,"invoice customer name",f,1400
+"AccTransaction",8400,159.48,3
+EOL
+$entries = test_import($file);
+$entry = $entries->[0];
+$entry->{object}->validate_acc_trans;
+is $entry->{object}->customer->name, "Test Customer", 'detects customer by name';
+
+##### detect missing chart
+$file = \<<EOL;
+datatype,taxzone_id,currency_id,invnumber,customer,archart
+datatype,amount,taxkey
+"Rechnung",4,1,"invoice missing chart","Test Customer",1400
+"AccTransaction",4,3
+EOL
+$entries = test_import($file);
+$entry = $entries->[1];
+is $entry->{errors}->[0], 'Error: chart missing', 'detects missing chart (chart_id or accno)';
+
+##### detect illegal chart by accno
+$file = \<<EOL;
+datatype,taxzone_id,currency_id,invnumber,customer,archart
+datatype,accno,amount,taxkey
+"Rechnung",4,1,"invoice illegal chart accno","Test Customer",1400
+"AccTransaction",9999,4,3
+EOL
+$entries = test_import($file);
+$entry = $entries->[1];
+is $entry->{errors}->[0], 'Error: invalid chart (accno)', 'detects invalid chart (chart_id or accno)';
+
+# ##### detect illegal archart
+$file = \<<EOL;
+datatype,taxzone_id,currency_id,invnumber,customer,taxincluded,archart
+datatype,accno,amount,taxkey
+"Rechnung",4,1,"invoice illegal archart","Test Customer",f,11400
+"AccTransaction",8400,159.48,3
+EOL
+$entries = test_import($file);
+$entry = $entries->[0];
+is $entry->{errors}->[0], "Error: can't find ar chart with accno 11400", 'detects illegal receivables chart (archart)';
+
+##### detect chart by id
+$file = \<<EOL;
+datatype,taxzone_id,currency_id,invnumber,customer,taxincluded,archart
+datatype,amount,chart_id,taxkey
+"Rechnung",4,1,"invoice chart_id","Test Customer",f,1400
+"AccTransaction",159.48,184,3
+EOL
+$entries = test_import($file);
+$entry = $entries->[1]; # acc_trans entry is at entry array pos 1
+$entries->[0]->{object}->validate_acc_trans;
+is $entry->{object}->chart->id, "184", 'detects chart by id';
+
+##### detect chart by accno
+$file = \<<EOL;
+datatype,taxzone_id,currency_id,invnumber,customer,taxincluded,archart
+datatype,amount,accno,taxkey
+"Rechnung",4,1,"invoice by chart accno","Test Customer",f,1400
+"AccTransaction",159.48,8400,3
+EOL
+$entries = test_import($file);
+$entry = $entries->[1];
+$entries->[0]->{object}->validate_acc_trans;
+is $entry->{object}->chart->accno, "8400", 'detects chart by accno';
+
+##### detect chart isn't an ar_chart
+$file = \<<EOL;
+datatype,taxzone_id,currency_id,invnumber,customer,taxincluded,archart
+datatype,amount,accno,taxkey
+"Rechnung",4,1,"invoice by chart accno","Test Customer",f,1400
+"AccTransaction",159.48,1400,3
+EOL
+$entries = test_import($file);
+$entry = $entries->[1];
+$entries->[0]->{object}->validate_acc_trans;
+is $entry->{errors}->[0], 'Error: chart isn\'t an ar_amount chart', 'detects valid chart that is not an ar_amount chart';
+
+# missing taxkey
+$file = \<<EOL;
+datatype,taxzone_id,currency_id,invnumber,customer,archart
+datatype,amount,accno
+"Rechnung",4,1,"invoice missing taxkey chart accno","Test Customer",1400
+"AccTransaction",159.48,8400
+EOL
+$entries = test_import($file);
+$entry = $entries->[1];
+is $entry->{errors}->[0], 'Error: taxkey missing', 'detects missing taxkey (DATEV Steuerschlüssel)';
+
+# illegal taxkey
+$file = \<<EOL;
+datatype,taxzone_id,currency_id,invnumber,customer,archart
+datatype,amount,accno,taxkey
+"Rechnung",4,1,"invoice illegal taxkey","Test Customer",1400
+"AccTransaction",4,8400,123
+EOL
+$entries = test_import($file);
+$entry = $entries->[1];
+is $entry->{errors}->[0], 'Error: invalid taxkey', 'detects invalid taxkey (DATEV Steuerschlüssel)';
+
+# taxkey
+$file = \<<EOL;
+datatype,customer_id,taxzone_id,currency_id,invnumber,archart
+datatype,accno,amount,taxkey
+"Rechnung",960,4,1,"invoice by taxkey",1400
+"AccTransaction",8400,4,3
+EOL
+
+$entries = test_import($file);
+$entry = $entries->[1];
+is $entry->{object}->taxkey, 3, 'detects taxkey';
+
+# acc_trans project
+$file = \<<EOL;
+datatype,customer_id,taxzone_id,currency_id,invnumber,archart,taxincluded
+datatype,accno,amount,taxkey,projectnumber
+"Rechnung",960,4,1,"invoice with acc_trans project",1400,f
+"AccTransaction",8400,159.48,3,P1
+EOL
+
+$entries = test_import($file);
+$entry = $entries->[1];
+# die Dumper($entries->[0]->{errors}) if scalar @{$entries->[0]->{errors}};
+is $entry->{object}->project->projectnumber, 'P1', 'detects acc_trans project';
+
+#####  various tests
+$file = \<<EOL;
+datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart,transdate,duedate,globalprojectnumber,department
+datatype,accno,amount,taxkey,projectnumber
+"Rechnung",960,4,1,"invoice various",t,1400,21.04.2016,30.04.2016,P1,Department 1
+"AccTransaction",8400,119,3,P1
+"AccTransaction",8300,107,2,P1
+"AccTransaction",8200,100,0,P1
+EOL
+
+$entries = test_import($file);
+$entry = $entries->[0];
+$entry->{object}->validate_acc_trans;
+is $entry->{object}->duedate->to_kivitendo,      '30.04.2016',    'duedate';
+is $entry->{object}->transdate->to_kivitendo,    '21.04.2016',    'transdate';
+is $entry->{object}->globalproject->description, 'Project X',     'project';
+is $entry->{object}->department->description,    'Department 1',  'department';
+# 8300 is third entry after 8400 and tax for 8400
+is $::form->round_amount($entry->{object}->transactions->[2]->amount),     '100',        '8300 net amount: 100';
+is $::form->round_amount($entry->{object}->transactions->[2]->taxkey),     '2',          '8300 has taxkey 2';
+is $::form->round_amount($entry->{object}->transactions->[2]->project_id), $project->id, 'AccTrans project';
+
+#####  ar amount test
+$file = \<<EOL;
+datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart,transdate,duedate,globalprojectnumber,department
+datatype,accno,amount,taxkey,projectnumber
+"Rechnung",960,4,1,"invoice various 1",t,1400,21.04.2016,30.04.2016,P1,Department 1
+"AccTransaction",8400,119,3,P1
+"AccTransaction",8300,107,2,P1
+"AccTransaction",8200,100,0,P1
+"Rechnung",960,4,1,"invoice various 2",t,1400,21.04.2016,30.04.2016,P1,Department 1
+"AccTransaction",8400,119,3,P1
+"AccTransaction",8300,107,2,P1
+"AccTransaction",8200,100,0,P1
+EOL
+
+$entries = test_import($file);
+$entry = $entries->[0];
+$entry->{object}->validate_acc_trans;
+is $entry->{object}->duedate->to_kivitendo,      '30.04.2016',    'duedate';
+is $entry->{info_data}->{amount}, '326', "First invoice amount displayed in info data";
+is $entries->[4]->{info_data}->{amount}, '326', "Second invoice amount displayed in info data";
+
+# multiple entries, taxincluded = f
+$file = \<<EOL;
+datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart
+datatype,accno,amount,taxkey
+"Rechnung",960,4,1,"invoice 4 acc_trans",f,1400
+"AccTransaction",8400,39.87,3
+"AccTransaction",8400,39.87,3
+"AccTransaction",8400,39.87,3
+"AccTransaction",8400,39.87,3
+"Rechnung",960,4,1,"invoice 4 acc_trans 2",f,1400
+"AccTransaction",8400,39.87,3
+"AccTransaction",8400,39.87,3
+"AccTransaction",8400,39.87,3
+"AccTransaction",8400,39.87,3
+"Rechnung",960,4,1,"invoice 4 acc_trans 3",f,1400
+"AccTransaction",8400,39.87,3
+"AccTransaction",8400,39.87,3
+"AccTransaction",8400,39.87,3
+"AccTransaction",8400,39.87,3
+"Rechnung",960,4,1,"invoice 4 acc_trans 4",f,1448
+"AccTransaction",8400,39.87,3
+"AccTransaction",8400,39.87,3
+"AccTransaction",8400,39.87,3
+"AccTransaction",8400,39.87,3
+EOL
+$entries = test_import($file);
+
+my $i = 0;
+foreach my $entry ( @$entries ) {
+  next unless $entry->{object}->isa('SL::DB::Invoice');
+  $i++;
+  is scalar @{$entry->{object}->transactions}, 9, "invoice $i: 'invoice 4 acc_trans' has 9 acc_trans entries";
+  $entry->{object}->validate_acc_trans;
+};
+
+##### missing acc_trans
+$file = \<<EOL;
+datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart,transdate,duedate,globalprojectnumber,department
+datatype,accno,amount,taxkey,projectnumber
+"Rechnung",960,4,1,"invoice acc_trans missing",t,1400,21.04.2016,30.04.2016,P1,Department 1
+"Rechnung",960,4,1,"invoice various a",t,1400,21.04.2016,30.04.2016,P1,Department 1
+"AccTransaction",8400,119,3,P1
+"AccTransaction",8300,107,2,P1
+EOL
+
+$entries = test_import($file);
+$entry = $entries->[0];
+is $entry->{errors}->[0], "Error: ar transaction doesn't validate", 'detects invalid ar, maybe acc_trans entry missing';
+
+my $number_of_imported_invoices = SL::DB::Manager::Invoice->get_all_count;
+is $number_of_imported_invoices, 19, 'All invoices saved';
+
+#### taxkey differs from active_taxkey
+$file = \<<EOL;
+datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart
+datatype,accno,amount,taxkey
+"Rechnung",960,4,1,"invoice 1 tax included no amounts",t,1400
+"AccTransaction",8400,189.78,2
+EOL
+
+$entries = test_import($file);
+$entry = $entries->[0];
+$entry->{object}->validate_acc_trans(debug => 0);
+
+clear_up(); # remove all data at end of tests
+# end of tests
+
+
+sub clear_up {
+  SL::DB::Manager::AccTransaction->delete_all(all => 1);
+  SL::DB::Manager::Invoice->delete_all       (all => 1);
+  SL::DB::Manager::Customer->delete_all      (all => 1);
+  SL::DB::Manager::Project->delete_all       (all => 1);
+  SL::DB::Manager::Department->delete_all    (all => 1);
+};
+
+
+1;
+
+#####
+# vim: ft=perl
+# set emacs to perl mode
+# Local Variables:
+# mode: perl
+# End:
diff --git a/t/controllers/csvimport/customervendor.t b/t/controllers/csvimport/customervendor.t
new file mode 100644 (file)
index 0000000..2eb987e
--- /dev/null
@@ -0,0 +1,283 @@
+use Test::More tests => 41;
+
+use strict;
+
+use lib 't';
+
+use Support::TestSetup;
+use List::MoreUtils qw(none any);
+
+use SL::DB::Customer;
+use SL::DB::CustomVariableConfig;
+use SL::DB::Default;
+
+use SL::Controller::CsvImport;
+use_ok 'SL::Controller::CsvImport::CustomerVendor';
+
+Support::TestSetup::login();
+
+#####
+sub do_import {
+  my ($file, $settings) = @_;
+
+  my $controller = SL::Controller::CsvImport->new(
+    type => 'customers_vendors',
+  );
+  $controller->load_default_profile;
+  $controller->profile->set(
+    charset  => 'utf-8',
+    sep_char => ';',
+    %$settings
+  );
+
+  my $worker = SL::Controller::CsvImport::CustomerVendor->new(
+    controller => $controller,
+    file       => $file,
+  );
+  $worker->run(test => 0);
+
+  return if $worker->controller->errors;
+
+  # don't try and save objects that have errors
+  $worker->save_objects unless scalar @{$worker->controller->data->[0]->{errors}};
+
+  return $worker->controller->data;
+}
+
+sub _obj_of {
+  return $_[0]->{object_to_save} || $_[0]->{object};
+}
+
+sub clear_up {
+  SL::DB::Manager::Customer->delete_all(all => 1);
+  SL::DB::Manager::CustomVariableConfig->delete_all(all => 1);
+
+  SL::DB::Default->get->update_attributes(customernumber => '10000');
+
+  # Reset request to clear caches. Here especially for cvar-configs.
+  $::request = Support::TestSetup->create_new_request;
+}
+
+#####
+
+# set numberformat and locale (so we can match errors)
+my $old_numberformat      = $::myconfig{numberformat};
+$::myconfig{numberformat} = '1.000,00';
+my $old_locale            = $::locale;
+$::locale                 = Locale->new('en');
+
+clear_up;
+
+#####
+# import and update entries
+
+my $file = \<<EOL;
+name;street;
+CustomerName;CustomerStreet
+EOL
+
+my $entries = do_import($file, {update_policy => 'update_existing'});
+
+ok none {'Updating existing entry in database' eq $_} @{$entries->[0]->{information}}, 'import entry - information (customer)';
+is _obj_of($entries->[0])->customernumber, '10001',          'import entry - number (customer)';
+is _obj_of($entries->[0])->name,           'CustomerName',   'import entry - name (customer)';
+is _obj_of($entries->[0])->street,         'CustomerStreet', 'import entry - street (customer)';
+is _obj_of($entries->[0]),                 $entries->[0]->{object}, 'import entry - object not object_to_save (customer)';
+
+my $default_customernumer = SL::DB::Default->get->load->customernumber;
+is $default_customernumer, '10001', 'import entry - defaults range of numbers (customer)';
+
+my $customer_id = _obj_of($entries->[0])->id;
+
+$entries = undef;
+
+$file = \<<EOL;
+customernumber;name;street;
+10001;CustomerName;NewCustomerStreet
+EOL
+
+$entries = do_import($file, {update_policy => 'update_existing'});
+
+ok any {'Updating existing entry in database' eq $_} @{ $entries->[0]->{information} }, 'update entry - information (customer)';
+is _obj_of($entries->[0])->customernumber, '10001',             'update entry - number (customer)';
+is _obj_of($entries->[0])->name,           'CustomerName',      'update entry - name (customer)';
+is _obj_of($entries->[0])->street,         'NewCustomerStreet', 'update entry - street (customer)';
+is _obj_of($entries->[0]),                 $entries->[0]->{object_to_save}, 'update entry - object is object_to_save (customer)';
+is _obj_of($entries->[0])->id,             $customer_id,        'update entry - same id (customer)';
+$default_customernumer = SL::DB::Default->get->load->customernumber;
+is $default_customernumer, '10001', 'update entry - defaults range of numbers (customer)';
+
+$entries = undef;
+
+$file = \<<EOL;
+customernumber;name;street;
+10001;WrongCustomerName;WrongCustomerStreet
+EOL
+
+$entries = do_import($file, {update_policy => 'skip'});
+
+ok any {'Skipping due to existing entry in database' eq $_} @{ $entries->[0]->{errors} }, 'skip entry - error (customer)';
+
+$default_customernumer = SL::DB::Default->get->load->customernumber;
+is $default_customernumer, '10001', 'skip entry - defaults range of numbers (customer)';
+
+$entries = undef;
+
+clear_up;
+#####
+
+$file = \<<EOL;
+name
+CustomerName
+EOL
+
+$entries = do_import($file);
+
+is scalar @$entries,             1,              'one entry - nuber of entries (customer)';
+is _obj_of($entries->[0])->name, 'CustomerName', 'simple file - name only (customer)';
+
+$entries = undef;
+clear_up;
+
+#####
+$file = \<<EOL;
+customernumber;name
+1;CustomerName1
+2;CustomerName2
+EOL
+
+$entries = do_import($file);
+
+is scalar @$entries,                       2,               'two entries - number of entries (customer)';
+is _obj_of($entries->[0])->name,           'CustomerName1', 'two entries, number and name - name  (customer)';
+is _obj_of($entries->[0])->customernumber, '1',             'two entries, number and name - number  (customer)';
+is _obj_of($entries->[1])->name,           'CustomerName2', 'two entries, number and name - name  (customer)';
+is _obj_of($entries->[1])->customernumber, '2',             'two entries, number and name - number  (customer)';
+
+$entries = undef;
+clear_up;
+
+#####
+$file = \<<EOL;
+name;creditlimit;discount
+CustomerName1;1.280,50;0,035
+EOL
+
+$entries = do_import($file);
+
+is scalar @$entries,                     1,              'creditlimit/discount - number of entries (customer)';
+is _obj_of($entries->[0])->name,        'CustomerName1', 'creditlimit/discount - name  (customer)';
+is _obj_of($entries->[0])->creditlimit, 1280.5,          'creditlimit/discount - creditlimit (customer))';
+# Should discount be given in percent or in decimal?
+is _obj_of($entries->[0])->discount,   0.035,            'creditlimit/discount - discount (customer)';
+
+$entries = undef;
+clear_up;
+
+#####
+# Test import with cvars.
+# Customer/vendor cvars can have a default value, so the following cases are to be
+# tested
+# - new customer in csv - no cvars given -> one should be unset, the other one
+#   should have the default value
+# - new customer in csv - both cvars given -> cvars should have the given values
+# - update customer with no cvars in csv -> cvars should not change
+# - update customer with both cvars in csv -> cvars should have the given values
+# (not explicitly testet: does an empty cvar field means to unset the cvar or to
+# leave it untouched?)
+
+# create cvars
+SL::DB::CustomVariableConfig->new(
+  module              => 'CT',
+  name                => 'no_default',
+  description         => 'no default',
+  type                => 'text',
+  searchable          => 1,
+  sortkey             => 1,
+  includeable         => 0,
+  included_by_default => 0,
+)->save;
+
+SL::DB::CustomVariableConfig->new(
+  module              => 'CT',
+  name                => 'with_default',
+  description         => 'with default',
+  type                => 'text',
+  default_value       => 'this is the default',
+  searchable          => 1,
+  sortkey             => 1,
+  includeable         => 0,
+  included_by_default => 0,
+)->save;
+
+# - new customer in csv - no cvars given -> one should be unset, the other one
+#   should have the default value
+$file = \<<EOL;
+customernumber;name;
+1;CustomerName1
+EOL
+
+$entries = do_import($file);
+
+is _obj_of($entries->[0])->customernumber,                      '1',                   'cvar test - import customer 1 with no cvars - number (customer)';
+is _obj_of($entries->[0])->cvar_by_name('no_default')->value,   undef,                 'cvar test - import customer 1 - do not set ungiven cvar which has no default';
+is _obj_of($entries->[0])->cvar_by_name('with_default')->value, 'this is the default', 'cvar test - import customer 1 - do set ungiven cvar which has default';
+
+$entries = undef;
+
+# - new customer in csv - both cvars given -> cvars should have the given values
+$file = \<<EOL;
+customernumber;name;cvar_no_default;cvar_with_default
+2;CustomerName2;"new cvar value abc";"new cvar value xyz"
+EOL
+
+$entries = do_import($file);
+
+is _obj_of($entries->[0])->customernumber,                      '2',                  'cvar test - import customer 2 with cvars - number (customer)';
+is _obj_of($entries->[0])->cvar_by_name('no_default')->value,   'new cvar value abc', 'cvar test - import customer 2 - do set given cvar which has default';
+is _obj_of($entries->[0])->cvar_by_name('with_default')->value, 'new cvar value xyz', 'cvar test - import customer 2 - do set given cvar which has default';
+
+$entries = undef;
+
+# - update customer with no cvars in csv -> cvars should not change
+$file = \<<EOL;
+customernumber;name;street
+1;CustomerName1;"street cs1"
+EOL
+
+$entries = do_import($file, {update_policy => 'update_existing'});
+is _obj_of($entries->[0])->customernumber,                      '1',                   'cvar test - update customer 1 - number (customer)';
+is _obj_of($entries->[0])->street,                              'street cs1',          'cvar test - update customer 1 - set new street (customer)';
+is _obj_of($entries->[0])->cvar_by_name('no_default')->value,   undef,                 'cvar test - update customer 1 - do not set ungiven cvar which has no default';
+is _obj_of($entries->[0])->cvar_by_name('with_default')->value, 'this is the default', 'cvar test - update customer 1 - do set ungiven cvar which has default';
+
+$entries = undef;
+
+# - update customer with both cvars in csv -> cvars should have the given values
+$file = \<<EOL;
+customernumber;name;street;cvar_no_default;cvar_with_default
+1;CustomerName1;"new street cs1";totaly new cvar 123;totaly new cvar abc
+EOL
+
+$entries = do_import($file, {update_policy => 'update_existing'});
+is _obj_of($entries->[0])->customernumber,                      '1',                   'cvar test - update customer 1 - number (customer)';
+is _obj_of($entries->[0])->street,                              'new street cs1',      'cvar test - update customer 1 - set new street (customer)';
+is _obj_of($entries->[0])->cvar_by_name('no_default')->value,   'totaly new cvar 123', 'cvar test - update customer 1 - do set given cvar which has no default (customer)';
+is _obj_of($entries->[0])->cvar_by_name('with_default')->value, 'totaly new cvar abc', 'cvar test - update customer 1 - do set given cvar which has default (customer)';
+
+$entries = undef;
+
+
+clear_up;
+
+$::myconfig{numberformat} = $old_numberformat;
+$::locale                 = $old_locale;
+
+1;
+
+#####
+# vim: ft=perl
+# set emacs to perl mode
+# Local Variables:
+# mode: perl
+# End:
diff --git a/t/controllers/csvimport/delivery_orders.t b/t/controllers/csvimport/delivery_orders.t
new file mode 100644 (file)
index 0000000..ca38c2d
--- /dev/null
@@ -0,0 +1,282 @@
+use Test::More tests => 28;
+
+use strict;
+
+use lib 't';
+use utf8;
+
+use Support::TestSetup;
+
+use List::MoreUtils qw(none any);
+
+use SL::Controller::CsvImport;
+use_ok 'SL::Controller::CsvImport::DeliveryOrder';
+
+use SL::Dev::ALL qw(:ALL);
+
+Support::TestSetup::login();
+
+#####
+sub do_import {
+  my ($file, $settings) = @_;
+
+  my $controller = SL::Controller::CsvImport->new(
+    type => 'delivery_orders',
+  );
+  $controller->load_default_profile;
+  $controller->profile->set(
+    charset  => 'utf-8',
+    sep_char => ';',
+    %$settings
+  );
+
+  my $worker = SL::Controller::CsvImport::DeliveryOrder->new(
+    controller => $controller,
+    file       => $file,
+  );
+  $worker->run(test => 0);
+
+  return if $worker->controller->errors;
+
+  # don't try and save objects that have errors
+  $worker->save_objects unless scalar @{$worker->controller->data->[0]->{errors}};
+
+  return $worker->controller->data;
+}
+
+sub clear_up {
+  foreach (qw(RecordLink Order DeliveryOrder Customer Part)) {
+    "SL::DB::Manager::${_}"->delete_all(all => 1);
+  }
+  SL::DB::Manager::Employee->delete_all(where => [ '!login' => 'unittests' ]);
+}
+
+#####
+
+# set numberformat and locale (so we can match errors)
+my $old_numberformat      = $::myconfig{numberformat};
+$::myconfig{numberformat} = '1.000,00';
+my $old_locale            = $::locale;
+$::locale                 = Locale->new('en');
+
+clear_up;
+
+#####
+my @customers;
+my @parts;
+my @orders;
+my $file;
+my $entries;
+my $entry;
+
+# simple import
+@customers = (new_customer(name => 'TestCustomer1', discount => 0)->save);
+@parts = (
+  new_part(description => 'TestPart1', ean => '')->save,
+  new_part(description => 'TestPart2', ean => '')->save
+);
+
+$file = \<<EOL;
+datatype;customer
+datatype;description;qty
+datatype
+DeliveryOrder;TestCustomer1
+OrderItem;TestPart1;5
+OrderItem;TestPart2;10
+EOL
+
+$entries = do_import($file);
+
+$entry = $entries->[0];
+is $entry->{object}->customer_id, $customers[0]->id, 'simple import: customer_id';
+
+$entry = $entries->[1];
+is $entry->{object}->parts_id,    $parts[0]->id,     'simple import: part 1: parts_id';
+is $entry->{object}->qty,         5,                 'simple import: part 1: qty';
+
+$entry = $entries->[2];
+is $entry->{object}->parts_id,    $parts[1]->id,     'simple import: part 2: parts_id';
+is $entry->{object}->qty,         10,                'simple import: part 2: qty';
+
+
+$entries = undef;
+clear_up;
+
+#####
+# with source order
+@customers = (new_customer(name => 'TestCustomer1', discount => 0)->save);
+@parts = (
+  new_part(description => 'TestPart1', ean => '')->save,
+  new_part(description => 'TestPart2', ean => '')->save,
+  new_part(description => 'TestPart3', ean => '')->save
+);
+@orders = (
+  create_sales_order(
+    save       => 1,
+    customer   => $customers[0],
+    ordnumber  => '1234',
+    orderitems => [ create_order_item(part => $parts[0], qty =>  3, sellprice => 70),
+                    create_order_item(part => $parts[1], qty => 10, sellprice => 50),
+                    create_order_item(part => $parts[2], qty =>  8, sellprice => 80),
+                    create_order_item(part => $parts[2], qty => 11, sellprice => 80)
+    ]
+  )
+);
+
+$file = \<<EOL;
+datatype;customer;ordnumber
+datatype;description;qty
+datatype
+DeliveryOrder;TestCustomer1;1234
+OrderItem;TestPart1;5
+OrderItem;TestPart2;10
+OrderItem;TestPart3;7
+OrderItem;TestPart3;1
+OrderItem;TestPart3;11
+EOL
+
+  1;                            # make emacs happy
+
+# should be:
+# delivery oder pos/qty <- order pos/qty
+#       1/5             <-       -
+#       2/10            <-      2/10
+#       3/7             <-      3/7
+#       4/1             <-      3/1
+#       5/11            <-      4/11
+
+$entries = do_import($file);
+$orders[0]->load;               # reload order to get correct delivered status
+
+$entry = $entries->[0];
+is $entry->{object}->ordnumber, '1234', 'with source order: ordnumber';
+
+my $linked = $orders[0]->linked_records(to => 'DeliveryOrder');
+ok(scalar @$linked == 1, 'with source order: order linked to one delivery order');
+ok($linked->[0]->id == $entry->{object}->id, 'with source order: order linked to imported delivery order');
+
+
+$linked = $entry->{object}->linked_records(from => 'Order');
+ok(scalar @$linked == 1, 'with source order: delivery order linked from one order');
+ok($linked->[0]->id == $orders[0]->id, 'with source order: delivery order linked from source order');
+
+$entry = $entries->[1];
+$linked = $entry->{object}->linked_records(from => 'OrderItem');
+ok(scalar @$linked == 0, 'with source order: delivered qty > ordered qty: delivery order item not linked');
+
+$entry = $entries->[2];
+$linked = $entry->{object}->linked_records(from => 'OrderItem');
+ok(scalar @$linked == 1, 'with source order: same qtys: delivery order item linked');
+ok($linked->[0]->id == $orders[0]->items_sorted->[1]->id, 'with source order: same qtys: delivery order item linked from source order item');
+
+$entry = $entries->[3];
+$linked = $entry->{object}->linked_records(from => 'OrderItem');
+ok(scalar @$linked == 1, 'with source order: delivered qty < ordered qty: delivery order item linked (fill up)');
+ok($linked->[0]->position == 3, 'with source order: delivered qty < ordered qty: order position ok (fill up)');
+
+$entry = $entries->[4];
+$linked = $entry->{object}->linked_records(from => 'OrderItem');
+ok(scalar @$linked == 1, 'with source order: delivered qty < ordered qty: delivery order item linked (fill up) 2');
+ok($linked->[0]->position == 3, 'with source order: delivered qty < ordered qty: order position ok (fill up) 2');
+
+$entry = $entries->[5];
+$linked = $entry->{object}->linked_records(from => 'OrderItem');
+ok(scalar @$linked == 1, 'with source order: delivered qty == ordered qty: found exact match after fill up');
+ok($linked->[0]->position == 4, 'with source order: delivered qty == ordered qty: order position ok after fill up');
+
+ok(!$orders[0]->delivered, 'with source order: order not completely delivered');
+
+$entries = undef;
+clear_up;
+
+#####
+@customers = (new_customer(name => 'TestCustomer1', discount => 0)->save);
+@parts = (
+  new_part(description => 'TestPart1', ean => '')->save,
+  new_part(description => 'TestPart2', ean => '')->save,
+  new_part(description => 'TestPart3', ean => '')->save
+);
+@orders = (
+  create_sales_order(
+    save       => 1,
+    customer   => $customers[0],
+    ordnumber  => '1234',
+    orderitems => [ create_order_item(part => $parts[0], qty =>  3, sellprice => 70),
+                    create_order_item(part => $parts[1], qty => 10, sellprice => 50),
+                    create_order_item(part => $parts[2], qty =>  8, sellprice => 80),
+                    create_order_item(part => $parts[2], qty => 11, sellprice => 80)
+    ]
+  )
+);
+
+$file = \<<EOL;
+datatype;customer;ordnumber
+datatype;description;qty
+datatype
+DeliveryOrder;TestCustomer1;1234
+OrderItem;TestPart1;1
+OrderItem;TestPart2;10
+OrderItem;TestPart1;2
+OrderItem;TestPart3;7
+OrderItem;TestPart3;11
+OrderItem;TestPart3;1
+
+EOL
+
+  1;                            # make emacs happy
+
+# should be:
+# delivery oder pos/qty <- order pos/qty
+#       1/1             <-      1/1
+#       2/10            <-      2/10
+#       3/2             <-      1/2
+#       4/7             <-      3/7
+#       5/11            <-      4/11
+#       6/1             <-      3/1
+
+$entries = do_import($file);
+$orders[0]->load;               # reload order to get correct delivered status
+
+$entry = $entries->[1];
+$linked = $entry->{object}->linked_records(from => 'OrderItem');
+ok($linked->[0]->position == 1, 'with source order: mixed qtys and fill up: order position ok 1');
+
+$entry = $entries->[2];
+$linked = $entry->{object}->linked_records(from => 'OrderItem');
+ok($linked->[0]->position == 2, 'with source order: mixed qtys and fill up: order position ok 2');
+
+$entry = $entries->[3];
+$linked = $entry->{object}->linked_records(from => 'OrderItem');
+ok($linked->[0]->position == 1, 'with source order: mixed qtys and fill up: order position ok 3');
+
+$entry = $entries->[4];
+$linked = $entry->{object}->linked_records(from => 'OrderItem');
+ok($linked->[0]->position == 3, 'with source order: mixed qtys and fill up: order position ok 4');
+
+$entry = $entries->[5];
+$linked = $entry->{object}->linked_records(from => 'OrderItem');
+ok($linked->[0]->position == 4, 'with source order: mixed qtys and fill up: order position ok 5');
+
+$entry = $entries->[6];
+$linked = $entry->{object}->linked_records(from => 'OrderItem');
+ok($linked->[0]->position == 3, 'with source order: mixed qtys and fill up: order position ok 6');
+
+ok(!!$orders[0]->delivered, 'with source order: mixed qtys and fill up: order completely delivered');
+
+#####
+$entries = undef;
+clear_up;
+
+#####
+
+$::myconfig{numberformat} = $old_numberformat;
+$::locale                 = $old_locale;
+
+1;
+
+#####
+# vim: ft=perl
+# set emacs to perl mode
+# Local Variables:
+# mode: perl
+# End:
diff --git a/t/controllers/csvimport/inventory.t b/t/controllers/csvimport/inventory.t
new file mode 100644 (file)
index 0000000..1492323
--- /dev/null
@@ -0,0 +1,384 @@
+use strict;
+
+use Data::Dumper; # maybe in Tests available?
+use Test::Deep qw(cmp_deeply superhashof ignore);
+use Test::More;
+use Test::Exception;
+
+use lib 't';
+
+use SL::Dev::Part qw(new_part new_assembly new_service);
+use SL::Dev::Inventory qw(create_warehouse_and_bins set_stock);
+
+use_ok 'Support::TestSetup';
+use_ok 'SL::Controller::CsvImport';
+use_ok 'SL::DB::Bin';
+use_ok 'SL::DB::Part';
+use_ok 'SL::DB::Warehouse';
+use_ok 'SL::DB::Inventory';
+use_ok 'SL::WH';
+use_ok 'SL::Helper::Inventory';
+
+Support::TestSetup::login();
+
+my ($wh, $bin1, $bin2, $assembly1, $assembly_service, $part1, $part2, $wh_moon, $bin_moon, $service1);
+
+sub reset_state {
+  # Create test data
+
+  clear_up();
+  create_standard_stock();
+
+}
+reset_state();
+
+#####
+sub test_import {
+  my ($file,$settings) = @_;
+
+  my $controller = SL::Controller::CsvImport->new(
+    type => 'inventories'
+  );
+  $controller->load_default_profile;
+  $controller->profile->set(
+    charset      => 'utf-8',
+    sep_char     => ',',
+    quote_char   => '"',
+    numberformat => $::myconfig{numberformat},
+  );
+  my $csv_inventory_import = SL::Controller::CsvImport::Inventory->new(
+    settings   => $settings,
+    controller => $controller,
+    file       => $file,
+  );
+  #print "profile param type=".$csv_part_import->settings->{parts_type}."\n";
+
+  $csv_inventory_import->run(test => 0);
+
+  # don't try and save objects that have errors
+  $csv_inventory_import->save_objects unless scalar @{$csv_inventory_import->controller->data->[0]->{errors}};
+
+  return $csv_inventory_import->controller->data;
+}
+
+$::myconfig{numberformat} = '1000.00';
+my $old_locale = $::locale;
+# set locale to en so we can match errors
+$::locale = Locale->new('en');
+
+
+my ($entries, $entry, $file);
+
+# different settings for tests
+#
+
+my $settings1 = {
+                  apply_comment => 'missing',
+                  comment       => 'Lager Inventur Standard',
+                };
+#
+#
+# starting test of csv imports
+# to debug errors in certain tests, run after test_import:
+#   die Dumper($entry->{errors});
+
+
+##### create complete bullshit
+$file = \<<EOL;
+bin,chargenumber,comment,employee_id,partnumber,qty,shippingdate,target_qty,warehouse
+P1000;100.10;90.20;95.30;kg;111.11;122.22;133.33
+EOL
+$entries = test_import($file, $settings1);
+$entry = $entries->[0];
+is scalar @{ $entry->{errors} }, 3, "Three errors occurred";
+
+cmp_deeply(\@{ $entry->{errors} }, [
+                                    'Error: Warehouse not found',
+                                    'Error: Bin not found',
+                                    'Error: Part not found'
+                                   ],
+          "Errors for bullshit import are ok"
+);
+
+##### create minor bullshit
+$file = \<<EOL;
+warehouse,bin,partnumber,qty,chargenumber,comment,employee_id,qty,shippingdate,target_qty
+Warehouse,"Bin 1","ap 1",3.4
+EOL
+
+$entries = test_import($file, $settings1);
+$entry = $entries->[0];
+is scalar @{ $entry->{errors} }, 1, "One error for minor bullshit occurred";
+
+cmp_deeply(\@{ $entry->{errors} }, [
+                                    'Error: A quantity and a target quantity could not be given both.'
+                                   ],
+          "Error for minor bullshit import are ok"
+);
+
+
+##### add some qty on earth, but we have something already stocked
+set_stock(
+  part => $part1,
+  qty => 25,
+  bin => $bin1,
+);
+
+is(SL::Helper::Inventory::get_stock(part => $part1), "25.00000", 'simple get_stock works');
+is(SL::Helper::Inventory::get_onhand(part => $part1), "25.00000", 'simple get_onhand works');
+
+my ($trans_id, $inv_obj, $tt);
+# add some stuff
+
+$file = \<<EOL;
+warehouse,bin,partnumber,qty,chargenumber,comment,employee_id,shippingdate
+Warehouse,"Bin 1","ap 1",3.4
+EOL
+$entries = test_import($file, $settings1);
+$entry = $entries->[0];
+is scalar @{ $entry->{errors} }, 0, "No error for valid data occurred";
+is $entry->{object}->qty, "3.4", "Valid qty accepted";  # evals to text
+is(SL::Helper::Inventory::get_stock(part => $part1),  "28.40000",  'simple add (stock) qty works');
+is(SL::Helper::Inventory::get_onhand(part => $part1), "28.40000", 'simple add (onhand) qty works');
+
+# now check the real Inventory entry
+$trans_id = $entry->{object}->trans_id;
+$inv_obj = SL::DB::Manager::Inventory->find_by(trans_id => $trans_id);
+
+# we expect one entry for one trans_id
+is ref $inv_obj, "SL::DB::Inventory",             "One inventory object, no array or undef";
+is $inv_obj->qty == 3.4, 1,                       "Valid qty accepted";  # evals to text
+is $inv_obj->comment, 'Lager Inventur Standard',  "Valid comment accepted";  # evals to text
+is $inv_obj->employee_id, 1,                      "Employee valid";  # evals to text
+is ref $inv_obj->shippingdate, 'DateTime',        "Valid DateTime for shippingdate";
+is $inv_obj->shippingdate, DateTime->today_local, "Default shippingdate set";
+
+$tt = SL::DB::Manager::TransferType->find_by(id => $inv_obj->trans_type_id);
+
+is ref $tt, 'SL::DB::TransferType',       "Valid TransferType, no undef";
+is $tt->direction, 'in',                  "Transfer direction correct";
+is $tt->description, 'correction',        "Transfer description correct";
+
+# remove some stuff
+
+$file = \<<EOL;
+warehouse,bin,partnumber,qty,chargenumber,comment,employee_id,shippingdate
+Warehouse,"Bin 1","ap 1",-13.4
+EOL
+$entries = test_import($file, $settings1);
+$entry = $entries->[0];
+is scalar @{ $entry->{errors} }, 0, "No error for valid data occurred";
+is $entry->{object}->qty, "-13.4", "Valid qty accepted";  # evals to text
+is(SL::Helper::Inventory::get_stock(part => $part1),  "15.00000",  'simple add (stock) qty works');
+is(SL::Helper::Inventory::get_onhand(part => $part1), "15.00000", 'simple add (onhand) qty works');
+
+# now check the real Inventory entry
+$trans_id = $entry->{object}->trans_id;
+$inv_obj = SL::DB::Manager::Inventory->find_by(trans_id => $trans_id);
+
+# we expect one entry for one trans_id
+is ref $inv_obj, "SL::DB::Inventory",             "One inventory object, no array or undef";
+is $inv_obj->qty == -13.4, 1,                       "Valid qty accepted";  # evals to text
+is $inv_obj->comment, 'Lager Inventur Standard',  "Valid comment accepted";  # evals to text
+is $inv_obj->employee_id, 1,                      "Employee valid";  # evals to text
+is ref $inv_obj->shippingdate, 'DateTime',        "Valid DateTime for shippingdate";
+is $inv_obj->shippingdate, DateTime->today_local, "Default shippingdate set";
+
+$tt = SL::DB::Manager::TransferType->find_by(id => $inv_obj->trans_type_id);
+
+is ref $tt, 'SL::DB::TransferType',       "Valid TransferType, no undef";
+is $tt->direction, 'out',                  "Transfer direction correct";
+is $tt->description, 'correction',        "Transfer description correct";
+
+# repeat both test cases but with target qty instead of qty (should throw an error for neg. case)
+# and customise comment
+# add some stuff
+
+$file = \<<EOL;
+warehouse,bin,partnumber,target_qty,comment
+Warehouse,"Bin 1","ap 1",3.4,"Alter, wir haben uns voll verhauen bei der aktuellen Zielmenge!"
+EOL
+$entries = test_import($file, $settings1);
+$entry = $entries->[0];
+is scalar @{ $entry->{errors} }, 0, "No error for valid data occurred";
+is $entry->{object}->qty, "-11.6", "Valid qty accepted";  # evals to text qty = target_qty - actual_qty
+is(SL::Helper::Inventory::get_stock(part => $part1),  "3.40000",  'simple add (stock) qty works');
+is(SL::Helper::Inventory::get_onhand(part => $part1), "3.40000", 'simple add (onhand) qty works');
+
+# now check the real Inventory entry
+$trans_id = $entry->{object}->trans_id;
+$inv_obj = SL::DB::Manager::Inventory->find_by(trans_id => $trans_id);
+
+# we expect one entry for one trans_id
+is ref $inv_obj, "SL::DB::Inventory",             "One inventory object, no array or undef";
+is $inv_obj->qty == -11.6, 1,                       "Valid qty accepted";
+is $inv_obj->comment,
+  "Alter, wir haben uns voll verhauen bei der aktuellen Zielmenge!",  "Valid comment accepted";
+is $inv_obj->employee_id, 1,                      "Employee valid";
+is ref $inv_obj->shippingdate, 'DateTime',        "Valid DateTime for shippingdate";
+is $inv_obj->shippingdate, DateTime->today_local, "Default shippingdate set";
+
+$tt = SL::DB::Manager::TransferType->find_by(id => $inv_obj->trans_type_id);
+
+is ref $tt, 'SL::DB::TransferType',       "Valid TransferType, no undef";
+is $tt->direction, 'out',                  "Transfer direction correct";
+is $tt->description, 'correction',        "Transfer description correct";
+
+# remove some stuff, but too much
+
+$file = \<<EOL;
+warehouse,bin,partnumber,target_qty,comment
+Warehouse,"Bin 1","ap 1",-13.4,"Jetzt stimmt aber alles"
+EOL
+$entries = test_import($file, $settings1);
+$entry = $entries->[0];
+is scalar @{ $entry->{errors} }, 1, "One error for invalid data occurred";
+is $entry->{object}->qty, undef, "No data accepted";  # evals to text
+is(SL::Helper::Inventory::get_stock(part => $part1),  "3.40000",  'simple add (stock) qty works');
+is(SL::Helper::Inventory::get_onhand(part => $part1), "3.40000", 'simple add (onhand) qty works');
+
+# now check the real Inventory entry
+$trans_id = $entry->{object}->trans_id;
+$inv_obj = SL::DB::Manager::Inventory->find_by(trans_id => $trans_id);
+
+is ref $trans_id, '',         "No trans_id -> undef";
+is ref $inv_obj,  '',         "No inventory object -> undef";
+
+# add some stuff, but realistic value
+
+$file = \<<EOL;
+warehouse,bin,partnumber,target_qty,comment
+Warehouse,"Bin 1","ap 1",33.75,"Jetzt wirklich"
+EOL
+$entries = test_import($file, $settings1);
+$entry = $entries->[0];
+is scalar @{ $entry->{errors} }, 0, "No error for valid data occurred";
+is $entry->{object}->qty, "30.35", "Valid qty accepted";  # evals to text qty = target_qty - actual_qty
+is(SL::Helper::Inventory::get_stock(part => $part1),  "33.75000",  'simple add (stock) qty works');
+is(SL::Helper::Inventory::get_onhand(part => $part1), "33.75000", 'simple add (onhand) qty works');
+
+# now check the real Inventory entry
+$trans_id = $entry->{object}->trans_id;
+$inv_obj = SL::DB::Manager::Inventory->find_by(trans_id => $trans_id);
+
+# we expect one entry for one trans_id
+is ref $inv_obj, "SL::DB::Inventory",             "One inventory object, no array or undef";
+is $inv_obj->qty == 30.35, 1,                     "Valid qty accepted";
+is $inv_obj->comment, "Jetzt wirklich",           "Valid comment accepted";
+is $inv_obj->employee_id, 1,                      "Employee valid";
+is ref $inv_obj->shippingdate, 'DateTime',        "Valid DateTime for shippingdate";
+is $inv_obj->shippingdate, DateTime->today_local, "Default shippingdate set";
+
+$tt = SL::DB::Manager::TransferType->find_by(id => $inv_obj->trans_type_id);
+
+is ref $tt, 'SL::DB::TransferType',       "Valid TransferType, no undef";
+is $tt->direction, 'in',                  "Transfer direction correct";
+is $tt->description, 'correction',        "Transfer description correct";
+
+# target_qty is 0
+
+$file = \<<EOL;
+warehouse,bin,partnumber,target_qty,comment
+Warehouse,"Bin 1","ap 1",0,"Jetzt wirklich"
+EOL
+$entries = test_import($file, $settings1);
+$entry = $entries->[0];
+is scalar @{ $entry->{errors} }, 0, "No error for valid data occurred";
+is $entry->{object}->qty, "-33.75", "Valid qty accepted";  # evals to text qty = target_qty - actual_qty
+is(SL::Helper::Inventory::get_stock(part => $part1),  "0.00000",  'simple add (stock) qty works');
+is(SL::Helper::Inventory::get_onhand(part => $part1), undef, 'simple add (onhand) qty works'); # hmm good return?
+
+# now check the real Inventory entry
+$trans_id = $entry->{object}->trans_id;
+$inv_obj = SL::DB::Manager::Inventory->find_by(trans_id => $trans_id);
+
+# we expect one entry for one trans_id
+is ref $inv_obj, "SL::DB::Inventory",             "One inventory object, no array or undef";
+is $inv_obj->qty == -33.75000, 1,                       "Valid qty accepted";
+is $inv_obj->comment,
+  "Jetzt wirklich",  "Valid comment accepted";
+is $inv_obj->employee_id, 1,                      "Employee valid";
+is ref $inv_obj->shippingdate, 'DateTime',        "Valid DateTime for shippingdate";
+is $inv_obj->shippingdate, DateTime->today_local, "Default shippingdate set";
+
+$tt = SL::DB::Manager::TransferType->find_by(id => $inv_obj->trans_type_id);
+
+is ref $tt, 'SL::DB::TransferType',       "Valid TransferType, no undef";
+is $tt->direction, 'out',                  "Transfer direction correct";
+is $tt->description, 'correction',        "Transfer description correct";
+
+# add some stuff with a different numberformat
+
+$::myconfig{numberformat} = '1.000,00';
+$file = \<<EOL;
+warehouse,bin,partnumber,target_qty,comment
+Warehouse,"Bin 1","ap 1","31,2","Jetzt wirklich"
+EOL
+$entries = test_import($file, $settings1);
+$entry = $entries->[0];
+is scalar @{ $entry->{errors} }, 0, "No error for valid data occurred";
+is $entry->{object}->qty, "31.2",  "Valid qty accepted";  # evals to text qty = target_qty - actual_qty
+is(SL::Helper::Inventory::get_stock(part => $part1),  "31.20000",  'simple add (stock) qty works');
+is(SL::Helper::Inventory::get_onhand(part => $part1), "31.20000", 'simple add (onhand) qty works');
+
+# now check the real Inventory entry
+$trans_id = $entry->{object}->trans_id;
+$inv_obj = SL::DB::Manager::Inventory->find_by(trans_id => $trans_id);
+
+# we expect one entry for one trans_id
+is ref $inv_obj, "SL::DB::Inventory",             "One inventory object, no array or undef";
+is $inv_obj->qty == 31.2, 1,                     "Valid qty calculated";
+
+
+
+clear_up(); # remove all data at end of tests
+
+# end of tests
+
+done_testing();
+
+sub clear_up {
+  SL::DB::Manager::Inventory->delete_all(all => 1);
+  SL::DB::Manager::Assembly->delete_all(all => 1);
+  SL::DB::Manager::Part->delete_all(all => 1);
+  SL::DB::Manager::Bin->delete_all(all => 1);
+  SL::DB::Manager::Warehouse->delete_all(all => 1);
+}
+
+sub create_standard_stock {
+  ($wh, $bin1)          = create_warehouse_and_bins();
+  ($wh_moon, $bin_moon) = create_warehouse_and_bins(
+      warehouse_description => 'Our warehouse location at the moon',
+      bin_description       => 'Lunar crater',
+    );
+  $bin2 = SL::DB::Bin->new(description => "Bin 2", warehouse => $wh)->save;
+  $wh->load;
+
+  $assembly1  =  new_assembly(number_of_parts => 2)->save;
+  ($part1, $part2) = map { $_->part } $assembly1->assemblies;
+
+  $service1 = new_service(partnumber  => "service number 1",
+                          description => "We really need this service",
+                         )->save;
+  my $assembly_items;
+  push( @{$assembly_items}, SL::DB::Assembly->new(parts_id => $part1->id,
+                                                  qty      => 12,
+                                                  position => 1,
+                                                  ));
+  push( @{$assembly_items}, SL::DB::Assembly->new(parts_id => $part2->id,
+                                                  qty      => 6.34,
+                                                  position => 2,
+                                                  ));
+  push( @{$assembly_items}, SL::DB::Assembly->new(parts_id => $service1->id,
+                                                  qty      => 1.2,
+                                                  position => 3,
+                                                  ));
+  $assembly_service  =  new_assembly(description    => 'Ein Erzeugnis mit Dienstleistungen',
+                                     assembly_items => $assembly_items
+                                    )->save;
+}
+
+
+
+
+1;
diff --git a/t/controllers/csvimport/parts.t b/t/controllers/csvimport/parts.t
new file mode 100644 (file)
index 0000000..86a3c78
--- /dev/null
@@ -0,0 +1,331 @@
+use Test::More tests => 47;
+
+use strict;
+
+use lib 't';
+
+use Carp;
+use Data::Dumper;
+use Support::TestSetup;
+use Test::Exception;
+
+use List::MoreUtils qw(pairwise);
+use SL::Controller::CsvImport;
+
+my $DEBUG = 0;
+
+use_ok 'SL::Controller::CsvImport::Part';
+
+use SL::DB::Buchungsgruppe;
+use SL::DB::Currency;
+use SL::DB::Customer;
+use SL::DB::Language;
+use SL::DB::Warehouse;
+use SL::DB::Pricegroup;
+use SL::DB::Price;
+use SL::DB::Bin;
+
+my ($translation, $bin1_1, $bin1_2, $bin2_1, $bin2_2, $wh1, $wh2, $bugru, $cvarconfig );
+my ($pg1_id, $pg2_id, $pg3_id);
+
+Support::TestSetup::login();
+
+sub reset_state {
+  # Create test data
+
+  clear_up();
+
+  $translation     = SL::DB::Language->new(
+    description    => 'Englisch',
+    article_code   => 'EN',
+    template_code  => 'EN',
+  )->save;
+  $translation     = SL::DB::Language->new(
+    description    => 'Italienisch',
+    article_code   => 'IT',
+    template_code  => 'IT',
+  )->save;
+  $wh1 = SL::DB::Warehouse->new(
+    description    => 'Lager1',
+    sortkey        => 1,
+  )->save;
+  $bin1_1 = SL::DB::Bin->new(
+    description    => 'Ort1_von_Lager1',
+    warehouse_id   => $wh1->id,
+  )->save;
+  $bin1_2 = SL::DB::Bin->new(
+    description    => 'Ort2_von_Lager1',
+    warehouse_id   => $wh1->id,
+  )->save;
+  $wh2 = SL::DB::Warehouse->new(
+    description    => 'Lager2',
+    sortkey        => 2,
+  )->save;
+  $bin2_1 = SL::DB::Bin->new(
+    description    => 'Ort1_von_Lager2',
+    warehouse_id   => $wh2->id,
+  )->save;
+  $bin2_2 = SL::DB::Bin->new(
+    description    => 'Ort2_von_Lager2',
+    warehouse_id   => $wh2->id,
+  )->save;
+
+  $cvarconfig = SL::DB::CustomVariableConfig->new(
+    module   => 'IC',
+    name     => 'mycvar',
+    type     => 'text',
+    description => 'mein Schatz',
+    searchable  => 1,
+    sortkey => 1,
+    includeable => 0,
+    included_by_default => 0,
+  )->save;
+
+  foreach ( { id => 1, pricegroup => 'A', sortkey => 1 },
+            { id => 2, pricegroup => 'B', sortkey => 2 },
+            { id => 3, pricegroup => 'C', sortkey => 3 },
+            { id => 4, pricegroup => 'D', sortkey => 4 } ) {
+    SL::DB::Pricegroup->new(%{$_})->save;
+  }
+}
+
+$bugru = SL::DB::Manager::Buchungsgruppe->find_by(description => { like => 'Standard%19%' });
+
+reset_state();
+
+#####
+sub test_import {
+  my ($file,$settings) = @_;
+
+  my $controller = SL::Controller::CsvImport->new(
+    type => 'parts'
+  );
+  $controller->load_default_profile;
+  $controller->profile->set(
+    charset      => 'utf-8',
+    sep_char     => ';',
+    quote_char   => '"',
+    numberformat => $::myconfig{numberformat},
+  );
+
+  my $csv_part_import = SL::Controller::CsvImport::Part->new(
+    settings   => $settings,
+    controller => $controller,
+    file       => $file,
+  );
+  #print "profile param type=".$csv_part_import->settings->{parts_type}."\n";
+
+  $csv_part_import->run(test => 0);
+
+  # don't try and save objects that have errors
+  $csv_part_import->save_objects unless scalar @{$csv_part_import->controller->data->[0]->{errors}};
+
+  return $csv_part_import->controller->data;
+}
+
+$::myconfig{numberformat} = '1000.00';
+my $old_locale = $::locale;
+# set locale to en so we can match errors
+$::locale = Locale->new('en');
+
+
+my ($entries, $entry, $file);
+
+# different settings for tests
+#
+
+my $settings1 = {
+                       sellprice_places          => 2,
+                       sellprice_adjustment      => 0,
+                       sellprice_adjustment_type => 'percent',
+                       article_number_policy     => 'update_prices',
+                       shoparticle_if_missing    => '0',
+                       part_type                 => 'part',
+                       part_classification       => 3,
+                       default_buchungsgruppe    => ($bugru ? $bugru->id : undef),
+                       apply_buchungsgruppe      => 'all',
+                };
+my $settings2 = {
+                       sellprice_places          => 2,
+                       sellprice_adjustment      => 0,
+                       sellprice_adjustment_type => 'percent',
+                       article_number_policy     => 'update_parts',
+                       shoparticle_if_missing    => '0',
+                       part_type                 => 'mixed',
+                       part_classification       => 4,
+                       default_buchungsgruppe    => ($bugru ? $bugru->id : undef),
+                       apply_buchungsgruppe      => 'missing',
+                       default_unit              => 'Stck',
+                };
+
+#
+#
+# starting test of csv imports
+# to debug errors in certain tests, run after test_import:
+#   die Dumper($entry->{errors});
+
+
+##### create part with prices and 3 pricegroup prices
+$file = \<<EOL;
+partnumber;sellprice;lastcost;listprice;unit;pricegroup_1;pricegroup_2;pricegroup_3
+P1000;100.10;90.20;95.30;kg;111.11;122.22;133.33
+EOL
+$entries = test_import($file,$settings1);
+$entry = $entries->[0];
+#foreach my $err ( @{ $entry->{errors} } ) {
+#  print $err;
+#}
+is $entry->{object}->partnumber,'P1000', 'partnumber';
+is $entry->{object}->sellprice, '100.1', 'sellprice';
+is $entry->{object}->lastcost,   '90.2', 'lastcost';
+is $entry->{object}->listprice,  '95.3', 'listprice';
+is $entry->{object}->find_prices( { pricegroup_id => 2 } )->[0]->price,  '122.22000', 'pricegroup_2 price';
+
+##### update prices of part, and price of pricegroup_2, keeping pricegroup_1 and pricegroup_3
+$file = \<<EOL;
+partnumber;sellprice;lastcost;listprice;unit;pricegroup_2;pricegroup_4
+P1000;110.10;95.20;97.30;kg;123.45;144.44
+EOL
+$entries = test_import($file,$settings1);
+$entry = $entries->[0];
+is $entry->{object}->sellprice, '110.1', 'updated sellprice';
+is $entry->{object}->lastcost,   '95.2', 'updated lastcost';
+is $entry->{object}->listprice,  '97.3', 'updated listprice';
+# $entry->{object}->prices currently only contains prices pricegroup_2 and pricegroup_4, reload object from db
+# printf("%s %s: %s\n", $_->pricegroup_id, $_->pricegroup->pricegroup, $_->price) foreach @{$entry->{object}->prices};
+$entry->{object}->load;
+is $entry->{object}->find_prices( { pricegroup_id => 1 } )->[0]->price,  '111.11000', 'pricegroup_1 price didn\'t change';
+is $entry->{object}->find_prices( { pricegroup_id => 2 } )->[0]->price,  '123.45000', 'pricegroup_2 price was updated';
+is $entry->{object}->find_prices( { pricegroup_id => 4 } )->[0]->price,  '144.44000', 'pricegroup_4 price was added';
+
+##### insert parts with warehouse,bin name
+
+$file = \<<EOL;
+partnumber;description;warehouse;bin;part_type
+P1000;Teil 1000;Lager1;Ort1_von_Lager1;part
+P1001;Teil 1001;Lager1;Ort2_von_Lager1;service
+P1002;Teil 1002;Lager2;Ort1_von_Lager2;service
+P1003;Teil 1003;Lager2;Ort2_von_Lager2;part
+EOL
+$entries = test_import($file,$settings2);
+$entry = $entries->[0];
+is $entry->{object}->description, 'Teil 1000', 'Teil 1000 set';
+is $entry->{object}->warehouse_id, $wh1->id, 'Lager1';
+is $entry->{object}->bin_id, $bin1_1->id, 'Lagerort1';
+is $entry->{object}->part_type, 'part', 'Typ ist part';
+$entry = $entries->[2];
+is $entry->{object}->description, 'Teil 1002', 'Teil 1002 set';
+is $entry->{object}->warehouse_id, $wh2->id, 'Lager2';
+is $entry->{object}->bin_id, $bin2_1->id, 'Lagerort1';
+is $entry->{object}->part_type, 'service', 'Typ ist service';
+
+##### update warehouse and bin
+$file = \<<EOL;
+partnumber;description;warehouse;bin;part_type
+P1000;Teil 1000;Lager2;Ort1_von_Lager2;part
+P1001;Teil 1001;Lager1;Ort1_von_Lager1;part
+P1002;Teil 1002;Lager2;Ort1_von_Lager1;part
+P1003;Teil 1003;Lager2;kein Lagerort;part
+EOL
+$entries = test_import($file,$settings2);
+$entry = $entries->[0];
+is $entry->{object}->description, 'Teil 1000', 'Teil 1000 set';
+is $entry->{object}->warehouse_id, $wh2->id, 'Lager2';
+is $entry->{object}->bin_id, $bin2_1->id, 'Lagerort1';
+$entry = $entries->[2];
+my $err1 = @{ $entry->{errors} }[0];
+#print "'".$err1."'\n";
+is $entry->{object}->description, 'Teil 1002', 'Teil 1002 set';
+is $entry->{object}->warehouse_id, $wh2->id, 'Lager2';
+is $err1, 'Error: Bin Ort1_von_Lager1 is not from warehouse Lager2','kein Lager von Lager2';
+$entry = $entries->[3];
+$err1 = @{ $entry->{errors} }[0];
+#print "'".$err1."'\n";
+is $entry->{object}->description, 'Teil 1003', 'Teil 1003 set';
+is $entry->{object}->warehouse_id, $wh2->id, 'Lager2';
+is $err1, 'Error: Invalid bin name kein Lagerort','kein Lagerort';
+
+##### add translations
+$file = \<<EOL;
+partnumber;description;description_EN;notes_EN;description_IT;notes_IT
+P1000;Teil 1000;descr EN 1000;notes EN;descr IT 1000;notes IT
+P1001;Teil 1001;descr EN 1001;notes EN;descr IT 1001;notes IT
+P1002;Teil 1002;descr EN 1002;notes EN;descr IT 1002;notes IT
+P1003;Teil 1003;descr EN 1003;notes EN;descr IT 1003;notes IT
+EOL
+$entries = test_import($file,$settings2);
+$entry = $entries->[0];
+is $entry->{object}->description, 'Teil 1000', 'Teil 1000 set';
+is $entry->{raw_data}->{description_EN},'descr EN 1000','EN set';
+is $entry->{raw_data}->{description_IT},'descr IT 1000','IT set';
+my $l = @{$entry->{object}->translations}[0];
+is $l->translation,'descr EN 1000','EN trans set';
+is $l->longdescription, 'notes EN','EN notes set';
+$l = @{$entry->{object}->translations}[1];
+is $l->translation,'descr IT 1000','IT trans set';
+is $l->longdescription, 'notes IT','IT notes set';
+
+##### add customvar
+$file = \<<EOL;
+partnumber;cvar_mycvar
+P1000;das ist der Ring
+P1001;nicht der Nibelungen
+P1002;sondern vom
+P1003;Herr der Ringe
+EOL
+$entries = test_import($file,$settings2);
+$entry = $entries->[0];
+is $entry->{object}->partnumber, 'P1000', 'P1000 set';
+is $entry->{raw_data}->{cvar_mycvar},'das ist der Ring','CVAR set';
+is @{$entry->{object}->custom_variables}[0]->text_value,'das ist der Ring','Cvar mit richtigem Wert';
+
+# set locale to de so we can match abbreviations
+$::locale = $old_locale;
+##### import part classification
+$file = \<<EOL;
+partnumber;pclass;description
+W1000;WE;Teil 1000
+W1001;WV;Teil 1001
+D1002;DV;Dienstleistung 1002
+D1003;DH;Dienstleistung 1003
+EOL
+$entries = test_import($file,$settings2);
+$entry = $entries->[0];
+is $entry->{object}->classification_id, '1', 'W1000 von Klasse Einkauf';
+is $entry->{object}->type, 'part', 'W1000 vom Type part';
+$entry = $entries->[1];
+is $entry->{object}->classification_id, '2', 'W1001 von Klasse Verkauf';
+is $entry->{object}->type, 'part', 'W1001 vom Type part';
+$entry = $entries->[2];
+is $entry->{object}->classification_id, '2', 'D1002 von Klasse Verkauf';
+is $entry->{object}->type, 'service', 'D1002 vom Type service';
+$entry = $entries->[3];
+is $entry->{object}->classification_id, '3', 'D1003 von Klasse Handelsware';
+is $entry->{object}->type, 'service', 'D1003 vom Type service';
+
+
+clear_up(); # remove all data at end of tests
+
+# end of tests
+
+
+sub clear_up {
+  SL::DB::Manager::Part       ->delete_all(all => 1);
+  SL::DB::Manager::Pricegroup ->delete_all(all => 1);
+  SL::DB::Manager::Price      ->delete_all(all => 1);
+  SL::DB::Manager::Translation->delete_all(all => 1);
+  SL::DB::Manager::Language   ->delete_all(all => 1);
+  SL::DB::Manager::Bin        ->delete_all(all => 1);
+  SL::DB::Manager::Warehouse  ->delete_all(all => 1);
+  SL::DB::Manager::CustomVariableConfig->delete_all(all => 1);
+}
+
+
+1;
+
+#####
+# vim: ft=perl
+# set emacs to perl mode
+# Local Variables:
+# mode: perl
+# End:
index 1dab1e8..5414e90 100644 (file)
@@ -22,6 +22,7 @@ use utf8;
 
 use Carp;
 use Support::TestSetup;
+use SL::Dev::ALL qw(:ALL);
 
 use_ok 'SL::BackgroundJob::CreatePeriodicInvoices';
 use_ok 'SL::Controller::FinancialControllingReport';
@@ -35,59 +36,51 @@ use_ok 'SL::DB::TaxZone';
 
 Support::TestSetup::login();
 
-our ($ar_chart, $buchungsgruppe, $ctrl, $currency_id, $customer, $employee, $order, $part, $tax_zone, $unit, @invoices);
+our ($ar_chart, $ctrl, $customer, $order, $part, $unit, @invoices);
+
+sub cleanup {
+  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(InvoiceItem Invoice OrderItem Order Customer Part);
+}
 
 sub init_common_state {
-  $ar_chart       = SL::DB::Manager::Chart->find_by(accno => '1400')                        || croak "No AR chart";
-  $buchungsgruppe = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%') || croak "No accounting group";
-  $currency_id    = SL::DB::Default->get->currency_id;
-  $employee       = SL::DB::Manager::Employee->current                                      || croak "No employee";
-  $tax_zone       = SL::DB::Manager::TaxZone->find_by( description => 'Inland')             || croak "No taxzone";
-  $unit           = SL::DB::Manager::Unit->find_by(name => 'psch')                          || croak "No unit";
+  $ar_chart       = SL::DB::Manager::Chart->find_by(accno => '1400') || croak "No AR chart";
+  $unit           = SL::DB::Manager::Unit->find_by(name => 'psch')   || croak "No unit";
 }
 
-sub create_sales_order {
+sub make_sales_order {
   my %params = @_;
 
-  $params{$_} ||= {} for qw(customer part tax order orderitem);
+  cleanup();
 
-  # Clean up: remove invoices, orders, parts and customers
-  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(InvoiceItem Invoice OrderItem Order Customer Part);
+  $params{$_} ||= {} for qw(customer part order orderitem);
 
-  $customer     = SL::DB::Customer->new(
+  $customer     = new_customer(
     name        => 'Test Customer',
-    currency_id => $currency_id,
-    taxzone_id  => $tax_zone->id,
     %{ $params{customer} }
   )->save;
 
-  $part = SL::DB::Part->new(
+  $part = new_part(
     partnumber         => 'T4254',
     description        => 'Fourty-two fifty-four',
     lastcost           => 222.22,
     sellprice          => 333.33,
-    buchungsgruppen_id => $buchungsgruppe->id,
     unit               => $unit->name,
     %{ $params{part} }
   )->save;
   $part->load;
 
-  $order                     = SL::DB::Order->new(
-    customer_id              => $customer->id,
-    currency_id              => $currency_id,
-    taxzone_id               => $tax_zone->id,
+  $order                     = create_sales_order(
+    save                     => 1,
+    customer                 => $customer,
     transaction_description  => '<%period_start_date%>',
     transdate                => DateTime->from_kivitendo('01.03.2014'),
     orderitems               => [
-      { parts_id             => $part->id,
-        description          => $part->description,
-        lastcost             => $part->lastcost,
-        sellprice            => $part->sellprice,
-        qty                  => 1,
-        unit                 => $unit->name,
-        %{ $params{orderitem} },
-      },
-    ],
+                                  create_order_item(
+                                    part => $part,
+                                    qty  => 1,
+                                    %{ $params{orderitem} },
+                                  ),
+                                ],
     periodic_invoices_config => $params{periodic_invoices_config} ? {
       active                 => 1,
       ar_chart_id            => $ar_chart->id,
@@ -96,11 +89,7 @@ sub create_sales_order {
     %{ $params{order} },
   );
 
-  $order->calculate_prices_and_taxes;
-
-  ok($order->save(cascade => 1));
-
-  $::form = Form->new('');
+  $::form = Support::TestSetup->create_new_form;
   $ctrl   = SL::Controller::FinancialControllingReport->new;
 
   $ctrl->orders($ctrl->models->get);
@@ -114,7 +103,7 @@ my @columns = qw(net_amount         other_amount
 sub run_tests {
   my ($msg, $num_orders, $values, %order_params) = @_;
 
-  create_sales_order(%order_params);
+  make_sales_order(%order_params);
 
   is($num_orders, scalar @{ $ctrl->orders }, "${msg}, #orders");
   is_deeply([ map { ($ctrl->orders->[0]->{$_} // 0) * 1 } @columns ],
@@ -544,5 +533,6 @@ run_tests(
     end_date                => DateTime->from_kivitendo('30.04.2014'),
   });
 
+cleanup();
 
 done_testing();
index 8994fad..4c613ee 100644 (file)
@@ -14,7 +14,7 @@ sub today_local {
 
 package main;
 
-use Test::More tests => 49;
+use Test::More tests => 43;
 
 use lib 't';
 use strict;
@@ -22,6 +22,9 @@ use utf8;
 
 use Carp;
 use Support::TestSetup;
+use SL::Dev::Record qw(create_sales_order create_order_item);
+use SL::Dev::CustomerVendor qw(new_customer);
+use SL::Dev::Part qw(new_part);
 
 use_ok 'SL::BackgroundJob::CreatePeriodicInvoices';
 use_ok 'SL::Controller::FinancialOverview';
@@ -31,67 +34,48 @@ use_ok 'SL::DB::Default';
 use_ok 'SL::DB::Invoice';
 use_ok 'SL::DB::Order';
 use_ok 'SL::DB::Part';
-use_ok 'SL::DB::TaxZone';
 
 Support::TestSetup::login();
 
-our ($ar_chart, $buchungsgruppe, $ctrl, $currency_id, $customer, $employee, $order, $part, $tax_zone, $unit, @invoices);
+our ($ar_chart, $ctrl, $customer, $order, $part, $unit, @invoices);
 
 sub clear_up {
   "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(InvoiceItem Invoice OrderItem Order Customer Part);
 };
 
 sub init_common_state {
-  $ar_chart       = SL::DB::Manager::Chart->find_by(accno => '1400')                        || croak "No AR chart";
-  $buchungsgruppe = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%') || croak "No accounting group";
-  $currency_id    = SL::DB::Default->get->currency_id;
-  $employee       = SL::DB::Manager::Employee->current                                      || croak "No employee";
-  $tax_zone       = SL::DB::Manager::TaxZone->find_by( description => 'Inland')             || croak "No taxzone";
-  $unit           = SL::DB::Manager::Unit->find_by(name => 'psch')                          || croak "No unit";
+  $ar_chart       = SL::DB::Manager::Chart->find_by(accno => '1400') || croak "No AR chart";
+  $unit           = SL::DB::Manager::Unit->find_by(name => 'psch')   || croak "No unit";
 }
 
-sub create_sales_order {
+sub make_sales_order {
   my %params = @_;
 
-  $params{$_} ||= {} for qw(customer part tax order orderitem);
+  $params{$_} ||= {} for qw(customer part order orderitem);
 
   # Clean up: remove invoices, orders, parts and customers
   clear_up();
 
-  $customer     = SL::DB::Customer->new(
+  $customer     = new_customer(
     name        => 'Test Customer',
-    currency_id => $currency_id,
-    taxzone_id  => $tax_zone->id,
     %{ $params{customer} }
   )->save;
 
-  $part = SL::DB::Part->new(
+  $part = new_part(
     partnumber         => 'T4254',
     description        => 'Fourty-two fifty-four',
     lastcost           => 222.22,
     sellprice          => 333.33,
-    buchungsgruppen_id => $buchungsgruppe->id,
-    unit               => $unit->name,
     %{ $params{part} }
   )->save;
   $part->load;
 
-  $order                     = SL::DB::Order->new(
-    customer_id              => $customer->id,
-    currency_id              => $currency_id,
-    taxzone_id               => $tax_zone->id,
+  $order                     = create_sales_order(
+    save                     => 1,
+    customer                 => $customer,
     transaction_description  => '<%period_start_date%>',
     transdate                => DateTime->from_kivitendo('01.03.2014'),
-    orderitems               => [
-      { parts_id             => $part->id,
-        description          => $part->description,
-        lastcost             => $part->lastcost,
-        sellprice            => $part->sellprice,
-        qty                  => 1,
-        unit                 => $unit->name,
-        %{ $params{orderitem} },
-      },
-    ],
+    orderitems => [ create_order_item(part => $part, qty =>  1, %{ $params{orderitem} }) ],
     periodic_invoices_config => $params{periodic_invoices_config} ? {
       active                 => 1,
       ar_chart_id            => $ar_chart->id,
@@ -100,11 +84,7 @@ sub create_sales_order {
     %{ $params{order} },
   );
 
-  $order->calculate_prices_and_taxes;
-
-  ok($order->save(cascade => 1));
-
-  $::form         = Form->new('');
+  $::form         = Support::TestSetup->create_new_form;
   $::form->{year} = 2014;
   $ctrl           = SL::Controller::FinancialOverview->new;
 
@@ -117,7 +97,7 @@ init_common_state();
 
 # ----------------------------------------------------------------------
 # An order without periodic invoices:
-create_sales_order();
+make_sales_order();
 
 is_deeply($ctrl->data->{$_}, { months => [ (0) x 12 ], quarters => [ 0, 0, 0, 0 ], year => 0 }, "no periodic invoices, data for $_")
   for qw(purchase_invoices purchase_orders requests_for_quotation sales_invoices sales_quotations);
@@ -127,7 +107,7 @@ is_deeply($ctrl->data->{$_}, { months => [ 0, 0, 333.33, 0, 0, 0, 0, 0, 0, 0, 0,
 
 # ----------------------------------------------------------------------
 # order_value_periodicity=y, periodicity=q
-create_sales_order(
+make_sales_order(
   periodic_invoices_config  => {
     periodicity             => 'm',
     order_value_periodicity => 'y',
@@ -146,7 +126,7 @@ is_deeply($ctrl->data->{sales_orders_per_inv},
 
 # ----------------------------------------------------------------------
 # order_value_periodicity=y, periodicity=q, starting in previous year
-create_sales_order(
+make_sales_order(
   order                     => {
     transdate               => DateTime->from_kivitendo('01.03.2013'),
   },
@@ -168,7 +148,7 @@ is_deeply($ctrl->data->{sales_orders_per_inv},
 
 # ----------------------------------------------------------------------
 # order_value_periodicity=y, periodicity=q, starting in previous year, ending middle of year
-create_sales_order(
+make_sales_order(
   order                     => {
     transdate               => DateTime->from_kivitendo('01.03.2013'),
   },
@@ -192,7 +172,7 @@ is_deeply($ctrl->data->{sales_orders_per_inv},
 
 # ----------------------------------------------------------------------
 # order_value_periodicity=y, periodicity=q, starting and ending before current
-create_sales_order(
+make_sales_order(
   order                     => {
     transdate               => DateTime->from_kivitendo('01.03.2012'),
   },
index 2c10d03..bfccf4e 100644 (file)
@@ -1,6 +1,6 @@
 use lib 't';
 
-use Test::More tests => 38;
+use Test::More tests => 41;
 use Test::Deep;
 use Data::Dumper;
 
@@ -243,47 +243,48 @@ test {
 }, 'object test simple', class => 'SL::DB::Manager::Part';
 
 test {
-  'type' => 'assembly',
+  'part_type' => 'assembly',
 }, {
   query => [
-    'assembly' => 1
-  ],
+             'part_type',
+             'assembly'
+           ] ,
 }, 'object test without prefix', class => 'SL::DB::Manager::Part';
 
 test {
-  'part.type' => 'assembly',
+  'part.part_type' => 'assembly',
 }, {
   query => [
-    'part.assembly' => 1
-  ],
+             'part.part_type',
+             'assembly'
+           ]
 }, 'object test with prefix', class => 'SL::DB::Manager::OrderItem';
 
 test {
-  'type' => [ 'part', 'assembly' ],
+  'part_type' => [ 'part', 'assembly' ],
 }, {
   query => [
-    or => [
-     and => [ or => [ assembly => 0, assembly => undef ],
-              "!inventory_accno_id" => 0,
-              "!inventory_accno_id" => undef,
-     ],
-     assembly => 1,
-    ]
-  ],
+             'or',
+             [
+               'part_type',
+               'part',
+               'part_type',
+               'assembly'
+             ]
+           ]
 }, 'object test without prefix but complex value', class => 'SL::DB::Manager::Part';
-
 test {
-  'part.type' => [ 'part', 'assembly' ],
+  'part.part_type' => [ 'part', 'assembly' ],
 }, {
   query => [
-    or => [
-     and => [ or => [ 'part.assembly' => 0, 'part.assembly' => undef ],
-              "!part.inventory_accno_id" => 0,
-              "!part.inventory_accno_id" => undef,
-     ],
-     'part.assembly' => 1,
-    ]
-  ],
+             'or',
+             [
+               'part.part_type',
+               'part',
+               'part.part_type',
+               'assembly'
+             ]
+           ]
 }, 'object test with prefix but complex value', class => 'SL::DB::Manager::OrderItem';
 
 test {
@@ -353,10 +354,12 @@ test {
       or => [
         'part.partnumber'  => { ilike => '%term1%' },
         'part.description' => { ilike => '%term1%' },
+        'part.ean'         => { ilike => '%term1%' },
       ],
       or => [
         'part.partnumber'  => { ilike => '%term2%' },
         'part.description' => { ilike => '%term2%' },
+        'part.ean'         => { ilike => '%term2%' },
       ],
     ]
   ],
@@ -420,3 +423,21 @@ test {
   with_objects => [ 'part' ],
 }, 'complex methods modifying the key';
 
+
+test {
+  'customer:substr::ilike' => ' Meyer'
+}, {
+  query => [ customer => { ilike => '%Meyer%' } ]
+}, 'auto trim 1';
+
+test {
+  'customer:head::ilike' => ' Meyer '
+}, {
+  query => [ customer => { ilike => 'Meyer%' } ]
+}, 'auto trim 2';
+
+test {
+  'customer:tail::ilike' => "\nMeyer\x{a0}"
+}, {
+  query => [ customer => { ilike => '%Meyer' } ]
+}, 'auto trim 2';
diff --git a/t/controllers/project/project_linked_records.t b/t/controllers/project/project_linked_records.t
new file mode 100644 (file)
index 0000000..5ca1224
--- /dev/null
@@ -0,0 +1,236 @@
+use strict;
+use Test::More;
+
+use lib 't';
+use Support::TestSetup;
+use Carp;
+use Test::Exception;
+use SL::Dev::ALL qw(:ALL);
+use SL::DB::Part;
+use SL::DB::Order;
+use SL::DB::Customer;
+use SL::DB::Vendor;
+use SL::DB::Chart;
+use SL::Controller::Project;
+use DateTime;
+
+use utf8;
+
+Support::TestSetup::login();
+
+clear_up();
+
+my $vendor   = new_vendor()->save;
+my $customer = new_customer()->save;
+my $project  = create_project(projectnumber => 'p1', description => 'Project 1');
+
+my $part1 = new_part(   partnumber => 'T4254')->save;
+my $part2 = new_service(partnumber => 'Serv1')->save;
+
+# sales order with globalproject_id and item project_ids
+my $sales_order = create_sales_order(
+  save             => 1,
+  customer         => $customer,
+  globalproject_id => $project->id,
+  taxincluded      => 0,
+  orderitems       => [ create_order_item(part => $part1, qty =>  3, sellprice => 70, project_id => $project->id),
+                        create_order_item(part => $part2, qty => 10, sellprice => 50, project_id => $project->id),
+                      ]
+);
+
+# sales order with no globalproject_id but item project_ids
+my $sales_order2 = create_sales_order(
+  save             => 1,
+  customer         => $customer,
+  taxincluded      => 0,
+  orderitems       => [ create_order_item(part => $part1, qty =>  3, sellprice => 70, project_id => $project->id),
+                        create_order_item(part => $part2, qty => 10, sellprice => 50),
+                      ]
+);
+
+# purchase order with globalproject_id and item project_ids
+my $purchase_order = create_purchase_order(
+  save             => 1,
+  vendor           => $vendor,
+  globalproject_id => $project->id,
+  taxincluded      => 0,
+  orderitems       => [ create_order_item(part => $part1, qty =>  3, sellprice => 70, project_id => $project->id),
+                        create_order_item(part => $part2, qty => 10, sellprice => 50, project_id => $project->id),
+                      ]
+);
+
+# sales_invoice with globalproject_id, and all items with project_id
+my $sales_invoice = create_sales_invoice(
+  customer         => $customer,
+  globalproject_id => $project->id,
+  taxincluded      => 0,
+  invoiceitems     => [ create_invoice_item(part => $part1, qty =>  3, sellprice => 70, project_id => $project->id),
+                        create_invoice_item(part => $part2, qty => 10, sellprice => 50, project_id => $project->id),
+                      ]
+);
+
+# sales_invoice with globalproject_id, but none of the items has a project_id
+my $sales_invoice2 = create_sales_invoice(
+  customer         => $customer,
+  globalproject_id => $project->id,
+  taxincluded      => 0,
+  invoiceitems     => [ create_invoice_item(part => $part1, qty =>  3, sellprice => 70),
+                        create_invoice_item(part => $part2, qty => 10, sellprice => 50),
+                      ]
+);
+
+# one of the invoice items has the project id, but there is no globalproject_id
+my $sales_invoice4 = create_sales_invoice(
+  customer         => $customer,
+  taxincluded      => 0,
+  invoiceitems     => [ create_invoice_item(part => $part1, qty =>  3, sellprice => 70),
+                        create_invoice_item(part => $part2, qty => 10, sellprice => 50, project_id => $project->id),
+                      ]
+);
+
+my $today = DateTime->today;
+my $expense_chart_porto = SL::DB::Manager::Chart->find_by(description => 'Porto');
+my $income_chart        = SL::DB::Manager::Chart->find_by(accno => 8400);
+my $tax_9 = SL::DB::Manager::Tax->find_by(taxkey => 9, rate => 0.19) || die "No tax";
+my $tax_3 = SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19) || die "No tax";
+
+# create an ar_transaction manually, with globalproject_id and acc_trans project_ids
+my $ar_transaction = SL::DB::Invoice->new(
+      invoice          => 0,
+      invnumber        => 'test ar_transaction globalproject_id',
+      amount           => 119,
+      netamount        => 100,
+      transdate        => $today,
+      taxincluded      => 0,
+      customer_id      => $customer->id,
+      taxzone_id       => $customer->taxzone_id,
+      currency_id      => $::instance_conf->get_currency_id,
+      transactions     => [],
+      notes            => 'test ar_transaction globalproject_id',
+      globalproject_id => $project->id,
+);
+$ar_transaction->add_ar_amount_row(
+    amount     => $ar_transaction->netamount,
+    chart      => $income_chart,
+    tax_id     => $tax_3->id,
+    project_id => $project->id,
+);
+my $ar_chart = SL::DB::Manager::Chart->find_by( accno => '1400' ); # Forderungen
+$ar_transaction->create_ar_row(chart => $ar_chart);
+$ar_transaction->save;
+
+# create an ap_transaction manually, with globalproject_id and acc_trans project_ids
+my $ap_transaction = SL::DB::PurchaseInvoice->new(
+      invoice          => 0,
+      invnumber        => 'test ap_transaction globalproject_id',
+      amount           => 119,
+      netamount        => 100,
+      transdate        => $today,
+      taxincluded      => 0,
+      vendor_id        => $vendor->id,
+      taxzone_id       => $vendor->taxzone_id,
+      currency_id      => $::instance_conf->get_currency_id,
+      transactions     => [],
+      notes            => 'test ap_transaction globalproject_id',
+      globalproject_id => $project->id,
+);
+$ap_transaction->add_ap_amount_row(
+    amount     => $ap_transaction->netamount,
+    chart      => $expense_chart_porto,
+    tax_id     => $tax_9->id,
+    project_id => $project->id,
+);
+my $ap_chart = SL::DB::Manager::Chart->find_by( accno => '1600' ); # Verbindlichkeiten
+$ap_transaction->create_ap_row(chart => $ap_chart);
+$ap_transaction->save;
+
+# create an ap_transaction manually, with no globalproject_id but acc_trans project_ids
+my $ap_transaction2 = SL::DB::PurchaseInvoice->new(
+      invoice          => 0,
+      invnumber        => 'test ap_transaction no globalproject_id',
+      amount           => 119,
+      netamount        => 100,
+      transdate        => $today,
+      taxincluded      => 0,
+      vendor_id        => $vendor->id,
+      taxzone_id       => $vendor->taxzone_id,
+      currency_id      => $::instance_conf->get_currency_id,
+      transactions     => [],
+      notes            => 'test ap_transaction no globalproject_id',
+);
+$ap_transaction2->add_ap_amount_row(
+    amount     => $ap_transaction2->netamount,
+    chart      => $expense_chart_porto,
+    tax_id     => $tax_9->id,
+    project_id => $project->id,
+);
+$ap_chart = SL::DB::Manager::Chart->find_by( accno => '1600' ); # Verbindlichkeiten
+$ap_transaction2->create_ap_row(chart => $ap_chart);
+$ap_transaction2->save;
+
+my $expense_chart = SL::DB::Manager::Chart->find_by(accno => '4660'); # Reisekosten
+my $cash_chart    = SL::DB::Manager::Chart->find_by(accno => '1000'); # Kasse
+my $tax_chart     = SL::DB::Manager::Chart->find_by(accno => '1576'); # Vorsteuer
+
+my @acc_trans;
+push(@acc_trans, SL::DB::AccTransaction->new(
+                                      chart_id   => $expense_chart->id,
+                                      chart_link => $expense_chart->link,
+                                      amount     => -84.03,
+                                      transdate  => $today,
+                                      source     => '',
+                                      taxkey     => 9,
+                                      tax_id     => $tax_9->id,
+                                      project_id => $project->id,
+));
+push(@acc_trans, SL::DB::AccTransaction->new(
+                                      chart_id   => $tax_chart->id,
+                                      chart_link => $tax_chart->link,
+                                      amount     => -15.97,
+                                      transdate  => $today,
+                                      source     => '',
+                                      taxkey     => 9,
+                                      tax_id     => $tax_9->id,
+                                      project_id => $project->id,
+));
+push(@acc_trans, SL::DB::AccTransaction->new(
+                                      chart_id   => $cash_chart->id,
+                                      chart_link => $cash_chart->link,
+                                      amount     => 100,
+                                      transdate  => $today,
+                                      source     => '',
+                                      taxkey     => 0,
+                                      tax_id     => 0,
+));
+
+my $gl_transaction = SL::DB::GLTransaction->new(
+  reference      => "reise",
+  description    => "reise",
+  transdate      => $today,
+  gldate         => $today,
+  employee_id    => SL::DB::Manager::Employee->current->id,
+  taxincluded    => 1,
+  type           => undef,
+  ob_transaction => 0,
+  cb_transaction => 0,
+  storno         => 0,
+  storno_id      => undef,
+  transactions   => \@acc_trans,
+)->save;
+
+my $controller = SL::Controller::Project->new;
+$::form->{id} = $project->id;
+$controller->load_project;
+is( scalar @{$controller->linked_records}, 10, "found all records that have a link to the project");
+
+clear_up();
+
+done_testing;
+
+sub clear_up {
+  foreach (qw(OrderItem Order InvoiceItem Invoice PurchaseInvoice Part GLTransaction AccTransaction PurchaseInvoice Project Vendor Customer)) {
+    "SL::DB::Manager::${_}"->delete_all(all => 1);
+  }
+}
+
+1
index 994a348..66e5851 100644 (file)
@@ -1,4 +1,4 @@
-use Test::More tests => 9;
+use Test::More tests => 17;
 
 use strict;
 use lib 't';
@@ -6,10 +6,8 @@ use utf8;
 
 use_ok 'SL::CTI';
 
-{
-  no warnings 'once';
-  $::lx_office_conf{cti}->{international_dialing_prefix} = '00';
-}
+$::lx_office_conf{cti}->{international_dialing_prefix} = '00';
+$::lx_office_conf{cti}->{dial_command}                 = 'dummy';
 
 is SL::CTI->call_link(number => '0371 5347 620'),        'controller.pl?action=CTI/call&number=03715347620';
 is SL::CTI->call_link(number => '0049(0)421-22232 22'),  'controller.pl?action=CTI/call&number=00494212223222';
@@ -20,3 +18,15 @@ is SL::CTI->call_link(number => '0371 5347 620',        internal => 1), 'control
 is SL::CTI->call_link(number => '0049(0)421-22232 22',  internal => 1), 'controller.pl?action=CTI/call&number=00494212223222&internal=1';
 is SL::CTI->call_link(number => '+49(0)421-22232 22',   internal => 1), 'controller.pl?action=CTI/call&number=00494212223222&internal=1';
 is SL::CTI->call_link(number => 'Tel: +49 40 809064 0', internal => 1), 'controller.pl?action=CTI/call&number=0049408090640&internal=1';
+
+$::lx_office_conf{cti}->{dial_command} = '';
+
+is SL::CTI->call_link(number => '0371 5347 620'),        'callto://03715347620';
+is SL::CTI->call_link(number => '0049(0)421-22232 22'),  'callto://00494212223222';
+is SL::CTI->call_link(number => '+49(0)421-22232 22'),   'callto://00494212223222';
+is SL::CTI->call_link(number => 'Tel: +49 40 809064 0'), 'callto://0049408090640';
+
+is SL::CTI->call_link(number => '0371 5347 620',        internal => 1), 'callto://03715347620';
+is SL::CTI->call_link(number => '0049(0)421-22232 22',  internal => 1), 'callto://00494212223222';
+is SL::CTI->call_link(number => '+49(0)421-22232 22',   internal => 1), 'callto://00494212223222';
+is SL::CTI->call_link(number => 'Tel: +49 40 809064 0', internal => 1), 'callto://0049408090640';
diff --git a/t/datev/datev_format_2018.t b/t/datev/datev_format_2018.t
new file mode 100644 (file)
index 0000000..6788958
--- /dev/null
@@ -0,0 +1,406 @@
+use strict;
+use Test::More;
+use Test::Deep qw(cmp_deeply);
+
+use lib 't';
+
+use_ok 'Support::TestSetup';
+use SL::DATEV qw(:CONSTANTS);
+use SL::Dev::ALL qw(:ALL);
+use List::Util qw(sum);
+use SL::DB::Buchungsgruppe;
+use SL::DB::Chart;
+use DateTime;
+use Data::Dumper;
+use utf8;
+
+Support::TestSetup::login();
+
+my $dbh = SL::DB->client->dbh;
+
+clear_up();
+
+my $d = SL::DB::Default->get;
+$d->update_attributes(datev_export_format => 'cp1252');
+
+my $ustid           = 'DE123456788';
+my $buchungsgruppe7 = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 7%') || die "No accounting group for 7\%";
+my $date            = DateTime->new(year => 2017, month =>  7, day => 19);
+my $department      = create_department(description => 'Kästchenweiße heiße Preise');
+my $project         = create_project(projectnumber => 2017, description => '299');
+my $bank            = SL::DB::Manager::Chart->find_by(description => 'Bank') || die 'Can\'t find chart "Bank"';
+my $customer        = new_customer(name => 'Test customer', ustid => $ustid)->save();
+my $part1 = new_part(partnumber => '19', description => 'Part 19%')->save;
+my $part2 = new_part(
+  partnumber         => '7',
+  description        => 'Part 7%',
+  buchungsgruppen_id => $buchungsgruppe7->id,
+)->save;
+
+my $invoice = create_sales_invoice(
+  invnumber    => "ݗݘݰݶ",
+  itime        => $date,
+  gldate       => $date,
+  taxincluded  => 0,
+  transdate    => $date,
+  invoiceitems => [ create_invoice_item(part => $part1, qty =>  3, sellprice => 550),
+                    create_invoice_item(part => $part2, qty => 10, sellprice => 50),
+                  ],
+  department_id    => $department->id,
+  globalproject_id => $project->id,
+  customer_id      => $customer->id,
+);
+
+# lets make a boom
+# generate_datev_* doesn't care about encoding but
+# csv_buchungsexport does! all arabic will be deleted
+# and no string will be left as invnumber
+
+my $datev1 = SL::DATEV->new(
+  dbh        => $dbh,
+  trans_id   => $invoice->id,
+);
+
+my $startdate = DateTime->new(year => 2017, month =>  1, day =>  1);
+my $enddate   = DateTime->new(year => 2017, month => 12, day => 31);
+my $today     = DateTime->new(year => 2017, month =>  3, day => 17);
+
+$datev1->from($startdate);
+$datev1->to($enddate);
+
+$datev1->generate_datev_data;
+$datev1->generate_datev_lines;
+
+# check conversion to csv
+$datev1->from($startdate);
+$datev1->to($enddate);
+my ($datev_csv, $die_message);
+eval {
+  $datev_csv = SL::DATEV::CSV->new(datev_lines  => $datev1->generate_datev_lines,
+                                   from         => $startdate,
+                                   to           => $enddate,
+                                   locked       => $datev1->locked,
+                                  );
+  my $lines_aref = $datev_csv->lines; # dies only if we assign (do stuff with the data)
+  1;
+} or do {
+  $die_message = $@;
+};
+ok($die_message =~ m/Falscher Feldwert 'ݗݘݰݶ' für Feld 'belegfeld1' bei der Transaktion mit dem Umsatz von/, 'wrong_encoding');
+
+
+$invoice->invnumber('ݗݘݰݶmuh');
+$invoice->save();
+
+my $datev3 = SL::DATEV->new(
+  dbh        => $dbh,
+  trans_id   => $invoice->id,
+);
+
+$datev3->from($startdate);
+$datev3->to($enddate);
+$datev3->generate_datev_data;
+$datev3->generate_datev_lines;
+my ($datev_csv2, $die_message2);
+eval {
+  $datev_csv2 = SL::DATEV::CSV->new(datev_lines  => $datev3->generate_datev_lines,
+                                    from         => $startdate,
+                                    to           => $enddate,
+                                    locked       => $datev3->locked,
+                                   );
+my $lines_aref = $datev_csv2->lines; # dies only if we assign (do stuff with the data)
+
+  1;
+} or do {
+  $die_message2 = $@;
+};
+
+# redefine invnumber, we have mixed encodings, should still fail
+ok($die_message2 =~ m/Falscher Feldwert 'ݗݘݰݶmuh' für Feld 'belegfeld1' bei der Transaktion mit dem Umsatz von/, 'mixed_wrong_encoding');
+
+# check with good number
+$invoice->invnumber('meine muh');
+$invoice->save();
+
+$invoice->pay_invoice(chart_id      => $bank->id,
+                      amount        => $invoice->open_amount,
+                      transdate     => $invoice->transdate->clone->add(days => 10),
+                      memo          => 'foobar',
+                      source        => 'barfoo',
+                     );
+
+my $datev4 = SL::DATEV->new(
+  dbh        => $dbh,
+  trans_id   => $invoice->id,
+);
+
+$datev4->from($startdate);
+$datev4->to($enddate);
+$datev4->generate_datev_data;
+$datev4->generate_datev_lines;
+
+my ($datev_csv4, $die_message3, $lines_aref);
+eval {
+  $datev_csv4 = SL::DATEV::CSV->new(datev_lines  => $datev4->generate_datev_lines,
+                                    from         => $startdate,
+                                    to           => $enddate,
+                                    locked       => $datev4->locked,
+                                   );
+  $lines_aref = $datev_csv4->lines; # dies only if we assign (do stuff with the data)
+
+  1;
+} or do {
+  $die_message3 = $@;
+};
+ok(!($die_message3), 'no die message');
+ok(scalar @{ $datev_csv4->warnings } == 0, 'no warnings');
+
+
+note('testing invoice without deliverydate');
+my @sorted =  sort { $a->[0] cmp $b->[0] } @{ $lines_aref }; # sort by string-comparison of amount
+cmp_deeply $sorted[0],
+           [ '1963,5', 'S', 'EUR', '', '', '',
+             '1400', '8400', '', '1907', 'meine muh',
+             '', '', 'Test customer', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', "K\x{e4}stchen",
+             '299', '', $ustid, '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '1', '',
+             '', '', '', '', '',
+           ],
+           'invoice without deliverydate 19% tax export ok';
+cmp_deeply $sorted[2],
+           [ '535', 'S', 'EUR', '', '', '',
+             '1400', '8300', '', '1907','meine muh',
+             '', '', 'Test customer', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', "K\x{e4}stchen",
+             '299', '', $ustid, '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '1', '',
+             '', '', '', '', '',
+           ],
+           'invoice without deliverydate 16% tax export ok';
+cmp_deeply $sorted[1],
+           [ '2498,5', 'S', 'EUR', '', '', '',
+             '1200', '1400', '', '2907','meine muh',
+             '', '', 'Test customer', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', "K\x{e4}stchen",
+             '299', '', $ustid, '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '1', '',
+             '', '', '', '', '',
+           ],
+           'invoice without deliverydate payment export ok';
+
+# create one haben buchung with GLTransaction today
+
+my $expense_chart = SL::DB::Manager::Chart->find_by(accno => '4660'); # Reisekosten
+my $cash_chart    = SL::DB::Manager::Chart->find_by(accno => '1000'); # Kasse
+
+note('testing gl transaction without deliverydate');
+my $gl_transaction = create_gl_transaction(
+  reference      => "Reise März 2018",
+  description    => "Reisekosten März 2018 / Ma Schmidt",
+  transdate      => $today,
+  taxincluded    => 1,
+  type           => undef,
+  bookings       => [
+                      {
+                        chart  => $expense_chart,
+                        taxkey => 9,
+                        debit  => 100, # net 84.03
+                      },
+                      {
+                        chart  => $cash_chart,
+                        taxkey => 0,
+                        credit => 100,
+                      },
+                    ],
+);
+
+my $datev2 = SL::DATEV->new(
+  dbh        => $dbh,
+  trans_id   => $gl_transaction->id,
+);
+
+$datev2->from($startdate);
+$datev2->to($enddate);
+$datev2->generate_datev_data;
+
+my $datev_csv3  = SL::DATEV::CSV->new(datev_lines  => $datev2->generate_datev_lines,
+                                      from         => $startdate,
+                                      to           => $enddate,
+                                      locked       => $datev2->locked,
+                                     );
+
+my @data_csv    = sort { $a->[0] cmp $b->[0] } @{ $datev_csv3->lines };
+cmp_deeply($data_csv[0],
+           [ '100', 'S', 'EUR', '', '', '', '4660', '1000', 9, '1703', 'Reise März 2',
+             '', '', 'Reisekosten März 2018 / Ma Schmidt', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '', '', '', '', '',
+             '', '', '1', '', '', '', '', '', '',
+           ],
+           'gl datev export without delivery date ok');
+
+
+note('testing same invoice, but with deliverydate');
+# 8400 and 8300 should have deliverydate in datev, payment should not
+$invoice->deliverydate(DateTime->new(year => 2017, month =>  7, day => 18));
+$invoice->save();
+
+$datev1 = SL::DATEV->new(
+  dbh        => $dbh,
+  trans_id   => $invoice->id,
+);
+
+$datev1->from($startdate);
+$datev1->to($enddate);
+$datev1->generate_datev_data;
+$datev1->generate_datev_lines;
+
+$datev_csv = SL::DATEV::CSV->new(datev_lines  => $datev1->generate_datev_lines,
+                                 from         => $startdate,
+                                 to           => $enddate,
+                                 locked       => $datev1->locked,
+);
+@sorted    = sort { $a->[0] cmp $b->[0] } @{ $datev_csv->lines };
+cmp_deeply $sorted[0],
+           [ '1963,5', 'S', 'EUR', '', '', '',
+             '1400', '8400', '', '1907', 'meine muh',
+             '', '', 'Test customer', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', "K\x{e4}stchen",
+             '299', '', $ustid, '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '1', '18072017',
+             '', '', '', '', '',
+           ],
+           'invoice with deliverydate 19% tax export ok';
+
+cmp_deeply $sorted[2],
+           [ '535', 'S', 'EUR', '', '', '',
+             '1400', '8300', '', '1907','meine muh',
+             '', '', 'Test customer', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', "K\x{e4}stchen",
+             '299', '', $ustid, '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '1', '18072017',
+             '', '', '', '', '',
+           ],
+           'invoice with deliverydate 16% tax export ok';
+
+cmp_deeply $sorted[1],
+           [ '2498,5', 'S', 'EUR', '', '', '',
+             '1200', '1400', '', '2907','meine muh',
+             '', '', 'Test customer', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', "K\x{e4}stchen",
+             '299', '', $ustid, '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '1', '',
+             '', '', '', '', '',
+           ],
+           'invoice with deliverydate payment export ok';
+
+note('testing same gl transaction with deliverydate');
+$gl_transaction->deliverydate(DateTime->new(year => 2017, month =>  7, day => 18));
+$gl_transaction->save;
+
+$datev1 = SL::DATEV->new(
+  dbh        => $dbh,
+  trans_id   => $gl_transaction->id,
+);
+
+$datev1->from($startdate);
+$datev1->to($enddate);
+$datev1->generate_datev_data;
+
+$datev_csv   = SL::DATEV::CSV->new(datev_lines  => $datev1->generate_datev_lines,
+                                   from         => $startdate,
+                                   to           => $enddate,
+                                   locked       => $datev1->locked,
+);
+
+@sorted      = sort { $a->[0] cmp $b->[0] } @{ $datev_csv->lines };
+cmp_deeply($sorted[0],
+           [ '100', 'S', 'EUR', '', '', '', '4660', '1000', 9, '1703', 'Reise März 2',
+             '', '', 'Reisekosten März 2018 / Ma Schmidt', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '', '', '', '', '',
+             '', '', '', '', '', '', '', '', '', '', '', '', '',
+             '', '', '1', '18072017', '', '', '', '', '',
+           ],
+          'testing gl transaction with delivery date datev export ok');
+
+# TODO warnings are not yet tested
+# currently most of the valid_checks are senseless because of
+# the strict input_checks before. Maybe something like encoding mismatch of invnumber,
+# can be altered to just a warning (not a mandantory field!)
+
+done_testing();
+clear_up();
+
+
+sub clear_up {
+  SL::DB::Manager::AccTransaction->delete_all( all => 1);
+  SL::DB::Manager::GLTransaction->delete_all(  all => 1);
+  SL::DB::Manager::InvoiceItem->delete_all(    all => 1);
+  SL::DB::Manager::Invoice->delete_all(        all => 1);
+  SL::DB::Manager::Customer->delete_all(       all => 1);
+  SL::DB::Manager::Part->delete_all(           all => 1);
+  SL::DB::Manager::Project->delete_all(        all => 1);
+  SL::DB::Manager::Department->delete_all(     all => 1);
+  SL::DATEV->clean_temporary_directories;
+};
+
+1;
diff --git a/t/datev/encoding.t b/t/datev/encoding.t
new file mode 100644 (file)
index 0000000..3c3369a
--- /dev/null
@@ -0,0 +1,23 @@
+use strict;
+use Test::More;
+
+use lib 't';
+
+use_ok 'Support::TestSetup';
+use SL::DATEV::CSV qw(check_text);
+use Support::TestSetup;
+
+use utf8;
+Support::TestSetup::login();
+
+my $ascii    = 'foobar 443334 hallo';
+my $german   = 'üßäüö €';
+my $croatia  = 'Kulašić hat viele €';
+my $armenian = 'Հայերեն  ֏';
+
+is 1,     SL::DATEV::CSV::check_encoding($ascii),    'ASCII Encoding';
+is 1,     SL::DATEV::CSV::check_encoding($german),   'German umlaut, euro and ligatur Encoding';
+is undef, SL::DATEV::CSV::check_encoding($croatia),  'croatia with euro Encoding';
+is undef, SL::DATEV::CSV::check_encoding($armenian), 'armenian Encoding';
+
+done_testing;
diff --git a/t/datev/invoices.t b/t/datev/invoices.t
new file mode 100644 (file)
index 0000000..3b2d4af
--- /dev/null
@@ -0,0 +1,368 @@
+use strict;
+use Test::More;
+use Test::Deep qw(cmp_deeply cmp_bag);
+
+use lib 't';
+use utf8;
+
+use_ok 'Support::TestSetup';
+use SL::DATEV qw(:CONSTANTS);
+use SL::Dev::ALL qw(:ALL);
+use List::Util qw(sum);
+use SL::DB::Buchungsgruppe;
+use SL::DB::Chart;
+use DateTime;
+
+Support::TestSetup::login();
+
+clear_up();
+
+my $dbh = SL::DB->client->dbh;
+
+my $buchungsgruppe7 = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 7%') || die "No accounting group for 7\%";
+my $bank            = SL::DB::Manager::Chart->find_by(description => 'Bank')                 || die 'Can\'t find chart "Bank"';
+my $date            = DateTime->new(year => 2017, month =>  1, day => 1);
+my $payment_date    = DateTime->new(year => 2017, month =>  1, day => 5);
+my $gldate          = DateTime->new(year => 2017, month =>  2, day => 9); # simulate bookings for Jan being made in Feb
+my $department      = create_department(description => 'Kostenstelle DATEV-Schnittstelle 2018');
+my $project         = create_project(projectnumber => 2017, description => 'Crowd-Funding September 2017');
+my $customer        = new_customer(customernumber => '10001', name => 'Testcustomer')->save;
+my $vendor          = new_vendor(vendornumber => '70001', name => 'Testvendor')->save;
+
+my $part1 = new_part(partnumber => '19', description => 'Part 19%')->save;
+my $part2 = new_part(
+  partnumber         => '7',
+  description        => 'Part 7%',
+  buchungsgruppen_id => $buchungsgruppe7->id,
+)->save;
+
+my $invoice = create_sales_invoice(
+  invnumber    => "Þ sales ¥& invöice",
+  customer     => $customer,
+  itime        => $gldate,
+  gldate       => $gldate,
+  intnotes     => 'booked in February',
+  taxincluded  => 0,
+  transdate    => $date,
+  invoiceitems => [ create_invoice_item(part => $part1, qty =>  3, sellprice => 70),
+                    create_invoice_item(part => $part2, qty => 10, sellprice => 50),
+                  ],
+  department_id    => $department->id,
+  globalproject_id => $project->id,
+);
+$invoice->pay_invoice(chart_id      => $bank->id,
+                      amount        => $invoice->open_amount,
+                      transdate     => $payment_date->to_kivitendo,
+                      memo          => 'foobar',
+                      source        => 'barfoo',
+                     );
+my $datev1 = SL::DATEV->new(
+  dbh        => $invoice->db->dbh,
+  trans_id   => $invoice->id,
+);
+
+$datev1->generate_datev_data;
+
+my @data_datev   = sort { $a->{umsatz} <=> $b->{umsatz} } @{ $datev1->generate_datev_lines() };
+cmp_deeply \@data_datev, [
+                                         {
+                                           'belegfeld1'   => "\x{de} sales \x{a5}& inv\x{f6}ice",
+                                           'buchungstext' => 'Testcustomer',
+                                           'datum'        => '01.01.2017',
+                                           'gegenkonto'   => '8400',
+                                           'konto'        => '1400',
+                                           'kost1'        => 'Kostenstelle DATEV-Schnittstelle 2018',
+                                           'kost2'        => 'Crowd-Funding September 2017',
+                                           'locked'       => undef,
+                                           'umsatz'       => '249.9',
+                                           'waehrung'     => 'EUR',
+                                         },
+                                         {
+                                           'belegfeld1'   => "\x{de} sales \x{a5}& inv\x{f6}ice",
+                                           'buchungstext' => 'Testcustomer',
+                                           'datum'        => '01.01.2017',
+                                           'gegenkonto'   => '8300',
+                                           'konto'        => '1400',
+                                           'kost1'        => 'Kostenstelle DATEV-Schnittstelle 2018',
+                                           'kost2'        => 'Crowd-Funding September 2017',
+                                           'locked'       => undef,
+                                           'umsatz'       => 535,
+                                           'waehrung'     => 'EUR',
+                                         },
+                                         {
+                                           'belegfeld1'   => "\x{de} sales \x{a5}& inv\x{f6}ice",
+                                           'buchungstext' => 'Testcustomer',
+                                           'buchungstext' => 'Testcustomer',
+                                           'datum'        => '05.01.2017',
+                                           'gegenkonto'   => '1400',
+                                           'konto'        => '1200',
+                                           'kost1'        => 'Kostenstelle DATEV-Schnittstelle 2018',
+                                           'kost2'        => 'Crowd-Funding September 2017',
+                                           'umsatz'       => '784.9',
+                                           'locked'       => undef,
+                                           'waehrung'     => 'EUR',
+                                         },
+                                       ], "trans_id datev check ok";
+
+$datev1->use_pk(1);
+$datev1->generate_datev_data;
+# TODO for cmp_deeply we need to sort the incoming data structure (see below)
+cmp_bag $datev1->generate_datev_lines, [
+                                         {
+                                           'belegfeld1'   => "\x{de} sales \x{a5}& inv\x{f6}ice",
+                                           'buchungstext' => 'Testcustomer',
+                                           'datum'        => '01.01.2017',
+                                           'gegenkonto'   => '8400',
+                                           'konto'        => $customer->customernumber,
+                                           'kost1'        => 'Kostenstelle DATEV-Schnittstelle 2018',
+                                           'kost2'        => 'Crowd-Funding September 2017',
+                                           'umsatz'       => '249.9',
+                                           'locked'       => undef,
+                                           'waehrung'     => 'EUR',
+                                         },
+                                         {
+                                           'belegfeld1'   => "\x{de} sales \x{a5}& inv\x{f6}ice",
+                                           'buchungstext' => 'Testcustomer',
+                                           'datum'        => '01.01.2017',
+                                           'gegenkonto'   => '8300',
+                                           'konto'        => $customer->customernumber,
+                                           'kost1'        => 'Kostenstelle DATEV-Schnittstelle 2018',
+                                           'kost2'        => 'Crowd-Funding September 2017',
+                                           'umsatz'       => 535,
+                                           'locked'       => undef,
+                                           'waehrung'     => 'EUR',
+                                         },
+                                         {
+                                           'belegfeld1'   => "\x{de} sales \x{a5}& inv\x{f6}ice",
+                                           'buchungstext' => 'Testcustomer',
+                                           'datum'        => '05.01.2017',
+                                           'gegenkonto'   => $customer->customernumber,
+                                           'konto'        => '1200',
+                                           'kost1'        => 'Kostenstelle DATEV-Schnittstelle 2018',
+                                           'kost2'        => 'Crowd-Funding September 2017',
+                                           'umsatz'       => '784.9',
+                                           'locked'       => undef,
+                                           'waehrung'     => 'EUR',
+                                         },
+                                       ], "trans_id datev check use_pk ok";
+
+
+my $startdate = DateTime->new(year => 2017, month =>  1, day =>  1);
+my $enddate   = DateTime->new(year => 2017, month => 12, day => 31);
+
+# check conversion to csv
+$datev1->from($startdate);
+$datev1->to($enddate);
+# reset use_pk for csv_buchungsexport
+$datev1->use_pk(0);
+$datev1->generate_datev_data;
+
+
+my $datev_csv = SL::DATEV::CSV->new(datev_lines  => $datev1->generate_datev_lines,
+                                    from         => $startdate,
+                                    to           => $enddate,
+                                    locked       => $datev1->locked,
+                                   );
+$datev_csv->lines;
+
+
+# we need sort, because pay_invoice is not acc_trans_id order safe
+my @data_csv    = sort { $a->[0] cmp $b->[0] } @{ $datev_csv->lines };
+# warnings should be undef -> no array elements at all
+is(scalar @{ $datev_csv->warnings }, 0);
+
+
+cmp_deeply($data_csv[1], [ '535', 'S', 'EUR', '', '', '', '1400', '8300', '', '0101', "\x{de} sales \x{a5}& i",
+                     '', '', 'Testcustomer', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', 'Kostenst', 'Crowd-Fu', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', '', '1', '', '', '', '', '', '' ]
+       );
+
+cmp_deeply($data_csv[0], [ '249,9', 'S', 'EUR', '', '', '', '1400', '8400', '', '0101', "\x{de} sales \x{a5}& i",
+                     '', '', 'Testcustomer', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', 'Kostenst', 'Crowd-Fu', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', '', '1', '', '', '', '', '', '' ]
+       );
+cmp_deeply($data_csv[2], [ '784,9', 'S', 'EUR', '', '', '', '1200', '1400', '', '0501', "\x{de} sales \x{a5}& i",
+                     '', '', 'Testcustomer', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', 'Kostenst', 'Crowd-Fu', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', '', '', '', '', '', '', '', '', '', '', '', '',
+                     '', '', '1', '', '', '', '', '', '' ]
+        );
+my $march_9 = DateTime->new(year => 2017, month =>  3, day => 9);
+my $invoice2 = create_sales_invoice(
+  invnumber    => "2 sales invoice",
+  customer     => $customer,
+  itime        => $march_9,
+  gldate       => $march_9,
+  intnotes     => 'booked in March',
+  taxincluded  => 0,
+  transdate    => $date,
+  invoiceitems => [ create_invoice_item(part => $part1, qty =>  6, sellprice => 70),
+                    create_invoice_item(part => $part2, qty => 20, sellprice => 50),
+                  ]
+);
+
+my $credit_note = create_credit_note(
+  invnumber    => 'Gutschrift 34',
+  customer     => $customer,
+  itime        => $gldate,
+  gldate       => $gldate,
+  intnotes     => 'booked in February',
+  taxincluded  => 0,
+  transdate    => $date,
+  invoiceitems => [ create_invoice_item(part => $part1, qty =>  3, sellprice => 70),
+                    create_invoice_item(part => $part2, qty => 10, sellprice => 50),
+                  ]
+);
+
+my $datev = SL::DATEV->new(
+  dbh        => $dbh,
+  from       => $startdate,
+  to         => $enddate,
+);
+$datev->generate_datev_data(from_to => $datev->fromto);
+my $datev_lines = $datev->generate_datev_lines;
+my $umsatzsumme = sum map { $_->{umsatz} } @{ $datev_lines };
+cmp_ok($::form->round_amount($umsatzsumme,2), '==', 3924.5, "Sum of all bookings ok");
+
+$datev->generate_datev_data(use_pk => 1, from_to => $datev->fromto);
+$datev_lines = $datev->generate_datev_lines;
+
+note('testing purchase invoice');
+my $purchase_invoice = create_ap_transaction(
+  vendor      => $vendor,
+  invnumber   => 'ap1',
+  amount      => '226',
+  netamount   => '200',
+  transdate   => $date,
+  gldate      => $date,
+  itime       => $date, # make sure itime is 1.1., as gldatefrom tests for itime!
+  taxincluded => 0,
+  bookings    => [
+                   {
+                     chart  => SL::DB::Manager::Chart->find_by(accno => '3400'),
+                     amount => 100,
+                   },
+                   {
+                     chart  => SL::DB::Manager::Chart->find_by(accno => '3300'),
+                     amount => 100,
+                   },
+                 ],
+);
+
+$datev1 = SL::DATEV->new(
+  dbh        => $purchase_invoice->db->dbh,
+  trans_id   => $purchase_invoice->id,
+);
+
+$datev1->generate_datev_data;
+cmp_deeply $datev1->generate_datev_lines, [
+                                        {
+                                          'belegfeld1'             => 'ap1',
+                                          'buchungstext'           => 'Testvendor',
+                                          'datum'                  => '01.01.2017',
+                                          'gegenkonto'             => '1600',
+                                          'konto'                  => '3400',
+                                          'kost1'                  => undef,
+                                          'kost2'                  => undef,
+                                          'umsatz'                 => 119,
+                                          'locked'                 => undef,
+                                          'waehrung'               => 'EUR'
+                                        },
+                                        {
+                                          'belegfeld1'             => 'ap1',
+                                          'buchungstext'           => 'Testvendor',
+                                          'datum'                  => '01.01.2017',
+                                          'gegenkonto'             => '1600',
+                                          'konto'                  => '3300',
+                                          'kost1'                  => undef,
+                                          'kost2'                  => undef,
+                                          'umsatz'                 => 107,
+                                          'locked'                 => undef,
+                                          'waehrung'               => 'EUR'
+                                        }
+                                       ], "trans_id datev check purchase_invoice ok";
+$datev1->use_pk(1);
+$datev1->generate_datev_data;
+cmp_deeply $datev1->generate_datev_lines, [
+                                        {
+                                          'belegfeld1'             => 'ap1',
+                                          'buchungstext'           => 'Testvendor',
+                                          'datum'                  => '01.01.2017',
+                                          'gegenkonto'             => $vendor->vendornumber,
+                                          'konto'                  => '3400',
+                                          'kost1'                  => undef,
+                                          'kost2'                  => undef,
+                                          'umsatz'                 => 119,
+                                          'locked'                 => undef,
+                                          'waehrung'               => 'EUR'
+                                        },
+                                        {
+                                          'belegfeld1'             => 'ap1',
+                                          'buchungstext'           => 'Testvendor',
+                                          'datum'                  => '01.01.2017',
+                                          'gegenkonto'             => $vendor->vendornumber,
+                                          'konto'                  => '3300',
+                                          'kost1'                  => undef,
+                                          'kost2'                  => undef,
+                                          'umsatz'                 => 107,
+                                          'locked'                 => undef,
+                                          'waehrung'               => 'EUR'
+                                        }
+                                       ], "trans_id datev check purchase_invoice use_pk ok";
+
+note('testing gldatefrom');
+# test an order with transdate in january, but that was booked in march
+# gldatefrom in DATEV.pm checks for itime, not gldate!!!
+$datev = SL::DATEV->new(
+  dbh        => $dbh,
+  from       => $startdate,
+  to         => DateTime->new(year => 2017, month => 01, day => 31),
+);
+
+$::form               = Support::TestSetup->create_new_form;
+$::form->{gldatefrom} = DateTime->new(year => 2017, month => 3, day => 1)->to_kivitendo;
+
+$datev->generate_datev_data(from_to => $datev->fromto);
+$datev_lines = $datev->generate_datev_lines;
+$umsatzsumme = sum map { $_->{umsatz} } @{ $datev_lines };
+cmp_ok($umsatzsumme, '==', 1569.8, "Sum of bookings made after March 1st (only invoice2) ok");
+
+$::form->{gldatefrom} = DateTime->new(year => 2017, month => 5, day => 1)->to_kivitendo;
+$datev->generate_datev_data(from_to => $datev->fromto);
+cmp_deeply $datev->generate_datev_lines, [], "no bookings for January made after May 1st: ok";
+
+done_testing();
+clear_up();
+
+sub clear_up {
+  SL::DB::Manager::AccTransaction->delete_all(all => 1);
+  SL::DB::Manager::InvoiceItem->delete_all(   all => 1);
+  SL::DB::Manager::Invoice->delete_all(       all => 1);
+  SL::DB::Manager::PurchaseInvoice->delete_all(all => 1);
+  SL::DB::Manager::Customer->delete_all(      all => 1);
+  SL::DB::Manager::Part->delete_all(          all => 1);
+  SL::DB::Manager::Project->delete_all(       all => 1);
+  SL::DB::Manager::Department->delete_all(    all => 1);
+  SL::DATEV->clean_temporary_directories;
+};
+
+1;
diff --git a/t/db/delivery_order.t b/t/db/delivery_order.t
new file mode 100644 (file)
index 0000000..372cc87
--- /dev/null
@@ -0,0 +1,49 @@
+use Test::More;
+
+use strict;
+
+use lib 't';
+use utf8;
+
+use Carp;
+use Data::Dumper;
+use Support::TestSetup;
+use Test::Exception;
+
+use SL::DB::Order;
+use SL::DB::Customer;
+use SL::DB::Department;
+use SL::DB::Currency;
+use SL::DB::PaymentTerm;
+use SL::DB::DeliveryTerm;
+use SL::DB::Employee;
+use SL::DB::Part;
+use SL::DB::Unit;
+use SL::DB::DeliveryOrder;
+use SL::DB::DeliveryOrder::TypeData qw(:types);
+
+use SL::Dev::ALL qw(:ALL);
+
+Support::TestSetup::login();
+
+
+#######
+
+my $order1 = SL::Dev::Record::create_purchase_order(
+  save                    => 1,
+  taxincluded             => 0,
+);
+
+my $delivery_order = SL::DB::DeliveryOrder->new_from($order1);
+
+is $delivery_order->type, PURCHASE_DELIVERY_ORDER_TYPE, "new_from purchase order gives purchase delivery order";
+is scalar @{ $delivery_order->items }, 2, "purchase delivery order keeps items";
+is $delivery_order->vendor_id, $order1->vendor_id, "purchase delivery order keeps vendor";
+
+my $supplier_delivery_order = SL::DB::DeliveryOrder->new_from($order1, type => SUPPLIER_DELIVERY_ORDER_TYPE);
+
+is $supplier_delivery_order->type, SUPPLIER_DELIVERY_ORDER_TYPE, "new_from purchase order with given type gives supplier delivery order";
+is scalar @{ $supplier_delivery_order->items }, 0, "supplier delivery order ignores items";
+is $supplier_delivery_order->vendor_id, $order1->vendor_id, "supplier delivery order keeps vendor";
+
+done_testing();
diff --git a/t/db/order.t b/t/db/order.t
new file mode 100644 (file)
index 0000000..205c497
--- /dev/null
@@ -0,0 +1,168 @@
+use Test::More;
+
+use strict;
+
+use lib 't';
+use utf8;
+
+use Carp;
+use Data::Dumper;
+use Support::TestSetup;
+use Test::Exception;
+
+use SL::DB::Order;
+use SL::DB::Customer;
+use SL::DB::Department;
+use SL::DB::Currency;
+use SL::DB::PaymentTerm;
+use SL::DB::DeliveryTerm;
+use SL::DB::Employee;
+use SL::DB::Part;
+use SL::DB::Unit;
+
+use SL::Dev::ALL qw(:ALL);
+
+my ($customer, $employee, $payment_term, $delivery_term, $unit, @parts, $department);
+
+
+sub clear_up {
+  foreach (qw(OrderItem Order Part Customer Department PaymentTerm DeliveryTerm)) {
+    "SL::DB::Manager::${_}"->delete_all(all => 1);
+  }
+  SL::DB::Manager::Employee->delete_all(where => [ login => 'testuser' ]);
+};
+
+sub reset_state {
+  my %params = @_;
+
+  clear_up();
+
+  $unit     = SL::DB::Manager::Unit->find_by(name => 'kg') || die "Can't find unit 'kg'";
+  $customer = new_customer()->save;
+
+  $employee = SL::DB::Employee->new(
+    'login' => 'testuser',
+    'name'  => 'Test User',
+  )->save;
+
+  $department = SL::DB::Department->new(
+    'description' => 'Test Department',
+  )->save;
+
+  $payment_term = create_payment_terms(
+     'description'      => '14Tage 2%Skonto, 30Tage netto',
+     'description_long' => "Innerhalb von 14 Tagen abzüglich 2 % Skonto, innerhalb von 30 Tagen rein netto.|Bei einer Zahlung bis zum <%skonto_date%> gewähren wir 2 % Skonto (EUR <%skonto_amount%>) entspricht EUR <%total_wo_skonto%>.Bei einer Zahlung bis zum <%netto_date%> ist der fällige Betrag in Höhe von <%total%> <%currency%> zu überweisen.",
+     'percent_skonto'   => '0.02',
+     'terms_netto'      => 30,
+     'terms_skonto'     => 14
+  );
+
+  $delivery_term = SL::DB::DeliveryTerm->new(
+    'description'      => 'Test Delivey Term',
+    'description_long' => 'Test Delivey Term Test Delivey Term',
+  )->save;
+
+  # some parts/services
+  @parts = ();
+  push @parts, new_part(
+    partnumber => 'T4254',
+    unit        => $unit->name,
+  )->save;
+  push @parts, new_service(
+    partnumber => 'Serv1',
+  )->save;
+  push @parts, new_part(
+    partnumber => 'P2445',
+  )->save;
+  push @parts, new_service(
+    partnumber => 'Serv2'
+  )->save;
+}
+
+Support::TestSetup::login();
+
+reset_state();
+
+
+#####
+my $order1 = SL::Dev::Record::create_sales_order(
+  save                    => 1,
+  customer                => $customer,
+  shippingpoint           => "sp",
+  transaction_description => "td1",
+  payment_terms           => $payment_term,
+  delivery_term           => $delivery_term,
+  taxincluded             => 0,
+  orderitems => [ SL::Dev::Record::create_order_item(part => $parts[0], qty =>  3, sellprice => 70),
+                  SL::Dev::Record::create_order_item(part => $parts[1], qty => 10, sellprice => 50),
+  ]
+);
+
+my $delivery_term2 = SL::DB::DeliveryTerm->new(
+  'description'      => 'Test Delivey Term2',
+  'description_long' => 'Test Delivey Term2 Test Delivey Term2',
+)->save;
+
+my $order2 = SL::Dev::Record::create_sales_order(
+  save                    => 1,
+  customer                => $customer,
+  shippingpoint           => "sp",
+  transaction_description => "td2",
+  payment_terms           => $payment_term,
+  delivery_term           => $delivery_term2,
+  taxincluded             => 0,
+  orderitems => [ SL::Dev::Record::create_order_item(part => $parts[2], qty =>  1, sellprice => 60),
+                  SL::Dev::Record::create_order_item(part => $parts[3], qty => 20, sellprice => 40),
+  ]
+);
+
+my $order = SL::DB::Order->new_from_multi([$order1, $order2]);
+
+ok    $order->items->[0]->part->id == $parts[0]->id
+   && $order->items->[1]->part->id == $parts[1]->id
+   && $order->items->[2]->part->id == $parts[2]->id
+   && $order->items->[3]->part->id == $parts[3]->id,
+  'new from multi: positions added ok';
+
+ok $order->shippingpoint eq "sp",           'new from multi: keep same info';
+ok !$order->transaction_description,        'new from multi: undefine differnt info';
+ok $order->payment_id == $payment_term->id, 'new from multi: keep same info';
+ok !$order->delivery_term,                  'new from multi: undefine differnt info';
+
+reset_state();
+
+#####
+$order1 = SL::Dev::Record::create_sales_order(
+  save         => 1,
+  taxincluded  => 0,
+  orderitems => [ SL::Dev::Record::create_order_item(part => $parts[0], qty =>  3, sellprice => 70),
+                  SL::Dev::Record::create_order_item(part => $parts[1], qty => 10, sellprice => 50),
+  ]
+);
+$order2 = SL::Dev::Record::create_sales_order(
+  save         => 1,
+  customer     => $customer,
+  taxincluded  => 0,
+  orderitems => [ SL::Dev::Record::create_order_item(part => $parts[2], qty =>  1, sellprice => 60),
+                  SL::Dev::Record::create_order_item(part => $parts[3], qty => 20, sellprice => 40),
+  ]
+);
+
+my $err_msg;
+eval { $order = SL::DB::Order->new_from_multi([$order1, $order2]); 1 } or do {$err_msg = $@};
+
+ok $err_msg =~ "^Cannot create order from source records of different customers", 'new from multi: fail on different customers';
+
+
+####
+clear_up();
+
+done_testing;
+
+1;
+
+
+# set emacs to perl mode
+# Local Variables:
+# mode: perl
+# End:
diff --git a/t/db/time_recordig.t b/t/db/time_recordig.t
new file mode 100644 (file)
index 0000000..33b0364
--- /dev/null
@@ -0,0 +1,582 @@
+use Test::More tests => 40;
+
+use strict;
+
+use lib 't';
+use utf8;
+
+use Support::TestSetup;
+use Test::Exception;
+use DateTime;
+
+use_ok 'SL::DB::TimeRecording';
+
+use SL::Dev::ALL qw(:ALL);
+
+Support::TestSetup::login();
+
+my @time_recordings;
+my ($s1, $e1, $s2, $e2);
+
+sub clear_up {
+  foreach (qw(TimeRecording Customer)) {
+    "SL::DB::Manager::${_}"->delete_all(all => 1);
+  }
+  SL::DB::Manager::Employee->delete_all(where => [ '!login' => 'unittests' ]);
+};
+
+########################################
+
+$s1 = DateTime->now_local;
+$e1 = $s1->clone;
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1);
+
+ok( $time_recordings[0]->is_time_in_wrong_order, 'same start and end detected' );
+ok( !$time_recordings[0]->is_time_overlapping, 'not overlapping if only one time recording entry in db' );
+
+###
+$time_recordings[0]->end_time(undef);
+ok( !$time_recordings[0]->is_time_in_wrong_order, 'order ok if no end' );
+
+########################################
+# ------------s1-----e1-----
+# --s2---e2-----------------
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 15, minute => 0);
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 11, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2)->save;
+
+ok( !$time_recordings[0]->is_time_overlapping, 'not overlapping: completely before 1' );
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: completely before 2' );
+
+
+# -------s1-----e1----------
+# --s2---e2-----------------
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 15, minute => 0);
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2)->save;
+
+ok( !$time_recordings[0]->is_time_overlapping, 'not overlapping: before 1' );
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: before 2' );
+
+# ---s1-----e1--------------
+# ---------------s2---e2----
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 13, minute => 0);
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2)->save;
+
+ok( !$time_recordings[0]->is_time_overlapping, 'not overlapping: completely after 1' );
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: completely after 2' );
+
+# ---s1-----e1--------------
+# ----------s2---e2---------
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2)->save;
+
+ok( !$time_recordings[0]->is_time_overlapping, 'not overlapping: after 1' );
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: after 2' );
+
+# -------s1-----e1----------
+# ---s2-----e2--------------
+# -> overlaps
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 15, minute => 0);
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour =>  9, minute => 0);
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( $time_recordings[1]->is_time_overlapping, 'overlapping: start before, end inbetween' );
+
+# -------s1-----e1----------
+# -----------s2-----e2------
+# -> overlaps
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 15, minute => 0);
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( $time_recordings[1]->is_time_overlapping, 'overlapping: start inbetween, end after' );
+
+# ---s1---------e1----------
+# ------s2---e2-------------
+# -> overlaps
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0);
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 15, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( $time_recordings[1]->is_time_overlapping, 'overlapping: completely inbetween' );
+
+
+# ------s1---e1-------------
+# ---s2---------e2----------
+# -> overlaps
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( $time_recordings[1]->is_time_overlapping, 'overlapping: completely oudside' );
+
+
+# ---s1---e1----------------
+# ---s2---------e2----------
+# -> overlaps
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( $time_recordings[1]->is_time_overlapping, 'overlapping: same start, end outside' );
+
+# ---s1------e1-------------
+# ------s2---e2-------------
+# -> overlaps
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( $time_recordings[1]->is_time_overlapping, 'overlapping: start after, same end' );
+
+# ---s1------e1-------------
+# ------s2------------------
+# e2 undef
+# -> overlaps
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e2 = undef;
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( $time_recordings[1]->is_time_overlapping, 'overlapping: start inbetween, no end' );
+
+# ---s1------e1-------------
+# ---s2---------------------
+# e2 undef
+# -> overlaps
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e2 = undef;
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( $time_recordings[1]->is_time_overlapping, 'overlapping: same start, no end' );
+
+# -------s1------e1---------
+# ---s2---------------------
+# e2 undef
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e2 = undef;
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: start before, no end' );
+
+# -------s1------e1---------
+# -------------------s2-----
+# e2 undef
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 16, minute => 0);
+$e2 = undef;
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: start after, no end' );
+
+# -------s1------e1---------
+# ---------------s2---------
+# e2 undef
+# -> does not overlap
+
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+$e2 = undef;
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: same start as other end, no end' );
+
+# -------s1------e1---------
+# -----------e2-------------
+# s2 undef
+# -> overlaps
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0);
+$s2 = undef;
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( $time_recordings[1]->is_time_overlapping, 'overlapping: no start, end inbetween' );
+
+# -------s1------e1---------
+# ---------------e2---------
+# s2 undef
+# -> overlaps
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0);
+$s2 = undef;
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( $time_recordings[1]->is_time_overlapping, 'overlapping: no start, same end' );
+
+# -------s1------e1---------
+# --e2----------------------
+# s2 undef
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0);
+$s2 = undef;
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no start, end before' );
+
+# -------s1------e1---------
+# -------------------e2-----
+# s2 undef
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 15, minute => 0);
+$s2 = undef;
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no start, end after' );
+
+# -------s1------e1---------
+# -------e2-----------------
+# s2 undef
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 15, minute => 0);
+$s2 = undef;
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no start, same end as other start' );
+
+# ----s1--------------------
+# ----s2-----e2-------------
+# e1 undef
+# -> overlaps
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = undef;
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( $time_recordings[1]->is_time_overlapping, 'overlapping: no end in db, same start' );
+
+# --------s1----------------
+# ----s2-----e2-------------
+# e1 undef
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = undef;
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no end in db, enclosing' );
+
+# ---s1---------------------
+# ---------s2-----e2--------
+# e1 undef
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e1 = undef;
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no end in db, completely after' );
+
+# ---------s1---------------
+# --------------------------
+# e1, s2, e2 undef
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = undef;
+$s2 = undef;
+$e2 = undef;
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no end in db, no times in object' );
+
+# ---------s1---------------
+# -----s2-------------------
+# e1, e2 undef
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = undef;
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e2 = undef;
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no end in db, start before, no end in object' );
+
+# ---------s1---------------
+# -------------s2-----------
+# e1, e2 undef
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = undef;
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+$e2 = undef;
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no end in db, start after, no end in object' );
+
+# ---------s1---------------
+# ---------s2---------------
+# e1, e2 undef
+# -> overlaps
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = undef;
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e2 = undef;
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( $time_recordings[1]->is_time_overlapping, 'overlapping: no end in db, same start' );
+
+# ---------s1---------------
+# ---e2---------------------
+# e1, s2 undef
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = undef;
+$s2 = undef;
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no end in db, no start in object, end before' );
+
+# ---------s1---------------
+# ---------------e2---------
+# e1, s2 undef
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = undef;
+$s2 = undef;
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no end in db, no start in object, end after' );
+
+# ---------s1---------------
+# ---------e2---------------
+# e1, s2 undef
+# -> does not overlap
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+$e1 = undef;
+$s2 = undef;
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no end in db, no start in object, same end' );
+
+########################################
+# not overlapping if different staff_member
+$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0);
+$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 15, minute => 0);
+$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 11, minute => 0);
+$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0);
+
+clear_up;
+
+@time_recordings = ();
+push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save;
+push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2);
+
+ok( $time_recordings[1]->is_time_overlapping, 'overlapping if same staff member' );
+$time_recordings[1]->update_attributes(staff_member => SL::DB::Employee->new(
+                                         'login' => 'testuser',
+                                         'name'  => 'Test User',
+                                       )->save);
+ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping if different staff member' );
+
+clear_up;
+
+1;
+
+
+# set emacs to perl mode
+# Local Variables:
+# mode: perl
+# End:
index efc2d54..0b023b6 100644 (file)
@@ -7,17 +7,18 @@ __PACKAGE__->meta->setup(
   columns => [
     dummy => { type => 'numeric', precision => 2, scale => 12 },
     inty  => { type => 'integer' },
+    miny  => { type => 'integer' },
   ]
 );
 
 use SL::DB::Helper::AttrDuration;
 
 __PACKAGE__->attr_duration('dummy');
-__PACKAGE__->attr_duration_minutes('inty');
+__PACKAGE__->attr_duration_minutes('inty', 'miny');
 
 package main;
 
-use Test::More tests => 120;
+use Test::More tests => 130;
 use Test::Exception;
 
 use strict;
@@ -218,6 +219,22 @@ is($item->inty_as_minutes,         1,      'write as_duration_string 03:1 read a
 is($item->inty_as_hours,           3,      'write as_duration_string 03:1 read as_hours');
 is($item->inty_as_duration_string, "3:01", 'write as_duration_string 03:1 read as_duration_string');
 
+local %::myconfig = (numberformat => "1.000,00");
+
+$item = new_item(miny_in_hours => 2.5);
+is($item->miny,                    150,    'write in_hours 2.5 read raw');
+is($item->miny_as_minutes,         30,     'write in_hours 2.5 read as_minutes');
+is($item->miny_as_hours,           2,      'write in_hours 2.5 read as_hours');
+is($item->miny_in_hours,           2.5,    'write in_hours 2.5 read in_hours');
+is($item->miny_in_hours_as_number, '2,50', 'write in_hours 2.5 read in_hours_as_number');
+
+$item = new_item(miny_in_hours_as_number => '4,25');
+is($item->miny,                    255,    'write in_hours_as_number 4,25 read raw');
+is($item->miny_as_minutes,         15,     'write in_hours_as_number 4,25 read as_minutes');
+is($item->miny_as_hours,           4,      'write in_hours_as_number 4,25 read as_hours');
+is($item->miny_in_hours,           4.25,   'write in_hours_as_number 4,25 read in_hours');
+is($item->miny_in_hours_as_number, '4,25', 'write in_hours_as_number 4,25 read in_hours_as_number');
+
 # Parametervalidierung
 throws_ok { new_item()->inty_as_duration_string('invalid') } qr/invalid.*format/i, 'invalid duration format';
 
index bfc538b..62adec4 100644 (file)
@@ -1,4 +1,4 @@
-use Test::More tests => 42;
+use Test::More tests => 75;
 
 use strict;
 
@@ -11,7 +11,6 @@ use Carp;
 use Data::Dumper;
 use Support::TestSetup;
 use Test::Exception;
-use List::Util qw(max);
 
 use SL::DB::Buchungsgruppe;
 use SL::DB::Currency;
@@ -22,18 +21,19 @@ use SL::DB::Order;
 use SL::DB::DeliveryOrder;
 use SL::DB::Part;
 use SL::DB::Unit;
-use SL::DB::TaxZone;
 
-my ($customer, $currency_id, $buchungsgruppe, $employee, $vendor, $taxzone, $buchungsgruppe7, $tax, $tax7,
-    $unit, @parts);
+use SL::Dev::ALL qw(:ALL);
+
+my ($customer, $employee, $payment_do, $unit, @parts, $department);
+my ($transdate);
 
 my $VISUAL_TEST = 0;  # just a sleep to click around
 
 sub clear_up {
-  foreach (qw(DeliveryOrderItem DeliveryOrder InvoiceItem Invoice Part Customer Vendor Department PaymentTerm)) {
+  foreach (qw(DeliveryOrderItem DeliveryOrder OrderItem Order InvoiceItem Invoice Part Customer Department PaymentTerm)) {
     "SL::DB::Manager::${_}"->delete_all(all => 1);
   }
-  SL::DB::Manager::Employee->delete_all(where => [ id => 31915 ]);
+  SL::DB::Manager::Employee->delete_all(where => [ login => 'testuser' ]);
 };
 
 sub reset_state {
@@ -41,97 +41,51 @@ sub reset_state {
 
   clear_up();
 
-  $buchungsgruppe   = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%', %{ $params{buchungsgruppe} }) || croak "No accounting group 19\%";
-  $buchungsgruppe7  = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 7%', %{ $params{buchungsgruppe} })  || croak "No accounting group 7\%";
-  $taxzone          = SL::DB::Manager::TaxZone->find_by( description => 'Inland')                                           || croak "No taxzone";
-  $tax              = SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19, %{ $params{tax} })                           || croak "No tax for 19\%";
-  $tax7             = SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.07)                                              || croak "No tax for 7\%";
-  $unit             = SL::DB::Manager::Unit->find_by(name => 'kg', %{ $params{unit} })                                      || croak "No unit";
-  $currency_id     = $::instance_conf->get_currency_id;
-
-  $customer     = SL::DB::Customer->new(
-    name        => '520484567dfaedc9e60fc',
-    currency_id => $currency_id,
-    taxzone_id  => $taxzone->id,
-    %{ $params{customer} }
-  )->save;
+  $transdate = DateTime->today_local;
+  $transdate->set_year(2019) if $transdate->year == 2020; # use year 2019 in 2020, because of tax rate change in Germany
 
-  # some od.rnr real anonym data
-  my $employee_bk = SL::DB::Employee->new(
-                'id' => 31915,
-                'login' => 'barbuschka.kappes',
-                'name' => 'Barbuschka Kappes',
-  )->save;
+  $unit     = SL::DB::Manager::Unit->find_by(name => 'kg') || die "Can't find unit 'kg'";
+  $customer = new_customer()->save;
 
-  my $department_do = SL::DB::Department->new(
-                 'description' => 'Maisenhaus-Versand',
-                 'id' => 32149,
-                 'itime' => undef,
-                 'mtime' => undef
+  $employee = SL::DB::Employee->new(
+    'login' => 'testuser',
+    'name'  => 'Test User',
   )->save;
 
-  my $payment_do = SL::DB::PaymentTerm->new(
-                 'description' => '14Tage 2%Skonto, 30Tage netto',
-                 'description_long' => "Innerhalb von 14 Tagen abzüglich 2 % Skonto, innerhalb von 30 Tagen rein netto.|Bei einer Zahlung bis zum <%skonto_date%> gewähren wir 2 % Skonto (EUR <%skonto_amount%>) entspricht EUR <%total_wo_skonto%>.Bei einer Zahlung bis zum <%netto_date%> ist der fällige Betrag in Höhe von <%total%> <%currency%> zu überweisen.",
-                 'id' => 11276,
-                 'itime' => undef,
-                 'mtime' => undef,
-                 'percent_skonto' => '0.02',
-                 'ranking' => undef,
-                 'sortkey' => 4,
-                 'terms_netto' => 30,
-                 'auto_calculation' => undef,
-                 'terms_skonto' => 14
+  $department = SL::DB::Department->new(
+    'description' => 'Test Department',
   )->save;
 
+  $payment_do = create_payment_terms(
+     'description'      => '14Tage 2%Skonto, 30Tage netto',
+     'description_long' => "Innerhalb von 14 Tagen abzüglich 2 % Skonto, innerhalb von 30 Tagen rein netto.|Bei einer Zahlung bis zum <%skonto_date%> gewähren wir 2 % Skonto (EUR <%skonto_amount%>) entspricht EUR <%total_wo_skonto%>.Bei einer Zahlung bis zum <%netto_date%> ist der fällige Betrag in Höhe von <%total%> <%currency%> zu überweisen.",
+     'percent_skonto'   => '0.02',
+     'terms_netto'      => 30,
+     'terms_skonto'     => 14
+  );
+
   # two real parts
   @parts = ();
-  push @parts, SL::DB::Part->new(
-                 'id' => 26321,
-                 'image' => '',
-                 'lastcost' => '49.95000',
-                 'listprice' => '0.00000',
-                 'onhand' => '5.00000',
-                 'partnumber' => 'v-519160549',
-                 #'partsgroup_id' => 111645,
-                 'rop' => '0',
-                 'sellprice' => '242.20000',
-                 #'warehouse_id' => 64702,
-                 'weight' => '0.79',
-                 description        => "Pflaumenbaum, Gr.5, Unterfilz weinrot, genietet[[Aufschnittbreite: 11,0, Kernform: US]]\"" ,
-                 buchungsgruppen_id => $buchungsgruppe->id,
-                 unit               => $unit->name,
-                 id                 => 26321,
+  push @parts, new_part(
+    description => "description 1",
+    lastcost    => '49.95000',
+    listprice   => '0.00000',
+    partnumber  => 'v-519160549',
+    sellprice   => '242.20000',
+    unit        => $unit->name,
+    weight      => '0.79',
   )->save;
 
-  push @parts, SL::DB::Part->new(
-                 'description' => "[[0640]]Flügel Hammerstiele bestehend aus:
-70 Stielen Standard in Weißbuche und
-20 Stielen Diskant abgekehlt in Weißbuche
-mit Röllchen aus Synthetikleder,
-Kapseln mit Yamaha Profil, Kerbenabstand 3,6 mm mit eingedrehten Abnickschrauben",
-                 'id' => 25505,
-                 'lastcost' => '153.00000',
-                 'listprice' => '0.00000',
-                 'onhand' => '9.00000',
-                 'partnumber' => 'v-120160086',
-                 # 'partsgroup_id' => 111639,
-                 'rop' => '0',
-                 'sellprice' => '344.30000',
-                 'weight' => '0.9',
-                  buchungsgruppen_id => $buchungsgruppe->id,
-                  unit               => $unit->name,
+  push @parts, new_part(
+    description => "description 2",
+    lastcost    => '153.00000',
+    listprice   => '0.00000',
+    partnumber  => 'v-120160086',
+    sellprice   => '344.30000',
+    unit        => $unit->name,
+    weight      => '0.9',
   )->save;
-}
-
-sub new_delivery_order {
-  my %params  = @_;
 
-  return SL::DB::DeliveryOrder->new(
-   currency_id => $currency_id,
-   taxzone_id  => $taxzone->id,
-    %params,
-  )->save;
 }
 
 Support::TestSetup::login();
@@ -139,161 +93,99 @@ Support::TestSetup::login();
 reset_state();
 
 # we create L20199 with two items
-my $do1 = new_delivery_order('department_id'    => 32149,
-                             'donumber'         => 'L20199',
-                             'employee_id'      => 31915,
-                             'intnotes'         => 'Achtung: Neue Lieferadresse ab 16.02.2015 in der Otto-Merck-Str. 7a!   13.02.2015/MH
-
-                                            Yamaha-Produkte (201...) immer plus 25% dazu rechnen / BK 13.02.2014',
-                              'ordnumber'       => 'A16399',
-                              'payment_id'      => 11276,
-                              'salesman_id'     => 31915,
-                              'shippingpoint'   => 'Maisenhaus',
-                              # 'shipto_id'     => 451463,
-                              'is_sales'        => 'true',
-                              'shipvia'         => 'DHL, Versand am 06.03.2015, 1 Paket  17,00 kg',
-                              'taxzone_id'      => 4,
-                              'closed'          => undef,
-                              # 'currency_id'   => 1,
-                              'cusordnumber'    => 'b84da',
-                              'customer_id'     => $customer->id,
-                              'id'              => 464003,
-                              'notes'           => '<ul><li><strong>fett</strong></li><li><strong>und</strong></li><li><strong>mit</strong></li><li><strong>bullets</strong></li><li>&nbsp;</li></ul>',
+my $do1 = create_sales_delivery_order(
+  'department_id' => $department->id,
+  'transdate'     => $transdate,
+  'donumber'      => 'L20199',
+  'employee_id'   => $employee->id,
+  'intnotes'      => 'some intnotes',
+  'ordnumber'     => 'A16399',
+  'payment_id'    => $payment_do->id,
+  'salesman_id'   => $employee->id,
+  'shippingpoint' => 'sendtome',
+  'shipvia'       => 'DHL, Versand am 06.03.2015, 1 Paket  17,00 kg',
+  'cusordnumber'  => 'b84da',
+  'customer_id'   => $customer->id,
+  'notes'         => '<ul><li><strong>fett</strong></li><li><strong>und</strong></li><li><strong>mit</strong></li><li><strong>bullets</strong></li><li>&nbsp;</li></ul>',
+  orderitems => [
+                  create_delivery_order_item(
+                    part               => $parts[0],
+                    discount           => '0.25',
+                    lastcost           => '49.95000',
+                    longdescription    => "<ol><li>27</li><li>28</li><li>29</li><li><sub>asdf</sub></li><li><sub>asdf</sub></li><li><sup>oben</sup></li></ol><p><s>kommt nicht mehr vor</s></p>",
+                    marge_price_factor => 1,
+                    qty                => '2.00000',
+                    sellprice          => '242.20000',
+                    unit               => $unit->name,
+                  ),
+                  create_delivery_order_item(
+                    part            => $parts[1],
+                    discount        => '0.25',
+                    lastcost        => '153.00000',
+                    qty             => '3.00000',
+                    sellprice       => '344.30000',
+                    transdate       => '06.03.2015',
+                    unit            => $unit->name,
+                  )
+                ],
+  transaction_description => 'Liefervorgang',
 );
 
-my $do1_item1 = SL::DB::DeliveryOrderItem->new('delivery_order_id' => 464003,
-                                               'description' => "Flügel Hammerkopf bestehend aus:
-                                                                 Bass/Diskant 26/65 Stück, Gesamtlänge 80/72, Bohrlänge 56/48
-                                                                 Pflaumenbaum, Gr.5, Unterfilz weinrot, genietet[[Aufschnittbreite: 11,0, Kernform: US]]",
-                                               'discount' => '0.25',
-                                               'id' => 144736,
-                                               'lastcost' => '49.95000',
-                                               'longdescription'    => "<ol><li>27</li><li>28</li><li>29</li><li><sub>asdf</sub></li><li><sub>asdf</sub></li><li><sup>oben</sup></li></ol><p><s>kommt nicht mehr vor</s></p>",
-                                               'marge_price_factor' => 1,
-                                               'mtime' => undef,
-                                               'ordnumber' => 'A16399',
-                                               'parts_id' => 26321,
-                                               'position' => 1,
-                                               'price_factor' => 1,
-                                               'qty' => '2.00000',
-                                               'sellprice' => '242.20000',
-                                               'transdate' => '06.03.2015',
-                                               'unit' => 'kg')->save;
-
-my $do1_item2 = SL::DB::DeliveryOrderItem->new('delivery_order_id' => 464003,
-                 'description' => "[[0640]]Flügel Hammerstiele bestehend aus:
-70 Stielen Standard in Weißbuche und
-20 Stielen Diskant abgekehlt in Weißbuche
-mit Röllchen aus Synthetikleder,
-Kapseln mit Yamaha Profil, Kerbenabstand 3,6 mm mit eingedrehten Abnickschrauben",
-                 'discount' => '0.25',
-                 'id' => 144737,
-                 'itime' => undef,
-                 'lastcost' => '153.00000',
-                 'longdescription' => '',
-                 'marge_price_factor' => 1,
-                 'mtime' => undef,
-                 'ordnumber' => 'A16399',
-                 'parts_id' => 25505,
-                 'position' => 2,
-                 'price_factor' => 1,
-                 'price_factor_id' => undef,
-                 'pricegroup_id' => undef,
-                 'project_id' => undef,
-                 'qty' => '3.00000',
-                 'reqdate' => undef,
-                 'sellprice' => '344.30000',
-                 'serialnumber' => '',
-                 'transdate' => '06.03.2015',
-                 'unit' => 'kg')->save;
 
 # TESTS
 
+my $do1_item1 = $do1->orderitems->[0];
+my $do1_item2 = $do1->orderitems->[1];
 
 # test delivery order before any conversion
 ok($do1->donumber eq "L20199", 'Delivery Order Number created');
 ok($do1->notes eq '<ul><li><strong>fett</strong></li><li><strong>und</strong></li><li><strong>mit</strong></li><li><strong>bullets</strong></li><li>&nbsp;</li></ul>', "do RichText notes saved");
 ok((not $do1->closed) , 'Delivery Order is not closed');
-ok($do1_item1->parts_id eq '26321', 'doi linked with part');
+is($do1_item1->parts_id, $parts[0]->id, 'doi linked with part');
 ok($do1_item1->qty == 2, 'qty check doi');
 ok($do1_item1->longdescription eq  "<ol><li>27</li><li>28</li><li>29</li><li><sub>asdf</sub></li><li><sub>asdf</sub></li><li><sup>oben</sup></li></ol><p><s>kommt nicht mehr vor</s></p>",
      "do item1 rich text longdescripition");
 ok ($do1_item2->position == 2, 'doi2 position check');
-ok (2 ==  scalar@{ SL::DB::Manager::DeliveryOrderItem->get_all(where => [ delivery_order_id => $do1->id ]) }, 'two doi linked');
+is (SL::DB::Manager::DeliveryOrderItem->get_all_count(where => [ delivery_order_id => $do1->id ]), 2 , 'two doi linked');
 
 
 # convert this do to invoice
-my $invoice = $do1->convert_to_invoice();
+my $invoice = $do1->convert_to_invoice(transdate => $transdate, attributes => {transaction_description => 'Rechnungsvorgang'});
 
 sleep (300) if $VISUAL_TEST; # we can do a real visual test via gui login
 # test invoice afterwards
 
 ok ($invoice->shipvia eq "DHL, Versand am 06.03.2015, 1 Paket  17,00 kg", "ship via check");
-ok ($invoice->shippingpoint eq "Maisenhaus", "shipping point check");
+ok ($invoice->shippingpoint eq "sendtome", "shipping point check");
 ok ($invoice->ordnumber eq "A16399", "ordnumber check");
 ok ($invoice->donumber eq "L20199", "donumber check");
 ok ($invoice->notes eq '<ul><li><strong>fett</strong></li><li><strong>und</strong></li><li><strong>mit</strong></li><li><strong>bullets</strong></li><li>&nbsp;</li></ul>', "do RichText notes saved");
 ok(($do1->closed) , 'Delivery Order is closed after conversion');
-ok (SL::DB::PaymentTerm->new(id => $invoice->{payment_id})->load->description eq "14Tage 2%Skonto, 30Tage netto", 'payment term description check');
-
-# some test data from original client invoice console (!)
-# my $invoice3 = SL::DB::Manager::Invoice->find_by( ordnumber => 'A16399' );
-# which will fail due to PTC Calculation differs from GUI-Calculation, see issue: http://redmine.kivitendo-premium.de/issues/82
-# pp $invoice3
-# values from gui should be:
-#ok($invoice->amount == 1354.20000, 'amount check');
-#ok($invoice->marge_percent == 50.88666, 'marge percent check');
-#ok($invoice->marge_total == 579.08000, 'marge total check');
-#ok($invoice->netamount == 1137.98000, 'netamount check');
-
-
-# the values change if one reloads the object
-# without reloading we get this failures
-#not ok 17 - amount check
-#   Failed test 'amount check'
-#   at t/db_helper/convert_invoice.t line 272.
-#          got: '1354.17'
-#     expected: '1354.17000'
-#not ok 18 - marge percent check
-#   Failed test 'marge percent check'
-#   at t/db_helper/convert_invoice.t line 273.
-#          got: '50.8857956342929'
-#     expected: '50.88580'
-#not ok 19 - marge total check
-#   Failed test 'marge total check'
-#   at t/db_helper/convert_invoice.t line 274.
-#          got: '579.06'
-#     expected: '579.06000'
-#not ok 20 - netamount check
-#   Failed test 'netamount check'
-#   at t/db_helper/convert_invoice.t line 275.
-#          got: '1137.96'
-#     expected: '1137.96000'
+is($invoice->payment_terms->description, "14Tage 2%Skonto, 30Tage netto", 'payment term description check');
 
 $invoice->load;
 
-ok($invoice->currency_id eq '1', 'currency_id');
-ok($invoice->cusordnumber eq 'b84da', 'cusordnumber check');
-ok(SL::DB::Department->new(id => $invoice->{department_id})->load->description eq "Maisenhaus-Versand", 'department description');
-is($invoice->amount, '1354.17000', 'amount check');
-is($invoice->marge_percent, '50.88580', 'marge percent check');
-is($invoice->marge_total, '579.06000', 'marge total check');
-is($invoice->netamount, '1137.96000', 'netamount check');
+is($invoice->cusordnumber            , 'b84da'           , 'cusordnumber check');
+is($invoice->transaction_description,  'Rechnungsvorgang', 'transaction description (changed on conversion) check');
+is($invoice->department->description , "Test Department" , 'department description ok');
+is($invoice->amount                  , '1354.20000'      , 'amount check');
+is($invoice->marge_percent           , '50.88666'        , 'marge percent check');
+is($invoice->marge_total             , '579.08000'       , 'marge total check');
+is($invoice->netamount               , '1137.98000'      , 'netamount check');
 
 # some item checks
-ok(@ {$invoice->items_sorted}[0]->parts_id eq '26321', 'invoiceitem 1 linked with part');
-ok(2 ==  scalar@{ $invoice->invoiceitems }, 'two invoice items linked with invoice');
-is(@ {$invoice->items_sorted}[0]->position, 1, "position 1 order correct");
-is(@ {$invoice->items_sorted}[1]->position, 2, "position 2 order correct");
-is(@ {$invoice->items_sorted}[0]->longdescription, "<ol><li>27</li><li>28</li><li>29</li><li><sub>asdf</sub></li><li><sub>asdf</sub></li><li><sup>oben</sup></li></ol><p><s>kommt nicht mehr vor</s></p>",
+is($invoice->items_sorted->[0]->parts_id         , $parts[0]->id , 'invoiceitem 1 linked with part');
+is(scalar @{ $invoice->invoiceitems }            , 2             , 'two invoice items linked with invoice');
+is($invoice->items_sorted->[0]->position         , 1             , "position 1 order correct");
+is($invoice->items_sorted->[1]->position         , 2             , "position 2 order correct");
+is($invoice->items_sorted->[0]->part->partnumber , 'v-519160549' , "partnumber 1 correct");
+is($invoice->items_sorted->[1]->part->partnumber , 'v-120160086' , "partnumber 2 correct");
+is($invoice->items_sorted->[0]->qty              , '2.00000'     , "pos 1 qty");
+is($invoice->items_sorted->[1]->qty              , '3.00000'     , "pos 2 qty");
+is($invoice->items_sorted->[0]->discount         , 0.25          , "pos 1 discount");
+is($invoice->items_sorted->[1]->discount         , 0.25          , "pos 2 discount");
+is($invoice->items_sorted->[0]->longdescription  , "<ol><li>27</li><li>28</li><li>29</li><li><sub>asdf</sub></li><li><sub>asdf</sub></li><li><sup>oben</sup></li></ol><p><s>kommt nicht mehr vor</s></p>",
      "invoice item1 rich text longdescripition");
-is(@ {$invoice->items_sorted}[0]->part->partnumber, 'v-519160549', "partnumber 1 correct");
-is(@ {$invoice->items_sorted}[1]->part->partnumber, 'v-120160086', "partnumber 2 correct");
-is(@ {$invoice->items_sorted}[0]->qty, '2.00000', "pos 1 qty");
-is(@ {$invoice->items_sorted}[1]->qty, '3.00000', "pos 2 qty");
-is(@ {$invoice->items_sorted}[0]->discount, 0.25, "pos 1 discount");
-is(@ {$invoice->items_sorted}[1]->discount, 0.25, "pos 2 discount");
-
 # more ideas: check onhand, lastcost (parsed lastcost)
 
 
@@ -325,24 +217,140 @@ is(@ {$invoice->items_sorted}[1]->discount, 0.25, "pos 2 discount");
 #         };
 
 
-
 my @links_record    = RecordLinks->get_links('from_table' => 'delivery_orders',
                                              'to_table'   => 'ar',
-                                             'from_id'      => 464003);
-is($links_record[0]->{from_id}, '464003', "record from id check");
-is($links_record[0]->{from_table}, 'delivery_orders', "record from table check");
-is($links_record[0]->{to_table}, 'ar', "record to table check");
+                                             'from_id'    => $do1->id,
+                                            );
+
+is($links_record[0]->{from_id}    , $do1->id          , "record from id check");
+is($links_record[0]->{from_table} , 'delivery_orders' , "record from table check");
+is($links_record[0]->{to_table}   , 'ar'              , "record to table check");
 
-foreach (qw(144736 144737)) {
+foreach ( $do1_item1->id, $do1_item2->id ) {
   my @links_record_item1 = RecordLinks->get_links('from_table' => 'delivery_order_items',
-                                                 'to_table'   => 'invoice',
-                                                 'from_id'      => $_);
-  is($links_record_item1[0]->{from_id}, $_, "record from id check $_");
-  is($links_record_item1[0]->{from_table}, 'delivery_order_items', "record from table check $_");
-  is($links_record_item1[0]->{to_table}, 'invoice', "record to table check $_");
+                                                  'to_table'   => 'invoice',
+                                                  'from_id'    => $_,
+                                                 );
+
+  is($links_record_item1[0]->{from_id}    , $_                     , "record from id check $_");
+  is($links_record_item1[0]->{from_table} , 'delivery_order_items' , "record from table check $_");
+  is($links_record_item1[0]->{to_table}   , 'invoice'              , "record to table check $_");
 }
 
+##############
+# test conversion from order to invoice
+##############
+
+reset_state();
+
+# we create A16399 with two items
+my $o1 = create_sales_order(
+  save            => 1,
+  'department_id' => $department->id,
+  'transdate'     => $transdate,
+  'employee_id'   => $employee->id,
+  'intnotes'      => 'some intnotes',
+  'ordnumber'     => 'A16399',
+  'payment_id'    => $payment_do->id,
+  'salesman_id'   => $employee->id,
+  'shippingpoint' => 'sendtome',
+  'shipvia'       => 'DHL, Versand am 06.03.2015, 1 Paket  17,00 kg',
+  'cusordnumber'  => 'b84da',
+  'customer_id'   => $customer->id,
+  'notes'         => '<ul><li><strong>fett</strong></li><li><strong>und</strong></li><li><strong>mit</strong></li><li><strong>bullets</strong></li><li>&nbsp;</li></ul>',
+  orderitems => [
+                  create_order_item(
+                    part               => $parts[0],
+                    discount           => '0.25',
+                    lastcost           => '49.95000',
+                    longdescription    => "<ol><li>27</li><li>28</li><li>29</li><li><sub>asdf</sub></li><li><sub>asdf</sub></li><li><sup>oben</sup></li></ol><p><s>kommt nicht mehr vor</s></p>",
+                    marge_price_factor => 1,
+                    qty                => '2.00000',
+                    sellprice          => '242.20000',
+                    unit               => $unit->name,
+                  ),
+                  create_order_item(
+                    part            => $parts[1],
+                    discount        => '0.25',
+                    lastcost        => '153.00000',
+                    qty             => '3.00000',
+                    sellprice       => '344.30000',
+                    transdate       => '06.03.2015',
+                    unit            => $unit->name,
+                  )
+  ],
+  transaction_description => 'Auftragsvorgang',
+);
+
+
+# TESTS
+
+my $o1_item1 = $o1->orderitems->[0];
+my $o1_item2 = $o1->orderitems->[1];
+
+# convert this order to invoice
+$invoice = $o1->convert_to_invoice(transdate => $transdate, attributes => {transaction_description => 'Rechnungsvorgang'});
+$invoice->load;
+
+# test invoice afterwards
+ok ($invoice->shipvia eq "DHL, Versand am 06.03.2015, 1 Paket  17,00 kg", "convert form order: ship via check");
+ok ($invoice->shippingpoint eq "sendtome", "convert form order: shipping point check");
+ok ($invoice->ordnumber eq "A16399", "convert form order: ordnumber check");
+ok ($invoice->notes eq '<ul><li><strong>fett</strong></li><li><strong>und</strong></li><li><strong>mit</strong></li><li><strong>bullets</strong></li><li>&nbsp;</li></ul>', "convert form order: do RichText notes saved");
+ok(($o1->closed) , 'convert form order: Order is closed after conversion');
+is($invoice->payment_terms->description, "14Tage 2%Skonto, 30Tage netto", 'convert form order: payment term description check');
+
+is($invoice->cusordnumber,            'b84da',            'convert form order: cusordnumber check');
+is($invoice->transaction_description, 'Rechnungsvorgang', 'convert form order: transaction description (changed on conversion) check');
+is($invoice->department->description, "Test Department",  'convert form order: department description ok');
+is($invoice->amount,                  '1354.20000',       'convert form order: amount check');
+is($invoice->marge_percent,           '50.88666',         'convert form order: marge percent check');
+is($invoice->marge_total,             '579.08000',        'convert form order: marge total check');
+is($invoice->netamount,               '1137.98000',       'convert form order: netamount check');
+
+# some item checks
+is($invoice->items_sorted->[0]->parts_id,         $parts[0]->id , 'convert form order: invoiceitem 1 linked with part');
+is(scalar @{ $invoice->invoiceitems },            2,              'convert form order: two invoice items linked with invoice');
+is($invoice->items_sorted->[0]->position,         1,              "convert form order: position 1 order correct");
+is($invoice->items_sorted->[1]->position,         2,              "convert form order: position 2 order correct");
+is($invoice->items_sorted->[0]->part->partnumber, 'v-519160549' , "convert form order: partnumber 1 correct");
+is($invoice->items_sorted->[1]->part->partnumber, 'v-120160086' , "convert form order: partnumber 2 correct");
+is($invoice->items_sorted->[0]->qty,              '2.00000',      "convert form order: pos 1 qty");
+is($invoice->items_sorted->[1]->qty,              '3.00000',      "convert form order: pos 2 qty");
+is($invoice->items_sorted->[0]->discount,         0.25,           "convert form order: pos 1 discount");
+is($invoice->items_sorted->[1]->discount,         0.25,           "convert form order: pos 2 discount");
+is($invoice->items_sorted->[0]->longdescription , "<ol><li>27</li><li>28</li><li>29</li><li><sub>asdf</sub></li><li><sub>asdf</sub></li><li><sup>oben</sup></li></ol><p><s>kommt nicht mehr vor</s></p>",
+     "convert form order: invoice item1 rich text longdescripition");
+
+# check linked records AND linked items
+@links_record = RecordLinks->get_links('from_table' => 'oe',
+                                       'to_table'   => 'ar',
+                                       'from_id'    => $o1->id,
+);
+
+is($links_record[0]->{from_id},    $o1->id, "convert form order: record from id check");
+is($links_record[0]->{from_table}, 'oe',    "convert form order: record from table check");
+is($links_record[0]->{to_table},   'ar',    "convert form order: record to table check");
+
+my $i = 0;
+foreach ( $o1_item1->id, $o1_item2->id ) {
+  $i++;
+  my @links_record_item = RecordLinks->get_links('from_table' => 'orderitems',
+                                                  'to_table'   => 'invoice',
+                                                  'from_id'    => $_,
+                                                 );
+
+  is($links_record_item[0]->{from_id},    $_ ,          "convert form order: record from id check item $i");
+  is($links_record_item[0]->{from_table}, 'orderitems', "convert form order: record from table check item $i");
+  is($links_record_item[0]->{to_table},   'invoice',    "convert form order: record to table check item $i");
+}
 
 clear_up();
 
 1;
+
+# vim: ft=perl
+# set emacs to perl mode
+# Local Variables:
+# mode: perl
+# End:
index 718f537..154e48c 100644 (file)
@@ -1,4 +1,5 @@
-use Test::More;
+use strict;
+use Test::More tests => 197;
 
 use strict;
 
@@ -10,8 +11,14 @@ use Support::TestSetup;
 use Test::Exception;
 use List::Util qw(sum);
 
+use SL::Dev::Record qw(create_invoice_item create_sales_invoice create_credit_note create_ap_transaction);
+use SL::Dev::CustomerVendor qw(new_customer new_vendor);
+use SL::Dev::Part qw(new_part);
+use SL::DB::BankTransaction;
+use SL::DB::BankTransactionAccTrans;
 use SL::DB::Buchungsgruppe;
 use SL::DB::Currency;
+use SL::DB::Exchangerate;
 use SL::DB::Customer;
 use SL::DB::Vendor;
 use SL::DB::Employee;
@@ -21,14 +28,72 @@ use SL::DB::Unit;
 use SL::DB::TaxZone;
 use SL::DB::BankAccount;
 use SL::DB::PaymentTerm;
+use SL::DBUtils qw(selectfirst_array_query);
+use Data::Dumper;
+
+my ($customer, $vendor, $currency_id, @parts, $buchungsgruppe, $buchungsgruppe7, $unit, $employee, $tax, $tax7, $tax_9, $taxzone, $payment_terms,
+    $bank_account, $bt);
+my ($transdate1, $transdate2, $transdate3, $transdate4, $currency, $exchangerate, $exchangerate2, $exchangerate3, $exchangerate4);
+my ($ar_chart,$bank,$ar_amount_chart, $ap_chart, $ap_amount_chart, $fxloss_chart, $fxgain_chart);
+
+my $ap_transaction_counter = 0; # used for generating purchase invnumber
+
+Support::TestSetup::login();
+
+init_state();
+
+# test cases: without_skonto
+test_default_invoice_one_item_19_without_skonto();
+test_default_invoice_two_items_19_7_tax_with_skonto();
+test_default_invoice_two_items_19_7_without_skonto();
+test_default_invoice_two_items_19_7_without_skonto_incomplete_payment();
+test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments();
+test_default_ap_transaction_two_charts_19_7_without_skonto();
+test_default_ap_transaction_two_charts_19_7_tax_partial_unrounded_payment_without_skonto();
+test_default_invoice_one_item_19_without_skonto_overpaid();
+test_credit_note_two_items_19_7_tax_tax_not_included();
+
+# test cases: free_skonto
+test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_free_skonto();
+test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_free_skonto_1cent();
+test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_free_skonto_2cent();
+test_default_invoice_one_item_19_multiple_payment_final_free_skonto();
+test_default_invoice_one_item_19_multiple_payment_final_free_skonto_1cent();
+test_default_ap_transaction_two_charts_19_7_tax_without_skonto_multiple_payments_final_free_skonto();
 
-my ($customer, $vendor, $currency_id, @parts, $buchungsgruppe, $buchungsgruppe7, $unit, $employee, $tax, $tax7, $taxzone, $payment_terms, $bank_account);
+# test cases: with_skonto_pt
+test_default_invoice_two_items_19_7_tax_with_skonto_50_50();
+test_default_invoice_four_items_19_7_tax_with_skonto_4x_25();
+test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_multiple();
+test_default_ap_transaction_two_charts_19_7_with_skonto();
+test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_tax_included();
+test_default_invoice_two_items_19_7_tax_with_skonto_tax_included();
+
+# test payment of ar and ap transactions with currency and tax included/not included
+# exchangerate = 1.33333
+test_ar_currency_tax_not_included_and_payment();
+test_ar_currency_tax_included();
+test_ap_currency_tax_not_included_and_payment();
+test_ap_currency_tax_included();
+
+test_ar_currency_tax_not_included_and_payment_2();              # exchangerate 0.8
+test_ar_currency_tax_not_included_and_payment_2_credit_note();  # exchangerate 0.8
+
+test_ap_currency_tax_not_included_and_payment_2();             # two exchangerates, with fx_gain_loss
+test_ap_currency_tax_not_included_and_payment_2_credit_note(); # two exchangerates, with fx_gain_loss
+
+is(SL::DB::Manager::Invoice->get_all_count(), 21,  "number of invoices at end of tests ok");
+TODO: {
+  local $TODO = "currently this test fails because the code writing the invoice is buggy, the calculation of skonto is correct";
+  my ($acc_trans_sum)  = selectfirst_array_query($::form, $currency->db->dbh, 'SELECT SUM(amount) FROM acc_trans');
+  is($acc_trans_sum, '0.00000', "sum of all acc_trans at end of all tests is 0");
+}
 
-my $ALWAYS_RESET = 1;
+# remove all created data at end of test
+clear_up();
 
-my $reset_state_counter = 0;
+done_testing();
 
-my $purchase_invoice_counter = 0; # used for generating purchase invnumber
 
 sub clear_up {
   SL::DB::Manager::InvoiceItem->delete_all(all => 1);
@@ -37,35 +102,73 @@ sub clear_up {
   SL::DB::Manager::Part->delete_all(all => 1);
   SL::DB::Manager::Customer->delete_all(all => 1);
   SL::DB::Manager::Vendor->delete_all(all => 1);
+  SL::DB::Manager::BankTransactionAccTrans->delete_all(all => 1);
+  SL::DB::Manager::BankTransaction->delete_all(all => 1);
   SL::DB::Manager::BankAccount->delete_all(all => 1);
   SL::DB::Manager::PaymentTerm->delete_all(all => 1);
+  SL::DB::Manager::Exchangerate->delete_all(all => 1);
+  SL::DB::Manager::Currency->delete_all(where => [ name => 'CUR' ]);
 };
 
-sub reset_state {
+sub init_state {
   my %params = @_;
 
-  return if $reset_state_counter;
-
-  $params{$_} ||= {} for qw(buchungsgruppe unit customer part tax vendor);
-
   clear_up();
 
-
-  $buchungsgruppe  = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%', %{ $params{buchungsgruppe} }) || croak "No accounting group";
-  $buchungsgruppe7 = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 7%')                                || croak "No accounting group for 7\%";
-  $unit            = SL::DB::Manager::Unit->find_by(name => 'kg', %{ $params{unit} })                                      || croak "No unit";
-  $employee        = SL::DB::Manager::Employee->current                                                                    || croak "No employee";
-  $tax             = SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19, %{ $params{tax} })                           || croak "No tax";
-  $tax7            = SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.07)                                              || croak "No tax for 7\%";
-  $taxzone         = SL::DB::Manager::TaxZone->find_by( description => 'Inland')                                           || croak "No taxzone";
+  $transdate1 = DateTime->today_local;
+  $transdate1->set_year(2019) if $transdate1->year == 2020; # hardcode for 2019 in 2020, because of tax rate change in Germany
+  $transdate2 = $transdate1->clone->add(days => 1);
+  $transdate3 = $transdate1->clone->add(days => 2);
+  $transdate4 = $transdate1->clone->add(days => 3);
+
+  $buchungsgruppe  = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%') || croak "No accounting group";
+  $buchungsgruppe7 = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 7%')  || croak "No accounting group for 7\%";
+  $unit            = SL::DB::Manager::Unit->find_by(name => 'kg')                            || croak "No unit";
+  $employee        = SL::DB::Manager::Employee->current                                      || croak "No employee";
+  $tax             = SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19)                || croak "No tax";
+  $tax7            = SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.07)                || croak "No tax for 7\%";
+  $taxzone         = SL::DB::Manager::TaxZone->find_by( description => 'Inland')             || croak "No taxzone";
+  $tax_9           = SL::DB::Manager::Tax->find_by(taxkey => 9, rate => 0.19)                || croak "No tax";
+  # $tax7            = SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.07)                || croak "No tax for 7\%";
 
   $currency_id     = $::instance_conf->get_currency_id;
 
-  $customer     = SL::DB::Customer->new(
+  $currency = SL::DB::Currency->new(name => 'CUR')->save;
+
+  $fxgain_chart = SL::DB::Manager::Chart->find_by(accno => '2660') or die "Can't find fxgain_chart in test";
+  $fxloss_chart = SL::DB::Manager::Chart->find_by(accno => '2150') or die "Can't find fxloss_chart in test";
+
+  $currency->db->dbh->do('UPDATE defaults SET fxgain_accno_id = ' . $fxgain_chart->id);
+  $currency->db->dbh->do('UPDATE defaults SET fxloss_accno_id = ' . $fxloss_chart->id);
+  $::instance_conf->reload->data;
+  is($fxgain_chart->id,  $::instance_conf->get_fxgain_accno_id, "fxgain_chart was updated in defaults");
+  is($fxloss_chart->id,  $::instance_conf->get_fxloss_accno_id, "fxloss_chart was updated in defaults");
+
+  $exchangerate  = SL::DB::Exchangerate->new(transdate   => $transdate1,
+                                             buy         => '1.33333',
+                                             sell        => '1.33333',
+                                             currency_id => $currency->id,
+                                            )->save;
+  $exchangerate2 = SL::DB::Exchangerate->new(transdate   => $transdate2,
+                                             buy         => '0.8',
+                                             sell        => '0.8',
+                                             currency_id => $currency->id,
+                                            )->save;
+  $exchangerate3 = SL::DB::Exchangerate->new(transdate   => $transdate3,
+                                             buy         => '1.55557',
+                                             sell        => '1.55557',
+                                             currency_id => $currency->id,
+                                            )->save;
+  $exchangerate4 = SL::DB::Exchangerate->new(transdate   => $transdate4,
+                                             buy         => '0.77777',
+                                             sell        => '0.77777',
+                                             currency_id => $currency->id,
+                                            )->save;
+
+  $customer     = new_customer(
     name        => 'Test Customer',
     currency_id => $currency_id,
     taxzone_id  => $taxzone->id,
-    %{ $params{customer} }
   )->save;
 
   $bank_account     =  SL::DB::BankAccount->new(
@@ -87,17 +190,16 @@ sub reset_state {
     auto_calculation => 1,
   )->save;
 
-  $vendor       = SL::DB::Vendor->new(
+  $vendor       = new_vendor(
     name        => 'Test Vendor',
     currency_id => $currency_id,
     taxzone_id  => $taxzone->id,
     payment_id  => $payment_terms->id,
-    %{ $params{vendor} }
   )->save;
 
 
   @parts = ();
-  push @parts, SL::DB::Part->new(
+  push @parts, new_part(
     partnumber         => 'T4254',
     description        => 'Fourty-two fifty-four',
     lastcost           => 1.93,
@@ -107,7 +209,7 @@ sub reset_state {
     %{ $params{part1} }
   )->save;
 
-  push @parts, SL::DB::Part->new(
+  push @parts, new_part(
     partnumber         => 'T0815',
     description        => 'Zero EIGHT fifteeN @ 7%',
     lastcost           => 5.473,
@@ -116,7 +218,7 @@ sub reset_state {
     unit               => $unit->name,
     %{ $params{part2} }
   )->save;
-  push @parts, SL::DB::Part->new(
+  push @parts, new_part(
     partnumber         => '19%',
     description        => 'Testware 19%',
     lastcost           => 0,
@@ -125,7 +227,7 @@ sub reset_state {
     unit               => $unit->name,
     %{ $params{part3} }
   )->save;
-  push @parts, SL::DB::Part->new(
+  push @parts, new_part(
     partnumber         => '7%',
     description        => 'Testware 7%',
     lastcost           => 0,
@@ -135,145 +237,69 @@ sub reset_state {
     %{ $params{part4} }
   )->save;
 
-  $reset_state_counter++;
-}
-
-sub new_invoice {
-  my %params  = @_;
-
-  return SL::DB::Invoice->new(
-    customer_id => $customer->id,
-    currency_id => $currency_id,
-    employee_id => $employee->id,
-    salesman_id => $employee->id,
-    gldate      => DateTime->today_local->to_kivitendo,
-    taxzone_id  => $taxzone->id,
-    transdate   => DateTime->today_local->to_kivitendo,
-    invoice     => 1,
-    type        => 'invoice',
-    %params,
+  $ar_chart        = SL::DB::Manager::Chart->find_by( accno => '1400' ); # Forderungen
+  $ap_chart        = SL::DB::Manager::Chart->find_by( accno => '1600' ); # Verbindlichkeiten
+  $bank            = SL::DB::Manager::Chart->find_by( accno => '1200' ); # Bank
+  $ar_amount_chart = SL::DB::Manager::Chart->find_by( accno => '8400' ); # Erlöse
+  $ap_amount_chart = SL::DB::Manager::Chart->find_by( accno => '3400' ); # Wareneingang 19%
+
+  $bt = SL::DB::BankTransaction->new(
+    local_bank_account_id => $bank_account->id,
+    transdate             => $transdate1,
+    valutadate            => $transdate1,
+    amount                => 27332.32,
+    purpose               => 'dummy',
+    currency              => $currency,
   );
+  $bt->save || die $@;
 
 }
 
-sub new_purchase_invoice {
-  # my %params  = @_;
-  # manually create a Kreditorenbuchung from scratch, ap + acc_trans bookings, as no helper exists yet, like $invoice->post.
-  # arap-Booking must come last in the acc_trans order
-  $purchase_invoice_counter++;
+sub new_ap_transaction {
+  $ap_transaction_counter++;
 
-  my $purchase_invoice = SL::DB::PurchaseInvoice->new(
-    vendor_id   => $vendor->id,
-    invnumber   => 'newap ' . $purchase_invoice_counter ,
-    currency_id => $currency_id,
-    employee_id => $employee->id,
-    gldate      => DateTime->today_local->to_kivitendo,
-    taxzone_id  => $taxzone->id,
-    transdate   => DateTime->today_local->to_kivitendo,
-    invoice     => 0,
-    type        => 'invoice',
+  my $ap_transaction = create_ap_transaction(
+    vendor      => $vendor,
+    invnumber   => 'newap ' . $ap_transaction_counter,
     taxincluded => 0,
     amount      => '226',
     netamount   => '200',
-    paid        => '0',
-    # %params,
-  )->save;
-
-  my $today = DateTime->today_local->to_kivitendo;
-  my $expense_chart  = SL::DB::Manager::Chart->find_by(accno => '3400');
-  my $expense_chart_booking= SL::DB::AccTransaction->new(
-                                        trans_id   => $purchase_invoice->id,
-                                        chart_id   => $expense_chart->id,
-                                        chart_link => $expense_chart->link,
-                                        amount     => '-100',
-                                        transdate  => $today,
-                                        source     => '',
-                                        taxkey     => 9,
-                                        tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 9)->id);
-  $expense_chart_booking->save;
-
-  my $tax_chart  = SL::DB::Manager::Chart->find_by(accno => '1576');
-  my $tax_chart_booking= SL::DB::AccTransaction->new(
-                                        trans_id   => $purchase_invoice->id,
-                                        chart_id   => $tax_chart->id,
-                                        chart_link => $tax_chart->link,
-                                        amount     => '-19',
-                                        transdate  => $today,
-                                        source     => '',
-                                        taxkey     => 0,
-                                        tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 9)->id);
-  $tax_chart_booking->save;
-  $expense_chart  = SL::DB::Manager::Chart->find_by(accno => '3300');
-  $expense_chart_booking= SL::DB::AccTransaction->new(
-                                        trans_id   => $purchase_invoice->id,
-                                        chart_id   => $expense_chart->id,
-                                        chart_link => $expense_chart->link,
-                                        amount     => '-100',
-                                        transdate  => $today,
-                                        source     => '',
-                                        taxkey     => 8,
-                                        tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 8)->id);
-  $expense_chart_booking->save;
-
-
-  $tax_chart  = SL::DB::Manager::Chart->find_by(accno => '1571');
-  $tax_chart_booking= SL::DB::AccTransaction->new(
-                                         trans_id   => $purchase_invoice->id,
-                                         chart_id   => $tax_chart->id,
-                                         chart_link => $tax_chart->link,
-                                         amount     => '-7',
-                                         transdate  => $today,
-                                         source     => '',
-                                         taxkey     => 0,
-                                         tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 8)->id);
-  $tax_chart_booking->save;
-  my $arap_chart  = SL::DB::Manager::Chart->find_by(accno => '1600');
-  my $arap_booking= SL::DB::AccTransaction->new(trans_id   => $purchase_invoice->id,
-                                                chart_id   => $arap_chart->id,
-                                                chart_link => $arap_chart->link,
-                                                amount     => '226',
-                                                transdate  => $today,
-                                                source     => '',
-                                                taxkey     => 0,
-                                                tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
-  $arap_booking->save;
-
-  return $purchase_invoice;
-}
-
-sub new_item {
-  my (%params) = @_;
-
-  my $part = delete($params{part}) || $parts[0];
-
-  return SL::DB::InvoiceItem->new(
-    parts_id    => $part->id,
-    lastcost    => $part->lastcost,
-    sellprice   => $part->sellprice,
-    description => $part->description,
-    unit        => $part->unit,
-    %params,
-  );
+    gldate      => $transdate1,
+    taxzone_id  => $taxzone->id,
+    transdate   => $transdate1,
+    bookings    => [
+                     {
+                       chart => SL::DB::Manager::Chart->find_by(accno => '3400'),
+                       amount => 100,
+                     },
+                     {
+                       chart => SL::DB::Manager::Chart->find_by(accno => '3300'),
+                       amount => 100,
+                     },
+                   ],
+ );
+
+  return $ap_transaction;
 }
 
 sub number_of_payments {
-  my $transactions = shift;
+  my $invoice = shift;
 
   my $number_of_payments;
   my $paid_amount;
-  foreach my $transaction ( @$transactions ) {
+  foreach my $transaction ( @{ $invoice->transactions } ) {
     if ( $transaction->chart_link =~ /(AR_paid|AP_paid)/ ) {
-      $paid_amount += $transaction->amount ;
+      $paid_amount += $transaction->amount;
       $number_of_payments++;
-    };
+    }
   };
   return ($number_of_payments, $paid_amount);
 };
 
 sub total_amount {
-  my $transactions = shift;
+  my $invoice = shift;
 
-  my $total = sum map { $_->amount } @$transactions;
+  my $total = sum map { $_->amount } @{ $invoice->transactions };
 
   return $::form->round_amount($total, 5);
 
@@ -281,23 +307,21 @@ sub total_amount {
 
 
 # test 1
-sub test_default_invoice_one_item_19_without_skonto() {
-  reset_state() if $ALWAYS_RESET;
-
-  my $item    = new_item(qty => 2.5);
-  my $invoice = new_invoice(
+sub test_default_invoice_one_item_19_without_skonto {
+  my $title = 'default invoice, one item, 19% tax, without_skonto';
+  my $item    = create_invoice_item(part => $parts[0], qty => 2.5);
+  my $invoice = create_sales_invoice(
+    transdate    => $transdate1,
     taxincluded  => 0,
     invoiceitems => [ $item ],
     payment_id   => $payment_terms->id,
   );
-  $invoice->post;
-
-  my $purchase_invoice = new_purchase_invoice();
 
+  my $ap_transaction = new_ap_transaction();
 
   # default values
   my %params = ( chart_id => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+                 transdate => $transdate1,
                );
 
   $params{amount} = '6.96';
@@ -305,10 +329,8 @@ sub test_default_invoice_one_item_19_without_skonto() {
 
   $invoice->pay_invoice( %params );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
-  my $total = total_amount($invoice->transactions);
-
-  my $title = 'default invoice, one item, 19% tax, without_skonto';
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
 
   is($invoice->netamount,   5.85,      "${title}: netamount");
   is($invoice->amount,      6.96,      "${title}: amount");
@@ -319,23 +341,23 @@ sub test_default_invoice_one_item_19_without_skonto() {
 
 }
 
-sub test_default_invoice_one_item_19_without_skonto_overpaid() {
-  reset_state() if $ALWAYS_RESET;
+sub test_default_invoice_one_item_19_without_skonto_overpaid {
+  my $title = 'default invoice, one item, 19% tax, without_skonto';
 
-  my $item    = new_item(qty => 2.5);
-  my $invoice = new_invoice(
+  my $item    = create_invoice_item(part => $parts[0], qty => 2.5);
+  my $invoice = create_sales_invoice(
     taxincluded  => 0,
+    transdate    => $transdate1,
     invoiceitems => [ $item ],
     payment_id   => $payment_terms->id,
   );
-  $invoice->post;
 
-  my $purchase_invoice = new_purchase_invoice();
+  my $ap_transaction = new_ap_transaction();
 
 
   # default values
   my %params = ( chart_id => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+                 transdate => $transdate1,
                );
 
   $params{amount} = '16.96';
@@ -345,10 +367,8 @@ sub test_default_invoice_one_item_19_without_skonto_overpaid() {
   $params{amount} = '-10.00';
   $invoice->pay_invoice( %params );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
-  my $total = total_amount($invoice->transactions);
-
-  my $title = 'default invoice, one item, 19% tax, without_skonto';
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
 
   is($invoice->netamount,   5.85,      "${title}: netamount");
   is($invoice->amount,      6.96,      "${title}: amount");
@@ -361,21 +381,22 @@ sub test_default_invoice_one_item_19_without_skonto_overpaid() {
 
 
 # test 2
-sub test_default_invoice_two_items_19_7_tax_with_skonto() {
-  reset_state() if $ALWAYS_RESET;
+sub test_default_invoice_two_items_19_7_tax_with_skonto {
+  my $title = 'default invoice, two items, 19/7% tax with_skonto_pt';
 
-  my $item1   = new_item(qty => 2.5);
-  my $item2   = new_item(qty => 1.2, part => $parts[1]);
-  my $invoice = new_invoice(
+  my $item1   = create_invoice_item(part => $parts[0], qty => 2.5);
+  my $item2   = create_invoice_item(part => $parts[1], qty => 1.2);
+  my $invoice = create_sales_invoice(
     taxincluded  => 0,
+    transdate    => $transdate1,
     invoiceitems => [ $item1, $item2 ],
-    payment_id  => $payment_terms->id,
+    payment_id   => $payment_terms->id,
   );
-  $invoice->post;
 
   # default values
-  my %params = ( chart_id => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+  my %params = ( chart_id  => $bank_account->chart_id,
+                 transdate => $transdate1,
+                 bt_id     => $bt->id,
                );
 
   $params{payment_type} = 'with_skonto_pt';
@@ -383,10 +404,8 @@ sub test_default_invoice_two_items_19_7_tax_with_skonto() {
 
   $invoice->pay_invoice( %params );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
-  my $total = total_amount($invoice->transactions);
-
-  my $title = 'default invoice, two items, 19/7% tax with_skonto_pt';
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
 
   is($invoice->netamount,  5.85 + 11.66,   "${title}: netamount");
   is($invoice->amount,     6.96 + 12.48,   "${title}: amount");
@@ -396,21 +415,22 @@ sub test_default_invoice_two_items_19_7_tax_with_skonto() {
   is($total,                          0,   "${title}: even balance");
 }
 
-sub test_default_invoice_two_items_19_7_tax_with_skonto_tax_included() {
-  reset_state() if $ALWAYS_RESET;
+sub test_default_invoice_two_items_19_7_tax_with_skonto_tax_included {
+  my $title = 'default invoice, two items, 19/7% tax with_skonto_pt';
 
-  my $item1   = new_item(qty => 2.5);
-  my $item2   = new_item(qty => 1.2, part => $parts[1]);
-  my $invoice = new_invoice(
+  my $item1   = create_invoice_item(part => $parts[0], qty => 2.5);
+  my $item2   = create_invoice_item(part => $parts[1], qty => 1.2);
+  my $invoice = create_sales_invoice(
+    transdate    => $transdate1,
     taxincluded  => 1,
     invoiceitems => [ $item1, $item2 ],
-    payment_id  => $payment_terms->id,
+    payment_id   => $payment_terms->id,
   );
-  $invoice->post;
 
   # default values
-  my %params = ( chart_id => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+  my %params = ( chart_id  => $bank_account->chart_id,
+                 transdate => $transdate1,
+                 bt_id     => $bt->id,
                );
 
   $params{payment_type} = 'with_skonto_pt';
@@ -418,37 +438,37 @@ sub test_default_invoice_two_items_19_7_tax_with_skonto_tax_included() {
 
   $invoice->pay_invoice( %params );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
-  my $total = total_amount($invoice->transactions);
-
-  my $title = 'default invoice, two items, 19/7% tax with_skonto_pt';
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
 
   is($invoice->netamount,         15.82,   "${title}: netamount");
   is($invoice->amount,            17.51,   "${title}: amount");
   is($paid_amount,               -17.51,   "${title}: paid amount");
   is($invoice->paid,              17.51,   "${title}: paid");
   is($number_of_payments,             3,   "${title}: 3 AR_paid bookings");
-  { local $TODO = "currently this test fails because the code writing the invoice is buggy, the calculation of skonto is correct";
+
+TODO: {
+  local $TODO = "currently this test fails because the code writing the invoice is buggy, the calculation of skonto is correct";
   is($total,                          0,   "${title}: even balance");
   }
 }
 
 # test 3 : two items, without skonto
-sub test_default_invoice_two_items_19_7_without_skonto() {
-  reset_state() if $ALWAYS_RESET;
+sub test_default_invoice_two_items_19_7_without_skonto {
+  my $title = 'default invoice, two items, 19/7% tax without skonto';
 
-  my $item1   = new_item(qty => 2.5);
-  my $item2   = new_item(qty => 1.2, part => $parts[1]);
-  my $invoice = new_invoice(
+  my $item1   = create_invoice_item(part => $parts[0], qty => 2.5);
+  my $item2   = create_invoice_item(part => $parts[1], qty => 1.2);
+  my $invoice = create_sales_invoice(
+    transdate    => $transdate1,
     taxincluded  => 0,
     invoiceitems => [ $item1, $item2 ],
-    payment_id  => $payment_terms->id,
+    payment_id   => $payment_terms->id,
   );
-  $invoice->post;
 
   # default values
   my %params = ( chart_id => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+                 transdate => $transdate1,
                );
 
   $params{amount} = '19.44'; # pass full amount
@@ -456,10 +476,8 @@ sub test_default_invoice_two_items_19_7_without_skonto() {
 
   $invoice->pay_invoice( %params );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
-  my $total = total_amount($invoice->transactions);
-
-  my $title = 'default invoice, two items, 19/7% tax without skonto';
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
 
   is($invoice->netamount,     5.85 + 11.66,     "${title}: netamount");
   is($invoice->amount,        6.96 + 12.48,     "${title}: amount");
@@ -470,28 +488,26 @@ sub test_default_invoice_two_items_19_7_without_skonto() {
 }
 
 # test 4
-sub test_default_invoice_two_items_19_7_without_skonto_incomplete_payment() {
-  reset_state() if $ALWAYS_RESET;
+sub test_default_invoice_two_items_19_7_without_skonto_incomplete_payment {
+  my $title = 'default invoice, two items, 19/7% tax without skonto incomplete payment';
 
-  my $item1   = new_item(qty => 2.5);
-  my $item2   = new_item(qty => 1.2, part => $parts[1]);
-  my $invoice = new_invoice(
+  my $item1   = create_invoice_item(part => $parts[0], qty => 2.5);
+  my $item2   = create_invoice_item(part => $parts[1], qty => 1.2);
+  my $invoice = create_sales_invoice(
     taxincluded  => 0,
+    transdate    => $transdate1,
     invoiceitems => [ $item1, $item2 ],
-    payment_id  => $payment_terms->id,
+    payment_id   => $payment_terms->id,
   );
-  $invoice->post;
 
   $invoice->pay_invoice( amount       => '9.44',
                          payment_type => 'without_skonto',
                          chart_id     => $bank_account->chart_id,
-                         transdate    => DateTime->today_local->to_kivitendo,
+                         transdate    => $transdate1,
                        );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
-  my $total = total_amount($invoice->transactions);
-
-  my $title = 'default invoice, two items, 19/7% tax without skonto incomplete payment';
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
 
   is($invoice->netamount,        5.85 + 11.66,     "${title}: netamount");
   is($invoice->amount,           6.96 + 12.48,     "${title}: amount");
@@ -502,32 +518,30 @@ sub test_default_invoice_two_items_19_7_without_skonto_incomplete_payment() {
 }
 
 # test 5
-sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments() {
-  reset_state() if $ALWAYS_RESET;
+sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments {
+  my $title = 'default invoice, two items, 19/7% tax not included';
 
-  my $item1   = new_item(qty => 2.5);
-  my $item2   = new_item(qty => 1.2, part => $parts[1]);
-  my $invoice = new_invoice(
+  my $item1   = create_invoice_item(part => $parts[0], qty => 2.5);
+  my $item2   = create_invoice_item(part => $parts[1], qty => 1.2);
+  my $invoice = create_sales_invoice(
+    transdate    => $transdate1,
     taxincluded  => 0,
     invoiceitems => [ $item1, $item2 ],
-    payment_id  => $payment_terms->id,
+    payment_id   => $payment_terms->id,
   );
-  $invoice->post;
 
   $invoice->pay_invoice( amount       => '9.44',
                          payment_type => 'without_skonto',
                          chart_id     => $bank_account->chart_id,
-                         transdate    => DateTime->today_local->to_kivitendo
+                         transdate    => $transdate1,
                        );
   $invoice->pay_invoice( amount       => '10.00',
                          chart_id     => $bank_account->chart_id,
-                         transdate    => DateTime->today_local->to_kivitendo
+                         transdate    => $transdate1,
                        );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
-  my $total = total_amount($invoice->transactions);
-
-  my $title = 'default invoice, two items, 19/7% tax not included';
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
 
   is($invoice->netamount,        5.85 + 11.66,     "${title}: netamount");
   is($invoice->amount,           6.96 + 12.48,     "${title}: amount");
@@ -539,39 +553,43 @@ sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments() {
 }
 
 # test 6
-sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto() {
-  reset_state() if $ALWAYS_RESET;
+sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_free_skonto {
+  my $title = 'default invoice, two items, 19/7% tax not included';
 
-  my $item1   = new_item(qty => 2.5);
-  my $item2   = new_item(qty => 1.2, part => $parts[1]);
-  my $invoice = new_invoice(
+  my $item1   = create_invoice_item(part => $parts[0], qty => 2.5);
+  my $item2   = create_invoice_item(part => $parts[1], qty => 1.2);
+  my $invoice = create_sales_invoice(
     taxincluded  => 0,
+    transdate    => $transdate1,
     invoiceitems => [ $item1, $item2 ],
-    payment_id  => $payment_terms->id,
+    payment_id   => $payment_terms->id,
   );
-  $invoice->post;
 
   $invoice->pay_invoice( amount       => '9.44',
                          payment_type => 'without_skonto',
                          chart_id     => $bank_account->chart_id,
-                         transdate    => DateTime->today_local->to_kivitendo
+                         transdate    => $transdate1,
                        );
   $invoice->pay_invoice( amount       => '8.73',
                          payment_type => 'without_skonto',
                          chart_id     => $bank_account->chart_id,
-                         transdate    => DateTime->today_local->to_kivitendo
+                         transdate    => $transdate1,
                        );
-  $invoice->pay_invoice( amount       => $invoice->open_amount,
-                         payment_type => 'difference_as_skonto',
-                         chart_id     => $bank_account->chart_id,
-                         transdate    => DateTime->today_local->to_kivitendo
+  # free_skonto does:
+  #  my $open_amount = $payment_type eq 'with_skonto_pt' ? $invoice->amount_less_skonto : $invoice->open_amount;
+  #  $open_amount    = abs($open_amount);
+  #  $open_amount   -= $free_skonto_amount if ($payment_type eq 'free_skonto');
+
+  $invoice->pay_invoice( skonto_amount => $invoice->open_amount,
+                         amount        => 0,
+                         payment_type  => 'free_skonto',
+                         chart_id      => $bank_account->chart_id,
+                         transdate     => $transdate1,
+                         bt_id         => $bt->id,
                        );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
-  my $total = total_amount($invoice->transactions);
-
-  my $title = 'default invoice, two items, 19/7% tax not included';
-
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
   is($invoice->netamount,        5.85 + 11.66,     "${title}: netamount");
   is($invoice->amount,           6.96 + 12.48,     "${title}: amount");
   is($paid_amount,                     -19.44,     "${title}: paid amount");
@@ -581,76 +599,76 @@ sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_fin
 
 }
 
-sub  test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto_1cent() {
-  reset_state() if $ALWAYS_RESET;
+sub  test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_free_skonto_1cent {
+  my $title = 'default invoice, two items, 19/7% tax not included';
 
   # if there is only one cent left there can only be one skonto booking, the
   # error handling should choose the highest amount, which is the 7% account
   # (11.66) rather than the 19% account (5.85).  The actual tax amount is
   # higher for the 19% case, though (1.11 compared to 0.82)
+  #
+  # -> wrong: sub name. two cents are still left. one cent for each tax case. no tax correction
 
-  my $item1   = new_item(qty => 2.5);
-  my $item2   = new_item(qty => 1.2, part => $parts[1]);
-  my $invoice = new_invoice(
+  my $item1   = create_invoice_item(part => $parts[0], qty => 2.5);
+  my $item2   = create_invoice_item(part => $parts[1], qty => 1.2);
+  my $invoice = create_sales_invoice(
     taxincluded  => 0,
+    transdate    => $transdate1,
     invoiceitems => [ $item1, $item2 ],
-    payment_id  => $payment_terms->id,
+    payment_id   => $payment_terms->id,
   );
-  $invoice->post;
 
   $invoice->pay_invoice( amount       => '19.42',
                          payment_type => 'without_skonto',
                          chart_id     => $bank_account->chart_id,
-                         transdate    => DateTime->today_local->to_kivitendo
+                         transdate    => $transdate1,
                        );
-  $invoice->pay_invoice( amount       => $invoice->open_amount,
-                         payment_type => 'difference_as_skonto',
+  $invoice->pay_invoice( skonto_amount => $invoice->open_amount,
+                         amount       => 0,
+                         payment_type => 'free_skonto',
                          chart_id     => $bank_account->chart_id,
-                         transdate    => DateTime->today_local->to_kivitendo
+                         transdate    => $transdate1,
+                         bt_id        => $bt->id,
                        );
-
-  my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
-  my $total = total_amount($invoice->transactions);
-
-  my $title = 'default invoice, two items, 19/7% tax not included';
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
 
   is($invoice->netamount,        5.85 + 11.66,     "${title}: netamount");
   is($invoice->amount,           6.96 + 12.48,     "${title}: amount");
   is($paid_amount,                     -19.44,     "${title}: paid amount");
   is($invoice->paid,                    19.44,     "${title}: paid");
-  is($number_of_payments,                   3,     "${title}: 2 AR_paid bookings");
+  is($number_of_payments,                   3,     "${title}: 3 AR_paid bookings");
   is($total,                                0,     "${title}: even balance");
-
 }
 
-sub  test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto_2cent() {
-  reset_state() if $ALWAYS_RESET;
+sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_free_skonto_2cent {
+  my $title = 'default invoice, two items, 19/7% tax not included';
 
   # if there are two cents left there will be two skonto bookings, 1 cent each
-  my $item1   = new_item(qty => 2.5);
-  my $item2   = new_item(qty => 1.2, part => $parts[1]);
-  my $invoice = new_invoice(
+  my $item1   = create_invoice_item(part => $parts[0], qty => 2.5);
+  my $item2   = create_invoice_item(part => $parts[1], qty => 1.2);
+  my $invoice = create_sales_invoice(
     taxincluded  => 0,
+    transdate    => $transdate1,
     invoiceitems => [ $item1, $item2 ],
-    payment_id  => $payment_terms->id,
+    payment_id   => $payment_terms->id,
   );
-  $invoice->post;
 
   $invoice->pay_invoice( amount       => '19.42',
                          payment_type => 'without_skonto',
                          chart_id     => $bank_account->chart_id,
-                         transdate    => DateTime->today_local->to_kivitendo
+                         transdate    => $transdate1,
                        );
-  $invoice->pay_invoice( amount       => $invoice->open_amount,
-                         payment_type => 'difference_as_skonto',
+  $invoice->pay_invoice( skonto_amount => $invoice->open_amount,
+                         amount       => 0,
+                         payment_type => 'free_skonto',
                          chart_id     => $bank_account->chart_id,
-                         transdate    => DateTime->today_local->to_kivitendo
+                         transdate    => $transdate1,
+                         bt_id        => $bt->id,
                        );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
-  my $total = total_amount($invoice->transactions);
-
-  my $title = 'default invoice, two items, 19/7% tax not included';
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
 
   is($invoice->netamount,        5.85 + 11.66,     "${title}: netamount");
   is($invoice->amount,           6.96 + 12.48,     "${title}: amount");
@@ -661,20 +679,21 @@ sub  test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_fi
 
 }
 
-sub test_default_invoice_one_item_19_multiple_payment_final_difference_as_skonto() {
-  reset_state() if $ALWAYS_RESET;
+sub test_default_invoice_one_item_19_multiple_payment_final_free_skonto {
+  my $title = 'default invoice, one item, 19% tax, without_skonto';
 
-  my $item    = new_item(qty => 2.5);
-  my $invoice = new_invoice(
+  my $item    = create_invoice_item(part => $parts[0], qty => 2.5);
+  my $invoice = create_sales_invoice(
     taxincluded  => 0,
+    transdate    => $transdate1,
     invoiceitems => [ $item ],
     payment_id   => $payment_terms->id,
   );
-  $invoice->post;
 
   # default values
   my %params = ( chart_id  => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+                 transdate => $transdate1,
+                 bt_id     => $bt->id,
                );
 
   $params{amount}       = '2.32';
@@ -685,14 +704,13 @@ sub test_default_invoice_one_item_19_multiple_payment_final_difference_as_skonto
   $params{payment_type} = 'without_skonto';
   $invoice->pay_invoice( %params );
 
-  $params{amount}       = $invoice->open_amount; # set amount, otherwise previous 3.81 is used
-  $params{payment_type} = 'difference_as_skonto';
+  $params{skonto_amount} = $invoice->open_amount; # set amount, otherwise previous 3.81 is used
+  $params{amount}        = 0,
+  $params{payment_type}  = 'free_skonto';
   $invoice->pay_invoice( %params );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
-  my $total = total_amount($invoice->transactions);
-
-  my $title = 'default invoice, one item, 19% tax, without_skonto';
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
 
   is($invoice->netamount,       5.85,     "${title}: netamount");
   is($invoice->amount,          6.96,     "${title}: amount");
@@ -703,34 +721,34 @@ sub test_default_invoice_one_item_19_multiple_payment_final_difference_as_skonto
 
 }
 
-sub test_default_invoice_one_item_19_multiple_payment_final_difference_as_skonto_1cent() {
-  reset_state() if $ALWAYS_RESET;
+sub test_default_invoice_one_item_19_multiple_payment_final_free_skonto_1cent {
+  my $title = 'default invoice, one item, 19% tax, without_skonto';
 
-  my $item    = new_item(qty => 2.5);
-  my $invoice = new_invoice(
+  my $item    = create_invoice_item(part => $parts[0], qty => 2.5);
+  my $invoice = create_sales_invoice(
     taxincluded  => 0,
+    transdate    => $transdate1,
     invoiceitems => [ $item ],
     payment_id   => $payment_terms->id,
   );
-  $invoice->post;
 
   # default values
   my %params = ( chart_id  => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+                 transdate => $transdate1,
+                 bt_id     => $bt->id,
                );
 
   $params{amount}       = '6.95';
   $params{payment_type} = 'without_skonto';
   $invoice->pay_invoice( %params );
 
-  $params{amount}       = $invoice->open_amount; # set amount, otherwise previous value 6.95 is used
-  $params{payment_type} = 'difference_as_skonto';
+  $params{skonto_amount} = $invoice->open_amount;
+  $params{amount}        = 0,
+  $params{payment_type} = 'free_skonto';
   $invoice->pay_invoice( %params );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
-  my $total = total_amount($invoice->transactions);
-
-  my $title = 'default invoice, one item, 19% tax, without_skonto';
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
 
   is($invoice->netamount,       5.85,     "${title}: netamount");
   is($invoice->amount,          6.96,     "${title}: amount");
@@ -742,24 +760,22 @@ sub test_default_invoice_one_item_19_multiple_payment_final_difference_as_skonto
 }
 
 # test 3 : two items, without skonto
-sub test_default_purchase_invoice_two_charts_19_7_without_skonto() {
-  reset_state() if $ALWAYS_RESET;
+sub test_default_ap_transaction_two_charts_19_7_without_skonto {
+  my $title = 'default invoice, two items, 19/7% tax without skonto';
 
-  my $purchase_invoice = new_purchase_invoice();
+  my $ap_transaction = new_ap_transaction();
 
   my %params = ( chart_id => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+                 transdate => $transdate1,
                );
 
   $params{amount} = '226'; # pass full amount
   $params{payment_type} = 'without_skonto';
 
-  $purchase_invoice->pay_invoice( %params );
-
-  my ($number_of_payments, $paid_amount) = number_of_payments($purchase_invoice->transactions);
-  my $total = total_amount($purchase_invoice->transactions);
+  $ap_transaction->pay_invoice( %params );
 
-  my $title = 'default invoice, two items, 19/7% tax without skonto';
+  my ($number_of_payments, $paid_amount) = number_of_payments($ap_transaction);
+  my $total = total_amount($ap_transaction);
 
   is($paid_amount,         226,     "${title}: paid amount");
   is($number_of_payments,    1,     "${title}: 1 AP_paid bookings");
@@ -767,24 +783,25 @@ sub test_default_purchase_invoice_two_charts_19_7_without_skonto() {
 
 }
 
-sub test_default_purchase_invoice_two_charts_19_7_with_skonto() {
-  reset_state() if $ALWAYS_RESET;
+sub test_default_ap_transaction_two_charts_19_7_with_skonto {
+  my $title = 'default invoice, two items, 19/7% tax without skonto';
 
-  my $purchase_invoice = new_purchase_invoice();
+  my $ap_transaction = new_ap_transaction();
 
-  my %params = ( chart_id => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+  my %params = ( chart_id  => $bank_account->chart_id,
+                 transdate => $transdate1,
+                 bt_id     => $bt->id,
                );
-
-  # $params{amount} = '226'; # pass full amount
+  # BankTransaction-Controller __always__ calcs amount:
+  # my $open_amount = $payment_type eq 'with_skonto_pt' ? $invoice->amount_less_skonto : $invoice->open_amount;
+  $ap_transaction->payment_terms($ap_transaction->vendor->payment);
+  $params{amount}       = $ap_transaction->amount_less_skonto; # pass calculated skonto amount
   $params{payment_type} = 'with_skonto_pt';
 
-  $purchase_invoice->pay_invoice( %params );
+  $ap_transaction->pay_invoice( %params );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($purchase_invoice->transactions);
-  my $total = total_amount($purchase_invoice->transactions);
-
-  my $title = 'default invoice, two items, 19/7% tax without skonto';
+  my ($number_of_payments, $paid_amount) = number_of_payments($ap_transaction);
+  my $total = total_amount($ap_transaction);
 
   is($paid_amount,         226,     "${title}: paid amount");
   is($number_of_payments,    3,     "${title}: 1 AP_paid bookings");
@@ -792,20 +809,19 @@ sub test_default_purchase_invoice_two_charts_19_7_with_skonto() {
 
 }
 
-sub test_default_purchase_invoice_two_charts_19_7_tax_partial_unrounded_payment_without_skonto() {
+sub test_default_ap_transaction_two_charts_19_7_tax_partial_unrounded_payment_without_skonto {
+  my $title = 'default ap_transaction, two charts, 19/7% tax multiple payments with final difference as skonto';
+
   # check whether unrounded amounts passed via $params{amount} are rounded for without_skonto case
-  reset_state() if $ALWAYS_RESET;
-  my $purchase_invoice = new_purchase_invoice();
-  $purchase_invoice->pay_invoice(
-                          amount       => ( $purchase_invoice->amount / 3 * 2),
+  my $ap_transaction = new_ap_transaction();
+  $ap_transaction->pay_invoice(
+                          amount       => ( $ap_transaction->amount / 3 * 2),
                           payment_type => 'without_skonto',
                           chart_id     => $bank_account->chart_id,
-                          transdate    => DateTime->today_local->to_kivitendo
+                          transdate    => $transdate1,
                          );
-  my ($number_of_payments, $paid_amount) = number_of_payments($purchase_invoice->transactions);
-  my $total = total_amount($purchase_invoice->transactions);
-
-  my $title = 'default purchase_invoice, two charts, 19/7% tax multiple payments with final difference as skonto';
+  my ($number_of_payments, $paid_amount) = number_of_payments($ap_transaction);
+  my $total = total_amount($ap_transaction);
 
   is($paid_amount,         150.67,   "${title}: paid amount");
   is($number_of_payments,       1,   "${title}: 1 AP_paid bookings");
@@ -813,57 +829,59 @@ sub test_default_purchase_invoice_two_charts_19_7_tax_partial_unrounded_payment_
 };
 
 
-sub test_default_purchase_invoice_two_charts_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto() {
-  reset_state() if $ALWAYS_RESET;
+sub test_default_ap_transaction_two_charts_19_7_tax_without_skonto_multiple_payments_final_free_skonto {
+  my $title = 'default ap_transaction, two charts, 19/7% tax multiple payments with final free skonto';
 
-  my $purchase_invoice = new_purchase_invoice();
+  my $ap_transaction = new_ap_transaction();
 
   # pay 2/3 and 1/5, leaves 3.83% to be used as Skonto
-  $purchase_invoice->pay_invoice(
-                          amount       => ( $purchase_invoice->amount / 3 * 2),
+  $ap_transaction->pay_invoice(
+                          amount       => ( $ap_transaction->amount / 3 * 2),
                           payment_type => 'without_skonto',
                           chart_id     => $bank_account->chart_id,
-                          transdate    => DateTime->today_local->to_kivitendo
+                          transdate    => $transdate1,
                          );
-  $purchase_invoice->pay_invoice(
-                          amount       => ( $purchase_invoice->amount / 5 ),
+  $ap_transaction->pay_invoice(
+                          amount       => ( $ap_transaction->amount / 5 ),
                           payment_type => 'without_skonto',
                           chart_id     => $bank_account->chart_id,
-                          transdate    => DateTime->today_local->to_kivitendo
+                          transdate    => $transdate1,
                          );
-  $purchase_invoice->pay_invoice(
-                          payment_type => 'difference_as_skonto',
+  $ap_transaction->pay_invoice(
+                          payment_type => 'free_skonto',
+                          skonto_amount => $ap_transaction->open_amount,
+                          amount       => 0,
                           chart_id     => $bank_account->chart_id,
-                          transdate    => DateTime->today_local->to_kivitendo
+                          transdate    => $transdate1,
+                          bt_id        => $bt->id,
                          );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($purchase_invoice->transactions);
-  my $total = total_amount($purchase_invoice->transactions);
-
-  my $title = 'default purchase_invoice, two charts, 19/7% tax multiple payments with final difference as skonto';
+  my ($number_of_payments, $paid_amount) = number_of_payments($ap_transaction);
+  my $total = total_amount($ap_transaction);
 
   is($paid_amount,         226, "${title}: paid amount");
-  is($number_of_payments,    4, "${title}: 1 AP_paid bookings");
+  is($number_of_payments,    4, "${title}: 4 AP_paid bookings");
   is($total,                 0, "${title}: even balance");
 
 }
 
 # test
-sub test_default_invoice_two_items_19_7_tax_with_skonto_50_50() {
-  reset_state() if $ALWAYS_RESET;
+sub test_default_invoice_two_items_19_7_tax_with_skonto_50_50 {
+  my $title = 'default invoice, two items, 19/7% tax with_skonto_pt 50/50';
 
-  my $item1   = new_item(qty => 1, part => $parts[2]);
-  my $item2   = new_item(qty => 1, part => $parts[3]);
-  my $invoice = new_invoice(
+  my $item1   = create_invoice_item(part => $parts[2], qty => 1);
+  my $item2   = create_invoice_item(part => $parts[3], qty => 1);
+  my $invoice = create_sales_invoice(
     taxincluded  => 0,
+    transdate    => $transdate1,
     invoiceitems => [ $item1, $item2 ],
-    payment_id  => $payment_terms->id,
+    payment_id   => $payment_terms->id,
   );
-  $invoice->post;
 
   # default values
   my %params = ( chart_id => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+                 transdate => $transdate1,
+                 bt_id     => $bt->id,
                );
 
   $params{amount} = $invoice->amount_less_skonto;
@@ -871,10 +889,8 @@ sub test_default_invoice_two_items_19_7_tax_with_skonto_50_50() {
 
   $invoice->pay_invoice( %params );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
-  my $total = total_amount($invoice->transactions);
-
-  my $title = 'default invoice, two items, 19/7% tax with_skonto_pt 50/50';
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
 
   is($invoice->netamount,        100,     "${title}: netamount");
   is($invoice->amount,           113,     "${title}: amount");
@@ -885,23 +901,24 @@ sub test_default_invoice_two_items_19_7_tax_with_skonto_50_50() {
 }
 
 # test
-sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25() {
-  reset_state() if $ALWAYS_RESET;
-
-  my $item1   = new_item(qty => 0.5, part => $parts[2]);
-  my $item2   = new_item(qty => 0.5, part => $parts[3]);
-  my $item3   = new_item(qty => 0.5, part => $parts[2]);
-  my $item4   = new_item(qty => 0.5, part => $parts[3]);
-  my $invoice = new_invoice(
+sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25 {
+  my $title = 'default invoice, four items, 19/7% tax with_skonto_pt 4x25';
+
+  my $item1   = create_invoice_item(part => $parts[2], qty => 0.5);
+  my $item2   = create_invoice_item(part => $parts[3], qty => 0.5);
+  my $item3   = create_invoice_item(part => $parts[2], qty => 0.5);
+  my $item4   = create_invoice_item(part => $parts[3], qty => 0.5);
+  my $invoice = create_sales_invoice(
     taxincluded  => 0,
+    transdate    => $transdate1,
     invoiceitems => [ $item1, $item2, $item3, $item4 ],
-    payment_id  => $payment_terms->id,
+    payment_id   => $payment_terms->id,
   );
-  $invoice->post;
 
   # default values
   my %params = ( chart_id => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+                 transdate => $transdate1,
+                 bt_id     => $bt->id,
                );
 
   $params{amount} = $invoice->amount_less_skonto;
@@ -909,10 +926,8 @@ sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25() {
 
   $invoice->pay_invoice( %params );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
-  my $total = total_amount($invoice->transactions);
-
-  my $title = 'default invoice, four items, 19/7% tax with_skonto_pt 4x25';
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
 
   is($invoice->netamount , 100  , "${title}: netamount");
   is($invoice->amount    , 113  , "${title}: amount");
@@ -922,23 +937,24 @@ sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25() {
   is($total              , 0    , "${title}: even balance");
 }
 
-sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_tax_included() {
-  reset_state() if $ALWAYS_RESET;
+sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_tax_included {
+  my $title = 'default invoice, four items, 19/7% tax with_skonto_pt 4x25';
 
-  my $item1   = new_item(qty => 0.5, part => $parts[2]);
-  my $item2   = new_item(qty => 0.5, part => $parts[3]);
-  my $item3   = new_item(qty => 0.5, part => $parts[2]);
-  my $item4   = new_item(qty => 0.5, part => $parts[3]);
-  my $invoice = new_invoice(
+  my $item1   = create_invoice_item(part => $parts[2], qty => 0.5);
+  my $item2   = create_invoice_item(part => $parts[3], qty => 0.5);
+  my $item3   = create_invoice_item(part => $parts[2], qty => 0.5);
+  my $item4   = create_invoice_item(part => $parts[3], qty => 0.5);
+  my $invoice = create_sales_invoice(
     taxincluded  => 1,
+    transdate    => $transdate1,
     invoiceitems => [ $item1, $item2, $item3, $item4 ],
-    payment_id  => $payment_terms->id,
+    payment_id   => $payment_terms->id,
   );
-  $invoice->post;
 
   # default values
   my %params = ( chart_id => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+                 transdate => $transdate1,
+                 bt_id     => $bt->id,
                );
 
   $params{amount} = $invoice->amount_less_skonto;
@@ -946,49 +962,49 @@ sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_tax_included() {
 
   $invoice->pay_invoice( %params );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
-  my $total = total_amount($invoice->transactions);
-
-  my $title = 'default invoice, four items, 19/7% tax with_skonto_pt 4x25';
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
 
   is($invoice->netamount,   88.75,    "${title}: netamount");
   is($invoice->amount,        100,    "${title}: amount");
   is($paid_amount,           -100,    "${title}: paid amount");
   is($invoice->paid,          100,    "${title}: paid");
   is($number_of_payments,       3,    "${title}: 3 AR_paid bookings");
-  { local $TODO = "currently this test fails because the code writing the invoice is buggy, the calculation of skonto is correct";
+TODO: {
+  local $TODO = "currently this test fails because the code writing the invoice is buggy, the calculation of skonto is correct";
   is($total,                    0,    "${title}: even balance");
   }
 }
 
-sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_multiple() {
-  reset_state() if $ALWAYS_RESET;
+sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_multiple {
+  my $title = 'default invoice, four items, 19/7% tax with_skonto_pt 4x25';
 
-  my $item1   = new_item(qty => 0.5, part => $parts[2]);
-  my $item2   = new_item(qty => 0.5, part => $parts[3]);
-  my $item3   = new_item(qty => 0.5, part => $parts[2]);
-  my $item4   = new_item(qty => 0.5, part => $parts[3]);
-  my $invoice = new_invoice(
+  my $item1   = create_invoice_item(part => $parts[2], qty => 0.5);
+  my $item2   = create_invoice_item(part => $parts[3], qty => 0.5);
+  my $item3   = create_invoice_item(part => $parts[2], qty => 0.5);
+  my $item4   = create_invoice_item(part => $parts[3], qty => 0.5);
+  my $invoice = create_sales_invoice(
     taxincluded  => 0,
+    transdate    => $transdate1,
     invoiceitems => [ $item1, $item2, $item3, $item4 ],
-    payment_id  => $payment_terms->id,
+    payment_id   => $payment_terms->id,
   );
-  $invoice->post;
 
   $invoice->pay_invoice( amount       => '90',
                          payment_type => 'without_skonto',
                          chart_id     => $bank_account->chart_id,
-                         transdate => DateTime->today_local->to_kivitendo
+                         transdate => $transdate1,
                        );
-  $invoice->pay_invoice( payment_type => 'difference_as_skonto',
+  $invoice->pay_invoice( payment_type => 'free_skonto',
                          chart_id     => $bank_account->chart_id,
-                         transdate    => DateTime->today_local->to_kivitendo
+                         transdate    => $transdate1,
+                         bt_id        => $bt->id,
+                         skonto_amount => $invoice->open_amount,
+                         amount        => 0,
                        );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
-  my $total = total_amount($invoice->transactions);
-
-  my $title = 'default invoice, four items, 19/7% tax with_skonto_pt 4x25';
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
 
   is($invoice->netamount,  100,     "${title}: netamount");
   is($invoice->amount,     113,     "${title}: amount");
@@ -998,38 +1014,460 @@ sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_multiple() {
   is($total,                 0,     "${title}: even balance: this will fail due to rounding error in invoice post, not the skonto");
 }
 
-Support::TestSetup::login();
- # die;
+sub test_ar_currency_tax_not_included_and_payment {
+  my $title = 'test_ar_currency_tax_not_included_and_payment_2';
+
+  my $netamount = $::form->round_amount(75 * $exchangerate->sell,2); #  75 in CUR, 100.00 in EUR
+  my $amount    = $::form->round_amount($netamount * 1.19,2);        # 100 in CUR, 119.00 in EUR
+  my $invoice   = SL::DB::Invoice->new(
+      invoice      => 0,
+      amount       => $amount,
+      netamount    => $netamount,
+      transdate    => $transdate1,
+      taxincluded  => 0,
+      customer_id  => $customer->id,
+      taxzone_id   => $customer->taxzone_id,
+      currency_id  => $currency->id,
+      transactions => [],
+      notes        => 'test_ar_currency_tax_not_included_and_payment',
+  );
+  $invoice->add_ar_amount_row(
+    amount     => $invoice->netamount,
+    chart      => $ar_amount_chart,
+    tax_id     => $tax->id,
+  );
 
-# test cases: without_skonto
- test_default_invoice_one_item_19_without_skonto();
- test_default_invoice_two_items_19_7_tax_with_skonto();
- test_default_invoice_two_items_19_7_without_skonto();
- test_default_invoice_two_items_19_7_without_skonto_incomplete_payment();
- test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments();
- test_default_purchase_invoice_two_charts_19_7_without_skonto();
- test_default_purchase_invoice_two_charts_19_7_tax_partial_unrounded_payment_without_skonto();
- test_default_invoice_one_item_19_without_skonto_overpaid();
-
-# test cases: difference_as_skonto
- test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto();
- test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto_1cent();
- test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto_2cent();
- test_default_invoice_one_item_19_multiple_payment_final_difference_as_skonto();
- test_default_invoice_one_item_19_multiple_payment_final_difference_as_skonto_1cent();
- test_default_purchase_invoice_two_charts_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto();
+  $invoice->create_ar_row(chart => $ar_chart);
+  $invoice->save;
+
+  is(SL::DB::Manager::Invoice->get_all_count(where => [ invoice => 0 ]), 1, 'there is one ar transaction');
+  is($invoice->currency_id , $currency->id , 'currency_id has been saved');
+  is($invoice->netamount   , 100           , 'ar amount has been converted');
+  is($invoice->amount      , 119           , 'ar amount has been converted');
+  is($invoice->taxincluded ,   0           , 'ar transaction doesn\'t have taxincluded');
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ar_amount_chart->id, trans_id => $invoice->id)->amount, '100.00000', $ar_amount_chart->accno . ': has been converted for currency');
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ar_chart->id, trans_id => $invoice->id)->amount, '-119.00000', $ar_chart->accno . ': has been converted for currency');
+
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => 50,
+                        currency   => 'CUR',
+                        transdate  => $transdate1->to_kivitendo,
+                       );
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => 39.25,
+                        currency   => 'CUR',
+                        transdate  => $transdate1->to_kivitendo,
+                       );
+  # $invoice->pay_invoice(chart_id   => $bank->id,
+  #                       amount     => 30,
+  #                       transdate  => $transdate2->to_kivitendo,
+  #                      );
+  is(scalar @{$invoice->transactions}, 9, 'ar transaction has 9 transactions (incl. fxtransactions)');
+  is($invoice->paid, $invoice->amount, 'ar transaction paid = amount in default currency');
+};
 
-# test cases: with_skonto_pt
- test_default_invoice_two_items_19_7_tax_with_skonto_50_50();
- test_default_invoice_four_items_19_7_tax_with_skonto_4x_25();
- test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_multiple();
- test_default_purchase_invoice_two_charts_19_7_with_skonto();
- test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_tax_included();
- test_default_invoice_two_items_19_7_tax_with_skonto_tax_included();
+sub test_ar_currency_tax_included {
+  my $title = 'test_ar_currency_tax_included';
+
+  # we want the acc_trans amount to be 100
+  my $amount    = $::form->round_amount(75 * $exchangerate->sell * 1.19);
+  my $netamount = $::form->round_amount($amount / 1.19,2);
+  my $invoice = SL::DB::Invoice->new(
+      invoice      => 0,
+      amount       => 119,
+      netamount    => 100,
+      transdate    => $transdate1,
+      taxincluded  => 1,
+      customer_id  => $customer->id,
+      taxzone_id   => $customer->taxzone_id,
+      currency_id  => $currency->id,
+      notes        => 'test_ar_currency_tax_included',
+      transactions => [],
+  );
+  $invoice->add_ar_amount_row( # should take care of taxincluded
+    amount     => $invoice->amount, # tax included in local currency
+    chart      => $ar_amount_chart,
+    tax_id     => $tax->id,
+  );
 
-# remove all created data at end of test
-clear_up();
+  $invoice->create_ar_row( chart => $ar_chart );
+  $invoice->save;
+  is(SL::DB::Manager::Invoice->get_all_count(where => [ invoice => 0 ]), 2, 'there are now two ar transactions');
+  is($invoice->currency_id , $currency->id , 'currency_id has been saved');
+  is($invoice->amount      , $amount       , 'amount ok');
+  is($invoice->netamount   , $netamount    , 'netamount ok');
+  is($invoice->taxincluded , 1             , 'ar transaction has taxincluded');
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ar_amount_chart->id, trans_id => $invoice->id)->amount, '100.00000', $ar_amount_chart->accno . ': has been converted for currency');
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ar_chart->id, trans_id => $invoice->id)->amount, '-119.00000', $ar_chart->accno . ': has been converted for currency');
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => 89.25,
+                        currency   => 'CUR',
+                        transdate  => $transdate1->to_kivitendo,
+                       );
 
-done_testing();
+};
+
+sub test_ap_currency_tax_not_included_and_payment {
+  my $title = 'test_ap_currency_tax_not_included_and_payment';
+
+  my $netamount = $::form->round_amount(75 * $exchangerate->buy,2); #  75 in CUR, 100.00 in EUR
+  my $amount    = $::form->round_amount($netamount * 1.19,2);        # 100 in CUR, 119.00 in EUR
+  my $invoice   = SL::DB::PurchaseInvoice->new(
+      invoice      => 0,
+      invnumber    => 'test_ap_currency_tax_not_included_and_payment',
+      amount       => $amount,
+      netamount    => $netamount,
+      transdate    => $transdate1,
+      taxincluded  => 0,
+      vendor_id    => $vendor->id,
+      taxzone_id   => $vendor->taxzone_id,
+      currency_id  => $currency->id,
+      transactions => [],
+      notes        => 'test_ap_currency_tax_not_included_and_payment',
+  );
+  $invoice->add_ap_amount_row(
+    amount     => $invoice->netamount,
+    chart      => $ap_amount_chart,
+    tax_id     => $tax_9->id,
+  );
+
+  $invoice->create_ap_row(chart => $ap_chart);
+  $invoice->save;
+
+  is($invoice->currency_id, $currency->id, 'currency_id has been saved');
+  is($invoice->netamount, 100, 'ap amount has been converted');
+  is($invoice->amount, 119, 'ap amount has been converted');
+  is($invoice->taxincluded, 0, 'ap transaction doesn\'t have taxincluded');
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ap_amount_chart->id, trans_id => $invoice->id)->amount, '-100.00000', $ap_amount_chart->accno . ': has been converted for currency');
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ap_chart->id, trans_id => $invoice->id)->amount, '119.00000', $ap_chart->accno . ': has been converted for currency');
+
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => 50,
+                        currency   => 'CUR',
+                        transdate  => $transdate1->to_kivitendo,
+                       );
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => 39.25,
+                        currency   => 'CUR',
+                        transdate  => $transdate1->to_kivitendo,
+                       );
+  is(scalar @{$invoice->transactions}, 9, 'ap transaction has 9 transactions (incl. fxtransactions)');
+  is($invoice->paid, $invoice->amount, 'ap transaction paid = amount in default currency');
+};
+
+sub test_ap_currency_tax_included {
+  my $title = 'test_ap_currency_tax_included';
+
+  # we want the acc_trans amount to be 100
+  my $amount    = $::form->round_amount(75 * $exchangerate->buy * 1.19);
+  my $netamount = $::form->round_amount($amount / 1.19,2);
+  my $invoice = SL::DB::PurchaseInvoice->new(
+      invoice      => 0,
+      amount       => 119, #$amount,
+      netamount    => 100, #$netamount,
+      transdate    => $transdate1,
+      taxincluded  => 1,
+      vendor_id    => $vendor->id,
+      taxzone_id   => $vendor->taxzone_id,
+      currency_id  => $currency->id,
+      notes        => 'test_ap_currency_tax_included',
+      invnumber    => 'test_ap_currency_tax_included',
+      transactions => [],
+  );
+  $invoice->add_ap_amount_row( # should take care of taxincluded
+    amount     => $invoice->amount, # tax included in local currency
+    chart      => $ap_amount_chart,
+    tax_id     => $tax_9->id,
+  );
+
+  $invoice->create_ap_row( chart => $ap_chart );
+  $invoice->save;
+  is($invoice->currency_id , $currency->id , 'currency_id has been saved');
+  is($invoice->amount      , $amount       , 'amount ok');
+  is($invoice->netamount   , $netamount    , 'netamount ok');
+  is($invoice->taxincluded , 1             , 'ap transaction has taxincluded');
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ap_amount_chart->id, trans_id => $invoice->id)->amount, '-100.00000', $ap_amount_chart->accno . ': has been converted for currency');
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ap_chart->id, trans_id => $invoice->id)->amount, '119.00000', $ap_chart->accno . ': has been converted for currency');
+
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => 89.25,
+                        currency   => 'CUR',
+                        transdate  => $transdate1->to_kivitendo,
+                       );
+
+};
+
+sub test_ar_currency_tax_not_included_and_payment_2 {
+  my $title = 'test_ar_currency_tax_not_included_and_payment_2';
+
+  my $netamount = $::form->round_amount(125 * $exchangerate2->sell,2); # 125.00 in CUR, 100.00 in EUR
+  my $amount    = $::form->round_amount($netamount * 1.19,2);          # 148.75 in CUR, 119.00 in EUR
+  my $invoice   = SL::DB::Invoice->new(
+      invoice      => 0,
+      amount       => $amount,
+      netamount    => $netamount,
+      transdate    => $transdate2,
+      taxincluded  => 0,
+      customer_id  => $customer->id,
+      taxzone_id   => $customer->taxzone_id,
+      currency_id  => $currency->id,
+      transactions => [],
+      notes        => 'test_ar_currency_tax_not_included_and_payment 0.8',
+      invnumber    => 'test_ar_currency_tax_not_included_and_payment 0.8',
+  );
+  $invoice->add_ar_amount_row(
+    amount     => $invoice->netamount,
+    chart      => $ar_amount_chart,
+    tax_id     => $tax->id,
+  );
+
+  $invoice->create_ar_row(chart => $ar_chart);
+  $invoice->save;
+
+  is($invoice->currency_id , $currency->id , "$title: currency_id has been saved");
+  is($invoice->netamount   , 100           , "$title: ar amount has been converted");
+  is($invoice->amount      , 119           , "$title: ar amount has been converted");
+  is($invoice->taxincluded ,   0           , "$title: ar transaction doesn\"t have taxincluded");
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ar_amount_chart->id, trans_id => $invoice->id)->amount, '100.00000', $title . " " . $ar_amount_chart->accno . ": has been converted for currency");
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ar_chart->id, trans_id => $invoice->id)->amount, '-119.00000', $title  . " " . $ar_chart->accno . ': has been converted for currency');
+
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => 123.45,
+                        currency   => 'CUR',
+                        transdate  => $transdate2->to_kivitendo,
+                       );
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => 15.30,
+                        currency   => 'CUR',
+                        transdate  => $transdate3->to_kivitendo,
+                       );
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => 10.00,
+                        currency   => 'CUR',
+                        transdate  => $transdate4->to_kivitendo,
+                       );
+  # $invoice->pay_invoice(chart_id   => $bank->id,
+  #                       amount     => 30,
+  #                       transdate  => $transdate2->to_kivitendo,
+  #                      );
+  my $fx_transactions = SL::DB::Manager::AccTransaction->get_all(where => [ trans_id => $invoice->id, fx_transaction => 1 ], sort_by => ('acc_trans_id'));
+  is(scalar @{$fx_transactions}, 3, "$title: ar transaction has 3 fx transactions");
+  is($fx_transactions->[0]->amount, '24.69000', "$title fx transactions 1: 123.45-(123.45*0.8) = 24.69");
+
+  is(scalar @{$invoice->transactions}, 14, "$title ar transaction has 14 transactions (incl. fxtransactions and fx_gain)");
+  is($invoice->paid, $invoice->amount, "$title ar transaction paid = amount in default currency");
+};
+
+sub test_ar_currency_tax_not_included_and_payment_2_credit_note {
+  my $title = 'test_ar_currency_tax_not_included_and_payment_2_credit_note';
+
+  my $netamount = $::form->round_amount(-125 * $exchangerate2->sell,2); # 125.00 in CUR, 100.00 in EUR
+  my $amount    = $::form->round_amount($netamount * 1.19,2);          # 148.75 in CUR, 119.00 in EUR
+  my $invoice   = SL::DB::Invoice->new(
+      invoice      => 0,
+      amount       => $amount,
+      netamount    => $netamount,
+      transdate    => $transdate2,
+      taxincluded  => 0,
+      customer_id  => $customer->id,
+      taxzone_id   => $customer->taxzone_id,
+      currency_id  => $currency->id,
+      transactions => [],
+      notes        => 'test_ar_currency_tax_not_included_and_payment credit note 0.8',
+      invnumber    => 'test_ar_currency_tax_not_included_and_payment credit note 0.8',
+  );
+  $invoice->add_ar_amount_row(
+    amount     => $invoice->netamount,
+    chart      => $ar_amount_chart,
+    tax_id     => $tax->id,
+  );
+
+  $invoice->create_ar_row(chart => $ar_chart);
+  $invoice->save;
+
+  is($invoice->currency_id , $currency->id , 'currency_id has been saved');
+  is($invoice->netamount   , -100          , 'ar amount has been converted');
+  is($invoice->amount      , -119          , 'ar amount has been converted');
+  is($invoice->taxincluded ,   0           , 'ar transaction doesn\'t have taxincluded');
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ar_amount_chart->id, trans_id => $invoice->id)->amount, '-100.00000', $ar_amount_chart->accno . ': has been converted for currency');
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ar_chart->id, trans_id => $invoice->id)->amount, '119.00000', $ar_chart->accno . ': has been converted for currency');
+
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => -123.45,
+                        currency   => 'CUR',
+                        transdate  => $transdate2->to_kivitendo,
+                       );
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => -25.30,
+                        currency   => 'CUR',
+                        transdate  => $transdate2->to_kivitendo,
+                       );
+  my $fx_transactions = SL::DB::Manager::AccTransaction->get_all(where => [ trans_id => $invoice->id, fx_transaction => 1 ], sort_by => ('acc_trans_id'));
+  is(scalar @{$fx_transactions}, 2, 'ar transaction has 2 fx transactions');
+  is($fx_transactions->[0]->amount, '-24.69000', 'fx transactions 1: 123.45-(123.45*0.8) = 24.69');
+
+  is(scalar @{$invoice->transactions}, 9, 'ar transaction has 9 transactions (incl. fxtransactions)');
+  is($invoice->paid, $invoice->amount, 'ar transaction paid = amount in default currency');
+};
+
+sub test_ap_currency_tax_not_included_and_payment_2 {
+  my $title = 'test_ap_currency_tax_not_included_and_payment_2';
+
+  my $netamount = $::form->round_amount(125 * $exchangerate2->sell,2); # 125.00 in CUR, 100.00 in EUR
+  my $amount    = $::form->round_amount($netamount * 1.19,2);          # 148.75 in CUR, 119.00 in EUR
+  my $invoice   = SL::DB::PurchaseInvoice->new(
+      invoice      => 0,
+      amount       => $amount,
+      netamount    => $netamount,
+      transdate    => $transdate2,
+      taxincluded  => 0,
+      vendor_id    => $vendor->id,
+      taxzone_id   => $vendor->taxzone_id,
+      currency_id  => $currency->id,
+      transactions => [],
+      notes        => 'test_ap_currency_tax_not_included_and_payment_2 0.8 + 1.33333',
+      invnumber    => 'test_ap_currency_tax_not_included_and_payment_2 0.8 + 1.33333',
+  );
+  $invoice->add_ap_amount_row(
+    amount     => $invoice->netamount,
+    chart      => $ap_amount_chart,
+    tax_id     => $tax_9->id,
+  );
+
+  $invoice->create_ap_row(chart => $ap_chart);
+  $invoice->save;
+
+  is($invoice->currency_id , $currency->id , "$title: currency_id has been saved");
+  is($invoice->netamount   ,  100          , "$title: ap amount has been converted");
+  is($invoice->amount      ,  119          , "$title: ap amount has been converted");
+  is($invoice->taxincluded ,    0          , "$title: ap transaction doesn\'t have taxincluded");
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ap_amount_chart->id, trans_id => $invoice->id)->amount, '-100.00000', $ap_amount_chart->accno . ': has been converted for currency');
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ap_chart->id, trans_id => $invoice->id)->amount, '119.00000', $ap_chart->accno . ': has been converted for currency');
+
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => 10,
+                        currency   => 'CUR',
+                        transdate  => $transdate2->to_kivitendo,
+                       );
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => 123.45,
+                        currency   => 'CUR',
+                        transdate  => $transdate3->to_kivitendo,
+                       );
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => 15.30,
+                        currency   => 'CUR',
+                        transdate  => $transdate4->to_kivitendo,
+                       );
+  my $fx_transactions = SL::DB::Manager::AccTransaction->get_all(where => [ trans_id => $invoice->id, fx_transaction => 1 ], sort_by => ('acc_trans_id'));
+  is(scalar @{$fx_transactions}, 3, "$title: ap transaction has 3 fx transactions");
+  is($fx_transactions->[0]->amount,  '-2.00000', "$title: fx transaction 1:  10.00-( 10.00*0.80000) =   2.00000");
+  is($fx_transactions->[1]->amount,  '68.59000', "$title: fx transaction 2: 123.45-(123.45*1.55557) = -68.58511");
+  is($fx_transactions->[2]->amount,  '-3.40000', "$title: fx transaction 3:  15.30-(15.30 *0.77777) =   3.40012");
+
+  my $fx_loss_transactions = SL::DB::Manager::AccTransaction->get_all(where => [ trans_id => $invoice->id, chart_id => $fxloss_chart->id ], sort_by => ('acc_trans_id'));
+  my $fx_gain_transactions = SL::DB::Manager::AccTransaction->get_all(where => [ trans_id => $invoice->id, chart_id => $fxgain_chart->id ], sort_by => ('acc_trans_id'));
+  is($fx_gain_transactions->[0]->amount,   '0.34000', "$title: fx gain amount ok");
+  is($fx_loss_transactions->[0]->amount, '-93.28000', "$title: fx loss amount ok");
+
+  is(scalar @{$invoice->transactions}, 14, "$title: ap transaction has 14 transactions (incl. fxtransactions and gain_loss)");
+  is($invoice->paid, $invoice->amount, "$title: ap transaction paid = amount in default currency");
+  is(total_amount($invoice), 0,   "$title: even balance");
+};
+
+sub test_ap_currency_tax_not_included_and_payment_2_credit_note {
+  my $title = 'test_ap_currency_tax_not_included_and_payment_2_credit_note';
+
+  my $netamount = $::form->round_amount(-125 * $exchangerate2->sell,2); # 125.00 in CUR, 100.00 in EUR
+  my $amount    = $::form->round_amount($netamount * 1.19,2);          # 148.75 in CUR, 119.00 in EUR
+  my $invoice   = SL::DB::PurchaseInvoice->new(
+      invoice      => 0,
+      amount       => $amount,
+      netamount    => $netamount,
+      transdate    => $transdate2,
+      taxincluded  => 0,
+      vendor_id    => $vendor->id,
+      taxzone_id   => $vendor->taxzone_id,
+      currency_id  => $currency->id,
+      transactions => [],
+      notes        => 'test_ap_currency_tax_not_included_and_payment credit note 0.8 + 1.33333',
+      invnumber    => 'test_ap_currency_tax_not_included_and_payment credit note 0.8 + 1.33333',
+  );
+  $invoice->add_ap_amount_row(
+    amount     => $invoice->netamount,
+    chart      => $ap_amount_chart,
+    tax_id     => $tax_9->id,
+  );
+
+  $invoice->create_ap_row(chart => $ap_chart);
+  $invoice->save;
+
+  is($invoice->currency_id , $currency->id , "$title: currency_id has been saved");
+  is($invoice->netamount   , -100          , "$title: ap amount has been converted");
+  is($invoice->amount      , -119          , "$title: ap amount has been converted");
+  is($invoice->taxincluded ,   0           , "$title: ap transaction doesn\'t have taxincluded");
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ap_amount_chart->id, trans_id => $invoice->id)->amount, '100.00000', $ap_amount_chart->accno . ': has been converted for currency');
+  is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ap_chart->id, trans_id => $invoice->id)->amount, '-119.00000', $ap_chart->accno . ': has been converted for currency');
+
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => -10,
+                        currency   => 'CUR',
+                        transdate  => $transdate2->to_kivitendo,
+                       );
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => -123.45,
+                        currency   => 'CUR',
+                        transdate  => $transdate3->to_kivitendo,
+                       );
+  $invoice->pay_invoice(chart_id   => $bank->id,
+                        amount     => -15.30,
+                        currency   => 'CUR',
+                        transdate  => $transdate4->to_kivitendo,
+                       );
+  my $fx_transactions = SL::DB::Manager::AccTransaction->get_all(where => [ trans_id => $invoice->id, fx_transaction => 1 ], sort_by => ('acc_trans_id'));
+  is(scalar @{$fx_transactions}, 3, "$title: ap transaction has 3 fx transactions");
+  is($fx_transactions->[0]->amount,   '2.00000', "$title: fx transaction 1:  10.00-( 10.00*0.80000) =   2.00000");
+  is($fx_transactions->[1]->amount, '-68.59000', "$title: fx transaction 2: 123.45-(123.45*1.55557) = -68.58511");
+  is($fx_transactions->[2]->amount,   '3.40000', "$title: fx transaction 3:  15.30-(15.30 *0.77777) =   3.40012");
+
+  my $fx_gain_loss_transactions = SL::DB::Manager::AccTransaction->get_all(where => [ trans_id => $invoice->id, chart_id => $fxgain_chart->id ], sort_by => ('acc_trans_id'));
+  is($fx_gain_loss_transactions->[0]->amount, '93.28000', "$title: fx gain loss amount ok");
+
+  is(scalar @{$invoice->transactions}, 14, "$title: ap transaction has 14 transactions (incl. fxtransactions and gain_loss)");
+  is($invoice->paid, $invoice->amount, "$title: ap transaction paid = amount in default currency");
+  is(total_amount($invoice), 0,   "$title: even balance");
+};
+
+sub test_credit_note_two_items_19_7_tax_tax_not_included {
+  my $title = 'test_credit_note_two_items_19_7_tax_tax_not_included';
+
+  my $item1   = create_invoice_item(part => $parts[0], qty => 5);
+  my $item2   = create_invoice_item(part => $parts[1], qty => 3);
+  my $invoice = create_credit_note(
+    invnumber    => 'cn1',
+    transdate    => $transdate1,
+    taxincluded  => 0,
+    invoiceitems => [ $item1, $item2 ],
+  );
+
+  # default values
+  my %params = ( chart_id => $bank_account->chart_id,
+                 transdate => $transdate1,
+               );
+
+  $params{amount}       = $invoice->amount,
+
+  $invoice->pay_invoice( %params );
+
+  my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
+  my $total = total_amount($invoice);
+
+  is($invoice->netamount,        -40.84,   "${title}: netamount");
+  is($invoice->amount,           -45.10,   "${title}: amount");
+  is($paid_amount,                45.10,   "${title}: paid amount according to acc_trans is positive (Haben)");
+  is($invoice->paid,             -45.10,   "${title}: paid");
+  is($number_of_payments,             1,   "${title}: 1 AR_paid bookings");
+  is($total,                          0,   "${title}: even balance");
+}
 
 1;
index 9846264..a4f6ddc 100644 (file)
@@ -10,6 +10,7 @@ use Data::Dumper;
 use List::MoreUtils qw(uniq);
 use Support::TestSetup;
 use Test::Exception;
+use SL::Dev::ALL qw(:ALL);
 
 use SL::DB::Buchungsgruppe;
 use SL::DB::Currency;
@@ -22,11 +23,13 @@ use SL::DB::Part;
 use SL::DB::Unit;
 use SL::DB::TaxZone;
 
-my ($customer, $currency_id, @parts, $buchungsgruppe, $buchungsgruppe7, $unit, $employee, $tax, $tax7, $taxzone);
+my ($customer, @parts, $buchungsgruppe, $buchungsgruppe7, $unit, $employee, $tax, $tax7, $taxzone);
+my ($transdate);
 
 sub clear_up {
   SL::DB::Manager::Order->delete_all(all => 1);
   SL::DB::Manager::DeliveryOrder->delete_all(all => 1);
+  SL::DB::Manager::InvoiceItem->delete_all(all => 1);
   SL::DB::Manager::Invoice->delete_all(all => 1);
   SL::DB::Manager::Part->delete_all(all => 1);
   SL::DB::Manager::Customer->delete_all(all => 1);
@@ -47,17 +50,14 @@ sub reset_state {
   $tax7            = SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.07)                                              || croak "No tax for 7\%";
   $taxzone         = SL::DB::Manager::TaxZone->find_by( description => 'Inland')                                           || croak "No taxzone";
 
-  $currency_id     = $::instance_conf->get_currency_id;
-
-  $customer     = SL::DB::Customer->new(
+  $customer     = new_customer(
     name        => 'Test Customer',
-    currency_id => $currency_id,
     taxzone_id  => $taxzone->id,
     %{ $params{customer} }
   )->save;
 
   @parts = ();
-  push @parts, SL::DB::Part->new(
+  push @parts, new_part(
     partnumber         => 'T4254',
     description        => 'Fourty-two fifty-four',
     lastcost           => 1.93,
@@ -67,7 +67,7 @@ sub reset_state {
     %{ $params{part1} }
   )->save;
 
-  push @parts, SL::DB::Part->new(
+  push @parts, new_part(
     partnumber         => 'T0815',
     description        => 'Zero EIGHT fifteeN @ 7%',
     lastcost           => 5.473,
@@ -76,21 +76,34 @@ sub reset_state {
     unit               => $unit->name,
     %{ $params{part2} }
   )->save;
+
+  push @parts, new_part(
+    partnumber         => 'T888',
+    description        => 'Triple 8',
+    lastcost           => 0,
+    sellprice          => 0.6,
+    buchungsgruppen_id => $buchungsgruppe->id,
+    unit               => $unit->name,
+    %{ $params{part3} }
+  )->save;
+
 }
 
 sub new_invoice {
   my %params  = @_;
 
-  return SL::DB::Invoice->new(
-    customer_id => $customer->id,
-    currency_id => $currency_id,
-    employee_id => $employee->id,
-    salesman_id => $employee->id,
-    gldate      => DateTime->today_local->to_kivitendo,
+  return create_sales_invoice(
+    transdate   => $transdate,
+    taxzone_id  => $taxzone->id,
+    %params,
+  );
+}
+sub new_order {
+  my %params  = @_;
+
+  return create_sales_order(
+    transdate   => $transdate,
     taxzone_id  => $taxzone->id,
-    transdate   => DateTime->today_local->to_kivitendo,
-    invoice     => 1,
-    type        => 'invoice',
     %params,
   );
 }
@@ -100,12 +113,18 @@ sub new_item {
 
   my $part = delete($params{part}) || $parts[0];
 
-  return SL::DB::InvoiceItem->new(
-    parts_id    => $part->id,
-    lastcost    => $part->lastcost,
-    sellprice   => $part->sellprice,
-    description => $part->description,
-    unit        => $part->unit,
+  return create_invoice_item(
+    part => $part,
+    %params,
+  );
+}
+sub new_order_item {
+  my (%params) = @_;
+
+  my $part = delete($params{part}) || $parts[0];
+
+  return create_order_item(
+    part => $part,
     %params,
   );
 }
@@ -113,13 +132,13 @@ sub new_item {
 sub test_default_invoice_one_item_19_tax_not_included() {
   reset_state();
 
-  my $item    = new_item(qty => 2.5);
+  my $item = new_item(qty => 2.5);
   my $invoice = new_invoice(
     taxincluded  => 0,
     invoiceitems => [ $item ],
   );
 
-  my $taxkey = $item->part->get_taxkey(date => DateTime->today_local, is_sales => 1, taxzone => $invoice->taxzone_id);
+  my $taxkey = $item->part->get_taxkey(date => $transdate, is_sales => 1, taxzone => $invoice->taxzone_id);
 
   # sellprice 2.34 * qty 2.5 = 5.85
   # 19%(5.85) = 1.1115; rounded = 1.11
@@ -155,9 +174,12 @@ sub test_default_invoice_one_item_19_tax_not_included() {
       [],
     ],
     exchangerate                                 => 1,
-    taxes                                        => {
+    taxes_by_chart_id                            => {
       $tax->chart_id                             => 1.11,
     },
+    taxes_by_tax_id                              => {
+      $tax->id                                   => 1.1115,
+    },
     items                                        => [
       { linetotal                                => 5.85,
         linetotal_cost                           => 4.83,
@@ -166,6 +188,7 @@ sub test_default_invoice_one_item_19_tax_not_included() {
         taxkey_id                                => $taxkey->id,
       },
     ],
+    rounding                                    =>  0,
   }, "${title}: calculated data");
 }
 
@@ -179,8 +202,8 @@ sub test_default_invoice_two_items_19_7_tax_not_included() {
     invoiceitems => [ $item1, $item2 ],
   );
 
-  my $taxkey1 = $item1->part->get_taxkey(date => DateTime->today_local, is_sales => 1, taxzone => $invoice->taxzone_id);
-  my $taxkey2 = $item2->part->get_taxkey(date => DateTime->today_local, is_sales => 1, taxzone => $invoice->taxzone_id);
+  my $taxkey1 = $item1->part->get_taxkey(date => $transdate, is_sales => 1, taxzone => $invoice->taxzone_id);
+  my $taxkey2 = $item2->part->get_taxkey(date => $transdate, is_sales => 1, taxzone => $invoice->taxzone_id);
 
   # item 1:
   # sellprice 2.34 * qty 2.5 = 5.85
@@ -194,6 +217,7 @@ sub test_default_invoice_two_items_19_7_tax_not_included() {
   # item 2:
   # sellprice 9.714 * qty 1.2 = 11.6568 rounded 11.66
   # 7%(11.6568) = 0.815976; rounded = 0.82
+  # 7%(11.66)   = 0.8162
   # total rounded = 12.48
 
   # lastcost 5.473 * qty 1.2 = 6.5676; rounded 6.57
@@ -235,10 +259,14 @@ sub test_default_invoice_two_items_19_7_tax_not_included() {
       [], [],
     ],
     exchangerate                                  => 1,
-    taxes                                         => {
+    taxes_by_chart_id                             => {
       $tax->chart_id                              => 1.11,
       $tax7->chart_id                             => 0.82,
     },
+    taxes_by_tax_id                               => {
+      $tax->id                                    => 1.1115,
+      $tax7->id                                   => 0.8162,
+    },
     items                                        => [
       { linetotal                                => 5.85,
         linetotal_cost                           => 4.83,
@@ -253,6 +281,7 @@ sub test_default_invoice_two_items_19_7_tax_not_included() {
         taxkey_id                                => $taxkey2->id,
       },
     ],
+    rounding                                    =>  0,
   }, "${title}: calculated data");
 }
 
@@ -267,47 +296,37 @@ sub test_default_invoice_three_items_sellprice_rounding_discount() {
     invoiceitems => [ $item1, $item2, $item3 ],
   );
 
-  my %taxkeys = map { ($_->id => $_->get_taxkey(date => DateTime->today_local, is_sales => 1, taxzone => $invoice->taxzone_id)) } uniq map { $_->part } ($item1, $item2, $item3);
-
-  # this is how price_tax_calculator is implemented. It differs from
-  # the way sales_order / invoice - forms are calculating:
-  # linetotal = sellprice 5.55 * qty 1 * (1 - 0.05) = 5.2725; rounded 5.27
-  # linetotal = sellprice 5.50 * qty 1 * (1 - 0.05) = 5.225 rounded 5.23
-  # linetotal = sellprice 5.00 * qty 1 * (1 - 0.05) = 4.75; rounded 4.75
-  # ...
+  my %taxkeys = map { ($_->id => $_->get_taxkey(date => $transdate, is_sales => 1, taxzone => $invoice->taxzone_id)) } uniq map { $_->part } ($item1, $item2, $item3);
 
   # item 1:
   # discount = sellprice 5.55 * discount (0.05) = 0.2775; rounded 0.28
-  # sellprice = sellprice 5.55 - discount 0.28 = 5.27; rounded 5.27
-  # linetotal = sellprice 5.27 * qty 1 = 5.27; rounded 5.27
+  # linetotal = sellprice 5.55 * (1 - discount 0.05) * qty 1 = 5.2725; rounded 5.27
   # 19%(5.27) = 1.0013; rounded = 1.00
   # total rounded = 6.27
 
   # lastcost 1.93 * qty 1 = 1.93; rounded 1.93
-  # line marge_total = 3.34
+  # line marge_total = 5.27 - 1.93 = 3.34
   # line marge_percent = 63.3776091081594
 
   # item 2:
   # discount = sellprice 5.50 * discount 0.05 = 0.275; rounded 0.28
-  # sellprice = sellprice 5.50 - discount 0.28 = 5.22; rounded 5.22
-  # linetotal = sellprice 5.22 * qty 1 = 5.22; rounded 5.22
-  # 19%(5.22) = 0.9918; rounded = 0.99
-  # total rounded = 6.21
+  # linetotal = sellprice 5.50 * (1 - discount 0.05) * qty 1 = 5.225; rounded 5.23
+  # 19%(5.23) = .99370; rounded = 0.99
+  # total rounded = 6.22
 
   # lastcost 1.93 * qty 1 = 1.93; rounded 1.93
-  # line marge_total = 5.22 - 1.93 = 3.29
-  # line marge_percent = 3.29/5.22 = 0.630268199233716
+  # line marge_total = 5.23 - 1.93 = 3.30
+  # line marge_percent = 3.30/5.23 = 0.630975143403442
 
   # item 3:
-  # discount = sellprice 5.00 * discount 0.25 = 0.25; rounded 0.25
-  # sellprice = sellprice 5.00 - discount 0.25 = 4.75; rounded 4.75
-  # linetotal = sellprice 4.75 * qty 1 = 4.75; rounded 4.75
+  # discount = sellprice 5.00 * discount 0.05 = 0.05 = 0.25; rounded 0.25
+  # linetotal = sellprice 5.00 (1 - discount 0.05) * qty 1 = 4.75; rounded 4.75
   # 19%(4.75) = 0.9025; rounded = 0.90
   # total rounded = 5.65
 
   # lastcost 1.93 * qty 1 = 1.93; rounded 1.93
-  # line marge_total = 2.82
-  # line marge_percent = 59.3684210526316
+  # line marge_total = 4.75 - 1.93 = 2.82
+  # line marge_percent = 2.82/4.75 = 59.3684210526316
 
   my $title = 'default invoice, three items, sellprice, rounding, discount';
   my %data  = $invoice->calculate_prices_and_taxes;
@@ -316,29 +335,29 @@ sub test_default_invoice_three_items_sellprice_rounding_discount() {
   is($item1->marge_percent,      63.3776091081594,   "${title}: item1 marge_percent");
   is($item1->marge_price_factor, 1,                  "${title}: item1 marge_price_factor");
 
-  is($item2->marge_total,        3.29,               "${title}: item2 marge_total");
-  is($item2->marge_percent,      63.0268199233716,  "${title}: item2 marge_percent");
+  is($item2->marge_total,        3.30,               "${title}: item2 marge_total");
+  is($item2->marge_percent,      63.0975143403442,   "${title}: item2 marge_percent");
   is($item2->marge_price_factor, 1,                  "${title}: item2 marge_price_factor");
 
   is($item3->marge_total,        2.82,               "${title}: item3 marge_total");
   is($item3->marge_percent,      59.3684210526316,   "${title}: item3 marge_percent");
   is($item3->marge_price_factor, 1,                  "${title}: item3 marge_price_factor");
 
-  is($invoice->netamount,        5.27 + 5.22 + 4.75, "${title}: netamount");
+  is($invoice->netamount,        5.27 + 5.23 + 4.75, "${title}: netamount");
 
-  # 6.27 + 6.21 + 5.65 = 18.13
-  # 1.19*(5.27 + 5.22 + 4.75) = 18.1356; rounded 18.14
-  #is($invoice->amount,           6.27 + 6.21 + 5.65, "${title}: amount");
-  is($invoice->amount,           18.14,              "${title}: amount");
+  # 6.27 + 6.22 + 5.65 = 18.14
+  # 1.19*(5.27 + 5.23 + 4.75) = 18.1475; rounded 18.15
+  #is($invoice->amount,           6.27 + 6.22 + 5.65, "${title}: amount");
+  is($invoice->amount,           18.15,              "${title}: amount");
 
-  is($invoice->marge_total,      3.34 + 3.29 + 2.82, "${title}: marge_total");
-  is($invoice->marge_percent,    62.007874015748,    "${title}: marge_percent");
+  is($invoice->marge_total,      3.34 + 3.30 + 2.82, "${title}: marge_total");
+  is($invoice->marge_percent,    62.0327868852459,   "${title}: marge_percent");
 
   is_deeply(\%data, {
     allocated                                    => {},
     amounts                                      => {
       $buchungsgruppe->income_accno_id($taxzone) => {
-        amount                                   => 15.24,
+        amount                                   => 15.25,
         tax_id                                   => $tax->id,
         taxkey                                   => 3,
       },
@@ -348,9 +367,12 @@ sub test_default_invoice_three_items_sellprice_rounding_discount() {
       [], [], [],
     ],
     exchangerate                                 => 1,
-    taxes                                        => {
+    taxes_by_chart_id                            => {
       $tax->chart_id                             => 2.9,
     },
+    taxes_by_tax_id                              => {
+      $tax->id                                   => 2.89750,
+    },
     items                                        => [
       { linetotal                                => 5.27,
         linetotal_cost                           => 1.93,
@@ -358,10 +380,10 @@ sub test_default_invoice_three_items_sellprice_rounding_discount() {
         tax_amount                               => 1.0013,
         taxkey_id                                => $taxkeys{$item1->parts_id}->id,
       },
-      { linetotal                                => 5.22,
+      { linetotal                                => 5.23,
         linetotal_cost                           => 1.93,
-        sellprice                                => 5.22,
-        tax_amount                               => 0.9918,
+        sellprice                                => 5.23,
+        tax_amount                               => 0.9937,
         taxkey_id                                => $taxkeys{$item2->parts_id}->id,
       },
       { linetotal                                => 4.75,
@@ -371,14 +393,284 @@ sub test_default_invoice_three_items_sellprice_rounding_discount() {
         taxkey_id                                => $taxkeys{$item3->parts_id}->id,
       }
     ],
+    rounding                                    =>  0,
+  }, "${title}: calculated data");
+}
+
+sub test_default_invoice_one_item_19_tax_not_included_rounding_discount() {
+  reset_state();
+
+  my $item   = new_item(qty => 6, part => $parts[2], discount => 0.03);
+  my $invoice = new_invoice(
+    taxincluded  => 0,
+    invoiceitems => [ $item ],
+  );
+
+  my %taxkeys = map { ($_->id => $_->get_taxkey(date => $transdate, is_sales => 1, taxzone => $invoice->taxzone_id)) } uniq map { $_->part } ($item);
+
+  # 6 parts for 0.60 with 3% discount
+  #
+  # linetotal = sellprice 0.60 * qty 6 * discount (1 - 0.03) = 3.492 rounded 3.49
+  # total = 3.49 + 0.66 = 4.15
+  #
+
+  my $title = 'default invoice, one item, sellprice, rounding, discount';
+  my %data  = $invoice->calculate_prices_and_taxes;
+
+  is($invoice->netamount,         3.49,              "${title}: netamount");
+
+  is($invoice->amount,            4.15,              "${title}: amount");
+
+  is($invoice->marge_total,       3.49,              "${title}: marge_total");
+  is($invoice->marge_percent,      100,              "${title}: marge_percent");
+
+  is_deeply(\%data, {
+    allocated                                    => {},
+    amounts                                      => {
+      $buchungsgruppe->income_accno_id($taxzone) => {
+        amount                                   => 3.49,
+        tax_id                                   => $tax->id,
+        taxkey                                   => 3,
+      },
+    },
+    amounts_cogs                                 => {},
+    assembly_items                               => [
+      [],
+    ],
+    exchangerate                                 => 1,
+    taxes_by_chart_id                            => {
+      $tax->chart_id                             => 0.66,
+    },
+    taxes_by_tax_id                              => {
+      $tax->id                                   => 0.66310,
+    },
+    items                                        => [
+      { linetotal                                => 3.49,
+        linetotal_cost                           => 0,
+        sellprice                                => 0.58,
+        tax_amount                               => 0.6631,
+        taxkey_id                                => $taxkeys{$item->parts_id}->id,
+      },
+    ],
+    rounding                                     =>  0,
+  }, "${title}: calculated data");
+}
+
+sub test_default_invoice_one_item_19_tax_not_included_rounding_discount_huge_qty() {
+  reset_state();
+
+  my $item   = new_item(qty => 100000, part => $parts[2], discount => 0.03, sellprice => 0.10);
+  my $invoice = new_invoice(
+    taxincluded  => 0,
+    invoiceitems => [ $item ],
+  );
+
+  my %taxkeys = map { ($_->id => $_->get_taxkey(date => $transdate, is_sales => 1, taxzone => $invoice->taxzone_id)) } uniq map { $_->part } ($item);
+
+  my $title = 'default invoice, one item, 19% tax not included, rounding, discount, huge qty';
+  my %data  = $invoice->calculate_prices_and_taxes;
+
+  is($invoice->netamount,         9700,              "${title}: netamount");
+
+  is($invoice->amount,           11543,              "${title}: amount");
+
+  is($invoice->marge_total,       9700,              "${title}: marge_total");
+  is($invoice->marge_percent,      100,              "${title}: marge_percent");
+
+  is_deeply(\%data, {
+    allocated                                    => {},
+    amounts                                      => {
+      $buchungsgruppe->income_accno_id($taxzone) => {
+        amount                                   => 9700,
+        tax_id                                   => $tax->id,
+        taxkey                                   => 3,
+      },
+    },
+    amounts_cogs                                 => {},
+    assembly_items                               => [
+      [],
+    ],
+    exchangerate                                 => 1,
+    taxes_by_chart_id                            => {
+      $tax->chart_id                             => 1843,
+    },
+    taxes_by_tax_id                              => {
+      $tax->id                                   => 1843,
+    },
+    items                                        => [
+      { linetotal                                => 9700,
+        linetotal_cost                           => 0,
+        sellprice                                => 0.1,
+        tax_amount                               => 1843,
+        taxkey_id                                => $taxkeys{$item->parts_id}->id,
+      },
+    ],
+    rounding                                    =>  0,
+  }, "${title}: calculated data");
+}
+
+sub test_default_invoice_one_item_19_tax_not_included_rounding_discount_big_qty_low_sellprice() {
+  reset_state();
+
+  my $item   = new_item(qty => 10001, sellprice => 0.007, discount => 0.035);
+  my $invoice = new_invoice(
+    taxincluded  => 0,
+    invoiceitems => [ $item ],
+  );
+
+  my %taxkeys = map { ($_->id => $_->get_taxkey(date => $transdate, is_sales => 1, taxzone => $invoice->taxzone_id)) } uniq map { $_->part } ($item);
+
+  # item 1:
+  # discount = sellprice 0.007 * discount (0.035) = 0.000245; rounded 0.00
+  # sellprice = sellprice 0.007 - discount 0.00 = 0.007
+  # linetotal = sellprice 0.007 * qty 10001 * (1 - 0.035) = 67.556755; rounded 67.56
+  # 19%(67.56) = 12.8364; rounded = 12.84
+  # total rounded = 80.40
+
+  # lastcost 1.93 * qty 10001 = 19301.93; rounded 19301.93
+  # line marge_total = 67.56-19301.93 = -19234.37
+  # line marge_percent = 100*-19234.37/67.56 = -28470.0562462996
+
+  my $title = 'default invoice one item 19 tax not included rounding discount big qty low sellprice';
+  my %data  = $invoice->calculate_prices_and_taxes;
+
+  is($invoice->netamount,                 67.56,    "${title}: netamount");
+
+  is($invoice->amount,                    80.40,    "${title}: amount");
+
+  is($invoice->marge_total,           -19234.37,    "${title}: marge_total");
+  is($invoice->marge_percent, -28470.0562462996,    "${title}: marge_percent");
+
+  is_deeply(\%data, {
+    allocated                                    => {},
+    amounts                                      => {
+      $buchungsgruppe->income_accno_id($taxzone) => {
+        amount                                   => 67.56,
+        tax_id                                   => $tax->id,
+        taxkey                                   => 3,
+      },
+    },
+    amounts_cogs                                 => {},
+    assembly_items                               => [
+      [],
+    ],
+    exchangerate                                 => 1,
+    taxes_by_chart_id                            => {
+      $tax->chart_id                             => 12.84,
+    },
+    taxes_by_tax_id                              => {
+      $tax->id                                   => 12.8364,
+    },
+    items                                        => [
+      { linetotal                                => 67.56,
+        linetotal_cost                           => 19301.93,
+        sellprice                                => 0.007,
+        tax_amount                               => 12.8364,
+        taxkey_id                                => $taxkeys{$item->parts_id}->id,
+      },
+    ],
+    rounding                                    =>  0,
   }, "${title}: calculated data");
 }
+sub test_default_order_two_items_19_one_optional() {
+  reset_state();
+
+  my $item          = new_order_item(qty => 2.5);
+  my $item_optional = new_order_item(qty => 2.5, optional => 1);
+
+  my $order = new_order(
+    taxincluded  => 0,
+    orderitems => [ $item, $item_optional ],
+  );
+
+  my $taxkey = $item->part->get_taxkey(date => $transdate, is_sales => 1, taxzone => $order->taxzone_id);
+
+  # sellprice 2.34 * qty 2.5 = 5.85
+  # 19%(5.85) = 1.1115; rounded = 1.11
+  # total rounded = 6.96
+
+  # lastcost 1.93 * qty 2.5 = 4.825; rounded 4.83
+  # line marge_total = 1.02
+  # line marge_percent = 17.4358974358974
+
+  my $title = 'default order, two item, one item optional, 19% tax not included';
+  my %data  = $order->calculate_prices_and_taxes;
+
+  is($item->marge_total,        1.02,             "${title}: item marge_total");
+  is($item->marge_percent,      17.4358974358974, "${title}: item marge_percent");
+  is($item->marge_price_factor, 1,                "${title}: item marge_price_factor");
+
+  # optional items have a linetotal and marge, but ...
+  is($item_optional->marge_total,        1.02,             "${title}: item optional marge_total");
+  is($item_optional->marge_percent,      17.4358974358974, "${title}: item optional marge_percent");
+  is($item_optional->marge_price_factor, 1,                "${title}: item optional marge_price_factor");
+
+  # ... should not be calculated for the record sum
+  is($order->netamount,       5.85,             "${title}: netamount");
+  is($order->amount,          6.96,             "${title}: amount");
+  is($order->marge_total,     1.02,             "${title}: marge_total");
+  is($order->marge_percent,   17.4358974358974, "${title}: marge_percent");
+  is($order->orderitems->[1]->optional, 1,      "${title}: second order item has attribute optional");
+  # diag explain $order->orderitems->[1]->optional;
+  # diag explain \%data;
+  is_deeply(\%data, {
+    allocated                                    => {},
+    amounts                                      => {
+      $buchungsgruppe->income_accno_id($taxzone) => {
+        amount                                   => 5.85,
+        tax_id                                   => $tax->id,
+        taxkey                                   => 3,
+      },
+    },
+    amounts_cogs                                 => {},
+    assembly_items                               => [
+      [],
+      [],
+    ],
+    exchangerate                                 => 1,
+    taxes_by_chart_id                            => {
+      $tax->chart_id                             => 1.11,
+    },
+    taxes_by_tax_id                              => {
+      $tax->id                                   => 1.1115,
+    },
+    items                                        => [
+      { linetotal                                => 5.85,
+        linetotal_cost                           => 4.83,
+        sellprice                                => 2.34,
+        tax_amount                               => 1.1115,
+        taxkey_id                                => $taxkey->id,
+      },
+      { linetotal                                => 5.85,
+        linetotal_cost                           => 4.83,
+        sellprice                                => 2.34,
+        tax_amount                               => 1.1115,
+        taxkey_id                                => $taxkey->id,
+      },
+    ],
+    rounding                                    =>  0,
+  }, "${title}: calculated data");
+}
+
 
 Support::TestSetup::login();
 
+$transdate = DateTime->today_local;
+$transdate->set_year(2019) if $transdate->year == 2020; # use year 2019 in 2020, because of tax rate change in Germany
+
 test_default_invoice_one_item_19_tax_not_included();
 test_default_invoice_two_items_19_7_tax_not_included();
 test_default_invoice_three_items_sellprice_rounding_discount();
+test_default_invoice_one_item_19_tax_not_included_rounding_discount();
+test_default_invoice_one_item_19_tax_not_included_rounding_discount_huge_qty();
+test_default_invoice_one_item_19_tax_not_included_rounding_discount_big_qty_low_sellprice();
+test_default_order_two_items_19_one_optional();
 
 clear_up();
 done_testing();
+
+# vim: ft=perl
+# set emacs to perl mode
+# Local Variables:
+# mode: perl
+# End:
index 5df89a4..2c7ea4d 100644 (file)
@@ -1,4 +1,4 @@
-use Test::More tests => 49;
+use Test::More tests => 66;
 
 use strict;
 
@@ -9,6 +9,7 @@ use Carp;
 use Data::Dumper;
 use Support::TestSetup;
 use Test::Exception;
+use Test::Deep qw(cmp_bag);
 use List::Util qw(max);
 
 use SL::DB::Buchungsgruppe;
@@ -18,6 +19,7 @@ use SL::DB::Employee;
 use SL::DB::Invoice;
 use SL::DB::Order;
 use SL::DB::DeliveryOrder;
+use SL::DB::DeliveryOrder::TypeData qw(:types);
 use SL::DB::Part;
 use SL::DB::Unit;
 use SL::DB::TaxZone;
@@ -85,6 +87,7 @@ sub new_delivery_order {
     employee_id => $employee->id,
     salesman_id => $employee->id,
     taxzone_id  => $taxzone->id,
+    order_type => SALES_DELIVERY_ORDER_TYPE,
     %params,
   )->save;
 }
@@ -318,6 +321,50 @@ is @$links, 3, 'recursive from i finds 3 (not i)';
 $links = $o1->linked_records(direction => 'both', recursive => 1, save_path => 1);
 is @$links, 4, 'recursive dir=both does not give duplicates';
 
+
+# test batch mode
+#
+#
+#
+
+reset_state();
+
+$o1 = new_order();
+$o2 = new_order();
+my $i1 = new_invoice();
+my $i2 = new_invoice();
+
+$o1->link_to_record($i1);
+$o2->link_to_record($i2);
+
+$links = $o1->linked_records(direction => 'to', to => 'Invoice', batch => [ $o1->id, $o2->id ]);
+is_deeply [ map { $_->id } @$links ], [ $i1->id , $i2->id ], "batch works";
+
+$links = $o1->linked_records(direction => 'to', recursive => 1, batch => [ $o1->id, $o2->id ]);
+cmp_bag [ map { $_->id } @$links ], [ $i1->id , $i2->id ], "batch works recursive";
+
+$links = $o1->linked_records(direction => 'to', to => 'Invoice', batch => [ $o1->id, $o2->id ], by_id => 1);
+# $::lxdebug->dump(0,  "links", $links);
+is @{ $links->{$o1->id} }, 1, "batch by_id 1";
+is @{ $links->{$o2->id} }, 1, "batch by_id 2";
+is keys %$links, 2, "batch by_id 3";
+is $links->{$o1->id}[0]->id, $i1->id, "batch by_id 4";
+is $links->{$o2->id}[0]->id, $i2->id, "batch by_id 5";
+
+$links = $o1->linked_records(direction => 'to', recursive => 1, batch => [ $o1->id, $o2->id ], by_id => 1);
+is @{ $links->{$o1->id} }, 1, "batch recursive by_id 1";
+is @{ $links->{$o2->id} }, 1, "batch recursive by_id 2";
+is keys %$links, 2, "batch recursive by_id 3";
+is $links->{$o1->id}[0]->id, $i1->id, "batch recursive by_id 4";
+is $links->{$o2->id}[0]->id, $i2->id, "batch recursive by_id 5";
+
+$links = $o1->linked_records(direction => 'both', recursive => 1, batch => [ $o1->id, $o2->id ], by_id => 1);
+is @{ $links->{$o1->id} }, 1, "batch recursive by_id direction both 1";
+is @{ $links->{$o2->id} }, 1, "batch recursive by_id direction both 2";
+is keys %$links, 2, "batch recursive by_id direction both 3";
+is $links->{$o1->id}[0]->id, $i1->id, "batch recursive by_id direction both 4";
+is $links->{$o2->id}[0]->id, $i2->id, "batch recursive by_id direction both 5";
+
 clear_up();
 
 1;
diff --git a/t/db_helper/with_transaction.t b/t/db_helper/with_transaction.t
new file mode 100644 (file)
index 0000000..cc3fc0e
--- /dev/null
@@ -0,0 +1,244 @@
+use Test::More tests => 17;
+use Test::Exception;
+
+use strict;
+
+use lib 't';
+use utf8;
+
+use Carp;
+use Data::Dumper;
+use Support::TestSetup;
+use SL::DB::Part;
+use SL::Dev::Part qw(new_part);
+
+Support::TestSetup::login();
+
+SL::DB::Manager::Part->delete_all(all => 1, cascade => 1);
+
+# silence the Test::Harness warn handler
+local $SIG{__WARN__} = sub {};
+
+# test simple transaction
+
+my $part = new_part();
+SL::DB->client->with_transaction(sub {
+  $part->save;
+  ok 1, 'part saved';
+  1;
+}) or do {
+  ok 0, 'error saving part';
+};
+
+# test failing transaction
+my $part2 = new_part(partnumber => $part->partnumber); # woops, duplicate partnumber
+SL::DB->client->with_transaction(sub {
+  $part2->save;
+  ok 0, 'part saved';
+  1;
+}) or do {
+  ok 1, 'saving part with duplicate partnumber generates graceful error';
+};
+
+# test transaction with run time exception
+dies_ok {
+  SL::DB->client->with_transaction(sub {
+    $part->method_that_does_not_exist;
+    ok 0, 'this should have died';
+    1;
+  }) or do {
+    ok 0, 'this should not get here';
+  };
+} 'method not found in transaction died as expect';
+
+# test transaction with hook error
+# TODO - not possible to test without locally adding hooks in run time
+
+# test if error gets correctly stored in db->error
+$part2 = new_part(partnumber => $part->partnumber); # woops, duplicate partnumber
+SL::DB->client->with_transaction(sub {
+  $part2->save;
+  ok 0, 'part saved';
+  1;
+}) or do {
+  like(SL::DB->client->error, qr/unique.constraint/i, 'error is in db->error');
+};
+
+# test stacked transactions
+# 1. test that it works
+SL::DB->client->with_transaction(sub {
+  $part->sellprice(1);
+  $part->save;
+
+  SL::DB->client->with_transaction(sub {
+    $part->sellprice(2);
+    $part->save;
+  }) or do {
+    ok 0, 'error saving part';
+  };
+
+  $part->sellprice(3);
+  $part->save;
+  1;
+}) or do {
+  ok 0, 'error saving part';
+};
+
+$part->load;
+is $part->sellprice, "3.00000", 'part saved';
+
+# 2. with a transaction rollback
+SL::DB->client->with_transaction(sub {
+  $part->sellprice(1);
+  $part2->save;
+  $part->save;
+
+  SL::DB->client->with_transaction(sub {
+    $part->sellprice(2);
+    $part->save;
+  }) or do {
+    ok 0, 'should not get here';
+  };
+
+  $part->sellprice(3);
+  $part->save;
+  ok 0, 'should not get here';
+  1;
+}) or do {
+  ok 1, 'sql error skips rest of the transaction';
+};
+
+
+SL::DB->client->with_transaction(sub {
+  $part->sellprice(1);
+  $part->save;
+
+  SL::DB->client->with_transaction(sub {
+    $part->sellprice(2);
+    $part->save;
+    $part2->save;
+  }) or do {
+    ok 0, 'should not get here';
+  };
+
+  $part->sellprice(3);
+  $part->save;
+  ok 0, 'should not get here';
+  1;
+}) or do {
+  ok 1, 'sql error in nested transaction rolls back';
+  like(SL::DB->client->error, qr/unique.constraint/i, 'error from nested transaction is in db->error');
+};
+
+$part->load;
+is $part->sellprice, "3.00000", 'saved part is not affected';
+
+
+
+SL::DB->client->with_transaction(sub {
+  $part->sellprice(1);
+  $part->save;
+
+  SL::DB->client->with_transaction(sub {
+    $part->sellprice(2);
+    $part->save;
+  }) or do {
+    ok 0, 'should not get here';
+  };
+
+  $part->sellprice(4);
+  $part->save;
+  $part2->save;
+  ok 0, 'should not get here';
+  1;
+}) or do {
+  ok 1, 'sql error after nested transaction rolls back';
+};
+
+$part->load;
+is $part->sellprice, "3.00000", 'saved part is not affected';
+
+eval {
+  SL::DB->client->with_transaction(sub {
+    $part->sellprice(1);
+    $part->not_existing_function();
+    $part->save;
+
+    SL::DB->client->with_transaction(sub {
+      $part->sellprice(2);
+      $part->save;
+    }) or do {
+      ok 0, 'should not get here';
+    };
+
+    $part->sellprice(4);
+    $part->save;
+    ok 0, 'should not get here';
+    1;
+  }) or do {
+    ok 0, 'should not get here';
+  };
+  1;
+} or do {
+  ok 1, 'runtime exception error before nested transaction rolls back';
+};
+
+$part->load;
+is $part->sellprice, "3.00000", 'saved part is not affected';
+
+eval {
+  SL::DB->client->with_transaction(sub {
+    $part->sellprice(1);
+    $part->save;
+
+    SL::DB->client->with_transaction(sub {
+      $part->sellprice(2);
+      $part->not_existing_function();
+      $part->save;
+    }) or do {
+      ok 0, 'should not get here';
+    };
+
+    $part->sellprice(4);
+    $part->save;
+    ok 0, 'should not get here';
+    1;
+  }) or do {
+    ok 0, 'should not get here';
+  };
+  1;
+} or do {
+  ok 1, 'runtime exception error in nested transaction rolls back';
+};
+
+$part->load;
+is $part->sellprice, "3.00000", 'saved part is not affected';
+
+
+eval {
+  SL::DB->client->with_transaction(sub {
+    $part->sellprice(1);
+    $part->save;
+
+    SL::DB->client->with_transaction(sub {
+      $part->sellprice(2);
+      $part->save;
+    }) or do {
+      ok 0, 'should not get here';
+    };
+
+    $part->sellprice(4);
+    $part->save;
+    $part->not_existing_function();
+    ok 0, 'should not get here';
+    1;
+  }) or do {
+    ok 0, 'should not get here';
+  };
+  1;
+} or do {
+  ok 1, 'runtime exception error after nested transaction rolls back';
+};
+
+$part->load;
+is $part->sellprice, "3.00000", 'saved part is not affected';
diff --git a/t/file/filesystem.t b/t/file/filesystem.t
new file mode 100644 (file)
index 0000000..05e3284
--- /dev/null
@@ -0,0 +1,118 @@
+use strict;
+use Test::More tests => 14;
+
+use lib 't';
+
+use File::Temp;
+use Support::TestSetup;
+use Test::Exception;
+use SL::File;
+use SL::Dev::File qw(create_uploaded create_scanned create_created);
+
+Support::TestSetup::login();
+
+my $temp_dir    = File::Temp::tempdir("kivi-t-file-filesystem.XXXXXX", TMPDIR => 1, CLEANUP => 1);
+my $storage_dir = "$temp_dir/storage";
+
+my %common_params = (
+  object_id   => 1,
+  object_type => 'sales_order',
+);
+
+mkdir($storage_dir) || die $!;
+{
+local $::lx_office_conf{paths}->{document_path} = $storage_dir;
+$::instance_conf->data;
+local $::instance_conf->{data}{doc_files} = 1;
+
+my $scanner_file = "${temp_dir}/f2";
+
+clear_up();
+
+note('testing SL::File');
+
+my $file1 = create_uploaded( %common_params, file_name => 'file1', file_contents => 'content1 uploaded' );
+my $file2 = create_scanned(  %common_params, file_name => 'file2', file_contents => 'content2 scanned', file_path => $scanner_file );
+my $file3 = create_created(  %common_params, file_name => 'file3', file_contents => 'content3 created'    );
+my $file4 = create_created(  %common_params, file_name => 'file3', file_contents => 'content3 new version');
+
+is( SL::File->get_all_count(%common_params), 3, "3 files were created");
+ok( $file1->file_name              eq 'file1' , "file1 has correct name");
+my $content1 = $file1->get_content;
+ok( $$content1 eq 'content1 uploaded'         , "file1 has correct content");
+
+is( -f $scanner_file ? 1 : 0,                0, "scanned document was moved from scanner");
+
+$file2->delete;
+is( -f $scanner_file ? 1 : 0,                1, "scanned document was moved back to scanner");
+my $content2 = File::Slurp::read_file($scanner_file);
+ok( $content2 eq 'content2 scanned'           , "scanned file has correct content");
+
+my @file5 = SL::File->get_all(%common_params, file_name => 'file3');
+is( scalar @file5,                           1, "get_all file3: one currnt file found");
+my $content5 = $file5[0]->get_content();
+ok( $$content5 eq 'content3 new version'      , "file has correct current content");
+
+my @file6 = SL::File->get_all_versions(%common_params, file_name => 'file3');
+is( scalar @file6 ,                           2, "file3: two file versions found");
+my $content6 = $file6[0]->get_content;
+ok( $$content6 eq 'content3 new version'      , "file has correct current content");
+$content6 = $file6[1]->get_content;
+ok( $$content6 eq 'content3 created'          , "file has correct old content");
+
+note('testing controller');
+my $output;
+open(my $outputFH, '>', \$output) or die; # This shouldn't fail
+my $oldFH = select $outputFH;
+
+$::form->{id} = $file1->id;
+use SL::Controller::File;
+SL::Controller::File->action_download();
+
+select $oldFH;
+close $outputFH;
+my @lines = split "\n" , $output;
+ok($lines[4] eq 'content1 uploaded', "controller download has correct content");
+
+#some controller checks
+$::form = Support::TestSetup->create_new_form;
+$::form->{object_id}   = 12345678;
+$::form->{object_type} = undef;
+my $result='xx1';
+eval {
+  SL::Controller::File->check_object_params();
+  $result = 'yy1';
+  1;
+} or do {
+  $result = $@;
+};
+is(substr($result,0,14), "No object type", "controller error response 'No object type' ok");
+
+$::form = Support::TestSetup->create_new_form;
+$::form->{object_type} = 'sales_order';
+$::form->{file_type}   = '';
+
+$result='xx2';
+eval {
+  SL::Controller::File->check_object_params();
+  $result='yy2';
+  1;
+} or do {
+  $result=$@;
+};
+is(substr($result,0,12), "No file type", "controller error response 'No file type' ok");
+
+sub clear_up {
+  # Cleaning up may fail.
+  eval {
+    SL::File->delete_all(%common_params);
+    unlink($scanner_file);
+  };
+}
+
+}
+
+clear_up();
+done_testing;
+
+1;
diff --git a/t/gl/gl.t b/t/gl/gl.t
new file mode 100644 (file)
index 0000000..ab08ee5
--- /dev/null
+++ b/t/gl/gl.t
@@ -0,0 +1,402 @@
+use strict;
+use Test::More tests => 8;
+
+use lib 't';
+use Support::TestSetup;
+use Carp;
+use Test::Exception;
+use SL::DB::Chart;
+use SL::DB::TaxKey;
+use SL::DB::GLTransaction;
+use Data::Dumper;
+use SL::DBUtils qw(selectall_hashref_query);
+
+Support::TestSetup::login();
+
+clear_up();
+
+my $cash           = SL::DB::Manager::Chart->find_by( description => 'Kasse'          );
+my $bank           = SL::DB::Manager::Chart->find_by( description => 'Bank'           );
+my $betriebsbedarf = SL::DB::Manager::Chart->find_by( description => 'Betriebsbedarf' );
+
+my $tax_9 = SL::DB::Manager::Tax->find_by(taxkey => 9, rate => 0.19);
+my $tax_8 = SL::DB::Manager::Tax->find_by(taxkey => 8, rate => 0.07);
+my $tax_0 = SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.00);
+
+my $dbh = SL::DB->client->dbh;
+
+# example with chaining of add_chart_booking
+my $gl_transaction = SL::DB::GLTransaction->new(
+  taxincluded => 1,
+  reference   => 'bank/cash',
+  description => 'bank/cash',
+  transdate   => DateTime->today_local,
+)->add_chart_booking(
+  chart  => $cash,
+  credit => 100,
+  tax_id => $tax_0->id,
+)->add_chart_booking(
+  chart  => $bank,
+  debit  => 100,
+  tax_id => $tax_0->id,
+)->post;
+
+# example where bookings is prepared separately as an arrayref
+my $gl_transaction_2 = SL::DB::GLTransaction->new(
+  reference   => 'betriebsbedarf several rows',
+  description => 'betriebsbedarf',
+  taxincluded => 1,
+  transdate   => DateTime->today_local,
+);
+
+my $bookings = [
+                {
+                  chart  => $betriebsbedarf,
+                  memo   => 'foo 1',
+                  source => 'foo 1',
+                  debit  => 119,
+                  tax_id => $tax_9->id,
+                },
+                {
+                  chart  => $betriebsbedarf,
+                  memo   => 'foo 2',
+                  source => 'foo 2',
+                  debit  => 119,
+                  tax_id => $tax_9->id,
+                },
+                {
+                  chart  => $cash,
+                  credit => 238,
+                  memo   => 'foo 1+2',
+                  source => 'foo 1+2',
+                  tax_id => $tax_0->id,
+                },
+               ];
+$gl_transaction_2->add_chart_booking(%{$_}) foreach @{ $bookings };
+$gl_transaction_2->post;
+
+
+# example where add_chart_booking is called via a foreach
+my $gl_transaction_3 = SL::DB::GLTransaction->new(
+  reference   => 'betriebsbedarf tax included',
+  description => 'bar',
+  taxincluded => 1,
+  transdate   => DateTime->today_local,
+);
+$gl_transaction_3->add_chart_booking(%{$_}) foreach (
+    {
+      chart  => $betriebsbedarf,
+      debit  => 119,
+      tax_id => $tax_9->id,
+    },
+    {
+      chart  => $betriebsbedarf,
+      debit  => 107,
+      tax_id => $tax_8->id,
+    },
+    {
+      chart  => $betriebsbedarf,
+      debit  => 100,
+      tax_id => $tax_0->id,
+    },
+    {
+      chart  => $cash,
+      credit => 326,
+      tax_id => $tax_0->id,
+    },
+);
+$gl_transaction_3->post;
+
+my $gl_transaction_4 = SL::DB::GLTransaction->new(
+  reference   => 'betriebsbedarf tax not included',
+  description => 'bar',
+  taxincluded => 0,
+  transdate   => DateTime->today_local,
+);
+$gl_transaction_4->add_chart_booking(%{$_}) foreach (
+    {
+      chart  => $betriebsbedarf,
+      debit  => 100,
+      tax_id => $tax_9->id,
+    },
+    {
+      chart  => $betriebsbedarf,
+      debit  => 100,
+      tax_id => $tax_8->id,
+    },
+    {
+      chart  => $betriebsbedarf,
+      debit  => 100,
+      tax_id => $tax_0->id,
+    },
+    {
+      chart  => $cash,
+      credit => 326,
+      tax_id => $tax_0->id,
+    },
+);
+$gl_transaction_4->post;
+
+is(SL::DB::Manager::GLTransaction->get_all_count(), 4, "gl transactions created ok");
+
+is_deeply(&get_account_balances,
+          [
+            {
+              'accno' => '1000',
+              'sum' => '990.00000'
+            },
+            {
+              'accno' => '1200',
+              'sum' => '-100.00000'
+            },
+            {
+              'accno' => '1571',
+              'sum' => '-14.00000'
+            },
+            {
+              'accno' => '1576',
+              'sum' => '-76.00000'
+            },
+            {
+              'accno' => '4980',
+              'sum' => '-800.00000'
+            }
+          ],
+          "chart balances ok"
+         );
+
+
+note('testing subcent');
+
+my $gl_transaction_5_taxinc = SL::DB::GLTransaction->new(
+  taxincluded => 1,
+  reference   => 'subcent tax included',
+  description => 'subcent tax included',
+  transdate   => DateTime->today_local,
+)->add_chart_booking(
+  chart  => $betriebsbedarf,
+  debit  => 0.02,
+  tax_id => $tax_9->id,
+)->add_chart_booking(
+  chart  => $cash,
+  credit => 0.02,
+  tax_id => $tax_0->id,
+)->post;
+
+my $gl_transaction_5_taxnoinc = SL::DB::GLTransaction->new(
+  taxincluded => 0,
+  reference   => 'subcent tax not included',
+  description => 'subcent tax not included',
+  transdate   => DateTime->today_local,
+)->add_chart_booking(
+  chart  => $betriebsbedarf,
+  debit  => 0.02,
+  tax_id => $tax_9->id,
+)->add_chart_booking(
+  chart  => $cash,
+  credit => 0.02,
+  tax_id => $tax_0->id,
+)->post;
+
+my $gl_transaction_6_taxinc = SL::DB::GLTransaction->new(
+  taxincluded => 1,
+  reference   => 'cent tax included',
+  description => 'cent tax included',
+  transdate   => DateTime->today_local,
+)->add_chart_booking(
+  chart  => $betriebsbedarf,
+  debit  => 0.05,
+  tax_id => $tax_9->id,
+)->add_chart_booking(
+  chart  => $cash,
+  credit => 0.05,
+  tax_id => $tax_0->id,
+)->post;
+
+my $gl_transaction_6_taxnoinc = SL::DB::GLTransaction->new(
+  taxincluded => 0,
+  reference   => 'cent tax included',
+  description => 'cent tax included',
+  transdate   => DateTime->today_local,
+)->add_chart_booking(
+  chart  => $betriebsbedarf,
+  debit  => 0.04,
+  tax_id => $tax_9->id,
+)->add_chart_booking(
+  chart  => $cash,
+  credit => 0.05,
+  tax_id => $tax_0->id,
+)->post;
+
+is(SL::DB::Manager::GLTransaction->get_all_count(), 8, "gl transactions created ok");
+
+
+is_deeply(&get_account_balances,
+          [
+            {
+              'accno' => '1000',
+              'sum' => '990.14000'
+            },
+            {
+              'accno' => '1200',
+              'sum' => '-100.00000'
+            },
+            {
+              'accno' => '1571',
+              'sum' => '-14.00000'
+            },
+            {
+              'accno' => '1576',
+              'sum' => '-76.02000'
+            },
+            {
+              'accno' => '4980',
+              'sum' => '-800.12000'
+            }
+          ],
+          "chart balances ok"
+         );
+
+note "testing automatic tax 19%";
+
+my $gl_transaction_7 = SL::DB::GLTransaction->new(
+  reference   => 'betriebsbedarf tax not included',
+  description => 'bar',
+  taxincluded => 0,
+  transdate   => DateTime->new(year => 2019, month => 12, day => 30),
+);
+
+$gl_transaction_7->add_chart_booking(%{$_}) foreach (
+    {
+      chart  => $betriebsbedarf,
+      debit  => 100,
+    },
+    {
+      chart  => $betriebsbedarf,
+      debit  => 100,
+    },
+    {
+      chart  => $betriebsbedarf,
+      debit  => 100,
+      tax_id => $tax_0->id,
+    },
+    {
+      chart  => $cash,
+      credit => 338,
+    },
+);
+$gl_transaction_7->post;
+
+is(SL::DB::Manager::GLTransaction->get_all_count(), 9, "gl transactions created ok");
+is_deeply(&get_account_balances,
+          [
+            {
+              'accno' => '1000',
+              'sum' => '1328.14000'
+            },
+            {
+              'accno' => '1200',
+              'sum' => '-100.00000'
+            },
+            {
+              'accno' => '1571',
+              'sum' => '-14.00000'
+            },
+            {
+              'accno' => '1576',
+              'sum' => '-114.02000'
+            },
+            {
+              'accno' => '4980',
+              'sum' => '-1100.12000'
+            }
+          ],
+          "chart balances ok"
+         );
+
+note "testing automatic tax 16%";
+
+my $gl_transaction_8 = SL::DB::GLTransaction->new(
+  reference   => 'betriebsbedarf tax not included',
+  description => 'bar',
+  taxincluded => 0,
+  transdate   => DateTime->new(year => 2020, month => 12, day => 31),
+);
+
+$gl_transaction_8->add_chart_booking(%{$_}) foreach (
+    {
+      chart  => $betriebsbedarf,
+      debit  => 100,
+    },
+    {
+      chart  => $betriebsbedarf,
+      debit  => 100,
+    },
+    {
+      chart  => $betriebsbedarf,
+      debit  => 100,
+      tax_id => $tax_0->id,
+    },
+    {
+      chart  => $cash,
+      credit => 332,
+    },
+);
+$gl_transaction_8->post;
+
+is(SL::DB::Manager::GLTransaction->get_all_count(), 10, "gl transactions created ok");
+is_deeply(&get_account_balances,
+          [
+            {
+              'accno' => '1000',
+              'sum' => '1660.14000'
+            },
+            {
+              'accno' => '1200',
+              'sum' => '-100.00000'
+            },
+            {
+              'accno' => '1571',
+              'sum' => '-14.00000'
+            },
+            {
+              'accno' => '1575',
+              'sum' => '-32.00000'
+            },
+            {
+              'accno' => '1576',
+              'sum' => '-114.02000'
+            },
+            {
+              'accno' => '4980',
+              'sum' => '-1400.12000'
+            }
+          ],
+          "chart balances ok"
+         );
+
+done_testing;
+clear_up();
+
+1;
+
+sub clear_up {
+  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(
+                                                       AccTransaction
+                                                       GLTransaction
+                                                      );
+};
+
+sub get_account_balances {
+  my $query = <<SQL;
+  select c.accno,
+         sum(a.amount)
+    from acc_trans a
+         left join chart c on (c.id = a.chart_id)
+group by c.accno
+order by c.accno;
+SQL
+
+  my $result = selectall_hashref_query($::form, $dbh, $query);
+  return $result;
+};
index 5d6238e..a8f69f1 100644 (file)
@@ -1,4 +1,4 @@
-use Test::More tests => 32;
+use Test::More tests => 44;
 
 use lib 't';
 
@@ -23,6 +23,18 @@ is($p->sellprice_as_number, '2,30');
 is($p->sellprice_as_number('2,3442'), '2,3442');
 is($p->sellprice, 2.3442);
 is($p->sellprice_as_number, '2,3442');
+is($p->listprice_as_null_number('2,30'), '2,30');
+is($p->listprice, 2.30);
+is($p->listprice_as_null_number, '2,30');
+is($p->listprice_as_null_number('2,3442'), '2,3442');
+is($p->listprice, 2.3442);
+is($p->listprice_as_null_number, '2,3442');
+is($p->listprice_as_null_number(''), '');
+is($p->listprice, undef);
+is($p->listprice_as_null_number, '');
+is($p->listprice_as_null_number('0'), '0,00');
+is($p->listprice, 0);
+is($p->listprice_as_null_number, '0,00');
 
 my $o = new_ok 'SL::DB::Order';
 is($o->reqdate_as_date('11.12.2007'), '11.12.2007');
@@ -59,4 +71,3 @@ is $o->closed_as_bool_yn, 'Nein', 'bool 2';
 # defaults according to the database
 $i->taxincluded(undef);
 is $i->taxincluded_as_bool_yn, '', 'bool 3';
-
index bab3825..95ca14b 100644 (file)
@@ -1,4 +1,4 @@
-use Test::More tests => 75;
+use Test::More tests => 91;
 
 use lib 't';
 use utf8;
@@ -141,7 +141,7 @@ EOL
   profile => [{class  => 'SL::DB::Part'}],
 );
 $csv->parse;
-is $csv->get_objects->[0]->lastcost, '1221.52', 'ignore_unkown_columns works';
+is $csv->get_objects->[0]->lastcost, '1221.52', 'ignore_unknown_columns works';
 
 #####
 
@@ -382,6 +382,39 @@ $csv->parse;
 is_deeply $csv->get_data, [ { description => 'Kaffee' } ], 'without profile and class works';
 
 #####
+
+$csv = SL::Helper::Csv->new(
+  file => \<<EOL,
+description;partnumber
+Kaffee;1
+
+;
+ ;
+Tee;3
+EOL
+# Note: The second last line is not empty. The description is a space character.
+);
+ok $csv->parse;
+is_deeply $csv->get_data, [ {partnumber => 1, description => 'Kaffee'}, {partnumber => '', description => ' '}, {partnumber => 3, description => 'Tee'} ], 'ignoring empty lines works (header in csv file)';
+
+#####
+
+$csv = SL::Helper::Csv->new(
+  file => \<<EOL,
+Kaffee;1
+
+;
+ ;
+Tee;3
+EOL
+# Note: The second last line is not empty. The description is a space character.
+  header => ['description', 'partnumber'],
+);
+ok $csv->parse;
+is_deeply $csv->get_data, [ {partnumber => 1, description => 'Kaffee'}, {partnumber => '', description => ' '}, {partnumber => 3, description => 'Tee'} ], 'ignoring empty lines works';
+
+#####
+
 $csv = SL::Helper::Csv->new(
   file    => \"Kaffee;1,50\nSchoke;0,89\n",
   header  => [
@@ -596,7 +629,7 @@ EOL
   ignore_unknown_columns => 1,
 );
 ok !$csv->parse, 'multiplex check detects incosistent datatype field position';
-is_deeply( ($csv->errors)[0], [ 0, 'datatype field must be at the same position for all datatypes for multiplexed data', 0, 0 ], 'multiplex data with inconsistent datatype field posiotion throws error');
+is_deeply( ($csv->errors)[0], [ undef, 0, 'datatype field must be at the same position for all datatypes for multiplexed data', 0, 0 ], 'multiplex data with inconsistent datatype field posiotion throws error');
 
 #####
 
@@ -726,10 +759,146 @@ ok $csv->get_objects->[0], 'multiplex: empty path gets ignored in object creatio
 
 #####
 
+$csv = SL::Helper::Csv->new(
+  file => \<<EOL,
+datatype;customernumber;name
+datatype;description;partnumber
+C;1000;Meier
+P;Kaffee;1
+
+;;
+C
+P; ;
+C;2000;Meister
+P;Tee;3
+EOL
+  ignore_unknown_columns => 1,
+  profile => [ { class => 'SL::DB::Customer', row_ident => 'C' },
+               { class => 'SL::DB::Part',     row_ident => 'P' },
+  ],
+);
+$csv->parse;
+is_deeply $csv->get_data, [
+  {datatype => 'C', customernumber => 1000, name => 'Meier'},
+  {datatype => 'P', partnumber => 1, description => 'Kaffee'},
+  {datatype => 'C', customernumber => undef, name => undef},
+  {datatype => 'P', partnumber => '', description => ' '},
+  {datatype => 'C', customernumber => 2000, name => 'Meister'},
+  {datatype => 'P', partnumber => '3', description => 'Tee'},
+], 'ignoring empty lines works (multiplex data)';
+
+#####
+
+# Mappings
+# simple case
+$csv = SL::Helper::Csv->new(
+  file   => \<<EOL,
+description,sellprice,lastcost_as_number,purchaseprice,
+Kaffee,0.12,'12,2','1,5234'
+EOL
+  sep_char => ',',
+  quote_char => "'",
+  profile => [
+    {
+      profile => { listprice => 'listprice_as_number' },
+      mapping => { purchaseprice => 'listprice' },
+      class   => 'SL::DB::Part',
+    }
+  ],
+);
+ok $csv->parse, 'simple mapping parses';
+is $csv->get_objects->[0]->listprice, 1.5234, 'simple mapping works';
+
+$csv = SL::Helper::Csv->new(
+  file   => \<<EOL,
+description;partnumber;sellprice;purchaseprice;wiener;
+Kaffee;;0.12;1,221.52;ja wiener
+Beer;1123245;0.12;1.5234;nein kein wieder
+EOL
+  numberformat => '1,000.00',
+  ignore_unknown_columns => 1,
+  strict_profile => 1,
+  profile => [{
+    profile => { lastcost => 'lastcost_as_number' },
+    mapping => { purchaseprice => 'lastcost' },
+    class  => 'SL::DB::Part',
+  }]
+);
+ok $csv->parse, 'strict mapping parses';
+is $csv->get_objects->[0]->lastcost, 1221.52, 'strict mapping works';
+
+# swapping
+$csv = SL::Helper::Csv->new(
+  file   => \<<EOL,
+description;partnumber;sellprice;lastcost;wiener;
+Kaffee;1;0.12;1,221.52;ja wiener
+Beer;1123245;0.12;1.5234;nein kein wieder
+EOL
+  numberformat => '1,000.00',
+  ignore_unknown_columns => 1,
+  strict_profile => 1,
+  profile => [{
+    mapping => { partnumber => 'description', description => 'partnumber' },
+    class  => 'SL::DB::Part',
+  }]
+);
+ok $csv->parse, 'swapping parses';
+is $csv->get_objects->[0]->partnumber, 'Kaffee', 'strict mapping works 1';
+is $csv->get_objects->[0]->description, '1', 'strict mapping works 2';
+
+# case insensitive shit
+$csv = SL::Helper::Csv->new(
+  file   => \"Description\nKaffee",        # " # make emacs happy
+  case_insensitive_header => 1,
+  profile => [{
+    mapping => { description => 'description' },
+    class  => 'SL::DB::Part'
+  }],
+);
+$csv->parse;
+is $csv->get_objects->[0]->description, 'Kaffee', 'case insensitive mapping without profile works';
+
+# case insensitive shit
+$csv = SL::Helper::Csv->new(
+  file   => \"Price\n4,99",        # " # make emacs happy
+  case_insensitive_header => 1,
+  profile => [{
+    profile => { sellprice => 'sellprice_as_number' },
+    mapping => { price => 'sellprice' },
+    class  => 'SL::DB::Part',
+  }],
+);
+$csv->parse;
+is $csv->get_objects->[0]->sellprice, 4.99, 'case insensitive mapping with profile works';
+
+
+# self-mapping with profile
+$csv = SL::Helper::Csv->new(
+  file   => \"sellprice\n4,99",        # " # make emacs happy
+  case_insensitive_header => 1,
+  profile => [{
+    profile => { sellprice => 'sellprice_as_number' },
+    mapping => { sellprice => 'sellprice' },
+    class  => 'SL::DB::Part',
+  }],
+);
+$csv->parse;
+is $csv->get_objects->[0]->sellprice, 4.99, 'self-mapping with profile works';
+
+# self-mapping without profile
+$csv = SL::Helper::Csv->new(
+  file   => \"sellprice\n4.99",        # " # make emacs happy
+  case_insensitive_header => 1,
+  profile => [{
+    mapping => { sellprice => 'sellprice' },
+    class  => 'SL::DB::Part',
+  }],
+);
+$csv->parse;
+is $csv->get_objects->[0]->sellprice, 4.99, 'self-mapping without profile works';
 
 # vim: ft=perl
 # set emacs to perl mode
 # Local Variables:
 # mode: perl
 # End:
-
index afaa49e..4966540 100644 (file)
@@ -1,4 +1,4 @@
-use Test::More tests => 50;
+use Test::More tests => 60;
 
 use lib 't';
 
@@ -7,6 +7,9 @@ use Data::Dumper;
 use DateTime;
 use_ok 'SL::Helper::DateTime';
 
+my $local_tz   = DateTime::TimeZone->new(name => 'local');
+my $mon_012345 = DateTime->new(year => 2014, month => 6, day => 23, hour => 1, minute => 23, second => 45, time_zone => $local_tz);
+
 sub mon { DateTime->new(year => 2014, month => 6, day => 23) }
 sub tue { DateTime->new(year => 2014, month => 6, day => 24) }
 sub wed { DateTime->new(year => 2014, month => 6, day => 25) }
@@ -45,7 +48,7 @@ is tue->add_businessdays(businessweek => 6, days => 9), tue->add(days => 10), "t
 is tue->add_businessdays(businessweek => 6, days => 8), tue->add(days => 9), "tue + 8 => thu (date) (6dw)";
 
 
-# same with substract
+# same with subtract
 
 is mon->subtract_businessdays(days => 5)->day_of_week, 1, "mon - 5 => mon";
 is mon->subtract_businessdays(days => 12)->day_of_week, 4, "mon - 12 => thu";
@@ -87,3 +90,17 @@ is sun->add_businessdays(days => 1), sun->add(days => 1), "1 day after sun is mo
 is sat->add_businessdays(days => 1), sat->add(days => 2), "1 day after sut is mon";
 is sun->add_businessdays(days => -1), sun->add(days => -2), "1 day before sun is fri";
 is sat->add_businessdays(days => -1), sat->add(days => -1), "1 day before sut is fri";
+
+# parsing YYYY-MM-DD formatted strings
+is(DateTime->from_ymd(),                                 undef,                                     "no argument results in undef");
+is(DateTime->from_ymd(''),                               undef,                                     "empty argument results in undef");
+is(DateTime->from_ymd('chunky bacon'),                   undef,                                     "invalid argument results in undef");
+is(DateTime->from_ymd('2014-06-23'),                     $mon_012345->clone->truncate(to => 'day'), "2014-06-23 is parsed correctly");
+is(DateTime->from_ymd('2014-06-23')->strftime('%H%M%S'), '000000',                                  "2014-06-23 is parsed correctly");
+
+# parsing YYYY-MM-DDTHH:MM:SS formatted strings
+is(DateTime->from_ymdhms(),                      undef,       "no argument results in undef");
+is(DateTime->from_ymdhms(''),                    undef,       "empty argument results in undef");
+is(DateTime->from_ymdhms('chunky bacon'),        undef,       "invalid argument results in undef");
+is(DateTime->from_ymdhms('2014-06-23T01:23:45'), $mon_012345, "2014-06-23T01:23:45 is parsed correctly");
+is(DateTime->from_ymdhms('2014-06-23 01:23:45'), $mon_012345, "2014-06-23 01:23:45 is parsed correctly");
diff --git a/t/helper/number.t b/t/helper/number.t
new file mode 100644 (file)
index 0000000..e50bd21
--- /dev/null
@@ -0,0 +1,136 @@
+use Test::More tests => 173;
+
+use lib 't';
+
+use SL::Helper::Number qw(:ALL);
+
+use_ok 'Support::TestSetup';
+
+Support::TestSetup::login();
+
+# format
+
+sub test_format {
+  my ($expected, $amount, $places, $numberformat, $dash, $comment) = @_;
+
+  my $other_numberformat = $numberformat eq '1.000,00' ? '1,000.00' : '1.000,00';
+
+  is (_format_number($amount, $places, numberformat => $numberformat, dash => $dash), $expected, "$comment - explicit");
+
+  {
+    local $::myconfig{numberformat} = $other_numberformat;
+    is (_format_number($amount, $places, numberformat => $numberformat, dash => $dash), $expected, "$comment - explicit with different numberformat");
+  }
+  {
+    local $::myconfig{numberformat} = $numberformat;
+    is (_format_number($amount, $places, dash => $dash), $expected, "$comment - implicit numberformat");
+  }
+
+  # test _format_total
+  if (($places // 0) == 2) {
+    is (_format_total($amount, numberformat => $numberformat, dash => $dash), $expected, "$comment - explicit");
+
+    {
+      local $::myconfig{numberformat} = $other_numberformat;
+      is (_format_total($amount, numberformat => $numberformat, dash => $dash), $expected, "$comment - explicit with different numberformat");
+    }
+    {
+      local $::myconfig{numberformat} = $numberformat;
+      is (_format_total($amount, dash => $dash), $expected, "$comment - implicit numberformat");
+    }
+  }
+}
+
+
+test_format('10,00', '1e1', 2, '1.000,00', undef, 'format 1e1 (numberformat: 1.000,00)');
+test_format('1.000,00', 1000, 2, '1.000,00', undef, 'format 1000 (numberformat: 1.000,00)');
+test_format('1.000,12', 1000.1234, 2, '1.000,00', undef,  'format 1000.1234 (numberformat: 1.000,00)');
+test_format('1.000.000.000,12', 1000000000.1234, 2, '1.000,00', undef, 'format 1000000000.1234 (numberformat: 1.000,00)');
+test_format('-1.000.000.000,12', -1000000000.1234, 2, '1.000,00', undef, 'format -1000000000.1234 (numberformat: 1.000,00)');
+
+test_format('10.00', '1e1', 2, '1,000.00', undef, 'format 1e1 (numberformat: 1,000.00)');
+test_format('1,000.00', 1000, 2, '1,000.00', undef, 'format 1000 (numberformat: 1,000.00)');
+test_format('1,000.12', 1000.1234, 2, '1,000.00', undef, 'format 1000.1234 (numberformat: 1,000.00)');
+test_format('1,000,000,000.12', 1000000000.1234, 2, '1,000.00', undef, 'format 1000000000.1234 (numberformat: 1,000.00)');
+test_format('-1,000,000,000.12', -1000000000.1234, 2, '1,000.00', undef, 'format -1000000000.1234 (numberformat: 1,000.00)');
+
+# negative places
+
+test_format('1.00045', 1.00045, -2, '1,000.00', undef, 'negative places');
+test_format('1.00045', 1.00045, -5, '1,000.00', undef, 'negative places 2');
+test_format('1.00', 1, -2, '1,000.00', undef, 'negative places 3');
+
+# bugs amd edge cases
+test_format('0,00005', 0.00005, undef, '1.000,00', undef, 'messing with small numbers and no precision');
+test_format('0', undef, undef, '1.000,00', undef, 'undef');
+test_format('0', '', undef, '1.000,00', undef, 'empty string');
+test_format('0,00', undef, 2, '1.000,00', undef, 'undef with precision');
+test_format('0,00', '', 2, '1.000,00', undef, 'empty string with prcesion');
+
+test_format('1', 0.545, 0, '1.000,00', undef, 'rounding up with precision 0');
+test_format('-1', -0.545, 0, '1.000,00', undef, 'neg rounding up with precision 0');
+
+test_format('1', 1.00, undef, '1.000,00', undef, 'autotrim to 0 places');
+
+test_format('10', 10, undef, '1.000,00', undef, 'autotrim does not harm integers');
+test_format('10,00', 10, 2, '1.000,00', undef, 'autotrim does not harm integers 2');
+test_format('10,00', 10, -2, '1.000,00', undef, 'autotrim does not harm integers 3');
+test_format('10', 10, 0, '1.000,00', undef, 'autotrim does not harm integers 4');
+
+test_format('0', 0, 0, '1.000,00', undef, 'trivial zero');
+test_format('0,00', -0.002, 2, '1.000,00', undef, 'negative zero');
+test_format('-0,002', -0.002, 3, '1.000,00', undef, 'negative zero');
+
+# dash
+
+test_format('(350,00)', -350, 2, '1.000,00', '-', 'dash -');
+
+# parse
+
+sub test_parse {
+  my ($expected, $amount, $numberformat, $comment) = @_;
+
+  my $other_numberformat = $numberformat eq '1.000,00' ? '1,000.00' : '1.000,00';
+
+  is (_parse_number($amount, numberformat => $numberformat), $expected, "$comment - explicit");
+
+  {
+    local $::myconfig{numberformat} = $other_numberformat;
+    is (_parse_number($amount, numberformat => $numberformat), $expected, "$comment - explicit with different numberformat");
+  }
+  {
+    local $::myconfig{numberformat} = $numberformat;
+    is (_parse_number($amount), $expected, "$comment - implicit numberformat");
+  }
+}
+
+
+test_parse(12345,     '12345',        '1.000,00', '12345 (numberformat: 1.000,00)');
+test_parse(1234.5,    '1.234,5',      '1.000,00', '1.234,5 (numberformat: 1.000,00)');
+test_parse(9871234.5, '9.871.234,5',  '1.000,00', '9.871.234,5 (numberformat: 1.000,00)');
+test_parse(1234.5,    '1234,5',       '1.000,00', '1234,5 (numberformat: 1.000,00)');
+test_parse(12345,     '012345',       '1.000,00', '012345 (numberformat: 1.000,00)');
+test_parse(1234.5,    '01.234,5',     '1.000,00', '01.234,5 (numberformat: 1.000,00)');
+test_parse(1234.5,    '01234,5',      '1.000,00', '01234,5 (numberformat: 1.000,00)');
+test_parse(9871234.5, '09.871.234,5', '1.000,00', '09.871.234,5 (numberformat: 1.000,00)');
+
+# round
+
+is(_round_number('3.231',2),'3.23');
+is(_round_number('3.234',2),'3.23');
+is(_round_number('3.235',2),'3.24');
+is(_round_number('5.786',2),'5.79');
+is(_round_number('2.342',2),'2.34');
+is(_round_number('1.2345',2),'1.23');
+is(_round_number('8.2345',2),'8.23');
+is(_round_number('8.2350',2),'8.24');
+
+
+is(_round_total('3.231'),'3.23');
+is(_round_total('3.234'),'3.23');
+is(_round_total('3.235'),'3.24');
+is(_round_total('5.786'),'5.79');
+is(_round_total('2.342'),'2.34');
+is(_round_total('1.2345'),'1.23');
+is(_round_total('8.2345'),'8.23');
+is(_round_total('8.2350'),'8.24');
diff --git a/t/helper/object.t b/t/helper/object.t
new file mode 100644 (file)
index 0000000..f61c894
--- /dev/null
@@ -0,0 +1,168 @@
+use strict;
+use Test::More tests => 37;
+
+use lib 't';
+
+# to test delegate, test a few of these combinations:
+#   target_class or object
+#   target_method given or not
+#   object or class invocation
+
+{ package T::Helper::Object::Delegatee;
+  sub test_simple { "simple" }
+  sub test_class { "classic" }
+  sub test_invocation { (ref $_[0] ? ref $_[0] : $_[0]) eq __PACKAGE__ }
+  sub test_method { !!ref $_[0] }
+  sub test_wantarray {
+    if (!defined wantarray) {
+      ${$_[1]} = 'void';
+    } else {
+      ${$_[1]} = wantarray ? 'list' : 'scalar';
+    }
+  }
+  sub args { @_ }
+}
+my $delegatee = bless {}, "T::Helper::Object::Delegatee";
+
+{
+  package T::Helper::Object::Test1;
+  use SL::Helper::Object (
+    delegate => [
+      obj => [ "test_simple", "test_invocation", "test_method", "test_wantarray", "args" ],
+      obj => [ { target_method => "test_simple" }, "test_simple_renamed" ],
+      "T::Helper::Object::Delegatee" => [ "test_class" ],
+      "T::Helper::Object::Delegatee" => [ { target_method => "test_class" }, "test_class_renamed" ],
+      "T::Helper::Object::Delegatee" => [ { target_method => "test_invocation" }, "test_class_invocation" ],
+      "T::Helper::Object::Delegatee" => [ { target_method => "test_method" }, "test_function" ],
+      obj => [ { target_method => 'test_wantarray', force_context => 'void' },   'test_void_context' ],
+      obj => [ { target_method => 'test_wantarray', force_context => 'scalar' }, 'test_scalar_context' ],
+      obj => [ { target_method => 'test_wantarray', force_context => 'list' },   'test_list_context' ],
+      obj => [ { target_method => 'args', args => 'none' }, 'no_args' ],
+      obj => [ { target_method => 'args', args => 'raw' }, 'raw_args' ],
+      obj => [ { target_method => 'args', args => 'standard' }, 'standard_args' ],
+      "T::Helper::Object::Delegatee" => [ { target_method => "args", args => 'raw' }, "raw_class_args" ],
+      "T::Helper::Object::Delegatee" => [ { target_method => "args", args => 'standard' }, "standard_class_args" ],
+      "T::Helper::Object::Delegatee" => [ { target_method => "args", args => 'standard', class_function => 1 }, "class_function_args" ],
+    ],
+  );
+  sub obj { $_[0]{obj} }
+};
+my $obj1 = bless { obj => $delegatee }, "T::Helper::Object::Test1";
+
+is $obj1->test_simple,           'simple',  'simple delegation works';
+is $obj1->test_simple_renamed,   'simple',  'renamed delegation works';
+is $obj1->test_class,            'classic', 'class delegation works';
+is $obj1->test_class_renamed,    'classic', 'renamed class delegation works';
+ok $obj1->test_invocation,       'object invocation works';
+ok $obj1->test_class_invocation, 'class invocation works';
+ok $obj1->test_method,           'method invocation works';
+ok !$obj1->test_function,        'function invocation works';
+
+
+#  3: args in [ none, raw,standard ]
+
+is scalar $obj1->no_args("test"), 1, 'args none ignores args';
+is [$obj1->raw_args("test")]->[0], $delegatee, 'args raw 1';
+is [$obj1->raw_args("test")]->[1], $obj1,      'args raw 2';
+is [$obj1->raw_args("test")]->[2], "test",     'args raw 3';
+is scalar $obj1->raw_args("test"), 3, 'args raw args list';
+is [$obj1->standard_args("test")]->[0], $delegatee, 'args standard 1';
+is [$obj1->standard_args("test")]->[1], "test",     'args standard 1';
+is scalar $obj1->standard_args("test"), 2, 'args standard args list';
+
+is [$obj1->raw_class_args("test")]->[0], ref $delegatee, 'args raw 1';
+is [$obj1->raw_class_args("test")]->[1], $obj1,          'args raw 2';
+is [$obj1->raw_class_args("test")]->[2], "test",         'args raw 3';
+is scalar $obj1->raw_class_args("test"), 3, 'args raw args list';
+is [$obj1->standard_class_args("test")]->[0], ref $delegatee, 'args standard 1';
+is [$obj1->standard_class_args("test")]->[1], "test",         'args standard 1';
+is scalar $obj1->standard_class_args("test"), 2, 'args standard args list';
+
+is [$obj1->class_function_args("test")]->[0], 'test', 'args class function standard 1';
+is scalar $obj1->class_function_args("test"), 1, 'args class function standard args list';
+
+
+#  4: force_context [ none, void, scalar, list ]
+
+my $c;
+$c = ''; $obj1->test_void_context(\$c);   is $c, 'void',   'force context void works';
+$c = ''; $obj1->test_scalar_context(\$c); is $c, 'scalar', 'force context scalar works';
+$c = ''; $obj1->test_list_context(\$c);   is $c, 'list',   'force context list works';
+
+# and without forcing:
+$c = ''; $obj1->test_wantarray(\$c);            is $c, 'void',   'natural context void works';
+$c = ''; my $test = $obj1->test_wantarray(\$c); is $c, 'scalar', 'natural context scalar works';
+$c = ''; my @test = $obj1->test_wantarray(\$c); is $c, 'list',   'natural context list works';
+
+
+# try stupid stuff that should die
+
+my $dies = 1;
+eval { package T::Helper::Object::Test2;
+  SL::Helper::Object->import(
+    delegate => [ one => [], "two" ],
+  );
+  $dies = 0;
+  1;
+};
+ok $dies, 'delegate with uneven number of args dies';
+
+$dies = 1;
+eval { package T::Helper::Object::Test3;
+  SL::Helper::Object->import(
+    delegate => {},
+  );
+  $dies = 0;
+  1;
+};
+ok $dies, 'delegate with hashref dies';
+
+$dies = 1;
+eval { package T::Helper::Object::Test4;
+  SL::Helper::Object->import(
+    delegate => [
+      "List::Util" => [ '{}; print "gotcha"' ],
+    ],
+  );
+  $dies = 0;
+  1;
+};
+ok $dies, 'code injection in method names dies';
+
+$dies = 1;
+eval { package T::Helper::Object::Test5;
+  SL::Helper::Object->import(
+    delegate => [
+      "print 'this'" => [ 'test' ],
+    ],
+  );
+  $dies = 0;
+  1;
+};
+ok $dies, 'code injection in target dies';
+
+$dies = 1;
+eval { package T::Helper::Object::Test6;
+  SL::Helper::Object->import(
+    delegate => [
+      "List::Util" => [ { target_method => 'system()' }, 'test' ],
+    ],
+  );
+  $dies = 0;
+  1;
+};
+ok $dies, 'code injection in target_method dies';
+
+$dies = 1;
+eval { package T::Helper::Object::Test6;
+  SL::Helper::Object->import(
+    delegate => [
+      "List::Util" => [ { target_name => 'test2' }, 'test' ],
+    ],
+  );
+  $dies = 0;
+  1;
+};
+ok $dies, 'unknown parameter dies';
+
+1;
diff --git a/t/helper/shipped_qty.t b/t/helper/shipped_qty.t
new file mode 100644 (file)
index 0000000..ae97ccb
--- /dev/null
@@ -0,0 +1,300 @@
+use strict;
+use Test::More;
+
+use lib 't';
+use Support::TestSetup;
+use Carp;
+use Test::Exception;
+use Data::Dumper;
+use SL::DB::Part;
+use SL::DB::Inventory;
+use SL::DB::TransferType;
+use SL::DB::Order;
+use SL::DB::DeliveryOrder;
+use SL::DB::Customer;
+use SL::DB::Vendor;
+use SL::DB::RecordLink;
+use SL::DB::DeliveryOrderItemsStock;
+use SL::DB::Bin;
+use SL::WH;
+use SL::AM;
+use SL::Dev::ALL qw(:ALL);
+use SL::Helper::ShippedQty;
+use DateTime;
+
+Support::TestSetup::login();
+
+clear_up();
+
+my ($customer, $vendor, @parts, $unit);
+
+$customer = new_customer(name => 'Testkunde'    )->save;
+$vendor   = new_vendor(  name => 'Testlieferant')->save;
+
+my $default_sellprice = 10;
+my $default_lastcost  =  4;
+
+my ($wh) = create_warehouse_and_bins();
+my $bin1 = SL::DB::Manager::Bin->find_by(description => "Bin 1");
+my $bin2 = SL::DB::Manager::Bin->find_by(description => "Bin 2");
+
+my %part_defaults = (
+    sellprice    => $default_sellprice,
+    warehouse_id => $wh->id,
+    bin_id       => $bin1->id
+);
+
+# create 3 parts to be used in test
+for my $i ( 1 .. 4 ) {
+  new_part( %part_defaults, partnumber => $i, description => "part $i test" )->save;
+};
+
+my $part1 = SL::DB::Manager::Part->find_by( partnumber => '1' ) or die;
+my $part2 = SL::DB::Manager::Part->find_by( partnumber => '2' ) or die;
+my $part3 = SL::DB::Manager::Part->find_by( partnumber => '3' ) or die;
+my $part4 = SL::DB::Manager::Part->find_by( partnumber => '4' ) or die;
+
+my @part_ids; # list of all part_ids to run checks against
+push( @part_ids, $_->id ) foreach ( $part1, $part2, $part3, $part4 );
+my %default_transfer_params = ( wh => $wh, bin => $bin1, unit => 'Stck');
+
+
+# test purchases first, so there is actually stock available when sales is tested
+
+note("testing purchases, no fill_up");
+
+$::form->{type} = 'purchase_order';
+my $purchase_order = create_purchase_order(
+  save       => 1,
+  orderitems => [ create_order_item(part => $part1, qty => 11),
+                  create_order_item(part => $part2, qty => 12),
+                  create_order_item(part => $part3, qty => 13),
+                ]
+);
+
+Rose::DB::Object::Helpers::forget_related($purchase_order, 'orderitems');
+$purchase_order->orderitems;
+
+local $::instance_conf->data->{shipped_qty_require_stock_out} = 1;
+SL::Helper::ShippedQty
+  ->new(require_stock_out => 1)  # should make no difference while there is no delivery order
+  ->calculate($purchase_order)
+  ->write_to_objects;
+
+is($purchase_order->items_sorted->[0]->{shipped_qty}, 0, "first purchase orderitem has no shipped_qty");
+ok(!$purchase_order->items_sorted->[0]->{delivered},     "first purchase orderitem is not delivered");
+
+my $purchase_orderitem_part1 = SL::DB::Manager::OrderItem->find_by( parts_id => $part1->id, trans_id => $purchase_order->id);
+
+is($purchase_orderitem_part1->shipped_qty, 0, "OrderItem shipped_qty method ok");
+
+is($purchase_order->closed,     0, 'purchase order is open');
+# set delivered only if the do is also stocked in
+ok(!$purchase_order->delivered,    'purchase order is not delivered');
+
+note('converting purchase order to delivery order');
+# create purchase delivery order from purchase order
+my $purchase_delivery_order = $purchase_order->convert_to_delivery_order;
+is($purchase_order->closed,    0, 'purchase order is open');
+note('purchase order is not general now delivered');
+ok(!$purchase_order->delivered,   'purchase order is not delivered');
+
+SL::Helper::ShippedQty
+  ->new(require_stock_out => 0)
+  ->calculate($purchase_order)
+  ->write_to_objects;
+
+is($purchase_order->items_sorted->[0]->{shipped_qty}, 11, "require_stock_out => 0: first purchase orderitem has shipped_qty");
+ok($purchase_order->items_sorted->[0]->{delivered},       "require_stock_out => 0: first purchase orderitem is delivered");
+
+Rose::DB::Object::Helpers::forget_related($purchase_order, 'orderitems');
+$purchase_order->orderitems;
+
+SL::Helper::ShippedQty
+  ->new(require_stock_out => 1)
+  ->calculate($purchase_order)
+  ->write_to_objects;
+
+is($purchase_order->items_sorted->[0]->{shipped_qty}, 0,  "require_stock_out => 1: first purchase orderitem has no shipped_qty");
+ok(!$purchase_order->items_sorted->[0]->{delivered},      "require_stock_out => 1: first purchase orderitem is not delivered");
+
+# ship items from delivery order
+transfer_purchase_delivery_order($purchase_delivery_order);
+
+Rose::DB::Object::Helpers::forget_related($purchase_order, 'orderitems');
+$purchase_order->orderitems;
+
+SL::Helper::ShippedQty
+  ->new(require_stock_out => 1, keep_matches => 1)  # shouldn't make a difference now after shipping
+  ->calculate($purchase_order)
+  ->write_to_objects;
+
+is($purchase_order->items_sorted->[0]->{shipped_qty}, 11, "require_stock_out => 1: first purchase orderitem has shipped_qty");
+ok($purchase_order->items_sorted->[0]->{delivered},       "require_stock_out => 1: first purchase orderitem is delivered");
+
+my $purchase_orderitem_part2 = SL::DB::Manager::OrderItem->find_by(parts_id => $part1->id, trans_id => $purchase_order->id);
+
+is($purchase_orderitem_part2->shipped_qty(require_stock_out => 1), 11, "OrderItem shipped_qty from helper ok");
+
+
+note('testing sales, no fill_up');
+
+$::form->{type} = 'sales_order';
+my $sales_order = create_sales_order(
+  save       => 1,
+  orderitems => [ create_order_item(part => $part1, qty => 5),
+                  create_order_item(part => $part2, qty => 6),
+                  create_order_item(part => $part3, qty => 7),
+                ]
+);
+
+Rose::DB::Object::Helpers::forget_related($sales_order, 'orderitems');
+$sales_order->orderitems;
+
+SL::Helper::ShippedQty
+  ->new(require_stock_out => 1)  # should make no difference while there is no delivery order
+  ->calculate($sales_order)
+  ->write_to_objects;
+
+is($sales_order->items_sorted->[0]->{shipped_qty}, 0,  "first sales orderitem has no shipped_qty");
+ok(!$sales_order->items_sorted->[0]->{delivered},      "first sales orderitem is not delivered");
+
+my $orderitem_part1 = SL::DB::Manager::OrderItem->find_by(parts_id => $part1->id, trans_id => $sales_order->id);
+my $orderitem_part2 = SL::DB::Manager::OrderItem->find_by(parts_id => $part2->id, trans_id => $sales_order->id);
+
+is($orderitem_part1->shipped_qty, 0, "OrderItem shipped_qty method ok");
+
+# create sales delivery order from sales order
+my $sales_delivery_order = $sales_order->convert_to_delivery_order;
+
+SL::Helper::ShippedQty
+  ->new(require_stock_out => 0)
+  ->calculate($sales_order)
+  ->write_to_objects;
+
+is($sales_order->items_sorted->[0]->{shipped_qty}, 5, "require_stock_out => 0: first sales orderitem has shipped_qty");
+ok($sales_order->items_sorted->[0]->{delivered},      "require_stock_out => 0: first sales orderitem is delivered");
+
+Rose::DB::Object::Helpers::forget_related($sales_order, 'orderitems');
+$sales_order->orderitems;
+
+SL::Helper::ShippedQty
+  ->new(require_stock_out => 1)
+  ->calculate($sales_order)
+  ->write_to_objects;
+
+is($sales_order->items_sorted->[0]->{shipped_qty}, 0,  "require_stock_out => 1: first sales orderitem has no shipped_qty");
+ok(!$sales_order->items_sorted->[0]->{delivered},      "require_stock_out => 1: first sales orderitem is not delivered");
+
+# ship items from delivery order
+transfer_sales_delivery_order($sales_delivery_order);
+
+Rose::DB::Object::Helpers::forget_related($sales_order, 'orderitems');
+$sales_order->orderitems;
+
+SL::Helper::ShippedQty
+  ->new(require_stock_out => 1)
+  ->calculate($sales_order)
+  ->write_to_objects;
+
+is($sales_order->items_sorted->[0]->{shipped_qty}, 5, "require_stock_out => 1: first sales orderitem has no shipped_qty");
+ok($sales_order->items_sorted->[0]->{delivered},      "require_stock_out => 1: first sales orderitem is not delivered");
+
+$orderitem_part1 = SL::DB::Manager::OrderItem->find_by(parts_id => $part1->id, trans_id => $sales_order->id);
+
+is($orderitem_part1->shipped_qty(require_stock_out => 1), 5, "OrderItem shipped_qty from helper ok");
+
+
+note('misc tests');
+my $number_of_linked_items = SL::DB::Manager::RecordLink->get_all_count( where => [ from_table => 'orderitems', to_table => 'delivery_order_items' ] );
+is ($number_of_linked_items , 6, "6 record_links for items, 3 from sales order, 3 from purchase order");
+
+note('testing optional orderitems');
+
+my $item_optional = create_order_item(part => $part3, qty => 7, optional => 1);
+ok($item_optional->{optional},       "optional order item");
+
+my $sales_order_opt = create_sales_order(
+  save       => 1,
+  orderitems => [ create_order_item(part => $part1, qty => 5),
+                  create_order_item(part => $part2, qty => 6),
+                  $item_optional,
+                ]
+);
+
+
+SL::Helper::ShippedQty
+  ->new(require_stock_out => 1)  # should make no difference while there is no delivery order
+  ->calculate($sales_order_opt)
+  ->write_to_objects;
+
+is($sales_order_opt->items_sorted->[2]->{shipped_qty}, 0,  "third optional sales orderitem has no shipped_qty");
+ok(!$sales_order_opt->items_sorted->[2]->{delivered},      "third optional sales orderitem is not delivered");
+ok($sales_order_opt->items_sorted->[2]->{optional},        "third optional sales orderitem is optional");
+
+my $orderitem_part3_opt = SL::DB::Manager::OrderItem->find_by(parts_id => $part3->id, trans_id => $sales_order_opt->id);
+is($orderitem_part3_opt->shipped_qty, 0, "OrderItem shipped_qty method ok");
+
+# create sales delivery order from sales order
+my $sales_delivery_order_opt = $sales_order_opt->convert_to_delivery_order;
+is(scalar @{ $sales_delivery_order_opt->items_sorted }, 3,   "third optional sales delivery orderitem is there");
+
+# and delete third item
+my $optional =  SL::DB::Manager::DeliveryOrderItem->find_by(parts_id => $part3->id, delivery_order_id => $sales_delivery_order_opt->id);
+SL::DB::DeliveryOrderItem->new(id => $optional->id)->delete;
+$sales_delivery_order_opt->save(cascade => 1);
+my $new_sales_delivery_order_opt = SL::DB::Manager::DeliveryOrder->find_by(id => $sales_delivery_order_opt->id);
+is(scalar @{ $new_sales_delivery_order_opt->items_sorted }, 2,   "third optional sales delivery orderitem is undef");
+
+SL::Helper::ShippedQty
+  ->new(require_stock_out => 0)
+  ->calculate($sales_order_opt)
+  ->write_to_objects;
+
+is($sales_order_opt->items_sorted->[0]->{shipped_qty}, 5,  "require_stock_out => 0: first sales orderitem has shipped_qty");
+ok($sales_order_opt->items_sorted->[0]->{delivered},       "require_stock_out => 0: first sales orderitem is delivered");
+ok($sales_order_opt->items_sorted->[1]->{delivered},       "require_stock_out => 0: second sales orderitem is delivered");
+ok(!$sales_order_opt->items_sorted->[2]->{delivered},      "require_stock_out => 0: third sales orderitem is NOT delivered");
+is($sales_order_opt->items_sorted->[2]->{shipped_qty}, 0,  "require_stock_out => 0: third sales orderitem has no shipped_qty");
+ok($sales_order_opt->{delivered},                          "require_stock_out => 0: order IS delivered");
+
+clear_up();
+
+{
+# edge case:
+#
+# suppose an order was delivered, and someone removes one item from the delivery order.
+# make sure the order is then shown as not delivered.
+#
+  my $sales_order = create_sales_order(
+    save       => 1,
+    orderitems => [ create_order_item(part => new_part()->save, qty => 5),
+                    create_order_item(part => new_part()->save, qty => 6),
+                    create_order_item(part => new_part()->save, qty => 7),
+                  ]
+  );
+  $sales_order->load;
+
+  my $delivery_order = SL::DB::DeliveryOrder->new_from($sales_order);
+  $delivery_order->save;
+
+  $delivery_order->items(@{ $delivery_order->items_sorted }[0..1]);
+  $delivery_order->save;
+
+  SL::Helper::ShippedQty
+    ->new(require_stock_out => 0)
+    ->calculate($sales_order)
+    ->write_to_objects;
+
+  ok !$sales_order->delivered, 'after deleting a position from a delivery order, the order is undelivered again';
+}
+
+clear_up();
+
+done_testing;
+
+sub clear_up {
+  foreach ( qw(Inventory DeliveryOrderItem DeliveryOrder Price OrderItem Order Part Customer Vendor Bin Warehouse) ) {
+    "SL::DB::Manager::${_}"->delete_all(all => 1);
+  }
+};
diff --git a/t/helper/trim.t b/t/helper/trim.t
new file mode 100644 (file)
index 0000000..52b0c8d
--- /dev/null
@@ -0,0 +1,20 @@
+use Test::More tests => 11;
+
+use strict;
+use utf8;
+
+use lib 't';
+
+use SL::Util qw(trim);
+
+is(trim("hello"),               "hello",         "hello");
+is(trim("hello "),              "hello",         "hello ");
+is(trim(" hello"),              "hello",         " hello");
+is(trim(" hello "),             "hello",         " hello ");
+is(trim(" h el lo "),           "h el lo",       " h el lo ");
+is(trim("\n\t\rh\nello"),       "h\nello",       "line feed, horizontal tab, carriage return; line feed within word");
+is(trim("\x{a0}h\nello"),       "h\nello",       "non-breaking space");
+is(trim("h\nello\n\t\r"),       "h\nello",       "line feed, horizontal tab, carriage return; line feed within word");
+is(trim("h\nello\x{a0}"),       "h\nello",       "non-breaking space");
+is(trim("h\ne\x{a0}llo\x{a0}"), "h\ne\x{a0}llo", "non-breaking space within word");
+is(trim(undef),                 undef,           "undef");
diff --git a/t/helper/user_preferencess.t b/t/helper/user_preferencess.t
new file mode 100644 (file)
index 0000000..8e68ff2
--- /dev/null
@@ -0,0 +1,71 @@
+use Test::More;
+use Test::Exception;
+use Test::Deep qw(bag cmp_deeply);
+
+use strict;
+use lib 't';
+
+use Support::TestSetup;
+use_ok 'SL::Helper::UserPreferences';
+
+Support::TestSetup::login();
+
+my $prefs;
+$prefs = new_ok 'SL::Helper::UserPreferences', [ current_version => 1 ];
+
+
+$prefs->store('test1', "val");
+$prefs->store('test2', "val2");
+
+cmp_deeply [ $prefs->get_keys ], bag('test1', 'test2'), 'get_keys works';
+
+is $prefs->get('test1'), 'val', 'get works';
+is $prefs->get_tuple('test2')->{value}, 'val2', 'get tuple works';
+is $prefs->get_all->[1]{value}, 'val2', 'get all works';
+is scalar @{ $prefs->get_all }, 2, 'get all works 2';
+
+$prefs = new_ok 'SL::Helper::UserPreferences', [
+  current_version => 2,
+  upgrade_callbacks => {
+    2 => sub { my ($val) = @_; $val . ' in space!'; }
+  }
+];
+
+is $prefs->get('test1'), 'val in space!', 'upgrading works';
+
+$prefs = new_ok 'SL::Helper::UserPreferences', [ current_version => 2 ];
+is $prefs->get('test1'), 'val in space!', 'auto store back works';
+
+$prefs = new_ok 'SL::Helper::UserPreferences', [ current_version => 1, namespace => 'namespace2' ];
+is $prefs->get('test1'), undef, 'other namespace does not find prior data';
+
+$prefs->store('test1', "namespace2 test");
+is $prefs->get('test1'), 'namespace2 test', 'other namespace finds data with same key';
+
+$prefs = new_ok 'SL::Helper::UserPreferences', [ current_version => 2 ];
+is $prefs->get('test1'), 'val in space!', 'original namepsace is not affected';
+
+$prefs = new_ok 'SL::Helper::UserPreferences', [ current_version => 1, login => 'demo2' ];
+$prefs->store('test1', "login test");
+
+$prefs = new_ok 'SL::Helper::UserPreferences', [ current_version => 2 ];
+is $prefs->get('test1'), 'val in space!', 'original login is not affected';
+
+$prefs->store('test1', 'new value');
+is scalar @{ $prefs->get_all }, 2, 'storing an existing value overwrites';
+
+my @array = $prefs->get_all;
+is scalar @array, 1, 'get_all in list context returns 1 element';
+isa_ok $array[0], 'ARRAY', 'get_all in list context returns 1 arrayref';
+
+$prefs = new_ok 'SL::Helper::UserPreferences', [ current_version => 1 ];
+dies_ok { $prefs->get('test1') } 'reading newer version dies';
+
+$prefs = new_ok 'SL::Helper::UserPreferences', [ current_version => 2 ];
+$prefs->delete('test1');
+is $prefs->get('test1'), undef, 'deleting works';
+
+$prefs->delete_all;
+is $prefs->get('test2'), undef, 'delete_all works';
+
+done_testing;
diff --git a/t/menu/parse_access_string.t b/t/menu/parse_access_string.t
new file mode 100644 (file)
index 0000000..5212699
--- /dev/null
@@ -0,0 +1,80 @@
+use strict;
+
+use Test::More;
+
+use lib 't';
+use Support::TestSetup;
+
+Support::TestSetup::login();
+
+use_ok 'SL::Menu';
+
+#my $menu = SL::Menu->new('user');
+my $menu = 'SL::Menu';
+
+my %node;
+$node{id} = 'test_node';
+
+$node{access} = 'sales_quotation_edit';
+ok($menu->parse_access_string(\%node), 'simple parse granted');
+
+$node{access} = 'no such right';
+ok(!$menu->parse_access_string(\%node), 'simple parse not granted');
+
+$node{access} = 'sales_quotation_edit)';
+eval {$menu->parse_access_string(\%node); ok(0, 'detect missing opening parenthesis'); 1} or do { ok(1, 'detect missing opening parenthesis'); };
+
+$node{access} = '(sales_quotation_edit';
+eval {$menu->parse_access_string(\%node); ok(0, 'detect missing closing parenthesis'); 1} or do { ok(1, 'detect missing closing parenthesis'); };
+
+$node{access} = 'sales_quotation_edit-';
+eval {$menu->parse_access_string(\%node); ok(0, 'detect unrecognized token'); 1} or do { ok(1, 'detect unrecognized token'); };
+
+$node{access} = 'sales_order_edit & sales_quotation_edit';
+ok($menu->parse_access_string(\%node), 'grant and grant');
+
+$node{access} = 'no_such_right & sales_quotation_edit';
+ok(!$menu->parse_access_string(\%node), 'not grant and grant');
+
+$node{access} = 'no_such_right & no_such_right';
+ok(!$menu->parse_access_string(\%node), 'not grant and not grant');
+
+$node{access} = 'sales_order_edit|sales_quotation_edit';
+ok($menu->parse_access_string(\%node), 'grant or grant');
+
+$node{access} = 'no_such_right | sales_quotation_edit';
+ok($menu->parse_access_string(\%node), 'not grant or grant');
+
+$node{access} = 'no_such_right | no_such_right';
+ok(!$menu->parse_access_string(\%node), 'not grant or not grant');
+
+$node{access} = '(sales_quotation_edit & sales_order_edit | (no_such_right & sales_order_edit))';
+ok($menu->parse_access_string(\%node), 'parenthesis 1');
+
+$node{access} = '(no_such_right & sales_order_edit | (no_such_right & sales_order_edit))';
+ok(!$menu->parse_access_string(\%node), 'parenthesis 2');
+
+$node{access} = 'sales_quotation_edit & client/feature_experimental_order';
+ok($menu->parse_access_string(\%node), 'client');
+
+$node{access} = '!no_such_right';
+ok($menu->parse_access_string(\%node), 'simple negation 1');
+
+$node{access} = '!sales_order_edit';
+ok(!$menu->parse_access_string(\%node), 'simple negation 2');
+
+$node{access} = '!!sales_order_edit';
+ok($menu->parse_access_string(\%node), 'double negation');
+
+$node{access} = '(no_such_right & sales_order_edit | !(no_such_right & sales_order_edit))';
+ok($menu->parse_access_string(\%node), 'parenthesis with negation 1');
+
+$node{access} = '(no_such_right & sales_order_edit | (!no_such_right | !sales_order_edit))';
+ok($menu->parse_access_string(\%node), 'parenthesis with negation 2');
+
+$node{access} = 'sales_quotation_edit & !client/feature_experimental_order';
+ok(!$menu->parse_access_string(\%node), 'client negation');
+
+done_testing;
+
+1;
diff --git a/t/part/assembly.t b/t/part/assembly.t
new file mode 100644 (file)
index 0000000..6500dfa
--- /dev/null
@@ -0,0 +1,98 @@
+use strict;
+use Test::More;
+
+use lib 't';
+use Support::TestSetup;
+use Test::Exception;
+use SL::DB::Unit;
+use SL::DB::Part;
+use SL::DB::Assembly;
+use SL::Dev::Part qw(new_assembly);
+use SL::DB::Helper::ValidateAssembly;
+
+Support::TestSetup::login();
+$::locale        = Locale->new("en");
+
+clear_up();
+reset_state();
+
+is( SL::DB::Manager::Part->get_all_count(), 4,  "total number of parts created by reset_state() is 4");
+
+my $assembly_part      = SL::DB::Manager::Part->find_by( partnumber => '19000' )   || die "Can't find assembly 19000";
+my $assembly_item_part = SL::DB::Manager::Part->find_by( partnumber => '19000 1' ) || die "Can't find assembly item part '19000 1'";
+
+is($assembly_part->part_type, 'assembly', 'assembly has correct type');
+is( scalar @{$assembly_part->assemblies}, 3, 'assembly consists of three parts' );
+
+# fetch assembly item corresponding to partnumber 19000
+my $assembly_items = $assembly_part->find_assemblies( { parts_id => $assembly_item_part->id } ) || die "can't find assembly_item";
+my $assembly_item = $assembly_items->[0];
+is($assembly_item->part->partnumber, '19000 1', 'assembly part part relation works');
+is($assembly_item->assembly_part->partnumber, '19000', 'assembly part assembly part relation works');
+
+
+
+my $assembly2_part = new_assembly( partnumber => '20000', assnumber => 'as2' )->save;
+my $retval = validate_assembly($assembly_part,$assembly2_part);
+ok(!defined $retval, 'assembly 19000 can be child of assembly 20000' );
+$assembly2_part->add_assemblies(SL::DB::Assembly->new(parts_id => $assembly_part->id, qty => 3, bom => 1));
+$assembly2_part->save;
+
+my $assembly3_part = new_assembly( partnumber => '30000', assnumber => 'as3' )->save;
+$retval = validate_assembly($assembly3_part,$assembly_part);
+ok(!defined $retval, 'assembly 30000 can be child of assembly 19000' );
+
+$retval = validate_assembly($assembly3_part,$assembly2_part);
+ok(!defined $retval, 'assembly 30000 can be child of assembly 20000' );
+
+$assembly_part->add_assemblies(SL::DB::Assembly->new(parts_id => $assembly3_part->id, qty => 4, bom => 1));
+$assembly_part->save;
+
+$retval = validate_assembly($assembly3_part,$assembly2_part);
+ok(!defined $retval, 'assembly 30000 can be child of assembly 20000' );
+
+$assembly2_part->add_assemblies(SL::DB::Assembly->new(parts_id => $assembly3_part->id, qty => 5, bom => 1));
+$assembly2_part->save;
+
+# fetch assembly item corresponding to partnumber 20000
+my $assembly2_items = $assembly2_part->find_assemblies() || die "can't find assembly_item";
+is( scalar @{$assembly2_items}, 5, 'assembly2 consists of ive parts' );
+my $assembly2_item = $assembly2_items->[3];
+is($assembly2_item->qty, 3, 'qty of 3rd assembly item is 3' );
+is($assembly2_item->part->part_type, 'assembly', '3rd assembly item \'' . $assembly2_item->part->partnumber . '\' is also an assembly');
+my $assembly3_items = $assembly2_item->part->find_assemblies() || die "can't find assembly_item";
+is( scalar @{$assembly3_items}, 4, 'assembly3 consists of four parts' );
+
+
+
+# check loop to itself
+$retval = validate_assembly($assembly_part,$assembly_part);
+is( $retval,"The assembly '19000' cannot be a part from itself.", 'assembly loops to itself' );
+if (!$retval && $assembly_part->add_assemblies( SL::DB::Assembly->new(parts_id => $assembly_part->id, qty => 8, bom => 1))) {
+  $assembly_part->save;
+}
+is( scalar @{$assembly_part->assemblies}, 4, 'assembly consists of four parts' );
+
+# check indirekt loop
+$retval = validate_assembly($assembly2_part,$assembly_part);
+ok( $retval, 'assembly indirect loop' );
+if (!$retval && $assembly_part->add_assemblies( SL::DB::Assembly->new(parts_id => $assembly2_part->id, qty => 9, bom => 1))) {
+  $assembly_part->save;
+}
+is( scalar @{$assembly_part->assemblies}, 4, 'assembly consists of four parts' );
+
+clear_up();
+done_testing;
+
+sub clear_up {
+  SL::DB::Manager::Assembly->delete_all(all => 1);
+  SL::DB::Manager::Part->delete_all(    all => 1);
+};
+
+sub reset_state {
+  my %params = @_;
+
+  my $assembly = new_assembly( assnumber => '19000', partnumber => '19000' )->save;
+};
+
+1;
diff --git a/t/part/assortment.t b/t/part/assortment.t
new file mode 100644 (file)
index 0000000..46e35de
--- /dev/null
@@ -0,0 +1,41 @@
+use strict;
+use Test::More;
+
+use lib 't';
+use Support::TestSetup;
+use Carp;
+use Test::Exception;
+use SL::DB::Part;
+use SL::Dev::Part qw(new_assortment);
+
+Support::TestSetup::login();
+
+clear_up();
+
+my $assortment = new_assortment( assnumber       => 'aso1',
+                                 description     => "Assortment 1",
+                                 number_of_parts => 10,
+                                                 )->save;
+
+is( SL::DB::Manager::Part->get_all_count(), 11,  "total number of parts created is 11");
+
+$assortment = SL::DB::Manager::Part->find_by( partnumber => 'aso1' ) or die "Can't find assortment with partnumber aso1";
+
+is($assortment->part_type,                  'assortment', 'assortment has correct part_type');
+is(scalar @{$assortment->assortment_items},  10,          'assortment has 10 parts');
+is($assortment->items_sellprice_sum,        100,          'assortment sellprice sum ok');
+is($assortment->items_lastcost_sum,          50,          'assortment lastcost sum ok');
+
+my $assortment_item = $assortment->assortment_items->[0];
+is( $assortment_item->assortment->partnumber, 'aso1', "assortment_item links back to correct assortment");
+
+clear_up();
+done_testing;
+
+sub clear_up {
+  SL::DB::Manager::AssortmentItem->delete_all(all => 1);
+  SL::DB::Manager::Part->delete_all(          all => 1);
+};
+
+
+1;
diff --git a/t/part/stock.t b/t/part/stock.t
new file mode 100644 (file)
index 0000000..39e5756
--- /dev/null
@@ -0,0 +1,61 @@
+use strict;
+use Test::More;
+
+use lib 't';
+use Support::TestSetup;
+use Carp;
+use Test::Exception;
+use SL::DB::Part;
+use SL::Dev::Part qw(new_part);
+use SL::Dev::Inventory qw(create_warehouse_and_bins set_stock transfer_stock);
+
+Support::TestSetup::login();
+
+clear_up();
+
+my ($wh1, $bin1_1) = create_warehouse_and_bins(
+  warehouse_description => 'Testlager',
+  bin_description       => 'Testlagerplatz',
+  number_of_bins        => 2,
+);
+my $bin1_2 = $wh1->bins->[1];
+my ($wh2, $bin2_1) = create_warehouse_and_bins(
+  warehouse_description => 'Testlager 2',
+  bin_description       => 'Testlagerplatz 2',
+  number_of_bins        => 2,
+);
+
+my $today     = DateTime->today;
+my $yesterday = $today->clone->add(days => -1);
+
+my $part = new_part()->save;
+set_stock(part => $part, bin_id => $bin1_1->id, qty => 7, shippingdate => $yesterday);
+set_stock(part => $part, bin_id => $bin1_1->id, qty => 5);
+set_stock(part => $part, bin_id => $bin1_1->id, abs_qty => 8); # apply -4 to get qty 8 in bin1_1
+set_stock(part => $part, bin_id => $bin1_2->id, qty => 9);
+
+set_stock(part => $part, bin_id => $bin2_1->id, abs_qty => 10);
+transfer_stock(part     => $part,
+               from_bin => $wh2->bins->[0],
+               to_bin   => $wh2->bins->[1],
+               qty      => 2,
+              );
+
+is( SL::DB::Manager::Part->get_all_count(), 1,  "total number of parts created is 1");
+is( $part->get_stock == 27                                     , 1 , "total stock of part is 27");
+is( $part->get_stock(shippingdate => $yesterday) == 7          , 1 , "total stock of part was 7 yesterday");
+is( $part->get_stock(shippingdate => $today) == 27             , 1 , "total stock of part is 27");
+is( $part->get_stock(bin_id       => $bin1_1->id) == 8         , 1 , "total stock of part in bin1_1 is 8");
+is( $part->get_stock(warehouse_id => $wh1->id) == 17           , 1 , "total stock of part in wh1 is 17");
+is( $part->get_stock(warehouse_id => $wh2->id) == 10           , 1 , "total stock of part in wh2 is 10");
+is( $part->get_stock(bin_id       => $wh2->bins->[0]->id) == 8 , 1 , "total stock of part in wh2 2nd bin is 8 after transfer");
+is( $part->get_stock(bin_id       => $wh2->bins->[1]->id) == 2 , 1 , "total stock of part in wh2 2nd bin is 2 after transfer");
+
+clear_up();
+done_testing;
+
+sub clear_up {
+  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(Inventory Part Bin Warehouse);
+}
+
+1;
diff --git a/t/pay_posting_import/datev.csv b/t/pay_posting_import/datev.csv
new file mode 100644 (file)
index 0000000..7f31ef5
--- /dev/null
@@ -0,0 +1,14 @@
+"DTVF";700;21;"Buchungsstapel";9;20210705140408955;20210705140408939;"LO";"";"kivitendo";49999;40392;20210101;6;20210601;20210630;"Lohn-Buchungen 06/2021";"LG";1;0;0;"EUR";;"";;55464;"";;;"";"004999940392F08"
+Umsatz;S/H;;;;;Konto;Gegenkonto (ohne BU-Schlüssel);;Belegdatum;Belegfeld 1;Belegfeld 2;;Buchungstext;;;;;;;;;;;;;;;;;;;;;;;KOST1;KOST2;KOST Menge;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Festschreibung
+2455,11;H;;;;;379000;136900;;3006;202106;;;AAG 06/2021 AOK Baden-Württemberg;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0
+230;H;;;;;379000;136900;;3006;202106;;;AAG 06/2021 BKK Debeka;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0
+142,31;H;;;;;379000;136900;;3006;202106;;;AAG 06/2021 IKK classic;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0
+43872,97;S;;;;;379000;372000;;3006;202106;;;AUSZAHLUNGSVERBINDL.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0
+1787,21;S;;;;;379000;372500;;3006;202106;;;Verbindl. Einbehaltung Arbeitn;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0
+10808,16;S;;;;;379000;373000;;3006;202106;;;VERBINDL.FINANZAMT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0
+23109,51;S;;;;;379000;374000;;3006;202106;;;VERBINDL.KRANKENKASSEN;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0
+40;S;;;;;379000;377000;;3006;202106;;;Verb. aus Vermögensbildung;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0
+384;S;;;;;379000;494700;;3006;202106;;;KFZ-NUTZUNG;;;;;;;;;;;;;;;;;;;;;;;wisavis;2016;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0
+623,57;S;;;;;379000;494700;;3006;202106;;;KFZ-NUTZUNG;;;;;;;;;;;;;;;;;;;;;;;wisavis;2018;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0
+662,65;S;;;;;379000;494700;;3006;202106;;;KFZ-NUTZUNG;;;;;;;;;;;;;;;;;;;;;;;wisavis;2019;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0
+298;S;;;;;379000;494700;;3006;202106;;;Diff. USt-MindestBMG (Firmenwagen);;;;;;;;;;;;;;;;;;;;;;;wisavis;2021;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0
diff --git a/t/pay_posting_import/datev_import.t b/t/pay_posting_import/datev_import.t
new file mode 100644 (file)
index 0000000..91934de
--- /dev/null
@@ -0,0 +1,127 @@
+use strict;
+use Test::More;
+use Test::Exception;
+
+use lib 't';
+
+use_ok 'Support::TestSetup';
+
+use SL::Controller::PayPostingImport;
+
+use utf8;
+use Data::Dumper;
+use File::Slurp;
+use Text::CSV_XS qw (csv);
+
+Support::TestSetup::login();
+
+my $dbh = SL::DB->client->dbh;
+my @charts = qw(379000 136900 372000 372500 373000 374000 377000 494700);
+local $::locale = Locale->new('en');
+note("init csv");
+clear_up();
+
+# datev naming convention and expected filename entry in $::form
+$::form->{ATTACHMENTS}{file}{filename} = 'DTVF_44979_43392_LOHNBUCHUNGEN_LUG_202106_20210623_0946';
+$::form->{file}                        = read_file('t/pay_posting_import/datev.csv');
+my $source                             = $::form->{ATTACHMENTS}{file}{filename};
+
+# get data as aoa datev encodes always CP1252
+my $csv_array = csv (in        => "t/pay_posting_import/datev.csv",
+                     binary    => 0,
+                     auto_diag => 1, sep_char => ";", encoding=> "cp1252");
+
+# probably no correct charts in test db
+throws_ok{
+  SL::Controller::PayPostingImport::parse_and_import();
+ } qr/No such Chart 379000/, "Importing Pay Postings without correct charts";
+
+# create charts
+foreach my $accno (@charts) {
+  SL::DB::Chart->new(
+    accno          => $accno,
+    description    => 'Löhne mit Gestöhne',
+    charttype      => 'A',
+    category       => 'Q',
+    link           => '',
+    taxkey_id      => '0',
+    datevautomatik => 'f',
+  )->save;
+}
+
+# and add department (KOST1 description)
+  SL::DB::Department->new(
+    description => 'Total falsche Abteilung, niemals zuordnen!'
+  )->save;
+
+  SL::DB::Department->new(
+    description => '2. Total falsche Abteilung, niemals zuordnen!'
+  )->save;
+
+  SL::DB::Department->new(
+    description => '3. Total falsche Abteilung, niemals zuordnen!'
+  )->save;
+
+  SL::DB::Department->new(
+    description => 'annahme stelle. Total falsche Abteilung, niemals zuordnen!'
+  )->save;
+
+
+  SL::DB::Department->new(
+    description => 'Wisavis'
+  )->save;
+
+SL::Controller::PayPostingImport::parse_and_import();
+
+# get all gl imported bookings
+my $gl_bookings = SL::DB::Manager::GLTransaction->get_all(where => [imported => 1] );
+
+# $i number of real data entries in the array (first two rows are headers)
+my $i = 2;
+is(scalar @{ $csv_array } - $i, scalar @{ $gl_bookings }, "Correct number of imported Pay Posting Bookings");
+
+# check all imported bookings
+foreach my $booking (@{ $gl_bookings }) {
+  my $current_row = $csv_array->[$i];
+
+  my $accno_credit = $current_row->[1] eq 'S' ? $current_row->[7] : $current_row->[6];
+  my $accno_debit  = $current_row->[1] eq 'S' ? $current_row->[6] : $current_row->[7];
+  my $amount       = $::form->parse_amount({ numberformat => '1000,00' }, $current_row->[0]);
+
+  # gl
+  is ($current_row->[13], $booking->reference, "Buchungstext correct");
+  if ($current_row->[36] eq 'wisavis') {
+    is(ref $booking->department eq 'SL::DB::Department', 1, "Department assigned");
+    is ($current_row->[36], 'wisavis', "Department correctly assigned");                # lowercase
+    is ('Wisavis', $booking->department->description, "Department correctly assigned"); # upper case
+  } else {
+    is ($current_row->[36], '', "No Department correctly assigned");
+
+  }
+  is ($source, $booking->transactions->[0]->source, "Source 0 correctly assigned");
+  is ($source, $booking->transactions->[1]->source, "Source 1 correctly assigned");
+
+  # acc_trans
+  cmp_ok ($amount,      '==',  $booking->transactions->[0]->amount, "Correct amount Soll");
+  cmp_ok ($amount * -1, '==',  $booking->transactions->[1]->amount, "Correct amount Haben");
+  is (ref $booking->transdate, 'DateTime', "Booking has a Transdate");
+  is ($accno_credit, $booking->transactions->[0]->chart->accno, "Sollkonto richtig");
+  is ($accno_debit, $booking->transactions->[1]->chart->accno, "Habenkonto richtig");
+
+  $i++;
+}
+
+clear_up();
+
+
+done_testing();
+
+1;
+
+sub clear_up {
+  SL::DB::Manager::AccTransaction->delete_all( all => 1);
+  SL::DB::Manager::GLTransaction->delete_all(  all => 1);
+  foreach my $accno (@charts) {
+    SL::DB::Manager::Chart->delete_all(where => [ accno => $accno ] );
+  }
+};
index 4e389f9..1809a97 100644 (file)
@@ -1,6 +1,6 @@
 use strict;
 use Test::Exception;
-use Test::More;
+use Test::More tests => 11;
 
 use lib 't';
 use Support::TestSetup;
@@ -9,12 +9,6 @@ use SL::Presenter;
 
 Support::TestSetup::login();
 
-if (!Support::TestSetup::templates_cache_writable()) {
-  plan skip_all => 'Cache dir not writable for this test';
-} else {
-  plan tests => 11;
-}
-
 my $pr = SL::Presenter->get;
 
 # Passing invalid parameters:
index 590f5cf..60b2d33 100644 (file)
@@ -45,6 +45,15 @@ f { a => [ { c => 1, d => 2 }, { c => 3, d => 4 }, ] },
   [ 'a[].d', 4  ],
 ], 'array of hashes';
 
+f { a => [ { a => 1, b => 2 }, { a => 3, c => 4 }, ] },
+[
+  [ 'a[+].a', 1 ],
+  [ 'a[].b', 2 ],
+  [ 'a[+].a', 3 ],
+  [ 'a[].c', 4  ],
+], 'array of hashes with not existing keys';
+
+
 # tests from Hash::Flatten below
 f {
   'x' => 1,
index 767807b..bae6a43 100644 (file)
@@ -2,7 +2,6 @@ use strict;
 use utf8;
 
 use lib 't';
-use lib 'modules/fallback';
 BEGIN {
   unshift @INC, 'modules/override';
 }
@@ -47,7 +46,7 @@ my $t_cmp = {
                         'charset' => undef,
                         'apply_buchungsgruppe' => undef,
                         'full_preview' => undef,
-                        'parts_type' => undef,
+                        'part_type' => undef,
                         'default_unit' => undef,
                         'default_buchungsgruppe' => undef,
                         'duplicates' => undef,
@@ -83,7 +82,7 @@ is_deeply $tt,
                         'charset' => 'UTF-8',
                         'apply_buchungsgruppe' => 'all',
                         'full_preview' => '0',
-                        'parts_type' => 'part',
+                        'part_type' => 'part',
                         'default_unit' => 'g',
                         'default_buchungsgruppe' => '815',
                         'duplicates' => 'no_check',
@@ -104,4 +103,3 @@ is_deeply $tt,
           'action' => 'CsvImport/dispatch',
           'FILENAME' => 'from_wikipedia.csv'
         };
-
index c3ced0f..e471de6 100644 (file)
@@ -67,7 +67,7 @@ Content-Disposition: form-data; name="settings.shoparticle_if_missing"
 
 0
 -----------------------------23281168279961
-Content-Disposition: form-data; name="settings.parts_type"
+Content-Disposition: form-data; name="settings.part_type"
 
 part
 -----------------------------23281168279961
diff --git a/t/run.sh b/t/run.sh
new file mode 100755 (executable)
index 0000000..4b8b13d
--- /dev/null
+++ b/t/run.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+dir="$(dirname "$0")"
+
+for TEST in "$@"; do
+  perl "-I${dir}/../modules/override" "-I${dir}/.." "-I${dir}/../modules/fallback" "$TEST"
+done
diff --git a/t/shop/json_ok.json b/t/shop/json_ok.json
new file mode 100644 (file)
index 0000000..a66bcde
--- /dev/null
@@ -0,0 +1 @@
+{"data":{"id":2,"number":"20001","customerId":1,"paymentId":5,"dispatchId":9,"partnerId":"","shopId":1,"invoiceAmount":153.85,"invoiceAmountNet":129.29,"invoiceShipping":3.9,"invoiceShippingNet":3.28,"orderTime":"2017-09-07T14:15:44+0200","transactionId":"","comment":"","customerComment":"","internalComment":"","net":0,"taxFree":0,"temporaryId":"","referer":"","clearedDate":null,"trackingCode":"","languageIso":"1","currency":"EUR","currencyFactor":1,"remoteAddress":"91.36.133.94","deviceType":"desktop","details":[{"id":2,"orderId":2,"articleId":2,"taxId":1,"taxRate":19,"statusId":0,"number":"20001","articleNumber":"SW10002","price":149.95,"quantity":1,"articleName":"TITANIUM CARBON GS 120 cm","shipped":0,"shippedGroup":0,"releaseDate":"-0001-11-30T00:00:00+0100","mode":0,"esdArticle":0,"config":"","ean":"","unit":"","packUnit":"Paar","attribute":{"id":2,"orderDetailId":2,"attribute1":"","attribute2":null,"attribute3":null,"attribute4":null,"attribute5":null,"attribute6":null}}],"documents":[],"payment":{"id":5,"name":"prepayment","description":"Vorkasse","template":"prepayment.tpl","class":"prepayment.php","table":"","hide":false,"additionalDescription":"Sie zahlen einfach vorab und erhalten die Ware bequem und g\u00fcnstig bei Zahlungseingang nach Hause geliefert.","debitPercent":0,"surcharge":0,"surchargeString":"","position":1,"active":true,"esdActive":false,"mobileInactive":false,"embedIFrame":"","hideProspect":0,"action":null,"pluginId":null,"source":null,"attribute":null},"paymentStatus":{"id":17,"name":"open","description":"Offen","position":0,"group":"payment","sendMail":0},"orderStatus":{"id":0,"name":"open","description":"Offen","position":1,"group":"state","sendMail":1},"customer":{"number":"20003","id":1,"paymentId":5,"groupKey":"EK","shopId":1,"priceGroupId":null,"encoderName":"bcrypt","hashPassword":"$2y$10$.TqdM5h2qNqtJbzKF7GpP.kUXWL2UOylkgbqKBU6WuOpDladvkGva","active":true,"email":"werner.hahn@vulke.de","firstLogin":"2017-09-07T00:00:00+0200","lastLogin":"2017-09-07T14:15:44+0200","accountMode":1,"confirmationKey":"","sessionId":"10i196b4lm3r1gjdnurkq18j22","newsletter":0,"validation":"","affiliate":0,"paymentPreset":0,"languageId":"1","referer":"","internalComment":"","failedLogins":0,"lockedUntil":null,"salutation":"mr","title":null,"firstname":"Kalli","lastname":"Kr\u00fcger","birthday":null},"paymentInstances":[{"orderId":2,"firstName":"Kalli","lastName":"Kr\u00fcger","address":"Dorfstr. 6","zipCode":"37318","city":"Sch\u00f6nhagen","bankName":null,"bankCode":null,"accountNumber":null,"accountHolder":null,"bic":null,"iban":null,"amount":"153.8500","createdAt":"2017-09-07T00:00:00+0200","id":1}],"billing":{"title":null,"additionalAddressLine1":null,"additionalAddressLine2":null,"id":1,"orderId":2,"customerId":1,"countryId":2,"stateId":null,"company":"","department":"","salutation":"mr","number":"20003","firstName":"Kalli","lastName":"Kr\u00fcger","street":"Dorfstr. 6","zipCode":"37318","city":"Sch\u00f6nhagen","phone":"","vatId":null,"country":{"id":2,"name":"Deutschland","iso":"DE","isoName":"GERMANY","position":1,"description":"","taxFree":0,"taxFreeUstId":0,"taxFreeUstIdChecked":0,"active":true,"iso3":"DEU","displayStateInRegistration":false,"forceStateInRegistration":false,"areaId":1},"state":null,"attribute":{"id":1,"orderBillingId":1,"text1":null,"text2":null,"text3":null,"text4":null,"text5":null,"text6":null}},"shipping":{"title":"","additionalAddressLine1":"","additionalAddressLine2":"","id":1,"orderId":2,"countryId":2,"stateId":null,"customerId":1,"company":"","department":"","salutation":"mr","firstName":"Kalli","lastName":"Kr\u00fcger","street":"Dorfstr. 6","zipCode":"37318","city":"Sch\u00f6nhagen","attribute":{"id":1,"orderShippingId":1,"text1":null,"text2":null,"text3":null,"text4":null,"text5":null,"text6":null},"country":{"id":2,"name":"Deutschland","iso":"DE","isoName":"GERMANY","position":1,"description":"","taxFree":0,"taxFreeUstId":0,"taxFreeUstIdChecked":0,"active":true,"iso3":"DEU","displayStateInRegistration":false,"forceStateInRegistration":false,"areaId":1},"state":null},"shop":{"id":1,"mainId":null,"categoryId":3,"name":"Demoshop","title":null,"position":0,"host":"shopware.vulke.de","basePath":"","baseUrl":null,"hosts":"shopware.vulke.de","secure":false,"alwaysSecure":false,"secureHost":null,"secureBasePath":null,"templateId":23,"default":true,"active":true,"customerScope":false},"dispatch":{"id":9,"name":"Standard Versand","type":0,"description":"","comment":"","active":true,"position":0,"calculation":0,"surchargeCalculation":3,"taxCalculation":0,"shippingFree":null,"multiShopId":null,"customerGroupId":null,"bindShippingFree":0,"bindTimeFrom":null,"bindTimeTo":null,"bindInStock":null,"bindLastStock":0,"bindWeekdayFrom":null,"bindWeekdayTo":null,"bindWeightFrom":null,"bindWeightTo":null,"bindPriceFrom":null,"bindPriceTo":null,"bindSql":null,"statusLink":"","calculationSql":null,"attribute":null},"attribute":{"id":2,"orderId":2,"attribute1":null,"attribute2":null,"attribute3":null,"attribute4":null,"attribute5":null,"attribute6":null},"languageSubShop":{"id":1,"mainId":null,"categoryId":3,"name":"Demoshop","title":null,"position":0,"host":"shopware.vulke.de","basePath":"","baseUrl":null,"hosts":"shopware.vulke.de","secure":false,"alwaysSecure":false,"secureHost":null,"secureBasePath":null,"templateId":23,"default":true,"active":true,"customerScope":false,"locale":{"id":1,"locale":"de_DE","language":"Deutsch","territory":"Deutschland"}},"paymentStatusId":17,"orderStatusId":0},"success":true}
diff --git a/t/shop/shop_order.t b/t/shop/shop_order.t
new file mode 100644 (file)
index 0000000..e40ec2f
--- /dev/null
@@ -0,0 +1,170 @@
+use strict;
+use Test::More;
+
+use lib 't';
+use Support::TestSetup;
+use Carp;
+use Test::Exception;
+use SL::DBUtils qw(check_trgm);
+use SL::Dev::ALL;
+use SL::Dev::Part qw(new_part);
+use SL::Dev::Shop qw(new_shop new_shop_part new_shop_order);
+use SL::Dev::CustomerVendor qw(new_customer);
+use SL::DB::Shop;
+use SL::DB::ShopOrder;
+use SL::DB::ShopOrderItem;
+use SL::Controller::ShopOrder;
+use Data::Dumper;
+
+my ($shop, $shop_order, $shop_part, $part, $customer, $employee);
+my ($transdate);
+
+sub reset_state {
+  my %params = @_;
+
+  clear_up();
+
+  $transdate = DateTime->today_local;
+  $transdate->set_year(2019) if $transdate->year == 2020; # use year 2019 in 2020, because of tax rate change in Germany
+
+  $shop = new_shop->save;
+  $part = new_part->save;
+  $shop_part = new_shop_part(part => $part, shop => $shop)->save;
+
+  $employee = SL::DB::Manager::Employee->current || croak "No employee";
+
+  $customer = new_customer( name    => 'Evil Inc',
+                            street  => 'Evil Street',
+                            zipcode => '66666',
+                            email   => 'evil@evilinc.com'
+                          )->save;
+}
+
+sub save_shopcontroller_to_string {
+
+  my $output;
+  open(my $outputFH, '>', \$output) or die "OUTPUT";
+  my $oldFH = select $outputFH;
+  my $shop_controller = SL::Controller::ShopOrder->new;
+  $shop_controller->action_transfer;
+
+  select $oldFH;
+  close $outputFH;
+  return $output;
+}
+sub test_transfer {
+  my ( %params ) = @_;
+  $::form = Support::TestSetup->create_new_form;
+  $::form->{import_id} = $params{import_id};
+  $::form->{customer} =  $params{customer};
+  my $test_name = 'Test Controller Action Transfer';
+  save_shopcontroller_to_string();
+  my @links_record = RecordLinks->get_links( 'from_table' => 'shop_orders',
+                                            'from_id'    => $params{import_id},
+                                            'to_table'   => 'oe',
+                                          );
+  is($links_record[0]->{from_id}    , $params{import_id}, "record from id check");
+  is($links_record[0]->{from_table} , 'shop_orders'     , "record from table <shop_orders> check");
+  is($links_record[0]->{to_table}   , 'oe'              , "record to table <oe> check");
+}
+
+Support::TestSetup::login();
+
+reset_state();
+
+my $trgm = check_trgm($::form->get_standard_dbh());
+
+my $shop_trans_id = 1;
+
+$shop_order = new_shop_order(
+  shop              => $shop,
+  transfer_date     => $transdate,
+  shop_trans_id     => $shop_trans_id,
+  order_date        => $transdate->datetime,
+  amount            => 59.5,
+  billing_lastname  => 'Schmidt',
+  billing_firstname => 'Sven',
+  billing_company   => 'Evil Inc',
+  billing_street    => 'Evil Street 666',
+  billing_zipcode   => $customer->zipcode,
+  billing_email     => 'email',
+);
+
+my $shop_order_item = SL::DB::ShopOrderItem->new(
+  partnumber    => $part->partnumber,
+  position      => 1,
+  quantity      => 5,
+  price         => 10,
+  shop_trans_id => $shop_trans_id,
+);
+$shop_order->shop_order_items( [ $shop_order_item ] );
+$shop_order->save;
+
+note('testing check_for_existing_customers');
+my $fuzzy_customers = $shop_order->check_for_existing_customers;
+
+is(scalar @{ $fuzzy_customers }, 1, 'found 1 matching customer');
+is($fuzzy_customers->[0]->name, 'Evil Inc', 'matched customer Evil Inc');
+
+note('adding a not-so-similar customer');
+my $customer_different = new_customer(
+  name    => "Different Name",
+  street  => 'Good Straet', # difference large enough from "Evil Street"
+  zipcode => $customer->zipcode,
+  email   => "foo",
+)->save;
+$fuzzy_customers = $shop_order->check_for_existing_customers;
+is(scalar @{ $fuzzy_customers }, 1, 'still only found 1 matching customer (zipcode equal + street dissimilar');
+
+note('adding 2 similar customers and 1 dissimilar but same email');
+my $customer_similar = new_customer(
+  name    => "Different Name",
+  street  => 'Evil Street 666', # difference not large enough from "Evil Street", street matches
+  zipcode => $customer->zipcode,
+  email   => "foo",
+)->save;
+my $customer_similar_2 = new_customer(
+  name    => "Different Name",
+  street  => 'Evil Straet', # difference not large enough from "Evil Street", street matches
+  zipcode => $customer->zipcode,
+  email   => "foofoo",
+)->save;
+my $customer_same_email = new_customer(
+  name    => "Different Name",
+  street  => 'Angel Way', # difference large enough from "Evil Street", street not matches , same email
+  zipcode => $customer->zipcode,
+  email   => 'email',
+)->save;
+my $customers = SL::DB::Manager::Customer->get_all();
+
+$fuzzy_customers = $shop_order->check_for_existing_customers;
+if($trgm){
+  is(scalar @{ $fuzzy_customers }, 4, 'found 4 matching customers (zipcode equal + street similar + same email) trgm_pg is installed');
+}else{
+  is(scalar @{ $fuzzy_customers }, 3, 'found 3 matching customers (zipcode equal + %street% + same email) trgm_pg is not installed, could be 4 with trgm_pg');
+}
+
+is($shop->description   , 'testshop' , 'shop description ok');
+is($shop_order->shop_id , $shop->id  , "shop_id ok");
+
+note('testing convert_to_sales_order');
+my $order = $shop_order->convert_to_sales_order(employee => $employee, customer => $customer, transdate => $shop_order->order_date);
+$order->calculate_prices_and_taxes;
+$order->save;
+
+is(ref($order), 'SL::DB::Order', 'order ok');
+is($order->amount,    59.5, 'order amount ok');
+is($order->netamount, 50,   'order netamount ok');
+
+test_transfer( import_id => $shop_order->id , customer => $customer->id );
+
+done_testing;
+
+clear_up();
+
+1;
+
+sub clear_up {
+  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(OrderItem Order);
+  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(ShopPart Part ShopOrderItem ShopOrder Shop Customer);
+}
diff --git a/t/shop/shopware.t b/t/shop/shopware.t
new file mode 100644 (file)
index 0000000..3f78498
--- /dev/null
@@ -0,0 +1,85 @@
+use strict;
+use Test::More;
+
+use lib 't';
+use Support::TestSetup;
+use Carp;
+use Test::Exception;
+use SL::Dev::ALL;
+use SL::Dev::Part qw(new_part);
+use SL::Dev::Shop qw(new_shop new_shop_part);
+use SL::Dev::CustomerVendor qw(new_customer);
+use SL::DB::Shop;
+use SL::DB::ShopOrder;
+use SL::DB::ShopOrderItem;
+use SL::Controller::ShopOrder;
+use SL::Shop;
+use Data::Dumper;
+use SL::JSON;
+use SL::ShopConnector::Shopware;
+my ($shop, $shopware, $shop_order, $shop_part, $part, $customer, $employee, $json_import);
+
+sub reset_state {
+  my %params = @_;
+
+  clear_up();
+
+  $shop = new_shop( connector         => 'shopware',
+                    last_order_number => 20000,
+                    pricetype         => 'brutto',
+                    price_source      => 'master_data',
+                    taxzone_id        => 1,
+                  );
+  $shopware = SL::Shop->new( config => $shop );
+  $part = new_part( partnumber   => 'SW10002',
+                    description  => 'TITANIUM CARBON GS 12m cm',
+                  );
+  $shop_part = new_shop_part(part => $part, shop => $shop);
+
+  $employee = SL::DB::Manager::Employee->current || croak "No employee";
+
+  $customer = new_customer( name    => 'Evil Inc',
+                            street  => 'Evil Street',
+                            zipcode => '66666',
+                            email   => 'evil@evilinc.com'
+                          )->save;
+}
+
+sub get_json {
+  local $/;
+  my $file = "t/shop/json_ok.json";
+  my $json_text = do {
+    open(my $json_fh, "<:encoding(UTF-8)", $file)
+         or die("Can't open \"$file\": $!\n");
+    local $/;
+    <$json_fh>
+  };
+
+  return $json_text;
+}
+
+sub test_import {
+
+  my $json_import = get_json();
+  note('testing shoporder mapping json good');
+  my $import = SL::JSON::decode_json($json_import);
+  $shop_order = $shopware->connector->import_data_to_shop_order($import);
+  is($shop_order->shop_id , $shop->id  , "shop_id ok");
+}
+
+Support::TestSetup::login();
+
+reset_state();
+
+test_import();
+
+done_testing;
+
+clear_up();
+
+1;
+
+sub clear_up {
+  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(OrderItem Order);
+  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(ShopPart Part ShopOrderItem ShopOrder Shop Customer);
+}
diff --git a/t/shop/woocommerce.t b/t/shop/woocommerce.t
new file mode 100644 (file)
index 0000000..fd984e7
--- /dev/null
@@ -0,0 +1,251 @@
+use strict;
+use Test::More;
+
+use lib 't';
+use Support::TestSetup;
+use Carp;
+use Test::Exception;
+use SL::Dev::ALL;
+use SL::Dev::Part qw(new_part);
+use SL::Dev::Shop qw(new_shop new_shop_part);
+use SL::Dev::CustomerVendor qw(new_customer);
+use SL::DB::Shop;
+use SL::DB::ShopOrder;
+use SL::DB::ShopOrderItem;
+use SL::Controller::ShopOrder;
+use SL::Shop;
+use Data::Dumper;
+use SL::JSON;
+use SL::ShopConnector::Shopware;
+my ($shop, $shopware, $shop_order, $shop_part, $part, $customer, $employee, $json_import);
+
+sub reset_state {
+  my %params = @_;
+
+  clear_up();
+
+  $shop = new_shop( connector         => 'woocommerce',
+                    last_order_number => 20000,
+                    pricetype         => 'brutto',
+                    price_source      => 'master_data',
+                    taxzone_id        => 1,
+                  );
+  $shopware = SL::Shop->new( config => $shop );
+  $part = new_part( partnumber   => 'SW10002',
+                    description  => 'TITANIUM CARBON GS 12m cm',
+                  );
+  $shop_part = new_shop_part(part => $part, shop => $shop);
+
+  $employee = SL::DB::Manager::Employee->current || croak "No employee";
+
+  $customer = new_customer( name    => 'Evil Inc',
+                            street  => 'Evil Street',
+                            zipcode => '66666',
+                            email   => 'evil@evilinc.com'
+                          )->save;
+}
+
+sub get_data {
+
+  my %data = (
+    '_links' => {
+                  'collection' => [
+                                    {
+                                      'href' => 'https://WOOCOMMERCESHOP/wp-json/wc/v3/orders'
+                                    }
+                                  ],
+                  'self' => [
+                              {
+                                'href' => 'https://WOOCOMMERCESHOP/wp-json/wc/v3/orders/8163'
+                              }
+                            ]
+                },
+    'billing' => {
+                   'address_1' => 'Hauptstrasse 52a',
+                   'address_2' => '',
+                   'city' => 'Halle',
+                   'company' => '',
+                   'country' => 'DE',
+                   'email' => 'test@test.de',
+                   'first_name' => 'Heike',
+                   'last_name' => 'Mustermann',
+                   'phone' => '12345',
+                   'postcode' => '06118',
+                   'state' => ''
+                 },
+    'cart_hash' => '4f978421d12277a81e8b6f83c02fba55',
+    'cart_tax' => '0.21',
+    'coupon_lines' => [],
+    'created_via' => 'checkout',
+    'currency' => 'EUR',
+    'currency_symbol' => "\x{20ac}",
+    'customer_id' => 0,
+    'customer_ip_address' => '888.888.888.888',
+    'customer_note' => '',
+    'customer_user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:87.0) Gecko/20100101 Firefox/87.0',
+    'date_completed' => '2021-04-13T10:04:12',
+    'date_completed_gmt' => '2021-04-13T08:04:12',
+    'date_created' => '2021-04-12T11:56:42',
+    'date_created_gmt' => '2021-04-12T09:56:42',
+    'date_modified' => '2021-04-13T10:04:12',
+    'date_modified_gmt' => '2021-04-13T08:04:12',
+    'date_paid' => '2021-04-12T11:56:44',
+    'date_paid_gmt' => '2021-04-12T09:56:44',
+    'discount_tax' => '0.00',
+    'discount_total' => '0.00',
+    'fee_lines' => [],
+    'id' => 8163,
+    'line_items' => [
+                      {
+                        'id' => 33594,
+                        'meta_data' => [
+                                         {
+                                           'display_key' => 'Verpackungseinheit',
+                                           'display_value' => "P\x{e4}ckchen mit 12 Samen",
+                                           'id' => 323242,
+                                           'key' => 'pa_verpackungseinheit',
+                                           'value' => 'paeckchen-mit-12-samen'
+                                         },
+                                         {
+                                           'display_key' => '_deliverytime',
+                                           'display_value' => '151',
+                                           'id' => 323243,
+                                           'key' => '_deliverytime',
+                                           'value' => '151'
+                                         }
+                                       ],
+                        'name' => "Wassermelone M 11 (Blacktail Mountain) - P\x{e4}ckchen mit 12 Samen",
+                        'parent_name' => 'Wassermelone M 11 (Blacktail Mountain)',
+                        'price' => '2.95',
+                        'product_id' => 4930,
+                        'quantity' => 1,
+                        'sku' => 'SW10002',
+                        'subtotal' => '2.95',
+                        'subtotal_tax' => '0.21',
+                        'tax_class' => 'reduced-rate',
+                        'taxes' => [
+                                     {
+                                       'id' => 255,
+                                       'subtotal' => '0.2065',
+                                       'total' => '0.2065'
+                                     }
+                                   ],
+                        'total' => '2.95',
+                        'total_tax' => '0.21',
+                        'variation_id' => 4931
+                      }
+                    ],
+    'meta_data' => [
+                     {
+                       'id' => 339538,
+                       'key' => '_billing_fax',
+                       'value' => ''
+                     },
+                     {
+                       'id' => 339539,
+                       'key' => '_shipping_fax',
+                       'value' => ''
+                     },
+                     {
+                       'id' => 339540,
+                       'key' => 'is_vat_exempt',
+                       'value' => 'no'
+                     },
+                     {
+                       'id' => 339541,
+                       'key' => 'wpml_language',
+                       'value' => 'de'
+                     }
+                   ],
+    'number' => '8163',
+    'order_key' => 'wc_order_HjusTgQrJZHFQ',
+    'parent_id' => 0,
+    'payment_method' => 'german_market_purchase_on_account',
+    'payment_method_title' => 'Kauf auf Rechnung',
+    'prices_include_tax' => '0',
+    'refunds' => [],
+    'shipping' => {
+                    'address_1' => 'Hauptstrasse 52a',
+                    'address_2' => '',
+                    'city' => 'Halle',
+                    'company' => '',
+                    'country' => 'DE',
+                    'first_name' => 'Heike',
+                    'last_name' => 'Mustermann',
+                    'postcode' => '06118',
+                    'state' => ''
+                  },
+    'shipping_lines' => [
+                          {
+                            'id' => 33595,
+                            'instance_id' => '1',
+                            'meta_data' => [
+                                             {
+                                               'display_key' => 'Positionen',
+                                               'display_value' => "Wassermelone M 11 (Blacktail Mountain) - P\x{e4}ckchen mit 12 Samen &times; 1",
+                                               'id' => 323249,
+                                               'key' => 'Positionen',
+                                               'value' => "Wassermelone M 11 (Blacktail Mountain) - P\x{e4}ckchen mit 12 Samen &times; 1"
+                                             }
+                                           ],
+                            'method_id' => 'flat_rate',
+                            'method_title' => 'Versandkostenpauschale',
+                            'taxes' => [
+                                         {
+                                           'id' => 255,
+                                           'subtotal' => '',
+                                           'total' => '0.16'
+                                         }
+                                       ],
+                            'total' => '2.34',
+                            'total_tax' => '0.16'
+                          }
+                        ],
+    'shipping_tax' => '0.16',
+    'shipping_total' => '2.34',
+    'status' => 'completed',
+    'tax_lines' => [
+                     {
+                       'compound' => '0',
+                       'id' => 33596,
+                       'label' => 'MwSt.',
+                       'meta_data' => [],
+                       'rate_code' => 'DE-MWST.-2',
+                       'rate_id' => 255,
+                       'rate_percent' => 7,
+                       'shipping_tax_total' => '0.16',
+                       'tax_total' => '0.21'
+                     }
+                   ],
+    'total' => '5.66',
+    'total_tax' => '0.37',
+    'transaction_id' => '',
+    'version' => '4.9.2'
+  );
+  return \%data;
+}
+
+sub test_import {
+
+  my $import = get_data();
+  note('testing shoporder mapping json good');
+  $shop_order = $shopware->connector->import_data_to_shop_order($import);
+  is($shop_order->shop_id , $shop->id  , "shop_id ok");
+}
+
+Support::TestSetup::login();
+
+reset_state();
+
+test_import();
+
+done_testing;
+
+clear_up();
+
+1;
+
+sub clear_up {
+  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(OrderItem Order);
+  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(ShopPart Part ShopOrderItem ShopOrder Shop Customer);
+}
old mode 100644 (file)
new mode 100755 (executable)
index c23c6b4..ab0ff3b
@@ -1,8 +1,11 @@
 #!/usr/bin/perl
 
 use strict;
+use threads;
 use lib 't';
 use Support::Files;
+use Sys::CPU;
+use Thread::Pool::Simple;
 
 my ($testcount);
 
@@ -38,9 +41,10 @@ my @common_errors = ([ '^\s*my\s+%[a-z0-9_]+\s*=\s*shift' ],
                      [ '\$slef'                           ],
                     );
 
-foreach my $file (@testitems) {
+sub test_file {
+  my ($file) = @_;
   $file =~ s/\s.*$//;           # nuke everything after the first space (#comment)
-  next if (!$file);             # skip null entries
+  return if (!$file);           # skip null entries
 
   if (open (FILE, $file)) {     # open the file for reading
     $_->[1] = [] foreach @common_errors;
@@ -67,5 +71,15 @@ foreach my $file (@testitems) {
   }
 }
 
-exit 0;
+my $pool = Thread::Pool::Simple->new(
+  min    => 2,
+  max    => Sys::CPU::cpu_count() + 1,
+  do     => [ \&test_file ],
+  passid => 0,
+);
+
+$pool->add($_) for @testitems;
 
+$pool->join;
+
+exit 0;
diff --git a/t/structure/double_colon_interpolation.t b/t/structure/double_colon_interpolation.t
new file mode 100644 (file)
index 0000000..2b0dce0
--- /dev/null
@@ -0,0 +1,57 @@
+use strict;
+use threads;
+use lib 't';
+use Support::Files;
+use Sys::CPU;
+use Test::More;
+use Thread::Pool::Simple;
+
+if (eval { require PPI; 1 }) {
+  plan tests => scalar(@Support::Files::testitems);
+} else {
+  plan skip_all => "PPI not installed";
+}
+
+my @testitems = @Support::Files::testitems;
+
+sub test_file {
+  my ($file) = @_;
+
+  my $clean = 1;
+  my $source;
+  {
+    local $^W = 0; # don't care about invalid chars in comments
+    local $/ = undef;
+    open my $fh, '<:utf8', $file or die $!;
+    $source = <$fh>;
+  }
+
+  my $doc = PPI::Document->new(\$source) or do {
+    print "?: PPI error for file $file: " . PPI::Document::errstr() . "\n";
+    ok 0, $file;
+    next;
+  };
+  my $stmts = $doc->find(sub { $_[1]->isa('PPI::Token::Quote::Double') || $_[1]->isa('PPI::Token::Quote::Interpolate') });
+
+  for my $stmt (@{ $stmts || [] }) {
+    my $content = $stmt->content;
+
+    if ($content =~ /(\$\w+::)\$/) {
+      print "?: @{[ $stmt->content ]} contains $1 \n";
+      $clean = 0;
+    }
+  }
+
+  ok $clean, $file;
+}
+
+my $pool = Thread::Pool::Simple->new(
+  min    => 2,
+  max    => Sys::CPU::cpu_count() + 1,
+  do     => [ \&test_file ],
+  passid => 0,
+);
+
+$pool->add($_) for @testitems;
+
+$pool->join;
index e442fa6..b252321 100755 (executable)
@@ -7,6 +7,7 @@ use File::Slurp;
 use Test::More;
 
 my %default_columns;
+my %compatibility_functions = map { ($_ => 1) } qw(address);
 
 sub read_default_columns {
   my $content   =  read_file('SL/DB/MetaSetup/Default.pm');
@@ -23,7 +24,7 @@ sub test_file_content {
   my $content = read_file($file);
 
   while ($content =~ m{(?:INSTANCE_CONF\.|\$(?:main)?::instance_conf->)get_([a-z0-9_]+)}gi) {
-    ok($default_columns{$1}, "'get_${1}' is a valid method call on \$::instance_conf in $file");
+    ok($default_columns{$1} || $compatibility_functions{$1}, "'get_${1}' is a valid method call on \$::instance_conf in $file");
   }
 }
 
index f0a54d9..d3e5fe0 100644 (file)
@@ -1,7 +1,10 @@
 use strict;
+use threads;
 use lib 't';
 use Support::Files;
+use Sys::CPU;
 use Test::More;
+use Thread::Pool::Simple;
 
 if (eval { require PPI; 1 }) {
   plan tests => scalar(@Support::Files::testitems);
@@ -11,7 +14,9 @@ if (eval { require PPI; 1 }) {
 
 my @testitems = @Support::Files::testitems;
 
-foreach my $file (@testitems) {
+sub test_file {
+  my ($file) = @_;
+
   my $clean = 1;
   my $source;
   {
@@ -56,3 +61,14 @@ foreach my $file (@testitems) {
 
   ok $clean, $file;
 }
+
+my $pool = Thread::Pool::Simple->new(
+  min    => 2,
+  max    => Sys::CPU::cpu_count() + 1,
+  do     => [ \&test_file ],
+  passid => 0,
+);
+
+$pool->add($_) for @testitems;
+
+$pool->join;
index 3f90693..b9f9c4c 100644 (file)
@@ -1,7 +1,10 @@
 use strict;
+use threads;
 use lib 't';
 use Support::Files;
+use Sys::CPU;
 use Test::More;
+use Thread::Pool::Simple;
 
 if (eval { require PPI; 1 }) {
   plan tests => scalar(@Support::Files::testitems);
@@ -23,7 +26,8 @@ my $fh;
 
 my @testitems = @Support::Files::testitems;
 
-foreach my $file (@testitems) {
+sub test_file {
+  my ($file) = @_;
   my $clean = 1;
   my $source;
   {
@@ -63,3 +67,14 @@ foreach my $file (@testitems) {
 
   ok $clean, $file;
 }
+
+my $pool = Thread::Pool::Simple->new(
+  min    => 2,
+  max    => Sys::CPU::cpu_count() + 1,
+  do     => [ \&test_file ],
+  passid => 0,
+);
+
+$pool->add($_) for @testitems;
+
+$pool->join;
diff --git a/t/tax/tax.t b/t/tax/tax.t
new file mode 100644 (file)
index 0000000..88638bb
--- /dev/null
@@ -0,0 +1,633 @@
+use Test::More tests => 48;
+use Test::Deep qw(cmp_deeply);
+
+use strict;
+
+use lib 't';
+use utf8;
+
+use Support::TestSetup;
+use Test::Exception;
+
+use SL::DB::BankTransaction;
+use SL::DB::BankTransactionAccTrans;
+use SL::DB::Customer;
+use SL::DB::Vendor;
+use SL::DB::Invoice;
+use SL::DB::GLTransaction;
+use SL::DB::AccTransaction;
+use SL::DB::Part;
+use SL::DB::PaymentTerm;
+use SL::DBUtils qw(selectall_hashref_query);
+use SL::Dev::Record qw(:ALL);
+use SL::Dev::CustomerVendor qw(new_customer new_vendor);
+use SL::Dev::Part qw(new_part);
+use SL::Dev::Payment qw(create_payment_terms);
+use Data::Dumper;
+
+Support::TestSetup::login();
+my $dbh = SL::DB->client->dbh;
+
+clear_up();
+
+# TODOs: Storno muß noch korrekt funktionieren
+#  neue Konten für 5% anlegen
+#  Leistungszeitraum vs. Datum Zuord. Steuerperiodest
+
+
+
+
+note('checking if all tax entries exist for Konjunkturprogramm');
+
+# create dates to test on
+my $date_2006   = DateTime->new(year => 2006, month => 6, day => 15);
+my $date_2020_1 = DateTime->new(year => 2020, month => 6, day => 15);
+my $date_2020_2 = DateTime->new(year => 2020, month => 7, day => 15);
+my $date_2021   = DateTime->new(year => 2021, month => 1, day => 15);
+
+# dummy bt_id
+my $bank_account     =  SL::DB::BankAccount->new(
+    account_number  => '123',
+    bank_code       => '123',
+    iban            => '123',
+    bic             => '123',
+    bank            => '123',
+    chart_id        => SL::DB::Manager::Chart->find_by( description => 'Bank' )->id,
+    name            => SL::DB::Manager::Chart->find_by( description => 'Bank' )->description,
+  )->save;
+
+
+my $currency_id     = $::instance_conf->get_currency_id;
+my  $bt = SL::DB::BankTransaction->new(
+    local_bank_account_id => $bank_account->id,
+    transdate             => $date_2021,
+    valutadate            => $date_2021,
+    amount                => 27332.32,
+    purpose               => 'dummy',
+    currency              => $currency_id,
+  );
+  $bt->save || die $@;
+
+
+# The only way to discern the pre-2007 16% tax from the 2020 16% tax is by
+# their configured automatic tax charts, so look them up here:
+
+my ($chart_vst_19, $chart_vst_16, $chart_vst_5, $chart_vst_7);
+my ($chart_ust_19, $chart_ust_16, $chart_ust_5, $chart_ust_7);
+my ($income_19_accno, $income_7_accno);
+my ($ar_accno, $ap_accno);
+my ($chart_reisekosten_accno, $chart_cash_accno, $chart_bank_accno);
+
+my ($skonto_5, $skonto_16, $skonto_7, $skonto_19); # store acc_trans entries during tests
+
+my $test_kontenrahmen = $::instance_conf->get_coa eq 'Germany-DATEV-SKR04EU' ? 'skr04' : 'skr03';
+
+if ( $test_kontenrahmen eq 'skr03' ) {
+
+  is(SL::DB::Default->get->coa, 'Germany-DATEV-SKR03EU', "coa SKR03 ok");
+
+  $chart_ust_19 = '1776';
+  $chart_ust_16 = '1775';
+  $chart_ust_5  = '1773';
+  $chart_ust_7  = '1771';
+
+  $chart_vst_19 = '1576';
+  $chart_vst_16 = '1575';
+  $chart_vst_5  = '1568';
+  $chart_vst_7  = '1571';
+
+  $income_19_accno = '8400';
+  $income_7_accno  = '8300';
+
+  $chart_reisekosten_accno = 4660;
+  $chart_cash_accno        = 1000;
+  $chart_bank_accno        = 1200;
+
+  $ar_accno = 1400;
+  $ap_accno = 1600;
+
+} elsif ( $test_kontenrahmen eq 'skr04') { # skr04 - test can be ran manually by running t/000setup_database.t with coa for SKR04
+  is(SL::DB::Default->get->coa, 'Germany-DATEV-SKR04EU', "coa SKR04 ok");
+  $chart_vst_19 = '1406';
+  $chart_vst_16 = '1405';
+  $chart_vst_5  = '1403';
+  $chart_vst_7  = '1401';
+
+  $chart_ust_19 = '3806';
+  $chart_ust_16 = '3805';
+  $chart_ust_5  = '3803';
+  $chart_ust_7  = '3801';
+
+  $income_19_accno = '4400';
+  $income_7_accno  = '4300';
+
+  $chart_reisekosten_accno = 6650;
+  $chart_cash_accno        = 1600;
+  $chart_bank_accno        = 1800;
+
+  $ar_accno = 1200;
+  $ap_accno = 3300;
+}
+
+my $tax_vst_19 = SL::DB::Manager::Chart->find_by(accno => $chart_vst_19) or die; # 19%
+my $tax_vst_16 = SL::DB::Manager::Chart->find_by(accno => $chart_vst_16) or die; # 16%
+my $tax_vst_5  = SL::DB::Manager::Chart->find_by(accno => $chart_vst_5 ) or die; #  5%
+my $tax_vst_7  = SL::DB::Manager::Chart->find_by(accno => $chart_vst_7 ) or die; #  7%
+
+my $tax_ust_19 = SL::DB::Manager::Chart->find_by(accno => $chart_ust_19) or die; # 19%
+my $tax_ust_16 = SL::DB::Manager::Chart->find_by(accno => $chart_ust_16) or die; # 16%
+my $tax_ust_5  = SL::DB::Manager::Chart->find_by(accno => $chart_ust_5)  or die; #  5%
+my $tax_ust_7  = SL::DB::Manager::Chart->find_by(accno => $chart_ust_7)  or die; #  7%
+
+my $chart_income_19  = SL::DB::Manager::Chart->find_by(accno => $income_19_accno) or die;
+my $chart_income_7   = SL::DB::Manager::Chart->find_by(accno => $income_7_accno) or die;
+
+my $chart_reisekosten = SL::DB::Manager::Chart->find_by(accno => $chart_reisekosten_accno) or die;
+my $chart_cash        = SL::DB::Manager::Chart->find_by(accno => $chart_cash_accno) or die;
+my $chart_bank        = SL::DB::Manager::Chart->find_by(accno => $chart_bank_accno) or die;
+
+my $payment_terms = create_payment_terms();
+
+is(defined SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.05), 1, "tax for taxkey 2 with 5% was created ok");
+is(defined SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.16, chart_id => $tax_ust_16->id), 1, "new sales tax for taxkey 3 with 16% exists ok");
+is(defined SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19, chart_id => $tax_ust_19->id), 1, "old sales tax for taxkey 3 with 19% exists ok");
+# is(defined SL::DB::Manager::Tax->find_by(taxkey => 5, rate => 0.16, chart_id => $tax_ust_16->id), 1, "new sales tax for taxkey 5 with 16% exists ok");
+
+# is(defined SL::DB::Manager::Tax->find_by(taxkey => 7, rate => 0.16, chart_id => $tax_ust_16->id), 1, "old purchase tax for taxkey 7 with 16% exists ok");
+is(defined SL::DB::Manager::Tax->find_by(taxkey => 8, rate => 0.07, chart_id => $tax_vst_7->id ), 1, "purchase tax for taxkey 8 with 7% exists ok");
+is(defined SL::DB::Manager::Tax->find_by(taxkey => 9, rate => 0.19, chart_id => $tax_vst_19->id), 1, "old purchase tax for taxkey 9 with 19% exists ok");
+is(defined SL::DB::Manager::Tax->find_by(taxkey => 9, rate => 0.16, chart_id => $tax_vst_16->id), 1, "new purchase tax for taxkey 9 with 16% exists ok");
+
+my $vendor   = new_vendor(  name => 'Testvendor',   payment_id => $payment_terms->id)->save;
+my $customer = new_customer(name => 'Testcustomer', payment_id => $payment_terms->id)->save;
+
+cmp_ok($chart_income_7->get_active_taxkey($date_2020_1)->tax->rate, '==', 0.07, "get_active_taxkey rate for 8300 in 2020_1 ok");
+cmp_ok($chart_income_7->get_active_taxkey($date_2020_2)->tax->rate, '==', 0.05, "get_active_taxkey rate for 8300 in 2020_2 ok");
+cmp_ok($chart_income_7->get_active_taxkey($date_2021  )->tax->rate, '==', 0.07, "get_active_taxkey rate for 8300 in 2021   ok");
+cmp_ok($chart_income_7->get_active_taxkey($date_2020_1)->tax->rate, '==', 0.07, "get_active_taxkey rate for $income_7_accno in 2020_1 ok");
+cmp_ok($chart_income_7->get_active_taxkey($date_2020_2)->tax->rate, '==', 0.05, "get_active_taxkey rate for $income_7_accno in 2020_2 ok");
+cmp_ok($chart_income_7->get_active_taxkey($date_2021  )->tax->rate, '==', 0.07, "get_active_taxkey rate for $income_7_accno in 2021   ok");
+cmp_ok($chart_income_7->get_active_taxkey($date_2006  )->tax->rate, '==', 0.07, "get_active_taxkey rate for $income_7_accno in 2016   ok");
+
+cmp_ok($chart_income_19->get_active_taxkey($date_2020_1)->tax->rate, '==', 0.19, "get_active_taxkey rate for $income_19_accno in 2020_1 ok");
+cmp_ok($chart_income_19->get_active_taxkey($date_2020_2)->tax->rate, '==', 0.16, "get_active_taxkey rate for $income_19_accno in 2020_2 ok");
+cmp_ok($chart_income_19->get_active_taxkey($date_2021  )->tax->rate, '==', 0.19, "get_active_taxkey rate for $income_19_accno in 2021   ok");
+cmp_ok($chart_income_19->get_active_taxkey($date_2006  )->tax->rate, '==', 0.16, "get_active_taxkey rate for $income_19_accno in 2016   ok");
+
+my $bugru19 = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%') or die "Can't find bugru19";
+my $bugru7  = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 7%' ) or die "Can't find bugru7";
+
+my $part1 = new_part(partnumber => '1', description => 'part19', buchungsgruppen_id => $bugru19->id)->save;
+my $part2 = new_part(partnumber => '2', description => 'part7',  buchungsgruppen_id => $bugru7->id )->save;
+
+note('sales invoices');
+my $sales_invoice_2006   = create_invoice_for_date('2006',   $date_2006);
+my $sales_invoice_2020_1 = create_invoice_for_date('2020_1', $date_2020_1);
+my $sales_invoice_2020_2 = create_invoice_for_date('2020_2', $date_2020_2);
+my $sales_invoice_2021   = create_invoice_for_date('2021',   $date_2021);
+
+is($sales_invoice_2006->amount,   223, '2006 sales invoice has 16% and 7% tax ok'   ); # 116 + 7
+is($sales_invoice_2020_1->amount, 226, '2020_01 sales invoice has 19% and 7% tax ok'); # 119 + 7
+is($sales_invoice_2020_2->amount, 221, '2020_02 sales invoice has 16% and 5% tax ok'); # 116 + 5
+is($sales_invoice_2021->amount,   226, '2021 sales invoice has 19% and 7% tax ok'   ); # 119 + 7
+
+&datev_test($sales_invoice_2020_2,
+           [
+             {
+               'belegfeld1' => 'test is 2020_2',
+               'buchungstext' => 'Testcustomer',
+               'datum' => '15.07.2020',
+               'leistungsdatum' => '15.07.2020', # should leistungsdatum be empty if it doesn't exist?
+               'gegenkonto' => $income_7_accno,
+               'konto' => $ar_accno,
+               'kost1' => undef,
+               'kost2' => undef,
+               'locked' => undef,
+               'umsatz' => 105,
+               'waehrung' => 'EUR'
+             },
+             {
+               'belegfeld1' => 'test is 2020_2',
+               'buchungstext' => 'Testcustomer',
+               'datum' => '15.07.2020',
+               'leistungsdatum' => '15.07.2020',
+               'gegenkonto' => $income_19_accno,
+               'konto' => $ar_accno,
+               'kost1' => undef,
+               'kost2' => undef,
+               'locked' => undef,
+               'umsatz' => 116,
+               'waehrung' => 'EUR'
+             }
+          ],
+          "datev check for 16/5 ok, no taxkey"
+);
+
+note('sales invoice with differing delivery dates');
+my $sales_invoice_2020_1_with_delivery_date_2020_2 = create_invoice_for_date('deliverydate 2020_1', $date_2020_1, $date_2020_2);
+is($sales_invoice_2020_1_with_delivery_date_2020_2->amount, 221, "sales_invoice from 2020_1 with future delivery_date 2020_2 tax ok");
+
+my $sales_invoice_2020_2_with_delivery_date_2020_1 = create_invoice_for_date('deliverydate 2020_2', $date_2020_2, $date_2020_1);
+is($sales_invoice_2020_2_with_delivery_date_2020_1->amount, 226, "sales_invoice from 2020_2 with   past delivery_date 2020_1 tax ok");
+
+&datev_test($sales_invoice_2020_2_with_delivery_date_2020_1,
+            [
+              {
+                'belegfeld1' => 'test is deliverydate 2020_2',
+                'buchungstext' => 'Testcustomer',
+                'datum' => '15.07.2020',
+                'gegenkonto' => $income_7_accno,
+                'konto' => $ar_accno,
+                'kost1' => undef,
+                'kost2' => undef,
+                'leistungsdatum' => '15.06.2020',
+                'locked' => undef,
+                'umsatz' => 107,
+                'waehrung' => 'EUR'
+              },
+              {
+                'belegfeld1' => 'test is deliverydate 2020_2',
+                'buchungstext' => 'Testcustomer',
+                'datum' => '15.07.2020',
+                'gegenkonto' => $income_19_accno,
+                'konto' => $ar_accno,
+                'kost1' => undef,
+                'kost2' => undef,
+                'leistungsdatum' => '15.06.2020',
+                'locked' => undef,
+                'umsatz' => 119,
+                'waehrung' => 'EUR'
+              }
+            ],
+            "datev check for datev export with delivery_date 19/7 ok, no taxkey"
+);
+
+my $sales_invoice_2021_with_delivery_date_2020_2   = create_invoice_for_date('deliverydate 2020_2', $date_2021, $date_2020_2);
+is($sales_invoice_2021_with_delivery_date_2020_2->amount,   221, "sales_invoice from 2021   with   past delivery_date 2020_2 tax ok");
+
+my $sales_invoice_2020_2_with_delivery_date_2021   = create_invoice_for_date('deliverydate 2021', $date_2020_2, $date_2021);
+is($sales_invoice_2020_2_with_delivery_date_2021->amount,   226, "sales_invoice from 2020_2 with future delivery_date 2021   tax ok");
+
+
+note('ap transactions');
+# in the test we want to test for Reisekosten with 19% and 7%. Normally the user
+# would select the entries from the dropdown, as they may differ from the
+# default, so we have to pass the tax we want to create_ap_transaction
+
+# my $tax_9_16_old = SL::DB::Manager::Tax->find_by(taxkey => 7, rate => 0.16, chart_id => $tax_vst_16->id);
+my $tax_9_19     = SL::DB::Manager::Tax->find_by(taxkey => 9, rate => 0.19, chart_id => $tax_vst_19->id) or die "missing 9_19";
+my $tax_9_16     = SL::DB::Manager::Tax->find_by(taxkey => 9, rate => 0.16, chart_id => $tax_vst_16->id) or die "missing 9_16";
+my $tax_8_7      = SL::DB::Manager::Tax->find_by(taxkey => 8, rate => 0.07, chart_id => $tax_vst_7->id)  or die "missing 8_7";
+my $tax_8_5      = SL::DB::Manager::Tax->find_by(taxkey => 8, rate => 0.05, chart_id => $tax_vst_5->id)  or die "missing 8_5";
+
+# simulate user selecting the "correct" taxes in dropdown:
+my $ap_transaction_2006   = create_ap_transaction_for_date('2006',   $date_2006,   undef, $tax_9_16, $tax_8_7);
+my $ap_transaction_2020_1 = create_ap_transaction_for_date('2020_1', $date_2020_1, undef, $tax_9_19, $tax_8_7);
+my $ap_transaction_2020_2 = create_ap_transaction_for_date('2020_2', $date_2020_2, undef, $tax_9_16, $tax_8_5);
+my $ap_transaction_2021   = create_ap_transaction_for_date('2021',   $date_2021,   undef, $tax_9_19, $tax_8_7);
+
+
+is($ap_transaction_2006->amount,   223, '2006    ap transaction has 16% and 7% tax ok'); # 116 + 7
+is($ap_transaction_2020_1->amount, 226, '2020_01 ap transaction has 19% and 7% tax ok'); # 119 + 7
+is($ap_transaction_2020_2->amount, 221, '2020_02 ap transaction has 16% and 5% tax ok'); # 116 + 5
+is($ap_transaction_2021->amount,   226, '2021    ap transaction has 19% and 7% tax ok'); # 119 + 7
+
+# ap transaction in july, but use old tax
+my $ap_transaction_2020_2_with_delivery_date_2020_1 = create_ap_transaction_for_date('2020_2 with delivery date 2020_1', $date_2020_2, $date_2020_1, $tax_9_19, $tax_8_7);
+is($ap_transaction_2020_2_with_delivery_date_2020_1->amount,   226, 'ap transaction 2020_2 with delivery date 2020_1, 19% and 7% tax ok'); # 119 + 7
+&datev_test($ap_transaction_2020_2_with_delivery_date_2020_1,
+            [
+              {
+                'belegfeld1' => 'test ap_transaction 2020_2 with delivery date 2020_1',
+                'buchungsschluessel' => 8,
+                'buchungstext' => 'Testvendor',
+                'datum' => '15.07.2020',
+                'gegenkonto' => $ap_accno,
+                'konto' => $chart_reisekosten_accno,
+                'kost1' => undef,
+                'kost2' => undef,
+                'leistungsdatum' => '15.06.2020',
+                'locked' => undef,
+                'umsatz' => 107,
+                'waehrung' => 'EUR'
+              },
+              {
+                'belegfeld1' => 'test ap_transaction 2020_2 with delivery date 2020_1',
+                'buchungsschluessel' => 9,
+                'buchungstext' => 'Testvendor',
+                'datum' => '15.07.2020',
+                'gegenkonto' => $ap_accno,
+                'konto' => $chart_reisekosten_accno,
+                'kost1' => undef,
+                'kost2' => undef,
+                'leistungsdatum' => '15.06.2020',
+                'locked' => undef,
+                'umsatz' => 119,
+                'waehrung' => 'EUR'
+              }
+            ],
+            "datev check for ap transaction 2020_2 with delivery date 2020_1, 19% and 7% tax ok"
+);
+
+note('ar transactions');
+
+my $ar_transaction_2006   = create_ar_transaction_for_date('2006',   $date_2006);
+my $ar_transaction_2020_1 = create_ar_transaction_for_date('2020_1', $date_2020_1);
+my $ar_transaction_2020_2 = create_ar_transaction_for_date('2020_2', $date_2020_2);
+my $ar_transaction_2021   = create_ar_transaction_for_date('2021',   $date_2021);
+
+is($ar_transaction_2006->amount,   223, '2006    ar transaction has 16% and 7% tax ok'); # 116 + 7
+is($ar_transaction_2020_1->amount, 226, '2020_01 ar transaction has 19% and 7% tax ok'); # 119 + 7
+is($ar_transaction_2020_2->amount, 221, '2020_02 ar transaction has 16% and 5% tax ok'); # 116 + 5
+is($ar_transaction_2021->amount,   226, '2021    ar transaction has 19% and 7% tax ok'); # 119 + 7
+
+note('gl transactions');
+
+my $gl_2006   = create_gl_transaction_for_date('glincome 2006',   $date_2006,   223);
+my $gl_2020_1 = create_gl_transaction_for_date('glincome 2020_1', $date_2020_1, 226);
+my $gl_2020_2 = create_gl_transaction_for_date('glincome 2020_2', $date_2020_2, 221);
+my $gl_2021   = create_gl_transaction_for_date('glincome 2021',   $date_2021,   226);
+
+is(SL::DB::Manager::GLTransaction->get_all_count(), 4, "4 gltransactions created correctly");
+
+my $result = &get_account_balances;
+# print Dumper($result);
+is_deeply( &get_account_balances,
+        [
+          # {
+          #   'accno' => '1000',
+          #   # 'description' => 'Kasse',
+          #   'sum' => '-896.00000'
+          # },
+          # {
+          #   'accno' => '1400',
+          #   # 'description' => 'Ford. a.Lieferungen und Leistungen',
+          #   'sum' => '-2686.00000'
+          # },
+          {
+            'accno' => '1568',
+            # 'description' => 'Abziehbare Vorsteuer 7%',
+            'sum' => '-5.00000'
+          },
+          {
+            'accno' => '1571',
+            # 'description' => 'Abziehbare Vorsteuer 7%',
+            'sum' => '-28.00000'
+          },
+          {
+            'accno' => '1575',
+            # 'description' => 'Abziehbare Vorsteuer 16%',
+            'sum' => '-32.00000'
+          },
+          {
+            'accno' => '1576',
+            # 'description' => 'Abziehbare Vorsteuer 19 %',
+            'sum' => '-57.00000'
+          },
+          # {
+          #   'accno' => '1600',
+          #   # 'description' => 'Verbindlichkeiten aus Lief.u.Leist.',
+          #   'sum' => '896.00000'
+          # },
+          {
+            'accno' => '1771',
+            # 'description' => 'Umsatzsteuer 7%',
+            'sum' => '77.00000'
+          },
+          {
+            'accno' => '1773',
+            # 'description' => 'Umsatzsteuer 5 %',
+            'sum' => '25.00000'
+          },
+          {
+            'accno' => '1775',
+            # 'description' => 'Umsatzsteuer 16%',
+            'sum' => '128.00000'
+          },
+          {
+            'accno' => '1776',
+            # 'description' => 'Umsatzsteuer 19 %',
+            'sum' => '152.00000'
+          },
+          # {
+          #   'accno' => '4660',
+          #   # 'description' => 'Reisekosten Arbeitnehmer',
+          #   'sum' => '-800.00000'
+          # },
+          # {
+          #   'accno' => $income_7_accno,
+          #   # 'description' => "Erl\x{f6}se 7%USt",
+          #   'sum' => '1600.00000'
+          # },
+          # {
+          #   'accno' => $income_19_accno,
+          #   # 'description' => "Erl\x{f6}se 16%/19% USt.",
+          #   'sum' => '1600.00000'
+          # }
+        ],
+        'account balances after invoices'
+);
+
+note('testing payments with skonto');
+
+my %params = ( chart_id     => $chart_bank->id,
+               payment_type => 'with_skonto_pt',
+               bt_id        => $bt->id,
+             );
+
+$sales_invoice_2020_2->pay_invoice( %params,
+                                    amount    => $sales_invoice_2020_2->amount_less_skonto,
+                                    transdate => $date_2020_2->to_kivitendo,
+                                  );
+
+
+$skonto_5 = SL::DB::Manager::AccTransaction->find_by(trans_id => $sales_invoice_2020_2->id, amount => -5.25);
+like($skonto_5->chart->description, qr/Skonti.*5/, "sales_invoice 2020_2 paid in 2020_2 - skonto 5% ok");
+$skonto_16 = SL::DB::Manager::AccTransaction->find_by(trans_id => $sales_invoice_2020_2->id, amount => -5.80);
+like($skonto_16->chart->description, qr/Skonti.*16/, "sales_invoice 2020_2 paid in 2020_2 - skonto 16% ok");
+
+$sales_invoice_2020_1->pay_invoice( %params,
+                                    amount    => $sales_invoice_2020_1->amount_less_skonto,
+                                    transdate => $date_2020_2->to_kivitendo,
+                                  );
+$skonto_7 = SL::DB::Manager::AccTransaction->find_by(trans_id => $sales_invoice_2020_1->id, amount => -5.35);
+like($skonto_7->chart->description, qr/Skonti.*7/, "sales_invoice 2020_1 paid with skonto in 2020_2 - skonto 7% ok");
+$skonto_19 = SL::DB::Manager::AccTransaction->find_by(trans_id => $sales_invoice_2020_1->id, amount => -5.95);
+like($skonto_19->chart->description, qr/Skonti.*19/, "sales_invoice 2020_1 paid with skonto in 2020_2 - skonto 19% ok");
+
+$ap_transaction_2020_1->pay_invoice( %params,
+                                     amount    => $ap_transaction_2020_1->amount_less_skonto,
+                                     transdate => $date_2020_2->to_kivitendo,
+                                   );
+$skonto_7 = SL::DB::Manager::AccTransaction->find_by(trans_id => $ap_transaction_2020_1->id, amount => 5.35);
+like($skonto_7->chart->description, qr/Skonti.*7/, "ap transaction 2020_1 paid with skonto in 2020_2 - skonto 7% ok");
+$skonto_19 = SL::DB::Manager::AccTransaction->find_by(trans_id => $ap_transaction_2020_1->id, amount => 5.95);
+like($skonto_19->chart->description, qr/Skonti.*19/, "ap transaction 2020_1 paid with skonto in 2020_2 - skonto 19% ok");
+
+
+$ap_transaction_2020_2->pay_invoice( %params,
+                                     amount    => $ap_transaction_2020_2->amount_less_skonto,
+                                     transdate => $date_2021->to_kivitendo,
+                                   );
+$skonto_5 = SL::DB::Manager::AccTransaction->find_by(trans_id => $ap_transaction_2020_2->id, amount => 5.25);
+like($skonto_5->chart->description, qr/Skonti.*5/, "ap transaction 2020_2 paid in 2021 - skonto 5% ok");
+
+$skonto_16 = SL::DB::Manager::AccTransaction->find_by(trans_id => $ap_transaction_2020_2->id, amount => 5.80);
+like($skonto_16->chart->description, qr/Skonti.*16/, "sales_invoice 2020_2 paid in 2021 - skonto 16% ok");
+
+clear_up();
+
+done_testing();
+
+###### functions for setting up data
+
+sub create_invoice_for_date {
+  my ($invnumber, $transdate, $deliverydate) = @_;
+
+  $deliverydate = $transdate unless defined $deliverydate;
+
+  my $sales_invoice = create_sales_invoice(
+    invnumber    => 'test is ' . $invnumber,
+    transdate    => $transdate,
+    customer     => $customer,
+    deliverydate => $deliverydate,
+    payment_terms => $payment_terms,
+    taxincluded  => 0,
+    invoiceitems => [ create_invoice_item(part => $part1, qty => 10, sellprice => 10),
+                      create_invoice_item(part => $part2, qty => 10, sellprice => 10),
+                    ]
+  );
+  return $sales_invoice;
+}
+
+sub create_ar_transaction_for_date {
+  my ($invnumber, $transdate) = @_;
+
+  my $ar_transaction = create_ar_transaction(
+    customer      => $customer,
+    invnumber   => 'test ar' . $invnumber,
+    taxincluded => 0,
+    transdate   => $transdate,
+    ar_chart     => SL::DB::Manager::Chart->find_by(accno => $ar_accno), # pass ar_chart, as it is hardcoded for SKR03 in SL::Dev::Record
+    bookings    => [
+                     {
+                       chart  => $chart_income_19,
+                       amount => 100,
+                     },
+                     {
+                       chart  => $chart_income_7,
+                       amount => 100,
+                     },
+                   ]
+  );
+  return $ar_transaction;
+}
+
+sub create_ap_transaction_for_date {
+  my ($invnumber, $transdate, $deliverydate, $tax_high, $tax_low) = @_;
+
+  # printf("invnumber = %s  tax_high = %s   tax_low = %s\n", $invnumber, $tax_high->accno , $tax_low->accno);
+  my $taxkey_ = $chart_reisekosten->get_active_taxkey($transdate);
+
+  my $ap_transaction = create_ap_transaction(
+    vendor       => $vendor,
+    invnumber    => 'test ap_transaction ' . $invnumber,
+    taxincluded  => 0,
+    transdate    => $transdate,
+    deliverydate => $deliverydate,
+    payment_id   => $payment_terms->id,
+    ap_chart     => SL::DB::Manager::Chart->find_by(accno => $ap_accno), # pass ap_chart, as it is hardcoded for SKR03 in SL::Dev::Record
+    bookings     => [
+                     {
+                       chart  => $chart_reisekosten,
+                       amount => 100,
+                       tax_id => $tax_high->id,
+                     },
+                     {
+                       chart  => $chart_reisekosten,
+                       amount => 100,
+                       tax_id => $tax_low->id,
+                     },
+                   ]
+  );
+  return $ap_transaction;
+}
+
+sub create_gl_transaction_for_date {
+  my ($reference, $transdate, $debitamount) = @_;
+
+  my $gl_transaction = create_gl_transaction(
+    reference   => $reference,
+    taxincluded => 0,
+    transdate   => $transdate,
+    bookings    => [
+                     {
+                       chart  => $chart_income_19,
+                       memo   => 'gl 19',
+                       source => 'gl 19',
+                       credit => 100,
+                     },
+                     {
+                       chart  => $chart_income_7,
+                       memo   => 'gl 7',
+                       source => 'gl 7',
+                       credit => 100,
+                     },
+                     {
+                       chart  => $chart_cash,
+                       debit  => $debitamount,
+                       memo   => 'gl 19+7',
+                       source => 'gl 19+7',
+                     },
+                   ],
+  );
+  return $gl_transaction;
+}
+
+sub get_account_balances {
+  my $query = <<SQL;
+  select c.accno, sum(a.amount)
+    from acc_trans a
+         left join chart c on (c.id = a.chart_id)
+   where c.accno ~ '^17' or c.accno ~ '^15'
+group by c.accno, c.description
+order by c.accno
+SQL
+
+  my $result = selectall_hashref_query($::form, $dbh, $query);
+  return $result;
+};
+
+sub datev_test {
+  my ($invoice, $expected_data, $msg) = @_;
+
+  my $datev = SL::DATEV->new(
+    dbh        => $invoice->db->dbh,
+    trans_id   => $invoice->id,
+  );
+
+  $datev->generate_datev_data;
+  my @data_datev   = sort { $a->{umsatz} <=> $b->{umsatz} } @{ $datev->generate_datev_lines() };
+
+  # print Dumper(\@data_datev);
+
+  cmp_deeply(\@data_datev, $expected_data, $msg);
+}
+
+sub clear_up {
+  SL::DB::Manager::BankTransactionAccTrans->delete_all(all => 1);
+  SL::DB::Manager::BankTransaction->delete_all(all => 1);
+  SL::DB::Manager::BankAccount->delete_all(all => 1);
+  SL::DB::Manager::OrderItem->delete_all(all => 1);
+  SL::DB::Manager::Order->delete_all(all => 1);
+  SL::DB::Manager::InvoiceItem->delete_all(all => 1);
+  SL::DB::Manager::Invoice->delete_all(all => 1);
+  SL::DB::Manager::PurchaseInvoice->delete_all(all => 1);
+  SL::DB::Manager::GLTransaction->delete_all(all => 1);
+  SL::DB::Manager::Part->delete_all(all => 1);
+  SL::DB::Manager::Customer->delete_all(all => 1);
+  SL::DB::Manager::Vendor->delete_all(all => 1);
+  SL::DB::Manager::PaymentTerm->delete_all(all => 1);
+};
+
+1;
index b1b4224..142f102 100644 (file)
@@ -3,6 +3,7 @@ use strict;
 use lib 't';
 
 use Support::Templates;
+use Support::TestSetup;
 
 use File::Spec;
 use File::Slurp;
@@ -12,14 +13,7 @@ use Test::More tests => ( scalar(@referenced_files));
 
 my $template_path = 'templates/webpages/';
 
-my $provider = Template::Provider->new({
-  INTERPOLATE  => 0,
-  EVAL_PERL    => 0,
-  ABSOLUTE     => 1,
-  CACHE_SIZE   => 0,
-  PLUGIN_BASE  => 'SL::Template::Plugin',
-  INCLUDE_PATH => '.:' . $template_path,
-});
+my $provider = Template::Provider->new(Support::TestSetup::template_config());
 
 foreach my $ref (@Support::Templates::referenced_files) {
   my $file              = "${template_path}${ref}.html";
@@ -33,7 +27,7 @@ foreach my $ref (@Support::Templates::referenced_files) {
     ok(0, "${file} contains syntax errors");
 
   } else {
-    die "Unknown result type: " . ref($result);
+    die "Unknown result type: " . ref($result) . " for file " . $file;
   }
 }
 
index d9fdaf4..ccdab40 100755 (executable)
--- a/t/test.pl
+++ b/t/test.pl
@@ -8,9 +8,14 @@ use Test::Harness qw(runtests execute_tests);
 use Getopt::Long;
 
 BEGIN {
-   $ENV{HARNESS_OPTIONS} = 'c';
-  unshift @INC, 'modules/override';
-  push    @INC, 'modules/fallback';
+  use FindBin;
+
+  unshift(@INC, $FindBin::Bin . '/../modules/override'); # Use our own versions of various modules (e.g. YAML).
+  push   (@INC, $FindBin::Bin . '/..');                  # '.' will be removed from @INC soon.
+
+  $ENV{HARNESS_OPTIONS} = 'c';
+
+  chdir($FindBin::Bin . '/..');
 }
 
 my @exclude_for_fast = (
diff --git a/t/wh/inventory.t b/t/wh/inventory.t
new file mode 100644 (file)
index 0000000..3c125a7
--- /dev/null
@@ -0,0 +1,514 @@
+use strict;
+use Test::Deep qw(cmp_deeply ignore superhashof);
+use Test::More;
+use Test::Exception;
+
+use lib 't';
+
+use SL::Dev::Part qw(new_part new_assembly new_service);
+use SL::Dev::Inventory qw(create_warehouse_and_bins set_stock);
+use SL::Dev::Record qw(create_sales_order);
+
+use_ok 'Support::TestSetup';
+use_ok 'SL::DB::Bin';
+use_ok 'SL::DB::Part';
+use_ok 'SL::DB::Warehouse';
+use_ok 'SL::DB::Inventory';
+use_ok 'SL::WH';
+use_ok 'SL::Helper::Inventory';
+
+Support::TestSetup::login();
+
+my ($wh, $bin1, $bin2, $assembly1, $assembly_service, $part1, $part2, $wh_moon, $bin_moon, $service1);
+my @contents;
+
+reset_db();
+create_standard_stock();
+
+
+# simple stock in, get_stock, get_onhand
+set_stock(
+  part => $part1,
+  qty => 25,
+  bin => $bin1,
+);
+
+is(SL::Helper::Inventory::get_stock(part => $part1), "25.00000", 'simple get_stock works');
+is(SL::Helper::Inventory::get_onhand(part => $part1), "25.00000", 'simple get_onhand works');
+
+# stock on some more, get_stock, get_onhand
+
+WH->transfer({
+  parts_id          => $part1->id,
+  qty               => 15,
+  transfer_type     => 'stock',
+  dst_warehouse_id  => $bin1->warehouse_id,
+  dst_bin_id        => $bin1->id,
+  comment           => 'more',
+});
+
+WH->transfer({
+  parts_id          => $part1->id,
+  qty               => 20,
+  transfer_type     => 'stock',
+  chargenumber      => '298345',
+  dst_warehouse_id  => $bin1->warehouse_id,
+  dst_bin_id        => $bin1->id,
+  comment           => 'more',
+});
+
+is(SL::Helper::Inventory::get_stock(part => $part1), "60.00000", 'normal get_stock works');
+is(SL::Helper::Inventory::get_onhand(part => $part1), "60.00000", 'normal get_onhand works');
+
+# allocate some stuff
+
+my @allocations = SL::Helper::Inventory::allocate(
+  part => $part1,
+  qty  => 12,
+);
+
+is_deeply(\%{ $allocations[0] }, {
+   bestbefore        => undef,
+   bin_id            => $bin1->id,
+   chargenumber      => '',
+   parts_id          => $part1->id,
+   qty               => 12,
+   warehouse_id      => $wh->id,
+   comment           => undef, # comment is not a partition so is not set by allocate
+   for_object_id     => undef,
+ }, 'allocation works');
+
+# allocate something where more than one result will match
+
+@allocations = SL::Helper::Inventory::allocate(
+  part => $part1,
+  qty  => 55,
+);
+
+is_deeply(\@allocations, [
+  {
+    bestbefore        => undef,
+    bin_id            => $bin1->id,
+    chargenumber      => '',
+    parts_id          => $part1->id,
+    qty               => '40.00000',
+    warehouse_id      => $wh->id,
+    comment           => undef,
+    for_object_id     => undef,
+  },
+  {
+    bestbefore        => undef,
+    bin_id            => $bin1->id,
+    chargenumber      => '298345',
+    parts_id          => $part1->id,
+    qty               => '15',
+    warehouse_id      => $wh->id,
+    comment           => undef,
+    for_object_id     => undef,
+  }
+], 'complex allocation works');
+
+# try to allocate too much
+
+dies_ok(sub {
+  SL::Helper::Inventory::allocate(part => $part1, qty => 100)
+},
+"allocate too much dies");
+
+# produce something
+
+reset_db();
+create_standard_stock();
+
+set_stock(
+  part => $part1,
+  qty => 5,
+  bin => $bin1,
+);
+set_stock(
+  part => $part2,
+  qty => 10,
+  bin => $bin1,
+);
+
+
+my @alloc1 = SL::Helper::Inventory::allocate(part => $part1, qty => 3);
+my @alloc2 = SL::Helper::Inventory::allocate(part => $part2, qty => 3);
+
+SL::Helper::Inventory::produce_assembly(
+  part          => $assembly1,
+  qty           => 3,
+  allocations => [ @alloc1, @alloc2 ],
+
+  # where to put it
+  bin          => $bin1,
+  chargenumber => "537",
+);
+
+is(SL::Helper::Inventory::get_stock(part => $assembly1), "3.00000", 'produce works');
+is(SL::Helper::Inventory::get_stock(part => $part1), "2.00000", 'and consumes...');
+is(SL::Helper::Inventory::get_stock(part => $part2), "7.00000", '..the materials');
+
+# produce the same using auto_allocation
+
+local $::locale = Locale->new('en');
+reset_db();
+create_standard_stock();
+
+set_stock(
+  part => $part1,
+  qty => 5,
+  bin => $bin1,
+);
+set_stock(
+  part => $part2,
+  qty => 10,
+  bin => $bin1,
+);
+
+SL::Helper::Inventory::produce_assembly(
+  part          => $assembly1,
+  qty           => 3,
+  auto_allocate => 1,
+
+  # where to put it
+  bin          => $bin1,
+  chargenumber => "537",
+);
+
+is(SL::Helper::Inventory::get_stock(part => $assembly1), "3.00000", 'produce with auto allocation works');
+is(SL::Helper::Inventory::get_stock(part => $part1), "2.00000", 'and consumes...');
+is(SL::Helper::Inventory::get_stock(part => $part2), "7.00000", '..the materials');
+
+# check comments and warehouses
+$::form->{l_comment}        = 'Y';
+$::form->{l_warehouse_from} = 'Y';
+$::form->{l_warehouse_to}   = 'Y';
+local $::instance_conf->data->{produce_assembly_same_warehouse} = 1;
+
+@contents = WH->get_warehouse_journal(sort => 'date');
+
+cmp_deeply(\@contents,
+           [ ignore(), ignore(),
+              superhashof({
+                'comment'        => 'Used for assembly '. $assembly1->partnumber .' Test Assembly',
+                'warehouse_from' => 'Warehouse'
+              }),
+              superhashof({
+                'comment'        => 'Used for assembly '. $assembly1->partnumber .' Test Assembly',
+                'warehouse_from' => 'Warehouse'
+              }),
+              superhashof({
+                'part_type'    => 'assembly',
+                'warehouse_to' => 'Warehouse'
+              }),
+           ],
+          "Comments for assembly productions are ok"
+);
+
+# try to produce something for our lunar warehouse, but parts are only available on earth
+dies_ok(sub {
+SL::Helper::Inventory::produce_assembly(
+  part          => $assembly1,
+  qty           => 1,
+  auto_allocate => 1,
+  # where to put it
+  bin          => $bin_moon,
+  chargenumber => "Lunar Dust inside",
+);
+}, "producing for wrong warehouse dies");
+
+# same test, but check exception class
+throws_ok{
+SL::Helper::Inventory::produce_assembly(
+  part          => $assembly1,
+  qty           => 1,
+  auto_allocate => 1,
+  # where to put it
+  bin          => $bin_moon,
+  chargenumber => "Lunar Dust inside",
+);
+ } "SL::X::Inventory::Allocation", "producing for wrong warehouse throws correct error class";
+
+# same test, but check user feedback for the error message
+throws_ok{
+SL::Helper::Inventory::produce_assembly(
+  part          => $assembly1,
+  qty           => 1,
+  auto_allocate => 1,
+  # where to put it
+  bin          => $bin_moon,
+  chargenumber => "Lunar Dust inside",
+);
+ } qr/Part ap (1|2) Testpart (1|2) exists in warehouse Warehouse, but not in warehouse Our warehouse location at the moon/, "producing for wrong warehouse throws correct error message";
+
+# try to produce without allocations dies
+
+dies_ok(sub {
+SL::Helper::Inventory::produce_assembly(
+  part          => $assembly1,
+  qty           => 3,
+
+  # where to put it
+  bin          => $bin1,
+  chargenumber => "537",
+);
+}, "producing without allocations dies");
+
+# try to produce with insufficient allocations dies
+
+@alloc1 = SL::Helper::Inventory::allocate(part => $part1, qty => 1);
+@alloc2 = SL::Helper::Inventory::allocate(part => $part2, qty => 1);
+
+dies_ok(sub {
+SL::Helper::Inventory::produce_assembly(
+  part          => $assembly1,
+  qty           => 3,
+  allocations => [ @alloc1, @alloc2 ],
+
+  # where to put it
+  bin          => $bin1,
+  chargenumber => "537",
+);
+}, "producing with insufficient allocations dies");
+
+
+# assembly with service default tests (services won't be consumed)
+
+local $::locale = Locale->new('en');
+reset_db();
+create_standard_stock();
+
+set_stock(
+  part => $part1,
+  qty => 12,
+  bin => $bin2,
+);
+set_stock(
+  part => $part2,
+  qty => 6.34,
+  bin => $bin2,
+);
+
+SL::Helper::Inventory::produce_assembly(
+  part          => $assembly_service,
+  qty           => 1,
+  auto_allocate => 1,
+  # where to put it
+  bin          => $bin1,
+);
+
+is(SL::Helper::Inventory::get_stock(part => $assembly_service), "1.00000", 'produce with auto allocation works');
+is(SL::Helper::Inventory::get_stock(part => $part1), "0.00000", 'and consumes...');
+is(SL::Helper::Inventory::get_stock(part => $part2), "0.00000", '..the materials');
+
+# check comments and warehouses
+$::form->{l_comment}        = 'Y';
+$::form->{l_warehouse_from} = 'Y';
+$::form->{l_warehouse_to}   = 'Y';
+local $::instance_conf->data->{produce_assembly_same_warehouse} = 1;
+
+@contents = WH->get_warehouse_journal(sort => 'date');
+
+cmp_deeply(\@contents,
+           [ ignore(), ignore(),
+              superhashof({
+                'comment'        => 'Used for assembly '. $assembly_service->partnumber .' Ein Erzeugnis mit Dienstleistungen',
+                'warehouse_from' => 'Warehouse'
+              }),
+              superhashof({
+                'comment'        => 'Used for assembly '. $assembly_service->partnumber .' Ein Erzeugnis mit Dienstleistungen',
+                'warehouse_from' => 'Warehouse'
+              }),
+              superhashof({
+                'part_type'    => 'assembly',
+                'warehouse_to' => 'Warehouse'
+              }),
+           ],
+          "Comments for assembly with service productions are ok"
+);
+
+# assembly with service non default tests (services will be consumed)
+
+local $::instance_conf->data->{produce_assembly_transfer_service} = 1;
+
+set_stock(
+  part => $part1,
+  qty => 12,
+  bin => $bin2,
+);
+set_stock(
+  part => $part2,
+  qty => 6.34,
+  bin => $bin2,
+);
+
+throws_ok{
+  SL::Helper::Inventory::produce_assembly(
+    part          => $assembly_service,
+    qty           => 1,
+    auto_allocate => 1,
+    # where to put it
+    bin          => $bin1,
+  );
+} qr/can not allocate 1,2 units of service number 1 We really need this service, missing 1,2 units/, "producing assembly with services and unstocked service throws correct error message";
+
+is(SL::Helper::Inventory::get_stock(part => $assembly_service), "1.00000", 'produce without service does not work');
+is(SL::Helper::Inventory::get_stock(part => $part1), "12.00000", 'and does not consume...');
+is(SL::Helper::Inventory::get_stock(part => $part2), "6.34000", '..the materials');
+
+
+# ok, now add the missing service
+is('SL::DB::Part', ref $service1);
+set_stock(
+  part => $service1,
+  qty => 1.2,
+  bin => $bin2,
+);
+
+SL::Helper::Inventory::produce_assembly(
+  part          => $assembly_service,
+  qty           => 1,
+  auto_allocate => 1,
+  # where to put it
+  bin          => $bin1,
+);
+
+is(SL::Helper::Inventory::get_stock(part => $assembly_service), "2.00000", 'produce with service does work if services is needed and stocked');
+is(SL::Helper::Inventory::get_stock(part => $part1), "0.00000", 'and does consume...');
+is(SL::Helper::Inventory::get_stock(part => $part2), "0.00000", '..the materials');
+is(SL::Helper::Inventory::get_stock(part => $service1), "0.00000", '..and service');
+
+# check comments and warehouses for assembly with service
+$::form->{l_comment}        = 'Y';
+$::form->{l_warehouse_from} = 'Y';
+$::form->{l_warehouse_to}   = 'Y';
+local $::instance_conf->data->{produce_assembly_same_warehouse} = 1;
+
+@contents = WH->get_warehouse_journal(sort => 'date');
+#use Data::Dumper;
+#diag("hier" . Dumper(@contents));
+cmp_deeply(\@contents,
+         [ ignore(), ignore(), ignore(), ignore(), ignore(), ignore(), ignore(), ignore(),
+              superhashof({
+                'comment'        => 'Used for assembly '. $assembly_service->partnumber .' Ein Erzeugnis mit Dienstleistungen',
+                'warehouse_from' => 'Warehouse'
+              }),
+              superhashof({
+                'comment'        => 'Used for assembly '. $assembly_service->partnumber .' Ein Erzeugnis mit Dienstleistungen',
+                'warehouse_from' => 'Warehouse'
+              }),
+              superhashof({
+                'comment'        => 'Used for assembly '. $assembly_service->partnumber .' Ein Erzeugnis mit Dienstleistungen',
+                'warehouse_from' => 'Warehouse',
+                'part_type'      => 'service',
+                'qty'            => '1.20000',
+              }),
+              superhashof({
+                'part_type'    => 'assembly',
+                'warehouse_to' => 'Warehouse'
+              }),
+           ],
+          "Comments for assembly with service productions are ok"
+);
+
+
+
+# bestbefore tests
+
+reset_db();
+create_standard_stock();
+
+set_stock(
+  part => $part1,
+  qty => 5,
+  bin => $bin1,
+);
+set_stock(
+  part => $part2,
+  qty => 10,
+  bin => $bin1,
+);
+
+
+
+SL::Helper::Inventory::produce_assembly(
+  part          => $assembly1,
+  qty           => 3,
+  auto_allocate => 1,
+
+  bin               => $bin1,
+  chargenumber      => "537",
+  bestbefore        => DateTime->today->clone->add(days => -14), # expired 2 weeks ago
+  shippingdate      => DateTime->today->clone->add(days => 1),
+);
+
+is(SL::Helper::Inventory::get_stock(part => $assembly1), "3.00000", 'produce with bestbefore works');
+is(SL::Helper::Inventory::get_onhand(part => $assembly1), "3.00000", 'produce with bestbefore works');
+is(SL::Helper::Inventory::get_stock(
+  part       => $assembly1,
+  bestbefore => DateTime->today,
+), undef, 'get_stock with bestbefore date skips expired');
+{
+  local $::instance_conf->data->{show_bestbefore} = 1;
+  is(SL::Helper::Inventory::get_onhand(
+    part       => $assembly1,
+  ), undef, 'get_onhand with bestbefore skips expired as of today');
+}
+
+{
+  local $::instance_conf->data->{show_bestbefore} = 0;
+  is(SL::Helper::Inventory::get_onhand(
+    part       => $assembly1,
+  ), "3.00000", 'get_onhand without bestbefore finds all');
+}
+
+
+sub reset_db {
+  SL::DB::Manager::Order->delete_all(all => 1);
+  SL::DB::Manager::Inventory->delete_all(all => 1);
+  SL::DB::Manager::Assembly->delete_all(all => 1);
+  SL::DB::Manager::Part->delete_all(all => 1);
+  SL::DB::Manager::Bin->delete_all(all => 1);
+  SL::DB::Manager::Warehouse->delete_all(all => 1);
+}
+
+sub create_standard_stock {
+  ($wh, $bin1)          = create_warehouse_and_bins();
+  ($wh_moon, $bin_moon) = create_warehouse_and_bins(
+      warehouse_description => 'Our warehouse location at the moon',
+      bin_description       => 'Lunar crater',
+    );
+  $bin2 = SL::DB::Bin->new(description => "Bin 2", warehouse => $wh)->save;
+  $wh->load;
+
+  $assembly1  =  new_assembly(number_of_parts => 2)->save;
+  ($part1, $part2) = map { $_->part } $assembly1->assemblies;
+
+  $service1 = new_service(partnumber  => "service number 1",
+                          description => "We really need this service",
+                         )->save;
+  my $assembly_items;
+  push( @{$assembly_items}, SL::DB::Assembly->new(parts_id => $part1->id,
+                                                  qty      => 12,
+                                                  position => 1,
+                                                  ));
+  push( @{$assembly_items}, SL::DB::Assembly->new(parts_id => $part2->id,
+                                                  qty      => 6.34,
+                                                  position => 2,
+                                                  ));
+  push( @{$assembly_items}, SL::DB::Assembly->new(parts_id => $service1->id,
+                                                  qty      => 1.2,
+                                                  position => 3,
+                                                  ));
+  $assembly_service  =  new_assembly(description    => 'Ein Erzeugnis mit Dienstleistungen',
+                                     assembly_items => $assembly_items
+                                    )->save;
+}
+
+
+reset_db();
+
+done_testing();
+
+1;
diff --git a/t/wh/journal.t b/t/wh/journal.t
new file mode 100644 (file)
index 0000000..e91bdb7
--- /dev/null
@@ -0,0 +1,75 @@
+use strict;
+use Test::More tests => 4;
+
+use lib 't';
+
+use SL::Dev::Part qw(new_part);
+use SL::Dev::Inventory qw(create_warehouse_and_bins);
+use SL::DB::Inventory;
+use Support::TestSetup;
+
+Support::TestSetup::login();
+
+use_ok("SL::WH");
+
+my ($wh, $bin, $part);
+
+sub init  {
+  ($wh, $bin) = create_warehouse_and_bins(
+    warehouse_description => 'Test warehouse',
+    bin_description       => 'Test bin',
+    number_of_bins        => 1,
+  );
+
+  $part = new_part()->save->load;
+
+  my $tt_used = SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'used') or die;
+  my $tt_assembled = SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'assembled') or die;
+
+  my %args = (
+    trans_id     => 1,
+    bin          => $bin,
+    warehouse    => $wh,
+    part         => $part,
+    qty          => 1,
+    employee     => SL::DB::Manager::Employee->current,
+    shippingdate => DateTime->now,
+  );
+
+  SL::DB::Inventory->new(%args, trans_type => $tt_used, qty => -1)->save;
+  SL::DB::Inventory->new(%args, trans_type => $tt_used, qty => -1)->save;
+  SL::DB::Inventory->new(%args, trans_type => $tt_assembled, qty => 1)->save;
+
+  qty                           => { type => 'numeric', precision => 25, scale => 5 },
+  shippingdate                  => { type => 'date', not_null => 1 },
+}
+
+sub reset_inventory {
+  SL::DB::Manager::Inventory->delete_all(all => 1);
+}
+
+reset_inventory();
+init();
+
+# l_date = Y
+# l_warehouse_from = Y
+# l_bin_from = Y
+# l_warehouse_to = Y
+# l_bin_to = Y
+# l_partnumber = Y
+# l_partdescription = Y
+# l_chargenumber = Y
+# l_trans_type = Y
+# l_qty = Y
+# l_oe_id = Y
+# l_projectnumber = Y
+# qty_op = dontcare
+
+
+my @contents = WH->get_warehouse_journal(sort => 'date');
+
+is $contents[0]{qty}, '1.00000', "produce assembly does not multiply qty (1)";
+is $contents[1]{qty}, '1.00000', "produce assembly does not multiply qty (2)";
+is $contents[2]{qty}, '1.00000', "produce assembly does not multiply qty (3)";
+
+1;
index 66daf58..b85cd68 100644 (file)
@@ -3,6 +3,8 @@ use Test::More;
 
 use lib 't';
 
+use SL::Dev::Part qw(new_part);
+
 use_ok 'Support::TestSetup';
 use_ok 'SL::DB::Bin';
 use_ok 'SL::DB::Part';
@@ -26,8 +28,7 @@ SL::DB::Manager::Bin      ->delete_all(where => [ or => [ description => NAME()
 SL::DB::Manager::Warehouse->delete_all(where => [ description => NAME() ]);
 
 # Create test data
-$part = SL::DB::Part->new(unit => 'mg', description => NAME(), partnumber => NAME());
-$part->save();
+$part = new_part(unit => 'mg', description => NAME(), partnumber => NAME())->save();
 
 is(ref($part), 'SL::DB::Part', 'loading a part to test with id ' . $part->id);
 
diff --git a/t/x/exceptions.t b/t/x/exceptions.t
new file mode 100644 (file)
index 0000000..e54327b
--- /dev/null
@@ -0,0 +1,68 @@
+use Test::More tests => 25;
+
+use lib 't';
+
+use SL::X;
+
+# check exception serialization
+
+my @classes = qw(
+  SL::X::DBError
+  SL::X::Inventory::Allocation
+  SL::X::ZUGFeRDValidation
+);
+
+# check basic mesage / error serialization
+
+for my $error_class (@classes) {
+
+  my $x = $error_class->new(message => "test message");
+
+  is $x->error,   "test message", "$error_class(message): error works";
+  is $x->message, "test message", "$error_class(message): message works";
+  is "$x",        "test message", "$error_class(message): stringify works";
+
+  $x = $error_class->new(error => "test message");
+
+  is $x->error,   "test message", "$error_class(error): error works";
+  is $x->message, "test message", "$error_class(error): message works";
+  is "$x",        "test message", "$error_class(error): stringify works";
+}
+
+
+# now create some classes with message templates and extra fields
+
+my $x = SL::X::DBError->new(msg => "stuff", db_error => "broke");
+
+is $x->error,   "stuff: broke", "template: error works";
+is $x->message, "stuff: broke", "tempalte: message works";
+is "$x",        "stuff: broke", "template: stringify works";
+
+
+$x = SL::X::Inventory::Allocation->new(code => "DEADCOFFEE", message => "something went wrong");
+
+is $x->code,   "DEADCOFFEE", "extra fields work";
+
+
+
+# check stack traces
+
+sub a { b() }
+sub b { c() }
+sub c { d() }
+sub d { e() }
+sub e { f() }
+sub f { SL::X::DBError->throw() }
+
+eval {
+  a();
+} or do {
+  if (my $e = SL::X::DBError->caught) {
+    ok 1, "caught db error";
+    ok $e->trace->as_string =~ /main::a/, "trace contains function a";
+    ok $e->trace->as_string =~ /main::f/, "trace contains function f";
+
+  } else {
+    ok 0, "didn't catch db error";
+  }
+};
diff --git a/t/year_end/year_end.t b/t/year_end/year_end.t
new file mode 100644 (file)
index 0000000..dc0be7c
--- /dev/null
@@ -0,0 +1,647 @@
+use strict;
+use warnings;
+
+use Test::More tests => 18;
+use lib 't';
+use utf8;
+
+use Carp;
+use Data::Dumper;
+use Support::TestSetup;
+use Test::Exception;
+use SL::DBUtils qw(selectall_hashref_query);
+
+use SL::DB::BankAccount;
+use SL::DB::Chart;
+use SL::DB::Invoice;
+use SL::DB::PurchaseInvoice;
+
+use SL::Dev::Record qw(create_ar_transaction create_ap_transaction create_gl_transaction);
+
+use SL::Controller::YearEndTransactions;
+  
+Support::TestSetup::login();
+
+clear_up();
+
+# comments:
+
+# * in the default test client the tax accounts are configured as I/E rather than A/L
+# * also the default test client has the accounting method "cash" rather than "accrual"
+#   (Ist-versteuerung, rather than Soll-versteuerung)
+
+# use 2019 instead of 2020 because of tax changes in Germany (19/16 and 7/5) because we check for account sums
+my $year = 2019;
+my $start_of_year = DateTime->new(year => $year, month => 01, day => 01);
+my $booking_date  = DateTime->new(year => $year, month => 12, day => 22);
+
+note('configuring accounts');
+my $bank_account = SL::DB::BankAccount->new(
+  account_number  => '123',
+  bank_code       => '123',
+  iban            => '123',
+  bic             => '123',
+  bank            => '123',
+  chart_id        => SL::DB::Manager::Chart->find_by(description => 'Bank')->id,
+  name            => SL::DB::Manager::Chart->find_by(description => 'Bank')->description,
+)->save;
+
+my $profit_account = SL::DB::Manager::Chart->find_by(accno => '0890') //
+                     SL::DB::Chart->new(
+                       accno          => '0890',
+                       description    => 'Gewinnvortrag vor Verwendung',
+                       charttype      => 'A',
+                       category       => 'Q',
+                       link           => '',
+                       taxkey_id      => '0',
+                       datevautomatik => 'f',
+                     )->save;
+
+my $loss_account = SL::DB::Manager::Chart->find_by(accno => '0868') //
+                   SL::DB::Chart->new(
+                     accno          => '0868',
+                     description    => 'Verlustvortrag vor Verwendung',
+                     charttype      => 'A',
+                     category       => 'Q',
+                     link           => '',
+                     taxkey_id      => '0',
+                     datevautomatik => 'f',
+                   )->save;
+
+my $carry_over_chart = SL::DB::Manager::Chart->find_by(accno => 9000); 
+my $income_chart     = SL::DB::Manager::Chart->find_by(accno => '8400'); # income 19%, taxkey 3
+my $bank             = SL::DB::Manager::Chart->find_by(description => 'Bank');
+my $cash             = SL::DB::Manager::Chart->find_by(description => 'Kasse');
+my $privateinlagen   = SL::DB::Manager::Chart->find_by(description => 'Privateinlagen');
+my $betriebsbedarf   = SL::DB::Manager::Chart->find_by(description => 'Betriebsbedarf'); 
+
+my $dbh = SL::DB->client->dbh;
+$dbh->do('UPDATE defaults SET carry_over_account_chart_id     = ' . $carry_over_chart->id);
+$dbh->do('UPDATE defaults SET profit_carried_forward_chart_id = ' . $profit_account->id);
+$dbh->do('UPDATE defaults SET loss_carried_forward_chart_id   = ' . $loss_account->id);
+
+
+note('creating transactions');
+my $ar_transaction = create_ar_transaction(
+  taxincluded => 0,
+  transdate   => $booking_date,
+  bookings    => [
+                   {
+                     chart  => $income_chart, # income 19%, taxkey 3
+                     amount => 140,
+                   }
+                 ],
+);
+  
+$ar_transaction->pay_invoice(
+                              chart_id     => $bank_account->chart_id,
+                              amount       => $ar_transaction->amount,
+                              transdate    => $booking_date,
+                              payment_type => 'without_skonto',
+                            );
+
+my $ar_transaction2 = create_ar_transaction(
+  taxincluded => 1,
+  transdate   => $booking_date,
+  bookings    => [
+                   {
+                     chart  => $income_chart, # income 19%, taxkey 3
+                     amount => 166.60,
+                   }
+                 ],
+);
+
+my $ap_transaction = create_ap_transaction(
+  taxincluded => 0,
+  transdate   => $booking_date,
+  bookings    => [
+                   {
+                     chart  => SL::DB::Manager::Chart->find_by( accno => '3400' ), # Wareneingang 19%, taxkey 9
+                     amount => 100,
+                   }
+                 ],
+);
+
+gl_booking(40, $start_of_year, 'foo', 'bar', $bank, $privateinlagen, 1, 0);
+
+is(SL::DB::Manager::AccTransaction->get_all_count(                                ), 13, 'acc_trans transactions created ok');
+is(SL::DB::Manager::AccTransaction->get_all_count(where => [ ob_transaction => 1 ]),  2, 'acc_trans ob_transactions created ok');
+is(SL::DB::Manager::AccTransaction->get_all_count(where => [ cb_transaction => 1 ]),  0, 'no cb_transactions created ok');
+
+is_deeply( &get_account_balances, 
+           [
+             {
+               'accno'        => '1200',
+               'account_type' => 'asset_account',
+               'sum'          => '-206.60000'
+             },
+             {
+               'accno'        => '1400',
+               'account_type' => 'asset_account',
+               'sum'          => '-166.60000'
+             },
+             {
+               'accno'        => '1600',
+               'account_type' => 'asset_account',
+               'sum'          => '119.00000'
+             },
+             {
+               'accno'        => '1890',
+               'account_type' => 'asset_account',
+               'sum'          => '40.00000'
+             },
+             {
+               'accno'        => '1576',
+               'account_type' => 'profit_loss_account',
+               'sum'          => '-19.00000'
+             },
+             {
+               'accno'        => '1776',
+               'account_type' => 'profit_loss_account',
+               'sum'          => '53.20000'
+             },
+             {
+               'accno'        => '3400',
+               'account_type' => 'profit_loss_account',
+               'sum'          => '-100.00000'
+             },
+             {
+               'accno'        => '8400',
+               'account_type' => 'profit_loss_account',
+               'sum'          => '280.00000'
+             }
+           ],
+           'account balances before year_end bookings ok',
+);
+
+#  accno |    account_type     |    sum     
+# -------+---------------------+------------
+#  1200  | asset_account       | -206.60000
+#  1400  | asset_account       | -166.60000
+#  1600  | asset_account       |  119.00000
+#  1890  | asset_account       |   40.00000
+#  1576  | profit_loss_account |  -19.00000
+#  1776  | profit_loss_account |   53.20000
+#  3400  | profit_loss_account | -100.00000
+#  8400  | profit_loss_account |  280.00000
+
+
+note('running year-end transactions');
+my $start_date = DateTime->new(year => $year, month => 1,  day => 1);  
+my $cb_date    = DateTime->new(year => $year, month => 12, day => 31);
+my $ob_date    = $cb_date->clone->add(days => 1);
+
+SL::Controller::YearEndTransactions::_year_end_bookings( start_date => $start_date,
+                                                         cb_date    => $cb_date,
+                                                       );
+
+is(SL::DB::Manager::AccTransaction->get_all_count(where => [ cb_transaction => 1 ]), 14, 'acc_trans cb_transactions created ok');
+is(SL::DB::Manager::AccTransaction->get_all_count(where => [ ob_transaction => 1 ]), 10, 'acc_trans ob_transactions created ok');
+is(SL::DB::Manager::GLTransaction->get_all_count( where => [ cb_transaction => 1 ]),  5, 'GL cb_transactions created ok');
+is(SL::DB::Manager::GLTransaction->get_all_count( where => [ ob_transaction => 1 ]),  4, 'GL ob_transactions created ok');
+
+my $final_account_balances = [
+                               {
+                                 'accno' => '0890',
+                                 'amount' => undef,
+                                 'amount_with_cb' => '0.00000',
+                                 'cat' => 'Q',
+                                 'cb_amount' => '0.00000',
+                                 'ob_amount' => undef,
+                                 'ob_next_year' => '214.20000',
+                                 'type' => 'asset',
+                                 'year_end_amount' => undef
+                               },
+                               {
+                                 'accno' => '1200',
+                                 'amount' => '-166.60000',
+                                 'amount_with_cb' => '0.00000',
+                                 'cat' => 'A',
+                                 'cb_amount' => '-206.60000',
+                                 'ob_amount' => '-40.00000',
+                                 'ob_next_year' => '-206.60000',
+                                 'type' => 'asset',
+                                 'year_end_amount' => '-206.60000'
+                               },
+                               {
+                                 'accno' => '1400',
+                                 'amount' => '-166.60000',
+                                 'amount_with_cb' => '0.00000',
+                                 'cat' => 'A',
+                                 'cb_amount' => '-166.60000',
+                                 'ob_amount' => undef,
+                                 'ob_next_year' => '-166.60000',
+                                 'type' => 'asset',
+                                 'year_end_amount' => '-166.60000'
+                               },
+                               {
+                                 'accno' => '1600',
+                                 'amount' => '119.00000',
+                                 'amount_with_cb' => '0.00000',
+                                 'cat' => 'L',
+                                 'cb_amount' => '119.00000',
+                                 'ob_amount' => undef,
+                                 'ob_next_year' => '119.00000',
+                                 'type' => 'asset',
+                                 'year_end_amount' => '119.00000'
+                               },
+                               {
+                                 'accno' => '1890',
+                                 'amount' => undef,
+                                 'amount_with_cb' => '0.00000',
+                                 'cat' => 'Q',
+                                 'cb_amount' => '40.00000',
+                                 'ob_amount' => '40.00000',
+                                 'ob_next_year' => '40.00000',
+                                 'type' => 'asset',
+                                 'year_end_amount' => '40.00000'
+                               },
+                               {
+                                 'accno' => '9000',
+                                 'amount' => undef,
+                                 'amount_with_cb' => '0.00000',
+                                 'cat' => 'A',
+                                 'cb_amount' => '0.00000',
+                                 'ob_amount' => undef,
+                                 'ob_next_year' => '0.00000',
+                                 'type' => 'asset',
+                                 'year_end_amount' => undef
+                               },
+                               {
+                                 'accno' => '1576',
+                                 'amount' => '-19.00000',
+                                 'amount_with_cb' => '0.00000',
+                                 'cat' => 'E',
+                                 'cb_amount' => '-19.00000',
+                                 'ob_amount' => undef,
+                                 'ob_next_year' => undef,
+                                 'type' => 'pl',
+                                 'year_end_amount' => '-19.00000'
+                               },
+                               {
+                                 'accno' => '1776',
+                                 'amount' => '53.20000',
+                                 'amount_with_cb' => '0.00000',
+                                 'cat' => 'I',
+                                 'cb_amount' => '53.20000',
+                                 'ob_amount' => undef,
+                                 'ob_next_year' => undef,
+                                 'type' => 'pl',
+                                 'year_end_amount' => '53.20000'
+                               },
+                               {
+                                 'accno' => '3400',
+                                 'amount' => '-100.00000',
+                                 'amount_with_cb' => '0.00000',
+                                 'cat' => 'E',
+                                 'cb_amount' => '-100.00000',
+                                 'ob_amount' => undef,
+                                 'ob_next_year' => undef,
+                                 'type' => 'pl',
+                                 'year_end_amount' => '-100.00000'
+                               },
+                               {
+                                 'accno' => '8400',
+                                 'amount' => '280.00000',
+                                 'amount_with_cb' => '0.00000',
+                                 'cat' => 'I',
+                                 'cb_amount' => '280.00000',
+                                 'ob_amount' => undef,
+                                 'ob_next_year' => undef,
+                                 'type' => 'pl',
+                                 'year_end_amount' => '280.00000'
+                               }
+                             ];
+
+# running _year_end_bookings several times shouldn't change the anything, the
+# second and third run should be no-ops, at least while no further bookings where
+# made
+
+SL::Controller::YearEndTransactions::_year_end_bookings( start_date => $start_date,
+                                                         cb_date    => $cb_date,
+                                                       );
+
+is(SL::DB::Manager::AccTransaction->get_all_count(where => [ cb_transaction => 1 ]), 14, 'acc_trans cb_transactions created ok');
+is(SL::DB::Manager::AccTransaction->get_all_count(where => [ ob_transaction => 1 ]), 10, 'acc_trans ob_transactions created ok');
+is(SL::DB::Manager::GLTransaction->get_all_count( where => [ cb_transaction => 1 ]),  5, 'GL cb_transactions created ok');
+is(SL::DB::Manager::GLTransaction->get_all_count( where => [ ob_transaction => 1 ]),  4, 'GL ob_transactions created ok');
+
+
+# all asset accounts should be the same, except 0890, which should be the sum of p/l-accounts
+# all p/l account should be 0
+
+#  accno |    account_type     |    sum     
+# -------+---------------------+------------
+#  0890  | asset_account       |  214.20000
+#  1200  | asset_account       | -206.60000
+#  1400  | asset_account       | -166.60000
+#  1600  | asset_account       |  119.00000
+#  1890  | asset_account       |   40.00000
+#  9000  | asset_account       |    0.00000
+#  1576  | profit_loss_account |    0.00000
+#  1776  | profit_loss_account |    0.00000
+#  3400  | profit_loss_account |    0.00000
+#  8400  | profit_loss_account |    0.00000
+# (10 rows)
+
+is_deeply( &get_final_balances, 
+           $final_account_balances,
+           'balances after second year_end ok (nothing changed)');
+
+
+# select c.accno,
+#        c.description,
+#        c.category as cat,
+#        sum(a.amount     ) filter (where ob_transaction is true                              and a.transdate  < '2020-01-01') as ob_amount,
+#        sum(a.amount     ) filter (where cb_transaction is false and ob_transaction is false and a.transdate  < '2020-01-01') as amount,
+#        sum(a.amount     ) filter (where cb_transaction is false                             and a.transdate  < '2020-01-01') as year_end_amount,
+#        sum(a.amount     ) filter (where                                                         a.transdate  < '2020-01-01') as amount_with_cb,
+#        sum(a.amount * -1) filter (where cb_transaction is true                              and a.transdate  < '2020-01-01') as cb_amount,
+#        sum(a.amount     ) filter (where ob_transaction is true                              and a.transdate >= '2020-01-01') as ob_next_year,
+#        case when c.category = ANY( '{I,E}'     ) then 'pl'
+#             when c.category = ANY( '{A,C,L,Q}' ) then 'asset'
+#                                                  else null
+#             end                                                                         as type
+#   from acc_trans a
+#        inner join chart c on (c.id = a.chart_id)
+#  where     a.transdate >= '2019-01-01'
+#        and a.transdate <= '2020-01-01'
+#  group by c.id, c.accno, c.category
+#  order by type, c.accno;
+#  accno |             description             | cat | ob_amount |   amount   | year_end_amount | amount_with_cb | cb_amount  | ob_next_year | type  
+# -------+-------------------------------------+-----+-----------+------------+-----------------+----------------+------------+--------------+-------
+#  0890  | Gewinnvortrag vor Verwendung        | Q   |           |            |                 |        0.00000 |    0.00000 |    214.20000 | asset
+#  1200  | Bank                                | A   | -40.00000 | -166.60000 |      -206.60000 |        0.00000 | -206.60000 |   -206.60000 | asset
+#  1400  | Ford. a.Lieferungen und Leistungen  | A   |           | -166.60000 |      -166.60000 |        0.00000 | -166.60000 |   -166.60000 | asset
+#  1600  | Verbindlichkeiten aus Lief.u.Leist. | L   |           |  119.00000 |       119.00000 |        0.00000 |  119.00000 |    119.00000 | asset
+#  1890  | Privateinlagen                      | Q   |  40.00000 |            |        40.00000 |        0.00000 |   40.00000 |     40.00000 | asset
+#  9000  | Saldenvorträge,Sachkonten           | A   |           |            |                 |        0.00000 |    0.00000 |      0.00000 | asset
+#  1576  | Abziehbare Vorsteuer 19 %           | E   |           |  -19.00000 |       -19.00000 |        0.00000 |  -19.00000 |              | pl
+#  1776  | Umsatzsteuer 19 %                   | I   |           |   53.20000 |        53.20000 |        0.00000 |   53.20000 |              | pl
+#  3400  | Wareneingang 16%/19% Vorsteuer      | E   |           | -100.00000 |      -100.00000 |        0.00000 | -100.00000 |              | pl
+#  8400  | Erlöse 16%/19% USt.                 | I   |           |  280.00000 |       280.00000 |        0.00000 |  280.00000 |              | pl
+# (10 rows) 
+
+# ob_amount + amount = year_end_amount
+# amount_with_cb should be 0 after year-end transactions
+# year_end_amount and cb_amount should be the same (will be true with amount_with_cb = 0)
+# cb_amount should match ob_next_year for asset accounts, except for profit-carried-forward
+# ob_next_year should be empty for profit-loss-accounts
+
+# Oops, we forgot some bookings, lets quickly add them and run
+#_year_end_bookings again.
+
+# Just these new bookings by themselves will lead to a loss, so the loss account
+# will be booked rather than the profit account.
+# It would probably be better to check the total profit/loss so far, and
+# adjust that profit-loss-carry-over # chart, rather than creating a new entry
+# for the loss.
+
+gl_booking(10, $booking_date, 'foo', 'bar', $cash, $bank, 0, 0);
+gl_booking(5,  $booking_date, 'foo', 'bar', $betriebsbedarf, $cash, 0, 0);
+
+SL::Controller::YearEndTransactions::_year_end_bookings( start_date => $start_date,
+                                                         cb_date    => $cb_date,
+                                                       );
+
+is(SL::DB::Manager::AccTransaction->get_all_count(where => [ cb_transaction => 1 ]), 23, 'acc_trans cb_transactions created ok');
+is(SL::DB::Manager::AccTransaction->get_all_count(where => [ ob_transaction => 1 ]), 16, 'acc_trans ob_transactions created ok');
+is(SL::DB::Manager::GLTransaction->get_all_count( where => [ cb_transaction => 1 ]),  9, 'GL cb_transactions created ok');
+is(SL::DB::Manager::GLTransaction->get_all_count( where => [ ob_transaction => 1 ]),  7, 'GL ob_transactions created ok');
+
+is_deeply( &get_final_balances, 
+           [
+             {
+               'accno' => '0868',
+               'amount' => undef,
+               'amount_with_cb' => '0.00000',
+               'cat' => 'Q',
+               'cb_amount' => '0.00000',
+               'ob_amount' => undef,
+               'ob_next_year' => '-5.00000',
+               'type' => 'asset',
+               'year_end_amount' => undef
+             },
+             {
+               'accno' => '0890',
+               'amount' => undef,
+               'amount_with_cb' => '0.00000',
+               'cat' => 'Q',
+               'cb_amount' => '0.00000',
+               'ob_amount' => undef,
+               'ob_next_year' => '214.20000',
+               'type' => 'asset',
+               'year_end_amount' => undef
+             },
+             {
+               'accno' => '1000',
+               'amount' => '-5.00000',
+               'amount_with_cb' => '0.00000',
+               'cat' => 'A',
+               'cb_amount' => '-5.00000',
+               'ob_amount' => undef,
+               'ob_next_year' => '-5.00000',
+               'type' => 'asset',
+               'year_end_amount' => '-5.00000'
+             },
+             {
+               'accno' => '1200',
+               'amount' => '-156.60000',
+               'amount_with_cb' => '0.00000',
+               'cat' => 'A',
+               'cb_amount' => '-196.60000',
+               'ob_amount' => '-40.00000',
+               'ob_next_year' => '-196.60000',
+               'type' => 'asset',
+               'year_end_amount' => '-196.60000'
+             },
+             {
+               'accno' => '1400',
+               'amount' => '-166.60000',
+               'amount_with_cb' => '0.00000',
+               'cat' => 'A',
+               'cb_amount' => '-166.60000',
+               'ob_amount' => undef,
+               'ob_next_year' => '-166.60000',
+               'type' => 'asset',
+               'year_end_amount' => '-166.60000'
+             },
+             {
+               'accno' => '1600',
+               'amount' => '119.00000',
+               'amount_with_cb' => '0.00000',
+               'cat' => 'L',
+               'cb_amount' => '119.00000',
+               'ob_amount' => undef,
+               'ob_next_year' => '119.00000',
+               'type' => 'asset',
+               'year_end_amount' => '119.00000'
+             },
+             {
+               'accno' => '1890',
+               'amount' => undef,
+               'amount_with_cb' => '0.00000',
+               'cat' => 'Q',
+               'cb_amount' => '40.00000',
+               'ob_amount' => '40.00000',
+               'ob_next_year' => '40.00000',
+               'type' => 'asset',
+               'year_end_amount' => '40.00000'
+             },
+             {
+               'accno' => '9000',
+               'amount' => undef,
+               'amount_with_cb' => '0.00000',
+               'cat' => 'A',
+               'cb_amount' => '0.00000',
+               'ob_amount' => undef,
+               'ob_next_year' => '0.00000',
+               'type' => 'asset',
+               'year_end_amount' => undef
+             },
+             {
+               'accno' => '1576',
+               'amount' => '-19.80000',
+               'amount_with_cb' => '0.00000',
+               'cat' => 'E',
+               'cb_amount' => '-19.80000',
+               'ob_amount' => undef,
+               'ob_next_year' => undef,
+               'type' => 'pl',
+               'year_end_amount' => '-19.80000'
+             },
+             {
+               'accno' => '1776',
+               'amount' => '53.20000',
+               'amount_with_cb' => '0.00000',
+               'cat' => 'I',
+               'cb_amount' => '53.20000',
+               'ob_amount' => undef,
+               'ob_next_year' => undef,
+               'type' => 'pl',
+               'year_end_amount' => '53.20000'
+             },
+             {
+               'accno' => '3400',
+               'amount' => '-100.00000',
+               'amount_with_cb' => '0.00000',
+               'cat' => 'E',
+               'cb_amount' => '-100.00000',
+               'ob_amount' => undef,
+               'ob_next_year' => undef,
+               'type' => 'pl',
+               'year_end_amount' => '-100.00000'
+             },
+             {
+               'accno' => '4980',
+               'amount' => '-4.20000',
+               'amount_with_cb' => '0.00000',
+               'cat' => 'E',
+               'cb_amount' => '-4.20000',
+               'ob_amount' => undef,
+               'ob_next_year' => undef,
+               'type' => 'pl',
+               'year_end_amount' => '-4.20000'
+             },
+             {
+               'accno' => '8400',
+               'amount' => '280.00000',
+               'amount_with_cb' => '0.00000',
+               'cat' => 'I',
+               'cb_amount' => '280.00000',
+               'ob_amount' => undef,
+               'ob_next_year' => undef,
+               'type' => 'pl',
+               'year_end_amount' => '280.00000'
+             },
+           ],
+           'balances after third year_end ok');
+
+clear_up();
+done_testing;
+
+1;
+
+sub clear_up {
+  foreach (qw(BankAccount
+              GLTransaction
+              AccTransaction
+              InvoiceItem
+              Invoice
+              PurchaseInvoice
+              Part
+              Customer
+             )
+           ) {
+    "SL::DB::Manager::${_}"->delete_all(all => 1);
+  }
+};
+sub get_account_balances {
+  my $query = <<SQL;
+  select c.accno,
+         case when c.category = ANY( '{I,E}'   )   then 'profit_loss_account'
+              when c.category = ANY( '{A,C,L,Q}' ) then 'asset_account'
+                                                   else null
+              end as account_type,
+         sum(a.amount)
+    from acc_trans a
+         left join chart c on (c.id = a.chart_id)
+group by c.accno, account_type
+order by account_type, c.accno;
+SQL
+
+  my $result = selectall_hashref_query($::form, $dbh, $query);
+  return $result;
+};
+
+sub get_final_balances {
+  my $query = <<SQL;
+ select c.accno,
+        c.category as cat,
+        sum(a.amount     ) filter (where ob_transaction is true                              and a.transdate  < ?) as ob_amount,
+        sum(a.amount     ) filter (where cb_transaction is false and ob_transaction is false and a.transdate  < ?) as amount,
+        sum(a.amount     ) filter (where cb_transaction is false                             and a.transdate  < ?) as year_end_amount,
+        sum(a.amount     ) filter (where                                                         a.transdate  < ?) as amount_with_cb,
+        sum(a.amount * -1) filter (where cb_transaction is true                              and a.transdate  < ?) as cb_amount,
+        sum(a.amount     ) filter (where ob_transaction is true                              and a.transdate  = ?) as ob_next_year,
+        case when c.category = ANY( '{I,E}'     ) then 'pl'
+             when c.category = ANY( '{A,C,L,Q}' ) then 'asset'
+                                                  else null
+             end as type
+   from acc_trans a
+        inner join chart c on (c.id = a.chart_id)
+  where     a.transdate >= ?
+        and a.transdate <= ?
+  group by c.id, c.accno, c.category
+  order by type, c.accno
+SQL
+
+  my $result = selectall_hashref_query($::form, $dbh, $query, $ob_date, $ob_date, $ob_date, $ob_date, $ob_date, $ob_date, $start_date, $ob_date);
+  return $result;
+}
+
+sub gl_booking {
+  # wrapper around SL::Dev::Record::create_gl_transaction for quickly creating transactions
+  my ($amount, $date, $reference, $description, $gegenkonto, $konto, $ob, $cb) = @_;
+
+  # my $transdate = $::locale->parse_date_to_object($date);
+
+  return create_gl_transaction(
+    ob_transaction => $ob,
+    cb_transaction => $cb,
+    transdate      => $date,
+    reference      => $reference,
+    description    => $description,
+    bookings       => [
+                        {
+                          chart  => $konto,
+                          credit => $amount,
+                        },
+                        {
+                          chart => $gegenkonto,
+                          debit => $amount,
+                        },
+                      ],
+  );
+};
index 0a9a047..cde5b44 100644 (file)
@@ -1,2 +1,9 @@
-Order Allow,Deny
-Deny from all
+<IfModule mod_authz_core.c>
+  # Apache 2.4
+  Require all denied
+</IfModule>
+<IfModule !mod_authz_core.c>
+  # Apache 2.2
+  Order deny,allow
+  Deny from all
+</IfModule>
index 2dc69dc..b34ce11 100644 (file)
@@ -1,15 +1,21 @@
 kivitendo selftest report.
 
+[% IF errors %]
+  General error(s) have occurred.
+  [% errors %]
+[% END %]
+
 Host:   [% host %]
 Path:   [% path %]
 DB:     [% database %]
+Client: [% client %]
 Result: [% SELF.aggreg.get_status %]
 
 ------------
 Full report:
 ------------
 
-[% FOREACH module = SELF.diag_per_module.keys %]
+[% FOREACH module = SELF.diag_per_module.keys.sort %]
 Module: [% module %]
 --------------------
 
diff --git a/templates/mobile_webpages/file/list.html b/templates/mobile_webpages/file/list.html
new file mode 100644 (file)
index 0000000..30c493b
--- /dev/null
@@ -0,0 +1,75 @@
+[%- USE LxERP -%]
+[%- USE L %]
+[%- USE HTML %]
+[%- USE P %]
+
+<h4>[% source.title | html %]</h4>
+
+<div>
+[%- FOREACH source = SOURCES %]
+ <table class="highlight striped">
+  <thead>
+   <tr>
+  [%- SET checkname = source.chk_action %]
+  [%- IF edit_attachments %]
+    <th>[% P.M.checkbox_tag(checkname _ '_checkall', label=' ', checkall=checkname _ "[]") %]</th>
+  [%- END %]
+    <th>[% LxERP.t8('Date') | html %]</th>
+    <th>[% source.file_title | html %]</th>
+  [%- IF file_type == 'image' %]
+    <th>[% LxERP.t8('Title') %]</th>
+    <th>[% LxERP.t8('ImagePreview') %]</th>
+    <th>[% LxERP.t8('Description') %]</th>
+  [%- ELSE %]
+    <th></th>
+  [%- END %]
+   </tr>
+  </thead>
+
+  <tbody>
+  [%- FOREACH file = source.files %]
+   <tr>
+    [%- IF edit_attachments %]
+    <td>[%- P.M.checkbox_tag(checkname _ '[]', value=file.id _ '_' _ file.version, class=checkname, label=' ') %]</td>
+    [%- END %]
+    <td>[% file.mtime_as_timestamp_s %][% L.hidden_tag("version[]", file.version) %]</td>
+    <td><a href="controller.pl?action=File/download&id=[% file.id %][%- IF file.version %]&version=[%- file.version %][%- END %]">
+        <span id="[% "filename_" _ file.id %][%- IF file.version %]_[% file.version %][%- END %]">[% file.file_name %]</span></a></td>
+    [%- IF file_type == 'image' %]
+    <td>[% file.title %]</td>
+    <td>
+      <img src="controller.pl?action=File/download&id=[% file.id %][%- IF file.version %]&version=[%- file.version %][%- END %]" alt="[% file.title %]" width="64px">
+    </td>
+    <td>[% file.description %]</td>
+    [%- ELSE %]
+    <td></td>
+    [%- END %]
+   </tr>
+  [%- END %]
+  </tbody>
+ </table>
+  <div>
+  [%- IF edit_attachments %]
+    [%- IF source.can_import %]
+      [% P.M.button_tag("kivi.File.unimport(" _ object_id _ ",'" _ object_type _ "','" _ file_type _ "','" _ checkname _ "');", source.chk_title) %]
+    [%- ELSE %]
+      [%- IF source.can_delete %]
+        [% P.M.button_tag("kivi.File.delete("   _ object_id _ ",'" _ object_type _ "','" _ file_type _ "','" _ checkname _ "');", source.chk_title) %]
+      [%- END %]
+    [%- END %]
+  [%- END %]
+  [%- IF source.can_rename %]
+    [% P.M.button_tag("kivi.File.rename(" _ object_id _ ",'" _ object_type _ "','" _ file_type _ "','" _ checkname _ "');",  source.rename_title ) %]
+  [%- END %]
+  [%- IF source.can_upload %]
+    [% P.M.button_tag("kivi.File.upload(" _ object_id _ ",'" _ object_type _ "','" _ file_type _ "','" _ source.upload_title _ "');", source.upload_title ) %]
+  [%- END %]
+  [%- IF source.can_import %]
+    [% P.M.button_tag("kivi.File.import("   _ object_id _ ",'" _ object_type _ "','" _ file_type _ "','" _ source.name _ "','" _ source.path _"');",  source.import_title ) %]
+  [%- END %]
+  </div>
+[%- END %]
+  <div></div><div>
+[% P.M.button_tag("kivi.File.update(" _ object_id _ ",'" _ object_type _ "','" _ file_type _ "');", LxERP.t8('Update')) %]
+  </div>
+</div>
diff --git a/templates/mobile_webpages/file/upload_dialog.html b/templates/mobile_webpages/file/upload_dialog.html
new file mode 100644 (file)
index 0000000..85df476
--- /dev/null
@@ -0,0 +1,38 @@
+[%- USE L -%]
+[%- USE P -%]
+[%- USE T8 -%]
+[%- USE LxERP -%]
+[%- USE JavaScript -%]
+
+<form method="post" id="upload_form" enctype="multipart/form-data" action="controller.pl">
+  [% SET multiple = 'true' %]
+  [% IF SELF.object_type == 'shop_image' %][% multiple = 'false' %][% END %]
+    <div class="file-field input-field col s12">
+      <div class="btn m3 s12">
+        <span>[% 'Filename' | $T8 %]</span>
+        <input
+          name="uploadfiles[]" type="file" [% IF multiple %]multiple[% END %]
+          id="upload_files" size="45" accept="[% SELF.accept_types %]" capture="camera"
+          onchange="kivi.File.allow_upload_submit();">
+      </div>
+      <div class="file-path-wrapper m9 s12">
+        <input class="file-path validate" type="text">
+      </div>
+    </div>
+
+    <div class="m12 s12">
+     [% P.M.button_tag(
+       P.escape_js_call("kivi.File.upload_selected_files", SELF.object_id, SELF.object_type, SELF.file_type, SELF.maxsize, SELF.is_global),
+       LxERP.t8("Upload file"),
+       id="upload_selected_button",
+       disabled=1)
+     %]
+     [% P.M.button_tag("kivi.File.reset_upload_form()", LxERP.t8('Reset'), flat=1) %]
+    </div>
+
+
+
+ <hr>
+
+ <div id="upload_result"></div>
+</form>
diff --git a/templates/mobile_webpages/generic/error.html b/templates/mobile_webpages/generic/error.html
new file mode 100644 (file)
index 0000000..53609a3
--- /dev/null
@@ -0,0 +1,5 @@
+[%- USE T8 %]
+[%- USE HTML %]
+
+ <h2>[% IF title_error %][% title_error %][% ELSE %][% 'Error!' | $T8 %][% END %]</h2>
+ <div class="card-panel red">[% label_error %]</div>
diff --git a/templates/mobile_webpages/generic/exception.html b/templates/mobile_webpages/generic/exception.html
new file mode 100644 (file)
index 0000000..47b1060
--- /dev/null
@@ -0,0 +1,22 @@
+[%- USE LxERP %]
+[%- USE HTML %]
+
+ <h1 class="message_error">[%- LxERP.t8('Error!') %]</h1>
+
+ <p>
+  [%- LxERP.t8('An exception occurred during execution.') %]
+ </p>
+
+ <div>
+  <table>
+   <tr>
+    <td valign="top">[%- LxERP.t8('Type') %]:</td>
+    <td valign="top">[%- HTML.escape(error.type) %]</td>
+   </tr>
+
+   <tr>
+    <td valign="top">[%- LxERP.t8('Information') %]:</td>
+    <td valign="top"><pre>[%- HTML.escape(error.info) %]</pre></td>
+   </tr>
+  </table>
+ </div>
diff --git a/templates/mobile_webpages/generic/information.html b/templates/mobile_webpages/generic/information.html
new file mode 100644 (file)
index 0000000..5f4230f
--- /dev/null
@@ -0,0 +1,3 @@
+[%- USE T8 %]
+<div class="message_ok">[% IF title_information %][% title_information %][% ELSE %][% 'Information' | $T8 %][% END %]</div>
+<p>[% label_information %]</p>
diff --git a/templates/mobile_webpages/image_upload/local_list.html b/templates/mobile_webpages/image_upload/local_list.html
new file mode 100644 (file)
index 0000000..4305260
--- /dev/null
@@ -0,0 +1,69 @@
+[%- USE LxERP -%] [%- USE L %]
+[%- USE HTML %]
+[%- USE P %]
+[%- USE T8 %]
+
+<h4>[% source.title | html %]</h4>
+
+  <p>Schritt 1: Bilder machen</p>
+
+  <div id="stored-images" class="container">
+  </div>
+
+
+  <div class="container">
+    <div class="row">
+
+      <div class="file-field input-field col s12">
+        <div class="btn m3 s12 col">
+          <span><i class='material-icons col s12 center'>add_a_photo</i></span>
+          <input
+            name="uploadfiles[]" type="file" [% IF multiple %]multiple[% END %]
+            id="upload_files" accept="[% SELF.accept_types %]" capture="camera"
+            onchange="kivi.ImageUpload.add_files(this)">
+        </div>
+        <div class="file-path-wrapper m9 s12">
+          <input class="file-path validate" type="hidden">
+        </div>
+      </div>
+
+    </div>
+  </div>
+
+    <hr>
+
+    <p>Schritt 2: Dateien hochladen</p>
+  <div class="container">
+    <div class="row">
+      [% P.M.input_tag("object_number", "", label=LxERP.t8("Number"), class="col s4", onkeyup="kivi.ImageUpload.resolve_object(event)") %]
+      <div id="object_description" class="col s8">-</div>
+      [% P.M.button_tag("kivi.ImageUpload.upload_files()", LxERP.t8("Upload Images"), id="upload_images_submit", class="col s12") %]
+    </div>
+
+
+  </div>
+</div>
+
+<div id="warn_modal" class="modal">
+  <div class="modal-content">
+    <h4>Warning</h4>
+
+    <p></p>
+  </div>
+</div>
+
+<div id="upload_modal" class="modal">
+  <div class="modal-content">
+    <h4>[% 'Uploading Data' | $T8 %]</h4>
+
+    <div id="upload_progress" class="progress">
+      <div class="indeterminate"></div>
+    </div>
+
+    [% P.M.button_tag("kivi.ImageUpload.upload_in_progress.abort()", LxERP.t8("Abort"), class="modal-close") %]
+  </div>
+</div>
+
+
+[% L.hidden_tag("object_type", FORM.object_type) %]
+[% L.hidden_tag("object_id") %]
diff --git a/templates/mobile_webpages/layout/javascript_setup.js b/templates/mobile_webpages/layout/javascript_setup.js
new file mode 100644 (file)
index 0000000..2b03f4a
--- /dev/null
@@ -0,0 +1,38 @@
+[%- USE LxERP %]
+[%- USE JavaScript %]
+[%- USE JSON %]
+kivi.myconfig = [% JSON.json(MYCONFIG) %];
+$(function() {
+  $.datepicker.setDefaults(
+    $.extend({}, $.datepicker.regional[kivi.myconfig.countrycode], {
+      dateFormat: kivi.myconfig.dateformat.replace(/d+/gi, 'dd').replace(/m+/gi, 'mm').replace(/y+/gi, 'yy'),
+      showOn: "button",
+      showButtonPanel: true,
+      changeMonth: true,
+      changeYear: true,
+      buttonImage: "image/calendar.png",
+      buttonImageOnly: true
+  }));
+
+  kivi.setup_formats({
+    numbers: kivi.myconfig.numberformat,
+    dates:   kivi.myconfig.dateformat,
+    times:   kivi.myconfig.timeformat
+  });
+
+  kivi.reinit_widgets();
+
+[%- IF ajax_spinner %]
+  $(document).ajaxSend(function() {
+    $('#ajax-spinner').show();
+  }).ajaxStop(function() {
+    $('#ajax-spinner').hide();
+  });
+[% END %]
+});
+
+[%- IF focus -%]
+function fokus() {
+  $('[% focus %]').focus();
+}
+[%- END -%]
diff --git a/templates/mobile_webpages/login/company_logo.html b/templates/mobile_webpages/login/company_logo.html
new file mode 100644 (file)
index 0000000..cb70466
--- /dev/null
@@ -0,0 +1,39 @@
+[%- USE T8 %]
+[%- USE HTML %][%- USE LxERP %]
+ <center>
+  <a class="nomobile" href="http://www.kivitendo.de" target="_top"><img src="image/kivitendo[% xmas %].png" class='kivitendo-logo' border="0" alt='[% 'kivitendo' | $T8 %]' title="[% 'kivitendo Homepage' | $T8 %]"></a>
+
+  <h3 class="login">[% 'kivitendo' | $T8 %] [% version %]</h3>
+
+[%- IF git_head %]
+  <p>[%- LxERP.t8("Git revision: #1, #2 #3", git_head.hash.substr(0, 7), git_head.author_date.to_kivitendo, git_head.author_date.strftime('%H:%M:%S %Z')) %]</p>
+[%- END %]
+
+  <p>[% 'companylogo_subtitle' | $T8 %]</p>
+  <p>
+   <b>
+    [% HTML.escape(defaults.company) %]
+    <br>
+    [% HTML.escape(defaults.address).replace('\\\\n', '<br>').replace('\n', '<br>') %]
+   </b>
+
+   <br>
+   <br>
+
+   <table border="0">
+    <tr>
+     <th align="left">[% 'User' | $T8 %]</th>
+     <td>[% MYCONFIG.name | html %]</td>
+    </tr>
+    <tr>
+     <th align="left">[% 'Client' | $T8 %]</th>
+     <td>[% client.name | html %]</td>
+    </tr>
+    <tr>
+     <th align="left">[% 'Language' | $T8 %]</th>
+     <td>[% MYCONFIG.countrycode | html %]</td>
+    </tr>
+   </table>
+ </center>
+
+ [%- todo_list %]
diff --git a/templates/mobile_webpages/login_screen/user_login.html b/templates/mobile_webpages/login_screen/user_login.html
new file mode 100644 (file)
index 0000000..6fb5671
--- /dev/null
@@ -0,0 +1,49 @@
+[%- USE T8 %]
+[%- USE P %]
+[%- USE HTML %][%- USE L -%][%- USE LxERP -%]
+
+<center>
+  <a href="https://www.kivitendo.de" target="_top" class="no-underlined-links center-align">
+    <img src="image/kivitendo.png" class="responsive-img kivitendo-logo">
+  </a>
+</center>
+<h5 class="center-align">[% LxERP.t8('kivitendo v#1', version) %]</h5>
+
+<div class="section">
+<div class="container">
+[% IF error %]
+  <div class="col s12 red">[% error | html %]</div>
+[% END %]
+[% IF warning %]
+  <div class="col s12 blue lighten-3">[% warning | html %]</div>
+[% END %]
+[% IF info %]
+  <div class="col s12 green lighten-1">[% ok | html %]</div>
+[% END %]
+</div>
+</div>
+
+<div class="section">
+<div class="container">
+  <div class="z-depth-1 grey lighten-4 row" style="padding: 32px 48px 0px 48px; border: 1px solid #EEE;">
+
+    <form method="post" name="loginscreen" action="controller.pl" target="_top" class="col s12">
+      <div class="row">
+       [% P.M.input_tag('{AUTH}login', FORM.$auth_login, id='auth_login', class='initial_focus validate col s12', label=LxERP.t8('Login Name')) %]
+      </div>
+      <div class="row">
+        [% P.M.input_tag('{AUTH}password', '', type='password', id='auth_password', class='validate col s12', label=LxERP.t8('Password')) %]
+      </div>
+      <div class="row">
+        [% P.M.select_tag('{AUTH}client_id', SELF.clients, id='auth_client_id', class='col s12', title_key='name', default=SELF.default_client_id) %]
+      </div>
+
+      <div class="row">
+        [% L.hidden_tag("action", "LoginScreen/login") %]
+        [% P.M.submit_tag("btn_login", "Login", class="col s12", large=1) %]
+      </div>
+    </form>
+
+  </div>
+</div>
+</div>
diff --git a/templates/mobile_webpages/menu/menu.html b/templates/mobile_webpages/menu/menu.html
new file mode 100644 (file)
index 0000000..8650cfa
--- /dev/null
@@ -0,0 +1,39 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE HTML %]
+[%- USE LxERP -%]
+<nav>
+  <div class="nav-wrapper">
+    <a class="brand-logo">Kivitendo</a>
+    <a href="#" data-target="nav-mobile" class="sidenav-trigger"><i class="material-icons">menu</i></a>
+    <ul id="nav" class="right hide-on-med-and-down">
+      [% PROCESS menu_items %]
+    </ul>
+  </div>
+</nav>
+
+<ul id="nav-mobile" class="sidenav">
+  [% PROCESS menu_items %]
+</ul>
+
+
+[%- BLOCK menu_items %]
+ [%- IF MYCONFIG.login %]
+  <li><a>[% 'User' | $T8 %]: [% MYCONFIG.login | html %]</a></li>
+ [%- ELSE %]
+  <li>[% L.link(C.url_for(controller='LoginScreen', action='user_login'), LxERP.t8('Login')) %]</li>
+ [%- END %]
+
+  <li><div class="divider"></div></li>
+
+  [%- FOREACH node = menu.tree %]
+    [%- NEXT UNLESS node.visible %]
+    <li>[% L.link(menu.href_for_node(node) || '#', menu.name_for_node(node), target=node.target) %]</li>
+  [%- END %]
+
+  <li><div class="divider"></div></li>
+
+ [%- IF MYCONFIG.login %]
+  <li>[% L.link(C.url_for(controller='LoginScreen', action='logout'), LxERP.t8('Logout')) %]</li>
+ [%- END %]
+[% END %]
diff --git a/templates/mobile_webpages/test/components.html b/templates/mobile_webpages/test/components.html
new file mode 100644 (file)
index 0000000..f539799
--- /dev/null
@@ -0,0 +1,48 @@
+[% USE P %]
+
+<h1>Material Components Tests</h1>
+
+
+<h2>Buttons</h2>
+
+[% P.M.button_tag("", "button") %]
+[% P.M.button_tag("", P.M.icon('add'), floating=1) %]
+[% P.M.submit_tag("", "submit") %]
+[% P.M.submit_tag("", "disabled submit", disabled=1) %]
+[% P.M.button_tag("", "button with left icon", icon="cloud", icon_left=1) %]
+[% P.M.button_tag("", "button with right icon", icon="save", icon_right=1) %]
+[% P.M.button_tag("", "flat button", flat=1) %]
+[% P.M.button_tag("", "large button", large=1) %]
+[% P.M.button_tag("", "small button", small=1) %]
+[% P.M.button_tag("", "disabled small button", disabled=1, small=1) %]
+
+<h2>Icons</h2>
+
+[% P.M.icon("alarm") %]
+[% P.M.icon("alarm", large=1) %]
+[% P.M.icon("alarm", medium=1) %]
+[% P.M.icon("alarm", small=1) %]
+[% P.M.icon("alarm", tiny=1) %]
+
+<h2>Inputs</h2>
+
+
+[% P.M.input_tag("", "", label="test input without anything") %]
+[% P.M.input_tag("", "default value", label="test input with default value") %]
+[% P.M.input_tag("", "", placeholder="with placeholder", label="test input with placeholder") %]
+[% P.M.input_tag("", "default value", placeholder="with placeholder", label="test input with placeholder and default value") %]
+
+<h3>With grid:</h3>
+<div class="row">
+[% P.M.input_tag("", "", label="2 cols", class="col s6") %]
+[% P.M.input_tag("", "", label="2 cols", class="col s6") %]
+[% P.M.input_tag("i1", "", placeholder="2 cols placeholder", icon="phone", class="col s6") %]
+[% P.M.input_tag("i2", "", label="2 cols label", icon="account_circle", class="col s6") %]
+</div>
+
+<h2>Date Picker</h2>
+
+<div class="row">
+[% P.M.date_tag("d1", "", label="date of birth", class="col s6", icon="date_range") %]
+[% P.M.date_tag("d1", "", class="col s6", placeholder="date of birth?", icon="access_time") %]
+</div>
diff --git a/templates/mobile_webpages/test/modal.html b/templates/mobile_webpages/test/modal.html
new file mode 100644 (file)
index 0000000..2d40229
--- /dev/null
@@ -0,0 +1,49 @@
+[% USE P %]
+
+<h1>Material Modal Tests</h1>
+
+
+<div>
+
+Button triggered modal, no close/agree button:<br>
+
+[% P.M.button_tag("", "Modal", class="modal-trigger", href="#modal1") %]
+<!-- Modal Structure -->
+<div id="modal1" class="modal">
+  <div class="modal-content">
+    <h4>Modal Header</h4>
+    <p>A bunch of text</p>
+  </div>
+</div>
+
+</div>
+
+
+<div>
+Button triggered modal, close/agree button:<br>
+
+[% P.M.button_tag("", "Modal", class="modal-trigger", href="#modal2") %]
+<!-- Modal Structure -->
+<div id="modal2" class="modal">
+  <div class="modal-content">
+    <h4>Modal Header</h4>
+    <p>A bunch of text</p>
+  </div>
+  <div class="modal-footer">
+    [% P.M.button_tag('', 'Cancel', class="modal-close", flat=1) %]
+    [% P.M.button_tag('', 'Agree', class="modal-close", flat=1) %]
+  </div>
+</div>
+
+</div>
+
+<div>
+Javascript triggered modal:<br>
+[% P.M.button_tag("\$('#modal1').modal('open')", "Open!") %]
+</div>
+
+
+<div>
+popup_dialog modal with given html:<br>
+[% P.M.button_tag("kivi.popup_dialog({ html: 'Testtext'})", "Testtext") %]
+</div>
diff --git a/templates/pdf/pdf_a_metadata.xmp b/templates/pdf/pdf_a_metadata.xmp
new file mode 100644 (file)
index 0000000..7c769bc
--- /dev/null
@@ -0,0 +1,138 @@
+<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d' ?>
+
+<x:xmpmeta xmlns:x="adobe:ns:meta/"
+           x:xmptk="Adobe XMP Core 4.0-c316 44.253921, Sun Oct 01 2006 17:14:39">
+ <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+  <rdf:Description rdf:about=""
+                   xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/"
+                   xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#"
+                   xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#"
+                   >
+   <pdfaExtension:schemas>
+    <rdf:Bag>
+     <rdf:li rdf:parseType="Resource">
+      <pdfaSchema:namespaceURI>http://ns.adobe.com/pdfx/1.3/</pdfaSchema:namespaceURI>
+      <pdfaSchema:prefix>pdfx</pdfaSchema:prefix>
+      <pdfaSchema:schema>PDF/X Schema</pdfaSchema:schema>
+      <pdfaSchema:property><rdf:Seq>
+       <rdf:li rdf:parseType="Resource">
+        <pdfaProperty:category>external</pdfaProperty:category>
+        <pdfaProperty:description>URL to an online version or preprint</pdfaProperty:description>
+        <pdfaProperty:name>AuthoritativeDomain</pdfaProperty:name>
+        <pdfaProperty:valueType>Text</pdfaProperty:valueType>
+       </rdf:li></rdf:Seq>
+      </pdfaSchema:property>
+     </rdf:li>
+     <rdf:li rdf:parseType="Resource">
+      <pdfaSchema:namespaceURI>http://www.aiim.org/pdfua/ns/id/</pdfaSchema:namespaceURI>
+      <pdfaSchema:prefix>pdfuaid</pdfaSchema:prefix>
+      <pdfaSchema:schema>PDF/UA ID Schema</pdfaSchema:schema>
+      <pdfaSchema:property><rdf:Seq>
+       <rdf:li rdf:parseType="Resource">
+        <pdfaProperty:category>internal</pdfaProperty:category>
+        <pdfaProperty:description>Part of PDF/UA standard</pdfaProperty:description>
+        <pdfaProperty:name>part</pdfaProperty:name>
+        <pdfaProperty:valueType>Integer</pdfaProperty:valueType>
+       </rdf:li></rdf:Seq>
+      </pdfaSchema:property>
+     </rdf:li>
+     <rdf:li rdf:parseType="Resource">
+      <pdfaSchema:schema>PRISM metadata</pdfaSchema:schema>
+      <pdfaSchema:namespaceURI>http://prismstandard.org/namespaces/basic/2.2/</pdfaSchema:namespaceURI>
+      <pdfaSchema:prefix>prism</pdfaSchema:prefix>
+      <pdfaSchema:property><rdf:Seq>
+       <rdf:li rdf:parseType="Resource">
+        <pdfaProperty:name>aggregationType</pdfaProperty:name>
+        <pdfaProperty:valueType>Text</pdfaProperty:valueType>
+        <pdfaProperty:category>external</pdfaProperty:category>
+        <pdfaProperty:description>The type of publication. If defined, must be one of book, catalog, feed, journal, magazine, manual, newsletter, pamphlet.</pdfaProperty:description>
+       </rdf:li>
+       <rdf:li rdf:parseType="Resource">
+        <pdfaProperty:name>url</pdfaProperty:name>
+        <pdfaProperty:valueType>URL</pdfaProperty:valueType>
+        <pdfaProperty:category>external</pdfaProperty:category>
+        <pdfaProperty:description>URL for the article or unit of content</pdfaProperty:description>
+       </rdf:li>
+      </rdf:Seq></pdfaSchema:property>
+     </rdf:li>
+[% IF zugferd %]
+     <rdf:li rdf:parseType="Resource">
+      <pdfaSchema:schema>Factur-X PDFA Extension Schema</pdfaSchema:schema>
+      <pdfaSchema:namespaceURI>urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#</pdfaSchema:namespaceURI>
+      <pdfaSchema:prefix>fx</pdfaSchema:prefix>
+      <pdfaSchema:property>
+       <rdf:Seq>
+        <rdf:li rdf:parseType="Resource">
+         <pdfaProperty:name>DocumentFileName</pdfaProperty:name>
+         <pdfaProperty:valueType>Text</pdfaProperty:valueType>
+         <pdfaProperty:category>external</pdfaProperty:category>
+         <pdfaProperty:description>name of the embedded XML invoice file</pdfaProperty:description>
+        </rdf:li>
+        <rdf:li rdf:parseType="Resource">
+         <pdfaProperty:name>DocumentType</pdfaProperty:name>
+         <pdfaProperty:valueType>Text</pdfaProperty:valueType>
+         <pdfaProperty:category>external</pdfaProperty:category>
+         <pdfaProperty:description>INVOICE</pdfaProperty:description>
+        </rdf:li>
+        <rdf:li rdf:parseType="Resource">
+         <pdfaProperty:name>Version</pdfaProperty:name>
+         <pdfaProperty:valueType>Text</pdfaProperty:valueType>
+         <pdfaProperty:category>external</pdfaProperty:category>
+         <pdfaProperty:description>The actual version of the Factur-X/ZUGFeRD data</pdfaProperty:description>
+        </rdf:li>
+        <rdf:li rdf:parseType="Resource">
+         <pdfaProperty:name>ConformanceLevel</pdfaProperty:name>
+         <pdfaProperty:valueType>Text</pdfaProperty:valueType>
+         <pdfaProperty:category>external</pdfaProperty:category>
+         <pdfaProperty:description>The conformance level of the Factur-X/ZUGFeRD data</pdfaProperty:description>
+        </rdf:li>
+       </rdf:Seq>
+      </pdfaSchema:property>
+     </rdf:li>
+[% END %]
+    </rdf:Bag>
+   </pdfaExtension:schemas>
+  </rdf:Description>
+  <rdf:Description rdf:about="" xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
+   <pdf:Producer>[% producer | xml %]</pdf:Producer>
+  </rdf:Description>
+  <rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">
+   <dc:format>application/pdf</dc:format>
+[% IF meta_data.title %]
+   <dc:title><rdf:Alt><rdf:li xml:lang="x-default">[% meta_data.title | xml %]</rdf:li></rdf:Alt></dc:title>
+[% END %]
+   <dc:creator><rdf:Seq><rdf:li>v3</rdf:li></rdf:Seq></dc:creator>
+[% IF meta_data.language %]
+   <dc:language><rdf:Bag><rdf:li>[% meta_data.language | xml %]</rdf:li></rdf:Bag></dc:language>
+[% END %]
+  </rdf:Description>
+  <rdf:Description rdf:about="" xmlns:prism="http://prismstandard.org/namespaces/basic/2.2/">
+  </rdf:Description>
+  <rdf:Description rdf:about="" xmlns:pdfx="http://ns.adobe.com/pdfx/1.3/">
+  </rdf:Description>
+  <rdf:Description rdf:about="" xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/">
+   <pdfaid:part>[% pdf_a_version | xml %]</pdfaid:part>
+   <pdfaid:conformance>[% pdf_a_conformance | xml %]</pdfaid:conformance>
+  </rdf:Description>
+  <rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/">
+   <xmp:CreatorTool>[% producer | xml %]</xmp:CreatorTool>
+   <xmp:ModifyDate>[% timestamp | xml %]</xmp:ModifyDate>
+   <xmp:CreateDate>[% timestamp | xml %]</xmp:CreateDate>
+   <xmp:MetadataDate>[% timestamp | xml %]</xmp:MetadataDate>
+  </rdf:Description>
+  <rdf:Description rdf:about="" xmlns:xmpRights = "http://ns.adobe.com/xap/1.0/rights/">
+  </rdf:Description>
+
+[% IF zugferd %]
+  <rdf:Description xmlns:fx="urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#"
+                   fx:ConformanceLevel="[% zugferd.conformance_level | xml %]"
+                   fx:DocumentFileName="[% zugferd.document_file_name | xml %]"
+                   fx:DocumentType="[% zugferd.document_type | xml %]"
+                   fx:Version="[% zugferd.version %]"
+                   rdf:about=""/>
+[% END %]
+
+ </rdf:RDF>
+</x:xmpmeta>
+
+<?xpacket end='w'?>
index 24b13e1..ca15217 100644 (file)
 %
 % Mandanten / Firma:
 %   Um gleiche Vorlagen für verschiedene Firmen verwenden zu können, wird je
-%   nach dem Wert der Lx-Office-Variablen <%titlebar%> ein
+%   nach dem Wert der Kivitendo-Variablen <%kivicompany%> ein
 %   Firmenverzeichnis ausgewählt (siehe 'insettings.tex'), in dem Briefkopf,
 %   Identitäten und Währungs-/Kontoeinstellungen hinterlegt sind.
-%   <%titlebar%> enthält den Namen des Benutzers und der verwendeten
-%   Mandantendatenbank. Ist kein Firmenname eingetragen, so wird das
+%   <%kivicompany%> enthält den Namen des verwendeten Mandantendaten.
+%   Ist kein Firmenname eingetragen, so wird das
 %   generische Unterverzeichnis 'firma' verwendet.
-%   Das heißt, dass ein Firmenverzeichnis mit dem Namen der Mandantendatenbank
-%   angelegt werden sollte.
 %
 % Identitäten:
 %    In jedem Firmen-Unterverzeichnis soll eine Datei 'ident.tex'
 %    \firma, \strasse, \ort, \ustid, \email und \homepage definiert.
 %
 % Währungen / Konten:
-%    Für jede Währung (siehe 'settings.tex') soll eine Datei vorhanden
+%    Für jede Währung (siehe 'insettings.tex') soll eine Datei vorhanden
 %    sein, die das Währungssymbol (\currency) und folgende Angaben für
 %    ein Konto in dieser Währung enthält \kontonummer, \bank,
 %    \bankleitzahl, \bic und \iban.
 %    So kann in den Dokumenten je nach Währung ein anderes Konto
 %    angegeben werden.
+%    Nach demselben Schema können auch weitere, alternative Bankverbindungen
+%    angelegt werden, die dann in insettings.tex als Variable im
+%    unteren Abschnitt der Datei 'insettings.tex', Kommentar Fusszeile
+%    (cfoot) eingefügt werden.
 %
 % Briefbogen/Logos:
 %    Eine Hintergrundgrafik oder ein Logo kann in Abhängigkeit vom
index f64222d..7a60d3d 100644 (file)
@@ -5,7 +5,7 @@
 \newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
 \newcommand{\lxmedia} {<%media%>}
 \newcommand{\lxcurrency} {<%currency%>}
-\newcommand{\lxtitlebar} {<%titlebar%>}
+\newcommand{\kivicompany} {<%employee_company%>}
 
 % settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
 \input{insettings.tex}
 \begin{document}
 
 \ourfont
-
 \begin{minipage}[t]{8cm}
-  \vspace*{1.0cm}
+  \scriptsize
+
+  {\color{gray}\underline{\firma\ $\cdot$ \strasse\ $\cdot$ \ort}}
+  \normalsize
+
+  \vspace*{0.3cm}
 
   <%name%>
 
+  <%if department_1%><%department_1%><%end if%>
+
+  <%if department_2%><%department_2%><%end if%>
+
+  <%cp_givenname%> <%cp_name%>
+
   <%street%>
 
   ~
@@ -31,7 +41,6 @@
   <%zipcode%> <%city%>
 
   <%country%>
-
 \end{minipage}
 \hfill
 \begin{minipage}[t]{6cm}
 
   \ansprechpartner:\hfill <%employee_name%>
 
+  \textTelefon \hfill <%employee_tel%>
+
+  \textEmail \hfill <%employee_email%>
+
   <%if globalprojectnumber%> \projektnummer:\hfill <%globalprojectnumber%> <%end globalprojectnumber%>
 \end{minipage}
 
+<%if shiptoname%>
+  \vspace{0.8cm}
+ % \hfill \fbox{\parbox{5.55cm}{%
+ % \raggedright
+  \scriptsize \underline{\abweichendeLieferadresse:}\\
+  \normalsize    <%shiptoname%>
+
+                 <%if shiptocontact%> <%shiptocontact%><%end if%>
+
+                 <%shiptodepartment_1%>
+
+                  <%shiptodepartment_2%>
+
+                  <%shiptostreet%>
+
+                  <%shiptozipcode%> <%shiptocity%>
+%  }}
+<%end if%>
 \vspace*{1.5cm}
 
 \hfill
 
-% Anrede nach Geschlecht unterscheiden
-\ifthenelse{\equal{<%cp_name%>}{}}{\anrede}{
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\ifthenelse{\equal{<%cp_name%>}{}}{
+  <%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}{
   \ifthenelse{\equal{<%cp_gender%>}{f}}
     {\anredefrau}{\anredeherr} <%cp_title%> <%cp_name%>,}\\
 
           <%if serialnumber%> && \scriptsize \seriennummer: <%serialnumber%>\\<%end serialnumber%>
           <%if ean%> && \scriptsize \ean: <%ean%>\\<%end ean%>
           <%if projectnumber%> && \scriptsize \projektnummer: <%projectnumber%>\\<%end projectnumber%>
-
           \\[-0.8em]
 <%end number%>
 
index c7f917b..e7ade80 100644 (file)
@@ -23,6 +23,7 @@
 
 \newcommand{\position} {Pos.}
 \newcommand{\artikelnummer} {Art.-Nr.}
+\newcommand{\kundenartnr} {Ihre Art.-Nr.}
 \newcommand{\bild} {Bild}
 \newcommand{\keinbild} {kein Bild}
 \newcommand{\menge} {Menge}
@@ -42,6 +43,7 @@
 \newcommand{\zahlung} {Zahlungsbedingungen:}
 \newcommand{\lieferung} {Lieferbedingungen:}
 \newcommand{\textTelefon} {Tel.:}
+\newcommand{\textEmail} {E-Mail:}
 \newcommand{\textFax} {Fax:}
 
 % angebot (sales_quotion)
 \newcommand{\angebotgueltig} {Das Angebot ist freibleibend gültig bis zum}            %Danach wird das Datum eingefügt, falls das grammatisch nicht funktionieren sollte müssen wir eine ausnahme für die sprache definieren
 \newcommand{\angebotfragen} {Sollten Sie noch Fragen oder Änderungswünsche haben, können Sie uns gerne jederzeit unter den unten genannten Telefonnummern oder E-Mail-Adressen kontaktieren.}
 \newcommand{\angebotagb} {Bei der Durchführung des Auftrags gelten unsere AGB, die wir Ihnen gerne zuschicken.}
-
+\newcommand{\auftragerteilt}{Auftrag erteilt:}
+\newcommand{\angebotortdatum}{Wir nehmen das vorstehende Angebot an.}
+\newcommand{\abweichendeLieferadresse}{abweichende Lieferadresse}
+\newcommand{\optional}{Optionale Position nach Absprache}
 
 % auftragbestätigung (sales_order)
 \newcommand{\auftragsbestaetigung} {Auftragsbestätigung}
 \newcommand{\lieferungErfolgtAm} {Die Lieferung erfolgt am} %Danach wird das Datum eingefügt, falls das grammatisch nicht funktionieren sollte müssen wir eine ausnahme für die sprache definieren
 \newcommand{\auftragpruefen} {Bitte kontrollieren Sie alle Positionen auf Übereinstimmung mit Ihrer Bestellung! Teilen Sie Abweichungen innerhalb von 3 Tagen mit!}
 \newcommand{\proformarechnung} {Proforma Rechnung}
+\newcommand{\nurort} {Ort}
+\newcommand{\den} {den}
+\newcommand{\unterschrift} {Unterschrift}
+\newcommand{\stempel} {ggf. Stempel}
 
 % lieferschein (sales_delivery_order)
 \newcommand{\lieferschein} {Lieferschein}
diff --git a/templates/print/RB/emptyPage.pdf b/templates/print/RB/emptyPage.pdf
new file mode 100644 (file)
index 0000000..5cb37c0
Binary files /dev/null and b/templates/print/RB/emptyPage.pdf differ
index 356a4bc..a3736c4 100644 (file)
@@ -22,6 +22,7 @@
 
 \newcommand{\position} {Pos.}
 \newcommand{\artikelnummer} {Part No.}
+\newcommand{\kundenartnr} {Your Part No.}
 \newcommand{\bild} {Picture}
 \newcommand{\keinbild} {n/a}
 \newcommand{\menge} {Qty}
@@ -41,6 +42,7 @@
 \newcommand{\zahlung} {Payment terms:}
 \newcommand{\lieferung} {Delivery terms:}
 \newcommand{\textTelefon} {Tel.:}
+\newcommand{\textEmail} {Email:}
 \newcommand{\textFax} {Fax:}
 
 % angebot (sales_quotion)
@@ -50,7 +52,9 @@
 \newcommand{\angebotgueltig} {This offer is valid until}               %Danach wird das Datum eingefügt, falls das grammatisch nicht funktionieren sollte müssen wir eine ausnahme für die sprache definieren
 \newcommand{\angebotfragen} {If you have any questions do not hesitate to conatct us.}
 \newcommand{\angebotagb} {Our general terms and conditions (AGB) apply. We will send them to you on request.}
-
+\newcommand{\auftragerteilt}{Order confirmed:}
+\newcommand{\angebotortdatum}{We hereby accept this offer.}
+\newcommand{\abweichendeLieferadresse}{alternate delivery address}
 
 % auftragbestätigung (sales_order)
 \newcommand{\auftragsbestaetigung} {Order}
 \newcommand{\lieferungErfolgtAm} {Your items will be delivered on:} %Danach wird das Datum eingefügt, falls das grammatisch nicht funktionieren sollte müssen wir eine ausnahme für die sprache definieren
 \newcommand{\auftragpruefen} {Please check that all items correspond to your order. Please tell us of any deviations within 3 days.}
 \newcommand{\proformarechnung} {Proforma invoice}
+\newcommand{\nurort} {Place}
+\newcommand{\den} {Date}
+\newcommand{\unterschrift} {Signature}
+\newcommand{\stempel} {Company stamp}
+\newcommand{\optional}{Optional position by arrangement}
 
 % lieferschein (sales_delivery_order)
 \newcommand{\lieferschein} {Delivery order}
index a98907c..bc29d17 100644 (file)
Binary files a/templates/print/RB/firma/briefkopf.png and b/templates/print/RB/firma/briefkopf.png differ
diff --git a/templates/print/RB/firma/chf_account.tex b/templates/print/RB/firma/chf_account.tex
new file mode 100644 (file)
index 0000000..00c78b7
--- /dev/null
@@ -0,0 +1,6 @@
+\newcommand{\currency}{CHF}
+\newcommand{\kontonummer}{4004 283 800}
+\newcommand{\bank}{GLS Bank eG}
+\newcommand{\bankleitzahl}{430 609 67}
+\newcommand{\bic}{DE87430609674004283800}
+\newcommand{\iban}{GENODEM1GLS}
index 0ee9060..fbb6c45 100644 (file)
@@ -1,8 +1,8 @@
 \newcommand{\telefon} {++49 228 92 98 2012}
 \newcommand{\fax} {}
-\newcommand{\firma} {Richardson \& Büren GmbH}
-\newcommand{\strasse} {Römerstr. 45 - 47}
-\newcommand{\ort} {53111 Bonn}
+\newcommand{\firma} {kivitendo GmbH}
+\newcommand{\strasse} {Kölnstr. 311}
+\newcommand{\ort} {53117 Bonn}
 \newcommand{\ustid} {DE292363254}
 \newcommand{\finanzamt} {Finanzamt Bonn-Innenstadt}
 \newcommand{\email} {information@kivitendo-premium.de}
diff --git a/templates/print/RB/ic_supply.tex b/templates/print/RB/ic_supply.tex
new file mode 100644 (file)
index 0000000..1195e70
--- /dev/null
@@ -0,0 +1,73 @@
+\input{inheaders.tex}
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+
+\usepackage{ulem}
+\usepackage{hyperref}
+\usepackage{xstring}
+\begin{document}
+
+\ourfont
+
+\begin{Form}
+\large
+Bestätigung über das Gelangen des Gegenstands einer innergemeinschaftlichen Lieferung
+in einen anderen EU-Mitgliedstaat (Gelangensbestätigung)
+\vspace{0.4cm}
+
+{\color{purple} Bitte unterschreiben und faxen/mailen an:
+  \begin{center} <%employee_fax%> / <%employee_email%> \end{center}}
+\normalsize
+\vspace{0.4cm}
+<%name%>, <%street%>, <%zipcode%> <%city%>, <%country%>\hspace*{\fill}\\
+\TextField[name=department, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(Name und Anschrift des Abnehmers der innergemeinschaftlichen Lieferung, ggf. E-Mail-Adresse)}
+
+\vspace{0.4cm}
+
+Hiermit bestätige ich als Abnehmer, dass ich folgenden Gegenstand / dass folgender Gegenstand \textsuperscript{1)} einer
+innergemeinschaftlichen Lieferung\\
+
+
+\TextField[name=qty, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(Menge des Gegenstands der Lieferung)}\\
+
+\TextField[name=desc, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(handelsübliche Bezeichnung, bei Fahrzeugen zusätzlich die Fahrzeug-Identifikationsnummer)}\\
+
+im\\
+
+\uline{ \StrGobbleLeft{<%reqdate%>}{3} \hspace*{\fill}}\\
+{\color{gray}(Monat und Jahr des Erhalts des Liefergegenstands im Mitgliedstaat, in den der Liefergegenstand gelangt ist, wenn der liefernde Unternehmer den Liefergegenstand befördert oder versendet hat oder wenn der Abnehmer den Liefergegenstand versendet hat)}\\
+
+
+\TextField[name=delivery, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(Monat und Jahr des Endes der Beförderung, wenn der Abnehmer den Liefergegenstand selbst befördert hat)}\\
+
+in / nach \textsuperscript{1)}\\
+
+
+\uline{<%country%>\hspace*{\fill}}\\
+{\color{gray}(Mitgliedstaat und Ort, wohin der Liefergegenstand im Rahmen einer Beförderung oder Versendung gelangt ist)}\\
+
+erhalten habe / gelangt ist.
+
+\TextField[name=delivery, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(Datum der Ausstellung der Bestätigung)}\\
+
+\uline{\hspace*{\fill}}
+
+{\color{gray}(Unterschrift des Abnehmers oder seines Vertretungsberechtigten sowie Name des Unterzeichnenden in Druckschrift)}\\
+
+\textbf{\textsuperscript{1)} Nichtzutreffendes bitte streichen.}
+\end{Form}
+\end{document}
+
diff --git a/templates/print/RB/ic_supply_EN.tex b/templates/print/RB/ic_supply_EN.tex
new file mode 100644 (file)
index 0000000..ac12cf4
--- /dev/null
@@ -0,0 +1,70 @@
+\input{inheaders.tex}
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+
+\usepackage{ulem}
+\usepackage{hyperref}
+\usepackage{xstring}
+\begin{document}
+
+\ourfont
+
+\begin{Form}
+\large
+Certification of the entry of the object of an intra-Community supply into another EU Member State (Entry Certificate)
+
+\vspace{0.4cm}
+
+{\color{purple} Please sign below and send back to fax-number/mail-address:
+  \begin{center} <%employee_fax%> / <%employee_email%> \end{center}}
+
+\normalsize
+\vspace{0.4cm}
+<%name%>, <%street%>, <%zipcode%> <%city%>, <%country%>\hspace*{\fill}\\
+\TextField[name=department, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(Name and address of the customer of the intra-Community supply, e-mail address if applicable)}
+
+
+\vspace{0.4cm}
+I as the customer hereby certify my receipt / the entry \textsuperscript{1)} of the following object of an intra-Community supply\\
+
+\TextField[name=qty, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(Quantity of the object of the supply)}\\
+
+\TextField[name=desc, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(Standard commercial description – in the case of vehicles, including vehicle identification number)}\\
+
+in\\
+
+\uline{ \StrGobbleLeft{<%reqdate%>}{3} \hspace*{\fill}}\\
+{\color{gray}(Month and year the object of the supply was received in the Member State of entry if the supplying trader transported or dispatched the object of the supply or if the customer dispatched the object of the supply)}\\
+
+\TextField[name=delivery, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(Month and year the transportation ended if the customer transported the object of the supply himself or herself)}\\
+
+in / at \textsuperscript{1)}\\
+
+\uline{<%country%>\hspace*{\fill}}\\
+{\color{gray}(Member State and place of entry as part of the transport or dispatch of the object)}\\
+
+
+% X\TextField[name=delivery, bordercolor=gray, width=\linewidth]{}\\
+\uline{X\hspace*{\fill}}\\
+{\color{gray}(Date of issue of the certificate)}\\
+
+\uline{X\hspace*{\fill}}\\
+{\color{gray}(Signature of the customer or of the authorised representative as well as the signatory’s name in capitals)}\\
+
+\textbf{\textsuperscript{1)} Delete as appropriate.}
+
+\end{Form}
+\end{document}
+
index 4eaad87..19954e7 100644 (file)
@@ -10,8 +10,8 @@
 % Hat man mehrere Mandanten muß man statt "Firma1" den Datenbanknamen seines
 % Mandanten eingeben.
 
-\IfSubStringInString{Firma1}{\lxtitlebar}{\newcommand{\identpath}{firma1}}{
-  \IfSubStringInString{Firma2}{\lxtitlebar}{\newcommand{\identpath}{firma2}}
+\IfSubStringInString{Firma1}{\kivicompany}{\newcommand{\identpath}{firma1}}{
+  \IfSubStringInString{Firma2}{\kivicompany}{\newcommand{\identpath}{firma2}}
     {\newcommand{\identpath}{firma}} % sonst
 } % Ende Firma1
 
 
 % Währungen/Konten
 \IfSubStringInString{USD}{\lxcurrency}{\input{\identpath/usd_account.tex}}{
-  \IfSubStringInString{EUR}{\lxcurrency}{\input{\identpath/euro_account.tex}}{\input{\identpath/euro_account.tex}}
+  \IfSubStringInString{CHF}{\lxcurrency}{\input{\identpath/chf_account.tex}}{
+    \IfSubStringInString{EUR}{\lxcurrency}{\input{\identpath/euro_account.tex}}{\input{\identpath/euro_account.tex}}
+  } % Ende CHF
 } % Ende USD
 
 % Briefkopf, Logo oder Briefpapier
-%% \IfSubStringInString{mail}{\lxmedia}{    % nur bei Mail
-  % Grafik als Briefkopf
-  %%\setlength{\wpYoffset}{380pt} % Verschiebung von der Mitte nach oben
+%%\IfSubStringInString{mail}{\lxmedia}{    % nur bei Mail
+  % Nur ein Logo oben rechts
   \setlength{\wpYoffset}{130mm} % Verschiebung von der Mitte nach oben
-  \CenterWallPaper{0.75}{\identpath/briefkopf.png} % mit Skalierung
-
-  % oder nur ein Logo oben rechts
-  %% \setlength{\wpXoffset}{180pt} % Verschiebung von der Mitte nach rechts
-  %% \setlength{\wpYoffset}{380pt} % Verschiebung von der Mitte nach oben
-  %% \CenterWallPaper{0.1}{\identpath/logo.png} % mit Skalierung
-
+  \setlength{\wpXoffset}{-68mm} % Verschiebung von der Mitte nach rechts
+  \CenterWallPaper{0.2}{\identpath/briefkopf.png} % mit Skalierung
   % oder ganzer Briefbogen als Hintergrund
   %% \CenterWallPaper{1}{\identpath/briefbogen.pdf}
-%% }
+%%} % Mail-Ende
 
 
 % keine Absätze nach rechts einrücken
 \geometry{
         a4paper,      % DINA4
         %% left=19mm,    % Linker Rand
-        %% width=142mm,  % Textbreite
-        top=46mm,     % Abstand Textanfang von oben
-        head=5mm,     % Höhe des Kopfes
-        headsep=12mm, % Abstand Kopf zu Textanfang
-        bottom=25mm,  % Abstand von unten
-        %% showframe,    % Rahmen zum Debuggen anzeigen
+        width=182mm,  % Textbreite
+        top=39mm,     % Abstand Textanfang von oben
+        head=44mm,     % Höhe des Kopfes
+        headsep=4mm, % Abstand Kopf zu Textanfang
+        bottom=30mm,  % Abstand von unten
+        % showframe,    % Rahmen zum Debuggen anzeigen
 }
 
 
index b4a5ec9..a167dc6 100644 (file)
@@ -5,7 +5,7 @@
 <tr valign=bottom>
   <td width=10>&nbsp;</td>
   <td>
-  
+
   <table width=100%>
   <tr>
     <td>
 
 
   <table width=100% callspacing=0 cellpadding=0>
-    
+
   <tr>
     <td align=right>
     <table>
     <tr>
       <th align=right>Ausgestellt am</th><td width=10>&nbsp;</td><td><%invdate%></td>
     </tr>
-  
+
     <tr>
       <th align=right>Bezahlbar bis</th><td width=10>&nbsp;</td><td><%duedate%></td>
     </tr>
@@ -48,7 +48,7 @@
     <tr>
       <th align=right>Nummer</th><td>&nbsp;</td><td><%invnumber%></td></tr>
     </tr>
-  
+
     <tr>
       <th align=right>Lieferdatum</th><td>&nbsp;</td><td><%deliverydate%></td></tr>
     </tr>
@@ -85,7 +85,7 @@
       <br><%city%>
       <br><%country%>
       </td>
-      
+
       <td><%shiptoname%>
       <br><%shiptostreet%>
       <br><%shiptozipcode%>
   <tr>
     <td>&nbsp;</td>
   </tr>
-  
+
   <tr>
     <td>
     <table width=100%>
@@ -139,7 +139,7 @@ netprice = linetotal/qty
     <tr>
       <td colspan=7><hr noshade></td>
     </tr>
-    
+
 <%if taxincluded%>
     <tr>
       <th colspan=5 align=right>Total</th>
@@ -160,6 +160,13 @@ netprice = linetotal/qty
     </tr>
 <%end tax%>
 
+<%if rounding%>
+    <tr>
+      <th colspan=5 align=right>Rundung</th>
+      <td colspan=2 align=right><%rounding%></td>
+    </tr>
+<%end rounding%>
+
 <%if paid%>
     <tr>
       <th colspan=5 align=right>Bezahlt</th>
@@ -207,7 +214,7 @@ netprice = linetotal/qty
 </tr>
 
 <tr><td>&nbsp;</td></tr>
-  
+
 <tr>
   <td>
   <table width=100%>
index 78d63b4..1435e73 100644 (file)
@@ -5,7 +5,7 @@
 \newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
 \newcommand{\lxmedia} {<%media%>}
 \newcommand{\lxcurrency} {<%currency%>}
-\newcommand{\lxtitlebar} {<%titlebar%>}
+\newcommand{\kivicompany} {<%employee_company%>}
 
 % settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
 \input{insettings.tex}
 \begin{document}
 
 \ourfont
-
 \begin{minipage}[t]{8cm}
-  \vspace*{1.0cm}
+  \scriptsize
+
+  {\color{gray}\underline{\firma\ $\cdot$ \strasse\ $\cdot$ \ort}}
+  \normalsize
+
+  \vspace*{0.3cm}
 
   <%name%>
 
-  <%department_1%>
+  <%if department_1%><%department_1%><%end if%>
 
-  <%department_2%>
+  <%if department_2%><%department_2%><%end if%>
 
   <%cp_givenname%> <%cp_name%>
 
 
   \ansprechpartner:\hfill <%employee_name%>
 
-  <%if globalprojectnumber%> \projektnummer:\hfill <%globalprojectnumber%> <%end globalprojectnumber%>
+  \textTelefon \hfill <%employee_tel%>
+
+  \textEmail \hfill <%employee_email%>
 \end{minipage}
+<%if shiptoname%>
+  \vspace{0.8cm}
+  \scriptsize \underline{\abweichendeLieferadresse:}\\
+  \normalsize    <%shiptoname%>
+
+                 <%if shiptocontact%> <%shiptocontact%><%end if%>
+
+                 <%shiptodepartment_1%>
+
+                  <%shiptodepartment_2%>
+
+                  <%shiptostreet%>
+
+                  <%shiptozipcode%> <%shiptocity%>
+<%end if%>
 
 \vspace*{1.5cm}
 
 \hfill
 
-% Anrede nach Geschlecht unterscheiden
-\ifthenelse{\equal{<%cp_name%>}{}}{\anrede}{
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\ifthenelse{\equal{<%cp_name%>}{}}{
+  <%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}{
   \ifthenelse{\equal{<%cp_gender%>}{f}}
     {\anredefrau}{\anredeherr} <%cp_title%> <%cp_name%>,}\\
 %\rechnungsformel\\
           <%if serialnumber%> && \scriptsize \seriennummer: <%serialnumber%>\\<%end serialnumber%>
           <%if ean%> && \scriptsize \ean: <%ean%>\\<%end ean%>
           <%if projectnumber%> && \scriptsize \projektnummer: <%projectnumber%>\\<%end projectnumber%>
+          <%if customer_make%>
+            <%foreach customer_make%>
+              \ifthenelse{\equal{<%customer_make%>}{<%name%>}}{&& \kundenartnr: <%customer_model%>\\}{}
+            <%end foreach%>
+          <%end if%>
 
           \\[-0.8em]
 <%end number%>
   <%employee_name%>
 
 \end{document}
-
index f783eca..df1bcde 100644 (file)
@@ -1,17 +1,22 @@
+% config: use-template-toolkit=1
+% config: tag-style=$( )$
+$( USE KiviLatex )$
+$( USE P )$
+$( SET customer = letter.customer_vendor )$
 \input{inheaders.tex}
-
+$( KiviLatex.required_packages_for_html )$
 
 % Variablen, die in settings verwendet werden
-\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
-\newcommand{\lxmedia} {<%media%>}
-\newcommand{\lxcurrency} {<%currency%>}
-\newcommand{\lxtitlebar} {<%titlebar%>}
+\newcommand{\lxlangcode}{$(template_meta.language.template_code)$}
+\newcommand{\lxmedia}{$(template_meta.media)$}
+\newcommand{\lxcurrency}{}
+\newcommand{\kivicompany}{$(employee_company)$}
 
 % settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
 \input{insettings.tex}
 
 % laufende Kopfzeile:
-%\ourhead{}{}{<%subject%>}{<%letternumber%>}{<%date%>}
+%\ourhead{}{}{$( KiviLatex.filter(letter.subject) )$}{$( KiviLatex.filter(letter.letternumber) )$}{$( KiviLatex.filter(letter.date.to_kivitendo) )$}
 \ourhead{}{}{}{}{}
 
 \begin{document}
 
 \begin{minipage}{14cm}
 
-  <%name%>
+  $( KiviLatex.filter(customer.name) )$
 
-  <%contact_formal%>
+  $( KiviLatex.filter(letter.contact.formal_greeting) )$
 
-  <%street%>
+  $( KiviLatex.filter(customer.street) )$
 
-  <%zipcode%> <%city%>
+  $( KiviLatex.filter(customer.zipcode) )$ $( KiviLatex.filter(customer.city) )$
 
-  <%country%>
+  $( KiviLatex.filter(customer.country) )$
 
 \end{minipage}
 
 \vspace{2.5cm}
-\hfill<%date%>
+\hfill $( KiviLatex.filter(letter.date.to_kivitendo) )$
 
-<%if reference%>
-\textbf{\ihrzeichen : <%reference%>}
-<%end if%>
+$( IF letter.reference )$
+\textbf{\ihrzeichen : $( KiviLatex.filter(letter.reference) )$}
 
 \vspace{1cm}
+$( END )$
 
-<%if subject%>
-\textbf{\betreff : <%subject%>}
-<%end if%>
+$( IF letter.subject )$
+\textbf{\betreff : $( KiviLatex.filter(letter.subject) )$}
 
 \vspace{1cm}
+$( END )$
 
-  <%greeting%>
+$( KiviLatex.filter(letter.greeting) )$
 
 \vspace{0.5cm}
 
-  <%body%>
+$( KiviLatex.filter_html(letter.body) )$
 
 \vspace{0.5cm}
 
-  <%close%>
-
-  <%company_name%>
-
-\vspace*{0.5cm}
-
 \begin{minipage}{6cm}
 
-\textbf{<%employee_name%>}
-
-<%employee_position%>
+\textbf{$( KiviLatex.filter(letter.employee.name) )$}
 
 \end{minipage}
 \begin{minipage}{6cm}
 
-\textbf{<%salesman_name%>}
-
-<%salesman_position%>
+\textbf{$( KiviLatex.filter(letter.salesman.name) )$}
 
 \end{minipage}
 
 \end{document}
-
index 84f2ac5..c114e5c 100644 (file)
@@ -5,7 +5,7 @@
 \newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
 \newcommand{\lxmedia} {<%media%>}
 \newcommand{\lxcurrency} {<%currency%>}
-\newcommand{\lxtitlebar} {<%titlebar%>}
+\newcommand{\kivicompany} {<%employee_company%>}
 
 % settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
 \input{insettings.tex}
 \begin{document}
 
 \ourfont
-
 \begin{minipage}[t]{8cm}
-  \vspace*{1.0cm}
+  \scriptsize
+
+  {\color{gray}\underline{\firma\ $\cdot$ \strasse\ $\cdot$ \ort}}
+  \normalsize
+
+  \vspace*{0.3cm}
 
   <%name%>
 
+  <%if department_1%><%department_1%><%end if%>
+
+  <%if department_2%><%department_2%><%end if%>
+
+  <%cp_givenname%> <%cp_name%>
+
   <%street%>
 
   ~
@@ -42,7 +52,7 @@
 
   \vspace*{0.2cm}
 
-  \datum:\hfill <%invdate%>
+  \datum:\hfill <%orddate%>
 
   \kundennummer:\hfill <%customernumber%>
 
 
   \ansprechpartner:\hfill <%employee_name%>
 
-  <%if globalprojectnumber%> \projektnummer:\hfill <%globalprojectnumber%> <%end globalprojectnumber%>
+  \textTelefon \hfill <%employee_tel%>
+
+  \textEmail \hfill <%employee_email%>
 \end{minipage}
 
+<%if shiptoname%>
+  \vspace{0.8cm}
+  \scriptsize \underline{\abweichendeLieferadresse:}\\
+  \normalsize    <%shiptoname%>
+
+                 <%if shiptocontact%> <%shiptocontact%><%end if%>
+
+                 <%shiptodepartment_1%>
+
+                  <%shiptodepartment_2%>
+
+                  <%shiptostreet%>
+
+                  <%shiptozipcode%> <%shiptocity%>
+<%end if%>
 \vspace*{1.5cm}
 
 \hfill
 
-% Anrede nach Geschlecht unterscheiden
-\ifthenelse{\equal{<%cp_name%>}{}}{\anrede}{
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\ifthenelse{\equal{<%cp_name%>}{}}{
+  <%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}{
   \ifthenelse{\equal{<%cp_gender%>}{f}}
     {\anredefrau}{\anredeherr} <%cp_title%> <%cp_name%>,}\\
 
index 09833b8..4269636 100644 (file)
@@ -5,7 +5,7 @@
 \newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
 \newcommand{\lxmedia} {<%media%>}
 \newcommand{\lxcurrency} {<%currency%>}
-\newcommand{\lxtitlebar} {<%titlebar%>}
+\newcommand{\kivicompany} {<%employee_company%>}
 
 % settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
 \input{insettings.tex}
 \begin{document}
 
 \ourfont
+\begin{minipage}[t]{8cm}
+  \scriptsize
+
+  {\color{gray}\underline{\firma\ $\cdot$ \strasse\ $\cdot$ \ort}}
+  \normalsize
+
+  \vspace*{0.3cm}
 
-\begin{minipage}{8cm}
   <%name%>
 
+  <%if department_1%><%department_1%><%end if%>
+
+  <%if department_2%><%department_2%><%end if%>
+
+  <%cp_givenname%> <%cp_name%>
+
   <%street%>
 
+  ~
+
   <%zipcode%> <%city%>
 
   <%country%>
   <%if globalprojectnumber%> \projektnummer:\hfill <%globalprojectnumber%> <%end globalprojectnumber%>
 \end{minipage}
 
+<%if shiptoname%>
+  \vspace{0.8cm}
+  \scriptsize \underline{\abweichendeLieferadresse:}\\
+  \normalsize    <%shiptoname%>
+
+                 <%if shiptocontact%> <%shiptocontact%><%end if%>
+
+                 <%shiptodepartment_1%>
+
+                  <%shiptodepartment_2%>
+
+                  <%shiptostreet%>
+
+                  <%shiptozipcode%> <%shiptocity%>
+<%end if%>
 \vspace*{1.5cm}
 
 
           <%if reqdate%> && \scriptsize \lieferdatum: <%reqdate%>\\<%end reqdate%>
           <%if serialnumber%> && \scriptsize \seriennummer: <%serialnumber%>\\<%end serialnumber%>
           <%if ean%> && \scriptsize \ean: <%ean%>\\<%end ean%>
-          <%foreach si_number%><%if si_number%> && \scriptsize \charge: <%si_chargenumber%> <%if si_bestbefore%> \mhd: <%si_bestbefore%><%end if%> <%si_qty%>~<%si_unit%><%end si_chargenumber%>\\<%end si_number%>
+          <%foreach si_number%><%if si_chargenumber%> && \scriptsize \charge: <%si_chargenumber%> <%if si_bestbefore%> \mhd: <%si_bestbefore%><%end if%> <%si_qty%>~<%si_unit%><%end si_chargenumber%>\\<%end si_number%>
 
           \\[-0.8em]
 <%end number%>
 <%end delivery_term%>
 
 \end{document}
-
index bd820ee..97cb004 100644 (file)
@@ -5,7 +5,7 @@
 \newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
 \newcommand{\lxmedia} {<%media%>}
 \newcommand{\lxcurrency} {<%currency%>}
-\newcommand{\lxtitlebar} {<%titlebar%>}
+\newcommand{\kivicompany} {<%employee_company%>}
 
 % settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
 \input{insettings.tex}
 \begin{document}
 
 \ourfont
+\begin{minipage}[t]{8cm}
+  \scriptsize
 
-\begin{minipage}{8cm}
-  \vspace*{1.0cm}
+  {\color{gray}\underline{\firma\ $\cdot$ \strasse\ $\cdot$ \ort}}
+  \normalsize
+
+  \vspace*{0.3cm}
 
   <%name%>
 
+  <%if department_1%><%department_1%><%end if%>
+
+  <%if department_2%><%department_2%><%end if%>
+
+  <%cp_givenname%> <%cp_name%>
+
   <%street%>
 
+  ~
+
   <%zipcode%> <%city%>
 
   <%country%>
 
   \ansprechpartner:\hfill <%employee_name%>
 
-  <%if globalprojectnumber%> \projektnummer:\hfill <%globalprojectnumber%> <%end globalprojectnumber%>
+  \textTelefon \hfill <%employee_tel%>
+
+  \textEmail \hfill <%employee_email%>
 \end{minipage}
 
+<%if shiptoname%>
+  \vspace{0.8cm}
+  \scriptsize \underline{\abweichendeLieferadresse:}\\
+  \normalsize    <%shiptoname%>
+
+                 <%if shiptocontact%> <%shiptocontact%><%end if%>
+
+                 <%shiptodepartment_1%>
+
+                  <%shiptodepartment_2%>
+
+                  <%shiptostreet%>
+
+                  <%shiptozipcode%> <%shiptocity%>
+<%end if%>
 \vspace*{1.5cm}
 
 \hfill
 
-% Anrede nach Geschlecht unterscheiden
-\ifthenelse{\equal{<%cp_name%>}{}}{\anrede}{
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\ifthenelse{\equal{<%cp_name%>}{}}{
+  <%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}{
   \ifthenelse{\equal{<%cp_gender%>}{f}}
     {\anredefrau}{\anredeherr} <%cp_title%> <%cp_name%>,}\\
 
           <%if serialnumber%> && \scriptsize \seriennummer: <%serialnumber%>\\<%end serialnumber%>
           <%if ean%> && \scriptsize \ean: <%ean%>\\<%end ean%>
           <%if projectnumber%> && \scriptsize \projektnummer: <%projectnumber%>\\<%end projectnumber%>
-
           <%if make%>
             <%foreach make%>
               \ifthenelse{\equal{<%make%>}{<%name%>}}{&& \artikelnummer: <%model%>\\}{}
             <%end foreach%>
           <%end if%>
-
           \\[-0.8em]
 <%end number%>
 
   <%employee_name%>
 
 \end{document}
-
index 5e02682..80b426b 100644 (file)
@@ -5,7 +5,7 @@
 \newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
 \newcommand{\lxmedia} {<%media%>}
 \newcommand{\lxcurrency} {<%currency%>}
-\newcommand{\lxtitlebar} {<%titlebar%>}
+\newcommand{\kivicompany} {<%employee_company%>}
 
 % settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
 \input{insettings.tex}
 \begin{document}
 
 \ourfont
+\begin{minipage}[t]{8cm}
+  \scriptsize
+
+  {\color{gray}\underline{\firma\ $\cdot$ \strasse\ $\cdot$ \ort}}
+  \normalsize
+
+  \vspace*{0.3cm}
 
-\begin{minipage}{8cm}
   <%name%>
 
+  <%if department_1%><%department_1%><%end if%>
+
+  <%if department_2%><%department_2%><%end if%>
+
+  <%cp_givenname%> <%cp_name%>
+
   <%street%>
 
+  ~
+
   <%zipcode%> <%city%>
 
   <%country%>
 
 \hfill
 
-% Anrede nach Geschlecht unterscheiden
-\ifthenelse{\equal{<%cp_name%>}{}}{\anrede}{
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\ifthenelse{\equal{<%cp_name%>}{}}{
+  <%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}{
   \ifthenelse{\equal{<%cp_gender%>}{f}}
     {\anredefrau}{\anredeherr} <%cp_title%> <%cp_name%>,}\\
 
@@ -65,7 +81,7 @@
 %
 \setlength\LTleft\parindent     % Tabelle beginnt am linken Textrand
 \setlength\LTright{0pt}         % Tabelle endet am rechten Textrand
-\begin{longtable}{@{}rrp{10.7cm}@{\extracolsep{\fill}}@{}}
+\begin{longtable}{@{}rrp{14cm}@{\extracolsep{\fill}}@{}}
 % Tabellenkopf
 \hline
 \textbf{\position} & \textbf{\menge} & \textbf{\bezeichnung} \\
   <%employee_name%>
 
 \end{document}
-
index 84ebec0..1d22e9c 100644 (file)
@@ -145,7 +145,7 @@ $( PROCESS text_block_outputter output_position=0 heading='Allgemeines' )$
 \begin{longtable}{p{2.8cm}p{11.7cm}}
   Funktionsblock & $( KiviLatex.filter(item.fb_number) )$\\
   Beschreibung & $( KiviLatex.filter_html(item.description_as_restricted_html) )$\\
-  Abhängigkeiten & $( KiviLatex.filter(P.requirement_spec_item_dependency_list(item)) )$
+  Abhängigkeiten & $( KiviLatex.filter(item.presenter.requirement_spec_item_dependency_list) )$
 \end{longtable}}
 
 %    $( FOREACH sub_item = item.children_sorted )$
@@ -155,7 +155,7 @@ $( PROCESS text_block_outputter output_position=0 heading='Allgemeines' )$
 \begin{longtable}{p{2.8cm}p{11.7cm}}
   Unterfunktionsblock & $( KiviLatex.filter(sub_item.fb_number) )$\\
   Beschreibung & $( KiviLatex.filter_html(sub_item.description_as_restricted_html) )$\\
-  Abhängigkeiten & $( KiviLatex.filter(P.requirement_spec_item_dependency_list(sub_item)) )$
+  Abhängigkeiten & $( KiviLatex.filter(sub_item.presenter.requirement_spec_item_dependency_list) )$
 \end{longtable}}
 
 %    $( END )$
index 681e6ac..fcda41a 100644 (file)
@@ -5,7 +5,7 @@
 \newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
 \newcommand{\lxmedia} {<%media%>}
 \newcommand{\lxcurrency} {<%currency%>}
-\newcommand{\lxtitlebar} {<%titlebar%>}
+\newcommand{\kivicompany} {<%employee_company%>}
 
 % settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
 \input{insettings.tex}
 \ourfont
 
 \begin{minipage}[t]{8cm}
-  \vspace*{1.0cm}
+  \scriptsize
+  {\color{gray}\underline{\firma\ $\cdot$ \strasse\ $\cdot$ \ort}}
+  \normalsize
+
+  \vspace*{0.3cm}
   \ifthenelse{\equal{<%shiptoname%>}{}}{ % KEINE ABWEICHENDE LIEFERADRESSE
 
     <%name%>
 
   \ansprechpartner:\hfill <%employee_name%>
 
+  \textTelefon \hfill <%employee_tel%>
+
+  \textEmail \hfill <%employee_email%>
+
   <%if globalprojectnumber%> \projektnummer:\hfill <%globalprojectnumber%> <%end globalprojectnumber%>
 \end{minipage}
 
           <%if serialnumber%> && \scriptsize \seriennummer: <%serialnumber%>\\<%end serialnumber%>
           <%if ean%> && \scriptsize \ean: <%ean%>\\<%end ean%>
           <%if projectnumber%> && \scriptsize \projektnummer: <%projectnumber%>\\<%end projectnumber%>
-          <%foreach si_number%><%if si_number%> && \scriptsize \charge: <%si_chargenumber%> <%if si_bestbefore%> \mhd: <%si_bestbefore%><%end if%> <%si_qty%>~<%si_unit%><%end si_chargenumber%>\\<%end si_number%>
+          <%if customer_make%>
+            <%foreach customer_make%>
+              \ifthenelse{\equal{<%customer_make%>}{<%name%>}}{&& \kundenartnr: <%customer_model%>\\}{}
+            <%end foreach%>
+          <%end if%>
+          <%foreach si_number%><%if si_chargenumber%> && \scriptsize \charge: <%si_chargenumber%> <%if si_bestbefore%> \mhd: <%si_bestbefore%><%end if%> <%si_qty%>~<%si_unit%><%end si_chargenumber%>\\<%end si_number%>
 
           \\[-0.8em]
 <%end number%>
 <%end delivery_term%>
 
 \end{document}
-
index 4cbe20a..260d1ed 100644 (file)
@@ -5,7 +5,7 @@
 <tr valign=bottom>
   <td width=10>&nbsp;</td>
   <td>
-  
+
   <table width=100%>
   <tr>
     <td>
 
 
   <table width=100% callspacing=0 cellpadding=0>
-    
+
   <tr>
     <td align=right>
     <table>
     <tr>
       <th align=right>Bestelldatum</th><td width=10>&nbsp;</td><td><%orddate%></td>
     </tr>
-  
+
     <tr>
       <th align=right>Lieferbar bei</th><td width=10>&nbsp;</td><td><%reqdate%></td>
     </tr>
@@ -48,7 +48,7 @@
     <tr>
       <th align=right>Bestellnummer</th><td>&nbsp;</td><td><%ordnumber%></td></tr>
     </tr>
-  
+
     <tr>
       <td>&nbsp;</td>
     </tr>
@@ -86,7 +86,7 @@
   <tr>
     <td>&nbsp;</td>
   </tr>
-  
+
   <tr>
     <td>
     <table width=100%>
@@ -119,7 +119,7 @@ adjust the colspan if you include this to shift subtotal one to the right
     <tr>
       <td colspan=7><hr noshade></td>
     </tr>
-    
+
 <%if taxincluded%>
     <tr>
       <th colspan=5 align=right>Total</th>
@@ -141,6 +141,11 @@ adjust the colspan if you include this to shift subtotal one to the right
     </tr>
 <%end tax%>
 
+<%if rounding%>
+      <th colspan=5 align=right>Rundung</th>
+      <td colspan=2 align=right><%rounding%></td>
+<%end rounding%>
+
     <tr>
       <td colspan=2>&nbsp;</td>
       <td colspan=5><hr noshade></td>
@@ -184,7 +189,7 @@ adjust the colspan if you include this to shift subtotal one to the right
 </tr>
 
 <tr><td>&nbsp;</td></tr>
-  
+
 <tr>
   <td>
   <table width=100%>
index b86f159..dfec85c 100644 (file)
@@ -5,7 +5,7 @@
 \newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
 \newcommand{\lxmedia} {<%media%>}
 \newcommand{\lxcurrency} {<%currency%>}
-\newcommand{\lxtitlebar} {<%titlebar%>}
+\newcommand{\kivicompany} {<%employee_company%>}
 
 % settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
 \input{insettings.tex}
 \begin{document}
 
 \ourfont
-
 \begin{minipage}[t]{8cm}
-  \vspace*{1.0cm}
+  \scriptsize
+
+  {\color{gray}\underline{\firma\ $\cdot$ \strasse\ $\cdot$ \ort}}
+  \normalsize
+
+  \vspace*{0.3cm}
 
   <%name%>
 
+  <%if department_1%><%department_1%><%end if%>
+
+  <%if department_2%><%department_2%><%end if%>
+
+  <%cp_givenname%> <%cp_name%>
+
   <%street%>
 
   ~
 
   \ansprechpartner:\hfill <%employee_name%>
 
-  <%if globalprojectnumber%> \projektnummer:\hfill <%globalprojectnumber%> <%end globalprojectnumber%>
+  \textTelefon \hfill <%employee_tel%>
+
+  \textEmail \hfill <%employee_email%>
 \end{minipage}
 
+<%if shiptoname%>
+  \vspace{0.8cm}
+  \scriptsize \underline{\abweichendeLieferadresse:}\\
+  \normalsize    <%shiptoname%>
+
+                 <%if shiptocontact%> <%shiptocontact%><%end if%>
+
+                 <%shiptodepartment_1%>
+
+                  <%shiptodepartment_2%>
+
+                  <%shiptostreet%>
+
+                  <%shiptozipcode%> <%shiptocity%>
+<%end if%>
 \vspace*{1.5cm}
 
 \hfill
 
-% Anrede nach Geschlecht unterscheiden
-\ifthenelse{\equal{<%cp_name%>}{}}{\anrede}{
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\ifthenelse{\equal{<%cp_name%>}{}}{
+  <%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}{
   \ifthenelse{\equal{<%cp_gender%>}{f}}
     {\anredefrau}{\anredeherr} <%cp_title%> <%cp_name%>,}\\
 
           <%if serialnumber%> && \scriptsize \seriennummer: <%serialnumber%>\\<%end serialnumber%>
           <%if ean%> && \scriptsize \ean: <%ean%>\\<%end ean%>
           <%if projectnumber%> && \scriptsize \projektnummer: <%projectnumber%>\\<%end projectnumber%>
+          <%if optional%> && \scriptsize \optional \\<%end%>
+          <%if customer_make%>
+            <%foreach customer_make%>
+              \ifthenelse{\equal{<%customer_make%>}{<%name%>}}{&& \kundenartnr: <%customer_model%>\\}{}
+            <%end foreach%>
+          <%end if%>
           \\[-0.8em]
 <%end number%>
 
index 1380631..983d976 100644 (file)
@@ -5,7 +5,7 @@
 <tr valign=bottom>
   <td width=10>&nbsp;</td>
   <td>
-  
+
   <table width=100%>
   <tr valign=top>
     <td>
     <td colspan=2>
       <table width=100% border=1>
         <tr>
-         <th width=17% align=left nowrap>Nummer</th>
-         <th width=17% align=left>Datum</th>
-         <th width=17% align=left>Gültig bis</th>
-         <th width=17% align=left nowrap>Kontakt</th>
-         <th width=17% align=left nowrap>Lagerplatz</th>
-         <th width=15% align=left nowrap>Lieferung mit</th>
-       </tr>
-
-       <tr>
-         <td><%quonumber%></td>
-         <td><%quodate%></td>
-         <td><%reqdate%></td>
-         <td><%employee%></td>
-         <td><%shippingpoint%></td>
-         <td><%shipvia%></td>
-       </tr>
+    <th width=17% align=left nowrap>Nummer</th>
+    <th width=17% align=left>Datum</th>
+    <th width=17% align=left>Gültig bis</th>
+    <th width=17% align=left nowrap>Kontakt</th>
+    <th width=17% align=left nowrap>Lagerplatz</th>
+    <th width=15% align=left nowrap>Lieferung mit</th>
+  </tr>
+
+  <tr>
+    <td><%quonumber%></td>
+    <td><%quodate%></td>
+    <td><%reqdate%></td>
+    <td><%employee%></td>
+    <td><%shippingpoint%></td>
+    <td><%shipvia%></td>
+  </tr>
       </table>
     </td>
   </tr>
     </tr>
 <%end tax%>
 
+<%if rounding%>
+      <th colspan=6 align=right>Rundung</th>
+      <td colspan=2 align=right><%rounding%></td>
+<%end rounding%>
+
     <tr>
       <td colspan=4>&nbsp;</td>
       <td colspan=4><hr noshade></td>
index 8ef6b5b..e8661cf 100644 (file)
@@ -5,7 +5,7 @@
 \newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
 \newcommand{\lxmedia} {<%media%>}
 \newcommand{\lxcurrency} {<%currency%>}
-\newcommand{\lxtitlebar} {<%titlebar%>}
+\newcommand{\kivicompany} {<%employee_company%>}
 
 % settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
 \input{insettings.tex}
 \begin{document}
 
 \ourfont
-
 \begin{minipage}[t]{8cm}
-  \vspace*{1.0cm}
+  \scriptsize
+
+  {\color{gray}\underline{\firma\ $\cdot$ \strasse\ $\cdot$ \ort}}
+  \normalsize
+
+  \vspace*{0.3cm}
 
   <%name%>
 
+  <%if department_1%><%department_1%><%end if%>
+
+  <%if department_2%><%department_2%><%end if%>
+
+  <%cp_givenname%> <%cp_name%>
+
   <%street%>
 
   ~
 
   \ansprechpartner:\hfill <%employee_name%>
 
-  <%if globalprojectnumber%> \projektnummer:\hfill <%globalprojectnumber%> <%end globalprojectnumber%>
+  \textTelefon \hfill <%employee_tel%>
+
+  \textEmail \hfill <%employee_email%>
+
 \end{minipage}
 
+<%if shiptoname%>
+  \vspace{0.8cm}
+  \scriptsize \underline{\abweichendeLieferadresse:}\\
+  \normalsize    <%shiptoname%>
+
+                 <%if shiptocontact%> <%shiptocontact%><%end if%>
+
+                 <%shiptodepartment_1%>
+
+                  <%shiptodepartment_2%>
+
+                  <%shiptostreet%>
+
+                  <%shiptozipcode%> <%shiptocity%>
+<%end if%>
 \vspace*{1.5cm}
 
 \hfill
 
-% Anrede nach Geschlecht unterscheiden
-\ifthenelse{\equal{<%cp_name%>}{}}{\anrede}{
-  \ifthenelse{\equal{<%cp_gender%>}{f}}
-    {\anredefrau}{\anredeherr} <%cp_title%> <%cp_name%>,}\\
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\ifthenelse{\equal{<%cp_name%>}{}}{
+    <%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}{
+    \ifthenelse{\equal{<%cp_gender%>}{f}}
+      {\anredefrau}{\anredeherr} <%cp_title%> <%cp_name%>,}\\
 
 \angebotsformel\\
 
           <%if serialnumber%> && \scriptsize \seriennummer: <%serialnumber%>\\<%end serialnumber%>
           <%if ean%> && \scriptsize \ean: <%ean%>\\<%end ean%>
           <%if projectnumber%> && \scriptsize \projektnummer: <%projectnumber%>\\<%end projectnumber%>
-
+          <%if optional%> && \scriptsize \optional \\<%end%>
+          <%if customer_make%>
+            <%foreach customer_make%>
+              \ifthenelse{\equal{<%customer_make%>}{<%name%>}}{&& \kundenartnr: <%customer_model%>\\}{}
+            <%end foreach%>
+          <%end if%>
           \\[-0.8em]
 <%end number%>
 
 \end{longtable}
 
-
-\vspace{0.2cm}
-
 <%if notes%>
       \vspace{5mm}
-        <%notes%>
-        \vspace{5mm}
+ \vspace{5mm}
+  <%notes%>
+  \vspace{5mm}
 <%end if%>
 
 <%if delivery_term%>
 \angebotagb \\ \\
 
 \gruesse \\ \\ \\
-  <%employee_name%>
+<%employee_company%>
 
-\end{document}
+<%salesman_name%>\\\\
+
+\parbox[t]{175mm}{
 
+  \auftragerteilt\\\\
+  \nurort:\uline{\hspace*{8cm}}\ ,\den\ \uline{\hspace*{5cm}}\\\\\\\\\\
+
+  \unterschrift / \stempel:\uline{\hspace*{6cm}}
+}
+\end{document}
index 4b3610e..7435a17 100644 (file)
@@ -5,7 +5,7 @@
 \newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
 \newcommand{\lxmedia} {<%media%>}
 \newcommand{\lxcurrency} {<%currency%>}
-\newcommand{\lxtitlebar} {<%titlebar%>}
+\newcommand{\kivicompany} {<%employee_company%>}
 
 % settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
 \input{insettings.tex}
 
 \hfill
 
-% Anrede nach Geschlecht unterscheiden
-\ifthenelse{\equal{<%cp_name%>}{}}{\anrede}{
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\ifthenelse{\equal{<%cp_name%>}{}}{
+  <%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}{
   \ifthenelse{\equal{<%cp_gender%>}{f}}
     {\anredefrau}{\anredeherr} <%cp_title%> <%cp_name%>,}\\
 
index 3460007..f125de9 100644 (file)
@@ -5,7 +5,7 @@
 \newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
 \newcommand{\lxmedia} {<%media%>}
 \newcommand{\lxcurrency} {<%currency%>}
-\newcommand{\lxtitlebar} {<%titlebar%>}
+\newcommand{\kivicompany} {<%employee_company%>}
 
 % settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
 \input{insettings.tex}
 \begin{document}
 
 \ourfont
-
 \begin{minipage}[t]{8cm}
-  \vspace*{1.0cm}
+  \scriptsize
+
+  {\color{gray}\underline{\firma\ $\cdot$ \strasse\ $\cdot$ \ort}}
+  \normalsize
+
+  \vspace*{0.3cm}
 
   <%name%>
 
+  <%if department_1%><%department_1%><%end if%>
+
+  <%if department_2%><%department_2%><%end if%>
+
+  <%cp_givenname%> <%cp_name%>
+
   <%street%>
 
   ~
 
   \ansprechpartner:\hfill <%employee_name%>
 
+  \textTelefon \hfill <%employee_tel%>
+
+  \textEmail \hfill <%employee_email%>
 \end{minipage}
 
 \vspace*{1.5cm}
 
 \hfill
 
-% Anrede nach Geschlecht unterscheiden
-\ifthenelse{\equal{<%cp_name%>}{}}{\anrede}{
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\ifthenelse{\equal{<%cp_name%>}{}}{
+  <%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}{
   \ifthenelse{\equal{<%cp_gender%>}{f}}
     {\anredefrau}{\anredeherr} <%cp_title%> <%cp_name%>,}\\
 
   <%employee_name%>
 
 \end{document}
-
index 7470888..b96dd96 100644 (file)
@@ -5,7 +5,7 @@
 \newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
 \newcommand{\lxmedia} {<%media%>}
 \newcommand{\lxcurrency} {<%currency%>}
-\newcommand{\lxtitlebar} {<%titlebar%>}
+\newcommand{\kivicompany} {<%employee_company%>}
 
 % settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
 \input{insettings.tex}
 \begin{document}
 
 \ourfont
-
 \begin{minipage}[t]{8cm}
-  \vspace*{1.0cm}
+  \scriptsize
+
+  {\color{gray}\underline{\firma\ $\cdot$ \strasse\ $\cdot$ \ort}}
+  \normalsize
+
+  \vspace*{0.3cm}
 
   <%name%>
 
+  <%if department_1%><%department_1%><%end if%>
+
+  <%if department_2%><%department_2%><%end if%>
+
+  <%cp_givenname%> <%cp_name%>
+
   <%street%>
 
   ~
 
 \hfill
 
-% Anrede nach Geschlecht unterscheiden
-\ifthenelse{\equal{<%cp_name%>}{}}{\anrede}{
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\ifthenelse{\equal{<%cp_name%>}{}}{
+  <%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}{
   \ifthenelse{\equal{<%cp_gender%>}{f}}
     {\anredefrau}{\anredeherr} <%cp_title%> <%cp_name%>,}\\
 
@@ -111,4 +123,3 @@ Zinsen & <%interest%> \currency \\
   <%employee_name%>
 
 \end{document}
-
index 233f192..2eca9eb 120000 (symlink)
@@ -1 +1 @@
-RB
\ No newline at end of file
+marei/
\ No newline at end of file
diff --git a/templates/print/f-tex/bin_list.html b/templates/print/f-tex/bin_list.html
deleted file mode 100644 (file)
index d57632d..0000000
+++ /dev/null
@@ -1,180 +0,0 @@
-<body bgcolor=ffffff>
-
-<table width=100%>
-  <tr>
-    <td width=10>&nbsp;</td>
-    
-    <td>
-      <table width=100%>
-       <tr>
-         <td>
-           <h4>
-           <%company%>
-           <br><%address%>
-           </h4>
-         </td>
-         
-         <th><img src=http://localhost/lx-erp/lx-office-erp.png border=0 width=64 height=58></th>
-
-         <th align=right>
-           <h4>
-           Tel: <%tel%>
-           <br>Fax: <%fax%>
-           </h4>
-         </td>
-       </tr>
-       
-       <tr>
-         <th colspan=3>
-           <h4>L A G E R L I S T E</h4>
-         </th>
-       </tr>
-      </table>
-    </td>
-  </tr>
-
-  <tr>
-    <td>&nbsp;</td>
-
-    <td>
-      <table width=100% cellspacing=0 cellpadding=0>
-       <tr bgcolor=000000>
-         <th align=left width=50%><font color=ffffff>Absender</th>
-         <th align=left width=50%><font color=ffffff>Lieferanschrift</th>
-       </tr>
-
-       <tr valign=top>
-         <td><%name%>
-         <br><%street%>
-         <br><%zipcode%>
-         <br><%city%>
-         <br><%country%>
-         <br>
-
-         <%if contact%>
-         <br>Kontakt: <%contact%>
-         <%end contact%>
-
-         <%if vendorphone%>
-         <br>Tel: <%vendorphone%>
-         <%end vendorphone%>
-
-         <%if vendorfax%>
-         <br>Fax: <%vendorfax%>
-         <%end vendorfax%>
-
-         <%if email%>
-         <br><%email%>
-         <%end email%>
-         
-         </td>
-         
-         <td><%shiptoname%>
-         <br><%shiptostreet%>
-         <br><%shiptozipcode%>
-         <br><%shiptocity%>
-         <br><%shiptocountry%>
-
-         <br>
-         <%if shiptocontact%>
-         <br>Kontakt: <%shiptocontact%>
-         <%end shiptocontact%>
-         
-         <%if shiptophone%>
-         <br>Tel: <%shiptophone%>
-         <%end shiptophone%>
-
-         <%if shiptofax%>
-         <br>Fax: <%shiptofax%>
-         <%end shiptofax%>
-         </td>
-       </tr>
-      </table>
-    </td>
-  </tr>
-
-  <tr height=5></tr>
-
-  <tr>
-    <td>&nbsp;</td>
-
-    <td>
-      <table width=100% border=1>
-       <tr>
-         <th width=17% align=left nowrap>BestellNr. #</th>
-         <th width=17% align=left nowrap>Datum</th>
-         <th width=17% align=left nowrap>Kontakt</th>
-         <%if warehouse%>
-         <th width=17% align=left nowrap>Lager</th>
-         <%end warehouse%>
-         <th width=17% align=left>Versandort</th>
-         <th width=15% align=left>Lieferung durch</th>
-       </tr>
-
-       <tr>
-         <td><%ordnumber%>&nbsp;</td>
-
-         <%if shippingdate%>
-         <td><%shippingdate%></td>
-         <%end shippingdate%>
-
-         <%if not shippingdate%>
-         <td><%orddate%></td>
-         <%end shippingdate%>
-
-         <td><%employee%>&nbsp;</td>
-
-         <%if warehouse%>
-         <td><%warehouse%></td>
-         <%end warehouse%>
-
-         <td><%shippingpoint%>&nbsp;</td>
-         <td><%shipvia%>&nbsp;</td>
-       </tr>
-      </table>
-    </td>
-  </tr>
-
-  <tr>
-    <td>&nbsp;</td>
-
-    <td>
-      <table width=100%>
-       <tr bgcolor=000000>
-         <th align=left><font color=ffffff>Pos</th>
-         <th align=left><font color=ffffff>ArtNr.</th>
-         <th align=left><font color=ffffff>Beschreibung</th>
-         <th><font color=ffffff>Seriennummer</th>
-         <th>&nbsp;</th>
-         <th><font color=ffffff>Menge</th>
-         <th><font color=ffffff>Erh</th>
-         <th>&nbsp;</th>
-         <th><font color=ffffff>Lagerplatz</th>
-       </tr>
-
-       <%foreach number%>
-       <tr valign=top>
-         <td><%runningnumber%></td>
-         <td><%number%></td>
-         <td><%description%></td>
-         <td><%serialnumber%></td>
-         <td><%deliverydate%></td>
-         <td align=right><%qty%></td>
-         <td align=right><%ship%></td>
-         <td><%unit%></td>
-         <td><%bin%></td>
-       </tr>
-       <%end number%>
-
-      </table>
-    </td>
-  </tr>
-
-  <tr>
-    <td>&nbsp;</td>
-
-    <td><hr noshade></td>
-  </tr>
-
-</table>
-
diff --git a/templates/print/f-tex/default.tex b/templates/print/f-tex/default.tex
deleted file mode 100644 (file)
index 07def34..0000000
+++ /dev/null
@@ -1,568 +0,0 @@
-% ----------------------------------------------------------
-%  letter.tex
-%  Globale Vorlage fuer Briefartige Documente LX-Office 2.6
-%
-%  Changelog: see gitlog
-   \newcommand{\ftLetterVersion}{1.2-u  (05.12.2012)}
-%
-%  Lizenz
-%  http://www.gnu.de/licenses/gpl-3.0.html
-%
-%  Siehe ./README
-%
-%  Autor: Wulf Coulmann scripts_at_gpl.coulmann.de
-%  Aufgebaut auf invoice.tex 0.1 kmk@lilalaser.de
-%
-% ----------------------------------------------------------
-
-\documentclass[letter,fontsize=11pt]{scrlttr2}
-
-
-\begingroup
-  \makeatletter
-  \@latex@warning@no@line{ #### this is default.tex \ftLetterVersion #####}
-\endgroup
-
-
-\usepackage{ifpdf}
-\usepackage{graphicx}
-\usepackage{german}
-\usepackage{textcomp}
-\usepackage{lastpage}
-\usepackage{filecontents}
-\usepackage{etex}
-\usepackage{ltxtable}
-\usepackage{tabularx}
-\usepackage{longtable}
-\usepackage{booktabs}
-\usepackage{numprint}
-\usepackage{xstring}
-\newcommand{\leer}{}
-\usepackage{zwischensumme}
-\ifthenelse{\isundefined{\employeecountry}}{\input{mydata}}{}
-
-%% meta infos
-\newcommand{\docname}{<%template_meta.formname NOESCAPE%>}
-\newcommand{\TemplateMetaLanguageDescription}{<%template_meta.language.description NOESCAPE%>}
-\newcommand{\LangCode}{<%template_meta.language.template_code NOESCAPE%>}
-\newcommand{\TemplateMetaLanguageOutputNumberformat}{<%template_meta.language.output_numberformat NOESCAPE%>}
-\newcommand{\TemplateMetaLanguageOutputDateformat}{<%template_meta.language.output_dateformat NOESCAPE%>}
-\newcommand{\TemplateMetaFormat}{<%template_meta.format NOESCAPE%>}
-\newcommand{\TemplateMetaExtension}{<%template_meta.extension NOESCAPE%>}
-\newcommand{\TemplateMetaMedia}{<%template_meta.media NOESCAPE%>}
-\newcommand{\TemplateMetaPrinterDescription}{<%template_meta.printer.description NOESCAPE%>}
-\newcommand{\TemplateMetaPrinterTemplateCode}{<%template_meta.printer.template_code NOESCAPE%>}
-
-%%%%%%%%% Report-Variablen umsetzen, damit latex sie in lxbriefkopf.tex sieht.
-%%%% Die eigenen Daten
-\newcommand{\employeename}{<%employee_name%>}
-\newcommand{\employeecompany}{<%employee_company%>}
-\newcommand{\employeeaddress}{<%employee_address%>}
-\newcommand{\employeetel}{<%employee_tel%>}
-\newcommand{\employeefax}{<%employee_fax%>}
-\newcommand{\employeecoustid}{<%employee_co_ustid%>}
-\newcommand{\employeetaxnumber}{<%employee_taxnumber%>}
-\newcommand{\media}{<%media%>}
-
-
-%%%% Adressat
-\newcommand{\name}{<%name%>}
-\newcommand{\Shipname}{\ifthenelse{\equal{<%shiptoname%>}{\leer}}{<%name%>}{<%shiptoname%>}}
-\newcommand{\departmentone}{<%department_1%>}
-\newcommand{\departmenttwo}{<%department_2%>}
-\newcommand{\cpgreeting}{<%cp_greeting%>}
-\newcommand{\cptitle}{<%cp_title%>}
-\newcommand{\cpgivenname}{<%cp_givenname%>}
-\newcommand{\cpname}{<%cp_name%>}
-\newcommand{\street}{<%street%>}
-\newcommand{\Shipstreet}{\ifthenelse{\equal{<%shiptostreet%>}{\leer}}{<%street%>}{<%shiptostreet%>}}
-\newcommand{\country}{<%country%>}
-\newcommand{\Shipcountry}{<%shiptocountry%>}
-\newcommand{\UstId}{<%ustid%>}
-\newcommand{\zipcode}{<%zipcode%>}
-\newcommand{\Shipzipcode}{\ifthenelse{\equal{<%shiptozipcode%>}{\leer}}{<%zipcode%>}{<%shiptozipcode%>}}
-\newcommand{\city}{<%city%>}
-\newcommand{\Shipcity}{\ifthenelse{\equal{<%shiptocity%>}{\leer}}{<%city%>}{<%shiptocity%>}}
-\newcommand{\phone}{<%customerphone%>}
-\newcommand{\fax}{<%customerfax%>}
-
-%%%% Variablen, die sich auf das ganze Dokument beziehen
-\newcommand{\kundennummer}{<%customernumber%>}
-\newcommand{\vendornumber}{<%vendornumber%>}
-\newcommand{\quonumber}{<%quonumber%>}                     % Angebotsnummer
-\newcommand{\ordnumber}{<%ordnumber%>}                     % Auftragsnummer bei uns
-\newcommand{\cusordnumber}{<%cusordnumber%>}               % Auftragsnummer beim Kunden
-\newcommand{\invnumber}{<%invnumber%>}                     % Rechnungsnummer
-\newcommand{\donumber}{<%donumber%>}                       % Lieferscheinnummer
-%\newcommand{\docnumber}{Rechnungsnummer: \invnumber}
-\newcommand{\quodate}{<%quodate%>}                         % Angebotsdatum
-\newcommand{\orddate}{<%orddate%>}                         % Auftragsdatum
-\newcommand{\reqdate}{<%reqdate%>}                         % gewuenschtes Lieferdatum
-\newcommand{\deliverydate}{<%deliverydate%>}                % Lieferdatum
-\newcommand{\invdate}{<%invdate%>}                         % Rechnungsdatum
-\newcommand{\transdate}{<%transdate%>}                     % Lieferscheindatum
-\newcommand{\terms}{<%terms%>}                             % Zahlungsfrist
-\newcommand{\duedate}{<%duedate%>}                         % Fälligkeitsdatum
-\newcommand{\invtotal}{<%invtotal NOFORMAT%>}              % Gesamtbetrag
-\newcommand{\paid}{<%paid NOFORMAT%>}                      % Schon bezahlt
-\newcommand{\total}{<%total NOFORMAT%>}                    % Restbetrag
-\newcommand{\subtotal}{<%subtotal NOFORMAT%>}              % Restbetrag
-\newcommand{\paymentterms}{<%payment_terms%>}              % Zahlungsbedingungen
-\newcommand{\paymentPrivatEnd}{E}                          % Endung bei Privatkunden
-\newcommand{\paymenttype}{<%payment_description%>}         % name der Zahlungs-art - fuer Steuerung brutto/netto
-
-
-%%%% Lieferadresse
-\newcommand{\shiptoname}{<%shiptoname%>}
-\newcommand{\shiptocontact}{<%shiptocontact%>}
-\newcommand{\shiptodepartmentone}{<%shiptodepartment_1%>}
-\newcommand{\shiptodepartmenttwo}{<%shiptodepartment_2%>}
-\newcommand{\shiptostreet}{<%shiptostreet%>}
-\newcommand{\shiptocity}{<%shiptocity%>}
-\newcommand{\shiptocountry}{<%shiptocountry%>}
-\newcommand{\shiptophone}{<%shiptophone%>}
-\newcommand{\shiptozipcode}{<%shiptozipcode%>}
-\newcommand{\shiptofax}{<%shiptofax%>}
-
-%%%% Die Waehrungsvariable in Waehrunszeichen umsetzen
-\newcommand{\currency}{<%currency%>}
-\ifthenelse{\equal{\currency}{EUR}}{\let\currency\euro}{}
-\ifthenelse{\equal{\currency}{YEN}}{\let\currency\textyen}{}
-\ifthenelse{\equal{\currency}{GBP}}{\let\currency\pounds}{}
-\ifthenelse{\equal{\currency}{USD}}{\let\currency\$}{}
-
-%%%%%%%%%%%%% Ende Reportvariablen-Umsetzung
-
-\newcommand{\NoValue}{0}
-\newcommand{\Picklist}{0}
-\newcommand{\PurchaseOrder}{0}
-\newcommand{\trash}{0}
-\newcommand{\nonemptyline}[2]{\ifthenelse{\equal{#2}{\leer}}{}{#1#2~\\}}
-\newcommand{\MyAdress}{\IfStrEq{\docname}{sales_delivery_order}{\Shipname~\\
-  % lieferadresse wenn Lieferschein
-    \nonemptyline{\cpgreeting{ }\cpgivenname{ }}{\cpname}
-    \nonemptyline{}{\departmentone}
-    \Shipstreet ~\\
-    \Shipzipcode{ }\Shipcity
-    \ifthenelse{\equal{\Shipcountry}{\employeecountry}}{}{~\\ \Shipcountry}   % Laenderangabe wird nur gedruckt,
-    ~                                             % wenn der Empfaenger nicht im eigenen Land sitzt.
-  }{
-    \name~\\
-    \nonemptyline{\cpgreeting{ }\cpgivenname{ }}{\cpname}
-    \nonemptyline{}{\departmentone}
-    \street ~\\
-    \zipcode{ }\city
-    \ifthenelse{\equal{\country} {\employeecountry}}{}{
-         \ifthenelse{\equal{\country}{\leer}}{}{ ~\\ \country} } % Laenderangabe wird nur gedruckt,
-    ~                                           % wenn der Empfaenger nicht im eigenen Land sitzt.
-  }
-}
-
-
-
-\begin{document}
-
-%%% dei folgenden Funktionen lesen den Dokumentennamen aus und _muessen_nach_ \begin{dokument} stehen.
-
-% ==== statische Begriffe in der aktuellen Sprache einlesen
-\input{translations}
-
-
-\ifthenelse{\bgPdfEmailOnly = 1 }{
-  \ifthenelse{\equal{\media}{email}}{
-  }{
-    \firsthead{}
-    \watermark{}
-  }
-}{}
-
-
-% ==== dokumenttyp ermitteln
-\IfStrEq{\docname}{pick_list}{
-  % Sammelliste
-  \setkomavar{backaddress}{\DeliveryAddress}
-  \firsthead{
-      \hspace{-3mm}
-     \resizebox{\useplength{firstheadwidth}-50mm}{!}{%
-           \huge \TitlePicklist
-    }
-  }
-  \renewcommand{\NoValue}{1}
-  \renewcommand{\Picklist}{1}
-  \newcommand{\doctype}{}
-  \newcommand{\MyDocdate}{\transdate}
-  \newcommand{\DocNoTitle}{\DelorderNumber}
-  \newcommand{\docnumber}{\donumber}
-  \renewcommand{\deliverydate}{\transdate}
-  % 2. Documentnummer
-    \ifthenelse{\equal{\ordnumber}{\leer}}{
-    % wenn keine Auftragsnummer -> Angebotsnummer
-      \newcommand{\SecNoTitle}{\QuotationNumber}
-      \newcommand{\secnumber}{\quonumber}
-    }{
-      \newcommand{\SecNoTitle}{\OrderNumber}
-      \newcommand{\secnumber}{\ordnumber}
-    }
-}{}
-\IfStrEq{\docname}{sales_delivery_order}{
-  % Lieferschein
-  \renewcommand{\NoValue}{1}
-  \newcommand{\doctype}{\TitleDelorder}
-  \newcommand{\MyDocdate}{\transdate}
-  \newcommand{\DocNoTitle}{\DelorderNumber}
-  \newcommand{\docnumber}{\donumber}
-  \renewcommand{\deliverydate}{\transdate}
-  % 2. Documentnummer
-    \ifthenelse{\equal{\ordnumber}{\leer}}{
-    % wenn keine Auftragsnummer -> Angebotsnummer
-      \newcommand{\SecNoTitle}{\QuotationNumber}
-      \newcommand{\secnumber}{\quonumber}
-    }{
-      \newcommand{\SecNoTitle}{\OrderNumber}
-      \newcommand{\secnumber}{\ordnumber}
-    }
-}{}
-\IfStrEq{\docname}{invoice}{
-  % Rechnung
-  \newcommand{\doctype}{\TitleInv}
-  \newcommand{\MyDocdate}{\invdate}
-  \newcommand{\DocNoTitle}{\InvNumber}
-  \newcommand{\docnumber}{\invnumber}
-  % 2. Documentnummer
-    \ifthenelse{\equal{\ordnumber}{\leer}}{
-    % wenn keine Auftragsnummer -> Angebotsnummer
-      \newcommand{\SecNoTitle}{\QuotationNumber}
-      \newcommand{\secnumber}{\quonumber}
-    }{
-      \newcommand{\SecNoTitle}{\OrderNumber}
-      \newcommand{\secnumber}{\ordnumber}
-    }
-}{}
-\IfStrEq{\docname}{proforma}{
-  \newcommand{\doctype}{\TitleProforma}
-  \newcommand{\MyDocdate}{\invdate}
-  \newcommand{\DocNoTitle}{\InvNumber}
-  \newcommand{\docnumber}{\invnumber}
-  % 2. Documentnummer
-    \ifthenelse{\equal{\ordnumber}{\leer}}{
-    % wenn keine Auftragsnummer -> Angebotsnummer
-      \newcommand{\SecNoTitle}{\QuotationNumber}
-      \newcommand{\secnumber}{\quonumber}
-    }{
-      \newcommand{\SecNoTitle}{\OrderNumber}
-      \newcommand{\secnumber}{\ordnumber}
-    }
-}{}
-\IfStrEq{\docname}{purchase_order}{
-  \renewcommand{\PurchaseOrder}{1}
-  \newcommand{\doctype}{\TitlePurchaseOrder}
-  \newcommand{\MyDocdate}{\orddate}
-  \newcommand{\DocNoTitle}{\RequestOrderNumber}
-  \newcommand{\docnumber}{\ordnumber}
-  \renewcommand{\deliverydate}{\reqdate}
-  \renewcommand{\DelDate}{\ReqByTitle}
-  \renewcommand{\CustomerID}{\VendorID}
-  \renewcommand{\kundennummer}{\vendornumber}
-  \newcommand{\SecNoTitle}{}
-  \newcommand{\secnumber}{}
-}{}
-\IfStrEq{\docname}{credit_note}{
-  \newcommand{\doctype}{\TitleCreditNote}
-  \newcommand{\MyDocdate}{\invdate}
-  \newcommand{\DocNoTitle}{\CredNumber}
-  \newcommand{\docnumber}{\invnumber}
-  % keine 2. Documentnummer
-    \newcommand{\SecNoTitle}{}
-    \newcommand{\secnumber}{}
-}{}
-\IfStrEq{\docname}{sales_order}{
-  % Auftragsbestaetigung
-  \newcommand{\doctype}{\TitleSalesOrder}
-  \newcommand{\MyDocdate}{\orddate}
-  \renewcommand{\deliverydate}{\reqdate}
-  \newcommand{\DocNoTitle}{\OrderNumber}
-  \newcommand{\docnumber}{\ordnumber}
-  % 2. Documentnummer
-    \ifthenelse{\equal{\ordnumber}{\leer}}{
-    % wenn keine Angebotsnummer -> leer
-      \newcommand{\SecNoTitle}{}
-      \newcommand{\secnumber}{}
-    }{
-      \newcommand{\SecNoTitle}{\QuotationNumber}
-      \newcommand{\secnumber}{\quonumber}
-    }
-}{ }
-\IfStrEq{\docname}{sales_quotation}{
-  % Angebot
-  \newcommand{\doctype}{\TitleSalesQuotation}
-  \newcommand{\MyDocdate}{\quodate}
-  \renewcommand{\DelDate}{\ValidUntil}
-  \renewcommand{\deliverydate}{\reqdate}
-  \newcommand{\DocNoTitle}{\QuotationNumber}
-  \newcommand{\docnumber}{\quonumber}
-  % 2. Documentnummer
-    \newcommand{\SecNoTitle}{}
-    \newcommand{\secnumber}{}
-}{ }
-
-
-
-% ==== \paid auf 0.00 falls leer
-\IfSubStr{\paid}{\DecimalSign}{}{\renewcommand{\paid}{0{\DecimalSign}00}}
-
-
-
-\setkomavar{date}{}
-
-
-\begin{letter}{{\ifthenelse{\isnamedefined{MyAdressfield}}{\MyAdressfield
-  }{\MyAdress
-  }}
-}
-\opening{}
-
-%========Datum und Nummern====================================================
-
-\newcommand{\DocId}{
-  \begin{tabular*}{\textwidth+1em }{@{\extracolsep{\fill}}llllr}
-    \MakeUppercase{\tiny \DocNoTitle} &
-    \MakeUppercase{\tiny \CustomerID} &
-    \MakeUppercase{\tiny \SecNoTitle } &
-    \MakeUppercase{\tiny \DelDate }   &
-    \MakeUppercase{\tiny \Date}~\\
-    \mainfont\docnumber      &
-    \mainfont\kundennummer   &
-    \mainfont\secnumber   &
-    \mainfont\deliverydate  &
-    \mainfont\MyDocdate~\\
-\end{tabular*}  ~\\
-}
-
-\hspace{-0.5em} \DocId
-
-
-
-
-\nexthead{
-  \ifthenelse{\bgPdfFirstPageOnly = 1 }{
-    \hspace{-4mm}  \DocId
-  }{}
-}
-\vspace{ 5mm}
-
-{\noindent\textbf\doctype}~\\
-\IfEndWith{\paymenttype}{\paymentPrivatEnd}{\PriceInclTax }{ }
-
-
-%======Die eigentliche-Tabelle========================================
-
-% temporaere Datei mit Tabelle anlegen
-\begin{filecontents}{<%template_meta.tmpfile NOESCAPE%>.table.tex}
-\mainfont
-\resetlaufsumme
-
-
-
-  \ifthenelse{\NoValue > 0 }
-  { % Tabelle ohne Preisen
-    \ifthenelse{\Picklist = 1 }{
-
-    \begin{longtable}{@{}rlX@{ }rlrrrl@{}}
-     }{
-    \begin{longtable}{@{}rlX@{ }rlrr@{}}
-
-     }
-      % Kopfzeile der Tabelle
-
-        {\Pos} &
-        {\Number} &
-        {\ItemNo} &
-        {\Count} &
-        {\Unit} \hspace{2mm}
-        \ifthenelse{\Picklist = 1 }{& {\Take} & {\Storage} }{}
-        ~\\
-        \midrule
-      \endfirsthead
-
-      % Tabellenkopf nach dem Umbruch
-        {\Pos} &
-        {\Number} &
-        {\ItemNo} &
-        {\Count} &
-        {\Unit} \hspace{2mm}
-        \ifthenelse{\Picklist = 1 }{& {\Take} & {\Storage} }{}
-        ~\\
-
-        \midrule
-      \endhead
-
-      <%foreach number%>
-        <%runningnumber%>                        % Laufende Positionsnummer
-        &
-        <%number%>                               % Artikelnummer
-        &
-        <%description%>                           % Kurzbeschreibung des Artikels
-        \ifthenelse{\equal{<%longdescription%>}{\leer}}{}{ \newline <%longdescription%>}
-        % Ein zeilenweises Auslieferdatum, wenn es gesetzt bei der Position hinterlegt ist.
-        \ifthenelse{\equal{<%deliverydate_oe%>}{\leer}}{}{
-                \newline \DelDate:~<%deliverydate_oe%>}
-        &
-        <%qty NOFORMAT%>                 % Menge
-        &
-        <%unit%>               % Einheit
-        %\ifthenelse{\Picklist = 1 }{& {x} & {x} }{}
-        %\ifthenelse{\Picklist = 1 }{& {x} & {x} \hhline{~~~~~--} }{~\\}
-        \ifthenelse{\Picklist = 1 }{& {\underline{;~~~~~~~~~}} & {\underline{;~~~~~~~~~}}~\\ }{~\\}
-        %~\\ %
-      <%end number%>
-    \end{longtable}     % Ende der zentralen Tabelle
-  }{ % Tabelle mit Preisen
-    \begin{longtable}{@{}rlX@{ }rlrrr@{}}
-      % Kopfzeile der Tabelle
-
-        {\Pos} &
-        {\Number} &
-        {\ItemNo} &
-        {\Count} &
-        {\Unit} &
-        {\Fee} &
-        {\Dis} &
-        {\Total} \hspace{2mm} ~\\
-        \midrule
-      \endfirsthead
-
-      % Tabellenkopf nach dem Umbruch
-        {\Pos} &
-        {\Number} &
-        {\ItemNo} &
-        {\Count} &
-        {\Unit} &
-        {\Fee} &
-        {\Dis} &
-        {\Total} \hspace{2mm} ~\\
-        \midrule
-        \multicolumn{7}{r}{ \rule{0mm}{5mm} \TabCarry{:} \MarkZwsumPos}
-      \endhead
-
-
-      % Fuss der Teiltabellen
-        \multicolumn{7}{r}{ \rule{0mm}{5mm} \TabSubTotal{:} \MarkZwsumPos } ~\\
-      \endfoot
-
-      % Das Ende der Tabelle
-        \midrule
-        \multicolumn{7}{r}{ \rule{0mm}{5mm} \TabSubTotal{:} \MarkZwsumPos} ~\\
-      \endlastfoot
-
-      <%foreach number%>
-        <%runningnumber%>                        % Laufende Positionsnummer
-        &
-        <%number%>                               % Artikelnummer
-        &
-        <%description%>                           % Kurzbeschreibung des Artikels
-        \ifthenelse{\equal{<%longdescription%>}{\leer}}{}{ \newline <%longdescription%>}
-        % Ein zeilenweises Auslieferdatum, wenn es gesetzt ist.
-        \ifthenelse{\equal{<%reqdate%>}{\leer}}{}{
-                \newline \DelDate:~<%reqdate%>}
-        &
-        <%qty NOFORMAT%>         % Menge
-        &
-        <%unit%>              % Einheit
-        &
-        %\IfEndWith{\paymentterms}{_e}{EN}{\brutto{<%sellprice NOFORMAT%>}{<%qty NOFORMAT%>}{<%p_discount%>}}
-        \IfEndWith{\paymenttype}{\paymentPrivatEnd}{
-            \BruttoSellPrice{<%sellprice NOFORMAT%>}{<%tax_rate%>}
-            &
-            \ifthenelse{\equal{<%p_discount%>}{0}}{}{ -<%p_discount%>\%}
-            &
-            \BruttoWert{<%linetotal NOFORMAT%>}{<%tax_rate%>}
-        }{
-            \numprint{<%sellprice NOFORMAT%>}
-            &
-            \ifthenelse{\equal{<%p_discount%>}{0}}{}{ -<%p_discount%>\%}
-            &
-            \Wert{<%linetotal NOFORMAT%>} % Zeilensumme addieren
-        }
-        ~\\ %
-      <%end number%>
-    \end{longtable}     % Ende der zentralen Tabelle
-  }
-\end{filecontents}  % Ende der Hilfsdatei.
-
-\LTXtable{\textwidth}{<%template_meta.tmpfile NOESCAPE%>.table.tex}
-
-\rule{\textwidth}{0pt}   % Ein (unsichtbarer) Strich quer ueber die Seite
-\vspace{ 5mm}
-\vspace{-2em plus 10em minus 2em}~\\
-\ifthenelse{\NoValue > 0 }
-{ % wenn keine Zahlen
-}{ % Wenn Zahlen
-  \parbox{\textwidth}{
-    \mainfont
-    %
-    %
-    \setlength{\tabcolsep}{0.2em}
-    \ifthenelse{\equal{\paid}{0{\DecimalSign}00} }
-    {  % Wenn noch nichts gezahlt wurde
-       \IfSubStr{\invtotal}{\DecimalSign}{}{
-         \fpAdd{\invtotal}{0}{<%subtotal NOFORMAT%>}
-         <%foreach tax%>
-         \fpAdd{\invtotal}{\invtotal}{<%tax NOFORMAT%>}
-         <%end tax%>
-       }
-       \hfill
-       \begin{tabular}{@{}rrr@{}}
-               %{Summe vor Steuern:}& {\numprint{<%subtotal NOFORMAT%>}} & ~\\
-
-               % Die unterschiedlichen Steueranteile getrennt ausweisen
-               <%foreach tax%>
-                 { \IfEndWith{\paymenttype}{\paymentPrivatEnd}{\TaxInc }{ } <%taxdescription%>}
-                          &
-                 {\numprint{<%tax NOFORMAT%>}}& ~\\
-               <%end tax%>
-               \midrule[1pt]
-               {\Sum~ \currency:} & \textbf{\numprint{\invtotal}}
-       \end{tabular}
-    }
-    {  % Wenn bereits etwas gezahlt wurde
-       \hfill
-       \begin{tabular}{@{}rrr@{}}
-
-               {\EbT}& {\numprint{<%subtotal NOFORMAT%>}} & ~\\
-
-               % Die unterschiedlichen Steueranteile getrennt ausweisen
-               <%foreach tax%>
-               {<%taxdescription%>}
-                        &
-               {\numprint{<%tax NOFORMAT%>}}& ~\\
-               <%end tax%>
-
-               \midrule  % Ein dünner Strich
-               \Sum & \numprint{\invtotal} & ~\\
-
-               <%foreach payment%>
-                       \AlreadyPayed~ {<%paymentdate%>}:& -{\numprint{<%payment%>}} & ~\\
-               <%end paymentdate%>
-
-               \midrule[2pt]  % Ein etwas dickerer Strich
-               {\Left~ \currency:} & \numprint{\total}
-       \end{tabular}
-    }% ende ithenelse
-
-  } %Ende des Summenkasten
-}
-
-\vfill                 % Den Rest-Text soweit wie möglich nach unten schieben
-\ifthenelse{\isempty{<%notes%>}}{}{
-      \mainfont
-\noindent <%notes%> ~\\[2em]
-      }%
-\small
-\noindent \YourOrder
-\ifthenelse{\Picklist = 0}{\noindent \ifthenelse{\equal{<%ustid%>}{\leer}}{}{\UstidTitle} \UstId}{}
-\noindent \paymenthints          % ist in translations.tex deffiniert
-\ifthenelse{\PurchaseOrder = 0}{\noindent \paymentterms}{}
-
-
-\end{letter}
-\end{document}
diff --git a/templates/print/f-tex/letter.lco b/templates/print/f-tex/letter.lco
deleted file mode 120000 (symlink)
index b83bf86..0000000
+++ /dev/null
@@ -1 +0,0 @@
-sample.lco
\ No newline at end of file
diff --git a/templates/print/f-tex/letter_head.pdf b/templates/print/f-tex/letter_head.pdf
deleted file mode 120000 (symlink)
index 157619b..0000000
+++ /dev/null
@@ -1 +0,0 @@
-sample_head.pdf
\ No newline at end of file
diff --git a/templates/print/f-tex/mydata.tex b/templates/print/f-tex/mydata.tex
deleted file mode 120000 (symlink)
index b6ccd4a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-mydata.tex.example
\ No newline at end of file
diff --git a/templates/print/f-tex/mydata.tex.example b/templates/print/f-tex/mydata.tex.example
deleted file mode 100644 (file)
index 6758fec..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-
-% \employeecountry wird fuer lxo fancy LaTeX benoetigt
-\newcommand{\employeecountry}{Deutschland}
-
-
-
-% die folgenden definitionen koennten auch direkt in der Steuerdatei *.lco stehen
-\newcommand{\MYfromname}{Die globalen Problemlöser}
-\newcommand{\MYaddrsecrow}{Gesellschaft für anderer Leute Sorgen mbH}
-\newcommand{\MYrechtsform}{Handelsregister: HRA 123456789 }
-\newcommand{\MYfromaddress}{Hauptstraße 5\\12345 Hier}
-\newcommand{\MYfromphone}{Tel: +49 (0)12 3456780}
-\newcommand{\MYfromfax}{Fax: +49 (0)12 3456781}
-\newcommand{\MYfromemail}{mail@g-problemloeser.com}
-\newcommand{\MYsignature}{Herbert Wichtig - Geschäftsführer}
-\newcommand{\MYustid}{UstID: DE 123 456 789}
-\newcommand{\MYfrombank}{Bankverbindung\\
-          Ensifera Bank\\
-          Kto 1234567800\\
-           BLZ 123 456 78
-}
diff --git a/templates/print/f-tex/sample.lco b/templates/print/f-tex/sample.lco
deleted file mode 100644 (file)
index 3a7e8b1..0000000
+++ /dev/null
@@ -1,142 +0,0 @@
-% ----------------------------------------------------------
-%  letter.lco
-%  Steuerdatei Briefklasse f-tex
-%
-%  Changelog: see gitlog
-   \newcommand{\ftLcoVTversion}{1.1-u  (03.01.2012)}
-%
-%  Lizenz
-%  http://www.gnu.de/licenses/gpl-3.0.html
-%
-%  Siehe ./README
-%
-%  Autor: Wulf Coulmann scripts_at_gpl.coulmann.de
-%
-%
-% ----------------------------------------------------------
-
-
-\begingroup
-  \makeatletter
-  \@latex@warning@no@line{ #### this is letter.lco \ftLcoVTversion #####}
-\endgroup
-
-
-
-\ProvidesFile{letter.lco}[%
-  2002/07/09 v0.9a LaTeX2e unsupported letter-class-option]
-
-\KOMAoptions{foldmarks=false}
-\usepackage{graphicx}
-\usepackage[utf8]{inputenc}
-\usepackage{ngerman}
-\usepackage{lmodern}
-\usepackage{xcolor}
-\usepackage{watermark}
-\usepackage{xifthen}
-
-
-% ================== settings ==============================
-
-  % Name der pdf Datei die den Briefkopf enthaelt
-  \newcommand{\bgPdfName}{letter_head.pdf}
-
-  % Hintergrund pdf nur bei erster Dokumentseite [1|0]
-  \newcommand{\bgPdfFirstPageOnly}{1}
-
-  % Hintergrundpdf nur bei versand per email [1|0]
-  % (setze diesen Wert auf 1, wenn auf bereits Bedruktes Briefpapier ausgedruckt werden soll)
-  \newcommand{\bgPdfEmailOnly}{0}
-
-  % Trennlienie unter der Seitenkopfzeile ab Seite 2 ff.
-  \KOMAoptions{headsepline=on}
-
-  % der Abstand zu den Fusszeilen
-  \addtolength{\textheight}{23mm}
-
-  % zusaetzlicher Zwischenraum zur Fusszeile ab Seite 2 ff.
-  % (nur bei bgPdfFirstPageOnly = 1)
-  \addtolength{\footskip}{10mm}
-
-
-% ================== end settings ==============================
-
-
-
-\setkomavar{backaddress}{}
-
-\setkomavar{fromname}{\MYfromname}
-\newcommand\addrsecrow{\MYaddrsecrow}
-\newcommand\rechtsform{\MYrechtsform}
-\setkomavar{fromaddress}{\MYfromaddress}
-\setkomavar{fromphone}{\MYfromphone}
-\setkomavar{fromfax}{\MYfromfax}
-\setkomavar{fromemail}{\MYfromemail}
-\setkomavar{signature}{\MYsignature}
-\newcommand\ustid{\MYustid}
-\setkomavar{frombank}{\MYfrombank}
-
-\renewcommand{\rmdefault}{cmss}
-\newlength\entrytblsub
-\setlength\entrytblsub{\dimexpr\tabcolsep+1.3mm+\arrayrulewidth\relax}
-\setlength\textwidth{166mm}
-\oddsidemargin -0.4mm
-\KOMAoptions{headsepline=on}
-
-\pagestyle{myheadings}
-\@addtoplength{firstfootvpos}{18mm}
-\@addtoplength{foldmarkhpos}{5mm}
-\@setplength{firstheadvpos}{0mm}
-\@setplength{firstheadwidth}{165mm}
-\@setplength{firstfootwidth}{165mm}
-\@setplength{toaddrhpos}{25mm}
-\@setplength{toaddrvpos}{38mm}
-\@setplength{refhpos}{26mm}
-\@addtoplength{refvpos}{-18mm}
-
-\font\mainfont=cmss9
-
-
-
-\ifthenelse{\bgPdfFirstPageOnly = 0 }{
-  \addtolength{\headheight}{50mm}
-  \watermark{
-    \setlength{\unitlength}{1mm}
-    \put(-22,-226){
-          \includegraphics[width=210mm]{\bgPdfName}
-    }
-  }
-}{}
-
-\firsthead{
-  \ifthenelse{\bgPdfFirstPageOnly = 1 }{
-      \put(-69,0){  % Mit diesem put-Befehl wird die Position des Logos bestimmt.
-        \includegraphics[width=210mm]{\bgPdfName}
-      }
-  }{}
-}
-
-
-
-
-\firstfoot{%
-}
-
-\nextfoot{%
-    \parbox{\useplength{firstfootwidth}}{
-       \hspace{-\entrytblsub}
-       \begin{tabular}{l}
-       \usekomavar{fromname}
-       \end{tabular}\hfill
-       \begin{tabular}{r}
-           \thepage
-       \end{tabular}
-       \hspace{-\entrytblsub}
-    }
-    \vspace{10mm}
-}
-
-
-
-\endinput
-% vim: set filetype=tex :EOF
diff --git a/templates/print/f-tex/sample_head.pdf b/templates/print/f-tex/sample_head.pdf
deleted file mode 100644 (file)
index 8248505..0000000
Binary files a/templates/print/f-tex/sample_head.pdf and /dev/null differ
diff --git a/templates/print/f-tex/statement.html b/templates/print/f-tex/statement.html
deleted file mode 100644 (file)
index 37e612c..0000000
+++ /dev/null
@@ -1,121 +0,0 @@
-
-<body bgcolor=ffffff>
-
-<table width=100%>
-  <tr>
-    <td width=10>&nbsp;</td>
-    <td>
-      <table width=100%>
-       <tr>
-         <td>
-           <h4>
-           <%company%>
-           <br><%address%>
-           </h4>
-         </td>
-         <th></th>
-         <td align=right>
-         <h4>
-         Tel: <%tel%>
-         <br>Fax: <%fax%>
-         </h4>
-         </td>
-       </tr>
-       <tr>
-         <th colspan=3><h4>S T A T E M E N T</h4></th>
-       </tr>
-       <tr>
-         <td colspan=3 align=right><%statementdate%></td>
-       </tr>
-      </table>
-    </td>
-  </tr>
-  <tr>
-    <td>&nbsp;</td>
-    <td>
-      <table width=100%>
-       <tr valign=top>
-         <td><%name%>
-         <br><%street%>
-         <br><%zipcode%>
-         <br><%city%>
-         <br><%country%>
-         <br>
-<%if customerphone%>
-         <br>Tel: <%customerphone%>
-<%end customerphone%>
-<%if customerfax%>
-         <br>Fax: <%customerfax%>
-<%end customerfax%>
-<%if email%>
-         <br><%email%>
-<%end email%>
-         </td>
-       </tr>
-      </table>
-    </td>
-  </tr>
-  <tr height=10></tr>
-  <tr>
-    <td>&nbsp;</td>
-    <td>
-      <table width=100%>
-        <tr>
-         <th align=left>Invoice #</th>
-         <th width=15%>Date</th>
-         <th width=15%>Due</th>
-         <th width=10%>Current</th>
-         <th width=10%>30</th>
-         <th width=10%>60</th>
-         <th width=10%>90+</th>
-       </tr>
-<%foreach invnumber%>
-       <tr>
-         <td><%invnumber%></td>
-         <td><%invdate%></td>
-         <td><%duedate%></td>
-         <td align=right><%c0%></td>
-         <td align=right><%c30%></td>
-         <td align=right><%c60%></td>
-         <td align=right><%c90%></td>
-       </tr>
-<%end invnumber%>
-        <tr>
-         <td colspan=7><hr size=1></td>
-       </tr>
-       <tr>
-         <td>&nbsp;</td>
-         <td>&nbsp;</td>
-         <td>&nbsp;</td>
-         <th align=right><%c0total%></td>
-         <th align=right><%c30total%></td>
-         <th align=right><%c60total%></td>
-         <th align=right><%c90total%></td>
-       </tr>
-      </table>
-    </td>
-  </tr>
-  <tr height=10></tr>
-  <tr>
-    <td>&nbsp;</td>
-    <td align=right>
-      <table width=50%>
-        <tr>
-         <th>Total Outstanding</th>
-          <th align=right><%total%></th>
-       </tr>
-      </table>
-    </td>
-  </tr>
-  <tr>
-    <td>&nbsp;</td>
-    <td><hr noshade></td>
-  </tr>
-  <tr>
-    <td>&nbsp;</td>
-    <td>Please make check payable to <b><%company%></b>.
-    </td>
-  </tr>
-  <tr height=20></tr>
-</table>
-
diff --git a/templates/print/f-tex/translations.tex b/templates/print/f-tex/translations.tex
deleted file mode 100644 (file)
index 76811da..0000000
+++ /dev/null
@@ -1,198 +0,0 @@
-% ----------------------------------------------------------
-%  translations.tex
-%  Zentrale Uebersetzungsdatei f-tex
-%
-%  Changelog: see gitlog
-   \newcommand{\ftTranslationsVersion}{1.2-u  (05.12.2012)}
-%
-%  Lizenz
-%  http://www.gnu.de/licenses/gpl-3.0.html
-%
-%  Siehe ./README
-%
-%  Autor: Wulf Coulmann scripts_at_gpl.coulmann.de
-%
-%
-% ----------------------------------------------------------
-
-
-\begingroup
-  \makeatletter
-  \@latex@warning@no@line{ #### this is translations.tex \ftTranslationsVersion #####}
-\endgroup
-
-
-%%%%% Anleitung zum zufuegen neuer Sprachen %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-% Am Beispiel Franzoesisch (fr)                                               %
-% - Es wird empfohlen die Datei translations.tex im Vorlagenordner            %
-%   und _nicht_ in [lxo-home]/templates/f-tex zu aendern.                     %
-% - Kopiere den Block                                                         %
-%     "\newcommand{\LoadDE}{"                                                 %
-%   bis zur schliessenden Klammer                                             %
-%     "}"                                                                     %
-%   und fuege ihn am Ende der Datei bei                                       %
-%     "codeblock mit neuer Sprache hier einfuegen"                            %
-%   an                                                                        %
-% - uebersetze die deutschen Begriffe im neu eingefuegten Block in            %
-%   die neue Sprache                                                          %
-% - aendere den Kommandonamen entsprechend der Neuen Sprache                  %
-%     "\newcommand{\LoadFR}                                                   %
-% - fuege am Ende der Datei eine neue Zeile mit dem neuen Sprachkuerzel       %
-%   und dem neuen Funktionsnamen an.                                          %
-%     "\IfEndWith{\docname}{_fr}{\loadFR}{}                                   %
-% - pruefe, ob lxo bereits ueber eine Konfiguration zu der neuen Sprache      %
-%   verfuegt. Das Feld Vorlagenkuerzel muss den zur hier zugefuegten Sprache  %
-%   passenden Wert enthalten (in unserem Beispiel "fr")                       %
-% - rufe das script [lxo-home]/templates/f-tex/setup.sh erneut auf, um        %
-%   sicherzustellen, dass die benoetigten Symlinks vorhanden sind.            %
-% - schicke die neue Version dieser Datei an                                  %
-%     scripts_at_gpl.coulmann.de                                              %
-%   damit in Zukunft die neue Sprache auch anderen Nutzern                    %
-%   von lxo zur Verfuegung steht                                              %
-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-
-
-
-
-% ===== de ===========
-\newcommand{\loadDE}{
-
-  \renewcommand{\TitleInv}{Rechnung}
-  \newcommand{\TitleProforma}{Proformarechnung}
-  \newcommand{\TitleCreditNote}{Gutschrift}
-  \newcommand{\TitleSalesOrder}{Auftragsbestätigung}
-  \newcommand{\TitleSalesQuotation}{Angebot}
-  \newcommand{\TitleDelorder}{Lieferschein}
-  \newcommand{\TitlePicklist}{Sammelliste}
-  \newcommand{\TitlePurchaseOrder}{Bestellung}
-  \newcommand{\DelorderNumber}{Lieferscheinnummer}
-  \newcommand{\DeliveryAddress}{Lieferadresse}
-  \newcommand{\InvNumber}{Rechnungsnummer}
-  \newcommand{\CredNumber}{Gutschriftnummer}
-  \newcommand{\OrderNumber}{Auftragsnummer}
-  \newcommand{\RequestOrderNumber}{Bestellauftragsnummer}
-  \newcommand{\QuotationNumber}{Angebotsnummer}
-  \newcommand{\CustomerID}{Kundennummer}
-  \newcommand{\VendorID}{Lieferantennummer}
-  \newcommand{\DelDate}{Lieferdatum}
-  \newcommand{\ReqByTitle}{Lieferung bis}
-  \newcommand{\ValidUntil}{gültig bis}
-  \newcommand{\Date}{Datum}
-  \newcommand{\Pos}{Pos}
-  \newcommand{\Number}{Best Nr.}
-  \newcommand{\ItemNo}{Artikel}
-  \newcommand{\Count}{Anz}
-  \newcommand{\Unit}{Einh}
-  \newcommand{\Storage}{Lagerplatz}
-  \newcommand{\Take}{entnommen}
-  \newcommand{\Fee}{Einzelp}
-  \newcommand{\Total}{Total}
-  \newcommand{\Sum}{Gesamtbetrag}
-  \newcommand{\EbT}{Summe vor Steuern}
-  \newcommand{\Left}{Restbetrag}
-  \newcommand{\AlreadyPayed}{bereits gezahlt am}
-  \newcommand{\TabSubTotal}{Zwischensumme}
-  \newcommand{\TabCarry}{Übertrag}
-  \newcommand{\Dis}{Rab}
-  \newcommand{\TaxInc}{bereits enthalten: }
-  \newcommand{\PriceInclTax}{Alle Preise incl. Mehrwertsteuer}
-  \newcommand{\UstidTitle}{Ihre Umsatzsteueridentnummer:}
-
-
-  % Zahlungshinweise
-  \newcommand{\paymenthints}{
-
-    \IfSubStr{\docname}{Angebot}{
-      Das Angebot hat 4 Wochen Gültigkeit.\\
-    }{}
-  }
-
-  \newcommand{\YourOrder}{
-    \ifthenelse{\equal{\cusordnumber}{\leer}}
-      {}
-      {Ihre Bestellung {\bf\cusordnumber}}\\[0.5em]
-  }
-
-}
-
-% ===== uk oder en ===========
-\newcommand{\loadUK}{
-
-  \renewcommand{\TitleInv}{Invoice}
-  \newcommand{\TitleProforma}{Pro Forma Invoice}
-  \newcommand{\TitleCreditNote}{Credit Note}
-  \newcommand{\TitleSalesOrder}{Sales Order}
-  \newcommand{\TitleSalesQuotation}{Sales Quotation}
-  \newcommand{\DelorderNumber}{delivery note no}
-  \newcommand{\DeliveryAddress}{delivery address}
-  \newcommand{\TitleDelorder}{Delivery Note}
-  \newcommand{\TitlePicklist}{Pick List}
-  \newcommand{\TitlePurchaseOrder}{Purchase Order}
-  \newcommand{\InvNumber}{invoice number}
-  \newcommand{\CredNumber}{credit number}
-  \newcommand{\OrderNumber}{order number}
-  \newcommand{\RequestOrderNumber}{purchase order no.}
-  \newcommand{\QuotationNumber}{quotation no}
-  \newcommand{\CustomerID}{customer id}
-  \newcommand{\VendorID}{vendor id}
-  \newcommand{\DelDate}{date of delivery}
-  \newcommand{\ReqByTitle}{required by}
-  \newcommand{\ValidUntil}{valid until}
-  \newcommand{\Date}{date}
-  \newcommand{\Pos}{pos}
-  \newcommand{\Number}{item id}
-  \newcommand{\ItemNo}{item}
-  \newcommand{\Count}{count}
-  \newcommand{\Unit}{unit}
-  \newcommand{\Storage}{location}
-  \newcommand{\Take}{taken}
-  \newcommand{\Fee}{fee}
-  \newcommand{\Total}{total}
-  \newcommand{\Sum}{total amount}
-  \newcommand{\EbT}{total without taxes}
-  \newcommand{\Left}{residue}
-  \newcommand{\AlreadyPayed}{already payed at}
-  \newcommand{\TabSubTotal}{subtotal}
-  \newcommand{\TabCarry}{carry}
-  \newcommand{\Dis}{dis}
-  \newcommand{\TaxInc}{already included: }
-  \newcommand{\PriceInclTax}{Prices incl. tax}
-  \newcommand{\UstidTitle}{Your VAT number:}
-
-  % Zahlungshinweise Rechnung
-  \newcommand{\paymenthints}{
-    \IfSubStr{\docname}{Angebot}{
-      The offer is valid for 4 weeks.\\
-    }{}
-  }
-
-  \newcommand{\YourOrder}{
-    \ifthenelse{\equal{\cusordnumber}{\leer}}
-      {}
-      {Your Order Number {\bf\cusordnumber}}\\[0.5em]
-  }
-
-}
-
-% ====== neuen Sprache ================================
-
-   % codeblock mit neuer Sprache hier einfuegen
-
-
-% ====== Ende Sprachblock =========
-\newcommand{\checkVal}{unknowen}
-\newcommand{\TitleInv}{\checkVal}
-
-
-\IfStrEq{\LangCode}{de}{\loadDE}{}
-\IfStrEq{\LangCode}{uk}{\loadUK}{}
-\IfStrEq{\LangCode}{en}{\loadUK}{}
-% neue Zeile mit dem neuen Sprachkuerzel und dem neuen Funktionsnamen hier anfuegen
-
-
-
-% ====== unterhalb dieser Zeile nichts aendern ==========================
-
-% defaultsprache
-  \ifthenelse{\equal{\TitleInv}{\checkVal}}{\loadDE}{}
-
diff --git a/templates/print/f-tex/zwischensumme.sty b/templates/print/f-tex/zwischensumme.sty
deleted file mode 100644 (file)
index 043d94f..0000000
+++ /dev/null
@@ -1,228 +0,0 @@
-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-% Makros zur Berechnung und Ausgabe einer Zwischensumme bei langen Tabellen
-% Der Hack der longtable Ausgabe ist von Heiko Oberdiek, das Paket zref auch.
-%                            ---<(kaimartin)>---(August, 2007)
-%
-%  - Dezimaltrennzeichenn nur noch "."               by scripts_at_gpl.coulmann.de 2010-12
-%    (raw_numbers patch)
-%  - \Wert -> default Wert 0,                        by scripts_at_gpl.coulmann.de 2009-08
-%    wenn kein Wert uebergebenn wird, dies
-%    ermoeglicht das Ausgeben von Tabellen ohne
-%    Preise (z.b. Lieferscheine)
-%  - keine Ausgabe der Zwischensumme, wenn 0
-%  - neu: \brutto zur Ausgabe von Bruttopreisen      by scripts_at_gpl.coulmann.de 2009-07
-%  - Anpassungen fuer fancy LaTeX                    by scripts_at_gpl.coulmann.de 2009-03
-%
-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-% Diese Datei steht unter der GPL-Lizenz, Version 3
-% siehe http://www.gnu.de/licenses/gpl-3.0.html
-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-
-\usepackage{etex}           % Damit Marken verwendet werden koennen
-\usepackage[savepos,user]{zref}  % Um die jeweils aktuelle Position zu merken
-\usepackage{fltpoint}       % Rechnen mit Komma-Zahlen
-\usepackage{numprint}       % Zahlen formatiert ausgeben
-\usepackage{eurosym}        % Das Euro-Zeichen
-\usepackage{calc}           % Fuer das Makro \widthof{}
-
-% Vorlagen sind auf raw_num Patch ausgelegt daher nur noch . als Trennzeichen
-\newcommand{\DecimalSign}{.}
-\fpDecimalSign{\DecimalSign}
-
-% Globale Einstellungen fuer numprint
-\npstylegerman      % Deutsche Zahlenformatierung, in der Ausgabe
-\nprounddigits{2}   % Zwei Nachkommasstellen
-
-% \leer ist bereits in letter.tex definiert, wenn nicht muss es hier passieren
-% \newcommand{\leer}{}
-
-%%%%%%%%%%%%%%Befehle zur Berechnung der Zwischensumme%%%%%%%%%%%%%%%%%%%%%%%
-\newcommand*\laufsumme{0}
-\newcommand*\resetlaufsumme{\global\def\laufsumme{0}}
-\newcommand*\addlaufsumme[1]{\fpAdd{\laufsumme}{\laufsumme}{#1}%
-                                 \global\let\laufsumme\laufsumme}
-\newcommand*\printwert[1]{
-  \ifthenelse{\NoValue > 0}{
-  }{
-    \numprint{#1}
-  }
-}
-
-
-%%%%%%%%Plaintex-Hack fuer Positionierung der Zwischensummen%%%%%%%%%%%%%%%%%%
-
-
-\makeatletter  % Das at-Zeichen in Variablen zulassen
-
-% Variablen bereit stellen
-  \newdimen\drx
-  \newdimen\dry
-
-  \newmarks\ltm@marks
-  \def\ltm@setmarks#1{%
-    \marks\ltm@marks{#1}%
-    }
-  \def\ltm@getmarks{%
-    \botmarks\ltm@marks
-    }
-
-
-% Den aktuellen Wert der Laufsumme berechnen und merken
-\newcommand*{\Wert}[1]{%
-  \ifthenelse{\equal{#1}{\leer}}{
-    \addlaufsumme{0}%  Den uebergebenen Wert zur Laufsumme addieren
-    \expandafter\ltm@setmarks\expandafter{\laufsumme}% Die Laufsumme merken
-  }{
-    \printwert{#1}%     Ausgabe des Werts vor Ort
-    \addlaufsumme{#1}%  Den uebergebenen Wert zur Laufsumme addieren
-    \expandafter\ltm@setmarks\expandafter{\laufsumme}% Die Laufsumme merken
-  }
-}
-
-% Merken der aktuellen Position
-\newcommand*{\MarkZwsumPos}{%
-  \leavevmode
-     \zsavepos{zwsumpos\thepage}%
-     \zrefused{zwsumpos\thepage}%
-}
-
-
-% Ausgabe der Zwischensumme
-\def\ltm@insertfoot#1{%
-    \vbox to\z@{%
-      \vss
-      \hb@xt@\z@{%
-        \color@begingroup
-           \zsavepos{tabende\thepage}%   % Die aktuelle Position merken
-           \drx=0sp
-           \dry=0sp
-           % Die aktuelle Position abziehen und die gemerkte addieren
-           \advance \drx by -\zposx{tabende\thepage}sp
-           \advance \drx by \zposx{zwsumpos\thepage}sp
-           \advance \dry by -\zposy{tabende\thepage}sp
-           \advance \dry by \zposy{zwsumpos\thepage}sp
-           \smash{\kern\drx\raise\dry%
-             %\hbox{\makebox[\widthof{ \currency}][r]{\printwert{#1} \currency}}%  % mit Waehrungszeichen
-            \hbox{\printwert{#1} }%                                                % ohne Waehrungszeichen
-           }% end smash
-        \color@endgroup
-      }%
-    }%
-    \vspace{4mm}
-}
-
-% Ausgabe des Uebertrags
-% Wie die Ausgabe der Zwischensumme, nur ohne neu gemerkte Position
-\def\ltm@inserthead#1{%
-    \vbox to\z@{%
-      \vss
-      \hb@xt@\z@{%
-        \color@begingroup
-           \drx=0sp
-           \dry=0sp
-           % Die Position des Tabellenendes abziehen und zur gemerkten gehen
-           \advance \drx by -\zposx{tabende\thepage}sp
-           \advance \drx by \zposx{zwsumpos\thepage}sp
-           \advance \dry by -\zposy{tabende\thepage}sp
-           \advance \dry by \zposy{zwsumpos\thepage}sp
-           \smash{\kern\drx\raise\dry%
-              % Die eigentliche Ausgabe.
-              %  Rechtsbuendig und um die Breite der Währung verschoben.
-              %\hbox{\makebox[\widthof{ \currency}][r]{\printwert{#1} \currency}}%
-             \hbox{\printwert{#1}}%                                                % ohne Waehrungszeichen
-            %\hbox{\makebox[\widthof{ \printwert{#1}}][r]{\printwert{#1}\rule{0mm}{10mm} }}%                                                % ohne Waehrungszeichen
-             }% end smash
-        \color@endgroup
-      }%
-    }%
-    \vspace{1mm}
-}
-
-
-\def\ltm@lastfoot{\ltm@insertfoot\ltm@getmarks}
-\def\ltm@foot{\ltm@insertfoot{\ltm@getmarks}}
-\def\ltm@head{\ltm@inserthead{\ltm@getmarks}}
-
-
-% Ueberschreiben der Output-Routine von longtable
-\def\LT@output{%
-  \ifnum\outputpenalty <-\@Mi
-    \ifnum\outputpenalty > -\LT@end@pen
-      \LT@err{floats and marginpars %
-              not allowed in a longtable}\@ehc
-    \else
-      \setbox\z@\vbox{\unvbox\@cclv}%
-      \ifdim \ht\LT@lastfoot>\ht\LT@foot
-        \dimen@\pagegoal
-        \advance\dimen@-\ht\LT@lastfoot
-        \ifdim\dimen@<\ht\z@
-          \setbox\@cclv\vbox{%
-            \unvbox\z@\copy\LT@foot\ltm@foot\vss
-          }%
-          \@makecol
-          \@outputpage
-          \setbox\z@\vbox{\box\LT@head}%
-        \fi
-      \fi
-      \global\@colroom\@colht
-      \global\vsize\@colht
-      \vbox{%
-        \unvbox\z@
-        \box\ifvoid\LT@lastfoot
-          \LT@foot\ltm@foot
-        \else
-          \LT@lastfoot\ltm@lastfoot
-        \fi
-      }%
-    \fi
-  \else
-    \setbox\@cclv\vbox{%
-      \unvbox\@cclv\copy\LT@foot\ltm@foot\vss
-    }%
-    \@makecol
-    \@outputpage
-    \global\vsize\@colroom
-    \copy\LT@head\ltm@head
-  \fi
-}
-
-\newcommand\BruttoSellPrice[2]{
-      \fpAdd{\tax}{#2}{100}
-      \fpDiv{\taxF}{\tax}{100}
-      \fpMul{\result}{#1}{\taxF}
-      \numprint{\result}
-}
-\newcommand\BruttoWert[2]{
-      \fpAdd{\tax}{#2}{100}
-      \fpDiv{\taxF}{\tax}{100}
-      \fpMul{\rawresult}{#1}{\taxF}
-      \Wert{\rawresult}
-}
-
-
-\newcommand\BruttoLineSum[4]{
-      \fpAdd{\tax}{#4}{100}
-      \fpDiv{\taxF}{\tax}{100}
-      \fpMul{\result}{#1}{\taxF}
-      \fpMul{\result}{#2}{\result}
-      \fpSub{\rabatt}{100}{#3}
-      \fpDiv{\rabatt}{\rabatt}{100}
-      \fpMul{\result}{\result}{\rabatt}
-      \Wert{\result}
-}
-
-%      \ifthenelse{\equal{<%p_discount%>}{0}}{}{ -<%p_discount%>\%} &
-%        %<%sellprice%>
-%      \Wert{<%linetotal%>}    % Zeilensumme
-
-%  \fpMul{\result}{#1}{1.19}
-%  \fpMul{\resultt}{#2}{\result}
-%  \fpSub{\rabatt}{100}{#3}
-%  \fpDiv{\rabattt}{\rabatt}{100}
-%  \fpMul{\resulttt}{\resultt}{\rabattt}
-%  %\fpRound{\roundresult}{\result}{3}
-%  %\roundresult
-%  \resulttt
-
-\makeatother    % Das at-Zeichen in Variablen wieder verbieten
-%%%%%%%%%%%%%%%%%%%%Ende plaintex-Hack%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
diff --git a/templates/print/marei/Readme.md b/templates/print/marei/Readme.md
new file mode 100644 (file)
index 0000000..3e75424
--- /dev/null
@@ -0,0 +1,203 @@
+# Bemerkungen zum Vorlagensatz
+### © 2020–2021 by Marei Peischl (peiTeX TeXnical Solutions)
+
+## Quickstart (wo kann was angepasst werden?):
+
+  * insettings.tex : Pfad zu Angaben über Mandanten (default: firma)
+                     Logo/Briefpapier
+                     Layout der Kopf/Fußzeile
+                     innerhalb dieser Datei werden auch die folgenden Dateien geladen:
+                     firma/ident.tex        : Angaben über Mandanten
+                     firma/<währungskürzel>_account.tex
+
+* Es muß mindestens eine Sprache angelegt werden!
+  -  deutsch.tex    : Textschnipsel für Deutsch
+                      Dafür eine Sprache mit Vorlagenkürzel DE anlegen
+  -  english.tex    : Textschnipsel für Englisch
+                      Dafür eine Sprache mit Vorlagenkürzel EN anlegen
+
+
+
+## Aufbau:
+Die Grundstruktur besteht je Dokumententyp aus einer Basisdatei und verschiedenen Setup-Dateien.
+
+Die Basis wurde so überarbeitet, dass Dokumente nun generell auf der Dokumentenklasse *scrartcl.cls* basieren und das Paket *kiviletter.sty* benutzen.
+
+Mandantenspezifische Konfiguration findet sich in der Datei *insettings.tex* und dem Ordner eines spezifischen Mandanten (default=*firma/*).
+
+
+### Struktur der Basisdatei (je Dokumententyp eine)
+
+1. Dokumentenklasse
+2. *kiviletter.sty*
+3. Einstellungen, die über Variablen gesetzt werden: Mandant, Währung, Sprache
+4. `\input{insettings.tex}` Anteil der spezifischen Anpassungen, die von den Variablen unter 2. abhängig sind. Geladen werden darin die Dateien:
+   - Sprache: lädt die entsprechende Sprachdatei, falls DE -> *deutsch.tex*, falls EN *englisch.tex* und setzt die babel Optionen. Die Datei enthält Übersetzungen von Einzelbegriffen und Textbausteinen.
+   - Lädt die Konfigurationsdatei, ohne spezielle Mandanten ist der Suchpfad zur Konfiguration der Unterordner *firma/*
+   - Lädt die Datei *ident.tex*, sowie die Abbildung Briefkopf.
+
+#### Mandanten / Firma:
+
+Um gleiche Vorlagen für verschiedene Firmen verwenden zu können, wird je
+nach dem Wert der Kivitendo-Variablen `<%kivicompany%>` ein
+Firmenverzeichnis ausgewählt (siehe *insettings.tex'), in dem Briefkopf,
+Identitäten und Währungs-/Kontoeinstellungen hinterlegt sind.
+`<%kivicompany%>` enthält den Namen des verwendeten Mandantendaten.
+Ist kein Firmenname eingetragen, so wird das
+generische Unterverzeichnis *firma* verwendet.
+
+#### Identitäten:
+
+In jedem Firmen-Unterverzeichnis soll eine Datei *ident.tex*
+vorhanden sein, die mit `\newcommand` Werte für \telefon, `\fax`,
+`\firma`, `\strasse`, `\ort`, `\ustid`, `\email` und `\homepage` definiert.
+
+#### Währungen / Konten:
+Für jede Währung (siehe *insettings.tex*) soll eine Datei vorhanden
+sein, die das Währungssymbol (`\currency`) und folgende Angaben für
+ein Konto in dieser Währung enthält `\kontonummer`, `\bank`,
+`\bankleitzahl`, `\bic` und `\iban`.
+So kann in den Dokumenten je nach Währung ein anderes Konto
+angegeben werden.
+Nach demselben Schema können auch weitere, alternative Bankverbindungen
+angelegt werden, die dann in *insettings.tex* als Variable in der Fußzeile eingefügt werden.
+Als Fallback (falls kivitendo keine Währung an das Druckvorlagen-System übergibt)
+ist Euro eingestellt. Dies lässt sich in der *insettings.tex* über das optionale Argument
+von `\setupCurrencyConfig` anpassen, z.B.
+
+```
+\setupCurrencyConfig[chf]{\identpath}{\lxcurrency}
+```
+für Schweizer Franken als Standardwährung.
+
+#### Briefbogen/Logos:
+Eine Hintergrundgrafik oder ein Logo kann in Abhängigkeit vom
+Medium (z.B. nur beim Verschicken mit E-Mail) eingebunden
+werden.
+
+Desweiteren sind (auskommentierte) Beispiele enthalten für eine
+Grafik als Briefkopf, nur ein Logo, oder ein komplettes DinA4-PDF
+als Briefpapier.
+
+Absolute Positionierung innerhalb des Brief-Layouts ist über die entsprechende Dokumentation des scrlayer-Paketes möglich.
+Da die Voreinstellungen bereits einige Sonderfälle automatisch berücksichtigen ist mit den Anpassungen Vorsicht geboten.
+Sämtliche Einstellungen sollten jedoch außerhalb der *.sty-Dateien vorgenommen werden.
+Anpassungen der insettings.tex betreffen hierbei alle Mandanten. Mandantenspezifische Einstellung sind über die zugehörige Konfigurationsdatei möglich.
+In diesem Fall kann zum Ende der insettings eine weitere Konfigurationsdatei über die Verwendung von \identpath geladen werden. Ein Beispiel ist in der insettings.tex enthalten.
+
+#### Fußzeile:
+Die Tabelle im Fuß verwendet die Angaben aus *firma/ident.tex* und
+*firma/*_account.tex*. Ihre Struktur wird in der *insettings.tex* definiert.
+
+#### Seitenstil/Basislayout:
+Das Seitenlayout wird über scrlayer-scrpage bestimmt. Es existieren in der Datei *insettings.tex* einige Hinweise zu den Anpassungen. Die Basiskonfiguration ist ebenfalls dort eingetragen.
+
+Die Kopfzeile unterscheidet sich von Dokumententyp zu Dokumententyp leicht, da diese über Datenbankvariablen befüllt wird. Hierfür wird das Makro `\ourhead` definiert. Diese Definition kann ebenfalls über die *insettings.tex* geändert werden.
+
+### Tabellen:
+
+Die Tabellenstruktur wurde komplett überarbeitet. Der Vorlagensatz verfügt über Tabellen, die automatisch die Breite der Textbreite anpassen und zusätzlich Seitenumbrüche erlauben.
+
+#### SimpleTabular
+
+Der einfache Tabellentyp ist die Umgebung `SimpleTabular`. die ist eine Tabelle basieren auf dem xltabular-Paket, die die sich der Textbreite anpasst. Sie wird in den Dateien *zahlungserinnerung_invoice.tex*, *zahlungserinnerung.tex* und *statement.tex* verwendet.
+
+Sie verfügt über ein optionales Argument um die Spaltenkonfiguration und die Kopfzeile anzupassen. Die Voreinstellung (also ohne optionales Argument) entspricht der, der folgenden Angabe:
+
+```
+\begin{SimpleTabular}[colspec=rrX,headline={\bfseries\position & \bfseries\menge & \bfseries\bezeichnung}]
+
+```
+
+##### Tabellenkopfzeile
+Die Kopfzeile wird über den Optionsschlüssel headline angepasst. Entsprechend dem LaTeX-Standard werden Tabellen Spalten mit `&` getrennt. `\bfseries` setzt den Tabellenkopf zusätzlich in Fettschrift.
+
+##### Spaltenkonfiguration (fortgeschrittene Nutzer)
+Die voreingestellte Spaltenkonfiguration entspricht `rrX`, also zwei rechtsbündigen Spalten und einer Blocksatzspalte, die die restliche Breite einnimmt. Soll von dieser Spaltenkonfiguration abgewichen werden, steht der Optionsschlüssel `colspec` zur Verfügung. Das folgende Beispiel tauscht die beiden rechtsbündigen Spalten in linksbündige:
+
+```
+\begin{SimpleTabular}[colspec=llX]
+
+```
+Als Spaltentypen sind Konfigurationen aus den folgenden Einträgen am sinnvollsten:
+* `l`, `r`, `c`: Linksbündig, rechtsbündig, zentriert. Spaltenbreite passt sich dem Inhalt an.
+* `X`: Blocksatz, Spaltenbreite füllt den übrigen Platz auf. Bei mehreren `X`-Spalten wird gleichmäßig aufgeteilt
+
+Zusätzlich ist es möglich die Währung automatisch in der Spalte zu ergänzen.
+Der Mechanismus ist so kontruiert, dass diese nicht in der Kopfzeile sondern lediglich in den Inhaltszeilen eingefügt wird.
+In diesem Fall wird die Spaltenspezifikation durch `<{\tabcurrency}` ergänzt.
+Eine rechtsbündige Spalte mit Währungsangabe wird somit durch `r<{\tabcurrency}` erzeugt.
+
+
+#### PricingTabular
+
+`PricingTabular` wurde entwickelt um Tabellen für Rechnungen vereinfacht erstellen zu können.
+Die Voreinstellung verfügt über die Spalten `pos`, `id`, `desc`, `amount`, `price`, `pricetotal'.
+Alle Spalten, außer der Spalte `desc` haben eine Feste Breite.
+
+Die Einstellungen können Entweder als Optionales Argument zu `\begin{PricingTabular}[<Optionen>]` vorgenommen werden oder über das Makro `\SetupPricingTabular{<Optionen>}` für alle folgenden Umgebungen gesetzt werden.
+
+
+###### Spaltenbreiten
+
+Die Spaltenbreiten werden angepasst indem der Spaltenname verwendet wird.
+Um die Positionsspalte zu ändern ist somit die Option `pos=<Breite>` notwendig.
+Hier können alle Längenangaben verwendet werden, die LaTeX versteht. (cm, mm, em, ex, …)
+
+Die Spaltenbreite der Spalte `desc` für die Artikelbeschreibung nimmt dabei jeweils den übrigen Platz ein.
+
+##### Kopfzeileneinträge
+
+Die Kopfzeileneinträge werden über die Option `<Spaltenname>/header=<Neue Beschriftung>` angepasst.
+Vorbelegt ist die Konfiguration:
+
+```
+\SetupPricingTabular{
+  pos/header=\position,
+  id/header=\artikelnummer,
+  desc/header=\bezeichnung,
+  amount/header=\menge,
+  price/header=\einzelpreis,
+  pricetotal/header=\gesamtpreis
+}
+```
+
+##### Farbige Tabellen
+Versionen ab Juli 2021 enthalten die Möglichkeit farbige Tabellen zu nutzen.
+Die Optionen für die `PricingTabular` Umgebung können wie folgt konfiguriert werden:
+```
+  color-rows=<true/false>,% false
+  rowcolor-odd=<Farbname>,% black!10
+  rowcolor-even=<Farbname>,% leer, also keine Farbbox wird erzeugt
+  rowcolor-header=<Farbname>,% black!35
+  rowcolor-total=<Farbname>,% black!35
+```
+Die Angabe hinter dem Kommentarzeichen entspricht der Voreinstellung.
+
+#### Trennlinien zwischen den Einträgen
+Die Umgebung `PricingTabular` hat die möglichkeit horizontale Linien zwischen den Einträgen der `\FakeTable` einzuziehen.
+Die einfachste Möglichkeit hierfür ist die Option hrule, sie setzt automatisch eine Linie der Dicke `\lightrulewidth`.
+Da diese Linie formal nicht innerhalb der Tabelle platziert wird, können Linienmakros für Tabellen heir nicht verwendet werden.
+Falls dennoch eine manuelle Anpassung der Maße notwendig ist, kann direkt der Code zur Erzeugung der Linie übergeben werden.
+Die Option `hrule` entspricht der Angabe
+```
+  rowsep={
+    \vskip\aboverulesep
+    \hrule\@height\lightrulewidth
+    \vskip\belowrulesep
+  }
+```
+Es wird somit auch der Abstand davor und danach mit eingefügt. In Kombination mit Farbigen Tabellen ist hier vorsicht geboten, da der Abstand nicht mit zur farbigen Box gerechnet wird.
+
+
+##### Reihenfolge/Anzahl der Spalten ändern
+
+Die Reihenfolge wurde über die Option `columns` festgelegt.
+Soll daher eine Tabelle mit nur drei Spalten und lediglich bestehend aus Produktnummer, Beschreibung und Menge genutzt werden, ist dies mit der Option `columns={id,desc,amount}` möglich.
+
+Einzelne Spalten können auch über `<Spaltenname>=false` abgeschaltet werden. Dies ist z.B. dann hilfreich, wenn die Angabe einer Produktnummer aus platzgründen nicht sinnvoll ist (`id=false`).
+
+
+
+
+
diff --git a/templates/print/marei/bin_list.html b/templates/print/marei/bin_list.html
new file mode 100644 (file)
index 0000000..d57632d
--- /dev/null
@@ -0,0 +1,180 @@
+<body bgcolor=ffffff>
+
+<table width=100%>
+  <tr>
+    <td width=10>&nbsp;</td>
+    
+    <td>
+      <table width=100%>
+       <tr>
+         <td>
+           <h4>
+           <%company%>
+           <br><%address%>
+           </h4>
+         </td>
+         
+         <th><img src=http://localhost/lx-erp/lx-office-erp.png border=0 width=64 height=58></th>
+
+         <th align=right>
+           <h4>
+           Tel: <%tel%>
+           <br>Fax: <%fax%>
+           </h4>
+         </td>
+       </tr>
+       
+       <tr>
+         <th colspan=3>
+           <h4>L A G E R L I S T E</h4>
+         </th>
+       </tr>
+      </table>
+    </td>
+  </tr>
+
+  <tr>
+    <td>&nbsp;</td>
+
+    <td>
+      <table width=100% cellspacing=0 cellpadding=0>
+       <tr bgcolor=000000>
+         <th align=left width=50%><font color=ffffff>Absender</th>
+         <th align=left width=50%><font color=ffffff>Lieferanschrift</th>
+       </tr>
+
+       <tr valign=top>
+         <td><%name%>
+         <br><%street%>
+         <br><%zipcode%>
+         <br><%city%>
+         <br><%country%>
+         <br>
+
+         <%if contact%>
+         <br>Kontakt: <%contact%>
+         <%end contact%>
+
+         <%if vendorphone%>
+         <br>Tel: <%vendorphone%>
+         <%end vendorphone%>
+
+         <%if vendorfax%>
+         <br>Fax: <%vendorfax%>
+         <%end vendorfax%>
+
+         <%if email%>
+         <br><%email%>
+         <%end email%>
+         
+         </td>
+         
+         <td><%shiptoname%>
+         <br><%shiptostreet%>
+         <br><%shiptozipcode%>
+         <br><%shiptocity%>
+         <br><%shiptocountry%>
+
+         <br>
+         <%if shiptocontact%>
+         <br>Kontakt: <%shiptocontact%>
+         <%end shiptocontact%>
+         
+         <%if shiptophone%>
+         <br>Tel: <%shiptophone%>
+         <%end shiptophone%>
+
+         <%if shiptofax%>
+         <br>Fax: <%shiptofax%>
+         <%end shiptofax%>
+         </td>
+       </tr>
+      </table>
+    </td>
+  </tr>
+
+  <tr height=5></tr>
+
+  <tr>
+    <td>&nbsp;</td>
+
+    <td>
+      <table width=100% border=1>
+       <tr>
+         <th width=17% align=left nowrap>BestellNr. #</th>
+         <th width=17% align=left nowrap>Datum</th>
+         <th width=17% align=left nowrap>Kontakt</th>
+         <%if warehouse%>
+         <th width=17% align=left nowrap>Lager</th>
+         <%end warehouse%>
+         <th width=17% align=left>Versandort</th>
+         <th width=15% align=left>Lieferung durch</th>
+       </tr>
+
+       <tr>
+         <td><%ordnumber%>&nbsp;</td>
+
+         <%if shippingdate%>
+         <td><%shippingdate%></td>
+         <%end shippingdate%>
+
+         <%if not shippingdate%>
+         <td><%orddate%></td>
+         <%end shippingdate%>
+
+         <td><%employee%>&nbsp;</td>
+
+         <%if warehouse%>
+         <td><%warehouse%></td>
+         <%end warehouse%>
+
+         <td><%shippingpoint%>&nbsp;</td>
+         <td><%shipvia%>&nbsp;</td>
+       </tr>
+      </table>
+    </td>
+  </tr>
+
+  <tr>
+    <td>&nbsp;</td>
+
+    <td>
+      <table width=100%>
+       <tr bgcolor=000000>
+         <th align=left><font color=ffffff>Pos</th>
+         <th align=left><font color=ffffff>ArtNr.</th>
+         <th align=left><font color=ffffff>Beschreibung</th>
+         <th><font color=ffffff>Seriennummer</th>
+         <th>&nbsp;</th>
+         <th><font color=ffffff>Menge</th>
+         <th><font color=ffffff>Erh</th>
+         <th>&nbsp;</th>
+         <th><font color=ffffff>Lagerplatz</th>
+       </tr>
+
+       <%foreach number%>
+       <tr valign=top>
+         <td><%runningnumber%></td>
+         <td><%number%></td>
+         <td><%description%></td>
+         <td><%serialnumber%></td>
+         <td><%deliverydate%></td>
+         <td align=right><%qty%></td>
+         <td align=right><%ship%></td>
+         <td><%unit%></td>
+         <td><%bin%></td>
+       </tr>
+       <%end number%>
+
+      </table>
+    </td>
+  </tr>
+
+  <tr>
+    <td>&nbsp;</td>
+
+    <td><hr noshade></td>
+  </tr>
+
+</table>
+
diff --git a/templates/print/marei/bin_list.tex b/templates/print/marei/bin_list.tex
new file mode 100644 (file)
index 0000000..7907ebe
--- /dev/null
@@ -0,0 +1,87 @@
+\documentclass[twoside,parskip=half-]{scrartcl}
+\usepackage[reffields,backaddress=false,addrfield=topaligned,nofooter]{kiviletter}
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+\KOMAoptions{fontsize=10pt}
+\begin{document}
+
+\setkomavar{title}{\lagerliste}
+
+\setkomavar{firsthead}{
+  \normalsize
+  \noindent\begin{tabular}[t]{@{}l@{}}
+    <%company%>\strut\\
+    <%address%>
+  \end{tabular}
+  \hfill
+  \begin{tabular}[t]{rr@{}}
+    Tel & < %tel%>\\
+    Fax & < %fax%>%
+  \end{tabular}
+  \rule{\linewidth}{\heavyrulewidth}
+}
+
+\makeatletter
+\setkomavar{location}{
+  \backaddr@format{\scriptsize\usekomafont{backaddress}%
+    \strut\lieferanschrift
+  }
+  \par\medskip\setlength{\parskip}{\z@}
+  \normalsize
+  <%shiptoname%>\par
+  <%if shiptocontact%> <%shiptocontact%><%end if%>\par
+  <%shiptodepartment_1%>\par
+  <%shiptodepartment_2%>\par
+  <%shiptostreet%>\par
+  <%shiptozipcode%> <%shiptocity%>%
+}
+\makeatother
+
+\begin{letter}{
+<%name%>\ifhmode\\\fi
+<%street%>\ifhmode\\\fi
+<%zipcode%> <%city%>\ifhmode\\\fi
+<%country%>
+}
+
+\opening{}
+
+\begin{SimpleTabular}[colspec=*6X,headline={\bfseries\bestellnummer&\bfseries\datum&\bfseries\kontakt
+        <%if warehouse%>%
+        &\bfseries\lager%
+        <%end warehouse%>%
+        &\bfseries\lagerplatz&\bfseries\lieferungMit}]
+
+  <%ordnumber%>%
+  &%
+  <%if shippingdate%>%
+  <%shippingdate%>%
+  <%end shippingdate%>%
+  <%if not shippingdate%>%
+  <%orddate%>%
+  <%end shippingdate%>%
+  & <%employee%>%
+  <%if warehouse%>%
+  & <%warehouse%>%
+  <%end warehouse%>%
+  & <%shippingpoint%> & <%shipvia%> \\
+\end{SimpleTabular}
+
+\bigskip
+
+\begin{SimpleTabular}[colspec=rlXllrrll,headline={\bfseries\position&\bfseries\nummer&\bfseries\beschreibung&\bfseries\seriennummer & &\bfseries\menge&\bfseries\erh&&\bfseries\lagerplatz}]
+  <%foreach number%>%
+  <%runningnumber%> & <%number%> & <%description%> & <%serialnumber%> &
+  <%deliverydate%> & <%qty%> & <%ship%> & <%unit%> & <%bin%> \\
+  <%end number%>%
+\end{SimpleTabular}
+
+\end{letter}
+
+\end{document}
+
diff --git a/templates/print/marei/check.tex b/templates/print/marei/check.tex
new file mode 100644 (file)
index 0000000..ad428f7
--- /dev/null
@@ -0,0 +1,54 @@
+\documentclass[paper=a4]{scrartcl}
+\usepackage[reffields,backaddress=false]{kiviletter}
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+\KOMAoptions{fontsize=9pt}
+
+
+\setplength{firstheadvpos}{4cm}
+
+\setkomavar{firsthead}{
+  \noindent\begin{tabular}[t]{@{}l@{}}
+    <%company%>\strut\\
+    <%address%>
+  \end{tabular}
+  \hfill
+  <%source%>\par
+  \medskip
+  <%text\_amount%> \dotfill <%decimal%>/100 \par\smallskip
+  \hfill <%datepaid%> \hspace{2cm}\strut<%amount%>
+}
+
+
+\begin{document}
+
+\begin{letter}{
+<%name%>\ifhmode\\\fi
+<%street%>\ifhmode\\\fi
+<%zipcode%> <%city%>\ifhmode\\\fi
+<%country%>%
+}
+
+\opening{<%company%>}
+\pagestyle{empty}
+
+<%name%> \hfill <%datepaid%> \hfill <%source%>%
+
+\begin{SimpleTabular}[colspec=lXrr,headline={\bfseries\rechnung&\bfseries\ausgestellt&\bfseries\faellig&\bfseries\verrechnet}]
+  <%foreach invnumber%>%
+  <%invnumber%> & <%invdate%> & <%due%> & <%paid%> \\
+  <%end invnumber%>%
+\end{SimpleTabular}
+
+\end{letter}
+
+\end{document}
+
diff --git a/templates/print/marei/credit_note.tex b/templates/print/marei/credit_note.tex
new file mode 100644 (file)
index 0000000..b764c1d
--- /dev/null
@@ -0,0 +1,125 @@
+\documentclass[paper=a4,fontsize=10pt]{scrartcl}
+\usepackage{kiviletter}
+
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+
+% laufende Kopfzeile:
+\ourhead{\kundennummer}{<%customernumber%>}{\gutschrift}{<%invnumber%>}{<%invdate%>}
+
+\setkomavar*{date}{\datum}
+\setkomavar{date}{<%invdate%>}
+\setkomavar{customer}{<%customernumber%>}
+\setkomavar{fromname}{<%employee_name%>}
+\setkomavar{fromphone}{<%employee_tel%>}
+\setkomavar{fromemail}{<%employee_email%>}
+\setkomavar{title}{
+  \gutschrift~
+  \nr ~<%invnumber%>%
+}
+
+<%if invnumber_for_credit_note%>%
+  \setkomavar*{myref}{\fuerRechnung}
+  \setkomavar{myref}{<%invnumber_for_credit_note%>}
+<%end if%>%
+
+\setkomavar{transaction}{<%transaction_description%>}
+<%if shiptoname%>%
+\makeatletter
+\begin{lrbox}\shippingAddressBox
+  \parbox{\useplength{toaddrwidth}}{
+    \backaddr@format{\scriptsize\usekomafont{backaddress}%
+      \strut\abweichendeLieferadresse
+    }
+    \par\smallskip
+    \setlength{\parskip}{\z@}
+    \par
+    \normalsize
+    <%shiptoname%>\par
+    <%if shiptocontact%> <%shiptocontact%><%end if%>\par
+    <%shiptodepartment_1%>\par
+    <%shiptodepartment_2%>\par
+    <%shiptostreet%>\par
+    <%shiptozipcode%> <%shiptocity%>%
+  }
+\end{lrbox}
+\makeatother
+<%end if%>%
+
+\begin{document}
+
+\begin{letter}{
+<%name%>\strut\\
+<%if department_1%><%department_1%>\\<%end if%>%
+<%if department_2%><%department_2%>\\<%end if%>%
+<%cp_givenname%> <%cp_name%>\strut\\
+<%street%>\strut\\
+<%zipcode%> <%city%>\strut\\
+<%country%> \strut
+}
+
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\opening{
+\Ifstr{<%cp_name%>}{}
+{<%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}
+  {
+    \Ifstr{<%cp_gender%>}{f}
+        {\anredefrau}
+        {\anredeherr}
+      <%cp_title%> <%cp_name%>,
+    }
+  }
+\thispagestyle{kivitendo.letter.first}
+
+
+\gutschriftformel
+
+<%if notes%>%
+<%notes%>%
+\vspace{0.5cm}
+<%end if%>%
+
+
+\begin{PricingTabular*}%
+  % eigentliche Tabelle%
+  \FakeTable{%
+  <%foreach number%>%
+  <%runningnumber%> &%
+  <%number%> &%
+  \textbf{<%description%>}%
+  <%if longdescription%>\ExtraDescription{<%longdescription%>}<%end longdescription%>%
+  <%if serialnumber%>\ExtraDescription{\seriennummer: <%serialnumber%>}<%end serialnumber%>%
+  <%if ean%>\ExtraDescription{\ean: <%ean%>}<%end ean%>%
+  <%if projectnumber%>\ExtraDescription{\projektnummer: <%projectnumber%>}<%end projectnumber%>%
+  &%
+  <%qty%> <%unit%> &%
+  <%sellprice%>&%
+  \Ifstr{<%p_discount%>}{0}{}{\sffamily\scriptsize{(-<%p_discount%>\,\%)}}%
+    <%linetotal%>\tabularnewline%
+    <%end number%>%
+  }%
+  \begin{PricingTotal}%
+    % Tabellenende letzte Seite
+    \nettobetrag & <%subtotal%>\\%
+    <%foreach tax%>%
+    <%taxdescription%> & <%tax%>\\%
+    <%end tax%>%
+    \bfseries\schlussbetrag &  \bfseries <%invtotal%>\\%
+  \end{PricingTotal}%
+\end{PricingTabular*}
+
+
+\closing{\gruesse}
+
+\end{letter}
+
+\end{document}
diff --git a/templates/print/marei/deutsch.tex b/templates/print/marei/deutsch.tex
new file mode 100644 (file)
index 0000000..47eafa1
--- /dev/null
@@ -0,0 +1,168 @@
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%standardphrasen und schnipsel in deutsch     %
+%dient als vorlage für alle anderen sprachen  %
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+
+\newcommand{\anrede} {Sehr geehrte Damen und Herren,}
+\newcommand{\anredefrau} {Sehr geehrte Frau}
+\newcommand{\anredeherr} {Sehr geehrter Herr}
+
+
+\newcommand{\nr} {Nr.}
+\newcommand{\datum} {Datum}
+\newcommand{\kundennummer} {Kunden-Nr.}
+\newcommand{\ansprechpartner} {Ansprechpartner}
+\newcommand{\bearbeiter} {Bearbeiter}
+\newcommand{\gruesse} {Mit freundlichen Grüßen}
+\newcommand{\vom} {vom}
+\newcommand{\von} {von}
+\newcommand{\seite} {Seite}
+\newcommand{\uebertrag} {Übertrag}
+
+
+\newcommand{\position} {Pos.}
+\newcommand{\artikelnummer} {Art.-Nr.}
+\newcommand{\bild} {Bild}
+\newcommand{\keinbild} {kein Bild}
+\newcommand{\menge} {Menge}
+\newcommand{\bezeichnung} {Bezeichnung}
+\newcommand{\seriennummer}{Seriennummer}
+\newcommand{\ean}{EAN}
+\newcommand{\projektnummer}{Projektnummer}
+\newcommand{\charge}{Charge}
+\newcommand{\mhd}{MHD}
+\newcommand{\einzelpreis} {E-Preis}
+\newcommand{\gesamtpreis} {G-Preis}
+\newcommand{\nettobetrag} {Nettobetrag}
+\newcommand{\schlussbetrag} {Gesamtbetrag}
+
+\newcommand{\weiteraufnaechsterseite} {weiter auf der nächsten Seite \ldots}
+
+\newcommand{\zahlung} {Zahlungsbedingungen}
+\newcommand{\lieferung} {Lieferbedingungen}
+\newcommand{\textTelefon} {Tel.}
+\newcommand{\textEmail} {E-Mail}
+\newcommand{\textFax} {Fax}
+
+% angebot (sales_quotion)
+\newcommand{\angebot} {Angebot}
+\newcommand{\angebotsformel} {gerne unterbreiten wir Ihnen folgendes Angebot:}
+\newcommand{\angebotdanke} {Wir danken für Ihre Anfrage und hoffen, Ihnen hiermit ein interessantes Angebot gemacht zu haben.}
+\newcommand{\angebotgueltig} {Das Angebot ist freibleibend gültig bis zum}% Danach wird das Datum eingefügt, falls das grammatisch nicht funktionieren sollte müssen wir eine ausnahme für die sprache definieren
+\newcommand{\angebotfragen} {Sollten Sie noch Fragen oder Änderungswünsche haben, können Sie uns gerne jederzeit unter den unten genannten Telefonnummern oder E-Mail-Adressen kontaktieren.}
+\newcommand{\angebotagb} {Bei der Durchführung des Auftrags gelten unsere AGB, die wir Ihnen gerne zuschicken.}
+\newcommand{\auftragerteilt}{Auftrag erteilt:}
+\newcommand{\angebotortdatum}{Wir nehmen das vorstehende Angebot an.}
+\newcommand{\abweichendeLieferadresse}{abweichende Lieferadresse}
+
+% auftragbestätigung (sales_order)
+\newcommand{\auftragsbestaetigung} {Auftragsbestätigung}
+\newcommand{\auftragsnummer} {Auftrags-Nr.}
+\newcommand{\ihreBestellnummer} {Ihre Bestellnummer}
+\newcommand{\auftragsformel} {hiermit bestätigen wir Ihnen folgende Bestellpositionen:}
+\newcommand{\lieferungErfolgtAm} {Die Lieferung erfolgt am} %Danach wird das Datum eingefügt, falls das grammatisch nicht funktionieren sollte müssen wir eine ausnahme für die sprache definieren
+\newcommand{\auftragpruefen} {Bitte kontrollieren Sie alle Positionen auf Übereinstimmung mit Ihrer Bestellung! Teilen Sie Abweichungen innerhalb von 3 Tagen mit!}
+\newcommand{\proformarechnung} {Proforma Rechnung}
+\newcommand{\nurort} {Ort}
+\newcommand{\den} {den}
+\newcommand{\unterschrift} {Unterschrift}
+\newcommand{\stempel} {ggf. Stempel}
+
+% lieferschein (sales_delivery_order)
+\newcommand{\lieferschein} {Lieferschein}
+
+% rechnung (invoice)
+\newcommand{\rechnung} {Rechnung}
+\newcommand{\rechnungskopie} {Rechnungskopie}
+\newcommand{\rechnungsdatum} {Rechnungsdatum}
+\newcommand{\ihrebestellung} {Ihre Bestellung}
+\newcommand{\lieferdatum} {Lieferdatum}
+\newcommand{\rechnungsformel} {für unsere Leistungen erlauben wir uns, folgende Positionen in Rechnung zu stellen:}
+\newcommand{\zwischensumme} {Zwischensumme}
+\newcommand{\leistungsdatumGleichRechnungsdatum} {Das Leistungsdatum entspricht, soweit nicht anders angegeben, dem Rechnungsdatum.}
+\newcommand{\leistungsdatum} {Leistungsdatum}
+\newcommand{\unserebankverbindung} {Unsere Bankverbindung}
+\newcommand{\textKontonummer} {Kontonummer:}
+\newcommand{\textBank} {bei der}
+\newcommand{\textBankleitzahl} {BLZ:}
+\newcommand{\textBic} {BIC:}
+\newcommand{\textIban} {IBAN:}
+\newcommand{\unsereustid} {Unsere USt-Identifikationsnummer lautet}
+\newcommand{\ihreustid} {Ihre USt-Identifikationsnummer:}
+\newcommand{\steuerfreiEU} {Sonstige Leistungen Steuerschuldnerschaft des Leistungsempfängers. Reverse Charge}
+\newcommand{\steuerfreiAUS} {Steuerfreie Lieferung ins außereuropäische Ausland.}
+
+\newcommand{\textUstid} {UStId:}
+
+% anzahlungsrechnung (invoice_for_advance_payment)
+\newcommand{\anzahlungsrechnung} {Anzahlungsrechnung}
+\newcommand{\schlussrechnung} {Schlussrechnung}
+\newcommand{\ust} {USt}
+\newcommand{\abzueglichAnzahlungsrechnungen} {Abzüglich folgender Anzahlungsrechnungen}
+\newcommand{\rechnungsbetrag} {Rechnungsbetrag}
+
+% gutschrift (credit_note)
+\newcommand{\gutschrift} {Gutschrift}
+\newcommand{\fuerRechnung} {für Rechnung}
+\newcommand{\gutschriftformel} {wir erlauben uns, Ihnen folgende Positionen gutzuschreiben:}
+
+% sammelrechnung (statement)
+\newcommand{\sammelrechnung} {Sammelrechnung}
+\newcommand{\sammelrechnungsformel} {bitte nehmen Sie zur Kenntnis, dass folgende Rechnungen unbeglichen sind:}
+\newcommand{\faellig} {Fälligkeit}
+\newcommand{\aktuell} {aktuell}
+\newcommand{\asDreissig} {30}
+\newcommand{\asSechzig} {60}
+\newcommand{\asNeunzig} {90+}
+
+% zahlungserinnerung (Mahnung)
+\newcommand{\mahnung} {Zahlungserinnerung}
+\newcommand{\mahnungsformel} {man kann seine Augen nicht überall haben - offensichtlich haben Sie übersehen, die folgenden Rechnungen zu begleichen:}
+\newcommand{\beruecksichtigtBis} {Zahlungseingänge sind nur berücksichtigt bis zum}
+\newcommand{\schonGezahlt} {Sollten Sie zwischenzeitlich bezahlt haben, betrachten Sie diese Zahlungserinnerung bitte als gegenstandslos.}
+
+% zahlungserinnerung_invoice (Rechnung zur Mahnung)
+\newcommand{\mahnungsrechnungsformel} {hiermit stellen wir Ihnen zu o.g. \mahnung \ folgende Posten in Rechnung:}
+\newcommand{\posten} {Posten}
+\newcommand{\betrag} {Betrag}
+\newcommand{\bitteZahlenBis} {Bitte begleichen Sie diese Forderung bis zum}
+
+% anfrage (request_quotion)
+\newcommand{\anfrage} {Anfrage}
+\newcommand{\anfrageformel} {bitte nennen Sie uns für folgende Artikel Preis und Liefertermin:}
+\newcommand{\anfrageBenoetigtBis} {Wir benötigen die Lieferung bis zum}  %Danach wird das Datum eingefügt, falls das grammatisch nicht funktionieren sollte müssen wir eine ausnahme für die sprache definieren
+\newcommand{\anfragedanke} {Im Voraus besten Dank für Ihre Bemühungen.}
+
+% bestellung/auftrag (purchase_order)
+\newcommand{\bestellung} {Bestellung}
+\newcommand{\unsereBestellnummer} {Unsere Bestellnummer}
+\newcommand{\bestellformel} {hiermit bestellen wir verbindlich folgende Positionen:}
+
+% einkaufslieferschein (purchase_delivery_order)
+\newcommand{\einkaufslieferschein} {Eingangslieferschein}
+\newcommand{\beistelllieferschein} {Beistell-Lieferschein}
+
+% Brief/letter
+\newcommand{\ihrzeichen}{Ihr Zeichen}
+\newcommand{\betreff}{Betreff}
+
+%check
+\newcommand*{\ausgestellt}{Ausgestellt}
+\newcommand*{\verrechnet}{Verrechnet}
+
+%lagerliste/bin_list
+\newcommand*{\lagerliste}{Lagerliste}
+\newcommand*{\lieferanschrift}{Lieferanschrift}
+\newcommand*{\bestellnummer}{Bestellnummer}
+\newcommand*{\kontakt}{Kontakt}
+\newcommand*{\lager}{Lager}
+\newcommand*{\lagerplatz}{Lagerplatz}
+\newcommand*{\lieferungMit}{Lieferung mit}
+\newcommand*{\nummer}{Nummer}
+\newcommand*{\beschreibung}{Beschreibung}
+\newcommand*{\erh}{Erh}
+
+%sammelliste
+\newcommand*{\sammelliste}{Sammelliste}
+\newcommand*{\lagerausgang}{Lagerausgang}
diff --git a/templates/print/marei/emptyPage.pdf b/templates/print/marei/emptyPage.pdf
new file mode 100644 (file)
index 0000000..5cb37c0
Binary files /dev/null and b/templates/print/marei/emptyPage.pdf differ
diff --git a/templates/print/marei/english.tex b/templates/print/marei/english.tex
new file mode 100644 (file)
index 0000000..f757c78
--- /dev/null
@@ -0,0 +1,147 @@
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%standardphrasen und schnipsel in englisch    %
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+
+\newcommand{\anrede} {Dear Sirs,}
+\newcommand{\anredefrau} {Dear Ms.}
+\newcommand{\anredeherr} {Dear Mr.}
+
+
+\newcommand{\nr} {No.}
+\newcommand{\datum} {Date}
+\newcommand{\kundennummer} {Customer-No.}
+\newcommand{\ansprechpartner} {Contact person}
+\newcommand{\bearbeiter} {Employee}
+\newcommand{\gruesse} {Sincerely yours, }
+\newcommand{\vom} {from}
+\newcommand{\von} {from}
+\newcommand{\seite} {page}
+\newcommand{\uebertrag} {amount carried over}
+
+
+\newcommand{\position} {Pos.}
+\newcommand{\artikelnummer} {Part No.}
+\newcommand{\bild} {Picture}
+\newcommand{\keinbild} {n/a}
+\newcommand{\menge} {Qty}
+\newcommand{\bezeichnung} {Description}
+\newcommand{\seriennummer}{Serial No.}
+\newcommand{\ean}{EAN}
+\newcommand{\projektnummer}{Project No.}
+\newcommand{\charge}{Charge}
+\newcommand{\mhd}{Best before}
+\newcommand{\einzelpreis} {Price}
+\newcommand{\gesamtpreis} {Amount}
+\newcommand{\nettobetrag} {Net amount}
+\newcommand{\schlussbetrag} {Total}
+
+\newcommand{\weiteraufnaechsterseite} {to be continued on next page  \ldots}
+
+\newcommand{\zahlung} {Payment terms:}
+\newcommand{\lieferung} {Delivery terms:}
+\newcommand{\textTelefon} {Tel.:}
+\newcommand{\textEmail} {Email:}
+\newcommand{\textFax} {Fax:}
+
+% angebot (sales_quotion)
+\newcommand{\angebot} {Quotation}
+\newcommand{\angebotsformel} {we are pleased to make the following offer:}
+\newcommand{\angebotdanke} {We thank you for your request and look forward to receiving your order.}
+\newcommand{\angebotgueltig} {This offer is valid until}% Danach wird das Datum eingefügt, falls das grammatisch nicht funktionieren sollte müssen wir eine Ausnahme für die Sprache definieren
+\newcommand{\angebotfragen} {If you have any questions do not hesitate to conatct us.}
+\newcommand{\angebotagb} {Our general terms and conditions (AGB) apply. We will send them to you on request.}
+\newcommand{\auftragerteilt}{Order confirmed:}
+\newcommand{\angebotortdatum}{We hereby accept this offer.}
+\newcommand{\abweichendeLieferadresse}{alternate delivery address}
+
+% auftragbestätigung (sales_order)
+\newcommand{\auftragsbestaetigung} {Order}
+\newcommand{\auftragsnummer} {Order No.}
+\newcommand{\ihreBestellnummer} {Your reference no.}
+\newcommand{\auftragsformel} {We hereby confirm your order for the following items:}
+\newcommand{\lieferungErfolgtAm} {Your items will be delivered on:} %Danach wird das Datum eingefügt, falls das grammatisch nicht funktionieren sollte müssen wir eine ausnahme für die sprache definieren
+\newcommand{\auftragpruefen} {Please check that all items correspond to your order. Please tell us of any deviations within 3 days.}
+\newcommand{\proformarechnung} {Proforma invoice}
+\newcommand{\nurort} {Place}
+\newcommand{\den} {Date}
+\newcommand{\unterschrift} {Signature}
+\newcommand{\stempel} {Company stamp}
+
+% lieferschein (sales_delivery_order)
+\newcommand{\lieferschein} {Delivery order}
+
+% rechnung (invoice)
+\newcommand{\rechnung} {Invoice}
+\newcommand{\rechnungskopie} {Invoice copy}
+\newcommand{\rechnungsdatum} {Invoice date}
+\newcommand{\ihrebestellung} {Your order}
+\newcommand{\lieferdatum} {Delivery date}
+\newcommand{\rechnungsformel} {we invoice you for the following items:}
+\newcommand{\zwischensumme} {Subtotal}
+\newcommand{\leistungsdatumGleichRechnungsdatum} {The date of service corresponds to that of the invoice.}
+\newcommand{\leistungsdatum} {Service Date}
+\newcommand{\unserebankverbindung} {Our bank details}
+\newcommand{\textKontonummer} {Account no.:}
+\newcommand{\textBank} {at}
+\newcommand{\textBankleitzahl} {Bank code:}
+\newcommand{\textBic} {BIC:}
+\newcommand{\textIban} {IBAN:}
+\newcommand{\unsereustid} {Our VAT number is}
+\newcommand{\ihreustid} {Your VAT number:}
+\newcommand{\steuerfreiEU} {VAT-exempt intra-community delivery. Reverse Charge.}
+\newcommand{\steuerfreiAUS} {VAT-exempt delivery for outside the EU.}
+
+\newcommand{\textUstid} {VAT number:}
+
+% anzahlungsrechnung (invoice_for_advance_payment)
+\newcommand{\anzahlungsrechnung} {Invoice for advance payment}
+\newcommand{\schlussrechnung} {Final Invoice}
+\newcommand{\ust} {VAT}
+\newcommand{\abzueglichAnzahlungsrechnungen} {Minus following invoices for advance payment}
+\newcommand{\rechnungsbetrag} {Invoice amount}
+
+% gutschrift (credit_note)
+\newcommand{\gutschrift} {Credit note}
+\newcommand{\fuerRechnung} {for invoice}
+\newcommand{\gutschriftformel} {we credit you with the following items:}
+
+% sammelrechnung (statement)
+\newcommand{\sammelrechnung} {Statement}
+\newcommand{\sammelrechnungsformel} {please note that the following invoices are outstanding:}
+\newcommand{\faellig} {Due}
+\newcommand{\aktuell} {Current}
+\newcommand{\asDreissig} {30}
+\newcommand{\asSechzig} {60}
+\newcommand{\asNeunzig} {90+}
+
+% zahlungserinnerung (Mahnung)
+\newcommand{\mahnung} {Payment reminder}
+\newcommand{\mahnungsformel} {our records show that the following invoices are still outstanding:}
+\newcommand{\beruecksichtigtBis} {We have taken into account payments received up until}
+\newcommand{\schonGezahlt} {If you have already paid in the meantime, please ignore this payment reminder.}
+
+% zahlungserinnerung_invoice (Rechnung zur Mahnung)
+\newcommand{\mahnungsrechnungsformel} {for the above-mentioned payment reminder we charge you the following fees:}
+\newcommand{\posten} {Item}
+\newcommand{\betrag} {Amount}
+\newcommand{\bitteZahlenBis} {Please settle the outstanding balance by }
+
+% anfrage (request_quotion)
+\newcommand{\anfrage} {Quotation request}
+\newcommand{\anfrageformel} {please quote us prices and delivery dates for the following items:}
+\newcommand{\anfrageBenoetigtBis} {We need the delivery by}  %Danach wird das Datum eingefügt, falls das grammatisch nicht funktionieren sollte müssen wir eine ausnahme für die sprache definieren
+\newcommand{\anfragedanke} {Thank you in advance.}
+
+% bestellung/auftrag (purchase_order)
+\newcommand{\bestellung} {Order}
+\newcommand{\unsereBestellnummer} {Our order number}
+\newcommand{\bestellformel} {we hereby order the following items:}
+
+% einkaufslieferschein (purchase_delivery_order)
+\newcommand{\einkaufslieferschein} {Purchase delivery order}
+\newcommand{\beistelllieferschein} {Supplier Delivery Order}
+
+% Brief/letter
+\newcommand{\ihrzeichen}{Your reference}
+\newcommand{\betreff}{Subject}
diff --git a/templates/print/marei/final_invoice.tex b/templates/print/marei/final_invoice.tex
new file mode 120000 (symlink)
index 0000000..b6a6ad8
--- /dev/null
@@ -0,0 +1 @@
+invoice.tex
\ No newline at end of file
diff --git a/templates/print/marei/firma/briefkopf.png b/templates/print/marei/firma/briefkopf.png
new file mode 100644 (file)
index 0000000..bc29d17
Binary files /dev/null and b/templates/print/marei/firma/briefkopf.png differ
diff --git a/templates/print/marei/firma/chf_account.tex b/templates/print/marei/firma/chf_account.tex
new file mode 100644 (file)
index 0000000..80e08d5
--- /dev/null
@@ -0,0 +1,6 @@
+\newcommand{\currency}{CHF}
+\newcommand{\kontonummer}{4004 283 800}
+\newcommand{\bank}{GLS Bank eG}
+\newcommand{\bankleitzahl}{430 609 67}
+\newcommand{\iban}{DE50430609674071953800}
+\newcommand{\bic}{GENODEM1GLS}
diff --git a/templates/print/marei/firma/euro_account.tex b/templates/print/marei/firma/euro_account.tex
new file mode 100644 (file)
index 0000000..2cecb8e
--- /dev/null
@@ -0,0 +1,6 @@
+\newcommand{\currency}{€}
+\newcommand{\kontonummer}{4071953800}
+\newcommand{\bank}{GLS Bank eG}
+\newcommand{\bankleitzahl}{430 609 67}
+\newcommand{\iban}{DE50430609674071953800}
+\newcommand{\bic}{GENODEM1GLS}
diff --git a/templates/print/marei/firma/ident.tex b/templates/print/marei/firma/ident.tex
new file mode 100644 (file)
index 0000000..3c40974
--- /dev/null
@@ -0,0 +1,24 @@
+\newcommand{\telefon} {++49 228 92 98 2012}
+\newcommand{\fax} {}
+\newcommand{\firma} {kivitendo GmbH}
+\newcommand{\strasse} {Kölnstr. 311}
+\newcommand{\ort} {53117 Bonn}
+\newcommand{\ustid} {DE292363254}
+\newcommand{\finanzamt} {Finanzamt Bonn-Innenstadt}
+\newcommand{\email} {information@kivitendo-premium.de}
+\newcommand{\homepage} {http://www.kivitendo-premium.de}
+
+
+%Überschreiben des Default-Briefkopfes in der insettings.tex
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%Setze Briefkopf-logo falls vorhanden
+%\setkomavar{fromlogo}{\includegraphics[width=.25\linewidth]{\identpath/briefkopf}}
+
+%%Ganzseitiger Briefbogen als Hintergrund:
+%\DeclareNewLayer[page,background,
+%      contents={\includegraphics{Briefbogen}} %Hier muss der Dateinamen und ggf. die Bildgröße angepasst werden, falls es abweichende Maße vom Papierformat hat.
+%]{background}
+%\AddLayersToPageStyle{kivitendo.letter.first}{background}%Hintergrund für die erste Seite aktivieren
+%\AddLayersToPageStyle{kivitendo.letter}{background}% Hintergrund für die übrigen Briefseiten aktivieren.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\ No newline at end of file
diff --git a/templates/print/marei/firma/kivitendo.png b/templates/print/marei/firma/kivitendo.png
new file mode 100644 (file)
index 0000000..e76c894
Binary files /dev/null and b/templates/print/marei/firma/kivitendo.png differ
diff --git a/templates/print/marei/firma/steigmann.png b/templates/print/marei/firma/steigmann.png
new file mode 100644 (file)
index 0000000..1f6bc5e
Binary files /dev/null and b/templates/print/marei/firma/steigmann.png differ
diff --git a/templates/print/marei/firma/usd_account.tex b/templates/print/marei/firma/usd_account.tex
new file mode 100644 (file)
index 0000000..01b8fc0
--- /dev/null
@@ -0,0 +1,6 @@
+\newcommand{\currency}{\$}
+\newcommand{\kontonummer}{4004 283 800}
+\newcommand{\bank}{GLS Bank eG}
+\newcommand{\bankleitzahl}{430 609 67}
+\newcommand{\bic}{DE87430609674004283800}
+\newcommand{\iban}{GENODEM1GLS}
diff --git a/templates/print/marei/ic_supply.tex b/templates/print/marei/ic_supply.tex
new file mode 100644 (file)
index 0000000..6539b11
--- /dev/null
@@ -0,0 +1,72 @@
+\documentclass[paper=a4,fontsize=10pt]{scrartcl}
+\usepackage{kiviletter}
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+
+\usepackage{ulem}
+\usepackage{hyperref}
+\usepackage{xstring}
+\begin{document}
+
+\begin{Form}
+\large
+Bestätigung über das Gelangen des Gegenstands einer innergemeinschaftlichen Lieferung
+in einen anderen EU-Mitgliedstaat (Gelangensbestätigung)
+\vspace{0.4cm}
+
+{\color{purple} Bitte unterschreiben und faxen/mailen an:
+\begin{center} <%employee_fax%> / <%employee_email%> \end{center}}
+\normalsize
+\vspace{0.4cm}
+<%name%>, <%street%>, <%zipcode%> <%city%>, <%country%>\hspace*{\fill}\\
+\TextField[name=department, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(Name und Anschrift des Abnehmers der innergemeinschaftlichen Lieferung, ggf. E-Mail-Adresse)}
+
+\vspace{0.4cm}
+
+Hiermit bestätige ich als Abnehmer, dass ich folgenden Gegenstand / dass folgender Gegenstand \textsuperscript{1)} einer
+innergemeinschaftlichen Lieferung\\
+
+
+\TextField[name=qty, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(Menge des Gegenstands der Lieferung)}\\
+
+\TextField[name=desc, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(handelsübliche Bezeichnung, bei Fahrzeugen zusätzlich die Fahrzeug-Identifikationsnummer)}\\
+
+im\\
+
+\uline{ \StrGobbleLeft{<%reqdate%>}{3} \hspace*{\fill}}\\
+  {\color{gray}(Monat und Jahr des Erhalts des Liefergegenstands im Mitgliedstaat, in den der Liefergegenstand gelangt ist, wenn der liefernde Unternehmer den Liefergegenstand befördert oder versendet hat oder wenn der Abnehmer den Liefergegenstand versendet hat)}\\
+
+
+\TextField[name=delivery, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(Monat und Jahr des Endes der Beförderung, wenn der Abnehmer den Liefergegenstand selbst befördert hat)}\\
+
+in / nach \textsuperscript{1)}\\
+
+
+\uline{<%country%>\hspace*{\fill}}\\
+  {\color{gray}(Mitgliedstaat und Ort, wohin der Liefergegenstand im Rahmen einer Beförderung oder Versendung gelangt ist)}\\
+
+erhalten habe / gelangt ist.
+
+\TextField[name=delivery, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(Datum der Ausstellung der Bestätigung)}\\
+
+\uline{\hspace*{\fill}}
+
+{\color{gray}(Unterschrift des Abnehmers oder seines Vertretungsberechtigten sowie Name des Unterzeichnenden in Druckschrift)}\\
+
+\textbf{\textsuperscript{1)} Nichtzutreffendes bitte streichen.}
+\end{Form}
+\end{document}
+
diff --git a/templates/print/marei/ic_supply_EN.tex b/templates/print/marei/ic_supply_EN.tex
new file mode 100644 (file)
index 0000000..5f9516c
--- /dev/null
@@ -0,0 +1,69 @@
+\documentclass[paper=a4,fontsize=10pt]{scrartcl}
+\usepackage{kiviletter}
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+
+\usepackage{ulem}
+\usepackage{hyperref}
+\usepackage{xstring}
+\begin{document}
+
+\begin{Form}
+\large
+Certification of the entry of the object of an intra-Community supply into another EU Member State (Entry Certificate)
+
+\vspace{0.4cm}
+
+{\color{purple} Please sign below and send back to fax-number/mail-address:
+\begin{center} <%employee_fax%> / <%employee_email%> \end{center}}
+
+\normalsize
+\vspace{0.4cm}
+<%name%>, <%street%>, <%zipcode%> <%city%>, <%country%>\hspace*{\fill}\\
+\TextField[name=department, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(Name and address of the customer of the intra-Community supply, e-mail address if applicable)}
+
+
+\vspace{0.4cm}
+I as the customer hereby certify my receipt / the entry \textsuperscript{1)} of the following object of an intra-Community supply\\
+
+\TextField[name=qty, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(Quantity of the object of the supply)}\\
+
+\TextField[name=desc, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(Standard commercial description – in the case of vehicles, including vehicle identification number)}\\
+
+in\\
+
+\uline{ \StrGobbleLeft{<%reqdate%>}{3} \hspace*{\fill}}\\
+  {\color{gray}(Month and year the object of the supply was received in the Member State of entry if the supplying trader transported or dispatched the object of the supply or if the customer dispatched the object of the supply)}\\
+
+\TextField[name=delivery, bordercolor=gray, width=\linewidth]{}\\
+{\color{gray}(Month and year the transportation ended if the customer transported the object of the supply himself or herself)}\\
+
+in / at \textsuperscript{1)}\\
+
+\uline{<%country%>\hspace*{\fill}}\\
+  {\color{gray}(Member State and place of entry as part of the transport or dispatch of the object)}\\
+
+
+% X\TextField[name=delivery, bordercolor=gray, width=\linewidth]{}\\
+\uline{X\hspace*{\fill}}\\
+{\color{gray}(Date of issue of the certificate)}\\
+
+\uline{X\hspace*{\fill}}\\
+{\color{gray}(Signature of the customer or of the authorised representative as well as the signatory’s name in capitals)}\\
+
+\textbf{\textsuperscript{1)} Delete as appropriate.}
+
+\end{Form}
+\end{document}
+
diff --git a/templates/print/marei/images/draft.png b/templates/print/marei/images/draft.png
new file mode 100644 (file)
index 0000000..8181beb
Binary files /dev/null and b/templates/print/marei/images/draft.png differ
diff --git a/templates/print/marei/images/hintergrund_seite1.png b/templates/print/marei/images/hintergrund_seite1.png
new file mode 100644 (file)
index 0000000..28610f4
Binary files /dev/null and b/templates/print/marei/images/hintergrund_seite1.png differ
diff --git a/templates/print/marei/images/hintergrund_seite2.png b/templates/print/marei/images/hintergrund_seite2.png
new file mode 100644 (file)
index 0000000..e4b204b
Binary files /dev/null and b/templates/print/marei/images/hintergrund_seite2.png differ
diff --git a/templates/print/marei/images/schachfiguren.jpg b/templates/print/marei/images/schachfiguren.jpg
new file mode 100644 (file)
index 0000000..d8e9cca
Binary files /dev/null and b/templates/print/marei/images/schachfiguren.jpg differ
diff --git a/templates/print/marei/inheaders.tex b/templates/print/marei/inheaders.tex
new file mode 100644 (file)
index 0000000..88d4bfe
--- /dev/null
@@ -0,0 +1,6 @@
+%Dokumentenklasse für DIN-Briefe auf A4
+\documentclass[paper=a4,fontsize=10pt]{scrartcl}
+\usepackage{kiviletter}
+
+%TODO babel setup
+\endinput
diff --git a/templates/print/marei/insettings.tex b/templates/print/marei/insettings.tex
new file mode 100644 (file)
index 0000000..4174909
--- /dev/null
@@ -0,0 +1,136 @@
+%% insettings.tex
+%% Copyright 2019–2022 Marei Peischl
+\ProvidesFile{insettings.tex}[2022/02/23 Konfigurationsdatei kivitendo ERP]
+% Sprachüberprüfung
+\RequirePackage[english, ngerman]{babel}
+
+\makeatletter
+\Ifstr{\lxlangcode}{EN}{
+  \main@language{english}
+  \input{english.tex}}{
+  \Ifstr{\lxlangcode}{DE}{
+    \main@language{ngerman}
+    \input{deutsch.tex}}{\input{deutsch.tex}}
+} % Ende EN
+
+% Mandanten-/Firmenabhängigkeiten
+
+% Pfad zu firmenspez. Angaben, sofern kein Unterordner mit dem Datenbanknamen des Mandanten in der Vorlage existiert, wird der Unterordner „firma“ verwendet. Der Datenbankname ist ab hier im Makro \identpath gespeichert
+\setupIdentpath{\kivicompany}
+
+%Setze Briefkopf-logo falls vorhanden
+\setkomavar{fromlogo}{\includegraphics[width=.25\linewidth]{\identpath/briefkopf}}
+
+%Ganzseitiger Briefbogen als Hintergrund:
+%\DeclareNewLayer[page,background,
+%  contents={\includegraphics{Briefbogen}} %Hier muss der Dateinamen und ggf. die Bildgröße angepasst werden, falls es abweichende Maße vom Papierformat hat.
+%]{background}
+%\AddLayersToPageStyle{kivitendo.letter.first}{background}%Hintergrund für die erste Seite aktivieren
+%\AddLayersToPageStyle{kivitendo.letter}{background}% Hintergrund für die übrigen Briefseiten aktivieren.
+
+% Lade die Konfiguration aus dem entsprechenden Unterordner
+\input{\identpath/ident.tex}
+
+
+% Währungen/Konten
+% Die Konfiguration bedindet sich in der Datei 
+% \identpath/<euro/chf/usd>_account.tex
+% das optionale Argument ist als euro vorbelegt und gibt die Einstellung an, falls \lxcurrency nicht von kivitendo übergeben wird.
+
+\setupCurrencyConfig[euro]{\identpath}{\lxcurrency}
+
+
+% Befehl f. normale Schriftart und -größe
+
+\KOMAoptions{
+  fontsize=10pt,
+  parskip=half-,% Absatzkennzeichnung durch Abstand statt Einzug
+}
+% Hier ist es auch möglich zusätzliche Schriftarten zu laden.
+% 
+% - Falls pdfLaTeX verwendet wird, findet man unter https://www.tug.org/FontCatalogue/ eine gute *bersicht, wie die Schrifteen geladen werden.
+%
+% In diesem Beispiel wird lediglich auf eine Serifenlose Schriftart umgestellt.
+\renewcommand*{\familydefault}{\sfdefault}
+% - Falls XeLaTeX/LuaLaTeX verwendet wird, kann mit fontspec über den Namen eine Installierte Systemschriftart verwendet werden.
+% \usepackage{fontspec}
+% \setmainfont{Schriftart}
+% ggf. muss dann die Änderung von \familydefault entfernt werden.
+
+% Einstellungen f. Kopf und Fuss
+\pagestyle{kivitendo.letter}
+
+
+% Beginn Anpassungen der Kopfzeile:
+\setkomafont{pagehead}{\scriptsize}
+% Das Standardformat setzt in der Kopfzeile die folgende Reihenfolge:
+%
+% 1. Text f. Kunden- oder Lieferantennummer (oder leer, wenn diese nicht ausgegeben werden soll)
+% 2. Kunden- oder Lieferantennummer (oder leer)
+% 3. Belegname {oder leer}
+% 4. Belegnummer {oder leer}
+% 5. Belegdatum {oder leer}
+% Beispiel: \ourhead{\kundennummer}{<%customernumber%>}{\angebot}{<%quonumber%>}{<%quodate%>}
+% Eine Anpassunge ist über Änderung dieses Makros möglich oder über direktes Befüllen der Felder 
+% % \ifoot{<inhalt innen/links>}\cfoot{<inhalt zentriert>}\ofoot{<inhalt außen/rechts>}
+% dann sollte jedoch darauf geachtet werden, dass das Makro in den einzelnen Vorlagen aufgerufen wird und daher definiert sein sollte. 
+\newcommand{\ourhead}[5] {
+  \chead{
+    \makebox[\textwidth]{
+      \Ifstr{#1}{}{}{#1: #2 \hspace{0.7cm}}
+      #3
+      \Ifstr{#4}{}{}{~\nr: #4}
+      \Ifstr{#5}{}{}{\vom ~ #5}
+      \hspace{0.7cm} - \seite ~ \thepage/\letterlastpage  ~-%
+    }
+  }
+}
+
+%Ende Anpassungen der Kopfzeile
+
+
+%Beginn Anpassungen der Fußzeile:
+%Der folgende Block passt die Fußzeile so an, dass sich der untere Rand automatisch anpasst. Der Inhaltsteil ist entsprechend markiert, anstatt dieser Anpassungen ist es auch möglich den Fuß über die KOMA-Script-Makros 
+% \ifoot{<inhalt innen/links>}\cfoot{<inhalt zentriert>}\ofoot{<inhalt außen/rechts>}
+% anzupassen.
+\normalfont %Basisschriftart aktivieren, damit der Fuß entsprechend gebaut wird.
+\setkomafont{pagefoot}{\tiny} %Kleine schriftart für den Fußblock
+
+%Box generieren, um die Höhe des Fußes zu kennen, damit ist eine automatische Anpassung des unteren Randes möglich
+\if@kivi@footer
+
+  \newsavebox\footerbox
+  \begin{lrbox}\footerbox
+    \usekomafont{pagefoot}%
+    % Anfang des eigentlichen Inhaltes der Fußzeile
+    \begin{tabular*}{\textwidth}[t]{@{\extracolsep{\fill}}p{.25\linewidth}p{.25\linewidth}r@{\extracolsep{0pt}\hspace{2\tabcolsep}}l@{}}%
+      \firma                 & \email              & \textKontonummer       & \kontonummer \\
+      \strasse               & \homepage           & \textBank             & \bank \\
+      \ort                   & \textUstid\ \ustid  & \textIban             & \iban \\
+      \textTelefon~\telefon  & \finanzamt          & \textBic              & \bic \\
+      \Ifstr{\fax}{}{}{\textFax~\fax} &        &\textBankleitzahl       & \bankleitzahl
+    \end{tabular*}
+    % Ende des Fußzeileninhaltes.
+  \end{lrbox}
+
+  %Box in den Fuß eintragen, durch die zusätzliche Angabe in der eckigen Klammer, wird die Fußzeile auch auf der ersten Seite verwendet, falls für die erste Seite eine unterschiedliche Fußzeile verwendet werden soll, ist es möglich den obigen Mechanismus mit einem anderen Makronamen als footerbox zu kopieren
+  \cfoot[\usebox\footerbox]{\usebox\footerbox}
+
+  %Fußhöhe auf Höhe der Box
+  %Automatische Anpassung des unteren Randes
+  \setlength{\footheight}{\dimexpr\ht\footerbox+\dp\footerbox}
+  \setlength{\footskip}{\dimexpr\footheight+\baselineskip}
+  \geometry{
+    includefoot,
+    %  bottom=1cm,% Falls der untere Rand kleiner sein soll, als die Seitenränder.
+    %   Weitere Anpassungen der Ränder sind hier ebenfalls möglich
+  }
+
+\fi
+% Ende Anpassungen der Fußzeile
+
+%Mandantenspezifische ergänzende Einstellungen, falls nötig:
+%\InputIfFileExists{\identpath/dateiname}{}{}
+
+\makeatother
+\endinput
diff --git a/templates/print/marei/invoice.html b/templates/print/marei/invoice.html
new file mode 100644 (file)
index 0000000..a167dc6
--- /dev/null
@@ -0,0 +1,275 @@
+
+<body bgcolor=ffffff>
+
+<table width=100%>
+<tr valign=bottom>
+  <td width=10>&nbsp;</td>
+  <td>
+
+  <table width=100%>
+  <tr>
+    <td>
+      <h4>
+      <%company%>
+      <br><%address%>
+      </h4>
+    </td>
+
+    <td align=right>
+      <h4>
+      Telefon <%tel%>
+      <br>Telefax <%fax%>
+      </h4>
+    </td>
+  </tr>
+
+  <tr>
+    <th colspan=3>
+      <h4>R E C H N U N G</h4>
+    </th>
+  </tr>
+
+  </table>
+
+
+  <table width=100% callspacing=0 cellpadding=0>
+
+  <tr>
+    <td align=right>
+    <table>
+    <tr>
+      <th align=right>Ausgestellt am</th><td width=10>&nbsp;</td><td><%invdate%></td>
+    </tr>
+
+    <tr>
+      <th align=right>Bezahlbar bis</th><td width=10>&nbsp;</td><td><%duedate%></td>
+    </tr>
+
+    <tr>
+      <th align=right>Nummer</th><td>&nbsp;</td><td><%invnumber%></td></tr>
+    </tr>
+
+    <tr>
+      <th align=right>Lieferdatum</th><td>&nbsp;</td><td><%deliverydate%></td></tr>
+    </tr>
+<!--
+    <tr>
+      <th align=right>Clerk:</th><td>&nbsp;</td><td><%username%></td>
+    </tr>
+-->
+
+    <tr>
+      <td>&nbsp;</td>
+    </tr>
+    </td>
+    </table>
+  </tr>
+
+  <tr>
+    <td>
+    <table width=100%>
+    <tr bgcolor=000000>
+      <th align=left><font color=ffffff>An:</th>
+      <th align=left><font color=ffffff>Lieferaddresse:</th>
+    </tr>
+
+<!--
+     other variables which can be use:
+     contact, shiptocontact, shiptophone, shiptofax
+-->
+
+    <tr>
+      <td><%name%>
+      <br><%street%>
+      <br><%zipcode%>
+      <br><%city%>
+      <br><%country%>
+      </td>
+
+      <td><%shiptoname%>
+      <br><%shiptostreet%>
+      <br><%shiptozipcode%>
+      <br><%shiptocity%>
+      <br><%shiptocountry%>
+      </td>
+    </tr>
+    </table>
+    </td>
+  </tr>
+
+  <tr>
+    <td>&nbsp;</td>
+  </tr>
+
+  <tr>
+    <td>
+    <table width=100%>
+    <tr bgcolor=000000>
+<!--      <th align=right><font color=ffffff>No.</th>  -->
+      <th align=left><font color=ffffff>Nummer</th>
+      <th align=left><font color=ffffff>Beschreibung</th>
+      <th><font color=ffffff>Anz.</th>
+      <th>&nbsp;</th>
+      <th><font color=ffffff>Preis</th>
+      <th><font color=ffffff>Rab</th>
+      <th><font color=ffffff>Total</th>
+    </tr>
+
+<%foreach number%>
+    <tr valign=top>
+<!--      <td align=right><%runningnumber%>.</td>
+adjust the colspan if you include this to shift subtotal one to the right
+-->
+      <td><%number%></td>
+      <td><%description%></td>
+      <td align=right><%qty%></td>
+      <td><%unit%></td>
+      <td align=right><%sellprice%></td>
+      <td align=right><%discount%></td>
+      <td align=right><%linetotal%></td>
+    </tr>
+<%end number%>
+
+<!--
+you can also use netprice instead of sellprice if you
+don't want to show the discount
+netprice = linetotal/qty
+-->
+
+    <tr>
+      <td colspan=7><hr noshade></td>
+    </tr>
+
+<%if taxincluded%>
+    <tr>
+      <th colspan=5 align=right>Total</th>
+      <td colspan=2 align=right><%invtotal%></td>
+    </tr>
+<%end taxincluded%>
+<%if not taxincluded%>
+    <tr>
+      <th colspan=5 align=right>Zwischensumme</th>
+      <td colspan=2 align=right><%subtotal%></td>
+    </tr>
+<%end taxincluded%>
+
+<%foreach tax%>
+    <tr>
+      <th colspan=5 align=right><%taxdescription%> auf <%taxbase%></th>
+      <td colspan=2 align=right><%tax%></td>
+    </tr>
+<%end tax%>
+
+<%if rounding%>
+    <tr>
+      <th colspan=5 align=right>Rundung</th>
+      <td colspan=2 align=right><%rounding%></td>
+    </tr>
+<%end rounding%>
+
+<%if paid%>
+    <tr>
+      <th colspan=5 align=right>Bezahlt</th>
+      <td colspan=2 align=right>- <%paid%></td>
+    </tr>
+<%end paid%>
+
+    <tr>
+      <td colspan=3>&nbsp;</td>
+      <td colspan=4><hr noshade></td>
+    </tr>
+
+    <tr>
+      <td colspan=3>Bezahlbar innerhalb von <b><%terms%></b> Tagen</td>
+<%if total%>
+      <th colspan=2 align=right>Total</th>
+      <th colspan=2 align=right><%total%></th>
+<%end total%>
+    </tr>
+
+    <tr>
+      <td>&nbsp;</td>
+    </tr>
+
+    </table>
+    </td>
+  </tr>
+
+<tr>
+  <td>
+  <table width=100%>
+    <tr valign=top>
+<%if notes%>
+      <td>Bemerkungen:</td>
+      <td><%notes%></td>
+<%end notes%>
+      <td align=right>
+      Alle Preise in <b><%currency%></b>
+      <br><%shippingpoint%>
+      </td>
+    </tr>
+
+  </table>
+  </td>
+</tr>
+
+<tr><td>&nbsp;</td></tr>
+
+<tr>
+  <td>
+  <table width=100%>
+  <tr valign=top>
+    <td><font size=-3>
+    Rechnung ist bezahlbar innerhalb von <%terms%> Tagen.
+    Nach dem <%duedate%> werden Zinsen zu einem
+    monatlichen Satz von 1.5% verrechnet.
+    Waren bleiben im Besitz von <%company%> bis die Rechnung voll bezahlt ist.
+    Rückgaben werden mit 10% Lagergebühren belastet. Beschädigte Waren
+    und Waren ohne eine Rückgabenummer werden nicht entgegengenommen.
+    </font>
+    </td>
+    <td width=150>
+    X <hr noshade>
+    </td>
+  </tr>
+  </table>
+  </td>
+</tr>
+
+<%foreach tax%>
+  <tr>
+    <th colspan=7 align=left><font size=-2><%taxdescription%> Registration <%taxnumber%></th>
+  </tr>
+<%end tax%>
+
+<%if taxincluded%>
+  <tr>
+    <th colspan=7 align=left><font size=-2>Steuern sind im Preis inbegriffen.</th>
+  </tr>
+<%end taxincluded%>
+
+<!-- business number
+  <tr>
+    <th colspan=7 align=left><font size=-2>Business Number: <%businessnumber%></font></th>
+  </tr>
+-->
+
+  <tr>
+    <th colspan=7 align=left>
+    <hr>
+    <br>Bankverbindung
+    <br>Bank
+    <br>Bankleitzahl
+    <br>Konto No.
+    </td>
+  </tr>
+
+</table>
+
+</td>
+</tr>
+</table>
+
+</body>
+</html>
+
diff --git a/templates/print/marei/invoice.tex b/templates/print/marei/invoice.tex
new file mode 100644 (file)
index 0000000..b8970c2
--- /dev/null
@@ -0,0 +1,202 @@
+\documentclass[paper=a4,fontsize=10pt]{scrartcl}
+\usepackage{kiviletter}
+<%if template_meta.formname == "invoice_copy"%>
+  \usepackage{transparent}
+  \DeclareNewLayer[page,foreground,contents={
+    \parbox[c][\layerheight][c]{\layerwidth}{\centering\color{gray}\scalebox{11}{\rotatebox{60}{\texttransparent{0.5}{\rechnungskopie}}}}
+  }]{foreground}
+  \AddLayersToPageStyle{kivitendo.letter.first}{foreground}%Hintergrund für die erste Seite aktivieren
+  \AddLayersToPageStyle{kivitendo.letter}{foreground}%Hintergrund für die erste Seite aktivieren
+<%end if%>
+
+
+
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+<%if template_meta.formname == "invoice_for_advance_payment"%>
+  \renewcommand{\rechnung}{\anzahlungsrechnung}
+<%end if%>
+
+<%if template_meta.formname == "final_invoice"%>
+  \renewcommand{\rechnung}{\schlussrechnung}
+<%end if%>
+
+% laufende Kopfzeile:
+\ourhead{\kundennummer}{<%customernumber%>}{\rechnung}{<%invnumber%>}{<%invdate%>}
+
+\setkomavar*{date}{\rechnungsdatum}
+\setkomavar{date}{<%invdate%>}
+\setkomavar{customer}{<%customernumber%>}
+\setkomavar{fromname}{<%employee_name%>}
+\setkomavar{fromphone}{<%employee_tel%>}
+\setkomavar{fromemail}{<%employee_email%>}
+\setkomavar{title}{
+  \rechnung~ \nr ~<%invnumber%>%
+}
+<%if ordnumber%>%
+  \setkomavar*{myref}{\auftragsnummer}
+  \setkomavar{myref}{<%ordnumber%>}
+<%end if%>%
+<%if tax_point%>%
+  \setkomavar*{taxpoint}{\leistungsdatum}
+  \setkomavar{taxpoint}{<%tax_point%>}
+<%end if%>%
+<%if cusordnumber%>%
+  \setkomavar*{yourref}{\ihreBestellnummer}
+  \setkomavar{yourref}{<%cusordnumber%>}
+<%end if%>%
+<%if donumber%>%
+  \setkomavar{delivery}{<%donumber%>}
+<%end if%>%
+
+<%if quonumber%>%
+\setkomavar{quote}{<%quonumber%>}
+<%end if%>%
+
+\setkomavar{transaction}{<%transaction_description%>}
+<%if shiptoname%>%
+\makeatletter
+\begin{lrbox}\shippingAddressBox
+  \parbox{\useplength{toaddrwidth}}{
+    \backaddr@format{\scriptsize\usekomafont{backaddress}%
+      \strut\abweichendeLieferadresse
+    }
+    \par\smallskip
+    \setlength{\parskip}{\z@}
+    \par
+    \normalsize
+    <%shiptoname%>\par
+    <%if shiptocontact%> <%shiptocontact%><%end if%>\par
+    <%shiptodepartment_1%>\par
+    <%shiptodepartment_2%>\par
+    <%shiptostreet%>\par
+    <%shiptozipcode%> <%shiptocity%>%
+  }
+\end{lrbox}
+\makeatother
+<%end if%>%
+
+\begin{document}
+
+\begin{letter}{
+<%name%>\strut\\
+<%if department_1%><%department_1%>\\<%end if%>%
+<%if department_2%><%department_2%>\\<%end if%>%
+<%cp_givenname%> <%cp_name%>\strut\\
+<%street%>\strut\\
+<%zipcode%> <%city%>\strut\\
+<%country%> \strut
+}
+
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\opening{
+\Ifstr{<%cp_name%>}{}
+{<%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}
+  {
+    \Ifstr{<%cp_gender%>}{f}
+        {\anredefrau}
+        {\anredeherr}
+      <%cp_title%> <%cp_name%>,
+    }
+  }
+\thispagestyle{kivitendo.letter.first}
+
+<%if notes%>%
+<%notes%>%
+\vspace{0.5cm}
+<%end if%>%
+
+
+%PricingTabular* kann automatisch spalten ignorieren
+% \begin{PricingTabular*}[id=false]
+% deaktiviert damit die Spalte der Produktnummer
+% analog ist dies für pos, amount, price, pricetotal möglich.
+% Die Spalte der Bezeichnung ist nicht deaktivierbar
+%
+% Darüber hinaus kann die Reihenfolge verändert werden, die Voreinstellung entspricht:
+% \begin{PricingTabular*[columns={pos, id, desc, amount, price, pricetotal}]
+% Auf diese Art ist auch möglich mehrSpalten anzulegen als definiert sind. Für jede Spalte kann die Breite über weitere Optionen angepasst werden, die Einträge der columns-Liste entspricht den Spaltennamen.
+%
+% id = false, % deaktiviert die Spalte der Artikelnummer
+% amount = 1cm, % Setzt die Breite der Mengenspalte auf 1cm
+% desc/header = Artikelbeschreibung, %Ändert die Überschrift der Bezeichnunsspalte in „Artikelbeschreibung”
+\begin{PricingTabular*}%
+  % eigentliche Tabelle
+  \FakeTable{%
+  <%foreach number%>%
+  <%runningnumber%> &%
+  <%number%> &%
+  \textbf{<%description%>}%
+  <%if longdescription%>\ExtraDescription{<%longdescription%>}<%end longdescription%>%
+  <%if serialnumber%>\ExtraDescription{\seriennummer: <%serialnumber%>}<%end serialnumber%>%
+  <%if ean%>\ExtraDescription{\ean: <%ean%>}<%end ean%>%
+  <%if projectnumber%>\ExtraDescription{\projektnummer: <%projectnumber%>}<%end projectnumber%>%
+  &%
+  <%qty%> <%unit%> &%
+  <%sellprice%>&%
+  \Ifstr{<%p_discount%>}{0}{}{\sffamily\scriptsize{(-<%p_discount%>\,\%)}}%
+    <%linetotal%>\tabularnewline%
+    <%end number%>%
+  }%
+  \begin{PricingTotal}%
+    % Tabellenende letzte Seite
+    \nettobetrag & <%subtotal%>\\%
+    <%foreach tax%>%
+    <%taxdescription%> & <%tax%>\\%
+    <%end tax%>%
+    \bfseries\schlussbetrag &  \bfseries <%invtotal%>\\%
+  \end{PricingTotal}%
+\end{PricingTabular*}
+
+\vspace{0.2cm}
+
+<%if template_meta.formname == "final_invoice"%>
+  <%if iap_existing%>
+    \abzueglichAnzahlungsrechnungen:\\
+    \begin{SimpleTabular}[colspec=llr<{\tabcurrency}r<{\tabcurrency},headline={\bfseries\nr& \bfseries\date& \bfseries\betrag & \bfseries\ust}]%
+      <%foreach iap_invnumber%>%
+        <%iap_invnumber%> & <%iap_transdate_as_date%> & <%iap_amount%> & <%iap_taxamount%>\\%
+      <%end iap_invnumber%>%
+    \end{SimpleTabular}%
+      \bfseries\rechnungsbetrag: <%iap_final_amount%> \currency\\%
+  <%end iap_available%>
+<%end%>%
+
+\Ifstr{<%deliverydate%>}{}{%
+  \leistungsdatumGleichRechnungsdatum%
+}{
+  \lieferungErfolgtAm ~<%deliverydate%>.
+}\\
+
+<%if payment_terms%>%
+\zahlung ~<%payment_terms%>\\
+<%end payment_terms%>%
+
+<%if delivery_term%>%
+\lieferung ~<%delivery_term.description_long%>\\
+<%end delivery_term%>%
+
+<%if ustid%>\ihreustid ~<%ustid%>.\\<%end if%>%
+
+\ifnum<%taxzone_id%>=1
+\steuerfreiEU\\  % EU mit USt-ID Nummer
+\else
+\ifnum<%taxzone_id%>=3
+\steuerfreiAUS\\  % Außerhalb EU
+\fi
+\fi
+
+\closing{\gruesse}
+
+\end{letter}
+
+\end{document}
diff --git a/templates/print/marei/invoice_copy.tex b/templates/print/marei/invoice_copy.tex
new file mode 120000 (symlink)
index 0000000..b6a6ad8
--- /dev/null
@@ -0,0 +1 @@
+invoice.tex
\ No newline at end of file
diff --git a/templates/print/marei/invoice_for_advance_payment.tex b/templates/print/marei/invoice_for_advance_payment.tex
new file mode 120000 (symlink)
index 0000000..b6a6ad8
--- /dev/null
@@ -0,0 +1 @@
+invoice.tex
\ No newline at end of file
diff --git a/templates/print/marei/kiviletter.sty b/templates/print/marei/kiviletter.sty
new file mode 100644 (file)
index 0000000..79b33a8
--- /dev/null
@@ -0,0 +1,678 @@
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{kiviletter}[2022/04/23 Letter Layouts for Kivitendo]
+
+\RequirePackage{l3keys2e}
+
+%Optionen vor den eigenen Paketoptionen hinzufügen, damit spätere diese ggf. überschreiben
+\PassOptionsToPackage{
+  fromlogo,
+  fromalign=right,
+  firstfoot=false,%Für einheitliche Randeinstellungen
+  refline=nodate,
+}{scrletter}
+
+\ExplSyntaxOn
+\newif\if@kivi@infobox
+\newif\if@kivi@footer
+\keys_define:nn {kiviletter} {
+  infobox .choices:nn = {true,false} {\use:c {@kivi@infobox\l_keys_choice_tl}},
+  infobox .default:n = true,
+  infobox .initial:n = true,
+  reffields .meta:n = {infobox=false},
+  footer .choices:nn = {true,false} {\use:c {@kivi@footer\l_keys_choice_tl}},
+  footer .default:n = true,
+  footer .initial:n = true,
+  nofooter .meta:n = {footer=false},
+  unknown .code:n = \PassOptionsToPackage{\l_keys_key_str=#1}{scrletter},
+}
+\ExplSyntaxOff
+
+\ProcessKeysOptions{kiviletter}
+
+\RequirePackage{xparse}
+\RequirePackage{iftex}
+
+% Schriftart, Eingabelayout der Tastatur
+\ifPDFTeX
+  \RequirePackage[utf8]{inputenc}% Nur notwendig, wenn Basis älter als TL2018
+  \RequirePackage[T1]{fontenc}
+  \RequirePackage{lmodern}
+
+  \RequirePackage{eurosym}
+  \DeclareUnicodeCharacter{20AC}{\euro}
+\else
+  \RequirePackage{fontspec}
+\fi
+
+\RequirePackage{xltabular}
+\RequirePackage{booktabs}
+\RequirePackage{graphicx}
+
+\RequirePackage{scrletter}
+\LoadLetterOption{DIN}
+
+\newkomavar{taxpoint}
+\newkomavar{transaction}
+\newkomavar[\lieferschein{}~\nr]{delivery}
+\newkomavar[\angebot{}~\nr]{quote}
+\newkomavar[\auftragsnummer]{orderID}
+\newkomavar[\projektnummer]{projectID}
+\setkomavar*{fromphone}{\textTelefon}
+\setkomavar*{fromemail}{\textEmail}
+\setkomavar*{fromfax}{\textFax}
+\setkomavar*{customer}{\kundennummer}
+
+
+\usepackage{geometry}
+
+\ExplSyntaxOn
+\dim_new:N \g_kivi_margin_dim
+\dim_gset:Nn \g_kivi_margin_dim {\useplength{toaddrhpos}}
+\geometry{a4paper,margin=\g_kivi_margin_dim,heightrounded}
+%Scratch variables
+\int_new:N \l_kivi_tmp_int
+\bool_new:N \l_kivi_tmp_bool
+\bool_new:N  \g_kivi_TableFoot_bool
+\dim_new:N \g_kivi_orig@textheight_dim
+\ExplSyntaxOff
+
+\newsavebox{\shippingAddressBox}
+
+
+\DeclareNewLayer[
+  foreground,
+  hoffset=\useplength{toaddrhpos},
+  voffset=\dimexpr\useplength{toaddrvpos}+\useplength{toaddrheight}+4\baselineskip,%sep to shippingaddressbox
+  contents={\usebox\shippingAddressBox}
+]{kivitendo.shippingaddress}
+
+\newpairofpagestyles{kivitendo.letter}{}
+
+\renewcommand*{\letterpagestyle}{kivitendo.letter}
+
+\DeclareNewPageStyleByLayers{kivitendo.letter.first}{
+  kivitendo.shippingaddress,
+  plain.kivitendo.letter.head.odd,plain.kivitendo.letter.head.even,plain.kivitendo.letter.head.oneside,%
+  plain.kivitendo.letter.foot.odd,plain.kivitendo.letter.foot.even,plain.kivitendo.letter.foot.oneside,%
+}
+
+\setkomavar{backaddress}{\firma\ $\cdot$ \strasse\ $\cdot$ \ort}
+
+\setkomavar{firsthead}{
+  \if@logo
+    \rlap{\usekomavar{fromlogo}}%
+  \fi
+}
+
+\@setplength{locwidth}{6cm}
+
+\ExplSyntaxOn
+\dim_new:N \l_kivi_tab_desc_leftskip_dim
+
+
+
+\cs_new:Nn \__kivi_set_colwidth:nn  {
+  \dim_set:cn {l_kivi_tab_#1_dim} {#2}
+}
+
+
+\cs_new:Nn \__kivi_initialize_columns: {
+  \clist_map_inline:Nn \g_kivi_pricingtable_col_clist {
+    \bool_if_exist:cF {l_kivi_col_##1_bool}
+    {
+      \bool_new:c {l_kivi_col_##1_bool}
+      \dim_new:c {l_kivi_tab_##1_dim}
+      \keys_define:nn {kivi/PricingTable} {
+        ##1 .choice:,
+        ##1 / true .code:n = \bool_set_true:c {l_kivi_col_##1_bool},
+        ##1 / false .code:n = \bool_set_false:c {l_kivi_col_##1_bool},
+        ##1 / unknown .code:n = {
+            \bool_set_true:c {l_kivi_col_##1_bool}
+            \dim_set:cn {l_kivi_tab_##1_dim} {####1}
+          },
+        ##1 .default:n = true,
+        ##1 .initial:n = true,
+        ##1 / header .prop_put:c = {l_kivi_col_##1_prop},
+        ##1 / colspec .prop_put:c = {l_kivi_col_##1_prop},
+      }
+    }
+  }
+}
+
+\clist_new:N \g_kivi_pricingtable_col_clist
+
+\keys_define:nn {kivi/PricingTable} {
+  columns .code:n =
+  \clist_gset:Nn \g_kivi_pricingtable_col_clist {#1}
+  \__kivi_initialize_columns:,
+  columns .initial:n = {pos, id, desc, amount, price, pricetotal},
+  unknown .code:n = \keys_set:no {kivi/Tabular} {\l_keys_key_str=#1}
+}
+
+% set default values for colwidth
+\keys_set:nn {kivi/PricingTable} {
+  pos=5ex,
+  id=4em,
+  amount=5em,
+  price=7em,
+  pricetotal=7em,
+  %  desc=auto,
+  pos/header=\position,
+  id/header=\artikelnummer,
+  desc/header=\bezeichnung,
+  amount/header=\menge,
+  price/header=\einzelpreis,
+  pricetotal/header=\gesamtpreis,
+  price / colspec = Price,
+  pricetotal / colspec = Price ,
+}
+
+\dim_new:N \g_kivi_tabcolsep_dim
+\dim_gset:Nn \g_kivi_tabcolsep_dim {.5\tabcolsep}
+\setlength\tabcolsep{.5\tabcolsep}
+
+\prg_new_conditional:Nnn \kivi_if_Price_col:n {T} {
+  \prop_get:cnN {l_kivi_col_#1_prop} {colspec} \l_tmpa_tl
+  \exp_args:NV \tl_if_eq:nnTF \l_tmpa_tl {Price}
+  {\prg_return_true:}
+  {\prg_return_false:}
+}
+
+
+\cs_new:Nn \__kivi_calc_desc_column: {
+  \bool_if:NTF \g__kivi_Tabular_rowcolor_bool
+  {\dim_set:Nn \l_kivi_tab_desc_leftskip_dim {2\g_kivi_tabcolsep_dim}}
+  {\dim_zero:N \l_kivi_tab_desc_leftskip_dim}
+  \dim_zero:N \l_kivi_tab_desc_dim
+  \bool_set_false:N \l_tmpa_bool
+  \tl_gclear:N \g_kivi_Pricing_colspec_tl
+  \clist_map_inline:Nn \g_kivi_pricingtable_col_clist {
+    \tl_if_eq:nnTF {##1} {desc}  {
+      \dim_set:Nn \l_kivi_tab_desc_dim {
+        \textwidth-\l_kivi_tab_desc_leftskip_dim
+      }
+      \bool_set_true:N \l_tmpa_bool
+      \tl_gput_right:Nn \g_kivi_Pricing_colspec_tl {p{\l_kivi_tab_desc_dim}}
+    }{
+      \bool_if:cT {l_kivi_col_##1_bool} {
+        \bool_if:NTF \l_tmpa_bool {
+          \dim_sub:Nn \l_kivi_tab_desc_dim {
+            \dim_use:c {l_kivi_tab_##1_dim}+2\g_kivi_tabcolsep_dim
+          }
+        }{
+          \dim_add:Nn \l_kivi_tab_desc_leftskip_dim {
+            \dim_use:c {l_kivi_tab_##1_dim}+2\g_kivi_tabcolsep_dim
+          }
+        }
+        \tl_gput_right:Nn \g_kivi_Pricing_colspec_tl {K{\dim_use:c {l_kivi_tab_##1_dim}}}
+        \kivi_if_Price_col:nT {##1} {\tl_gput_right:Nn \g_kivi_Pricing_colspec_tl {<{\__kivi_tab_column_currency:}}}
+      }
+    }
+  }
+  \bool_if:NF \g__kivi_Tabular_rowcolor_bool {
+    \tl_gput_left:Nn \g_kivi_Pricing_colspec_tl {@{}}
+    \tl_gput_right:Nn \g_kivi_Pricing_colspec_tl {@{}}
+  }
+}
+
+\newcolumntype{K}[1]{>{\raggedleft\arraybackslash}p{#1}}
+\newcolumntype{P}[1]{K{#1}<{\__kivi_tab_column_currency:}}
+
+\RequirePackage{tcolorbox}
+\tcbuselibrary{breakable, skins}
+
+\tcb@new@skin{kivi@LT}{base@unbroken,%
+  frame~engine=empty,interior~titled~engine=empty,interior~engine=empty,segmentation~engine=empty,title~engine=empty,%
+  skin~first=kivi@LT@first,skin~middle=kivi@LT@middle,skin~last=kivi@LT@last,
+  underlay~first~and~middle={
+    \node[anchor=north]  at (interior.north)  {\csname box_use:c\endcsname  {g_kivi_LT@head_box}};
+    \node[anchor=south]  at (interior.south)  {\csname box_use:c\endcsname  {g_kivi_LT@foot_box}};
+},
+  underlay~unbroken~and~last={
+    \node[anchor=north]  at (interior.north)  {\csname box_use:c\endcsname  {g_kivi_LT@head_box}};
+    \node[anchor=south]  at (interior.south)  {\csname box_use:c\endcsname  {g_kivi_LT@lastfoot_box}};
+  },
+  boxsep=0pt,
+  boxrule=0pt,
+  left=0pt,
+  right=0pt,
+  bottom=\box_ht:N  \g_kivi_LT@foot_box+\box_dp:N  \g_kivi_LT@foot_box + \aboverulesep,
+  top=\box_ht:N  \g_kivi_LT@head_box+\box_dp:N  \g_kivi_LT@head_box +\belowrulesep,
+  parbox=false,
+}
+
+\tcb@new@skin{kivi@LT@first}{base@first,%
+  frame~engine=empty,interior~titled~engine=empty,interior~engine=empty,segmentation~engine=empty,title~engine=empty,%
+  skin~first=kivi@LT@first,skin~middle=kivi@LT@middle,skin~last=kivi@LT@middle,
+}
+
+\tcb@new@skin{kivi@LT@middle}{base@middle,%
+  frame~engine=empty,interior~titled~engine=empty,interior~engine=empty,segmentation~engine=empty,title~engine=empty,%
+  skin~first=kivi@LT@middle,skin~middle=kivi@LT@middle,skin~last=kivi@LT@middle,
+}
+
+\tcb@new@skin{kivi@LT@last}{base@last,%
+  frame~engine=empty,interior~titled~engine=empty,interior~engine=empty,segmentation~engine=empty,title~engine=empty,%
+  skin~first=kivi@LT@middle,skin~middle=kivi@LT@middle,skin~last=kivi@LT@last,
+}
+
+\tcbset{kivi@LT/.style={skin=kivi@LT}}%
+
+\seq_new:N \l_kivi_PricingTable_seq
+\seq_new:N \l_kivi_columns_seq
+\seq_new:N \g_kivi_extraDescription_seq
+
+\int_new:N \g__kivi_PricingTable_rowcolor_int
+\dim_new:N \l__kivi_fboxsep_dim
+\dim_set:Nn \l__kivi_fboxsep_dim {\g_kivi_tabcolsep_dim}
+
+%colorbox variant to only add vertical spacing
+%based on colorbox definition from xcolor.sty
+%% ----------------------------------------------------------------
+%% Copyright (C) 2003-2016 by Dr. Uwe Kern <xcolor at ukern dot de>
+%% ----------------------------------------------------------------
+%% This variant of colorbox adds a space of \l__kivi_fboxsep_dim along the vertical axes but no horizontal space
+\def\kivi@tabcolorbox#1#{\protect\kivi@tabcolor@box{#1}}
+
+\def\kivi@tabcolor@box#1#2{
+  \tl_if_empty:oTF {#2}
+  \kivi@nocolor@b@x
+  \kivi@color@b@x
+  \relax{\color#1{#2}}
+}
+\long\def\kivi@color@b@x#1#2#3%
+{\leavevmode
+  \setbox\z@\hbox{{\set@color#3}}%
+  \dimen@\ht\z@\advance\dimen@\l__kivi_fboxsep_dim\ht\z@\dimen@
+  \dimen@\dp\z@\advance\dimen@\l__kivi_fboxsep_dim\dp\z@\dimen@
+  {#1{#2\color@block{\wd\z@}{\ht\z@}{\dp\z@}\box\z@}}}
+
+\long\def\kivi@nocolor@b@x#1#2#3%
+{\leavevmode
+  \setbox\z@\hbox{#3}%
+  \dimen@\ht\z@\advance\dimen@\l__kivi_fboxsep_dim\ht\z@\dimen@
+  \dimen@\dp\z@\advance\dimen@\l__kivi_fboxsep_dim\dp\z@\dimen@
+  {\box\z@}}
+
+%%%
+
+
+\newcommand{\FakeTable}[1]{
+  \par
+  \seq_set_split:Nnn \l_kivi_PricingTable_seq {\tabularnewline} {#1}
+  \seq_remove_all:Nn \l_kivi_PricingTable_seq {}
+  \begingroup
+  \setlength{\parskip}{\c_zero_dim}
+  \let\ExtraDescription\__kivi_addExtraDescription:n
+  \setlength{\tabcolsep}{\g_kivi_tabcolsep_dim}
+  \seq_map_inline:Nn \l_kivi_PricingTable_seq {
+    \if_mode_horizontal: \par \fi
+    \bool_if:NT \g__kivi_Tabular_rowcolor_bool {
+      \int_gincr:N \g__kivi_PricingTable_rowcolor_int
+      \int_if_odd:nTF {\g__kivi_PricingTable_rowcolor_int}
+      {\nointerlineskip\kivi@tabcolorbox{\g__kivi_Tabular_rowcolor_odd_tl}}
+      {\nointerlineskip\kivi@tabcolorbox{\g__kivi_Tabular_rowcolor_even_tl}}
+    }
+    {\parbox{\linewidth}{
+        \seq_set_split:Nnn  \l_kivi_columns_seq {&} {##1}
+        \seq_gclear:N \g_kivi_extraDescription_seq
+        \exp_args:Nnx \use:n {\tabular[t]}\g_kivi_Pricing_colspec_tl
+        \seq_pop_left:NN \__l_FakeTable_columns_seq \l_tmpa_tl
+        \seq_item:Nn \l_kivi_columns_seq {\l_tmpa_tl}
+        \seq_map_inline:Nn \__l_FakeTable_columns_seq {
+          &\seq_item:Nn \l_kivi_columns_seq {####1}
+        }
+        \endtabular
+        \seq_if_empty:NTF \g_kivi_extraDescription_seq
+        {\par}
+        {\par\nopagebreak
+          \begingroup
+          \setlength{\leftskip}{\dim_eval:n {\bool_if:NT \g__kivi_Tabular_rowcolor_bool {-\tabcolsep} +\l_kivi_tab_desc_leftskip_dim}}
+          \setlength{\hsize}{\dim_eval:n {\l_kivi_tab_desc_dim+\leftskip}}
+          \usekomafont{extraDescription}
+          \seq_use:Nn \g_kivi_extraDescription_seq {\\}
+          \par
+          \endgroup
+        }
+      }}
+  }
+  \endgroup\par
+  \l__kivi_Tabular_rowsep_tl
+}
+
+
+\seq_new:N  \__l_FakeTable_columns_seq
+\cs_new:Nn \__kivi_setup_FakeTable: {
+  \seq_clear:N \__l_FakeTable_columns_seq
+  \int_zero:N \l_tmpa_int
+  \clist_map_inline:Nn \g_kivi_pricingtable_col_clist {
+    \int_incr:N \l_tmpa_int
+    \bool_if:cT {l_kivi_col_##1_bool} {\seq_put_right:Nx \__l_FakeTable_columns_seq {\int_use:N \l_tmpa_int}}
+  }
+}
+
+\tl_new:N \g_kivi_Pricing_colspec_tl
+\tl_gset:Nn \g_kivi_Pricing_colspec_tl {
+  \bool_if:NF \g__kivi_Tabular_rowcolor_bool {@{}}
+  \bool_if:NT \l_kivi_col_pos_bool {p{\l_kivi_tab_pos_dim}}
+  \bool_if:NT \l_kivi_col_id_bool {p{\l_kivi_tab_id_dim}}
+  p{\l_kivi_tab_desc_dim}
+  \bool_if:NT \l_kivi_col_amount_bool {\exp_not:n {>{\raggedleft\arraybackslash}p{\l_kivi_tab_amount_dim}}}
+  \bool_if:NT \l_kivi_col_price_bool {\exp_not:n {>{\raggedleft\arraybackslash}p{\l_kivi_tab_price_dim}<{\__kivi_tab_column_currency:}}}
+  \bool_if:NT \l_kivi_col_pricetotal_bool {\exp_not:n {>{\raggedleft\arraybackslash}p{\l_kivi_tab_pricetotal_dim}<{\__kivi_tab_column_currency:}}}
+  \bool_if:NF \g__kivi_Tabular_rowcolor_bool {@{}}
+}
+
+\cs_new_protected:Nn \__kivi_tab_column_currency: {\,\currency}
+\def\tabcurrency{\__kivi_tab_column_currency:}
+\cs_set:Nn \__kivi_tab_column_header_currency: {}
+\cs_set_eq:NN \__kivi_tab_column_body_currency:  \__kivi_tab_column_currency:
+
+\clist_map_inline:nn {head, foot, firsthead, lastfoot} {%TODO reduce
+  \box_new:c {g_kivi_LT@#1_box}
+}
+
+\newkomafont{PricingTableHeader}{\bfseries}
+
+\cs_new:Nn \__kivi_setup_LT_boxes: {
+  \__kivi_calc_desc_column:
+  \hbox_gset:Nn \g_kivi_LT@head_box {
+    \setlength{\tabcolsep}{\g_kivi_tabcolsep_dim}
+    \bool_if:NT \g__kivi_Tabular_rowcolor_bool {\kivi@tabcolorbox{\g__kivi_Tabular_rowcolor_header_tl}}%
+    {
+      \exp_args:Nnx \use:n {\tabular[b]}\g_kivi_Pricing_colspec_tl
+      \__kivi_PricingTabular_header:
+      \endtabular
+    }
+  }
+  \hbox_gset:Nn \g_kivi_LT@foot_box {
+    \begin{tabular*}{\textwidth}[t]{@{\extracolsep{\fill}}r@{\bool_if:NT \g__kivi_Tabular_rowcolor_bool {\hskip\tabcolsep}}}
+      \bool_if:NTF \g__kivi_Tabular_rowcolor_bool
+      {\hline\noalign{\vskip1pt}}
+      \midrule
+      \strut\weiteraufnaechsterseite
+    \end{tabular*}
+  }
+  \hbox_gset:Nn \g_kivi_LT@lastfoot_box {
+    \raisebox{\dimexpr\depth+\baselineskip}[0pt][0pt]{
+      \begin{tabular*}{\textwidth}{@{\bool_if:NT \g__kivi_Tabular_rowcolor_bool {\hskip\tabcolsep}\extracolsep{\fill}}r@{\bool_if:NT \g__kivi_Tabular_rowcolor_bool {\hskip\tabcolsep}}}
+        \bool_if:NF \g__kivi_Tabular_rowcolor_bool \bottomrule
+      \end{tabular*}
+    }
+  }
+}
+
+
+%Macht es sinn hier eine Variante zu machen, in der alle Spalten Belegbar sind?
+\NewDocumentEnvironment{PricingTotal}{+b}{
+  \par\nointerlineskip
+}{
+  \bool_if:NT \g__kivi_Tabular_rowcolor_bool   {\nointerlineskip\kivi@tabcolorbox{\g__kivi_Tabular_rowcolor_PricingTotal_tl}}
+  {
+    \tabular[t]{
+      @{\bool_if:NT \g__kivi_Tabular_rowcolor_bool {\hskip\tabcolsep}}
+      p{\dim_eval:n {\linewidth-\l_kivi_tab_pricetotal_dim-\bool_if:NTF \g__kivi_Tabular_rowcolor_bool {4}{2}\tabcolsep}}P{\l_kivi_tab_pricetotal_dim}@{\bool_if:NT \g__kivi_Tabular_rowcolor_bool {\hskip\tabcolsep}}
+    }
+    \l__kivi_Tabular_PricingTotal_topsep_tl
+    #1
+    \endtabular
+  }
+}
+
+\tl_new:N \l__kivi_Tabular_PricingTotal_topsep_tl
+%TODO
+\tl_set:Nn \l__kivi_Tabular_PricingTotal_topsep_tl {\bool_if:NF \g__kivi_Tabular_rowcolor_bool \midrule}
+
+\newcommand*\ExtraDescription{
+  \PackageError{kiviletter}{The~command~\string\ExtraDescription\space~may~be~only~used~inside~the~\string\FakeTable\space~environment.}{See~documentation~for~details}
+}
+
+
+\cs_new:Nn \__kivi_addExtraDescription:n {\seq_gput_right:Nn \g_kivi_extraDescription_seq {#1}}
+
+\newenvironment{PricingTabular}[1][]{
+  \begingroup
+  \dim_set:Nn \parskip {\c_zero_dim}
+  \tl_if_empty:nF {#1} {\keys_set:nn {kivi/PricingTable} {#1}}
+  \setlength{\tabcolsep}{\g_kivi_tabcolsep_dim}
+  \__kivi_calc_desc_column:
+  \exp_args:Nx \longtable \g_kivi_Pricing_colspec_tl
+  % Tabellenkopf
+  \__kivi_PricingTabular_header:
+  \endhead
+  \midrule
+  \rlap{\makebox[\textwidth][r]{\weiteraufnaechsterseite}}\\
+  \endfoot
+  \bool_if:NF \g__kivi_Tabular_rowcolor_bool \bottomrule
+  \endlastfoot
+}{
+  \endlongtable
+  \endgroup
+}
+
+\cs_set:Nn \__kivi_PricingTabular_header: {
+  \bool_if:NTF \g__kivi_Tabular_rowcolor_bool {\noalign{\skip_vertical:n {\dp\strutbox}}}\toprule
+  \cs_gset_eq:NN \__kivi_tab_column_currency: \__kivi_tab_column_header_currency:
+  \bool_set_false:N \l_tmpa_bool
+  \clist_map_inline:Nn \g_kivi_pricingtable_col_clist  {
+    \bool_if:cT {l_kivi_col_##1_bool} {
+      \bool_if:NT \l_tmpa_bool {&}
+      \bool_set_true:N \l_tmpa_bool
+      \usekomafont{PricingTableHeader}
+      \prop_item:cn {l_kivi_col_##1_prop} {header}
+    }
+  }
+  \cs_gset_eq:NN \__kivi_tab_column_currency: \__kivi_tab_column_body_currency:
+  \\
+  \bool_if:NF \g__kivi_Tabular_rowcolor_bool \midrule
+}
+
+\newkomafont{tablehead}{\bfseries}
+
+\keys_define:nn {kivi/SimpleTabular} {
+  colspec .tl_set:N =\l_kivi_SimpleTabular_colspec_tl,
+  colspec .initial:n = {rrX},
+  headline .tl_set:N = \l_kivi_SimpleTabular_headline_tl,
+  headline .initial:n = {\usekomafont{tablehead}\position & \usekomafont{tablehead}\menge & \usekomafont{tablehead}\bezeichnung},
+}
+
+\keys_define:nn {kivi/Tabular} {
+  color-rows .bool_gset:N =  \g__kivi_Tabular_rowcolor_bool ,
+  color-rows .initial:n = false,
+  color-rows .default:n = true,
+  rowcolor-odd .tl_gset:N = \g__kivi_Tabular_rowcolor_odd_tl,
+  rowcolor-odd .initial:n = black!10,
+  rowcolor-even .tl_gset:N = \g__kivi_Tabular_rowcolor_even_tl,
+  rowcolor-even .initial:n =,
+  rowcolor-header .tl_gset:N = \g__kivi_Tabular_rowcolor_header_tl,
+  rowcolor-header .initial:n = black!35,
+  rowcolor-total .tl_gset:N = \g__kivi_Tabular_rowcolor_PricingTotal_tl,
+  rowcolor-total .initial:n = black!35,
+  rowsep .tl_set:N =\l__kivi_Tabular_rowsep_tl,
+  rowsep .initial:n = ,
+  hrule .meta:n = {
+    rowsep={
+      \vskip\aboverulesep
+      \leavevmode\hrule\@height\lightrulewidth
+      \vskip\belowrulesep}},
+}
+
+\newcommand*{\SetupSimpleTabular}[1]{\keys_set:nn {kivi/SimpleTabular} {#1}}
+\newcommand*{\SetupPricingTabular}[1]{\keys_set:nn {kivi/PricingTable} {#1}}
+
+\newenvironment{SimpleTabular}[1][]
+{
+  \tl_if_in:nnTF {#1} {=} {\keys_set:nn {kivi/SimpleTabular} {#1}} {\tl_if_empty:nF {#1} {\tl_set:Nn \l_kivi_SimpleTabular_headline_tl {#1}}}
+  \setlength{\tabcolsep}{\g_kivi_tabcolsep_dim}
+  \dim_set:Nn \parskip {\c_zero_dim}
+  \bool_if:NF \g__kivi_Tabular_rowcolor_bool {
+    \tl_put_right:Nn \l_kivi_SimpleTabular_colspec_tl {@{}}
+    \tl_put_left:Nn \l_kivi_SimpleTabular_colspec_tl {@{}}
+  }
+  \exp_args:NnV \xltabular{\linewidth}\l_kivi_SimpleTabular_colspec_tl
+  \toprule
+  \cs_gset_eq:NN \__kivi_tab_column_currency: \__kivi_tab_column_header_currency:
+  \l_kivi_SimpleTabular_headline_tl
+  \\
+  \noalign{\cs_gset_eq:NN \__kivi_tab_column_currency: \__kivi_tab_column_body_currency:}
+  \midrule
+  \endhead
+  \midrule
+  \rlap{\makebox[\textwidth][r]{\weiteraufnaechsterseite}}\\
+  \endfoot
+  \bool_if:NF \g__kivi_Tabular_rowcolor_bool \bottomrule
+  \endlastfoot
+  \ignorespaces
+}{
+  \def\@currenvir{tabularx}
+  \endxltabular
+}
+
+%PricingTabular* kann automatisch spalten ignorieren
+% \begin{PricingTabular*}[id=false]
+% deaktiviert damit die Spalte der Produktnummer
+% analog ist dies für pos, amount, price, pricetotal möglich.
+% Die Spalte der Bezeichnung ist nicht deaktivierbar
+\newenvironment{PricingTabular*}[1][]{
+  \int_gzero:N \g__kivi_PricingTable_rowcolor_int
+  \tl_if_empty:nF {#1} {\keys_set:nn {kivi/PricingTable} {#1}}
+  \__kivi_setup_LT_boxes:
+  \__kivi_setup_FakeTable:
+  \dim_set:Nn \parskip {\c_zero_dim}
+  \PricingTabularBox\ignorespaces
+}{\endPricingTabularBox
+  %compensate footer spacing
+  \skip_vertical:n {-\box_ht:N  \g_kivi_LT@foot_box-\box_dp:N  \g_kivi_LT@foot_box}
+}
+
+\newtcolorbox{PricingTabularBox}{breakable,skin=kivi@LT}
+
+\if@kivi@infobox
+
+  \def\locationsep{:}
+
+  \NewDocumentCommand{\locationentry}{som}{
+    \Ifkomavarempty{#3}{}{
+      \IfBooleanTF {#1} {
+        \strut
+        \IfNoValueTF {#2}
+        {\usekomavar*{#3}}
+        {#2}
+        \locationsep
+        \hfill\strut\space
+        \hbox_set:Nn \l_tmpa_box {\usekomavar{#3}}
+        \dim_compare:nTF {\box_wd:N \l_tmpa_box>\linewidth}
+        {\newline\hspace*{\fill}\llap}
+        {\hspace*{\fill}}
+        {\box_use:N \l_tmpa_box\strut}
+      }{
+        \@hangfrom{\strut
+          \IfNoValueTF {#2}
+          {\usekomavar*{#3}}
+          {#2}\locationsep~
+        }{
+          \parbox[t]{\dimexpr\linewidth-\hangindent}{
+            \raggedleft
+            \usekomavar{#3}\strut
+          }
+        }
+      }
+    }
+    \par
+  }
+
+  \newkomafont{transaction}{\bfseries}
+
+  \setkomavar{location}{
+    \Ifkomavarempty{transaction}{}{{
+          \usekomafont{transaction}
+          \usekomavar{transaction}
+        }
+    }
+    \par
+    \medskip
+    \parbox{\useplength{locwidth}}{
+      \locationentry{date}
+      \locationentry{myref}
+      \locationentry{customer}
+      \locationentry{yourref}
+      \locationentry{delivery}
+      \locationentry{quote}
+      \locationentry{orderID}
+      \locationentry{projectID}
+      \locationentry{taxpoint}
+      \locationentry[\ansprechpartner]{fromname}
+      \locationentry{fromphone}
+      \locationentry*{fromemail}
+    }
+  }
+  \removereffields
+  \AtBeginLetter{
+    \ifdim\ht\shippingAddressBox>\z@
+      \@addtoplength{refvpos}{\dimexpr\ht\shippingAddressBox+\dp\shippingAddressBox}
+      \@addtoplength{refvpos}{4\baselineskip}%sep between address boxes
+    \fi
+  }
+
+\fi
+
+%Fallback for older KOMA-Script-Versions
+\cs_if_exist:NF \Ifstr {\let\Ifstr\ifstr}
+\cs_if_exist:NF \Ifkomavarempty {\let\Ifkomavarempty\ifkomavarempty}
+
+%Definitionen für die insettings.tex
+
+\newcommand*{\setupIdentpath}[1]{
+  \int_set:Nn \l_kivi_tmp_int {1}
+  \bool_set_true:N \l_kivi_tmp_bool
+  \bool_while_do:Nn \l_kivi_tmp_bool {
+    \file_if_exist:nTF {firma\int_use:N \l_kivi_tmp_int/ident.tex}
+    {
+      \exp_args:Nf \str_if_in:nnTF {#1} {Firma\int_use:N \l_kivi_tmp_int}
+      {
+        \newcommand*{\identpath}{firma\int_use:N \l_kivi_tmpa_int}
+        \bool_set_false:N \l_kivi_tmp_bool
+      }
+      {\int_incr:N \l_kivi_tmp_int}
+    }
+    {
+      \bool_set_false:N \l_kivi_tmp_bool
+      \newcommand*{\identpath}{firma}
+    }
+  }
+}
+
+\newcommand*{\setupCurrencyConfig}[3][euro]{
+  \tl_new:N \g_kivi_currency_tl
+  \exp_args:Nf \str_if_in:nnT {#3} {USD} {\tl_gset:Nn \g_kivi_currency_tl {usd}}
+  \exp_args:Nf \str_if_in:nnT {#3} {CHF} {\tl_gset:Nn \g_kivi_currency_tl {chf}}
+  \exp_args:Nf \str_if_in:nnT {#3} {EUR} {\tl_gset:Nn \g_kivi_currency_tl {euro}}
+  \tl_if_empty:NT  \g_kivi_currency_tl {
+    \tl_if_empty:oTF {#3} {
+      \tl_gset:Nn \g_kivi_currency_tl {#1}
+    } {
+      \tl_gset:Nn \g_kivi_currency_tl {#3}
+    }
+  }
+  \input{#2/\g_kivi_currency_tl _account.tex}
+  \let\setupCurrencyConfig\_kivi_currency_already_configured:w
+}
+
+\newcommand*{\_kivi_currency_already_configured:w}[3][euro]{
+  \msg_error:nnx {kiviletter} {currency-already-configured} {\g_kivi_currency_tl}
+}
+
+\msg_new:nnn {kiviletter} {currency-already-configured} {
+  The~currency~configuration~is~a~global~setting~for~each~document.\\
+  It's~already~set~to~#1,~please~remove~the~second~call~of~\string\setupCurrencyConfig.
+}
+\ExplSyntaxOff
+
+
+\renewcommand*{\raggedsignature}{\raggedright}
+
+\newkomafont{extraDescription}{}
+\newkomafont{subtotal}{}
+\newkomafont{total}{}
+
+\endinput
diff --git a/templates/print/marei/kivitendo.sty b/templates/print/marei/kivitendo.sty
new file mode 100644 (file)
index 0000000..4e6ed0b
--- /dev/null
@@ -0,0 +1,191 @@
+\ProvidesFile{kivitendo.sty}
+\usepackage{colortbl}
+\usepackage{eurosym}
+\usepackage{german}
+\usepackage{graphicx}
+\usepackage{ifthen}
+\usepackage{iftex}
+%Compilerunabhängigkeit
+\ifPDFTeX
+  \usepackage[utf8]{inputenc}
+  \usepackage[T1]{fontenc}
+\fi
+\usepackage{latexsym}
+\usepackage{longtable}
+\usepackage{textcomp}
+
+%% Paketoptionen
+\newboolean{defaultbg}\setboolean{defaultbg}{true}
+\newboolean{draftbg}
+\newboolean{reqspeclogo}
+\newboolean{secondpagelogo}
+\DeclareOption{nologo}{\setboolean{defaultbg}{false}}
+\DeclareOption{draftlogo}{\setboolean{defaultbg}{false}\setboolean{draftbg}{true}}
+\DeclareOption{reqspeclogo}{\setboolean{reqspeclogo}{true}}
+\DeclareOption{secondpagelogo}{\setboolean{defaultbg}{false}\setboolean{secondpagelogo}{true}}
+\ProcessOptions
+
+%% Seitenlayout
+\setlength{\voffset}{-1.5cm}
+\setlength{\hoffset}{-2.5cm}
+\setlength{\topmargin}{0cm}
+\setlength{\headheight}{0.5cm}
+\setlength{\headsep}{1cm}
+\setlength{\topskip}{0pt}
+\setlength{\oddsidemargin}{2cm}
+\setlength{\textwidth}{16.4cm}
+\setlength{\textheight}{25cm}
+\setlength{\footskip}{1cm}
+\setlength{\parindent}{0pt}
+\setlength{\tabcolsep}{0.2cm}
+
+\setlength{\unitlength}{1cm}
+
+\newcommand{\kivitendobgsettings}{%
+  \setlength{\headsep}{2.5cm}
+  \setlength{\textheight}{22.5cm}
+  \setlength{\footskip}{0.9cm}
+}
+
+%% Standardschrift Compilerunabhängig
+\newcommand*{\defaultfont}{\normalfont}
+\renewcommand*{\familydefault}{\sfdefault}
+\ifPDFTeX
+\else
+  \usepackage{fontspec}
+\fi
+
+%% Checkboxen
+\newsavebox{\checkedbox}
+\savebox{\checkedbox}(0.2,0.4){
+  \put(-0.15,-0.425){$\times$}
+  \put(-0.15,-0.45){$\Box$}
+}
+\newsavebox{\uncheckedbox}
+\savebox{\uncheckedbox}(0.2,0.4){
+  \put(-0.15,-0.45){$\Box$}
+}
+
+%% Farben
+\definecolor{kivitendoorange}{rgb}{1,0.4,0.2}
+\definecolor{kivitendodarkred}{rgb}{0.49,0,0}
+\definecolor{kivitendoyellow}{rgb}{1,1,0.4}
+\definecolor{kivitendobggray}{gray}{0.9}
+\definecolor{kivitendowhite}{gray}{1}
+
+%% Kopf- und Fußzeilen
+\newcommand{\kivitendofirsthead}{}
+\newcommand{\kivitendofirstfoot}{}
+\newcommand{\kivitendosecondhead}{}
+\newcommand{\kivitendosecondfoot}{\centerline{\defaultfont\small Seite \thepage}}
+
+\newcommand{\myhead}{%
+  \ifthenelse{\boolean{defaultbg}}{%
+    \begin{picture}(0,0)
+      \put(-2.025,-28.1){\includegraphics*[width=\paperwidth,keepaspectratio=true]{images/hintergrund_seite1.png}}
+    \end{picture}%
+  }{}%
+  \ifthenelse{\boolean{secondpagelogo}}{%
+    \begin{picture}(0,0)
+      \put(-2.025,-28.1){\includegraphics*[width=\paperwidth,keepaspectratio=true]{images/hintergrund_seite2.png}}
+    \end{picture}%
+  }{}%
+  \ifthenelse{\boolean{draftbg}}{%
+    \begin{picture}(0,0)
+      \put(-2.025,-26.9){\includegraphics*[width=\paperwidth,keepaspectratio=true]{images/draft.png}}
+    \end{picture}%
+  }{}%
+  \ifthenelse{\boolean{reqspeclogo}}{%
+    \begin{picture}(0,0)
+      \put(3,-22){\includegraphics*[width=13cm,keepaspectratio=true]{images/schachfiguren.jpg}}
+      \put(0.275,-4.1){\colorbox{kivitendoorange}{\begin{minipage}[t][4.5cm]{2.5cm}\hspace*{2.5cm}\end{minipage}}}
+      \put(0.275,-8.8){\colorbox{kivitendodarkred}{\begin{minipage}[t][4.5cm]{2.5cm}\hspace*{2.5cm}\end{minipage}}}
+      \put(0.275,-13.5){\colorbox{kivitendoyellow}{\begin{minipage}[t][4.5cm]{2.5cm}\hspace*{2.5cm}\end{minipage}}}
+    \end{picture}%
+  }{}%
+  \kivitendofirsthead
+}
+
+\newcommand{\mysecondhead}{%
+  \ifthenelse{\boolean{defaultbg} \or \boolean{secondpagelogo}}{%
+    \begin{picture}(0,0)
+      \put(-2.025,-28.1){\includegraphics*[width=\paperwidth,keepaspectratio=true]{images/hintergrund_seite2.png}}
+    \end{picture}%
+  }{}%
+  \ifthenelse{\boolean{draftbg}}{%
+    \begin{picture}(0,0)
+      \put(-2.025,-26.9){\includegraphics*[width=\paperwidth,keepaspectratio=true]{images/draft.png}}
+    \end{picture}%
+  }{}%
+  \kivitendosecondhead
+}
+
+\newcommand{\myfoot}{\kivitendofirstfoot}
+\newcommand{\mysecondfoot}{\kivitendosecondfoot}
+
+\renewcommand{\ps@headings}{%
+  \renewcommand{\@oddhead}{\myhead}
+  \renewcommand{\@evenhead}{\@oddhead}%
+  \renewcommand{\@oddfoot}{\myfoot}
+  \renewcommand{\@evenfoot}{\@oddfoot}%
+}
+
+\renewcommand{\ps@plain}{%
+  \renewcommand{\@oddhead}{\mysecondhead}
+  \renewcommand{\@evenhead}{\@oddhead}%
+  \renewcommand{\@oddfoot}{\mysecondfoot}
+  \renewcommand{\@evenfoot}{\@oddfoot}%
+}
+
+\pagestyle{plain}
+\thispagestyle{headings}
+
+% Abschnitte mit Kasten hinterlegt
+
+\newcommand{\reqspecsectionstyle}{%
+  \renewcommand{\thesection}{\alph{section}}
+  \makeatletter
+  \def\section{\@ifstar\unnumberedsection\numberedsection}
+  \makeatother
+}
+
+\makeatletter
+\def\numberedsection{\@ifnextchar[%]
+\numberedsectionwithtwoarguments\numberedsectionwithoneargument}
+\def\unnumberedsection{\@ifnextchar[%]
+\unnumberedsectionwithtwoarguments\unnumberedsectionwithoneargument}
+\def\numberedsectionwithoneargument#1{\numberedsectionwithtwoarguments[#1]{#1}}
+\def\unnumberedsectionwithoneargument#1{\unnumberedsectionwithtwoarguments[#1]{#1}}
+\def\numberedsectionwithtwoarguments[#1]#2{%
+  \ifhmode\par\fi
+  \removelastskip
+  \vskip 3ex\goodbreak
+  \refstepcounter{section}%
+  \noindent
+  \begingroup
+  \leavevmode\Large\bfseries\raggedright
+  \begin{picture}(0,0)
+    \put(0,0){\colorbox{kivitendoorange}{\parbox{0.7cm}{\hspace*{0.7cm}\\\vspace*{0.2cm}}}}
+  \end{picture}%
+  \hspace*{0.3cm}\textcolor{white}{\thesection{}.}%
+  \quad%
+  #2
+  \par
+  \endgroup
+  \vskip 2ex\nobreak
+  \addcontentsline{toc}{section}{\protect\numberline{\thesection{}.}#1}%
+}
+\def\unnumberedsectionwithtwoarguments[#1]#2{%
+  \ifhmode\par\fi
+  \removelastskip
+  \vskip 3ex\goodbreak
+  \noindent
+  \begingroup
+  \leavevmode\Large\bfseries\raggedright
+  \leavevmode\Large\bfseries\raggedright
+  #2
+  \par
+  \endgroup
+  \vskip 2ex\nobreak%
+}
+\makeatother
diff --git a/templates/print/marei/letter.tex b/templates/print/marei/letter.tex
new file mode 100644 (file)
index 0000000..1f29b1d
--- /dev/null
@@ -0,0 +1,53 @@
+% config: use-template-toolkit=1
+% config: tag-style=$( )$
+$( USE KiviLatex )$
+$( USE P )$
+$( SET customer = letter.customer_vendor )$
+\documentclass[paper=a4,fontsize=10pt]{scrartcl}
+\usepackage{kiviletter}
+$( KiviLatex.required_packages_for_html )$
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode}{$(template_meta.language.template_code)$}
+\newcommand{\lxmedia}{$(template_meta.media)$}
+\newcommand{\lxcurrency}{}
+\newcommand{\kivicompany}{$(employee_company)$}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+% laufende Kopfzeile:
+%\ourhead{}{}{$( KiviLatex.filter(letter.subject) )$}{$( KiviLatex.filter(letter.letternumber) )$}{$( KiviLatex.filter(letter.date.to_kivitendo) )$}
+\ourhead{}{}{}{}{}
+
+\begin{document}
+
+\setkomavar{date}{$( KiviLatex.filter(letter.date.to_kivitendo) )$}
+
+$( IF letter.reference )$
+\setkomavar*{yourref}{\ihrzeichen}
+\setkomavar{yourref}{$( KiviLatex.filter(letter.reference) )$}
+$( END )$
+
+$( IF letter.subject )$
+\setkomavar{subject}{$( KiviLatex.filter(letter.subject) )$}
+$( END )$
+
+\begin{letter}{
+$( KiviLatex.filter(customer.name) )$\strut\\
+$( KiviLatex.filter(letter.contact.formal_greeting) )$\strut\\
+$( KiviLatex.filter(customer.street) )$\strut\\
+$( KiviLatex.filter(customer.zipcode) )$ $( KiviLatex.filter(customer.city) )$\strut\\
+$( KiviLatex.filter(customer.country) )$%
+}
+
+\opening{$( KiviLatex.filter(letter.greeting) )$}
+\thispagestyle{kivitendo.letter.first}
+
+$( KiviLatex.filter_html(letter.body) )$
+
+\closing{\gruesse}
+
+\end{letter}
+
+\end{document}
diff --git a/templates/print/marei/pick_list.html b/templates/print/marei/pick_list.html
new file mode 100644 (file)
index 0000000..0de88eb
--- /dev/null
@@ -0,0 +1,154 @@
+
+<body bgcolor=ffffff>
+
+<table width=100%>
+  <tr>
+    <td width=10>&nbsp;</td>
+    
+    <td>
+      <table width=100%>
+       <tr>
+         <td>
+         <h4>
+         <%company%>
+         <br><%address%>
+         </h4>
+         </td>
+
+         <th><img src=http://localhost/lx-erp/lx-office-erp.png border=0 width=64 height=58></th>
+
+         <td align=right>
+         <h4>
+         Tel: <%tel%>
+         <br>Fax: <%fax%>
+         </h4>
+         </td>
+       </tr>
+
+       <tr>
+         <th colspan=3>
+           <h4>S A M M E L L I S T E</h4>
+         </th>
+       </tr>
+      </table>
+    </td>
+  </tr>
+
+  <tr>
+    <td>&nbsp;</td>
+
+    <td>
+      <table width=100% callspacing=0 cellpadding=0>
+        <tr bgcolor=000000>
+         <th width=50% align=left><font color=ffffff>Lieferanschrift:</th>
+         <th width=50%>&nbsp;</th>
+       </tr>
+
+       <tr valign=top>
+         <td><%shiptoname%>
+         <br><%shiptostreet%>
+         <br><%shiptozipcode%>
+         <br><%shiptocity%>
+         <br><%shiptocountry%>
+         </td>
+
+         <td>
+         <%if shiptocontact%>
+         <br>Kontakt: <%shiptocontact%>
+         <%end shiptocontact%>
+
+         <%if shiptophone%>
+         <br>Tel: <%shiptophone%>
+         <%end shiptophone%>
+
+         <%if shiptofax%>
+         <br>Fax: <%shiptofax%>
+         <%end shiptofax%>
+
+         <%shiptoemail%>
+         </td>
+       </tr>
+      </table>
+    </td>
+  </tr>
+
+  <tr height=5></tr>
+
+  <tr>
+    <td>&nbsp;</td>
+
+    <td>
+      <table width=100% border=1>
+        <tr>
+         <th width=17% align=left>BestellNr. #</th>
+         <th width=17% align=left>Datum</th>
+         <th width=17% align=left nowrap>Kontakt</th>
+         <%if warehouse%>
+         <th width=17% align=left>Lager</th>
+         <%end warehouse%>
+         <th width=17% align=left>Versandort</th>
+         <th width=15% align=left>Transportmittel</th>
+       </tr>
+
+        <tr>
+         <td><%ordnumber%>&nbsp;</td>
+
+         <%if shippingdate%>
+         <td><%shippingdate%></td>
+         <%end shippingdate%>
+
+         <%if not shippingdate%>
+         <td><%orddate%></td>
+         <%end shippingdate%>
+
+         <td><%employee%>&nbsp;</td>
+
+         <%if warehouse%>
+         <td><%warehouse%>&nbsp;</td>
+         <%end warehouse%>
+
+         <td><%shippingpoint%>&nbsp;</td>
+         <td><%shipvia%>&nbsp;</td>
+       </tr>
+      </table>
+    </td>
+  </tr>
+
+  <tr>
+    <td>&nbsp;</td>
+
+    <td>
+      <table width=100%>
+       <tr bgcolor=000000>
+         <th align=left><font color=ffffff>Pos</th>
+         <th align=left><font color=ffffff>Nummer</th>
+         <th align=left><font color=ffffff>Beschreibung</th>
+         <th><font color=ffffff>Menge</th>
+         <th><font color=ffffff>geliefert</th>
+         <th>&nbsp;</th>
+         <th><font color=ffffff>Lagerplatz</th>
+       </tr>
+
+        <%foreach number%>
+       <tr valign=top>
+         <td><%runningnumber%>
+         <td><%number%></td>
+         <td><%description%></td>
+         <td align=right><%qty%></td>
+         <td align=right>[&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;]</td>
+         <td><%unit%></td>
+         <td align=right><%bin%></td>
+       </tr>
+       <%end number%>
+      </table>
+    </td>
+  </tr>
+
+  <tr>
+    <td>&nbsp;</td>
+
+    <td><hr noshade></td>
+  </tr>
+
+</table>
+
diff --git a/templates/print/marei/pick_list.tex b/templates/print/marei/pick_list.tex
new file mode 100644 (file)
index 0000000..23736b5
--- /dev/null
@@ -0,0 +1,89 @@
+\documentclass[twoside,parskip=half-]{scrartcl}
+\usepackage[reffields,backaddress=false,addrfield=topaligned,nofooter]{kiviletter}
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+\KOMAoptions{fontsize=10pt}
+\begin{document}
+
+\setkomavar{title}{\sammelliste}
+
+\setkomavar{firsthead}{
+  \normalsize
+  \noindent\begin{tabular}[t]{@{}l@{}}
+    <%company%>\strut\\
+    <%address%>
+  \end{tabular}
+  \hfill
+  \begin{tabular}[t]{rr@{}}
+    Tel & < %tel%>\\
+    Fax & < %fax%>%
+  \end{tabular}
+  \rule{\linewidth}{\heavyrulewidth}
+}
+
+\makeatletter
+\setkomavar{location}{
+  \normalsize
+  <%shiptocontact%>%
+  <%if shiptophone%>%
+  \\\textTelefon : <%shiptophone%>
+  <%end shiptophone%>%
+  <%if shiptofax%>%
+  \\\textFax : <%shiptofax%>
+  <%end shiptofax%>%
+  \\%
+  <%shiptoemail%>%
+}
+\makeatother
+
+\setkomavar{backaddress}{\lieferanschrift}
+
+\begin{letter}{\strut%
+<%shiptoname%>\ifhmode\\\fi
+<%shiptostreet%>\ifhmode\\\fi
+<%shiptozipcode%>\ifhmode\\\fi
+<%shiptocity%>\ifhmode\\\fi
+<%shiptocountry%>%
+}
+
+\opening{}
+
+
+\begin{SimpleTabular}[colspec=*6X,headline={\bfseries\bestellnummer&\bfseries\datum&\bfseries\kontakt
+        <%if warehouse%>%
+        &\bfseries\lager%
+        <%end warehouse%>%
+        &\bfseries\lagerplatz&\bfseries\lieferungMit}]
+  <%ordnumber%>%
+  &%
+  <%if shippingdate%>%
+  <%shippingdate%>%
+  <%end shippingdate%>%
+  <%if not shippingdate%>%
+  <%orddate%>%
+  <%end shippingdate%>%
+  & <%employee%>%
+  <%if warehouse%>%
+  & <%warehouse%>%
+  <%end warehouse%>%
+  & <%shippingpoint%> & <%shipvia%> \\
+\end{SimpleTabular}
+
+\bigskip
+
+\begin{SimpleTabular}[colspec=rlXrcll,headline={\bfseries\position&\bfseries\nummer&\bfseries\beschreibung&\bfseries\menge&\bfseries\lagerausgang&&\bfseries\lagerplatz}]%
+  <%foreach number%>%
+  <%runningnumber%> & <%number%> & <%description%> &%
+  <%qty%> & [\hspace{1cm}] & <%unit%> & <%bin%> \\%
+  <%end number%>%
+\end{SimpleTabular}
+
+\end{letter}
+
+\end{document}
+
diff --git a/templates/print/marei/proforma.tex b/templates/print/marei/proforma.tex
new file mode 100644 (file)
index 0000000..6730a7e
--- /dev/null
@@ -0,0 +1,132 @@
+\documentclass[paper=a4,fontsize=10pt]{scrartcl}
+\usepackage{kiviletter}
+
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+
+% laufende Kopfzeile:
+\ourhead{\kundennummer}{<%customernumber%>}{\proformarechnung}{<%ordnumber%>}{<%invdate%>}
+
+
+\begin{document}
+
+\setkomavar{title}{
+  \proformarechnung~
+  \nr ~<%ordnumber%>%
+}
+\setkomavar*{date}{\datum}
+
+\setkomavar{date}{<%orddate%>}
+\setkomavar{customer}{<%customernumber%>}
+<%if cusordnumber%>%
+\setkomavar*{yourref}{\ihreBestellnummer}
+\setkomavar{yourref}{<%cusordnumber%>}
+<%end if%>%
+<%if quonumber%>\setkomavar{quote}{<%quonumber%>}<%end if%>%
+\setkomavar{fromname}{<%employee_name%>}
+\setkomavar{fromphone}{<%employee_tel%>}
+\setkomavar{fromemail}{<%employee_email%>}
+\setkomavar{transaction}{<%transaction_description%>}
+
+<%if shiptoname%>%
+\makeatletter
+\begin{lrbox}\shippingAddressBox
+  \parbox{\useplength{toaddrwidth}}{
+    \backaddr@format{\scriptsize\usekomafont{backaddress}%
+      \strut\abweichendeLieferadresse
+    }
+    \par\smallskip
+    \setlength{\parskip}{\z@}
+    \par
+    \normalsize
+    <%shiptoname%>\par
+    <%if shiptocontact%> <%shiptocontact%><%end if%>\par
+    <%shiptodepartment_1%>\par
+    <%shiptodepartment_2%>\par
+    <%shiptostreet%>\par
+    <%shiptozipcode%> <%shiptocity%>%
+  }
+\end{lrbox}
+\makeatother
+<%end if%>%
+
+\begin{letter}{
+<%name%>\strut\\
+<%if department_1%><%department_1%>\\<%end if%>%
+<%if department_2%><%department_2%>\\<%end if%>%
+<%cp_givenname%> <%cp_name%>\strut\\
+<%street%>\strut\\
+<%zipcode%> <%city%>\strut\\
+<%country%> \strut
+}
+
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\opening{
+\Ifstr{<%cp_name%>}{}
+{<%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}
+  {
+    \Ifstr{<%cp_gender%>}{f}
+        {\anredefrau}
+        {\anredeherr}
+      <%cp_title%> <%cp_name%>,
+    }
+  }
+\thispagestyle{kivitendo.letter.first}
+
+\auftragsformel
+
+\begin{PricingTabular*}%
+  % eigentliche Tabelle
+  \FakeTable{%
+  <%foreach number%>%
+  <%runningnumber%> &%
+  <%number%> &%
+  \textbf{<%description%>}%
+  <%if longdescription%>\ExtraDescription{<%longdescription%>}<%end longdescription%>%
+  <%if reqdate%>\ExtraDescription{\lieferdatum: <%reqdate%>}<%end reqdate%>%
+  <%if serialnumber%>\ExtraDescription{\seriennummer: <%serialnumber%>}<%end serialnumber%>%
+  <%if ean%>\ExtraDescription{\ean: <%ean%>}<%end ean%>%
+  <%if projectnumber%>\ExtraDescription{\projektnummer: <%projectnumber%>}<%end projectnumber%>%
+  &%
+  <%qty%> <%unit%> &%
+  <%sellprice%>&%
+  \Ifstr{<%p_discount%>}{0}{}{\sffamily\scriptsize{(-<%p_discount%>\,\%)}}%
+    <%linetotal%>\tabularnewline%
+    <%end number%>%
+  }%
+  \begin{PricingTotal}%
+    % Tabellenende letzte Seite%
+    \nettobetrag & <%subtotal%>\\%
+    <%foreach tax%>%
+    <%taxdescription%> & <%tax%>\\%
+    <%end tax%>%
+    \bfseries\schlussbetrag &  \bfseries <%invtotal%>\\%
+  \end{PricingTotal}%
+\end{PricingTabular*}
+
+<%if notes%>%
+<%notes%>%
+\medskip
+<%end if%>%
+
+<%if reqdate%>%
+\lieferungErfolgtAm ~<%reqdate%>. \\
+<%end if%>%
+
+\textit{\auftragpruefen}
+
+\closing{\gruesse}
+
+\end{letter}
+
+\end{document}
+
diff --git a/templates/print/marei/purchase_delivery_order.tex b/templates/print/marei/purchase_delivery_order.tex
new file mode 100644 (file)
index 0000000..ddadec8
--- /dev/null
@@ -0,0 +1,120 @@
+\documentclass[paper=a4,fontsize=10pt]{scrartcl}
+\usepackage{kiviletter}
+
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+<%if template_meta.formname == "supplier_delivery_order"%>
+  \renewcommand{\einkaufslieferschein} {\beistelllieferschein}
+<%end%>
+
+
+% laufende Kopfzeile:
+\ourhead{}{}{\einkaufslieferschein}{<%donumber%>}{<%dodate%>}
+
+\setkomavar*{date}{\datum}
+\setkomavar{date}{<%dodate%>}
+
+\setkomavar{fromname}{<%employee_name%>}
+\setkomavar{fromphone}{<%employee_tel%>}
+\setkomavar{fromemail}{<%employee_email%>}
+\setkomavar{title}{
+  \einkaufslieferschein~\nr ~<%donumber%>%
+}
+<%if ordnumber%>
+  \setkomavar{orderID}{<%ordnumber%>}
+<%end if%>%
+<%if cusordnumber%>%
+  \setkomavar*{yourref}{\unsereBestellnummer}
+  \setkomavar{yourref}{<%cusordnumber%>}
+<%end if%>%
+\setkomavar{transaction}{<%transaction_description%>}
+
+<%if globalprojectnumber%>%
+  \setkomavar{projectID}{<%globalprojectnumber%>}
+<%end globalprojectnumber%>%
+
+\setkomavar{transaction}{<%transaction_description%>}
+\setkomafont{extraDescription}{\scriptsize}
+
+<%if shiptoname%>%
+\makeatletter
+\begin{lrbox}\shippingAddressBox
+  \parbox{\useplength{toaddrwidth}}{
+    \backaddr@format{\scriptsize\usekomafont{backaddress}%
+      \strut\abweichendeLieferadresse
+    }
+    \par\smallskip
+    \setlength{\parskip}{\z@}
+    \par
+    \normalsize
+    <%shiptoname%>\par
+    <%if shiptocontact%> <%shiptocontact%><%end if%>\par
+    <%shiptodepartment_1%>\par
+    <%shiptodepartment_2%>\par
+    <%shiptostreet%>\par
+    <%shiptozipcode%> <%shiptocity%>%
+  }
+\end{lrbox}
+\makeatother
+<%end if%>%
+
+
+\begin{document}
+
+
+\begin{letter}{
+<%name%>\strut\\
+<%if department_1%><%department_1%>\\<%end if%>%
+<%if department_2%><%department_2%>\\<%end if%>%
+<%cp_givenname%> <%cp_name%>\strut\\
+<%street%>\strut\\
+<%zipcode%> <%city%>\strut\\
+<%country%> \strut
+}
+
+
+\opening{}
+\thispagestyle{kivitendo.letter.first}
+
+\begin{PricingTabular*}[columns={pos,amount,desc}]
+  % eigentliche Tabelle
+  \FakeTable{
+  <%foreach number%>%
+  <%runningnumber%> &
+  <%qty%> <%unit%> &
+  \textbf{<%description%>}
+    <%if longdescription%>\ExtraDescription{<%longdescription%>}<%end longdescription%>%
+    <%if reqdate%>\ExtraDescription{\lieferdatum: <%reqdate%>}<%end reqdate%>%
+    <%if serialnumber%>\ExtraDescription{\seriennummer: <%serialnumber%>}<%end serialnumber%>%
+    <%if ean%>\ExtraDescription{\ean: <%ean%>}<%end ean%>%
+    <%if projectnumber%>\ExtraDescription{\projektnummer: <%projectnumber%>}<%end projectnumber%>%
+    <%foreach si_number%>%
+    <%if si_chargenumber%>\ExtraDescription{\charge: <%si_chargenumber%> <%if si_bestbefore%> \mhd: <%si_bestbefore%><%end if%><%si_qty%>~<%si_unit%><%end si_chargenumber%>}%
+    <%end si_number%>%
+    \tabularnewline
+    <%end number%>%
+  }
+\end{PricingTabular*}
+
+\medskip
+
+<%if notes%>%
+<%notes%>%
+\medskip
+<%end if%>%
+
+<%if delivery_term%>%
+\lieferung ~<%delivery_term.description_long%>\\
+<%end delivery_term%>%
+
+\end{letter}
+
+\end{document}
diff --git a/templates/print/marei/purchase_order.html b/templates/print/marei/purchase_order.html
new file mode 100644 (file)
index 0000000..e83c67a
--- /dev/null
@@ -0,0 +1,188 @@
+
+<body bgcolor=ffffff>
+
+<table width=100%>
+<tr valign=bottom>
+  <td width=10>&nbsp;</td>
+  <td>
+  
+  <table width=100%>
+  <tr>
+    <td>
+      <h4>
+      <%company%>
+      <br><%address%>
+      </h4>
+    </td>
+
+    <td align=right>
+      <h4>
+      Telefon <%tel%>
+      <br>Telefax <%fax%>
+      </h4>
+    </td>
+  </tr>
+
+  <tr>
+    <th colspan=3>
+      <h4>B E S T E L L U N G</h4>
+    </th>
+  </tr>
+
+  </table>
+
+
+  <table width=100% callspacing=0 cellpadding=0>
+    
+  <tr>
+    <td align=right>
+    <table>
+    <tr>
+      <th align=right>Bestellungsdatum</th><td width=10>&nbsp;</td><td><%orddate%></td>
+    </tr>
+  
+    <tr>
+      <th align=right>Lieferbar bis</th><td width=10>&nbsp;</td><td><%reqdate%></td>
+    </tr>
+
+    <tr>
+      <th align=right>Bestellnummer</th><td>&nbsp;</td><td><%ordnumber%></td></tr>
+    </tr>
+  
+    <tr>
+      <td>&nbsp;</td>
+    </tr>
+    </td>
+    </table>
+  </tr>
+
+  <tr>
+    <td>
+    <table width=100%>
+    <tr bgcolor=000000>
+      <th align=left><font color=ffffff>An:</th>
+    </tr>
+
+    <tr>
+      <td><%name%>
+      <br><%street%>
+      <br><%zipcode%>
+      <br><%city%>
+      <br><%country%>
+      </td>
+    </tr>
+    </table>
+    </td>
+  </tr>
+
+  <tr>
+    <td>&nbsp;</td>
+  </tr>
+  
+  <tr>
+    <td>
+    <table width=100%>
+    <tr bgcolor=000000>
+<!--      <th align=right><font color=ffffff>No.</th>  -->
+      <th align=left><font color=ffffff>Nummer</th>
+      <th align=left><font color=ffffff>Artikel</th>
+      <th><font color=ffffff>Anz</th>
+      <th>&nbsp;</th>
+      <th><font color=ffffff>Preis</th>
+      <th><font color=ffffff>Total</th>
+    </tr>
+
+<%foreach number%>
+    <tr valign=top>
+<!--      <td align=right><%runningnumber%>.</td>
+adjust the colspan if you include this to shift subtotal one to the right
+-->
+      <td><%number%></td>
+      <td><%description%></td>
+      <td align=right><%qty%></td>
+      <td><%unit%></td>
+      <td align=right><%sellprice%></td>
+      <td align=right><%linetotal%></td>
+    </tr>
+<%end number%>
+
+    <tr>
+      <td colspan=6><hr noshade></td>
+    </tr>
+    
+    <tr>
+      <th colspan=4 align=right>Zwischensumme</th>
+      <td colspan=2 align=right><%subtotal%></td>
+    </tr>
+
+<%foreach tax%>
+    <tr>
+      <th colspan=4 align=right><%taxdescription%> @ <%taxrate%> %</th>
+      <td colspan=2 align=right><%tax%></td>
+    </tr>
+<%end tax%>
+
+    <tr>
+      <td colspan=2>&nbsp;</td>
+      <td colspan=4><hr noshade></td>
+    </tr>
+
+    <tr>
+      <td colspan=2>Netto <b><%terms%></b> Tage</td>
+      <th colspan=2 align=right>Total</th>
+      <th colspan=2 align=right><%total%></th>
+    </tr>
+
+    <tr>
+      <td>&nbsp;</td>
+    </tr>
+
+    </table>
+    </td>
+  </tr>
+
+<tr>
+  <td>
+  <table width=100%>
+    <tr valign=top>
+<%if notes%>
+      <td>Bemerkungen</td>
+      <td><%notes%></td>
+<%end notes%>
+      <td align=right>
+      Alle Preise in <b><%currency%></b>
+      <br><%shippingpoint%>
+      </td>
+    </tr>
+
+  </table>
+  </td>
+</tr>
+
+<tr><td>&nbsp;</td></tr>
+  
+<tr>
+  <td>
+  <table width=100%>
+  <tr valign=top>
+    <td><font size=-3>
+    &nbsp;
+    </font>
+    </td>
+    <td width=150>
+    X <hr noshade>
+    </td>
+  </tr>
+  </table>
+  </td>
+</tr>
+
+</table>
+
+</td>
+</tr>
+</table>
+
+</body>
+</html>
+
diff --git a/templates/print/marei/purchase_order.tex b/templates/print/marei/purchase_order.tex
new file mode 100644 (file)
index 0000000..9c8d610
--- /dev/null
@@ -0,0 +1,131 @@
+\documentclass[paper=a4,fontsize=10pt]{scrartcl}
+\usepackage{kiviletter}
+
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+
+% laufende Kopfzeile:
+\ourhead{}{}{\bestellung}{<%ordnumber%>}{<%orddate%>}
+
+
+\setkomavar*{date}{\datum}
+\setkomavar{date}{<%orddate%>}
+
+\setkomavar{fromname}{<%employee_name%>}
+\setkomavar{fromphone}{<%employee_tel%>}
+\setkomavar{fromemail}{<%employee_email%>}
+\setkomavar{title}{
+  \bestellung~
+  \nr~<%ordnumber%>%
+}
+
+<%if cusordnumber%>%
+  \setkomavar*{yourref}{\unsereBestellnummer}
+  \setkomavar{yourref}{<%cusordnumber%>}
+<%end if%>%
+\setkomavar{transaction}{<%transaction_description%>}
+
+<%if quonumber%>
+  \setkomavar{quote}{<%quonumber%>}
+<%end if%>%
+
+<%if shiptoname%>%
+\makeatletter
+\begin{lrbox}\shippingAddressBox
+  \parbox{\useplength{toaddrwidth}}{
+    \backaddr@format{\scriptsize\usekomafont{backaddress}%
+      \strut abweichende Lieferadresse
+    }
+    \par\smallskip
+    \setlength{\parskip}{\z@}
+    \par
+    \normalsize
+    <%shiptoname%>\par
+    <%if shiptocontact%> <%shiptocontact%><%end if%>\par
+    <%shiptodepartment_1%>\par
+    <%shiptodepartment_2%>\par
+    <%shiptostreet%>\par
+    <%shiptozipcode%> <%shiptocity%>%
+  }
+\end{lrbox}
+\makeatother
+<%end if%>%
+
+\begin{document}
+
+\begin{letter}{
+<%name%>\strut\\
+<%if department_1%><%department_1%>\\<%end if%>%
+<%if department_2%><%department_2%>\\<%end if%>%
+<%cp_givenname%> <%cp_name%>\strut\\
+<%street%>\strut\\
+<%zipcode%> <%city%>\strut\\
+<%country%> \strut
+}
+
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\opening{
+\Ifstr{<%cp_name%>}{}
+{<%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}
+  {
+    \Ifstr{<%cp_gender%>}{f}
+        {\anredefrau}
+        {\anredeherr}
+      <%cp_title%> <%cp_name%>,
+    }
+  }
+\thispagestyle{kivitendo.letter.first}
+
+\bestellformel
+
+\begin{PricingTabular*}
+  % eigentliche Tabelle
+  \FakeTable{
+  <%foreach number%>%
+  <%runningnumber%> &%
+  <%number%> &%
+  \textbf{<%description%>}%
+  <%if longdescription%>\ExtraDescription{<%longdescription%>}<%end longdescription%>%
+  <%if reqdate%>\ExtraDescription{\lieferdatum: <%reqdate%>}<%end reqdate%>%
+  <%if serialnumber%>\ExtraDescription{\seriennummer: <%serialnumber%>}<%end serialnumber%>%
+  <%if ean%>\ExtraDescription{\ean: <%ean%>}<%end ean%>%
+  <%if projectnumber%>\ExtraDescription{\projektnummer: <%projectnumber%>}<%end projectnumber%>%
+  &%
+  <%qty%> <%unit%> &%
+  <%sellprice%>&%
+  \Ifstr{<%p_discount%>}{0}{}{\sffamily\scriptsize{(-<%p_discount%>\,\%)}}%
+    <%linetotal%>\tabularnewline
+    <%end number%>%
+  }%
+  \begin{PricingTotal}%
+    % Tabellenende letzte Seite
+    \nettobetrag & <%subtotal%>\\%
+    <%foreach tax%>%
+    <%taxdescription%> & <%tax%>\\%
+    <%end tax%>%
+    \bfseries\schlussbetrag &  \bfseries <%ordtotal%>\\%
+  \end{PricingTotal}
+\end{PricingTabular*}
+
+<%if notes%>%
+<%notes%>%
+\medskip
+<%end if%>%
+
+<%if delivery_term%>%
+\lieferung ~<%delivery_term.description_long%>\\
+<%end delivery_term%>%
+
+\closing{\gruesse}
+\end{letter}
+
+\end{document}
diff --git a/templates/print/marei/receipt.tex b/templates/print/marei/receipt.tex
new file mode 100644 (file)
index 0000000..ad428f7
--- /dev/null
@@ -0,0 +1,54 @@
+\documentclass[paper=a4]{scrartcl}
+\usepackage[reffields,backaddress=false]{kiviletter}
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+\KOMAoptions{fontsize=9pt}
+
+
+\setplength{firstheadvpos}{4cm}
+
+\setkomavar{firsthead}{
+  \noindent\begin{tabular}[t]{@{}l@{}}
+    <%company%>\strut\\
+    <%address%>
+  \end{tabular}
+  \hfill
+  <%source%>\par
+  \medskip
+  <%text\_amount%> \dotfill <%decimal%>/100 \par\smallskip
+  \hfill <%datepaid%> \hspace{2cm}\strut<%amount%>
+}
+
+
+\begin{document}
+
+\begin{letter}{
+<%name%>\ifhmode\\\fi
+<%street%>\ifhmode\\\fi
+<%zipcode%> <%city%>\ifhmode\\\fi
+<%country%>%
+}
+
+\opening{<%company%>}
+\pagestyle{empty}
+
+<%name%> \hfill <%datepaid%> \hfill <%source%>%
+
+\begin{SimpleTabular}[colspec=lXrr,headline={\bfseries\rechnung&\bfseries\ausgestellt&\bfseries\faellig&\bfseries\verrechnet}]
+  <%foreach invnumber%>%
+  <%invnumber%> & <%invdate%> & <%due%> & <%paid%> \\
+  <%end invnumber%>%
+\end{SimpleTabular}
+
+\end{letter}
+
+\end{document}
+
diff --git a/templates/print/marei/request_quotation.html b/templates/print/marei/request_quotation.html
new file mode 100644 (file)
index 0000000..6ff0036
--- /dev/null
@@ -0,0 +1,194 @@
+
+<body bgcolor=ffffff>
+
+<table width=100%>
+<tr valign=bottom>
+  <td width=10>&nbsp;</td>
+  <td>
+  
+  <table width=100%>
+  <tr>
+    <td>
+      <h4>
+      <%company%>
+      <br><%address%>
+      </h4>
+    </td>
+
+    <td><img src=http://localhost/lx-erp/lx-office-erp.png border=0 width=64 height=58>
+    </td>
+
+    <td align=right>
+      <h4>
+      Tel: <%tel%>
+      <br>Fax: <%fax%>
+      </h4>
+    </td>
+  </tr>
+
+  <tr>
+    <th colspan=3>
+      <h4>A N F R A G E</h4>
+    </th>
+  </tr>
+
+  </table>
+
+
+  <table width=100% callspacing=0 cellpadding=0>
+
+  <tr>
+    <td>
+    <table width=100%>
+    <tr bgcolor=000000>
+      <th align=left width=50%><font color=ffffff>Rechnungsanschrift:</th>
+      <th align=left width=50%><font color=ffffff>Lieferanschrift:</th>
+    </tr>
+
+    <tr valign=top>
+      <td><%name%>
+      <br><%street%>
+      <br><%zipcode%>
+      <br><%city%>
+      <br><%country%>
+<br>
+<%if contact%>
+<br>Kontakt: <%contact%>
+<%end contact%>
+<%if vendorphone%>
+<br>Tel: <%vendorphone%>
+<%end vendorphone%>
+<%if vendorfax%>
+<br>Fax: <%vendorfax%>
+<%end vendorfax%>
+      </td>
+
+      <td><%shiptoname%>
+      <br><%shiptostreet%>
+      <br><%shiptozipcode%>
+      <br><%shiptocity%>
+      <br><%shiptocountry%>
+<br>
+<%if shiptocontact%>
+<br>Kontakt: <%shiptocontact%>
+<%end shiptocontact%>
+<%if shiptophone%>
+<br>Tel: <%shiptophone%>
+<%end shiptophone%>
+<%if shiptofax%>
+<br>Fax: <%shiptofax%>
+<%end shiptofax%>
+    </tr>
+    </table>
+    </td>
+  </tr>
+
+  <tr><td>&nbsp;</td></tr>
+
+  <tr>
+    <td colspan=2>
+    <table width=100% border=1>
+    <tr>
+      <th width=17% align=left>AnfrageNr. #</th>
+      <th width=17% align=left>Datum</th>
+      <th width=17% align=left>Erforderlich am</th>
+      <th width=17% align=left>Kontakt</th>
+      <th width=17% align=left>Lagerplatz</th>
+      <th width=15% align=left>Versand mit:</th>
+    </tr>
+
+    <tr>
+      <td><%quonumber%></td>
+      <td><%quodate%></td>
+      <td><%reqdate%></td>
+      <td><%employee%></td>
+      <td><%shippingpoint%></td>
+      <td><%shipvia%></td>
+    </tr>
+    </table>
+    </td>
+  </tr>
+
+  <tr height="10"></tr>
+
+  <tr>
+    <td>Bitte teilen Sie uns Preise und Lieferzeit für folgende Artikel mit:</td>
+  </tr>
+
+  <tr height="10"></tr>
+
+  <tr>
+    <td>
+    <table width=100%>
+    <tr>
+<!--      <th align=right>No.</th>  -->
+      <th align=left>ArtNr.</th>
+      <th align=left>Beschreibung</th>
+      <th>Menge</th>
+      <th>&nbsp;</th>
+      <th>Lieferung</th>
+      <th>Stückpreis</th>
+      <th>Gesamtpreis</th>
+    </tr>
+
+<%foreach number%>
+    <tr valign=top>
+<!--      <td align=right><%runningnumber%>.</td>
+other per line item variables available <%reqdate%>
+adjust the colspan if you include this to shift subtotal one to the right
+-->
+      <td><%number%></td>
+      <td><%description%></td>
+      <td align=right><%qty%></td>
+      <td><%unit%></td>
+
+    </tr>
+<%end number%>
+
+    <tr>
+      <td colspan=7><hr noshade></td>
+    </tr>
+
+    </table>
+    </td>
+  </tr>
+
+<tr>
+  <td>
+  <table width=100%>
+<%if notes%>
+    <tr valign=top>
+      <td>Bemerkungen</td>
+      <td><%notes%></td>
+    </tr>
+<%end notes%>
+
+  </table>
+  </td>
+</tr>
+
+<tr><td>&nbsp;</td></tr>
+  
+<tr>
+  <td>
+  <table width=100%>
+  <tr valign=top>
+    <td width=70%>&nbsp;</td>
+
+    <td width=30%>
+    X <hr noshade>
+    </td>
+  </tr>
+  </table>
+  </td>
+</tr>
+
+</table>
+
+</td>
+</tr>
+</table>
+
+</body>
+</html>
+
diff --git a/templates/print/marei/request_quotation.tex b/templates/print/marei/request_quotation.tex
new file mode 100644 (file)
index 0000000..0cfc119
--- /dev/null
@@ -0,0 +1,103 @@
+\documentclass[paper=a4,fontsize=10pt]{scrartcl}
+\usepackage{kiviletter}
+
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+
+% laufende Kopfzeile:
+\ourhead{}{}{\anfrage}{<%quonumber%>}{<%transdate%>}
+
+
+\setkomavar*{date}{\datum}
+\setkomavar{date}{<%transdate%>}
+\setkomavar{customer}{<%customernumber%>}
+\setkomavar{fromname}{<%employee_name%>}
+\setkomavar{fromphone}{<%employee_tel%>}
+\setkomavar{fromemail}{<%employee_email%>}
+\setkomavar{title}{
+  \anfrage~
+  \nr ~<%quonumber%>%
+}
+<%if globalprojectnumber%>%
+  \setkomavar{projectID}{<%globalprojectnumber%>}
+<%end globalprojectnumber%>%
+
+\setkomavar{transaction}{<%transaction_description%>}
+
+\begin{document}
+
+\begin{letter}{
+<%name%>\strut\\
+<%if department_1%><%department_1%>\\<%end if%>%
+<%if department_2%><%department_2%>\\<%end if%>%
+<%cp_givenname%> <%cp_name%>\strut\\
+<%street%>\strut\\
+<%zipcode%> <%city%>\strut\\
+<%country%> \strut
+}
+
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\opening{
+\Ifstr{<%cp_name%>}{}
+{<%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}
+  {
+    \Ifstr{<%cp_gender%>}{f}
+        {\anredefrau}
+        {\anredeherr}
+      <%cp_title%> <%cp_name%>,
+    }
+  }
+\thispagestyle{kivitendo.letter.first}
+
+\anfrageformel
+
+\begin{PricingTabular*}[columns={pos,amount,desc}]
+  % eigentliche Tabelle
+  \FakeTable{
+  <%foreach number%>%
+  <%runningnumber%> &
+  <%qty%> <%unit%> &
+  \textbf{<%description%>}
+  <%if longdescription%>\ExtraDescription{<%longdescription%>}<%end longdescription%>%
+  <%if projectnumber%>\ExtraDescription{\projektnummer: <%projectnumber%>}<%end projectnumber%>%
+  <%if make%>%
+  <%foreach make%>%
+  \Ifstr{<%make%>}{<%name%>}{\ExtraDescription{\artikelnummer: <%model%>}}{}
+    <%end foreach%>%
+    <%end if%>%
+    \tabularnewline
+    <%end number%>%
+  }%
+\end{PricingTabular*}
+
+
+<%if notes%>%
+<%notes%>%
+\medskip
+<%end if%>%
+
+
+<%if delivery_term%>%
+\lieferung ~<%delivery_term.description_long%>\\
+<%end delivery_term%>%
+
+<%if reqdate%>%
+\anfrageBenoetigtBis~<%reqdate%>.
+<%end if%>%
+
+\anfragedanke
+
+\closing{\gruesse}
+
+\end{letter}
+
+\end{document}
diff --git a/templates/print/marei/requirement_spec.tex b/templates/print/marei/requirement_spec.tex
new file mode 100644 (file)
index 0000000..bd08dc6
--- /dev/null
@@ -0,0 +1,195 @@
+% config: use-template-toolkit=1
+% config: tag-style=$( )$
+$( USE KiviLatex )$
+$( USE P )$
+\documentclass{scrartcl}
+
+\usepackage[reqspeclogo,$( IF !rspec.version )$draftlogo$( ELSE )$secondpagelogo$( END )$]{kivitendo}
+$( KiviLatex.required_packages_for_html )$
+
+\kivitendobgsettings
+
+\setlength{\LTpre}{0pt}
+\setlength{\LTpost}{0pt}
+
+\renewcommand{\kivitendosecondfoot}{%
+  \parbox{12cm}{%
+    \defaultfont\scriptsize%
+    $( KiviLatex.filter(rspec.displayable_name) )$\\
+    $( !rspec.version ? "Arbeitskopie ohne Version" : "Version " _ rspec.version.version_number _ " vom " _ rspec.version.itime.to_kivitendo(precision='minute') )$
+
+    \vspace*{0.2cm}%
+    Seite \thepage%
+  }%
+}
+
+\reqspecsectionstyle
+
+\begin{document}
+
+%% Titelseite
+
+\setlongtables
+\defaultfont
+
+\begin{picture}(0,0)
+  \put(3.5,-5){%
+    \begin{minipage}[t][6cm]{12cm}
+      \Large
+      \textcolor{kivitendodarkred}{$( KiviLatex.filter(rspec.type.description) )$}
+
+      \huge
+      $( KiviLatex.filter(rspec.customer.name) )$
+
+      \vspace*{0.5cm}
+      \Large
+      $( KiviLatex.filter(rspec.title) )$
+      \normalsize
+      %$( IF rspec.version )$
+
+      Version $( KiviLatex.filter(rspec.version.version_number) )$
+      %$( END )$
+    \end{minipage}%
+  }
+\end{picture}
+
+%% Inhaltsverzeichnis
+
+%\newpage
+
+%\tableofcontents
+
+%%%% Deaktiviertes Beispiel, wie benutzerdefinierte Variablen ausgegeben werden können: %%%%
+%% \newpage
+%%
+%% \section{Benutzerdefinierte Variablen}
+%%
+%% %$ ( FOREACH cvar = rspec.cvars_by_config ) $
+%% Name: $ ( KiviLatex.filter(cvar.config.name) ) $
+%%
+%% Wert:% $ ( IF cvar.config.type == 'htmlfield' ) $
+%% $ ( KiviLatex.filter_html(cvar.value_as_text) ) $
+%% % $ ( ELSE ) $
+%% $ ( KiviLatex.filter(cvar.value_as_text) ) $
+%% % $ ( END ) $
+%%
+%% %$ ( END ) $
+%%%% ENDE Beispiel für benutzerdefinierte Variablen %%%%
+
+%% Versionen
+\newpage
+
+\section{Versionen}
+
+\vspace*{0.7cm}
+
+%$( SET working_copy     = rspec.working_copy_id ? rspec.working_copy : rspec )$
+%$( SET versioned_copies = rspec.version ? working_copy.versioned_copies_sorted(max_version_number = rspec.version.version_number) : working_copy.versioned_copies_sorted )$
+%$( IF !versioned_copies.size )$
+Bisher wurden noch keine Versionen angelegt.
+%$( ELSE )$
+\begin{longtable}{|p{2cm}|p{2cm}|p{12cm}|}
+  \hline
+  \multicolumn{1}{|r}{\small Version}                                                                                &
+  \multicolumn{1}{|r|}{\small Datum}                                                                                 &
+  \small Beschreibung                                                                                                  \\
+  \hline
+  %$( FOREACH versioned_copy = versioned_copies )$
+  \multicolumn{1}{|r}{\small $( KiviLatex.filter(versioned_copy.version.version_number) )$}                          &
+  \multicolumn{1}{|r|}{\small $( KiviLatex.filter(versioned_copy.version.itime.to_kivitendo(precision='minute')) )$} &
+  \small $( KiviLatex.filter(versioned_copy.version.description) )$                                                    \\
+  %$( END )$
+  \hline
+\end{longtable}
+%$( END )$
+
+%$( BLOCK picture_outputter )$
+%  $( SET width_cm = (picture.picture_width / 150.0) * 2.54 )$
+%  $( SET width_cm = width_cm < 16.4 ? width_cm : 16.4 )$
+\begin{figure}[h!]
+  \centering
+  \includegraphics[width=$( width_cm )$cm,keepaspectratio]{$( picture.print_file_name )$}
+
+  \mbox{Abbildung $( picture.number )$: $( KiviLatex.filter(picture.description ? picture.description : picture.picture_file_name) )$}
+\end{figure}
+%$( END )$
+
+%$( BLOCK text_block_outputter )$
+%  $( SET text_blocks = rspec.text_blocks_sorted(output_position=output_position) )$
+%  $( IF text_blocks.size )$
+
+\newpage
+
+\section{$( heading )$}
+
+%    $( FOREACH text_block = text_blocks )$
+
+\subsection{$( KiviLatex.filter(text_block.title) )$}
+
+$( KiviLatex.filter_html(text_block.text_as_restricted_html) )$
+
+%      $( FOREACH picture = text_block.pictures_sorted.as_list )$
+$( PROCESS picture_outputter picture=picture )$
+%      $( END )$
+
+%    $( END )$
+%  $( END )$
+%$( END )$
+
+%% Textblöcke davor
+$( PROCESS text_block_outputter output_position=0 heading='Allgemeines' )$
+
+%% Abschnitte und Funktionsblöcke
+\newpage
+
+\section{Spezifikation}
+
+\setlength{\LTpre}{-0.3cm}
+
+
+%$( FOREACH top_item = rspec.sections_sorted )$
+
+\subsection{Abschnitt $( KiviLatex.filter(top_item.fb_number) )$: $( KiviLatex.filter(top_item.title) )$}
+
+%  $( IF top_item.description )$
+$( KiviLatex.filter_html(top_item.description_as_restricted_html.replace('\r', '').replace('\n+\Z', '')) )$
+
+\vspace{0.5cm}
+%  $( END )$
+%  $( FOREACH item = top_item.children_sorted )$
+\parbox[t]{1.0cm}{\textcolor{kivitendodarkred}{$>>>$}}%
+\parbox[t]{15.0cm}{%
+  \begin{longtable}{p{2.8cm}p{11.7cm}}
+    Funktionsblock & $( KiviLatex.filter(item.fb_number) )$                                       \\
+    Beschreibung   & $( KiviLatex.filter_html(item.description_as_restricted_html) )$             \\
+    Abhängigkeiten & $( KiviLatex.filter(item.presenter.requirement_spec_item_dependency_list) )$
+  \end{longtable}}
+
+%    $( FOREACH sub_item = item.children_sorted )$
+\hspace*{1.15cm}\rule{15.2cm}{0.2pt}\\
+\hspace*{1.0cm}%
+\parbox[t]{15.0cm}{%
+  \begin{longtable}{p{2.8cm}p{11.7cm}}
+    Unterfunktionsblock & $( KiviLatex.filter(sub_item.fb_number) )$                                       \\
+    Beschreibung        & $( KiviLatex.filter_html(sub_item.description_as_restricted_html) )$             \\
+    Abhängigkeiten      & $( KiviLatex.filter(sub_item.presenter.requirement_spec_item_dependency_list) )$
+  \end{longtable}}
+
+%    $( END )$
+
+%    $( UNLESS loop.last )$
+\vspace{0.2cm}
+\hrule
+\vspace{0.4cm}
+
+%    $( END )$
+
+%  $( END )$
+%
+%$( END )$
+
+%% Textblöcke dahinter
+$( PROCESS text_block_outputter output_position=1 heading='Weitere Punkte' )$
+
+
+\end{document}
diff --git a/templates/print/marei/sales_delivery_order.tex b/templates/print/marei/sales_delivery_order.tex
new file mode 100644 (file)
index 0000000..2811e50
--- /dev/null
@@ -0,0 +1,102 @@
+\documentclass[paper=a4,fontsize=10pt]{scrartcl}
+\usepackage{kiviletter}
+
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+
+% laufende Kopfzeile:
+\ourhead{\kundennummer}{<%customernumber%>}{\lieferschein}{<%donumber%>}{<%dodate%>}
+
+\setkomavar*{date}{\datum}
+\setkomavar{date}{<%dodate%>}
+\setkomavar{fromname}{<%employee_name%>}
+\setkomavar{fromphone}{<%employee_tel%>}
+\setkomavar{fromemail}{<%employee_email%>}
+\setkomavar{title}{
+  \lieferschein~
+  \nr~<%donumber%>%
+}
+<%if ordnumber%>
+  \setkomavar{orderID}{<%ordnumber%>}
+<%end if%>%
+<%if cusordnumber%>%
+  \setkomavar*{yourref}{\unsereBestellnummer}
+  \setkomavar{yourref}{<%cusordnumber%>}
+<%end if%>%
+\setkomavar{transaction}{<%transaction_description%>}
+
+<%if globalprojectnumber%>%
+  \setkomavar{projectID}{<%globalprojectnumber%>}
+<%end globalprojectnumber%>%
+
+\setkomavar{transaction}{<%transaction_description%>}
+\setkomafont{extraDescription}{\scriptsize}
+
+\begin{document}
+
+\begin{letter}{
+\Ifstr{<%shiptoname%>}{}{ % KEINE ABWEICHENDE LIEFERADRESSE
+  <%name%>\strut\\
+  <%if department_1%><%department_1%>\\<%end if%>%
+  <%if department_2%><%department_2%>\\<%end if%>%
+  <%cp_givenname%> <%cp_name%>\strut\\
+  <%street%>\strut\\
+  <%zipcode%> <%city%>\strut\\
+  <%country%> \strut
+}{ % ABWEICHENDE LIEFERADRESSE (Aus Stammdaten oder Beleg)
+  <%shiptoname%>\strut\\
+  <%if shiptocontact%> <%shiptocontact%><%end if%>\strut\\
+  <%shiptodepartment_1%>\strut\\
+  <%shiptodepartment_2%>\strut\\
+  <%shiptostreet%>\strut\\
+  <%shiptozipcode%> <%shiptocity%>\strut
+} % ende ifthenelse LIEFERADRESSE
+}
+
+\opening{}
+\thispagestyle{kivitendo.letter.first}
+
+\begin{PricingTabular*}[columns={pos, id, desc, amount}]
+  % eigentliche Tabelle
+  \FakeTable{
+  <%foreach number%>%
+  <%runningnumber%> &
+  <%number%> &
+  \textbf{<%description%>}
+    <%if longdescription%>\ExtraDescription{<%longdescription%>}<%end longdescription%>%
+    <%if reqdate%>\ExtraDescription{\lieferdatum: <%reqdate%>}<%end reqdate%>%
+    <%if serialnumber%>\ExtraDescription{\seriennummer: <%serialnumber%>}<%end serialnumber%>%
+    <%if ean%>\ExtraDescription{\ean: <%ean%>}<%end ean%>%
+    <%if projectnumber%>\ExtraDescription{\projektnummer: <%projectnumber%>}<%end projectnumber%>%
+    <%foreach si_number%>%
+    <%if si_chargenumber%>\ExtraDescription{\charge: <%si_chargenumber%> <%if si_bestbefore%> \mhd: <%si_bestbefore%><%end if%><%si_qty%>~<%si_unit%><%end si_chargenumber%>}%
+    <%end si_number%>%
+    &
+    <%qty%> <%unit%>%
+    \tabularnewline
+    <%end number%>%
+  }
+\end{PricingTabular*}
+
+\medskip
+
+<%if notes%>%
+<%notes%>%
+\medskip
+<%end if%>%
+
+<%if delivery_term%>%
+\lieferung ~<%delivery_term.description_long%>\\
+<%end delivery_term%>%
+
+\end{letter}
+
+\end{document}
diff --git a/templates/print/marei/sales_order.html b/templates/print/marei/sales_order.html
new file mode 100644 (file)
index 0000000..260d1ed
--- /dev/null
@@ -0,0 +1,218 @@
+
+<body bgcolor=ffffff>
+
+<table width=100%>
+<tr valign=bottom>
+  <td width=10>&nbsp;</td>
+  <td>
+
+  <table width=100%>
+  <tr>
+    <td>
+      <h4>
+      <%company%>
+      <br><%address%>
+      </h4>
+    </td>
+
+    <td align=right>
+      <h4>
+      Telefon <%tel%>
+      <br>Telefax <%fax%>
+      </h4>
+    </td>
+  </tr>
+
+  <tr>
+    <th colspan=3>
+      <h4>B E S T E L L U N G</h4>
+    </th>
+  </tr>
+
+  </table>
+
+
+  <table width=100% callspacing=0 cellpadding=0>
+
+  <tr>
+    <td align=right>
+    <table>
+    <tr>
+      <th align=right>Bestelldatum</th><td width=10>&nbsp;</td><td><%orddate%></td>
+    </tr>
+
+    <tr>
+      <th align=right>Lieferbar bei</th><td width=10>&nbsp;</td><td><%reqdate%></td>
+    </tr>
+
+    <tr>
+      <th align=right>Bestellnummer</th><td>&nbsp;</td><td><%ordnumber%></td></tr>
+    </tr>
+
+    <tr>
+      <td>&nbsp;</td>
+    </tr>
+    </td>
+    </table>
+  </tr>
+
+  <tr>
+    <td>
+    <table width=100%>
+    <tr bgcolor=000000>
+      <th align=left><font color=ffffff>Verrechnet An:</th>
+      <th align=left><font color=ffffff>Lieferaddresse:</th>
+    </tr>
+
+    <tr>
+      <td><%name%>
+      <br><%street%>
+      <br><%zipcode%>
+      <br><%city%>
+      <br><%country%>
+      </td>
+
+      <td><%shiptoname%>
+      <br><%shiptostreet%>
+      <br><%shiptozipcode%>
+      <br><%shiptocity%>
+      <br><%shiptocountry%>
+      </td>
+    </tr>
+    </table>
+    </td>
+  </tr>
+
+  <tr>
+    <td>&nbsp;</td>
+  </tr>
+
+  <tr>
+    <td>
+    <table width=100%>
+    <tr bgcolor=000000>
+<!--      <th align=right><font color=ffffff>No.</th>  -->
+      <th align=left><font color=ffffff>Nummer</th>
+      <th align=left><font color=ffffff>Artikel</th>
+      <th><font color=ffffff>Anz</th>
+      <th>&nbsp;</th>
+      <th><font color=ffffff>Preis</th>
+      <th><font color=ffffff>Rab</th>
+      <th><font color=ffffff>Total</th>
+    </tr>
+
+<%foreach number%>
+    <tr valign=top>
+<!--      <td align=right><%runningnumber%>.</td>
+adjust the colspan if you include this to shift subtotal one to the right
+-->
+      <td><%number%></td>
+      <td><%description%></td>
+      <td align=right><%qty%></td>
+      <td><%unit%></td>
+      <td align=right><%sellprice%></td>
+      <td align=right><%discount%></td>
+      <td align=right><%linetotal%></td>
+    </tr>
+<%end number%>
+
+    <tr>
+      <td colspan=7><hr noshade></td>
+    </tr>
+
+<%if taxincluded%>
+    <tr>
+      <th colspan=5 align=right>Total</th>
+      <td colspan=2 align=right><%ordtotal%></td>
+    </tr>
+<%end taxincluded%>
+
+<%if not taxincluded%>
+    <tr>
+      <th colspan=5 align=right>Zwischensumme</th>
+      <td colspan=2 align=right><%subtotal%></td>
+    </tr>
+<%end taxincluded%>
+
+<%foreach tax%>
+    <tr>
+      <th colspan=5 align=right><%taxdescription%> auf <%taxbase%> @ <%taxrate%> %</th>
+      <td colspan=2 align=right><%tax%></td>
+    </tr>
+<%end tax%>
+
+<%if rounding%>
+      <th colspan=5 align=right>Rundung</th>
+      <td colspan=2 align=right><%rounding%></td>
+<%end rounding%>
+
+    <tr>
+      <td colspan=2>&nbsp;</td>
+      <td colspan=5><hr noshade></td>
+    </tr>
+
+    <tr>
+      <td colspan=3>Netto <b><%terms%></b> Tage</td>
+      <th colspan=2 align=right>Total</th>
+      <th colspan=2 align=right><%ordtotal%></th>
+    </tr>
+<%if taxincluded%>
+    <tr>
+      <td colspan=3>Steuern sind im Preis inbegriffen</td>
+    </tr>
+<%end taxincluded%>
+
+    <tr>
+      <td>&nbsp;</td>
+    </tr>
+
+    </table>
+    </td>
+  </tr>
+
+<tr>
+  <td>
+  <table width=100%>
+    <tr valign=top>
+<%if notes%>
+      <td>Bemerkungen</td>
+      <td><%notes%></td>
+<%end notes%>
+      <td align=right>
+      Alle Preise in <b><%currency%></b>
+      <br><%shippingpoint%>
+      </td>
+    </tr>
+
+  </table>
+  </td>
+</tr>
+
+<tr><td>&nbsp;</td></tr>
+
+<tr>
+  <td>
+  <table width=100%>
+  <tr valign=top>
+    <td><font size=-3>
+    Spezialprodukte werden nicht zurückgenommen. Für alle anderen Waren
+    wird eine 10% Stornogebühr verrechnet.
+    </font>
+    </td>
+    <td width=150>
+    X <hr noshade>
+    </td>
+  </tr>
+  </table>
+  </td>
+</tr>
+
+</table>
+
+</td>
+</tr>
+</table>
+
+</body>
+</html>
+
diff --git a/templates/print/marei/sales_order.tex b/templates/print/marei/sales_order.tex
new file mode 100644 (file)
index 0000000..e0c8f30
--- /dev/null
@@ -0,0 +1,142 @@
+\documentclass[paper=a4,fontsize=10pt]{scrartcl}
+\usepackage{kiviletter}
+
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+
+% laufende Kopfzeile:
+\ourhead{\kundennummer}{<%customernumber%>}{\auftragsbestaetigung}{<%ordnumber%>}{<%orddate%>}
+
+
+
+\setkomavar*{date}{\datum}
+\setkomavar{date}{<%orddate%>}
+\setkomavar{customer}{<%customernumber%>}
+\setkomavar{fromname}{<%employee_name%>}
+\setkomavar{fromphone}{<%employee_tel%>}
+\setkomavar{fromemail}{<%employee_email%>}
+\setkomavar{title}{
+  \auftragsbestaetigung~
+  \nr~<%ordnumber%>%
+}
+<%if tax_point%>%
+  \setkomavar*{taxpoint}{\leistungsdatum}
+  \setkomavar{taxpoint}{<%tax_point%>}
+<%end if%>%
+<%if cusordnumber%>%
+  \setkomavar*{yourref}{\ihreBestellnummer}
+  \setkomavar{yourref}{<%cusordnumber%>}
+<%end if%>%
+\setkomavar{transaction}{<%transaction_description%>}
+
+<%if quonumber%>
+  \setkomavar{quote}{<%quonumber%>}
+<%end if%>%
+
+<%if shiptoname%>%
+\makeatletter
+\begin{lrbox}\shippingAddressBox
+  \parbox{\useplength{toaddrwidth}}{
+    \backaddr@format{\scriptsize\usekomafont{backaddress}%
+      \strut abweichende Lieferadresse
+    }
+    \par\smallskip
+    \setlength{\parskip}{\z@}
+    \par
+    \normalsize
+    <%shiptoname%>\par
+    <%if shiptocontact%> <%shiptocontact%><%end if%>\par
+    <%shiptodepartment_1%>\par
+    <%shiptodepartment_2%>\par
+    <%shiptostreet%>\par
+    <%shiptozipcode%> <%shiptocity%>%
+  }
+\end{lrbox}
+\makeatother
+<%end if%>%
+
+\begin{document}
+
+\begin{letter}{
+<%name%>\strut\\
+<%if department_1%><%department_1%>\\<%end if%>%
+<%if department_2%><%department_2%>\\<%end if%>%
+<%cp_givenname%> <%cp_name%>\strut\\
+<%street%>\strut\\
+<%zipcode%> <%city%>\strut\\
+<%country%> \strut
+}
+
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\opening{
+\Ifstr{<%cp_name%>}{}
+{<%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}
+  {
+    \Ifstr{<%cp_gender%>}{f}
+        {\anredefrau}
+        {\anredeherr}
+      <%cp_title%> <%cp_name%>,
+    }
+  }
+\thispagestyle{kivitendo.letter.first}
+
+\auftragsformel
+
+\begin{PricingTabular*}
+  % eigentliche Tabelle
+  \FakeTable{
+  <%foreach number%>%
+  <%runningnumber%> &%
+  <%number%> &%
+  \textbf{<%description%>}%
+  <%if longdescription%>\ExtraDescription{<%longdescription%>}<%end longdescription%>%
+  <%if reqdate%>\ExtraDescription{\lieferdatum: <%reqdate%>}<%end reqdate%>%
+  <%if serialnumber%>\ExtraDescription{\seriennummer: <%serialnumber%>}<%end serialnumber%>%
+  <%if ean%>\ExtraDescription{\ean: <%ean%>}<%end ean%>%
+  <%if projectnumber%>\ExtraDescription{\projektnummer: <%projectnumber%>}<%end projectnumber%>%
+  &%
+  <%qty%> <%unit%> &%
+  <%sellprice%>&%
+  \Ifstr{<%p_discount%>}{0}{}{\sffamily\scriptsize{(-<%p_discount%>\,\%)}}%
+    <%linetotal%>\tabularnewline
+    <%end number%>%
+  }%
+  \begin{PricingTotal}%
+    % Tabellenende letzte Seite
+    \nettobetrag & <%subtotal%>\\%
+    <%foreach tax%>%
+    <%taxdescription%> & <%tax%>\\%
+    <%end tax%>%
+    \bfseries\schlussbetrag &  \bfseries <%ordtotal%>\\%
+  \end{PricingTotal}
+\end{PricingTabular*}
+
+<%if notes%>%
+<%notes%>%
+\medskip
+<%end if%>%
+
+<%if delivery_term%>%
+\lieferung ~<%delivery_term.description_long%>\\
+<%end delivery_term%>%
+
+<%if reqdate%>%
+\lieferungErfolgtAm ~<%reqdate%>.
+<%end if%>%
+
+\textit{\auftragpruefen}
+
+\closing{\gruesse}
+
+\end{letter}
+
+\end{document}
diff --git a/templates/print/marei/sales_quotation.html b/templates/print/marei/sales_quotation.html
new file mode 100644 (file)
index 0000000..983d976
--- /dev/null
@@ -0,0 +1,226 @@
+
+<body bgcolor=ffffff>
+
+<table width=100%>
+<tr valign=bottom>
+  <td width=10>&nbsp;</td>
+  <td>
+
+  <table width=100%>
+  <tr valign=top>
+    <td>
+      <h4>
+      <%company%>
+      <br><%address%>
+      </h4>
+    </td>
+
+    <th><img src=http://localhost/lx-erp/lx-office-erp.png border=0 width=64 height=58></th>
+
+    <td align=right>
+      <h4>
+      Tel: <%tel%>
+      <br>Fax: <%fax%>
+      </h4>
+    </td>
+  </tr>
+
+<tr><td colspan=3>&nbsp;</td></tr>
+
+  <tr>
+    <th colspan=3>
+      <h4>A N G E B O T</h4>
+    </th>
+  </tr>
+
+  </table>
+
+  <table width=100% callspacing=0 cellpadding=0>
+
+  <tr>
+    <td>
+    <table width=100%>
+
+    <tr valign=top>
+      <td><%name%>
+      <br><%street%>
+      <br><%zipcode%>
+      <br><%city%>
+      <br><%country%>
+<br>
+<%if contact%>
+<br>Kontakt: <%contact%>
+<%end contact%>
+
+<%if customerphone%>
+<br>Tel: <%customerphone%>
+<%end customerphone%>
+
+<%if customerfax%>
+<br>Fax: <%customerfax%>
+<%end customerfax%>
+
+<%if email%>
+<br><%email%>
+<%end email%>
+      </td>
+
+    </tr>
+    </table>
+    </td>
+  </tr>
+
+  <tr><td>&nbsp;</td></tr>
+
+  <tr>
+    <td colspan=2>
+      <table width=100% border=1>
+        <tr>
+    <th width=17% align=left nowrap>Nummer</th>
+    <th width=17% align=left>Datum</th>
+    <th width=17% align=left>Gültig bis</th>
+    <th width=17% align=left nowrap>Kontakt</th>
+    <th width=17% align=left nowrap>Lagerplatz</th>
+    <th width=15% align=left nowrap>Lieferung mit</th>
+  </tr>
+
+  <tr>
+    <td><%quonumber%></td>
+    <td><%quodate%></td>
+    <td><%reqdate%></td>
+    <td><%employee%></td>
+    <td><%shippingpoint%></td>
+    <td><%shipvia%></td>
+  </tr>
+      </table>
+    </td>
+  </tr>
+
+  <tr>
+    <td>&nbsp;</td>
+  </tr>
+
+  <tr>
+    <td>
+    <table width=100%>
+    <tr bgcolor=000000>
+      <th align=right><font color=ffffff>Nr.</th>
+      <th align=left><font color=ffffff>Artikelnummer</th>
+      <th align=left><font color=ffffff>Beschreibung</th>
+      <th><font color=ffffff>Menge</th>
+      <th>&nbsp;</th>
+      <th><font color=ffffff>Preis</th>
+      <th><font color=ffffff>Rabatt</th>
+      <th><font color=ffffff>Gesamtpreis</th>
+    </tr>
+
+<%foreach number%>
+    <tr valign=top>
+    <td align=right><%runningnumber%></td>
+
+      <td><%number%></td>
+      <td><%description%></td>
+      <td align=right><%qty%></td>
+      <td><%unit%></td>
+      <td align=right><%sellprice%></td>
+      <td align=right><%discount%></td>
+      <td align=right><%linetotal%></td>
+    </tr>
+<%end number%>
+
+    <tr>
+      <td colspan=8><hr noshade></td>
+    </tr>
+
+    <tr>
+<%if taxincluded%>
+      <th colspan=6 align=right>Gesamtbetrag netto</th>
+      <td colspan=2 align=right><%invtotal%></td>
+<%end taxincluded%>
+
+<%if not taxincluded%>
+      <th colspan=6 align=right>Zwischensumme</th>
+      <td colspan=2 align=right><%subtotal%></td>
+<%end taxincluded%>
+    </tr>
+
+<%foreach tax%>
+    <tr>
+      <th colspan=6 align=right><%taxdescription%> von <%taxbase%> @ <%taxrate%> %</th>
+      <td colspan=2 align=right><%tax%></td>
+    </tr>
+<%end tax%>
+
+<%if rounding%>
+      <th colspan=6 align=right>Rundung</th>
+      <td colspan=2 align=right><%rounding%></td>
+<%end rounding%>
+
+    <tr>
+      <td colspan=4>&nbsp;</td>
+      <td colspan=4><hr noshade></td>
+    </tr>
+
+    <tr>
+      <td colspan=4>&nbsp;
+<%if terms%>
+      Zahlungsziel <b><%terms%></b> Tage
+<%end terms%>
+      </td>
+      <th colspan=2 align=right>Gesamtbetrag brutto</th>
+      <th colspan=2 align=right><%quototal%></th>
+    </tr>
+
+    <tr>
+      <td>&nbsp;</td>
+    </tr>
+
+    </table>
+    </td>
+  </tr>
+
+<tr>
+  <td>
+  <table width=100%>
+    <tr valign=top>
+<%if notes%>
+      <td>Bemerkungen</td>
+      <td><%notes%></td>
+<%end notes%>
+      <td align=right>
+      Alle Preise in <b><%currency%></b> Euro
+      </td>
+    </tr>
+
+  </table>
+  </td>
+</tr>
+
+<tr><td>&nbsp;</td></tr>
+
+<tr>
+  <td>
+  <table width=100%>
+  <tr valign=top>
+    <td width=60%><font size=-3>
+    Spezialanfertigungen können nicht zurückgenommen werden.
+    </font>
+    </td>
+    <td width=40%>
+    X <hr noshade>
+    </td>
+  </tr>
+  </table>
+  </td>
+</tr>
+
+</table>
+
+</td>
+</tr>
+</table>
+
+</body>
+</html>
+
+
diff --git a/templates/print/marei/sales_quotation.tex b/templates/print/marei/sales_quotation.tex
new file mode 100644 (file)
index 0000000..d6e03c8
--- /dev/null
@@ -0,0 +1,150 @@
+\documentclass[paper=a4,fontsize=10pt]{scrartcl}
+\usepackage{kiviletter}
+
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+
+% laufende Kopfzeile:
+\ourhead{\kundennummer}{<%customernumber%>}{\angebot}{<%quonumber%>}{<%transdate%>}
+
+
+\begin{document}
+
+\setkomavar{signature}{%
+  <%employee_company%>%
+  \ifhmode\\\fi
+  <%salesman_name%>%
+}
+
+\setkomavar*{date}{\datum}
+
+\setkomavar{date}{<%transdate%>}
+\setkomavar{customer}{<%customernumber%>}
+\setkomavar{fromname}{<%employee_name%>}
+\setkomavar{fromphone}{<%employee_tel%>}
+\setkomavar{fromemail}{<%employee_email%>}
+\setkomavar{title}{
+  \angebot~
+  <%quonumber%>%
+}
+
+\setkomavar{transaction}{<%transaction_description%>}
+
+<%if shiptoname%>%
+\makeatletter
+\begin{lrbox}\shippingAddressBox
+  \parbox{\useplength{toaddrwidth}}{
+    \backaddr@format{\scriptsize\usekomafont{backaddress}%
+      \strut\abweichendeLieferadresse
+    }
+    \par\smallskip
+    \setlength{\parskip}{\z@}
+    \par
+    \normalsize
+    <%shiptoname%>\par
+    <%if shiptocontact%> <%shiptocontact%><%end if%>\par
+    <%shiptodepartment_1%>\par
+    <%shiptodepartment_2%>\par
+    <%shiptostreet%>\par
+    <%shiptozipcode%> <%shiptocity%>%
+  }
+\end{lrbox}
+\makeatother
+<%end if%>%
+
+
+\begin{letter}{
+<%name%>\strut\\
+<%if department_1%><%department_1%>\\<%end if%>%
+<%if department_2%><%department_2%>\\<%end if%>%
+<%cp_givenname%> <%cp_name%>\strut\\
+<%street%>\strut\\
+<%zipcode%> <%city%>\strut\\
+<%country%> \strut
+}
+
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\opening{
+\Ifstr{<%cp_name%>}{}
+{<%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}
+  {
+    \Ifstr{<%cp_gender%>}{f}
+        {\anredefrau}
+        {\anredeherr}
+      <%cp_title%> <%cp_name%>,
+    }
+  }
+\thispagestyle{kivitendo.letter.first}
+
+\angebotsformel
+
+
+\begin{PricingTabular*}
+  % eigentliche Tabelle
+  \FakeTable{
+  <%foreach number%>%
+  <%runningnumber%> &%
+  <%number%> &%
+  \textbf{<%description%>}%
+  <%if longdescription%>\ExtraDescription{<%longdescription%>}<%end longdescription%>%
+  <%if reqdate%>\ExtraDescription{\lieferdatum: <%reqdate%>}<%end reqdate%>%
+  <%if serialnumber%>\ExtraDescription{\seriennummer: <%serialnumber%>}<%end serialnumber%>%
+  <%if ean%>\ExtraDescription{\ean: <%ean%>}<%end ean%>%
+  <%if projectnumber%>\ExtraDescription{\projektnummer: <%projectnumber%>}<%end projectnumber%>%
+  &%
+  <%qty%> <%unit%> &%
+  <%sellprice%>&%
+  \Ifstr{<%p_discount%>}{0}{}{\sffamily\scriptsize{(-<%p_discount%>\,\%)}}%
+    <%linetotal%>\tabularnewline
+    <%end number%>%
+  }
+  \begin{PricingTotal}
+    % Tabellenende letzte Seite
+    \nettobetrag & <%subtotal%>\\
+    <%foreach tax%>%
+    <%taxdescription%> & <%tax%>\\
+    <%end tax%>%
+    \bfseries\schlussbetrag &  \bfseries <%ordtotal%>\\
+  \end{PricingTotal}
+\end{PricingTabular*}
+
+<%if notes%>%
+<%notes%>%
+\medskip
+<%end if%>%
+
+<%if delivery_term%>%
+\lieferung ~<%delivery_term.description_long%>\\
+<%end delivery_term%>%
+
+\angebotdanke\\
+<%if reqdate%>%
+\angebotgueltig~<%reqdate%>.
+<%end if%>%
+\angebotfragen
+
+
+\angebotagb
+
+\closing{\gruesse}
+
+\begin{minipage}{\textwidth}
+  \rule{\linewidth}{.2pt}\par
+  \auftragerteilt\par\bigskip
+  \nurort:\rule[-.5ex]{8cm}{.2pt}\ ,\den\ \rule[-.5ex]{5cm}{.2pt}\par\bigskip
+
+  \unterschrift/\stempel:\rule[-.5ex]{6cm}{.2pt}
+\end{minipage}
+
+
+\end{letter}
+\end{document}
diff --git a/templates/print/marei/statement.html b/templates/print/marei/statement.html
new file mode 100644 (file)
index 0000000..37e612c
--- /dev/null
@@ -0,0 +1,121 @@
+
+<body bgcolor=ffffff>
+
+<table width=100%>
+  <tr>
+    <td width=10>&nbsp;</td>
+    <td>
+      <table width=100%>
+       <tr>
+         <td>
+           <h4>
+           <%company%>
+           <br><%address%>
+           </h4>
+         </td>
+         <th></th>
+         <td align=right>
+         <h4>
+         Tel: <%tel%>
+         <br>Fax: <%fax%>
+         </h4>
+         </td>
+       </tr>
+       <tr>
+         <th colspan=3><h4>S T A T E M E N T</h4></th>
+       </tr>
+       <tr>
+         <td colspan=3 align=right><%statementdate%></td>
+       </tr>
+      </table>
+    </td>
+  </tr>
+  <tr>
+    <td>&nbsp;</td>
+    <td>
+      <table width=100%>
+       <tr valign=top>
+         <td><%name%>
+         <br><%street%>
+         <br><%zipcode%>
+         <br><%city%>
+         <br><%country%>
+         <br>
+<%if customerphone%>
+         <br>Tel: <%customerphone%>
+<%end customerphone%>
+<%if customerfax%>
+         <br>Fax: <%customerfax%>
+<%end customerfax%>
+<%if email%>
+         <br><%email%>
+<%end email%>
+         </td>
+       </tr>
+      </table>
+    </td>
+  </tr>
+  <tr height=10></tr>
+  <tr>
+    <td>&nbsp;</td>
+    <td>
+      <table width=100%>
+        <tr>
+         <th align=left>Invoice #</th>
+         <th width=15%>Date</th>
+         <th width=15%>Due</th>
+         <th width=10%>Current</th>
+         <th width=10%>30</th>
+         <th width=10%>60</th>
+         <th width=10%>90+</th>
+       </tr>
+<%foreach invnumber%>
+       <tr>
+         <td><%invnumber%></td>
+         <td><%invdate%></td>
+         <td><%duedate%></td>
+         <td align=right><%c0%></td>
+         <td align=right><%c30%></td>
+         <td align=right><%c60%></td>
+         <td align=right><%c90%></td>
+       </tr>
+<%end invnumber%>
+        <tr>
+         <td colspan=7><hr size=1></td>
+       </tr>
+       <tr>
+         <td>&nbsp;</td>
+         <td>&nbsp;</td>
+         <td>&nbsp;</td>
+         <th align=right><%c0total%></td>
+         <th align=right><%c30total%></td>
+         <th align=right><%c60total%></td>
+         <th align=right><%c90total%></td>
+       </tr>
+      </table>
+    </td>
+  </tr>
+  <tr height=10></tr>
+  <tr>
+    <td>&nbsp;</td>
+    <td align=right>
+      <table width=50%>
+        <tr>
+         <th>Total Outstanding</th>
+          <th align=right><%total%></th>
+       </tr>
+      </table>
+    </td>
+  </tr>
+  <tr>
+    <td>&nbsp;</td>
+    <td><hr noshade></td>
+  </tr>
+  <tr>
+    <td>&nbsp;</td>
+    <td>Please make check payable to <b><%company%></b>.
+    </td>
+  </tr>
+  <tr height=20></tr>
+</table>
+
diff --git a/templates/print/marei/statement.tex b/templates/print/marei/statement.tex
new file mode 100644 (file)
index 0000000..7cd91a0
--- /dev/null
@@ -0,0 +1,79 @@
+\documentclass[paper=a4,fontsize=10pt]{scrartcl}
+\usepackage{kiviletter}
+
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+
+% laufende Kopfzeile:
+\ourhead{}{}{\sammelrechnung}{}{}
+
+
+\setkomavar{title}{
+  \sammelrechnung~
+  \nr~<%quonumber%>%
+}
+\setkomavar{transaction}{<%transaction_description%>}
+\setkomavar{customer}{<%customernumber%>}
+\setkomavar{fromname}{<%employee_name%>}
+\setkomavar{fromphone}{<%employee_tel%>}
+\setkomavar{fromemail}{<%employee_email%>}
+
+\begin{document}
+
+\begin{letter}{
+<%name%>\strut\\
+<%if department_1%><%department_1%>\\<%end if%>%
+<%if department_2%><%department_2%>\\<%end if%>%
+<%cp_givenname%> <%cp_name%>\strut\\
+<%street%>\strut\\
+<%zipcode%> <%city%>\strut\\
+<%country%> \strut
+}
+
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\opening{
+\Ifstr{<%cp_name%>}{}
+{<%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}
+  {
+    \Ifstr{<%cp_gender%>}{f}
+        {\anredefrau}
+        {\anredeherr}
+      <%cp_title%> <%cp_name%>,
+    }
+  }
+\thispagestyle{kivitendo.letter.first}
+
+\sammelrechnungsformel
+
+\begin{SimpleTabular}[
+    colspec=l*6X,
+    headline={\bfseries\rechnung~\nr & \bfseries\datum & \bfseries\faellig &
+        \bfseries\aktuell & \bfseries\asDreissig & \bfseries\asSechzig & \bfseries\asNeunzig}
+  ]
+  % eigentliche Tabelle
+  <%foreach invnumber%>%
+  <%invnumber%> & <%invdate%> & <%duedate%> &
+  <%c0%> & <%c30%> & <%c60%> & <%c90%> \\
+  <%end invnumber%>%
+  % Tabellenende letzte Seite
+  \midrule[\heavyrulewidth]
+  \multicolumn{3}{@{}l}{\bfseries\zwischensumme} & \bfseries<%c0total%> & \bfseries<%c30total%> & \bfseries<%c60total%> & \bfseries<%c90total%>\\*
+  \midrule
+  \multicolumn{3}{@{}l}{\bfseries\schlussbetrag} & &&&\bfseries<%total%> \\
+\end{SimpleTabular}
+
+\closing{\gruesse}
+
+\end{letter}
+
+\end{document}
+
diff --git a/templates/print/marei/supplier_delivery_order.tex b/templates/print/marei/supplier_delivery_order.tex
new file mode 120000 (symlink)
index 0000000..7d185e6
--- /dev/null
@@ -0,0 +1 @@
+purchase_delivery_order.tex
\ No newline at end of file
diff --git a/templates/print/marei/zahlungserinnerung.tex b/templates/print/marei/zahlungserinnerung.tex
new file mode 100644 (file)
index 0000000..abb601f
--- /dev/null
@@ -0,0 +1,80 @@
+\documentclass[paper=a4,fontsize=10pt]{scrartcl}
+\usepackage{kiviletter}
+
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+
+% laufende Kopfzeile:
+\ourhead{\kundennummer}{<%customernumber%>}{\mahnung}{<%dunning_id%>}{<%dunning%>}
+
+\setkomavar*{date}{\datum}
+\setkomavar{date}{<%dunning_date%>}
+\setkomavar{customer}{<%customernumber%>}
+\setkomavar{fromname}{<%employee_name%>}
+\setkomavar{fromphone}{<%employee_tel%>}
+\setkomavar{fromemail}{<%employee_email%>}
+\setkomavar{title}{
+  \mahnung
+  <%if dunning_id%>~\nr~<%dunning_id%><%end if%>%
+}
+\setkomavar{transaction}{<%transaction_description%>}
+
+\begin{document}
+
+\begin{letter}{
+<%name%>\strut\\
+<%if department_1%><%department_1%>\\<%end if%>%
+<%if department_2%><%department_2%>\\<%end if%>%
+<%cp_givenname%> <%cp_name%>\strut\\
+<%street%>\strut\\
+<%zipcode%> <%city%>\strut\\
+<%country%> \strut
+}
+
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\opening{
+\Ifstr{<%cp_name%>}{}
+{<%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}
+  {
+    \Ifstr{<%cp_gender%>}{f}
+        {\anredefrau}
+        {\anredeherr}
+      <%cp_title%> <%cp_name%>,
+    }
+  }
+\thispagestyle{kivitendo.letter.first}
+
+\mahnungsformel
+
+\begin{SimpleTabular}[headline=\bfseries\rechnung~\nr&\bfseries\datum&\bfseries\betrag,colspec=rXr<{\tabcurrency}]
+  % eigentliche Tabelle
+  <%foreach dn_invnumber%>%
+  <%dn_invnumber%> & <%dn_transdate%> & <%dn_amount%> \\[0.1cm]
+  <%end dn_invnumber%>%
+\end{SimpleTabular}
+
+
+\smallskip
+
+\bitteZahlenBis~<%dunning_duedate%>.
+
+
+\beruecksichtigtBis~<%dunning_date%>.
+
+
+\schonGezahlt
+
+\closing{\gruesse}
+
+\end{letter}
+
+\end{document}
diff --git a/templates/print/marei/zahlungserinnerung_invoice.tex b/templates/print/marei/zahlungserinnerung_invoice.tex
new file mode 100644 (file)
index 0000000..9acdeab
--- /dev/null
@@ -0,0 +1,79 @@
+\documentclass[paper=a4,fontsize=10pt]{scrartcl}
+\usepackage{kiviletter}
+
+
+% Variablen, die in settings verwendet werden
+\newcommand{\lxlangcode} {<%template_meta.language.template_code%>}
+\newcommand{\lxmedia} {<%media%>}
+\newcommand{\lxcurrency} {<%currency%>}
+\newcommand{\kivicompany} {<%employee_company%>}
+
+% settings: Einstellungen, Logo, Briefpapier, Kopfzeile, Fusszeile
+\input{insettings.tex}
+
+
+% laufende Kopfzeile:
+\ourhead{\kundennummer}{<%customernumber%>}{\rechnung}{<%invnumber%>}{<%invdate%>}
+
+\setkomavar*{date}{\rechnungsdatum}
+\setkomavar{date}{<%invdate%>}
+\setkomavar{customer}{<%customernumber%>}
+\setkomavar{fromname}{<%employee_name%>}
+\setkomavar{fromphone}{<%employee_tel%>}
+\setkomavar{fromemail}{<%employee_email%>}
+\setkomavar{title}{
+  \rechnung~
+  \nr ~<%invnumber%>%
+}
+\setkomavar*{myref}{\mahnung~\nr}
+\setkomavar{myref}{<%dunning_id%>}
+<%if globalprojectnumber%>%
+  \setkomavar{projectID}{<%globalprojectnumber%>}
+<%end globalprojectnumber%>%
+\setkomavar{transaction}{<%transaction_description%>}
+
+\begin{document}
+
+
+\begin{letter}{
+<%name%>\strut\\
+<%if department_1%><%department_1%>\\<%end if%>%
+<%if department_2%><%department_2%>\\<%end if%>%
+<%cp_givenname%> <%cp_name%>\strut\\
+<%street%>\strut\\
+<%zipcode%> <%city%>\strut\\
+<%country%> \strut
+}
+
+% Bei Kontaktperson Anrede nach Geschlecht unterscheiden.
+% Bei natürlichen Personen persönliche Anrede, sonst allgemeine Anrede.
+\opening{
+\Ifstr{<%cp_name%>}{}
+{<%if natural_person%><%greeting%> <%name%>,<%else%>\anrede<%end if%>}
+  {
+    \Ifstr{<%cp_gender%>}{f}
+        {\anredefrau}
+        {\anredeherr}
+      <%cp_title%> <%cp_name%>,
+    }
+  }
+\thispagestyle{kivitendo.letter.first}
+
+\mahnungsrechnungsformel
+
+\begin{SimpleTabular}[colspec=Xr<{\tabcurrency},headline={\bfseries\posten& \bfseries\betrag}]
+  Mahngebühren & <%fee%> \\
+  Zinsen & <%interest%> \\
+  \midrule[\heavyrulewidth]
+  \multicolumn{1}{@{}l}{\schlussbetrag}& <%invamount%>\\
+\end{SimpleTabular}
+
+\smallskip
+
+\bitteZahlenBis~<%duedate%>.
+
+\closing{\gruesse}
+
+\end{letter}
+
+\end{document}
index 9ec4502..dda141a 100644 (file)
Binary files a/templates/print/rev-odt/credit_note.odt and b/templates/print/rev-odt/credit_note.odt differ
index 9ea1c85..b338f4d 100644 (file)
Binary files a/templates/print/rev-odt/invoice.odt and b/templates/print/rev-odt/invoice.odt differ
diff --git a/templates/print/rev-odt/invoice_besr.odt b/templates/print/rev-odt/invoice_besr.odt
new file mode 100644 (file)
index 0000000..82b77ca
Binary files /dev/null and b/templates/print/rev-odt/invoice_besr.odt differ
diff --git a/templates/print/rev-odt/invoice_qr.odt b/templates/print/rev-odt/invoice_qr.odt
new file mode 100644 (file)
index 0000000..621e478
Binary files /dev/null and b/templates/print/rev-odt/invoice_qr.odt differ
diff --git a/templates/print/rev-odt/mahnung.odt b/templates/print/rev-odt/mahnung.odt
new file mode 100644 (file)
index 0000000..c689709
Binary files /dev/null and b/templates/print/rev-odt/mahnung.odt differ
diff --git a/templates/print/rev-odt/mahnung_invoice.odt b/templates/print/rev-odt/mahnung_invoice.odt
new file mode 100644 (file)
index 0000000..50c56d7
Binary files /dev/null and b/templates/print/rev-odt/mahnung_invoice.odt differ
index a526580..c5c913a 100644 (file)
Binary files a/templates/print/rev-odt/proforma.odt and b/templates/print/rev-odt/proforma.odt differ
index cadd8d0..984d909 100644 (file)
@@ -1,8 +1,6 @@
-Die odt-Vorlagen in diesem Verzeichnis "rev-odt" wurden von revamp-it, Zürich erstellt
-und werden laufend aktualisiert.
+Die odt-Vorlagen in diesem Verzeichnis "rev-odt" wurden von revamp-it, Zürich erstellt und werden laufend aktualisiert.
 
-Aktuell (August 2015 - kivitendo-erp Version 3.3.0) stehen folgende
-Vorlagen zur Verfügung:
+Aktuell ( 2016 - kivitendo-erp Version 3.4.1) stehen folgende Vorlagen zur Verfügung:
 
 Verkaufsvorlagen:
 sales_quotation.odt            Offerte
@@ -13,6 +11,10 @@ invoice.odt                  Rechnung
 proforma.odt                   Proforma-Rechnung
 credit_note.odt                        Gutschrift
 
+Verkaufsvorlagen mit Schweizer Bank-Einzahlungsschein mit Referenznummer (BESR) inkl. Makro zum Erstellen der Referenzzeile:
+invoice_besr.odt               Rechnung mit BESR
+sales_order_besr.odt           Auftragsbestätigung mit BESR
+
 Einkaufsvorlagen:
 request_quotation.odt          Preisanfrage
 purchase_order.odt             Bestellung/Lieferantenauftrag
@@ -20,24 +22,17 @@ purchase_delivery_order.odt Einkaufslieferschein
 
 Hinweis zum Einsatz des Feldes "Land" bei den Stammdaten für KundInnen und LieferantInnen,
 sowie bei Lieferadressen:
-Die in diesem Vorlagensatz vorhandenen Vorlagen erwarten für "Land" das entsprechende
-Kürzel, das in Adressen vor die Postleitzahl gesetzt wird.
+Die in diesem Vorlagensatz vorhandenen Vorlagen erwarten für "Land" das entsprechende Kürzel, das in Adressen vor die Postleitzahl gesetzt wird.
 Das Feld kann auch komplett leer bleiben.
 Wer dies anders handhaben möchte, muss die Vorlagen entsprechend anpassen.
 
-odt-Vorlagen können mit LibreOffice oder OpenOffice editiert
-und den eigenen Bedürfnissen angepasst werden.
-Wichtig beim Editieren von if-Blöcken ist, dass immer der gesamte Block
-überschrieben werden muss und nicht nur Teile davon, da dies sonst oft
-zu einer odt-Datei führt, die vom Parser nicht korrekt gelesen werden kann.
+odt-Vorlagen können mit LibreOffice oder OpenOffice editiert und den eigenen Bedürfnissen angepasst werden.
+Wichtig beim Editieren von if-Blöcken ist, dass immer der gesamte Block überschrieben werden muss und nicht nur Teile davon, da dies sonst oft zu einer odt-Datei führt, die vom Parser nicht korrekt gelesen werden kann.
 
-Zur Zeit gibt es in Kivitendo noch keine Möglichkeit, odt-Vorlagen bei Mahnungen
-einzusetzen. Entsprechende Vorlagen sind deshalb nicht vorhanden.
+Zur Zeit gibt es in Kivitendo noch keine Möglichkeit, odt-Vorlagen bei Mahnungen einzusetzen. Entsprechende Vorlagen sind deshalb nicht vorhanden.
 
-Inwieweit es möglich ist, für die in Version 3.2.0 neu eingeführten Pflichtenhefte
-odt-Vorlagen zu erstellen, sind wir am abklären.
-Wenn dies möglich ist, werden wir in Zukunft auch eine odt-Vorlage für Pflichtenhefte
-in diesem Vorlagensatz zur Verfügung stellen.
+Inwieweit es möglich ist, für die in Version 3.2.0 neu eingeführten Pflichtenhefte odt-Vorlagen zu erstellen, sind wir am abklären.
+Wenn dies möglich ist, werden wir in Zukunft auch eine odt-Vorlage für Pflichtenhefte in diesem Vorlagensatz zur Verfügung stellen.
 
 Fehlermeldungen, Anregungen und Wünsche bitte senden an:
 empfang@revamp-it.ch
index 4a8af66..051dd86 100644 (file)
Binary files a/templates/print/rev-odt/sales_order.odt and b/templates/print/rev-odt/sales_order.odt differ
diff --git a/templates/print/rev-odt/sales_order_besr.odt b/templates/print/rev-odt/sales_order_besr.odt
new file mode 100644 (file)
index 0000000..0c89b69
Binary files /dev/null and b/templates/print/rev-odt/sales_order_besr.odt differ
index f39bf4e..e224144 100644 (file)
Binary files a/templates/print/rev-odt/sales_quotation.odt and b/templates/print/rev-odt/sales_quotation.odt differ
diff --git a/templates/print/rev-odt/zahlungserinnerung.odt b/templates/print/rev-odt/zahlungserinnerung.odt
new file mode 100644 (file)
index 0000000..22ba13f
Binary files /dev/null and b/templates/print/rev-odt/zahlungserinnerung.odt differ
index 3a18861..4164ef5 100644 (file)
@@ -20,7 +20,7 @@
     <td align="right">[%- transaction.chart.accno -%]</td>
     <td>[%- transaction.chart.description -%]</td>
     <td align="right">[%- IF transaction.amount < 0 %] [%- LxERP.format_amount(transaction.amount * -1, 2) %] [% END %]</td>
-    <td align="right">[%- IF transaction.amount > 0 %] [%- transaction.amount_as_number -%] [%- END -%]</td>
+    <td align="right">[%- IF transaction.amount > 0 %] [%- LxERP.format_amount(transaction.amount     , 2) %] [%- END -%]</td>
    </tr>
    [%- END %]
     <td colspan="2"></td>
index 91a193b..11c6136 100644 (file)
@@ -9,7 +9,7 @@
   [% 'This module can help you identify and correct such entries by analyzing the general ledger and presenting you likely solutions but also allowing you to fix problems yourself.' | $T8 %]
  </p>
 
- <form name="filter" method="post" action="acctranscorrections.pl">
+ <form name="filter" method="post" action="acctranscorrections.pl" id="form">
 
   <p>[% 'Time period for the analysis:' | $T8 %]</p>
 
@@ -33,9 +33,5 @@
    </table>
   </p>
 
-  <p>
-   <input type="submit" value="[% 'Start analysis' | $T8 %]">
-  </p>
-
   <input type="hidden" name="action" value="analyze">
  </form>
index 3ed544f..2336683 100644 (file)
@@ -23,6 +23,8 @@
 
   [% 'The second reason is that kivitendo allowed the user to enter the tax amount manually regardless of the taxkey used.' | $T8 %]
 
+  [% 'The third reason is that wrong (taxkey) settings for the credit / debit CSV-import were used.' | $T8 %]
+
   [% 'Such entries cannot be exported into the DATEV format and have to be fixed as well.' | $T8 %]
  </p>
 
index c84e602..9fca94a 100644 (file)
@@ -5,7 +5,7 @@
   <table class="login" border="3" cellpadding="20">
    <tr>
     <td class="login" align="center">
-     <a href="http://www.kivitendo.de" target="_top" class="no-underlined-links"><img src="image/kivitendo.png" border="0"></a>
+     <a href="http://www.kivitendo.de" target="_top" class="no-underlined-links"><img src="image/kivitendo.png" class='kivitendo-logo' border="0"></a>
      <h1>[% LxERP.t8('kivitendo v#1 administration', version) %]</h1>
 
 [% IF error %]
index 5b77656..48998c6 100644 (file)
   [% LxERP.t8('In the latter case the tables needed by kivitendo will be created in that database.') %]
  </p>
 
+ [% IF !superuser.have_privileges %]
+  <p>
+   [% LxERP.t8("Database superuser privileges are required for parts of the database modifications.") %]
+   [% LxERP.t8("Please provide corresponding credentials.") %]
+  </p>
+ [% END %]
+
  <table border="0">
   <tr>
    <th valign="top" align="right" nowrap>[% LxERP.t8('Existing Datasets') %]</th>
    <td>[% L.input_tag('db', FORM.db, class="initial_focus") %]</td>
   </tr>
 
+  [% IF !superuser.have_privileges %]
+   <tr>
+    <th align="right" nowrap>[% LxERP.t8("Database Superuser") %]</th>
+    <td>[% L.input_tag("database_superuser_user", superuser.username) %]</td>
+   </tr>
+
+   <tr>
+    <th align="right" nowrap>[% LxERP.t8("Password") %]</th>
+    <td>[% L.input_tag("database_superuser_password", superuser.password, type="password") %]</td>
+   </tr>
+  [% END %]
+
+  <tr>
+   <td colspan="1"> </td>
+   <td><hr size="1" noshade></td>
+  </tr>
+
   <tr>
    <th align="right" nowrap>[% LxERP.t8('Default currency') %]</th>
-   <td>[% L.input_tag('defaultcurrency', FORM.defaultcurrency || 'EUR') %]</td>
+   <td>[% L.input_tag('defaultcurrency', FORM.defaultcurrency) %]</td>
+  </tr>
+
+  <tr>
+   <th align="right" nowrap>[% LxERP.t8('Precision') %]</th>
+   <td>[% L.input_tag('precision_as_number', LxERP.format_amount(FORM.precision, 2)) %] [% LxERP.t8('Precision Note') %]</td>
   </tr>
 
   <tr>
    <th valign="top" align="right" nowrap>[% LxERP.t8('Create Chart of Accounts') %]</th>
-   <td>[% L.select_tag('chart', SELF.all_charts, onchange='comment_selected_chart(this.value)', default=(FORM.chart || 'Germany-DATEV-SKR03EU')) %]</td>
+   <td>[% L.select_tag('chart', SELF.all_charts, default=(FORM.chart), onchange='comment_selected_chart(this.value)') %]</td>
   </tr>
 
   <tr>
    <th valign="top" align="right" nowrap>[% LxERP.t8('Accounting method') %]</th>
-   <td>[% L.select_tag('accounting_method', SELF.all_accounting_methods, title_key='name', default=(FORM.accounting_method || 'cash')) %]</td>
+   <td>[% L.select_tag('accounting_method', SELF.all_accounting_methods, title_key='name', default=(FORM.accounting_method)) %]</td>
   </tr>
 
   <tr>
    <th valign="top" align="right" nowrap>[% LxERP.t8('Inventory system') %]</th>
-   <td>[% L.select_tag('inventory_system', SELF.all_inventory_systems, title_key='name', default=(FORM.inventory_system || 'periodic')) %]</td>
+   <td>[% L.select_tag('inventory_system', SELF.all_inventory_systems, title_key='name', default=(FORM.inventory_system)) %]</td>
   </tr>
 
   <tr>
    <th valign="top" align="right" nowrap>[% LxERP.t8('Profit determination') %]</th>
-   <td>[% L.select_tag('profit_determination', SELF.all_profit_determinations, title_key='name', default=(FORM.profit_determination || 'income')) %]</td>
+   <td>[% L.select_tag('profit_determination', SELF.all_profit_determinations, title_key='name', default=(FORM.profit_determination)) %]</td>
   </tr>
  </table>
 
  [% L.hidden_tag("dbpasswd", FORM.dbpasswd) %]
  [% L.hidden_tag("dbdefault", FORM.dbdefault) %]
  [% L.hidden_tag("action", "Admin/do_create_dataset") %]
+ [% L.hidden_tag("feature_balance", FORM.feature_balance) %]
+ [% L.hidden_tag("feature_datev", FORM.feature_datev) %]
+ [% L.hidden_tag("feature_erfolgsrechnung", FORM.feature_erfolgsrechnung) %]
+ [% L.hidden_tag("feature_eurechnung", FORM.feature_eurechnung) %]
+ [% L.hidden_tag("feature_ustva", FORM.feature_ustva) %]
+
 
  <hr size="3" noshade>
 
 
 function comment_selected_chart(s) {
   if (s == 'Austria') {
-   alert("SKR07 Austria ist noch Stand 2002." +
+    alert("SKR07 Austria ist noch Stand 2002." +
          "\n" +
          "Die Buchungsgruppen sind nicht korrekt vorkonfiguriert" +
          "\n" +
          "fuer Kunden im Ausland." +
          "\n" +
          "Hinweis vom 20.09.2011");
-
-  } else if (s == 'Swiss-German') {
-   alert("Hinweis: Das ist weder ein Schweizer Kontorahmen nach Kaefer noch ein " +
-         "Schweizer KMU-Kontenrahmen, sondern ein angelehnter KMU-Kontenrahmen fuer " +
-         "ein EDV-Dienstleistungsunternehmen mit Stand 2006 (Bspw. 32001 Hardware, " +
-         "statt 3200 Warenertrag)." +
-         "\n" +
-         "Ferner sind keine Buchungsgruppe vorkonfiguriert, somit wird " +
-         "standardmaessig keine Rechnung mit Steuer ausgewiesen." +
-         "\n" +
-         "Zum schnellen Testen und Zusammenhaenge verstehen waehlen Sie lieber einen " +
-         "deutschen Kontenrahmen aus (SKR03 oder SKR04) und passen die Steuer an." +
-         "\n" +
-         "Hinweis vom 21.09.2011");
   }
+  return true;
+}
 
+function select_country_defaults(country) {
+  if (/^CH/.test(country)) {
+    document.getElementById('defaultcurrency').value='CHF';
+    document.getElementById('precision').value='0.05';
+    document.getElementById('chart').value='Switzerland-deutsch-MWST-2014';
+    document.getElementById('accounting_method').value='accrual';
+    document.getElementById('inventory_system').value='periodic';
+    document.getElementById('profit_determination').value='balance';
+  } else {
+    document.getElementById('defaultcurrency').value='EUR';
+    document.getElementById('precision').value='0.01';
+    document.getElementById('chart').value='Germany-DATEV-SKR03EU';
+    document.getElementById('accounting_method').value='cash';
+    document.getElementById('inventory_system').value='periodic';
+    document.getElementById('profit_determination').value='income';
+  }
   return true;
 }
+
    -->
 </script>
index 1fe42a1..01363b8 100644 (file)
     [% L.input_tag("client.dbpasswd", SELF.client.dbpasswd, class="contains_dbsettings", type="password") %]
    </td>
   </tr>
+
+  <tr>
+   <th align="right">[% LxERP.t8("Run task server for this client with the following user") %]</th>
+   <td>
+    [% L.select_tag("client.task_server_user_id", SELF.all_users, with_empty=1, empty_title=LxERP.t8("Do not run the task server for this client"), title_key="login", default=SELF.client.task_server_user_id) %]
+   </td>
+  </tr>
  </table>
 
  <div>
index 3a8f129..1676be3 100644 (file)
@@ -1,5 +1,5 @@
 [%- USE HTML %]
-[%- USE L %][%- USE LxERP -%]
+[%- USE L %][%- USE LxERP -%][%- USE JavaScript -%]
 
 [%- INCLUDE 'common/flash.html' %]
 
@@ -40,7 +40,7 @@
 
      <tr valign="top">
       <th align="right">[% LxERP.t8('Signature') %]</th>
-      <td>[% L.textarea_tag("user.config_values.signature", props.signature, rows=3, cols=35) %]</td>
+      <td>[% L.textarea_tag("user.config_values.signature", props.signature, rows=3, cols=35, class="texteditor") %]</td>
      </tr>
 
      <tr>
       <td>[% L.select_tag("user.config_values.numberformat", SELF.all_numberformats, default=props.numberformat) %]</td>
      </tr>
 
-     <tr>
-      <th align="right">[% LxERP.t8("Dropdown Limit") %]</th>
-      <td>[% L.input_tag("user.config_values.vclimit", props.vclimit) %]</td>
-     </tr>
-
      <tr>
       <th align="right">[% LxERP.t8("Language") %]</th>
       <td>[% L.select_tag("user.config_values.countrycode", SELF.all_countrycodes, title_key="title", default=props.countrycode) %]</td>
  [% L.button_tag("submit_with_action('save_user')", LxERP.t8("Save")) %]
  [% IF SELF.user.id %]
   [% L.button_tag("save_as_new()", LxERP.t8("Save as new")) %]
-  [% L.button_tag("submit_with_action('delete_user')", LxERP.t8("Delete"), confirm=LxERP.t8("Are you sure?")) %]
+  [% L.button_tag("submit_delete()", LxERP.t8("Delete"), confirm=LxERP.t8("Are you sure?")) %]
  [%- END %]
 </p>
 
     $("#form").submit();
   }
 
+  function submit_delete() {
+[% SET used_for_task_server_in_clients = SELF.is_user_used_for_task_server(SELF.user) %]
+[% IF used_for_task_server_in_clients %]
+   alert('[% JavaScript.escape(LxERP.t8('The user cannot be deleted as it is used in the following clients: #1', used_for_task_server_in_clients)) %]');
+   return false;
+[% ELSE %]
+    submit_with_action('delete_user');
+[% END %]
+  }
+
   function save_as_new() {
     $("#user_id").val("");
     submit_with_action("save_user");
index 7189665..2ae4acb 100644 (file)
@@ -54,6 +54,7 @@
     <th>[% LxERP.t8('Database Host') %]</th>
     <th>[% LxERP.t8('Database User') %]</th>
     <th>[% LxERP.t8('Default client') %]</th>
+    <th>[% LxERP.t8('Task server') %]</th>
    </tr>
 
 [%- FOREACH client = SELF.all_clients %]
     <td>[% HTML.escape(client.dbhost) %][% IF client.dbport %]:[%- HTML.escape(client.dbport) %][%- END %]</td>
     <td>[% HTML.escape(client.dbuser) %]</td>
     <td>[% IF client.is_default %][% LxERP.t8("Yes") %][%- ELSE %][% LxERP.t8("No") %][%- END %]</td>
+    <td>
+     [% IF client.task_server_user %]
+      [% LxERP.t8("execution as user '#1'", client.task_server_user.login) %]
+     [% ELSE %]
+      [% LxERP.t8("no execution for this client") %]
+     [% END %]
+    </td>
    </tr>
 [%- END %]
   </table>
diff --git a/templates/webpages/am/_units_header_info.html b/templates/webpages/am/_units_header_info.html
new file mode 100644 (file)
index 0000000..a54a782
--- /dev/null
@@ -0,0 +1,7 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE T8 -%]
+<p>
+ [% LxERP.t8('All units have either no or exactly one base unit of which they are multiples.') %]
+ [% LxERP.t8('If you select a base unit then you also have to enter a factor.') %]
+ [% LxERP.t8('You have to define a unit as a multiple of a smaller unit.') %]
+ [% LxERP.t8('Therefore the definition of "kg" with the base unit "g" and a factor of 1000 is valid while defining "g" with a base unit of "kg" and a factor of "0.001" is not.') %]
+</p>
diff --git a/templates/webpages/am/add_unit.html b/templates/webpages/am/add_unit.html
new file mode 100644 (file)
index 0000000..573121c
--- /dev/null
@@ -0,0 +1,39 @@
+[%- USE HTML -%][%- USE T8 -%]
+<h1>[% 'Add unit' | $T8 %]</h1>
+
+[% PROCESS "am/_units_header_info.html" %]
+
+<hr>
+
+<form method="post" action="[% HTML.escape(script) %]" id="form">
+
+ <input type="hidden" name="type" value="unit">
+
+ <table>
+  <tr>
+   <th align="right">[% 'Unit' | $T8 %]</th>
+   <td><input name="new_name" size="20" maxlength="20"></td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Base unit' | $T8 %]</th>
+   <td>
+    <select name="new_base_unit">
+     [% FOREACH row = NEW_BASE_UNIT_DDBOX %]<option [% row.selected %]>[% row.name %]</option>[% END %]
+    </select>
+   </td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Factor' | $T8 %]</th>
+   <td><input name="new_factor"></td>
+  </tr>
+
+  [% FOREACH language = LANGUAGES %]
+   <tr>
+    <th align="right">[% language.description %]</th>
+    <td><input name="new_localized_[% language.id %]" size="20" maxlength="20"></td>
+    <th align="right">[% 'Plural' | $T8 %]</th>
+    <td><input name="new_localized_plural_[% language.id %]" size="20" maxlength="20"></td>
+   </tr>
+  [% END %]
+ </table>
+</form>
index bd127f2..6452b14 100644 (file)
@@ -5,7 +5,7 @@
 
 <h1>[% title | html %]</h1>
 
-<form method=post action=am.pl>
+<form method="post" action="am.pl" id="form">
 
 <table>
   <tr>
   </tr>
 </table>
 
-<hr size=3 noshade>
-
-<br>
-<input type=hidden name=nextsub value=doclose>
-<input type=submit class=submit name=action value="[% 'Continue' | $T8 %]">
-
 </form>
-
index ac7875b..cd0e7ee 100644 (file)
@@ -1,11 +1,11 @@
 [%- USE T8 %]
 [%- USE LxERP %]
-[%- USE HTML %][%- USE L %]
-<h1>[% title %]</h1>
+[%- USE HTML %]
+[%- USE L %][%- USE P -%]
 
- <form method="post" action="am.pl" name="Form">
-  <input type="hidden" name="type" value="preferences">
+<h1>[% title %]</h1>
 
+ <form method="post" action="am.pl" name="Form" id="form">
   <div class="tabwidget">
    <ul>
     <li><a href="#page_personal_settings">[% 'Personal settings' | $T8 %]</a></li>
@@ -19,7 +19,7 @@
     <table>
      <tr>
       <th align="right">[% 'Name' | $T8 %]</th>
-      <td><input name="name" size="15" value="[% HTML.escape(myconfig_name) %]"></td>
+      <td><input name="name" size="15" value="[% HTML.escape(MYCONFIG.name) %]"></td>
      </tr>
 
      <tr>
      </tr>
 
      <tr>
-      <th align="right">[% 'E-mail' | $T8 %]</th>
-      <td><input name="email" size="30" value="[% HTML.escape(myconfig_email) %]"></td>
+      <th align="right">[% 'Email address' | $T8 %]</th>
+      <td><input name="email" size="30" value="[% HTML.escape(MYCONFIG.email) %]"></td>
+     </tr>
+
+     <tr valign="top">
+      <th align="right">[% 'Email signature' | $T8 %]</th>
+      <td>
+       [% P.textarea_tag("signature", MYCONFIG.signature, class="texteditor", rows="5", cols="50") %]
+      </td>
      </tr>
 
      <tr valign="top">
-      <th align="right">[% 'Signature' | $T8 %]</th>
-      <td><textarea id="signature" name="signature" class="toggletextarea" rows="5" cols="50">[% HTML.escape(myconfig_signature) %] </textarea>
-         <span id="full_signature" class="toggletextarea"> <textarea readonly name="full_signature" rows="10" cols="50" >[% HTML.escape(full_signature) %]</textarea> </span>
-         <a href="#" class="togglelink">[% 'Check full signature' | $T8 %]</a>
-         <a href="#" id="edit_signature" class="togglelink">[% 'Edit user signature' | $T8 %]</a>
-          </td> </tr>
+      <th align="right">[% "Company's email signature" | $T8 %]</th>
+      <td>[% P.restricted_html(company_signature) %]</td>
+     </tr>
+
      <tr>
       <th align="right">[% 'Phone' | $T8 %]</th>
-      <td><input name="tel" size="14" value="[% HTML.escape(myconfig_tel) %]"></td>
+      <td><input name="tel" size="14" value="[% HTML.escape(MYCONFIG.tel) %]"></td>
      </tr>
 
      <tr>
       <th align="right">[% 'Fax' | $T8 %]</th>
-      <td><input name="fax" size="14" value="[% HTML.escape(myconfig_fax) %]"></td>
+      <td><input name="fax" size="14" value="[% HTML.escape(MYCONFIG.fax) %]"></td>
      </tr>
 
       <tr>
         <th align="right">[% 'taxincluded checked' | $T8 %]</th>
         <td>
-          [% L.yes_no_tag('taxincluded_checked', myconfig_taxincluded_checked) %]
+          [% L.yes_no_tag('taxincluded_checked', MYCONFIG.taxincluded_checked) %]
         </td>
       </tr>
 
           [% L.select_tag(
             'focus_position',
             [
-              ['new_description', LxERP.t8('New row, description')],
-              ['new_partnumber', LxERP.t8('New row, partnumber')],
+              ['new_description',  LxERP.t8('New row, description')],
+              ['new_partnumber',   LxERP.t8('New row, partnumber')],
+              ['new_qty',          LxERP.t8('New row, qty')],
               ['last_description', LxERP.t8('Last row, description')],
-              ['last_partnumber', LxERP.t8('Last row, partnumber')],
+              ['last_partnumber',  LxERP.t8('Last row, partnumber')],
+              ['last_qty',         LxERP.t8('Last row, qty')],
             ],
-            default => myconfig_focus_position)
+            default => MYCONFIG.focus_position)
           %]
         </td>
       </tr>
       <tr>
         <th align="right">[% 'Item multi selection with qty' | $T8 %]</th>
         <td>
-          [% L.yes_no_tag('item_multiselect', myconfig_item_multiselect) %]
+          [% L.yes_no_tag('item_multiselect', MYCONFIG.item_multiselect) %]
         </td>
       </tr>
 
+     <tr>
+      <th align="right">[% 'Use date and duration for time recordings' | $T8 %]</th>
+      <td>
+        [% L.yes_no_tag('time_recording_use_duration', time_recording_use_duration) %]
+      </td>
+     </tr>
+
     </table>
    </div>
 
       </td>
      </tr>
 
-     <tr>
-      <th align="right">[% 'Dropdown Limit' | $T8 %]</th>
-      <td><input name="vclimit" size="10" value="[% HTML.escape(myconfig_vclimit) %]"></td>
-     </tr>
-
      <tr>
       <th align="right">[% 'Language' | $T8 %]</th>
       <td>
       <th align="right">[% 'Setup Menu' | $T8 %]</th>
       <td>
        <select name="menustyle">
-        <option value="old"[% IF myconfig_menustyle == 'old' %] selected[% END %]>[% 'Old (on the side)' | $T8 %]</option>
-        <option value="v3"[% IF myconfig_menustyle == 'v3' %] selected[% END %]>[% 'Top (CSS)' | $T8 %]</option>
-        <option value="neu"[% IF myconfig_menustyle == 'neu' %] selected[% END %]>[% 'Top (Javascript)' | $T8 %]</option>
+        <option value="old"[% IF MYCONFIG.menustyle == 'old' %] selected[% END %]>[% 'Old (on the side)' | $T8 %]</option>
+        <option value="v3"[% IF MYCONFIG.menustyle == 'v3' %] selected[% END %]>[% 'Top (CSS)' | $T8 %]</option>
+        <option value="neu"[% IF MYCONFIG.menustyle == 'neu' %] selected[% END %]>[% 'Top (Javascript)' | $T8 %]</option>
        </select>
       </td>
      </tr>
       <th align="right">[% 'Form details (second row)' | $T8 %]</th>
       <td>
        <select name="show_form_details">
-        <option value="1"[% IF  myconfig_show_form_details %] selected[% END %]>[% 'Show by default' | $T8 %]</option>
-        <option value="0"[% IF !myconfig_show_form_details %] selected[% END %]>[% 'Hide by default' | $T8 %]</option>
+        <option value="1"[% IF  MYCONFIG.show_form_details %] selected[% END %]>[% 'Show by default' | $T8 %]</option>
+        <option value="0"[% IF !MYCONFIG.show_form_details %] selected[% END %]>[% 'Hide by default' | $T8 %]</option>
        </select>
       </td>
      </tr>
 
+     <tr>
+      <th align="right">[% 'Longdescription dialog size percentage from main window (0 means fix values)' | $T8 %]</th>
+      <td>
+        [% L.input_tag('longdescription_dialog_size_percentage', longdescription_dialog_size_percentage, size = 5) %]
+      </td>
+     </tr>
+     [%- IF INSTANCE_CONF.get_feature_experimental_order -%]
+     <tr>
+      <th align="right">[% 'Scrollbar height percentage for form postion area (0 means no scrollbar)' | $T8 %]</th>
+      <td>
+        [% L.input_tag('positions_scrollbar_height',  positions_scrollbar_height, size = 5) %]
+      </td>
+     </tr>
+     <tr>
+      <th align="right">[% 'Search parts by vendor partnumber (model) in purchase order forms' | $T8 %]</th>
+      <td>
+        [% L.yes_no_tag('purchase_search_makemodel', purchase_search_makemodel) %]
+        [%- 'This also enables displaying a column with the vendor partnumber (model) (new order controller).' | $T8 %]
+      </td>
+     </tr>
+     <tr>
+      <th align="right">[% 'Search parts by customer partnumber in sales order forms' | $T8 %]</th>
+      <td>
+        [% L.yes_no_tag('sales_search_customer_partnumber', sales_search_customer_partnumber) %]
+        [%- 'This also enables displaying a column with the customer partnumber (new order controller).' | $T8 %]
+      </td>
+     </tr>
+     <tr>
+      <th align="right">[% 'Show update button for positions in order forms' | $T8 %]</th>
+      <td>
+        [% L.yes_no_tag('positions_show_update_button', positions_show_update_button) %]
+      </td>
+     </tr>
+     [%- END -%]
+
      <tr>
       <th align="right">[% 'Show custom variable search inputs' | $T8 %]</th>
       <td>
        <select name="hide_cvar_search_options">
-        <option value="0"[% IF !myconfig_hide_cvar_search_options %] selected[% END %]>[% 'Show by default' | $T8 %]</option>
-        <option value="1"[% IF  myconfig_hide_cvar_search_options %] selected[% END %]>[% 'Hide by default' | $T8 %]</option>
+        <option value="0"[% IF !MYCONFIG.hide_cvar_search_options %] selected[% END %]>[% 'Show by default' | $T8 %]</option>
+        <option value="1"[% IF  MYCONFIG.hide_cvar_search_options %] selected[% END %]>[% 'Hide by default' | $T8 %]</option>
        </select>
       </td>
      </tr>
      <tr>
       <th align="right">[% 'Number of columns of custom variables in form details (second row)' | $T8 %]</th>
       <td>
-        [% L.input_tag('form_cvars_nr_cols',  myconfig_form_cvars_nr_cols || 3,  size = 5) %]
+        [% L.input_tag('form_cvars_nr_cols',  MYCONFIG.form_cvars_nr_cols || 3,  size = 5) %]
       </td>
      </tr>
 
+     <tr>
+      <th align="right">[% 'Quick Searches that will be shown in the header for this user' | $T8 %]</th>
+      <td colspan=2>
+        <div class="clearfix">
+         [% L.select_tag("quick_search_modules[]",
+           enabled_quick_searchmodules,
+           value_key  = "name",
+           title_key  = "description_config",
+           id         = "quick_searches",
+           multiple   = 1,
+           with_empty = 1
+           size       = enabled_quick_searchmodules.size,
+           default    = default_quick_searchmodules) %]
+        </div>
+      </td>
+    </tr>
+
+     <tr>
+       <th align="right">[% 'Displayable Name Preferences' | $T8 %]</th>
+       <td>
+         <table>
+           [% FOREACH module=displayable_name_specs_by_module.keys.sort %]
+           [%- SET spec=displayable_name_specs_by_module.$module -%]
+           <tr>
+             <td align="right">[% spec.specs.title %]</td>
+             <td>
+               <table>
+                 <tr>
+                   <th align="left" class="listheading">[% 'Option' | $T8 %]</th>
+                   <th align="left" class="listheading">[% 'Name'   | $T8 %]</th>
+                 </tr>
+                 [% FOREACH option=spec.specs.options %]
+                 <tr>
+                   <td>[% option.title %]</td>
+                   <td>[% option.name  %]</td>
+                 </tr>
+                 [% END %]
+                 <tr>
+                   <th align="left">[% 'Display' | $T8 %]:</th>
+                   <td>
+                     [% L.hidden_tag("displayable_name_specs[+].module", module) %]
+                     [% L.input_tag("displayable_name_specs[].value", spec.prefs.get, size=50) %]
+                   </td>
+                 </tr>
+               </table>
+             </td>
+           </tr>
+           [% END %]
+         </table>
+       </td>
+
     </table>
    </div>
 
    <div id="page_print_options">
 
     <table>
-     <input name="printer" type="hidden" value="[% HTML.escape(myconfig_printer) %]">
+     <input name="printer" type="hidden" value="[% HTML.escape(MYCONFIG.printer) %]">
 
      <tr>
       <th align="right">[% 'Default template format' | $T8 %]</th>
      <tr>
       <th align="right">[% 'Default printer' | $T8 %]</th>
       <td>
-       [% L.select_tag('default_printer_id', PRINTERS, default = myconfig_default_printer_id, title_key = 'printer_description', with_empty = 1) %]
+       [% L.select_tag('default_printer_id', PRINTERS, default = MYCONFIG.default_printer_id, title_key = 'printer_description', with_empty = 1) %]
       </td>
      </tr>
 
      <tr>
       <th align="right">[% 'Number of copies' | $T8 %]</th>
-      <td><input name="copies" size="10" value="[% HTML.escape(myconfig_copies) %]"></td>
+      <td><input name="copies" size="10" value="[% HTML.escape(MYCONFIG.copies) %]"></td>
      </tr>
     </table>
    </div>
       </td>
      </tr>
 
-     [%- IF AUTH_RIGHTS_SALES_QUOTATION_EDIT %]
+     [%- IF AUTH.assert('sales_quotation_edit', 'may_fail') %]
      <tr>
       <th align="right">[% 'Show overdue sales quotations and requests for quotations...' | $T8 %]</th>
       <td>
     </table>
    </div>
   </div>
-
-  <p><input type="submit" class="submit" name="action" value="[% 'Save' | $T8 %]"></p>
  </form>
-
- <script type="text/javascript">
-  <!--
-$(function() {
-  $("#full_signature").toggle();
-  $("#edit_signature").toggle();
-  $('.togglelink').click(function() {
-    $('.toggletextarea').toggle();
-    $('.togglelink').toggle();
-    return false;
-  });
-});
-    -->
- </script>
diff --git a/templates/webpages/am/confirm_delete_warehouse.html b/templates/webpages/am/confirm_delete_warehouse.html
deleted file mode 100644 (file)
index d26b75d..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
- <h1>[% title %]</h1>
-
- <p>[% 'Do you really want to delete this warehouse?' | $T8 %]</p>
-
- <p>[% 'Warehouse' | $T8 %]: [% HTML.escape(orig_description) %]</p>
-
- <form action="am.pl" method="post">
-
-  <input type="hidden" name="id" value="[% HTML.escape(id) %]">
-  <input type="hidden" name="callback" value="[% HTML.escape(callback) %]">
-  <input type="hidden" name="type" value="warehouse">
-  <input type="hidden" name="confirmed" value="1">
-
-  <button type="button" class="submit" onclick="history.back()">[% 'Back' | $T8 %]</button>
-  <input type="submit" class="submit" name="action" value="[% 'Delete' | $T8 %]">
- </form>
-
-</form>
index 95cd8c0..e07484b 100644 (file)
 
 <script type="text/javascript">
 $(function() {
-    setupDependencies('EditAccount'); //name of form(s). Seperate each with a comma (ie: 'weboptions', 'myotherform' )
+    setupDependencies('EditAccount'); //name of form(s). Separate each with a comma (ie: 'weboptions', 'myotherform' )
 });
 </script>
 
-<form method="post" name="EditAccount" action="am.pl">
+<form method="post" name="EditAccount" action="am.pl" id="form">
 
 <input type="hidden" name="id"                 value="[% HTML.escape(id) %]">
 <input type="hidden" name="type"               value="account">
@@ -27,6 +27,8 @@ $(function() {
 <input type="hidden" name="expense_accno_id"   value="[% HTML.escape(expense_accno_id) %]">
 <input type="hidden" name="fxgain_accno_id"    value="[% HTML.escape(fxgain_accno_id) %]">
 <input type="hidden" name="fxloss_accno_id"    value="[% HTML.escape(fxloss_accno_id) %]">
+<input type="hidden" name="rndgain_accno_id"   value="[% HTML.escape(rndgain_accno_id) %]">
+<input type="hidden" name="rndloss_accno_id"   value="[% HTML.escape(rndloss_accno_id) %]">
 
 <fieldset>
   <legend>
@@ -126,7 +128,7 @@ $(function() {
   <legend>[% 'Include in drop-down menus' | $T8 %]</legend>
   <p style='font-weight:normal'>[% 'Changes in this block are only sensible if the account is NOT a summary account AND there exists one valid taxkey. To select both Receivables and Payables only make sense for Payment / Receipt (i.e. account cash).' | $T8 %]</p>
   <p style='font-weight:normal'>[% 'Changes to Receivables and Payables are only possible if no transactions to this account are posted yet.' | $T8 %]
-  [% 'The changing of tax-o-matic account is NOT recommended, but if you do so please also (re)configure buchungsgruppen and reconfigure ALL charts which point to this tax-o-matic account. ' | $T8 %]</p>
+  [% 'The changing of tax-o-matic account is NOT recommended, but if you do so please also (re)configure booking groups and reconfigure ALL charts which point to this tax-o-matic account. ' | $T8 %]</p>
   <table width="100%">
     <tr>
       <th align="left">[% 'Receivables' | $T8 %]</th>
@@ -215,7 +217,9 @@ $(function() {
               <tr>
                 <th align="left">[% 'Taxkey' | $T8 %]</th>
                 <th align="left">[% 'valid from' | $T8 %]</th>
-                <th align="left">[% 'pos_ustva' | $T8 %]</th>
+                [% IF feature_ustva %]
+                  <th align="left">[% 'pos_ustva' | $T8 %]</th>
+                [% END %]
                 <th align="left">[% 'delete' | $T8 %] ? </th>
               </tr>
 [% FOREACH tk = ACCOUNT_TAXKEYS %]
@@ -225,14 +229,18 @@ $(function() {
                 <input type="hidden" name="taxkey_id_[% tk.runningnumber %]" value="[% tk.id %]">
                 <td><select name="taxkey_tax_[% tk.runningnumber %]">[% tk.selecttaxkey %]</select></td>
                 <td><input name="taxkey_startdate_[% tk.runningnumber %]" value="[% HTML.escape(tk.startdate) %]"></td>
-                <td><select name="taxkey_pos_ustva_[% tk.runningnumber %]">[% tk.select_tax %]</select></td>
+                [% IF feature_ustva %]
+                  <td><select name="taxkey_pos_ustva_[% tk.runningnumber %]">[% tk.select_tax %]</select></td>
+                [% END %]
                 <td><input name="taxkey_del_[% tk.runningnumber %]" type="checkbox"
                 class="checkbox" value="delete"></td>
     [% ELSE %]
                 <input type="hidden" name="taxkey_id_[% tk.runningnumber %]" value="NEW">
                 <td><select name="taxkey_tax_[% tk.runningnumber %]">[% tk.selecttaxkey %]</select></td>
                 <td><input name="taxkey_startdate_[% tk.runningnumber %]" value="[% HTML.escape(tk.startdate) %]"></td>
-                <td><select name="taxkey_pos_ustva_[% tk.runningnumber %]">[% tk.select_tax %]</select></td>
+                [% IF feature_ustva %]
+                  <td><select name="taxkey_pos_ustva_[% tk.runningnumber %]">[% tk.select_tax %]</select></td>
+                [% END %]
                 <td>&nbsp;</td>
     [% END %]
               </tr>
@@ -245,23 +253,35 @@ $(function() {
 <fieldset class="DEPENDS ON charttype BEING A">
   <legend>[% 'Report and misc. Preferences' | $T8 %]</legend>
   <table>
-        <tr>
-          <th align="left">[% 'EUER' | $T8 %]</th>
-          <td colspan="3"><select name="pos_eur">[% select_eur %]</select></td>
-        </tr>
-        <tr>
-          <th align="left">[% 'BWA' | $T8 %]</th>
-          <td colspan="3"><select name="pos_bwa">[% select_bwa %]</select></td>
-        </tr>
-        <!-- Diese Steuerfunktion hat keine Auswirkung in der Bilanz und wird erstmal deaktiviert. -->
-        <!-- tr>
-          <th align="left">[% 'Bilanz' | $T8 %]</th>
-          <td colspan="3"><select name="pos_bilanz">[% select_bilanz %]</select></td>
-        </tr -->
-        <tr>
-          <th align="left">[% 'Datevautomatik' | $T8 %]</th>
-          <td colspan="3"><input name="datevautomatik" type="checkbox" class="checkbox" value="T" [% IF datevautomatik %]checked [% END %]>[% 'If checked the taxkey will not be exported in the DATEV Export, but only IF chart taxkeys differ from general ledger taxkeys' | $T8 %] </td>
-        </tr>
+        [% IF feature_erfolgsrechnung %]
+          <tr>
+            <th align="left">[% 'Erfolgsrechnung' | $T8 %]</th>
+            <td colspan="3"><select name="pos_er">[% select_er %]</select></td>
+          </tr>
+        [% END %]
+        [% IF feature_eurechnung %]
+          <tr>
+            <th align="left">[% 'EUER' | $T8 %]</th>
+            <td colspan="3"><select name="pos_eur">[% select_eur %]</select></td>
+          </tr>
+          <tr>
+            <th align="left">[% 'BWA' | $T8 %]</th>
+            <td colspan="3"><select name="pos_bwa">[% select_bwa %]</select></td>
+          </tr>
+        [% END %]
+        [% IF feature_balance %]
+          <!-- Diese Steuerfunktion hat keine Auswirkung in der Bilanz und wird erstmal deaktiviert. -->
+          <!-- tr>
+            <th align="left">[% 'Bilanz' | $T8 %]</th>
+            <td colspan="3"><select name="pos_bilanz">[% select_bilanz %]</select></td>
+          </tr -->
+        [% END %]
+        [% IF feature_datev %]
+          <tr>
+            <th align="left">[% 'Datevautomatik' | $T8 %]</th>
+            <td colspan="3"><input name="datevautomatik" type="checkbox" class="checkbox" value="T" [% IF datevautomatik %]checked [% END %]>[% 'If checked the taxkey will not be exported in the DATEV Export, but only IF chart taxkeys differ from general ledger taxkeys' | $T8 %] </td>
+          </tr>
+        [% END %]
         <tr>
           <th align="left">[% 'Folgekonto' | $T8 %]</th>
           <td><select name="new_chart_id">[% selectnewaccount %]</select></td>
@@ -288,3 +308,5 @@ $(function() {
   }
   </script>
 [% END %]
+[% L.hidden_tag('callback', callback) %]
+</form>
diff --git a/templates/webpages/am/edit_bins.html b/templates/webpages/am/edit_bins.html
new file mode 100644 (file)
index 0000000..8762dd3
--- /dev/null
@@ -0,0 +1,53 @@
+[%- USE HTML -%][%- USE T8 -%]
+
+<h1>[% title %]</h1>
+
+[% UNLESS BINS.size %]
+ <p>[% 'No bins have been added to this warehouse yet.' | $T8 %]</p>
+
+[% ELSE %]
+
+ <p>
+  [%- 'Bins that have been used in the past cannot be deleted anymore. For these bins there\'s no checkbox in the &quot;Delete&quot; column.' | $T8 %]
+ </p>
+
+ <form method="post" action="am.pl" id="form">
+
+  <input type="hidden" name="warehouse_id" value="[% HTML.escape(id) %]">
+
+  <input type="hidden" name="type" value="bin">
+  <input type="hidden" name="callback" value="[% HTML.escape(callback) %]">
+
+  <table border="0">
+   <tr>
+    <th class="listheading">[% 'Delete' | $T8 %]</th><th class="listheading">[% 'Description' | $T8 %]</th>
+    <th class="listheading">[% 'Delete' | $T8 %]</th><th class="listheading">[% 'Description' | $T8 %]</th>
+   </tr>
+   [%- SET row_odd = '1' %]
+   [%- USE bin_it = Iterator(BINS) %]
+   [%- FOREACH bin = bin_it %]
+   [%- IF row_odd %]
+   <tr>
+    [%- END %]
+
+    <td>[% IF bin.in_use %]&nbsp;[% ELSE %]<input type="checkbox" name="delete_[% bin_it.count %]" value="1">[% END %]</td>
+    <td>
+     <input type="hidden" name="id_[% bin_it.count %]" value="[% HTML.escape(bin.id) %]">
+     <input name="description_[% bin_it.count %]" value="[% HTML.escape(bin.description) %]">
+    </td>
+
+    [%- SET end_tr = '0' %]
+    [%- UNLESS row_odd %][%- SET end_tr = '1' %][%- END %]
+    [%- IF bin_it.last %][%- SET end_tr = '1' %][%- END %]
+    [%- IF end_tr %]
+   </tr>
+   [%- END %]
+
+   [%- IF row_odd %][% SET row_odd = '0' %][% ELSE %][% SET row_odd = '1' %][% END %]
+   [%- END %]
+  </table>
+
+  <input type="hidden" name="rowcount" value="[% BINS.size %]">
+ </form>
+
+[% END %]
diff --git a/templates/webpages/am/edit_price_factor.html b/templates/webpages/am/edit_price_factor.html
deleted file mode 100644 (file)
index 0e484df..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-<h1>[% title %]</h1>
-
-
- [% IF MESSAGE %]<p>[% MESSAGE %]</p>[% END %]
-
- <form method="post" action="am.pl">
-
-  <p>
-   <table border="0">
-    <tr>
-     <td align="right">[% 'Description' | $T8 %]</td>
-     <td><input id="description" name="description" value="[% HTML.escape(description) %]" class="initial_focus"></td>
-    </tr>
-
-    <tr>
-     <td align="right">[% 'Factor' | $T8 %]</td>
-[% IF !id || orphaned %]
-     <td><input name="factor" value="[% HTML.escape(factor) %]"></td>
-[% ELSE %]
-     <td><input type="hidden" name="factor" value="[% HTML.escape(factor) %]">
-         [% HTML.escape(factor) %] [% ' (in use so no change allowed)' | $T8 %]</td>
-[% END %]
-    </tr>
-   </table>
-  </p>
-
-  <p>
-   <input type="hidden" name="callback" value="[% HTML.escape(callback) %]">
-
-   <input type="hidden" name="type" value="price_factor">
-
-   <input type="hidden" name="id" value="[% HTML.escape(id) %]">
-   <input type="submit" name="action" value="[% 'Save' | $T8 %]">
-   [% IF id %][% IF orphaned %]<input type="submit" name="action" value="[% 'Delete' | $T8 %]">[% END %][% END %]
-  </p>
- </form>
index 46c45d8..616ac2d 100644 (file)
@@ -4,7 +4,7 @@
 [%- USE LxERP %]
 <h1>[% 'Tax-O-Matic' | $T8 %] [% title %]</h1>
 
- <form method="post" action="am.pl">
+ <form method="post" action="am.pl" id="form">
   <input type="hidden" name="id" value="[% HTML.escape(id) %]">
   <input type="hidden" name="type" value="tax">
 
   [% END %]
 
   <input type="hidden" name="callback" value="[% HTML.escape(callback) %]">
-
-  <input type="submit" class="submit" name="action" value="[% 'Save' | $T8 %]">
-
-  [% IF orphaned AND NOT tax_already_used %]
-  <input type="submit" class="submit" name="action" value="[% 'Delete' | $T8 %]">
-  [% END %]
-
 </form>
-
index ff6ece2..3b18ce0 100644 (file)
@@ -7,7 +7,7 @@
    <p style="text-align: right;">[<a href="doc/html/ch03s03.html" target="_blank" title="[% 'Open in new window' | $T8 %]">[% 'Help Template Variables' | $T8 %]</a>]</p>
   [% END %]
 
- <form method="post" name="Form" action="amtemplates.pl">
+ <form method="post" name="Form" action="amtemplates.pl" id="form">
 
   [% FOREACH var = HIDDEN %]<input type="hidden" name="[% HTML.escape(var.name) %]" value="[% HTML.escape(var.value) %]">[% END %]
 
@@ -43,8 +43,6 @@
    <hr>
   [% END %]
 
-
-
   [% IF SHOW_CONTENT %]
     <h2>
      [% IF CAN_EDIT %][% 'Edit file' | $T8 %][% ELSE %][% 'Display file' | $T8 %][% END %] [% HTML.escape(display_filename) %]
    [% IF CAN_EDIT %]
     <p><textarea name="content" id="edit_content" cols="100" rows="25"[% IF edit %] class="initial_focus"[% END %]>[% HTML.escape(content) %]</textarea></p>
 
-    <p>
-     <input type="hidden" name="save_nextsub" value="save_template">
-     <input type="submit" name="action" value="[% 'Save' | $T8 %]">
-     <input type="button" onclick="history.back()" value="[% 'Back' | $T8 %]">
-    </p>
-
     [% ELSE %]
 
-    <input type="hidden" name="edit_nextsub" value="edit_template">
-
-    <p><input name="action" type="submit" class="submit" value="[% 'Edit' | $T8 %]"></p>
-
     <p><pre class="filecontent">[% HTML.escape(content) %]</pre></p>
 
-    [% IF SHOW_SECOND_EDIT_BUTTON %]
-     <p><input name="action" type="submit" class="submit" value="[% 'Edit' | $T8 %]"></p>
-    [% END %]
-
    [% END %] <!-- CAN_EDIT -->
 
   [% END %] <!-- SHOW_CONTENT -->
index 1879d56..00bea73 100644 (file)
@@ -2,72 +2,27 @@
 [% USE HTML %]
 <h1>[% title %]</h1>
 
- [% IF saved_message %]
-  <p>[% saved_message %]</p>
+[% INCLUDE "common/flash.html" %]
 
-  <hr>
- [% END %]
+[% PROCESS "am/_units_header_info.html" %]
 
- <form method="post" action="[% HTML.escape(script) %]">
+<hr>
 
- <input type="hidden" name="type" value="unit">
-
- <p>
-  [% LxERP.t8('All units have either no or exactly one base unit of which they are multiples.') %]
-  [% LxERP.t8('If you select a base unit then you also have to enter a factor.') %]
-  [% LxERP.t8('You have to define a unit as a multiple of a smaller unit.') %]
-  [% LxERP.t8('Therefore the definition of "kg" with the base unit "g" and a factor of 1000 is valid while defining "g" with a base unit of "kg" and a factor of "0.001" is not.') %]
- </p>
+<p>
+ [% 'Units that have already been used (e.g. for parts and services or in invoices or warehouse transactions) cannot be changed.' | $T8 %]
+</p>
 
- <hr>
+<p>
+ [% 'Units marked for deletion will be deleted upon saving.' | $T8 %]
+</p>
 
- <h2>[% 'Add unit' | $T8 %]</h2>
+<p>
+ [% 'You can declare different translations for singular and plural for each unit (e.g. &quot;day&quot; and &quot;days).' | $T8 %]
+</p>
 
- <table>
-  <tr>
-   <th align="right">[% 'Unit' | $T8 %]</th>
-   <td><input name="new_name" size="20" maxlength="20"></td>
-  </tr>
-  <tr>
-   <th align="right">[% 'Base unit' | $T8 %]</th>
-   <td>
-    <select name="new_base_unit">
-     [% FOREACH row = NEW_BASE_UNIT_DDBOX %]<option [% row.selected %]>[% row.name %]</option>[% END %]
-    </select>
-   </td>
-  </tr>
-  <tr>
-   <th align="right">[% 'Factor' | $T8 %]</th>
-   <td><input name="new_factor"></td>
-  </tr>
-
-  [% FOREACH language = LANGUAGES %]
-   <tr>
-    <th align="right">[% language.description %]</th>
-    <td><input name="new_localized_[% language.id %]" size="20" maxlength="20"></td>
-    <th align="right">[% 'Plural' | $T8 %]</th>
-    <td><input name="new_localized_plural_[% language.id %]" size="20" maxlength="20"></td>
-   </tr>
-  [% END %]
- </table>
+<form method="post" action="[% HTML.escape(script) %]" id="form">
 
- <input type="submit" class="submit" name="action" value="[% 'Add' | $T8 %]">
-
- <hr>
-
- <h2>[% 'Edit units' | $T8 %]</h2>
-
- <p>
-  [% 'Units that have already been used (e.g. for parts and services or in invoices or warehouse transactions) cannot be changed.' | $T8 %]
- </p>
-
- <p>
-  [% 'Units marked for deletion will be deleted upon saving.' | $T8 %]
- </p>
-
- <p>
-  [% 'You can declare different translations for singular and plural for each unit (e.g. &quot;day&quot; and &quot;days).' | $T8 %]
- </p>
+ <input type="hidden" name="type" value="unit">
 
  <table id="unit_list">
   <thead>
 
  <input type="hidden" name="rowcount" value="[% UNITS.size %]">
 
- <input type="submit" class="submit" name="action" value="[% 'Save' | $T8 %]">
-
  </form>
 
  [% L.sortable_element('#unit_list tbody', url => 'controller.pl?action=Unit/reorder', with => 'unit_id') %]
-
index 9257308..f7731d3 100644 (file)
@@ -6,7 +6,7 @@
   <p>[% saved_message %]</p>
  [% END %]
 
- <form method="post" action="am.pl">
+ <form method="post" action="am.pl" id="form">
 
   <input type="hidden" name="id" value="[% HTML.escape(id) %]">
 
     <td><input name="prefix" value="[% 'Bin' | $T8 %]"></td>
    </tr>
   </table>
-
-  <p>
-   <input type="submit" class="submit" name="action" value="[% 'Save' | $T8 %]">
-   [%- IF id %][%- UNLESS in_use %]
-   <input type="submit" class="submit" name="action" value="[% 'Delete' | $T8 %]">
-   [%- END %][%- END %]
-  </p>
-
  </form>
-
- [% IF id %]
-
- <hr height="3">
-
- <h2>[% 'Edit Bins' | $T8 %]</h2>
-
- [% UNLESS BINS.size %]
- <p>[% 'No bins have been added to this warehouse yet.' | $T8 %]</p>
-
- [% ELSE %]
-
- <p>
-  [%- 'Bins that have been used in the past cannot be deleted anymore. For these bins there\'s no checkbox in the &quot;Delete&quot; column.' | $T8 %]
- </p>
-
- <form method="post" action="am.pl">
-
-  <input type="hidden" name="warehouse_id" value="[% HTML.escape(id) %]">
-
-  <input type="hidden" name="type" value="bin">
-  <input type="hidden" name="callback" value="[% HTML.escape(callback) %]">
-
-  <table border="0">
-   <tr>
-    <th class="listheading">[% 'Delete' | $T8 %]</th><th class="listheading">[% 'Description' | $T8 %]</th>
-    <th class="listheading">[% 'Delete' | $T8 %]</th><th class="listheading">[% 'Description' | $T8 %]</th>
-   </tr>
-   [%- SET row_odd = '1' %]
-   [%- USE bin_it = Iterator(BINS) %]
-   [%- FOREACH bin = bin_it %]
-   [%- IF row_odd %]
-   <tr>
-    [%- END %]
-
-    <td>[% IF bin.in_use %]&nbsp;[% ELSE %]<input type="checkbox" name="delete_[% bin_it.count %]" value="1">[% END %]</td>
-    <td>
-     <input type="hidden" name="id_[% bin_it.count %]" value="[% HTML.escape(bin.id) %]">
-     <input name="description_[% bin_it.count %]" value="[% HTML.escape(bin.description) %]">
-    </td>
-
-    [%- SET end_tr = '0' %]
-    [%- UNLESS row_odd %][%- SET end_tr = '1' %][%- END %]
-    [%- IF bin_it.last %][%- SET end_tr = '1' %][%- END %]
-    [%- IF end_tr %]
-   </tr>
-   [%- END %]
-
-   [%- IF row_odd %][% SET row_odd = '0' %][% ELSE %][% SET row_odd = '1' %][% END %]
-   [%- END %]
-  </table>
-
-  <input type="hidden" name="rowcount" value="[% BINS.size %]">
-
-  <p><input type="submit" class="submit" name="action" value="[% 'Save' | $T8 %]"></p>
- </form>
-
- [% END %]
-
- [% END %]
diff --git a/templates/webpages/am/form_footer.html b/templates/webpages/am/form_footer.html
deleted file mode 100644 (file)
index 18a561f..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-[%- USE T8 %]
-[%- USE L %]
-[%- USE HTML %]
-[%- USE LxERP %]
-[% L.hidden_tag('callback', callback) %]
-
-<br>
-[%- IF show_save %][% L.submit_tag('action', LxERP.t8('Save'), onclick = 'if ( typeof(callback_save) === "function" ) return callback_save(); ') %][% END %]
-[%- IF show_delete %][% L.submit_tag('action', LxERP.t8('Delete')) %][% END %]
-[%- IF show_save_as_new %][% L.submit_tag('action', LxERP.t8('Save as new')) %][% END %]
-
-</form>
-
diff --git a/templates/webpages/am/language_header.html b/templates/webpages/am/language_header.html
deleted file mode 100644 (file)
index 82124c0..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-[%- USE L %]
-[%- USE HTML %]
-[%- USE LxERP %]
-[%- USE T8 %]
-<h1>[% title | html %]</h1>
-
-<form method=post action=am.pl>
-
-<input type=hidden name=id value='[% id %]'>
-<input type=hidden name=type value=language>
-
-<table width=100%>
-  <tr>
-    <th align=right>[% 'Language' | $T8 %]</th>
-    <td><input name=description size=30 value="[% description | html %]"></td>
-  <tr>
-  <tr>
-    <th align=right>[% 'Template Code' | $T8 %]</th>
-    <td><input name=template_code size=5 value="[% template_code | html %]"></td>
-  </tr>
-  <tr>
-    <th align=right>[% 'Article Code' | $T8 %]</th>
-    <td><input name=article_code size=10 value="[% article_code | html %]"></td>
-  </tr>
-  <tr>
-    <th align=right>[% 'Number Format' | $T8 %]</th>
-    <td>[% L.select_tag('output_numberformat', numberformats, default = output_numberformat, with_empty = 1, empty_title = LxERP.t8('use program settings')) %]</td>
-  </tr>
-  <tr>
-    <th align=right>[% 'Date Format' | $T8 %]</th>
-    <td>[% L.select_tag('output_dateformat', dateformats, default = output_dateformat, with_empty = 1, empty_title=LxERP.t8('use program settings')) %]</td>
-  </tr>
-  <tr>
-    <th align=right>[% 'Long Dates' | $T8 %]</th>
-    <td>[% L.radio_button_tag('output_longdates', checked=output_longdates, label=LxERP.t8('Yes')) %]
-        [% L.radio_button_tag('output_longdates', checked=!output_longdates, label=LxERP.t8('No')) %]</td>
-  </tr>
-  <td colspan=2><hr size=3 noshade></td>
-  </tr>
-</table>
diff --git a/templates/webpages/am/language_list.html b/templates/webpages/am/language_list.html
deleted file mode 100644 (file)
index 9d71dda..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-[%- USE HTML %]
-[%- USE L %]
-[%- USE LxERP %]
-[%- USE T8 %]
-<h1>[% title | html %]</h1>
-
-<table width=100%>
-  <tr>
-    <td>
-      <table width=100%>
-        <tr class=listheading>
-          <th class=listheading>[% 'Description' | $T8 %]</th>
-          <th class=listheading>[% 'Template Code' | $T8 %]</th>
-          <th class=listheading>[% 'Article Code' | $T8 %]</th>
-          <th class=listheading>[% 'Number Format' | $T8 %]</th>
-          <th class=listheading>[% 'Date Format' | $T8 %]</th>
-          <th class=listheading>[% 'Long Dates' | $T8 %]</th>
-        </tr>
-[%- FOREACH row = ALL %]
-        <tr valign=top class=listrow[% loop.count % 2 %]>
-         <td><a href="am.pl?action=edit_language&id=[% row.id | html %]&callback=[% callback | html %]">[% row.description %]</a></td>
-         <td align=right>[% row.template_code | html %]</td>
-         <td align=right>[% row.article_code | html %]</td>
-         <td nowrap>[% row.output_numberformat ? row.output_numberformat : LxERP.t8('use program settings') | html %]</td>
-         <td nowrap>[% row.output_dateformat   ? row.output_dateformat   : LxERP.t8('use program settings') | html %]</td>
-         <td nowrap>[% row.output_longdates    ? LxERP.t8('Yes')         : LxERP.t8('No') %]</td>
-[%- END %]
-       </tr>
-      </table>
-    </td>
-  </tr>
-  <tr>
-  <td><hr size=3 noshade></td>
-  </tr>
-</table>
-
-<br>
-<form method=post action=am.pl>
-
-<input name=callback type=hidden value="[% callback | html %]">
-<input type=hidden name=type value=language>
-<input class=submit type=submit name=action value="[% 'Add' | $T8 %]">
-
-  </form>
-
diff --git a/templates/webpages/am/lead_header.html b/templates/webpages/am/lead_header.html
deleted file mode 100644 (file)
index f29b0ec..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-[%- USE HTML %]
-[%- USE T8 %]
-<h1>[% title | html %]</h1>
-
-<form method=post action=am.pl>
-
-<input type=hidden name=id value='[% id | html %]'>
-<input type=hidden name=type value=lead>
-
-<table width=100%>
-  <tr>
-    <th align=right>[% 'Description' | $T8 %]</th>
-    <td><input name=description size=50 value="[% lead | html %]"></td>
-  </tr>
-    <td colspan=2><hr size=3 noshade></td>
-  </tr>
-</table>
diff --git a/templates/webpages/am/lead_list.html b/templates/webpages/am/lead_list.html
deleted file mode 100644 (file)
index 1bdac5a..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-[%- USE LxERP %]
-[%- USE L %]
-<h1>[% title | html %]</h1>
-
-<table width=100%>
-  <tr class=listheading>
-    <th class=listheading width=100%>[% 'Description' | $T8 %]</th>
-  </tr>
-[%- FOREACH row = ALL  %]
-  <tr valign=top class=listrow[% loop.count % 2 %]>
-    <td><a href="am.pl?action=edit_lead&id=[% row.id | html %]&callback=[% callback | html %]">[% row.lead | html %]</a></td>
-  </tr>
-[%- END %]
-  <tr>
-  <td><hr size=3 noshade></td>
-  </tr>
-</table>
-
-<br>
-
-  <a href="am.pl?action=add&type=lead&callback=[% HTML.url(callback) %]">[%- 'Add' | $T8 %]</a>
index 884dc7d..e35b7d2 100644 (file)
  </tr>
 
  <tr class="coa_listrow[% loop.count % 2 %]">
-  <td class="coa_detail_emph">[% IF row.taxkey         %][% HTML.escape(row.taxkey).replace(',', '<br>')         %][% ELSE %]-[% END %]</td>
-  <td class="coa_detail_emph">[% IF row.taxaccount     %][% HTML.escape(row.taxaccount).replace(',', '<br>')     %][% ELSE %]-[% END %]</td>
-  <td class="coa_detail_emph">[% IF row.taxdescription %][% HTML.escape(row.taxdescription).replace(',', '<br>') %][% ELSE %]-[% END %]</td>
-  <td class="coa_detail_emph">[% IF row.tk_ustva       %][% HTML.escape(row.tk_ustva).replace(',', '<br>')       %][% ELSE %]-[% END %]</td>
-  <td class="coa_detail_emph">[% IF row.startdate      %][% HTML.escape(row.startdate).replace(',', '<br>')      %][% ELSE %]-[% END %]</td>
+  <td class = "coa_detail_emph">[% IF row.taxkeys.size         %][% FOR taxkey         = row.taxkeys         %][% HTML.escape(taxkey)         %]<br>[% END %][% ELSE %]-[% END %]</td>
+  <td class = "coa_detail_emph">[% IF row.taxaccounts.size     %][% FOR taxaccount     = row.taxaccounts     %][% HTML.escape(taxaccount)     %]<br>[% END %][% ELSE %]-[% END %]</td>
+  <td class = "coa_detail_emph">[% IF row.taxdescriptions.size %][% FOR taxdescription = row.taxdescriptions %][% HTML.escape(taxdescription) %]<br>[% END %][% ELSE %]-[% END %]</td>
+  <td class = "coa_detail_emph">[% IF row.pos_ustvas.size      %][% FOR pos_ustva      = row.pos_ustvas      %][% HTML.escape(pos_ustva)      %]<br>[% END %][% ELSE %]-[% END %]</td>
+  <td class = "coa_detail_emph">[% IF row.startdates.size      %][% FOR startdate      = row.startdates      %][% HTML.escape(startdate)      %]<br>[% END %][% ELSE %]-[% END %]</td>
  </tr>
 
  <tr class="coa_listrow[% loop.count % 2 %]">
diff --git a/templates/webpages/am/list_price_factors.html b/templates/webpages/am/list_price_factors.html
deleted file mode 100644 (file)
index bc2d0f5..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-[%- USE T8 %][% USE L %][% USE LxERP %]
-[% USE HTML %]
-<h1>[% title %]</h1>
-
- [% IF MESSAGE %]<p>[% MESSAGE %]</p>[% END %]
-
-
- <p>
-  <table width="100%" id="price_factor_list">
-   <thead>
-   <tr class="listheading">
-    <th align="center"><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]"></th>
-    <th width="80%">[% 'Description' | $T8 %]</th>
-    <th width="20%">[% 'Factor' | $T8 %]</th>
-   </tr>
-   </thead>
-
-   <tbody>
-   [% FOREACH factor = PRICE_FACTORS %]
-   <tr class="listrow[% loop.count % 2 %]" id="price_factor_id_[% factor.id %]">
-    <td align="center" class="dragdrop"><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]"></td>
-    <td><a href="[% url_base %]&action=edit_price_factor&id=[% HTML.url(factor.id) %]">[% HTML.escape(factor.description) %]</a></td>
-    <td>[% HTML.escape(factor.factor) %]</td>
-   </tr>
-   [% END %]
-   </tbody>
-  </table>
- </p>
-
- <hr height="3">
-
- <p>
-  <a href="am.pl?action=add&type=price_factor&callback=[% HTML.url(callback) %]">[%- 'Add' | $T8 %]</a>
- </p>
-
- [% L.sortable_element('#price_factor_list tbody', url => 'controller.pl?action=PriceFactor/reorder', with => 'price_factor_id') %]
index 836cca3..980d229 100644 (file)
@@ -1,5 +1,6 @@
 [%- USE T8 %]
 [%- USE HTML %]
+[% INCLUDE "common/flash.html" %]
  <h1>[% title %]</h1>
 
  <table>
@@ -25,7 +26,3 @@
   </tr>
   [% END %]
  </table>
-
- <p>
-  <a href="am.pl?action=add&type=tax&callback=[% HTML.url(callback) %]">[%- 'Add' | $T8 %]</a>
- </p>
index d6735d2..b9677b3 100644 (file)
   </table>
  </p>
 
- <hr height="3">
-
- <p>
-  <a href="am.pl?action=add&type=warehouse&callback=[% HTML.url(callback) %]">[%- 'Add' | $T8 %]</a>
- </p>
-
  [% L.sortable_element('#warehouse_list tbody', url => 'controller.pl?action=Warehouse/reorder', with => 'warehouse_id') %]
index a2f39ac..e269b8e 100644 (file)
@@ -1,6 +1,7 @@
 [%- USE T8 %]
 [%- USE HTML %]
 [%- USE L %]
+[%- USE P %]
 
 [%- SET var_name = HTML.escape(name_prefix) _ "cvar_" _ HTML.escape(var.name) _ HTML.escape(name_postfix) -%]
 
@@ -18,6 +19,9 @@
 [%- ELSIF var.type == 'textfield' %]
 <textarea name="[% var_name %]" cols="[% HTML.escape(var.width) %]" rows="[% HTML.escape(var.height) %]">[% HTML.escape(var.value) %]</textarea>
 
+[%- ELSIF var.type == 'htmlfield' %]
+<textarea name="[% var_name %]" cols="[% HTML.escape(var.width) %]" rows="[% HTML.escape(var.height) %]" class="texteditor">[% HTML.escape(var.value) %]</textarea>
+
 [%- ELSIF var.type == 'date' %]
 [% L.date_tag(var_name, var.value) %]
 
 <input name="[% var_name %]" value="[% HTML.escape(var.value) %]">
 
 [%- ELSIF var.type == 'customer' %]
-[% L.customer_vendor_picker(var_name, var.value, type='customer') %]
+[% P.customer_vendor.picker(var_name, var.value, type='customer') %]
 
 [%- ELSIF var.type == 'vendor' %]
-[% L.customer_vendor_picker(var_name, var.value, type='vendor') %]
+[% P.customer_vendor.picker(var_name, var.value, type='vendor') %]
 
 [%- ELSIF var.type == 'part' %]
-[% L.part_picker(var_name, var.value) %]
+[% P.part.picker(var_name, var.value) %]
 
 [%- ELSIF var.type == 'select' %]
 
index 8f0b20f..984c589 100644 (file)
@@ -1,6 +1,7 @@
 [%- USE T8 %]
 [%- USE HTML %]
 [%- USE L %]
+[%- USE P %]
 [%- USE LxERP %][%- USE P -%]
 [%- BLOCK cvar_inputs %]
 [%- SET render_cvar_tag_options = {};
 [% render_cvar_tag_options.import(cols=cvar.var.width, rows=cvar.var.height);
    L.textarea_tag(cvar_tag_name, cvar.value, render_cvar_tag_options) %]
 
+[%- ELSIF cvar.var.type == 'htmlfield' %]
+[% render_cvar_tag_options.import(cols=cvar.var.width, rows=cvar.var.height, class="texteditor");
+   L.textarea_tag(cvar_tag_name, L.restricted_html(cvar.value), render_cvar_tag_options) %]
+
 [%- ELSIF cvar.var.type == 'date' %]
 [%- L.date_tag(cvar_tag_name, cvar.value, render_cvar_tag_options) %]
 
 
 [%- ELSIF cvar.var.type == 'customer' %]
 [%- render_cvar_tag_options.type = 'customer' %]
-[%- L.customer_vendor_picker(cvar_tag_name, cvar.value, render_cvar_tag_options) %]
+[%- P.customer_vendor.picker(cvar_tag_name, cvar.value, render_cvar_tag_options) %]
 
 [%- ELSIF cvar.var.type == 'vendor' %]
 [%- render_cvar_tag_options.type = 'vendor' %]
-[%- L.customer_vendor_picker(cvar_tag_name, cvar.value, render_cvar_tag_options) %]
+[%- P.customer_vendor.picker(cvar_tag_name, cvar.value, render_cvar_tag_options) %]
 
 [%- ELSIF cvar.var.type == 'part' %]
-[% L.part_picker(cvar_tag_name, cvar.value, render_cvar_tag_options) %]
+[% P.part.picker(cvar_tag_name, cvar.value, render_cvar_tag_options) %]
 
 [%- ELSIF cvar.var.type == 'number' %]
 [%- L.input_tag(cvar_tag_name, LxERP.format_amount(cvar.value, -2), render_cvar_tag_options) %]
index e2ff615..96b7744 100644 (file)
@@ -16,7 +16,6 @@
      </select>
 
      [%- ELSIF var.type == 'date' %]
-     [% 'from (time)' | $T8 %]
      [% L.date_tag(filter_prefix _'cvar_'_ HTML.escape(var.name) _'_from') %]
 
      [% 'to (time)' | $T8 %]
diff --git a/templates/webpages/ap/ap_transactions_bottom.html b/templates/webpages/ap/ap_transactions_bottom.html
deleted file mode 100644 (file)
index dc2f2f5..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-[% USE T8 %][% USE HTML %] <form method="post" action="dispatcher.pl?M=ap">
-
-  <input name="callback" type="hidden" value="[% callback %]">
-
-  [% 'Create new' | $T8 %]<br>
-
-  <input class="submit" type="submit" name="A_ap_transaction" value="[%- 'AP Transaction' | $T8 %]">
-  <input class="submit" type="submit" name="A_vendor_invoice" value="[%- 'Vendor Invoice' | $T8 %]">
-
- </form>
-
index b7659fc..8f02e0c 100644 (file)
@@ -1,7 +1,7 @@
 [%- USE T8 %]
 [%- USE HTML %]
 [%- USE L %]
-[%- USE LxERP %]
+[%- USE LxERP %][%- USE P -%]
 
 [%- IF (num_follow_ups && num_due) %]
   <p>[% LxERP.t8('There are #1 unfinished follow-ups of which #2 are due.', num_follow_ups, num_due) %]</p>
@@ -9,42 +9,20 @@
 
 <input name=callback type=hidden value="[% callback | html %]">
 <input name=gldate type=hidden value="[% gldate | html %]">
-<input type=hidden name=draft_id value="[% draft_id %]">
-<input type=hidden name=draft_description value="[% draft_description | html %]">
-
-[%- IF ( !id && draft_id ) %]
-  [% L.checkbox_tag('remove_draft', checked=remove_draft, label=LxERP.t8('Remove draft when posting')) %]
-  <br>
-[%- END %]
-
-<br>
-
-<input class="submit" type="submit" name="action" id="update_button" value="[% 'Update' | $T8 %]">
-
-[%- IF id %]
-  [%- IF radier %]
-    <input class=submit type=submit name=action value="[% 'Post' | $T8 %]">
-    <input class=submit type=submit name=action value="[% 'Delete' | $T8 %]">
-  [%- END %]
+[% P.hidden_tag('draft_id', draft_id) %]
+[% P.hidden_tag('draft_description', draft_description) %]
+</form>
 
-  [%- IF show_storno %]
-    <input class=submit type=submit name=action value="[% 'Storno' | $T8 %]">
+<script type="text/javascript">
+ <!--
+$(document).ready(function() {
+  [%- SET row=0 %]
+  [%- WHILE row < rowcount %]
+   [%- SET row=row + 1 %]
+   $('#AP_amount_chart_id_[% row %]').on('set_item:ChartPicker', function(e, item) {
+     kivi.GL.update_taxes(this);
+   });
   [%- END %]
-
-  <input class=submit type=submit name=action value="[% 'Post Payment' | $T8 %]">
-  <input class=submit type=submit name=action value="[% 'Use As New' | $T8 %]">
-  <input type="button" class="submit" onclick="follow_up_window()" value="[% 'Follow-Up' | $T8 %]">
-
-[%- ELSIF show_post_draft %]
-    <input class=submit type=submit name=action value="[% 'Post' | $T8 %]">
-    <input type="submit" name="action" value="[% 'Save draft' | $T8 %]" class="submit">
-[%- END %]
-
-[%- IF id %]
-  <input type=button class=submit onclick="set_history_window([% id %], 'glid');" name="history" id="history" value="[% 'history' | $T8 %]">
-  [% IF INSTANCE_CONF.get_ap_show_mark_as_paid %]
-    <input type="submit" name="action" value="[% 'mark as paid' | $T8 %]">
-  [% END %]
-[%- END %]
-
-</form>
+});
+-->
+</script>
index fdcbefa..e3bc2e9 100644 (file)
@@ -1,38 +1,15 @@
 [%- USE L %]
+[%- USE P %]
 [%- USE HTML %]
 [%- USE T8 %]
-[%- USE LxERP %]
-
-<script type="text/javascript">
-<!--
-  function setTaxkey(accno, row) {
-    var taxkey = accno.options[accno.selectedIndex].value;
-    var reg = /--([0-9]*)/;
-    var found = reg.exec(taxkey);
-    var index = found[1];
-    index = parseInt(index);
-    var tax = 'taxchart_' + row;
-    for (var i = 0; i < document.getElementById(tax).options.length; ++i) {
-      var reg2 = new RegExp("^"+ index, "");
-      if (reg2.exec(document.getElementById(tax).options[i].value)) {
-        document.getElementById(tax).options[i].selected = true;
-        break;
-      }
-    }
-  };
-//-->
-</script>
+[%- USE LxERP %][%- USE P -%]
 
-<script type="text/javascript" src="js/show_history.js"></script>
+<h1>[% title | html %]</h1>
 
-<form method="post" action="[% script | html %]">
+[%- INCLUDE 'common/flash.html' %]
 
-<input type="hidden" name="selectvendor" value="[% selectvendor | html %]">
-<input type="hidden" name="selectdepartment" value="[% selectdepartment | html %]">
-<input type="hidden" name="selectcurrency" value="[% selectcurrency | html %]">
+<form method="post" action="[% script | html %]" id="form">
 
-<input type="hidden" name="oldvendor" value="[% oldvendor | html %]">
-<input type="hidden" name="vendor_id" value="[% vendor_id | html %]">
 <input type="hidden" name="terms" value="[% terms | html %]">
 
 <input type="hidden" name="creditlimit" value="[% creditlimit | html %]">
@@ -41,7 +18,7 @@
 <input type="hidden" name="forex" value="[% forex | html %]">
 <input type="hidden" name="lastmtime" value="[% HTML.escape(lastmtime) %]">
 
-<input type="hidden" name="id" value="[% id | html %]">
+<input id="id" type="hidden" name="id" value="[% id | html %]">
 <input type="hidden" name="sort" value="[% sort | html %]">
 <input type="hidden" name="closedto" value="[% closedto | html %]">
 <input type="hidden" name="locked" value="[% locked | html %]">
@@ -65,6 +42,8 @@
 
 <input type="hidden" name="paidaccounts" value="[% paidaccounts | html %]">
 
+[%- P.hidden_tag('convert_from_oe_id', convert_from_oe_id) -%]
+
 [% FOREACH i IN [1..paidaccounts] %]
   [% temp = "acc_trans_id_"_ i %]
   <input type="hidden" name="[% temp %]" value="[% $temp | html %]">
   <input type="hidden" name="[% temp %]" value="[% $temp | html %]">
 [% END %]
 
-<h1>[% title | html %]</h1>
-
 [% IF ( saved_message ) %]
   <p>[% saved_message | html %]</p>
 [% END %]
 
-<div class="tabwidget">
+<div id="ap_tabs" class="tabwidget">
  <ul>
   <li><a href="#ui-tabs-basic-data">[% 'Basic Data' | $T8 %]</a></li>
+[%- IF INSTANCE_CONF.get_webdav %]
+  <li><a href="#ui-tabs-webdav">[% 'WebDAV' | $T8 %]</a></li>
+[%- END %]
+[%- IF id AND INSTANCE_CONF.get_doc_storage %]
+      <li><a href="#ui-tabs-docs">[% 'Documents' | $T8 %]</a></li>
+      <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=purchase_invoice&object_id=[% HTML.url(id) %]">[% 'Attachments' | $T8 %]</a></li>
+[%- END %]
 [%- IF id %]
+  [%- IF AUTH.assert('record_links', 1) %]
   <li><a href="controller.pl?action=RecordLinks/ajax_list&object_model=PurchaseInvoice&object_id=[% HTML.url(id) %]">[% 'Linked Records' | $T8 %]</a></li>
+  [%- END %]
   <li><a href="[% 'controller.pl?action=AccTrans/list_transactions&trans_id=' _ HTML.url(id) | html %]">[% LxERP.t8('Transactions') %]</a></li>
 [%- END %]
  </ul>
 
+[%- IF id AND INSTANCE_CONF.get_doc_storage %]
+   <div id="ui-tabs-docs"></div>
+[%- END %]
+
 <div id="ui-tabs-basic-data">
 <table width="100%">
   <tr valign="top">
               <tr>
                 <th align="right" nowrap>[% 'Vendor' | $T8 %]</th>
                 <td colspan="3">
-                  [% IF ( selectvendor ) %]
-                    <select name="vendor" onchange="document.getElementById('update_button').click();">[% selectvendor %]</select>
-                  [% ELSE %]
-                    <input name=vendor value="[% vendor | html %]" size="35">
-                  [% END %]
-                  <input type="button" value="D" onclick="show_vc_details('vendor')">
+                 [% P.customer_vendor.picker("vendor_id", vendor_id, type="vendor", style="width: 330px", onchange="\$('#update_button').click()") %]
+                 [% L.button_tag("show_vc_details('vendor')", LxERP.t8('Details (one letter abbreviation)')) %]
+                 [% L.hidden_tag("previous_vendor_id", vendor_id) %]
                 </td>
               </tr>
 
 
               <tr>
                 <th align="right" nowrap>[% 'Currency' | $T8 %]</th>
-                <td><select name="currency">[% selectcurrency %]</select></td>
+                <td>[% L.select_tag("currency", currencies, default=currency, value_key="name", title_key="name") %]</td>
 
                 [% IF ( defaultcurrency && (currency != defaultcurrency) ) %]
                   <th align=right>[% 'Exchangerate' | $T8 %]</th>
 
               </tr>
 
-              [% IF ( selectdepartment ) %]
+              [% IF ALL_DEPARTMENTS %]
                 <tr>
                   <th align="right" nowrap>[% 'Department' | $T8 %]</th>
-                  <td colspan="3">
-                    <select name="department">[% selectdepartment %]</select>
-                  </td>
+                  <td colspan=3>[% L.select_tag('department_id', ALL_DEPARTMENTS, default = department_id, title_key = 'description', with_empty = 1, style = "width:334px") %]</td>
                 </tr>
               [% END %]
 
+              <tr>
+                <th align="right">[% 'Transaction description' | $T8 %]</th>
+                <td colspan="3">[% L.input_tag("transaction_description", transaction_description, style="width:330px", "data-validate"=INSTANCE_CONF.get_require_transaction_description_ps ? 'required' : '') %]</td>
+              </tr>
               <tr>
                 <td align="right"><input name="taxincluded" class="checkbox" type="checkbox" value="1" [% IF ( taxincluded ) %]checked[% END %]></td>
                 <th align=left nowrap>[% 'Tax Included' | $T8 %]</th>
             <table>
               <tr>
                 <th align="right" nowrap>[% 'Invoice Number' | $T8 %]</th>
-                <td><input name="invnumber" size="11" value="[% invnumber | html %]" [% readonly %]></td>
+                <td>[% L.input_tag("invnumber", invnumber, size="11", readonly=readonly) %]</td>
               </tr>
               <tr>
                 <th align="right" nowrap>[% 'Order Number' | $T8 %]</th>
-                <td><input name="ordnumber" size="11" value="[% ordnumber | html %]" [% readonly %]></td>
+                <td>[% L.input_tag("ordnumber", ordnumber, size="11", readonly=readonly) %]</td>
               </tr>
               <tr>
                 <th align="right" nowrap>[% 'Invoice Date' | $T8 %]</th>
-                <td>[% L.date_tag('transdate', transdate) %]</td>
+                <td>[% L.date_tag('transdate', transdate, onchange='kivi.SalesPurchase.set_duedate_on_reference_date_change("transdate")') %]</td>
               </tr>
               <tr>
                 <th align="right" nowrap>[% 'Due Date' | $T8 %]</th>
                 <td>[% L.date_tag('duedate', duedate) %]</td>
               </tr>
+              <tr>
+                <th align="right" nowrap>[% LxERP.t8('Tax point') %]</th>
+                <td>[% L.date_tag('tax_point', tax_point, id='tax_point') %]</td>
+              </tr>
+              <tr>
+                <th align=right nowrap>[% 'Delivery Date' | $T8 %]</th>
+                <td>[% L.date_tag('deliverydate', deliverydate) %]</td>
+              </tr>
               <tr>
                 <th align="right" nowrap>[% 'Project Number' | $T8 %]</th>
                 <td>
-                  [% L.select_tag('globalproject_id', ALL_PROJECTS, with_empty = 1, default = globalproject_id, value_key = 'id', title_key = 'projectnumber') %]
+                  [% P.project.picker('globalproject_id', globalproject_id, onchange="document.getElementById('update_button').click();") %]
                 </td>
               </tr>
             </table>
         [% FOREACH i IN [1..rowcount] %]
           <tr>
             <td>
-              [% selected_accno_full = "selected_accno_full_"_ i %]
-              [% L.select_tag('AP_amount_'_ i, ALL_CHARTS_AP_amount, value_title_sub = \AP_amount_value_title_sub, onchange = 'setTaxkey(this, '_ i _')', default = $selected_accno_full) %]
-
-              <input type="hidden" name="previous_AP_amount_[% i %]" value="[% $selected_accno_full %]">
+              [% SET selected_chart_id = "AP_amount_chart_id_"_ i %]
+              [% P.chart.picker("AP_amount_chart_id_" _ i, $selected_chart_id, style="width: 400px", type="AP_amount", class=(initial_focus == 'row_' _ i ? "initial_focus" : "")) %]
+              [% L.hidden_tag("previous_AP_amount_chart_id_" _ i, $selected_chart_id) %]
               <input type="hidden" name="tax_[% i %]" value="[% temp = "tax"_ i %][% $temp | html %]">
             </td>
             <td>
               <input name="amount_[% i %]" size="10" value="[% temp = "amount_"_ i %][% $temp | html %]">
             </td>
             <td>
-              [% temp = "tax_"_ i %][% $temp | html %]
+              [% temp_r = "tax_reverse_"_ i %]
+              [% IF ($temp_r) %]
+                [% $temp_r | html %]
+                &nbsp;&nbsp;&nbsp;
+                [% temp_c = "tax_charge_"_ i %][% $temp_c | html %]
+              [% ELSE %]
+                [% temp = "tax_"_ i %][% $temp | html %]
+              [% END %]
             </td>
             <td>
               [% temp = 'selected_taxchart_'_ i %]
-              [% L.select_tag('taxchart_'_ i, ALL_TAXCHARTS, value_title_sub = \taxchart_value_title_sub, default = $temp) %]
+              [% taxcharts = 'taxcharts_' _ i %]
+              [% L.select_tag('taxchart_'_ i, $taxcharts, value_title_sub = \taxchart_value_title_sub, default = $temp, style="width: 250px") %]
             </td>
             <td>
               [% temp = "project_id_"_ i %]
-              [% L.select_tag(temp, ALL_PROJECTS, with_empty = 1, default = loop.last ? globalproject_id : $temp, value_key = 'id', title_key = 'projectnumber') %]
+              [% P.project.picker(temp, loop.last ? globalproject_id : $temp) %]
             </td>
           </tr>
         [% END %]
         </tr>
         <tr>
           <td>
-            [% L.select_tag('APselected', ALL_CHARTS_AP, value_title_sub = \APselected_value_title_sub, default = APselected) %]
+            [% P.chart.picker('AP_chart_id', AP_chart_id, style="width: 400px", type="AP") %]
           </td>
           <th align="left">[% invtotal | html %]</th>
           <td colspan="4"></td>
     </tr>
     <tr>
       <td>
-        <table width="100%">
-        <tr>
-          <th align="left" width="1%">[% 'Notes' | $T8 %]</th>
-          <td align="left">
-            <textarea name="notes" rows="[% textarea_rows %]" cols="50" wrap="soft" [% readonly %]>[% notes | html %]</textarea>
-          </td>
-
-          <th align="left" width=1%>[% 'Notes for vendor' | $T8 %]</th>
-          <td align="left">
-            <textarea name="intnotes" rows="[% textarea_rows %]" cols="50" wrap="soft" readonly>[% intnotes | html %]</textarea>
-          </td>
-        </tr>
-      </table>
+        <table>
+          <tr>
+           <th align="left">[% 'Notes' | $T8 %]</th>
+           <th align="left">[% 'Internal Notes' | $T8 %]</th>
+           <th align="left">[% 'Payment Terms' | $T8 %]</th>
+          </tr>
+          <tr valign="top">
+           <td>
+            [% L.textarea_tag("notes", notes, wrap="soft", rows=textarea_rows, cols=50, readonly=readonly) %]
+           </td>
+           <td>
+            [% L.textarea_tag("intnotes", intnotes, wrap="soft", rows=textarea_rows, cols=50, readonly=readonly) %]
+           </td>
+           <td>
+             [% L.select_tag('payment_id', payment_terms, default=payment_id, title_key='description', with_empty=1, style="width: 250px", onchange="kivi.SalesPurchase.set_duedate_on_reference_date_change('transdate')") %]
+           </td>
+          <tr>
+        </table>
     </td>
   </tr>
   <tr>
     <td>
+      [% UNLESS no_payment_bookings %]
       <table width="100%">
         <tr class="listheading">
           <th class="listheading" colspan="7">[% 'Payments' | $T8 %]</th>
             [% temp = "paid_"_ i %]
             <td align="center">
               [% IF( changeable ) %]
-                <input name="[% temp %]" size="11" value="[% $temp | html %]" onBlur="check_right_number_format(this);">
+                <input name="[% temp %]" size="11" data-validate="number" class="numeric" value="[% $temp | html %]" id="[%- 'payment_' _ temp -%]">
               [% ELSE %]
                 [% $temp | html %]
                 <input type="hidden" name="[% temp %]" value="[% $temp | html %]">
               </td>
             [% END %]
 
-            [% temp = "AP_paid_"_ i %]
+            [% temp     = "AP_paid_"_ i %]
+            [% readonly = "AP_paid_readonly_desc_"_ i %]
             <td align="center">
               [% IF( changeable ) %]
                 [% L.select_tag(temp, ALL_CHARTS_AP_paid, value_title_sub = \AP_paid_value_title_sub, default = ($temp || accno_arap))  %]
               [% ELSE %]
-                [% $temp | html %]
+                [% $readonly | html %]
                 <input type="hidden" name="[% temp %]" value="[% $temp | html %]">
               [% END %]
             </td>
             <td align="center">
               [% temp = "paid_project_id_"_ i %]
               [% IF( changeable ) %]
-                [% L.select_tag(temp, ALL_PROJECTS, with_empty = 1, default = $temp, value_key = 'id', title_key = 'projectnumber') %]
+                [% P.project.picker(temp, $temp) %]
               [% ELSE %]
                 <input type="hidden" name="[% temp %]" value="[% $temp | html %]">
                 [% temp = "label"_ temp %]
           <td align="center">[% LxERP.format_amount(paid_missing, 2) | html %]</td>
         </tr>
       </table>
+      [% END %]
     </td>
   </tr>
 </table>
 </div>
+[% PROCESS 'webdav/_list.html' %]
+<div id="ui-tabs-1">
+ [% LxERP.t8('Loading...') %]
 </div>
+</div>
+
+<hr size="3" noshade>
 
 <script type='text/javascript'>
- $('#ap_set_to_paid_missing').click(function(){ $('input[name^="paid_"]:last').val('[% LxERP.format_amount(paid_missing, 2) %]') });
+ $('#ap_set_to_paid_missing').click(function(){ $('input[id^="payment_paid_"]:last').val("[% LxERP.format_amount(paid_missing, 2) %]") });
 </script>
index ba2c118..a676eb8 100644 (file)
@@ -1,8 +1,10 @@
 [%- USE T8 %]
-[%- USE L %]
+[%- USE L %][%- USE P -%]
+[% SET style="width: 250px" %]
 <h1>[% title %]</h1>
 
- <form method=post name="search" action=[% script %]>
+ <form method="post" name="search" action="ap.pl" id="form">
+  [% L.hidden_tag("sort", "transdate") %]
 
   <table width=100%>
   <tr>
     <table>
      <tr>
       <th align=right>[% 'Vendor' | $T8 %]</th>
-      <td colspan=3>
-            [%- INCLUDE 'generic/multibox.html'
-                 id            = 'vendor',
-                 name          = 'vendor',
-                 default       = oldvendor,
-                 style         = 'width: 250px',
-                 class         = 'initial_focus',
-                 DATA          = ALL_VC,
-                 id_sub        = 'vc_keys',
-                 label_key     = 'name',
-                 select        = vc_select,
-                 limit         = vclimit,
-                 show_empty    = 1,
-                 allow_textbox = 1,
-                 -%]
-      </td>
+      <td>[% L.input_tag("vendor", vendor, style=style, class="initial_focus") %]</td>
      </tr>
     <tr>
      <th align="right" nowrap>[% 'Contact Person' | $T8 %]</th>
-     <td colspan="3">[% L.input_tag("cp_name", '', size=20) %]</td>
+     <td>[% L.input_tag("cp_name", '', style=style) %]</td>
     </tr>
      <tr>
       <th align=right nowrap>[% 'Department' | $T8 %]</th>
-      <td>
-            [%- INCLUDE 'generic/multibox.html'
-                 name          = 'department',
-                 style         = 'width: 250px',
-                 DATA          = ALL_DEPARTMENTS,
-                 id_key        = 'id',
-                 label_key     = 'description',
-                 show_empty    = 1,
-                 allow_textbox = 0,
-            -%]
-      </td>
+      <td>[% L.select_tag('department_id', ALL_DEPARTMENTS, title_key = 'description', with_empty = 1, style=style) %]</td>
      </tr>
      <tr>
       <th align=right nowrap>[% 'Invoice Number' | $T8 %]</th>
-      <td colspan=3><input name=invnumber size=20></td>
+      <td>[% L.input_tag("invnumber", "", style=style) %]</td>
      </tr>
      <tr>
       <th align=right nowrap>[% 'Order Number' | $T8 %]</th>
-      <td colspan=3><input name=ordnumber size=20></td>
+      <td>[% L.input_tag("ordnumber", "", style=style) %]</td>
+     </tr>
+     <tr>
+      <th align="right">[% 'Steuersatz' | $T8 %]</th>
+      <td>[% L.select_tag('taxzone_id', ALL_TAXZONES, with_empty=1, title_key='description', style=style) %]</td>
      </tr>
+     <tr>
+      <th align="right" nowrap>[% 'Transaction description' | $T8 %]</th>
+      <td>[% L.input_tag("transaction_description", "", style=style) %]</td>
      <tr>
       <th align=right nowrap>[% 'Notes' | $T8 %]</th>
-      <td colspan=3><input name=notes size=40></td>
+      <td>[% L.input_tag("notes", "", style=style) %]</td>
+      <th align="right">[% 'Part Description' | $T8 %]</th>
+      <td>[% L.input_tag("parts_description", "", style=style) %]</td>
      </tr>
      <tr>
       <th align="right">[% 'Project Number' | $T8 %]</th>
-      <td colspan="3">
-            [%- INCLUDE 'generic/multibox.html'
-                 name          =  'project_id',
-                 style         = "width: 250px",
-                 DATA          =  ALL_PROJECTS,
-                 id_key        = 'id',
-                 label_key     = 'projectnumber',
-                 limit         = vclimit,
-                 show_empty    = 1,
-                 allow_textbox = 0,
-            -%]
-      </td>
+      <td>[% P.project.picker("project_id", project_id, active="both", valid="both", style=style) %]</td>
+      <th align="right">[% 'Part Number' | $T8 %]</th>
+      <td>[% L.input_tag("parts_partnumber", "", style=style) %]</td>
      </tr>
      <tr>
-      <th align=right nowrap>[% 'From' | $T8 %]</th>
+      <th align=right nowrap>[% 'Invoice Date' | $T8 %]</th>
       <td>
        [% L.date_tag('transdatefrom') %]
-      </td>
-     <th align=right>[% 'Bis' | $T8 %]</th>
-     <td>
-      [% L.date_tag('transdateto') %]
+       [% 'Bis' | $T8 %]
+       [% L.date_tag('transdateto') %]
+     </td>
+    </tr>
+     <tr>
+      <th align=right nowrap>[% 'Due Date' | $T8 %]</th>
+      <td>
+       [% L.date_tag('duedatefrom') %]
+       [% 'Bis' | $T8 %]
+       [% L.date_tag('duedateto') %]
      </td>
     </tr>
-   <input type=hidden name=sort value=transdate>
    </table>
     </td>
     </tr>
            <td nowrap>[% 'Notes' | $T8 %]</td>
            <td align=right><input name="l_employee" class=checkbox type=checkbox value=Y></td>
            <td nowrap>[% 'Employee' | $T8 %]</td>
+           <td align=right><input name="l_department" class=checkbox type=checkbox value=Y></td>
+           <td nowrap>[% 'Department' | $T8 %]</td>
           </tr>
           <tr>
            <td align=right><input name="l_subtotal" class=checkbox type=checkbox value=Y></td>
            <td nowrap>[% 'Subtotal' | $T8 %]</td>
            <td align=right><input name="l_globalprojectnumber" class=checkbox type=checkbox value=Y></td>
            <td nowrap>[% 'Document Project Number' | $T8 %]</td>
+           <td align="right"><input name="l_transaction_description" id="l_transaction_description" class=checkbox type=checkbox value=Y[% IF INSTANCE_CONF.get_require_transaction_description_ps %] checked[% END %]></td>
+           <td nowrap>[% 'Transaction description' | $T8 %]</td>
          </tr>
           <tr>
            <td align=right><input name="l_taxzone" class=checkbox type=checkbox value=Y></td>
            <td align=right><input name="l_payment_terms" class=checkbox type=checkbox value=Y></td>
            <td nowrap>[% 'Payment Terms' | $T8 %]</td>
            <td align=right><input name="l_charts" class=checkbox type=checkbox value=Y></td>
-           <td nowrap>[% 'Buchungskonto' | $T8 %]</td>
+           <td nowrap>[% 'Chart' | $T8 %]</td>
            <td align=right><input name="l_direct_debit" id="l_direct_debit" class=checkbox type=checkbox value=Y></td>
            <td nowrap>[% 'direct debit' | $T8 %]</td>
           </tr>
+          <tr>
+           <td align=right><input name="l_debit_chart" class=checkbox type=checkbox value=Y></td>
+           <td nowrap>[% 'Debit Account' | $T8 %]</td>
+           <td align=right><input name="l_insertdate" class=checkbox type=checkbox value=Y></td>
+           <td nowrap>[% 'Insert Date' | $T8 %]</td>
+          </tr>
          <tr>
-          <td colspan=4 align=left><b>[% 'Vendor' | $T8 %] </td>
+          <td colspan=4 align=left><b>[% 'Vendor' | $T8 %]</b></td>
          </tr>
          <tr>
            <td align=right><input name="l_vendornumber" class=checkbox type=checkbox value=Y></td>
       </table>
      </td>
     </tr>
-    <tr>
-     <td><hr size=3 noshade></td>
-    </tr>
    </table>
-   <input type=hidden name=nextsub value=[% nextsub %]>
-   <br>
-   <input class=submit type=submit name=action value="[% 'Continue' | $T8 %]">
   </form>
index ad631f3..e24771b 100644 (file)
@@ -1,11 +1,29 @@
-[% USE T8 %][% USE HTML %] <form method="post" action="dispatcher.pl?M=ar">
+[% USE T8 %][% USE HTML %][%- USE LxERP -%][%- USE L -%]
 
-  <input name="callback" type="hidden" value="[% callback %]">
-
-  [% 'Create new' | $T8 %]<br>
-
-  <input class="submit" type="submit" name="A_ar_transaction" value="[%- 'AR Transaction' | $T8 %]">
-  <input class="submit" type="submit" name="A_sales_invoice" value="[%- 'Sales Invoice' | $T8 %]">
+  [% L.hidden_tag("action", "MassInvoiceCreatePrint/dispatch") %]
+  [% L.hidden_tag("printer_id") %]
+  [% L.hidden_tag("bothsided") %]
+ </form>
 
+ <form method="post" action="ar.pl" id="create_new_form">
+  [% L.hidden_tag("callback", callback) %]
  </form>
 
+[% IF ALL_PRINTERS.size %]
+ <div id="print_options" class="hidden">
+   <p>
+     [% LxERP.t8("Print both sided") %]:
+     [% L.checkbox_tag('', id="print_options_bothsided") %]
+   </p>
+  <p>
+  [% LxERP.t8("Print destination") %]:
+  [% SET  printers = [ { description=LxERP.t8("Download PDF, do not print") } ] ;
+     CALL printers.import(ALL_PRINTERS);
+     L.select_tag("", printers, id="print_options_printer_id", title_key="description", default=printer_id) %]
+  </p>
+
+  <p>
+   [% L.button_tag("kivi.MassInvoiceCreatePrint.massPrint()", LxERP.t8('Print')) %]
+  </p>
+ </div>
+[% END %]
diff --git a/templates/webpages/ar/ar_transactions_header.html b/templates/webpages/ar/ar_transactions_header.html
new file mode 100644 (file)
index 0000000..fcb3184
--- /dev/null
@@ -0,0 +1 @@
+<form method="post" action="controller.pl" id="report_form">
index 9927cc8..3fedfe1 100644 (file)
@@ -1,5 +1,6 @@
 [% USE LxERP %]
 [% USE T8 %]
+[% USE L %][%- USE P -%]
 
   [% IF ( follow_up_length && follow_up_due_length ) %]
     [% LxERP.t8('There are #1 unfinished follow-ups of which #2 are due.', follow_up_length , follow_up_due_length) %]
@@ -7,46 +8,20 @@
 
   <input type="hidden" name="gldate" value="[% gldate | html %]">
   <input type="hidden" name="callback" value="[% callback | html %]">
-  <input type="hidden" name="draft_id" value="[% draft_id | html %]">
-  <input type="hidden" name="draft_description" value="[% draft_description | html %]">
-
-  <br>
-
-  [% IF ( !id && draft_id ) %]
-    <input type="checkbox" name="remove_draft" id="remove_draft" value="1" [% IF ( remove_draft ) %]checked[% END %]>
-    <label for="remove_draft">[% 'Remove draft when posting' | $T8 %]</label>
-  [% END %]
-
-  <br>
-
-  <input class="submit" type="submit" name="action" id="update_button" value="[% 'Update' | $T8 %]">
-
-  [% IF ( show_storno_button ) %]
-    <input class="submit" type="submit" name="action" value="[% 'Storno' | $T8 %]">
-  [% END %]
-
-  [% IF ( id ) %]
-    [% IF ( radier ) %]
-      <input class="submit" type="submit" name="action" value="[% 'Post' | $T8 %]">
-      <input class="submit" type="submit" name="action" value="[% 'Delete' | $T8 %]">
-    [% END %]
-
-    [% IF ( !is_closed ) %]
-      <input class="submit" type="submit" name="action" value="[% 'Use As New' | $T8 %]">
-    [% END %]
-
-    <input class="submit" type="submit" name="action" value="[% 'Post Payment' | $T8 %]">
-    <input type="button" class="submit" onclick="follow_up_window()" value="[% 'Follow-Up' | $T8 %]">
-    <input type="button" class="submit" onclick="set_history_window([% id %], 'glid');" name="history" id="history" value="[% 'history' | $T8 %]">
-  [% ELSE %]
-    [% IF ( !is_closed ) %]
-      <input class="submit" type="submit" name="action" value="[% 'Post' | $T8 %]">
-      <input class="submit" type="submit" name="action" value="[% 'Save draft' | $T8 %]">
-    [% END %]
-  [% END %]
-
-  [% IF ( show_mark_as_paid_button ) %]
-    <input type="submit" class="submit" name="action" value="[% 'mark as paid' | $T8 %]">
-  [% END %]
-
+  [% P.hidden_tag('draft_id', draft_id) %]
+  [% P.hidden_tag('draft_description', draft_description) %]
 </form>
+
+<script type="text/javascript">
+ <!--
+$(document).ready(function() {
+  [%- SET row=0 %]
+  [%- WHILE row < rowcount %]
+   [%- SET row=row + 1 %]
+   $('#AR_amount_chart_id_[% row %]').on('set_item:ChartPicker', function(e, item) {
+     kivi.GL.update_taxes(this);
+   });
+  [%- END %]
+});
+-->
+</script>
index 2c9870f..d42d70c 100644 (file)
@@ -1,9 +1,13 @@
 [%- USE HTML %]
 [%- USE L %]
 [%- USE T8 %]
-[%- USE LxERP %]
+[%- USE LxERP %][%- USE P -%]
 
-<form method=post name="arledger" action="[% script %]">
+<h1>[% title | html %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+<form method=post name="arledger" action="[% script %]" id="form">
 
 [% L.hidden_tag('id', id) %]
 [% L.hidden_tag('sort', sort) %]
 [% L.hidden_tag('follow_up_rowcount', 1) %]
 <input type="hidden" name="lastmtime" value="[% HTML.escape(lastmtime) %]">
 
-<h1>[% title | html %]</h1>
-
 [%- IF saved_message %]<p>[% saved_message | html  %]</p>[% END %]
 
 <div class="tabwidget">
  <ul>
   <li><a href="#ui-tabs-basic-data">[% 'Basic Data' | $T8 %]</a></li>
 [%- IF id %]
+  [%- IF INSTANCE_CONF.get_doc_storage %]
+  <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=invoice&object_id=[% HTML.url(id) %]">[% 'Attachments' | $T8 %]</a></li>
+  [%- END %]
+  [% IF AUTH.assert('record_links', 1) %]
   <li><a href="controller.pl?action=RecordLinks/ajax_list&object_model=Invoice&object_id=[% HTML.url(id) %]">[% 'Linked Records' | $T8 %]</a></li>
+  [%- END %]
   <li><a href="[% 'controller.pl?action=AccTrans/list_transactions&trans_id=' _ HTML.url(id) | html %]">[% LxERP.t8('Transactions') %]</a></li>
 [%- END %]
  </ul>
               <tr>
                 <th align="right" nowrap>[% 'Customer' | $T8 %]</th>
                 <td colspan=3>
-[%- IF selectcustomer %]
-    <select id='customer' name="customer" onchange="document.getElementById('update_button').click();" class="initial_focus">[% selectcustomer %]</select>
-[%- ELSE %]
-    <input id='customer' name=customer value="[% customer | html %]" size=35 class="initial_focus">
-[%- END %]
-                <input type="button" value="[% 'Details (one letter abbreviation)' | $T8 %]" onclick="show_vc_details('customer')"></td>
-                [% L.hidden_tag('selectcustomer', selectcustomer) %]
-                [% L.hidden_tag('oldcustomer', oldcustomer) %]
-                [% L.hidden_tag('customer_id', customer_id) %]
-                [% L.hidden_tag('terms', terms) %]
+                 [% P.customer_vendor.picker("customer_id", customer_id, type="customer", style="width: 330px", class=(initial_focus == 'customer_id' ? "initial_focus" : ""), onchange="\$('#update_button').click()") %]
+                 [% L.button_tag("show_vc_details('customer')", LxERP.t8('Details (one letter abbreviation)')) %]
+                 [% L.hidden_tag("previous_customer_id", customer_id) %]
+                 [% L.hidden_tag('terms', terms) %]
+               </td>
               </tr>
 [%- IF max_dunning_level || invoice_obj.dunning_config_id  %]
               <tr>
@@ -90,8 +92,7 @@
 [%- END %]
               <tr>
                 <th align=right>[% 'Currency' | $T8 %]</th>
-                <td><select name=currency>[% selectcurrency %]</select></td>
-                [% L.hidden_tag('selectcurrency', selectcurrency) %]
+                <td>[% L.select_tag("currency", currencies, default=currency, value_key="name", title_key="name") %]</td>
                 [% L.hidden_tag('defaultcurrency', defaultcurrency) %]
                 [% L.hidden_tag('fxgain_accno', fxgain_accno) %]
                 [% L.hidden_tag('fxloss_accno', fxloss_accno) %]
                 [% L.hidden_tag('forex', forex) %]
                 [% IF show_exch %]
                    <th align=right>[% 'Exchangerate' | $T8 %]</th>
-                   <td>[%- IF forex %][% L.hidden_tag('exchangerate', LxERP.format_amount(exchangerate, 2)) %][% LxERP.format_amount(exchangerate, 2) %][%- ELSE %][% L.input_tag('exchangerate', LxERP.format_amount(exchangerate, 2), size=10) %][%- END %]</td>
+                   <td>[%- IF forex %][% L.hidden_tag('exchangerate', LxERP.format_amount(exchangerate, 5, 1)) %][% LxERP.format_amount(exchangerate, 5, 1) %][%- ELSE %][% L.input_tag('exchangerate', LxERP.format_amount(exchangerate, 5, 1), size=10) %][%- END %]</td>
                 [% END %]
               </tr>
-              [% department_html %]
-              [%- IF selectdepartment %]
+              [% IF ALL_DEPARTMENTS %]
+                <tr>
+                  <th align="right" nowrap>[% 'Department' | $T8 %]</th>
+                  <td colspan=3>[% L.select_tag('department_id', ALL_DEPARTMENTS, default = department_id, title_key = 'description', with_empty = 1, style = 'width:334px') %]</td>
+                </tr>
+              [% END %]
               <tr>
-                <th align="right" nowrap>[% 'Department' | $T8 %]</th>
-                <td colspan=3><select name=department>[% selectdepartment %]</select>
-                <input type=hidden name=selectdepartment value="[% selectdepartment | html %]">
-                </td>
+                <th align="right">[% 'Transaction description' | $T8 %]</th>
+                <td colspan="3">[% L.input_tag("transaction_description", transaction_description, style="width:330px", "data-validate"=INSTANCE_CONF.get_require_transaction_description_ps ? 'required' : '') %]</td>
               </tr>
-              [%- END %]
               <tr>
                 <td align=right>[% L.checkbox_tag('taxincluded', checked=taxincluded) %]</td>
                 <th align="left" nowrap><label for="taxincluded">[% 'Tax Included' | $T8 %]</label></th>
           </td>
           <td align=right>
             <table>
-[%- IF selectemployee %]
               <tr>
                 <th align=right nowrap>[% 'Salesperson' | $T8 %]</th>
-                <td  colspan=2><select name=employee>[% selectemployee %]</select>[% L.hidden_tag('selectemployee', selectemployee) %]</td>
+                <td  colspan=2>[% P.select_tag("employee_id", employees, default=employee_id, title_key='safe_name') %]</td>
               </tr>
-[%- ELSE %]
-                [% L.hidden_tag('employee', employee) %]
-[%- END %]
               <tr>
                 <th align=right nowrap>[% 'Invoice Number' | $T8 %]</th>
                 <td><input name=invnumber size=11 value="[% invnumber | html %]"></td>
                 <th align=right nowrap>[% 'Due Date' | $T8 %]</th>
                 <td>[% L.date_tag('duedate', duedate) %]</td>
               </tr>
+              <tr>
+                <th align="right" nowrap>[% LxERP.t8('Tax point') %]</th>
+                <td>[% L.date_tag('tax_point', tax_point, id='tax_point') %]</td>
+              </tr>
+              <tr>
+                <th align=right nowrap>[% 'Delivery Date' | $T8 %]</th>
+                <td>[% L.date_tag('deliverydate', deliverydate) %]</td>
+              </tr>
               <tr>
                 <th align=right nowrap>[% 'Project Number' | $T8 %]</th>
-                <td>[% L.select_tag('globalproject_id', ALL_PROJECTS, title_key = 'projectnumber', default = globalproject_id, with_empty = 1) %]</td>
+                <td>[% L.select_tag('globalproject_id', ALL_PROJECTS, title_key = 'projectnumber', default = globalproject_id, with_empty = 1, onChange = "document.getElementById('update_button').click();") %]</td>
               </tr>
      </table>
           </td>
           </td>
         </tr>
         <tr>
-          <td>[% ARselected %]</td>
+          <td>[% P.chart.picker("AR_chart_id", AR_chart_id, style="width: 400px", type="AR") %]</td>
           <th align=left>[% LxERP.format_amount(invtotal, 2) | html %]</th>
 
           <input type=hidden name=oldinvtotal value='[% oldinvtotal %]'>
          </td>
          <td align=center>
   [%- IF row.changeable %]
-          <input name="paid_[% loop.count %]" size=11 value="[% row.paid ? LxERP.format_amount(row.paid, 2) : '' | html %]" onBlur="check_right_number_format(this)">
+          <input name="paid_[% loop.count %]" size=11 data-validate="number" class="numeric" value="[% row.paid ? LxERP.format_amount(row.paid, 2) : '' | html %]">
   [%- ELSE %]
          [% row.paid | html %]<input type=hidden name="paid_[% loop.count %]" value="[% row.paid ? LxERP.format_amount(row.paid, 2) : '' | html %]">
   [%- END %]
 [%- IF show_exch %]
          <td align=center>
     [%- IF row.forex || !row.changeable%]
-          <input type=hidden name="exchangerate_[% loop.count %]" value='[% row.exchangerate | html %]'>[% row.exchangerate | html %]
+          <input type=hidden name="exchangerate_[% loop.count %]" value="[%- LxERP.format_amount(row.exchangerate, 5, 1) -%]">[%- LxERP.format_amount(row.exchangerate, 5, 1) -%]
     [%- ELSE %]
-          <input name="exchangerate_[% loop.count %]" size=10 value='[% row.exchangerate | html %]'>
+          <input name="exchangerate_[% loop.count %]" size=10 value="[%- LxERP.format_amount(row.exchangerate, 5, 1) -%]">
     [%- END %]
           <input type=hidden name="forex_[% loop.count %]" value='[% row.forex | html %]'>
          </td>
 </div>
 
 <script type='text/javascript'>
- $('#ar_set_to_paid_missing').click(function(){ $('input[name^="paid_"]:last').val('[% LxERP.format_amount(paid_missing, 2) %]') });
+ $('#ar_set_to_paid_missing').click(function(){ $('input[name^="paid_"]:last').val("[% LxERP.format_amount(paid_missing, 2) %]") });
 </script>
index 7d3158e..3a68962 100644 (file)
@@ -1,8 +1,11 @@
 [%- USE T8 %]
-[%- USE L %]
+[%- USE L %][%- USE P -%]
+[%- SET style="width: 250px" %]
 <h1>[% title %]</h1>
 
- <form method=post name="search" action=[% script %]>
+ <form method=post name="search" id="form" action=[% script %]>
+  [% L.hidden_tag("action", nextsub) %]
+  [% L.hidden_tag("sort", "transdate") %]
 
   <table width=100% border="0">
   <tr>
     <table>
      <tr>
       <th align=right>[% 'Customer' | $T8 %]</th>
-      <td colspan=3>
-            [%- INCLUDE 'generic/multibox.html'
-                 name          = 'customer',
-                 default       = oldcustomer,
-                 style         = 'width: 250px',
-                 DATA          = ALL_VC,
-                 id_sub        = 'vc_keys',
-                 label_key     = 'name',
-                 select        = vc_select,
-                 limit         = vclimit,
-                 show_empty    = 1,
-                 allow_textbox = 1,
-                 class         = 'initial_focus',
-                 -%]
-      </td>
+      <td>[% L.input_tag("customer", customer, style=style, class="initial_focus") %]</td>
      </tr>
     <tr>
      <th align="right" nowrap>[% 'Contact Person' | $T8 %]</th>
-     <td colspan="3">[% L.input_tag("cp_name", '', size=20) %]</td>
+     <td>[% L.input_tag("cp_name", '', style=style) %]</td>
     </tr>
      <tr>
       <th align=right nowrap>[% 'Department' | $T8 %]</th>
-      <td>
-            [%- INCLUDE 'generic/multibox.html'
-                 name          = 'department',
-                 select_name   = 'department_id',
-                 style         = 'width: 250px',
-                 DATA          = ALL_DEPARTMENTS,
-                 id_key        = 'id',
-                 label_key     = 'description',
-                 show_empty    = 1,
-                 allow_textbox = 0,
-            -%]
-      </td>
+      <td>[% L.select_tag('department_id', ALL_DEPARTMENTS, title_key = 'description', with_empty = 1, style=style) %]</td>
      </tr>
      <tr>
       <th align=right nowrap>[% 'Invoice Number' | $T8 %]</th>
-      <td colspan=3><input name=invnumber id=invnumber size=20></td>
+      <td>[% L.input_tag("invnumber", "", style=style) %]</td>
      </tr>
      <tr>
       <th align=right nowrap>[% 'Order Number' | $T8 %]</th>
-      <td colspan=3><input name=ordnumber id=ordnumber size=20></td>
+      <td>[% L.input_tag("ordnumber", "", style=style) %]</td>
      </tr>
      <tr>
       <th align="right" nowrap>[% 'Customer Order Number' | $T8 %]</th>
-      <td colspan=3><input name="cusordnumber" id="cusordnumber" size="20"></td>
+      <td>[% L.input_tag("cusordnumber", "", style=style) %]</td>
      </tr>
      <tr>
       <th align="right">[% 'Employee' | $T8 %]</th>
-      <td>[% L.select_tag('employee_id', ALL_EMPLOYEES, title_key = 'safe_name', with_empty = 1, style = 'width:250px') %]</td>
+      <td>[% L.select_tag('employee_id', ALL_EMPLOYEES, title_key = 'safe_name', with_empty = 1, style=style) %]</td>
      </tr>
     <tr>
      <th align="right">[% 'Salesman' | $T8 %]</th>
-     <td>[% L.select_tag('salesman_id', ALL_EMPLOYEES, title_key = 'safe_name', with_empty = 1, style = 'width:250px') %]</td>
+     <td>[% L.select_tag('salesman_id', ALL_EMPLOYEES, title_key = 'safe_name', with_empty = 1, style=style) %]</td>
+     </tr>
+     <tr>
+      <th align="right">[% 'Steuersatz' | $T8 %]</th>
+      <td>[% L.select_tag('taxzone_id', ALL_TAXZONES, with_empty=1, title_key='description', style=style) %]</td>
      </tr>
      <tr>
       <th align=right nowrap>[% 'Transaction description' | $T8 %]</th>
-      <td colspan=3><input name=transaction_description id=transaction_description size=40></td>
+      <td>[% L.input_tag("transaction_description", "", style=style) %]</td>
+      <th align="right">[% 'Part Description' | $T8 %]</th>
+      <td>[% L.input_tag("parts_description", "", style=style) %]</td>
      </tr>
      <tr>
       <th align=right nowrap>[% 'Notes' | $T8 %]</th>
-      <td colspan=3><input name=notes id=notes size=40></td>
+      <td>[% L.input_tag("notes", "", style=style) %]</td>
+      <th align="right">[% 'Part Number' | $T8 %]</th>
+      <td>[% L.input_tag("parts_partnumber", "", style=style) %]</td>
+     </tr>
+     <tr>
+      <th align=right nowrap>[% 'Shipping Point' | $T8 %]</th>
+      <td>[% L.input_tag("shippingpoint", "", style=style) %]</td>
+      <th align="right">[% 'Ship via' | $T8 %]</th>
+      <td>[% L.input_tag("shipvia", "", style=style) %]</td>
      </tr>
      <tr>
       <th align="right">[% 'Project Number' | $T8 %]</th>
-      <td colspan="3">
-            [%- INCLUDE 'generic/multibox.html'
-                 name          =  'project_id',
-                 style         = "width: 250px",
-                 DATA          =  ALL_PROJECTS,
-                 id_key        = 'id',
-                 label_key     = 'projectnumber',
-                 limit         = vclimit,
-                 show_empty    = 1,
-                 allow_textbox = 0,
-            -%]
-      </td>
+      <td>[% P.project.picker("project_id", project_id, active="both", valid="both", style=style) %]</td>
      </tr>
-    [% IF SHOW_BUSINESS_TYPES %]
+    [% IF ALL_BUSINESS_TYPES.as_list.size > 0 %]
      <tr>
       <th align="right" nowrap>[% 'Customer type' | $T8 %]</th>
-      <td colspan="3">
-          [%- INCLUDE 'generic/multibox.html'
-                 name          =  'business_id',
-                 style         = "width: 250px",
-                 DATA          =  ALL_BUSINESS_TYPES,
-                 id_key        = 'id',
-                 label_key     = 'description',
-                 limit         = vclimit,
-                 show_empty    = 1,
-                 allow_textbox = 0,
-            -%]
-      </td>
+      <td>[% L.select_tag("business_id", ALL_BUSINESS_TYPES, with_empty=1, title_key="description", style=style) %]</td>
+     </tr>
+    [% END %]
+     <tr>
+      <th>[% 'Show only marked as paid invoices' | $T8 %]</th>
+      <td>[% L.checkbox_tag('show_marked_as_closed') %]</td>
+     </tr>
+    [% IF INSTANCE_CONF.get_email_journal %]
+     <tr>
+      <th>[% 'Show only not mailed invoices' | $T8 %]</th>
+      <td>[% L.checkbox_tag('show_not_mailed') %]</td>
      </tr>
     [% END %]
      <tr>
       <th align=right nowrap>[% 'Invoice Date' | $T8 %]</th>
       <td>
-       [% 'From' | $T8 %]
        [% L.date_tag('transdatefrom') %]
-      </td>
-     <th align=right>[% 'Bis' | $T8 %]</th>
-     <td>
-      [% L.date_tag('transdateto') %]
+       [% 'Bis' | $T8 %]
+       [% L.date_tag('transdateto') %]
      </td>
     </tr>
      <tr>
       <th align=right nowrap>[% 'Due Date' | $T8 %]</th>
       <td>
-       [% 'From' | $T8 %]
        [% L.date_tag('duedatefrom') %]
-      </td>
-     <th align=right>[% 'Bis' | $T8 %]</th>
-     <td>
-      [% L.date_tag('duedateto') %]
+       [% 'Bis' | $T8 %]
+       [% L.date_tag('duedateto') %]
      </td>
     </tr>
 
 [%- IF CT_CUSTOM_VARIABLES.size %]
     <tr>
       <td></td>
-      <td colspan=4 align=left><b>[% 'Custom variables for module' | $T8 %]: [%'Customers and vendors' | $T8 %]</td>
+      <td colspan=4 align=left><b>[% 'Custom variables for module' | $T8 %]: [%'Customers and vendors' | $T8 %]</b></td>
     </tr>
     [% CT_CUSTOM_VARIABLES_FILTER_CODE %]
 [%- END %]
 
-   <input type=hidden name=sort value=transdate>
    </table>
     </td>
     </tr>
            <td nowrap>[% 'Invoice Number' | $T8 %]</td>
            <td align=right><input name="l_ordnumber" id="l_ordnumber" class=checkbox type=checkbox value=Y></td>
            <td nowrap>[% 'Order Number' | $T8 %]</td>
-           <td align=right><input name="l_cusordnumber" id="l_cusordnumber" class=checkbox type=checkbox value=Y checked></td>
+           <td align=right><input name="l_cusordnumber" id="l_cusordnumber" class=checkbox type=checkbox value=Y></td>
            <td nowrap>[% 'Customer Order Number' | $T8 %]</td>
           </tr>
           <tr>
            <td nowrap>[% 'Customer' | $T8 %]</td>
            <td align=right><input name="l_customernumber" id="l_customernumber" class=checkbox type=checkbox value=Y></td>
            <td nowrap>[% 'Customer Number' | $T8 %]</td>
+           <td align=right><input name="l_department" id="l_department" class=checkbox type=checkbox value=Y></td>
+           <td nowrap>[% 'Department' | $T8 %]</td>
+          </tr>
+          <tr>
+           <td align=right><input name="l_donumber" id="l_donumber" class=checkbox type=checkbox value=Y></td>
+           <td nowrap>[% 'Delivery Order Number' | $T8 %]</td>
+           <td align=right><input name="l_deliverydate" id="l_deliverydate" class=checkbox type=checkbox value=Y></td>
+           <td nowrap>[% 'Delivery Date' | $T8 %]</td>
           </tr>
           <tr>
            <td align=right><input name="l_netamount" id="l_netamount" class=checkbox type=checkbox value="Y" checked></td>
            <td align=right><input name="l_payment_terms" id="l_payment_terms" class=checkbox type=checkbox value=Y></td>
            <td nowrap>[% 'Payment Terms' | $T8 %]</td>
            <td align=right><input name="l_charts" id="l_charts" class=checkbox type=checkbox value=Y></td>
-           <td nowrap>[% 'Buchungskonto' | $T8 %]</td>
+           <td nowrap>[% 'Chart' | $T8 %]</td>
            <td align=right><input name="l_direct_debit" id="l_direct_debit" class=checkbox type=checkbox value=Y></td>
            <td nowrap>[% 'direct debit' | $T8 %]</td>
           </tr>
+[% IF INSTANCE_CONF.get_doc_storage -%]
           <tr>
-           <td colspan=4 align=left><b>[% 'Customer' | $T8 %] </td>
+           <td align=right><input name="l_attachments" id="l_attachments" class=checkbox type=checkbox value=Y></td>
+           <td nowrap>[% 'Attachments' | $T8 %]</td>
+          </tr>
+[% END-%]
+          <tr>
+           <td colspan=4 align=left><b>[% 'Customer' | $T8 %]</b></td>
           </tr>
           <tr>
            <td align=right><input name="l_customernumber" id="l_customernumber" class=checkbox type=checkbox value=Y></td>
       </table>
      </td>
     </tr>
-    <tr>
-     <td><hr size=3 noshade></td>
-    </tr>
    </table>
-   <input type=hidden name=nextsub value=[% nextsub %]>
-   <br>
-   <input class=submit type=submit name=action id="continue" value="[% 'Continue' | $T8 %]">
   </form>
- <script type="text/javascript">
- <!--
-   $(document).ready(function(){
-    $('customer').focus();
-   })
- //-->
- </script>
diff --git a/templates/webpages/arap/select_project.html b/templates/webpages/arap/select_project.html
deleted file mode 100644 (file)
index ffd0b86..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-[%- USE HTML %]
-[%- USE T8 %]
-[%- USE L  %]
-[%- USE LxERP %]
-
-<h1>[% 'Select from one of the projects below' | $T8 %]</h1>
-
-<form method=post action="[% script %]">
-
-<table width=100%>
- <tr class=listheading>
-  <th>&nbsp;</th>
-  <th>[% 'Number' | $T8 %]</th>
-  <th>[% 'Description' | $T8 %]</th>
- </tr>
-[%- FOREACH row IN project_list %]
- <tr class=listrow[% loop.count % 2 %]>
-  <td>[% L.radio_button_tag('ndx', value=loop.count, checked=loop.first) %]</td>
-  <td>[% row.projectnumber | html %]</td>
-  <td>[% row.description | html %]</td>
-  [% L.hidden_tag('new_id_' _ loop.count, row.id) %]
-  [% L.hidden_tag('new_projectnumber_' _ loop.count, row.projectnumber) %]
- </tr>
-[%- END %]
-</table>
-
-<hr size=3 noshade>
-
-[% L.hidden_tag(row.key, row.value) FOREACH row = hiddens %]
-[% L.hidden_tag('lastndx', project_list.size) %]
-[% L.hidden_tag('nextsub', 'project_selected') %]
-[% L.hidden_tag('rownumber', rownumber) %]
-[% L.submit_tag('action', LxERP.t8('Continue')) %]
-
-</form>
-
index 81bf0e7..f7170ec 100644 (file)
@@ -4,7 +4,10 @@
 
 [%- INCLUDE 'common/flash.html' %]
 
- <form method="post" action="controller.pl">
+ <form method="post" action="controller.pl" id="form">
+  [% L.hidden_tag("id", SELF.background_job.id) %]
+  [% L.hidden_tag("back_to", SELF.back_to) %]
+
   <table>
    <tr>
     <th align="right">[%- LxERP.t8('Active') %]</th>
    </tr>
 
   </table>
-
-  <p>
-   [% L.hidden_tag("id", SELF.background_job.id) %]
-   [% L.hidden_tag("back_to", SELF.back_to) %]
-   [% L.hidden_tag("action", "BackgroundJob/dispatch") %]
-   [% L.submit_tag("action_" _  (SELF.background_job.id ? "update" : "create"), LxERP.t8('Save')) %]
-   [%- IF SELF.background_job.id %]
-    [% L.submit_tag("action_execute", LxERP.t8("Execute now")) %]
-    [% L.submit_tag("action_destroy", LxERP.t8("Delete"), "confirm", LxERP.t8("Are you sure you want to delete this background job?")) %]
-   [%- ELSE %]
-    [% L.submit_tag("action_save_and_execute", LxERP.t8("Save and execute")) %]
-   [%- END %]
-   <a href="[% SELF.url_for(action => 'list') %]">[%- LxERP.t8('Abort') %]</a>
-   [%- IF SELF.background_job.id %]
-    <a href="[% SELF.url_for(controller='BackgroundJobHistory', action='list', 'filter.package_name:substr::ilike'=SELF.background_job.package_name) %]">[%- LxERP.t8('Show history') %]</a>
-   [%- END %]
-  </p>
  </form>
index b3d53ea..f5668da 100644 (file)
@@ -4,7 +4,6 @@
 
 [%- INCLUDE 'common/flash.html' %]
 
- <form method="post" action="controller.pl">
   [% IF !BACKGROUND_JOBS.size %]
    <p>
     [%- LxERP.t8('No background job has been created yet.') %]
   [%- END %]
 
   [% L.paginate_controls %]
-
-  <hr size="3" noshade>
-
-  <p>
-   <a href="[% SELF.url_for(action => 'new', back_to => SELF.get_callback) %]">[%- LxERP.t8('Create new background job') %]</a>
-   |
-   <a href="[% SELF.url_for(controller => 'BackgroundJobHistory', action => 'list') %]">[%- LxERP.t8('View background job history') %]</a>
-   |
-   <a href="[% SELF.url_for(controller => 'TaskServer', action => 'show') %]">[%- LxERP.t8('Task server control') %]</a>
-  </p>
- </form>
index 862acbc..9c0c96e 100644 (file)
@@ -1,5 +1,5 @@
 [%- USE L %][%- USE LxERP %][%- USE HTML %]
-<form action="controller.pl" method="post">
+<form action="controller.pl" method="post" id="filter_form">
  <div class="filter_toggle">
   <a href="#" onClick="javascript:$('.filter_toggle').toggle()">[% LxERP.t8('Show Filter') %]</a>
   [% IF SELF.filter_summary %]([% LxERP.t8("Current filter") %]: [% SELF.filter_summary %])[% END %]
     <td>[% L.select_tag('filter.status:eq_ignore_empty', [ [ '', '' ], [ 'failure', LxERP.t8('failed') ], [ 'success', LxERP.t8('succeeded') ] ], default=filter.status_eq_ignore_empty) %]</td>
    </tr>
    <tr>
-    <th align="right">[% LxERP.t8('Run at') %] [% LxERP.t8('From Date') %]</th>
+    <th align="right">[% LxERP.t8('Execution date from') %]</th>
     <td>[% L.date_tag('filter.run_at:date::ge', filter.run_at_date__ge) %]</td>
    </tr>
    <tr>
-    <th align="right">[% LxERP.t8('Run at') %] [% LxERP.t8('To Date') %]</th>
+    <th align="right">[% LxERP.t8('Execution date to') %]</th>
     <td>[% L.date_tag('filter.run_at:date::le', filter.run_at_date__le) %]</td>
    </tr>
   </table>
@@ -40,7 +40,7 @@
   [% L.hidden_tag('page', FORM.page) %]
   [% L.submit_tag('action_list', LxERP.t8('Continue'))%]
 
-  <a href="#" onClick="javascript:$('#filter_table input,#filter_table select').val('');">[% LxERP.t8('Reset') %]</a>
+  [% L.button_tag("\$('#filter_form').resetForm();", LxERP.t8('Reset')) %]
 
  </div>
 
index 0296274..d83bb28 100644 (file)
 [%- END %]
 
 [% L.paginate_controls %]
-
-<hr size="3" noshade>
-
-<p>
- <a href="[% SELF.url_for(controller => 'BackgroundJob', action => 'list') %]">[%- LxERP.t8('View background jobs') %]</a>
- |
- <a href="[% SELF.url_for(controller => 'TaskServer', action => 'show') %]">[%- LxERP.t8('Task server control') %]</a>
-</p>
index 5e42d08..a2f3ab6 100644 (file)
@@ -45,7 +45,3 @@
    </tr>
   </tbody>
  </table>
-
- <p>
-  <a href="[% back_to %]">[%- LxERP.t8('Back') %]</a>
- </p>
diff --git a/templates/webpages/bank_import/import_mt940.html b/templates/webpages/bank_import/import_mt940.html
new file mode 100644 (file)
index 0000000..e2baa5f
--- /dev/null
@@ -0,0 +1,94 @@
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE T8 %]
+
+<h1>[% FORM.title %]</h1>
+
+[% IF preview %]
+  <form method="post" action="controller.pl" enctype="multipart/form-data" id="form">
+    [% L.hidden_tag('file_name', SELF.file_name) %]
+    [% L.hidden_tag('charset', SELF.charset) %]
+  </form>
+[% END %]
+
+<h2>[% LxERP.t8("Overview") %]</h2>
+
+<div>
+  <table>
+    <tr>
+      <td>[% LxERP.t8("Total number of entries") %]:</td>
+      <td align="right">[% SELF.statistics.total %]</td>
+    </tr>
+
+    <tr>
+      <td>[% LxERP.t8("Entries with errors") %]:</td>
+      <td align="right">[% SELF.statistics.errors %]</td>
+    </tr>
+
+    <tr>
+      <td>[% LxERP.t8("Already imported entries (duplicates)") %]:</td>
+      <td align="right">[% SELF.statistics.duplicates %]</td>
+    </tr>
+
+    <tr>
+[% IF preview %]
+      <td>[% LxERP.t8("Entries ready to import") %]:</td>
+      <td align="right">[% SELF.statistics.to_import %]</td>
+[% ELSE %]
+      <td>[% LxERP.t8("Imported entries") %]:</td>
+      <td align="right">[% SELF.statistics.imported %]</td>
+[% END %]
+    </tr>
+  </table>
+</div>
+
+[% IF SELF.statistics.total %]
+
+<h2>[% LxERP.t8("Transactions") %]</h2>
+
+<table>
+  <thead>
+    <tr class="listheading">
+      <th>[% LxERP.t8("Transaction date") %]</th>
+      <th>[% LxERP.t8("Valutadate") %]</th>
+      <th>[% LxERP.t8("Other party") %]</th>
+      <th>[% LxERP.t8("Purpose") %]</th>
+      <th>[% LxERP.t8("Amount") %]</th>
+      <th>[% LxERP.t8("Remote account") %]</th>
+      <th>[% LxERP.t8("Local account") %]</th>
+      <th>[% LxERP.t8("Information") %]</th>
+    </tr>
+  </thead>
+
+  <tbody>
+    [% FOREACH transaction = SELF.transactions %]
+      <tr class="listrow[% IF transaction.error %]_error[% END %]">
+        <td align="right">[% transaction.transdate.to_kivitendo %]</td>
+        <td align="right">[% transaction.valutadate.to_kivitendo %]</td>
+        <td>[% HTML.escape(transaction.remote_name) %]</td>
+        <td>[% HTML.escape(transaction.purpose) %]</td>
+        <td align="right">[% LxERP.format_amount(transaction.amount, 2) %]</td>
+        <td>
+          [% IF transaction.remote_bank_code && transaction.remote_account_number %]
+            [% HTML.escape(transaction.remote_bank_code) %] / [% HTML.escape(transaction.remote_account_number) %]
+          [% END %]
+        </td>
+        <td>[% HTML.escape(transaction.bank_account.name) %]</td>
+        <td>
+          [% IF transaction.error %]
+            [% HTML.escape(transaction.error) %]
+          [% ELSIF transaction.duplicate %]
+            [% LxERP.t8("Duplicate") %]
+          [% ELSIF preview %]
+            [% LxERP.t8("To import") %]
+          [% ELSE %]
+            [% LxERP.t8("Imported") %]
+          [% END %]
+        </td>
+      </tr>
+    [% END %]
+  </tbody>
+</table>
+
+[% END %]
diff --git a/templates/webpages/bank_import/upload_mt940.html b/templates/webpages/bank_import/upload_mt940.html
new file mode 100644 (file)
index 0000000..7d3233f
--- /dev/null
@@ -0,0 +1,22 @@
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE T8 %]
+
+[%- INCLUDE 'common/flash.html' %]
+
+<h1>[% FORM.title %]</h1>
+
+<form method="post" action="controller.pl" enctype="multipart/form-data" id="form">
+  <table>
+    <tr>
+      <td>[% LxERP.t8("Charset") %]:</td>
+      <td>[% L.select_tag('charset', [ [ 'ISO-8859-15', 'ISO-8859-15 (Latin 1)' ], [ 'UTF-8', 'UTF-8' ], [ 'Windows-1252', 'Windows-1252' ] ], default='UTF-8') %]</td>
+    </tr>
+
+    <tr>
+      <td>[% LxERP.t8("MT940 file") %]:</td>
+      <td>[% L.input_tag('file', '', type => 'file', accept => '*') %]</td>
+    </tr>
+  </table>
+</form>
index 0aaab14..662554f 100644 (file)
@@ -2,7 +2,7 @@
 [%- USE L %]
 [%- USE LxERP %]
 [%- USE HTML %]
-<form action='controller.pl' method='post'>
+<form action='controller.pl' method='post' id='filter_form'>
 <div class='filter_toggle'>
 <a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Show Filter' | $T8 %]</a>
   [% SELF.filter_summary | html %]
     </tr>
  </table>
 
-[% L.hidden_tag('action', 'BankTransaction/dispatch') %]
 [% L.hidden_tag('sort_by', FORM.sort_by) %]
 [% L.hidden_tag('sort_dir', FORM.sort_dir) %]
 [% L.hidden_tag('page', FORM.page) %]
-[% L.input_tag('action_list_all', LxERP.t8('Continue'), type = 'submit', class='submit')%]
-
-
-<a href='#' onClick='javascript:$("#filter_table input").val("");$("#filter_table input[type=checkbox]").prop("checked", 0);'>[% 'Reset' | $T8 %]</a>
 
+[% L.button_tag('$("#filter_form").resetForm()', LxERP.t8('Reset')) %]
 </div>
 
 </form>
diff --git a/templates/webpages/bank_transactions/_payment_suggestion.html b/templates/webpages/bank_transactions/_payment_suggestion.html
new file mode 100644 (file)
index 0000000..ebc252b
--- /dev/null
@@ -0,0 +1,16 @@
+[%- USE P -%][%- USE HTML -%][%- USE LxERP -%]
+[% SELECT_OPTIONS = invoice.get_payment_select_options_for_bank_transaction(bt_id) %]
+[% is_skonto_pt   = SELECT_OPTIONS.1.selected %]
+[% formatted_skonto_amount          = LxERP.format_amount(invoice.skonto_amount, 2) %]
+[% formatted_skonto_amount_selected = is_skonto_pt ? formatted_skonto_amount : LxERP.format_amount(0, 2) %]
+<span id="[% HTML.escape(bt_id) %].[% HTML.escape(invoice.id) %]"
+  data-invoice-amount="[% HTML.escape(invoice.open_amount * 1) %]"
+  data-invoice-amount-less-skonto="[% HTML.escape(invoice.amount_less_skonto * 1) %]">
+ [% P.hidden_tag("invoice_ids." _ bt_id _ "[]", invoice.id) %]
+ [% P.hidden_tag("skonto_pt." _ bt_id _ "." _ invoice.id _ "", is_skonto_pt) %]
+ [% LxERP.t8("Invno.") %]: [% HTML.escape(invoice.invnumber) %]</br>
+ [% LxERP.t8("Open amount") %]: [% LxERP.format_amount(invoice.open_amount, 2) %]</br>
+ [% P.select_tag("invoice_skontos." _ bt_id _ "[]", SELECT_OPTIONS, value_key="payment_type", title_key="display", onChange="kivi.BankTransaction.update_skonto(this, " _ bt_id _ ", " _ invoice.id _ ", '$formatted_skonto_amount')"  ) %]</br>
+ [% LxERP.t8("Skonto amount") %]: [% P.input_tag("free_skonto_amount." _ bt_id _ "." _ invoice.id _ "", "$formatted_skonto_amount_selected", default=0, style=style, disabled=1, size=4, class='numeric', onblur="kivi.BankTransaction.update_invoice_amount(" _ bt_id _ ", 0, this)") %]
+ [% P.link_tag("#", "x", onclick="kivi.BankTransaction.delete_invoice(" _ bt_id _ "," _ invoice.id _ ")") %]
+</span>
diff --git a/templates/webpages/bank_transactions/_problems.html b/templates/webpages/bank_transactions/_problems.html
new file mode 100644 (file)
index 0000000..e2796d5
--- /dev/null
@@ -0,0 +1,46 @@
+[%- USE LxERP -%][%- USE T8 -%][%- USE HTML -%][%- USE P -%]
+<h3>
+ [% LxERP.t8("Warnings and errors") %]
+</h3>
+
+<p>
+ [% LxERP.t8("Bank transactions with errors have not been posted.") %]
+ [% LxERP.t8("Bank transactions that either only have warnings or no message at all have been posted.") %]
+</p>
+
+<table>
+ <thead>
+  <tr class="listheading">
+   <th>[% LxERP.t8("Type") %]</th>
+   <th>[% LxERP.t8("Invoices") %]</th>
+   <th>[% LxERP.t8("Transdate") %]</th>
+   <th>[% LxERP.t8("Amount") %]</th>
+   <th>[% LxERP.t8("Remote name") %]</th>
+   <th>[% LxERP.t8("Purpose") %]</th>
+   <th>[% LxERP.t8("Remote account number") %]</th>
+   <th>[% LxERP.t8("Remote bank code") %]</th>
+   <th>[% LxERP.t8("Message") %]</th>
+  </tr>
+ </thead>
+
+ <tbody>
+  [% FOREACH problem = SELF.problems %]
+   <tr class="listrow[% IF problem.result == 'error' %]_error[% END %]">
+    <td>[% IF problem.result == 'error' %][% LxERP.t8("Error") %][% ELSE %][% LxERP.t8("Warning") %][% END %]</td>
+    <td>
+     [% FOREACH invoice = problem.invoices %]
+      [% invoice.presenter.invoice %]
+      [% UNLESS loop.last %]<br>[% END %]
+     [% END %]
+    </td>
+    <td>[% HTML.escape(problem.bank_transaction.transdate.to_kivitendo) %]</td>
+    <td>[% HTML.escape(LxERP.format_amount(problem.bank_transaction.amount, 2)) %]</td>
+    <td>[% HTML.escape(problem.bank_transaction.remote_name) %]</td>
+    <td>[% HTML.escape(problem.bank_transaction.purpose) %]</td>
+    <td>[% HTML.escape(problem.bank_transaction.remote_account_number) %]</td>
+    <td>[% HTML.escape(problem.bank_transaction.remote_bank_code) %]</td>
+    <td>[% HTML.escape(problem.message) %]</td>
+   </tr>
+  [% END %]
+ </tbody>
+</table>
diff --git a/templates/webpages/bank_transactions/_template_list.html b/templates/webpages/bank_transactions/_template_list.html
new file mode 100644 (file)
index 0000000..245f062
--- /dev/null
@@ -0,0 +1,55 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE P -%]
+
+[% IF TEMPLATES_AP.size %]
+ [% LxERP.t8('AP template suggestions') %]:
+ <table>
+  <thead>
+   <tr>
+    <th class="listheading">[% LxERP.t8('Description') %]</th>
+    <th class="listheading">[% LxERP.t8('Vendor') %]</th>
+    <th class="listheading">[% LxERP.t8('Employee') %]</th>
+    <th class="listheading">[% LxERP.t8('Template date') %]</th>
+   </tr>
+  </thead>
+
+  <tbody>
+   [% FOREACH template = TEMPLATES_AP %]
+    <tr class="listrow">
+     <td>[% P.link_tag(SELF.load_ap_record_template_url(template), template.template_name) %]</td>
+     <td>[% HTML.escape(template.vendor.name) %]</td>
+     <td>[% HTML.escape(template.employee.name || template.employee.login) %]</td>
+     <td>[% HTML.escape(template.itime_as_date) %]</td>
+    </tr>
+   [% END %]
+  </tbody>
+ </table>
+[% ELSE %]
+ <p class="message_hint">[% LxERP.t8('No AP template was found.') %]</p>
+[% END %]
+
+[% IF TEMPLATES_GL.size %]
+ [% LxERP.t8('GL template suggestions') %]:
+ <table>
+  <thead>
+   <tr>
+    <th class="listheading">[% LxERP.t8('Description') %]</th>
+    <th class="listheading">[% LxERP.t8('Reference') %]</th>
+    <th class="listheading">[% LxERP.t8('Employee') %]</th>
+    <th class="listheading">[% LxERP.t8('Template date') %]</th>
+   </tr>
+  </thead>
+
+  <tbody>
+   [% FOREACH template = TEMPLATES_GL %]
+    <tr class="listrow">
+     <td>[% P.link_tag(SELF.load_gl_record_template_url(template), template.template_name) %]</td>
+     <td>[% HTML.escape(template.reference) %]</td>
+     <td>[% HTML.escape(template.employee.name || template.employee.login) %]</td>
+     <td>[% HTML.escape(template.itime_as_date) %]</td>
+    </tr>
+   [% END %]
+  </tbody>
+ </table>
+[% ELSE %]
+ <p class="message_hint">[% LxERP.t8('No GL template was found.') %]</p>
+[% END %]
index ad48bfa..6a13b9c 100644 (file)
@@ -9,9 +9,9 @@
     <th>[%- LxERP.t8("Amount") %]</th>
     <th>[%- LxERP.t8("Open amount") %]</th>
     <th>[%- LxERP.t8("Amount less skonto") %]</th>
-    <th>[%- LxERP.t8("Transdate") %]</th>
-    <th>[%- LxERP.t8("Customer/Vendor number") %]</th>
-    <th>[%- LxERP.t8("Customer/Vendor name") %]</th>
+    <th>[%- LxERP.t8("Invoice Date") %]</th>
+    <th>[%- LxERP.t8("Customer/Vendor Number") %]</th>
+    <th>[%- LxERP.t8("Customer/Vendor Name") %]</th>
    </tr>
 
   [%- FOREACH invoice = INVOICES %]
@@ -19,7 +19,7 @@
     <td>[% L.checkbox_tag('invoice_id[]', value=invoice.id) %]</td>
     <td>[%- invoice.invnumber %]</td>
     <td align="right">[%- LxERP.format_amount(invoice.amount, 2) %]</td>
-    <td align="right">[%- LxERP.format_amount(invoice.amount - invoice.paid, 2) %]</td>
+    <td align="right">[%- LxERP.format_amount(invoice.open_amount, 2) %]</td>
     <td align="right">[%- LxERP.format_amount(invoice.amount_less_skonto, 2) %]</td>
     <td align="right">[%- invoice.transdate_as_date %]</td>
     <td>[%- invoice.vendor.vendornumber %][%- invoice.customer.customernumber %]</td>
index 26229b8..1df0af2 100644 (file)
@@ -2,7 +2,7 @@
 
 [% SET debug = 0 %]
 
-<form method="post" action="javascript:filter_invoices();">
+<form method="post" action="javascript:kivi.BankTransaction.filter_invoices();" id="assign_invoice_window_form" data-bank-transaction-id="[% HTML.escape(SELF.transaction.id) %]">
   <b>[%- LxERP.t8("Bank transaction") %]:</b>
   <table>
    <tr class="listheading">
@@ -32,7 +32,7 @@
     <th align="right">[%- LxERP.t8("Invoice number") %]</th>
     <td>[% L.input_tag('invnumber', '', style=style) %]</td>
 
-    <th align="right">[%- LxERP.t8("Customer/Vendor name") %]</th>
+    <th align="right">[%- LxERP.t8("Customer/Vendor Name") %]</th>
     <td>[% L.input_tag('vcname', '', style=style) %]</td>
    </tr>
 
@@ -40,7 +40,7 @@
     <th align="right">[%- LxERP.t8("Amount") %]</th>
     <td>[% L.input_tag('amount', '', style=style) %]</td>
 
-    <th align="right">[%- LxERP.t8("Customer/Vendor number") %]</th>
+    <th align="right">[%- LxERP.t8("Customer/Vendor Number") %]</th>
     <td>[% L.input_tag('vcnumber', '', style=style) %]</td>
    </tr>
 
@@ -55,8 +55,8 @@
 
   <p>
    [% L.submit_tag('', LxERP.t8("Search")) %]
-   [% L.button_tag('add_selected_invoices()', LxERP.t8("Add invoices"), id='add_selected_record_links_button') %]
-   <a href="#" onclick="assign_invoice_reset_form();">[%- LxERP.t8("Reset") %]</a>
+   [% L.button_tag('kivi.BankTransaction.add_selected_invoices()', LxERP.t8("Add invoices"), id='add_selected_record_links_button') %]
+   [% L.button_tag('$("#assign_invoice_window_form").resetForm()', LxERP.t8('Reset')) %]
    <a href="#" onclick="$('#assign_invoice_window').dialog('close');">[% LxERP.t8("Cancel") %]</a>
   </p>
 
 
 <script type="text/javascript">
 <!--
-
-function filter_invoices() {
-  var url="controller.pl?action=BankTransaction/ajax_add_list&" + $("#assign_invoice_window form").serialize();
-  $.ajax({
-    url: url,
-    success: function(new_data) {
-      $("#record_list_filtered_list").html(new_data['html']);
-    }
-  });
-}
-
-function add_selected_invoices() {
-  var url="controller.pl?action=BankTransaction/ajax_accept_invoices&" + 'bt_id=[% SELF.transaction.id %]&' + $("#assign_invoice_window form").serialize();
-  $.ajax({
-    url: url,
-    success: function(new_html) {
-      var invoices = document.getElementById('assigned_invoices_[% SELF.transaction.id %]');
-      if (invoices.innerHTML == '') {
-        invoices.innerHTML = new_html;
-      } else {
-        invoices.innerHTML += '<br />' + new_html;
-      }
-      $('#assign_invoice_window').dialog('close');
-    }
-  });
-}
-
-function assign_invoice_reset_form() {
-  $('#assign_invoice_window form input[type=text]').val('');
-}
+$(function() {
+  $('#invnumber').focus();
+});
 
 //-->
 </script>
-
index cfc9ce8..2af7755 100644 (file)
@@ -1,6 +1,6 @@
-[%- USE HTML %][%- USE L %][%- USE LxERP %][%- USE T8 %]
+[%- USE HTML %][%- USE L %][%- USE LxERP %][%- USE P -%]
 
-  <b>Transaction</b>
+  <b>[% LxERP.t8("Bank transaction") %]</b>
   <table>
    <tr class="listheading">
     <td>[%- LxERP.t8("ID") %]:</td>
 
 
 <br>
-[% 'Vendor filter for AP transaction drafts' | $T8 %]:
-
-<form method="post" action="javascript:filter_drafts();">
-[% L.hidden_tag('bt_id', SELF.transaction.id) %]
-  <table>
-   <tr>
-    <th align="right">[%- LxERP.t8("Vendor") %]</th>
-    <td>
-            [%- INCLUDE 'generic/multibox.html'
-                 name          = 'vendor',
-                 select_name   = 'vendor_id',
-                 default       = ALL_VENDORS.size < limit ? vendor_id : vendor_name,
-                 style         = 'width: 250px',
-                 DATA          = ALL_VENDORS,
-                 id_key        = 'id',
-                 label_key     = 'name',
-                 limit         = limit,
-                 show_empty    = 1,
-                 allow_textbox = 1,
-                 class         = 'initial_focus',
-                 onChange      = 'filter_drafts();',
-                 -%]
-    </td>
-   </tr>
-  </table>
-</form>
 
+<form method="post" action="javascript:kivi.BankTransaction.filter_templates()" id="create_invoice_window_form">
+ [% L.hidden_tag("bt_id",               SELF.transaction.id) %]
+ [% L.hidden_tag("filter.bank_account", FORM.filter.bank_account) %]
+ [% L.hidden_tag("filter.fromdate",     FORM.filter.fromdate) %]
+ [% L.hidden_tag("filter.todate",      FORM.filter.todate) %]
+ <table>
+  <tr>
+   <th align="right">[%- LxERP.t8("Template Description") %]</th>
+   <td>[% P.input_tag("template", template_name, style="width: 250px") %]</td>
+  </tr>
+  <tr>
+   <th align="right">[%- LxERP.t8("Vendor") %]</th>
+   <td>[% P.input_tag("vendor", vendor_name,  style="width: 250px") %]</td>
+  </tr>
+  <tr>
+   <th align="right">[%- LxERP.t8("Reference") %]</th>
+   <td>[% P.input_tag("reference", reference_name, style="width: 250px") %]</td>
+  </tr>
+ </table>
   <p>
+   [% P.submit_tag('', LxERP.t8("Filter")) %]
+   [% P.button_tag('$("#create_invoice_window_form").resetForm()', LxERP.t8('Reset')) %]
    <a href="#" onclick="$('#create_invoice_window').dialog('close');">[% LxERP.t8("Cancel") %]</a>
   </p>
-
+</form>
   <hr>
-<div id="drafts">
-[% IF DRAFTS.size %]
-[% 'Draft suggestions' | $T8 %]:
-
-
-  <table>
-   <tr>
-    <th class="listheading">[% 'Description' | $T8 %]</th>
-    <th class="listheading">[% 'Vendor' | $T8 %]</th>
-    <th class="listheading">[% 'Employee' | $T8 %]</th>
-    <th class="listheading">[% 'Draft from:' | $T8 %]</th>
-   </tr>
-
-   [% FOREACH draft = DRAFTS %]
-    <tr class="listrow[% loop.count % 2 %]">
-     <td><a href="[% draft.module %].pl?action=load_draft&id=[% HTML.url(draft.id) %]&amount_1=[% LxERP.format_amount(-1 * SELF.transaction.amount, 2) %]&transdate=[% HTML.url(SELF.transaction.transdate_as_date) %]&duedate=[% HTML.url(SELF.transaction.transdate_as_date) %]&datepaid_1=[% HTML.url(SELF.transaction.transdate_as_date) %]&paid_1=[% LxERP.format_amount(-1 * SELF.transaction.amount, 2) %]&currency=[% HTML.url(SELF.transaction.currency.name) %]&AP_paid_1=[% HTML.url(SELF.transaction.local_bank_account.chart.accno) %]&remove_draft=0&callback=[% HTML.url(callback) %]">[% HTML.escape(draft.description) %]</a></td>
-     <td>[% HTML.escape(draft.vendor) %]</td>
-     <td>[% HTML.escape(draft.employee.name) %]</td>
-     <td>[% HTML.escape(draft.itime_as_date) %]</td>
-    </tr>
-   [% END %]
-  </table>
-[% ELSE %]
-  <p class="message_hint">[% 'No draft was found.' | $T8 %]</p>
-[% END %]
+<div id="templates">
+ [% PROCESS "bank_transactions/_template_list.html" %]
 </div>
 
 <script type="text/javascript">
 <!--
+$(function() {
+  $('#template').focus();
+});
 
-function filter_drafts() {
-  var url="controller.pl?action=BankTransaction/filter_drafts&" + $("#create_invoice_window form").serialize();
-  $.ajax({
-    url: url,
-    success: function(new_data) {
-      $("#drafts").html(new_data['html']);
-    }
-  });
-}
 //-->
 </script>
-
diff --git a/templates/webpages/bank_transactions/filter_drafts.html b/templates/webpages/bank_transactions/filter_drafts.html
deleted file mode 100644 (file)
index 7b2c8de..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-[%- USE T8 -%][%- USE HTML -%][%- USE LxERP -%][%- USE P -%][%- USE L -%]
-[%- IF !DRAFTS.size %]
-  <p class="message_hint">[% 'No draft was found.' | $T8 %]</p>
-[%- ELSE %]
-  <table>
-   <tr>
-    <th class="listheading">[% 'Date' | $T8 %]</th>
-    <th class="listheading">[% 'Description' | $T8 %]</th>
-    <th class="listheading">[% 'Employee' | $T8 %]</th>
-    <th class="listheading">[% 'Vendor' | $T8 %]</th>
-   </tr>
-
-   [% FOREACH draft = DRAFTS %]
-    <tr class="listrow[% loop.count % 2 %]">
-     <td>[% HTML.escape(draft.itime_as_date) %]</td>
-     <td><a href="[% draft.module %].pl?action=load_draft&id=[% HTML.url(draft.id) %]&amount_1=[% SELF.transaction.amount_as_number %]&datepaid_1=[% HTML.url(SELF.transaction.transdate_as_date) %]&paid_1=[% SELF.transaction.amount_as_number %]&remove_draft=0&callback=[% HTML.url(callback) %]">[% HTML.escape(draft.description) %]</a></td>
-     <td>[% HTML.escape(draft.employee.name) %]</td>
-     <td>[% HTML.escape(draft.vendor) %]</td>
-    </tr>
-   [% END %]
-  </table>
-[%- END %]
-
index 6ba716c..e5976c4 100644 (file)
@@ -1,8 +1,27 @@
-[% USE L %]
+[%- USE HTML -%][%- USE LxERP -%][%- USE P -%]
+[% SET debug=1 %]
 [% FOREACH invoice = INVOICES %]
-  <div id="[% bt_id %].[% invoice.id %]">
-    [% L.hidden_tag('invoice_ids.' _ bt_id _'[]', invoice.id) %]
-    [% invoice.invnumber %]
-    <a href=# onclick="delete_invoice([% bt_id %], [% invoice.id %])">x</a>
-  </div>
+ <tr id="extra_row_[% HTML.escape(bt_id) %]_[% HTML.escape(invoice.id) %]">
+  <td></td>
+  <td></td>
+  <td id="assigned_invoices_[% bt.id %]_[% invoice.id %]">
+    [% PROCESS "bank_transactions/_payment_suggestion.html" %]
+  </td>
+  <td>[% P.input_tag("sources_" _ bt_id _ "_" _ invoice.id, "") %]</td>
+  <td>[% P.input_tag("memos_" _ bt_id _ "_" _ invoice.id, "") %]</td>
+  [% IF debug %]
+  <td></td>
+  [% END %]
+  <td></td>
+  <td></td>
+  <td></td>
+  <td></td>
+  <td></td>
+  <td></td>
+  <td></td>
+  <td></td>
+  <td></td>
+  <td></td>
+  <td></td>
+ </tr>
 [% END %]
index af6ef22..2828d54 100644 (file)
 
 [%- INCLUDE 'common/flash.html' %]
 
+[% IF SELF.problems.size %]
+ [% INCLUDE 'bank_transactions/_problems.html' %]
+[% END %]
+
 <p>[% HTML.escape(bank_account.name) %] [% HTML.escape(bank_account.iban) %], [% 'Bank code' | $T8 %] [% HTML.escape(bank_account.bank_code) %], [% 'Bank' | $T8 %] [% HTML.escape(bank_account.bank) %]</p>
 <p>
 [% IF FORM.filter.fromdate %] [% 'From' | $T8 %] [% FORM.filter.fromdate %] [% END %]
 [% IF FORM.filter.todate %]   [% 'to (date)' | $T8 %] [% FORM.filter.todate %][% END %]
+[% L.hidden_tag("filter.bank_account", FORM.filter.bank_account) %]
+[% L.hidden_tag("filter.fromdate",     FORM.filter.fromdate) %]
+[% L.hidden_tag("filter.todate",       FORM.filter.todate) %]
 </p>
 
-<form method="post" id="list_form">
-[% L.hidden_tag('filter.bank_account', FORM.filter.bank_account) %]
-[% L.hidden_tag('filter.fromdate', FORM.filter.fromdate) %]
-[% L.hidden_tag('filter.todate',   FORM.filter.todate) %]
-
-<div class="tabwidget">
+<div id="bt_tabs" class="tabwidget">
   <ul>
-    <li><a href="#all" onclick="show_invoice_button();">[% 'All transactions' | $T8 %]</a></li>
-    <li><a href="#automatic" onclick="show_proposal_button();">[% 'Proposals' | $T8 %]</a></li>
+    <li><a href="#all">[% 'All transactions' | $T8 %]</a></li>
+    <li><a href="#automatic">[% 'Proposals' | $T8 %]</a></li>
   </ul>
 
   <div id="all">[% PROCESS "bank_transactions/tabs/all.html" %]</div>
   <div id="automatic">[% PROCESS "bank_transactions/tabs/automatic.html" %]</div>
 </div>
 
-[% L.hidden_tag('action', 'BankTransaction/dispatch') %]
-[% L.submit_tag('action_save_invoices', LxERP.t8('Save invoices')) %]
-[% L.submit_tag('action_save_proposals', LxERP.t8('Save proposals'), style='display: none') %]
-
-</form>
+<div id="set_all_sources_memos_dialog" class="hidden">
+ <table>
+  <tr>
+   <th>[% LxERP.t8("Source") %]:</th>
+   <td>[% L.input_tag("set_all_sources", "") %]</td>
+  </tr>
+
+  <tr>
+   <th>[% LxERP.t8("Memo") %]:</th>
+   <td>[% L.input_tag("set_all_memos", "") %]</td>
+  </tr>
+ </table>
+
+ <p>
+  [% L.button_tag("kivi.BankTransaction.set_all_sources_memos()", LxERP.t8("Set fields")) %]
+  <a href="#" onclick="$('#set_all_sources_memos_dialog').dialog('close');">[%- LxERP.t8("Cancel") %]</a>
+ </p>
+</div>
 
 <script type="text/javascript">
 <!--
-
-$(function() {
-  $('#check_all').checkall('INPUT[name^="proposal_ids"]');
-});
-
 $(function() {
-  $('.sort_link').each(function() {
-    var _href = $(this).attr("href");
-    $(this).attr("href", _href + "&filter.fromdate=" + "[% FORM.filter.fromdate %]" + "&filter.todate=" + "[% FORM.filter.todate %]");
-  });
+  kivi.BankTransaction.init_list([% ui_tab %]);
 });
-
-function show_invoice_button () {
-  $("#action_save_proposals").hide();
-  $("#action_save_invoices").show();
-}
-
-function show_proposal_button () {
-  $("#action_save_invoices").hide();
-  $("#action_save_proposals").show();
-}
-
-function assign_invoice(bt_id) {
-  kivi.popup_dialog({
-    url:    'controller.pl?action=BankTransaction/assign_invoice',
-    data:   '&bt_id=' + bt_id,
-    type:   'POST',
-    id:     'assign_invoice_window',
-    dialog: { title: kivi.t8('Assign invoice') }
-  });
-  return true;
-}
-
-function add_invoices(bt_id, prop_id, prop_invnumber) {
-  // prop_id is a proposed invoice_id
-  // remove the added invoice from all the other suggestions
-  var number_of_elements = document.getElementsByName(prop_id).length;
-  for( var i = 0; i < number_of_elements; i++ ) {
-    var node = document.getElementsByName(prop_id)[0];
-    node.parentNode.removeChild(node);
-  }
-  var invoices = document.getElementById('assigned_invoices_' + bt_id);
-
-  $.ajax({
-    url: 'controller.pl?action=BankTransaction/ajax_payment_suggestion&bt_id=' + bt_id  + '&prop_id=' + prop_id,
-    success: function(data) {
-      invoices.innerHTML += data.html;
-    }
-  });
-}
-
-function delete_invoice(bt_id, prop_id) {
-  $( "#" + bt_id + "\\." + prop_id ).remove();
-}
-
-function create_invoice(bt_id) {
-  kivi.popup_dialog({
-    url:    'controller.pl?action=BankTransaction/create_invoice',
-    data:   '&bt_id=' + bt_id + "&filter.bank_account=[% FORM.filter.bank_account %]&filter.todate=[% FORM.filter.todate %]&filter.fromdate=[% FORM.filter.fromdate %]",
-    type:   'POST',
-    id:     'create_invoice_window',
-    dialog: { title: kivi.t8('Create invoice') }
-  });
-  return true;
-}
-
 //-->
 </script>
index 8b98dcb..bf137b8 100644 (file)
@@ -1,3 +1,4 @@
 [%- USE L %]
 [%- PROCESS 'bank_transactions/_filter.html' filter=SELF.models.filtered.laundered %]
  <hr>
+<form method="post" action="controller.pl" id="form">
index 3e234cd..65fdf7f 100644 (file)
@@ -5,11 +5,13 @@
 
 [%- INCLUDE 'common/flash.html' %]
 
- <form method="post" action="controller.pl">
+ <form method="post" action="controller.pl" id="search_form">
 
   <div class="listtop">[% 'Search bank transactions' | $T8 %]</div>
-
   <p>
+    [%- IF INSTANCE_CONF.get_payments_changeable != '0' -%]
+      [% 'Cannot safely book imported bank transactions due to lax posting settings for payments' | $T8 %]
+    [%- ELSE -%]
    <table>
 
     <tr>
      <td>[% L.date_tag('filter.todate', filter.todate) %]</td>
     </tr>
    </table>
+  [%- END -%]
   </p>
-
-  <hr size="3" noshade>
-
-  [% L.hidden_tag('action', 'BankTransaction/list') %]
-
-  <p>[% L.submit_tag('dummy', LxERP.t8('Continue')) %]</p>
  </form>
index 0b1d614..eae9008 100644 (file)
@@ -1,13 +1,29 @@
-[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%][%- USE P -%]
 
-[% SET debug=0 %]
+[% BLOCK proposal_div %]
+        <div data-proposal-id="[% prop.id %]">
+         <a href=# onclick="kivi.BankTransaction.add_invoices('[% bt.id %]', '[% prop.id %]');"
+            title="<table><tr><th></th><th>[% 'Suggested invoice' | $T8 %][% IF !prop.is_sales %] ([% 'AP' | $T8 %])[% END %]</th><th>[% 'Bank transaction' | $T8 %]</th></tr><tr><th>[% 'Amount' | $T8 %]</th><td>[% prop.realamount %] ([% 'open' | $T8 %]: [% LxERP.format_amount(prop.open_amount, 2) %])</td><td>[% LxERP.format_amount(bt.amount, 2) %]</td></tr>[% IF prop.skonto_date %]<tr><th>[% 'Payment terms' | $T8 %]</th><td>[% LxERP.format_amount(prop.amount_less_skonto, 2) %] [% 'until' | $T8 %] [% HTML.escape(prop.skonto_date.to_kivitendo) %] ([% prop.percent_skonto * 100 %] %)</td><td></td></tr>[% END %]<tr><th>[% 'Customer/Vendor' | $T8 %]</th><td>[% HTML.escape(prop.customer.displayable_name) %][% HTML.escape(prop.vendor.displayable_name) %]</td><td>[% HTML.escape(bt.remote_name) %]</td></tr><tr><th>[% 'Invoice Date' | $T8 %]</th><td>[% HTML.escape(prop.transdate_as_date) %]</td><td>[% HTML.escape(bt.transdate_as_date) %] ([% HTML.escape(bt.transdate.utc_rd_days - prop.transdate.utc_rd_days) %])</td></tr><tr><th>[% 'Invoice Number' | $T8 %]</th><td>[% HTML.escape(prop.invnumber) %]</td><td>[% HTML.escape(bt.purpose) %]</td></tr></table>"
+              class="[% IF bt.agreement >= 5 %]green[% ELSIF bt.agreement < 5 and bt.agreement >= 3 %]orange[% ELSE %]red[% END %] tooltipster-html">&larr;[% HTML.escape(prop.invnumber)%]</a>
+        </div>
+[% END %]
+
+[% SET debug=1 %]
+<form method="post" id="list_all_form">
+[% L.hidden_tag('filter.bank_account', FORM.filter.bank_account) %]
+[% L.hidden_tag('filter.fromdate', FORM.filter.fromdate) %]
+[% L.hidden_tag('filter.todate',   FORM.filter.todate) %]
+[% L.hidden_tag('action', 'BankTransaction/dispatch') %]
+[% L.hidden_tag('ui_tab', ui_tab) %]
 
  <table id="bt_list">
   <thead>
    <tr class="listheading">
     <th></th>
     <th></th>
-    <th>[% 'Assigned invoices' | $T8 %]</th>
+    <th>[% LxERP.t8("Assigned invoices with amount") %]</th>
+    <th style="width: 100px">[% LxERP.t8("Source") %]</th>
+    <th style="width: 100px">[% LxERP.t8("Memo") %]</th>
     [% IF debug %]
     <th>[% 'Score' | $T8 %]</th>
     [% END %]
@@ -45,6 +61,7 @@
         [% END %]
     </th>
     <th>[% 'Purpose' | $T8 %]</th>
+    <th>[% 'Type' | $T8 %]</th>
     <th>[% IF FORM.sort_by == 'remote_account_number'%]
           <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=remote_account_number&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
             [% 'Remote account number' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
    </tr>
   </thead>
 
-  <tbody>
-   [%- FOREACH bt = BANK_TRANSACTIONS %]
-    <tr class="listrow" id="bt_id_[% bt.id %]">
-     <td><a href=# onclick="assign_invoice('[% bt.id %]'); return false;">[% 'Assign invoice' | $T8 %]</a></td>
-     <td><a href=# onclick="create_invoice('[% bt.id %]'); return false;">[% 'Create invoice' | $T8 %]</a></td>
-     <td id="assigned_invoices_[% bt.id %]" nowrap></td>
-     [% IF debug %]
-     <td class="tooltipster-html" title="[% FOREACH match = bt.rule_matches %] [% match %]<br> [% END %]">[% bt.agreement %]</td>
-     [% END %]
-     <td>
-      [% FOREACH prop = bt.proposals %]
-        <div name='[% prop.id %]'>
-         <a href=# onclick="add_invoices('[% bt.id %]', '[% prop.id %]', '[% HTML.escape(prop.invnumber) %]');"
-            title="<table><tr><th></th><th>[% 'Suggested invoice' | $T8 %][% IF !prop.is_sales %] ([% 'AP' | $T8 %])[% END %]</th><th>[% 'Bank transaction' | $T8 %]</th></tr><tr><th>[% 'Amount' | $T8 %]</th><td>[% LxERP.format_amount(prop.amount, 2) %] ([% 'open' | $T8 %]: [% LxERP.format_amount(prop.open_amount, 2) %])</td><td>[% LxERP.format_amount(bt.amount, 2) %]</td></tr>[% IF prop.skonto_date %]<tr><th>[% 'Payment terms' | $T8 %]</th><td>[% LxERP.format_amount(prop.amount_less_skonto, 2) %] [% 'until' | $T8 %] [% HTML.escape(prop.skonto_date.to_kivitendo) %] ([% prop.percent_skonto * 100 %] %)</td><td></td></tr>[% END %]<tr><th>[% 'Customer/Vendor' | $T8 %]</th><td>[% HTML.escape(prop.customer.displayable_name) %][% HTML.escape(prop.vendor.displayable_name) %]</td><td>[% HTML.escape(bt.remote_name) %]</td></tr><tr><th>[% 'Invoice Date' | $T8 %]</th><td>[% HTML.escape(prop.transdate_as_date) %]</td><td>[% HTML.escape(bt.transdate_as_date) %] ([% HTML.escape(bt.transdate.utc_rd_days - prop.transdate.utc_rd_days) %])</td></tr><tr><th>[% 'Invoice Number' | $T8 %]</th><td>[% HTML.escape(prop.invnumber) %]</td><td>[% HTML.escape(bt.purpose) %]</td></tr></table>"
-              class="[% IF bt.agreement >= 5 %]green[% ELSIF bt.agreement < 5 and bt.agreement >= 3 %]orange[% ELSE %]red[% END %] tooltipster-html">&larr;[% HTML.escape(prop.invnumber)%]</a></div>
-      [% END %]
-     </td>
-     <td align=right>[% bt.transdate_as_date %]</td>
-     <td align=right>[% bt.amount_as_number %]</td>
-     <td align=right>[% bt.invoice_amount_as_number %]</td>
-     <td>[% HTML.escape(bt.remote_name) %]</td>
-     <td>[% HTML.escape(bt.purpose) %]</td>
-     <td>[% HTML.escape(bt.remote_account_number) %]</td>
-     <td>[% HTML.escape(bt.remote_bank_code) %]</td>
-     <td align=right>[% bt.valutadate_as_date %]</td>
-     <td align=center>[% HTML.escape(bt.currency.name) %]</td>
-    </tr>
-    [%- END %]
-  </tbody>
+  [%- FOREACH bt = BANK_TRANSACTIONS %]
+   <tbody class="listrow" id="bt_rows_[% HTML.escape(bt.id) %]">
+    [% SET proposals = bt.proposals.as_list.size > 0 ? bt.proposals.as_list : [{}] ;
+       FOREACH prop = proposals %]
+     [% IF loop.first %]
+      <tr id="bt_id_[% bt.id %]">
+       <td><a href=# onclick="kivi.BankTransaction.assign_invoice('[% bt.id %]'); return false;">[% 'Assign invoice' | $T8 %]</a></td>
+       <td><a href=# onclick="kivi.BankTransaction.create_invoice('[% bt.id %]'); return false;">[% 'Create invoice' | $T8 %]</a></td>
+       <td id="assigned_invoices_[% bt.id %]_[% prop.id %]"></td>
+       <td>[% P.input_tag("sources_" _ bt.id _ "_" _ prop.id, "", class="hidden") %]</td>
+       <td>[% P.input_tag("memos_" _ bt.id _ "_" _ prop.id, "", class="hidden") %]</td>
+       [% IF debug %]
+       <td class="tooltipster-html" title="[% FOREACH match = bt.rule_matches %] [% match %]<br> [% END %]">[% bt.agreement %]</td>
+       [% END %]
+       <td>
+        [% IF prop.id ;
+             PROCESS proposal_div ;
+           END %]
+       </td>
+       <td align="right">[% bt.transdate_as_date %]</td>
+       <td align="right">[% LxERP.format_amount(bt.amount, 2) %]</td>
+       <td align="right" id="invoice_amount_[% HTML.escape(bt.id) %]" data-invoice-amount="[% HTML.escape(bt.invoice_amount) %]">[% HTML.escape(LxERP.format_amount(bt.invoice_amount, 2)) %]</td>
+       <td>[% HTML.escape(bt.remote_name) %]</td>
+       <td>[% HTML.escape(bt.purpose) %]</td>
+       <td>[% HTML.escape(bt.transaction_text) %]</td>
+       <td>[% HTML.escape(bt.remote_account_number) %]</td>
+       <td>[% HTML.escape(bt.remote_bank_code) %]</td>
+       <td align="right">[% bt.valutadate_as_date %]</td>
+       <td align="center">[% HTML.escape(bt.currency.name) %]</td>
+      </tr>
+     [% ELSE # loop.first %]
+      <tr>
+       <td></td>
+       <td></td>
+       <td id="assigned_invoices_[% bt.id %]_[% prop.id %]"></td>
+       <td>[% P.input_tag("sources_" _ bt.id _ "_" _ prop.id, "", class="hidden") %]</td>
+       <td>[% P.input_tag("memos_" _ bt.id _ "_" _ prop.id, "", class="hidden") %]</td>
+       [% IF debug %]
+       <td></td>
+       [% END %]
+       <td>
+        [% PROCESS proposal_div %]
+       </td>
+       <td></td>
+       <td></td>
+       <td></td>
+       <td></td>
+       <td></td>
+       <td></td>
+       <td></td>
+       <td></td>
+       <td></td>
+       <td></td>
+      </tr>
+     [% END # loop.first %]
+    [% END # FOREACH proposal %]
+   </tbody>
+  [%- END %]
  </table>
+
+ <p>
+  [% L.submit_tag('action_save_invoices', LxERP.t8('Save invoices')) %]
+  [% L.button_tag('kivi.BankTransaction.show_set_all_sources_memos_dialog("#list_all_form [name^=\\"sources_\\"]:visible", "#list_all_form [name^=\\"memos_\\"]:visible")', LxERP.t8('Set all source and memo fields')) %]
+ </p>
+
+</form>
index 42f387f..b0dcc2b 100644 (file)
@@ -1,6 +1,13 @@
 [%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
 
-<table>
+<form method="post" id="list_automatic_form">
+[% L.hidden_tag('filter.bank_account', FORM.filter.bank_account) %]
+[% L.hidden_tag('filter.fromdate', FORM.filter.fromdate) %]
+[% L.hidden_tag('filter.todate',   FORM.filter.todate) %]
+[% L.hidden_tag('action', 'BankTransaction/dispatch') %]
+[% L.hidden_tag('ui_tab', ui_tab) %]
+
+<table id="bank_transactions_proposals">
   <thead>
     <tr class="listheading">
       <th>[% L.checkbox_tag('check_all') %]</th>
@@ -9,8 +16,11 @@
       <th>[% 'ID' | $T8 %]</th>
       <th>[% 'Transdate' | $T8 %]</th>
       <th>[% 'Amount' | $T8 %]</th>
+      <th>[% 'Skonto' | $T8 %]</th>
       <th>[% 'Purpose/Reference' | $T8 %]</th>
       <th>[% 'Customer/Vendor/Remote name' | $T8 %]</th>
+      <th>[% LxERP.t8("Source") %]</th>
+      <th>[% LxERP.t8("Memo") %]</th>
     </tr>
   </thead>
   [% IF !PROPOSALS.size %]
     [% FOREACH proposal = PROPOSALS %]
       <tbody class="listrow">
         <tr>
-          <td rowspan=2 style="valign:center;">
+          <td rowspan=[% proposal.rowspan %] style="valign:center;">
             [% L.checkbox_tag('proposal_ids[]', checked=0, value=proposal.id) %]
           </td>
 
-          <td>[% 'Bank transaction' | $T8 %]</td>
+          <td>[% HTML.escape(proposal.transaction_text) %]</td>
           <td>[% proposal.id %]</td>
           <td>[% proposal.transdate_as_date %]</td>
-          <td>[% proposal.amount_as_number %]</td>
-          <td>[% HTML.escape(proposal.purpose) %]</td>
+          <td align="right">[% LxERP.format_amount(proposal.amount,2) %]</td>
+          <td></td>
+          <td>
+           [% SET purpose = HTML.escape(proposal.purpose)
+                  invnumber_found = '' ;
+              FOREACH proposed_invoice = proposal.proposals;
+                IF purpose.match(proposed_invoice.invnumber);
+                  SET invnumber_found = proposed_invoice.invnumber ;
+                END ;
+              END ;
+
+              IF invnumber_found ;
+                purpose.replace(invnumber_found, '<span class="invoice_number_highlight">' _ invnumber_found _ '</span>') ;
+              ELSE ;
+                purpose ;
+              END %]
+          </td>
           <td>[% HTML.escape(proposal.remote_name) %]</td>
+          <td></td>
+          <td></td>
         </tr>
 
       [% FOREACH proposed_invoice = proposal.proposals %]
         <tr>
 
+          <td></td>
           <td>[% 'Invoice' | $T8 %]</td>
           <td>[% proposed_invoice.id %]</td>
-          <td>[% proposed_invoice.transdate_as_date %]</td>
-          <td>[% proposed_invoice.amount_as_number %]</td>
-          <td>[% proposed_invoice.link %]</td>
+          <td>[% proposed_invoice.transdate_as_date %]
+              [% L.hidden_tag("invoice_ids." _ proposal.id _ "[]", proposed_invoice.id) %]</td>
+          <td align="right">[% proposed_invoice.realamount %]</td>
+          <td>[% proposed_invoice.skonto_type | $T8 %]
+              [% L.hidden_tag("invoice_skontos." _ proposal.id _ "[]", proposed_invoice.skonto_type) %]</td>
+          <td[% IF proposed_invoice.invnumber == invnumber_found %] class="invoice_number_highlight"[% END %]>[% proposed_invoice.link %]</td>
           <td>[% HTML.escape(proposed_invoice.customer.name) %][% HTML.escape(proposed_invoice.vendor.name) %]</td>
+          <td>[% L.input_tag("sources." _ proposal.id _ "[]", "", size=20) %]</td>
+          <td>[% L.input_tag("memos." _ proposal.id _ "[]", "", size=20) %]</td>
         </tr>
-            [% L.hidden_tag("proposed_invoice_" _ proposal.id, proposed_invoice.id) %]
-      [% END %]
+        [% END %]
+        <tr><td style="height:10px" colspan="10"></td></tr>
       </tbody>
     [% END %]
   [% END %]
 </table>
+
+<p>
+ [% L.submit_tag('action_save_proposals', LxERP.t8('Save proposals')) %]
+ [% L.button_tag('kivi.BankTransaction.show_set_all_sources_memos_dialog("#list_automatic_form [name^=\\"sources.\\"]", "#list_automatic_form [name^=\\"memos.\\"]")', LxERP.t8('Set all source and memo fields')) %]
+</p>
+
+</form>
diff --git a/templates/webpages/bankaccounts/form.html b/templates/webpages/bankaccounts/form.html
deleted file mode 100644 (file)
index e1c323c..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
-
-[% SET style="width: 400px" %]
-[% SET size=34 %]
-
-<h1>[% HTML.escape(title) %]</h1>
-
-<form action="controller.pl" method="post">
-
-[%- INCLUDE 'common/flash.html' %]
-
-[%- L.hidden_tag("id", SELF.bank_account.id) %]
-
-<table>
-  <tr>
-    <th align="right">[% 'Description' | $T8 %]</th>
-    <td>[%- L.input_tag("bank_account.name", SELF.bank_account.name, size=size) %]</td>
-  </tr>
-  <tr>
-    <th align="right">[% 'IBAN' | $T8 %]</th>
-    <td>[%- L.input_tag("bank_account.iban", SELF.bank_account.iban, size=size) %]</td>
-  </tr>
-  <tr>
-    <th align="right">[% 'Bank' | $T8 %]</th>
-    <td>[%- L.input_tag("bank_account.bank", SELF.bank_account.bank, size=size) %]</td>
-  </tr>
-  <tr>
-    <th align="right">[% 'Account number' | $T8 %]</th>
-    <td>[%- L.input_tag("bank_account.account_number", SELF.bank_account.account_number, size=size) %]</td>
-  </tr>
-  <tr>
-    <th align="right">[% 'BIC' | $T8 %]</th>
-    <td>[%- L.input_tag("bank_account.bic", SELF.bank_account.bic, size=size) %]</td>
-  </tr>
-  <tr>
-    <th align="right">[% 'Bank code' | $T8 %]</th>
-    <td>[%- L.input_tag("bank_account.bank_code", SELF.bank_account.bank_code, size=size) %]</td>
-  </tr>
-  <tr>
-    <th align="right">[% 'Chart' | $T8 %]</th>
-    <td>[% L.chart_picker('bank_account.chart_id', SELF.bank_account.chart_id, type='AR_paid,AP_paid', category='A,L,Q', choose=1, style=style) %]</td>
-  </tr>
-  <tr>
-    <th align="right">[% 'Obsolete' | $T8 %]</th>
-    <td>[% L.checkbox_tag('bank_account.obsolete', checked = SELF.bank_account.obsolete, for_submit=1) %]</td>
-  </tr>
-  <tr>
-    <td align="left">[% 'Reconciliation' | $T8 %]:</td>
-    <td></td>
-  </tr>
-  <tr>
-    <th align="right">[% 'Starting date' | $T8 %]</th>
-    <td>[% L.date_tag('bank_account.reconciliation_starting_date', SELF.bank_account.reconciliation_starting_date) %]</td>
-  </tr>
-  <tr>
-    <th align="right">[% 'Starting balance' | $T8 %]</th>
-    <td>[%- L.input_tag('bank_account.reconciliation_starting_balance_as_number', SELF.bank_account.reconciliation_starting_balance_as_number) %]</td>
-  </tr>
-</table>
-
- <p>
-  [% L.hidden_tag("action", "BankAccount/dispatch") %]
-  [% L.submit_tag("action_" _  (SELF.bank_account.id ? "update" : "create"), LxERP.t8('Save'), onclick="return check_prerequisites();") %]
-  [%- IF SELF.bank_account.id AND SELF.bank_account.number_of_bank_transactions == 0 -%]
-    [% L.submit_tag("action_delete", LxERP.t8('Delete')) %]
-  [%- END %]
-  <a href="[% SELF.url_for(action='list') %]">[%- LxERP.t8("Cancel") %]</a>
- </p>
-
- <hr>
-
-<script type="text/javascript">
-<!--
-function check_prerequisites() {
-  if ($('#bank_account_name').val() === "") {
-    alert(kivi.t8('The name is missing.'));
-    return false;
-  }
-  if ($('#bank_account_iban').val() === "") {
-    alert(kivi.t8('The IBAN is missing.'));
-    return false;
-  }
-  if ($('#bank_account_chart_id').val() === "") {
-    alert(kivi.t8('There is no connected chart.'));
-    return false;
-  }
-
-  return true;
-}
--->
-</script>
-</form>
diff --git a/templates/webpages/bankaccounts/list.html b/templates/webpages/bankaccounts/list.html
deleted file mode 100644 (file)
index c1a9ff0..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%][%- INCLUDE 'common/flash.html' %]
-
-<h1>[% title %]</h1>
-
-<p>
- <table width="100%" id="bankaccount_list">
-  <thead>
-   <tr class="listheading">
-    <th align="center" width="1%"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></th>
-    <th>[% 'Name' | $T8 %]</th>
-    <th>[% 'IBAN' | $T8 %]</th>
-    <th>[% 'Bank' | $T8 %]</th>
-    <th>[% 'Bank code' | $T8 %]</th>
-    <th>[% 'BIC' | $T8 %]</th>
-    <th>[% 'Date' | $T8 %]</th>
-    <th>[% 'Balance' | $T8 %]</th>
-   </tr>
-  </thead>
-
-  <tbody>
-   [%- FOREACH account = BANKACCOUNTS %]
-    <tr class="listrow" id="account_id_[% account.id %]">
-     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></td>
-     <td><a href="[% SELF.url_for(action='edit', id=account.id) %]">[% HTML.escape(account.name) %]</a></td>
-     <td>[% HTML.escape(account.iban) %]</a></td>
-     <td>[% HTML.escape(account.bank) %]</a></td>
-     <td>[% HTML.escape(account.bank_code) %]</a></td>
-     <td>[% HTML.escape(account.bic) %]</a></td>
-     <td>[% HTML.escape(account.reconciliation_starting_date.to_kivitendo) %]</a></td>
-     <td align="right">[% HTML.escape(account.reconciliation_starting_balance_as_number) %]</a></td>
-    </tr>
-   [%- END %]
-  </tbody>
- </table>
-</p>
-
-<hr height="3">
-
-[% L.sortable_element('#bankaccount_list tbody', url=SELF.url_for(action='reorder'), with='account_id') %]
-
-<p>
- <a href="[% SELF.url_for(action='new') %]">[%- 'Add' | $T8 %]</a>
-</p>
diff --git a/templates/webpages/bankimport/form.html b/templates/webpages/bankimport/form.html
deleted file mode 100644 (file)
index 1c3d672..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-[%- USE HTML %]
-[%- USE LxERP %]
-[%- USE L %]
-[%- USE T8 %]
-
- <div class="listtop">[% FORM.title %]</div>
-
- [% IF profile %]
- <p>
- [% "Import a MT940 file:" | $T8 %]
- </p>
-
- <form method="post" action="controller.pl" enctype="multipart/form-data">
-  [% L.hidden_tag('action', 'BankImport/import_mt940') %]
-
-    [% L.input_tag('file', '', type => 'file', accept => '*') %]
-    [% L.submit_tag('action_import_mt940', LxERP.t8('Import')) %]
-
- </form>
- [% ELSE %]
- <p>
- [% "Please create a CSV import profile called \"MT940\" for the import type bank transactions:" | $T8 %] <a href="[% SELF.url_for(controller => 'CsvImport', action => 'new', 'profile.type' => 'bank_transactions' ) %]">CsvImport</a>
- </p>
- [% END %]
index 2ce20ff..51e7382 100644 (file)
@@ -7,7 +7,11 @@
 
 <h1>[% title | html %]</h1>
 
-<form method=post action=bp.pl>
+<form method="post" action="bp.pl" id="form">
+
+<p>
+ [% LxERP.t8("Printer") %]: [% L.select_tag('printer', ALL_PRINTERS, title_key = 'printer_description') %]
+</p>
 
 <p>
 [% FOREACH option IN options %]
@@ -46,7 +50,7 @@
   <td>[% L.link(url(row.module _ '.pl', action='edit' ,type=type, callback=list_spool__callback, id=row.id), row.quonumber) %]</td>
 [%- END %]
   <td>[% row.name | html %]</td>
-  <td>[% L.link(spool _ '/' _ row.spoolfile, row.spoolfile) %][% L.hidden_tag('spoolfile_' _ loop.count, row.spoolfile) %]</td>
+  <td>[% L.link(LXCONFIG.paths.spool _ '/' _ row.spoolfile, row.spoolfile) %][% L.hidden_tag('spoolfile_' _ loop.count, row.spoolfile) %]</td>
  </tr>
 [%- END %]
 
@@ -54,9 +58,6 @@
 
 </table>
 
-<hr size=3 noshade>
-<br>
-
 [% L.hidden_tag('callback', callback) %]
 [% L.hidden_tag('title', title) %]
 [% L.hidden_tag('vc', vc) %]
 [% L.hidden_tag('quonumber', quonumber) %]
 [% L.hidden_tag('customer', customer) %]
 [% L.hidden_tag('vendor', vendor) %]
-
-[% L.submit_tag('action', LxERP.t8('Remove'), confirm=LxERP.t8('Are you sure you want to remove the marked entries from the queue?')) %]
-[% L.submit_tag('action', LxERP.t8('Print')) %]
-
-[% L.select_tag('printer', ALL_PRINTERS, title_key = 'printer_description') %]
-
 </form>
index 6fd9767..c211aef 100644 (file)
@@ -2,29 +2,14 @@
 [%- USE T8 %]
 [%- USE LxERP %]
 [%- USE HTML %]
-<form method=post action=bp.pl>
+<form method="post" action="bp.pl" id="form">
 
 <h1>[% 'Print' | $T8 %] [% label.$type.title %]</h1>[% L.hidden_tag('title', LxERP.t8('Print') _ ' ' _ label.$type.title) %]
 
 <table>
   <tr>
     <th align=right>[% 'Customer' | $T8 %]</th>
-    <td colspan=3>
-  [%- IF vc == 'customer' ? all_customer.size : all_vendor.size %]
-      [%- INCLUDE 'generic/multibox.html'
-           name          = vc,
-           DATA          = vc == 'customer' ? all_customer : all_vendor,
-           id_sub        = 'vc_keys',
-           label_sub     = 'vc_keys',
-           select        = vc_select,
-           limit         = vclimit,
-           show_empty    = 1,
-           allow_textbox = 1,
-      -%]
-  [%- ELSE %]
-    [% L.input_tag(vc, '', size=35) %]
-  [%- END %]
-</td>
+    <td colspan=3>[% L.input_tag(vc, '', size=35) %]</td>
   </tr>
 [% IF show_accounts %]
   <tr>
   </tr>
 </table>
 
-<hr size=3 noshade>
-<br>
 
 [% L.hidden_tag('sort', 'transdate') %]
 [% L.hidden_tag('vc', vc) %]
 [% L.hidden_tag('type', type) %]
-[% L.hidden_tag('nextsub', 'list_spool') %]
-
-[% L.submit_tag('action', LxERP.t8('Continue')) %]
-
 </form>
-
-
index ad9db14..d068a07 100644 (file)
@@ -1,22 +1,22 @@
-[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%][%- USE P -%]
 [% SET style="width: 400px" %]
 
 <h1>[% HTML.escape(title) %]</h1>
 
-<form action="controller.pl" method="post">
+<form action="controller.pl" method="post" id="form">
 [%- L.hidden_tag("id", SELF.config.id) %]
 
 <table>
   <tr>
     <th align="right">[% 'Description' | $T8 %]</th>
-    <td>[%- L.input_tag("config.description", SELF.config.description) %]</td>
+    <td>[%- L.input_tag("config.description", SELF.config.description, "data-validate"="required", "data-title"=LxERP.t8("Description")) %]</td>
   </tr>
   <tr>
     <th align="right">[% 'Inventory Account' | $T8 %]</th>
     [%- IF NOT SELF.config.id %]
-    <td>[% L.chart_picker("config.inventory_accno_id", SELF.defaults.inventory_accno_id, choose=1, type='IC', style=style) %]</td>
+    <td>[% P.chart.picker("config.inventory_accno_id", SELF.defaults.inventory_accno_id, choose=1, type='IC', style=style) %]</td>
     [%- ELSIF SELF.config.id AND SELF.config.orphaned %]
-    <td>[% L.chart_picker("config.inventory_accno_id", SELF.config.inventory_accno_id, choose=1, type='IC', style=style) %]</td>
+    <td>[% P.chart.picker("config.inventory_accno_id", SELF.config.inventory_accno_id, choose=1, type='IC', style=style) %]</td>
     [%- ELSE %]
       <td>[%- CHARTLIST.inventory_accno_description %]</td>
     [%- END %]
@@ -25,9 +25,9 @@
   <tr>
     <th align="right">[% 'Revenue' | $T8 %] [% HTML.escape(tz.description) %]</th>
     [%- IF NOT SELF.config.id %]
-    <td>[% L.chart_picker('income_accno_id_' _ tz.id, SELF.defaults.income_accno_id, choose=1, type='IC_income,IC_sale', style=style) %]</td>
+    <td>[% P.chart.picker('income_accno_id_' _ tz.id, SELF.defaults.income_accno_id, choose=1, type='IC_income,IC_sale', style=style) %]</td>
     [%- ELSIF SELF.config.id AND SELF.config.orphaned %]
-    <td>[% L.chart_picker('income_accno_id_' _ tz.id, CHARTLIST.${tz.id}.income_accno_id, choose=1, type='IC_income,IC_sale', style=style) %]</td>
+    <td>[% P.chart.picker('income_accno_id_' _ tz.id, CHARTLIST.${tz.id}.income_accno_id, choose=1, type='IC_income,IC_sale', style=style) %]</td>
     [%- ELSE %]
       <td>[% CHARTLIST.${tz.id}.income_accno_description %]</td>
     [%- END %]
   <tr>
     <th align="right">[% 'Expense' | $T8 %] [% HTML.escape(tz.description) %]</th>
     [%- IF NOT SELF.config.id %]
-      <td>[% L.chart_picker('expense_accno_id_' _ tz.id, SELF.defaults.expense_accno_id, choose=1, type='IC_expense,IC_cogs', style=style) %]</td>
+      <td>[% P.chart.picker('expense_accno_id_' _ tz.id, SELF.defaults.expense_accno_id, choose=1, type='IC_expense,IC_cogs', style=style) %]</td>
     [%- ELSIF SELF.config.id AND SELF.config.orphaned %]
-      <td>[% L.chart_picker('expense_accno_id_' _ tz.id, CHARTLIST.${tz.id}.expense_accno_id, choose=1, type='IC_expense,IC_cogs', style=style) %]</td>
+      <td>[% P.chart.picker('expense_accno_id_' _ tz.id, CHARTLIST.${tz.id}.expense_accno_id, choose=1, type='IC_expense,IC_cogs', style=style) %]</td>
     [%- ELSE %]
       <td>[% CHARTLIST.${tz.id}.expense_accno_description %]</td>
     [%- END %]
   </tr>
 [%- END %]
 </table>
-
- <p>
-  [% L.hidden_tag("action", "Buchungsgruppen/dispatch") %]
-  [% L.submit_tag("action_" _  (SELF.config.id ? "update" : "create"), LxERP.t8('Save'), onclick="return check_prerequisites();") %]
-  [%- IF SELF.config.id AND SELF.config.orphaned %]
-    [% L.submit_tag("action_delete", LxERP.t8('Delete'), confirm=LxERP.t8('Are you sure?')) %]
-  [%- END %]
- </p>
-
- <hr>
-
-<script type="text/javascript">
-<!--
-function check_prerequisites() {
-  if ($('#config_description').val() === "") {
-    alert(kivi.t8('The description is missing.'));
-    return false;
-  }
-
-  return true;
-}
--->
-</script>
 </form>
index 8621e85..319dcb9 100644 (file)
@@ -6,7 +6,7 @@
  <table width="100%" id="buchungsgruppen_list">
   <thead>
    <tr class="listheading">
-    <th align="center" width="1%"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></th>
+    <th align="center" width="1%"><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]"></th>
     <th width="20%">[% 'Description' | $T8 %]</th>
     <th width="20%">[% 'Inventory Account' | $T8 %]</th>
      [%- FOREACH tz = TAXZONES %]
@@ -19,7 +19,7 @@
   <tbody>
    [%- FOREACH bg = BUCHUNGSGRUPPEN %]
     <tr class="listrow" id="bg_id_[% bg.id %]">
-     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></td>
+     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]"></td>
      <td><a href="[% SELF.url_for(action='edit', id=bg.id) %]">[% HTML.escape(bg.description) %]</a></td>
      <td>[% HTML.escape(CHARTLIST.${bg.id}.inventory_accno) %]</td>
      [%- FOREACH tz = TAXZONES %]
  </table>
 </p>
 
-<hr height="3">
-
 [% L.sortable_element('#buchungsgruppen_list tbody', url=SELF.url_for(action='reorder'), with='bg_id') %]
-
-<p>
- <a href="[% SELF.url_for(action='new') %]">[%- 'Add' | $T8 %]</a>
-</p>
-
diff --git a/templates/webpages/business/form.html b/templates/webpages/business/form.html
deleted file mode 100644 (file)
index 6fa326b..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
- <form method="post" action="controller.pl">
-
-[%- INCLUDE 'common/flash.html' %]
-
-  <table>
-   <tr>
-    <td>[%- LxERP.t8('Description') %]</td>
-    <td>[% L.input_tag("business.description", SELF.business.description, "size", 30) %]</td>
-   </tr>
-
-   <tr>
-    <td>[%- LxERP.t8('Discount') %]</td>
-    <td>[% L.input_tag("business.discount_as_percent", SELF.business.discount_as_percent, "size", 5) %]%</td>
-   </tr>
-
-   <tr>
-    <td>[%- LxERP.t8('Customernumberinit') %]</td>
-    <td>[% L.input_tag("business.customernumberinit", SELF.business.customernumberinit, "size", 10) %]</td>
-   </tr>
-
-   [%- IF LXCONFIG.features.vertreter %]
-    <tr>
-     <td>[%- LxERP.t8('Representative') %]</td>
-     <td>[% L.checkbox_tag("business.salesman", "value", 1, "checked", SELF.business.salesman) %]</td>
-    </tr>
-   [%- END %]
-  </table>
-
-  <p>
-   [% L.hidden_tag("id", SELF.business.id) %]
-   [% L.hidden_tag("action", "Business/dispatch") %]
-   [% L.submit_tag("action_" _  (SELF.business.id ? "update" : "create"), LxERP.t8('Save')) %]
-   [%- IF SELF.business.id %]
-    [% L.submit_tag("action_destroy", LxERP.t8("Delete"), "confirm", LxERP.t8("Are you sure you want to delete this business?")) %]
-   [%- END %]
-   <a href="[% SELF.url_for(action => 'list') %]">[%- LxERP.t8('Abort') %]</a>
-  </p>
- </form>
diff --git a/templates/webpages/business/list.html b/templates/webpages/business/list.html
deleted file mode 100644 (file)
index f1e5c55..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
-[%- INCLUDE 'common/flash.html' %]
-
- <form method="post" action="controller.pl">
-  [% IF !BUSINESSS.size %]
-   <p>
-    [%- LxERP.t8('No business has been created yet.') %]
-   </p>
-
-  [%- ELSE %]
-   <table id="business_list" width="100%">
-    <thead>
-    <tr class="listheading">
-     <th width="80%">[%- LxERP.t8('Description') %]</th>
-     <th>[%- LxERP.t8('Discount') %]</th>
-     <th>[%- LxERP.t8('Customernumberinit') %]</th>
-     [%- IF LXCONFIG.features.vertreter %]
-      <th>[%- LxERP.t8('Representative') %]</th>
-     [%- END %]
-    </tr>
-    </thead>
-
-    <tbody>
-    [%- FOREACH business = BUSINESSS %]
-    <tr class="listrow[% loop.count % 2 %]" id="business_id_[% business.id %]">
-     <td>
-      <a href="[% SELF.url_for(action => 'edit', id => business.id) %]">
-       [%- HTML.escape(business.description) %]
-      </a>
-     </td>
-     <td align="right">[% LxERP.format_amount(business.discount * 100) %] %</td>
-     <td align="right">[%- HTML.escape(business.customernumberinit) %]</td>
-     [%- IF LXCONFIG.features.vertreter %]
-      <td>[%- IF business.salesman %][%- LxERP.t8('Yes') %][%- ELSE %][%- LxERP.t8('No') %][%- END %]</td>
-     [%- END %]
-    </tr>
-    [%- END %]
-    </tbody>
-   </table>
-  [%- END %]
-
-  <hr size="3" noshade>
-
-  <p>
-   <a href="[% SELF.url_for(action => 'new') %]">[%- LxERP.t8('Create new business') %]</a>
-  </p>
- </form>
index 964790c..c0392c2 100644 (file)
@@ -4,7 +4,7 @@
 [% USE LxERP %]
 <h1>[% title | html %]</h1>
 
-<form method=post action="[% script %]">
+<form method=post action="[% script | html %]">
 
 [% L.hidden_tag('accno', accno) %]
 [% L.hidden_tag('description', description) %]
diff --git a/templates/webpages/chart/report_configuration_overview.html b/templates/webpages/chart/report_configuration_overview.html
new file mode 100644 (file)
index 0000000..5178ba6
--- /dev/null
@@ -0,0 +1,40 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%]<h1>[% LxERP.t8("Chart configuration overview regarding reports") %]</h1>
+
+[% FOREACH data = DATA %]
+ [% UNLESS data.size == 1 %]
+  <div id="[% HTML.escape(data.type) %]">
+   [% LxERP.t8("Jump to") %]:
+   [% FOREACH jump = DATA %]
+    [% IF jump.type != data.type %]
+     [% L.link("#" _ jump.type, jump.heading) %]
+    [% END %]
+   [% END %]
+  </div>
+ [% END %]
+
+ <h2>[% HTML.escape(data.heading) %]</h2>
+
+ [% FOREACH pos = data.positions %]
+  [%- SET name = data.names.item(pos) %]
+  <h3>[% IF name %][% LxERP.t8("Position #1: #2", pos, name) %][% ELSE %][% LxERP.t8("Position #1", pos) %][% END %]</h3>
+
+  [%- SET charts = data.charts.$pos %]
+  <table>
+   <thead>
+    <tr class="listheading">
+     <th>[% LxERP.t8("Account") %]</th>
+     <th>[% LxERP.t8("Description") %]</th>
+    </tr>
+   </thead>
+
+   <tbody>
+    [% FOREACH chart = charts %]
+    <tr>
+     <td>[% L.link("am.pl?action=edit_account&id=" _ chart.id, chart.accno) %]</td>
+     <td>[% HTML.escape(chart.description) %]</td>
+    </tr>
+    [% END %]
+   </tbody>
+  </table>
+ [% END %]
+[% END %]
index 4fc4688..721c066 100644 (file)
@@ -1,4 +1,5 @@
 [% USE L %]
+[% USE P %]
 [% SET style="width: 400px" %]
 
 <h1>Chart Picker Testpage</h1>
@@ -6,45 +7,50 @@
 <div>
 
 <p>
-All charts: [% L.chart_picker('chart_id', '', style=style) %]text after icon<br>
-Only booked charts: [% L.chart_picker('chart_id_booked', '', booked=1, style=style) %]<br>
-All charts choose: [% L.chart_picker('chart_id_choose', '', choose=1, style=style) %]<br>
+All charts: [% P.chart.picker('chart_id', '', style=style) %]text after icon<br>
+Only booked charts: [% P.chart.picker('chart_id_booked', '', booked=1, style=style) %]<br>
+All charts choose: [% P.chart.picker('chart_id_choose', '', choose=1, style=style) %]<br>
 </p>
 
 <p>
 Filter by link:<br>
-AR_paid: [% L.chart_picker('chart_id_ar_paid', undef, type='AR_paid', category='I,A' style=style) %]<br>
-AR: [% L.chart_picker('chart_id_ar', undef, type='AR', style=style) %]<br>
-AP: [% L.chart_picker('chart_id_ap', undef, type='AP', style=style) %]<br>
-AR or AP: [% L.chart_picker('chart_id_arap', undef, type='AR,AP', style=style) %]<br>
-IC_income,IC_sale: [% L.chart_picker('chart_id_icis', undef, type='IC_income,IC_sale', style=style) %]<br>
-IC_expense,IC_cogs: [% L.chart_picker('chart_id_icco', undef, type='IC_expense,IC_cogs', style=style) %]<br>
-IC: [% L.chart_picker('chart_id_ic', undef, type='IC', style=style) %]<br>
+AR_paid: [% P.chart.picker('chart_id_ar_paid', undef, type='AR_paid', category='I,A' style=style) %]<br>
+AR: [% P.chart.picker('chart_id_ar', undef, type='AR', style=style) %]<br>
+AP: [% P.chart.picker('chart_id_ap', undef, type='AP', style=style) %]<br>
+AR or AP: [% P.chart.picker('chart_id_arap', undef, type='AR,AP', style=style) %]<br>
+IC_income,IC_sale: [% P.chart.picker('chart_id_icis', undef, type='IC_income,IC_sale', style=style) %]<br>
+IC_expense,IC_cogs: [% P.chart.picker('chart_id_icco', undef, type='IC_expense,IC_cogs', style=style) %]<br>
+IC: [% P.chart.picker('chart_id_ic', undef, type='IC', style=style) %]<br>
 </p>
 
 <p>
 Filter by category:<br>
-I: [% L.chart_picker('chart_id_i', undef, category='I', style=style) %]<br>
-IE: [% L.chart_picker('chart_id_ie', undef, category='I,E', style=style) %]<br>
-AQL: [% L.chart_picker('chart_id_aql', undef, category='A,Q,L', style=style) %]<br>
+I: [% P.chart.picker('chart_id_i', undef, category='I', style=style) %]<br>
+IE: [% P.chart.picker('chart_id_ie', undef, category='I,E', style=style) %]<br>
+AQL: [% P.chart.picker('chart_id_aql', undef, category='A,Q,L', style=style) %]<br>
 </p>
 
 <p>
 Filter by special type:<br>
-GuV: [% L.chart_picker('chart_id_guv', undef, type='guv', style=style) %]<br>
+GuV: [% P.chart.picker('chart_id_guv', undef, type='guv', style=style) %]<br>
 </p>
 
-<p>bank (fat): [% L.chart_picker('bank_id', '', type='bank', fat_set_item=1, choose=1, style=style) %]
+<p>bank (fat): [% P.chart.picker('bank_id', '', type='bank', fat_set_item=1, choose=1, style=style) %]
 </p>
 <p id="banktext"></p>
 
 
 <p>
 [% FOREACH i IN [ 1 2 3 4 5 6 ] %]
-S [% i %]: [% L.chart_picker('credit_' _ i) %] - &nbsp;&nbsp;  H [% i %]: [% L.chart_picker('debit' _ i) %] <br>
+S [% i %]: [% P.chart.picker('credit_' _ i) %] - &nbsp;&nbsp;  H [% i %]: [% P.chart.picker('debit' _ i) %] <br>
 [% END %]
 </p>
 
+<p>
+Pre-filled chart object: [% P.chart.picker('pre_filled_chart_object', pre_filled_chart, style=style) %]<br>
+Pre-filled chart ID: [% P.chart.picker('pre_filled_chart_id', pre_filled_chart.id, style=style) %]<br>
+</p>
+
 </div>
 
 <script type='text/javascript'>
@@ -57,4 +63,3 @@ S [% i %]: [% L.chart_picker('credit_' _ i) %] - &nbsp;&nbsp;  H [% i %]: [% L.c
   });
 
 </script>
-
diff --git a/templates/webpages/client_config/_attachments.html b/templates/webpages/client_config/_attachments.html
new file mode 100644 (file)
index 0000000..9d1cc73
--- /dev/null
@@ -0,0 +1,31 @@
+[%- USE LxERP -%][%- USE L -%]
+<div id="attachments">
+[% SET file_type = 'attachment' %]
+[% INCLUDE 'file/rename_dialog.html' %]
+ <table>
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Global Attachments") %]
+  [% LxERP.t8("for Document types") %]
+</td></tr>
+  <tr><td>  <div class="tabwidget">
+     <ul>
+      <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=sales_quotation&object_id=0&is_global=1">[%
+      LxERP.t8("Sales Quotations") %]</a></li>
+      <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=sales_order&object_id=0&is_global=1">[%
+      LxERP.t8("Sales Orders") %]</a></li>
+      <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=sales_delivery_order&object_id=0&is_global=1">[%
+      LxERP.t8("Sales Delivery Orders") %]</a></li>
+      <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=invoice&object_id=0&is_global=1">[%
+      LxERP.t8("Invoices") %]</a></li>
+      <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=request_quotation&object_id=0&is_global=1">[%
+      LxERP.t8("Request Quotations") %]</a></li>
+      <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=purchase_order&object_id=0&is_global=1">[%
+      LxERP.t8("Purchase Orders") %]</a></li>
+      <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=purchase_delivery_order&object_id=0&is_global=1">[%
+      LxERP.t8("Purchase Delivery Orders") %]</a></li>
+      <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=purchase_invoice&object_id=0&is_global=1">[%
+      LxERP.t8("Purchase Invoices") %]</a></li>
+     </ul>
+    </div>
+  </td></tr>
+ </table>
+</div>
index e71487d..dc99c21 100644 (file)
@@ -2,7 +2,7 @@
 <div id="datev_check_configuration">
  <table>
   <tr>
-   <td colspan="3">[% LxERP.t8('It is possible to make a quick DATEV export everytime you post a record to ensure things work nicely with their data requirements. This will result in a slight overhead though you can enable this for each type of record independantly.') %]</td>
+   <td colspan="3">[% LxERP.t8('It is possible to make a quick DATEV export everytime you post a record to ensure things work nicely with their data requirements. This will result in a slight overhead though you can enable this for each type of record independently.') %]</td>
   </tr>
   <tr>
    <td align="right">[% LxERP.t8('Check on sales invoice') %]</td>
    <td>[% L.yes_no_tag('defaults.datev_check_on_gl_transaction', SELF.defaults.datev_check_on_gl_transaction) %]</td>
    <td>[% LxERP.t8('Perform check when a gl transaction is posted?') %]</td>
   </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Datev export encoding') %]</td>
+   <td>[% L.select_tag('defaults.datev_export_format', [ [ 'cp1252', LxERP.t8('Strict and halt') ],[ 'cp1252-translit', LxERP.t8('Strict but replace') ],[ 'utf-8', LxERP.t8('Relaxed (UTF-8)') ]  ], default=SELF.defaults.datev_export_format) %]
+   <td>[% LxERP.t8('DATEV expects the encoding to be Western Europe conform (LATIN-1, cp1252). By setting this to "Strict and halt" the DATEV export halts with a error if there is a single character in "Posting Text" which is not LATIN-1 encodeable. By setting this to "Strict but replace" kivitendo will replace the character with a similar one and the export will simply warn about those fields. By setting this to relaxed (UTF-8) the DATEV export encoding will be in kivitendo (UTF-8) encoded and the external import program has to handle this (this may work for DATEV deriviates or future versions of DATEV). Background details: For example turkish characters (Ç) are not valid cp1252 charactes and armenian characters like "Գեղարդ" are probably not replaceable in cp1252') %]</td>
+  </tr>
  </table>
 </div>
index 0a6ec1e..05f4e9e 100644 (file)
@@ -1,4 +1,4 @@
-[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE P -%][%- USE T8 -%]
 [% SET style="width: 500px" %]
 [%# L.dump( SELF.defaults ) %]
 <div id="default_accounts">
@@ -6,33 +6,81 @@
   <tr>
    <td align="right">[% LxERP.t8("Inventory Account") %]</td>
    <td>
-   [% L.chart_picker('defaults.inventory_accno_id', SELF.defaults.inventory_accno_id, type='IC', choose=1, style=style) %]
+   [% P.chart.picker('defaults.inventory_accno_id', SELF.defaults.inventory_accno_id, type='IC', choose=1, style=style) %]
    <td>
   </tr>
 
   <tr>
    <td align="right">[% LxERP.t8("Revenue Account") %]</td>
-   <td>[% L.chart_picker('defaults.income_accno_id', SELF.defaults.income_accno_id, type='IC_income,IC_sale', choose=1, style=style) %]</td>
+   <td>[% P.chart.picker('defaults.income_accno_id', SELF.defaults.income_accno_id, type='IC_income,IC_sale', choose=1, style=style) %]</td>
   </tr>
 
   <tr>
    <td align="right">[% LxERP.t8("Expense Account") %]</td>
-   <td>[% L.chart_picker('defaults.expense_accno_id', SELF.defaults.expense_accno_id, type='IC_expense,IC_cogs', choose=1, style=style) %]</td>
+   <td>[% P.chart.picker('defaults.expense_accno_id', SELF.defaults.expense_accno_id, type='IC_expense,IC_cogs', choose=1, style=style) %]</td>
   </tr>
 
   <tr>
    <td align="right">[% LxERP.t8("Foreign Exchange Gain") %]</td>
-   <td>[% L.chart_picker('defaults.fxgain_accno_id', SELF.defaults.fxgain_accno_id, category='I,A', choose=1, style=style) %]<td>
+   <td>[% P.chart.picker('defaults.fxgain_accno_id', SELF.defaults.fxgain_accno_id, category='I,A', choose=1, style=style) %]<td>
   </tr>
 
   <tr>
    <td align="right">[% LxERP.t8("Foreign Exchange Loss") %]</td>
-   <td>[% L.chart_picker('defaults.fxloss_accno_id', SELF.defaults.fxloss_accno_id, category='E,A', choose=1, style=style) %]<td>
+   <td>[% P.chart.picker('defaults.fxloss_accno_id', SELF.defaults.fxloss_accno_id, category='E,A', choose=1, style=style) %]<td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Rounding Gain") %]</td>
+   <td>[% P.chart.picker('defaults.rndgain_accno_id', SELF.defaults.rndgain_accno_id, category='I,A', choose=1, style=style) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Rounding Loss") %]</td>
+   <td>[% P.chart.picker('defaults.rndloss_accno_id', SELF.defaults.rndloss_accno_id,  category='E,A', choose=1, style=style) %]</td>
   </tr>
 
   <tr>
    <td align="right">[% LxERP.t8("Current assets account") %]</td>
-   <td>[% L.chart_picker('defaults.ar_paid_accno_id', SELF.defaults.ar_paid_accno_id, type='AR_paid', choose=1, style=style) %]<td>
+   <td>[% P.chart.picker('defaults.ar_paid_accno_id', SELF.defaults.ar_paid_accno_id, type='AR_paid', choose=1, style=style) %]<td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Payable account") %]</td>
+   <td>[% P.chart.picker('defaults.ap_chart_id', SELF.defaults.ap_chart_id, type='AP', choose=1, style=style) %]<td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Receivable account") %]</td>
+   <td>[% P.chart.picker('defaults.ar_chart_id', SELF.defaults.ar_chart_id, type='AR', choose=1, style=style) %]<td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Clearing account for advance payments") %]</td>
+   <td>[% P.chart.picker('defaults.advance_payment_clearing_chart_id', SELF.defaults.advance_payment_clearing_chart_id, choose=1, style=style) %]<td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Account for workflow from purchase order to ap transaction") %]</td>
+   <td>[% P.chart.picker('defaults.workflow_po_ap_chart_id', SELF.defaults.workflow_po_ap_chart_id, type='AP_amount', choose=1, style=style) %]<td>
+  </tr>
+
+  <tr>
+    <th align="right">[% LxERP.t8("Year-end closing") %]</th>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8("Carry over account for year-end closing") %]</td>
+   <td>[% P.chart.picker('defaults.carry_over_account_chart_id', SELF.defaults.carry_over_account_chart_id, category='A', choose=1, style=style) %]<td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Profit carried forward account") %]</td>
+   <td>[% P.chart.picker('defaults.profit_carried_forward_chart_id', SELF.defaults.profit_carried_forward_chart_id, category='A', choose=1, style=style) %]<td>
+  </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8("Loss carried forward account") %]</td>
+   <td>[% P.chart.picker('defaults.loss_carried_forward_chart_id', SELF.defaults.loss_carried_forward_chart_id, category='A', choose=1, style=style) %]<td>
   </tr>
- </table>
+</table>
 </div>
index de9a954..cd6aee2 100644 (file)
@@ -1,8 +1,24 @@
 [%- USE LxERP -%][%- USE L -%][%- USE P -%][%- USE T8 %]
+[% SET style="width: 250px" %]
 <div id="features">
  <table>
-  <tr><td class="listheading" colspan="4">[% LxERP.t8("WebDAV") %]</td></tr>
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("DATEV") %]</td></tr>
 
+  <tr>
+   <td align="right">[% LxERP.t8('Datevautomatik') %]</td>
+   <td>[% L.yes_no_tag('defaults.feature_datev', SELF.defaults.feature_datev) %]</td>
+   <td>[% LxERP.t8('Use Datevautomatik') %]</td>
+  </tr>
+
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("UStVA") %]</td></tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8('UStVA') %]</td>
+   <td>[% L.yes_no_tag('defaults.feature_ustva', SELF.defaults.feature_ustva) %]</td>
+   <td>[% LxERP.t8('Use UStVA') %]</td>
+  </tr>
+
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("File Management") %]</td></tr>
   <tr>
    <td align="right">[% LxERP.t8('WebDAV') %]</td>
    <td>[% L.yes_no_tag('defaults.webdav', SELF.defaults.webdav) %]</td>
    <td>[% L.yes_no_tag('defaults.webdav_documents', SELF.defaults.webdav_documents) %]</td>
    <td>[% LxERP.t8('Save document in WebDAV repository') %]</td>
   </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Filemanagement') %]</td>
+   <td>[% L.yes_no_tag('defaults.doc_storage', SELF.defaults.doc_storage) %]</td>
+   <td>[% LxERP.t8('Use Filemanagement') %]</td>
+  </tr>
+  <tr>
+    <td align="right">[% LxERP.t8('Storage Type for generated/imported PDF Documents') %]</td>
+    <td>[% L.select_tag('defaults.doc_storage_for_documents',
+         [ [ 'None', LxERP.t8('None') ],[ 'Filesystem', LxERP.t8('Files') ],[ 'Webdav', LxERP.t8('WebDAV') ] ],
+                               default = SELF.defaults.doc_storage_for_documents,
+                               onchange="return checkavailable_filebackend(this);") %]</td>
+    <td>[% LxERP.t8('Use this storage backend for all generated PDF-Files') %]</td>
+  </tr>
+  <tr>
+    <td align="right">[% LxERP.t8('Storage Type for Attachments') %]</td>
+    <td>[% L.select_tag('defaults.doc_storage_for_attachments',
+         [ [ 'None', LxERP.t8('None') ], [ 'Filesystem', LxERP.t8('Files') ],[ 'Webdav', LxERP.t8('WebDAV') ] ],
+                               default = SELF.defaults.doc_storage_for_attachments,
+                               onchange="return checkavailable_filebackend(this);") %]</td>
+    <td>[% LxERP.t8('Use this storage backend for all uploaded attachments') %]</td>
+  </tr>
+  <tr>
+    <td align="right">[% LxERP.t8('Storage Type for images') %]</td>
+    <td>[% L.select_tag('defaults.doc_storage_for_images',
+         [ [ 'None', LxERP.t8('None') ], [ 'Filesystem', LxERP.t8('Files') ],[ 'Webdav', LxERP.t8('WebDAV') ] ],
+                               default = SELF.defaults.doc_storage_for_images,
+                               onchange="return checkavailable_filebackend(this);") %]</td>
+    <td>[% LxERP.t8('Use this storage backend for uploaded images') %]</td>
+  </tr>
+  <tr>
+    <td align="right">[% LxERP.t8('Storage Type for shopimages') %]</td>
+    <td>[% L.select_tag('defaults.doc_storage_for_shopimages',
+         [ [ 'None', LxERP.t8('None') ], [ 'Filesystem', LxERP.t8('Files') ],[ 'Webdav', LxERP.t8('WebDAV') ] ],
+                               default = SELF.defaults.doc_storage_for_shopimages,
+                               onchange="return checkavailable_filebackend(this);") %]</td>
+    <td>[% LxERP.t8('Use this storage backend for uploaded images') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Delete printfiles') %]</td>
+   <td>[% L.yes_no_tag('defaults.doc_delete_printfiles', SELF.defaults.doc_delete_printfiles) %]</td>
+   <td>[% LxERP.t8('Allow to delete generated printfiles') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('max filesize') %]</td>
+   <td>[% L.input_tag('doc_max_filesize_MB','', size=10, onchange="verifyMBSize(this);") %]
+       [% L.hidden_tag('defaults.doc_max_filesize',SELF.defaults.doc_max_filesize) %] MB</td>
+   <td>[% LxERP.t8('The maximum of uploadable filesize in Megabyte') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Preselect Customer/Vendor documents as email attachments') %]</td>
+   <td>[% L.yes_no_tag('defaults.email_attachment_vc_files_checked', SELF.defaults.email_attachment_vc_files_checked) %]</td>
+   <td>[% LxERP.t8('Preselect all documents saved for the current customer/vendor as a mail attachment.') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Preselect part documents as email attachments') %]</td>
+   <td>[% L.yes_no_tag('defaults.email_attachment_part_files_checked', SELF.defaults.email_attachment_part_files_checked) %]</td>
+   <td>[% LxERP.t8('Preselect all documents for the current selected parts in a record as a mail attachment.') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Preselect record documents as email attachments') %]</td>
+   <td>[% L.yes_no_tag('defaults.email_attachment_record_files_checked', SELF.defaults.email_attachment_record_files_checked) %]</td>
+   <td>[% LxERP.t8('Preselect all documents saved for the current record as a mail attachment.') %]</td>
+  </tr>
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Storage Backends") %]</td></tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Files') %]</td>
+   <td>[% L.yes_no_tag('defaults.doc_files', SELF.defaults.doc_files) %]</td>
+   <td>[% LxERP.t8('Use File Storage backend') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('WebDAV') %]</td>
+   <td>[% L.yes_no_tag('defaults.doc_webdav', SELF.defaults.doc_webdav) %]</td>
+   <td>[% LxERP.t8('Use WebDAV Storage backend') %]</td>
+  </tr>
+
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Reports") %]</td></tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8('Balance') %]</td>
+   <td>[% L.yes_no_tag('defaults.feature_balance', SELF.defaults.feature_balance) %]</td>
+   <td>[% LxERP.t8('Use Balance Sheet') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('income') %]</td>
+   <td>[% L.yes_no_tag('defaults.feature_eurechnung', SELF.defaults.feature_eurechnung) %]</td>
+   <td>[% LxERP.t8('Use Income') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Erfolgsrechnung') %]</td>
+   <td>[% L.yes_no_tag('defaults.feature_erfolgsrechnung', SELF.defaults.feature_erfolgsrechnung) %]</td>
+   <td>[% LxERP.t8('Use Erfolgsrechnung') %]</td>
+  </tr>
 
   <tr><td class="listheading" colspan="4">[% LxERP.t8("Customer Master Data") %]</td></tr>
 
    <td>[% L.yes_no_tag('defaults.vertreter', SELF.defaults.vertreter) %]</td>
    <td>[% LxERP.t8('Representative for Customer') %]</td>
   </tr>
- <tr>
 <tr>
    <td align="right">[% LxERP.t8('Normalize Customer / Vendor names') %]</td>
    <td>   [% L.yes_no_tag('defaults.normalize_vc_names', SELF.defaults.normalize_vc_names) %]</td>
    <td>[% LxERP.t8('Automatic deletion of leading, trailing and excessive (repetitive) spaces in customer or vendor names') %]</td>
   </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Use text field for greetings') %]</td>
+   <td>   [% L.yes_no_tag('defaults.vc_greetings_use_textfield', SELF.defaults.vc_greetings_use_textfield) %]</td>
+   <td>[% LxERP.t8('Use a text field to enter (new) greetings if enabled. Otherwise, only a drop down box is offered.') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Use text field for title of contacts') %]</td>
+   <td>   [% L.yes_no_tag('defaults.contact_titles_use_textfield', SELF.defaults.contact_titles_use_textfield) %]</td>
+   <td>[% LxERP.t8('Use a text field to enter (new) contact titles if enabled. Otherwise, only a drop down box is offered.') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Use text field for department of contacts') %]</td>
+   <td>   [% L.yes_no_tag('defaults.contact_departments_use_textfield', SELF.defaults.contact_departments_use_textfield) %]</td>
+   <td>[% LxERP.t8('Use a text field to enter (new) contact departments if enabled. Otherwise, only a drop down box is offered.') %]</td>
+  </tr>
 
   <tr>
    <td align="right">[% LxERP.t8('Hourly Rate') %]</td>
    <td>[% LxERP.t8('Default hourly rate for new customers') %]</td>
   </tr>
 
+  <tr>
+   <td align="right">[% LxERP.t8('Customers: VAT ID / taxnumber unique') %]</td>
+   <td>[% L.yes_no_tag('defaults.customer_ustid_taxnummer_unique', SELF.defaults.customer_ustid_taxnummer_unique) %]</td>
+   <td>[% LxERP.t8('Should VAT ID or taxnumber be unique for customers? This is checked when saving a customer\'s master data. One of the fields is sufficient and required.') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Vendors: VAT ID / taxnumber unique') %]</td>
+   <td>[% L.yes_no_tag('defaults.vendor_ustid_taxnummer_unique', SELF.defaults.vendor_ustid_taxnummer_unique) %]</td>
+   <td>[% LxERP.t8('Should VAT ID or taxnumber be unique for all vendors? This is checked when saving a vendor\'s master data. One of the fields is sufficient and required.') %]</td>
+  </tr>
+
   <tr><td class="listheading" colspan="4">[% LxERP.t8("Parts Master Data") %]</td></tr>
 
   <tr>
   </tr>
   <tr>
    <td align="right">[% LxERP.t8('CSS style for pictures') %]</td>
-   <td>   [% L.input_tag('defaults.parts_image_css', SELF.defaults.parts_image_css, style=style) %]</td>
+   <td>   [% L.input_tag('defaults.parts_image_css',SELF.defaults.parts_image_css, style=style) %]</td>
    <td>[% LxERP.t8('Style the picture with the following CSS code') %]</td>
   </tr>
+  <tr>
+    <td align="right">[% LxERP.t8('If item not found, allow creation of new item') %]</td>
+    <td>[% L.yes_no_tag('defaults.create_part_if_not_found', SELF.defaults.create_part_if_not_found) %]</td>
+    <td>[% LxERP.t8('If searching a part from a document and no part is found then offer to create a new part.') %]</td>
+  </tr>
  <tr>
    <td align="right">[% LxERP.t8('Normalize part description and part notes') %]</td>
    <td>   [% L.yes_no_tag('defaults.normalize_part_descriptions', SELF.defaults.normalize_part_descriptions) %]</td>
    <td>[% LxERP.t8('Automatic deletion of leading, trailing and excessive (repetitive) spaces in part description and part notes. Affects the CSV import as well.') %]</td>
-  </tr>
-</tr>
+ </tr>
+ <tr>
+   <td align="right">[% LxERP.t8('Partsgroup is required for parts') %]</td>
+   <td>   [% L.yes_no_tag('defaults.partsgroup_required', SELF.defaults.partsgroup_required) %]</td>
+   <td>[% LxERP.t8('If enabled, when saving parts the partsgroup must be not be empty.') %]</td>
+ </tr>
 
   <tr><td class="listheading" colspan="4">[% LxERP.t8("Purchasing & Sales") %]</td></tr>
 
    <td>[% LxERP.t8("If enabled a column will be shown in sales and purchase orders that lists both the amount and the value not shipped yet for each item.") %]</td>
   </tr>
 
+  <tr>
+   <td align="right">[% LxERP.t8("Warn before saving orders with duplicate parts (new controller only)") %]</td>
+   <td>[% L.yes_no_tag("defaults.order_warn_duplicate_parts", SELF.defaults.order_warn_duplicate_parts) %]</td>
+   <td>[% LxERP.t8("If enabled a warning will be shown in sales and purchase orders if there are two or more positions of the same part (new controller only).") %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8("Warn before saving orders without a delivery date") %]</td>
+   <td>[% L.yes_no_tag("defaults.order_warn_no_deliverydate", SELF.defaults.order_warn_no_deliverydate) %]</td>
+   <td>[% LxERP.t8("If enabled a warning will be shown in sales and purchase orders if there the delivery date is empty.") %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8("Warn before saving sales orders with missing customer order number (new controller only)") %]</td>
+   <td>[% L.yes_no_tag("defaults.order_warn_no_cusordnumber", SELF.defaults.order_warn_no_cusordnumber) %]</td>
+   <td>[% LxERP.t8("If enabled a warning will be shown in sales delivery orders if the customer order number is missing.") %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8("For sales delivery orders, warn on workflow to invoice if not stocked out") %]</td>
+   <td>[% L.yes_no_tag("defaults.sales_delivery_order_check_stocked", SELF.defaults.sales_delivery_order_check_stocked) %]</td>
+   <td>[% LxERP.t8("If enabled a warning will be shown in sales delivery orders on workflow to invoices if positions are not stocked out.") %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8("For purchase delivery orders, warn on workflow to invoice if not stocked in") %]</td>
+   <td>[% L.yes_no_tag("defaults.purchase_delivery_order_check_stocked", SELF.defaults.purchase_delivery_order_check_stocked) %]</td>
+   <td>[% LxERP.t8("If enabled a warning will be shown in purchase delivery orders on workflow to invoices if positions are not stocked in.") %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8("For sales invoices, warn if invoice has no delivery order as a predecessor") %]</td>
+   <td>[% L.yes_no_tag("defaults.warn_no_delivery_order_for_invoice", SELF.defaults.warn_no_delivery_order_for_invoice ) %]</td>
+   <td>[% LxERP.t8("If enabled a warning will be shown if a sales invoices is created without having a sales delivery order as a predecessor.") %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8("Create sales invoices with Factur-X/ZUGFeRD data") %]</td>
+   <td>[% L.select_tag("defaults.create_zugferd_invoices", SELF.zugferd_settings, default=SELF.defaults.create_zugferd_invoices) %]</td>
+   <td>
+     [% LxERP.t8("If enabled Factur-X/ZUGFeRD conformant sales invoice PDFs will be created.") %]
+     [% LxERP.t8("If the test mode is enabled, the Factur-X/ZUGFeRD invoices will be flagged so that they're only fit to be used for testing purposes.") %]
+   </td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8("Create sales invoices with Swiss QR-bill") %]</td>
+   <td>
+     [% L.select_tag("defaults.create_qrbill_invoices", [ [ 0, LxERP.t8('Do not create QR-bill invoices') ], [ 1, LxERP.t8('Create variant QR-IBAN with QR reference') ], [ 2, LxERP.t8('Create variant IBAN without reference') ] ], default=SELF.defaults.create_qrbill_invoices) %]
+   </td>
+   <td>[% LxERP.t8("If enabled sales invoices created using OpenDocument/OASIS format will include data for Swiss QR-Bill creation.") %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8("Prevent browser's back button in sales invoices") %]</td>
+   <td>[% L.yes_no_tag("defaults.invoice_prevent_browser_back", SELF.defaults.invoice_prevent_browser_back) %]</td>
+   <td>[% LxERP.t8("If enabled try to overrule the brower's back button to prevent double booking of sales invoices.") %]</td>
+  </tr>
+
   <tr><td class="listheading" colspan="4">[% LxERP.t8("E-mail") %]</td></tr>
 
   <tr>
    <td>[% 'Manually sent E-Mails will have their BCC field appended with this address. Will not trigger for employees without the right to send bcc, and will not apply to mails sent by automated jobs.' | $T8 %]</td>
   </tr>
 
+  <tr>
+   <td align="right">[% LxERP.t8('Send a BCC to logged in user?') %]</td>
+   <td>
+     [% L.yes_no_tag('defaults.bcc_to_login', SELF.defaults.bcc_to_login) %]
+   </td>
+   <td>
+     [% LxERP.t8('Send a blind copy of all outgoing emails to current user\'s email address?') %]
+   </td>
+ </tr>
+
   <tr>
    <td align="right">[% LxERP.t8('Email journal') %]</td>
    <td>
    </td>
    <td>[% 'Sent emails can be optionally stored in the database with or without their attachments.' | $T8 %]</td>
   </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Invoice email settings') %]</td>
+   <td>
+     [% L.select_tag('defaults.invoice_mail_settings', [ [ 'cp', LxERP.t8('Contact Person') ],[ 'invoice_mail', LxERP.t8('Invoice email') ],[ 'invoice_mail_cc_cp', LxERP.t8('Invoice email and Contact Person') ]  ], default=SELF.defaults.invoice_mail_settings) %]
+   </td>
+   <td>[% 'The invoice recipient can either be a selected contact person (default) or the email adress set in the master data of the customer. Additionally a contact persons mail and the company\'s invoicing mail can be combined.' | $T8 %]</td>
+  </tr>
 
   <tr><td class="listheading" colspan="4">[% LxERP.t8("Requirement Specs") %]</td></tr>
-
   <tr>
    <td align="right">[% LxERP.t8('Default article for converting into quotations and orders') %]</td>
    <td>
     [% IF SELF.h_unit_name %]
-     [% P.part_picker('defaults.requirement_spec_section_order_part_id', SELF.defaults.requirement_spec_section_order_part_id, convertible_unit=SELF.h_unit_name, style=style) %]
+     [% P.part.picker('defaults.requirement_spec_section_order_part_id', SELF.defaults.requirement_spec_section_order_part_id, convertible_unit=SELF.h_unit_name, style=style) %]
     [% ELSE %]
      [% LxERP.t8("Error: this feature requires that articles with a time-based unit (e.g. 'h' or 'min') exist.") %]
     [% END %]
   <tr><td class="listheading" colspan="4">[% LxERP.t8('Transport and service costs reminder') %]</td></tr>
  <tr>
   <td align="right">[% LxERP.t8('Default transport article number') %]</td>
-  <td>[% L.part_picker('defaults.transport_cost_reminder_article_number_id', SELF.defaults.transport_cost_reminder_article_number_id, style=style) %]</td>
+  <td>[% P.part.picker('defaults.transport_cost_reminder_article_number_id', SELF.defaults.transport_cost_reminder_article_number_id, style=style) %]</td>
    <td>[% LxERP.t8('Before saving a sales order, this article will be checked and a warning is generated.') %]</td>
  </tr>
 
    </td>
  </tr>
 
- </table>
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Projects") %]</td></tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Always save orders with a projectnumber (create new projects)') %]</td>
+   <td>[% L.yes_no_tag('defaults.order_always_project', SELF.defaults.order_always_project) %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Project type') %]</td>
+   <td>[% L.select_tag('defaults.project_type_id', SELF.all_project_types, default=SELF.defaults.project_type_id, title_key='description', with_empty=0, style="width: 200px") %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Project Status') %]</td>
+   <td>[% L.select_tag('defaults.project_status_id', SELF.all_project_statuses, default=SELF.defaults.project_status_id, title_key='description', with_empty=0, style="width: 200px") %]</td>
+  </tr>
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Select Mulit-Item Options") %]</td></tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Show parts longdescription (notes) in select list') %]</td>
+   <td>[% L.yes_no_tag('defaults.show_longdescription_select_item', SELF.defaults.show_longdescription_select_item) %]</td>
+  </tr>
+
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Enabled Quick Searched") %]</td></tr>
+  <tr>
+    <td align="right">[% 'Quick Searches that will be shown in the header in this client' | $T8 %]</td>
+    <td colspan=2>
+      <div class="clearfix">
+       [% L.select_tag("defaults.quick_search_modules[]", SELF.available_quick_search_modules, value_key="name", title_key="description_config", id="defaults_quick_searches", multiple=1, default=SELF.defaults.quick_search_modules) %]
+       [% L.multiselect2side("defaults_quick_searches", labelsx=LxERP.t8("All modules"), labeldx=LxERP.t8("Enabled modules")) %]
+      </div>
+    </td>
+  </tr>
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("SEPA") %]</td></tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Add Customer/Vendor Number as a reference add-on for SEPA export.') %]</td>
+   <td>[% L.yes_no_tag('defaults.sepa_reference_add_vc_vc_id', SELF.defaults.sepa_reference_add_vc_vc_id) %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Set the invoice duedate as the default execution date for SEPA export.') %]</td>
+   <td>[% L.yes_no_tag('defaults.sepa_set_duedate_as_default_exec_date', SELF.defaults.sepa_set_duedate_as_default_exec_date) %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Set the invoice skonto date (if exists) as the default execution date for SEPA export.') %]</td>
+   <td>[% L.yes_no_tag('defaults.sepa_set_skonto_date_as_default_exec_date', SELF.defaults.sepa_set_skonto_date_as_default_exec_date) %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('In addition to the above date functions, subtract the following amount of days from the calculated date as a buffer.') %]</td>
+   <td>[% L.input_tag('defaults.sepa_set_skonto_date_buffer_in_days', LxERP.format_amount(SELF.defaults.sepa_set_skonto_date_buffer_in_days, 0), style=style) %]</td>
+  </tr>
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Experimental Features") %]</td></tr>
+  <tr>
+   <td align="right">[% LxERP.t8('new order controller') %]</td>
+   <td>[% L.yes_no_tag('defaults.feature_experimental_order', SELF.defaults.feature_experimental_order) %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Assortment') %]</td>
+   <td>[% L.yes_no_tag('defaults.feature_experimental_assortment', SELF.defaults.feature_experimental_assortment) %]</td>
+  </tr>
+ <tr><td class="listheading" colspan="4">[% 'Displayable Name Preferences' | $T8 %]</td></tr>
+ [% FOREACH module=SELF.displayable_name_specs_by_module.keys.sort %]
+ [%- SET spec=SELF.displayable_name_specs_by_module.$module -%]
+  <tr>
+    <td align="right">[% spec.specs.title %]</td>
+    <td>
+      <table>
+        <tr>
+          <th align="left" class="listheading">[% 'Option' | $T8 %]</th>
+          <th align="left" class="listheading">[% 'Name'   | $T8 %]</th>
+        </tr>
+        [% FOREACH option=spec.specs.options %]
+        <tr>
+          <td>[% option.title %]</td>
+          <td>[% option.name  %]</td>
+        </tr>
+        [% END %]
+        <tr>
+          <th align="left">[% 'Display' | $T8 %]:</th>
+          <td>
+            [% L.hidden_tag("displayable_name_specs[+].module", module) %]
+            [% L.input_tag("displayable_name_specs[].default", spec.prefs.get_default, size=50) %]
+          </td>
+        </tr>
+      </table>
+    </td>
+    [% IF loop.first %]
+    <td>[% 'The display of (mainly) picker results can be configured. To insert the value of one option use <%Name%>.' | $T8 %]<br>
+        [% 'E.g. "<%customernumber%> <%name%>"' | $T8 %]
+    </td>
+    [% END %]
+  </tr>
+  [% END %]
+
+
+</table>
 </div>
index 0b04012..752bcd8 100644 (file)
@@ -2,7 +2,7 @@
 [% SET style="width: 400px" %]
 <div id="miscellaneous">
  <table>
-  <tr><td class="listheading" colspan="4">[% LxERP.t8("Company settings") %]</td></tr>
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Company name and address") %]</td></tr>
 
   <tr>
    <td align="right">[% LxERP.t8("Company name") %]</td>
   </tr>
 
   <tr>
-   <td align="right" valign="top">[% LxERP.t8("Address") %]</td>
-   <td valign="top">[% L.textarea_tag('defaults.address', SELF.defaults.address, style=style, rows=4) %]</td>
+   <td align="right" valign="top">[% LxERP.t8("Street 1") %]</td>
+   <td>[% L.input_tag('defaults.address_street1', SELF.defaults.address_street1, style=style) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right" valign="top">[% LxERP.t8("Street 2") %]</td>
+   <td>[% L.input_tag('defaults.address_street2', SELF.defaults.address_street2, style=style) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right" valign="top">[% LxERP.t8("Zipcode and city") %]</td>
+   <td>
+     [% L.input_tag('defaults.address_zipcode', SELF.defaults.address_zipcode, size=8) %]
+     [% L.input_tag('defaults.address_city', SELF.defaults.address_city, size=30) %]
+   </td>
+  </tr>
+
+  <tr>
+   <td align="right" valign="top">[% LxERP.t8("Country") %]</td>
+   <td>[% L.input_tag('defaults.address_country', SELF.defaults.address_country, style=style) %]</td>
   </tr>
 
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Company settings") %]</td></tr>
+
   <tr>
    <td align="right" valign="top">[% LxERP.t8("Signature") %]</td>
-   <td valign="top">[% L.textarea_tag('defaults.signature', SELF.defaults.signature, style=style, rows=4) %]</td>
+   <td valign="top">[% L.textarea_tag('defaults.signature', SELF.defaults.signature, style=style, rows=4, class='texteditor') %]</td>
   </tr>
 
   <tr>
    <td align="right">[% LxERP.t8("Tax number") %]</td>
+   [% IF SELF.defaults.feature_ustva %]
+   <td>[% SELF.defaults.taxnumber %]&nbsp;&nbsp;<a href="ustva.pl?action=config_step1">([% LxERP.t8("For changeing goto USTVA Config") %])</a></td>
+   [% ELSE %]
    <td>[% L.input_tag('defaults.taxnumber', SELF.defaults.taxnumber, style=style) %]</td>
+   [% END %]
   </tr>
 
   <tr>
    </td>
   </tr>
 
+  <tr>
+   <td align="right">[% LxERP.t8('Interpolate variables in texts of positions') %]</td>
+   <td>[% L.yes_no_tag('defaults.print_interpolate_variables_in_positions', SELF.defaults.print_interpolate_variables_in_positions) %]</td>
+   <td>[% LxERP.t8('Whether or not to replace variable placeholders such as "<%invdate%>" in texts in positions such as the part description by the record\'s actual value') %]</td>
+  </tr>
+
   <tr><td class="listheading" colspan="4">[% LxERP.t8("Currencies") %]</td></tr>
 
   <tr>
   </tr>
 
 [% FOREACH currency = SELF.all_currencies %]
-  [% L.hidden_tag("currencies[+].id", currency.id) %]
   <tr>
-   <td align="right">[% IF loop.count == 1 %][% LxERP.t8("Currencies") %][% END %]</td>
+   <td align="right">
+     [% L.hidden_tag("currencies[+].id", currency.id) %]
+     [% IF loop.count == 1 %][% LxERP.t8("Currencies") %][% END %]
+   </td>
    <td>[% L.input_tag("currencies[].name", currency.name, style=style) %]</td>
    <td align="center">[% L.radio_button_tag('defaults.currency_id', value=currency.id, id='defaults.currency_id_' _ currency.id, checked=(SELF.defaults.currency_id == currency.id)) %]</td>
    <td>[% IF loop.count == 1 %][% LxERP.t8("Edit the currency names in order to rename them.") %][%- END %]</td>
 [% END %]
  </table>
 </div>
-</div>
index 4f2d31d..0235cc3 100644 (file)
    <td>[% L.select_tag('defaults.gl_changeable', SELF.posting_options, value_key = 'value', title_key = 'title', default = SELF.defaults.gl_changeable) %]</td>
    <td>[% LxERP.t8('Should gl transactions be and when should they be changeable or deleteable after posting?') %]</td>
   </tr>
-
   <tr> </tr>
   <tr> </tr>
-
   <tr>
    <td align="right">[% LxERP.t8('Payments Changeable') %]</td>
    <td>[% L.select_tag('defaults.payments_changeable', SELF.payment_options, value_key = 'value', title_key = 'title', default = SELF.defaults.payments_changeable) %]</td>
    <td>[% LxERP.t8('Should payments be and when should they be changeable after posting?') %]</td>
   </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Record numbers changeable') %]</td>
+   <td>[% L.yes_no_tag('defaults.sales_purchase_record_numbers_changeable', SELF.defaults.sales_purchase_record_numbers_changeable) %]</td>
+   <td>[% LxERP.t8('If disabled, record numbers for sales records & purchase records produced by our side will always be auto-generated and cannot be changed later.') %]</td>
+  </tr>
+
+  <tr> </tr>
+  <tr> </tr>
+
+  <tr>
+   <td align="right">[% LxERP.t8('Add document for Purchase invoices') %]</td>
+   <td>[% L.yes_no_tag('defaults.ir_add_doc', SELF.posting_options, value_key = 'value', title_key = 'title', default = SELF.defaults.ir_add_doc) %]</td>
+   <td>[% LxERP.t8('Show document tab after posting?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Add document for AR transactions') %]</td>
+   <td>[% L.yes_no_tag('defaults.ar_add_doc', SELF.posting_options, value_key = 'value', title_key = 'title', default = SELF.defaults.ar_add_doc) %]</td>
+   <td>[% LxERP.t8('Do not leave booking form?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Add document for AP transactions') %]</td>
+   <td>[% L.yes_no_tag('defaults.ap_add_doc', SELF.posting_options, value_key = 'value', title_key = 'title', default = SELF.defaults.ap_add_doc) %]</td>
+   <td>[% LxERP.t8('Show document tab after posting?') %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Add document for GL transactions') %]</td>
+   <td>[% L.yes_no_tag('defaults.gl_add_doc', SELF.posting_options, value_key = 'value', title_key = 'title', default = SELF.defaults.gl_add_doc) %]</td>
+   <td>[% LxERP.t8('Show document tab after posting?') %]</td>
+  </tr>
 
   <tr> </tr>
   <tr> </tr>
    <td>[% L.select_tag('defaults.balance_startdate_method', SELF.balance_startdate_method_options, value_key = 'value', title_key = 'title', default = SELF.defaults.balance_startdate_method) %]</td>
    <td>[% LxERP.t8('This option controls the method used for determining the startdate for the balance report.') %]</td>
   </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Set valid until date for Sales Quotation') %]</td>
+   <td>[% L.yes_no_tag('defaults.reqdate_on', SELF.defaults.reqdate_on) %]</td>
+   <td>[% LxERP.t8("If set to no the 'valid until' field for sales quotation won't be set at all.") %]</td>
+  </tr>
   <tr>
    <td align="right">[% LxERP.t8('Sales Quotation valid interval') %]</td>
    <td>[% L.input_tag('defaults.reqdate_interval', LxERP.format_amount(SELF.defaults.reqdate_interval, 0), style=style) %]</td>
    <td>[% LxERP.t8('Usually the sales quotation is valid until the next working day. If a value is set here then the quotation will be valid for at least that many days. The resulting date will be adjusted to the next working day if it ends up on a weekend.') %]</td>
   </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Set delivery date for Sales Orders') %]</td>
+   <td>[% L.yes_no_tag('defaults.deliverydate_on', SELF.defaults.deliverydate_on) %]</td>
+   <td>[% LxERP.t8("If set to no the 'delivery date' field for sales orders won't be set at all.") %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Sales Order delivery date interval') %]</td>
+   <td>[% L.input_tag('defaults.delivery_date_interval', LxERP.format_amount(SELF.defaults.delivery_date_interval, 0), style=style) %]</td>
+   <td>[% LxERP.t8('Usually the delivery date of an order is the next working day. If a value is set here this value will be added to the delivery date of the sales order. The resulting date will be adjusted to the next working day if it ends up on a weekend.') %]</td>
+  </tr>
  </table>
 </div>
index 51b11b9..90c0751 100644 (file)
   <tr>
    <td align="right" nowrap>[% LxERP.t8('Last Sales Delivery Order Number') %]</td>
    <td>[% L.input_tag("defaults.sdonumber", SELF.defaults.sdonumber, size="15") %]</td>
+   [%- IF INSTANCE_CONF.get_feature_experimental_assortment %]
+   <td align="right" nowrap>[% LxERP.t8('Last Assortment Number') %]</td>
+   <td>[% L.input_tag("defaults.assortmentnumber", SELF.defaults.assortmentnumber, size="15") %]</td>
+   [%- END -%]
   </tr>
 
   <tr>
diff --git a/templates/webpages/client_config/_record_links.html b/templates/webpages/client_config/_record_links.html
new file mode 100644 (file)
index 0000000..5dd49f6
--- /dev/null
@@ -0,0 +1,12 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%]
+<div id="record_links">
+ <table>
+  <tr>
+   <td align="right" nowrap="true">[% LxERP.t8('View record links from Sales Order') %]</td>
+   <td>[% L.yes_no_tag('defaults.always_record_links_from_order', SELF.defaults.always_record_links_from_order) %]</td>
+   <td>
+    [% LxERP.t8('If enabled the record links view starts always from the sales order including all sublevels') %]<br>
+   </td>
+  </tr>
+ </table>
+</div>
diff --git a/templates/webpages/client_config/_stocktaking.html b/templates/webpages/client_config/_stocktaking.html
new file mode 100644 (file)
index 0000000..d95d2a5
--- /dev/null
@@ -0,0 +1,48 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%]
+<div id="stocktaking">
+ <table>
+  <tr>
+   <td align="right" nowrap="true">[% LxERP.t8('Preselected warehouse') %]</td>
+   <td>
+     [% L.select_tag('defaults.stocktaking_warehouse_id',
+                     SELF.all_warehouses,
+                     id='stocktaking_warehouse_id',
+                     with_empty=1,
+                     default=SELF.defaults.stocktaking_warehouse_id,
+                     title_key='description',
+                     onchange="warehouse_selected(this.selectedIndex == 0 ? -1 : warehouses[this.selectedIndex - 1].id, -1 ,'stocktaking_bin_id')") %]
+   </td>
+   <td>
+    [% LxERP.t8('If configured this warehouse will be preselected for stocktaking.') %]<br>
+   </td>
+  </tr>
+
+  <tr>
+   <td align="right" nowrap="true">[% LxERP.t8('Preselected bin') %]</td>
+   <td>[% L.select_tag('defaults.stocktaking_bin_id', [], id='stocktaking_bin_id', with_empty=1) %]</td>
+   <td>
+    [% LxERP.t8('If configured this bin will be preselected for stocktaking.') %]<br>
+   </td>
+  </tr>
+  <tr>
+
+  <tr>
+   <td align="right" nowrap="true">[% LxERP.t8('Preselected cutoff date') %]</td>
+   <td>[% L.date_tag('defaults.stocktaking_cutoff_date', SELF.defaults.stocktaking_cutoff_date) %]</td>
+   <td>
+    [% LxERP.t8('If configured this date will used as preselected cutoff date for stocktaking.') %]<br>
+   </td>
+  </tr>
+  <tr>
+
+  <tr>
+   <td align="right" nowrap="true">[% LxERP.t8('Threshold for warning on quantity difference') %]</td>
+   <td>[% L.input_tag('defaults.stocktaking_qty_threshold_as_number', SELF.defaults.stocktaking_qty_threshold_as_number, size=15, class="numeric") %]</td>
+   <td>
+    [% LxERP.t8('If the counted quantity differs more than this threshold from the quantity in the database, a warning will be shown. Set to 0 to switch of this feature.') %]<br>
+   </td>
+  </tr>
+  <tr>
+
+ </table>
+</div>
index 89faba5..60199fc 100644 (file)
    [% LxERP.t8('Transfer out all items of a sales invoice when posting it. Items are transfered out acording to the settings above.') %]
    </td>
   </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Match Sales Invoice Serial numbers with inventory charge numbers?') %]</td>
+   <td>
+    [% L.yes_no_tag('defaults.sales_serial_eq_charge', SELF.defaults.sales_serial_eq_charge) %]
+   </td>
+   <td>
+   [% LxERP.t8('If one or more space separated serial numbers are assigned in a sales invoice, match the charge number of the inventory item. Assumes that Serial Number and Charge Number have 1:1 relation. Otherwise throw a error message for the default sales invoice transfer.') %]
+   </td>
+  </tr>
   <tr><td colspan="3"><hr /></td></tr>
   <tr>
   <tr>
     [% LxERP.t8('Any stock contents containing a best before date will be impossible to stock out otherwise.') %]
    </td>
   </tr>
-  <tr><td colspan="3"><hr /></td></tr>
- <tr>
-  <td align="right">[% LxERP.t8('Delivery Plan check for transferred delivery orders') %]</td>
+  <tr>
+   <td align="right">[% LxERP.t8('Undo Transfer Interval') %]</td>
+   <td>[% L.input_tag('defaults.undo_transfer_interval', LxERP.format_amount(SELF.defaults.undo_transfer_interval, 0), style=style) %]</td>
+   <td>[% LxERP.t8('Defines the interval where undoing transfers from a delivery order are allowed.') %]</td>
+  </tr>
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Produce Assembly Configuration") %]</td></tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Assembly creation warehouse dependent') %]</td>
+   <td>
+    [% L.yes_no_tag('defaults.produce_assembly_same_warehouse', SELF.defaults.produce_assembly_same_warehouse) %]
+   </td>
+   <td>
+    [% LxERP.t8('Produce assembly only if all parts are in the same warehouse') %]
+   </td>
+  </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Assembly creation transfers services') %]</td>
+   <td>
+    [% L.yes_no_tag('defaults.produce_assembly_transfer_service', SELF.defaults.produce_assembly_transfer_service) %]
+   </td>
+   <td>
+    [% LxERP.t8('Produce assembly consumes services if assigned as a assembly item') %]
+   </td>
+  </tr>
+  <tr><td class="listheading" colspan="4">[% LxERP.t8("Services in Delivery Orders") %]</td></tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Sales Orders Services are deliverable') %]</td>
    <td>
-    [% L.yes_no_tag('defaults.delivery_plan_calculate_transferred_do', SELF.defaults.delivery_plan_calculate_transferred_do) %]
+    [% L.yes_no_tag('defaults.sales_delivery_order_check_service', SELF.defaults.sales_delivery_order_check_service) %]
    </td>
    <td>
-   [% LxERP.t8('The default delivery value report only checks if all delivery orders have been created not if the goods are transferred. This feature will check if all the goods are transferred. Caveat: Only the state of the delivery orders are checked not partial transferred delivery orders (in technical terms: the table inventory is not checked') %]
+    [% LxERP.t8('Ignore services for the sales orders state of delivery') %]
    </td>
   </tr>
+  <tr>
+   <td align="right">[% LxERP.t8('Purchase Orders Services are deliverable') %]</td>
+   <td>
+    [% L.yes_no_tag('defaults.purchase_delivery_order_check_service', SELF.defaults.purchase_delivery_order_check_service) %]
+   </td>
+   <td>
+    [% LxERP.t8('Ignore services for the purchase orders state of delivery') %]
+   </td>
+  </tr>
+ <tr><td class="listheading" colspan="4">[% LxERP.t8("Shipped Quantity Algorithm") %]</td></tr>
+ <tr>
+  <td align="right">[% LxERP.t8('Require stock out to consider a delivery order position delivered?') %]</td>
+  <td>[% L.yes_no_tag('defaults.shipped_qty_require_stock_out', SELF.defaults.shipped_qty_require_stock_out) %]</td>
+  <td>[% LxERP.t8('If yes, delivery order positions are considered "delivered" only if they have been stocked out of the inventory. Otherwise saving the delivery order is considered delivered.') %]</td>
+ </tr>
  </table>
 </div>
index 0f85512..79c2cdd 100644 (file)
@@ -38,30 +38,62 @@ function enable_template_controls() {
   $('#new_templates,#new_master_templates').prop('disabled', existing);
 }
 
+function verifyMBSize(elem) {
+  var fsize = parseInt($('#doc_max_filesize_MB').val());
+  $('#defaults_doc_max_filesize').val(fsize*1000000.0);
+  $('#doc_max_filesize_MB').val(fsize);
+}
+
+function verifyRootPath(elem) {
+  if ( elem.value == "" ) {
+    elem.value="./documents";
+  }
+}
+
+function checkavailable_filebackend(elem) {
+  var selval = elem.value;
+  if ( selval == 'Webdav' && $("#defaults_doc_webdav").val() == 0 ) {
+     elem.value = 'Filesystem';
+  }
+  if ( elem.value == 'Filesystem' && $("#defaults_doc_files").val() == 0 ) {
+     elem.value = 'None';
+  }
+  return false;
+}
+
 $(function() {
   warehouse_selected([% SELF.defaults.warehouse_id || -1 %], [% SELF.defaults.bin_id || -1 %], 'bin_id');
   warehouse_selected([% SELF.defaults.warehouse_id_ignore_onhand || -1 %], [% SELF.defaults.bin_id_ignore_onhand || -1 %], 'bin_id_ignore_onhand');
+  warehouse_selected([% SELF.defaults.stocktaking_warehouse_id || -1 %], [% SELF.defaults.stocktaking_bin_id || -1 %], 'stocktaking_bin_id');
 
   enable_template_controls();
+  $('#doc_max_filesize_MB').val(parseInt($('#defaults_doc_max_filesize').val())/1000000.0);
   $('#use_templates_existing,#use_templates_new').change(enable_template_controls);
 })
     -->
  </script>
 <h1>[% title | html %]</h1>
 
-[% PROCESS 'common/flash.html' %]
+[% INCLUDE 'common/flash.html' %]
 
-<form action='controller.pl' method='POST'>
+<form action='controller.pl' method='POST' id='form'>
  <div class="tabwidget">
   <ul>
    <li><a href="#miscellaneous">[% LxERP.t8('Miscellaneous') %]</a></li>
    <li><a href="#ranges_of_numbers">[% LxERP.t8('Ranges of numbers') %]</a></li>
    <li><a href="#default_accounts">[% LxERP.t8('Default Accounts') %]</a></li>
    <li><a href="#posting_configuration">[% LxERP.t8('Posting Configuration') %]</a></li>
-   <li><a href="#datev_check_configuration">[% LxERP.t8('DATEV check configuration') %]</a></li>
+   [% IF FORM.feature_datev %]
+     <li><a href="#datev_check_configuration">[% LxERP.t8('DATEV configuration') %]</a></li>
+   [% END %]
    <li><a href="#orders_deleteable">[% LxERP.t8('Orders / Delivery Orders deleteable') %]</a></li>
+[%- IF INSTANCE_CONF.get_doc_storage %]
+   <li><a href="#attachments">[% LxERP.t8('Global Attachments') %]</a></li>
+[%- END %]
    <li><a href="#warehouse">[% LxERP.t8('Warehouse') %]</a></li>
    <li><a href="#features">[% LxERP.t8('Features') %]</a></li>
+   <li><a href="#stocktaking">[% LxERP.t8('Stocktaking') %]</a></li>
+   <li><a href="#record_links">[% LxERP.t8('Linked Records') %]</a></li>
   </ul>
 
 [% PROCESS 'client_config/_ranges_of_numbers.html' %]
@@ -69,13 +101,13 @@ $(function() {
 [% PROCESS 'client_config/_posting_configuration.html' %]
 [% PROCESS 'client_config/_datev_check_configuration.html' %]
 [% PROCESS 'client_config/_orders_deleteable.html' %]
+[%- IF INSTANCE_CONF.get_doc_storage %]
+[% PROCESS 'client_config/_attachments.html' %]
+[%- END %]
 [% PROCESS 'client_config/_warehouse.html' %]
 [% PROCESS 'client_config/_features.html' %]
+[% PROCESS 'client_config/_stocktaking.html' %]
+[% PROCESS 'client_config/_record_links.html' %]
 [% PROCESS 'client_config/_miscellaneous.html' %]
-
- <div>
-  [%- L.hidden_tag('action',  'ClientConfig/dispatch')  %]
-  [%- L.submit_tag('action_save',  LxERP.t8('Save'))  %]
  </div>
-
 </form>
diff --git a/templates/webpages/common/_print_dialog.html b/templates/webpages/common/_print_dialog.html
new file mode 100644 (file)
index 0000000..ffad490
--- /dev/null
@@ -0,0 +1,7 @@
+[%- USE LxERP -%][%- USE L -%]
+<div id="print_dialog_print_options"></div>
+
+<p>
+ [% L.button_tag("kivi.SalesPurchase.print_record()", LxERP.t8("Print"), id="print_dialog_print_button") %]
+ [% L.button_tag("\$('#print_dialog').dialog('close');", LxERP.t8("Abort"), id="print_dialog_abort_button") %]
+</p>
diff --git a/templates/webpages/common/_send_email_dialog.html b/templates/webpages/common/_send_email_dialog.html
new file mode 100644 (file)
index 0000000..487943c
--- /dev/null
@@ -0,0 +1,141 @@
+[%- USE HTML %][%- USE LxERP -%][%- USE L -%][%- USE P -%]
+[%- SET have_files = 0 %]
+
+[% BLOCK attach_file_list %]
+  [% IF files.as_list.size %]
+   [% SET have_files = 1;
+      FOREACH file = files.as_list %]
+    <tr>
+     <th align="right" nowrap>
+      [% IF loop.first %]
+       [% label %]
+      [% END %]
+     </th>
+     <td>
+      [% IF checked %]
+        [% P.checkbox_tag("email_form.attach_file_ids[]", label=file.db_file.file_name, value=file.db_file.id, checked="1") %]
+      [% ELSE %]
+        [% P.checkbox_tag("email_form.attach_file_ids[]", label=file.db_file.file_name, value=file.db_file.id, checked="0") %]
+      [% END %]
+     </td>
+    </tr>
+   [% END %]
+  [% END %]
+[% END %]
+
+<table>
+ <tbody>
+  <tr>
+   <th align="right" nowrap>
+    [% IF is_invoice_mail  %]
+      [% LxERP.t8("Invoice to:") %]
+    [% ELSE %]
+      [% LxERP.t8("Recipients") %]
+    [% END %]
+   </th>
+   <td>
+    [% L.input_tag("email_form.to", email_form.to, size="80",readonly=is_invoice_mail ) %]
+    <span class="interactive cursor-pointer"        onclick="$('[data-toggle-recipients=1]').toggle()" data-toggle-recipients="1">[+]</span>
+    <span class="interactive cursor-pointer hidden" onclick="$('[data-toggle-recipients=1]').toggle()" data-toggle-recipients="1">[-]</span>
+   </td>
+  </tr>
+
+  [%- IF ALL_PARTNER_EMAIL_ADDRESSES.size %]
+   [%- FOREACH email = ALL_PARTNER_EMAIL_ADDRESSES %]
+    <tr class="hidden" data-toggle-recipients="1">
+     <th align="right" nowrap>
+      [%- IF loop.first %]
+       [% LxERP.t8("Other recipients") %]
+      [%- END %]
+     </th>
+     <td>
+       [% P.checkbox_tag("email_form.additional_to[]", label=email, value=email, checked="0") %]
+     </td>
+    </tr>
+   [%- END %]
+  [%- END %]
+
+ [%- IF ALL_EMPLOYEES.size %]
+  <tr class="hidden" data-toggle-recipients="1">
+   <th align="right" nowrap>[% LxERP.t8("CC to Employee") %]</th>
+   <td>[% L.select_tag('email_form.cc_employee', ALL_EMPLOYEES, value_key='login' title_key='safe_name', with_empty=1, style=style) %]</td>
+  </tr>
+ [%- END %]
+
+  <tr class="hidden" data-toggle-recipients="1">
+   <th align="right" nowrap>[% LxERP.t8("Cc") %]</th>
+   <td>[% L.input_tag("email_form.cc", email_form.cc, size="80") %]</td>
+  </tr>
+
+ [%- IF show_bcc %]
+  <tr class="hidden" data-toggle-recipients="1">
+   <th align="right" nowrap>[% LxERP.t8("Bcc") %]</th>
+   <td>[% L.input_tag("email_form.bcc", email_form.bcc, size="80") %]</td>
+  </tr>
+ [%- END %]
+
+  <tr>
+   <th align="right" nowrap>[% LxERP.t8("Subject") %]</th>
+   <td>[% L.input_tag("email_form.subject", email_form.subject, size="80") %]</td>
+  </tr>
+
+  <tr valign="top">
+   <th align="right" nowrap>[% LxERP.t8("Message") %]
+    <sup> [% L.link("generictranslations.pl?action=edit_email_strings", "1)", title=LxERP.t8('Tired of copying always nice phrases for this message? Click here to use the new preset message option!'), target="_blank") %]</sup>
+  </th>
+   <td>[% L.textarea_tag("email_form.message", email_form.message, rows="15", cols="80", class="texteditor texteditor-space-for-toolbar") %]</td>
+  </tr>
+
+[% IF INSTANCE_CONF.get_doc_storage %]
+  <tr>
+   <th align="right" nowrap>[% LxERP.t8("Send printout of record") %]</th>
+   <td>
+    [% SET no_file_label = have_files ? LxERP.t8("Don't include a printout of the record with the email, only selected files") : LxERP.t8("Don't include a printout of the record with the email") ;
+           options       = [
+             [ "old_file", LxERP.t8("Send the last or create the first version for this record") ],
+             [ "normal",   LxERP.t8("Create and send a new printout for this record") ],
+             [ "no_file",  no_file_label ],
+           ] ;
+       L.select_tag("email_form.attachment_policy", options, onchange="kivi.SalesPurchase.activate_send_email_actions_regarding_printout()") %]
+   </td>
+  </tr>
+[% END %]
+
+  <tr>
+   <th align="right" nowrap>
+[% IF !INSTANCE_CONF.get_doc_storage %]
+    [% LxERP.t8("Attachment name") %]
+[% END %]
+   </th>
+   <td>[% L.input_tag("email_form.attachment_filename", email_form.attachment_filename, size="80") %]</td>
+  </tr>
+
+[% IF INSTANCE_CONF.get_doc_storage %]
+  [% PROCESS attach_file_list
+             files = FILES.files
+             checked = INSTANCE_CONF.get_email_attachment_record_files_checked
+             label = LxERP.t8("Record's files") %]
+
+  [% PROCESS attach_file_list
+             files = FILES.vc_files
+             checked = INSTANCE_CONF.get_email_attachment_vc_files_checked
+             label = is_customer ? LxERP.t8("Files from customer") : LxERP.t8("Files from vendor") %]
+
+  [% PROCESS attach_file_list
+             files = FILES.part_files
+             checked = INSTANCE_CONF.get_email_attachment_part_files_checked
+             label = LxERP.t8("Files from parts") %]
+
+  [% PROCESS attach_file_list
+             files = FILES.project_files
+             label = LxERP.t8("Files from projects") %]
+[% END %]
+ </tbody>
+</table>
+
+<div id="email_form_print_options"></div>
+
+<p>
+ [% L.button_tag(email_form.js_send_function, LxERP.t8("Send email"), id='send_email') %]
+ [% L.button_tag("\$('#send_email_dialog').dialog('close');", LxERP.t8("Abort")) %]
+</p>
diff --git a/templates/webpages/common/_ship_to_dialog.html b/templates/webpages/common/_ship_to_dialog.html
new file mode 100644 (file)
index 0000000..cb21c4f
--- /dev/null
@@ -0,0 +1,164 @@
+[% USE HTML %][% USE L %][% USE LxERP %][%- USE JavaScript -%]
+
+<script type="text/javascript">
+  $(function() {
+    kivi.SalesPurchase.shipto_addresses = [
+      { shiptoname:         "[% JavaScript.escape(vc_obj.name) %]",
+        shiptodepartment_1: "[% JavaScript.escape(vc_obj.department_1) %]",
+        shiptodepartment_2: "[% JavaScript.escape(vc_obj.department_2) %]",
+        shiptostreet:       "[% JavaScript.escape(vc_obj.street) %]",
+        shiptozipcode:      "[% JavaScript.escape(vc_obj.zipcode) %]",
+        shiptocity:         "[% JavaScript.escape(vc_obj.city) %]",
+        shiptocountry:      "[% JavaScript.escape(vc_obj.country) %]",
+        shiptogln:          "[% JavaScript.escape(vc_obj.gln) %]",
+        shiptocontact:      "[% JavaScript.escape(vc_obj.contact) %]",
+        shiptophone:        "[% JavaScript.escape(vc_obj.phone) %]",
+        shiptofax:          "[% JavaScript.escape(vc_obj.fax) %]",
+        shiptoemail:        "[% JavaScript.escape(vc_obj.email) %]"
+      [% FOREACH var = cvars %]
+        , "shiptocvar_[% JavaScript.escape(var.config.name) %]": ""
+      [% END %]
+      }
+
+    [% FOREACH shipto = vc_obj.shipto %]
+      ,
+      { shiptoname:         "[% JavaScript.escape(shipto.shiptoname) %]",
+        shiptodepartment_1: "[% JavaScript.escape(shipto.shiptodepartment_1) %]",
+        shiptodepartment_2: "[% JavaScript.escape(shipto.shiptodepartment_2) %]",
+        shiptostreet:       "[% JavaScript.escape(shipto.shiptostreet) %]",
+        shiptozipcode:      "[% JavaScript.escape(shipto.shiptozipcode) %]",
+        shiptocity:         "[% JavaScript.escape(shipto.shiptocity) %]",
+        shiptocountry:      "[% JavaScript.escape(shipto.shiptocountry) %]",
+        shiptogln:          "[% JavaScript.escape(shipto.shiptogln) %]",
+        shiptocontact:      "[% JavaScript.escape(shipto.shiptocontact) %]",
+        shiptophone:        "[% JavaScript.escape(shipto.shiptophone) %]",
+        shiptofax:          "[% JavaScript.escape(shipto.shiptofax) %]",
+        shiptoemail:        "[% JavaScript.escape(shipto.shiptoemail) %]"
+      [% FOREACH var = shipto.cvars_by_config %]
+        , "shiptocvar_[% JavaScript.escape(var.config.name) %]": "[% JavaScript.escape(var.value_as_text) %]"
+      [% END %]
+      }
+    [% END %]
+    ];
+  });
+</script>
+
+[% select_options = [ [ 0, LxERP.t8("Billing Address") ] ] ;
+   FOREACH shipto = vc_obj.shipto ;
+     tmpcity  = shipto.shiptozipcode _ ' ' _ shipto.shiptocity ;
+     tmptitle = [ shipto.shiptoname, shipto.shiptostreet, tmpcity ] ;
+     CALL select_options.import([ [ loop.count, tmptitle.grep('\S').join("; ") ] ]) ;
+   END ;
+   '' %]
+
+<p>
+ [% LxERP.t8("Copy address from master data") %]:
+ [% L.select_tag("", select_options, id="shipto_to_copy", style="width: 300px") %]
+ [% L.button_tag("kivi.SalesPurchase.copy_shipto_address()", LxERP.t8("Copy")) %]
+</p>
+
+[% IF cs_obj ;
+  fields = ['shiptoname', 'shiptodepartment_1', 'shiptodepartment_2',
+            'shiptostreet', 'shiptozipcode', 'shiptocity', 'shiptocountry',
+            'shiptogln', 'shiptocontact', 'shiptocp_gender', 'shiptophone',
+            'shiptofax', 'shiptoemail'] ;
+  FOREACH field = fields ;
+      $field = cs_obj.$field ;
+  END ;
+END ;
+'' %]
+
+
+<table>
+ <tr class="listheading">
+  <th></th>
+  <th>[% LxERP.t8('Billing Address') %]</th>
+  <th>[% LxERP.t8('Shipping Address') %]</th>
+ </tr>
+ <tr height="5"></tr>
+ <tr>
+  <th align="right" nowrap>[%- IF vc == "customer" %][%- LxERP.t8('Customer Number') %][%- ELSE %][%- LxERP.t8('Vendor Number') %][%- END %]</th>
+  <td>[%- IF vc == "customer" %][%- HTML.escape(vc_obj.customernumber) %][%- ELSE %][%- HTML.escape(vc_obj.vendornumber) %][%- END %]</td>
+ </tr>
+ <tr>
+  <th align="right" nowrap>[% LxERP.t8('Company Name') %]</th>
+  <td>[% HTML.escape(vc_obj.name) %]</td>
+  <td>[% L.input_tag("shiptoname", shiptoname, "size", "35") %]</td>
+ </tr>
+ <tr>
+  <th align="right" nowrap>[% LxERP.t8('Department') %]</th>
+  <td>[% HTML.escape(vc_obj.department_1) %]</td>
+  <td>[% L.input_tag("shiptodepartment_1", shiptodepartment_1, "size", "35") %]</td>
+ </tr>
+ <tr>
+  <th align="right" nowrap>&nbsp;</th>
+  <td>[% HTML.escape(vc_obj.department_2) %]</td>
+  <td>[% L.input_tag("shiptodepartment_2", shiptodepartment_2, "size", "35") %]</td>
+ </tr>
+ <tr>
+  <th align="right" nowrap>[% LxERP.t8('Street') %]</th>
+  <td>[% HTML.escape(vc_obj.street) %]</td>
+  <td>[% L.input_tag("shiptostreet", shiptostreet, "size", "35") %]</td>
+ </tr>
+ <tr>
+  <th align="right" nowrap>[% LxERP.t8('Zipcode') %]</th>
+  <td>[% HTML.escape(vc_obj.zipcode) %]</td>
+  <td>[% L.input_tag("shiptozipcode", shiptozipcode, "size", "35") %]</td>
+ </tr>
+ <tr>
+  <th align="right" nowrap>[% LxERP.t8('City') %]</th>
+  <td>[% HTML.escape(vc_obj.city) %]</td>
+  <td>[% L.input_tag("shiptocity", shiptocity, "size", "35") %]</td>
+ </tr>
+ <tr>
+  <th align="right" nowrap>[% LxERP.t8('Country') %]</th>
+  <td>[% HTML.escape(vc_obj.country) %]</td>
+  <td>[% L.input_tag("shiptocountry", shiptocountry, "size", "35") %]</td>
+ </tr>
+ <tr>
+  <th align="right" nowrap>[% LxERP.t8('GLN') %]</th>
+  <td>[% HTML.escape(vc_obj.gln) %]</td>
+  <td>[% L.input_tag("shiptogln", shiptogln, "size", "35") %]</td>
+ </tr>
+ <tr>
+  <th align="right" nowrap>[% LxERP.t8('Contact') %]</th>
+  <td>[% HTML.escape(vc_obj.contact) %]</td>
+  <td>[% L.input_tag("shiptocontact", shiptocontact, "size", "35") %]</td>
+ </tr>
+ <tr>
+  <th align="right" nowrap>[% LxERP.t8('Gender') %]</th>
+  <td></td>
+  <td>
+   [% L.select_tag('shiptocp_gender', [ [ 'm', LxERP.t8('male') ], [ 'f', LxERP.t8('female') ] ], 'default' = shiptocp_gender) %]
+  </td>
+ </tr>
+ <tr>
+  <th align="right" nowrap>[% LxERP.t8('Phone') %]</th>
+  <td>[% HTML.escape(vc_obj.phone) %]</td>
+  <td>[% L.input_tag("shiptophone", shiptophone, "size", "35") %]</td>
+ </tr>
+ <tr>
+  <th align="right" nowrap>[% LxERP.t8('Fax') %]</th>
+  <td>[% HTML.escape(vc_obj.fax) %]</td>
+  <td>[% L.input_tag("shiptofax", shiptofax, "size", "35") %]</td>
+ </tr>
+ <tr>
+  <th align="right" nowrap>[% LxERP.t8('E-mail') %]</th>
+  <td>[% HTML.escape(vc_obj.email) %]</td>
+  <td>[% L.input_tag("shiptoemail", shiptoemail, "size", "35") %]</td>
+ </tr>
+[% FOREACH var = cvars %]
+ <tr valign="top">
+  <th align="right" nowrap>[% HTML.escape(var.config.description) %]</th>
+  <td></td>
+  <td>[% INCLUDE 'common/render_cvar_input.html' cvar_name_prefix='shiptocvar_' %]</td>
+ </tr>
+[% END %]
+</table>
+
+<p>
+ [% L.button_tag("kivi.SalesPurchase.submit_custom_shipto('" _ id_selector _ "')", LxERP.t8("Apply")) %]
+ [% L.button_tag("kivi.SalesPurchase.reset_shipto_fields()", LxERP.t8("Reset")) %]
+ [% L.button_tag("kivi.SalesPurchase.clear_shipto_fields()", LxERP.t8("Clear fields")) %]
+ [% L.button_tag("\$('#shipto_dialog').dialog('close');", LxERP.t8("Abort")) %]
+</p>
index 4b705cd..9ff22ad 100644 (file)
@@ -1,14 +1,26 @@
 [%- USE HTML -%][%- USE LxERP %][%- USE T8 %]
 [%- BLOCK output %]
  <div id="flash_[% type %]" class="flash_message_[% type %]"[% IF !messages || !messages.size %] style="display: none"[% END %]>
-  <a href='#' style='float:right' onclick='$(this).closest("div").find(".flash_content").empty(); $(this).closest("div").hide()'><img src='image/close.png' border='0' alt='[% 'Close Flash' | $T8 %]'></a>
+  <a href='#' style='float:right'
+     onclick='$("#flash_[% type %]_content").empty();$("#flash_[% type %]_detail").empty();$("#flash_[% type %]").hide()'>
+     <img src='image/close.png' border='0' alt='[% 'Close Flash' | $T8 %]'></a>
   <span class="flash_title">[%- title %]:</span>
-  <span id="flash_[% type %]_content" class="flash_content">
+  <span id="flash_[% type %]_content">
    [% FOREACH message = messages %]
     [%- HTML.escape(message) %]
     [%- UNLESS loop.last %]<br>[% END %]
    [%- END %]
   </span>
+  <span id="flash_[% type %]_disp" style="display: none">
+  <a href='#' style='float:left' onclick='$("#flash_detail_[% type %]").toggle();'>
+     [[% 'Details' | $T8 %]]</a>&nbsp;&nbsp;</span>
+  <div id="flash_detail_[% type %]" style="display: none">
+    <br>
+    <span id="flash_[% type %]_detail"></span><br>
+    <a href='#' style='float:left'
+      onclick='$("#flash_detail_[% type %]").hide()'>
+      <img src='image/close.png' border='0' alt='[% 'Close Details' | $T8 %]'></a><br>
+  </div>
  </div>
 [%- END %]
 [%- PROCESS output title=LxERP.t8('Error')       type='error'   messages = FLASH.error %]
index 4ec984b..8654854 100644 (file)
@@ -1,4 +1,4 @@
-[%- USE HTML -%][%- USE L -%][%- USE LxERP -%][%- USE T8 -%]
+[%- USE HTML -%][%- USE L -%][%- USE LxERP -%][%- USE T8 -%][%- USE P -%]
 [%- SET id__    = cvar_cfg.id
         name__  = 'filter.cvar.' _ id__
         value__ = filter.cvar.$id__ %]
     L.select_tag(name__, options__, default=value__, class=cvar_class) %]
 
 [% ELSIF cvar_cfg.type == 'customer' %]
- [%- L.customer_vendor_picker(name__, value__, type='customer', class=cvar_class) %]
+ [%- P.customer_vendor.picker(name__, value__, type='customer', class=cvar_class) %]
 
 [% ELSIF cvar_cfg.type == 'vendor' %]
- [%- L.customer_vendor_picker(name__, value__, type='vendor', class=cvar_class) %]
+ [%- P.customer_vendor.picker(name__, value__, type='vendor', class=cvar_class) %]
 
 [% ELSIF cvar_cfg.type == 'part' %]
- [%- L.part_picker(name__, value__, class=cvar_class) %]
+ [%- P.part.picker(name__, value__, class=cvar_class) %]
 
 [%- ELSE %]
  [% SET value_name__ = id__ _ '_substr__ilike'
index c7072d4..2db49e8 100644 (file)
@@ -1,9 +1,11 @@
 [%- USE T8 %]
 [%- USE HTML %]
 [%- USE L %]
+[%- USE P %]
 [%- USE LxERP %]
 
 [%- DEFAULT var_name = HTML.escape(cvar_name_prefix) _ HTML.escape(var.config.name) _ HTML.escape(cvar_name_postfix) %]
+[%- SET style_ = "width: " _ var.config.processed_options.WIDTH _ "px; height: " _ var.config.processed_options.HEIGHT _ "px" %]
 
 [%- IF ( hide_non_editable && !var.config.is_flag('editable') ) %]
   [% L.hidden_tag(var_name, var.value) %]
     [% 'Element disabled' | $T8 %]
   [%- END %]
 [%- ELSIF ( var.config .type == 'bool' ) %]
-  [% L.checkbox_tag(var_name, checked = var.value) %]
+  [% L.checkbox_tag(var_name, checked = var.value, for_submit = 1) %]
 [%- ELSIF ( var.config .type == 'textfield' ) %]
-  [% L.textarea_tag(var_name, var.value, cols = var.config.processed_options.WIDTH, rows = var.config.processed_options.HEIGHT) %]
+  [% L.textarea_tag(var_name, var.value, style=style_) %]
+[%- ELSIF ( var.config .type == 'htmlfield' ) %]
+  [% L.textarea_tag(var_name, L.restricted_html(var.value), class='texteditor', style=style_) %]
 [%- ELSIF ( var.config.type == 'date' ) %]
   [% L.date_tag(var_name, var.value) %]
 [%- ELSIF ( var.config.type == 'timestamp' ) %]
   [% L.input_tag(var_name, var.value) %]
 [%- ELSIF ( var.config.type == 'customer' ) %]
-  [% L.customer_vendor_picker(var_name, var.value, type='customer') %]
+  [% P.customer_vendor.picker(var_name, var.value, type='customer') %]
 [%- ELSIF ( var.config.type == 'vendor' ) %]
-  [% L.customer_vendor_picker(var_name, var.value, type='vendor') %]
+  [% P.customer_vendor.picker(var_name, var.value, type='vendor') %]
 [%- ELSIF ( var.config.type == 'part' ) %]
-  [% L.part_picker(var_name, var.value) %]
+  [% P.part.picker(var_name, var.value) %]
 [%- ELSIF ( var.config.type == 'select' ) %]
   [% L.select_tag(var_name, var.config.processed_options, default = var.value) %]
 [%- ELSIF ( var.config.type == 'number' ) %]
index 53b92e8..d345313 100644 (file)
@@ -3,7 +3,7 @@
 [% USE HTML %]
 <h1>[% 'history search engine' | $T8 %]</h1>
 
-<form method="post" action="am.pl">
+<form method="post" action="am.pl" id="form">
 
 <input type="hidden" name="action" value="show_am_history">
 
     </td>
   </tr>
 </table>
-
-<hr>
-<input type="submit" class="submit" value="[% 'submit' | $T8 %]">
-<input type="reset" class="submit" value="[% 'reset' | $T8 %]" id='reset_button'>
-
 </form>
 
 <script type="text/javascript">
   <!--
-  var defaults = ['SAVED', 'DELETED', 'ADDED', 'PAYMENT POSTED', 'POSTED', 'POSTED AS NEW', 'SAVED FOR DUNNING', 'DUNNING STARTED', 'PRINTED'];
+  var defaults = ['SAVED', 'DELETED', 'ADDED', 'PAYMENT POSTED', 'POSTED',
+  'POSTED AS NEW', 'SAVED FOR DUNNING', 'DUNNING STARTED', 'PRINTED',
+  'QUEUED', 'CANCELED' ,'IMPORT', 'UNIMPORT' ];
   var available;
   var selected;
   var translated = {
     'SAVED'             : '[% 'SAVED' | $T8 %]',
+    'SCREENED'          : '[% 'SCREENED' | $T8 %]',
     'DELETED'           : '[% 'DELETED' | $T8 %]',
     'ADDED'             : '[% 'ADDED' | $T8 %]',
     'PAYMENT POSTED'    : '[% 'PAYMENT POSTED' | $T8 %]',
     'SAVED FOR DUNNING' : '[% 'SAVED FOR DUNNING' | $T8 %]',
     'DUNNING STARTED'   : '[% 'DUNNING STARTED' | $T8 %]',
     'PRINTED'           : '[% 'PRINTED' | $T8 %]',
+    'QUEUED'            : '[% 'QUEUED' | $T8 %]',
+    'CANCELED'          : '[% 'CANCELED' | $T8 %]',
+    'IMPORT'            : '[% 'IMPORT' | $T8 %]',
+    'UNIMPORT'          : '[% 'UNIMPORT' | $T8 %]',
   };
 
   function addForm(index) {
     $('#inputHead').show();
-    selected.push(available.splice(index, 1));
+    selected.push(available.splice(index.index-1, 1));
     $('#inputText').html($(selected).map(function(){ return translated[this]; }).get().join('<br>'));
     $('#einschraenkungen').val(selected.join(','));
 
index 3b916ca..ae9ef17 100644 (file)
       warehouses[[% WAREHOUSES_it.count - 1 %]] = new Array();
       warehouses[[% WAREHOUSES_it.count - 1 %]]['id'] = [% warehouse.id %];
       warehouses[[% WAREHOUSES_it.count - 1 %]]['bins'] = new Array();
+      warehouses[[% WAREHOUSES_it.count - 1 %]]['bins'][0] = new Array();
+      warehouses[[% WAREHOUSES_it.count - 1 %]]['bins'][0]['description'] = "---";
+      warehouses[[% WAREHOUSES_it.count - 1 %]]['bins'][0]['id'] = "";
       [% USE BINS_it = Iterator(warehouse.BINS) %][% FOREACH bin = BINS_it %]
-      warehouses[[% WAREHOUSES_it.count - 1 %]]['bins'][[% BINS_it.count - 1 %]] = new Array();
-      warehouses[[% WAREHOUSES_it.count - 1 %]]['bins'][[% BINS_it.count - 1 %]]['description'] = "[% JavaScript.escape(bin.description) %]";
-      warehouses[[% WAREHOUSES_it.count - 1 %]]['bins'][[% BINS_it.count - 1 %]]['id'] = [% bin.id %];
+      warehouses[[% WAREHOUSES_it.count - 1 %]]['bins'][[% BINS_it.count %]] = new Array();
+      warehouses[[% WAREHOUSES_it.count - 1 %]]['bins'][[% BINS_it.count %]]['description'] = "[% JavaScript.escape(bin.description) %]";
+      warehouses[[% WAREHOUSES_it.count - 1 %]]['bins'][[% BINS_it.count %]]['id'] = [% bin.id %];
       [% END %]
       [% END %]
 
index 2368be3..46496b1 100644 (file)
@@ -23,7 +23,7 @@
       <table height="0" width="0" cellpadding="0" cellspacing="0" marginheight="0" marginwidth="0" border="0">
         <tr>
           <td align="center" valign="middle">
-           &nbsp;[% 'Zeitpunkt' | $T8 %]&nbsp;
+           &nbsp;[% 'Time' | $T8 %]&nbsp;
           </td>
           <td valign="top">
            [% PROCESS column_header THIS_COLUMN=ITIME THIS_COLUMN_DOWN=ITIMEBY column='h.itime' %]
@@ -35,7 +35,7 @@
       <table height="0" width="0" cellpadding="0" cellspacing="0" marginheight="0" marginwidth="0" border="0">
         <tr>
           <td align="center" valign="middle">
-            &nbsp;[% 'Mitarbeiter' | $T8 %]&nbsp;
+            &nbsp;[% 'Employee' | $T8 %]&nbsp;
           </td>
           <td valign="top">
            [% PROCESS column_header THIS_COLUMN=NAME THIS_COLUMN_DOWN=NAMEBY column='emp.name' %]
@@ -47,7 +47,7 @@
       <table>
         <tr>
           <td>
-            &nbsp;[% 'Aktion' | $T8 %]&nbsp;
+            &nbsp;[% 'Action' | $T8 %]&nbsp;
           </td>
         </tr>
       </table>
@@ -56,7 +56,7 @@
       <table>
         <tr>
           <td>
-            &nbsp;[% 'Zusatz' | $T8 %]&nbsp;
+            &nbsp;[% 'Addition' | $T8 %]&nbsp;
           </td>
         </tr>
       </table>
@@ -65,7 +65,7 @@
       <table height="0" width="0" cellpadding="0" cellspacing="0" marginheight="0" marginwidth="0" border="0">
         <tr>
           <td align="center" valign="middle">
-           &nbsp;[% 'ID-Nummer' | $T8 %]&nbsp;
+           &nbsp;[% 'ID number' | $T8 %]&nbsp;
           </td>
           <td valign="top">
             [% PROCESS column_header THIS_COLUMN=TRANS_ID THIS_COLUMN_DOWN=TRANS_IDBY column='h.trans_id' %]
@@ -77,7 +77,7 @@
       <table height="0" width="0" cellpadding="0" cellspacing="0" marginheight="0" marginwidth="0" border="0">
         <tr>
           <td align="center" valign="middle">
-           &nbsp;[% 'Belegnummer' | $T8 %]&nbsp;
+           &nbsp;[% 'Record number' | $T8 %]&nbsp;
           </td>
           <td valign="top">
             [% PROCESS column_header THIS_COLUMN=SNUMBERS THIS_COLUMN_DOWN=SNUMBERSBY column='h.snumbers' %]
       &nbsp;[% HTML.escape(row.id) %]&nbsp;
     </td>
     <td>
-      &nbsp;[% HTML.escape(row.snumbers) %]&nbsp;
+      &nbsp;
+      [% IF row.haslink %]
+      <a href="[% row.haslink %]" target="_blank">[% HTML.escape(row.snumbers) %]</a>
+      [% ELSE %]
+      [% HTML.escape(row.snumbers) %]
+      [% END %]
+      &nbsp;
     </td>
   </tr>
 [% END %]
 </tbody>
 </table>
 [% ELSE %]
-<b>[% 'Keine Suchergebnisse gefunden!' | $T8 %]</b><br>
+<b>[% 'No search results found!' | $T8 %]</b><br>
 [% END %]
 <p>
 [% IF NONEWWINDOW %]
index b1f502f..cd9d4b3 100644 (file)
@@ -1,11 +1,18 @@
 [%- USE T8 %]
-[% USE HTML %]
+[% USE HTML %][%- USE LxERP -%]
 <h1>[% IF is_customer %][% 'Customer details' | $T8 %][% ELSE %][% 'Vendor details' | $T8 %][% END %] &quot;[% HTML.escape(name) %]&quot;</h1>
 
 [% BLOCK jump_block %]
 [%- IF SHIPTO.size || CONTACTS.size %]
  <p>
   [% 'Jump to' | $T8 %] <a href="#billing">[% 'Billing Address' | $T8 %]</a>
+  [%- FOREACH additional_billing_addresses = ADDITIONAL_BILLING_ADDRESSES %]
+   ,
+   <a href="#additional_billing_address[% loop.count %]">
+    [% 'Additional Billing Address' | $T8 %]
+    "[% HTML.escape(additional_billing_addresses.name) %]"
+   </a>
+  [%- END %]
   [%- FOREACH shipto = SHIPTO %]
    ,
    <a href="#shipping[% loop.count %]">
@@ -26,7 +33,7 @@
 [%- END %]
 [% END %]
 
- [%- INCLUDE jump_block CONTACTS = CONTACTS, SHIPTO = SHIPTO %]
+ [%- INCLUDE jump_block CONTACTS = CONTACTS, SHIPTO = SHIPTO, ADDITIONAL_BILLING_ADDRESSES = ADDITIONAL_BILLING_ADDRESSES %]
 
  <a name="billing"><h1>[% 'Billing Address' | $T8 %]</h1></a>
 
    <td>[% HTML.escape(country) %]</td>
   </tr>
 
+  <tr>
+   <td align="right">[% 'GLN' | $T8 %]</td>
+   <td>[% HTML.escape(gln) %]</td>
+  </tr>
+
   <tr>
    <td align="right">[% 'Contact Person' | $T8 %]</td>
    <td>[% IF greeting %][% HTML.escape(greeting) %] [% END %][% HTML.escape(contact) %]</td>
  </table>
 
 
+ [% FOREACH row = ADDITIONAL_BILLING_ADDRESSES %]
+
+  <hr>
+
+  [%- INCLUDE jump_block CONTACTS = CONTACTS, SHIPTO = SHIPTO, ADDITIONAL_BILLING_ADDRESSES = ADDITIONAL_BILLING_ADDRESSES %]
+
+  <a name="additional_billing_address[% loop.count %]"><h1>[% 'Additional Billing Address' | $T8 %] "[% HTML.escape(row.name) %]"</h1></a>
+
+  <table>
+   <tr>
+    <td align="right">[% 'Default Billing Address' | $T8 %]</td>
+    <td>[% row.default_address ? LxERP.t8('yes') : LxERP.t8('no') %]</td>
+   </tr>
+
+   <tr>
+    <td align="right">[% 'Name' | $T8 %]</td>
+    <td>[% HTML.escape(row.name) %]</td>
+   </tr>
+
+   <tr>
+    <td align="right">[% 'Department' | $T8 %]</td>
+    <td>[% HTML.escape(row.department_1) %][% IF row.department_2 %][% IF row.department_1 %]; [% END %][% HTML.escape(row.department_2) %][% END %]</td>
+   </tr>
+
+   <tr>
+    <td align="right">[% 'Street' | $T8 %]</td>
+    <td>[% HTML.escape(row.street) %]</td>
+   </tr>
+
+   <tr>
+    <td align="right">[% 'Zip, City' | $T8 %]</td>
+    <td>[% HTML.escape(row.zipcode) %] [% HTML.escape(row.city) %]</td>
+   </tr>
+
+   <tr>
+    <td align="right">[% 'Country' | $T8 %]</td>
+    <td>[% HTML.escape(row.country) %]</td>
+   </tr>
+
+   <tr>
+    <td align="right">[% 'GLN' | $T8 %]</td>
+    <td>[% HTML.escape(row.gln) %]</td>
+   </tr>
+
+   <tr>
+    <td align="right">[% 'Contact' | $T8 %]</td>
+    <td>[% HTML.escape(row.contact) %]</td>
+   </tr>
+
+   <tr>
+    <td align="right">[% 'Phone' | $T8 %]</td>
+    <td>[% HTML.escape(row.phone) %]</td>
+   </tr>
+
+   <tr>
+    <td align="right">[% 'Fax' | $T8 %]</td>
+    <td>[% HTML.escape(row.fax) %]</td>
+   </tr>
+
+   <tr>
+    <td align="right">[% 'E-mail' | $T8 %]</td>
+    <td>[% HTML.escape(row.email) %]</td>
+   </tr>
+
+  </table>
+ [% END %]
 
 
  [% FOREACH row = SHIPTO %]
 
   <hr>
 
-  [%- INCLUDE jump_block CONTACTS = CONTACTS, SHIPTO = SHIPTO %]
+  [%- INCLUDE jump_block CONTACTS = CONTACTS, SHIPTO = SHIPTO, ADDITIONAL_BILLING_ADDRESSES = ADDITIONAL_BILLING_ADDRESSES %]
 
   <a name="shipping[% loop.count %]"><h1>[% 'Shipping Address' | $T8 %] &quot;[% HTML.escape(row.shiptoname) %]&quot;</h1></a>
 
     <td>[% HTML.escape(row.shiptocountry) %]</td>
    </tr>
 
+   <tr>
+    <td align="right">[% 'GLN' | $T8 %]</td>
+    <td>[% HTML.escape(row.shiptogln) %]</td>
+   </tr>
+
    <tr>
     <td align="right">[% 'Contact Person' | $T8 %]</td>
     <td>[% HTML.escape(row.shiptocontact) %]</td>
 
   <hr>
 
-  [%- INCLUDE jump_block CONTACTS = CONTACTS, SHIPTO = SHIPTO %]
+  [%- INCLUDE jump_block CONTACTS = CONTACTS, SHIPTO = SHIPTO, ADDITIONAL_BILLING_ADDRESSES = ADDITIONAL_BILLING_ADDRESSES %]
 
   <a name="contact[% loop.count %]"><h1>[% 'Contact Person' | $T8 %] &quot;[% HTML.escape(row.cp_name) %]&quot;</h1></a>
 
index e675105..3cca0ea 100644 (file)
@@ -5,9 +5,4 @@
   </tr>
 </table>
 <input type=hidden name=rowcount value="[% rowcount | html %]">
-
-<br>
-<input class=submit type=submit name=action value="[% 'Update' | $T8 %]">
-<input class=submit type=submit name=action value="[% 'Post' | $T8 %]">
  </form>
-
index 7b87c2f..e460924 100644 (file)
@@ -1,9 +1,11 @@
 [%- USE L %]
 [%- USE HTML %]
 [%- USE T8 %]
-[%- USE LxERP %]
-<form method=post action=cp.pl>
+[%- USE LxERP %][%- USE P -%]
+<form method="post" action="cp.pl" id="form">
 
+[% SET vc_id = vc _ '_id'
+       style = "width: 250px" %]
 [% L.hidden_tag('defaultcurrency', defaultcurrency) %]
 [% L.hidden_tag('closedto', closedto) %]
 [% L.hidden_tag('vc', vc) %]
 
 <h1>[% is_receipt ? LxERP.t8('Receipt') : LxERP.t8('Payment') %]</h1>
 
-<table width=100%>
+<table>
   <tr valign=top>
     <td>
       <table>
         <tr>
           <th align=right>[% is_customer ? LxERP.t8('Customer') : LxERP.t8('Vendor') %]</th>
-          <td>[% vccontent %]</td>
-         [% IF vc == 'customer' %]
-           [% L.hidden_tag('selectcustomer', selectcustomer) %]
-           [% L.hidden_tag('customer_id', customer_id) %]
-           [% L.hidden_tag('oldcustomer', oldcustomer) %]
-          [% ELSE %]
-           [% L.hidden_tag('selectvendor', selectvendor) %]
-           [% L.hidden_tag('vendor_id', vendor_id) %]
-           [% L.hidden_tag('oldvendor', oldvendor) %]
-          [% END %]
-        </tr>
-        [% IF vc == 'customer' %]
-        <tr>
-          <th align=right>[% 'Customer Number' | $T8 %]</th>
-          <td><input name="customernumber" size="35"></td>
+          <td>
+           [% P.customer_vendor.picker(vc_id, $vc_id, type=vc, class="initial_focus", style=style) %]
+           [% P.hidden_tag("previous_" _ vc_id, $vc_id) %]
+          </td>
         </tr>
-        [% END %]
         <tr>
           <th align=right>[% 'Invoice Number' | $T8 %]</th>
           <td><input name="invnumber" size="35"></td>
index b7e5dd4..2680cfe 100644 (file)
@@ -1,4 +1,4 @@
-[% PROCESS 'common/flash.html' %]
+[% INCLUDE 'common/flash.html' %]
 <div id='csv_import_report'></div>
 
 <script type='text/javascript'>
   }
 
   $(document).ready(function(){
-    get_report('#csv_import_report', 'controller.pl', { action: 'CsvImport/report', 'no_layout': 1, 'id': [% SELF.background_job.data_as_hash.report_id %] });
+    [%- IF SELF.background_job.data_as_hash.report_id %]
+      get_report('#csv_import_report', 'controller.pl', { action: 'CsvImport/report', 'no_layout': 1, 'id': [% SELF.background_job.data_as_hash.report_id %] });
+    [%- END %]
   });
 
 
 </script>
-
index 7f68a32..f206189 100644 (file)
@@ -4,7 +4,7 @@
 
 <h2>[% 'Import Status' | $T8 %]</h2>
 
-[% PROCESS 'common/flash.html' %]
+[% INCLUDE 'common/flash.html' %]
 [% UNLESS SELF.background_job.data_as_hash.errors %]
 <div id='progress_description'></div>
 <div id='progressbar'></div>
diff --git a/templates/webpages/csv_import/_errors.html b/templates/webpages/csv_import/_errors.html
deleted file mode 100644 (file)
index 1a7d661..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-[% USE LxERP %]
-
-<h3>[%- LxERP.t8('Errors') %]</h3>
-
-<p>[%- LxERP.t8('Found #1 errors.', SELF.errors.size) %]</p>
-
-<table>
- <tr class="listheading">
-  <th>[%- LxERP.t8('Line and column') %]</th>
-  <th>[%- LxERP.t8('Block') %]</th>
-  <th>[%- LxERP.t8('Error') %]</th>
- </tr>
- [% FOREACH err = SELF.errors %]
-  <tr class="listrow[% loop.count % 2 %]">
-   <td>[% err.4 %]:[% err.3 %]</td>
-   <td>[% err.0 %]</td>
-   <td>[% err.2 %]</td>
-  </tr>
- [% END %]
-</table>
diff --git a/templates/webpages/csv_import/_form_artransactions.html b/templates/webpages/csv_import/_form_artransactions.html
new file mode 100644 (file)
index 0000000..eeb7922
--- /dev/null
@@ -0,0 +1,15 @@
+[% USE LxERP %]
+[% USE L %]
+<tr>
+ <th align="right">[%- LxERP.t8('AR Transaction/AccTrans Item row names') %]:</th>
+ <td colspan="10">
+  [% L.input_tag('settings.ar_column', SELF.profile.get('ar_column'), size => "10") %]
+  [% L.input_tag('settings.transaction_column',  SELF.profile.get('transaction_column'),  size => "20") %]
+ </td>
+
+<tr>
+ <th align="right">[%- LxERP.t8('Maximal amount difference') %]:</th>
+ <td colspan="10">
+  [% L.input_tag('settings.max_amount_diff', LxERP.format_amount(SELF.profile.get('max_amount_diff')), size => "5") %]
+ </td>
+</tr>
diff --git a/templates/webpages/csv_import/_form_delivery_orders.html b/templates/webpages/csv_import/_form_delivery_orders.html
new file mode 100644 (file)
index 0000000..62cc4a0
--- /dev/null
@@ -0,0 +1,17 @@
+[% USE LxERP %]
+[% USE L %]
+<tr>
+ <th align="right">[%- LxERP.t8('Order/Item/Stock row name') %]:</th>
+ <td colspan="10">
+  [% L.input_tag('settings.order_column', SELF.profile.get('order_column'), size => "10") %]
+  [% L.input_tag('settings.item_column',  SELF.profile.get('item_column'),  size => "10") %]
+  [% L.input_tag('settings.stock_column', SELF.profile.get('stock_column'), size => "10") %]
+ </td>
+<tr>
+ <th align="right">[%- LxERP.t8('Error handling') %]:</th>
+ <td colspan="10">
+  [% L.checkbox_tag('settings.ignore_faulty_positions',
+                    label   => LxERP.t8('Ignore faulty positions'),
+                    checked => SELF.profile.get('ignore_faulty_positions')) %]
+ </td>
+</tr>
index beb09a0..9e9101d 100644 (file)
@@ -1,9 +1,10 @@
 [% USE LxERP %]
 [% USE L %]
+[% USE P %]
 <tr>
  <th align="right">[%- LxERP.t8('Parts with existing part numbers') %]:</th>
  <td colspan="10">
-  [% opts = [ [ 'update_prices', LxERP.t8('Update prices of existing entries') ], [ 'insert_new', LxERP.t8('Insert with new part number') ], [ 'skip', LxERP.t8('Skip entry') ] ] %]
+  [% opts = [[ 'update_parts', LxERP.t8('Update properties of existing entries') ], [ 'update_parts_sn', LxERP.t8('Update properties of existing entries / skip non-existent') ], [ 'update_prices', LxERP.t8('Update prices of existing entries') ],[ 'update_prices_sn', LxERP.t8('Update prices of existing entries / skip non-existent') ] ,[ 'insert_new', LxERP.t8('Insert with new part number') ], [ 'skip', LxERP.t8('Skip entry') ] ] %]
   [% L.select_tag('settings.article_number_policy', opts, default = SELF.profile.get('article_number_policy'), style = 'width: 300px') %]
  </td>
 </tr>
 <tr>
  <th align="right">[%- LxERP.t8('Type') %]:</th>
  <td colspan="10">
-  [% opts = [ [ 'part', LxERP.t8('Parts') ], [ 'service', LxERP.t8('Services') ], [ 'mixed', LxERP.t8('Mixed (requires column "type")') ] ] %]
-  [% L.select_tag('settings.parts_type', opts, default = SELF.profile.get('parts_type'), style = 'width: 300px') %]
+  [% opts = [ [ 'part', LxERP.t8('Parts') ], [ 'service', LxERP.t8('Services') ], [ 'mixed', LxERP.t8('Mixed (requires column "type" or "pclass")') ] ] %]
+  [% L.select_tag('settings.part_type', opts, default = SELF.profile.get('part_type'), style = 'width: 300px') %]
+ </td>
+</tr>
+<tr>
+ <th align="right">[%- LxERP.t8('Parts Classification') %]:</th>
+ <td colspan="10">
+  [% P.part.select_classification('settings.part_classification', default = SELF.profile.get('part_classification'), style = 'width: 300px') %]
  </td>
 </tr>
 
 <tr>
- <th align="right" valign="top">[%- LxERP.t8('Default buchungsgruppe') %]:</th>
+ <th align="right" valign="top">[%- LxERP.t8('Default booking group') %]:</th>
  <td colspan="10" valign="top">
   [% L.select_tag('settings.default_buchungsgruppe', SELF.all_buchungsgruppen, title_key = 'description', default = SELF.profile.get('default_buchungsgruppe'), style => 'width: 300px') %]
   <br>
-  [% opts = [ [ 'never', LxERP.t8('Do not set default buchungsgruppe') ], [ 'all', LxERP.t8('Apply to all parts') ], [ 'missing', LxERP.t8('Apply to parts without buchungsgruppe') ] ] %]
+  [% opts = [ [ 'never', LxERP.t8('Do not set default booking group') ], [ 'all', LxERP.t8('Apply to all parts') ], [ 'missing', LxERP.t8('Apply to parts without booking group') ] ] %]
   [% L.select_tag('settings.apply_buchungsgruppe', opts, default = SELF.profile.get('apply_buchungsgruppe'), style = 'width: 300px') %]
  </td>
 </tr>
diff --git a/templates/webpages/csv_import/_mapping_item.html b/templates/webpages/csv_import/_mapping_item.html
new file mode 100644 (file)
index 0000000..9e5f74c
--- /dev/null
@@ -0,0 +1,14 @@
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE T8 %]
+ <tr class=listrow>
+    <td><a class='remove_line interact cursor-pointer'>✘</a></td>
+[%- IF item.from %]
+  <td>[% L.hidden_tag('mappings[+].from', item.from) %][% item.from | html %]</td>
+[%- ELSE %]
+  <td>[% L.input_tag('mappings[+].from', '') %]</td>
+[%- END %]
+  <td>[% L.select_tag('mappings[].to', SELF.displayable_columns, value_key='name', title_key='name', default=item.to) %]</td>
+ </tr>
+
diff --git a/templates/webpages/csv_import/_preview.html b/templates/webpages/csv_import/_preview.html
deleted file mode 100644 (file)
index 01fe4d9..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-[% USE HTML %]
-[% USE LxERP %]
-
-[% IF SELF.data.size %]
- <h3>
-  [%- IF SELF.import_status == 'tested' %]
-   [%- LxERP.t8('Import preview') %]
-  [%- ELSE %]
-   [%- LxERP.t8('Import result') %]
-  [%- END %]
- </h3>
-
- <table>
-  <tr class="listheading">
-   [%- FOREACH column = SELF.info_headers.headers %]
-    <th>[%- HTML.escape(column) %]</th>
-   [%- END %]
-   [%- FOREACH column = SELF.headers.headers %]
-    <th>[%- HTML.escape(column) %]</th>
-   [%- END %]
-   [%- FOREACH column = SELF.raw_data_headers.headers %]
-    <th>[%- HTML.escape(column) %]</th>
-   [%- END %]
-   <th>[%- LxERP.t8('Notes') %]</th>
-  </tr>
-
-  [%- FOREACH row = SELF.data %]
-  [%- IF (SELF.profile.get('full_preview') == 2) || ((SELF.profile.get('full_preview') == 1) && (row.errors.size || row.information.size)) || ((SELF.profile.get('full_preview') == 0) && (loop.count < 21)) %]
-  <tr class="[% IF row.errors.size %]redrow[% ELSE %]listrow[% END %][% loop.count % 2 %]">
-   [%- FOREACH method = SELF.info_headers.methods %]
-    <td>[%- HTML.escape(row.info_data.$method) %]</td>
-   [%- END %]
-   [%- FOREACH method = SELF.headers.methods %]
-    <td>[%- HTML.escape(row.object.$method) %]</td>
-   [%- END %]
-   [%- FOREACH method = SELF.raw_data_headers.headers %]
-    <td>[%- HTML.escape(row.raw_data.$method) %]</td>
-   [%- END %]
-   <td>
-    [%- FOREACH error = row.errors %][%- HTML.escape(error) %][% UNLESS loop.last %]<br>[%- END %][%- END %]
-    [%- FOREACH info  = row.information %][% IF !loop.first || row.errors.size %]<br>[%- END %][%- HTML.escape(info) %][%- END %]
-   </td>
-  </tr>
-  [%- END %]
-  [%- END %]
-
- </table>
-[%- END %]
diff --git a/templates/webpages/csv_import/_result.html b/templates/webpages/csv_import/_result.html
deleted file mode 100644 (file)
index b67bb7f..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-[% USE LxERP %]
-
-<h3>[%- LxERP.t8('Import summary') %]</h3>
-
-[%- IF SELF.import_status == 'imported' %]
- <p>[%- LxERP.t8('#1 of #2 importable objects were imported.', SELF.num_imported, SELF.num_importable || 0) %]</p>
-[%- ELSE %]
- <p>[%- LxERP.t8('Found #1 objects of which #2 can be imported.', SELF.data.size || 0, SELF.num_importable || 0) %]</p>
-[%- END %]
diff --git a/templates/webpages/csv_import/_results.html b/templates/webpages/csv_import/_results.html
deleted file mode 100644 (file)
index 56ec933..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-  [%- IF SELF.errors %]
-   [%- PROCESS 'csv_import/_errors.html' %]
-  [%- END %]
-
-  [%- PROCESS 'csv_import/_result.html' %]
-  [%- PROCESS 'csv_import/_preview.html' %]
-
-[% IF progress == 100 %]
-<script type='text/javascript'>
-  $(function(){ $('#action_import').show() })
-</script>
-[% END %]
index c40c563..568324e 100644 (file)
@@ -6,11 +6,16 @@
 
  [%- INCLUDE 'common/flash.html' %]
 
- <form method="post" action="controller.pl" enctype="multipart/form-data">
+ <form method="post" action="controller.pl" enctype="multipart/form-data" id="form">
   [% L.hidden_tag('form_sent', '1') %]
   [% L.hidden_tag('action', 'CsvImport/dispatch') %]
   [% L.hidden_tag('profile.type', SELF.profile.type) %]
+  [% L.hidden_tag('tmp_profile_id', SELF.profile.id) %]
 
+ [%- IF SELF.profile.get('dont_edit_profile') %]
+  [% L.hidden_tag('force_profile', 1) %]
+  [% L.hidden_tag('profile.id', SELF.profile.id) %]
+ [%- ELSE %][%# IF SELF.profile.get('dont_edit_profile') %]
   <h2>[%- LxERP.t8('Import profiles') %]</h2>
 
   <table>
@@ -19,7 +24,7 @@
      <th align="right">[%- LxERP.t8('Current profile') %]:</th>
      <td>[%- HTML.escape(SELF.profile.name) %]</td>
     </tr>
-   [%- END %]
+   [%- END %][%# IF SELF.profile.id %]
 
    [%- IF SELF.all_profiles.size %]
     <tr>
@@ -32,7 +37,7 @@
       [% L.submit_tag('action_destroy', LxERP.t8('Delete profile'), confirm => LxERP.t8('Do you really want to delete this object?')) %]
      </td>
     </tr>
-   [%- END %]
+   [%- END %][%# IF SELF.all_profiles.size %]
 
    <tr>
     <th align="right" valign="top">[%- LxERP.t8('Save settings as') %]:</th>
@@ -61,9 +66,9 @@
        <tr class="listheading">
          [%- FOREACH p = SELF.worker.profile %]
            <th>[%- p.row_ident %]</th>
-         [%- END %]
+         [%- END %][%# FOREACH SELF.worker.profile %]
        </tr>
-       <tr class="listrow[% loop.count % 2 %]">
+       <tr style="vertical-align:top">
          [%- FOREACH p = SELF.worker.profile %]
            [% SET ri = p.row_ident %]
          <td>
                <td>[%- HTML.escape(row.name) %]</td>
                <td>[%- HTML.escape(row.description) %]</td>
              </tr>
-             [%- END %]
+             [%- END %][%# FOREACH SELF.displayable_columns.$ri %]
            </table>
          </td>
-         [%- END %]
+         [%- END %][%# FOREACH SELF.worker.profile %]
        </tr>
      </table>
-   [%- ELSE %]
+   [%- ELSE %][%# IF SELF.worker.is_multiplexed %]
      <table>
        <tr class="listheading">
          <th>[%- LxERP.t8('Column name') %]</th>
          <td>[%- HTML.escape(row.name) %]</td>
          <td>[%- HTML.escape(row.description) %]</td>
        </tr>
-       [%- END %]
+       [%- END %][%# FOREACH SELF.displayable_columns %]
      </table>
-   [%- END %]
+   [%- END %][%# SELF.worker.is_multiplexed %]
 
 [%- IF SELF.type == 'contacts' %]
    <p>
     [%- LxERP.t8("You can update existing contacts by providing the 'cp_id' column with their database IDs. Otherwise: ") %]
-    [%- LxERP.t8('At least one of the columns #1, customer, customernumber, vendor, vendornumber (depending on the target table) is required for matching the entry to an existing customer or vendor.', 'cp_cv_id') %]
+    [%- LxERP.t8('At least one of the columns #1, customer, customernumber, customer_gln, vendor, vendornumber, vendor_gln (depending on the target table) is required for matching the entry to an existing customer or vendor.', 'cp_cv_id') %]
    </p>
 
 [%- ELSIF SELF.type == 'addresses' %]
    <p>
-    [%- LxERP.t8('At least one of the columns #1, customer, customernumber, vendor, vendornumber (depending on the target table) is required for matching the entry to an existing customer or vendor.', 'trans_id') %]
+    [%- LxERP.t8('At least one of the columns #1, customer, customernumber, customer_gln, vendor, vendornumber, vendor_gln (depending on the target table) is required for matching the entry to an existing customer or vendor.', 'trans_id') %]
    </p>
 
 [%- ELSIF SELF.type == 'parts' %]
    </p>
    <p>
     [3]:
-    [% LxERP.t8("If the article type is set to 'mixed' then a column called 'type' must be present.") %]
+    [% LxERP.t8("If the article type is set to 'mixed' then a column called 'part_type' or called 'pclass' must be present.") %]
     [% LxERP.t8("Type can be either 'part', 'service' or 'assembly'.") %]
-    [% LxERP.t8("Assemblies can not be imported (yet). But the type column is used for sanity checks on price updates in order to prevent that articles with the wrong type will be updated.") %]
+    [%- LxERP.t8("If column 'pclass' is present the article type is then irrelevant or used as default ") %]
+    [% LxERP.t8("The 'pclass' column has the same abbreviation like a part export. The first letter is for the type Part,Assembly or Service, the second(and third) for Part Classification") %]
    </p>
 
 [%- ELSIF SELF.type == 'inventories' %]
     [%- LxERP.t8('One of the columns "qty" or "target_qty" must be given. If "target_qty" is given, the quantity to transfer for each transfer will be calculate, so that the quantity for this part, warehouse and bin will result in the given "target_qty" after each transfer.') %]
    </p>
 
-[%- ELSIF SELF.type == 'orders' %]
+[%- ELSIF SELF.type == 'orders' OR SELF.type == 'delivery_orders' OR SELF.type == 'ar_transactions' %]
    <p>
     [1]:
     [% LxERP.t8('The column "datatype" must be present and must be at the same position / column in each data set. The values must be the row names (see settings) for order and item data respectively.') %]
    </p>
-   <p>
-    [2]:
-    [%- LxERP.t8('Amount and net amount are calculated by kivitendo. "verify_amount" and "verify_netamount" can be used for sanity checks.') %]<br>
-    [%- LxERP.t8('If amounts differ more than "Maximal amount difference" (see settings), this item is marked as invalid.') %]<br>
-   </p>
-[%- END %]
-
-   <p>
-    [%- L.submit_tag('action_download_sample', LxERP.t8('Download sample file')) %]
-   </p>
+   [%- IF SELF.type == 'orders' OR SELF.type == 'ar_transactions' %]
+    <p>
+     [2]:
+     [%- LxERP.t8('Amount and net amount are calculated by kivitendo. "verify_amount" and "verify_netamount" can be used for sanity checks.') %]<br>
+     [%- LxERP.t8('If amounts differ more than "Maximal amount difference" (see settings), this item is marked as invalid.') %]<br>
+    </p>
+   [%- END %]
+[%- END %][%# IF SELF.type == … %]
+  </div>
 
+  <div>
+    <p>
+      [%- L.submit_tag('action_download_sample', LxERP.t8('Download sample file')) %]
+    </p>
   </div>
 
   <hr>
    <tr>
     <th align="right">[%- LxERP.t8('Number Format') %]:</th>
     <td colspan="10">
-     [% L.select_tag('settings.numberformat', ['1.000,00', '1000,00', '1,000.00', '1000.00'], default = SELF.profile.get('numberformat'), style = 'width: 300px') %]
+     [% L.select_tag('settings.numberformat', ['1.000,00', '1000,00', '1,000.00', '1000.00', "1'000.00"], default = SELF.profile.get('numberformat'), style = 'width: 300px') %]
+    </td>
+   </tr>
+
+   <tr>
+    <th align="right">[%- LxERP.t8('Date Format') %]:</th>
+    <td colspan="10">
+     [% L.select_tag('settings.dateformat', ['dd.mm.yyyy', 'yyyy-mm-dd', 'dd/mm/yyyy', 'mm/dd/yyyy' ], default = SELF.profile.get('dateformat'), style = 'width: 300px') %]
     </td>
    </tr>
 
       [% IF SELF.sep_char == entry.first %] [% SET custom_sep_char = '' %] [%- END %]
       [% L.radio_button_tag('sep_char', value => entry.first, label => entry.last, checked => SELF.sep_char == entry.first) %]
      </td>
-    [%- END %]
+    [%- END %][%# FOREACH SELF.all_sep_chars %]
 
     <td>
      [% L.radio_button_tag('sep_char', value => 'custom', checked => custom_sep_char != '') %]
       [% IF SELF.quote_char == entry.first %] [% SET custom_quote_char = '' %] [%- END %]
       [% L.radio_button_tag('quote_char', value => entry.first, label => entry.last, checked => SELF.quote_char == entry.first) %]
      </td>
-    [%- END %]
+    [%- END %][%# FOREACH SELF.all_quote_chars %]
 
     <td>
      [% L.radio_button_tag('quote_char', value => 'custom', checked => custom_quote_char != '') %]
       [% IF SELF.escape_char == entry.first %] [% SET custom_escape_char = '' %] [%- END %]
       [% L.radio_button_tag('escape_char', value => entry.first, label => entry.last, checked => SELF.escape_char == entry.first) %]
      </td>
-    [%- END %]
+    [%- END %][%# FOREACH SELF.all_escape_chars %]
 
     <td>
      [% L.radio_button_tag('escape_char', value => 'custom', checked => custom_escape_char != '') %]
          [% FOREACH key = duplicate_fields.keys %]
            <input type="checkbox" name="settings.duplicates_[% key | html %]" id="settings.duplicates_[% key | html %]" value="1"[% IF ( SELF.profile.get('duplicates_'_ key) || (duplicate_fields.$key.default && !FORM.form_sent ) ) %] checked="checked"[% END %]>
            <label for="settings.duplicates_[% key | html %]">[% duplicate_fields.$key.label | html %]</label>
-         [% END %]
+         [% END %][%# FOREACH duplicate_fields.keys %]
        </td>
      </tr>
 
          [% L.select_tag('settings.duplicates', opts, default = SELF.profile.get('duplicates'), style = 'width: 300px') %]
        </td>
      </tr>
-   [% END %]
+   [% END %][%# IF duplicate_fields.size %]
 
 [%- IF SELF.type == 'parts' %]
  [%- INCLUDE 'csv_import/_form_parts.html' %]
  [%- INCLUDE 'csv_import/_form_inventories.html' %]
 [%- ELSIF SELF.type == 'orders' %]
  [%- INCLUDE 'csv_import/_form_orders.html' %]
+[%- ELSIF SELF.type == 'delivery_orders' %]
+ [%- INCLUDE 'csv_import/_form_delivery_orders.html' %]
+[%- ELSIF SELF.type == 'ar_transactions' %]
+ [%- INCLUDE 'csv_import/_form_artransactions.html' %]
 [%- ELSIF SELF.type == 'bank_transactions' %]
  [%- INCLUDE 'csv_import/_form_banktransactions.html' %]
 [%- END %]
    <tr>
     <th align="right">[%- LxERP.t8('Preview Mode') %]:</th>
     <td colspan="10">
-      [% L.radio_button_tag('settings.full_preview', value=2, checked=SELF.profile.get('full_preview')==2, label=LxERP.t8('Full Preview')) %]
-      [% L.radio_button_tag('settings.full_preview', value=1, checked=SELF.profile.get('full_preview')==1, label=LxERP.t8('Only Warnings and Errors')) %]
-      [% L.radio_button_tag('settings.full_preview', value=0, checked=!SELF.profile.get('full_preview'),   label=LxERP.t8('First 20 Lines')) %]
+      [% L.radio_button_tag('settings.full_preview', value=0, checked=!SELF.profile.get('full_preview'),   label=LxERP.t8('Full Preview')) %]
+      [% L.radio_button_tag('settings.full_preview', value=1, checked=SELF.profile.get('full_preview')==1, label=LxERP.t8('Only Lines with Notes or Errors')) %]
+      [% L.radio_button_tag('settings.full_preview', value=2, checked=SELF.profile.get('full_preview')==2, label=LxERP.t8('First 20 Lines')) %]
     </td>
    </tr>
 
      <th align="right">[%- LxERP.t8('Existing file on server') %]:</th>
      <td colspan="10">[%- LxERP.t8('Uploaded on #1, size #2 kB', SELF.file.displayable_mtime, LxERP.format_amount(SELF.file.size / 1024, 2)) %]</td>
     </tr>
-   [%- END %]
+   [%- END %][%# IF SELF.file.exists %]
 
   </table>
 
   </div>
   <hr>
 
-  [% L.submit_tag('action_test', LxERP.t8('Test and preview')) %]
-  [% L.submit_tag('action_import', LxERP.t8('Import'), style='display:none') %]
+[%- UNLESS SELF.worker.is_multiplexed %]
+  <h2>[% 'Mappings (csv_import)' | $T8 %]</h2>
+
+  <div class="mappings_toggle"[% UNLESS SELF.deferred || SELF.import_status %] style="display:none"[% END %]>
+   <a href="#" onClick="javascript:$('.mappings_toggle').toggle()">[% LxERP.t8("Show mappings (csv_import)") %]</a>
+  </div>
+  <div class="mappings_toggle"[% IF SELF.deferred || SELF.import_status %] style="display:none"[% END %]>
+   <p><a href="#" onClick="javascript:$('.mappings_toggle').toggle()">[% LxERP.t8("Hide mappings (csv_import)") %]</a></p>
+
+    <p>[% 'These mappings can be used to map heading from non standard csv files to known columns. These will also be saved in profiles, so you can save profiles for every source of formats.' | $T8 %]</p>
+
+  <table id="csv_import_mappings">
+   <tr class=listheading>
+    <th></th>
+    <th>[% 'Text in CSV File' | $T8 %]</th>
+    <th>[% 'Known Column' | $T8 %]</th>
+   </tr>
+   <tr id='mapping_empty' style='display:none'>
+    <td colspan=3>[% 'There is nothing here yet (csv_import)' | $T8 %]</td>
+   </tr>
+[%- FOREACH row = SELF.mappings %]
+   [% PROCESS 'csv_import/_mapping_item.html', item=row IF row.from %]
+[%- END %][%# FOREACH SELF.mappings %]
+   [% PROCESS 'csv_import/_mapping_item.html', item={} %]
+  </table>
+
+  <input type=button id='add_empty_mapping_line' value='[% 'Add empty line (csv_import)' | $T8 %]'>
+  <input type=button id='add_mapping_from_upload' value='[% 'Add headers from last uploaded file (csv_import)' | $T8 %]'>
 
+  </div>
+  <hr>
+[%- END %][%# UNLESS SELF.worker.is_multiplexed %]
+[%- END %][%# IF SELF.profile.get('dont_edit_profile') %]
  </form>
 
  <div id='results'>
  [%- IF SELF.deferred %]
    [%- PROCESS 'csv_import/_deferred_results.html' %]
- [%- ELSIF SELF.import_status %]
-   [%- PROCESS 'csv_import/_results.html' %]
- [%- END %]
+ [%- END %][%# IF SELF.deferred %]
  </div>
 
 
           return true;
         alert('[% LxERP.t8('Please enter a profile name.') %]');
         return false;
-      })
+      });
+      $('#add_empty_mapping_line').click(function(){
+        $.get('controller.pl', { action: 'CsvImport/add_empty_mapping_line', 'profile.type': $('#profile_type').val() }, kivi.eval_json_result);
+      });
+      $('#add_mapping_from_upload').click(function(){
+        $.get('controller.pl?action_add_mapping_from_upload=1', $('form').serialize() , kivi.eval_json_result);
+      });
+      $('#csv_import_mappings').on('click', '.remove_line', function(){ $(this).closest('tr').remove(); if (1==$('#csv_import_mappings tr:visible').length) $('#mapping_empty').show() });
     });
     -->
  </script>
index 539ce5a..8bb3930 100644 (file)
@@ -1,24 +1,50 @@
 [% USE HTML %]
 [% USE LxERP %]
 [% USE L %]
- <h3>[%- LxERP.t8('Import result') %]</h3>
+ <h2>
+ [%- IF SELF.report.test_mode %]
+  [%- LxERP.t8('Import preview') %]
+ [%- ELSE %]
+  [%- LxERP.t8('Import result') %]
+ [%- END %]
+ [%- IF SELF.num_errors -%]
+   <font color="red">([%- SELF.num_errors -%]&nbsp;[%- LxERP.t8('Errors') -%])</font>
+ [%- END -%]
+</h2>
+
+[%- IF SELF.report.test_mode %]
+ <p>
+  [% LxERP.t8("The following is only a preview.") %]
+  [% LxERP.t8("No entries have been imported yet.") %]
+ </p>
+[%- END %]
 
 [%- PROCESS 'common/paginate.html' pages=SELF.pages, base_url = SELF.base_url %]
  <table>
+
+[%- SET max_col = 0 %]
 [%- FOREACH rownum = SELF.display_rows %]
+  [%- SET max_col = max_col > SELF.report_rows.${rownum}.size ? max_col : SELF.report_rows.${rownum}.size  %]
+  [%- LAST IF rownum >= SELF.report_numheaders %]
+[%- END %]
+
+[%- FOREACH rownum = SELF.display_rows %]
+ [%- SET to_pad = max_col - SELF.report_rows.${rownum}.size %]
  [%- IF rownum < SELF.report_numheaders %]
   <tr class="listheading">
   [%- FOREACH value = SELF.report_rows.${rownum} %]
    <th>[% value | html %]</th>
-  [%- END  %]
+  [%- END %]
+   [%- IF to_pad -%]<th style="text-align:center" colspan="[%- to_pad -%]">-</th>[%- END -%]
    <th>[%- LxERP.t8('Notes') %]</th>
   </tr>
  [%- ELSE %]
   [% csv_import_report_errors = SELF.report_status.${rownum}.errors %]
-  <tr class="[% IF csv_import_report_errors && csv_import_report_errors.size %]redrow[% ELSE %]listrow[% END %][% 1 - loop.count % 2 %]">
+  <tr class="listrow[% IF csv_import_report_errors && csv_import_report_errors.size %]_error[% END %][% (loop.count + SELF.report_numheaders) % 2 %]">
   [%- FOREACH value = SELF.report_rows.${rownum} %]
    <td>[%- value | html  %]</td>
   [%- END %]
+   [%- IF to_pad -%]<td align="center" colspan="[%- to_pad -%]">-</td>[%- END -%]
    <td>
     [%- FOREACH error = csv_import_report_errors %][%- error | html %][% UNLESS loop.last %]<br>[%- END %][%- END %]
     [%- FOREACH info  = SELF.report_status.${rownum}.information %][% IF rownum >= SELF.report_numheaders || csv_import_report_errors.size %]<br>[%- END %][%- info | html %][%- END %]
 [%- PROCESS 'common/paginate.html' pages=SELF.pages, base_url = SELF.base_url %]
 
 <script type='text/javascript'>
-  $(function(){ $('#action_import').show() });
+  $(function(){
+    [%- IF SELF.reporterror %]
+      kivi.clear_flash('info',0);
+      kivi.display_flash('error','[% SELF.reporterror %]',0);
+    [%- ELSIF SELF.report.test_mode %]
+      kivi.ActionBar.Action('#action_import').enable();
+    [%- END %]
+  });
 </script>
index 9546101..cce6d2c 100644 (file)
@@ -1,10 +1,6 @@
 [% USE T8 %][% USE HTML %]
 
-<form method="post" action="controller.pl?action=CustomerVendor/add">
+<form method="post" action="controller.pl" id="new_form">
   <input name="callback" type="hidden" value="[% HTML.escape(callback) %]">
   <input name="db" type="hidden" value="[% HTML.escape(db) %]">
-
-  [% IF IS_CUSTOMER %][% 'New customer' | $T8 %][% ELSE %][% 'New vendor' | $T8 %][% END %]<br>
-
-  <input class="submit" type="submit" value="[%- 'Add' | $T8 %]">
 </form>
index aa51707..6557628 100644 (file)
@@ -1,9 +1,9 @@
 [%- USE T8 %]
 [%- USE L %]
-[%- USE HTML %]
+[%- USE HTML %][%- USE LxERP -%]
 <h1>[% title %]</h1>
 
- <form method="post" action="ct.pl" name="Form">
+ <form method="post" action="ct.pl" name="Form" id="form">
 
   <input type="hidden" name="db" value="[% HTML.escape(db) %]">
 
     <th align="right" nowrap>[% 'E-mail' | $T8 %]</th>
     <td><input name="email" size="35"></td>
    </tr>
+   <tr>
+    <th align="right" nowrap>[% 'All phone numbers' | $T8 %]</th>
+    <td><input name="all_phonenumbers" size="35"></td>
+   </tr>
    <tr>
     <th align="right" nowrap>[% 'Contact person (surname)' | $T8 %]</th>
     <td><input name="cp_name" size="35"></td>
    </tr>
-   </tr>
+   <tr>
     <th align="right" nowrap>[% 'Billing/shipping address (street)' | $T8 %]</th>
     <td><input name="addr_street" size="35"></td>
    </tr>
- <tr>
  <tr>
     <th align="right" nowrap>[% 'Billing/shipping address (zipcode)' | $T8 %]</th>
     <td><input name="addr_zipcode" size="35"></td>
- </tr>
  </tr>
    <tr>
     <th align="right" nowrap>[% 'Billing/shipping address (city)' | $T8 %]</th>
     <td><input name="addr_city" size="35"></td>
     <th align="right" nowrap>[% 'Billing/shipping address (country)' | $T8 %]</th>
     <td><input name="addr_country" size="35"></td>
    </tr>
+   <tr>
+    <th align="right" nowrap>[% 'Billing/shipping address (GLN)' | $T8 %]</th>
+    <td><input name="addr_gln" size="35"></td>
+   </tr>
 
    [% IF SHOW_BUSINESS_TYPES %]
    <tr>
    </tr>
    [% END %]
 
+[% IF IS_CUSTOMER %]
+   <tr>
+     <th align="right" nowrap>[% LxERP.t8("Factur-X/ZUGFeRD settings") %]</th>
+     <td>[% L.select_tag('create_zugferd_invoices', ZUGFERD_SETTINGS, with_empty = 1) %]</td>
+   </tr>
+[% END %]
+
    [% IF IS_CUSTOMER && ALL_SALESMEN.size %]
    <tr>
     <th align="right" nowrap>[% 'Salesman' | $T8 %]</th>
         <label for="l_id">[% 'ID' | $T8 %]</label>
        </td>
        <td>
-        <input name="l_[% db %]number" id="l_[% db %]number" type="checkbox" class="checkbox" value="Y" checked>
-        <label for="l_[% db %]number">[% IF IS_CUSTOMER %][% 'Customer Number' | $T8 %][% ELSE %][% 'Vendor Number' | $T8 %][% END %]</label>
+        <input name="l_[% db | html %]number" id="l_[% db | html %]number" type="checkbox" class="checkbox" value="Y" checked>
+        <label for="l_[% db | html %]number">[% IF IS_CUSTOMER %][% 'Customer Number' | $T8 %][% ELSE %][% 'Vendor Number' | $T8 %][% END %]</label>
        </td>
        <td>
         <input name="l_name" id="l_name" type="checkbox" class="checkbox" value="Y" checked>
        <td>
         <input name="l_country" id="l_country" type="checkbox" class="checkbox" value="Y" checked>
         <label for="l_country">[% 'Country' | $T8 %]</label>
-      </td>
+       </td>
       </tr>
       <tr>
        <td>
         <input name="l_insertdate" id="l_insertdate" class="checkbox" type="checkbox" value="Y">
         <label for="l_insertdate">[% 'Insert Date' | $T8 %]</label>
        </td>
-      [% IF IS_CUSTOMER %]
-      <td>
-       <input name="l_salesman" id="l_salesman" type="checkbox" class="checkbox" value="Y">
-       <label for="l_salesman">[% 'Salesman' | $T8 %]</label>
-      </td>
+       <td>
+        <input name="l_gln" id="l_gln" type="checkbox" class="checkbox" value="Y" checked>
+        <label for="l_gln">[% 'GLN' | $T8 %]</label>
+       </td>
       </tr>
+      [% IF IS_CUSTOMER %]
       <tr>
-      <td>
-       <input name="l_pricegroup" id="l_pricegroup" type="checkbox" class="checkbox" value="Y">
-       <label for="l_pricegroup">[% 'Pricegroup' | $T8 %]</label>
-      </td>
+       <td>
+        <input name="l_salesman" id="l_salesman" type="checkbox" class="checkbox" value="Y">
+        <label for="l_salesman">[% 'Salesman' | $T8 %]</label>
+       </td>
+       <td>
+        <input name="l_pricegroup" id="l_pricegroup" type="checkbox" class="checkbox" value="Y">
+        <label for="l_pricegroup">[% 'Pricegroup' | $T8 %]</label>
+       </td>
+       <td>
+        <input name="l_contact_origin" id="l_contact_origin" type="checkbox" class="checkbox" value="Y">
+        <label for="l_contact_origin">[% 'Origin of personal data' | $T8 %]</label>
+       </td>
+       <td>
+        <input name="l_invoice_mail" id="l_invoice_mail" type="checkbox" class="checkbox" value="Y">
+        <label for="l_invoice_mail">[% 'Email of the invoice recipient' | $T8 %]</label>
+       </td>
+      </tr>
       [% END %]
+      <tr>
+       <td>
+        <input name="l_ustid" id="l_ustid" type="checkbox" class="checkbox" value="Y">
+        <label for="l_ustid">[% 'VAT ID' | $T8 %]</label>
+       </td>
+       <td>
+        <input name="l_main_contact_person" id="l_main_contact_person" type="checkbox" class="checkbox" value="Y">
+        <label for="l_main_contact_person">[% 'Main Contact Person' | $T8 %]</label>
+       </td>
+      [% IF !IS_CUSTOMER %]
       </tr>
+      [% ELSE %]
+       <td>
+        <input name="l_creditlimit" id="l_creditlimit" type="checkbox" class="checkbox" value="Y">
+        <label for="l_creditlimit">[% 'Credit Limit' | $T8 %]</label>
+       </td>
+       <td>
+        <input name="l_commercial_court" id="l_commercial_court" type="checkbox" class="checkbox" value="Y">
+        <label for="l_commercial_court">[% 'Commercial court' | $T8 %]</label>
+       </td>
+
+      </tr>
+      <tr>
+       <td>
+        <input name="l_delivery_order_mail" id="l_delivery_order_mail" type="checkbox" class="checkbox" value="Y">
+        <label for="l_delivery_order_mail">[% 'Email of the delivery order recipient' | $T8 %]</label>
+       </td>
+        <td>
+        <input name="l_department_1" id="l_department_1" type="checkbox" class="checkbox" value="Y">
+        <label for="l_department_1">[% 'Department' | $T8 %] 1</label>
+       </td>
+       <td>
+        <input name="l_department_2" id="l_department_2" type="checkbox" class="checkbox" value="Y">
+        <label for="l_department_2">[% 'Department' | $T8 %] 2</label>
+       </td>
+        <td></td>
+      </tr>
+      [% END %]
 
       [% CUSTOM_VARIABLES_INCLUSION_CODE %]
 
     </td>
    </tr>
   </table>
-
-  <input type="hidden" name="nextsub" value="list_names">
-
-  <input type="submit" class="submit" name="action" value="[% 'Continue' | $T8 %]">
  </form>
index de9ba8b..aa4f8ed 100644 (file)
@@ -4,7 +4,7 @@
 [%- USE LxERP %]
 <h1>[% 'Contacts' | $T8 %]</h1>
 
- <form method="post" action="ct.pl" name="Form">
+ <form method="post" action="ct.pl" name="Form" id="form">
 
   <table>
    <tr>
     </td>
    </tr>
   </table>
-
-  <input type="hidden" name="nextsub" value="list_contacts">
-
-  <input type="submit" class="submit" name="action" value="[% 'Continue' | $T8 %]">
  </form>
index 60bbcd7..d24a640 100644 (file)
@@ -1,4 +1,4 @@
 <body>
-[% PROCESS 'common/flash.html' %]
+[% INCLUDE 'common/flash.html' %]
 </body>
 </html>
diff --git a/templates/webpages/custom_data_export/empty_result_set.html b/templates/webpages/custom_data_export/empty_result_set.html
new file mode 100644 (file)
index 0000000..3abde9c
--- /dev/null
@@ -0,0 +1,9 @@
+[% USE HTML %][% USE L %][% USE LxERP %]
+
+<h1>[% FORM.title %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+<p>
+ [% LxERP.t8("The query did not return any data.") %]
+</p>
diff --git a/templates/webpages/custom_data_export/export.html b/templates/webpages/custom_data_export/export.html
new file mode 100644 (file)
index 0000000..283cae3
--- /dev/null
@@ -0,0 +1,55 @@
+[% USE HTML %][% USE L %][% USE LxERP %]
+
+<h1>[% FORM.title %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+<form method="post" action="controller.pl" id="form">
+ [% L.hidden_tag("id", SELF.query.id) %]
+ [% L.hidden_tag("format", "csv") %]
+
+ [% IF !SELF.parameters.size %]
+  <p>
+   [% LxERP.t8("The SQL query does not contain any parameter that need to be configured.") %]
+  </p>
+
+ [% ELSE %]
+
+ <table>
+  <thead>
+   <tr class="listheading">
+    <th>[% LxERP.t8("Variable Name") %]</th>
+    <th>[% LxERP.t8("Type") %]</th>
+    <th>[% LxERP.t8("Value") %]</th>
+    <th>[% LxERP.t8("Description") %]</th>
+   </tr>
+  </thead>
+
+  <tbody>
+   [% FOREACH parameter = SELF.parameters %]
+    <tr class="listrow">
+     <td>
+      [% HTML.escape(parameter.name) %]
+     </td>
+
+     [% SET value = parameter.calculate_default_value %]
+
+     [% IF parameter.parameter_type == "number" %]
+      <td>[% LxERP.t8("Number") %]</td>
+      <td>[% L.input_tag("parameters." _ parameter.name, value, style="width: 300px", "data-validate"="required") %]</td>
+
+     [% ELSIF parameter.parameter_type == "date" %]
+      <td>[% LxERP.t8("Date") %]</td>
+      <td>[% L.date_tag("parameters." _ parameter.name, value, style="width: 300px", "data-validate"="required") %]</td>
+
+     [% ELSE %]
+      <td>[% LxERP.t8("Text") %]</td>
+      <td>[% L.input_tag("parameters." _ parameter.name, value, style="width: 300px", "data-validate"="required") %]</td>
+     [% END %]
+
+     <td>[% HTML.escape(parameter.description) %]</td>
+    </tr>
+   [% END %]
+  </tbody>
+ [% END %]
+</form>
diff --git a/templates/webpages/custom_data_export/list.html b/templates/webpages/custom_data_export/list.html
new file mode 100644 (file)
index 0000000..7e22b4e
--- /dev/null
@@ -0,0 +1,30 @@
+[% USE HTML %][% USE L %][% USE LxERP %]
+
+<h1>[% FORM.title %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+[% IF !SELF.queries.size %]
+ <p>
+  [%- LxERP.t8("You do not have access to any custom data export.") %]
+ </p>
+
+[%- ELSE %]
+ <table width="100%">
+  <thead>
+   <tr class="listheading">
+    <th>[% LxERP.t8("Name") %]</th>
+    <th>[% LxERP.t8("Description") %]</th>
+   </tr>
+  </thead>
+
+  <tbody>
+   [%- FOREACH query = SELF.queries %]
+    <tr class="listrow">
+     <td>[% L.link(SELF.url_for(action="export", id=query.id), query.name) %]</td>
+     <td>[% IF query.description %][% L.link(SELF.url_for(action="export", id=query.id), query.description) %][% END %]</td>
+    </tr>
+   [%- END %]
+  </tbody>
+ </table>
+[%- END %]
diff --git a/templates/webpages/custom_data_export_designer/edit.html b/templates/webpages/custom_data_export_designer/edit.html
new file mode 100644 (file)
index 0000000..9b8f68c
--- /dev/null
@@ -0,0 +1,49 @@
+[% USE HTML %][% USE L %][% USE LxERP %]
+
+<h1>[% FORM.title %] — [% LxERP.t8("Step #1/#2", 1, 2) %] — [% LxERP.t8("Basic Data") %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+<form method="post" action="controller.pl" id="form">
+ [% L.hidden_tag("id", SELF.query.id) %]
+
+ <table>
+  <tr>
+   <th align="right">[%- LxERP.t8("Name") %]</th>
+   <td>[% L.input_tag("query.name", SELF.query.name, style="width: 800px", "data-validate"="required") %]</td>
+  </tr>
+
+  <tr>
+   <th align="right">[%- LxERP.t8("Required access right") %]</th>
+   <td>[% L.select_tag("query.access_right", SELF.access_rights, default=SELF.query.access_right, style="width: 800px") %]</td>
+  </tr>
+
+  <tr>
+   <th valign="top" align="right">[%- LxERP.t8("Description") %]</th>
+   <td valign="top">[% L.textarea_tag("query.description", SELF.query.description, rows=5, style="width: 800px") %]</td>
+  </tr>
+
+  <tr>
+   <th valign="top" align="right">[%- LxERP.t8("SQL query") %]</th>
+   <td valign="top">[% L.textarea_tag("query.sql_query", SELF.query.sql_query, rows=20, style="width: 800px", "data-validate"="required") %]</td>
+  </tr>
+ </table>
+</form>
+
+<p>
+ [% LxERP.t8("The SQL query can be parameterized with variables named as follows: <%name%>.") %]
+ [% LxERP.t8("On the next page the type of all variables can be set.") %]
+ [% LxERP.t8("Note that parameter names must not be quoted.") %]
+ [% LxERP.t8("Example") %]:
+</p>
+
+<pre>
+SELECT extract(YEAR FROM oe.transdate) AS &quot;Jahr&quot;, SUM(oe.amount) AS &quot;Angebotssumme&quot;
+FROM oe
+LEFT JOIN employee ON (oe.employee_id = employee.id)
+WHERE (oe.customer_id IS NOT NULL)
+  AND COALESCE(oe.quotation, FALSE)
+  AND (employee.login = &lt;%Benutzer-Login%&gt;)
+GROUP BY &quot;Jahr&quot;
+ORDER BY &quot;Jahr&quot;
+</pre>
diff --git a/templates/webpages/custom_data_export_designer/edit_parameters.html b/templates/webpages/custom_data_export_designer/edit_parameters.html
new file mode 100644 (file)
index 0000000..e1c9418
--- /dev/null
@@ -0,0 +1,57 @@
+[% USE HTML %][% USE L %][% USE LxERP %]
+
+<h1>[% FORM.title %] — [% LxERP.t8("Step #1/#2", 2, 2) %] — [% LxERP.t8("Query parameters") %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+<form method="post" action="controller.pl" id="form">
+ [% L.hidden_tag("id", SELF.query.id) %]
+ [% L.hidden_tag("query.name", SELF.query.name) %]
+ [% L.hidden_tag("query.access_right", SELF.query.access_right) %]
+ [% L.hidden_tag("query.description", SELF.query.description) %]
+ [% L.hidden_tag("query.sql_query", SELF.query.sql_query) %]
+
+ [% IF !PARAMETERS.size %]
+  <p>
+   [% LxERP.t8("The SQL query does not contain any parameter that need to be configured.") %]
+  </p>
+
+ [% ELSE %]
+
+ <table>
+  <thead>
+   <tr class="listheading">
+    <th>[% LxERP.t8("Variable Name") %]</th>
+    <th>[% LxERP.t8("Type") %]</th>
+    <th>[% LxERP.t8("Description") %]</th>
+    <th colspan="2">[% LxERP.t8("Default value") %]</th>
+   </tr>
+  </thead>
+
+  <tbody>
+   [% FOREACH parameter = PARAMETERS %]
+    <tr class="listrow">
+     <td valign="top">
+      [% L.hidden_tag("parameters[+].name", parameter.name) %]
+      [% HTML.escape(parameter.name) %]
+     </td>
+     <td valign="top">
+      [% L.select_tag("parameters[].parameter_type", [ [ "text", LxERP.t8("Text") ], [ "number", LxERP.t8("Number") ], [ "date", LxERP.t8("Date") ] ], default=parameter.parameter_type) %]
+     </td>
+     <td valign="top">[% L.input_tag("parameters[].description", parameter.description, size=100) %]</td>
+     <td valign="top">
+      [% L.select_tag("parameters[].default_value_type",
+                      [ [ "none", LxERP.t8("No default value") ], [ "current_user_login", LxERP.t8("Current user's login") ], [ "sql_query", LxERP.t8("Result of SQL query") ],
+                        [ "fixed_value", LxERP.t8("Fixed value") ] ],
+                      default=parameter.default_value_type,
+                      id="default_value_type_" _ loop.count) %]
+     </td>
+     <td valign="top">
+      [% SET disabled = (parameter.default_value_type == "none") || (parameter.default_value_type == "current_user_login") ? "disabled" : "" %]
+      [% L.textarea_tag("parameters[].default_value", parameter.default_value, id="default_value_" _ loop.count, cols=80, rows=3, disabled=disabled) %]
+     </td>
+    </tr>
+   [% END %]
+  </tbody>
+ [% END %]
+</form>
diff --git a/templates/webpages/custom_data_export_designer/list.html b/templates/webpages/custom_data_export_designer/list.html
new file mode 100644 (file)
index 0000000..734ecc7
--- /dev/null
@@ -0,0 +1,30 @@
+[% USE HTML %][% USE L %][% USE LxERP %]
+
+<h1>[% FORM.title %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+[% IF !SELF.queries.size %]
+ <p>
+  [%- LxERP.t8("No custom data exports have been created yet.") %]
+ </p>
+
+[%- ELSE %]
+ <table width="100%">
+  <thead>
+   <tr class="listheading">
+    <th>[% LxERP.t8("Name") %]</th>
+    <th>[% LxERP.t8("Description") %]</th>
+   </tr>
+  </thead>
+
+  <tbody>
+   [%- FOREACH query = SELF.queries %]
+    <tr class="listrow">
+     <td>[% L.link(SELF.url_for(action="edit", id=query.id), query.name) %]</td>
+     <td>[% IF query.description %][% L.link(SELF.url_for(action="edit", id=query.id), query.description) %][% END %]</td>
+    </tr>
+   [%- END %]
+  </tbody>
+ </table>
+[%- END %]
index eb6aee7..c88711a 100644 (file)
@@ -1,6 +1,6 @@
 [%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]<h1>[% HTML.escape(title) %]</h1>
 
-<form action="controller.pl" method="post">
+<form action="controller.pl" method="post" id="form">
  [%- L.hidden_tag("id", SELF.config.id) %]
 
  <p>
                             labeldx => LxERP.t8("Partsgroups where variables are shown")) %]
     </td>
    </tr>
-  </table>
- </p>
+   <tr data-show-for="IC"[% UNLESS SELF.module == 'IC' %] style="display: none;"[% END %]>
+    <td align="right">[% 'Display in basic data tab' | $T8 %]</td>
+    <td>
+     [% L.radio_button_tag('config.first_tab', value='1', id='config.first_tab', label=LxERP.t8('Yes'), checked=(SELF.config.first_tab ?  1 : '')) %]
+     [% L.radio_button_tag('config.first_tab', value='0', id='config.first_tab', label=LxERP.t8('No'),  checked=(SELF.config.first_tab ? '' :  1)) %]
+    </td>
+   </tr>
 
- <p>
-  [% L.hidden_tag("action", "CustomVariableConfig/dispatch") %]
-  [% L.submit_tag("action_" _  (SELF.config.id ? "update" : "create"), LxERP.t8('Save'), onclick="return check_prerequisites();") %]
-  [%- IF SELF.config.id %]
-   [% L.submit_tag("action_create", LxERP.t8('Save as new'), onclick="return check_prerequisites();") %]
-   [% L.submit_tag("action_destroy", LxERP.t8('Delete'), confirm=LxERP.t8('Are you sure?')) %]
-  [%- END %]
-  <a href="[% SELF.url_for(action='list', module=SELF.module) %]">[%- LxERP.t8("Cancel") %]</a>
+  </table>
  </p>
 
  <hr>
   (4) [% 'The default value depends on the variable type:' | $T8 %]
   <br>
   <ul>
-   <li>[%- 'Text, text field and number variables: The default value will be used as-is.' | $T8 %]</li>
+   <li>[%- 'Text, text field, HTML field and number variables: The default value will be used as-is.' | $T8 %]</li>
    <li>[%- 'Boolean variables: If the default value is non-empty then the checkbox will be checked by default and unchecked otherwise.' | $T8 %]</li>
    <li>[%- 'Date and timestamp variables: If the default value equals \'NOW\' then the current date/current timestamp will be used. Otherwise the default value is copied as-is.' | $T8 %]</li>
   </ul>
   <br>
   <ul>
    <li>[%- 'Text variables: \'MAXLENGTH=n\' sets the maximum entry length to \'n\'.' | $T8 %]</li>
-   <li>[%- 'Text field variables: \'WIDTH=w HEIGHT=h\' sets the width and height of the text field. They default to 30 and 5 respectively.' | $T8 %]</li>
+   <li>[%- 'Text field and HTML field variables: \'WIDTH=w HEIGHT=h\' sets the width and height of the field in pixels. They default to 225 and 90 respectively.' | $T8 %]</li>
    <li>[%- 'Number variables: \'PRECISION=n\' forces numbers to be shown with exactly n decimal places.' | $T8 %]</li>
    <li>[%- 'Selection fields: The option field must contain the available options for the selection. Options are separated by \'##\', for example \'Early##Normal##Late\'.' | $T8 %]</li>
   </ul>
index 64c6986..f17b20d 100644 (file)
  </table>
 </p>
 
-<hr height="3">
-
-<p>
- <a href="[% SELF.url_for(action='new', module=SELF.module) %]">[%- 'Add' | $T8 %]</a>
-</p>
-
 [% L.sortable_element('#cvarcfg_list tbody', url=SELF.url_for(action='reorder'), with='cvarcfg_id', params='"&module=" + encodeURIComponent($("#module").val())') %]
 
 <script type="text/javascript">
index 356bb1f..2fce8d6 100644 (file)
@@ -1,13 +1,14 @@
 [%- USE T8 %]
+[%- USE HTML %]
 [%- USE LxERP %]
 [%- USE L %]
-<h1>[% FORM.title %]</h1>
-
+[%- USE Dumper %]
+<h1>[% FORM.title %] [% IF SELF.cv.id %] - [% HTML.escape(SELF.cv.displayable_name) %][% END %]</h1>
 [% L.hidden_tag('_cti_enabled', !!LXCONFIG.cti.dial_command) %]
 
 [% cv_cvars = SELF.cv.cvars_by_config %]
 
-<form method="post" action="controller.pl">
+<form id='form' method="post" action="controller.pl">
 
   [% L.hidden_tag('db', FORM.db) %]
   [% L.hidden_tag('callback', FORM.callback) %]
 
   [%- INCLUDE 'common/flash.html' %]
 
+  [%- SET show_deliveries = ( SELF.cv.id && ((SELF.is_customer && AUTH.assert('sales_all_edit', 1)) || (SELF.is_vendor && AUTH.assert('purchase_all_edit', 1))) ) -%]
   <div class="tabwidget" id="customer_vendor_tabs">
     <ul>
       <li><a href="#billing">[% 'Billing Address' | $T8 %]</a></li>
+      [% IF SELF.is_customer %]
+        <li><a href="#additional_billing_addresses">[% 'Additional Billing Addresses' | $T8 %]</a></li>
+      [% END %]
       <li><a href="#bank">[% 'Bank account' | $T8 %]</a></li>
       <li><a href="#shipto">[% 'Shipping Address' | $T8 %]</a></li>
       <li><a href="#contacts">[% 'Contacts' | $T8 %]</a></li>
-      [% IF ( SELF.cv.id && AUTH.assert('sales_all_edit', 1) ) %]
+      [% IF show_deliveries %]
         <li><a href="#deliveries">[% 'Supplies' | $T8 %]</a></li>
       [% END %]
+      [%- IF INSTANCE_CONF.get_webdav %]
+        <li><a href="#ui-tabs-webdav">[% 'WebDAV' | $T8 %]</a></li>
+      [%- END %]
+      [%- IF INSTANCE_CONF.get_doc_storage %]
+        <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=[% FORM.db == 'vendor' ? 'vendor' : 'customer' %]&object_id=[% SELF.cv.id %]">[% 'Attachments' | $T8 %]</a></li>
+      [%- END %]
       <li><a href="#vcnotes">[% 'Notes' | $T8 %]</a></li>
 
       [% IF ( cv_cvars.size ) %]
       [% IF SELF.cv.id %]
         <li><a href="#price_rules">[% 'Price Rules' | $T8 %]</a></li>
       [% END %]
+
+      [% IF ( SELF.cv.id && SELF.cv.pricegroup_id && AUTH.assert('part_service_assembly_details', 1) ) %]
+        <li><a href="#price_list">[% 'Price List' | $T8 %]</a></li>
+      [% END %]
+
+      [% IF SELF.cv.id %]
+        [% IF ( FORM.db == 'customer' && AUTH.assert('show_extra_record_tab_customer',1) ) %]
+          <li><a href="[% 'controller.pl?action=CustomerVendorTurnover/list_turnover&id=' _ SELF.cv.id _ '&db=' _ FORM.db %]">[% LxERP.t8('Records') %]
+              [%- IF SELF.open_items > 0 %] <span style="color:red;">&nbsp;$</span>[% END %] [%- IF SELF.open_orders > 0 %] <span style="color:red;">&nbsp;!</span>[% END %]</a>
+          </li>
+        [% END %]
+        [% IF ( FORM.db == 'vendor' && AUTH.assert('show_extra_record_tab_vendor',1) ) %]
+          <li><a href="[% 'controller.pl?action=CustomerVendorTurnover/list_turnover&id=' _ SELF.cv.id _ '&db=' _ FORM.db %]">[% LxERP.t8('Records') %]
+              [%- IF SELF.open_items > 0 %] <span style="color:red;">&nbsp;$</span>[% END %] [%- IF SELF.open_orders > 0 %] <span style="color:red;">&nbsp;!</span>[% END %]</a>
+          </li>
+        [% END %]
+      [% END %]
+
     </ul>
 
     [% PROCESS "customer_vendor/tabs/billing.html" %]
+    [% IF SELF.is_customer %]
+      [% PROCESS "customer_vendor/tabs/additional_billing_addresses.html" %]
+    [% END %]
     [% PROCESS "customer_vendor/tabs/bank.html" %]
     [% PROCESS "customer_vendor/tabs/shipto.html" %]
     [% PROCESS "customer_vendor/tabs/contacts.html" %]
-    [% IF ( SELF.cv.id && AUTH.assert('sales_all_edit', 1) ) %]
+    [% IF show_deliveries %]
       [% PROCESS "customer_vendor/tabs/deliveries.html" %]
     [% END %]
+    [% PROCESS 'webdav/_list.html' %]
     [% PROCESS "customer_vendor/tabs/vcnotes.html" %]
     [% IF ( cv_cvars.size ) %]
       [% PROCESS "customer_vendor/tabs/custom_variables.html" %]
     [% IF SELF.cv.id %]
       [% PROCESS "customer_vendor/tabs/price_rules.html" %]
     [% END %]
+    [% IF ( SELF.cv.id && SELF.cv.pricegroup_id && AUTH.assert('part_service_assembly_details', 1) ) %]
+      [% PROCESS "customer_vendor/tabs/price_list.html" %]
+    [% END %]
   </div>
-
-  <br>
-
-  [% L.hidden_tag('action', 'CustomerVendor/dispatch') %]
-
-  [% L.submit_tag('action_save', LxERP.t8('Save'), onclick = "return check_taxzone_and_ustid()", accesskey = "s") %]
-  [% L.submit_tag('action_save_and_close', LxERP.t8('Save and Close'), onclick = "return check_taxzone_and_ustid()") %]
-
-  [%- IF ( SELF.is_vendor ) %]
-    [% L.submit_tag('action_save_and_ap_transaction', LxERP.t8('Save and AP Transaction'), onclick = "return check_taxzone_and_ustid()") %]
-  [%- ELSE %]
-    [% L.submit_tag('action_save_and_ar_transaction', LxERP.t8('Save and AR Transaction'), onclick = "return check_taxzone_and_ustid()") %]
-  [%- END %]
-
-  [% L.submit_tag('action_save_and_invoice', LxERP.t8('Save and Invoice'), onclick = "return check_taxzone_and_ustid()") %]
-  [% L.submit_tag('action_save_and_order', LxERP.t8('Save and Order'), onclick = "return check_taxzone_and_ustid()") %]
-
-  [%- IF ( SELF.is_vendor ) %]
-    [% L.submit_tag('action_save_and_rfq', LxERP.t8('Save and RFQ'), onclick = "return check_taxzone_and_ustid()") %]
-  [%- ELSE %]
-    [% L.submit_tag('action_save_and_quotation', LxERP.t8('Save and Quotation'), onclick = "return check_taxzone_and_ustid()") %]
-  [%- END %]
-
-  [%- IF ( SELF.cv.id && SELF.is_orphaned ) %]
-    [% L.submit_tag('action_delete', LxERP.t8('Delete'), confirm => LxERP.t8('Do you really want to delete this object?')) %]
-  [%- END %]
-
-  [%- IF ( SELF.cv.id ) %]
-    <input type="button" class="submit" onclick="kivi.CustomerVendor.showHistoryWindow([% SELF.cv.id %]);" name="history" id="history" value="[% 'history' | $T8 %]">
-  [%- END %]
-
 </form>
 
 <script type="text/javascript">
 <!--
-  function submitInputButton(button)
+  function submitInputButton(action)
   {
-    var hidden = document.createElement("input");
-    hidden.setAttribute("type", "hidden");
-
-    if ( button.hasAttribute("name") )
-      hidden.setAttribute("name", button.getAttribute("name"));
-
-    if ( button.hasAttribute("value") )
-      hidden.setAttribute("value", button.getAttribute("value"));
-
-
-    button.form.appendChild(hidden);
-
-    button.disabled = true;
+    var $hidden = $("<input type='hidden' name='action' value='CustomerVendor/" + action + "'>"),
+        $form   = $('#form');
 
-    button.form.submit();
+    $form.append($hidden);
+    $form.submit();
   }
 
   function check_taxzone_and_ustid() {
index 5250110..fffb3ce 100644 (file)
             <tr class="listrow[% loop.count % 2 %]">
               <td>[% HTML.escape(row.shiptoname) UNLESS loop.prev.shiptoname == row.shiptoname %]&nbsp;</td>
               <td>[% IF row.id %]<a href='[% row.script %].pl?action=edit&id=[% HTML.escape(row.id) %]'>[% END %][% HTML.escape(row.invnumber)   || '&nbsp;' %][% IF row.id %]</a>[% END %]</td>
-              <td>[% IF row.oe_id %]<a href='oe.pl?action=edit&type=[% IF SELF.is_customer %]sales_order[% ELSE %]purchase_order[% END %]&vc=customer&id=[% HTML.escape(row.oe_id) %]'>[% END %][% HTML.escape(row.ordnumber)   || '&nbsp;' %][% IF row.oe_id %]</a>[% END %]</td>
+              [%- IF INSTANCE_CONF.get_feature_experimental_order -%]
+                <td>[% IF row.oe_id %]<a href='controller.pl?action=Order/edit&type=[% IF SELF.is_customer %]sales_order[% ELSE %]purchase_order[% END %]&id=[% HTML.escape(row.oe_id) %]'>[% END %][% HTML.escape(row.ordnumber)   || '&nbsp;' %][% IF row.oe_id %]</a>[% END %]</td>
+              [%- ELSE -%]
+                <td>[% IF row.oe_id %]<a href='oe.pl?action=edit&type=[% IF SELF.is_customer %]sales_order[% ELSE %]purchase_order[% END %]&vc=customer&id=[% HTML.escape(row.oe_id) %]'>[% END %][% HTML.escape(row.ordnumber)   || '&nbsp;' %][% IF row.oe_id %]</a>[% END %]</td>
+              [%- END -%]
               <td>[% HTML.escape(row.transdate)   || '&nbsp;' %]</td>
               <td>[% HTML.escape(row.description) || '&nbsp;' %]</td>
               <td>[% HTML.escape(row.qty)         || '&nbsp;' %]</td>
diff --git a/templates/webpages/customer_vendor/tabs/additional_billing_addresses.html b/templates/webpages/customer_vendor/tabs/additional_billing_addresses.html
new file mode 100644 (file)
index 0000000..d8a284c
--- /dev/null
@@ -0,0 +1,127 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+
+<div id="additional_billing_addresses">
+  <table id="additional_billing_address_table">
+    <tr>
+      <th align="right">[% 'Billing Address' | $T8 %]</th>
+
+      <td>
+        [% L.select_tag(
+             'additional_billing_address.id',
+             SELF.additional_billing_addresses,
+             default     = SELF.additional_billing_address.id,
+             value_key   = 'id',
+             title_key   = 'displayable_id',
+             with_empty  = 1,
+             empty_title = LxERP.t8('New address'),
+             onchange    = "kivi.CustomerVendor.selectAdditionalBillingAddress({onFormSet: function(){ additionalBillingAddressMapWidget.testInputs(); kivi.reinit_widgets(); }});",
+           )
+        %]
+      </td>
+    </tr>
+
+    <tr>
+      <th align="right" nowrap>[% 'Default Billing Address' | $T8 %]</th>
+      <td>[% L.yes_no_tag('additional_billing_address.default_address', SELF.additional_billing_address.default_address) %]</td>
+    </tr>
+
+    <tr>
+      <th align="right" nowrap>[% 'Name' | $T8 %]</th>
+
+      <td>
+        [% L.input_tag('additional_billing_address.name', SELF.additional_billing_address.name,  size = 35) %]
+      </td>
+    </tr>
+
+    <tr>
+      <th align="right" nowrap>[% 'Department' | $T8 %]</th>
+
+      <td>
+        [% L.input_tag('additional_billing_address.department_1', SELF.additional_billing_address.department_1,  size = 16) %]
+        [% L.input_tag('additional_billing_address.department_2', SELF.additional_billing_address.department_2,  size = 16) %]
+      </td>
+    </tr>
+
+    <tr>
+      <th align="right" nowrap>[% 'Street' | $T8 %]</th>
+
+      <td>
+        [% L.input_tag('additional_billing_address.street', SELF.additional_billing_address.street,  size = 35) %]
+
+        <span id="additional_billing_address_map"></span>
+        <script type="text/javascript">
+          additionalBillingAddressMapWidget = new kivi.CustomerVendor.MapWidget('additional_billing_address_');
+          $(function() {
+            additionalBillingAddressMapWidget.render($('#additional_billing_address_map'));
+          });
+        </script>
+      </td>
+    </tr>
+
+    <tr>
+      <th align="right" nowrap>[% 'Zipcode' | $T8 %]/[% 'City' | $T8 %]</th>
+
+      <td>
+        [% L.input_tag('additional_billing_address.zipcode', SELF.additional_billing_address.zipcode,  size = 5) %]
+        [% L.input_tag('additional_billing_address.city', SELF.additional_billing_address.city,  size = 30) %]
+      </td>
+    </tr>
+
+    <tr>
+      <th align="right" nowrap>[% 'Country' | $T8 %]</th>
+
+      <td>
+        [% L.input_tag('additional_billing_address.country', SELF.additional_billing_address.country,  size = 35) %]
+      </td>
+    </tr>
+
+    <tr>
+      <th align="right" nowrap>[% 'GLN' | $T8 %]</th>
+
+      <td>
+        [% L.input_tag('additional_billing_address.gln', SELF.additional_billing_address.gln,  size = 30) %]
+      </td>
+    </tr>
+
+    <tr>
+      <th align="right" nowrap>[% 'Contact' | $T8 %]</th>
+
+      <td>
+        [% L.input_tag('additional_billing_address.contact', SELF.additional_billing_address.contact,  size = 30) %]
+      </td>
+    </tr>
+
+    <tr>
+      <th align="right" nowrap>[% 'Phone' | $T8 %]</th>
+
+      <td>
+        [% L.input_tag('additional_billing_address.phone', SELF.additional_billing_address.phone,  size = 30) %]
+      </td>
+    </tr>
+
+    <tr>
+      <th align="right" nowrap>[% 'Fax' | $T8 %]</th>
+
+      <td>
+        [% L.input_tag('additional_billing_address.fax', SELF.additional_billing_address.fax,  size = 30) %]
+      </td>
+    </tr>
+
+    <tr>
+      <th align="right" nowrap>[% 'E-mail' | $T8 %]</th>
+
+      <td>
+        [% L.input_tag('additional_billing_address.email', SELF.additional_billing_address.email,  size = 45) %]
+      </td>
+    </tr>
+  </table>
+
+  [% L.button_tag('submitInputButton("delete_additional_billing_address");', LxERP.t8('Delete address'), class = 'submit') %]
+  [% IF ( !SELF.additional_billing_address.id ) %]
+    <script type="text/javascript">
+      $('#action_delete_additional_billing_address').hide();
+    </script>
+  [% END %]
+</div>
index 82ff5f2..300643e 100644 (file)
@@ -9,7 +9,7 @@
 
     <tr height="5"></tr>
 
-    [% IF ( INSTANCE_CONF.get_vertreter ) %]
+    [% IF (0 && INSTANCE_CONF.get_vertreter ) %]
       <tr>
         <th align="right">
           [% IF SELF.is_vendor() %]
       <th align="right" nowrap>[% 'Greeting' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('cv.greeting', SELF.cv.greeting) %]
-        [% L.select_tag('cv_greeting_select', SELF.all_greetings, default = SELF.cv.greeting, with_empty = 1, onchange = '$("#cv_greeting").val(this.value);') %]
+        [%- IF INSTANCE_CONF.get_vc_greetings_use_textfield -%]
+          [% L.input_tag('cv.greeting', SELF.cv.greeting) %]
+          [% L.select_tag('cv_greeting_select', SELF.all_greetings, default = SELF.cv.greeting, value_key = 'description', title_key = 'description', with_empty = 1, onchange = '$("#cv_greeting").val(this.value);') %]
+        [%- ELSE -%]
+          [% L.select_tag('cv.greeting', SELF.all_greetings, default = SELF.cv.greeting, value_key = 'description', title_key = 'description', with_empty = 1) %]
+        [%- END -%]
       </td>
     </tr>
 
@@ -70,6 +74,8 @@
 
       <td>
         [% L.input_tag('cv.name', SELF.cv.name) %]
+        <label for="cv_natural_person">[% 'natural person' | $T8 %]</label>
+        [% L.checkbox_tag('cv.natural_person', checked = SELF.cv.natural_person, for_submit=1) %]
       </td>
     </tr>
 
       </td>
     </tr>
 
+    <tr>
+      <th align="right" nowrap>[% 'GLN' | $T8 %]</th>
+
+      <td>
+        [% L.input_tag('cv.gln', SELF.cv.gln, size = 30) %]
+      </td>
+    </tr>
+
     <tr>
       <th align="right" nowrap>[% 'Contact' | $T8 %]</th>
 
         [% L.input_tag('cv.bcc', SELF.cv.bcc, size = 45) %]
       </td>
     </tr>
-
+    [% IF ( SELF.is_customer() ) %]
+    <tr>
+      <th align="right">[% 'Email of the invoice recipient' | $T8 %]</th>
+      <td>[% L.input_tag('cv.invoice_mail', SELF.cv.invoice_mail, size = 30) %] <label for="cv_postal_invoice">[% 'Postal Invoice' | $T8 %]</label>
+        [% L.checkbox_tag('cv.postal_invoice', checked = SELF.cv.postal_invoice, for_submit=1) %]</td>
+    </tr>
+    <tr>
+      <th align="right">[% 'Email of the delivery order recipient' | $T8 %]</th>
+      <td>[% L.input_tag('cv.delivery_order_mail', SELF.cv.delivery_order_mail, size = 45) %]</td>
+    </tr>
+    [% END %]
     <tr>
       <th align="right" nowrap>
-        [% IF homepage %]
+        [% IF SELF.cv.homepage %]
           <a href="[% HTML.escape(SELF.cv.homepage) %]" title="[% 'Open this Website' | $T8 %]" target="_blank">[% 'Homepage' | $T8 %]</a>
         [% ELSE %]
           [% 'Homepage' | $T8 %]
     </tr>
 
     <tr>
+      [% IF ( SELF.all_currencies.size ) %]
+        <th align="right">[% 'Currency' | $T8 %]</th>
+        <td>
+          [% L.select_tag('cv.currency_id', SELF.all_currencies, title_key = 'name', value_key = 'id', default = SELF.cv.currency_id) %]
+        </td>
+      [% END %]
+
       <th align="right">[% 'Tax Number / SSN' | $T8 %]</th>
 
       <td>
       <td>
         [% L.input_tag('cv.ustid', SELF.cv.ustid, size = 20 ) %]
       </td>
+    </tr>
 
-
-      [%- IF ( SELF.is_vendor() ) %]
-        <th align="right">[% 'Customer Number' | $T8 %]</th>
+    <tr>
+      [% IF ( SELF.is_customer() ) %]
+        <th align="right">[% 'Commercial court' | $T8 %]</th>
         <td>
-          [% L.input_tag('cv.v_customer_id', SELF.cv.v_customer_id, size = 10) %]
+          [% L.input_tag('cv.commercial_court', SELF.cv.commercial_court, size = 20) %]
         </td>
-      [%- ELSE %]
+
         <th align="right">[% 'our vendor number at customer' | $T8 %]</th>
         <td>
-          [% L.input_tag('cv.c_vendor_id', SELF.cv.c_vendor_id, size = 10) %]
+          [% L.input_tag('cv.c_vendor_id', SELF.cv.c_vendor_id, size = 20) %]
         </td>
-      [%- END %]
-    </tr>
 
-  [% IF ( SELF.all_currencies.size ) %]
-    <tr>
-        <th align="right">[% 'Currency' | $T8 %]</th>
+        <th align="right">[% 'Our routing id at customer' | $T8 %]</th>
+        <td>
+          [% L.input_tag('cv.c_vendor_routing_id', SELF.cv.c_vendor_routing_id, size = 20) %]
+        </td>
 
+      [%- ELSE %]
+        <th align="right">[% 'Customer Number' | $T8 %]</th>
         <td>
-          [% L.select_tag('cv.currency_id', SELF.all_currencies, title_key = 'name', value_key = 'id', default = SELF.cv.currency_id) %]
+          [% L.input_tag('cv.v_customer_id', SELF.cv.v_customer_id, size = 20) %]
         </td>
+      [%- END %]
     </tr>
-  [% END %]
-
     <tr>
       [% IF ( !INSTANCE_CONF.get_vertreter ) %]
         <th align="right">
       </td>
 
       [% IF ( SELF.is_customer() ) %]
-        <th align="right">[% 'Preisklasse' | $T8 %]</th>
+        <th align="right">[% 'Price group' | $T8 %]</th>
 
         <td>
-          [% L.select_tag('cv.klass', SELF.all_pricegroups, default = SELF.cv.klass, value_key = 'id', title_key = 'pricegroup', with_empty = 1) %]
+          [% L.select_tag('cv.pricegroup_id', SELF.all_pricegroups, default = SELF.cv.pricegroup_id, value_key = 'id', title_key = 'pricegroup', with_empty = 1) %]
         </td>
       [% END  %]
 
     </tr>
 
     <tr>
-      <th align="right">[% 'Steuersatz' | $T8 %]</th>
+      <th align="right">[% 'Tax rate' | $T8 %]</th>
 
       <td>
         [% L.select_tag('cv.taxzone_id', SELF.all_taxzones, default = SELF.cv.taxzone_id, value_key = 'id', title_key = 'description') %]
      <tr>
       <th align="right">[%- LxERP.t8("Hourly rate") %]</th>
       <td>[% L.input_tag("cv.hourly_rate_as_number", SELF.cv.hourly_rate_as_number) %]</td>
+      <th align="right" valign="top" nowrap>[% 'Shoporderlock' | $T8 %]</th>
+      <td>
+        [% L.checkbox_tag('cv.order_lock', checked = SELF.cv.order_lock, for_submit=1) %]
+      </td>
+      <th align="right">[% LxERP.t8("Create sales invoices with Factur-X/ZUGFeRD data") %]</th>
+      <td>[% L.select_tag("cv.create_zugferd_invoices", SELF.zugferd_settings, default=SELF.cv.create_zugferd_invoices) %]</td>
      </tr>
     [% END %]
   </table>
   <table>
     <tr>
       <th align="left" nowrap>[% 'Internal Notes' | $T8 %]</th>
+    [% IF ( SELF.is_customer() ) %]
+      <th align="left">[% 'Origin of personal data' | $T8 %]</th>
+    [% END %]
     </tr>
-
     <tr>
       <td>
         [% L.textarea_tag('cv.notes', SELF.cv.notes, rows = 3 cols = 60 wrap = soft) %]
       </td>
+    [% IF ( SELF.is_customer() ) %]
+      <td>
+        [% L.textarea_tag('cv.contact_origin', SELF.cv.contact_origin,  rows = 3 cols = 60 wrap = soft) %]
+      </td>
+    [% END %]
     </tr>
   </table>
 </div>
index 630a607..90793f4 100644 (file)
@@ -6,7 +6,7 @@
 <div id="contacts">
   <table>
     <tr>
-      <th align="left">[% 'Contacts' | $T8 %]</th>
+      <th align="right">[% 'Contacts' | $T8 %]</th>
 
       <td>
         [%
             empty_title = LxERP.t8('New contact'),
             value_key = 'cp_id',
             title_key = 'full_name',
-            onchange = "kivi.CustomerVendor.selectContact({onFormSet: function(){ contactsMapWidget.testInputs(); local_reinit_widgets(); }});",
+            onchange = "kivi.CustomerVendor.selectContact({onFormSet: function(){ contactsMapWidget.testInputs(); kivi.reinit_widgets(); }});",
           )
         %]
       </td>
     </tr>
-
     <tr>
-      <th align="left" nowrap>[% 'Gender' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Main Contact Person' | $T8 %]</th>
+      <td>[% L.yes_no_tag('contact.cp_main', SELF.contact.cp_main) %]</td>
+    </tr>
+    <tr>
+      <th align="right" nowrap>[% 'Gender' | $T8 %]</th>
 
       <td>
         [%
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Title' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Title' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('contact.cp_title', SELF.contact.cp_title, size = 40) %]
-        [% L.select_tag('contact_cp_title_select', SELF.all_titles, with_empty = 1, onchange = '$("#contact_cp_title").val(this.value);') %]
+        [%- IF INSTANCE_CONF.get_contact_titles_use_textfield -%]
+          [% L.input_tag('contact.cp_title', SELF.contact.cp_title, size = 40) %]
+          [% L.select_tag('contact_cp_title_select', SELF.all_contact_titles, default = SELF.contact.cp_title, value_key = 'description', title_key = 'description', with_empty = 1, onchange = '$("#contact_cp_title").val(this.value);') %]
+        [%- ELSE -%]
+          [% L.select_tag('contact.cp_title', SELF.all_contact_titles, default = SELF.contact.cp_title, value_key = 'description', title_key = 'description', with_empty = 1) %]
+        [%- END -%]
       </td>
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Department' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Department' | $T8 %]</th>
 
       <td>
-        [% L.input_tag('contact.cp_abteilung', SELF.contact.cp_abteilung, size = 40) %]
-        [% L.select_tag('contact_cp_abteilung_select', SELF.all_departments, default = SELF.contact.cp_abteilung,  with_empty = 1, onchange = '$("#contact_cp_abteilung").val(this.value);') %]
+        [%- IF INSTANCE_CONF.get_contact_departments_use_textfield -%]
+          [% L.input_tag('contact.cp_abteilung', SELF.contact.cp_abteilung, size = 40) %]
+          [% L.select_tag('contact_cp_abteilung_select', SELF.all_contact_departments, default = SELF.contact.cp_abteilung, value_key = 'description', title_key = 'description', with_empty = 1, onchange = '$("#contact_cp_abteilung").val(this.value);') %]
+        [%- ELSE -%]
+          [% L.select_tag('contact.cp_abteilung', SELF.all_contact_departments, default = SELF.contact.cp_abteilung, value_key = 'description', title_key = 'description', with_empty = 1) %]
+        [%- END -%]
       </td>
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Function/position' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Function/position' | $T8 %]</th>
 
       <td>
         [% L.input_tag('contact.cp_position', SELF.contact.cp_position, size = 40) %]
@@ -65,7 +76,7 @@
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Given Name' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Given Name' | $T8 %]</th>
 
       <td>
         [% L.input_tag('contact.cp_givenname', SELF.contact.cp_givenname, size = 40) %]
@@ -73,7 +84,7 @@
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Name' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Surname' | $T8 %]</th>
 
       <td>
         [% L.input_tag('contact.cp_name', SELF.contact.cp_name, size = 40) %]
@@ -81,7 +92,7 @@
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'E-mail' | $T8 %]</th>
+      <th align="right" nowrap>[% 'E-mail' | $T8 %]</th>
 
       <td>
         [% L.input_tag('contact.cp_email', SELF.contact.cp_email, size = 40) %]
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Phone1' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Phone1' | $T8 %]</th>
 
       <td>
         [% L.input_tag('contact.cp_phone1', SELF.contact.cp_phone1, size = 40) %]
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Phone2' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Phone2' | $T8 %]</th>
 
       <td>
         [% L.input_tag('contact.cp_phone2', SELF.contact.cp_phone2, size = 40) %]
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Fax' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Fax' | $T8 %]</th>
 
       <td>
         [% L.input_tag('contact.cp_fax', SELF.contact.cp_fax, size = 40) %]
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Mobile1' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Mobile1' | $T8 %]</th>
 
       <td>
         [% L.input_tag('contact.cp_mobile1', SELF.contact.cp_mobile1, size = 40) %]
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Mobile2' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Mobile2' | $T8 %]</th>
 
       <td>
         [% L.input_tag('contact.cp_mobile2', SELF.contact.cp_mobile2, size = 40) %]
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Sat. Phone' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Sat. Phone' | $T8 %]</th>
 
       <td>
         [% L.input_tag('contact.cp_satphone', SELF.contact.cp_satphone, size = 40) %]
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Sat. Fax' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Sat. Fax' | $T8 %]</th>
 
       <td>
         [% L.input_tag('contact.cp_satfax', SELF.contact.cp_satfax, size = 40) %]
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Project' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Project' | $T8 %]</th>
 
       <td>
         [% L.input_tag('contact.cp_project', SELF.contact.cp_project, size = 40) %]
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Street' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Street' | $T8 %]</th>
 
       <td>
         [% L.input_tag('contact.cp_street', SELF.contact.cp_street, size = 40) %]
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Zip, City' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Zip, City' | $T8 %]</th>
 
       <td>
         [% L.input_tag('contact.cp_zipcode', SELF.contact.cp_zipcode, size = 5) %]
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Private Phone' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Private Phone' | $T8 %]</th>
 
       <td>
         [% L.input_tag('contact.cp_privatphone', SELF.contact.cp_privatphone, size = 40) %]
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Private E-mail' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Private E-mail' | $T8 %]</th>
 
       <td>
         [% L.input_tag('contact.cp_privatemail', SELF.contact.cp_privatemail, size = 40) %]
     </tr>
 
     <tr>
-      <th align="left" nowrap>[% 'Birthday' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Birthday' | $T8 %]</th>
 
       <td>
         [% L.date_tag('contact.cp_birthday', SELF.contact.cp_birthday) %]
 
       [% FOREACH var = contact_cvars %]
         <tr>
-          <th align="left" valign="top" nowrap>[% var.config.description | html %]</th>
+          <th align="right" valign="top" nowrap>[% var.config.description | html %]</th>
 
           <td valign="top">
             [% INCLUDE 'common/render_cvar_input.html'
 
   </table>
 
-  [% L.button_tag('submitInputButton(this);', LxERP.t8('Delete Contact'), name = 'action_delete_contact', class = 'submit') %]
+  [% L.button_tag('submitInputButton("delete_contact");', LxERP.t8('Delete Contact'), id = 'action_delete_contact', class = 'submit') %]
   [% IF ( !SELF.contact.cp_id ) %]
     <script type="text/javascript">
       $('#action_delete_contact').hide();
index 7b9abf3..10fee4d 100644 (file)
@@ -5,7 +5,7 @@
     <table>
       [% FOREACH var = SELF.cv.cvars_by_config %]
         <tr>
-          <th align="left" valign="top" nowrap>[% var.config.description | html %]</th>
+          <th align="right" valign="top" nowrap>[% var.config.description | html %]</th>
 
           <td valign="top">
             [% INCLUDE 'common/render_cvar_input.html'
diff --git a/templates/webpages/customer_vendor/tabs/price_list.html b/templates/webpages/customer_vendor/tabs/price_list.html
new file mode 100644 (file)
index 0000000..e8fabf5
--- /dev/null
@@ -0,0 +1,8 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE HTML %]
+[%- USE L %]
+
+<div id="price_list">
+  [%- LxERP.t8("Loading...") %]
+</div>
index 97cfa9d..c47d272 100644 (file)
@@ -3,7 +3,7 @@
 [%- USE L %]
 
 <div id="shipto">
-  <table width="100%" id="shipto_table">
+  <table id="shipto_table">
     <tr>
       <th align="right">[% 'Shipping Address' | $T8 %]</th>
 
@@ -16,7 +16,7 @@
              title_key = 'displayable_id',
              with_empty = 1,
              empty_title = LxERP.t8('New shipto'),
-             onchange = "kivi.CustomerVendor.selectShipto({onFormSet: function(){ shiptoMapWidget.testInputs(); local_reinit_widgets(); }});",
+             onchange = "kivi.CustomerVendor.selectShipto({onFormSet: function(){ shiptoMapWidget.testInputs(); kivi.reinit_widgets(); }});",
            )
         %]
       </td>
@@ -31,7 +31,7 @@
     </tr>
 
     <tr>
-      <th align="right" nowrap>[% 'Abteilung' | $T8 %]</th>
+      <th align="right" nowrap>[% 'Department' | $T8 %]</th>
 
       <td>
         [% L.input_tag('shipto.shiptodepartment_1', SELF.shipto.shiptodepartment_1,  size = 16) %]
       </td>
     </tr>
 
+    <tr>
+      <th align="right" nowrap>[% 'GLN' | $T8 %]</th>
+
+      <td>
+        [% L.input_tag('shipto.shiptogln', SELF.shipto.shiptogln,  size = 35) %]
+      </td>
+    </tr>
+
     <tr>
       <th align="right" nowrap>[% 'Contact' | $T8 %]</th>
 
         [% L.input_tag('shipto.shiptoemail', SELF.shipto.shiptoemail,  size = 45) %]
       </td>
     </tr>
+
+    [% shipto_cvars = SELF.shipto.cvars_by_config %]
+
+    [% IF ( shipto_cvars.size ) %]
+      <tr>
+        <td colspan="2">
+          <hr>
+        </td>
+      </tr>
+
+      [% FOREACH var = shipto_cvars %]
+        <tr>
+          <th align="right" valign="top" nowrap>[% var.config.description | html %]</th>
+
+          <td valign="top">
+            [% INCLUDE 'common/render_cvar_input.html'
+                       cvar_name_prefix = 'shipto_cvars.'
+            %]
+          </td>
+        </tr>
+      [% END %]
+    [% END %]
   </table>
 
-  [% L.button_tag('submitInputButton(this);', LxERP.t8('Delete Shipto'), name = 'action_delete_shipto', class = 'submit') %]
+  [% L.button_tag('submitInputButton("delete_shipto");', LxERP.t8('Delete Shipto'), class = 'submit') %]
   [% IF ( !SELF.shipto.shipto_id ) %]
     <script type="text/javascript">
       $('#action_delete_shipto').hide();
index cf4eeb5..cf21b51 100644 (file)
@@ -41,7 +41,7 @@
             </td>
 
             <td>
-              [% row.follow_up.created_for.safe_name | html %]
+              [% row.follow_up.created_for_employee.safe_name | html %]
             </td>
 
             <td>
index 41652b8..198a64c 100644 (file)
@@ -1,38 +1,37 @@
-[% USE L %]
+[% USE P %]
 
 <h1>Customer Vendor Autocomplete Testpage</h1>
 
 <br>
 Customer: with preselected id 822<br>
-[% L.customer_vendor_picker('customer_id', 822, type='customer') %]<br>
+[% P.customer_vendor.picker('customer_id', 822, type='customer') %]<br>
 
 <br><hr>
 Vendor: <br>
-[% L.customer_vendor_picker('vendor_id', '', type='vendor') %]<br>
+[% P.customer_vendor.picker('vendor_id', '', type='vendor') %]<br>
 
 <br><hr>
 customer with fat change<br>
-[% L.customer_vendor_picker('customer_id2', '', type='customer', fat_set_item=1) %]<br>
+[% P.customer_vendor.picker('customer_id2', '', type='customer', fat_set_item=1) %]<br>
 <div>id from change <span id='change1'></span></div>
 <div>greeting from fat change <span id='change2'></span></div>
 
 <br><hr>
 fat vendor with change<br>
-[% L.customer_vendor_picker('vendor_id2', '', type='vendor', fat_set_item=1) %]<br>
+[% P.customer_vendor.picker('vendor_id2', '', type='vendor', fat_set_item=1) %]<br>
 <div>id  from change<span id='change3'></span></div>
 <div>greeting from fat change <span id='change4'></span></div>
 
 <br><hr>
-this one will be reinit_widget after 4s:<br>
-<span id='vendor3' class="">
-<input id="vendor3_id" class="" type="hidden" name="vendor3_id" value="">
-<input id="vendor3_id_type" type="hidden" name="" value="vendor">
+this one will be a reinit_widget after 4s:<br>
+<span id='vendor3' class="customer_vendor_picker">
+<input id="vendor3_id" class="" type="hidden" name="vendor3_id" value="" data-customer-vendor-picker-data="{&quot;cv_type&quot;:&quot;vendor&quot;}">
 <input id="vendor3_id_name" type="text" name="" value="">
 </span>
 
 <br><hr>
-this shouold have three '-' before and after touching:<br>
----[% L.customer_vendor_picker('vendor5_id', '', type='vendor') %]---
+this should have three '-' before and after touching:<br>
+---[% P.customer_vendor.picker('vendor5_id', '', type='vendor') %]---
 
 
 <script type='text/javascript'>
@@ -49,4 +48,3 @@ window.setTimeout(function() {
   kivi.reinit_widgets();
 }, 4000);
 </script>
-
diff --git a/templates/webpages/customer_vendor_turnover/_list_open_items.html b/templates/webpages/customer_vendor_turnover/_list_open_items.html
new file mode 100644 (file)
index 0000000..f7e8ef1
--- /dev/null
@@ -0,0 +1,65 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE HTML %]
+
+<div id="sales_report">
+  <table width="100%">
+    <caption class="listtop">[%- HTML.escape(title) %]</caption>
+    <tbody>
+      <tr>
+        <td class="listheading">[% 'Type' | $T8 %]</td>
+        <td class="listheading">[% 'Invoice Number' | $T8 %]</td>
+        <td class="listheading">[% 'Invoice Date' | $T8 %]</td>
+        <td class="listheading">[% 'Amount' | $T8 %]</td>
+        <td class="listheading">[% 'Inv. Duedate' | $T8 %]</td>
+        <td class="listheading">[% 'Paid' | $T8 %]</td>
+        <td class="listheading">[% 'Open Amount' | $T8 %]</td>
+        [% IF FORM.db == 'customer' %]
+          <td class="listheading">[% 'Dunnings (Id -- Dunning Date --Dunning Level -- Dunning Fee)' | $T8 %]</td>
+        [% END %]
+      </tr>
+
+      [%- FOREACH row = OPEN_ITEMS %]
+        [% IF FORM.db == 'customer' %]
+          [% IF row.type == 'invoice' %]
+            [% SET type = 'Invoice (one letter abbreviation)' %]
+            [% SET link = 'is.pl' %]
+          [% ELSIF row.type == 'credit_note' %]
+            [% SET type = 'Credit note (one letter abbreviation)' %]
+            [% SET link = 'is.pl' %]
+          [% ELSE %]
+            [% SET type = 'AR Transaction (abbreviation)' %]
+            [% SET link = 'ar.pl' %]
+          [% END %]
+        [% ELSE %]
+          [% IF row.invoice %]
+            [% SET type = 'Invoice (one letter abbreviation)' %]
+            [% SET link = 'ir.pl' %]
+          [% ELSE %]
+            [% SET type = 'AP Transaction (abbreviation)' %]
+            [% SET link = 'ap.pl' %]
+          [% END %]
+        [% END %]
+        <tr class="listrow[% loop.count % 2 %]">
+          <td>[% type | $T8 %]</td>
+          <td><a href="[% link %]?action=edit&id=[% row.id %]">[% row.invnumber | html %]</a></td>
+          <td>[% row.transdate.to_kivitendo | html %]</td>
+          <td class="numeric">[%- LxERP.format_amount(row.amount, 2) %]</td>
+          <td>[% row.duedate.to_kivitendo | html %]</td>
+          <td class="numeric">[%- LxERP.format_amount(row.paid, 2) %]</td>
+          <td class="numeric">[%- LxERP.format_amount(row.amount - row.paid,2) %]
+          [% IF FORM.db == 'customer' %]
+            <td>
+            [%- IF row.dunning_config_id != '' %]
+              [%- FOREACH dun = row.dunnings %]
+              [% dun.dunning_id | html %] -- [% dun.transdate.to_kivitendo | html %] -- [% dun.dunning_level | html %] -- [%- LxERP.format_amount(dun.fee, 2) %]<br>
+              [% END %]
+            [% END %]
+            </td>
+          [% END %]
+        </tr>
+      [% END %]
+    </tbody>
+  </table>
+</div>
diff --git a/templates/webpages/customer_vendor_turnover/_list_open_orders.html b/templates/webpages/customer_vendor_turnover/_list_open_orders.html
new file mode 100644 (file)
index 0000000..d15933a
--- /dev/null
@@ -0,0 +1,48 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE HTML %]
+
+<div id="orders_report">
+  <table width="100%">
+    <caption class="listtop">[%- HTML.escape(title) %]</caption>
+    <tbody>
+      <tr>
+        <td class="listheading">[% 'Type' | $T8 %]</td>
+        <td class="listheading">[% 'Order/RFQ Number' | $T8 %]</td>
+        <td class="listheading">[% 'Date' | $T8 %]</td>
+        <td class="listheading">[% 'Amount' | $T8 %]</td>
+        <td class="listheading">[% 'Salesman' | $T8 %]</td>
+        <td class="listheading">[% 'Transaction description' | $T8 %]</td>
+      </tr>
+
+      [%- FOREACH row = orders %]
+      <tr class="listrow[% loop.count % 2 %]">
+        <td>[% IF row.quotation %]
+          [% IF FORM.db == 'customer' %][% 'Sales quotation' | $T8 %][% ELSE %][% 'RFQ' | $T8 %][% END %]</td>
+              [%- IF INSTANCE_CONF.get_feature_experimental_order -%]
+                <td>[% IF row.id %]<a href='controller.pl?action=Order/edit&type=[% IF FORM.db == "customer" %]sales_quotation[% ELSE %]request_quotation[% END %]&id=[% HTML.escape(row.id) %]'>
+                    [% END %][% HTML.escape(row.quonumber)   || '&nbsp;' %][% IF row.id %]</a>[% END %]</td>
+              [%- ELSE -%]
+                <td>[% IF row.id %]<a href='oe.pl?action=edit&type=[% IF FORM.db == "customer" %]sales_quotation[% ELSE %]request_quotation[% END %]&vc=[% FORM.db %]&id=[% HTML.escape(row.oe_id) %]'>
+                    [% END %][% HTML.escape(row.quonumber)   || '&nbsp;' %][% IF row.id %]</a>[% END %]</td>
+              [%- END -%]
+            [% ELSE %]
+            [% IF FORM.db == 'customer' %][% 'Sales Order' | $T8 %][% ELSE %][% 'Purchase Order' | $T8 %][% END %]</td>
+              [%- IF INSTANCE_CONF.get_feature_experimental_order -%]
+                <td>[% IF row.id %]<a href='controller.pl?action=Order/edit&type=[% IF FORM.db == "customer" %]sales_order[% ELSE %]purchase_order[% END %]&id=[% HTML.escape(row.id) %]'>
+                    [% END %][% HTML.escape(row.ordnumber)   || '&nbsp;' %][% IF row.id %]</a>[% END %]</td>
+              [%- ELSE -%]
+                <td>[% IF row.id %]<a href='oe.pl?action=edit&type=[% IF FORM.db == "customer" %]sales_order[% ELSE %]purchase_order[% END %]&vc=[% FORM.db %]&id=[% HTML.escape(row.oe_id) %]'>
+                    [% END %][% HTML.escape(row.ordnumber)   || '&nbsp;' %][% IF row.id %]</a>[% END %]</td>
+              [%- END -%]
+            [% END %]
+        <td>[% row.transdate.to_kivitendo | html %]</td>
+        <td class="numeric">[%- LxERP.format_amount(row.amount, 2) %]</td>
+        <td>[% row.employee.name | html %]</td>
+        <td>[% row.transaction_description | html %]</td>
+      </tr>
+      [% END %]
+    </tbody>
+  </table>
+</div>
diff --git a/templates/webpages/customer_vendor_turnover/_statistic_tabs.html b/templates/webpages/customer_vendor_turnover/_statistic_tabs.html
new file mode 100644 (file)
index 0000000..b6fdac4
--- /dev/null
@@ -0,0 +1,27 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE HTML %]
+<script type="text/javascript">
+  $(function() {
+    $ ( "#statistic_tabs" ).tabs();
+  });
+</script>
+<div class="tabwidget" id="statistic_tabs">
+  <ul>
+    <li><a href="#turnover_stat">[% 'Turnoverstatistic' | $T8 %]</a></li>
+    [% IF FORM.db == "customer" %]<li><a href="#dun_stat">[% 'Dunningstatistic' | $T8 %]</a></li>[% END %]
+    <li><a href="#quotations" onclick="kivi.CustomerVendorTurnover.get_sales_quotations();">[% IF FORM.db == "customer" %][% 'Sales Quotations' | $T8 %][% ELSE %][% 'Request Quotations' | $T8 %][% END %]</a></li>
+    <li><a href="#orders" onclick="kivi.CustomerVendorTurnover.get_orders();">[% 'Orders' | $T8 %]</a></li>
+    <li><a href="#invoices" onclick="kivi.CustomerVendorTurnover.get_invoices();">[% 'Invoices' | $T8 %]</a></li>
+    <li><a href="#mails" onclick="kivi.CustomerVendorTurnover.get_mails();">[% 'Mails' | $T8 %]</a></li>
+    <li><a href="#letters" onclick="kivi.CustomerVendorTurnover.get_letters();">[% 'Letters' | $T8 %]</a></li>
+  </ul>
+  <div id="turnover_stat">[% PROCESS "customer_vendor_turnover/turnover_statistic.html" %]</div>
+  <div id="dun_stat">[% PROCESS "customer_vendor_turnover/dun_statistic.html" %]</div>
+  <div id="quotations"></div>
+  <div id="orders"></div>
+  <div id="invoices"></div>
+  <div id="mails"></div>
+  <div id="letters"></div>
+</div>
diff --git a/templates/webpages/customer_vendor_turnover/count_open_items_by_year.html b/templates/webpages/customer_vendor_turnover/count_open_items_by_year.html
new file mode 100644 (file)
index 0000000..8309b05
--- /dev/null
@@ -0,0 +1,23 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE HTML %]
+<div id="dun_statistic">
+  <table width="100%">
+    <tbody>
+      <tr>
+        <td class="listheading">[% 'Month/Year' | $T8 %]</td>
+        <td class="listheading">[% 'Dunnings' | $T8 %]</td>
+        <td class="listheading">[% 'Highest Dunninglevel' | $T8 %]</td>
+      </tr>
+
+      [%- FOREACH row = SELF.dun_statistic %]
+      <tr class="listrow[% loop.count % 2 %]">
+        <td>[% row.date_part | html %]</td>
+        <td>[% row.count | html %]</td>
+        <td>[% row.max | html %]</td>
+      </tr>
+      [% END %]
+    </tbody>
+  </table>
+</div>
diff --git a/templates/webpages/customer_vendor_turnover/count_turnover.html b/templates/webpages/customer_vendor_turnover/count_turnover.html
new file mode 100644 (file)
index 0000000..6df5e45
--- /dev/null
@@ -0,0 +1,27 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE HTML %]
+<div id="turnover_statistic">
+  <table width="100%">
+    <tbody>
+      <tr>
+        <td class="listheading">[% 'Month/Year' | $T8 %]</td>
+        <td class="listheading">[% 'Invoices' | $T8 %]</td>
+        <td class="listheading">[% 'Turnover' | $T8 %]</td>
+        <td class="listheading">[% 'Net.Turnover' | $T8 %]</td>
+        <td class="listheading">[% 'Paid' | $T8 %]</td>
+      </tr>
+      [%- FOREACH row = SELF.turnover_statistic %]
+      <tr class="listrow[% loop.count % 2 %]">
+        <td>[% row.date_part | html %]</td>
+        <td>[% row.count | html %]</td>
+        <td class="numeric">[%- LxERP.format_amount(row.amount,2) %]</td>
+        <td class="numeric">[%- LxERP.format_amount(row.netamount,2) %]</td>
+        <td class="numeric">[%- LxERP.format_amount(row.paid,2) %]</td>
+      </tr>
+      [% END %]
+
+    </tbody>
+  </table>
+</div>
diff --git a/templates/webpages/customer_vendor_turnover/dun_statistic.html b/templates/webpages/customer_vendor_turnover/dun_statistic.html
new file mode 100644 (file)
index 0000000..e0c95c5
--- /dev/null
@@ -0,0 +1,11 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE HTML %]
+
+<p>
+[% L.radio_button_tag('period', value='year', label= LxERP.t8('Year'), onclick='kivi.CustomerVendorTurnover.show_dun_stat("y");') %]
+
+[% L.radio_button_tag('period', value='month', label= LxERP.t8('Month'), onclick='kivi.CustomerVendorTurnover.show_dun_stat("m");') %]
+</p>
+<div id="duns"></div>
diff --git a/templates/webpages/customer_vendor_turnover/email_statistic.html b/templates/webpages/customer_vendor_turnover/email_statistic.html
new file mode 100644 (file)
index 0000000..ba60899
--- /dev/null
@@ -0,0 +1,40 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE HTML %]
+[%- USE Dumper %]
+
+<div id="invoice_statistic">
+  <table width="100%">
+    <tbody>
+      <tr>
+        <td class="listheading">[% 'Sent on' | $T8 %]</td>
+        <td class="listheading">[% 'Subject' | $T8 %]</td>
+        <td class="listheading">[% 'Record Type' | $T8 %]</td>
+        <td class="listheading">[% 'Record number' | $T8 %]</td>
+        <td class="listheading">[% 'From' | $T8 %]</td>
+        <td class="listheading">[% 'To' | $T8 %]</td>
+        <td class="listheading">[% 'Status' | $T8 %]</td>
+      </tr>
+
+      [%- FOREACH row = emails %]
+      <tr class="listrow[% loop.count % 2 %]">
+        <td>[% row.sent_on | html %]</td>
+        <td>
+          <a href="[% SELF.url_for(controller='controller.pl', action => 'EmailJournal/show', id => row.id, back_to => SELF.get_callback) %]">
+           [%- HTML.escape(row.subject) %]
+          </a></td>
+        <td>[% row.type | $T8 %]</td>
+        <td>[% row.recordnumber | html %]</td>
+        <td>[% row.from %]</td>
+        <td>[% row.recipients %]</td>
+        <td>[% row.status %]</td>
+      </tr>
+      [% END %]
+    </tbody>
+  </table>
+
+
+    </tbody>
+  </table>
+</div>
diff --git a/templates/webpages/customer_vendor_turnover/invoices_statistic.html b/templates/webpages/customer_vendor_turnover/invoices_statistic.html
new file mode 100644 (file)
index 0000000..8d6e61b
--- /dev/null
@@ -0,0 +1,55 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE HTML %]
+<div id="invoice_statistic">
+  <table width="100%">
+    <tbody>
+      <tr>
+        <td class="listheading">[% 'Type' | $T8 %]</td>
+        <td class="listheading">[% 'Invoice Number' | $T8 %]</td>
+        <td class="listheading">[% 'Invoice Date' | $T8 %]</td>
+        <td class="listheading">[% 'Amount' | $T8 %]</td>
+        <td class="listheading">[% 'Inv. Duedate' | $T8 %]</td>
+        <td class="listheading">[% 'Paid' | $T8 %]</td>
+        <td class="listheading">[% 'Open Amount' | $T8 %]</td>
+      </tr>
+
+      [%- FOREACH row = invoices %]
+        [% IF FORM.db == 'customer' %]
+          [% IF row.type == 'invoice' %]
+            [% SET type = 'Invoice (one letter abbreviation)' %]
+            [% SET link = 'is.pl' %]
+          [% ELSIF row.type == 'credit_note' %]
+            [% SET type = 'Credit note (one letter abbreviation)' %]
+            [% SET link = 'is.pl' %]
+          [% ELSE %]
+            [% SET type = 'AR Transaction (abbreviation)' %]
+            [% SET link = 'ar.pl' %]
+          [% END %]
+        [% ELSE %]
+          [% IF row.invoice %]
+            [% SET type = 'Invoice (one letter abbreviation)' %]
+            [% SET link = 'ir.pl' %]
+          [% ELSE %]
+            [% SET type = 'AP Transaction (abbreviation)' %]
+            [% SET link = 'ap.pl' %]
+          [% END %]
+        [% END %]
+        <tr class="listrow[% loop.count % 2 %]">
+          <td>[% type | $T8 %]</td>
+          <td><a href="[% link %]?action=edit&id=[% row.id %]">[% row.invnumber | html %]</a></td>
+          <td>[% row.transdate.to_kivitendo | html %]</td>
+          <td class="numeric">[%- LxERP.format_amount(row.amount, 2) %]</td>
+          <td>[% row.duedate.to_kivitendo | html %]</td>
+          <td class="numeric">[%- LxERP.format_amount(row.paid, 2) %]</td>
+          <td class="numeric">[%- LxERP.format_amount(row.amount - row.paid, 2) %]
+        </tr>
+      [% END %]
+    </tbody>
+  </table>
+
+
+    </tbody>
+  </table>
+</div>
diff --git a/templates/webpages/customer_vendor_turnover/letter_statistic.html b/templates/webpages/customer_vendor_turnover/letter_statistic.html
new file mode 100644 (file)
index 0000000..c320884
--- /dev/null
@@ -0,0 +1,46 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE HTML %]
+[%- USE Dumper %]
+
+<div id="invoice_statistic">
+  <table width="100%">
+    <tbody>
+      <tr>
+        <td class="listheading">[% 'Date' | $T8 %]</td>
+        <td class="listheading">[% 'Subject' | $T8 %]</td>
+        <td class="listheading">[% 'Letternumber' | $T8 %]</td>
+        <td class="listheading">[% 'Contact' | $T8 %]</td>
+        <td class="listheading">[% 'Author' | $T8 %]</td>
+      </tr>
+
+      [%- FOREACH row = letters %]
+      [% IF row.customer_id %]
+        [% SET is_sales = 1 %]
+      [% ELSE %]
+        [% SET is_sales = 0 %]
+      [% END %]
+      <tr class="listrow[% loop.count % 2 %]">
+        <td>[% row.date.to_kivitendo | html %]</td>
+        <td>
+          <a href="[% SELF.url_for(controller='controller.pl', action => 'Letter/edit', 'letter.id' => row.id, is_sales=is_sales, back_to => SELF.get_callback) %]">
+           [%- HTML.escape(row.subject) %]
+          </a>
+        </td>
+        <td>
+          <a href="[% SELF.url_for(controller='controller.pl', action => 'Letter/edit', 'letter.id' => row.id, is_sales=is_sales, back_to => SELF.get_callback) %]">
+          [% row.letternumber | html %]
+          </a>
+        </td>
+        <td>[% row.contact.cp_givenname %] [% row.contact.cp_name %]</td>
+        <td>[% row.employee.name %]</td>
+      </tr>
+      [% END %]
+    </tbody>
+  </table>
+
+
+    </tbody>
+  </table>
+</div>
diff --git a/templates/webpages/customer_vendor_turnover/order_statistic.html b/templates/webpages/customer_vendor_turnover/order_statistic.html
new file mode 100644 (file)
index 0000000..c295699
--- /dev/null
@@ -0,0 +1,35 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE HTML %]
+<div id="invoice_statistic">
+  <table width="100%">
+    <tbody>
+      <tr>
+        <td class="listheading">[% 'Order Number' | $T8 %]</td>
+        <td class="listheading">[% 'Order Date' | $T8 %]</td>
+        <td class="listheading">[% 'Amount' | $T8 %]</td>
+        <td class="listheading">[% 'Delivery Date' | $T8 %]</td>
+        <td class="listheading">[% 'Transaction description' | $T8 %]</td>
+      </tr>
+
+      [%- FOREACH row = orders %]
+      <tr class="listrow[% loop.count % 2 %]">
+        [%- IF INSTANCE_CONF.get_feature_experimental_order -%]
+          <td>[% IF row.id %]<a href='controller.pl?action=Order/edit&type=[% IF FORM.db == "customer" %]sales_order[% ELSE %]purchase_order[% END %]&id=[% HTML.escape(row.id) %]'>[% END %][% HTML.escape(row.ordnumber)   || '&nbsp;' %][% IF row.id %]</a>[% END %]</td>
+        [%- ELSE -%]
+          <td>[% IF row.id %]<a href='oe.pl?action=edit&type=[% IF FORM.db == "customer" %]sales_order[% ELSE %]purchase_order[% END %]&vc=[% FORM.db %]&id=[% HTML.escape(row.id) %]'>[% END %][% HTML.escape(row.ordnumber)   || '&nbsp;' %][% IF row.id %]</a>[% END %]</td>
+        [%- END -%]
+        <td>[% row.transdate.to_kivitendo | html %]</td>
+        <td class="numeric">[%- LxERP.format_amount(row.amount, 2) %]</td>
+        <td>[% row.reqdate.to_kivitendo | html %]</td>
+        <td>[% row.transaction_description %]</td>
+      </tr>
+      [% END %]
+    </tbody>
+  </table>
+
+
+    </tbody>
+  </table>
+</div>
diff --git a/templates/webpages/customer_vendor_turnover/quotation_statistic.html b/templates/webpages/customer_vendor_turnover/quotation_statistic.html
new file mode 100644 (file)
index 0000000..02b808d
--- /dev/null
@@ -0,0 +1,37 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE HTML %]
+<div id="invoice_statistic">
+  <table width="100%">
+    <tbody>
+      <tr>
+        <td class="listheading">[% IF FORM.db == 'customer' %][% 'Quotation Number' | $T8 %][% ELSE %][% 'RFQ Number' | $T8 %][% END %]</td>
+        <td class="listheading">[% IF FORM.db == 'customer' %][% 'Quotation Date' | $T8 %][% ELSE %][% 'RFQ Date' | $T8 %][% END %]</td>
+        <td class="listheading">[% 'Amount' | $T8 %]</td>
+        <td class="listheading">[% 'Delivery Date' | $T8 %]</td>
+        <td class="listheading">[% 'Transaction description' | $T8 %]</td>
+      </tr>
+
+      [%- FOREACH row = orders %]
+      <tr class="listrow[% loop.count % 2 %]">
+        [%- IF INSTANCE_CONF.get_feature_experimental_order -%]
+          <td>[% IF row.id %]<a href='controller.pl?action=Order/edit&type=[% IF FORM.db == "customer" %]sales_quotation[% ELSE %]request_quotation[% END %]&id=[% HTML.escape(row.id) %]'>
+              [% END %][% HTML.escape(row.quonumber)   || '&nbsp;' %][% IF row.id %]</a>[% END %]</td>
+        [%- ELSE -%]
+          <td>[% IF row.id %]<a href='oe.pl?action=edit&type=[% IF FORM.db == "customer" %]sales_quotation[% ELSE %]request_quotation[% END %]&vc=[% FORM.db %]&id=[% HTML.escape(row.id) %]'>
+              [% END %][% HTML.escape(row.quonumber)   || '&nbsp;' %][% IF row.id %]</a>[% END %]</td>
+        [%- END -%]
+        <td>[% row.transdate.to_kivitendo | html %]</td>
+        <td class="numeric">[%- LxERP.format_amount(row.amount, 2) %]</td>
+        <td>[% row.reqdate.to_kivitendo | html %]</td>
+        <td>[% row.transaction_description %]</td>
+      </tr>
+      [% END %]
+    </tbody>
+  </table>
+
+
+    </tbody>
+  </table>
+</div>
diff --git a/templates/webpages/customer_vendor_turnover/turnover.html b/templates/webpages/customer_vendor_turnover/turnover.html
new file mode 100644 (file)
index 0000000..28c8264
--- /dev/null
@@ -0,0 +1,15 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE HTML %]
+[%- USE JavaScript -%]
+
+[%- IF open_items %]
+[% open_items %]
+[% END %]
+
+[%- IF open_orders %]
+[% open_orders %]
+[% END %]
+
+[% PROCESS "customer_vendor_turnover/_statistic_tabs.html" %]
diff --git a/templates/webpages/customer_vendor_turnover/turnover_statistic.html b/templates/webpages/customer_vendor_turnover/turnover_statistic.html
new file mode 100644 (file)
index 0000000..3908358
--- /dev/null
@@ -0,0 +1,10 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE HTML %]
+<p>
+[% L.radio_button_tag('period', value='year', label= LxERP.t8('Year'), onclick='kivi.CustomerVendorTurnover.show_turnover_stat("y");') %]
+
+[% L.radio_button_tag('period', value='month', label= LxERP.t8('Month'), onclick='kivi.CustomerVendorTurnover.show_turnover_stat("m");') %]
+</p>
+<div id="turnovers"></div>
index 6381c45..2e1fc62 100644 (file)
@@ -1,7 +1,8 @@
 [%- USE T8 %]
-<h1>[% 'DATEX - Export Assistent' | $T8 %]</h1>
+[%- USE L %]
+<h1>[% 'DATEV - Export Assistent' | $T8 %]</h1>
 
-<form method=post action='[% script %]'>
+<form method='post' action='[% script | html %]' id='form'>
 
 <table width=100%>
   <tr>
       <table>
         <tr>
           <td align=left nowrap>[% 'Beraternummer' | $T8 %]</td>
-          <td><input name=beraternr size=10 maxlength=7 value="[% beraternr %]"></td>
+          <td>[% L.input_tag("beraternr", beraternr, size=10, maxlength=7) %]</td>
 
           <td align=left nowrap>[% 'DFV-Kennzeichen' | $T8 %]</td>
-          <td><input name=dfvkz size=5 maxlength=2 value="[% dfvkz %]"></td>
+          <td>[% L.input_tag("dfvkz", dfvkz, size=5, maxlength=2) %]</td>
         </tr>
         <tr>
           <td align=left nowrap>[% 'Beratername' | $T8 %]</td>
-          <td><input name=beratername size=10 maxlength=9 value="[% beratername %]"></td>
+          <td>[% L.input_tag("beratername", berater, size=10, maxlength=9) %]</td>
 
           <td align=left nowrap>[% 'Password' | $T8 %]</td>
-          <td><input name=passwort size=5 maxlength=4 value="[% passwort %]"></td>
+          <td>[% L.input_tag("passwort", passwort, size=5, maxlength=4) %]</td>
         </tr>
         <tr>
           <td align=left nowrap>[% 'Mandantennummer' | $T8 %]</td>
-          <td><input name=mandantennr size=10 maxlength=5 value="[% mandantennr %]"></td>
+          <td>[% L.input_tag("mandantennr", mandantennr, size=10, maxlength=5) %]</td>
 
           <td align=left nowrap>[% 'Medium Number' | $T8 %]</td>
-          <td><input name=datentraegernr size=5 maxlength=3 value="[% datentraegernr %]"></td>
+          <td>[% L.input_tag("datentraegernr", datentraegernr, size=5, maxlength=3) %]</td>
         </tr>
         <tr>
-          <td><input type="hidden" name="kne" value="1"></td>
+          <td></td>
           <td></td>
 
           <td align=left nowrap>[% 'Abrechnungsnummer' | $T8 %]</td>
-          <td><input name=abrechnungsnr size=5 maxlength=3 value="[% abrechnungsnr %]"></td>
+          <td>[% L.input_tag("abrechnungsnr", abrechnungsnr, size=5, maxlength=3) %]</td>
         </tr>
-
-        <tr>
-          <td><input name=exporttype type=radio class=radio value=0 checked> [% 'Export Buchungsdaten' | $T8 %]</td>
-          <td></td>
-
-          <td><input name=exporttype type=radio class=radio value=1> [% 'Export Stammdaten' | $T8 %]</td>
-          <td></td>
-        </td>
       </table>
     </td>
   </tr>
-  <tr>
-    <td><hr size=3 noshade></td>
-  </tr>
 </table>
-
-<input type=hidden name=nextsub value=export2>
-
-<br>
-<input type=submit class=submit name=action value="[% 'Continue' | $T8 %]">
+[%- L.hidden_tag("exporttype",  0) %]
+[%- L.hidden_tag("exportformat",  'csv') %]
 </form>
-
index 7ea1943..d36f652 100644 (file)
@@ -1,15 +1,23 @@
 [%- USE T8 %]
 [%- USE HTML %]
-  Export in Bearbeitung<br>
+  [% 'Working on export' | $T8 %]<br>
 
   <br>
   Done.
   <br>
 
-  <br><b>[% 'KNE-Export erfolgreich!' | $T8 %]</b>
-  <br>
-  <br>
-  <a href="datev.pl?action=download&download_token=[% datev.download_token | html %]">Download</a>
+  <br><b>
+  [% 'CSV Export successful!' | $T8 %]
+
+</b>
+<br/><br/>
+[% IF WARNINGS.size %]
+  <b>[% 'Warning: One or more field value are not in valid DATEV format at:' | $T8 %]</b><br /><br />
+  [%- FOREACH warning = WARNINGS %]
+    [% warning | $T8 %]<br/>
+  [%- END %]
+<br />
+[% END %]
 
 [% IF datev.net_gross_differences.size %]
 [% INCLUDE 'datev/net_gross_difference.html'
@@ -17,4 +25,3 @@
   sum_net_gross_differences = datev.sum_net_gross_differences
 %]
 [% END %]
-
index 2593bb4..80f6c3a 100644 (file)
@@ -2,7 +2,7 @@
 [%- USE L %]
 <h1>[% 'DATEX - Export Assistent' | $T8 %]</h1>
 
-<form method=post action="[% script %]">
+<form method="post" action="[% script %]" id="form">
 
 <table width=100%>
   <tr>
@@ -29,7 +29,7 @@
             <option value=9>[% 'September' | $T8 %]</option>
             <option value=10>[% 'October' | $T8 %]</option>
             <option value=11>[% 'November' | $T8 %]</option>
-            <option value=12>[% 'December' | $T8 %]</option>
+            <option value=12>[% 'December last year period' | $T8 %]</option>
           </select></td>
         </tr>
         <tr>
     </td>
   </tr>
   <tr>
-    <td><hr size=3 noshade></td>
+   <td><hr size=1 noshade></td>
+   </tr>
+   <tr>
+     <td>
+        <table>
+         <tr>
+           <td align=left>[% 'Gldate' | $T8 %] [% 'From' | $T8 %]</td>
+           <td align=left></td>
+           <td>[% L.date_tag('gldatefrom') %]</td>
+         </tr>
+        </table>
+     </td>
+   </tr>
+   <tr>
+   <tr>
+    <td><hr size=1 noshade></td>
+   </tr>
+  [% IF ALL_DEPARTMENTS.as_list.size %]
+   <tr>
+     <td>
+        <table>
+         <tr>
+           <td align=left>[% 'Department' | $T8 %]</td>
+           <td align=left></td>
+           <td>[% L.select_tag('department_id', ALL_DEPARTMENTS, title_key = 'description', with_empty = 1) %]</td>
+         </tr>
+        </table>
+     </td>
+   </tr>
+   <tr>
+  <td><hr size=3 noshade></td>
+  </tr>
+  [% END %]
+   <tr>
+     <td>
+        <table>
+         <tr>
+          [% IF show_pk_option %]
+           <td align=left>[% 'Export with CV Charts' | $T8 %]</td>
+           <td align=left></td>
+           <td>[% L.checkbox_tag('use_pk', value = 1, checked = 0) %]</td>
+          [% ELSE %]
+           <td align=left><font color="gray">[% 'Export with CV Charts' | $T8 %]</font></td>
+           <td align=left></td>
+           <td>[% L.checkbox_tag('use_pk', value = 1, checked = 0, disabled = 1) %] </td>
+           <td colspan="2"><font color="gray">[% 'Hint: Not all VC Numbers are personal accounts compliant' | $T8 %]</font></td>
+          [% END %]
+         </tr>
+         <tr>
+           <td align=left>[% 'Lock bookings' | $T8 %]</td>
+           <td align=left></td>
+           <td colspan="3">[% L.yes_no_tag('locked', 0) %]</td>
+        </tr>
+         <tr>
+           <td align=left>[% 'Export imported bookings' | $T8 %]</td>
+           <td align=left></td>
+           <td colspan="3">[% L.yes_no_tag('imported', 0) %]</td>
+        </tr>
+        </table>
+     </td>
+   </tr>
+   <tr>
+  <td><hr size=3 noshade></td>
   </tr>
 </table>
 
-<input type=hidden name=beraternr value="[% beraternr %]">
-<input type=hidden name=dfvkz value="[% dfvkz %]">
-<input type=hidden name=beratername value="[% beratername %]">
-<input type=hidden name=passwort value="[% passwort %]">
-<input type=hidden name=mandantennr value="[% mandantennr %]">
-<input type=hidden name=datentraegernr value="[% datentraegernr %]">
-<input type=hidden name=kne value="[% kne %]">
-<input type=hidden name=abrechnungsnr value="[% abrechnungsnr %]">
+<input type=hidden name=beraternr value="[% beraternr | html %]">
+<input type=hidden name=dfvkz value="[% dfvkz | html %]">
+<input type=hidden name=beratername value="[% beratername | html %]">
+<input type=hidden name=passwort value="[% passwort | html %]">
+<input type=hidden name=mandantennr value="[% mandantennr | html %]">
+<input type=hidden name=datentraegernr value="[% datentraegernr | html %]">
+<input type=hidden name=exportformat value="[% exportformat | html %]">
+<input type=hidden name=abrechnungsnr value="[% abrechnungsnr | html %]">
 
-<input type=hidden name=exporttype value="[% exporttype %]">
+<input type=hidden name=exporttype value="[% exporttype | html %]">
 
-<input type=hidden name=nextsub value=export3>
-
-<br>
-<input type=submit class=submit name=action value="[% 'Continue' | $T8 %]">
 </form>
-
diff --git a/templates/webpages/datev/export_stammdaten.html b/templates/webpages/datev/export_stammdaten.html
deleted file mode 100644 (file)
index 593060b..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-[%- USE T8 %]
-<h1>[% 'DATEX - Export Assistent' | $T8 %]</h1>
-
-<form method=post action="[% script %]">
-<table width=100%>
-  <tr>
-    <th align=left>[% 'Konten' | $T8 %]</th>
-  </tr>
-  <tr height="5"></tr>
-  <tr valign=top>
-    <td>
-      <table>
-        <tr>
-          <td align=left>[% 'Von Konto: ' | $T8 %]</td>
-          <td align=left><input name=accnofrom size=8 maxlength=8></td>
-        </tr>
-        <tr>
-          <td align=left>[% 'Bis Konto: ' | $T8 %]</td>
-          <td align=left><input name=accnoto size=8 maxlength=8></td>
-        </tr>
-      </table>
-    </td>
-  </tr>
-  <tr>
-    <td><hr size=3 noshade></td>
-  </tr>
-</table>
-<input type=hidden name=beraternr value="[% beraternr %]">
-<input type=hidden name=dfvkz value="[% dfvkz %]">
-<input type=hidden name=beratername value="[% beratername %]">
-<input type=hidden name=passwort value="[% passwort %]">
-<input type=hidden name=mandantennr value="[% mandantennr %]">
-<input type=hidden name=datentraegernr value="[% datentraegernr %]">
-<input type=hidden name=kne value="[% kne %]">
-<input type=hidden name=abrechnungsnr value="[% abrechnungsnr %]">
-
-<input type=hidden name=exporttype value="[% exporttype %]">
-
-<input type=hidden name=nextsub value=export3>
-
-<br>
-<input type=submit class=submit name=action value="[% 'Continue' | $T8 %]">
-</form>
-
index 238e58b..f2e19e1 100644 (file)
@@ -21,7 +21,7 @@
 </p>
 
 <p>
- In dem gerade durchgeführten Export gab es [% net_gross_differences.size %]
+ In dem gerade durchgeführten Export gab es [% net_gross_differences.size | html %]
  solcher Fälle. Die Summe aller Abweichungen beläuft sich auf
  [% LxERP.format_amount(sum_net_gross_differences, 2) %].
 </p>
index 7cfa410..b0e1e8d 100644 (file)
   <table>
     <tr>
       <th class="listheading">[% 'Database ID' | $T8 %]</th>
-      <th class="listheading">[% 'Booking Date' | $T8 %]</th>
-      <th class="listheading">[% 'Invoice Date' | $T8 %]</th>
+      <th class="listheading">[% 'Gldate' | $T8 %]</th>
+      <th class="listheading">[% 'Transdate' | $T8 %]</th>
       <th class="listheading">[% 'Amount' | $T8 %]</th>
-      <th class="listheading">[% 'Buchungsnummer' | $T8 %]</th>
+      <th class="listheading">[% 'ID' | $T8 %]</th>
       <th class="listheading">[% 'Source' | $T8 %]</th>
       <th class="listheading">[% 'Reference / Invoice Number' | $T8 %]</th>
       <th class="listheading">[% 'Description' | $T8 %]</th>
@@ -54,8 +54,8 @@
   <table>
     <tr>
       <th class="listheading">[% 'Database ID' | $T8 %]</th>
-      <th class="listheading">[% 'Booking Date' | $T8 %]</th>
-      <th class="listheading">[% 'Invoice Date' | $T8 %]</th>
+      <th class="listheading">[% 'Gldate' | $T8 %]</th>
+      <th class="listheading">[% 'Transdate' | $T8 %]</th>
       <th class="listheading">[% 'Amount' | $T8 %]</th>
       <th class="listheading">[% 'Source' | $T8 %]</th>
       <th class="listheading">[% 'Account Number' | $T8 %]</th>
index 297ab06..cd8a44a 100644 (file)
@@ -1,6 +1,6 @@
 [%- USE T8 %]
 [%- USE HTML %]
-[%- USE LxERP %]
+[%- USE LxERP %][%- USE L -%]
 <form name="Form" method="post" action="controller.pl">
 
  <input type="hidden" name="action" value="LoginScreen/login">
@@ -8,6 +8,30 @@
  <p class="message_hint">
   [% LxERP.t8('kivitendo is about to update the database [ #1 ].', dbname) | html %]
  </p>
+
+ [% IF superuser.need_privileges && !superuser.have_privileges %]
+  <p>
+   [% LxERP.t8("Database superuser privileges are required for the update.") %]
+   [% LxERP.t8("Please provide corresponding credentials.") %]
+  </p>
+
+  [% IF superuser.error %]
+   <p>[% LxERP.t8("Error: #1", superuser.error) %]</p>
+  [% END %]
+
+  <table border="0">
+   <tr>
+    <td>[% LxERP.t8("User name") %]:</td>
+    <td>[% L.input_tag("database_superuser_username", superuser.username) %]</td>
+   </tr>
+
+   <tr>
+    <td>[% LxERP.t8("Password") %]:</td>
+    <td>[% L.input_tag("database_superuser_password", superuser.password, type="password") %]</td>
+   </tr>
+  </table>
+ [% END %]
+
  <p>
   [% 'You should create a backup of the database before proceeding because the backup might not be reversible.' | $T8 %]
  </p>
diff --git a/templates/webpages/delivery_order/form.html b/templates/webpages/delivery_order/form.html
new file mode 100644 (file)
index 0000000..d7facc2
--- /dev/null
@@ -0,0 +1,57 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE HTML %]
+<h1>[% FORM.title %] <span id='nr_in_title'>[%- SELF.order.number -%]</span></h1>
+
+<div id="print_options" style="display:none">
+  <form method="post" action="controller.pl" id="print_options_form">
+    [% SELF.print_options %]
+    <br>
+    [% L.button_tag('kivi.DeliveryOrder.print()', LxERP.t8('Print')) %]
+    <a href="#" onclick="$('#print_options').dialog('close');">[% LxERP.t8("Cancel") %]</a>
+  </form>
+</div>
+
+<div id="shipto_dialog" class="hidden"></div>
+
+<form method="post" action="controller.pl" id="order_form">
+  [% L.hidden_tag('callback',             FORM.callback) %]
+  [% L.hidden_tag('type',                 FORM.type) %]
+  [% L.hidden_tag('id',                   SELF.order.id) %]
+  [% L.hidden_tag('converted_from_oe_id', SELF.converted_from_oe_id) %]
+
+  [%- INCLUDE 'common/flash.html' %]
+
+  <div class="tabwidget" id="order_tabs">
+    <ul>
+      <li><a href="#ui-tabs-basic-data">[% 'Basic Data' | $T8 %]</a></li>
+[%- IF INSTANCE_CONF.get_webdav %]
+      <li><a href="#ui-tabs-webdav">[% 'WebDAV' | $T8 %]</a></li>
+[%- END %]
+[%- IF SELF.order.id AND INSTANCE_CONF.get_doc_storage %]
+      <li><a href="controller.pl?action=File/list&file_type=document&object_type=[% HTML.escape(FORM.type) %]&object_id=[% HTML.url(SELF.order.id) %]">[% 'Documents' | $T8 %]</a></li>
+      <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=[% HTML.escape(FORM.type) %]&object_id=[% HTML.url(SELF.order.id) %]">[% 'Attachments' | $T8 %]</a></li>
+[%- END %]
+[%- IF SELF.order.id %]
+      <li><a href="controller.pl?action=RecordLinks/ajax_list&object_model=DeliveryOrder&object_id=[% HTML.url(SELF.order.id) %]">[% 'Linked Records' | $T8 %]</a></li>
+[%- END %]
+    </ul>
+
+    [% PROCESS "delivery_order/tabs/basic_data.html" %]
+    [% PROCESS 'webdav/_list.html' %]
+    <div id="ui-tabs-1">
+      [%- LxERP.t8("Loading...") %]
+    </div>
+
+    <div id="shipto_inputs" class="hidden">
+      [%- PROCESS 'common/_ship_to_dialog.html'
+        vc_obj=SELF.order.customervendor
+        cs_obj=SELF.order.custom_shipto
+        cvars=SELF.order.custom_shipto.cvars_by_config
+        id_selector='#order_shipto_id' %]
+    </div>
+
+  </div>
+
+</form>
diff --git a/templates/webpages/delivery_order/stock_dialog.html b/templates/webpages/delivery_order/stock_dialog.html
new file mode 100644 (file)
index 0000000..f4935cf
--- /dev/null
@@ -0,0 +1,100 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+ [%- IF delivered %]
+ [%- SET RO = ' readonly' %]
+ [%- END %]
+
+ <table>
+  <tr>
+   <td>[% 'Part Number' | $T8 %]</td>
+   <td>[% part.partnumber | html %]</td>
+  </tr>
+  <tr>
+   <td>[% 'Description' | $T8 %]</td>
+   <td>[% part.description | html %]</td>
+  </tr>
+  <tr>
+   <td>[% 'Qty according to delivery order' | $T8 %]</td>
+   <td>[% LxERP.format_amount(do_qty) %] [% do_unit | html %]</td>
+  </tr>
+ </table>
+
+  [%- UNLESS WHCONTENTS.size %]
+  <p>[% 'There are no items in stock.' | $T8 %]</p>
+  [%- ELSE %]
+
+  [% L.hidden_tag("in_out", in_out) %]
+  [% L.hidden_tag("parts_id", parts_id) %]
+  [% L.hidden_tag("do_qty", do_qty) %]
+  [% L.hidden_tag("do_unit", do_unit) %]
+  [% L.hidden_tag("row", row, class="data-row") %]
+  [% L.hidden_tag("item_id", item_id) %]
+
+  <p>
+   <table id="stock-in-out-table">
+    <tr class="listheading">
+     <th>&nbsp;</th>
+     <th>[% 'Warehouse' | $T8 %]</th>
+     <th>[% 'Bin' | $T8 %]</th>
+     <th>[% 'Charge Number' | $T8 %]</th>
+     [% IF INSTANCE_CONF.get_show_bestbefore %]
+     <th>[% 'Best Before' | $T8 %]</th>
+     [% END %]
+     [%- UNLESS delivered %]
+     <th align="right">[% 'Available qty' | $T8 %]</th>
+     [%- END %]
+     <th align="right">[% 'Qty' | $T8 %]</th>
+     <th align="right">[% 'Unit' | $T8 %]</th>
+    </tr>
+
+    [%- FOREACH row = WHCONTENTS %]
+    <tr [% IF row.stock_error %] class="error"[% ELSE %]class="listrow"[% END %]>
+     <td>[% loop.count %]</td>
+     <td>[% row.warehousedescription | html %]</td>
+     <td>[% row.bindescription | html %]</td>
+     <td>[% row.chargenumber | html %]</td>
+     [% IF INSTANCE_CONF.get_show_bestbefore %]
+     <td>[% row.bestbefore | html %]</td>
+     [% END %]
+
+     [%- IF delivered %]
+
+     <td>[% LxERP.format_amount(row.stock_qty) | html %]</td>
+     <td>
+      [% row.stock_unit | html %]
+      [% L.hidden_tag("unit", row.stock_unit, class="data-unit") %]
+     </td>
+
+     [%- ELSE %]
+
+     <td>[% row.available_qty | html %]</td>
+     <td>
+      [% L.input_tag("qty", row.stock_qty                              ? LxERP.format_amount(row.stock_qty)
+                          : (WHCONTENTS.size == 1) && (!row.stock_qty) ? LxERP.format_amount(do_qty)
+                          : "", class="numeric data-qty", size="12") %]</td>
+     <td>[% L.select_tag("unit_" _ loop.count, part.unit_obj.convertible_units, value_key="name", default=row.stock_unit, class="data-unit") %]</td>
+
+     [%- END %]
+     <td style="display:none">
+      [% L.hidden_tag("warehouse_id", row.warehouse_id, class="data-warehouse-id") %]
+      [% L.hidden_tag("bin_id", row.bin_id, class="data-bin-id") %]
+      [% L.hidden_tag("chargenumber", row.chargenumber, class="data-chargenumber") %]
+      [% L.hidden_tag("delivery_order_items_stock_id", row.delivery_order_items_stock_id, class="data-stock-id") %]
+      [% L.hidden_tag("bestbefore", row.bestbefore, class="data-bestbefore") IF INSTANCE_CONF.get_show_bestbefore %]
+     </td>
+    </tr>
+
+    [%- END %]
+   </table>
+  </p>
+
+  <hr size="3" noshade>
+
+  <p>[% L.button_tag('kivi.DeliveryOrder.save_updated_stock()', LxERP.t8('Save')) IF !delivered %]</p>
+
+  [%- END %]
+ </form>
+
+
diff --git a/templates/webpages/delivery_order/tabs/_business_info_row.html b/templates/webpages/delivery_order/tabs/_business_info_row.html
new file mode 100644 (file)
index 0000000..cdd61a6
--- /dev/null
@@ -0,0 +1,10 @@
+[%- USE T8 %][%- USE HTML %]
+
+<tr id='business_info_row' [%- IF !SELF.order.customervendor.business_id %]style='display:none'[%- END %]>
+  <th align="right">[%- IF SELF.cv == 'customer' -%]
+                      [%- 'Customer type' | $T8 -%]
+                    [%- ELSE -%]
+                      [%- 'Vendor type' | $T8 -%]
+                    [%- END -%]</th>
+  <td>[% HTML.escape(SELF.order.customervendor.business.description) %]; [% 'Trade Discount' | $T8 %] [% SELF.order.customervendor.business.discount_as_percent %] %</td>
+</tr>
diff --git a/templates/webpages/delivery_order/tabs/_item_input.html b/templates/webpages/delivery_order/tabs/_item_input.html
new file mode 100644 (file)
index 0000000..ab62bd9
--- /dev/null
@@ -0,0 +1,44 @@
+[%- USE T8 %][%- USE HTML %][%- USE LxERP %][%- USE L %][%- USE P %]
+
+ <div>
+  <table id="input_row_table_id">
+    <thead>
+      <tr class="listheading">
+        <th class="listheading" nowrap >[%- '+'            | $T8 %] </th>
+        <th class="listheading" nowrap >[%- 'position'     | $T8 %] </th>
+        <th class="listheading" nowrap >[%- 'Part'         | $T8 %] </th>
+        <th class="listheading" nowrap >[%- 'Description'  | $T8 %] </th>
+        <th class="listheading" nowrap width="5" >[%- 'Qty'          | $T8 %] </th>
+        <th></th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr valign="top" class="listrow">
+        <td class="tooltipster-html" title="[%- 'Create a new part' | $T8 -%]">
+          [% SET type_options = [[ 'part', LxERP.t8('Part') ], [ 'assembly', LxERP.t8('Assembly') ]] %]
+          [%- IF INSTANCE_CONF.get_feature_experimental_assortment %]
+            [%- type_options.push([ 'assortment', LxERP.t8('Assortment')]) %]
+          [%- END %]
+          [% L.select_tag('add_item.create_part_type', type_options) %]
+          [% L.button_tag('kivi.DeliveryOrder.create_part()', LxERP.t8('+')) %]
+        </td>
+        <td>[% L.input_tag('add_item.position', '', size = 5, class="add_item_input numeric") %]</td>
+        <td>
+          [%- SET PARAM_KEY = SELF.type_data.properties('is_customer') ? 'with_customer_partnumber' : 'with_makemodel' -%]
+          [%- SET PARAM_VAL = SELF.search_cvpartnumber -%]
+          [% P.part.picker('add_item.parts_id', SELF.created_part, style='width: 300px', class="add_item_input",
+                            fat_set_item=1,
+                            multiple_pos_input=1,
+                            action={set_multi_items='kivi.DeliveryOrder.add_multi_items'},
+                            classification_id=SELF.part_picker_classification_ids.as_list.join(','),
+                            $PARAM_KEY=PARAM_VAL) %]</td>
+        <td>[% L.input_tag('add_item.description', SELF.created_part.description, class="add_item_input") %]</td>
+        <td>
+          [% L.input_tag('add_item.qty_as_number', '', size = 5, class="add_item_input numeric") %]
+          [% L.hidden_tag('add_item.unit', SELF.created_part.unit, class="add_item_input") %]
+        </td>
+        <td>[% L.button_tag('kivi.DeliveryOrder.add_item()', LxERP.t8('Add part')) %]</td>
+      </tr>
+    </tbody>
+  </table>
+ </div>
diff --git a/templates/webpages/delivery_order/tabs/_row.html b/templates/webpages/delivery_order/tabs/_row.html
new file mode 100644 (file)
index 0000000..82422ef
--- /dev/null
@@ -0,0 +1,110 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE P %]
+
+<tbody class="row_entry listrow" data-position="[%- HTML.escape(ITEM.position) -%]"[%- IF MYCONFIG.show_form_details -%] data-expanded="1"[%- END -%]>
+  <tr>
+    <td align="center">
+      [%- IF MYCONFIG.show_form_details %]
+        [% L.img_tag(src="image/collapse.svg",
+                     alt=LxERP.t8('Hide details'), title=LxERP.t8('Hide details'), class="expand") %]
+      [%- ELSE %]
+        [% L.img_tag(src="image/expand.svg",
+                     alt=LxERP.t8('Show details'), title=LxERP.t8('Show details'), class="expand") %]
+      [%- END %]
+      [% L.hidden_tag("orderitem_ids[+]", ID) %]
+      [% L.hidden_tag("converted_from_orderitems_ids[+]", ITEM.converted_from_orderitems_id) %]
+      [% L.hidden_tag("order.orderitems[+].id", ITEM.id, id='item_' _ ID) %]
+      [% L.hidden_tag("order.orderitems[].stock_info", ITEM.stock_info, class="data-stock-info") %]
+      [% L.hidden_tag("order.orderitems[].parts_id", ITEM.parts_id) %]
+    </td>
+    <td>
+      <div name="position" class="numeric">
+        [% HTML.escape(ITEM.position) %]
+      </div>
+    </td>
+    <td align="center">
+      <img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]" class="dragdrop">
+    </td>
+    <td align="center">
+      [%- L.button_tag("kivi.DeliveryOrder.delete_order_item_row(this)",
+                       LxERP.t8("X"),
+                       confirm=LxERP.t8("Are you sure?")) %]
+    </td>
+    [%- IF SELF.show_update_button -%]
+    <td align="center">
+      [%- L.img_tag(src="image/rotate_cw.svg",
+                    alt=LxERP.t8('Update from master data'),
+                    title= LxERP.t8('Update from master data'),
+                    onclick="if (!confirm('" _ LxERP.t8("Are you sure to update this position from master data?") _ "')) return false; kivi.DeliveryOrder.update_row_from_master_data(this);",
+                    id='update_from_master') %]
+    </td>
+    [%- END -%]
+    <td>
+      <div name="partnumber">
+        [%- P.link_tag(SELF.url_for(controller='Part', action='edit', 'part.id'=ITEM.part.id), ITEM.part.partnumber, target="_blank", title=LxERP.t8('Open in new window')) -%]
+      </div>
+    </td>
+    [%- IF SELF.search_cvpartnumber -%]
+    <td>
+      <div name="cvpartnumber">[% HTML.escape(ITEM.cvpartnumber) %]</div>
+    </td>
+    [%- END -%]
+    <td>
+      <div name="partclassification">[% ITEM.part.presenter.typeclass_abbreviation %]</div>
+    </td>
+    <td>
+      [% L.areainput_tag("order.orderitems[].description",
+                     ITEM.description,
+                     size='40',
+                     style='width: 300px') %]
+      [%- L.hidden_tag("order.orderitems[].longdescription", ITEM.longdescription) %]
+      [%- L.button_tag("kivi.DeliveryOrder.show_longdescription_dialog(this)", LxERP.t8("L")) %]
+    </td>
+    [%- IF (SELF.type == "sales_order" || SELF.type == "purchase_order") -%]
+    <td nowrap>
+      [%- L.div_tag(LxERP.format_amount(ITEM.shipped_qty, 2, 0) _ ' ' _ ITEM.unit, name="shipped_qty", class="numeric") %]
+    </td>
+    [%- END -%]
+    <td nowrap>
+      [%- L.input_tag("order.orderitems[].qty_as_number",
+                      ITEM.qty_as_number,
+                      size = 5,
+                      class="reformat_number numeric") %]
+      [%- IF ITEM.part.formel -%]
+        [%- L.button_tag("kivi.DeliveryOrder.show_calculate_qty_dialog(this)", LxERP.t8("*/")) %]
+        [%- L.hidden_tag("formula[+]", ITEM.part.formel) -%]
+      [%- END -%]
+    </td>
+    <td nowrap>
+      [%- L.select_tag("order.orderitems[].unit",
+                      ITEM.part.available_units,
+                      default = ITEM.unit,
+                      title_key = 'name',
+                      value_key = 'name',
+                      class = 'unitselect') %]
+    </td>
+
+    <td>
+      <span id="stock_[% ID %]" class="data-stock-qty">[% SELF.calculate_stock_in_out(ITEM) %] [% ITEM.unit %]</span>
+      [% P.button_tag("kivi.DeliveryOrder.open_stock_in_out_dialog(this, '" _ in_out _"')", "?") %]
+    </td>
+  </tr>
+
+  <tr [%- IF !MYCONFIG.show_form_details -%]style="display:none"[%- END -%]>
+    <td colspan="100%">
+      [%- IF MYCONFIG.show_form_details || ITEM.render_second_row %]
+        <div name="second_row" data-loaded="1">
+          [%- PROCESS delivery_order/tabs/_second_row.html ITEM=ITEM TYPE=SELF.type %]
+        </div>
+      [%- ELSE %]
+        <div name="second_row" id="second_row_[% ID %]">
+          [%- LxERP.t8("Loading...") %]
+        </div>
+      [%- END %]
+    </td>
+  </tr>
+
+</tbody>
diff --git a/templates/webpages/delivery_order/tabs/_second_row.html b/templates/webpages/delivery_order/tabs/_second_row.html
new file mode 100644 (file)
index 0000000..6b140ac
--- /dev/null
@@ -0,0 +1,45 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE P %]
+
+<table>
+  <tr><td colspan="100%">
+    [%- IF (TYPE == "sales_order" || TYPE == "purchase_order") %]
+      <b>[%- 'Serial No.' | $T8 %]</b>&nbsp;
+      [%- L.input_tag("order.orderitems[].serialnumber", ITEM.serialnumber, size = 15) %]&nbsp;
+    [%- END %]
+    <b>[%- 'Project' | $T8 %]</b>&nbsp;
+    [% P.project.picker("order.orderitems[].project_id", ITEM.project_id, size = 15) %]&nbsp;
+    [%- IF (TYPE == "sales_order" || TYPE == "purchase_order") %]
+      <b>[%- 'Reqdate' | $T8 %]</b>&nbsp;
+      [% L.date_tag("order.orderitems[].reqdate_as_date", ITEM.reqdate_as_date) %]&nbsp;
+    [%- END %]
+    <b>[%- 'On Hand' | $T8 %]</b>&nbsp;
+      <span[%- IF ITEM.part.onhand < ITEM.part.rop -%] class="numeric plus0"[%- END -%]>
+        [%- ITEM.part.onhand_as_number -%]&nbsp;[%- ITEM.part.unit -%]
+      </span>&nbsp;
+  </td></tr>
+
+  <tr>
+    [%- SET n = 0 %]
+    [%- FOREACH var = ITEM.cvars_by_config %]
+    [%- NEXT UNLESS (var.config.processed_flags.editable && ITEM.part.cvar_by_name(var.config.name).is_valid) %]
+    [%- SET n = n + 1 %]
+    <th>
+      [% var.config.description %]
+    </th>
+    <td>
+      [% L.hidden_tag('order.orderitems[].custom_variables[+].config_id', var.config.id) %]
+      [% L.hidden_tag('order.orderitems[].custom_variables[].id', var.id) %]
+      [% L.hidden_tag('order.orderitems[].custom_variables[].sub_module', var.sub_module) %]
+      [% INCLUDE 'common/render_cvar_input.html' var_name='order.orderitems[].custom_variables[].unparsed_value' %]
+    </td>
+    [%- IF (n % (MYCONFIG.form_cvars_nr_cols || 3)) == 0 %]
+
+  </tr><tr>
+    [%- END %]
+    [%- END %]
+  </tr>
+</table>
diff --git a/templates/webpages/delivery_order/tabs/basic_data.html b/templates/webpages/delivery_order/tabs/basic_data.html
new file mode 100644 (file)
index 0000000..f4c052b
--- /dev/null
@@ -0,0 +1,280 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE P %]
+
+[%- INCLUDE 'generic/set_longdescription.html' %]
+
+<div id="ui-tabs-basic-data">
+  <table width="100%">
+    <tr valign="top">
+      <td>
+        <table width="100%">
+          <tr>
+            <th align="right">[%- SELF.cv == "customer" ? LxERP.t8('Customer') : LxERP.t8('Vendor') -%]</th>
+            [% SET cv_id = SELF.cv _ '_id' %]
+            <td>
+              [% P.customer_vendor.picker("order.${SELF.cv}" _ '_id', SELF.order.$cv_id, type=SELF.cv, style='width: 300px') %]
+              [% P.button_tag("kivi.DeliveryOrder.show_vc_details_dialog()", LxERP.t8("Details (one letter abbreviation)")) %]
+            </td>
+          </tr>
+
+          <tr id='cp_row' [%- IF !SELF.order.${SELF.cv}.contacts.size %]style='display:none'[%- END %]>
+            <th align="right">[% 'Contact Person' | $T8 %]</th>
+            <td>[% L.select_tag('order.cp_id',
+                                SELF.order.${SELF.cv}.contacts,
+                                default=SELF.order.cp_id,
+                                title_key='full_name_dep',
+                                value_key='cp_id',
+                                with_empty=1,
+                                style='width: 300px') %]</td>
+          </tr>
+
+          <tr>
+            <th align="right">[% 'Shipping Address' | $T8 %]</th>
+            <td>
+              <span id='shipto_selection' [%- IF !SELF.order.${SELF.cv}.shipto.size %]style='display:none'[%- END %]>
+                [% shiptos = [ { shipto_id => "", displayable_id => LxERP.t8("No/individual shipping address") } ] ;
+                   FOREACH s = SELF.order.${SELF.cv}.shipto ;
+                     shiptos.push(s) ;
+                   END ;
+                   L.select_tag('order.shipto_id',
+                                 shiptos,
+                                 default=SELF.order.shipto_id,
+                                 title_key='displayable_id',
+                                 value_key='shipto_id',
+                                 with_empty=0,
+                                 style='width: 300px') %]
+              </span>
+              [% L.button_tag("kivi.DeliveryOrder.edit_custom_shipto()", LxERP.t8("Custom shipto")) %]
+            </td>
+          </tr>
+
+          [%- PROCESS delivery_order/tabs/_business_info_row.html SELF=SELF %]
+
+[%- IF SELF.all_languages.size %]
+          <tr>
+            <th align="right">[% 'Language' | $T8 %]</th>
+            <td>
+              [% L.select_tag('order.language_id', SELF.all_languages, default=SELF.order.language_id, title_key='description', with_empty=1, style='width:300px') %]
+            </td>
+          </tr>
+[%- END %]
+
+[%- IF SELF.all_departments.size %]
+          <tr>
+            <th align="right">[% 'Department' | $T8 %]</th>
+            <td>
+              [% L.select_tag('order.department_id', SELF.all_departments, default=SELF.order.department_id, title_key='description', with_empty=1, style='width:300px') %]
+            </td>
+          </tr>
+[%- END %]
+
+          <tr>
+            <th align="right">[% 'Shipping Point' | $T8 %]</th>
+            <td>[% L.input_tag('order.shippingpoint', SELF.order.shippingpoint, style='width: 300px') %]</td>
+          </tr>
+
+          <tr>
+            <th align="right">[% 'Ship via' | $T8 %]</th>
+            <td>[% L.input_tag('order.shipvia', SELF.order.shipvia, style='width: 300px') %]</td>
+          </tr>
+
+          <tr>
+            <th align="right">[% 'Transaction description' | $T8 %]</th>
+            <td>[% L.input_tag('order.transaction_description', SELF.order.transaction_description, 'data-validate'=INSTANCE_CONF.get_require_transaction_description_ps ? 'required' : '', style='width: 300px') %]</td>
+          </tr>
+
+          <tr>
+            <th align="right">[% 'Project Number' | $T8 %]</th>
+            <td>[% P.project.picker('order.globalproject_id', SELF.order.globalproject_id, style='width: 300px') %]</td>
+          </tr>
+
+        </table>
+      </td>
+
+      <td align="right">
+        <table>
+
+          <tr>
+            <td colspan="2" align="center" id="data-status-line">[% SELF.order.presenter.status_line %]</td>
+          </tr>
+
+          <tr>
+            <th align="right">[% 'Employee' | $T8 %]</th>
+            <td>[% L.select_tag('order.employee_id',
+              SELF.all_employees,
+              default=(SELF.order.employee_id ? SELF.order.employee_id : SELF.current_employee_id),
+              title_key='safe_name') %]</td>
+          </tr>
+
+          [% IF SELF.cv == 'customer' %]
+          <tr>
+            <th align="right">[% 'Salesman' | $T8 %]</th>
+            <td>[% L.select_tag('order.salesman_id',
+              SELF.all_salesmen,
+              default=(SELF.order.salesman_id ? SELF.order.salesman_id : SELF.current_employee_id),
+              title_key='safe_name') %]</td>
+          </tr>
+          [% END %]
+
+          <tr>
+            <th width="70%" align="right" nowrap>[% 'Delivery Order Number' | $T8 %]</th>
+            <td>[% L.input_tag('order.donumber', SELF.order.donumber, size = 11, onchange='kivi.DeliveryOrder.set_number_in_title(this)') %]</td>
+          </tr>
+
+          <tr>
+            <th width="70%" align="right" nowrap>[% 'Order Number' | $T8 %]</th>
+            <td>[% L.input_tag('order.ordnumber', SELF.order.ordnumber, size = 11) %]</td>
+          </tr>
+
+          <tr>
+            <th width="70%" align="right" nowrap>[% IF SELF.type_data.properties('is_customer') %][% 'Customer Order Number' | $T8 %][% ELSE %][% 'Vendor Order Number' | $T8 %][% END %]</th>
+            <td>[% L.input_tag('order.cusordnumber', SELF.order.cusordnumber, size = 11) %]</td>
+          </tr>
+
+          <tr>
+            <th width="70%" align="right" nowrap>[% 'Delivery Order Date' | $T8 %]</th>
+            <td>[% L.date_tag('order.transdate_as_date', SELF.order.transdate_as_date) %]</td>
+          </tr>
+
+          <tr>
+            <th width="70%" align="right" nowrap>[% 'Reqdate' | $T8 %]</th>
+            <td>[% L.date_tag('order.reqdate_as_date', SELF.order.reqdate_as_date, class=reqdate_class) %]</td>
+          </tr>
+
+          <tr>
+            <th width="70%" align="right" nowrap>[% 'Insert Date' | $T8 %]</th>
+            <td>[% SELF.order.itime_as_date %]</td>
+          </tr>
+        </table>
+
+      </td>
+    </tr>
+  </table>
+
+  [%- PROCESS delivery_order/tabs/_item_input.html SELF=SELF %]
+
+  [% L.button_tag('kivi.DeliveryOrder.open_multi_items_dialog()', LxERP.t8('Add multiple items')) %]
+
+  <table width="100%">
+    <tr>
+      <td>
+        [%- IF SELF.positions_scrollbar_height -%]
+          [%- SET scroll_style = 'style="overflow-y: auto; height:' _ SELF.positions_scrollbar_height _ 'vh;"' -%]
+        [%- ELSE -%]
+          [%- SET scroll_style = '' -%]
+        [%- END -%]
+        <div id="row_table_scroll_id" [%- scroll_style -%]>
+          <table id="row_table_id" width="100%">
+            <thead>
+              <tr class="listheading">
+                <th class="listheading" style='text-align:center' nowrap width="1">
+                  [%- IF MYCONFIG.show_form_details %]
+                    [%- L.img_tag(src="image/collapse.svg", alt=LxERP.t8('Hide all details'), title=LxERP.t8('Hide all details'), id='expand_all', "data-expanded"="1") %]
+                  [%- ELSE %]
+                    [%- L.img_tag(src="image/expand.svg", alt=LxERP.t8('Show all details'), title=LxERP.t8('Show all details'), id='expand_all') %]
+                  [%- END %]
+                </th>
+                <th class="listheading" nowrap width="3" >[%- 'position'     | $T8 %] </th>
+                <th class="listheading" style='text-align:center' nowrap width="1"><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]"></th>
+                <th class="listheading" style='text-align:center' nowrap width="1"><img src="image/close.png" alt="[%- LxERP.t8('delete item') %]"></th>
+                [%- IF SELF.show_update_button -%]
+                <th class="listheading" style='text-align:center' nowrap width="1">
+                  [%- L.img_tag(src="image/rotate_cw.svg",
+                                alt=LxERP.t8('Update from master data'),
+                                title= LxERP.t8('Update from master data'),
+                                onclick="if (!confirm('" _ LxERP.t8("Are you sure to update all positions from master data?") _ "')) return false; kivi.DeliveryOrder.update_all_rows_from_master_data();",
+                                id='update_from_master') %]
+                </th>
+                [%- END %]
+                <th id="partnumber_header_id"   class="listheading" nowrap width="15"><a href='#' onClick='javascript:kivi.DeliveryOrder.reorder_items("partnumber")'> [%- 'Partnumber'  | $T8 %]</a></th>
+                [%- IF SELF.search_cvpartnumber -%]
+                <th id="cvpartnumber_header_id" class="listheading" nowrap width="15"><a href='#' onClick='javascript:kivi.DeliveryOrder.reorder_items("cvpartnumber")' > [%- SELF.cv == "customer" ? LxERP.t8('Customer Part Number') : LxERP.t8('Model') %]</a></th>
+                [%- END -%]
+                <th id="partclass_header_id"    class="listheading" nowrap width="2">[%- 'Type'  | $T8 %]</th>
+                <th id="description_header_id"  class="listheading" nowrap           ><a href='#' onClick='javascript:kivi.DeliveryOrder.reorder_items("description")'>[%- 'Description' | $T8 %]</a></th>
+                [%- IF (SELF.type == "sales_order" || SELF.type == "purchase_order") -%]
+                <th id="shipped_qty_header_id"  class="listheading" nowrap width="5" ><a href='#' onClick='javascript:kivi.DeliveryOrder.reorder_items("shipped_qty")'>[%- 'Delivered'   | $T8 %]</a></th>
+                [%- END -%]
+                <th id="qty_header_id"          class="listheading" nowrap width="5" ><a href='#' onClick='javascript:kivi.DeliveryOrder.reorder_items("qty")'>        [%- 'Qty'         | $T8 %]</a></th>
+                <th class="listheading" nowrap width="5" >[%- 'Unit'         | $T8 %] </th>
+                [% IF in_out == 'in' %]
+                <th class="listheading" nowrap width="5" >[%- 'Transfer To Stock' | $T8 %] </th>
+                [% END %]
+                [% IF in_out == 'out' %]
+                <th class="listheading" nowrap width="5" >[%- 'Release From Stock' | $T8 %] </th>
+                [% END %]
+              </tr>
+            </thead>
+
+            [%- FOREACH item = SELF.order.items_sorted %]
+              [%- PROCESS delivery_order/tabs/_row.html ITEM=item ID=(item.id||item.new_fake_id)  -%]
+            [%- END %]
+
+          </table>
+        </div>
+
+      </td>
+    </tr>
+
+    <tr>
+    </tr>
+
+    <tr>
+      <td colspan="100%" width="100%">
+        <table width="100%">
+          <tr>
+            <td>
+              <table>
+                <tr>
+                  <th align="left">[% 'Notes' | $T8 %]</th>
+                  <th align="left">[% 'Internal Notes' | $T8 %]</th>
+                </tr>
+                <tr valign="top">
+                  <td>
+                    [% L.textarea_tag('order.notes', SELF.order.notes, wrap="soft", style="width: 350px; height: 150px", class="texteditor") %]
+                  </td>
+                  <td>
+                    [% L.textarea_tag('order.intnotes', SELF.order.intnotes, wrap="soft", style="width: 350px; height: 150px") %]
+                  </td>
+                </tr>
+              </table>
+            </td>
+
+            <td>
+              <table>
+                <tr>
+                  <th align="right">[% 'Payment Terms' | $T8 %]</th>
+                  <td>[% L.select_tag('order.payment_id',
+                                      SELF.all_payment_terms,
+                                      default = SELF.order.payment_id,
+                                      with_empty = 1,
+                                      title_key = 'description',
+                                      style = 'width: 250px') %]</td>
+                </tr>
+                <tr>
+                  <th align="right">[% 'Delivery Terms' | $T8 %]</th>
+                  <td>[% L.select_tag('order.delivery_term_id',
+                                      SELF.all_delivery_terms,
+                                      default = SELF.order.delivery_term_id,
+                                      with_empty = 1,
+                                      title_key = 'description',
+                                      style = 'width: 250px') %]</td>
+                </tr>
+              </table>
+            </td>
+
+          </tr>
+        </table>
+      </td>
+    </tr>
+
+  </table>
+
+  [% L.hidden_tag('order.taxzone_id', SELF.order.taxzone_id) %]
+
+</div>
+
+[% L.sortable_element('#row_table_id') %]
index e7e1199..87a9418 100644 (file)
@@ -2,7 +2,7 @@
 [%- USE L %]
 [%- USE LxERP %]
 [%- USE HTML %]
-<form action='controller.pl' method='post'>
+<form action='controller.pl' method='post' id='filter_form'>
 <div class='filter_toggle'>
 <a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Show Filter' | $T8 %]</a>
   [% SELF.filter_summary | html %]
                      style      => 'width: 200px') %]
    </td>
   </tr>
+  [%- IF SELF.all_departments.size %]
+    <tr>
+      <th align="right">[% 'Department' | $T8 %]</th>
+      <td>
+        [% L.select_tag('filter.order.department_id', SELF.all_departments, default=filter.order.department_id, title_key='description', with_empty=1, style='width:200px') %]
+      </td>
+    </tr>
+  [%- END %]
   <tr>
    <th align="right">[% 'Type' | $T8 %]</th>
    <td>
-     [% L.checkbox_tag('filter.part.type[]', checked=filter.part.type_.part,     value='part',     label=LxERP.t8('Part')) %]
-     [% L.checkbox_tag('filter.part.type[]', checked=filter.part.type_.service,  value='service',  label=LxERP.t8('Service')) %]
-     [% L.checkbox_tag('filter.part.type[]', checked=filter.part.type_.assembly, value='assembly', label=LxERP.t8('Assembly')) %]
+     [% L.checkbox_tag('filter.part.part_type[]', checked=filter.part.part_type_.part,     value='part',     label=LxERP.t8('Part')) %]
+     [% L.checkbox_tag('filter.part.part_type[]', checked=filter.part.part_type_.service,  value='service',  label=LxERP.t8('Service')) %]
+     [% L.checkbox_tag('filter.part.part_type[]', checked=filter.part.part_type_.assembly, value='assembly', label=LxERP.t8('Assembly')) %]
+     [%- IF INSTANCE_CONF.get_feature_experimental_assortment %]
+       [% L.checkbox_tag('filter.part.part_type[]', checked=filter.part.part_type_.assortment, value='assortment', label=LxERP.t8('Assortment')) %]
+     [% END %]
    </td>
   </tr>
  </table>
 
-[% L.hidden_tag('action', 'DeliveryPlan/dispatch') %]
 [% L.hidden_tag('sort_by', FORM.sort_by) %]
 [% L.hidden_tag('sort_dir', FORM.sort_dir) %]
 [% L.hidden_tag('page', FORM.page) %]
 [% L.hidden_tag('vc', SELF.vc) %]
 [% L.hidden_tag('mode', SELF.mode) %]
-[% L.input_tag('action_list', LxERP.t8('Continue'), type = 'submit', class='submit')%]
-
-
-<a href='#' onClick='javascript:$("#filter_table input").val("");$("#filter_table input[type=checkbox]").prop("checked", 0);$("#filter_table select").prop("selectedIndex", 0);'>[% 'Reset' | $T8 %]</a>
-
+[% L.button_tag('$("#filter_form").resetForm()', LxERP.t8('Reset')) %]
 </div>
 
 </form>
index 01d0ff9..7689c31 100644 (file)
@@ -1,3 +1,3 @@
 [%- USE L %]
-[%- PROCESS 'delivery_plan/_filter.html' filter=SELF.models.filtered.laundered %]
+[%- PROCESS 'delivery_plan/_filter.html' filter=SELF.models.filtered.laundered use_linked_items=SELF.use_linked_items %]
  <hr>
index 1aa2f52..3049617 100755 (executable)
@@ -1,7 +1,7 @@
-[% USE HTML %][% USE T8 %][% USE L %][% USE LxERP %]
+[% USE HTML %][% USE T8 %][% USE L %][% USE LxERP %][%- USE P -%]
 <h1>[% FORM.title %]</h1>
 
- <form method="post" action="controller.pl">
+ <form method="post" action="controller.pl" id="form">
 
 [%- INCLUDE 'common/flash.html' %]
 
@@ -9,14 +9,14 @@
    <tr>
     <td>[%- 'Description' | $T8 %]</td>
     <td>
-     <input name="delivery_term.description" value="[%- HTML.escape(SELF.delivery_term.description) %]">
+     [% P.input_tag("delivery_term.description", SELF.delivery_term.description, size="60", "data-validate"="required", "data-title"=LxERP.t8("Description")) %]
     </td>
    </tr>
 
    <tr>
     <td>[%- 'Long Description' | $T8 %]</td>
     <td>
-     <input name="delivery_term.description_long" value="[%- HTML.escape(SELF.delivery_term.description_long) %]" size="60">
+     [% P.input_tag("delivery_term.description_long", SELF.delivery_term.description_long, size="60", "data-validate"="required", "data-title"=LxERP.t8("Long Description")) %]
     </td>
    </tr>
 
      </td>
     </tr>
    [%- END %]
+  </table>
 
-  <p>
-   <input type="hidden" name="id" value="[% SELF.delivery_term.id %]">
-   <input type="hidden" name="action" value="DeliveryTerm/dispatch">
-   <input type="submit" class="submit" name="action_[% IF SELF.delivery_term.id %]update[% ELSE %]create[% END %]" value="[% 'Save' | $T8 %]">
-   [%- IF SELF.delivery_term.id %]
-    <input type="submit" class="submit" name="action_destroy" value="[% 'Delete' | $T8 %]"
-           onclick="if (confirm('[% 'Are you sure you want to delete this delivery term?' | $T8 %]')) return true; else return false;">
-   [%- END %]
-   <a href="[% SELF.url_for(action => 'list') %]">[%- 'Abort' | $T8 %]</a>
-  </p>
-
+  [% P.hidden_tag("id", SELF.delivery_term.id) %]
  </form>
-
index 517d912..6877e92 100644 (file)
@@ -3,7 +3,7 @@
 
 [%- INCLUDE 'common/flash.html' %]
 
- <form method="post" action="controller.pl">
+ <form method="post" action="controller.pl" id="form">
   [% IF !DELIVERY_TERMS.size %]
    <p>
     [%- 'No delivery term has been created yet.' | $T8 %]
     </tbody>
    </table>
   [%- END %]
-
-  <p>
-   <a href="[% SELF.url_for(action => 'new') %]">[%- 'Create new delivery term' | $T8 %]</a>
-  </p>
  </form>
 
  [% L.sortable_element('#delivery_term_list tbody', url => 'controller.pl?action=DeliveryTerm/reorder', with => 'delivery_term_id') %]
-
index fdac622..c94135f 100644 (file)
@@ -2,7 +2,7 @@
 [%- USE L %]
 [%- USE LxERP %]
 [%- USE HTML %]
-<form action='controller.pl' method='post'>
+<form action='controller.pl' method='post' id='filter_form'>
 <div class='filter_toggle'>
 <a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Show Filter' | $T8 %]</a>
   [% SELF.filter_summary | html %]
                      style      => 'width: 200px') %]
    </td>
   </tr>
+  [%- IF SELF.all_partsgroups.size %]
+  <tr>
+     <th align="right">[% 'Partsgroup' | $T8 %]</th>
+     <td>[%- L.select_tag('filter.part.partsgroup_id', SELF.all_partsgroups, default=filter.part.partsgroup_id, title_key='partsgroup', value_key='id', with_empty=1 style='width: 200px') %]</td>
+  </tr>
+  [% END %]
   <tr>
    <th align="right">[% 'Type' | $T8 %]</th>
    <td>
-     [% L.checkbox_tag('filter.part.type[]', checked=filter.part.type_.part,     value='part',     label=LxERP.t8('Part')) %]
-     [% L.checkbox_tag('filter.part.type[]', checked=filter.part.type_.service,  value='service',  label=LxERP.t8('Service')) %]
-     [% L.checkbox_tag('filter.part.type[]', checked=filter.part.type_.assembly, value='assembly', label=LxERP.t8('Assembly')) %]
+     [% L.checkbox_tag('filter.part.part_type[]', checked=filter.part.part_type_.part,     value='part',     label=LxERP.t8('Part')) %]
+     [% L.checkbox_tag('filter.part.part_type[]', checked=filter.part.part_type_.service,  value='service',  label=LxERP.t8('Service')) %]
+     [% L.checkbox_tag('filter.part.part_type[]', checked=filter.part.part_type_.assembly, value='assembly', label=LxERP.t8('Assembly')) %]
+     [%- IF INSTANCE_CONF.get_feature_experimental_assortment %]
+       [% L.checkbox_tag('filter.part.part_type[]', checked=filter.part.part_type_.assortment, value='assortment', label=LxERP.t8('Assortment')) %]
+     [% END %]
    </td>
   </tr>
  </table>
 
-[% L.hidden_tag('action', 'DeliveryValueReport/dispatch') %]
 [% L.hidden_tag('sort_by', FORM.sort_by) %]
 [% L.hidden_tag('sort_dir', FORM.sort_dir) %]
 [% L.hidden_tag('page', FORM.page) %]
 [% L.hidden_tag('vc', SELF.vc) %]
-[% L.input_tag('action_list', LxERP.t8('Continue'), type = 'submit', class='submit')%]
-
-
-<a href='#' onClick='javascript:$("#filter_table input").val("");$("#filter_table input[type=checkbox]").prop("checked", 0);$("#filter_table select").prop("selectedIndex", 0);'>[% 'Reset' | $T8 %]</a>
-
+[% L.button_tag('$("#filter_form").resetForm()', LxERP.t8('Reset')) %]
 </div>
 
 </form>
diff --git a/templates/webpages/department/form.html b/templates/webpages/department/form.html
deleted file mode 100644 (file)
index 894e259..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-[% USE HTML %][% USE T8 %][% USE L %][% USE LxERP %]
-[% SET is_used = SELF.department.is_used %]
-<h1>[% FORM.title %]</h1>
-
- <form method="post" action="controller.pl">
-
-[%- INCLUDE 'common/flash.html' %]
-
-  <table>
-   <tr>
-    <td>[%- 'Description' | $T8 %]</td>
-    <td>[% L.input_tag("department.description", SELF.department.description) %]</td>
-   </tr>
-  </table>
-
-  <p>
-   [% L.hidden_tag("id", SELF.department.id) %]
-   [% L.hidden_tag("action", "Department/dispatch") %]
-   [% L.submit_tag("action_" _  (SELF.department.id ? "update" : "create"), LxERP.t8('Save')) %]
-   [%- IF SELF.department.id && !is_used %]
-    [% L.submit_tag("action_destroy", LxERP.t8("Delete"), "confirm", LxERP.t8("Are you sure you want to delete this department?")) %]
-   [%- END %]
-   <a href="[% SELF.url_for(action => 'list') %]">[%- 'Abort' | $T8 %]</a>
-  </p>
- </form>
diff --git a/templates/webpages/department/list.html b/templates/webpages/department/list.html
deleted file mode 100644 (file)
index b932b52..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-[% USE HTML %][% USE T8 %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
-[%- INCLUDE 'common/flash.html' %]
-
- <form method="post" action="controller.pl">
-  [% IF !DEPARTMENTS.size %]
-   <p>
-    [%- 'No department has been created yet.' | $T8 %]
-   </p>
-
-  [%- ELSE %]
-   <table id="department_list" width="100%">
-    <thead>
-    <tr class="listheading">
-     <th width="100%">[%- 'Description' | $T8 %]</th>
-    </tr>
-    </thead>
-
-    <tbody>
-    [%- FOREACH department = DEPARTMENTS %]
-    <tr class="listrow[% loop.count % 2 %]" id="department_id_[% department.id %]">
-     <td>
-      <a href="[% SELF.url_for(action => 'edit', id => department.id) %]">
-       [%- HTML.escape(department.description) %]
-      </a>
-     </td>
-    </tr>
-    [%- END %]
-    </tbody>
-   </table>
-  [%- END %]
-
-  <hr size="3" noshade>
-
-  <p>
-   <a href="[% SELF.url_for(action => 'new') %]">[%- 'Create new department' | $T8 %]</a>
-  </p>
- </form>
index 6c8e0c6..1ad546c 100644 (file)
@@ -21,7 +21,7 @@
           [% L.textarea_tag("notes", notes, wrap="soft", style="width: 350px; height: 150px", class="texteditor") %]
          [% END %]
         </td>
-        <td><textarea name="intnotes" rows="[% LxERP.numtextrows(intnotes, 35, 8, 2) %]" cols="35" wrap="soft"[% RO %]>[% HTML.escape(intnotes) %]</textarea></td>
+        <td>[% L.textarea_tag("intnotes", intnotes, wrap="soft", style="width: 350px; height: 150px") %]</td>
        </tr>
 
        <tr>
  </div>
 </div>
 
-<hr size="3" noshade>
-
-  <p>[% PRINT_OPTIONS %]</p>
-
-  <p>
-   [% 'Edit the Delivery Order' | $T8 %]<br>
-   <input type="hidden" name="action" value="dispatcher">
-   <input class="submit" type="submit" name="action_update" id="update_button" value="[% 'Update' | $T8 %]">
-   [%- UNLESS delivered %]
-   [%- IF vc == 'customer' %]
-   <input class="submit" type="submit" name="action_ship_to" value="[% 'Ship to' | $T8 %]">
-   [%- END %]
-   [%- END %]
-   <input class="submit" type="submit" name="action_print" value="[% 'Print' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-   <input class="submit" type="submit" name="action_e_mail" value="[% 'E-mail' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-   [%- UNLESS delivered %]
-   <input class="submit" type="submit" name="action_save" value="[% 'Save' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-   [%- IF vc == 'customer' %]
-   <input class="submit" type="submit" name="action_transfer_out" value="[% 'Transfer out' | $T8 %]" data-check-transfer-qty="1" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-   [% IF transfer_default %]
-   <input class="submit" type="submit" name="action_transfer_out_default" value="[% 'Transfer out via default' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-   [%- END %]
-   [%- ELSE %]
-   <input class="submit" type="submit" name="action_transfer_in" value="[% 'Transfer in' | $T8 %]" data-check-transfer-qty="1" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-   [% IF transfer_default %]
-   <input class="submit" type="submit" name="action_transfer_in_default" value="[% 'Transfer in via default' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-   [%- END %]
-   [%- END %]
-   [%- END %]
-   [%- IF id %]
-     <input type="button" class="submit" onclick="follow_up_window()" value="[% 'Follow-Up' | $T8 %]">
-   [%- UNLESS closed %]
-   <input class="submit" type="submit" name="action_mark_closed" value="[% 'Mark closed' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-   [%- END %]
-   <input type="button" class="submit" onclick="set_history_window([% id %], 'id');" name="history" id="history" value="[% 'history' | $T8 %]">
-   [%- END %]
-  </p>
+  <input type="hidden" id="rowcount" name="rowcount" value="[% HTML.escape(rowcount) %]">
+  <input name="callback" type="hidden" value="[% HTML.escape(callback) %]">
 
-  [%- IF id %]
-  <p>
-   [% 'Workflow Delivery Order' | $T8 %]<br>
-   <input class="submit" type="submit" name="action_save_as_new" value="[% 'Save as new' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-   [% UNLESS delivered || (vc == 'customer' && !INSTANCE_CONF.get_sales_delivery_order_show_delete) || (vc == 'vendor' && !INSTANCE_CONF.get_purchase_delivery_order_show_delete) %]
-    [% L.submit_tag('action_delete', LxERP.t8('Delete'), confirm=LxERP.t8('Are you sure?')) %]
-   [% END %]
-   <input class="submit" type="submit" name="action_invoice" value="[% 'Invoice' | $T8 %]">
-  </p>
-  [%- END %]
+ [%- IF !delivered %]
+  <div id="shipto_inputs" class="hidden">
+   [%- PROCESS 'common/_ship_to_dialog.html' vc_obj=VC_OBJ cvars=shipto_cvars %]
+  </div>
+ [%- END %]
 
-  <input type="hidden" name="rowcount" value="[% HTML.escape(rowcount) %]">
-  <input name="callback" type="hidden" value="[% HTML.escape(callback) %]">
+  <div id="email_inputs" class="hidden"></div>
 
+  <div id="print_options" class="hidden">
+   [% PRINT_OPTIONS %]
+  </div>
  </form>
 <script type='text/javascript'>
  $(kivi.SalesPurchase.init_on_submit_checks);
 </script>
+
+[%- IF !delivered %]
+ <div id="shipto_dialog" class="hidden"></div>
+[%- END %]
+<div id="print_dialog" class="hidden">
+ [%- PROCESS 'common/_print_dialog.html' %]
+</div>
index 52b0e70..bee0ee6 100644 (file)
@@ -1,15 +1,33 @@
 [%- USE T8 %]
 [%- USE HTML %]
 [%- USE LxERP %]
-[%- USE L %]
+[%- USE L %][%- USE P -%]
+
+[%# Determine which shipping address to show if the delivery order has been shipped already. %]
+[%- IF delivered;
+      SET shipto_label = [];
+      IF shipto_id;
+        FOREACH row = ALL_SHIPTO ;
+          IF row.shipto_id == shipto_id ;
+            SET shipto_label = [ row.shiptoname, row.shiptodepartment_1, row.shiptostreet, row.shiptocity ] ;
+          END ;
+        END ;
+      ELSE ;
+        SET shipto_label = [ shiptoname, shiptodepartment_1, shiptostreet, shiptocity ] ;
+      END ;
+
+      SET shipto_label = shipto_label.grep('.') ;
+      IF !shipto_label.size ;
+        shipto_label = [ LxERP.t8('no shipping address') ] ;
+      END ;
+    END ; %]
+
 <h1>[% title %]</h1>
 
  <script type="text/javascript" src="js/show_form_details.js"></script>
  <script type="text/javascript" src="js/show_history.js"></script>
  <script type="text/javascript" src="js/show_vc_details.js"></script>
- <script type="text/javascript" src="js/common.js"></script>
  <script type="text/javascript" src="js/delivery_customer_selection.js"></script>
- <script type="text/javascript" src="js/vendor_selection.js"></script>
  <script type="text/javascript" src="js/calculate_qty.js"></script>
  <script type="text/javascript" src="js/stock_in_out.js"></script>
  <script type="text/javascript" src="js/follow_up.js"></script>
  </style>
 
  [%- IF vc == 'customer' %]
- [%- SET vc = 'customer' %]
- [%- SET the_vc_id = customer_id %]
- [%- SET the_vc = customer %]
- [%- SET the_oldvc = oldcustomer %]
  [%- SET is_customer = '1' %]
  [%- ELSE %]
  [%- SET vc = 'vendor' %]
- [%- SET the_vc_id = vendor_id %]
- [%- SET the_vc = vendor %]
- [%- SET the_oldvc = oldvendor %]
  [%- SET is_customer = '0' %]
  [%- END %]
+ [%- SET vc_id = vc _ "_id" %]
  [%- IF delivered %]
  [%- SET DISABLED = ' disabled' %]
  [%- END %]
@@ -45,7 +57,7 @@
  <p><font color="#ff0000">[% ERRORS.join('<br>') %]</font></p>
  [%- END %]
 
- <form method="post" name="do" action="do.pl">
+ <form id="form" method="post" name="do" action="do.pl">
 
  <div id="do_tabs" class="tabwidget">
   <ul>
 [%- IF INSTANCE_CONF.get_webdav %]
    <li><a href="#ui-tabs-webdav">[% 'WebDAV' | $T8 %]</a></li>
 [%- END %]
-[%- IF id %]
+[%- IF id AND INSTANCE_CONF.get_doc_storage %]
+      <li><a href="controller.pl?action=File/list&file_type=document&object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">[% 'Documents' | $T8 %]</a></li>
+      <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">[% 'Attachments' | $T8 %]</a></li>
+[%- END %]
+[%- IF id AND AUTH.assert('record_links', 1) %]
    <li><a href="controller.pl?action=RecordLinks/ajax_list&object_model=DeliveryOrder&object_id=[% HTML.url(id) %]">[% 'Linked Records' | $T8 %]</a></li>
 [%- END %]
   </ul>
 
   <div id="ui-tabs-basic-data">
 
-  <input type="hidden" name="follow_up_trans_id_1" value="[% HTML.escape(id) %]">
-  <input type="hidden" name="follow_up_trans_type_1" value="[% HTML.escape(type) %]">
-  <input type="hidden" name="follow_up_trans_info_1" value="[% HTML.escape(follow_up_trans_info) %]">
-  <input type="hidden" name="follow_up_rowcount" value="1">
-
-
-  <input type="hidden" name="action" value="[% HTML.escape(action) %]">
-  <input type="hidden" name="bcc" value="[% HTML.escape(bcc) %]">
-  <input type="hidden" name="business" value="[% HTML.escape(business) %]">
-  <input type="hidden" name="cc" value="[% HTML.escape(cc) %]">
-  <input type="hidden" name="closed" value="[% HTML.escape(closed) %]">
-  <input type="hidden" name="convert_from_oe_ids" value="[% HTML.escape(convert_from_oe_ids) %]">
-  <input type="hidden" name="currency" value="[% HTML.escape(currency) %]">
-  <input type="hidden" name="customer_klass" value="[% HTML.escape(customer_klass) %]">
-  <input type="hidden" name="discount" value="[% HTML.escape(discount) %]">
-  <input type="hidden" name="dunning_amount" value="[% HTML.escape(dunning_amount) %]">
-  <input type="hidden" name="email" value="[% HTML.escape(email) %]">
-  <input type="hidden" name="emailed" value="[% HTML.escape(emailed) %]">
-  <input type="hidden" name="format" value="[% HTML.escape(format) %]">
-  <input type="hidden" name="formname" value="[% HTML.escape(formname) %]">
-  <input type="hidden" name="id" value="[% HTML.escape(id) %]">
-  <input type="hidden" name="max_dunning_level" value="[% HTML.escape(max_dunning_level) %]">
-  <input type="hidden" name="media" value="[% HTML.escape(media) %]">
-  <input type="hidden" name="message" value="[% HTML.escape(message) %]">
-  <input type="hidden" name="printed" value="[% HTML.escape(printed) %]">
-  <input type="hidden" name="proforma" value="[% HTML.escape(proforma) %]">
-  <input type="hidden" name="queued" value="[% HTML.escape(queued) %]">
-  <input type="hidden" name="saved_donumber" value="[% HTML.escape(saved_donumber) %]">
-  <input type="hidden" name="shiptocity" value="[% HTML.escape(shiptocity) %]">
-  <input type="hidden" name="shiptocontact" value="[% HTML.escape(shiptocontact) %]">
-  <input type="hidden" name="shiptocp_gender" value="[% HTML.escape(shiptocp_gender) %]">
-  <input type="hidden" name="shiptocountry" value="[% HTML.escape(shiptocountry) %]">
-  <input type="hidden" name="shiptodepartment_1" value="[% HTML.escape(shiptodepartment_1) %]">
-  <input type="hidden" name="shiptodepartment_2" value="[% HTML.escape(shiptodepartment_2) %]">
-  <input type="hidden" name="shiptoemail" value="[% HTML.escape(shiptoemail) %]">
-  <input type="hidden" name="shiptofax" value="[% HTML.escape(shiptofax) %]">
-  <input type="hidden" name="shiptoname" value="[% HTML.escape(shiptoname) %]">
-  <input type="hidden" name="shiptophone" value="[% HTML.escape(shiptophone) %]">
-  <input type="hidden" name="shiptostreet" value="[% HTML.escape(shiptostreet) %]">
-  <input type="hidden" name="shiptozipcode" value="[% HTML.escape(shiptozipcode) %]">
-  <input type="hidden" name="shiptocp_gender" value="[% HTML.escape(shiptocp_gender) %]">
-  <input type="hidden" name="show_details" value="[% HTML.escape(show_details) %]">
-  <input type="hidden" name="subject" value="[% HTML.escape(subject) %]">
-  <input type="hidden" name="taxincluded" value="[% HTML.escape(taxincluded) %]">
-  <input type="hidden" name="taxzone_id" value="[% HTML.escape(taxzone_id) %]">
-  <input type="hidden" name="title" value="[% HTML.escape(title) %]">
-  <input type="hidden" name="type" value="[% HTML.escape(type) %]">
-  <input type="hidden" name="vc" value="[% HTML.escape(vc) %]">
-  <input type="hidden" name="lastmtime" value="[% HTML.escape(lastmtime) %]">
+  <input type="hidden" name="follow_up_trans_id_1" id="follow_up_trans_id_1" value="[% HTML.escape(id) %]">
+  <input type="hidden" name="follow_up_trans_type_1" id="follow_up_trans_type_1" value="[% HTML.escape(type) %]">
+  <input type="hidden" name="follow_up_trans_info_1" id="follow_up_trans_info_1" value="[% HTML.escape(follow_up_trans_info) %]">
+  <input type="hidden" name="follow_up_rowcount" id="follow_up_rowcount" value="1">
+
+
+  <input type="hidden" name="business" id="business" value="[% HTML.escape(business) %]">
+  <input type="hidden" name="closed" id="closed" value="[% HTML.escape(closed) %]">
+  <input type="hidden" name="convert_from_oe_ids" id="convert_from_oe_ids" value="[% HTML.escape(convert_from_oe_ids) %]">
+  <input type="hidden" name="currency" id="currency" value="[% HTML.escape(currency) %]">
+  <input type="hidden" name="customer_pricegroup_id" id="customer_pricegroup_id" value="[% HTML.escape(customer_pricegroup_id) %]">
+  <input type="hidden" name="discount" id="discount" value="[% HTML.escape(discount) %]">
+  <input type="hidden" name="dunning_amount" id="dunning_amount" value="[% HTML.escape(dunning_amount) %]">
+  <input type="hidden" name="emailed" id="emailed" value="[% HTML.escape(emailed) %]">
+  <input type="hidden" name="id" id="id" value="[% HTML.escape(id) %]">
+  <input type="hidden" name="max_dunning_level" id="max_dunning_level" value="[% HTML.escape(max_dunning_level) %]">
+  <input type="hidden" name="printed" id="printed" value="[% HTML.escape(printed) %]">
+  <input type="hidden" name="proforma" id="proforma" value="[% HTML.escape(proforma) %]">
+  <input type="hidden" name="queued" id="queued" value="[% HTML.escape(queued) %]">
+  <input type="hidden" name="saved_donumber" id="saved_donumber" value="[% HTML.escape(saved_donumber) %]">
+ [%- IF delivered %]
+  <input type="hidden" name="shipto_id" id="shipto_id" value="[% HTML.escape(shipto_id) %]">
+  <input type="hidden" name="shiptocity" id="shiptocity" value="[% HTML.escape(shiptocity) %]">
+  <input type="hidden" name="shiptocontact" id="shiptocontact" value="[% HTML.escape(shiptocontact) %]">
+  <input type="hidden" name="shiptocp_gender" id="shiptocp_gender" value="[% HTML.escape(shiptocp_gender) %]">
+  <input type="hidden" name="shiptocountry" id="shiptocountry" value="[% HTML.escape(shiptocountry) %]">
+  <input type="hidden" name="shiptogln" id="shiptogln" value="[% HTML.escape(shiptogln) %]">
+  <input type="hidden" name="shiptodepartment_1" id="shiptodepartment_1" value="[% HTML.escape(shiptodepartment_1) %]">
+  <input type="hidden" name="shiptodepartment_2" id="shiptodepartment_2" value="[% HTML.escape(shiptodepartment_2) %]">
+  <input type="hidden" name="shiptoemail" id="shiptoemail" value="[% HTML.escape(shiptoemail) %]">
+  <input type="hidden" name="shiptofax" id="shiptofax" value="[% HTML.escape(shiptofax) %]">
+  <input type="hidden" name="shiptoname" id="shiptoname" value="[% HTML.escape(shiptoname) %]">
+  <input type="hidden" name="shiptophone" id="shiptophone" value="[% HTML.escape(shiptophone) %]">
+  <input type="hidden" name="shiptostreet" id="shiptostreet" value="[% HTML.escape(shiptostreet) %]">
+  <input type="hidden" name="shiptozipcode" id="shiptozipcode" value="[% HTML.escape(shiptozipcode) %]">
+  <input type="hidden" name="shiptocp_gender" id="shiptocp_gender" value="[% HTML.escape(shiptocp_gender) %]">
+ [%- END %]
+  <input type="hidden" name="show_details" id="show_details" value="[% HTML.escape(show_details) %]">
+  <input type="hidden" name="taxincluded" id="taxincluded" value="[% HTML.escape(taxincluded) %]">
+  <input type="hidden" name="taxzone_id" id="taxzone_id" value="[% HTML.escape(taxzone_id) %]">
+  <input type="hidden" name="title" id="title" value="[% HTML.escape(title) %]">
+  <input type="hidden" name="type" id="type" value="[% HTML.escape(type) %]">
+  <input type="hidden" name="vc" id="vc" value="[% HTML.escape(vc) %]">
+  <input type="hidden" name="lastmtime" id="lastmtime" value="[% HTML.escape(lastmtime) %]">
+  <input type="hidden" name="tax_point" id="tax_point" value="[% HTML.escape(tax_point) %]">
 
   <p>
    <table width="100%">
      <td>
       <table width="100%">
        <tr>
-        <input type="hidden" name="[% vc %]_id" value="[% HTML.escape(the_vc_id) %]">
-        <input type="hidden" name="old[% vc %]" value="[% HTML.escape(the_oldvc) %]">
-        <input type="hidden" name="tradediscount" value="[% HTML.escape(tradediscount) %]">
         <th align="right">[% IF is_customer %][% 'Customer' | $T8 %][% ELSE %][% 'Vendor' | $T8 %][% END %]</th>
         <td>
-         [%- UNLESS !delivered && SHOW_VC_DROP_DOWN %]
-         <input type="text" value="[% HTML.escape(oldvcname) %]" name="[% HTML.escape(vc) %]"[% RO %]>
-         [%- ELSE %]
-         <select name="[% vc %]" class="fixed_width" onchange="document.do.update_button.click();"[% RO %]>
-          [%- FOREACH row = ALL_VC %]
-          <option value="[% HTML.escape(row.value) %]" [% IF the_oldvc == row.value %] selected[% END %]>[% HTML.escape(row.name) %]</option>
-          [%- END %]
-         </select>
-         <input type="hidden" name="select[% vc %]" value="1">
-         [%- END %]
-         <input type="button" value="[% 'Details (one letter abbreviation)' | $T8 %]" onclick="show_vc_details('[% vc %]')">
+         [% IF RO %]
+          [% P.hidden_tag(vc_id, $vc_id) %]
+          [% HTML.escape(VC_OBJ.name) %]
+         [% ELSE %]
+          [% P.customer_vendor.picker(vc_id, $vc_id, type=vc, class="fixed_width", onchange="\$('#update_button').click()") %]
+         [% END %]
+         [% P.hidden_tag("previous_" _ vc_id, $vc_id) %]
+         [% P.button_tag("show_vc_details('" _ HTML.escape(vc) _ "')", LxERP.t8("Details (one letter abbreviation)")) %]
         </td>
 
         [%- IF ALL_CONTACTS.size %]
          <th align="right">[% 'Contact Person' | $T8 %]</th>
          <td>
           [%- IF delivered %]
-          <input type="hidden" name="cp_id" value="[% HTML.escape(cp_id) %]">
-          [%- IF cp_id == row.cp_id %]
-          [%- HTML.escape(row.cp_name) %][%- IF row.cp_abteilung %] ([% HTML.escape(row.cp_abteilung) %])[% END -%]
-          [%- END %]
+            [% L.hidden_tag("cp_id", cp_id) %]
+            [% HTML.escape(CONTACT_OBJ.full_name) %][% IF CONTACT_OBJ.cp_abteilung %] ([% HTML.escape(CONTACT_OBJ.cp_abteilung) %])[% END %]
           [%- ELSE %]
             [% L.select_tag('cp_id', ALL_CONTACTS, default = cp_id, value_key = 'cp_id', title_key = 'full_name_dep', with_empty = 1, style='width: 250px') %]
           [%- END %]
         </tr>
         [%- END %]
 
-        [%- IF ALL_SHIPTO.size %]
         <tr>
          <th align="right">[% 'Shipping Address' | $T8 %]</th>
          <td>
           [%- IF delivered %]
-          <input type="hidden" name="shipto_id" value="[% HTML.escape(shipto_id) %]">
-          [%- FOREACH row = ALL_SHIPTO %]
-          [%- IF shipto_id == row.shipto_id %]
-          [%- HTML.escape(row.shiptoname) -%]
-          [%- IF row.shiptodepartment_1 %]; [% HTML.escape(row.shiptodepartment_1) -%][% END -%]
-          [%- IF row.shiptostreet %]; [% HTML.escape(row.shiptostreet) -%][% END -%]
-          [%- IF row.shiptocity %]; [% HTML.escape(row.shiptocity) -%][% END -%]
-          [%- END %]
-          [%- END %]
-
+           [% HTML.escape(shipto_label.join('; ')) %]
           [%- ELSE %]
+           [%- IF ALL_SHIPTO.size %]
             [% shiptos = [ [ "", LxERP.t8("No/individual shipping address") ] ] ;
                L.select_tag('shipto_id', shiptos.import(ALL_SHIPTO), default=shipto_id, value_key='shipto_id', title_key='displayable_id', style='width: 250px') %]
+           [%- END %]
+           [% L.button_tag("kivi.SalesPurchase.edit_custom_shipto()", LxERP.t8("Custom shipto")) %]
           [%- END %]
          </td>
         </tr>
+
+        [%- IF (vc == 'customer') && VC_OBJ.additional_billing_addresses.as_list.size %]
+        <tr>
+          <th align="right">[% 'Custom Billing Address' | $T8 %]</th>
+          <td>
+            [% L.select_tag('billing_address_id', VC_OBJ.additional_billing_addresses,
+                            with_empty=1, default=billing_address_id, value_key='id', title_key='displayable_id', style='width: 250px') %]
+          </td>
+        </tr>
         [%- END %]
 
         [%- IF business %]
         [%- END %]
        </tr>
 
+       [%- IF ALL_LANGUAGES.size %]
+       <tr>
+        <th align="right" nowrap>[% 'Language' | $T8 %]</th>
+        <td colspan="3">
+         [% L.select_tag('language_id', ALL_LANGUAGES, default = language_id, title_key = 'description', with_empty = 1, style = 'width: 250px' )%]
+        </td>
+       </tr>
+       [%- END %]
+
        [%- IF ALL_DEPARTMENTS.size %]
        <tr>
         <th align="right" nowrap>[% 'Department' | $T8 %]</th>
         <td colspan="3">
           [% IF ( delivered ) %]
             [% L.hidden_tag('department_id', department_id) %]
+          [% ELSE %]
+            [% L.select_tag('department_id', ALL_DEPARTMENTS, default = department_id, title_key = 'description', with_empty = 1, style = 'width: 250px', disabled = delivered )%]
           [% END %]
-          [% L.select_tag('department_id', ALL_DEPARTMENTS, default = department_id, title_key = 'description', with_empty = 1, style = 'width: 250px', disabled = delivered )%]
        </td>
        </tr>
        [%- END %]
 
        <tr>
         <th align="right">[% 'Transaction description' | $T8 %]</th>
-        <td colspan="3"><input name="transaction_description" id="transaction_description" size="35" value="[% HTML.escape(transaction_description) %]"[% RO %]></td>
+        <td colspan="3">[% L.input_tag("transaction_description", transaction_description, size=35, "data-validate"=(INSTANCE_CONF.get_require_transaction_description_ps ? 'required' : ''), readonly=delivered) %]</td>
        </tr>
 
       </table>
 
        <tr>
         <th width="70%" align="right" nowrap>[% 'Delivery Order Number' | $T8 %]</th>
-        <td><input name="donumber" size="11" value="[% HTML.escape(donumber) %]"[% RO %]></td>
+        <td>
+[%- IF !is_customer || INSTANCE_CONF.get_sales_purchase_record_numbers_changeable %]
+          [% L.input_tag("donumber", donumber, size="11", readonly=delivered) %]
+[%- ELSIF id %]
+          [% HTML.escape(donumber) %]
+          [% L.hidden_tag("donumber", donumber) %]
+[%- ELSE %]
+          [% LxERP.t8("will be set upon saving") %]
+[%- END %]
+        </td>
        </tr>
 
        <tr>
         <th width="70%" align="right" nowrap>[% 'Order Number' | $T8 %]</th>
-        <td><input name="ordnumber" size="11" value="[% HTML.escape(ordnumber) %]"[% RO %]></td>
+        <td><input name="ordnumber" id="ordnumber" size="11" value="[% HTML.escape(ordnumber) %]"[% RO %]></td>
        </tr>
 
        <tr>
         <th width="70%" align="right" nowrap>[% IF is_customer %][% 'Customer Order Number' | $T8 %][% ELSE %][% 'Vendor Order Number' | $T8 %][% END %]</th>
-        <td><input name="cusordnumber" size="11" value="[% HTML.escape(cusordnumber) %]"[% RO %]></td>
+        <td><input name="cusordnumber" id="cusordnumber" size="11" value="[% HTML.escape(cusordnumber) %]"[% RO %]></td>
        </tr>
 
        <tr>
index 4138ca4..e51ac70 100644 (file)
@@ -1,10 +1,15 @@
-[%- USE T8 %]
-[% USE HTML %]
- [% 'New invoice' | $T8 %]<br>
- <input class="submit" type="submit" name="action" value="[% 'Continue' | $T8 %]">
- <input type="hidden" name="nextsub" value="invoice_multi">
- <input type="hidden" name="type" value="[% HTML.escape(type) %]">
- <input type="hidden" name="vc" value="[% HTML.escape(vc) %]">
- <input type="hidden" name="callback" value="[% HTML.escape(callback) %]">
- <input type="hidden" name="rowcount" value="[% HTML.escape(rowcount) %]">
+[% USE HTML %][%- USE T8 -%][%- USE LxERP -%][%- USE P -%]
+ [% P.hidden_tag("type", type) %]
+ [% P.hidden_tag("vc", vc) %]
+ [% P.hidden_tag("rowcount", rowcount) %]
+ [% P.hidden_tag("callback", callback) %]
+
+ <div id="print_options" class="hidden">
+  [% print_options %]
+ </div>
 </form>
+
+<div id="print_dialog" class="hidden">
+ [%- PROCESS 'common/_print_dialog.html' %]
+</div>
+<div id="mass_print_dialog" style="display: none"></div>
index bd402c4..181f25a 100644 (file)
@@ -1 +1,2 @@
-<form method="post" action="do.pl">
+[%- INCLUDE 'common/flash.html' %]
+<form method="post" action="do.pl" id="form">
index a5ecf75..c143626 100644 (file)
@@ -1,6 +1,6 @@
 [%- USE T8 %]
 [%- USE L %]
-[%- USE HTML %][%- USE LxERP %]
+[%- USE HTML %][%- USE LxERP %][%- USE P -%]
 <h1>[% title %]</h1>
 
  [%- IF vc == 'customer' %]
   }
  </style>
 
- <form method="post" action="do.pl" name="Form">
+ <form method="post" action="do.pl" name="Form" id="form">
 
   <p>
    <table>
     <tr>
      <th align="right">[% IF is_customer %][% 'Customer' | $T8 %][% ELSE %][% 'Vendor' | $T8 %][% END %]</th>
-     <td colspan="3">
-      [%- UNLESS SHOW_VC_DROP_DOWN %]
-      <input type="text" name="[% HTML.escape(vc) %]" class="fixed_width initial_focus">
-      [%- ELSE %]
-      <select name="[% vc %]" class="fixed_width initial_focus">
-       <option></option>
-       [%- FOREACH row = ALL_VC %]
-       <option>[% HTML.escape(row.name) %]--[% HTML.escape(row.id) %]</option>
-       [%- END %]
-      </select>
-      <input type="hidden" name="select[% vc %]" value="1">
-      [%- END %]
-     </td>
+     <td colspan="3">[% P.input_tag(vc, "", class="fixed_width initial_focus") %]</td>
     </tr>
 
     <tr>
     [%- IF ALL_DEPARTMENTS.size %]
     <tr>
      <th align="right" nowrap>[% 'Department' | $T8 %]</th>
-     <td colspan="3">
-      <select name="department" class="fixed_width">
-       <option></option>
-       [%- FOREACH row = ALL_DEPARTMENTS %]
-       <option[% IF department == row.id %] selected[% END %]>[% HTML.escape(row.description) %]--[% HTML.escape(row.id) %]</option>
-       [%- END %]
-      </select>
-     </td>
+     <td colspan=3>[% L.select_tag('department_id', ALL_DEPARTMENTS, default = department_id, title_key = 'description', with_empty = 1, class="fixed_width") %]</td>
     </tr>
     [%- END %]
 
 
     <tr>
      <th align="right">[% 'Transaction description' | $T8 %]</th>
-     <td colspan="3"><input name="transaction_description" class="fixed_width"></td>
+     <td><input name="transaction_description" class="fixed_width"></td>
+     <th align="right">[% 'Part Description' | $T8 %]</th>
+     <td><input name="parts_description" size="20" class="fixed_width"></td>
     </tr>
 
     <tr>
      <th align="right">[% 'Project Number' | $T8 %]</th>
-     <td colspan="3">
+     <td>
       <select name="project_id" class="fixed_width">
        <option></option>
        [%- FOREACH row = ALL_PROJECTS %]
@@ -99,6 +82,8 @@
        [%- END %]
       </select>
      </td>
+     <th align="right">[% 'Part Number' | $T8 %]</th>
+     <td><input name="parts_partnumber" size="20", class="fixed_width"></td>
     </tr>
 
     <tr>
      <th align="right">[% 'Delivery Order Date' | $T8 %] [% 'From' | $T8 %]</th>
      <td>
       [% L.date_tag('transdatefrom') %]
-     </td>
-     <th align="right">[% 'Bis' | $T8 %]</th>
-     <td>
+      [% 'Bis' | $T8 %]
       [% L.date_tag('transdateto') %]
      </td>
     </tr>
      <th align="right">[% 'Reqdate' | $T8 %] [% 'From' | $T8 %]</th>
      <td>
       [% L.date_tag('reqdatefrom') %]
-     </td>
-     <th align="right">[% 'Bis' | $T8 %]</th>
-     <td>
+      [% 'Bis' | $T8 %]
       [% L.date_tag('reqdateto') %]
      </td>
     </tr>
 
-    [%- IF is_customer %]
     <tr>
      <th align="right">[% 'Insert Date' | $T8 %] [% 'From' | $T8 %]</th>
      <td>
        [% L.date_tag('insertdatefrom') %]
-     </td>
-     <th align="right">[% 'Bis' | $T8 %]</th>
-     <td>
+       [% 'Bis' | $T8 %]
        [% L.date_tag('insertdateto') %]
      </td>
     </tr>
-    [%- END %]
 
     <tr>
      <th align="right">[% 'Include in Report' | $T8 %]</th>
    </table>
   </p>
 
-  <hr size="3" noshade>
-
-  <p>
-   <input type="hidden" name="nextsub" value="orders">
+   <input type="hidden" name="action" value="orders">
    <input type="hidden" name="vc" value="[% HTML.escape(vc) %]">
    <input type="hidden" name="type" value="[% HTML.escape(type) %]">
-
-   <input class="submit" type="submit" name="action" value="[% 'Continue' | $T8 %]">
-  </p>
  </form>
index 08384f7..f8fa9d8 100644 (file)
        [% L.date_tag('bestbefore_'_ loop.count, row.bestbefore) %]
      </td>
      [% END %]
-     <td><input name="qty_[% loop.count %]" size="12" value="[% HTML.escape(LxERP.format_amount(row.qty)) %]"></td>
-
+     <td><input name="qty_[% loop.count %]" size="12"
+     [%- IF (!row.qty) && (loop.count == 1) %]
+       value="[% HTML.escape(do_qty) %]"
+     [%- ELSE %]
+       value="[% HTML.escape(LxERP.format_amount(row.qty)) %]"
+     [% END %]
+     ></td>
      <td>
       <select name="unit_[% loop.count %]">
        [%- FOREACH unit = UNITS %]
diff --git a/templates/webpages/drafts/form.html b/templates/webpages/drafts/form.html
new file mode 100644 (file)
index 0000000..1036de6
--- /dev/null
@@ -0,0 +1,43 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE LxERP %]
+[%- USE HTML %]
+
+[% INCLUDE 'common/flash.html' %]
+
+[%- IF FORM.id %]
+<h3>[% 'Update this draft.' | $T8 %]</h3>
+[%- ELSE %]
+<h3>[% 'Save as a new draft.' | $T8 %]</h3>
+[%- END %]
+
+[% L.hidden_tag('', FORM.id, id='new_draft_id') %]
+[% 'Description' | $T8 %]: [% L.input_tag('new_draft_description', FORM.description) %]
+[% L.button_tag('kivi.Draft.save("' _ HTML.escape(SELF.module) _ '", "' _ HTML.escape(SELF.submodule) _ '")', LxERP.t8('Save draft')) %]
+
+[%- IF drafts_list.size %]
+<h3>[% 'Load an existing draft' | $T8 %]</h3>
+
+<p>[% 'Warning! Loading a draft will discard unsaved data!' | $T8 %]</p>
+
+<table>
+ <tr class="listheading">
+  <th>[% 'Date' | $T8 %]</th>
+  <th>[% 'Description' | $T8 %]</th>
+ </tr>
+
+[% FOREACH row = drafts_list %]
+ <tr class="listrow">
+  <td>[% row.date | html %]</td>
+  <td>
+  [%- IF row.id == FORM.id %]
+   <b>[% row.description | html %]</b>
+  [%- ELSE %]
+   [% L.link(SELF.url_for(action='load',id=row.id), row.description) %]
+  [%- END %]
+  </td>
+  <td>[% L.html_tag('span', LxERP.t8('Delete'), class='cursor-pointer interact', onclick="kivi.Draft.delete('" _ row.id _ "')") %]</a></td>
+ </tr>
+[% END %]
+</table>
+[%- END %]
diff --git a/templates/webpages/drafts/load.html b/templates/webpages/drafts/load.html
deleted file mode 100644 (file)
index 60337a5..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-<h1>[% 'Load draft' | $T8 %]</h1>
-
- <form action="[% HTML.escape(script) %]" method="post">
-
-  <input type="hidden" name="SAVED_FORM" value="[% HTML.escape(SAVED_FORM) %]">
-
-  <table width="100%">
-   <tr>
-    <td>
-     [% 'The following drafts have been saved and can be loaded.' | $T8 %]
-    </td>
-   </tr>
-
-   <tr>
-    <td>
-     <table>
-      <tr>
-       <th class="listheading">&nbsp;</th>
-       <th class="listheading">[% 'Date' | $T8 %]</th>
-       <th class="listheading">[% 'Description' | $T8 %]</th>
-       <th class="listheading">[% 'Employee' | $T8 %]</th>
-      </tr>
-
-      [% FOREACH row = DRAFTS %]
-       <tr class="listrow[% loop.count % 2 %]">
-        <td><input type="checkbox" name="checked_[% row.id %]" value="1"></td>
-        <td>[% HTML.escape(row.itime) %]</td>
-        <td><a href="[% HTML.url(script) %]?action=load_draft&id=[% HTML.url(row.id) %]">[% HTML.escape(row.description) %]</a></td>
-        <td>[% HTML.escape(row.employee_name) %]</td>
-       </tr>
-      [% END %]
-     </table>
-    </td>
-   </tr>
-
-   <tr>
-    <td>
-     <input type="hidden" name="action" value="draft_action_dispatcher">
-     <input type="submit" class="submit" name="draft_action" value="[% 'Skip' | $T8 %]">
-     <input type="submit" class="submit" name="draft_action" value="[% 'Delete drafts' | $T8 %]">
-    </td>
-   </tr>
-  </table>
-
- </form>
diff --git a/templates/webpages/drafts/save_new.html b/templates/webpages/drafts/save_new.html
deleted file mode 100644 (file)
index 3e11d50..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-<h1>[% 'Save draft' | $T8 %]</h1>
-
- <form action="[% HTML.escape(script) %]" method="post">
-
-  <input type="hidden" name="SAVED_FORM" value="[% HTML.escape(SAVED_FORM) %]">
-
-  <table width="100%">
-   <tr>
-    <td>[% 'Enter a description for this new draft.' | $T8 %]</td>
-   </tr>
-
-   <tr>
-    <td>
-    [% 'Description' | $T8 %]:
-    <input name="draft_description">
-    </td>
-   </tr>
-
-   <tr>
-    <td>
-     <input type="submit" class="submit" name="action" value="[% 'Save draft' | $T8 %]">
-    </td>
-   </tr>
-  </table>
-
- </form>
index 4173a51..d061d2c 100644 (file)
@@ -1,43 +1,33 @@
-[%- USE T8 %]
+[%- USE T8 %][%- USE L %]
 [% USE HTML %]<script type="text/javascript" src="js/common.js"></script>
 <h1>[% title %]</h1>
 
- <form method="post" name="search" action="dn.pl">
+ <form method="post" name="search" action="dn.pl" id="form">
 
   <table>
    <tr>
     <th align="right">[% 'Customer' | $T8 %]</th>
     <td colspan="3">
-     [% IF SHOW_CUSTOMER_SELECTION %]
-      <select name="customer" class="initial_focus">
-       <option></option>
-       [% FOREACH row = all_customer %]<option>[% HTML.escape(row.name) %]--[% HTML.escape(row.id) %]</option>[% END %]
-      </select>
-      [% ELSE %]
       <input name="customer" size="35" class="initial_focus">
-     [% END %]
     </td>
    </tr>
 
-   [% IF SHOW_DUNNING_LEVEL_SELECTION %]
+   [% IF SHOW_DEPARTMENT_SELECTION %]
     <tr>
-     <th align="right">[% 'Next Dunning Level' | $T8 %]</th>
+     <th align="right">[% 'Department' | $T8 %]</th>
      <td colspan="3">
-      <select name="dunning_level">
-       <option></option>
-       [% FOREACH row = DUNNING %]<option value="[% HTML.escape(row.id) %]">[% HTML.escape(row.dunning_description) %]</option>[% END %]
-      </select>
+     [% L.select_tag('department_id', ALL_DEPARTMENTS, title_key = 'description', with_empty = 1, style=style) %]
      </td>
     </tr>
    [% END %]
 
-   [% IF SHOW_DEPARTMENT_SELECTION %]
+   [% IF SHOW_DUNNING_LEVEL_SELECTION %]
     <tr>
-     <th align="right">[% 'Department' | $T8 %]</th>
+     <th align="right">[% 'Next Dunning Level' | $T8 %]</th>
      <td colspan="3">
-      <select name="department">
+      <select name="dunning_level">
        <option></option>
-       [% FOREACH row = all_departments %]<option>[% HTML.escape(row.description) %]--[% HTML.escape(row.id) %]</option>[% END %]
+       [% FOREACH row = DUNNING %]<option value="[% HTML.escape(row.id) %]">[% HTML.escape(row.dunning_description) %]</option>[% END %]
       </select>
      </td>
     </tr>
     <th align="right" nowrap><label for="l_include_direct_debit">[% 'Include invoices with direct debit' | $T8 %]</label></th>
     <td><input type="checkbox" value="1" id="l_include_direct_debit" name="l_include_direct_debit"></td>
    </tr>
+   <tr>
+    <th align="right" nowrap><label for="l_include_credit_notes">[% 'Add open Credit Notes' | $T8 %]</label></th>
+    <td><input type="checkbox" value="1" id="l_include_credit_notes" name="l_include_credit_notes"></td>
+   </tr>
   </table>
-
-  <input type="hidden" name="nextsub" value="show_invoices">
-
-  <br>
-  <input class="submit" type="submit" name="action" value="[% 'Continue' | $T8 %]">
-
  </form>
index 87345bf..19686f0 100644 (file)
@@ -1,11 +1,12 @@
 [%- USE T8 %]
 [%- USE HTML %]
+[%- USE LxERP -%][%- USE L -%]
 <h1>[% title %]</h1>
 
  <script type="text/javascript" src="js/common.js"></script>
  <script type="text/javascript" src="js/dunning.js"></script>
 
- <form method="post" action="dn.pl" name="Form">
+ <form method="post" action="dn.pl" name="Form" id="form">
   <table>
 
    <tr>
@@ -15,6 +16,7 @@
     <th class="listheading">[% 'eMail Send?' | $T8 %]</th>
 <!--     <th class="listheading">[% 'Auto Send?' | $T8 %]</th>  -->
     <th class="listheading">[% 'Create invoice?' | $T8 %]</th>
+    <th class="listheading">[% 'Include original Invoices?' | $T8 %]</th>
     <th class="listheading">[% 'Fristsetzung' | $T8 %]</th>
     <th class="listheading">[% 'Duedate +Days' | $T8 %]</th>
     <th class="listheading">[% 'Fee' | $T8 %]</th>
@@ -44,6 +46,7 @@
 
 <!--      <td><input type="checkbox" name="auto_[% DUNNING_it.count %]" value="1" [% IF row.auto %]checked[% END %]></td> -->
      <td><input type="checkbox" name="create_invoices_for_fees_[% DUNNING_it.count %]" value="1" [% IF row.create_invoices_for_fees %]checked[% END %]></td>
+     <td><input type="checkbox" name="print_original_invoice_[% DUNNING_it.count %]" value="1" [% IF row.print_original_invoice %]checked[% END %]></td>
      <td><input name="payment_terms_[% DUNNING_it.count %]" size="3" value="[% HTML.escape(row.payment_terms) %]"></td>
      <td><input name="terms_[% DUNNING_it.count %]" size="3" value="[% HTML.escape(row.terms) %]"></td>
      <td><input name="fee_[% DUNNING_it.count %]" size="5" value="[% HTML.escape(row.fee) %]"></td>
@@ -75,6 +78,7 @@
 
 <!--     <td><input type="checkbox" name="auto_[% rowcount %]" value="1" checked></td> -->
     <td><input type="checkbox" name="create_invoices_for_fees_[% rowcount %]" value="1" checked></td>
+    <td><input type="checkbox" name="print_original_invoice_[% DUNNING_it.count %]" value="1" [% IF row.print_original_invoice %]checked[% END %]></td>
     <td><input name="payment_terms_[% rowcount %]" size="3"></td>
     <td><input name="terms_[% rowcount %]" size="3"></td>
     <td><input name="fee_[% rowcount %]" size="5"></td>
      </select>
     </td>
    </tr>
+   <tr>
+    <th align="right">[% 'Dunning Creator' | $T8 %]</th>
+    <td>[% L.select_tag('dunning_creator', [ [ 'current_employee', LxERP.t8('Current Employee') ],[ 'invoice_employee', LxERP.t8('Employee from the original invoice') ]  ], default=dunning_creator) %]
+    </td>
+   </tr>
   </table>
 
-  <hr size="3" noshade>
-
   <input type="hidden" name="callback" value="[% HTML.escape(callback) %]">
-
-  <input class="submit" type="submit" name="action" value="[% 'Save' | $T8 %]">
-
  </form>
-
index 83b59c6..53b34e4 100644 (file)
@@ -1,11 +1,11 @@
 [%- USE T8 %]
 [%- USE HTML %]
-[%- USE L %]
+[%- USE L %][%- USE P -%]
 <h1>[% title %]</h1>
 
-[% PROCESS 'common/flash.html' %]
+[% INCLUDE 'common/flash.html' %]
 
- <form method="post" name="search" action="dn.pl">
+ <form method="post" name="search" action="dn.pl" id="form">
 
   <table width="100%">
    <tr>
      <table>
       <tr>
        <th align="right">[% 'Customer' | $T8 %]</th>
-       <td colspan="3">
-        [% IF SHOW_CUSTOMER_DDBOX %]
-         <select id='customer' name="customer_id" class="initial_focus">
-          <option value=""></option>
-          [% FOREACH row = ALL_CUSTOMERS %]<option value="[% HTML.escape(row.id) %]">[% HTML.escape(row.name) %]</option>
-          [% END %]
-         </select>
-         [% ELSE %]
-         <input id='customer' name="customer" size="35" class="initial_focus">
-        [% END %]
-       </td>
+       <td colspan="3">[% P.input_tag("customer", "", size="35", class="initial_focus") %]</td>
       </tr>
 
       [% IF SHOW_DUNNING_LEVELS %]
@@ -39,7 +29,7 @@
        </tr>
       [% END %]
 
-      [% IF SHOW_DEPARTMENT_DDBOX %]
+      [% IF ALL_DEPARTMENTS.as_list.size %]
        <tr>
         <th align="right" nowrap>[% 'Department' | $T8 %]</th>
         <td colspan="3">
        <td colspan="3"><input name="invnumber" size="20"></td>
       </tr>
 
+      <tr>
+       <th align="right" nowrap>[% 'Dunning number' | $T8 %]</th>
+       <td colspan="3"><input name="dunning_id" size="20"></td>
+      </tr>
+
       <tr>
        <th align="right" nowrap>[% 'Order Number' | $T8 %]</th>
        <td colspan="3"><input name="ordnumber" size="20"></td>
        <th align="right" nowrap>[% 'Show Salesman' | $T8 %]</th>
        <td><input type="checkbox" value="1" name="l_salesman"></td>
       </tr>
+      <tr>
+       [%- IF INSTANCE_CONF.get_email_journal %]
+        <th align="right" nowrap>[% 'Show E-Mails' | $T8 %]</th>
+        <td><input type="checkbox" value="1" name="l_mails" checked></td>
+       [%- END %]
+       [%- IF INSTANCE_CONF.get_webdav %]
+        <th align="right" nowrap>[% 'Show documents in WebDAV' | $T8 %]</th>
+        <td><input type="checkbox" value="1" name="l_webdav" checked></td>
+       [%- END %]
+       [%- IF INSTANCE_CONF.get_doc_storage %]
+        <th align="right" nowrap>[% 'Show documents in file storage' | $T8 %]</th>
+        <td><input type="checkbox" value="1" name="l_documents" checked></td>
+       [%- END %]
+      </tr>
      </table>
     </td>
    </tr>
   </table>
-
-  <input type="hidden" name="nextsub" value="show_dunning">
-
-  <br>
-
-  <input class="submit" type="submit" name="action" value="[% 'Continue' | $T8 %]">
-
  </form>
index 1a40a45..b740f58 100644 (file)
@@ -1,13 +1,14 @@
 [%- USE T8 %]
-[%- USE HTML %]
+[%- USE HTML %][%- USE L -%]
 <h1>[% title %]</h1>
 
  <script type="text/javascript">
   <!--
       function email_updated() {
         window.opener.document.getElementsByName(document.Form.input_subject.value)[0].value = document.getElementsByName("email_subject")[0].value;
-        window.opener.document.getElementsByName(document.Form.input_body.value)[0].value = document.getElementsByName("email_body")[0].value;
-        window.opener.document.getElementsByName(document.Form.input_attachment.value)[0].value = document.getElementsByName("email_attachment")[0].value;        self.close();
+        window.opener.document.getElementsByName(document.Form.input_body.value)[0].value = $("#email_body").val();
+        window.opener.document.getElementsByName(document.Form.input_attachment.value)[0].value = document.getElementsByName("email_attachment")[0].value;
+        self.close();
       }
     -->
  </script>
@@ -26,7 +27,7 @@
 
    <tr>
     <td valign="top">[% 'Body:' | $T8 %]</td>
-    <td valign="top"><textarea id="email_body" name="email_body" rows="20" cols="70" wrap="soft">[% HTML.escape(email_body) %]</textarea></td>
+    <td valign="top">[% L.textarea_tag('email_body', email_body, rows=20, cols=70, class='texteditor') %]</td>
    </tr>
 
    <tr>
index 9255680..8f06327 100644 (file)
@@ -1,26 +1,2 @@
-[%- USE T8 %]
-[%- USE HTML %]
-[%- USE LxERP %]
-[%- USE L %]
 <input type="hidden" name="rowcount" value="[% rowcount %]">
-
-  <p>
-    <input type="checkbox" id='force_lang' name="force_lang" size="6" value="1">
-    [% 'Override invoice language' | $T8 %]
-    [% PRINT_OPTIONS %]
-  </p>
-
-  <p>
-   [% 'Dunnings' | $T8 %]<br>
-   [% L.hidden_tag('action', 'dispatcher') %]
-   <input type="submit" class="submit" name='action_print_multiple' value="[%- 'Print' | $T8 %]">
-   [% L.submit_tag('action_delete', LxERP.t8('Delete'), confirm=LxERP.t8('This resets the dunning process for the selected invoices. Posted dunning invoices will not be changed!')) %]
-  </p>
-
  </form>
- <script type='text/javascript'>
-   $(function() {
-     $("select[name='language_id']").prop('disabled', !$('#force_lang').prop('checked'));
-     $('#force_lang').checkall('select[name="language_id"]', 'disabled', 'inverted');
-   });
- </script>
index f835f95..403d832 100644 (file)
@@ -1,5 +1,9 @@
-[% USE HTML %] <script type="text/javascript" src="js/common.js"></script>
- <script type="text/javascript" src="js/dunning.js"></script>
+[% USE HTML %][%- USE LxERP -%][%- USE L -%]
+ <form method="post" action="dn.pl" id="form">
 
<form method="post" action="dn.pl">
 <h2>[% LxERP.t8("Print options") %]</h2>
 
+  [% L.checkbox_tag("force_lang", label=LxERP.t8('Override invoice language'), checked=force_lang) %]
+  [% PRINT_OPTIONS %]
+
+  <h2>[% LxERP.t8("Dunnings") %]</h2>
index bfc6403..949d060 100644 (file)
@@ -5,16 +5,22 @@
 
 [% SET all_active = 1 %][% FOREACH row = DUNNINGS %][% IF !row.active %][% SET all_active = 0 %][% LAST %][% END %][% END %]
 [% SET all_email = 1 %][% FOREACH row = DUNNINGS %][% IF !row.email %][% SET all_email = 0 %][% LAST %][% END %][% END %]
- <script type="text/javascript" src="js/common.js"></script>
- <script type="text/javascript" src="js/dunning.js"></script>
+[% SET all_include_invoices = 1 %][% FOREACH row = DUNNINGS %][% IF !row.print_original_invoice %][% SET all_include_invoices = 0 %][% LAST %][% END %][% END %]
+ <form name="Form" method="post" action="dn.pl" id="form">
 
<p>[% 'The columns &quot;Dunning Duedate&quot;, &quot;Total Fees&quot; and &quot;Interest&quot; show data for the previous dunning created for this invoice.' | $T8 %]</p>
 <h2>[% LxERP.t8("Print options") %]</h2>
 
- <form name="Form" method="post" action="dn.pl">
+  [% L.checkbox_tag("force_lang", label=LxERP.t8('Override invoice language'), checked=force_lang) %]
+  [% PRINT_OPTIONS %]
+
+  <h2>[% LxERP.t8("Overdue invoices") %]</h2>
+
+  <p>[% 'The columns &quot;Dunning Duedate&quot;, &quot;Total Fees&quot; and &quot;Interest&quot; show data for the previous dunning created for this invoice.' | $T8 %]</p>
 
   <table width="100%" id="dunning_invoice_list">
    <th class="listheading" colspan="2">[% 'Current / Next Level' | $T8 %]</th>
 
+   <th class="listheading">[% 'Payment description' | $T8 %]</th>
    <th class="listheading">
     [% L.checkbox_tag('selectall_active', checkall='INPUT[name*=active_]', checked=all_active) %]
     <label for="selectall_active">[% 'Active?' | $T8 %]</label>
     [% L.checkbox_tag('selectall_email', checkall='INPUT[name*=email_]', checked=all_email) %]
     <label for="selectall_email">[% 'eMail?' | $T8 %]</label>
    </th>
+   <th class="listheading">
+    [% L.checkbox_tag('selectall_include_invoices', checkall='INPUT[name*=include_invoice_]', checked=all_include_invoices) %]
+    <label for="selectall_include_invoices">[% 'Include original Invoices?' | $T8 %]</label>
+   </th>
 
    <th class="listheading">[% 'Customername' | $T8 %]</th>
+   <th class="listheading">[% 'Department' | $T8 %]</th>
    <th class="listheading">[% 'Language' | $T8 %]</th>
    <th class="listheading">[% 'Invno.' | $T8 %]</th>
    <th class="listheading">[% 'Invdate' | $T8 %]</th>
      </td>
 
      <td>
+      [% IF row.credit_note %]
+        [% LxERP.t8("Add Credit Note for this dunning level:") %]
+        <input type="hidden" name="credit_note_[% loop.count %]" value="1">
+      [% END %]
       <select name="next_dunning_config_id_[% loop.count %]">
        [% FOREACH cfg_row = row.DUNNING_CONFIG %]<option value="[% HTML.escape(cfg_row.id) %]" [% IF cfg_row.SELECTED %]selected[% END %]>[% HTML.escape(cfg_row.dunning_description) %]</option>[% END %]
       </select>
      </td>
+     <td>[% HTML.escape(row.payment_term) %]</td>
 
      <td><input type="checkbox" name="active_[% loop.count %]" value="1" [% IF row.active %]checked[% END %]></td>
-     <td><input type="checkbox" name="email_[% loop.count %]" value="1" [% IF row.email %]checked[% END %]></td>
+     <td><input type="checkbox" name="email_[% loop.count %]" value="1" [% IF row.email && row.cv_email %]checked[% END %]>[% HTML.escape(row.cv_email) %]</td>
+     <td><input type="checkbox" name="include_invoice_[% loop.count %]" value="1" [% IF row.print_original_invoice %]checked[% END %]></td>
      <td><input type="hidden" name="customername_[% loop.count %]" size="6" value="[% HTML.escape(row.customername) %]">[% HTML.escape(row.customername) %]</td>
+     <td><input type="hidden" name="department_[% loop.count %]" size="6" value="[% HTML.escape(row.departmentname) %]">[% HTML.escape(row.departmentname) %]</td>
      <td><input type="hidden" name="language_id_[% loop.count %]" size="6" value="[% HTML.escape(row.language_id) %]">[% HTML.escape(row.language) %]</td>
      <td>
       <input type="hidden" name="invnumber_[% loop.count %]" size="6" value="[% HTML.escape(row.invnumber) %]">
@@ -68,7 +86,7 @@
      <td><input type="hidden" name="inv_duedate_[% loop.count %]" size="6" value="[% HTML.escape(row.duedate) %]">[% HTML.escape(row.duedate) %]</td>
      <td align="right"><input type="hidden" name="amount_[% loop.count %]" size="6" value="[% HTML.escape(row.amount) %]">[% HTML.escape(row.amount) %]</td>
      <td align="right"><input type="hidden" name="open_amount_[% loop.count %]" size="6" value="[% HTML.escape(row.open_amount) %]">[% HTML.escape(row.open_amount) %]</td>
-     <td>[% HTML.escape(row.next_duedate) %]</td>
+     <td>[% HTML.escape(row.dunning_duedate) %]</td>
      <td align="right"><input type="hidden" name="fee_[% loop.count %]" size="6" value="[% HTML.escape(row.fee) %]">[% HTML.escape(row.fee) %]</td>
      <td align="right"><input type="hidden" name="interest_[% loop.count %]" size="6" value="[% HTML.escape(row.interest) %]">[% HTML.escape(row.interest) %]</td>
      [% IF l_include_direct_debit %]
    [% END %]
   </table>
 
-  <hr size=3 noshade>
-
-  <input type="checkbox" id='force_lang' name="force_lang" size="6" value="1">
-  [% 'Override invoice language' | $T8 %]
-  [% PRINT_OPTIONS %]
-
-  <br>
-
   <input name="rowcount" type="hidden" value="[% HTML.escape(rowcount) %]">
   <input name="groupinvoices" type="hidden" value="[% HTML.escape(groupinvoices) %]">
-
+  <input name="l_include_credit_notes" type="hidden" value="[% HTML.escape(l_include_credit_notes) %]">
   <input name="callback" type="hidden" value="[% HTML.escape(callback) %]">
-  <input name="nextsub" type="hidden" value="save_dunning">
-
-  <input type="hidden" name="action" value="[% 'Continue' | $T8 %]">
-
-  <input type="submit" name="dummy" value="[% 'Continue' | $T8 %]"
-         [% UNLESS DEBUG_DUNNING %]onclick="this.disabled=true; this.value='[% 'The dunning process started' | $T8 %]'; document.Form.submit()"[% END %]>
-
  </form>
- <script type='text/javascript'>
-   $(function() {
-     $("select[name='language_id']").prop('disabled', $('#force_lang').prop('checked'));
-     $('#force_lang').checkall('select[name="language_id"]', 'disabled');
-   });
- </script>
diff --git a/templates/webpages/dunning/status.html b/templates/webpages/dunning/status.html
new file mode 100644 (file)
index 0000000..bcdad04
--- /dev/null
@@ -0,0 +1,40 @@
+[% USE HTML %]
+[% USE LxERP -%]
+[% USE P -%]
+[% USE T8 -%]
+[% USE Base64 -%]
+
+<h1>[% title | html %]</h1>
+
+[%- INCLUDE 'common/flash.html' -%]
+
+<table>
+  <thead class="listheading">
+    <tr>
+      <th>[% 'Dunning number' | $T8 %]</th>
+      <th width="250px">[% 'Invoice Number' | $T8 %]</th>
+      <th>[% 'Include original Invoices?' | $T8 %]</th>
+      <th>[% 'eMail?' | $T8 %]</th>
+      <th>[% 'Status' | $T8 %]</th>
+    </tr>
+  </thead>
+  <tbody>
+    [% FOREACH s = status -%]
+    <tr class=[%- IF s.error %]"listrow_error"[% ELSE %]"listrow"[% END %]>
+      <td>[% IF !s.error %][% P.link_tag('dn.pl?action=show_dunning&showold=1&dunning_id=' _ s.dunning_id, s.dunning_id) %][% END %]</td>
+      <td>[% s.invnumbers.join(", ") %]</td>
+      <td>[% s.print_original_invoice ? LxERP.t8('Yes') : LxERP.t8('No') %]</td>
+      <td>[% s.send_email ? LxERP.t8('Yes') : LxERP.t8('No') %]</td>
+      <td>[% s.error ? s.error : LxERP.t8('Ok') %]</td>
+    </tr>
+    [%- END %]
+  </tbody>
+</table>
+
+[%- IF pdf_filename && pdf_content -%]
+  <script type="text/javascript">
+    <!--
+      $(function() {kivi.save_file('[% pdf_content.encode_base64 %]', 'application/pdf', 0, '[% pdf_filename %]');});
+    -->
+  </script>
+[%- END %]
index 282fd09..05a1a75 100644 (file)
@@ -1,5 +1,5 @@
 [%- USE L %][%- USE LxERP %][%- USE HTML %]
-<form action="controller.pl" method="post">
+<form action="controller.pl" method="post" id="filter_form">
  <div class="filter_toggle">
   <a href="#" onClick="javascript:$('.filter_toggle').toggle()">[% LxERP.t8('Show Filter') %]</a>
   [% IF SELF.filter_summary %]([% LxERP.t8("Current filter") %]: [% SELF.filter_summary %])[% END %]
    </tr>
   </table>
 
-  [% L.hidden_tag("action", "EmailJournal/dispatch") %]
   [% L.hidden_tag("sort_by", FORM.sort_by) %]
   [% L.hidden_tag("sort_dir", FORM.sort_dir) %]
   [% L.hidden_tag("page", FORM.page) %]
-  [% L.submit_tag("action_list", LxERP.t8("Continue"))%]
 
-  <a href="#" onClick="javascript:$('#filter_table input,#filter_table select').val("");">[% LxERP.t8("Reset") %]</a>
+  [% L.button_tag('$("#filter_form").resetForm()', LxERP.t8('Reset')) %]
 
  </div>
 
index 67d7621..b484bc7 100644 (file)
@@ -1,4 +1,4 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
+[% USE HTML %][% USE L %][% USE LxERP %][%- USE P -%]
 
  <h1>[% FORM.title %]</h1>
 
 
    <tr class="listrow">
     <th>[%- LxERP.t8("Body") %]</th>
-    <td><pre>[% HTML.escape(SELF.entry.body) %]</pre></td>
+    <td>
+     [%- IF SELF.entry.headers.match('(?i)content-type:.*text/html') %]
+      [% P.restricted_html(SELF.entry.body) %]
+     [%- ELSE %]
+      <pre>[% HTML.escape(SELF.entry.body) %]</pre>
+     [%- END %]
+    </td>
    </tr>
  </table>
 
@@ -77,7 +83,3 @@
    </tbody>
   </table>
  [% END %]
-
- <p>
-  <a href="[% back_to %]">[%- LxERP.t8("Back") %]</a>
- </p>
diff --git a/templates/webpages/employee/_form.html b/templates/webpages/employee/_form.html
deleted file mode 100644 (file)
index 841cdc5..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-[%- USE HTML %]
-[%- USE LxERP %]
-[%- USE T8 %]
-[%- USE L %]
-
-<form action='controller.pl' method='POST'>
-
-<table>
-<tr>
- <td align='right' class=''>[% 'Login of User' | $T8 %]:</td>
- <td>[% employee.login | html %]</td>
-</tr>
-<tr>
- <td align='right'>[% 'Name' | $T8 %]:</td>
- <td>[% employee.name | html %]</td>
-</tr>
-<tr>
- <td align='right'>[% 'Deleted' | $T8 %]:</td>
- <td> [% L.radio_button_tag('employee.deleted', value=1, checked=employee.deleted, label=LxERP.t8('Yes')) %]
-      [% L.radio_button_tag('employee.deleted', value=0, checked=!employee.deleted, label=LxERP.t8('No')) %]
- </td>
-</tr>
-</table>
-
-[%- L.hidden_tag('employee.id', employee.id) %]
-[%- L.hidden_tag('action',  'Employee/dispatch')  %]
-[%- L.submit_tag('action_save',  LxERP.t8('Save'))  %]
-</form>
-
index d4b3ed8..d2da97e 100644 (file)
@@ -1,9 +1,25 @@
-<h1>[% title | html %]</h1>
+[%- USE LxERP -%][%- USE L -%][%- USE T8 -%]<h1>[% title | html %]</h1>
 
-[% PROCESS 'common/flash.html' %]
+[% INCLUDE "common/flash.html" %]
 
-[% PROCESS 'employee/_form.html' employee=SELF.employee %]
+<form action="controller.pl" method="POST" id="form">
 
-<hr>
+ <table>
+  <tr>
+   <td align="right" class="">[% "Login of User" | $T8 %]:</td>
+   <td>[% SELF.employee.login | html %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% "Name" | $T8 %]:</td>
+   <td>[% SELF.employee.name | html %]</td>
+  </tr>
+  <tr>
+   <td align="right">[% "Deleted" | $T8 %]:</td>
+   <td> [% L.radio_button_tag("employee.deleted", value=1, checked=SELF.employee.deleted, label=LxERP.t8("Yes")) %]
+    [% L.radio_button_tag("employee.deleted", value=0, checked=!SELF.employee.deleted, label=LxERP.t8("No")) %]
+   </td>
+  </tr>
+ </table>
 
-[% PROCESS 'employee/_list.html' %]
+ [%- L.hidden_tag("employee.id", SELF.employee.id) %]
+</form>
index 056a828..483c437 100644 (file)
@@ -1,5 +1,5 @@
 <h1>[% title | html %]</h1>
 
-[% PROCESS 'common/flash.html' %]
+[% INCLUDE 'common/flash.html' %]
 
 [% PROCESS 'employee/_list.html' %]
diff --git a/templates/webpages/file/import_dialog.html b/templates/webpages/file/import_dialog.html
new file mode 100644 (file)
index 0000000..1e95de4
--- /dev/null
@@ -0,0 +1,36 @@
+[%- USE L -%][%- USE LxERP -%]
+
+<form method="post" id="file_import_form" action="controller.pl">
+ <table>
+  <thead>
+   <tr>
+    <th class="listheading" width="3%">[% L.checkbox_tag(source.chk_action _ '_checkall') %]</th>
+    <th class="listheading" width="11%">[% source.chkall_title %]</th>
+    <th>[% LxERP.t8("Attached Filename") %]</th>
+   </tr>
+  </thead>
+  <tbody>
+   [%- FOREACH file = source.files %]
+    <tr class="listrow[% loop.count % 2 %]">
+     <td>[%- L.checkbox_tag(source.chk_action _ '[]', 'value'=file.name, 'class'=source.chk_action) %]</td>
+     <td></td>
+     <td><span id="[% "filename_" _ file.name %]">[% file.filename %]</span></td>
+    </tr>
+   [%- END %]
+  </tbody>
+ </table>
+
+ <p>
+  [%- L.button_tag("kivi.File.importaction(" _ SELF.object_id _ ",'" _ SELF.object_type _ "','" _ SELF.file_type _ "','" _ source.name _ "','" _ source.path _ "','"_ source.chk_action _ "');", LxERP.t8('Continue'), id => "import_cont_btn") %]
+  [%- L.button_tag("kivi.File.importclose();" , LxERP.t8('Cancel')  , class => "submit") %]
+ </p>
+
+</form>
+
+<script type="text/javascript">
+ <!--
+  $(function() {
+   $('#[% source.chk_action %]_checkall').checkall('INPUT[name="[% source.chk_action %][]"]');
+  });
+ -->
+</script>
diff --git a/templates/webpages/file/list.html b/templates/webpages/file/list.html
new file mode 100644 (file)
index 0000000..2868d4f
--- /dev/null
@@ -0,0 +1,149 @@
+[%- USE LxERP -%][% USE L %]
+[% USE T8 %]
+[% USE Base64 %]
+[% USE HTML %]
+[%- IF ! json %]
+ <div id="[% file_type %]_list_[% object_type %]">
+[%- END %]
+<div class="listtop">[% title %]</div>
+
+<div style="padding-bottom: 15px">
+ [%- SET can_rename = 0 %]
+ [%- FOREACH source = SOURCES %]
+  <table style="width: 100%" >
+   <thead>
+    <tr><th class="listheading" colspan="[% IF file_type == 'image' %]8[% ELSE %]6[% END %]">[% source.title %]</th></tr>
+    <tr>
+     [%- SET checkname = source.chk_action %]
+     [%- IF is_global %]
+      [%- SET checkname = object_type _ '_' _ source.chk_action %]
+     [%- END %]
+     [%- IF edit_attachments %]
+      <script type="text/javascript">
+       <!--
+        $(function() {
+         $('#[% checkname %]_checkall').checkall('INPUT[name="[% checkname %][]"]');
+        });
+       -->
+      </script>
+      <th class="listheading" width="3%">[% L.checkbox_tag(checkname _ '_checkall') %]</th>
+      <th class="listheading" width="7%">[% source.chkall_title %]</th>
+     [%- END %]
+     <th class="listheading" width="2%"><b>[%  LxERP.t8('Version') %]</b></th>
+     <th class="listheading" width="15%"><b>[%  LxERP.t8('Date') %]</b></th>
+     <th class="listheading" width="20%"><b>[%  source.file_title %]</b></th>
+     [%- IF file_type == 'image' %]
+      <th class="listheading" width="15%"><b>[%  LxERP.t8('Title') %]</b></th>
+      <th class="listheading" width="10%">
+       <b>[%  LxERP.t8('ImagePreview') %]</b>
+      </th>
+      <th class="listheading" width="30%"><b>[%  LxERP.t8('Description') %]</b></th>
+     [%- ELSE %]
+      <th class="listheading" width="40%"><b>[%  LxERP.t8('ImagePreview') %]</b></th>
+     [%- END %]
+    </tr>
+   </thead>
+
+   <tbody>
+    [%- FOREACH file = source.files %]
+     [%- is_other_version = 1 IF last_id == file.id %]
+     [%- last_id = file.id %]
+     [%- IF !is_other_version %]
+      [%- row_cnt = row_cnt + 1 %]
+      <tr class="listrow[% row_cnt % 2 %]">
+     [%- ELSE %]
+      <tr class="[% 'version_row_' _ file.id %] listrow[% row_cnt % 2 %] hidden">
+     [%- END %]
+      [%- IF edit_attachments %]
+       <td>[%- L.checkbox_tag(checkname _ '[]', 'value'=file.id _ '_' _ file.version, 'class'=checkname) %]</td>
+       <td></td>
+      [%- END %]
+      <td align="right" [%- IF file.version_count > 1 && !is_other_version %] class="cursor-pointer" onclick="kivi.File.toggle_versions('[% file.id %]')"[%- END %]>
+       [%- IF file.version_count > 1 && !is_other_version %]<span id="[% 'version_toggle_' _ file.id %]">⏷ </span>[% END %]
+       [% file.version _ '/' _ file.version_count %]
+       [% L.hidden_tag("version[]", file.version) %]
+      </td>
+      <td>[% file.mtime_as_timestamp_s %]</td>
+      <td>
+       <a href="controller.pl?action=File/download&id=[% file.id %][%- IF file.version %]&version=[%- file.version %][%- END %]">
+        <span id="[% "filename_" _ file.id %][%- IF file.version %]_[% file.version %][%- END %]">[% file.file_name %]</span>
+       </a>
+      </td>
+      [%- IF file_type == 'image' %]
+       <td>[% file.title %]</td>
+       <td>
+        <img src="controller.pl?action=File/download&id=[% file.id %][%- IF file.version %]&version=[%- file.version %][%- END %]" alt="[% file.title %]" width="64px">
+       </td>
+       <td>[% file.description %]</td>
+      [%- ELSE %]
+       <td align="left">
+        [%- IF file.thumbnail %]
+         <div class="overlay_div">
+          <img id="thumb_[% file.id %]" class="thumbnail"
+               data-file-id="[% file.id %]" data-file-version="[% file.version %]"
+               src="data:[% HTML.escape(file.thumbnail.thumbnail_img_content_type) %];base64,[% file.thumbnail.thumbnail_img_content.encode_base64 %]"
+               alt="[% file.file_name %]">
+          <img id="enlarged_thumb_[% file.id %][% IF file.version %]_[% file.version %][% END %]" class="overlay_img" style="display:none;"
+               data-file-id="[% file.id %]" data-file-version="[% file.version %]">
+         </div>
+        [%- ELSE %]
+         -
+        [%- END %]
+       </td>
+      [%- END %]
+     </tr>
+    [%- END # FOREACH file %]
+   </tbody>
+  </table>
+
+  <div>
+   [%- IF edit_attachments %]
+    [%- IF source.can_import %]
+     [% L.button_tag("kivi.File.unimport(" _ object_id _ ",'" _ object_type _ "','" _ file_type _ "','" _ checkname _ "');",source.chk_title) %]
+    [%- ELSE %]
+     [%- IF source.can_delete %]
+      [% L.button_tag("kivi.File.delete("   _ object_id _ ",'" _ object_type _ "','" _ file_type _ "','" _ checkname _ "'," _ is_global _ ");",  source.chk_title) %]
+     [%- END %]
+    [%- END %]
+   [%- END %]
+   [%- IF source.can_rename %]
+    [%- can_rename = 1 %]
+    [% L.button_tag("kivi.File.rename(" _ object_id _ ",'" _ object_type _ "','" _ file_type _ "','" _ checkname _ "'," _ is_global _ ");",  source.rename_title ) %]
+   [%- END %]
+   [%- IF source.can_import %]
+    [% L.button_tag("kivi.File.import("   _ object_id _ ",'" _ object_type _ "','" _ file_type _ "','" _ source.name _ "','" _ source.path _"');",  source.import_title ) %]
+   [%- END %]
+   [%- IF source.can_upload %]
+    [% L.button_tag("kivi.File.upload(" _ object_id _ ",'" _ object_type _ "','" _ file_type _ "','" _ source.upload_title _ "'," _ is_global _ ");", source.upload_title ) %]
+    <span class="upload_drop_zone"
+          data-object-type="[% object_type %]"
+          data-object-id="[% object_id %]"
+          data-file-type="[% file_type %]"
+          data-is-global="[% is_global %]"
+          data-maxsize="[% INSTANCE_CONF.get_doc_max_filesize %]">
+      [% 'Drag and drop files here' | $T8 %]
+    </span>
+   [%- END %]
+  </div>
+ [%- END # FOREACH source %]
+ <div></div>
+ <div>
+  [% L.button_tag("kivi.File.update(" _ object_id _ ",'" _ object_type _ "','" _ file_type _ "'," _ is_global _ ");", LxERP.t8('Update')) %]
+ </div>
+</div>
+
+[%- IF ! json %]
+ </div>
+ [%- UNLESS is_global %]
+  [%- IF can_rename %]
+   [% INCLUDE 'file/rename_dialog.html' -%]
+  [%- END %]
+ [%- END %]
+[%- END %]
+
+<script>
+  $(function() {
+    kivi.File.list_div_id = "[% file_type %]_list_[% object_type %]";
+    kivi.File.init();
+  });
+</script>
diff --git a/templates/webpages/file/rename_dialog.html b/templates/webpages/file/rename_dialog.html
new file mode 100644 (file)
index 0000000..7330692
--- /dev/null
@@ -0,0 +1,22 @@
+[%- USE LxERP -%][%- USE L -%]
+<div class="loading" id="rename_dialog_[% file_type %]" style="display:none">
+ <div style="padding-bottom: 15px">
+  <div>
+   <table>
+    <tr><td colspan="2" id="rename_extra_text_[% file_type %]"></td></tr>
+    <tr><td colspan="2">[%- LxERP.t8("Please modify filename") %]:</td></tr>
+    <tr><td colspan="2"><input size='40'  name="newfilename" id="newfilename_id_[% file_type %]" value=""></td></tr>
+    <tr>
+     <td>
+      <input type="hidden"          name="next_ids"    id="next_ids_id_[% file_type %]"    value="">
+      <input type="hidden"          name="sessionfile" id="sessionfile_id_[% file_type %]" value="">
+      <input type="hidden"          name="rename_id"   id="rename_id_id_[% file_type %]"   value="">
+      <input type="hidden"          name="is_global"   id="is_global_id_[% file_type %]"   value="">
+      [% L.button_tag("return kivi.File.renameaction('" _ file_type _ "');", LxERP.t8('Continue'), id => "rename_cont_btn") %]
+     </td>
+     <td>[% L.button_tag("return kivi.File.renameclose('" _ file_type _"');" , LxERP.t8('Cancel')  , class => "submit") %]</td>
+    </tr>
+   </table>
+  </div>
+ </div>
+</div>
diff --git a/templates/webpages/file/upload_dialog.html b/templates/webpages/file/upload_dialog.html
new file mode 100644 (file)
index 0000000..6dde5a3
--- /dev/null
@@ -0,0 +1,37 @@
+[%- USE L -%][%- USE LxERP -%]
+
+<form method="post" id="upload_form" enctype="multipart/form-data" action="controller.pl">
+ [% SET multiple = 'true' %]
+ [% IF SELF.object_type == 'shop_image' %][% multiple = 'false' %][% END %]
+ <table>
+  <tr>
+   <td>[%- LxERP.t8("Filename") %]:</td>
+   <td>
+    <input type="file" name="uploadfiles[]" multiple="[% multiple %]" id="upload_files" size="45" accept="[% SELF.accept_types %]" onchange="kivi.File.allow_upload_submit();">
+   </td>
+  </tr>
+  [% IF SELF.object_type == 'shop_image' %]
+   <tr>
+    <td>[% LxERP.t8("Title") %]</td>
+    <td>[% L.input_tag("title",'') %]</td>
+   </tr>
+   <tr>
+    <td>[% LxERP.t8("Description") %]</td>
+    <td>[% L.input_tag("description",'') %]</td>
+   </tr>
+  [% END %]
+ </table>
+
+ <p>
+  <input value="[%- LxERP.t8("Upload file") %]" id="upload_selected_button"
+         onclick="kivi.File.upload_selected_files([% SELF.object_id %],'[% SELF.object_type %]','[% SELF.file_type %]',[% SELF.maxsize %],[% SELF.is_global %]);"
+         type="button" disabled>
+  <a href="#" onclick="kivi.File.reset_upload_form();">[%- LxERP.t8("Reset") %]</a>
+  <a href="#" onclick="$('#files_upload').dialog('close');">[% LxERP.t8("Cancel") %]</a>
+ </p>
+
+ <hr>
+
+ <div id="upload_result"><p>&nbsp;</p></div>
+
+</form>
index 3876cff..87b0118 100644 (file)
@@ -3,7 +3,7 @@
 [%- USE LxERP %]
 [%- USE HTML %]
 [%- SET style='width: 400px' %]
-<form action='controller.pl' method='post'>
+<form action='controller.pl' method='post' id='filter_form'>
 <div class='filter_toggle'>
 <a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Show Filter' | $T8 %]</a>
   [% SELF.filter_summary %]
   </tr>
  </table>
 
-[% L.hidden_tag('action', 'FinancialControllingReport/dispatch') %]
 [% L.hidden_tag('sort_by', FORM.sort_by) %]
 [% L.hidden_tag('sort_dir', FORM.sort_dir) %]
 [% L.hidden_tag('page', FORM.page) %]
-[% L.input_tag('action_list', LxERP.t8('Continue'), type = 'submit', class='submit')%]
-
-
-<a href='#' onClick='javascript:$("#filter_table input").val("");$("#filter_table option").prop("selected",0)'>[% 'Reset' | $T8 %]</a>
-
+[% L.button_tag('$("#filter_form").resetForm()', LxERP.t8('Reset')) %]
 </div>
 
 </form>
index e3b3191..3a6f126 100644 (file)
@@ -7,7 +7,7 @@
    $(function(){ document.Form.subject.focus(); });
  </script>
 
- <form action="fu.pl" method="post" name="Form">
+ <form action="fu.pl" method="post" name="Form" id="form">
 
   [%- IF SAVED_MESSAGE %]
   <p>[% SAVED_MESSAGE %]</p>
@@ -18,8 +18,7 @@
   <input type="hidden" name="callback" value="[% HTML.escape(callback) %]">
   <input type="hidden" name="POPUP_MODE" value="[% HTML.escape(POPUP_MODE) %]">
 
-  <p>
-   <table>
+  <table>
     <tr>
      <td valign="top">[% 'Follow-Up for user' | $T8 %]</td>
      <td valign="top">
@@ -48,9 +47,9 @@
      <td valign="right" align="top">[% 'Body' | $T8 %]</td>
      <td align="top"><textarea cols="50" rows="10" name="body">[% HTML.escape(body) %]</textarea></td>
     </tr>
-   </table>
-  </p>
+  </table>
 
+ [%- IF POPUP_MODE %]
   <p>
    <input type="hidden" name="action" value="dispatcher">
    <input type="submit" class="submit" name="action_save" value="[% 'Save' | $T8 %]">
    <input type="submit" class="submit" name="action_finish" value="[% 'Finish' | $T8 %]">
    <input type="submit" class="submit" name="action_delete" value="[% 'Delete' | $T8 %]">
    [%- END %]
-   [%- IF POPUP_MODE %]
    <input type="submit" class="submit" onclick="window.close()" value="[% 'Cancel' | $T8 %]">
-   [%- END %]
   </p>
 
-  [%- IF POPUP_MODE %]
-  [%- IF FOLLOW_UPS.size %]
+  [%- IF FOLLOW_UPS_PENDING.size %]
   <hr height="3" noshade>
 
   <h2>[% 'Existing pending follow-ups for this item' | $T8 %]</h2>
 
-  <p>
-   <table>
+  <table>
     <tr>
      <th class="listheading">[% 'Follow-Up Date' | $T8 %]</th>
      <th class="listheading">[% 'Subject' | $T8 %]</th>
@@ -78,7 +73,7 @@
      <th class="listheading">[% 'Follow-up for' | $T8 %]</th>
     </tr>
 
-    [%- FOREACH row = FOLLOW_UPS %]
+    [%- FOREACH row = FOLLOW_UPS_PENDING %]
     <tr class="listrow[% loop.count % 2 %]">
      <td valign="top">[% HTML.escape(row.follow_up_date) %]</td>
      <td valign="top"><a href="fu.pl?action=edit&id=[% HTML.escape(row.id) %][% IF POPUP_MODE %]&POPUP_MODE=1[% END %]">[% HTML.escape(row.subject) %]</a></td>
      <td valign="top">[% HTML.escape(row.created_for_user_name) %]</td>
     </tr>
     [%- END %]
-   </table>
-  </p>
+  </table>
   [%- END %]
+
+  [%- IF FOLLOW_UPS_DONE.size %]
+  <hr height="3" noshade>
+
+  <h2>[% 'Existing finished follow-ups for this item' | $T8 %]</h2>
+
+  <table>
+    <tr>
+     <th class="listheading">[% 'Follow-Up Date' | $T8 %]</th>
+     <th class="listheading">[% 'Subject' | $T8 %]</th>
+     <th class="listheading">[% 'Created by' | $T8 %]</th>
+     <th class="listheading">[% 'Follow-up for' | $T8 %]</th>
+    </tr>
+
+    [%- FOREACH row = FOLLOW_UPS_DONE %]
+    <tr class="listrow[% loop.count % 2 %]">
+     <td valign="top">[% HTML.escape(row.follow_up_date) %]</td>
+     <td valign="top"><a href="fu.pl?action=edit&id=[% HTML.escape(row.id) %][% IF POPUP_MODE %]&POPUP_MODE=1[% END %]">[% HTML.escape(row.subject) %]</a></td>
+     <td valign="top">[% HTML.escape(row.created_by_name) %]</td>
+     <td valign="top">[% HTML.escape(row.created_for_user_name) %]</td>
+    </tr>
+    [%- END %]
+  </table>
   [%- END %]
 
+ [%- END %]
+
   [%- FOREACH row = LINKS %]
   <input type="hidden" name="trans_id_[% loop.count %]"   value="[% HTML.escape(row.trans_id) %]">
   <input type="hidden" name="trans_type_[% loop.count %]" value="[% HTML.escape(row.trans_type) %]">
 
   <input type="hidden" name="trans_rowcount" value="[% LINKS.size %]">
  </form>
-
index b2c6536..c2389cf 100644 (file)
@@ -9,7 +9,7 @@
 
  <p>[% 'Allow the following users access to my follow-ups:' | $T8 %]</p>
 
- <form action="fu.pl" method="post" name="Form">
+ <form action="fu.pl" method="post" name="Form" id="form">
   <p>
    <table>
     <tr>
   </p>
 
   <input type="hidden" name="rowcount" value="[% EMPLOYEES.size %]">
-  <input type="hidden" name="save_nextsub" value="save_access_rights">
-
-  <p>
-   <input type="submit" class="submit" name="action" value="[% 'Save' | $T8 %]">
-  </p>
-
  </form>
index 54927a7..432449c 100644 (file)
@@ -4,11 +4,4 @@
  [%- FOREACH item = HIDDEN %]
  <input type="hidden" name="[% HTML.escape(item.key) %]" value="[% HTML.escape(item.value) %]">
  [%- END %]
-
- <p>
-  [% 'Follow-Ups' | $T8 %]<br>
-  <input type="hidden" name="action" value="dispatcher">
-  <input type="submit" name="action_finish" value="[% 'Finish' | $T8 %]">
-  <input type="submit" name="action_delete" value="[% 'Delete' | $T8 %]">
- </p>
 </form>
index 2c0fa1b..f41a750 100644 (file)
@@ -10,5 +10,4 @@
 </p>
 [%- END %]
 
-<form action="fu.pl" method="post" name="Form">
-
+<form action="fu.pl" method="post" name="Form" id="form">
index 0d1a22a..073adcd 100644 (file)
@@ -7,9 +7,7 @@
    $(function(){ document.Form.subject.focus(); });
  </script>
 
- <form action="fu.pl" method="post" name="Form">
-  <input type="hidden" name="nextsub" value="report">
-
+ <form action="fu.pl" method="post" name="Form" id="form">
   <p>
    <table>
     <tr>
 
    </table>
   </p>
-
-  <p>
-   <input type="submit" name="action" value="[% 'Continue' | $T8 %]">
-  </p>
  </form>
-
diff --git a/templates/webpages/generic/autocomplete.html b/templates/webpages/generic/autocomplete.html
deleted file mode 100644 (file)
index 5bd35c0..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-[%#-
-     Autocompletion
-
-  This template enables auto completion for input fields.
-  Calling Syntax is:
-
-    INCLUDE 'generic/autocomplete', [
-      { SPEC_1 },
-      { SPEC_2 },
-      ...
-    ]
-
-  where SPEC is a hash containing the following keys:
-
-   script   : the script that is called for autocompletion, defaults to the invoking script
-   action   : action in the ajax script, defaults to 'ajax_autocomplete'
-   selector : a jquery selector, specifying the input fields
-   column   : specifies the column that is represented by the bound field. typically description or name.
-   params   : additional params that should be included in the request, like customer/vendor information. expects a hash.
-
-  TODO FIELDS:
-   - addition fields like type, vc etc.
-   - additional dependencies, see jquery.autocomplete documentation
-   - hook function on select, again see jquery documentation
-   - limit: maximum number of results shown.
-
-  a simple SPEC would look like this:
-
-   { selector => '#description', column => 'description' }
-     # field with id="description" should be autocompleted with descriptions
-
-   { script => 'ic.pl', selector => '[name^="partnumber_"]', column => 'partnumber' }
-     # let ic.pl autocomplete by partnumbers, bind this to all fields where the name begins with "partnumber_"
-
-
-
-    The Backend Side
-
-  The called function will recieve the queried string as hashkey "q" in form, as well as every other param specified here.
-  It should generate a generic ajax header (see form), followed by newline separated list of possible completion values.
-
-%]
-<script type='text/javascript'>
-[%- FOREACH token = AUTOCOMPLETES %]
-[%- DEFAULT token.script         = script              %]
-[%- DEFAULT token.action         = 'ajax_autocomplete' %]
-[%- DEFAULT token.INPUT_ENCODING = 'utf8'              %]
-[%- FOREACH key = token.params.keys %]
-[%- token.additional_url = token.additional_url _ '&' _ key _ '=' _ token.params.$key %]
-[%- END %]
-[%- token.url = token.script
-              _ '?action=' _ token.action
-              _ '&INPUT_ENCODING=' _ token.INPUT_ENCODING %]
-[%- SET token.url = token.url _ '&column=' _ token.column IF token.column %]
-[%- SET token.url = token.url _ token.additional_url IF token.additional_url %]
-$(document).ready( $('[% token.selector %]').autocomplete('[% token.url %]'));
-[%- END %]
-</script>
index 035caa6..6b37127 100644 (file)
@@ -1,11 +1,9 @@
 [%- USE T8 %]
 [%- USE HTML %]
-<h1>[% title %]</h1>
-
- <form name="Form">
+ <form name="CalcQtyForm" id="calc_qty_form_id">
 
   <input type="hidden" name="input_name" value="[% HTML.escape(input_name) %]">
-  <input type="hidden" name="input_id" value="[% HTML.escape(input_id) %]">
+  <input type="hidden" name="input_id"   value="[% HTML.escape(input_id) %]">
 
   <table width="100%">
    <tr><td>[% 'Please insert object dimensions below.' | $T8 %]</td></tr>
  <script type="text/javascript">
    function calculate_qty() {
 [%- FOREACH row = VARIABLES %]
-     var [% row.name %] = parse_amount('[% myconfig.numberformat %]', document.getElementsByName("[% row.name %]")[0].value);
+     var [% row.name %] = kivi.parse_amount($('#calc_qty_form_id #[% row.name %]').val());
 [%- END %]
      var result = [% formel %];
-     result = number_format(result, 2, '[% myconfig.numberformat %]');
-     window.opener.document.getElementsByName(document.Form.input_name.value)[0].value = result;
-     self.close();
-   }
-
-   function parse_amount(numberformat, amount) {
-     if (numberformat == '1.000,00' || numberformat == '1000,00')
-       amount = amount.replace(/\./g, "").replace(/,/, ".");
-     if (numberformat == "1'000.00")
-       amount = amount.replace(/\'/g, '');
-     return amount.replace(/,/g, '');
-   }
-
-   function number_format(number, precision, numberformat) {
-     number = Math.round( number * Math.pow(10, precision) ) / Math.pow(10, precision);
-     var nf     = numberformat.replace(/\d/g, '').split('').reverse();
-     var sep    = nf[0];
-     var th_sep = nf[1];
-
-     str_number = number+"";
-     arr_int = str_number.split(".");
-     if(!arr_int[0]) arr_int[0] = "0";
-     if(!arr_int[1]) arr_int[1] = "";
-     if(arr_int[1].length < precision) {
-       nachkomma = arr_int[1];
-       for(i=arr_int[1].length+1; i <= precision; i++) {
-         nachkomma += "0";
-       }
-       arr_int[1] = nachkomma;
-     }
-     if(th_sep != "" && arr_int[0].length > 3) {
-       raw_arr_int = arr_int[0];
-       arr_int[0] = "";
-       for(j = 3; j < raw_arr_int.length ; j+=3) {
-         arr_int[0] = th_sep + raw_arr_int.slice(raw_arr_int.length - j, raw_arr_int.length - j + 3) +  arr_int[0] + "";
-       }
-       str_first = raw_arr_int.substr(0, (raw_arr_int.length % 3 == 0) ? 3 : (raw_arr_int.length % 3));
-       arr_int[0] = str_first + arr_int[0];
+     result = kivi.format_amount(result, 2);
+     if (document.CalcQtyForm.input_id.value) {
+       document.getElementById(document.CalcQtyForm.input_id.value).value = result;
+     } else {
+       document.getElementsByName(document.CalcQtyForm.input_name.value)[0].value = result;
      }
-     return arr_int[0] + sep + arr_int[1];
+     $('#calc_qty_dialog').dialog('close');
    }
- </script>
 
+ </script>
diff --git a/templates/webpages/generic/cov_selection.html b/templates/webpages/generic/cov_selection.html
deleted file mode 100644 (file)
index a2db0d4..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-<h1>[% title %]</h1>
-
- <form method="post">
-
-  <input type="hidden" name="input_name" value="[% HTML.escape(input_name) %]">
-  <input type="hidden" name="input_id" value="[% HTML.escape(input_id) %]">
-  <input type="hidden" name="is_vendor" value="[% HTML.escape(is_vendor) %]">
-  <input type="hidden" name="allow_both" value="[% HTML.escape(allow_both) %]">
-  <input type="hidden" name="action_on_cov_selected" value="[% HTML.escape(action_on_cov_selected) %]">
-
-   <tr>
-    <td>
-     [%- IF !is_vendor %]
-      [% 'Please select a customer from the list below.' | $T8 %]
-     [%- ELSE %]
-      [% 'Please select a vendor from the list below.' | $T8 %]
-     [%- END %]
-    </td>
-   </tr>
-
-   <tr>
-    <td>
-
-     <table>
-      <tr class="listheading">
-       <th class="listheading">&nbsp;</th>
-       [%- FOREACH row = HEADER %]
-        <th nowrap class="listheading"><a href="[% HTML.escape(row.callback) %]">[% row.column_title %]</a></th>
-       [%- END %]
-      </tr>
-
-      [%- FOREACH row = COVS %]
-       <tr class="listrow[% loop.count % 2 %]">
-        <td valign="top"><button type="button" onclick="cov_selected('[% loop.count %]')">Auswahl</button></td>
-        <td valign="top"><input type="hidden" id="id_[% loop.count %]" name="id_[% loop.count %]" value="[% HTML.escape(row.id) %]">
-         <input type="hidden" id="name_[% loop.count %]" name="name_[% loop.count %]" value="[% HTML.escape(row.name) %]">
-         <input type="hidden" id="customer_is_vendor_[% loop.count %]" name="customer_is_vendor_[% loop.count %]" value="[% HTML.escape(row.customer_is_vendor) %]">
-         [% HTML.escape(row.name) %]</td>
-        <td valign="top">[% HTML.escape(row.address) %]</td>
-        <td valign="top">[% HTML.escape(row.contact) %]</td>
-       </tr>
-      [% END %]
-     </table>
-
-    </td>
-   </tr>
-  </table>
-
- </form>
-
- <script type="text/javascript">
-  <!--
-      function cov_selected(selected) {
-        var name = document.getElementsByName("name_" + selected)[0].value
-        var id = document.getElementsByName("id_" + selected)[0].value
-        var customer_is_vendor = document.getElementsByName("customer_is_vendor_" + selected)[0].value
-        var cov_name = document.forms[0].input_name.value;
-        window.opener.document.getElementsByName(cov_name)[0].value = name;
-        if (document.forms[0].input_id.value != "") {
-          window.opener.document.getElementsByName(document.forms[0].input_id.value)[0].value = id;
-        }
-
-        var cov_is_vendor = cov_name + "_is_vendor";
-        var input = window.opener.document.getElementsByName(cov_is_vendor)[0];
-        if (input) {
-          input.value = customer_is_vendor;
-        }
-
-        var prefix = "";
-        if (cov_name.substr(0, 2) == "f_") {
-          prefix = "f_";
-          cov_name = cov_name.substr(2);
-        }
-        cov_name = prefix + "old_" + cov_name;
-        var input = window.opener.document.getElementsByName(cov_name)[0];
-        if (input) {
-          input.value = name;
-        }
-        cov_name = prefix + "old" + cov_name;
-        input = window.opener.document.getElementsByName(cov_name)[0];
-        if (input) {
-          input.value = name;
-        }
-
-        if (document.forms[0].action_on_cov_selected.value != "") {
-          window.opener.document.getElementsByName("action")[0].value = document.forms[0].action_on_cov_selected.value;
-          window.opener.document.forms[0].submit();
-        }
-
-        self.close();
-      }
-      //-->
- </script>
-
diff --git a/templates/webpages/generic/edit_email.html b/templates/webpages/generic/edit_email.html
deleted file mode 100644 (file)
index d26de92..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %][%- USE LxERP -%][%- USE L -%]
-<h1>[% title %]</h1>
-
-<form name="Form" method="post" action="[% script %]">
-
-<table width="100%">
-  <tr>
-    <td>
-      <table>
-        <tr>
-          <th align="right" nowrap>[% 'To' | $T8 %]</th>
-
-          <td>[% L.input_tag('email', email, size=30, class=(email ? '' : 'initial_focus')) %]</td>
-        </tr>
-        <tr>
-          <th align="right" nowrap>[% 'Cc' | $T8 %]</th>
-          <td><input name="cc" size="30" value="[% HTML.escape(cc) %]"></td>
-        </tr>
-[%- IF SHOW_BCC %]
-        <tr>
-          <th align="right" nowrap>[% 'Bcc' | $T8 %]</th>
-          <td><input name="bcc" size="30" value="[% HTML.escape(bcc) %]"></td>
-        </tr>
-[%- END %]
-        <tr>
-          <th align="right" nowrap>[% 'Subject' | $T8 %]</th>
-          <td>[% L.input_tag('subject', subject, size=30, class=(email ? 'initial_focus' : '')) %]</td>
-        </tr>
-        <tr>
-          <th align="right" nowrap>[% 'Attachment name' | $T8 %]</th>
-          <td><input name="attachment_filename" size="30" value="[% HTML.escape(a_filename) %]"></td>
-        </tr>
-      </table>
-    </td>
-  </tr>
-
-  <tr>
-    <td>
-      <table>
-        <tr>
-          <th align="left" nowrap>[% 'Message' | $T8 %]</th>
-        </tr>
-        <tr>
-          <td><textarea name="message" id="message" rows="15" cols="60" wrap="soft">[% HTML.escape(message) %]</textarea></td>
-
-        </tr>
-      </table>
-    </td>
-  </tr>
-  <tr>
-    <td>
-
-[% print_options %]
-[% FOREACH row = HIDDEN %]<input type="hidden" name="[% row.name %]" value="[% HTML.escape(row.value) %]">
-[% END %]
-
-    </td>
-  </tr>
-
-  <tr>
-    <td><hr size="3" noshade></td>
-  </tr>
-</table>
-
-<input type="hidden" name="nextsub" value="send_email">
-
-<br>
-[% L.submit_tag('action', LxERP.t8('Continue'), onclick="return check_prerequisites();") %]
-</form>
-
-<script type="text/javascript">
-<!--
-function check_prerequisites() {
-  if (!$('#email,#subject,#message').filter(function(idx, elt) { return $(elt).val() === ""; }).size())
-    return true;
-
-  alert(kivi.t8('The recipient, subject or body is missing.'));
-  return false;
-}
--->
-</script>
index a77f862..1e23f73 100644 (file)
@@ -4,33 +4,3 @@
  <div class="message_error">[% IF title_error %][% title_error %][% ELSE %][% 'Error!' | $T8 %][% END %]
  <p class="message_error_label">[% label_error %]</p>
  </div>
-
- <p style="text-align: left;"><input type="button" class="submit" onclick="history.back()" value="[% 'Back' | $T8 %]"></p>
-
- [%- IF SHOW_BACK_BUTTON %]
- <form>
-  <p>
-   <!--- TODO: show back button always hack
-         In which situation is it necessary to hide it?
-   <input type="button" onclick="history.back()" value="[% 'Back' | $T8 %]">
-   -->
-  </p>
- </form>
-
- [%- ELSIF SHOW_BUTTON %]
-
- <form action="[% HTML.escape(script) %]" method="post">
-
-  [%- FOREACH var = VARIABLES %]
-  <input type="hidden" name="[% HTML.escape(var.name) %]" value="[% HTML.escape(var.value) %]">
-  [%- END %]
-
-  <input type="hidden" name="action" value="[% HTML.escape(action) %]">
-
-  <p>
-   <input type="submit" value="[% BUTTON_LABEL %]">
-  </p>
- </form>
-
- [%- END %]
-
diff --git a/templates/webpages/generic/multibox.html b/templates/webpages/generic/multibox.html
deleted file mode 100644 (file)
index bf39fe8..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-[%- USE HTML %]
-[%#-
-     Multibox
-
-  This template makes an input box for you,
-  decides whether it should be a text field or a drop down box,
-  generates the HTML code, and fixes everything just right.
-
-  call:  PROCESS generic/multibox.html var = var, var2 = ....
-
-  options and variables:
-    name          : name of the select/textfield
-    id            : id of the select/textfield, optional, defaults to name
-    default       : entered/selected value. defaults to a dereference of name, since it is usually set to that for update mechanisms
-    id_key        : key that holds the id in each row.
-    id_sub        : name of a perl sub that calculates the id for each row. will be called with a hashref.
-    label_key     : key that holds the label in each row.
-    label_sub     : name of a perl sub that calculates the label for each row. will be called with a hashref.
-    DATA          : the actual data, expected to be arrayref of hashrefs, usually what's returned by the all_vc routines.
-    show_empty    : show an empty first line in select boxes. defaults to false
-    style         : additional style information
-    onChange      : java magic on change
-    select        : java function call for a selection popup or other magic
-    allow_textbox : allow to display a textbox instead of a drop down box if there are more entries than 'limit' entries.
-    limit         : defines the limit of entries, after which a textbox is generated. defaults to vclimit, or, failing to find that, 200.
-    select_name   : if a select is displayed, use a different name. ex.: department for textinput, but department_id for selects
-    readonly      : softly prevents modification
-    class         : CSS class names (optional)
--%]
-[%-
-  Multibox__limit      = limit   != '' ? limit   : vclimit != '' ? vclimit : 200
-  Multibox__show_text  = allow_textbox and DATA.size and Multibox__limit < DATA.size ? 1 : 0
-  Multibox__id         = id      != '' && id * 1 != id ? id      : name
-  Multibox__default    = default != '' ? default : $name
-  Multibox__name       = (select_name != '' and ! Multibox__show_text) ? select_name : name
--%]
-[%- IF Multibox__show_text %]
-<input type="text"
- [%- IF Multibox__name     %] name="[%  Multibox__name    | html %]"[% END -%]
- [%- IF Multibox__id       %] id="[%    Multibox__id      | html %]"[% END -%]
- [%- IF Multibox__default  %] value="[% Multibox__default | html %]"[% END -%]
- [%- IF style              %] style="[% style             | html %]"[% END -%]
- [%- IF class              %] class="[% class             | html %]"[% END -%]
- [%- IF readonly           %] readonly[% END -%]
-[%- -%]>
-[%- IF select -%]
-  <input type="button" onclick="[% select %]" value="?">
-[%- END -%]
-[%- ELSE %]
-<select
- [%- IF Multibox__name     %] name="[%     Multibox__name     | html %]"[% END -%]
- [%- IF Multibox__id       %] id="[%       Multibox__id       | html %]"[% END -%]
- [%- IF style              %] style="[%    style              | html %]"[% END -%]
- [%- IF class              %] class="[%     class             | html %]"[% END -%]
- [%- IF onChange           %] onChange="[% onChange           | html %]"[% END -%]
- [%- IF readonly           %] disabled[% END -%]
-[%- -%]>
-  [%- IF show_empty %]
-  <option value=""></option>
-  [%- END %]
-  [%- FOREACH row = DATA %]
-  [%-
-      Multibox__row_id       = row.$id_key     != ''  ? row.$id_key    : $id_sub(row)
-      Multibox__row_label    = row.$label_key  != ''  ? row.$label_key
-                             : $label_sub(row) != ''  ? $label_sub(row)
-                             :                          Multibox__row_id
-      Multibox__row_selected = Multibox__default == Multibox__row_id
-  %]
-  <option value="[% Multibox__row_id | html %]"[% IF Multibox__row_selected %] selected[% END %]>[% Multibox__row_label | html %]</option>
-  [%- END %]
-</select>
-[%- END %]
index d6219e6..0a37257 100644 (file)
@@ -1,28 +1,40 @@
 [%- USE T8 %]
 [%- USE HTML %]
-
-    <h4 class="error">[% 'Item not on file!' | $T8 %]
-
+[%- IF is_wrong_pclass == NOTFORSALE %]
+<h4 class="error">[% 'searched part not for sale' | $T8 %]</h4>
+[%- ELSE %]
+[%- IF is_wrong_pclass == NOTFORPURCHASE %]
+<h4 class="error">[% 'searched part not for purchase' | $T8 %]</h4>
+[%- ELSE %]
+<h4 class="error">[% 'Item does not exists in the database' | $T8 %]
+[% IF INSTANCE_CONF.get_create_part_if_not_found %]
     <p>[% 'What type of item is this?' | $T8 %]</h4>
 
-    <form method="post" action="ic.pl">
+    <form method="post" action="controller.pl">
 
       <p>
 
-      <input class="radio" type="radio" name="item" value="part" checked>&nbsp;[% 'Part' | $T8 %]<br>
-      <input class="radio" type="radio" name="item" value="service">&nbsp;[% 'Service' | $T8 %]
+      <input class="radio" type="radio" name="part.part_type" value="part" checked>&nbsp;[% 'Part'       | $T8 %]<br>
+      <input class="radio" type="radio" name="part.part_type" value="assembly">    &nbsp;[% 'Assembly'   | $T8 %]<br>
+      <input class="radio" type="radio" name="part.part_type" value="service">     &nbsp;[% 'Service'    | $T8 %]<br>
+      <input class="radio" type="radio" name="part.part_type" value="assortment">  &nbsp;[% 'Assortment' | $T8 %]
       <p>
 
       [%- FOREACH var = HIDDENS %]
       <input type="hidden" name="[% HTML.escape(var.name) %]" value="[% HTML.escape(var.value) %]">
       [%- END %]
-
-      <input type="hidden" name="action" value="dispatcher">
-      <input class="submit" type="submit" name="action_add" value="[% 'Continue' | $T8 %]">
+    </p>
+
+      <input type="hidden" name="action" value="Part/dispatch">
+      <input class="submit" type="submit" name="action_add_from_record" value="[% 'Continue' | $T8 %]">
+[%- ELSE %]
+</h4>
+[%- END %]
+[%- END %]
+[%- END %]
       <input id='back_button' type='button' class="submit" value="[% 'Back' | $T8 %]">
-    </form>
+    </p>
+  </form>
 <script type='text/javascript'>
   $(function(){ $('#back_button').click(function(){ window.history.back(-1) }) })
 </script>
-
-
diff --git a/templates/webpages/generic/part_selection.html b/templates/webpages/generic/part_selection.html
deleted file mode 100644 (file)
index 9745334..0000000
+++ /dev/null
@@ -1,119 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-<h1>[% title %]</h1>
-
- <form action="[% HTML.escape(script) %]" method="post" name="Form">
-
-  <input type="hidden" name="input_partnumber" value="[% HTML.escape(input_partnumber) %]">
-  <input type="hidden" name="input_description" value="[% HTML.escape(input_description) %]">
-  <input type="hidden" name="input_partsid" value="[% HTML.escape(input_partsid) %]">
-  <input type="hidden" name="input_partnotes" value="[% HTML.escape(input_partnotes) %]">
-  <input type="hidden" name="allow_creation" value="[% HTML.escape(allow_creation) %]">
-  <input type="hidden" name="action_on_part_selected" value="[% HTML.escape(action_on_part_selected) %]">
-  <input type="hidden" name="filter" value="[% HTML.escape(filter) %]">
-  <input type="hidden" name="options" value="[% HTML.escape(options) %]">
-  <input type="hidden" name="new_description" value="[% HTML.escape(description) %]">
-
-  <table width="100%">
-   <tr>
-    <td>
-     [% IF no_parts_found %]
-     [% 'No part was found matching the search parameters.' | $T8 %]
-     [% IF allow_creation %]
-     [% 'However, you can create a new part which will then be selected.' | $T8 %]
-     [% END %]
-     [% ELSE %]
-     [% 'Please select a part from the list below.' | $T8 %]
-     [% IF allow_creation %]
-     [% 'Alternatively you can create a new part which will then be selected.' | $T8 %]
-     [% END %]
-     [% END %]
-    </td>
-   </tr>
-
-   [% UNLESS no_parts_found %]
-   <tr>
-    <td>
-     <table>
-      <tr class="listheading">
-       <th class="listheading">&nbsp;</th>
-       [% FOREACH header = HEADER %]
-       <th nowrap class="listheading"><a href="[% HTML.escape(header.callback) %]">[% header.column_title %]</a></th>
-       [% END %]
-      </tr>
-
-      [% FOREACH part = PARTS %]
-      <tr class="listrow[% IF loop.count % 2 %]1[% ELSE %]0[% END %]">
-       <td><button type="button" onclick="part_selected('[% loop.count %]')">[% 'Select' | $T8 %]</button></td>
-       <td>
-        <input type="hidden" id="partsid_[% loop.count %]" name="partsid_[% loop.count %]" value="[% HTML.escape(part.id) %]">
-        <input type="hidden" id="partnumber_[% loop.count %]" name="partnumber_[% loop.count %]" value="[% HTML.escape(part.partnumber) %]">
-        [% HTML.escape(part.partnumber) %]
-       </td>
-       <td>
-        <input type="hidden" id="description_[% loop.count %]" name="description_[% loop.count %]" value="[% HTML.escape(part.description) %]">
-        <input type="hidden" id="partnotes_[% loop.count %]" name="partnotes_[% loop.count %]" value="[% HTML.escape(part.partnotes) %]">
-        [% HTML.escape(part.description) %]
-       </td>
-<!--        <td> -->
-<!--         <input type="hidden" id="onhand_[% loop.count %]" name="onhand_[% loop.count %]" value="[% HTML.escape(part.onhand) %]"> -->
-<!--         [% HTML.escape(part.onhand) %] -->
-<!--        </td> -->
-      </tr>
-      [% END %]
-     </table>
-    </td>
-   </tr>
-   [% END %]
-  </table>
-
-  [% IF allow_creation %]
-  <p><input type="submit" name="action" value="[% 'New part' | $T8 %]"></p>
-  [% END %]
-
- </form>
-
- <script type="text/javascript">
-  <!--
-      function part_selected(selected) {
-        var partnumber = document.getElementsByName("partnumber_" + selected)[0].value;
-        var description = document.getElementsByName("description_" + selected)[0].value;
-        var partsid = document.getElementsByName("partsid_" + selected)[0].value;
-        var partnotes = document.getElementsByName("partnotes_" + selected)[0].value;
-        var pnum_name = document.Form.input_partnumber.value;
-        window.opener.document.getElementsByName(pnum_name)[0].value = partnumber;
-        window.opener.document.getElementsByName(document.Form.input_description.value)[0].value = description;
-        if (document.Form.input_partsid.value != "") {
-          window.opener.document.getElementsByName(document.Form.input_partsid.value)[0].value = partsid;
-        }
-        if (document.Form.input_partnotes.value != "") {
-          var el = window.opener.document.getElementsByName(document.Form.input_partnotes.value)[0];
-          if (el)
-            el.value = partnotes;
-        }
-        if (document.Form.action_on_part_selected.value != "") {
-          window.opener.document.getElementsByName("action")[0].value = document.Form.action_on_part_selected.value;
-          window.opener.document.[% formname %].submit();
-        }
-
-
-        var prefix = "";
-        if (pnum_name.substr(0, 2) == "f_") {
-          prefix = "f_";
-          pnum_name = pnum_name.substr(2);
-        }
-        pnum_name = prefix + "old_" + pnum_name;
-        var input = window.opener.document.getElementsByName(pnum_name)[0];
-        if (input) {
-          input.value = name;
-        }
-
-        [%- IF click_button %]
-        window.opener.document.[% formname %].[% click_button %].click();
-        [%- END %]
-
-        self.close();
-      }
-      //-->
- </script>
-
index f8ee3ef..be8343f 100644 (file)
@@ -5,28 +5,56 @@
  <tr>
   <td>
    <table>
+    [%- IF show_headers %]
     <tr>
-     <td>
-      [%- FOREACH row = SELECTS %]
-      [%- IF row.show %]
-      <select name="[% row.sname %]">
+     [%- FOREACH row = SELECTS %]
+     [%- IF row.show %]
+     <th align="left" id="print_options_header_[% row.sname %]">[%- row.hname %]</th>
+     [%- END %]
+     [%- END %]
+     [%- IF display_copies %]
+     <th align="left" id="print_options_header_copies">[% 'Copies' | $T8 %]</th>
+     [%- END %]
+     [%- IF display_groupitems %]
+     <th align="left" id="print_options_header_groupitems">[% 'Group Items' | $T8 %]</th>
+     [%- END %]
+     [%- IF display_bothsided %]
+     <th align="left" id="print_options_header_bothsided">[% 'Both-sided' | $T8 %]</th>
+     [%- END %]
+     [%- IF display_remove_draft %]
+     <th align="left" id="print_options_header_remove_draft">[% 'Remove Draft' | $T8 %]</th>
+     [%- END %]
+    </tr>
+    [%- END %]
+    <tr>
+     [%- FOREACH row = SELECTS %]
+     [%- IF row.show %]
+     <td id="print_options_input_[% row.sname %]">
+      <select name="[%- name_prefix %][%- row.sname %]" id="[%- id_prefix %][%- row.sname %]">
        [%- FOREACH data = row.DATA %]
-       <option value="[% data.value %]" [% data.selected %]>[% data.oname %]</option>
+        <option value="[% data.value %]" [% data.selected %]>[% data.oname %]</option>
        [%- END %]
       </select>
-      [%- END %]
-      [%- END %]
      </td>
+     [%- END %]
+     [%- END %]
      [%- IF display_copies %]
-     <td>[% 'Copies' | $T8 %] <input name="copies" size="2" value="[% HTML.escape(copies) %]"></td>
+     <td id="print_options_input_copies">[%- IF !show_headers %][%- 'Copies' | $T8 %][%- END %]<input name="[%- name_prefix %]copies" id="[% id_prefix %]copies" size="2" value="[% HTML.escape(copies) %]"></td>
      [%- END %]
      [%- IF display_groupitems %]
-     <td>[% 'Group Items' | $T8 %]</td>
-     <td><input name="groupitems" type="checkbox" class="checkbox" [% groupitems_checked %]></td>
+     <td id="print_options_input_groupitems">[%- IF !show_headers %][% 'Group Items' | $T8 %][%- END %]
+      <input name="[%- name_prefix %]groupitems" id="[% id_prefix %]groupitems" type="checkbox" class="checkbox" [% groupitems_checked %]>
+     </td>
+     [%- END %]
+     [%- IF display_bothsided %]
+     <td id="print_options_input_bothsided">[%- IF !show_headers %][% 'Both-sided' | $T8 %][%- END %]
+      <input name="[%- name_prefix %]bothsided" id="[% id_prefix %]bothsided" type="checkbox" class="checkbox" [% bothsided_checked %]>
+     </td>
      [%- END %]
      [%- IF display_remove_draft %]
-     <td>[% 'Remove Draft' | $T8 %]</td>
-     <td><input name="remove_draft" type="checkbox" class="checkbox" [% remove_draft_checked %]></td>
+     <td id="print_options_input_remove_draft">[%- IF !show_headers %][% 'Remove Draft' | $T8 %][%- END %]
+      <input name="[%- name_prefix %]remove_draft" id="[% id_prefix %]remove_draft" type="checkbox" class="checkbox" [% remove_draft_checked %]>
+     </td>
      [%- END %]
     </tr>
    </table>
@@ -36,4 +64,3 @@
   </td>
  </tr>
 </table>
-
diff --git a/templates/webpages/generic/select_part.html b/templates/webpages/generic/select_part.html
deleted file mode 100644 (file)
index 300bf06..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
- <form method="post" action="[% HTML.escape(script) %]">
-
-  <input type="hidden" name="nextsub" value="[% HTML.escape(nextsub) %]">
-  <input type="hidden" name="callback_sub" value="[% HTML.escape(callback_sub) %]">
-
-  <input type="hidden" name="old_form" value="[% HTML.escape(old_form) %]">
-  <input type="hidden" name="remap_parts_id" value="[% HTML.escape(remap_parts_id) %]">
-  <input type="hidden" name="remap_partnumber" value="[% HTML.escape(remap_partnumber) %]">
-
-  <h1>[% 'Select a part or assembly' | $T8 %]</h1>
-
-  <p>
-   <table>
-    <tr>
-     <th class="listheading">&nbsp;</th>
-     <th class="listheading">[% 'Number' | $T8 %]</th>
-     <th class="listheading">[% 'Description' | $T8 %]</th>
-     [% IF has_charge %]
-     <th class="listheading">[% 'Charge number' | $T8 %]</th>
-     [% END %]
-     [% IF has_bestbefore %]
-     [% IF INSTANCE_CONF.get_show_bestbefore %]
-     <th class="listheading">[% 'Best Before' | $T8 %]</th>
-     [% END %]
-     [% END %]
-     [% IF has_ean %]
-     <th class="listheading">[% 'EAN' | $T8 %]</th>
-     [% END %]
-    </tr>
-
-    [% FOREACH part = PARTS %]
-    <tr class="listrow[% loop.count % 2 %]">
-     <td>
-      <input type="radio" name="selection" value="[% loop.count %]"[% IF loop.first %] checked[% END %]>
-     </td>
-
-     <td>
-      <input type="hidden" name="new_id_[% loop.count %]" value="[% HTML.escape(part.id) %]">
-      <input type="hidden" name="new_number_[% loop.count %]" value="[% HTML.escape(part.number) %]">
-      <input type="hidden" name="new_warehouse_id_[% loop.count %]" value="[% HTML.escape(part.warehouse_id) %]">
-      <input type="hidden" name="new_bin_id_[% loop.count %]" value="[% HTML.escape(part.bin_id) %]">
-      [% HTML.escape(part.number) %]
-     </td>
-
-     <td>
-      <input type="hidden" name="new_description_[% loop.count %]" value="[% HTML.escape(part.description) %]">
-      [% HTML.escape(part.description) %]
-     </td>
-
-     [% IF has_charge %]
-     <td>
-      <input type="hidden" name="new_charge_id_[% loop.count %]" value="[% HTML.escape(part.charge_id) %]">
-      <input type="hidden" name="new_chargenumber_[% loop.count %]" value="[% HTML.escape(part.chargenumber) %]">
-      [% HTML.escape(part.chargenumber) %]
-     </td>
-     [% END %]
-     [% IF has_bestbefore %]
-     [% IF INSTANCE_CONF.get_show_bestbefore %]
-     <td>
-      <input type="hidden" name="new_bestbefore_id_[% loop.count %]" value="[% HTML.escape(part.bestbefore_id) %]">
-      <input type="hidden" name="new_bestbefore_[% loop.count %]" value="[% HTML.escape(part.bestbefore) %]">
-      [% HTML.escape(part.bestbefore) %]
-     </td>
-     [% END %]
-     [% END %]
-     [% IF has_ean %]
-     <td>
-      <input type="hidden" name="new_ean_[% loop.count %]" value="[% HTML.escape(part.ean) %]">
-      [% HTML.escape(part.ean) %]
-     [% END %]
-     </td>
-    </tr>
-    [% END %]
-   </table>
-  </p>
-
-  <p>
-   <input type="submit" class="submit" name="action" value="[% 'Continue' | $T8 %]">
-  </p>
-
- </form>
-
index e7a357b..eb74675 100644 (file)
@@ -1,7 +1,6 @@
 [%- USE L -%][%- USE LxERP -%]
 
 <div id="edit_longdescription_dialog" style="display: none">
- <p>
   <table>
    <tr>
     <th align="right">[% LxERP.t8("Row") %]:</th>
     <td id="popup_edit_longdescription_description"></td>
    </tr>
   </table>
- </p>
 
  <p id="popup_edit_longdescription_input_container"></p>
 
  <p>
-  [% L.button_tag("kivi.SalesPurchase.set_longdescription()", LxERP.t8("Close")) %]
+  [% L.button_tag("kivi.SalesPurchase.set_longdescription()", LxERP.t8("Assign")) %]
   <a onclick="$('#edit_longdescription_dialog').dialog('close');" href="#">[% LxERP.t8("Abort") %]</a>
  </p>
 </div>
+
+<script>
+  $(function() {
+    kivi.SalesPurchase.longdescription_dialog_size_percentage = "[% longdescription_dialog_size_percentage %]";
+  });
+</script>
diff --git a/templates/webpages/generictranslations/edit_email_strings.html b/templates/webpages/generictranslations/edit_email_strings.html
new file mode 100644 (file)
index 0000000..a25cc7e
--- /dev/null
@@ -0,0 +1,41 @@
+[%- USE T8 %]
+[%- USE HTML %]
+<h1>[% HTML.escape(title) %]</h1>
+[%- IF message %]
+ <p>
+ [% HTML.escape(message) %]
+ </p>
+[%- END %]
+ <form method="post" action="generictranslations.pl" id="form">
+  <table>
+   [%- FOREACH mail_string IN MAIL_STRINGS.keys.sort %]
+    <tr>
+      <th class="listheading">&nbsp;</th>
+      <th class="listheading">[% MAIL_STRINGS.$mail_string %]</th>
+     </tr>
+
+     [%- FOREACH language = LANGUAGES %]
+     <tr>
+      <td>
+       [%- IF language.id == 'default' %]
+       [% 'Default (no language selected)' | $T8 %]
+       [%- ELSE %]
+       [%- HTML.escape(language.description) %]
+       [%- END %]
+       [%- IF mail_string.search('preset_text_periodic_invoices') %]
+        <br />
+        <a href="doc/html/ch03.html#features.periodic-invoices.variables" target="_blank">?</a>
+       [%- END %]
+      </td>
+      <td>
+       [%- IF mail_string.search('preset') && !mail_string.search('subject')%]
+        <textarea name="translation__[% language.id %]__[% mail_string %]" rows="4" cols="60" class="texteditor">[% HTML.escape(language.$mail_string) %]</textarea>
+       [%- ELSE %]
+        <input name="translation__[% language.id %]__[% mail_string %]" size="40" value="[% HTML.escape(language.$mail_string) %]">
+       [%- END %]
+      </td>
+     </tr>
+     [%- END %]
+   [%- END %]
+  </table>
+ </form>
index 5e2f311..8072d37 100644 (file)
@@ -8,7 +8,7 @@
  </p>
  [%- END %]
 
- <form method="post" action="generictranslations.pl">
+ <form method="post" action="generictranslations.pl" id="form">
 
   <table>
 
    [%- END %]
 
   </table>
-
-  <p>
-   <input type="hidden" name="action" value="save_greetings">
-   <input type="submit" class="submit" value="[% 'Save' | $T8 %]">
-  </p>
-
  </form>
-
index e9875b0..e4eb9d0 100644 (file)
@@ -8,7 +8,7 @@
  </p>
  [%- END %]
 
- <form method="post" action="generictranslations.pl">
+ <form method="post" action="generictranslations.pl" id="form">
 
   <table>
 
     <td><input name="translation__[% language.id %]" size="40" value="[% HTML.escape(language.translation) %]"></td>
    </tr>
    [%- END %]
+   <tr>
+    <th class="listheading">&nbsp;</th>
+    <th class="listheading">[% 'Remittance information optional Vendor/Customer No postfix' | $T8 %]</th>
+   </tr>
 
-  </table>
-
-  <p>
-   <input type="hidden" name="action" value="save_sepa_strings">
-   <input type="submit" class="submit" value="[% 'Save' | $T8 %]">
-  </p>
+   [%- FOREACH language = LANGUAGES %]
+   <tr>
+    <td>
+     [%- IF language.id == 'default' %]
+     [% 'Default (no language selected)' | $T8 %]
+     [%- ELSE %]
+     [%- HTML.escape(language.description) %]
+     [%- END %]
+    </td>
+    <td><input name="translation__[% language.id %]__vc" size="40" value="[% HTML.escape(language.translation_vc) %]"></td>
+   </tr>
+   [%- END %]
 
+  </table>
  </form>
-
diff --git a/templates/webpages/generictranslations/edit_zugferd_notes.html b/templates/webpages/generictranslations/edit_zugferd_notes.html
new file mode 100644 (file)
index 0000000..5c1ee4e
--- /dev/null
@@ -0,0 +1,33 @@
+[%- USE T8 %]
+[%- USE HTML %]
+<h1>[% HTML.escape(title) %]</h1>
+
+ [%- IF message %]
+ <p>
+  [% HTML.escape(message) %]
+ </p>
+ [%- END %]
+
+ <form method="post" action="generictranslations.pl" id="form">
+
+  <table>
+
+   <tr>
+    <th class="listheading">&nbsp;</th>
+    <th class="listheading">[% 'Factur-X/ZUGFeRD notes for each invoice' | $T8 %]</th>
+   </tr>
+
+   [%- FOREACH language = LANGUAGES %]
+   <tr>
+    <td>
+     [%- IF language.id == 'default' %]
+     [% 'Default (no language selected)' | $T8 %]
+     [%- ELSE %]
+     [%- HTML.escape(language.description) %]
+     [%- END %]
+    </td>
+    <td><input name="translation__[% language.id %]" size="40" value="[% HTML.escape(language.translation) %]"></td>
+   </tr>
+   [%- END %]
+  </table>
+ </form>
index ede815b..b5769bd 100644 (file)
@@ -2,49 +2,43 @@
 [%- USE LxERP %]
 [%- USE T8 %]
 [%- USE L %]
-    <tr class=listtotal>
-    <th colspan="3" align=right class=listtotal> [% LxERP.format_amount(totaldebit, 2) | html %]</th>
-    <th align=right class=listtotal> [% LxERP.format_amount(totalcredit, 2) | html %]</th>
-    <td colspan=6></td>
+      <tr class=listtotal>
+      <th colspan="3" align=right class=listtotal> [% LxERP.format_amount(totaldebit, 2) | html %]</th>
+      <th align=right class=listtotal> [% LxERP.format_amount(totalcredit, 2) | html %]</th>
+      <td colspan=6></td>
+      </tr>
+    </table>
+    </td>
     </tr>
   </table>
   </td>
   </tr>
 </table>
+</div>
+[% PROCESS 'webdav/_list.html' %]
+</div>
 
+<hr size="3" noshade>
 <input name=callback type=hidden value="[% callback %]">
+<input name=bt_id    type=hidden value="[% bt_id %]">
+<input name=bt_chart_id type=hidden value="[% bt_chart_id %]">
 
 [%- IF id && follow_ups.size %]
   <p>[% LxERP.t8('There are #1 unfinished follow-ups of which #2 are due.', follow_ups.size , follow_ups_due) %]</p>
 [%- END %]
-
-<br>
-
-[%- IF id %]
-  [% L.submit_tag('action', LxERP.t8('Update'), id='update_button') %]
-
-  [% IF !locked && radieren %]
-    [% L.submit_tag('action', LxERP.t8('Post'), accesskey='b') %]
-    [% L.submit_tag('action', LxERP.t8('Delete')) %]
+</form>
+
+<script type="text/javascript">
+ <!--
+$(document).ready(function() {
+  [%- SET row=0 %]
+  [%- WHILE row < rowcount %]
+   [%- SET row=row + 1 %]
+   $('#accno_id_[% row %]').on('set_item:ChartPicker', function(e, item) {
+     kivi.GL.show_chart_balance(this);
+     kivi.GL.update_taxes(this);
+   });
   [%- END %]
-
-  [%- IF !storno %]
-    [% L.submit_tag('action', LxERP.t8('Storno')) %]
-  [%- END %]
-
-  [% L.submit_tag('action', LxERP.t8('Follow-Up'), onclick='follow_up_window()') %]
-[%- ELSE %]
-
- [%- IF draft_id %]
-      <p>[% L.checkbox_tag('remove_draft', checked=remove_draft, label=LxERP.t8('Remove Draft')) %]</p>
- [%- END %]
-
-      [% L.submit_tag('action', LxERP.t8('Update'), id='update_button') %]
-      [% L.submit_tag('action', LxERP.t8('Post')) %]
-      [% L.submit_tag('action', LxERP.t8('Save Draft')) %]
-      [% L.hidden_tag('draft_id', draft_id) %]
-      [% L.hidden_tag('draft_description', draft_description) %]
-[%- END %]
-
-  </form>
-
+});
+-->
+</script>
index e349761..8dd130a 100644 (file)
@@ -4,28 +4,10 @@
 [%- USE L %]
 <h1>[% title | html %]</h1>
 
+[%- INCLUDE 'common/flash.html' %]
+
 <script type="text/javascript">
   <!--
-function updateTaxes(row)
-{
-  var accno  = document.getElementById('accno_' + row);
-  var taxkey = accno.options[accno.selectedIndex].value;
-  var reg = /^(.*?)--(\d*)$/;
-  var found = reg.exec(taxkey);
-  var account = found[1];
-  var default_tax_id = found[2];
-
-  $.ajax({
-    url: 'gl.pl?action=get_tax_dropdown',
-    data: { accno: account,
-            selected_tax_id: default_tax_id},
-    dataType: 'html',
-    success: function (new_html) {
-                                $("#taxchart_" + row).html(new_html);
-                              },
-  });
-};
-
   function copy_debit_to_credit() {
     var txt = document.getElementsByName('debit_1')[0].value;
     document.getElementsByName('credit_2')[0].value = txt;
@@ -34,8 +16,9 @@ function updateTaxes(row)
   </script>
   <script type="text/javascript" src="js/show_form_details.js"></script>
 <script type="text/javascript" src="js/follow_up.js"></script>
+<script type="text/javascript" src="js/kivi.Draft.js"></script>
 
-<form method=post name="gl" action=gl.pl>
+<form method="post" name="gl" action="gl.pl" id="form">
 
 [% FOREACH name IN [ 'id', 'closedto', 'locked', 'storno', 'storno_id', 'previous_id', 'previous_gldate' ] %]
 [% L.hidden_tag(name, $name) %]
@@ -48,6 +31,29 @@ function updateTaxes(row)
 <input type="hidden" name="follow_up_trans_info_1" value="[% id | html %]">
 <input type="hidden" name="follow_up_rowcount" value="1">
 
+<div id="gl_tabs" class="tabwidget">
+ <ul>
+  <li><a href="#ui-tabs-basic-data">[% 'Basic Data' | $T8 %]</a></li>
+[%- IF INSTANCE_CONF.get_webdav %]
+  <li><a href="#ui-tabs-webdav">[% 'WebDAV' | $T8 %]</a></li>
+[%- END %]
+[%- IF id %]
+[%- IF INSTANCE_CONF.get_doc_storage %]
+  <li><a href="#ui-tabs-docs">[% 'Documents' | $T8 %]</a></li>
+  <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=gl_transaction&object_id=[% HTML.url(id) %]">[% 'Attachments' | $T8 %]</a></li>
+[%- END %]
+  [%- IF AUTH.assert('record_links', 1) %]
+  <li><a href="controller.pl?action=RecordLinks/ajax_list&object_model=GLTransaction&object_id=[% HTML.url(id) %]">[% 'Linked Records' | $T8 %]</a></li>
+  [%- END %]
+[%- END %]
+ </ul>
+
+[%- IF INSTANCE_CONF.get_doc_storage %]
+  <div id="ui-tabs-docs"></div>
+[%- END %]
+
+ <div id="ui-tabs-basic-data">
+
 <table width=100%>
 [%- IF saved_message %]
   <tr>
@@ -58,76 +64,54 @@ function updateTaxes(row)
   <tr height="5"></tr>
   <tr>
     <td>
-      <table width=100%>
+      <table>
         <tr>
           <td colspan="6" align="left">[% 'Previous transnumber text' | $T8 %] [% previous_id  %] . [% 'Previous transdate text' | $T8 %] [% previous_gldate %]</td>
         </tr>
         <tr>
-          <th align=right>[% 'Reference' | $T8 %]</th>
-          <td>[% L.input_tag('reference', reference,  size=20, readonly=readonly) %]</td>
-          <td align=left>
-            <table>
-              <tr>
-                <th align=right width=50% nowrap>[% 'Date' | $T8 %]</th>
-                <td>[% L.date_tag('transdate', transdate, readonly=readonly) %]</td>
-              </tr>
-            </table>
-          </td>
+          <th align="right">[% 'Reference' | $T8 %]</th>
+          <td>[% L.input_tag('reference', reference,  style='width:330px', readonly=readonly) %]</td>
+          <th align="right">[% 'Transdate' | $T8 %]</th>
+          <td>[% L.date_tag('transdate', transdate, readonly=readonly) %]</td>
         </tr>
 [%- IF id %]
         <tr>
-          <th align=right>[% 'Belegnummer' | $T8 %]</th>
-          <td>[% L.input_tag('id', id,  size=20, readonly=readonly) %]</td>
-          <td align=left>
-          <table>
-              <tr>
-                <th align=right width=50%>[% 'Buchungsdatum' | $T8 %]</th>
-                <td align=left>[% L.date_tag('gldate', gldate, readonly=1) %]</td>
-              </tr>
-            </table>
-          </td>
+          <th align="right">[% 'ID' | $T8 %]</th>
+          <td>[% id %]</td>
+          <th align="right">[% 'Gldate' | $T8 %]</th>
+          <td>[% L.date_tag('gldate', gldate, readonly=1) %]<img class="ui-datepicker-trigger" src="image/calendar.png" alt="..." title="..." style='visibility:hidden'></td>
         </tr>
 [%- END %]
 
-[%- IF selectdepartment %]
+        [% SET departments_style = "";
+           SET departments_style = "style='visibility:hidden'" IF ALL_DEPARTMENTS.size == 0 %]
         <tr>
-          <th align=right nowrap>[% 'Department' | $T8 %]</th>
-          <td colspan=3><select name=department>[% selectdepartment %]</select></td>
-          <input type=hidden name=selectdepartment value="[% selectdepartment | html %]">
+          <th [%- departments_style -%]align="right">[% 'Department' | $T8 %]</th>
+          <td [%- departments_style -%]>[% L.select_tag('department_id', ALL_DEPARTMENTS, default = department_id, title_key = 'description', with_empty = 1, style = 'width:334px') %]</td>
+          <th align=right>[% 'Tax point' | $T8 %]</th>
+          <td>[% L.date_tag('tax_point', tax_point) %]</td>
         </tr>
-[%- END %]
-
         <tr>
-          <th align=right width=1%>[% 'Description' | $T8 %]</th>
-          <td width=1%>[% L.areainput_tag('description', description, cols=50, readonly=readonly) %]</td>
-          <td>
-            <table>
-              <tr>
-                <th align=left>[% 'MwSt. inkl.' | $T8 %]</th>
-                <td>[% L.checkbox_tag('taxincluded', checked=taxincluded) %]</td>
-              </tr>
-            </table>
-         </td>
-[%- IF id %]
-          <td align=left>
-            <table width=100%>
-              <tr>
-                <th align=right width=50%>[% 'Mitarbeiter' | $T8 %]</th>
-                <td align=left>[% L.input_tag('employee', employee, size=20, readonly=readonly) %]</td>
-              </tr>
-            </table>
-          </td>
-[%- END %]
+          <th align="right">[% 'Transaction description' | $T8 %]</th>
+          <td>[% L.input_tag("transaction_description", transaction_description, style='width:330px') %]</td>
+          <th align=right>[% 'Delivery Date' | $T8 %]</th>
+          <td>[% L.date_tag('deliverydate', deliverydate) %]</td>
+        </tr>
+        <tr>
+          <th align="right">[% 'Description' | $T8 %]</th>
+          <td>[% L.areainput_tag('description', description, cols=50, style='width:330px', readonly=readonly) %]</td>
+        </tr>
+        <tr>
+          <th align="right">[%- IF id %][% 'Mitarbeiter' | $T8 %][% END %]</th>
+          <td>[%- IF id %][% L.input_tag('employee', employee, size=20, readonly=readonly) %][% END %]</td>
+          <th align="right">[% 'MwSt. inkl.' | $T8 %]</th>
+          <td>[% L.checkbox_tag('taxincluded', checked=taxincluded) %]</td>
         </tr>
 
       <tr>
        <td colspan=4>
-        <table>
-         <tr>
-          <td>[% 'OB Transaction' | $T8 %] [% L.checkbox_tag('ob_transaction' checked=ob_transaction) %]</td>
-          <td>[% 'CB Transaction' | $T8 %] [% L.checkbox_tag('cb_transaction' checked=cb_transaction) %]</td>
-         </tr>
-        </table>
+         [% 'OB Transaction' | $T8 %] [% L.checkbox_tag('ob_transaction' checked=ob_transaction) %]
+         [% 'CB Transaction' | $T8 %] [% L.checkbox_tag('cb_transaction' checked=cb_transaction) %]
        </td>
       </tr>
       <tr>
@@ -153,5 +137,3 @@ function updateTaxes(row)
 [%- END %]
 
         </tr>
-
-[%- PROCESS 'gl/form_header_chart_balances_js.html' %]
diff --git a/templates/webpages/gl/form_header_chart_balances_js.html b/templates/webpages/gl/form_header_chart_balances_js.html
deleted file mode 100644 (file)
index 705d8f6..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-[% USE HTML %]
-[% USE JavaScript %]
-[% USE LxERP %]
-
-<script type="text/javascript">
- <!--
-var chart_balances = new Array();
-
-[% FOREACH chart = ALL_CHARTS %]
-chart_balances[[% loop.count - 1 %]] = '[% JavaScript.escape(LxERP.format_amount(chart.balance, 2, 0, 'DRCR')) %]';
-[%- END %]
-
-function show_chart_balance(obj) {
-  var row = $(obj).attr('name').replace(/.*_/, '');
-  var idx = $('#accno_' + row).prop('selectedIndex');
-  $('#chart_balance_' + row).html(chart_balances[idx]);
-}
-
-$(document).ready(function() {
-  [%- SET row=0 %]
-  [%- WHILE row < rowcount %]
-   [%- SET row=row + 1 %]
-   $('#accno_[% row %]').change(function() { show_chart_balance(this); });
-   show_chart_balance($('#accno_[% row %]'));
-  [%- END %]
-});
--->
-</script>
index 98d5c71..460ba02 100644 (file)
@@ -1,11 +1,4 @@
-[% USE T8 %][% USE HTML %]<form method="post" action="dispatcher.pl?M=gl">
-
+[% USE HTML %]
+<form method="post" id="create_new_form" action="gl.pl">
  <input name="callback" type="hidden" value="[% HTML.escape(callback) %]">
-
- <input class="submit" type="submit" name="A_gl_transaction" value="[%- 'GL Transaction' | $T8 %]">
- <input class="submit" type="submit" name="A_ar_transaction" value="[%- 'AR Transaction' | $T8 %]">
- <input class="submit" type="submit" name="A_ap_transaction" value="[%- 'AP Transaction' | $T8 %]">
- <input class="submit" type="submit" name="A_sales_invoice" value="[%- 'Sales Invoice' | $T8 %]">
- <input class="submit" type="submit" name="A_vendor_invoice" value="[%- 'Vendor Invoice' | $T8 %]">
-
 </form>
index e9ec797..e7e1d5b 100644 (file)
@@ -4,7 +4,7 @@
 [%- USE L %]
 <h1>[% 'Journal' | $T8 %]</h1>
 
-<form method=post action=gl.pl>
+<form method=post id="form" action=gl.pl>
 
 <input type=hidden name=sort value=datesort>
 
           <th align=right>[% 'Source' | $T8 %]</th>
           <td><input name=source size=20></td>
         </tr>
-        [%- IF all_departments %]
+        [%- IF ALL_DEPARTMENTS %]
         <tr>
           <th align=right nowrap>[% 'Department' | $T8 %]</th>
-          <td colspan=3>[% L.select_tag('department', all_departments, value_title_sub = \department_label, with_empty = 1) %]</td>
+          <td colspan=3>[% L.select_tag('department_id', ALL_DEPARTMENTS, title_key = 'description', with_empty = 1) %]</td>
         </tr>
         [%- END %]
         <tr>
           <th align=right>[% 'Notes' | $T8 %]</th>
           <td colspan=3><input name=notes size=40></td>
         </tr>
+        <tr>
+          <th align=right>[% 'Transaction description' | $T8 %]</th>
+          <td>[% L.input_tag("transaction_description", "", size=40) %]</td>
+        </tr>
         <tr>
           <th align=right>[% 'Project Number' | $T8 %]</th>
           <td colspan=3>[% L.select_tag('project_id', ALL_PROJECTS, title_key = 'projectnumber', with_empty = 1) %]</td>
         </tr>
- <tr>
-    <th align=right>[% 'Employee' | $T8 %]</th>
-    <td colspan=3>[% L.select_tag('employee_id', ALL_EMPLOYEES, title_key = 'safe_name', with_empty = 1) %]</td>
-  </tr>
-  <tr>
-    <th align=right>[% 'Filter date by' | $T8 %]</th>
-    <td colspan=3>
-    <input name=datesort class=radio type=radio value=gldate checked> [% 'Booking Date' | $T8 %]
-    <input name=datesort class=radio type=radio value=transdate> [% 'Invoice Date' | $T8 %]
-  </td>
-  </tr>
-  <tr>
-    <th align=right>[% 'From' | $T8 %]</th>
       <tr>
+          <th align=right>[% 'Employee' | $T8 %]</th>
+          <td colspan=3>[% L.select_tag('employee_id', ALL_EMPLOYEES, title_key = 'safe_name', with_empty = 1) %]</td>
+        </tr>
+        <tr>
+          <th align=right>[% 'Filter date by' | $T8 %]</th>
+          <td colspan=3>
+            <input name=datesort class=radio type=radio value=transdate checked> [% 'Transdate' | $T8 %]
+            <input name=datesort class=radio type=radio value=gldate> [% 'Gldate' | $T8 %]
+          </td>
+        </tr>
+        <tr>
+          <th align=right>[% 'From' | $T8 %]</th>
           <td>[% L.date_tag('datefrom') %]</td>
           <th align=right>[% 'To (time)' | $T8 %]</th>
           <td>[% L.date_tag('dateto') %]</td>
                 </td>
               </tr>
               <tr>
-                <table>
-                  <tr>
-                    <td align=right><input name="l_id" class=checkbox type=checkbox value=Y></td>
-                    <td>[% 'ID' | $T8 %]</td>
-                    <td align=right><input name="l_transdate" class=checkbox type=checkbox value=Y checked></td>
-                    <td>[% 'Invoice Date' | $T8 %]</td>
-                    <td align=right><input name="l_gldate" class=checkbox type=checkbox value=Y checked></td>
-                    <td>[% 'Booking Date' | $T8 %]</td>
-                    <td align=right><input name="l_reference" class=checkbox type=checkbox value=Y checked></td>
-                    <td>[% 'Reference' | $T8 %]</td>
-                    <td align=right><input name="l_description" class=checkbox type=checkbox value=Y checked></td>
-                    <td>[% 'Description' | $T8 %]</td>
-                    <td align=right><input name="l_notes" class=checkbox type=checkbox value=Y></td>
-                    <td>[% 'Notes' | $T8 %]</td>
-                  </tr>
-                  <tr>
-                    <td align=right><input name="l_debit" class=checkbox type=checkbox value=Y checked></td>
-                    <td>[% 'Debit' | $T8 %]</td>
-                    <td align=right><input name="l_credit" class=checkbox type=checkbox value=Y checked></td>
-                    <td>[% 'Credit' | $T8 %]</td>
-                    <td align=right><input name="l_source" class=checkbox type=checkbox value=Y checked></td>
-                    <td>[% 'Source' | $T8 %]</td>
-                    <td align=right><input name="l_accno" class=checkbox type=checkbox value=Y checked></td>
-                    <td>[% 'Account' | $T8 %]</td>
-                  </tr>
-                  <tr>
-                    <td align=right><input name="l_subtotal" class=checkbox type=checkbox value=Y></td>
-                    <td>[% 'Subtotal' | $T8 %]</td>
-                    <td align=right><input name="l_projectnumbers" class=checkbox type=checkbox value=Y></td>
-                    <td>[% 'Project Number' | $T8 %]</td>
-                    <td align=right><input name="l_employee" class=checkbox type=checkbox value=Y></td>
-                    <td>[% 'Employee' | $T8 %]</td>
-                  </tr>
-                </table>
+                <td>
+                  <table>
+                    <tr>
+                      <td align=right><input name="l_id" class=checkbox type=checkbox value=Y></td>
+                      <td>[% 'ID' | $T8 %]</td>
+                      <td align=right><input name="l_transdate" class=checkbox type=checkbox value=Y checked></td>
+                      <td>[% 'Transdate' | $T8 %]</td>
+                      <td align=right><input name="l_gldate" class=checkbox type=checkbox value=Y checked></td>
+                      <td>[% 'Gldate' | $T8 %]</td>
+                      <td align=right><input name="l_reference" class=checkbox type=checkbox value=Y checked></td>
+                      <td>[% 'Reference' | $T8 %]</td>
+                      <td align=right><input name="l_description" class=checkbox type=checkbox value=Y checked></td>
+                      <td>[% 'Description' | $T8 %]</td>
+                      <td align=right><input name="l_notes" class=checkbox type=checkbox value=Y></td>
+                      <td>[% 'Notes' | $T8 %]</td>
+                    </tr>
+                    <tr>
+                      <td align=right><input name="l_debit" class=checkbox type=checkbox value=Y checked></td>
+                      <td>[% 'Debit' | $T8 %]</td>
+                      <td align=right><input name="l_credit" class=checkbox type=checkbox value=Y checked></td>
+                      <td>[% 'Credit' | $T8 %]</td>
+                      <td align=right><input name="l_source" class=checkbox type=checkbox value=Y checked></td>
+                      <td>[% 'Source' | $T8 %]</td>
+                      <td align=right><input name="l_accno" class=checkbox type=checkbox value=Y checked></td>
+                      <td>[% 'Account' | $T8 %]</td>
+                      [%- IF ALL_DEPARTMENTS %]
+                      <td align=right><input name="l_department" class=checkbox type=checkbox value=Y></td>
+                      <td>[% 'Department' | $T8 %]</td>
+                      [%- END %]
+                    </tr>
+                    <tr>
+                      <td align=right><input name="l_projectnumbers" class=checkbox type=checkbox value=Y></td>
+                      <td>[% 'Project Number' | $T8 %]</td>
+                      <td align=right><input name="l_employee" class=checkbox type=checkbox value=Y></td>
+                      <td>[% 'Employee' | $T8 %]</td>
+                      <td align=right><input name="l_transaction_description" id="l_transaction_description" class=checkbox type=checkbox value=Y[% IF INSTANCE_CONF.get_require_transaction_description_ps %] checked[% END %]></td>
+                      <td>[% 'Transaction description' | $T8 %]</td>
+                    </tr>
+                    <tr>
+                      <td align=right><input name="l_subtotal" class=checkbox type=checkbox value=Y></td>
+                      <td>[% 'Subtotal' | $T8 %]</td>
+                    </tr>
+                  </table>
+                </td>
               </tr>
             </table>
         </tr>
       </table>
     </td>
   </tr>
-  <tr>
-    <td><hr size=3 noshade></td>
-  </tr>
 </table>
-
-<input type=hidden name=nextsub value=generate_report>
-
-<br>
-<input class=submit type=submit name=action value="[% 'Continue' | $T8 %]">
 </form>
index d3c9586..72ce6ef 100644 (file)
@@ -1,4 +1,4 @@
-[% USE L %]
+[% USE L %][%- USE LxERP -%]
 [% FOR row = TAX_ACCOUNTS %]
-<option value='[% row.id %]--[% row.rate %]'[% IF row.id == selected_tax_id%] selected[% END %]>[% row.taxdescription %] %</option>
+<option value='[% row.id %]--[% row.rate %]'[% IF row.is_default %] selected[% END %]>[% row.taxkey _ " - " _ row.taxdescription %] [% LxERP.round_amount(row.rate * 100) %] %</option>
 [% END %]
diff --git a/templates/webpages/gobd/filter.html b/templates/webpages/gobd/filter.html
new file mode 100644 (file)
index 0000000..78c0b83
--- /dev/null
@@ -0,0 +1,28 @@
+[%- USE HTML %]
+[%- USE T8 %]
+[%- USE L %]
+[%- USE LxERP %]
+
+<h1>[% title | html %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+<p>[% 'This export will include all records in the given time range and all supplicant information from checked entities. You will receive a single zip file. Please extract this file onto the data medium requested by your auditor.' | $T8 %]</p>
+
+<form id='filter_form'>
+
+<table>
+  <tr>
+    <td>[% L.radio_button_tag('method', value='year', checked=1) %]
+    <td>[% 'Year' | $T8 %]</td>
+    <td>[% L.select_tag('year', SELF.available_years, default=current_year) %]</td>
+  </tr>
+  <tr>
+    <td>[% L.radio_button_tag('method') %]
+    <td>[% 'From Date' | $T8 %]</td>
+    <td>[% L.date_tag('from', SELF.from) %]</td>
+    <td>[% 'To Date' | $T8 %]</td>
+    <td>[% L.date_tag('to', SELF.to) %]</td>
+  </tr>
+</table>
+</form>
diff --git a/templates/webpages/ic/ajax_autocomplete.html b/templates/webpages/ic/ajax_autocomplete.html
deleted file mode 100644 (file)
index 5bbd3b6..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-[%- USE HTML %]
-[%- FOREACH part = parts %]
-[% IF loop.count < limit %]
-[% part.$column %]
-[%- END %]
-[%- END %]
diff --git a/templates/webpages/ic/assembly_row.html b/templates/webpages/ic/assembly_row.html
deleted file mode 100644 (file)
index 401b54a..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-[%- USE T8 %]
-[%- USE LxERP %]
-[%- USE HTML %]
-[%- USE L %]
-  <tr class=listheading>
-   <th class=listheading>[% 'Individual Items' | $T8 %]</th>
-  </tr>
-  <tr>
-   <td>
-    <table width=100%>
-     <tr>
-[%- FOREACH col = COLUMNS %]
-[%- SET hcol = HEADER.$col %]
-      <th[% ' nowrap' IF hcol.nowrap %][% ' width=' _ hcol.width IF hcol.width %][% ' align=' _ hcol.align IF hcol.align %]>[% hcol.text %]</th>
-[%- END %]
-     </tr>
-[%- FOREACH row = ROWS %]
-     <tr>
- [%- FOREACH col = COLUMNS %]
-  [%- SET rcol = row.$col %]
-  [%- IF rcol.escape %]
-      <td[% ' align=' _ rcol.align IF rcol.align %]>[%- HTML.escape(rcol.data) %]</td>
-  [%- ELSE %]
-      <td[% ' align=' _ rcol.align IF rcol.align %]>[%- IF rcol.link %][% L.link(rcol.link, rcol.data) %][% ELSE %][% rcol.data %][% END %]</td>
-  [%- END %]
- [%- END %]
- [%- FOREACH hidden = row.hiddens %]
-      <input type=hidden name="[% HTML.escape(hidden.name) %]" value="[% HTML.escape(hidden.value) %]">
- [%- END %]
-     </tr>
-[%- END %]
-     <tr>
-      <td colspan="6"></td>
-      <td>[% 'Totals' | $T8 %]</td>
-      <td align="right">[%- LxERP.format_amount(assembly_purchase_price_total, 2) %]</td>
-      <td align="right">[%- LxERP.format_amount(assemblytotal, 2) %]</td>
-     </tr>
-     <input type="hidden" name="assembly_rows" value="[% assembly_rows %]">
-    </table>
-   </td>
-  </tr>
diff --git a/templates/webpages/ic/choice.html b/templates/webpages/ic/choice.html
deleted file mode 100644 (file)
index 938c001..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-[%- USE LxERP %]
-
- <form method="post" action="ic.pl">
-
-  [%- FOREACH row = HIDDENS %]
-  <input type="hidden" name="[% HTML.escape(row.name) %]" value="[% HTML.escape(row.value) %]" >
-  [%- END %]
-
-  <p>
-   <table>
-    <tr class="listheading">
-     <th class="listheading" nowrap>[% 'Part Number' | $T8 %]</th>
-     <th class="listheading" nowrap>[% 'Part Description' | $T8 %]</th>
-    </tr>
-    <tr valign="top">
-     <td><input type="text" name="partnumber" size="20"></td>
-     <td><input type="text" name="description" size="30"></td>
-    </tr>
-   </table>
-  </p>
-
-  [%- FOREACH row = PARTS %]
-  <input type="hidden" name="totop100_partnumber_[% loop.count %]" value="[% row.totop100_partnumber %]">
-  <input type="hidden" name="totop100_description_[% loop.count %]" value="[% row.totop100_description %]">
-  <input type="hidden" name="totop100_unit_[% loop.count %]" value="[% row.totop100_unit %]">
-  <input type="hidden" name="totop100_sellprice_[% loop.count %]" value="[% row.totop100_sellprice %]">
-  <input type="hidden" name="totop100_soldtotal_[% loop.count %]" value="[% row.totop100_soldtotal %]">
-  [%- END %]
-
-  <p>
-   <input class="submit" type="submit" name="action" value="[% 'list' | $T8 %]">
-  </p>
- </form>
-
index 910c91a..ed18d7e 100644 (file)
@@ -1,23 +1,15 @@
 [%- USE T8 %]
 [%- USE HTML %]
 [%- USE LxERP %]
+[%- USE L %]
 
- <form method="post" action="ic.pl">
-
-  [%- FOREACH row = HIDDENS %]
-  <input type="hidden" name="[% HTML.escape(row.name) %]" value="[% HTML.escape(row.value) %]" >
-  [%- END %]
+ <form method="post" action="controller.pl" id="form">
 
   <h2 class="confirm">[% 'Confirm!' | $T8 %]</h2>
 
-  <p>
-   [% LxERP.t8('Approximately #1 prices will be updated.', num_matches) %]
-  </p>
+  <p>[% LxERP.t8('Approximately #1 prices will be updated.', num_matches) %]</p>
 
   <p>[% 'Are you sure you want to update the prices' | $T8 %]?</p>
 
-  <p>
-   <input name="action" class="submit" type="submit" value="[% 'Continue' | $T8 %]">
-   <input type="button" class="submit" onclick="history.back()" value="[% 'Back' | $T8 %]">
-  </p>
+  [% L.hidden_tag('filter_key', filter_key) %]
  </form>
diff --git a/templates/webpages/ic/form_footer.html b/templates/webpages/ic/form_footer.html
deleted file mode 100644 (file)
index a11c578..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-[%- USE LxERP %]
-  [%- IF item == 'assembly' %]
-  <!-- Wieder zurueck in die ic.pl, entweder alle Einzelteile auslagern oder gar keine -->
-  <!-- tr>
-    <td>
-      <table border="0" width="100%">
-        <tr>
-          <th colspan="2" align=right>[% 'Total' | $T8 %]&nbsp;</th>
-          <th align=right>[% 'Purchase Price' | $T8 %]:[% LxERP.format_amount(assembly_purchase_price_total, 2) %]  [% 'Sell Price' | $T8 %]: [% LxERP.format_amount(assemblytotal, 2) %]</th>
-        </tr>
-      </table>
-    </td>
-  </tr>
-  <input type="hidden" name="assembly_rows" value="[% HTML.escape(assembly_rows) %]" -->
-  [%- END %]
-
-  <input type="hidden" name="callback" value="[% HTML.escape(callback) %]">
-  <input type="hidden" name="previousform" value="[% HTML.escape(previousform) %]">
-  <input type="hidden" name="taxaccount2" value="[% HTML.escape(taxaccount2) %]">
-  <input type="hidden" name="vc" value="[% HTML.escape(vc) %]">
-  <tr>
-    <td><hr size="3" noshade></td>
-  </tr>
- </table>
-</div>
-
-[%- IF LANGUAGES.size %]
- [% PROCESS 'ic/tabs/_edit_translations.html' %]
-[%- END %]
-
-[%- IF id %]
-<div id="sales_price_information">
-  [% PROCESS ic/sales_price_information.html id=id %]
-</div>
-[%- END %]
-
-[%- IF CUSTOM_VARIABLES.size %]
-<div id="custom_variables">
-
- <p>[% 'Unchecked custom variables will not appear in orders and invoices.' | $T8 %]</p>
-
- <p>
-  <table>
-   [%- FOREACH var = CUSTOM_VARIABLES %]
-   <tr>
-    <td align="right" valign="top">[% var.VALID_BOX %]</td>
-    [%- IF !var.partsgroup_filtered %]
-      <td align="right" valign="top">[% HTML.escape(var.description) %]</td>
-    [%- END %]
-    <td valign="top">[% var.HTML_CODE %]</td>
-   </tr>
-   [%- END %]
-  </table>
- </p>
-</div>
-[%- END %]
-
-[%- IF id %]
-<div id='price_rules'>
-  <div id='price_rules_customer_report'></div>
-  <div id='price_rules_vendor_report'></div>
-</div>
-[%- END %]
-
-</div>
-
-[%- IF show_edit_buttons %]
-
-<input class="submit" type="submit" name="action" value="[% 'Update' | $T8 %]">
-<input type="hidden" name="price_rows" value="[% HTML.escape(price_rows) %]">
-<input class="submit" type="submit" name="action" value="[% 'Save' | $T8 %]">
-
-  [%- IF id %]
-    [%- UNLESS previousform %]
-<input class="submit" type="submit" name="action" value="[% 'Save as new' | $T8 %]">
-    [%- END %]
-
-    [%- IF orphaned %]
-      [%- UNLESS previousform %]
-        [%- IF item == 'assembly' %]
-          [%- UNLESS onhand %]
-<input class="submit" type="submit" name="action" value="[% 'Delete' | $T8 %]">
-          [%- END %]
-        [%- ELSE %]
-<input class="submit" type="submit" name="action" value="[% 'Delete' | $T8 %]">
-        [%- END %]
-      [%- END %]
-    [%- END %]
-  [%- END %]
-
-[%- END %]
-
-  [%- IF id != "" %]
-<input type="button" class="submit" onclick="set_history_window([% id %], 'id');" name="history" id="history" value="[% 'history' | $T8 %]">
-  [%- END %]
-
-</form>
diff --git a/templates/webpages/ic/form_header.html b/templates/webpages/ic/form_header.html
deleted file mode 100644 (file)
index d9d635b..0000000
+++ /dev/null
@@ -1,296 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-[%- USE LxERP %][%- USE L -%][%- USE P -%]
-[% PROCESS 'common/select_warehouse_bin.html' %]
- <h1>[% title %]  [% HTML.escape(partnumber) %]  [% HTML.escape(description) %]</h1>
-
-[% PROCESS 'common/flash.html' %]
-
- <form method="post" name="ic" action="[% script %]">
-
-  <input name="id" type="hidden" value="[% HTML.escape(id) %]">
-  <input name="item" type="hidden" value="[% HTML.escape(item) %]">
-  <input name="title" type="hidden" value="[% HTML.escape(title) %]">
-  <input name="makemodel" type="hidden" value="[% HTML.escape(makemodel) %]">
-  <input name="alternate" type="hidden" value="[% HTML.escape(alternate) %]">
-  <input name="onhand" type="hidden" value="[% HTML.escape(onhand) %]">
-  <input name="orphaned" type="hidden" value="[% HTML.escape(orphaned) %]">
-  <input name="taxaccounts" type="hidden" value="[% HTML.escape(taxaccounts) %]">
-  <input name="rowcount" type="hidden" value="[% HTML.escape(rowcount) %]">
-  <input name="eur" type="hidden" value="[% HTML.escape(eur) %]">
-  <input name="original_partnumber" type="hidden" value="[% HTML.escape(original_partnumber) %]">
-  <input name="currow" type="hidden" value="[% HTML.escape(currow) %]">
-
-  <div id="ic_tabs" class="tabwidget">
-   <ul>
-    <li><a href="#master_data">[% 'Basic Data' | $T8 %]</a></li>
-[% IF LANGUAGES.size %]
-    <li><a href="#translations_tab">[% 'Translations' | $T8 %]</a></li>
-[% END %]
-    [%- IF id %]
-    <li><a href="#sales_price_information">[% 'Price information' | $T8 %]</a></li>
-    [%- END %]
-    [%- IF CUSTOM_VARIABLES.size %]
-    <li><a href="#custom_variables">[% 'Custom Variables' | $T8 %]</a></li>
-    [%- END %]
-    [%- IF id  %]
-    <li><a href="#price_rules">[% 'Price Rules' | $T8 %]</a></li>
-    [% END %]
-   </ul>
-
-  <div id="master_data">
-
-   <table width="100%">
-    <tr>
-     <td>
-      <table width="100%">
-       <tr valign="top">
-        <td>
-[%- IF image && INSTANCE_CONF.get_parts_show_image %]
-         <a href="[% image | html %]" target="_blank"><img style="[% INSTANCE_CONF.get_parts_image_css %]" src="[% image | html %]"/></a>
-[%- END %]
-
-         <table>
-          <tr>
-           <td colspan="2">
-            <table>
-             <tr>
-              <th align="right">[% 'Part Number' | $T8 %]</th>
-              <td><input id='partnumber' name="partnumber" value="[% HTML.escape(partnumber) %]" size="40" class="initial_focus"></td>
-             </tr>
-             <tr>
-              <th align="right">[% 'Part Description' | $T8 %]</th>
-              <td>
-               [%- IF description_area %]
-               <textarea name="description" rows="[% HTML.escape(rows) %]" cols="40" wrap="soft">[% HTML.escape(description) %]</textarea>
-               [%- ELSE %]
-               <input name="description" size="40" value="[% HTML.escape(description) %]">
-               [%- END %]
-              </td>
-             </tr>
-             <tr>
-               <th align="right">[% 'EAN-Code' | $T8 %]</th>
-               <td><input name="ean" size="40" value="[% HTML.escape(ean) %]"></td>
-             </tr>
-             <tr>
-              [%- IF all_partsgroup.size %]
-              <th align="right">[% 'Group' | $T8 %]</th>
-              <td>
-               [%- INCLUDE generic/multibox.html
-                     name       = 'partsgroup',
-                     DATA       = all_partsgroup,
-                     show_empty = 1,
-                     id_sub     = 'pg_keys',
-                     label_key  = 'partsgroup',
-                     style      = 'width:250px'
-               -%]
-              </td>
-              <input type="hidden" name="oldpartsgroup" value="[% HTML.escape(oldpartsgroup) %]">
-              [% END %]
-             </tr>
-
-
-             [%- IF BUCHUNGSGRUPPEN.size %]
-             <tr>
-              <th align="right">[% 'Buchungsgruppe' | $T8 %]</th>
-              <td>
-               [%- INCLUDE generic/multibox.html
-                     name       = 'buchungsgruppen_id',
-                     DATA       = BUCHUNGSGRUPPEN,
-                     id_key     = 'id',
-                     label_key  = 'description',
-                     style      = 'width:250px'
-               -%]
-             </tr>
-             [%- END %]
-             <input type="hidden" name="IC_income" value="[% HTML.escape(IC_income_default) %]">
-             [%- UNLESS is_assembly %]
-             <input type="hidden" name="IC_expense" value="[% HTML.escape(IC_expense_default) %]">
-             [%- END %]
-             [%- IF is_part %]
-             <input type="hidden" name="IC" value="[% HTML.escape(IC_default) %]">
-             [%- END %]
-             <tr>
-              <th align="right">[% 'Payment Terms' | $T8 %]</th>
-              <td>
-               [%- INCLUDE generic/multibox.html
-                     name       = 'payment_id',
-                     DATA       = payment_terms,
-                     show_empty = 1,
-                     id_key     = 'id',
-                     label_key  = 'description',
-                     style      = 'width:250px'
-               -%]
-              </td>
-             </tr>
-            </table>
-           </td>
-          </tr>
-
-          <tr height="5"></tr>
-
-          <tr>
-           <td>
-            <table>
-             <tr>
-              <th align="left">[% 'Part Notes' | $T8 %]</th>
-              <th align="left">[% 'Formula' | $T8 %]</th>
-             </tr>
-             <tr valign="top">
-              <td>
-               [% L.textarea_tag("notes", P.restricted_html(notes), class="texteditor", style="width: 600px; height: 200px") %]
-              </td>
-              <td>
-                 <textarea name="formel" rows="[% HTML.escape(notes_rows) %]" cols="30" wrap="soft" class="tooltipster-html" title="[% 'The formula needs the following syntax:<br>For regular article:<br>Variablename= Variable Unit;<br>Variablename2= Variable2 Unit2;<br>...<br>###<br>Variable + ( Variable2 / Variable )<br><b>Please be beware of the spaces in the formula</b><br>' | $T8 %]">[% HTML.escape(formel) %]</textarea>
-               </td>
-             </tr>
-            </table>
-           </td>
-          </tr>
-         </table>
-        </td>
-
-        <td>
-         <table>
-          <tr>
-           <th align="right" nowrap="true">[% 'Updated' | $T8 %]</th>
-           <td>
-            <input name="priceupdate" id="priceupdate" size="11"  title="[% HTML.escape(dateformat) %]" value="[% HTML.escape(priceupdate) %]" readonly>
-           </td>
-          </tr>
-
-          <tr>
-           <th align="right" nowrap="true">[% 'List Price' | $T8 %]</th>
-           <td><input name="listprice" size="11" value="[% LxERP.format_amount(listprice, 2) %]"></td>
-          </tr>
-
-          <tr>
-           <th align="right" nowrap="true">[% 'Sell Price' | $T8 %]</th>
-           <td><input name="sellprice" size="11" value="[% LxERP.format_amount(sellprice, 2) %]"></td>
-          </tr>
-
-          [%- UNLESS is_assembly %]
-          <tr>
-           <th align="right" nowrap="true">[% 'Last Cost' | $T8 %]</th>
-           <td><input name="lastcost" size="11" value="[% LxERP.format_amount(lastcost, 2) %]"></td>
-          </tr>
-          [%- END %]
-
-          [%- IF ALL_PRICE_FACTORS.size %]
-          <tr>
-           <th align="right">[% 'Price Factor' | $T8 %]</th>
-           <td>
-            [%- INCLUDE generic/multibox.html
-                  name       = 'price_factor_id',
-                  DATA       = ALL_PRICE_FACTORS,
-                  show_empty = 1,
-                  id_key     = 'id',
-                  label_key  = 'description',
-                  style      = 'width:100px'
-            -%]
-           </td>
-          </tr>
-          [%- END %]
-
-          <tr>
-           <th align="right" nowrap="true">[% 'Unit' | $T8 %]</th>
-           <td>
-            <input type="hidden" name="unit_changeable" value="[% HTML.escape(unit_changeable) %]">
-            [%- UNLESS unit_changeable %]
-            <input type="hidden" name="unit" value="[% HTML.escape(unit) %]">[% HTML.escape(unit) %]
-            [%- ELSE %]
-            [%- INCLUDE generic/multibox.html
-                  name       = 'unit',
-                  DATA       = ALL_UNITS,
-                  id_key     = 'name',
-                  label_key  = 'name',
-                  style      = 'width:100px'
-            -%]
-            [%- END %]
-           </td>
-          </tr>
-
-        [%- UNLESS is_service %]
-          <tr>
-           <th align="right" nowrap="true">[% 'Weight' | $T8 %]</th>
-           <td>
-            [%- IF is_assembly %]&nbsp;[% LxERP.format_amount(weight) %][%- END %]
-            <input[% IF is_assembly %] type="hidden"[% END %] size="10" name="weight" value="[% LxERP.format_amount(weight) %]">
-            [% HTML.escape(defaults.weightunit) %]
-           </td>
-          </tr>
-        [%- END %]
-          <tr>
-           <th align="right" nowrap>[% 'On Hand' | $T8 %]</th>
-           <th align="left" nowrap class="plus[% IF onhand > 0 %]1[% ELSE %]0[% END %]">&nbsp;[% LxERP.format_amount(onhand) %]</th>
-          </tr>
-          <tr>
-           <th align="right" nowrap="true">[% 'ROP' | $T8 %]</th>
-           <td><input name="rop" size="10" value="[% LxERP.format_amount(rop) %]"></td>
-          </tr>
-          <tr>
-           <th align="right" nowrap="true">[% 'Default Warehouse' | $T8 %]</th>
-           <td>
-            <select name="warehouse_id" onchange="warehouse_selected(warehouses[this.selectedIndex]['id'], 0)">
-             [%- FOREACH warehouse = WAREHOUSES %]
-               <option value="[% HTML.escape(warehouse.id) %]"[% IF warehouse_id == warehouse.id %] selected[% END %]>[% warehouse.description %]</option>
-             [%- END %]
-            </select>
-          </td>
-          </tr>
-          <tr>
-           <th align="right" nowrap="true">[% 'Default Bin' | $T8 %]</th>
-           <td><select id="bin_id" name="bin_id"></select></td>
-          </tr>
-          <tr>
-           <th align="right" nowrap="true">[% 'Verrechnungseinheit' | $T8 %]</th>
-           <td><input name="ve" size="10" value="[% HTML.escape(ve) %]"></td>
-          </tr>
-          <tr>
-           <th align="right" nowrap="true">[% 'Business Volume' | $T8 %]</th>
-           <td><input name="gv" size="10" value="[% LxERP.format_amount(gv) %]"></td>
-          </tr>
-          <tr>
-           <th align="right" nowrap><label for="not_discountable">[% 'Not Discountable' | $T8 %]</label></th>
-           <td><input class="checkbox" type="checkbox" name="not_discountable" id="not_discountable" value="1" [% IF not_discountable %]checked[% END %]></td>
-          </tr>
-        [%- IF id %]
-          <tr>
-           <th align="right" nowrap="true"><label for="obsolete">[% 'Obsolete' | $T8 %]</label></th>
-           <td><input name="obsolete" id="obsolete" type="checkbox" class="checkbox" value="1" [% IF obsolete %]checked[% END %]></td>
-          </tr>
-        [%- END %]
-        [%- UNLESS is_service %]
-          <tr>
-           <th align="right" nowrap><label for="has_sernumber">[% 'Has serial number' | $T8 %]</label></th>
-           <td><input class="checkbox" type="checkbox" name="has_sernumber" id="has_sernumber" value="1" [% IF has_sernumber %]checked[% END %]></td>
-          </tr>
-        [%- END %]
-          <tr>
-           <th align="right" nowrap><label for="shop">[% 'Shopartikel' | $T8 %]</label></th>
-           <td><input class="checkbox" type="checkbox" name="shop" id="shop" value="1" [% IF shop %]checked[% END %]></td>
-          </tr>
-         </table>
-        </td>
-       </tr>
-      </table>
-     </td>
-    </tr>
-
-
-    <tr>
-     <td>
-      <table>
-       <tr>
-        <th align="right" nowrap>[% 'Image' | $T8 %]</th>
-        <td><input name="image" size="40" value="[% HTML.escape(image) %]"></td>
-        <th align="right" nowrap>[% 'Microfiche' | $T8 %]</th>
-        <td><input name="microfiche" size="20" value="[% HTML.escape(microfiche) %]"></td>
-       </tr>
-       <tr>
-        <th align="right" nowrap>[% 'Drawing' | $T8 %]</th>
-        <td><input name="drawing" size="40" value="[% HTML.escape(drawing) %]"></td>
-       </tr>
-      </table>
-     </td>
-    </tr>
index 499cdcb..9dc3ea8 100644 (file)
@@ -1,17 +1,54 @@
 [%- USE T8 %]
-[% USE HTML %]<form method="post" action="ic.pl">
+[% USE HTML %]
+<h4>[%- 'Abbreviation Legend' | $T8  %]</h4>
+<table valign="top">
+ <tr valign="top"><td>
+ <table valign="top">
+  <thead>
+    <tr class="listheading">
+     <th>[%- 'TypAbbreviation' | $T8  %]</th>
+     <th>[%- 'Description'     | $T8  %]</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr class="listrow0">
+     <td>[%- 'Part (typeabbreviation)'       | $T8 %]</td>
+     <td>[%- 'Part'                          | $T8 %]</td>
+    </tr>
+    <tr class="listrow1">
+     <td>[%- 'Assembly (typeabbreviation)'   | $T8 %]</td>
+     <td>[%- 'Assembly'                      | $T8 %]</td>
+    </tr>
+    <tr class="listrow0">
+     <td>[%- 'Service (typeabbreviation)'    | $T8 %]</td>
+     <td>[%- 'Service'                       | $T8 %]</td>
+    </tr>
+    [%- IF INSTANCE_CONF.get_feature_experimental_assortment %]
+    <tr class="listrow1">
+     <td>[%- 'Assortment (typeabbreviation)' | $T8 %]</td>
+     <td>[%- 'Assortment'                    | $T8 %]</td>
+    </tr>
+    [%- END %]
+ </tbody>
+ </table></td>
+ <td><table valign="top">
+  <thead>
+    <tr  valign="top" class="listheading">
+     <th>[%- 'PartClassAbbreviation' | $T8  %]</th>
+     <th>[%- 'Description'           | $T8  %]</th>
+    </tr>
+  </thead>
+  <tbody>
+  [%- FOREACH part_classification = PART_CLASSIFICATIONS %]
+    <tr class="listrow[% loop.count % 2 %]">
+     <td>[%- part_classification.abbreviation | $T8 %]</td>
+     <td>[%- part_classification.description  | $T8 %]</td>
+    </tr>
+  [%- END %]
+  </tbody>
+ </table></td></tr>
+</table>
 
+<form method="post" action="controller.pl" id="new_form">
  <input name="callback" type="hidden" value="[% HTML.escape(callback) %]">
-
- <input type="hidden" name="item" value="[% HTML.escape(searchitems) %]">
- <input type="hidden" name="action" value="add">
-
- [% SWITCH searchitems %]
-   [% CASE 'part' %][% 'New part' | $T8 %]
-   [% CASE 'service' %][% 'New service' | $T8 %]
-   [% CASE 'assembly' %][% 'New assembly' | $T8 %]
- [% END %]
- <br>
- <input class="submit" type="submit" value="[%- 'Add' | $T8 %]">
-
 </form>
index 3e757a5..212bb3f 100644 (file)
@@ -1,6 +1,5 @@
 [%- USE HTML %]
 [%- USE T8 %]
 [%- USE L %]
-[%- PROCESS 'common/flash.html' %]
 
 [% 'Options' | $T8 %]: [% options.join(', ') %]
diff --git a/templates/webpages/ic/makemodel.html b/templates/webpages/ic/makemodel.html
deleted file mode 100644 (file)
index 4daad81..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-[%- USE LxERP %]
-  <tr>
-    <td>
-      <table>
-        <tr>
-          <th class="listheading">[% 'Make' | $T8 %]</th>
-          <th class="listheading">[% 'Model' | $T8 %]</th>
-          <th class="listheading">[% 'Last Cost' | $T8 %]</th>
-          <th class="listheading">[% 'Updated' | $T8 %]</th>
-          <th class="listheading">[% 'order' | $T8 %]</th>
-        </tr>
-      [%- FOREACH row = MM_DATA %]
-        <tr>
-          <td>
-            [%- INCLUDE generic/multibox.html
-                  name       = "make_$loop.count",
-                  default    = row.make,
-                  DATA       = ALL_VENDORS,
-                  show_empty = 1,
-                  label_key  = 'name',
-                  id_key     = 'id',
-            -%]
-          </td>
-          <td><input name="model_[% loop.count %]" size="30" value="[% HTML.escape(row.model) %]"></td>
-          <td><input type="hidden" name="old_lastcost_[% loop.count %]" value="[% LxERP.format_amount(row.lastcost, 2) %]">
-              <input name="lastcost_[% loop.count %]" size="10" value="[% LxERP.format_amount(row.lastcost, 2) %]"></td>
-          <td><input name="lastupdate_[% loop.count %]" size="10" value="[% HTML.escape(row.lastupdate) %]"></td>
-          <td><input name="sortorder_[% loop.count %]" size="3" value="[% HTML.escape(row.sortorder) %]"></td>
-        </tr>
-      [%- END %]
-      </table>
-    </td>
-  </tr>
-  <input type="hidden" name="makemodel_rows" value="[% mm_rows %]">
diff --git a/templates/webpages/ic/price_row.html b/templates/webpages/ic/price_row.html
deleted file mode 100644 (file)
index b89b102..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-[%- USE LxERP %]
-[%- IF PRICES.size %]
-  <tr>
-    <td>
-      <table width=100%>
-        <tr>
-          <th class="listheading">[% 'Preisklasse' | $T8 %]</th>
-          <th class="listheading">[% 'Preis' | $T8 %]</th>
-        </tr>
-[%- FOREACH row = PRICES %]
-        <tr class="listrow[% loop.count % 2 %]">
-          <td width=50%><input type=hidden name="pricegroup_[% loop.count %]" size=30  value="[% HTML.escape(row.pricegroup) %]">[% HTML.escape(row.pricegroup) %]</td>
-          <td width=50%><input name="price_[% loop.count %]" size=11 value="[% LxERP.format_amount(row.price, 2) %]"></td>
-          <input type="hidden" name="pricegroup_id_[% loop.count %]" value="[% HTML.escape(row.pricegroup_id) %]">
-        </tr>
-[%- END %]
-      </table>
-    </td>
-  </tr>
-[%- END %]
diff --git a/templates/webpages/ic/sales_price_information.html b/templates/webpages/ic/sales_price_information.html
deleted file mode 100644 (file)
index 4f5794c..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-<div id='sales_price_information_sales_order'></div>
-<div id='sales_price_information_sales_quotation'></div>
-
-<script type='text/javascript'>
-  function get_report(target, source, data){
-    $.ajax({
-      url:        source,
-//      beforeSend: function () { $(target).html('<img src="image/spinner.gif">') },
-      success:    function (rsp) {
-        $(target).html(rsp);
-        $(target).find('.paginate').find('a').click(function(event){ redirect_event(event, target) });
-        $(target).find('a.report-generator-header-link').click(function(event){ redirect_event(event, target) });
-      },
-      data:       data,
-    });
-  };
-
-  function redirect_event(event, target){
-    event.preventDefault();
-    get_report(target, event.target + '', {});
-  }
-
-  $('.tabwidget').on('tabsbeforeactivate', function(event, ui){
-    if (ui.newPanel.attr('id') == 'sales_price_information') {
-      get_report('#sales_price_information_sales_order', 'controller.pl', { action: 'SellPriceInformation/list', 'filter.part.id': [% id %], 'filter.order.type': 'sales_order' });
-      get_report('#sales_price_information_sales_quotation', 'controller.pl', { action: 'SellPriceInformation/list', 'filter.part.id': [% id %], 'filter.order.type': 'sales_quotation' });
-    }
-    return 1;
-  });
-
-
-</script>
index 8e71cb6..91f665c 100644 (file)
@@ -2,9 +2,12 @@
 [%- USE HTML %]
 [%- USE LxERP %]
 [%- USE L %]
+[%- USE P %]
+[% SET style="width: 250px" %]
+[%- INCLUDE 'common/flash.html' %]
 <h1>[% title %]</h1>
 
- <form method="post" action="ic.pl">
+ <form method="post" action="ic.pl" id="form">
 
   <input type="hidden" name="searchitems" value="[% HTML.escape(searchitems) %]">
   <input type="hidden" name="title" value="[% HTML.escape(title) %]">
@@ -12,7 +15,6 @@
   <input type="hidden" name="revers" value="[% HTML.escape(revers) %]">
   <input type="hidden" name="lastsort" value="[% HTML.escape(lastsort) %]">
 
-  <input type="hidden" name="nextsub" value="generate_report">
   <input type="hidden" name="sort" value="description">
 
   <input type="hidden" name="ndxs_counter" value="[% HTML.escape(ndxs_counter) %]">
    <tr valign="top">
     <td>
      <table>
+      <tr>
+       <th align="right" nowrap>[% 'Part Type' | $T8 %]</th>
+       <td  colspan="4" ><table><tr>
+        <td>
+          <input name="l_part" id="l_part" class="checkbox" type="checkbox" value="Y" checked>
+          <label for="l_part">[% 'Part' | $T8 %]</label>
+        </td>
+        <td>
+          <input name="l_service" id="l_service" class="checkbox" type="checkbox" value="Y" checked>
+          <label for="l_service">[% 'Service' | $T8 %]</label>
+        </td>
+        <td>
+          <input name="l_assembly" id="l_assembly" class="checkbox" type="checkbox" value="Y" checked>
+          <label for="l_assembly">[% 'Assembly' | $T8 %]</label>
+        </td>
+        [%- IF INSTANCE_CONF.get_feature_experimental_assortment %]
+        <td>
+          <input name="l_assortment" id="l_assortment" class="checkbox" type="checkbox" value="Y" checked>
+          <label for="l_assortment">[% 'Assortment' | $T8 %]</label>
+        </td>
+        [%- END %]
+       </tr></table></td>
+      </tr>
       <tr>
        <th align="right" nowrap>[% 'Part Number' | $T8 %]</th>
-       <td><input name="partnumber" size="20"></td>
+       <td>[% L.input_tag("partnumber", "", style=style) %]</td>
        <th align="right" nowrap>[% 'EAN' | $T8 %]</th>
-       <td><input name="ean" size="20"></td>
+       <td>[% L.input_tag("ean", "", style=style) %]</td>
+      </tr>
+      <tr>
+       <th align="right" nowrap>[% 'Part Classification' | $T8 %]:</th>
+       <td>[% P.part.select_classification('classification_id', style=style) %]</td>
       </tr>
-
       <tr>
        <th align="right" nowrap>[% 'Part Description' | $T8 %]</th>
-       <td colspan="3"><input name="description" size="40" class="initial_focus"></td>
+       <td colspan="3">[% L.input_tag("description", "", style=style, class="initial_focus") %]</td>
       </tr>
-
       <tr>
-       <th align="right" nowrap>[% 'Group' | $T8 %]</th>
-       <td>
-         [%- INCLUDE generic/multibox.html
-           name          = 'partsgroup',
-           select_name   = 'partsgroup_id',
-           DATA          = ALL_PARTSGROUPS,
-           show_empty    = 1,
-           id_key        = 'id',
-           label_key     = 'partsgroup',
-           style         = 'width:250px',
-           limit         = limit,
-           allow_textbox = 1
-         -%]
-       </td>
-       <th align="right" nowrap>[% 'Serial Number' | $T8 %]</th> <td><input name="serialnumber" size="20"></td>
+       <th align="right" nowrap>[% 'Partsgroup' | $T8 %]</th>
+       <td>[% P.select_tag("partsgroup_id", ALL_PARTSGROUPS, with_empty=1, default=partsgroup, title_key="partsgroup", style=style) %]</td>
+       <th align="right" nowrap>[% 'Serial Number' | $T8 %]</th>
+       <td>[% L.input_tag("serialnumber", "", style=style) %]</td>
       </tr>
 
-      [%- UNLESS is_service %]
       <tr>
-       <th align="right" nowrap>[% 'Make' | $T8 %]</th> <td><input name="make" size="20"></td>
-       <th align="right" nowrap>[% 'Model' | $T8 %]</th> <td><input name="model" size="20"></td>
+       <th align="right" nowrap>[% 'Make' | $T8 %]</th>
+       <td>[% L.input_tag("make", "", style=style) %]</td>
+       <th align="right" nowrap>[% 'Model' | $T8 %]</th>
+       <td>[% L.input_tag("model", "", style=style) %]</td>
       </tr>
-      [%- END %]
 
       <tr>
        <th align="right" nowrap>[% 'Drawing' | $T8 %]</th>
-       <td><input name="drawing" size="20"></td>
+       <td>[% L.input_tag("drawing", "", style=style) %]</td>
        <th align="right" nowrap>[% 'Microfiche' | $T8 %]</th>
-       <td><input name="microfiche" size="20"></td>
+       <td>[% L.input_tag("microfiche", "", style=style) %]</td>
       </tr>
 
       <tr>
-       <th align="right" nowrap>[% 'Shopartikel' | $T8 %]</th>
-       <td>[% L.yes_no_tag('shop', shop, default='', with_empty=1, empty_title='---') %]</td>
+       <th align="right" nowrap>[% 'Shop article' | $T8 %]</th>
+       <td>[% L.yes_no_tag('shop', shop, default='', with_empty=1, empty_title='---', style=style) %]</td>
       </tr>
 
       <tr>
        <th align="right">[% 'Insert Date' | $T8 %]</th>
        <td>
-        [% 'From' | $T8 %][% L.date_tag('insertdatefrom') %]
-        [% 'Bis' | $T8 %] [% L.date_tag('insertdateto') %]
+        [% L.date_tag('insertdatefrom') %]
+        [% 'Bis' | $T8 %]
+        [% L.date_tag('insertdateto') %]
        </td>
       </tr>
 
       [% CUSTOM_VARIABLES_FILTER_CODE %]
 
-      [%- IF is_assembly %]
       <tr>
        <td></td>
        <td colspan="3">
         [% L.radio_button_tag('bom', id='bom_0', value=0, checked=1, label=LxERP.t8('Top Level Designation only')) %]
         [% L.radio_button_tag('bom', id='bom_1', value=1,            label=LxERP.t8('Individual Items')) %]
+        [% L.radio_button_tag('bom', id='bom_2', value=2,            label=LxERP.t8('Search for Items used in Assemblies')) %]
        </td>
       </tr>
-      [%- END %]
 
       <tr>
        <td></td>
        <td colspan="3">
         [%- L.radio_button_tag('itemstatus', value='active', id='itemstatus_active', label=LxERP.t8('Active'), checked=1) %]
-      [%- UNLESS is_service %]
         [%- L.radio_button_tag('itemstatus', value='onhand', id='itemstatus_onhand', label=LxERP.t8('On Hand')) %]
         [%- L.radio_button_tag('itemstatus', value='short', id='itemstatus_short', label=LxERP.t8('Short')) %]
         [%- L.radio_button_tag('itemstatus', value='obsolete', id='itemstatus_obsolete', label=LxERP.t8('Obsolete')) %]
-      [%- END %]
         [%- L.radio_button_tag('itemstatus', value='orphaned', id='itemstatus_orphaned', label=LxERP.t8('Orphaned')) %]
         [%- L.radio_button_tag('itemstatus', value='', id='itemstatus_all', label=LxERP.t8('All')) %]
        </td>
           <td>
            <table>
             <tr>
-             [%- UNLESS is_assembly %]
              <td>[%- L.checkbox_tag('bought', label=LxERP.t8('Bought')) %]</td>
-             [%- END %]
              <td>[%- L.checkbox_tag('sold', label=LxERP.t8('Sold')) %]</td>
             </tr>
 
             </tr>
 
             <tr>
-             [%- UNLESS is_assembly %]
              <td>[%- L.checkbox_tag('onorder', label=LxERP.t8('On Order')) %]</td>
-             [%- END %]
              <td>[%- L.checkbox_tag('ordered', label=LxERP.t8('Ordered')) %]</td>
             </tr>
 
             </tr>
 
             <tr>
-             [%- UNLESS is_assembly %]
              <td>[%- L.checkbox_tag('rfq', label=LxERP.t8('RFQ')) %]</td>
-             [%- END %]
              <td>[%- L.checkbox_tag('quoted', label=LxERP.t8('Quoted')) %]</td>
             </tr>
            </table>
          <tr>
           <td>[%- L.checkbox_tag('l_partnumber', label=LxERP.t8('Part Number'), checked=1, value='Y') %]</td>
           <td>[%- L.checkbox_tag('l_description', label=LxERP.t8('Part Description'), checked=1, value='Y') %]</td>
-      [%- UNLESS is_service %]
           <td>[%- L.checkbox_tag('l_serialnumber', label=LxERP.t8('Serial Number'), value='Y') %]</td>
-      [%- END %]
           <td>[%- L.checkbox_tag('l_unit', label=LxERP.t8('Unit of measure'), value='Y', checked=1) %]</td>
          </tr>
 
          </tr>
 
          <tr>
-          <td>[%- L.checkbox_tag('l_priceupdate', label=LxERP.t8('Updated'), value='Y') %]</td>
+          <td>[%- L.checkbox_tag('l_priceupdate', label=LxERP.t8('Price updated'), value='Y') %]</td>
           <td>[%- L.checkbox_tag('l_deliverydate', label=LxERP.t8('Delivery Date'), value='Y') %]</td>
-      [%- UNLESS is_service %]
           <td>[%- L.checkbox_tag('l_rop', label=LxERP.t8('ROP'), value='Y') %]</td>
           <td>[%- L.checkbox_tag('l_weight', label=LxERP.t8('Weight'), value='Y') %]</td>
-      [%- END %]
          </tr>
 
          <tr>
           <td>[%- L.checkbox_tag('l_image', label=LxERP.t8('Image'), value='Y', checked=(INSTANCE_CONF.get_parts_listing_image ? 1 : 0)) %]</td>
           <td>[%- L.checkbox_tag('l_drawing', label=LxERP.t8('Drawing'), value='Y') %]</td>
           <td>[%- L.checkbox_tag('l_microfiche', label=LxERP.t8('Microfiche'), value='Y') %]</td>
-          <td>[%- L.checkbox_tag('l_partsgroup', label=LxERP.t8('Group'), value='Y') %]</td>
+          <td>[%- L.checkbox_tag('l_partsgroup', label=LxERP.t8('Partsgroup'), value='Y') %]</td>
           </td>
          </tr>
 
          <tr>
-          <td>[%- L.checkbox_tag('l_transdate', label=LxERP.t8('Transdate'), value='Y') %]</td>
-          <td>[%- L.checkbox_tag('l_subtotal', label=LxERP.t8('Subtotal'), value='Y') %]</td>
-          <td>[%- L.checkbox_tag('l_soldtotal', label=LxERP.t8('Qty in Selected Records'), value='Y') %]</td>
+          <td>[%- L.checkbox_tag('l_insertdate', label=LxERP.t8('Insert Date'), value='Y') %]</td>
+          <td>[%- L.checkbox_tag('l_notes', label=LxERP.t8('Notes'), value='Y') %]</td>
           <td>[%- L.checkbox_tag('l_ean', label=LxERP.t8('EAN'), value='Y') %]</td>
+          <td>[%- L.checkbox_tag('l_shop', label=LxERP.t8('Shop article'), value='Y') %]</td>
          </tr>
 
          <tr>
-      [%- UNLESS is_service %]
           <td>[%- L.checkbox_tag('l_onhand', label=LxERP.t8('Stocked Qty'), value='Y') %]</td>
-      [%- END %]
           <td>[%- L.checkbox_tag('l_projectnumber', label=LxERP.t8('Project Number'), value='Y') %]</td>
           <td>[%- L.checkbox_tag('l_projectdescription', label=LxERP.t8('Project Description'), value='Y') %]</td>
           <td>[%- L.checkbox_tag('l_pricegroups', label=LxERP.t8('Pricegroups'), value='Y', checked=1) %]</td>
          </tr>
 
          <tr>
-          <td>[%- L.checkbox_tag('l_notes', label=LxERP.t8('Notes'), value='Y') %]</td>
+          <td>[%- L.checkbox_tag('l_subtotal', label=LxERP.t8('Subtotal'), value='Y') %]</td>
+          <td>[%- L.checkbox_tag('l_transdate', label=LxERP.t8('Transdate Record'), value='Y') %]</td>
           <td>[%- L.checkbox_tag('l_name', label=LxERP.t8('Name in Selected Records'), value='Y') %]</td>
-          <td>[%- L.checkbox_tag('l_shop', label=LxERP.t8('Shopartikel'), value='Y') %]</td>
-          <td>[%- L.checkbox_tag('l_insertdate', label=LxERP.t8('Insert Date'), value='Y') %]</td>
+          <td>[%- L.checkbox_tag('l_soldtotal', label=LxERP.t8('Qty in Selected Records'), value='Y') %]</td>
+         </tr>
+         <tr>
+          <td>[%- L.checkbox_tag('l_warehouse', label=LxERP.t8('Default Warehouse'), value='Y') %]</td>
+          <td>[%- L.checkbox_tag('l_bin', label=LxERP.t8('Default Bin'), value='Y') %]</td>
+          <td>[%- L.checkbox_tag('l_make', label=LxERP.t8('Make'), value='Y') %]</td>
+          <td>[%- L.checkbox_tag('l_model', label=LxERP.t8('Model'), value='Y') %]</td>
          </tr>
 
          [% CUSTOM_VARIABLES_INCLUSION_CODE %]
    </tr>
    <tr><td colspan="4"><hr size="3" noshade></td></tr>
   </table>
-
-  <p>
-   <input class="submit" type="submit" name="action" value="[% 'Continue' | $T8 %]">
-   <input class="submit" type="submit" name="action" value="[% 'TOP100' | $T8 %]">
-  </p>
  </form>
index bca42df..ba54126 100644 (file)
 [%- USE T8 %]
 [%- USE HTML %]
 [%- USE LxERP %]
+[%- USE L %]
 <h1>[% 'Update prices' | $T8 %]</h1>
 
- <form method="post" action="ic.pl">
-
-  <input type="hidden" name="title" value="[% 'Update prices' | $T8 %]">
-
-  <p>
-   <table>
-    <tr>
-     <th align="right" nowrap>[% 'Part Number' | $T8 %]</th>
-     <td><input name="partnumber" size="20"></td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Part Description' | $T8 %]</th>
-     <td colspan="3"><input name="description" size="20"></td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Group' | $T8 %]</th>
-     <td><input name="partsgroup" size="20"></td>
-     <th align="right" nowrap>[% 'Serial Number' | $T8 %]</th>
-     <td><input name="serialnumber" size="20"></td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Make' | $T8 %]</th>
-     <td><input name="make" size="20"></td>
-     <th align="right" nowrap>[% 'Model' | $T8 %]</th>
-     <td><input name="model" size="20"></td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Drawing' | $T8 %]</th>
-     <td><input name="drawing" size="20"></td>
-     <th align="right" nowrap>[% 'Microfiche' | $T8 %]</th>
-     <td><input name="microfiche" size="20"></td>
-    </tr>
-
-    <tr>
-     <td></td>
-     <td colspan="3">
-      <input name="itemstatus" id="itemstatus_active" class="radio" type="radio" value="active" checked>
-      <label for="itemstatus_active">[% 'Active' | $T8 %]</label>
-      <input name="itemstatus" id="itemstatus_onhand" class="radio" type="radio" value="onhand">
-      <label for="itemstatus_onhand">[% 'On Hand' | $T8 %]</label>
-      <input name="itemstatus" id="itemstatus_short" class="radio" type="radio" value="short">
-      <label for="itemstatus_short">[% 'Short' | $T8 %]</label>
-      <input name="itemstatus" id="itemstatus_obsolete" class="radio" type="radio" value="obsolete">
-      <label for="itemstatus_obsolete">[% 'Obsolete' | $T8 %]</label>
-      <input name="itemstatus" id="itemstatus_orphaned" class="radio" type="radio" value="orphaned">
-      <label for="itemstatus_orphaned">[% 'Orphaned' | $T8 %]</label>
-     </td>
-    </tr>
-   </table>
-  </p>
-
-  <hr size="1" noshade>
-
-  <p>
-   <table>
-    <tr>
-     <th class="listheading">[% 'Preisklasse' | $T8 %]</th>
-     <th class="listheading">[% 'Preis' | $T8 %]</th>
-     <th class="listheading">[% 'Prozentual/Absolut' | $T8 %]</th>
-    </tr>
-
-    <tr>
-     <td>[% 'Sell Price' | $T8 %]</td>
-     <td><input name="sellprice" size="11" value="[% HTML.escape(sellprice) %]"></td>
-     <td align="center">
-      <input name="sellprice_type" class="radio" type="radio" value="percent" checked> /
-      <input name="sellprice_type" class="radio" type="radio" value="absolut">
-     </td>
-    </tr>
-
-    <tr>
-     <td>[% 'List Price' | $T8 %]</td>
-     <td><input name="listprice" size="11" value="[% HTML.escape(listprice) %]"></td>
-     <td align="center">
-      <input name="listprice_type" class="radio" type="radio" value="percent" checked> /
-      <input name="listprice_type" class="radio" type="radio" value="absolut">
-     </td>
-    </tr>
-
-    [%- FOREACH row = PRICE_ROWS %]
-    <input type="hidden" name="pricegroup_id_[% loop.count %]" value="[% HTML.escape(row.id) %]">
-
-    <tr>
-     <td><input type="hidden" name="pricegroup_[% loop.count %]" size="30"  value="[% HTML.escape(row.pricegroup) %]">[% HTML.escape(row.pricegroup) %]</td>
-     <td><input name="price_[% loop.count %]" size="11"></td>
-     <td align="center">
-      <input name="pricegroup_type_[% loop.count %]" class="radio" type="radio" value="percent" checked> /
-      <input name="pricegroup_type_[% loop.count %]" class="radio" type="radio" value="absolut">
-     </td>
-    </tr>
-    [%- END %]
-
-   </table>
-  </p>
-
-  <hr size="3" noshade>
-
-  <input type="hidden" name="nextsub" value="confirm_price_update">
-  <input type="hidden" name="price_rows" value="[% HTML.escape(price_rows) %]">
-
-  <p>
-   <input class="submit" type="submit" name="action" value="[% 'Continue' | $T8 %]">
-  </p>
- </form>
-
+[% INCLUDE 'common/flash.html' %]
+
+<form method="post" action="controller.pl" id="form">
+ <table>
+  <tr>
+   <th align="right" nowrap>[% 'Part Number' | $T8 %]</th>
+   <td>[% L.input_tag('filter.partnumber', FORM.filter.partnumber, size=20) %]</td>
+  </tr>
+
+  <tr>
+   <th align="right" nowrap>[% 'Part Description' | $T8 %]</th>
+   <td colspan="3">[% L.input_tag('filter.description', FORM.filter.description, size=20) %]</td>
+  </tr>
+
+  <tr>
+   <th align="right" nowrap>[% 'Partsgroup' | $T8 %]</th>
+   <td>[% L.input_tag('filter.partsgroup', FORM.filter.partsgroup, size=20) %]</td>
+   <th align="right" nowrap>[% 'Serial Number' | $T8 %]</th>
+   <td>[% L.input_tag('filter.serialnumber', FORM.filter.serialnumber, size=20) %]</td>
+  </tr>
+
+  <tr>
+   <th align="right" nowrap>[% 'Make' | $T8 %]</th>
+   <td>[% L.input_tag('filter.make', FORM.filter.make, size=20) %]</td>
+   <th align="right" nowrap>[% 'Model' | $T8 %]</th>
+   <td>[% L.input_tag('filter.model', FORM.filter.model, size=20) %]</td>
+  </tr>
+
+  <tr>
+   <th align="right" nowrap>[% 'Drawing' | $T8 %]</th>
+   <td>[% L.input_tag('filter.drawing', FORM.filter.drawing, size=20) %]</td>
+   <th align="right" nowrap>[% 'Microfiche' | $T8 %]</th>
+   <td>[% L.input_tag('filter.microfiche', FORM.filter.microfiche, size=20) %]</td>
+  </tr>
+
+  <tr>
+   <td></td>
+   <td colspan="3">
+    [% L.radio_button_tag('filter.itemstatus', value='active',   label=LxERP.t8('Active'),   checked=!FORM.filter.itemstatus||FORM.filter.itemstatus=='active') %]
+    [% L.radio_button_tag('filter.itemstatus', value='onhand',   label=LxERP.t8('On Hand'),  checked=FORM.filter.itemstatus=='onhand') %]
+    [% L.radio_button_tag('filter.itemstatus', value='short',    label=LxERP.t8('Short'),    checked=FORM.filter.itemstatus=='short') %]
+    [% L.radio_button_tag('filter.itemstatus', value='obsolete', label=LxERP.t8('Obsolete'), checked=FORM.filter.itemstatus=='obsolete') %]
+    [% L.radio_button_tag('filter.itemstatus', value='orphaned', label=LxERP.t8('Orphaned'), checked=FORM.filter.itemstatus=='orphaned') %]
+   </td>
+  </tr>
+ </table>
+
+ <hr size="1" noshade>
+
+ <table>
+  <tr>
+   <th class="listheading">[% 'Price group' | $T8 %]</th>
+   <th class="listheading">[% 'Preis' | $T8 %]</th>
+   <th class="listheading">[% 'Prozentual/Absolut' | $T8 %]</th>
+  </tr>
+
+  <tr>
+   <td>[% 'Sell Price' | $T8 %]</td>
+   <td>[% L.input_tag('filter.prices.sellprice.price_as_number', FORM.filter.prices.sellprice.price_as_number, size=11) %]</td>
+   <td align="center">
+    [% L.radio_button_tag("filter.prices.sellprice.type",
+       value="percent",
+       checked=!FORM.filter.prices.sellprice.type || FORM.filter.prices.sellprice.type == 'percent') %] /
+    [% L.radio_button_tag("filter.prices.sellprice.type",
+       value="absolut",
+       checked=FORM.filter.prices.sellprice.type == 'absolut') %]
+   </td>
+  </tr>
+
+  <tr>
+   <td>[% 'List Price' | $T8 %]</td>
+   <td>[% L.input_tag('filter.prices.listprice.price_as_number', FORM.filter.prices.listprice.price_as_number, size=11) %]</td>
+   <td align="center">
+    [% L.radio_button_tag("filter.prices.listprice.type",
+       value="percent",
+       checked=!FORM.filter.prices.listprice.type || FORM.filter.prices.listprice.type == 'percent') %] /
+    [% L.radio_button_tag("filter.prices.listprice.type",
+       value="absolut",
+       checked=FORM.filter.prices.listprice.type == 'absolut') %]
+   </td>
+  </tr>
+
+[%- FOREACH pg = SELF.pricegroups %]
+  <tr>
+   <td>[% pg.pricegroup | html %]</td>
+   <td>[% L.input_tag('filter.prices.' _ pg.id _ '.price_as_number', FORM.filter.prices.${pg.id}.price_as_number, size=11) %]</td>
+   <td align="center">
+    [% L.radio_button_tag("filter.prices." _ pg.id  _ ".type",
+       value="percent",
+       checked=!FORM.filter.prices.${pg.id}.type || FORM.filter.prices.${pg.id}.type == 'percent') %] /
+    [% L.radio_button_tag("filter.prices." _ pg.id _ ".type",
+       value="absolut",
+       checked=FORM.filter.prices.${pg.id}.type == 'absolut') %]
+   </td>
+  </tr>
+[%- END %]
+
+ </table>
+</form>
diff --git a/templates/webpages/ic/tabs/_edit_translations.html b/templates/webpages/ic/tabs/_edit_translations.html
deleted file mode 100644 (file)
index 1d0d6c1..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-[%- USE HTML %][%- USE L -%][%- USE P -%][%- USE LxERP -%]
-
-<div id="translations_tab">
- <table>
-  <tr class="listheading">
-   <th>[% LxERP.t8("Language") %]</th>
-   <th>[% LxERP.t8("Description") %]</th>
-   <th>[% LxERP.t8("Long Description") %]</th>
-  </tr>
-
-  [%- FOREACH language = LANGUAGES %]
-   [% SET language_id = language.id
-          translation = translations_map.$language_id %]
-   [% L.hidden_tag('translations[+].language_id', language.id) %]
-   <tr class="listrow" valign="top">
-    <td>[% HTML.escape(language.description) %]</td>
-    <td>[% L.input_tag("translations[].translation", translation.translation) %]</td>
-    <td>[% L.textarea_tag("translations[].longdescription", P.restricted_html(translation.longdescription), id="translations_longdescription_" _ language_id, class="texteditor", style="width: 500px; height: 100px") %]</td>
-   </tr>
-  [%- END %]
- </table>
-</div>
index b9ecc1c..7e55a05 100644 (file)
@@ -3,7 +3,7 @@
 [%- USE L %]
 [%- USE T8 %]
 [%- IF SELF.part.id %]
-<h3>[% LxERP.t8('Stock for part #1', SELF.part.displayable_name) %]</h3>
+<h3>[% LxERP.t8('Stock for part #1', SELF.part.displayable_name) %][%- IF SELF.part.ean -%] ([%- SELF.part.ean -%])[%- END -%]</h3>
 
 [%- IF SELF.stock_empty && !SELF.part.bin_id  %]
 <p>[% 'Nothing stocked yet.' | $T8 %]</p>
@@ -22,7 +22,7 @@
   <tr class='listrow'>
     <td>[% bin.warehouse.description %]</td>
     <td>[% bin.description %]</td>
-    <td class='numeric'>[% LxERP.format_amount(stock__set.sum, 2) %]</td>
+    <td class='numeric'>[% LxERP.format_amount(stock__set.sum, 2) %]&nbsp;[%- SELF.part.unit -%]</td>
   </tr>
   [%- END -%]
 [%- END -%]
diff --git a/templates/webpages/inventory/report_bottom.html b/templates/webpages/inventory/report_bottom.html
new file mode 100644 (file)
index 0000000..c85ec73
--- /dev/null
@@ -0,0 +1 @@
+[%- PROCESS 'common/paginate.html' pages=SELF.pages, base_url = SELF.base_url %]
diff --git a/templates/webpages/inventory/stocktaking/_already_counted_dialog.html b/templates/webpages/inventory/stocktaking/_already_counted_dialog.html
new file mode 100644 (file)
index 0000000..325c799
--- /dev/null
@@ -0,0 +1,42 @@
+[%- USE T8 %][%- USE HTML %][%- USE L %][%- USE LxERP %]
+
+<form method="post" id="already_counted_form" method="POST">
+
+  [% 'This part was already counted for this bin:' | $T8 %]<br>
+  [% SELF.part.displayable_name %] / [% SELF.part.ean %]<br>
+  [% already_counted.first.bin.full_description %], [% 'Stocked Qty' | $T8 %]: [%- LxERP.format_amount(stocked_qty, -2) -%]&nbsp;[%- SELF.part.unit -%]
+  [%- IF SELF.part.unit != SELF.unit.name -%]
+    ([%- LxERP.format_amount(stocked_qty_in_form_units, -2) -%]&nbsp;[%- SELF.unit.name -%])<br>
+  [%- END -%]
+  <br>
+  <br>
+  <table>
+    <tr class='listheading'>
+      <th>[% 'Insert Date' | $T8 %]</th>
+      <th>[% 'Employee' | $T8 %]</th>
+      <th>[% 'Bin' | $T8 %]</th>
+      <th>[% 'Target Qty' | $T8 %]</th>
+    </tr>
+    [% FOREACH ac = already_counted %]
+    <tr class='listrow'>
+      <td>[%- ac.itime_as_timestamp -%]</td>
+      <td>[%- ac.employee.safe_name -%]</td>
+      <td>[%- ac.bin.full_description -%]</td>
+      <td class="numeric">[%- ac.qty_as_number -%]&nbsp;[%- ac.part.unit -%]</td>
+    </tr>
+    [% END %]
+  </table>
+
+  <p>
+    [% 'Please choose the action to be processed for your target quantity:' | $T8 %]<br>
+    [% 'Correct counted' | $T8 %]: [% 'The stock will be changed to your target quantity.' | $T8 %]<br>
+    [% 'Add counted' | $T8 %]: [% 'Your target quantity will be added to the stocked quantity.' | $T8 %]<br>
+  </p>
+
+  <br>
+  [% L.hidden_tag('action', 'Inventory/dispatch') %]
+  [% L.button_tag('kivi.Inventory.stocktaking_correct_counted()', LxERP.t8("Correct counted")) %]
+  [% L.button_tag('kivi.Inventory.stocktaking_add_counted(' _ stocked_qty_in_form_units _ ')', LxERP.t8("Add counted")) %]
+  <a href="#" onclick="kivi.Inventory.close_already_counted_dialog();">[%- LxERP.t8("Cancel") %]</a>
+
+</form>
diff --git a/templates/webpages/inventory/stocktaking/_filter.html b/templates/webpages/inventory/stocktaking/_filter.html
new file mode 100644 (file)
index 0000000..9ed4549
--- /dev/null
@@ -0,0 +1,27 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE L %]
+[%- USE LxERP %]
+
+<table id="filter_table">
+ <tr>
+  <th align="right">[% 'EAN' | $T8 %]</th>
+  <td>[% L.input_tag('filter.parts.ean:substr::ilike', filter.parts.ean_substr__ilike) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% 'Part Number' | $T8 %]</th>
+  <td>[% L.input_tag('filter.parts.partnumber:substr::ilike', filter.parts.partnumber_substr__ilike) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% 'Part Description' | $T8 %]</th>
+  <td>[% L.input_tag('filter.parts.description:substr::ilike', filter.parts.description_substr__ilike) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% 'Cutoff Date' | $T8 %]</th>
+  <td>[% L.date_tag('filter.cutoff_date:date', filter.cutoff_date_date) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% 'Comment' | $T8 %]</th>
+  <td>[% L.input_tag('filter.comment:substr::ilike', filter.comment_substr__ilike, size=60) %]</td>
+ </tr>
+</table>
diff --git a/templates/webpages/inventory/stocktaking/form.html b/templates/webpages/inventory/stocktaking/form.html
new file mode 100644 (file)
index 0000000..ffe3600
--- /dev/null
@@ -0,0 +1,79 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE P %]
+[%- USE HTML %]
+[%- USE LxERP %]
+
+<h1>[% title | html %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+<form method="post" action="controller.pl" id="stocktaking_form">
+
+ <p>
+  <label for="part_id">[% "Article" | $T8 %]</label>
+  [% P.part.picker("part_id", "", with_makemodel=1) %]
+ </p>
+
+ <p>
+   <div id="stock"></div>
+ </p>
+
+ <table id="stocktaking_settings_table">
+   <tr>
+     <th align="right" nowrap>[% 'Destination warehouse' | $T8 %]</th>
+     <td>[% L.select_tag('warehouse_id', SELF.warehouses, default=SELF.warehouse.id, title_key='description') %]
+       [% IF SELF.warehouse.id %]
+         [% L.select_tag('bin_id', SELF.warehouse.bins, default=SELF.bin.id, title_key='description') %]
+       [%- ELSE %]
+         <span id='bin_id'></span>
+       [% END %]
+     </td>
+   </tr>
+
+   <tr>
+     <th align="right" nowrap>[% 'Charge number' | $T8 %]</th>
+     <td>[% L.input_tag('chargenumber', "", size=30) %]</td>
+   </tr>
+
+   [% IF INSTANCE_CONF.get_show_bestbefore %]
+     <tr>
+       <th align="right" nowrap>[% 'Best Before' | $T8 %]</th>
+       <td>[% L.date_tag('bestbefore', "") %]</td>
+     </tr>
+   [%- END %]
+
+   <tr>
+     <th align="right" nowrap>[% 'Target Qty' | $T8 %]</th>
+     <td>
+       [% L.input_tag('target_qty', '', size=10, class='numeric') %]
+       [%- IF SELF.part.unit %]
+         [% L.select_tag('unit_id', SELF.part.available_units, title_key='name', default=SELF.unit.id) %]
+       [%- ELSE %]
+         [% L.select_tag('unit_id', SELF.units, title_key='name') %]
+       [%- END %]
+     </td>
+   </tr>
+
+   <tr>
+     <th align="right" nowrap>[% 'Cutoff Date' | $T8 %]</th>
+     <td>
+       [% L.date_tag('cutoff_date_as_date', SELF.stocktaking_cutoff_date) %]
+     </td>
+   </tr>
+
+   <tr>
+     <th align="right" nowrap>[% 'Optional comment' | $T8 %]</th>
+     <td>
+       [% L.input_tag('comment', SELF.stocktaking_comment, size=40) %]
+     </td>
+   </tr>
+ </table>
+
+</form>
+
+<p>
+  <div id="stocktaking_history">
+    [%- LxERP.t8("Loading...") %]
+  </div>
+</p>
diff --git a/templates/webpages/inventory/stocktaking/full_report_top.html b/templates/webpages/inventory/stocktaking/full_report_top.html
new file mode 100644 (file)
index 0000000..4b13a7e
--- /dev/null
@@ -0,0 +1,26 @@
+[%- USE L %]
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE HTML %]
+<form action='controller.pl' method='post'>
+<div class='filter_toggle'>
+<a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Show Filter' | $T8 %]</a>
+  [% SELF.filter_summary | html %]
+</div>
+<div class='filter_toggle' style='display:none'>
+<a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Hide Filter' | $T8 %]</a>
+[%- PROCESS 'inventory/stocktaking/_filter.html' filter=SELF.stocktaking_models.filtered.laundered %]
+
+[% L.hidden_tag('action', 'Inventory/dispatch') %]
+[% L.hidden_tag('sort_by', FORM.sort_by) %]
+[% L.hidden_tag('sort_dir', FORM.sort_dir) %]
+[% L.hidden_tag('page', FORM.page) %]
+[% L.input_tag('action_stocktaking_journal', LxERP.t8('Continue'), type = 'submit', class='submit')%]
+
+
+<a href='#' onClick='javascript:$("#filter_table input").val("");$("#filter_table input[type=checkbox]").prop("checked", 0);'>[% 'Reset' | $T8 %]</a>
+
+</div>
+
+</form>
+ <hr>
diff --git a/templates/webpages/inventory/stocktaking/report_bottom.html b/templates/webpages/inventory/stocktaking/report_bottom.html
new file mode 100644 (file)
index 0000000..e147c71
--- /dev/null
@@ -0,0 +1,2 @@
+[% USE L %]
+[%- L.paginate_controls(models=SELF.stocktaking_models) %]
index 5901f6d..9aca162 100644 (file)
@@ -1,18 +1,19 @@
 [%- USE T8 %]
 [%- USE L %]
+[%- USE P %]
 [%- USE HTML %]
 [%- USE LxERP %]
 
 <h1>[% title | html %]</h1>
 
-[%- PROCESS 'common/flash.html' %]
+[%- INCLUDE 'common/flash.html' %]
 
-<form name="Form" method="post" action="controller.pl">
+<form name="Form" method="post" action="controller.pl" id="form">
 
  <table>
   <tr>
    <th align="right" nowrap>[% 'Part' | $T8 %]</th>
-   <td>[% L.part_picker('part_id', SELF.part) %]</td>
+   <td>[% P.part.picker('part_id', SELF.part) %]</td>
   </tr>
 
   <tr>
 
   <tr>
    <th align="right" nowrap>[% 'Charge number' | $T8 %]</th>
-   <td>[% L.input_tag('chargenumber', SELF.chargenumber, size=30) %]</td>
+   <td>[% L.input_tag('chargenumber', FORM.chargenumber, size=30) %]</td>
   </tr>
 
 [% IF INSTANCE_CONF.get_show_bestbefore %]
   <tr>
    <th align="right" nowrap>[% 'Best Before' | $T8 %]</th>
-   <td>[% L.date_tag('bestbefore', SELF.bestbefore) %]</td>
+   <td>[% L.date_tag('bestbefore', FORM.bestbefore) %]</td>
   </tr>
 [%- END %]
 
-  <tr>
-   <th align="right" nowrap>[% 'EAN' | $T8 %]</th>
-   <td><input name="ean" size="30" value="[% HTML.escape(ean) %]"></td>
-  </tr>
-
   <tr>
    <th align="right" nowrap>[% 'Quantity' | $T8 %]</th>
    <td>
-    <input name="qty" size="10" value="[% HTML.escape(LxERP.format_amount(qty)) %]">
+    [% L.input_tag('qty', LxERP.format_amount(FORM.qty), size=10) %]
 [%- IF SELF.part.unit %]
     [% L.select_tag('unit_id', SELF.part.available_units, title_key='name', default=SELF.unit.id) %]
 [%- ELSE %]
    </td>
   </tr>
 
+  <tr>
+   <td>[% 'Select type of transfer in' | $T8 %]:</td>
+   <td>[% L.select_tag('transfer_type_id', TRANSFER_TYPES, title_key='description') %] </td>
+  </tr>
+
   <tr>
    <th align="right" nowrap>[% 'Optional comment' | $T8 %]</th>
-   <td><input name="comment" size="60" value="[% HTML.escape(comment) %]"></td>
+   <td>[% L.input_tag('comment', FORM.comment, size=60) %]</td>
   </tr>
  </table>
-
- <input type="hidden" name="action" value="Inventory/dispatch">
- <input type="submit" id='action_stock' class="submit" name="action_stock" value="[% 'Stock' | $T8 %]" [% IF !SELF.part.id %]disabled[% END %]>
 </form>
 
 <div id='stock'>
@@ -81,14 +79,15 @@ function reload_warehouse_selection () {
 function reload_bin_selection () {
   $.post("controller.pl", { action: 'Inventory/warehouse_changed', warehouse_id: function(){ return $('#warehouse_id').val() } }, kivi.eval_json_result);
 }
+function check_part_selection_before_stocking() {
+  if ($('#part_id').val() !== '')
+    return true;
+
+  alert(kivi.t8('No article has been selected yet.'));
+  return false;
+}
 $(function(){
   $('#part_id').change(reload_warehouse_selection);
-  $('#part_id').change(function(){
-    if ($('#part_id').val() > 0)
-      $('#action_stock').removeAttr('disabled');
-    else
-      $('#action_stock').attr('disabled', 'disabled');
-  });
   $('#warehouse_id').change(reload_bin_selection);
 })
 </script>
diff --git a/templates/webpages/inventory/warehouse_usage.html b/templates/webpages/inventory/warehouse_usage.html
new file mode 100644 (file)
index 0000000..36c23ab
--- /dev/null
@@ -0,0 +1,121 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- WAREHOUSE_FILTER = 1 %]
+[%- PROCESS 'common/select_warehouse_bin.html' %]
+
+<h1>[% title | html %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+<form name="Form" method="post" action="controller.pl" id="form">
+
+ <table border="0">
+    <tr>
+     <th class="listheading" align="left" valign="top" colspan="5" nowrap>[% 'Period:' | $T8 %]</th>
+    </tr>
+  <tr>
+    <th align=left><input name=reporttype class=radio type=radio value="custom" checked>[% 'Customized Report' | $T8 %]</th>
+  </tr>
+  <tr>
+    <th colspan=1>[% 'Year' | $T8 %]</th>
+    <td><input name=year size=11 title="[% 'YYYY' | $T8 %]" value="[% year %]" class="initial_focus"></td>
+  </tr>
+  <tr>
+    <td align=right> <b>[% 'Yearly' | $T8 %]</b> </td>
+    <th align=left>[% 'Quarterly' | $T8 %]</th>
+    <th align=left colspan=3>[% 'Monthly' | $T8 %]</th>
+  </tr>
+  <tr>
+    <td align=right>&nbsp; <input name=duetyp class=radio type=radio value="13" checked></td>
+    <td><input name=duetyp class=radio type=radio value="A">&nbsp;1. [% 'Quarter' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="1">&nbsp;[% 'January' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="5">&nbsp;[% 'May' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="9">&nbsp;[% 'September' | $T8 %]</td>
+  </tr>
+  <tr>
+    <td align= right>&nbsp;</td>
+    <td><input name=duetyp class=radio type=radio value="B">&nbsp;2. [% 'Quarter' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="2">&nbsp;[% 'February' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="6">&nbsp;[% 'June' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="10">&nbsp;[% 'October' | $T8 %]</td>
+  </tr>
+  <tr>
+    <td> &nbsp;</td>
+    <td><input name=duetyp class=radio type=radio value="C">&nbsp;3. [% 'Quarter' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="3">&nbsp;[% 'March' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="7">&nbsp;[% 'July' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="11">&nbsp;[% 'November' | $T8 %]</td>
+  </tr>
+  <tr>
+    <td> &nbsp;</td>
+    <td><input name=duetyp class=radio type=radio value="D">&nbsp;4. [% 'Quarter' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="4">&nbsp;[% 'April' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="8">&nbsp;[% 'August' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="12">&nbsp;[% 'December' | $T8 %]</td>
+  </tr>
+  <tr>
+    <td colspan="5"><hr size=3 noshade></td>
+  </tr>
+  <tr>
+    <th align=left><input name=reporttype class=radio type=radio value="free">[% 'Free report period' | $T8 %]</th>
+    <td align=left colspan=4>
+      [% 'From' | $T8 %] [% L.date_tag('fromdate', fromdate) %]
+      [% 'Bis' | $T8 %] [% L.date_tag('todate', todate) %]
+    </td>
+  </tr>
+    <tr>
+     <th class="listheading" align="left" valign="top" colspan="5" nowrap>[% 'Filter' | $T8 %]</th>
+    </tr>
+    <tr>
+     <td colspan="5">
+      <table>
+       <tr>
+        <th align="right" nowrap>[% 'Warehouse' | $T8 %]:</th>
+        <td>
+         <select name="warehouse_id" id="warehouse_id" onchange="warehouse_selected(this.value, 0)">
+          <option value="">---</option>
+          [%- FOREACH warehouse = WAREHOUSES %]
+          <option value="[% HTML.escape(warehouse.id) %]">[% warehouse.description %]</option>
+          [%- END %]
+         </select>
+        </td>
+       </tr>
+       <tr>
+        <th align="right" nowrap>[% 'Bin' | $T8 %]:</th>
+        <td><select name="bin_id" id="bin_id"></select></td>
+       </tr>
+       <tr>
+        <th align="right" nowrap>[% 'Part Number' | $T8 %]:</th>
+        <td><input name="partnumber" size=20></td>
+       </tr>
+       <tr>
+        <th align="right" nowrap>[% 'Part Description' | $T8 %]:</th>
+        <td><input name="description" size=40></td>
+       </tr>
+[% IF PARTSCLASSIFICATIONS %]
+       <tr>
+        <td>
+           [% L.select_tag('partsclassification',PARTSCLASSIFICATION,title_key="partsclassification") %]
+        </td>
+       </tr>
+[% END %]
+       <tr>
+        <th align="right" nowrap>[% 'Charge Number' | $T8 %]:</th>
+        <td><input name="chargenumber" size=40></td>
+       </tr>
+       [% IF INSTANCE_CONF.get_show_bestbefore %]
+       <tr>
+        <th align="right" nowrap>[% 'Best Before' | $T8 %]:</th>
+        <td>
+          [% L.date_tag('bestbefore') %]
+        </td>
+       </tr>
+       [% END %]
+      </table>
+     </td>
+    </tr>
+   </table>
+  </p>
+ </form>
index 11dfbf6..c6330d2 100644 (file)
@@ -1,20 +1,27 @@
-[% USE LxERP %][% USE HTML %][% USE L %]
+[% USE LxERP %][% USE HTML %][% USE L %][% USE P %]
+[% SET COLS = 8 %]
 <h1>[% title %]</h1>
 
- <form method="post" action="[% HTML.escape(script) %]">
+ <form method="post" action="[% HTML.escape(script) %]" id="form">
 
   <table width="100%">
    <tr class="listheading">
-    [%- IF myconfig_item_multiselect %]
+    [%- IF MYCONFIG.item_multiselect %]
       <th>[% LxERP.t8('Qty') %]</th>
     [%- ELSE %]
       <th>&nbsp;</th>
     [%- END %]
     <th>[% LxERP.t8('Number') %]</th>
+    <th>[% LxERP.t8('Part Classification') %]</th>
     <th>[% LxERP.t8('Part Description') %]</th>
+    [%- IF INSTANCE_CONF.get_show_longdescription_select_item %]
+      [% SET COLS = COLS + 1 %]
+      <th>[% LxERP.t8('Long Description') %]</th>
+    [%- END %]
     <th>[% LxERP.t8('Other Matches') %]</th>
     <th>[% LxERP.t8('Price') %]</th>
     [%- IF IS_PURCHASE %]
+      [% SET COLS = COLS + 1 %]
      <th>[% LxERP.t8('ROP') %]</th>
     [%- END %]
     <th>[% LxERP.t8('Qty') %]</th>
 
    [%- FOREACH item = ITEM_LIST %]
    <tr class="listrow[% loop.count % 2 %]">
-    [%- IF myconfig_item_multiselect %]
+    [%- IF MYCONFIG.item_multiselect %]
       <td>[% L.input_tag('select_qty_' _ HTML.escape(item.id), '', size => 5) %]</td>
     [%- ELSE %]
       <td><input name="select_item_id" class="radio" type="radio" value="[% HTML.escape(item.id) %]"[% IF loop.first %] checked[% END %]></td>
     [%- END %]
     <td>[% HTML.escape(item.partnumber) %]</td>
+    <td>[% HTML.escape(item.type_and_classific) %]</td>
     <td>[% HTML.escape(item.description) %]</td>
+    [%- IF INSTANCE_CONF.get_show_longdescription_select_item %]
+      <td>[% P.restricted_html(item.longdescription) %]</td>
+    [%- END %]
     <td>[% HTML.escape(item.matches).join('<br>') %]</td>
     <td align="right">[% LxERP.format_amount(item.display_sellprice, 2) %]</td>
     [%- IF IS_PURCHASE %]
    </tr>
    [%- END %]
 
-   <tr><td colspan="8"><hr size="3" noshade></td></tr>
+   <tr><td colspan="[% COLS %]"><hr size="3" noshade></td></tr>
   </table>
 
   [% L.hidden_tag('select_item_mode', MODE) %]
   [% L.hidden_tag('select_item_previous_form', PREVIOUS_FORM) %]
-  [% L.hidden_tag('nextsub', 'item_selected') %]
-
-  [% L.submit_tag('action', LxERP.t8('Continue')) %]
+  [% L.hidden_tag('action', 'item_selected') %]
  </form>
 
-[%- IF myconfig_item_multiselect %]
+[%- IF MYCONFIG.item_multiselect %]
  <script type='text/javascript'>
    var first_click = 1;;
    [%- FOREACH item = ITEM_LIST %]
diff --git a/templates/webpages/io/ship_to.html b/templates/webpages/io/ship_to.html
deleted file mode 100644 (file)
index ca5560c..0000000
+++ /dev/null
@@ -1,161 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %][%- USE JavaScript -%]
-
-<script type="text/javascript">
-  var addresses = [
-    { shiptoname:         "[% JavaScript.escape(vc_obj.name) %]",
-      shiptodepartment_1: "[% JavaScript.escape(vc_obj.department_1) %]",
-      shiptodepartment_2: "[% JavaScript.escape(vc_obj.department_2) %]",
-      shiptostreet:       "[% JavaScript.escape(vc_obj.street) %]",
-      shiptozipcode:      "[% JavaScript.escape(vc_obj.zipcode) %]",
-      shiptocity:         "[% JavaScript.escape(vc_obj.city) %]",
-      shiptocountry:      "[% JavaScript.escape(vc_obj.country) %]",
-      shiptocontact:      "[% JavaScript.escape(vc_obj.contact) %]",
-      shiptocp_gender:    "[% JavaScript.escape(vc_obj.cp_gender) %]",
-      shiptophone:        "[% JavaScript.escape(vc_obj.phone) %]",
-      shiptofax:          "[% JavaScript.escape(vc_obj.fax) %]",
-      shiptoemail:        "[% JavaScript.escape(vc_obj.email) %]"
-    }
-
-  [% FOREACH shipto = vc_obj.shipto %]
-    ,
-    { shiptoname:         "[% JavaScript.escape(shipto.shiptoname) %]",
-      shiptodepartment_1: "[% JavaScript.escape(shipto.shiptodepartment_1) %]",
-      shiptodepartment_2: "[% JavaScript.escape(shipto.shiptodepartment_2) %]",
-      shiptostreet:       "[% JavaScript.escape(shipto.shiptostreet) %]",
-      shiptozipcode:      "[% JavaScript.escape(shipto.shiptozipcode) %]",
-      shiptocity:         "[% JavaScript.escape(shipto.shiptocity) %]",
-      shiptocountry:      "[% JavaScript.escape(shipto.shiptocountry) %]",
-      shiptocontact:      "[% JavaScript.escape(shipto.shiptocontact) %]",
-      shiptocp_gender:    "[% JavaScript.escape(shipto.shiptocp_gender) %]",
-      shiptophone:        "[% JavaScript.escape(shipto.shiptophone) %]",
-      shiptofax:          "[% JavaScript.escape(shipto.shiptofax) %]",
-      shiptoemail:        "[% JavaScript.escape(shipto.shiptoemail) %]"
-    }
-  [% END %]
-  ];
-
-  function copy_address() {
-    var shipto = addresses[ $('#shipto_to_copy').val() ];
-    for (key in shipto)
-      $('#' + key).val(shipto[key]);
-  }
-
-  function clear_fields() {
-    var shipto = addresses[0];
-    for (key in shipto)
-      $('#' + key).val('');
-    $('#shiptocp_gender').val('m');
-  }
-
-  function clear_shipto_id_before_submit() {
-    var shipto = addresses[0];
-    for (key in shipto)
-      if ((key != 'shiptocp_gender') && ($('#' + key).val() != '')) {
-        $('#shipto_id').val('');
-        break;
-      }
-
-    $('form').submit();
-  }
-</script>
-
-[% select_options = [ [ 0, LxERP.t8("Billing Address") ] ] ;
-   FOREACH shipto = vc_obj.shipto ;
-     city  = shipto.shiptozipcode _ ' ' _ shipto.shiptocity ;
-     title = [ shipto.shiptoname, shipto.shiptostreet, city ] ;
-     CALL select_options.import([ [ loop.count, title.grep('\S').join("; ") ] ]) ;
-   END ;
-   '' %]
-
- <form method="post" action="[% HTML.escape(script) %]">
-  [% L.hidden_tag("shipto_id", shipto_id) %]
-
-  <p>
-   [% LxERP.t8("Copy address from master data") %]:
-   [% L.select_tag("", select_options, id="shipto_to_copy", style="width: 400px") %]
-   [% L.button_tag("copy_address()", LxERP.t8("Copy")) %]
-   [% L.button_tag("clear_fields()", LxERP.t8("Clear fields")) %]
-  </p>
-
-  <table>
-   <tr class="listheading">
-    <th colspan="2" width="50%">[% LxERP.t8('Billing Address') %]</th>
-    <th width="50%">[% LxERP.t8('Shipping Address') %]</th>
-   </tr>
-   <tr height="5"></tr>
-   <tr>
-    <th align="right" nowrap>[%- IF vc == "customer" %][%- LxERP.t8('Customer Number') %][%- ELSE %][%- LxERP.t8('Vendor Number') %][%- END %]</th>
-    <td>[%- IF vc == "customer" %][%- HTML.escape(customernumber) %][%- ELSE %][%- HTML.escape(vendornumber) %][%- END %]</td>
-   </tr>
-   <tr>
-    <th align="right" nowrap>[% LxERP.t8('Company Name') %]</th>
-    <td>[% HTML.escape(name) %]</td>
-    <td>[% L.input_tag("shiptoname", shiptoname, "size", "35") %]</td>
-   </tr>
-   <tr>
-    <th align="right" nowrap>[% LxERP.t8('Department') %]</th>
-    <td>[% HTML.escape(department_1) %]</td>
-    <td>[% L.input_tag("shiptodepartment_1", shiptodepartment_1, "size", "35") %]</td>
-   </tr>
-   <tr>
-    <th align="right" nowrap>&nbsp;</th>
-    <td>[% HTML.escape(department_2) %]</td>
-    <td>[% L.input_tag("shiptodepartment_2", shiptodepartment_2, "size", "35") %]</td>
-   </tr>
-   <tr>
-    <th align="right" nowrap>[% LxERP.t8('Street') %]</th>
-    <td>[% HTML.escape(street) %]</td>
-    <td>[% L.input_tag("shiptostreet", shiptostreet, "size", "35") %]</td>
-   </tr>
-   <tr>
-    <th align="right" nowrap>[% LxERP.t8('Zipcode') %]</th>
-    <td>[% HTML.escape(zipcode) %]</td>
-    <td>[% L.input_tag("shiptozipcode", shiptozipcode, "size", "35") %]</td>
-   </tr>
-   <tr>
-    <th align="right" nowrap>[% LxERP.t8('City') %]</th>
-    <td>[% HTML.escape(city) %]</td>
-    <td>[% L.input_tag("shiptocity", shiptocity, "size", "35") %]</td>
-   </tr>
-   <tr>
-    <th align="right" nowrap>[% LxERP.t8('Country') %]</th>
-    <td>[% HTML.escape(country) %]</td>
-    <td>[% L.input_tag("shiptocountry", shiptocountry, "size", "35") %]</td>
-   </tr>
-   <tr>
-    <th align="right" nowrap>[% LxERP.t8('Contact') %]</th>
-    <td>[% HTML.escape(contact) %]</td>
-    <td>[% L.input_tag("shiptocontact", shiptocontact, "size", "35") %]</td>
-   </tr>
-   <tr>
-    <th align="right" nowrap>[% LxERP.t8('Gender') %]</th>
-    <td></td>
-    <td>
-     [% L.select_tag('shiptocp_gender', [ [ 'm', LxERP.t8('male') ], [ 'f', LxERP.t8('female') ] ], 'default' = shiptocp_gender) %]
-    </td>
-   </tr>
-   <tr>
-    <th align="right" nowrap>[% LxERP.t8('Phone') %]</th>
-    <td>[% HTML.escape(phone) %]</td>
-    <td>[% L.input_tag("shiptophone", shiptophone, "size", "35") %]</td>
-   </tr>
-   <tr>
-    <th align="right" nowrap>[% LxERP.t8('Fax') %]</th>
-    <td>[% HTML.escape(fax) %]</td>
-    <td>[% L.input_tag("shiptofax", shiptofax, "size", "35") %]</td>
-   </tr>
-   <tr>
-    <th align="right" nowrap>[% LxERP.t8('E-mail') %]</th>
-    <td>[% HTML.escape(email) %]</td>
-    <td>[% L.input_tag("shiptoemail", shiptoemail, "size", "35") %]</td>
-   </tr>
-  </table>
-
-  <hr size="3" noshade>
-
-  [% L.hidden_tag("action", "ship_to_entered") %]
-  [% L.hidden_tag("nextsub", nextsub) %]
-  [% L.hidden_tag("previousform", previousform) %]
-
-  [% L.button_tag("clear_shipto_id_before_submit()", LxERP.t8("Continue")) %]
- </form>
index bef8c33..1110839 100644 (file)
@@ -6,14 +6,8 @@
    <td>
     <table width="100%">
      <tr class="listheading">
-[% IF is_type_credit_note || vc == 'vendor' %]
       <th colspan="6" class="listheading">[% 'Payments' | $T8 %]</th>
-[% ELSE %]
-      <th colspan="6" class="listheading">[% 'Incoming Payments' | $T8 %]</th>
-[%- END %]
      </tr>
-
-
      <tr>
       <th>[% 'Date' | $T8 %]</th>
       <th>[% 'Source' | $T8 %]</th>
   [% SET forex        = 'forex_'        _ i %]
   [% SET exchangerate = 'exchangerate_' _ i %]
   [% IF $forex %]
-        <input type="hidden" name="exchangerate_[% i %]" value="[% LxERP.format_amount($exchangerate, 2) %]">
-        [% LxERP.format_amount($forex, 2) %]
+        <input type="hidden" name="exchangerate_[% i %]" value="[% LxERP.format_amount($exchangerate, 5) %]">
+        [% LxERP.format_amount($forex, 5) %]
   [% ELSE %]
      [% IF $changeable %]
-        <input name="exchangerate_[% i %]" size="10" value="[% LxERP.format_amount($exchangerate, 2, 1) %]">
+        <input name="exchangerate_[% i %]" size="10" value="[% LxERP.format_amount($exchangerate, 5, 1) %]">
      [% ELSE %]
-        <input type="hidden" name="exchangerate_[% i %]" value="[% LxERP.format_amount($exchangerate, 2, 1) %]">
-        [% LxERP.format_amount($exchangerate, 2, 1) %]
+        <input type="hidden" name="exchangerate_[% i %]" value="[% LxERP.format_amount($exchangerate, 5, 1) %]">
+        [% LxERP.format_amount($exchangerate, 5, 1) %]
      [% END %]
   [% END %]
         <input type="hidden" name="forex_[% i %]" value="[% $forex %]">
     </td>
   </tr>
     <script type='text/javascript'>
-     $('#is_set_to_paid_missing').click(function(){ $('input[name^="paid_"]:last').val('[% LxERP.format_amount(paid_missing, 2) %]') });
+     $('#is_set_to_paid_missing').click(function(){ $('input[name^="paid_"]:last').val("[% LxERP.format_amount(paid_missing, 2) %]") });
     </script>
index 08fb4cb..30f77c5 100644 (file)
@@ -1,7 +1,7 @@
 [%- USE T8 %]
 [%- USE HTML %]
 [%- USE LxERP %]
-[%- USE L %]
+[%- USE L %][%- USE P -%]
   <tr>
    <td>
     <table width="100%">
           [% L.textarea_tag("notes", notes, wrap="soft", style="width: 350px; height: 150px", class="texteditor") %]
          </td>
          <td>
-          <textarea name="intnotes" rows="[% rows %]" cols="35" wrap="soft">[% intnotes %]</textarea>
+          [% L.textarea_tag("intnotes", intnotes, wrap="soft", style="width: 350px; height: 150px") %]
          </td>
          <td>
            <table>
+             <tr>
+               <th align="right">[% 'Payment Terms' | $T8 %]</th>
+               <td>[% L.select_tag('payment_id', payment_terms, default = payment_id, title_key = 'description', with_empty = 1, style="width: 250px") %]
+                 <script type='text/javascript'>$('#payment_id').change(function(){ kivi.SalesPurchase.set_duedate_on_reference_date_change("invdate"); })</script>
+               </td>
+             </tr>
              <tr>
                <th align="right">[% 'Delivery Terms' | $T8 %] </th>
                <td>
 
 <p>[% print_options %]</p>
 
-  [% IF id %]
-
-    <input class="submit" type="submit" accesskey="u" name="action" id="update_button" value="[% 'Update' | $T8 %]">
-[% IF  show_storno %]
-    <input class="submit" type="submit" name="action" value="[% 'Storno' | $T8 %]">
-[% END %]
-    <input class="submit" type="submit" name="action" value="[% 'Post Payment' | $T8 %]">
-    <input class="submit" type="submit" name="action" value="[% 'Use As New' | $T8 %]">
-
-[% IF show_delete %]
-    <input class="submit" type="submit" name="action" value="[% 'Delete' | $T8 %]">
-    <input class="submit" type="submit" name="action" value="[% 'Post' | $T8 %]">
-[% END %]
-
-    <input type="button" class="submit" onclick="follow_up_window()" value="[% 'Follow-Up' | $T8 %]">
-
- [% ELSE # no id %]
-      <input class="submit" type="submit" name="action" id="update_button" value="[% 'Update' | $T8 %]">
-   [% UNLESS locked %]
-      <input class="submit" type="submit" name="action" value="[% 'Post' | $T8 %]">
-   [%- END %]
-      <input class="submit" type="submit" name="action" value="[% 'Save Draft' | $T8 %]">
- [% END # id %]
-
-  [% IF id %]
-      [%#- button for saving history %]
-      <input type="button" class="submit" onclick="set_history_window([% id | html %], 'glid');" name="history" id="history" value="[% 'history' | $T8 %]">
-
-      [% IF INSTANCE_CONF.get_ir_show_mark_as_paid %]
-          <input type="submit" class="submit" name="action" value="[% 'mark as paid' | $T8 %]">
-      [% END %]
-  [% END %]
-
 <input type="hidden" name="rowcount" value="[% rowcount %]">
 <input type="hidden" name="callback" value="[% callback %]">
-<input type="hidden" name="draft_id" value="[% draft_id %]">
-<input type="hidden" name="draft_description" value="[% draft_description %]">
+[% P.hidden_tag('draft_id', draft_id) %]
+[% P.hidden_tag('draft_description', draft_description) %]
 <input type="hidden" name="vendor_discount" value="[% vendor_discount %]">
 
 </form>
index 866bb8f..680d2d1 100644 (file)
@@ -1,25 +1,23 @@
 [%- USE T8 %]
 [%- USE HTML %]
 [%- USE LxERP %]
-[%- USE L %]
+[%- USE L %][%- USE P -%]
 <h1>[% title %]</h1>
 
-[%- SET follow_up_trans_info = invnumber _ ' (' _ vendor_name _ ')' %]
+[%- SET follow_up_trans_info = invnumber _ ' (' _ vendor_obj.name _ ')' %]
 <script type="text/javascript" src="js/common.js"></script>
-<script type="text/javascript" src="js/vendor_selection.js"></script>
 <script type="text/javascript" src="js/calculate_qty.js"></script>
 <script type="text/javascript" src="js/follow_up.js"></script>
-<script type="text/javascript" src="js/customer_or_vendor_selection.js"></script>
 
-<form method="post" name="invoice" action="[% script %]">
+<form id='form' method="post" name="invoice" action="[% script %]">
 
 <p>[% saved_message %]</p>
 
 [%- FOREACH key = HIDDENS %]
-<input type="hidden" name="[% HTML.escape(key) %]" value="[% HTML.escape($key)  %]">
+  [% L.hidden_tag(key, $key) %]
 [%- END %]
 <input type="hidden" name="follow_up_trans_id_1" value="[% id %]">
-<input type="hidden" name="follow_up_trans_type_1" value="sales_invoice">
+<input type="hidden" name="follow_up_trans_type_1" value="purchase_invoice">
 <input type="hidden" name="follow_up_trans_info_1" value="[% HTML.escape(follow_up_trans_info) %]">
 <input type="hidden" name="follow_up_rowcount" value="1">
 <input type="hidden" name="lastmtime" value="[% HTML.escape(lastmtime) %]">
   <li><a href="#ui-tabs-webdav">[% 'WebDAV' | $T8 %]</a></li>
 [%- END %]
 [%- IF id %]
+  [%- IF INSTANCE_CONF.get_doc_storage %]
+  <li><a href="#ui-tabs-docs">[% 'Documents' | $T8 %]</a></li>
+  <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=purchase_invoice&object_id=[% HTML.url(id) %]">[% 'Attachments' | $T8 %]</a></li>
+  [%- END %]
+  [%- IF AUTH.assert('record_links', 1) %]
   <li><a href="controller.pl?action=RecordLinks/ajax_list&object_model=PurchaseInvoice&object_id=[% HTML.url(id) %]">[% 'Linked Records' | $T8 %]</a></li>
+  [%- END %]
   <li><a href="[% 'controller.pl?action=AccTrans/list_transactions&trans_id=' _ HTML.url(id) | html %]">[% LxERP.t8('Transactions') %]</a></li>
 [%- END %]
  </ul>
 
+[%- IF INSTANCE_CONF.get_doc_storage %]
+  <div id="ui-tabs-docs"></div>
+[%- END %]
+
  <div id="ui-tabs-basic-data">
 <table width="100%">
   <tr>
         <tr>
           <th align="right">[% 'Vendor' | $T8 %]</th>
           <td>
-            [%- INCLUDE 'generic/multibox.html'
-                 id            = 'vendor',
-                 name          = 'vendor',
-                 style         = 'width: 250px',
-                 class         = 'initial_focus',
-                 DATA          = ALL_VENDORS,
-                 id_sub        = 'vc_keys',
-                 label_key     = 'name',
-                 select        = vc_select,
-                 limit         = vclimit,
-                 allow_textbox = 1,
-                 onChange      = "document.getElementById('update_button').click();" -%]
-            <input type="button" value="[% 'Details (one letter abbreviation)' | $T8 %]" onclick="show_vc_details('[% vc | html %]')">
+           [% P.customer_vendor.picker("vendor_id", vendor_id, type="vendor", style="width: 250px", class="initial_focus", onchange="\$('#update_button').click()") %]
+           [% P.button_tag("show_vc_details('vendor')", LxERP.t8('Details (one letter abbreviation)')) %]
+           [% P.hidden_tag("previous_vendor_id", vendor_id) %]
           </td>
-          <input type="hidden" name="vendor_klass" value="[% HTML.escape(vendor_klass) %]">
-          <input type="hidden" name="vendor_id" value="[% HTML.escape(vendor_id) %]">
-          <input type="hidden" name="oldvendor" value="[% HTML.escape(oldvendor) %]">
-          <input type="hidden" name="selectvendor" value="[% HTML.escape(selectvendor) %]">
         </tr>
 [%- IF ALL_CONTACTS.size %]
         <tr>
         <tr>
           <th align="right">[% 'Steuersatz' | $T8 %]</th>
           <td>
-            [%- INCLUDE 'generic/multibox.html'
-                 name       = 'taxzone_id'
-                 style      = 'width: 250px'
-                 DATA       = ALL_TAXZONES
-                 id_key     = 'id'
-                 readonly   = (id ? 1 : 0)
-                 label_key  = 'description' -%]
+            [% L.select_tag('taxzone_id', ( id ? ALL_TAXZONES : ALL_ACTIVE_TAXZONES) , default = taxzone_id, title_key = 'description', disabled = (id ? 1 : 0), style='width: 250px', onchange = "document.getElementById('update_button').click();") %]
+  [%- IF id %]
+          <input type='hidden' name='taxzone_id' value='[% taxzone_id %]'>
+  [%- END %]
           </td>
   [%- IF id %]
           <input type='hidden' name='taxzone_id' value='[% taxzone_id %]'>
   [%- END %]
         </tr>
-[%- IF all_departments %]
+[%- IF ALL_DEPARTMENTS.as_list.size %]
         <tr>
           <th align="right" nowrap>[% 'Department' | $T8 %]</th>
-          <td colspan="3">
-            [%- INCLUDE 'generic/multibox.html'
-                 name       = 'department_id',
-                 style      = 'width: 250px',
-                 DATA       = all_departments,
-                 id_key     = 'id',
-                 label_sub  = 'department_labels',
-                 show_empty = 1 -%]
-          </td>
+          <td colspan="3">[% P.select_tag("department_id", ALL_DEPARTMENTS, with_empty=1, default=department_id, title_key="description", style="width: 250px") %]</td>
         </tr>
 [%- END %]
 [%- IF currencies %]
           <th align="right">[% 'Exchangerate' | $T8 %]</th>
           <td>
            [%- IF forex %]
-            [% LxERP.format_amount(exchangerate, 2) %]
+            [% LxERP.format_amount(exchangerate, 5) %]
            [%- ELSE %]
             <input name="exchangerate" size="10" value="[% HTML.escape(LxERP.format_amount(exchangerate)) %]">
            [%- END %]
           </td>
         </tr>
 [%- END %]
+        <tr>
+          <th align="right">[% 'Transaction description' | $T8 %]</th>
+          <td colspan="3">[% L.input_tag("transaction_description", transaction_description, size=35, "data-validate"=INSTANCE_CONF.get_require_transaction_description_ps ? 'required' : '') %]</td>
+        </tr>
       </table>
     </td>
     <td align="right" valign="top">
           <th align="right">[% 'Employee' | $T8 %]</th>
           <td>[% L.select_tag('employee_id', ALL_EMPLOYEES, default = employee_id, title_key = 'safe_name') %]</td>
         </tr>
-
-[%- IF is_type_credit_note %]
-        <tr>
-          <th align="right" nowrap>[% 'Credit Note Number' | $T8 %]</th>
-          <td colspan="3"><input size='11' name="invnumber" value="[% HTML.escape(invnumber) %]"></td>
-        </tr>
-        <tr>
-          <th align="right">[% 'Credit Note Date' | $T8 %]</th>
-          <td>[% L.date_tag('invdate', invdate) %]</td>
-        </tr>
-[%- ELSE %]
         <tr>
           <th align="right" nowrap>[% 'Invoice Number' | $T8 %]</th>
-          <td colspan="3"><input size='11' name="invnumber" value="[% HTML.escape(invnumber) %]"></td>
+          <td colspan="3">[% L.input_tag("invnumber", invnumber, size="11") %]</td>
         </tr>
         <tr>
           <th align="right">[% 'Invoice Date' | $T8 %]</th>
            <span id="duedate_fixed"[% IF !payment_terms_obj.auto_calculation %] style="display:none"[% END %]>[% HTML.escape(duedate) %]</span>
           </td>
         </tr>
-[%- END %]
-
+        <tr>
+          <th align="right" nowrap>[% LxERP.t8('Tax point') %]</th>
+          <td nowrap>[% L.date_tag('tax_point', tax_point, id='tax_point') %]</td>
+        </tr>
+        <tr>
+          <th align="right">[% 'Delivery Date' | $T8 %]</th>
+          <td>[% L.date_tag('deliverydate', deliverydate) %]</td>
+        </tr>
         <tr>
           <th align="right" nowrap>[% 'Order Number' | $T8 %]</th>
           <td colspan="3"><input size='11' name="ordnumber" value="[% HTML.escape(ordnumber) %]"></td>
         </tr>
         <tr>
           <th align="right" nowrap>[% 'Project Number' | $T8 %]</th>
-          <td>
-            [%- INCLUDE 'generic/multibox.html'
-                 name       = 'globalproject_id',
-                 DATA       = ALL_PROJECTS,
-                 id_key     = 'id',
-                 label_key  = 'projectnumber',
-                 show_empty = 1,
-                 onChange   = "document.getElementById('update_button').click();" -%]
-          </td>
+          <td>[% P.project.picker('globalproject_id', globalproject_id, onchange="document.getElementById('update_button').click();") %]</td>
         </tr>
       </table>
     </td>
      $('document').ready(function(){
 [% IF creditwarning != '' %]
        alert('[% 'Credit Limit exceeded!!!' | $T8 %]');
-[% ELSE %]
 [% END %]
+       kivi.File.doc_tab_init('ir_tabs', 'ui-tabs-docs', $('#id').val(), 'purchase_invoice');
      });
    //-->
   </script>
index ffab321..8072df3 100644 (file)
@@ -72,7 +72,7 @@
      </td>
      <td align="center">
      [% IF $changeable %]
-       <input name="paid_[% i %]" size="11" value="[% LxERP.format_amount($paid, 2, 1) %]">
+       <input name="paid_[% i %]" size="11" data-validate="number" class="numeric" value="[% LxERP.format_amount($paid, 2, 1) %]">
      [% ELSE %]
        <input type="hidden" name="paid_[% i %]" value="[% LxERP.format_amount($paid, 2, 1) %]">
        [% LxERP.format_amount($paid, 2, 1) %]
   [% SET forex        = 'forex_'        _ i %]
   [% SET exchangerate = 'exchangerate_' _ i %]
   [% IF $forex %]
-        <input type="hidden" name="exchangerate_[% i %]" value="[% LxERP.format_amount($exchangerate, 2) %]">
-        [% LxERP.format_amount($forex, 2) %]
+        <input type="hidden" name="exchangerate_[% i %]" value="[% LxERP.format_amount($exchangerate, 5) %]">
+        [% LxERP.format_amount($forex, 5) %]
   [% ELSE %]
      [% IF $changeable %]
-        <input name="exchangerate_[% i %]" size="10" value="[% LxERP.format_amount($exchangerate, 2, 1) %]">
+        <input name="exchangerate_[% i %]" size="10" value="[% LxERP.format_amount($exchangerate, 5, 1) %]">
      [% ELSE %]
-        <input type="hidden" name="exchangerate_[% i %]" value="[% LxERP.format_amount($exchangerate, 2, 1) %]">
-        [% LxERP.format_amount($exchangerate, 2, 1) %]
+        <input type="hidden" name="exchangerate_[% i %]" value="[% LxERP.format_amount($exchangerate, 5, 1) %]">
+        [% LxERP.format_amount($exchangerate, 5, 1) %]
      [% END %]
   [% END %]
         <input type="hidden" name="forex_[% i %]" value="[% $forex %]">
      </td>
 
     </tr>
-  [% IF $changeable %]
-    <tr style='display:none'>
-     <td>
-    <script type='text/javascript'>
-     $('input[name="paid_[% i %]"]').blur(function(){ check_right_number_format(this) });
-     $('#datepaid_[% i %]').blur(function(){ check_right_date_format(this) });
-    </script>
-     </td>
-    </tr>
-  [% END %]
-
 [% END # foreach %]
 
     <tr>
     </td>
   </tr>
     <script type='text/javascript'>
-     $('#is_set_to_paid_missing').click(function(){ $('input[name^="paid_"]:last').val('[% LxERP.format_amount(paid_missing, 2) %]') });
+     $('#is_set_to_paid_missing').click(function(){ $('input[name^="paid_"]:last').val("[% LxERP.format_amount(paid_missing, 2) %]") });
     </script>
index 2c271dd..7c749cf 100644 (file)
           [% L.textarea_tag("notes", notes, wrap="soft", style="width: 350px; height: 150px", class="texteditor") %]
          </td>
          <td>
-          <textarea name="intnotes" rows="[% rows %]" cols="35">[% intnotes %]</textarea>
+          [% L.textarea_tag("intnotes", intnotes, wrap="soft", style="width: 350px; height: 150px") %]
          </td>
          <td>
            <table>
              <tr>
                <th align="right">[% 'Payment Terms' | $T8 %]</th>
-               <td>
-                 [%- INCLUDE 'generic/multibox.html'
-                   name          = 'payment_id',
-                   style         = 'width: 250px',
-                   DATA          = payment_terms,
-                   id_key        = 'id',
-                   label_key     = 'description',
-                   show_empty    = 1
-                   allow_textbox = 0 -%]
+               <td>[% L.select_tag('payment_id', payment_terms, default = payment_id, title_key = 'description', with_empty = 1, style="width: 250px") %]
                  <script type='text/javascript'>$('#payment_id').change(function(){ kivi.SalesPurchase.set_duedate_on_reference_date_change("invdate"); })</script>
                </td>
              </tr>
                  [%- L.checkbox_tag('direct_debit', 'checked', direct_debit) %]
                </td>
              </tr>
+[%- IF INSTANCE_CONF.get_create_qrbill_invoices > 0 %]
+             <tr>
+               <th align="right">[% 'QR bill without amount' | $T8 %]</th>
+               <td>
+                 [%- L.checkbox_tag('qrbill_without_amount', 'checked', qrbill_without_amount) %]
+               </td>
+             </tr>
+[%- END %]
            </table>
          </td>
         </tr>
   [%- END %]
 [%- END %]
 
+[%- IF rounding %]
+        <tr>
+          <th align='right'>[% 'Rounding' | $T8 %]</th>
+          <td align='right'>[% LxERP.format_amount(rounding, 2) %]</td>
+        </tr>
+[%- END %]
+
         <tr>
          <th align="right">[% 'Total' | $T8 %]</th>
          <td align="right">[% LxERP.format_amount(invtotal, 2) %]</td>
     </table>
    </td>
   </tr>
-
-[% PROCESS 'is/_payments.html' %]
+[% IF is_type_normal_invoice OR  is_type_credit_note %]
+  [% PROCESS 'is/_payments.html' %]
+[% END %]
  </table>
 </div>
 [% PROCESS 'webdav/_list.html' %]
 </div>
 </div>
 
-<hr size="3" noshade>
-
-<p>[% print_options %]</p>
-
-  [% IF id %]
-
-    <input class="submit" type="submit" accesskey="u" name="action" id="update_button" value="[% 'Update' | $T8 %]">
-    <input class="submit" type="submit" name="action" value="[% 'Ship to' | $T8 %]">
-    <input class="submit" type="submit" name="action" value="[% 'Print' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-    <input class="submit" type="submit" name="action" value="[% 'E-mail' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-[% IF  show_storno %]
-    <input class="submit" type="submit" name="action" value="[% 'Storno' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-[% END %]
-    <input class="submit" type="submit" name="action" value="[% 'Post Payment' | $T8 %]">
-    <input class="submit" type="submit" name="action" value="[% 'Use As New' | $T8 %]">
-
-[% IF id && !is_type_credit_note %]
-    <input class="submit" type="submit" name="action" value="[% 'Credit Note' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-[% END %]
-[% IF show_delete && !storno %]
-    <input class="submit" type="submit" name="action" value="[% 'Delete' | $T8 %]">
-    <input class="submit" type="submit" name="action" value="[% 'Post' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-[% END %]
-    <input class="submit" type="submit" name="action" value="[% 'Order' | $T8 %]">
-    <input type="button" class="submit" onclick="follow_up_window()" value="[% 'Follow-Up' | $T8 %]">
-
- [% ELSE # no id %]
-   [% UNLESS locked %]
-      <input class="submit" type="submit" name="action" id="update_button" value="[% 'Update' | $T8 %]">
-      <input class="submit" type="submit" name="action" value="[% 'Ship to' | $T8 %]">
-      <input class="submit" type="submit" name="action" value="[% 'Preview' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-      <input class="submit" type="submit" name="action" value="[% 'Post and E-mail' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-      <input class="submit" type="submit" name="action" value="[% 'Print and Post' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-      <input class="submit" type="submit" name="action" value="[% 'Post' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-      <input class="submit" type="submit" name="action" value="[% 'Save Draft' | $T8 %]">
-   [%- END %]
- [% END # id %]
-
-  [% IF id %]
-      [%#- button for saving history %]
-      <input type="button" class="submit" onclick="set_history_window([% id | html %], 'glid');" name="history" id="history" value="[% 'history' | $T8 %]">
-      [% IF INSTANCE_CONF.get_is_show_mark_as_paid %]
-          <input type="submit" class="submit" name="action" value="[% 'mark as paid' | $T8 %]">
-      [% END %]
-  [% END %]
-
-  [% IF callback %]
-    <a href="[% callback %]">[% 'back' | $T8  %]</a>
-  [% END %]
-
 <input type="hidden" name="rowcount" value="[% rowcount %]">
 <input type="hidden" name="callback" value="[% callback | html %]">
-<input type="hidden" name="draft_id" value="[% draft_id %]">
-<input type="hidden" name="draft_description" value="[% draft_description %]">
+[% P.hidden_tag('draft_id', draft_id) %]
+[% P.hidden_tag('draft_description', draft_description) %]
 <input type="hidden" name="customer_discount" value="[% customer_discount %]">
 <input type="hidden" name="gldate" value="[% gldate %]">
+
+[%- IF INSTANCE_CONF.get_create_qrbill_invoices <= 0 %]
+ <input type="hidden" name="qrbill_without_amount" value="[% qrbill_without_amount %]">
+[%- END %]
+
+<div id="shipto_inputs" class="hidden">
+ [%- PROCESS 'common/_ship_to_dialog.html' cvars=shipto_cvars %]
+</div>
+
+<div id="email_inputs" style="display: none"></div>
+
+<div id="print_options" style="display: none">
+ [% print_options %]
+</div>
 </form>
 <script type='text/javascript'>
  $(kivi.SalesPurchase.init_on_submit_checks);
 </script>
+
+<div id="shipto_dialog" class="hidden"></div>
+<div id="print_dialog" class="hidden">
+ [%- PROCESS 'common/_print_dialog.html' %]
+</div>
index 2ccdf5f..c870191 100644 (file)
@@ -1,31 +1,30 @@
 [%- USE T8 %]
 [%- USE HTML %]
 [%- USE LxERP %]
-[%- USE L %]
-[%- SET follow_up_trans_info = invnumber _ ' (' _ customer_name _ ')' %]
+[%- USE L %][%- USE P -%]
+[%- SET follow_up_trans_info = invnumber _ ' (' _ customer_obj.name _ ')' %]
 <script type="text/javascript" src="js/common.js"></script>
 <script type="text/javascript" src="js/delivery_customer_selection.js"></script>
-<script type="text/javascript" src="js/vendor_selection.js"></script>
 <script type="text/javascript" src="js/calculate_qty.js"></script>
 <script type="text/javascript" src="js/follow_up.js"></script>
-<script type="text/javascript" src="js/customer_or_vendor_selection.js"></script>
 
-<form method="post" name="invoice" action="[% script %]">
+<form method="post" id='form' name="invoice" action="[% script %]">
 
 [%- FOREACH key = HIDDENS %]
-<input type="hidden" name="[% HTML.escape(key) %]" value="[% HTML.escape($key)  %]">
+<input type="hidden" name="[% HTML.escape(key) %]" id="[% HTML.escape(key) %]" value="[% HTML.escape($key)  %]">
 [%- END %]
-<input type="hidden" name="follow_up_trans_id_1" value="[% id %]">
-<input type="hidden" name="follow_up_trans_type_1" value="sales_invoice">
-<input type="hidden" name="follow_up_trans_info_1" value="[% HTML.escape(follow_up_trans_info) %]">
-<input type="hidden" name="follow_up_rowcount" value="1">
-<input type="hidden" name="lastmtime" value="[% HTML.escape(lastmtime) %]">
+<input type="hidden" name="follow_up_trans_id_1" id="follow_up_trans_id_1" value="[% id %]">
+<input type="hidden" name="follow_up_trans_type_1" id="follow_up_trans_type_1" value="sales_invoice">
+<input type="hidden" name="follow_up_trans_info_1" id="follow_up_trans_info_1" value="[% HTML.escape(follow_up_trans_info) %]">
+<input type="hidden" name="follow_up_rowcount" id="follow_up_rowcount" value="1">
+<input type="hidden" name="lastmtime" id="lastmtime" value="[% HTML.escape(lastmtime) %]">
+<input type="hidden" name="already_printed_flag" id="already_printed_flag" value="0">
 
 <h1>[% title %]</h1>
 
 <p>[% saved_message %]</p>
 
-[%- PROCESS 'common/flash.html' %]
+[%- INCLUDE 'common/flash.html' %]
 [%- INCLUDE 'generic/set_longdescription.html' %]
 
 <div id="is_tabs" class="tabwidget">
   <li><a href="#ui-tabs-webdav">[% 'WebDAV' | $T8 %]</a></li>
 [%- END %]
 [%- IF id %]
+  [%- IF INSTANCE_CONF.get_doc_storage %]
+  <li><a href="controller.pl?action=File/list&file_type=document&object_type=[% type %]&object_id=[% HTML.url(id) %]">[% 'Documents' | $T8 %]</a></li>
+  <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=[% type %]&object_id=[% HTML.url(id) %]">[% 'Attachments' | $T8 %]</a></li>
+  [%- END %]
+  [%- IF AUTH.assert('record_links', 1) %]
   <li><a href="controller.pl?action=RecordLinks/ajax_list&object_model=Invoice&object_id=[% HTML.url(id) %]">[% 'Linked Records' | $T8 %]</a></li>
+  [%- END %]
+  [%- IF AUTH.assert('invoice_edit', 1) %]
   <li><a href="[% 'controller.pl?action=AccTrans/list_transactions&trans_id=' _ HTML.url(id) | html %]">[% LxERP.t8('Transactions') %]</a></li>
+  [%- END %]
 [%- END %]
  </ul>
 
         <tr>
           <th align="right">[% 'Customer' | $T8 %]</th>
           <td>
-            [%- INCLUDE 'generic/multibox.html'
-                 id            = 'customer',
-                 name          = 'customer',
-                 style         = 'width: 250px',
-                 class         = 'initial_focus',
-                 DATA          = ALL_CUSTOMERS,
-                 id_sub        = 'vc_keys',
-                 label_key     = 'name',
-                 select        = vc_select,
-                 limit         = vclimit,
-                 allow_textbox = 1,
-                 onChange      = "document.getElementById('update_button').click();" -%]
-            <input type="button" value="[% 'Details (one letter abbreviation)' | $T8 %]" onclick="show_vc_details('[% HTML.escape(vc) %]')">
-          <input type="hidden" name="customer_klass" value="[% HTML.escape(customer_klass) %]">
-          <input type="hidden" name="customer_id" value="[% HTML.escape(customer_id) %]">
-          <input type="hidden" name="oldcustomer" value="[% HTML.escape(oldcustomer) %]">
-          <input type="hidden" name="selectcustomer" value="[% HTML.escape(selectcustomer) %]">
+           [% P.customer_vendor.picker("customer_id", customer_id, type="customer", style="width: 250px", class="initial_focus", onchange="\$('#update_button').click()") %]
+           [% L.button_tag("show_vc_details('customer')", LxERP.t8('Details (one letter abbreviation)')) %]
+           [% P.link_tag('controller.pl?action=CustomerVendor/edit&db=customer&id=' _ customer_id, LxERP.t8('Edit'), target="_blank", title=LxERP.t8('Open in new window')) %]
+           [% L.hidden_tag("previous_customer_id", customer_id) %]
+           [% L.hidden_tag("customer_pricegroup_id", customer_pricegroup_id) %]
           </td>
         </tr>
 [%- IF ALL_CONTACTS.size %]
           </td>
         </tr>
 [%- END %]
-[%- IF ALL_SHIPTO.size %]
         <tr>
           <th align="right">[% 'Shipping Address' | $T8 %]</th>
           <td>
+           [%- IF ALL_SHIPTO.size %]
             [% shiptos = [ [ "", LxERP.t8("No/individual shipping address") ] ] ;
                L.select_tag('shipto_id', shiptos.import(ALL_SHIPTO), default=shipto_id, value_key='shipto_id', title_key='displayable_id', style='width: 250px') %]
+           [%- END %]
+           [% L.button_tag("kivi.SalesPurchase.edit_custom_shipto()", LxERP.t8("Custom shipto")) %]
+          </td>
+        </tr>
+
+[%- IF customer_obj.additional_billing_addresses.as_list.size %]
+        <tr>
+          <th align="right">[% 'Custom Billing Address' | $T8 %]</th>
+          <td>
+            [% L.select_tag('billing_address_id', customer_obj.additional_billing_addresses,
+                            with_empty=1, default=billing_address_id, value_key='id', title_key='displayable_id', style='width: 250px') %]
           </td>
         </tr>
 [%- END %]
+
         <tr>
           <td align="right">[% 'Credit Limit' | $T8 %]</td>
           <td>
   [%- END %]
           </td>
         </tr>
-[%- IF all_departments %]
+[%- IF ALL_LANGUAGES %]
+        <tr>
+          <th align="right" nowrap>[% 'Language' | $T8 %]</th>
+          <td colspan="3">
+            [% L.select_tag('language_id', ALL_LANGUAGES, default = language_id, title_key = 'description', with_empty = 1, style = 'width:250px') %]
+          </td>
+        </tr>
+[%- END %]
+[%- IF ALL_DEPARTMENTS %]
         <tr>
           <th align="right" nowrap>[% 'Department' | $T8 %]</th>
           <td colspan="3">
-            [% L.select_tag('department_id', all_departments, default = department_id, title_sub = \department_labels, with_empty = 1, style = 'width:250px') %]
+            [% L.select_tag('department_id', ALL_DEPARTMENTS, default = department_id, title_key = 'description', with_empty = 1, style = 'width:250px') %]
           </td>
         </tr>
 [%- END %]
           <td>
             <input type="hidden" name="fxgain_accno" value="[% fxgain_accno %]">
             <input type="hidden" name="fxloss_accno" value="[% fxloss_accno %]">
+            <input type="hidden" name="rndgain_accno" value="[% rndgain_accno %]">
+            <input type="hidden" name="rndloss_accno" value="[% rndloss_accno %]">
           </td>
         </tr>
 [%- IF show_exchangerate %]
           <th align="right">[% 'Exchangerate' | $T8 %]</th>
           <td>
            [%- IF forex %]
-            [% LxERP.format_amount(exchangerate, 2) %]
+            [% LxERP.format_amount(exchangerate, 5) %]
            [%- ELSE %]
             <input name="exchangerate" size="10" value="[% HTML.escape(LxERP.format_amount(exchangerate)) %]">
            [%- END %]
         </tr>
         <tr>
           <th align="right">[% 'Transaction description' | $T8 %]</th>
-          <td colspan="3"><input size='35' name="transaction_description" id="transaction_description" value="[% HTML.escape(transaction_description) %]"></td>
+          <td colspan="3">[% L.input_tag("transaction_description", transaction_description, size=35, "data-validate"=INSTANCE_CONF.get_require_transaction_description_ps ? 'required' : '') %]</td>
         </tr>
       </table>
     </td>
 [%- IF is_type_credit_note %]
         <tr>
           <th align="right" nowrap>[% 'Credit Note Number' | $T8 %]</th>
-          <td colspan="3"><input size='11' name="invnumber" value="[% HTML.escape(invnumber) %]"></td>
+          <td colspan="3">
+[%- IF INSTANCE_CONF.get_sales_purchase_record_numbers_changeable %]
+            [% L.input_tag("invnumber", invnumber, size="11") %]
+[%- ELSIF id %]
+            [% L.hidden_tag("invnumber", invnumber) %]
+            [% HTML.escape(invnumber) %]
+[%- ELSE %]
+            [% LxERP.t8("will be set upon posting") %]
+[%- END %]
+          </td>
         </tr>
         <tr>
           <th align="right" nowrap>[% 'Invoice Number' | $T8 %]</th>
-          <td colspan="3"><input size='11' name="invnumber_for_credit_note" value="[% HTML.escape(invnumber_for_credit_note) %]"></td>
+          <td colspan="3"><input size='11' name="invnumber_for_credit_note" id="invnumber_for_credit_note" value="[% HTML.escape(invnumber_for_credit_note) %]"></td>
         </tr>
         <tr>
           <th align="right">[% 'Credit Note Date' | $T8 %]</th>
 [%- ELSE %]
         <tr>
           <th align="right" nowrap>[% 'Invoice Number' | $T8 %]</th>
-          <td colspan="3"><input size='11' name="invnumber" value="[% HTML.escape(invnumber) %]"></td>
+          <td colspan="3">
+[%- IF INSTANCE_CONF.get_sales_purchase_record_numbers_changeable %]
+          [% L.input_tag("invnumber", invnumber, size="11") %]
+[%- ELSIF id %]
+            [% L.hidden_tag("invnumber", invnumber) %]
+            [% HTML.escape(invnumber) %]
+[%- ELSE %]
+            [% LxERP.t8("will be set upon posting") %]
+[%- END %]
+          </td>
         </tr>
         <tr>
           <th align="right">[% 'Invoice Date' | $T8 %]</th>
            <span id="duedate_fixed"[% IF !payment_terms_obj.auto_calculation %] style="display:none"[% END %]>[% HTML.escape(duedate) %]</span>
           </td>
         </tr>
+[%- END %]
         <tr>
-        <th align="right" nowrap>[% 'Delivery Order Number' | $T8 %]</th>
-          <td colspan="3"><input size='11' name="donumber" value="[% HTML.escape(donumber) %]"></td>
+          <th align="right" nowrap>[% LxERP.t8('Tax point') %]</th>
+          <td nowrap>[% L.date_tag('tax_point', tax_point, id='tax_point') %]</td>
+        </tr>
+[%- IF !is_type_credit_note %]
+        <tr>
+          <th align="right" nowrap>[% 'Delivery Order Number' | $T8 %]</th>
+          <td colspan="3"><input size='11' name="donumber" id="donumber" value="[% HTML.escape(donumber) %]"></td>
         </tr>
 [%- END %]
         <tr>
         </tr>
         <tr>
           <th align="right" nowrap>[% 'Order Number' | $T8 %]</th>
-          <td colspan="3"><input size='11' name="ordnumber" value="[% HTML.escape(ordnumber) %]"></td>
+          <td colspan="3"><input size='11' name="ordnumber" id="ordnumber" value="[% HTML.escape(ordnumber) %]"></td>
         </tr>
         <tr>
           <th align="right" nowrap>[% 'Order Date' | $T8 %]</th>
         </tr>
         <tr>
           <th align="right" nowrap>[% 'Quotation Number' | $T8 %]</th>
-          <td colspan="3"><input size='11' name="quonumber" value="[% HTML.escape(quonumber) %]"></td>
+          <td colspan="3"><input size='11' name="quonumber" id="quonumber" value="[% HTML.escape(quonumber) %]"></td>
         </tr>
         <tr>
           <th align="right" nowrap>[% 'Quotation Date' | $T8 %]</th>
         </tr>
         <tr>
           <th align="right" nowrap>[% 'Customer Order Number' | $T8 %]</th>
-          <td colspan="3"><input size='11' name="cusordnumber" value="[% HTML.escape(cusordnumber) %]"></td>
+          <td colspan="3"><input size='11' name="cusordnumber" id="cusordnumber" value="[% HTML.escape(cusordnumber) %]"></td>
         </tr>
         <tr>
           <th align="right" nowrap>[% 'Project Number' | $T8 %]</th>
   <script type="text/javascript">
    <!--
      $('document').ready(function(){
+[% IF INSTANCE_CONF.get_invoice_prevent_browser_back %]
+       function disableBack() { window.history.forward() };
+       window.onload = disableBack();
+       window.onpageshow = function(evt) { if (evt.persisted) disableBack() };
+[% END %]
+
 [% IF resubmit && is_format_html %]
        window.open('about:blank','Beleg');
        document.invoice.target = 'Beleg';
-       document.invoice.submit();
+       kivi.SalesPurchase.show_print_dialog();
+       kivi.SalesPurchase.print_record();
 [% ELSIF resubmit %]
-       document.invoice.submit();
+       if ($('#already_printed_flag').val() === "0") {
+         kivi.SalesPurchase.show_print_dialog();
+         kivi.SalesPurchase.print_record();
+         $('#already_printed_flag').val("1");
+       }
 [% ELSIF creditwarning != '' %]
        alert('[% 'Credit Limit exceeded!!!' | $T8 %]');
 [% ELSE %]
index 8deb9e9..2b03f4a 100644 (file)
@@ -3,9 +3,6 @@
 [%- USE JSON %]
 kivi.myconfig = [% JSON.json(MYCONFIG) %];
 $(function() {
-  setupPoints(kivi.myconfig.numberformat, '[% JavaScript.escape(LxERP.t8("wrongformat")) %]');
-  setupDateFormat(kivi.myconfig.dateformat, '[% JavaScript.escape(LxERP.t8("Falsches Datumsformat!")) %]');
-
   $.datepicker.setDefaults(
     $.extend({}, $.datepicker.regional[kivi.myconfig.countrycode], {
       dateFormat: kivi.myconfig.dateformat.replace(/d+/gi, 'dd').replace(/m+/gi, 'mm').replace(/y+/gi, 'yy'),
@@ -19,7 +16,8 @@ $(function() {
 
   kivi.setup_formats({
     numbers: kivi.myconfig.numberformat,
-    dates:   kivi.myconfig.dateformat
+    dates:   kivi.myconfig.dateformat,
+    times:   kivi.myconfig.timeformat
   });
 
   kivi.reinit_widgets();
index c002d8e..0c4e4e4 100644 (file)
 [%- USE HTML %]
 [%- USE T8 %]
 [%- USE L %]
-<body onload="[% onload %]" width=100%>
-
- <div class="listtop">[% title %]</div>
-
- <form action='letter.pl' method='POST'>
+[%- USE P %]
+[%- USE LxERP %]
+[%- SET WEBDAV = SELF.webdav_objects %]
+<h1>[% title | html %]</h1>
 
+<form action='controller.pl' method='POST' id='form'>
   <input type="hidden" name="letter.id" value="[% letter.id | html %]">
-  <input type="hidden" name="letter.draft_id" value="[% letter.draft_id | html %]">
-  <input type="hidden" name="title" value="[% title | html %]">
-  <input type="hidden" name="type" value="[% type | html %]">
-  <input type="hidden" name="print_nextsub" value="print_letter">
+  <input type="hidden" name="draft.id" value="[% draft.id | html %]">
+  <input type="hidden" name="type" value="[% FORM.type | html %]">
+  [% L.hidden_tag('is_sales', SELF.is_sales) %]
 
+  [%- INCLUDE 'common/flash.html' %]
 
-<p>
- <table width=100%>
-  [%- IF SAVED_MESSAGE %]
-  <tr>
-    <td colspan=2>[% SAVED_MESSAGE %]</td>
-  </tr>
-  <tr height=10px><td></td></tr>
-  [%- END %]
-  <tr>
-   <td width=50%>
-    <!-- upper left block -->
-     <table width=90%>
-      <tr>
-       <th align='right'>[% 'Customer' | $T8 %]:</th>
-       <td>
-        [%- INCLUDE 'generic/multibox.html'
-             name          = 'letter.customer',
-             style         = 'width:60%',
-             DATA          = ALL_CUSTOMERS,
-             id_sub        = 'vc_keys',
-             label_key     = 'name',
-             select        = vc_select,
-             limit         = myconfig_vclimit,
-             allow_textbox = 1,
-             force_textbox = limit_exceeded_all_customer
-             onChange      = "document.getElementById('update_button').click();" -%]
-[%- IF letter.customer_id %]
-        <input type="button" value="[% 'Details (one letter abbreviation)' | $T8 %]" onclick="show_vc_details('customer')">
-[%- END %]
-        <input type='hidden' name='letter.oldcustomer' value='[% letter.oldcustomer | html %]'>
-        <input type='hidden' name='letter.customer_id' value='[% letter.customer_id | html %]'>
-        <input type='hidden' name='customer_id' value='[% customer_id | html %]'>
-[%- UNLESS myconfig_vclimit < ALL_CUSTOMERS.size %]
-        <input type="hidden" name="letter.select[% vc %]" value="1">
+  <div id="oe_tabs" class="tabwidget">
+   <ul>
+    <li><a href="#ui-tabs-letter">[% LxERP.t8("Letter") %]</a></li>
+[%- IF letter.id %]
+ [%- IF INSTANCE_CONF.get_webdav %]
+     <li><a href="#ui-tabs-webdav">[% LxERP.t8('WebDAV') %]</a></li>
+ [%- END %]
+ [%- IF INSTANCE_CONF.get_doc_storage %]
+      <li><a href="controller.pl?action=File/list&file_type=document&object_type=[% HTML.escape(FORM.type) %]&object_id=[% HTML.url(letter.id) %]">[% 'Documents' | $T8 %]</a></li>
+ [%- END %]
+    <li><a href="controller.pl?action=RecordLinks/ajax_list&object_model=Letter&object_id=[% HTML.url(letter.id) %]">[% LxERP.t8("Linked Records") %]</a></li>
 [%- END %]
-      </tr>
-      <tr>
-       <th align='right'>[% 'Contact Person' | $T8 %]</th>
-       <td>
-        [%- INCLUDE 'generic/multibox.html'
-             name       = 'letter.cp_id',
-             style      = 'width:100%',
-             DATA       = ALL_CONTACTS,
-             id_key     = 'cp_id',
-             label_sub  = 'ct_labels',
-             show_empty = 1 -%]
-       </td>
-      </tr>
-      <tr>
-       <th align='right'>[% 'Your Reference' | $T8 %]:</th>
-       <td><input name='letter.reference' style='width:70%' value='[% letter.reference %]'></td>
-      </tr>
-     </table>
-    <!-- /upper left block -->
-   </td>
-   <td width=50%>
-    <!-- upper right block -->
-     <table align=center width=90%>
-      <tr>
-       <th align='right'>[% 'Letternumber' | $T8 %]:</th>
-       <td><input name='letter.letternumber' style='width:70%' value='[% letter.letternumber %]'></td>
-      </tr>
-      <tr>
-       <th align='right'>[% 'Date' | $T8 %]:</th>
-        <td>[% L.date_tag('letter.date', letter.date, readonly=readonly) %]</td>
-       </td>
-      </tr>
-     </table>
-    <!-- /upper right block -->
-   </td>
-  </tr>
+   </ul>
 
-  <tr height=20></tr>
+   <div id="ui-tabs-letter">
 
-  <tr>
-   <td colspan=2 width=100%>
-    <!-- central block -->
-     <table width=80%>
-      <tr>
-       <th align=right>[% 'Subject' | $T8 %]</th>
-       <td><textarea name='letter.subject' style='width:100%;font-weight:bold' rows=1>[% letter.subject %]</textarea></td>
-      </tr>
-       <th align=right>[% 'Greeting' | $T8 %]</th>
-       <td><input name='letter.greeting' style='width:100%;font-weight:bold' value="[% letter.greeting | html %]"></td>
-      </tr>
-      <tr>
-      <tr>
-       <th align=right>[% 'Body' | $T8 %]</th>
-       <td><textarea name='letter.body' style='width:100%' rows=20>[% letter.body | html %]</textarea></td>
-      </tr>
-      <tr height=10></tr>
-       <th align=right>[% 'Internal Notes' | $T8 %]</th>
-       <td><textarea name='letter.intnotes' style='width:100%' rows=4>[% letter.intnotes | html %]</textarea></td>
-      </tr>
-
-      <tr>
-       <th></th>
-       <td>
-        <table width=90%>
-         <tr>
-          <td>
-           <table width=100%>
-            <tr>
-             <td>[% 'Employee' | $T8 %]</td>
-            </tr>
-            <tr>
-             <td>
-              [%- INCLUDE 'generic/multibox.html'
-                   name          = 'letter.employee_id',
-                   default       = letter.employee_id,
-                   style         = 'width:70%;font-weight:bold',
-                   DATA          = ALL_EMPLOYEES,
-                   id_key        = 'id',
-                   label_key     = 'name',
-                   limit         = vclimit,
-                   show_empty    = 1,
-                   allow_textbox = 0,
-                   force_textbox = limit_exceeded_ALL_EMPLOYEES
-                   onChange      = "document.getElementById('update_button').click();" -%]
-             </td>
-            </tr>
-            <!-- tr><td><input name="employee_position" style=width:70% value='[% employee_position %]'></td></tr -->
-           </table>
-          </td>
-          <td>
-           <table width=100%>
-            <tr>
-             <td>[% 'Salesman' | $T8 %]</td>
-            </tr>
-            <tr>
-             <td>
-              [%- INCLUDE 'generic/multibox.html'
-                   name          = 'letter.salesman_id',
-                   default       = letter.salesman_id,
-                   style         = 'width:70%;font-weight:bold',
-                   DATA          = ALL_SALESMEN,
-                   id_key        = 'id',
-                   label_key     = 'name',
-                   limit         = vclimit,
-                   show_empty    = 1,
-                   allow_textbox = 0,
-                   force_textbox = limit_exceeded_ALL_SALESMAN
-                   onChange      = "document.getElementById('update_button').click();" -%]
-             </td>
-            </tr>
-           </table>
-          </td>
-          <td>
-          </td>
-         </tr>
-        </table>
-       </td>
-      </tr>
-     </table>
-    <!-- /central block -->
-   </td>
-  </tr>
-  <tr>
-    <td colspan=3><hr size="3" noshade></td>
-  </tr>
-  <tr>
-   <td>
-     [% print_options %]
-   </td>
-  </tr>
- </table>
-</p>
+<table width=100%>
+<tr>
+ <td width=50%>
+  <!-- upper left block -->
+   <table width=90%>
+[%- IF SELF.is_sales %]
+    <tr>
+     <th align='right'>[% 'Customer' | $T8 %]:</th>
+     <td>[% P.customer_vendor.picker('letter.customer_id', letter.customer_id, type='customer') %]</td>
+    </tr>
+[%- ELSE %]
+    <tr>
+     <th align='right'>[% 'Vendor' | $T8 %]:</th>
+     <td>[% P.customer_vendor.picker('letter.vendor_id', letter.vendor_id, type='vendor') %]</td>
+    </tr>
+[%- END %]
+    <tr>
+     <th align='right'>[% 'Contact Person' | $T8 %]</th>
+     <td>[% L.select_tag('letter.cp_id', letter.customer_vendor_id ? letter.customer_vendor.contacts : [], value_key='cp_id', title_key='full_name', default=letter.cp_id) %]</td>
+    </tr>
+    <tr>
+     <th align='right'>[% 'Your Reference' | $T8 %]:</th>
+     <td><input name='letter.reference' style='width:70%' value='[% letter.reference | html %]'></td>
+    </tr>
+   </table>
+  <!-- /upper left block -->
+ </td>
+ <td width=50%>
+  <!-- upper right block -->
+   <table align=center width=90%>
+    <tr>
+     <th align='right'>[% 'Letternumber' | $T8 %]:</th>
+     <td><input name='letter.letternumber' style='width:70%' value='[% letter.letternumber | html %]'></td>
+    </tr>
+    <tr>
+     <th align='right'>[% 'Date' | $T8 %]:</th>
+      <td>[% L.date_tag('letter.date_as_date', letter.date_as_date, readonly=readonly) %]</td>
+     </td>
+    </tr>
+   </table>
+  <!-- /upper right block -->
+ </td>
+</tr>
 
-<input type="hidden" name="action" value="dispatcher">
-<input class="submit" type="submit" name="action_update" id="update_button" value="[% 'Update' | $T8 %]">
+<tr height=20></tr>
 
-[%- IF letter.letternumber %]
-  <input class="submit" type="submit" name="action_print" value="[% 'Print' | $T8 %]">
-[% END %]
+<tr>
+ <td colspan=2 width=100%>
+  <!-- central block -->
+   <table width=80%>
+    <tr>
+     <th align=right>[% 'Subject' | $T8 %]</th>
+     <td><textarea name='letter.subject' style='width:100%;font-weight:bold' rows=1>[% letter.subject | html %]</textarea></td>
+    </tr>
+     <th align=right>[% 'Greeting' | $T8 %]</th>
+     <td><input name='letter.greeting' style='width:100%;font-weight:bold' value="[% letter.greeting | html %]"></td>
+    </tr>
+    <tr>
+    <tr>
+     <th align=right>[% 'Body' | $T8 %]</th>
+     <td>[% L.textarea_tag('letter.body_as_restricted_html', letter.body_as_restricted_html, style='width:100%', rows=20, class="texteditor") %]</td>
+    </tr>
+    <tr height=10></tr>
+     <th align=right>[% 'Internal Notes' | $T8 %]</th>
+     <td><textarea name='letter.intnotes' style='width:100%' rows=4>[% letter.intnotes | html %]</textarea></td>
+    </tr>
 
-<input class="submit" type="submit" name="action_save" value="[% 'Save' | $T8 %]">
-<input class="submit" type="submit" name="action_save_letter_draft" value="[% 'Save Draft' | $T8 %]">
+    <tr>
+     <th></th>
+     <td>
+      <table width=90% align='center'>
+       <tr>
+        <td>
+         <table width=100%>
+          <tr>
+           <td>[% 'Employee' | $T8 %]</td>
+          </tr>
+          <tr>
+           <td>
+            [%- L.select_tag('letter.employee_id', employees, default=letter.employee_id, title_key='safe_name', class='bold', allow_empty=1, style='width:70%') %]
+           </td>
+          </tr>
+         </table>
+        </td>
+        <td>
+         <table width=100%>
+          <tr>
+           <td>[% 'Salesman' | $T8 %]</td>
+          </tr>
+          <tr>
+           <td>
+            [%- L.select_tag('letter.salesman_id', employees, default=letter.salesman_id, title_key='safe_name', class='bold', allow_empty=1, style='width:70%') %]
+           </td>
+          </tr>
+         </table>
+        </td>
+        <td>
+        </td>
+       </tr>
+      </table>
+     </td>
+    </tr>
+   </table>
+  <!-- /central block -->
+ </td>
+</tr>
+</table>
 
- </form>
+<div id="email_inputs" class="hidden"></div>
+<div id="print_options" class="hidden">
+ [% print_options %]
+</div>
 
   <script type="text/javascript">
-     <!--
-       Calendar.setup({ inputField : "date", ifFormat :"[% myconfig_jsc_dateformat %]", align : "BL", button : "date_button" });
-     //-->
   </script>
-    <script type="text/javascript" src="js/show_vc_details.js"></script>
-</body>
</div>
+ [% PROCESS 'webdav/_list.html' %]
+ <div id="ui-tabs-1">
+  [%- LxERP.t8("Loading...") %]
</div>
+</div>
+</form>
 
+<div id="print_dialog" class="hidden">
+ [%- PROCESS 'common/_print_dialog.html' %]
+</div>
index 78c04cd..088defa 100644 (file)
@@ -1,12 +1,11 @@
 [%- USE T8 %]
-[%- USE HTML %]
+[%- USE HTML %][%- USE L -%]
 <h1>[% 'Load letter draft' | $T8 %]</h1>
 
- <form method="post" name="Form" action="letter.pl">
+ <form method="post" name="Form" action="controller.pl" id="form">
+  [% L.hidden_tag('is_sales', SELF.is_sales) %]
+  <p>[% 'The following drafts have been saved and can be loaded.' | $T8 %]</p>
   <table width="100%">
-   <tr>
-    <td>
-     [% 'The following drafts have been saved and can be loaded.' | $T8 %]
     </td>
    </tr>
    <tr>
        <th class="listheading">&nbsp;</th>
        <th class="listheading">[% 'Date' | $T8 %]</th>
        <th class="listheading">[% 'Subject' | $T8 %]</th>
+[%- IF SELF.is_sales %]
        <th class="listheading">[% 'Customer' | $T8 %]</th>
+[%- ELSE %]
+       <th class="listheading">[% 'Vendor' | $T8 %]</th>
+[%- END %]
       </tr>
 
       [% FOREACH row = LETTER_DRAFTS %]
        <tr class="listrow[% loop.count % 2 %]">
-        <td><input type="checkbox" name="checked_[% row.id %]" value="1"></td>
-        <td>[% HTML.escape(row.date) %]</td>
-        <td><a href="letter.pl?action=edit&callback=letter.pl&draft=1&id=[% HTML.url(row.id) %]">[% HTML.escape(row.subject) %]</a></td>
-        <td>[% HTML.escape(row.customer) %]</td>
+        <td>[% L.checkbox_tag("ids[+]", value=row.id) %]</td>
+        <td>[% row.date.to_kivitendo | html %]</td>
+        <td><a href="[% SELF.url_for(action='edit', 'draft.id'=row.id) %]">[% row.subject | html %]</a></td>
+[%- IF SELF.is_sales %]
+        <td>[% row.customer.displayable_name | html %]</td>
+[%- ELSE %]
+        <td>[% row.vendor.displayable_name | html %]</td>
+[%- END %]
        </tr>
       [% END %]
      </table>
     </td>
    </tr>
-   <tr>
-     <input type="hidden" name="action" value="dispatcher">
-     <input type="hidden" name="draft" value="1"><!-- maybe not needed -->
-     <input type="submit" class="submit" name="letter_draft_action" value="[% 'Skip' | $T8 %]">
-     <input type="submit" class="submit" name="letter_draft_action" value="[% 'Delete drafts' | $T8 %]">
-   </td>
-   </tr>
   </table>
  </form>
index da543df..a27818a 100644 (file)
@@ -1,5 +1,7 @@
 [% USE HTML%]
 [%- USE T8 %]
+[%- USE L %][%- USE LxERP -%]
+ [% L.paginate_controls(models=SELF.models) %]
  <input type="hidden" name="rowcount" value="[% HTML.escape(rowcount) %]">
  [%- FOREACH item = HIDDEN %]
  <input type="hidden" name="[% HTML.escape(item.key) %]" value="[% HTML.escape(item.value) %]">
index 548c4ed..dbbeda3 100644 (file)
@@ -1,10 +1,2 @@
-[%- IF OPTIONS.size %]
-<p>
- [%- FOREACH option = OPTIONS %]
- [%- option %][% UNLESS loop.last %]<br>[% END %]
- [%- END %]
-</p>
-[%- END %]
-
-<form action="letter.pl" method="post" name="Form">
-
+[%- PROCESS 'letter/search.html' filter=SELF.models.filtered.laundered %]
+<hr>
index e8ce72f..3fd32dd 100644 (file)
@@ -1,92 +1,61 @@
 [% USE HTML %]
 [% USE T8 %]
-<body onload="on_load()">
-
- <script type="text/javascript">
-  <!--
-      function on_load() {
-        Calendar.setup({ inputField : "date_from", ifFormat :"[% myconfig_jsc_dateformat %]", align : "BR", button : "date_from_trigger" });
-        Calendar.setup({ inputField : "date_to",   ifFormat :"[% myconfig_jsc_dateformat %]", align : "BR", button : "date_to_trigger" });
-        document.Form.subject.focus();
-      }
-    -->
- </script>
-
- <div class="listtop">[% title %]</div>
-
- <form action="letter.pl" method="post" name="Form">
-  <input type="hidden" name="nextsub" value="report">
-
-  <p>
-   <table>
-    <tr>
-     <th align='right'>[% 'Letternumber' | $T8 %]</th>
-     <td><input name='letternumber' style='width:250px' value='[% letternumber %]'></th>
-    </tr>
-    <tr>
-     <td align="right">[% 'Customer' | $T8 %]</td>
-     <td>
-        [%- INCLUDE 'generic/multibox.html'
-             name          = 'customer',
-             style         = 'width:250px',
-             DATA          = ALL_CUSTOMERS,
-             id_key        = 'id',
-             label_key     = 'name',
-             select        = vc_select,
-             limit         = myconfig_vclimit,
-             allow_textbox = 1,
-             show_empty    = 1,
-             force_textbox = limit_exceeded_all_customer
-             onChange      = "document.getElementById('update_button').click();" -%]
-[%- IF myconfig_vclimit > ALL_CUSTOMERS.size %]
-       <input type="hidden" name='selectcustomer' value="1">
+[% USE L %]
+[% USE P %]
+[% USE LxERP %]
+<form action="controller.pl" method="post" name="Form" id="search_form">
+
+<div class='filter_toggle'>
+<a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Show Filter' | $T8 %]</a>
+  [% SELF.filter_summary | html %]
+</div>
+<div class='filter_toggle' style='display:none'>
+<a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Hide Filter' | $T8 %]</a>
+
+
+ <table id='filter_table'>
+  <tr>
+   <th align='right'>[% 'Letternumber' | $T8 %]</th>
+   <td>[% L.input_tag('filter.letternumber:substr::ilike', filter.letternumber_substr__ilike, style='width:250px') %]</th>
+  </tr>
+[%- IF SELF.is_sales %]
+  <tr>
+   <td align="right">[% 'Customer' | $T8 %]</td>
+   <td>[% P.customer_vendor.picker('filter.customer_id', filter.customer_id, type='customer', style='width:250px') %]</td>
+  </tr>
+[%- ELSE %]
+  <tr>
+   <td align="right">[% 'Vendor' | $T8 %]</td>
+   <td>[% P.customer_vendor.picker('filter.vendor_id', filter.vendor_id, type='vendor', style='width:250px') %]</td>
+  </tr>
 [%- END %]
-     </td>
-    </tr>
-
-    <tr>
-     <td align="right">[% 'Contact' | $T8 %]</td>
-     <td><input name="contact" style='width:250px'></td>
-    </tr>
-
-    <tr>
-     <td align="right">[% 'Subject' | $T8 %]</td>
-     <td><input name="subject" style='width:250px'></td>
-    </tr>
-
-    <tr>
-     <td align="right">[% 'Body' | $T8 %]</td>
-     <td><input name="body" style='width:250px'></td>
-    </tr>
-
-    <tr>
-     <td align='right'>[% 'From' | $T8 %]</td>
-     <td>
-      <input name="date_from" id="date_from" size="12">
-      <input type="button" name="date_from_button" id="date_from_trigger" value="?">
-      [% 'To (time)' | $T8 %]
-      <input name="date_to" id="date_to" size="12">
-      <input type="button" name="date_to_button" id="date_to_trigger" value="?">
-     </td>
-    </tr>
-<!--
-    <tr>
-     <td align="right">[% 'Include in Report' | $T8 %]</td>
-     <td>
-
-      <table>
-      </table>
-
-     </td>
-    </tr>
--->
-   </table>
-  </p>
-
-  <p>
-   <input type="submit" name="action" value="[% 'Continue' | $T8 %]">
-  </p>
- </form>
-
-</body>
-</html>
+  <tr>
+   <td align="right">[% 'Contact' | $T8 %]</td>
+   <td>[% L.input_tag('filter.contact.cp_name:substr::ilike', filter.contact.cp_name_substr__ilike, style='width:250px') %]</th>
+  </tr>
+
+  <tr>
+   <td align="right">[% 'Subject' | $T8 %]</td>
+   <td>[% L.input_tag('filter.subject:substr::ilike', filter.subject_substr__ilike, style='width:250px') %]</th>
+  </tr>
+
+  <tr>
+   <td align="right">[% 'Body' | $T8 %]</td>
+   <td>[% L.input_tag('filter.body:substr::ilike', filter.body_substr__ilike, style='width:250px') %]</th>
+  </tr>
+
+  <tr>
+   <td align='right'>[% 'From' | $T8 %]</td>
+   <td> [% L.date_tag('filter.date:date::ge', filter.date_date__ge) %]
+        [% 'To (time)' | $T8 %]
+        [% L.date_tag('filter.date:date::le', filter.date_date__le) %]</td>
+  </tr>
+ </table>
+
+ [% L.hidden_tag('is_sales', SELF.is_sales) %]
+ [% L.hidden_tag('sort_by', FORM.sort_by) %]
+ [% L.hidden_tag('sort_dir', FORM.sort_dir) %]
+ [% L.hidden_tag('page', FORM.page) %]
+ [% L.button_tag("\$('#search_form').resetForm()", LxERP.t8("Reset")) %]
+</div>
+</form>
index 34bf90a..3289539 100644 (file)
@@ -1,8 +1,6 @@
 [%- USE LxERP -%][%- USE L -%]
 
-<form method="post" action="controller.pl">
- [% L.hidden_tag('action', 'LiquidityProjection/show') %]
-
+<form method="post" action="controller.pl" id="filter_form">
  <table border="0">
   <tr>
    <th align="right">[% LxERP.t8("Number of months") %]</th>
     <br>
     [% L.checkbox_tag("params.salesman",       value=1, checked=FORM.params.salesman,       label=LxERP.t8("Salesman")) %]
     <br>
-    [% L.checkbox_tag("params.buchungsgruppe", value=1, checked=FORM.params.buchungsgruppe, label=LxERP.t8("Buchungsgruppe")) %]
+    [% L.checkbox_tag("params.buchungsgruppe", value=1, checked=FORM.params.buchungsgruppe, label=LxERP.t8("Booking group")) %]
    </td>
   </tr>
  </table>
-
- <p>
-  [% L.submit_tag("dummy", LxERP.t8("Show")) %]
- </p>
 </form>
index bcc6e20..8207d95 100644 (file)
@@ -46,7 +46,7 @@
  [%- IF FORM.params.buchungsgruppe %]
   [%- FOREACH buchungsgruppe = SELF.liquidity.sorted.buchungsgruppe %]
    <tr class="listrow">
-    <td>[% IF loop.first %][% LxERP.t8("Buchungsgruppe") %][% END %]</td>
+    <td>[% IF loop.first %][% LxERP.t8("Booking group") %][% END %]</td>
     <td>[%- HTML.escape(buchungsgruppe) %]</td>
 
     [%- FOREACH month = SELF.liquidity.sorted.month %]
index 00273aa..bfb4016 100644 (file)
@@ -7,7 +7,7 @@
    %]
    </noscript>
  <center>
-  <a class="nomobile" href="http://www.kivitendo.de" target="_top"><img src="image/kivitendo.png" border="0" alt='[% 'kivitendo' | $T8 %]' title="[% 'kivitendo Homepage' | $T8 %]"></a>
+  <a class="nomobile" href="http://www.kivitendo.de" target="_top"><img src="image/kivitendo[% xmas %].png" class='kivitendo-logo' border="0" alt='[% 'kivitendo' | $T8 %]' title="[% 'kivitendo Homepage' | $T8 %]"></a>
 
   <h3 class="login">[% 'kivitendo' | $T8 %] [% version %]</h3>
 
    <table border="0">
     <tr>
      <th align="left"><a href="am.pl?action=config" title="[% 'Preferences' | $T8 %]">[% 'User' | $T8 %]</a></th>
-     <td>[% HTML.escape(myconfig_name) %]</td>
+     <td>[% HTML.escape(MYCONFIG.name) %]</td>
     </tr>
     <tr>
-     <th align="left">[% IF AUTH_RIGHTS_ADMIN %]<a href="controller.pl?action=ClientConfig/edit" title="[% 'Client Configuration' | $T8 %]">[% END %][% 'Client' | $T8 %][% IF AUTH_RIGHTS_ADMIN %]</a>[% END %]</th>
+     <th align="left">[% IF AUTH.assert('admin', 'may_fail') %]<a href="controller.pl?action=ClientConfig/edit" title="[% 'Client Configuration' | $T8 %]">[% END %][% 'Client' | $T8 %][% IF AUTH.assert('admin', 'may_fail') %]</a>[% END %]</th>
      <td>[% HTML.escape(client.name) %]</td>
     </tr>
     <tr>
      <th align="left"><a href="am.pl?action=config" title="[% 'Preferences' | $T8 %]">[% 'Language' | $T8 %]</a></th>
-     <td>[% HTML.escape(myconfig_countrycode) %]</td>
+     <td>[% HTML.escape(MYCONFIG.countrycode) %]</td>
     </tr>
     <tr>
      <th align="left">[% 'Webserver interface' | $T8 %]</th>
index 24ba16d..20c63f1 100644 (file)
@@ -6,7 +6,7 @@
   <table class="login" border="3" cellpadding="20">
    <tr>
     <td class="login" align="center">
-     <a href="http://www.kivitendo.de" target="_top" class="no-underlined-links"><img src="image/kivitendo.png" border="0"></a>
+     <a href="http://www.kivitendo.de" target="_top" class="no-underlined-links"><img src="image/kivitendo.png" class='kivitendo-logo' border="0"></a>
      <h1>[% LxERP.t8('kivitendo v#1', version) %]</h1>
 
 [% IF error %]
@@ -24,6 +24,7 @@
       <form method="post" name="loginscreen" action="controller.pl" target="_top">
 
        <input type="hidden" name="show_dbupdate_warning" value="1">
+       [% L.hidden_tag("callback", callback) %]
 
        <table width="100%">
         <tr>
diff --git a/templates/webpages/mass_delivery_order_print/_filter.html b/templates/webpages/mass_delivery_order_print/_filter.html
new file mode 100644 (file)
index 0000000..7bb547e
--- /dev/null
@@ -0,0 +1,41 @@
+[%- USE L %][%- USE LxERP %][%- USE HTML %]
+<div>
+ <form action="controller.pl" method="post">
+  <div class="filter_toggle" [% IF nowshow==0 %]style="display:none"[% END %]>
+   <a href="#" onClick="javascript:$('.filter_toggle').toggle()">[% LxERP.t8('Show Filter') %]</a>
+   [% IF SELF.filter_summary %]([% LxERP.t8("Current filter") %]: [% SELF.filter_summary %])[% END %]
+  </div>
+
+  <div class="filter_toggle" [% IF nowshow==1 %]style="display:none"[% END %]>
+   <a href="#" onClick="javascript:$('.filter_toggle').toggle()">[% LxERP.t8('Hide Filter') %]</a>
+   <table id="filter_table">
+    <tr>
+     <th align="right">[% LxERP.t8('Customer') %]</th>
+     <td>[% L.input_tag('filter.customer.name:substr::ilike', filter.customer.name_substr__ilike, size = 20) %]</td>
+    </tr>
+    <tr>
+     <th align="right">[% LxERP.t8('Shipping address (name)') %]</th>
+     <td>[% L.input_tag('filter.shipto.shiptoname:substr::ilike', filter.shipto.shiptoname_substr__ilike, size = 20) %]</td>
+    </tr>
+    <tr>
+     <th align="right">[% LxERP.t8('Delivery Date') %] [% LxERP.t8('From Date') %]</th>
+     <td>[% L.date_tag('filter.reqdate:date::ge', filter.reqdate_date__ge) %]</td>
+    </tr>
+    <tr>
+     <th align="right">[% LxERP.t8('Delivery Date') %] [% LxERP.t8('To Date') %]</th>
+     <td>[% L.date_tag('filter.reqdate:date::le', filter.reqdate_date__le) %]</td>
+    </tr>
+   </table>
+
+   [% L.hidden_tag('action', 'ODMassPrint/dispatch') %]
+   [% L.hidden_tag('sort_by', FORM.sort_by) %]
+   [% L.hidden_tag('sort_dir', FORM.sort_dir) %]
+   [% L.hidden_tag('page', FORM.page) %]
+   [% L.submit_tag(LIST_ACTION, LxERP.t8('Continue'))%]
+
+   <a href="#" onClick="javascript:$('#filter_table input,#filter_table select').val('');">[% LxERP.t8('Reset') %]</a>
+
+  </div>
+
+ </form>
+</div>
diff --git a/templates/webpages/mass_delivery_order_print/_print_status.html b/templates/webpages/mass_delivery_order_print/_print_status.html
new file mode 100644 (file)
index 0000000..922d89d
--- /dev/null
@@ -0,0 +1,106 @@
+[%- USE LxERP -%][%- USE L -%][%- USE HTML -%]
+[% SET data = job.data_as_hash %]
+<h2>[% LxERP.t8("Watch status") %]</h2>
+
+[% L.hidden_tag('', job.id, id="mdo_job_id") %]
+
+<p>
+ [% LxERP.t8("This status output will be refreshed every five seconds.") %]
+</p>
+
+<p>
+ [% IF data.status < 2 %]
+  [% L.link("login.pl?action=company_logo", LxERP.t8("Open new tab"), target="_blank") %]
+
+ [% ELSE %]
+ [% IF data.pdf_file_name %]
+   [% L.link(SELF.url_for(action="mass_mdo_download", job_id=job.id), LxERP.t8("Download PDF")) %]
+ [% END %]
+ [% L.link("#", LxERP.t8("Close window"), onclick="kivi.MassDeliveryOrderPrint.massConversionFinishProcess();") %]
+[% END %]
+</p>
+
+<p>
+ <table>
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Current status:") %]</th>
+   <td valign="top">
+    [% IF !data.status %]
+     [% LxERP.t8("waiting for job to be started") %]
+    [% ELSIF data.status == 1 %]
+     [% LxERP.t8("Creating Documents") %]
+    [% ELSIF data.status == 2 %]
+     [% LxERP.t8("Printing Documents") %]
+    [% ELSE %]
+     [% LxERP.t8("Done.") %]
+     [% IF data.pdf_file_name %]
+      [% LxERP.t8("The file is available for download.") %]
+     [% ELSIF data.printer_id %]
+      [% LxERP.t8("The file has been sent to the printer.") %]
+     [% END %]
+    [% END %]
+   </td>
+  </tr>
+
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Number of delivery orders created:") %]</th>
+   <td valign="top">[% IF data.status > 0 %][% HTML.escape(data.num_created) %] / [% HTML.escape(data.record_ids.size) %][% ELSE %]–[% END %]</td>
+  </tr>
+
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Number of delivery orders printed:") %]</th>
+   <td valign="top">[% IF data.status > 1 %][% HTML.escape(data.num_printed) %] / [% HTML.escape(data.record_ids.size) %][% ELSE %]–[% END %]</td>
+  </tr>
+
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Errors during conversion:") %]</th>
+   <td valign="top">
+[% IF !data.status %]
+  –
+[% ELSIF !data.conversion_errors.size %]
+ [% LxERP.t8("No errors have occurred.") %]
+[% ELSE %]
+    <table>
+     <tr class="listheader">
+      <th>[% LxERP.t8("Delivery Order") %]</th>
+      <th>[% LxERP.t8("Error") %]</th>
+     </tr>
+
+ [% FOREACH error = data.conversion_errors %]
+     <tr>
+      <td valign="top">[% IF error.id %][% L.link(SELF.url_for(controller='do.pl', action='edit', type='sales_delivery_order', id=error.id), HTML.escape(error.number), target="_blank") %][% ELSE %]–[% END %]</td>
+      <td valign="top">[% HTML.escape(error.message) %]</td>
+     </tr>
+ [% END %]
+    </table>
+[% END %]
+   </td>
+  </tr>
+
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Errors during printing:") %]</th>
+   <td valign="top">
+[% IF data.status < 2 %]
+ –
+[% ELSIF !data.print_errors.size %]
+ [% LxERP.t8("No errors have occurred.") %]
+[% ELSE %]
+    <table>
+     <tr class="listheader">
+      <th>[% LxERP.t8("Invoice") %]</th>
+      <th>[% LxERP.t8("Error") %]</th>
+     </tr>
+
+ [% FOREACH error = data.print_errors %]
+     <tr>
+      <td valign="top">[% IF error.id %][% L.link(SELF.url_for(controller='is.pl', action='edit', type='sales_invoice',id=error.id), HTML.escape(error.number), target="_blank") %][% ELSE %]–[% END %]</td>
+      <td valign="top">[% HTML.escape(error.message) %]</td>
+     </tr>
+ [% END %]
+    </table>
+[% END %]
+   </td>
+  </tr>
+
+ </table>
+</p>
diff --git a/templates/webpages/mass_delivery_order_print/list_delivery_orders.html b/templates/webpages/mass_delivery_order_print/list_delivery_orders.html
new file mode 100644 (file)
index 0000000..a7fa7a2
--- /dev/null
@@ -0,0 +1,72 @@
+[% USE HTML %][% USE L %][% USE LxERP %]
+
+<h1>[% FORM.title %]</h1>
+
+[%- INCLUDE "common/flash.html" %]
+
+[% LIST_ACTION     = 'action_list_delivery_orders' %]
+[%- PROCESS 'mass_delivery_order_print/_filter.html' filter=SELF.filter %]
+
+[% IF nowshow==1 %]
+[% delivery_orders = SELF.delivery_order_models.get;
+   MODELS          = SELF.delivery_order_models %]
+[%- IF !delivery_orders.size %]
+ <p>
+  [%- LxERP.t8("There are currently no delivery orders, or none matches your filter conditions.") %]
+ </p>
+[%- ELSE %]
+
+ <form method="post" action="controller.pl">
+  <table width="100%">
+   <thead>
+    <tr class="listheading">
+     <th>[% L.checkbox_tag("", id="check_all", checkall="[data-checkall=1]") %]</th>
+     <th>[% L.sortable_table_header("transdate") %]</th>
+     <th>[% L.sortable_table_header("reqdate") %]</th>
+     <th>[% L.sortable_table_header("donumber") %]</th>
+     <th>[% L.sortable_table_header("ordnumber") %]</th>
+     <th>[% L.sortable_table_header("customer") %]</th>
+     <th>[% LxERP.t8("Shipto") %]</th>
+    </tr>
+   </thead>
+
+   <tbody>
+    [%- FOREACH delivery_order = delivery_orders %]
+     [% delivery_order_id = delivery_order.id
+        sales_order       = delivery_order.sales_order %]
+     <tr class="listrow">
+      <td>[% L.checkbox_tag('id[]', value=delivery_order.id, "data-checkall"=1, checked=selected_ids.$delivery_order_id) %]</td>
+      <td>[% HTML.escape(delivery_order.transdate_as_date) %]</td>
+      <td>[% HTML.escape(delivery_order.reqdate_as_date) %]</td>
+      <td>[% L.link(SELF.url_for(controller="do.pl", action="edit", type="sales_delivery_order", id=delivery_order.id), delivery_order.donumber) %]</td>
+      <td>[% HTML.escape(delivery_order.ordnumber) %]</td>
+      <td>[% HTML.escape(delivery_order.customer.name) %]</td>
+      <td>[% HTML.escape(SELF.make_shipto_title(delivery_order.shipto || delivery_order.custom_shipto)) %]</td>
+     </tr>
+    [%- END %]
+   </tbody>
+  </table>
+
+  [% IF !SELF.delivery_order_ids.size %]
+   [% L.paginate_controls %]
+  [% END %]
+
+  <hr size="3" noshade>
+
+  <p>[% print_opt %]</p>
+  [% IF SELF.printers.size %]
+   <p>
+    [% LxERP.t8("Print destination") %]:
+    [% SET  printers = [ { description=LxERP.t8("Download PDF, do not print") } ] ;
+       CALL printers.import(SELF.printers);
+       L.select_tag("printer_id", printers, title_key="description", default=FORM.printer_id) %]
+   </p>
+  [% END %]
+
+  <p>
+   [% L.hidden_tag("action", "MassDeliveryOrderPrint/dispatch") %]
+   [% L.submit_tag("action_print", LxERP.t8("Print")) %]
+  </p>
+ </form>
+[%- END %]
+[%- END %]
index 794f518..e66b41f 100644 (file)
   <td>[% LxERP.t8("Number of invoices to create") %]:</td>
   <td>[% L.input_tag('', num_delivery_orders, size="5", id="cpa_number_of_invoices") %]</td>
  </tr>
-
+ <tr>
+  <td>[% LxERP.t8("Print both sided") %]:</td>
+  <td>[% L.checkbox_tag('', id="cpa_bothsided") %]</td>
+ </tr>
  <tr>
   <td>[% LxERP.t8("Print destination") %]:</td>
   <td>
index 1d906e1..08caf09 100644 (file)
@@ -1,6 +1,6 @@
 [%- USE L %][%- USE LxERP %][%- USE HTML %]
 <div>
- <form action="controller.pl" method="post">
+ <form action="controller.pl" method="post" id="search_form">
   <div class="filter_toggle" [% IF noshow == 0 %]style="display:none"[% END %]>
    <a href="#" onClick="javascript:$('.filter_toggle').toggle()">[% LxERP.t8('Show Filter') %]</a>
       [% SELF.filter_summary %]
      <th align="right">[% LxERP.t8('Customer') %]</th>
      <td>[% L.input_tag('filter.customer.name:substr::ilike', filter.customer.name_substr__ilike, size = 20) %]</td>
     </tr>
-     <th align="right">[% LxERP.t8('Transdate') %] [% LxERP.t8('From Date') %]</th>
+    <tr>
+     <th align="right">[% LxERP.t8('Customer type') %]</th>
+     <td>
+      [% L.select_tag('filter.customer.business_id', SELF.all_businesses,
+                      default    => filter.customer.business_id
+                      title_key  => 'description',
+                      value_key  => 'id',
+                      with_empty => 1,
+                      style      => 'width: 200px') %]
+     </td>
+    </tr>
+    <tr>
+     <th align="right">[% LxERP.t8('Delivery Order Date') %] [% LxERP.t8('From Date') %]</th>
      <td>[% L.date_tag('filter.transdate:date::ge', filter.transdate_date__ge) %]</td>
     </tr>
     <tr>
-     <th align="right">[% LxERP.t8('Transdate') %] [% LxERP.t8('To Date') %]</th>
+     <th align="right">[% LxERP.t8('Delivery Order Date') %] [% LxERP.t8('To Date') %]</th>
      <td>[% L.date_tag('filter.transdate:date::le', filter.transdate_date__le) %]</td>
     </tr>
   </table>
 
-   [% L.hidden_tag('action', 'MassInvoiceCreatePrint/dispatch') %]
+   [% L.hidden_tag('action', 'MassInvoiceCreatePrint/' _ LIST_ACTION, id='filter_action') %]
    [% L.hidden_tag('sort_by', FORM.sort_by) %]
    [% L.hidden_tag('sort_dir', FORM.sort_dir) %]
    [% L.hidden_tag('page', FORM.page) %]
-   [% L.submit_tag(LIST_ACTION, LxERP.t8('Continue'))%]
-
-   <a href="#" onClick="javascript:$('#filter_table input,#filter_table select').val('');">[% LxERP.t8('Reset') %]</a>
-
+   [% L.button_tag('$("#search_form").resetForm()', LxERP.t8('Reset')) %]
   </div>
 
  </form>
index 8bec62a..c382a5c 100644 (file)
@@ -4,8 +4,8 @@
 
 [%- INCLUDE "common/flash.html" %]
 
-[% LIST_ACTION     = 'action_list_invoices' %]
-[%- PROCESS 'mass_invoice_create_print_from_do/_filter.html' filter=SELF.filter %]
+[% LIST_ACTION     = 'list_invoices' %]
+[%- PROCESS 'mass_invoice_create_print_from_do/_filter.html' filter=SELF.invoice_models.filtered.laundered %]
 
 [% IF noshow == 1 %]
 [% invoices = SELF.invoice_models.get;
@@ -16,7 +16,7 @@
  </p>
 [%- ELSE %]
 
- <form method="post" action="controller.pl">
+ <form method="post" action="controller.pl" id="report_form">
   <table width="100%">
    <thead>
     <tr class="listheading">
    [% L.paginate_controls %]
   [% END %]
 
-  <hr size="3" noshade>
+  [% L.hidden_tag("action", "MassInvoiceCreatePrint/dispatch") %]
+  [% L.hidden_tag("printer_id") %]
+  [% L.hidden_tag("bothsided") %]
+ </form>
 
-  [% IF SELF.printers.size %]
+ [% IF SELF.printers.size %]
+  <div id="print_options" class="hidden">
+   <p>
+     [% LxERP.t8("Print both sided") %]:
+     [% L.checkbox_tag('', id="print_options_bothsided") %]
+   </p>
    <p>
     [% LxERP.t8("Print destination") %]:
     [% SET  printers = [ { description=LxERP.t8("Download PDF, do not print") } ] ;
        CALL printers.import(SELF.printers);
-       L.select_tag("printer_id", printers, title_key="description", default=FORM.printer_id) %]
+       L.select_tag("", printers, id="print_options_printer_id", title_key="description", default=FORM.printer_id) %]
    </p>
-  [% END %]
 
-  <p>
-   [% L.hidden_tag("action", "MassInvoiceCreatePrint/dispatch") %]
-   [% L.submit_tag("action_print", LxERP.t8("Print")) %]
-  </p>
- </form>
+   <p>
+    [% L.button_tag("kivi.MassInvoiceCreatePrint.massPrint()", LxERP.t8('Print')) %]
+   </p>
+  </div>
+ [% END %]
 [%- END %]
 [%- END %]
index 7d2ac03..b020743 100644 (file)
@@ -5,7 +5,7 @@
 
 [%- INCLUDE "common/flash.html" %]
 
-[% LIST_ACTION  = 'action_list_sales_delivery_orders' %]
+[% LIST_ACTION  = 'list_sales_delivery_orders' %]
 [% SET MODELS = SELF.sales_delivery_order_models;
        dummy  = MODELS.finalize %]
 
       <td>[% L.checkbox_tag('id[]', value=sales_delivery_order.id, "data-checkall"=1) %]</td>
       <td>[% HTML.escape(sales_delivery_order.transdate_as_date) %]</td>
       <td>[% L.link(SELF.url_for(controller="do.pl", action="edit", type="sales_delivery_order", id=sales_delivery_order.id), sales_delivery_order.donumber) %]</td>
-      <td>[% L.link(SELF.url_for(controller="oe.pl", action="edit", type="sales_order", id=sales_delivery_order.sales_order.id), sales_delivery_order.ordnumber) %]</td>
+      [%- IF INSTANCE_CONF.get_feature_experimental_order -%]
+        <td>[% L.link(SELF.url_for(controller="controller.pl", action="Order/edit", type="sales_order", id=sales_delivery_order.sales_order.id), sales_delivery_order.ordnumber) %]</td>
+      [%- ELSE -%]
+        <td>[% L.link(SELF.url_for(controller="oe.pl", action="edit", type="sales_order", id=sales_delivery_order.sales_order.id), sales_delivery_order.ordnumber) %]</td>
+      [%- END -%]
       <td>[% HTML.escape(sales_delivery_order.customer.name) %]</td>
      </tr>
      [%- END %]
 
     [% L.paginate_controls %]
 
-    <hr size="3" noshade>
-
-    <p>
-     [% L.hidden_tag("action", "MassInvoiceCreatePrint/create_invoices") %]
-     [% L.button_tag("", LxERP.t8("Create invoices"), name="create_button") %]
-     [% L.button_tag("", LxERP.t8("For all delivery orders create and print invoices"), name="create_print_all_button") %]
-    </p>
     <div id="create_print_all_dialog" style="display: none;">
      [%- INCLUDE 'mass_invoice_create_print_from_do/_create_print_all_step_1.html' %]
     </div>
index f7ba0ee..09db656 100644 (file)
@@ -5,12 +5,6 @@
  <span class="frame-header-element frame-header-left">
     [<a href="controller.pl?action=LoginScreen/user_login" target="_blank" title="[% 'Open a further kivitendo window or tab' | $T8 %]">[% 'New window/tab' | $T8 %]</a>]
     [<a href="JavaScript:top.print();" title="[% 'Hardcopy' | $T8 %]">[% 'Print' | $T8 %]</a>]
-[%- IF AUTH.assert('customer_vendor_edit|customer_vendor_edit_all', 1) %]
-    [<input name="frame_header_contact_search" id="frame_header_contact_search" placeholder="[% 'Search contacts' | $T8 %]" size="14">]
-[%- END %]
-[%- IF AUTH.assert('general_ledger', 1) %]
-    [<input id="glquicksearch" name="glquicksearch" type="text" class="ui-widget" placeholder="[% 'GL search' | $T8 %]" maxlength="20">]
-[%- END %]
  </span>
 [%- END %]
  <span class="frame-header-element frame-header-right">
    <a href="controller.pl?action=LoginScreen/logout" target="_top" title="[% 'Logout now' | $T8 %]">[% 'Logout' | $T8 %]</a>]
  </span>
  <span class="frame-header-element frame-header-right" id="ajax-spinner">
-  <img src="image/[% IF MYCONFIG.stylesheet == 'lx-office-erp.css' %]spinner-blue.gif[% ELSE %]spinner-white.gif[% END %]" alt="[% LxERP.t8('Loading...') %]">
+  <img src="image/spinner-white.gif" alt="[% LxERP.t8('Loading...') %]">
  </span>
+[%- FOREACH search = quick_search.enabled_modules %]
+   <span class='frame-header-quicksearch'><input id="top-quick-search-[% search.name %]" module="[% search.name %]" placeholder="[% search.description_field %]" maxlength="20"></span>
+[%- END %]
+
 </div>
 [%- END %]
diff --git a/templates/webpages/menu/menu.html b/templates/webpages/menu/menu.html
deleted file mode 100644 (file)
index f75035a..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-[%- USE JSON %]
-$(function(){$([% JSON.json(sections) %]).each(function(i,b){var a=$('<a class="ml">').append($('<span class="mii ms">').append($('<div>').addClass(b[3])),$('<span class="mic">').append(b[0]));if(b[5])a.attr('href', b[5]);if(b[6])a.attr('target', b[6]);$('#html-menu').append($('<div class="mi">').addClass(b[4]).addClass(b[1]).attr('id','mi'+b[2]).append(a))});$('#html-menu div.i, #html-menu div.sm').not('[id^='+$.cookie('html-menu-selection')+'_]').hide();$('#html-menu div.m').each(function(){$(this).click(function(){$.cookie('html-menu-selection',$(this).attr('id'));$('#html-menu div.mi').not('div.m').not('[id^='+$(this).attr('id')+'_]').hide();$('#html-menu div.mi[id^='+$(this).attr('id')+'_]').toggle()})})})
diff --git a/templates/webpages/menu/menunew.html b/templates/webpages/menu/menunew.html
deleted file mode 100644 (file)
index df3ed7b..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-[%- USE T8 %]
-[%- USE L %]
-[%- USE HTML %]
-[%- USE LxERP -%]
- <div id="main_menu_div"></div>
- [%- SET main_id = '100' %]
- <ul id="main_menu_model"  style='display:none'>
- [%- FOREACH node = menu.tree %]
-  [%- NEXT UNLESS node.visible %]
-  [%- SET main_id = main_id + 1 %]
-  <li id="[% main_id %]"[% IF icon_path(node.icon) %] itemIcon="[% icon_path(node.icon) %]"[% END %]>
-   [% L.link(menu.href_for_node(node) || '#', menu.name_for_node(node), target=node.target) %]
-   [%- IF node.children %]
-    <ul width="[% max_width(node) %]">
-     [%- SET sub1_id = main_id * 100 %]
-     [%- FOREACH sub1node = node.children %]
-      [%- NEXT UNLESS sub1node.visible %]
-      [%- SET sub1_id = sub1_id + 1 %]
-      <li id="[% sub1_id %]"[% IF icon_path(sub1node.icon) %] itemIcon="[% icon_path(sub1node.icon) %]"[% END %]>
-       [% L.link(menu.href_for_node(sub1node) || '#', menu.name_for_node(sub1node), target=sub1node.target) %]
-       [%- IF sub1node.children %]
-        <ul width="[% max_width(sub1node) %]">
-         [%- SET sub2_id = sub1_id * 100 %]
-         [%- FOREACH sub2node = sub1node.children %]
-          [%- NEXT UNLESS sub2node.visible %]
-          [%- SET sub2_id = sub2_id + 1 %]
-          <li id="[% sub2_id %]"[% IF icon_path(sub2node.icon) %] itemIcon="[% icon_path(sub2node.icon) %]"[% END %]>
-            [% L.link(menu.href_for_node(sub2node) || '#', menu.name_for_node(sub2node), target=sub2node.target) %]
-          </li>
-         [%- END %]
-        </ul>
-       [%- END %]
-      </li>
-     [%- END %]
-    </ul>
-   [%- END %]
-  </li>
- [%- END %]
- </ul>
index b2a5b16..8160188 100644 (file)
     <td valign="top">[% HTML.escape(CFDD_shiptocountry) %]</td>
    </tr>
 
+   <tr>
+    <th align="right" valign="top">[% 'GLN' | $T8 %]:</th>
+    <td valign="top">[% HTML.escape(CFDD_shiptogln) %]</td>
+   </tr>
+
    <tr>
     <th align="right" valign="top">[% 'Contact' | $T8 %]:</th>
     <td valign="top">[% HTML.escape(CFDD_shiptocontact) %]</td>
     <th align="right" valign="top">[% 'E-mail' | $T8 %]:</th>
     <td valign="top">[% HTML.escape(CFDD_shiptoemail) %]</td>
    </tr>
+
+[% FOREACH var = cvars %]
+   <tr valign="top">
+    <th align="right" nowrap>[% HTML.escape(var.config.description) %]</th>
+    <td>[% HTML.escape(var.value_as_text) %]</td>
+   </tr>
+[% END %]
   </table>
  </p>
 
index 8ee4952..94aeb5b 100644 (file)
@@ -2,28 +2,43 @@
 [% USE LxERP %]
 [% USE L %]
 [% SET style="width: 400px" %]
+
+[%- IF !popup_dialog -%]
 <h1>[% title %]</h1>
+[%- END -%]
 
  <form name="Form" action="oe.pl" method="post">
+  [%- IF popup_dialog -%]
+    [% L.button_tag(popup_js_assign_function, LxERP.t8('Assign')) %]
+    [% L.button_tag(popup_js_close_function, LxERP.t8('Cancel')) %]
+
+  [%- ELSE -%]
+    [% L.hidden_tag('action', 'save_periodic_invoices_config') %]
+
+    <p>
+     [% L.submit_tag('', LxERP.t8('Assign')) %]
+     [% L.submit_tag('', LxERP.t8('Cancel'), onclick => "self.close(); return false;") %]
+    </p>
+  [%- END -%]
 
   <p>
    <table border="0">
     <tr>
      <th align="right">[% LxERP.t8('Status') %]</th>
-     <td>[% L.checkbox_tag("active", checked => active, label => LxERP.t8('Active')) %]</td>
+     <td>[% L.checkbox_tag("active", checked => config.active, label => LxERP.t8('Active')) %]</td>
     </tr>
 
     <tr>
      <td>&nbsp;</td>
      <td>
-      [% L.checkbox_tag('terminated', label => LxERP.t8('terminated'), checked => terminated) %]
+      [% L.checkbox_tag('terminated', label => LxERP.t8('terminated'), checked => config.terminated) %]
      </td>
     </tr>
 
     <tr>
      <th align="right" valign="top">[%- LxERP.t8('Billing Periodicity') %]</th>
      <td valign="top">
-      [% L.select_tag("periodicity", [ [ "m", LxERP.t8("monthly") ], [ "q", LxERP.t8("every third month") ], [ "b", LxERP.t8("semiannually") ], [ "y", LxERP.t8("yearly") ] ], default=periodicity, style=style) %]
+      [% L.select_tag("periodicity", [ [ "o", LxERP.t8("one time") ], [ "m", LxERP.t8("monthly") ], [ "q", LxERP.t8("every third month") ], [ "b", LxERP.t8("semiannually") ], [ "y", LxERP.t8("yearly") ] ], default=config.periodicity, style=style) %]
      </td>
     </tr>
 
       [% L.select_tag("order_value_periodicity",
                       [ [ "p", LxERP.t8("same as periodicity") ], [ "m", LxERP.t8("monthly") ], [ "q", LxERP.t8("every third month") ], [ "b", LxERP.t8("semiannually") ], [ "y", LxERP.t8("yearly") ],
                         [ "2", LxERP.t8("2 years") ], [ "3", LxERP.t8("3 years") ], [ "4", LxERP.t8("4 years") ], [ "5", LxERP.t8("5 years") ], ],
-                      default=order_value_periodicity, style=style) %]
+                      default=config.order_value_periodicity, style=style) %]
      </td>
     </tr>
 
     <tr>
      <th align="right">[%- LxERP.t8('Start date') %]</th>
      <td valign="top">
-      [% L.date_tag("start_date_as_date", start_date_as_date) %]
+      [% L.date_tag("start_date_as_date", config.start_date_as_date) %]
      </td>
     </tr>
 
     <tr>
      <th align="right">[%- LxERP.t8('End date') %]<sup>(1)</sup></th>
      <td valign="top">
-      [% L.date_tag("end_date_as_date", end_date_as_date) %]
+      [% L.date_tag("end_date_as_date", config.end_date_as_date) %]
      </td>
     </tr>
 
     <tr>
      <th align="right">[%- LxERP.t8('Create first invoice on') %]<sup>(2)</sup></th>
      <td valign="top">
-      [% L.date_tag("first_billing_date_as_date", first_billing_date_as_date) %]
+      [% L.date_tag("first_billing_date_as_date", config.first_billing_date_as_date) %]
      </td>
     </tr>
 
     <tr>
      <th align="right">[% LxERP.t8('Extend automatically by n months') %]</th>
      <td valign="top">
-      [% L.input_tag("extend_automatically_by", extend_automatically_by, size => 10) %]
+      [% L.input_tag("extend_automatically_by", config.extend_automatically_by, size => 10) %]
      </td>
     </tr>
 
     <tr>
      <th align="right">[%- LxERP.t8('Record in') %]</th>
      <td valign="top">
-      [% L.select_tag("ar_chart_id", AR, title_key => 'description', default => ar_chart_id, style=style) %]
+      [% L.select_tag("ar_chart_id", AR, title_key => 'description', default => config.ar_chart_id, style=style) %]
      </td>
     </tr>
 
     <tr>
      <th align="right">[%- LxERP.t8('direct debit') %]</th>
-     <td valign="top">[% L.checkbox_tag("direct_debit", checked=direct_debit) %]</td>
+     <td valign="top">[% L.checkbox_tag("direct_debit", checked=config.direct_debit) %]</td>
     </tr>
 
-    <tr>
+    <tr class="rule-before">
      <th align="right">[%- LxERP.t8('Print automatically') %]</th>
      <td valign="top">
-      [% L.checkbox_tag("print", onclick => "toggle_printer_id_ctrl()", checked => print) %]
+      [% L.checkbox_tag("print", onclick => "toggle_printer_id_ctrl()", checked => config.print) %]
      </td>
     </tr>
 
     <tr>
      <th align="right">[%- LxERP.t8('Printer') %]</th>
      <td valign="top">
-      [% L.select_tag("printer_id", ALL_PRINTERS, title_key = 'printer_description', default = printer_id, disabled = !print, style=style) %]
+      [% L.select_tag("printer_id", ALL_PRINTERS, title_key = 'printer_description', default = config.printer_id, disabled = !config.print, id = "pic_printer_id", style=style) %]
      </td>
     </tr>
 
     <tr>
      <th align="right">[%- LxERP.t8('Copies') %]</th>
-     <td valign="top">[% L.input_tag("copies", copies, size => 6, disabled => !print) %]</td>
+     <td valign="top">[% L.input_tag("copies", config.copies, size => 6, disabled => !config.print, id = "pic_copies") %]</td>
+    </tr>
+
+    <tr class="rule-before">
+     <th align="right">[%- LxERP.t8("Send invoice via email") %]</th>
+     <td>[% L.checkbox_tag("send_email", onclick => "toggle_send_email_ctrl()", checked=config.send_email, disabled=postal_invoice) %]</td>
+    </tr>
+    <tr>
+     <th align="right">[%- LxERP.t8("Email of the invoice recipient") %]</th>
+     <td>[% email_recipient_invoice_address %]</td>
+    </tr>
+    <tr>
+     <th align="right">[%- LxERP.t8("Contact to send to") %]</th>
+     <td>[% L.select_tag("email_recipient_contact_id", ALL_CONTACTS, title_key="full_name_dep", value_key="cp_id", default=config.email_recipient_contact_id, with_empty=1, disabled=!config.send_email, style=style) %]</td>
+    </tr>
+
+    <tr>
+     <th align="right">[%- LxERP.t8("Other recipients") %]<sup>3</sup></th>
+     <td>[% L.input_tag("email_recipient_address", config.email_recipient_address, disabled=!config.send_email, style=style) %]</td>
+    </tr>
+
+    <tr>
+     <th align="right">[%- LxERP.t8("Sender") %]<sup>4</sup></th>
+     <td>[% L.input_tag("email_sender", config.email_sender, disabled=!config.send_email, style=style) %]</td>
+    </tr>
+
+    <tr>
+     <th align="right">[%- LxERP.t8("Subject") %]</th>
+     <td>[% L.input_tag("email_subject", config.email_subject, disabled=!config.send_email, style=style) %]</td>
+    </tr>
+
+    <tr>
+     <th align="right" valign="top">[%- LxERP.t8("Message") %]</th>
+     <td valign="top">[% L.textarea_tag("email_body", config.email_body, disabled=!config.send_email, rows=8, style=style, class="texteditor texteditor-space-for-toolbar") %]</td>
     </tr>
    </table>
   </p>
 
   <p>(1): [%- LxERP.t8('The end date is the last day for which invoices will possibly be created.') %]</p>
   <p>(2): [% LxERP.t8("If missing then the start date will be used.") %]</p>
-
-  [% L.hidden_tag('action', 'save_periodic_invoices_config') %]
-
-  <p>
-   [% L.submit_tag('', LxERP.t8('Close')) %]
-   [% L.submit_tag('', LxERP.t8('Cancel'), onclick => "self.close(); return false;") %]
-  </p>
+  <p>(3): [% LxERP.t8("Multiple addresses can be entered separated by commas.") %]</p>
+  <p>(4): [% LxERP.t8("If left empty the default sender from the kivitendo configuration will be used (key 'email_from' in section 'periodic_invoices'; current value: #1).", HTML.escape(LXCONFIG.periodic_invoices.email_from)) %]</p>
  </form>
 
  <script type="text/javascript">
   <!--
     function toggle_printer_id_ctrl() {
       var disabled = !$('#print').prop('checked');
-      $('#printer_id').prop('disabled', disabled);
-      $('#copies').prop('disabled', disabled);
+      $('#pic_printer_id').prop('disabled', disabled);
+      $('#pic_copies').prop('disabled', disabled);
+    }
+
+    function toggle_send_email_ctrl() {
+      var disabled = !$('#send_email').prop('checked');
+      $('#email_recipient_contact_id').prop('disabled', disabled);
+      $('#email_recipient_address').prop('disabled', disabled);
+      $('#email_sender').prop('disabled', disabled);
+      $('#email_subject').prop('disabled', disabled);
+      $('#email_body').data('ckeditorInstance').setReadOnly(disabled);
     }
     -->
  </script>
index 0956f20..14f2954 100644 (file)
                 <th align="left">[% 'Internal Notes' | $T8 %]</th>
               </tr>
               <tr valign="top">
-                <td>[% notes %]</td>
-                <td>[% intnotes %]</td>
+                <td>[% L.textarea_tag('notes',    notes,    style="width: 350px; height: 150px", class="texteditor") %]</td>
+                <td>[% L.textarea_tag('intnotes', intnotes, style="width: 350px; height: 150px") %]</td>
               </tr>
               <tr>
                 <th align="right">[% 'Payment Terms' | $T8 %]</th>
-                <td>
-                      [%- INCLUDE 'generic/multibox.html'
-                           name       = 'payment_id',
-                           style      = 'width: 250px',
-                           DATA       = ALL_PAYMENTS,
-                           id_key     = 'id',
-                           label_key  = 'description',
-                           show_empty = 1 -%]
-                </td>
+                <td>[% L.select_tag('payment_id', ALL_PAYMENTS, default = payment_id, title_key = 'description', with_empty = 1, style="width: 250px") %]</td>
               </tr>
               <tr>
                 <th align="right">[% 'Delivery Terms' | $T8 %]</th>
               </tr>
 [%- END %]
               [% tax %]
+[%- IF rounding %]
+              <tr>
+                <th align='right'>[% 'Rounding' | $T8 %]</th>
+                <td align='right'>[% LxERP.format_amount(rounding, 2) %]</td>
+              </tr>
+[%- END %]
               <tr>
                 <th align="right">[% 'Total' | $T8 %]</th>
                 <td align="right">[% LxERP.format_amount(invtotal, 2) %]
 </div>
 </div>
 
-<hr size="3" noshade>
-
-<p>[% print_options %]</p>
-
-[% label_edit %]<br>
-<input class="submit" type="submit" name="action_update" id="update_button" value="[% 'Update' | $T8 %]">
-<input class="submit" type="submit" name="action_ship_to" value="[% 'Ship to' | $T8 %]">
-<input class="submit" type="submit" name="action_print" value="[% 'Print' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-<input class="submit" type="submit" name="action_e_mail" value="[% 'E-mail' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-
-[% IF not tpca_reminder %]
-  <input class="submit" type="submit" name="action_save" value="[% 'Save' | $T8 %]"[% IF warn_save_active_periodic_invoice %] data-warn-save-active-periodic-invoice="1"[% END %] data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-  <input class="submit" type="submit" name="action_save_and_close" value="[% 'Save and Close' | $T8 %]"[% IF warn_save_active_periodic_invoice %] data-warn-save-active-periodic-invoice="1"[% END %] data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-[% ELSE %]
-  [% IF warn_save_active_periodic_invoice  %] [% warn_save_active_periodic_invoice=1 %] [% END %]
-    [% L.submit_tag('action_save', LxERP.t8('Save'), confirm=LxERP.t8('Missing transport cost: #1  Are you sure?', tpca_reminder), 'data-require-transaction-description'=INSTANCE_CONF.get_require_transaction_description_ps, 'data-warn-save-active-periodic-invoice'=warn_save_active_periodic_invoice) %]
-    [% L.submit_tag('action_save_and_close', LxERP.t8('Save and close'), confirm=LxERP.t8('Missing transport cost: #1  Are you sure?', tpca_reminder), 'data-require-transaction-description'=INSTANCE_CONF.get_require_transaction_description_ps, 'data-warn-save-active-periodic-invoice'=warn_save_active_periodic_invoice) %]
-[% END %]
-
-[%- IF id %]
-  <input type="button" class="submit" onclick="follow_up_window()" value="[% 'Follow-Up' | $T8 %]">
-  <input type="button" class="submit" onclick="set_history_window([% HTML.escape(id) %], 'id')" name="history" id="history" value="[% 'history' | $T8 %]">
-
-  <br>[% label_workflow %]<br>
-  <input class="submit" type="submit" name="action_save_as_new" value="[% 'Save as new' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
-
-  [%- UNLESS (is_sales_ord && !INSTANCE_CONF.get_sales_order_show_delete) || (is_pur_ord && !INSTANCE_CONF.get_purchase_order_show_delete) %]
-    [% L.submit_tag('action_delete', LxERP.t8('Delete'), confirm=LxERP.t8('Are you sure?')) %]
-  [%- END %]
-
-  [%- IF is_sales_quo %]
-    <input class="submit" type="submit" name="action_sales_order" value="[% 'Sales Order' | $T8 %]">
-  [%- END %]
-
-  [%- IF is_req_quo %]
-    <input class="submit" type="submit" name="action_purchase_order" value="[% 'Purchase Order' | $T8 %]">
-  [%- END %]
-
-  [%- IF is_sales_ord || is_pur_ord %]
-    <input class="submit" type="submit" name="action_delivery_order" value="[% 'Delivery Order' | $T8 %]">
-  [%- END %]
-
-  [%- IF allow_invoice %]
-  <input class="submit" type="submit" name="action_invoice" value="[% 'Invoice' | $T8 %]">
-  [%- END %]
-
-  [%- IF is_sales_ord || is_pur_ord %]
-    <br>[% heading %] [% 'to be used as template for' | $T8 %]<br>
-    [%- IF is_sales_ord %]
-      <input class="submit" type="submit" name="action_purchase_order" value="[% 'Purchase Order' | $T8 %]">
-     <input class="submit" type="submit" name="action_quotation" value="[% 'Quotation' | $T8 %]">
-    [%- ELSE %]
-    [%- IF is_pur_ord %]
-      <input class="submit" type="submit" name="action_sales_order" value="[% 'Sales Order' | $T8 %]">
-     <input class="submit" type="submit" name="action_request_for_quotation" value="[% 'Request for Quotation' | $T8 %]">
-    [%- END %]
-    [%- END %]
-  [%- END %]
-[%- END %]
-<input type="hidden" name="action" value="dispatcher">
 <input type="hidden" name="saved_xyznumber" value="[% HTML.escape(saved_xyznumber) %]">
-<input type="hidden" name="rowcount" value="[% HTML.escape(rowcount) %]">
+[% L.hidden_tag("rowcount", rowcount) %]
 <input type="hidden" name="callback" value="[% callback | html %]">
 [% IF vc == 'customer' %]
   <input type="hidden" name="customer_discount" value="[% HTML.escape(customer_discount) %]">
   <input type="hidden" name="vendor_discount" value="[% HTML.escape(vendor_discount) %]">
 [% END %]
 
+<div id="shipto_inputs" class="hidden">
+ [%- PROCESS 'common/_ship_to_dialog.html' cvars=shipto_cvars %]
+</div>
+
+<div id="email_inputs" class="hidden"></div>
+
+<div id="print_options" class="hidden">
+ [% print_options %]
+</div>
 </form>
-<script type='text/javascript'>
- $(kivi.SalesPurchase.init_on_submit_checks);
-</script>
+
+<div id="shipto_dialog" class="hidden"></div>
+<div id="print_dialog" class="hidden">
+ [%- PROCESS 'common/_print_dialog.html' %]
+</div>
index 3d1b307..002fa55 100644 (file)
@@ -1,25 +1,25 @@
 [%- USE T8 %]
 [%- USE HTML %]
 [%- USE LxERP %]
-[%- USE L %]
+[%- USE L %][%- USE P -%]
 
-  <form method="post" name="oe" action="[% script %]">
+  <form method="post" id='form' name="oe" action="[% script %]"
+        data-transport-cost-reminder-article-id="[% HTML.escape(transport_cost_reminder_article.id) %]"
+        data-transport-cost-reminder-article-description="[% HTML.escape(transport_cost_reminder_article.displayable_name) %]"
+        >
 
     <script type="text/javascript" src="js/delivery_customer_selection.js"></script>
-    <script type="text/javascript" src="js/vendor_selection.js"></script>
     <script type="text/javascript" src="js/calculate_qty.js"></script>
-    <script type="text/javascript" src="js/customer_or_vendor_selection.js"></script>
     <script type="text/javascript" src="js/follow_up.js"></script>
     [%- IF is_sales_ord %]
      [% L.javascript_tag("js/edit_periodic_invoices_config") %]
     [%- END %]
 
 [%- FOREACH row = HIDDENS %]
-   <input type="hidden" name="[% HTML.escape(row.name) %]" value="[% HTML.escape(row.value) %]" >
+   <input type="hidden" name="[% HTML.escape(row.name) %]" id="[% HTML.escape(row.name) %]" value="[% HTML.escape(row.value) %]" >
 [%- END %]
 
     <input type="hidden" name="convert_from_oe_ids" value="[% HTML.escape(convert_from_oe_ids) %]">
-    <input type="hidden" name="convert_from_ar_ids" value="[% HTML.escape(convert_from_ar_ids) %]">
 
     <input type="hidden" name="follow_up_trans_id_1" value="[% HTML.escape(id) %]">
     <input type="hidden" name="follow_up_trans_type_1" value="[% HTML.escape(type) %]">
 [%- IF INSTANCE_CONF.get_webdav %]
       <li><a href="#ui-tabs-webdav">[% 'WebDAV' | $T8 %]</a></li>
 [%- END %]
-[%- IF id %]
+[%- IF id AND INSTANCE_CONF.get_doc_storage %]
+      <li><a href="controller.pl?action=File/list&file_type=document&object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">[% 'Documents' | $T8 %]</a></li>
+      <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">[% 'Attachments' | $T8 %]</a></li>
+[%- END %]
+[%- IF id AND AUTH.assert('record_links', 1) %]
       <li><a href="controller.pl?action=RecordLinks/ajax_list&object_model=Order&object_id=[% HTML.url(id) %]">[% 'Linked Records' | $T8 %]</a></li>
 [%- END %]
      </ul>
                   <tr>
                     <th align="right">[% IF vc == 'customer' %][% 'Customer' | $T8 %][% ELSE %][% 'Vendor' | $T8 %][% END %]</th>
                     <td>
-                      [%- INCLUDE 'generic/multibox.html'
-                           name          = vc,
-                           style         = 'width: 250px',
-                           class         = 'initial_focus',
-                           DATA          = vc == 'customer' ? ALL_CUSTOMERS : ALL_VENDORS,
-                           id_sub        = 'vc_keys',
-                           label_key     = 'name',
-                           select        = vc_select,
-                           limit         = vclimit,
-                           allow_textbox = 1,
-                           onChange      = "document.getElementById('update_button').click();" -%]
-                      <input type="button" value="[% 'Details (one letter abbreviation)' | $T8 %]" onclick="show_vc_details('[% HTML.escape(vc) %]')">
+                     [%- SET vc_id = vc _ "_id" %]
+                     [% P.customer_vendor.picker(vc_id, $vc_id, type=vc, style="width: 250px", class="initial_focus", onchange="\$('#update_button').click()") %]
+                     [% P.button_tag("show_vc_details('" _ HTML.escape(vc) _  "')", LxERP.t8("Details (one letter abbreviation)")) %]
+                     [% P.hidden_tag("previous_" _ vc_id, $vc_id) %]
                     </td>
                   </tr>
 [%- IF ALL_CONTACTS.size %]
                     </td>
                   </tr>
 [%- END %]
-[%- IF ALL_SHIPTO.size %]
                   <tr>
                     <th align="right">[% 'Shipping Address' | $T8 %]</th>
                     <td>
+[%- IF ALL_SHIPTO.size %]
                       [% shiptos = [ [ "", LxERP.t8("No/individual shipping address") ] ] ;
                          L.select_tag('shipto_id', shiptos.import(ALL_SHIPTO), default=shipto_id, value_key='shipto_id', title_key='displayable_id', style='width: 250px') %]
+[%- END %]
+                      [% L.button_tag("kivi.SalesPurchase.edit_custom_shipto()", LxERP.t8("Custom shipto")) %]
+                    </td>
+                  </tr>
+
+[%- IF is_sales && vc_obj.additional_billing_addresses.as_list.size %]
+                  <tr>
+                    <th align="right">[% 'Custom Billing Address' | $T8 %]</th>
+                    <td>
+                      [% L.select_tag('billing_address_id', vc_obj.additional_billing_addresses,
+                                      with_empty=1, default=billing_address_id, value_key='id', title_key='displayable_id', style='width: 250px') %]
                     </td>
                   </tr>
 [%- END %]
+
 [%- IF is_order %]
                   <tr>
                     <td align="right">[% 'Credit Limit' | $T8 %]</td>
                       [% L.select_tag('taxzone_id', ( id ? ALL_TAXZONES : ALL_ACTIVE_TAXZONES), default=taxzone_id, title_key='description', style='width: 250px') %]
                     </td>
                   </tr>
-[%- IF ALL_DEPARTMENTS %]
+[%- IF ALL_LANGUAGES.size %]
+                  <tr>
+                    <th align="right" nowrap>[% 'Language' | $T8 %]</th>
+                    <td colspan="3">
+                      [% L.select_tag('language_id', ALL_LANGUAGES, default=language_id, title_key = 'description', with_empty=1, style='width:250px') %]
+                    </td>
+                  </tr>
+[%- END %]
+[%- IF ALL_DEPARTMENTS.size %]
                   <tr>
                     <th align="right" nowrap>[% 'Department' | $T8 %]</th>
                     <td colspan="3">
-                      [% L.select_tag('department_id', ALL_DEPARTMENTS, default=department_id, title_sub=\department_labels, with_empty=1, style='width:250px') %]
+                      [% L.select_tag('department_id', ALL_DEPARTMENTS, default=department_id, title_key = 'description', with_empty=1, style='width:250px') %]
                     </td>
                   </tr>
 [%- END %]
-[%- IF currencies %]
                   <tr>
                     <th align="right">[% 'Currency' | $T8 %]</th>
-                    <td>[% currencies %]</td>
+                    <td>[% L.select_tag("currency", ALL_CURRENCIES, value_key="name", default=currency, onchange="document.getElementById('update_button').click();") %]</td>
                   </tr>
-[%- END %]
 [%- IF show_exchangerate %]
                   <tr>
                     <th align="right">[% 'Exchangerate' | $T8 %]</th>
                     <td>
                      [%- IF forex %]
-                      [% LxERP.format_amount(exchangerate, 2) %]
+                      [% LxERP.format_amount(exchangerate, 5) %]
                      [%- ELSE %]
                       <input name="exchangerate" size="10" value="[% HTML.escape(LxERP.format_amount(exchangerate)) %]">
                      [%- END %]
                   </tr>
                   <tr>
                     <th align="right">[% 'Transaction description' | $T8 %]</th>
-                    <td colspan="3"><input name="transaction_description" id="transaction_description" size="35" value="[% HTML.escape(transaction_description) %]"></td>
+                    <td colspan="3">[% L.input_tag("transaction_description", transaction_description, size=35, "data-validate"=INSTANCE_CONF.get_require_transaction_description_ps ? 'required' : '') %]</td>
                   </tr>
 [%- IF show_delivery_customer %]
                   <tr>
 [%- IF is_order %]
                   <tr>
                     <th width="70%" align="right" nowrap>[% 'Order Number' | $T8 %]</th>
-                    <td><input name="ordnumber" size="11" value="[% HTML.escape(ordnumber) %]"></td>
+                    <td>
+[%- IF INSTANCE_CONF.get_sales_purchase_record_numbers_changeable %]
+                      [% L.input_tag("ordnumber", ordnumber, size="11") %]
+[%- ELSIF id %]
+                      [% HTML.escape(ordnumber) %]
+                      [% L.hidden_tag("ordnumber", ordnumber) %]
+[%- ELSE %]
+                      [% LxERP.t8("will be set upon saving") %]
+[%- END %]
+                    </td>
                   </tr>
 [%- END %]
                   <tr>
                     <th width="70%" align="right" nowrap>[% IF is_req_quo %][% 'RFQ Number' | $T8 %][% ELSE %][% 'Quotation Number' | $T8 %][% END %]</th>
-                    <td><input name="quonumber" size="11" value="[% HTML.escape(quonumber) %]"></td>
+                    <td>
+[%- IF is_order || INSTANCE_CONF.get_sales_purchase_record_numbers_changeable %]
+                      [% L.input_tag("quonumber", quonumber, size="11") %]
+[%- ELSIF id %]
+                      [% HTML.escape(quonumber) %]
+                      [% L.hidden_tag("quonumber", quonumber) %]
+[%- ELSE %]
+                      [% LxERP.t8("will be set upon saving") %]
+[%- END %]
+                    </td>
                   </tr>
 [%- IF is_order %]
                   <tr>
                     <th width="70%" align="right" nowrap>[% 'Customer Order Number' | $T8 %]</th>
-                    <td><input name="cusordnumber" size="11" value="[% HTML.escape(cusordnumber) %]"></td>
+                    <td><input name="cusordnumber" id="cusordnumber" size="11" value="[% HTML.escape(cusordnumber) %]"></td>
                   </tr>
 [%- END %]
                   <tr>
                       [% L.date_tag('transdate', transdate, id='transdate') %]
                     </td>
                   </tr>
+                  <tr>
+                    <th align="right" nowrap>[% LxERP.t8('Tax point') %]</th>
+                    <td nowrap>[% L.date_tag('tax_point', tax_point, id='tax_point') %]</td>
+                  </tr>
                   <tr>
                     <th align="right" nowrap>
                      [%- IF is_sales_quo %]
                   [%- IF is_sales_ord %]
                   <tr>
                     <th align="right" nowrap>[% 'Insert Date' | $T8 %]</th>
-                    <td>[% insertdate %]</td>
+                    <td>[% oe_obj.itime_as_date %]</td>
                   </tr>
                   [%- END %]
                   <tr>
                   <tr>
                     <th width="70%" align="right" nowrap>[% 'Expected billing date' | $T8 %]</th>
                     <td nowrap>
-                      [%- L.date_tag('expected_billing_date', expected_billing_date 'BL') %]
+                      [%- L.date_tag('expected_billing_date', expected_billing_date) %]
                     </td>
                   </tr>
 [%- END %]
index 5ceb69c..704374f 100644 (file)
@@ -1,10 +1,5 @@
 [%- USE T8 %]
 [% USE HTML %]
- [%- IF SHOW_CONTINUE_BUTTON %]
- [% 'New sales order' | $T8 %]<br>
- <input class="submit" type="submit" name="action" value="[% 'Continue' | $T8 %]">
- [%- END %]
- <input type="hidden" name="nextsub" value="edit">
  <input type="hidden" name="type" value="[% HTML.escape(type) %]">
  <input type="hidden" name="vc" value="[% HTML.escape(vc) %]">
  <input type="hidden" name="callback" value="[% HTML.escape(callback) %]">
index f6bc1de..b94d6f5 100644 (file)
@@ -1 +1 @@
-<form method="post" action="oe.pl">
+<form method="post" action="oe.pl" id="form">
index 15d6039..5ec1495 100644 (file)
@@ -1,4 +1,4 @@
-Sehr geehrter Benutzer,
+Guten Tag
 
 die folgenden wiederkehrenden Rechnungen wurden automatisch erzeugt:
 
@@ -9,3 +9,31 @@ Davon wurden die folgenden Rechnungen automatisch ausgedruckt:
 
 [% FOREACH inv = PRINTED_INVOICES %][% inv.invnumber %] [% END %]
 [%- END %]
+
+[% IF EMAILED_INVOICES.size -%]
+Davon wurden die folgenden Rechnungen automatisch per E-Mail versandt:
+
+[% FOREACH inv = EMAILED_INVOICES %][% inv.invnumber %] [% END %]
+[%- END %]
+
+[% IF DISABLED_ORDERS.size -%]
+Bei folgenden Auftragsnummern, wurde die Konfiguration auf inaktiv (Periodenwahl 'einmalig') gesetzt.
+
+[% FOREACH disabled_order = DISABLED_ORDERS %][% disabled_order %] [% END %]
+[%- END %]
+[% IF PRINTED_FAILED.size %]
+
+Beim Drucken der folgenden Rechnungen gab es Fehler:
+
+[% FOREACH invoice = PRINTED_FAILED %]
+* [% invoice.0.invnumber %]: [% invoice.1 %]
+[% END %]
+[% END %]
+[% IF EMAILED_FAILED.size %]
+
+Beim Versand der folgenden Rechnungen per E-Mail gab es Fehler:
+
+[% FOREACH invoice = EMAILED_FAILED %]
+* [% invoice.0.invnumber %]: [% invoice.1 %]
+[% END %]
+[% END %]
index c8b85c6..7cfd7f8 100644 (file)
@@ -35,7 +35,7 @@
     <td><b>[% 'Selected' | $T8 %]</b></td>
 [% END %]
      <td>[% price.source_description | html %]</td>
-     <td>[% price.price_as_number %]</td>
+     <td class="numeric">[% price.price_as_number %]</td>
 [% IF price.source == best_price.source %]
      <td align='center'>&#x2022;</td>
 [% ELSE %]
index 76587d7..4c72780 100644 (file)
@@ -28,7 +28,7 @@
 [%- FOREACH row = ROWS %]
      <tr valign="top" class="row [% IF row.error %]error_message[% ELSE %]listrow[% loop.count % 2 %][% END %]">
  [%- FOREACH row1 = row.ROW1 %]
-      <td[% IF row1.align %] align="[% row1.align %]"[% END %][% IF row1.nowrap %] nowrap[% END %]>[% row1.value %]</td>
+      <td[% IF row1.align %] align="[% row1.align %]"[% END %][% IF row1.nowrap %] nowrap[% END %][% IF row1.class %] class="[% row1.class %]"[% END %]>[% row1.value %]</td>
  [%- END %]
      </tr>
      <tr style='display:none'>
 [%- END %]
 
   </table>
-
-  <script type='text/javascript'>
-    $(function() {
-      setTimeout(function(){
-        [% SWITCH( myconfig_focus_position ) %]
-          [% CASE 'last_partnumber' %]
-            $('#display_row tr.row:gt(-3):lt(-1) input[name*="partnumber"]').focus();
-          [% CASE 'last_description' %]
-            $('#display_row tr.row:gt(-3):lt(-1) input[name*="description"]').focus();
-          [% CASE 'new_partnumber' %]
-            $('#display_row tr:gt(1) input[name*="partnumber"]').focus();
-          [% CASE DEFAULT %]
-            $('#display_row tr:gt(1) input[name*="description"]').focus();
-        [% END %]
-      }, 1);
-    });
-  </script>
-
  </td>
 </tr>
index b7a2491..7f85ff3 100644 (file)
@@ -1,14 +1,16 @@
 [%- USE HTML %]
 [%- USE T8 %]
 [%- USE LxERP %]
-[%- USE L %]
+[%- USE L %][%- USE P -%]
 <h1>[% HTML.escape(title) %]</h1>
 
 [%- SET vclabel = vc == 'customer' ? LxERP.t8('Customer') : LxERP.t8('Vendor') %]
 [%- SET vcnumberlabel = vc == 'customer' ? LxERP.t8('Customer Number') : LxERP.t8('Vendor Number') %]
 [%- SET vctypelabel = vc == 'customer' ? LxERP.t8('Customer type') : LxERP.t8('Vendor type') %]
+[%- SET vcdefault = 'old' _ vc %]
+[%- SET style="width: 250px" %]
 
-<form method="post" action="oe.pl">
+<form method="post" action="oe.pl" id="form">
 
 <table width="100%">
  <tr>
    <table>
     <tr>
      <th align="right">[% HTML.escape(vclabel) %]</th>
-     <td colspan="3">
-            [%- INCLUDE 'generic/multibox.html'
-                 name          = vc,
-                 default       = vc == 'customer' ? oldcustomer : oldvendor,
-                 style         = 'width: 250px',
-                 DATA          = ALL_VC,
-                 id_sub        = 'vc_keys',
-                 label_key     = 'name',
-                 select        = vc_select,
-                 limit         = vclimit,
-                 show_empty    = 1,
-                 allow_textbox = 1,
-                 class         = 'initial_focus',
-                 -%]
-     </td>
+     <td>[% L.input_tag(vc, $vcdefault, style=style, class="initial_focus") %]</td>
     </tr>
     <tr>
      <th align="right" nowrap>[% 'Contact Person' | $T8 %]</th>
-     <td colspan="3">[% L.input_tag("cp_name", '', style="width: 250px") %]</td>
+     <td>[% L.input_tag("cp_name", '', style=style) %]</td>
     </tr>
 [%- IF ALL_DEPARTMENTS.size %]
     <tr>
      <th align="right" nowrap>[% 'Department' | $T8 %]</th>
-     <td colspan="3">
-            [%- INCLUDE 'generic/multibox.html'
-                 name          = 'department_id',
-                 style         = 'width: 250px',
-                 DATA          = ALL_DEPARTMENTS,
-                 id_key        = 'id',
-                 label_key     = 'description',
-                 limit         = vclimit,
-                 show_empty    = 1,
-                 allow_textbox = 1,
-            -%]
-     </td>
+     <td colspan=3>[% L.select_tag('department_id', ALL_DEPARTMENTS, default = department_id, title_key = 'description', with_empty = 1, style=style) %]</td>
     </tr>
 [%- END %]
     <tr>
      <th align="right">[% HTML.escape(ordlabel) %]</th>
-     <td colspan="3"><input name="[% HTML.escape(ordnrname) %]" style="width: 250px"></td>
+     <td>[% L.input_tag(ordnrname, "", style=style) %]</td>
     </tr>
 [% IF is_order %]
     <tr>
      <th align="right">[% LxERP.t8("Customer Order Number") %]</th>
-     <td colspan="3">[% L.input_tag("cusordnumber", '', style="width: 250px") %]</td>
+     <td>[% L.input_tag("cusordnumber", '', style=style) %]</td>
     </tr>
 [% END %]
     <tr>
      <th align="right">[% 'Employee' | $T8 %]</th>
-     <td>[% L.select_tag('employee_id', ALL_EMPLOYEES, title_key='safe_name', with_empty=1, style='width:250px') %]</td>
+     <td>[% L.select_tag('employee_id', ALL_EMPLOYEES, title_key='safe_name', with_empty=1, style=style) %]</td>
     </tr>
     <tr>
      <th align="right">[% 'Salesman' | $T8 %]</th>
-     <td>[% L.select_tag('salesman_id', ALL_EMPLOYEES, title_key='safe_name', with_empty=1, style='width:250px') %]</td>
+     <td>[% L.select_tag('salesman_id', ALL_EMPLOYEES, title_key='safe_name', with_empty=1, style=style) %]</td>
     </tr>
     <tr>
      <th align="right">[% 'Steuersatz' | $T8 %]</th>
-     <td>[% L.select_tag('taxzone_id', ALL_TAXZONES, with_empty=1, title_key='description', style='width: 250px') %]</td>
+     <td>[% L.select_tag('taxzone_id', ALL_TAXZONES, with_empty=1, title_key='description', style=style) %]</td>
     </tr>
     <tr>
      <th align="right">[% 'Shipping Point' | $T8 %]</th>
-     <td colspan="3">[% L.input_tag('shippingpoint', '', style='width:250px') %]</td>
+     <td>[% L.input_tag('shippingpoint', '', style=style) %]</td>
     </tr>
     <tr>
      <th align="right">[% 'Transaction description' | $T8 %]</th>
-     <td colspan="3"><input name="transaction_description" style="width: 250px"></td>
+     <td>[% L.input_tag("transaction_description", "", style=style) %]</td>
+     <th align="right">[% 'Part Description' | $T8 %]</th>
+     <td>[% L.input_tag("parts_description", "", style=style) %]</td>
     </tr>
     <tr>
-     <th align="right">[% 'Project Number' | $T8 %]</th>
-     <td colspan="3">
-            [%- INCLUDE 'generic/multibox.html'
-                 name          =  vclimit < ALL_PROJECTS.size ? 'projectnumber' : 'project_id',
-                 style         = "width: 250px",
-                 DATA          =  ALL_PROJECTS,
-                 id_key        = 'id',
-                 label_key     = 'projectnumber',
-                 limit         = vclimit,
-                 show_empty    = 1,
-                 allow_textbox = 1,
-            -%]
-     </td>
+     <th align="right">[% 'Project' | $T8 %]</th>
+     <td>[% P.project.picker("project_id", '', active="both", valid="both", style=style) %]</td>
+     <th align="right">[% 'Part Number' | $T8 %]</th>
+     <td>[% L.input_tag("parts_partnumber", "", style=style) %]</td>
     </tr>
     [%- UNLESS ALL_BUSINESS_TYPES.size == 0 %]
     <tr>
      <th align="right" nowrap>[% vctypelabel %]</th>
-     <td colspan="3">
-      [% L.select_tag('business_id', ALL_BUSINESS_TYPES, title_key = 'description', with_empty = 1, style='width:250px') %]
+     <td>
+      [% L.select_tag('business_id', ALL_BUSINESS_TYPES, title_key = 'description', with_empty = 1, style=style) %]
      </td>
     </tr>
     [%- END %]
+    <tr>
+     <th align="right">[% 'Internal Notes' | $T8 %]</th>
+     <td>[% L.input_tag('intnotes', '', style=style) %]</td>
+    </tr>
+    <tr>
+     <th align="right">[% 'Phone Notes' | $T8 %]</th>
+     <td>[% L.input_tag('phone_notes', '', style=style) %]</td>
+    </tr>
+    [%- IF type == 'sales_order' %]
+    <tr>
+     <th align="right">[% 'Full Text' | $T8 %]</th>
+     <td>[% L.input_tag('fulltext', '', style=style) %]</td>
+    </tr>
+    [%- END %]
     <tr>
      <th align="right">[% IF is_order %][% 'Order Date' | $T8 %][% ELSE %][% 'Quotation Date' | $T8 %][% END %] [% 'From' | $T8 %]</th>
      <td>
        [% L.date_tag('transdatefrom') %]
-     </td>
-     <th align="right">[% 'Bis' | $T8 %]</th>
-     <td>
+       [% 'Bis' | $T8 %]
       [% L.date_tag('transdateto') %]
      </td>
     </tr>
      <th align="right">[% IF is_order %][% 'Delivery Date' | $T8 %][% ELSE %][% 'Valid until' | $T8 %][% END %] [% 'From' | $T8 %]</th>
      <td>
        [% L.date_tag('reqdatefrom') %]
-     </td>
-     <th align="right">[% 'Bis' | $T8 %]</th>
-     <td>
+       [% 'Bis' | $T8 %]
        [% L.date_tag('reqdateto') %]
      </td>
     </tr>
      <th align="right">[% 'Insert Date' | $T8 %] [% 'From' | $T8 %]</th>
      <td>
        [% L.date_tag('insertdatefrom') %]
-     </td>
-     <th align="right">[% 'Bis' | $T8 %]</th>
-     <td>
+       [% 'Bis' | $T8 %]
        [% L.date_tag('insertdateto') %]
      </td>
     </tr>
     <tr>
      <th align="right">[% 'Expected billing date' | $T8 %] [% 'From' | $T8 %]</th>
      <td>
-      [% L.date_tag('expected_billing_date_from', '' 'BL') %]
-     </td>
-     <th align="right">[% 'Expected billing date' | $T8 %] [% 'Bis' | $T8 %]</th>
-     <td>
-      [% L.date_tag('expected_billing_date_to', '' 'BL') %]
+      [% L.date_tag('expected_billing_date_from', '') %]
+      [% 'Bis' | $T8 %]
+      [% L.date_tag('expected_billing_date_to', '') %]
      </td>
     </tr>
     <tr>
      <th align="right">[% 'Order probability' | $T8 %]</th>
-     <td colspan="3">
+     <td>
       [% L.select_tag('order_probability_op', [[ 'ge', '>=' ], [ 'le', '<=' ]]) %]
       [% L.select_tag('order_probability_value', ORDER_PROBABILITIES, title='title', with_empty=1) %]
      </td>
 [%- IF CT_CUSTOM_VARIABLES.size %]
     <tr>
       <td></td>
-      <td colspan=4 align=left><b>[% 'Custom variables for module' | $T8 %]: [%'Customers and vendors' | $T8 %]</td>
+      <td colspan=4 align=left><b>[% 'Custom variables for module' | $T8 %]: [%'Customers and vendors' | $T8 %]</b></td>
     </tr>
     [% CT_CUSTOM_VARIABLES_FILTER_CODE %]
 [%- END %]
        <tr>
         <td>
          <input name="l_name" id="l_name" class="checkbox" type="checkbox" value="Y" checked>
-         <label for="l_name">[% HTML.escape(vclabel) %]
+         <label for="l_name">[% HTML.escape(vclabel) %]</label>
         </td>
         <td>
          <input name="l_employee" id="l_employee" class="checkbox" type="checkbox" value="Y" checked>
          <input name="l_transaction_description" id="l_transaction_description" class="checkbox" type="checkbox" value="Y"[% IF INSTANCE_CONF.get_require_transaction_description_ps %] checked[% END %]>
          <label for="l_transaction_description">[% 'Transaction description' | $T8 %]</label>
         </td>
+        <td>
+         [%- L.checkbox_tag('l_department', label => LxERP.t8('Department')) %]
+        </td>
+
        </tr>
        <tr>
         <td>
          <input name="l_salesman" id="l_salesman" class="checkbox" type="checkbox" value="Y">
          <label for="l_salesman">[% 'Salesman' | $T8 %]</label>
         </td>
+        <td>
+         <input name="l_intnotes" id="l_intnotes" class="checkbox" type="checkbox" value="Y">
+         <label for="l_intnotes">[% 'Internal Notes' | $T8 %]</label>
+        </td>
        </tr>
 [% IF type == 'sales_quotation' %]
        <tr>
         </td>
        </tr>
        <tr>
-        <td colspan=4 align=left><b>[% HTML.escape(vclabel) %]</td>
+        <td colspan=4 align=left><b>[% HTML.escape(vclabel) %]</b></td>
        </tr>
        <tr>
         <td>
 
       [% CT_CUSTOM_VARIABLES_INCLUSION_CODE %]
 
-[%- IF type == 'sales_order' %]
-       <tr><td colspan="3"><hr></td></tr>
-[%- END %]
       </table>
      </td>
     </tr>
  </tr>
 </table>
 
-<br>
-<input type="hidden" name="nextsub" value="orders">
 <input type="hidden" name="vc" value="[% HTML.escape(vc) %]">
 <input type="hidden" name="type" value="[% HTML.escape(type) %]">
-<input class="submit" type="submit" name="action" value="[% 'Continue' | $T8 %]">
+<input type="hidden" name="action" value="orders">
 </form>
diff --git a/templates/webpages/order/form.html b/templates/webpages/order/form.html
new file mode 100644 (file)
index 0000000..82bc2f1
--- /dev/null
@@ -0,0 +1,66 @@
+[%- USE T8 %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE HTML %]
+<h1>[% FORM.title %] <span id='nr_in_title'>[%- SELF.order.number -%]</span></h1>
+
+<div id="print_options" style="display:none">
+  <form method="post" action="controller.pl" id="print_options_form">
+    [% SELF.print_options %]
+    <br>
+    [% L.button_tag('kivi.Order.print()', LxERP.t8('Print')) %]
+    <a href="#" onclick="$('#print_options').dialog('close');">[% LxERP.t8("Cancel") %]</a>
+  </form>
+</div>
+
+<div id="shipto_dialog" class="hidden"></div>
+
+<form method="post" action="controller.pl" id="order_form"
+      data-transport-cost-reminder-article-id="[% HTML.escape(transport_cost_reminder_article.id) %]"
+      data-transport-cost-reminder-article-description="[% HTML.escape(transport_cost_reminder_article.displayable_name) %]">
+  [% L.hidden_tag('callback',             FORM.callback) %]
+  [% L.hidden_tag('type',                 FORM.type) %]
+  [% L.hidden_tag('id',                   SELF.order.id) %]
+  [% L.hidden_tag('converted_from_oe_id', SELF.converted_from_oe_id) %]
+
+  [%- INCLUDE 'common/flash.html' %]
+
+  <div class="tabwidget" id="order_tabs">
+    <ul>
+      <li><a href="#ui-tabs-basic-data">[% 'Basic Data' | $T8 %]</a></li>
+[%- IF INSTANCE_CONF.get_webdav %]
+      <li><a href="#ui-tabs-webdav">[% 'WebDAV' | $T8 %]</a></li>
+[%- END %]
+[%- IF SELF.order.id AND INSTANCE_CONF.get_doc_storage %]
+      <li><a href="controller.pl?action=File/list&file_type=document&object_type=[% HTML.escape(FORM.type) %]&object_id=[% HTML.url(SELF.order.id) %]">[% 'Documents' | $T8 %]</a></li>
+      <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=[% HTML.escape(FORM.type) %]&object_id=[% HTML.url(SELF.order.id) %]">[% 'Attachments' | $T8 %]</a></li>
+[%- END %]
+[%- IF SELF.order.id %]
+      <li><a href="controller.pl?action=RecordLinks/ajax_list&object_model=Order&object_id=[% HTML.url(SELF.order.id) %]">[% 'Linked Records' | $T8 %]</a></li>
+[%- END %]
+[% IF SELF.order.id %]
+      <li><a href="#ui-tabs-phone-notes">[% 'Phone Notes' | $T8 %]<span id="num_phone_notes">[%- num_phone_notes ? ' (' _ num_phone_notes _ ')' : '' -%]</span></a></li>
+[% END %]
+    </ul>
+
+    [% PROCESS "order/tabs/basic_data.html" %]
+    [% PROCESS 'webdav/_list.html' %]
+    <div id="ui-tabs-1">
+      [%- LxERP.t8("Loading...") %]
+    </div>
+[% IF SELF.order.id %]
+    <div id="ui-tabs-phone-notes">
+      [% PROCESS "order/tabs/phone_notes.html" %]
+    </div>
+[% END %]
+    <div id="shipto_inputs" class="hidden">
+      [%- PROCESS 'common/_ship_to_dialog.html'
+        vc_obj=SELF.order.customervendor
+        cs_obj=SELF.order.custom_shipto
+        cvars=SELF.order.custom_shipto.cvars_by_config
+        id_selector='#order_shipto_id' %]
+    </div>
+
+  </div>
+
+</form>
diff --git a/templates/webpages/order/tabs/_business_info_row.html b/templates/webpages/order/tabs/_business_info_row.html
new file mode 100644 (file)
index 0000000..cdd61a6
--- /dev/null
@@ -0,0 +1,10 @@
+[%- USE T8 %][%- USE HTML %]
+
+<tr id='business_info_row' [%- IF !SELF.order.customervendor.business_id %]style='display:none'[%- END %]>
+  <th align="right">[%- IF SELF.cv == 'customer' -%]
+                      [%- 'Customer type' | $T8 -%]
+                    [%- ELSE -%]
+                      [%- 'Vendor type' | $T8 -%]
+                    [%- END -%]</th>
+  <td>[% HTML.escape(SELF.order.customervendor.business.description) %]; [% 'Trade Discount' | $T8 %] [% SELF.order.customervendor.business.discount_as_percent %] %</td>
+</tr>
diff --git a/templates/webpages/order/tabs/_item_input.html b/templates/webpages/order/tabs/_item_input.html
new file mode 100644 (file)
index 0000000..8f75b6f
--- /dev/null
@@ -0,0 +1,51 @@
+[%- USE T8 %][%- USE HTML %][%- USE LxERP %][%- USE L %][%- USE P %]
+
+ <div>
+  <table id="input_row_table_id">
+    <thead>
+      <tr class="listheading">
+        <th class="listheading" nowrap >[%- '+'            | $T8 %] </th>
+        <th class="listheading" nowrap >[%- 'position'     | $T8 %] </th>
+        <th class="listheading" nowrap >[%- 'Part'         | $T8 %] </th>
+        <th class="listheading" nowrap >[%- 'Description'  | $T8 %] </th>
+        <th class="listheading" nowrap width="5" >[%- 'Qty'          | $T8 %] </th>
+        <th class="listheading" nowrap width="15">[%- 'Price'        | $T8 %] </th>
+        <th class="listheading" nowrap width="5" >[%- 'Discount'     | $T8 %] </th>
+        <th></th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr valign="top" class="listrow">
+        <td class="tooltipster-html" title="[%- 'Create a new part' | $T8 -%]">
+          [% SET type_options = [[ 'part', LxERP.t8('Part') ], [ 'assembly', LxERP.t8('Assembly') ], [ 'service', LxERP.t8('Service') ] ] %]
+          [%- IF INSTANCE_CONF.get_feature_experimental_assortment %]
+            [%- type_options.push([ 'assortment', LxERP.t8('Assortment')]) %]
+          [%- END %]
+          [% L.select_tag('add_item.create_part_type', type_options) %]
+          [% L.button_tag('kivi.Order.create_part()', LxERP.t8('+')) %]
+        </td>
+        <td>[% L.input_tag('add_item.position', '', size = 5, class="add_item_input numeric") %]</td>
+        <td>
+          [%- SET PARAM_KEY = SELF.cv == "customer" ? 'with_customer_partnumber' : 'with_makemodel' -%]
+          [%- SET PARAM_VAL = SELF.search_cvpartnumber -%]
+          [% P.part.picker('add_item.parts_id', SELF.created_part, style='width: 300px', class="add_item_input",
+                            multiple_pos_input=1,
+                            action={set_multi_items='kivi.Order.add_multi_items'},
+                            classification_id=SELF.part_picker_classification_ids.as_list.join(','),
+                            $PARAM_KEY=PARAM_VAL) %]</td>
+        <td>[% L.input_tag('add_item.description', SELF.created_part.description, class="add_item_input") %]</td>
+        <td>
+          [% L.input_tag('add_item.qty_as_number', '', size = 5, class="add_item_input numeric") %]
+          [% L.hidden_tag('add_item.unit', SELF.created_part.unit, class="add_item_input") %]
+        </td>
+        [%- SET price = '' %]
+        [%- IF SELF.created_part %]
+          [%- SET price = LxERP.format_amount(((SELF.type == 'sales_quotation' || SELF.type == 'sales_order') ? SELF.created_part.sellprice : SELF.created_part.lastcost), -2) -%]
+        [%- END %]
+        <td>[% L.input_tag('add_item.sellprice_as_number', price, size = 10, class="add_item_input numeric tooltipster-html") %]</td>
+        <td>[% L.input_tag('add_item.discount_as_percent', '', size = 5, class="add_item_input numeric tooltipster-html") %]</td>
+        <td>[% L.button_tag('kivi.Order.add_item()', LxERP.t8('Add part')) %]</td>
+      </tr>
+    </tbody>
+  </table>
+ </div>
diff --git a/templates/webpages/order/tabs/_price_sources_dialog.html b/templates/webpages/order/tabs/_price_sources_dialog.html
new file mode 100644 (file)
index 0000000..02f1fd3
--- /dev/null
@@ -0,0 +1,106 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE L %]
+[%- USE LxERP %]
+[% SET best_price = price_source.best_price %]
+[% SET best_discount = price_source.best_discount %]
+[% SET price_editable = 0 %]
+[% IF (FORM.type == "sales_order" || FORM.type == "sales_quotation") %]
+  [% SET price_editable = AUTH.assert('sales_edit_prices', 1) %]
+[% END %]
+[% IF (FORM.type == "purchase_order" || FORM.type == "request_quotation") %]
+  [% SET price_editable = AUTH.assert('purchase_edit_prices', 1) %]
+[% END %]
+[% SET exfactor = price_source.record.exchangerate ? 1 / price_source.record.exchangerate : 1 %]
+[% SET exnoshow = price_source.record.currency_id==INSTANCE_CONF.get_currency_id %]
+[% SET places   = exnoshow ? -2 : 5 %]
+  <h2>[% 'Prices' | $T8 %]</h2>
+
+  <table>
+   <tr class='listheading'>
+    <th></th>
+    <th>[% 'Price Source' | $T8 %]</th>
+    <th>[% 'Price' | $T8 %]</th>
+    <th [%- IF exnoshow -%]style='display:none'[%- END %]>
+      [% 'Price' | $T8 -%]/[%- price_source.record.currency.name %]
+    </th>
+    <th>[% 'Best Price' | $T8 %]</th>
+    <th>[% 'Details' | $T8 %]</th>
+   </tr>
+   <tr class='listrow'>
+[%- IF price_source.record_item.active_price_source %]
+    <td>[% L.button_tag('kivi.Order.update_price_source(\'' _ FORM.item_id _ '\', \'\', \'' _ LxERP.t8('None (PriceSource)') _ '\', \'\', ' _ price_editable _ ')', LxERP.t8('Select')) %]</td>
+[%- ELSE %]
+    <td><b>[% 'Selected' | $T8 %]</b></td>
+[%- END %]
+    <td>[% 'None (PriceSource)' | $T8 %]</td>
+    <td>-</td>
+    <td [%- IF exnoshow -%]style='display:none'[%- END %]>-</td>
+    <td></td>
+    <td></td>
+   </tr>
+   [%- FOREACH price IN price_source.available_prices %]
+    <tr class='listrow'>
+[%- IF price_source.record_item.active_price_source != price.source %]
+     <td>[% L.button_tag('kivi.Order.update_price_source(\'' _ FORM.item_id _ '\', \'' _ price.source _ '\', \'' _ price.source_description _ '\', \'' _ LxERP.format_amount(price.price * exfactor, places) _ '\', ' _ price_editable _ ')', LxERP.t8('Select')) %]</td>
+[%- ELSIF price_source.record_item.sellprice * 1 != price.price * 1 %]
+     <td>[% L.button_tag('kivi.Order.update_price_source(\'' _ FORM.item_id _ '\', \'' _ price.source _ '\', \'' _ price.source_description _ '\', \'' _ LxERP.format_amount(price.price * exfactor, places) _ '\', ' _ price_editable _ ')', LxERP.t8('Update Price')) %]</td>
+[%- ELSE %]
+    <td><b>[% 'Selected' | $T8 %]</b></td>
+[% END %]
+     <td>[% price.source_description | html %]</td>
+     <td>[% price.price_as_number %]</td>
+     <td [%- IF exnoshow -%]style='display:none'[%- END %]>
+       [% LxERP.format_amount(price.price * exfactor, places) %]
+     </td>
+[% IF price.source == best_price.source %]
+     <td align='center'>&#x2022;</td>
+[% ELSE %]
+     <td></td>
+[% END %]
+     <td>[% price.description | html %]</td>
+    </tr>
+   [%- END %]
+  </table>
+
+  <h2>[% 'Discounts' | $T8 %]</h2>
+
+  <table>
+   <tr class='listheading'>
+    <th></th>
+    <th>[% 'Price Source' | $T8 %]</th>
+    <th>[% 'Discount' | $T8 %]</th>
+    <th>[% 'Best Discount' | $T8 %]</th>
+    <th>[% 'Details' | $T8 %]</th>
+   </tr>
+   <tr class='listrow'>
+[%- IF price_source.record_item.active_discount_source %]
+    <td>[% L.button_tag('kivi.Order.update_discount_source(\'' _ FORM.item_id _ '\', \'\', \'' _ LxERP.t8('None (PriceSource Discount)') _ '\', \'\', ' _ price_editable _ ')', LxERP.t8('Select')) %]</td>
+[%- ELSE %]
+    <td><b>[% 'Selected' | $T8 %]</b></td>
+[%- END %]
+    <td>[% 'None (PriceSource Discount)' | $T8 %]</td>
+    <td>-</td>
+    <td></td>
+    <td></td>
+   </tr>
+   [%- FOREACH price IN price_source.available_discounts %]
+    <tr class='listrow'>
+[%- IF price_source.record_item.active_discount_source != price.source %]
+     <td>[% L.button_tag('kivi.Order.update_discount_source(\'' _ FORM.item_id _ '\', \'' _ price.source _ '\', \'' _ price.source_description _ '\', \'' _ price.discount_as_percent _ '\', ' _ price_editable _ ')', LxERP.t8('Select')) %]</td>
+[%- ELSIF price_source.record_item.discount * 1 != price.discount * 1 %]
+     <td>[% L.button_tag('kivi.Order.update_discount_source(\'' _ FORM.item_id _ '\', \'' _ price.source _ '\', \'' _ price.source_description _ '\', \'' _ price.discount_as_percent  _ '\', ' _ price_editable _ ')', LxERP.t8('Update Discount')) %]</td>
+[%- ELSE %]
+    <td><b>[% 'Selected' | $T8 %]</b></td>
+[% END %]
+     <td>[% price.source_description | html %]</td>
+     <td>[% price.discount_as_percent %] %</td>
+[% IF price.source == best_discount.source %]
+     <td align='center'>&#x2022;</td>
+[% ELSE %]
+     <td></td>
+[% END %]
+     <td>[% price.description | html %]</td>
+    </tr>
+   [%- END %]
+  </table>
diff --git a/templates/webpages/order/tabs/_row.html b/templates/webpages/order/tabs/_row.html
new file mode 100644 (file)
index 0000000..0c29b3b
--- /dev/null
@@ -0,0 +1,162 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE P %]
+
+<tbody class="row_entry listrow" data-position="[%- HTML.escape(ITEM.position) -%]"[%- IF MYCONFIG.show_form_details -%] data-expanded="1"[%- END -%]>
+  <tr>
+    <td align="center">
+      [%- IF MYCONFIG.show_form_details %]
+        [% L.img_tag(src="image/collapse.svg",
+                     alt=LxERP.t8('Hide details'), title=LxERP.t8('Hide details'), class="expand") %]
+      [%- ELSE %]
+        [% L.img_tag(src="image/expand.svg",
+                     alt=LxERP.t8('Show details'), title=LxERP.t8('Show details'), class="expand") %]
+      [%- END %]
+      [% L.hidden_tag("orderitem_ids[+]", ID) %]
+      [% L.hidden_tag("converted_from_orderitems_ids[+]", ITEM.converted_from_orderitems_id) %]
+      [% L.hidden_tag("order.orderitems[+].id", ITEM.id, id='item_' _ ID) %]
+      [% L.hidden_tag("order.orderitems[].parts_id", ITEM.parts_id) %]
+    </td>
+    <td>
+      <div name="position" class="numeric">
+        [% HTML.escape(ITEM.position) %]
+      </div>
+    </td>
+    <td align="center">
+      <img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]" class="dragdrop">
+    </td>
+    <td align="center">
+      [%- L.button_tag("kivi.Order.delete_order_item_row(this)",
+                       LxERP.t8("X"),
+                       confirm=LxERP.t8("Are you sure?")) %]
+    </td>
+    [%- IF SELF.show_update_button -%]
+    <td align="center">
+      [%- L.img_tag(src="image/rotate_cw.svg",
+                    alt=LxERP.t8('Update from master data'),
+                    title= LxERP.t8('Update from master data'),
+                    onclick="if (!confirm('" _ LxERP.t8("Are you sure to update this position from master data?") _ "')) return false; kivi.Order.update_row_from_master_data(this);",
+                    id='update_from_master') %]
+    </td>
+    [%- END -%]
+    <td>
+      <div name="partnumber">
+        [%- P.link_tag(SELF.url_for(controller='Part', action='edit', 'part.id'=ITEM.part.id), ITEM.part.partnumber, target="_blank", title=LxERP.t8('Open in new window')) -%]
+      </div>
+    </td>
+    [%- IF SELF.search_cvpartnumber -%]
+    <td>
+      <div name="cvpartnumber">[% HTML.escape(ITEM.cvpartnumber) %]</div>
+    </td>
+    [%- END -%]
+    <td>
+      <div name="partclassification">[% ITEM.part.presenter.typeclass_abbreviation %]</div>
+    </td>
+    <td>
+      [% L.areainput_tag("order.orderitems[].description",
+                     ITEM.description,
+                     size='40',
+                     style='width: 300px') %]
+      [%- L.hidden_tag("order.orderitems[].longdescription", ITEM.longdescription) %]
+      [%- L.button_tag("kivi.Order.show_longdescription_dialog(this)", LxERP.t8("L")) %]
+    </td>
+    [%- IF (SELF.type == "sales_order" || SELF.type == "purchase_order") -%]
+    <td nowrap>
+      [%- L.div_tag(LxERP.format_amount(ITEM.shipped_qty, 2, 0) _ ' ' _ ITEM.unit, name="shipped_qty", class="numeric") %]
+    </td>
+    [%- END -%]
+    <td nowrap>
+      [%- L.input_tag("order.orderitems[].qty_as_number",
+                      ITEM.qty_as_number,
+                      size = 5,
+                      class="recalc reformat_number numeric") %]
+      [%- IF ITEM.part.formel -%]
+        [%- L.button_tag("kivi.Order.show_calculate_qty_dialog(this)", LxERP.t8("*/")) %]
+        [%- L.hidden_tag("formula[+]", ITEM.part.formel) -%]
+      [%- END -%]
+    </td>
+    <td>
+      [%- L.select_tag("order.orderitems[].price_factor_id",
+                       SELF.all_price_factors,
+                       default = ITEM.price_factor_id,
+                       title_key = 'description',
+                       with_empty = 1,
+                       class="recalc") %]
+    </td>
+    <td nowrap>
+      [%- L.select_tag("order.orderitems[].unit",
+                      ITEM.part.available_units,
+                      default = ITEM.unit,
+                      title_key = 'name',
+                      value_key = 'name',
+                      class = 'unitselect') %]
+    </td>
+    <td>
+      [%- L.button_tag("kivi.Order.price_chooser_item_row(this)",
+                       ITEM.active_price_source.source_description _ ' | ' _ ITEM.active_discount_source.source_description,
+                       name = "price_chooser_button") %]
+    </td>
+    [% SET RIGHT_TO_EDIT_PRICES = 0 %]
+    [% IF (SELF.type == "sales_order" || SELF.type == "sales_quotation") %]
+      [% SET RIGHT_TO_EDIT_PRICES = AUTH.assert('sales_edit_prices', 1) %]
+    [% END %]
+    [% IF (SELF.type == "purchase_order" || SELF.type == "request_quotation") %]
+      [% SET RIGHT_TO_EDIT_PRICES = AUTH.assert('purchase_edit_prices', 1) %]
+    [% END %]
+    <td>
+      [%- L.hidden_tag("order.orderitems[].active_price_source", ITEM.active_price_source.source) %]
+      [%- SET EDIT_PRICE = (RIGHT_TO_EDIT_PRICES && ITEM.active_price_source.source == '') %]
+      <div name="editable_price" [%- IF !EDIT_PRICE %]style="display:none"[%- END %] class="numeric">
+        [%- L.input_tag("order.orderitems[].sellprice_as_number",
+                        ITEM.sellprice_as_number,
+                        size = 10,
+                        disabled=(EDIT_PRICE? '' : 1),
+                        class="recalc reformat_number numeric") %]
+      </div>
+      <div name="not_editable_price" [%- IF EDIT_PRICE %]style="display:none"[%- END %]>
+        [%- L.div_tag(ITEM.sellprice_as_number, name="sellprice_text", class="numeric") %]
+        [%- L.hidden_tag("order.orderitems[].sellprice_as_number",
+                         ITEM.sellprice_as_number,
+                         disabled=(EDIT_PRICE? 1 : '')) %]
+      </div>
+    </td>
+    <td>
+      [%- L.hidden_tag("order.orderitems[].active_discount_source", ITEM.active_discount_source.source) %]
+      [%- SET EDIT_DISCOUNT = (RIGHT_TO_EDIT_PRICES && ITEM.active_discount_source.source == '') %]
+      <div name="editable_discount" [%- IF !EDIT_DISCOUNT %]style="display:none"[%- END %] class="numeric">
+        [%- L.input_tag("order.orderitems[].discount_as_percent",
+                        ITEM.discount_as_percent,
+                        size = 5,
+                        disabled=(EDIT_DISCOUNT? '' : 1),
+                        class="recalc reformat_number numeric") %]
+      </div>
+      <div name="not_editable_discount" [%- IF EDIT_DISCOUNT %]style="display:none"[%- END %]>
+        [%- L.div_tag(ITEM.discount_as_percent, name="discount_text", class="numeric") %]
+        [%- L.hidden_tag("order.orderitems[].discount_as_percent",
+                         ITEM.discount_as_percent,
+                         disabled=(EDIT_DISCOUNT? 1 : '')) %]
+      </div>
+    </td>
+    <td align="right">
+      [%- L.div_tag(LxERP.format_amount(ITEM.linetotal, 2, 0), name="linetotal") %]
+    </td>
+
+  </tr>
+
+  <tr [%- IF !MYCONFIG.show_form_details -%]style="display:none"[%- END -%]>
+    <td colspan="100%">
+      [%- IF MYCONFIG.show_form_details || ITEM.render_second_row %]
+        <div name="second_row" data-loaded="1">
+          [%- PROCESS order/tabs/_second_row.html ITEM=ITEM TYPE=SELF.type %]
+        </div>
+      [%- ELSE %]
+        <div name="second_row" id="second_row_[% ID %]">
+          [%- LxERP.t8("Loading...") %]
+        </div>
+      [%- END %]
+    </td>
+  </tr>
+
+</tbody>
diff --git a/templates/webpages/order/tabs/_second_row.html b/templates/webpages/order/tabs/_second_row.html
new file mode 100644 (file)
index 0000000..8b4d83a
--- /dev/null
@@ -0,0 +1,66 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE P %]
+
+<table>
+  <tr><td colspan="100%">
+    [%- IF (TYPE == "sales_order" || TYPE == "purchase_order") %]
+      <b>[%- 'Serial No.' | $T8 %]</b>&nbsp;
+      [%- L.input_tag("order.orderitems[].serialnumber", ITEM.serialnumber, size = 15) %]&nbsp;
+    [%- END %]
+    <b>[%- 'Project' | $T8 %]</b>&nbsp;
+    [% P.project.picker("order.orderitems[].project_id", ITEM.project_id, size = 15) %]&nbsp;
+    [%- IF (TYPE == "sales_order" || TYPE == "purchase_order") %]
+      <b>[%- 'Reqdate' | $T8 %]</b>&nbsp;
+      [% L.date_tag("order.orderitems[].reqdate_as_date", ITEM.reqdate_as_date) %]&nbsp;
+    [%- END %]
+    <b>[%- 'Subtotal' | $T8 %]</b>&nbsp;
+    [% L.yes_no_tag("order.orderitems[].subtotal", ITEM.subtotal) %]&nbsp;
+    [%- IF (TYPE == "sales_order" || TYPE == "sales_quotation") %]
+      <b>[%- 'Ertrag' | $T8 %]</b>&nbsp;
+        <span name="linemargin">
+          <span[%- IF ITEM.marge_total < 0 -%] class="plus0"[%- END -%]>
+            [%- LxERP.format_amount(ITEM.marge_total, 2, 0) %]&nbsp;&nbsp;
+            [%- LxERP.format_amount(ITEM.marge_percent, 2, 0) %]%
+          </span>
+       </span>&nbsp;
+      <b>[%- 'LP' | $T8 %]</b>&nbsp;
+      [%- LxERP.format_amount(ITEM.part.listprice, 2, 0) %]&nbsp;
+      <b>[%- 'EK' | $T8 %]</b>&nbsp;
+        [%- L.input_tag("order.orderitems[].lastcost_as_number",
+                        ITEM.lastcost_as_number,
+                        size = 5,
+                        class="recalc reformat_number numeric") %]&nbsp;
+    [%- END %]
+    <b>[%- 'On Hand' | $T8 %]</b>&nbsp;
+      <span[%- IF ITEM.part.onhand < ITEM.part.rop -%] class="numeric plus0"[%- END -%]>
+        [%- ITEM.part.onhand_as_number -%]&nbsp;[%- ITEM.part.unit -%]
+      </span>&nbsp;
+    <b>[%- 'Optional' | $T8 %]</b>&nbsp;
+      [%- L.yes_no_tag("order.orderitems[].optional", ITEM.optional
+                        class="recalc") %]&nbsp;
+  </td></tr>
+
+  <tr>
+    [%- SET n = 0 %]
+    [%- FOREACH var = ITEM.cvars_by_config %]
+    [%- NEXT UNLESS (var.config.processed_flags.editable && ITEM.part.cvar_by_name(var.config.name).is_valid) %]
+    [%- SET n = n + 1 %]
+    <th>
+      [% var.config.description %]
+    </th>
+    <td>
+      [% L.hidden_tag('order.orderitems[].custom_variables[+].config_id', var.config.id) %]
+      [% L.hidden_tag('order.orderitems[].custom_variables[].id', var.id) %]
+      [% L.hidden_tag('order.orderitems[].custom_variables[].sub_module', var.sub_module) %]
+      [% INCLUDE 'common/render_cvar_input.html' var_name='order.orderitems[].custom_variables[].unparsed_value' %]
+    </td>
+    [%- IF (n % (MYCONFIG.form_cvars_nr_cols || 3)) == 0 %]
+
+  </tr><tr>
+    [%- END %]
+    [%- END %]
+  </tr>
+</table>
diff --git a/templates/webpages/order/tabs/_tax_row.html b/templates/webpages/order/tabs/_tax_row.html
new file mode 100644 (file)
index 0000000..aa09a3a
--- /dev/null
@@ -0,0 +1,15 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+
+<tr class="tax_row">
+  <th align="right">[%- IF TAXINCLUDED %][%- 'Including' | $T8 %]&nbsp;[%- END %][%- TAX.tax.taxdescription %] [% TAX.tax.rate_as_percent %]%</th>
+  <td align="right">[%- LxERP.format_amount(TAX.amount, 2, 0) %]</td>
+</tr>
+[%- IF TAXINCLUDED %]
+<tr class="tax_row">
+  <th align="right">[%- 'Net amount' | $T8 %]</th>
+  <td align="right">[%- LxERP.format_amount(TAX.netamount, 2, 0) %]</td>
+</tr>
+[%- END%]
diff --git a/templates/webpages/order/tabs/basic_data.html b/templates/webpages/order/tabs/basic_data.html
new file mode 100644 (file)
index 0000000..4314902
--- /dev/null
@@ -0,0 +1,437 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE P %]
+
+[%- INCLUDE 'generic/set_longdescription.html' %]
+
+<div id="ui-tabs-basic-data">
+  <table width="100%">
+    <tr valign="top">
+      <td>
+        <table width="100%">
+          <tr>
+            <th align="right">[%- SELF.cv == "customer" ? LxERP.t8('Customer') : LxERP.t8('Vendor') -%]</th>
+            [% SET cv_id = SELF.cv _ '_id' %]
+            <td>
+              [% P.customer_vendor.picker("order.${SELF.cv}" _ '_id', SELF.order.$cv_id, type=SELF.cv, style='width: 300px') %]
+              [% P.button_tag("kivi.Order.show_vc_details_dialog()", LxERP.t8("Details (one letter abbreviation)")) %]
+              [% P.link_tag(SELF.url_for(controller='CustomerVendor', action='edit', 'id'=SELF.order.$cv_id, 'db'=SELF.cv), LxERP.t8('Edit'), target="_blank", title=LxERP.t8('Open in new window')) %]
+            </td>
+          </tr>
+
+          <tr id='cp_row' [%- IF !SELF.order.${SELF.cv}.contacts.size %]style='display:none'[%- END %]>
+            <th align="right">[% 'Contact Person' | $T8 %]</th>
+            <td>[% L.select_tag('order.cp_id',
+                                SELF.order.${SELF.cv}.contacts,
+                                default=SELF.order.cp_id,
+                                title_key='full_name_dep',
+                                value_key='cp_id',
+                                with_empty=1,
+                                style='width: 300px') %]</td>
+          </tr>
+
+          <tr>
+            <th align="right">[% 'Shipping Address' | $T8 %]</th>
+            <td>
+              <span id='shipto_selection' [%- IF !SELF.order.${SELF.cv}.shipto.size %]style='display:none'[%- END %]>
+                [% shiptos = [ { shipto_id => "", displayable_id => LxERP.t8("No/individual shipping address") } ] ;
+                   FOREACH s = SELF.order.${SELF.cv}.shipto ;
+                     shiptos.push(s) ;
+                   END ;
+                   L.select_tag('order.shipto_id',
+                                 shiptos,
+                                 default=SELF.order.shipto_id,
+                                 title_key='displayable_id',
+                                 value_key='shipto_id',
+                                 with_empty=0,
+                                 style='width: 300px') %]
+              </span>
+              [% L.button_tag("kivi.Order.edit_custom_shipto()", LxERP.t8("Custom shipto")) %]
+            </td>
+          </tr>
+
+          [%- IF SELF.cv == "customer" %]
+          <tr id="billing_address_row"[% IF !SELF.order.customer.additional_billing_addresses.as_list.size %] style="display:none"[% END %]>
+            <th align="right">[% 'Custom Billing Address' | $T8 %]</th>
+            <td>
+              [% L.select_tag('order.billing_address_id',
+                               SELF.order.customer.additional_billing_addresses,
+                               default=SELF.order.billing_address_id,
+                               title_key='displayable_id',
+                               value_key='id',
+                               with_empty=1,
+                               style='width: 300px') %]
+            </td>
+          </tr>
+          [%- END %]
+
+          [%- PROCESS order/tabs/_business_info_row.html SELF=SELF %]
+
+          <tr>
+            <th align="right">[% 'Steuersatz' | $T8 %]</th>
+            <td>[% L.select_tag('order.taxzone_id', SELF.all_taxzones, default=SELF.order.taxzone_id, title_key='description', style='width: 300px', class='recalc') %]</td>
+          </tr>
+
+          [% SET currency_id = SELF.order.currency_id || INSTANCE_CONF.get_currency_id  # use default currency for new order %]
+          <tr id="currency_settings">
+            <th align="right">[% 'Currency' | $T8 %]</th>
+            <td>[% L.select_tag('order.currency_id', SELF.all_currencies, default=currency_id, value_key='id', title_key='name') %]</td>
+          </tr>
+          <tr id="exchangerate_settings" [%- IF SELF.order.currency_id==INSTANCE_CONF.get_currency_id %]style='display:none'[%- END %]>
+            <th align="right">[% 'Exchangerate' | $T8 %]</th>
+            <td> 1 <span id="currency_name">[% SELF.order.currency.name %]</span> =
+              [% L.input_tag('order.exchangerate_as_null_number', SELF.order.exchangerate_as_null_number, size="15", class="reformat_number_as_null_number numeric") %]
+              [% INSTANCE_CONF.default_currency %]
+              [% L.hidden_tag('old_currency_id', currency_id) %]
+              [% L.hidden_tag('old_exchangerate', SELF.order.exchangerate_as_null_number) %]
+            </td>
+          </tr>
+
+[%- IF SELF.all_languages.size %]
+          <tr>
+            <th align="right">[% 'Language' | $T8 %]</th>
+            <td>
+              [% L.select_tag('order.language_id', SELF.all_languages, default=SELF.order.language_id, title_key='description', with_empty=1, style='width:300px') %]
+            </td>
+          </tr>
+[%- END %]
+
+[%- IF SELF.all_departments.size %]
+          <tr>
+            <th align="right">[% 'Department' | $T8 %]</th>
+            <td>
+              [% L.select_tag('order.department_id', SELF.all_departments, default=SELF.order.department_id, title_key='description', with_empty=1, style='width:300px') %]
+            </td>
+          </tr>
+[%- END %]
+
+          <tr>
+            <th align="right">[% 'Shipping Point' | $T8 %]</th>
+            <td>[% L.input_tag('order.shippingpoint', SELF.order.shippingpoint, style='width: 300px') %]</td>
+          </tr>
+
+          <tr>
+            <th align="right">[% 'Ship via' | $T8 %]</th>
+            <td>[% L.input_tag('order.shipvia', SELF.order.shipvia, style='width: 300px') %]</td>
+          </tr>
+
+          <tr>
+            <th align="right">[% 'Transaction description' | $T8 %]</th>
+            <td>[% L.input_tag('order.transaction_description', SELF.order.transaction_description, 'data-validate'=INSTANCE_CONF.get_require_transaction_description_ps ? 'required' : '', style='width: 300px') %]</td>
+          </tr>
+
+          <tr>
+            <th align="right">[% 'Project Number' | $T8 %]</th>
+            <td>[% P.project.picker('order.globalproject_id', SELF.order.globalproject_id, style='width: 300px') %]</td>
+          </tr>
+
+        </table>
+      </td>
+
+      <td align="right">
+        <table>
+
+          <tr>
+            <td colspan="2" align="center">
+              [%- IF SELF.order.id %]
+                <label for="order.delivered">[% 'Delivery Order(s) for full qty created' | $T8 %]</label>
+                [% L.yes_no_tag('order.delivered', SELF.order.delivered) %]
+                <label for="order.closed">[% 'Closed' | $T8 %]</label>
+                [% L.yes_no_tag('order.closed', SELF.order.closed) %]
+              [%- END %]
+            </td>
+          </tr>
+
+          <tr>
+            <th align="right">[% 'Employee' | $T8 %]</th>
+            <td>[% L.select_tag('order.employee_id',
+              SELF.all_employees,
+              default=(SELF.order.employee_id ? SELF.order.employee_id : SELF.current_employee_id),
+              title_key='safe_name') %]</td>
+          </tr>
+
+          [% IF SELF.cv == 'customer' %]
+          <tr>
+            <th align="right">[% 'Salesman' | $T8 %]</th>
+            <td>[% L.select_tag('order.salesman_id',
+              SELF.all_salesmen,
+              default=(SELF.order.salesman_id ? SELF.order.salesman_id : SELF.current_employee_id),
+              title_key='safe_name') %]</td>
+          </tr>
+          [% END %]
+
+          [%- IF (SELF.type == "sales_order" || SELF.type == "purchase_order") -%]
+          <tr>
+            <th width="70%" align="right" nowrap>[% 'Order Number' | $T8 %]</th>
+            <td>
+              [%- IF INSTANCE_CONF.get_sales_purchase_record_numbers_changeable %]
+                [% L.input_tag('order.ordnumber', SELF.order.ordnumber, size = 11, onchange='kivi.Order.set_number_in_title(this)') %]
+              [%- ELSIF SELF.order.id %]
+                [% HTML.escape(SELF.order.ordnumber) %]
+                [% L.hidden_tag("order.ordnumber", SELF.order.ordnumber) %]
+              [% ELSE %]
+                [% LxERP.t8("will be set upon saving") %]
+              [%- END %]
+            </td>
+          </tr>
+          [%- END -%]
+
+          [%- IF (SELF.type == "sales_order" || SELF.type == "sales_quotation") -%]
+            [%- SET quo_nr_txt = 'Quotation Number' -%]
+          [%- ELSE -%]
+            [%- SET quo_nr_txt = 'RFQ Number' -%]
+          [%- END -%]
+          <tr>
+            <th width="70%" align="right" nowrap>[% quo_nr_txt | $T8 %]</th>
+            <td>
+              [%- IF (SELF.type == "sales_order" || SELF.type == "purchase_order") -%]
+                [% L.input_tag('order.quonumber', SELF.order.quonumber, size = 11) %]
+              [%- ELSIF INSTANCE_CONF.get_sales_purchase_record_numbers_changeable %]
+                [% L.input_tag('order.quonumber', SELF.order.quonumber, size = 11, onchange='kivi.Order.set_number_in_title(this)') %]
+              [%- ELSIF SELF.order.id %]
+                [% HTML.escape(SELF.order.quonumber) %]
+                [% L.hidden_tag("order.quonumber", SELF.order.quonumber) %]
+              [% ELSE %]
+                [% LxERP.t8("will be set upon saving") %]
+              [%- END %]
+            </td>
+          </tr>
+
+          [%- IF (SELF.type == "sales_order" || SELF.type == "purchase_order") -%]
+          <tr>
+            <th width="70%" align="right" nowrap>[% 'Customer Order Number' | $T8 %]</th>
+            <td>[% L.input_tag('order.cusordnumber', SELF.order.cusordnumber, size = 11) %]</td>
+          </tr>
+          [%- END -%]
+
+          [%- IF (SELF.type == "sales_order" || SELF.type == "purchase_order") -%]
+            [%- SET transdate_txt = 'Order Date' -%]
+          [%- ELSIF SELF.type == "sales_quotation" -%]
+            [%- SET transdate_txt = 'Quotation Date' -%]
+          [%- ELSE -%]
+            [%- SET transdate_txt = 'RFQ Date' -%]
+          [%- END -%]
+          <tr>
+            <th width="70%" align="right" nowrap>[% transdate_txt | $T8 %]</th>
+            <td>[% L.date_tag('order.transdate_as_date', SELF.order.transdate_as_date) %]</td>
+          </tr>
+
+          <tr>
+            <th width="70%" align="right" nowrap>[% 'Tax point' | $T8 %]</th>
+            <td>[% L.date_tag('order.tax_point_as_date', SELF.order.tax_point_as_date, class="recalc") %]</td>
+          </tr>
+
+          [%- IF (SELF.type == "sales_order" || SELF.type == "purchase_order") -%]
+            [%- SET reqdate_txt = 'Reqdate'; SET reqdate_class = 'recalc' -%]
+          [%- ELSIF SELF.type == "sales_quotation" -%]
+            [%- SET reqdate_txt = 'Valid until'; SET reqdate_class = '' -%]
+          [%- ELSE -%]
+            [%- SET reqdate_txt = 'Required by'; SET reqdate_class = 'recalc' -%]
+          [%- END -%]
+          <tr>
+            <th width="70%" align="right" nowrap>[% reqdate_txt | $T8 %]</th>
+            <td>[% L.date_tag('order.reqdate_as_date', SELF.order.reqdate_as_date, class=reqdate_class) %]</td>
+          </tr>
+
+          [%- IF SELF.type == "sales_quotation" -%]
+          <tr>
+            <th width="70%" align="right" nowrap>[% 'Order probability' | $T8 %]</th>
+            <td>[%- L.select_tag('order.order_probability', SELF.order_probabilities, title='title', default=SELF.order.order_probability) %]%</td>
+          </tr>
+          <tr>
+            <th width="70%" align="right" nowrap>[% 'Expected billing date' | $T8 %]</th>
+            <td>[%- L.date_tag('order.expected_billing_date_as_date', SELF.order.expected_billing_date_as_date) %]</td>
+          </tr>
+          [%- END %]
+
+          <tr>
+            <th width="70%" align="right" nowrap>[% 'Insert Date' | $T8 %]</th>
+            <td>[% SELF.order.itime_as_date %]</td>
+          </tr>
+
+        </table>
+
+      </td>
+    </tr>
+  </table>
+
+  [%- PROCESS order/tabs/_item_input.html SELF=SELF %]
+
+  [% L.button_tag('kivi.Order.open_multi_items_dialog()', LxERP.t8('Add multiple items')) %]
+
+  <table width="100%">
+    <tr>
+      <td>
+        [%- IF SELF.positions_scrollbar_height -%]
+          [%- SET scroll_style = 'style="overflow-y: auto; height:' _ SELF.positions_scrollbar_height _ 'vh;"' -%]
+        [%- ELSE -%]
+          [%- SET scroll_style = '' -%]
+        [%- END -%]
+        <div id="row_table_scroll_id" [%- scroll_style -%]>
+          <table id="row_table_id" width="100%">
+            <thead>
+              <tr class="listheading">
+                <th class="listheading" style='text-align:center' nowrap width="1">
+                  [%- IF MYCONFIG.show_form_details %]
+                    [%- L.img_tag(src="image/collapse.svg", alt=LxERP.t8('Hide all details'), title=LxERP.t8('Hide all details'), id='expand_all', "data-expanded"="1") %]
+                  [%- ELSE %]
+                    [%- L.img_tag(src="image/expand.svg", alt=LxERP.t8('Show all details'), title=LxERP.t8('Show all details'), id='expand_all') %]
+                  [%- END %]
+                </th>
+                <th class="listheading" nowrap width="3" >[%- 'position'     | $T8 %] </th>
+                <th class="listheading" style='text-align:center' nowrap width="1"><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]"></th>
+                <th class="listheading" style='text-align:center' nowrap width="1"><img src="image/close.png" alt="[%- LxERP.t8('delete item') %]"></th>
+                [%- IF SELF.show_update_button -%]
+                <th class="listheading" style='text-align:center' nowrap width="1">
+                  [%- L.img_tag(src="image/rotate_cw.svg",
+                                alt=LxERP.t8('Update from master data'),
+                                title= LxERP.t8('Update from master data'),
+                                onclick="if (!confirm('" _ LxERP.t8("Are you sure to update all positions from master data?") _ "')) return false; kivi.Order.update_all_rows_from_master_data();",
+                                id='update_from_master') %]
+                </th>
+                [%- END %]
+                <th id="partnumber_header_id"   class="listheading" nowrap width="15"><a href='#' onClick='javascript:kivi.Order.reorder_items("partnumber")'> [%- 'Partnumber'  | $T8 %]</a></th>
+                [%- IF SELF.search_cvpartnumber -%]
+                <th id="cvpartnumber_header_id" class="listheading" nowrap width="15"><a href='#' onClick='javascript:kivi.Order.reorder_items("cvpartnumber")' > [%- SELF.cv == "customer" ? LxERP.t8('Customer Part Number') : LxERP.t8('Model') %]</a></th>
+                [%- END -%]
+                <th id="partclass_header_id"    class="listheading" nowrap width="2">[%- 'Type'  | $T8 %]</th>
+                <th id="description_header_id"  class="listheading" nowrap           ><a href='#' onClick='javascript:kivi.Order.reorder_items("description")'>[%- 'Description' | $T8 %]</a></th>
+                [%- IF (SELF.type == "sales_order" || SELF.type == "purchase_order") -%]
+                <th id="shipped_qty_header_id"  class="listheading" nowrap width="5" ><a href='#' onClick='javascript:kivi.Order.reorder_items("shipped_qty")'>[%- 'Delivered'   | $T8 %]</a></th>
+                [%- END -%]
+                <th id="qty_header_id"          class="listheading" nowrap width="5" ><a href='#' onClick='javascript:kivi.Order.reorder_items("qty")'>        [%- 'Qty'         | $T8 %]</a></th>
+                <th class="listheading" nowrap width="5" >[%- 'Price Factor' | $T8 %] </th>
+                <th class="listheading" nowrap width="5" >[%- 'Unit'         | $T8 %] </th>
+                <th class="listheading" nowrap width="5" >[%- 'Price Source' | $T8 %] </th>
+                <th id="sellprice_header_id"   class="listheading" nowrap width="15" ><a href='#' onClick='javascript:kivi.Order.reorder_items("sellprice")'> [%- 'Price'       | $T8 %]</a></th>
+                <th id="discount_header_id"    class="listheading" nowrap width="15" ><a href='#' onClick='javascript:kivi.Order.reorder_items("discount")'>  [%- 'Discount'    | $T8 %]</a></th>
+                <th class="listheading" nowrap width="10">[%- 'Extended'     | $T8 %] </th>
+              </tr>
+            </thead>
+
+            [%- FOREACH item = SELF.order.items_sorted %]
+              [%- PROCESS order/tabs/_row.html ITEM=item ID=(item.id||item.new_fake_id)  -%]
+            [%- END %]
+
+          </table>
+        </div>
+
+      </td>
+    </tr>
+
+    <tr>
+    </tr>
+
+    <tr>
+      <td colspan="100%" width="100%">
+        <table width="100%">
+          <tr>
+            <td>
+              <table>
+                <tr>
+                  <th align="left">[% 'Notes' | $T8 %]</th>
+                  <th align="left">[% 'Internal Notes' | $T8 %]</th>
+                </tr>
+                <tr valign="top">
+                  <td>
+                    [% L.textarea_tag('order.notes', SELF.order.notes, wrap="soft", style="width: 350px; height: 150px", class="texteditor") %]
+                  </td>
+                  <td>
+                    [% L.textarea_tag('order.intnotes', SELF.order.intnotes, wrap="soft", style="width: 350px; height: 150px") %]
+                  </td>
+                </tr>
+              </table>
+            </td>
+
+            <td>
+              <table>
+                <tr>
+                  <th align="right">[% 'Payment Terms' | $T8 %]</th>
+                  <td>[% L.select_tag('order.payment_id',
+                                      SELF.all_payment_terms,
+                                      default = SELF.order.payment_id,
+                                      with_empty = 1,
+                                      title_key = 'description',
+                                      style = 'width: 250px') %]</td>
+                </tr>
+                <tr>
+                  <th align="right">[% 'Delivery Terms' | $T8 %]</th>
+                  <td>[% L.select_tag('order.delivery_term_id',
+                                      SELF.all_delivery_terms,
+                                      default = SELF.order.delivery_term_id,
+                                      with_empty = 1,
+                                      title_key = 'description',
+                                      style = 'width: 250px') %]</td>
+                </tr>
+                [%- IF SELF.type == "sales_order" %]
+                <tr>
+                  <th align="right">[%- 'Periodic Invoices' | $T8 %]</th>
+                  <td>[% L.button_tag('kivi.Order.show_periodic_invoices_config_dialog()', LxERP.t8('Configure')) %]
+                    (<span id='periodic_invoices_status'>[%- SELF.periodic_invoices_status -%]</span>)
+                    <a href="doc/html/ch03.html#features.periodic-invoices.variables" target="_blank">?</a>
+                  </td>
+                </tr>
+                [%- END %]
+              </table>
+            </td>
+
+            [%- IF (SELF.type == "sales_order" || SELF.type == "sales_quotation") -%]
+            [%- SET marge_class = (SELF.order.marge_total < 0) ? 'plus0' : '' -%]
+            <td>
+              <table>
+                <tr>
+                  <th  align="left">[% 'Ertrag' | $T8 %]</th>
+                  <td align="right">
+                    [%- L.div_tag(SELF.order.marge_total_as_number, id='marge_total_id', class=marge_class) %]
+                  </td>
+                </tr>
+                <tr>
+                  <th  align="left">[% 'Ertrag prozentual' | $T8 %]</th>
+                  <td align="right">
+                    [%- L.div_tag(LxERP.format_amount(SELF.order.marge_percent, 2), id='marge_percent_id', class=marge_class) %]
+                  </td>
+                  <td>[%- L.div_tag('%', id='marge_percent_sign_id', class=marge_class) %]</td>
+                </tr>
+              </table>
+            </td>
+            [%- END %]
+
+            <td align="right">
+              <table>
+                <tr id="taxincluded_row_id" [%- IF !SELF.taxes.size %]style="display:none"[%- END %]>
+                  <td align=right colspan="2">
+                    <label for="order.taxincluded"><b>[% 'Tax Included' | $T8 %]</b></label>
+                    [% L.yes_no_tag('order.taxincluded', SELF.order.taxincluded, class='recalc') %]
+                  </td>
+                </tr>
+
+                <tr id="subtotal_row_id" [%- IF SELF.order.taxincluded %]style="display:none"[%- END %]>
+                  <th align="right">[%- 'Subtotal' | $T8 %]</th>
+                  <td align="right">
+                    [%- L.div_tag(SELF.order.netamount_as_number, id='netamount_id') %]
+                  </td>
+                </tr>
+                [%- FOREACH tax = SELF.taxes %]
+                  [%- PROCESS order/tabs/_tax_row.html TAX=tax TAXINCLUDED=SELF.order.taxincluded %]
+                [%- END %]
+                <tr id="amount_row_id">
+                  <th align="right">[%- 'Total' | $T8 %]</th>
+                  <td align="right">
+                    [%- L.div_tag(SELF.order.amount_as_number, id='amount_id') %]
+                  </td>
+                </tr>
+              </table>
+            </td>
+
+          </tr>
+        </table>
+      </td>
+    </tr>
+
+  </table>
+
+</div>
+
+[% L.sortable_element('#row_table_id') %]
diff --git a/templates/webpages/order/tabs/phone_notes.html b/templates/webpages/order/tabs/phone_notes.html
new file mode 100644 (file)
index 0000000..b1d2117
--- /dev/null
@@ -0,0 +1,47 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE L %]
+[%- USE LxERP %]
+[%- USE P %]
+
+<div id="phone-notes">
+ [% IF ( SELF.order.phone_notes && SELF.order.phone_notes.size ) %]
+  <table>
+    <tr>
+      <th class="listheading">[% 'Subject' | $T8 %]</th>
+      <th class="listheading">[% 'Created on' | $T8 %]</th>
+      <th class="listheading">[% 'Created by' | $T8 %]</th>
+    </tr>
+
+    [%- FOREACH row = SELF.order.phone_notes %]
+     <tr class="listrow">
+       <td>[% P.link_tag('#', row.subject, onclick="kivi.Order.load_phone_note(" _ HTML.url(row.id) _ ", '" _ HTML.escape(row.subject) _ "', '" _ HTML.escape(row.body) _ "')") %]</td>
+       <td>[% row.itime.to_kivitendo | html %]</td>
+       <td>[% row.employee.safe_name | html %]</td>
+     </tr>
+    [% END %]
+  </table>
+ [% END %]
+
+  <h2 id='phone_note_edit_text'>[% 'Add note' | $T8 %]</h2>
+
+  [% L.hidden_tag('phone_note.id') %]
+
+  <table>
+    <tr>
+      <td valign="right">[% 'Subject' | $T8 %]</td>
+      <td>[% L.input_tag('phone_note.subject', '', size = 50) %]</td>
+    </tr>
+    <tr>
+      <td valign="right" align="top">[% 'Body' | $T8 %]</td>
+      <td align="top">[% L.textarea_tag('phone_note.body', '', cols = 50 rows = 10) %]</td>
+    </tr>
+  </table>
+
+ <p>
+   [% P.button_tag("kivi.Order.save_phone_note()",   LxERP.t8('Save')) %]
+   [% P.button_tag("kivi.Order.delete_phone_note()", LxERP.t8('Delete'), id = 'phone_note_delete_button', style='display:none') %]
+   [% P.button_tag("kivi.Order.cancel_phone_note()", LxERP.t8('Cancel')) %]
+ </p>
+
+</div>
diff --git a/templates/webpages/order_items_search/_order_item_list.html b/templates/webpages/order_items_search/_order_item_list.html
new file mode 100644 (file)
index 0000000..e79d0e4
--- /dev/null
@@ -0,0 +1,40 @@
+[%- USE LxERP %]
+[%- USE T8 %]
+[%- USE L %]
+[%- USE HTML %]
+[%- USE P %]
+[% SET qty_round = 2 %]
+<table cellpadding="3px">
+ <tr class="listheading">
+  <th>[%- LxERP.t8("Part")           %]</th>
+  <th>[%- LxERP.t8("Customer")       %]</th>
+  <th>[%- LxERP.t8("Order")          %]</th>
+  <th>[%- LxERP.t8("Order Date")      %]</th>
+  <th>[%- LxERP.t8("Qty")            %]</th>
+  <th>[%- LxERP.t8("Delivered")      %]</th>
+  <th>[%- LxERP.t8("Price")          %]</th>
+  <th>[%- LxERP.t8("Discount")       %] %</th>
+  <th>[%- LxERP.t8("Delivery Order") %]</th>
+  [% IF FORM.show_images %]
+  <th>[%- LxERP.t8("Image")          %]</th>
+  [% END %]
+ </tr>
+ [% FOREACH order_item = SELF.orderitems %]
+ <tr id="tr_[% loop.count %]" class="listrow[% loop.count % 2 %]">
+  <td>                 [% order_item.part.presenter.part(no_link => 0)               %]</td>
+  <td>                 [% order_item.order.customer.presenter.customer(no_link => 0) %]</td>
+  <td class="numeric"> [% order_item.order.presenter.sales_order(no_link => 0)       %]</td>
+  <td>                 [% order_item.order.transdate.to_kivitendo             %]</td>
+  <td class="numeric [% IF order_item.delivered_qty == order_item.qty %]shipped[% ELSE %]not_shipped[% END %]">
+    [% LxERP.format_amount(order_item.qty, qty_round) %] [% order_item.unit | html %]
+  </td>
+  <td class="numeric"> [% LxERP.format_amount(order_item.delivered_qty, qty_round) %] [% order_item.unit | html %] </td>
+  <td class="numeric"> [% order_item.sellprice_as_number                      %]</td>
+  <td class="numeric"> [% order_item.discount_as_percent                      %]</td>
+  <td>                 [% order_item.deliveryorders                           %]</td>
+  [% IF FORM.show_images %]
+  <td> [% IF order_item.part.image %]<a href="[% order_item.part.image | html %]" target="_blank"><img height="32" border="0" src="[% order_item.part.image | html %]"/></a>[% END %]</td>
+  [% END %]
+ </tr>
+ [% END %]
+</table>
diff --git a/templates/webpages/order_items_search/order_items.html b/templates/webpages/order_items_search/order_items.html
new file mode 100644 (file)
index 0000000..495d2a5
--- /dev/null
@@ -0,0 +1,93 @@
+[% USE HTML %]
+[%- USE LxERP %]
+[%- USE T8 %]
+[%- USE L %]
+[%- USE P %]
+
+[% SET size=50 %]
+[% SET show_images=0 %]
+
+<h1>[% title %]</h1>
+<div style="padding-bottom: 15px">
+[% 'Filter' | $T8 %]:
+<form id="filter" name="filter" method="post" action="controller.pl">
+ <table>
+  </tr>
+    <td>[% 'Customer' | $T8 %]</td>
+    <td>[% P.customer_vendor.picker('filter.order.customer.id', FORM.customer_id, type='customer', class="filter", size=size) %]</td>
+  </tr>
+  <tr>
+    <td>[% 'Part' | $T8 %]</td>
+    <td>[% L.input_tag('filter.part.all:substr:multi::ilike', FORM.part, size = size, class="filter") %]</td>
+  </tr>
+  <tr>
+    <td>[% 'Order Number' | $T8 %]</td>
+    <td>[% L.input_tag('filter.order.ordnumber:substr::ilike', FORM.ordnumber, size = 10, class="filter") %]</td>
+  <tr>
+  <tr>
+    <td>[% 'Order Date' | $T8 %]</td>
+    <td>[% 'From' | $T8 %] [% L.date_tag("filter.order.transdate:date::ge", filter.order.transdate_date___ge, class="filter") %] [% 'Until' | $T8 %] [% L.date_tag('filter.order.transdate:date::le', filter.order.transdate_date__le, class="filter") %]</td>
+  <tr>
+  <tr>
+    <td>[% 'Description' | $T8 %]</td>
+    <td>[% L.input_tag('filter.description:substr::ilike', filter.description_substr__ilike, size = size, class="filter") %]</td>
+  </tr>
+  <tr>
+    <td>[% 'Long Description' | $T8 %]</td>
+    <td>[% L.input_tag('filter.longdescription:substr::ilike', filter.longdescription_substr__ilike, size = size, class="filter") %]  </tr>
+  <tr>
+    <td>[% 'Show images' | $T8 %]</td>
+    <td>[% L.checkbox_tag('show_images', checked=show_images) %]  </tr>
+  </tr>
+</table>
+[% L.button_tag("this.form.reset(); refresh_plot();", LxERP.t8("Reset")) %]
+</form>
+
+<div id="orderitems" style="padding-top: 20px">
+[% PROCESS 'order_items_search/_order_item_list.html' %]
+</div>
+
+
+<script type="text/javascript">
+  $(function() {
+    [% IF FORM.customer_id %]
+      $( "#filter_part_all_substr_multi_ilike" ).focus();
+    [% ELSE %]
+      $( "#filter_order_customer_id_name" ).focus();
+    [% END %]
+
+    addInputCallback($(".filter"), refresh_plot , 300 );
+
+    $('#show_images').change(function(){
+      refresh_plot();
+    });
+  });
+
+
+  function refresh_plot() {
+    var filterdata = $('#filter').serialize()
+    var url = './controller.pl?action=OrderItem/order_item_list_dynamic_table&' + filterdata;
+    $.ajax({
+        url : url,
+        type: 'POST',
+        success: function(data){
+            $('#orderitems').html(data);
+        }
+    })
+
+  };
+
+function addInputCallback(inputfield, callback, delay) {
+    var timer = null;
+    inputfield.on('keyup', function() {
+        if (timer) {
+            window.clearTimeout(timer);
+        }
+        timer = window.setTimeout( function() {
+            timer = null;
+            callback();
+        }, delay );
+    });
+    inputfield = null;
+}
+</script>
diff --git a/templates/webpages/part/_assembly.html b/templates/webpages/part/_assembly.html
new file mode 100644 (file)
index 0000000..bc32cf8
--- /dev/null
@@ -0,0 +1,133 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE P %]
+
+<div id="assembly" name="assembly">
+
+<h2>[% 'Assembly items' | $T8 %]</h2>
+
+[% L.hidden_tag('assembly_id', SELF.part.id) %]
+
+<table id="assembly_items">
+ <thead>
+   <tr class="listheading">
+     <th class="listheading" style='display:none'></th>
+     [% IF SELF.orphaned || AUTH.assert('assembly_edit', 1) %]
+     <th class="listheading" style='text-align:center' nowrap width="1"><img src="image/close.png" alt="[%- LxERP.t8('delete item') %]"></th>
+     [% END %]
+     <th class="listheading" nowrap width="3" >[%- 'position'     | $T8 %] </th>
+     [% IF SELF.orphaned || AUTH.assert('assembly_edit', 1) %]
+     <th class="listheading" style='text-align:center' nowrap width="1"><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]"></th>
+     [% END %]
+     <th id="partnumber_header_id"  class="listheading" nowrap width="5"><a href='#' onClick='javascript:kivi.Part.reorder_items("partnumber")' >[%- 'Partnumber'  | $T8 %]</a></th>
+     <th class="listheading" nowrap width="5">[% 'Type' | $T8 %]</th>
+     <th id="partdescription_header_id"  class="listheading" nowrap width="15"><a href='#' onClick='javascript:kivi.Part.reorder_items("description")' >[%- 'Description' | $T8 %]</a></th>
+     <th id="qty_header_id"         class="listheading" nowrap width="15" ><a href='#' onClick='javascript:kivi.Part.reorder_items("qty")'        >[%- 'Qty'         | $T8 %]</a></th>
+     <th class="listheading" nowrap width="5" >[%- 'Unit'         | $T8 %] </th>
+     <th class="listheading" nowrap width="5" >[%- 'BOM'          | $T8 %] </th>
+     <th class="listheading" nowrap width="5" >[%- 'Line Total'   | $T8 %] </th>
+     <th id="sellprice_header_id"   class="listheading" nowrap width="10" ><a href='#' onClick='javascript:kivi.Part.reorder_items("sellprice")' >[%- 'Sellprice'       | $T8 %]</a></th>
+     <th id="lastcost_header_id"   class="listheading" nowrap width="10" ><a href='#' onClick='javascript:kivi.Part.reorder_items("lastcost")'   >[%- 'Lastcost'       | $T8 %]</a></th>
+     <th id="_header_id"   class="listheading" nowrap width="15" ><a href='#' onClick='javascript:kivi.Part.reorder_items("partsgroup")'         >[%- 'Partsgroup'       | $T8 %]</a></th>
+   </tr>
+ </thead>
+<tbody id="assembly_rows">
+  [% assembly_html %]
+</tbody>
+<tbody id="assembly_input">
+<tr>
+ [% IF SELF.orphaned || AUTH.assert('assembly_edit', 1) %]
+ <td></td>
+ <td></td>
+ <td align="right">[% 'Part' | $T8 %]:</td>
+ <td>[% P.part.picker('add_items[+].parts_id', '', style='width: 300px', multiple=1, id='assembly_picker', action={set_multi_items='kivi.Part.set_multi_assembly_items', commit_one='kivi.Part.add_assembly_item'}) %]</td>
+ <td>[%- L.button_tag("kivi.Part.add_assembly_item()", LxERP.t8("Add")) %]</td>
+ <td>[% L.button_tag('$("#assembly_picker").data("part_picker").open_dialog()', LxERP.t8('Add multiple items')) %]</td>
+ <td>[% L.hidden_tag('add_items[].qty_as_number', 1) %]</td>
+ [% ELSE %]
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ [% END %]
+ <td></td>
+ <td align="right">[% 'Totals' | $T8 %]:</td>
+ <td></td>
+ <td id="items_sellprice_sum" class="numeric">[%- LxERP.format_amount(items_sellprice_sum, 2, 0) %]</td>
+ <td id="items_lastcost_sum"  class="numeric">[%- LxERP.format_amount(items_lastcost_sum,  2, 0) %]</td>
+ <td></td>
+</tr>
+<tr>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td align="right">[% 'Margepercent' | $T8 %]:</td>
+ <td></td>
+ <td class="numeric">
+ [% IF items_sellprice_sum > 0 %]
+   [%- LxERP.format_amount(100 - (items_lastcost_sum / items_sellprice_sum * 100), 2, 0) %]
+ [% END %]
+ </td>
+</tr>
+<tr>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td align="right">[% 'Margetotal' | $T8 %]:</td>
+ <td></td>
+ <td id="items_sum_diff"      class="numeric">[%- LxERP.format_amount(items_sum_diff,      2, 0) %]</td>
+</tr>
+<tr>
+ [% IF SELF.orphaned || AUTH.assert('assembly_edit', 1) %]
+ <td></td>
+ <td></td>
+ [% END %]
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td align="right">[% L.button_tag("kivi.Part.set_assembly_sellprice()", LxERP.t8("Set sellprice")) %]</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+
+[% L.sortable_element('#assembly_rows') %]
+
+<div>
+<p>
+</p>
+</div>
+
+
+</div>
+
+<script type="text/javascript">
+  $(function() {
+    $("#assembly").on( "focusout", ".recalc", function( event )  {
+      kivi.Part.assembly_recalc();
+    });
+
+    $('#assembly_rows').on('sortstop', function(event, ui) {
+      $('#assembly thead a img').remove();
+      kivi.Part.renumber_positions();
+    });
+  })
+</script>
diff --git a/templates/webpages/part/_assembly_row.html b/templates/webpages/part/_assembly_row.html
new file mode 100644 (file)
index 0000000..a974212
--- /dev/null
@@ -0,0 +1,63 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE P %]
+  <tr class="listrow[% listrow %] assembly_item_row">
+    <td style='display:none'>
+      [% IF orphaned || AUTH.assert('assembly_edit', 1) %]
+      [% L.hidden_tag("assembly_items[+].parts_id", ITEM.part.id) %]
+      [% END %]
+    </td>
+    <td align="center" [% UNLESS orphaned || AUTH.assert('assembly_edit', 1) %]style='display:none'[% END %]>
+      [%- L.button_tag("kivi.Part.delete_item_row(this)",
+                       LxERP.t8("X")) %] [% # , confirm=LxERP.t8("Are you sure?")) %]
+    </td>
+    <td>
+      <div name="position" class="numeric">
+        [% HTML.escape(position) or HTML.escape(ITEM.position) %]
+      </div>
+    </td>
+    <td align="center" [% UNLESS orphaned || AUTH.assert('assembly_edit', 1) %]style='display:none'[% END %]>
+      <img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]" class="dragdrop">
+    </td>
+    <td nowrap>
+       [% ITEM.part.presenter.part %]
+    </td>
+    <td nowrap>
+       [% P.part.type_abbreviation(ITEM.part.part_type) %][% P.part.classification_abbreviation(ITEM.part.classification_id) %]
+    </td>
+    <td>
+       [% HTML.escape(ITEM.part.description) %]
+    </td>
+    <td>
+    [% IF orphaned || AUTH.assert('assembly_edit', 1) %]
+      [%- L.input_tag("assembly_items[].qty_as_number",
+                      ITEM.qty_as_number,
+                      size = 10,
+                      class="recalc reformat_number numeric") %]
+    [% ELSE %]
+      [% ITEM.qty_as_number | html %]
+    [% END %]
+    </td>
+    <td nowrap>
+      [% ITEM.part.unit | html %]
+    </td>
+    [% IF orphaned || AUTH.assert('assembly_edit', 1) %]
+    <td>[% L.checkbox_tag("assembly_items[].bom", checked=ITEM.bom, for_submit=1) %]</td>
+    [% ELSE %]
+    <td>[% IF ITEM.bom %][% 'Yes' | $T8 %][% ELSE %][% 'No' | $T8 %][% END %]</td>
+    [% END %]
+    <td align="right">
+      [%- L.div_tag(LxERP.format_amount(ITEM.linetotal_sellprice, 3, 0), name="linetotal") %]
+      </td>
+    <td align="right">
+      [% ITEM.part.sellprice_as_number %]
+      </td>
+    <td align="right">
+      [% ITEM.part.lastcost_as_number %]
+      </td>
+    <td align="right">
+      [% HTML.escape(ITEM.part.partsgroup.partsgroup) %]
+      </td>
+  </tr>
diff --git a/templates/webpages/part/_assortment.html b/templates/webpages/part/_assortment.html
new file mode 100644 (file)
index 0000000..db43305
--- /dev/null
@@ -0,0 +1,100 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE P %]
+
+<div id="assortment" name="assortment">
+
+<h2>[% 'Assortment items' | $T8 %]</h2>
+
+[% L.hidden_tag('assortment_id', SELF.part.id) %]
+
+<table id="assortment_items">
+ <thead>
+   <tr class="listheading">
+     <th class="listheading" style='display:none'></th>
+     [% IF SELF.orphaned || AUTH.assert('assortment_edit', 1) %]
+     <th class="listheading" style='text-align:center' nowrap width="1"><img src="image/close.png" alt="[%- LxERP.t8('delete item') %]"></th>
+     [% END %]
+     <th class="listheading" nowrap width="3" >[%- 'position'     | $T8 %] </th>
+     [% IF SELF.orphaned || AUTH.assert('assortment_edit', 1) %]
+     <th class="listheading" style='text-align:center' nowrap width="1"><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]"></th>
+     [% END %]
+     <th id="partnumber_header_id"  class="listheading" nowrap width="15"><a href='#' onClick='javascript:kivi.Part.reorder_items("partnumber")'> [%- 'Partnumber'  | $T8 %]</a></th>
+     <th id="partdescription_header_id"  class="listheading" nowrap width="15"><a href='#' onClick='javascript:kivi.Part.reorder_items("description")' >[%- 'Description' | $T8 %]</a></th>
+     <th id="qty_header_id"         class="listheading" nowrap width="5" ><a href='#' onClick='javascript:kivi.Part.reorder_items("qty")'>        [%- 'Qty'         | $T8 %]</a></th>
+     <th class="listheading" nowrap width="5" >[%- 'Unit'         | $T8 %] </th>
+     <th class="listheading" nowrap width="5" >[%- 'Charge'       | $T8 %] </th>
+     <th class="listheading" nowrap width="5" >[%- 'Line Total'   | $T8 %] </th>
+     <th id="sellprice_header_id"   class="listheading" nowrap width="10" ><a href='#' onClick='javascript:kivi.Part.reorder_items("sellprice")'> [%- 'Sellprice'   | $T8 %]</a></th>
+     <th id="lastcost_header_id"   class="listheading" nowrap width="10" ><a href='#' onClick='javascript:kivi.Part.reorder_items("lastcost")'> [%- 'Lastcost'      | $T8 %]</a></th>
+     <th id="_header_id"   class="listheading" nowrap width="15" ><a href='#' onClick='javascript:kivi.Part.reorder_items("partsgroup")'> [%- 'Partsgroup'       | $T8 %]</a></th>
+   </tr>
+ </thead>
+<tbody id="assortment_rows">
+  [% assortment_html %]
+</tbody>
+<tbody id="assortment_input">
+<tr>
+ [% IF SELF.orphaned || AUTH.assert('assortment_edit', 1) %]
+ <td></td>
+ <td></td>
+ <td align="right">[% 'Part' | $T8 %]:</td>
+ <td>[% P.part.picker('add_items[+].parts_id', '', style='width: 300px', multiple=1, id='assortment_picker', action={set_multi_items='kivi.Part.set_multi_assortment_items'}) %]</td>
+ <td>[%- L.button_tag("kivi.Part.add_assortment_item()", LxERP.t8("Add")) %]</td>
+ <td>[% L.button_tag('$("#assortment_picker").data("part_picker").open_dialog()', LxERP.t8('Add multiple items')) %]</td>
+ <td>[% L.hidden_tag('add_items[].qty_as_number', 1) %]</td>
+ [% ELSE %]
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ [% END %]
+ <td></td>
+ <td align="right">[% 'Totals' | $T8 %]:</td>
+ <th id="items_sellprice_sum" class="numeric">[%- LxERP.format_amount(items_sellprice_sum, 2, 0) %]</td>
+ <th id="items_lastcost_sum"  class="numeric">[%- LxERP.format_amount(items_lastcost_sum,  2, 0) %]</td>
+ <th id="items_sum_diff"      class="numeric">[%- LxERP.format_amount(items_sum_diff,      2, 0) %]</td>
+</tr>
+<tr>
+ [% IF SELF.orphaned || AUTH.assert('assortment_edit', 1) %]
+ <td></td>
+ <td></td>
+ [% END %]
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td align="right">[% L.button_tag("kivi.Part.set_assortment_sellprice()", LxERP.t8("Set sellprice")) %]</td>
+ <td align="right">[% L.button_tag("kivi.Part.set_assortment_lastcost()",  LxERP.t8("Set lastcost"))  %]</td>
+ <td></td>
+</tr>
+</tbody>
+</table>
+
+[% L.sortable_element('#assortment_rows') %]
+
+</div>
+
+<script type="text/javascript">
+  $(function() {
+    $("#assortment").on( "focusout", ".recalc", function( event )  {
+      kivi.Part.assortment_recalc();
+    });
+
+    $("#assortment").on( "change", ":checkbox", function( event )  {
+      kivi.Part.assortment_recalc();
+    });
+
+    $('#assortment_rows').on('sortstop', function(event, ui) {
+      $('#assortment thead a img').remove();
+      kivi.Part.renumber_positions();
+    });
+  })
+</script>
diff --git a/templates/webpages/part/_assortment_row.html b/templates/webpages/part/_assortment_row.html
new file mode 100644 (file)
index 0000000..6edd9bb
--- /dev/null
@@ -0,0 +1,71 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE P %]
+  <tr class="listrow[% listrow %] assortment_item_row">
+    <td style='display:none'>
+      [% IF orphaned || AUTH.assert('assortment_edit', 1) %]
+      [% L.hidden_tag("assortment_items[+].parts_id", ITEM.part.id) %]
+      [% END %]
+    </td>
+    <td align="center" [% UNLESS orphaned || AUTH.assert('assortment_edit', 1) %]style='display:none'[% END %]>
+      [%- L.button_tag("kivi.Part.delete_item_row(this)",
+                       LxERP.t8("X")) %] [% # , confirm=LxERP.t8("Are you sure?")) %]
+    </td>
+    <td>
+      <div name="position" class="numeric">
+        [% HTML.escape(position) or HTML.escape(ITEM.position) %]
+      </div>
+    </td>
+    <td align="center" [% UNLESS orphaned || AUTH.assert('assortment_edit', 1) %]style='display:none'[% END %]>
+      <img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]" class="dragdrop">
+    </td>
+    <td nowrap>
+      [% ITEM.part.presenter.part %]
+    </td>
+    <td>
+       [% HTML.escape(ITEM.part.description) %]
+    </td>
+    <td nowrap>
+    [% IF orphaned || AUTH.assert('assortment_edit', 1) %]
+      [%- L.input_tag("assortment_items[].qty_as_number",
+                      ITEM.qty_as_number,
+                      size = 10,
+                      class="recalc reformat_number numeric") %]
+    [% ELSE %]
+      [% ITEM.qty_as_number | html %]
+    [% END %]
+    </td>
+    <td nowrap>
+    [% IF orphaned || AUTH.assert('assortment_edit', 1) %]
+      [%- L.select_tag("assortment_items[].unit",
+                      ITEM.part.available_units,
+                      default = ITEM.part.unit,
+                      title_key = 'name',
+                      value_key = 'name',
+                      class = 'unitselect') %]
+    [% ELSE %]
+      [% ITEM.part.unit | html %]
+    [% END %]
+    </td>
+    <td>
+    [% IF orphaned || AUTH.assert('assortment_edit', 1) %]
+      [% L.checkbox_tag('assortment_items[].charge', checked => ITEM.charge, class => 'checkbox', for_submit=1) %]
+    [% ELSE %]
+      [% IF ITEM.charge %][% 'Yes' | $T8 %][%- ELSE %][% 'No' | $T8 %][%- END %]
+    [% END %]
+    </td>
+    <td align="right">
+      [%- L.div_tag(LxERP.format_amount(ITEM.linetotal_sellprice, 2, 0), name="linetotal") %]
+      </td>
+    <td align="right">
+      [% ITEM.part.sellprice_as_number %]
+      </td>
+    <td align="right">
+      [% ITEM.part.lastcost_as_number %]
+      </td>
+    <td align="right">
+      [% HTML.escape(ITEM.part.partsgroup.partsgroup) %]
+      </td>
+  </tr>
diff --git a/templates/webpages/part/_basic_data.html b/templates/webpages/part/_basic_data.html
new file mode 100644 (file)
index 0000000..a9fa531
--- /dev/null
@@ -0,0 +1,243 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE P %]
+   <table width="100%" id="basic_data_table">
+    <tr>
+     <td>
+      <table width="100%" id="ic1">
+       <tr valign="top">
+        <td>
+         [%- IF SELF.part.image && INSTANCE_CONF.get_parts_show_image %]
+         <a href="[% SELF.part.image | html %]" target="_blank"><img style="[% INSTANCE_CONF.get_parts_image_css %]" src="[% SELF.part.image | html %]"/></a>
+         [%- END %]
+
+         <table id="ic2">
+          <tr>
+           <td colspan="2">
+            <table id="ic3">
+             <tr>
+              <th align="right">[% 'Part Number' | $T8 %]</th>
+              <td>[% L.input_tag("part.partnumber", SELF.part.partnumber, size=40, class="initial_focus", "data-validate"="trimmed_whitespaces") %]</td>
+             </tr>
+             <tr>
+              <th align="right">[% 'Part Classification' | $T8 %]</th>
+              <td>[% P.part.select_classification('part.classification_id', default => SELF.part.classification_id, type => SELF.parts_classification_filter ) %]</td>
+             </tr>
+             <tr>
+              <th align="right">[% 'Part Description' | $T8 %]</th>
+              <td>
+               [% L.areainput_tag("part.description", SELF.part.description, size=40) %]
+              </td>
+             </tr>
+             <tr>
+               <th align="right">[% 'EAN-Code' | $T8 %]</th>
+               <td>[% L.input_tag("part.ean", SELF.part.ean, size=40, "data-validate"="trimmed_whitespaces") %]</td>
+             </tr>
+             <tr>
+              [%- IF SELF.all_partsgroups.size %]
+              <th align="right">[% 'Partsgroup' | $T8 %]</th>
+              <td>[%- L.select_tag('part.partsgroup_id', SELF.all_partsgroups, default=SELF.part.partsgroup_id, title_key='partsgroup', value_key='id', with_empty=1 style='width: 200px') %]</td>
+              [% END %]
+             </tr>
+             [%- IF SELF.all_buchungsgruppen.size %]
+             <tr>
+              <th align="right">[% 'Booking group' | $T8 %]</th>
+              <td>[%- L.select_tag('part.buchungsgruppen_id', SELF.all_buchungsgruppen, default=SELF.part.buchungsgruppen_id, title_key='description', value_key='id', with_empty=0 style='width: 200px') %]</td>
+             </tr>
+             [%- END %]
+             [%- IF SELF.all_payment_terms.size %]
+             <tr>
+              <th align="right">[% 'Payment Terms' | $T8 %]</th>
+              <td>
+              [%- L.select_tag('part.payment_id', SELF.all_payment_terms, default=SELF.part.payment_id, title_key='description', value_key='id', with_empty=1 style='width: 200px') %]</td>
+             </tr>
+             [% END %]
+            </table>
+           </td>
+          </tr>
+
+          <tr height="5"></tr>
+
+          <tr>
+           <td>
+            <table id="ic4">
+             <tr>
+              <th align="left">[% 'Part Notes' | $T8 %]</th>
+              <th align="left">[% 'Formula' | $T8 %]</th>
+             </tr>
+             <tr valign="top">
+              <td>
+               [% L.textarea_tag("part.notes", P.restricted_html(SELF.part.notes), class="texteditor", style="width: 600px; height: 200px") %]
+              </td>
+              <td>
+                 <textarea id="part.formel" name="part.formel" rows="[% HTML.escape(notes_rows) %]" cols="30" wrap="soft" class="tooltipster-html" title="[% 'The formula needs the following syntax:<br>For regular article:<br>Variablename= Variable Unit;<br>Variablename2= Variable2 Unit2;<br>...<br>###<br>Variable + ( Variable2 / Variable )<br><b>Please be beware of the spaces in the formula</b><br>' | $T8 %]">[% HTML.escape(SELF.part.formel) %]</textarea>
+               </td>
+             </tr>
+             [% IF CUSTOM_VARIABLES_FIRST_TAB %]
+              <tr><td>[% 'Unchecked custom variables will not appear in orders and invoices.' | $T8 %]</td></tr>
+               [%- FOREACH var = CUSTOM_VARIABLES_FIRST_TAB %]
+               <tr>
+                <td align="left" valign="top">[% var.VALID_BOX %]
+                [%- IF !var.partsgroup_filtered %]
+                  [% HTML.escape(var.description) %]
+                [%- END %]
+               </tr>
+               <tr><td>[% var.HTML_CODE %]</td></tr>
+               [%- END %]
+             [% END %]
+            </table>
+           </td>
+          </tr>
+         </table>
+        </td>
+
+        <td>
+         <table id="ic5">
+          <tr>
+           <th align="right" nowrap="true">[% 'Price updated' | $T8 %]</th>
+           <td>
+           [% SELF.part.last_price_update.valid_from.to_kivitendo | html %]
+           </td>
+          </tr>
+
+          <tr>
+           <th align="right" nowrap="true">[% 'List Price' | $T8 %]</th>
+           <td>[% L.input_tag("part.listprice_as_number", SELF.part.listprice_as_number, size=11 class='reformat_number numeric') %]</td>
+          </tr>
+
+          <tr  >
+           <th align="right" nowrap="true">[% 'Sell Price' | $T8 %]</th>
+           <td>[% L.input_tag("part.sellprice_as_number", SELF.part.sellprice_as_number, size=11, class='reformat_number numeric') %] [% IF (SELF.part.is_assortment or SELF.part.is_assembly) %] (<span id="items_sellprice_sum_basic">[% LxERP.format_amount(SELF.part.items_sellprice_sum, 2) %]</span>) [% END %]</td>
+          </tr>
+
+          [%- UNLESS SELF.part.is_assembly %]
+          <tr>
+           <th align="right" nowrap="true">[% 'Last Cost' | $T8 %]</th>
+           <td>[% L.input_tag("part.lastcost_as_number", SELF.part.lastcost_as_number, size=11 class='reformat_number numeric') %]
+           [% IF SELF.part.is_assortment %] (<span id="items_lastcost_sum_basic">[% LxERP.format_amount(SELF.part.items_lastcost_sum, 2) %]</span>) [% END %]</td>
+          </tr>
+          [%- END %]
+
+          [%- IF SELF.all_price_factors.size %]
+          <tr>
+           <th align="right">[% 'Price Factor' | $T8 %]</th>
+           <td>
+            [%- L.select_tag('part.price_factor_id', SELF.all_price_factors, default=SELF.part.price_factor_id, title_key='description', value_key='id', with_empty=1) %]
+           </td>
+          </tr>
+          [%- END %]
+
+          <tr>
+           <th align="right" nowrap="true">[% 'Unit' | $T8 %]</th>
+           <td>
+            [%- IF !SELF.part.id or SELF.part.orphaned # same logic as unit_changable %]
+            [%- L.select_tag('part.unit', SELF.all_units, default=SELF.part.unit, title_key='name', value_key='name') %]
+            [%- ELSE %]
+            [% L.hidden_tag('part.unit', SELF.part.unit) %] [% HTML.escape(SELF.part.unit) %]
+            [%- END %]
+           </td>
+          </tr>
+
+        [%- UNLESS SELF.part.is_service %]
+          <tr>
+           <th align="right" nowrap="true">[% 'Weight' | $T8 %]</th>
+           <td>
+            [%- IF SELF.part.is_assembly %]
+              <span id="items_weight_sum_basic">[% LxERP.format_amount(SELF.part.weight) %]</span>
+            [% ELSE %]
+              [% L.input_tag('part.weight_as_number', SELF.part.weight_as_number, size=10, class='reformat_number numeric') %]
+            [% END %]
+            [% HTML.escape(INSTANCE_CONF.get_weightunit) %]
+           </td>
+          </tr>
+          <tr>
+           <th align="right" nowrap>[% 'On Hand' | $T8 %]</th>
+           <th align="left" nowrap>[% LxERP.format_amount(SELF.part.onhand) %] [% SELF.part.unit | html %]</th>
+          </tr>
+          <tr>
+           <th align="right" nowrap="true">[% 'ROP' | $T8 %]</th>
+           <td>[% L.input_tag("part.rop_as_number", SELF.part.rop_as_number, size=10, class="reformat_number numeric") %]</td>
+          </tr>
+          [% IF SELF.all_warehouses.size %]
+          <tr>
+           <th align="right" nowrap="true">[% 'Default Warehouse' | $T8 %]</th>
+           <td>[% L.select_tag('part.warehouse_id', SELF.all_warehouses, default=SELF.part.warehouse.id, title_key='description', with_empty=1) %]
+           </td>
+          </tr>
+          [% END %]
+          <tr>
+           <th align="right" nowrap="true">[% 'Default Bin' | $T8 %]</th>
+           <td>
+            <span id='bin'>
+            [% IF SELF.part.warehouse.id %]
+            [% L.select_tag('part.bin_id', SELF.part.warehouse.bins, default=SELF.part.bin.id, title_key='description') %]
+            [%- END %]
+            </span>
+           </td>
+          </tr>
+        [%- END %]
+          <tr>
+           <th align="right" nowrap="true">[% 'Verrechnungseinheit' | $T8 %]</th>
+           <td>[% L.input_tag("part.ve", SELF.part.ve, size=10) %]</td>
+          </tr>
+          <tr>
+           <th align="right" nowrap="true">[% 'Business Volume' | $T8 %]</th>
+           <td>[% L.input_tag("part.gv_as_number", SELF.part.gv_as_number, size=10, class='reformat_number numeric') %]</td>
+          </tr>
+          <tr>
+           <th align="right" nowrap><label for="not_discountable">[% 'Not Discountable' | $T8 %]</label></th>
+           <td>[% L.checkbox_tag('part.not_discountable', checked = SELF.part.not_discountable, for_submit=1) %]</td>
+          </tr>
+        [%- IF SELF.part.id %]
+          <tr>
+           <th align="right" nowrap="true"><label for="obsolete">[% 'Obsolete' | $T8 %]</label></th>
+           <td>[% L.checkbox_tag('part.obsolete', checked = SELF.part.obsolete, for_submit=1) %]</td>
+          </tr>
+        [%- END %]
+        [%- UNLESS SELF.part.is_service %]
+          <tr>
+           <th align="right" nowrap><label for="has_sernumber">[% 'Has serial number' | $T8 %]</label></th>
+           <td>[% L.checkbox_tag('part.has_sernumber', checked = SELF.part.has_sernumber, for_submit=1) %]</td>
+          </tr>
+        [%- END %]
+          <tr>
+           <th align="right" nowrap><label for="shop">[% 'Shop article' | $T8 %]</label></th>
+           <td>[% L.checkbox_tag('part.shop', checked = SELF.part.shop, for_submit=1) %]</td>
+          </tr>
+         </table>
+        </td>
+       </tr>
+      </table>
+     </td>
+    </tr>
+
+
+    <tr>
+     <td>
+      <table id="ic6">
+       <tr>
+        <th align="right" nowrap>[% 'Image' | $T8 %]</th>
+        <td>[% L.input_tag("part.image", SELF.part.image, size=40, "data-validate"="trimmed_whitespaces") %]</td>
+        <th align="right" nowrap>[% 'Microfiche' | $T8 %]</th>
+        <td>[% L.input_tag("part.microfiche", SELF.part.microfiche, size=20, "data-validate"="trimmed_whitespaces") %]</td>
+       </tr>
+       <tr>
+        <th align="right" nowrap>[% 'Drawing' | $T8 %]</th>
+        <td>[% L.input_tag("part.drawing", SELF.part.drawing, size=40, "data-validate"="trimmed_whitespaces") %]</td>
+       </tr>
+      </table>
+     </td>
+    </tr>
+
+ [% PROCESS 'part/_pricegroup_prices.html' %]
+ [% PROCESS 'part/_customerprices.html' %]
+[%- UNLESS SELF.part.is_assembly %]
+ [% PROCESS 'part/_makemodel.html' %]
+[% END %]
+
+  <tr>
+    <td><hr size="3" noshade></td>
+  </tr>
+ </table>
diff --git a/templates/webpages/part/_customerprice_row.html b/templates/webpages/part/_customerprice_row.html
new file mode 100644 (file)
index 0000000..5888bba
--- /dev/null
@@ -0,0 +1,23 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE HTML %]
+[%- USE LxERP %]
+        <tr class="listrow customerprice_row">
+         <td style='display:none'>
+         [% L.hidden_tag("customerprices[+].customer_id", customerprice.customer_id) %]
+         [% L.hidden_tag("customerprices[].id"   , customerprice.id) %]
+         </td>
+         <td align="center">
+           [%- L.button_tag("kivi.Part.delete_customerprice_row(this)",
+                            LxERP.t8("X")) %] [% # , confirm=LxERP.t8("Are you sure?")) %]
+         </td>
+         <td><span name="position" class="numeric">[% HTML.escape(customerprice.sortorder) %]</span></td>
+         <td align="center">
+           <img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]" class="dragdrop">
+         </td>
+         <td>[% customerprice.customer.customernumber | html %]</td>
+         <td>[% customerprice.customer.name         | html %] </td>
+         <td>[% L.input_tag('customerprices[].customer_partnumber', customerprice.customer_partnumber, size=30 ) %]</td>
+         <td>[% L.input_tag('customerprices[].price_as_number'    , customerprice.price_as_number    , size=15 , class="reformat_number numeric") %]</td>
+         <td>[% L.hidden_tag('customerprices[].lastupdate'         , customerprice.lastupdate.to_kivitendo) %][% customerprice.lastupdate.to_kivitendo | html %]</td>
+        </tr>
diff --git a/templates/webpages/part/_customerprices.html b/templates/webpages/part/_customerprices.html
new file mode 100644 (file)
index 0000000..3f7eeb6
--- /dev/null
@@ -0,0 +1,42 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE HTML %]
+[%- USE LxERP %]
+  <tr>
+  </tr>
+  <tr>
+    <td>
+      <table id="customerprice_table">
+        <thead>
+         <tr>
+          <th class="listheading" style='text-align:center' nowrap width="1"><img src="image/close.png" alt="[%- LxERP.t8('delete item') %]"></th>
+          <th class="listheading">[% 'position'     | $T8 %]</th>
+          <th class="listheading" style='text-align:center' nowrap width="1"><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]"></th>
+          <th class="listheading" style='width:12em'>[% 'Customer Number'      | $T8 %]</th>
+          <th class="listheading">[% 'Customer'             | $T8 %]</th>
+          <th class="listheading">[% 'Customer Part Number' | $T8 %]</th>
+          <th class="listheading">[% 'Customer Price'       | $T8 %]</th>
+          <th class="listheading">[% 'Updated'              | $T8 %]</th>
+         </tr>
+        </thead>
+        <tbody id="customerprice_rows">
+        [% SET listrow = 0 %]
+        [%- FOREACH customerprice = SELF.part.customerprices_sorted %]
+        [% listrow = listrow + 1 %]
+        [% PROCESS 'part/_customerprice_row.html' customerprice=customerprice listrow=listrow %]
+        [%- END %]
+       </tbody>
+       <tbody>
+        <tr>
+         <td></td>
+         <td></td>
+         <td></td>
+         <td align="right">[% 'Customer' | $T8 %]</td>
+         <td rowspan="2">[% P.customer_vendor.customer_picker('add_customerprice', '', style='width: 300px', class="add_customerprice_input", action={commit_one='kivi.Part.add_customerprice_row'}) %]</td>
+         <td rowspan="2" align="right">[% L.button_tag('kivi.Part.add_customerprice_row()', LxERP.t8('Add')) %]</td>
+        </tr>
+       </tbody>
+      </table>
+    </td>
+  </tr>
+  [% L.sortable_element('#customerprice_rows') %]
diff --git a/templates/webpages/part/_cvars.html b/templates/webpages/part/_cvars.html
new file mode 100644 (file)
index 0000000..04233e1
--- /dev/null
@@ -0,0 +1,15 @@
+[%- USE HTML  %][%- USE T8 -%]
+
+ <p>[% 'Unchecked custom variables will not appear in orders and invoices.' | $T8 %]</p>
+
+  <table>
+   [%- FOREACH var = CUSTOM_VARIABLES %]
+   <tr>
+    <td align="right" valign="top">[% var.VALID_BOX %]</td>
+    [%- IF !var.partsgroup_filtered %]
+      <td align="right" valign="top">[% HTML.escape(var.description) %]</td>
+    [%- END %]
+    <td valign="top">[% var.HTML_CODE %]</td>
+   </tr>
+   [%- END %]
+  </table>
diff --git a/templates/webpages/part/_edit_translations.html b/templates/webpages/part/_edit_translations.html
new file mode 100644 (file)
index 0000000..5a540ee
--- /dev/null
@@ -0,0 +1,24 @@
+[%- USE HTML %][%- USE L -%][%- USE P -%][%- USE LxERP -%]
+
+<div id="translations_tab">
+ <table>
+  <tr class="listheading">
+   <th>[% LxERP.t8("Language") %]</th>
+   <th>[% LxERP.t8("Description") %]</th>
+   <th>[% LxERP.t8("Long Description") %]</th>
+  </tr>
+
+  [%- FOREACH language = SELF.all_languages %]
+   [% SET language_id = language.id
+          translation = translations_map.$language_id %]
+   <tr class="listrow" valign="top">
+    <td>
+      [% L.hidden_tag('translations[+].language_id', language.id) %]
+      [% HTML.escape(language.description) %]
+    </td>
+    <td>[% L.areainput_tag("translations[].translation", translation.translation, size=40) %]</td>
+    <td>[% L.textarea_tag("translations[].longdescription", P.restricted_html(translation.longdescription), id="translations_longdescription_" _ language_id, class="texteditor", style="width: 500px; height: 100px") %]</td>
+   </tr>
+  [%- END %]
+ </table>
+</div>
diff --git a/templates/webpages/part/_inventory.html b/templates/webpages/part/_inventory.html
new file mode 100644 (file)
index 0000000..8b48b18
--- /dev/null
@@ -0,0 +1,29 @@
+[%- USE HTML %][%- USE L -%][%- USE P -%][%- USE LxERP -%][%- USE T8 -%]
+
+[%- IF AUTH.assert('warehouse_management', 1) -%]
+<p>
+[% 'Actions' | $T8 %]:
+ <span><a href="controller.pl?action=Inventory/stock_in&part_id=[% HTML.escape(SELF.part.id)%]&select_default_bin=1">[% 'Stock' | $T8 %]</a></span>
+ <span><a href="wh.pl?trans_type=transfer&action=transfer_warehouse_selection&parts_id=[% HTML.escape(SELF.part.id) %]">[% 'Transfer' | $T8 %]</a></span>
+ <span><a href="wh.pl?action=transfer_warehouse_selection&trans_type=removal&parts_id=[% HTML.escape(SELF.part.id) %]">[% 'Removal' | $T8 %]</a></span>
+</p>
+[%- END -%]
+
+<div id="inventory_data">
+</div>
+
+<script type='text/javascript'>
+$(function() {
+  $('.tabwidget').on('tabsbeforeactivate', function(event, ui){
+    if (ui.newPanel.attr('id') == 'inventory') {
+      $.ajax({
+        url: 'controller.pl?action=Part/inventory&id=[% SELF.part.id %]',
+        success: function (html) {
+          $("#inventory_data").html(html);
+        },
+      });
+    }
+    return 1;
+   });
+});
+</script>
diff --git a/templates/webpages/part/_inventory_data.html b/templates/webpages/part/_inventory_data.html
new file mode 100644 (file)
index 0000000..d04edd0
--- /dev/null
@@ -0,0 +1,93 @@
+[%- USE HTML %][%- USE L -%][%- USE P -%][%- USE LxERP -%][%- USE T8 -%]
+
+[%- SET dec = 2 %]
+[%- SET show_warehouse_subtotals = 1 %]
+
+<div id="stock_levels">
+
+<h3>[% 'Stock levels' | $T8 %]</h3>
+
+[%- IF SELF.stock_amounts.size %]
+<a href="wh.pl?action=report&partnumber=[% HTML.escape(SELF.part.partnumber) %]">[% 'Stock levels' | $T8 %]</a>:
+<table>
+ <thead>
+  <tr class='listheading'>
+   <th>[% 'Warehouse'   | $T8 %]</th>
+   <th>[% 'Bin'         | $T8 %]</th>
+   <th>[% 'Qty'         | $T8 %]</th>
+   <th>[% 'Unit'        | $T8 %]</th>
+   <th>[% 'Stock value' | $T8 %]</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH stock = SELF.stock_amounts %]
+  <tr class='listrow'>
+   <td                >[% HTML.escape(stock.warehouse_description)  %]</td>
+   <td                >[% IF stock.order_link %]<a target="_blank" href="[% stock.order_link %]">[% END %]
+                       [% HTML.escape(stock.bin_description)        %]
+                       [% IF stock.order_link %]</a>[% END %]
+   </td>
+   <td class='numeric'>[% LxERP.format_amount(stock.qty, dec)       %]</td>
+   <td                >[% HTML.escape(stock.unit)                   %]</td>
+   <td class='numeric'>[% LxERP.format_amount(stock.stock_value, 2) %]</td>
+  </tr>
+  [% IF show_warehouse_subtotals AND stock.wh_lead != stock.warehouse_description %]
+  <tr class='listheading'>
+   <th                >[% HTML.escape(stock.warehouse_description)           %]</th>
+   <td></td>
+   <td class='numeric bold'>[% LxERP.format_amount(stock.wh_run_qty, dec)         %]</td>
+   <td></td>
+   <td class='numeric bold'>[% LxERP.format_amount(stock.wh_run_stock_value, dec) %]</td>
+  </tr>
+  [% END %]
+  [% IF loop.last %]
+  <tr class='listheading'>
+   <th>[% 'Total' | $T8 %]</th>
+   <td></td>
+   <td class='numeric bold'>[% LxERP.format_amount(stock.run_qty, dec)         %]</td>
+   <td></td>
+   <td class='numeric bold'>[% LxERP.format_amount(stock.run_stock_value, dec) %]</td>
+  </tr>
+  [% END %]
+ [% END %]
+ </tbody>
+</table>
+[% ELSE %]
+  <p>[% 'No transactions yet.' | $T8 %]</p>
+[% END %]
+</div>
+
+[% IF AUTH.assert('warehouse_management', 1) %]
+<div>
+<h3>[% 'Journal of Last 10 Transfers' | $T8 %]</h3>
+<a href="wh.pl?action=journal&partnumber=[% HTML.escape(SELF.part.partnumber) %]">[% 'WHJournal' | $T8 %]</a>:
+[%- IF SELF.journal.size %]
+<table>
+ <tr class='listheading'>
+  <th>[% 'Date'           | $T8 %]</th>
+  <th>[% 'Trans Type'     | $T8 %]</th>
+  <th>[% 'Warehouse From' | $T8 %]</th>
+  <th>[% 'Qty'            | $T8 %]</th>
+  <th>[% 'Unit'           | $T8 %]</th>
+  <th>[% 'Warehouse To'   | $T8 %]</th>
+  <th>[% 'Charge Number'  | $T8 %]</th>
+  <th>[% 'Comment'        | $T8 %]</th>
+ </tr>
+[% FOREACH row = SELF.journal %]
+ <tr class='listrow'>
+  <td>[% row.base.itime_as_date  %]</td>
+  <td>[% row.base.trans_type.description | $T8 %]</td>
+  <td>[% row.out ? row.out.bin.full_description : '-' | html %]</td>
+  <td class='numeric'>[% row.in ? row.in.qty_as_number : LxERP.format_amount(-1 * row.out.qty, 2) %]</td>
+  <td>[% row.base.part.unit | html %]</td>
+  <td>[% row.in ? row.in.bin.full_description : '-' | html %]</td>
+  <td>[% row.base.chargenumber | html %]</td>
+  <td>[% row.base.comment | html %]</td>
+ </tr>
+[% END %]
+</table>
+[%- ELSE %]
+<p>[% 'No transactions yet.' | $T8 %]</p>
+[%- END %]
+</div>
+[% END # assert warehouse_management %]
diff --git a/templates/webpages/part/_makemodel.html b/templates/webpages/part/_makemodel.html
new file mode 100644 (file)
index 0000000..e157977
--- /dev/null
@@ -0,0 +1,43 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE P %]
+[%- USE HTML %]
+[%- USE LxERP %]
+  <tr>
+  </tr>
+  <tr>
+    <td>
+      <table id="makemodel_table">
+        <thead>
+         <tr>
+          <th class="listheading" style='text-align:center' nowrap width="1"><img src="image/close.png" alt="[%- LxERP.t8('delete item') %]"></th>
+          <th class="listheading">[% 'position'     | $T8 %]</th>
+          <th class="listheading" style='text-align:center' nowrap width="1"><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]"></th>
+          <th class="listheading" style='width:12em'>[% 'Vendor Number' | $T8 %]</th>
+          <th class="listheading">[% 'Vendor'        | $T8 %]</th>
+          <th class="listheading">[% 'Model'         | $T8 %]</th>
+          <th class="listheading">[% 'Last Cost'     | $T8 %]</th>
+          <th class="listheading">[% 'Updated'       | $T8 %]</th>
+         </tr>
+        </thead>
+        <tbody id="makemodel_rows">
+        [% SET listrow = 0 %]
+        [%- FOREACH makemodel = SELF.part.makemodels %]
+        [% listrow = listrow + 1 %]
+        [% PROCESS 'part/_makemodel_row.html' makemodel=makemodel listrow=listrow %]
+        [%- END %]
+       </tbody>
+       <tbody>
+        <tr>
+         <td></td>
+         <td></td>
+         <td></td>
+         <td align="right">[% 'Vendor' | $T8 %]</td>
+         <td rowspan="2">[% P.customer_vendor.picker('add_makemodel', '', type='vendor', style='width: 300px', class="add_makemodel_input", action={commit_one='kivi.Part.add_makemodel_row'}) %]</td>
+         <td rowspan="2" align="right">[% L.button_tag('kivi.Part.add_makemodel_row()', LxERP.t8('Add')) %]</td>
+        </tr>
+       </tbody>
+      </table>
+    </td>
+  </tr>
+  [% L.sortable_element('#makemodel_rows') %]
diff --git a/templates/webpages/part/_makemodel_row.html b/templates/webpages/part/_makemodel_row.html
new file mode 100644 (file)
index 0000000..8e7308c
--- /dev/null
@@ -0,0 +1,23 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE HTML %]
+[%- USE LxERP %]
+        <tr class="listrow makemodel_row">
+         <td style='display:none'>
+         [% L.hidden_tag("makemodels[+].make", makemodel.make) %]
+         [% L.hidden_tag("makemodels[].id"   , makemodel.id) %]
+         </td>
+         <td align="center">
+           [%- L.button_tag("kivi.Part.delete_makemodel_row(this)",
+                            LxERP.t8("X")) %] [% # , confirm=LxERP.t8("Are you sure?")) %]
+         </td>
+         <td><span name="position" class="numeric">[% HTML.escape(makemodel.sortorder) %]</span></td>
+         <td align="center">
+           <img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]" class="dragdrop">
+         </td>
+         <td>[% makemodel.vendor.vendornumber | html %]</td>
+         <td>[% makemodel.vendor.name         | html %] </td>
+         <td>[% L.input_tag('makemodels[].model'              , makemodel.model                   , size=30 ) %]</td>
+         <td>[% L.input_tag('makemodels[].lastcost_as_number' , makemodel.lastcost_as_number      , size=15 , class="reformat_number numeric") %]</td>
+         <td>[% L.hidden_tag('makemodels[].lastupdate'         , makemodel.lastupdate.to_kivitendo) %][% makemodel.lastupdate.to_kivitendo | html %]</td>
+        </tr>
diff --git a/templates/webpages/part/_multi_items_dialog.html b/templates/webpages/part/_multi_items_dialog.html
new file mode 100644 (file)
index 0000000..a862828
--- /dev/null
@@ -0,0 +1,27 @@
+[%- USE T8 %][%- USE HTML %][%- USE L %][%- USE LxERP %]
+
+<form method="post" id="multi_items_form" method="POST">
+
+<table id='multi_items_filter_table'>
+  <tr>
+    <th>[%- LxERP.t8("Description") %]/[%- LxERP.t8("Partnumber") %]:</th>
+    <td>[%- L.input_tag('multi_items_filter', search_term) %]</td>
+    <th>[%- LxERP.t8("Partsgroup") %]</th>
+    <td>[%- L.select_tag('multi_items.filter.partsgroup_id', all_partsgroups, title_key='partsgroup', value_key='id', with_empty=1) %]</td>
+  <tr>
+</table>
+
+[% L.button_tag('', LxERP.t8('Filter'), id='multi_items_filter_button') %]
+[% L.button_tag('', LxERP.t8('Reset'), id='multi_items_filter_reset') %]
+
+<hr>
+<div id='multi_items_result'></div>
+<hr>
+
+[%- IF FORM.show_pos_input -%]
+  [% 'At position' | $T8 %]
+  [% L.input_tag('multi_items.position', '', id='multi_items_position', size=5, class="numeric") %]
+[%- END -%]
+[% L.button_tag('', LxERP.t8('Continue'), id='continue_button') %]
+
+</form>
diff --git a/templates/webpages/part/_multi_items_result.html b/templates/webpages/part/_multi_items_result.html
new file mode 100644 (file)
index 0000000..dc12aca
--- /dev/null
@@ -0,0 +1,33 @@
+[%- USE T8 %][%- USE HTML %][%- USE L %][%- USE LxERP %][% USE P %]
+
+<table id="multi_items">
+    <tr class="listheading">
+      <td>[% 'for all' | $T8 %]
+      <td>[% L.input_tag("multi_items.all_qty", '', size = 5, class='numeric') %]</td>
+    </tr>
+    <tr>
+      <td colspan="6"><hr></td>
+    </tr>
+    <tr>
+      <th></th>
+      <th>[% 'Qty'        | $T8 %]</th>
+      <th>[% 'Unit'       | $T8 %]</th>
+      <th>[% 'Article'    | $T8 %]</th>
+      <th>[% 'Sellprice'  | $T8 %]</th>
+      <th>[% 'Partsgroup' | $T8 %]</th>
+    </tr>
+  [%- FOREACH item = multi_items %]
+    <tr class="listrow">
+      <td></td>
+      <td>
+        [% L.hidden_tag("add_items[+].parts_id", item.id) %]
+        [% L.input_tag("add_items[].qty_as_number", '', size = 5,
+                       class = 'multi_items_qty numeric') %]
+      </td>
+      <td>[% HTML.escape(item.unit) %]</td>
+      <td>[% item.presenter.part %] [% HTML.escape(item.description) %]</td>
+      <td class="numeric">[% HTML.escape(item.sellprice_as_number) %]</td>
+      <td class="numeric">[% HTML.escape(item.partsgroup.partsgroup) %]</td>
+    </tr>
+  [%- END %]
+</table>
index d283e6c..cdcbf8d 100644 (file)
@@ -1,6 +1,7 @@
 [%- USE T8 %]
 [%- USE HTML %]
 [%- USE L %]
+[%- USE P %]
 [%- USE LxERP %]
 
 [%# L.dump(SELF.parts) %]
   <input type='hidden' class='part_picker_id' value='[% part.id %]'>
   <input type='hidden' class='part_picker_partnumber' value='[% part.partnumber %]'>
   <input type='hidden' class='part_picker_description' value='[% part.displayable_name %]'>
+  <input type='hidden' class='part_picker_ean' value='[% part.ean %]'>
   <input type='hidden' class='part_picker_unit' value='[% part.unit %]'>
   <span class='ppp_block_number'>[% part.partnumber | html %]</span>
+  <span class='ppp_block_ean'>[% part.ean | html %]</span>
   <span class='ppp_block_description'>[% part.description | html %]</span>
   <div style='clear:both;'></div>
   <span class='ppp_block_sellprice'>[% 'Sellprice' | $T8 %]: [% part.sellprice_as_number | html %]</span>
+  <span class='ppp_block_description'>[% part.presenter.typeclass_abbreviation %]</span>
 </div>
 [%- END %]
 
@@ -26,7 +30,6 @@
 <div style='clear:both'></div>
 
 [% L.paginate_controls(target='#part_picker_result', selector='#part_picker_result', models=SELF.models) %]
-
 <script type='text/javascript'>
-  kivi.PartPicker($('#'+$('#part_picker_real_id').val())).init_results()
+  $('#'+$('#part_picker_real_id').val()).data("part_picker").dialog.init_results();
 </script>
diff --git a/templates/webpages/part/_pricegroup_prices.html b/templates/webpages/part/_pricegroup_prices.html
new file mode 100644 (file)
index 0000000..f481724
--- /dev/null
@@ -0,0 +1,24 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE HTML %]
+[%- USE LxERP %]
+  <tr>
+    <td>
+      <table width=50%>
+        <tr>
+          <th class="listheading">[% 'Price group' | $T8 %]</th>
+          <th class="listheading">[% 'Price'       | $T8 %]</th>
+        </tr>
+        [%- FOREACH pricegroup = SELF.all_pricegroups %]
+          [% SET pricegroup_id = pricegroup.id
+                 price         = prices_map.$pricegroup_id %]
+        <tr class="listrow[% loop.count % 2 %]">
+          <td style='display:none'>[% L.hidden_tag('prices[+].pricegroup_id', pricegroup.id) %]
+          [% L.hidden_tag('prices[].price_id', price.id) # id not used? %]</td>
+          <td width=50%>[% L.hidden_tag('prices[].pricegroup', pricegroup.pricegroup) %][% HTML.escape(pricegroup.pricegroup) %]</td>
+          <td width=50%>[% L.input_tag('prices[].price', price.price_as_number, size=11, class='numeric reformat_number') %]</td>
+        </tr>
+        [%- END %]
+      </table>
+    </td>
+  </tr>
diff --git a/templates/webpages/part/_sales_price_information.html b/templates/webpages/part/_sales_price_information.html
new file mode 100644 (file)
index 0000000..012a68b
--- /dev/null
@@ -0,0 +1,34 @@
+<div id='sales_price_information_sales_order'></div>
+<div id='sales_price_information_sales_quotation'></div>
+<div id='parts_price_history'></div>
+
+<script type='text/javascript'>
+  function get_report(target, source, data){
+    $.ajax({
+      url:        source,
+//      beforeSend: function () { $(target).html('<img src="image/spinner.gif">') },
+      success:    function (rsp) {
+        $(target).html(rsp);
+        $(target).find('.paginate').find('a').click(function(event){ redirect_event(event, target) });
+        $(target).find('a.report-generator-header-link').click(function(event){ redirect_event(event, target) });
+      },
+      data:       data,
+    });
+  };
+
+  function redirect_event(event, target){
+    event.preventDefault();
+    get_report(target, event.target + '', {});
+  }
+
+  $('.tabwidget').on('tabsbeforeactivate', function(event, ui){
+    if (ui.newPanel.attr('id') == 'sales_price_information') {
+      get_report('#sales_price_information_sales_order', 'controller.pl', { action: 'SellPriceInformation/list', 'filter.part.id': [% id %], 'filter.order.type': 'sales_order' });
+      get_report('#sales_price_information_sales_quotation', 'controller.pl', { action: 'SellPriceInformation/list', 'filter.part.id': [% id %], 'filter.order.type': 'sales_quotation' });
+      get_report('#parts_price_history', 'controller.pl', { action: 'PartsPriceHistory/list', 'filter.part_id': [% id %] });
+    }
+    return 1;
+  });
+
+
+</script>
diff --git a/templates/webpages/part/_shop.html b/templates/webpages/part/_shop.html
new file mode 100644 (file)
index 0000000..3c65b4d
--- /dev/null
@@ -0,0 +1,89 @@
+[%- USE HTML %][%- USE L -%][%- USE P -%][%- USE LxERP -%]
+[%- USE Dumper %]
+[%- USE JavaScript -%]
+<div id="shop_variables">
+ <h2>[% LxERP.t8("Active shops:") %]</h2>
+ <table width="100%">
+  <thead>
+  <tr class="listheading">
+   <th>[% LxERP.t8("Shop") %]</th>
+   <th>[% LxERP.t8("Active") %]</th>
+   <th>[% LxERP.t8("Shop part") %]</th>
+   <th>[% LxERP.t8("Price source") %]</th>
+   <th>[% LxERP.t8("Price") %]</th>
+   <th>[% LxERP.t8("Stock Local/Shop") %]</th>
+   <th>[% LxERP.t8("Last update") %]</th>
+   <th>[% LxERP.t8("Action") %]</th>
+   <th>[% LxERP.t8("Action") %]</th>
+   <th>[% LxERP.t8("Action") %]</th>
+  </tr>
+  </thead>
+  [%#  L.dump(SELF.part) %]
+  [%- FOREACH shop_part = SELF.part.shop_parts %]
+  [% IF !shop_part.shop.obsolete %]
+
+  <tr class="listrow">
+   <td>[% HTML.escape( shop_part.shop.description ) %]</td>
+   <td>[% L.html_tag('span', shop_part.active, id => 'shop_part_active_' _ shop_part.id ) %]</td>
+   <td>
+    [% IF shop_part.shop.use_part_longdescription %]
+      [% L.html_tag('span', shop_part.part.notes, id => 'shop_part_description_' _ shop_part.id ) %]
+    [% ELSE %]
+      [% L.html_tag('span', shop_part.shop_description, id => 'shop_part_description_' _ shop_part.id ) %]
+    [% END %]
+  </td>
+   <td>[% L.html_tag('span',LxERP.t8(), id => 'active_price_source_' _ shop_part.id) %] </td>
+   <td>[% L.html_tag('span','Price', id => 'price_' _ shop_part.id) %]</td>
+   <td>[% L.html_tag('span','Stock', id => 'stock_' _ shop_part.id) %]</td>
+   <td>[% L.html_tag('span', shop_part.last_update.to_kivitendo('precision' => 'minute'), id => 'shop_part_last_update_' _ shop_part.id ) %]</td>
+   <td>[% L.button_tag("kivi.ShopPart.edit_shop_part(" _ shop_part.id _ ")", LxERP.t8("Edit"))  %]</td>
+   <td>[% L.button_tag("kivi.ShopPart.update_shop_part(" _ shop_part.id _ ")", LxERP.t8("Upload"))  %]</td>
+   <td>[% L.button_tag("kivi.ShopPart.get_all_categories(" _ shop_part.id _ ")", LxERP.t8("Shopcategories"))  %]<br>
+    [% IF shop_part.shop_category %]
+      [% IF shop_part.shop_category.1.size > 1%]
+        [% FOREACH cat = shop_part.shop_category %]
+          [% HTML.escape(cat.1) %]<br>
+        [% END %]
+      [% ELSE %]
+        [% HTML.escape(shop_part.shop_category.1) %]<br>
+      [% END %]
+    [% END %]
+   </td>
+  </tr>
+  <script type="text/javascript">
+    $(function() {
+      kivi.ShopPart.update_price_n_price_source([% shop_part.id %],'[% shop_part.active_price_source %]');
+      kivi.ShopPart.update_stock([% shop_part.id %]);
+    });
+  </script>
+  [% END %]
+  [%- END %]
+  [%- FOREACH shop = SELF.shops_not_assigned %]
+  <tr>
+   <td>[% HTML.escape( shop.description ) %]</td>
+   <td></td>
+   <td></td>
+   <td></td>
+   <td></td>
+   <td></td>
+   <td>[% L.button_tag("kivi.ShopPart.create_shop_part(" _ id _ ", " _ shop.id _ ")", LxERP.t8("Add"))  %]</td>
+  </tr>
+  </thead>
+  [%- END %]
+</table>
+
+
+[% # L.dump(shop_part) %]
+<h2>[% LxERP.t8("Shopimages - valid for all shops") %]</h2>
+  [%- IF shop_part.part_id %]
+    <script type="text/javascript">
+      $(function() {
+        kivi.ShopPart.show_images([% shop_part.part_id %]);
+      });
+    </script>
+    <div id="shop_images" border=1 ></div>
+  [%- ELSE %]
+    <div id="shop_images" border=1 >[% LxERP.t8('To upload images: Please create shoppart first') %]</div>
+  [%- END %]
+</div>
+
diff --git a/templates/webpages/part/form.html b/templates/webpages/part/form.html
new file mode 100644 (file)
index 0000000..05e73f3
--- /dev/null
@@ -0,0 +1,104 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE P %]
+<h1>[% FORM.title %] [% IF SELF.part.id %]: [% HTML.escape(SELF.part.displayable_name) %][% END %]</h1>
+
+[% INCLUDE 'common/flash.html' %]
+
+ <form method="post" id="ic" name="ic" action="controller.pl">
+
+  [% L.hidden_tag('part.part_type'   , SELF.part.part_type) %]
+  [% L.hidden_tag('part.id'          , SELF.part.id) %]
+  [% L.hidden_tag('last_modification', SELF.part.last_modification) %]
+  [% L.hidden_tag('callback'         , FORM.callback) %]
+
+  <div id="ic_tabs" class="tabwidget">
+   <ul>
+    <li><a href="#basic_data">[% 'Basic Data' | $T8 %]</a></li>
+    [%- IF SELF.part.is_assortment %]
+    <li><a href="#assortment_tab">[% 'Assortment items' | $T8 %]</a></li>
+    [%- END %]
+    [%- IF SELF.part.is_assembly %]
+    <li><a href="#assembly_tab">[% 'Assembly items' | $T8 %]</a></li>
+    [%- END %]
+    [%- IF SELF.part.id %]
+    [%- IF INSTANCE_CONF.get_doc_storage %]
+    <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=part&object_id=[% SELF.part.id %]">[% 'Attachments' | $T8 %]</a></li>
+    <li><a href="controller.pl?action=File/list&file_type=image&object_type=part&object_id=[% SELF.part.id %]">[% 'Images' | $T8 %]</a></li>
+    [%- END %]
+    [%- END %]
+    [% IF SELF.all_languages.size %]
+    <li><a href="#translations_tab">[% 'Translations' | $T8 %]</a></li>
+    [% END %]
+    [%- IF SELF.part.id %]
+    <li><a href="#sales_price_information">[% 'Price information' | $T8 %]</a></li>
+    [%- END %]
+    [%- IF SELF.part.id  %]
+    <li><a href="#price_rules">[% 'Price Rules' | $T8 %]</a></li>
+    [% END %]
+    [%- IF (AUTH.assert('warehouse_contents', 1) AND SELF.part.id AND NOT SELF.part.is_service) %]
+    <li><a href="#inventory">[% 'Inventories' | $T8 %]</a></li>
+    [%- END %]
+    [%- IF CUSTOM_VARIABLES.size %]
+    <li><a href="#custom_variables">[% 'Custom Variables' | $T8 %]</a></li>
+    [%- END %]
+    [% IF AUTH.assert('shop_part_edit', 1) && SELF.part.id && SELF.part.shop %]
+    <li><a href="#shop_variables">[% 'Shop variables' | $T8 %]</a></li>
+    [% END %]
+   </ul>
+
+   <div id="basic_data">
+   [%- PROCESS 'part/_basic_data.html' %]
+   </div>
+
+   [%- IF SELF.part.is_assortment %]
+   <div id="assortment_tab">
+    [% PROCESS 'part/_assortment.html' id=part.id assortment_id=SELF.part.id %]
+   </div>
+   [%- END %]
+
+   [%- IF SELF.part.is_assembly %]
+   <div id="assembly_tab">
+    [% PROCESS 'part/_assembly.html' id=part.id assembly_id=SELF.part.id %]
+   </div>
+   [%- END %]
+
+   [%- IF SELF.all_languages.size %]
+    [% PROCESS 'part/_edit_translations.html' %]
+   [%- END %]
+
+   [%- IF SELF.part.id %]
+   <div id="sales_price_information">
+     [% PROCESS part/_sales_price_information.html id=SELF.part.id %]
+   </div>
+   [% IF AUTH.assert('shop_part_edit', 1) && SELF.part.id %]
+   <div id="shop_variables">
+     [% PROCESS 'part/_shop.html' %]
+   </div>
+   [%- END %]
+
+   [%- IF AUTH.assert('warehouse_contents', 1) AND SELF.part.id AND NOT SELF.part.is_service %]
+   <div id="inventory">
+    [% PROCESS 'part/_inventory.html' %]
+   </div>
+   [%- END %]
+
+   [%- END %]
+
+   [%- IF CUSTOM_VARIABLES.size %]
+   <div id="custom_variables">
+      [%- PROCESS 'part/_cvars.html' %]
+   </div>
+   [%- END %]
+
+   [%- IF SELF.part.id %]
+   <div id='price_rules'>
+     <div id='price_rules_customer_report'></div>
+     <div id='price_rules_vendor_report'></div>
+   </div>
+   [%- END %]
+
+</div>
+</form>
diff --git a/templates/webpages/part/history.html b/templates/webpages/part/history.html
new file mode 100644 (file)
index 0000000..d8931fb
--- /dev/null
@@ -0,0 +1,19 @@
+[% USE T8 %]
+[% USE HTML %]
+
+<table>
+<tr>
+<th>[% 'Time'       | $T8 %]</th>
+<th>[% 'Aktion'     | $T8 %]</th>
+<th>[% 'Employee'   | $T8 %]</th>
+<th>[% 'Partnumber' | $T8 %]</th>
+</tr>
+[% FOREACH history = history_entries %]
+<tr>
+ <td>[% history.itime.to_kivitendo %] [% history.itime.to_kivitendo_time %]</td>
+ <td>[% history.addition | $T8 %]</td>
+ <td>[% HTML.escape(history.employee.name) %]</td>
+ <td>[% HTML.escape(history.parsed_snumber) %]</td>
+</tr>
+[% END %]
+</table>
index 5669d8e..3147fd1 100644 (file)
@@ -5,8 +5,8 @@
 
 <div style='overflow:hidden'>
 
-[% LxERP.t8("Filter") %]: [% L.input_tag('part_picker_filter', SELF.models.filtered.laundered.all_substr_multi__ilike, class='part_picker_filter') %]
 [% L.hidden_tag('part_picker_real_id', FORM.real_id) %]
+[% LxERP.t8("Filter") %]: [% L.input_tag('part_picker_filter', search_term, class='part_picker_filter') %]
 
 <div class='float-right'>
   [% L.checkbox_tag('no_paginate', checked=FORM.no_paginate, id='no_paginate', for_submit=1, label=LxERP.t8('All as list')) %]
 <div style='clear:both'></div>
 <div id='part_picker_result'></div>
 </div>
-
-<script type='text/javascript'>
-  var pp = kivi.PartPicker($('#[% FORM.real_id %]'));
-  $(function(){
-    $('#part_picker_filter').focus();
-    pp.update_results();
-  });
-  $('#part_picker_filter').keypress(pp.result_timer);
-  $('#no_paginate').change(pp.update_results);
-
-</script>
index 61aa105..0101db1 100644 (file)
 [% USE L %]
+[% USE P %]
 
-<h1>Waren Picker Testpage</h1>
+<h1>Part Picker Testpage</h1>
 
 <br>
 Alle: <br>
-[% L.part_picker('part_id') %] text<br>
+[% P.part.picker('part_id') %] text<br>
 Nur Waren: <br>
-[% L.part_picker('part_id2', undef, type='part') %]<br>
+[% P.part.picker('part_id2', undef, part_type='part') %]<br>
 Nur Dienstleistungen: <br>
-[% L.part_picker('part_id3', undef, type='service') %]<br>
+[% P.part.picker('part_id3', undef, part_type='service') %]<br>
+Nur Erzeugnisse: <br>
+[% P.part.picker('part_id4', undef, part_type='assembly') %]<br>
 Waren und Dienstleistungen: <br>
-[% L.part_picker('part_id4', undef, type='part,service') %]<br>
+[% P.part.picker('part_id5', undef, part_type='part,service') %]<br>
+Artikel-Klassifizierung: Einkauf <br>
+[% P.part.picker('part_id10', undef, classification_id='1') %]<br>
+Artikel-Klassifizierung: Verkauf <br>
+[% P.part.picker('part_id11', undef, classification_id='2') %]<br>
+Artikel-Klassifizierung: Handelsware <br>
+[% P.part.picker('part_id12', undef, classification_id='3') %]<br>
+Artikel-Klassifizierung: Produktion <br>
+[% P.part.picker('part_id13', undef, classification_id='4') %]<br>
+Artikel-Klassifizierung: Eink.,Verk.,Prod. <br>
+[% P.part.picker('part_id14', undef, classification_id='1,2,4') %]<br>
+Artikel-Status: Aktiv (default)<br>
+[% P.part.picker('part_id15') %]<br>
+Artikel-Status: Aktiv (explizit)<br>
+[% P.part.picker('part_id16', undef, status="active") %]<br>
+Artikel-Status: Ungültig<br>
+[% P.part.picker('part_id17', undef, status="obsolete") %]<br>
+Artikel-Status: Alle<br>
+[% P.part.picker('part_id18', undef, status="all") %]<br>
+<br>
+Pre-filled:<br>
+[% P.part.picker('part_id6', pre_filled_part) %]<br>
+Convertible unit 'Std': (only select parts with unit Tag/Std/Min)<br>
+[% P.part.picker('part_id7', undef, convertible_unit='Std') %]<br>
+<br>
+With multi select popup<br>
+[% P.part.picker('part_id8', undef, multiple=1) %]<br>
+With multi select popup (only obsolete)<br>
+[% P.part.picker('part_id9', undef, multiple=1, status='obsolete') %]<br>
+<br>
+All parts including make models of all vendors: <br>
+[% P.part.picker('part_id21', undef, with_makemodel=1) %]<br>
+All parts including make models of all vendors with multi select popup: <br>
+[% P.part.picker('part_id22', undef, with_makemodel=1, multiple=1) %]<br>
+All parts including customer partnumbers of all customers: <br>
+[% P.part.picker('part_id23', undef, with_customer_partnumber=1) %]<br>
+<br>
+single select dialog for glass-popup-button; multi select with extra button (and limited to 5 results):<br>
+[% P.part.picker('part_id31', undef, multiple=0, multiple_limit=5) %]
+[% L.button_tag('$("#part_id31").data("part_picker").o.multiple=1; $("#part_id31").data("part_picker").open_dialog()', 'Add multiple items') %]<br>
+
+<h2>Styling</h2>
+
+In a span:
+<span>Leading text: [% P.part.picker('p1', undef, part_type='part,service') %] and text after with spacing</span><br>
+<span>Leading text:[% P.part.picker('p2', undef, part_type='part,service') %]and text after without spacing</span><br>
+<div>Leading text: [% P.part.picker('p3', undef, part_type='part,service') %] and text after with spacing with div</div><br>
+<div>Leading text:[% P.part.picker('p4', undef, part_type='part,service') %]and text after with spacing with div</div><br>
 
+<span>Picker + input next to each other: [% P.part.picker('p5', undef, part_type='part,service', width="100%") %]<input type=text></span>
+
+<div>[% P.part.picker('p6', undef, part_type='part,service', style="width:500px") %] 500px width</div>
+<div>[% P.part.picker('p7', undef, part_type='part,service', style="width:200px") %] 200px width</div>
+<div>[% P.part.picker('p8', undef, part_type='part,service', style="height:40px") %] 40px height</div>
 
 [%# FOREACH i IN 1..50 %]
-[%# L.part_picker('part_id_' _ i) %] <br>
+[%# P.part.picker('part_id_' _ i) %] <br>
 [%# END %]
+
+<h2>In tables</h2>
+
+<p>No classes:</p>
+
+<table>
+ <tr>
+  <th>Part picker in table heading</th>
+  <th>[% P.part.picker('p9', undef, part_type='part,service') %]</th>
+ </tr>
+ <tr>
+  <td>Part picker in table cell</td>
+  <td>[% P.part.picker('p10', undef, part_type='part,service') %]</td>
+ </tr>
+</table>
+
+<p>With classes:</p>
+
+<table>
+ <tr class=listheading>
+  <th>Part picker in table heading</th>
+  <th>[% P.part.picker('p11', undef, part_type='part,service') %]</th>
+ </tr>
+ <tr class=listrow>
+  <td>Part picker in table cell</td>
+  <td>[% P.part.picker('p12', undef, part_type='part,service') %]</td>
+ </tr>
+</table>
diff --git a/templates/webpages/parts_price_history/report_bottom.html b/templates/webpages/parts_price_history/report_bottom.html
new file mode 100644 (file)
index 0000000..587fb06
--- /dev/null
@@ -0,0 +1 @@
+<p align=right>[% PROCESS 'common/paginate.html' pages=SELF.pages, base_url=SELF.self_url %]</p>
diff --git a/templates/webpages/pay_posting_import/form.html b/templates/webpages/pay_posting_import/form.html
new file mode 100644 (file)
index 0000000..06f6f02
--- /dev/null
@@ -0,0 +1,41 @@
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE T8 %]
+[%- INCLUDE 'common/flash.html' %]
+<div class="listtop">[% FORM.title %]</div>
+
+[% IF (SELF.gl_trans.size) %]
+  <p>[% SELF.gl_trans.size %]&nbsp;[% "entries imported" | $T8 %].</p>
+<div style="padding-bottom: 15px">
+ <table id="gl_trans">
+  <thead>
+   <tr>
+    <th class="listheading">[%- LxERP.t8("Date") %]</th>
+    <th class="listheading">[%- LxERP.t8("Description") %]</th>
+    <th class="listheading">[%- LxERP.t8("Debit") %]</th>
+    <th class="listheading">[%- LxERP.t8("Credit") %]</th>
+    <th class="listheading">[%- LxERP.t8("Amount") %]</th>
+   </tr>
+  </thead>
+  <tbody>
+  [%- FOREACH gl = SELF.gl_trans %]
+    <tr class="listrow[% loop.count % 2 %]">
+    <td>[%- gl.transdate.to_kivitendo -%]</td>
+    <td align="left">[%- gl.description -%]</td>
+    <td align="left">[%- gl.transactions.1.chart.accno -%]&nbsp;[%- gl.transactions.1.chart.description -%]</td>
+    <td align="left">[%- gl.transactions.0.chart.accno -%]&nbsp;[%- gl.transactions.0.chart.description -%]</td>
+    <td align="right"> [%- LxERP.format_amount(gl.transactions.0.amount    , 2) %]</td>
+   </tr>
+  [% END %]
+     </tbody>
+ </table>
+ <div>
+[% END %]
+
+<p>
+ [% "Import a File:" | $T8 %]
+</p>
+<form method="post" action="controller.pl" enctype="multipart/form-data" id="form">
+  [% L.input_tag('file', '', type => 'file', accept => '.csv') %]
+</form>
index c77e719..c371c46 100755 (executable)
@@ -1,34 +1,23 @@
-[% USE HTML %][% USE T8 %][% USE L %][% USE LxERP %]
+[% USE HTML %][% USE T8 %][% USE L %][% USE LxERP %][%- USE P -%]
 <h1>[% FORM.title %]</h1>
 
- <form method="post" action="controller.pl">
+ <form method="post" action="controller.pl" id="form">
 
 [%- INCLUDE 'common/flash.html' %]
 
   <table>
-   <tr>
-    <td>[%- 'Description' | $T8 %]</td>
-    <td>
-     <input name="payment_term.description" value="[%- HTML.escape(SELF.payment_term.description) %]">
-    </td>
+   <tr class="listheading">
+    <th></th>
+    <th>[% LxERP.t8("General settings") %]</th>
    </tr>
 
    <tr>
-    <td>[%- 'Long Description' | $T8 %]</td>
+    <td>[%- 'Description' | $T8 %]</td>
     <td>
-     <input name="payment_term.description_long" value="[%- HTML.escape(SELF.payment_term.description_long) %]" size="60">
+     [% P.input_tag("payment_term.description", SELF.payment_term.description, "data-validate"="required", "data-title"=LxERP.t8("Description")) %]
     </td>
    </tr>
 
-   [%- FOREACH language = SELF.languages %]
-    <tr>
-     <td>[%- HTML.escape(language.description) %] ([%- LxERP.t8('Translation') %])</td>
-     <td>
-      <input name="translation_[% language.id %]" value="[%- HTML.escape(SELF.payment_term.translated_attribute('description_long', language, 1)) %]" size="60">
-     </td>
-    </tr>
-   [%- END %]
-
    <tr>
     <td>[% LxERP.t8("Calculate due date automatically") %]</td>
     <td>[% L.yes_no_tag("payment_term.auto_calculation", SELF.payment_term.auto_calculation, "data-auto-calculation-toggle"="1") %]</td>
      <input name="payment_term.percent_skonto_as_percent" value="[%- HTML.escape(SELF.payment_term.percent_skonto_as_percent) %]" size="6">%
     </td>
    </tr>
-  </table>
 
-  <p>
-   <input type="hidden" name="id" value="[% SELF.payment_term.id %]">
-   <input type="hidden" name="action" value="PaymentTerm/dispatch">
-   <input type="submit" class="submit" name="action_[% IF SELF.payment_term.id %]update[% ELSE %]create[% END %]" value="[% 'Save' | $T8 %]">
-   [%- IF SELF.payment_term.id %]
-    <input type="submit" class="submit" name="action_destroy" value="[% 'Delete' | $T8 %]"
-           onclick="if (confirm('[% 'Are you sure you want to delete this payment term?' | $T8 %]')) return true; else return false;">
+   [% IF SELF.payment_term.id %]
+   <tr>
+     <td>[% 'Obsolete' | $T8 %]</td>
+     <td>[% L.checkbox_tag('payment_term.obsolete', checked = SELF.payment_term.obsolete, for_submit=1) %]</td>
+   </tr>
+   </tr>
+   [% END %]
+
+   <tr class="listheading">
+    <th></th>
+    <th>[% LxERP.t8("Texts for quotations & orders") %]</th>
+    <th>[% LxERP.t8("Texts for invoices") %]</th>
+   </tr>
+
+   <tr>
+    <td>[%- 'Long Description' | $T8 %]</td>
+    <td>
+     [% P.input_tag("payment_term.description_long", SELF.payment_term.description_long, size="60", "data-validate"="required", "data-title"=LxERP.t8("Long Description for quotations & orders")) %]
+    </td>
+
+    <td>
+     [% P.input_tag("payment_term.description_long_invoice", SELF.payment_term.description_long_invoice, size="60", "data-validate"="required", "data-title"=LxERP.t8("Long Description for invoices")) %]
+    </td>
+   </tr>
+
+   [%- FOREACH language = SELF.languages %]
+    <tr>
+     <td>[%- HTML.escape(language.description) %] ([%- LxERP.t8('Translation') %])</td>
+     <td>
+      <input name="translation_[% language.id %]" value="[%- HTML.escape(SELF.payment_term.translated_attribute('description_long', language, 1)) %]" size="60">
+     </td>
+
+     <td>
+      <input name="translation_invoice_[% language.id %]" value="[%- HTML.escape(SELF.payment_term.translated_attribute('description_long_invoice', language, 1)) %]" size="60">
+     </td>
+    </tr>
    [%- END %]
-   <a href="[% SELF.url_for(action => 'list') %]">[%- 'Abort' | $T8 %]</a>
-  </p>
+  </table>
+
+  [% P.hidden_tag("id", SELF.payment_term.id) %]
 
   <hr size="3" noshade>
 
@@ -73,8 +91,6 @@
 
   <table>
    <tr class="listheading"><th>[%- LxERP.t8('Field') %]</th><th>[%- LxERP.t8('Description') %]</th></tr>
-   <tr><td>&lt;%payment_terms%&gt;</td><td>[% LxERP.t8("Payment description") %]</td></tr>
-   <tr><td>&lt;%payment_description%&gt;</td><td>[% LxERP.t8("Payment description detail") %]</td></tr>
    <tr><td>&lt;%netto_date%&gt;</td><td>[% LxERP.t8("Date the payment is due in full") %]</td></tr>
    <tr><td>&lt;%skonto_date%&gt;</td><td>[% LxERP.t8("Date the payment is due with discount") %]</td></tr>
    <tr><td>&lt;%skonto_amount%&gt;</td><td>[% LxERP.t8("The deductible amount") %]</td></tr>
index d2f4748..bc56b91 100644 (file)
     <tr class="listheading">
      <th align="center"><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]"></th>
      <th>[%- 'Description' | $T8 %]</th>
-     <th>[%- 'Long Description' | $T8 %]</th>
+     <th>[%- 'Long Description (quotations & orders)' | $T8 %]</th>
+     <th>[%- 'Long Description (invoices)' | $T8 %]</th>
      <th>[% 'Automatic date calculation' | $T8 %]</th>
      <th align="right">[%- 'Netto Terms' | $T8 %]</th>
      <th align="right">[%- 'Skonto Terms' | $T8 %]</th>
      <th align="right">[%- 'Skonto' | $T8 %]</th>
+     <th align="right">[%- 'Obsolete' | $T8 %]</th>
     </tr>
     </thead>
 
       </a>
      </td>
      <td>[%- HTML.escape(payment_term.description_long) %]</td>
+     <td>[%- HTML.escape(payment_term.description_long_invoice) %]</td>
      <td>[% IF payment_term.auto_calculation %][% LxERP.t8("yes") %][% ELSE %][% LxERP.t8("no") %][% END %]</td>
      <td align="right">[%- HTML.escape(payment_term.terms_netto_as_number) %]</td>
      <td align="right">[%- HTML.escape(payment_term.terms_skonto_as_number) %]</td>
      <td align="right">[%- HTML.escape(payment_term.percent_skonto_as_percent) %] %</td>
+     <td align="right">[%- HTML.escape(payment_term.obsolete) %]</td>
     </tr>
     [%- END %]
     </tbody>
    </table>
   [%- END %]
-
-  <p>
-   <a href="[% SELF.url_for(action => 'new') %]">[%- 'Create new payment term' | $T8 %]</a>
-  </p>
  </form>
 
  [% L.sortable_element('#payment_term_list tbody', url => 'controller.pl?action=PaymentTerm/reorder', with => 'payment_term_id') %]
diff --git a/templates/webpages/pe/partsgroup_form.html b/templates/webpages/pe/partsgroup_form.html
deleted file mode 100644 (file)
index 632cd7a..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-[%- USE L %]
-[%- USE T8 %]
-[%- USE HTML %]
-<h1>[% title %]</h1>
-[% L.javascript_tag('show_history.js') %]
-
-<form method=post action="[% script %]">
-
-<input type=hidden name=id value="[% id %]">
-<input type=hidden name=type value="[% type %]">
-
-<table width=100%>
-  <tr>
-    <td>
-      <table width=100%>
-        <tr>
-          <th align=right>[% 'Group' | $T8 %]</th>
-          <td><input name=partsgroup size=30 value="[% partsgroup | html %]"></td>
-        </tr>
-      </table>
-    </td>
-  </tr>
-  <tr>
-    <td colspan=2><hr size=3 noshade></td>
-  </tr>
-</table>
-
-<br>
-
-<input name=callback type=hidden value="[% callback | html %]">
-<input type=submit class=submit name=action value="[% 'Save' | $T8 %]">
-[%- IF id && orphaned %]
-<input type=submit class=submit name=action value="[% 'Delete' | $T8 %]">
-[%- END %]
-
-[%- IF ( id ) %]
-  <input type=button onclick="set_history_window([% id %], 'id');" name=history id=history value="[% 'history' | $T8 %]">
-[%- END %]
-
-</form>
-
diff --git a/templates/webpages/pe/partsgroup_report.html b/templates/webpages/pe/partsgroup_report.html
deleted file mode 100644 (file)
index a9fc79e..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-[%- USE HTML %]
-[%- USE T8 %]
-<h1>[% 'Groups' | $T8 %]</h1>
-
-<table width=100%>
-  <tr>
-    <td>[% option %]</td>
-  </tr>
-  <tr>
-    <td>
-      <table width=100%>
-        <tr class=listheading>
-          <th class=listheading width=90%>[% 'Group' | $T8 %]</th>
-        </tr>
-[%- FOREACH row = item_list %]
-        <tr valign=top class="listrow[% loop.count % 2 %]">
-          <td><a href="[% editlink %]&id=[% row.id %]">[% row.partsgroup %]</a></td>
-        </tr>
-[%- END %]
-      </table>
-    </td>
-  </tr>
-  <tr>
-    <td><hr size=3 noshade></td>
-  </tr>
-</table>
-
-<br>
-<form method=post action="[% script %]">
-  <input name=callback type=hidden value="[% callback | html %]">
-  <input type=hidden name=type value="[% type %]">
-  <input class=submit type=submit name=action value="[% 'Add' | $T8 %]">
-</form>
-
-
diff --git a/templates/webpages/pe/pricegroup_form.html b/templates/webpages/pe/pricegroup_form.html
deleted file mode 100644 (file)
index 850275f..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-[%- USE L %]
-[%- USE T8 %]
-[%- USE HTML %]
-<h1>[% title %]</h1>
-[% L.javascript_tag('show_history.js') %]
-
-<form method=post action="[% script %]">
-
-<input type=hidden name=id value="[% id %]">
-<input type=hidden name=type value="[% type %]">
-
-<table width=100%>
-  <tr>
-    <td>
-      <table width=100%>
-        <tr>
-          <th align=right>[% 'Pricegroup' | $T8 %]</th>
-          <td><input name=pricegroup size=30 value="[% pricegroup | html %]"></td>
-        </tr>
-      </table>
-    </td>
-  </tr>
-  <tr>
-    <td colspan=2><hr size=3 noshade></td>
-  </tr>
-</table>
-
-<br>
-
-<input name=callback type=hidden value="[% callback | html %]">
-<input type=submit class=submit name=action value="[% 'Save' | $T8 %]">
-[%- IF id && orphaned %]
-<input type=submit class=submit name=action value="[% 'Delete' | $T8 %]">
-[%- END %]
-
-[%- IF ( id ) %]
-  <input type=button onclick="set_history_window([% id %], 'id');" name=history id=history value="[% 'history' | $T8 %]">
-[%- END %]
-
-</form>
-
diff --git a/templates/webpages/pe/pricegroup_report.html b/templates/webpages/pe/pricegroup_report.html
deleted file mode 100644 (file)
index 0b04b9d..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-[%- USE HTML %]
-[%- USE T8 %]
-<h1>[% 'Pricegroup' | $T8 %]</h1>
-
-<table width=100%>
-  <tr>
-    <td>[% option %]</td>
-  </tr>
-  <tr>
-    <td>
-      <table width=100%>
-        <tr class=listheading>
-          <th class=listheading width=90%>[% 'Pricegroup' | $T8 %]</th>
-        </tr>
-[%- FOREACH row = item_list %]
-        <tr valign=top class="listrow[% loop.count % 2 %]">
-          <td><a href="[% editlink %]&id=[% row.id %]">[% row.pricegroup %]</a></td>
-        </tr>
-[%- END %]
-      </table>
-    </td>
-  </tr>
-  <tr>
-    <td><hr size=3 noshade></td>
-  </tr>
-</table>
-
-<br>
-<form method=post action="[% script %]">
-  <input name=callback type=hidden value="[% callback | html %]">
-  <input type=hidden name=type value="[% type %]">
-  <input class=submit type=submit name=action value="[% 'Add' | $T8 %]">
-</form>
-
-
diff --git a/templates/webpages/pe/search.html b/templates/webpages/pe/search.html
deleted file mode 100644 (file)
index 871eb34..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-[%- USE T8 %]
-[%- USE LxERP %]
-[%- USE HTML %]
-<h1>[% is_pricegroup ? LxERP.t8('Pricegroup') : LxERP.t8('Groups') %]</h1>
-
-<form method=post action="[% script %]">
-
-<input type=hidden name=sort value="[% is_pricegroup ? 'pricegroup' : 'partsgroup' %]">
-<input type=hidden name=type value="[% type %]">
-
-<table width=100%>
-  <tr>
-    <td>
-      <table width=100%>
-        <tr>
-[%- IF is_pricegroup %]
-          <th align=right width=1%>[% 'Pricegroup' | $T8 %]</th>
-          <td><input name=pricegroup size=20></td>
-[%- ELSE %]
-          <th align=right width=1%>[% 'Group' | $T8 %]</th>
-          <td><input name=partsgroup size=20></td>
-[%- END %]
-        </tr>
-        <tr>
-          <td></td>
-          <td><input name=status class=radio type=radio value=all checked> [% 'All' | $T8 %]
-              <input name=status class=radio type=radio value=orphaned> [% 'Orphaned' | $T8 %]</td>
-        </tr>
-      </table>
-    </td>
-  </tr>
-  <tr>
-    <td><hr size=3 noshade></td>
-  </tr>
-</table>
-
-<input type=hidden name=nextsub value="[% is_pricegroup ? 'pricegroup_report' : 'partsgroup_report' %]">
-
-<br>
-<input class=submit type=submit name=action value="[% 'Continue' | $T8 %]">
-|
-<a href="pe.pl?action=add&type=[% HTML.url(type) %]">[%- LxERP.t8('Add') %]</a>
-</form>
index 93d1863..d2c217f 100644 (file)
@@ -1,8 +1,9 @@
 [%- USE T8 %]
 [%- USE L %]
+[%- USE P %]
 [%- USE LxERP %]
 [%- USE HTML %]
-<form action='controller.pl' method='post'>
+<form action='controller.pl' method='post' id='search_form'>
 <div class='filter_toggle'>
 <a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Show Filter' | $T8 %]</a>
   [% SELF.filter_summary | html %]
   </tr>
   <tr>
    <th align="right">[% 'Part' | $T8 %]</th>
-   <td>[% L.part_picker('filter.item_type_matches[].part', FORM.filter.item_type_matches.0.part, style='width: 300px') %]</td>
+   <td>[% P.part.picker('filter.item_type_matches[].part', FORM.filter.item_type_matches.0.part, style='width: 300px') %]</td>
   </tr>
   <tr id='price_rule_filter_customer_tr' [% "style='display:hidden' " UNLESS SELF.vc == 'customer' %]>
    <th align="right">[% 'Customer' | $T8 %]</th>
-   <td>[% L.customer_vendor_picker('filter.item_type_matches[].customer', FORM.filter.item_type_matches.0.customer, type='customer', id='price_rule_filter_customer', style='width: 300px') %]</td>
+   <td>[% P.customer_vendor.picker('filter.item_type_matches[].customer', FORM.filter.item_type_matches.0.customer, type='customer', id='price_rule_filter_customer', style='width: 300px') %]</td>
   </tr>
   <tr id='price_rule_filter_vendor_tr' [% "style='display:hidden' " UNLESS SELF.vc == 'customer' %]>
    <th align="right">[% 'Vendor' | $T8 %]</th>
-   <td>[% L.customer_vendor_picker('filter.item_type_matches[].vendor', FORM.filter.item_type_matches.0.vendor, type='vendor', id='price_rule_filter_vendor', style='width: 300px') %]</td>
+   <td>[% P.customer_vendor.picker('filter.item_type_matches[].vendor', FORM.filter.item_type_matches.0.vendor, type='vendor', id='price_rule_filter_vendor', style='width: 300px') %]</td>
   </tr>
   <tr>
    <th align="right">[% 'Business' | $T8 %]</th>
    <td>[% L.select_tag('filter.item_type_matches[].business', SELF.businesses, title_key='description', default=FORM.filter.item_type_matches.0.business, with_empty=1, style='width: 300px') %]</td>
   </tr>
   <tr>
-   <th align="right">[% 'Group' | $T8 %]</th>
+   <th align="right">[% 'Partsgroup' | $T8 %]</th>
    <td>[% L.select_tag('filter.item_type_matches[].partsgroup', SELF.partsgroups, title_key='partsgroup', default=FORM.filter.item_type_matches.0.partsgroup, with_empty=1, style='width: 300px') %]</td>
   </tr>
   <tr>
@@ -47,7 +48,7 @@
    <td>[% L.date_tag('filter.item_type_matches[].reqdate', FORM.filter.item_type_matches.0.reqdate, style='width: 300px') %]</td>
   </tr>
   <tr>
-   <th align="right">[% 'Transdate' | $T8 %]</th>
+   <th align="right">[% 'Transdate Record' | $T8 %]</th>
    <td>[% L.date_tag('filter.item_type_matches[].transdate', FORM.filter.item_type_matches.0.transdate, style='width: 300px') %]</td>
   </tr>
   <tr>
      [%- END %]
    </td>
   </tr>
-
  </table>
 
-[% L.hidden_tag('action', 'PriceRule/dispatch') %]
 [% L.hidden_tag('sort_by', FORM.sort_by) %]
 [% L.hidden_tag('sort_dir', FORM.sort_dir) %]
 [% L.hidden_tag('page', FORM.page) %]
-[% L.input_tag('action_list', LxERP.t8('Continue'), type = 'submit', class='submit') %]
-
-<a class='interact cursor-pointer' onClick='javascript:$("#filter_table input").val("");$("#filter_table input[type=checkbox]").prop("checked", 0);$("#filter_table select").val("")'>[% 'Reset' | $T8 %]</a>
 
+ [% L.button_tag("\$('#search_form').resetForm()", LxERP.t8("Reset")) %]
 </div>
 
 </form>
index 51f6066..70d8cf6 100644 (file)
@@ -6,7 +6,7 @@
 
 [%- INCLUDE 'common/flash.html' %]
 
- <form method="post" action="controller.pl">
+ <form method="post" action="controller.pl" id="form">
   [% L.hidden_tag("price_rule.id",   SELF.price_rule.id) %]
   [% L.hidden_tag("price_rule.type", SELF.price_rule.type) %]
 
 <h3>[% 'Then' | $T8 %]:</h3>
 <div>[% 'Set (set to)' | $T8 %] [% L.select_tag('price_rule.price_type', SELF.all_price_types, default=SELF.price_rule.price_type) %] [% 'to (set to)' | $T8 %] [% L.input_tag('price_rule.price_or_discount_as_number', SELF.price_rule.price_or_discount_as_number) %] <a id='price_rule_price_type_help' class='interact cursor-help' title='[% 'Price type explanation' | $T8 %]'>[?]</a>
 </div>
-
-  <p>
-   [% L.hidden_tag("action", "PriceRule/dispatch") %]
-   [% L.hidden_tag("callback", FORM.callback) %]
-   [% L.submit_tag("action_" _  (SELF.price_rule.id ? "update" : "create"), LxERP.t8('Save')) %]
-   [%- IF SELF.price_rule.id %]
-    [% L.submit_tag("action_create", LxERP.t8('Save as new')) %]
-    [% L.submit_tag("action_destroy", LxERP.t8('Delete'), confirm=LxERP.t8('Do you really want to delete this object?')) IF !SELF.price_rule.in_use %]
-   [%- END %]
-   <a href="[% SELF.url_for(action='list', 'filter.type'=SELF.price_rule.type) %]">[%- LxERP.t8('Abort') %]</a>
-  </p>
  </form>
index 04a7546..2916507 100644 (file)
@@ -1,4 +1,5 @@
 [%- USE L %]
+[%- USE P %]
 [%- USE HTML %]
 [%- USE T8 %]
 [%- USE LxERP %]
 [% L.hidden_tag('price_rule.items[].type', item.type) %]
 [%- SWITCH item.type %]
   [% CASE 'part' %]
-    [% 'Part' | $T8 %] [% 'is' | $T8 %] [% L.part_picker('price_rule.items[].value_int', item.part) %]
+    [% 'Part' | $T8 %] [% 'is' | $T8 %] [% P.part.picker('price_rule.items[].value_int', item.part) %]
   [% CASE 'customer' %]
-    [% 'Customer' | $T8 %] [% 'is' | $T8 %] [% L.customer_vendor_picker('price_rule.items[].value_int', item.customer, type='customer') %]
+    [% 'Customer' | $T8 %] [% 'is' | $T8 %] [% P.customer_vendor.picker('price_rule.items[].value_int', item.customer, type='customer') %]
   [% CASE 'vendor' %]
-    [% 'Vendor' | $T8 %] [% 'is' | $T8 %] [% L.customer_vendor_picker('price_rule.items[].value_int', item.vendor, type='vendor') %]
+    [% 'Vendor' | $T8 %] [% 'is' | $T8 %] [% P.customer_vendor.picker('price_rule.items[].value_int', item.vendor, type='vendor') %]
   [% CASE 'business' %]
     [% 'Type of Business' | $T8 %] [% 'is' | $T8 %] [% L.select_tag('price_rule.items[].value_int', SELF.businesses, title_key='description', default=item.value_int) %]
   [% CASE 'partsgroup' %]
-    [% 'Group' | $T8 %] [% 'is' | $T8 %] [% L.select_tag('price_rule.items[].value_int', SELF.partsgroups, title_key='partsgroup', default=item.value_int) %]
+    [% 'Partsgroup' | $T8 %] [% 'is' | $T8 %] [% L.select_tag('price_rule.items[].value_int', SELF.partsgroups, title_key='partsgroup', default=item.value_int) %]
   [% CASE 'qty' %]
     [% 'Quantity' | $T8 %] [% L.select_tag('price_rule.items[].op', num_compare_ops, default=item.op) %] [% L.input_tag('price_rule.items[].value_num_as_number', item.value_num_as_number) %]
   [% CASE 'reqdate' %]
     [% 'Reqdate' | $T8 %] [% L.select_tag('price_rule.items[].op', date_compare_ops, default=item.op) %] [% L.date_tag('price_rule.items[].value_date', item.value_date) %]
   [% CASE 'transdate' %]
-    [% 'Transdate' | $T8 %] [% L.select_tag('price_rule.items[].op', date_compare_ops, default=item.op) %] [% L.date_tag('price_rule.items[].value_date', item.value_date) %]
+    [% 'Transdate Record' | $T8 %] [% L.select_tag('price_rule.items[].op', date_compare_ops, default=item.op) %] [% L.date_tag('price_rule.items[].value_date', item.value_date) %]
   [% CASE 'pricegroup' %]
     [% 'Pricegroup' | $T8 %] [% 'is' | $T8 %] [% L.select_tag('price_rule.items[].value_int', SELF.pricegroups, title_key='pricegroup', default=item.value_int) %]
   [% CASE %]
index f471ab7..e618212 100644 (file)
@@ -2,8 +2,3 @@
 [% USE T8 %]
 [% USE HTML %]
 [%- L.paginate_controls(models=SELF.models) %]
-
-[%- UNLESS FORM.inline %]
-<a href="[% SELF.url_for(action='new', 'price_rule.type'='customer', callback=SELF.models.get_callback) | html %]">[% 'New Sales Price Rule' | $T8 %]</a>
-<a href="[% SELF.url_for(action='new', 'price_rule.type'='vendor', callback=SELF.models.get_callback) | html %]">[% 'New Purchase Price Rule' | $T8 %]</a>
-[%- END %]
index a534a87..1b5dad5 100644 (file)
@@ -1,4 +1,3 @@
 [%- USE L %]
-[%- PROCESS 'common/flash.html' %]
 [%- PROCESS 'price_rule/_filter.html' filter=SELF.models.filtered.laundered UNLESS FORM.inline %]
  <hr>
index 31bf79d..b2f2528 100644 (file)
@@ -28,7 +28,7 @@
 
  <tr>
   <th align="right">[% 'Customer' | $T8 %]</th>
-  <td>[% L.select_tag('project.customer_id', SELF.customers, default=SELF.project.customer_id, title_key='name', with_empty=1, style='width: 300px') %]</td>
+  <td>[% P.customer_vendor.picker('project.customer_id', SELF.project.customer_id, type='customer', fat_set_item=1, style='width: 300px', default=SELF.project.customer_id)%]</td>
  </tr>
 
  <tr>
index 3bd1980..924ca95 100644 (file)
@@ -3,7 +3,9 @@
 [%- USE L %]
 [%- USE LxERP %]
 
-<table>
+[% L.hidden_tag("_include_cvars_from_form", 1) %]
+
+<table id="filter_table">
  <tr>
   <th align="right">[% 'Number' | $T8 %]</th>
   <td>[% L.input_tag('filter.projectnumber:substr::ilike', filter.projectnumber_substr__ilike, size=60) %]</td>
   <td>[% L.select_tag('filter.project_status_id', SELF.project_statuses, default=filter.project_status_id, title_key='description', with_empty=1, style="width: 200px") %]</td>
  </tr>
 
- [% CUSTOM_VARIABLES_FILTER_CODE %]
+ [% FOREACH cvar_cfg = SELF.cvar_configs %]
+  [%- IF cvar_cfg.searchable %]
+   <tr>
+    <th align="right">[% HTML.escape(cvar_cfg.description) %]</th>
+    <td>[% INCLUDE 'common/render_cvar_filter_input.html' cvar_cfg=cvar_cfg cvar_class="rs_input_field" %]</td>
+   </tr>
+  [% END %]
+ [% END %]
 
  <tr>
   <th>[% 'Include in Report' | $T8 %]</th>
      <td>[% L.select_tag('filter.status', [ [ 'all', LxERP.t8('All') ], [ 'orphaned', LxERP.t8('Orphaned') ] ], default=filter.status, style="width: 200px") %]</td>
     </tr>
 
-    [% CUSTOM_VARIABLES_INCLUSION_CODE %]
+    [% FOREACH cvar_cfg = SELF.includeable_cvar_configs %]
+     <tr>
+      <td>
+       [% name__ = cvar_cfg.name;
+          L.checkbox_tag("include_cvars_" _ name__, value="1", checked=(SELF.include_cvars.$name__ ? 1 : ''), label=cvar_cfg.description) %]
+      </td>
+     </tr>
+    [% END %]
 
    </table>
   </td>
  </tr>
 </table>
+
+[% L.button_tag('$("#search_form").resetForm()', LxERP.t8('Reset')) %]
diff --git a/templates/webpages/project/_invoice_permissions.html b/templates/webpages/project/_invoice_permissions.html
new file mode 100644 (file)
index 0000000..6419748
--- /dev/null
@@ -0,0 +1,4 @@
+[%- USE LxERP -%][%- USE L -%]<div class="clearfix">
+ [% L.select_tag("project.employee_invoice_permissions[]", SELF.employees, id="employee_invoice_permissions", title_key="safe_name", default=SELF.project.employee_invoice_permissions, default_value_key='id', multiple=1) %]
+ [% L.multiselect2side("employee_invoice_permissions", labelsx => LxERP.t8("All employees"), labeldx => LxERP.t8("Employees with read access to the project's invoices")) %]
+</div>
index e7057ac..5175c71 100644 (file)
@@ -1,2 +1,2 @@
 [%- USE P %]
- [% P.grouped_record_list(records) %]
+ [% P.record.grouped_list(records) %]
diff --git a/templates/webpages/project/_project_picker_result.html b/templates/webpages/project/_project_picker_result.html
new file mode 100644 (file)
index 0000000..ee52c5d
--- /dev/null
@@ -0,0 +1,20 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE L %]
+[%- USE LxERP %]
+
+[% FOREACH project = SELF.projects %]
+<div class='project_picker_project [% FORM.hide_project_details ? 'ppp_line' : 'ppp_block' %]'>
+  <input type='hidden' class='project_picker_id' value='[% project.id %]'>
+  <input type='hidden' class='project_picker_description' value='[% project.displayable_name %]'>
+  <span class='ppp_block_number'>[% project.projectnumber | html %]</span>
+  <span class='ppp_block_description'>[% project.description | html %]</span>
+</div>
+[%- END %]
+
+<div style='clear:both'></div>
+
+[% L.paginate_controls(target='#project_picker_result', selector='#project_picker_result', models=SELF.models) %]
+<script type='text/javascript'>
+  kivi.ProjectPicker($('#'+$('#project_picker_real_id').val())).init_results();
+</script>
index c93a768..ee132ef 100644 (file)
@@ -7,7 +7,7 @@
 
 [%- INCLUDE 'common/flash.html' %]
 
- <form method="post" action="controller.pl">
+ <form method="post" action="controller.pl" id="form">
   [% L.hidden_tag("callback", callback) %]
   [% L.hidden_tag("id", SELF.project.id) %]
 
     [%- IF CUSTOM_VARIABLES.size %]
     <li><a href="#custom_variables">[% 'Custom Variables' | $T8 %]</a></li>
     [%- END %]
+    [%- IF SELF.may_edit_invoice_permissions %]
+     <li><a href="#invoice_permissions">[% 'Permissions for invoices' | $T8 %]</a></li>
+    [%- END %]
     [%- IF SELF.project.id %]
+      [%- IF INSTANCE_CONF.get_doc_storage %]
+        <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=project&object_id=[% SELF.project.id %]">[% 'Attachments' | $T8 %]</a></li>
+      [%- END %]
+    [%- END %]
+    [%- IF SELF.project.id and AUTH.assert('record_links', 1) %]
     <li><a href="#linked_records">[% 'Linked Records' | $T8 %]</a></li>
     [%- END %]
    </ul>
    </div>
    [%- END %]
 
-   [%- IF SELF.project.id %]
+   [%- IF SELF.may_edit_invoice_permissions %]
+    <div id="invoice_permissions">
+     [%- PROCESS 'project/_invoice_permissions.html' %]
+    </div>
+   [%- END %]
+
+   [%- IF SELF.project.id and AUTH.assert('record_links', 1) %]
    <div id="linked_records">
    [%- PROCESS 'project/_linked_records.html' records=SELF.linked_records %]
    </div>
    [%- END %]
 
   </div>
-
-  <p>
-   [% L.hidden_tag("action", "Project/dispatch") %]
-   [% L.submit_tag("action_" _  (SELF.project.id ? "update" : "create"), LxERP.t8('Save')) %]
-   [%- IF SELF.project.id %]
-    [% L.submit_tag("action_create", LxERP.t8('Save as new')) %]
-    [% L.submit_tag("action_destroy", LxERP.t8('Delete'), confirm=LxERP.t8('Do you really want to delete this object?')) IF !SELF.project.is_used %]
-   [%- END %]
-   <a href="[% IF callback %][% callback %][% ELSE %][% SELF.url_for(action => 'search') %][% END %]">[%- LxERP.t8('Abort') %]</a>
-  </p>
  </form>
diff --git a/templates/webpages/project/project_picker_search.html b/templates/webpages/project/project_picker_search.html
new file mode 100644 (file)
index 0000000..18b0632
--- /dev/null
@@ -0,0 +1,16 @@
+[%- USE HTML %]
+[%- USE L %]
+[%- USE P %]
+[%- USE LxERP %]
+[%- USE T8 %]
+
+[% L.hidden_tag('project_picker_real_id', FORM.real_id) %]
+[% LxERP.t8("Filter") %]: [% L.input_tag('project_picker_filter', SELF.models.filtered.laundered.all_substr_multi__ilike, class='project_picker_filter') %]
+[% P.link_tag("javascript:void(0);", "x", id='project_picker_clear_filter') %]
+<div class='float-right'>
+  [% L.checkbox_tag('no_paginate', checked=FORM.no_paginate, id='no_paginate', for_submit=1, label=LxERP.t8('All as list')) %]
+</div>
+
+<div id='project_picker_result'>
+  [% PROCESS 'project/_project_picker_result.html' %]
+</div>
index 87c7efe..ae3509a 100644 (file)
@@ -2,7 +2,7 @@
 [%- USE T8 %]
 [%- USE LxERP %]
 [%- USE HTML %]
-<form action='controller.pl' method='post'>
+<form action='controller.pl' method='post' id='search_form'>
 <div class='filter_toggle'>
 <a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Show Filter' | $T8 %]</a>
   [% SELF.filter_summary | html %]
 [% L.hidden_tag('sort_by', FORM.sort_by) %]
 [% L.hidden_tag('sort_dir', FORM.sort_dir) %]
 [% L.hidden_tag('page', FORM.page) %]
-[% L.input_tag('action_list', LxERP.t8('Continue'), type = 'submit', class='submit')%]
-
-
-<a href='#' onClick='javascript:$("#filter_table input").val("");$("#filter_table input[type=checkbox]").prop("checked", 0);'>[% 'Reset' | $T8 %]</a>
-
 </div>
 
 </form>
diff --git a/templates/webpages/project/search.html b/templates/webpages/project/search.html
deleted file mode 100644 (file)
index 7e6cdee..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-[%- USE L %]
-[%- USE LxERP %]
-<h1>[% 'Search projects' | $T8 %]</h1>
-
-[%- INCLUDE 'common/flash.html' %]
-
- <form method="post" action="controller.pl">
-
-  <p>
-[%- INCLUDE 'project/_filter.html' %]
-  </p>
-
-  <hr size="3" noshade>
-
-  [% L.hidden_tag('action', 'Project/list') %]
-
-  <p>[% L.submit_tag('dummy', LxERP.t8('Continue')) %]</p>
- </form>
index e4b8d92..2580482 100644 (file)
@@ -1,17 +1,79 @@
 [% USE L %]
+[% USE P %]
 
 <h1>Projekt-Picker-Testpage</h1>
 
 <br>
-[% L.project_picker('project_id', '', style='width: 600px') %] text<br>
+[% P.project.picker('project_id', '', style='width: 600px') %] text<br>
+
+<br>
+[% P.project.picker('project3_id', '', customer_id='1323', style='width: 300px') %] Filter one customer id
+<br>
+
+<br>
+[% P.project.picker('project4_id', '', customer_id='1323,1325', style='width: 300px') %] Filter two customer ids
+<br>
+
+<br>
+[% P.project.picker('project5_id', '', style='width: 300px') %] active/valid (default)
+<br>
+
+<br>
+[% P.project.picker('project6_id', '', active='active', style='width: 300px') %] active (explicit)
+<br>
+
+<br>
+[% P.project.picker('project7_id', '', active='inactive', style='width: 300px') %] inactive
+<br>
+
+<br>
+[% P.project.picker('project8_id', '', active='both', style='width: 300px') %] active and inactive
+<br>
+
+<br>
+[% P.project.picker('project9_id', '', valid='valid', style='width: 300px') %] valid (explicit)
+<br>
+
+<br>
+[% P.project.picker('project10_id', '', valid='invalid', style='width: 300px') %] invalid
+<br>
+
+<br>
+[% P.project.picker('project11_id', '', valid='both', style='width: 300px') %] valid and invalid
+<br>
+
+<br>
+[% P.project.picker('project12_id', '', active='both', valid='both',style='width: 300px') %] all (active, inactive, valid, invalid)
+<br>
+
+<br>
+[% P.project.picker('project13_id', '', style='width: 300px') %] description style full (default)
+<br>
+
+<br>
+[% P.project.picker('project14_id', '', description_style='full', style='width: 300px') %] description style full (explicit)
+<br>
+
+<br>
+[% P.project.picker('project15_id', '', description_style='both', style='width: 300px') %] description style both
+<br>
+
+<br>
+[% P.project.picker('project16_id', '', description_style='number', style='width: 300px') %] description style number
+<br>
+
+<br>
+[% P.project.picker('project17_id', '', description_style='description', style='width: 300px') %] description style description
+<br>
+
+<br>
 
 Runtime test:<br>
 <div id='runtime_picker'>'
 
 <script>
 $(function() {
-  $('#runtime_picker').html('[% L.project_picker("project2_id") %]');
+  $('#runtime_picker').html('[% P.project.picker("project2_id") %]');
   kivi.reinit_widgets();
 })
 </script>
-
diff --git a/templates/webpages/project_status/form.html b/templates/webpages/project_status/form.html
deleted file mode 100644 (file)
index 2173516..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
- <form method="post" action="controller.pl">
-
-[%- INCLUDE 'common/flash.html' %]
-
-  <table>
-   <tr>
-    <td>[% LxERP.t8('Name') %]</td>
-    <td>[% L.input_tag("project_status.name" SELF.project_status.name) %]</td>
-   </tr>
-   <tr>
-    <td>[% LxERP.t8('Description') %]</td>
-    <td>[% L.input_tag("project_status.description" SELF.project_status.description) %]</td>
-   </tr>
-  </table>
-
-  <p>
-   [% L.hidden_tag("id", SELF.project_status.id) %]
-   [% L.hidden_tag("action", "ProjectStatus/dispatch") %]
-   [% L.submit_tag("action_" _ (SELF.project_status.id ? 'update' : 'create'), LxERP.t8('Save')) %]
-   [%- IF SELF.project_status.id %]
-    [% L.submit_tag("action_destroy", LxERP.t8('Delete'), confirm=LxERP.t8('Do you really want to delete this object?')) %]
-   [%- END %]
-   <a href="[% SELF.url_for(action => 'list') %]">[% LxERP.t8('Abort') %]</a>
-  </p>
-
- </form>
diff --git a/templates/webpages/project_status/list.html b/templates/webpages/project_status/list.html
deleted file mode 100644 (file)
index f510b6d..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-[% USE HTML %][% USE T8 %][% USE L %][% USE LxERP %]
- <h1>[% FORM.title %]</h1>
-
-[%- INCLUDE 'common/flash.html' %]
-
- <form method="post" action="controller.pl">
-  [% IF !PROJECT_STATUS.size %]
-   <p>
-    [%- 'No project status has been created yet.' | $T8 %]
-   </p>
-
-  [%- ELSE %]
-   <table id="project_status_list">
-    <thead>
-    <tr class="listheading">
-     <th align="center"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></th>
-     <th>[%- 'Name' | $T8 %]</th>
-     <th>[%- 'Description' | $T8 %]</th>
-    </tr>
-    </thead>
-
-    <tbody>
-    [%- FOREACH project_status = PROJECT_STATUS %]
-    <tr class="listrow[% loop.count % 2 %]" id="project_status_id_[% project_status.id %]">
-     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></td>
-     <td><a href="[% SELF.url_for(action => 'edit', id => project_status.id) %]">[% project_status.name | html %]</a></td>
-     <td><a href="[% SELF.url_for(action => 'edit', id => project_status.id) %]">[% project_status.description | html %]</a></td>
-    </tr>
-    [%- END %]
-    </tbody>
-   </table>
-  [%- END %]
-
-  <p>
-   <a href="[% SELF.url_for(action => 'new') %]">[%- 'Create a new project status' | $T8 %]</a>
-  </p>
- </form>
-
- [% L.sortable_element('#project_status_list tbody', url => 'controller.pl?action=ProjectStatus/reorder', with => 'project_status_id') %]
diff --git a/templates/webpages/project_type/form.html b/templates/webpages/project_type/form.html
deleted file mode 100755 (executable)
index 79bb121..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
- <form method="post" action="controller.pl">
-
-[%- INCLUDE 'common/flash.html' %]
-
-  <table>
-   <tr>
-    <td>[% LxERP.t8('Description') %]</td>
-    <td>[% L.input_tag("project_type.description" SELF.project_type.description) %]</td>
-   </tr>
-  </table>
-
-  <p>
-   [% L.hidden_tag("id", SELF.project_type.id) %]
-   [% L.hidden_tag("action", "ProjectType/dispatch") %]
-   [% L.submit_tag("action_" _ (SELF.project_type.id ? 'update' : 'create'), LxERP.t8('Save')) %]
-   [%- IF SELF.project_type.id %]
-    [% L.submit_tag("action_destroy", LxERP.t8('Delete'), confirm=LxERP.t8('Do you really want to delete this object?')) %]
-   [%- END %]
-   <a href="[% SELF.url_for(action => 'list') %]">[% LxERP.t8('Abort') %]</a>
-  </p>
-
- </form>
diff --git a/templates/webpages/project_type/list.html b/templates/webpages/project_type/list.html
deleted file mode 100644 (file)
index d3d9181..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-[% USE HTML %][% USE T8 %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
-[%- INCLUDE 'common/flash.html' %]
-
- <form method="post" action="controller.pl">
-  [% IF !PROJECT_TYPES.size %]
-   <p>
-    [%- 'No project type has been created yet.' | $T8 %]
-   </p>
-
-  [%- ELSE %]
-   <table id="project_type_list">
-    <thead>
-    <tr class="listheading">
-     <th align="center"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></th>
-     <th>[%- 'Description' | $T8 %]</th>
-    </tr>
-    </thead>
-
-    <tbody>
-    [%- FOREACH project_type = PROJECT_TYPES %]
-    <tr class="listrow[% loop.count % 2 %]" id="project_type_id_[% project_type.id %]">
-     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></td>
-     <td>
-      <a href="[% SELF.url_for(action => 'edit', id => project_type.id) %]">
-       [%- HTML.escape(project_type.description) %]
-      </a>
-     </td>
-    </tr>
-    [%- END %]
-    </tbody>
-   </table>
-  [%- END %]
-
-  <p>
-   <a href="[% SELF.url_for(action => 'new') %]">[%- 'Create new project type' | $T8 %]</a>
-  </p>
- </form>
-
- [% L.sortable_element('#project_type_list tbody', url => 'controller.pl?action=ProjectType/reorder', with => 'project_type_id') %]
index 940b306..7debc66 100644 (file)
@@ -4,7 +4,7 @@
 [%- USE LxERP %]
 <h1>[% 'Reconciliation' | $T8 %]</h1>
 
-<form method=post action="[% script %]">
+<form method="post" action="rc.pl" id="form">
 
 <table>
   <tr>
     <td>[% L.date_tag('todate') %]</td>
   </tr>
 </table>
-
-<hr size=3 noshade>
-<br>
-
-[% L.hidden_tag('nextsub', 'get_payments') %]
-[% L.submit_tag('action', LxERP.t8('Continue')) %]
-
 </form>
-
index 0270d5b..369ccc7 100644 (file)
@@ -7,7 +7,7 @@
 
 <p>[% FOREACH row IN option %][% row %][% ', ' UNLESS loop.last %][% END %]</p>
 
-<form method=post action="[% script %]">
+<form method="post" action="rc.pl" id="form">
 
 <table width=100%>
   <tr class=listheading>
@@ -98,9 +98,6 @@
         </tr>
       </table>
 
-<hr size=3 noshade>
-<br>
-
 <input type=hidden name=rowcount value="[% rowcount %]">
 <input type=hidden name=accno value="[% accno %]">
 <input type=hidden name=account value="[% account %]">
 <input type=hidden name=fromdate value="[% fromdate %]">
 <input type=hidden name=todate value="[% todate %]">
 
-<br>
-<input type=submit class=submit name=action value="[% 'Update' | $T8 %]">
-<input type=submit class=submit name=action value="[% 'Done' | $T8 %]">
-
 </form>
index 0da95a2..ec8b5ed 100644 (file)
     </tr>
   </tbody>
 </table>
-  [% IF show_button %][% L.button_tag("submit_with_action('reconcile')", LxERP.t8("Reconcile")) %][% END %]
+  [% UNLESS errors %]
+   [% L.button_tag("submit_with_action('reconcile')", LxERP.t8("Reconcile")) %]
+  [% ELSE %]
+    [% FOREACH error IN errors %]
+      [% error %]
+    [% END %]
+  [% END %]
 [% END %]
index d8c0844..926ff79 100644 (file)
@@ -24,10 +24,9 @@ html, body {
 </style>
 
 <div class="listtop">[% title %]</div>
-
 [%- INCLUDE 'common/flash.html' %]
 
-<form id="reconciliation_form" method="post" action="controller.pl" style="height:100%">
+<form id="reconciliation_form" method="post" action="controller.pl" style="height:100%" id="filter_form">
   <table>
     <tr>
      <th align="right">[% 'Bank account' | $T8 %]</th>
@@ -78,16 +77,20 @@ html, body {
     </tr>
   </table>
 
-  [% L.submit_tag('submit_filter', LxERP.t8("Filter"), onclick='filter_table();return false;', style='display: none') %]
-
-  <div class="tabwidget" style="height:100%">
+  <div id="reconc_tabs" class="tabwidget" style="height:100%">
     <ul>
       <li><a href="#overview" onclick="load_overview();">[% 'Overview' | $T8 %]</a></li>
       <li><a href="#automatic" onclick="load_proposals();">[% 'Proposals' | $T8 %]</a></li>
     </ul>
 
-    <div id="overview" style="height:calc(100% - 60px);overflow: auto;">[% PROCESS "reconciliation/tabs/overview.html" %]</div>
-    <div id="automatic" style="height:calc(100% - 60px);overflow: auto;"></div>
+    <div id="overview" style="height:calc(100% - 60px);overflow: auto;">
+    [%- IF ui_tab == 0 %]
+    [% PROCESS "reconciliation/tabs/overview.html" %]
+    [%- END %]</div>
+    <div id="automatic" style="height:calc(100% - 60px);overflow: auto;">
+    [%- IF ui_tab == 1 %]
+    [% PROCESS "reconciliation/tabs/automatic.html" %]
+    [%- END %]    </div>
   </div>
 
 </form>
@@ -121,6 +124,7 @@ function load_overview () {
   });
 }
 
+$.cookie('jquery_ui_tab_reconc_tabs', [% ui_tab %] );
+
 //-->
 </script>
-
index 3474b28..7ed1986 100644 (file)
@@ -3,7 +3,7 @@
 [%- USE L %]
 [%- USE LxERP %]
 
-<form method="post" action="controller.pl">
+<form method="post" action="controller.pl" id="search_form">
 
 <div class="listtop">[% 'Choose bank account for reconciliation' | $T8 %]</div>
 
   </tr>
  </table>
 </p>
-
-<hr size="3" noshade>
-
-[% L.hidden_tag('action', FORM.next_sub) %]
-
-<p>[% L.submit_tag('dummy', LxERP.t8('Continue')) %]</p>
 </form>
index a455840..d9b0209 100644 (file)
@@ -3,7 +3,7 @@
 <h1>[%- LxERP.t8("Add link: select records to link with") %]</h1>
 
 
-<form method="post" action="controller.pl">
+<form method="post" action="controller.pl" id="record_links_add_filter_form">
  [% L.hidden_tag('object_model',   SELF.object_model) %]
  [% L.hidden_tag('object_id',      SELF.object_id) %]
 
                        style=style) %]</td>
   </tr>
 
+  <tr>
+   <td>[%- LxERP.t8("Record number") %]:</td>
+   <td>[% L.input_tag('number', '', style=style) %]</td>
+  </tr>
+
   <tr>
    <td>[%- LxERP.t8("Customer/Vendor Number") %]:</td>
    <td>[% L.input_tag('vc_number', is_sales ? SELF.object.customer.customernumber : SELF.object.vendor.vendornumber, style=style) %]</td>
@@ -31,7 +36,7 @@
    <td>[% L.input_tag('vc_name', is_sales ? SELF.object.customer.name : SELF.object.vendor.name, style=style) %]</td>
   </tr>
 
-  <tr>
+  <tr id="record_links_add_filter_project_row">
    <td>[%- LxERP.t8("Project") %]:</td>
    <td>[% L.select_tag('globalproject_id', PROJECTS, default=SELF.object.globalproject_id, with_empty=1, style=style) %]</td>
   </tr>
@@ -45,7 +50,7 @@
  <p>
   [% L.button_tag('filter_record_links()', LxERP.t8("Search")) %]
   [% L.button_tag('add_selected_record_links()', LxERP.t8("Add links"), id='add_selected_record_links_button', disabled=1) %]
-  <a href="#" onclick="record_links_reset_form();">[%- LxERP.t8("Reset") %]</a>
+  [% L.button_tag('$("#record_links_add_filter_form").resetForm()', LxERP.t8('Reset')) %]
   <a href="#" onclick="$('#record_links_add').dialog('close');">[% LxERP.t8("Cancel") %]</a>
  </p>
 
 <!--
 $(function() {
   $('#record_links_add input[name=vc_name]').focus();
-  $('#record_links_add_filter_link_type').change(function() {
-    var title = $('#record_links_add_filter_link_type').val() == 'requirement_spec' ? kivi.t8('Title') : kivi.t8('Transaction description');
-    $('#record_links_add_filter_title').html(title);
-  });
+  $('#record_links_add_filter_link_type').change(record_links_change_form_to_match_type);
+  record_links_change_form_to_match_type();
 });
 
-function record_links_reset_form() {
-  $('#record_links_add form input[type=text]').val('');
-  $('#record_links_add form select').prop('selectedIndex', 0);
-}
-
 function filter_record_links() {
   var url="controller.pl?action=RecordLinks/ajax_add_list&" + $("#record_links_add form").serialize();
   $.ajax({
@@ -91,5 +89,21 @@ function add_selected_record_links() {
     }
   });
 }
+
+function record_links_change_form_to_match_type() {
+  var type  = $('#record_links_add_filter_link_type').val();
+  var title = type == 'requirement_spec' ? kivi.t8('Title')
+            : type == 'letter'           ? kivi.t8('Subject')
+            :                              kivi.t8('Transaction description');
+
+  if (type == 'letter') {
+    $('#record_links_add_filter_project_row').hide();
+
+  } else {
+    $('#record_links_add_filter_project_row').show();
+  }
+
+  $('#record_links_add_filter_title').html(title);
+}
 -->
 </script>
index edbadb2..0c97bd6 100644 (file)
   [% IF date_column %]
    <th>[%- LxERP.t8("Date") %]</th>
   [% END %]
-  <th>[% IF SELF.link_type == 'requirement_spec' %][%- LxERP.t8("Title") %][% ELSE %][%- LxERP.t8("Transaction description") %][% END %]</th>
-  <th>[%- LxERP.t8("Project") %]</th>
+  <th>[% HTML.escape(description_title) %]</th>
+  [% IF project_column %]
+   <th>[%- LxERP.t8("Project") %]</th>
+  [% END %]
  </tr>
 
  [%- FOREACH object = OBJECTS %]
@@ -23,7 +25,9 @@
    <td>[%- HTML.escape(object.$date_column.to_kivitendo) %]</td>
   [% END %]
   <td>[%- HTML.escape(object.$description_column) %]</td>
-  <td>[%- P.project(object.$project_column, no_link=1) %]</td>
+  [% IF project_column %]
+   <td>[%- object.$project_column.presenter.project(no_link=1) %]</td>
+  [% END %]
  </tr>
  [%- END %]
 </table>
diff --git a/templates/webpages/record_template/dialog.html b/templates/webpages/record_template/dialog.html
new file mode 100644 (file)
index 0000000..d51a72b
--- /dev/null
@@ -0,0 +1,66 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE LxERP %]
+[%- USE HTML %][%- USE JavaScript -%]
+
+[% L.hidden_tag("", SELF.template_type, id="record_template_dialog_template_type",
+                "data-controller"=SELF.data.controller,
+                "data-load_action"=SELF.data.load_action,
+                "data-save_action"=SELF.data.save_action,
+                "data-form_selector"=SELF.data.form_selector) %]
+
+<h2 class="listheading">[% LxERP.t8("Add new record template") %]</h2>
+
+<p>
+ [% LxERP.t8("Name") %]:
+ [% L.input_tag("", "", id="record_template_dialog_new_template_name") %]
+ [% L.button_tag("kivi.RecordTemplate.create()", LxERP.t8("Save")) %]
+</p>
+
+<h2 class="listheading">[% LxERP.t8("Filter record template") %]</h2>
+<p>
+<form method="post" action="javascript:kivi.RecordTemplate.filter_templates()">
+ [% LxERP.t8("Name") %]:
+ [% L.input_tag("template_filter", SELF.template_filter) %]
+ [% L.submit_tag('', LxERP.t8("Filter")) %]
+ [% L.button_tag('$("#template_filter").val("")', LxERP.t8('Reset')) %]
+</form>
+</p>
+
+[% SET templates = SELF.templates.as_list %]
+
+[% IF templates.size %]
+
+<h2 class="listheading">[% LxERP.t8("Existing templates") %]</h2>
+
+<table>
+ <thead>
+  <tr class="listheading">
+   <th>[% LxERP.t8("Action") %]</th>
+   <th>[% LxERP.t8("Name") %]</th>
+   <th>[% LxERP.t8("Modification date") %]</th>
+  </tr>
+ </thead>
+
+ <tbody>
+[% FOREACH template = templates %]
+  <tr class="listrow">
+   <td>
+    [% L.hidden_tag("", template.template_name, id="record_template_dialog_template_name_" _ template.id) %]
+    [% L.button_tag("kivi.RecordTemplate.load(" _ template.id _ ")", LxERP.t8("Load")) %]
+    [% L.button_tag("kivi.RecordTemplate.save(" _ template.id _ ")", LxERP.t8("Save")) %]
+    [% L.button_tag("kivi.RecordTemplate.rename(" _ template.id _ ")", LxERP.t8("Rename")) %]
+    [% L.button_tag("kivi.RecordTemplate.delete(" _ template.id _ ")", LxERP.t8("Delete")) %]
+   </td>
+   <td>[% HTML.escape(template.template_name) %]</td>
+   <td>[% HTML.escape(template.mtime.to_kivitendo) %] [% HTML.escape(template.mtime.to_kivitendo_time) %]</td>
+  </tr>
+[% END %]
+ </tbody>
+
+</table>
+[% ELSE %]
+
+<p>[% LxERP.t8("There are no record templates yet.") %]</p>
+
+[% END %]
index 45a1fce..0857151 100644 (file)
@@ -3,7 +3,7 @@
 
  <h1>[% HTML.escape(title) %]</h1>
 
- <form action="[% HTML.escape(script) %]" method="post" name="report_generator_form">
+ <form action="[% HTML.escape(script) %]" method="post" name="report_generator_form" id="report_generator_form">
 
   [%- FOREACH var = HIDDEN %]
   <input type="hidden" name="[% HTML.escape(var.key) %]" value="[% HTML.escape(var.value) %]">
   </table>
 
 [%- IF CONTROLLER_DISPATCH %]
-   <p>
-    <input type="hidden" name="action" value="[% CONTROLLER_DISPATCH | html %]/dispatch">
-    <input type="submit" name="action_report_generator_export_as_csv" value="[% 'Export as CSV' | $T8 %]">
-    <input type="submit" name="action_report_generator_back" value="[% 'Back' | $T8 %]">
     <input type="hidden" name="CONTROLLER_DISPATCH" value="[% CONTROLLER_DISPATCH | html %]">
-   </p>
 [%- ELSE %]
-  <p>
    <input type="hidden" name="action" value="report_generator_dispatcher">
-   <input type="submit" class="submit" onclick="submit_report_generator_form('report_generator_export_as_csv')" value="[% 'Export as CSV' | $T8 %]">
-   <input type="submit" class="submit" onclick="submit_report_generator_form('report_generator_back')" value="[% 'Back' | $T8 %]">
-  </p>
- <script type="text/javascript"><!--
-      function submit_report_generator_form(nextsub) {
-        document.report_generator_form.report_generator_dispatch_to.value = nextsub;
-        document.report_generator_form.submit();
-      } // -->
- </script>
 [%- END %]
 
 
index 1430969..4ed1ca8 100644 (file)
@@ -19,6 +19,8 @@
 
  <h1>[% TITLE %]</h1>
 
+ [%- INCLUDE 'common/flash.html' %]
+
  [% IF TOP_INFO_TEXT %]
   <p>[% TOP_INFO_TEXT %]</p>
  [% END %]
@@ -26,8 +28,8 @@
  [% RAW_TOP_INFO_TEXT %]
 
  [% IF DATA_PRESENT %]
<p>
-  <table [% IF TABLE_CLASS %]class="[% TABLE_CLASS %]"[% END %] width="100%">
 <table [% IF TABLE_CLASS %]class="[% TABLE_CLASS %]"[% END %] id="report_table_id" width="100%">
+   <thead>
    [%- FOREACH row = HEADER_ROWS %]
    <tr>
     [% FOREACH col = row %]
      [%- IF col.align %] align="[% HTML.escape(col.align) %]" style="text-align: [% HTML.escape(col.align) %]"[% END -%]
      [%- IF col.colspan && col.colspan > 1 %] colspan="[% HTML.escape(col.colspan) %]"[% END -%]
      >
-      [%- IF col.link -%]<a class="[% col.link_class ? col.link_class : 'report-generator-header-link' %]" href="[% HTML.escape(col.link) %]">[%- END -%]
-      [%- col.text -%]
-      [%- IF col.show_sort_indicator -%]<img border="0" src="image/[% IF col.sort_indicator_direction %]down[% ELSE %]up[% END %].png">[%- END -%]
-      [%- IF col.link -%]</a>[%- END -%]
+      [%- IF col.raw_header_data %]
+       [% col.raw_header_data %]
+      [% ELSE %]
+       [%- IF col.link -%]<a class="[% col.link_class ? col.link_class : 'report-generator-header-link' %]" href="[% HTML.escape(col.link) %]">[%- END -%]
+       [%- col.text -%]
+       [%- IF col.show_sort_indicator -%]<img border="0" src="image/[% IF col.sort_indicator_direction %]down[% ELSE %]up[% END %].png">[%- END -%]
+       [%- IF col.link -%]</a>[%- END -%]
+      [%- END %]
      </th>
     [% END %]
    </tr>
    [%- END %]
+   </thead>
 
+   <tbody>
    [% FOREACH row = ROWS %]
     [% IF row.IS_CONTROL %]
      [% IF row.IS_COLSPAN_DATA %]<tr><td colspan="[% row.NUM_COLUMNS %]">[% row.data %]</td></tr>[% END %]
@@ -76,9 +84,9 @@
     [% END %]
    [% END %]
 
+   </tbody>
   </table>
   <hr size="3" noshade>
- </p>
  [% ELSE %]
   <p class="message_hint">[% 'No data was found.' | $T8 %]</p>
  [% END %]
  [% END %]
 
  [% IF SHOW_EXPORT_BUTTONS %]
-  <form action="[% HTML.escape(script) %]" name="report_generator_form" method="post">
+  <form action="[% HTML.escape(script) %]" name="report_generator_form" id="report_generator_form" method="post">
    [% FOREACH var = EXPORT_VARIABLES %]<input type="hidden" name="report_generator_hidden_[% var.key %]" value="[% HTML.escape(var.value) %]">
    [% END %]
 
 [%- IF CONTROLLER_DISPATCH %]
+[% IF !SKIP_BUTTONS %]
    <input type="hidden" name="action" value="[% CONTROLLER_DISPATCH %]/dispatch">
+[%- END %][%# !SKIP_BUTTONS %]
    <input type="hidden" name="report_generator_nextsub" value="[% HTML.escape(EXPORT_NEXTSUB) %]">
    <input type="hidden" name="report_generator_variable_list" value="[% HTML.escape(EXPORT_VARIABLE_LIST) %]">
    <input type="hidden" name="CONTROLLER_DISPATCH" value="[% CONTROLLER_DISPATCH | html %]">
 
+[% IF !SKIP_BUTTONS %]
    <p>
     [% 'List export' | $T8 %]<br>
     [% IF ALLOW_PDF_EXPORT %]<input type="submit" name="action_report_generator_export_as_pdf" value="[% 'Export as PDF' | $T8 %]">[% END %]
     [% IF ALLOW_CSV_EXPORT %]<input type="submit" name="action_report_generator_export_as_csv" value="[% 'Export as CSV' | $T8 %]">[% END %]
    </p>
+[%- END %][%# !SKIP_BUTTONS %]
 [%- ELSE %]
    <input type="hidden" name="report_generator_nextsub" value="[% HTML.escape(EXPORT_NEXTSUB) %]">
    <input type="hidden" name="report_generator_variable_list" value="[% HTML.escape(EXPORT_VARIABLE_LIST) %]">
    <input type="hidden" name="report_generator_dispatch_to" value="">
    <input type="hidden" name="action" value="report_generator_dispatcher">
 
+[% IF !SKIP_BUTTONS %]
    <p>
     [% 'List export' | $T8 %]<br>
     [% IF ALLOW_PDF_EXPORT %]<input type="submit" class="submit" onclick="submit_report_generator_form('report_generator_export_as_pdf')" value="[% 'Export as PDF' | $T8 %]">[% END %]
         document.report_generator_form.submit();
       } // -->
  </script>
+[%- END %][%# !SKIP_BUTTONS %]
 [%- END %]
 
   </form>
index 59a98b7..86594b5 100644 (file)
@@ -1,11 +1,12 @@
 [%- USE T8 %]
 [%- USE HTML %][%- USE LxERP %]
 
- [%- SET default_margin = LxERP.format_amount(1.5) %]
+ [%- SET default_ymargin = LxERP.format_amount(1.5) %]
+ [%- SET default_xmargin = LxERP.format_amount(0.8) %]
 
  <h1>[% HTML.escape(title) %]</h1>
 
- <form action="[% HTML.escape(script) %]" method="post" name="report_generator_form">
+ <form action="[% HTML.escape(script) %]" method="post" name="report_generator_form" id="report_generator_form">
 
   [%- FOREACH var = HIDDEN %]
   <input type="hidden" name="[% HTML.escape(var.key) %]" value="[% HTML.escape(var.value) %]">
 
    <tr>
     <td align="right">[% 'Top' | $T8 %]</td>
-    <td><input name="report_generator_pdf_options_margin_top" size="4" value="[% HTML.escape(default_margin) %]"> cm</td>
+    <td><input name="report_generator_pdf_options_margin_top" size="4" value="[% HTML.escape(default_ymargin) %]"> cm</td>
    </tr>
 
    <tr>
     <td align="right">[% 'Left' | $T8 %]</td>
-    <td><input name="report_generator_pdf_options_margin_left" size="4" value="[% HTML.escape(default_margin) %]"> cm</td>
+    <td><input name="report_generator_pdf_options_margin_left" size="4" value="[% HTML.escape(default_xmargin) %]"> cm</td>
    </tr>
 
    <tr>
     <td align="right">[% 'Bottom' | $T8 %]</td>
-    <td><input name="report_generator_pdf_options_margin_bottom" size="4" value="[% HTML.escape(default_margin) %]"> cm</td>
+    <td><input name="report_generator_pdf_options_margin_bottom" size="4" value="[% HTML.escape(default_ymargin) %]"> cm</td>
    </tr>
 
    <tr>
     <td align="right">[% 'Right' | $T8 %]</td>
-    <td><input name="report_generator_pdf_options_margin_right" size="4" value="[% HTML.escape(default_margin) %]"> cm</td>
+    <td><input name="report_generator_pdf_options_margin_right" size="4" value="[% HTML.escape(default_xmargin) %]"> cm</td>
    </tr>
 
    <tr>
   </table>
 
 [%- IF CONTROLLER_DISPATCH %]
-   <p>
-    <input type="hidden" name="action" value="[% CONTROLLER_DISPATCH | html %]/dispatch">
-    <input type="submit" name="action_report_generator_export_as_pdf" value="[% 'Export as PDF' | $T8 %]">
-    <input type="submit" name="action_report_generator_back" value="[% 'Back' | $T8 %]">
     <input type="hidden" name="CONTROLLER_DISPATCH" value="[% CONTROLLER_DISPATCH | html %]">
-   </p>
 [%- ELSE %]
-  <p>
    <input type="hidden" name="action" value="report_generator_dispatcher">
-   <input type="submit" class="submit" onclick="submit_report_generator_form('report_generator_export_as_pdf')" value="[% 'Export as PDF' | $T8 %]">
-   <input type="submit" class="submit" onclick="submit_report_generator_form('report_generator_back')" value="[% 'Back' | $T8 %]">
-  </p>
- <script type="text/javascript"><!--
-      function submit_report_generator_form(nextsub) {
-        document.report_generator_form.report_generator_dispatch_to.value = nextsub;
-        document.report_generator_form.submit();
-      } // -->
- </script>
 [%- END %]
 
  </form>
index 201709f..2e4209a 100644 (file)
@@ -7,8 +7,9 @@
 <div class="filter_toggle" style="display:none">
  <a href="#" onClick="javascript:$('.filter_toggle').toggle()">[% LxERP.t8("Hide Filter") %]</a>
 
- <form method="post" action="controller.pl">
+ <form method="post" action="controller.pl" id="search_form">
   [%- L.hidden_tag("is_template", is_template) %]
+  [%- L.hidden_tag("_include_cvars_from_form", 1) %]
 
   <p>
    <table>
@@ -56,7 +57,6 @@
      [% END %]
     [% END %]
 
-    [% L.hidden_tag("include_cvars.dummy__", 1) %]
     [% IF SELF.includeable_cvar_configs.size %]
      <tr>
       <th align="right">[% LxERP.t8("Include in Report") %]</th>
@@ -66,7 +66,7 @@
          [% FOREACH cvar_cfg = SELF.includeable_cvar_configs %]
           <td>
            [% name__ = cvar_cfg.name;
-              L.checkbox_tag("include_cvars." _ name__, value="1", checked=(SELF.include_cvars.$name__ ? 1 : ''), label=cvar_cfg.description) %]
+              L.checkbox_tag("include_cvars_" _ name__, value="1", checked=(SELF.include_cvars.$name__ ? 1 : ''), label=cvar_cfg.description) %]
           </td>
           [%- IF !loop.last && ((loop.count % 3) == 0) %]
            </tr><tr>
@@ -80,9 +80,5 @@
 [%- END %]
    </table>
   </p>
-
-  [% L.hidden_tag("action", "RequirementSpec/list") %]
-
-  <p>[% L.submit_tag("dummy", LxERP.t8("Continue")) %]</p>
  </form>
 </div>
index a89a99b..ea73e4d 100644 (file)
 [%- END %]
 
  <p>
-[% IF submit_as == 'post' %]
-  [% L.hidden_tag("action", "RequirementSpec/dispatch", id=id_prefix _ '_action') %]
-  [% L.submit_tag("action_" _ (SELF.requirement_spec.id ? "update" : "create"), LxERP.t8('Save'), id=id_prefix _ '_action_update') %]
-  <a href="[% SELF.url_for(action="list", is_template=SELF.requirement_spec.is_template) %]">[% LxERP.t8('Abort') %]</a>
-[% ELSE %]
+[% IF submit_as != 'post' %]
   [% L.ajax_submit_tag("controller.pl?action=RequirementSpec/update",  "#" _ id_prefix, LxERP.t8("Save"), id=id_prefix _ '_submit') %]
   <script type="text/javascript"><!--
   $(function() {
index af75628..54166f1 100644 (file)
@@ -1,4 +1,4 @@
-[%- USE HTML -%][%- USE LxERP -%]
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%]
 <div id="basic_settings" class="basic-settings-context-menu">
  <h2>
   [% IF SELF.requirement_spec.is_template %]
   [% FOREACH var = cvars %]
    <tr class="listrow">
     <td>[% HTML.escape(var.config.description) %]</td>
-    <td>[% HTML.escape(var.value_as_text) %]</td>
+    <td>
+      [%- IF var.config.type == 'htmlfield' -%]
+        [%- L.restricted_html(var.value_as_text) -%]
+      [%- ELSE -%]
+        [%- HTML.escape(var.value_as_text) -%]
+      [%- END -%]
+    </td>
    </tr>
   [% END %]
 
index e8e4c3d..2cff5d9 100644 (file)
@@ -1,5 +1,6 @@
 [%- USE LxERP -%][%- USE L -%][%- USE HTML -%][%- USE P -%]
 [%- DEFAULT id_prefix = 'time_and_cost_estimate_form' %]
+[%- SET total_cost = 0 %]
 
 <div id="time_cost_estimate"[% IF initially_hidden %] style="display: none;"[% END %]>
  [%- IF !SELF.requirement_spec.sections.size %]
@@ -18,7 +19,7 @@
       <th>[%- LxERP.t8("Risk") %]</th>
       <th align="right">[%- LxERP.t8("Time estimate") %]</th>
       [%- UNLESS SELF.requirement_spec.is_template %]
-       <th align="right">[%- LxERP.t8("Cost") %]</th>
+       <th align="right">[%- LxERP.t8("Price") %]</th>
       [%- END %]
      </tr>
 
       [%- SET at_least_one_function_block = 1 %]
       [%- FOREACH child = section.children_sorted %]
        [%- INCLUDE 'requirement_spec/_show_time_and_cost_estimate_item.html'
-                   item  = child
-                   level = 1 %]
+                   section = section
+                   item    = child
+                   level   = 1 %]
       [%- END %]
 
       <tr class="listrow subtotal">
        <td style="padding-left: 50px" colspan="3" class="sum">[%- LxERP.t8("Sum for section") -%]:</td>
        <td align="right" nowrap>[%- P.format_man_days(section.time_estimation, 'skip_zero'=1) -%]</td>
        [%- UNLESS SELF.requirement_spec.is_template %]
-        <td align="right" nowrap>[%- LxERP.format_amount(section.time_estimation * SELF.requirement_spec.hourly_rate, 2) -%] EUR</td>
+        [%- SET section_cost = section.time_estimation * SELF.requirement_spec.hourly_rate * section.sellprice_factor;
+                total_cost   = total_cost + section_cost %]
+        <td align="right" nowrap>[%- LxERP.format_amount(section_cost, 2) -%] EUR</td>
        [%- END %]
       </tr>
      [%- END -%]
@@ -50,7 +54,7 @@
      <td colspan="3">[%- LxERP.t8("Sum for #1", SELF.requirement_spec.type.description) -%]:</td>
      <td align="right" nowrap>[%- P.format_man_days(SELF.requirement_spec.time_estimation) -%]</td>
      [%- UNLESS SELF.requirement_spec.is_template %]
-      <td align="right" nowrap>[%- LxERP.format_amount(SELF.requirement_spec.time_estimation * SELF.requirement_spec.hourly_rate, 2) -%] EUR</td>
+      <td align="right" nowrap>[%- LxERP.format_amount(total_cost, 2) -%] EUR</td>
      [%- END %]
     </tr>
    </tfoot>
index c3d21e3..d051972 100644 (file)
@@ -8,7 +8,7 @@
  [%- IF !item.children.size -%]
   <td align="right" nowrap>[%- P.format_man_days(item.time_estimation, skip_zero=1) -%]</td>
   [%- UNLESS SELF.requirement_spec.is_template %]
-   <td align="right" nowrap>[%- LxERP.format_amount(item.time_estimation * SELF.requirement_spec.hourly_rate, 2) -%] EUR</td>
+   <td align="right" nowrap>[%- LxERP.format_amount(item.time_estimation * SELF.requirement_spec.hourly_rate * section.sellprice_factor, 2) -%] EUR</td>
   [%- END %]
  [%- ELSE -%]
   <td>&nbsp;</td>
 [%- IF item.children.size -%]
  [%- FOREACH child = item.children_sorted -%]
   [%- INCLUDE 'requirement_spec/_show_time_and_cost_estimate_item.html'
-              item  = child
-              level = level + 1 -%]
+              section = section
+              item    = child
+              level   = level + 1 -%]
  [%- END -%]
 
  <tr class="listrow subtotal">
   <td style="padding-left: [%- (level + 1) * 50 -%]px" colspan="3">[%- LxERP.t8("Sum for #1", item.fb_number) -%]:</td>
   <td align="right" nowrap>[%- P.format_man_days(item.time_estimation, skip_zero=1) -%]</td>
   [%- UNLESS SELF.requirement_spec.is_template %]
-   <td align="right" nowrap>[%- LxERP.format_amount(item.time_estimation * SELF.requirement_spec.hourly_rate, 2) -%] EUR</td>
+   <td align="right" nowrap>[%- LxERP.format_amount(item.time_estimation * SELF.requirement_spec.hourly_rate * section.sellprice_factor, 2) -%] EUR</td>
   [%- END %]
  </tr>
 [%- END -%]
index 285dcee..3cd23ef 100644 (file)
@@ -11,7 +11,7 @@
  <ul>
   <li id="tab-header-function-block"><a href="#function-blocks-tab">[%- LxERP.t8("Content") %]</a></li>
   <li id="tab-header-basic-settings"><a href="controller.pl?action=RequirementSpec/ajax_show_basic_settings&id=[% HTML.url(SELF.requirement_spec.id) %]">[%- LxERP.t8("Basic settings") %]</a></li>
-  <li id="tab-header-time-cost-estimate"><a href="controller.pl?action=RequirementSpec/ajax_show_time_and_cost_estimate&id=[% HTML.url(SELF.requirement_spec.id) %]">[%- LxERP.t8("Time and cost estimate") %]</a></li>
+  <li id="tab-header-time-cost-estimate"><a href="controller.pl?action=RequirementSpec/ajax_show_time_and_cost_estimate&id=[% HTML.url(SELF.requirement_spec.id) %]">[%- LxERP.t8("Time and price estimate") %]</a></li>
   <li id="tab-header-additional-parts"><a href="controller.pl?action=RequirementSpecPart/show&requirement_spec_id=[% HTML.url(SELF.requirement_spec.id) %]">[%- LxERP.t8("Additional articles") %]</a></li>
   [%- UNLESS SELF.requirement_spec.is_template %]
    <li id="tab-header-versions"><a href="controller.pl?action=RequirementSpecVersion/list&requirement_spec_id=[% HTML.url(SELF.requirement_spec.id) %]">[%- LxERP.t8("Versions") %]</a></li>
@@ -56,7 +56,7 @@ $(function() {
       attr:     { id: "tb-front", class: "text-block-context-menu" },
       children: [
 [% FOREACH tb = SELF.requirement_spec.text_blocks_sorted(output_position=0) %]
- [% P.requirement_spec_text_block_jstree_data(tb).json %][% IF !loop.last %],[% END %]
+ [% tb.presenter.jstree_data.json %][% IF !loop.last %],[% END %]
 [% END %]
       ]},
 
@@ -65,7 +65,7 @@ $(function() {
       attr:     { id: "sections", class: "section-context-menu" },
       children: [
 [% FOREACH section = sections %]
- [% P.requirement_spec_item_jstree_data(section).json %][% IF !loop.last %],[% END %]
+ [% section.presenter.jstree_data.json %][% IF !loop.last %],[% END %]
 [% END %]
       ]},
 
@@ -74,7 +74,7 @@ $(function() {
       attr:     { id: "tb-back", class: "text-block-context-menu" },
       children: [
 [% FOREACH tb = SELF.requirement_spec.text_blocks_sorted(output_position=1) %]
- [% P.requirement_spec_text_block_jstree_data(tb).json %][% IF !loop.last %],[% END %]
+ [% tb.presenter.jstree_data.json %][% IF !loop.last %],[% END %]
 [% END %]
       ]}
   ];
diff --git a/templates/webpages/requirement_spec_acceptance_status/form.html b/templates/webpages/requirement_spec_acceptance_status/form.html
deleted file mode 100755 (executable)
index 299326c..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
- <form method="post" action="controller.pl">
-
-[%- INCLUDE 'common/flash.html' %]
-
-  <table>
-   <tr>
-    <td>[% LxERP.t8('Name') %]</td>
-    <td>[% L.select_tag("requirement_spec_acceptance_status.name",  SELF.valid_names, default = SELF.requirement_spec_acceptance_status.name) %]</td>
-   </tr>
-
-   <tr>
-    <td>[% LxERP.t8('Description') %]</td>
-    <td>[% L.input_tag("requirement_spec_acceptance_status.description", SELF.requirement_spec_acceptance_status.description) %]</td>
-   </tr>
-  </table>
-
-  <p>
-   [% L.hidden_tag("id", SELF.requirement_spec_acceptance_status.id) %]
-   [% L.hidden_tag("action", "RequirementSpecAcceptanceStatus/dispatch") %]
-   [% L.submit_tag("action_" _ (SELF.requirement_spec_acceptance_status.id ? "update" : "create"), LxERP.t8('Save')) %]
-   [%- IF SELF.requirement_spec_acceptance_status.id %]
-    [% L.submit_tag("action_destroy", LxERP.t8('Delete'), confirm=LxERP.t8('Do you really want to delete this object?')) %]
-   [%- END %]
-   <a href="[% SELF.url_for(action => 'list') %]">[%- LxERP.t8('Abort') %]</a>
-  </p>
- </form>
diff --git a/templates/webpages/requirement_spec_acceptance_status/list.html b/templates/webpages/requirement_spec_acceptance_status/list.html
deleted file mode 100644 (file)
index 93049e2..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
-[%- INCLUDE 'common/flash.html' %]
-
- <form method="post" action="controller.pl">
-  [% IF !REQUIREMENT_SPEC_ACCEPTANCE_STATUSES.size %]
-   <p>
-    [%- LxERP.t8('No acceptance statuses has been created yet.') %]
-   </p>
-
-  [%- ELSE %]
-   <table id="requirement_spec_acceptance_status_list">
-    <thead>
-    <tr class="listheading">
-     <th align="center"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></th>
-     <th>[%- LxERP.t8('Name') %]</th>
-     <th>[%- LxERP.t8('Description') %]</th>
-    </tr>
-    </thead>
-
-    <tbody>
-    [%- FOREACH requirement_spec_acceptance_status = REQUIREMENT_SPEC_ACCEPTANCE_STATUSES %]
-    <tr class="listrow[% loop.count % 2 %]" id="requirement_spec_acceptance_status_id_[% requirement_spec_acceptance_status.id %]">
-     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></td>
-     <td>
-      <a href="[% SELF.url_for(action => 'edit', id => requirement_spec_acceptance_status.id) %]">
-       [%- HTML.escape(requirement_spec_acceptance_status.name) %]
-      </a>
-     </td>
-
-     <td>[%- HTML.escape(requirement_spec_acceptance_status.description) %]</td>
-    </tr>
-    [%- END %]
-    </tbody>
-   </table>
-  [%- END %]
-
-  <p>
-   <a href="[% SELF.url_for(action => 'new') %]">[%- LxERP.t8('Create a new acceptance status') %]</a>
-  </p>
- </form>
-
- [% L.sortable_element('#requirement_spec_acceptance_status_list tbody', url => 'controller.pl?action=RequirementSpecAcceptanceStatus/reorder', with => 'requirement_spec_acceptance_status_id') %]
diff --git a/templates/webpages/requirement_spec_complexity/form.html b/templates/webpages/requirement_spec_complexity/form.html
deleted file mode 100755 (executable)
index 8088db5..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
- <form method="post" action="controller.pl">
-
-[%- INCLUDE 'common/flash.html' %]
-
-  <table>
-   <tr>
-    <td>[% LxERP.t8("Description") %]</td>
-    <td>[% L.input_tag("requirement_spec_complexity.description", SELF.requirement_spec_complexity.description) %]</td>
-   </tr>
-  </table>
-
-  <p>
-   [% L.hidden_tag("id", SELF.requirement_spec_complexity.id) %]
-   [% L.hidden_tag("action", "RequirementSpecComplexity/dispatch") %]
-   [% L.submit_tag("action_" _ (SELF.requirement_spec_complexity.id ? "update" : "create"), LxERP.t8('Save')) %]
-   [%- IF SELF.requirement_spec_complexity.id %]
-    [% L.submit_tag("action_destroy", LxERP.t8('Delete'), confirm=LxERP.t8('Do you really want to delete this object?')) %]
-   [%- END %]
-   <a href="[% SELF.url_for(action => 'list') %]">[%- LxERP.t8('Abort') %]</a>
-  </p>
- </form>
diff --git a/templates/webpages/requirement_spec_complexity/list.html b/templates/webpages/requirement_spec_complexity/list.html
deleted file mode 100644 (file)
index 3ce2d94..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
-[%- INCLUDE "common/flash.html" %]
-
- <form method="post" action="controller.pl">
-  [% IF !REQUIREMENT_SPEC_COMPLEXITIES.size %]
-   <p>
-    [%- LxERP.t8("No complexities has been created yet.") %]
-   </p>
-
-  [%- ELSE %]
-   <table id="requirement_spec_complexity_list">
-    <thead>
-    <tr class="listheading">
-     <th align="center"><img src="image/updown.png" alt="[ LxERP.t8("reorder item") %]"></th>
-     <th>[%- LxERP.t8("Description") %]</th>
-    </tr>
-    </thead>
-
-    <tbody>
-    [%- FOREACH requirement_spec_complexity = REQUIREMENT_SPEC_COMPLEXITIES %]
-    <tr class="listrow[% loop.count % 2 %]" id="requirement_spec_complexity_id_[% requirement_spec_complexity.id %]">
-     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[ LxERP.t8("reorder item") %]"></td>
-     <td>
-      <a href="[% SELF.url_for(action => "edit", id => requirement_spec_complexity.id) %]">
-       [%- HTML.escape(requirement_spec_complexity.description) %]
-      </a>
-     </td>
-    </tr>
-    [%- END %]
-    </tbody>
-   </table>
-  [%- END %]
-
-  <p>
-   <a href="[% SELF.url_for(action => "new") %]">[%- LxERP.t8("Create a new complexity") %]</a>
-  </p>
- </form>
-
- [% L.sortable_element("#requirement_spec_complexity_list tbody", url => "controller.pl?action=RequirementSpecComplexity/reorder", with => "requirement_spec_complexity_id") %]
index 55fbeef..0f8c01f 100644 (file)
@@ -1,7 +1,7 @@
 [%- USE LxERP -%][%- USE P -%]<div id="[% id_prefix %]function-block-content-bottom-[% requirement_spec_item.id %]" class="smaller" style="text-align:right">
  [%- IF requirement_spec_item.dependencies.size -%]
  <span class="gray">
-  [%- LxERP.t8("Dependencies") -%]: [%- P.requirement_spec_item_dependency_list(requirement_spec_item) -%]
+  [%- LxERP.t8("Dependencies") -%]: [%- requirement_spec_item.presenter.requirement_spec_item_dependency_list -%]
  </span><br>
  [%- END -%]
  <span class="gray">
index 93d75be..14c5af5 100644 (file)
   </p>
  [%- END %]
 
- <p>
-  [%- LxERP.t8("Description") %]:<br>
-  [% L.textarea_tag(id_base _ '.description_as_restricted_html', SELF.item.description_as_restricted_html, id=id_base _ '_description', rows=8, cols=80, style=style, class='texteditor') %]
- </p>
-
- <p>
-  [% L.ajax_submit_tag('controller.pl?action=RequirementSpecItem/ajax_' _ (SELF.item.id ? 'update' : 'create'), '#' _ id_base _ '_form', LxERP.t8('Save')) %]
-  <a href="#" onclick="kivi.requirement_spec.cancel_edit_item_form('[% id_base %]', { to_show: '[% hidden %]' })">[%- LxERP.t8("Cancel") %]</a>
- </p>
+ <table border="0">
+  <tr valign="top">
+   <td>
+    [%- LxERP.t8("Description") %]:<br>
+    [% L.textarea_tag(id_base _ '.description_as_restricted_html', SELF.item.description_as_restricted_html, id=id_base _ '_description', rows=8, cols=80, style=style, class='texteditor') %]<br>
+    [% L.ajax_submit_tag('controller.pl?action=RequirementSpecItem/ajax_' _ (SELF.item.id ? 'update' : 'create'), '#' _ id_base _ '_form', LxERP.t8('Save')) %]
+    <a href="#" onclick="kivi.requirement_spec.cancel_edit_item_form('[% id_base %]', { to_show: '[% hidden %]' })">[%- LxERP.t8("Cancel") %]</a>
+   </td>
+
+   <td>
+   [% LxERP.t8("Price Factor") %]:<br>
+   [% L.input_tag(id_base _ ".sellprice_factor_as_number", SELF.item.sellprice_factor_as_number, size="6") %]<br>
+   </td>
+  </tr>
+ </table>
 
 [%- IF SELF.predefined_texts.size %]
  <script type="text/javascript">
index 4df9892..4c4743f 100644 (file)
@@ -18,3 +18,7 @@
   <span class="dimmed-text">[%- LxERP.t8("No text has been entered yet.") %]</span>
  [%- END %]
 </div>
+
+<div class="smaller gray" style="text-align:right">
+ [%- LxERP.t8("Price Factor") -%]: [%- LxERP.format_amount(requirement_spec_item.sellprice_factor, -2) -%]
+</div>
index 634cb24..9ec6d13 100644 (file)
@@ -18,7 +18,7 @@
   <tr>
    <td>[% LxERP.t8("Assign the following article to all sections") %]:</td>
    <td data-unit="[% HTML.escape(SELF.section_order_part.unit) %]">
-    [% P.part_picker('quotations_and_orders_dummy', SELF.section_order_part.id, id='quotations_and_orders_order_id', style=style) %]
+    [% P.part.picker('quotations_and_orders_dummy', SELF.section_order_part.id, id='quotations_and_orders_order_id', style=style) %]
     [% L.button_tag('kivi.requirement_spec.assign_order_part_id_to_all()', LxERP.t8('Assign article')) %]
    </td>
   </tr>
@@ -43,7 +43,7 @@
     <td>[% HTML.escape(section.fb_number) %]</td>
     <td>[% HTML.escape(section.title) %]</td>
     <td>[% HTML.escape(P.truncate(section.description_as_stripped_html)) %]</td>
-    <td>[% P.part_picker('sections[].order_part_id', section.order_part_id, id='quotations_and_orders_sections_order_pard_id_' _ loop.count, style=style) %]</td>
+    <td>[% P.part.picker('sections[].order_part_id', section.order_part_id, id='quotations_and_orders_sections_order_pard_id_' _ loop.count, style=style) %]</td>
     <td data-unit-column=1>[% HTML.escape(section.order_part.unit) %]</td>
     <td data-position-type-column=1>
      [% IF section.order_part_id && section.order_part.unit_obj.is_time_based %]
index 4f8278d..d1c2278 100644 (file)
       [% END %]
      </td>
      <td>
-      <a href="oe.pl?action=edit&id=[% HTML.url(rs_order.order_id) %]&type=[% HTML.url(rs_order.order.type) %]">
+      [%- IF INSTANCE_CONF.get_feature_experimental_order -%]
+        <a href="controller.pl?action=Order/edit&id=[% HTML.url(rs_order.order_id) %]&type=[% HTML.url(rs_order.order.type) %]">
+      [%- ELSE -%]
+        <a href="oe.pl?action=edit&id=[% HTML.url(rs_order.order_id) %]&type=[% HTML.url(rs_order.order.type) %]">
+      [%- END -%]
        [% HTML.escape(rs_order.order.quotation ? rs_order.order.quonumber : rs_order.order.ordnumber) %]
       </a>
      </td>
index c1b2795..2c68e76 100644 (file)
@@ -7,7 +7,7 @@
 
  <div>
   [% LxERP.t8("Add part") %]:
-  [% P.part_picker('additional_parts_add_part_id', '', style="width: 300px") %]
+  [% P.part.picker('additional_parts_add_part_id', '', style="width: 300px") %]
   [% L.button_tag('kivi.requirement_spec.add_additional_part()', LxERP.t8('Add part')) %]
  </div>
 
diff --git a/templates/webpages/requirement_spec_predefined_text/form.html b/templates/webpages/requirement_spec_predefined_text/form.html
deleted file mode 100755 (executable)
index 2f03e35..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
- <form method="post" action="controller.pl">
-
-[%- INCLUDE 'common/flash.html' %]
-
-  <table>
-   <tr>
-    <td>[% LxERP.t8("Description") %]</td>
-    <td>[% L.input_tag("requirement_spec_predefined_text.description", SELF.requirement_spec_predefined_text.description, size=60, class='initial_focus') %]</td>
-   </tr>
-
-   <tr>
-    <td>[% LxERP.t8("Title") %]</td>
-    <td>[% L.input_tag("requirement_spec_predefined_text.title", SELF.requirement_spec_predefined_text.title, size=60) %]</td>
-   </tr>
-
-   <tr valign="top">
-    <td>[% LxERP.t8("Content") %]</td>
-    <td>[% L.textarea_tag("requirement_spec_predefined_text.text_as_restricted_html", SELF.requirement_spec_predefined_text.text_as_restricted_html, class='texteditor', style='width: 800px; height: 300px') %]</td>
-   </tr>
-
-   <tr>
-    <td>[% LxERP.t8("Useable for…") %]</sup></td>
-    <td>
-     [% L.checkbox_tag("requirement_spec_predefined_text.useable_for_text_blocks", label=LxERP.t8("Text blocks"), value=1, checked=SELF.requirement_spec_predefined_text.useable_for_text_blocks) %]
-     [% L.checkbox_tag("requirement_spec_predefined_text.useable_for_sections",    label=LxERP.t8("Sections"),    value=1, checked=SELF.requirement_spec_predefined_text.useable_for_sections) %]
-    </td>
-   </tr>
-  </table>
-
-  <p>
-   [% L.hidden_tag("id", SELF.requirement_spec_predefined_text.id) %]
-   [% L.hidden_tag("action", "RequirementSpecPredefinedText/dispatch") %]
-   [% L.submit_tag("action_" _ (SELF.requirement_spec_predefined_text.id ? "update" : "create"), LxERP.t8('Save')) %]
-   [%- IF SELF.requirement_spec_predefined_text.id %]
-    [% L.submit_tag("action_destroy", LxERP.t8('Delete'), confirm=LxERP.t8('Do you really want to delete this object?')) %]
-   [%- END %]
-   <a href="[% SELF.url_for(action => 'list') %]">[%- LxERP.t8('Abort') %]</a>
-  </p>
- </form>
diff --git a/templates/webpages/requirement_spec_predefined_text/list.html b/templates/webpages/requirement_spec_predefined_text/list.html
deleted file mode 100644 (file)
index 3e1ee28..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
-[%- INCLUDE 'common/flash.html' %]
-
- <form method="post" action="controller.pl">
-  [% IF !REQUIREMENT_SPEC_PREDEFINED_TEXTS.size %]
-   <p>
-    [%- LxERP.t8('No predefined texts has been created yet.') %]
-   </p>
-
-  [%- ELSE %]
-   <table id="requirement_spec_predefined_text_list">
-    <thead>
-    <tr class="listheading">
-     <th colspan="4"></th>
-     <th colspan="2" align="center">[% LxERP.t8("Useable for…") %]</th>
-    </tr>
-    <tr class="listheading">
-     <th align="center"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></th>
-     <th>[% LxERP.t8("Description") %]</th>
-     <th>[% LxERP.t8("Title") %]</th>
-     <th>[% LxERP.t8("Content") %]</th>
-     <th>[% LxERP.t8("Text blocks") %]</th>
-     <th>[% LxERP.t8("Sections") %]</th>
-    </tr>
-    </thead>
-
-    <tbody>
-    [%- FOREACH requirement_spec_predefined_text = REQUIREMENT_SPEC_PREDEFINED_TEXTS %]
-    <tr class="listrow[% loop.count % 2 %]" id="requirement_spec_predefined_text_id_[% requirement_spec_predefined_text.id %]">
-     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></td>
-     <td>
-      <a href="[% SELF.url_for(action => 'edit', id => requirement_spec_predefined_text.id) %]">
-       [%- HTML.escape(requirement_spec_predefined_text.description) -%]
-      </a>
-     </td>
-
-     <td>[% HTML.escape(requirement_spec_predefined_text.title) %]</td>
-     <td>[% HTML.escape(L.truncate(requirement_spec_predefined_text.text_as_stripped_html)) %]</td>
-     <td align="right">[% IF requirement_spec_predefined_text.useable_for_text_blocks %][% LxERP.t8("Yes") %][% ELSE %][% LxERP.t8("No") %][% END %]</td>
-     <td align="right">[% IF requirement_spec_predefined_text.useable_for_sections %][% LxERP.t8("Yes") %][% ELSE %][% LxERP.t8("No") %][% END %]</td>
-    </tr>
-    [%- END %]
-    </tbody>
-   </table>
-  [%- END %]
-
-  <p>
-   <a href="[% SELF.url_for(action => 'new') %]">[%- LxERP.t8("Create a new predefined text") %]</a>
-  </p>
- </form>
-
- [% L.sortable_element('#requirement_spec_predefined_text_list tbody', url => 'controller.pl?action=RequirementSpecPredefinedText/reorder', with => 'requirement_spec_predefined_text_id') %]
diff --git a/templates/webpages/requirement_spec_risk/form.html b/templates/webpages/requirement_spec_risk/form.html
deleted file mode 100755 (executable)
index 5f44502..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
- <form method="post" action="controller.pl">
-
-[%- INCLUDE 'common/flash.html' %]
-
-  <table>
-   <tr>
-    <td>[% LxERP.t8("Description") %]</td>
-    <td>[% L.input_tag("requirement_spec_risk.description", SELF.requirement_spec_risk.description) %]</td>
-   </tr>
-  </table>
-
-  <p>
-   [% L.hidden_tag("id", SELF.requirement_spec_risk.id) %]
-   [% L.hidden_tag("action", "RequirementSpecRisk/dispatch") %]
-   [% L.submit_tag("action_" _ (SELF.requirement_spec_risk.id ? "update" : "create"), LxERP.t8('Save')) %]
-   [%- IF SELF.requirement_spec_risk.id %]
-    [% L.submit_tag("action_destroy", LxERP.t8('Delete'), confirm=LxERP.t8('Do you really want to delete this object?')) %]
-   [%- END %]
-   <a href="[% SELF.url_for(action => 'list') %]">[%- LxERP.t8('Abort') %]</a>
-  </p>
- </form>
diff --git a/templates/webpages/requirement_spec_risk/list.html b/templates/webpages/requirement_spec_risk/list.html
deleted file mode 100644 (file)
index 1803b0f..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
-[%- INCLUDE "common/flash.html" %]
-
- <form method="post" action="controller.pl">
-  [% IF !REQUIREMENT_SPEC_RISKS.size %]
-   <p>
-    [%- LxERP.t8("No risks level has been created yet.") %]
-   </p>
-
-  [%- ELSE %]
-   <table id="requirement_spec_risk_list">
-    <thead>
-    <tr class="listheading">
-     <th align="center"><img src="image/updown.png" alt="[ LxERP.t8("reorder item") %]"></th>
-     <th>[%- LxERP.t8("Description") %]</th>
-    </tr>
-    </thead>
-
-    <tbody>
-    [%- FOREACH requirement_spec_risk = REQUIREMENT_SPEC_RISKS %]
-    <tr class="listrow[% loop.count % 2 %]" id="requirement_spec_risk_id_[% requirement_spec_risk.id %]">
-     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[ LxERP.t8("reorder item") %]"></td>
-     <td>
-      <a href="[% SELF.url_for(action => "edit", id => requirement_spec_risk.id) %]">
-       [%- HTML.escape(requirement_spec_risk.description) %]
-      </a>
-     </td>
-    </tr>
-    [%- END %]
-    </tbody>
-   </table>
-  [%- END %]
-
-  <p>
-   <a href="[% SELF.url_for(action => "new") %]">[%- LxERP.t8("Create a new risk level") %]</a>
-  </p>
- </form>
-
- [% L.sortable_element("#requirement_spec_risk_list tbody", url => "controller.pl?action=RequirementSpecRisk/reorder", with => "requirement_spec_risk_id") %]
diff --git a/templates/webpages/requirement_spec_status/form.html b/templates/webpages/requirement_spec_status/form.html
deleted file mode 100755 (executable)
index 8b034f9..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
- <form method="post" action="controller.pl">
-
-[%- INCLUDE 'common/flash.html' %]
-
-  <table>
-   <tr>
-    <td>[% LxERP.t8('Name') %]</td>
-    <td>[% L.select_tag("requirement_spec_status.name",  SELF.valid_names, default = SELF.requirement_spec_status.name) %]</td>
-   </tr>
-
-   <tr>
-    <td>[% LxERP.t8('Description') %]</td>
-    <td>[% L.input_tag("requirement_spec_status.description", SELF.requirement_spec_status.description) %]</td>
-   </tr>
-  </table>
-
-  <p>
-   [% L.hidden_tag("id", SELF.requirement_spec_status.id) %]
-   [% L.hidden_tag("action", "RequirementSpecStatus/dispatch") %]
-   [% L.submit_tag("action_" _ (SELF.requirement_spec_status.id ? "update" : "create"), LxERP.t8('Save')) %]
-   [%- IF SELF.requirement_spec_status.id %]
-    [% L.submit_tag("action_destroy", LxERP.t8('Delete'), confirm=LxERP.t8('Do you really want to delete this object?')) %]
-   [%- END %]
-   <a href="[% SELF.url_for(action => 'list') %]">[%- LxERP.t8('Abort') %]</a>
-  </p>
- </form>
diff --git a/templates/webpages/requirement_spec_status/list.html b/templates/webpages/requirement_spec_status/list.html
deleted file mode 100644 (file)
index 35b78c9..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
-[%- INCLUDE 'common/flash.html' %]
-
- <form method="post" action="controller.pl">
-  [% IF !REQUIREMENT_SPEC_STATUSES.size %]
-   <p>
-    [%- LxERP.t8('No requirement spec statuses has been created yet.') %]
-   </p>
-
-  [%- ELSE %]
-   <table id="requirement_spec_status_list">
-    <thead>
-    <tr class="listheading">
-     <th align="center"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></th>
-     <th>[%- LxERP.t8('Name') %]</th>
-     <th>[%- LxERP.t8('Description') %]</th>
-    </tr>
-    </thead>
-
-    <tbody>
-    [%- FOREACH requirement_spec_status = REQUIREMENT_SPEC_STATUSES %]
-    <tr class="listrow[% loop.count % 2 %]" id="requirement_spec_status_id_[% requirement_spec_status.id %]">
-     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></td>
-     <td>
-      <a href="[% SELF.url_for(action => 'edit', id => requirement_spec_status.id) %]">
-       [%- HTML.escape(requirement_spec_status.name) %]
-      </a>
-     </td>
-
-     <td>[%- HTML.escape(requirement_spec_status.description) %]</td>
-    </tr>
-    [%- END %]
-    </tbody>
-   </table>
-  [%- END %]
-
-  <p>
-   <a href="[% SELF.url_for(action => 'new') %]">[%- LxERP.t8('Create a new requirement spec status') %]</a>
-  </p>
- </form>
-
- [% L.sortable_element('#requirement_spec_status_list tbody', url => 'controller.pl?action=RequirementSpecStatus/reorder', with => 'requirement_spec_status_id') %]
diff --git a/templates/webpages/requirement_spec_type/form.html b/templates/webpages/requirement_spec_type/form.html
deleted file mode 100755 (executable)
index a965c86..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
- <form method="post" action="controller.pl">
-
-[%- INCLUDE 'common/flash.html' %]
-
-  <table>
-   <tr>
-    <td>[% LxERP.t8('Description') %]</td>
-    <td>[% L.input_tag("requirement_spec_type.description", SELF.requirement_spec_type.description) %]</td>
-   </tr>
-
-   <tr>
-    <td>[% LxERP.t8('Print template base file name') %]<sup>(1)</sup></td>
-    <td>[% L.input_tag("requirement_spec_type.template_file_name", SELF.requirement_spec_type.template_file_name) %]</td>
-   </tr>
-
-   <tr>
-    <td>[% LxERP.t8('Section number format') %]<sup>(2)</sup></td>
-    <td>[% L.input_tag("requirement_spec_type.section_number_format", SELF.requirement_spec_type.section_number_format, size="15") %]</td>
-   </tr>
-
-   <tr>
-    <td>[% LxERP.t8('Function block number format') %]<sup>(2)</sup></td>
-    <td>[% L.input_tag("requirement_spec_type.function_block_number_format", SELF.requirement_spec_type.function_block_number_format, size="15") %]</td>
-   </tr>
-  </table>
-
-  <p>
-   [% L.hidden_tag("id", SELF.requirement_spec_type.id) %]
-   [% L.hidden_tag("action", "RequirementSpecType/dispatch") %]
-   [% L.submit_tag("action_" _ (SELF.requirement_spec_type.id ? "update" : "create"), LxERP.t8('Save')) %]
-   [%- IF SELF.requirement_spec_type.id %]
-    [% L.submit_tag("action_destroy", LxERP.t8('Delete'), confirm=LxERP.t8('Do you really want to delete this object?')) %]
-   [%- END %]
-   <a href="[% SELF.url_for(action => 'list') %]">[%- LxERP.t8('Abort') %]</a>
-  </p>
-
-  <p>
-   <sup>(1)</sup>: [% LxERP.t8("The base file name without a path or an extension to be used for printing for this type of requirement spec.") %]
-   <br>
-   <sup>(2)</sup>: [% LxERP.t8('The numbering will start at 1 with each requirement spec.') %]
-  </p>
- </form>
diff --git a/templates/webpages/requirement_spec_type/list.html b/templates/webpages/requirement_spec_type/list.html
deleted file mode 100644 (file)
index 3c81986..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
-<h1>[% FORM.title %]</h1>
-
-[%- INCLUDE 'common/flash.html' %]
-
- <form method="post" action="controller.pl">
-  [% IF !REQUIREMENT_SPEC_TYPES.size %]
-   <p>
-    [%- LxERP.t8('No requirement spec type has been created yet.') %]
-   </p>
-
-  [%- ELSE %]
-   <table id="requirement_spec_type_list">
-    <thead>
-    <tr class="listheading">
-     <th align="center"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></th>
-     <th>[%- LxERP.t8('Description') %]</th>
-     <th>[%- LxERP.t8('Section number format') %]</th>
-     <th>[%- LxERP.t8('Function block number format') %]</th>
-    </tr>
-    </thead>
-
-    <tbody>
-    [%- FOREACH requirement_spec_type = REQUIREMENT_SPEC_TYPES %]
-    <tr class="listrow[% loop.count % 2 %]" id="requirement_spec_type_id_[% requirement_spec_type.id %]">
-     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></td>
-     <td>
-      <a href="[% SELF.url_for(action => 'edit', id => requirement_spec_type.id) %]">
-       [%- HTML.escape(requirement_spec_type.description) %]
-      </a>
-     </td>
-
-     <td>[% HTML.escape(requirement_spec_type.section_number_format) %]</td>
-     <td>[% HTML.escape(requirement_spec_type.function_block_number_format) %]</td>
-    </tr>
-    [%- END %]
-    </tbody>
-   </table>
-  [%- END %]
-
-  <p>
-   <a href="[% SELF.url_for(action => 'new') %]">[%- LxERP.t8('Create a new requirement spec type') %]</a>
-  </p>
- </form>
-
- [% L.sortable_element('#requirement_spec_type_list tbody', url => 'controller.pl?action=RequirementSpecType/reorder', with => 'requirement_spec_type_id') %]
index d148e22..c1448ac 100644 (file)
@@ -1,20 +1,23 @@
 [%- USE T8 %]
-[% USE HTML %] <input type="hidden" name="rowcount" value="[% HTML.escape(row_idx) %]">
-
- [% PRINT_OPTIONS %]
+[% USE HTML %][%- USE L -%] <input type="hidden" name="rowcount" value="[% HTML.escape(row_idx) %]">
 
  <input type="hidden" name="todate" value="[% HTML.escape(todate) %]">
+ <input type="hidden" name="fromdate" value="[% HTML.escape(fromdate) %]">
  <input type="hidden" name="title" value="[% HTML.escape(title) %]">
  <input type="hidden" name="arap" value="[% HTML.escape(arap) %]">
  <input type="hidden" name="ct" value="[% HTML.escape(ct) %]">
  <input type="hidden" name="customer" value="[% HTML.escape(customer) %]">
  <input type="hidden" name="vendor" value="[% HTML.escape(vendor) %]">
  <input type="hidden" name="department" value="[% HTML.escape(department) %]">
+ [% L.hidden_tag("formname", "statement") %]
 
- [% 'Statement' | $T8 %]
- <br>
- <input class="submit" type="submit" name="action" value="[% 'Select all' | $T8 %]">
- <input class="submit" type="submit" name="action" value="[% 'Print' | $T8 %]">
- <input class="submit" type="submit" name="action" value="[% 'E-mail' | $T8 %]">
+<div id="email_inputs" class="hidden"></div>
+<div id="print_options" class="hidden">
+ [% PRINT_OPTIONS %]
+</div>
+</form>
 
 </form>
+<div id="print_dialog" class="hidden">
+ [%- PROCESS 'common/_print_dialog.html' %]
+</div>
index 066ac98..cae322a 100644 (file)
@@ -1 +1 @@
-<form method="post" action="rp.pl">
+<form method="post" action="rp.pl" id="form">
index 8c87931..7c15915 100644 (file)
@@ -1,6 +1,7 @@
 [%- USE T8 %]
 [%- USE HTML %]
 [%- USE LxERP %]
+[%- USE L %]
 
 <table border=0 cellpadding=0 cellspacing=0>
 <tr class="headline">
@@ -37,7 +38,8 @@
 <tr class="white"><td class="left right" colspan="11">&nbsp;</td></tr>
 
 <tr class="grey">
-  <td class="left"><nobr>Umsatzerl&ouml;se</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(1)) %]</td>
+ </nobr></td>
   <td><nobr>[% jetzt1 %]</nobr></td>
   <td><nobr>[% jetztgl1 %]</nobr></td>
   <td></td>
@@ -51,7 +53,7 @@
 </tr>
 
 <tr class="white">
-  <td class="left"><nobr>Best.Verdg. FE/UE</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(2)) %]</nobr></td>
   <td><nobr>[% jetzt2 %]</nobr></td>
   <td><nobr>[% jetztgl2 %]</nobr></td>
   <td></td>
@@ -65,7 +67,7 @@
 </tr>
 
 <tr class="grey">
-  <td class="left"><nobr>Akt.Eigenleistungen</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(3)) %]</nobr></td>
   <td><nobr>[% jetzt3 %]</nobr></td>
   <td><nobr>[% jetztgl3 %]</nobr></td>
   <td></td>
@@ -97,7 +99,7 @@
 <tr class="white"><td class="left right" colspan="11">&nbsp;</td></tr>
 
 <tr class="grey">
-  <td class="left"><nobr>Mat./Wareneinkauf</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(4)) %]</nobr></td>
   <td><nobr>[% jetzt4 %]</nobr></td>
   <td><nobr>[% jetztgl4 %]</nobr></td>
   <td><nobr>[% jetztgk4 %]</nobr></td>
 <tr class="white"><td class="left right" colspan="11">&nbsp;</td></tr>
 
 <tr class="grey">
-  <td class="left"><nobr>So.betr.Erl&ouml;se</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(5)) %]</nobr></td>
   <td><nobr>[% jetzt5 %]</nobr></td>
   <td><nobr>[% jetztgl5 %]</nobr></td>
   <td><nobr>[% jetztgk5 %]</nobr></td>
@@ -167,7 +169,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 </tr>
 
 <tr class="white">
-  <td class="left"><nobr>Personalkosten</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(10)) %]</nobr></td>
   <td><nobr>[% jetzt10 %]</nobr></td>
   <td><nobr>[% jetztgl10 %]</nobr></td>
   <td><nobr>[% jetztgk10 %]</nobr></td>
@@ -181,7 +183,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 </tr>
 
 <tr class="grey">
-  <td class="left"><nobr>Raumkosten</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(11)) %]</nobr></td>
   <td><nobr>[% jetzt11 %]</nobr></td>
   <td><nobr>[% jetztgl11 %]</nobr></td>
   <td><nobr>[% jetztgk11 %]</nobr></td>
@@ -195,7 +197,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 </tr>
 
 <tr class="white">
-  <td class="left"><nobr>Betriebl.Steuern</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(12)) %]</nobr></td>
   <td><nobr>[% jetzt12 %]</nobr></td>
   <td><nobr>[% jetztgl12 %]</nobr></td>
   <td><nobr>[% jetztgk12 %]</nobr></td>
@@ -209,7 +211,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 </tr>
 
 <tr class="grey">
-  <td class="left"><nobr>Versich./Beitr&auml;ge</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(13)) %]</nobr></td>
   <td><nobr>[% jetzt13 %]</nobr></td>
   <td><nobr>[% jetztgl13 %]</nobr></td>
   <td><nobr>[% jetztgk13 %]</nobr></td>
@@ -223,7 +225,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 </tr>
 
 <tr class="grey">
-  <td class="left"><nobr>Kfz-Kosten (o.St.)</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(14)) %]</nobr></td>
   <td><nobr>[% jetzt14 %]</nobr></td>
   <td><nobr>[% jetztgl14 %]</nobr></td>
   <td><nobr>[% jetztgk14 %]</nobr></td>
@@ -237,7 +239,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 </tr>
 
 <tr class="white">
-  <td class="left"><nobr>Werbe-/Reisekosten</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(15)) %]</nobr></td>
   <td><nobr>[% jetzt15 %]</nobr></td>
   <td><nobr>[% jetztgl15 %]</nobr></td>
   <td><nobr>[% jetztgk15 %]</nobr></td>
@@ -251,7 +253,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 </tr>
 
 <tr class="grey">
-  <td class="left"><nobr>Kosten Warenabgabe</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(16)) %]Kosten </nobr></td>
   <td><nobr>[% jetzt16 %]</nobr></td>
   <td><nobr>[% jetztgl16 %]</nobr></td>
   <td><nobr>[% jetztgk16 %]</nobr></td>
@@ -266,7 +268,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 </tr>
 
 <tr class="white">
-  <td class="left"><nobr>Abschreibungen</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(17)) %]</nobr></td>
   <td><nobr>[% jetzt17 %]</nobr></td>
   <td><nobr>[% jetztgl17 %]</nobr></td>
   <td><nobr>[% jetztgk17 %]</nobr></td>
@@ -280,7 +282,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 </tr>
 
 <tr class="grey">
-  <td class="left"><nobr>Reparatur/Instandh.</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(18)) %]</nobr></td>
   <td><nobr>[% jetzt18 %]</nobr></td>
   <td><nobr>[% jetztgl18 %]</nobr></td>
   <td><nobr>[% jetztgk18 %]</nobr></td>
@@ -294,7 +296,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 </tr>
 
 <tr class="white">
-  <td class="left"><nobr>Sonstige Kosten</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(20)) %]</nobr></td>
   <td><nobr>[% jetzt20 %]</nobr></td>
   <td><nobr>[% jetztgl20 %]</nobr></td>
   <td><nobr>[% jetztgk20 %]</nobr></td>
@@ -343,7 +345,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 <tr class="white"><td class="left right" colspan="11">&nbsp;</td></tr>
 
 <tr class="grey">
-  <td class="left"><nobr>Zinsaufwand</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(30)) %]</nobr></td>
   <td><nobr>[% jetzt30 %]</nobr></td>
   <td><nobr>[% jetztgl30 %]</nobr></td>
   <td><nobr>[% jetztgk30 %]</nobr></td>
@@ -357,7 +359,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 </tr>
 
 <tr class="white">
-  <td class="left"><nobr>&Uuml;brige Steuern</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(19)) %]</nobr></td>
   <td><nobr>[% jetzt19 %]</nobr></td>
   <td><nobr>[% jetztgl19 %]</nobr></td>
   <td><nobr>[% jetztgk19 %]</nobr></td>
@@ -371,7 +373,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 </tr>
 
 <tr class="grey">
-  <td class="left"><nobr>Sonst. neutr. Aufwand</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(31)) %]</nobr></td>
   <td><nobr>[% jetzt31 %]</nobr></td>
   <td><nobr>[% jetztgl31 %]</nobr></td>
   <td><nobr>[% jetztgk31 %]</nobr></td>
@@ -401,7 +403,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 <tr class="grey"><td class="left right" colspan="11">&nbsp;</td></tr>
 
 <tr class="white">
-  <td class="left"><nobr>Zinsertr&auml;ge</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(32)) %]</nobr></td>
   <td><nobr>[% jetzt32 %]</nobr></td>
   <td><nobr>[% jetztgl32 %]</nobr></td>
   <td><nobr>[% jetztgk32 %]</nobr></td>
@@ -415,7 +417,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 </tr>
 
 <tr class="grey">
-  <td class="left"><nobr>Sonst. neutr. Ertr.</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(33)) %]</nobr></td>
   <td><nobr>[% jetzt33 %]</nobr></td>
   <td><nobr>[% jetztgl33 %]</nobr></td>
   <td><nobr>[% jetztgk33 %]</nobr></td>
@@ -429,7 +431,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 </tr>
 
 <tr class="white">
-  <td class="left"><nobr>Verr.kalk.Kosten</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(34)) %]</nobr></td>
   <td><nobr>[% jetzt34 %]</nobr></td>
   <td><nobr>[% jetztgl34 %]</nobr>
   <td><nobr>[% jetztgk34 %]</nobr></td>
@@ -475,7 +477,7 @@ class="right"><nobr>[% kummaufbetriebrohertrag %]</nobr>&nbsp;</td>
 <tr class="white"><td class="left right" colspan="11">&nbsp;</td></tr>
 
 <tr class="grey">
-  <td class="left"><nobr>Steuern Eink.u.Ertr.</nobr></td>
+  <td class="left"><nobr>[% HTML.escape(category_names.item(35)) %]</nobr></td>
   <td><nobr>[% jetzt35 %]</nobr></td>
   <td><nobr>[% jetztgl35 %]</nobr></td>
   <td><nobr>[% jetztgk35 %]</nobr></td>
@@ -513,3 +515,60 @@ colspan="11">&nbsp;</td></tr>
 </tr>
 
 </table>
+
+<br>
+[% L.button_tag('', LxERP.t8('Show chart list'), id="show_chartlist_button", class="hide") %]
+[% L.button_tag('', LxERP.t8('Hide chart list'), id="hide_chartlist_button", class="hide") %]
+<div id="chartlist">
+<div>[% 'Chart list' | $T8 %]</div>
+<div>
+<table>
+<tr>
+  <th>[% 'Chart'        | $T8 %]</th>
+  <th>[% 'Description'  | $T8 %]</th>
+  <th>[% 'Amount'       | $T8 %]</th>
+  <th>[% 'Category'     | $T8 %]</th>
+</tr>
+[% FOREACH key = charts.keys.sort %]
+[% UNLESS charts.$key.pos_bwa %]
+[% NEXT %]
+[% END %]
+<tr>
+ <td>[% charts.$key.accno %]</td>
+ <td>[% charts.$key.description %]</td>
+ <td class="numeric">[%  LxERP.format_amount( charts.$key.amount, 2 ) %]</td>
+ <td>[% HTML.escape(category_names.item(charts.$key.pos_bwa)) %]</td>
+</tr>
+[% END %]
+</table>
+</div>
+</div>
+
+[% # L.dump(charts_by_category.item(1)) # Debug Umsatzerlöse %]
+
+<script language="javascript">
+$( document ).ready(function() {
+  $( "#hide_chartlist_button" ).hide();
+  $( "#chartlist" ).hide();
+
+  $( "#show_chartlist_button" ).click(function() {
+    $( "#chartlist" ).toggle();
+    $('html, body').animate({
+        scrollTop: $(this).offset().top
+    }, 500);
+    $(this).hide();
+    $("#hide_chartlist_button").show();
+  });
+
+  $( "#hide_chartlist_button" ).click(function() {
+    $( "#chartlist" ).toggle();
+    $('html, body').animate({
+        scrollTop: $(this).offset().top
+    }, 500);
+    $(this).hide();
+    $("#show_chartlist_button").show();
+  });
+
+})
+
+</script>
diff --git a/templates/webpages/rp/e_mail.html b/templates/webpages/rp/e_mail.html
deleted file mode 100644 (file)
index 387769e..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-[%- USE HTML %]
-[%- USE L %]
-[%- USE LxERP %]
-[%- USE T8 %]
-
-<h1>[% 'E-mail Statement to' | $T8 %] [% $ct %]</h1>
-
-<form method=post action='[% script %]'>
-
-<table>
-  <tr>
-    <th align=right nowrap>[% 'E-mail' | $T8 %]</th>
-    <td><input name=email size=30 value="[% email %]"></td>
-    <th align=right nowrap>[% 'Cc' | $T8 %]</th>
-    <td><input name=cc size=30 value="[% cc %]"></td>
-  </tr>
-  <tr>
-    <th align=right nowrap>[% 'Subject' | $T8 %]</th>
-    <td><input name=subject size=30 value="[% subject %]"></td>
-[% IF show_bcc %]
-    <th align=right nowrap=true>[% 'Bcc' | $T8 %]</th>
-    <td><input name=bcc size=30 value="[% bcc | $T8 %]"></td>
-[%- END %]
-  </tr>
-</table>
-<table width=100%>
-  <tr>
-    <th align=left nowrap>[% 'Message' | $T8 %]</th>
-  </tr>
-  <tr>
-    <td><textarea name=message rows=15 cols=60 wrap=soft>[% message %]</textarea></td>
-  </tr>
-</table>
-[% print_options %]
-<hr size=3 noshade>
-
-<input type=hidden name=nextsub value=send_email>
-[%- FOREACH var = hidden_values %]
-[% L.hidden_tag(var, $var) %]
-[%- END %]
-
-<br>
-<input name=action class=submit type=submit value="[% 'Continue' | $T8 %]">
-</form>
-
diff --git a/templates/webpages/rp/erfolgsrechnung.html b/templates/webpages/rp/erfolgsrechnung.html
new file mode 100644 (file)
index 0000000..f3a73c7
--- /dev/null
@@ -0,0 +1,40 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+
+<h2 align="center">
+  <br>[% company %]
+  <br>[% address %]
+  <p>[% 'ERFOLGSRECHNUNG' %]
+  <br>[% fromdate %] bis [% todate %]
+</h2>
+<table border="0">
+  <tr>
+    <th align="left" width="400" colspan="2"><br></th>
+  </tr>
+  [%- FOREACH category = categories %]
+    <tr valign="top">
+      <th align="left" colspan="4">[% category.name %]<b><hr align="left" width="250" size="5" noshade></th>
+    </tr>
+    [%- FOREACH row = category.accounts %]
+      <tr>
+        <td align="left">[% row.accno %]</td>
+        <td align="left">[% row.description %]</td>
+        <td align="right">[% row.total %]</td>
+      </tr>
+    [%- END %]
+    <tr>
+      <td colspan="2"> </td>
+      <td><hr noshade size="1"></td>
+      <td><hr noshade size="1"></td>
+    </tr>
+    <tr valign="top">
+      <th align="left" colspan="2">TOTAL</th>
+      <td align="right">[% category.total %]<hr noshade size="2"></td>
+    </tr>
+  [%- END %]
+  <tr valign="top">
+    <th align="left" colspan="2">GEWINN/VERLUST</th>
+    <td align="right">[% total %]<br><hr noshade size="2"></td>
+  </tr>
+</table>
index 3fb2e9a..5004a30 100644 (file)
@@ -84,9 +84,6 @@
      </tr>
     [% END %]
    [% END %]
-
-   <tr><td colspan="[% NUM_COLUMNS %]"><hr size="3" noshade></td></tr>
-
   </table>
  </p>
  [% ELSE %]
  [% END %]
 
  [% IF SHOW_EXPORT_BUTTONS %]
-  <form action="[% HTML.escape(script) %]" name="report_generator_form" method="post">
+  <form action="[% HTML.escape(script) %]" name="report_generator_form" id="report_generator_form" method="post">
    [% FOREACH var = EXPORT_VARIABLES %]<input type="hidden" name="report_generator_hidden_[% var.key %]" value="[% HTML.escape(var.value) %]">
    [% END %]
 
    <input type="hidden" name="report_generator_variable_list" value="[% HTML.escape(EXPORT_VARIABLE_LIST) %]">
    <input type="hidden" name="report_generator_dispatch_to" value="">
    <input type="hidden" name="action" value="report_generator_dispatcher">
-
-   <p>
-    Listenexport<br>
-    [% IF ALLOW_PDF_EXPORT %]<button type="button" class="submit" onclick="submit_report_generator_form('report_generator_export_as_pdf')">Als PDF exportieren</button>[% END %]
-    [% IF ALLOW_CSV_EXPORT %]<button type="button" class="submit" onclick="submit_report_generator_form('report_generator_export_as_csv')">Als CSV exportieren</button>[% END %]
-   </p>
   </form>
  [% END %]
-
index a38cd99..9800d1e 100644 (file)
@@ -1,8 +1,12 @@
 [%- USE T8 %]
 [%- USE HTML %]
 [%- USE LxERP %]
+[%- USE L %]
+[% L.button_tag('', LxERP.t8('Hide buttons'), id="hide_buttons_button", class="hide") %]
+[% L.button_tag('', LxERP.t8('Show charts'),  id="show_charts_button",  class="hide") %]
+[% L.button_tag('', LxERP.t8('Hide charts'),  id="hide_charts_button",  class="hide") %]
 
-<h3 align=center> [% title %]</h3>
+<h3 align="center" id="show_buttons"> [% title %]</h3>
 <h3 align=center>
 [% period %]<br>
 [% accounting_method %]<br>
 <br>[% report_date %]
 </h3>
 
-<table width=100% border=0>
+<style type="text/css">
+
+#eurtable {
+  border-collapse: collapse;
+  width: 100%;
+}
+
+tr.category {
+  /* background set via jquery */
+}
+
+tr.chart {
+  border: 0;
+}
+
+td.chartname {
+  padding-left: 50px;
+}
+
+tr.chartrow {
+  font-size: 75%;
+}
+
+.guv_row_background {
+  background:#f0f0f0;
+}
+</style>
+
+<table id="eurtable">
 <tr>
   <td width=75% align=left colspan=2><font size="+1"><b>A. Betriebseinnahmen</font></b><br></td>
   <td></td>
 </tr>
 
-<tr>
-  <td>
-    Umsatzerl&ouml;se
-  </td>
-  <td>
-    [% eur1 %]
-  </td>
-</tr>
-<tr>
-  <td>
-    sonstige Erl&ouml;se
-  </td>
-  <td>
-    [% eur2 %]
-  </td>
-</tr>
-<tr>
-  <td>
-    Privatanteile
-  </td>
-  <td>
-    [% eur3 %]
-  </td>
-</tr>
-<tr>
-  <td>
-    Zinsertr&auml;ge
-  </td>
-  <td>
-    [% eur4 %]
-  </td>
-</tr>
-<tr>
-  <td>
-    Au&szlig;erordentliche Ertr&auml;ge
-  </td>
-  <td>
-    [% eur5 %]
-  </td>
-</tr>
-<tr>
-  <td>
-    Vereinnahmte Umsatzsteuer
-  </td>
-  <td>
-    [% eur6 %]
-  </td>
-</tr>
-<tr>
-  <td>
-    Umsatzsteuererstattungen
-  </td>
-  <td>
-    [% eur7 %]
-  </td>
-</tr>
-
-
+[% FOREACH i IN categories_income %]
+  <tr class="category" data-catid="cat[% i %]">
+    <td>
+      [% HTML.escape(category_names.item(i)) %]
+    </td>
+    <td class="numeric">
+      [% eur_amounts.item(i) %]
+    </td>
+  </tr>
+  [% FOREACH chart = charts_by_category.item(i).list %]
+  <tr class="chartrow cat[% i %]">
+    <td class="chartname">[% chart.accno %] [% chart.description %]</td>
+    <td class="numeric"> [% LxERP.format_amount(chart.amount,2) %] </td>
+  </tr>
+  [% END %]
+[% END %]
 <tr>
   <td> </td>
   <td><hr noshade size=1></td>
   <td></td>
 </tr>
 
-<tr>
-  <td>
-    Wareneing&auml;nge
-  </td>
-  <td>
-    [% eur8 %]
-  </td>
-</tr>
-<tr>
-  <td>
-    L&ouml;hne und Geh&auml;lter
-  </td>
-  <td>
-    [% eur9 %]
-  </td>
-</tr>
-<tr>
-  <td>
-    Gesetzlicher sozialer Aufwand
-  </td>
-  <td>
-    [% eur10 %]
-  </td>
-</tr>
-<tr>
-  <td>
-    Mieten
-  </td>
-  <td>
-    [% eur11 %]
-  </td>
-</tr>
-<tr>
-  <td>
-    Gas, Strom, Wasser
-  </td>
-  <td>
-    [% eur12 %]
-  </td>
-</tr>
-<tr>
-  <td>
-    Instandhaltung
-  </td>
-  <td>
-    [% eur13 %]
-  </td>
-</tr>
-<tr>
-  <td>
-    Steuern, Versicherungen, Beitr&auml;ge
-  </td>
-  <td>
-    [% eur14 %]
-  </td>
-</tr>
-<tr>
-  <td>
-    Kfz-Steuern
-  </td>
-  <td>
-    [% eur15 %]
-  </td>
-</tr><tr>
-  <td>
-    Kfz-Versicherungen
-  </td>
-  <td>
-    [% eur16 %]
-  </td>
-</tr><tr>
-  <td>
-    Sonstige Fahrzeugkosten
-  </td>
-  <td>
-    [% eur17 %]
-  </td>
-</tr><tr>
-  <td>
-    Werbe- und Reisekosten
-  </td>
-  <td>
-    [% eur18 %]
-  </td>
-</tr><tr>
-  <td>
-    Instandhaltung und Werkzeuge
-  </td>
-  <td>
-    [% eur19 %]
-  </td>
-</tr><tr>
-  <td>
-    Fachzeitschriften, B&uuml;cher
-  </td>
-  <td>
-    [% eur20 %]
-  </td>
-</tr><tr>
-  <td>
-    Miete f&uuml;r Einrichtungen
-  </td>
-  <td>
-    [% eur21 %]
-  </td>
-</tr><tr>
-  <td>
-    Rechts- und Beratungskosten
-  </td>
-  <td>
-    [% eur22 %]
-  </td>
-</tr><tr>
-  <td>
-    B&uuml;robedarf, Porto, Telefon
-  </td>
-  <td>
-    [% eur23 %]
-  </td>
-</tr><tr>
-  <td>
-    Sonstige Aufwendungen
-  </td>
-  <td>
-    [% eur24 %]
-  </td>
-</tr><tr>
-  <td>
-    Abschreibungen auf Anlageverm&ouml;gen
-  </td>
-  <td>
-    [% eur25 %]
-  </td>
-</tr><tr>
-  <td>
-    Abschreibungen auf GWG
-  </td>
-  <td>
-    [% eur26 %]
-  </td>
-</tr><tr>
-  <td>
-    Vorsteuer
-  </td>
-  <td>
-    [% eur27 %]
-  </td>
-</tr><tr>
-  <td>
-    Umsatzsteuerzahlungen
-  </td>
-  <td>
-    [% eur28 %]
-  </td>
-</tr><tr>
-  <td>
-    Zinsaufwand
-  </td>
-  <td>
-    [% eur29 %]
-  </td>
-</tr><tr>
-  <td>
-    Au&szlig;erordentlicher Aufwand
-  </td>
-  <td>
-    [% eur30 %]
-  </td>
-</tr><tr>
-  <td>
-    Betriebliche Steuern
-  </td>
-  <td>
-    [% eur31 %]
-  </td>
-</tr>
-
-
+[% FOREACH i IN categories_expense %]
+  <tr class="category" data-catid="cat[% i %]">
+    <td>
+      [% HTML.escape(category_names.item(i)) %]
+    </td>
+    <td class="numeric">
+      [% eur_amounts.item(i) %]
+    </td>
+  </tr>
+  [% FOREACH chart = charts_by_category.item(i).list %]
+  <tr class="chartrow cat[% i %]">
+    <td class="chartname">[% chart.accno %] [% chart.description %]</td>
+    <td class="numeric"> [% LxERP.format_amount(chart.amount,2) %] </td>
+  </tr>
+  [% END %]
+[% END %]
 <tr>
   <td> </td>
   <td><hr noshade size=1></td>
 
 </table>
 
+
+<br>
+
+[% L.button_tag('', LxERP.t8('Show chart list'), id="show_chartlist_button", class="hide") %]
+[% L.button_tag('', LxERP.t8('Hide chart list'), id="hide_chartlist_button", class="hide") %]
+
+<div id="chartlist">
+<div>[% 'Chart list' | $T8 %]</div>
+<div>
+<table>
+<tr>
+  <th>[% 'Chart'    | $T8 %]</th>
+  <th>[% 'Amount'   | $T8 %]</th>
+  <th>[% 'Category' | $T8 %]</th>
+</tr>
+[% FOREACH key = charts.keys.sort %]
+[% UNLESS charts.$key.pos_eur %]
+[% NEXT %]
+[% END %]
+<tr>
+ <td>[% charts.$key.accno %]</td>
+ <td class="numeric">[%  LxERP.format_amount( charts.$key.amount, 2 ) %]</td>
+ <td>[% HTML.escape(category_names.item(charts.$key.pos_eur)) %]</td>
+</tr>
+[% END %]
+</table>
+</div>
+</div>
 </body>
 </html>
 
+<script language="javascript">
+$( document ).ready(function() {
+  $( ".chartrow" ).hide();
+  $( "#hide_charts_button" ).hide();
+  $( "#hide_chartlist_button" ).hide();
+  $( "#chartlist" ).hide();
+  $( '.category:even' ).css('background-color','#f0f0f0');
+  $( '.category:odd' ).css('background-color','#f8f8f8');
+
+  $( "#show_chartlist_button" ).click(function() {
+    $( "#chartlist" ).toggle();
+    $('html, body').animate({
+        scrollTop: $(this).offset().top
+    }, 500);
+    $(this).hide();
+    $("#hide_chartlist_button").show();
+  });
+
+  $( "#hide_chartlist_button" ).click(function() {
+    $( "#chartlist" ).toggle();
+    $('html, body').animate({
+        scrollTop: $(this).offset().top
+    }, 500);
+    $(this).hide();
+    $("#show_chartlist_button").show();
+  });
+
+  $( "#hide_buttons_button" ).click(function() {
+    $( ".hide" ).hide();
+  });
+
+  $( "#show_buttons" ).click(function() {
+    $( ".hide" ).show();
+  });
+
+  $( "#show_charts_button" ).click(function() {
+    $( ".chartrow" ).show();
+    $(this).hide();
+    $("#hide_charts_button").show();
+  });
+
+  $( "#hide_charts_button" ).click(function() {
+    $( ".chartrow" ).hide();
+    $(this).hide();
+    $("#show_charts_button").show();
+  });
+
+  $( ".category" ).click(function() {
+    var chartrow_class = $(this).attr('data-catid');
+    $('.' + chartrow_class).toggle();
+  });
+})
+
+</script>
index 776a02d..d824f2e 100644 (file)
@@ -5,12 +5,12 @@
 <table>
   <tr>
     <td>
-     <select name=type>
+     <select name="type" id="type">
       <option value=statement [% PD.statement %]>[% 'Statement' | $T8 %]</option>
      </select>
     </td>
     <td>
-     <select name=format>
+     <select name="format" id="format">
       <option value=html [% DF.html %]>[% 'HTML' | $T8 %]</option>
       <option value=pdf [% DF.pdf %]>[% 'PDF' | $T8 %]</option>
       <option value=postscript [% DF.postscript %]>[% 'Postscript' | $T8 %]</option>
       <option value=inline [% SM.inline %]>[% 'In-line' | $T8 %]</option>
 [%- ELSE %]
       <option value=screen [% OP.screen %]>[% 'Screen' | $T8 %]</option>
-  [%- IF got_printer && show_latex %]
+  [%- IF MYCONFIG.printer && LXCONFIG.print_templates.latex %]
       <option value=printer [% OP.printer %]>[% 'Printer' | $T8 %]</option>
   [%- END %]
 [%- END %]
      </select>
     </td>
-[%- IF got_printer && show_latex && !is_email %]
+[%- IF MYCONFIG.printer && LXCONFIG.print_templates.latex && !is_email %]
       <td>[% 'Copies' | $T8 %]<input name=copies size=2 value=[% copies %]></td>
 [%- END %]
   </tr>
index 93ef0a9..6866da5 100644 (file)
@@ -1,6 +1,7 @@
 [%- USE HTML %]
 [%- USE LxERP %]
 [%- USE L %]
+[%- USE P %]
 [%- USE T8 %]
 
 [%- BLOCK customized_report %]
@@ -9,7 +10,9 @@
   </tr>
   <tr>
     <th colspan=1>[% 'Year' | $T8 %]</th>
-    <td><input name=year size=11 title="[% 'YYYY' | $T8 %]" value="[% year %]" class="initial_focus"></td>
+    <td>
+      <input name=year size=11 title="[% 'YYYY' | $T8 %]" value="[% year %]" class="initial_focus" oninput='set_from_to(duetyp.value, this.value)'>
+    </td>
   </tr>
   <tr>
     <td align=right> <b>[% 'Yearly' | $T8 %]</b> </td>
     <th align=left colspan=3>[% 'Monthly' | $T8 %]</th>
   </tr>
   <tr>
-    <td align=right>&nbsp; <input name=duetyp class=radio type=radio value="13"></td>
-    <td><input name=duetyp class=radio type=radio value="A">&nbsp;1. [% 'Quarter' | $T8 %]</td>
-    <td><input name=duetyp class=radio type=radio value="1" checked>&nbsp;[% 'January' | $T8 %]</td>
-    <td><input name=duetyp class=radio type=radio value="5">&nbsp;[% 'May' | $T8 %]</td>
-    <td><input name=duetyp class=radio type=radio value="9">&nbsp;[% 'September' | $T8 %]</td>
+    <td align=right>&nbsp;
+      <input name=duetyp class=radio type=radio value="13" checked onchange='set_from_to(this.value, year.value)'>
+    </td>
+    <td><input name=duetyp class=radio type=radio value="A" onchange='set_from_to(this.value, year.value)'>
+      &nbsp;1. [% 'Quarter' | $T8 %]
+    </td>
+    <td><input name=duetyp class=radio type=radio value="1" onchange='set_from_to(this.value, year.value)'>
+      &nbsp;[% 'January' | $T8 %]
+    </td>
+    <td><input name=duetyp class=radio type=radio value="5" onchange='set_from_to(this.value, year.value)'>
+      &nbsp;[% 'May' | $T8 %]
+    </td>
+    <td><input name=duetyp class=radio type=radio value="9" onchange='set_from_to(this.value, year.value)'>
+      &nbsp;[% 'September' | $T8 %]
+    </td>
   </tr>
   <tr>
     <td align= right>&nbsp;</td>
-    <td><input name=duetyp class=radio type=radio value="B">&nbsp;2. [% 'Quarter' | $T8 %]</td>
-    <td><input name=duetyp class=radio type=radio value="2">&nbsp;[% 'February' | $T8 %]</td>
-    <td><input name=duetyp class=radio type=radio value="6">&nbsp;[% 'June' | $T8 %]</td>
-    <td><input name=duetyp class=radio type=radio value="10">&nbsp;[% 'October' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="B" onchange='set_from_to(this.value, year.value)'>
+      &nbsp;2. [% 'Quarter' | $T8 %]
+    </td>
+    <td><input name=duetyp class=radio type=radio value="2" onchange='set_from_to(this.value, year.value)'>
+      &nbsp;[% 'February' | $T8 %]
+    </td>
+    <td><input name=duetyp class=radio type=radio value="6" onchange='set_from_to(this.value, year.value)'>
+      &nbsp;[% 'June' | $T8 %]
+    </td>
+    <td><input name=duetyp class=radio type=radio value="10" onchange='set_from_to(this.value, year.value)'>
+      &nbsp;[% 'October' | $T8 %]
+    </td>
   </tr>
   <tr>
     <td> &nbsp;</td>
-    <td><input name=duetyp class=radio type=radio value="C">&nbsp;3. [% 'Quarter' | $T8 %]</td>
-    <td><input name=duetyp class=radio type=radio value="3">&nbsp;[% 'March' | $T8 %]</td>
-    <td><input name=duetyp class=radio type=radio value="7">&nbsp;[% 'July' | $T8 %]</td>
-    <td><input name=duetyp class=radio type=radio value="11">&nbsp;[% 'November' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="C" onchange='set_from_to(this.value, year.value)'>
+      &nbsp;3. [% 'Quarter' | $T8 %]
+    </td>
+    <td><input name=duetyp class=radio type=radio value="3" onchange='set_from_to(this.value, year.value)'>
+      &nbsp;[% 'March' | $T8 %]
+    </td>
+    <td><input name=duetyp class=radio type=radio value="7" onchange='set_from_to(this.value, year.value)'>
+      &nbsp;[% 'July' | $T8 %]
+    </td>
+    <td><input name=duetyp class=radio type=radio value="11" onchange='set_from_to(this.value, year.value)'>
+      &nbsp;[% 'November' | $T8 %]
+    </td>
   </tr>
   <tr>
     <td> &nbsp;</td>
-    <td><input name=duetyp class=radio type=radio value="D">&nbsp;4. [% 'Quarter' | $T8 %]</td>
-    <td><input name=duetyp class=radio type=radio value="4">&nbsp;[% 'April' | $T8 %]</td>
-    <td><input name=duetyp class=radio type=radio value="8">&nbsp;[% 'August' | $T8 %]</td>
-    <td><input name=duetyp class=radio type=radio value="12">&nbsp;[% 'December' | $T8 %]</td>
+    <td><input name=duetyp class=radio type=radio value="D" onchange='set_from_to(this.value, year.value)'>
+      &nbsp;4. [% 'Quarter' | $T8 %]
+    </td>
+    <td><input name=duetyp class=radio type=radio value="4" onchange='set_from_to(this.value, year.value)'>
+      &nbsp;[% 'April' | $T8 %]
+    </td>
+    <td><input name=duetyp class=radio type=radio value="8" onchange='set_from_to(this.value, year.value)'>
+      &nbsp;[% 'August' | $T8 %]
+    </td>
+    <td><input name=duetyp class=radio type=radio value="12" onchange='set_from_to(this.value, year.value)'>
+      &nbsp;[% 'December' | $T8 %]
+    </td>
   </tr>
   <tr>
     <td colspan=5><hr size=3 noshade></td>
@@ -51,7 +88,7 @@
     <th align=left><input name=reporttype class=radio type=radio value="free">[% 'Free report period' | $T8 %]</th>
     <td align=left colspan=4>
       [% 'From' | $T8 %] [% L.date_tag('fromdate', fromdate) %]
-      [% 'Bis' | $T8 %] [% L.date_tag('todate') %]
+      [% 'Bis' | $T8 %] [% L.date_tag('todate', todate)  %]
     </td>
   </tr>
   <tr>
   <tr>
     <th align=left>[% 'Method' | $T8 %]</th>
     <td colspan=3>
-      [% L.radio_button_tag('method', value='accrual', checked=accrual, label=LxERP.t8('Accrual')) %]
-      [% L.radio_button_tag('method', value='cash', checked=cash, label=LxERP.t8('cash')) %]
+      [% L.radio_button_tag('method', value='accrual', checked=(INSTANCE_CONF.get_accounting_method=='accrual'), label=LxERP.t8('Accrual')) %]
+      [% L.radio_button_tag('method', value='cash', checked=(INSTANCE_CONF.get_accounting_method=='cash'), label=LxERP.t8('cash')) %]
     </td>
   </tr>
 [%- END %]
 [%- BLOCK customer %]
   <tr>
     <th align=right nowrap>[% 'Customer' | $T8 %]</th>
-    <td colspan=3>[% L.customer_vendor_picker('customer_id', '', type='customer') %]</td>
+    <td colspan=3>[% P.customer_vendor.picker('customer_id', '', type='customer') %]</td>
   </tr>
 [%- END %]
 [%- BLOCK projectnumber %]
   <tr>
     <th align=right nowrap>[% 'Project' | $T8 %]</th>
-    <td colspan=3>[% L.select_tag('project_id', ALL_PROJECTS, title_key = 'projectnumber', with_empty = 1) %]</td>
+    <td colspan=3>[% P.project.picker('project_id', '', active="both", valid="both") %]</td>
   </tr>
 [%- END %]
 
 <h1>[% title %]</h1>
 
-<form method=post action='[% script %]'>
+<form method='post' action='[% script %]' id='form'>
 
 <input type=hidden name=title value="[% title %]">
 
   </tr>
 [%- END %]
 
+
 [%- IF is_bwa %]
 [%- PROCESS projectnumber %]
   <input type=hidden name=nextsub value=generate_bwa>
   </tr>
 [%- END %]
 
+
+[%- IF is_erfolgsrechnung %]
+  <input type=hidden name=nextsub value=generate_erfolgsrechnung>
+</table>
+<table>
+[%- PROCESS customized_report %]
+[%- END %]
+
+
 [%- IF is_balance_sheet %]
   <input type=hidden name=nextsub value=generate_balance_sheet>
   <tr>
     </td>
   </tr>
   <input type=hidden name=type value=statement>
-  <input type=hidden name=format value=html>
+  [% L.hidden_tag("format", format) %]
   <input type=hidden name=media value=screen>
 
   <input type=hidden name=nextsub value='[% nextsub %]'>
   </tr>
 [%- END %]
 </table>
+</form>
 
-<hr size=3 noshade>
-<br>
-<input type=submit class=submit name=action value="[% 'Continue' | $T8 %]">
+<script type="text/javascript">
+function set_from_to(duetyp, year) {
+  var date = {
+    1:  [ 1,  1, 1,  31 ],
+    2:  [ 2,  1, 2,  new Date(year, 1, 29).getMonth() == 1 ? 29 : 28 ],
+    3:  [ 3,  1, 3,  31 ],
+    4:  [ 4,  1, 4,  30 ],
+    5:  [ 5,  1, 5,  31 ],
+    6:  [ 6,  1, 6,  30 ],
+    7:  [ 7,  1, 7,  31 ],
+    8:  [ 8,  1, 8,  31 ],
+    9:  [ 9,  1, 9,  30 ],
+    10: [ 10, 1, 10, 31 ],
+    11: [ 11, 1, 11, 30 ],
+    12: [ 12, 1, 12, 31 ],
+    13: [  1, 1, 12, 31 ],
+    'A': [ 1,  1, 3,  31 ],
+    'B': [ 4,  1, 6,  30 ],
+    'C': [ 7,  1, 9,  30 ],
+    'D': [ 10, 1, 12, 31 ]
+  }[duetyp];
 
-</form>
+  $('#fromdate').val(kivi.format_date(new Date(year, date[0]-1, date[1])));
+  $('#todate').val(kivi.format_date(new Date(year, date[2]-1, date[3])));
+
+  return true;
+}
+</script>
index 4b25022..c8cc07c 100644 (file)
@@ -13,7 +13,7 @@
 
  <p><div class="listtop">[% title %]</div></p>
 
- <form action="sepa.pl" method="post">
+ <form action="sepa.pl" method="post" id="form">
   <p>
    [%- IF is_vendor %]
     [% 'Please select the source bank account for the transfers:' | $T8 %]
@@ -55,7 +55,7 @@
      <tr class="listrow[% IF !invoice.vc_bank_info_ok && invoice.checked %]_error[% END %]">
       <td align="center">
        [%- IF invoice.vc_bank_info_ok %]
-        <input type="checkbox" name="bank_transfers[].selected" value="1"[% IF invoice.checked %] checked[% END %]>
+        [% L.checkbox_tag("ids[]", value=invoice.id, checked=invoice.checked) %]
        [%- END %]
       </td>
       <td>
       <td align="right">[% invoice.transdate %]</td>
       <td align="right">[% invoice.duedate %]</td>
       <td>
+    [% IF INSTANCE_CONF.get_sepa_reference_add_vc_vc_id %]
+       [%- SET reference = invoice.reference_prefix _ invoice.invnumber _ invoice.reference_prefix_vc _ invoice.vc_vc_id %]
+    [% ELSE %]
        [%- SET reference = invoice.reference_prefix _ invoice.invnumber %]
-       <input name="bank_transfers[].reference" value="[% HTML.escape(reference.substr(0, 140)) %]" maxlength="140" size="20">
+    [% END %]
+       <input name="bank_transfers[].reference" value="[% HTML.escape(reference.substr(0, 140)) %]" maxlength="140" size="30">
       </td>
       <td align="right">
        <input id=[% loop.count %] name="bank_transfers[].amount" id="amount_[% loop.count %]" value="[% LxERP.format_amount(invoice.invoice_amount_suggestion, 2) %]" style="text-align: right" size="12">
    </p>
   [%- END %]
 
-  <p>
-   <input type="submit" class="submit" name="action_bank_transfer_create" value="[% 'Step 2' | $T8 %]">
-  </p>
-
-  <input type="hidden" name="action" value="dispatcher">
   <input type="hidden" name="vc" value="[%- HTML.escape(vc) %]">
  </form>
 
  <script type="text/javascript">
   <!--
     $(function() {
-      $("#select_all").checkall('INPUT[name="bank_transfers[].selected"]');
+      $("#select_all").checkall('INPUT[name="ids[]"]');
     });
     -->
 
index 35b2fb9..0eab6ff 100644 (file)
@@ -17,7 +17,7 @@
 
  <p><div class="listtop">[% title %]</div></p>
 
- <form action="sepa.pl" method="post">
+ <form action="sepa.pl" method="post" id="form">
   <p>1.
    [%- IF is_vendor %]
     [% 'Please select the source bank account for the transfers:' | $T8 %]
       </td>
       <td align="left" [%- IF bank_transfer.within_skonto_period %]style="background-color: LightGreen"[%- END %]>[%- IF bank_transfer.skonto_amount %] [% LxERP.format_amount(bank_transfer.percent_skonto, 2) %] % = [% LxERP.format_amount(bank_transfer.skonto_amount, 2) %] € [% 'until' | $T8 %] [% bank_transfer.skonto_date %] [% END %]</td>
       <td nowrap>
-        [% L.date_tag('bank_transfers[].requested_execution_date', bank_transfer.requested_execution_date) %]
+        [% L.date_tag('bank_transfers[].requested_execution_date', bank_transfer.recommended_execution_date) %]
       </td>
      </tr>
     [%- END %]
    [% 'Sum open amount' | $T8 %]: [% LxERP.format_amount(total_trans, -2) %]
   </p>
 
-  <p>
-   [%- IF is_vendor %]
-    <input type="submit" class="submit" name="action_bank_transfer_create" value="[% 'Create bank transfer' | $T8 %]">
-   [%- ELSE %]
-    <input type="submit" class="submit" name="action_bank_transfer_create" value="[% 'Create bank collection' | $T8 %]">
-   [%- END %]
-  </p>
-
-  <input type="hidden" name="action" value="dispatcher">
   <input type="hidden" name="vc" value="[%- HTML.escape(vc) %]">
   <input type="hidden" name="confirmation" value="1">
  </form>
 
  <script type="text/javascript">
-
-    // function toggle(id) {
-    //   $('#skonto_' + id).change(function() {
-    //     if($('#skonto_' + id).prop("checked")) {
-    //         $('#' + id).val( $('#amount_less_skonto_' + id).val() );
-    //     } else {
-    //         $('#' + id).val( $('#invoice_open_amount_' + id).val() );
-    //     }
-    //   });
-    // };
-
 $( ".type_target" ).change(function() {
   type_id = $(this).attr('id');
   var id = type_id.match(/\d*$/);
index 37f1575..e171ef2 100644 (file)
 [%- END %]
 <h1>[% title %]: [% HTML.escape(export.ids.join(', ')) %]</h1>
 
- <form action="sepa.pl" method="post">
-  <input type="hidden" name="action" value="dispatcher">
-
+ <form action="sepa.pl" method="post" id="form">
   <p>
    <table>
     <tr>
-     [%- IF show_post_payments_button %]
-      <th class="listheading" align="center"><input type="checkbox" id="select_all"></th>
-     [%- END %]
+     <th class="listheading" align="center"><input type="checkbox" id="select_all"></th>
      <th class="listheading">[% 'Invoice' | $T8 %]</th>
      <th class="listheading">[%- IF is_vendor %][% 'Vendor' | $T8 %][%- ELSE %][%- LxERP.t8('Customer') %][%- END %]</th>
      [%- IF is_vendor %]
@@ -42,7 +38,7 @@
      <th class="listheading" align="right">[% 'Execution date' | $T8 %]</th>
     </tr>
     <tr>
-     <th class="listheading" colspan="[% IF show_post_payments_button %]3[% ELSE %]2[% END %]">&nbsp;</th>
+     <th class="listheading" colspan="3">&nbsp;</th>
      <th class="listheading">[% 'IBAN' | $T8 %]</th>
      <th class="listheading">[% 'BIC' | $T8 %]</th>
      <th class="listheading">[% 'IBAN' | $T8 %]</th>
         [% L.date_tag('set_all_execution_date', '', onchange='set_all_execution_date_fields(this);') %]
       </th>
      [%- ELSE %]
-      <th class="listheading" colspan="4">&nbsp;</th>
+      <th class="listheading" colspan="[% IF vc == 'customer' %]7[% ELSE %]6[% END %]">&nbsp;</th>
      [%- END %]
     </tr>
 
     [%- FOREACH item = export.items %]
-     <tr class="listrow[% loop.count % 2 %]">
-      [%- IF show_post_payments_button %]
-       <input type="hidden" name="items[+].id" value="[% HTML.escape(item.id) %]">
-       <input type="hidden" name="items[].sepa_export_id" value="[% HTML.escape(item.sepa_export_id) %]">
-       <td align="center">
-        [%- UNLESS item.executed %]
-        <input type="checkbox" name="items[].selected" value="1">
-        [%- END %]
-       </td>
-      [%- END %]
+     <tr class="listrow">
+      <td align="center">
+       [%- IF (show_post_payments_button && !item.executed) || (!show_post_payments_button && item.executed) %]
+        [% L.hidden_tag("items[+].id", item.id) %]
+        [% L.hidden_tag("items[].sepa_export_id", item.sepa_export_id) %]
+        [% L.checkbox_tag("ids[]", value=item.id) %]
+       [%- END %]
+      </td>
       <td>
        <a href="[% IF item.invoice %][% iris %][% ELSE %][% arap %][% END %].pl?action=edit&type=invoice&id=[% IF is_vendor %][% HTML.url(item.ap_id) %][% ELSE %][% HTML.url(item.ar_id) %][% END %]">[% HTML.escape(item.invnumber) %]</a>
       </td>
    </table>
   </p>
 
-  <p><hr></p>
-
-  [%- IF show_post_payments_button %]
-  <p>
-   <input type="submit" class="submit" name="action_bank_transfer_post_payments" value="[% 'Post payments' | $T8 %]">
-  </p>
-
   <script type="text/javascript">
    <!--
     function set_all_execution_date_fields(input) {
     }
 
     $(function() {
-      $("#select_all").checkall('INPUT[name="items[].selected"]');
+      $("#select_all").checkall('INPUT[name="ids[]"]');
     });
      -->
   </script>
 
-  [%- ELSE %]
-  <p>
-   <input type="submit" class="submit" name="action_bank_transfer_payment_list_as_pdf" value="[% 'Payment list as PDF' | $T8 %]">
-  </p>
-
-   [%- FOREACH item = export.items %]
-    [%- IF item.executed %]
-     <input type="hidden" name="items[+].id" value="[% HTML.escape(item.id) %]">
-     <input type="hidden" name="items[].export_id" value="[% HTML.escape(item.export_id) %]">
-    [%- END %]
-   [%- END %]
-  [%- END %]
-
      <input type="hidden" name="vc" value="[% HTML.escape(vc) %]">
  </form>
index 20db365..c623d67 100644 (file)
@@ -2,20 +2,13 @@
 [% USE HTML %]
 
 [%- IF show_buttons %]
- <input type="hidden" name="action" value="dispatcher">
  <input type="hidden" name="mode" value="multi">
  <input type="hidden" name="vc" value="[%- HTML.escape(vc) %]">
 
- <p>
-  <input type="submit" class="submit" name="action_bank_transfer_download_sepa_xml" value="[% 'SEPA XML download' | $T8 %]">
-  <input type="submit" class="submit" name="action_bank_transfer_edit" value="[% 'Post payments' | $T8 %]">
-  <input type="submit" class="submit" name="action_bank_transfer_mark_as_closed_step1" value="[% 'Mark as closed' | $T8 %]">
- </p>
-
  <script type="text/javascript">
   <!--
     $(function() {
-      $("#select_all").checkall('INPUT[name="exports[].selected"]');
+      $("#select_all").checkall('INPUT[name="ids[]"]');
     });
     -->
  </script>
index f17b456..6dd43d3 100644 (file)
@@ -1 +1 @@
-<form action="sepa.pl" method="post">
+<form action="sepa.pl" method="post" id="form">
diff --git a/templates/webpages/sepa/bank_transfer_mark_as_closed_step1.html b/templates/webpages/sepa/bank_transfer_mark_as_closed_step1.html
deleted file mode 100644 (file)
index f1c0498..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-[%- USE T8 %]
-[% USE HTML %]
-<h1>[% title %]</h1>
-
- <form action="sepa.pl" method="post">
-  <p>
-   [%- IF vc == 'vendor' %]
-    [%- 'Do you really want to close the following SEPA exports? No payment will be recorded for bank transfers that haven\'t been marked as executed yet.' | $T8 %]
-   [%- ELSE %]
-    [%- 'Do you really want to close the following SEPA exports? No payment will be recorded for bank collections that haven\'t been marked as executed yet.' | $T8 %]
-   [%- END %]
-  </p>
-
-  <p>
-   [% 'SEPA exports:' | $T8 %]
-   [%- FOREACH id = OPEN_EXPORT_IDS %]
-    [%- UNLESS loop.first %], [%- END %]
-    <input type="hidden" name="open_export_ids[]" value="[% HTML.escape(id) %]">
-    <a href="sepa.pl?action=bank_transfer_edit&id=[% HTML.url(id) %]&vc=[% HTML.url(vc) %]">[% HTML.escape(id) %]</a>
-   [%- END %]
-  </p>
-
-  <p>
-   <input type="submit" class="submit" name="action_bank_transfer_mark_as_closed_step2" value="[% 'Mark as closed' | $T8 %]">
-   <input type="button" class="submit" value="[% 'Back' | $T8 %]" onclick="history.back()">
-  </p>
-
-  <input type="hidden" name="action" value="dispatcher">
-  <input type="hidden" name="vc" value="[%- HTML.escape(vc) %]">
- </form>
-
index e9fb9f6..bd832dc 100644 (file)
@@ -4,7 +4,7 @@
 [%- USE L %]
 <h1>[% title %]</h1>
 
- <form action="sepa.pl" method="post">
+ <form action="sepa.pl" method="post" id="form">
   <p>
    <table>
     <tr>
@@ -83,9 +83,5 @@
    </table>
   </p>
 
-  <p>
-   <input type="hidden" name="action" value="dispatcher">
    <input type="hidden" name="vc" value="[%- HTML.escape(vc) %]">
-   <input type="submit" class="submit" name="action_bank_transfer_list" value="[% 'Continue' | $T8 %]">
-  </p>
  </form>
diff --git a/templates/webpages/shop_order/_filter.html b/templates/webpages/shop_order/_filter.html
new file mode 100644 (file)
index 0000000..b07d7f7
--- /dev/null
@@ -0,0 +1,38 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE LxERP %]
+[%- USE HTML %]
+ <form method="post" action="controller.pl" name="shop_orders" id="shoporders">
+ <table id='filter_table'>
+
+    <tr>
+     <th align="right">[% 'Shop' | $T8 %]</th>
+     <td>[% L.select_tag('filter.shop_id:eq_ignore_empty', SELF.shops, value_key = 'value', title_key = 'title', default=0) %]</td>
+    </tr>
+
+    <tr>
+     <th align="right">[% 'Status' | $T8 %]</th>
+     <td>[% L.select_tag('filter.transferred:eq_ignore_empty', SELF.transferred, value_key = 'value', title_key = 'title', default=0) %]</td>
+    </tr>
+
+
+    <tr>
+     <th align="right">[% 'from' | $T8 %]</th>
+     <td>[% L.date_tag('filter.order_date:date::ge', FORM.filter.order_date_date__ge) %]</td>
+    </tr>
+
+    <tr>
+     <th align="right">[% 'to' | $T8 %]</th>
+     <td>[% L.date_tag('filter.order_date:date::le', FORM.filter.order_date_date__le) %]</td>
+    </tr>
+
+    <tr>
+      <th align="right">[% 'Obsolete' | $T8 %]</th>
+      <td>[% L.yes_no_tag('filter.obsolete', FORM.filter.obsolete, default='0', with_empty=1, empty_title='---') %]</td>
+    </tr>
+
+ </table>
+
+
+<a href='#' onClick='javascript:$("#filter_table input").val("");$("#filter_table input[type=checkbox]").prop("checked", 0);'>[% 'Reset' | $T8 %]</a>
+<br>
diff --git a/templates/webpages/shop_order/_get_one.html b/templates/webpages/shop_order/_get_one.html
new file mode 100644 (file)
index 0000000..6a368d3
--- /dev/null
@@ -0,0 +1,18 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+[% USE Dumper %]
+[% L.stylesheet_tag('webshop') %]
+[%- INCLUDE 'common/flash.html' %]
+<form id="get_one_order_form" action="controller.pl" method="post" style="padding-left:1em;">
+ <table>
+    <tr>
+     <th align="right">[% 'Shop' | $T8 %]</th>
+     <td>[% L.select_tag('shop_id', SELF.shops, value_key = 'value', title_key = 'title', default=1) %]</td>
+    </tr>
+    <tr>
+     <th align="right">[% 'Shop ordernumber' | $T8 %]</th>
+     <td>[% L.input_tag('shop_ordernumber', "") %]</td>
+    </tr>
+ </table>
+  [%  L.hidden_tag("action", "ShopOrder/dispatch") %]
+  [%  L.button_tag("kivi.ShopOrder.get_orders_one()", LxERP.t8('Fetch order')) %]
+</form>
diff --git a/templates/webpages/shop_order/_transfer_status.html b/templates/webpages/shop_order/_transfer_status.html
new file mode 100644 (file)
index 0000000..24b7bf9
--- /dev/null
@@ -0,0 +1,59 @@
+[%- USE LxERP -%][%- USE L -%][%- USE HTML -%]
+[% SET data = job.data_as_hash %]
+
+<h2>[% LxERP.t8("Watch status") %]</h2>
+
+[% L.hidden_tag('', job.id, id="smt_job_id") %]
+
+JOBID: [% job.id %] <p>
+ [% LxERP.t8("This status output will be refreshed every five seconds.") %]
+</p>
+<p>
+</p>
+<p>
+ [% L.link("#", LxERP.t8("Close window"), onclick="kivi.ShopOrder.processClose();") %]
+ <table>
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Current status:") %]</th>
+   <td valign="top">
+    [% IF !data.status %]
+     [% LxERP.t8("waiting for job to be started") %]
+    [% ELSIF data.status == 1 %]
+     [% LxERP.t8("Converting to deliveryorder") %]
+     [% ELSE %]
+     [% LxERP.t8("Done.") %]
+    [% END %]
+   </td>
+  </tr>
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Number of orders created:") %]</th>
+   <td valign="top">[% IF data.status > 0 %][% HTML.escape(data.num_order_created) %] / [% HTML.escape(data.shop_order_record_ids.size) %][% ELSE %]–[% END %]</td>
+  </tr>
+
+
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Errors during conversion:") %]</th>
+   <td valign="top">
+[% IF !data.status %]
+  –
+[% ELSIF !data.conversion_errors.size %]
+ [% LxERP.t8("No errors have occurred.") %]
+[% ELSE %]
+    <table>
+     <tr class="listheader">
+      <th>[% LxERP.t8("Shoporder") %]</th>
+      <th>[% LxERP.t8("Error") %]</th>
+     </tr>
+
+ [% FOREACH error = data.conversion_errors %]
+     <tr>
+      <td valign="top">[% HTML.escape(error.number) %]</td>
+      <td valign="top">[% FOREACH message = error.message %][% HTML.escape(message) %]<br>[% END %]</td>
+     </tr>
+ [% END %]
+    </table>
+[% END %]
+   </td>
+  </tr>
+ </table>
+</p>
diff --git a/templates/webpages/shop_order/list.html b/templates/webpages/shop_order/list.html
new file mode 100644 (file)
index 0000000..e8c1034
--- /dev/null
@@ -0,0 +1,205 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+
+[% L.stylesheet_tag('webshop') %]
+[%- INCLUDE 'common/flash.html' %]
+<h1>[% title %]<span style="float:right;">[% 'Number of data sets' | $T8 %]: [% SHOPORDERS.size %]</span></h1>
+[%- PROCESS 'shop_order/_filter.html' filter=SELF.models.filtered.laundered %]
+
+<hr>
+
+ <table id="shoplist" width="100%">
+  <thead>
+   <tr class="listheading">
+    <th>[% 'Shop' | $T8 %]</th>
+    <th>[% IF FORM.sort_by == 'order_date' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=order_date&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Shop orderdate' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=order_date&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Shop orderdate' | $T8 %]</a>
+    [% END %]
+    <br>
+    [% IF FORM.sort_by == 'itime' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=itime&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Importdate' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=itime&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Importdate' | $T8 %]</a>
+    [% END %]
+    </th>
+    <th>[% IF FORM.sort_by == 'shop_ordernumber' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=shop_ordernumber&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Shop ordernumber' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=shop_ordernumber&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Shop ordernumber' | $T8 %]</a>
+    [% END %]
+    </th>
+    <th>[% IF FORM.sort_by == 'shop_customer_number' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=shop_customer_number&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Shop customernumber' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=shop_customer_number&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Shop customernumber' | $T8 %]</a>
+    [% END %]
+    </th>
+    <th>[% 'Shop Customer Address' | $T8 %]<br>
+    [% IF FORM.sort_by == 'customer_lastname' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=customer_lastname&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Name' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>|
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=customer_lastname&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Name' | $T8 %]</a>|
+    [% END %]
+    [% IF FORM.sort_by == 'customer_zipcode' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=customer_zipcode&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Zip' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>|
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=customer_zipcode&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Zip' | $T8 %]</a>|
+    [% END %]
+    [% IF FORM.sort_by == 'customer_country' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=customer_country&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Country' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=customer_country&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Country' | $T8 %]</a>
+    [% END %]
+      </th>
+    <th>[% 'Shop Billing Address' | $T8 %]</br>
+    [% IF FORM.sort_by == 'billing_lastname' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=billing_lastname&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Name' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>|
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=billing_lastname&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Name' | $T8 %]</a>|
+    [% END %]
+    [% IF FORM.sort_by == 'billing_zipcode' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=billing_zipcode&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Zip' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>|
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=billing_zipcode&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Zip' | $T8 %]</a>|
+    [% END %]
+    [% IF FORM.sort_by == 'billing_country' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=billing_country&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Country' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=billing_country&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Country' | $T8 %]</a>
+    [% END %]
+      </th>
+    <th>[% 'Shop Delivery Address' | $T8 %]</br>
+    [% IF FORM.sort_by == 'delivery_lastname' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=delivery_lastname&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Name' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>|
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=delivery_lastname&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Name' | $T8 %]</a>|
+    [% END %]
+    [% IF FORM.sort_by == 'delivery_zipcode' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=delivery_zipcode&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Zip' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>|
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=delivery_zipcode&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Zip' | $T8 %]</a>|
+    [% END %]
+    [% IF FORM.sort_by == 'delivery_country' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=delivery_country&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Country' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=delivery_country&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Country' | $T8 %]</a>
+    [% END %]
+      </th>
+    <th>[% IF FORM.sort_by == 'shop_customer_comment' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=shop_customer_comment&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Notes' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=shop_customer_comment&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Notes' | $T8 %]</a>
+    [% END %]
+    </th>
+    <th>
+      [% IF FORM.sort_by == 'positions' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=positions&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Positions' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a><br>
+      [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=positions&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link"> [% 'Positions' | $T8 %]</a><br>
+      [% END %]
+      [% IF FORM.sort_by == 'amount' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=amount&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Amount' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a><br>
+      [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=amount&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link"> [% 'Amount' | $T8 %]</a><br>
+      [% END %]
+      [% IF FORM.sort_by == 'shipping_costs' %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=shipping_costs&sort_dir=[% 1 - FORM.sort_dir %]&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link">
+        [% 'Shippingcosts' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+      [% ELSE %]
+      <a href ="controller.pl?action=ShopOrder/list&filter.transferred:eq_ignore_empty=[% FORM.filter.transferred_eq_ignore_empty %]&sort_by=shipping_costs&sort_dir=0&filter.order_date:date::ge=[% FORM.filter.order_date_date__ge %]&filter.order_date:date::le=[% FORM.filter.order_date_date__le %]&filter.obsolete=[% FORM.filter.obsolete %]" class="sort_link"> [% 'Shippingcosts' | $T8 %]</a>
+      [% END %]
+    </th>
+    <th>[% 'Action' | $T8 %]<br>[% L.checkbox_tag('check_all') %]</th>
+   </tr>
+  </thead>
+ </form>
+ <form method="post" action="controller.pl" name="shop_orders_list" id="shoporderslist">
+  [% FOREACH shop_order = SHOPORDERS %]
+    [% IF shop_order.kivi_customer.id && shop_order.kivi_customer.order_lock == 0 && shop_order.open_invoices == 0 %] [% SET transferable = 1 %] [% SET transferable_class = 'class="shop_transferable"' %] [% ELSE %] [% SET transferable = 0 %] [% SET transferable_class = '' %][% END %]
+  <tr class="listrow">
+    <td>[% HTML.escape(shop_order.shop.description) %]</td>
+    <td>[% shop_order.order_date.to_kivitendo('precision' => 'minute') %]<br>[% shop_order.itime.to_kivitendo('precision' => 'minute') %]</td>
+    <td>[% HTML.escape(shop_order.shop_ordernumber) %]</td>
+    <td>[% HTML.escape(shop_order.shop_customer_number) %]</td>
+    <td>[% IF shop_order.customer_company %]<b>[% HTML.escape(shop_order.customer_company) %]</b><br>[% END %]
+      <b>[% HTML.escape(shop_order.customer_lastname) %],&nbsp;[% HTML.escape(shop_order.customer_firstname) %]</b>
+      <br>[% HTML.escape(shop_order.customer_street) %]
+      <br>[% HTML.escape(shop_order.customer_zipcode) %]&nbsp;[% HTML.escape(shop_order.customer_city) %]
+      <br>[% HTML.escape(shop_order.customer_country) %] </td>
+    <td [% transferable_class %]>[% IF shop_order.customer_company %]<b>[% HTML.escape(shop_order.customer_company) %]</b><br>[% END %]
+      <b>[% HTML.escape(shop_order.billing_lastname) %],&nbsp;[% HTML.escape(shop_order.billing_firstname) %]</b>
+      <br>[% HTML.escape(shop_order.billing_street) %]
+      <br>[% HTML.escape(shop_order.billing_zipcode) %]&nbsp;[% HTML.escape(shop_order.billing_city) %]
+      <br>[% HTML.escape(shop_order.billing_country) %]
+      <br>[% IF shop_order.open_invoices > 0 || shop_order.customer.order_lock == 1 %][% SET alertclass = 'class="shop_alert"' %][% ELSE %][% SET alertclass = '' %][% END %]<span [% alertclass %]>&nbsp;&nbsp;[% 'Customernumber' | $T8 %] [% HTML.escape(shop_order.kivi_customer.customernumber) %] -- [% 'Invoices' | $T8 %] [% shop_order.open_invoices %]&nbsp;&nbsp;</span></td>
+    [% IF (shop_order.delivery_lastname != shop_order.billing_lastname || shop_order.delivery_firstname != shop_order.billing_firstname || shop_order.delivery_street != shop_order.billing_street || shop_order.delivery_city != shop_order.billing_city) %] [% SET deliveryclass = 'class="shop_delivery"' %] [% ELSE %] [% SET deliveryclass = '' %] [% END %]
+    <td [% deliveryclass %]>[% IF shop_order.customer_company %]<b>[% HTML.escape(shop_order.customer_company) %]</b><br>[% END %]
+      <b>[% HTML.escape(shop_order.delivery_lastname) %],&nbsp;[% HTML.escape(shop_order.delivery_firstname) %]</b>
+      <br>[% HTML.escape(shop_order.delivery_street) %]
+      <br>[% HTML.escape(shop_order.delivery_zipcode) %]&nbsp;[% HTML.escape(shop_order.delivery_city) %]
+      <br>[% HTML.escape(shop_order.delivery_country) %] </td>
+    <td>[% HTML.escape(shop_order.shop_customer_comment) %]</td>
+    <td><span class="tooltipster-html" title="[% FOREACH item = shop_order.shop_order_items %] [% LxERP.format_amount(item.quantity,0) %] x [% item.partnumber %] [% item.description %] <br> [% END %]">[% shop_order.positions %]<br>[% shop_order.amount_as_number %]<br>[% shop_order.shipping_costs_as_number %]</td><span>
+    <td valign="middle">[% IF shop_order.transferred == 1 %]<a href="controller.pl?id=[% shop_order.id %]&action=ShopOrder/show">[% 'Show order' | $T8 %]<br>[% shop_order.transferred_date_as_date %]</a>
+        [% ELSE %]
+          [% IF transferable == 1 && shop_order.obsolete == 0 %]
+            [% L.checkbox_tag('id[]', checked = '1',  value=shop_order.id) %]<br>
+          [% END %]
+          [% IF shop_order.obsolete == 0 %]<a href="controller.pl?id=[% shop_order.id %]&action=ShopOrder/show">[% 'Create order' | $T8 %]</a></br></br>
+          <a href="controller.pl?import_id=[% shop_order.id %]&action=ShopOrder/delete_order">[% 'Delete shoporder' | $T8 %]</a>
+          [% ELSE %]
+          [% 'Obsolete' | $T8 %]<br><br>
+            <a href="controller.pl?id=[% shop_order.id %]&action=ShopOrder/show">[% 'Show order' | $T8 %]
+          [% END %]
+    </td>
+        [% END %]
+  </tr>
+  [% END %]
+ </table>
+ <hr>
+  <div id="status_mass_transfer" style="display: none;">
+    [%- INCLUDE 'shop_order/_transfer_status.html' %]
+  </div>
+ </form>
+ <div id="get_one" style="display:none;">
+   [% INCLUDE 'shop_order/_get_one.html' %]
+ </div>
+<script type="text/javascript">
+<!--
+
+$(function() {
+  $('#check_all').checkall('INPUT[name^="id"]');
+});
+-->
+</script>
diff --git a/templates/webpages/shop_order/show.html b/templates/webpages/shop_order/show.html
new file mode 100644 (file)
index 0000000..da045f1
--- /dev/null
@@ -0,0 +1,207 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+[% L.stylesheet_tag('webshop') %]
+[%- INCLUDE 'common/flash.html' %]
+<h1>[% title %]</h1>
+
+  <div class="shop_table shop_main">
+    <div class="shop_table-row">
+      <div class="shop_table-cell">
+      <form method="post" action="controller.pl" id="customer">[% L.hidden_tag('create_customer','customer') %][% L.hidden_tag('import_id', IMPORT.id) %]
+        <div class="shop_table shop_table_address">
+          <div class="shop_table-row listheading">
+            <div class="shop_table-cell">[% 'Shop Customer Address' | $T8 %]</div>
+            <div class="shop_table-cell"></div>
+          </div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Greeting' | $T8 %]</div><div class="shop_table-cell">[% HTML.escape(IMPORT.customer_greeting) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Firstname' | $T8 %]</div><div class="shop_table-cell">[% HTML.escape(IMPORT.customer_firstname) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Lastname' | $T8 %]</div><div class="shop_table-cell">[% HTML.escape(IMPORT.customer_lastname) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Company' | $T8 %]</div><div class="shop_table-cell">[% HTML.escape(IMPORT.customer_company) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Department' | $T8 %]</div><div class="shop_table-cell">[% HTML.escape(IMPORT.customer_department) %]</div></div>
+            [% SET customer = IMPORT.customer_firstname _ ' ' _ IMPORT.customer_lastname %]
+          <hr>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Greeting' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('customer_greeting', IMPORT.customer_greeting) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Customer' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('customer_name', customer) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Name 2' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('customer_company', IMPORT.customer_company) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Name 3' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('customer_department', IMPORT.customer_department) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Street' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('customer_street', IMPORT.customer_street) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Zipcode' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('customer_zipcode', IMPORT.customer_zipcode) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'City' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('customer_city', IMPORT.customer_city) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Country' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('customer_country', IMPORT.customer_country) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Phone' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('customer_phone', IMPORT.customer_phone) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Email' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('customer_email', IMPORT.customer_email) %]</div></div>
+          [% IF C_ADDRESS %]
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Customernumber' | $T8 %]</div><div class="shop_table-cell">[% C_ADDRESS.customernumber %]</div></div>
+          [% ELSE %]
+          <div>[% L.ajax_submit_tag("controller.pl?action=ShopOrder/apply_customer",  "#customer", LxERP.t8("Apply customer")) %]</div>
+          [% END %]
+        </div>
+      </form>
+      </div>
+      <div class="shop_table-cell">
+      <form method="post" action="controller.pl" id="billing">[% L.hidden_tag('create_customer','billing') %][% L.hidden_tag('import_id', IMPORT.id) %]
+        <div class="shop_table shop_table_address">
+          <div class="shop_table-row listheading">
+            <div class="shop_table-cell">[% 'Shop Billing Address' | $T8 %]</div>
+            <div class="shop_table-cell"></div>
+          </div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Greeting' | $T8 %]</div><div class="shop_table-cell">[% HTML.escape(IMPORT.billing_greeting) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Firstname' | $T8 %]</div><div class="shop_table-cell">[% HTML.escape(IMPORT.billing_firstname) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Lastname' | $T8 %]</div><div class="shop_table-cell">[% HTML.escape(IMPORT.billing_lastname) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Company' | $T8 %]</div><div class="shop_table-cell">[% HTML.escape(IMPORT.billing_company) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Department' | $T8 %]</div><div class="shop_table-cell">[% HTML.escape(IMPORT.billing_department) %]</div></div>
+            [% SET billing = IMPORT.billing_firstname _ ' ' _ IMPORT.billing_lastname %]
+          <hr>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Greeting' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('billing_greeting', IMPORT.billing_greeting) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Customer' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('billing_name', billing) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Name 2' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('billing_company', IMPORT.billing_company) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Name 3' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('billing_department', IMPORT.billing_department) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Street' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('billing_street', IMPORT.billing_street) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Zipcode' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('billing_zipcode', IMPORT.billing_zipcode) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'City' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('billing_city', IMPORT.billing_city) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Country' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('billing_country', IMPORT.billing_country) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Phone' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('billing_phone', IMPORT.billing_phone) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Email' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('billing_email', IMPORT.billing_email) %]</div></div>
+          [% IF B_ADDRESS %]
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Customernumber' | $T8 %]</div><div class="shop_table-cell">[% B_ADDRESS.customernumber %]</div></div>
+          [% ELSE %]
+          <div>[% L.ajax_submit_tag("controller.pl?action=ShopOrder/apply_customer",  "#billing", LxERP.t8("Apply customer")) %]</div>
+          [% END %]
+        </div>
+      </form>
+      </div>
+      <div class="shop_table-cell">
+      <form method="post" action="controller.pl" id="delivery">[% L.hidden_tag('create_customer','delivery') %][% L.hidden_tag('import_id', IMPORT.id) %]
+        <div class="shop_table shop_table_address">
+          <div class="shop_table-row listheading">
+            <div class="shop_table-cell">[% 'Shop Delivery Address' | $T8 %]</div>
+            <div class="shop_table-cell"></div>
+          </div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Greeting' | $T8 %]</div><div class="shop_table-cell">[% HTML.escape(IMPORT.delivery_greeting) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Firstname' | $T8 %]</div><div class="shop_table-cell">[% HTML.escape(IMPORT.delivery_firstname) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Lastname' | $T8 %]</div><div class="shop_table-cell">[% HTML.escape(IMPORT.delivery_lastname) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Company' | $T8 %]</div><div class="shop_table-cell">[% HTML.escape(IMPORT.delivery_company) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Department' | $T8 %]</div><div class="shop_table-cell">[% HTML.escape(IMPORT.delivery_department) %]</div></div>
+            [% SET delivery = IMPORT.delivery_firstname _ ' ' _ IMPORT.delivery_lastname %]
+          <hr>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Greeting' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('delivery_greeting', IMPORT.delivery_greeting) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Customer' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('delivery_name', delivery) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Name 2' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('delivery_company', IMPORT.delivery_company) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Name 3' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('delivery_department', IMPORT.delivery_department) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Street' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('delivery_street', IMPORT.delivery_street) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Zipcode' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('delivery_zipcode', IMPORT.delivery_zipcode) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'City' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('delivery_city', IMPORT.delivery_city) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Country' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('delivery_country', IMPORT.delivery_country) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Phone' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('delivery_phone', IMPORT.delivery_phone) %]</div></div>
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Email' | $T8 %]</div><div class="shop_table-cell">[% L.input_tag('delivery_email', IMPORT.delivery_email) %]</div></div>
+          [% IF D_ADDRESS %]
+          <div class="shop_table-row"><div class="shop_table-cell listheading">[% 'Customernumber' | $T8 %]</div><div class="shop_table-cell">[% D_ADDRESS.customernumber %]</div></div>
+          [% ELSE %]
+          <div>[% L.ajax_submit_tag("controller.pl?action=ShopOrder/apply_customer",  "#delivery", LxERP.t8("Apply customer")) %]</div>
+          [% END %]
+        </div>
+      </form>
+      </div>
+    </div>
+  </div>
+  <hr>
+  <table width="100%">
+    <tr>
+      <td width="35%">
+        <table>
+          <tr class="listheading">
+            <th colspan="2">[% 'Shop Headdata' | $T8 %]</th>
+          </tr>
+          <tr><td><b>[% 'Shop Ordernumber' | $T8 %]</b></td><td>[% HTML.escape(IMPORT.shop_ordernumber) %]</td></tr>
+          <tr><td><b>[% 'Shop Orderdate' | $T8 %]</b></td><td>[% HTML.escape(IMPORT.order_date.dmy('.')) _ ' ' _ HTML.escape(IMPORT.order_date.hms(':')) %]</td></tr>
+          <tr><td><b>[% 'Shop Host' | $T8 %]</b></td><td>[% HTML.escape(IMPORT.host) %]</td></tr>
+          <tr><td><b>[% 'Shop OrderIP' | $T8 %]</b></td><td>[% HTML.escape(IMPORT.remote_ip) %]</td></tr>
+          <tr><td><b>[% 'Shop Ordernotes' | $T8 %]</b></td><td>[% HTML.escape(IMPORT.shop_customer_comment) %]</td></tr>
+          <tr><td><b>[% 'Shop Orderamount' | $T8 %]</b></td><td>[% HTML.escape(IMPORT.amount_as_number) %]</td></tr>
+          <tr><td><b>[% 'Shipping costs' | $T8 %]</b></td><td>[% HTML.escape(IMPORT.shipping_costs_as_number) %]</td></tr>
+          <tr><td><b>[% 'Payment description' | $T8 %]</b></td><td>[% HTML.escape(IMPORT.payment_description) %]</td></tr>
+        </table>
+      </td>
+      <td style="padding-left: 20px; vertical-align: top;">
+        [% IF IMPORT.obsolete %]
+        <b>[% 'Shoporder deleted -- ' | $T8 %]</b><a href="controller.pl?action=ShopOrder/undelete_order&import_id=[% IMPORT.id %]">[% 'revert deleted' | $T8 %]</a>
+        [% ELSE %]
+        [% UNLESS IMPORT.transferred %]
+        [% IF PROPOSALS %]
+          <form method="post" action="controller.pl" id="create_order">
+            [% L.hidden_tag('import_id', IMPORT.id) %]
+            <div style="height: 125px; overflow:auto;">
+              <table>
+                <tr class="listheading">
+                  <th colspan="7">[% 'Customer Proposals' | $T8 %]</td>
+                </tr>
+                [% FOREACH prop = PROPOSALS %][% IF prop.order_lock %][% SET orderlock_class = 'style="background:rgba(232, 32, 23, 0.2);"' %][% ELSE %][% SET orderlock_class = '' %][% END %]
+                <tr class="listrow" [% orderlock_class %]>
+                  <td>[% IF !prop.order_lock %][% L.radio_button_tag('customer', value=prop.id) %][% END %]</td>
+                  <td><a href="controller.pl?action=CustomerVendor/edit&id=[% prop.id %]&db=customer&callback=[% HTML.url('controller.pl?action=ShopOrder/show&id=' _ IMPORT.id) %]">[% HTML.escape(prop.customernumber) %]</a></td>
+                  <td>[% IF !prop.notes == '' %]<span class="tooltipster-html" title="[% HTML.escape(prop.notes) %]"><span style="color:red;font-weight: bold;">[% HTML.escape(prop.name) %]</span>[% ELSE %][% HTML.escape(prop.name) %][% END %]</td>
+                  <td>[% HTML.escape(prop.street) %]</td>
+                  <td>[% HTML.escape(prop.zipcode) %]</td>
+                  <td>[% HTML.escape(prop.city) %]</td>
+                  <td>[% HTML.escape(prop.email) %]</td>
+                </tr>
+                [% END %]
+              </table>
+            </div>
+            <div id="transfer" style="float:left; display:none;">
+              [% # 'Customernumber: ' _ %]
+              [% L.ajax_submit_tag('controller.pl?action=ShopOrder/transfer', "#create_order", LxERP.t8('Create order')) %]
+            </div>
+            [% FOREACH prop = PROPOSALS %]
+            <div id="shop_update_customer_[% prop.id %]" class="div_hidden" style="display:none;">
+              [% L.ajax_submit_tag("controller.pl?action=ShopOrder/apply_customer&cv_id=" _ prop.id,  "#billing", LxERP.t8("Update customer using billing address")) %]
+            </div>
+            [% END %]
+          </form>
+        <a href="controller.pl?action=ShopOrder/delete_order&import_id=[% IMPORT.id %]">[% 'delete order' | $T8 %]</a>
+        [% END # PROPOSALS %]
+        [% ELSE %]
+        <div>
+          [% 'Transferred' | $T8 %]
+          <div id="recordlinks"></div>
+          <script type="text/javascript">
+            var url = 'controller.pl?action=RecordLinks/ajax_list&object_model=ShopOrder&object_id=[% IMPORT.id %]';
+            $('#recordlinks').load(url);
+          </script>
+        </div>
+        [% END %]
+        [% END %]
+      </td>
+    </tr>
+  </table>
+  <hr>
+  <div style="height: 250px; overflow:auto; margin:15px;">
+    <table width="99%">
+      <tr class="listheading">
+        <th>[% 'Position'          | $T8 %]</th>
+        <th>[% 'Partnumber'        | $T8 %]</th>
+        <th>[% 'Partdescriptipion' | $T8 %]</th>
+        <th>[% 'Qty'               | $T8 %]</th>
+        <th>[% 'Price'             | $T8 %]</th>
+        <th>[% 'Extended'          | $T8 %]</th>
+      </tr>
+      <tr class="listrow">
+      [% FOREACH pos = IMPORT.shop_order_items %]
+        <td>[% loop.count                                      %]</td>
+        <td>[% HTML.escape(pos.partnumber)                     %]</td>
+        <td>[% HTML.escape(pos.description)                    %]</td>
+        <td>[% pos.quantity_as_number                          %]</td>
+        <td>[% pos.price_as_number                             %]</td>
+        <td>[% LxERP.format_amount(pos.price * pos.quantity,2) %]</td>
+      </tr>
+      [% END %]
+    </table>
+  </div>
+  <hr>
+<script type="text/javascript">
+$("input[type=radio]").change(function(){
+      $('.div_hidden').css("display", 'none');
+      var cv_id = $("input[type=radio][id="+ this.id + "]").val();
+      $('#shop_update_customer_'+ cv_id).css("display", 'block');
+      $('#transfer').css("display", 'block');
+});
+</script>
diff --git a/templates/webpages/shop_part/_filter.html b/templates/webpages/shop_part/_filter.html
new file mode 100644 (file)
index 0000000..bdf2908
--- /dev/null
@@ -0,0 +1,26 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE LxERP %]
+[%- USE HTML %]
+<form action='controller.pl' metdod='post' id="shop_part_filter">
+  [% L.hidden_tag('filter.shop.obsolete', 0) %]
+ <table id='filter_table'>
+    <tr>
+     <td>[% 'Shop' | $T8 %]</td>
+     <td>[% L.select_tag('filter.shop_id:eq_ignore_empty', SELF.shops, value_key = 'value', title_key = 'title', default=0) %]</td>
+    </tr>
+    <tr>
+     <td>[% 'Part marked as "Shop part"' | $T8 %]
+     <td>[% L.yes_no_tag('filter.part.shop', FORM.filter.part.shop, default='1', with_empty=1, empty_title='---') %]</td>
+    </tr>
+ </table>
+
+ <p>
+  <a href='#' onClick='javascript:$("#filter_table input").val("");$("#filter_table input[type=checkbox]").prop("checked", 0);'>[% 'Reset' | $T8 %]</a>
+  <br>
+ </p>
+ <p>
+   [% L.hidden_tag('action', 'ShopPart/dispatch') %]
+   [% L.submit_tag('action_list_articles',LxERP.t8('renew')) %]
+ </p>
+</form>
diff --git a/templates/webpages/shop_part/_list_articles.html b/templates/webpages/shop_part/_list_articles.html
new file mode 100644 (file)
index 0000000..34d4045
--- /dev/null
@@ -0,0 +1,134 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+
+<h1>[% title %]</h1>
+[%- PROCESS 'shop_part/_filter.html' filter=SELF.models.filtered.laundered %]
+<hr>
+<form method="post" action="controller.pl" name="shop_parts" id="shopparts">
+  <div class="data_count">[% 'Number of Data: ' | $T8 %] [% SHOP_PARTS.size %]</div>
+  <table id="shoplist" width="100%" >
+    <thead>
+      <tr class="listheading">
+      <th>[% L.checkbox_tag('check_all') %]</th>
+    <th>[% IF FORM.sort_by == 'shop.description' %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=shop.description&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
+        [% 'Shop Host/Connector' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=shop.description&sort_dir=0" class="sort_link">
+        [% 'Shop Host/Connector' | $T8 %]</a>
+    [% END %]
+    </th>
+    <th>[% IF FORM.sort_by == 'part.partnumber' %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=part.partnumber&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
+        [% 'Partnumber' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=part.partnumber&sort_dir=0" class="sort_link">
+        [% 'Partnumber' | $T8 %]</a>
+    [% END %]
+    </th>
+    <th>[% IF FORM.sort_by == 'part.description' %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=part.description&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
+        [% 'Description' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=part.description&sort_dir=0" class="sort_link">
+        [% 'Description' | $T8 %]</a>
+    [% END %]
+    </th>
+      <th>[% 'Info' | $T8 %]</th>
+      <th>[% 'Active' | $T8 %]</th>
+      <th>[% 'Price source' | $T8 %]</th>
+      <th>[% 'Price' | $T8 %]</th>
+    <th>[% IF FORM.sort_by == 'part.onhand' %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=part.onhand&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
+        [% 'Stock Local/Shop' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=part.onhand&sort_dir=0" class="sort_link">
+        [% 'Stock Local/Shop' | $T8 %]</a>
+    [% END %]
+    </th>
+      <th>[% 'Last update' | $T8 %]</th>
+      <th>[% 'Images' | $T8 %]</th>
+    <th>[% IF FORM.sort_by == 'part.partsgroup_id' %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=part.partsgroup_id&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
+        [% 'Category' %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=part.partsgroup_id&sort_dir=0" class="sort_link">
+        [% 'Category' | $T8 %]</a>
+    [% END %]
+    </th>
+    </tr>
+  </thead>
+  [%- FOREACH shop_part = SHOP_PARTS %]
+  [%- # IF shop_part.shop.obsolete %]
+    <tr class="listrow">
+      <td>[% L.checkbox_tag('shop_parts_ids[]', checked=0, value=shop_part.id) %]</td>
+      <td>[% HTML.escape( shop_part.shop.description ) %]/[% HTML.escape( shop_part.shop.connector ) %]</td>
+      <td>[% HTML.escape( shop_part.part.partnumber ) %]</td>
+      <td><a href="controller.pl?part.id=[% shop_part.part.id %]&action=Part/edit&callback=[% HTML.url('controller.pl?action=ShopPart/list_articles') %]#shop_variables">[% HTML.escape( shop_part.part.description ) %]</a></td>
+      <td>
+        [% IF shop_part.shop_description %]
+          [% 'Info' | $T8 %]
+        [% ELSE %]
+          [% 'No Shopdescription' | $T8 %]
+        [% END %]
+      </td>
+      <td style="vertical-align:middle;text-align:center;">
+        [% IF shop_part.active %]
+        <div id="toogle_[% shop_part.id %]" style="background-image:url(image/gruener_punkt.gif);background-repeat:no-repeat;witdh:15px;height:15px;">&nbsp; </div>
+        [% ELSE %]
+        <div id="toogle_[% shop_part.id %]" style="background-image:url(image/roter_punkt.gif);background-repeat:no-repeat;witdh:15px;height:15px;">&nbsp; </div>
+        [% END %]
+      </td>
+      <td>[% L.html_tag('span',LxERP.t8(), id => 'active_price_source_' _ shop_part.id) %] </td>
+      <td>[% L.html_tag('span','Price', id => 'price_' _ shop_part.id) %]</td>
+      <td>[% L.html_tag('span','Stock', id => 'stock_' _ shop_part.id) %]</td>
+      <td>[% L.html_tag('span', shop_part.last_update.to_kivitendo('precision' => 'minute'), id => 'shop_part_last_update_' _ shop_part.id ) %]</td>
+      <td>
+        [% IF shop_part.images %]
+          [% shop_part.images %]
+        [% ELSE %]
+          [% 'No Shopimages' | $T8 %]
+        [% END %]
+      </td>
+      <td>
+        [% IF shop_part.shop_category %]
+          [% IF shop_part.shop_category.1.size > 1%]
+            [% FOREACH cat = shop_part.shop_category %]
+              [% HTML.escape(cat.1) %]<br>
+            [% END %]
+          [% ELSE %]
+            [% HTML.escape(shop_part.shop_category.1) %]<br>
+          [% END %]
+        [% END %]
+      </td>
+    <script type="text/javascript">
+      $(function() {
+         kivi.ShopPart.update_price_n_price_source([% shop_part.id %],'[% shop_part.active_price_source %]');
+         kivi.ShopPart.update_stock([% shop_part.id %]);
+      });
+    </script>
+    </tr>
+    [%- # END %]
+  [%- END %]
+</table>
+
+  <hr>
+  <div>
+    [% L.radio_button_tag('upload_todo', value='all', label= LxERP.t8('All Data')) %]
+    [% L.radio_button_tag('upload_todo', value='price', label= LxERP.t8('Only Price')) %]
+    [% L.radio_button_tag('upload_todo', value='stock', label= LxERP.t8('Only Stock')) %]
+    [% L.radio_button_tag('upload_todo', value='price_stock', checked=1, label= LxERP.t8('Price and Stock')) %]
+    [% L.button_tag("kivi.ShopPart.setup();", LxERP.t8("Upload all marked"), id="mass_transfer") %]
+  </div>
+  <div id="status_mass_upload" style="display: none;">
+    [%- INCLUDE 'shop_part/_upload_status.html' %]
+  </div>
+</form>
+<hr>
+<script type="text/javascript">
+<!--
+
+$(function() {
+  $('#check_all').checkall('INPUT[name^="shop_parts_ids"]');
+});
+-->
+</script>
diff --git a/templates/webpages/shop_part/_list_images.html b/templates/webpages/shop_part/_list_images.html
new file mode 100644 (file)
index 0000000..2fa6e88
--- /dev/null
@@ -0,0 +1,33 @@
+[%- USE HTML %][%- USE L -%][%- USE P -%][%- USE LxERP -%]
+[%- USE T8 %][% USE Base64 %]
+<table width="100%" id="images_list">
+  <thead>
+    <tr class="listheading">
+      <th width="10px"><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]" class="dragdrop"></th>
+      <th width="70px"></th>
+      <th>[% 'Title'          | $T8 %]</th>
+      <th>[% 'Description'    | $T8 %]</th>
+      <th>[% 'Filename'       | $T8 %]</th>
+      <th>[% 'Orig. Size w/h' | $T8 %]</th>
+      <th>[% 'Action'         | $T8 %]</th>
+    </tr>
+  </thead>
+  <tbody>
+   [%-  FOREACH img = IMAGES %]
+    <tr class="listrow" id="image_id_[%  img.id %]">
+      <td><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]" class="dragdrop"></td>
+      <td width="70px"><img src="data:[%  img.thumbnail_content_type %];base64,[%  img.thumbnail_content.encode_base64 %]" alt="[%  img.file.title %]"></td>
+      <td>[% HTML.escape(img.file.title)       %]</td>
+      <td>[% HTML.escape(img.file.description) %]</td>
+      <td>[% HTML.escape(img.file.file_name)   %]</td>
+      <td>[% HTML.escape(img.org_file_width) _  ' x ' _ HTML.escape(img.org_file_height) %]</td>
+      <td>[% L.button_tag("kivi.File.delete_file(" _ img.file_id _ ", 'ShopPart/ajax_delete_file')", LxERP.t8('Delete'), confirm=LxERP.t8("Are you sure?")) %]</td>
+    </tr>
+   [%  END %]
+  </tbody>
+</table>
+
+[% L.sortable_element('#images_list tbody', url=SELF.url_for(action='reorder'), with='image_id') %]
+<p>
+[% L.button_tag("kivi.ShopPart.imageUpload(" _ FORM.id _ ",'shop_image','image', '',0);", LxERP.t8('File upload') ) %]
+</p>
diff --git a/templates/webpages/shop_part/_upload_status.html b/templates/webpages/shop_part/_upload_status.html
new file mode 100644 (file)
index 0000000..2209a20
--- /dev/null
@@ -0,0 +1,51 @@
+[%- USE LxERP -%][%- USE L -%][%- USE HTML -%]
+[%- USE Dumper -%]
+[% SET data = job.data_as_hash %]
+
+<h2>[% LxERP.t8("Watch status") %]</h2>
+
+[% L.hidden_tag('', job.id, id="smu_job_id") %]
+
+JOBID: [% job.id %] <p>
+ [% LxERP.t8("This status output will be refreshed every five seconds.") %]
+</p>
+<p>
+ <table>
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Current status:") %]</th>
+   <td valign="top">
+    [% IF !data.status %]
+     [% LxERP.t8("waiting for job to be started") %]
+    [% ELSIF data.status == 1 %]
+     [% LxERP.t8("Uploading Data") %]
+    [% ELSE %]
+     [% LxERP.t8("Done.") %]
+    [% END %]
+   </td>
+  </tr>
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Number of data uploaded:") %]</th>
+   <td valign="top">[% IF data.status > 0 %][% HTML.escape(data.num_uploaded) %] / [% HTML.escape(data.shop_part_record_ids.size) %][% ELSE %]–[% END %]</td>
+  </tr>
+
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Conversion:") %]</th>
+   <td valign="top">
+  –
+    <table>
+     <tr class="listheader">
+      <th>[% LxERP.t8("Part")       %]</th>
+      <th>[% LxERP.t8("Partnumber") %]</th>
+      <th>[% LxERP.t8("Message")    %]</th>
+     </tr>
+
+ [% FOREACH message = data.conversion %]
+     <tr>
+      <td valign="top">[% HTML.escape(message.id)      %]</td>
+      <td valign="top">[% HTML.escape(message.number)  %]</td>
+      <td valign="top">[% HTML.escape(message.message) %]</td>
+     </tr>
+ [% END %]
+    </table>
+ </table>
+</p>
diff --git a/templates/webpages/shop_part/categories.html b/templates/webpages/shop_part/categories.html
new file mode 100644 (file)
index 0000000..55e6628
--- /dev/null
@@ -0,0 +1,35 @@
+[%- USE HTML %]
+[%- USE T8 %]
+[%- USE L -%]
+[%- USE P -%]
+[%- USE LxERP -%]
+[%- USE Dumper -%]
+
+[%  LxERP.t8("Part") %]: [% HTML.escape(SELF.shop_part.part.displayable_name) %]<br>
+[%  LxERP.t8("Shop") %]: [% HTML.escape(SELF.shop_part.shop.description) %]<br>
+
+<form action="controller.pl" method="post">
+  [% BLOCK recurse %]
+      [% FOREACH categorie = categories_array %]
+        <ul>
+          <li>
+          [% checked = '' %]
+          [% FOREACH cat_row = SELF.shop_part.shop_category %]
+            [% IF (cat_row.0 == categorie.id) || (SELF.shop_part.shop.connector == 'shopware6' && cat_row == categorie.id) %]
+              [% checked = 'checked' %]
+            [% END %]
+          [% END %]
+            [% L.checkbox_tag('categories[]',value=categorie.id, checked=checked) %][% HTML.escape(categorie.name) %][% L.hidden_tag("cat_id_" _ categorie.id, categorie.name) %]
+          </li>
+          [% IF categorie.children.size %]
+            [% INCLUDE recurse categories_array=categorie.children %]
+          [% END %]
+        </ul>
+    [% END %]
+  [% END %]
+  <div><h2>[% LxERP.t8("Shopcategories") %]</h2>
+    [% # Dumper.dump_html( CATEGORIES ) %]
+    [% INCLUDE recurse categories_array=CATEGORIES %]
+  </div>
+    [% L.button_tag("kivi.ShopPart.save_categories(" _ SELF.shop_part.id _", " _ SELF.shop_part.shop.id _")", LxERP.t8("Save"))  %]</td>
+</form>
diff --git a/templates/webpages/shop_part/edit.html b/templates/webpages/shop_part/edit.html
new file mode 100644 (file)
index 0000000..5379532
--- /dev/null
@@ -0,0 +1,73 @@
+[%- USE HTML %]
+[%- USE T8 %]
+[%- USE L -%]
+[%- USE P -%]
+[%- USE LxERP -%]
+
+<p>
+[% LxERP.t8("Part") %]: [% HTML.escape(SELF.shop_part.part.displayable_name) %]<br>
+[% LxERP.t8("Shop") %]: [% HTML.escape(SELF.shop_part.shop.description) %]
+<p>
+<form action="controller.pl" method="post">
+  <div>
+    [% IF SELF.shop_part.id %]
+    [%- L.hidden_tag("shop_part.id", SELF.shop_part.id) %]
+    [%- L.hidden_tag("shop_part.shop_id", SELF.shop_part.shop_id) %]
+    [% ELSE %]
+    [%- L.hidden_tag("shop_part.shop_id", FORM.shop_id) %]
+    [%- L.hidden_tag("shop_part.part_id", FORM.part_id) %]
+    [% END %]
+
+  [% # L.dump(SELF.shop_part.shop) %]
+    <table>
+    <tr>
+     <td>[% LxERP.t8("Description") %]</td>
+     <td colspan="3">
+       [% IF SELF.shop_part.shop.use_part_longdescription %]
+         [% L.textarea_tag('notes', SELF.shop_part.part.notes, wrap="soft", readonly="readonly", style="width: 350px; height: 150px", class="texteditor") %]
+       [% ELSE %]
+         [% L.textarea_tag('shop_part.shop_description', SELF.shop_part.shop_description, wrap="soft", style="width: 350px; height: 150px", class="texteditor") %]
+       [% END %]
+     </td>
+    </tr>
+    <tr>
+     <td>[% LxERP.t8("Active") %]</td>
+     <td>[% L.yes_no_tag("shop_part.active", SELF.shop_part.active, default = "yes") %]</td>
+     <td>[% LxERP.t8("Date") %]</td>
+     <td>[% L.date_tag("shop_part.show_date", SELF.shop_part.show_date) %]</td>
+    </tr>
+    <tr>
+      <td>[% 'Price Source' | $T8 %]</th>
+      [% IF SELF.shop_part.active_price_source %]
+        [% SET price_source = SELF.shop_part.active_price_source %]
+      [% ELSE %]
+        [% SET price_source = SELF.shop_part.shop.price_source %]
+      [% END %]
+      <td>[% L.select_tag('shop_part.active_price_source', SELF.price_sources, value_key = 'id', title_key = 'name', with_empty = 0, default = price_source, default_value_key='id' ) %]</td>
+     <td>[% LxERP.t8("Front page") %]</td>
+     <td>[% L.yes_no_tag('shop_part.front_page', SELF.shop_part.front_page) %]</td>
+    </tr>
+    <tr>
+     <td>[% LxERP.t8("Sort order") %]</td>
+     <td>[% L.input_tag("shop_part.sortorder", SELF.shop_part.sortorder, size=2) %]</td>
+     <td>[% LxERP.t8("Meta tag title") %]</td>
+     <td>[% L.input_tag("shop_part.metatag_title", SELF.shop_part.metatag_title, size=12) %]</td>
+    </tr>
+    <tr>
+     <td>[% LxERP.t8("Meta tag keywords") %]</td>
+     <td>[% L.input_tag("shop_part.metatag_keywords", SELF.shop_part.metatag_keywords, size=22) %]</td>
+     <td>[% LxERP.t8("Meta tag description") %]</td>
+     <td>[% L.textarea_tag("shop_part.metatag_description", SELF.shop_part.metatag_description, rows=4) %]</td>
+    </tr>
+    </table>
+    [% IF SELF.shop_part.id %]
+      [% L.button_tag("kivi.ShopPart.save_shop_part(" _ SELF.shop_part.id _ ")", LxERP.t8("Save"))  %]</td>
+    [% ELSE %]
+      [% L.button_tag("kivi.ShopPart.add_shop_part()", LxERP.t8("Save"))  %]</td>
+    [% END %]
+  </div>
+</form>
+
+[%- IF SELF.shop_part.part.image && INSTANCE_CONF.get_parts_show_image %]
+         <a href="[% SELF.shop_part.part.image | html %]" target="_blank"><img style="[% INSTANCE_CONF.get_parts_image_css %]" src="[% SELF.shop_part.part.image | html %]"/></a>
+[%- END %]
diff --git a/templates/webpages/shops/form.html b/templates/webpages/shops/form.html
new file mode 100644 (file)
index 0000000..2f08e72
--- /dev/null
@@ -0,0 +1,114 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE P -%][%- USE T8 -%]
+
+[% SET style="width: 400px" %]
+[% SET size=34 %]
+
+<h1>[% HTML.escape(title) %]</h1>
+<form id="form" action="controller.pl" method="post">
+
+[%- INCLUDE 'common/flash.html' %]
+
+[%- L.hidden_tag("id", SELF.shop.id) %]
+
+<table>
+  <tr>
+    <th align="right">[% 'Description' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.description", SELF.shop.description, size=size) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Shop type' | $T8 %]</th>
+    <td>[% L.select_tag('shop.connector', SELF.connectors, value_key = 'id', title_key = 'description', with_empty = 0, default = SELF.shop.connector, default_value_key='id' ) %]</td>
+  <tr>
+  <tr>
+    <th align="right">[% 'Price type' | $T8 %]</th>
+    <td>[% L.select_tag('shop.pricetype', SELF.price_types, value_key = 'id', title_key = 'name', with_empty = 0, default = SELF.shop.pricetype, default_value_key='id' ) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Price Source' | $T8 %]</th>
+    <td>[% L.select_tag('shop.price_source', SELF.price_sources, value_key = 'id', title_key = 'name', with_empty = 0, default = SELF.shop.price_source, default_value_key='id' ) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Bookinggroup/Tax' | $T8 %]</th>
+    <td>[% L.select_tag('shop.taxzone_id', SELF.taxzone_id, value_key = 'id', title_key = 'name', with_empty = 0, default = SELF.shop.taxzone_id, default_value_key='id' ) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Protocol' | $T8 %]</th>
+    <td>[% L.select_tag('shop.protocol', SELF.protocols value_key = 'id', title_key = 'name', with_empty = 0, default = SELF.shop.protocol, default_value_key='id' ) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Server' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.server", SELF.shop.server, size=size) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Port' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.port", SELF.shop.port, size=5) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Proxy' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.proxy", SELF.shop.proxy, size=size) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Path' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.path", SELF.shop.path, size=size) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Realm' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.realm", SELF.shop.realm, size=size) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'User' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.login", SELF.shop.login, size=size) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Password' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.password", SELF.shop.password, size=size) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Last ordernumber' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.last_order_number", SELF.shop.last_order_number, size=12) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Orders to fetch' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.orders_to_fetch", SELF.shop.orders_to_fetch, size=12) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Transaction description' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.transaction_description", SELF.shop.transaction_description, size=size) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Default part for shipping costs' | $T8 %]</th>
+    <td>[%- P.part.picker('shop.shipping_costs_parts_id', SELF.shop.shipping_costs_parts_id, style="width: 300px") %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Obsolete' | $T8 %]</th>
+    <td>[% L.checkbox_tag('shop.obsolete', checked = SELF.shop.obsolete, for_submit=1) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Use Long Description from Parts for Shop Long Description' | $T8 %]</th>
+    <td>[% L.yes_no_tag('shop.use_part_longdescription', SELF.shop.use_part_longdescription) %]</td>
+  </tr>
+</table>
+
+ <hr>
+
+<script type="text/javascript">
+<!--
+function check_prerequisites() {
+  if ($('#shop_description').val() === "") {
+    alert(kivi.t8('The name is missing.'));
+    return false;
+  }
+  if ($('#shop_url').val() === "") {
+    alert(kivi.t8('The URL is missing.'));
+    return false;
+  }
+  if ($('#shop_port').val() === "") {
+    alert(kivi.t8('The port is missing.'));
+    return false;
+  }
+
+  return true;
+}
+-->
+</script>
+</form>
diff --git a/templates/webpages/shops/list.html b/templates/webpages/shops/list.html
new file mode 100644 (file)
index 0000000..6ea62b4
--- /dev/null
@@ -0,0 +1,31 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%][%- INCLUDE 'common/flash.html' %]
+
+<h1>[% title %]</h1>
+
+<p>
+ <table width="100%" id="shop_list">
+  <thead>
+   <tr class="listheading">
+    <th align="center" width="1%"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></th>
+    <th>[% 'Description' | $T8 %]</th>
+    <th>[% 'Type' | $T8 %]</th>
+    <th>[% 'Obsolete' | $T8 %]</th>
+   </tr>
+  </thead>
+
+  <tbody>
+   [%- FOREACH shop = SHOPS %]
+    <tr class="listrow" id="shop_id_[% shop.id %]">
+     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></td>
+     <td><a href="[% SELF.url_for(action='edit', id=shop.id) %]">[% HTML.escape(shop.description) %]</a></td>
+     <td>[% HTML.escape(shop.connector) %]</a></td>
+     <td>[% HTML.escape(shop.obsolete) %]</td>
+    </tr>
+   [%- END %]
+  </tbody>
+ </table>
+</p>
+
+<hr height="3">
+
+[% L.sortable_element('#shop_list tbody', url=SELF.url_for(action='reorder'), with='shop_id') %]
diff --git a/templates/webpages/shops/test_shop_connection.html b/templates/webpages/shops/test_shop_connection.html
new file mode 100644 (file)
index 0000000..65e23a3
--- /dev/null
@@ -0,0 +1,20 @@
+[%- USE HTML %][%- USE LxERP -%][%- USE L -%]
+[%- IF ok %]
+
+ <p class="message_ok">[% LxERP.t8('The connection to the shop was established successfully.') %]</p>
+ <p>[% LxERP.t8('Version')%]: [% HTML.escape(version) %]</p>
+
+[%- ELSE %]
+
+ <p class="message_error">
+  [% LxERP.t8('The connection to the shop could not be established.') %]
+  [% LxERP.t8('Error message from the webshop api:') %]
+ </p>
+
+ <p>[% HTML.escape(version) %]</p>
+
+[%- END %]
+
+<div>
+ <a href="#" onclick="$('#test_shop_connection_window').dialog('close');">[% LxERP.t8("Close Window") %]</a>
+</div>
diff --git a/templates/webpages/simple_system_setting/_bank_account_form.html b/templates/webpages/simple_system_setting/_bank_account_form.html
new file mode 100644 (file)
index 0000000..2d364aa
--- /dev/null
@@ -0,0 +1,62 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE P -%]
+
+[% SET style="width: 400px" %]
+
+<table>
+ <tr>
+  <th align="right">[% LxERP.t8('Description') %]</th>
+  <td>[%- L.input_tag("object.name", SELF.object.name, style=style, "data-validate"="required", "data-title"=LxERP.t8("Description")) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8('IBAN') %]</th>
+  <td>[%- L.input_tag("object.iban", SELF.object.iban, style=style, "data-validate"="required", "data-title"=LxERP.t8("IBAN")) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8('Bank') %]</th>
+  <td>[%- L.input_tag("object.bank", SELF.object.bank, style=style) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8('Account number') %]</th>
+  <td>[%- L.input_tag("object.account_number", SELF.object.account_number, style=style) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8('BIC') %]</th>
+  <td>[%- L.input_tag("object.bic", SELF.object.bic, style=style) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8('Bank code') %]</th>
+  <td>[%- L.input_tag("object.bank_code", SELF.object.bank_code, style=style) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8('Bank Account Id Number (Swiss)') %]</th>
+  <td>[%- L.input_tag("object.bank_account_id", SELF.object.bank_account_id, style=style) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8('Chart') %]</th>
+  <td>[% P.chart.picker('object.chart_id', SELF.object.chart_id, type='AR_paid,AP_paid', category='A,L,Q', choose=1, style=style, "data-validate"="required", "data-title"=LxERP.t8("Chart")) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8('Use for Factur-X/ZUGFeRD') %]</th>
+  <td>[% L.checkbox_tag('object.use_for_zugferd', checked = SELF.object.use_for_zugferd, for_submit=1) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8('Use for Swiss QR-Bill') %]</th>
+  <td>[% L.checkbox_tag('object.use_for_qrbill', checked = SELF.object.use_for_qrbill, for_submit=1) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8('Obsolete') %]</th>
+  <td>[% L.checkbox_tag('object.obsolete', checked = SELF.object.obsolete, for_submit=1) %]</td>
+ </tr>
+ <tr>
+  <td align="left">[% LxERP.t8('Reconciliation') %]:</td>
+  <td></td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8('Starting date') %]</th>
+  <td>[% L.date_tag('object.reconciliation_starting_date', SELF.object.reconciliation_starting_date) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8('Starting balance') %]</th>
+  <td>[%- L.input_tag('object.reconciliation_starting_balance_as_number', SELF.object.reconciliation_starting_balance_as_number) %]</td>
+ </tr>
+</table>
diff --git a/templates/webpages/simple_system_setting/_business_form.html b/templates/webpages/simple_system_setting/_business_form.html
new file mode 100644 (file)
index 0000000..85fe387
--- /dev/null
@@ -0,0 +1,25 @@
+[%- USE LxERP -%][%- USE L -%]
+[% SET style="width: 200px" %]
+<table>
+ <tr>
+  <td>[%- LxERP.t8("Description") %]</td>
+  <td>[% L.input_tag("object.description", SELF.object.description, style=style, "data-validate"="required", "data-title"=LxERP.t8("Description")) %]</td>
+ </tr>
+
+ <tr>
+  <td>[%- LxERP.t8("Discount") %]</td>
+  <td>[% L.input_tag("object.discount_as_percent", SELF.object.discount_as_percent, style=style) %]%</td>
+ </tr>
+
+ <tr>
+  <td>[%- LxERP.t8("Customernumberinit") %]</td>
+  <td>[% L.input_tag("object.customernumberinit", SELF.object.customernumberinit, style=style) %]</td>
+ </tr>
+
+ [%- IF LXCONFIG.features.vertreter %]
+ <tr>
+  <td>[%- LxERP.t8("Representative") %]</td>
+  <td>[% L.checkbox_tag("object.salesman", "value", 1, "checked", SELF.object.salesman, for_submit=1) %]</td>
+ </tr>
+ [%- END %]
+</table>
diff --git a/templates/webpages/simple_system_setting/_default_form.html b/templates/webpages/simple_system_setting/_default_form.html
new file mode 100644 (file)
index 0000000..c9bf657
--- /dev/null
@@ -0,0 +1,7 @@
+[%- USE LxERP -%][%- USE L -%]
+<table>
+ <tr>
+  <th align="right">[% LxERP.t8("Description") %]</th>
+  <td>[% L.input_tag("object.description", LxERP.t8(SELF.object.description), "data-validate"="required", "data-title"=LxERP.t8("Description")) %]</td>
+ </tr>
+</table>
diff --git a/templates/webpages/simple_system_setting/_language_form.html b/templates/webpages/simple_system_setting/_language_form.html
new file mode 100644 (file)
index 0000000..cf252b9
--- /dev/null
@@ -0,0 +1,33 @@
+[%- USE LxERP -%][%- USE L -%][%- USE HTML -%]
+[% SET style="width: 250px" %]
+<table>
+ <tr>
+  <th align="right">[% LxERP.t8("Language") %]</th>
+  <td>[% L.input_tag("object.description", SELF.object.description, style=style, "data-validate"="required", "data-title"=LxERP.t8("Language")) %]</td>
+ <tr>
+ <tr>
+  <th align="right">[% LxERP.t8("Template Code") %]</th>
+  <td>[% L.input_tag("object.template_code", SELF.object.template_code, style=style) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8("Article Code") %]</th>
+  <td>[% L.input_tag("object.article_code", SELF.object.article_code, style=style) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8("Number Format") %]</th>
+  <td>[% L.select_tag("object.output_numberformat", SELF.numberformats, default=SELF.object.output_numberformat, with_empty=1, empty_title=LxERP.t8("use program settings"), style=style) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8("Date Format") %]</th>
+  <td>[% L.select_tag("object.output_dateformat", SELF.dateformats, default=SELF.object.output_dateformat, with_empty=1, empty_title=LxERP.t8("use program settings"), style=style) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8("Long Dates") %]</th>
+  <td>[% L.yes_no_tag("object.output_longdates", SELF.object.output_longdates, style=style) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8("Obsolete") %]</th>
+  <td>[% L.yes_no_tag("object.obsolete", SELF.object.obsolete, style=style) %]</td>
+ </tr>
+
+</table>
diff --git a/templates/webpages/simple_system_setting/_part_classification_form.html b/templates/webpages/simple_system_setting/_part_classification_form.html
new file mode 100644 (file)
index 0000000..309f67c
--- /dev/null
@@ -0,0 +1,23 @@
+[%- USE LxERP -%][%- USE L -%]
+<table>
+ <tr>
+  <th align="right">[% LxERP.t8('Description') %]</th>
+  <td>[% L.input_tag("object.description",  LxERP.t8(SELF.object.description), "data-validate"="required", "data-title"=LxERP.t8("Description")) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8('TypeAbbreviation') %]</th>
+  <td>[% L.input_tag("object.abbreviation",  LxERP.t8(SELF.object.abbreviation), size="2", maxlength="2" ) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8('Used for Purchase') %]</th>
+  <td>[% L.checkbox_tag("object.used_for_purchase", checked=(SELF.object.used_for_purchase ? 1:''), for_submit=1) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8('Used for Sale') %]</th>
+  <td>[% L.checkbox_tag("object.used_for_sale", checked=(SELF.object.used_for_sale ? 1:''), for_submit=1) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8('Report separately') %]</th>
+  <td>[% L.checkbox_tag("object.report_separate", checked=(SELF.object.report_separate ? 1:''), for_submit=1) %]</td>
+ </tr>
+</table>
diff --git a/templates/webpages/simple_system_setting/_parts_group_form.html b/templates/webpages/simple_system_setting/_parts_group_form.html
new file mode 100644 (file)
index 0000000..8e9025a
--- /dev/null
@@ -0,0 +1,13 @@
+[%- USE LxERP -%][%- USE L -%]
+<table>
+ <tr>
+  <th align="right">[% LxERP.t8("Description") %]</th>
+  <td>
+   [%- L.input_tag("object.partsgroup", SELF.object.partsgroup, "data-validate"="required", "data-title"=LxERP.t8("Description")) %]
+  </td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8("Obsolete") %]</th>
+  <td>[% L.checkbox_tag("object.obsolete", checked=SELF.object.obsolete, for_submit=1) %]</td>
+ </tr>
+</table>
diff --git a/templates/webpages/simple_system_setting/_price_factor_form.html b/templates/webpages/simple_system_setting/_price_factor_form.html
new file mode 100644 (file)
index 0000000..9e5bc55
--- /dev/null
@@ -0,0 +1,24 @@
+[%- USE LxERP -%][%- USE L -%][%- USE HTML -%]
+[%- SET orphaned = SELF.object.orphaned %]
+<table>
+ <tr>
+  <th align="right">[% LxERP.t8("Description") %]</th>
+  <td>[% L.input_tag("object.description", LxERP.t8(SELF.object.description), "data-validate"="required", "data-title"=LxERP.t8("Description")) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8("Factor") %]</th>
+  <td>
+   [% IF orphaned %]
+    [% L.input_tag("object.factor_as_number", LxERP.t8(SELF.object.factor_as_number), "data-validate"="required", "data-title"=LxERP.t8("Factor")) %]
+   [% ELSE %]
+    [% HTML.escape(SELF.object.factor_as_number) %]
+   [% END %]
+  </td>
+ </tr>
+</table>
+
+[% UNLESS orphaned %]
+ <p>
+  [% LxERP.t8("Note: the object is already in use. Therefore some values cannot be changed.") %]
+ </p>
+[% END %]
diff --git a/templates/webpages/simple_system_setting/_pricegroup_form.html b/templates/webpages/simple_system_setting/_pricegroup_form.html
new file mode 100644 (file)
index 0000000..ec7f817
--- /dev/null
@@ -0,0 +1,18 @@
+[%- USE LxERP -%][%- USE L -%]
+<table>
+ <tr>
+  <th align="right">[% LxERP.t8("Description") %]</th>
+  <td>
+   [%- L.input_tag("object.pricegroup", SELF.object.pricegroup, "data-validate"="required", "data-title"=LxERP.t8("Description")) %]
+  </td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8("Obsolete") %]</th>
+  <td>[% L.checkbox_tag("object.obsolete", checked=SELF.object.obsolete, for_submit=1) %]</td>
+ </tr>
+ <tr>
+  <th align="right">[% LxERP.t8("Delete for Customers") %]</th>
+  <td>[% L.checkbox_tag("SELF.remove_customer_pricegroup", checked=SELF.remove_customer_pricegroup, for_submit=1) %] [% LxERP.t8("This will also remove this pricegroup for all customers.") %]</td>
+ </tr>
+
+</table>
diff --git a/templates/webpages/simple_system_setting/_requirement_spec_acceptance_status_form.html b/templates/webpages/simple_system_setting/_requirement_spec_acceptance_status_form.html
new file mode 100644 (file)
index 0000000..472c1aa
--- /dev/null
@@ -0,0 +1,13 @@
+[%- USE LxERP -%][%- USE L -%]
+[% SET style="width: 250px" %]
+<table>
+ <tr>
+  <td>[% LxERP.t8("Name") %]</td>
+  <td>[% L.select_tag("object.name",  SELF.valid_names, default=SELF.object.name, style=style) %]</td>
+ </tr>
+
+ <tr>
+  <td>[% LxERP.t8("Description") %]</td>
+  <td>[% L.input_tag("object.description", SELF.object.description, style=style, "data-validate"="required", "data-title"=LxERP.t8("Description")) %]</td>
+ </tr>
+</table>
diff --git a/templates/webpages/simple_system_setting/_requirement_spec_predefined_text_form.html b/templates/webpages/simple_system_setting/_requirement_spec_predefined_text_form.html
new file mode 100644 (file)
index 0000000..c302ca5
--- /dev/null
@@ -0,0 +1,24 @@
+[%- USE LxERP -%][%- USE L -%]<table>
+ <tr>
+  <th align="right">[% LxERP.t8("Description") %]</th>
+  <td>[% L.input_tag("object.description", SELF.object.description, size=60, class="initial_focus", "data-validate"="required", "data-title"=LxERP.t8("Description")) %]</td>
+ </tr>
+
+ <tr>
+  <th align="right">[% LxERP.t8("Title") %]</th>
+  <td>[% L.input_tag("object.title", SELF.object.title, size=60) %]</td>
+ </tr>
+
+ <tr valign="top">
+  <th align="right">[% LxERP.t8("Content") %]</th>
+  <td>[% L.textarea_tag("object.text_as_restricted_html", SELF.object.text_as_restricted_html, class="texteditor", style="width: 800px; height: 300px") %]</td>
+ </tr>
+
+ <tr>
+  <th align="right">[% LxERP.t8("Useable for…") %]</th>
+  <td>
+   [% L.checkbox_tag("object.useable_for_text_blocks", label=LxERP.t8("Text blocks"), for_submit=1, value=1, checked=SELF.object.useable_for_text_blocks) %]
+   [% L.checkbox_tag("object.useable_for_sections",    label=LxERP.t8("Sections"),    for_submit=1, value=1, checked=SELF.object.useable_for_sections) %]
+  </td>
+ </tr>
+</table>
diff --git a/templates/webpages/simple_system_setting/_requirement_spec_status_form.html b/templates/webpages/simple_system_setting/_requirement_spec_status_form.html
new file mode 100644 (file)
index 0000000..472c1aa
--- /dev/null
@@ -0,0 +1,13 @@
+[%- USE LxERP -%][%- USE L -%]
+[% SET style="width: 250px" %]
+<table>
+ <tr>
+  <td>[% LxERP.t8("Name") %]</td>
+  <td>[% L.select_tag("object.name",  SELF.valid_names, default=SELF.object.name, style=style) %]</td>
+ </tr>
+
+ <tr>
+  <td>[% LxERP.t8("Description") %]</td>
+  <td>[% L.input_tag("object.description", SELF.object.description, style=style, "data-validate"="required", "data-title"=LxERP.t8("Description")) %]</td>
+ </tr>
+</table>
diff --git a/templates/webpages/simple_system_setting/_requirement_spec_type_form.html b/templates/webpages/simple_system_setting/_requirement_spec_type_form.html
new file mode 100644 (file)
index 0000000..96fdfb4
--- /dev/null
@@ -0,0 +1,28 @@
+[% USE HTML %][% USE L %][% USE LxERP %]
+<table>
+ <tr>
+  <th align="right">[% LxERP.t8("Description") %]</th>
+  <td>[% L.input_tag("object.description", SELF.object.description, "data-validate"="required", "data-title"=LxERP.t8("Description")) %]</td>
+ </tr>
+
+ <tr>
+  <th align="right">[% LxERP.t8("Print template base file name") %]<sup>(1)</sup></th>
+  <td>[% L.input_tag("object.template_file_name", SELF.object.template_file_name, "data-validate"="required", "data-title"=LxERP.t8("Print template base file name")) %]</td>
+ </tr>
+
+ <tr>
+  <th align="right">[% LxERP.t8("Section number format") %]<sup>(2)</sup></th>
+  <td>[% L.input_tag("object.section_number_format", SELF.object.section_number_format, size="15", "data-validate"="required", "data-title"=LxERP.t8("Section number format")) %]</td>
+ </tr>
+
+ <tr>
+  <th align="right">[% LxERP.t8("Function block number format") %]<sup>(2)</sup></th>
+  <td>[% L.input_tag("object.function_block_number_format", SELF.object.function_block_number_format, size="15", "data-validate"="required", "data-title"=LxERP.t8("Function block number format")) %]</td>
+ </tr>
+</table>
+
+<p>
+ <sup>(1)</sup>: [% LxERP.t8("The base file name without a path or an extension to be used for printing for this type of requirement spec.") %]
+ <br>
+ <sup>(2)</sup>: [% LxERP.t8("The numbering will start at 1 with each requirement spec.") %]
+</p>
diff --git a/templates/webpages/simple_system_setting/_time_recording_article_form.html b/templates/webpages/simple_system_setting/_time_recording_article_form.html
new file mode 100644 (file)
index 0000000..2c7ddf3
--- /dev/null
@@ -0,0 +1,11 @@
+[%- USE LxERP -%]
+[%- USE L -%]
+[%- USE P -%]
+<table>
+ <tr>
+  <th align="right">[% LxERP.t8("Article") %]</th>
+  <td>
+   [% P.part.picker('object.part_id', SELF.object.part, convertible_unit='min', "data-validate"="required", "data-title"=LxERP.t8("Article")) %]
+  </td>
+ </tr>
+</table>
diff --git a/templates/webpages/simple_system_setting/form.html b/templates/webpages/simple_system_setting/form.html
new file mode 100644 (file)
index 0000000..b30680e
--- /dev/null
@@ -0,0 +1,17 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+
+[% SET style="width: 400px" %]
+[% SET size=15 %]
+
+<h1>[% HTML.escape(title) %]</h1>
+
+[%- INCLUDE "common/flash.html" %]
+
+<form action="controller.pl" method="post" id="form">
+
+ [%- L.hidden_tag("type", SELF.type) %]
+ [%- L.hidden_tag("id", SELF.object.id) %]
+
+ [%- SET sub_file = "simple_system_setting/_" _ sub_form_template _ "_form.html";
+     INCLUDE $sub_file %]
+</form>
diff --git a/templates/webpages/simple_system_setting/list.html b/templates/webpages/simple_system_setting/list.html
new file mode 100644 (file)
index 0000000..0f89642
--- /dev/null
@@ -0,0 +1,45 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+
+<h1>[% HTML.escape(title) %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+<table width="100%" id="object_list">
+ <thead>
+  <tr class="listheading">
+   [% IF SELF.supports_reordering %]
+    <th align="center" width="1%"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></th>
+   [% END %]
+   [% FOREACH attribute = SELF.list_attributes %]
+    <th[% IF attribute.align %] align="[% attribute.align %]"[% END %]>[% HTML.escape(attribute.title) %]</th>
+   [% END %]
+  </tr>
+ </thead>
+
+ <tbody>
+  [%- FOREACH object = SELF.all_objects %]
+   <tr class="listrow" id="object_id_[% object.id %]">
+   [% IF SELF.supports_reordering %]
+    <td align="center" class="dragdrop">[% L.img_tag(src="image/updown.png", alt=LxERP.t8("reorder item")) %]</td>
+   [% END %][%# IF SELF.supports_reordering %]
+   [% FOREACH attribute = SELF.list_attributes %]
+    <td[% IF attribute.align %] align="[% attribute.align %]"[% END %]>
+     [% IF loop.count == 1 %]
+      <a href="[% SELF.url_for(action='edit', type=SELF.type, id=object.id) %]">
+     [% END %][%# IF loop.count == 0 %]
+     [% SET method = attribute.method
+            value  = attribute.exists('formatter') ? attribute.formatter(object) : object.$method ;
+        HTML.escape(value) %]
+     [% IF loop.count == 1 %]
+      </a>
+     [% END %][%# IF loop.count == 0 %]
+    </td>
+   [% END %][%# FOREACH attribute… %]
+   </tr>
+  [%- END %][%# FOREACH object… %]
+ </tbody>
+</table>
+
+[% IF SELF.supports_reordering %]
+[% L.sortable_element("#object_list tbody", url=SELF.url_for(action="reorder", type=SELF.type), with="object_id") %]
+[% END %]
index 4a44ad0..4b56f9e 100644 (file)
@@ -1,4 +1,4 @@
-[% USE HTML %][% USE L %][% USE LxERP %]
+[% USE HTML %][% USE L %][% USE LxERP %][%- USE P -%]
 <h1>[% FORM.title %]</h1>
 
 [%- INCLUDE 'common/flash.html' %]
   </tbody>
  </table>
 
- <p>
-[% IF SELF.task_server.is_running %]
-  <a href="[% SELF.url_for(action => 'stop') %]">[%- LxERP.t8('Stop task server') %]</a>
-[%- ELSE %]
-  <a href="[% SELF.url_for(action => 'start') %]">[%- LxERP.t8('Start task server') %]</a>
-[%- END %]
-  |
-  <a href="[% SELF.url_for(controller => 'BackgroundJob', action => 'list') %]">[%- LxERP.t8('View background jobs') %]</a>
-  |
-  <a href="[% SELF.url_for(controller => 'BackgroundJobHistory', action => 'list') %]">[%- LxERP.t8('View background job history') %]</a>
- </p>
+<form id="form" method="post" action="controller.pl">
+ [% P.hidden_tag("action", "TaskServer/" _ (SELF.task_server.is_running ? "stop" : "start")) %]
+</form>
index d615378..b59385a 100644 (file)
@@ -1,21 +1,21 @@
-[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]<h1>[% HTML.escape(title) %]</h1>
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE P -%][%- USE T8 -%]<h1>[% HTML.escape(title) %]</h1>
 [% SET style="width: 400px" %]
 
-<form action="controller.pl" method="post">
+<form action="controller.pl" method="post" id="form">
 [%- L.hidden_tag("id", SELF.config.id) %]
 
 <table>
   <tr>
     <th align="right">[% 'Description' | $T8 %]</th>
-    <td>[%- L.input_tag("config.description", SELF.config.description) %]</td>
+    <td>[%- L.input_tag("config.description", SELF.config.description, "data-validate"="required", "data-title"=LxERP.t8("Description")) %]</td>
   </tr>
 [%- FOREACH bg = BUCHUNGSGRUPPEN %]
   <tr>
     <th align="right">[% 'Revenue' | $T8 %] [% HTML.escape(bg.description) %]</th>
     [%- IF NOT SELF.config.id %]
-    <td>[% L.chart_picker('income_accno_id_' _ bg.id, SELF.defaults.income_accno_id, choose=1, type='IC_income,IC_sale', style=style) %]</td>
+    <td>[% P.chart.picker('income_accno_id_' _ bg.id, SELF.defaults.income_accno_id, choose=1, type='IC_income,IC_sale', style=style) %]</td>
     [%- ELSIF SELF.config.id AND SELF.config.orphaned %]
-    <td>[% L.chart_picker('income_accno_id_' _ bg.id, CHARTLIST.${bg.id}.income_accno_id, choose=1, type='IC_income,IC_sale', style=style) %]</td>
+    <td>[% P.chart.picker('income_accno_id_' _ bg.id, CHARTLIST.${bg.id}.income_accno_id, choose=1, type='IC_income,IC_sale', style=style) %]</td>
     [%- ELSE %]
     <td>[% CHARTLIST.${bg.id}.income_accno_description %]</td>
     [%- END %]
@@ -23,9 +23,9 @@
   <tr>
     <th align="right">[% 'Expense' | $T8 %] [% HTML.escape(bg.description) %]</th>
     [%- IF NOT SELF.config.id %]
-    <td>[% L.chart_picker('expense_accno_id_' _ bg.id, SELF.defaults.expense_accno_id, choose=1, type='IC_expense,IC_cogs', style=style) %]</td>
+    <td>[% P.chart.picker('expense_accno_id_' _ bg.id, SELF.defaults.expense_accno_id, choose=1, type='IC_expense,IC_cogs', style=style) %]</td>
     [%- ELSIF SELF.config.id AND SELF.config.orphaned %]
-    <td>[% L.chart_picker('expense_accno_id_' _ bg.id, CHARTLIST.${bg.id}.expense_accno_id, choose=1, type='IC_expense,IC_cogs', style=style) %]</td>
+    <td>[% P.chart.picker('expense_accno_id_' _ bg.id, CHARTLIST.${bg.id}.expense_accno_id, choose=1, type='IC_expense,IC_cogs', style=style) %]</td>
     [%- ELSE %]
     <td>[% CHARTLIST.${bg.id}.expense_accno_description %]</td>
     [%- END %]
 </table>
 
 [% LxERP.t8('Obsolete') %]: [% L.checkbox_tag('config.obsolete', checked = SELF.config.obsolete, for_submit=1) %]
-
- <p>
-  [% L.hidden_tag("action", "Taxzones/dispatch") %]
-  [% L.submit_tag("action_" _  (SELF.config.id ? "update" : "create"), LxERP.t8('Save'), onclick="return check_prerequisites();") %]
-  [%- IF SELF.config.id AND SELF.config.orphaned %]
-    [% L.submit_tag("action_delete", LxERP.t8('Delete'), confirm=LxERP.t8('Are you sure?')) %]
-  [%- END %]
-  <a href="[% SELF.url_for(action='list') %]">[%- LxERP.t8("Cancel") %]</a>
- </p>
-
- <hr>
-
-<script type="text/javascript">
-<!--
-function check_prerequisites() {
-  if ($('#config_description').val() === "") {
-    alert(kivi.t8('The description is missing.'));
-    return false;
-  }
-
-  return true;
-}
--->
-</script>
 </form>
index 0229804..793f88c 100644 (file)
@@ -6,7 +6,7 @@
  <table width="100%" id="taxzone_list">
   <thead>
    <tr class="listheading">
-    <th align="center" width="1%"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></th>
+    <th align="center" width="1%"><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]"></th>
     <th>[% 'Description' | $T8 %]</th>
    </tr>
   </thead>
@@ -14,7 +14,7 @@
   <tbody>
    [%- FOREACH tz = TAXZONES %]
     <tr class="listrow" id="tzone_id_[% tz.id %]">
-     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></td>
+     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]"></td>
      <td><a href="[% SELF.url_for(action='edit', id=tz.id) %]">[% HTML.escape(tz.description) %]</a></td>
     </tr>
    [%- END %]
  </table>
 </p>
 
-<hr height="3">
-
 [% L.sortable_element('#taxzone_list tbody', url=SELF.url_for(action='reorder'), with='tzone_id') %]
-
-<p>
- <a href="[% SELF.url_for(action='new') %]">[%- 'Add' | $T8 %]</a>
-</p>
-
diff --git a/templates/webpages/time_recording/_filter.html b/templates/webpages/time_recording/_filter.html
new file mode 100644 (file)
index 0000000..16fbe6d
--- /dev/null
@@ -0,0 +1,70 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE P %]
+[%- USE LxERP %]
+[%- USE HTML %]
+<form action='controller.pl' method='post' id='filter_form'>
+<div class='filter_toggle'>
+<a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Show Filter' | $T8 %]</a>
+  [% SELF.filter_summary | html %]
+</div>
+<div class='filter_toggle' style='display:none'>
+<a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Hide Filter' | $T8 %]</a>
+ <table id='filter_table'>
+  <tr>
+   <th align="right">[% 'Date' | $T8 %] [% 'From Date' | $T8 %]</th>
+   <td>[% L.date_tag('filter.date:date::ge', filter.date_date__ge) %]</td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Date' | $T8 %] [% 'To Date' | $T8 %]</th>
+   <td>[% L.date_tag('filter.date:date::le', filter.date_date__le) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Customer' | $T8 %]</th>
+    <td>[% L.input_tag('filter.customer.name:substr::ilike', filter.customer.name_substr__ilike, size = 20) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Customer Number' | $T8 %]</th>
+    <td>[% L.input_tag('filter.customer.customernumber:substr::ilike', filter.customer.customernumber_substr__ilike, size = 20) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Order Number' | $T8 %]</th>
+    <td>[% L.input_tag('filter.order.ordnumber:substr::ilike', filter.order.ordnumber_substr__ilike, size = 20) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Project' | $T8 %]</th>
+    <td>[% P.project.picker('filter.project_id', filter.project_id, active="both", valid="both", description_style='both', size = 20) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Description' | $T8 %]</th>
+    <td>[% L.input_tag('filter.description:substr::ilike', filter.description_substr__ilike, size = 20) %]</td>
+  </tr>
+
+  [%- IF SELF.can_view_all -%]
+  <tr>
+   <th align="right">[% 'Mitarbeiter' | $T8 %]</th>
+   <td>
+     [% L.select_tag('filter.staff_member_id', SELF.all_employees,
+                     default    => filter.staff_member_id,
+                     title_key  => 'name',
+                     value_key  => 'id',
+                     with_empty => 1,
+                     style      => 'width: 200px') %]
+   </td>
+  </tr>
+  [%- END -%]
+
+  <tr>
+    <th align="right">[% 'Booked' | $T8 %]</th>
+    <td>[% L.select_tag('filter.booked', [ [ '1', LxERP.t8('Yes') ], [ '0', LxERP.t8('No') ] ], default=filter.booked, with_empty=1, style="width: 200px") %]</td>
+  </tr>
+
+ </table>
+
+[% L.hidden_tag('sort_by', FORM.sort_by) %]
+[% L.hidden_tag('sort_dir', FORM.sort_dir) %]
+[% L.hidden_tag('page', FORM.page) %]
+[% L.button_tag('$("#filter_form").clearForm()', LxERP.t8('Reset')) %]
+</div>
+
+</form>
diff --git a/templates/webpages/time_recording/form.html b/templates/webpages/time_recording/form.html
new file mode 100644 (file)
index 0000000..8af1da4
--- /dev/null
@@ -0,0 +1,103 @@
+[% USE L %]
+[% USE P %]
+[% USE T8 %]
+[% USE LxERP %]
+[% USE HTML %]
+
+<h1>[% title %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+<form method="post" action="controller.pl" id="form">
+  [% P.hidden_tag('id',       SELF.time_recording.id) %]
+  [% L.hidden_tag('callback', FORM.callback) %]
+
+  <table>
+   [%- IF SELF.use_duration %]
+    <tr>
+      <th align="right">[% 'Date' | $T8 %]</th>
+      <td>
+        [% P.date_tag('time_recording.date_as_date', SELF.time_recording.date_as_date, "data-validate"="required", "data-title"=LxERP.t8('Date')) %]<br>
+      </td>
+    </tr>
+    <tr>
+      <th align="right">[% 'Duration' | $T8 %]</th>
+      <td>
+        [% P.input_tag('duration_h', SELF.time_recording.duration_as_hours   || '', size=4, class='numeric',
+           "data-validate"="number", "data-title"=LxERP.t8('h'),   "placeholder"=LxERP.format_amount(0.00, 2)) %] [% 'h'   | $T8 %]<sup>(1)</sup>
+        [% P.input_tag('duration_m', SELF.time_recording.duration_as_minutes || '', size=4, class='numeric',
+           "data-validate"="number", "data-title"=LxERP.t8('min'), "placeholder"="0"                         ) %] [% 'min' | $T8 %]
+      </td>
+    </tr>
+   [%- ELSE %]
+    <tr>
+      <th align="right">[% 'Start' | $T8 %]</th>
+      <td>
+        [% P.date_tag('start_date',  SELF.start_date, "data-validate"="required", "data-title"=LxERP.t8('Start date'), onchange='kivi.TimeRecording.set_end_date()') %]
+        [% P.input_tag('start_time', SELF.start_time, type="time", "data-validate"="required", "data-title"=LxERP.t8('Start time')) %]
+        [% P.button_tag('kivi.TimeRecording.set_current_date_time("start")', LxERP.t8('now')) %]
+      </td>
+    </tr>
+    <tr>
+      <th align="right">[% 'End' | $T8 %]</th>
+      <td>
+        [% P.date_tag('end_date',  SELF.end_date) %]
+        [% P.input_tag('end_time', SELF.end_time, type="time") %]
+        [% P.button_tag('kivi.TimeRecording.set_current_date_time("end")', LxERP.t8('now')) %]
+      </td>
+    </tr>
+   [%- END %]
+    <tr></tr><tr></tr>
+    <tr>
+      <th align="right">[% 'Sales Order' | $T8 %]</th>
+      <td>[% P.select_tag('time_recording.order_id', SELF.all_orders, default=SELF.time_recording.order_id, with_empty=1, style='width: 300px', onchange='kivi.TimeRecording.order_changed(this.value)') %]</td>
+    </tr>
+    <tr>
+      <th align="right">[% 'Customer' | $T8 %]</th>
+      <td>[% P.customer_vendor.picker('time_recording.customer_id', SELF.time_recording.customer_id, type='customer', style='width: 300px', "data-validate"="required", "data-title"=LxERP.t8('Customer')) %]</td>
+    </tr>
+    <tr>
+      <th align="right">[% 'Project' | $T8 %]</th>
+      <td>[% P.project.picker('time_recording.project_id', SELF.time_recording.project_id, description_style='both', style='width: 300px') %]</td>
+    </tr>
+    <tr></tr><tr></tr>
+    <tr>
+      <th align="right">[% 'Article' | $T8 %]</th>
+      <td>[% P.select_tag('time_recording.part_id', SELF.all_time_recording_articles, default=SELF.time_recording.part_id, with_empty=1, value_key='id', title_key='description', style='width: 300px') %]</td>
+    </tr>
+    <tr>
+      <th align="right">[% 'Description' | $T8 %]</th>
+      <td>[% L.textarea_tag('time_recording.description', SELF.time_recording.description, wrap="soft", style="width: 300px; height: 150px", class="texteditor", "data-validate"="required", "data-title"=LxERP.t8('Description')) %]</td>
+    </tr>
+    <tr>
+      <th align="right">[% 'Mitarbeiter' | $T8 %]</th>
+      <td>
+       [%- IF SELF.can_edit_all -%]
+        [% L.select_tag('time_recording.staff_member_id', SELF.all_employees,
+                        default    => SELF.time_recording.staff_member_id,
+                        title_key  => 'safe_name',
+                        value_key  => 'id',
+                        style      => 'width: 300px') %]
+       [%- ELSE -%]
+        [% SELF.time_recording.staff_member.safe_name | html %]
+       [%- END -%]
+      </td>
+    </tr>
+  </table>
+
+  [%- IF SELF.use_duration %]
+  <p>
+    <sup>(1)</sup>
+    [% 'Valid are integer values and floating point numbers, e.g. 4.75h = 4 hours and 45 minutes.' | $T8 %]
+  </p>
+  [%- END %]
+
+</form>
+
+<script type="text/javascript">
+<!--
+  [% FOREACH i = inputs_to_disable %]
+    kivi.TimeRecording.inputs_to_disable.push("[% i %]");
+  [% END %]
+-->
+</script>
diff --git a/templates/webpages/time_recording/report_bottom.html b/templates/webpages/time_recording/report_bottom.html
new file mode 100644 (file)
index 0000000..71dddfe
--- /dev/null
@@ -0,0 +1,3 @@
+[%- USE L %]
+ [% L.paginate_controls(models=SELF.models) %]
+</form>
diff --git a/templates/webpages/time_recording/report_top.html b/templates/webpages/time_recording/report_top.html
new file mode 100644 (file)
index 0000000..12bb508
--- /dev/null
@@ -0,0 +1,3 @@
+[%- PROCESS 'time_recording/_filter.html' filter=SELF.models.filtered.laundered %]
+<hr>
+<form method="post" action="controller.pl" id="form">
index ad1f8b6..6afb008 100644 (file)
@@ -2,7 +2,7 @@
 [% USE HTML %]
 <h1>[% 'Tax Office Preferences' | $T8 %]</h1>
 
-<form name="verzeichnis" method="post" action="[% HTML.escape(script) %]">
+<form name="verzeichnis" method="post" action="[% HTML.escape(script) %]" id="form">
 <table width="100%">
     <tr>
      <td>
        <fieldset>
        <legend><b>[% 'Taxation' | $T8 %]</b>
        </legend>
-       <input name="method" id="accrual" class="radio" type="radio" value="accrual"
-         [% checked_accrual %]>
-       <label for="accrual">[% 'accrual' | $T8 %]</label>
-       <br>
-       <input name="method" id="cash" class="radio" type="radio" value="cash"
-         [% checked_cash %]>
-       <label for="cash">[% 'cash' | $T8 %]</label>
+            [%- IF method_local %]
+              [% method_local %]
+            [%- END %]
        </fieldset>
        <br>
        <fieldset>
        </legend>
 
 
-           <input name=FA_voranmeld id=month class=radio type=radio value="month"
+           <input name=fa_voranmeld id=month class=radio type=radio value="month"
              [% checked_monthly %]>
            <label for="month">[% 'month' | $T8 %]</label>
            <br>
-           <input name="FA_voranmeld" id=quarter class=radio type=radio value="quarter"
+           <input name="fa_voranmeld" id=quarter class=radio type=radio value="quarter"
              [% checked_quarterly %]>
            <label for="quarter">[% 'quarter' | $T8 %]</label>
            <br>
-           <input name="FA_dauerfrist" id=FA_dauerfrist class=checkbox type=checkbox value="1"
+           <input name="fa_dauerfrist" id=fa_dauerfrist class=checkbox type=checkbox value="1"
              [% checked_dauerfristverlaengerung %]>
            <label for="">[% 'Extension Of Time' | $T8 %]</label>
 
@@ -49,9 +45,9 @@
            <fieldset>
            <legend><b>[% 'Tax Consultant' | $T8 %]</b>
            </legend>
-          <!-- <input name="FA_71" id=FA_71 class=checkbox type=checkbox value="X"
+          <!-- <input name="fa_71" id=fa_71 class=checkbox type=checkbox value="X"
              [% checked_kz_71 %]>
-           <label for="FA_71">[% 'Clearing Tax Received (No 71)' | $T8 %]
+           <label for="fa_71">[% 'Clearing Tax Received (No 71)' | $T8 %]
     .      </label>
            <br>
            <br>-->
            </tr>
            <tr>
            <td>
-           <input name="FA_steuerberater_name" id=steuerberater size=25
-             value="[% HTML.escape(FA_steuerberater_name) %]">
+           <input name="fa_steuerberater_name" id=steuerberater size=25
+             value="[% HTML.escape(fa_steuerberater_name) %]">
            </td>
            <td>
-           <input name="FA_steuerberater_street" id=steuerberater size=25
-             value="[% HTML.escape(FA_steuerberater_street) %]">
+           <input name="fa_steuerberater_street" id=steuerberater size=25
+             value="[% HTML.escape(fa_steuerberater_street) %]">
            </td>
            <td>
-           <input name="FA_steuerberater_city" id=steuerberater size=25
-             value="[% HTML.escape(FA_steuerberater_city) %]">
+           <input name="fa_steuerberater_city" id=steuerberater size=25
+             value="[% HTML.escape(fa_steuerberater_city) %]">
            </td>
            <td>
-           <input name="FA_steuerberater_tel" id=steuerberater size=25
-             value="[% HTML.escape(FA_steuerberater_tel) %]">
+           <input name="fa_steuerberater_tel" id=steuerberater size=25
+             value="[% HTML.escape(fa_steuerberater_tel) %]">
            </tr>
            </table>
 
            </fieldset>
-
-           <br>
-           <br>
-           <hr>
-           <!--<input type=submit class=submit name=action value="
-           [% 'debug' | $T8 %]">-->
-           <input type=submit class=submit name=action
-             value="[% 'continue' | $T8 %]">
          </td>
        </tr>
      </table>
index 083118d..8325c50 100644 (file)
@@ -2,7 +2,7 @@
 [% USE HTML %]
 <h1>[% 'Tax Office Preferences' | $T8 %]</h1>
 
-  <form name="elsterform" method="post" action="[% script %]">
+  <form name="elsterform" method="post" action="[% script %]" id="form">
     <table width="100%">
        <tr>
          <td colspan=2>
@@ -12,7 +12,7 @@
            <fieldset>
              <legend>
                <font size="+1">[% 'Tax Office' | $T8 %]
-               [% HTML.escape(FA_Name) %]</font>
+               [% HTML.escape(fa_name) %]</font>
              </legend>
              <table width="100%" valign="top">
                 <tr>
                         </tr>
                         <tr>
                           <td colspan="2">
-                            <input name="FA_Name" size="40" title="FA_Name"
-                              value="[% HTML.escape(FA_Name) %]" [% readonly %]>
+                            <input name="fa_name" size="40" title="Name"
+                              value="[% HTML.escape(fa_name) %]" [% readonly %]>
                           <td>
                         </tr>
                         <tr>
                           <td colspan="2">
-                            <input name="FA_Strasse" size="40" title="FA_Strasse"
-                              value="[% HTML.escape(FA_Strasse) %]" [% readonly %]>
-                          </td width="100%">
+                            <input name="fa_strasse" size="40" title="Strasse"
+                              value="[% HTML.escape(fa_strasse) %]" [% readonly %]>
+                          </td>
                         </tr>
                         <tr>
                           <td width="116px">
-                            <input name="FA_PLZ" size="10" title="FA_PLZ"
-                              value="[% HTML.escape(FA_PLZ) %]" [% readonly %]>
+                            <input name="fa_plz" size="10" title="PLZ"
+                              value="[% HTML.escape(fa_plz) %]" [% readonly %]>
+                          </td>
+                          <td>
+                            <input name="fa_ort" size="20" title="Ort"
+                              value="[% HTML.escape(fa_ort) %]" [% readonly %]>
+                          </td>
+                        </tr>
+                        <tr>
+                          <td>
+                            [% 'PLZ Grosskunden' | $T8 %]
                           </td>
                           <td>
-                            <input name="FA_Ort" size="20" title="FA_Ort"
-                              value="[% HTML.escape(FA_Ort) %]" [% readonly %]>
+                            <input name="fa_plz_grosskunden" size="20" title="OrtGK"
+                              value="[% HTML.escape(fa_plz_grosskunden) %]" [% readonly %]>
                           </td>
                         </tr>
                       </table>
                         <b>[% 'Contact' | $T8 %]</b>
                       </legend>
                         [% 'Telephone' | $T8 %]<br>
-                        <input name="FA_Telefon" size="40" title="FA_Telefon"
-                          value="[% HTML.escape(FA_Telefon) %]" [% readonly %]>
+                        <input name="fa_telefon" size="40" title="Telefon"
+                          value="[% HTML.escape(fa_telefon) %]" [% readonly %]>
                         <br>
                         <br>
                         [% 'Fax' | $T8 %]<br>
-                        <input name="FA_Fax" size="40" title="FA_Fax"
-                          value="[% HTML.escape(FA_Fax) %]" [% readonly %]>
+                        <input name="fa_fax" size="40" title="Fax"
+                          value="[% HTML.escape(fa_fax) %]" [% readonly %]>
                         <br>
                         <br>
-                        [% 'Internet' | $T8 %]<br>
-                        <input name="FA_Email" size="40" title="FA_Email"
-                          value="[% HTML.escape(FA_Email) %]" [% readonly %]>
+                        [% 'Email' | $T8 %]<br>
+                        <input name="fa_email" size="40" title="Email"
+                          value="[% HTML.escape(fa_email) %]" [% readonly %]>
                         <br>
+                        [% 'Internet' | $T8 %]<br>
                         <br>
-                        <input name="FA_Internet" size="40" title="" title="FA_Internet"
-                          value="[% HTML.escape(FA_Internet) %]" [% readonly %]>
+                        <input name="fa_internet" size="40" title="Internet"
+                          value="[% HTML.escape(fa_internet) %]" [% readonly %]>
                         <br>
                     </fieldset>
                   </td>
                     <legend>
                     <b>[% 'Openings' | $T8 %]</b>
                     </legend>
-                    <textarea name="FA_Oeffnungszeiten" rows="4" cols="40"
-                      [% readonly %]>[% HTML.escape(FA_Oeffnungszeiten) %]</textarea>
+                    <textarea name="fa_oeffnungszeiten" rows="4" cols="40"
+                      [% readonly %]>[% HTML.escape(fa_oeffnungszeiten) %]</textarea>
                     </fieldset>
                     <br>
                       <fieldset>
                       <legend>
                         <b>[% 'Bank Connection Tax Office' | $T8 %]</b>
-                      <legend>
+                      </legend>
                       <table>
                       <tr>
                         <td width="40%">
                           [% 'Bank' | $T8 %]
                           <br>
-                          <input name="FA_Bankbezeichnung_1" size="30"
-                            value="[% HTML.escape(FA_Bankbezeichnung_1) %]" [% readonly %]>
+                          <input name="fa_bankbezeichnung_1" size="30"
+                            value="[% HTML.escape(fa_bankbezeichnung_1) %]" [% readonly %]>
                           <br>
                           <br>
                           [% 'Account Nummer' | $T8 %]
                           <br>
-                          <input name="FA_Kontonummer_1" size="15"
-                            value="[% HTML.escape(FA_Kontonummer_1) %]" [% readonly %]>
+                          <input name="fa_kontonummer_1" size="15"
+                            value="[% HTML.escape(fa_kontonummer_1) %]" [% readonly %]>
                           <br>
                           <br>
                           [% 'Bank Code (long)' | $T8 %]
                           <br>
-                          <input name="FA_BLZ_1" size="15"
-                            value="[% HTML.escape(FA_BLZ_1) %]" [% readonly %]>
+                          <input name="fa_blz_1" size="15"
+                            value="[% HTML.escape(fa_blz_1) %]" [% readonly %]>
                         </td>
                         <td width="40%">
                           [% 'Bank' | $T8 %]
                           <br>
-                          <input name="FA_Bankbezeichnung_oertlich" size="30"
-                            value="[% HTML.escape(FA_Bankbezeichnung_oertlich) %]" [% readonly %]>
+                          <input name="fa_bankbezeichnung_2" size="30"
+                            value="[% HTML.escape(fa_bankbezeichnung_2) %]" [% readonly %]>
                           <br>
                           <br>
                           [% 'Account Nummer' | $T8 %]
                           <br>
-                          <input name="FA_Kontonummer_2" size="15"
-                            value="[% HTML.escape(FA_Kontonummer_2) %]" [% readonly %]>
+                          <input name="fa_kontonummer_2" size="15"
+                            value="[% HTML.escape(fa_kontonummer_2) %]" [% readonly %]>
                           <br>
                           <br>
                           [% 'Bank Code (long)' | $T8 %]
                           <br>
-                          <input name="FA_BLZ_2" size="15"
-                            value="[% HTML.escape(FA_BLZ_2) %]" [% readonly %]>
+                          <input name="fa_blz_2" size="15"
+                            value="[% HTML.escape(fa_blz_2) %]" [% readonly %]>
                         </td>
-                   </tr>
+              </tr>
              </table>
            </fieldset>
          </td>
            [% input_steuernummer %]
 [%- ELSE %]
 [% 'Please enter the taxnumber in the client configuration.' | $T8 %]
-[% 'Current value:' | $T8 %] [% HTML.escape(myconfig_taxnumber) %]
+[% 'Current value:' | $T8 %] [% HTML.escape(MYCONFIG.taxnumber) %]
 [%- END %]
 
 
-           </H2><br>
+           <br>
            </fieldset>
            <br>
            <br>
            <hr>
          </td>
        </tr>
-       <tr>
-         <td align="left">
-
-          [%- IF callback %]
-           <input type="button" name="Verweis" value="[% 'User Config' | $T8 %]"
-            onClick="self.location.href='[% callback %]">
-          [%- ELSE %]
-            <input type="submit" class="submit" name="action" value="[% 'back' | $T8 %]">
-          [%- END %]
-
-          [%- IF warnung %]
-
-            <input type="hidden" name="nextsub" value="config_step2">
-            <input type="submit" class="submit" name="action"
-              value="[% 'continue' | $T8 %]">
-
-            <input type="hidden" name="saved" value="[% 'Check
-              Details' | $T8 %]">
-
-          [%- ELSE %]
-
-            <input type="hidden" name="nextsub" value="save">
-            <input type="hidden" name="filename" value="finanzamt.ini">
-            <input type="submit" class="submit" name="action" value="[% 'Save' | $T8 %]">
-
-          [%- END %]
-
-         </td>
-         <td align="right">
-           <H2 class="confirm">[%- saved %]</H2>
-         </td>
-      </tr>
   </table>
 
 [%- FOREACH var = hidden_variables %]
diff --git a/templates/webpages/ustva/generic_taxreport.html b/templates/webpages/ustva/generic_taxreport.html
deleted file mode 100644 (file)
index 53946be..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-
-<h1>[% 'Generic Tax Report' | $T8 %]</h1>
-<p>[% 'Taxnumber' | $T8 %]: [% taxnumber %]</p>
-<p>[% 'Year' | $T8 %]: [% year %]</p>
-<p>[% 'Period' | $T8 %]: [% period %]</p>
-<br />
-<table width="33%">
-  <tr>
-    <th>[% 'Tax Position' | $T8 %]</th>
-    <th>[% 'Amount' | $T8 %]</th>
-  </tr>
-[% FOREACH row = USTVA %]
-  <tr class="listrow[% loop.count % 2 %]">
-
-    <td align="left">[% HTML.escape(row.id) %]</td>
-    <td align="right">[% HTML.escape(row.amount) %]</td>
-  </tr>
-[% END %]
-
-</table>
-
index 998bbfd..4a9f5eb 100644 (file)
@@ -1,9 +1,10 @@
 [%- USE T8 %]
-[% USE HTML %]
+[% USE HTML %][%- USE L -%]
 <h1>[% HTML.escape(title) %]</h1>
 
- <form method="post" action="[% HTML.escape(script) %]">
+ <form method="post" id="form_do" action="[% HTML.escape(script) %]">
 
+ [%- INCLUDE 'common/flash.html' %]
  <input type="hidden" name="title" value="[% HTML.escape(title) %]">
 
  <table width="100%">
 [%- IF COA_Germany %]
             [% taxnumber_given %]
             <br />
-            [% 'ELSTER Tax Number' | $T8 %]:&nbsp;
-            [% HTML.escape(elstersteuernummer) %]
-            <br />
-            <br />
 [%- ELSE %]
             [% taxnumber %]
 [%- END %]
             </fieldset>
             <br />
 
-            [%- IF FA_steuerberater_name %]
+            [%- IF fa_steuerberater_name %]
               <fieldset>
               <legend>
                 <input checked="checked"
                  title="[% 'Assume Tax Consultant Data in Tax Computation?' | $T8 %]"
-                 name="FA_steuerberater"
+                 name="fa_steuerberater"
                  id="steuerberater"
                  class="checkbox"
                  type="checkbox"
                  &nbsp;<b>[% 'Tax Consultant' | $T8 %]</b>
                 </legend>
 
-                [% HTML.escape(FA_steuerberater_name) %]<br />
-                [% HTML.escape(FA_steuerberater_street) %]<br />
-                [% HTML.escape(FA_steuerberater_city) %]<br />
-                [% 'Tel.' | $T8 %] [% HTML.escape(FA_steuerberater_tel) %]<br />
+                [% HTML.escape(fa_steuerberater_name) %]<br />
+                [% HTML.escape(fa_steuerberater_street) %]<br />
+                [% HTML.escape(fa_steuerberater_city) %]<br />
+                [% 'Tel.' | $T8 %] [% HTML.escape(fa_steuerberater_tel) %]<br />
               </fieldset>
               <br />
             [%- END %]
             <b>[% 'Tax Period' | $T8 %]</b>
             </legend>
             [% select_year %] [% ustva_vorauswahl %]
+            <br />
             [% checkbox_kz_10 %]
+            [% checkbox_kz_22 %]
             <br />
-            [%- IF FA_voranmeld %]
+            [% checkbox_kz_29 %]
+            [% checkbox_kz_26 %]
+            <br />
+            [%- IF fa_voranmeld %]
               <br />
               [% period_local %]
-              [%- IF FA_dauerfrist %]
+              [%- IF fa_dauerfrist %]
                 [% 'With Extension Of Time' | $T8 %]
               [%- END %]
               <br />
               <legend>
                  <b>[% 'Tax Office' | $T8 %]</b>
               </legend>
-              <h3>[% HTML.escape(FA_Name) %]</h3>
+              <h3>[% fa_name_given %]</h3>
 
-              [% HTML.escape(FA_Strasse) %]
+              [% HTML.escape(fa_strasse) %]
               <br>
-              [% HTML.escape(FA_PLZ) %]&nbsp; &nbsp;[% HTML.escape(FA_Ort) %]
+              [% HTML.escape(fa_plz) %]&nbsp; &nbsp;[% HTML.escape(fa_ort) %]
               <br>
               <br>
               [% 'Tel' | $T8 %].:&nbsp;
-              [% HTML.escape(FA_Telefon) %]
+              [% HTML.escape(fa_telefon) %]
               <br>
               [% 'Fax' | $T8 %].:&nbsp;
-              [% HTML.escape(FA_Fax) %]
+              [% HTML.escape(fa_fax) %]
               <br>
               <br>
               <!-- Mailto alles Maskieren! -->
-              <a href="mailto:[% HTML.escape(FA_Email) %]
-                ?subject=&quot;[% HTML.escape(steuernummer) %]:&quot;
+              <a href="mailto:[% HTML.escape(fa_email) %]
+                ?subject=&quot;[% HTML.escape(taxnumber) %]:&quot;
                 &amp;bcc=&quot;[% HTML.escape(email) %]&quot;
                 &amp;body=&quot;Sehr%20geehrte%20Damen%20und%20Herren,
                 %0D%0A%0D%0A%0D%0AMit%20freundlichen%20Gr&uuml;&szlig;en
                 %0D%0A%0D%0A[% HTML.escape(signature) %]&quot;">
-              [% HTML.escape(FA_Email) %]
+              [% HTML.escape(fa_email) %]
               </a>
               <br>
-              <a href="[% FA_Internet %]">
-              [% HTML.escape(FA_Internet) %]
+              <a href="[% fa_internet %]">
+              [% HTML.escape(fa_internet) %]
               </a>
               <br>
               <br>
               <table>
               <tr>
 
-              [%- FOREACH row = "tax_office_banks" %]
+              [%- FOREACH row = tax_office_banks %]
                   <td width="40%">
                   [% HTML.escape(row.Bankbezeichnung) %]
                   <br>
               </table>
               <br>
               </fieldset>
-
-              <br>
-
-              <fieldset>
-              <legend>
-              <b>[% 'Outputformat' | $T8 %]</b>
-              </legend>
-[%- IF COA_Germany %]
-              [% select_options %]
-[%- ELSE %]
-              <SELECT name="format">
-                <option value="generic">[% 'Preview' | $T8 %]</option>
-              </SELECT>
-[%- END %]
-
-              </fieldset>
           </td>
         </tr>
       </table>
   <br>
   <input type="hidden" name="address" value="[% HTML.escape(address) %]">
   <input type="hidden" name="reporttype" value="custom">
+  <input type="hidden" name="co_zip" value="[% HTML.escape(co_zip) %]">
+  <input type="hidden" name="co_tel" value="[% HTML.escape(co_tel) %]">
+  <input type="hidden" name="co_email" value="[% HTML.escape(co_email) %]">
   <input type="hidden" name="co_street" value="[% HTML.escape(co_street) %]">
   <input type="hidden" name="co_city" value="[% HTML.escape(co_city) %]">
-  <table width="100%">
-  <tr>
-   <td align="left">
-     <input type=hidden name=nextsub value=generate_ustva>
-     <input type=submit class=submit name=action value="[% 'Show' | $T8 %]">
-   </td>
-   <td align="right">
-
-    </form>
-   </td>
-  </tr>
-  </table>
+  <input type="hidden" name="account_method" value="[% HTML.escape(account_method) %]">
+  <input type="hidden" name="fa_bufa_nr" value="[% HTML.escape(fa_bufa_nr) %]">
+  [% L.hidden_tag("format", "html") %]
+</form>
+[%- IF LXCONFIG.paths.geierlein_path %]
+<script type='text/javascript'>
+
+  function sendGeierlein () {
+    kivi.submit_ajax_form('controller.pl?action=ODGeierlein/send', $('#form_do'));
+    return false;
+  }
+
+  function openGeierlein(myimport) {
+    localStorage["geierlein.import"] = myimport;
+    var geierpath = '[% LXCONFIG.paths.geierlein_path %]';
+    window.open(geierpath + '/#importLocalStorage','_blank','');
+    return false;
+  }
+</script>
+[%- END %]
index 27a4331..8158c0a 100644 (file)
@@ -125,6 +125,22 @@ Vorsteuerabzug. </b><br />Ums&auml;tze nach &sect; 4 Nr. 8 bis 20 UStG</td>
       <td class="spalte"><span class="nodis">(Spalte 86 rechts)</span></td>
       <td class="betrag">[%pos_ustva_861%]</td>
     </tr>
+[% IF pos_ustva_81b_kivi || pos_ustva_86b_kivi %]
+    <tr>
+      <td class="text2">zum Steuersatz von 16 v.H.(2020 Konjunktur)</td>
+      <td class="spalte ausfuellen"><span class="nodis">(Spalte </span>81b-kivi<span class="nodis">)</span></td>
+      <td class="betrag ausfuellen" width="70">[%pos_ustva_81b_kivi%]<br></td>
+      <td class="spalte"><span class="nodis">(Informativ)</span></td>
+      <td class="betrag">[%pos_ustva_811b_kivi%]</td>
+    </tr>
+    <tr>
+      <td class="text2">zum Steuersatz von 5 v.H.(2020 Konjunktur)</td>
+      <td class="spalte ausfuellen"><span class="nodis">(Spalte </span>86b-kivi<span class="nodis">)</span></td>
+      <td class="betrag ausfuellen" width="70">[%pos_ustva_86b_kivi%]<br></td>
+      <td class="spalte"><span class="nodis">(Informativ)</span></td>
+      <td class="betrag">[%pos_ustva_861b_kivi%]</td>
+    </tr>
+[%END%]
     <tr>
       <td class="text2">andere Steuers&auml;tze</td>
       <td class="spalte ausfuellen"><span class="nodis"></span>35 <span class="nodis"></span></td>
index 8762646..b81cc79 100644 (file)
@@ -1,5 +1,5 @@
-[%- USE T8 %]
-[%- USE L %]
+[%- USE T8 %][%- USE L %][%- USE P -%]
+[%- SET style="width: 250px" %]
 <h1>[% title %]</h1>
 
 <form method=post name="search_invoice" action=[% script %]>
           <option value="description">[% 'Part' | $T8 %]</option>
           <option value="customername">[% 'Customer' | $T8 %]</option>
           <option value="country">[% 'Country' | $T8 %]</option>
-          <option value="partsgroup">[% 'Group' | $T8 %]</option>
+          <option value="partsgroup">[% 'Partsgroup' | $T8 %]</option>
           <option value="business">[% 'Customer type' | $T8 %]</option>
           <option value="salesman" selected="selected">[% 'Salesman' | $T8 %]</option>
           <option value="month">[% 'Month' | $T8 %]</option>
+          <option value="shipvia">[% 'Ship via' | $T8 %]</option>
         </select>
       </td>
       <td align=left><input name="l_headers_mainsort" class=checkbox type=checkbox value=Y checked> [% 'Heading' | $T8 %]</td>
@@ -32,7 +33,7 @@
           <option value="description">[% 'Part' | $T8 %]</option>
           <option value="customername">[% 'Customer' | $T8 %]</option>
           <option value="country">[% 'Country' | $T8 %]</option>
-          <option value="partsgroup">[% 'Group' | $T8 %]</option>
+          <option value="partsgroup">[% 'Partsgroup' | $T8 %]</option>
           <option value="business">[% 'Customer type' | $T8 %]</option>
           <option value="salesman">[% 'Salesman' | $T8 %]</option>
           <option value="month" selected="selected">[% 'Month' | $T8 %]</option>
 
     <tr>
       <th align=right>[% 'Customer' | $T8 %]</th>
-      <td>
-        [%- INCLUDE 'generic/multibox.html'
-          name          = 'customer',
-          default       = oldcustomer,
-          style         = 'width: 250px',
-          DATA          = ALL_VC,
-          id_sub        = 'vc_keys',
-          label_key     = 'name',
-          select        = vc_select,
-          limit         = vclimit,
-          show_empty    = 1,
-          allow_textbox = 1,
-          class         = 'initial_focus',
-        -%]
-      </td>
+      <td>[% P.input_tag("customer", "", class="initial_focus", style=style) %]</td>
 
       <th align="right" nowrap>[% 'Customer Number' | $T8 %]</th>
-      <td>
-        <input name="customernumber" size="20">
-      </td>
+      <td>[% P.input_tag("customernumber", "", style=style) %]</td>
     </tr>
 
     <tr>
       <th align=right nowrap>[% 'Department' | $T8 %]</th>
       <td>
-        [%- INCLUDE 'generic/multibox.html'
-          name          = 'department',
-          style         = 'width: 250px',
-          DATA          = ALL_DEPARTMENTS,
-          id_key        = 'id',
-          label_key     = 'description',
-          show_empty    = 1,
-          allow_textbox = 0,
-        -%]
+        [%- L.select_tag('department_id',
+                         ALL_DEPARTMENTS,
+                         title_key  = 'description',
+                         with_empty = 1,
+                         style      = style)
+      -%]
       </td>
 
       <th align="right">[% 'Project Number' | $T8 %]</th>
-      <td>
-        [%- INCLUDE 'generic/multibox.html'
-          name          =  'project_id',
-          style         = "width: 250px",
-          DATA          =  ALL_PROJECTS,
-          id_key        = 'id',
-          label_key     = 'projectnumber',
-          limit         = vclimit,
-          show_empty    = 1,
-          allow_textbox = 0,
-        -%]
-      </td>
+      <td>[% P.project.picker("project_id", "", active="both", valid="both", style=style) %]</td>
     </tr>
 
     <tr>
       <th align="right" nowrap>[% 'Part Number' | $T8 %]</th>
-      <td><input name="partnumber" size="20"></td>
+      <td>[% P.input_tag("partnumber", "", style=style) %]</td>
     </tr>
 
     <tr>
       <th align="right" nowrap>[% 'Part Description' | $T8 %]</th>
-      <td>
-        <input name="description" size="40">
-      </td>
+      <td>[% P.input_tag("description", "", style=style) %]</td>
     </tr>
 
     <tr>
-      <th align="right">[% 'Group' | $T8 %]</th>
-      <td>
-        [%- INCLUDE 'generic/multibox.html'
-          name          = 'partsgroup_id',
-          style         = 'width: 250px',
-          DATA          =  ALL_PARTSGROUPS,
-          id_key        = 'id',
-          label_key     = 'partsgroup',
-          show_empty    = 1,
-          allow_textbox = 0,
-        -%]
-      </td>
-
+      <th align="right">[% 'Partsgroup' | $T8 %]</th>
+      <td>[% P.select_tag("partsgroup_id", ALL_PARTSGROUPS, title_key="partsgroup", with_empty=1, style=style) %]</td>
       <td align="right" nowrap>[% 'Country' | $T8 %]</td>
-      <td><input name="country" size="20"></td>
+      <td>[% P.input_tag("country", "", style=style) %]</td>
     </tr>
 
     <tr>
       <th align="right">[% 'Employee' | $T8 %]</th>
-      <td>
-        [%- INCLUDE 'generic/multibox.html'
-          name          = 'employee_id',
-          style         = 'width: 250px',
-          DATA          =  ALL_EMPLOYEES,
-          id_key        = 'id',
-          label_sub     = 'employee_labels',
-          limit         = vclimit,
-          show_empty    = 1,
-          allow_textbox = 0,
-          default       = ' ',
-        -%]
-      </td>
+      <td>[% L.select_tag("employee_id", ALL_EMPLOYEES, title_key="safe_name", with_empty=1, style=style) %]</td>
 
       <th align="right">[% 'Salesman' | $T8 %]</th>
-      <td>
-        [%- INCLUDE 'generic/multibox.html'
-          name          = 'salesman_id',
-          style         = 'width: 250px',
-          DATA          =  ALL_SALESMEN,
-          id_key        = 'id',
-          label_sub     = 'salesman_labels',
-          limit         = vclimit,
-          show_empty    = 1,
-          allow_textbox = 0,
-        -%]
-      </td>
+      <td>[% L.select_tag("salesman_id", ALL_EMPLOYEES, title_key="safe_name", with_empty=1, style=style) %]</td>
     </tr>
 
     <tr>
       <th align="right">[% 'Customer type' | $T8 %]</th>
-      <td>
-        [%- INCLUDE 'generic/multibox.html'
-           name          =  'business_id',
-           style         = "width: 250px",
-           DATA          =  ALL_BUSINESS_TYPES,
-           id_key        = 'id',
-           label_key     = 'description',
-           limit         = vclimit,
-           show_empty    = 1,
-           allow_textbox = 0,
-        -%]
-      </td>
+      <td>[% L.select_tag("business_id", ALL_BUSINESS_TYPES, title_key="description", with_empty=1, style=style) %]</td>
     </tr>
 
     <tr>
       <th align=right nowrap>[% 'Invoice Date' | $T8 %] [% 'From' | $T8 %]</th>
       <td>
         [% L.date_tag('transdatefrom') %]
-      </td>
-
-      <th align=right>[% 'Bis' | $T8 %]</th>
-
-      <td>
+        [% 'Bis' | $T8 %]
         [% L.date_tag('transdateto') %]
       </td>
     </tr>
 
           <tr>
             <td align=left><input name="l_parts_unit" class=checkbox type=checkbox value=Y>[% 'Base unit' | $T8 %]</td>
-            <td align=left><input name="l_partsgroup" class=checkbox type=checkbox value=Y>[% 'Group' | $T8 %]</td>
+            <td align=left><input name="l_partsgroup" class=checkbox type=checkbox value=Y>[% 'Partsgroup' | $T8 %]</td>
             <td align=left><input name="l_salesman" class=checkbox type=checkbox value=Y>[% 'Salesperson' | $T8 %]</td>
             <td align=left><input name="l_employee" class=checkbox type=checkbox value=Y>[% 'Employee' | $T8 %]</td>
           </tr>
             <td align=left><input name="l_country" class=checkbox type=checkbox value=Y>[% 'Country' | $T8 %]</td>
             <td align=left><input name="l_business" class=checkbox type=checkbox value=Y>[% 'Customer type' | $T8 %]</td>
           </tr>
-
+          <tr>
+            <td align=left><input name="l_shipvia" class=checkbox type=checkbox value=Y>[% 'Ship via' | $T8 %]</td>
+          </tr>
           <tr>
             <th colspan="4" align="left">
               [% 'Customer variables' | $T8 %] ([% 'Only shown in item mode' | $T8 %])
index 2f84d52..3869e09 100644 (file)
@@ -1,22 +1,31 @@
-[% USE HTML %][% USE T8 %]
+[% USE HTML %][% USE T8 %][%- USE LxERP -%]
 
 [%- IF INSTANCE_CONF.get_webdav %]
 <div id="ui-tabs-webdav">
 <div class="listtop">[%- 'Documents in the WebDAV repository' | $T8 %]</div>
 
- <table width="100%">
-  <tr>
-   <td align="left" width="30%"><b>[% 'File name' | $T8 %]</b></td>
-   <td align="left" width="70%"><b>[% 'WebDAV link' | $T8 %]</b></td>
-  </tr>
+[% IF WEBDAV && WEBDAV.size %]
+ <table>
+  <thead>
+   <tr class="listheading">
+    <th>[% 'File name' | $T8 %]</th>
+    <th>[% 'WebDAV link' | $T8 %]</th>
+   </tr>
+  </thead>
 
+  <tbody>
 [%- FOREACH file = WEBDAV %]
-  <tr>
-   <td align="left">[% HTML.escape(file.name) %]</td>
-   <td align="left"><a href="[% HTML.escape(file.link) %]">[% HTML.escape(file.type) %]</a></td>
-  </tr>
+   <tr class="listrow">
+    <td>[% HTML.escape(file.name) %]</td>
+    <td><a href="[% HTML.escape(file.link) %]">[% HTML.escape(file.type) %]</a></td>
+   </tr>
 [%- END %]
+  </tbody>
  </table>
+
+[% ELSE %]
+ <p>[% LxERP.t8("There are no documents in the WebDAV directory at the moment.") %]</p>
+[% END %]
 </div>
 
 [%- END %]
index 07ec2e7..694e1fd 100644 (file)
@@ -1,5 +1,6 @@
 [%- USE T8 %]
 [%- USE L %]
+[%- USE P %]
 [%- USE HTML %][%- USE JavaScript %]
 <h1>[% 'Report about warehouse transactions' | $T8 %]</h1>
 
      -->
  </script>
 
- <form method="post" name="Form" action="wh.pl">
+ <form method="post" name="Form" action="wh.pl" id="form">
 
-  <input type="hidden" name="nextsub" value="generate_journal">
-
-  <p>
    <table>
     <tr>
      <th class="listheading" align="left" valign="top" colspan="6" nowrap>[% 'Filter' | $T8 %]</th>
        </tr>
        <tr>
         <th align="right" nowrap>[% 'Part Number' | $T8 %]:</th>
-        <td><input name="partnumber" id="partnumber" size=20></td>
+        <td><input name="partnumber" id="partnumber" size=20 value="[% partnumber %]"></td>
+       </tr>
+       <tr>
+        <th align="right" nowrap>[% 'Parts Classification' | $T8 %]:</th>
+        <td>[% P.part.select_classification('classification_id') %]</td>
        </tr>
        <tr>
         <th align="right" nowrap>[% 'Part Description' | $T8 %]:</th>
           [% L.date_tag('todate') %]
         </td>
        </tr>
+       [% CUSTOM_VARIABLES_FILTER_CODE %]
       </table>
      </td>
     </tr>
        <tr>
         <td align="right"><input name="l_employee" id="l_employee" class="checkbox" type="checkbox" value="Y"></td>
         <td nowrap><label for="l_employee">[% 'Employee' | $T8 %]</label></td>
-        <td align="right"><input name="l_oe_id" id="l_oe_id" class="checkbox" type="checkbox" value="Y"></td>
+        <td align="right"><input name="l_oe_id" id="l_oe_id" class="checkbox" type="checkbox" value="Y" checked></td>
         <td nowrap><label for="l_oe_id">[% 'Document' | $T8 %]</label></td>
         <td align="right"><input name="l_projectnumber" id="l_projectnumber" class="checkbox" type="checkbox" value="Y" checked></td>
         <td nowrap><label for="l_projectnumber">[% 'Project Number' | $T8 %]</label></td>
        </tr>
       </table>
+      <table>
+       [% CUSTOM_VARIABLES_INCLUSION_CODE %]
+      </table>
      </td>
     </tr>
    </table>
-  </p>
-
-  <p>
-   <input type="submit" class="submit" name="action" value="[% 'Continue' | $T8 %]">
-  </p>
  </form>
-
index 5bc1e30..edc45d3 100644 (file)
@@ -2,9 +2,8 @@
 [%- USE HTML %][%- USE JavaScript %]
 <h1>[% title %]</h1>
 
- <form method="post" action="wh.pl">
+ <form method="post" action="wh.pl" id="form">
 
-  <input type="hidden" name="nextsub" value="remove_parts">
   <input type="hidden" name="warehouse_id" value="[% HTML.escape(warehouse_id) %]">
 
   <p>[% 'Removal from warehouse' | $T8 %]: [% warehouse_description %]</p>
@@ -78,9 +77,4 @@
 
    </table>
   </p>
-
-  <p>
-   <input type="submit" class="submit" name="action" value="[% 'Continue' | $T8 %]">
-  </p>
  </form>
-
diff --git a/templates/webpages/wh/report_bottom.html b/templates/webpages/wh/report_bottom.html
new file mode 100644 (file)
index 0000000..106943e
--- /dev/null
@@ -0,0 +1,3 @@
+[%- USE HTML %]
+ <input type="hidden" name="callback" value="[% HTML.escape(callback) %]">
+</form>
index 70fb4f1..f0635c1 100644 (file)
@@ -1,5 +1,7 @@
 [%- USE T8 %]
 [%- USE L %]
+[%- USE P %]
+[%- USE LxERP %]
 [%- USE HTML %][%- USE JavaScript %]
 <h1>[% 'Report about warehouse contents' | $T8 %]</h1>
 
      -->
  </script>
 
- <form method="post" name="Form" action="wh.pl">
+ <form method="post" name="Form" action="wh.pl" id="form">
 
   <input type="hidden" name="nextsub" value="generate_report">
 
-  <p>
    <table>
     <tr>
      <th class="listheading" align="left" valign="top" colspan="6" nowrap>[% 'Filter' | $T8 %]</th>
        </tr>
        <tr>
         <th align="right" nowrap>[% 'Part Number' | $T8 %]:</th>
-        <td><input name="partnumber" size=20></td>
+        <td><input name="partnumber" size=20 value="[% partnumber %]"></td>
+       </tr>
+       <tr>
+        <th align="right" nowrap>[% 'Parts Classification' | $T8 %]:</th>
+        <td>[% P.part.select_classification('classification_id') %]</td>
        </tr>
        <tr>
         <th align="right" nowrap>[% 'Part Description' | $T8 %]:</th>
         <td><input name="description" size=40></td>
        </tr>
+       <tr>
+        <th align="right" nowrap>[% 'Partsgroup' | $T8 %]:</th>
+        <td>[% L.select_tag('partsgroup_id', PARTSGROUPS, value_key = 'id', title_key = 'partsgroup', with_empty = 1) %]</td>
+       </tr>
        <tr>
         <th align="right" nowrap>[% 'Charge Number' | $T8 %]:</th>
         <td><input name="chargenumber" size=40></td>
          </select>
         </td>
        </tr>
+       <tr>
         <th align="right" nowrap>[% 'Stock Qty for Date' | $T8 %]:</th>
         <td>[% L.date_tag('date') %]</td>
+       </tr>
+        <tr>
+        <th align="right">
+          [% "basis for stock value" | $T8 %]:
+        </th>
+        <td align="left">
+         [% L.radio_button_tag("stock_value_basis", value='purchase_price', checked=1, label=LxERP.t8('Purchase price')) %]
+         [% L.radio_button_tag("stock_value_basis", value='list_price',     checked=0, label=LxERP.t8('List Price')) %]
+        </td>
+       </tr>
        <tr>
+        <th align="right">
+          [% "List all rows" | $T8 %]:
+        </th>
+        <td align="left">
+         [% L.yes_no_tag("allrows", 1) %]
+        </td>
        </tr>
+       <tr>
+        <th align="right">
+          [% "Results per page" | $T8 %]:
+        </th>
+        <td align="left">
+         [% L.input_number_tag("per_page", 20, size=4) %]
+        </td>
+       </tr>
+       [% CUSTOM_VARIABLES_FILTER_CODE %]
       </table>
      </td>
     </tr>
         [% END %]
        </tr>
 
-       <tr><td colspan="4"><hr noshade height="1"></td></tr>
+       <tr><td colspan="6"><hr noshade height="1"></td></tr>
 
        <tr>
         <td align="right"><input name="subtotal" id="subtotal" class="checkbox" type="checkbox" value="Y"></td>
        <tr>
         <td align="right"><input name="l_stock_value" id="l_stock_value" class="checkbox" type="checkbox" value="Y"></td>
         <td nowrap><label for="l_stock_value">[% 'Stock value' | $T8 %]</label></td>
+        <td align="right"><input name="l_purchase_price" id="l_purchase_price" class="checkbox" type="checkbox" value="Y"></td>
+        <td nowrap><label for="l_purchase_price">[% 'Purchase price' | $T8 %]</label></td>
+        <td align="right"><input name="l_list_price" id="l_list_price" class="checkbox" type="checkbox" value="Y"></td>
+        <td nowrap><label for="l_list_price">[% 'List Price' | $T8 %]</label></td>
        </tr>
-
+      </table>
+      <table>
+       [% CUSTOM_VARIABLES_INCLUSION_CODE %]
       </table>
      </td>
     </tr>
    </table>
-  </p>
-
-  <p>
-   <input type="submit" class="submit" name="action" value="[% 'Continue' | $T8 %]">
-  </p>
  </form>
-
diff --git a/templates/webpages/wh/report_top.html b/templates/webpages/wh/report_top.html
new file mode 100644 (file)
index 0000000..6198bd9
--- /dev/null
@@ -0,0 +1 @@
+<form method="post" action="wh.pl" id="form">
index 0f5d2f5..245da8b 100644 (file)
@@ -34,9 +34,8 @@
     -->
  </script>
 
- <form method="post" action="wh.pl">
+ <form method="post" action="wh.pl" id="form">
 
-  <input type="hidden" name="nextsub" value="transfer_parts">
   <input type="hidden" name="warehouse_id" value="[% HTML.escape(warehouse_id) %]">
 
   <p>[% 'Transfer from warehouse' | $T8 %]: [% warehouse_description %]</p>
 
    </table>
   </p>
-
-  <hr size="3" noshade>
-
-  <p>
-   <input type="submit" class="submit" name="action" value="[% 'Continue' | $T8 %]">
-  </p>
  </form>
-
index 6b48f40..efab904 100644 (file)
@@ -1,11 +1,9 @@
 [%- USE T8 %]
 [%- USE HTML %]
 [%- USE L %]
-[%- USE JavaScript %]
+[%- USE JavaScript %][%- USE P -%]
 <h1>[% title %]</h1>
 
- <script type="text/javascript" src="js/common.js"></script>
- <script type="text/javascript" src="js/part_selection.js"></script>
  <script type="text/javascript">
    <!--
       warehouses = new Array();
 
       $(function() {
         warehouse_selected(0, 0);
-        document.Form.partnumber.focus();
+        document.Form.part_id_name.focus();
       });
      -->
  </script>
 
- <form name="Form" method="post" action="wh.pl">
-
-  <input type="hidden" name="nextsub" value="[% HTML.escape(nextsub) %]">
+ <form name="Form" method="post" action="wh.pl" id="form">
 
   [% IF saved_message %]
   <p>[% saved_message %]</p>
     </tr>
 
     <tr>
-     <th align="right" nowrap>[% 'Part Number' | $T8 %]</th>
-     <td>
-      <input type="hidden" name="parts_id" id="parts_id">
-      <input name="partnumber" id="partnumber" size="30">
-     </td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Part Description' | $T8 %]</th>
+     <th align="right" nowrap>[% 'Part' | $T8 %]</th>
      <td>
-      <input name="description" size="30">
-      <input type="button" onclick="part_selection_window('partnumber', 'description', 'parts_id', 0, 'Form', '')" value="?">
+      [% P.part.picker("part_id", parts_id, size="30", part_type="part,assembly") %]
      </td>
     </tr>
 
     </tr>
    </table>
   </p>
-
-  <p>
-   <input type="submit" class="submit" name="action" value="[% 'Continue' | $T8 %]">
-  </p>
  </form>
-
index 17a6f9d..fba6b71 100644 (file)
@@ -1,10 +1,8 @@
 [%- USE T8 %]
 [%- USE L %]
-[%- USE HTML %][%- USE JavaScript %][%- USE LxERP %]
+[%- USE HTML %][%- USE JavaScript %][%- USE LxERP %][%- USE P -%]
 <h1>[% title %]</h1>
 
- <script type="text/javascript" src="js/common.js"></script>
- <script type="text/javascript" src="js/part_selection.js"></script>
  <script type="text/javascript">
   <!--
       warehouses = new Array();
     -->
  </script>
 
- <form name="Form" method="post" action="wh.pl">
-
-  <input type="hidden" name="nextsub" value="transfer_assembly">
-  <input type="hidden" name="update_nextsub" value="transfer_assembly_update_part">
+ <form name="Form" method="post" action="wh.pl" id="form">
 
   [% IF saved_message %]
   <p>[% saved_message %]</p>
 
   <p>
    <table>
+    <tr>
+     <th align="right" nowrap>[% 'Assembly' | $T8 %]</th>
+     <td>
+      [% P.part.picker("parts_id", parts_id, part_type="assembly", class="initial_focus", fat_set_item="1") %]
+     </td>
+    </tr>
+
     <tr>
      <th align="right" nowrap>[% 'Destination warehouse' | $T8 %]</th>
      <td>
-      <select name="warehouse_id" onchange="warehouse_selected(warehouses[this.selectedIndex]['id'], 0)">
+      <select name="warehouse_id" id="warehouse_id" onchange="warehouse_selected(warehouses[this.selectedIndex]['id'], 0)">
        [%- FOREACH warehouse = WAREHOUSES %]
        <option value="[% HTML.escape(warehouse.id) %]"[% IF warehouse_id == warehouse.id %] selected[% END %]>[% warehouse.description %]</option>
        [%- END %]
      <td><select id="bin_id" name="bin_id"></select></td>
     </tr>
 
-    <tr>
-     <th align="right" nowrap>[% 'Assembly Number' | $T8 %]</th>
-     <td>
-      <input type="hidden" name="parts_id" id="parts_id" value="[% HTML.escape(parts_id) %]">
-      <input type="hidden" name="old_partnumber" id="old_partnumber" value="[% HTML.escape(partnumber) %]">
-      <input name="partnumber" size="30" value="[% HTML.escape(partnumber) %]">
-     </td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Assembly Description' | $T8 %]</th>
-     <td>
-      <input name="description" size="30" value="[% HTML.escape(description) %]">
-      <input type="button" onclick="part_selection_window('partnumber', 'description', 'parts_id', 0, 'Form', 'assemblies:click_button=update_button')" value="?">
-     </td>
-    </tr>
-
     <tr>
      <th align="right" nowrap>[% 'Charge number' | $T8 %]</th>
      <td><input name="chargenumber" size="30" value="[% HTML.escape(chargenumber) %]"></td>
 
    </table>
   </p>
-
-  <p>
-   <input type="submit" class="submit" name="action" id="update_button" value="[% 'Update' | $T8 %]">
-   [%- IF parts_id %]
-   <input type="submit" class="submit" name="action" value="[% 'Create Assembly' | $T8 %]">
-   [%- END %]
-  </p>
  </form>
 
+<script type='text/javascript'>
+$(function(){
+  $('#parts_id').on('set_item:PartPicker', function(event, item) {
+    if (!item.warehouse_id)
+      return;
+
+    $('#warehouse_id').val(item.warehouse_id);
+    warehouse_selected(item.warehouse_id, item.bin_id);
+  });
+})
+</script>
diff --git a/templates/webpages/wh/warehouse_selection_stock.html b/templates/webpages/wh/warehouse_selection_stock.html
deleted file mode 100644 (file)
index d77f8c5..0000000
+++ /dev/null
@@ -1,148 +0,0 @@
-[%- USE T8 %]
-[%- USE L %]
-[%- USE HTML %][%- USE JavaScript %][%- USE LxERP %]
-<h1>[% title %]</h1>
-
- <script type="text/javascript" src="js/common.js"></script>
- <script type="text/javascript" src="js/part_selection.js"></script>
- <script type="text/javascript">
-  <!--
-      warehouses = new Array();
-      [%- USE WAREHOUSES_it = Iterator(WAREHOUSES) %][%- FOREACH warehouse = WAREHOUSES_it %]
-      warehouses[[% WAREHOUSES_it.count - 1 %]] = new Array();
-      warehouses[[% WAREHOUSES_it.count - 1 %]]['id'] = [% warehouse.id %];
-      warehouses[[% WAREHOUSES_it.count - 1 %]]['bins'] = new Array();
-      [% USE BINS_it = Iterator(warehouse.BINS) %][% FOREACH bin = BINS_it %]
-      warehouses[[% WAREHOUSES_it.count - 1%]]['bins'][[% BINS_it.count - 1 %]] = new Array();
-      warehouses[[% WAREHOUSES_it.count - 1%]]['bins'][[% BINS_it.count - 1 %]]['description'] = "[% JavaScript.escape(bin.description) %]";
-      warehouses[[% WAREHOUSES_it.count - 1%]]['bins'][[% BINS_it.count - 1 %]]['id'] = [% bin.id %];
-      [% END %]
-      [% END %]
-
-      function warehouse_selected(warehouse_id, bin_id) {
-        var control = document.getElementById("bin_id");
-
-        for (var i = control.options.length - 1; i >= 0; i--) {
-          control.options[i] = null;
-        }
-
-        var warehouse_index = 0;
-
-        for (i = 0; i < warehouses.length; i++)
-          if (warehouses[i]['id'] == warehouse_id) {
-            warehouse_index = i;
-            break;
-          }
-
-        var warehouse = warehouses[warehouse_index];
-        var bin_index = 0;
-
-        for (i = 0; i < warehouse['bins'].length; i++)
-          if (warehouse['bins'][i]['id'] == bin_id) {
-            bin_index = i;
-            break;
-          }
-
-        for (i = 0; i < warehouse['bins'].length; i++) {
-          control.options[i] = new Option(warehouse['bins'][i]['description'], warehouse['bins'][i]['id']);
-        }
-
-
-        control.options[bin_index].selected = true;
-      }
-
-      $(function() {
-        warehouse_selected([% warehouse_id %], [% bin_id %]);
-      })
-    -->
- </script>
-
- <form name="Form" method="post" action="wh.pl">
-
-  <input type="hidden" name="nextsub" value="transfer_stock">
-  <input type="hidden" name="update_nextsub" value="transfer_stock_update_part">
-
-  [% IF saved_message %]
-  <p>[% saved_message %]</p>
-  [% END %]
-
-  <p>
-   <table>
-    <tr>
-     <th align="right" nowrap>[% 'Destination warehouse' | $T8 %]</th>
-     <td>
-      <select name="warehouse_id" onchange="warehouse_selected(warehouses[this.selectedIndex]['id'], 0)">
-       [%- FOREACH warehouse = WAREHOUSES %]
-       <option value="[% HTML.escape(warehouse.id) %]"[% IF warehouse_id == warehouse.id %] selected[% END %]>[% warehouse.description %]</option>
-       [%- END %]
-      </select>
-     </td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Destination bin' | $T8 %]:</th>
-     <td><select id="bin_id" name="bin_id"></select></td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Part Number' | $T8 %]</th>
-     <td>
-      <input type="hidden" name="parts_id" id="parts_id" value="[% HTML.escape(parts_id) %]">
-      <input type="hidden" name="old_partnumber" id="old_partnumber" value="[% HTML.escape(partnumber) %]">
-      <input name="partnumber" size="30" value="[% HTML.escape(partnumber) %]">
-     </td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Part Description' | $T8 %]</th>
-     <td>
-      <input name="description" size="30" value="[% HTML.escape(description) %]">
-      <input type="button" onclick="part_selection_window('partnumber', 'description', 'parts_id', 0, 'Form', 'click_button=update_button')" value="?">
-     </td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Charge number' | $T8 %]</th>
-     <td><input name="chargenumber" size="30" value="[% HTML.escape(chargenumber) %]"></td>
-    </tr>
-
-    [% IF INSTANCE_CONF.get_show_bestbefore %]
-    <tr>
-     <th align="right" nowrap>[% 'Best Before' | $T8 %]</th>
-     <td>
-       [% L.date_tag('bestbefore', bestbefore) %]
-     </td>
-    </tr>
-    [% END %]
-
-    <tr>
-     <th align="right" nowrap>[% 'EAN' | $T8 %]</th>
-     <td><input name="ean" size="30" value="[% HTML.escape(ean) %]"></td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Quantity' | $T8 %]</th>
-     <td>
-      <input name="qty" size="10" value="[% HTML.escape(LxERP.format_amount(qty)) %]">
-      <select name="unit">
-       [%- FOREACH unit = UNITS %]<option[% IF unit.selected %] selected[% END %]>[% HTML.escape(unit.name) %]</option>[% END %]
-      </select>
-     </td>
-    </tr>
-
-    <tr>
-     <th align="right" nowrap>[% 'Optional comment' | $T8 %]</th>
-     <td><input name="comment" size="60" value="[% HTML.escape(comment) %]"></td>
-    </tr>
-
-   </table>
-  </p>
-
-  <p>
-   <input type="submit" class="submit" name="action" id="update_button" value="[% 'Update' | $T8 %]">
-   [%- IF parts_id %]
-   <input type="submit" class="submit" name="action" value="[% 'Stock' | $T8 %]">
-   [%- END %]
-  </p>
- </form>
-
diff --git a/templates/webpages/yearend/_charts.html b/templates/webpages/yearend/_charts.html
new file mode 100644 (file)
index 0000000..20f7621
--- /dev/null
@@ -0,0 +1,79 @@
+[%- USE LxERP -%]
+[%- USE T8    -%]
+[%- USE L     -%]
+[%- USE HTML  -%]
+[%- USE P     -%]
+
+
+[%- SET dec = 2 %]
+
+<h2>[% 'Balance accounts' | $T8 %]</h2>
+<table cellpadding="3px">
+ <tr class="listheading">
+  <th            >[%- 'Account'          | $T8 %]</th>
+  <th            >[%- 'Description'      | $T8 %]</th>
+  <th colspan="2">[%- 'Starting Balance' | $T8 %]</th>
+  <th colspan="2">[%- 'Balance with CB'  | $T8 %]</th>
+  <th colspan="2">[%- 'Closing Balance'  | $T8 %]</th>
+ </tr>
+ <tr class="listheading">
+  <th></th>
+  <th></th>
+  <th>[%- 'Debit'  | $T8 %]</th>
+  <th>[%- 'Credit' | $T8 %]</th>
+  <th>[%- 'Debit'  | $T8 %]</th>
+  <th>[%- 'Credit' | $T8 %]</th>
+  <th>[%- 'Debit'  | $T8 %]</th>
+  <th>[%- 'Credit' | $T8 %]</th>
+ </tr>
+ [% FOREACH chart = charts %]
+   [%- NEXT UNLESS chart.account_type == 'asset_account' -%]
+ <tr id="tr_[% loop.count %]" class="listrow[% loop.count % 2 %]">
+  <td>                 [% chart.accno | html %]</td>
+  <td>                 [% chart.description | html %]</td>
+  <td class="numeric"> [% IF chart.ob_amount < 0      %]  [% LxERP.format_amount(chart.ob_amount * -1, dec)       %] [% END %]</td>
+  <td class="numeric"> [% IF chart.ob_amount > 0      %]  [% LxERP.format_amount(chart.ob_amount, dec)            %] [% END %]</td>
+  <td class="numeric"> [% IF chart.amount_with_cb < 0 %]  [% LxERP.format_amount(chart.amount_with_cb * -1, dec)  %] [% END %]</td>
+  <td class="numeric"> [% IF chart.amount_with_cb > 0 %]  [% LxERP.format_amount(chart.amount_with_cb, dec)       %] [% END %]</td>
+  [% # cb amounts: >/< are switched and cb_amounts are multiplied with -1. The closing balance as calculated by cb_amount negates the actual balance, but when displaying it as the closing balance we want to display it in the same form as the actual balance %]
+  <td class="numeric"> [% IF chart.cb_amount > 0 %]  [% LxERP.format_amount(chart.cb_amount *  1, dec) %] [% END %]</td>
+  <td class="numeric"> [% IF chart.cb_amount < 0 %]  [% LxERP.format_amount(chart.cb_amount * -1, dec) %] [% END %]</td>
+ </tr>
+ [% END %]
+</table>
+
+<h2>[% 'Profit and loss accounts' | $T8 %]</h2>
+
+<p>
+[% IF profit_loss_sum < 0 %] [% THEN %][% 'Loss' | $T8 %] [% ELSE %] [% 'Profit' | $T8 %] [% END %]:   
+[% LxERP.format_amount(profit_loss_sum, dec) %]
+</p>
+
+<table cellpadding="3px">
+ <tr class="listheading">
+  <th          >[%- 'Account'         | $T8 %]</th>
+  <th          >[%- 'Description'     | $T8 %]</th>
+  <th colspan=2>[%- 'Balance with CB' | $T8 %]</th>
+  <th colspan=2>[%- 'Closing Balance' | $T8 %]</th>
+ </tr>
+ <tr class="listheading">
+  <th></th>
+  <th></th>
+  <th>[%- 'Debit'  | $T8 %]</th>
+  <th>[%- 'Credit' | $T8 %]</th>
+  <th>[%- 'Debit'  | $T8 %]</th>
+  <th>[%- 'Credit' | $T8 %]</th>
+ </tr>
+ [% FOREACH chart = charts %]
+   [%- NEXT UNLESS chart.account_type == 'profit_loss_account' -%]
+ <tr id="tr_[% loop.count %]" class="listrow[% loop.count % 2 %]">
+  <td                >[% chart.accno | html %]</td>
+  <td                >[% chart.description | html %]</td>
+  <td class="numeric">[% IF chart.amount_with_cb < 0 %] [% LxERP.format_amount(chart.amount_with_cb * -1, dec) %] [% END %]</td>
+  <td class="numeric">[% IF chart.amount_with_cb > 0 %] [% LxERP.format_amount(chart.amount_with_cb, dec)      %] [% END %]</td>
+  <td class="numeric">[% IF chart.cb_amount > 0 %] [% LxERP.format_amount(chart.cb_amount *  1, dec) %] [% END %]</td>
+  <td class="numeric">[% IF chart.cb_amount < 0 %] [% LxERP.format_amount(chart.cb_amount * -1, dec)      %] [% END %]</td>
+ </tr>
+ [% END %]
+</table>
+[% # L.dump(charts) %]
diff --git a/templates/webpages/yearend/form.html b/templates/webpages/yearend/form.html
new file mode 100644 (file)
index 0000000..d28dfc1
--- /dev/null
@@ -0,0 +1,98 @@
+[%- USE HTML %]
+[%- USE T8 %]
+[%- USE L %]
+[%- USE LxERP %]
+
+<h1>[% title | html %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+[% IF carry_over_chart AND profit_chart AND loss_chart %] [% THEN %]
+<form id="filter" name="filter" method="post" action="controller.pl">
+<table>
+  <tr>
+    <td align="right">[% 'Year-end date' | $T8 %]</td>
+    <td>[% L.date_tag('cb_date', SELF.cb_date) %]</td>
+  </tr>
+  <tr class="startdate">
+   <td align="right">[% 'Startdate method' | $T8 %]</td>
+   <td>[% L.select_tag('balance_startdate_method', balance_startdate_method_options, value_key = 'value', title_key = 'title') %]</td>
+  </tr>
+  <tr class="startdate">
+    <td align="right">[% 'Start date' | $T8 %]</td>
+    <td>[% L.date_tag('cb_startdate', '', readonly=1) %]</td>
+  </tr>
+  <tr>
+    <td align="right">[% 'Carry over account for year-end closing' | $T8 %]</td>
+    <td>[% carry_over_chart.displayable_name | html %]</td>
+  </tr>
+  <tr>
+    <td align="right">[% 'Profit carried forward account' | $T8 %]</td>
+    <td>[% profit_chart.displayable_name | html %]</td>
+  </tr>
+  <tr>
+    <td align="right">[% 'Loss carried forward account' | $T8 %]</td>
+    <td>[% loss_chart.displayable_name | html %]</td>
+  </tr>
+</table>
+</form>
+[% ELSE %]
+  [% 'Please configure the carry over and profit and loss accounts for year-end closing in the client configuration!' | $T8 %]
+[% END %]
+
+[% # L.button_tag("refresh_charts();", LxERP.t8("Preview")) %]
+[% L.button_tag("year_end_bookings();", LxERP.t8("Apply year-end bookings"), id='apply_year_end_bookings_button', confirm=LxERP.t8("Are you sure?")) %]
+
+<div id="charts" style="padding-top: 20px">
+</div>
+
+<script type="text/javascript">
+
+  function get_startdate() {
+    $.get("controller.pl", {
+      action:                   'YearEndTransactions/get_start_date',
+      cb_date:                  $('#cb_date').val(),
+      balance_startdate_method: $('#balance_startdate_method').val()
+    }, kivi.eval_json_result)
+  }
+
+  function year_end_bookings() {
+    $.post("controller.pl", {
+      action:  'YearEndTransactions/year_end_bookings',
+      cb_date: $('#cb_date').val(),
+    }, kivi.eval_json_result)
+  }
+
+  function refresh_charts() {
+    var filterdata = $('#filter').serialize()
+    var url = './controller.pl?action=YearEndTransactions/update_charts&' + filterdata;
+    $.ajax({
+       url : url,
+       type: 'GET',
+       success: function(data){
+           $('#charts').html(data);
+       }
+    })
+  };
+
+$(function(){
+
+  $('#apply_year_end_bookings_button').hide();
+  $('.startdate').hide();
+
+  $('#balance_startdate_method').change(function(){
+    get_startdate();
+    setTimeout(function() {
+      refresh_charts();
+    }, 200);    
+  });
+
+  $('#cb_date').change(function(){
+    get_startdate();
+    setTimeout(function() {
+      refresh_charts();
+    }, 200);    
+  });
+})
+
+</script>
diff --git a/templates/webpages/zugferd/form.html b/templates/webpages/zugferd/form.html
new file mode 100644 (file)
index 0000000..49f88f5
--- /dev/null
@@ -0,0 +1,14 @@
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE T8 %]
+[%- INCLUDE 'common/flash.html' %]
+ <div class="listtop">[% FORM.title %]</div>
+
+ <p>
+ [% "Import a Factur-X/ZUGFeRD file:" | $T8 %]
+ </p>
+
+ <form method="post" action="controller.pl" enctype="multipart/form-data" id="form">
+    [% L.input_tag('file', '', type => 'file', accept => '.pdf') %]
+ </form>
diff --git a/texmf/embedfile.sty b/texmf/embedfile.sty
new file mode 100644 (file)
index 0000000..167dea1
--- /dev/null
@@ -0,0 +1,799 @@
+%% !!NOTE NOTE NOTE!!
+%%
+%% This is a modified version of `embedfile.sty' generated from a
+%% modified `embedfile.dtx' incorporating the following pull request:
+%% https://github.com/ho-tex/oberdiek/pull/72
+%%
+%% This PR adds support for creating PDF/A-compliant attachments. See
+%% also the following issue:
+%% https://github.com/ho-tex/oberdiek/issues/37
+%%
+%% !!END OF NOTE NOTE NOTE!!
+%%
+%%
+%% This is file `embedfile.sty',
+%% generated with the docstrip utility.
+%%
+%% The original source files were:
+%%
+%% embedfile.dtx  (with options: `package')
+%%
+%% This is a generated file.
+%%
+%% Project: embedfile
+%% Version: 2018/11/01 v2.8
+%%
+%% Copyright (C) 2006-2011 by
+%%    Heiko Oberdiek <heiko.oberdiek at googlemail.com>
+%%
+%% This work may be distributed and/or modified under the
+%% conditions of the LaTeX Project Public License, either
+%% version 1.3c of this license or (at your option) any later
+%% version. This version of this license is in
+%%    http://www.latex-project.org/lppl/lppl-1-3c.txt
+%% and the latest version of this license is in
+%%    http://www.latex-project.org/lppl.txt
+%% and version 1.3 or later is part of all distributions of
+%% LaTeX version 2005/12/01 or later.
+%%
+%% This work has the LPPL maintenance status "maintained".
+%%
+%% This Current Maintainer of this work is Heiko Oberdiek.
+%%
+%% The Base Interpreter refers to any `TeX-Format',
+%% because some files are installed in TDS:tex/generic//.
+%%
+%% This work consists of the main source file embedfile.dtx
+%% and the derived files
+%%    embedfile.sty, embedfile.pdf, embedfile.ins, embedfile.drv,
+%%    dtx-attach.sty, embedfile-example-plain.tex,
+%%    embedfile-example-collection.tex, embedfile-test1.tex,
+%%    embedfile-test2.tex, embedfile-test3.tex,
+%%    embedfile-test4.tex.
+%%
+\begingroup\catcode61\catcode48\catcode32=10\relax%
+  \catcode13=5 % ^^M
+  \endlinechar=13 %
+  \catcode35=6 % #
+  \catcode39=12 % '
+  \catcode44=12 % ,
+  \catcode45=12 % -
+  \catcode46=12 % .
+  \catcode58=12 % :
+  \catcode64=11 % @
+  \catcode123=1 % {
+  \catcode125=2 % }
+  \expandafter\let\expandafter\x\csname ver@embedfile.sty\endcsname
+  \ifx\x\relax % plain-TeX, first loading
+  \else
+    \def\empty{}%
+    \ifx\x\empty % LaTeX, first loading,
+      % variable is initialized, but \ProvidesPackage not yet seen
+    \else
+      \expandafter\ifx\csname PackageInfo\endcsname\relax
+        \def\x#1#2{%
+          \immediate\write-1{Package #1 Info: #2.}%
+        }%
+      \else
+        \def\x#1#2{\PackageInfo{#1}{#2, stopped}}%
+      \fi
+      \x{embedfile}{The package is already loaded}%
+      \aftergroup\endinput
+    \fi
+  \fi
+\endgroup%
+\begingroup\catcode61\catcode48\catcode32=10\relax%
+  \catcode13=5 % ^^M
+  \endlinechar=13 %
+  \catcode35=6 % #
+  \catcode39=12 % '
+  \catcode40=12 % (
+  \catcode41=12 % )
+  \catcode44=12 % ,
+  \catcode45=12 % -
+  \catcode46=12 % .
+  \catcode47=12 % /
+  \catcode58=12 % :
+  \catcode64=11 % @
+  \catcode91=12 % [
+  \catcode93=12 % ]
+  \catcode123=1 % {
+  \catcode125=2 % }
+  \expandafter\ifx\csname ProvidesPackage\endcsname\relax
+    \def\x#1#2#3[#4]{\endgroup
+      \immediate\write-1{Package: #3 #4}%
+      \xdef#1{#4}%
+    }%
+  \else
+    \def\x#1#2[#3]{\endgroup
+      #2[{#3}]%
+      \ifx#1\@undefined
+        \xdef#1{#3}%
+      \fi
+      \ifx#1\relax
+        \xdef#1{#3}%
+      \fi
+    }%
+  \fi
+\expandafter\x\csname ver@embedfile.sty\endcsname
+\ProvidesPackage{embedfile}%
+  [2018/11/01 v2.8 Embed files into PDF (HO)]%
+\begingroup\catcode61\catcode48\catcode32=10\relax%
+  \catcode13=5 % ^^M
+  \endlinechar=13 %
+  \catcode123=1 % {
+  \catcode125=2 % }
+  \catcode64=11 % @
+  \def\x{\endgroup
+    \expandafter\edef\csname EmFi@AtEnd\endcsname{%
+      \endlinechar=\the\endlinechar\relax
+      \catcode13=\the\catcode13\relax
+      \catcode32=\the\catcode32\relax
+      \catcode35=\the\catcode35\relax
+      \catcode61=\the\catcode61\relax
+      \catcode64=\the\catcode64\relax
+      \catcode123=\the\catcode123\relax
+      \catcode125=\the\catcode125\relax
+    }%
+  }%
+\x\catcode61\catcode48\catcode32=10\relax%
+\catcode13=5 % ^^M
+\endlinechar=13 %
+\catcode35=6 % #
+\catcode64=11 % @
+\catcode123=1 % {
+\catcode125=2 % }
+\def\TMP@EnsureCode#1#2{%
+  \edef\EmFi@AtEnd{%
+    \EmFi@AtEnd
+    \catcode#1=\the\catcode#1\relax
+  }%
+  \catcode#1=#2\relax
+}
+\TMP@EnsureCode{39}{12}% '
+\TMP@EnsureCode{40}{12}% (
+\TMP@EnsureCode{41}{12}% )
+\TMP@EnsureCode{44}{12}% ,
+\TMP@EnsureCode{46}{12}% .
+\TMP@EnsureCode{47}{12}% /
+\TMP@EnsureCode{58}{12}% :
+\TMP@EnsureCode{60}{12}% <
+\TMP@EnsureCode{62}{12}% >
+\TMP@EnsureCode{91}{12}% [
+\TMP@EnsureCode{93}{12}% ]
+\TMP@EnsureCode{96}{12}% `
+\edef\EmFi@AtEnd{\EmFi@AtEnd\noexpand\endinput}
+\begingroup\expandafter\expandafter\expandafter\endgroup
+\expandafter\ifx\csname RequirePackage\endcsname\relax
+  \def\EmFi@RequirePackage#1[#2]{%
+    \input #1.sty\relax
+  }%
+\else
+  \let\EmFi@RequirePackage\RequirePackage
+\fi
+\EmFi@RequirePackage{infwarerr}[2007/09/09]%
+\def\EmFi@Error{%
+  \@PackageError{embedfile}%
+}
+\ifx\pdfextension\@undefined\else
+    \protected\def\pdflastobj {\numexpr\pdffeedback lastobj\relax}
+    \protected\def\pdfnames   {\pdfextension names }
+    \protected\def\pdfobj     {\pdfextension obj }
+    \let\pdfoutput            \outputmode
+\fi
+\EmFi@RequirePackage{ifpdf}[2007/09/09]
+\ifpdf
+\else
+  \EmFi@Error{%
+    Missing pdfTeX in PDF mode%
+  }{%
+    Currently other drivers are not supported. %
+    Package loading is aborted.%
+  }%
+  \expandafter\EmFi@AtEnd
+\fi%
+\EmFi@RequirePackage{pdftexcmds}[2007/11/11]
+\EmFi@RequirePackage{ltxcmds}[2010/03/01]
+\EmFi@RequirePackage{kvsetkeys}[2010/03/01]
+\EmFi@RequirePackage{kvdefinekeys}[2010/03/01]
+\begingroup\expandafter\expandafter\expandafter\endgroup
+\expandafter\ifx\csname pdf@filesize\endcsname\relax
+  \EmFi@Error{%
+    Unsupported pdfTeX version%
+  }{%
+    At least version 1.30 is necessary. Package loading is aborted.%
+  }%
+  \expandafter\EmFi@AtEnd
+\fi%
+\EmFi@RequirePackage{pdfescape}[2007/11/11]
+\def\EmFi@temp#1{%
+  \expandafter\EdefSanitize\csname EmFi@S@#1\endcsname{#1}%
+}
+\EmFi@temp{details}%
+\EmFi@temp{tile}%
+\EmFi@temp{hidden}%
+\EmFi@temp{text}
+\EmFi@temp{date}
+\EmFi@temp{number}
+\EmFi@temp{file}
+\EmFi@temp{desc}
+\EmFi@temp{afrelationship}
+\EmFi@temp{moddate}
+\EmFi@temp{creationdate}
+\EmFi@temp{size}
+\EmFi@temp{ascending}
+\EmFi@temp{descending}
+\EmFi@temp{true}
+\EmFi@temp{false}
+\ltx@newif\ifEmFi@collection
+\ltx@newif\ifEmFi@sort
+\ltx@newif\ifEmFi@visible
+\ltx@newif\ifEmFi@edit
+\ltx@newif\ifEmFi@item
+\ltx@newif\ifEmFi@finished
+\ltx@newif\ifEmFi@id
+\def\EmFi@GlobalKey#1#2{%
+  \global\expandafter\let\csname KV@#1@#2\expandafter\endcsname
+                         \csname KV@#1@#2\endcsname
+}
+\def\EmFi@GlobalDefaultKey#1#2{%
+  \EmFi@GlobalKey{#1}{#2}%
+  \global\expandafter\let
+      \csname KV@#1@#2@default\expandafter\endcsname
+      \csname KV@#1@#2@default\endcsname
+}
+\def\EmFi@DefineKey#1#2{%
+  \kv@define@key{EmFi}{#1}{%
+    \expandafter\def\csname EmFi@#1\endcsname{##1}%
+  }%
+  \expandafter\def\csname EmFi@#1\endcsname{#2}%
+}
+\EmFi@DefineKey{mimetype}{}
+\EmFi@DefineKey{filespec}{\EmFi@file}
+\EmFi@DefineKey{ucfilespec}{}
+\EmFi@DefineKey{filesystem}{}
+\EmFi@DefineKey{desc}{}
+\EmFi@DefineKey{afrelationship}{}
+\EmFi@DefineKey{stringmethod}{%
+  \ifx\pdfstringdef\@undefined
+    escape%
+  \else
+    \ifx\pdfstringdef\relax
+      escape%
+    \else
+      psd%
+    \fi
+  \fi
+}
+\kv@define@key{EmFi}{id}{%
+  \def\EmFi@id{#1}%
+  \EmFi@idtrue
+}
+\def\EmFi@defobj#1{%
+  \ifEmFi@id
+    \expandafter\xdef\csname EmFi@#1@\EmFi@id\endcsname{%
+      \the\pdflastobj\ltx@space 0 R%
+    }%
+  \fi
+}
+\def\embedfileifobjectexists#1#2{%
+  \expandafter\ifx\csname EmFi@#2@#1\endcsname\relax
+    \expandafter\ltx@secondoftwo
+  \else
+    \expandafter\ltx@firstoftwo
+  \fi
+}
+\def\embedfilegetobject#1#2{%
+  \embedfileifobjectexists{#1}{#2}{%
+    \csname EmFi@#2@#1\endcsname
+  }{%
+    0 0 R%
+  }%
+}
+\kv@define@key{EmFi}{view}[]{%
+  \EdefSanitize\EmFi@temp{#1}%
+  \def\EmFi@next{%
+    \global\EmFi@collectiontrue
+  }%
+  \ifx\EmFi@temp\ltx@empty
+    \let\EmFi@view\EmFi@S@details
+  \else\ifx\EmFi@temp\EmFi@S@details
+    \let\EmFi@view\EmFi@S@details
+  \else\ifx\EmFi@temp\EmFi@S@tile
+    \let\EmFi@view\EmFi@S@tile
+  \else\ifx\EmFi@temp\EmFi@S@hidden
+    \let\EmFi@view\EmFi@S@hidden
+  \else
+    \let\EmFi@next\relax
+    \EmFi@Error{%
+      Unknown value `\EmFi@temp' for key `view'.\MessageBreak
+      Supported values: `details', `tile', `hidden'.%
+    }\@ehc
+  \fi\fi\fi\fi
+  \EmFi@next
+}
+\EmFi@DefineKey{initialfile}{}
+\def\embedfilesetup{%
+  \ifEmFi@finished
+    \def\EmFi@next##1{}%
+    \EmFi@Error{%
+      \string\embedfilefield\ltx@space after \string\embedfilefinish
+    }{%
+      The list of embedded files is already written.%
+    }%
+  \else
+    \def\EmFi@next{%
+      \kvsetkeys{EmFi}%
+    }%
+  \fi
+  \EmFi@next
+}
+\def\EmFi@schema{}
+\gdef\EmFi@order{0}
+\let\EmFi@@order\relax
+\def\EmFi@fieldlist{}
+\def\EmFi@sortcase{0}%
+\def\embedfilefield#1#2{%
+  \ifEmFi@finished
+    \EmFi@Error{%
+      \string\embedfilefield\ltx@space after \string\embedfilefinish
+    }{%
+      The list of embedded files is already written.%
+    }%
+  \else
+    \global\EmFi@collectiontrue
+    \EdefSanitize\EmFi@key{#1}%
+    \expandafter\ifx\csname KV@EmFi@\EmFi@key.prefix\endcsname\relax
+      \begingroup
+        \count@=\EmFi@order
+        \advance\count@ 1 %
+        \xdef\EmFi@order{\the\count@}%
+        \let\EmFi@title\EmFi@key
+        \let\EmFi@type\EmFi@S@text
+        \EmFi@visibletrue
+        \EmFi@editfalse
+        \kvsetkeys{EmFiFi}{#2}%
+        \EmFi@convert\EmFi@title\EmFi@title
+        \xdef\EmFi@schema{%
+          \EmFi@schema
+          /\pdf@escapename{\EmFi@key}<<%
+            /Subtype/%
+            \ifx\EmFi@type\EmFi@S@date D%
+            \else\ifx\EmFi@type\EmFi@S@number N%
+            \else\ifx\EmFi@type\EmFi@S@file F%
+            \else\ifx\EmFi@type\EmFi@S@desc Desc%
+            \else\ifx\EmFi@type\EmFi@S@afrelationship AFRelationship%
+            \else\ifx\EmFi@type\EmFi@S@moddate ModDate%
+            \else\ifx\EmFi@type\EmFi@S@creationdate CreationDate%
+            \else\ifx\EmFi@type\EmFi@S@size Size%
+            \else S%
+            \fi\fi\fi\fi\fi\fi\fi
+            /N(\EmFi@title)%
+            \EmFi@@order{\EmFi@order}%
+            \ifEmFi@visible
+            \else
+              /V false%
+            \fi
+            \ifEmFi@edit
+              /E true%
+            \fi
+          >>%
+        }%
+        \let\do\relax
+        \xdef\EmFi@fieldlist{%
+          \EmFi@fieldlist
+          \do{\EmFi@key}%
+        }%
+        \ifx\EmFi@type\EmFi@S@text
+          \kv@define@key{EmFi}{\EmFi@key.value}{%
+            \EmFi@itemtrue
+            \def\EmFi@temp{##1}%
+            \EmFi@convert\EmFi@temp\EmFi@temp
+            \expandafter\def\csname EmFi@V@#1%
+            \expandafter\endcsname\expandafter{%
+              \expandafter(\EmFi@temp)%
+            }%
+          }%
+          \EmFi@GlobalKey{EmFi}{\EmFi@key.value}%
+        \else\ifx\EmFi@type\EmFi@S@date
+          \kv@define@key{EmFi}{\EmFi@key.value}{%
+            \EmFi@itemtrue
+            \def\EmFi@temp{##1}%
+            \EmFi@convert\EmFi@temp\EmFi@temp
+            \expandafter\def\csname EmFi@V@#1%
+            \expandafter\endcsname\expandafter{%
+              \expandafter(\EmFi@temp)%
+            }%
+          }%
+          \EmFi@GlobalKey{EmFi}{\EmFi@key.value}%
+        \else\ifx\EmFi@type\EmFi@S@number
+          \kv@define@key{EmFi}{\EmFi@key.value}{%
+            \EmFi@itemtrue
+            \expandafter\EdefSanitize\csname EmFi@V@#1\endcsname{ ##1}%
+          }%
+          \EmFi@GlobalKey{EmFi}{\EmFi@key.value}%
+        \fi\fi\fi
+        \kv@define@key{EmFi}{\EmFi@key.prefix}{%
+          \EmFi@itemtrue
+          \expandafter\def\csname EmFi@P@#1\endcsname{##1}%
+        }%
+        \EmFi@GlobalKey{EmFi}{\EmFi@key.prefix}%
+        \kv@define@key{EmFiSo}{\EmFi@key}[ascending]{%
+          \EdefSanitize\EmFi@temp{##1}%
+          \ifx\EmFi@temp\EmFi@S@ascending
+            \def\EmFi@temp{true}%
+          \else\ifx\EmFi@temp\EmFi@S@descending
+            \def\EmFi@temp{false}%
+          \else
+            \def\EmFi@temp{}%
+            \EmFi@Error{%
+              Unknown sort order `\EmFi@temp'.\MessageBreak
+              Supported values: `\EmFi@S@ascending', %
+              `\EmFi@S@descending
+            }\@ehc
+          \fi\fi
+          \ifx\EmFi@temp\ltx@empty
+          \else
+            \xdef\EmFi@sortkeys{%
+              \EmFi@sortkeys
+              /\pdf@escapename{#1}%
+            }%
+            \ifx\EmFi@sortorders\ltx@empty
+              \global\let\EmFi@sortorders\EmFi@temp
+              \gdef\EmFi@sortcase{1}%
+            \else
+              \xdef\EmFi@sortorders{%
+                \EmFi@sortorders
+                \ltx@space
+                \EmFi@temp
+              }%
+              \xdef\EmFi@sortcase{2}%
+            \fi
+          \fi
+        }%
+        \EmFi@GlobalDefaultKey{EmFiSo}\EmFi@key
+      \endgroup
+    \else
+      \EmFi@Error{%
+        Field `\EmFi@key' is already defined%
+      }\@ehc
+    \fi
+  \fi
+}
+\kv@define@key{EmFiFi}{type}{%
+  \EdefSanitize\EmFi@temp{#1}%
+  \ifx\EmFi@temp\EmFi@S@text
+    \let\EmFi@type\EmFi@temp
+  \else\ifx\EmFi@temp\EmFi@S@date
+    \let\EmFi@type\EmFi@temp
+  \else\ifx\EmFi@temp\EmFi@S@number
+    \let\EmFi@type\EmFi@temp
+  \else\ifx\EmFi@temp\EmFi@S@file
+    \let\EmFi@type\EmFi@temp
+  \else\ifx\EmFi@temp\EmFi@S@desc
+    \let\EmFi@type\EmFi@temp
+  \else\ifx\EmFi@temp\EmFi@S@afrelationship
+    \let\EmFi@type\EmFi@temp
+  \else\ifx\EmFi@temp\EmFi@S@moddate
+    \let\EmFi@type\EmFi@temp
+  \else\ifx\EmFi@temp\EmFi@S@creationdate
+    \let\EmFi@type\EmFi@temp
+  \else\ifx\EmFi@temp\EmFi@S@size
+    \let\EmFi@type\EmFi@temp
+  \else
+    \EmFi@Error{%
+      Unknown type `\EmFi@temp'.\MessageBreak
+      Supported types: `text', `date', `number', `file',\MessageBreak
+      `desc', `afrelationship', `moddate', `creationdate', `size'%
+    }%
+  \fi\fi\fi\fi\fi\fi\fi\fi\fi
+}
+\kv@define@key{EmFiFi}{title}{%
+  \def\EmFi@title{#1}%
+}
+\def\EmFi@setboolean#1#2{%
+  \EdefSanitize\EmFi@temp{#2}%
+  \ifx\EmFi@temp\EmFi@S@true
+    \csname EmFi@#1true\endcsname
+  \else
+    \ifx\EmFi@temp\EmFi@S@false
+      \csname EmFi@#1false\endcsname
+    \else
+      \EmFi@Error{%
+        Unknown value `\EmFi@temp' for key `#1'.\MessageBreak
+        Supported values: `true', `false'%
+      }\@ehc
+    \fi
+  \fi
+}
+\kv@define@key{EmFiFi}{visible}[true]{%
+  \EmFi@setboolean{visible}{#1}%
+}
+\kv@define@key{EmFiFi}{edit}[true]{%
+  \EmFi@setboolean{edit}{#1}%
+}
+\def\EmFi@sortkeys{}
+\def\EmFi@sortorders{}
+\def\embedfilesort{%
+  \kvsetkeys{EmFiSo}%
+}
+\def\embedfile{%
+  \ltx@ifnextchar[\EmFi@embedfile{\EmFi@embedfile[]}%
+}
+\def\EmFi@embedfile[#1]#2{%
+  \ifEmFi@finished
+    \EmFi@Error{%
+      \string\embedfile\ltx@space after \string\embedfilefinish
+    }{%
+      The list of embedded files is already written.%
+    }%
+  \else
+    \begingroup
+      \def\EmFi@file{#2}%
+      \kvsetkeys{EmFi}{#1}%
+      \expandafter\expandafter\expandafter
+      \ifx\expandafter\expandafter\expandafter
+          \\\pdf@filesize{\EmFi@file}\\%
+        \EmFi@Error{%
+          File `\EmFi@file' not found%
+        }{%
+          The unknown file is not embedded.%
+        }%
+      \else
+        \edef\EmFi@@filespec{%
+          \pdf@escapestring{\EmFi@filespec}%
+        }%
+        \ifx\EmFi@ucfilespec\ltx@empty
+          \let\EmFi@@ucfilespec\ltx@empty
+        \else
+          \EmFi@convert\EmFi@ucfilespec\EmFi@@ucfilespec
+        \fi
+        \ifx\EmFi@desc\ltx@empty
+          \let\EmFi@@desc\ltx@empty
+        \else
+          \EmFi@convert\EmFi@desc\EmFi@@desc
+        \fi
+        \ifx\EmFi@afrelationship\ltx@empty
+          \let\EmFi@@afrelationship\ltx@empty
+        \else
+          \EmFi@convert\EmFi@afrelationship\EmFi@@afrelationship
+        \fi
+        \ifEmFi@item
+          \let\do\EmFi@do
+          \immediate\pdfobj{%
+            <<%
+              \EmFi@fieldlist
+            >>%
+          }%
+          \edef\EmFi@ci{\the\pdflastobj}%
+        \fi
+        \immediate\pdfobj stream attr{%
+          /Type/EmbeddedFile%
+          \ifx\EmFi@mimetype\ltx@empty
+          \else
+            /Subtype/\pdf@escapename{\EmFi@mimetype}%
+          \fi
+          /Params<<%
+            /ModDate(\pdf@filemoddate{\EmFi@file})%
+            /Size \pdf@filesize{\EmFi@file}%
+            /CheckSum<\pdf@filemdfivesum{\EmFi@file}>%
+          >>%
+        }file{\EmFi@file}\relax
+        \EmFi@defobj{EmbeddedFile}%
+        \immediate\pdfobj{%
+          <<%
+            /Type/Filespec%
+            \ifx\EmFi@filesystem\ltx@empty
+            \else
+            /FS/\pdf@escapename{\EmFi@filesystem}%
+            \fi
+            /F(\EmFi@@filespec)%
+            \ifx\EmFi@@ucfilespec\ltx@empty
+            \else
+              /UF(\EmFi@@ucfilespec)%
+            \fi
+            \ifx\EmFi@@desc\ltx@empty
+            \else
+              /Desc(\EmFi@@desc)%
+            \fi
+            \ifx\EmFi@@afrelationship\ltx@empty
+            \else
+              /AFRelationship\EmFi@@afrelationship%
+            \fi
+            /EF<<%
+              /F \the\pdflastobj\ltx@space 0 R%
+            >>%
+            \ifEmFi@item
+              /CI \EmFi@ci\ltx@space 0 R%
+            \fi
+          >>%
+        }%
+        \EmFi@defobj{Filespec}%
+        \EmFi@add{%
+          \EmFi@@filespec
+        }{\the\pdflastobj\ltx@space 0 R}%
+      \fi
+    \endgroup
+  \fi
+}
+\def\EmFi@do#1{%
+  \expandafter\ifx\csname EmFi@P@#1\endcsname\relax
+    \expandafter\ifx\csname EmFi@V@#1\endcsname\relax
+    \else
+      /\pdf@escapename{#1}\csname EmFi@V@#1\endcsname
+    \fi
+  \else
+    /\pdf@escapename{#1}<<%
+      \expandafter\ifx\csname EmFi@V@#1\endcsname\relax
+      \else
+        /D\csname EmFi@V@#1\endcsname
+      \fi
+      /P(\csname EmFi@P@#1\endcsname)%
+    >>%
+  \fi
+}
+\def\EmFi@convert#1#2{%
+  \ifnum\pdf@strcmp{\EmFi@stringmethod}{psd}=0 %
+    \pdfstringdef\EmFi@temp{#1}%
+    \let#2\EmFi@temp
+  \else
+    \edef#2{\pdf@escapestring{#1}}%
+  \fi
+}
+\global\let\EmFi@list\ltx@empty
+\def\EmFi@add#1#2{%
+  \begingroup
+    \ifx\EmFi@list\ltx@empty
+      \xdef\EmFi@list{\noexpand\do{#1}{#2}}%
+    \else
+      \def\do##1##2{%
+        \ifnum\pdf@strcmp{##1}{#1}>0 %
+          \edef\x{%
+            \toks@{%
+              \the\toks@%
+              \noexpand\do{#1}{#2}%
+              \noexpand\do{##1}{##2}%
+            }%
+          }%
+          \x
+          \def\do####1####2{%
+            \toks@\expandafter{\the\toks@\do{####1}{####2}}%
+          }%
+          \def\stop{%
+            \xdef\EmFi@list{\the\toks@}%
+          }%
+        \else
+          \toks@\expandafter{\the\toks@\do{##1}{##2}}%
+        \fi
+      }%
+      \def\stop{%
+        \xdef\EmFi@list{\the\toks@\noexpand\do{#1}{#2}}%
+      }%
+      \toks@{}%
+      \EmFi@list\stop
+    \fi
+  \endgroup
+}
+\def\embedfilefinish{%
+  \ifEmFi@finished
+    \EmFi@Error{%
+      Too many invocations of \string\embedfilefinish
+    }{%
+      The list of embedded files is already written.%
+    }%
+  \else
+    \ifx\EmFi@list\ltx@empty
+    \else
+      \global\EmFi@finishedtrue
+      \begingroup
+        \def\do##1##2{%
+          (##1)##2%
+        }%
+        \immediate\pdfobj{%
+          <<%
+            /Names[\EmFi@list]%
+          >>%
+        }%
+        \pdfnames{%
+          /EmbeddedFiles \the\pdflastobj\ltx@space 0 R%
+        }%
+      \endgroup
+      \begingroup
+        \def\do##1##2{%
+          \ltx@space##2%
+        }%
+        \immediate\pdfobj{%
+          [\EmFi@list]%
+        }%
+        \pdfcatalog{%
+          /AF \the\pdflastobj\ltx@space 0 R%
+        }%
+      \endgroup
+      \ifx\EmFi@initialfile\ltx@empty
+      \else
+        \EmFi@collectiontrue
+      \fi
+      \ifEmFi@collection
+        \ifx\EmFi@initialfile\ltx@empty
+          \let\EmFi@@initialfile\ltx@empty
+        \else
+          \edef\EmFi@@initialfile{%
+            \pdf@escapestring{\EmFi@initialfile}%
+          }%
+        \fi
+        \begingroup
+          \let\f=N%
+          \def\do##1##2{%
+            \def\x{##1}%
+            \ifx\x\EmFi@@initialfile
+              \let\f=Y%
+              \let\do\ltx@gobbletwo
+            \fi
+          }%
+          \EmFi@list
+        \expandafter\endgroup
+        \ifx\f Y%
+        \else
+          \@PackageWarningNoLine{embedfile}{%
+            Missing initial file `\EmFi@initialfile'\MessageBreak
+            among the embedded files%
+          }%
+          \let\EmFi@initialfile\ltx@empty
+          \let\EmFi@@initialfile\ltx@empty
+        \fi
+        \ifcase\EmFi@sortcase
+          \def\EmFi@temp{}%
+        \or
+          \def\EmFi@temp{%
+            /S\EmFi@sortkeys
+            /A \EmFi@sortorders
+          }%
+        \else
+          \def\EmFi@temp{%
+            /S[\EmFi@sortkeys]%
+            /A[\EmFi@sortorders]%
+          }%
+        \fi
+        \def\EmFi@@order##1{%
+          \ifnum\EmFi@order>1 %
+            /O ##1%
+          \fi
+        }%
+        \immediate\pdfobj{%
+          <<%
+            \ifx\EmFi@schema\ltx@empty
+            \else
+              /Schema<<\EmFi@schema>>%
+            \fi
+            \ifx\EmFi@@initialfile\ltx@empty
+            \else
+              /D(\EmFi@@initialfile)%
+            \fi
+            \ifx\EmFi@view\EmFi@S@tile
+              /View/T%
+            \else\ifx\EmFi@view\EmFi@S@hidden
+              /View/H%
+            \fi\fi
+            \ifx\EmFi@temp\ltx@empty
+              \EmFi@temp
+            \else
+              /Sort<<\EmFi@temp>>%
+            \fi
+          >>%
+        }%
+        \pdfcatalog{%
+          /Collection \the\pdflastobj\ltx@space0 R%
+        }%
+      \fi
+    \fi
+  \fi
+}
+\begingroup\expandafter\expandafter\expandafter\endgroup
+\expandafter\ifx\csname AtEndDocument\endcsname\relax
+\else
+  \AtEndDocument{\embedfilefinish}%
+\fi
+\EmFi@AtEnd%
+\endinput
+%%
+%% End of file `embedfile.sty'.
diff --git a/users/gdpdu-01-08-2002.dtd b/users/gdpdu-01-08-2002.dtd
new file mode 100644 (file)
index 0000000..68b7bc5
--- /dev/null
@@ -0,0 +1,189 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<!--Versions available:\r
+1.1 (August-01-2002)\r
+-->\r
+\r
+<!-- Start Simple Types -->\r
+\r
+<!-- Supplementary Vocabulary -->\r
+<!ELEMENT Version (#PCDATA)>\r
+<!ELEMENT Location (#PCDATA)>\r
+<!ELEMENT Comment (#PCDATA)>\r
+<!ELEMENT Length (#PCDATA)>\r
+<!ELEMENT References (#PCDATA)>\r
+<!ELEMENT From (#PCDATA)>\r
+<!ELEMENT To (#PCDATA)>\r
+\r
+<!-- Specifying a maximum length for a VariableLength column can\r
+     reduce a VariableLength tables' import time. If MaxLength\r
+        is not specified then we parse URL to determine the MaxLength\r
+        for each column.\r
+       \r
+        * Only applies to VariableLength tables. -->\r
+<!ELEMENT MaxLength (#PCDATA)>\r
+\r
+<!-- Specifies which character (if any) encapsulates a\r
+     VariableLength AlphaNumeric column.\r
+       \r
+        Doublequote is the default TextEncapsulator "\r
+       \r
+        * Only applies to VariableLength tables. (Optional) -->\r
+<!ELEMENT TextEncapsulator (#PCDATA)>\r
+\r
+<!-- Specifies how many        digits appear to the right of the decimal symbol.\r
+\r
+        CAUTION: Results are undefined when importing numeric data with\r
+                 greater Accuracy than the Accuracy defined in index.xml                       \r
+                       \r
+                         For example trying to import the value 1000,25 with an\r
+                         accuracy of 0 might result in 1000 or an error. This\r
+                         behavior is specific to the implementation.\r
+       \r
+        Zero is the default Accuracy '0' (Optional)            \r
+-->\r
+<!ELEMENT Accuracy (#PCDATA)>\r
+\r
+<!-- The decimal place is not always stored with numbers. If each number\r
+     is supposed to have decimal places use ImpliedAccuracy -->\r
+<!ELEMENT ImpliedAccuracy (#PCDATA)>\r
+\r
+<!-- Enables you to change how GDPdU displays dates.\r
+        DD.MM.YYYY is the default Format -->\r
+<!ELEMENT Format (#PCDATA)>\r
+\r
+<!-- Specifies the symbol that indicates decimal values.\r
+     Comma is the default DecimalSymbol. ','\r
+        Specified once per Table. -->\r
+<!ELEMENT DecimalSymbol (#PCDATA)>\r
+\r
+<!-- Specifies the symbol that groups the digits in large numbers.\r
+     Dot is the default DigitGroupingSymbol or ThousandsSeperator. '.'\r
+        Specified once per Table -->\r
+<!ELEMENT DigitGroupingSymbol (#PCDATA)>\r
+\r
+<!-- Command(s) are executed in the following manner\r
+      * before the import process\r
+         * after the import process\r
+         * before a Media is imported\r
+         * after a Media is imported\r
+-->\r
+<!ELEMENT Command (#PCDATA)>\r
+\r
+<!-- Only the file protocol is supported at this time.\r
+\r
+     * The standard uses relative URLs.\r
+               \r
+        Absolute URLs are not allowed. The following are all invalid:\r
+        * http://www.somewhere.com/data/Accounts.dat\r
+        * ftp://ftp.somewhere.com/data/Accounts.dat\r
+        * file://localhost/Accounts.dat\r
+     * file:///Accounts.dat\r
+       \r
+        The following are valid examples\r
+         * Accounts.dat        \r
+      * data/Accounts.dat\r
+      * data/january/Accounts.dat\r
+      * ../Accounts.dat\r
+-->\r
+<!ELEMENT URL (#PCDATA)>\r
+\r
+<!-- Textual description of specified element (Optional) -->\r
+<!ELEMENT Description (#PCDATA)>\r
+\r
+<!-- The logical name of specified element.\r
+     Sometimes referred to business name.\r
+       \r
+        If missing, URL will be used in place of Name. -->\r
+<!ELEMENT Name (#PCDATA)>\r
+\r
+<!-- Y2K Window Any year before Epoch is 2000+\r
+     Default value 30.  -->\r
+<!ELEMENT Epoch (#PCDATA)>\r
+\r
+<!-- Element(s) that separate columns or records.\r
+     Semicolon is the default ColumnDelimiter. ';'\r
+        CRLF or &#13;&#10; is the default RecordDelimiter. -->\r
+<!ELEMENT ColumnDelimiter (#PCDATA)>\r
+<!ELEMENT RecordDelimiter (#PCDATA)>\r
+\r
+<!-- The number of bytes skipped before reading of URL commences.\r
+     Zero is the default when not specified. '0'\r
+-->\r
+<!ELEMENT SkipNumBytes (#PCDATA)>\r
+\r
+<!-- End Simple Types -->\r
+<!-- Start Complex Types -->\r
+<!-- Self-explanatory -->\r
+<!ELEMENT Range (From, (To | Length)?)>\r
+<!ELEMENT FixedRange (From, (To | Length))>\r
+\r
+<!-- The document element -->\r
+<!ELEMENT DataSet (Version, DataSupplier?, Command*, Media+, Command*)>\r
+\r
+<!-- Supported datatypes (mandatory) -->\r
+<!ELEMENT AlphaNumeric EMPTY>\r
+<!ELEMENT Date (Format?)>\r
+<!ELEMENT Numeric ((ImpliedAccuracy | Accuracy)?)>\r
+\r
+<!-- Supported codepages:\r
+     Be careful to explicitly define RecordDelimiter when using\r
+        a non-default codepage.\r
+\r
+     ANSI is the default codepage when not specified -->\r
+<!ELEMENT ANSI EMPTY>\r
+<!ELEMENT Macintosh EMPTY>\r
+<!ELEMENT OEM EMPTY>\r
+<!ELEMENT UTF16 EMPTY>\r
+<!ELEMENT UTF7 EMPTY>\r
+<!ELEMENT UTF8 EMPTY>\r
+\r
+<!-- Supported file formats:\r
+     FixedLength\r
+        VariableLength -->\r
+<!ELEMENT FixedLength ((Length | RecordDelimiter)?, ((FixedPrimaryKey+, FixedColumn*) | (FixedColumn+)), ForeignKey*)>\r
+<!ELEMENT FixedColumn (Name, Description?, (Numeric | AlphaNumeric | Date), Map*, FixedRange)>\r
+<!ELEMENT FixedPrimaryKey (Name, Description?, (Numeric | AlphaNumeric | Date), Map*, FixedRange)>\r
+<!ELEMENT VariableLength (ColumnDelimiter?, RecordDelimiter?, TextEncapsulator?, ((VariablePrimaryKey+, VariableColumn*) | (VariableColumn+)), ForeignKey*)>\r
+<!ELEMENT VariableColumn (Name, Description?, (Numeric | (AlphaNumeric, MaxLength?) | Date), Map*)>\r
+<!ELEMENT VariablePrimaryKey (Name, Description?, (Numeric | (AlphaNumeric, MaxLength?) | Date), Map*)>\r
+\r
+<!-- Description of the entity supplying the data. (Optional) -->\r
+<!ELEMENT DataSupplier (Name, Location, Comment)>\r
+\r
+<!-- The first Media will contain index.xml. Importing will process each media listed -->\r
+<!ELEMENT Media (Name, Command*, Table+, Command*)>\r
+\r
+<!-- Elements common to FixedLength & VariableLength are propagated to Table. -->\r
+<!ELEMENT Table (URL, Name?, Description?, Validity?, (ANSI | Macintosh | OEM | UTF16 | UTF7 | UTF8)?, (DecimalSymbol, DigitGroupingSymbol)?, SkipNumBytes?, Range?, Epoch?, (VariableLength | FixedLength))>\r
+\r
+<!-- ForeignKeys denote joins or relationships between tables.\r
+     To successfully join two tables make sure both the PrimaryKey\r
+        and the referenced column (foreignkey) are of the same datatype.\r
+        Results are undefined when joining two tables with different\r
+        key datatypes. Most likely an error will occur. -->\r
+<!ELEMENT ForeignKey (Name+, References)>\r
+\r
+<!-- Maps AlphaNumeric columns from 'From' to 'To'\r
+     ie. From         To\r
+            ============ =============\r
+                True         1\r
+                True         -1\r
+                False        0\r
+               \r
+        Basically, a map is an associative container.          \r
+       \r
+        The standard implementation only supports\r
+        AlphaNumeric datatypes. The following\r
+        conversions are NOT supported.\r
+       \r
+        Numeric      -> AlphaNumeric\r
+        Date         -> AlphaNumeric\r
+        AplhaNumeric -> Date\r
+        AlphaNumeric -> Numeric                        \r
+-->\r
+<!ELEMENT Map (Description?, From, To)>\r
+\r
+<!-- Documentation for table validity. -->\r
+<!ELEMENT Validity (Range, Format?)>\r
+\r
+<!-- End Complex Types -->\r